前回(第33回)、ローカルLLMたちに早口言葉を解かせて「judge は自分の知らない正解を選べない」ことを観測した。そのとき同時に、もっと身近で深刻な問題も実測で確定していた ― OpenWebUI 経由の回答が低品質な主因は、パイプラインの配管ではなく、子タスクを担う小型モデルが事実を捏造していることだ。同じ技術質問を 8B クラスのモデルに直接投げると正答する。つまり配管は正しく、中身(モデル)が足りていなかった。
今回はその対処 ― 汎用推論プールを 8B クラスだけに絞る「Phase 1」の記録である。コードは一行も書いていない。設定(各ワーカーが自分の対応モデルとして登録するリスト)を変えただけだ。にもかかわらず、「モデルを登録リストから外す」と「ルーティングの候補から消す」が別物だったという、設計の地雷を踏むことになった。
何が起きていたか ― 小型モデルの捏造
うちのクラスタは、Coordinator が質問をワーカー群にルーティングする構成になっている。ワーカーには非力な CPU ノード(1B〜7B の小型モデル)から、M1 Mac や GPU 機(8B クラス)まで混在している。問題は、ルーティングがしばしば小型モデルを選んでいたことだった。
たとえば PostgreSQL の「dead tuple」と「XID 周回」について聞くと、小型モデルはこう答える ― 「XID 周回とは同じ XID 値が2回使われること」「dead tuple はコミットされていないと削除されるデータ」。どちらももっともらしいが完全な誤りだ。参考リンクとして実在しない URL まで添えてくる。一方、同じ質問を 8B(qwen3:8b)に投げると、更新で不可視化された旧版タプル、約42億で一巡する32bitトランザクションID、VACUUM による回収 ― と、おおむね正しく答える。
結論はシンプルだった。品質を上げるには、汎用推論を 8B クラスだけに任せればよい。
第一の罠 ― 「登録から外す」だけでは候補から消えない
各ワーカーは、自分が処理できるモデルのリスト(sanity_models)を Coordinator に登録する。だから単純に考えれば、小型ノードの sanity_models から小型モデルを外せば、ルーティングはもう小型を選べなくなる ― はずだった。
ところが、小型ノードに「作業用に1つだけ」モデルを残したまま 8B ノードを足してテストしたら、5回投げて5回とも小型モデルに着地した。8B には一度も回らない。
原因はルーティングのスコアリングにあった。ルーターは過去の実績(速度・採点)からスコアを計算してベストな組み合わせを選ぶ。ところが 実績がまだ無い 8B モデルには固定の初期スコア(0.3)しか付かない。一方、使用実績のある小型モデルは速度評価が効いて 0.7 前後のスコアになる。速度の重みが大きいので、「速いが間違える小型」が「実績ゼロで正しい 8B」に構造的に勝ち続けるのだ。登録リストに小型が1つでも残っている限り、それが主経路を奪う。
つまり「退場させる」には、登録リストを減らすのでは足りず、その小型モデルを候補から完全に消す(sanity_models を空にする)必要があった。空にするとワーカーは「登録できるモデルがない」と判断して登録自体をスキップする ― これも最初は驚いたが、結果として狙いどおり「汎用ルーティングに一切出てこない」状態になる。作業用に小型を叩きたいときは、ワーカーを名指しする明示ルーティングで呼べばよい。常時リストに載せておく必要はなかった。
第二の罠 ― CPU で 8B は「温めても」間に合わない
堅牢性のために、CPU ノードの一台(16GBメモリ機)を「遅いけれど 8B を返せる最後の砦」にしようとした。ところが、ワーカー起動時の疎通チェック(モデルに1回応答させる health check)が30秒のタイムアウトに引っかかって、登録に失敗し続けた。
最初は「初回ロードが遅いだけ」と思い、モデルを事前にメモリへ常駐させてから再起動した。それでも失敗する。モデルがメモリに載っていること(warm)と、生成が速いことは別問題だった ― 100% CPU 実行では、温まっていても1回の応答が30秒に収まらない。「CPU で 8B を常用枠にする」のは、このハードでは構造的に無理だと実測で分かった。当初の構成案を捨て、この CPU ノードは推論プールから外して別の役割(RAG の回答合成)に専念させた。
落ち着いた構成
最終的に、汎用推論プールはこうなった。
- M1 Mac(8B×2を登録・常時の土台) ― Metal で qwen3:8b と日本語特化の elyza-jp-8b。ただし16GBでは2つを同時にメモリ常駐させると競合するので、実質1つずつ。
- GPU 機(8B・best-effort) ― 上がっていれば高速枠として自動で候補に入り、落ちていれば登録が切れて自然に候補から外れる。「上がっていれば使う」が、特別な実装なしに成立した。
- 小型 CPU ノード3台 ― 汎用から退場。分類・RAG 合成・予備といった、ルーティングを経由しない専用役に専念。
検証として、小型モデルが捏造していたあの PostgreSQL の質問を5回投げ直した。今度は5回とも 8B に着地し、捏造していた架空の用語は消えていた。Phase 1 の目的 ― 「子モデルの底上げで捏造を潰す」 ― は達成された。
残った宿題
8B にしても万能ではない。「自分のインフラ固有の知識」や「学習データに薄い専門APIの細部」では、8B もやはりもっともらしく捏造する。これは容量では直らない領域で、RAG(自分のドキュメントを検索して事実を供給する)や、領域特化モデルへのルーティングが要る ― それは次の回の話だ。
今回いちばん持ち帰るべき教訓は、地味だがこれだと思う。「設定で登録モデルを絞った」ことと「実際にルーティングがどう振る舞うか」は、別々に計測しないと食い違う。 スコアリングの初期値ひとつで、退場させたはずのモデルが主役に居座り続ける。計測してから信じる ― 今回もそれに何度も助けられた。