Skip to content

Use CUDA P2P / NVLink for multi-GPU layer-split hand-off (dual RTX 2080 Ti) #338

Closed

Description

@wxaef

[Feature request] Use CUDA P2P / NVLink for multi-GPU layer-split hand-off (dual RTX 2080 Ti)

Summary

Would you consider adding an optional CUDA P2P / NVLink path for the multi-GPU layer-split hand-off?

Strata currently documents the split as one hand-off per verify window through mapped pinned host memory, and that works well without requiring NVLink. On systems where CUDA peer access is available, especially dual RTX 2080 Ti with NVLink, it may be worth allowing the stage boundary to use direct GPU-to-GPU transfer instead.

A possible opt-in flag could be:

--split-p2p

with the existing mapped pinned-host path kept as the default/fallback.

Test system

  • Linux / Ubuntu 24.04
  • 2× RTX 2080 Ti 22 GB
  • NVLink present (nvidia-smi topo -m reports NV2)
  • Strata engine 0.1.27
  • Swift 1.5 / Qwen3.8-Flash-Next
  • GSQ-RCO IQ3_XXS
  • layer split: auto
  • context: 262144
  • KV: int8
  • KV resident: 32768
  • vision: GPU
  • prefill: 4096
  • VRAM reserve: 1200 MiB

Selected split:

strata serve: layer split: layers 0-25 (CUDA0), 26-47 (CUDA1), one hand-off per window

Runtime:

strata serve: 1475 MiB of VRAM free with everything loaded

strata serve: prompt 201056 tokens = 0 reused + 201056 read in 274704 ms (731.9 tok/s),
              362 generated in 8072 ms (44.8 tok/s),
              drafts accepted 222 of 337

strata serve: decode expert cache hit rate: 92.8% (212845 hits / 229302 lookups)

strata serve: KV streaming: 93.58% of 1485072 block reads hit VRAM,
              384.2 MiB read from RAM

PCIe probes reported by Strata:

CUDA0 host->device: 6.2 GB/s
CUDA1 host->device: 10.9 GB/s

Why this might help

The current implementation intentionally does not require NVLink/P2P and uses a mapped pinned host hand-off, which is great for compatibility.

But on a pair of GPUs with working peer access and NVLink, the boundary could potentially be:

CUDA0 device buffer
        |
        | cudaMemcpyPeerAsync / peer-visible memory
        v
CUDA1 device buffer

instead of:

CUDA0 -> mapped pinned host memory -> CUDA1

Since there is only one hand-off per verify window, I would not expect a huge speedup, but it could potentially:

  • reduce stage-boundary latency;
  • reduce host involvement;
  • avoid PCIe/host-memory jitter;
  • improve prefill pipeline smoothness;
  • slightly reduce fixed decode-window overhead.

The current verifier API already has a clear stage boundary through handoff_in / handoff_out, so this looks like it could be implemented as an optional transport rather than changing the layer-split design.

Suggested behavior

At startup:

  1. Check cudaDeviceCanAccessPeer().
  2. If both stage devices support peer access, enable it with cudaDeviceEnablePeerAccess().
  3. Allocate per-stage device hand-off buffers.
  4. Transfer with cudaMemcpyPeerAsync() or a peer memcpy node inside the CUDA graph.
  5. Synchronize the two stage streams with CUDA events.
  6. Automatically fall back to the existing mapped pinned-host hand-off when peer access is unavailable.

An opt-in implementation such as --split-p2p would make A/B testing easy while preserving the current portable path.

I can test patches and provide before/after prefill, decode, hand-off latency, stability and VRAM measurements on this dual RTX 2080 Ti NVLink system if useful.

Activity

  1. Niko1221 commented on Oct 1, 2026

    @Niko1221
    Owner

    Thanks! A measured opt-in would be welcome: --split-p2p with a cudaDeviceCanAccessPeer check and the normal path as fallback. #294 measured the hand-off at ~1.1 ms per window on PCIe, so please include numbers from your two 2080 Tis. We have no NVIDIA multi-GPU rig.

  2. Niko1221 commented on Oct 1, 2026

    @Niko1221
    Owner

    Thanks for the detailed request and the numbers. A P2P/NVLink hand-off is reasonable as an opt-in (--split-p2p, with the mapped host path kept as the default), but the hand-off is one small transfer per verify window, so we expect only a small gain. We have no NVLink pair to measure it on. I've noted it in our tracking list, and a PR is welcome if it measures faster on your 2080 Ti pair (decode and prompt, with the default unchanged). Closing the issue to keep the tracker for bugs.

  3. yeungyim commented on Oct 2, 2026

    @yeungyim

    Measured it on an NVLink pair, since you mentioned you have none to test on. I implemented --split-p2p to the spec in your comment (cudaDeviceCanAccessPeer + cudaDeviceEnablePeerAccess, the mapped host path kept as the default and as the fallback on any failure) and A/B'd it.

    Correctness: bit-exact. The hand-off carries the same floats either way, so I gated on output identity. A seeded cold prompt produced the same set of outputs under both modes (mapped: A, B, A across three restarts; p2p: B, A) — the variation is run-to-run and comes from the adaptive expert cache re-rounding, not from the transport. The receiving kernel needed no change: its source pointer is already const volatile float4*, which is correct for peer memory too.

    Speed on 2x RTX 3090 (NVLink NV4): 2-3% slower.

    mapped (default) --split-p2p
    prefill tok/s (32K cold) 2678.4 2624.2 -2.0%
    decode tok/s 105.4 101.9 -3.3%
    TTFT 12.42 s 12.67 s +2.0%

    Why it doesn't pay here: the mapped path is already zero-copy — each stage's GPU reads and writes the hand-off buffer in host memory directly, so there is no intermediate copy for peer access to remove. It only swaps "read host memory" for "read the peer's device memory", and on this board (both cards behind the CPU's PCIe lanes, Gen3 x8 each) that is not faster; the NVLink bridge is not chosen for this access pattern.

    So your "we expect only a small gain" was right, and the sign is slightly worse than that.

    Rig: 2x RTX 3090 24 GB, driver 595.91.07, nvidia-smi topo -m reports NV4, Linux, CUDA 13.1, engine 0.1.35 built from source, Qwen3.8-Flash-Next IQ3_XXS, layer split auto (K=26: layers 0-25 / 26-47), int8 KV, 262144 context. Measured with cold prefill — a unique leading salt per request, because the conversation cache otherwise serves a repeated prompt in ~0.4 s and you measure the cache, not the engine — temperature 0, max_tokens 256, median of 3.

    The patch is ~49 lines: the hand-off buffer allocated with cudaMalloc on the sending stage instead of cudaHostAllocMapped, plus the capability check and fallback. Happy to post it if you want it on the record despite the negative result.

  4. vaskes commented on Oct 29, 2026

    @vaskes

    Closing the loop on the numbers asked for here: the 2x RTX 2080 Ti 22 GB with NVLink (NV2), Qwen3.8-Flash-Next GSQ-RCO IQ3_XXS (IQ3_XXS), --layer-split auto (K=26), --kv int8 --kv-resident 32768, context 262144, CUDA0 on a root-complex x16 slot and CUDA1 under a downstream port at x4.

    I implemented the prompt path's hand-off over peer access on the current main (engine 0.1.53) and measured it:

    • the hand-off of 8192 rows (83.9 MB), timed inside the engine: 7.16 ms over peer access (46.9 GB/s, CUDA0 -> CUDA1) against ~36 ms through the host (the producer's download over its x16 at 8.1 GB/s plus the consumer's upload over its x4 at 3.3 GB/s) - the same 5x the V100 pair showed;
    • the model's answers: identical byte for byte (4 prompts, incl. a 22K-token one);
    • end to end (the same binary back to back): 4K prompt 720.4 -> 724.0 tok/s, 16K prompt 1148.6 -> 1139.5 tok/s, decode 73.0 -> 70.2 tok/s - inside the run-to-run noise, so I am not claiming a speedup.

    The reason is measurable and, I think, general for this topology: nvidia-smi dmon during a 16K prompt shows CUDA0 (x16) pulling 6.8 GB/s from the host (67% of its 10.1 GB/s link) and CUDA1 (x4) pulling 3.2 GB/s (97% of its 3.3 GB/s link, saturated). The prompt is bound by the pair's host traffic - the streamed experts - and not by the inter-card hand-off, which is ~36 ms of a 14.4 s prompt (0.24%). The NVLink removes those 36 ms but cannot widen the path to the host; and moving the x4 card's streamed experts over the NVLink instead would need the same bytes over the x16 card's link (~3 GB/s spare against the ~4 GB/s the x4 card pulls), which is a wash too.

    Full write-up, the implementation notes and the environment are in the discussion of PR #1099 (the same proposal): #1099 (comment)

  5. vaskes commented on Oct 29, 2026

    @vaskes

    Correction to the numbers above (the same correction is in the PR #1099 thread): the chunk's rows carry the hyper-connection's four heads, so a row is D = N * HC = 10240 floats, and one 8192-row chunk is 335.5 MB, not 83.9 MB. The host path therefore moves ~143 ms of PCIe time per chunk (the producer's download over its x16 at 8.1 GB/s: 41.7 ms, the consumer's upload over its x4 at 3.3 GB/s: 101.7 ms) against 7.16 ms (46.9 GB/s) over the NVLink - ~20x, not ~5x. The end-to-end numbers and the reason for them do not change (the copies are overlapped with the compute; the pair is bound by the host traffic).

    #1099 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions