Skip to content

DeepSeek V4.1: fp64 reference tests for the attention and hyperconnection operators - #826

Open
akol1 wants to merge 31 commits into
ashhart:mainfrom
akol1:pr-dsv41-c-tests
Open

akol1 wants to merge 31 commits into
ashhart:mainfrom
akol1:pr-dsv41-c-tests

Conversation

@akol1

@akol1 akol1 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Piece (c)'s test side of the #690 split, rebased onto the 1.1.0 release (76aafcf27) and stacked on #825: each shared sparse-attention and hyperconnection Metal operator gets an fp64 reference test — the harness computes the same stage three ways (the Metal kernel, the FP32 source contract, an FP64 ideal) on identical decoded rows, and asserts the kernel-vs-contract column at rounding level as a regression gate.

Covered: hyperconnection mixing (every sub-stage; the mix attenuates 1-ULP noise 0.18–0.76×), sparse/compressed attention stage by stage — compressor pool + RMS, indexer score and top-k selection, sparse QK + PV, tile denominators, output mix, RoPE (both spectra, positions to 16383), sinks. Verdict: no stage above rounding level; the two named residuals (an exact-cancellation tie-break and the sink exp-overflow edge at sink−max ≈ 83–86) are unreachable for real sinks.

The one family operator outside this coverage is the Engram gather/gate stage: its behavioral regressions live in engram_test.zig, its FP32 equations in engram_math.zig, and its WKV projection already gets an fp64 receipt from operators; a three-way fp64 harness for that stage itself is the gap left open here.

The margin measurement is part of it: the oracle records each indexed window's selection boundary — the contract's margin, the FP64-ideal margin, the pick flips — and the margins mode captures the selection scratch verbatim at depth. Measured at 2600 tokens: the 512-pick boundary sits inside an exact BF16 tie at every indexed layer (tie-runs 2–15 keys; 71–288 distinct scores per 512 picks). A FP32-vs-FP64 score difference (~1e-7 relative) is far below the BF16 quantum (~2^-8), so support flips at tied boundaries are live — folded into the sensitivity accounting, not a kernel defect; each flip's output effect is bounded by the tied-key weight × value spread.

The three-way harnesses are now split at their seams — one file per operator family (pool/RMS, index, sparse, pack, RoPE, HC split/amplify) plus the shared math and margins helpers — so no single test file sits near the lean checker's 600-line limit.

Suites at this head: zig build test -Dcpu=apple_m1 exit 0, 304/304 steps, the reference and margins suites included (the aggregate case tally moves with the build cache, so it is not quoted); zig build native -Dcpu=apple_m1: 54/54 steps; tools/zig/lean_new.py: 502 problems against 503 on the 1.1.0 base, 0 new; zig fmt --check clean.

M3-class hardware here. tpischke offered an M5 Ultra run on the same packs and depths and we took it; those receipts will follow on the issue and are not claimed here.

#827 stacks on this branch.


Status on the 1.1.2 line. This head is unchanged and still rebased onto 76aafcf27; it is not retargeted here. The family stack refresh lives on the fork branch pr-dsv41-bc-family-11 at 4f69fea10 (12 commits on d1018b318, the full family change preserved), and the #1052 and #1051 reference work is combined on that base in akol1#3. No action requested on this PR.

@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thank you, @akol1, we will carry the independent operator/reference harnesses with your credit into the common DeepSeek qualification gate; the landing order and remaining checks are in our reply on #827.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart mentioned this pull request Oct 17, 2026
@akol1 akol1 changed the title DeepSeek V4.1: the fp64 reference tests — every operator against a three-way harness DeepSeek V4.1: fp64 reference tests for the attention and hyperconnection operators Oct 18, 2026
@akol1

akol1 commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Scoped the title and the opening claim to what this suite actually asserts: the three-way harnesses cover the shared sparse-attention and hyperconnection operators, and the Engram gather/gate stage keeps its behavioral regressions plus the projection-side receipt, with a stage-level three-way harness called out as the known gap. No code change.

@akol1

akol1 commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Scoping note: the Engram gap this PR named is closed in #1051. operator_engram_fp64.zig drives the hash, the FP8 gather and the signed-sqrt gate three ways: the Metal kernel, the FP32 source contract, and an FP64 widening of the same equations. It pins the gather's subnormal-flush semantics explicitly (the contract encodes the flush; against the FP64 ideal only those flushed cells differ), and checks the DEAD-mask pass-through.

The only non-test change is one line exporting deepseek_v41_engram from zig/src/tensorfold.zig, because the reference tree is an external consumer of the family. No kernel, table, scale, receipt or serving-path change. #1051 is stacked on this PR.

@ashhart

ashhart commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Thanks @akol1 for #826, alongside @jayleaton's CUDA chain. 1.1.1 includes your shared contribution. Shared host references and row/admission checks strengthen the consolidated checkpoint and Engram contracts without weakening numerical bounds. These shared pieces land with the CUDA family; the complete DeepSeek Metal backend remains separate. No release binary serves DeepSeek checkpoints yet; #827 tracks the whole-model gate.

Posted by Ash's AI Assistant Synthena

@akol1

akol1 commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Mirroring the status now on #825: the 12-commit family stack is rebased onto the 1.1.1 main (d1018b318), head 4f69fea10, and pushed to the fork as pr-dsv41-bc-family-11. It carries this piece's reference side in full: the csa2/mHC fp64 stage harnesses, the operator harness split at its seams, the margins capture, and the one-line comment tidy.

Fresh at that head, M3 Ultra (80-core GPU, 512 GB unified memory), macOS 26.6.2, Zig 0.17.0 from .zig-version: test-deepseek-v41-reference 13/13 and test-deepseek-v41 78/78; the five family steps together are 55/55 steps and 166/167 tests (1 skipped); zig fmt --check zig and git diff --check clean; lean_check.py 444 findings, identical at base and head (0 added).

The gap this PR names is unchanged: the Engram gather/gate stage still has no three-way fp64 harness of its own. Its behavioral regressions and FP32 equations are in engram_test.zig/engram_math.zig, and the WKV projection receives an fp64 receipt from operators, but the stage itself is not covered. No action requested; #825 stays the tracking PR and the branch above is the base-main view for the carry.

@ashhart

ashhart commented Oct 20, 2026

Copy link
Copy Markdown
Owner

The review for the whole stack is on #825. For this PR: please fix the Debug panic at operator_csa2_index_fp64.zig:240 (bug 2 there), then regenerate the stage and checkpoint receipts, keeping the 1% teacher gate. Its compensated reductions aren't in #1052, so #1052 doesn't supersede this one.

- Synthema, Ash's AI Assistant.

@akol1

akol1 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Bug 2 fixed on this head (operator_csa2_index_fp64.zig:241): the unequal-length zip is now an indexed broadcast of row 0, verified bit-identical to the slow form across keys = 1…4096 with NaN, -0.0, inf and subnormal payloads, in Debug and ReleaseFast. Debug was panicking for loop over objects with non-equal lengths; test-deepseek-v41-reference is now 43/43 steps and 26/26 tests in Debug.

The full write-up of all three findings is on #825. Bug 3 does not affect this head — this tree never had that producer.

@akol1

akol1 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Head updated to carry bug 1 from the stack review.

beginSpan gained a metadata: u32 argument in gpu_counters.zig, but the four call sites in zig/src/metal_counter_consumer.zig still passed two, so that consumer no longer compiled. zig build test does not build it, which is why this went unnoticed rather than showing up as a red step.

All four now pass a value: the loop at :84 passes its own index with @intCast(i), matching the profiler.zig convention of @intCast(@backingInt(stage)), and the three lifecycle tests at :210-214 pass 0, the field's own default.

Verified on this head: zig build test has no failing binary, test-metal-counters-host is 6/6, tf-metal-counters builds, and zig fmt --check and git diff --check are clean.

This is a fast-forward, no rebase and no history change. The PR remains CONFLICTING because it predates 1.1.2 — unchanged, and not something I am addressing before 1.1.2 is released.

akol1 added 12 commits October 21, 2026 15:29
… staging

Config parsing, the Engram hash layout, table plans over the original shards' embed
weight and scale, and the EXL3 resident weight staging behind paced reads. Host tests
cover exact integer boundaries, table selection and the staged bytes. On a 512 GiB Mac
the resident payload is about 385 GiB; freeBytes counts reclaimable speculative pages
so the load admission sees real room. No kernels; the served engine comes with the
lane backend.
… qualification runner

The Engram hash/commit, sparse and compressed attention (CSA2), the hyperconnection
mix, routed and shared experts, the cache and rotary glue as source Metal kernels,
with zig/tests/deepseek_v41_reference as the independent CPU oracle: plain reductions
in fp64, no native statistics, no injected coefficients. tf-dsv41-run drives admission,
teacher-forced logits, chunk seams, same-input operator replays and profiling on the
resident checkpoint. The retained whole-model gate stays failed at 4.0961% NRMS
against the unchanged 1% limit; operator checks are evidence, not qualification.
…ps to it

Rank-split packs (jayleaton's 2.8bpw) duplicate whole-load tensors in
native.safetensors beside the per-layer shards; the index's weight map decides
each name's owner file, so the plan no longer refuses the pack as duplicates.
The released checkpoints store the gate f16 and its bias f16; rank-split repacks
store the weight bf16 and the bias f32. The dense kernel's weight dtype and the
route kernel's bias dtype already decode all three, so the loader accepts the
range and the encoders follow the stored tensor.
The gate kernel decodes its q/k weights by dtype, the Gate descriptor carries it,
and the loader accepts the rank-split packs' bf16 projections beside the released
f32 ones.
Jayleaton's 2.8bpw stores the compressor and indexer matrices as a bf16 weight
beside the rotation scales, and prefers it over the trellis copy it also ships.
The exl3 module gains the dense form: a plain product kernel over the rotated
rows writing the sums region the rotate_out stage reads, so the stage
composition and its bit boundaries stay the ones the reference defines. The
loader rides the pack's dtypes for the router gate, bias, Engram gate weights
and the indexer's weights projection; the reference takes the union and names
the dense apply it still owes. The 2.8bpw plan passes: 406.6 GB, 44 shards.
The fp64 oracle's dense path mirrors the quantized one's boundaries: the input
rotation keeps its f16 rounding, the product is an FP64 dot over the f16-rounded
rotated row and the bf16 weight, and the output stages are the rotate_out
contract. The reference errors until now named the missing half; the union is
complete.
…ument, and the duplicate-tensor case stops logging at error level

d58f281f grew Plan.add an owned-index argument and left the in-file tests
calling the old shape; the duplicate-tensor path also logged at error level,
which the test runner treats as a failure even where the test expects the
error.
…nuated

Ported from the mhc worktree: the hyperconnection mix's per-stage fp64
harness (operator_hc_fp64.zig) and the comptime-Real reference parameterization
in math.zig. Verdicts and the 1-ULP attenuation receipts live in the fork.
…se-head boundaries

Carries the in-progress edits: comptime Real-style math (wide/d64/b64, rmsOf/rmsOfValues, poolOf, sparseHeadOf + KeyTable) with the FP32 instances kept as the source contract. Two repairs to that draft: model.zig re-exported kv as a duplicate member (compile error), and rmsOfValues captured the unused row index in its f32/f64 loops.
akol1 and others added 19 commits October 21, 2026 15:29
… in the FP64 ideal, read positions through the view offset

The written-but-unrun indexer harness reported a 0.37% NRMS metal-vs-ideal
delta and a vacuous selection check. Both were reference-side:

* the ideal skipped the pinned source's per-head einsum BF16 output and the
  score's BF16 storage, so it compared an unrounded equation against a
  quantized contract;
* the selection check sliced scratch.positions() from the buffer start, so it
  re-read the FP32 score words as positions, everything fell outside the key
  range and kernel_only counted zero by construction.

With both boundaries in the ideal, the Metal kernel is bit-identical to the FP64
ideal and to the FP32 source reference on all 4096 scores of every case; the
selection sets agree with valid positions (invalid_positions 0). The remaining
0.16% is the documented BF16 quantum itself, reported as bf16_score_boundary.
…output quantum

sparseHeadPre captures acc/denominator before the output BF16 rounding, so the
FP32-versus-FP64 distance is measurable instead of hidden by the 2^-9 output
grid. Across 16 window/compressed/sink/cancellation cases the stage's intrinsic
level is 2.5e-8..3.2e-7 NRMS (sub-ULP on the pre-rounding value), and the Metal
kernel stays bit-identical to the FP32 source contract on every cell — 0 of
8704 BF16 cells differ from the FP64 ideal.

Receipts: receipts/sparse-stage.jsonl (metal/f32_reference BF16 deltas,
f32_vs_ideal_pre_rounding ULPs, scale lines).
…d RoPE

Each stage boundary now has its own adversarial case family, and every case
reports the kernel against BOTH the FP64 ideal and the FP32 source-contract
reference, so a kernel-only delta is distinguishable from a contract delta.

* sinks: one live column pins the running maximum to a known score, so the
  sweep -104..90 walks FP32 exp() from underflow to overflow, plus per-head
  sink indexing.
* tile denominators: equal-score rows for the pure sum of exponentials, ramped
  scores that push the online correction through the FP32 underflow limit, a
  descending set where no correction is ever applied, and a single-tile row.
* output mix: window and compressed rows on independent scale grids, and a
  128-row mirror shape that cancels out of the score and the accumulator alike.
* RoPE: production shape (64 rotary columns, YaRN compressed spectrum),
  positions 1..16383, inverse rotations and multi-row prefills, comparing the
  kernel's resident factor table against both references.

Also: sparseCaseFrom gains split window/compressed scale exponents, and each
sparse case now prints metal_vs_f32_reference on the identical decoded rows.

Receipts: receipts/sinks-stage.jsonl, tile-denominators-stage.jsonl,
output-mix-stage.jsonl, rope-stage.jsonl. Both qualification suites green.
The stage-by-stage verdict is that every stage is bit-identical to the FP32
source contract, which leaves the possibility that what this operator contributes
to the whole-model gate is not arithmetic but a knife-edge: a pick that sits closer
to the boundary than the score's own rounding noise.

So the indexer case now also selects from the FP64 scores the ideal kept, and reports
how many of the 512 picks change, the boundary margin and that margin in score
quanta. Random score draws tie exactly at the boundary and flip 2 of 512; the tie
case, whose margin is 13 quanta, flips none. Selection is insensitive to arithmetic
noise except where it is a genuine tie.

Also records the per-stage verdict table in RESUME.md, and the sink sweep now walks
the FP32 exp() overflow edge explicitly (80/83/84/85/86) so that the one place the
kernel and the contract disagree is a named case rather than an outlier.

Receipts regenerated from a single run: receipts/raw/final-run.log plus
{pool,indexer,sparse,tiles,mix,rope,sinks}-stage.jsonl. Both suites green.
…-plan test calls

math.zig's wide and b64 arrived twice through the two worktree lines; the
shard-plan tests now pass the index-ownership argument the loader gained.
The reference state now probes every scored window: the contract's boundary
margin (BF16-rounded FP32 scores), the FP64-ideal margin on the same inputs,
and the top-k pick flips between them — the real-data support-flip measurement.
The poolOf call takes the partials by address; the oracle path had never been
compiled since the runner landed, which the probe's first build exposed.
… scratch, verbatim

The margins mode prefills a long prompt, then runs a single diagnostic token
with the observer hooking each indexer layer, copying the selection scratch
(score tiles, heap, positions) out verbatim. The offline decode reads the
score tile directly: at 2600 tokens the boundary of the 512-pick selection
sits inside an exact tie of the BF16-quantized scores at every writable layer
— the support-flip condition the synthetic harness predicted, now on real
weights. The candidate-masked layers' tiles are unwritten; their boundaries
need the candidate-region decode.
…ompensated FP32 pairs

The pinned source contract narrows the accumulator once per 64-column tile with the
in-tile sums in wide precision; the kernel kept a running FP32 accumulator and crossed
a BF16 rounding boundary on real inputs at layer 21 (one cell of 32768, head 8 dim 130),
which the projections then amplified into the whole-model delta. The tile now accumulates
as a hi/lo FP32 pair — the same two-sum the dot products already use — and narrows once
per tile, matching the contract. The layer-21 replay is bit-exact after this; the
whole-model error drops from 4.096% to 3.05%.
The reference chain sums the dense-kept dot sequentially in FP64; the kernel's plain
FP32 loop crossed BF16 rounding boundaries on real rows. The 2.8bpw whole-model error
drops 5.163% to 5.005% with this alone; the remaining delta is the other wide-reduction
sites (the EXL3 tile shape, the RMS trees, the Hadamard) enumerated in the reduction
inventory.
…ests

The generated nfkd_data.zig now comes from tools/zig/nfkd_tables.py, a peer
of tools/zig/unicode_tables.py: it rebuilds the table from unicodedata
16.0.0 and zig fmt, and its --check fails on drift. The table is one file
with a generated header, and lean_check.py's generator-output exemption
covers it, as it already covers the Flash Next generator outputs.
tf-dsv41-token-map's test blocks were never reachable because the build
registered the program only as an executable; a host_tests flag in the
build's engine programs table makes zig build test run them, so the
normalizer chain and the PCG64 draws are gated from now on. The tests'
Hangul case expected a recomposed syllable; NFKD decomposes it and the
release's chain never recomposes, so the expectation and a comment say so.
…-column rule

Folds the remaining comment groups in the V4.1 reference, family, backend,
native and Metal sources into single lines of at most 120 columns, keeping
the technical content. The lean budget counts a comment group as a
multi-line comment, so this keeps the tree at its pinned baseline.
operator_csa2_fp64.zig (1331 lines) and operator_hc_fp64.zig (1095) sit
above the 600-line file budget. Split by what the harness measures, each
part with the harness helpers it uses: csa2 into the shared view/buffers,
the code tables, the sparse stages, rope and the indexer; hc into the
shared view/config, the split-and-recombine bit-identity case and the
amplify stages. The reference root imports every part in dependency order,
so the tests.deepseek_v41_reference. filter and the suite counts are
unchanged. No tolerance, assertion or receipt text moved.
Folds the last comment groups in the reference harnesses and the dense
EXL3 kernel into single lines of at most 120 columns, keeping the
technical content. The lean budget counts a comment group as a
multi-line comment, so this keeps the tree at its pinned baseline.
beginSpan gained a metadata argument, so the four call sites in the Metal
counter consumer no longer compiled. zig build test does not build that
consumer, so nothing caught it.

The loop passes its index, matching the profiler convention, and the three
lifecycle tests pass the field default.
@ashhart

ashhart commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Thank you, @akol1, for the FP64 reference work and the two compensated reductions. We're carrying it as part of the Metal integration described on #825. Two things need fixing on the way: the CSA2 checks print their comparisons but never assert (operator_csa2_fp64.zig:180), and the index receipt computes its overlap from its own reference mask, so 512 copies of one valid key would pass (operator_csa2_index_fp64.zig:145). We'll keep your reductions where the FP64 references show they're at least as precise, with a stated bound for the hi/lo dot's dynamic-range case.

  • Synthema, Ash's AI Assistant.

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