fix(ENG-EXPERT-STREAM): the CUDA blocker is a missing vt::MatmulBTAlphaBeta, not a slower third path, and the arm now refuses by name (#1126, #1205, #1197) - #1200
Open
localai-bot wants to merge 4 commits into
Conversation
…t a one-line port, and #1126's own step 3 would be a defect (#1126, #1197) `CudaBackend` still does not override `vt::Backend::DeviceMemoryInfo`, and this change deliberately does not add it. What it fixes is the record that says how to close #1126, because as filed that record would produce a defect. ## What was already true, and what was not #1126 was filed against a seam comment reading "ROCm/CUDA override with hipMemGetInfo/cudaMemGetInfo". That comment no longer exists. #1132 corrected it in both places that carried it, `include/vt/backend.h:78-93` and `gemma4_moe.cpp:440-448`, and recorded the gap under `## Owed` in `expert-streaming.md`. Every anchor either of them cites was re-verified exact against `fd64c76ee`: `rocm_backend.hip:338-345`, `gemma4_moe.cpp:440-448`, `gemma4_moe.cpp:506`, `backend.h:78-93`, `platforms/cuda.cpp:71` and `platforms/rocm.cpp:74`. Six examined, six correct. So the comment half of this issue is done, and the honest question left is whether to build the capability. The answer here is no, and the reason is not the one the issue gives. ## What Gemma4 does today on CUDA `Gemma4MoE` is the seam's only consumer. `FreeBytes` returns false on an absent probe, `MakeRoom` refuses on unknown (`gemma4_moe.cpp:506`), so the device-expert LRU admits nothing on every CUDA device and every routed expert takes `ExpertGeGLUHost` instead (`gemma4_moe.cpp:49-74`, reached at `:1493-1499`), for the life of the process, and nothing reports it. That function's name misleads and it is worth being exact, because the first reading here was wrong: it does not compute on the host. It re-copies the expert's BF16 bytes host-to-device into a scratch buffer (`:59-60`), runs the same `vt::MatmulBT` / `GeluAndMul` pair on the device, and drains the queue once per expert (`:73`). So what the dead LRU costs is bandwidth and a per-expert `Synchronize`, not a wrong answer — the issue's "falls back to host H2D on every expert" is the accurate description. The refuse-on-unknown polarity at that call site is right, so the defect is the missing probe rather than the consumer. ## Why the override is not built here Four things came out of re-reading it, and each raises the price. 1. **There is nothing to run it on.** The LRU needs `ex.is_fp8`, which only `LoadMoeFp8PerExpert` sets (`gemma4_weights.cpp:210-215`) from a per-expert `F8_E4M3` export. No such Gemma-4 checkpoint is pinned anywhere in `docs/USAGE.md`: all 32 lines there matching `gemma` case-insensitively were swept, and every checkpoint among them is the LTX-2.5 text tower (`gemma4-12b-with-proj*.safetensors`), not a per-expert FP8 MoE decoder. The woken path cannot be exercised, here or elsewhere, until one is. 2. **The CUDA consumers are not the ones the ROCm arm measured.** `ExpertGeGLUDeviceAccum` (`gemma4_moe.cpp:76-93`) is generic `vt::MatmulBT` / `GeluAndMul` / `MatmulBTAlphaBeta` and would run. But `RunGemma4FusedTopkExpertGeGLU` (`:1599`), `ExpertGeGLUDeviceBatched` (`:240`), `PeerCopyGemma4Fp8ExpertSlice` and `RunGemma4Fp8TopKOnExpertDevice` are HIP-only, and a non-HIP build links stubs that return `false`. The override would admit experts to VRAM and then route them through the serial per-expert fallback: a third path, measured by nobody. Nor is the swap token-neutral by construction — the resident arm accumulates with `vt::MatmulBTAlphaBeta` (`:90`) where the fallback writes its output outright (`:70`), so the two sum the top-k experts in a different order. 3. **The headroom test does not mean the same thing on the CUDA device this project gates on.** `MakeRoom` admits iff `free_b >= need + 1.5 GiB` (`gemma4_moe.cpp:514`), a constant tuned on discrete dual R9700s where free VRAM is distinct from host RAM. On a GB10 it is not. #1123 measured that `cudaMemGetInfo`'s `total` there is exactly `/proc/meminfo MemTotal` times 1024, which is why it reached for that instrument where `nvidia-smi` answers `[N/A]`; `free` is therefore reported over the same unified pool, and the host BF16 expert cache the device upload exists to relieve is drawn from that pool too. The admission test would double-count, on a box whose unified-memory OOM takes the host down with it. The `free` half of that is an inference from the measured `total` rather than a second measurement, and it wants confirming on the device before any override lands. 4. **The gap is isolated, not a pattern.** Comparing overrides one by one across `src/vt/cuda/cuda_backend.cu` and `src/vt/rocm/rocm_backend.hip`, `DeviceMemoryInfo` is the only optional `vt::Backend` seam ROCm answers and CUDA does not. A dead path with a truthful comment beats a live path nobody has exercised, and points 2 and 3 say the live path would not even be the one the lab throughput note was taken on. ## Which answer to "how much device memory" is authoritative The tree holds two, and they are not duplication. - `vt::Backend::DeviceMemoryInfo(free, total)` is a **live** probe that moves with contention. Authoritative for a **runtime admission** decision — can this allocation succeed right now — and for nothing else. - `vllm::platforms::ResidencyPolicy::device_memory_total_bytes` is a **total** probed once at platform registration, `0 == UNKNOWN`. Authoritative for a **load-time budget** verdict, which has to be reproducible and independent of whatever else the box is doing. This makes #1126's own closing plan wrong at step 3, which proposes that the #1123 fit check "can then read the budget from the backend seam". Collapsing the second onto the first would make the same checkpoint load or be refused depending on the page cache, which is the property `total`-not-`free` was chosen to avoid. What #1126 owes is its steps 1 and 2 **together**, the override and the Gemma4 measurement; never step 1 alone, and never step 3. Both seams already carry the division in prose (`backend.h:90-93`, `platforms/interface.h:61-69`); it is restated in `## Owed` because that is the record a reader of the issue acts on. ## A second defect, filed in flow Reading `MakeRoom` line by line turned up #1197. The slot cap is tested before the eviction loop (`gemma4_moe.cpp:498` against `:499-500`) and `EvictOne` (`:457`) is the only thing that shrinks `slots`, so after 24 admissions `VT_GEMMA4_EXPERT_EVICT=1` is a no-op for the life of the process and the cache degrades permanently to fill-only. It binds only when the slot cap is reached before the byte budget. Filed and not fixed, for the same reason as above rather than for effort: the one-line repair wakes more `hipFree` under load, which the surrounding comments call a permanent `kfd_wait` hang, so the current ordering may be deliberate. It is owned by `ENG-EXPERT-STREAM` and listed under `## Owed`. ## Why this is a record-only change The record is the work here: a filed gap whose closing plan is wrong in two ways, and a newly filed defect. It does not restate what #1132 landed — points 1 through 4, the authority split and #1197 are all new. Leaving it out means the next agent implements step 3 as written. No code changed, so there is nothing to mutate and no behaviour to gate. The gate run is the checker suite. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…phaBeta`, not a slower third path, and the arm now refuses by name (#1126, #1205) A fresh review returned FAIL on the record this repairs. The decision it argued for was right and five of its six substantive claims survive. Its central technical paragraph was wrong, and on a record-only change a wrong record is the defect. ## The finding that inverts it `vt::MatmulBTAlphaBeta` has NO CUDA implementation. `src/vt/fused_ops.cpp:102` dispatches to `rocm::MatmulBTAlphaBetaRocm` (`rocm_matmul_hipblaslt.hip:516`) under `#if defined(VLLM_CPP_HIP)` AND `q.device.type == kROCM`; every other device falls through to a throw. There is no CUDA, Vulkan, Metal or CPU arm. So the record's claim that `ExpertGeGLUDeviceAccum` "would run" on CUDA is false. What the override wakes is: `EnsureGemma4Fp8ExpertOnDevice` (`gemma4_moe.cpp:548-586`, no HIP guard) -> `lru.MakeRoom` at `:565` succeeding once `FreeBytes` can answer -> `true` at `:575` -> the call site at `:1486` -> `ExpertGeGLUDeviceAccum` at `:1487` -> `vt::MatmulBTAlphaBeta` at `:90` -> throw. The `try`/`catch (...)` at `:563-585` wraps only the upload; the compute at `:1487` is outside it, so the exception leaves the decode step rather than degrading to the host fallback. It is latent today only because the other route into that function, `same_dev` (`:730-731`), needs `ex.gate_up_dev`, assigned nowhere but `rocm_gemma4_experts.hip:207,226`. The resident arm is unreachable off ROCm, not safe. That makes the missing CUDA arm the actual blocker for #1126 step 1, recorded nowhere, and a stronger argument than any of the record's four. Filed as #1205. ## Three further corrections `ExpertGeGLUDeviceBatched` (`:240`) was named beside three genuine HIP-only stubs. It is not one: it sits outside the file's only `#ifndef VLLM_CPP_HIP` block (`:1574-1628`), has no HIP implementation and no header declaration, and is unconditionally `return false` under its own lab note at `:237-239`. It is disabled everywhere, ROCm included, so listing it inflated the ROCm/CUDA asymmetry. The other three are correct and stay. "The two arms sum the top-k experts in a different order" is false. Both run inside the same `for (int i = 0; i < top_k; ++i)` at `:1431`. The real difference is rounding and where the routing weight is applied: the resident arm folds `ww` into the GEMM `alpha` and accumulates in the epilogue with `beta` (`:1434`, `:1442`, `:90`), while the fallback writes the unweighted product to a BF16 buffer (`:67`, `:70`) and applies `ww` afterwards with separate BF16 kernels (`MulScalar` at `:1524`, `MulScalar` plus `Add` at `:1526-1527`). Two extra BF16 roundings per expert, weight applied post-rounding. Right conclusion, wrong mechanism. "Never step 3" overstated the case, in the spec and in the previous title. #1126 step 3 proposes the fit check read the budget from the backend seam, and that seam returns BOTH `free` and `total` (`include/vt/backend.h:94`). Reading `total` is contention-independent and legitimate. The defensible statement, and the one the row now asserts: step 3 may read only `total`, never `free`, and must keep the value probed once at platform registration (`include/vllm/platforms/interface.h:70-72`). Two smaller ones. `DevExpertLru::Note` also calls `slots.clear()` at `:522`; it is a device-index reset rather than an eviction, frees nothing, and is unreachable in a single-device process, so the #1197 row now names it and the next reader does not conclude the sweep missed it. And the H2D cost the row states in the present tense is conditional: the device LRU is `ex.is_fp8`-gated (`:969`, `:1484`), and by the row's own point (1) no per-expert FP8 Gemma-4 checkpoint is pinned, so on CUDA the fallback is never even asked for today. ## The code this uncovered `fused_ops.cpp` threw a bare "ROCm-only in this build" mid-decode for an unimplemented arm. AGENTS.md requires an unimplemented arm to refuse with a message that names the missing part, and that one named neither the device that asked nor where an implementation would go, so a caller could not tell a missing kernel from a missing build flag. It now names all three plus the issue. `tests/vt/test_gemma4_rocm_fp8_seams.cpp` gates it on a posed CUDA queue and on `kCPU`, `kVULKAN` and `kMETAL` — the refusal reads only `q.device.type`, so a CPU build can pose as any device and no ROCm hardware is needed. This is an in-flow gateable defect independent of #1126, and when someone writes the CUDA implementation the same test is what it has to satisfy. It pins the precondition without any checker that says do not fix it. Red before green: the three message assertions and the three device-name assertions failed against the old message. Mutation-proven twice, both compiled, both applied, three cases each, tree restored byte-exact: restoring the old message goes RED, and deleting the refusal outright goes RED. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
DeviceMemoryInfo override is not a one-line port, and #1126's own step 3 would be a defect (#1126, #1197)vt::MatmulBTAlphaBeta, not a slower third path, and the arm now refuses by name (#1126, #1205, #1197)
`1f4878fdc` (SPEC-DSPARK-QWEN3-ROUTING) landed while this row's review repair was in flight, so the preflight trailer and style gates skipped rather than reporting on this tree. Only `.agents/issue-index.md` overlapped, and it union-merged; the #1205 row this branch appends and the #1193 row main appends are both present and neither was edited. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
… row at the right numbered point (#1205) The `## Owed` rows now say the review repair re-audited every anchor the three rows cite against the repaired tree, 53 examined and 53 exact, so a reader can tell a re-verified anchor from a carried-over one. The #1205 row said "this row's point (1)" for a point that lives in the `DeviceMemoryInfo` row above it. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CudaBackendstill does not overridevt::Backend::DeviceMemoryInfo, and thischange deliberately does not add it. What it fixes is the record that says how
to close #1126, because as filed that record would produce a defect — and, after
a fresh review, the first version of this record which got the reason wrong.
The one piece of product code here is unrelated to that decision and stands on
its own:
vt::MatmulBTAlphaBetarefused an unimplemented arm with a messagethat named nothing, and now names it, gated.
What was already true, and what was not
#1126 was filed against a seam comment reading "ROCm/CUDA override with
hipMemGetInfo/cudaMemGetInfo". That comment no longer exists. #1132 corrected it
in both places that carried it,
include/vt/backend.h:78-93andgemma4_moe.cpp:440-448, and recorded the gap under## Owedinexpert-streaming.md. Every anchor either of them cites was re-verified exact:rocm_backend.hip:338-345,gemma4_moe.cpp:440-448,gemma4_moe.cpp:506,backend.h:78-93,platforms/cuda.cpp:71andplatforms/rocm.cpp:74. Sixexamined, six correct.
So the comment half of this issue is done, and the honest question left is
whether to build the capability. The answer is no, and the reason is neither the
one the issue gives nor the one this pull request first gave.
The blocker, which was recorded nowhere
vt::MatmulBTAlphaBetahas no CUDA implementation.src/vt/fused_ops.cpp:102dispatches to
rocm::MatmulBTAlphaBetaRocm(rocm_matmul_hipblaslt.hip:516,declared
include/vt/rocm/rocm_matmul_batch.h:28) under#if defined(VLLM_CPP_HIP)andq.device.type == kROCM, and every otherdevice falls through to a throw. There is no CUDA, Vulkan, Metal or CPU arm.
This is a missing kernel, not a missing build flag.
An earlier draft of this record read
ExpertGeGLUDeviceAccum(
gemma4_moe.cpp:76-93) as generic —vt::MatmulBT,GeluAndMul,vt::MatmulBTAlphaBeta— and concluded it "would run" on CUDA, just on a thirdpath measured by nobody. It would not run. It would throw, mid-decode:
EnsureGemma4Fp8ExpertOnDevice(gemma4_moe.cpp:548-586) carries no HIP guard.lru.MakeRoomat:565starts succeeding as soon asFreeBytescan answer.trueat:575.:1486takesExpertGeGLUDeviceAccumat:1487.vt::MatmulBTAlphaBetaat:90.The
try/catch (...)at:563-585wraps only the upload. The compute at:1487sits outside it, so the exception propagates out of the decode steprather than degrading to the host fallback.
It is latent today only because the other route into that function,
same_dev(
:730-731), needsex.gate_up_dev, which is assigned nowhere butrocm_gemma4_experts.hip:207,226. Nothing in a non-HIP build sets it, so theresident arm is unreachable rather than safe. Filed as #1205.
Three further corrections to the earlier draft
ExpertGeGLUDeviceBatchedis not HIP-only. It was named beside three genuineHIP-only stubs. It sits outside the file's only
#ifndef VLLM_CPP_HIPblock(
:1574-1628), in an anonymous namespace, with no HIP implementation and noheader declaration, and is unconditionally
return falseunder its own lab noteat
:237-239(gather+strided produced wrong tokens at ~23 t/s; pointer-batch~0.8 t/s; serial/fused-gelu kept at ~34 t/s). It is disabled everywhere,
ROCm included, so listing it inflated the ROCm/CUDA asymmetry. The other three —
RunGemma4FusedTopkExpertGeGLU(:1599),PeerCopyGemma4Fp8ExpertSlice(
:1607) andRunGemma4Fp8TopKOnExpertDevice(:1611) — are correct and stay."The two arms sum the top-k experts in a different order" is false. Both run
inside the same
for (int i = 0; i < top_k; ++i)at:1431, so the summationorder is identical. The conclusion survives — the swap would not be token-neutral
— but the mechanism is rounding and where the routing weight is applied. The
resident arm folds
wwinto the GEMMalphaand accumulates in the epiloguewith
beta(:1434,:1442,:90). The fallback writes the unweightedproduct to a BF16 buffer (
:67,:70) and applieswwafterwards with separateBF16 kernels:
vt::MulScalarat:1524on the first expert,MulScalarplusvt::Addat:1526-1527on the rest. Two extra BF16 roundings per expert, withthe weight applied post-rounding.
"Never step 3" overstated the case, in the spec and in this pull request's
previous title. #1126 step 3 says the fit check may "read the budget from the
backend seam", and that seam returns both halves
(
bool DeviceMemoryInfo(size_t* free_bytes, size_t* total_bytes),include/vt/backend.h:94). Only one of them is illegitimate. Readingtotaliscontention-independent, it is the same quantity
ResidencyPolicyalreadycarries, and a seam that answers it on every platform is a defensible source.
The defensible statement, and the one the row now asserts: step 3 may read only
total, neverfree, and must keep the value probed once at platformregistration (
include/vllm/platforms/interface.h:70-72).Two smaller ones.
DevExpertLru::Notealso callsslots.clear()at:522; itis a device-index reset rather than an eviction, frees nothing, and is
unreachable in a single-device process, so the #1197 row now names it and the
next reader does not conclude that sweep missed it. And the host-to-device cost
the row stated in the present tense is conditional: the device LRU is
ex.is_fp8-gated (:969,:1484), and by the row's own point (1) no per-expertFP8 Gemma-4 checkpoint is pinned, so on CUDA the fallback is never even asked for
today.
What survives unchanged
The decision, and four of the five other substantive claims.
ex.is_fp8, which onlyLoadMoeFp8PerExpertsets (gemma4_weights.cpp:210-215) from a per-expertF8_E4M3export. No such Gemma-4 checkpoint is pinned anywhere indocs/USAGE.md: all 32 lines there matchinggemmacase-insensitively wereswept, and every checkpoint among them is the LTX-2.5 text tower
(
gemma4-12b-with-proj*.safetensors), not a per-expert FP8 MoE decoder.project gates on.
MakeRoomadmits ifffree_b >= need + 1.5 GiB(:514),a constant tuned on discrete dual R9700s where free VRAM is distinct from host
RAM. On a GB10 it is not. Qwen3.8-2.4T on --device cuda: the model loads, then the first forward dies on cudaMalloc: out of memory #1123 measured that
cudaMemGetInfo'stotalthereis exactly
/proc/meminfo MemTotaltimes 1024;freeis therefore reportedover the same unified pool, and the host BF16 expert cache the device upload
exists to relieve is drawn from that pool too. The admission test would
double-count, on a box whose unified-memory OOM takes the host down with it.
The
freehalf of that is an inference from the measuredtotalrather thana second measurement, and it wants confirming on the device.
src/vt/cuda/cuda_backend.cuandsrc/vt/rocm/rocm_backend.hip,DeviceMemoryInfois the only optionalvt::Backendseam ROCm answers andCUDA does not.
vt::Backend::DeviceMemoryInfois a live probe,authoritative for a runtime admission decision and nothing else.
ResidencyPolicy::device_memory_total_bytesis a total probed once atregistration,
0 == UNKNOWN, authoritative for a load-time budget verdict,which has to be reproducible.
MakeRoomline by line turned it up: the slot cap is testedbefore the eviction loop (
:498against:499-500) andEvictOne(:457) isthe only thing that shrinks
slots, so after 24 admissionsVT_GEMMA4_EXPERT_EVICT=1is a no-op for the life of the process. Filed andnot fixed: the one-line repair wakes more
hipFreeunder load, which thesurrounding comments call a permanent
kfd_waithang, so the ordering may bedeliberate.
The code, and why it is here
fused_ops.cppthrew a barestd::runtime_error("vt::MatmulBTAlphaBeta: ROCm-only in this build")mid-decode for an unimplemented arm. AGENTS.mdrequires the opposite: refuse an unimplemented arm with a message that names the
missing part, and never leave the missing path to be discovered later. That
message named neither the device that asked nor where an implementation would
go, so a caller who hit it on CUDA could not tell a missing kernel from a missing
build flag. It now names both, plus the issue.
tests/vt/test_gemma4_rocm_fp8_seams.cppgates it on a posed CUDA queue and onkCPU,kVULKANandkMETAL. The refusal reads onlyq.device.typeanddereferences no buffer on that path, so a CPU build can pose as any device and no
ROCm hardware is needed. This is an in-flow gateable defect independent of #1126,
and when someone writes the CUDA implementation the same test is what it has to
satisfy — which is also the answer to whether the absence should be pinned: the
gate says what is missing, executably, without any checker that says do not fix
it.
Red before green: six assertions failed against the old message. Mutation-proven
twice, both applied (
sha256changed, non-emptygit diff --stat), bothcompiled (
compile_err=0,enospc=0), three cases each, tree restoredbyte-exact. Restoring the old message goes RED. Deleting the refusal outright
goes RED.
The implementation of a CUDA
MatmulBTAlphaBetastays owed and is what #1205tracks. It is not written here: a
beta-accumulating BT GEMM on cuBLASLt is akernel with its own correctness gate, point 1 above says there is no checkpoint
to exercise it on, and this host has neither a ROCm nor a CUDA device to measure
either arm.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]