Merge #803: Muse Glimmer 30B as its MLX build, with figures measured on a Mac

The two commits of #803 that came after 1.51.0 was cut. The earlier commits (every model that fits, Muse Glimmer 30B and Ornith 1.0 9B) shipped in 1.51.0. No changeset: the next release names it.
This commit is contained in:
Ark0N authored and GitHub committed 2026-11-12 14:08:41 +01:00
commit a26be7c98f
6 files changed
+216 -46

No files matched your search

+46 -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 for the peak the
* first measured row's 5 GB (its own run gave only a floor of 3.0 to 3.3 GB).
*/
const MEASURED_MUSE_MLX: [number, number, number] = [45_000, 8 * GIB + 500_000_000, 5_000_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.
@@ -405,6 +443,9 @@ 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 peak is the
// first measured row's: this run gave only a floor for it (3.0 to 3.3 GB).
'muse-glimmer:30b-mlx': [45_000, 9_089_934_592, 5_000_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
@@ -501,6 +501,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 (45,000 bytes per token,
// measured) + the 8 GiB of prefix snapshots and 0.5 GB + 5 GB of peak.
'muse-glimmer:30b-mlx': 39_041_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,
@@ -667,6 +670,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: 36.1 GB at 65536 tokens (0.960 of the estimate), and
// 5.4 x 3.07 = 16.6 tokens per second. At 131072 it needs 39.0 GB, 1.04: 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'],
@@ -677,11 +683,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);
});
@@ -715,6 +727,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'],
@@ -825,11 +838,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 () => {
@@ -911,6 +920,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,
// 36.1 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);
@@ -923,7 +948,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 {
@@ -1342,10 +1370,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 },
@@ -2167,18 +2203,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.960) to its whole window of 131072 (0.972).
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.96, 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.972, 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);