Skip to content

multi-GPU: second GPU as an opt-in expert-cache tier (--peer-device), resubmit of #229 on v0.1.36 - #531

Closed
q8atnight wants to merge 3 commits into
Niko1221:mainfrom
q8atnight:pr/peer-tier-036-sub
Closed

q8atnight wants to merge 3 commits into
Niko1221:mainfrom
q8atnight:pr/peer-tier-036-sub

Conversation

@q8atnight

@q8atnight q8atnight commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Resubmit of #229 part 1, rebased on v0.1.36, per your closing comment there. --peer-device N puts an adaptive
expert cache on a second visible GPU beside CUDA0's, holding the ranked pairs the primary cache does not hold.
Activations and result rows cross NVLink or another P2P path; the peer computes its experts' rows for decode
windows and prompt chunks. Without the flag everything is exactly as released.

Checklist from #229

  • Rebase past Multi-GPU: the session carve and the per-stage prompt loans #216's session carve and 1024-token streaming: the base is v0.1.36 (both landed in 0.1.30);
    the peer tier is rebased on top and keeps them intact.
  • cudaHostAllocPortable only with a peer: core::set_peer_portable(o.peer_device >= 1) runs once after arg
    parsing; verify.cpp, mtp.cpp and the prefill group table add the Portable flag only then. No layer-split or
    single-GPU run pays for it.
  • Which --layer-split / --expert-cache-device1 combinations are refused: --peer-device now refuses both
    --layer-split (a different second-GPU mode, use one or the other) and --expert-cache-device1..3 (both would fill
    the same card with an expert cache), with a one-line message each; plus the existing refusals (no --expert-profile /
    expert cache, device not visible). Documented in docs/SECOND_GPU.md and in --help (new lines for the --peer-* flags).
  • Auto sizing on 22 GB cards needs Multi-GPU: the session carve and the per-stage prompt loans #216's lending: not in this PR. --peer-reserve-mib and --peer-slots
    are the manual knobs; the docs say so and name Multi-GPU: the session carve and the per-stage prompt loans #216 as the follow-up.

Numbers (2x RTX 3090 + NVLink, IQ3_S, v0.1.36)

v0.1.36 prefill 8K / 32K / 128K (t/s) decode 8K / 32K / 128K (t/s) 3,000-token story
stock, one 3090 2093 / 2201 / 2019 96.7 / 91.3 / 86.0 88 t/s
+ this PR (--peer-device 1 --peer-reserve-mib 1850) 2401 / 2747 / 2514 (+15 / +25 / +25 %) 115.2 / 104.5 / 99.6 (+19 / +14 / +16 %) 97 t/s

Our private build with the follow-ups (GDN head split etc.) reaches 2657 / 3118 / 3010.

Exactness

Gate: STRATA_IQ_MT_MIN=1 + our local debug STRATA_MMQ_NO_STREAMK=1, --adapt-swaps 0, same expert set
(single --expert-cache 6000 vs dual --expert-cache 3000 + --peer-slots 3950).

  • v0.1.36 single reproduces our 0.1.31 single reference (flashbench gate PASS).
  • This build without --peer-device == v0.1.36 single (flashbench gate + 19.5K-token long gate PASS): default unchanged.
  • This build with --peer-device 1 == v0.1.36 single (both gates PASS): byte-identical.

Independent data

Limits / follow-ups

A second visible CUDA GPU (N >= 1) holds an adaptive expert cache beside
CUDA0's, filled with the ranked pairs the primary does not hold.  Activations
and result rows cross NVLink or another P2P path; the peer also computes its
experts' rows for prompt chunks (MMQ; the fused path is not combined yet).
Knobs: --peer-device N, --peer-reserve-mib, --peer-slots, --peer-adapt-swaps,
--peer-prefill-rows; sizing stays manual until Niko1221#216's buffer lending lands.
Default output unchanged: without the flag byte-identical to v0.1.36, with it
byte-identical under the exactness gate.  cudaHostAllocPortable only with a
peer.  docs/SECOND_GPU.md documents the flags and refused combinations.
Measured on 2x RTX 3090 + NVLink, IQ3_S: prefill +15/+25/+25 %, decode +19/+14/+16 %.
…device1..3 (both would use the same second card); --help lines for the --peer-* flags
Niko1221 added a commit that referenced this pull request Oct 3, 2026
…peer declines the prompt path (#531 follow-up)

fused_ring() turns the fused ring sizing off for any --peer-device session (core::peer_portable), but the layer's
fused choice tested the peer's prompt-path state (m.pp), which is null when set_peer declines (no P2P, the buffers do
not fit, --peer-prefill-rows 0). Both now follow the flag, so a peer session's prompt path is MMQ with the MMQ ring
and buffers it was sized for. Without --peer-device nothing changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Niko1221 added a commit that referenced this pull request Oct 3, 2026
peer_experts.cpp calls cudaDeviceCanAccessPeer, cudaDeviceEnablePeerAccess and cudaMemcpyPeerAsync and tests
cudaErrorPeerAccessAlreadyEnabled; the HIP shim had none of them, so the Windows HIP build stopped there. Mapped to
their hip* names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Niko1221 added a commit that referenced this pull request Oct 3, 2026
…the GPU (#473), IQ4_XS on AVX-2 (#415), unbuffered loads on Windows (#357 #362), --peer-device (#531), a 6 GB card starts (#496), PR batch; setup requires it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Niko1221 added a commit that referenced this pull request Oct 3, 2026
…l's expert count (#531 follow-up)

#531's peer-aware lookup indexed the residency table with the 512-expert constant NE instead of the model's own
count. The Coder has 256 experts per layer, so from its second layer a resident expert was looked up in the wrong row,
and a prompt long enough for the streamed walk stopped with "an expert is neither resident, streamed nor on the peer"
(found by the 57K real-use check). The 512-expert models index the same row as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
@Niko1221

Niko1221 commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Released in 0.1.38 as an opt-in (--peer-device); without the flag the output is unchanged. Three follow-ups of ours: the streamed walk's resident lookup indexes by the model's own expert count (it used 512, which broke the Coder's 256-expert layers on long prompts); a peer session never takes the fused prompt path, even when the peer declines the prompt path, so the ring and buffers match what fused_ring() sized; and the HIP shim gained the peer-access calls (the Windows HIP build stopped on them). Merged under your name (GitHub doesn't show it as merged because it went in by hand). Thank you!

@Niko1221 Niko1221 closed this Oct 3, 2026
rwkeyes added a commit to rwkeyes/Strata_Dirigo that referenced this pull request Oct 3, 2026
Upstream v0.1.38: faster prompts (one gather per expert group Niko1221#372, the first chunk's PLE rows
beside layer 0 Niko1221#374, DeltaNet three heads per thread Niko1221#413), --kv q4_0 prompts on tensor cores
Niko1221#452, Q5_0 experts on the GPU Niko1221#473, IQ4_XS on AVX-2 Niko1221#415, unbuffered expert loading on Windows
Niko1221#357/Niko1221#362, the --peer-device second-GPU expert tier Niko1221#531, a 6 GB card starts Niko1221#496, the Host
header / DNS-rebinding checks, PR batch.

Our side: the AVX1 floor (STRATA_ISA_FLOOR, cpu_avx1_ok, the relaxed gate, the scalar router
fallback), the AVX1 router kernel (kq_avx1.cpp), the retune patch + setup hooks.
@q8atnight

Copy link
Copy Markdown
Contributor Author

Thanks — great to see it in 0.1.38, and the three follow-ups are the right calls. The n_expert indexing one in particular is the kind of bug our exactness gate could never catch (our 512-expert models index the same row), and keeping the fused prompt path off whenever --peer-device is set closes the ring/buffer mismatch before anyone combining it with STRATA_PF_FUSED=1 hits it.

We rebased our own tree onto 0.1.38 tonight (our peer-tier superset kept, your follow-ups taken): the 0.1.38 single-GPU path still matches our 0.1.36 gate reference bit for bit on the 3090s, and the dual path passes the same gate.

Heads-up on the follow-up we have queued: the prompt path can split every GDN layer's heads across the two GPUs on top of the peer tier (opt-in STRATA_PF_GDN_SPLIT=1, default off). With a split-K-matched cuBLASLt product for the half shapes it stays bit-identical to the unsplit path (gate + 20K-token long gate pass both ways) and adds +6–8 % prompt reading on 2× RTX 3090 + NVLink (8K/32K/128K). Now that the peer tier is official, we'll rebase it onto --peer-device itself and open it as a small PR against 0.1.38 — shout if you'd rather it wait for anything in flight.

@pdoyle12

pdoyle12 commented Oct 3, 2026 •

Copy link
Copy Markdown

I'm sure this is a very stupid question, but how do we disable --layer-split so we can try this flag? I've tried every combination of deleting the flag, setting to -1, 0, setting --no-layer-split that I can think of, but I haven't found the right combination. If it's not explicitly set, it's set to auto automatically, and no other setting I can think of turns it off.

Edit: the fix appeared within a minute of my comment. That is some fast work! :)

fks pushed a commit to fks/Strata that referenced this pull request Oct 3, 2026
With several GPUs in the config's "gpu" the server appended --layer-split auto, which the engine refuses beside
--peer-device (Niko1221#531), so the peer tier could not be started through the server at all:

    strata generate: --peer-device cannot be combined with --layer-split (use one or the other)

The split (and --split-skip-if-fits) is now added only when --peer-device is not among the args; the start line
names the peer tier instead of a layer split. Tests in serve/test_server.py (PeerDevice), docs/SECOND_GPU.md says
how to configure it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@q8atnight

Copy link
Copy Markdown
Contributor Author

Not a stupid question, and on a stock install there is genuinely no way to switch it off — that is why every combination you tried failed:

  • the server adds the split itself whenever the config names more than one GPU
    (serve/server.py: if len(gpu_list(cfg)) > 1 and "--layer-split" not in args: args += ["--layer-split", …]);
  • the engine has no "off" value: --layer-split accepts auto or a rising list of layer numbers starting at 2,
    anything else exits with --layer-split K[,K2..]|auto needs --serve, rising K from 2, …. So -1, 0, none can
    never work, and --no-layer-split does not exist.

Still true on 0.1.39 (checked today's serve/server.py) — the quick fix you noticed is fks's #665, which is open, not merged yet.

Workaround that works today (this is how we run it on 2× RTX 3090): keep the config's "gpu" entry to a single
card (or leave it out) so the server never appends the split, and put the peer flags in the args instead:

"args": ["--peer-device", "1", "--peer-reserve-mib", "1850", "…"]

The peer card is picked from the args, so it does not need to be in the "gpu" list. Two things to watch while you are
there: don't combine --peer-device with --expert-cache-device1..3 / --expert-cache-remote (both fill the same
card and you end up with the experts resident twice), and the --peer-* flags have no --help usage lines yet —
docs/SECOND_GPU.md is the reference.

fks's #665 makes this unnecessary by skipping the automatic --layer-split (and --split-skip-if-fits) as soon as
--peer-device is in the args, and updates docs/SECOND_GPU.md — worth merging, since the current failure mode
(--peer-device cannot be combined with --layer-split) looks like the engine refusing your hardware rather than the
server adding a flag you never asked for.

@pdoyle12

pdoyle12 commented Oct 4, 2026

Copy link
Copy Markdown

Thank you for the explanation! I pulled in #665 manually and rebuilt, and it's working fine now as you described.

I'll mention in passing that for my rig -- a 5090+3090 with 256 GB of RAM running the Unsloth Q4 quant -- this flag noticeably bumped performance, and I'm seeing ~1275 t/s prefill and ~90 t/s generation, which is frankly astonishing.

Niko1221 added a commit that referenced this pull request Oct 6, 2026
…peer declines the prompt path (#531 follow-up)

fused_ring() turns the fused ring sizing off for any --peer-device session (core::peer_portable), but the layer's
fused choice tested the peer's prompt-path state (m.pp), which is null when set_peer declines (no P2P, the buffers do
not fit, --peer-prefill-rows 0). Both now follow the flag, so a peer session's prompt path is MMQ with the MMQ ring
and buffers it was sized for. Without --peer-device nothing changes.
Niko1221 added a commit that referenced this pull request Oct 6, 2026
peer_experts.cpp calls cudaDeviceCanAccessPeer, cudaDeviceEnablePeerAccess and cudaMemcpyPeerAsync and tests
cudaErrorPeerAccessAlreadyEnabled; the HIP shim had none of them, so the Windows HIP build stopped there. Mapped to
their hip* names.
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…the GPU (#473), IQ4_XS on AVX-2 (#415), unbuffered loads on Windows (#357 #362), --peer-device (#531), a 6 GB card starts (#496), PR batch; setup requires it
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…l's expert count (#531 follow-up)

#531's peer-aware lookup indexed the residency table with the 512-expert constant NE instead of the model's own
count. The Coder has 256 experts per layer, so from its second layer a resident expert was looked up in the wrong row,
and a prompt long enough for the streamed walk stopped with "an expert is neither resident, streamed nor on the peer"
(found by the 57K real-use check). The 512-expert models index the same row as before.
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…peer declines the prompt path (Niko1221#531 follow-up)

fused_ring() turns the fused ring sizing off for any --peer-device session (core::peer_portable), but the layer's
fused choice tested the peer's prompt-path state (m.pp), which is null when set_peer declines (no P2P, the buffers do
not fit, --peer-prefill-rows 0). Both now follow the flag, so a peer session's prompt path is MMQ with the MMQ ring
and buffers it was sized for. Without --peer-device nothing changes.
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
peer_experts.cpp calls cudaDeviceCanAccessPeer, cudaDeviceEnablePeerAccess and cudaMemcpyPeerAsync and tests
cudaErrorPeerAccessAlreadyEnabled; the HIP shim had none of them, so the Windows HIP build stopped there. Mapped to
their hip* names.
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…iko1221#452), Q5_0 experts on the GPU (Niko1221#473), IQ4_XS on AVX-2 (Niko1221#415), unbuffered loads on Windows (Niko1221#357 Niko1221#362), --peer-device (Niko1221#531), a 6 GB card starts (Niko1221#496), PR batch; setup requires it
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…l's expert count (Niko1221#531 follow-up)

Niko1221#531's peer-aware lookup indexed the residency table with the 512-expert constant NE instead of the model's own
count. The Coder has 256 experts per layer, so from its second layer a resident expert was looked up in the wrong row,
and a prompt long enough for the streamed walk stopped with "an expert is neither resident, streamed nor on the peer"
(found by the 57K real-use check). The 512-expert models index the same row as before.
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…peer declines the prompt path (Niko1221#531 follow-up)

fused_ring() turns the fused ring sizing off for any --peer-device session (core::peer_portable), but the layer's
fused choice tested the peer's prompt-path state (m.pp), which is null when set_peer declines (no P2P, the buffers do
not fit, --peer-prefill-rows 0). Both now follow the flag, so a peer session's prompt path is MMQ with the MMQ ring
and buffers it was sized for. Without --peer-device nothing changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
peer_experts.cpp calls cudaDeviceCanAccessPeer, cudaDeviceEnablePeerAccess and cudaMemcpyPeerAsync and tests
cudaErrorPeerAccessAlreadyEnabled; the HIP shim had none of them, so the Windows HIP build stopped there. Mapped to
their hip* names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…iko1221#452), Q5_0 experts on the GPU (Niko1221#473), IQ4_XS on AVX-2 (Niko1221#415), unbuffered loads on Windows (Niko1221#357 Niko1221#362), --peer-device (Niko1221#531), a 6 GB card starts (Niko1221#496), PR batch; setup requires it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…l's expert count (Niko1221#531 follow-up)

Niko1221#531's peer-aware lookup indexed the residency table with the 512-expert constant NE instead of the model's own
count. The Coder has 256 experts per layer, so from its second layer a resident expert was looked up in the wrong row,
and a prompt long enough for the streamed walk stopped with "an expert is neither resident, streamed nor on the peer"
(found by the 57K real-use check). The 512-expert models index the same row as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Niko1221 pushed a commit that referenced this pull request Oct 7, 2026
With several GPUs in the config's "gpu" the server appended --layer-split auto, which the engine refuses beside
--peer-device (#531), so the peer tier could not be started through the server at all:

    strata generate: --peer-device cannot be combined with --layer-split (use one or the other)

The split (and --split-skip-if-fits) is now added only when --peer-device is not among the args; the start line
names the peer tier instead of a layer split. Tests in serve/test_server.py (PeerDevice), docs/SECOND_GPU.md says
how to configure it.

Ported by hand onto the 0.1.40 engine_args (layer_split_value); the start banner now says the peer tier is in use when the split is not.

(PR #665)
0xrjman pushed a commit to 0xrjman/Strata that referenced this pull request Oct 7, 2026
With several GPUs in the config's "gpu" the server appended --layer-split auto, which the engine refuses beside
--peer-device (Niko1221#531), so the peer tier could not be started through the server at all:

    strata generate: --peer-device cannot be combined with --layer-split (use one or the other)

The split (and --split-skip-if-fits) is now added only when --peer-device is not among the args; the start line
names the peer tier instead of a layer split. Tests in serve/test_server.py (PeerDevice), docs/SECOND_GPU.md says
how to configure it.

Ported by hand onto the 0.1.40 engine_args (layer_split_value); the start banner now says the peer tier is in use when the split is not.

(PR Niko1221#665)
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.

3 participants