DistilBERTの推論速度がCPUとGPUでどれくらい変わるのか比較してみた
Google社が開発した自然言語処理モデルBERTですが、使い方次第では様々なタスクで高い精度を得られるものの、そのパラメータの多さゆえに推論にかなり時間がかかります。
そのためBERTを実運用しようとすると、処理時間がボトルネックになって頓挫する場合もあるのではと思います。
BERTを蒸留したDistilBERT(軽量版BERT)をさらに量子化することで、CPUでも高いパフォーマンスを得られるという記事もあるものの、ある程度長いテキストをBERTで処理する場合は非常にレスポンスに時間がかかります。
そこで、今回は運用コストはいったん度外視し、CPUではなくGPUだとどれくらいBERTの推論処理が速くなるのかを検証します。
なお、コードはColaboratoryでの実行を想定しています。
また、使用するモデルはバンダイナムコ社が公開しているDistilBERTです。
(単純にモデルサイズが軽量でダウンロードが楽という理由です。BERTを使ってもらっても構いません。)
DistilBERTのロード
こちらからDistilBERTをダウンロードし、任意のディレクトリに配置します。
以下のコードでは、Google Driveにマウントし、マイドライブにモデルを配置しています。
from transformers import (DistilBertTokenizer, DistilBertForMaskedLM, DistilBertConfig)
# DitilBERTロード
config = DistilBertConfig.from_json_file(f'{model_path}/config.json')
model_path = '/content/drive/My Drive/Colab Notebooks/DistilBERT-base-jp'
model = DistilBertForMaskedLM.from_pretrained(f'{model_path}/pytorch_model.bin', config=config)
tokenizer = DistilBertTokenizer(f'{model_path}/vocab.txt', do_lower_case=False, do_basic_tokenize=False)
# 推論モード
model.eval()
サンプルテキスト生成
推論速度を比較するため、ある程度大きなサイズの日本語の単語を生成します。
以下のコードでは505単語のリストを作成し、そのリストが10個あるリスト(2次元リスト)を作成しています。
こんなイメージです。
# 作成するリストのイメージ [ ['私', 'は', '毎日', '朝', 'に', '[MASK]', 'し', 'ます', 'でも', '明日', 'は', '雨', 'が', '降り', 'そう', 'なので', 'やめ', 'ます'], ['私', 'は', '毎日', '朝', 'に', '[MASK]', 'し', 'ます', 'でも', '明日', 'は', '雨', 'が', '降り', 'そう', 'なので', 'やめ', 'ます'], ['私', 'は', '毎日', '朝', 'に', '[MASK]', 'し', 'ます', 'でも', '明日', 'は', '雨', 'が', '降り', 'そう', 'なので', 'やめ', 'ます'], ... ['私', 'は', '毎日', '朝', 'に', '[MASK]', 'し', 'ます', 'でも', '明日', 'は', '雨', 'が', '降り', 'そう', 'なので', 'やめ', 'ます'], ]
txt = ['私', 'は', '毎日', '朝', 'に', '[MASK]', 'し', 'ます'] # 先頭
append_txt = ['でも', '明日', 'は', '雨', 'が', '降り', 'そう', 'なので', 'やめ', 'ます'] # 追記テキスト
txt.insert(0, '[CLS]')
txt.append('[SEP]')
# n回テキストを追記
n = 45
for i in range(n):
txt.extend(append_txt)
txt.append('[SEP]')
print(f'token size:{len(txt)}')
# バッチ化
txt_list = [txt for i in range(10)]
print(f'text size: {len(txt_list)}')
token size:505 text size: 10
CPUとGPUで推論速度比較
CPUとGPUで処理速度を比較します。
1文単位で推論する場合と、全文まとめてバッチ処理する場合に分けて検証します。
import time
def get_elapsed_time_for_bert(model, tokenizer, tokens, device, by_sentence):
"""
BERTの推論パフォーマンスを計測する
"""
def predict(model, tokens_tensor):
with torch.no_grad():
outputs = model(tokens_tensor)
predictions = outputs[0]
return predictions
start = time.time()
model.to(device)
ids = [tokenizer.convert_tokens_to_ids(token) for token in tokens]
if by_sentence:
for id in ids:
tokens_tensor = torch.tensor([id]).to(device)
predict(model, tokens_tensor)
else:
tokens_tensor = torch.tensor(ids).to(device)
predict(model, tokens_tensor)
elapsed_time = time.time() - start
print(f'on {device}: {elapsed_time}')
for by_sentence in [True, False]:
print(f'by_sentence: {by_sentence}')
for device in ['cpu', 'cuda:0']:
get_elapsed_time_for_bert(
model=distilbert_model_fine,
tokenizer=distilbert_tokenizer_fine,
tokens=txt_list,
device=device,
by_sentence=by_sentence
)
by_sentence: True on cpu: 18.735729455947876 on cuda:0: 0.3672614097595215 by_sentence: False on cpu: 18.155377864837646 on cuda:0: 0.08703923225402832
「by_sentence: True」は1文単位の処理、「by_sentence: False」は全文まとめたバッチ処理を意味しています。
処理単位にかかわらずGPUの方が圧倒的に処理速度が向上していることがわかります。
また、CPUでは1文単位の処理であろうとバッチ処理であろうと推論速度にほとんど差はありませんが、GPUではバッチ処理の方が格段に速くなっています。
これは、GPUがバッチ処理に秀でているということを考えれば当然の結果ではありますが。。
最後に
GPUとCPUを比較した結果、やはりGPUを使った方がBERTの推論処理が圧倒的に速くなることがわかりました。
ただ、だからと言って本番で運用する際に何でもかんでもGPUにすればいいというわけではありません。
GPUはCPUに比べて運用コストが高くなるためです。
実運用していくなかで、推論で入力するデータ量、許容できるレスポンスの時間などを考慮すればCPUでも十分なパフォーマンスを担保できることもあるでしょう。
- 前の記事
BPEでサブワード分割することでDistilBERTに未知語が入力されるのを防ぐ方法 2020.10.19
- 次の記事
『キーエンス~驚異的な業績を生み続ける経営哲学』を読んだ感想 2020.10.27