dasLLAMA: dig the last two board reds green — lcpp-shape mx4 kernels + GPU PLE pre-step, fresh M1 board#3558
Merged
Merged
Conversation
The shim's g_metalLiveObjects counter says HOW MANY handles leaked but not which — the matrix-suite red (51 live objects) has been undiagnosable from the count alone. Every retain_handle now records "type @ file:line" of the das creation site (LineInfoArg already reaches every creator); metal_live_object_report() returns the grouped live set. Volume is low (planes at load, cb/encoder per step), so the mutex-guarded map costs nothing measurable. assert_no_metal_leaks panics with the report; the new metal_log_leaks_if_any() keeps dastest leak gates one line while a red run names its leaks — all five dasLLAMA metal test gates swapped onto it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
…ceipt The 27B-MTP cpu row predated the dn kq-native fix (#3544): pp 31.0/tg 5.97 at 8d6fe07. Fresh same-window trio (das + both refs, gen_bench_records): das 29.88/6.73 -> tg 1.04x both refs (the board red flipped), pp 1.14x clean / 1.12x stock. model_exec_fmt never walked the deltanet fmt tag arrays, so the receipt hid the dn plane formats (27B ran k5 ssm_out unlisted — the old and new rows read identically despite the format change underneath). Recurrent rows now feed the receipt: "weights k4/k5/k6/q8 native". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
The matrix-suite leak (51 objects across the three MoE family gates, untracked since <=92033b08e) was three uniforms: the single-decode run state acquires 12 r.u_moe_* buffers but its release ledger freed only 9 — u_moe_ss / u_moe_kdim / u_moe_tot never came back (the batch path's deferred list carries all three; the single path forgot them). 3 leaks per run-state cycle explains every historic count (15/30/36, all divisible by 3, MoE families only). Found in one run by the new per-site attribution: the report named "MetalBuffer[256] @ das_metal_boost.das:160 x15" — the untracked pool's 256-byte floor bucket, i.e. 4-byte uniforms. Buffer tags now carry the byte size (pooled buffers all create at one das line, so the size bucket is what tells leaked classes apart). Verified 0 live objects on fam-gptoss (was 15), fam-gemma4moe, and fam-qwen35moe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
The dasLLAMA tutorial 00 reader-feedback revision introduced both; CI's PDF steps are continue_on_error so they shipped silently — the local pdflatex gate caught them (the π/U+03C0 pattern). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
…riminator The reds-dig2 chase put the routed mx4 expert GEMVs at 216 wGB/s in-graph (48% of every gpt-oss decode step) vs the 321 the k4 sibling sustains. New gmx4 section prices the PRODUCTION MetalMoeGemvMx4 at the exact gpt-oss shapes (E=32, nfe=dim=2880, k=4, 24 layers, 403MB routed/pass), CPU-ref-gated bit-exact: gmx4_pipe 243 wGB/s kernel ceiling, no barriers gmx4_serial 209 wGB/s per-dispatch barrier tax (brackets the in-graph 216) gmx4_arith 97 wGB/s REFUTED: ALU e2m1 decode (no vtab loads) loses 2.5x So ~70% of the gap is kernel-internal and it is NOT the vtab table loads (disasm: 15 device loads per block iter, 8 vtab — yet computing values in ALU is far worse; table lookup wins on AGX, re-confirming the 0.40x-era verdict at the current shape). Remaining suspect: weight-load width — mx4 issues 4B uint weight loads where k4 streams 16B uint4. Next arms: a uint4-per-block form and the lcpp-verbatim port. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
…refuted Round 2 of the gpt-oss mx4 chase. llama.cpp's mxfp4 mv copies its 16-value LUT into threadgroup memory at kernel start; same change on our kernel (vtab device loads -> tgmem reads, nothing else): gmx4_pipe 240.5 wGB/s -> gmx4_tgv 258.4 wGB/s (+7.4%), bit-exact Also refuted: etab into tgmem regressed (253 vs 260) — its per-block reads are same-address broadcasts riding the uniform path, cheaper than the 1KB fill. The dump rail is now built in (DASMETAL_LAB_DUMP_MSL=<dir> writes the gmx4 kernel sources for applegpu disasm and exits). Ladder to k4's 321: next arms are weight-load width (mx4 issues 4B uints where k4 streams 16B uint4) and lcpp's 4-float4 x prefetch. Production port waits until the lab form clears ~300. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
…ape clears 301 wGB/s The gmx4 ladder round 3 (gpt-oss shapes, bit-exact vs CPU ref): pipe 239 -> tgv 257 -> lcpp 272 -> lcppw 275 -> lcppe 301 wGB/s - gmx4_lcpp: llama.cpp's mul_mv_mxfp4 thread mapping verbatim (16 blocks in flight x 2 threads/block, yl[4] float4 x prefetch shared across 2 rows, float4 accumulators, 32-slot tgmem LUT) — +6% over tgv - gmx4_lcppw: same shape, weight bytes as one uint2 (8B) per row-visit — +1% - gmx4_lcppe: e8m0 scale computed in ALU (as_type<float>) instead of the mxe->etab dependent device-load chain — +10%, clears the ~300 port gate msl_emit grows the daslib/math_bits bitcast family (uint/int<->float, scalar + 2/3/4-lane) lowering to as_type<T>, plus exp2/log2 in the math whitelist; real-GPU coverage added to tests/metal/test_metal_math.das. Lab dump rail (DASMETAL_LAB_DUMP_MSL) hoisted to main — no more full-lab wait for a disasm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
…gpt-oss tg GREEN
MetalMoeGemvMx4 rewritten to the moe-lab lcppe rung (239->301 wGB/s standalone,
serial-encoder 209->266): lcpp's mul_mv_mxfp4 mapping (16 blocks in flight x 2
threads/block, yl[4] float4 x prefetch shared across both rows, float4
accumulators), uint2 weight loads (binding 0 becomes the uint2 view — encoder
unchanged), tgmem value LUT, and the e8m0 scale computed in ALU via
uint_bits_to_float instead of the mxe->etab dependent device-load chain.
MetalMoeMulMmMx4's A-stage gets the same three: uint4 block load (16 byte loads
-> 1), tgmem LUT, computed e8m0. Both e8m0 device tables (g_e8m0_tab,
g_pf_e8m0_tab) deleted — the ALU form is bit-exact for all 256 bytes incl. the
e<2 denormals. Kernel-unit gate updated (tgmem length + dropped etab bind).
gpt-oss-20b paired vs llama.cpp stock (M1 Max, llama-bench shapes):
tg128 80.20 -> 86.58 vs 81.62 = 1.061 GREEN (board red was 0.963)
pp512 908.9 -> 986.91 vs 1024.28 = 0.963 (was 0.88; steady GPU 515ms
vs lcpp wall 500ms — remaining ~3.5% needs its own prefill chase)
fam-gptoss tolerance cells bit-identical pre/post (maxd 0.2558/0.2596);
kernels suite bit-exact incl. odd-shape tails and the bias/batch-stream arms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
…7 GREEN The gemma-4 E-series prefill pre-step (gather 512 token rows off the 3GB Q8_0 per_layer_token_embd + the 14-GMAC bf16 model_proj GEMM + rms/combine) ran on CPU: 126ms beside a 529ms GPU window that already beat lcpp's whole 558ms wall. Now it runs at the head of the prefill graph, three dispatches on the serial encoder: - MetalPleGatherQ8: dequants token-row slices straight off blob_of's q8 pair (zero-copy on mapped images) into the LAYER-major side panel, sqrt(ple) folded — the CPU-side transpose memcpy is gone with it - MetalBf16MulMm: MetalQ8MulMm with the A stage swapped to native BF16 rows (widen = exact bit shift via the new as_type bitcast) — model_proj streams from the kept-bf16 plane, no resident f32 copy - MetalPleFinish: per (layer, position) slice rms + pnorm + combine, in place Plumbing: register_ple_gpu_gate — forward_prefill asks the driver BEFORE the CPU pre-step, stashes token rows on true; the driver falls back to ple_pre_prefill on a late decline, and ple_block_prefill panics loud if a promise ever leaks to the CPU body. Decode PLE (1 row/step) stays CPU. gemma-4-E4B-it-Q8_0 paired vs llama.cpp stock (M1 Max, llama-bench shapes): pp512 784.9 -> 959.22 vs 916.13 = 1.047 GREEN (board red was 0.856) tg128 51.27 vs 52.08 = 0.98 (unchanged path) Steady prefill wall 655 -> 530ms; GPU window +1-3ms. fam-gemma4e cell green incl. the token-for-token generate vs CPU truth; llama prefill base arm green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
A bare-filename DASLLAMA_CONFIRM_MODEL made every confirm arm die on 'cannot open gguf' — but the only surfaced line was "the FALLBACK arm produced no pp number (fix the box, rerun)", swallowing the actual error and costing a debug round. An arm that produces no CONFIRM_PP now prints the child's last lines before scoring 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
… cells Fresh paired sweep (all 8 catalog models x cpu+metal, refs adjacent per cell, quiet window, r=5) after the reds-dig2 kernel work; hidden site section 02 regenerated from the store. Both dug reds confirmed green in sweep conditions: E4B metal pp 0.856 -> 1.058, gpt-oss metal tg 0.963 -> 1.060. Every sweep-era window-skew red re-paired clean (30B metal tg 1.14, Mistral metal pp 1.01 / cpu pp 1.06, 27B cpu tg 1.05, 12B cpu pp 1.11). Remaining sub-parity, honest: 27B metal pp 0.91 (dense k4 mul_mm A-stage — next chase), gpt-oss metal pp 0.92 (0.96 quiet-paired; fresh-process first-rep PSO stall skews the sweep mean), 26B/E4B cpu pp vs AMX-stock 0.86/0.96 (both lead clean-cpu). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
Contributor
There was a problem hiding this comment.
Pull request overview
This PR targets the remaining public-board Metal performance regressions for dasLLAMA on Apple Silicon by (1) improving measurement/leak hygiene, (2) reshaping the gpt-oss MXFP4 MoE kernels to the lab-proven mapping, and (3) moving gemma-4 E-series PLE prefill pre-step onto the GPU.
Changes:
- Add per-creation-site Metal live-handle reporting and integrate it into existing Metal leak gates.
- Extend Metal MSL codegen to support
exp2/log2anddaslib/math_bitsbitcasts via MSLas_type, with a new GPU-vs-CPU math/bitcast test. - Implement gpt-oss MXFP4 kernel updates and an E-series GPU PLE pre-step (gather + BF16 GEMM + finish), plus refreshed M1 performance records and harness diagnostics.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/metal/test_metal_math.das | Expands GPU-vs-CPU math test coverage to include exp2/log2 and bitcast round-trips via daslib/math_bits. |
| modules/dasMetal/src/dasMetal.mm | Tracks live Metal handles with a tagged registry and exposes metal_live_object_report() for leak attribution. |
| modules/dasMetal/src/dasMetal.h | Declares the new metal_live_object_report API for module/AOT integration. |
| modules/dasMetal/metal/msl_emit.das | Allows exp2/log2 and lowers math_bits bitcasts to MSL as_type<>(). |
| modules/dasMetal/metal/das_metal_boost.das | Enhances leak assertions/logging using metal_live_object_report() and adds metal_log_leaks_if_any(). |
| modules/dasLLAMA/tests/test_metal_support_matrix.das | Switches leak-gate checks to metal_log_leaks_if_any() for better diagnostics on failures. |
| modules/dasLLAMA/tests/test_metal_prefill_parity.das | Uses metal_log_leaks_if_any() for more actionable leak test failures. |
| modules/dasLLAMA/tests/test_metal_prefill_kernels.das | Updates the mx4 GEMV kernel gate for tgmem LUT usage and removes now-obsolete e8m0 table plumbing. |
| modules/dasLLAMA/tests/test_metal_gemm.das | Uses metal_log_leaks_if_any() for improved Metal leak reporting in GEMM tests. |
| modules/dasLLAMA/performance/records/m1.json | Refreshes M1 performance record snapshots and tune metadata. |
| modules/dasLLAMA/performance/profile_common.das | Fixes/extends exec-format “receipt” reporting to include deltanet projection formats for recurrent layers. |
| modules/dasLLAMA/harness/gen_tune_probe.das | Improves tune-confirm diagnostics by printing the child output tail when parsing yields no score. |
| modules/dasLLAMA/dasllama/dasllama_metal_prefill.das | Adds BF16 mul_mm variant, implements GPU PLE pre-step dispatches, and updates mx4 kernels to the new mapping/bitcast path. |
| modules/dasLLAMA/dasllama/dasllama_metal_llama.das | Fixes MoE step resource releases by returning missing uniform buffers to the pool. |
| modules/dasLLAMA/dasllama/dasllama_metal_kernels.das | Removes e8m0 device-table usage and updates mx4 GEMV dispatch to set required threadgroup memory. |
| modules/dasLLAMA/dasllama/dasllama_metal_common.das | Adds bf16_of helper to bind kept-BF16 planes for zero-copy or cached residency. |
| modules/dasLLAMA/dasllama/dasllama_common.das | Introduces a PLE GPU gate mechanism and exposes ple_pre_prefill for late-decline fallback. |
| modules/dasLLAMA/benchmarks/matmul/bench_metal_moe_lab.das | Adds a gpt-oss mx4 “lab” section to validate and time multiple kernel rungs at exact shapes. |
| doc/source/conf.py | Adds LaTeX unicode declarations for θ and typographic minus to keep PDF builds passing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…e-paired Chasing the two fresh-board tails turned up measurement structure, not kernels: - 27B metal pp "0.911" was cell-order bias: das metal ran straight off the 222s das-cpu heat soak while the lcpp metal ref ran minutes later (das 110.7 soaked vs 138-143 quiet). gen_bench_records now runs each ratio's das cell and its ref ADJACENT, metal pair first, cpu trio after — the measurement protocol's interleave rule applied at the pass level. - Short cells benched into the kernel's ASYNC reclaim of the previous child's 10-20GB map (12B pp +-33 at back-to-back, +-0.8 settled): every pass now idles --settle seconds (default 12). - lcpp_bench grows DASLLAMA_BENCH_VERBOSE=1 per-rep tps output — the mean +- sd line was hiding which rep carried a blip. Metal board re-paired under the fair order (cpu rows keep the sweep's): 35B 0.98/1.19, 27B 1.11/1.22 (was the phantom red), 12B 1.04/1.16, E4B 1.06/0.99, gpt-oss 0.92/1.06 (pp carries its intrinsic one-blippy-rep +-47 on the receipt; 0.96 on blip-free runs), 30B 0.99/1.14, Mistral 1.01/1.15, 26B 1.10/1.02. Hidden site section regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
The leak-attribution code uses std::string/std::to_string via transitive includes only — fragile across toolchains/SDKs. One line, file's own style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
modules/dasMetal/src/dasMetal.mm:88
- If g_metalLiveTags is updated to track retain counts per handle, release_handle should decrement that count and only erase when it reaches zero. Otherwise the tag entry can disappear while the handle is still logically live (multiple retains).
static void release_handle ( void * h ) {
if ( !h ) return;
g_metalLiveObjects.fetch_sub(1, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> guard(g_metalLiveTagsMx);
g_metalLiveTags.erase(h);
}
modules/dasMetal/src/dasMetal.mm:560
- metal_live_object_report currently assumes g_metalLiveTags values are plain strings. If g_metalLiveTags tracks (tag,count) per handle, aggregate bySite using the stored count so the report reflects multiple retains of the same ObjC object address.
char * metal_live_object_report ( Context * ctx, LineInfoArg * at ) {
std::map<std::string, int64_t> bySite;
{
std::lock_guard<std::mutex> guard(g_metalLiveTagsMx);
for ( auto & kv : g_metalLiveTags ) bySite[kv.second]++;
…with the counter Copilot round 3: the same ObjC address can be retained repeatedly (MTLCreateSystemDefaultDevice is a singleton, and both dasLLAMA drivers create the device), so a pointer-keyed tag MAP overwrote on re-retain and erased on first release — a leaked extra retain became invisible in metal_live_object_report() while metal_live_object_count() said non-zero, the exact blind spot the detector exists to remove. Tags now stack per pointer (push on retain, pop on release); the report walks all entries. dasModuleMetal rebuilt; metal math gate + dasLLAMA kernel units green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Digs the two remaining public-board reds — gpt-oss (tg 0.963 / pp 0.88) and E4B (pp 0.856) — with measurement hygiene as position 0. Both cells flip green; every number below is a paired same-process run vs llama.cpp stock on M1 Max at llama-bench shapes.
Position 0 — hygiene
type[bytes] @ file:linetag (metal_live_object_report(),metal_log_leaks_if_any()in das_metal_boost); the 5 dasLLAMA metal test gates assert through it. It named the historic 51-object leak in one shot: the single-decode MoE release ledger forgotu_moe_ss/u_moe_kdim/u_moe_tot— 3 pool_release lines, fam-gptoss 15 leaks -> 0.kstep2pin beat by mr8_budget +6.8%).gpt-oss: mx4 kernels to the lab-winning shape
bench_metal_moe_labgrew a gmx4 section at exact gpt-oss shapes (E=32, nfe=dim=2880, k=4, CPU-ref bit-exact) and laddered the routed expert GEMV from the production 239 wGB/s to 301:as_type<float>in ALU — kills the mxe->etab dependent device-load chainRefuted along the way: ALU e2m1 decode (2.5x worse — tables win on AGX), etab-to-tgmem (broadcast reads are already on the uniform path).
Both production mx4 kernels get the winning shape —
MetalMoeGemvMx4(decode GEMV) andMetalMoeMulMmMx4's A-stage (uint4 block load + tgmem LUT + computed e8m0). Both e8m0 device tables are deleted; the ALU form is bit-exact for all 256 exponent bytes including the e<2 denormals.msl_emit grows the standing-rule capability behind this: the
daslib/math_bitsbitcast family lowers toas_type(plus exp2/log2 in the math whitelist), with real-GPU coverage intests/metal/test_metal_math.das.E4B: PLE pre-step on GPU
The E-series prefill pre-step (gather 512 token rows off the 3GB Q8_0
per_layer_token_embd, the 14-GMAC bf16model_projGEMM, rms/combine) ran on CPU — 126ms beside a 529ms GPU window that already beat lcpp's whole wall. It now runs as three dispatches at the head of the prefill graph:MetalPleGatherQ8— dequants token-row slices straight off the blob's q8 views (zero-copy on mapped images) into the layer-major side panel, sqrt(ple) folded; the CPU-side transpose memcpy is gone with itMetalBf16MulMm— the q8 mul_mm with its A stage swapped to native BF16 rows (widen = exact bit shift via the new bitcast); model_proj streams from the kept-bf16 plane, no resident f32 copyMetalPleFinish— per (layer, position) slice rms + pnorm + combine, in placePlumbing:
register_ple_gpu_gate— forward_prefill asks the driver before the CPU pre-step and stashes the token rows on yes; the driver falls back tople_pre_prefillon a late decline, andple_block_prefillpanics loud if a promise ever leaks to the CPU body. Decode PLE (1 row/step) stays CPU by design.Verification: fam-gptoss tolerance cells bit-identical pre/post the kernel swap; fam-gemma4e green including the token-for-token generate vs CPU truth; model-less kernel-unit gates bit-exact (odd-shape tails, bias and batch-stream arms); llama prefill base arm green; full preflight green.
The fresh board
Full M1 re-sweep after the fixes (all 8 catalog models, cpu+metal, refs adjacent per cell, quiet window, r=5) — records + the hidden site section regenerated. das leads or ties 41 of 48 cells:
Every sweep-era window-skew red re-paired clean (30B metal tg 0.95 -> 1.14, Mistral pp cells, 27B cpu tg, 12B cpu pp). Remaining sub-parity, honest: 27B metal pp 0.91 (dense k4 mul_mm A-stage — next chase), gpt-oss metal pp 0.92 (0.96 quiet-paired; a fresh-process first-rep PSO-compile stall skews the sweep mean — warmup-rep fix is a tool follow-up), and the AMX-stock cpu-pp class on 26B/E4B (0.86/0.96 — both lead the kernel-fair clean-cpu ref).
Ride-along: the tune confirm pass now echoes its child's output tail when an arm scores 0 (a swallowed 'cannot open gguf' cost a debug round mid-sweep).
🤖 Generated with Claude Code
https://claude.ai/code/session_01XLduse7eHSS1iJ2BG2P4LT