約6万件の日本語テキストをベクトル化をGPUとCPU処理で比較してみた
約6万件の日本語テキストのベクトル化を試みる機会がありました。
今回、2つの日本語対応モデル(multilingual-e5-large と ruri-base)を使い、CPU(c7i.xlarge)とGPU(g4dn.xlarge / g6.xlarge)のEC2インスタンス上でDocker環境を構築、その性能を比較した結果を紹介します。
実行環境とデータ
検証に使用したリソースおよび入力データは以下の通りです。
インスタンス構成
GPU環境 (前世代):
- インスタンス: g4dn.xlarge (4 vCPU / 16 GiB / カスタム Intel Cascade Lake / NVIDIA Tesla T4)
- AMI: Deep Learning OSS Nvidia Driver AMI GPU PyTorch 2.9 (Amazon Linux 2023) 20260128 (ami-00cef3bb12d4413ab)
GPU環境 (現行世代):
- インスタンス: g6.xlarge (4 vCPU / 16 GiB / NVIDIA L4)
- AMI: Deep Learning OSS Nvidia Driver AMI GPU PyTorch 2.9 (Amazon Linux 2023) 20260214 (ami-072dabda4a812fd06)
CPU環境:
- インスタンス: c7i.xlarge (4 vCPU / 8 GiB / 第4世代 Intel Xeon Scalable - Sapphire Rapids)
- AMI: Amazon Linux 2023 AMI 2023.10.20260120.4 (ami-055a9df0c8c9f681c)
入力データ
初回検証 (g4dn / c7i):
- 内容: 記事要約JSON(タイトル、要約、ID)
- レコード数: 59,768 件
- ファイル容量: 約32.0 MB
- 平均サイズ: 約560 bytes / 件
追加検証 (g6):
- 内容: 記事要約JSONL.gz(タイトル、要約、ID)
- レコード数: 117,009 件(日本語 56,955件 + 英語 60,054件)
- ファイル容量: 約86.2 MB(展開後)
- 平均サイズ: 約231文字 / 件
- バッチエンコード(batch_size=256)を使用
性能比較結果
multilingual-e5-large モデル
多言語対応の高性能モデル(1024次元、391Mパラメータ)での比較結果:
| 項目 | CPU (c7i.xlarge) | GPU (g4dn.xlarge) | GPU (g6.xlarge) |
|---|---|---|---|
| 処理時間 | 約7時間 (推定) | 27分51秒 | 約25分 ※1 |
| スループット | 143 記事/分 | 2,145 記事/分 | 約4,680 記事/分 ※1 |
| コスト | $1.25 (7h × $0.178) | $0.24 (0.46h × $0.526) | $0.33 (0.42h × $0.80) ※1 |
- CPU版は1,000記事処理時点でのスループット(143記事/分)から全体の処理時間を推定
- 単価: c7i.xlarge $0.178/h、g4dn.xlarge $0.526/h、g6.xlarge $0.80/h(us-west-2 オンデマンド)
- ※1 g6.xlargeはバッチエンコード(batch_size=256)使用、117,009件での計測値。S3オブジェクト作成時刻からの推定
ruri-base モデル
日本語特化の軽量モデル(768次元、199Mパラメータ)での比較結果:
| 項目 | CPU (c7i.xlarge) | GPU (g4dn.xlarge) | GPU (g6.xlarge) |
|---|---|---|---|
| 処理時間 | 38分19秒 | 21分43秒 | 約5分 ※1 |
| スループット | 1,560 記事/分 | 2,752 記事/分 | 約23,400 記事/分 ※1 |
| コスト | $0.11 (0.64h × $0.178) | $0.19 (0.36h × $0.526) | $0.07 (0.08h × $0.80) ※1 |
コストと所要時間の比較
| モデル | 環境 | 処理時間 | コスト |
|---|---|---|---|
| ruri-base | CPU (c7i.xlarge) | 38分19秒 | $0.11 |
| ruri-base | GPU (g4dn.xlarge) | 21分43秒 | $0.19 |
| ruri-base | GPU (g6.xlarge) ※1 | 約5分 | $0.07 |
| multilingual-e5-large | CPU (c7i.xlarge) | 約7時間 | $1.25 |
| multilingual-e5-large | GPU (g4dn.xlarge) | 27分51秒 | $0.24 |
| multilingual-e5-large | GPU (g6.xlarge) ※1 | 約25分 | $0.33 |
参考: モデルロード時間
- ruri-base: 6.8秒
- multilingual-e5-large: 58.4秒(約8.6倍の差)
※Dockerのキャッシュを利用して、メモリへの展開時間のみ計測しました(CPU環境)。処理時間には含まれていません。
g4dn vs g6 の比較
重要: g4dn.xlargeの計測は1件ずつの逐次処理、g6.xlargeの計測はバッチエンコード(batch_size=256)を使用しています。以下のスループット差はハードウェア性能の差ではなく、主にバッチ処理の最適化効果によるものです。g4dn.xlargeでもバッチエンコードを適用すれば、同等のスループット向上が期待できます。
| 項目 | g4dn.xlarge (T4) 逐次処理 | g6.xlarge (L4) バッチ処理 | 備考 |
|---|---|---|---|
| GPU | NVIDIA Tesla T4 (16GB) | NVIDIA L4 (24GB) | |
| 料金 | $0.526/h | $0.80/h | |
| e5-large スループット | 2,145 記事/分 | 約4,680 記事/分 | 実装条件が異なる |
| ruri-base スループット | 2,752 記事/分 | 約23,400 記事/分 | 実装条件が異なる |
| ハードウェア性能差(推定) | 1.0x | 1.5x〜2.0x | 同一コードで計測した場合 |
スループット差の要因を分解すると:
- バッチエンコードの効果: 逐次処理→バッチ処理で、特に軽量モデルでは数倍〜10倍程度の高速化が見込める
- ハードウェアの性能差: L4はT4と比較してFP16性能が約2倍。同一実装で比較した場合、1.5〜2.0倍程度の差と推定
まとめ
今回の検証により、利用するモデルに合わせた適切なインスタンスの選定の重要性が確認できました。
当面は費用対効果を優先し、通常のFargateなどの実行環境での利用を予定しているため、CPU処理で実用的な性能を発揮できた ruri-base をベクトル化のモデルとして採用予定です。
今後の多言語対応や、よりベクトル精度の高いモデルとして multilingual-e5-large を利用することになった場合、GPU対応インスタンスや実行環境を活用したいと思います。
また、追加検証により以下の知見が得られました:
- バッチエンコードの効果は絶大:
model.encode()にリスト渡し +batch_size指定することで、1件ずつの逐次処理と比較して大幅なスループット向上が得られる。特に軽量モデルほど効果が大きい - 現行世代GPU (g6/L4) の優位性: g4dn (T4) と比較してVRAMが24GBに増加し、バッチサイズを大きく取れるため、バッチエンコードとの相性が良い
- ruri-base + g6 + バッチエンコードの組み合わせ: 117,009件を約5分・$0.07で処理でき、コスト効率が最も高い
参考情報
今回の検証に利用したDockerとPythonコードです。
AWS公式が提供するDeep Learning AMI(Nvidia Driver版)を利用することで、特別なドライバ設定やCUDAのインストール作業なしに、Docker環境でGPUを活用できました。--gpus all フラグを指定するだけで、コンテナ内からGPUリソースにアクセス可能です。
また、sentence-transformers ライブラリはGPU/CPUを自動判定するため、同一のPythonコードでCPU環境とGPU環境の両方で動作します。環境の違いはDockerfileのベースイメージと実行時の --gpus フラグのみで吸収できるため、開発・検証がスムーズに行えました。
multilingual-e5-large の実装
Dockerfile の比較
GPU版 (g4dn.xlarge用):
# CUDA対応のPyTorchイメージ
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ=Asia/Tokyo
WORKDIR /app
RUN pip install --no-cache-dir sentence-transformers boto3
COPY vectorize-simple.py .
CMD ["python", "-u", "vectorize-simple.py"]
CPU版 (c7i.xlarge用):
# CPU専用のPyTorchイメージ
FROM pytorch/pytorch:2.5.1-cpu
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ=Asia/Tokyo
WORKDIR /app
RUN pip install --no-cache-dir sentence-transformers boto3
COPY vectorize-simple.py .
CMD ["python", "-u", "vectorize-simple.py"]
注: 今回の検証ではCUDAイメージを使用しましたが、CPU専用イメージ(
pytorch:2.5.1-cpu)を使用することで、Intel MKLなどのCPU最適化により、さらに性能が向上する可能性があります。
Pythonコード (CPU/GPU共通) — 逐次処理版
sentence-transformers はCUDAを自動検出するため、コード側での特別な処理は不要でした。
import json
import os
from datetime import datetime
from sentence_transformers import SentenceTransformer
import boto3
# 環境変数から設定を取得
bucket = os.environ['S3_BUCKET']
input_key = os.environ['INPUT_KEY']
output_key = os.environ['OUTPUT_KEY']
s3 = boto3.client('s3')
# S3から入力ファイルをダウンロード
print('Downloading input file...')
s3.download_file(bucket, input_key, '/tmp/articles_ja.json')
# モデルロード(GPUがあれば自動的に使用される)
model_name = os.environ.get('MODEL_NAME', 'intfloat/multilingual-e5-large')
print(f'Loading model: {model_name}...')
model = SentenceTransformer(model_name)
# 記事データ読み込み
print('Loading articles...')
with open('/tmp/articles_ja.json', 'r') as f:
articles = json.load(f)
print(f'Processing {len(articles)} articles...')
# ベクトル化処理(1件ずつ処理、100件ごとに進捗表示)
with open('/tmp/vectors_ja.jsonl', 'w') as f:
for i, article in enumerate(articles):
text = f"{article['title']} {article['summary']}"
vector = model.encode(text).tolist()
result = {
'article_id': article['id'],
'vector': vector,
'dimension': len(vector),
'model': model_name,
'timestamp': datetime.utcnow().isoformat()
}
f.write(json.dumps(result, ensure_ascii=False) + '\n')
if (i + 1) % 100 == 0:
print(f'Processed {i + 1}/{len(articles)} articles')
# S3にアップロード
print('Uploading results...')
s3.upload_file('/tmp/vectors_ja.jsonl', bucket, output_key)
print('Done!')
ポイント:
SentenceTransformerが自動的にGPU/CPUを判定- デバイス指定コード不要
- 同じコードでCPU/GPU両環境で動作
- 100件ごとに進捗ログを出力してスループット測定を可能に
Pythonコード (GPU最適化) — バッチエンコード版
g6.xlarge での追加検証で使用したバッチエンコード版です。model.encode() にテキストのリストを渡すことで、GPU並列処理の恩恵を最大限に活用できます。
import json, gzip, os, time
from sentence_transformers import SentenceTransformer
import boto3
BUCKET = os.environ["S3_BUCKET"]
INPUT_KEY = os.environ["INPUT_KEY"]
OUTPUT_KEY = os.environ["OUTPUT_KEY"]
MODEL_NAME = os.environ.get("MODEL_NAME", "intfloat/multilingual-e5-large")
BATCH_SIZE = int(os.environ.get("BATCH_SIZE", "256"))
PREFIX = os.environ.get("TEXT_PREFIX", "")
s3 = boto3.client("s3")
print(f"Downloading s3://{BUCKET}/{INPUT_KEY}")
s3.download_file(BUCKET, INPUT_KEY, "/tmp/input.jsonl.gz")
print(f"Loading model: {MODEL_NAME}")
model = SentenceTransformer(MODEL_NAME)
print("Reading input...")
records = []
with gzip.open("/tmp/input.jsonl.gz", "rt") as f:
for line in f:
d = json.loads(line)
rid = d["recordId"]
text = d["modelInput"]["singleEmbeddingParams"]["text"]["value"]
records.append((rid, PREFIX + text))
print(f"Processing {len(records)} records with batch_size={BATCH_SIZE}")
start = time.time()
texts = [r[1] for r in records]
vectors = model.encode(texts, batch_size=BATCH_SIZE, show_progress_bar=True)
elapsed = time.time() - start
rate = len(records) / (elapsed / 60)
print(f"Encoding done: {elapsed:.1f}s ({rate:.0f} records/min)")
print("Writing output...")
with open("/tmp/output.jsonl", "w") as f:
for i, (rid, _) in enumerate(records):
out = {"recordId": rid, "vector": vectors[i].tolist(), "dimension": len(vectors[i]), "model": MODEL_NAME}
f.write(json.dumps(out, ensure_ascii=False) + "\n")
print(f"Uploading to s3://{BUCKET}/{OUTPUT_KEY}")
s3.upload_file("/tmp/output.jsonl", BUCKET, OUTPUT_KEY)
print(f"Done! {len(records)} records, {elapsed:.1f}s, {rate:.0f} records/min")
逐次処理版との主な違い:
model.encode()にテキストのリストを一括渡し(バッチエンコード)batch_size=256でGPUメモリを効率的に活用- gzip圧縮入力に対応
- 処理時間・スループットの自動計測
注(大規模データへのスケール時): 本コードは全テキストをリストとしてメモリに展開しています。
model.encode()は内部でbatch_size単位に分割してGPUに送るためGPU OOMのリスクは低いですが、テキストリスト自体のCPUメモリ消費には注意が必要です。数百万件規模のデータを扱う場合は、入力ファイルを分割するか、チャンク単位で読み込み・エンコード・書き出しを行う実装を検討してください。
実行コマンド
GPU版 (g4dn.xlarge):
docker run --gpus all --rm \
-e S3_BUCKET=your-bucket-name \
-e INPUT_KEY=articles_ja.json \
-e OUTPUT_KEY=vectors_ja_gpu_${TIMESTAMP}.jsonl \
vectorize-gpu
CPU版 (c7i.xlarge):
docker run --rm \
-e S3_BUCKET=your-bucket-name \
-e INPUT_KEY=articles_ja.json \
-e OUTPUT_KEY=vectors_ja_cpu_${TIMESTAMP}.jsonl \
vectorize-cpu
差分: GPU版のみ --gpus all フラグが必要です。
ruri-base の実装
日本語特化モデル ruri-base を使用する場合、日本語形態素解析のための追加パッケージが必要でした。
Dockerfile
GPU版:
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime
# 日本語形態素解析用パッケージを追加
RUN pip install --no-cache-dir sentence-transformers boto3 protobuf fugashi unidic-lite
COPY vectorize-ruri-base.py /app/vectorize-ruri-base.py
WORKDIR /app
CMD ["python", "vectorize-ruri-base.py"]
CPU版:
FROM pytorch/pytorch:2.5.1-cpu
# 日本語形態素解析用パッケージを追加
RUN pip install --no-cache-dir sentence-transformers boto3 protobuf fugashi unidic-lite
COPY vectorize-ruri-base.py /app/vectorize-ruri-base.py
WORKDIR /app
CMD ["python", "vectorize-ruri-base.py"]
追加パッケージ:
protobuf: モデル設定ファイルの読み込みfugashi: 日本語形態素解析器(MeCabのPythonバインディング)unidic-lite: 日本語辞書(軽量版、約45MB)
Pythonコード (CPU/GPU共通)
import json
import os
from datetime import datetime
from sentence_transformers import SentenceTransformer
import boto3
bucket = os.environ['S3_BUCKET']
input_key = os.environ['INPUT_KEY']
output_key = os.environ['OUTPUT_KEY']
s3 = boto3.client('s3')
print('Downloading input file...')
s3.download_file(bucket, input_key, '/tmp/articles_ja.json')
print('Loading model: cl-nagoya/ruri-base...')
model = SentenceTransformer('cl-nagoya/ruri-base')
print('Loading articles...')
with open('/tmp/articles_ja.json', 'r') as f:
articles = json.load(f)
print(f'Processing {len(articles)} articles...')
with open('/tmp/vectors_ja.jsonl', 'w') as f:
for i, article in enumerate(articles):
# ruriモデルは "文章: " プレフィックスを推奨
text = f"文章: {article['title']} {article['summary']}"
vector = model.encode(text).tolist()
result = {
'article_id': article['id'],
'vector': vector,
'dimension': len(vector),
'model': 'cl-nagoya/ruri-base',
'timestamp': datetime.utcnow().isoformat()
}
f.write(json.dumps(result, ensure_ascii=False) + '\n')
if (i + 1) % 100 == 0:
print(f'Processed {i + 1}/{len(articles)} articles')
print('Uploading results...')
s3.upload_file('/tmp/vectors_ja.jsonl', bucket, output_key)
print('Done!')
ポイント:
- ruriモデルは入力テキストに
"文章: "プレフィックスを付けることを推奨 - それ以外はmultilingual-e5-largeと同じ構造
実行コマンド
GPU版:
docker run --gpus all --rm \
-e S3_BUCKET=your-bucket-name \
-e INPUT_KEY=articles_ja.json \
-e OUTPUT_KEY=vectors_ja_ruri_gpu_${TIMESTAMP}.jsonl \
vectorize-ruri-gpu
CPU版:
docker run --rm \
-e S3_BUCKET=your-bucket-name \
-e INPUT_KEY=articles_ja.json \
-e OUTPUT_KEY=vectors_ja_ruri_cpu_${TIMESTAMP}.jsonl \
vectorize-ruri-cpu






