Round 5 Track C Report: Direct FP16 qcurh in qsa_prep + PromptGemm Dead Buffer Removal & Layer-Local Aliasing #
- Agent:
argon-qsa-prep-f16-and-scratch(opencode-go/gemini-4-argon-contributor) - Patch Artifact:
/tmp/argon_r5_scratch/argon_r5_scratch.patch(verified clean viagit -C vendor/strata apply --check /tmp/argon_r5_scratch/argon_r5_scratch.patchandclang++ -fsyntax-onlyonwindow.cpp) - Test Binaries:
/tmp/argon_r5_scratch/{qsa_prompt_parity,qsa_parity,arena_budget_test,qsa_prep_bench,need_bytes_check}
1. Summary of Changes #
Target 1: Fuse qsa_q_f16 into qsa_prep when p.flash is enabled (metal/kernels/qsa.metal + metal/src/engine/window.cpp) #
- Added
constant bool QSA_Q_F16_FC [[function_constant(25)]](qsa_qf16) tometal/kernels/qsa.metal:- When
qsa_qf16istrue,qsa_prepwrites((device half*)qcur)[(t * QH + r) * QD + tid] = half(o);directly to buffer 10 instead offloat(qcur[(t * QH + r) * QD + tid] = o;).
- When
- In
Window::prompt_chunk(metal/src/engine/window.cpplines 3424–3444):- When
p.flashistrue,qsa_prepis dispatched withqsa_c().b(25, true)(pipeline keyqsa_k() + "#qf16") andp.qcurhbound directly at buffer 10 (p.flash ? p.qcurh : p.qcur), eliminating the separateqsa_q_f16kernel dispatch. - Across the 12 QSA layers per 2048-token chunk, this eliminates 12
qsa_q_f16dispatches, 906 MB of DRAM reads/writes (12 * (50.33 MB FP32 write + 50.33 MB FP32 read - 25.17 MB FP16 write)), and makesp.qcur(50.33 MB) completely unused whenp.flash == true.
- When
Target 2: Remove Dead Prompt Scratch Allocations (p.XQ, p.yg, p.qcur) + Alias Disjoint GDN vs QSA Prompt Scratch Buffers (metal/src/engine/window.cpp lines 3080–3189) #
- Dead buffer removal (
-323.96 MBatMT=2048,-171.39 MBatMT=1024):p.XQ: Changed from unconditionalF32(4 * MT * 6144 + 3 * (size_t) p.QS * 6144 + MT * 384)(223.35 MBatMT=2048) top.flash ? F32(16) : F32(4 * MT * 6144 + 3 * (size_t) p.QS * 6144 + MT * 384).p.yg: Changed fromF32(MT * 6144)(50.33 MBatMT=2048, never written becausegdn_postruns withga.f32 = 0) toF32(16).p.qcur: Changed fromF32(MT * 24 * 256)(50.33 MBatMT=2048) top.flash ? F32(16) : p.z.
- Disjoint GDN (
l % 4 != 3) vs QSA (l % 4 == 3) layer-local scratch aliasing (-250.59 MBadditional atMT=2048,-165.15 MBadditional atMT=1024):- Because every GDN and QSA scratch buffer is produced and consumed strictly inside its own
if (l % 4 != 3) ... else ...branch beforegemm(..., p.Wout, p.bo, ...)writesp.bo, QSA's layer-local buffers cleanly alias GDN's allocations:p.qkvsized toF32(MT * 12288)(+16.78 MBover10240) and shared withp.qfull = p.qkv(-83.89 MBnet atMT=2048).p.kcur = p.gqn(512 <= 2048floats/token,-4.19 MB) andp.vcur = p.gkn(512 <= 2048floats/token,-4.19 MB).p.qcurh = p.ygh(bothF16(MT * 6144),-25.17 MB).p.gvcsized toF32(MT * std::max<size_t>(6144, (size_t) MB))and shared withp.scores = p.gvc(-50.33 MB).p.qids = p.go(2051 <= 6144words/token,-16.81 MB).p.Wqkvsized toF16((size_t) 12288 * N)(+10.49 MBover10240 * N) and shared withp.Wq = p.Wqkv(-52.43 MBnet).p.Wk = p.Wz(512 * N <= 6144 * N,-2.62 MB),p.Wv = p.Wal(Walsized toF16(512 * N),-0.25 MBnet),p.Wiq = p.Wbe(Wbesized toF16(512 * N),-0.25 MBnet).
- Both
make()inWindow::prompt_gemm_initandWindow::prompt_gemm_need_bytes()are updated in lockstep.
- Because every GDN and QSA scratch buffer is produced and consumed strictly inside its own
2. Verification & Benchmark Results on Apple M5 #
2.1 Bitwise Parity (qsa_prompt_parity, qsa_parity, arena_budget_test) #
qsa_prompt_parity:qsa_prep(QSA_Q_F16_FC) vs qsa_prep+qsa_q_f16: qcurh memcmp PASS, qsa_flash memcmp PASS(100% bitwise identicalqcurhandqsa_flashoutput).- All existing
qsa_attn_mm,qsa_flash, andqsa_score_mmchecksPASS.
qsa_parity:ALL PASS(prep,score,top-k,attention + gate).arena_budget_test:all passed(46/46 checks).need_bytes_check: Verified across 480 combinations ofC in {64, 512, 1024, 2048, 4096},flash in {false, true},stream in {false, true},pred2 in {false, true},bo_alias in {false, true},ple_on in {false, true},max_cells in {4096, 32768, 131072}thatneed == prompt_gemm_need_bytes()and every aliased buffer's capacity is>=its required QSA byte size.
2.2 Microbenchmark (qsa_prep_bench on Apple M5) #
Chunk Size T |
qsa_prep(f32) (us) |
qsa_q_f16 (us) |
Unfused Pair (us / 12L ms) |
Fused qsa_prep(qf16) (us / 12L ms) |
Savings per Chunk (12 QSA Layers) | qcurh memcmp |
|---|---|---|---|---|---|---|
512 |
226.0 us |
101.9 us |
284.5 us (3.41 ms) |
186.7 us (2.24 ms) |
-1.17 ms (-34.3%) |
PASS |
1024 |
454.7 us |
239.1 us |
699.7 us (8.40 ms) |
373.2 us (4.48 ms) |
-3.92 ms (-46.7%) |
PASS |
2048 |
933.9 us |
544.6 us |
1497.2 us (17.97 ms) |
754.4 us (9.05 ms) |
-8.91 ms (-49.6%) |
PASS |
2.3 prompt_gemm_need_bytes() Memory Reduction (max_cells = 32768, stream = true, ple_on = true) #
Chunk Size C |
Baseline need |
After Dead-Buffer Removal (-XQ, -yg, -qcur) |
After GDN/QSA Layer-Local Aliasing | Total Saved |
|---|---|---|---|---|
1024 (flash=true) |
1.286 GB (1,286.24 MB) |
1.115 GB (-171.39 MB) |
0.950 GB (949.70 MB) |
-336.54 MB (-26.2%) |
2048 (flash=true) |
2.146 GB (2,146.29 MB) |
1.822 GB (-323.96 MB) |
1.573 GB (1,572.76 MB) |
-573.53 MB (-26.7%) |