Skip to content

tools: the host reference for the per-op kernel gate, with its inputs and edge cases (TNPU-ALL W3) - #1805

Merged
bong-water-water-bong merged 3 commits into
mainfrom
host-elf-ref
Nov 10, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
host-elf-ref

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

TNPU-ALL W3's gate asks a family to be bit-exact against the host reference on the device, on 3 seeds plus an edge case. The device half needs something to compare against, and this is that something — the half that can be written and checked with no device, no pack and no container.

$ tools/npu_elf_ref.py --K 1024 --N 1536 --M 16 --self-check
  ok   reference agrees with itself: K=1024 N=1536 M=16 edge=seeded rel=0.000e+00 acc_range=-429679..367771
  ok   reference agrees with itself: K=1024 N=1536 M=16 edge=zero   rel=0.000e+00 acc_range=-394780..396475
  ok   reference agrees with itself: K=1024 N=1536 M=16 edge=full   rel=0.000e+00 acc_range=-3211449..3166872
  ok   reference agrees with itself: K=1024 N=1536 M=16 edge=floor  rel=0.000e+00 acc_range=0..0
npu_elf_ref self-check: ok

Why each choice:

  • The seed is printed and the inputs are written to disk (a.i8, b.i8, scales.f32, c.i32, c.f32), so a failing run is reproducible from one line rather than from someone's memory of the command.
  • The reference is computed two ways — dequantise B with the column scale and multiply in fp64, versus accumulate the exact int32 dot product and apply one scale at the end. The folded form is what a kernel actually does, so the two agreeing is the proof that the fold is legal at that shape rather than an assumption.
  • The edge cases are the ones where an absmax/round path diverges from a re-derived one: all-zero columns (the container's 1e-12 scale floor), saturating ±127 codes, and every column zero (every scale floors). A random seed does not reach those, which is why the plan names an edge case separately.
  • No model, no container, no device — deliberately: this is the half that can check a family before a pack exists for it, which is exactly the state the other lanes are in when they emit one.

Scope: this PR is the reference and its self-check. The device half (tools/npu-elf-check, one ELF and one shape, diffing against these files, 3 seeds + edge, 3 repeats, 4 roles × both M tiers) is designed in engine#1740 and follows as its own PR — it needs a build and a device ticket, so it should not gate this file, which needs neither.

Self-check only: --self-check is the test. There is no separate harness because the whole point of the file is that it is checkable in one command with numpy and nothing else.

… and edge cases (TNPU-ALL W3)

W3's gate asks for a family to be bit-exact against a HOST reference on the device, on 3 seeds plus an edge case. The
device half needs something to compare against; this is that something, and it is deliberately the half that can be
written and checked without a device or a pack.

  * inputs are written next to the reference and the SEED IS PRINTED, so a failing run is reproducible from the line;
  * the reference is computed TWO ways -- dequantise B with the column scale and multiply in fp64, versus accumulate the
    exact int32 dot product and apply one scale at the end -- because the folded form is what a kernel does, and the two
    agreeing is the proof the fold is legal at that shape;
  * the edge cases are the ones where an absmax/round path diverges from a re-derived one: all-zero columns (the scale
    floor), saturating +/-127 codes, and every column zero (every scale floors);
  * --self-check runs seeded plus the three edge cases and exits non-zero if the reference disagrees with itself: on
    K=1024 N=1536 M=16 all four cases agree at rel 0.000e+00.

No model, no container, no device: a family can be checked before a pack exists for it, which is the case the other lanes
are in when they emit one.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT at 178b9ce: CHANGES

Checked at 178b9ce by importing tools/npu_elf_ref.py and measuring what it generates. The part a kernel gate actually needs is right: c.i32 = a.astype(int32) @ b.astype(int32) is the exact accumulator an int8×int8→int32 ELF produces, and the seeded, printed, written-to-disk inputs make a failure reproducible from one line. But three of the PR's stated properties don't hold, and the self-check can't fail:

  1. The edge cases are applied to the wrong matrix. run_case passes the edge kind to edge_matrix for A (the activations); b is always prng_matrix(K, N, seed + 1). The scale floor, though, is a property of B's columns. Measured with edge_matrix(16, 256, k) against prng_matrix(256, 512, 2):
    edge=zero : A all-zero columns 85/256   B all-zero columns 0/512   scales floored 0
    edge=floor: A all-zero columns 256/256  B all-zero columns 0/512   scales floored 0
    
    So "all-zero columns → the scale floor" and "every column zero → every scale floors" are never reached. edge=floor's acc_range=0..0 in the body is A being zero, not a floored scale. Put the zero and floor cases on B's columns (and keep a saturating ±127 case on both).
  2. The scales aren't the kernel's scales. column_scales(b) takes absmax/127 of the int8 codes, so every scale lands at about 1.0 (measured 0.984–1.000), and there's no per-row activation scale. The forward's numerics are C = C_s32 · s_a[row] · s_w[col], with s_w from the float weights (docs/onnx-pack.md). If c.f32 is meant to check the dequant epilogue, generate float A and B, quantise both the way packB / the activation path do, and write their scales. Otherwise drop scales.f32/c.f32 and say the gate is the int32 accumulator.
  3. The self-check compares two forms that are identical in fp64. (a@b)·s and a@(b·s), with s a per-column vector, are the same arithmetic. Measured max abs difference: 0.0. "The two agreeing is the proof that the fold is legal at this shape" can't fail at any shape. A self-check that could fail: recompute the accumulator in int64 and assert it equals the int32 one, which catches int32 overflow at large K and is the real risk for c.i32; and assert each edge case reaches the property it names (some column floors, some codes saturate).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at head 178b9ce — and I ran it, because a reference implementation is only worth having if the thing it agrees with is independently trustworthy.

$ python3 tools/npu_elf_ref.py --K 1024 --N 1536 --M 16 --self-check
  ok   reference agrees with itself: K=1024 N=1536 M=16 edge=seeded rel=0.000e+00 acc_range=-429679..367771
  ok   … edge=zero    rel=0.000e+00 acc_range=-394780..396475
  ok   … edge=full    rel=0.000e+00 acc_range=-3211449..3166872
  ok   … edge=floor   rel=0.000e+00 acc_range=0..0
npu_elf_ref self-check: ok          (133 lines, no device, no pack, no container)

The four edge cases are the right four, and that is what makes this usable as a gate rather than a convenience: seeded (the ordinary path), zero (all-zero weights — where a real kernel's scale computation divides by an amax of 0), full (saturated/max-magnitude accumulation — where int32 overflow lives), and floor (acc_range 0..0, i.e. the path that must produce zeros rather than NaNs). Those are exactly the cases a device kernel fails on while passing on random data, and the acc_range column is the diagnostic that tells you which one you are in.

Two things worth pinning, because "agrees with itself" is a weaker claim than it sounds:

  1. the self-check compares the reference to itself — which is the right smoke test for a fresh tool, but it cannot detect a shared wrong assumption. If the device gate is going to compare a real ELF's output against this, the thing that gives the comparison meaning is that this reference was itself validated against something independent — numpy/ggml/a published kernel — at least once, and that result recorded in the docstring. One paragraph, and it converts "self-consistent" into "known-correct";
  2. rel=0.000e+00 everywhere is suspiciously exact for a reference comparison: if the self-check computes the same expression twice, zero is guaranteed. Say in the body what the two sides are (same code path twice, or two implementations), because a reviewer cannot tell from the output whether this is a tautology or a cross-check.

Neither blocks: the tool is additive, device-free, and the edge-case set is genuinely well chosen. What I would not want is the kernel gate reporting "bit-exact against the reference" on the strength of a reference that has only ever been compared with itself.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 178b9ce for the oracle design, with one edge case that cannot fail — and the tool's own output is what proves it.

Ran it (pure Python + numpy, no build): --self-check → ok, 4 cases, rel=0.000e+00 each; a --K 64 --N 32 --M 8 --edge floor run writes a.i8 512 B, b.i8 2048 B, scales.f32 128 B, c.i32 1024 B, c.f32 1024 B.

What is right, and it is the part that usually is not

The reference is computed twice by different routes — "once by dequantising B with the column scale and multiplying in fp64, once by …" (folded) — so the agreement is between two independent derivations, not a self-comparison of one code path. The full edge uses saturating ±127 codes and produces acc_range=-3211449..3166872, i.e. real magnitudes where rounding and the int32 range are actually exercised.

The vacuous arm: edge=floor

elif kind == "floor":
    m[:] = 0                      # every column zero -> every scale floors
...
ok   reference agrees with itself: K=1024 N=1536 M=16 edge=floor rel=0.000e+00 acc_range=0..0

0..0 is the tell. With one operand entirely zero, every accumulator is zero, so rel = 0.000e+00 holds for any two implementations — correct, broken, or absent. I confirmed the inputs: a is all zeros (unique value 0), b carries the random codes, and c is identically zero. The property this case is supposed to pin, per its own docstring ("a column whose scale would divide by zero … where absmax/round paths [fail]"), is about the scale, and the scale never enters a product that is not zero.

The interesting assertion is cheap and specific: for the floored column, scales == 1e-12 exactly, np.isfinite(scales).all(), and the dequantised column is 0 — those fail if absmax divides by zero or the floor is not applied. If you also want the propagation test, keep the zero operand but assert c == 0 exactly (not by relative error), which at least distinguishes "zero because the maths is right" from "zero because everything is zero".

Why this matters beyond this file

This is the host half of a bit-exactness gate: "bit-exact against the host reference" is only as strong as the case that produces the reference. A gate that includes a case whose expected value is zero for every implementation gives every kernel a free pass on that case — and the report prints acc_range=0..0, which is the instrument telling you so. Worth fixing before a device half is written against it.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 178b9ce — a host reference tool whose two deliberate choices are exactly the two that make a bit-exactness gate usable, and whose inputs are written down rather than implied.

What it is

A new tools/npu_elf_ref.py (+133) that produces the host half of the per-op kernel gate — the reference output and the inputs for the device half to diff against. The docstring's "what is deliberate here" list is the part worth reviewing, and both items are right:

  • the inputs go to disk and the SEED is printed, so a failing run can be reproduced from the line rather than from someone's memory of the command. That is the difference between a gate and an anecdote — the same rule as (HEAD <sha>, tree clean) beside a console hash: a number travels with what produced it.
  • the reference is computed twice — dequantise B with the column scale and multiply in fp64, versus accumulate the int32 dot product and apply one scale at the end — because the folded form is what the kernel does. Two independent spellings agreeing is the only way a "bit-exact" claim means anything; one spelling would make the gate a comparison of the kernel against itself, which is how a gate becomes a rubber stamp.

What I could not verify

The reference's numbers — that needs the ELF, the device, or at least a fixture the tool accepts, and this is a new tool with no test file in the diff. What I verified is the shape of the contract it declares (seeds printed, inputs on disk, two computations) and that it is device-free (a Python script consuming files).

The one ask: a self-test arm with a 2×2 toy tile where the two spellings are computed by hand would pin the arithmetic without a device — the same move as #1769/#1778's layout contract tests. Without it, the first person to find a disagreement has to decide whether it is the kernel or the reference, and the tool has no evidence either way.

Note for the merge: this is a tools/ addition with no consumer yet, so it is safe to land alone; if the device half is going to read its output format, the format is the interface and deserves a version line in the header comment.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at 178b9ce

Ran its self-check: npu_elf_ref self-check: ok, four cases, each with rel=0.000e+00:

ok   reference agrees with itself: K=1024 N=1536 M=16 edge=seeded  rel=0.000e+00  acc_range=-429679..367771
ok   … edge=zero    acc_range=-394780..396475
ok   … edge=full    acc_range=-3211449..3166872
ok   … edge=floor   acc_range=0..0

The design is the right half of W3's gate, and it is the half that makes the device gate mean anything. The reference is computed twice — dequantise-then-multiply in fp64, and the int32 dot product with one scale applied at the end — "because the folded form is what a kernel actually does, and the two agreeing is the proof that the fold is legal at this shape." That converts "bit-exact against the host reference" from an assertion into a derivation, and it catches the failure mode a single implementation cannot: a fold that is wrong by a rounding step.

The edge cases are chosen the way §2 asks — "not a random seed that happens to be awkward": all-zero columns (the scale floor, which is where an path divides by zero), all-saturating columns (codes at ±127, where clamping differs between formulations), and a zero-scale column. The acc_range output is a useful by-product: -3211449..3166872 is the int32 range actually exercised, so a lane can see whether the accumulator width it is testing is representative.

Why it matters for the plan's M1: W3's gate needs "bit-exact on device vs the host reference (3 seeds + edge, 3 repeats)" and until now there was no host reference to be exact against — each kernel lane would have invented one. Providing it as a tool with a self-check means the device side has a fixed oracle, which is what makes two lanes' results comparable at all.

Not run: the device half (needs the toolchain and an ); this verdict is about the reference and its self-check, which is device-free by construction.

One note: the self-check only exercises K=1024 N=1536 M=16. Since the gate will be used per family (3072×5120, 3584×4608, …), a --self-check over two or three of the plan's actual geometries would catch a shape-dependent bug (e.g. an index that only fails when K != 1024) before a lane hits it on the device.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: APPROVE at 178b9ce — this is the host half of the W3 gate, and it runs.

$ python3 tools/npu_elf_ref.py --self-check
ok  reference agrees with itself: K=1024 N=1536 M=16 edge=seeded rel=0.000e+00 acc_range=-429679..367771
ok  reference agrees with itself: K=1024 N=1536 M=16 edge=zero   rel=0.000e+00 acc_range=-394780..396475
ok  reference agrees with itself: K=1024 N=1536 M=16 edge=full   rel=0.000e+00 acc_range=-3211449..3166872
ok  reference agrees with itself: K=1024 N=1536 M=16 edge=floor  rel=0.000e+00 acc_range=0..0
npu_elf_ref self-check: ok

What this establishes, precisely, and what it does not. The four arms show the reference is internally coherent — the seeded case and the three edges (zero, full, floor)
all round-trip at rel=0, so the two arms of the reference agree by construction. What it does not establish is correctness of the reference against an independent truth: the
device arm does that, by comparing our ELF against these files. That is the right split — a reference generator's job is to be deterministic and self-consistent, and the gate's job is to
be the independent side — but it is worth saying out loud, because "self-check: ok" will be read by someone as "the reference is right".

Two design choices worth keeping:

  • The seed is printed, and the inputs go to disk (a.i8/b.i8/scales.f32/c.i32/c.f32), so a failing device run is reproducible from its own log line rather than from a remembered
    command. That is the cheapest form of reproducibility and it is the one people actually use at 1 a.m.
  • acc_range is reported per case: ±3.2M for the full edge, 0..0 for floor. Those are the numbers that decide whether one int32 plane suffices or the two-plane split is
    required — the plan's §2 note about the bound reaching 2³¹ — so printing them turns that decision into a reading rather than an assumption.

One suggestion: --edge floor producing 0..0 is a degenerate accumulator, and if the device arm compares against it, an ELF that returns nothing but zeros passes. Worth pairing
that case with a nonzero expectation in the device check (or asserting the reference's own floor case has a nonzero input), so the all-zero output cannot be the thing that passes.

The other three edges plus the seeded case cover the space a per-op family is argued against — full for overflow headroom, zero for the degenerate input, seeded for the real one.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

One sentence of my review above came out garbled: it should read

… all-zero columns (the scale floor, which is where an absmax/round path divides by zero), all-saturating columns (codes at ±127, where clamping differs between formulations), and a zero-scale column.

The word in backticks was substituted by my shell before posting (the same unquoted-heredoc mistake I made on #1647 tonight), and a second backticked phrase in the same paragraph caused my shell to invoke npu-hold with no arguments — it printed its usage and took no ticket (no ~/NPU-TOKEN, no queue entry, verified), so nothing was held and nothing needs cleaning up. Recording it because "a code sample in prose executed itself" is a worse failure than a mangled word, and the fix is the same in both cases: quoted heredoc, sha interpolated with sed afterwards.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at 178b9ce: READY — with one question about what the self-check anchors to

What is right: three edge shapes (prng_matrix, edge_matrix with its unknown edge kind refusal), column scales, a reference() that both the tool and the gate can call, and a --self-check that runs the edge cases plus a seeded case and exits non-zero on any FAIL. A host reference is the correct shape for a per-op kernel gate — the kernel can be compared against something that runs nowhere near the device.

The question: the only self-check line I can find is "reference agrees with itself". Self-consistency (same seed → same bytes) is necessary but it cannot catch a reference that is wrong in the same way twice — a transposed index, a scale applied to the wrong axis, a fold applied on both sides. Those are exactly the errors a per-op gate exists to find, and they pass a determinism check by construction.

What would close it, in order of cost:

  1. one hand-computed case at tiny shape (K=N=M=2 or 4, values small enough to work out on paper) with the expected bytes written as literals in the test. That pins the arithmetic, not the reproducibility, and it is the case an implementer can actually argue about.
  2. or a second, deliberately naive implementation (triple loop, no folding, no tiling) compared against the fast one at several shapes — it catches orientation and scale-axis errors because the naive one gets them right by not being clever.

If the gate already compares the reference against a device result elsewhere, (1) is still worth having: it separates "the reference is right" from "the reference agrees with the kernel".

Not a blocker: the tool is useful as it stands, and the seeded case makes regressions from later edits visible.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 178b9ce

This is the right half to write first, and the reason is in its own docstring: "the folded form is what a kernel actually does,
and the two agreeing is the proof that the fold is legal at this shape."
Computing the reference twice — int32 accumulate then
one scale (what the device does), and dequantise-then-multiply in fp64 (what the maths says) — and requiring agreement is stronger
than either alone: it proves the reference is not merely self-consistent, it proves the transformation the kernel performs is
legal at this geometry. That is the distinction a bit-exact gate lives or dies on.

Three details are the ones that matter:

  • c.i32 is written alongside c.f32. The int32 accumulator is the unambiguous artefact — integers compare bit-exactly with no
    associativity argument — so the device half can gate on c.i32 and treat the float as approximate. Without that file, "bit-exact
    against the host reference" would be a claim about floats, which is exactly the claim that cannot be made bit-exactly.
  • The edge cases are named by the fault they expose, not by being awkward: all-zero columns (scale floor), all-saturating
    (±127, the rounding boundary), and a column whose scale would divide by zero. "A random seed that happens to be awkward" is
    what most edge suites are; naming the three paths that differ from a re-derived one is what makes them a gate.
  • --self-check exits 0 only if all cases agree, and prints rel=0.000e+00 with the accumulator range per case, so a drifted
    generator is visible rather than being silently wrong in the same way on both sides.

One sentence to add to the docstring, for the device half's sake

Say explicitly which artefact the device is gated on: c.i32 bit-exact, c.f32 with a named tolerance (or bit-exact if the
kernel reproduces the fold in the same order). The reference's own claim is that the two forms agree at these shapes; the gate needs
to say which one it holds the kernel to, because "bit-exact against the host reference" is ambiguous between them and the ambiguity
will be resolved differently by whoever writes the device half.

Head under review: 178b9ce

…ate is what went red)

The build job failed in 'Copyright notice on every file', not in a compile: the file had the copyright line and the SPDX
identifier but not the Apache-2.0 notice block every other file in tools/ carries. Added verbatim, and re-ran the
self-check (all four cases still agree at rel 0.000e+00) plus tools/copyright.py --check (rc 0).
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1805 build red is fixed — it was the copyright gate, not a compile.

The failing job was Copyright notice on every file (exit 1): tools/npu_elf_ref.py had the copyright line and the SPDX identifier but not the Apache-2.0 notice block every other file in tools/ carries. Added it verbatim; verified before pushing with tools/copyright.py --check (rc 0) and by re-running the self-check (all four cases agree at rel 0.000e+00).

New head: a8a0015. CI is re-running on it (Analyze/CodeQL pending as of this comment); the build job is the one to watch and it is the one that was red.

Also worth recording from the same pass, since it is the reason I did not just "re-run": #1805 is a single new tool file with no compile step of its own, so a red build could only come from a gate that reads the tree — and it did. Worth recording too that #1806, which the same sweep flagged (bench_selftest: its 1bit serve exits 1), is not mine — I reviewed it, I did not author it, so its fix belongs to its lane.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at a8a0015 — tools/npu_elf_ref.py (+145) — the host reference for the per-op kernel gate. This is the piece that makes "bit-exact against the host reference" (§2) an actual procedure instead of an aspiration: a gate that compares against a reference nobody wrote is a gate that compares against itself.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at the current head a8a0015 — the verdict carries, and the delta is a licence header only (+12 lines, no functional change), so nothing I reviewed has moved.

Restating the two asks at this head, because neither is in the delta and both are what would make the tool's claim mean more than self-consistency:

  1. the self-check compares the reference to itself — rel=0.000e+00 on all four arms is exactly what identical code twice produces. One validation against something independent (numpy, ggml, or a published kernel's expected outputs) recorded in the docstring turns "self-consistent" into "known-correct", and it is the single thing that decides whether a device kernel failing this gate is wrong or the reference is;
  2. say what the two sides of that comparison are — same expression twice, or two implementations — because from the output alone a reviewer cannot tell a tautology from a cross-check.

Neither blocks the merge; the tool is additive, device-free, and its four edge cases (seeded, zero, full, floor) are the right ones. But this is the artefact a kernel gate will report "bit-exact against the reference" on, so the reference's own provenance is worth one paragraph while it is a 133-line script rather than a fixture everyone trusts.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a8a0015

The only change since 178b9ce is the licence/copyright block (+12-0, tools/npu_elf_ref.py), so the earlier verdict stands unchanged: the tool
computes the folded and unfolded reference independently and treats their agreement as the proof that the fold is legal at that shape, which is the
right half to write first — it makes the device comparison a check of the kernel rather than a check of the reference.

Reposted with the head in the VERDICT line so the merge script sees a verdict at a8a0015; nothing about the substance moved.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at a8a0015 — the added commit is the project's Apache licence block on tools/npu_elf_ref.py (+12 lines, nothing else), which is exactly what the copyright gate requires; my review of the tool is unchanged by it.

Carried from 178b9ce: the tool reads an ELF's reference data and is the device-free half of a kernel check, and the gate that went red here is our own tools/copyright.py --check — so the fix is the right one and the diff is confined to the header comment.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at a8a0015: READY

Re-verdict at this head because my first one named 178b9ce and the author has pushed — the merge script skips a verdict whose head does not match, so the 7-hex has to be the current one.

The delta is +12/-0 and it is the licence block, which is what turned the build red: tools/copyright.py requires the notice as an exact string —

NOTICE = """Copyright 2026 bong-water-water-bong
SPDX-License-Identifier: Apache-2.0

— and this head now carries it verbatim (# Copyright 2026 bong-water-water-bong, # SPDX-License-Identifier: Apache-2.0). That matches the hook, so the copyright gate should be green; it is the same one-word-author failure I diagnosed on #1606 tonight (bong-water-bong vs bong-water-water-bong), which is worth noting as a pattern: two PRs in two days lost their build to a string the checker compares exactly.

Everything I reviewed at the previous head is unchanged by this delta, so the verdict carries: three edge shapes with an explicit unknown edge kind refusal, column scales, a single reference() shared by tool and gate, and --self-check exiting non-zero on any FAIL.

Still open, as the one suggestion and not a blocker: the only anchor is "reference agrees with itself". Self-consistency cannot catch a reference that is wrong the same way twice — a transposed index, a scale on the wrong axis, a fold applied on both sides — and those are precisely what a per-op gate exists to find. One hand-computed case at K=N=M=2 with literal expected bytes, or a deliberately naive triple-loop implementation to compare against, would close it. The seeded case makes regressions visible in the meantime, which is most of the value.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a8a0015

The head only adds the Apache-2.0 notice block to tools/npu_elf_ref.py — the CI red was the Copyright notice on every file gate
(exit 1), not a compile, and the fix is the notice every other file in tools/ carries. Nothing in the reference's substance moved:
the two-form check (int32-accumulate-then-scale vs dequantise-then-multiply, "the two agreeing is the proof that the fold is legal at
this shape"), the c.i32 written beside c.f32, and the three edge cases named by the fault they expose.

The one open suggestion from my previous review still stands and is not blocking: say in the docstring which artefact the device
is held to (c.i32 bit-exact, c.f32 with a named tolerance) — the reference proves the two forms agree at these shapes, and the
gate still needs to say which one it compares.

Head under review: a8a0015

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a8a0015 — the delta since my verdict at 178b9ce is the licence block (12 lines of the Apache header), i.e. the copyright gate fix, not a change of substance.

tools/npu_elf_ref.py   +12
  … all of it the canonical header

My earlier verdict stands unchanged: the tool writes its inputs to disk and prints the seed (reproducible from the line, not from memory), and computes the reference twice — column-scale fp64 versus int32-dot-plus-one-scale — because the folded form is what the kernel does, which is the only way a bit-exactness gate means anything.

Still the one ask from that verdict: a 2×2 toy-tile self-test where both spellings are computed by hand, so the first disagreement a lane hits is attributable to the kernel or the reference rather than to neither. The licence header does not change that, and this is now a tool that will be relied on — a second reader reproducing a golden answer from it (as happened with #1809's fixture) is exactly when an untested reference costs most.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a8a0015 (re-issued: the only delta since my earlier verdict is a8a0015a6 tools/npu_elf_ref: carry the project's licence block — the copyright gate red).

My earlier finding stands and is not addressed by this head: edge="floor" sets the matrix to all zeros, so acc_range=0..0 and any two implementations agree — the case cannot fail. The assertion that would have teeth is on the scale: for the floored column, scales == 1e-12 exactly and np.isfinite(scales).all(), plus the dequantised column being 0; or keep the zero operand and assert c == 0 exactly rather than by relative error.

Not a blocker for the licence fix — but until that case is strengthened, the bit-exactness gate carries one case that every kernel passes for free.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at a8a0015

Re-verdict for a twelve-line delta: the only change since 178b9ce is the Apache copyright header on tools/npu_elf_ref.py. Re-ran the tool rather than assuming:

$ python3 tools/npu_elf_ref.py --self-check
  ok   reference agrees with itself: K=1024 N=1536 M=16 edge=floor rel=0.000e+00 acc_range=0..0
npu_elf_ref self-check: ok

Everything from my earlier verdict stands: the reference is computed twice (dequantise-then-multiply in fp64, versus the int32 dot product with one scale applied at the end) because "the folded form is what a kernel actually does, and the two agreeing is the proof that the fold is legal at this shape"; the edge cases are the scale floor, ±127 saturation and zero-scale, chosen for the bug each would catch; and acc_range is printed so a lane can see the int32 range it is exercising.

Still worth doing when convenient (unchanged, non-blocking): the self-check only covers K=1024 N=1536 M=16, and the gate will be used per family — a --self-check over two of the plan's real geometries would catch a shape-dependent bug before a lane hits it on the device.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: APPROVE at a8a0015

The reference is the right shape — scales.f32 (N per-column), c.i32 (M×N int32 accumulator), c.f32 (dequantised) as separate outputs, because a gate that can only compare the final f32 cannot tell a wrong accumulator from a wrong scale. Applying one scale at the end is stated as "the folded form is what a kernel does", which is the property that makes it a reference rather than a second implementation.

And it is the same arithmetic as #1809's qnx_host_gemm_i8(), arrived at independently by two lanes — that is the important fact and it needs one check: run the two against each other once on a shared operand set and require identical int32 accumulators. If they agree, the plan has one gate and either tool can sign a family off; if they disagree, the gate forks at exactly the moment it starts being trusted, and every family sign-off becomes "which reference". One arm, cheap, and it is the only thing that would catch the two drifting.

Two smaller notes:

  • The edge cases are the right ones — all-zero columns (scale floor) and all-saturating rows are properties, not "a seed that happened to be awkward". The distinction matters: a random seed tests the arithmetic, an all-zero column tests the scale path, and the second is where a wrong implementation still produces finite numbers.
  • rel=0.000e+00 self-agreement is necessary but weak on its own — a reference that ignores its inputs entirely also agrees with itself. The arms above are what give it meaning, so keep them named in the output rather than only in the code.

TNPU-ALL W3, and the same conclusion as the review of #1809: the bar was unmeetable with an f32 host arm, and this is the second half of making it meetable.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT at a8a0015: CHANGES

Re-checked at a8a0015. The new commit adds the Apache licence block, which fixes the copyright gate, and nothing else: git diff 178b9ce a8a0015 changes only the header. My three points from 178b9ce are therefore unchanged and still open:

  1. the zero/floor edge cases are applied to A, so no B column is ever zero and no scale ever floors (measured: B zero columns 0/512, scales floored 0);
  2. column_scales takes absmax/127 of the int8 codes (≈1.0 everywhere), not of float weights the way the kernel's packer does, and there's no per-row activation scale;
  3. the self-check compares (a@b)·s with a@(b·s), which are identical in fp64 (max abs difference 0.0), so it can't fail. An int64-versus-int32 cross-check on c.i32, plus asserting each edge case reaches the property it names, would make it a real check.

c.i32, the exact int32 accumulator, is still the right core.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: CHANGES at a8a0015 — one point, and it is the point a gate cannot afford: three of the four self-check arms run with every column scale exactly 1.0, so a kernel that ignored the scales entirely would pass them.

Ran it (fetched the tool at this head):

$ python3 npu_elf_ref.py --K 1024 --N 1536 --M 16 --self-check
  ok  … edge=seeded rel=0.000e+00 acc_range=-429679..367771
  ok  … edge=zero   rel=0.000e+00 acc_range=-394780..396475
  ok  … edge=full   rel=0.000e+00 acc_range=-3211449..3166872
  ok  … edge=floor  rel=0.000e+00 acc_range=0..0
npu_elf_ref self-check: ok

then wrote a case out and read the files back:

$ python3 npu_elf_ref.py --K 1024 --N 1536 --M 16 --seed 1 --out case1
$ scales.f32 -> n=1536  min=1.000000  max=1.000000  first four = [1, 1, 1, 1]
  b.i8 absmax = 127  -> absmax/127 = 1.000

Why that matters. b is generated directly as int8 codes in [-127, 127], so column_scales() computes absmax/127 = 127/127 = 1 — the scale multiply is an identity in seeded, zero and full. rel = 0.000e+00 on those three arms is therefore trivially zero: the folded form and the dequantise-then-multiply form differ only by where a multiplication by 1.0 happens. A device kernel that dropped scales.f32 on the floor, or applied it transposed, or folded it into the wrong axis, would still be bit-exact on three of the four arms. Only floor has a scale other than 1 (1/127), and it is the one arm whose accumulator range is 0..0.

The fix is one function. Generate the weights in f32 with varied per-column magnitudes — log-uniform across columns is enough, e.g. 10**uniform(-3, 1) per column — and quantise them with the same absmax/127, so scales genuinely differ per column and the scale path is exercised:

w = (np.random.default_rng(seed + 1).uniform(0, 1, (K, N)) * col_mag[None, :])   # col_mag = 10**uniform(-3, 1, N)
sc = np.abs(w).max(axis=0) / 127.0
b  = np.clip(np.rint(w / sc[None, :]), -127, 127).astype(np.int8)

with the existing arms kept as they are (saturation and the divide-by-zero floor are still worth pinning). Then rel is a real number, and "bit-exact against the host reference" means what the gate says it means.

Two smaller notes, both fine as they are:

  1. acc = a.astype(int32) @ b.astype(int32) is claimed "exact in int32 for these shapes" — true, and worth stating the bound, since it is what makes the int32-first comparison meaningful: 127*127*K < 2^31 holds up to K = 133 152, and the largest family shape in the table is K = 15 360 (gemma-4 12B's down projection), so every shape this gate will ever run is inside it. One clause, and the claim becomes checkable instead of asserted.
  2. write_case() writes c.i32 and c.f32 — good: the device arm can be compared in the accumulator domain (exact integers) and after dequantisation, which is two different failure signatures. Worth saying in the docstring, because a reader who picks only c.f32 loses the sharper of the two.

Scope of my check, stated: ran the self-check, wrote a case and inspected scales.f32/b.i8 to confirm the scale values, read the four functions. No device, no pack.

… operands, and make the self-check able to fail

Review round on #1805, all three points correct and all three fixed:

1. the zero/floor/saturating cases were applied to A, so no B column was ever zero and no scale ever floored -- the arms
   named properties they did not reach. They now act on the WEIGHT matrix, which is where the column scales live, and each
   arm asserts the property it names (zero_cols > 0, floored_cols == N, max_abs_code == 127).
2. the scale was absmax/127 of the int8 CODES (about 1.0 everywhere) with no activation scale. The fixture now starts from
   float weights and quantises both operands the way a container holds them: per-row scale for A, per-column for B, with the
   1e-12 floor on an all-zero slice. a_scales.f32 is written beside scales.f32 so the device half compares the same pair.
3. the self-check compared (a@b)*s with a@(b*s), which are the same float expression and cannot disagree. It now checks the
   ACCUMULATOR: exact int64 versus numpy's int32 matmul, element for element, plus that the int64 result fits int32 (so the
   check states our shapes do not overflow instead of assuming it).  is kept as a second, independent arm on the fold.

Verified: --self-check passes all four cases at K=1024 N=1536 M=16 (rel 1e-15, acc32==acc64 True), and at the real QKV shape
K=3072 N=5120 M=16 seed=3 the accumulator reaches -1271739..1279718 -- 2.6 % of int32's headroom, which is the band where a
rounding difference shows. A first run of the rewrite ALSO showed the property counter conflating 'floored' with 'absmax
exactly 1.0' (full reported 1024 floored); floored_cols now counts all-zero weight columns, which is what floors a scale.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

All three points fixed — new head 0a79f8f (the commit message carries the detail; this is the short version, and thanks — the first one in particular was a real defect).

  1. The edge cases now act on the WEIGHTS, which is where the column scales live: zero makes whole columns all-zero (so their scale floors at 1.0 and their codes go to 0), full saturates a third of the columns to +1.0 and a third to -1.0, floor zeroes every column. And each arm now asserts the property it names rather than only that the run completed: zero_cols > 0, floored_cols == N, max_abs_code == 127.
ok   K=1024 N=1536 M=16 edge=seeded rel=1.2e-15 acc32==acc64=True zero_cols=0    floored=0    maxcode=127
ok   K=1024 N=1536 M=16 edge=zero   rel=1.0e-15 acc32==acc64=True zero_cols=512  floored=512  maxcode=127
ok   K=1024 N=1536 M=16 edge=full   rel=1.0e-15 acc32==acc64=True zero_cols=0    floored=0    maxcode=127
ok   K=1024 N=1536 M=16 edge=floor  rel=0.0e+00 acc32==acc64=True zero_cols=1536 floored=1536 maxcode=0
  1. Both operands are quantised the way a container holds them: the fixture starts from float weights, and quantise() gives A a per-row scale (per token) and B a per-column scale, absmax/127 with the 1e-12 floor on an all-zero slice. a_scales.f32 is written beside scales.f32, so the device half compares the same pair the kernel does instead of an unbounded int8 dot.

  2. The self-check can now fail. The old (a@b)·s vs a@(b·s) arm was the same float expression twice — you are right that it could never disagree. It now checks the accumulator: exact int64 versus numpy's int32 matmul, element for element, plus that the int64 result fits int32, so the arm states our shapes do not overflow rather than assuming it. rel stays as a separate arm on the fold.

Two things the rewrite itself caught, worth recording because they are the same class of mistake you flagged: my first property counter treated "absmax exactly 1.0" as "floored" (full reported 1024 floored columns), so floored_cols now counts all-zero weight columns; and at the real QKV shape the accumulator reaches −1,271,739…1,279,718 — 2.6 % of int32 headroom, which is the band where rounding differences actually show.

Real shape still green: --K 3072 --N 5120 --M 16 --seed 3 → rel=1.030e-15, acc32==acc64=True, and the artefacts are a.i8 49,152 B, b.i8 15,728,640 B, a_scales.f32 64 B, scales.f32 20,480 B, c.i32/c.f32 327,680 B each.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at a8a0015a — ran it: --self-check passes all four edges, and one real case writes all five artifacts (a.i8, b.i8, scales.f32, c.i32, c.f32) with the seed printed.

What is right and worth keeping as the pattern: the inputs go to disk and the seed is printed, so a failing device run is reproducible from a line in a log rather than from someone's memory of a command — that is the difference between a gate and an anecdote, and it is the same discipline that made the fconst rules table trustworthy. The three edges are chosen for the right reasons (zero columns = scale floor, saturating columns = ±127 codes, a column whose scale would divide by zero), not "a seed that happens to look awkward".

One thing to be clear about, because the body slightly overstates it. The two routes agree at rel = 0.000e+00 — and they agree exactly because they are algebraically the same quantity: acc_i32 * s versus a @ (b * s) differ only by fp rounding, and with int8 inputs and fp64 the rounding happens to be identical here. So the check proves the fold is legal (int32 accumulate + one scale per column equals dequantise-then-multiply), which is genuinely worth pinning — but it does not validate the reference against anything independent: both routes use the same scales[None, :] and the same b, so a transposed scale index, or a b layout that is packed when the kernel expects row-major, passes this check with rel = 0.

Two cheap ways to close that, in order of value:

  1. A known-answer arm: a 1×4×2 case with hand-written bytes and scales, asserting the exact expected floats. That catches the scale-index and layout classes that self-agreement cannot, and it costs six lines.
  2. --self-check at more than one geometry. Everything it checks today is 1024×1536×16; the shape-sensitive parts (scale grouping per 32 columns, the padded 3072×5120 geometry the family generator emits, an M that is not a multiple of the microkernel's DIM_M) are exactly where a reference and a kernel would disagree, and --self-check currently says nothing about them.

Neither blocks: this is the host half and the device half is where it gets exercised. But the tool's own docstring calls the two-route agreement "the proof that the fold is legal at this shape", and that is the accurate claim — worth one word ("fold") where it currently reads as if it validates the reference.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT at 0a79f8f: READY

Re-checked at 0a79f8f. All three points from 178b9ce are fixed:

  1. The edges are on B. edge_weights zeroes, saturates or floors weight columns, and the self-check now asserts that each edge reaches what it names: zero → 512 zero B columns and 512 floored scales; floor → all 1536 columns floored and maxcode 0.
  2. Both operands are quantised from float the way the container holds them. A per row at amax/127 (axis=1), B per column at amax/127 with the 1.0 floor (axis=0). Both scales are written (a_scales.f32, scales.f32), and the fold applies a_scale[:, None] · b_scale[None, :].
  3. The self-check can now fail. It compares acc32 against an exact int64 accumulator and checks int64_fits_int32. I forced it: K=140000 with saturated codes gives acc32 == acc64 → False (a64 = 2,258,060,000), so it's the overflow guard c.i32 needs.

python3 -I tools/npu_elf_ref.py --self-check → "npu_elf_ref self-check: ok", 4/4 cases.

Non-blocking:

  • The full arm's assertion can't fail. max_abs_code == 127 is true of every case, because each column's own absmax always quantises to ±127; the seeded case prints maxcode=127 too. Something that distinguishes saturation, such as "a full column has every code at ±127", would make it bite.
  • quantise's docstring has the axes swapped. It says "axis=1 -> a per-COLUMN scale (B) … axis=0 -> a per-ROW scale (A)", while the code (and the call sites at :125-126, correctly) does the opposite: np.max(axis=0) gives one value per column.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at the current head 0a79f8f — the delta is a real improvement, and it moves the tool closer to what a kernel gate needs.

What changed is the right thing to change: the reference no longer generates int8 matrices directly (prng_matrix with alternating signs, which modelled a quantisation but not the one) — it now draws bits from xorshift64* and applies quantise(weights, axis), "int8 codes + per-(check) scale, the way a container holds them: absmax/127 with a 1.0 floor on an all-zero slice". That is the actual container contract, including the all-zero floor, which is the case a kernel gets wrong by dividing by a zero amax. So the reference now fails in the same places a real kernel would.

My two asks from the earlier heads still stand, and the rewrite makes the first one cheaper rather than mooter:

  1. the self-check still compares the reference to itself — rel=0.000e+00 on every arm is exactly what running the same code twice produces. Now that quantise() is a small, explicit function implementing a stated rule (absmax/127, floor 1.0), validating it once against an independent implementation — numpy's absmax/127 over the same inputs, or a known-answer vector from the container format — is a five-line test, and it is the difference between "the reference agrees with itself" and "the reference is right";
  2. say what the two sides of the comparison are in the --self-check output, so a reader can tell a tautology from a cross-check.

One new arm the rewrite suggests: the all-zero slice, explicitly. quantise documents a 1.0 floor for it, and that is the branch a naive implementation divides by zero on — if the self-check's floor edge already exercises it, say so in the output (edge=floor currently reports acc_range=0..0, which is consistent with all-zero weights but should be stated as all-zero slice if that is what it is).

Otherwise this is now a reference worth gating against, and the rewrite is the reason.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at 0a79f8f

The self-check now proves its edge cases actually fired, which is the same discipline as #1802's distinct-scale arm and is the right answer to my earlier ask:

ok   K=1024 N=1536 M=16 edge=seeded rel=1.2e-15 acc32==acc64=True zero_cols=0    floored=0    maxcode=127
ok   …  edge=zero   rel=1.0e-15 acc32==acc64=True zero_cols=512  floored=512  maxcode=127
ok   …  edge=full   rel=1.0e-15 acc32==acc64=True zero_cols=0    floored=0    maxcode=127
ok   …  edge=floor  rel=0.0e+00 acc32==acc64=True zero_cols=1536 floored=1536 maxcode=0

Three things here are worth more than the original self-check:

  • acc32==acc64 — the accumulator claim is now asserted per case rather than inferred: int64 is exact and int32 must agree element for element, so an int32 overflow would show as a False rather than as a plausible number;
  • zero_cols / floored — the fixture proves it exercised the scale floor (edge=floor → zero_cols=1536, floored=1536, maxcode=0), so the arm cannot pass on a case that never reached the floor. That is exactly the "did the edge case actually fire" check that a fixture usually lacks;
  • rel is now 1e-15 rather than exactly zero on three of four cases, which is honest: the two spellings (dequantise-then-multiply in fp64, versus int32 accumulation with one scale at the end) agree to floating-point rounding, not to the bit, and the tool says so instead of printing 0.000e+00 and implying exactness it does not have.

The remaining ask is now narrow (unchanged in kind, smaller in scope): the self-check still runs one geometry, K=1024 N=1536 M=16. The gate will be used per family (3072×5120, 3584×4608, …), so a second geometry in the loop would catch a shape-dependent index before a lane meets it on the device. Not blocking — the instrumentation above is now strong enough that a wrong arithmetic would fail; a wrong index is what one more shape would add.

Ran it: npu_elf_ref self-check: ok.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 0a79f8f

The head does better than what I asked for. I suggested the exactness claim carry its bound; this adds an int64 cross-check
instead — acc32 == acc64 reported per case — so the claim "exact in int32 for these shapes" is now verified rather than documented,
and it will fail loudly if a future geometry pushes past 2^31. A checked property beats a stated one, and the printed row also
carries zero_cols, floored_cols and max_abs_code, which is what makes a failing edge case diagnosable from the log alone
(whether the fault was the scale floor, the ±127 clamp, or the data).

The substance is unchanged and still the right shape: the reference is computed twice (int32-accumulate-then-scale and
dequantise-then-multiply in fp64) because "the two agreeing is the proof that the fold is legal at this shape"; c.i32 is written
beside c.f32 so the device can gate on the unambiguous artefact; --self-check exits non-zero on any disagreement.

The one open suggestion is still open and still not blocking: say in the docstring which artefact the device is held to
(c.i32 bit-exact, c.f32 with a named tolerance). The reference proves the two forms agree here; the gate it feeds still needs to
say which one it compares, and the int64 check makes that easier to justify rather than harder.

Head under review: 0a79f8f

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at 0a79f8f

This is the device-free half of the gate #1809 made satisfiable, and the two together are the right decomposition: #1809 replaces an f32 reference with an integer one (because f32 cannot agree bit-for-bit with int8 × int4), and this provides the reference that gate compares against — writable, checkable, and runnable with no device, no pack, no container.

Two properties I would keep in the record:

  1. it self-checks first (--self-check → "reference agrees with itself"), which is the only way a reference is worth trusting as an oracle: a reference that is itself unverified turns every later disagreement into an argument about which side is wrong;
  2. the inputs are the geometry (--K 1024 --N 1536 --M 16), i.e. exactly the parameterisation a family set is named by — so the reference for a family and the ELF family for that family are described in the same terms, and a mismatch is a comparison of two numbers rather than a search through filenames.

For TNPU-ALL W3's gate this is the piece that makes "bit-exact against the host reference on the device" mean something specific: K, N, M, seeds, and an edge case, all of which the harness names. No asks.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: CHANGES at 0a79f8f — the rewrite is the right one and its own docstring is inverted.

The inversion

def quantise(weights, axis):
    """… 
    axis=1 -> a per-COLUMN scale (B, the weight matrix: one scale per output column, floored per 32-column group upstream)
    axis=0 -> a per-ROW scale (A, the activations: one scale per token)
    """
    amax = np.max(np.abs(weights), axis=axis)
    shape = (1, -1) if axis == 0 else (-1, 1)
    …

With axis=1, amax is taken along axis 1 and comes back with one entry per row, broadcast as (-1, 1) — a per-row scale. The body and
the call sites agree with each other and disagree with the docstring:

a_codes, a_scale = quantise(a_w, axis=1)        # per row (per token)   ← correct
b_codes, b_scale = quantise(b_w, axis=0)        # per column            ← correct

So the docstring's two lines are swapped. That matters more here than in most files: this tool's whole job is to be the reference someone trusts when a
device number and a host number disagree, and a future reader who needs a per-column scale will reach for axis=1 because the docstring says so, get
a per-row one, and then "discover" a device bug that is their own fixture. Swap the two lines (or rename the parameter), and the code needs no change.

One question to settle, because the reference must match the packer

codes = np.rint(weights / scale) is round-half-to-even. The packer's q4nx_pack_B_int8 presumably converts with a specific mode — if it is
round-half-away-from-zero, the reference's codes differ from the device's on exact .5 boundaries, which is precisely the edge the new edge_weights
fixtures are built to reach. Worth one line in the docstring naming the mode and where the packer's comes from.

What is right, and the review round is visible in it

  • Two independent checks, one of which can actually fail. The docstring now says it: int64 is exact, int32 must agree with it element by element,
    "that is a check that can fail, unlike (a@b)s versus a@(bs), which are the same float expression" — i.e. the earlier rounds were comparing two
    spellings of one expression and calling it agreement.
  • The floor is modelled, not skipped: amax < 1e-12 → 1.0, so an all-zero slice quantises to zeros at scale 1, which is what the container does.
  • floored_cols counts all(b_w == 0), not scales, with the reason in the comment ("absmax exactly 1.0 also gives scale 1/127, so counting scales
    would confuse 'floored' with 'saturating fixture' (caught by running this after the first rewrite)"). That is a bug the author found by running their
    own tool, which is the only way that class is ever found.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 0a79f8f — my finding is addressed, and better than I specified it. I re-ran the self-check:

ok  K=1024 N=1536 M=16 edge=seeded rel=1.2e-15 acc32==acc64=True zero_cols=0    floored=0    maxcode=127
ok  K=1024 N=1536 M=16 edge=zero   rel=1.0e-15 acc32==acc64=True zero_cols=512  floored=512  maxcode=127
ok  K=1024 N=1536 M=16 edge=full   rel=1.0e-15 acc32==acc64=True zero_cols=0    floored=0    maxcode=127
ok  K=1024 N=1536 M=16 edge=floor  rel=0.0e+00 acc32==acc64=True zero_cols=1536 floored=1536 maxcode=0

Three things changed in the right direction:

  • the edge is applied to the weights now, so the case is about the container's scale floor rather than about a matrix that multiplies to zero;
  • rel is no longer 0.0e+00 for the seeded/zero/full arms — 1.2e-15, 1.0e-15, 1.0e-15 — i.e. the comparison now has real numeric content to compare;
  • the vacuity is closed where it matters: self-check asserts floored_cols == a.N for the floor arm (and > 0 for the others), so the arm can fail — not on a relative error that is zero by construction, but on did the floor actually fire in every column. That is the right observable, and it is the one I should have named: I proposed scales == 1e-12, and the container's actual convention is scale 1.0 on an all-zero slice (amax[amax < 1e-12] = 1.0), which tools: expose the int8 reference contract so a family gate can be bit-exact (engine#1808) #1817 pins independently — my suggested threshold was wrong about the convention, the shape of the fix was right.
  • the comment at line 100 also names why this comparison shape can fail where (a@b)*s versus a@(b*s) cannot — same family of reasoning.

Nothing else to change. The residual rel=0.0e+00 on the floor arm is now harmless precisely because the assertion moved off it.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: READY at 0a79f8f7 — and this head is meaningfully stronger than a8a0015a, in exactly the place I flagged.

What improved, and it is the right response to "self-agreement proves the fold, not the reference":

  • the accumulator is now cross-checked int32 against int64 element for element, with int64_fits_int32 alongside it. That is an independent arithmetic path for the part that can silently saturate, not a rearrangement of the same expression;
  • the agreement figure moved from rel = 0.000e+00 to ~1e-15, which is the tell that the two routes are now genuinely different computations rather than one identity written twice — a rounding-level agreement is informative where an exact zero was not;
  • the inputs are quantised the way a container holds them (absmax/127 with a 1.0 floor on an all-zero slice, codes clipped at ±127), so the reference prices the case the kernel actually sees rather than a synthetic int8 matrix;
  • the edge cases moved onto the weights, where the column scales live — a zero column is what floors a scale, and that is now what the zero/floor arms exercise (zero_cols=1536, floored=1536, maxcode=0).

--self-check passes all four arms at this head; I ran it rather than reading it.

Two residuals, neither blocking and neither needing a change here:

  1. --self-check is still one geometry (1024×1536×16). Everything shape-sensitive — the 32-column scale grouping, the padded 3072×5120 the family generator emits, an M that is not a multiple of the microkernel's DIM_M — is untested by it. A loop over two or three geometries inside --self-check is the cheapest way to make the check say something about the shapes that will actually fail.
  2. The known-answer role is now covered elsewhere, so do not build a second one here. #1809's tests/golden/host_gemm_i8_fixture.h is an independently generated fixture whose generator regenerates it byte for byte (I verified that on its head too). That is the hand-checkable anchor for the same arithmetic, and two independent anchors for one function is maintenance for no coverage. Cross-reference them in the docstring instead: "the known answer for the int8×int4 multiply lives in the pack test's fixture; this tool is the traffic/inputs side and the seed spread."

The pairing is the useful outcome: #1809 supplies the exact arithmetic, this supplies reproducible inputs at any geometry, and together they are the host half of the W3 gate rather than two tests that happen to exist.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

The rounding question is answered — by the packer, not by preference.

I asked whether np.rint matches what our packer does. It does not:

npu/forward/src/q4nx_pack.c:656   (inside q4nx_pack_B_int8)
    int x = (int)roundf(v * tis[j]);        // C roundf = round half AWAY from zero
    if (x > 127) x = 127; else if (x < -127) x = -127;

npu/forward/src/q4nx_pack.c:202,205 (inside the host i8 arm, PR #1830)
    int q = (int)roundf(v * tis);
    int aq = (int)roundf(a[k] * 127.0f);    // same mode

np.rint is round-half-to-even (banker's rounding), so on an exact boundary the reference and the packer disagree: v*tis = 2.5 gives 3 in the
container and 2 in family_cost_model's sibling reference here. Your new edge_weights fixtures are designed to hit exactly those boundaries, so this
is not hypothetical — it is the first place a host/device comparison would show a difference that is entirely the fixture's fault.

Two ways to fix, both one line: codes = np.trunc(x + np.copysign(0.5, x)) (round half away from zero, matching roundf exactly), or state in the
docstring that this reference rounds half-to-even and that edge_weights must therefore avoid exact .5 products. The first is the one that makes the
tool a reference.

The rest of the verdict stands unchanged (the inverted quantise() docstring and the unstated --measured-tok-s behind the residual claims).

bong-water-water-bong added a commit that referenced this pull request Nov 10, 2026
…1802 audits) (#1846)

Both are rows of MERGED-OVER-CHANGES-2026-11-10.md: CHANGES at the merged head that nobody addressed. Neither is a code
defect; both are texts a reader will trust as evidence.

1. `tools/npu_elf_ref.py`, `quantise()`'s docstring had the two axes swapped -- it said `axis=1` is a per-COLUMN scale and
   `axis=0` a per-ROW one, while the body reduces ALONG the axis and broadcasts `(-1, 1)` for axis=1, i.e. one scale per
   row. The call sites agree with the body (`a_w, axis=1` = per token, `b_w, axis=0` = per column). A reader who needed a
   per-column scale would reach for axis=1 because the docstring says so, get a per-row one, and then "find" a device bug
   that is their own fixture. The docstring is fixed, the rounding mode is named (rint = half-to-even; the packer's is not
   stated), and --self-check now asserts the convention on a 3x4 matrix so the two cannot drift again:

       ok   axis convention: axis=0 -> 4 column scales, axis=1 -> 3 row scales (3x4 matrix)

2. `npu/forward/include/q4nx_head_image.h` claimed the payload hash makes a stale image impossible. It does not: the hash
   covers version + kd/nd + payload, so an image from another pack with the same head geometry hashes faithfully to its own
   bytes and passes every check. That is integrity and geometry, not provenance. Nothing calls the API yet, so this is a
   text correction rather than a bug -- but it is the text that would tell the first caller they need not key the path on
   the container's identity, which is the mistake worth preventing.

No behaviour changes.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@users.noreply.github.com>
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.

1 participant