dasMetal/dasLLAMA: Metal-4 tensor lane — tune-raced matmul2d twins across 12 kernel families - #3584
dasMetal/dasLLAMA: Metal-4 tensor lane — tune-raced matmul2d twins across 12 kernel families#3584borisbat wants to merge 17 commits into
Conversation
…he whole Metal surface The metal rails could not adopt the #3580 hot-path contracts: every kernel dispatch pushes capture/hazard records, and PERF026's escapes could not describe why that is fine. Four checker gaps, each with a fixture case: - @scratch on MODULE GLOBALS (the clear()-recycled capture-rail shape; `var @scratch g : array<T>` — the annotation parses after `var`) via an ExprVar arm in is_scratch_dest, which also FOLLOWS LOCAL REFERENCE BINDINGS (`var lst & = pool.free_bufs[b]` carries the mark). - `pop` joins the sizing set (das pop lowers through resize; a shrink of a reused buffer is a store). - indexing a @scratch TABLE is exempt (the pool / residency-cache shape: insert on first touch, steady-state [] is a hit). - chain suppression honors a nolint ANYWHERE along the chain — the sink often bottoms in daslib/builtin.das, so the honest line is an intermediate call site in a required module; that line now works. dasMetal: metal_command_buffer_failed(cb) : bool — the hot-path probe (status only; the error-string extern allocates its message, success paths should not fetch a string to test emptiness). free_bufs / pipeline cache marked @scratch. Metal surface: [hot_path] on all six roots (upgrading #3580's [no_env, no_io] "no_alloc pending" markers — this is that audit) — metal_decode_forward, metal_batch_decode_forward, metal_prefill_forward, metal_mtp_draft_forward, metal_mtp_seam_row, metal_mtp_spec_eval. 801 PERF026 findings driven to ZERO: - ~20 @scratch marks on genuinely recycled containers (capture/hazard rails, buffer pools, residency + PSO + schedule caches, KV/DN mirrors, per-step staging, the capped trace ring, session rope/mtp scratch). - [cold_path] on the decline recorders, live-reload flushes, arena rebuild/defrag, schedule compile, and the signs-table lazy build (split out of the hot accessor). - six error-check sites converted to the bool probe with a [cold_path] cb_error_text fetch on the failure leg — the per-step string alloc is gone for real. - prefill per-call transients (bks/bvs/cbs) hoisted to reused @scratch globals through reference bindings (their end-of-scope deletes dropped). - parallel_argmax partials became fixed arrays (ARGMAX_MAX_CHUNKS=64 caps chunking, not correctness) — no heap on that path at all. - three reasoned nolints (the batch cb list: two steps' lists are alive at once, a reused global cannot serve). Gates: perf fixtures (expect 7, cases 9-12), tests/metal/test_metal_sdot, dasLLAMA test_kernel_backend / test_forward / test_prefill / test_batch_decode / test_mtp -jit, and the metal decode + prefill parity suites (llama arm, DASLLAMA_METAL_HAZARD_STRICT=1) — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tal-4 tensor lane, step 0) metal_new_library_from_source relied on MTLCompileOptions' DEFAULT language version, which tracks the SDK the module binary was linked against — the same OS compiles metal_tensor on a freshly built module and rejects it (`mpp` / `dextents` undeclared) on one built under an older CLT (probe-verified: M1 fresh vs m4's 07-26 build, both macOS 26.5.2). Pin languageVersion to MTLLanguageVersion4_0 under @available(macOS 26) so runtime compilation is deterministic. MSL 4 compiles existing 3.x sources; the metal test suite and the dasLLAMA metal kernels are unaffected (probe + test_metal_sdot green). Step-0 probe results (recorded in plans/metal4-tensor-lane.md): matmul2d over tensor_inline views built in-shader from raw device pointers runs EXACT on the M1 Max fallback — the tensor lane integrates emitter-only, no host MTLTensor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssion) Phase A of the tensor lane: three tmm2d_* builtins (f32*bf16, q8*q8 with f32 or f16 block scales) lower through msl_emit into generated mpp::tensor_ops::matmul2d helpers over tensor_inline views — plain buffer binds, no host MTLTensor. CPU-replay stubs make every kernel exactly replayable; conformance test proves GPU==CPU bit-exact on the M1 fallback. Crown store (runtime.metal_tensor sidecar knob) lands in dasllama_common. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MetalBf16MulMmT — the first tune-raced tensor pso twin: same binds, same 32x64 tile and 128-thread dispatch as the simdgroup kernel, body lowered to a single tmm2d_f32_bf16_f32. Crown consulted once at pso build (runtime.metal_tensor); tgmem bind skipped when the twin is selected. The access classifier learns tmm2d pointer operands (buffer-pointer locals resolve to their tracked buffer; C writes, the rest read). Gate: gemma4e-e4b matrix row with a force-crowned manifest — PLE GPU pre-step rides the twin, token-for-token vs CPU truth green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
metal_tensor_race() races each tensor pso twin against its shipped simdgroup kernel on this box's GPU (compile + output-envelope to qualify, best GPU time wins); tune_kernels runs it after the CPU sweeps and writes the winner list into the sidecar's runtime.metal_tensor. M1 gate: twin qualifies and loses (0.86ms vs 1.5ms) — empty crown list, simdgroup everywhere, exactly the pre-M5 expectation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…32 X) New tmm2d_q8b_f32 builtin for the production MulMm dataflow: 34B-block W (two views of one blob) x raw f32 activations, mixed i8xf32 matmul2d chunks with the per-(col,chunk) W-scale fold. Conformance kernel is GPU==CPU exact through the interleaved 34-stride views. MetalQ8MulMmT rides the "mulmm_q8" crown on g_pf_pso_mm; race entry added (loses on M1 fallback: 1.79ms vs 0.71ms, envelope-clean). Also restores the [init] adjacency on dasllama_metal_prefill_register that the race section had split (unknown-override failure), and moves the race to end-of-module. Gate: llama prefill parity base+cont with the crown force-applied — gemm=mulmm rode the twin, token-for-token vs CPU green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… stores Q8GemmB/Q8GemmBSk/Q8Gemm64B twins (decode side) and the MetalMoeMulMmQ8 twin — contiguous down-site only, picked per dispatch (bkt-gathered X cannot form a strided tensor view, so gate/up keep the simdgroup kernel even when "moe_mulmm_q8" is crowned). tmm2d_q8b_f32 gains an explicit W-row-stride-in-blocks arg (split-K passes a full-row stride with a k-slice kk). Race scaffolding moves to dasllama_metal_common; the tuner aggregates prefill + decode races (six families, all envelope-clean and losing on M1 fallback). Gate: llama batch decode parity with all crowns forced — 3/3 green. Compiler fix uncovered by the move: dead-store elimination stripped a store whose impure rhs is operator-topped, leaving a bare `/` statement that the top-level no-side-effect lint rejects when a reused shared module re-lints its optimized AST. Such stores now stay whole (src/ast/ast_dse.cpp); the optimization_dead_stores arm re-pins it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tmm2d_tg_begin/step/store trio for dequant-staged formats (mx4 LUT, k-quant sub-scales): stage flat row-major f16 tiles per K-chunk, step folds each chunk into a persistent accumulator, store writes C. CPU stubs are the full replay; the MSL emitter arms land next. Probe-proven ground truth behind the shape: threadgroup-half tensor_inline views work as matmul2d operands, and run() OVERWRITES a cooperative destination — accumulation must be the explicit per-chunk fold (exact in the chunked probe). Also proven: e2m1-doubled does not fit int4b (+/-8, +/-12 overflow), so LUT->f16 staging supersedes the repack-plane route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline MSL emission keyed by the accumulator variable: begin declares the descriptor/op/threadgroup-half view type/persistent cooperative fold target (zeroed), step builds flat [rows][kk] views over the staged @Workgroup f16 tiles, runs the chunk, and folds (run() overwrites a cooperative destination), store writes C strided. The acc local is CPU-replay state only — its declaration is suppressed on GPU; the MPP include now also rides tg-protocol use (no generated helper involved). Conformance gains a staged two-chunk kernel: GPU == width-1 CPU replay exact. Access classifier learns the three calls (store writes arg 1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…here The first tmm2d_tg consumer: flat [rows][32] f16 tiles per K-chunk with per-element doubled-e2m1 LUT dequant, per-expert bias folded as one rank-1 extra chunk (f16-staged — parity-tolerance vs the base's f32 accumulator seed), contiguous down-site only behind the "moe_mulmm_mx4" crown (gate/up keep the simdgroup kernel per dispatch). Gate: gptoss matrix row force-crowned — engage + logits-tolerance cells green. @ROLE on @ssbo fields is now optional on un-lensed classes too (Boris): undeclared = the body derivation is the truth, declared still cross-checks. The new twin classes carry none. The access classifier suffix-matches the tg-protocol trio — generic instances carry a module prefix in post-infer call names that exact matching missed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KqMulMmK4T/K5T/K6T on the tmm2d_tg rail: flat f16 tiles per K-chunk with per-element kmask / qh-plane / sub-scale dequant — same real products as the base kernels' staged va values. Crowns kq_mulmm_k4/k5/k6 picked in pf_enc_kq_site_mm per format. Races run on random quant planes with constant scales — the twin-vs-base envelope doubles as a layout-agreement proof of the per-element decodes (a missing twin tgmem bind initially zeroed the staged tiles in the race; staged twins bind their own tgmem). Gate: llama prefill kq arm force-crowned (k4 alone, then all three) — token parity green; all nine raced families envelope-clean on M1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
race_moe_mulmm_mx4 on the expert scaffolding: random nibble planes, near-1.0 e8m0 scales, the doubled-e2m1 LUT, bias off — twin-vs-base envelope proves the mx4 per-element decode agrees with the base kernel on arbitrary bit patterns. Ten families race on M1, all envelope-clean, all losing to their simdgroup kernels on fallback hardware (empty crown list — the pre-M5 expectation the tuner writes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both ride the staged-tile protocol with no new builtins: QK pre-scales Q at staging (the base's discipline) and copies its causal/window block skips (threadgroup-uniform, cooperative-safe); AV stages V transposed with pad positions >= npos zeroed and walks the same causal klimit. Crowns attn_qkmm/attn_avmm in enc_qk_mm/enc_av_mm. Races pre-zero BOTH output slabs (causal-skipped tiles stay unwritten in both kernels). Gate: llama prefill base+cont+dim+qkv with all TWELVE families force-crowned — token parity green; full race sweep envelope-clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sequential per-side timing blocks read the box's thermal/clock ramp as a phantom winner — measured live on the m4: the standalone probe crowned kq k4/k5/k6 +64% and q8 +23% for tensor, the tuner's identical race in its own process context INVERTED both (k4 base 0.51ms vs the probe's 1.60ms — same binary, same box). race_pair_ms alternates one base and one twin dispatch per rep (the reds-dig2 adjacent-pairs doctrine), best-of per side; every family race site rewritten onto it. M1 verdicts unchanged (all simdgroup, stable margins). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR extends the Metal backend and dasLLAMA Metal kernels with a “tensor lane” by introducing Metal-4 mpp::tensor_ops::matmul2d-lowered twins for matmul-shaped kernels, then adds a tuner race to crown the best variant per box. It also includes a dead-store-elimination fix to avoid generating lint-illegal “pure-op” statements, and expands PERF026’s @scratch contract to cover additional reuse patterns.
Changes:
- Add MSL emitter support + CPU-replay stubs for
tmm2d_*tensor-op markers (including a staged-tile protocol) and introduce a conformance test covering GPU vs CPU replay. - Add dasLLAMA Metal prefill/decode “twin” kernel families, plus tuner-side racing that writes winners into the sidecar runtime’s
metal_tensorcrown list; runtime consults crowns at PSO build. - Fix
ast_dseto keep operator-topped impure dead stores intact (avoids top-level no-side-effect lint failures) and broaden PERF026@scratchhandling (globals, table indexing, ref bindings, mid-chainnolint).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| utils/lint/tests/perf026_hot_path_alloc.das | Updates PERF026 fixture expectations and adds new cases for @scratch globals/tables/ref bindings and mid-chain nolint. |
| tests/metal/test_metal_tensor_ops.das | New test validating generated MSL helpers + CPU replay vs GPU exact match for tensor/staged-tile paths. |
| tests/language/optimization_dead_stores.das | Adjusts test to pin the new DSE behavior for operator-topped impure RHS. |
| src/ast/ast_dse.cpp | Prevents DSE from stripping impure stores into pure operator statements that trip lint. |
| skills/perf_lint.md | Documents @scratch on globals and expanded suppression/coverage semantics. |
| modules/dasMetal/src/dasMetal.mm | Pins MSL language version on macOS 26+, adds non-allocating command-buffer failure probe, and exports it. |
| modules/dasMetal/src/dasMetal.h | Declares metal_command_buffer_failed. |
| modules/dasMetal/metal/msl_emit.das | Adds tensor-op lowering: generated helper emission, write-set scanning updates, staged-tile protocol emission, and MPP include injection. |
| modules/dasMetal/metal/metal_builtins.das | Adds CPU-replay reference implementations for tmm2d_* and staged-tile begin/step/store protocol. |
| modules/dasMetal/metal/das_metal_boost.das | Marks pipeline cache and buffer pool tables as @scratch to match intended reuse semantics. |
| modules/dasLLAMA/harness/tune_kernels.das | Runs tensor twin races and writes the metal_tensor crown list into the runtime sidecar snapshot. |
| modules/dasLLAMA/dasllama/dasllama_metal_prefill.das | Adds prefill-side twin kernels, crown-based PSO selection, scratch reuse of per-call arrays, and race implementations. |
| modules/dasLLAMA/dasllama/dasllama_metal_llama.das | Uses non-alloc failure probe + cold-path error text; expands @scratch use; annotates hot paths. |
| modules/dasLLAMA/dasllama/dasllama_metal_lens.das | Makes @role optional (derivation becomes authoritative) for Metal dispatch census. |
| modules/dasLLAMA/dasllama/dasllama_metal_kernels.das | Adds decode-side batch-GEMM tensor twins, crown-based PSO selection, and decode-side races. |
| modules/dasLLAMA/dasllama/dasllama_metal_common.das | Adds cold_path annotations, scratch globals for reuse, and shared race scaffolding utilities. |
| modules/dasLLAMA/dasllama/dasllama_math.das | Makes parallel_argmax allocation-free on the hot path via fixed-size partial buffers. |
| modules/dasLLAMA/dasllama/dasllama_kernel_access.das | Updates access classifier to understand tmm2d pointer operands + tg protocol and pointer-local aliasing. |
| modules/dasLLAMA/dasllama/dasllama_common.das | Adds runtime metal_tensor crown parsing/storage and marks rope-table builders @scratch. |
| doc/source/reference/language/lint.rst | Updates PERF026 docs to include @scratch globals and expanded semantics. |
| daslib/perf_lint.das | Extends PERF026: pop counted as sizing, @scratch globals/ref bindings recognized, scratch table indexing exempt, and nolint honored along the full call chain. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Measurements are a property of the box AND its OS (Metal fallback perf moves with macOS updates), so the sidecar's provenance gains a box identity stamp (platform|arch|hardware|OS-build|cpu) and the staleness check treats a mismatched or missing stamp exactly like an outdated mtime: reads as absent, load_sidecar_doc resets, the tuner regenerates. Closes the cross-box-copy hazard end to end — a foreign-seeded sidecar can no longer satisfy daspkg release completeness or ship wrong crowns, and the reset also drops the foreign file's other-scope keys instead of re-stamping them under a fresh identity. Manifest test gains the foreign-box arm (winners must not stamp; fallback tier engages). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Two additions since open, both from walking the release-flow end to end:
Note for existing boxes: pre-identity sidecars re-tune once on the next run (they'd already be mtime-stale against a rebuilt binary, so no extra churn in practice). |
The direct tmm2d builtins are non-generic so their resolved names stay bare today, but the exact/suffix split in one classifier was a latent under-tracking path — one rule now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
daslib/perf_lint.das:226
- The casts in is_scratch_dest use invalid member access syntax (
(expr as ExprRef2Value.subexpr)etc.). This won’t compile; the.subexprneeds to be accessed on the cast result ((expr as ExprRef2Value).subexpr).
if (expr is ExprRef2Value) return is_scratch_dest((expr as ExprRef2Value.subexpr))
if (expr is ExprAt) return is_scratch_dest((expr as ExprAt.subexpr))
if (expr is ExprSafeAt) return is_scratch_dest((expr as ExprSafeAt.subexpr))
if (expr is ExprCast) return is_scratch_dest((expr as ExprCast.subexpr))
The darwin15 CI runner has a Metal device but no MPP / MSL 4.0, so the pso-compile assertions tripped. A trivial MPP-include probe now feints the GPU half where Metal 4 is absent — the production races already degrade the same way (compile-probe eligibility); on a Metal-4 box the real kernels still assert loudly. CPU-replay half runs everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
daslib/perf_lint.das:227
- These casts are malformed:
(expr as ExprRef2Value.subexpr)(and similar) tries to access.subexpras part of the cast type, which should be a syntax/compile error. It looks like the intent is to recurse into the wrapper node’ssubexprfield.
Fix by casting first, then accessing .subexpr (same for ExprAt/ExprSafeAt/ExprCast).
if (expr is ExprRef2Value) return is_scratch_dest((expr as ExprRef2Value.subexpr))
if (expr is ExprAt) return is_scratch_dest((expr as ExprAt.subexpr))
if (expr is ExprSafeAt) return is_scratch_dest((expr as ExprSafeAt.subexpr))
if (expr is ExprCast) return is_scratch_dest((expr as ExprCast.subexpr))
return false
The S3 Metal-4 tensor lane: every matmul-shaped Metal kernel family gains a tensor pso twin lowered through
mpp::tensor_ops::matmul2d, raced against its shipped simdgroup kernel by the tuner per box, with the winner crowned into the tune sidecar'sruntime.metal_tensorlist. Pre-M5 boxes keep simdgroup everywhere (measured); M5 day is a tuner run per box, zero construction.Emitter rail (dasMetal)
tmm2d_f32_bf16_f32,tmm2d_q8_f32/tmm2d_q8_f16s(i8×i8 with per-block scale folds),tmm2d_q8b_f32(34B-interleaved q8_0 W × raw f32 X, explicit W-row-stride-in-blocks for split-K): named-call interception emits generated MPP preamble helpers overtensor_inlineviews built from the existing plain buffer binds — no host MTLTensor, encode paths untouched.tmm2d_tg_begin/step/storefor dequant-staged formats: flat row-major f16 tiles per K-chunk in threadgroup memory, a persistent cooperative accumulator with an explicit per-chunk fold (run()on a cooperative destination overwrites — probe-proven), the das-side accumulator array is the CPU-replay state and its declaration is suppressed on GPU.tests/metal/test_metal_tensor_ops.das— every form GPU == CPU-replay bit-exact (integer-valued data + pow2 scales make all float orders exact), incl. the staged two-chunk kernel.Twin families (dasLLAMA)
Prefill:
Bf16MulMm,Q8MulMm,MoeMulMmQ8+MoeMulMmMx4(contiguous down-site only — bkt-gathered X cannot form a strided tensor view; gate/up fall back per dispatch),KqMulMmK4/K5/K6(per-element kmask / qh-plane / sub-scale dequant into flat tiles),AttnQKMm/AttnAVMm(Q pre-scaled at staging; V staged transposed with pad-position zeroing; causal/window skips are threadgroup-uniform, cooperative-safe). Decode:Q8GemmB/Q8GemmBSk/Q8Gemm64B. Crowns are consulted once at pso build; the twin variant skips the tgmem bind when it has no tiles.GEMV twins are structurally impossible:
matmul2dhard-asserts M % 8 == 0 (MPPTensorOpsMatMul2dImpl.h:4257), so the skinny-M decode forms (m=1/2/4) cannot compile — the race's compile-probe eligibility handles this automatically. Revisit on M5 only via an X-pad-to-8 staging redesign if the GEMM races justify it.Tuner race
tune_kernelsrunsmetal_tensor_race()+metal_tensor_race_decode()after the CPU sweeps and writes the winner list into the sidecar. Races run on synthetic planes (random quant bit patterns + constant scales — the twin-vs-base envelope doubles as a layout-agreement proof of every per-element decode). Races interleave adjacent base/twin dispatch pairs per rep — sequential per-side blocks read the GPU clock ramp as a phantom winner (caught live on an M4: sequential timing manufactured +64% k-quant "wins" that the interleaved protocol and the tuner's own context both refute).Measured verdicts: M1 Max — all 12 families stay simdgroup. M4 Pro —
mulmm_bf16tensor +2.8% (crowned), rest simdgroup.tests/metal41/41 green on both boxes.Also in this PR
ast_dse.cppfix: dead-store elimination stripped a store whose impure rhs is operator-topped to a bare op2 statement, which the top-level no-side-effect lint rejects when a reused shared module re-lints its optimized AST. Operator-topped impure stores now stay whole;optimization_dead_stores.dasre-pins the behavior (35/35).@roleis now optional everywhere: undeclared@ssbofields derive read/write from the kernel body (the census still cross-checks declared roles). The access classifier learns the tmm2d pointer operands and suffix-matches generic-instance call names.Gates
Force-crowned via a
DAS_TUNE_MANIFESTcopy: llama prefill (base/cont/dim/qkv/kq), llama decode arm+batch suites, gemma4e-E4B token parity (PLE bf16), qwen3moe matrix row (MoE down-site), gptoss matrix row (mx4).tests/metal15 files, kernel-units suite, DSE suite.preflight --full: 16/17 — the one red is a known pre-existing untracked local test (tests/jit_tests/exe_funcptr_globals.das, not part of this PR), hence the announced no-verify push.🤖 Generated with Claude Code