mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
feat(local-model): Ornith 1.0 9B, the first model a 16 GB Mac can hold
A 16 GB Mac got no model: the two 9B builds of the catalogue need 10.2 GB at 65,536 tokens, 0.987 of the estimated budget, just past the 0.98 a tight fit may reach. New row `ornith:9b-q4_k_m` (Ornith 1.0 9B, MIT), a fine-tune of Qwen3.5-9B for coding agents whose GGUF build has no vision projector: 5.63 GB against 6.55 GB, so 9.3 GB at 65,536 tokens, 0.898 of that budget. SWE-bench Verified 69.4, SWE-bench Pro 42.9, Terminal-Bench 2.1 43.1 on Ornith's card. Its config.json and its GGUF header are those of Qwen3.5-9B (no prediction layer), so it shares the figures of the two other 9B rows. Never run on a Mac: `estimated`. - What changes for a Mac: 16 GB goes from no model to `limited` with this row, tight. That is the only pick that changes: the row is ranked under MiMo V2.6 9B, so every Mac that could already run a 9B keeps its pick and gains an alternative. With the budget a 16 GB Mac mini measured (10.7 GB) the pick is MiMo as before. - Why under MiMo although its own card's numbers are higher: the two vendors' tables put their common base 7 points apart, and runtime issue 16992 (open) was filed on the 35B sibling, which has the same renderer and parser. The 35B stays set aside. - The 16 GB rule itself is untouched: same budget estimate, same 0.98, same eight context checkpoints. - Ranks renumbered 1 to 10; the pins of test/local-model-recommend.test.ts and the table of docs/local-models.md follow. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
9608a17d99
commit
d25cc08606
5 files changed
+151
-69
No files matched your search
@@ -67,7 +67,8 @@ const GIB = 1024 * MIB;
|
||||
* Hugging Face (2026-11-08; Muse Glimmer on 2026-11-10): full-attention layers, their K/V heads
|
||||
* and head size, and the layers whose cache does not grow with the context (Gated DeltaNet
|
||||
* layers with their value and key heads, or sliding-window layers with their window).
|
||||
* Fine-tunes share their base's.
|
||||
* Fine-tunes share their base's: the config.json of Ornith 1.0 9B (2026-11-10) is that of
|
||||
* Qwen3.5-9B in every field below, and the GGUF header of its build says the same.
|
||||
*/
|
||||
const ARCHITECTURES = {
|
||||
'Qwen3.5-9B': { attentionLayers: 8, kvHeads: 4, headDim: 256, linear: { layers: 24, vHeads: 32, kHeads: 16 } },
|
||||
@@ -91,7 +92,7 @@ type Architecture = (typeof ARCHITECTURES)[ArchitectureName];
|
||||
* Which architecture each stock row runs, and how many tokens the runtime drafts per step with
|
||||
* its multi-token-prediction layer: 0 when the GGUF has none (`nextn_predict_layers` in the GGUF
|
||||
* header, read on 2026-11-08 from the first bytes of each model layer; on 2026-11-10 for the
|
||||
* Muse Glimmer row), else the tag's own
|
||||
* Ornith 1.0 and Muse Glimmer rows), else the tag's own
|
||||
* `draft_num_predict` (its params layer) or the runtime's default of 4 when the tag sets none.
|
||||
* Prediction costs a cache for that layer and one rollback copy of the recurrent state per
|
||||
* drafted token (see the catalogue header). A row the catalogue gains must be added here, or the
|
||||
@@ -99,6 +100,8 @@ type Architecture = (typeof ARCHITECTURES)[ArchitectureName];
|
||||
*/
|
||||
const ROW_ARCHITECTURE: Record<string, { architecture: ArchitectureName; draftTokens: number }> = {
|
||||
'qwen3.5:9b-q4_k_m': { architecture: 'Qwen3.5-9B', draftTokens: 0 },
|
||||
// A fine-tune of Qwen3.5-9B; its GGUF has 32 blocks and no prediction layer.
|
||||
'ornith:9b-q4_k_m': { architecture: 'Qwen3.5-9B', draftTokens: 0 },
|
||||
'mimo-v2.6:9b-q4_k_m': { architecture: 'Qwen3.5-9B', draftTokens: 0 },
|
||||
'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.
|
||||
@@ -190,9 +193,10 @@ describe('the stock catalogue', () => {
|
||||
expect(getLocalModelCatalog()).toBe(first);
|
||||
});
|
||||
|
||||
it('lists nine models, by rank, and none of those set aside in the header', () => {
|
||||
it('lists ten 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',
|
||||
@@ -202,9 +206,9 @@ describe('the stock catalogue', () => {
|
||||
'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]);
|
||||
expect(models.map((m) => m.rank)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
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:(9b|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:.*(mlx|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']) {
|
||||
@@ -215,6 +219,7 @@ describe('the stock catalogue', () => {
|
||||
it('names the runner a Mac uses for each tag', () => {
|
||||
expect(Object.fromEntries(models.map((m) => [m.id, m.runner]))).toEqual({
|
||||
'qwen3.5:9b-q4_k_m': 'llamacpp',
|
||||
'ornith:9b-q4_k_m': 'llamacpp',
|
||||
'mimo-v2.6:9b-q4_k_m': 'llamacpp',
|
||||
'north-mini-code-1.0:q4_k_m': 'llamacpp',
|
||||
'muse-glimmer:30b-q4_k_m': 'llamacpp',
|
||||
@@ -231,6 +236,8 @@ describe('the stock catalogue', () => {
|
||||
expect(Object.fromEntries(models.map((m) => [m.id, m.bytes]))).toEqual({
|
||||
// The registry's manifests as fetched on 2026-11-08, in lowercase.
|
||||
'qwen3.5:9b-q4_k_m': 6_550_825_550,
|
||||
// Fetched on 2026-11-10. No vision projector: the weights, a system prompt, the licence.
|
||||
'ornith:9b-q4_k_m': 5_629_110_568,
|
||||
'mimo-v2.6:9b-q4_k_m': 6_550_811_033,
|
||||
'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.
|
||||
@@ -265,6 +272,7 @@ describe('the stock catalogue', () => {
|
||||
it('gives every model a parameter count, an id of the default registry and a license', () => {
|
||||
expect(Object.fromEntries(models.map((m) => [m.id, [m.paramsB, m.license]]))).toEqual({
|
||||
'qwen3.5:9b-q4_k_m': [9, 'Apache-2.0'],
|
||||
'ornith:9b-q4_k_m': [9, 'MIT'],
|
||||
'mimo-v2.6:9b-q4_k_m': [9, 'MIT'],
|
||||
'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.
|
||||
@@ -276,8 +284,12 @@ describe('the stock catalogue', () => {
|
||||
'ornith-1.5:122b': [122, 'MIT'],
|
||||
});
|
||||
for (const model of models) expect(model.id).toMatch(LOCAL_MODEL_ID_PATTERN);
|
||||
// The two 9B rows are the ones under the 12 billion line of the `limited` verdict.
|
||||
expect(models.filter((m) => m.paramsB < 12).map((m) => m.id)).toEqual(['qwen3.5:9b-q4_k_m', 'mimo-v2.6:9b-q4_k_m']);
|
||||
// The three 9B rows are the ones under the 12 billion line of the `limited` verdict.
|
||||
expect(models.filter((m) => m.paramsB < 12).map((m) => m.id)).toEqual([
|
||||
'qwen3.5:9b-q4_k_m',
|
||||
'ornith:9b-q4_k_m',
|
||||
'mimo-v2.6:9b-q4_k_m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('offers no model below the 65536 tokens a harness needs, nor beyond its own window', () => {
|
||||
@@ -289,8 +301,9 @@ describe('the stock catalogue', () => {
|
||||
|
||||
it('has a speed figure only where one was measured or scaled from a measurement', () => {
|
||||
expect(Object.fromEntries(models.map((m) => [m.id, m.speedPer100Gbps ?? null]))).toEqual({
|
||||
// 37.1 tokens per second on 400 GB/s, and the same for the fine-tune of the same size.
|
||||
// 37.1 tokens per second on 400 GB/s, and the same for the two fine-tunes of the same size.
|
||||
'qwen3.5:9b-q4_k_m': 9.3,
|
||||
'ornith:9b-q4_k_m': 9.3,
|
||||
'mimo-v2.6:9b-q4_k_m': 9.3,
|
||||
// The 35B-A3B's llama.cpp figure: both have 3 billion active parameters.
|
||||
'north-mini-code-1.0:q4_k_m': 13.6,
|
||||
@@ -370,6 +383,7 @@ describe('the memory figures of the stock rows: each row has its own', () => {
|
||||
expect(Object.fromEntries(models.map((m) => [m.id, figuresOf(m)]))).toEqual({
|
||||
// State x (1 live + 8 checkpoints) + 1.0 GB + 1 GiB.
|
||||
'qwen3.5:9b-q4_k_m': [17_408, 2_547_960_320, 0],
|
||||
'ornith:9b-q4_k_m': [17_408, 2_547_960_320, 0],
|
||||
'mimo-v2.6:9b-q4_k_m': [17_408, 2_547_960_320, 0],
|
||||
// The sliding window x (1 live + 8 checkpoints) + 1.0 GB + 1 GiB.
|
||||
'north-mini-code-1.0:q4_k_m': [14_144, 3_698_117_120, 0],
|
||||
@@ -398,9 +412,9 @@ describe('the memory figures of the stock rows: each row has its own', () => {
|
||||
byFigures.set(key, [...(byFigures.get(key) ?? []), model]);
|
||||
}
|
||||
const shared = [...byFigures.values()].filter((group) => group.length > 1);
|
||||
// The two 9B rows (a fine-tune and its base) and the two 27B rows (two quantizations of one model).
|
||||
// The three 9B rows (a base and two fine-tunes of it) and the two 27B rows (two quantizations of one model).
|
||||
expect(shared.map((group) => group.map((m) => m.id))).toEqual([
|
||||
['qwen3.5:9b-q4_k_m', 'mimo-v2.6:9b-q4_k_m'],
|
||||
['qwen3.5:9b-q4_k_m', 'ornith:9b-q4_k_m', 'mimo-v2.6:9b-q4_k_m'],
|
||||
['qwen3.8:27b-q8_0', 'qwen3.8:27b-mtp-q4_k_m'],
|
||||
]);
|
||||
for (const group of shared) {
|
||||
|
||||
@@ -487,6 +487,8 @@ describe('needBytes', () => {
|
||||
it('states the need of every stock row at its largest context', () => {
|
||||
expect(Object.fromEntries(STOCK.models.map((m) => [m.id, needBytes(m, m.contexts[0])]))).toEqual({
|
||||
'qwen3.5:9b-q4_k_m': 11_380_487_246,
|
||||
// 0.92 GB under the other two 9B builds: it has no vision projector.
|
||||
'ornith:9b-q4_k_m': 10_458_772_264,
|
||||
'mimo-v2.6:9b-q4_k_m': 11_380_472_729,
|
||||
'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.
|
||||
@@ -656,6 +658,7 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
|
||||
['qwen3.6:35b-a3b-mtp-q4_k_m', 262144, 'good', 'ok', 41.8, undefined],
|
||||
['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'],
|
||||
['qwen3.5:9b-q4_k_m', 131072, 'good', 'ok', 28.6, 'small'],
|
||||
// The dense models compute to 2.95 x 3.07 = 9.1 tokens per second on this chip (5.2 in
|
||||
// 8 bits): slow, so they come after every fast model, in the order of their rank.
|
||||
@@ -704,6 +707,7 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
|
||||
['muse-glimmer:30b-q4_k_m', 'unknown'],
|
||||
['north-mini-code-1.0:q4_k_m', 'unknown'],
|
||||
['mimo-v2.6:9b-q4_k_m', 'unknown'],
|
||||
['ornith:9b-q4_k_m', 'unknown'],
|
||||
['qwen3.5:9b-q4_k_m', 'unknown'],
|
||||
]);
|
||||
});
|
||||
@@ -711,34 +715,57 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
|
||||
|
||||
describe('the Mac mini (Mac16,10: Apple M4, 16 GB)', () => {
|
||||
/**
|
||||
* Expected: verdict `none` (reason `memory`) on the estimated budget, and `limited` with
|
||||
* MiMo V2.6 9B (a GGUF build) at 65,536 tokens, a TIGHT fit, once the runtime measured its
|
||||
* budget.
|
||||
* Expected: `limited` with Ornith 1.0 9B at 65,536 tokens, a TIGHT fit, on the estimated
|
||||
* budget, and MiMo V2.6 9B at 65,536 tokens, also tight, once the runtime measured its budget.
|
||||
*
|
||||
* Why. The estimated budget is 9.67 GiB (10.4 GB: 2/3 of 16 GiB minus 1). The 9B on
|
||||
* llama.cpp needs 6.55 GB of weights + 1.14 GB for a q8_0 K/V cache of 65,536 tokens +
|
||||
* 2.55 GB beside them (its recurrent state live and in 8 context checkpoints, the runner, and
|
||||
* the 1 GiB prompt cache the runtime allows) = 10.2 GB: 0.987 of the estimate, just past the
|
||||
* 0.98 a tight fit may reach. The runtime measured 11.8 GiB on this Mac; the 6 GiB reserve
|
||||
* leaves 10 GiB (10.7 GB) of it, and the same 10.2 GB is 0.954 of that.
|
||||
* Why. The estimated budget is 9.67 GiB (10.4 GB: 2/3 of 16 GiB minus 1). A 9B on llama.cpp
|
||||
* needs its weights + 1.14 GB for a q8_0 K/V cache of 65,536 tokens + 2.55 GB beside them (its
|
||||
* recurrent state live and in 8 context checkpoints, the runner, and the 1 GiB prompt cache
|
||||
* the runtime allows). MiMo and its base are 6.55 GB (a vision projector of 0.92 GB
|
||||
* included): 10.2 GB, 0.987 of the estimate, just past the 0.98 a tight fit may reach. The
|
||||
* Ornith build has no projector, 5.63 GB: 9.3 GB, 0.898 of the estimate. The runtime measured
|
||||
* 11.8 GiB on this Mac; the 6 GiB reserve leaves 10 GiB (10.7 GB) of it, which admits all
|
||||
* three, and the rank puts MiMo first.
|
||||
*
|
||||
* Every figure of this row is computed from the model's architecture, none was measured
|
||||
* (`needSource: 'estimated'`), so this Mac is on the edge either way. Only a run of the 9B on
|
||||
* it settles which side.
|
||||
* Every figure of these rows is computed from the model's architecture, none was measured
|
||||
* (`needSource: 'estimated'`), and no 9B was ever run on such a Mac by us.
|
||||
*/
|
||||
it('gets no model on the estimated budget: the 9B needs 0.987 of it', async () => {
|
||||
it('gets Ornith 1.0 9B at 65536 tokens on the estimated budget, tight: the other 9B builds need 0.987 of it', async () => {
|
||||
const hw = await macMini();
|
||||
expect(hw.chip).toBe('Apple M4');
|
||||
expect(hw.memClassGb).toBe(16);
|
||||
|
||||
const result = recommend(hw, STOCK, ALL_INSTALLED);
|
||||
|
||||
expect(result).toMatchObject({ verdict: 'none', reason: 'memory', budgetSource: 'estimated' });
|
||||
expect(result.headline).toEqual({ text: LOCAL_MODEL_LABELS.noneMemory, vars: { memory: '16' } });
|
||||
expect(result.pick).toBeUndefined();
|
||||
expect(result).toMatchObject({ verdict: 'limited', budgetSource: 'estimated' });
|
||||
expect(result.headline).toEqual({ text: LOCAL_MODEL_LABELS.headlineLimited });
|
||||
expect(result.notes).toEqual([{ text: LOCAL_MODEL_LABELS.noteUntested }]);
|
||||
// floor(17,179,869,184 x 2/3) - 1 GiB.
|
||||
expect(result.budgetBytes).toBe(10_379_504_298);
|
||||
expect(result.budgetGb).toBe('10.4');
|
||||
expect(result.pick).toMatchObject({
|
||||
modelId: 'ornith:9b-q4_k_m',
|
||||
modelLabel: 'Ornith 1.0 9B',
|
||||
license: 'MIT',
|
||||
context: 65536,
|
||||
cli: 'opencode',
|
||||
// 5,629,110,568 + 17,408 x 65,536 + 2,547,960,320.
|
||||
needBytes: 9_317_921_576,
|
||||
needGb: '9.3',
|
||||
memory: { modelBytes: 8_177_070_888, contextBytes: 1_140_850_688, peakBytes: 0 },
|
||||
downloadGb: '5.6',
|
||||
budgetShare: 0.898,
|
||||
fit: 'tight',
|
||||
// 9.3 x 120 / 100.
|
||||
speed: 'ok',
|
||||
tokensPerSecond: 11.2,
|
||||
tested: false,
|
||||
needSource: 'estimated',
|
||||
tag: { kind: 'best-tight', label: 'Best fit, tight' },
|
||||
});
|
||||
// It is the only model that fits, and only at this context: at 131072 it needs 10.5 GB.
|
||||
expect(order(result)).toEqual(['ornith:9b-q4_k_m@65536']);
|
||||
expect(modelFitsBudget(hw, STOCK, 'ornith:9b-q4_k_m', 131072)).toBe(false);
|
||||
const nineB = stockModel('mimo-v2.6:9b-q4_k_m');
|
||||
// 6,550,811,033 + 17,408 x 65,536 + 2,547,960,320.
|
||||
expect(needBytes(nineB, 65536)).toBe(10_239_622_041);
|
||||
@@ -746,7 +773,7 @@ describe('the Mac mini (Mac16,10: Apple M4, 16 GB)', () => {
|
||||
expect(needBytes(nineB, 65536) / result.budgetBytes).toBeLessThan(0.99);
|
||||
});
|
||||
|
||||
it('would need 11.4 GB at 131072 tokens, more than the whole budget', async () => {
|
||||
it('would need 11.4 GB for MiMo at 131072 tokens, more than the whole budget', async () => {
|
||||
const hw = await macMini();
|
||||
const nineB = stockModel('mimo-v2.6:9b-q4_k_m');
|
||||
const budget = deviceBudget(hw).budgetBytes;
|
||||
@@ -756,7 +783,7 @@ describe('the Mac mini (Mac16,10: Apple M4, 16 GB)', () => {
|
||||
expect(modelFitsBudget(hw, STOCK, 'mimo-v2.6:9b-q4_k_m', 65536)).toBe(false);
|
||||
});
|
||||
|
||||
it('gets a small model with the budget its runtime measured: the 9B at 65536 tokens, tight', async () => {
|
||||
it('gets MiMo with the budget its runtime measured: at 65536 tokens, tight', async () => {
|
||||
// The runtime reported 11.8 GiB here; the 6 GiB reserve leaves 10 GiB (10.7 GB) of it.
|
||||
const result = recommend(await macMini(), STOCK, ALL_INSTALLED, { measuredTotalBytes: MACMINI_MEASURED_TOTAL });
|
||||
expect(result).toMatchObject({
|
||||
@@ -785,13 +812,20 @@ describe('the Mac mini (Mac16,10: Apple M4, 16 GB)', () => {
|
||||
needSource: 'estimated',
|
||||
tag: { kind: 'best-tight', label: 'Best fit, tight' },
|
||||
});
|
||||
// Its base model, the same figures, is the one alternative.
|
||||
expect(order(result)).toEqual(['mimo-v2.6:9b-q4_k_m@65536', 'qwen3.5:9b-q4_k_m@65536']);
|
||||
// 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',
|
||||
]);
|
||||
});
|
||||
|
||||
it('gets the same with Claude Code as its only coding agent, which also needs 65536 tokens', async () => {
|
||||
const hw = await macMini();
|
||||
expect(recommend(hw, STOCK, [fact('claude')])).toMatchObject({ verdict: 'none', reason: 'memory' });
|
||||
const estimated = recommend(hw, STOCK, [fact('claude')]);
|
||||
expect(estimated.verdict).toBe('limited');
|
||||
expect(estimated.pick).toMatchObject({ modelId: 'ornith:9b-q4_k_m', context: 65536, cli: 'claude' });
|
||||
const measured = recommend(hw, STOCK, [fact('claude')], { measuredTotalBytes: MACMINI_MEASURED_TOTAL });
|
||||
expect(measured.verdict).toBe('limited');
|
||||
expect(measured.pick).toMatchObject({ modelId: 'mimo-v2.6:9b-q4_k_m', context: 65536, cli: 'claude' });
|
||||
@@ -803,12 +837,13 @@ describe('every memory size, with the stock catalogue', () => {
|
||||
// size. A change of a catalogue row or of a threshold that moves one of these answers has to
|
||||
// change this table on purpose. docs/local-models.md has the same table for people.
|
||||
const EXPECTED: [soldGb: number, chip: string, answer: string][] = [
|
||||
// The smallest need at 65536 tokens is 10.2 GB; the budget is 2.1 GB.
|
||||
// The smallest need at 65536 tokens is 9.3 GB; the budget is 2.1 GB.
|
||||
[8, 'Apple M2', 'none memory'],
|
||||
// The 9B needs 10.2 GB at 65536 tokens, 0.987 of the 10.4 GB estimate: just out. A runtime
|
||||
// that measured the budget a real 16 GB Mac measured admits it (see the Mac mini above).
|
||||
[16, 'Apple M4', 'none memory'],
|
||||
[16, 'Apple M1', 'none memory'],
|
||||
// The 9B build without a vision projector needs 9.3 GB at 65536 tokens, 0.898 of the 10.4 GB
|
||||
// estimate. The other two need 10.2 GB, 0.987: just out, until a runtime measures what a
|
||||
// real 16 GB Mac measured (see the Mac mini above). On an M1 (68 GB/s) a 9B is slow: 6.3.
|
||||
[16, 'Apple M4', 'limited ornith:9b-q4_k_m@65536 tight ok'],
|
||||
[16, 'Apple M1', 'limited ornith:9b-q4_k_m@65536 tight slow'],
|
||||
// 11.4 GB at 131072 tokens, 0.964 of 11.8 GB, and nothing of 12 billion fits: `limited`.
|
||||
[18, 'Apple M3 Pro', 'limited mimo-v2.6:9b-q4_k_m@131072 tight ok'],
|
||||
// On a base chip with 100 GB/s the 9B computes to 9.3 tokens per second: slow.
|
||||
@@ -861,15 +896,15 @@ describe('every memory size, with the stock catalogue', () => {
|
||||
expect(result.headline).toEqual({ text: LOCAL_MODEL_LABELS.noneMemory, vars: { memory: '8' } });
|
||||
// The smallest model a harness can work with needs more than four times the budget.
|
||||
const smallest = Math.min(...STOCK.models.map((m) => needBytes(m, 65536)));
|
||||
expect(formatGb(smallest)).toBe('10.2');
|
||||
expect(formatGb(smallest)).toBe('9.3');
|
||||
expect(smallest / result.budgetBytes).toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
it('gives no model to 8 and 16 GB, a small model to every Mac of 18 and 24 GB, and every Mac from 36 GB a model of 27 billion or more', () => {
|
||||
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);
|
||||
const label = `${hw.memClassGb} GB ${hw.chip}`;
|
||||
if (hw.memClassGb <= 16) expect(result.reason, label).toBe('memory');
|
||||
if (hw.memClassGb <= 8) expect(result.reason, label).toBe('memory');
|
||||
else if (hw.memClassGb <= 24) {
|
||||
expect(result.verdict, label).toBe('limited');
|
||||
expect(result.pick?.paramsB, label).toBe(9);
|
||||
@@ -2125,8 +2160,12 @@ describe('the measured budget, end to end', () => {
|
||||
const stepped = order(estimated).filter((entry, index) => order(measured)[index] !== entry);
|
||||
expect(stepped).toEqual(['qwen3.8:27b-q8_0@65536']);
|
||||
expect(order(measured)).toContain('qwen3.8:27b-q8_0@131072');
|
||||
expect(rows(estimated).map((c) => c.budgetShare)).toEqual([0.957, 0.767, 0.692, 0.303, 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.283, 0.798, 0.976, 0.539]);
|
||||
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,
|
||||
]);
|
||||
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,
|
||||
]);
|
||||
for (const candidate of rows(measured)) {
|
||||
expect(candidate.budgetShare).toBeCloseTo(candidate.needBytes / MACBOOK_MEASURED_TOTAL, 3);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user