cuda: COLI_GROUP_ASYNC=1 — overlap the CPU expert rows with the GPU groups at decode (opt-in, +6-8%) - #342
Conversation
|
Pushed two follow-ups from the profiling I promised: 1. Layernorm weights cached on-device ( 2. The overlap window measured — and it reframes this whole PR. With The GPU side of a token's experts is ~3.8 s; the CPU side (the ~90 RAM-tier experts each token routes to) is 44.5 s. So the overlap is working perfectly — it hides essentially all of the GPU time — but the end-to-end win is only +6% because the GPU was never the bottleneck at S=1. Decode on this class of host is CPU-bound (independently: 1.1% SM utilization). Full writeup in Discussion #208. So this PR's real value is diagnostic: it's the instrument that proved the RAM tail, not the GPU, is the lever. The next moves it points to (streaming-weight GPU path for RAM experts, or a smaller container so the whole set fits VRAM — the #81 lattice line) are separate, larger PRs. Happy to keep this opt-in and default-off; the layernorm cache is worth taking on its own regardless. |
|
check cuda and i merge it! |
7916089 to
39eabeb
Compare
|
Rebased onto current Validation:
|
|
@ZacharyZcR — one gate left before this merges, and it's the same one #365/#372 passed: token-identity The check is one A/B on your box: same prompt, |
Token-identity A/B: COLI_GROUP_ASYNC=0 vs =1 — IDENTICALMachine: rs-yuesheng-gpu (2× Xeon Silver 4510, 6× RTX 5090, 251 GiB RAM) Token identity
All 64 generated token IDs match byte-for-byte across sync/async arms in every run. The Token stream (round 1, both arms): Performance (COLI_CUDA_PIPE=2, 128 tokens, NGEN=128)
At 96% hit rate (9280 VRAM + 1574 RAM experts) the CPU expert tail dominates, so the overlap window is noise-level — consistent with the PR's own analysis (GPU side ~3.8s vs CPU 44.5s). The +6-8% from the original measurement was at a warmer cache state where the GPU fraction was proportionally larger. VerdictThe gate is met: token-exact under controlled |
|
Heads-up: |
… overlap at decode (opt-in, +6-8% measured)
…2 sync H2D/token) + overlap-window profiling counters
5978d05 to
7d01d21
Compare
|
Rebased onto current |
…gg#431 PR-C0) Decode-time (S=1) expert groups without the host round-trip. Today each VRAM-tier group costs: host gather-memcpy of the input row, H2D, grouped kernels, D2H of every expert row, a per-device stream sync, and a host weighted accumulation — per device, per layer. Under this flag: issue input row P2P from the layer's home device -> broadcast -> grouped-W4 kernels -> per-expert weighting and a fixed-order reduce ON the group's device -> partial sum peer-pushed into a per-issue slot on the home device -> event. Fully async: zero host bytes, zero syncs. take after moe() returns, the home legacy stream waits every issue event and reduces the slots in issue order (deterministic, no atomics), then the accumulator joins the residual exactly where the routed upload used to. The CPU tier overlaps with the issued GPU work through moe() as before — this subsumes the overlap JustVugg#342 was after, with the take phase collapsed into a stream dependency instead of a collection pass. A failed take would silently drop routed experts (a wrong answer, not a slow one), so it is fatal by design; a failed issue falls back to the synchronous path per device. Buffers reserve at the 64-expert cap so re-issue never reallocs under a still-queued stream. Measured (6x RTX 5090, ROUTER=1 TC_W4A16=1, TEMP=0 DRAFT=0): parity at every operating point reachable in a cold-start A/B (96-token: 74.0 vs 74.3 s; 256-token warm segment t=128..256: 2.91 vs 2.89 tok/s) — at hit 94-97% the choreography this removes hides behind ~350-700 MB/token of disk misses. 256-token greedy text is byte-identical across the flag despite the accumulation-order change. The champion regime (hit 99-100%, where the removed syncs sit on the critical path) needs a matured-cache A/B and is the acceptance gate for un-drafting; either way this is the dataflow a single-graph decode (JustVugg#431 PR-C) requires — a graph cannot contain a host-side accumulation.
…gg#431 PR-C0) Decode-time (S=1) expert groups without the host round-trip. Today each VRAM-tier group costs: host gather-memcpy of the input row, H2D, grouped kernels, D2H of every expert row, a per-device stream sync, and a host weighted accumulation — per device, per layer. Under this flag: issue input row P2P from the layer's home device -> broadcast -> grouped-W4 kernels -> per-expert weighting and a fixed-order reduce ON the group's device -> partial sum peer-pushed into a per-issue slot on the home device -> event. Fully async: zero host bytes, zero syncs. take after moe() returns, the home legacy stream waits every issue event and reduces the slots in issue order (deterministic, no atomics), then the accumulator joins the residual exactly where the routed upload used to. The CPU tier overlaps with the issued GPU work through moe() as before — this subsumes the overlap JustVugg#342 was after, with the take phase collapsed into a stream dependency instead of a collection pass. A failed take would silently drop routed experts (a wrong answer, not a slow one), so it is fatal by design; a failed issue falls back to the synchronous path per device. Buffers reserve at the 64-expert cap so re-issue never reallocs under a still-queued stream. Measured (6x RTX 5090, ROUTER=1 TC_W4A16=1, TEMP=0 DRAFT=0): parity at every operating point reachable in a cold-start A/B (96-token: 74.0 vs 74.3 s; 256-token warm segment t=128..256: 2.91 vs 2.89 tok/s) — at hit 94-97% the choreography this removes hides behind ~350-700 MB/token of disk misses. 256-token greedy text is byte-identical across the flag despite the accumulation-order change. The champion regime (hit 99-100%, where the removed syncs sit on the critical path) needs a matured-cache A/B and is the acceptance gate for un-drafting; either way this is the dataflow a single-graph decode (JustVugg#431 PR-C) requires — a graph cannot contain a host-side accumulation.
…gg#431 PR-C0) Decode-time (S=1) expert groups without the host round-trip. Today each VRAM-tier group costs: host gather-memcpy of the input row, H2D, grouped kernels, D2H of every expert row, a per-device stream sync, and a host weighted accumulation — per device, per layer. Under this flag: issue input row P2P from the layer's home device -> broadcast -> grouped-W4 kernels -> per-expert weighting and a fixed-order reduce ON the group's device -> partial sum peer-pushed into a per-issue slot on the home device -> event. Fully async: zero host bytes, zero syncs. take after moe() returns, the home legacy stream waits every issue event and reduces the slots in issue order (deterministic, no atomics), then the accumulator joins the residual exactly where the routed upload used to. The CPU tier overlaps with the issued GPU work through moe() as before — this subsumes the overlap JustVugg#342 was after, with the take phase collapsed into a stream dependency instead of a collection pass. A failed take would silently drop routed experts (a wrong answer, not a slow one), so it is fatal by design; a failed issue falls back to the synchronous path per device. Buffers reserve at the 64-expert cap so re-issue never reallocs under a still-queued stream. Measured (6x RTX 5090, ROUTER=1 TC_W4A16=1, TEMP=0 DRAFT=0): parity at every operating point reachable in a cold-start A/B (96-token: 74.0 vs 74.3 s; 256-token warm segment t=128..256: 2.91 vs 2.89 tok/s) — at hit 94-97% the choreography this removes hides behind ~350-700 MB/token of disk misses. 256-token greedy text is byte-identical across the flag despite the accumulation-order change. The champion regime (hit 99-100%, where the removed syncs sit on the critical path) needs a matured-cache A/B and is the acceptance gate for un-drafting; either way this is the dataflow a single-graph decode (JustVugg#431 PR-C) requires — a graph cannot contain a host-side accumulation.
…Windows CI) The rebase that renamed my PR-C0 entries to *_resident_* left two stubs in backend_loader.c with the new function name but the OLD field name in the availability guard, so the resident_issue stub kept the plain name and collided with JustVugg#342's coli_cuda_expert_group_issue on the MinGW CUDA-DLL build (conflicting types — JustVugg#342's take returns a host pointer, mine reduces on-device). Name and guard field now agree on the _resident_ variant in both stubs. MinGW syntax check clean; make check 105/105.
What
At S≤4 decode on a multi-GPU full-residency host,
moe()runs two phases serially: the VRAM-resident experts' grouped GPU calls, and the CPU rows (RAM-tier + misses). Per-phase measurement on 6× RTX 5090 put them at roughly 50 ms + 50 ms per token — a textbook overlap opportunity.This adds an issue/take split of
coli_cuda_expert_group(launch on the device stream and return; sync at layer end) and a two-pass moe block: pass 1 collects the VRAM experts' groups and issues them async; the CPU loop then computes its rows while the GPUs work; a take phase collects and accumulates. Per-device failure recomputes on the CPU (expert_host_ensurereloads slabs released byCUDA_RELEASE_HOST); any issue failure drops the whole layer back to the existing sync path.Opt-in:
COLI_GROUP_ASYNC=1, default off.Measured (6× RTX 5090, full residency, warm segment 256→512 of a 512-token greedy run,
.coli_usagesnapshot-restored between arms)Reproduced across two independent A/B rounds. The async-issue without the overlap is a wash (+1.5%) — the win is the concurrency, not the removed sync.
The honest caveat (why default-off)
Greedy output under the overlap is not byte-identical to the sync path: a near-tie token flips early in the run (with usage state controlled, so it is not placement drift — some floating-point accumulation-order difference I have not yet pinned down; the kernels are byte-for-byte the sync path's small-batch kernels and the packing order mirrors the sync path's). Same class as the documented CPU-vs-GPU / kernel-family divergences (#100): every emitted token remains the argmax of a valid forward, but the stream forks. Under #294's standard the flag also implies it should stay off during speculation until verified — at S≤4 with drafts live,
SPEC_PINsemantics take precedence.Follow-ups I intend regardless of merge: (a) a position-aligned
ROUTE_TRACEdiff (the #163 method) to locate the first flipped layer and, if it's an ordering bug rather than inherent, fix it to byte-exactness; (b) profiling the remaining serial residue in the overlap window (take-phase device syncs, pass-1 packing).