colibri: add CONSIST=1 prefill/decode self-consistency check - #1555
Open
phreakocious wants to merge 1 commit into
Open
phreakocious wants to merge 1 commit into
phreakocious wants to merge 1 commit into
Conversation
The same positions evaluated two ways must produce the same logits. Arm A
pushes the whole sequence through step_all in one batched prefill; arm B
prefills a prefix and then walks the continuation one token at a time through
the KV cache, exactly as generate() does. The two arms share weights, so a
disagreement beyond float accumulation order is a KV-addressing or masking
defect in one of them.
It needs no reference PREDICTIONS -- no tf_pred, no second implementation --
because it compares the engine against itself. Tokens come from either source:
SNAP=<model> REF=<ref.json> CONSIST=1 ./colibri 64 16 16 # prompt_ids/full_ids
SNAP=<model> PROMPT="..." CONSIST=1 ./colibri # no ref file at all
The PROMPT form is what lets the check run on a model no fixture exists for. It
tokenizes with the same [gMASK]<sop> prefix run_text applies -- skip that and
the sequence is out-of-distribution, both arms agree on garbage, and the check
cannot fail -- then splits at CONSIST_NP (default halfway).
The gate is the largest RELATIVE logit gap, which is what separates the two
failure classes: reordered f32 accumulation over the hidden dim lands near
D*eps, a misaddressed KV row lands at O(1). Argmax flips are reported but NOT
gated: a flip requires |a[ia]-a[ib]| < 2*gap by construction, so "the flip is
explained by the gap" holds for every flip and would be an assertion that
cannot fail.
Measured on GLM-5.2, 744B int4-gs64, D=6144, 15 tokens split 8/7:
CONSIST_TOL=1e-2 (default) worst relative gap 3.902e-04 OK, exit 0
CONSIST_TOL=1e-6 worst relative gap 2.762e-04 FAIL, exit 1
D*eps at D=6144 is 7.3e-04, so the observed gap sits just under the float
reordering noise floor and ~3 orders of magnitude below where a KV defect would
land. The second run is the positive control: the gate fires and exits non-zero,
so the passing result is not an assertion that cannot fail. The two gaps differ
because expert cache state changes accumulation order between runs; both sit far
below the default tolerance.
make check passes (921 tests, 85 skipped).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prefill and decode must agree on the same positions. Arm A pushes the whole sequence through
step_allin one batched prefill; arm B prefills a prefix and then walks the continuation one token at a time through the KV cache, exactly asgenerate()does. Both arms share weights, so any disagreement beyond float accumulation order is a KV-addressing or masking defect in one of them.It needs no reference predictions β no
tf_pred, no second implementation β because it compares the engine against itself. Tokens come from either source:The
PROMPTform is what lets this run on a model no fixture exists for β including quantizations and backends no CI runner can build a reference for. It tokenizes with the same[gMASK]<sop>prefixrun_textapplies (skip that and the sequence is out-of-distribution, both arms agree on garbage, and the check cannot fail), then splits atCONSIST_NP, default halfway.It also covers
COLI_PREFILL_CHUNK, which only arm B honours, and whose single-shot equivalence was previously asserted only in a comment onstep().Why the gate is the relative gap
The two failure classes separate cleanly by magnitude: reordered f32 accumulation over the hidden dim lands near
D*eps, a misaddressed KV row lands at O(1). Argmax flips are reported but deliberately not gated β a flip requires|a[ia]-a[ib]| < 2*gapby construction, so "the flip is explained by the gap" holds for every flip and would be an assertion that cannot fail.Evidence
Measured on GLM-5.2, 744B int4-gs64, D=6144, 15 tokens split 8/7, CPU path:
1e-2(default)1e-6D*epsat D=6144 is 7.3e-04, so the observed gap sits just under the float-reordering noise floor and ~3 orders of magnitude below where a KV defect would land.The second row is a positive control: the gate fires and exits non-zero, so the passing result is not an assertion that cannot fail. The two gaps differ between runs because expert cache state changes accumulation order; both sit far below the default tolerance.
Validation
make -C c checkβ 921 tests, OK (85 skipped), exit 0. Builds with 0 warnings.make -C c cuda-testβ n/a, no CUDA code touchedCompatibility
The existing
REF-driven path is unchanged;PROMPTmode previously returned before reaching the CONSIST branch, so nothing that worked before behaves differently.