opus audit: phase 0 + phase 2 m5 air validation & strata metal patches #
scope: scripts/patch_strata_metal.py, scripts/prepare_strata_k256.py, vendor/strata/metal/src/convert_k256_q2_0.cpp, the patched vendor/strata/metal tree (git -C vendor/strata diff), scripts/patch_m5_lazy_moe.py, scripts/run_phase0_phase2_m5_validation.py, results/phase0_phase2_m5_validation.json, PLAN.md §2.7.A #29–#30 and §7.1 item 5. read-only, except one run of the existing vendor/strata/build-metal/route_parity binary.
verdict: ISSUES. the per-run raw numbers for the 1024/2048-token strata runs are real and recompute exactly. the patches work for K=256, and the Q2_0 layout is right. but:
- the short-prompt headline row in PLAN §7.1 item 5 and §2.7.A #30 has no raw source.
13.88 tok/s,72.04 ms/tok,92.85%(87.80% + 5.05%),34.3 misses/tok,84.0 MB/tok,13.60 mswait,57.4 msGPU and the83.8 -> 34.3/ "−59%" claim appear nowhere in the archived raw runs. the archived short run gives13.80 tok/s,91.24%,42.1,107.8 MB/tok,16.05 ms,55.79 ms.summary_comparisonin the same JSON was edited by hand to the unarchived numbers. - the llama.cpp
3.81 tok/sdecode and the-7.8xNVMe-transfer claim have no raw source either. the "MB per decode tok" figures (1,628/515) are whole-rundisk0deltas (load + prefill + decode) divided by 65, measured on runs with different prompts. - the code that produced the results is not the code in the tree. the results JSON is from 06:52:33.
patch_strata_metal.py, every patched vendor file andstrata_window_cliall have mtimes from 06:54:06–06:54:09. the JSON's schema is also not the onerun_phase0_phase2_m5_validation.pywrites, and no strata runner, cmdline or env (STRATA_METAL_MOCE_BONUS!) is archived. - code-level issues: a GPU/IO-thread race on
smapinside the MoCE router can mis-scalew.ahead()does not mirror the top-3 protection.MADV_DONTNEEDdoes not reclaim the prompt scratch on darwin, andhold_region(true)memsets all 1.36 GB on every prompt. the "dropped 1.60 GB" line is a hard-coded string, and PLAN's "+1.60 GB freed vs upstream" is wrong: upstream already skipped the BF16 HC originals, so the real saving is ≈0.34 GB. the IQ3_S/IQ4_NL→Q2_0 requantized model and MoCE bonus 0.85 have no quality measurement, so the phase 1c KLD numbers do not describe the model that was benchmarked.
1. metrics vs raw output (results/phase0_phase2_m5_validation.json) #
i recomputed every strata run from STRATA_RESULT and the expert stream line in output_tail. decode = 63 / eval_ms, prefill = prompt_tokens / prefill_ms, and combined = arena + read-ahead. the parsed perf dicts match the tails exactly.
| claim (PLAN §7.1 #5 / §2.7.A / task) | raw source | status |
|---|---|---|
320.80 tok/s 1K prefill (3.19 s) |
step2: 1024 tok / 3191.98 ms = 320.80 | ✓ |
363.58 tok/s 2K prefill (5.63 s) |
step3: 2048 / 5632.86 ms = 363.58 | ✓ |
13.53 tok/s (73.91 ms) after 1K |
step2: 63 / 4656.22 ms = 13.53 | ✓ |
12.96 tok/s (77.17 ms) after 2K |
step3: 63 / 4861.89 ms = 12.96 | ✓ |
91.32% / 99.9 MB/tok after 1K |
0.8566 + 0.0566 = 0.9132; 99.9 | ✓ |
90.73% / 114.5 MB/tok after 2K |
0.8343 + 0.0730 = 0.9073; 114.5 | ✓ |
13.88 tok/s (72.04 ms/tok) short prompt |
step1: 13.80 tok/s, 72.49 ms/tok | ✗ not in raw |
92.85% = 87.80% arena + 5.05% RA |
step1: 91.24% = 84.33% + 6.91% | ✗ not in raw |
34.3 misses/tok |
step1/2/3: 42.1 / 41.7 / 44.5 | ✗ not in raw |
84.0 MB/tok (lower end of "84.0–114.5") |
step1/2/3: 107.8 / 99.9 / 114.5 | ✗ not in raw; true range 99.9–114.5 |
13.60 ms SSD wait + 57.4 ms GPU |
step1: 16.05 ms wait, 55.79 ms pure GPU | ✗ not in raw |
83.8 -> 34.3 misses/tok (−59%) |
no MoCE-off run archived at all | ✗ unsupported (the arithmetic 49.5/83.8 = 59.1% is right) |
| "12.96–13.88 tok/s" decode range | raw range 12.96–13.80 | ✗ upper end |
| "90.73%–92.85%" hit-rate range | raw range 90.73%–91.32% | ✗ upper end |
0.34 s (341 ms) init |
384.31 / 340.83 / 340.94 ms | ✓ (the best 2 of 3; short run 384) |
0.5 s (6.94 GB/s) arena fill |
0.5 s 6.94 / 0.6 s 6.84 / 0.5 s 6.94 | ✓ |
2,728–3,015 slots, 3.8–4.2 GB arena |
every run: 2728 slots, 3.80 GB | ✗ 3,015 / 4.2 GB not in raw |
2.90 GB mlocked |
log locked 2.90 GB (measured locked_bytes) |
✓, and matches the header: live dense 2.897 GB, page-rounded 2.904 GB |
1.60 GB dead HC + embd dropped |
log text is a hard-coded literal ("dropped 1.60 GB ..." in the patch) |
⚠ size ≈ right by header (1.279 GB BF16 hc_* + 0.338 GB IQ4_XS token_embd = 1.617 GB), "dropped" unmeasured, see §2.3 |
1.36 GB scratch, 0 arena slots stolen |
log line is printed by region() itself; "0 slots stolen" is a literal |
⚠ true by construction (region_slots_.clear()), not a measurement |
0.0 MiB swapouts |
max_swapout_delta_mib = 0.0 in all 11 runs |
✓ (0.25 s poll, min_avail 2.376–2.398 GiB on strata runs, 0.18–0.2 GiB above the 2.2 GiB fuse) |
llama unpatched 1.40 tok/s, 14.30 tok/s "1K prefill" |
unpatched step2_ub1024 bracket line: 14.3 / 1.4 | ✓ value, ✗ label: 1264-token prompt at -ub 1024, not a 1K prompt |
llama patched 38.11 "1K" / 63.27 "2K prefill" |
patched step2 ub1024 / ub2048 | ✓ values, ✗ labels: both are the same 1264-token prompt, -ub 1024 vs 2048. neither is a 2K prompt, so it is not comparable with strata's 2048-token 363.58 |
llama patched 3.22–3.81 tok/s decode, "1.4 -> 3.81" |
only decode run: step1 3.22. step2 runs hit EOS at once (eval 0.00 ms / 1 runs). step3 --moe-cache-mib 1536 = 0.28 tok/s |
✗ 3.81 not in raw. the 0.28 tok/s moe-cache regression isn't reported |
1,628 MB/tok unpatched |
unpatched step2_ub1024 disk0 105,851.73 MB / 65 = 1628.5 |
⚠ whole-run I/O (load + 1264-tok prefill + decode) / 65, not per decode token |
515 MB/tok patched |
patched step1 disk0 33,428.34 / 65 = 514.3 |
⚠ same whole-run metric, rounded up, and on a 22-token prompt, so the comparison with 1,628 mixes prompt lengths |
-7.8x NVMe transfers |
no pair of runs gives 7.8 (pairwise xfrs ratios 0.57–26.2; nearest 8.10 = unpatched step3 / patched step1) | ✗ unsupported |
| "96 graph splits/token" | not in any archived output | unverified |
the JSON's own summary_comparison.strata_metal_optimized_k256_q2_0 carries the unarchived numbers (13.88, 0.9285, 34.3, 84.0) next to raw runs that contradict them. summary_comparison.llama_cpp_patched_m5_lazy_moe.decode_tok_per_s = 3.81 contradicts the only raw decode (3.22). fix: either archive the runs those numbers came from (with env + cmdline), or replace them with the archived 13.80 / 91.24% / 42.1 / 107.8 / 3.22 and drop the −59% and −7.8x claims.
other measurement caveats:
- every strata number is n = 1 run of 63 decode tokens: no variance, no repeat.
prefill_msinstrata_window_cliincludes the lazy arena fill (~0.5–0.6 s, logged afterready in …ms), the 1.36 GBmemsetinhold_region(true), and the first decode step. so real GEMM prefill is faster than 320.8/363.6, but these are cold-start numbers, not prefill throughput.- the 1024/2048-token prompt token files aren't archived. decoded GEN_IDS show step2 continues raw text without a chat template, and step3 ends a truncated chat turn. all three outputs are coherent: step1 begins "The user is asking about SQLite's WAL (Write-Ahead Logging) mode…".
watchdog_policy.max_swapout_delta_mib = 128in the JSON, butrun_guarded_llamauses its 64 MiB default (main()never passes it), and the module docstring still saysmin_avail 3.0 GiB.
2. code review #
2.1 dynamic n_expert (K=256): PASS, with notes #
router.metalroute_body,moe.metalmoe_routeandprompt.metalroute_topkmaskidx >= n_expertto-INFINITYbefore the max, setexpto 0, and set maskedvto-INFINITYafter normalisation. the logits row stride stays 512, consistently withGemvArgs{…, n_exp_, …, 512}andlogits_ = A(4*MAXT*512). ✓- the fused
router_routegrid is(n_exp_+15)/16. the "last threadgroup routes" test usesngx(threadgroups_per_grid), so it follows the grid size.plan_bodyloopse < a.n_expert. ✓ route_topkpacksn_expert << 16 | n_tok. chunks are ≤1024 tokens, so no overflow.PgPlan/PcMmPlanArgs.n_expert=n_exp_, andmm_scanlaunchesn_exp_threads.moe_mm.metalalready handlesn_expert < 512("simdgroups past the launched ones must count 0"). ✓stream.cpp:ne_comes fromblk.0.ffn_gate_exps.weightshape[2],lay_slice sizes arebytes / ne_, the STRX header check isne == ne_, and the STRP check ish[2] == ne_.where_/smap_keep the 512 stride, which matches thel*512*4smap offsets inwindow.cpp. ✓window.cppdecodegemv(…, logits_, 512, ntok)and thepredL_gemvs pass 512 as y_stride, andgemv()takesn_outfromt.shape[1]= 256. no OOB. ✓- waste, not a bug:
PromptGemmandprompt_chain.incstill rungemm(…, Wr, logits, T, NEXP, N)with NEXP = 512. the F16Wrcopy has rows 256..511 that are never written, so prefill computes a router GEMM twice as large as needed. the extra rows are masked inroute_topk. passn_exp_here. - untested at K=256: i ran
route_parityand it passes (0 mismatches, worst weight rel err 2.16e-07). but it setsa.n_expert = 512andpad = 0. it exercises neither the K=256 masking nor MoCE, so PLAN's "(route_parity: 0 mismatches across 200 windows)" doesn't back the n_expert claim it's attached to.tests/token_chain.cpponly got the extra buffer(10) argument and still hard-codes 512 (GemvArgs{N, 512, …},grid 512/16). thepatch_strata_metal.pydocstring wrongly lists it as a dynamic-n_expert patch target. - the MTP path (
window.cpp~L1806) still hasa.n_expert = NEXPand no smap. it's fine as long as the MTP head keeps 512 experts, but nobody has checked that.
2.2 MoCE cache-prior reranking: the logic is right, with one race and one mismatch #
the GPU logic does what PLAN says:
- ranks 0..2 are picked from unperturbed
v. atrank == 3, every resident (smap[idx] != 0xFFFFFFFF), still-unpicked,v > 0candidate is scaled bymoce_scale = exp(bonus).moce_pad_isexp(atof(STRATA_METAL_MOCE_BONUS)), bit-cast into the previously unusedMoeArgs.pad. ✓ - for picks at ranks ≥3 whose
smapis resident,true_prob = best / scale.ssumandselectedusetrue_prob, sowis the unperturbed probability renormalised over the selected 10. ✓ multiplying the prob by e^b is the same as adding b to the logit, which is consistent witheff_lginahead(). - with
stream_ == false,a.pad = 0, so thercounter_buffer bound assmapis never read. ✓
problems:
- race on
smap(medium).smapis read twice per candidate: once when scaling at rank 3 and once when restoring. I/O threads writesmap_[l*NE+e] = codewhen a read completes (io_main, L368), andvictim()/ahead()evictions write0xFFFFFFFF. both run while the GPU executes the single 48-segment command buffer, and read-aheads for layer l+1/l+2 complete during it. if an expert's entry flips between the two reads,wfor that expert is off by ×e^±0.85 (2.34×) with no error. fix: record a per-lane 16-bit "scaled" mask at rank 3 and test that mask instead of re-readingsmap[ex](asimd_shuffleof the owning lane's bit). ahead()is only an approximate match for the GPU rule.eff_lgadds the bonus to every resident candidate, top-3 included, and useswhere_ >= 0, which counts in-flight reads as resident. the GPU usessmap, which counts completed reads only, and protects the top 3. so a non-resident expert in the true top-3, which the GPU always picks, can be pushed out of the topK = 8read-ahead by ≥8 boosted residents. the read-ahead under-predicts exactly the misses MoCE can't avoid. fix: rank by rawlgfor the first 3, then byeff_lg.- exactness and quality are unmeasured. MoCE with bonus 0.85 (×2.34 prob) changes which experts run for ranks 3–9. phase 1b's closed-loop λ = 0.09 is a different scale, and PLAN itself says MoCE quality is deferred to phase 5. nothing in the archived runs records whether the env var was set, so we can't even tell whether the 91% hit rates are with or without MoCE.
- the hot-slot protection (
slot.hot, capn - (ne_+32)= 2,440 of 2,728) is set only by the arena fill.hold_region(false)clears allhotflags after the first prompt, so from the second prompt of a multi-turn session onward, prompt streaming evicts decode-hot experts again. the single-prompt CLI benchmark can't show this. PLAN #30's "protected … during prompt streaming chunks" holds for the first prompt only.
2.3 zero-copy dense + mlock + DONTNEED: works, but the savings are overstated #
- the header of
K256-q2_0-metal.ggufgives 1079 dense tensors,max_dense_end = min_exp_off = file size = 4.5317 GB. the expert tensor infos point past EOF, so this is a dense-only file and the wrap guard passes.newBufferWithBytesNoCopyis called on aPROT_READ, MAP_SHAREDmap with a page-aligned length. ✓ skip tensors keep their real file offsets, and sinceW_wraps fromf.map + 0, every non-skip offset stays valid. ✓ - live dense = 2.897 GB, 2.904 GB page-rounded, which matches the measured
locked 2.90 GB. ✓ BF16hc_*(non-norm) = 290 tensors / 1.279 GB, which matchesHC read in Q8_0: 290 matrices, 680 MB(Q8 = 679.6 MB).token_embd= IQ4_XS 0.338 GB. - PLAN #30's premise is wrong. upstream
load_dense(git show HEAD:…/stream.cpp) already skipped thehc_*originals (offset = 0; continue). it copied only the other 3.24 GB (4.514 − 1.279) into anon memory, not "4.53 GB", via F_NOCACHE preads. the HC Q8 conversion also reads the BF16 originals throughst_->read(F_NOCACHE pread) when streaming, so those mmap pages are never faulted in, andMADV_DONTNEEDon them is a no-op. compared with upstream, the change trades 3.24 GB of anon memory for 2.90 GB of mlocked file pages, a real saving of ≈0.34 GB (token_embd), not "+1.60 GB freed for the expert arena". both kinds are equally unreclaimable once wired. - the log line
"(dropped 1.60 GB dead BF16 HC + CPU embd pages)"and"0.00 GB anon"are string literals in the patch. they don't count anything. compute and print the actual skip/embd byte totals. mlockfailures are silent (locked_bytesjust comes out smaller). fine, since the run printed 2.90.
2.4 separate PromptGemm scratch: the "reclaim" doesn't work as described #
region()withSTRATA_METAL_SEPARATE_PROMPT_BUF=1(the default)mmaps a 1.36 GBMAP_ANON|MAP_PRIVATEbuffer, andwindow.cppwraps it withcount_allocated=false. ✓ no arena slots are taken.hold_region(true)still doesmemset(region_, 0, 1.36 GB)on every prompt. that made sense when the region aliased arena slots. for a fresh separate buffer it dirties all 1.36 GB on every prompt and is counted inprefill_ms.MADV_DONTNEEDon darwin anonymous memory does not discard contents or reducephys_footprint. it's advisory and the contents are preserved. under pressure the dirty pages get compressed or swapped, not dropped. the darwin idiom isMADV_FREE_REUSABLE, withMADV_FREE_REUSEbefore reuse, ormunmap+ re-mmap(sources: Mozilla bug 1567366, Chromium's switch toMADV_FREE_REUSABLE, arcbox's measurement that DONTNEED leaves "footprint unchanged, contents preserved"). the buffer is also wrapped as a Metal buffer, which can keep it resident. so "reclaimed via MADV_DONTNEED after prefill" is unverified and probably false. nothing measured footprint after prefill: the only footprint line is printed before the prompt.count_allocated=falsealso takes the 1.36 GB out of the arena planner's "planned resident footprint 5.5 GB". the real peak during prefill is ≈ 5.0 + 1.36 GB, which the cap logic never sees. on this machinemin_availalready bottomed at 2.38 GiB against a 2.2 GiB fuse, so a smaller-RAM or busier mac has less margin than the planner thinks. fix: count the scratch in the plan, skip the memset for the separate buffer, useMADV_FREE_REUSABLE/munmap, and logtask_infophys_footprintafter prefill to prove the reclaim.
2.5 convert_k256_q2_0.cpp vs ref_quant.cpp: layout PASS, quality unmeasured #
- block = 64 elements / 18 bytes.
dis fp16 at bytes 0–1, thenqs[16]. elementi = 4j+kgoes in byte2 + jat shift2k, which is exactlyref_quant.cppdeq_block(Q2_0):b[2 + j/4] >> ((j%4)*2) & 3. code = q + 1 and value = (code − 1)·d. ✓ it matches the ggml codec recorded in PLAN #28. - slice = 2560·640/64·18 = 460,800 B and expert = 1,382,400 B. 48·256 experts = 16.987 GB, which matches the file size of
K256-q2_0-metal.gguf.experts(16,986,947,584 B = 15.82 GiB). 85 × 16 KiB slots × 2728 = 3.799 GB arena. ✓ rows (2560 for gate/up, 640 for down) are multiples of 64, so no block straddles a row. ✓ the input.expertsis 23.42 GiB. ✓ - the header patch rewrites only the type of the 144
*_expstensor infos.gguf_literecomputesbytesfrom type and shape, solay_sizes match the STRX table, andsrc_sz(the input file size) equals the byte-copied output file. ✓ - the quantizer searches
d ∈ {0.5…1.0}·amax(9 steps) for minimal SSE on the asymmetric {−1,0,1,2} grid. that's reasonable, and better than ggml's fixedd = amax. note the error is evaluated with the unroundedd. - the quality gap is high-impact. this is a second lossy quantization (IQ3_S 3.44 bpw / IQ4_NL 4.5 bpw → Q2_0 2.25 bpw on already-quantized weights). phase 1c's
K256_reap_l2_sumKLD 0.564 and top-1 79.8% were measured on the IQ3_S GGUF, not on this file. no KLD, top-1 or tier-2 result exists forK256-q2_0-metal, alone or with MoCE 0.85. every strata speed number in §7.1 #5 is for a model of unknown quality. - per-expert size isn't a multiple of 4 KiB (1,382,400 = 337.5 pages), so odd experts start 2 KiB off alignment for F_NOCACHE preads. upstream's mixed format was unaligned too, so this isn't a regression, but padding each expert to 85 × 16 KiB in the file would make every read aligned.
2.6 prepare_strata_k256.py #
- STRP:
<4s5I= magic, ver, NL, NE, n, n, and the reader checksh[1]==NL,h[2]==ne_and iteratesh[4]. ✓ new expert ids follow keep-map order, which is sorted, and the slicer also sorts (slice_qwen4exp_gguf.pyL511). ✓ - the profile is ranked by in-sample REAP calibration counts. if the eval prompts (the SQLite prompt,
data/eval_ops_tools.txt) overlap that calibration distribution, the warm hit rates are optimistic. not checked. - the docstring says "6.8 GiB dense" and "17.0 GiB STRX". the real files are 4.22 GiB and 23.42 GiB. stale.
2.7 patch_m5_lazy_moe.py #
- the lazy-range,
F_RDAHEAD 0andPOSIX_MADV_WILLNEEDprefetch logic is sound. the decode fast path (ids->ne[1] <= 2) gives each thread whole experts (one_chunk(…, 0, ne01, 0, cne1, …)) and returns early. that's correct, but load-balanced only to whole-expert granularity (10 experts over 8 threads). - the patched
--moe-cache-mib 1536path collapsed to 0.28 tok/s (step3). PLAN never mentions it, and it's the one result that argues against that path.
3. provenance #
results/phase0_phase2_m5_validation.jsonmtime 06:52:33.scripts/patch_strata_metal.pyand every patchedvendor/strata/metal/**file 06:54:06,build-metal/strata_window_cli06:54:09.patch_strata_metal.pystarts withgit checkout -- ., so the vendor mtimes would move even for a byte-identical re-run, but the script itself was rewritten after the results. the measured binary is unrecoverable.run_phase0_phase2_m5_validation.pywrites{hardware, model_variant, shards, initial_vm_stat, runs}. the results file has{summary_comparison, strata_optimized_runs, llama_cpp_patched_runs, llama_cpp_unpatched_runs, watchdog_policy}, so it was assembled by something that isn't in the repo. the strata runs carry nocmd,vm_before/vm_afteror env, soSTRATA_METAL_MOCE_BONUS,--n-expert,--ple-ggufand the prompt token files can't be recovered.
4. what to fix (in order) #
- PLAN §7.1 #5, §2.7.A #29–#30 and
summary_comparison: replace the unarchived numbers with the raw ones (13.80 / 91.24% / 42.1 misses / 107.8 MB/tok / 16.05 ms / 55.79 ms. llama decode 3.22, ssd metric relabelled "whole-run MB / 65 tok". prefill labels "1264-tok prompt at ub=1024/2048"). or re-run and archive the MoCE-on vs MoCE-off pair behind −59%. drop −7.8x and "+1.60 GB freed" (≈0.34 GB vs upstream). add the 0.28 tok/s moe-cache result. - commit a strata runner that records cmdline, env, binary hash and vendor diff hash into the JSON, plus the prompt token files. re-run 3× per config.
- measure the quality of
K256-q2_0-metal(KLD/top-1 vs the IQ3_S K256 and vs unpruned) at bonus 0 and 0.85 before anyone quotes its tok/s as the finalist's speed. - MoCE: a register mask instead of the second
smapread, top-3 protection inahead(), and a parity test withn_expert = 256,pad = exp(0.85)and a random smap, against a CPU replica. - scratch: no memset for the separate buffer,
MADV_FREE_REUSABLEormunmap, count it in the arena plan, and logphys_footprintafter prefill. replace the hard-coded "1.60 GB" / "0.00 GB anon" / "0 slots stolen" strings with computed values. - prompt router GEMM over
n_exp_rows, notNEXP. keep the hot-slot flags (or re-mark them) across prompts.