前回の記事ではOpenWebUI連携とWorker実負荷ルーティングを実装した。今回はその土台をさらに強化し、「GPUタスクはGPU Workerへ、CPUタスクはCPU Workerへ」という能力ベースのルーティングと、新モデルの大量追加を行った。
今回やったこと
- Redisキューを
tasks:gpu/tasks:cpuに分離 - WorkerにCapability(能力宣言)を持たせるcapability routingを実装
- moon・rtx3070tiに新モデルを追加(合計6モデル)
- GPU情報取得の修正(WSL2環境のnvidia-smiパス問題)
- 全ドメインの実績データ収集
なぜqueue分離が必要だったか
これまでタスクキューは tasks という1本だった。この構成には問題があった。
gemma3:12b のような大型モデルはVRAM 8GBのGPUがないと実用速度が出ない。しかしキューが1本だと、GPUを持たないmoon(ThinkCentre・CPU推論専用)にもこのタスクが飛んでしまう。moonで gemma3:12b を動かすと数分待つことになる。
さらに将来、埋め込みWorkerやWeb検索Workerを追加することを考えると、「誰でも何でも処理する」設計では破綻する。Worker側が「自分は何ができるか」を宣言し、Coordinatorがそれを見てタスクを振る仕組みが必要だった。
設計:supportsとキューの対応
各WorkerのHeartbeatに supports フィールドを追加した。
# moon(CPU推論のみ)
"supports": ["cpu_inference"]
# rtx3070ti(CPU・GPU両方)
"supports": ["cpu_inference", "gpu_inference"]
# 将来のRAG/Web検索Worker(未実装)
"supports": ["embedding", "web_search"]
Coordinator側ではモデル名を見てキューを決める。
GPU_MODELS = {"gemma3:12b", "qwen2.5:7b", "phi4-mini:3.8b", "gemma3:4b"}
def resolve_queue(model: str) -> str:
return "tasks:gpu" if model in GPU_MODELS else "tasks:cpu"
GPU_MODELS に追記するだけで新しいGPU専用モデルを追加できる。
Workerは自分が監視するキューを設定で持つ。rtx3070tiは tasks:gpu を優先しつつ tasks:cpu も監視する。moonが落ちているときのフォールバックにもなる。
# rtx3070ti:GPU優先・CPUもフォールバック
"redis_queues": ["tasks:gpu", "tasks:cpu"]
# moon:CPUのみ
"redis_queues": ["tasks:cpu"]
Redisの brpop はリストを渡すと先頭から順に優先して取り出す。この性質を使って優先度制御を実現している。
実装の変更点
変更したファイルは4つ。
worker_base.py
最大の変更は brpop の呼び出し部分。
# 変更前
r.brpop("tasks", timeout=5)
# 変更後(複数キューを優先順に監視)
r.brpop(["tasks:gpu", "tasks:cpu"], timeout=5)
また register() 関数に supports パラメータを追加し、HeartbeatのたびにCoordinatorへcapabilityを送信するようにした。後方互換として旧形式の redis_queue(単数文字列)が渡された場合は自動でリストに変換する。
coordinator_api.py
Worker選択ロジックに capability フィルタリングを追加。
def filter_workers_by_capability(workers, required):
return [w for w in workers if required in w.get("supports", [])]
Worker登録時に supports をRedisに保存する。supports を送ってこない古いWorkerは ["cpu_inference"] とみなすことで後方互換を確保している。
追加したモデル
rtx3070ti(VRAM 8GB)
| モデル | 追加理由 |
|---|---|
| phi4-mini:3.8b | Microsoftモデル・コード系に強いとされる |
| gemma3:4b | 12bと1bの間を埋める中間サイズ |
| llama3.2:1b | 最速候補・moonとの比較 |
| qwen2.5:3b | GPU推論なら実用速度が出るはず |
moon(ThinkCentre・CPU推論)
| モデル | 追加理由 |
|---|---|
| llama3.2:1b | CPU推論でも実用速度が出る超軽量モデル |
| qwen2.5:3b | 追加後に実測で遅すぎたためrtx3070ti専用に変更 |
qwen2.5:3b はmoonのCPUで動かすと26〜70秒かかることが判明した。rtx3070tiのGPU推論に限定することで適切な速度が期待できる。「試して実測して判断する」という設計思想が早速活きた。
GPU情報取得の修正(WSL2のPATH問題)
rtx3070tiのWorkerはWSL2上で動いている。GPU使用率やVRAM使用量を取得するために nvidia-smi を呼んでいるが、systemdサービスはデフォルトのPATHが短く /usr/lib/wsl/lib が含まれていない。
解決策はsystemdのserviceファイルに Environment=PATH=... を1行追加するだけだった。
[Service]
User=hogehoge
Environment=PATH=/usr/lib/wsl/lib:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/home/hogehoge/worker/venv/bin/python /home/hogehoge/worker_gpu.py
修正後、/workers エンドポイントでVRAM情報が正しく取れるようになった。
"gpu_util": 30.0,
"vram_used_mb": 6674.0,
"vram_total_mb": 8192.0
実績データの収集結果
/compare で全モデルに同じプロンプトを投げ、ドメインごとの実績を収集した。
codeドメイン
| モデル | avg_ms | n |
|---|---|---|
| LFM2.5-1.2B-JP | 6,838 | 5 |
| qwen2.5:7b | 10,320 | 3 |
| phi4-mini:3.8b | 11,399 | 1 |
| gemma3:4b | 13,773 | 1 |
| gemma3:12b | 143,702 | 5 |
japaneseドメイン(今回重点収集)
| モデル | avg_ms | n |
|---|---|---|
| gemma3:1b | 1,943 | 5 |
| llama3.2:1b | 4,647 | 4 |
| phi4-mini:3.8b | 4,703 | 4 |
| qwen2.5:7b | 4,877 | 4 |
| gemma3:12b | 30,570 | 4 |
前回まで japanese ドメインの実績がほぼゼロだったが、今回の収集で全モデルn=4以上に揃った。
興味深いのは gemma3:1b がjapaneseで突出して速いこと(2位の2倍以上速い)。日本語タスクではモデルサイズよりアーキテクチャの相性が出ているのかもしれない。ただし速度と品質は別の話で、品質評価(LLM-as-judge)は今後の課題だ。
一方 qwen2.5:3b はjapaneseでp50=2,540msと速いのにp95=22,856msと大きく乖離している。moonのCPUで動いたケースとrtx3070tiのGPUで動いたケースが混在しているためで、Worker間のばらつきが数字に出ている。
capability routingの動作確認
/route/preview エンドポイントで振り分け結果を確認できる。
curl -G http://localhost:8000/route/preview \
--data-urlencode "prompt=大きなデータを分析して"
{
"domain": "general",
"candidates": [
{"model": "llama3.2:3b", "queue": "tasks:cpu", "capability": "cpu_inference"},
{"model": "qwen2.5:7b", "queue": "tasks:gpu", "capability": "gpu_inference"},
{"model": "gemma3:12b", "queue": "tasks:gpu", "capability": "gpu_inference"},
...
]
}
GPU専用モデルが tasks:gpu に、それ以外が tasks:cpu に正しく振り分けられている。
次回の予定
次はmars(専用回線サーバー)にSearXNGをDockerで立てる。SearXNGはオープンソースのメタ検索エンジンで、JSON APIで検索結果を取得できる。これをAI基盤に組み込むことで、ローカルLLMに「最新情報を踏まえた回答」という新しい能力を持たせるのが目的だ。
完全ローカル完結・無料無制限・プライバシー保護という基盤の思想とも合っている。
実装順序としては:SearXNG構築 → Web検索Worker → RAG基盤(Qdrant)→ Coordinatorへの統合、という流れで進める予定だ。