概要
現状、Qwen3.8 Flash-Next を Qwen Code などのバックエンドとして動作させる場合は、ランタイムとして omlx を利用するのが良いだろうという結論になった。
Qwen3.8 Flash-Next を llama と mlx_vlm で動かすと、会話やコードが長くなるにつれて、回答の生成が遅くなる問題にあたり、 Qwen Code が使い物にならなかった。
ランタイムは、モデルを実際に動かすプログラムである。トークンは文字や単語を細かく区切った単位で、tok/sは1秒に生成できるトークン数を表す。
速度を「入力を読む時間」と「回答を書く時間」に分けて測ると、mlx_vlm 0.6.17 は短い入力で約21 tok/sだった生成速度が、約5Kトークンで2.4 tok/sまで低下した。一方、同じMac上で OMLX 0.6.4 を使うと、約109Kトークンでも17.72 tok/s の速度をキープした。
最終的に、Qwen Code → OMLX → Qwen3.8 Flash-Nextという構成で、74,460トークンの入力を含む処理が完了した。初回の生成速度は19.8 tok/sだった。
検証コードと測定記録はこのリポジトリにある。
数値はMac Studio M3 Ultra・256GBでの測定結果。各点は原則1回の測定で、ばらつきは調べていない。3ランタイムではモデルを小さく保存する方法(量子化)も同じではないため、処理系だけを完全に同条件で比べた結果ではない。
入力Token数が増えると遅くなる
今回の問題は、入力が長くなると decode まで崩れる点にあった。入力を読む時間が増えるだけでなく、回答そのものが約9分の1の速度になった。
Qwen Codeまでつなぐ構成
Qwen Code は、ターミナルから使うコーディングエージェントである。OpenAI APIと同じ形式の接続先を指定できるため、クラウドのAPIだけでなく、ローカルで動くOMLXもバックエンドにできる。
検証時の構成は次の通り。
| 項目 | 使用したもの |
|---|---|
| マシン | Mac Studio M3 Ultra / 256GB / 819GB/s |
| モデル | Jundot/Qwen3.8-Flash-Next-oQ4e-mtp |
| モデル形式 |
qwen4_exp / oQ4e(OMLX向け量子化)/ MTP重み入り |
| 推論サーバ | OMLX 0.6.4 |
| コーディングエージェント | Qwen Code 0.23.0 |
| 最大コンテキスト | 262,144トークン |
モデルのダウンロードは約99GB、実行時の常駐メモリは約98GBだった。検証リポジトリでは128GB以上のユニファイドメモリを目安としている。
OMLXとモデルを準備する
必要なのはApple Silicon搭載Mac、128GB以上を目安とするユニファイドメモリ、Homebrew、uv、約110GBの空き容量である。
まず検証用リポジトリを取得し、Python環境を作る。
git clone https://github.com/aRaikoFunakami/qwen3_8_flash_next_with_qwen_code.git
cd qwen3_8_flash_next_with_qwen_code
uv sync
OMLXは第三者のHomebrew tapから導入する。
brew tap jundot/omlx https://github.com/jundot/omlx
brew install jundot/omlx/omlx
Qwen3.8 Flash-NextのOMLX向けモデルを取得する。
uv run hf download Jundot/Qwen3.8-Flash-Next-oQ4e-mtp \
--local-dir ~/.omlx/models/Qwen3.8-Flash-Next-oQ4e-mtp \
--max-workers 8
検証ではMTPを無効にした。ファイルがない場合は、次の内容で ~/.omlx/model_settings.json を作る。既存の設定がある場合は、内容を消さずに models へ対象モデルの項目を追加する。
{
"version": 1,
"models": {
"Qwen3.8-Flash-Next-oQ4e-mtp": {
"mtp_enabled": false
}
}
}
設定はサーバ起動時に読まれる。変更後はOMLXを再起動する。
omlx serve --model-dir ~/.omlx/models --port 8000 --log-level info
別のターミナルからモデル一覧を取得できれば、APIは起動している。
curl -s http://127.0.0.1:8000/v1/models
OMLX 0.6.4では、Qwen3.8 Flash-Next向けのprefill・decode処理やprefix cacheの修正が入っている。変更内容はOMLXのリリースノートで確認できる。
Qwen CodeをOMLXへ向ける
Qwen CodeをHomebrewで導入する場合は次のコマンドを使う。
brew install qwen-code
qwen --version
検証値を比べる場合は、表示されたバージョンが検証時の 0.23.0 と同じかを確認する。異なる版では、システムプロンプトやツール定義の長さが変わり、OMLXへ送られるトークン数も変わる可能性がある。
Qwen Codeを起動し、接続先を明示する。
qwen \
--auth-type openai \
--model Qwen3.8-Flash-Next-oQ4e-mtp \
--openai-api-key dummy \
--openai-base-url http://127.0.0.1:8000/v1
APIキーはローカルのOMLX向けのダミー値で、外部サービスの鍵ではない。作業したいプロジェクトのディレクトリでこのコマンドを実行すると、そのディレクトリを対象にQwen Codeが起動する。
毎回オプションを書く代わりに、環境変数でも接続先を指定できる。
export OPENAI_BASE_URL=http://127.0.0.1:8000/v1
export OPENAI_API_KEY=dummy
export OPENAI_MODEL=Qwen3.8-Flash-Next-oQ4e-mtp
qwen
RustのHello Worldを作成した結果
空のディレクトリを作り、前節のコマンドでQwen Codeを起動した。
mkdir hello_rust
cd hello_rust
入力した指示は次の一文である。
Hello, World のプログラムをRustで作成しなさい
Qwen Code 0.23.0はディレクトリとRustの実行環境を確認し、Cargo.toml と src/main.rs を作成した。最初に書いた文字列は Hello, World だったが、続く編集で Hello, World! に変更した。
その後、Qwen Codeが cargo run を実行した。ビルドは終了コード0で完了し、Hello, World! が出力された。画面下部からもう一度 cargo run を実行した場合も、同じ出力になった。
これはモデルの能力を測るベンチマークではない。Qwen CodeからローカルのQwen3.8 Flash-Nextへ接続し、ファイルの作成、編集、コマンド実行まで進められることを確認した記録である。
74KトークンをQwen Codeで処理した結果
長いコードを確実に入力へ入れるため、800個の関数を持つ155KBのPythonファイルを生成した。末尾には SENTINEL_final_gateway という名前の関数を置いた。
uv run python scripts/make_bigfile.py bigmodule.py 800
scripts/run_qwen_code.sh bigmodule.py 8000 Qwen3.8-Flash-Next-oQ4e-mtp
生成したファイル自体は約38.7Kトークンだった。Qwen Codeのシステムプロンプトとツール定義が加わり、OMLXが受け取ったプロンプトは74,460トークンになった。
回答は関数数を800と数え、末尾の関数名 SENTINEL_final_gateway も返した。これにより、少なくとも入力の末尾が切り捨てられず、回答に使われたことを確認できた。
初回は178トークンを生成し、サーバ処理時間は293.39秒、decodeは19.8 tok/sだった。時間の多くは最初に入力を読む処理である。
同じ文脈を使った2回目は、Qwen Codeがツール呼び出しと最終回答の2リクエストを送った。サーバログ上の時間は17.68秒と11.03秒、合計28.71秒だった。Qwen Code全体の壁時計では約31秒である。OMLXのログには73,728トークン分のprefix cacheを利用した記録が残った。
decodeだけを測るdelta法
入力を読む時間と回答を書く時間を分けるため、同じプロンプトに対して max_tokens=32 と max_tokens=160 を測った。両者の時間差には、追加で生成した128トークン分の時間が残る。
decode tok/s = (160 - 32) / (t160 - t32)
各測定の前には短いリクエストを送り、二つの測定が同じキャッシュ状態になるようにした。実装はbench/bench_omlx2.pyにある。
uv run python bench/bench_omlx2.py \
http://127.0.0.1:8000 \
Qwen3.8-Flash-Next-oQ4e-mtp \
40,2000,8000,32000
OMLX・MTP offの結果は次の通り。
| 実際のプロンプト長 | decode tok/s |
|---|---|
| 71 | 22.09 |
| 14,580 | 19.92 |
| 78,956 | 18.29 |
| 108,956 | 17.72 |
上限に近い範囲は、長いタイムアウトを使う別スクリプトで測った。
uv run python bench/bench_long.py \
http://127.0.0.1:8000 \
Qwen3.8-Flash-Next-oQ4e-mtp \
40000,56000,68000
| 実際のプロンプト長 | 上限に対する割合 | decode tok/s |
|---|---|---|
| 138,956 | 53% | 17.18 |
| 198,956 | 76% | 14.71 |
| 243,956 | 93% | 10.35 |
上限へ近づくと速度は下がる。ただし、mlx_vlm で約5Kトークン時に見られた2.4 tok/sへの急落とは形が異なる。243,956トークンでも10.35 tok/sだった。
MTPを無効にした理由
MTPは、一度の計算で複数トークンを予測する仕組みである。サーバログには約2 tok/cycleと出たが、候補を確認する計算も増えた。
同じ71トークンで直接比べられた点では、MTP onが20.45 tok/s、offが22.09 tok/sだった。約109Kトークンではonが14.71 tok/s、offが17.72 tok/sだったが、中間の測定点は一致していない。
このため「MTPは常に遅い」とは判断できない。temperature=0のカウント継続という今回の条件では、decodeが速くなった証拠が得られなかったため、再現構成ではoffを選んだ。
この構成でも残る待ち時間
OMLXへ替えても、長い入力を初めて読む時間は消えない。74Kトークンの初回処理では約4.9分、244K付近の未キャッシュprefillでは約4〜5分かかった。同じ先頭部分を再利用できる処理はprefix cacheで短くなる。
Qwen Codeは、入力したファイルだけでなく、システムプロンプトとツール定義もサーバへ送る。今回の38.7Kトークンのファイルは、サーバ側で約74.5Kトークンになった。モデルの上限を考えるときは、この追加分も含める必要がある。
中断したQwen Codeのプロセスが残ると、OMLXへ別のリクエストを送り続ける場合がある。検証では、この状態で別の測定が約5 tok/sまで低下した。速度が急に変わったときは、別ターミナルで残っているプロセスを確認する。
pgrep -fl qwen
対象を確認してから停止する。別のQwen Code作業までまとめて止めないため、PIDを見ずに一括終了する操作は避ける。
詳細な生ログ上の判断、失敗した測定方法、量子化の違いはdocs/BENCHMARK_REPORT.mdに、Qwen Codeの検証結果はdocs/QWEN_CODE_OMLX_VALIDATION.mdに残してある。




