技術約7分で読めます

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性能に強く依存するので参考値として扱った。

検証環境

こんな感じ。

項目この開発機本番機(参考)
OSWindows 11Windows 11
CPU13th Gen Intel Core i7-13700H(14コア/20スレッド)AMD Ryzen 7 5800HS(8コア)
GPUNVIDIA GeForce RTX 4060 Laptop 8GBRTX 3050 Ti Laptop(VRAM 4096MiB、常時3945MiB使用)
RAM32GB16GB
Python3.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-transformersqdrant-clientを追加で入れた。バージョンは次のとおり。

パッケージバージョン
torch2.9.1+cpu
numpy2.3.3
psutil7.1.3
sentence-transformers6.0.1
transformers5.16.1
qdrant-client1.19.0
huggingface-hub1.30.0
scikit-learn1.9.0
pydantic-core2.46.5
tokenizers0.23.2

この状態で計測に進んだ。

Qwen3-Embedding-0.6BをCPUで動かす

使ったテキストは前回記事のJSON例と同程度の長さで、topicsummaryentitiesをまとめた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=2561.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件upsert0.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の元発話まで戻れるかを確認した。

テーブルはturnsmemoriesの2つ。

テーブル
turnsturn_id, speaker, text, occurred_at
memoriesmemory_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や速度低下が起きないか