技術約7分で読めます

Qwen3-EmbeddingをローカルとAPIで比べた

いけさん目次

前回の記事では、開発機(i7-13700H、RTX 4060 Laptop 8GB、RAM32GB)でQwen3-Embedding-0.6BのCPU実行とQdrantのローカルモードを測った
今回は本番機(AMD Ryzen 7 5800HS、RTX 3050 Ti 4GB、RAM16GB)で同じ計測をやり直し、ModelScopeアンバサダー枠のAPI経由でQwen3-Embedding-0.6Bを叩いた場合とも比べた。さらに音声認識のQwen3-ASRと音声合成のIrodori-TTSが常駐した状態で埋め込みを同時に動かした。

本文に書いた方法論(RSS(実際に使用中の物理メモリ量)差分でのメモリ測定、truncate_dim=256、137文字の日本語テキスト、QdrantClient(path=...)のローカルモード、query_points())からベンチスクリプトを再構成して使った。
SQLite部分だけは前回記事のsource_turn_ids経由の復元ではなく、直近ターンをまとめて読み出すだけの簡略版にしている。

検証環境

項目開発機(前回記事)本番機(今回実測)
OSWindows 11Windows 11 Home (10.0.26200)
CPUi7-13700H(14コア/20スレッド)AMD Ryzen 7 5800HS(8コア/16スレッド)
GPURTX 4060 Laptop 8GBRTX 3050 Ti Laptop 4GB(今回は不使用、CPU実行)
RAM32GB15.4GB
Python3.14.03.12.10
torch2.9.1+cpu2.14.0+cpu
sentence-transformers6.0.16.0.1(一致)
qdrant-client1.19.01.19.0(一致)

計測開始時点の空きRAMは6.3GBで、前回記事に記録した約6.5GBとほぼ同じだった。

準備でこけた2点

モデルダウンロードがOSError: [WinError 1314] クライアントは要求された特権を保有していませんで失敗した。
huggingface_hubがシンボリックリンクを作ろうとするが、開発者モードOFFのWindowsではその権限がない。HF_HUB_DISABLE_SYMLINKS=1を設定して解決した(ディスク使用は少し増えるが動作は同じ)。

qdrant_client.__version__が存在せずAttributeErrorになった。importlib.metadata.version("qdrant-client")に変更した。

ローカルCPU実行を本番機で再現する

測定項目開発機(i7-13700H)本番機(5800HS)判定
モデルロード後RSS529.1 MB482.2 MBほぼ一致
初回推論後RSS1,386.5 MB1,342.2 MBほぼ一致
100件バッチ後RSS1,695.0 MB1,381.2 MBやや少ない
ウォームアップ後1件1.114 s2.10 s(3回平均)約1.9倍遅い
100件バッチ(1件あたり)1.1789 s1.912 s(計191.2 s)約1.6倍遅い
Qdrant類似検索0.00698 s0.00041 s(ウォーム平均)どちらも10ミリ秒未満
Qdrantディスク(100件)0.99 MB0.41 MB(256次元)
SQLite復元0.00054 s0.00156 sどちらも2ミリ秒未満

モデルロードは6.56秒、初回推論は2.10秒だった。
※ ディスクが前回記事より小さいのは次元数の違いによる(後述のAPI版は1024次元で条件が異なる)。

この実機でも前回記事の内容は最後まで動いた。メモリフットプリント(約1.4GB)は前回記事とほぼ同じだが、埋め込み速度は1.6〜1.9倍遅い。16GB機では、埋め込みだけで常時1.4GBを使う。

ModelScope API経由で同じ処理を測る

Qwen/Qwen3-Embedding-0.6Bapi-inference.modelscope.ai/v1/embeddingsで叩いた。事前の疎通確認でチャット(Qwen-Ambassador/Qwen3.7-Plus)も約2秒で通ることを確かめている。

測定項目ローカルCPUAPI経由
1件埋め込み2.10 s0.83 s(5回平均、初回0.81 s)2.5倍速い
100件バッチ(1件あたり)1.912 s0.078 s(1リクエスト一括、計7.8 s)約24倍速い
クライアントRSS(全工程後)1,413 MB96.9 MB約1/15
Qdrant検索(100件、1024次元)0.00165 s
検索E2E(クエリ埋め込み+検索)約2.1 s1.20 sAPI側が速い
Qdrantディスク(100件、1024次元)1.0 MB前回記事の0.99 MBと一致

dimensions: 256パラメータもサーバー側で使え、指定するとQdrantの容量を1/4にできる。
100件も分割せず1リクエストで送れた。encoding_format: "float"の指定は必須で、省略すると400が返る。
コストはMagicube記事の基準で埋め込み1回=1コイン、このベンチ全体で埋め込み9回+チャット疎通2回。アンバサダーの月次上限1万回に対しては0.1%程度だった。

TTS常駐下での同時実行

この本番機は、STT・LLM・TTSをひとまとめにした音声対話サーバーを常時稼働させている機体で、Irodori-TTSによる音声合成もそこで動いている。以下ではこのサーバーをVoiceChatサーバーと呼ぶ。
そのVoiceChatサーバーが常駐した実運用状態(開始時空きRAM 6.1GB)で、TTSに合成リクエストを連続で投げながら埋め込みを実行した。「Qwenをローカルに置く構成」と「APIに回す構成」の両方を測った。

フェーズ埋め込み1件埋め込み20件/件TTS合成空きRAM
A: TTS単独(基準)2.5 s(初回4.6 s)6.1 GB
B: ローカル埋め込み単独(TTSアイドル)1.84 s1.76 s4.6 GB
C: TTS合成中 + ローカル埋め込み2.64 s(+44%)2.22 s(+27%)3.4 s(+35%悪化)4.7 GB
D: TTS合成中 + API埋め込み0.80 s(変化なし)0.075 s2.5 s(劣化なし)5.6 GB

TTS推論はGPUでも前後処理はCPUで、ローカル埋め込みとCPUで競合する。だから埋め込みだけでなくTTS合成自体も+35%遅くなる(C)。API構成はネットワークI/O待ちだけでCPUを使わないので、TTS合成中でも単独時と数値が変わらない(D)。

RAMはローカルモデルの常駐で空きが6.1→4.6GBに減り(約1.5GB消費)、アンロードで5.6GBまで回復した。空き6GB前後の構成にこの1.5GBが常時足されると、後述のフルパイプライン計測のように空きは2GBを切る水準まで落ち込む。

VoiceChatサーバーにはQwen3-ASR-0.6B(CPU)もロード済み・常駐しているが、この計測中はアイドルだった(STT推論は流していない)。
このときのコミットメモリ(OSが確保を約束した仮想メモリ量)はTTS 8.8GB+VoiceChat 5.6GBと大きく、動いていない分はページアウト(使わないメモリをディスクへ退避する処理)されたままだった。

ASR→LLM→TTSのフルパイプラインとの同時実行

VoiceChatサーバーの音声対話処理(音声→Qwen3-ASR(CPU)→ModelScope LLM→Irodori-TTS→音声)をループで回しながら埋め込みを実行した。
入力音声はTTSで生成したWAVを使うループバック方式で、ASRのCPU推論も含めた構成になる。

基準(音声対話単独、ウォーム)は9.3秒で、内訳はSTT 2.3秒・LLM初トークン1.5秒・TTS待ち5.2秒だった。初回だけは31.6秒かかり、内訳はSTTだけで16.8秒だった。

構成音声E2E埋め込み1件空きRAM
音声対話単独(ウォーム)9.3 s3.3 GB
+ ローカル埋め込み9.5〜11.9 s(平均10.9 s)2.31 s(+25%)1.73 GB
+ API埋め込み8.9〜9.8 s(変化なし)0.77 s(変化なし)2.66 GB

内訳はSTTが2.2〜2.9秒(平均2.7秒)、TTS待ちが5.0〜7.0秒(平均6.3秒)で、基準のSTT 2.3秒・TTS待ち5.2秒よりどちらも重くなった。LLM(ModelScope API)の初トークンは1.5秒前後で変化せず、CPU競合はSTT・TTSの前後処理にだけ表れる。API構成はネットワークI/O待ちだけなので、この内訳もろとも変化がなかった。

空きRAMはローカル構成で1.73GBまで下がった。ASRが実際にページインした状態に埋め込みモデル1.5GBが加わった。

アイドルでページアウトされたASRの初回呼び出しは31.6秒かかった。
原因はWindowsのワーキングセット・トリム(アイドル時にOSがメモリ占有を切り詰める処理)で、一晩アイドル後のVoiceChatプロセスはコミット5.6GBに対しワーキングセットが10MBまで縮んでおり、STTを叩いた後は2,614MBに膨張していた(=16.8秒はページファイルからの読み戻し)。
モデルやサーバーの問題ではなく、この実機に常駐するプロセスのコミット合計が16GBに対して過大なために起きていた。フルパイプライン計測中に測り直すと、TTS 10.3GB+VoiceChat 5.1GBだった(前節のTTS常駐単体時点とは別の計測)。
対策は数分おきにダミーでSTTを呼んでおく(keep-warm)か、コミットメモリの総量そのものを減らすかになりそうだ。後者は埋め込みのAPI化なら常駐モデル分の約1.5GBがそのまま減り、ASRのfp16/int8化も候補になる。

本番機の構成をどうするか

前回記事に残していた未確認4項目は、今回の計測で確認できた。

前回記事の確認項目結果
空きRAMは記録の約6.5GBから変わっていないか6.3GBでほぼ同じ
RAM増分と処理時間の実測約1.4GB、1件あたり1.6〜1.9倍遅い(開発機比)
Qdrantローカルモードの登録・検索速度どちらも数ミリ秒以下
STT・TTSと同時に動かしてOOM(メモリ不足によるプロセス強制終了)や速度低下が起きないかOOMはないが、音声応答が毎ターン+1.5〜2.5秒遅くなり空きRAMは1.73GBまで枯渇した

音声パイプラインの長期記憶は、埋め込みをアンバサダー枠のAPI(dimensions: 256指定)に出し、実機にはQdrantローカルモード(RSS増分は数十MB、検索は2ミリ秒未満)だけを置く構成にする。
ローカルCPU実行はオフライン時のフォールバックとして残すが、常駐はさせない。

前回と今回の実測で、埋め込みとQdrantの組み合わせとしては動かせそうなところまで来た。
次にやるのは、この長期記憶をCoreS3から非同期で呼び出しているところへ実際に足すか、その前にサーボで動くStackChan BodyへCoreS3を組み込むか。手元のCoreS3は音声チャットと口パクまでは動いているが、StackChan Bodyにはまだ入れていない。