Skip to content

ci: the engine#1962 narrowing enumeration is a ratchet, so it cannot drift while it is being finished - #2114

Merged
bong-water-water-bong merged 2 commits into
mainfrom
prs2-f16-narrowing
Nov 11, 2026
Merged

bong-water-water-bong merged 2 commits into
mainfrom
prs2-f16-narrowing

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Refs #1962 (release-blocker). Not a fix — the enumeration that keeps the fix's question answerable.

Why this exists

#1962's repairs land on the producers: a plan records a 2^-6 prescale and the consumer scales back. The issue's remaining question is
"which of the narrowing consumers are protected", and the enumeration already on the thread answers it the only way available today:

  • 25 .loom bodies narrow f32 to f16 (f32 to f16: 24, truncf: 1 more);
  • 5 files carry a guard (scale_log2/prescale) — and none of the 25 is one of them.

That answer is worth exactly as long as the list cannot change underneath whoever is acting on it. So the list becomes a ratchet.

What lands

tools/ci/hrx_f16_narrowing.txt — the 25 bodies at pin 7a1f2b81c, with the reason it is a ratchet rather than a bug list written at the top: a body
narrowing a bounded scale or a weight is perfectly correct, and this file does not claim otherwise.

tools/ci/hrx_f16_narrowing.py — --check, --write, --self-test:

arm what it catches
a body that narrows but is not in the baseline a new narrowing path, which must be written down with what feeds it
a baselined body that no longer narrows a rename or a deletion, silently dropped from the set (engine#1728's lesson: a list that can only grow cannot see a rename)
a # line counted as a path the trailing-comment trap that has bitten a baseline in this tree before
a missing corpus returns 77, not 0 — a check that cannot see the corpus must not report "clean"

The guard count is reported, never asserted: moving the guard from the producer to the consumer is the issue's own option 2, and when someone does that the
count changes and the baseline is updated deliberately.

Verified on the real corpus

$ tools/ci/hrx_f16_narrowing.py --check --root .
hrx_f16_narrowing: 25 narrowing body(ies), none new, none stale; 0 of them carry a guard marker (the guard sits on the producer, engine#1962)

$ echo '  %x = scalar.fptrunc %y : f32 to f16' > …/loom-libs/ops/_ab_probe.loom && … --check
hrx_f16_narrowing: NEW narrowing body loom-libs/ops/_ab_probe.loom -- it narrows an f32 value to f16 and the baseline does not name it
hrx_f16_narrowing: FAIL (1 new, 0 stale of 25)

$ (probe removed) -> clean again

--self-test: 7 arms, ok. Registered as ctest tool_selftest_hrx_f16_narrowing (runs everywhere) and hrx_f16_narrowing_check (skip 77 without the corpus).

What this does not do

It does not answer which producer feeds which consumer — the issue says plainly that this "cannot be answered by reading the 25", and a baseline cannot
resolve a producer→consumer edge. That needs either the planner to declare the edge or the guard to move into each consumer; this PR only guarantees that the set
somebody reasons about today is the same set tomorrow.

Also worth stating: the corpus is a submodule, so hrx_f16_narrowing_check skips in CI today. The CI row that actually runs is the self-test over fixtures;
the drift arm is live for any lane with third_party/llama.cpp checked out.

…drift while it is being finished

engine#1962 is a release-blocker (a dense MUL_MAT returns NaN or wrong values when an activation exceeds
the f16 range). Its repairs land on the *producers*: a plan records a 2^-6 prescale, the consumer scales
back. The issue's remaining question is "which of the narrowing consumers are protected", and the
enumeration published on the thread answers it the only way available today - by listing the 25 .loom
bodies that narrow f32 to f16 and finding that **none of them carries the guard** (the 5 files that do are
producers, and are not in the 25).

That answer is worth exactly as long as the list cannot change underneath the person acting on it.

tools/ci/hrx_f16_narrowing.txt: the 25 bodies, as a baseline at pin 7a1f2b81c, with the reason the list is
a ratchet rather than a bug list written at the top of the file (a body narrowing a bounded scale or a
weight is perfectly correct).

tools/ci/hrx_f16_narrowing.py: --check fails on a body that appears without a line *and* on a line whose
body is gone (a list that can only grow cannot see a rename, engine#1728); --write rewrites the baseline;
--self-test covers both arms on fixtures, plus "a '#' line in the baseline is not a path" and "a missing
corpus returns 77, not 0". --check skips 77 without the submodule - the corpus is third_party/llama.cpp -
so the CI row tests the checker and the arm that needs the corpus runs where it exists.

Registered as ctest tool_selftest_hrx_f16_narrowing (runs everywhere) and hrx_f16_narrowing_check (skip 77
without the corpus).

Verified on the real corpus: `--check` -> "25 narrowing body(ies), none new, none stale; 0 of them carry a
guard marker (the guard sits on the producer, engine#1962)"; adding a probe .loom with one fptrunc ->
`NEW narrowing body loom-libs/ops/_ab_probe.loom ... FAIL (1 new, 0 stale of 25)`; removing it -> clean.

Refs #1962.
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 04a443c)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1962 - Partially compliant

Compliant requirements:

  • The ratchet mechanism is implemented correctly with the script tools/ci/hrx_f16_narrowing.py
  • The baseline file tools/ci/hrx_f16_narrowing.txt is created with exactly 25 entries as required
  • The script handles missing corpus by returning exit code 77
  • The baseline is based on the correct pin (7a1f2b81c) as specified in the ticket

Non-compliant requirements:

  • The PR does not include the actual fix for the NaN/wrong values issue, only the enumeration mechanism

Requires further human verification:

  • Verification that the 25 listed bodies are indeed the correct set from pin 7a1f2b81c
  • Confirmation that the ratchet mechanism works correctly in CI environments
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Corpus Dependency

The script relies on the third_party/llama.cpp corpus being checked out, and returns exit code 77 when it's not available. This could mask issues in CI environments where the corpus is not present, potentially leading to unnoticed drift in the enumeration.

def check(root):
    corpus = root / CORPUS
    if not corpus.is_dir() or not any(corpus.rglob("*.loom")):
        print(f"skip: no .loom corpus at {CORPUS} (check out third_party/llama.cpp)", file=sys.stderr)
        return 77
Baseline Completeness

The baseline file lists 25 narrowing bodies, but the PR description indicates that this enumeration is based on a specific pin (7a1f2b81c) and that the actual fix implementation needs to be done in a separate PR. It's important to ensure that this baseline is complete and accurate for the current state of the codebase, as the PR description mentions that the enumeration is a ratchet to prevent drift while the fix is being implemented.

# 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.
#
# Targets whose add_test() names them and which CI never builds (engine#1595).
#
# engine#1962: the .loom bodies that narrow an f32 value to f16, at llama.cpp pin 7a1f2b81c.
# Paths are relative to third_party/llama.cpp/ggml/src/ggml-hrx/kernel-corpus/kernels. This is a
# RATCHET, not a list of bugs: a body here may narrow a bounded value (a scale, a weight) and be
# perfectly correct. What the list buys is that a NEW narrowing body cannot appear without someone
# writing it down and saying what feeds it -- and that is the open question on engine#1962: which of
# these are protected by a producer-side prescale cannot be answered by reading them.
loom-libs/motifs/mul_mat_id_f16_f16_wmma_core.loom
loom-libs/motifs/mul_mat_id_q4k_f16_wmma_projection.loom
loom-libs/motifs/mul_mat_id_q5k_f16_wmma_projection.loom
loom-libs/motifs/q4_k_f16.loom
loom-libs/motifs/quantize_q8_1_x4.loom
loom-libs/motifs/unary_f32_apply.loom
loom-libs/ops/flash_attention_decode_split_f32_f16_wmma.loom
loom-libs/ops/flash_attention_f32_f16_wmma.loom
loom-libs/ops/gated_delta_net_f32_wmma.loom
loom-libs/ops/llm_attention_v_matmul_set_rows_decode_f32_f32.loom
loom-libs/ops/mul_mat_swiglu_symmetric_i4_wmma.loom
loom-libs/ops/mul_mat_symmetric_i8_wmma.loom
loom-libs/ops/set_rows.loom
loom-libs/ops/small_rows_f32.loom
qwen_moe/ggml/quantize_q8_1_x4.loom
qwen_moe/qwen3_moe/attention_prepare_quantized.loom
qwen_moe/qwen3_moe/dense_linear_quantized_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_decode_f32_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_decode_q128_f32_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_decode_split_f32_f16_wmma.loom
qwen_moe/qwen3_moe/flash_attention_decode_split_next_q8_test.loom
qwen_moe/qwen3_moe/flash_attention_f32_f16_wmma.loom
qwen_moe/qwen3_moe/routed_down_weighted_reduce_next_rmsnorm_q8_1_x4.loom
qwen_moe/qwen3_moe/routed_gate_up_swiglu_q4k.loom
qwen_moe/qwen3_moe/routed_linear_q4k_f16_wmma.loom
✅ Resolved findings

tools/ci/hrx_f16_narrowing.py:81-85

Ratchet Implementation

The ratchet implementation in hrx_f16_narrowing.py correctly tracks narrowing bodies to prevent drift in the enumeration. However, it relies on the corpus being checked out, and returns exit code 77 when the corpus is missing, which may mask issues in CI environments where the corpus is not available.

tools/ci/hrx_f16_narrowing.txt:1-34

Baseline Completeness

The baseline file hrx_f16_narrowing.txt lists 25 narrowing bodies, but the PR description mentions that the enumeration is based on a specific pin (7a1f2b81c) and that the actual fix implementation needs to be done in a separate PR. It's important to ensure that this baseline is complete and accurate for the current state of the codebase.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at d38ce8d — and the ratchet has the two arms that make a ratchet rather than a list.

+# engine#1962: the narrowing-body enumeration, as a ratchet. The corpus is a submodule, so the check
+# cannot be answered by reading the consumers -- the guard is not in them -- so the enumeration is only …
+    fails the check -- the two arms a ratchet needs (a list that can only grow cannot see a rename,
+    new, stale = drift(baseline, found)

Why "not a fix" is the correct scope for this PR

#1962's repairs land on the producers — a plan records a 2^-6 prescale and the consumer scales back — and a producer-side change is exactly the kind that can be silently undone by the next producer added. A baseline that records which narrowing bodies exist is the difference between "the fix landed" and "the fix stays landed", and saying "Not a fix — the enumeration that keeps the fix's question answerable" in the first line stops a reviewer grading it as one.

The two arms, and why the second is the one that matters

  • new — a narrowing body not in the baseline fails, so a new producer cannot quietly bypass the prescale;
  • stale — an entry in the baseline that is no longer found fails too, so a rename is caught. A grow-only list sees additions and is blind to renames, which is precisely how a ratchet rots: the entry stays, pointing at a symbol that no longer exists, and every future run reports green.

drift(baseline, found) returning both is the correct shape, and it is the same pair #1835 needed for the unreferenced-test allow-list (idle alongside gone).

Two details worth stating in the body

  1. how the baseline is regenerated, in one command — a ratchet whose baseline cannot be updated deliberately becomes an obstacle someone disables;
  2. whether the enumeration is complete by construction or by inspection. "The corpus is a submodule, so the check cannot be answered by reading the consumers" is honest, but it means the corpus is the definition: say which revision of the submodule the baseline was taken at, or the ratchet's meaning drifts with the submodule's own history (npu: unblock #1931 — main merged in, the fork-join tested for bit-identity and a throwing unit, honest timing labels (Refs #1783) #2035's lesson: the number describes what it was measured against).

Nothing else — a ratchet with both arms, and a scope statement that prevents it being read as the fix.

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

The build stops on the fifth instance of one root in a day, so the pattern — not this file — is now the finding:

no comment style known (add one to LINE_STYLES/BLOCK_STYLES or EXEMPT): tools/ci/hrx_f16_narrowing.txt
# PR file that tripped it
1 npu-kernels#54 npu/families/*.elf
2 #1920 a source licence header (different cause)
3 #2021 tests/golden/q4nx-container/*.q4nx
4 #2095 tools/ci/ctest_skips.txt
5 this PR tools/ci/hrx_f16_narrowing.txt

On #2021 I said a fifth instance should be the point where "no comment style known" becomes a printed skip rather than a failure, because EXEMPT is enumerated by extension and cannot keep up with the file kinds the tree is adding. This is the fifth, inside one day, and every one of them cost a full CI round trip to learn a filename. The prediction is now a measurement.

One EXEMPT line fixes this PR (tools/ci/hrx_f16_narrowing.txt, a ratchet baseline). But the policy question is worth settling in the same merge, because there will be a sixth: either

  • the checker prints and continues for a file kind it has no style for, and keeps failing only for a file it could have noticed (which is the case it actually exists for), or
  • EXEMPT grows by glob for whole directories of data (tools/ci/*.txt), so a new baseline file needs no edit at all.

Either is defensible; what is not is discovering the same filename cost six times. This PR is a release-blocker fix (#1962), so it should not be waiting on a copyright exemption round trip either way.

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

Reviewed head d38ce8d (refs #1962). This is an independent second review. The tool is sound. The only blocker is the red build job, and below is the exact fix, tested on this head.

The tool: every claim reproduces

  • --self-test: 7 arms ok, rc 0.
  • --check against the corpus exported from llama.cpp at the engine pin 7a1f2b81c: "25 narrowing body(ies), none new, none stale; 0 of them carry a guard marker", rc 0. It is also clean on a lane checkout at a different fork commit.
  • Independent count: I listed .loom files containing f32 to f16 or truncf with grep. That gives 25 files, the same set as the baseline. The guard grep (scale_log2|prescale) finds 5 files.
  • New-body arm: a probe .loom with one fptrunc gives "NEW … _cl_probe.loom", rc 1.
  • Rename arm: set_rows.loom → set_rows2.loom gives 1 new + 1 stale, rc 1. After restoring it, the check is clean again.
  • Missing-corpus arm: --root /nonexistent returns 77.
  • git merge-tree onto main: no conflicts.

What is red: the build job's failed step is "Copyright notice on every file". Locally, tools/copyright.py --check gives "no comment style known … tools/ci/hrx_f16_narrowing.txt", rc 1.

The fix, tested. It has two parts, and both are needed:

  1. Add "hrx_f16_narrowing.txt", to the # entry in LINE_STYLES, next to add_test_gaps.txt / unreferenced_*.txt (same kind of file: #-comment ratchet data). With only this line, the check moves on to "missing copyright notice". The two-line Copyright/SPDX header is not the full notice the # style requires.
  2. Replace the file's first two lines with the full Apache header that tools/ci/add_test_gaps.txt carries (its lines 1–15). Do not run copyright.py --fix on top of the existing two lines: it prepends a second notice, and the check then fails with "copyright notice duplicated inside the header window".

With both parts: copyright.py --check rc 0, hrx_f16_narrowing.py --check still 25/none new/none stale rc 0 (the parser skips the longer header), and --self-test rc 0. The diff is +12 lines in the .txt and +1 in tools/copyright.py.

On the policy question in the earlier verdict (make "no comment style known" a skip, or exempt by glob): I'd keep that out of this release-blocker PR. A # style entry keeps the notice enforced on this file, which EXEMPT would not.

… file

Answers the CHANGES at d38ce8d from two reviewers, both on the same point: the
`build` job's "Copyright notice on every file" step fails on
tools/ci/hrx_f16_narrowing.txt, because `.txt` has no comment style the checker
knows.

Both parts are needed and both are here:

1. the baseline carries the same full Apache header as tools/ci/add_test_gaps.txt
   (the other '#'-comment data files) instead of a two-line stub;
2. `hrx_f16_narrowing.txt` joins the '#' entry in LINE_STYLES next to
   `add_test_gaps.txt` / `unreferenced_*.txt`, which is the same kind of file: data
   whose header IS the notice it would otherwise be asked for.

Verified: tools/copyright.py --check rc 0; the checker still reports
"25 narrowing body(ies), none new, none stale" against the corpus at the engine pin
(checked with the corpus symlinked in, then the link removed); --self-test 7 arms ok.

Note on why I did not see this before pushing: I ran `copyright.py --check` while the
file was still untracked, and the check does not see untracked files. That is the hole
I reviewed earlier tonight in the other direction (copyright_check_untracked_test.py);
this is the same hole from the inside.

Refs #1962.

@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 04a443c

Both CHANGES answered, with exactly the fix the second reviewer specified — and the reason I could not see it before pushing is the hole I reviewed earlier tonight, from the inside.

The two parts, applied as described:

  1. the baseline carries the full Apache header that tools/ci/add_test_gaps.txt carries (16 lines), replacing my two-line stub;
  2. hrx_f16_narrowing.txt joins the # entry in LINE_STYLES, next to add_test_gaps.txt / unreferenced_*.txt — the same kind of file: data whose header is the notice it would
    otherwise be asked for.

Verified after the change:

tools/copyright.py --check                                   rc 0
hrx_f16_narrowing.py --check (corpus symlinked to the pin)   25 narrowing body(ies), none new, none stale
hrx_f16_narrowing.py --self-test                             7 arms ok

Why I missed it, and it is worth stating rather than fixing quietly: I ran python3 tools/copyright.py --check while
tools/ci/hrx_f16_narrowing.txt was still untracked, and the check does not see untracked files. So my verification was green on a tree that did not yet contain the
thing that fails. That is the same hole as copyright_check_untracked_test.py — which I reviewed earlier tonight, in the direction of "an untracked file escapes the gate" —
and I then walked into it as the author. The durable fix is in that test's scope, not this PR's: --check either sees untracked files or says it did not.

On the policy question the first reviewer raised (no comment style known → skip, or exempt by glob): I agree with the second reviewer's position that it belongs outside
this PR, and I would add the reason: a checker that skips an unknown style silently is how #2027's /tmp literals got where they are. Naming the file in LINE_STYLES is one
deliberate line per file; a glob is a hole that grows.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 04a443c

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 04a443c

Correction to my own explanation in the review above: the checker does see untracked files — it deliberately skips an untracked file whose style it does not know. My "the check does not see untracked files" was wrong, and the real reason is more interesting.

tools/copyright.py:266-292 already lists tracked | untracked:

tracked   = set(filter(None, listing()))
untracked = set(filter(None, listing("--others", "--exclude-standard")))
files = sorted(tracked | untracked)
...
if block is None:
    # … an undtracked unknown-style file is skipped and a tracked one is still an error.
    if path not in untracked:
        unknown.append(path)
    continue

So my baseline was seen and skipped on purpose, because it was untracked and .txt had no known style. The skip is right and the comment says why (engine#1564: .hrx123/logs/*.jsonl reddened a developer clone while main was clean — i.e. an untracked probe dump must not fail a licence gate).

What that means for my own process, accurately this time: the only way to catch this before pushing is git add first and re-run — because staging flips the file from "untracked unknown-style, tolerated" to "tracked unknown-style, error". I ran the check in the one state where the gate is designed to stay quiet, and read quiet as green.

And it means the durable fix I proposed is not needed: "make --check see untracked files" is already true. What would help is a line in the tool's output when it skips such a file — skipped 1 untracked file with an unknown style (git add it to check) — because a silent skip that is documented in the source is still invisible to the lane reading the last line of a green run.

@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 04a443c: READY — withdraws my CHANGES

You solved it better than I asked, and it is the counter-argument to my own suggestion. I offered five times today to add an EXEMPT line for each data file that tripped the checker, and to make "no comment style known" a printed skip once the count got embarrassing. This does neither: it teaches copyright.py that hrx_f16_narrowing.txt takes # comments, and then the baseline carries the full notice —

# Copyright 2026 bong-water-water-bong
# SPDX-License-Identifier: Apache-2.0

That is strictly better. An exemption says "this file is invisible to the check"; a line style says "this file can carry the notice" and then requires it. The EXEMPT list stops growing, and a .txt baseline is no longer a second-class citizen in the tree.

So my policy proposal was aiming one step too low. The right generalisation is not "skip unknown kinds" but "teach the kinds the tree actually uses" — # for a .txt baseline, # for a shell-escaping record, and so on — reserving EXEMPT for genuinely comment-less binaries (.elf, .q4nx, .gguf) where no notice is possible. With this change, the four remaining instances from today's table would each have had a two-line fix rather than an exemption, and the fifth would not have been a surprise.

CI green, and this unblocks #1962 — the last blocker-fix PR that was still red.

@bong-water-water-bong
bong-water-water-bong merged commit 544771f into main Nov 11, 2026
10 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the prs2-f16-narrowing branch November 11, 2026 07:38
bong-water-water-bong pushed a commit that referenced this pull request Nov 11, 2026
…s the ratchet (engine#2219)

`scalar.truncf %v : f32` and `vector.truncf %w : vector<4xf32>` in
unary_f32_apply.loom round within f32; the file never mentions f16. The bare
`truncf` alternative listed it anyway, a false positive carried over from
#2114. Every truncf that narrows names its f16 type, which the scalar and
vector patterns already match. Dropping the alternative leaves 51 bodies,
none new and none stale, at llama.cpp 2b7960bae and at 0494dfa28. The
self-test gains an f32-only truncf control (9 arms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…2-only truncf, so it lists the 51 bodies (#2220)

* ci: the f16 narrowing ratchet matches the vector form, so it lists all 52 bodies (engine#2219)

The ratchet matched "f32 to f16" or "truncf" as substrings. That misses
`vector.fptrunc %x : vector<4xf32> to vector<4xf16>`, which is 132 of the
233 narrowing lines at llama.cpp 2b7960bae. The baseline listed 25 of the 52
bodies that narrow. The match is now a regex that also takes
vector<Nxf32> to vector<Nxf16> (not bf16). The baseline is regenerated at
2b7960bae, with 27 lines added and none removed. The self-test gains a
vector-form arm and a bf16 control. The header's stray engine#1595 line and
its stale pin are corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: a bare truncf is not a narrowing to f16, so unary_f32_apply leaves the ratchet (engine#2219)

`scalar.truncf %v : f32` and `vector.truncf %w : vector<4xf32>` in
unary_f32_apply.loom round within f32; the file never mentions f16. The bare
`truncf` alternative listed it anyway, a false positive carried over from
#2114. Every truncf that narrows names its f16 type, which the scalar and
vector patterns already match. Dropping the alternative leaves 51 bodies,
none new and none stale, at llama.cpp 2b7960bae and at 0494dfa28. The
self-test gains an f32-only truncf control (9 arms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: an fptrunc the narrowing ratchet cannot classify fails the check (engine#2219)

The review of 8614166 asked for a matcher that covers every spelling by
construction rather than by type pattern. Matching the bare op would count
f32 -> bf16 truncations, which cannot reach inf. Instead, every `fptrunc`
line must now match the f16 patterns or a known non-f16 target (bf16).
Anything else is reported as UNCLASSIFIED and fails the check, so the next
unknown spelling cannot pass the way the vector form did.

At 2b7960bae and 0494dfa28: 51 bodies, none new, none stale, 0
unclassified. Mutation (vector pattern removed): FAIL with 132
unclassified, the corpus's vector-form count. Self-test: 11 arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…e no truncation scan can see (engine#2186, #2167) (#2231)

* ci: the ratchet also covers f16 accumulators, the half-precision shape 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

* copyright: the accumulator baseline is data with a '#' style, like its 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.

---------

Co-authored-by: agent <agent@local>
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