spec(ENG-EXPERT-STREAM-DEVICE): scope the device destination for streamed expert slices, and the two facts that make W0 four edits (#1124) - #1233
Open
localai-bot wants to merge 2 commits into
Conversation
…amed expert slices, and the two facts that make W0 four edits (#1124) `--device cuda` cannot serve `Qwen3.8-2.4T-A95B UD-Q1_0` (369.96 GiB) on a 119.631 GiB GB10. Since `76f2a6d84` it refuses at load by name instead of dying mid-stream, so there is no GPU number for this checkpoint to take at all. This commit scopes the capability that would produce one. Spec only; no implementation, and none in this commit. The row splits out of `ENG-EXPERT-STREAM`, which owns the streaming MECHANISM -- the slot cache, the streamer, the `pread` filler and the host store, all of it landed and running on `--device cpu`. This row owns only the DESTINATION: where a streamed slice lives, and which platform may read it. ## Three waves, and W0 is the critical path W0 lets a platform whose host memory is device-addressable read slices out of the existing host slot store. On GB10 host and device are one physical pool, so a device store there would buy nothing but an allocator label. The predicate is probed -- a new `host_memory_is_device_addressable()` beside the already-ported `is_integrated_gpu`, taken from `cudaDevAttrPageableMemoryAccess` at registration -- never a device name or an architecture string. A discrete CUDA device answers false, keeps falling through to `KqResidentSlice`, and keeps the #1123 refusal, which is correct: there host memory genuinely is not device-addressable. W1 adds `DeviceExpertSlotStore` and W2 adds the virtual read that makes it reachable. `expert_streamer.h` already claims "the production destination is a contiguous device-side slot array"; W1 makes that sentence true rather than adding a second claim beside it. ## Two facts the issue does not name, and W0 fails without both The slot arm stages the tower itself. `KqExpertSlice`'s slot branch calls `ResidentWeight` for its dtype and repack markers, and on a staging platform that takes the upload branch -- `d.b.Alloc(w.bytes.size())` on the whole 1.1875 GiB stacked `[E*N,K]` tower, memoized. That is the identical allocation #1123 died on. Lifting the `is_cpu()` guard alone reproduces #1123 rather than fixing it. The load-time refusal fires first. `CheckDeviceWeightFit` runs before the tokenizer and before any weight I/O, gated on `needs_weight_staging()`, which is true on CUDA including GB10, and its bound sums the whole tensor table including all 335.62 GiB of `*_exps`. So it refuses before a forward exists to take the slot arm. W0 therefore carries a conditional refusal, and the conditional is exact arithmetic rather than a headroom guess: with the lane on, the staged set is the 26.01 GiB non-expert remainder plus the slot arena, 44.56 GiB at 8000 slots against a 119.631 GiB pool. This is why W0 is four small edits and not one guard. It is stated here rather than discovered during implementation. ## Issue #1124's piece 3 is not a wave The issue lists "the filler is `pread`-into-host" as a fourth independent piece. It is not independent. A device slot cannot be filled at all through today's contract, because `SlotForWrite` is handed straight to `::pread` and `DeviceMemoryIsHostAddressable` is false for CUDA. W1 carries a `CommitSlot` fill contract or W1 cannot land. The spec chooses a staging bounce over a zero-copy `cuFile` filler and says why: the bounce lands in one wave and keeps the zero-copy path genuinely optional, and the host store's `CommitSlot` is a no-op so the CPU path stays byte-identical. ## What the gates are, and what cannot be gated here W0's correctness gate is token-exactness against the CPU arm on the same checkpoint and binary, not "it ran". Its liveness gate reads `steps > 0` and DECODE-PHASE `exhausted`, taken as a snapshot difference across the prefill boundary. Total exhaustion is not usable: the last two-arm run recorded `exhausted=7813` on an 8000-slot cache, all of it in prefill, and no slot count fixes that -- one step is one forward, and a T-token prompt protects `93 * 3 * min(512, 10*T)` slices, which saturates at 331 GiB. W1 and W2 can be built and unit-gated here but only validated on hardware nobody here has, because on `dgx:gpu0` device memory IS host memory. That is recorded under `## Owed` with the exact measurement it needs, rather than implied as a gate we could run. The CPU arm's own streaming decode figure is still VOID (#912 F1), so it is named as the denominator W0's speed number will need, not as a precondition for starting. ## Records New engine-matrix row plus its section, total and `ENGINE_ROWS` counters. #1124 already has an issue-index row, so nothing is appended there: the index is append-only and its checker refuses a duplicate issue number. `.agents/specs/expert-streaming.md` is deliberately untouched -- PRs #1200 and #1216 both edit it, and its `## Owed` entry for #1124 remains true as written. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ord, resolved by key `.agents/engine-matrix.md` is the only file both sides touched. `c20018f8d` (#1203) rewrote the `KV-WARMUP-PROFILE` row's upstream anchors from line numbers to symbol anchors, three lines above `ENG-EXPERT-STREAM`; this branch appends `ENG-EXPERT-STREAM-DEVICE` at the end of the same section and bumps the two summary counters. Git merged the two hunks without a conflict, and both edits are present afterwards: the row appears once, and `gpu_worker.py::determine_available_memory` survives verbatim. The merge also brings `scripts/check-symbol-anchors.py`, which now gates this branch's new spec. It reports 620 citations, 95 in-repo checked, 0 stale. 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.
--device cudacannot serveQwen3.8-2.4T-A95B UD-Q1_0(369.96 GiB) on a119.631 GiB GB10. Since
76f2a6d84it refuses at load by name instead of dyingmid-stream, so there is no GPU number for this checkpoint to take at all. This
pull request scopes the capability that would produce one. Spec only; there is
no implementation here, and none in the same commit.
Issue #1124. New row
ENG-EXPERT-STREAM-DEVICE, specexpert-stream-device-slots.md.The row splits out of
ENG-EXPERT-STREAM, which owns the streaming MECHANISM —the slot cache, the streamer, the
preadfiller and the host store, all of itlanded and running on
--device cpu. This row owns only the DESTINATION: wherea streamed slice lives, and which platform may read it.
Three waves, and W0 is the critical path
W0 lets a platform whose host memory is device-addressable read slices out of
the existing host slot store. On GB10 host and device are one physical pool, so
a device store there would buy nothing but an allocator label. The predicate is
probed — a new
host_memory_is_device_addressable()beside the already-portedis_integrated_gpu, taken fromcudaDevAttrPageableMemoryAccessonce atregistration — never a device name or an architecture string. A discrete CUDA
device answers false, keeps falling through to
KqResidentSlice, and keeps the#1123 refusal, which is
correct: there host memory genuinely is not device-addressable.
W1 adds
DeviceExpertSlotStoreand W2 adds the virtual read that makes itreachable.
expert_streamer.halready claims "the production destination is acontiguous device-side slot array"; W1 makes that sentence true rather than
adding a second claim beside it.
Two facts #1124 does not name, and W0 fails without both
The slot arm stages the tower itself.
KqExpertSlice's slot branch callsResidentWeightfor its dtype and repack markers, and on a staging platformthat takes the upload branch —
d.b.Alloc(w.bytes.size())on the whole1.1875 GiB stacked
[E*N,K]tower, memoized inw.d_dev. That is the identicalallocation #1123 died on. Lifting the
is_cpu()guard alone reproduces #1123rather than fixing it.
The load-time refusal fires first.
CheckDeviceWeightFitruns before thetokenizer and before any weight I/O, gated on
needs_weight_staging(), which istrue on CUDA including GB10, and its bound sums the whole tensor table including
all 335.62 GiB of
*_exps. So it refuses before a forward exists to take theslot arm. W0 therefore carries a conditional refusal, and the conditional is
exact arithmetic rather than a headroom guess: with the lane on, the staged set
is the 26.01 GiB non-expert remainder plus the slot arena, 44.56 GiB at 8000
slots against a 119.631 GiB pool.
This is why W0 is four small edits and not one guard. It is stated here rather
than discovered during implementation.
#1124's piece 3 is not a wave
The issue lists "the filler is
pread-into-host" as a fourth independent piece.It is not independent. A device slot cannot be filled at all through today's
contract, because
SlotForWriteis handed straight to::preadandDeviceMemoryIsHostAddressableis false for CUDA. W1 carries aCommitSlotfill contract or W1 cannot land. The spec chooses a staging bounce over a
zero-copy
cuFilefiller and says why: the bounce lands in one wave and keepsthe zero-copy path genuinely optional, and the host store's
CommitSlotis ano-op so the CPU path stays byte-identical.
What the gates are, and what cannot be gated here
W0's correctness gate is token-exactness against the CPU arm on the same
checkpoint and binary, not "it ran". Its liveness gate reads
steps > 0andDECODE-PHASE
exhausted, taken as a snapshot difference across the prefillboundary. Total exhaustion is not usable: the last two-arm run recorded
exhausted=7813on an 8000-slot cache, all of it in prefill, and no slot countfixes that — one step is one forward, and a T-token prompt protects
93 * 3 * min(512, 10*T)slices, which saturates at 331 GiB.W1 and W2 can be built and unit-gated here but only validated on hardware nobody
here has, because on
dgx:gpu0device memory IS host memory. That is recordedunder
## Owedwith the exact measurement it needs, rather than implied as agate we could run.
The CPU arm's own streaming decode figure is still VOID
(#912 F1), so it is named as
the denominator W0's speed number will need, not as a precondition for starting.
Records
New engine-matrix row plus its section, total and
ENGINE_ROWScounters. #1124already has an issue-index row, so nothing is appended there: the index is
append-only and its checker refuses a duplicate issue number.
.agents/specs/expert-streaming.mdis deliberately untouched — PRs#1200 and
#1216 both edit it, and its
## Owedentry for #1124 remains true as written.The merge of
origin/mainresolved one keyed record by key.#1203 rewrote the
KV-WARMUP-PROFILErow's upstream anchors three lines aboveENG-EXPERT-STREAM;this branch appends its row at the end of the same section. Both edits are
present afterwards, checked rather than assumed.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]