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経由の復元ではなく、直近ターンをまとめて読み出すだけの簡略版にしている。
検証環境
| 項目 | 開発機(前回記事) | 本番機(今回実測) |
|---|---|---|
| OS | Windows 11 | Windows 11 Home (10.0.26200) |
| CPU | i7-13700H(14コア/20スレッド) | AMD Ryzen 7 5800HS(8コア/16スレッド) |
| GPU | RTX 4060 Laptop 8GB | RTX 3050 Ti Laptop 4GB(今回は不使用、CPU実行) |
| RAM | 32GB | 15.4GB |
| Python | 3.14.0 | 3.12.10 |
| torch | 2.9.1+cpu | 2.14.0+cpu |
| sentence-transformers | 6.0.1 | 6.0.1(一致) |
| qdrant-client | 1.19.0 | 1.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) | 判定 |
|---|---|---|---|
| モデルロード後RSS | 529.1 MB | 482.2 MB | ほぼ一致 |
| 初回推論後RSS | 1,386.5 MB | 1,342.2 MB | ほぼ一致 |
| 100件バッチ後RSS | 1,695.0 MB | 1,381.2 MB | やや少ない |
| ウォームアップ後1件 | 1.114 s | 2.10 s(3回平均) | 約1.9倍遅い |
| 100件バッチ(1件あたり) | 1.1789 s | 1.912 s(計191.2 s) | 約1.6倍遅い |
| Qdrant類似検索 | 0.00698 s | 0.00041 s(ウォーム平均) | どちらも10ミリ秒未満 |
| Qdrantディスク(100件) | 0.99 MB | 0.41 MB(256次元) | ※ |
| SQLite復元 | 0.00054 s | 0.00156 s | どちらも2ミリ秒未満 |
モデルロードは6.56秒、初回推論は2.10秒だった。
※ ディスクが前回記事より小さいのは次元数の違いによる(後述のAPI版は1024次元で条件が異なる)。
この実機でも前回記事の内容は最後まで動いた。メモリフットプリント(約1.4GB)は前回記事とほぼ同じだが、埋め込み速度は1.6〜1.9倍遅い。16GB機では、埋め込みだけで常時1.4GBを使う。
ModelScope API経由で同じ処理を測る
Qwen/Qwen3-Embedding-0.6Bをapi-inference.modelscope.ai/v1/embeddingsで叩いた。事前の疎通確認でチャット(Qwen-Ambassador/Qwen3.7-Plus)も約2秒で通ることを確かめている。
| 測定項目 | ローカルCPU | API経由 | 差 |
|---|---|---|---|
| 1件埋め込み | 2.10 s | 0.83 s(5回平均、初回0.81 s) | 2.5倍速い |
| 100件バッチ(1件あたり) | 1.912 s | 0.078 s(1リクエスト一括、計7.8 s) | 約24倍速い |
| クライアントRSS(全工程後) | 1,413 MB | 96.9 MB | 約1/15 |
| Qdrant検索(100件、1024次元) | — | 0.00165 s | — |
| 検索E2E(クエリ埋め込み+検索) | 約2.1 s | 1.20 s | API側が速い |
| 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 s | 1.76 s | — | 4.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 s | 2.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 s | — | 3.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にはまだ入れていない。