Qwen3-EmbeddingとQdrantのコストを測った
目次
2026-09-09 追記: 本番機(Ryzen 7 5800HS)で同じ計測を再現し、ModelScope APIとの比較とTTS・ASR常駐下で同時実行したときの干渉も測った → Qwen3-EmbeddingをローカルとAPIで比べた
前回、QwenとQdrantで長期記憶付きStackChanの設計を固めた。
音声チャットサーバーを動かしているRTX 3050 TiノートはVRAMが3,945/4,096 MiBまで埋まっていて、Qwen3-EmbeddingをGPUへ追加する余裕はない。
0.6B版をCPUで動かす方針にしたが、「RAM使用量と処理時間を、保存容量より先に測る」がまだだった。
そこで、その本番機ではなく手元の開発機(Windows 11、RTX 4060 Laptop 8GB、RAM32GB)でQwen3-Embedding-0.6BのCPU実行とQdrantのローカルモードを動かし、新しく追加する2点のコストを先に測った。
この開発機と本番機はCPU・GPUとも別物なので、ここでの数字がそのまま本番機に当てはまるわけではない。
一方でRAM消費はモデルサイズに比例する部分が大きく機種依存は薄いはずなので、そこだけ先に当たりをつけた。処理時間はこの開発機のCPU性能に強く依存するので参考値として扱った。
検証環境
こんな感じ。
| 項目 | この開発機 | 本番機(参考) |
|---|---|---|
| OS | Windows 11 | Windows 11 |
| CPU | 13th Gen Intel Core i7-13700H(14コア/20スレッド) | AMD Ryzen 7 5800HS(8コア) |
| GPU | NVIDIA GeForce RTX 4060 Laptop 8GB | RTX 3050 Ti Laptop(VRAM 4096MiB、常時3945MiB使用) |
| RAM | 32GB | 16GB |
| Python | 3.14.0 | 前回記事に記載なし |
本番機の値はRTX3050Ti 4GBでSTT+LLM+TTSサーバーを立てるとVRAM 4GBのRTX 3050 Ti LaptopでIrodori-TTSの日本語ボイスクローンを試したから。
準備
python -m venv --system-site-packages .venv で作業用のvenvを切り、グローバルに入っていたtorch、numpy、psutilをそのまま流用した。torchは最初からCPU版だった。
そこへsentence-transformersとqdrant-clientを追加で入れた。バージョンは次のとおり。
| パッケージ | バージョン |
|---|---|
| torch | 2.9.1+cpu |
| numpy | 2.3.3 |
| psutil | 7.1.3 |
| sentence-transformers | 6.0.1 |
| transformers | 5.16.1 |
| qdrant-client | 1.19.0 |
| huggingface-hub | 1.30.0 |
| scikit-learn | 1.9.0 |
| pydantic-core | 2.46.5 |
| tokenizers | 0.23.2 |
この状態で計測に進んだ。
Qwen3-Embedding-0.6BをCPUで動かす
使ったテキストは前回記事のJSON例と同程度の長さで、topic・summary・entitiesをまとめた137文字の日本語テキストを使った。これでモデルロード前後のRSS(プロセスが実際に使用中の物理メモリ量)差分と、1件・10件・100件の埋め込み処理時間を測った。
RSSはモデルロード後よりも、実際に1回推論を通したあとの方が大きく伸びた。
| 時点 | RSS |
|---|---|
| インポート前 | 19.2 MB |
| sentence_transformersインポート後 | 404.9 MB |
| モデルロード後 | 529.1 MB |
| 初回推論後 | 1386.5 MB |
| 100件バッチ後 | 1695.0 MB |
処理時間は、初回ダウンロードを含むロードだけ回線待ちで長い。ウォームアップ後は1件でもバッチでも大差がなかった。
| 項目 | 時間 |
|---|---|
| モデルロード(初回ダウンロード込み) | 193.50s |
| 初回推論(遅延初期化込み) | 1.22s |
| ウォームアップ後、1件 | 1.114s |
| ウォームアップ後、10件バッチ(1件あたり) | 1.0753s |
| ウォームアップ後、100件バッチ(1件あたり) | 1.1789s |
ウォームアップ後、1件(truncate_dim=256) | 1.1208s |
sentence-transformersの既定設定では、CPU上でバッチにしても1件あたりの時間がほとんど短くならず、1.08〜1.18秒/件でほぼ同じだった。Matryoshka Representation Learning(MRL)で次元を1024から256へ落としても、この処理時間は同水準だった。MRLは出力ベクトルを切り詰めるだけで、モデル本体の計算量はそのまま。
次元を落とす利点はベクトルの保存サイズが4096バイトから1024バイトへ小さくなる点と、Qdrant側のメモリ・ディスクが減る点。ただし埋め込み処理自体の速度は変わらない。
まずSentenceTransformerのコンストラクタ引数でtruncate_dim=256を指定した。すると既存の1024次元モデルとは別インスタンスが作られ、2つ分のRSSが加わった。呼び出し時のmodel.encode(texts, truncate_dim=256)に変えると、モデルのインスタンスは1個のままになった。
Qdrantをローカルモードで動かす
QdrantClient(path=...) でサーバーを立てずにディスクへ残すローカルモードを使う。前回記事で決めた検索用メモリの項目に沿ったダミーデータを、実際にQwen3-Embeddingでベクトル化して100件登録した。
最初のスクリプトはQdrantClient.search()で書いたが、qdrant-client 1.19.0ではsearch()が廃止されていてAttributeErrorが出た。query_points()に置き換えて、戻り値をresp.pointsから取り出す形に直して再実行した。
| 項目 | 値 |
|---|---|
| 100件ベクトル化 | 0.5997s/件 |
| 100件upsert | 0.429s |
| 類似度検索のみ(上位5件) | 0.00698s |
| entitiesフィルター検索(上位5件) | 0.00370s |
qdrant_dataディレクトリのディスク使用量 | 0.99 MB |
| 両方動かした時のRSS(upsert後) | 1562.1 MB |
entitiesでフィルターした検索の方が、フィルターなしより速かった。100件程度の規模では、絞り込みのオーバーヘッドより検索対象が減る効果の方が大きいようだ。埋め込みモデル単体とQdrantを合わせた合計RSSは、この開発機ではおおむね1.5〜1.7GBの範囲になる。
クエリ文「RTX 3050 Tiのノートで何を動かしたっけ」で検索すると、ダミーデータの中で唯一RTX 3050 Tiに言及したトピック「StackChanの音声バックエンド」が上位3件を占めた。
| 順位 | スコア |
|---|---|
| 1位 | 0.4762 |
| 2位 | 0.4692 |
| 3位 | 0.4667 |
SQLiteから元発話を復元する
ここまでは埋め込みとQdrantを単体で測っただけで、前回記事で決めた「元の発話はSQLiteへ残し、Qdrantには要約のベクトルと検索用の項目だけを入れる」という設計は試していなかった。SQLiteのスキーマを実際に作り、検索結果からSQLiteの元発話まで戻れるかを確認した。
テーブルはturnsとmemoriesの2つ。
| テーブル | 列 |
|---|---|
turns | turn_id, speaker, text, occurred_at |
memories | memory_id, topic, summary, entities, memory_type, occurred_at, source_turn_ids, status, qdrant_point_id |
memories.qdrant_point_idでQdrant側のポイントと紐付けた。
投入した話題は5つ。音声バックエンド、PCの購入、CO2モニターのセンサー配線、好きな飲み物、Qdrantのフィルター相談を、それぞれ4回ずつ繰り返して計20話題分にした。1話題ごとにuser発話とassistant発話が1つずつ付くので、turn 40件・memory 20件になる。要約文はQwenを呼ばず、固定テンプレートで作った仮のもの。
| 項目 | 値 |
|---|---|
| SQLite書き込み(turn 40件 + memory 20件) | 0.0030s |
| 20件ベクトル化 | 13.560s(0.678s/件) |
| Qdrant upsert(20件) | 0.0954s |
| 検索(クエリ文の埋め込み込み) | 0.2673s |
SQLiteから元発話を復元(source_turn_ids経由) | 0.00054s |
| SQLiteファイルサイズ | 24.0 KB |
| Qdrantディスク使用量 | 0.21 MB |
クエリ文「富士通のPCの件、結局どうしたっけ」で検索すると、「富士通のPCの購入」トピックが最上位(スコア0.6801)でヒットした。そのポイントIDからmemoriesテーブル経由でturnsテーブルをたどり、元発話2件を復元できた。
turn12 [user] PCどれ買った?
turn13 [assistant] 富士通のesprimoにした。安定重視で選んだ。
前回記事の設計どおり、Qdrantの要約検索からmemory_id経由でSQLiteの元発話に戻る流れが動くことを確認できた。SQLiteからの復元は0.5ミリ秒台で、ここはボトルネックにならなかった。かかった時間の大部分はクエリ文の埋め込み処理(0.267秒)が占めている。
本番機に乗せられそうか
ここまでの実測(埋め込みモデルとQdrantを合わせて1.5〜1.7GB)を、本番機の空きと突き合わせる。本番機でSTT・TTSを検証していたときの記録では、16GB中の空きは約6.5GBだった。この開発機での消費量なら、その空きに入る。
処理時間の面では、前回記事で決めた設計どおり検索が必要と判断されたときだけ埋め込みを1回呼ぶので、検索が動くターンだけ発生するコストになる。それでも検索が動くターンでは、埋め込みだけで1秒前後の遅延が加わる。本番機のCPUはRyzen 7 5800HSで、この開発機のi7-13700Hとは世代が異なる。
このコードを本番機(Irodori-TTSを動かしているノート)に持って行って確認するのは次の点。
- 現在の空きRAMが、記録の約6.5GBから変わっていないか
- Qwen3-Embedding-0.6BをRyzen 7 5800HS上で動かしたときの、実際のRAM増分と処理時間
- Qdrantローカルモードの登録・検索速度
- STT・TTSと同時に動かして、OOMや速度低下が起きないか