feat(tools): offline ablation-evidence checker and an evidence-bound eval harness - #1355
monotophic wants to merge 8 commits into
Conversation
The evidence tooling reads the engine's own stdout. Two of its lines are typed records -- the "== GLM C engine" banner and the "loaded in ..." line that follows it -- and every consumer that wants a field out of them was about to reimplement the same grammar. This adds one module that owns the exact text, the field ranges each number may take, and the errors a malformed line produces, so the consumers agree by construction. It also owns the canonical form of an ablation manifest: the engine reads a manifest a line at a time, drops the terminator and one carriage return before it, and digests the record followed by a single newline. canonical_manifest_bytes() is that rule written once for the Python side, so a checker and the engine cannot drift into hashing different bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_ablate_evidence.py re-checks one ablation manifest and its JSONL evidence artifact against an independently supplied config.json, without the engine and without any test binary: the config and manifest field bounds, every record's key set and field types, the header's binding to the config and to the manifest digest, and the truncation, replay and mismatch cases at each record boundary. Three cross-record invariants are enforced because the producer guarantees them for every row it can emit -- nll is never negative, corr agrees with the argmax-equals-gold comparison, and the argmax logit is never below the target's. Top-k order is deliberately not enforced: it is unsorted on the wire by design. The manifest is reduced through the shared canonical form before it is parsed or hashed, so a manifest saved with CRLF endings or without a final newline -- both of which the engine accepts and normalises -- is checked rather than refused, and binds the same digest the engine bound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
eval_glm.py scored answers from whatever the engine printed and wrote its results only after the whole run finished. Two consequences: a foreign line on that stream was absorbed silently, and an interrupted run left nothing to read. Result rows are now appended per request, so a run that dies mid-task leaves a valid partial file carrying an INCOMPLETE marker and still prints the accuracy table over the rows that landed. Every line of the scoring stream is classified against the engine's documented SCORE-mode output and an unrecognised line is refused by name instead of ignored, and the numeric grammar accepts both the six-decimal form the engine ships today and the older %.17g form. Where the engine build cannot supply the evidence record the harness would bind to, the run is marked UNBOUND by name rather than reported as bound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pack_python.py reaches a file two ways: by following imports out from coli, and by spotting scripts that are launched as subprocesses. Only the first kind had its own imports followed, so a module that only a subprocess-launched script imports was left out of the release archive while --check still reported the archive complete. On dev this happens twice: tools/iq3_pack.py, which tools/convert_fp8_to_int4.py imports inside quant_e8(), and tools/engine_evidence.py, which this branch's eval_glm.py imports. Both are now reached. The test suite grows a real-tree assertion as well as fixture cases, because every existing case builds a disposable tree and so could not have caught either omission. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On Windows, os.kill(pid, SIGTERM) is TerminateProcess with the exit code set to the signal number: no Python handler runs and the whole unittest process dies with status 15, which is how the Windows UCRT64 job reported `make: *** [test-python] Error 15` with no test summary. The behaviour the test asserts (a real SIGTERM converted into engine-child cleanup) is a POSIX mechanism; the SIGINT/exception half of the pair covers child cleanup on every platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… its newline The checker prints its PASS line with print(), so a Windows child hands the test CRLF where POSIX hands LF, and the byte-exact assertion failed on the Windows UCRT64 job (the only failure in a 914-test run). Nothing consumes that line byte-for-byte; the pin is on the content, so the test now folds CRLF to LF before comparing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolves the conflict JustVugg#1364 created, so this branch does not need a rebase. JustVugg#1364 landed the packer commit from this PR (cherry-picked, authorship intact) plus a second edge it left open: a data file opened next to a reached module. tools/pack_python.py therefore takes dev's version wholesale, which is a strict superset of this branch's -- the only lines unique here were the ones that commit replaced. tests/test_pack_python.py takes dev's version and keeps this branch's engine_evidence assertion as an ADDITIONAL real-tree case rather than a replacement. On dev that assertion could not run, because engine_evidence.py is introduced here; on this branch it pins the edge this branch adds, next to the one that pins the historical bug. 12 tests, all green.
|
Conflict resolved on your branch, pushed as What I did, so you can check rather than trust it:
12 tests, green locally. CI is running. One thing I noticed while verifying, worth a decision from you
Not a defect in your change: nothing in the code names it, and it takes So it is a decision, not a bug: if the checker is meant to be usable from a release archive, it wants the same explicit copy plus a Thanks again for #1359. Cherry-picking your commit rather than rewriting it was the least I could do after duplicating another contributor's work last week by not checking the PR list first. |
…erifies pack_python.py computes the archive from what coli reaches, so a tool whose only caller is a person typing argparse arguments never makes it in. k3_tokenizer.py has always been in that position and release.yml copies it by hand; check_ablate_evidence.py joined it with this branch. It verifies the ABLATE evidence that the packaged eval_glm.py produces, so an archive that ships the producer without the verifier is half a tool. Copy it explicitly, gate on it with the same test -f the tokenizer helper gets, and parse-check the packaged file. A real-tree test pins both halves of the decision: the walk does not reach either human-only tool (if it ever does, the explicit copy is redundant and the pin says so), and the workflow both copies and gates each one. A copy without a gate is how v1.10.0 shipped green with four broken commands (JustVugg#1296). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@JustVugg: Thanks for patching the bugs, resolving the conflict on this branch, and the write-up. I checked the merge: pack_python.py at 9e96637 is dev's file with nothing of ours dropped, and test_pack_python.py keeps the engine_evidence case as an added assertion. I have pulled 9e96637 and have not rewritten the branch. On check_ablate_evidence.py: it should ship in the release archive. It verifies the evidence that eval_glm.py produces, and that harness is in the archive, so anyone running a released engine and producing ablation evidence wants the checker beside it. Added in ba2802a: the same copy and test -f gate that release.yml gives k3_tokenizer.py, plus a parse check, and a real-tree test that pins both halves (the walk does not reach either human-only tool; the workflow copies and gates each). #1356 [Authored by Claude Fable 5.1 in Claude Code, analysis in partnership with @monotophic.] |
Authored by Claude Opus in Claude Code, analysis in partnership with @monotophic
The engine's diagnostic modes write artifacts that something else has to read
back: an ablation run writes a JSONL evidence file, and the evaluation harness
reads scoring output off the engine's stdout. Today nothing re-checks either one
independently, and the harness trusts whatever it is handed.
This adds three tools and closes one packaging gap.
tools/engine_evidence.pyowns the grammar of the engine's two typed startuplines (the
== GLM C enginebanner and theloaded in …record) and thecanonical form of an ablation manifest, so every consumer agrees by construction
instead of each re-deriving the same regular expressions and the same
normalisation.
tools/check_ablate_evidence.pyre-checks one manifest and its evidenceartifact against an independently supplied
config.json, with no engine and notest binary: field bounds, every record's key set and types, the header's
binding to the config digest and the manifest digest, and the truncation,
replay and mismatch cases at each record boundary. Three cross-record
invariants are enforced because the producer guarantees them for every row it
can emit; top-k order is deliberately not enforced, because it is unsorted on
the wire by design.
tools/eval_glm.pygains two properties it did not have. Results are appendedper request, so a run that dies mid-task leaves a valid partial file with an
INCOMPLETEmarker and still prints the accuracy table over the rows thatlanded — before, an interrupted run left nothing. And every line of the scoring
stream is classified against the engine's documented SCORE-mode output, so an
unrecognised line is refused by name instead of silently absorbed.
tools/pack_python.pyreached a file two ways — by following imports out fromcoli, and by spotting scripts launched as subprocesses — but only followed thefirst kind's own imports. A module that only a subprocess-launched script
imports was therefore left out of the release archive while
--checkstillreported the archive complete. On
devthat istools/iq3_pack.py, whichtools/convert_fp8_to_int4.pyimports insidequant_e8(). This is apre-existing packaging defect on
dev, not one this branch introduces; wefound it while checking that the new helper module would itself be packaged. We
will open a separate issue describing it, once this PR is up so the issue can
point at the fix.
Context. This PR is one of four independent contributions derived from a
single locally-verified working tree (offline evidence tooling, the engine's
ablation scoring mode, two engine-transcript check tools, and a raw wire-format
adapter). This one carries the Python evidence tooling and the packaging fix.
It stands alone — its whole suite is green with the other three absent (169
cases, measured on a checkout of
devcarrying only these eight files), andmerging or declining the others does not affect it. The others are proposed
separately, each with its own evidence.
Behavioral contract
loading, per-option scoring, exact-vs-greedy accounting, and the output format
existing consumers read are unchanged, including
--drywithout a vocabularyand the accuracy table on a partial run.
including manifests saved with CRLF endings or without a final newline, and
binds the same digest the engine binds for all three framings of the same
content.
digest reproducible outside the tool.
UNBOUNDbyname; it is never reported as bound.
subprocess-launched script is packaged, and
--checkfails when it is not.Capstone matrix — one decisive artifact per claim; each would fail if the
claim were false.
tests/test_check_ablate_evidence.py:605(Python) andtests/test_ablate_mode.c:287(C, in the engine PR) — and reproduces outside both:printf 'coli-ablate-manifest/2\n0 3 2 0 0 1 2 3\n' | shasum -a 256→c63a48c375b14ca60f26c7e3c5dd36b5929ffaf669a45511c93deee6e8bbd5edcoli-ablate/2header, one item header, three logit records, one terminal record) that the checker validated against the model's ownconfig.json:[ablate-evidence] PASS manifest=4e18e3aba08f4b0a0f5e33619ad910cc6c7d4b90108efde734521bd6fac8b24a items=1 targets=3, exit 0UNBOUND— a named status — rather than claiming a binding it did not have; the run exited 0dev's own harness on the same fixtures: byte-identical output, including--drywithout a vocabulary and a partial run's table plus itsINCOMPLETElinedev'stools/pack_python.pyandtests/test_pack_python.pyfails attest_engine_evidence_is_needed_by_the_real_tree, naming the file that fell out of the computed setc/tree, not a fixture the test built; every other case in the module builds a disposable tree, which is exactly why none of them caught this ondevFuller matrix, deferrals, and origin accounting
loaded inregular expressions read against the engine's own format strings and the seven kernel names it can printnllnever negative; the correctness flag equals the argmax-equals-gold comparison; the argmax logit never below the target's); top-k order deliberately not enforced%.17gform, plusnan/inf/-infINCOMPLETEmarker, table printed, child terminateddevit was absorbed)python3 -m unittest tests.test_engine_evidence tests.test_check_ablate_evidence tests.test_eval_glm tests.test_pack_pythonondev+ these eight filesRan 169 tests … OKDeferrals, stated plainly: two harness cases that need a test binary this build
does not produce are not present in the module — they are described in its
docstring as deferred, rather than shipped as skips. Nothing here is skipped for
a reason a reviewer cannot see.
Scope note:
tools/check_ablate_evidence.pyis a repository tool, not anarchive tool — it is not reached from
coli, so it is deliberately not in therelease archive.
tools/engine_evidence.pyis, because the harness imports it,and that is what the packaging fix restores.
Origin accounting. The helper module, the checker and the adapter-side harness
work are new content. Two changes are fixes to defects that exist on
devtoday and were found while reviewing this area: the packaging closure gap
described above, and the harness's silent acceptance of a foreign stdout line
together with its all-or-nothing result writing. The three cross-record
invariants and the byte-level rejection cases were added during review, each
proven against the producer's own arithmetic rather than assumed. No file
outside the eight listed is touched, and no surrounding code was reformatted.
Durable vs current state: the tools, their grammars and the packaging rule
are durable. The counts (169 cases) and the fleet run quoted above are current
state, measured 2026-09-04 against base
e1efc68on macOS (Apple silicon) and,for the fleet cell, on a Linux CPU host with GCC.