Skip to content

dasMetal/dasLLAMA: Metal-4 tensor lane — tune-raced matmul2d twins across 12 kernel families - #3584

Merged
borisbat merged 18 commits into
masterfrom
bbatkin/metal4-tensor-lane
Jul 28, 2026
Merged

dasMetal/dasLLAMA: Metal-4 tensor lane — tune-raced matmul2d twins across 12 kernel families#3584
borisbat merged 18 commits into
masterfrom
bbatkin/metal4-tensor-lane

Conversation

@borisbat

Copy link
Copy Markdown
Collaborator

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's runtime.metal_tensor list. Pre-M5 boxes keep simdgroup everywhere (measured); M5 day is a tuner run per box, zero construction.

Emitter rail (dasMetal)

  • Direct formstmm2d_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 over tensor_inline views built from the existing plain buffer binds — no host MTLTensor, encode paths untouched.
  • Staged-tile protocoltmm2d_tg_begin/step/store for 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.
  • Runtime MSL language version pinned to 4.0 on macOS 26 (the default tracks the SDK the module binary was linked against — verified on both dev boxes).
  • Conformance: 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: matmul2d hard-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_kernels runs metal_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_bf16 tensor +2.8% (crowned), rest simdgroup. tests/metal 41/41 green on both boxes.

Also in this PR

  • ast_dse.cpp fix: 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.das re-pins the behavior (35/35).
  • @role is now optional everywhere: undeclared @ssbo fields 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_MANIFEST copy: 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/metal 15 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

borisbat and others added 14 commits July 27, 2026 20:02
…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>
Copilot AI review requested due to automatic review settings July 28, 2026 06:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_tensor crown list; runtime consults crowns at PSO build.
  • Fix ast_dse to keep operator-topped impure dead stores intact (avoids top-level no-side-effect lint failures) and broaden PERF026 @scratch handling (globals, table indexing, ref bindings, mid-chain nolint).

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.

Comment thread modules/dasLLAMA/dasllama/dasllama_kernel_access.das
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>
Copilot AI review requested due to automatic review settings July 28, 2026 07:21
@borisbat

Copy link
Copy Markdown
Collaborator Author

Two additions since open, both from walking the release-flow end to end:

  • 2b61c1e55 — races interleave adjacent base/twin pairs. Sequential per-side timing blocks read the GPU clock ramp as a phantom winner: on an M4 the sequential harness manufactured +64% k-quant tensor "wins" that its own tuner context and the interleaved protocol both refute. Verdicts are now identical from three different process contexts on that box (real M4 result: mulmm_bf16 tensor +2.8%, everything else simdgroup).
  • b27b224d1 — sidecar box-identity gate (llvm_tune). daspkg release runs the scope tuners only for incomplete sidecars, so a sidecar copied from another box (CPU-complete, foreign crowns) would skip the tune and ship wrong winners. The provenance now carries a box identity stamp (platform|arch|hardware|OS-build|cpu) and a mismatched or missing stamp reads as stale — winners can't outlive the box+OS that measured them. The reset path also drops a foreign file's other-scope keys instead of re-stamping them. Manifest test gains the foreign-box arm.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 28, 2026 07:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .subexpr needs 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>
Copilot AI review requested due to automatic review settings July 28, 2026 08:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .subexpr as part of the cast type, which should be a syntax/compile error. It looks like the intent is to recurse into the wrapper node’s subexpr field.

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

…pile)

The helper named MetalDevice at top level — a type that only exists when
das_metal is built, breaking the Windows/Linux compile of the test. An
untyped parameter makes it a generic instantiated only inside the
Apple static_if half, matching the rest of the file's guard discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 08:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • is_scratch_dest has malformed casts like (expr as ExprRef2Value.subexpr) (missing ).subexpr). This won’t parse/compile and breaks the new @scratch-destination logic for refs/tables/globals.
    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))

@borisbat
borisbat merged commit 4fc408a into master Jul 28, 2026
34 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants