Skip to content

dsv41 metal: bf16 and fp32 matvecs and the phase-1 glue - #1710

Merged
ashhart merged 1 commit into
ashhart:mainfrom
gilby:dsv41-metal-pr/06-matvecs-phase1-glue
Nov 3, 2026
Merged

ashhart merged 1 commit into
ashhart:mainfrom
gilby:dsv41-metal-pr/06-matvecs-phase1-glue

Conversation

@gilby

@gilby gilby commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

The router, indexer, compressor and head matvecs (mv.metal, bits independent of the row count) and the layer's phase-1 glue (glue_tmp.metal: norms, act_quant, RoPE, FP4, pools, candidates, hc and MoE glue). Both are registered with the kernel contract: the phase-1 glue has 14 launch records (dsv41_glue_tmp_records.zig), planted controls in every kernel (a non-finite read as 0, a lost or swapped term, an fp16 total, a write past the end; they compile away at TF_CTL 0) and gate suites against float64: metal_dsv41_glue_tmp.zig (rmsnorm, RoPE, scatter, embed, hc mean; references in dsv41_glue_ref.zig) and metal_dsv41_glue_tmp2.zig (the FP4 pack and round trip, the compressor pool, the candidate blocks and the hc mix, pre and post; references in dsv41_glue_tmp_ref.zig, which models each kernel's own arithmetic so non-finite inputs match bit for bit). Each of these seven kernels has its two planted controls (a lost term or wrong value, and a stray write) going red through data, and gives every row the same bits at 1, 7 and rows/3 rows a launch.

Out-of-range inputs are bounded: embed takes the table's row count, and a token id past it gives zero rows and reads nothing; RoPE takes the position table's length, and a position past it leaves the row untouched; both refuse a zero size by name (RoPE also a head under 64 or odd, before head - 64 is computed); the matvec wrapper returns an error for a zero shape instead of asserting, and every call site up the stack takes the try. Host tests cover the refusals, and the contract has a table bounds check (embed ids past the vocabulary, RoPE positions past the table) that goes red with the bounds removed.

Main's shared files change only by one-line hooks; this part's records, suites and limits go into the family's own files (dsv41_metal_records.zig, metal_dsv41_main.zig, metal_dsv41_limits.zig).

Part 6 of 18 of the stack for #1676; stacks on main, with parts 1 to 5 (#1705 to #1709) merged.

Receipt

  • Environment, with the TensorFold commit, MLX or PyTorch version, machine and GPU, checkpoint and revision: TensorFold 9d6513e on main ad98b60 (native Zig 0.17.0; no MLX or PyTorch); Apple M3 Ultra (60-core GPU), 96 GB, macOS, for the kernel and device tests; checkpoint deepseek-ai/DeepSeek-V4.1-Flash-1017.
  • Exactness, from tools/bench_concurrent.py --alone --serial or token_sha against "draft": false: n/a: kernels only; the kernel contract checks each launch geometry's bits against the others and against float64.
  • Decode speed before and after, from tools/bench_openai.py: measured on the PR that turns serving on.
  • Prompt speed before and after: the cold first request's ttft_s from tools/bench_cache.py --require-cold on a long prompt, or "n/a" with the reason when the change cannot reach prompt processing: measured on the PR that turns serving on.
  • Tests run, with pass, fail and skip counts: zig build test tf-kernel-contract -Dcpu=apple_m1: exit 0, 621/621 steps succeeded; kernel contract, tf-kernel-contract --only dsv41 (Metal): SUMMARY 38 kernels: 38 pass gates 1-4, 0 fail, 0 known findings; 66 controls: 66 red as planted, 0 missed; 0 limit problems; gates 98147 ms, limits 173 ms; python3 tools/zig/lean_check.py: 5040 files, 424 problems, the same count as main (5027 files, 424). Zig's summary counts only the test binaries it ran in that invocation, so the step count is the comparable figure.
  • Not run, and why: CUDA: no CUDA code changes; Metal on M1, M2, M4 and M5: one M3 Ultra only (the contract's limits check covers the M1 to M5 pipeline limits).

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.
  • No precision traded for speed. If the bits change, the description says which sums change and why.
  • A new or changed GPU kernel is registered with the kernel contract (docs/kernel-contract.md); each backend's SUMMARY line is in the receipt.
  • Prompt processing is no slower than the last release.
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

@gilby

gilby commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. #1710 now has a float64 case and live controls for all seven of glue_tmp.metal's remaining kernels, and the stack is rebased onto current main (9e8c368b) with parts 1 to 4 in. On your point and what changed:

@gilby
gilby force-pushed the dsv41-metal-pr/06-matvecs-phase1-glue branch from 01b15e4 to 7a2a318 Compare November 2, 2026 07:49
@ashhart

ashhart commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby, and sorry for the long wait on this stack. We're reviewing it from the bottom, starting here. #1710 reads well: the matvec bits don't depend on the launch, since every tier uses the same lane chain order, a fixed butterfly, and contraction off. The 12 glue kernels match their stated formulas (E8M0, E4M3 and E2M1 rounding ties to even, checked). Each is gated against float64 with 2 to 4 planted controls, and the glue's own matvec is held byte-equal to the gated one. The records match the source, only family files change, every file is at or under 592 lines, and lean adds nothing.

One small change before we run it, on bounds in the glue:

  • tf_dsv41_g_embed reads the table with no vocab bound;
  • RoPE takes head-64 in a u32 and an unbounded table position;
  • mv's catch unreachable is reachable on zero shapes.

Please bound each (refuse a token id or position past the table, and refuse a zero shape by name). Then we'll run tf-kernel-contract --only dsv41 on an M5 (we expect 38 pass, 66 red as planted, 0 limit problems) and land it, then #1711 on top.

@gilby

gilby commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Done, and the stack is rebased onto main ad98b602 (this part is now 9d6513ea). embed takes the table's row count: a token id past it gives zero rows and reads nothing. RoPE takes the position table's length: a position past it leaves the row untouched. Both refuse a zero size by name, and RoPE also a head under 64 or odd, before head - 64 is computed. The matvec wrapper returns error.BadMvShape for a zero shape instead of asserting, and every call site up the stack takes the try. Host tests cover the refusals, and the contract's new table bounds check goes red with the bounds removed (6 differ). At this part: SUMMARY 38 kernels: 38 pass gates 1-4, 0 fail, 0 known findings; 66 controls: 66 red as planted, 0 missed. One line you may want to check: zig/src/tensorfold.zig gains an import of glue.zig, because Zig does not collect tests from files that are only reached through another import, and without it the three refusal tests were silently not collected. The receipt in the body is at this head.

mv.metal runs the router, indexer, compressor and head matvecs as lane chains over K with one butterfly, so the
bits never depend on the row count. glue_tmp.metal is the layer's phase-1 glue (norms, act_quant, RoPE, FP4, pools,
candidates, hc and MoE glue), the reference bits the registered glue kernels are checked against.
@ashhart
ashhart force-pushed the dsv41-metal-pr/06-matvecs-phase1-glue branch from 9d6513e to 4733fdd Compare November 3, 2026 09:16
@ashhart
ashhart merged commit d8b80bd into ashhart:main Nov 3, 2026
2 of 7 checks passed
@ashhart

ashhart commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Thank you @gilby. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

@ashhart

ashhart commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby! This is in TensorFold 1.2.2, out now: https://github.com/ashhart/TensorFold/releases/tag/v1.2.2

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