Meta の Muse Glimmer 30B が llama.cpp にマージされたので、手元の Ryzen AI Max+ 395
(Strix Halo)で動かして一通り測りました。
結論を先に書くと、こうなりました。
- 生成速度は 10.7 tok/s(dense 30B、Q4 相当、iGPU)
- 素のままだと最初の1文字が出るまで平均 43.7 秒。CoT が長すぎる
- チャットテンプレートの
reasoning_strengthをlowにすると 15.2 秒まで縮む(-65%) - 画像入力は 2800 トークン(≒220万画素)を超えると必ず壊れる。出力が
2222222…に崩壊して HTTP 500 - 壊れたリクエストの直後、別の画像の内容が次の回答に混入する
- 犯人は llama-server の既定の slot 構成。
--parallel 1で両方とも解決
音声対話に使えるかを見たかったのですが、そこは残念な結果でした。以下、実測値と切り分けの過程です。
環境
CPU/GPU : AMD Ryzen AI MAX+ 395 w/ Radeon 8060S (gfx1151) / VRAM 48GB
OS : Ubuntu 26.04
ROCm : 7.14.0 (/opt/rocm)
llama.cpp: 153d324bc (build 10364) — GGML_HIP=ON, AMDGPU_TARGETS=gfx1151
Muse Glimmer とは何者か
llama.cpp 側の実装(src/models/muse-glimmer.cpp)を読むと、素性はこうです。
- dense 52層の 30B。MoE ではない
- SWA(sliding window attention)+ iSWA KV cache
- QK-norm、attention output gate(sigmoid ゲートを o_proj の前に掛ける)
- final logit softcapping
- 別ファイルの mmproj による vision、さらに DFlash という投機デコード用 drafter 付き
arch 名は muse-glimmer、コンバータは conversion/muse_glimmer.py(HF クラス名
MuseGlimmerForConditionalGeneration)。サポートは
#26841 で入りました。
dense であることが後で効いてきます。普段使っている Qwen3.6-35B-A3B は active 3B のMoE なので、パラメータ数が近くても速度は段違いです。
セットアップ
llama.cpp を上流に追従してビルドし直します。私の環境は5月から放置していたので 1070 commits遅れていました。
cd ~/llama.cpp
git fetch origin
git merge --ff-only origin/master
cmake --build build -j32
GGUF は Meta 公式のものを使いました。
hf download meta-models/Muse-Glimmer-30B-GGUF \
muse-glimmer-30B-kquant-dynamic.gguf --local-dir ~/MuseGlimmer
hf download meta-models/Muse-Glimmer-30B-GGUF \
mmproj-kquant.gguf --local-dir ~/MuseGlimmer
| ファイル | サイズ | 用途 |
|---|---|---|
muse-glimmer-30B-kquant-dynamic.gguf |
19.65 GB | 本体(高 VRAM 向け) |
muse-glimmer-30B-kquant-17gb.gguf |
16.8 GB | 24GB VRAM 向けの縮小版 |
mmproj-kquant.gguf |
1.4 GB | 画像エンコーダ |
dflash-kquant.gguf |
1.63 GB | 投機デコード用 drafter |
unsloth と bartowski からも量子化版が出ています。
余談: Ubuntu 26.04 で hf が動かなくなっていた
最初 hf download が ModuleNotFoundError: No module named 'huggingface_hub' で落ちました。
~/.local/bin/hf の shebang は #!/usr/bin/python3 ですが、その実体が 3.14 に上がっていて、
huggingface_hub は ~/.local/lib/python3.12/site-packages にしか入っていない、というオチです。
python3.12 のバイナリ自体がもう消えているので shebang の書き換えでは戻せません。
uv で隔離環境に入れ直すのが早いです。今後 system python が上がっても壊れません。
uv tool install --force huggingface_hub
同じ理由で ~/.local/bin の 46 個のスクリプトが道連れになっていました。26.04 に上げた人は一度確認しておくとよさそうです。
起動
~/llama.cpp/build/bin/llama-server \
-m ~/MuseGlimmer/muse-glimmer-30B-kquant-dynamic.gguf \
-mm ~/MuseGlimmer/mmproj-kquant.gguf \
--host 127.0.0.1 --port 8081 -ngl 99 -c 8192 -fit off
無事に上がりました。/props を見ると modalities は {vision: true, video: true, audio: false}。
起動時にこの警告が出ます。今回の範囲では実害は出ませんでしたが、生成が止まらない症状が出たらまずここを疑うことになりそうです。
W load: special_eot_id is not in special_eog_ids - the tokenizer config may be incorrect
素の速度はこうでした。プロンプト評価 135.2 tok/s、生成 10.70 tok/s。dense 30B を iGPU で回している値としては妥当なところです。
独特なチャット形式
Muse Glimmer は gpt-oss に似た受信者付きのマルチメッセージ形式を採ります。1ターンのアシスタント応答が複数メッセージに分かれます。
<|start|>assistant to=self<|message|>(思考)<|eom|>
<|start|>assistant to=user<|message|>(本文)<|eot|>
to=self が CoT、to=user が本文です。ツール呼び出しのときは to=<tool名> になり、中身は<atem:function_calls> という Anthropic 風のタグで書かれます。llama.cpp 側はcommon/chat.cpp の common_chat_params_init_muse_glimmer() で PEG パーサを組み立ててこれを解釈し、to=self の中身を reasoning_content に、to=user の中身を content に振り分けてくれます。
テンプレートの判定は、チャットテンプレート内に <atem:function_calls> と <|eom|> の両方が含まれるか、で行われています。GGUF にテンプレートが埋まっていないと専用パーサに落ちず、CoT が content に漏れてくるので、/props で確認しておくと安心です。
「最初の発話」を測る
音声アシスタントに繋ぐ前提なので、知りたいのは総生成時間ではなく content の1トークン目が届くまでの時間です。CoT は reasoning_content に分離されるので読み上げ対象にはなりませんが、時間は容赦なく消費します。
SSE を舐めて、delta.reasoning_content と delta.content それぞれの初回到達時刻を取る、というだけのスクリプトで測りました。
payload = {
"messages": [{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_text}],
"temperature": 0.7, "max_tokens": 512, "stream": True,
}
...
if delta.get("reasoning_content") and t_reasoning is None:
t_reasoning = time.perf_counter() - t0 # ≒ prompt eval 完了
if delta.get("content") and t_content is None:
t_content = time.perf_counter() - t0 # ★これが「最初の発話」
キャラ設定のシステムプロンプトを与えて、雑談4本で測った結果です。
| 発話 | 思考開始 | ★発話開始 | 完了 | CoT chunk |
|---|---|---|---|---|
| こんにちは! | 0.93 s | 47.15 s | 48.66 s | 484 |
| 今日はいい天気だね | 0.70 s | 38.55 s | 41.76 s | 397 |
| おすすめの朝ごはんは何かな | 0.65 s | 45.99 s | 48.66 s | 473 |
| 疲れたときはどうしたらいい? | 0.65 s | 42.92 s | 48.91 s | 438 |
平均 43.7 秒。会話になりません。
しかも遅延の正体はモデルサイズではありません。プロンプト評価は 0.7 秒で終わっていて、残りはすべて CoT です。「こんにちは!」に「こんにちは!コテコだよ!」と返すために 484 chunk考えています。仮に生成が3倍速くなっても15秒で、まだ厳しい。
CoT を止められない
では CoT を切ろう、と思ったのですが、llama.cpp 側には手段がありませんでした。
-
chat_template_kwargs: {"enable_thinking": false}→common_chat_params_init_muse_glimmer()
はinputs.enable_thinkingを参照していない -
--reasoning-budget 0/-rea off→ 予算サンプラはthinking_start_tag/
thinking_end_tagが設定されている場合にだけ組み込まれる。Muse Glimmer 用の初期化関数は
これらを設定していない
Qwen3 系のつもりで enable_thinking: false を投げても素通りします。ここはハマりどころです。
reasoning_strength を見つける
/props からチャットテンプレートを引っ張ってきて中を眺めたら、こんなマクロがありました。
{%- macro render_reasoning() -%}
{%- set rs = reasoning_strength if reasoning_strength is defined and reasoning_strength else 'high' -%}
{{- 'Reasoning strength: ' + rs + '.' -}}
{%- endmacro -%}
システムメッセージに Reasoning strength: <値>. と書き込むだけの素朴な仕組みで、既定はhigh。llama.cpp は chat_template_kwargs の値を json::parse して jinja コンテキストに渡すので、文字列がそのまま通ります。
"chat_template_kwargs": {"reasoning_strength": "low"}
同じ4本を強度別に測り直しました。
| 発話 | low | medium | high(既定) | xhigh |
|---|---|---|---|---|
| こんにちは! | 16.24 | 21.51 | 47.15 | 44.02 |
| 今日はいい天気だね | 20.92 | 30.47 | 38.55 | 48.51 |
| おすすめの朝ごはんは何かな | 8.99 | 30.57 | 45.99 | 41.36 |
| 疲れたときはどうしたらいい? | 14.58 | 22.94 | 42.92 | 31.02 |
| 平均 | 15.18 | 26.37 | 43.65 | 41.23 |
| CoT chunk | 82–207 | 211–309 | 397–484 | 313–497 |
low で 43.65 秒 → 15.18 秒、65% 短縮。生成速度は 10.7 tok/s のままなので、短縮分はすべてCoT が短くなったぶんです。回答の質も、見た限り落ちていません。
xhigh は high とほぼ同じ(むしろ僅かに速い)で、4サンプルのばらつきの範囲内でした。テンプレートは値を検証せず文字列をそのまま埋めるだけなので、モデルが知らない語は high 相当に落ちるのだと思います。効くのは low / medium / high の3段階と見てよさそうです。
max_tokens に注意
xhigh の「今日はいい天気だね」で、CoT が 497 chunk まで伸びた結果、本文が
「そうだね!今日は」の6 chunk で切れました。max_tokens: 512 を CoT が食い潰したためです。
CoT を止める手段がない以上、これは構造的なリスクです。Qwen3 感覚で max_tokens: 512 のまま繋ぎ替えると、回答が空文字で返ってくる事故が起きます。high 運用なら 1024〜2048 は要ります。
マルチモーダルを測る
ここからが本題の画像入力です。テスト画像は3枚用意しました。
- COCO サンプル(640x480、ソファの上の猫2匹とリモコン)
- 花に止まるマルハナバチ(5184x3456、Canon EOS 4000D の実写)
- AI2D の火山断面図(1435x1500、1〜15 の番号ラベル付き)
OpenAI 互換の image_url に data URI で埋めて投げます。
| 画像 | 解像度 | prompt tok | 総時間 | 結果 |
|---|---|---|---|---|
| coco.png | 640x480 | 465 | 13.4 s | 正確 |
| bee_640 | 640x426 | 419 | 12.8 s | 正確 |
| bee_896 | 896x597 | 746 | 13.6 s | 正確 |
| diagram_640 | 640x668 | 626 | 14.0 s | 正確 |
| diagram_896 | 896x936 | 1130 | 27.7 s | 正確 |
| bee_1536 | 1536x1024 | 2018 | 23.4 s | 正確 |
| diagram.jpg | 1435x1500 | 2777 | 31.7 s | 正確 |
認識精度は良好です。640x480 まで縮めても「ピンクの秋桜(コスモス)にマルハナバチが止まっている」「マグマ溜まりから火道を通って噴火口へ、番号付きで示された火山の断面模式図」と正確に読めています。
ストリーミングでの内訳も取りました。
| 画像 | 画像処理+prompt eval | ★発話開始 | 完了 |
|---|---|---|---|
| coco.png (640x480) | 2.50 s | 8.76 s | 36.5 s |
| diagram.jpg (1435x1500) | 22.60 s | 29.69 s | 38.0 s |
テキストのみなら 0.7 秒だった前処理が、1435x1500 では 22.6 秒。画像が入った瞬間に前処理が支配的になります。画像を小さくする動機はここにもあります。
不具合①: 大きい画像で出力が崩壊する(犯人は既定の 4 slot + unified KV)
原寸の bee.jpg(5184x3456)を投げたら、91 秒待たされた挙げ句何も返ってきませんでした。
サーバーログを見ると、こうなっています。
prompt eval time = 42054.39 ms / 4123 tokens
eval time = 48992.82 ms / 512 tokens
W common_chat_peg_parse: unparsed peg-native output: 2222222222222222222222...
W srv stop: cancel task, id_task = 6668
出力が 2222222… に崩壊し、512 トークンを使い切って終了。PEG パーサが解釈できず、
- 非ストリーミング → HTTP 500
The model produced output that does not match the expected peg-native format - ストリーミング → 空応答(content も reasoning も 0 chunk)
となります。クライアント側からは「何も返ってこない」ように見えるので、最初は原因が分かりませんでした。
解像度を振って境界を探ります。ここは**寸法ではなく画像トークン数(=面積)**が効きます。
決定的なのは太字の2行で、長辺 3072px でも面積が小さければ通り、長辺 1700px でも面積が大きければ落ちます。
| 画像 | 解像度 | 長辺 | prompt tok | 結果 |
|---|---|---|---|---|
| bee | 1536x1024 | 1536 | 2018 | OK |
| bee(横長ストリップ) | 3072x410 | 3072 | 1724 | OK |
| ai2d 火山図 | 1435x1500 | 1435 | 2779 | OK |
| bee | 1792x1195 | 1792 | 2826 | 失敗 |
| bee | 2048x1365 | 2048 | 3700 | 失敗 |
| bee(正方形) | 1700x1700 | 1700 | 3795 | 失敗 |
| bee | 2560 / 3584 / 5184 px | — | 4130(頭打ち) | 失敗 |
境界は 2779 tok(成功)〜 2826 tok(失敗) の間。temperature 0、cache_prompt: false、サーバー再起動直後でも 100% 再現し、reasoning_strength の値にも依存しません。
犯人は llama-server の slot 構成だった
前処理(clip のタイル分割)を疑っていたのですが、外れでした。llama-mtmd-cli で同じ画像を投げると壊れません。つまり画像エンコーダでもモデルでもなく、server 側の問題です。
構成を振って切り分けた結果がこれです。
| 構成 | 1700x1700(3795 tok) |
|---|---|
既定(n_slots = 4, kv_unified = true, n_ctx_slot = 8192) |
崩壊 → HTTP 500(3/3) |
--parallel 1(n_slots = 1) |
正常(原寸 5184x3456 / 4130 tok も通る) |
--parallel 4 --no-kv-unified(n_ctx_slot = 2048) |
HTTP 400 で正しく拒否 |
llama-mtmd-cli --jinja |
正常 |
既定 + -b 4096
|
崩壊 → HTTP 500 |
3行目が示唆的です。unified KV を切ると「コンテキストに入りません」ときちんと 400 で断ってくるのに、既定の unified KV では n_ctx_slot = 8192 と称してリクエストを受け付け、そのまま壊れた出力を返します。画像ぶんの KV が正しく勘定されていないのに、エラーにならず黙って壊れる、という形です。
なお、このモデルの sliding_window は 2048(SWA×3 + full×1 の周期 4)でした。閾値の 2800 とは一致しないので、SWA 窓が直接の原因ではなさそうです。
不具合②: 別画像の内容が次の回答に混入する
①で失敗した直後のリクエストで、過去に投げた別画像の説明が混ざります。火山の断面図に対して返ってきたのがこれです。
ピンクの毛布が掛けられた赤いソファの上でくつろぐ2匹の縞模様の猫と、その周りに置かれた
リモコン2台が写った火山の断面を模したイラストです。
猫もリモコンも、数リクエスト前に投げた COCO サンプルの内容です。切り分けた結果はこうでした。
-
cache_prompt: falseでもcached_tokens: 0でも発生する -
①の失敗を挟まない限り、一度も起きない。再起動直後に「火山図 → 猫 → 火山図 → 火山図」と
連続で投げても全部クリーン - 混入した回のスロットは、そのセッションで未使用のスロットだった(
n_slots = 4)
そして --parallel 1 にすると②も消えます。①と同じ根(複数シーケンスで共有される unified KV)だと考えるのが自然です。
対処: --parallel 1 を付ける
これだけです。
llama-server -m muse-glimmer-30B-kquant-dynamic.gguf -mm mmproj-kquant.gguf \
--host 127.0.0.1 --port 8081 -ngl 99 -c 8192 --parallel 1
原寸 5184x3456(4130 tok)まで通るようになり、混入も起きなくなります。ローカルで1人で使うぶんには slot を増やす理由もないので、実質的なコストはありません。
とはいえ画像を小さくするのは別の理由で有効です。前処理が 1435x1500 で 22.6 秒、640x480 なら2.5 秒。精度も 640px の時点で「コスモスに止まるマルハナバチ」まで読めているので、速度重視なら縮小してから送るのがおすすめです。
from PIL import Image
im = Image.open(path).convert("RGB")
if max(im.size) > 1024:
s = 1024 / max(im.size)
im = im.resize((int(im.width * s), int(im.height * s)), Image.LANCZOS)
まとめ
Muse Glimmer 30B は認識も日本語もかなり良いモデルでした。640x480 まで縮めた図から番号ラベル付きの火山断面図を正しく読み、キャラ設定にも素直に乗ってきます。
一方で、私の当初の目的(ローカル音声アシスタントの LLM 差し替え)には不向きでした。
- dense 30B で 10.7 tok/s
- CoT を止める手段が llama.cpp 側にない
-
reasoning_strength: lowでも最初の発話まで 15 秒
音声対話は active 3B の MoE(Qwen3.6-35B-A3B)に任せたまま、Muse Glimmer は別ポートでエージェント用途・画像理解用に立てる、という住み分けに落ち着きました。用途が合えば
reasoning_strength は既定の high のままで問題ありません。
画像まわりの不具合はissueをたてるか悩んでいます。同じ環境で試す方は、まず --parallel 1 を付けてください。これを知らないと「大きい画像を投げると空応答が返る謎のモデル」に見えてしまいます。