diff --git a/CMakeLists.txt b/CMakeLists.txt index 610142905..ce51ba4de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -323,6 +323,13 @@ add_python_test(tests_referenced_self ${CMAKE_SOURCE_DIR}/tools/ci/tests_referen # Registered through add_python_test (engine#1864), so they run THROUGH the interpreter: my first version used the raw # `${Python3_EXECUTABLE}` form above the find_package and the repository's own ordering guard caught it by name -- # tools/mtp_head_ref.py is the one of the five without an exec bit, and it became BAD_COMMAND under ctest. +# engine#1962: the narrowing-body enumeration, as a ratchet. The corpus is a submodule, so the check +# skips (rc 77) where third_party/llama.cpp is not checked out; the baseline file is in the engine tree, +# so the recorded set is reviewable everywhere. +add_python_test(tool_selftest_hrx_f16_narrowing ${CMAKE_SOURCE_DIR}/tools/ci/hrx_f16_narrowing.py --self-test) +add_test(NAME hrx_f16_narrowing_check COMMAND ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/tools/ci/hrx_f16_narrowing.py --check --root ${CMAKE_SOURCE_DIR}) +set_tests_properties(hrx_f16_narrowing_check PROPERTIES SKIP_RETURN_CODE 77) add_python_test(tool_selftest_batched_repeat ${CMAKE_SOURCE_DIR}/tools/batched_repeat.py --selftest) add_python_test(tool_selftest_logprob_agree ${CMAKE_SOURCE_DIR}/tools/logprob_agree.py --selftest) add_python_test(tool_selftest_mtp_head_ref ${CMAKE_SOURCE_DIR}/tools/mtp_head_ref.py --selftest) diff --git a/tools/ci/hrx_f16_narrowing.py b/tools/ci/hrx_f16_narrowing.py new file mode 100755 index 000000000..4d4034a0f --- /dev/null +++ b/tools/ci/hrx_f16_narrowing.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# 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. + +"""hrx_f16_narrowing.py -- keep the engine#1962 enumeration from drifting while it is being finished. + +engine#1962 is "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 and the consumer scales back), and +the question the issue still holds open is: **which of the narrowing consumers are protected**. That +cannot be answered by reading the consumers -- the guard is not in them -- so the enumeration is only +worth anything if it cannot silently change underneath the person answering it. + +This is the ratchet: + + * every .loom body that narrows f32 to f16 (`f32 to f16` or `truncf`) is listed in + tools/ci/hrx_f16_narrowing.txt, and a body that appears without a line, or a line whose body is gone, + fails the check -- the two arms a ratchet needs (a list that can only grow cannot see a rename, + engine#1728's lesson); + * the producer-side guard markers (`scale_log2`, `prescale`) are counted per body and reported, but + **not** asserted as zero: moving the guard from the producer to the consumer is the issue's own + option 2, and when that happens the count must change and the baseline must be updated deliberately. + +The corpus is a submodule, so the check skips (rc 77) without third_party/llama.cpp checked out -- the +data file is in the engine tree, so the recorded set is visible in review even where the check cannot run. + +usage: hrx_f16_narrowing.py --check [--root .] rc 1 on drift, rc 77 without the corpus + hrx_f16_narrowing.py --self-test fixtures only, no corpus + hrx_f16_narrowing.py --write rewrite the baseline from the corpus +""" +import argparse +import pathlib +import sys + +CORPUS = pathlib.Path("third_party/llama.cpp/ggml/src/ggml-hrx/kernel-corpus/kernels") +BASELINE = pathlib.Path("tools/ci/hrx_f16_narrowing.txt") +NARROW = ("f32 to f16", "truncf") +GUARD = ("scale_log2", "prescale") + + +def baseline_paths(text): + return {line.strip() for line in text.splitlines() + if line.strip() and not line.lstrip().startswith("#")} + + +def narrowing_bodies(corpus): + """Every .loom under the corpus that narrows f32 to f16, as corpus-relative POSIX paths.""" + found = set() + for path in corpus.rglob("*.loom"): + body = path.read_text(encoding="utf-8", errors="replace") + if any(marker in body for marker in NARROW): + found.add(path.relative_to(corpus).as_posix()) + return found + + +def guarded(bodies, corpus): + """Bodies that also carry a producer-side guard marker -- reported, never asserted.""" + out = set() + for rel in bodies: + body = (corpus / rel).read_text(encoding="utf-8", errors="replace") + if any(marker in body for marker in GUARD): + out.add(rel) + return out + + +def drift(baseline, found): + return sorted(found - baseline), sorted(baseline - found) + + +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 + base_text = (root / BASELINE).read_text(encoding="utf-8") + baseline = baseline_paths(base_text) + found = narrowing_bodies(corpus) + new, stale = drift(baseline, found) + guard = guarded(found, corpus) + 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") + if new or stale: + print(f"hrx_f16_narrowing: FAIL ({len(new)} new, {len(stale)} stale of {len(baseline)})") + return 1 + print(f"hrx_f16_narrowing: {len(baseline)} narrowing body(ies), none new, none stale; " + f"{len(guard)} of them carry a guard marker" + + (f" ({', '.join(sorted(guard))})" if guard else " (the guard sits on the producer, engine#1962)")) + return 0 + + +def write(root): + corpus = root / CORPUS + if not corpus.is_dir(): + print(f"skip: no .loom corpus at {CORPUS}", file=sys.stderr) + return 77 + head = "".join(line for line in (root / BASELINE).read_text(encoding="utf-8").splitlines(True) + if line.lstrip().startswith("#")) + bodies = sorted(narrowing_bodies(corpus)) + (root / BASELINE).write_text(head + "\n".join(bodies) + "\n", encoding="utf-8") + print(f"hrx_f16_narrowing: wrote {len(bodies)} body(ies) to {BASELINE}") + return 0 + + +def self_test(): + import tempfile + failures = 0 + + def check_that(ok, what): + nonlocal failures + print(("ok " if ok else "FAIL ") + what) + if not ok: + failures += 1 + + with tempfile.TemporaryDirectory() as tmp: + root = pathlib.Path(tmp) + corpus = root / CORPUS + corpus.mkdir(parents=True) + (root / BASELINE).parent.mkdir(parents=True, exist_ok=True) + (corpus / "a.loom").write_text(" %x = scalar.fptrunc %y : f32 to f16\n") + (corpus / "b.loom").write_text(" %x = scalar.constant 1.0 : f32\n") + (root / BASELINE).write_text("# comment\n# another\na.loom\n") + check_that(not drift(baseline_paths((root / BASELINE).read_text()), narrowing_bodies(corpus))[0], + "an unchanged corpus has no drift") + (corpus / "c.loom").write_text(" %z = scalar.fptrunc %w : f32 to f16\n") + new, stale = drift(baseline_paths((root / BASELINE).read_text()), narrowing_bodies(corpus)) + check_that(new == ["c.loom"] and not stale, "a new narrowing body is drift, and only it") + (corpus / "c.loom").write_text(" %z = scalar.constant 0.0 : f32\n") + (corpus / "a.loom").rename(corpus / "renamed.loom") + new, stale = drift(baseline_paths((root / BASELINE).read_text()), narrowing_bodies(corpus)) + check_that(stale == ["a.loom"], + "a rename leaves a stale baseline line (the engine#1728 lesson)") + (corpus / "renamed.loom").write_text(" %x = scalar.fptrunc %y : f32 to f16 # scale_log2\n") + check_that(guarded({"renamed.loom"}, corpus) == {"renamed.loom"}, + "a guard marker is reported, not denied") + (corpus / "renamed.loom").write_text(" %x = scalar.fptrunc %y : f32 to f16\n") + check_that(guarded({"renamed.loom"}, corpus) == set(), + "and an unguarded body reports no guard") + # The comments in the baseline are not bodies. + (root / BASELINE).write_text("# f32 to f16 is the marker\nrenamed.loom\n") + check_that(baseline_paths((root / BASELINE).read_text()) == {"renamed.loom"}, + "a '#' line in the baseline is not a path (the trailing-comment trap)") + # A missing corpus is a skip, not a pass. + empty = root / "empty" + empty.mkdir() + check_that(check(empty) == 77, "a missing corpus returns 77, not 0") + + print(f"hrx_f16_narrowing self-test: {'ok' if not failures else f'{failures} failure(s)'}") + return 1 if failures else 0 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--check", action="store_true") + ap.add_argument("--write", action="store_true") + ap.add_argument("--self-test", action="store_true") + ap.add_argument("--root", default=".") + args = ap.parse_args() + root = pathlib.Path(args.root).resolve() + if args.self_test: + return self_test() + if args.write: + return write(root) + if args.check: + return check(root) + ap.print_help() + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/ci/hrx_f16_narrowing.txt b/tools/ci/hrx_f16_narrowing.txt new file mode 100644 index 000000000..b7e67e613 --- /dev/null +++ b/tools/ci/hrx_f16_narrowing.txt @@ -0,0 +1,48 @@ +# 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 diff --git a/tools/copyright.py b/tools/copyright.py index 840a92fa9..caf90f353 100644 --- a/tools/copyright.py +++ b/tools/copyright.py @@ -46,6 +46,7 @@ "unreferenced_tests.txt", # engine#2018: the third test-registration baseline, same shape and the same reason -- data, `.txt` has no style. "unreferenced_py_tests.txt", + "hrx_f16_narrowing.txt", "21serial-console", "90onboard-limits", "Dockerfile", "*.Dockerfile", # cycle 17: Puppy's switch that skips its first-run Quick Setup is an empty marker file whose only content # is this notice - Puppy tests for its existence, so a comment is free