こんにちは、皆さん。
MacでLLMを動かす方法は増えましたが、PyTorchのモデルをApple Silicon向けに書き出し、軽いruntimeで実行する経路はまだ発展中です。実際にどこまで速くなり、4-bit量子化で出力は変わるのでしょうか。
さて、今日は2026年5月に公開された、ExecuTorchの実験的なMLX delegateを取り上げます。PyTorchモデルをApple SiliconのGPUで動かせる仕組みです。ExecuTorch 1.3.1でQwen3-0.6Bを動かし、PyTorch MPSと比較します。
先に結果を書くと、文章を続けて生成するdecode速度は、PyTorch MPS BF16の41.8 token/sに対し、MLX BF16が134.8 token/s、MLX INT4が188.9 token/sでした。MLX INT4は4.52倍高速で、ファイルもBF16より71.8%小さくなりました。ただし、INT4は簡単な3問のうち2問で生成結果が変わりました。
ExecuTorch MLX delegateとは
ExecuTorchは、学習済みPyTorchモデルをPC、スマートフォン、組み込み機器などで推論するためのruntimeです。推論は、学習済みモデルへ入力を渡して答えを得る処理を指します。
MLX delegateは2026年5月18日に公開されました。PyTorchの計算graphをAppleのMLXへ渡し、Apple SiliconのGPUで実行します。BF16、FP16、FP32と2/4/8-bit量子化などに対応しますが、現時点ではexperimentalです。APIや対応範囲は変わる可能性があります。
量子化は、重みを少ないbit数で表して、容量や計算量を減らす方法です。今回は通常のBF16と、linear layerとembeddingを4-bitへ縮めたINT4を比較しました。
使用したQwen3-0.6B
Qwen3は、Qwen Teamが2025年4月29日に発表したLLMシリーズです。考える過程を使うthinking modeと、短く答えるnon-thinking modeを1つのモデルで切り替えられ、100以上の言語・方言に対応します。
今回使ったQwen3-0.6Bは、その中でも小さいdenseモデルです。denseは、入力ごとに一部だけを使うMoEと違い、基本的にモデル全体を使って計算する方式です。公称0.6B、つまり約6億parameter、28 layer、context lengthは32,768 tokenです。tokenは文章をAIが処理する細かな単位です。
検証ではthinking modeを無効にしました。モデルのrevisionはc1899de289a04d12100db370d81485cdf75e47caへ固定しています。
ライセンス
| 対象 | ライセンス |
|---|---|
| Qwen3-0.6Bの重み | Apache License 2.0 |
| ExecuTorch | BSD 3-Clause License |
| MLX | MIT License |
| PyTorch | BSD 3-Clause License |
いずれも商用利用を含めて利用しやすいOSSライセンスですが、再配布時の著作権表示など、それぞれの条件は残ります。利用時にはリンク先の原文も確認してください。
3つの経路とデータの流れ
使った学習済みモデルはQwen3-0.6Bの1つです。同じ重みを3つの実行経路で比べました。
同じprompt
-> tokenizer: 文章をtoken IDへ変換
-> Qwen3-0.6B
├─ ExecuTorch MLX BF16 -> .pte -> MLX / Metal GPU
├─ ExecuTorch MLX INT4 -> 4-bit .pte -> MLX / Metal GPU
└─ PyTorch MPS BF16 ----------------> MPS / Metal GPU
-> greedy生成: 毎回もっとも確率の高い次のtokenを選ぶ
-> tokenizer: token IDを文章へ戻す
.pteはExecuTorch用に書き出したmodel fileです。MLX経路では計算graph全体を1つのMLX subgraphへ変換できました。PyTorch MPS BF16は、同じBF16重みを通常のTransformers/PyTorch経路で動かす比較対象です。
今回検証する内容
- Qwen3-0.6Bのgraph全体をMLXへ変換して実行できるか
- MLX BF16とPyTorch MPS BF16が同じtokenを生成するか
- INT4でmodel size、速度、process memoryがどう変わるか
- 公開wheelだけで環境を再現できるか
完全なコードとJSON reportは、kiarina/labsのexecutorch-mlx-qwen3 labで公開しています。
検証環境の再現
Apple Silicon Mac、Xcode Command Line Tools、mise、uv、インターネット接続が必要です。初回はQwen3の重みを取得し、合計約1.53 GBのPTEを生成します。
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/kiarina/labs.git
cd labs
git sparse-checkout set .mise/tasks 2026/07/22/executorch-mlx-qwen3
mise trust . && mise trust 2026/07/22/executorch-mlx-qwen3
mise -C 2026/07/22/executorch-mlx-qwen3 run
個別に実行する場合は、次のtaskを使えます。
mise -C 2026/07/22/executorch-mlx-qwen3 run export
mise -C 2026/07/22/executorch-mlx-qwen3 run benchmark
検証条件
machine: MacBook Pro (Apple M1 Max, 32 GPU cores, 64 GB)
OS: macOS 26.5.2
Python: 3.13.7
ExecuTorch: 1.3.1
PyTorch: 2.12.1
Transformers: 4.56.1
model: Qwen/Qwen3-0.6B
generation: greedy、batch 1、最大16 token
PTE: custom MLX SDPA / KV cache、最大sequence 128を指定
速度は同じ日本語promptから16 tokenを強制生成し、warm-up後に5回測った中央値です。prefillは入力をまとめて読み、最初のtokenを出す処理です。decodeは、その後に1 tokenずつ続きを作る処理です。
各backendは独立processで実行しました。MLXでは試行ごとにforward methodを読み直してKV cacheを初期化し、PyTorchでも試行ごとに新しいcacheを作っています。
検証結果
PTEの容量
| PTE | Size | BF16比 | Export time | SHA-256 |
|---|---|---|---|---|
| MLX BF16 | 1,192,264,196 bytes | 100.0% | 41.61 s | 83da47c2…bfb8c0 |
| MLX INT4 | 335,662,976 bytes | 28.2% | 54.55 s | 0e30a054…71267 |
INT4はBF16より856,601,220 bytes、71.8%小さくなりました。量子化には時間がかかるため、export自体はINT4の方が約13秒長くなっています。
生成速度
入力は「Apple Silicon上のローカル推論について短く説明してください。」です。
| Backend | Load | Prefill中央値 | Decode中央値 | 16 token合計 | Peak RSS増加 |
|---|---|---|---|---|---|
| ExecuTorch MLX BF16 | 0.002 s | 0.020 s | 134.8 token/s | 0.131 s | 1.27 GiB |
| ExecuTorch MLX INT4 | 0.003 s | 0.028 s | 188.9 token/s | 0.108 s | 0.47 GiB |
| PyTorch MPS BF16 | 0.726 s | 0.038 s | 41.8 token/s | 0.396 s | 0.14 GiB |
decodeはMLX BF16がPyTorch MPS BF16の3.22倍、MLX INT4が4.52倍でした。INT4はMLX BF16より1.40倍高速で、RSS増加も約63%小さくなりました。
ただし、この表のloadはMLXではPTE programを開く時間だけです。weightがGPUで使える状態になるまでのすべての準備時間ではありません。またRSSはprocessのmain memoryを見た値で、GPU memoryそのものではありません。PyTorch側は測定終了時のMPS driver allocated memoryが1.20 GiBでした。backend間のmemoryを同じ基準で比較できていないため、PyTorchが最も省memoryだとは結論づけられません。
初回だけはMLX BF16のprefillが0.434秒、INT4が1.014秒かかりました。Metalの準備や初回compileを含むcold startは、warm-up後よりかなり遅くなります。
生成結果
3つの短いpromptで、生成したtoken列を比較しました。
| 質問 | PyTorch MPS BF16 | MLX BF16 | MLX INT4 |
|---|---|---|---|
| 日本の首都を一語で | 日本の首都は、**大阪**です。 |
完全一致 | 日本の首都は、**东京**です。 |
| 1+1を数字一文字で | 1+1=2 |
完全一致 | 1 |
MLXをそのまま複写 |
MLX |
完全一致 | 完全一致 |
MLX BF16は3件すべてでPyTorch MPS BF16とtoken単位で一致しました。少なくともこの範囲では、実行経路をMLXへ変えたことによる差は見つかりませんでした。
INT4の完全一致は1件だけです。量子化は数値を粗く表すため、次のtokenの順位が近いと生成結果が変わることがあります。
さらに、首都の回答はBF16もINT4も誤っています。これは0.6Bという小型モデルの品質、prompt、greedy生成などが影響し得ますが、今回の3問だけで原因は特定できません。この確認はbackend間の差を見るためのもので、モデルの知識や回答品質を保証する試験ではありません。
結果を簡単に読む
-
同じ小型LLMを、MLX経路でかなり速く動かせました。
文章の続きを作る速度は、BF16でもPyTorch MPSの3.22倍でした。PyTorchモデルをExecuTorchへ書き出し、Mac向けの軽いruntimeで使う経路として有望です。
-
4-bit化は小さく速くなりますが、答えが変わることがあります。
INT4は約1.19 GBから約336 MBへ縮み、decodeも最速でした。一方、わずか3問でも2問のtoken列が変わりました。実際の用途に近い問題集で品質を測ってから選ぶ必要があります。
-
速度差をMLX kernelだけの効果とはいえません。
比べたのはExecuTorch MLX pipelineとTransformers/PyTorch MPS pipelineです。runtime、cache、実行方法も異なるため、結果はpipeline全体の差です。
試行中にわかったこと
executorch==1.3.1は依存条件上PyTorch 2.13.0も選べましたが、公開済みのExecuTorch extensionをimportするとmaterialize_cow_storage symbol不足で失敗しました。PyTorchを2.12.1へ固定すると動作しました。失敗は次のtaskで再現できます。
mise -C 2026/07/22/executorch-mlx-qwen3 run probe-torch-2-13
また、ExecuTorch付属のPTE inspectorは、同梱されたflatcが--json optionを認識せず実行できませんでした。graph全体がMLXへ渡ったことはexport時のpartitioner logで確認しています。
制限
- M1 Max 1台、Qwen3-0.6B、batch 1、短い日本語promptだけを測った
- 5回の短時間測定で、発熱、消費電力、ほかのGPU負荷を管理していない
- BF16とINT4だけで、FP16、2/8-bit、Core MLなどは比較していない
- 品質確認は3問だけで、一般的なbenchmarkやperplexityを測っていない
- 最大sequence 128を指定し、長い文章は試していない
- memoryはMLXとMPSで同じ方法のGPU使用量を取得できていない
- cold startは1回だけの観測である
- MLX delegateはexperimentalである
検証後の感想
Qwen3-0.6Bのgraph全体をMLXへ渡し、BF16の生成を変えずにdecodeを3倍以上へ伸ばせたのは良い結果でした。INT4ではファイルが3分の1以下になり、約189 token/sまで出たため、小型モデルをMac上で組み込むときの速度と容量には魅力があります。
一方、4-bit化による出力変化は、簡単な3問でもすぐ見えました。量子化は無料の高速化ではありません。用途ごとの品質テストと一緒に使えば、ローカルの補助機能や応答の速い小型agentにも活かせそうです。