著者: 著者: やまねこ タグ:
Mac mini上でmlx-optiqのOptiQサーバーがKV cacheを圧縮しながらHermes Agentへ高速に応答している概念図

こちらの記事の手順で、Mac mini上のDWQ環境とHermes Agentは無事に繋がりました。

ただ、実際に運用してみると、次の壁にぶつかります。長文ingest(情報商材や講義録、PDF抽出)に時間がかかる。Hermes Agentの応答が、途中で切れることもある。

この記事では、バックエンドをmlx_lm.server(DWQ)からoptiq serve(mlx-optiq)へ置き換えて実測した結果と、出力が途中で切れる症状の切り分け方を解説します。
基本構築がまだの方は、先にこちらの記事を読んでおくと理解しやすくなります。

ざっくりまとめ

  • OptiQはDWQ比、同条件A/Bで約1.85倍速でした。メモリにも余裕が生まれます。
  • 決め手は、per-layer混合精度KVと、Qwen3.6-35B-A3Bのハイブリッド注意機構という構造的な理由です。
  • MTP(speculative decoding)は実測で効果がなく、この構成では不採用にしました。
  • 「出力が途中で切れる」は、finish_reasonを見れば機械的に切り分けられます。
  • 128K/256Kのcontextは、まだ実測できていません。77.6Kまでは実測合格です。

数値には必ず検証条件が付きます。「常に速い」ではなく、あくまでMac mini M4 Pro 64GB・特定のcontext長・特定の日付での結果として読んでください。

結論から言うと — 1.85倍速、メモリにも余裕

2026年6月30日、本番バックエンドをmlx_lm.server(DWQ)からoptiq serve(Qwen3.6-35B-A3B-OptiQ-4bit)へ置き換えました。

同条件A/Bベンチマークでは、約1.85倍速、メモリは単体常駐で約27GBという結果になりました。ingestの品質もDWQと同等以上です。

構成は次の通りです。

役割機体詳細
推論サーバーMac mini M4 Pro 64GBoptiq serve / port 8080
AgentMac mini M4 16GBHermes Agent

これから紹介する数値は、すべて2026年6月29日から30日にかけて、この機体構成で検証したものです。環境が異なれば、結果も変わります。

なぜOptiQはここまで速いのか — per-layer混合精度KV+ハイブリッド注意機構

速さの理由は、2つの構造的な仕組みにあります。

1つ目は、per-layer混合精度KVです。optiq kv-cacheを実行すると、層ごとの感度をKL divergenceベースで測定し、4bitと8bitを混在させます。
実際にQwen3.6-35B-A3Bで測定したところ、Layer3は8bit化で誤差が大きく敏感、Layer15と39は8bitでもほぼ無損失という層差が見つかりました。最終的には、7層を4bit、3層を8bitに割り当て、平均5.20 bitsに最適化されています。

2つ目は、Qwen3.6-35B-A3B自体のハイブリッド注意機構です。このモデルは、40層のうち10層だけがfull-attention(KV cacheを保持する層)で、残り30層はlinear-attnのためKVを蓄積しません。
つまり、長いcontextでKVメモリが増えるのは、全体の4分の1にあたる10層分だけです。これが、64GBのマシンでも64Kから128K程度のcontextを現実的に扱える構造的な理由になります。

mlx-optiqの公式ドキュメントでも、均一4bitのKV量子化では品質が破綻しやすい層があり、混合精度であればそれを避けられると説明されています。
(参考: mlx-optiq公式サイト
層ごとに感度が異なるという前提そのものは、著者の実測だけでなく、開発元も同じ立場を取っています。

optiq serveの実体は、mlx_lm.serverにKV/SDPAのパッチを加えたものです。
(参考: mlx-lm GitHub
tool calling、OpenAIのtools構造、parserの選択ロジックは、こちらの記事で解説したmlx_lm.serverの仕組みをそのまま引き継いでいます。新しく覚えることは、KV cacheの扱い方だけです。

実際に測ってみた — 同条件A/Bベンチマーク

数字は、検証条件とセットで見て初めて意味を持ちます。

入力には日本語の長文を使い、7つの見出しに構造化するingestプロンプトで、temperatureは0に固定しました。DWQとOptiQで、tokenizerによるトークン充填率も揃えています。

指標DWQ 32KOptiQ 32KDWQ 64KOptiQ 64K
wall time177.9秒95.8秒334.8秒180.5秒
e2e tok/s10.221.44.311.3
finish_reasonstoplengthstoplength
peak mem49GB(併存)50GB(単体)55GB(併存)

64Kのcontextでは、約1.85倍速という結果です。32Kでは、約2.1倍まで差が開いています。

OptiQのfinish_reasonlengthになっているのは、テスト時のmax_tokensが2048で頭打ちになったためです。記述が濃くなる傾向があるので、実運用ではmax_tokensを3500から4000あたりに設定することをおすすめします。

メモリの面でも違いが出ています。DWQの64K時50GBは、full-attentionによるKV膨張が主な要因です(base約22GB+KV約28GB)。
一方のOptiQは、10層分のKVだけで済むため、切り替え後の単体常駐が約27GBに収まっています。

swapについても触れておきます。OptiQ起動時、DWQと一時的に併存した瞬間に一回だけswapoutが発生しますが、ingest実行中のswap増分は約119MBで、実質ゼロです。
1.85倍という数字は、swapに助けられた結果ではありません。

速くなるかと試したMTPは採用しなかった話

速くなると期待して試した機能が、必ずしも採用に至るとは限りません。

Qwen3.6-35B-A3Bの配布モデルには、MTP(Multi-Token Prediction)用のheadが同梱されています。Hugging Faceのモデルカードでは、depth 2で受け入れ率約70%、decodeが約1.4倍速くなると説明されています。
(参考: Qwen3.6-35B-A3B-OptiQ-4bit モデルカード

実際に--mtp(depth=2)を有効にして測定したところ、decode速度はほぼ変わらず、むしろ微減という結果になりました(56.1→55.8 tok/s)。

理由は、モデルの構造にあります。Qwen3.6-35B-A3Bは、MoE(Mixture of Experts)でactiveパラメータが3Bと小さいため、1トークンのdecode自体がもともと高速です。
そこにdraft生成と検証という追加の計算を挟むと、本来の速さの利得を相殺してしまいます。

公式スペック上の期待値と、手元の実測結果が食い違うのは珍しいことではありません。mtp headの分だけメモリと可動部が増えるだけの結果になったため、この構成ではMTPを不採用としました。

新しい機能はためらわず試す価値がありますが、採用するかどうかは、自分の環境での実測で判断するべきです。

カットオーバー手順とロールバック手順

本番切り替えは、戻せる手順とセットで進めます。

DWQからOptiQへのカットオーバーでは、まずDWQのwrapperスクリプトを、日付入りの明示的なファイル名でバックアップします。.bakという名前は、どのタイミングの構成を指すか曖昧になりやすいため避けています。

続けて、DWQのlaunchd labelをbootoutで停止し、lsofでポートが空いたことを確認してから、OptiQのlabelをbootstrapenablekickstart -kの順で起動します。

cp ~/bin/run-mlx-lm-hermes.sh ~/bin/run-mlx-lm-hermes.dwq-20260630.sh

launchctl bootout gui/$(id -u)/local.mlx-lm-hermes
lsof -nP -iTCP:8080 -sTCP:LISTEN

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.mlx-optiq-hermes.plist
launchctl enable gui/$(id -u)/local.mlx-optiq-hermes
launchctl kickstart -k gui/$(id -u)/local.mlx-optiq-hermes

Hermes Agent側のプロファイル切り替えは、wizard(hermes -p brain model)で行います。Hermes Agentは、コード変更なしにプロバイダーを切り替えられる設計になっており、今回のOptiQへの切り替えも、このカスタムエンドポイント対応の枠組みで行っています。
(参考: Hermes Agent GitHub
こちらの記事でも触れた通り、config set model.defaultの単発実行では、context_lengthの設定が引き継がれずauto-detectに戻ってしまうため、モデルとcontext lengthは必ずセットで設定してください。

もう一つ、注意点があります。optiq serveは既定で--authが有効です。この状態だと、Hermes Agentの会話本体やtool callは通るものの、補助的なタイトル生成だけが別の認証ヘッダーを送ってしまい、401エラーで弾かれます。
実害はタイトルが生成されない程度ですが、本構成では--no-authにして、既存のDWQ環境と同じく、Tailscale Grantsを実質的なアクセス制御として扱う設計に揃えています。

ロールバックは、逆の手順です。OptiQのlabelを停止し、バックアップしておいたDWQのwrapperを戻し、DWQのlabelを起動します。Tailscale Grantsは8080のまま運用するため、ネットワーク設定側の変更は必要ありません。

ここまでのポイント

OptiQへの置き換えは、検証条件のもとでは速度・メモリともに実測で優位という結果でした。ただし、MTPのように、試したものの採用しなかった技術もあります。ここからは、出力が途中で切れる症状の切り分けと、KV cache運用の実務に入ります。

「出力が切れる」を2つに切り分ける — max_tokens不足かKV cache逼迫か

Hermes Agentで「前回の応答は出力長の上限で切り詰められました」というメッセージが出たら、原因は2系統のどちらかです。

1つ目は、出力token上限の問題です。API応答のfinish_reasonlengthになっているケースで、max_tokensやHermes側の出力上限を調整します。
2つ目は、長context時のメモリ問題です。KV cacheが膨らみ、swapや速度低下、失敗につながります。KV cacheの圧縮はメモリの余裕を作る対策であり、max_tokensの上限そのものを増やすものではありません。この2つを混同すると、的外れな対処をしてしまいます。

まず、API単体でfinish_reasonを確認してください。

結果対応
finish_reason=stopサーバー側は正常。Hermes側の出力上限を確認する
finish_reason=lengthmax_tokensが不足している。増やすか、タスクを分割する
contentが空でreasoningだけ長いthinkingがtokenを消費している。enable_thinkingをfalseにする
API単体でもOOMやtimeoutになるKV config、併存プロセス、swapを確認する

Qwen3.6は、思考の過程をmessage.reasoningに出力するreasoningモデルです。max_tokensが小さいと、思考でtokenを使い切ってしまい、contentが空のままfinish_reason:lengthになることがあります。
chat_template_kwargs{"enable_thinking": false}を指定して思考をオフにするのが、正しい対処です。日本語のingestでは、思考をオフにしておくことをおすすめします。

Hermes Agent側の挙動も押さえておくとよいでしょう。finish_reason=lengthを検知すると、Hermes Agentは継続を促すプロンプトを付けて、最大3回まで自動的に再送します。
3回を超えて完走しない場合だけが、本当に対処が必要な失敗です。1、2回の再送メッセージが出ること自体は、想定内の動作だと考えてください。

さらに実機を調べたところ、model.max_tokensという設定キーをconfig.yamlmodel:セクションに明示していない場合、呼び出し箇所によってフォールバックの挙動が揃っていないことが分かりました。
未設定のまま運用するより、次のように明示しておくと、動きが予測しやすくなります。

model:
  base_url: http://<MLX_TAILSCALE_IP>:8080/v1
  default: mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit
  provider: custom
  api_mode: chat_completions
  max_tokens: 4000

この4000という数値は、先ほどのベンチマークで見た、OptiQの出力が濃くなりやすい傾向を踏まえた目安です。

KV cache設定を生成して差し替える

KV cache設定は、生成、検証、反映という3段階で、安全に差し替えられます。

生成には、optiq kv-cacheを使います。

optiq kv-cache mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit \
  --target-bits 5.0 --candidate-bits 4,8 \
  -o ~/llm-optiq/kv/qwen36

このコマンドは、検証用にモデルをもう1つロードします。本番のoptiq serveが常駐中で、メモリに余裕がない場合や、swapが増えている場合は、生成前に本番serveを停止してください。

生成が終わったら、wrapperスクリプトのKV_CONFIGを新しいパスに差し替え、意図しない--mtp--kv-bitsが残っていないかを確認してから、launchdで再起動します。
起動後は、ログにmixed-precision KV from ...という行が出ているかを確認してください。これが出ていれば、KV cacheの設定が正しく反映されています。

検証は、短いtool callと長文ingestの両方で行います。いきなり100Kや128Kを試さず、32K、64K、80K前後、100Kという順に、段階を踏んで確認してください。

メモリの目安も記録しておきます。切り替え後のOptiQ単体baseは約27GB、64Kのingestで単体換算約33GB、77.6Kでは約39GBという実測値です。
128Kと256Kについては、まだ実測できていません。64Kから77.6Kにかけてのメモリの伸び方が、単純な線形よりも急なため、この先の外挿は楽観的にしすぎないほうがよいと考えています。
256Kまでのcontext拡張は、モデルの仕様上は範囲内ですが、100Kや128Kの実測を先に埋めてから、段階的に検証する予定です。

うまくいかない場合のチューニング方針も整理しておきます。

症状次の候補
品質劣化、tool call崩れ--target-bits 6.0で再生成し、8bit層を増やす
メモリがまだ厳しいDWQ併存を排除、MTPを外す、contextを固定する
finish_reason:lengthKVではなくmax_tokensやHermes側の出力上限を調整する

実際に起きた話 — 設定変更直後に無応答になった一件

ここからは、短いコラムです。詳しい事後検証は、別の機会に譲ります。

2026年7月2日、Hermes Agent側のconfig.yamlmodel.max_tokens: 4000を追記した直後、Hermes Agentからの応答が完全に返らなくなりました。

一次切り分けとして、サーバー機でpsを使い、推論プロセスの状態を確認しました。プロセス自体は生きているものの、CPU使用率は0%付近で、STATS(sleeping)のまま動いていません。
エラーログを見ても、/v1/chat/completionsへのPOSTが一件も記録されていませんでした。vm_statのSwapouts累積も、かなり大きな値になっていました。

対処は、launchctl kickstart -kによるプロセスの再起動です。再起動後、軽いchat completionを送ったところ、finish_reason:"length"、応答時間0.6秒程度で、正常に応答が返るようになりました。

ここで気になるのは、config変更とハングの因果関係です。Hermes側のconfig.yamlはクライアント側の設定であり、サーバー機の状態に直接影響するものではありません。
そのため、今回のハングは、config変更そのものが引き金というより、過去の長文ingestで蓄積したswapのような、既存の問題がたまたまこのタイミングで表面化した可能性が高いと考えています。

この1件だけを根拠に、「max_tokensの設定は危険だ」といった一般的な結論を出すつもりはありません。応答が返らなくなった場合の一次対応、つまりプロセスの状態を確認し、再起動し、軽いリクエストで復旧を確かめるという手順を押さえておけば、当面は十分です。根本原因の詳しい調査は、次の機会にまとめます。

まとめ:結局どうすればいい?

ここまでの内容を整理します。

  • OptiQは、Mac mini M4 Pro 64GB・64K context・2026年6月29日の検証条件下で、約1.85倍速でした
  • 決め手は、per-layer混合精度KVと、Qwen3.6-35B-A3Bのハイブリッド注意機構という構造的な理由です
  • MTPは試しましたが、この構成では速度の利得がなく、採用しませんでした
  • 「出力が途中で切れる」は、finish_reasonを見れば、max_tokens不足かKV cache逼迫かを機械的に切り分けられます
  • 128Kや256Kのcontextは、まだ実測できていません。段階的な検証がこの先の課題です

速度や長いcontextへの対応は、実測データをもとに判断してください。出力が切れる症状に出会っても、慌てずfinish_reasonから切り分ければ、対処の方向はすぐに見えてきます。

最小権限構成の考え方は、既存記事でも扱っています。

Tailscale Grantsまわりの基礎は、こちらの記事を参照してください。

よくある質問

OptiQとDWQ、どちらが速いの?

Mac mini M4 Pro 64GB・64K contextという検証条件では、OptiQがDWQ比で約1.85倍速でした。ただし、環境が異なれば結果も変わります。

OptiQはなぜ速いの?

per-layer混合精度KVと、Qwen3.6-35B-A3Bのハイブリッド注意機構(40層中10層だけがKVを保持する構造)という、2つの理由によるものです。

speculative decoding(MTP)は効果があるの?

この構成での実測では、速度の利得はほとんどありませんでした(56.1→55.8 tok/s)。モデルやタスクによって結果が変わる可能性はあるため、自分の環境で実測することをおすすめします。
(参考: Qwen3.6-35B-A3B-OptiQ-4bit モデルカード

Hermes Agentの応答が途中で切れるのはなぜ?

出力token上限(finish_reason:length)か、長context時のKV cacheのメモリ逼迫のどちらかです。finish_reasonを確認すれば、機械的に切り分けられます。

finish_reasonがlengthになったらどうすればいい?

max_tokensをHermes側のconfig.yamlに明示的に設定するか、タスクを分割してください。Qwen3.6のthinkingが原因の場合は、enable_thinkingをfalseにします。

KV cache設定はどう作ればいい?

optiq kv-cacheで生成し、wrapperスクリプトの--kv-configに反映します。いきなり大きなcontextを試さず、段階的に検証してください。

128Kや256Kのcontextは使えるの?

2026年7月時点では、77.6Kまでの実測にとどまっています。128Kと256Kは、まだ検証できていません。