Skip to content

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
mainfrom
row/ENG-EXPERT-STREAM-DEVICE
Open

spec(ENG-EXPERT-STREAM-DEVICE): scope the device destination for streamed expert slices, and the two facts that make W0 four edits (#1124)#1233
localai-bot wants to merge 2 commits into
mainfrom
row/ENG-EXPERT-STREAM-DEVICE

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

--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
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, spec
expert-stream-device-slots.md.

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 once 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 #1124 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 in w.d_dev. 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.

#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.

The merge of origin/main resolved one keyed record by key.
#1203 rewrote the
KV-WARMUP-PROFILE row's upstream anchors three lines above ENG-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]

mudler added 2 commits August 18, 2026 12:48
…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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants