feat(VT-FP8-W8A8-CPU-ARM): register the static fp8 W8A8 path on the CPU backend (#468) - #842
Conversation
…on CPU Issue #468 records that the model-layer wiring of VT_GDN_FP8_ALPHA_IN_CONV has no test at any tier, and names the structural reason: the fp8 matmul registers only on CUDA, so a CPU-tier test cannot reach the path at all and the CUDA tier needs a GPU. Its "What done would look like" option 1 is a CPU registration for the fp8 matmul -- valuable well beyond that lever, because it makes the whole fp8 model path CPU-testable. This is the spec for option 1, committed before any implementation. It scopes two CPU registrations (kQuantFp8Static, kMatmulFp8Cutlass), the comment repairs they create the need for, and the consequential change to the fusion additivity test, whose three fp8-terminal recipes currently assert the full composite THROWS on CPU. That assertion is a checker's claim, so changing it takes a spec plus red-before and green-after evidence, which is what this file declares and what the implementation commit carries. Two things are stated up front rather than discovered later. First the residual gap: the model-layer entry points key on kMatmulFp8CublasLt, which deliberately stays CUDA-only, so this row makes the OP seam CPU-reachable and does not claim to make MatmulFp8CutlassD execute on a CPU queue. Second the stale comment: the op contract and the CUDA kernel both describe the quant math as x / input_scale while the code multiplies by the reciprocal, which is what upstream ships. The code is right and the comment is wrong, and left alone it invites someone to "correct" a default-ON 35B path into a divide. Also repoints the #468 roadmap row at this spec and its work branch. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
The fp8 matmul registered only on CUDA, so the fp8 model path was structurally untestable on the machine most of this work happens on: a CPU-tier test could not reach it and the CUDA tier needs a GPU. That is the reason #468 gives for the wiring shipping uncovered, and it is what this closes at the op seam. Two CPU registrations, per .agents/specs/vt-fp8-w8a8-cpu-arm.md: kQuantFp8Static -- the scale application, over the fp8-e4m3fn codec already in cpu_ops.cpp. It multiplies by the RECIPROCAL, formed once outside the loop, which is what upstream ships (common.cuh:62 `x = val * scale` with the inverse formed at common.cu:31) and what our CUDA kernel does. Not a divide: the two differ by up to one f32 ulp before the fp8 round, and near an e4m3 tie that ulp changes the emitted byte. kMatmulFp8Cutlass -- a CORRECTNESS REFERENCE. f32 accumulate, one folded alpha, naive triple loop. It makes no speed claim, nothing routes a production model through it, and it says so in the code. No CUDA behavior changes. kMatmulFp8CublasLt stays CUDA-only, so the MODEL-layer predicate still refuses on CPU; the spec records that residual gap and the new test pins it rather than letting it be assumed closed. Comment repairs, in-flow under #468. Three comments said "CUDA only" and become false here. A fourth was ALREADY false and matters most: include/vt/ops.h and cuda_matmul_fp8_cutlass.cu both describe the quant as `x / input_scale` while the code three lines below multiplies by the reciprocal. The code is right and the comment is wrong; left alone it invites someone to "correct" the code on a default-ON 35B path. Both now carry the upstream anchor and say not to. Gate: tests/vt/test_ops_fp8_cpu.cpp. G1 is BITWISE with zero tolerance against an independently written reference -- it enumerates all 128 finite e4m3fn magnitudes, decodes each from the field layout, and picks nearest-even by scanning, which is a different algorithm from both F32ToFp8 (frexp/nearbyint) and vllm::F32ToF8E4M3, so agreement is evidence rather than tautology. Not expressed as doctest Approx, whose scale term defaults to 1.0 and would give a byte compare a ~1.19e-5 floor. G3 checks the GEMM against a double reference that reproduces upstream's LOSSY pipeline -- clamp, e4m3 RNE, dequant -- because an exact-arithmetic reference would let a wrong implementation look better than upstream and pass. G2 (CPU vs CUDA, byte for byte) is committed and CUDA-gated; it is PENDING, not skipped, on this GPU-less host and says so in its output. test_fused_chain_additivity asserted, for all three fp8-terminal recipes, that the full Tier-0 composite THROWS on CPU. Registering the terminal makes that false. The assertions are not deleted to go green -- each is replaced by the strictly stronger byte-exact check the registration makes available, the prefix check is kept beside it so a future tail regression is localised, and cpu_full flips to true for those three rows. Assertions go 19 -> 25. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
The mutation series killed every mutant, but M2 (divide instead of reciprocal-multiply) died by 2 assertions, and measuring WHY showed the margin was thinner than the green suggested. `x/s` and `x*(1/s)` agree on almost every input. Over 20000 random values in [-2,2] they never disagree at ANY scale tried. The difference is only visible where an input lands on an e4m3 tie after scaling, which is what G1's constructed tie population exists for -- and even then it is scale-dependent: over that population 10 of 18 candidate scales expose it at all, and of the five G1 shipped with, only 0.0092 did, at 24 of 209 words. So the assertion that keeps the reciprocal form -- the one the repaired comment points at, guarding a default-ON 35B path -- was one scale-list edit away from being silently disarmed. 0.13 (78/209) and 0.77 (82/209) are the strongest detectors measured and are added with that measurement written down beside them, so a later reader prunes the list knowing what it costs. Assertions 44 -> 56, still 4 cases. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
G1 and G3 PASS; G2 (CPU vs CUDA, byte for byte) is PENDING on this host because it has no GPU, and is recorded as owed rather than counted. Beyond the mutation table, three things the series measured that the pass/fail column does not carry: M1 moves ~99.7% of bytes at every scale but 1.0 (where ignoring the scale is correctly a no-op); M2 is nearly invisible and dies only because the population contains constructed exact ties AND the scale list contains a detector; and F32ToFp8 saturates in two places, so a reviewer mutating only the obvious guard would wrongly read the gate as blind. Also records that M1/M2/M5a first failed to BUILD on -Werror rather than to assert, and were re-expressed before being counted as verdicts. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
Merges the immutable SHA 5da1d7f, not the moving ref: origin/main advances under a shared checkout, and a merge recorded against a ref nobody can pin again is not reproducible. Seven commits land, two of which touch files this branch also touches: .agents/roadmap_v1.md -- a keyed record. Resolved by taking main's version WHOLESALE and reapplying only this branch's scoped edit (the #468 row, which is repointed at .agents/specs/vt-fp8-w8a8-cpu-arm.md and this work branch). Never an automatic three-way merge of a keyed record. tests/CMakeLists.txt -- append-only in both directions: main adds the campplus and offload-config registrations, this branch adds test_ops_fp8_cpu. Union of the two additions, no registration dropped. Nothing incoming touches the fp8 seam: no change to src/vt/, include/vt/, the fused-chain catalog, or any fp8 kernel. The gate is re-run on the merge result regardless, because merge-tree CLEAN is not merge-tree BUILDS. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
… it is measured The implementation commit said the fused-chain additivity test went 19 -> 25 assertions. The 19 was wrong; the measured base count is 21. Corrected in the spec rather than by rewriting pushed history. More useful than the number: checking out the BASE version of that test -- the one carrying the three CHECK_THROWS -- and running it against the new CPU registrations gives 21 assertions, 18 passed, 3 failed, Status FAILURE. Exactly three failures, exactly the three CHECK_THROWS, with the other 18 untouched. That makes the test change a FORCED consequence rather than a judgement call: registering kQuantFp8Static on CPU makes those three claims false, so they had to change, and they were replaced by the stronger byte-exact assertion rather than deleted. Anyone reviewing whether this was scope creep can now re-run that one command instead of taking the argument on trust. Also records that the whole mutation series was re-run on the post-merge tree with identical results. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
Merges the immutable SHA 3ce5a1d rather than the moving ref, so the merge point can be pinned again later. Nine commits land. None touches the fp8 seam: src/vt/, include/vt/, the fused-chain catalog and every fp8 kernel are untouched by main. Two record surfaces overlap, and are resolved the way the protocol requires: .agents/roadmap_v1.md a keyed record. Main's version is taken WHOLESALE and only this branch's scoped edit is reapplied -- the #468 row, repointed at .agents/specs/vt-fp8-w8a8-cpu-arm.md and this work branch. Never an automatic three-way merge of a keyed record. tests/CMakeLists.txt append-only on both sides. Union of the additions, with no registration dropped. What makes this merge worth more than freshness: POLICY-SINGLE-PR-AND-STYLE lands AGENTS.md changes, .agents/style/commits.md, .agents/style/prose.md, scripts/check-commit-style.py and a pull request template. Absorbing it means this branch is gated by the checker that will judge it, rather than by the one that existed when it was written. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [1m] [claude-code]
|
Supersedes #826 (closed). Same content, trailer-repaired history; the tree is byte-identical to |
G2 (CPU vs CUDA byte agreement) — recorded as PENDING, not passed, not skippedI attempted the owed G2 gate on GB10 ( Attempt 1 — VOID. Built with This is not a defect in this PR. The gate was therefore comparing CPU against the CPU fallback. Had it not segfaulted it would have passed byte-for-byte while proving nothing. Filed separately as #844: the reference-tier fallback should refuse by name when handed non-CPU tensors instead of dereferencing them, and its "correct but slow" message is wrong when the device is not CPU. Attempt 2 — no verdict. Re-run with I am not attempting it a third time right now. GB10 is the gate host for the NemotronH end-to-end work, and risking it again for a non-blocking gate is the wrong trade. G2 stays PENDING, owed, with the recipe recorded for whoever runs it next: What this does and does not leave unproven. G1 already pins the CPU quantizer bitwise against a reference derived independently from the e4m3 format, and this PR does not touch the CUDA kernel — so a later G2 red would indicate pre-existing CPU/CUDA divergence rather than a regression introduced here. That reasoning was written into the PR body before either attempt, which is the right order. Do not read this PR as having proved CPU/CUDA byte agreement. |
…the relocated issue table Merges the immutable SHA 2f2bce9. The branch was CONFLICTING against main: `git merge-tree --write-tree origin/main 810182e` exited 1 on `.agents/roadmap_v1.md`. The conflict was not a text collision. The intake table MOVED: main relocated it out of `.agents/roadmap_v1.md` into `.agents/issue-index.md`, and `2f2bce926` carries no #468 row in the roadmap at all. Reapplying my edit where I authored it would have resurrected a row that main deleted, which is the duplicate a move-plus-edit produces when both sides merge cleanly. So the edit MOVES with the table: .agents/roadmap_v1.md target taken WHOLESALE, byte-identical, my edit NOT carried here. The table no longer lives in this file. .agents/issue-index.md target taken WHOLESALE, then ONLY the #468 row reapplied. The row text now names `row/VT-FP8-W8A8-CPU-ARM-V2`, which is the branch that actually carries the work. tests/CMakeLists.txt append-only both sides; union, nothing dropped. Verified four ways on the keyed record, because a clean auto-merge can silently interleave rows and still satisfy the obvious checks: (1) the diff against the target is EXACTLY one '-' and one '+' at the SAME line index, with the file length unchanged. The reviewer's check is stated as "the target is a strict PREFIX of the result", which assumes the branch only APPENDS; my edit modifies one row in place, so a prefix test cannot hold and would pass vacuously. This is the strictly stronger form: it forbids an interleave, a reorder, an insertion and a deletion simultaneously. (2) my #468 row is byte-identical to the row I authored. (3) all 216 keys are byte-identical to the target except mine, and the key SET is unchanged: none dropped, none added. (4) no key appears twice. Nothing incoming touches the fp8 seam. src/vt/, include/vt/, the fused-chain catalog and every fp8 kernel are untouched by main. The gate is re-run on the merge result regardless, because merge-tree CLEAN is not merge-tree BUILDS. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…make G2 selectable Review findings F3, F4, F5 and F6 on PR #842. F6 is the one that mattered most. F6. The G2 case was named "G2: CPU QuantFp8Static == CUDA QuantFp8Static, byte for byte". doctest splits `-tc=` on commas, so that name was UNSELECTABLE: the filter became two patterns that each match nothing. Selecting the one gate this row still owes, by its exact shipped name, measured [doctest] test cases: 0 | 0 passed | 0 failed | 4 skipped [doctest] assertions: 0 | 0 passed | 0 failed | [doctest] Status: SUCCESS! exit 0 A gate that examines nothing and reports success is the worst failure mode available, and it was sitting on the arm nobody has run yet. Renamed comma-free; the same selector now returns `test cases: 1 | 1 passed`. The whole file stays 4 cases / 56 assertions. The reason is written next to the case, not only in the spec, because it generalizes to every case name in the tree. F3. Two shipped comments asserted CPU/CUDA equivalence as FACT while G2 has never run: cuda_matmul_fp8_cutlass.cu called the CPU arm "the byte-for-byte mirror of this kernel", and ops.h said it "agrees with the CUDA kernel to fp8/bf16 tolerance". Both now say the equivalence is DECLARED AND OWED under G2, and both name what is actually measured instead: G1 proves the CPU kernel matches an independent e4m3 reference derived from the format, and two implementations each matching a reference is a weaker claim than the two matching each other. This is the row's own thesis turned on the row -- it repaired a comment asserting an unverified contract, then shipped two more. F4. The spec's reference-tier risk row reasoned from the ANNOUNCEMENT and never tested the CONSEQUENCE. MaybeInstallReferenceTier declines only while the CPU provider count is zero (op_provider.cpp:213-214); these registrations make it one, so on a unified-memory device those ops flip from refusing by name to installing a host kernel over device pointers. Measured: SIGSEGV on GB10, exit 139 (#844). The "correct but slow" banner is not mitigation, it is a misleading label. Recorded as debt this row CREATES; #844 owns the fix, which is broader than this row. F5. "Option 1" is softened wherever it appeared. What landed is the OP seam. The GEMM registered here is kMatmulFp8Cutlass while the model predicate keys on kMatmulFp8CublasLt, so the mixed_scale wiring #468 set out to cover remains unreachable -- which §Residual gap said from the start, and the summary prose should not have implied otherwise. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…ting its row `scripts/check-issue-index-append-only.py` refused this branch: FAIL: .agents/issue-index.md is append-only, and this range removes or edits lines. A union merge duplicates an edited line instead of merging it. The file says so in its own preamble, and `.gitattributes` gives it `merge=union`. I edited the #468 row in place. That is the keyed-record discipline, and it is the correct discipline for `.agents/roadmap_v1.md`, which is where that row lived when this branch started. Main RELOCATED the intake table into `issue-index.md`, and the relocation changed the file's merge CONTRACT as well as its path: keyed becomes append-only. I carried the old discipline across with the row and did not check whether it still applied. Worth recording because the review told me how to catch this and I reasoned it away. The prescribed check was "the target's file is a strict PREFIX of the result", which is exactly the right test for an append-only file. I judged it vacuous because my edit modified a row in place rather than appending, and substituted a weaker in-place-diff check that my edit passed by construction. The unadapted check would have failed and would have been right. The fix is to make no edit at all. `.agents/issue-index.md` is now byte-identical to `2f2bce926`. Nothing is owed here: main's existing #468 row already carries the three-way linkage AGENTS.md requires, because the issue is linked from the index, from `.agents/specs/vt-fp8-w8a8-cpu-arm.md`, and from the PR body. The spec link I was adding to that row was enrichment, not the obligation, and it is not worth a duplicate row in an append-only log. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…d the numbers that stand Two defects this row found in its own EVIDENCE rather than in the code. Both made a green result mean less than it looked, so both are written down. Every full gate before the last one configured VLLM_CPP_BUILD_EXAMPLES=OFF and VLLM_CPP_SERVER=OFF. I chose those flags while the box was at 100% disk and then never revisited them. CI lets both default ON. The reduced form builds 925 targets and runs 463 tests; the CI form builds 1422 and runs 481. That gap is not cosmetic: test_minimax_music3_e2e_real links ApiServer and cannot BUILD without the server, so the reduced gate printed "100% tests passed" over a target it had never compiled. The binding numbers are now the CI-configuration run alone: build exit 0 at 1422 targets with 0 warnings, ctest 481/481 with 0 failures. The second is the merge contract. When main relocated the intake table from roadmap_v1.md into issue-index.md it also changed that record from keyed to append-only, and I carried the keyed discipline across with the row. The prescribed verification was "the target's file is a strict PREFIX of the result", which is exactly the append-only test; I judged it vacuous for an in-place edit and substituted a weaker check that my own edit satisfied by construction. The unadapted check would have caught it. Recorded next to the row it bit. Also classifies the 7 preflight gates still red as BASELINE: all seven fail at 2f2bce9 itself in a clean worktree, and this branch adds none. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
G2 — RUN, and it PASSES. The gate is no longer owed.Operator run on GB10 ( The proof G2 actually executed rather than printing its PENDING banner is the assertion count: 56 on a CPU-only host, 60 here. The four extra assertions are G2's. The word And the comma rename is load-bearing: Three earlier attempts, and why each was VOID rather than a verdictRecording these because each failed in the direction that looks like a pass, and the recipe below is what a future runner needs.
The working recipe# CUTLASS v4.5.0 (the tag CMakeLists.txt:557 pins) staged host-side at /home/mudler/cutlass,
# because dgx has no egress. Transferred from a box that does.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DVLLM_CPP_CUDA=ON -DVLLM_CPP_CUDA_ARCHITECTURES=121a \
-DVLLM_CPP_CUTLASS_DIR=/w/cutlass
grep 'cutlass-fp8' cfg.log # must read ENABLED; DISABLED VOIDS the gate, it does not fail it
ninja -C build -j 4 test_ops_fp8_cpu # unconstrained parallelism OOM-reboots this boxMount What this does and does not settleIt settles the equivalence the PR declared and owed: the CPU arm and the CUDA kernel agree byte-for-byte on the tested population, on real sm_121a hardware with the fp8 CUTLASS feature genuinely enabled. It does not retroactively validate anything else. G1's independence, the M2 scale characterisation and the mutation table stand on their own evidence, reviewed separately. The comments repaired under F3 should now be revisited: they currently say the equivalence is declared and owed under G2. It is measured. Whoever lands this can state that plainly. |
A1 (#903) and the FP8 CPU arm (#842) landed while this row waited, and A1 rewrote both doc rows this branch edits. docs/USAGE.md and docs/FEATURES.md resolved BY HAND: origin/main's rows taken wholesale, then this row's SCOPED edit reapplied on top. USAGE "a GGUF file is refused by name" -> names the architectures and the real dispatch (#809). Main's newer text about the loader and the engine-construction change is preserved verbatim. FEATURES main's rewrite DROPPED the standing "GGUF owed" debt fact -- flagged as a LOW finding in #903's review. Restored here in this row's stronger form: a `nemotron_h*` GGUF refuses BY NAME (#809). Reapplying either edit where it was authored would have reverted main's newer statements about the weight loader and the paged runner. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
Three commits landed on main during this row's gate: VT-FP8-W8A8-CPU-ARM (#842) and the two MODEL-NEMOTRON-H changes (#872, #903). None touches this row's code. `docs/FEATURES.md` and `docs/USAGE.md` AUTO-MERGED, and an automatic three-way merge of a keyed record is exactly what the rule forbids accepting on trust. So both were verified by hand against the target-branch version rather than by the merge reporting success: FEATURES 366 of 368 lines byte-identical with only 165 and 337 moving, USAGE 3117 lines byte-identical, and zero unrelated keys touched in either. Main had not edited either file, so the merge was trivial - but that is a conclusion from checking, not an assumption that let the check be skipped. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
GitHub reported the pull request CONFLICTING while `git merge-tree` reported the same pair clean, which is #883: the union driver `.gitattributes` declares for `.agents/issue-index.md` is a local driver, and the forge does not run it. The only file both sides touched is that index, and both sides only appended to it. Merging here applies the driver and leaves the forge nothing to resolve. Also brings in #842's fp8 W8A8 CPU path, so the gate reruns against it rather than against the base this row branched from. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…seline caught a segfault on main (#699, #955, #960) Option B of the dots3-note spec §6.4 needs somewhere our own arm can run, and Thor at 192.168.68.23 is the only non-GB10 CUDA box we have. It had no CUDA toolkit, no cmake and no nvcc, and it cannot get them: `/` is a read-only 4.4 G ext2 loop on an immutable Kairos image with 1.3 G free, so `apt install` into it does not exist as an option. The toolchain therefore lives in a container pinned by DIGEST — the `13.0.1` tag moves — and `/home` (362 G free) holds the checkout. The three flags that make the GPU visible are `--runtime=nvidia`, `-e NVIDIA_DISABLE_REQUIRE=1` and `sudo -n`. The box's own `/home/mudler/_build_thor.sh` asserts the runtime "REFUSES this image on its driver" and omits `--runtime` on that basis; it is wrong, and only because it also omits the DISABLE_REQUIRE flag. With all three, `cudaGetDeviceCount` returns 1 and an `nvcc -arch=sm_110` kernel launches and returns correct values. Verified rather than assumed, because a build that quietly disables CUDA is the failure mode here: configure prints `CUDA target architectures: 110`; all 30 `*.cu.o` contain exactly one cubin each and every one is `sm_110`; `libvllm.so` links `libcudart.so.13`. 30 of 53 `.cu` files is CORRECT — `fa2`, `cutlass-nvfp4`, `cutlass-fp8`, `scaledmm-c3x-*` and `fp4-mma` all resolve DISABLED for `[110]` and only `marlin-nvfp4` is ENABLED, so the feature table narrows the TU set on purpose. On the device, `test_cuda_backend` reports `sm_110`, `integrated=1`, `UnifiedMemory=true`, 25/25. THE BASELINE MOVED WHILE THIS PR WAS OPEN, AND THAT IS THE HEADLINE. Measured at 5a0ffe9 it was 484 tests / 14 red. Re-measured on the same box in the same container after rebasing onto 2daa328 it is 485 tests / 15 red, and the FP8 group changed CHARACTER: `vt: no kernel for op QuantFp8Static (id 52) on device cuda` — a loud, correct refusal — became `[vt reference-tier] ... running the PORTABLE CPU fallback` followed by SIGSEGV. Five clean refusals became five crashes and #842's new `test_ops_fp8_cpu` added a sixth, crashing at :279 in the case named "G2: CPU QuantFp8Static equals CUDA QuantFp8Static byte for byte". `cutlass-fp8` is ENABLED on GB10 and DISABLED for [110], so the native kernel exists on the gate host, the fallback is unreachable there, and nothing in CI could see it. Filed as #960; not fixed here, because it is someone else's kernel row and wants its own spec. The rest of the 15 are recorded as #955, the sm_110 counterpart of #907, and nothing is fixed among them either — knowing which tests are red on sm_110 IS the deliverable. Four throw `built without the vendored FlashAttention-2`, two hardcode GB10 (`test_platform` asserts capability family 120; `test_op_parity` replays a dgx-captured hash in a case that names itself dgx-only and runs anyway), and `test_capi`/`test_linear_method` are already red on GB10. The one substantive standing finding is `test_ops_moe_grouped` at bitdiff=15/32768 on a Marlin NVFP4 cell that IS enabled for [110] — a live kernel disagreeing with itself across block sizes, not an absent feature. So the spec's W0.5 gate as written — "the existing suite passes there" — is not met and was the wrong gate: it would either block every brick forever or invite someone to weaken a test. The spec now carries the replacement and the reasoning for it, so a future agent reads it there rather than in a PR body: a row regresses on Thor only if it lengthens the list. And because the list is not constant, the spec also says to re-measure whenever the base moves across `src/`, `tests/` or `CMakeLists.txt`. One trap cost a false red. `test_serve_low_tools` failed the first run with `FileNotFoundError: 'shellcheck'` — `tests/tools/test_online_gate_startup.py:260` shells out and raises instead of skipping when the binary is absent, an absent instrument reading as a code verdict. `shellcheck` is in the recorded Dockerfile so it passes here, and the record says plainly that this HIDES the harness defect rather than fixing it. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
… a second model can reach (#940) (#973) Closes #940. Spec: [`.agents/specs/vt-fp8-shared-seam.md`](https://github.com/mudler/vllm.cpp/blob/row/VT-FP8-SHARED-SEAM/.agents/specs/vt-fp8-shared-seam.md). Owning row: `MODEL-TEXT-nemotron-h-nemotron-hfor-causal-lm` (#517), which is what forces it. ## What was wrong AGENTS.md §"Shared seams": *"If a shared seam cannot represent the upstream behavior, extend it or record one exact tracked exception. Never hand-roll a parallel path."* NVFP4 honoured that. FP8 W8A8 did not: `Fp8Weight` was in a header, but everything that made it usable sat in the anonymous namespace of one `.cpp`. | Entry point | Was (`qwen3_5.cpp @ c7cb59f`) | Is | |---|---|---| | `ResidentFp8` | `:1458` | `models/dense_fp8_gemm.h` | | `DenseCublasLtFp8Enabled` | `:1478` | `models/dense_fp8_gemm.h` | | `MatmulFp8CutlassD` | `:1495` (guard `:1497-1498`) | `models/dense_fp8_gemm.h` | | `MatmulFp8CutlassPreQuantD` | `:1517` (guard `:1519-1520`) | `models/dense_fp8_gemm.h` | | policy layer | did not exist | `layers/quantization/fp8.h` (`Fp8W8A8LinearMethod`, `MakeLinearMethod`) | A second model had three options and the policy forbids two. #517 is that second model: 46 FP8 W8A8 mamba projections, 36.6% of decode bytes and 27.6% of GEMM FLOPs, and its `in_proj` produces the fused `zxbcdt` the conv and the SSD scan consume, so the block cannot be split and has no device path at all without this. ## Correspondence with `dense_nvfp4_gemm.h` | `dense_nvfp4_gemm.h` | `dense_fp8_gemm.h` | |---|---| | preamble: upstream chain, scope, known duplication | same shape, chain cites `compressed_tensors_w8a8_fp8.py:60,201-207`, `modelopt.py:444,531-537`, `fp8.py:267,446` | | `MarlinW4A16Enabled()` lever | `DenseCublasLtFp8Enabled()` lever (same spelling, same default) | | `ResidentNvfp4` `:294` | `ResidentFp8` | | `MatmulNvfp4MarlinD` `:506` | `MatmulFp8CutlassD` | | `MatmulNvfp4W4A16D` `:730` dispatcher | `MatmulFp8CutlassPreQuantD` (the `QuantizedActivation` overload) | | `schemes/nvfp4.h:35` `Nvfp4W4A16LinearMethod` | `quantization/fp8.h` `Fp8W8A8LinearMethod` | | `schemes/nvfp4.h:97` `MakeLinearMethod` | `quantization/fp8.h` `MakeLinearMethod` (overloaded on the weight type) | Two deliberate divergences, both argued in the spec: 1. **Path is `quantization/fp8.h`, not `quantization/schemes/fp8.h`.** #940 writes "schemes/nvfp4.h" for a file that lives at `compressed_tensors/schemes/nvfp4.h`, so it is shorthand; inventing a `quantization/schemes/` directory mirrors nothing upstream. `fp8.py` sits directly under `quantization/` in vLLM, and `layers/quantization/modelopt_mixed_precision.h` is the local precedent. 2. **The seam is templated on `Dev`/`DBuf`.** `qwen3_5.cpp` keeps its own anonymous-namespace copies of those types (the KNOWN DUPLICATION `dense_nvfp4_gemm.h:45-50` records). A non-template header could only have been *copied* into that file, which is the seam-dead-beside-the-path failure #940 exists to prevent. One definition, two instantiations instead. ## The call-site change `qwen3_5.cpp` loses 69 lines and gains a `using` plus two one-line adapters. Its ~14 call sites are unchanged text. ```c++ using dense_fp8::DenseCublasLtFp8Enabled; DBuf MatmulFp8CutlassD(Dev d, const Tensor& x, const Fp8Weight& w, DType out_dtype) { return dense_fp8::MatmulFp8CutlassD<DBuf>(d, x, w, out_dtype); } DBuf MatmulFp8CutlassPreQuantD(Dev d, const Tensor& a_fp8, const Fp8Weight& w, DType out_dtype) { return dense_fp8::MatmulFp8CutlassPreQuantD<DBuf>(d, a_fp8, w, out_dtype); } ``` No numerics, tolerance, guard or dispatch condition changed. The CUDA-only `VT_CHECK` travelled with the code it guards. ## Gate: Qwen3.5 byte-identity Baseline measured in a clean detached worktree at `c7cb59fbb`; after at `a0693813a`. Both on the CPU-only dev box, so `test_linear_method` reads without its two `VT_MARLIN_NVFP4` cases — the dgx shape is 10 / 97 and appears in the mutation table below. Identical counts for every pre-existing suite. | Suite | Before | After | |---|---|---| | `test_qwen3_5_gdn_spec_routing` | `test cases: 6 \| 6 passed`, `assertions: 52 \| 52 passed`, `SUCCESS!` | 6 / 52, `SUCCESS!` | | `test_ops_fp8_cpu` | 4 / 56, `SUCCESS!` | 4 / 56, `SUCCESS!` | | `test_qwen27_paged_forward` | 29 / 765, `SUCCESS!` | 29 / 765, `SUCCESS!` | | `test_qwen27_dense_forward` | 9 / 583, `SUCCESS!` | 9 / 583, `SUCCESS!` | | `test_linear_method` (CPU box) | 6 / 76, `SUCCESS!` | **8 / 88**, `SUCCESS!` (two new cases, declared additive) | Full CPU gate, run THREE times — once on the extraction commit and again after each pinned merge of `origin/main`, because a merge that brings in new source is a new binary and the earlier green does not cover it: | At | build | `ctest -j 4` | time | |---|---|---|---| | `a0693813a` (extraction) | exit 0, **0 warnings** under `-Werror` | **485/485 passed, 0 failed** | 828.96 s | | `018c9d1cb` (post-merge, `origin/main` @ `e5351776c`) | exit 0, **0 warnings** under `-Werror` | **485/485 passed, 0 failed** | 682.90 s | | `66c1e805c` (post-merge, `origin/main` @ `c90e3fc02`) | exit 0, **0 warnings** under `-Werror` | 487/488 passed, **1 failed** | 837.59 s | All three skip the same 2 absent-fixture tests (`test_modelopt_mixed_precision_checkpoint`, `test_voxtral_e2e`); the third run's 488 is 485 plus the 3 tests `c90e3fc02` brought in. **That one red is #294, and it was re-run before being called that.** `.agents/verification.md`: *"tests that starve under `ctest -j` are re-run serially before being called a regression."* #294 records `test_async_llm` reusing an aborted request id as racing the core abort at a **26% failure rate under contention, on main**. It PASSED in both earlier full runs of this same tree (0.14 s, 0.37 s) and failed in 0.31 s only in the third, which ran with the box at load average 29-35 from concurrent sessions. Re-run serially: **3/3 green, 15/15 assertions each**. Disk on the build host: 91% before, 99% at peak, 93% after, build tree removed afterwards. ## Mutation table: the seam is LIVE, not dead beside the path Every row rebuilt from source, `compile_exit` and `error:` count reported, binary sha distinct from baseline, case count non-zero. No `TEST_CASE` name contains a comma. **Every arm below ran on `dgx.casa`**, GB10 sm_121a, CUDA 13.0.88, image `vllmcpp-build:gb10`, `-j 4`, `$HOME/gpu.lock` held with `flock -n` for every test run. Both configure logs printed `CUDA feature cutlass-fp8: ENABLED for [121a]`, `cutlass-nvfp4: ENABLED for [121a]`, `fa2: ENABLED for [121a]`, `CUTLASS found at /cutlass` and `Triton AOT: ... sm_121a` — so no arm is voided. The seam header was restored byte-for-byte after every arm and verified against a pristine `sha256`, and each series' BASE re-run reproduced its own first binary sha exactly. CPU arms are `test_linear_method`; GPU arms are `test_qwen3_5_gdn_spec_routing`. **Each kind carries its own BASE control measured in the same tree**, because a mutation row on a box with pre-existing failures cannot be read without one. Two trees, stated per row. **T1 = `a0693813a`** (the extraction) carries CONTROL, BASE, M3, M4, M5, M7, M8. **T2 = `32980afda`** (this branch's reviewed head, transferred with `git archive`, clean configure + full build, `build rc=0`, 0 `error:`, 0 `warning:`) carries GDN_BASE, LM_BASE, M1, M2 and M9. Binary shas are comparable WITHIN a tree, never across the two. | Arm | Tree | Perturbation | `compile_exit` | `error:` | binary sha16 | `[doctest] test cases:` | `[doctest] assertions:` | `Status:` / exit | |---|---|---|---|---|---|---|---|---| | **CONTROL** (GPU) | T1 | none, and `qwen3_5.cpp` replaced by its `c7cb59fbb` (pre-extraction) content in the SAME tree, SAME flags | 0 | 0 | `8b740f86eeb7da5d` | `12 \| 11 passed \| 1 failed` | `123 \| 119 passed \| 4 failed` | `FAILURE!` / 1 | | **BASE** (GPU) | T1 | none | 0 | 0 | `090bc6e47a478cb2` | `12 \| 11 passed \| 1 failed` | `123 \| 119 passed \| 4 failed` | `FAILURE!` / 1 | | M3 (GPU) | T1 | drop the `input_scale`: `QuantFp8Static(..., w.input_scale)` → `..., 1.0F` | 0 | 0 | `91cfeeb337fec5a3` | `12 \| 11 passed \| 1 failed` | `123 \| 107 passed \| 16 failed` | `FAILURE!` / 1 | | M4 (GPU) | T1 | change the alpha fold: `w.alpha` → `w.alpha * 2.0F` in `MatmulFp8CutlassD` | 0 | 0 | `6a7fb2f31de34576` | `12 \| 11 passed \| 1 failed` | `123 \| 107 passed \| 16 failed` | `FAILURE!` / 1 | | M5 (GPU) | T1 | same alpha fold in `MatmulFp8CutlassPreQuantD` | 0 | 0 | `ecebc93903e7d801` | `12 \| 11 passed \| 1 failed` | `123 \| 119 passed \| 4 failed` | **UNCHANGED vs BASE — negative result** | | M7 (GPU) | T1 | `VT_CHECK(false, ...)` as the first statement of `MatmulFp8CutlassPreQuantD` | 0 | 0 | `84fd9f9d2d7386f7` | `12 \| 9 passed \| 3 failed` | `103 \| 99 passed \| 4 failed` | `FAILURE!` / 1 | | M8 (GPU) | T1 | `VT_CHECK(false, ...)` as the first statement of `MatmulFp8CutlassD` | 0 | 0 | `dc8bcfc1e97fafac` | `12 \| 11 passed \| 1 failed` | `91 \| 91 passed \| 0 failed` | `FAILURE!` / 1 | | **GDN_BASE** (GPU) | T2 | none | 0 | 0 | `47a9960ac92d4b66` | `12 \| 11 passed \| 1 failed` | `123 \| 119 passed \| 4 failed` | `FAILURE!` / 1 | | **LM_BASE** (CPU) | T2 | none | 0 | 0 | `f8f5b2d3a0116980` | `10 \| 9 passed \| 1 failed` | `97 \| 95 passed \| 2 failed` | `FAILURE!` / 1 | | M1 (CPU) | T2 | delete the CUDA guard in `MatmulFp8CutlassD` | 0 | 0 | `2a3a1a8e5df413fd` | `10 \| 8 passed \| 2 failed` | `97 \| 94 passed \| 3 failed` | `FAILURE!` / 1 | | M2 (CPU) | T2 | delete the CUDA guard in `MatmulFp8CutlassPreQuantD` | 0 | 0 | `15d702dd127b1c35` | `10 \| 8 passed \| 2 failed` | `97 \| 94 passed \| 3 failed` | `FAILURE!` / 1 | | M9 (GPU) | T2 | zero the alpha fold: `w.alpha` → `0.0F` in `MatmulFp8CutlassPreQuantD` (both GEMM arms; anchor count asserted `== 2`) | 0 | 0 | `6c9335e8b6a53228` | `12 \| 9 passed \| 3 failed` | `123 \| 115 passed \| 8 failed` | `FAILURE!` / 1 | | **GDN_BASE re-run** (GPU) | T2 | none, after every T2 mutation was reverted | 0 | 0 | `47a9960ac92d4b66` (identical to GDN_BASE) | `12 \| 11 passed \| 1 failed` | `123 \| 119 passed \| 4 failed` | `FAILURE!` / 1 | **Reading it.** *The BASE red is inherited, not introduced.* `test_qwen3_5_gdn_spec_routing` reads 119/123 on GB10 at `main`, which is exactly what #907 already records for this box. The CONTROL row proves it in the same tree rather than by citation: pre-extraction `qwen3_5.cpp`, same build directory, same flags, distinct binary, and the result is identical down to the individual mismatch counts (`30504`, `48756`, `30504`, `48756`) and the same four `H=5120 / T=3` combinations. **That equality is the byte-identity evidence** — the extraction reproduces the production numerics including a pre-existing defect. *The seam is on the live Qwen3.5 path.* M3 and M4 each take the failures from 4 to **16** (every combination in the case), from a source change inside `dense_fp8_gemm.h`, with a clean compile and a distinct binary. The split arm of `ProjectGdnFp8QkvzForTest` (`qwen3_5.cpp:6693-6694`) calls the extracted `MatmulFp8CutlassD` while the merged arm does not, so perturbing the seam breaks their bitwise equality. A seam sitting dead beside the model cannot do that. *M5 is a negative result and is reported as one.* Doubling alpha in the pre-quantized arm changed nothing. M7 shows the arm is nevertheless REACHED: forcing that function to throw turns **two additional cases** red — `GDN gate POLARITY on the FP8 tail: GdnBlockPaged (CUDA)` and `... the MIXED spec batch (CUDA)`. *M9 is why nothing SAW the doubling, and it corrects the first reading of M5.* The mechanism is **scale invariance of each predicate, not cancellation between the two arms** — those two cases compare their arms against nothing at all. Each arm has its own predicate: `CHECK(silu_nonzero == 0)` (`tests/vllm/models/test_qwen3_5_gdn_spec_routing.cpp:895`), invariant under ANY non-zero rescale, and `CHECK(max_sigmoid > 0.0)` (`:904`), invariant under any POSITIVE one. `w.alpha * 2.0F` is positive and non-zero, so M5 could not have moved either. M9 sets that same alpha to `0.0F` — a factor COMMON to both arms, which the cancellation reading predicts would stay green — and it turns those two cases RED. The failure detail is what settles it: the four NEW assertion failures are all `:904 CHECK( max_sigmoid > 0.0 )` with `values: CHECK( 0 > 0 )` and `max_sigmoid := 0` (two dims each in both fp8 polarity cases), while `CHECK(silu_nonzero == 0)` at `:895` stays green with `silu_nonzero := 0`. The pre-existing four at `:525` (`30504`, `48756`, `30504`, `48756`) are untouched, which is how `123 | 119 passed | 4 failed` becomes `123 | 115 passed | 8 failed`. What the suite therefore owes is a gate sensitive to any POSITIVE rescale of that arm — see §Owed. *M8 is also the reason to read cases and not just assertions.* It prints `assertions: 91 | 91 passed | 0 failed` — and `Status: FAILURE!` with exit 1, because the throw aborted the case before its `CHECK`s ran. An assertion-only reading of that line would have called a fully-blocked GEMM a pass. *M1 and M2, corrected.* As first recorded these two rows were attributed to `dgx.casa` and read `8 | 7 passed | 1 failed` / `88 | 87 passed | 1 failed`. That is the CPU-ONLY box's shape, not this host's: `test_linear_method` puts 2 cases and 9 assertions behind `VT_MARLIN_NVFP4` (`tests/vllm/model_executor/layers/test_linear_method.cpp:105-272`), so a CUDA build here reads 10 cases / 97 assertions, and the 2 pre-existing failures sit inside one of those Marlin cases — `linear_method: MXFP4 fused gate_up ~= split`, both at `:247 CHECK( after == before + 1 )` with `values: CHECK( 0 == 1 )`. Those two are #907's `test_linear_method 83 of 85` plus this row's 12 additive assertions, all passing (85 + 12 = 97, 83 + 12 = 95). The rows above are RE-RUNS on `dgx.casa` against the `LM_BASE` control measured in the same tree, and the conclusion is unchanged: each guard deletion turns exactly ONE further case red, `linear_method: the fp8 w8a8 method reaches the shared seam in both arms`, at the `CHECK_THROWS_WITH_AS` that pins that guard's message (`:540` for M1, `:550` for M2) — the deleted guard lets the call reach `vt::MatmulFp8CublasLt`, which throws a different message. *M6 is unused, and no arm is missing.* The labels run M1-M5 and M7-M9. Nothing was measured under an M6 label and nothing was withheld: the number was skipped when the GPU arms were planned, and the gap went unexplained until this round. It stays unused rather than renumbered, because `73d67f9d2` and this row's fresh review both cite M7 and M8 by those labels, and renumbering would silently break every citation. M9 continues the sequence. ## Found and deliberately NOT fixed `ResidentFp8` (and its siblings `ResidentFp8Qkv` / `ResidentFp8Qkvz`) `Alloc` + `Copy` the fp8 weight bytes to the device **without** `vllm::load_stats::AddDeviceUpload` and **without** the post-upload `AdoptDeviceBytesAsHost`. Every other resident-weight helper in the same file does both: `ResidentWeight` (`qwen3_5.cpp:1008,1015 @ c7cb59f`) and `ResidentNvfp4` (`:1105,1110,1115,1120`), and `dense_nvfp4_gemm.h:294-328` carries the comment explaining why the pair is mandatory (ENG-LOAD-DIRECT-UPLOAD, #150). So the 35B fp8 tower's upload is missing from load accounting and its pages are never re-tagged, which is the shape of the GB10 weight-residency ATS penalty. The accounting half is BOUNDED and #974 says so: `load_stats` has exactly one non-test consumer, `PrintLoadBytes` (`src/vllm/entrypoints/model_loader.cpp:133`), a stderr diagnostic behind `LoadStatsEnabled()`, so the under-report costs a diagnostic and not a decision. The residency half carries no such bound. Carried across unchanged: repairing it inside an extraction is exactly the behaviour change a byte-identity gate cannot see. Recorded in the spec under §Found, not fixed; filed as **#974**, which also names extending `scripts/check-fp4-resident-consistency.py` — today the same invariant for `ResidentNvfp4` alone — as the gate the fix should carry. Separately, **not a new find**: the CUDA-only refusal keys on `kMatmulFp8CublasLt` while the ops it would run (`kQuantFp8Static`, `kMatmulFp8Cutlass`) have CPU reference arms since #468/#842. That is already recorded in `vt-fp8-w8a8-cpu-arm.md` §Residual gap and pinned at `tests/vt/test_ops_fp8_cpu.cpp:445-453`. Carried unchanged; the new CPU case re-pins it at the model tier. ## Protocol deviation: the spec was not committed before the implementation AGENTS.md §"Spec before code" requires the spec to be committed first. `.agents/specs/vt-fp8-shared-seam.md` was added in `a0693813a`, the SAME commit as the extraction, and the superseded PR #972 had the same shape. Commit order is what proves the spec came first when one PR carries both, and here it proves the opposite. It is recorded rather than repaired because it cannot be repaired: neither `main` nor a branch under review is force-pushed, so one commit cannot be rewritten into two, and back-dating a spec commit that never existed would be worse than the ordering it hides. The spec now carries a `## Protocol deviation` section stating what the deviation cost — the scope, the gate and the two divergences were argued after the code existed, so none of them constrained the implementation — and what it did not cost: the mutation table was designed and run by sessions that did not write the code, and reproduced independently by the fresh review. This is visible debt, not success. A reviewer who does not accept the argument does not merge it. ## Not in scope Wiring NemotronH to the seam (A2-Q under #517), `ResidentFp8Qkv`/`Qkvz` and the merged-QKV(z) path, and any widening of the device predicate. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Row
VT-FP8-W8A8-CPU-ARM, owned by roadmap rowPERF-27B-LMHEAD-FP4. One row per PR.Issue: #468. The issue stays OPEN. See "Honest gaps".
Before starting
src/vt/cpu/cpu_ops.cpp,include/vt/ops.handtests/vt/test_fused_chain_additivity.cpp. Only PR LTX-2.5: 21B joint video+audio DiT and the generalized video seam #437 (LTX-2.5) touchesinclude/vt/ops.h, and it adds enum entries. It does not touch the fp8 contract text. No other claim existed on VT_GDN_FP8_ALPHA_IN_CONV model-layer wiring is untestable on CPU: fp8 matmul registers only on CUDA, and ProjectGdnFp8QkvzForTest is called with default args #468.POLICY-SINGLE-PR-AND-STYLE. The spec commit precedes the implementation commit on this branch..agents/roadmap_v1.mdnow points at.agents/specs/vt-fp8-w8a8-cpu-arm.mdand at branchrow/VT-FP8-W8A8-CPU-ARM.src/vt/cpu/cpu_ops.cppF32ToFp8andkFp8Max;include/vt/ops.hQuantFp8StaticandMatmulFp8Cutlasscontracts;src/vt/cuda/cuda_matmul_fp8_cutlass.cuquant kernel and registrar;src/vllm/model_executor/models/qwen3_5.cppMatmulFp8CutlassD;tests/vt/test_fused_chain_additivity.cppcatalog and its threeCHECK_THROWS. Upstream at the pinned oracle5559679229bc961848b121ccdeaa8fa5d79bec98:csrc/quantization/w8a8/fp8/common.cuh:58-77,csrc/libtorch_stable/quantization/w8a8/fp8/common.cu:31and:204-210,vllm/model_executor/layers/quantization/modelopt.py:510-513,:528and:2527. Every line number above was re-derived at HEAD.Base SHA
b1cd4d8f6bb7ec5f0bd923a75dcc140becc7fdd8. This branch mergesorigin/mainthree times, each time at an immutable SHA:5da1d7f2fa89472c47860da9b31f8959e809acc0,3ce5a1dc1b0f1baaeab7598fbf3835abe6d53c2fand2f2bce926d673111c9816275906d4df8aa20effe.The third merge resolves review finding F2, and it was not a text collision.
git merge-tree --write-tree origin/main 810182ea1exited 1 on.agents/roadmap_v1.md. The reason: main relocated the intake table out ofroadmap_v1.mdinto.agents/issue-index.md, and2f2bce926carries no#468row in the roadmap at all. Reapplying my edit where I authored it would have resurrected a row main had deleted, which is the duplicate a move-plus-edit produces even when both sides merge cleanly. So the edit moved with the table:roadmap_v1.mdtakes the target wholesale and byte-identical, and the#468row is reapplied inissue-index.md.Verified four ways, because a clean auto-merge can silently interleave rows and still satisfy the obvious checks:
-and one+at the same line index, file length unchanged. The reviewer states this check as "the target is a strict PREFIX of the result", which assumes the branch only appends; my edit modifies one row in place, so a prefix test cannot hold and would pass vacuously. This is the strictly stronger form, and it forbids an interleave, a reorder, an insertion and a deletion at once.#468row is byte-identical to the row I authored.merge-treeagainstorigin/mainnow exits 0.What changed
The static FP8 W8A8 path now registers on the CPU backend.
src/vt/cpu/cpu_ops.cppgains two kernels:QuantFp8StaticKernelregistersOpId::kQuantFp8Static, andMatmulFp8CutlassKernelregistersOpId::kMatmulFp8Cutlass. The quant kernel multiplies by the reciprocal ofinput_scaleand forms that reciprocal once outside the loop, which mirrors upstream and mirrors our CUDA kernel. The matmul kernel is a correctness reference with f32 accumulation and one foldedalpha. It makes no speed claim, and no production model routes through it. This is the first half of option 1 of the two closures #468 names. It makes the fp8 OP seam reachable, and therefore gateable, without a GPU. It is not sufficient to exercise themixed_scalewiring #468 set out to cover: the GEMM registered here iskMatmulFp8Cutlass, while the model-layer predicate keys onkMatmulFp8CublasLt, so that wiring stays unreachable. See "Honest gaps".Evidence
scripts/agent-preflight.shpasses. Result:All gates green, 72 checks, exit 0.tests/vt/test_ops_fp8_cpu.cpp(new, 4 cases, 56 assertions) andtests/vt/test_fused_chain_additivity.cpp(1 case, 25 assertions).AGENTS.mdstates that editingsrc/,include/ortests/alone owes no public document.Full gate on the merge result
Build flags:
-Wall -Wextra -Werror, noCMAKE_BUILD_TYPE, soNDEBUGis off andassertstays live. Disk free at finish: 59 GiB.Read the earlier numbers in this PR's history as void. Every full gate before this one ran
-DVLLM_CPP_BUILD_EXAMPLES=OFF -DVLLM_CPP_SERVER=OFF, flags I chose while the box was at 100% disk and then failed to revisit. That configuration builds 925 targets and runs 463 tests; CI's builds 1422 and runs 481. The gap is not cosmetic:test_minimax_music3_e2e_reallinksApiServerand cannot BUILD without the server, so the reduced gate reported100% tests passedover a target it had never compiled. Same class as F6, an instrument reporting on a state it was not given. Only the CI-configuration run above is binding.The 7 remaining preflight failures are BASELINE, verified rather than assumed.
check-release-binary-contract,check-release-workflow,check-test-registration,test_check_release_binary_contract,test_release_manifest,test_release_pipeline,test_check_test_registration. Each was run in a clean detached worktree at2f2bce926itself and each fails there. This branch adds none and touches no release workflow, CI file, or test-registration surface.issue-index append-onlyalso failed until I fixed it; it is now green and is the only one that was ever mine.G1, G2 and G3
doublereference that reproduces the upstream lossy pipeline.G1 builds its reference from the format, not from our code. It enumerates all 128 finite e4m3fn magnitudes, decodes each one from the field layout, and picks the nearest value with an even-significand tie break by scanning.
F32ToFp8instead usesfrexpandstd::nearbyint. The two algorithms differ, so agreement is evidence and not a tautology. G1 compares bytes. It does not usedoctest::Approx, whosescaleterm defaults to 1.0 and would put a floor near 1.19e-5 on a byte comparison.G3 quantizes through the same clamp and the same round-to-nearest-even that the hardware path uses, then dequantizes. An exact-arithmetic reference would let a wrong implementation sit closer to it than the correct implementation does. The tolerance bounds only the accumulation width, as
4 * K * FLT_EPSILON * alpha * sum|terms|.Mutations
Each mutation was applied alone to a restored tree. Each was rebuilt, run, and restored. The compiler exit status appears beside every result, because a mutation that fails to build reads as a passing test. The whole series was re-run on the post-merge tree with identical results.
[doctest] test cases:[doctest] assertions:Status:04 | 0 passed | 4 failed | 0 skipped6 | 1 passed | 5 failedFAILURE!input_scaleignored04 | 3 passed | 1 failed | 0 skipped56 | 32 passed | 24 failedFAILURE!04 | 3 passed | 1 failed | 0 skipped56 | 50 passed | 6 failedFAILURE!04 | 3 passed | 1 failed | 0 skipped56 | 28 passed | 28 failedFAILURE!04 | 3 passed | 1 failed | 0 skipped56 | 28 passed | 28 failedFAILURE!alpha04 | 3 passed | 1 failed | 0 skipped56 | 53 passed | 3 failedFAILURE!weight_scaleonly04 | 3 passed | 1 failed | 0 skipped56 | 53 passed | 3 failedFAILURE!04 | 4 passed | 0 failed | 0 skipped56 | 56 passed | 0 failedSUCCESS!M0 reports 6 assertions and not 56. A changed case count is signal. This is why the series reads
Status:next toassertions:.M1, M2 and M5a first failed to build rather than to assert. The compiler rejected the dead
input_scale,inv_scaleandalphaunder-Werror=unused-parameterand-Wunused-variable. I re-expressed each mutation with an explicit(void)and re-ran it. Only rows withcompile_exit=0are verdicts.M2 goes red, and the margin was itself a finding
x / sandx * (1/s)agree on almost every input. Over 20000 random values in the range -2.0 to 2.0 the two forms never disagreed, at any of 14 scales tried. The difference appears only where a scaled input lands on an e4m3 tie, which is what G1's constructed tie population creates. Even there the effect depends on the scale. Over that population, 10 of 18 candidate scales expose the defect at all.Of the five scales G1 first shipped with, only 0.0092 exposed it, at 24 of 209 words. The mutant died by 2 assertions. The assertion that protects the reciprocal form was one scale-list edit away from silent disarming. I measured 0.13 at 78 of 209 words and 0.77 at 82 of 209 words, added both, and recorded those counts in the test beside the list. M2 now dies by 6 assertions across 3 scales.
Two further measurements: M1 moves about 99.7% of output bytes at every scale except 1.0, where ignoring the scale is correctly a no-op, for 17229 of 21595 words overall.
F32ToFp8also saturates in two places, an earlya >= kFp8Maxreturn and a lateexp_field > 15guard, so a reviewer who mutates only the obvious guard would wrongly read the gate as blind. M3 removes both.Two files outside the original brief
tests/vt/test_fused_chain_additivity.cppchanged by 117 lines, andinclude/vt/fused_recipe.hby 4. Both are forced consequences of the registration.vt::FusedChainCompositewalks a recipe's opcodes and dispatches each step to the standalonevt::op onq.device. Three catalog recipes end in opcodeFOp::kQuantFp8, which dispatches tovt::QuantFp8Static:kRmsNormQuantFp8,kRmsNormGatedQuantFp8andkSiluMulQuantFp8. Without a CPU registration that dispatch threw. The test asserted the throw with threeCHECK_THROWSand marked those catalog rowscpu_full = false.The registration makes those three assertions false. The proof is a command and not an argument. Check out the base version of that test, then run it against the new registrations:
Three assertions fail. They are exactly the three
CHECK_THROWS. The other 18 pass unchanged. The change is therefore not scope creep and not a judgement call.I did not delete those assertions to reach green. I replaced each one with the stronger byte-exact check that the registration makes available, which compares the full composite against the standalone-op-sequence golden including the fp8 output. Each driver keeps its prefix check as well, so a future regression in the tail stays localised. The
cpu_fullflag flips totruefor the three rows. Assertions go from 21 to 25.The
include/vt/fused_recipe.hchange is comment only. It sits inside the opcode table, on the line that describedkQuantFp8asCUDA-only. That is a documented contract statement which this change falsifies.A comment that was already wrong
include/vt/ops.handsrc/vt/cuda/cuda_matmul_fp8_cutlass.cuboth described the quant asx / input_scale. The code three lines below each comment multiplies by the reciprocal. The code is correct and the comment was wrong. Left in place, the comment invites a reader to "correct" a default-ON 35B path into a divide. Both comments now carry the upstream anchor and state the hazard. This repair is in flow under #468.Speed claims
Honest gaps
G2 is PENDING. It has not run. Do not read this PR as having proved CPU/CUDA byte agreement. This host has no CUDA device, so one of the three declared gates is unmeasured. The case is committed and CUDA gated. It prints its reason and still asserts the CPU registration, so it can never pass vacuously, but a vacuity guard is not the measurement. Running it needs
ctest -R test_ops_fp8_cpuin the container build ondgx.casa(GB10/sm_121,vllmcpp-build:gb10) or on192.168.68.23(Thor/sm_110,vllmcpp-build:aarch64). The operator has offered to run it; I did not improvise a remote invocation.What is at stake if G2 later fails: G1 already proves the CPU quant matches an independent reference derived from the format, and the CUDA kernel is unchanged by this PR, so a G2 failure would indicate a pre-existing CPU/CUDA divergence rather than a regression introduced here. That is worth knowing either way, which is why the gate stays open rather than being dropped.
This PR does not make the model layer run fp8 on CPU.
kMatmulFp8CublasLtstays CUDA only, because a kernel named for cuBLASLt does not belong on the host.MatmulFp8CutlassDandMatmulFp8CutlassPreQuantDinsrc/vllm/model_executor/models/qwen3_5.cppgate on that op id, so they still refuse on a CPU queue. The new test pins the refusal withCHECK_FALSE(vt::OpRegistered(vt::OpId::kMatmulFp8CublasLt, DeviceType::kCPU)), so the gap stays visible. #468 remains open for it. Option 2 of #468, themixed_scaleforwarding test, is untouched, and so is the parked lever.test_op_paritydid not reproduce the baseline red. The task brief named it as base-inherited red under #755 and #672. It passed twice here: at base SHAb1cd4d8f6in 1.87 s, and again after the first merge in 4.39 s. My build is CPU only, withVLLM_CPP_BUILD_EXAMPLES=OFFandVLLM_CPP_SERVER=OFF. Either the red depends on configuration or load, or main had already repaired it by this base. I did not investigate, because it is outside this row. I record the discrepancy rather than resolve it silently.test_cpu_x86_llamacpp_floordid red once. The first preflight ran at load average 172 and the harness exited 4,NO_QUIET_WINDOW, instead of 2. That is the known load-dependent failure in #618. The final preflight passed once the box quietened.A shared-scratchpad hazard bit this session and could bite a reader. Another session was writing
ctest.loginto the same scratchpad directory. My first monitor keyed on that filename and would have reported a foreign tree's result as mine. I moved my logs to a uniquely named directory. I also stopped a chained command of mine that would have overwritten that other session's log.Disk reached 100% mid-session because of concurrent build trees. I cleared
~/.cache/go-build, which regenerates, to recover. I did not delete any other session's worktree or build.F6, and it is the one that mattered. The G2 case was named
"G2: CPU QuantFp8Static == CUDA QuantFp8Static, byte for byte". doctest splits-tc=on commas, so that name was unselectable. Selecting the one gate this row still owes, by its exact shipped name, measured:A gate that examines nothing and prints
SUCCESS!was sitting on the arm nobody has run. Renamed comma-free; the same selector now returnstest cases: 1 | 1 passed. The whole file stays 4 cases and 56 assertions. The reason is written next to the case, not only in the spec, because it applies to every case name in the tree.F3. Two shipped comments asserted CPU/CUDA equivalence as fact while G2 has never run.
cuda_matmul_fp8_cutlass.cucalled the CPU arm "the byte-for-byte mirror of this kernel", andops.hsaid it "agrees with the CUDA kernel to fp8/bf16 tolerance". Both now state that the equivalence is declared and owed under G2, and both name what is actually measured instead: G1 proves the CPU kernel matches an independent e4m3 reference derived from the format, and two implementations each matching a reference is a weaker claim than the two matching each other. This is this PR's own thesis turned on itself. It repaired a comment asserting an unverified contract, then shipped two more.F4. The spec's reference-tier risk row was refuted by measurement, and the refutation is recorded. It reasoned from the announcement and never tested the consequence.
MaybeInstallReferenceTierdeclines only while the CPU provider count is zero (op_provider.cpp:213-214); these registrations make it one, so on a unified-memory device those ops flip from refusing by name to installing a host kernel over device pointers. Measured: SIGSEGV on GB10, exit 139 (#844). Thecorrect but slowbanner is not mitigation, it is a misleading label. This is debt this PR creates, and #844 owns the fix, which is broader than this row.The relocated intake table changed its merge CONTRACT, and I got it wrong first.
.agents/issue-index.mdis append-only, carriesmerge=unionin.gitattributes, and says "Never edit a row" in its own preamble. I moved my#468edit there along with the table and carried the keyed-record discipline with it, soscripts/check-issue-index-append-only.pyrefused the branch. The review had prescribed "the target's file is a strict PREFIX of the result", which is exactly the append-only test; I judged it vacuous for an in-place edit and substituted a weaker in-place-diff check that my own edit satisfied by construction. The prescribed check was right. Resolved by making no edit at all: the file is byte-identical to main, and main's existing#468row already carries the three-way linkage, since the issue is linked from the index, from the spec, and from this PR.This branch supersedes
row/VT-FP8-W8A8-CPU-ARM, and PR #826 opened from it.scripts/check-commit-trailers.pyrejected every commit on that branch:[attribution] malformed Assisted-by value 'AGENT:claude-opus-5[1m] [claude-code]'. The regex requires a space before a bracket group, soclaude-opus-5[1m]is not a valid model token. Repairing it rewrote the commit messages, which makes the push non-fast-forward. Force-pushing is prohibited byAGENTS.md, including--force-with-lease, so the repaired history was pushed to this new branch instead and the old branch is left untouched as a record. The tree is byte-identical:HEAD^{tree}is unchanged andgit diff ca7358f59..810182ea1 --statis empty. Only commit messages differ.Commit prefixes use the issue number, not the row ID. My commits read
spec(#468)andfeat(#468)..agents/style/commits.mdlanded on main after those commits and asks fortype(ROW-ID).scripts/check-commit-style.py --range 3ce5a1dc1..HEADreportsOK: commit writing style, so the checker accepts them. I did not rewrite pushed history to change them.One commit message carries wrong arithmetic. The implementation commit says the additivity assertions went from 19 to 25. The measured base count is 21. I corrected this in the spec rather than by rewriting pushed history, and the correcting commit explains why.
🤖 Generated with Claude Code
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]