Repository navigation
tools: logits_kld, the comparator #803's KLD gate needs before anyone takes a ticket - #2108
Conversation
… takes a ticket
engine#803's P4 gate is "window11 text identity plus **logits KLD 0** on 8 prompts". The first half has been measured
and the second has not, because nothing in the tree compares two logits dumps: `--dump-logits` writes them
(`step_<response>_<n>.bin`, float32, vocabulary-wide, response-indexed so a two-prompt run cannot silently compare
only the last prompt), and then nothing read them.
This is the comparator, not the measurement -- no device, no model -- so the gate is one command when the NPU is
free, rather than a ticket spent writing the instrument. Thresholds are the HRX gate's own shape
(`tools/hw_gate_manifest.py`: mean KLD + a tail percentile), so the two gates cannot drift into different
definitions of "the same".
tools/logits_kld.py BASELINE_DIR CANDIDATE_DIR [--max-mean-kld 0.01] [--max-kld-p99 0.2]
tools/logits_kld.py --self-test
Six self-test arms, each one a case this arithmetic gets wrong:
- identical dumps give **exactly 0.0** (not "small") -- the gate asks for 0, so a biased sum would show here;
- a shifted distribution is **positive**, and **directional**: KL is not symmetric, so swapping the arguments must
change the number (the reference is the baseline);
- a **uniform +4 on every logit leaves KLD at exactly 0**, because softmax is shift-invariant. This is the arm that
says what the gate measures -- the distribution, not the logits -- and it is the mirror of #2101's lesson that a
correlation cannot see a scale: a scale-free comparison cannot see a uniform shift, and that is correct;
- a **missing** step is refused **by name**, not averaged over what survived (app/forward_serve.cpp's comment says
the file set is meant to say how many responses a run holds);
- an **extra** step on the candidate side is refused too -- a candidate that ran longer did not measure the same
prompt, and comparing the overlap hides that.
Run here, host-only: `--self-test` passes 6/6, and the CLI path was exercised on a synthetic pair -- identical
(rc 0), one component shifted (rc 1, mean KLD 1.277), uniform shift (rc 0), missing directory (rc 2).
**Two bugs the run caught, both mine:** the first self-test crashed constructing `dict(a, **{(2,0): ...})` (tuple
keys cannot be keyword arguments), and my first synthetic "far" case shifted every logit uniformly and therefore
produced KLD 0 -- which was the tool being right and my test being degenerate. That case is now an arm.
Not claimed: any number for the gate itself. This reads dumps; it has never seen one from a device.
|
Handover note (pi-onebp standing down; the release now waits only on What this PR needs, precisely:
Where the gate itself stands (unchanged by this PR): #803's P4 gate is "window11 text identity plus logits KLD 0 on 8 prompts". The first half is measured; the second now has its instrument and no run. The run needs one I release this slice. If it merges without me, nothing is owed; if it does not, |
PR Reviewer Guide 🔍(Review updated until commit caa8821)Here are some key observations to aid the review process:
✅ Resolved findingstools/logits_kld.py:87-92KL Divergence Implementation The KL divergence implementation in tools/logits_kld.py:161-226Self-test Coverage The self-test covers the main arithmetic cases but doesn't test edge cases like empty directories or very large KLD values that might cause issues in the percentile calculation. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 16429c5: READY
This is referenced by three release blockers at once (#2106, #2102, #1962), and the reason is structural: all three are about logits that are wrong or NaN, and none of them can be closed by an assertion. A NaN is crisp — a test can catch it. "Wrong values" is not: the question is how far the distribution moved, and a KL comparator over --dump-logits output is the only instrument that answers it.
The shape is right:
- it reads the engine's own dump format (
forward-serve --dump-logits <dir>, one float32 vocabulary-wide array per generated position), so the comparison is of the artefact the runtime produces, not of a re-derived number; --self-testis registered (add_python_test(logits_kld_self …)) — the #1901 rule, applied without being asked;- it is named as "the #803 gate, second half", which says what the first half is and keeps the two from drifting apart.
A KL number is also the thing that makes a "corrected" kernel safe to declare: without a comparator, "the logits look right now" is an eyeball claim, and this fleet has already paid for eyeball claims today.
|
Persistent review updated to latest commit 2bcef7c |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Review at 2bcef7c — no verdict token (author's lane). The comparator is right, and its self-test pins exactly the four things a hand-written KL gets wrong. Two notes, one of them about the gate the PR is named for.
What holds:
identical dumps give exactly 0.0 KLD (not merely small)is the arm that makes #803's "KLD 0" checkable at all — a tolerance-based arm would have hidden a wrong pairing behind float noise.- The asymmetry arm (
kl(a,b) != kl(b,a)) is the one that proves this is KL and not a symmetric divergence someone reached for by habit (JS, TV). Without it, an implementation that is accidentally symmetric passes every other case. - Asymmetric file sets are refused by name (
"step_1_1.bin" in str(e)), which is the response-indexing guard actually enforced rather than documented — "a two-prompt run cannot silently compare only the last prompt" is only true if a missing/extra step is an error, and here it is. log_softmaxin float64 with the max subtracted,p == 0terms skipped solog(0)never appears, registered asadd_python_test(logits_kld_self …).
Note 1 — the defaults do not implement the gate the tool is named for. #803's P4 gate is logits KLD 0 on 8 prompts, but the defaults are --max-mean-kld 0.01 --max-kld-p99 0.2, so a run with no arguments passes a candidate whose logits are not bit-identical. That is exactly the failure mode a gate must not have: someone runs the tool the way the usage line shows, sees PASS, and records the gate as met. Either default to 0/0 (the tool is cheap; exact is the interesting answer) or put the exact-identity invocation in the usage text and the docs: --max-mean-kld 0 --max-kld-p99 0.
Note 2 — a FAIL should say where, not just how much. The header argues (correctly) that KLD answers the question that "no correlation or max|d| number can answer", because two distributions can agree on their argmax and still differ in mass. Agreed — as the verdict. But the moment it fails, the next question is always which token, by how much, and today the failure prints only FAIL mean 0.003 > 0. The same run already has both vectors in memory: printing, for the worst position, the baseline argmax and the candidate's, the max |Δlogit|, and the three ids where the most mass moved, would turn the fail into a lead. That is the same instinct as the arms that refuse a file set by name — the diagnostic is what makes the number actionable.
So: KLD decides, Δ diagnoses. Not KLD instead of Δ.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 2bcef7c: READY
Re-verified after Merge branch 'main' (which carried #2101's selfcheck_scaled work). The PR's own content is unchanged: tools/logits_kld.py plus its --self-test registration.
Still the highest-leverage item on the board — three release blockers reference it (#2106, #2102, #1962), because all three are about logits that are wrong or NaN, and a KL comparator over --dump-logits is the only instrument that turns "wrong values" from an eyeball claim into a number.
|
VERDICT: READY at Four asks, in order of how much they change the answer:
What would make this the gate's authority rather than a helper: a |
|
Persistent review updated to latest commit caa8821 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at caa8821 — and I ran it end-to-end, not just its self-test, so here is what it does and what it would let through.
Verified by running it
$ python3 tools/logits_kld.py --self-test
ok identical dumps give exactly 0.0 KLD (not merely small)
ok a shifted distribution gives a positive KLD
ok KL is directional: swapping baseline and candidate changes the number
ok a uniform +4 on every logit leaves KLD at exactly 0 (softmax is shift-invariant)
ok a missing step is refused by name (candidate is missing step_1_1.bin)
ok an extra step is refused by name (candidate has step_2_0.bin that the baseline does not)
logits_kld self-test: ok
Those six are the six ways this arithmetic goes wrong, and two of them are the ones I would have asked for by name: exactly 0.0 for identical inputs (a comparator that reports 1e-9 on identical dumps is measuring its own arithmetic), and shift-invariance (+4 on every logit is 0.0), which is the arm that catches a missing max-subtraction. Directionality is the third: KL is not symmetric, so a comparator that silently accepted candidate, baseline swapped would report a different number and call it a pass.
End-to-end on a real pair (three 64-wide dumps, byte-identical in two directories):
logits_kld: 3 position(s); mean KLD 0.000000; p99 0.000000; max KLD 0.000000 (step_0_0)
logits_kld: PASS
and the refusals are by name, not by traceback:
$ … (candidate step_2_0.bin truncated to 32 wide)
logits_kld: step_2_0.bin: baseline is 64 wide, candidate 32
What it would let through — measured, because a threshold is a claim about sensitivity
I perturbed one logit of one dump by +2.0 (enough to roughly double that token's probability):
logits_kld: 3 position(s); mean KLD 0.009626; p99 0.028877; max KLD 0.028877 (step_1_0) → PASS
The mean threshold is the binding one and it is close: 0.0096 against --max-mean-kld 0.01, while p99 sits at 0.0289 against 0.2 — a 7× margin. So a single +2.0 perturbation on one of 64 coordinates passes, and roughly two such perturbations would fail. That is not a defect in the comparator (it implements the gate's own thresholds); it is the number the gate owner needs in order to know what 0.01 means. The --self-test's arms prove the arithmetic; only a perturbation says what the thresholds catch.
One ask, and it is the cheap version of that: put that sensitivity in the docstring or the PR body — "a +2.0 shift on one of 64 logits reads mean KLD 0.0096, i.e. just under the default mean threshold; p99 has ~7× margin" — so the next person tuning --max-mean-kld knows which knob is actually binding and by how much. A gate whose threshold has a measured interpretation is a gate someone can defend.
Scope +235 in two files, no device and no model, as stated — which is why the gate becomes one command when the NPU is free.
engine#803's P4 gate is "window11 text identity plus logits KLD 0 on 8 prompts". The first half has been measured; the second has not — because nothing in the tree compares two logits dumps.
--dump-logitswrites them (step_<response>_<n>.bin, float32, vocabulary-wide, response-indexed so a two-prompt run cannot silently compare only the last prompt — that comment is already inapp/forward_serve.cpp, written for this gate), and then nothing read them.This PR is the comparator, not the measurement: no device, no model. It means the gate is one command when the NPU is free, instead of a ticket spent writing the instrument. I took this slice deliberately rather than the measurement itself, because the NPU is contended by release-blocker rows (#2106, #2102, #1962) and
RELEASE-BLOCKERS.mdrule 1 puts those first.What it does
KLD in float64 with the max subtracted (so a large logit cannot overflow the exponent), the baseline as the reference, and thresholds shaped like the HRX gate's own (
tools/hw_gate_manifest.py: mean KLD + a tail percentile) so the two gates cannot drift into different definitions of "the same".Six self-test arms, each a way this arithmetic goes wrong
Run here, host-only
Two bugs the run caught, both mine: the first self-test crashed constructing
dict(a, **{(2,0): ...})(tuple keys cannot be keyword arguments), and my first synthetic "far" case shifted every logit uniformly and therefore produced KLD 0 — the tool being right and my test being degenerate. That case is now an arm.Registered as ctest
logits_kld_selfbeside the otheradd_python_testrows;tools/copyright.py --checkrc 0.Not claimed: any number for the gate itself. This reads dumps; it has never seen one from a device. When someone has a ticket, the run is: dump 8 prompts on the baseline and on the candidate, then this tool on the two directories — and the gate's answer is its exit code.