技術 約29分で読めます

oMLX 0.3.9.dev2をM1 Max 64GBで実測、SSD KVキャッシュ・Gemma 4 VLM MTP・DFlash・omlx launch copilot

いけさん目次

oMLX 0.3.9.dev2のリリースノートを読んだ前回の続報として、M1 Max 64GBで実機検証した内容を順次まとめる。
焦点はSSD KVキャッシュ、VLM MTP(26B A4Bと31B Dense両方)、DFlash engine(単発と10ターン会話)、TurboQuant KV量子化、5並列リクエストでcontinuous batchingが効くか、Thinking budget、oQ量子化、specprefill、Qwen2.5-VL 7BとGemma 4 VLMの品質と速度比較、コールドstart誤差、そしてomlx launch がGitHub Copilot / OpenAI Codex / Claude CodeをローカルLLMで本当に立ち上げられるか。検証は計11本。

VLM入力にはWAI-Animaで生成した「かなちゃん」画像3種(短文質問用・長文説明用・OCR寄り)を揃えた。
プロンプトを揃えるためで、深い意味はない。

検証環境

項目
マシンMacBook Pro M1 Max 64GB unified memory
OSmacOS 26.3 Tahoe (Build 25D125)
Python3.13.11(venvに分離)
oMLX0.3.9.dev2(pre-release、ソースからpip install)
本体モデルmlx-community/gemma-4-26B-A4B-it-4bit (15.26GB)
MTP draftmlx-community/gemma-4-26B-A4B-it-assistant-bf16 (0.82GB)
追加で用意したモデルgemma-4-31B-it-4bit + gemma-4-31B-it-assistant-bf16
計測指標TTFT、prefill tokens/sec、generation tokens/sec、generation_duration、prompt_tokens、cached_tokens
計測方法Streaming + stream_options.include_usage=true の最終チャンク

oMLXは起動時に paged SSD cache: ~/.omlx/cache (max: 92.6GB) を確保する(デフォルト)。
今回は誤ってディスクを埋めないように --paged-ssd-cache-max-size 20GB で起動した。

oMLXインストールでハマったところ

最初は素直に pip install omlx==0.3.9.dev2 でいけるつもりが、PyPIには出ていない。
Homebrew formulaは標準tapには無く、brew info omlxNo available formula

$ pip install --pre omlx==0.3.9.dev2
ERROR: Could not find a version that satisfies the requirement omlx==0.3.9.dev2 (from versions: none)
ERROR: No matching distribution found for omlx==0.3.9.dev2

$ brew info omlx
Error: No available formula with the name "omlx". Did you mean mlx?

Releasesには .dmg が2種類(macos15-sequoia / macos26-tahoe)置かれているが、READMEに明記されている通り、macOSアプリ版は omlx CLIコマンドをインストールしない。
今回のように omlx launch copilotomlx serve を直接叩きたい場合は、ソースを取ってきて pip install -e . する。

mkdir -p ~/omlx-test && cd ~/omlx-test
python3 -m venv .venv && source .venv/bin/activate
git clone --depth 1 --branch v0.3.9.dev2 https://github.com/jundot/omlx.git
cd omlx
pip install -e .

これで omlx serve / omlx launch / omlx diagnose が通る。
Pythonは3.13.11、venvに分離した状態でビルド成功(macOS 26.3 Tahoe / M1 Max)。
依存にmlx 0.31.2、mlx-vlm 0.5.0、dflash-mlx 0.1.5.1が同梱される。

ちなみに事前のディスク掃除でつまずいた点もあった。Python依存とMLXモデルキャッシュで合計100GB近く要る環境を想定しないと、いきなりCould not find a version のエラーより先に容量枯渇で死ぬ。今回はHugging Faceキャッシュから古い画像生成モデル類を130GBほど落として進めた。

検証1: SSD KVキャッシュで2回目prefillはどれだけ短くなるか

oMLXはデフォルトでホット層 (RAM) + コールド層 (SSD) のpaged KVキャッシュを有効にしている。今回は同一プロンプトを3回投げる前に /admin/api/ssd-cache/clear でキャッシュをクリアし、1回目をcold、2,3回目をwarmとして計測した。

ストリーミングだとレスポンスの最終チャンクに time_to_first_tokenprompt_eval_durationgeneration_durationprompt_tokens_per_secondgeneration_tokens_per_second が含まれる(非ストリーミングだとnullで返ってくる、これが地味なハマりどころ)。
ストリーミングONかつ stream_options: { "include_usage": true } で発火する。

投げたプロンプト

「長いシステムプロンプト + 短いユーザー質問」の構成。コーディングエージェント用途を模した中身(FastAPI + Postgres + Celery のSaaS、操作ルール、過去インシデント、コードスタイル、ツール定義)を詰め込んだ。

  • 828トークン版: 11個の操作ルール + 4件のインシデント + 6種類のツール定義 + 短いコードスタイル節
  • 1555トークン版: より大規模なバージョン。9層のスタック構成 + 11個の操作ルール + 10件のインシデント + 4言語のコードスタイル + 9種類のツール定義 + アーキテクチャの詳細サブセット

ユーザーメッセージはどちらも /v1/eventstenant_id が “a” で始まるテナントだけ500を返すというトラブルシュートの依頼。max_tokens=80〜100temperature=0 固定。

結果: Gemma 4 26B A4B、828 prompt tokens

指標cold (1回目)warm (2回目)warm (3回目)差分
TTFT2.35s1.77s1.77s-24.7%
prefill tokens/sec352469467+33.0%
generation tokens/sec53.753.753.7±0%
cached_tokens field000

結果: Gemma 4 26B A4B、1555 prompt tokens

指標coldwarm1warm2差分
TTFT3.95s1.28s1.27s-67.6%
prefill tokens/sec39412131228+208%
generation tokens/sec54.454.554.6±0%
cached_tokens field000

読み方

generation tokens/secはどのケースでも54付近で動かない。期待通りで、KVキャッシュはdecodeフェーズには効かない。

prefill側(TTFT)の効きはプロンプト長に強く依存する。
828トークンでは24%短縮、1555トークンでは67%短縮。Claude CodeやCodexのようなツール定義+リポジトリ説明+会話履歴を抱える用途では、コンテキストが伸びるほど2回目以降のTTFTがcoldより速くなる量も増える。

地味な観察ポイント: oMLXは prompt_tokens_details.cached_tokens を返してくれない(常に0)。OpenAI互換クライアントから「キャッシュヒットしたか」を見ても0なので、効いてないように見えてしまう。実際にはTTFTとprompt_tokens_per_secondの改善で確認するしかない。
このフィールドの未実装はリリースノートにも書かれておらず、ベンチを組む側がハマる箇所。

検証2: Gemma 4 VLM MTPオン/オフ

oMLXは vlm_mtp_enabledvlm_mtp_draft_model を設定→モデル再ロードで切り替える。
今回は26B A4B (4bit) を本体、gemma-4-26B-A4B-it-assistant-bf16 をdraftにペアリング。

テスト画像

WAI-Animaで生成したかなちゃん画像3種。
キャラ一貫性は3枚とも保たれていて、サイドポニー+青スクランチー+アホゲ+制服が共通している。プロンプト難易度を変えるための背景・要素差はそれぞれ違う。

01_portrait: カフェ室内でコーヒーカップを持つ半身ポートレート

01_portrait — シンプル構図。半身、カフェ室内、コーヒーカップ。

02_fullbody: 夕方の東京街並みで全身動的ポーズ

02_fullbody — 複雑な背景。夕方の東京街並み、和文ネオン看板、後ろに通行人、振り返り動的ポーズ。

03_ocr: デスクでラップトップ、本棚・付箋・WAIマグ

03_ocr — テキスト要素多め。デスクでラップトップ、本棚・付箋・“WAI”マグ。OCRやテキスト認識を試すために用意。

投げたプロンプト

プロンプト内容
short”What is shown in this image? Answer in one short sentence.”
long”Describe this image in detail. Cover the character, clothing, setting, background, lighting, and overall atmosphere. Use multiple sentences.”
ocr”List any visible text, labels, or written characters in the image. Output as a bullet list.”

temperature=0max_tokens=120 固定。各画像×プロンプトの組み合わせでwarmup後の2回目を採用。

結果(gen_d = generation_duration, gen_tps = generation tokens/sec)

各セルは2回目以降の値(warmup後)。

01_portrait(シンプル構図)

PromptMTPTTFTgen_dgen_tps出力tok差 (gen_tps)
shortOFF0.85s0.38s55.9121
shortON0.85s0.44s48.2021-13.8%
longOFF0.86s2.12s56.59120
longON0.84s2.72s44.05120-22.2%
ocrOFF0.89s0.30s55.9517
ocrON0.81s0.30s56.6817+1.3%

02_fullbody(複雑な背景)

PromptMTPTTFTgen_dgen_tps出力tok
shortOFF0.87s0.33s54.4518
shortON0.79s0.34s52.8618-2.9%
longOFF0.89s2.13s56.26120
longON0.86s2.44s49.25120-12.5%
ocrOFF0.87s2.09s57.41120
ocrON0.84s2.39s50.30120-12.4%

03_ocr(テキスト要素多め)

PromptMTPTTFTgen_dgen_tps出力tok
shortOFF0.84s0.34s56.3219
shortON0.84s0.33s54.2318-3.7%
longOFF0.83s2.09s57.29120
longON0.80s2.69s44.57120-22.2%
ocrOFF1.38s0.31s55.6417
ocrON0.85s0.33s51.2017-8.0%

読み方

M1 Max 64GB + Gemma 4 26B A4B 4bit + assistant-bf16 draft の組み合わせでは、VLM MTPは全パターンで体感速度を悪化させる。
特に120トークン生成のlong descriptionで-22%、長めOCR出力でも-12%。出力が短い(15〜20 tokens)場合は誤差レベル。

これは前回のGemma 4テキストMTP実測で31B DenseとE4Bが遅くなったのと同じ向き。
公式ベンチでは「Gemma 4 image + text requests now decode noticeably faster」と書かれているが、Apple Silicon単発実行(batch=1)では draft model のforward + 検証コストが効きの上回るらしく、逆転する。

TTFT (prefill側) はMTPの有無で差が出にくいのも特徴。MTPはdecodeフェーズに効くタイプの高速化なので、TTFT勝負のSSDキャッシュとは別の軸。

31B Denseで再現するか

26Bだけの偏り(A4BというMoEの特性)か気になったので、31B Denseでも同じ3枚×3プロンプトを回した。

ImagePromptMTP_OFF gen_tpsMTP_ON gen_tps
01_portraitshort14.8612.07-18.8%
01_portraitlong15.1412.50-17.4%
01_portraitocr14.7614.06-4.7%
02_fullbodyshort14.8614.86±0%
02_fullbodylong15.1212.06-20.2%
02_fullbodyocr14.9810.53-29.7%
03_ocrshort14.8414.47-2.5%
03_ocrlong15.0911.65-22.8%

26B A4B同様、31B Denseでも長文出力ほどMTP_ONで遅くなる。02_fullbody/ocr では-29.7%。MoEだから遅い、Denseだから速い、という非対称ではない。
TTFTは4.4s前後でフラット。decodeだけが負ける構図。

31B Denseのベースgen_tpsは 14-15 tps(26B A4B の56 tpsの1/4弱)。Apple Siliconでdenseで31Bを動かす重さがそのまま出ている。

地味だが重要なつまずきポイント:

  • model.mtp_compatible フィールドはテキスト側MTPを意味していて、VLM MTPの可否ではない。VLM MTPは別フラグ。READMEからは読み取れない。
  • vlm_mtp_enabled=true 設定後はモデルの unload + load が必要。reload経由でないと反映されない。

出力が変わるかどうか(temperature=0で同一プロンプト同一画像を投げて比較)

投機的デコーディング系の高速化はlosslessなのが普通。temperature=0なら本体モデルだけで生成した結果と、draft+検証で生成した結果が完全一致するはずだ。
実際にmax_tokens=200で 01_portrait の長文説明をbaseline / vlm_mtp_on / dflash_on でそれぞれ2回ずつ流して、ハッシュを取った。

入力baseline (1)baseline (2)vlm_mtp_on (1)vlm_mtp_on (2)dflash_on
テキストのみ (Python関数生成)ca96bf14...(同)ca96bf14...(同)ca96bf14...
01_portrait (長文説明)835c37a9...835c37a9...770090a9...770090a9...835c37a9...
03_ocr (長文説明)6f2620a5...(同)89835b69...(同)6f2620a5...
  • テキストonly: 全パターン一致。MTP/DFlashとも完全にlossless。
  • 画像+テキスト(VLM): dflash_onはbaselineと一致、vlm_mtp_onはbaselineと異なる。同じ設定内では決定論的(run1 == run2)だが、cross-configで差が出る。

01_portrait の長文説明での具体的なdiff:

-An anime-style digital illustration depicts a young woman with light brown hair...
-Her hair is styled in a playful manner, with long bangs framing her face and a small ponytail held by a light blue scrunchie.
+An anime-style digital illustration depicts a young girl with light brown hair...
+Her hair is styled in a playful way, with a prominent cowlick at the top and a side ponytail held by a light blue scrunchie.

-A small potted plant sits on the windowsill, and a hanging lamp provides warm illumination from above.
+A small potted plant sits on the windowsill, and a hanging lamp provides warm indoor lighting.

差分は具体的にこういう所:

  • “young woman” → “young girl”
  • “long bangs framing her face and a small ponytail” → “a prominent cowlick at the top and a side ponytail”
  • “warm illumination from above” → “warm indoor lighting”

面白いことにVLM MTPの方が画像の特徴を正しく拾っている(サイドポニーがちゃんと side ponytail、アホゲがちゃんと prominent cowlick になっている)ケースもある。
ただ、ベンチ目的では「同じ入力で同じ出力にならない高速化」は判断材料として扱いにくい。VLM MTPの受理(accept)判定が緩い、あるいはdraftとmainの数値経路でわずかに差が出ている可能性がある。

dflash_onとtext-only MTPは完全に同じ出力を返す(hash一致)。lossless性の保証はそちら側にある。0.3.9.dev2 pre-releaseのVLM MTPだけ挙動が違う。

正直なところ、ここまで触ってみて「これ本当に動いてる?」感がある。
本来「速くなる」が売り文句のはずが速度は逆方向、本来「temperature=0でlossless」がspeculative decodingの最低限の前提なのに出力が変わる。リリースノートで先頭に置かれてる目玉機能としては挙動が荒い。
pre-release dev2なので品質揃いきってないのは前提、batch>1のサーバー用途・並列リクエスト・もっと長いプロンプトで効くシナリオはあるはず。が、少なくとも今回測った「M1 Max単発、temperature=0、120トークン生成」の範囲では実用に踏み込む段階に見えない。vlm_mtp_enabled はオフのまま触らない方が現状安全。

検証3: DFlash engine vs 通常MLX engine

dflash_enabled のtrue/falseを設定→モデル再ロードで切り替える。
プロンプトはテキストonly(IncidentのPost-mortem書け、約400 tokens入力 / 200 tokens出力)。各設定でwarmup後の2回平均。

投げたプロンプト

「過去のインシデント(イベントingestion パイプラインで500、migration が18分パーティションをロック、Kafka consumer OOM、47分でリカバリ)について、Summary / Impact / Timeline / Root Cause / Detection / Resolution / Action Itemsの構成で 200-word の post-mortem を書け」というテキストonlyのリクエスト。

結果(Gemma 4 26B A4B、warm runs平均)

設定TTFTprefill tokens/secgen_durationgen tokens/sec
baseline (plain MLX)0.69s279.23.54s56.37
DFlash ON0.68s285.53.50s57.11
DFlash + draft FP16 boost0.69s285.73.54s56.55

読み方

単発の200トークン生成では、DFlashの効きは+1.3% gen_tps程度にとどまり、誤差と区別がつかない。
リリースノートを読む限り、DFlash側の旨味は「長いセッションでprefix cacheが churn しにくくなる」「DFlash向けの量子化選択肢」「FP16 draft model boost」あたりに集中している。今回の単発one-shotワークロードでは引き出せていない可能性が高い。

あとunload系の操作中、/admin/api/models/{id}/unload が状態によって400 (Model not loaded) を返すことがある。設定変更が内部で先にunloadを誘発する場合があるので、二重unloadを叩くと400になる。実害はないが、ベンチスクリプトで毎回unload→loadを書くと例外処理が必要。

10ターン会話で再測 (DFlashの本領を見る)

単発one-shotでは差が出ないので、10ターンの会話を実時間で回す形でDFlash ON/OFFを比較した。プロンプトは1ターン目107トークン→10ターン目1229トークンに伸びる。FastAPIのCPU pinned問題のトラブルシュートをモデルに連続質問していくシナリオ。

Turnprompt_tokbaseline TTFTDFlash TTFTbaseline gen_tpsDFlash gen_tpsgen_tps差
11075.79s7.31s56.3257.72+2.5%
22190.67s0.66s55.7257.58+3.3%
33530.89s0.90s54.8356.26+2.6%
44451.11s1.08s55.0156.17+2.1%
55791.37s1.33s52.1954.22+3.9%
67011.58s1.54s52.4053.68+2.5%
78181.75s1.76s52.7253.25+1.0%
89632.04s1.98s50.8852.32+2.8%
910872.41s2.40s54.3854.36±0%
1012290.63s0.64s54.3554.38±0%

平均で+2.3% gen_tps。一貫して正方向に出るが、リリースノートの「不味い churn が消える」みたいなドラマチックな差にはならない。
TTFTは両方ともほぼ同じ。SSDキャッシュ層が共通で効いていて、turn 2以降は前ターン分のprefixキャッシュをほぼ全て再利用できる。turn 10で唐突に prefill_tps が1900超まで跳ねたのはキャッシュヒット率の偶然的な高さによるもの。

DFlashが本当に光るのは恐らく(a) 同時並列リクエスト、(b) prefix cacheが churn しやすい状況(複数会話が同じサーバーに乗る本番運用)、(c) FP16 draft model boostやParoQuantを組んだ構成。今回の単発ユーザー1人10ターンではフラットな改善で着地する。

検証4: TurboQuant KV量子化

KVキャッシュをON-the-flyで量子化することでメモリ使用量を減らす機能。turboquant_kv_enabledturboquant_kv_bits (2/4/8) で切り替える。
442トークン入力+200トークン生成のテキストonlyプロンプトで4パターン比較。

設定cold TTFTwarm TTFTcold gen_tpswarm gen_tps
baseline (no KV quant)1.42s1.04s56.3456.44
TurboQuant KV 8-bit1.37s1.06s56.3656.29
TurboQuant KV 4-bit1.39s1.05s56.0855.26
TurboQuant KV 2-bit1.41s1.06s56.0956.20

全部誤差レベル。442トークンではKVキャッシュ自体が数MBクラスなので、量子化前後でメモリ差も速度差も拾えない。
TurboQuant KVが効くのはおそらく10K+トークンの長コンテキスト時、または同時並列リクエストでKVが切り上がるレベル。今回の単発短プロンプトではテスト失敗。

ちなみに current_model_memory/admin/api/stats で見ようとしたら n/a 返却。設定変更前後でメモリ占有量を確認しようとして詰まる。アプリ用admin UIで見える数字がAPI経由では一部欠ける。

検証5: 並列リクエスト負荷テスト(continuous batching)

oMLX serveは継続バッチング(continuous batching)を謳っている。実際に5並列で同じテキスト生成タスクを投げて、シーケンシャル実行と比較した。プロンプトは1リクエストあたり~30トークン、出力150トークン×5本。

実行方式総wall時間スループット (合計gen_tps相当)
Sequential 5 reqs13.81s~58 tps × 5回
Parallel (concurrency=5)8.42s~20 tps × 5並走

5リクエスト同時投入で1.64倍のスループット改善。1リクエストあたりのgen_tpsは58→20へ落ちるが、5本同時に進むので全体時間が13.81s→8.42sに短くなる。
DFlash ONでも同じ1.64倍で、追加効果は無し。今回のシナリオでは continuous batching そのものが効いている。

DFlashが本番運用で語られるのは「より多くの並列度(10/20/50並列)」「より長いプロンプト(10K+トークン)」「キャッシュ churnが激しい状況」の組み合わせなので、5並列短プロンプトでは引き出せていない可能性。

検証6: Thinking budget(Gemma 4では動かない)

enable_thinkingthinking_budget_tokens という設定がある。Qwen3.x系のような思考モデルなら効くはずなので、Gemma 4 26B A4Bでフィールドを有効化してみた。

設定出力
baseline通常のテキスト出力(400 tokens、gen_tps 57.9)
enable_thinking=true空出力 (400 tokens生成されたが空)
thinking_budget=200空出力
thinking_budget=1000空出力

Gemma 4はthinking出力をネイティブサポートしていないので、thinkingモードを有効にすると生成された全トークンが「内部思考」として扱われ、ユーザー向けには空が返る挙動。
oMLX側で「このモデルはthinkingに対応してない」とユーザーに警告する仕組みはなく、無音失敗する。設定UIで触れる箇所にあるので、知らずに有効化すると延々と空返答が続く。

検証7: oQ量子化(assistant 4bitへ)

dev2の目玉のひとつ、oQ sensitivity measurementをassistant-bf16 (800MB) に対して試した。本体の26B/31Bだとbf16が必要でディスクが厳しいので、まず軽い方で動作確認。

$ curl -s -b cookie -X POST 'http://127.0.0.1:8765/admin/api/oq/start' \
  -H 'Content-Type: application/json' \
  -d '{"model_path":"~/.omlx/models/mlx-community/gemma-4-26B-A4B-it-assistant-bf16","oq_level":4,"text_only":true}'

{"success":true,"task":{"task_id":"c5225f40-...","output_name":"gemma-4-26B-A4B-it-assistant-oQ4",
"output_path":"~/.omlx/models/gemma-4-26B-A4B-it-assistant-oQ4","status":"pending"}}

estimate endpointで事前見積もり: 入力800MB bf16 → 出力235MB (oq_level=4)、量子化中のメモリピーク6.2GB。effective bpw=4.7。

タスクをスタートすると status: pending → in progress で動き始めるが、すぐ status: failed で止まった。

{
  "status": "failed",
  "progress": 15.0,
  "phase": "Loading model...",
  "error": "oQ4: sensitivity measurement produced no scores. Check the preceding log lines for the root cause (model load, calibration data, or layer discovery), and either fix it or pass an explicit sensitivity_model_path."
}

エラーメッセージから、sensitivity measurement (oQの感度測定) が「スコアを返さない」結果に終わっている。
assistantモデルはMTP用のドラフトで、num_layers: 4 しかない(メインの26B / 31Bは数十層)。oQはこの層数では感度の山と谷を取れない。つまりoQはdraftモデルには使えない。

本来のoQの動作確認をするなら、bf16の本体モデル(gemma-4-26B-A4B-it-bf16 や gemma-4-31B-it-bf16)を別途用意する必要がある。今回は時間とディスクの都合でそこまで踏み込まず、estimate endpointとstart endpointの存在、エラー時の振る舞いまでで止める。

ちなみにdev2のリリースノートで「oQ proxy auto-build when model exceeds RAM」と謳われていた auto_proxy_sensitivity フィールドはstart時にデフォルトtrueになっているが、今回のケースは「メモリ不足」ではなく「層が足りない」ので別の救済対象。両方ともpre-releaseで挙動が荒い印象。

検証8: specprefill(おまけ、動かなかった)

検証3で触れたDFlashとは別に、oMLXには specprefill_enabled という設定がある。同じspeculativeでもprefill側を加速するという話で、SSDキャッシュと並んでTTFT短縮側の選択肢になりそうだったので試した。
26B A4Bを本体、gemma-4-26B-A4B-it-assistant-bf16をdraftにペアリング、specprefill_keep_pct を0.5 / 0.3 で2パターン。Gemma 4テキストMTPで使った同様の長プロンプト(866 tokens)。

設定cold TTFTwarm TTFTcold prefill_tps
baseline7.99s1.88s108.4
specprefill ON (keep_pct=0.5)7.48s1.86s115.8
specprefill ON (keep_pct=0.3)7.33s1.91s118.2

数字だけ見るとほぼ誤差。warmはSSDキャッシュが完全に支配しているのでpoint外れ。coldで2〜8%程度の改善に見えるが、それすらサーバーログで失敗が確認できる:

2026-05-13 18:27:06,561 - omlx.engine.vlm - ERROR - SpecPrefill: draft model load failed: 404 Client Error.
2026-05-13 18:27:38,074 - omlx.engine.vlm - ERROR - SpecPrefill: draft model load failed: 404 Client Error.

specprefill_draft_modelで指定した gemma-4-26B-A4B-it-assistant-bf16 がHugging Faceからのダウンロードを試そうとして失敗している(既にローカルにある)。
ERRORログは出るがリクエスト自体は通る = フォールバック実装で本体モデルだけで処理されてる。つまりspecprefillは事実上未動作のまま、本体モデルだけで走った結果がbaselineと同じになるのは当然。
2〜8%の差は単に2回目以降のキャッシュ状態のゆらぎ。

リリースノートにspecprefillの記載は無いが、設定フィールドとしては specprefill_enabled / specprefill_draft_model / specprefill_keep_pct / specprefill_threshold が存在する。Pre-releaseで未完成機能が混ざっていると考えるのが妥当。触らないのが安全。

検証9: Qwen2.5-VL vs Gemma 4 VLM(Ollama vs oMLX 比較)

ローカルVLMの選択肢としてOllamaのqwen2.5vl:7b(6GB)と、oMLXのGemma 4 26B A4B 4bit(15GB)を同じ画像3枚で比較した。プロンプトはVLM MTPテストで使った長文説明(“Describe this image in detail…”)、max_tokens=200、temperature=0。

Imageバックエンド所要時間出力char数備考
01_portraitoMLX Gemma 4 26B A4B4.32s975warmupで画像エンコード済み
01_portraitOllama Qwen2.5-VL 7B24.73s790
02_fullbodyoMLX Gemma 4 26B A4B90.12s1000画像エンコードcold、TTFT 86.6s
02_fullbodyOllama Qwen2.5-VL 7B30.75s1024
03_ocroMLX Gemma 4 26B A4B88.53s942同上、TTFT 85.0s
03_ocrOllama Qwen2.5-VL 7B31.15s942

速度の構造

Gemma 4 26B A4Bは初めて見る画像で画像エンコーダーの起動コストが約85秒かかる(M1 Max 64GB上)。一度warm upしてしまえば同じ画像での再質問は4秒前後。
Qwen2.5-VL 7BはOllama側で各画像24-30秒で安定。MLXのJIT/Metal kernelコンパイルのキャッシュロジックの差か、純粋にモデルサイズの差か、たぶんその両方。

「同じ画像に何度も質問する」用途ならGemma 4が圧倒的に速い(warm 4s vs cold 30s)。「次々違う画像を処理する」用途ならQwen2.5-VLの方が安定して30秒/枚で済む。

出力品質の比較(01_portrait)

両者の描写差:

[Gemma 4 26B A4B]
An anime-style digital illustration depicts a young woman with light brown hair and
large, expressive amber eyes sitting at a wooden table in a cozy cafe. Her hair is
styled in a playful manner, with long bangs framing her face and a small ponytail
held by a light blue scrunchie. She has a gentle, friendly expression with a slight
blush on her cheeks. She is wearing a white, long-sleeved button-down blouse
featuring a small red bow at the collar. Her hands are delicately holding a white
ceramic cup filled with dark coffee, resting on a matching saucer on the table in
front of her.
[Qwen2.5-VL 7B]
The image depicts an anime-style character with light brown hair styled in two
pigtails, each adorned with a blue hair tie. The character is wearing a white blouse
with a red bow tie, giving a school uniform-like appearance. She is holding a white
coffee cup with both hands, suggesting she is enjoying a warm beverage. The setting
appears to be a cozy café with wooden furniture, including tables and chairs, and a
window that lets in natural light. The background includes a chalkboard menu on the
wall and some potted plants, adding to the warm and inviting atmosphere.

Gemma 4は「side ponytail with a light blue scrunchie」「small red bow at the collar」「ceramic cup filled with dark coffee」と詳細を拾う。
Qwen2.5-VL 7Bは「two pigtails」と誤認する(実物は片側のサイドポニーテール、ツインテールではない)。3枚全てで「two pigtails」と書いており、髪型の認識が安定して間違っている。

03_ocrでQwen2.5-VL 7BはWAIマグやApple Logoまで拾ってきていて、OCR寄りはむしろこっちの方が強いケースもある。
26Bと7B、量子化精度、訓練データの違いを抱えての勝負なので一方的でないのは妥当だが、「Macローカルでまずまずの記述精度+OCR」を狙うならQwen2.5-VL 7Bを基準にして、必要があればGemma 4側に切り替えるのが現実線になりそう。

検証10: コールドstart誤差(モデルロード後の初回呼び出しコスト)

実運用でモデルをunload/loadして使い分ける場合、ロード直後の最初の応答がどれだけ遅いかを知りたい。4つのシナリオで測った。

シナリオ1st call wallTTFT
A: 完全warm(モデル既ロード、SSDキャッシュもwarm)0.23s0.18s
B: モデル既ロード + SSDキャッシュclear0.25s0.20s
C: モデルunload→load→SSDキャッシュclear→初回9.79s9.74s
C’: Cの再現(2回目)8.99s8.94s

ロード時間自体: unload 0.9s、load 9.7〜11.9s。
さらにロード直後の初回inferenceで+9〜10秒のwarmup taxが乗る。Metal kernelのJITコンパイルや初回のMLXグラフ最適化に時間を取られているはずで、omlx serveを起動してからすぐ叩くタイプの用途(CIに組み込む、自前ベンチ自動化)ではここを意識しないと「30秒待っても応答が来ない」になる。

合計のcold start budget: 約20〜22秒。
ちなみにSSDキャッシュをclearしてもwarmなモデルへの小プロンプト1発( A → B)はほぼ差がない。検証1で出た大きな効果は、ある程度プロンプトが伸びる前提でしか露出しない。

検証11: omlx launch でローカルLLMをエージェントCLIに繋ぐ

omlx launch list でCLI検出状況が確認できる。

$ omlx launch list
Available integrations:
  claude       Claude Code (installed)
  codex        Codex (installed)
  opencode     OpenCode (not installed)
  openclaw     OpenClaw (not installed)
  pi           Pi (not installed)
  copilot      Copilot CLI (installed)

3大LLMでサポート差を見ると、Anthropic Claude Code、OpenAI Codexは入っている。OpenAI Codexはdev1から、Microsoft GitHub Copilot CLIはdev2で追加。
一方、Google Gemini CLIは未対応(omlx-test/omlx/omlx/integrations/ 配下にgemini.pyすら存在しない)。手元には /opt/homebrew/bin/gemini が入っているので「インストール検出失敗」ではなく実装されてない。Gemini CLI自体の出来は別議論として、3大LLMのCLI対応で1つ穴があるのは把握しておく。

Copilot CLI × Gemma 4 26B A4B

Copilot CLIはdev2の追加機能。npm install -g @github/copilot で入れた後に omlx launch copilot --model gemma-4-26B-A4B-it-4bit --port 8765 --api-key omlxtest で起動する。
内部的にはCopilotの環境変数(COPILOT_PROVIDER_BASE_URLCOPILOT_PROVIDER_WIRE_API=responsesCOPILOT_MODEL など)を組んで copilot バイナリにexecする実装になっている。

ソース (integrations/copilot.py) を見ると、Copilotは responses エンドポイント (/v1/responses) を使う。oMLXはこのエンドポイントもサポートしている。コメントに「Copilot CLI appears to have issues with the completions endpoint, responses appears to work as expected」と書かれている。

copilot -p "..." で非インタラクティブに3回連続でクエリを投げた結果。

Runクエリ入力tokensキャッシュヒット所要時間 (26B A4B)
1”What is 2+2?“26,62701m 28s
2”What is 5*7?“26,62814,330 (54%)33s
3”What is 100/4?“26,62914,335 (54%)34s

Copilot CLIは1回のリクエストで26.6kトークンのシステム/ツール定義をすべて送り込む。本気でローカルLLMのagent backendを試すなら、prefill時間がそのままUX体感に直結する。
oMLX 0.3.9.dev2のpaged SSDキャッシュは、ここで効く。54%がキャッシュヒットして、prefill時間がほぼ半分強消えている。88秒 → 33秒で-63%。

同じCopilot CLI を 31B Dense に向けて投げると、絵が様変わりする:

Runクエリ所要時間 (31B Dense)26B A4B比
1”What is 2+2?“4m 40s3.2x
2”What is 5*7?“3m 16s5.9x
3”What is 100/4?“3m 21s5.9x

31B Denseは1回の応答に3〜5分。SSDキャッシュは効く(run1→run2 で-30%)が、絶対時間が日常使いから遠い。
Copilot CLI を M1 Max 64GB のローカルバックエンドに繋ぐなら 26B A4B が実用線、31B Dense は実用検証用にしか持っていけない、という線引きが立つ。

地味なつまずきポイント:

  • 検証1のchat/completionsエンドポイントでは cached_tokens が常に0だったが、responses エンドポイント(Copilotが使う)はちゃんとキャッシュ済みトークン数を返す。エンドポイントによってメトリクスの埋め方が違うのは把握しておく必要がある。
  • omlx launch copilot 自体はcurses TUI起動でstdinがTTYでないと落ちる(Error: stdin is not a terminal)。CIや並列実行で叩く場合はTUIをスキップする呼び方を選ぶ必要がある。今回は環境変数を手で組んで copilot を直接呼ぶ形で計測した。
  • 初回88秒は実時間で長い。普段Anthropicや本物のCopilotバックエンドに繋いでいる感覚で叩くとタイムアウト設定で死ぬ可能性がある。--api-timeout 系の引数が必要なケースが想定される。

Codex × Gemma 4 26B A4B

Codex (OpenAI) はdev1から対応。omlx launch codex を実行すると ~/.codex/config.toml[model_providers.omlx] セクションを書き込む。
今回はTUI回避のため、config.tomlを手書きで以下のように用意して codex exec で叩いた。

model = "gemma-4-26B-A4B-it-4bit"
model_provider = "omlx"

[model_providers.omlx]
name = "oMLX"
base_url = "http://127.0.0.1:8765/v1"
env_key = "OMLX_API_KEY"

3回連続で投げた結果(SSDキャッシュクリア後)。

Runクエリ入力tokens所要時間
1”What is 6*8?“823.1s
2”What is 7*7?“832.4s
3コード生成(memoizationでFibonacci)1634.4s

Copilotとの差が劇的。Codexは1リクエストたかだか82トークンで、Copilotの26,627トークンに対して300倍少ない。
Copilotがリクエストごとに全ツール定義を送るのに対し、Codexは必要最小限のsystem promptで始めて、tool callが必要になったタイミングで段階的に拡張する設計に見える。
ローカルLLMにエージェントを乗せるとき、Codexの方が体感速度に直結するprefillの圧が圧倒的に低い。

ハマりポイント:

  • Codex起動時に failed to refresh available models: stream disconnected before completion: failed to decode models response: missing field 'models' というERRORが毎回出る。CodexはOpenAI互換でも {models: [...]} 形式を期待するが、oMLXは標準の {object: "list", data: [...]} を返すため。動作には影響しないが、ログは赤くなる
  • codex exec --skip-git-repo-check を指定しないと、gitリポジトリ外では拒否される

Claude Code × Gemma 4 26B A4B

Claude Code (Anthropic) はdev1から対応。omlx launch claude は内部で ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKENANTHROPIC_DEFAULT_OPUS_MODEL/SONNET_MODEL/HAIKU_MODEL などの環境変数を組み、claude バイナリへexecする。

今回は --bare モードで投げた(hooks、LSP、plugin sync、auto-memory等をスキップする最小モード)。

Runクエリ所要時間
1”What is 2+2?“8.22s
2”What is 5*7?“2.07s
3”What is 100/4?“2.03s

サーバーログから推定すると、claude --bare のシステムプロンプトは約1,795トークン。Codexの82トークンより重く、Copilotの26,627トークンより圧倒的に軽い。

CLIプロンプトサイズ目安1回目所要時間2回目所要時間2回目短縮
Codex82 tokens3.1s2.4s-23%
Claude —bare1,795 tokens8.2s2.1s-75%
Copilot26,627 tokens88s33s-63%

--bare を外せばClaude Codeも本来のフルプロンプト(CLAUDE.md読み込み、MCP、hooks等を含む)を送るので、実運用の体感はもっとCopilot寄りになる。今回はサーバー側の挙動を見たかったので最小モードで計測。

ハマりポイント:

  • 環境変数 ANTHROPIC_API_KEY="" を明示的に空にしないと、Claude CodeがAnthropic公式の認証を試そうとして失敗する。omlx launch claude の実装は空文字列を明示的に入れている
  • --bare 指定でも ANTHROPIC_DEFAULT_OPUS/SONNET/HAIKU_MODEL を3種全部設定しないと、内部ルーティングで指定したモデルが使われないことがある

ここまでで見えたこと

実用上の整理:

  • SSDキャッシュは効く。短いプロンプトでも+24%、1.5kトークンで+208%、Claude —bare の1.8kトークンで-75%、26kトークンを抱えるCopilot CLIで初回88秒→2回目33秒(-63%)。コーディングエージェント用途では一番分かりやすく効くMacローカル推論の高速化。
  • VLM MTPは現状オフが安全。26B A4Bでも31B Denseでも全パターンで体感速度を悪化させる(long出力で-17〜-30%)。temperature=0でも出力が変わる。「本当に動いてる?」感が拭えない出来。pre-releaseなのでissueとして妥当。
  • テキスト側MTP・DFlash は出力lossless。DFlashは単発one-shotでほぼゼロ(+1.3%)、10ターン会話で+2.3%程度。長セッションでprefix cache churnを抑えるのが本領だが、5並列リクエストでもDFlash独自効果は見えず、continuous batching自体で1.64倍スループット改善する。
  • TurboQuant KV量子化は442トークンでは差出ず。長コンテキスト・大量並列でメモリ削減として効くはずだが、今回スコープ外。
  • Thinking budgetは Gemma 4では空出力。oMLXは「このモデルはthinking対応してない」を判定せず、設定UIから有効化できてしまう。Gemma 4以外の思考モデル(Qwen3.x系等)で再テストすべき領域。
  • oQ量子化は assistant draft model (4層しかない) では sensitivity measurement が成立せず失敗。本体bf16モデルでないとちゃんと走らない。estimateエンドポイントは正しく動く。
  • specprefillは設定としては露出しているが、gemma-4-26B-A4B-it-assistant-bf16 をdraftに指定するとHFダウンロードで404して無音フォールバックする。事実上未動作。pre-releaseらしいバグ。
  • Qwen2.5-VL 7B (Ollama) vs Gemma 4 26B A4B (oMLX) の比較: 1枚の画像に複数質問するならGemma 4が圧倒的に速い(warm 4s、cold 85s)。次々違う画像を処理するならQwen2.5-VLが安定(24-30s/枚)。記述精度はGemma 4の方が細かい(side ponytail、red bow at collar等)が、Qwen2.5-VL もOCR寄り画像でWAIマグやApple Logoを拾えるので一方的でない。
  • コールドstartは初回inferenceで+9〜10秒のwarmup tax。モデルロード自体10〜12秒。合計約20〜22秒。Metal kernel JITコンパイルが見えている。
  • Copilot CLI × 26B A4B: 1m 28s → 33s (-63%) で実用。31B Denseに切り替えると3〜5分かかり実用線を超える。M1 Max 64GB環境では 26B A4B が現実的なライン。
  • omlx launch はCopilot/Codex/Claude Codeいずれも動く。プロンプトサイズはCodex(~82) / Claude —bare(~1.8K) / Copilot(~26.6K)とCLIごとに桁が違う。同じローカルLLMでもUXの圧がまるで違うので、エージェントCLI選びはトークン量を測ってから決めた方がいい。
  • 3大LLMでGoogle Gemini CLIだけ未対応。omlx-test/omlx/omlx/integrations/ 配下にgemini.pyすら無い。OSS PRが入るのを待つか、自前で書く必要がある。Gemini CLI自体の使い勝手は別議論として、インテグレーションの穴は事実として残る。
  • Macで26B/31Bを動かす前提なら、64GB unified memoryは必要。今回も max_model_memory: 50.4GBmax_process_memory: 56GB がオートで割り当たっていた。32GBだと別のモデル選定が要る。

逆に、これからoMLX 0.3.9.dev2を触る人向けの「先にハマる箇所」リスト:

  • インストールは pip install omlxbrew install omlx も通らない。ソース取得+pip install -e .
  • ディスク。SSDキャッシュ用に92.6GB自動確保される(--paged-ssd-cache-max-size で絞れる)
  • 計測値(TTFT、prefill_tps、generation_tps)はストリーミングONかつ stream_options.include_usage=true でしか入ってこない
  • 管理APIは /admin/api/login + Cookie か、Bearer形式どちらかで認証する。完全に同等ではなく、Bearer単体だと admin endpoints では弾かれる
  • 同名フィールド mtp_compatible はテキスト側のみ。VLM MTPは別フラグ vlm_mtp_enabled
  • 設定変更後は modelを unload → load 。設定変更が内部的に先にunloadを誘発するので、二重unloadで Model not loaded (400) が出る

参考