opus r15 track A: prompt_chunk wave zipping, fused alpha|beta GEMM, QSA PSO pre-warm, 24 GB chunk 2048 #

patch: /tmp/opus_r15_win/opus_r15_win.patch (git -C vendor/strata apply --check clean). touches only metal/src/engine/window.cpp (prompt_gemm_init / prompt_gemm_need_bytes / prompt_chunk, original lines 3045-3606) and metal/kernels/gdn_prompt.metal (gdn_pre). scratch tree: /tmp/opus_r15_win/strata (build in build-metal). scratch-only test harnesses (NOT in the patch): /tmp/opus_r15_win/scratch_gdn_prompt_parity.cpp.diff, /tmp/opus_r15_win/scratch_dense_mm_parity.cpp.diff.

what changed #

  1. QSA PSO pre-warm. prompt_gemm_init compiles qsa_score_mm / qsa_topk (keys pg:qsa_score_mm, pg:qsa_topk, same as PP() in prompt_chunk). pcache_ outlives free_prompt_gemm, and the streaming load path calls prompt_gemm_init at load (window.cpp ~3783), so the ~200 ms compile moves to startup. caveat: it only ever hit the first >2051-token prompt of a process (pcache persists), so later prompts were already fine.
  2. stream_chunk 2048 on >= 24 GB in both prompt_gemm_init and prompt_gemm_need_bytes (kept identical, the need != prompt_gemm_need_bytes() check guards it). honest number from the need_bytes formula (k4, flash, stream, ple, alias): C=1024 0.74 GB / C=2048 1.19 GB at 64K cells (0.94 / 1.60 GB at 256K). so 2048 is NOT smaller than 1024 here, it costs ~+0.45 GB transient scratch (separate mmap while a prompt runs, unmapped after). if dawn's serve script already sets STRATA_METAL_PROMPT_CHUNK=2048 (her trace shows T=2048 chunks) this changes nothing for it.
  3. router look-ahead zip (stream && pred_on && l+1<48): dq(blk.l+1 ffn_gate_inp -> Wr2) joins wave 0, gemm(Wr2) runs beside gemm(Wr), route_topk(ids2) beside route_topk(ids). -2 barriers/layer (-94/chunk). mm_hist now always has its barrier (it used to run beside route_topk(ids2) with nb_ = true; now both top-ks are in one wave). PRED2 (ids3) kept as a serial tail after the zipped pair.
  4. MoE plan / shared-expert zip (comb_fold): mm_hist | Wshg (+Wshu when !swu) + Wsg GEMMs -> barrier -> mm_scan | gemm_swiglu_up (or swiglu_h) -> barrier -> mm_place | Wshd GEMM -> barrier -> mm_gather_h. 6 waves -> 4, -2 barriers/layer (-96/chunk); the 1-TG mm_scan and 20-TG mm_place now overlap real GEMM work. all of it stays inside [sigs, cuts) so it still runs beside the host's expert placement. buffer audit: plan writes bcnt/boff/eoff/tiles/meta/stok/sw/pos, shared writes gsh(=gqn)/ush(=gkn)/hh(=ygh)/ys(=bo)/sgv, both only read mixh/ids/wts/W* -> disjoint. !comb_fold (STRATA_METAL_DUMP_MOE) path unchanged.
  5. fused alpha|beta GEMM in GDN layers: beta dequantized into p.Wal at byte 2*48*N, one gemm(p.mixh, p.Wal, p.al, T, 96, N), gdn_pre gets p.al and p.al + 192 B. p.al = F32(MT*96), p.be no longer allocated (nothing else reads it; p.Wbe stays, it is QSA's Wiq). need_bytes updated the same way (equal bytes for MT % 256 == 0 anyway). gdn_pre: ab = beta == alpha + hv ? 2*hv : hv used for the alpha/beta reads only; separate buffers (prompt_chain.inc, the tests) keep stride hv. -36 dispatches/chunk. gemm() picks for Nn == 96: M < 768 ? 16_64_2 : 64_32_2 (measured below; 32_64_2, the old Nn<=128 pick, was slower).

verification (M5 Air, all < 3 s each) #

  • gdn_prompt_parity: ALL PASS (63 PASS, 0 FAIL), incl. scratch case: interleaved [T][96] alpha|beta with beta = alpha + 48 vs separate buffers, both grids (packed 8 and legacy 40), T in {1,3,37,300,2048}: gdn_pre outputs (qn, kn, vc, gb, kq, history) bitwise identical.

  • gdn_prompt_parity --ab <original gdn_prompt.metal> 2048 {0,1} bench: every stage output bitwise PASS vs the old kernel; gdn_pre time ratio 1.0000 / 0.9998 (the pointer compare costs nothing).

  • arena_budget_test: all passed (it does not exercise prompt_gemm_need_bytes; the in-process need != prompt_gemm_need_bytes() check is the guard there).

  • dense_mm_parity --fuse96 M (scratch mode): one N=96 GEMM vs two N=48 (window's current tiles), both halves memcmp-identical for every candidate tile at M = 256..2048. median us (two N48 serial -> N96):

    M 2 x N48 16_64_2 32_64_2 64_32_2
    256 43.0 23.1 28.9 29.3
    512 50.7 29.7 34.5 32.5
    768 55.5 39.0 40.0 35.9
    1024 74.5 54.1 51.3 41.2
    1536 102.2 91.2 74.7 58.4
    2048 143-168 121 102-106 90-103

    caveat: in the chunk these GEMMs run in the same concurrent wave as Wqkv/Wz, so the real saving is the removed work (one 10.5 MB mixh read + 32 wasted columns + a dispatch per layer), likely ~1-2 ms per 2048-row chunk, not the standalone 50 us x 36.

  • strata_window_cli builds clean. not run (rules: :8080 live, no full-model runs), so targets 2/3's effect on real chunk time and end-to-end greedy-token parity are unverified. recommended before merge: one short strata_window_cli prefill A/B (orig vs patch) on a 10K prompt with greedy token compare + STRATA_METAL_PKPROF per-chunk time, when the server is down.

risks #

  • zipping depends on the dispatch-order / nb_ hazard reasoning above (no automated race test for prompt_chunk).
  • the 24 GB 2048 default costs ~0.45 GB more transient scratch than 1024 at 64K cells, contrary to the brief's premise.