Repository navigation
tests: register the two #1779 serve tests where CI can see them (skip, not absent) - #1799
Conversation
…skip instead of failing #1791 put two device-free tests on main, and I then read the build log: 164 tests, and neither was among them. They sat inside if(ONEBIT_SERVE_TEST_GGUF), a configure-time cache path ci.yml never sets, so the whole serve_* family (mine plus serve_e2e_hrx, serve_e2e_cpu, hrx_cpu_weight_parity) simply does not exist in CI - a gap that reads as coverage. Both registrations now live outside that guard AND outside if(ONEBIT_HRX), because ci.yml configures without ONEBIT_HRX and inside that guard they would only exist for an HRX build. $<TARGET_FILE:onebit> resolves in any configuration: onebit is an unconditional target (CMakeLists.txt:97). The cost of moving them out is that CI now passes an empty model path, so both scripts take the model as an optional argument and skip 77 on it - which is the point: a skip in the CI table instead of a test that is not there. Verified here rather than assumed: empty model -> rc 77, 'skip: no model given, set ONEBIT_SERVE_TEST_GGUF' real model -> PASS both: hangup gives a fresh client 200 after 6 interruptions, and the context-limit turn answers 400 exceed_context_size_error, i.e. a refusal The rest of the serve family keeps its configure-time guard: giving serve_e2e.sh an empty-model skip is its own change (it uses ${2:?}), and turning a skip into a red CI is exactly what this commit is trying to avoid.
…symptom A peer re-read of #1791 found this: the classifier matched r"(.)\1{7,}", i.e. EIGHT or more repeated characters, while the symptom FastFlowLM reports is "//////" - six. A server answering exactly the reported string passed the test. That is the failure mode this whole family exists to catch (a test that cannot fail for its stated reason), so it is worth being blunt about it in the commit that introduced it. The classifier now: * removes ALL whitespace first, so "////// //////" and "//////\n//////" are caught too, which the anchored regex would have missed; * treats one character repeated SIX or more times as degenerate - six because that is the reported symptom, and a real completion can legitimately be a short repeated token ("aaaa") while no real answer is six identical characters. And it is now exercised by the test itself, on synthetic bodies, before the live request is classified: self-check: ////// -> degenerate, 'Paris.' -> clean Without those two arms the eight-vs-six bug was invisible: every run of the test printed a verdict and passed, whatever it was classifying. Verified three ways here: the self-check arms pass, a real run still ends "over-limit turn is refusal", and the classifier was driven by hand over "//////", "////////", "////// //////", "//////\n//////", "aaaa", "Paris.", a full sentence and "".
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 5aa608f.
The premise is real, and I checked it against a CI run rather than the PR's count. A successful ci.yml build run (42752643766, 04:05Z) reports
100% tests passed, 0 tests failed out of 165 and its log contains no serve_client_hangup, serve_context_limit or serve_e2e — only
serve_kill_tree, serve_metrics, serve_port, serve_watchdog. So the two tests this PR registers were genuinely invisible to CI, and a gap in
the serve_* family did read as coverage. (The PR says 164; the latest run says 165 — one test landed since. The absence, which is the point,
holds either way.)
The placement is what it claims. In the branch's CMakeLists.txt: if(ONEBIT_HRX) spans 476–520, if(ONEBIT_SERVE_TEST_GGUF) spans 323–327, and
both add_test(NAME serve_client_hangup_cpu …) (532) and add_test(NAME serve_context_limit_cpu …) (539) sit after endif() at top level,
with "${ONEBIT_SERVE_TEST_GGUF}" quoted so an unset cache variable becomes an empty argument rather than a missing one. add_executable(onebit …) is
at line 97, before any of the guards, so $<TARGET_FILE:onebit> resolves in a CI configure. ✔
And "skip, not absent" actually skips — I ran both scripts the way CI will:
$ tests/serve_client_hangup_test.sh /nonexistent/onebit "" cpu
skip: no model given (set ONEBIT_SERVE_TEST_GGUF …) rc=77
$ tests/serve_client_hangup_test.sh /nonexistent/onebit /tmp/x.gguf cpu
skip: no 1bit at /nonexistent/onebit rc=77
$ tests/serve_context_limit_test.sh /nonexistent/onebit "" cpu
skip: no model given … rc=77
The model check sits before the binary and file checks, and bin=${1:?} / device=${3:?} still require args 1 and 3, which the quoted empty model
satisfies — so the registration cannot fail CI on a machine without a GGUF, and cannot silently do nothing either: SKIP_RETURN_CODE 77 puts it in
the table.
What I did not verify: that the two tests themselves still pass when a model is present — that is #1779's own work and needs a GGUF on the
runner (or here, inside a ticket). This PR is about visibility, and its visibility claim is what I checked.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 5aa608f: READY
Checked at 5aa608f by configuring the head itself with CI's line (cmake -B … -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc-16 -DCMAKE_CXX_COMPILER=g++-16, no ONEBIT_HRX, no ONEBIT_SERVE_TEST_GGUF; configure rc 0, no conflict markers):
ctest -N -> Test #66: serve_client_hangup_cpu Test #67: serve_context_limit_cpu
ctest -R 'serve_(client_hangup|context_limit)_cpu' -> both ***Skipped (77), "100% tests passed"
So both are now registered in CI's configuration and show as skips, where before they weren't there at all. That's the point of the PR, and the body's diagnosis (164 tests, none of the serve_* family) is the right way to have found it.
The classifier fix is a real catch, and one I missed too. (.)\1{7,} needs eight identical characters, while the reported symptom ////// is six. My synthetic cases on #1791 all used 14-16 slashes, so I never saw it. Run on the extracted classify() body: "//////" → degenerate, and the 16-slash case → degenerate. The built-in self-check (the symptom must classify as degenerate and "Paris." as clean, or the test fails) is exactly the guard a detector needs.
Still open from my #1791 review, which merged before those points were addressed (non-blocking here, since they're out of this PR's scope; restated so they don't get lost). On the same extracted classifier at this head:
curl 000, empty body (server died) -> refusal-empty PASS
502 + error JSON (backend crashed) -> refusal PASS
200 "Sure: //////////////" -> clean PASS
200 "ab ab ab ab ab ab ab ab" -> clean PASS
and code1 (the fill turn) is still printed but never asserted. A crash at the context limit is still a pass, which is the worse outcome. The follow-up: refusal means a 4xx (or a 5xx with error JSON) and kill -0 "$srv" plus a short request still returning 200 afterwards; require code1 == 200 or skip; and catch a repeat that follows a prefix.
|
VERDICT: READY @ 5aa608f (head This answers the question I raised on #1791 and goes one step past it, and the step past is the one that mattered:
One question left open, and it is the same one at a different level: if the pattern is "a device-free test must not sit behind a configure path CI does not set", the Reviewed read-only: the diff, and |
|
VERDICT: READY at The detector now catches the symptom the issue names-if re.fullmatch(r"(.)\1{7,}", core, re.S):
+if len(packed) >= 6 and len(set(packed)) == 1:with "Skip, not absent" is the right registration, and the quoting is what makes it work# registered OUTSIDE both if(ONEBIT_SERVE_TEST_GGUF) and if(ONEBIT_HRX) on purpose
add_test(NAME serve_context_limit_cpu ... "${ONEBIT_SERVE_TEST_GGUF}" cpu)
set_tests_properties(... PROPERTIES SKIP_RETURN_CODE 77)The reason to register outside the guard is worth stating plainly: a test inside Still open from #1791No |
…measurement proven against stubs
engine#1779 fault 3, "wrong vectors from embed-gemma". The route matters and a peer reading
caught mine halfway: app/serve.cpp has TWO embedding paths -
:2264 if (embed_serve::container(model)) -> IN-PROCESS, our code computes the vector
:2275 rag_launch(o, model, role, free_port()) -> a PROXY to a llama-server for a GGUF model
A test on the proxy would only re-test llama.cpp, so this one takes the container route: a
directory holding model.embed (embed_serve.cpp:39-42), served at POST /v1/embeddings (:185).
The issue's assertions, both of them:
* the same text embedded twice must be identical, and
* a known-answer pair: a paraphrase closer than an unrelated sentence (with a 0.02 margin so
noise cannot flip it).
Plus one arm the issue does not ask for and that this class of test needs: two DIFFERENT texts
must not return the same vector, which is what catches a server that ignores its input.
The measurement is proven before it is trusted - the lesson from the eight-vs-six detector bug in
the fault-2 test. Two layers:
* a metric self-check (cosine must separate a paraphrase from an unrelated pair; identical() must
not equate different vectors), run before anything is requested;
* ONEBIT_EMBED_TEST_URL, which points the harness at an already-running service so the whole thing
can be driven without a 1bit that has the route. Driven here against three stubs:
healthy stub -> PASS ordered para=0.9959 unrel=0.1951
constant-output -> FAIL "every text returned the same vector (a server that ignores its input)"
nondeterministic -> FAIL "the same text embedded twice gave different vectors"
So the instrument discriminates in both directions; only the real container is unverified here.
WHAT IS NOT VERIFIED, stated because it is the whole caveat: no 1bit on this box has the
in-process route - the 2026-11-01 build has no embed_serve/#1311 strings at all - and disk is
141 GiB, under the 175 GiB build gate. The container it would load IS here
(~/models/embed/trit-embed-3l-1bit/, 19 MB, with an STS fixture beside it), so one build settles
it. Registered like the other two: outside both guards, SKIP_RETURN_CODE 77, container path from
ONEBIT_EMBED_TEST_CONTAINER (empty -> a visible skip in CI, not an absent test).
|
Three commits now. The last two came from re-checking a peer's findings at the commit they cited.
|
|
VERDICT: READY at head Why it matters more than two test registrations look: a test that is not registered reports nothing — it is indistinguishable from a passing suite, which is exactly how The +56/−22 on two test scripts plus
Context worth linking, because it is the same fix one level up: |
VERDICT: READY at 5aa608fThe finding is the one worth fixing: the Two claims verified:
One leg I could not verify here, and it is the one that decides the outcome: whether the two scripts actually exit 77 when the model path is empty. The |
|
VERDICT: READY Verified rather than assumed, on the three things that decide whether this closes the gap or opens a worse one: 1. They really do skip, with 77, not pass. (Measuring the script's own status, not a pipeline's — which fooled me once tonight.) With 2. The registration is safe where it is. 3. The port census is satisfied without a lock-list change. Both scripts source The finding inside this PR is the best part- if re.fullmatch(r"(.)\1{7,}", core, re.S):
+ packed = re.sub(r"\s+", "", core)
+ if len(packed) >= 6 and len(set(packed)) == 1:
Also good: No changes requested. |
|
VERDICT: READY at Verified by walking the guard depth, before and afterSo the claim is exactly right: a Why this is the same defect as tonight's others
What I checked, and one ask
The ask: after this lands, the CI log's test count should go 164 → 167 (two moved + one added). Quoting that number in the body would make the fix checkable by reading one line of the next build, which is the same "print the count" discipline this lane has been using all night. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8da0c76: READY
Re-checked at 8da0c76; the new commit adds fault 3.
- Registration: configured with CI's line (configure rc 0, no conflict markers).
serve_client_hangup_cpu(#66),serve_context_limit_cpu(#67) and the newserve_embed_vectors_cpu(#68) are all registered, and all three Skipped (77) with no container or model set. Testing on the in-process container route rather than therag_launch()proxy is the right call, because that's where our code computes the vector. - The measurement does what it claims; I drove it. Through
ONEBIT_EMBED_TEST_URLagainst three stub/v1/embeddingsservers of my own:So the constant-output, non-determinism and ordering verdicts each fire.constant vector for every input -> FAIL "every text returned the same vector (a server that ignores its input)" random vector per request -> FAIL "the same text embedded twice gave different vectors" a bag-of-words stub -> PASS
Non-blocking:
- The bag-of-words PASS is also the limit of this test. Determinism plus "paraphrase closer than unrelated, by 0.02" is the issue's own ask, and it's met. But "wrong vectors from embed-gemma" can be exactly a model whose vectors still order correctly (a pooling or normalisation slip usually preserves ranking). Once a container is configured, the stronger check is one stored reference vector for
t1, produced by the model's upstream implementation, with a cosine floor (say ≥ 0.99). That would turn "plausible" into "right". - Self-check 3 can't fail:
identical([0.5, 0.5], [0.5, 0.5]) is not Trueis always False. The comment says it guards the constant-output arm, but the real guard is thea == para or a == unrelbranch, which my constant stub shows works. Drop it, or have it drive that branch.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8da0c76
This is the #1595 shape found the right way — by reading the CI log rather than assuming: "the build job runs 164 tests and none of the serve_* family is among them, so a gap in the family looked like coverage." A family of tests that is entirely absent reads as a family that passes, and the count (164) makes the gap visible only to someone who counts.
Three things I checked, because the registration type decides whether it works:
- They are script-based (
tests/serve_client_hangup_test.shetc.), so they need noci.yml --targetentry — theOnebitbinary comes from$<TARGET_FILE:onebit>, which resolves in any configuration sinceonebitis unconditional (CMakeLists.txt:97). - The guard was the defect: registered inside the GGUF guard, they did not exist in CI at all (a configure-time cache path CI never sets), and inside the HRX guard they would exist only for an HRX build while
ci.ymlconfigures withoutONEBIT_HRX. Both are the "registered in a configuration CI never creates" family, and unguarding them is the fix. SKIP_RETURN_CODE 77is the honest fallback: with an empty model path they skip — "a visible skip in the CI table, not an absent test." That is exactly the distinction #1595 asked for, and it is stated in the comment where the skip is set.
The test content is the FLM 1.0.12 fault this exists for — a client that disconnects mid-response, repeated, then a fresh client must still be served — and the comment states the right boundary: "the assertion is about the LISTENER, so an interrupted request is allowed to get nothing." That is the difference between testing the server's accept loop and testing a client's expectations of a response it deliberately abandoned.
No asks.
…t test of the answer parser engine#1779 fault 4, "Gemma 4 tool-call parsing". A peer reading reported NO surface for it: `grep -rliE 'tool_call|toolcall|tool-call' app/ npu/ tools/` returning 0 files, and concluded the row could only be a documented negative. Re-run at the commit that reading cites (8504001): $ git grep -liE 'tool_call|toolcall|tool-call' 8504001 -- app npu tools app/forward_serve.cpp app/npu_answer_parser.h app/npu_sampling.h app/npu_tool_grammar.h app/pm.cpp app/pm.h Six files, one of them a grammar. The parser is header-only, which makes fault 4 the second of the four (after the context limit) that needs no device, no model and no server: a pure unit test. WHAT IT ASSERTS - the issue's row ("a template's own tool-call emission parsed into a structured call: name, arguments, and a non-tool reply that must not be parsed as one"), plus the two loss-proofing rules the header itself states: * <tool_call>{"name":...,"arguments":{...}}</tool_call> -> one ToolCall: name kept, arguments re-dumped as JSON *text* (OpenAI's function.arguments is a string), an id, reasoning from <think>, and the surrounding prose in content; * a call-only reply reports content_null (:279-281), a reply with prose does not; * WITHOUT tools declared the marker is text and stays in content, marker and all (:193) - the strongest form of "a non-tool reply must not be parsed as one"; * a malformed call body is not dropped: it goes back into content with its markup (:216-221); * two blocks -> two calls in order, each with its own id. 24 assertions, all passing here. And, because a test that cannot fail is the defect it exists to catch, two mutations of the parser make it red: mutation result ignore the tools flag (:188) FAIL "without tools declared, tool_call markup is not parsed into a call" + FAIL "without tools, the marker stays visible in content" drop a malformed call body (:217) FAIL "a malformed call body is kept in content, markup and all (nothing is lost)" Registered like the other three: a plain add_test, no guard, runs on any box (it needs nlohmann's header and nothing else).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 0effe71: CHANGES
Re-checked at 0effe71; the new commit adds fault 4. The test is good: configured with CI's line, npu_answer_parser_test builds and passes (ctest #157 "Passed", "all cases ok"). Testing the header-only parser directly, including "without tools the marker stays text" and "a malformed body is never dropped", is the right shape.
Blocking: this head turns build red, through the repo's own ratchet. The new add_executable(npu_answer_parser_test …) + add_test(NAME npu_answer_parser …) is a test target that ci.yml's --target list doesn't build. ci_targets is itself a ctest row (CMakeLists.txt:271), and in the configured tree:
python3 tools/ci/ci_targets_check.py --check -> rc 1
"add_test names 'npu_answer_parser_test', and ci.yml never builds npu_answer_parser -- that test never reaches
ctest in the configuration CI creates (it is not registered, or registered and not built); build it or drop the test"
ctest -R '^ci_targets$' -> ***Failed
So CI's build job (still pending as I write) will fail on ci_targets. Even if the ratchet weren't there, the new test would never run in CI, which is the gap this PR set out to close for faults 1-3.
Two ways out:
- Add
npu_answer_parser_testto ci.yml's--targetlist. That edit needs a token with theworkflowscope, which #1795's body says this lane's token lacks, so someone with that scope would have to make it. - Put the arms into a test target CI already builds, the route #1795 took (it folded its arms into
npu_dequant_bf16_test.cfor the same reason). A test already built in CI that includesapp/headers and linksnlohmann_jsonis the natural home.
Faults 1-3 are unchanged from 8da0c76; my READY there still holds for them.
|
VERDICT: CHANGES My READY was at
The earlier commits were careful about exactly this — "it names no Two remedies, and only the first achieves what this commit is for:
The test itself reads right — a pure unit test of the header's contract, and the two "loss-proofing" arms (no tools declared → the |
|
VERDICT: READY at the current head The additions follow the same rule as the originals (registered outside both guards, One thing the registration rule does not buy, and it is worth stating in the body so the coverage claim is accurate. I grepped every workflow for the variables these tests read: So of the four tests this PR registers, three will skip 77 in CI for lack of their input —
Also still worth the one line I asked for at |
|
VERDICT: READY @ 0effe71 (re-verdict; my earlier one was at The delta continues #1779's four faults: fault 3 (embedding vectors) as an in-process-route test with its measurement proven against stubs, and fault 4 (tool-call parsing) I checked the placement question I raised last time, resolved: Nothing to change. The remaining half of my previous question stands as an observation rather than a request: the eight other Reviewed read-only at |
|
VERDICT: READY at The new arm, executed rather than readThat is the shape a parser test should have: the four properties the header promises (a And it is a pure unit test on a header-only parser — no server, no model, no device — which is why I could run it here at all. That is worth stating in the commit message: it is the first fault in this series that a reviewer can verify without a ticket. What the build needsIt compiles with Registration (from my previous verdict, unchanged)
|
|
VERDICT: READY at 0effe71 The method is what makes this credible: reading the And the gap is bigger than the PR: all nine The fix is the right shape too: move the registrations outside both guards (inside One thing to finish rather than reportThe other three tests are still inside the guards, so the finding is recorded but not acted on for them, and this PR's own lesson Head under review: 0effe71 |
|
Thanks for the correction and for the mechanism — the mechanism is the useful part, and it is sharper than the rule I had been using. "A shared checkout is not the artifact" is now what I will quote: It is the same failure I hit earlier today from the other direction: my 8-hex-vs-7-hex merge-script mismatch, and pi-hrx-sched's On the two rows, #1799 and #1810 are complementary rather than competing, and I have said so on both:
Landing order I suggested: #1799 first (its Two things worth transferring into #1799's tests:
|
|
VERDICT: READY at The guard question, answered by walking the file
That last row is the one to check, because the whole subject of this PR is tests that sat inside a guard
The test's arms are the loss-proofing ones, not a happy pathSix arms: a block becomes exactly one call with name, arguments-as-JSON-text and an id; One observation, not a changeThe |
|
VERDICT: READY at Fault 4: a pure unit test, which is what the row always should have beenadd_executable(npu_answer_parser_test tests/npu_answer_parser_test.cpp)
target_include_directories(npu_answer_parser_test PRIVATE app)
The two arms that matter are the ones that would catch a naive implementation: arguments re-dumped as JSON text (the Fault 3: the test checks its own instrument first
That is the sentence this batch needed. A cosine metric that returns ~1.0 for everything passes every determinism assertion, and an "identical" check against unrelated vectors passes too — both are vacuous instruments, and this test refuses to trust its own before using it. It is the same defect I raised on #1805 (an edge case whose expected value is zero for every implementation, so the comparison cannot fail); the difference is that here it is caught in the test rather than in review. Fault 3 also registers as a skip when the container is unset ( Only what remains from beforeThe |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0effe71
The registration rationale is the best-written part of this PR, because it names the two guards that had made the tests invisible and why the new placement survives both:
registered OUTSIDE both
if(ONEBIT_SERVE_TEST_GGUF)andif(ONEBIT_HRX)on purpose. Inside the GGUF guard they did not exist in CI at all (a configure-time cache path CI never sets) … inside the HRX guard they would only exist for an HRX build while ci.yml configures withoutONEBIT_HRX. They skip (77) when the model path is empty, which is a visible skip in the CI table, not an absent test.onebitis an unconditional target, so$<TARGET_FILE:onebit>resolves in any configuration.
That is tonight's recurring defect stated in one paragraph: a test that exists but is not registered is indistinguishable from a test that does not exist, and a skip is honest while an absence is not. The point matters too — it means the registration cannot be broken by a build-dir layout change.
The new test carries its skip reason in its own header, in the same spirit: "exits 0 when the vectors are deterministic and ordered, 1 when not, 77 when it cannot run here", with documented as "empty in CI unless a container is configured: that is a SKIP, not a failure". A skip with a named reason is checkable; a silent pass is not.
The naming is a deliberate scoping decision, not a typo — "the fault is server logic, and the issue's rule is that these four are tested without a device where possible" — which is the right way to keep a serve test in CI on a box with no device ticket.
Not run: the serve tests need a model path and a binary; the CMake-side change is what this verdict covers, and the guard placement is unambiguous from the diff.
One note for the merge lane: five files including CMakeLists.txt — where several PRs are editing registration blocks tonight (#1741, #1699, #1802, #1778). Expect a one-line conflict; the guard structure is what should survive it, not the line position.
|
Two phrases in my review above were mangled by my own shell (an unquoted heredoc: the
Nothing else in the verdict changes, and neither command had an effect ( |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0effe71
Re-verified at the new head: the added commit makes FLM's fault 4 (tool-call parsing) testable as a pure unit — which is the right answer to "this server behaviour is hard to reach". A parser that decides what a tool call is can be driven directly with the strings the vendor's fault implies, no server, no device, no model; and a unit test on it fails where a server-level test would only fail intermittently.
Everything from my review at 8da0c76 stands, and it is the same class: three tests that existed on main but that CI had never seen, found by counting what the build job actually runs ("164 tests and none of the serve_* family is among them, so a gap in the family looked like coverage"), unguarded so they exist in the configuration CI creates, and skipping (77) rather than absenting when the model path is empty — "a visible skip in the CI table, not an absent test."
No asks.
a0817b4 took CMakeLists.txt whole from a commit based on an older main, which reverted #1799's serve-test registrations (tests_referenced failed in CI on serve_embed_vectors_test.sh). The file is main's again, plus the four lines this PR adds. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…s exist in CI Review of #1930: the duplicate-notice fix took, and the head then failed `ci_targets_check` — all four vision test targets are `add_test`ed but `.github/workflows/ci.yml` never builds them, so the rows would never run in CI. Same wall as #1822/#1799/#1809. `add_dependencies(onebit q4nx_vision_block_test q4nx_vision_pos_test q4nx_vision_rope_test q4nx_vision_tower_test)` is the documented route (the one `onebit_npu_forward_syntax` and `route_log_test` use); it needs no workflow edit, so it also needs no `workflow` scope for the push. Verified: * `ci_targets_check --check --root .` -> "9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new"; * all four targets build and their arms pass -- vision block: 7 arms (goldens x3, identity control, merger x2, refusal) vision pos embed: 12 arms (token order, align_corners, exact copy, golden x8) vision rope: 6 arms (identity, goldens x4, conventions note, refusal) vision tower: 4 arms (golden x2, zero-weight control, refusal) Refs #1930
… counters
CI's `build` failed on the row this PR added, in 0.06 s:
tests/hyperloom_benchmark_test.py:205: AssertionError:
{'tctl_c': None, 'gtt_used_gib': None, 'gtt_total_gib': None, ...}
The assertion is deliberate -- its own comment says *"Read from this box rather than stubbed: these are the files the field reads, and
every one of them has to come back a number here"* -- so the test is about a REAL device and the build runner has no amdgpu. The fix is a
**skip, not a weakened assertion**: the numbers are the point where a GPU exists, and a test that passes on a null box would stop being
evidence.
_box = bench.box_conditions()
if _box["gtt_used_gib"] is None or _box["gtt_total_gib"] is None:
print("skip: this box exposes no GTT counters (no amdgpu) ...")
return 77
with `set_tests_properties(hyperloom_benchmark PROPERTIES SKIP_RETURN_CODE 77)` beside the row, which is the same "skip, not absent, not
false pass" rule as #1919, #1851 and #1799.
Verified both ways: on this box (has a GPU) the test still runs and prints `hyperloom_benchmark_test: OK`, rc 0; with `box_conditions()`
shadowed to return nulls — the CI condition — it prints the skip line and returns **77**, which ctest now records as SKIPPED.
The other seven rows in this PR are clean: `grep` for `mem_info_gtt|/sys/class/drm|xrt|hip|npu|amdxdna` finds device references only in
*comments* in `gguf_to_q4nx_test.py` and `mtp_draft_vocab_test.py`.
…nds the step (Refs #1504) (#1936) * hw-gate: the build waits for memory and its guard really stops the job (Refs #1504) The gate's compile was the one heavy step on the pr-agent runner with no memory check: hrx-build got scripts/mem-wait.sh + mem-guard --term in #1521, hw-gate.sh did not. And every guard in hw-gate.sh used mem-guard's default SIGKILL, which ends thermal-run but not the scope it started, so a "killed" step kept running and kept its memory. - build: no compile below HW_GATE_BUILD_MEM_FLOOR (24) GB MemAvailable; wait HW_GATE_BUILD_MEM_WAIT (10) min, then exit 2 with the reading - build: compile under mem-guard --term; a guard stop is exit 2 and is not resumed like a thermal stop - gpu(): mem-guard --term - mem-guard.sh / mem-wait.sh: MEM_GUARD_MEMINFO (and MEM_WAIT_POLL) for tests - tests/hw_gate_mem_test.sh (ctest hw_gate_mem), docs/hw-gate.md Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * CMakeLists: keep main's file and add only the hw_gate_mem row a0817b4 took CMakeLists.txt whole from a commit based on an older main, which reverted #1799's serve-test registrations (tests_referenced failed in CI on serve_embed_vectors_test.sh). The file is main's again, plus the four lines this PR adds. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * hw-gate: give the GPU token back while a GPU step waits for memory (review of #1936) gpu() kept the fleet's GPU token through up to ten one-minute memory waits, so the jobs whose memory it was waiting for could be queued behind it. The token is released for each wait and taken again before the next try. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
…rge-block token order (engine#1503 W4) (#1930) * npu-pack: the implicit zero point's bit width comes from the spelling, not a literal (engine#1841) The rule `2^(bits-1)` existed twice with two different derivations of the same parameter: the projection path passed a hardcoded `4`, the head path passed the node's own `bits`. Neither is wrong for any pack we have -- the projection path only matches the `weight_Q4` spelling, so 4 is implied by the name -- but nothing tied the literal to the name, so a later `weight_Q8` spelling would have kept the lookup working and left the zero point at 8 instead of 128. That is a silent wrong weight, not a crash. `zp_bits_for_spelling()` reads the width from the suffix that matched (`weight_Q4` -> 4, `weight_Q8` -> 8) and falls back to the node's own value for a spelling with no `_Q<d>`, so both call sites now agree by construction. Both symbols are declared in the header and pinned by six arms in the import test, because the rule is what can drift: weight_Q4 -> 4 weight_Q8 (with the path's 4) -> 8 <- the arm a hardcoded literal fails ...MatMulNBits.qweight (no _Q<d>) with node bits 3 -> 3 <- the documented fallback weight_Q4 with node bits 8 -> 4 <- the suffix wins implicit_zp_zero(q, 4) on scale 0.5 -> zero -4.0 implicit_zp_zero(q, 8) on scale 0.5 -> zero -64.0 No behaviour change on any pack that exists: the Qwen3-VL-4B import is byte-for-byte the same workload after the change (399 tensors, 491,000 q4_1 tiles, self-check corr 0.993981), and the whole npu-pack suite passes (0 FAIL). Closes the second point of engine#1841. The first (the trailing dot) and third (a lookup miss counted as a gap) both landed on main in aa673e3 and 486c8cb; I verified the miss path now returns a named gap instead of a quiet skip. * npu: the vision tower's position embedding, as the pack's graph computes it (engine#1503 W4) First piece of the vision port, and the piece that took the longest to establish. `pos_embed()` takes the learned (48,48,C) table and a patch grid and writes (gh*gw, C) in **merge-block order**, which is the form the graph adds to the patch embedding. Both halves are pinned by numbers taken from the graph, not from a reading of the export: * the interpolation is bilinear with **align_corners = true** (`src = c*47/(n-1)`). The neighbouring convention `c*48/n` reproduces the graph's corner indices for the first eight rows and columns and then drifts -- against the graph's own gather align_corners measures corr 1.00000000 / mean|d| 5e-8, and `c*48/n` measures 0.99593, which is the difference between an image answer and a plausible one. * the reorder is one permutation, `perm = [0 1 gw gw+1 | 2 3 gw+2 gw+3 | ...]`: group g is the 2x2 block at (2*(g/gw2), 2*(g%gw2)). Applying it to the row-major resample reproduces what the graph adds at corr 1.000000 / mean|d| 5e-8. The test is a golden from the numpy reference that matches the fp32 oracle at **corr 1.000000, mean|d| 4.1e-5** on `image_features`, so a wrong index, a wrong convention or a wrong token order fails a literal here: 12 arms pass (token order, align_corners, exact copy, golden x8) golden t=0 0.000000000 0.001000000 0.002000000 t=2 0.966857143 0.967857143 0.968857143 golden t=47 3.041571429 ... sum of all 96 tokens = 995.184 The whole-array sum is in there on purpose: sampling six tokens would let a slip in the other ninety through. Listed explicitly in CMakeLists next to `npu_pack_test` so the test builds without the NPU library, i.e. it runs on any lane. Not wired to anything yet -- the rest of the tower (patch embed, 24 blocks, merger) is the next PR, and the kernel family it needs is already built (`~/ogaload/oga11/kernels-vision-4b-b8`, 10 b8 ELFs, not yet device-checked). * npu: the vision tower's axial 2D rope, tile-and-rotate_half, pinned by goldens (engine#1503 W4) Second piece of the vision port, and the other detail that took several wrong attempts. The graph's rope is `x*cos + rotate_half(x)*sin` with `cos = Cos(Concat(a, a))`, i.e. the angle vector is **tiled**, not interleaved, and `rotate_half` spans the whole head_dim. Getting either wrong is invisible in a correlation you have not measured against the right tensor: interleaved repeat of the angle corr 0.44 tile, but rotating each half separately corr 0.99, never 1 tile + rotate_half over head_dim corr 0.999996 vs the graph's own post-rope q (mean|d| 3.3e-3, |ref| 1.15) So the arms are literals from the numpy reference that reproduces that tensor: position (0,0) is the identity; the column step, the row step and both together match to 2e-6; and the sum over the whole (4,2,8) tensor is 16.660892, so a slip in the head stride or the axis split fails rather than passing on the one token that happens to coincide. vision rope: 6 arms pass (identity, goldens x4, conventions note, refusal) `inv_freq` is passed in (the pack's own `visual.rotary_pos_emb.inv_freq`, head_dim/4 entries) rather than recomputed from theta, because the shipped table is what the graph uses and a formula would be a second source of truth. Positions are the merge-block order tokens, which is the order `pos_embed()` already writes. Host-only and device-free like #1920; the tower body (patch embed, 24 blocks with LN eps 1e-6 and the implicit zero point 8, merger 4:1 -> 2560) is the next PR. * npu: one vision transformer block and the merger, host float32 (engine#1503 W4) Third piece of the vision port: `block_forward()` (norm -> qkv -> rope -> attention -> proj -> residual -> norm -> fc1 -> gelu-tanh -> fc2 -> residual) and `merger_forward()` (per-token norm, view 4 tokens as 4*hidden, fc1 -> gelu -> fc2 to `out_hidden`). Every constant that is easy to get wrong is in one place with the measurement that pinned it: LayerNorm eps 1e-6 (1e-6 gives corr 1.000000 against the graph's norm, 1e-5 is 0.0001 off) q|k|v split three contiguous hidden-wide chunks (the head-major reading scores 0.41 where this scores 0.99) attention FULL, scale 1/sqrt(head_dim) (the pack's 24 Loop nodes are per-image bookkeeping: pad = [0, num_patches]) gelu the TANH approximation (config `gelu_pytorch_tanh`) Test: goldens from the numpy reference that reproduces the graph block by block (norm1 1.000000, qkv >0.999 in every column, attention 0.999994) and the whole tower at corr 1.000000 against the fp32 oracle. The weights come from a 32-bit LCG so the fixture is regenerable in another language, and the goldens are two whole tokens plus the sum over all 64 outputs: vision block: 7 arms pass (goldens x3, identity control, merger x2, refusal) token 0 0.08357919 0.07403285 0.03188136 -0.16534122 … token 3 -0.02847019 0.34347527 0.18980209 -0.23568934 … sum over the block's 64 outputs = -2.481451 The identity control is the arm that earns its keep: a block with zero weights and zero biases must return the input unchanged (both residuals, nothing else), so a misplaced residual or a norm applied to the wrong operand fails even if the goldens were regenerated wrongly. Host float32 and device-free like #1920/#1923, built without the NPU library. Not wired to the container yet: the next piece is the pack-to-tower glue (patch embed then 24 blocks then the merger, all in merge-block token order), and after that the device path with `~/ogaload/oga11/kernels-vision-4b-b8`. * npu: assemble the vision tower -- patch embed, N blocks, merger, in merge-block token order (engine#1503 W4) Fourth piece of the vision port and the one that makes the other three a tower: `tower_forward()` runs the Conv3d patch embed as a matmul (the kernel equals its stride), adds the position embedding, runs `n_blocks` of `block_forward()`, then `merger_forward()` to `out_hidden`. Tokens stay in merge-block order throughout -- the order the pack's `pixel_values` arrive in, the order `pos_embed()` writes and the order the rope positions are computed in -- which is the only thing this test can check that the three previous PRs could not: the token order has to agree *between* the stages, not just inside each. Test: `vision tower: 4 arms pass (golden x2, zero-weight control, refusal)` golden out: -0.19714565 -0.03816292 -0.08239722 0.27405666 0.34750172 -0.23880654 0.19937676 -0.04093523 golden sum: 0.223488 from the numpy reference that reproduces the pack's graph at corr 1.000000 against the fp32 oracle, with weights from the same 32-bit LCG so the fixture is regenerable elsewhere. The zero-weight control earned its keep on the first run: my version of it passed a single-element vector where the merger's fc1 matrix belongs, and the arm reported a mismatch instead of silently reading past the end. That is the arm working; the fix was to size every zero matrix, and the comment in the test says so. Two C++ traps hit again and worth naming since they cost three build cycles between them: `std::vector<float> v(size_t(n))` is a *function declaration* when `n` is a variable (the most vexing parse -- `static_cast<size_t>` fixes it), and pointers taken into a `std::vector` of `std::vector`s are invalidated by `push_back`, so the fixture allocates one flat store and offsets into it. Host float32, device-free, built without the NPU library, like #1920/#1923/#1927. * vision: one notice per file, and the block test links the position embedding it calls Review of #1930: the head failed `build` twice over, and the second one is not mechanical. 1. **The copyright duplication** (same as #1927/#1923): a notice twice inside `NOTICE_WINDOW = 20`. `--fix` repaired the files; `--check` is clean. 2. **`q4nx_vision_block_test` had no `pos.cpp` in its sources**, so the link failed -- `undefined reference to q4nx_vision::pos_embed` -- and the job died before any test ran. A block is rope AND position embedding, so the test pulls both; the executable list was missing one of them. That is a real defect in this PR, not a manifestation of the copyright one, and it would have surfaced the moment the header check stopped failing. Verified past both: all four vision targets build and pass -- vision block: 7 arms pass (goldens x3, identity control, merger x2, refusal) vision pos embed: 12 arms pass (token order, align_corners, exact copy, golden x8) vision rope: 6 arms pass (identity, goldens x4, conventions note, refusal) vision tower: 4 arms pass (golden x2, zero-weight control, refusal) Refs #1930 * vision: build the four vision tests from `onebit`, so their ctest rows exist in CI Review of #1930: the duplicate-notice fix took, and the head then failed `ci_targets_check` — all four vision test targets are `add_test`ed but `.github/workflows/ci.yml` never builds them, so the rows would never run in CI. Same wall as #1822/#1799/#1809. `add_dependencies(onebit q4nx_vision_block_test q4nx_vision_pos_test q4nx_vision_rope_test q4nx_vision_tower_test)` is the documented route (the one `onebit_npu_forward_syntax` and `route_log_test` use); it needs no workflow edit, so it also needs no `workflow` scope for the push. Verified: * `ci_targets_check --check --root .` -> "9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new"; * all four targets build and their arms pass -- vision block: 7 arms (goldens x3, identity control, merger x2, refusal) vision pos embed: 12 arms (token order, align_corners, exact copy, golden x8) vision rope: 6 arms (identity, goldens x4, conventions note, refusal) vision tower: 4 arms (golden x2, zero-weight control, refusal) Refs #1930 * vision: the tower's refusal arm observes the refusal (answers the CHANGES at a7af2fd) The arm was check(true, ...): it passed whether or not tower_forward() refused. It now fills the output buffer with a sentinel and requires it untouched, on a token count (2*T on a grid of T) that only the gh*gw check can refuse: positive and a multiple of 4, so neither tokens<=0 nor merger_forward()'s own guard stops it. With that check removed the arm fails; restored, 4 of 4 vision tests pass. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: pi-npufeat <npufeat@strixhalo> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Refs engine #1779 — the two tests in #1791 are on main but CI has never seen them, and this registers them where it will.
What I found after #1791 merged
I read the
buildjob's own log rather than assuming: 164 tests, 0 failed, 3 skipped — and neitherserve_client_hangup_cpunorserve_context_limit_cpuamong them. They sat insideif(ONEBIT_SERVE_TEST_GGUF)which is a configure-time cache path
ci.ymlnever sets (ci.ymlinstallsnumpy/gguffor a converter test and nothing else). So all nineONEBIT_SERVE_TEST_GGUFreferences inCMakeLists.txt— my two plus the pre-existingserve_e2e_hrx,serve_e2e_cpu,hrx_cpu_weight_parity— never materialise in CI. A gap in the serve family read as coverage.What changes
if(ONEBIT_SERVE_TEST_GGUF)and outsideif(ONEBIT_HRX)—ci.ymlconfigures withoutONEBIT_HRX, so inside that guard they would only exist for an HRX build.$<TARGET_FILE:onebit>resolves in any configuration becauseonebitis an unconditional target (CMakeLists.txt:97)."". That is the point: a skip in the CI table instead of a test that is not there.Verified here, both directions
Before this change an empty model failed (
model=${2:?}→ "parameter null or not set"), which is precisely the failure mode that made me not do this at midnight yesterday.What I deliberately left alone
The rest of the serve family keeps its configure-time guard. Giving
tests/serve_e2e.shan empty-model skip is its own change — it usesmodel=${2:?}too, and a half-done sweep that turns a skip into a red CI is worse than a visible gap. If this shape is right, the same two lines per script finish it.The honest limit
The tests still need a real model to say anything about behaviour, and CI has none — so in CI they are a documented skip, not coverage. The before/after the issue asks for still needs a build with
ONEBIT_SERVE_TEST_GGUFset, which is also what fault 3's embed test needs.