feat(local-model): Muse Glimmer 30B as its MLX build, with figures measured on a Mac

Run on a MacBook Pro (M5 Pro, 48 GB, runtime 0.40.15) on 2026-11-12, after the GGUF build
this branch lists turned out to be unusable in agent use there: an OpenCode turn of one
line showed no answer after 7 minutes, twice, although the runtime alone wrote 15 tokens
per second.

The MLX build of the same model (`muse-glimmer:30b-mlx`, 19.1 GB) on that Mac:
- 32.6 tokens per second for a 1,500-token answer. The tag carries a draft model, and the
  runtime logs speculative decoding with 0.34 to 0.62 of the drafts accepted.
- A correct tool call in under 3 seconds, and the same OpenCode turn finished in 45 seconds.
- Memory, read from the runner's own log line after every request: 37,900 to 45,100 bytes
  held per token (the GGUF build computes to 7,072), prefix snapshots that filled their
  8 GiB during twelve separate prompts, and a process peak 3.0 to 3.3 GB above what stayed
  held.

So it gets a row of its own, the second with measured figures. They put it at 34.6 GB for
65,536 tokens: offered from 48 GB up, never on the 32 and 36 GB Macs, which keep the GGUF
row untouched (it is still the pick of a 32 GB Mac with a Max chip: whether it should be
is the maintainer's decision). It is never the pick of a Mac whose disk has room for
Qwen3.6 35B-A3B, and it has no `tested` entry: one turn is not a test.

Also written down from the same session: the session of many prompts the Qwen3.6 35B-A3B
row owed (5.6 GiB of prefix snapshots after twelve prompts, still growing; its figures are
unchanged), and what the two open runtime issues on the MLX tag say against what ran.

Not done: the row was not installed through the window, and the measurement used a second
instance of the pinned runtime, not Codeman's.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-12 02:52:59 +01:00
1 parent d25cc08606
commit 5396e33b48
5 files changed
+203 -45

No files matched your search

+45 -5
View File
@@ -106,6 +106,9 @@ const ROW_ARCHITECTURE: Record<string, { architecture: ArchitectureName; draftTo
'north-mini-code-1.0:q4_k_m': { architecture: 'North-Mini-Code-1.0', draftTokens: 0 },
// No prediction layer; the tags with a separate draft model (`-dflash`) are not listed.
'muse-glimmer:30b-q4_k_m': { architecture: 'Muse-Glimmer-30B', draftTokens: 0 },
// The MLX build. Its figures are measured, so nothing is recomputed from this entry. The tag
// carries a draft MODEL (not a prediction layer of llama.cpp): what that holds was measured too.
'muse-glimmer:30b-mlx': { architecture: 'Muse-Glimmer-30B', draftTokens: 0 },
// Its params layer sets `draft_num_predict` 2.
'qwen3.6:35b-a3b-mtp-q4_k_m': { architecture: 'Qwen3.6-35B-A3B', draftTokens: 2 },
'qwen3.6:35b-a3b': { architecture: 'Qwen3.6-35B-A3B', draftTokens: 0 },
@@ -167,6 +170,12 @@ function mlxComputedPerToken(arch: Architecture): number {
/** The figures measured for `qwen3.6:35b-a3b` on the MacBook Pro. */
const MEASURED: [number, number, number] = [37_000, 3_000_000_000, 5_000_000_000];
/**
* The figures measured for `muse-glimmer:30b-mlx` on the same MacBook Pro (2026-11-12): the upper
* end of what stayed held per token, the whole prefix budget plus 0.5 GB, and the peak of the
* process above what stayed held.
*/
const MEASURED_MUSE_MLX: [number, number, number] = [45_000, 8 * GIB + 500_000_000, 3_500_000_000];
/** What an estimated `mlx` row's three figures must be: computed, scaled by what the measured row showed. */
function mlxFigures(arch: Architecture): [number, number, number] {
@@ -193,22 +202,23 @@ describe('the stock catalogue', () => {
expect(getLocalModelCatalog()).toBe(first);
});
it('lists ten models, by rank, and none of those set aside in the header', () => {
it('lists eleven models, by rank, and none of those set aside in the header', () => {
expect(models.map((m) => m.id)).toEqual([
'qwen3.5:9b-q4_k_m',
'ornith:9b-q4_k_m',
'mimo-v2.6:9b-q4_k_m',
'north-mini-code-1.0:q4_k_m',
'muse-glimmer:30b-q4_k_m',
'muse-glimmer:30b-mlx',
'qwen3.6:35b-a3b-mtp-q4_k_m',
'qwen3.6:35b-a3b',
'qwen3.8:27b-q8_0',
'qwen3.8:27b-mtp-q4_k_m',
'ornith-1.5:122b',
]);
expect(models.map((m) => m.rank)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(models.map((m) => m.rank)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
const setAside =
/^(bonsai-2|gpt-oss|flint|lfm2\.5|laguna|qwen3\.8-flash-next|qwen3\.5:(122b|4b)|ripple-preview|nemotron-3\.5-super|qwen3-coder-next)|^ornith-1\.5:(9b|35b|397b)|^ornith:35b|^muse-glimmer:.*(mlx|dflash)/;
/^(bonsai-2|gpt-oss|flint|lfm2\.5|laguna|qwen3\.8-flash-next|qwen3\.5:(122b|4b)|ripple-preview|nemotron-3\.5-super|qwen3-coder-next)|^ornith-1\.5:(9b|35b|397b)|^ornith:35b|^muse-glimmer:.*dflash/;
expect(models.filter((m) => setAside.test(m.id)).map((m) => m.id)).toEqual([]);
// The plain 9B and 27B tags are MLX builds on a Mac: their GGUF builds are listed instead.
for (const plain of ['qwen3.5:9b', 'mimo-v2.6:9b', 'qwen3.8:27b']) {
@@ -223,6 +233,8 @@ describe('the stock catalogue', () => {
'mimo-v2.6:9b-q4_k_m': 'llamacpp',
'north-mini-code-1.0:q4_k_m': 'llamacpp',
'muse-glimmer:30b-q4_k_m': 'llamacpp',
// Its manifest names no runner in a list; the runtime starts its MLX runner for it.
'muse-glimmer:30b-mlx': 'mlx',
'qwen3.6:35b-a3b-mtp-q4_k_m': 'llamacpp',
// Manifest lists: a Mac takes the MLX child.
'qwen3.6:35b-a3b': 'mlx',
@@ -242,6 +254,9 @@ describe('the stock catalogue', () => {
'north-mini-code-1.0:q4_k_m': 18_593_967_008,
// Fetched on 2026-11-10: 16.76 GB of weights and a 1.40 GB projector.
'muse-glimmer:30b-q4_k_m': 18_157_010_252,
// Fetched on 2026-11-12: 1,503 layers, the draft model's among them. A pull on a Mac
// that day ended at 19.1 GB.
'muse-glimmer:30b-mlx': 19_053_621_992,
'qwen3.6:35b-a3b-mtp-q4_k_m': 22_621_314_381,
// The MLX child of the list, and the total of a real pull on a Mac, byte for byte.
'qwen3.6:35b-a3b': 23_105_798_956,
@@ -252,9 +267,29 @@ describe('the stock catalogue', () => {
});
});
it('says which row was measured on a Mac: one, and every other row is an estimate', () => {
expect(models.filter((m) => m.figures === 'measured').map((m) => m.id)).toEqual(['qwen3.6:35b-a3b']);
it('says which rows were measured on a Mac: two, and every other row is an estimate', () => {
expect(models.filter((m) => m.figures === 'measured').map((m) => m.id)).toEqual([
'muse-glimmer:30b-mlx',
'qwen3.6:35b-a3b',
]);
expect(figuresOf(models.find((m) => m.id === 'qwen3.6:35b-a3b') as LocalModelEntry)).toEqual(MEASURED);
expect(figuresOf(models.find((m) => m.id === 'muse-glimmer:30b-mlx') as LocalModelEntry)).toEqual(
MEASURED_MUSE_MLX
);
});
it('counts the whole prefix budget of the MLX runner in the row that was measured filling it', () => {
// Every estimated MLX row counts the 8 GiB too (mlxFigures). The first measured row does
// not: the catalogue header says why, and what a session on it really held.
expect(MEASURED_MUSE_MLX[1]).toBe(8 * GIB + 500_000_000);
expect(MEASURED[1]).toBeLessThan(8 * GIB);
// What the runner really keeps per token is far above the 13 full-attention layers alone
// and under all 52 layers at full length: it evidently does not trim the sliding-window layers.
const arch = ARCHITECTURES['Muse-Glimmer-30B'];
expect(arch.attentionLayers * arch.kvHeads * arch.headDim * 2 * 2).toBe(13_312);
expect((arch.attentionLayers + arch.sliding.layers) * arch.kvHeads * arch.headDim * 2 * 2).toBe(53_248);
expect(MEASURED_MUSE_MLX[0]).toBeGreaterThan(13_312 * 2);
expect(MEASURED_MUSE_MLX[0]).toBeLessThanOrEqual(53_248);
});
it('has one tested entry: the 35B-A3B with OpenCode on the MacBook Pro, with its measured speed', () => {
@@ -277,6 +312,7 @@ describe('the stock catalogue', () => {
'north-mini-code-1.0:q4_k_m': [30, 'Apache-2.0'],
// The card's total: a 27.9B language model and a vision encoder.
'muse-glimmer:30b-q4_k_m': [29.6, 'Apache-2.0'],
'muse-glimmer:30b-mlx': [29.6, 'Apache-2.0'],
'qwen3.6:35b-a3b-mtp-q4_k_m': [35, 'Apache-2.0'],
'qwen3.6:35b-a3b': [35, 'Apache-2.0'],
'qwen3.8:27b-q8_0': [27.8, 'Apache-2.0'],
@@ -309,6 +345,8 @@ describe('the stock catalogue', () => {
'north-mini-code-1.0:q4_k_m': 13.6,
// The dense 27B's figure: the weights of the two are the same size.
'muse-glimmer:30b-q4_k_m': 2.95,
// Measured beside the 35B-A3B on MLX, same Mac and request: 32.6 against 101.9, x 16.9.
'muse-glimmer:30b-mlx': 5.4,
// 54.5 on 400 GB/s with llama.cpp.
'qwen3.6:35b-a3b-mtp-q4_k_m': 13.6,
// 67.8 on 400 GB/s with MLX.
@@ -389,6 +427,8 @@ describe('the memory figures of the stock rows: each row has its own', () => {
'north-mini-code-1.0:q4_k_m': [14_144, 3_698_117_120, 0],
// The same rule: 39 layers x 2,560 cells x 544 bytes = 54,312,960 per copy.
'muse-glimmer:30b-q4_k_m': [7_072, 2_562_558_464, 0],
// Measured: the upper end per token, 8 GiB of prefix snapshots + 0.5 GB, the process peak.
'muse-glimmer:30b-mlx': [45_000, 9_089_934_592, 3_500_000_000],
// State x (1 live + 2 rollback copies + 8 checkpoints) + 1.0 GB + 1 GiB; the prediction
// layer's cache in f16 per token. The 27B rows below draft 4 tokens: 4 rollback copies.
'qwen3.6:35b-a3b-mtp-q4_k_m': [12_928, 2_798_242_304, 0],
+54 -16
View File
@@ -493,6 +493,9 @@ describe('needBytes', () => {
'north-mini-code-1.0:q4_k_m': 25_999_848_864,
// At its own window of 131072 tokens: 18.2 GB + 0.9 GB of K/V cache + 2.6 GB.
'muse-glimmer:30b-q4_k_m': 21_646_509_900,
// The MLX build at the same window: 19.1 GB + 5.9 GB of cache (every layer at full length,
// measured) + the 8 GiB of prefix snapshots and 0.5 GB + 3.5 GB of peak.
'muse-glimmer:30b-mlx': 37_541_796_584,
'qwen3.6:35b-a3b-mtp-q4_k_m': 28_808_554_317,
'qwen3.6:35b-a3b': 35_955_462_956,
'qwen3.8:27b-q8_0': 44_292_142_594,
@@ -656,6 +659,9 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
).toEqual([
// The same model as a GGUF build: its fixed footprint holds the full window, 28.8 GB.
['qwen3.6:35b-a3b-mtp-q4_k_m', 262144, 'good', 'ok', 41.8, undefined],
// Muse Glimmer as the MLX build: 34.6 GB at 65536 tokens (0.920 of the estimate), and
// 5.4 x 3.07 = 16.6 tokens per second. At 131072 it needs 37.5 GB, 0.998: out.
['muse-glimmer:30b-mlx', 65536, 'tight', 'ok', 16.6, 'tight'],
['north-mini-code-1.0:q4_k_m', 262144, 'good', 'ok', 41.8, undefined],
['mimo-v2.6:9b-q4_k_m', 131072, 'good', 'ok', 28.6, 'small'],
['ornith:9b-q4_k_m', 131072, 'good', 'ok', 28.6, 'small'],
@@ -666,11 +672,17 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
['qwen3.8:27b-q8_0', 65536, 'tight', 'slow', 5.2, 'tight'],
['muse-glimmer:30b-q4_k_m', 131072, 'good', 'slow', 9.1, 'slow'],
]);
// Only the tested row rests on a measurement of that model.
expect(new Set(result.alternatives.map((c) => c.needSource))).toEqual(new Set(['estimated']));
// Beside the tested pick, one more row rests on a measurement of its own model's memory.
expect(result.alternatives.filter((c) => c.needSource === 'measured').map((c) => c.modelId)).toEqual([
'muse-glimmer:30b-mlx',
]);
// Every model of the catalogue that fits is on the list: nothing is cut off after four.
const fitting = STOCK.models.filter((m) => m.contexts.some((ctx) => modelFitsBudget(hw48, STOCK, m.id, ctx)));
expect(rows(result).map((c) => c.modelId).sort()).toEqual(fitting.map((m) => m.id).sort());
expect(
rows(result)
.map((c) => c.modelId)
.sort()
).toEqual(fitting.map((m) => m.id).sort());
// The 122B fits at no context.
expect(order(result).some((entry) => entry.startsWith('ornith-1.5'))).toBe(false);
});
@@ -704,6 +716,7 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
['qwen3.8:27b-mtp-q4_k_m', 'unknown'],
['qwen3.8:27b-q8_0', 'unknown'],
['qwen3.6:35b-a3b-mtp-q4_k_m', 'unknown'],
['muse-glimmer:30b-mlx', 'unknown'],
['muse-glimmer:30b-q4_k_m', 'unknown'],
['north-mini-code-1.0:q4_k_m', 'unknown'],
['mimo-v2.6:9b-q4_k_m', 'unknown'],
@@ -814,11 +827,7 @@ describe('the Mac mini (Mac16,10: Apple M4, 16 GB)', () => {
});
// The other two 9B builds follow by rank. The Ornith build is small enough for 131072
// tokens here (10.5 GB, 0.974), and a model is listed at the largest context it fits at.
expect(order(result)).toEqual([
'mimo-v2.6:9b-q4_k_m@65536',
'ornith:9b-q4_k_m@131072',
'qwen3.5:9b-q4_k_m@65536',
]);
expect(order(result)).toEqual(['mimo-v2.6:9b-q4_k_m@65536', 'ornith:9b-q4_k_m@131072', 'qwen3.5:9b-q4_k_m@65536']);
});
it('gets the same with Claude Code as its only coding agent, which also needs 65536 tokens', async () => {
@@ -900,6 +909,22 @@ describe('every memory size, with the stock catalogue', () => {
expect(smallest / result.budgetBytes).toBeGreaterThan(4);
});
it('offers Muse Glimmer as the MLX build from 48 GB only, and never as the pick of a Mac whose disk has room', () => {
// Measured on a 48 GB Mac: 45,000 bytes held per token and up to 8 GiB of prefix snapshots,
// 34.6 GB at 65536 tokens. Runtime issue 18269 reports this build stuck, and a restart, on
// a 32 GB Mac: the figures keep it away from there.
for (const hw of SWEEP) {
const result = recommend(hw, STOCK, ALL_INSTALLED);
const label = `${hw.memClassGb} GB ${hw.chip}`;
const offered = rows(result).some((c) => c.modelId === 'muse-glimmer:30b-mlx');
expect(offered, label).toBe(hw.memClassGb >= 48);
expect(result.pick?.modelId, label).not.toBe('muse-glimmer:30b-mlx');
}
// The 32 GB Mac with a Max chip keeps the GGUF build as its pick.
const m1max = recommend(mac(32, 'Apple M1 Max'), STOCK, ALL_INSTALLED);
expect(m1max.pick?.modelId).toBe('muse-glimmer:30b-q4_k_m');
});
it('gives no model to 8 GB, a small model to every Mac from 16 to 24 GB, and every Mac from 36 GB a model of 27 billion or more', () => {
for (const hw of SWEEP) {
const result = recommend(hw, STOCK, ALL_INSTALLED);
@@ -912,7 +937,10 @@ describe('every memory size, with the stock catalogue', () => {
// A model of 30 billion fits, so the Mac is not `limited`. It is dense: the pick only
// where it is not slow (a chip this file knows is fast, or one it knows nothing about).
expect(result.verdict, label).toBe('ok');
expect(rows(result).map((c) => c.modelId), label).toContain('muse-glimmer:30b-q4_k_m');
expect(
rows(result).map((c) => c.modelId),
label
).toContain('muse-glimmer:30b-q4_k_m');
expect(result.pick?.paramsB, label).toBe(hw.chip === 'Apple M4' ? 9 : 29.6);
if (hw.chip === 'Apple M4') expect(result.notes[0], label).toEqual({ text: LOCAL_MODEL_LABELS.noteSmallPick });
} else {
@@ -1331,10 +1359,18 @@ describe('verdict none', () => {
const oneShort = recommend(withFree(cut - 1), STOCK, ALL_INSTALLED);
expect(oneShort.pick?.modelId).toBe('qwen3.6:35b-a3b-mtp-q4_k_m');
expect(oneShort.headline.text).toBe(LOCAL_MODEL_LABELS.headlineOk);
// At 33 GB neither 35B-A3B build has room: North Mini Code, and the tested pair is named.
// At 33 GB neither 35B-A3B build has room: the next rank whose speed is `ok` and whose
// download fits is Muse Glimmer as the MLX build (19.1 GB), and the tested pair is named.
const short = recommend(withFree(33 * GB), STOCK, ALL_INSTALLED);
expect(short.verdict).toBe('ok');
expect(short.pick?.modelId).toBe('north-mini-code-1.0:q4_k_m');
expect(short.pick).toMatchObject({
modelId: 'muse-glimmer:30b-mlx',
context: 65536,
fit: 'tight',
tested: false,
});
// Below 29.8 GB that build has no room either, and North Mini Code (18.6 GB) is the pick.
expect(recommend(withFree(29.5 * GB), STOCK, ALL_INSTALLED).pick?.modelId).toBe('north-mini-code-1.0:q4_k_m');
expect(short.notes).toEqual([
{ text: LOCAL_MODEL_LABELS.noteDiskDropped, vars: { model: 'Qwen3.6 35B-A3B', missing: '0.8' } },
{ text: LOCAL_MODEL_LABELS.noteUntested },
@@ -2153,18 +2189,20 @@ describe('the measured budget, end to end', () => {
const measured = recommend(hw, STOCK, ALL_INSTALLED, { measuredTotalBytes: MACBOOK_MEASURED_TOTAL });
expect([estimated.budgetSource, estimated.budgetGb]).toEqual(['estimated', '37.6']);
expect([measured.budgetSource, measured.budgetGb]).toEqual(['measured', '40.2']);
// The same models in the same order. The share of each changes, and one model steps up a
// The same models in the same order. The share of each changes, and two models step up a
// context: the 27B in 8 bits fits 65536 tokens under the estimate (0.975) and 131072 under
// what the runtime reported (0.976).
// what the runtime reported (0.976), and Muse Glimmer as the MLX build goes from 65536
// (0.920) to its whole window of 131072 (0.935).
expect(rows(measured).map((c) => c.modelId)).toEqual(rows(estimated).map((c) => c.modelId));
const stepped = order(estimated).filter((entry, index) => order(measured)[index] !== entry);
expect(stepped).toEqual(['qwen3.8:27b-q8_0@65536']);
expect(stepped).toEqual(['muse-glimmer:30b-mlx@65536', 'qwen3.8:27b-q8_0@65536']);
expect(order(measured)).toContain('qwen3.8:27b-q8_0@131072');
expect(order(measured)).toContain('muse-glimmer:30b-mlx@131072');
expect(rows(estimated).map((c) => c.budgetShare)).toEqual([
0.957, 0.767, 0.692, 0.303, 0.278, 0.303, 0.853, 0.975, 0.576,
0.957, 0.767, 0.92, 0.692, 0.303, 0.278, 0.303, 0.853, 0.975, 0.576,
]);
expect(rows(measured).map((c) => c.budgetShare)).toEqual([
0.895, 0.717, 0.647, 0.283, 0.26, 0.283, 0.798, 0.976, 0.539,
0.895, 0.717, 0.935, 0.647, 0.283, 0.26, 0.283, 0.798, 0.976, 0.539,
]);
for (const candidate of rows(measured)) {
expect(candidate.budgetShare).toBeCloseTo(candidate.needBytes / MACBOOK_MEASURED_TOTAL, 3);