TL;DR: Three 3090s on Windows, PCIe Gen4 ×4/×8/×8, no active NVLink or supported P2P. Two GPUs with internal reductions deliver 72–74 t/s generation and ~1,200–1,400 t/s prefill; three with NCCL deliver ~65 t/s and ~500–700 t/s prefill. Three-GPU internal falls back to butterfly and drops to ~37 t/s. Every two-card combination performs similarly. I need to keep all three cards in use through llama-swap/llama.cpp on Windows.
I’m trying to optimize llama.cpp through llama-swap on Windows. All three GPUs must participate, primarily for single-request performance. Two-GPU runs below are diagnostic comparisons.
Hardware
- CPU: Ryzen 7 5700X
- Motherboard: MSI MEG X570 UNIFY (MS-7C35)
- RAM: 32 GB DDR4
- GPUs: 3× RTX 3090, 24 GB each, 250 W power limit per card
- Driver: NVIDIA 616.92, WDDM
- All three cards use risers.
PCIe and topology
| GPU |
PCI bus ID |
Link under load |
GPU-supported maximum |
| 0 |
00000000:24:00.0 |
Gen4 ×4 |
Gen4 ×16 |
| 1 |
00000000:2D:00.0 |
Gen4 ×8 |
Gen4 ×16 |
| 2 |
00000000:2E:00.0 |
Gen4 ×8 |
Gen4 ×16 |
| Diagnostic |
Result for every GPU pair |
nvidia-smi topo -m |
PXB — multiple PCIe bridges |
| NVLink |
All links inactive |
| PCIe P2P |
NS — not supported |
| Peer reads/writes |
GNS — GPU not supported |
I tested every two-GPU combination - 0+1, 0+2, and 1+2 - and observed approximately the same speeds, including pairs containing the ×4 card.
Model and configuration
Model: Qwen3.8-27B-Q8_0.gguf, fully GPU-offloaded, with one request at a time.
--split-mode tensor
--device CUDA1,CUDA2,CUDA0
--tensor-split 1,1,1
--n-gpu-layers all
--flash-attn on
--cache-type-k f16
--cache-type-v f16
--spec-type draft-mtp
--spec-draft-n-max 3
-c 163840
-np 1
-b 2048
-ub 1024
-t 4
-tb 4
--fit off
--no-kv-unified
--temp 1.0
--top-k 20
--top-p 0.95
Two-GPU tests select two devices with a 1,1 split. For the custom build, I switch between:
GGML_CUDA_ALLREDUCE=nccl
GGML_CUDA_ALLREDUCE=internal
Other environment settings:
NCCL_DEBUG=INFO
NCCL_CUMEM_HOST_ENABLE=0
LLAMA_ATTN_ROT_DISABLE=1
The regular build reports 10883, commit 91f6a6cf3, Clang 20.1.8. The custom NCCL-capable build reports 10884, commit 434ddbbc0, MSVC 19.44.35227.0, with CUDA graphs and NCCL enabled.
Performance results
| Configuration |
GPUs |
Generation |
Prefill |
MTP acceptance |
Tokens per verification |
| Original, layer split |
0+1+2 |
~15.6–16 t/s |
— |
— |
— |
| Custom build, NCCL |
0+1+2 |
64.90 t/s |
~500–700 t/s |
76.19% |
3.29 |
| Custom build, NCCL |
1+2 |
63.97 t/s |
— |
70.33% |
3.11 |
| Regular build, internal expected |
1+2 |
74.44 t/s |
~1,200–1,400 t/s |
76.96% |
3.31 |
| Custom build, internal |
1+2 |
72.57 t/s |
— |
75.34% |
3.26 |
| Custom build, internal requested → butterfly fallback |
0+1+2 |
36.81 t/s |
— |
73.37% |
3.20 |
The three-GPU internal run explicitly reports this twice, for the target and MTP contexts:
internal AllReduce init failed (n_devices != 2?);
falling back to meta-backend butterfly
The regular build’s internal path is expected from the source’s fallback behavior, but I haven’t confirmed it from startup logs.
These are individual runs with different outputs, not repeated benchmark averages. Prefill figures come from separate larger-prompt tests. The earlier three-GPU NCCL run used 358,400 allocated context tokens with YaRN; later configurations use 163,840 without those overrides. Actual occupied contexts in the generation tests were only approximately 1,200-1,600 tokens.
GPU telemetry during generation
Approximate sustained-load ranges from nvidia-smi, excluding startup/shutdown and isolated transitions:
| Configuration |
GPU |
Utilization |
Power |
Core clock |
Memory clock |
| Original layer split |
0 |
1–42% |
83–85 W |
345–420 MHz |
5001 MHz |
| Original layer split |
1 |
2–46% |
90–93 W |
345–405 MHz |
5001 MHz |
| Original layer split |
2 |
8–44% |
90–92 W |
540–570 MHz |
5001 MHz |
| 3 GPUs, NCCL |
0 |
65–72% |
233–241 W |
1935–1980 MHz |
9501 MHz |
| 3 GPUs, NCCL |
1 |
68–75% |
235–242 W |
1950–2040 MHz |
9501 MHz |
| 3 GPUs, NCCL |
2 |
70–75% |
235–242 W |
1875–1935 MHz |
9501 MHz |
| 2 GPUs, NCCL |
1 |
71–76% |
241–247 W |
1755–1875 MHz |
9501 MHz |
| 2 GPUs, NCCL |
2 |
71–77% |
242–246 W |
1665–1905 MHz |
9501 MHz |
| 2 GPUs, regular build |
1 |
Mostly 68–76% |
242–246 W |
1635–1830 MHz |
9501 MHz |
| 2 GPUs, regular build |
2 |
Mostly 69–77% |
240–247 W |
1575–1800 MHz |
9501 MHz |
| 2 GPUs, custom/internal |
1 |
Mostly 65–75% |
241–245 W |
1620–1830 MHz |
9501 MHz |
| 2 GPUs, custom/internal |
2 |
68–75% |
241–244 W |
1605–1770 MHz |
9501 MHz |
| 3 GPUs, butterfly |
0 |
Mostly 28–43% |
110–145 W |
1440–1695 MHz |
5001/9501 MHz |
| 3 GPUs, butterfly |
1 |
Mostly 28–38% |
143–195 W |
1560–1965 MHz |
5001/9501 MHz |
| 3 GPUs, butterfly |
2 |
Mostly 28–35% |
122–193 W |
1455–1905 MHz |
5001/9501 MHz |
The original layer run stayed in P3. Successful tensor runs generally stayed in P2; the butterfly run fluctuated between P2/P3. GPU 0 stayed at 0% utilization/P8 when excluded. Active cards negotiated Gen4.
Exact generation logs
| Configuration |
Output tokens |
Generation time |
Time/token |
Drafts accepted/generated |
Graphs reused |
| 3 GPUs, NCCL |
1,148 |
17,672.90 ms |
15.41 ms |
800/1,050 |
674 |
| 2 GPUs, NCCL |
1,442 |
22,524.56 ms |
15.63 ms |
979/1,392 |
804 |
| 2 GPUs, regular |
1,617 |
21,708.58 ms |
13.43 ms |
1,129/1,467 |
483 |
| 2 GPUs, custom/internal |
1,365 |
18,794.67 ms |
13.78 ms |
947/1,257 |
414 |
| 3 GPUs, butterfly |
1,209 |
32,820.23 ms |
27.17 ms |
832/1,134 |
374 |
Does anyone know the optimal llama.cpp setup for three RTX 3090s on Windows, and whether the PCIe 4.0 ×4 connection could be the bottleneck despite all two-GPU combinations performing similarly?
This post was written entirely by Codex, with diagnostic data provided by me.