Repository navigation
Conversation
Test hardware
Prefill benchmark results
There is no "Before" benchmark for the 64 GB configuration, so no optimization percentage can be calculated for the 64 GB results. For the 48 GB configuration, the optimization has little effect at 8K, but the benefit increases significantly with prompt size: about 10% at 16K, 20% at 32K, and 28% at 64K. |
|
Tested with qwen3.8-flash-next-iq3_xxs |
|
I tested the suggestions #559 on top of #598 on a 4× RTX 3060 system with I added two experimental changes:
Test setup:
I tested all four combinations:
The dedicated async D2H stream did not improve performance. With queue depth 1 it was actually around 0.6–1.8% slower, and with depth 2 it was essentially neutral compared with the synchronous handoff. Increasing the handoff depth from 1 to 2 without the async copy stream produced only a small improvement, roughly 1% in the latest back-to-back run. So the main proposed async-copy optimization does not appear to address the remaining bottleneck on this setup. For now I would therefore not add the dedicated copy-stream implementation to #598. The only potentially useful part is the slightly deeper handoff queue, but the gain is small enough that it probably needs more validation before being included. |
Prefill chunk-size measurementsTest setup:
Change relative to
The results suggest that smaller prefill chunks can significantly improve the multi-GPU layer-split pipeline introduced by #598, but simply making the chunks as small as possible is not beneficial.
The optimum also depends on prompt size. For the ~5.9K-token prompt, This suggests that the current |
…mpt's experts A prompt that fits one chunk runs a layer split's stages one after the other, so while one card reads its layers the other idles. --peer-device already has the mechanism for this (PeerPrefill's peer streaming: a share of the experts the primary streams goes over the peer's own PCIe link and is computed there), but it needs P2P and is refused with --layer-split. This makes PeerPrefill usable as a stream-only peer between the stages: - set_stage_helper: each stage's prompt path gets the next stage's (the last one the first's); a stream-only PeerPrefill holds no experts (peer == null), so every use of the peer tier is guarded - per prompt, bind_stage_helper points it at the idle stage's own lent prompt buffers (mixed, Xq, GU, H, Hq, Dm, ring, MMQ context): no VRAM of its own - without P2P the activations, the row table, the group bounds and the result rows go through mapped host memory, read by copy kernels on the device that needs them. Copy-engine uploads queued behind the ring's expert blobs (the grouping tables' reason for mapped memory), which cost ~20 ms per layer and ate the whole gain; cudaMemcpyPeerAsync without P2P is staged by the driver - only for one-chunk prompts on the MMQ path (not the fused kernels, not with --peer-device), with a share that falls with the chunk: min(0.45, 0.5 - T/16384), off below 0.3 (~3.3K tokens) - measured, below - STRATA_PREFILL_HELP=0 turns it off, STRATA_PREFILL_HELP_FRAC fixes the share 2x RTX 3090 (no P2P), UD-Q4_K_XL, layer split auto, 262K context, --adapt-swaps 0, prompt tok/s without / with: 1.1K 496 / 723, 1.5K 674 / 833, 2K 878 / 1128, 2.5K 1017 / 1296, 3K 1260 / 1440, 4K 1605 / 1607, 8K 2215 / 2221; decode unchanged. The output is repeatable (same prompt, same answer) but not bit-identical to STRATA_PREFILL_HELP=0's: the helped rows are computed in another MMQ grouping. 0.1.39b (Niko1221): opt-in (STRATA_PREFILL_HELP=1), since the helped rows round differently from the default; merged with #598 (run_impl / next_run_). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks! Taken into 0.1.39. With one or two GPUs the pipeline runs exactly as before: the last stage has no successor, so nothing waits differently. We checked one GPU here (output byte-identical); the 3+ GPU overlap and the +10-28% prompt figures are your 4x RTX 3060 measurements. We merged it with #663. |
217 upstream commits (0.1.38 -> 0.1.39: Niko1221#646 decode kernels and zero-doorbell verify graph, Niko1221#603 long-context top-k, Niko1221#583 byte-budget ring, Niko1221#598 pipelined layer split, Niko1221#578 helper GPUs, Niko1221#663, Niko1221#540, Niko1221#600 in the experimental build, batch slots Niko1221#465, the SYCL port, older-CPU builds, ...). 23 files conflicted (~70 hunks); every hunk was resolved by hand and checked against both sides. Resolution rules: - The prompt path's stage overlap: upstream's Niko1221#598 (equivalent to our e85f7ee; run() drains the whole chain on every exit), with our release_inherited_pin in the stage threads and our early ids copy for the host grouping (a5a3254) kept. - STRATA_FP16=load's form-aware weights kept wherever upstream's new code assumed BF16: the shared-expert gate (sigmoid+scale fused as upstream), GDN alpha/beta (our kernels; upstream's new gdn_step_commit_kernel added), the hc v3 kernels (upstream's EXACT_T with our dot8w). - verify.cpp: upstream's all-resident fast path, plan copy and Niko1221#578 rows, with our 2-GPU fetch overlap and partner fetch back in the non-resident branch; mapped buffers stay portable (the partner reads them). - Volta prompt attention: upstream's dispatch (the m8n8k4 kernel on sm_70 in this build) with our split hi/lo accumulator chains inside it; the wmma kernel stays opt-in (STRATA_ATTN_WMMA=1 without STRATA_ATTN_M884). - qsa_decode_attn.cu: upstream's (our adf56cc imported the same PR Niko1221#540; upstream's is it plus review fixes, bit-exact). - POWER / aarch64: upstream's new x86 intrinsics in verify.cpp through the portable strata_store_fence / strata_cpu_pause; its AVX2 start-up checks and log line behind x86 guards; stubs for the new CPU probes; the router lookahead keeps portable.cpp's VSX dot on non-x86 (upstream's dispatch would have sent POWER to its scalar fallback); the pool's ThreadAffinity (any CPU count) with our note_host_pin; SMT2 selection with Niko1221#642's sort. - setup.py / tests: both sides (CUDA 12 engine, ISA floor, ARM / non-x86). Checked: - every line either side added since 0.1.38 is in the merge or replaced on purpose (script over all 45 files both sides changed); - all parity tests pass (iq, native_expert on the real shards, mmvq_multi, gr, gdn, gdn_rec, qsa, qsa_topk, elementwise, router_dot, shared_expert, kv_*, sampler, rope, s2/s_gemv, gemm_bf16, ...); - teacher-forced 2K and 32K logs byte-identical to the pre-merge build (4 GPUs, launch config); - 4 GPUs: decode text identical, same windows and acceptance, 28.25 -> 26.9 ms/window (~86 -> ~90 tok/s); prompts 7.8K 3,992 -> 4,038, 65K 7,102 -> 7,172, 123K 7,323 -> 7,334 tok/s; --prefill-ring 320 applies; - 2 GPUs: decode texts identical to the pre-merge binary on three prompts, decode 61.5-71.0 -> 63.5-72.7 tok/s, 65K prompt 3,975 -> 4,083. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZHWKWn7UYZV64KwpGrWuN
|
@Niko1221 Thanks for the merge! I compared the current 0.1.39 version using several fixed prefill sizes ( Cache: unique nonce is placed at the START of every prompt All values below are median prefill throughput in tok/s.
The fixed prefill-size tests also show that So on this system, 0.1.39 roughly doubles long-context prefill performance compared with 0.1.38. At the largest tested prompt, throughput increased from 1,128.6 tok/s to 2,472.6 tok/s, or about 2.19× the previous performance. |
…mpt's experts A prompt that fits one chunk runs a layer split's stages one after the other, so while one card reads its layers the other idles. --peer-device already has the mechanism for this (PeerPrefill's peer streaming: a share of the experts the primary streams goes over the peer's own PCIe link and is computed there), but it needs P2P and is refused with --layer-split. This makes PeerPrefill usable as a stream-only peer between the stages: - set_stage_helper: each stage's prompt path gets the next stage's (the last one the first's); a stream-only PeerPrefill holds no experts (peer == null), so every use of the peer tier is guarded - per prompt, bind_stage_helper points it at the idle stage's own lent prompt buffers (mixed, Xq, GU, H, Hq, Dm, ring, MMQ context): no VRAM of its own - without P2P the activations, the row table, the group bounds and the result rows go through mapped host memory, read by copy kernels on the device that needs them. Copy-engine uploads queued behind the ring's expert blobs (the grouping tables' reason for mapped memory), which cost ~20 ms per layer and ate the whole gain; cudaMemcpyPeerAsync without P2P is staged by the driver - only for one-chunk prompts on the MMQ path (not the fused kernels, not with --peer-device), with a share that falls with the chunk: min(0.45, 0.5 - T/16384), off below 0.3 (~3.3K tokens) - measured, below - STRATA_PREFILL_HELP=0 turns it off, STRATA_PREFILL_HELP_FRAC fixes the share 2x RTX 3090 (no P2P), UD-Q4_K_XL, layer split auto, 262K context, --adapt-swaps 0, prompt tok/s without / with: 1.1K 496 / 723, 1.5K 674 / 833, 2K 878 / 1128, 2.5K 1017 / 1296, 3K 1260 / 1440, 4K 1605 / 1607, 8K 2215 / 2221; decode unchanged. The output is repeatable (same prompt, same answer) but not bit-identical to STRATA_PREFILL_HELP=0's: the helped rows are computed in another MMQ grouping. 0.1.39b (Niko1221): opt-in (STRATA_PREFILL_HELP=1), since the helped rows round differently from the default; merged with #598 (run_impl / next_run_).
…mpt's experts A prompt that fits one chunk runs a layer split's stages one after the other, so while one card reads its layers the other idles. --peer-device already has the mechanism for this (PeerPrefill's peer streaming: a share of the experts the primary streams goes over the peer's own PCIe link and is computed there), but it needs P2P and is refused with --layer-split. This makes PeerPrefill usable as a stream-only peer between the stages: - set_stage_helper: each stage's prompt path gets the next stage's (the last one the first's); a stream-only PeerPrefill holds no experts (peer == null), so every use of the peer tier is guarded - per prompt, bind_stage_helper points it at the idle stage's own lent prompt buffers (mixed, Xq, GU, H, Hq, Dm, ring, MMQ context): no VRAM of its own - without P2P the activations, the row table, the group bounds and the result rows go through mapped host memory, read by copy kernels on the device that needs them. Copy-engine uploads queued behind the ring's expert blobs (the grouping tables' reason for mapped memory), which cost ~20 ms per layer and ate the whole gain; cudaMemcpyPeerAsync without P2P is staged by the driver - only for one-chunk prompts on the MMQ path (not the fused kernels, not with --peer-device), with a share that falls with the chunk: min(0.45, 0.5 - T/16384), off below 0.3 (~3.3K tokens) - measured, below - STRATA_PREFILL_HELP=0 turns it off, STRATA_PREFILL_HELP_FRAC fixes the share 2x RTX 3090 (no P2P), UD-Q4_K_XL, layer split auto, 262K context, --adapt-swaps 0, prompt tok/s without / with: 1.1K 496 / 723, 1.5K 674 / 833, 2K 878 / 1128, 2.5K 1017 / 1296, 3K 1260 / 1440, 4K 1605 / 1607, 8K 2215 / 2221; decode unchanged. The output is repeatable (same prompt, same answer) but not bit-identical to STRATA_PREFILL_HELP=0's: the helped rows are computed in another MMQ grouping. 0.1.39b (Niko1221): opt-in (STRATA_PREFILL_HELP=1), since the helped rows round differently from the default; merged with Niko1221#598 (run_impl / next_run_).
…mpt's experts A prompt that fits one chunk runs a layer split's stages one after the other, so while one card reads its layers the other idles. --peer-device already has the mechanism for this (PeerPrefill's peer streaming: a share of the experts the primary streams goes over the peer's own PCIe link and is computed there), but it needs P2P and is refused with --layer-split. This makes PeerPrefill usable as a stream-only peer between the stages: - set_stage_helper: each stage's prompt path gets the next stage's (the last one the first's); a stream-only PeerPrefill holds no experts (peer == null), so every use of the peer tier is guarded - per prompt, bind_stage_helper points it at the idle stage's own lent prompt buffers (mixed, Xq, GU, H, Hq, Dm, ring, MMQ context): no VRAM of its own - without P2P the activations, the row table, the group bounds and the result rows go through mapped host memory, read by copy kernels on the device that needs them. Copy-engine uploads queued behind the ring's expert blobs (the grouping tables' reason for mapped memory), which cost ~20 ms per layer and ate the whole gain; cudaMemcpyPeerAsync without P2P is staged by the driver - only for one-chunk prompts on the MMQ path (not the fused kernels, not with --peer-device), with a share that falls with the chunk: min(0.45, 0.5 - T/16384), off below 0.3 (~3.3K tokens) - measured, below - STRATA_PREFILL_HELP=0 turns it off, STRATA_PREFILL_HELP_FRAC fixes the share 2x RTX 3090 (no P2P), UD-Q4_K_XL, layer split auto, 262K context, --adapt-swaps 0, prompt tok/s without / with: 1.1K 496 / 723, 1.5K 674 / 833, 2K 878 / 1128, 2.5K 1017 / 1296, 3K 1260 / 1440, 4K 1605 / 1607, 8K 2215 / 2221; decode unchanged. The output is repeatable (same prompt, same answer) but not bit-identical to STRATA_PREFILL_HELP=0's: the helped rows are computed in another MMQ grouping. 0.1.39b (Niko1221): opt-in (STRATA_PREFILL_HELP=1), since the helped rows round differently from the default; merged with Niko1221#598 (run_impl / next_run_). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Prefill: allow deeper overlap across multi-GPU pipeline stages
Problem
In the current multi-GPU prefill path, every stage launches the next stage asynchronously, but still waits for that stage to finish before returning.
With more than two GPUs this creates a recursive wait chain:
As a result, intermediate stages cannot accept the next prompt chunk while later GPUs are still processing the previous one.
On a 4-GPU setup this can lead to visibly staggered GPU utilization during prompt processing instead of a fully filled pipeline.
Change
This PR changes the lifetime of the per-stage asynchronous hand-off.
Intermediate stages now return after handing their chunk to the direct successor instead of draining the complete remaining GPU chain.
The pipeline is drained only once, when the outer/root
Prefill::run()has submitted all prompt chunks.Conceptually, the current behavior is closer to:
The intended behavior after this change is:
This does not introduce tensor parallelism and does not change the CUDA kernels or model computation.
It only increases overlap between existing layer-split prefill stages.
Implementation
The change:
std::futurefrom localPrefill::run()state to thePrefillinstancePrefillinstanceExpected effect
The main benefit should be visible on configurations with 3 or more GPUs and prompts large enough to contain several prefill chunks.
For example, with four GPUs and a 2K prefill chunk size, a 32K prompt provides enough chunks to keep all stages active concurrently after pipeline warm-up.
Short prompts or a single prefill chunk should see little or no improvement.
Decode/token-generation performance is not expected to change.
Motivation / observed behavior
On a 4× RTX 3060 system, prompt processing currently shows GPU activity progressing largely stage by stage, while decode performance is normal.
This suggests that part of the low prefill throughput may come from the recursive synchronization between layer-split stages rather than raw GPU compute.
This PR is intended as a small, isolated change to test that hypothesis without changing expert streaming, tensor parallelism, CUDA kernels, or the model architecture.
The most useful validation is whether multiple GPUs stay active concurrently during long prompt prefill and whether prompt throughput improves without affecting model output or decode performance.