fix(BACKEND-ROCM): ask the device that will RUN a placed expert tower whether it may keep its blocks - #2562
Merged
Merged
Conversation
…tower's residency through the device that will run it GLM-5.3 `UD-IQ1_S` refuses at load on `strix:gpu0` because `LoadStackedExperts` asks the ENGINE device whether an IQ1_S tower may keep its blocks, for a tower the installed hybrid-placement plan has already decided will execute on the CPU and whose bytes never reach the GPU. `DeviceKeepQuantSupported` serves four k-quants on ROCm, so the tower expands and the loader refuses by name. The spec traces the whole chain rather than the next hop: GLM-5.3 never uploads an expert tower to any device, its unplaced MoE forward cannot run on a non-host-addressable board at all, and the CPU already has a `vec_dot` for every encoding this checkpoint's experts use. Two edits close it -- the routing device for a placed tower, and the fit check crediting the plan it installed 220 lines earlier (#2517). Neither an i-quant ROCm kernel (#1940) nor the host-slot lane (#2515) is on the path to a token here, and the spec says why for each. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
… whether it may keep its blocks
GLM-5.3 `UD-IQ1_S` refused at load on `strix:gpu0` (`gfx1151`) with
`blk.3.ffn_gate_exps.weight routed to an EXPAND residency`. The tower is IQ1_S,
`DeviceKeepQuantSupported` serves {Q8_0, Q4_K, Q5_K, Q6_K} on ROCm, so it
expanded and `LoadStackedExperts` refused by name -- for a tower the installed
hybrid-placement plan had already sent to the CPU, whose `vec_dot` table covers
every encoding this checkpoint's experts use, and whose bytes never reach the
GPU at all: that model reads a tower only through `GlmExpertSlice`, never
through `ResidentWeight`.
`GgufLoadPolicy::device` is the ENGINE's device, and its own field comment says
every device-dependent decision reads it. Hybrid placement (#2023, #2314)
introduced a second, narrower answer for exactly one class of tensor:
`RunMoePlaced` hands `MoeBlock` the PLACEMENT device. A keep-quant decision asks
whether the device that will execute this GEMM has a `vec_dot` for the encoding,
so for a placed routed-expert tower it is a question about that device. `Route`
and `PeekRoute` now resolve it through one shared `ComputeDeviceFor`, so the
lane predicate and the loader cannot disagree about a file. This is #1136 and
#2406 one seam further along, and it is inert in four terms -- the last, that
the plan must actively move THIS tensor off its own engine device, is what stops
a never-installed global (whose engine device is `kCPU`) from routing a CUDA
load's experts as though they were on the host.
The fit check now credits the plan installed 220 lines above it (#2517). One
load printed "56 layers run their routed experts on cpu ... to bring a
216433205760 B footprint under a 68719476736 B budget" and then refused needing
216433205760 B: two lines of one load contradicting each other, because nothing
in `CheckDeviceWeightFit` carried any representation of a resolved placement. A
placed routed-expert tensor moves into `placed_tensor_count` / `placed_bytes`
and leaves the bound, exactly as a streamed one already did, and the refusal
says what the placement took out rather than leaving it to be inferred.
`test_gguf_device_fit`'s IQ case asked `CurrentPlatform()`, which made it RED on
every ROCm build for a reason the case did not state (#2516). It now pins both
devices it is actually about: `kCPU` keeps the blocks, `kROCM` expands them, so
the gap #1940 owns is asserted instead of stumbled over.
Local: `test_gguf_device_fit` 24 cases / 182 assertions, 0 failed. Deleting the
placement term from `ComputeDeviceFor` kills 1 case / 1 assertion; deleting it
from the footprint kills 3 cases / 8 assertions.
Refs #2516, #2517. No token is claimed here.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
…d nothing this row asserts `origin/main` moved 42 commits while this row was measured on `strix:gpu0`, including `BACKEND-ROCM-TOKEN-GATE` (#2546) and its record that `gfx1151` cannot complete a gate-sized Q4_K run. The merge is clean and touches none of this row's four product hunks: the new ROCm work is a measurement record and a defaulted-off `Fmt == 3` arm, and `git show aec65e6 -- <this row's files>` is empty. Taken rather than left for the operator because CI builds the merge commit and a conflict is worth finding here rather than in a queue. Every claim this row makes was re-verified after it: `agent-ready` reports 0 FAIL with 80 of 80 in-scope translation units compiling, and the hardware evidence is unaffected because it was taken against the row's own base and its own patch, both named by sha256 in the pull request body. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
This was referenced Sep 2, 2026
…r that is now next The two predicates this row names are sequential rather than alternative, which the hardware settled: on the base build the fit check refuses with all 78 layers already placed, and suppressing that refusal only buys one more stage before the loader refuses the first IQ1_S tower. With both fixed the same command loads -- 1809 tensors, 11.620 GiB paged in, the engine sizes its KV cache and the scheduler starts -- and all 228 towers stayed compressed, which is proved by the load completing at all because the loader throws by name on any tower that does not. No token came out and none is claimed. The first forward throws in the MLA block: `fused_nr` asks `vt::OpRegistered(kFusedNormRope, ...)`, ROCm registers no such op, and `OpRegistered` deliberately cannot see the reference tier -- so the split A-projection path is taken and refuses a block-quantized `kv_a_proj_with_mqa`. Its own comment says that path needs `VT_MLA_FUSED_NORM_ROPE=0`; this run had it unset. Filed as #2564, with #2565 for the `--fit` resolver gap the same run exposed. Both are recorded under `## Owed` rather than started, which is this spec's own stop condition. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…151, which is load and not generate The checkpoint row's build-requirements cell said `--device cuda` and named the CPU queue as the only alternative. That is now incomplete: with #2562 the same artifact LOADS on `strix:gpu0` under `VT_CPU_MOE=1 --device auto`, all 228 routed towers stay compressed, 11.620 GiB is paged in, and the engine sizes its KV cache. It then emits NOTHING, because the first forward throws in the MLA block (#2564), and the sentence says that in the same breath rather than leaving a reader to infer a token from a successful load. The three surrounding facts a reader on an AMD box needs are stated with it and each carries its issue: `auto` is the only route to ROCm (#2505), `cpu_moe` is required because `--fit` leaves towers on a device that cannot hold them (#2565), and the streamed-expert lane is not what serves them and cannot be on this board (#2515). No speed number is offered, because the run reaches no token at all. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
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.
fix(BACKEND-ROCM): ask the device that will RUN a placed expert tower whether it may keep its blocks
Row:
BACKEND-ROCM-IQ-EXPERT-RESIDENCY. Spec:.agents/specs/rocm-placed-expert-residency.md. Base35116605b.Fixes #2516. Fixes #2517.
GLM-5.3
UD-IQ1_Snow LOADS onstrix:gpu0(gfx1151, ROCm 7.2.4), where itpreviously refused twice. No token is claimed: the first forward throws in the
MLA block for a reason this row does not own and has filed as #2564.
Two predicates stopped the load, in this order, and they are sequential rather
than alternative -- fixing either alone still refuses.
The fit check ignored the plan installed 220 lines above it (#2517). With
VT_CPU_MOE=1the load announceddevice placement INSTALLED: 78 layers run their routed experts on cpuand then refused on the next line needing216433205760 bytes (201.56 GiB)— the UN-reduced figure, with every routedexpert already placed on the host. Nothing in
CheckDeviceWeightFitcarried anyrepresentation of a resolved placement, so there was no path by which the placed
bytes could leave the bound.
The loader asked the wrong device (#2516). Past that refusal, the load
reached
LoadStackedExpertsand refused again:blk.3.ffn_gate_exps.weight routed to an EXPAND residency. The tower is IQ1_S,DeviceKeepQuantSupportedserves{Q8_0, Q4_K, Q5_K, Q6_K}on ROCm, so itexpanded — for a tower the plan had already sent to the CPU, whose
vec_dottable covers all six encodings this checkpoint's experts use, and whose bytes
never reach the GPU at all: GLM-5.3 reads a tower only through
GlmExpertSlice, never throughResidentWeight, and its registrypreparehook is a no-op.
What changed
GgufLoadPolicy::deviceis the ENGINE's device, and its own field comment saysevery device-dependent decision reads it. Hybrid placement (#2023, #2314) added
a second, narrower answer for exactly one class of tensor:
RunMoePlacedhandsMoeBlockthe PLACEMENT device. A keep-quant decision asks whether the devicethat will execute this GEMM has a
vec_dotfor the encoding, so for a placedrouted-expert tower it is a question about that device.
RouteandPeekRoutenow resolve it through one shared
ComputeDeviceFor, so the lane predicate andthe loader cannot disagree about a file (#1378). This is #1136 and #2406 one
seam further along.
It is inert in four terms, and the last one is load-bearing rather than
defensive: a never-installed plan's
engine_device()iskCPU, so the placedanswer is adopted ONLY when the plan actively moves THIS tensor off the device
the plan itself was resolved for. Every load in this tree that configures no
placement routes byte for byte as before.
On CUDA it is inert even WITH a placement, and that is checkable by
inspection rather than by hope.
RouteGgufTensorhas exactly threedevice-dependent terms.
DeviceQuantGatherSupportedis reached only forkEmbeddingTable, a role this override does not touch.DeviceKeepQuantSupportedanswerstruefor every device except ROCm, andDeviceKeepF16Supportedisdev != kROCM. So substitutingkCPUfor a CUDA,Metal, Vulkan, XPU or Tenstorrent engine cannot change any answer at all; ROCm
is the one platform whose answer moves, which is the platform the defect was
measured on.
GgufStagedWeightFootprintgained aconst MoePlacementPlan*(null = today'sanswer, byte for byte). A placed routed-expert tensor moves into
placed_tensor_count/placed_bytesand leaves the bound, exactly as astreamed one already did, and the refusal now says what the placement took out.
test_gguf_device_fit's IQ case askedCurrentPlatform(), which made it red onevery ROCm build for a reason the case did not state (#2516's own filing). It
now pins both devices it is about:
kCPUkeeps the blocks,kROCMexpandsthem, so the gap #1940 owns is asserted rather than stumbled over.
The box, and the two predicates re-measured on it
Read with
hipGetDevicePropertieson the leased board rather than inherited, andboth predicates printed side by side because #2507 confounded them once.
#2515 is confirmed:
pageableMemoryAccess = 0, soHostMemoryIsDeviceAddressableis FALSE and the host-slot expert lane cannotserve here at all. That is why this row does not repair the lane. Numbers from
gfx1100/gfx1200/gfx1201in other ROCm specs do not transfer to this boardand none is reused.
RED, on the real artifact
Base
35116605b, HIP build,-DVLLM_CPP_HIP_ARCHITECTURES=gfx1151,ROCm backend: ENABLED for arch(es) [gfx1151]asserted at configure, no ccache(#2506).
libvllm.so.0.0.3sha256ecfc903d1b70794ceaacc80d0624405985c66ca9e23b6b46427b7e0e504cbb98;sentinel
routed to an EXPAND residency= 1,ALREADY EXCLUDED from the figure= 0. Model: the derived first shard, sha256
b3e9838651a5c279533c98390ab4bc03cf1d8c176d5be0754180f07d9ed85c01(never thedirectory).
test_gguf_device_fiton that build — 21 cases / 20 passed / 1 failed; 155assertions / 153 passed / 2 failed, which is #2516's filing reproduced to the
number.
The binary-identity guard earned its keep, and the measurement is worth
recording. Across the base and patched builds
vllm-clihas the identicalsha256
ac37fb6d11ba3c1ae7d3d931777ac32ef6fe6bee00231ba18470330740192529— it isa thin ABI client and nothing in it changed — while
libvllm.so.0.0.3movesecfc903d1b70794ceaacc80d0624405985c66ca9e23b6b46427b7e0e504cbb98->b7f8b962d17fee11eb72b137efcbf6f9e7374789a2e5499edc4e5e90df385e35. A guard thatdigested only the executable would have reported two different builds as one. The
source-level sentinel is the second discriminator:
ALREADY EXCLUDED from the figureappears 0 times in the base.so(and 0 times in the base SOURCE, whichis checkable without a build) and 1 time in the patched one.
Three product legs,
vllm-cli --device auto --prompt "The capital of France is":--fitis on)VT_CPU_MOE=1VT_CPU_MOE=1 VT_DEVICE_WEIGHT_BUDGET_BYTES=0vt: glm-dsa gguf: blk.3.ffn_gate_exps.weight routed to an EXPAND residency ... at glm_moe_dsa_loader.cpp:392. #2516.Two blockers, in that order, and C is what proves B is not the only one.
reference-tier distinct=0in all three: the process refused before any forward,so no leg here says anything about a kernel.
GREEN
All seven suites GREEN on the
gfx1151HIP build of the patched tree, cases andassertions read together (a
3 failed / 0 assertions failedrun reads clean ifyou grep only one of them):
test_gguf_device_fittest_gguf_keep_quanttest_device_placementtest_gguf_device_fit_reachtest_glm_moe_dsa_gguf_loadtest_expert_stream_wiringtest_platformtest_gguf_device_fitwas 21 cases / 1 failed and 155 assertions / 2 failed onthe same box before this change.
The product legs
Leg E —
VT_CPU_MOE=1 vllm-cli --model <derived shard 1> --device auto --prompt "The capital of France is" --max-tokens 8 --temperature 0. THE MODELLOADS. Where the base binary refused twice, the patched one gets all the way
through:
1809 tensors resolved, 11.620 GiB paged in (
device=5is ROCm, and the threeops selected before the throw are all
vt-native), the engine sizes its KVcache, and the scheduler starts. All 228 routed-expert towers stayed
compressed, and that is proof rather than inference:
LoadStackedExpertsthrows by name on any tower that expands, so a load which completes at all has
kept every one of them. The 11.5 MiB/s is the CIFS share the artifact lives on
and is not a property of anything this row changed.
NO TOKEN CAME OUT, AND NONE IS CLAIMED. The first forward throws in the MLA
block:
VT_MLA_FUSED_NORM_ROPEwas not set.fused_nr(
mla_attention.cpp:550-551) also asksvt::OpRegistered(kFusedNormRope, device), ROCm registers no such op, andOpRegisteredis deliberately a native-only probe that cannot see the referencetier — so the message's own diagnosis is one cause short and this run is the
counterexample. Filed as #2564, which is now the single thing between this
checkpoint and a token on
gfx1151. It is this spec's own W6 stop condition, andthe row stops there rather than widening into the MLA arm.
reference-tier distinct=0at the throw.Leg F — the same command with no placement configured, so
--fitis on bydefault. The fit check now PASSES (this row's repair, visible as the absence of
the
cannot serveline that leg A had), the plan places the trailing 56 of 78,and the load then refuses on
blk.3.ffn_gate_exps.weight— one of the 22 layers--fitleft on the device.--fitsizes a placement from ON-DISK bytes andcannot know an unplaced tower expands here. Predicted in the spec's
## Owedbefore the run, confirmed by it, and filed as #2565.
cpu_moeis thesupported route on this board.
Leg G —
--device cpu --max-tokens 1, the control. It loads in 8.990 s offthe now-warm page cache (
gguf prefault spans=590 paged_in=12.119 GiB in 0.648 s)and is then killed by this job's own 1800 s timeout inside the forward, having
emitted nothing. That timeout was mine and was too short: the sibling
measurement on
dgx:gpu0puts one token of this model's CPU forward at 950 s on20 cores, and this run had 1790 s of forward. It is a timeout, not a failure,
and it is reported as one —
rc=124, nowhat(), no throw. It is also theresident-footprint reading this row owes: 12.119 GiB on a CPU engine and
11.620 GiB on the ROCm engine under
cpu_moe, both against this board'sMEASURED 58.000 GiB
hipMallocManagedceiling and its reported 64.00 GiB pool.Which is why #2518's 6.00 GiB of optimism cannot decide this load in either
direction, and why it is filed rather than fixed here.
Reference-tier hits: 0 on every leg, base and patched. #2505 means no
--devicevalue names ROCm, so configuration alone cannot separate a GPU runfrom a host fallback; this is the op table's own answer, and the three ops leg E
selected before the throw are
device=5 selected=vt-native. Nothing here fellback, because nothing here got far enough to need to.
Mutations
Every mutation was REBUILT before it was run — a mutation the compiler kills is
not a killed mutation — and every restore was verified by sha256 against the
pre-mutation digest of both product files.
test_gguf_device_fitComputeDeviceFornever consults the planCheckDeviceWeightFitpassesnullptrto the footprintThese three ran locally against a hand-linked binary of the same test file, which
is an INSTRUMENT and is reported as one; the CMake targets are run on the box
alongside the product legs, and the reachability proof is the production entry
point rather than any of these — the base binary refuses this checkpoint and the
patched binary loads it, through
vllm-cli→vllm_engine_loadon the realartifact.
A fourth mutation, deleting
&vllm::ActiveMoePlacementPlan()from the ONEproduction
CheckDeviceWeightFitcall site inmodel_loader.cppand rebuildingon the board, is queued on
strix:gpu0and will write/workspace/glm53-iqlane/out3/job3.log. It is not needed for the claims aboveand is not counted among them; it exists so the call-site deletion is measured on
the same binary the product legs used rather than on a hand-linked one.
What this does NOT do
vec_dot(ROCm ports zero I-quant formats, and porting a complex-vec_dot one would test whether #1910's nwarps split transfers #1940). Measured unnecessary here:a placed tower is executed by
vt::cpu'svec_dot, and on this board anUNPLACED tower cannot be executed at all —
GlmResidentExpertSlicerefuses adevice that is neither CPU nor host-addressable before any kernel is asked
for.
device_memory_total_bytes(ROCm's device-weight budget is hipMemGetInfo's 64.00 GiB total, but every allocation on an integrated board is managed and tops out at a measured 58.000 GiB #2518). That issue is real andstays open: the budget is
hipMemGetInfo's 64.00 GiB while every allocationhere is
hipMallocManagedwith a measured 58.000 GiB ceiling. It cannotdecide this load — the credited footprint is far below both figures — so it
is not fixed under a row that would have no way to gate the difference.
--fit's resolver residency-aware (--fit sizes a placement from ON-DISK bytes, so it can leave a tower on a device whose keep-quant set cannot hold it #2565, filed from leg F).cpu_moeis the supported route on this board.condition and the row honours it.
docs/ROCM.md:60-61disqualifies aperformance result from a run with a non-zero reference-tier count, and this
model's MLA/DSA arm is eight ops short on ROCm — every one of them lands on
that tier.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]