Skip to content

hrx_f16_narrowing: match the type pair, so the vector form is visible and an f32 truncation stops being listed (#2219) - #2222

Closed
bong-water-water-bong wants to merge 3 commits into
mainfrom
pkg/2219-f16-ratchet
Closed

bong-water-water-bong wants to merge 3 commits into
mainfrom
pkg/2219-f16-ratchet

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Fixes #2219 — and the measurement corrects the issue's own count, which is why the baseline came from --write rather than from arithmetic.

The gap, measured at the pin

233 fptrunc lines      = 101 scalar + 132 vector      (exactly the issue's numbers)
bodies, old matcher    = 25
bodies, type-pair      = 51       ← 27 more: the gap #2219 describes

The matcher was ("f32 to f16", "truncf") — substrings. The corpus writes:

%x = scalar.fptrunc %y : f32 to f16                       matched
%h = vector.fptrunc %v : vector<4xf32> to vector<4xf16>   not matched

And the 52nd body was never narrowing to f16

--write produced 52, not 51, and chasing the difference found the interesting half of this PR. The truncf alternative was matching exactly one body the type pair does not:

loom-libs/motifs/unary_f32_apply.loom
  %a = scalar.truncf %value : f32
  %b = vector.truncf %values : vector<4xf32>

A truncation that stays inside f32 and names no f16 type at all — listed as a narrowing to f16 for as long as this ratchet has existed. So the real count is 25 → 51, not 25 → 52: the issue's 52 is the old matcher's output, and one line of it was a false member.

Checked before removing it: no body in this corpus spells an f16 narrowing as a truncf that the two type patterns miss. Dropping the alternative therefore removes exactly that line and nothing else, and the rule becomes "the type pair decides, not the opcode spelling".

Verification

run result
--check at the pin with the new baseline 51 narrowing body(ies), none new, none stale; 10 of them carry a guard marker
old matcher against the same baseline (negative control) FAIL (1 new, 27 stale of 51) — the 27 from the issue, plus the false positive as the 1 new
--self-test ok, 3 new arms
tools/ci/tests_referenced_check.py 83/83, 34 tool self-tests
tools/copyright.py --check clean

The three new arms pin the distinction the tool now draws: the vector form is a narrowing, a truncf naming no f16 type is not, and the two are decided by the type pair rather than by which opcode is spelled.

Deliberately unchanged

The two bodies #2219 flags as unprescaled (motifs/f32_f16.loom, qwen3_moe/attention_postprocess_f32_f16.loom) are both in the new baseline, so #2204's classification and this ratchet now agree on the set they are talking about.

bong-water-water-bong added 3 commits November 11, 2026 10:02
…s "at their pinned commit"

engine#2129 is already fixed on main by tools/ci/submodule_pin_check.py -- another lane's, registered in
cmake/tests-submodule-pin.cmake as a ctest row -- and it reads `git submodule status`, which reports four states. This adds
the fifth, which that command cannot report, and the wiring that runs the check before a build instead of only in CI.

**The missing state: at the pin, not clean.** `git submodule status` prints ' ' for "the checkout IS the pinned commit"
whatever the submodule's own work tree holds, and the build compiles that work tree. Measured on the box that motivated
#2129: on top of the checkouts being off-pin, `third_party/llama.cpp` had 301 uncommitted paths, `xdna-driver` 32,
`tokenizers` 20, and four others 4 each. Every one of those would read as "at their pinned commit" to the existing check.

Reported always, with its count, and a failure only under the new `--require-clean`: a deliberate local patch in a
submodule is a real thing a box does, and moving the pin is not always the caller's to do, so the default is to say it
rather than to stop. Five self-test arms, including that a dirty '+' row is not double-counted (the commit is the failure
there).

**The wiring: nothing ran it before a build.** A ctest row runs in CI; the drift was measured on the box, where the release
and the weekly build actually run. `scripts/weekly.sh`'s preflight now runs the check for the `bumps` and `build` stages
through `preflight_submodules()` (a function of its own, testable against stub checkers like `preflight_runner_units`):
exit 1 **blocks** the run -- the build would not be of the pinned sources -- and exit 2 (no submodule initialised, i.e. a
fresh clone) only **warns**, saying that the check inspected nothing rather than implying a pass.

    tools/ci/submodule_pin_check.py --self-test        19 arms, 5 of them new
    tests/weekly_submodule_preflight_test.sh           6/6, the function cut out of weekly.sh, stub checkers
    tools/copyright.py --check                         clean
    tools/ci/tests_referenced_check.py                 83/83 referenced

`--require-clean` is deliberately not passed in the preflight: a dirty submodule is reported there and does not stop a
build, while a wrong commit does.

This supersedes #2177, which grew a parallel `scripts/check-submodules.sh` before main's tool landed. Two checks for one
property is the failure mode the standing rules name, so that script is withdrawn rather than maintained beside it.
…it was a no-op that reported success

Found by checking my own wiring against where the variables are set rather than against whether the tests pass. `SRC=$W/src`
is created by `checkout`, which the `bumps` and `build` stages call, and `preflight()` runs before any stage -- so
`[ -f "$SRC/tools/ci/submodule_pin_check.py" ]` was false on every real run, the function returned 0, and the gate inspected
nothing while reporting success. A preflight row that cannot see the tree it is meant to inspect is worse than no row: it
reads as verified.

The gate now runs at the top of `build()`, immediately after `checkout` has updated `$SRC`'s submodules and before the first
compile -- the tree that is about to be compiled, stopped before the compile rather than after it. `build()` returning
non-zero fails the stage.

And the missing-checker path now says so. An older ref does not carry the tool, so that build's submodule drift is not
checked; it prints a WARNING naming the ref's reason instead of returning 0 silently. That was the same defect one level up:
the function's first version treated "no file to run" as "fine", which is the reporting shape this whole check exists to
remove.

    tests/weekly_submodule_preflight_test.sh   6/6, including that a missing checker warns rather than passing quietly
    tools/ci/submodule_pin_check.py --self-test  19 arms
    tools/copyright.py --check                  clean
… and an f32 truncation stops being listed (engine#2219)

The ratchet matched the substrings `f32 to f16` or `truncf`, which sees the scalar narrowing and misses the vector one:

    %x = scalar.fptrunc %y : f32 to f16                      matched
    %h = vector.fptrunc %v : vector<4xf32> to vector<4xf16>   not matched

Measured on the corpus at the pin 2b7960bae (git archive of the submodule commit, then --root):

    233 fptrunc lines      = 101 scalar + 132 vector
    bodies, old matcher    = 25
    bodies, type-pair      = 51      (27 more, the gap the issue describes)

The `truncf` alternative is dropped, and that is a correction rather than a tidy-up: it was the only thing matching
loom-libs/motifs/unary_f32_apply.loom, whose narrowing is spelled

    %a = scalar.truncf %value : f32
    %b = vector.truncf %values : vector<4xf32>

-- a truncation that stays inside f32 and names no f16 type at all, listed as a narrowing to f16 for as long as this
ratchet has existed. No body in this corpus spells an f16 narrowing as a `truncf` the two type patterns miss (checked), so
dropping the alternative removes exactly that line and nothing else. That is why the count goes 25 -> 51 and not 25 -> 52:
the issue's 52 is the old matcher's output, one of which was never narrowing to f16.

Both arms are intact and the baseline was written by --write against the pin, not by hand: `--check` reports
"51 narrowing body(ies), none new, none stale; 10 of them carry a guard marker". Against the old matcher the same baseline
fails with "FAIL (1 new, 27 stale of 51)" -- the 27 the issue found, and the false positive as the 1 new.

Three self-test arms pin the distinction: the vector form is a narrowing, a truncf naming no f16 type is not, and the two
are decided by the type pair rather than by which opcode is spelled.

The header comment in tools/ci/hrx_f16_narrowing.txt is unchanged; the entries are regenerated.

@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 (canonical fix for #2219)

Head pkg/2219-f16-ratchet, 51 bodies. Verified against the corpus at the engine pin 2b7960bae (124 .loom blobs fetched from that sha):

  • 51 narrowing bodies with this matcher, --check clean (none new, none stale in my equivalent run), 27 added over the old baseline of 25, and the stale loom-libs/motifs/unary_f32_apply.loom correctly dropped.
  • The negative test is the reason this is the canonical one: dropping the bare truncf marker is not cosmetic. The pinned corpus has exactly one body with a truncf that is not fptrunc -- unary_f32_apply.loom, vector.truncf %values : vector<4xf32> -- which truncates within f32 and names no f16 type. An alternation that keeps truncf (#2220) lists it as a narrowing, which is a false positive in a ratchet whose whole job is to be a list of true narrowings.
  • vector<\d+xf32> to vector<\d+xf16> is the right shape: it matches the 132 vector lines and not vector<4xbf16>.

One remark, not a blocker: this PR also carries cmake/tests-submodule-pin.cmake, scripts/weekly.sh, tests/weekly_submodule_preflight_test.sh and tools/ci/submodule_pin_check.py, which are not part of #2219. If that preflight work has its own issue, splitting it keeps the ratchet change reviewable on its own; if it is deliberate (the corpus comes from a pinned submodule, so a pin preflight is the natural companion), ignore this.

I am closing my own duplicate (#2223) in favour of this one.

@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 c6ab8d8

This is the correct version of the fix, and it caught an error in my own baseline that I made in #2114 and repeated in my review of #2220.

The truncf alternative had to go, and the reason is checkable in one file:

$ grep -n truncf …/loom-libs/motifs/unary_f32_apply.loom
74:  %truncated = scalar.truncf %value : f32
261: %truncated = vector.truncf %values : vector<4xf32>
$ grep -c f16 …/loom-libs/motifs/unary_f32_apply.loom
0

Zero mentions of f16. So truncf there is a truncation within f32 — it was never a narrowing to f16, and my #2114 regex matched it as one. That is my defect: the ratchet has carried a false
positive since it landed, in the very list whose purpose is "which bodies narrow an f32 value to f16".

Ran it: --self-test ok (including the #-line and missing-corpus arms), and --check on the corpus gives

51 narrowing body(ies), none new, none stale; 10 of them carry a guard marker

with unary_f32_apply absent from the baseline (grep 0) — so the 51 is the set minus exactly that one body, which is what "25 -> 51, with the 52nd body being the one that was never
narrowing to f16"
says.

And I owe a correction to #2220: I reviewed it as "52; the comment's 51 is stale by one". It is the other way round — 52 is the over-count, 51 is right, and the sentence in #2220's comment
that I called stale was the correct number. #2222 does what #2220 should have. #2222 supersedes #2223 as well (the same .txt, and #2223 keeps the truncf alternative, so its 51 comes
out of a different matcher and would re-acquire the false positive the moment a new truncf-only file appears).

What I would keep from the pair: #2222's comment is the better record, because it says why dropping truncf is safe ("No body in this corpus spells an f16 narrowing as a truncf that
these two patterns miss (checked)"
) rather than just removing it.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #2220, per the lane cap (at most 2 open PRs) and the duplicate rule. #2220 has the same fix, opened 19 minutes before this one; I claimed #2219 on the board without checking that a claim had just landed, which is the same mistake as #2177 earlier today and it is mine to stop repeating.

Two things carried over so nothing is lost:

  1. The one finding this PR had that ci: the f16 narrowing ratchet matches the vector form and drops an f32-only truncf, so it lists the 51 bodies #2220's baseline does not: the bare truncf alternative lists loom-libs/motifs/unary_f32_apply.loom, whose narrowing is scalar.truncf %value : f32 / vector.truncf %values : vector<4xf32> — no f16 type at all. That is why this PR's count came out 51 where ci: the f16 narrowing ratchet matches the vector form and drops an f32-only truncf, so it lists the 51 bodies #2220 says 52, and it is posted on ci: the f16 narrowing ratchet matches the vector form and drops an f32-only truncf, so it lists the 51 bodies #2220 with the negative control (old matcher vs new baseline → FAIL (1 new, 27 stale of 51), the 27 the issue found plus that false positive).

  2. A branch defect in this PR itself, worth recording: it carried the submodule_pin: a submodule at its pin with a dirty work tree passes as 'at their pinned commit' (#2129) #2202 stack as well (cmake/tests-submodule-pin.cmake, scripts/weekly.sh, tools/ci/submodule_pin_check.py, tests/weekly_submodule_preflight_test.sh) because the commit was made in the worktree whose HEAD already had those commits. git commit -- <paths> scoped the change, not the branch ancestry. The submodule_pin: a submodule at its pin with a dirty work tree passes as 'at their pinned commit' (#2129) #2202 work is in its own PR and needs nothing from here.

Branch pkg/2219-f16-ratchet is kept, so this can be reopened if #2220 stalls; the fix is six lines of matcher plus a regenerated baseline.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant