Skip to content

ci: the ratchet also covers f16 accumulators, the half-precision shape no truncation scan can see (engine#2186, #2167) - #2231

Merged
bong-water-water-bong merged 2 commits into
mainfrom
1bit/2219-accumulator-ratchet
Nov 11, 2026
Merged

bong-water-water-bong merged 2 commits into
mainfrom
1bit/2219-accumulator-ratchet

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Refs #2186, #2167. The ratchet from #2114/#2220 matches fptrunc — a value converted to f16. That is the first two of the three shapes that lose a value to half precision, and it is blind to the third by construction: an f16 accumulator has no conversion instruction anywhere, because the accumulator already is f16. The sum leaves 65,504 inside vector.mulf or a WMMA accumulate.

Why this is not hypothetical: #2167 is exactly that — a plain MUL_MAT_ID on bf16 fails at activation_scale=10000, far below the f16 overflow point, because 256 terms of that magnitude accumulate past it. #2186 item 1 is the same class in the routed gate/up. Both were found by hand; neither could trip the existing gate.

What it adds

ACC_INIT    vector.fragment<init> … vector<Nxf16>
ACC_MUL     vector.mulf | template.apply<@…wmma…> | vector.fma|contract|dot|outerproduct | wmma.mma
ACC_WINDOW  250 lines past the init

The window is measured, not chosen. At 120 lines, mul_mat_id_f16_f16_wmma_core's four vector<8xf16> accumulators read as unaffected — their update sits ~250 lines below in a long Q4_K/Q6 loop body — and a gate that reports a real overflow as unaffected is the exact failure mode this family keeps producing. My own tool had the same defect and the same fix.

The baseline is 7 files, and three of them are new information

Including flash_attention_f32_f16_wmma.loom, flash_attention_decode_f32_f16_wmma.loom and their qwen_moe siblings, which accumulate in f16 too. For attention that is a sum over the context, so the longer the prompt the more room the accumulator needs — a candidate for the long-context NaNs (#1536, #1346) that this family had not named, and one the body-granularity ratchet could never surface: attention's logits stay f32, so the narrowing is not the thing that overflows there.

Verified end to end, through a temp root with a real corpus

with the baseline:  51 narrowing body(ies), none new, none stale;
                    7 f16 accumulator body(ies), none new, none stale; 10 of the narrowing bodies carry a guard marker
+ one new file:     NEW f16 accumulator zztest/new.loom -- … add the line and say what the accumulator holds
                    FAIL (0 new, 0 stale of 51, 0 unclassified fptrunc; 1 new, 0 stale of 7 accumulator(s))

--self-test: ok — including a fixture whose update is 200 lines below the init, a WMMA-update case, and an add-only negative (an add-only f16 accumulator needs a single ~65,504 term rather than a product, so it is not this class).

Scope

Adds a check and a baseline; does not change any kernel. If a reviewer would rather this live in the inventory tool (tools/hrx_f16_narrowing_check.py, merged in #2204) than in the ratchet, that is a fair call — but the ratchet is the thing that fails a PR, and this is the class that has slipped through twice.

…e no truncation scan can see (engine#2186, engine#2167)

The ratchet in #2114/#2220 matches `fptrunc` -- a value converted to f16. That is the first two of the
three shapes that lose a value to half precision, and it is blind to the third **by construction**: an
f16 accumulator has no conversion instruction anywhere, because the accumulator already IS f16. The
sum leaves 65,504 inside `vector.mulf`/a WMMA accumulate.

engine#2167 is exactly that: a plain MUL_MAT_ID on bf16 fails at `activation_scale=10000`, which is
well below the f16 overflow point, because 256 terms of that magnitude accumulate past it. engine#2186
item 1 is the same class in the routed gate/up.

So this adds a second baseline beside the body list:

  ACC_INIT    `vector.fragment<init> … vector<Nxf16>`
  ACC_MUL     `vector.mulf`, `template.apply<@…wmma…>`, `vector.fma|contract|dot|outerproduct`, `wmma.mma`
  ACC_WINDOW  250 lines past the init

The window is the part that matters and it is measured, not chosen: at 120 lines,
`mul_mat_id_f16_f16_wmma_core`'s four `vector<8xf16>` accumulators read as unaffected -- their update sits
~250 lines below in a long Q4_K/Q6 loop body -- and a gate that reports a real overflow as unaffected is
the failure mode this whole family keeps producing. The self-test has a fixture whose update is 200 lines
below the init, plus a WMMA-update case and an add-only negative.

The baseline is 7 files, and three of them are worth naming: `flash_attention_f32_f16_wmma.loom`,
`flash_attention_decode_f32_f16_wmma.loom` and their qwen_moe siblings accumulate in f16 as well. For
attention that is a *sum over the context*, so the longer the prompt the more room the accumulator needs
-- a candidate this issue family had not had named for the long-context NaNs (#1536, #1346), and one the
body-granularity ratchet could never have surfaced because attention's logits are f32 and its narrowing
is not the thing overflowing.

Verified end to end against a real corpus through a temp root: with the baseline, `51 narrowing bodies,
none new, none stale; 7 f16 accumulator bodies, none new, none stale`; adding one new accumulator file
fails with `NEW f16 accumulator … add the line and say what the accumulator holds`.

Refs #2186, #2167
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit a7051bc)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

2186 - Partially compliant

Compliant requirements:

  • Added a ratchet mechanism to detect f16 fragment accumulators updated by multiply operations
  • Created a new baseline file tools/ci/hrx_f16_accumulators.txt to track these accumulators
  • Updated tools/ci/hrx_f16_narrowing.py to include logic for detecting these accumulators

Non-compliant requirements:

  • The actual fixes for the overflow/NaN issues in these accumulators are not implemented in this PR

Requires further human verification:

  • Verification that the identified accumulators are indeed causing the issues described in the ticket
  • Confirmation that the ratchet correctly identifies all relevant accumulators

2167 - Partially compliant

Compliant requirements:

  • Added detection for f16 accumulators that can cause the described NaN issues
  • Created baseline tracking for these accumulators

Non-compliant requirements:

  • The actual fix for the NaN issue in MUL_MAT_ID with bf16 is not implemented in this PR

Requires further human verification:

  • Verification that the identified accumulators are indeed causing the NaN issue described in the ticket
  • Confirmation that the ratchet correctly identifies all relevant accumulators

1536 - Partially compliant

Compliant requirements:

  • Added detection for f16 accumulators that could contribute to the NaN logits issue
  • Created baseline tracking for these accumulators

Non-compliant requirements:

  • The actual fix for the long-context NaN logits issue is not implemented in this PR

Requires further human verification:

  • Verification that the identified accumulators are indeed contributing to the NaN logits issue
  • Confirmation that the ratchet correctly identifies all relevant accumulators
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Missing Fix Implementation

The PR introduces a ratchet to detect f16 accumulators, which is a necessary step towards fixing issues #2186, #2167, and #1536. However, the actual fix implementations for these issues are not included in this PR. The ratchet detects the presence of f16 accumulators but does not implement the scaling or other fixes needed to prevent overflow or NaN issues. This means that while the detection mechanism is in place, the underlying problems remain unresolved.

# engine#2186 item 1 / engine#2167: the accumulator class, which no truncation scan can see.
acc_base_file = root / ACC_BASELINE
if not acc_base_file.is_file():
    print(f"hrx_f16_narrowing: {ACC_BASELINE} is missing -- run `--write` once to record the accumulators this "
          f"corpus has (engine#2186)")
    return 1
acc_baseline = baseline_paths(acc_base_file.read_text(encoding="utf-8"))
acc_found = accumulator_bodies(corpus)
acc_new, acc_stale = drift(acc_baseline, acc_found)
guard = guarded(found, corpus)
unknown = unclassified_truncations(corpus)
for where, text in unknown:
    print(f"hrx_f16_narrowing: UNCLASSIFIED fptrunc at {where}: {text} -- its type pair matches neither the f16 "
          f"patterns nor a known non-f16 target; extend NARROW or KNOWN_NON_F16 (engine#2219)")
for rel in new:
    print(f"hrx_f16_narrowing: NEW narrowing body {rel} -- it narrows an f32 value to f16 and the "
          f"baseline does not name it; add the line and say what feeds it (engine#1962)")
for rel in stale:
    print(f"hrx_f16_narrowing: {rel} is in the baseline but no longer narrows -- drop the line, the "
          f"list should shrink")
for rel in acc_new:
    print(f"hrx_f16_narrowing: NEW f16 accumulator {rel} -- an f16 fragment is updated by a multiply, so the sum "
          f"overflows f16 long before any single input does and no fptrunc is involved; add the line and say what "
          f"the accumulator holds (engine#2186, engine#2167)")
for rel in acc_stale:
    print(f"hrx_f16_narrowing: {rel} is in the accumulator baseline but has no mul-updated f16 fragment -- drop "
          f"the line, the list should shrink")
if new or stale or unknown or acc_new or acc_stale:
    print(f"hrx_f16_narrowing: FAIL ({len(new)} new, {len(stale)} stale of {len(baseline)}, "
          f"{len(unknown)} unclassified fptrunc; {len(acc_new)} new, {len(acc_stale)} stale of "
          f"{len(acc_baseline)} accumulator(s))")
Baseline File Creation

The PR creates a new baseline file tools/ci/hrx_f16_accumulators.txt to track f16 accumulators. While this is a good step for tracking, it does not address the core issues of overflow or NaN in the accumulators. The file is correctly formatted and includes the necessary Apache-2.0 notice, but it only serves as a tracking mechanism without implementing fixes.

# Copyright 2026 bong-water-water-bong
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# f16 fragment accumulators updated by a multiply (engine#2186 item 1, engine#2167).
#
# This is the third shape that loses a value to half precision, and the only one a truncation scan
# cannot see: the accumulator IS f16, so there is no `fptrunc` to find. The sum over the reduction
# leaves 65,504 long before any single input does -- engine#2167's case fails at
# activation_scale=10000, well below the f16 overflow point, because k=256 terms of that magnitude
# accumulate past it.
#
# One line per .loom that has such an accumulator. `tools/ci/hrx_f16_narrowing.py --write`
# regenerates this file and tools/ci/hrx_f16_narrowing.txt; a new entry is drift the ratchet
# refuses, and a line whose file no longer has a mul-updated f16 fragment is stale and should be
# dropped as the corpus is fixed.
loom-libs/motifs/mul_mat_id_f16_f16_wmma_core.loom
loom-libs/motifs/mul_mat_id_swiglu_f16_f16_wmma_body.loom
loom-libs/ops/flash_attention_f32_f16_wmma.loom
loom-libs/ops/mul_mat_q6_k_packed_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_decode_f32_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_f32_f16_wmma.loom
qwen_moe/qwen3_moe/routed_linear_q4k_f16_wmma.loom
✅ Resolved findings

tools/ci/hrx_f16_narrowing.py:144-174

Missing Fix Implementation

The PR introduces a ratchet to detect f16 accumulators, which is a necessary step towards fixing issues #2186, #2167, and #1536. However, the actual fix implementations for these issues are not included in this PR. The ratchet detects the presence of f16 accumulators but does not implement the scaling or other fixes needed to prevent overflow or NaN issues.

tools/ci/hrx_f16_accumulators.txt:1-20

Baseline File Creation

The PR creates a new baseline file tools/ci/hrx_f16_accumulators.txt to track f16 accumulators. While this is a good step for tracking, it does not address the core issues of overflow or NaN in the accumulators. The file is correctly formatted and includes the necessary Apache-2.0 notice.

@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 cf2528f: CHANGES

The third class belongs in the gate, and the 250-line window is the right size. But ACC_MUL does not know vector.mma, the instruction most of these accumulators are actually updated by. The baseline therefore misses four real f16-accumulator bodies, including the one #2186 measured overflowing.

Measured on main's pin 2b7960bae (clean git archive of the corpus, this head's tools/ci/hrx_f16_narrowing.py and both baselines, --check --root):

ACC_MUL result
as in this PR rc 0, 7 f16 accumulator body(ies), none new
plus vector\.mma\b rc 1, 4 new, 0 stale of 7

The four, each an f16 vector.fragment<init> whose loop-carried value is the third operand of vector.mma … : vector<16xf16>, vector<16xf16>, vector<8xf16>:

qwen_moe/qwen3_moe/routed_down_quantized_f16_wmma.loom      init :261  vector.mma :428  (%half_acc00 …)   <- #2186 item 1, the routed down
qwen_moe/qwen3_moe/dense_linear_quantized_f16_wmma.loom     init :367  vector.mma :418
loom-libs/ops/flash_attention_decode_split_f32_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_decode_split_f32_f16_wmma.loom

The three flash-attention files the baseline does list are listed by coincidence. Their accumulator (%product_init, vector<8xf16>) is updated by vector.mma at flash_attention_f32_f16_wmma.loom:507 / decode:277. What ACC_MUL actually matched is an unrelated f32 vector.mulf 180 and 130 lines below (%scaled_current_sum = vector.mulf … %current_sum, %old_scale, the online-softmax rescale). The split-decode variants have no such line within 250, and that is the only reason they are missing. The rule is "an f16 init with any multiply nearby", not "this accumulator is updated by a multiply".

Ask:

  1. Add vector\.mma\b to ACC_MUL and re-baseline. That gives 11 bodies here.
  2. Better: follow the init through its scf.for iter-arg and require the update to name it (the third vector.mma operand, or a mulf/WMMA result yielded into it). That would make the flash-attention entries right for the right reason and stop an unrelated nearby mulf from adding entries.
  3. Add a self-test arm with a vector.mma-updated f16 accumulator that has no other multiply in its window.

The same blind spot is on main: tools/hrx_f16_narrowing_check.py (#2204), WMMA = template.apply<@…wmma…>|vector.(fma|contract|dot|outerproduct)|wmma.(mma|load|store), so its "class 3: 9" undercounts too. On the overlap: two classifiers for one class will drift (this is that drift). I would make the inventory tool import the ratchet's predicate, or the reverse, rather than keep two regexes.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at cf2528f — the window is measured rather than chosen, which is the difference between a heuristic and a guess.

Why a second ratchet is needed, stated as a construction argument

"The ratchet from #2114/#2220 matches fptrunc — a value converted to f16… it is blind to the third [shape] by construction" — an f16 accumulator overflows without any narrowing instruction appearing in the text. That is exactly the gap the reachability inventory (#2204) was built to make visible and the reason a pattern match cannot be the whole check; making it explicit here is what keeps someone from believing the first ratchet is exhaustive.

And the instance is real, not hypothetical: #2167 — a plain MUL_MAT_ID on bf16 failing at activation_scale=… — is that shape.

The 250-line window is the part I would keep in the body for a year

ACC_INIT    vector.fragment<init> … vector<Nxf16>
ACC_MUL     vector.mulf | template.apply<@…wmma…> | vector.fma|contract|dot|outerproduct | wmma.mma
ACC_WINDOW  250 lines past the init

"The window is measured, not chosen. At 120 lines, mul_mat_id_f16_f16_wmma_core's four vector<8xf16> accumulators…" — a distance heuristic that was tested at a smaller value and found to miss is a heuristic with evidence, and the next person who wonders whether 250 is arbitrary has an answer.

Two asks

  1. what bounds the window, and what falls outside it — def-use distance is unbounded in principle, so 250 lines is a policy. One sentence naming the false-negative case ("an accumulator initialised more than 250 lines before its first multiply is not reported") turns a silent limitation into a known one, and that is the difference between a ratchet and a false sense of coverage;
  2. the three new files are the evidence — say what they mean. "Including flash_attention_f32_f16_wmma.loom, flash_attention_decode_f32_f16_wmma.loom and their qwen_mo… siblings" — if those are newly discovered f16 accumulator sites, then this PR has found overflow risk in flash attention, which is a statement about long-context behaviour and deserves a sentence of its own. If they are only new to the baseline (previously reviewed and accepted), say that instead — the two readings have very different weight.

…s sibling (CI caught this)

Two failures in one CI step, both worth recording because both are the guard doing its job:

1. `no comment style known … tools/ci/hrx_f16_accumulators.txt` -- the tool refuses an unknown
   style rather than passing it (the behaviour its own comment praises). A `.txt` baseline is data,
   so it needs a line beside `hrx_f16_narrowing.txt` in the known-style list.
2. `missing copyright notice` -- my header had the copyright and SPDX lines plus an explanation and
   no licence body. `--fix` supplied it.

Then `--fix` collapsed the blank `#` separator lines in my explanation (the defect engine#2228
fixes), so the paragraphs were rewritten by hand afterwards; the notice is unchanged and
`copyright.py --check` is clean.

That is the second and third time this guard has caught this lane tonight -- a missing header on a
new .py, an unknown style on a new .txt, then a missing licence body -- and each time the right
response was to run the step's own tool locally, which now takes ten seconds.

@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 -- with one measured ask: the 250-line window is a heuristic that already runs within 15 % of its own limit, and a function scope would be exact

The framing is the valuable part and it is right: an f16 accumulator has no conversion instruction because the accumulator already is f16, so a fptrunc scan is blind to it by construction -- that is why #2167 and #2186 item 1 were found by hand and why neither could trip the existing gate. Splitting the baseline (hrx_f16_accumulators.txt, same --write symmetry) rather than widening the narrowing list is also the right call: they are different shapes and a single count would hide which one grew.

What I measured on the corpus at the registry pin 2b7960bae (124 .loom bodies), using this PR's own two regexes:

init -> multiply pairs found 14, across 6 bodies
distance min 31, median 146, p90 211, max 213
pairs more than 60 lines apart 10 of 14
pairs crossing a func.func boundary 0
ACC_INIT with no ACC_MUL inside 250 lines 17

Two things follow.

1. ACC_WINDOW = 250 is nearly binding in practice. The real distances are median 146 and max 213, so the margin between the longest true pair and the cut-off is 37 lines. An edit that moves a fill or adds a comment block between the init and the multiply silently drops that body out of the ratchet -- the failure mode is "the gate stops looking" with no diff to show it. Since 0 of 14 pairs cross a function boundary, the search can simply be scoped to the enclosing func.func instead of counted in lines: that is exact rather than heuristic, drops the constant entirely, and costs nothing measurable. Line windows are the same shape of instrument as a bare grep count, which is what #2219 was.

2. The 17 unmatched inits should be named, or the invariant asserted. I spot-checked the first one (flash_attention_f32_f16_wmma.loom:736): it is inside a check.case, a check.generate.fill value(1.0) : tensor<…xf16> fixture that never multiplies. If all 17 are of that kind -- f16 test fixtures rather than accumulators -- one comment saying so is enough, because otherwise a third of the init corpus is being excluded by a rule nobody wrote down, and the next person to add a kernel will not know whether their in-range-less init is exempt or broken.

Everything else: ACC_MUL covers vector.mulf, vector.fma|contract|dot|outerproduct, the wmma template.apply and the wmma.mma|load|store forms, which is the right set; the ACC_INIT capture of the lane count is the useful part for a future "how wide is the accumulation" check; and the baseline lives beside the narrowing one rather than inside it.

Not blocking -- the ratchet as written finds the #2167 and #2186 items -- but the window is the one line in this PR I would not want to inherit.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit a7051bc

@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 a7051bc: READY

Accumulators get their own ratchet rather than being folded into the narrowing one, and that is the right call for a reason today made concrete. ACC_INIT (declares an f16 fragment) and ACC_MUL (is updated by a multiply / WMMA / contract) are two patterns with a window between them, plus their own baseline file — so "this body accumulates in f16" is a conjunction of a declaration and an update, not a substring.

That separation matters because I got the neighbouring ratchet wrong today (#2220, and I closed my own #2225 over it): matching a name like truncf broadly caught a body that truncates f32 to f32 and narrows nothing. A conjunction of two specific facts is harder to over-match than any single marker, and keeping it in a separate file means the two enumerations cannot drift into each other.

It also lands with baseline lines rather than an assertion of zero, which is the same discipline the narrowing ratchet uses: the point is to see the set change, not to declare it empty.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a7051bc — fresh configure rc 0, and hrx_f16_narrowing.py --self-test now runs 15 arms (was 8), green.

The argument is structural and I want to record it because it is the reason a ratchet needed extending rather than duplicating: the existing check matches fptrunc — a value converted to f16 — which is blind by construction to the third way a value is lost, an f16 accumulator, because there is no conversion instruction to match: the accumulator already is f16 and the sum leaves 65,504 inside vector.mulf or a WMMA accumulate. #2167 (bf16 MUL_MAT_ID failing at activation_scale=10000, far below the f16 overflow point, because 256 terms accumulate past it) and #2186 item 1 are both that class, and neither could trip the old gate — they were found by hand. A ratchet that cannot see the failure mode it was built for is the same defect as #2220's vector blind spot, one category over.

The shape is consistent with what already works: a second baseline file (tools/ci/hrx_f16_accumulators.txt), the same --write-once ergonomics, and drift() returning new/stale pairs so a moved or fixed accumulator also fails — which is the property that keeps a baseline honest. Reusing drift() instead of writing a second comparator is the right call: two comparators for one ledger is how the two disagree.

Two asks:

  1. Say in the message what an accumulator failure means for a reader who is not in the kernel. The fptrunc failure says "this line converts to f16"; the accumulator case is "this line sums in f16, so N terms of magnitude M overflow at N·M > 65,504" — one clause naming the product (N × M) makes the finding actionable, and it is the sentence hrx: plain (non-fused) MUL_MAT_ID on bf16 weights returns NaN for activations above f16's range, and #2127/#309 does not cover it #2167's diagnosis actually used.
  2. An arm for the case the regex cannot see: an accumulator declared without an explicit vector<…xf16> in the init fragment (e.g. a type alias or a vector<Nxf16> spelled through a variable). If the corpus has none today, an arm asserting the shape is reported as unclassified would keep the next spelling from being silently invisible — the same unclassified_truncations() treatment ci: the f16 narrowing ratchet matches the vector form and drops an f32-only truncf, so it lists the 51 bodies #2220 got.

Scope of my check: fresh configure rc 0, the 15-arm self-test (green), and a read of the new regex, the second baseline and the reuse of drift(). No corpus: third_party/llama.cpp is uninitialised in this tree, so --check correctly 77-skips (verified previously on this tool).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a7051bc — the window's justification is now at the place it is used, which is where a future reader will need it.

The number is explained in the code, not only in the body

# unaffected, because their update sits ~250 lines below in a long Q4_K/Q6 loop body. 250 finds them.
ACC_WINDOW = 250
The window matters more than the matcher: the update of `mul_mat_id_f16_f16_wmma_core`'s four `vector<8xf16>…

That is the right home for it. A constant with a comment naming the exact case it was chosen for (mul_mat_id_f16_f16_wmma_core's accumulators, ~250 lines below their init in a long loop body) is a constant someone can re-derive, and the sentence "the window matters more than the matcher" is the insight of the whole PR: the ACC_MUL pattern list is easy, and what finds the real sites is following the fragment far enough.

One line still worth adding

The limitation, stated as a limitation: an accumulator initialised more than 250 lines before its first multiply is not reported. The two comments say why 250 is right for this corpus; they do not say what falls outside it. That sentence matters for the same reason as the UNKNOWN bucket in #2232 — a check whose blind spot is unnamed gets treated as coverage, and this ratchet's whole purpose is to be the thing that cannot miss a shape the fptrunc matcher is blind to by construction.

If there is a cheap way to make it self-reporting — "N ACC_INIT fragments had no ACC_MUL within the window; see --verbose" — that is strictly better than a comment, because the reader of a clean run learns how many starts went unmatched.

Why this and #2232 belong in the same hour

Both are the same finding from different ends: the instrument's definition was narrower than the artifact it reports on — a fptrunc pattern that cannot see accumulation, a runtime_status that cannot express a transition. The fix in both cases is to name the unrepresentable state and count it apart, rather than let the tool's silence read as health.

@bong-water-water-bong
bong-water-water-bong merged commit c95ee8a into main Nov 11, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/2219-accumulator-ratchet branch November 11, 2026 22:53
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Verified from the CI log that the ratchet actually runs — and that it passes at the engine's pin, not just at the commit I generated the baseline from.

Start  41: tool_selftest_hrx_f16_narrowing .......   Passed   0.04 sec
Start  42: hrx_f16_narrowing_check ...............   Passed   0.16 sec

Two things this settles:

  1. hrx_f16_narrowing_check is a real ctest row in the build job, not only the --self-test. It appears in CMakeLists.txt (5 places) and nowhere in ci.yml — which looks like a missing registration until you see that ci.yml runs ctest, and the corpus is present because the job initialises the third_party/llama.cpp submodule. A gate that is registered but never executed is the #1858 shape; this one executes.
  2. The baseline survives a pin difference. I generated tools/ci/hrx_f16_accumulators.txt from 2b7960bae (the fork branch the defects were measured on), while main pins 4107043c1 — so the gate is passing against a different commit than the one it was generated from. That means those two pins present the same 51 narrowing bodies and the same 7 accumulator bodies, which is worth knowing: it is evidence that the baseline is a property of the corpus rather than of my working tree.

The fragility, stated so it is not a surprise: the baseline is commit-specific while the pin moves, so a llama.cpp bump that fixes one of these sites will fail CI with a stale line and instruction to drop it, and one that adds a body will fail with NEW. That is the ratchet working as designed — it makes a pin bump acknowledge the change — but it does mean a bump PR (#2198-shaped) may need a two-line baseline edit alongside it, and that is the intended cost of a gate that cannot silently regress.

@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 -- the new fixture is the right idea, but it pins 200 while the corpus reaches 213, and the window's reach is 249 while the comment says 250

The added arms are exactly the coverage I asked for, and the add-only arm is the one that was missing: a fragment initialised to f16 and updated by vector.addf is not an f16 accumulator in the overflow sense, and excluding it is what keeps this list meaningful rather than "every f16 fragment".

Two numbers to correct, both cheap and both about the same constant.

1. lines[i:i + ACC_WINDOW] reaches 249, not 250. Python's slice end is exclusive, so with ACC_WINDOW = 250 a multiply at offset 250 is outside the window. The comment says the pairs "sit ~250 lines below ... 250 finds them", and the code cannot find an update at exactly 250. Either the constant becomes 251, or the comment says 249 -- the current text is a claim the code does not keep, which is the kind of sentence that later gets trusted.

2. The fixture sits at 200 lines, and the corpus's worst real case is 213. Measured at the pin this issue uses (2b7960bae, 124 .loom bodies), with this PR's own two regexes:

pairs found 14 across 6 bodies
distance min 31, median 146, p90 211, max 213

So " // pad\n" * 200 asserts a displacement below the largest real one, which means the arm passes while the hardest real case has 13 lines less headroom than the fixture pretends. Padding to 220 (or to an explicit ACC_WINDOW - 25) makes the fixture test the thing the constant is supposed to survive, instead of a case comfortably inside it.

Neither is a defect in the finding -- the ratchet still finds #2167 and #2186 item 1 -- and both are one-line changes in the file this PR already touches. The reason I am raising them at this level of nit is that this constant is now load-bearing twice: once for the real corpus (worst case 213, i.e. 15 % of margin at 250) and once for the fixture (201). A future editor who lowers it to "the value the test needs" would go to 205 and pass, while quietly dropping two real bodies out of the gate.

Suggested wording for the comment, since it currently claims more than it measures: updates sit up to 213 lines below the init at the pin (measured over 124 bodies); the window is 250 for headroom, and the slice covers 249.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant