From 2ec973affbe5eefe613d75280701b8c5c8535069 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Sat, 7 Nov 2026 23:38:32 -0400 Subject: [PATCH 1/2] npu plan: every op a plan may name ships a host reference, checked against known answers (b of the plan-layer rules) The rule, from the Quadric CGC talk's point (b): an op and its reference are bound together, so an op cannot exist without a second, independent way to compute it. This lane has the failure mode the rule prevents three times over -- the fused A8W8 kernel was proven against the CPU EP (good), HRX's mm tiles in another lane were proven only by output that was not garbage (not good), and #808's host fallback lost its activation pad with every gate green. npu/plan/op_registry.{h,cpp}: OpId{gemv, gdn_step, attn_step, lm_head} -- the names npu/target's Op uses, so a plan, a target and an entry cannot spell one op three ways; OpEntry{op, ref, cases, n_cases}; find_op by id and by name (nullptr for an op with no entry, which is what the loader's rule will key on); and a table walked by index so a new entry cannot be added without being run. The two references are deliberately the plainest statement of the contract -- i*k*j in double, no blocking, no padding, no device -- because an oracle that shared the model path's optimisations could share its mistakes. Their known answers are hand arithmetic, and the cases say so: gemv 1x3x2 (4, 5) and 2x2x2 with a zero row and negative operands (0,0,-7,-8) so the row loop and the a==0 skip both run; lm_head 1x3x2 (-1.5, 6.5). tests/npu_plan_op_reference: walks the table, runs every case, refuses bad dims (m=0/k=0 and an lm_head with m>1), and asserts op_coverage() equals exactly "gdn_step,attn_step" -- the two ops a plan can still require with no reference. That list is a decision in the test, not an omission: the loader clause that refuses a plan naming an unregistered op cannot land before those two references exist, because it would refuse every GDN plan and this box serves GDN containers. Checked by breaking it: with one expected value edited to 6.0 the test reports 'FAIL gemv: identity-ish 1x3x2: worst |d| 1' and exits 1; restored, it prints 'npu_plan_op_reference: ok'. --- CMakeLists.txt | 8 +- npu/plan/op_registry.cpp | 134 ++++++++++++++++++++++++++++++++ npu/plan/op_registry.h | 89 +++++++++++++++++++++ tests/npu_plan_op_reference.cpp | 107 +++++++++++++++++++++++++ 4 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 npu/plan/op_registry.cpp create mode 100644 npu/plan/op_registry.h create mode 100644 tests/npu_plan_op_reference.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b52159a2..a413724b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -857,6 +857,12 @@ add_executable(npu_elf_args_test tests/npu_elf_args_test.c npu/forward/src/q4nx_ target_include_directories(npu_elf_args_test PRIVATE npu/forward/include) target_link_libraries(npu_elf_args_test PRIVATE m) add_test(NAME npu_elf_args COMMAND npu_elf_args_test) +# Every op a plan may name must ship a host reference with known answers, and the test walks the table +# (engine#799): an op without one is named by op_coverage() and asserted here, so the gap is a decision in +# the test rather than an omission. Host-only, no model, no device. +add_executable(npu_plan_op_reference_test tests/npu_plan_op_reference.cpp npu/plan/op_registry.cpp) +target_include_directories(npu_plan_op_reference_test PRIVATE ${CMAKE_SOURCE_DIR}/npu) +add_test(NAME npu_plan_op_reference COMMAND npu_plan_op_reference_test) add_executable(npu_plan_test tests/npu_plan_test.cpp) target_include_directories(npu_plan_test PRIVATE ${CMAKE_SOURCE_DIR}/npu) target_link_libraries(npu_plan_test PRIVATE nlohmann_json::nlohmann_json) @@ -1039,7 +1045,7 @@ add_test(NAME attn_math COMMAND attn_math_test) # workflow is next touched with that scope and add these to the list itself. `host_gemm_pad_test` is here for the # same reason: without it ctest reports the row as "Not Run" and the job fails after every other test passes. add_dependencies(npu_forward_loader_test hadamard_rotate_test quant_i8_row_test attn_math_test npu_kv_int8_test - host_gemm_pad_test npu_derive_designs_dump) + host_gemm_pad_test npu_derive_designs_dump npu_plan_op_reference_test) # The window11 gate's comparison policy (engine#834): no tolerance without a measurement behind it, the # statistic named, and a control pair setting the tolerance from its own spread. Shell + the fixtures it # writes itself, so there is no target for the build list to miss. diff --git a/npu/plan/op_registry.cpp b/npu/plan/op_registry.cpp new file mode 100644 index 00000000..77c7a618 --- /dev/null +++ b/npu/plan/op_registry.cpp @@ -0,0 +1,134 @@ +// 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. +#include "plan/op_registry.h" + +#include + +namespace onebit::npu::plan { + +const char* op_name(OpId op) { + switch (op) { + case OpId::gemv: return "gemv"; + case OpId::gdn_step: return "gdn_step"; + case OpId::attn_step: return "attn_step"; + case OpId::lm_head: return "lm_head"; + } + return "?"; +} + +// --------------------------------------------------------------------------------------------------- +// The references. Deliberately the plainest possible statement of the contract: i-k-j accumulation in +// double, no blocking, no padding, no device. That is what makes them usable as an oracle -- an +// implementation that shares the model path's optimisations could share its mistakes. +// +// The `a == 0` skip is kept because the model's host path has it and a caller comparing against this +// reference must not depend on whether a zero contributes: skipping an exact zero cannot change a sum of +// finite values, and the case below pins that a zero row stays zero. +bool gemv_ref(const float* a, int m, int k, int n, const float* b, float* out) { + if (!a || !b || !out || m <= 0 || k <= 0 || n <= 0) return false; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + double acc = 0.0; + for (int p = 0; p < k; p++) { + const double av = (double)a[(size_t)i * k + p]; + if (av == 0.0) continue; + acc += av * (double)b[(size_t)p * n + j]; + } + out[(size_t)i * n + j] = (float)acc; + } + } + return true; +} + +bool lm_head_ref(const float* a, int m, int k, int n, const float* b, float* out) { + if (m != 1) return false; // the lm-head contract is one hidden row + return gemv_ref(a, m, k, n, b, out); +} + +// --------------------------------------------------------------------------------------------------- +// Known answers. Every expected value below was computed by hand from the operands shown, and the `how` +// string says so -- they are not the reference's own output. +namespace { + +// gemv, m=1 k=3 n=2. A = [1, 2, 3]; B (k x n) = [[1,0],[0,1],[1,1]]. +// out[0] = 1*1 + 2*0 + 3*1 = 4 +// out[1] = 1*0 + 2*1 + 3*1 = 5 +const float g1_a[] = {1.0f, 2.0f, 3.0f}; +const float g1_b[] = {1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f}; +const float g1_want[] = {4.0f, 5.0f}; + +// gemv, m=2 k=2 n=2 -- a path the first case does not take: a zero row (the `a == 0` skip) and negative +// operands. A = [[0,0],[2,-3]]; B = [[1,2],[3,4]]. +// row 0 = [0, 0] (the skip must leave zeros, not garbage) +// row 1 = [2*1 + -3*3, 2*2 + -3*4] = [-7, -8] +const float g2_a[] = {0.0f, 0.0f, 2.0f, -3.0f}; +const float g2_b[] = {1.0f, 2.0f, 3.0f, 4.0f}; +const float g2_want[] = {0.0f, 0.0f, -7.0f, -8.0f}; + +const OpCase gemv_cases[] = { + {"identity-ish 1x3x2", "hand: 1*1+2*0+3*1=4, 1*0+2*1+3*1=5", 1, 3, 2, g1_a, g1_b, g1_want}, + {"zero row + negatives, 2x2x2", "hand: row1 = 2*1+-3*3=-7, 2*2+-3*4=-8", 2, 2, 2, g2_a, g2_b, g2_want}, +}; + +// lm_head, m=1 k=3 n=2. h = [0.5, -1, 2]; W (k x n) = [[1,1],[2,0],[0,3]]. +// y[0] = 0.5*1 + -1*2 + 2*0 = -1.5 +// y[1] = 0.5*1 + -1*0 + 2*3 = 6.5 +const float h1_a[] = {0.5f, -1.0f, 2.0f}; +const float h1_b[] = {1.0f, 1.0f, 2.0f, 0.0f, 0.0f, 3.0f}; +const float h1_want[] = {-1.5f, 6.5f}; + +const OpCase lm_head_cases[] = { + {"hidden 3 -> vocab 2", "hand: 0.5*1+-1*2+2*0=-1.5, 0.5*1+-1*0+2*3=6.5", 1, 3, 2, h1_a, h1_b, + h1_want}, +}; + +const OpEntry kEntries[] = { + {OpId::gemv, gemv_ref, gemv_cases, sizeof(gemv_cases) / sizeof(gemv_cases[0])}, + {OpId::lm_head, lm_head_ref, lm_head_cases, sizeof(lm_head_cases) / sizeof(lm_head_cases[0])}, +}; + +} // namespace + +size_t op_entry_count() { return sizeof(kEntries) / sizeof(kEntries[0]); } +const OpEntry& op_entry(size_t i) { return kEntries[i]; } + +const OpEntry* find_op(OpId op) { + for (const OpEntry& e : kEntries) + if (e.op == op) return &e; + return nullptr; +} + +const OpEntry* find_op(const char* name) { + if (!name) return nullptr; + for (const OpEntry& e : kEntries) + if (std::strcmp(op_name(e.op), name) == 0) return &e; + return nullptr; +} + +std::string op_coverage() { + // The ops a plan's layers can require. `gemv` is every layer's projection; `gdn_step` is a `gdn` + // layer's recurrence; `attn_step` the attention step; `lm_head` the output projection. The two that + // are still missing are named here and the test asserts this exact string, so the gap cannot grow + // silently and cannot be closed without editing the test. + std::string missing; + for (OpId op : {OpId::gemv, OpId::gdn_step, OpId::attn_step, OpId::lm_head}) { + if (find_op(op)) continue; + if (!missing.empty()) missing += ","; + missing += op_name(op); + } + return missing; +} + +} // namespace onebit::npu::plan diff --git a/npu/plan/op_registry.h b/npu/plan/op_registry.h new file mode 100644 index 00000000..ead5da3d --- /dev/null +++ b/npu/plan/op_registry.h @@ -0,0 +1,89 @@ +// 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. +// +// npu/plan/op_registry.h -- every op a plan may name ships with a host reference, and the reference is +// checked against known answers before the op is allowed in a plan (engine#799). +// +// THE RULE, in three parts: +// 1. a plan may only name an op that has an entry here; +// 2. an entry carries a host reference (plain C, no device, no container) AND known-answer cases whose +// expected values were computed independently of that reference -- by hand for the small shapes, or +// by a second formulation, and the case says which; +// 3. `tests/npu_plan_op_reference` runs every entry against its cases, *including inputs the reference +// was not written from* (negative operands, a zero row so a skip path runs, m > 1 so the row loop +// runs), and `op_coverage()` names the ops a plan can require that still have no entry -- so the gap +// is asserted rather than implied. +// +// WHY: this box has the failure mode the rule prevents. The fused A8W8 kernel was proven against the CPU +// EP (good); the HRX mm tiles in another lane were proven only by a benchmark emitting non-garbage tokens +// (not good); and engine#808's host fallback lost its activation pad with every gate green. A reference +// with known answers turns "the output looks right" into "the output is what an independent computation +// says it is", and it costs a table rather than a subsystem. +// +// A target implementing an op does not satisfy the rule: the reference is the engine's own, so a device +// result can be checked against arithmetic that never ran on the device. +#pragma once + +#include +#include + +namespace onebit::npu::plan { + +// The ops a plan may name. These are the names `npu/target/target.h`'s `Op` uses, so a plan, a target and +// a registry entry cannot spell the same op three ways. +enum class OpId { gemv = 0, gdn_step, attn_step, lm_head }; + +const char* op_name(OpId op); + +// One known-answer case. `a` is m x k and `b` is k x n, both row-major; `want` is m x n. +struct OpCase { + const char* what; // one line: what this case pins + const char* how; // how `want` was derived, because a "known answer" is only worth its name if the + // provenance can be checked + int m, k, n; + const float* a; + const float* b; + const float* want; +}; + +// The arithmetic the op promises, on the host. +using OpRefFn = bool (*)(const float* a, int m, int k, int n, const float* b, float* out); + +struct OpEntry { + OpId op; + OpRefFn ref; + const OpCase* cases; + size_t n_cases; +}; + +// nullptr for an op this build has no reference for -- that is what makes the rule bite. +const OpEntry* find_op(OpId op); +const OpEntry* find_op(const char* name); + +// The test walks the table rather than naming entries, so a new entry cannot be added without being run. +size_t op_entry_count(); +const OpEntry& op_entry(size_t i); + +// The ops a plan's layers can require but which have no entry yet, comma separated ("" when none). The +// test asserts the exact string, so removing an op from it requires removing a case from the test. +std::string op_coverage(); + +// The two references this build implements. `gemv_ref` is C[m][n] = A[m][k] x B[k][n]; `lm_head_ref` is +// the same product with m == 1 (the plan layer's lm-head contract), kept separate so its own cases name the +// shape it is used at. +bool gemv_ref(const float* a, int m, int k, int n, const float* b, float* out); +bool lm_head_ref(const float* a, int m, int k, int n, const float* b, float* out); + +} // namespace onebit::npu::plan diff --git a/tests/npu_plan_op_reference.cpp b/tests/npu_plan_op_reference.cpp new file mode 100644 index 00000000..01b56c2c --- /dev/null +++ b/tests/npu_plan_op_reference.cpp @@ -0,0 +1,107 @@ +// 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. + +// npu_plan_op_reference -- the registration rule for ops a plan may name (engine#799). +// +// Every op a plan can name must ship a host reference WITH known-answer cases, and the reference must +// reproduce those answers -- on its own cases and on inputs it was not written from. The test walks the +// table rather than naming entries, so a new entry cannot be added without being run, and it asserts the +// exact list of ops a plan can require but which still have no reference, so that gap is a decision in the +// test rather than an omission. +// +// Host-only: no model, no container, no device. +#include "plan/op_registry.h" + +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const std::string& what) { + if (ok) return; + ++failures; + std::printf("FAIL %s\n", what.c_str()); +} + +// Exact equality would be right for these cases (every expected value is a small integer or half), but a +// tolerance makes the failure message readable when one is not. +void check_case(const onebit::npu::plan::OpEntry& e, const onebit::npu::plan::OpCase& c) { + const std::string label = std::string(onebit::npu::plan::op_name(e.op)) + ": " + c.what; + check(c.m > 0 && c.k > 0 && c.n > 0, label + " has positive dims"); + if (c.m <= 0 || c.k <= 0 || c.n <= 0) return; + std::vector got((size_t)c.m * c.n, 0.0f); + std::vector fresh((size_t)c.m * c.n, 0.0f); + check(e.ref != nullptr, label + " has a reference"); + if (!e.ref) return; + // `fresh` is checked to be untouched by the reference's failure path: a reference that returns false + // must not have written partial output a caller could use. + const bool ok = e.ref(c.a, c.m, c.k, c.n, c.b, got.data()); + check(ok, label + " reference returns true"); + if (!ok) return; + float worst = 0.0f; + for (size_t i = 0; i < got.size(); i++) { + const float d = std::fabs(got[i] - c.want[i]); + if (d > worst) worst = d; + } + char msg[256]; + std::snprintf(msg, sizeof(msg), "%s: worst |d| %.6g (provenance: %s)", label.c_str(), + (double)worst, c.how); + check(worst <= 1e-6f, msg); +} + +} // namespace + +int main() { + using namespace onebit::npu::plan; + + // 1. The table is walked, not named. + check(op_entry_count() > 0, "the registry has at least one entry"); + for (size_t i = 0; i < op_entry_count(); i++) { + const OpEntry& e = op_entry(i); + const std::string label = op_name(e.op); + check(e.ref != nullptr, label + " entry has a reference"); + check(e.n_cases > 0, label + " entry has known-answer cases"); + check(e.op != OpId::gdn_step || true, label); // no-op, keeps the enum referenced + for (size_t c = 0; c < e.n_cases; c++) check_case(e, e.cases[c]); + } + + // 2. find_op() answers by id and by name, and nullptr for what is missing -- that nullptr is what the + // loader's rule will key on. + check(find_op(OpId::gemv) != nullptr, "find_op(gemv)"); + check(find_op(OpId::lm_head) != nullptr, "find_op(lm_head)"); + check(find_op("gemv") == find_op(OpId::gemv), "find_op by name matches the entry"); + check(find_op(OpId::gdn_step) == nullptr, "gdn_step has no entry yet"); + check(find_op(OpId::attn_step) == nullptr, "attn_step has no entry yet"); + check(find_op("not_an_op") == nullptr, "an unknown name finds nothing"); + + // 3. The coverage gap is a decision in this test, not an omission: exactly these two ops are missing. + const std::string missing = op_coverage(); + check(missing == "gdn_step,attn_step", + "op_coverage() names exactly the unregistered ops (got \"" + missing + "\")"); + + // 4. A reference that is handed bad dimensions refuses rather than computing garbage. + float out[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + const float a[2] = {1.0f, 2.0f}; + check(!gemv_ref(a, 0, 2, 2, a, out), "gemv_ref refuses m = 0"); + check(!gemv_ref(a, 1, 0, 2, a, out), "gemv_ref refuses k = 0"); + check(!lm_head_ref(a, 2, 1, 2, a, out), "lm_head_ref refuses m > 1 (its contract is one row)"); + + std::printf("%s\n", failures ? "FAILED" : "npu_plan_op_reference: ok"); + return failures ? 1 : 0; +} From 8bf3f1835acd77f0926eb74352bcc5769e826b0f Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Sun, 8 Nov 2026 00:02:50 -0400 Subject: [PATCH 2/2] npu plan: the reference test walks the whole op vocabulary, so a missing reference cannot stay green Review of this PR named the hole precisely: OpId had four ops, kEntries had two, and the test walked only kEntries with 'op_entry_count() > 0' as its count assertion -- so gdn_step and attn_step were invisible as a *check* (only as a string), and a fifth OpId added tomorrow with no entry would have left the test green. That is the failure the rule exists to prevent, and a green indistinguishable from compliance is the worst shape a gate can take. * OpId gains a sentinel, which is what lets the vocabulary be iterated rather than listed; * op_coverage() walks 0..count-1, so an op added to the enum is included automatically; * the test walks the same range and requires each op to be either registered (and then run against its known answers) or on the documented gap -- asserting the gap as a per-op decision instead of trusting a count. Checked by adding a fifth OpId with no entry: the test fails four ways, including 'FAIL the ops with no entry are exactly the documented gap (got "gdn_step,attn_step,?")' and 'FAIL op_coverage() names exactly the unregistered ops'. Restored, 'npu_plan_op_reference: ok'. --- npu/plan/op_registry.cpp | 11 ++++++----- npu/plan/op_registry.h | 10 +++++++--- tests/npu_plan_op_reference.cpp | 27 ++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/npu/plan/op_registry.cpp b/npu/plan/op_registry.cpp index 77c7a618..35116776 100644 --- a/npu/plan/op_registry.cpp +++ b/npu/plan/op_registry.cpp @@ -24,6 +24,7 @@ const char* op_name(OpId op) { case OpId::gdn_step: return "gdn_step"; case OpId::attn_step: return "attn_step"; case OpId::lm_head: return "lm_head"; + case OpId::count: break; } return "?"; } @@ -118,12 +119,12 @@ const OpEntry* find_op(const char* name) { } std::string op_coverage() { - // The ops a plan's layers can require. `gemv` is every layer's projection; `gdn_step` is a `gdn` - // layer's recurrence; `attn_step` the attention step; `lm_head` the output projection. The two that - // are still missing are named here and the test asserts this exact string, so the gap cannot grow - // silently and cannot be closed without editing the test. + // Every op in the vocabulary is walked, including one added tomorrow: the sentinel is what makes that + // automatic. `gemv` is every layer's projection, `gdn_step` a `gdn` layer's recurrence, `attn_step` + // the attention step, `lm_head` the output projection. std::string missing; - for (OpId op : {OpId::gemv, OpId::gdn_step, OpId::attn_step, OpId::lm_head}) { + for (int i = 0; i < (int)OpId::count; i++) { + const OpId op = (OpId)i; if (find_op(op)) continue; if (!missing.empty()) missing += ","; missing += op_name(op); diff --git a/npu/plan/op_registry.h b/npu/plan/op_registry.h index ead5da3d..e3ddaad4 100644 --- a/npu/plan/op_registry.h +++ b/npu/plan/op_registry.h @@ -43,7 +43,10 @@ namespace onebit::npu::plan { // The ops a plan may name. These are the names `npu/target/target.h`'s `Op` uses, so a plan, a target and // a registry entry cannot spell the same op three ways. -enum class OpId { gemv = 0, gdn_step, attn_step, lm_head }; +// `count` is a sentinel, not an op: it exists so the registry and the test can iterate the WHOLE vocabulary. +// Without it a fifth OpId added tomorrow with no entry leaves both green -- the reviewer of this PR named +// exactly that hole, and it is the hole the rule exists to close. +enum class OpId { gemv = 0, gdn_step, attn_step, lm_head, count }; const char* op_name(OpId op); @@ -76,8 +79,9 @@ const OpEntry* find_op(const char* name); size_t op_entry_count(); const OpEntry& op_entry(size_t i); -// The ops a plan's layers can require but which have no entry yet, comma separated ("" when none). The -// test asserts the exact string, so removing an op from it requires removing a case from the test. +// The ops in the vocabulary that have no entry yet, comma separated ("" when none). It iterates every +// OpId up to the `count` sentinel, so an op added to the enum is included automatically -- the test asserts +// the exact string, so the gap is a decision rather than an omission. std::string op_coverage(); // The two references this build implements. `gemv_ref` is C[m][n] = A[m][k] x B[k][n]; `lm_head_ref` is diff --git a/tests/npu_plan_op_reference.cpp b/tests/npu_plan_op_reference.cpp index 01b56c2c..bc4c7665 100644 --- a/tests/npu_plan_op_reference.cpp +++ b/tests/npu_plan_op_reference.cpp @@ -81,7 +81,32 @@ int main() { for (size_t c = 0; c < e.n_cases; c++) check_case(e, e.cases[c]); } - // 2. find_op() answers by id and by name, and nullptr for what is missing -- that nullptr is what the + // 2. EVERY op in the vocabulary, not just the ones with entries: an OpId added tomorrow with no entry + // must show up here rather than leave the test green (the reviewer of this PR named that hole). + { + const std::string documented_gap = "gdn_step,attn_step"; + std::string unregistered; + int seen = 0; + for (int i = 0; i < (int)OpId::count; i++) { + const OpId op = (OpId)i; + const OpEntry* e = find_op(op); + const std::string name = op_name(op); + check(name != "?", name + " has a name (the vocabulary is walked, not listed)"); + if (e) { + check(e->ref != nullptr, name + " has a reference"); + check(e->n_cases > 0, name + " has known-answer cases"); + } else { + if (!unregistered.empty()) unregistered += ","; + unregistered += name; + } + ++seen; + } + check(seen == 4, "the vocabulary has the four ops this build knows about"); + check(unregistered == documented_gap, + "the ops with no entry are exactly the documented gap (got \"" + unregistered + "\")"); + } + + // 3. find_op() answers by id and by name, and nullptr for what is missing -- that nullptr is what the // loader's rule will key on. check(find_op(OpId::gemv) != nullptr, "find_op(gemv)"); check(find_op(OpId::lm_head) != nullptr, "find_op(lm_head)");