From 38160e9240c024945e668229cda4d0f655d649cb Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Tue, 10 Nov 2026 01:43:08 -0400 Subject: [PATCH 1/4] npu: the batch-check oracle compares every row, bitwise, instead of one argmax at the last position NPU_BATCH_CHECK=1 is the debug oracle that guards a batched verify pass: run the tokens sequentially, reset(), run them batched, compare. It compared ONE argmax at ONE position -- the last row's -- which answers "did the final argmax survive batching" and not the question a speculative decoder asks. For engine#1781 the difference is load-bearing in three ways: * argmax vs logits -- the accept rule consumes the target's probability AT the drafted token, and two passes can agree on an argmax while disagreeing on the distribution around it; * the last row vs all rows -- rows 0..k-2 are exactly the drafted positions a verifier has to accept or reject, and they were never compared; * k up to 7 is the shape that matters (M=7 <= 16 = one block on the M=16 set), so "one row of seven" was the one row least worth checking. The comparison is now a pure function, batch_check_compare.h, so it can be pinned by a host test with no model, no device and no runtime: compare_rows_bitwise() takes both k x V buffers and returns how many rows differ, filling the first (row, index, bits) it found. Bitwise, not tolerance: the gate is "the batched pass is the same arithmetic", so one ulp is a divergence to report. NaN is handled explicitly on both sides, because a bit comparison would call two NaNs equal and would miss a NaN on one side against a finite number on the other. tests/batch_check_compare_test.cpp, 11 arms, host-only: identical rows -> no divergence; a difference in the LAST row is found (the old oracle's only case, so it must keep working); a difference in an EARLY row is found (row 2 of 7 -- what the old one could not see); one ulp apart is reported (the bar is bit-exact); all seven differing rows are counted; a one-sided NaN is a mismatch and two NaNs in the same place agree; null and zero-row shapes are refusals, not silent success. The oracle keeps its old [batchcheck] argmax line for continuity and adds: [batchcheck] rows M=7 identical=7 differing=0 [batchcheck] rows M=7 identical=6 differing=1 first row=2 index=11 seq=0x... bat=0x... Two mistakes of my own that the compile caught before this commit: the first version replaced the residual-dump.h include instead of adding beside it (write_residual_dump then failed to resolve), and RowMismatch was unqualified at the call site. Both were found by a -fsyntax-only pass with the private runtime defines, which is why the forward is not being pushed unbuilt. --- CMakeLists.txt | 8 ++ npu/forward/include/batch_check_compare.h | 87 ++++++++++++++++ npu/forward/src/q4nx_forward.cpp | 28 +++++- tests/batch_check_compare_test.cpp | 115 ++++++++++++++++++++++ 4 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 npu/forward/include/batch_check_compare.h create mode 100644 tests/batch_check_compare_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7df77955f..564ef75ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -341,6 +341,10 @@ target_include_directories(serve_watchdog_test PRIVATE app) add_test(NAME serve_watchdog COMMAND serve_watchdog_test) # The NPU route's sampling controls and GBNF output constraint (app/npu_sampling.h, # app/npu_grammar.h, docs/npu.md "Sampling and structured output"); header only, no device. +# engine#1781: the comparator behind NPU_BATCH_CHECK -- pure arithmetic over two float arrays, so it runs with no model, +# no device and no runtime. It is a header, so the test includes it directly and needs no link libraries. +add_executable(batch_check_compare_test tests/batch_check_compare_test.cpp) +target_include_directories(batch_check_compare_test PRIVATE npu/forward/include) add_executable(npu_sampling_test tests/npu_sampling_test.cpp) target_include_directories(npu_sampling_test PRIVATE app) target_link_libraries(npu_sampling_test PRIVATE nlohmann_json::nlohmann_json) @@ -802,6 +806,10 @@ add_test(NAME route_port_lock COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/ # the HRX hardware gate's judges (docs/hw-gate.md) against a fake server and synthetic logs; # the gate itself needs the GPU and runs on Strix Halo (.github/workflows/hw-gate.yml) add_test(NAME hw_gate_judges COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/hw_gate_test.py) + # engine#1781: the comparator behind NPU_BATCH_CHECK, host-only (no model, no device, no runtime): + # the batched verify pass is compared row for row against the sequential one, bitwise. + add_test(NAME batch_check_compare + COMMAND batch_check_compare_test) add_test(NAME q4nx_pb16i COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/q4nx_pb16i_test.py) # The padded-grid read (engine#1097/#1110): tools/q4nx_diff.py decode reads a container whose weight sits on a # WIDER tile grid than the model's dimensions (the FastFlowLM repack pads intermediate_size to 512 columns, IM diff --git a/npu/forward/include/batch_check_compare.h b/npu/forward/include/batch_check_compare.h new file mode 100644 index 000000000..11ed0252d --- /dev/null +++ b/npu/forward/include/batch_check_compare.h @@ -0,0 +1,87 @@ +// 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. +// +// batch_check_compare.h -- the comparator behind NPU_BATCH_CHECK (engine#1781). +// +// The oracle that guards a batched verify pass used to compare ONE argmax at ONE position: the last row's. That +// answers "did the final argmax survive batching", which is not the question a speculative decoder asks -- it needs +// every drafted position's LOGITS (the accept rule consumes the target's probability at the drafted token, and two +// passes can agree on an argmax while disagreeing on the distribution around it). +// +// Pure and header-only on purpose: the comparison is arithmetic over two float arrays, so it can be pinned by a host +// test with no device, no model and no runtime, while the device-only half stays a single call in the forward. + +#ifndef ONEBIT_NPU_FORWARD_BATCH_CHECK_COMPARE_H +#define ONEBIT_NPU_FORWARD_BATCH_CHECK_COMPARE_H + +#include +#include +#include + +namespace onebit { +namespace npu { + +struct RowMismatch { + int row = -1; // first row that differs, -1 when every row matched + int index = -1; // column of the first difference within that row + uint32_t seq_bits = 0; // the two values, as bits, because a float print of two near-equal values reads as equal + uint32_t bat_bits = 0; + bool nan_mismatch = false; // one side NaN and the other not (bit comparison alone would call two NaNs equal) +}; + +// Compare `rows` x `cols` float rows BITWISE. Returns the number of differing rows (0 == identical). +// +// Bitwise, not tolerance-based: the gate is "the batched pass is the same arithmetic as the sequential one", so a +// value that differs in the last mantissa bit is a divergence to report, not a rounding to forgive. NaN is handled +// explicitly because the usual bit comparison would consider two NaN payloads equal and, more importantly, a NaN on +// one side with a finite number on the other is exactly the failure this oracle exists to name. +inline int compare_rows_bitwise(const float* seq, const float* bat, int rows, int cols, RowMismatch* first) { + if (!seq || !bat || rows <= 0 || cols <= 0) return rows > 0 ? rows : 0; + int differing = 0; + for (int r = 0; r < rows; ++r) { + const float* s = seq + (size_t)r * (size_t)cols; + const float* b = bat + (size_t)r * (size_t)cols; + for (int i = 0; i < cols; ++i) { + const bool s_nan = std::isnan(s[i]), b_nan = std::isnan(b[i]); + if (s_nan || b_nan) { + if (s_nan && b_nan) continue; // both not-a-number: the same non-answer, not a divergence + ++differing; + if (first && first->row < 0) { + first->row = r; first->index = i; first->nan_mismatch = true; + std::memcpy(&first->seq_bits, &s[i], 4); + std::memcpy(&first->bat_bits, &b[i], 4); + } + break; // one report per row is enough to know the row differs + } + uint32_t sb = 0, bb = 0; + std::memcpy(&sb, &s[i], 4); + std::memcpy(&bb, &b[i], 4); + if (sb != bb) { + ++differing; + if (first && first->row < 0) { + first->row = r; first->index = i; + first->seq_bits = sb; first->bat_bits = bb; + } + break; + } + } + } + return differing; +} + +} // namespace npu +} // namespace onebit + +#endif // ONEBIT_NPU_FORWARD_BATCH_CHECK_COMPARE_H diff --git a/npu/forward/src/q4nx_forward.cpp b/npu/forward/src/q4nx_forward.cpp index aa8179083..039dacc62 100644 --- a/npu/forward/src/q4nx_forward.cpp +++ b/npu/forward/src/q4nx_forward.cpp @@ -33,6 +33,7 @@ #include #include #include +#include "batch_check_compare.h" #include "residual-dump.h" #include #include @@ -3264,16 +3265,25 @@ bool Q4nxNpuForward::step_batch(const std::vector& tokens, int start_pos, // device state -- localises a batched-vs-sequential divergence immediately. static const bool batch_check = getenv("NPU_BATCH_CHECK") != nullptr; int seq_argmax = -1; + // engine#1781: the old oracle compared ONE argmax at ONE position -- the last row's -- which is not the question a + // speculative decoder asks. Every drafted position's LOGITS matter (the accept rule consumes the target's + // probability at the drafted token), so the sequential rows are kept whole and compared row for row, bitwise. + // M x vocab floats: 4-7 MB on the models this runs, allocated only under the debug flag. + std::vector seq_rows, bat_rows; // Not with injected rows: the oracle's own sequential pass runs step(tokens[m]) on the placeholder ids this // entry point supplies, so it would compare against a zero-token pass and report a divergence that is not // there. A debug tool that lies is worse than one that says "not applicable". if (batch_check && !inject_embeds_) { std::vector seq; - for (int m = 0; m < M; m++) + if (c_.NV > 0) seq_rows.reserve((size_t)M * (size_t)c_.NV); + for (int m = 0; m < M; m++) { if (!step(tokens[m], start_pos + m, seq)) return false; + seq_rows.insert(seq_rows.end(), seq.begin(), seq.end()); // every row, not only the last + if (m == M - 1) + for (size_t i = 0; i < seq.size(); i++) + if (seq_argmax < 0 || seq[i] > seq[(size_t)seq_argmax]) seq_argmax = (int)i; + } reset(); - for (size_t i = 0; i < seq.size(); i++) - if (seq_argmax < 0 || seq[i] > seq[(size_t)seq_argmax]) seq_argmax = (int)i; } // Batch phase attribution: the same partition step() prints, for an M-row verify pass, so the @@ -3685,6 +3695,8 @@ bool Q4nxNpuForward::step_batch(const std::vector& tokens, int start_pos, ok = run_lm_head_rows(norm_all, M, *batch_all_out_); } logits.assign(batch_all_out_->begin() + (size_t)(M - 1) * (size_t)c_.NV, batch_all_out_->end()); + // engine#1781: keep the batched rows so the oracle can compare all of them, not just the last one's argmax. + if (batch_check) bat_rows.assign(batch_all_out_->begin(), batch_all_out_->end()); } else { std::vector last(X.begin() + (size_t)(M - 1) * H, X.end()); rmsnorm(last.data(), final_norm_.data(), H, fnorm.data()); @@ -3712,6 +3724,16 @@ bool Q4nxNpuForward::step_batch(const std::vector& tokens, int start_pos, if (logits[i] > logits[(size_t)ba]) ba = (int)i; std::fprintf(stderr, "[batchcheck] M=%d start=%d seq_argmax=%d batch_argmax=%d %s\n", M, start_pos, seq_argmax, ba, seq_argmax == ba ? "MATCH" : "MISMATCH"); + // engine#1781: and the comparison the gate actually asks for -- every row, bitwise. + if (!seq_rows.empty() && bat_rows.size() == seq_rows.size() && c_.NV > 0) { + onebit::npu::RowMismatch mm; + const int differing = onebit::npu::compare_rows_bitwise(seq_rows.data(), bat_rows.data(), M, c_.NV, &mm); + std::fprintf(stderr, "[batchcheck] rows M=%d identical=%d differing=%d", M, M - differing, differing); + if (differing > 0) + std::fprintf(stderr, " first row=%d index=%d seq=0x%08x bat=0x%08x%s", mm.row, mm.index, + (unsigned)mm.seq_bits, (unsigned)mm.bat_bits, mm.nan_mismatch ? " (NaN)" : ""); + std::fprintf(stderr, "\n"); + } } else if (getenv("NPU_BATCH_DBG")) { std::fprintf(stderr, "[batch] M=%d start=%d batched (%d layers, two_widths=%d)\n", M, start_pos, c_.NL, (int)two_widths); diff --git a/tests/batch_check_compare_test.cpp b/tests/batch_check_compare_test.cpp new file mode 100644 index 000000000..2f831b95f --- /dev/null +++ b/tests/batch_check_compare_test.cpp @@ -0,0 +1,115 @@ +// 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. +// +// batch_check_compare_test.cpp -- pins the comparator behind NPU_BATCH_CHECK (engine#1781), so the oracle's answer is +// arithmetic that a host test can check rather than a device run that has to be trusted. No model, no device, no +// runtime: it is two float arrays and a count. + +#include +#include +#include +#include +#include + +#include "batch_check_compare.h" + +using onebit::npu::compare_rows_bitwise; +using onebit::npu::RowMismatch; + +static int failures = 0; + +static void check(bool ok, const char* what, const char* detail = "") { + std::printf("%s %s%s%s\n", ok ? "ok " : "FAIL", what, ok ? "" : " -- ", ok ? "" : detail); + if (!ok) ++failures; +} + +static std::vector rows(int nrows, int cols, float base) { + std::vector v((size_t)nrows * (size_t)cols); + for (size_t i = 0; i < v.size(); ++i) v[i] = base + (float)(i % 97) * 0.011f; + return v; +} + +int main() { + const int R = 7, C = 64; // the shape the gate asks about: k = 7 rows of logits + + { // 1. identical buffers match, row for row + auto a = rows(R, C, 0.25f); + auto b = a; + RowMismatch m; + check(compare_rows_bitwise(a.data(), b.data(), R, C, &m) == 0, "identical rows report no divergence"); + check(m.row == -1 && m.index == -1, "and leave the mismatch untouched"); + } + { // 2. a difference in the LAST row is found -- the old oracle's only case, so it must still work + auto a = rows(R, C, 0.25f); + auto b = a; + b[(size_t)(R - 1) * C + 5] += 1e-7f; + RowMismatch m; + int n = compare_rows_bitwise(a.data(), b.data(), R, C, &m); + char d[160]; + std::snprintf(d, sizeof d, "n=%d row=%d index=%d", n, m.row, m.index); + check(n == 1 && m.row == R - 1 && m.index == 5, "a difference in the last row is found", d); + } + { // 3. ...and a difference in an EARLY row is found, which the old oracle could not see at all + auto a = rows(R, C, 0.25f); + auto b = a; + b[(size_t)2 * C + 11] += 1e-7f; // row 2 of 7: a drafted position, not the last one + RowMismatch m; + int n = compare_rows_bitwise(a.data(), b.data(), R, C, &m); + char d[160]; + std::snprintf(d, sizeof d, "n=%d row=%d index=%d", n, m.row, m.index); + check(n == 1 && m.row == 2 && m.index == 11, "a difference in an early row is found too", d); + } + { // 4. bit-exactness is the standard: one ulp apart is a divergence, not a rounding + auto a = rows(R, C, 1.0f); + auto b = a; + b[3] = std::nextafter(b[3], 2.0f); + RowMismatch m; + int n = compare_rows_bitwise(a.data(), b.data(), R, C, &m); + check(n == 1 && m.row == 0 && m.index == 3, "a one-ulp difference is reported (the bar is bit-exact)", "no divergence reported"); + } + { // 5. every differing row is counted, so a run reports the size of the divergence and not just its presence + auto a = rows(R, C, 0.25f); + auto b = a; + for (int r = 0; r < R; ++r) b[(size_t)r * C + 1] += 1e-6f; + RowMismatch m; + int n = compare_rows_bitwise(a.data(), b.data(), R, C, &m); + char d[80]; + std::snprintf(d, sizeof d, "n=%d (wanted %d)", n, R); + check(n == R, "all seven differing rows are counted", d); + check(m.row == 0, "and the first one is the first row"); + } + { // 6. NaN on one side only is a divergence; NaN on both is not (the same non-answer) + const float nan = std::numeric_limits::quiet_NaN(); + auto a = rows(2, 4, 0.5f); + auto b = a; + b[6] = nan; // row 1, col 2: batched has NaN, sequential does not + RowMismatch m; + check(compare_rows_bitwise(a.data(), b.data(), 2, 4, &m) == 1 && m.row == 1 && m.index == 2 && m.nan_mismatch, + "a one-sided NaN is reported as a mismatch"); + auto c = a; + c[6] = nan; + auto d2 = a; + d2[6] = nan; + check(compare_rows_bitwise(c.data(), d2.data(), 2, 4, nullptr) == 0, "and two NaNs in the same place agree"); + } + { // 7. degenerate shapes are refusals, not silent success + auto a = rows(2, 4, 0.5f); + check(compare_rows_bitwise(nullptr, a.data(), 2, 4, nullptr) == 2, "a null side counts every row as differing"); + check(compare_rows_bitwise(a.data(), a.data(), 0, 4, nullptr) == 0, "zero rows compares as equal (nothing to check)"); + } + + std::printf("batch_check_compare: %d failure(s)\n", failures); + return failures ? 1 : 0; +} From 46633cce9730bcd878bd14a01a32b13485f2cfe4 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Tue, 10 Nov 2026 02:31:50 -0400 Subject: [PATCH 2/4] npu: the batch-check test's target is built by CI, and its registration is unconditional (fixes #1813's red) The build failed at ci_targets, and it was right to: the new test target was registered but never built by ci.yml's --target list, so ctest reported 'Not Run' and the ci_targets ratchet counted a target whose test CI cannot run. That is the direction-B check doing exactly its job. * .github/workflows/ci.yml --target list gains batch_check_compare_test * CMakeLists.txt: the add_test moves out of the if(Python3_Interpreter_FOUND) block (it is a C++ test, and inside that guard it would vanish on a configure without Python) and sits beside its add_executable, which is where the other C++ tests are registered Verified with the check that failed: python3 tools/ci/ci_targets_check.py --check -> rc=0, 'all 31 target(s) ci.yml builds exist in the configuration it creates', '9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new'. The test binary itself still builds with a plain g++ and passes its 11 arms. --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7c8399f8..0e9e472e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,7 +129,7 @@ jobs: laya_encoder_test http_guard_test moe_gguf_index_test laya_bounds_test \ npu_model_bounds_test npu_forward_loader_test npu_design_grid_test npu_sampling_test \ npu_flm_head_import_test npu_elf_args_test \ - npu_template_test npu_chatml_test \ + npu_template_test npu_chatml_test batch_check_compare_test \ tokenizer_linear_test hrx_placement_test hrx_cwsr_test hrx_runtime_test npu_device_lock_test -j - name: Test # One hung test must not hold the job for hours: each test gets 10 minutes. diff --git a/CMakeLists.txt b/CMakeLists.txt index 564ef75ad..3e5b228d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -345,6 +345,7 @@ add_test(NAME serve_watchdog COMMAND serve_watchdog_test) # no device and no runtime. It is a header, so the test includes it directly and needs no link libraries. add_executable(batch_check_compare_test tests/batch_check_compare_test.cpp) target_include_directories(batch_check_compare_test PRIVATE npu/forward/include) +add_test(NAME batch_check_compare COMMAND batch_check_compare_test) add_executable(npu_sampling_test tests/npu_sampling_test.cpp) target_include_directories(npu_sampling_test PRIVATE app) target_link_libraries(npu_sampling_test PRIVATE nlohmann_json::nlohmann_json) @@ -806,10 +807,6 @@ add_test(NAME route_port_lock COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/ # the HRX hardware gate's judges (docs/hw-gate.md) against a fake server and synthetic logs; # the gate itself needs the GPU and runs on Strix Halo (.github/workflows/hw-gate.yml) add_test(NAME hw_gate_judges COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/hw_gate_test.py) - # engine#1781: the comparator behind NPU_BATCH_CHECK, host-only (no model, no device, no runtime): - # the batched verify pass is compared row for row against the sequential one, bitwise. - add_test(NAME batch_check_compare - COMMAND batch_check_compare_test) add_test(NAME q4nx_pb16i COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/q4nx_pb16i_test.py) # The padded-grid read (engine#1097/#1110): tools/q4nx_diff.py decode reads a container whose weight sits on a # WIDER tile grid than the model's dimensions (the FastFlowLM repack pads intermediate_size to 512 columns, IM From 2fee8c9714432dd409dccc843476b9e6aeb9a0bb Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Tue, 10 Nov 2026 02:32:34 -0400 Subject: [PATCH 3/4] Revert "npu: the batch-check test's target is built by CI, and its registration is unconditional (fixes #1813's red)" This reverts commit 46633cce9730bcd878bd14a01a32b13485f2cfe4. --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e9e472e1..b7c8399f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,7 +129,7 @@ jobs: laya_encoder_test http_guard_test moe_gguf_index_test laya_bounds_test \ npu_model_bounds_test npu_forward_loader_test npu_design_grid_test npu_sampling_test \ npu_flm_head_import_test npu_elf_args_test \ - npu_template_test npu_chatml_test batch_check_compare_test \ + npu_template_test npu_chatml_test \ tokenizer_linear_test hrx_placement_test hrx_cwsr_test hrx_runtime_test npu_device_lock_test -j - name: Test # One hung test must not hold the job for hours: each test gets 10 minutes. diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e5b228d0..564ef75ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -345,7 +345,6 @@ add_test(NAME serve_watchdog COMMAND serve_watchdog_test) # no device and no runtime. It is a header, so the test includes it directly and needs no link libraries. add_executable(batch_check_compare_test tests/batch_check_compare_test.cpp) target_include_directories(batch_check_compare_test PRIVATE npu/forward/include) -add_test(NAME batch_check_compare COMMAND batch_check_compare_test) add_executable(npu_sampling_test tests/npu_sampling_test.cpp) target_include_directories(npu_sampling_test PRIVATE app) target_link_libraries(npu_sampling_test PRIVATE nlohmann_json::nlohmann_json) @@ -807,6 +806,10 @@ add_test(NAME route_port_lock COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/ # the HRX hardware gate's judges (docs/hw-gate.md) against a fake server and synthetic logs; # the gate itself needs the GPU and runs on Strix Halo (.github/workflows/hw-gate.yml) add_test(NAME hw_gate_judges COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/hw_gate_test.py) + # engine#1781: the comparator behind NPU_BATCH_CHECK, host-only (no model, no device, no runtime): + # the batched verify pass is compared row for row against the sequential one, bitwise. + add_test(NAME batch_check_compare + COMMAND batch_check_compare_test) add_test(NAME q4nx_pb16i COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/q4nx_pb16i_test.py) # The padded-grid read (engine#1097/#1110): tools/q4nx_diff.py decode reads a container whose weight sits on a # WIDER tile grid than the model's dimensions (the FastFlowLM repack pads intermediate_size to 512 columns, IM From fb3d0f8d49e2983e9ec52605c98adc05ce81e85d Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Tue, 10 Nov 2026 02:32:49 -0400 Subject: [PATCH 4/4] npu: no new test target (a workflow-scope token is needed to build it in CI), and the arms stay hand-run The build was red at ci_targets and the check was right: batch_check_compare_test was registered as a ctest whose target ci.yml never builds, so ctest reported 'Not Run' and the direction-B ratchet counted a new gap. The fix would be one word in .github/workflows/ci.yml, and this box's token cannot push that file -- 'refusing to allow an OAuth App to create or update workflow ... without workflow scope', the same wall #1699 hit for its cmake entry. So the target and its ctest are withdrawn rather than left half-registered: * no add_executable, no add_test, and the two orphaned registration lines removed; * npu/forward/include/batch_check_compare.h stays (the oracle uses it, and that is the change that matters); * tests/batch_check_compare_test.cpp stays as a HAND-RUN check, and its 11 arms still pass: g++ -O1 -std=c++17 -I npu/forward/include tests/batch_check_compare_test.cpp -o /tmp/bcc && /tmp/bcc Follow-up for a lane with workflow scope, or for the owner: add batch_check_compare_test to ci.yml's --target list and re-add add_executable/add_test beside the target in CMakeLists.txt -- two lines that turn the hand-run check into a gate. Recorded rather than left implied, because a test that is never run by CI is the class of defect this PR's own subject is about. Verified with the check that failed: python3 tools/ci/ci_targets_check.py --check -> 30 targets, '9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new'. --- CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 564ef75ad..5fb8adf82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -343,8 +343,6 @@ add_test(NAME serve_watchdog COMMAND serve_watchdog_test) # app/npu_grammar.h, docs/npu.md "Sampling and structured output"); header only, no device. # engine#1781: the comparator behind NPU_BATCH_CHECK -- pure arithmetic over two float arrays, so it runs with no model, # no device and no runtime. It is a header, so the test includes it directly and needs no link libraries. -add_executable(batch_check_compare_test tests/batch_check_compare_test.cpp) -target_include_directories(batch_check_compare_test PRIVATE npu/forward/include) add_executable(npu_sampling_test tests/npu_sampling_test.cpp) target_include_directories(npu_sampling_test PRIVATE app) target_link_libraries(npu_sampling_test PRIVATE nlohmann_json::nlohmann_json) @@ -808,8 +806,6 @@ add_test(NAME route_port_lock COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/ add_test(NAME hw_gate_judges COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/hw_gate_test.py) # engine#1781: the comparator behind NPU_BATCH_CHECK, host-only (no model, no device, no runtime): # the batched verify pass is compared row for row against the sequential one, bitwise. - add_test(NAME batch_check_compare - COMMAND batch_check_compare_test) add_test(NAME q4nx_pb16i COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/q4nx_pb16i_test.py) # The padded-grid read (engine#1097/#1110): tools/q4nx_diff.py decode reads a container whose weight sits on a # WIDER tile grid than the model's dimensions (the FastFlowLM repack pads intermediate_size to 512 columns, IM