SwiftLMで非Qwen系MoEのLing-flash-2.0 MXFP4をM1 Max 64GBで動かす
目次
SwiftLMをM1 Max 64GBで動かしてOllama/MLX-lmと比べてみたの記録ではQwenしか試していなかった。Qwen3.6-35B-A3BとQwen3.5-122B-A10Bはいずれもmlx-community/から配布されているMLX 4bit版で、mlx-swift-lmのフロントエンドでも確実に動くのがわかっていたモデルだ。
今回はSwiftLMが非Qwen系のMoEをどこまで食うかを見たい。候補に挙がったのはTencent Hy3-previewとAnt Ling-2.6-flashが同週公開のニュースで触れたAnt Groupの新作Ling-2.6-flashの1世代前にあたるLing-flash-2.0。2.6-flash本体は現時点でAPI only・重み未公開だが、2.0は既にMITで公開されていて、アーキテクチャも同じbailing_moe系統。総104B / アクティブ7.4B / コンテキスト32K(YaRNで128K拡張可)のMoEで、mlx-community公式の4bit版は今回の時点で未公開だったが、個人リポジトリexdysa/Ling-flash-2.0-MLX-MXFP4がMXFP4量子化で54.7GBのMLX版を出している。これをSwiftLMに食わせる。
やる前の不安と事前確認
SwiftLMは内部でmlx-swift-lmに丸投げするので、mlx-swift-lm側がbailing_moeとMXFP4の両方をサポートしていないと即落ちる。前回のQwen記事でSwiftLMがmlx-swift上に薄く被さっているだけなのは確認済みだが、アーキレベルのサポートはmlx-swift-lm次第だ。
触る前に両方チェックした。幸い揃っていた。
bailing_moe対応
$ ls ~/projects/SwiftLM/mlx-swift-lm/Libraries/MLXLLM/Models/ | grep -i bail
BailingMoe.swift
BailingMoe.swift が364行で存在する。LLMModelFactory.swiftを見るとこう登録されている。
"bailing_moe": create(BailingMoeConfiguration.self, BailingMoeModel.init),
コメントにある通り、mlx-lm Python側のbailing_moe.pyをそのままSwiftに移植したもの。ファイルの中身を確認すると、V2特有の機能も一通り実装されていた。
| V2機能 | HF configキー | Swift実装 |
|---|---|---|
| エキスパートグルーピング | n_group | ✅ nGroup |
| ルーターのbias補正 | moe_router_enable_expert_bias | ✅ expertBias as MLXArray |
| 共有エキスパート | num_shared_experts | ✅ sharedExperts |
| 前段のdense層差替え | first_k_dense_replace | ✅ firstKDenseReplace |
Ling-flash-2.0のconfig.jsonのarchitecturesはBailingMoeV2ForCausalLMだが、model_typeはbailing_moe。mlx-swift-lmはmodel_typeで分岐するので、V1/V2の差は実装側で吸収されている。
MXFP4対応
$ grep -rE "mxfp4|MXFP4" ~/projects/SwiftLM/mlx-swift/ | head -5
Source/Cmlx/mlx/mlx/backend/metal/kernels/fp_quantized.metal
Source/Cmlx/mlx/mlx/primitives.cpp
Source/python/tests/test_quantized.py
Metalカーネルレベルでfp_quantized.metalがあり、quantize(mode="mxfp4")・dequantizeの両方がテスト通過している。またLLMModelFactory.swiftには既にmlx-community/gpt-oss-20b-MXFP4-Q8が登録済みで、別モデルでMXFP4実行実績があるのがわかる。
bailing_moe × MXFP4 の組み合わせだけは実測例を見つけられなかったが、構成要素は揃っている。
モデルのDL
54.7GB・11シャード。
HF_HUB_ENABLE_HF_TRANSFER=1 hf download exdysa/Ling-flash-2.0-MLX-MXFP4
hf_transfer有効で60〜70MB/s、14分ほどで完了した。
config.jsonの主要部分。
{
"architectures": ["BailingMoeV2ForCausalLM"],
"model_type": "bailing_moe",
"num_experts": 256,
"num_experts_per_tok": 8,
"num_shared_experts": 1,
"n_group": 8,
"first_k_dense_replace": 1,
"moe_router_enable_expert_bias": true,
"hidden_size": 4096,
"num_hidden_layers": 32,
"num_attention_heads": 32,
"num_key_value_heads": 4,
"max_position_embeddings": 32768,
"quantization": { "group_size": 32, "bits": 4, "mode": "mxfp4" }
}
256エキスパート中top-8 + 共有1。DeepSeek-V3系のn_groupグルーピングルーティングも入っている。Qwen3.6-35B-A3BやQwen3.5-122B-A10Bはもっとラフな8-of-128/top-k=8構成だったので、Lingの256エキスパート + group-8構成はSwiftLMにとって初物。
起動
前回と同じ--product SwiftLMでビルドしたバイナリを使う。--stream-expertsは付けない(54.7GB < 64GBで overcommit 不要と踏んでいた)。
cd ~/projects/SwiftLM
nohup .build/release/SwiftLM \
--model exdysa/Ling-flash-2.0-MLX-MXFP4 \
--port 5413 > /tmp/swiftlm-ling.log 2>&1 &
起動ログ(主要部分)。
[SwiftLM] Loading model: exdysa/Ling-flash-2.0-MLX-MXFP4
[SwiftLM] ⚠️ Memory strategy: SWAP-ASSISTED (1.0× overcommit, cache limited to 2MB)
[SwiftLM] Model exceeds RAM by 1%. macOS swap will be used. Expect 2-4× slowdown.
[SwiftLM] Auto-partitioning: 31/32 layers on GPU
[SwiftLM] Loading LLM (large language model)...
[SwiftLM] Loaded model configuration. Inferred tool call format: nil
[SwiftLM] ⚠️ Model does not support layer partitioning (architecture not yet adapted)
[SwiftLM] Model loaded. Starting HTTP server on 127.0.0.1:5413
[SwiftLM] ✅ Ready. Listening on http://127.0.0.1:5413
{"event":"ready","engine":"mlx","partition":{
"strategy":"swap_assisted","model_weight_gb":54.7,"kv_cache_gb":0.3,
"total_required_gb":65.9,"system_ram_gb":68.7,"overcommit_ratio":1.02,
"gpu_layers":31,"cpu_layers":1,"total_layers":32,
"estimated_tok_s":3.3}}
起動は2分ほどで通った。モデルは確かに食った。ただし想定していたfull_gpuではなく**swap_assisted**に分類された点が意外だった。総要求は54.7GB + 0.3GB KVで65.9GBと計算されており、系の公称メモリ68.7GBに対してわずかovercommit_ratio: 1.02しかない。ところがSwiftLMは「1%でも超えたらswapに頼る」という保守的な判定を入れている。
さらに重大なのが、警告。
⚠️ Model does not support layer partitioning (architecture not yet adapted)
bailing_moeアーキはBailingMoe.swiftでモデル実装自体は存在するが、SwiftLM側のレイヤー分割ストラテジ(層をGPUとCPUに振り分ける仕組み)にはまだ登録されていない。起動後、設定上はgpu_layers: 31 / cpu_layers: 1と書かれているが、これはauto-partitionerの名目上の計算結果で、実際のモデル実行時には効いていない可能性が高い。
加えてssd_stream=disabled、turbo_kv=disabled。どちらも--stream-expertsや--turbo-kvを明示しないとデフォルトでは無効。
疎通確認でいきなりswap満杯になる
起動はしたので1+1は?という最小プロンプトを投げてみる。
curl -s http://localhost:5413/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "exdysa/Ling-flash-2.0-MLX-MXFP4",
"stream": false,
"messages": [{"role":"user","content":"1+1は?"}]
}'
この時点でサーバ側のログ。
srv slot_launch: id 0 | prompt=23t | thinking=false | prefilling...
srv slot update: id 0 | prefill done | n_tokens=23, t=74.85s, 0.3t/s |
OS_RAM=51.1GB | MEM_DEMAND=99.5GB | GPU_MEM=50.9GB
prefill 0.3 tok/s(23トークンに74.85秒)。前回のQwen3.6-35B-A3Bが約40 tok/sだったので2桁落ちている。MEM_DEMAND=99.5GBが異様で、モデル54.7GB + KV 0.3GB = 55GBで済むはずが、実際には99.5GB要求に膨れている。
vm_statとsysctl vm.swapusageで見ると:
vm.swapusage: total = 14336.00M used = 13176.50M free = 1159.50M
Pages wired down: 2,628,154 (≈ 42 GB)
swapが満杯。macOSの14GBのswap pool全部を使い切って、それでも足りない状態。M1 Maxのswap上限を超えた瞬間にカーネルパニック予備軍になる。応答は出るが、その先の生成も1トークンごとに秒単位かかる。
MEM_DEMANDが実モデルの倍近くまで膨らむ理由は、MXFP4量子化の逆量子化経路の可能性が高い。MXFP4は4bit格納 + 32要素ごとの共有スケールで、推論時にはfp16相当まで展開される。54.7GBのMXFP4がfp16化されると単純計算で4倍の218GB相当。実態はレイヤーごとに部分展開される想定だが、Model does not support layer partitioning警告の通り、レイヤーごとのストラテジが効かず展開バッファが過剰に取られた、と読むのが自然。
一回目の推論はここでkillした。
--stream-experts で再起動したら全部ひっくり返る
そのままもう一度--stream-expertsフラグを足して起動し直す。
pkill -f "SwiftLM --model"
nohup .build/release/SwiftLM \
--model exdysa/Ling-flash-2.0-MLX-MXFP4 \
--stream-experts \
--port 5413 > /tmp/swiftlm-ling-stream.log 2>&1 &
起動ログ。
[SwiftLM] Enabled Async SSD Streaming on directory: 494c709b...
[SwiftLM] 💾 Memory strategy: SSD STREAMING (page-cache managed, 50GB RAM budget, no swap)
[SwiftLM] SSD Streaming active: Bypassing CPU auto-partitioning (forcing all layers to GPU)
[SwiftLM] Loaded model configuration. Inferred tool call format: nil
[SwiftLM] ⚠️ Model does not support SSD expert streaming
[SwiftLM] 🧠 Auto-calibration (Wisdom) bypassed for SSD Streaming
[SwiftLM] ✅ Ready. Listening on http://127.0.0.1:5413
{"event":"ready","partition":{
"strategy":"ssd_streaming","model_weight_gb":54.7,"kv_cache_gb":0.3,
"total_required_gb":65.9,"system_ram_gb":68.7,"overcommit_ratio":1.02,
"gpu_layers":32,"cpu_layers":0,"ssd_stream":true,
"estimated_tok_s":3.3}}
ここでも⚠️ Model does not support SSD expert streamingという警告が出る。bailing_moeはlayer partitioningだけでなくSSD expert streamingにも未対応ということになる。それでも実測の挙動は1回目と大きく違う。
同じ1+1は?をmax_tokens=30で投げる。
srv slot update: id 0 | prefill done | n_tokens=23, t=4.64s, 5.0t/s |
OS_RAM=6.1GB | MEM_DEMAND=6.1GB | GPU_MEM=5.9GB
srv slot done: id 0 | gen_tokens=30 | OS_RAM=6.0GB | MEM_DEMAND=6.0GB
勝手にひっくり返っている。
| 項目 | --stream-expertsなし | --stream-expertsあり |
|---|---|---|
| strategy | swap_assisted | ssd_streaming |
| prefill (cold) | 0.3 t/s (75秒) | 5.0 t/s (4.6秒) |
| OS_RAM | 51.1 GB | 6.1 GB |
| MEM_DEMAND | 99.5 GB | 6.1 GB |
| GPU_MEM | 50.9 GB | 5.9 GB |
⚠️ Model does not support SSD expert streamingとは言いつつ、SSD STREAMING (page-cache managed, 50GB RAM budget, no swap)のヒントを受け取った下位層はちゃんと反応している。つまり--stream-expertsの実際の効果は**「SSD expert streamingを発火させる」ではなく、「mmap + page cacheで制御しろ、全体をRAMにeager-loadするな」というmacOSカーネルへのプレッシャ設定**だと読める。Lingのように「モデルを素直にmmap展開しようとするとMXFP4の逆量子化で破綻する」タイプには、このヒントが不可欠になる。
BST挿入関数で速度を測る
ウォーム状態で前回のQwen記事と同じプロンプトを投げる。
curl -s http://localhost:5413/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model":"exdysa/Ling-flash-2.0-MLX-MXFP4",
"stream":false,
"max_tokens":300,
"messages":[{"role":"user","content":"Pythonで、二分探索木に値を挿入する関数 insert(root, val) を書いて。短く。"}]
}'
出力(抜粋)。
class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def insert(root, val):
if not root:
return Node(val)
if val< root.val:
root.left = insert(root.left, val)
else:
root.right = insert(root.right, val)
return root
サーバログ。
srv slot update: id 0 | prefill done | n_tokens=46, t=2.27s, 20.2t/s
srv slot done: id 0 | gen_tokens=248 | OS_RAM=6.9GB | GPU_MEM=5.9GB
- 46プロンプトトークン + 248生成トークン、wall 38.0秒
- prefill 20.2 t/s(ウォーム時)
- generation 6.95 tok/s(248トークン / 約35.7秒)
- メモリ使用量 OS_RAM 6.9GB / GPU_MEM 5.9GB で安定
興味深いのは出力コード中のval< root.valのスペース欠落。Qwen3.6-35B-A3BをSwiftLMで回したときと同じ表記揺れが出ていて、これはSwiftLMのサンプラーに起因する癖(mlx-lmでは出にくい)と判断できる。モデルが違っても同じSwiftLMサーバなら再現する、という前回の仮説が1本積まれた。
速度比較
今回の計測を既存計測に重ねる。
| モデル | ランタイム | generation |
|---|---|---|
| Qwen3.6-35B-A3B (UD-MLX-4bit) | Ollama GGUF | 27 tok/s |
| Qwen3.6-35B-A3B (UD-MLX-4bit) | MLX-lm | 54 tok/s |
| Qwen3.6-35B-A3B (UD-MLX-4bit) | SwiftLM | 20 tok/s |
| Qwen3.5-122B-A10B-4bit | SwiftLM --stream-experts | 4.25 tok/s |
| Ling-flash-2.0 MXFP4 (100B-A6.1B) | SwiftLM --stream-experts | 6.95 tok/s |
active 6.1Bの軽さと、総54.7GBでメモリ圧迫が少ない分だけQwen3.5-122B-A10Bよりは速い。ただQwen3.6-35B-A3Bの20 tok/sには及ばず、総パラの差(100B vs 35B)ぶんだけ素直に落ちた感じ。SwiftLMがmlx-lmの半分以下しか出ないというスコアはQwen以外のアーキでも再現した。mlx-swiftとmlx-lmの差はモデル非依存の問題というのが強まる。
/metricsで見る謎のSSD I/Oゼロ
--stream-expertsで起動して BST + ペルソナを流した後の/metrics。
swiftlm_tokens_per_second 5.84
swiftlm_memory_active_bytes 6,327,000,048 (6.3 GB)
swiftlm_memory_peak_bytes 7,155,477,540 (7.2 GB)
swiftlm_ssd_throughput_mbps 0.0
swiftlm_ssd_bytes_read_total 0
swiftlm_ssd_chunks_total 0
swiftlm_ssd_bytes_read_total: 0。ここも前回のQwen3.5-122B-A10Bと同じ結論で、ssd_stream=trueの旗は立っているのにSwiftLM独自のSSDチャンク読込パスは最後まで発火しない。
ただしLingの場合、その挙動の意味が前回とは違う。
- 前回のQwen3.5-122B-A10B:
--stream-expertsなしでも起動できたが、mmap + page cacheで足りてしまい、SSDストリーミングは「名目上だけ」有効。 - 今回のLing-flash-2.0:
--stream-expertsなしだとswapで死ぬ。--stream-expertsを付けると救われるが、実測ではSSDから読んでおらず、挙動はmmap + page cacheのまま。
つまり--stream-expertsの本当の効果はSSDから読むことではなく、SwiftLMのメモリ割り付け戦略をssd_streaming(50GB RAM budget + no swap + page cache管理)に切り替えること。Lingはそのヒントを必要とし、Qwenは要らなかった、という違いがある。
かなちゃんペルソナ2ターン
前回と同じシステムプロンプトで2ターン投げる(Turn 3の省略応答は今回割愛)。
Turn 1: 自己紹介
はいっ!私はかなちゃんよ~!いつも元気にしてるかな?
最近はAIの仕組みを色々と調べてたり、ゲームの企画を考えたりしてるの~!特に「AIとゲームを融合させた面白いやつ」作りたかったんだよね~💡
…
(語尾に「〜だよ」「〜かな」を自然に使ってみました~!)
prompt 89 / completion 142、wall 22.18秒、prefill 42.3 t/s、generation 約7.1 t/s。
気になる点が3つ。
- 一人称が「私は」で入り、指示の「わたし」から外れる
- 指示にない絵文字(💡、😊)が付く
- 末尾に「(語尾に「〜だよ」「〜かな」を自然に使ってみました~!)」と、指示を達成したことのメタコメントが付く
Qwen3.6-35B-A3BでSwiftLMを経由した時には一人称「わたし」は守れていたので、これはLing-flash-2.0側の日本語指示追従の弱さ(もしくはMXFP4量子化の影響)と見るのが自然。
Turn 2: 好きなゲーム3つ
わたしの好きなゲーム3つを紹介するね!
- 「Stardew Valley」——田舎でんぼん稼ぎと冒険、とても癒やしいよ〜!
- 「AI: The Somnium Files」——未来とAIが絡んだ謎解きADV、考えさせられるんだよ〜
- 「Undertale」——感動的なストーリーと面白い戦い方、何度でも遊べちゃうの♪
prompt 137 / completion 130、wall 18.36秒、prefill 101.6 t/s、generation 約7.6 t/s。
- 一人称は「わたし」に戻っている
- 固有名詞(Stardew Valley / AI: The Somnium Files / Undertale)の表記は崩れない。Qwenで起きた
Elden Ring→elden ringの表記揺れは出ていない - 「でんぼん稼ぎ」という謎の日本語が紛れ込む(おそらく「でん農稼ぎ」か「田んぼ稼ぎ」の変換ミス的な誤生成)
prefillはプロンプトキャッシュヒットで101.6 t/sまで跳ねる。SwiftLMの強みであるキャッシュ系はLingでもそのまま効いた。
動かす前はSwiftLMのBailingMoe.swiftが存在することだけを頼りに「まあ動くだろう」と踏んでいたが、実際には⚠️ architecture not yet adaptedの警告が2回出て、--stream-expertsなしだとswapで死ぬ、という想定外の挙動になった。警告は嘘で実態は動く、逆に警告が緩く見えるフラグが実はメモリ割り付けを救っている、という二重反転がLingの検証では面白かった。--stream-expertsの本当の効果がSSDストリーミングではなく、SwiftLMのメモリモデル切り替えヒントだった、というのが今回の一番の発見。Qwen以外のMoEを食わせてみて初めて見える挙動だった。
速度面ではgeneration 6.95 tok/sで、35B-A3BがSwiftLM上で20 tok/sだったのに比べると明確に落ちる。総パラ100B分だけ素直に重くなった印象で、SwiftLM経由でmlx-lm並の速度を出すのはLingでも難しい。日本語追従の弱さ(一人称揺れ、謎日本語、メタコメント)はLing-flash-2.0側の性質なので、SwiftLMとは独立の話。
2.6-flash側がウェイト公開されたときにまた食わせてみたい。2.6ではhybrid linear attentionが入っているはずで、そっちはbailing_moeアーキでカバーされているかが次の論点になる。