Skip to content

Host-bridge TP4: cross-quadrant peer-read stalls and fan-out write collapse make decode ~40% slower; forced phased-write PoC reaches switch level #146

Closed

Description

@voipmonitor

Summary

On frank2, DeepSeek V4.1 TP4 on GPUs behind different CPU IO-die quadrants (0,4,8,12) runs ~40% slower per decode round than on switch-local GPUs (12-15). The same package and settings are used on both. Two fabric effects cause it:

  1. Peer reads across quadrants stall in periodic windows (1-1.6 ms per 80 KiB read). This hits pull, bulk_pull and every other pull-style transport.
  2. Writes that fan out to several quadrants at once collapse by 8-13x. This hits full, payload_rsag and bulk_push above a few KiB.

Every joint mode in the GB202 ladder is one of these two kinds, so the runtime race can only pick between stalling reads and collapsing writes.

A proof of concept forces write-only, one-destination-at-a-time ("phased") collectives through a ladder with one profile per bucket. The rest of the package is unchanged. It brings host-bridge GPUs to the level of the switches:

DS4.1 c32 CSF, frank2 current (Luke's GB202 ladder) on 0,4,8,12 PoC on 0,4,8,12 current on switches 12-15 current on switches 0-3 PoC on switches 0-3
GPU time per b1w8 round 13.9-14.1 ms 8.2-8.3 ms 7.6 ms 7.6 ms 8.0-8.1 ms
c16 tok/s 1001-1102 1466-1474 1524-1559 1558-1578 1482-1499
c32 tok/s 1007-1764 2155-2204 2309-2312 2266-2306 2162-2239

The forced phased modes also run on switches (0-3) and cost 4-6% there. On host bridges the PoC is within 1-2% of the same PoC on switches: once reads and fan-out are gone, the host-bridge fabric adds almost nothing.

PoC branches. The phased kernels are Claude-written, so they are not mergeable under design-review principle 14:


1. Hardware and topology

frank2:

  • AMD EPYC 9575F, 1 socket, NPS1;
  • kernel pcie_aspm=off pcie_port_pm=off amd_iommu=off iommu=off;
  • driver 615.71.10;
  • 17x RTX PRO 6000 behind c-payne Switchtec Gen5 boards.

Each switch chip is split into two partitions with separate x16 uplinks, and both uplinks land in the same IO-die quadrant (one NBIO):

quadrant (NBIO) host bridges GPUs
0 00, 20 0,1 / 2,3
1 40, 60 4,5 / 6,7
2 80, a0 8,9 / 10,11
3 c0, e0 12,13 / 14,15,16

GPUs 0-3 are all in quadrant 0, which is why 0-3 performs like frank1. GPUs 0,4,8,12 span four quadrants. Side note: GPU 16 links at Gen5 x4.

2. What we measured

The tool is a standalone harness (benchmarks/experimental/b12x/pcie_fabric_stall/ on the FI branch):

  • one process, one persistent kernel per GPU;
  • every operation is timestamped with %globaltimer (start, after a cross-GPU start barrier, end), with clock alignment;
  • patterns: read/write ring/all/incast/pair, one-shot pull/push, phased push, ring, RSAG push/phased/pull, host read/write;
  • a second harness runs NCCL all-reduce inside CUDA graphs with per-operation stamps.

2.1 Cross-quadrant peer reads stall in periodic windows

  • Windows: every quadrant has a ~150-200 ms window every ~2.30 s. The quadrants take turns ~300 ms apart (00 -> 40 -> 80 -> c0). The phase is fixed in wall-clock time and does not depend on the measuring process. A 4-quadrant group therefore has a degraded target ~30% of the time.
  • Size of the slowdown: inside a window, reads whose target is in that quadrant are 10-50x slower. Single 80 KiB reads take 1.1-1.6 ms, and up to 2.7 ms at 400 KiB.
  • Not the GPU: a reader behind the same switch reads the same target at the same moment without any stall.
  • Within one quadrant reads are clean: reads between host bridges 00 and 20 (GPU 1 <-> 2,3) had 0 stalls in 700k reads.
  • Posted writes never stall: write-only patterns peaked at <= 55 us over 150k+ operations spanning many windows. Host-memory reads and writes are clean too.

2.2 Fan-out writes across quadrants collapse

  • A GPU that interleaves writes to destinations in different quadrants is 8x (3 quadrants) to 13x (4 quadrants) slower than one writing one destination at a time.
  • Interleaving to one remote quadrant, or within a switch, is fine.
  • This looks like the source side ordering posted writes across fabric destinations. Rotating destinations per GPU-wide phase, or a ring, avoids it.

2.3 Same patterns on 0-3 versus 0,4,8,12

80 KiB, 16 CTAs; per-GPU mean / max per operation, including the ~5.5 us start barrier.

pattern 0-3 0,4,8,12 ops > 1 ms (0,4,8,12)
read_ring 4.2 / 5.1 us 11.1 / 1224 us 7-11 per GPU
pull (one-shot) 10.8 / 15.0 us 17.7 / 1622 us 19-22 per GPU
write_all (fan-out) 10.1 / 11.6 us 110.8 / 209 us 0
push (fan-out one-shot) 12.4 / 13.8 us 110.7 / 203 us 0
push_phased - 16.1 / 53.7 us 0
rsag_phased - 18.6 / 42.0 us 0
ring - 20.5 / 45.1 us 0

2.4 Transport comparison on 1,5,9,13 (same switches and quadrants as 0,4,8,12)

= 3 s per cell, so every cell spans several windows.

KiB transport mean p50 p99 max ops > 1 ms
80 NCCL P2P (NCCL_P2P_LEVEL=SYS, LL ring writes) 14.3 14.2 15.9 44 0
80 phased one-shot push, 8 CTAs 15.3 15.2 15.9 53 0
80 phased RSAG, 4 CTAs 15.5 15.5 15.7 43 0
80 write ring 18.5 18.5 19.1 42 0
80 NCCL default (SHM through host memory) 22.3 22.3 23.4 53 0
80 one-shot pull 23.4 11.5 402 1606 24
80 fan-out RSAG push 33.3 33.0 43.8 62 0
80 fan-out one-shot push 110.5 110.3 136.5 195 0
400 phased RSAG 26.7 26.6 28.3 61 0
400 write ring 27.0 27.0 29.7 64 0
400 NCCL P2P 34.6 34.8 40.6 99 0
400 phased one-shot push 39.5 38.9 47.2 130 0
400 NCCL default (SHM) 59.3 59.2 62.1 92 0
400 one-shot pull 69.4 47.1 1431 2686 1106
400 fan-out one-shot push 653.5 651.8 722.6 772 0

Pull has the best median and the worst mean and tail. A median-based startup race prefers it, but serving pays the mean.

The phased cost on a fabric where fan-out works is small. On one quadrant (13-16) at 16 KiB: pull 8.2 us, phased push 8.3 us, fan-out push 9.2 us.

2.5 NCCL and vLLM

  • NCCL 2.30.7 is not affected by default. On this topology it picks SHM/direct (GPU writes to host memory), which gives 22 us at 80 KiB with no stalls.

    • With NCCL_P2P_LEVEL=SYS it uses an LL ring of posted writes, one destination per GPU, and is the fastest small transport (14.3 us).
    • Only NCCL_P2P_READ_ENABLE=1 NCCL_PROTO=Simple makes it read across quadrants, and then it shows the same stalls (max 1.63 ms).
  • vLLM (fraserprice DeepSeek-4.1-Flash-1017-BRRR image):

    • <= 96 KB: b12x one-shot pull, including the fused add+RMSNorm path;
    • 96 KB-1 MB: its own phased_all_reduce.cu (posted writes, one destination per GPU-wide phase, rotation);
    • 1 MB: NCCL with NCCL_P2P_LEVEL=SYS NCCL_P2P_READ_ENABLE=1.

    Its small path would hit the same read windows on 0,4,8,12; we did not serve it there.

3. Ruled out

hypothesis test result
AER / bad link AER counters, GPU replay counters 31 correctable events in 2.2 days, all on root port c0:01.1 (~1/hour), GPU replays 0; windows occur every 2.3 s on every quadrant
link speed/width changes, retraining LnkSta sampled every 0.5 ms during stalls stable Gen5 x16, no training, no bandwidth-management events
ASPM / port PM kernel cmdline already off
CPU C-states hold /dev/cpu_dma_latency = 0 no change
activity-driven DMA power management 200 MB/s background host->GPU copies no change
DF P-states FCLK via HSMP every 2 ms constant 1733 MHz
LCLK DPM HSMP pin of LCLK DPM range to 1..1 on all 4 NBIOs, A/B/A no change (read_ring ops > 1 ms: 60 / 67 / 56; pull: 139 / 134 / 147)
power profile HSMP power efficiency mode 2 (IO performance), A/B/A no change
DF C-states HSMP DF C-state disable, A/B/A no change
GPU-side tasks, fan control, NVML, Switchtec MRPC polling GPU 14 has no fan controller; NVML/MRPC calls fired at known times GPU 14 stalls the same; timed calls cause no windows
SMU mailbox busy HSMP round-trip latency inside and outside windows identical (p50 106 us)

All HSMP changes were read back and restored. One side observation: CPU config-space reads of a GPU (our 0.5 ms LnkSta sampler) add extra read stalls targeting that GPU.

Conclusion: the windows come from per-NBIO periodic firmware activity that no OS-visible control reaches. Only BIOS options or AMD can change it. Software can avoid it completely by not reading across quadrants.

4. Why the current runtime selection cannot fix it

  • No good candidate on host bridges. Every joint mode is either a peer read (pull, bulk_pull) or a fan-out write (full, payload_rsag, bulk_push).
  • The race reads medians. It decides on medians over a few ms, which hides 2.3 s windows. Pull wins on median.
  • Choices change per start. Two starts of the same package on 0,4,8,12 picked Pull 8 / Bulk Pull 8 / Full 4 / Payload RSAG 2, then Pull 9 / Bulk Pull 7 / Payload RSAG 4 / Full 2. In the second start, c32 fell to 1007-1026 tok/s because the adaptive scheduler mostly ran b16w8 rounds.

5. Everything we tried

# attempt result verdict
1 Luke's runtime tuning, exported on switches, served on host bridges (DS4.1 c16 package, 2026-10-28) c16 341-364 tok/s vs 507-521 for a static package exported natively on host bridges runtime worse than static on host bridges
2 Select by mean instead of median over the same short window flips more often (128 rows: RSAG chosen 8/20 vs 3/20), short windows cannot resolve clustered stalls rejected
3 Staggered peer order, still concurrent writes 250 us vs 500 us interleaved (3x320 KiB); one destination at a time 27 us not enough
4 Single-CTA profiles (one write stream) RSAG/1 at 128 rows 395 us vs pull long-run mean 110-220 us not enough
5 Phased joint submodes (FI #62) added as startup-race candidates (DS4.1 c16) 562-576 tok/s c16 vs 507-521 native static works, but kernels violate principle 14; race still sees reads
6 Export release deadline (raise it / handshake in #128) rejected as a workaround; main has since fixed it (release takes 14.7 s) obsolete
7 Stall harness on 1,5,9,13, 0,4,8,12 and 0-3 localized: cross-quadrant reads (periodic windows) and cross-quadrant fan-out writes root cause found
8 LCLK DPM pin, IO-performance power mode, DF C-states off no change firmware, not OS-controllable
9 Phased cost on a single quadrant (13-16) 16 KiB: phased 8.3 us vs pull 8.2 us vs fan-out 9.2 us phased is cheap where fan-out works
10 PoC: forced phased ladder (below) host bridges at switch level works

6. The PoC

What changed against the measured reference package. The reference is Flare 4f20ac5 (= main ladder code) with FI 443d8fad, DS4.1 c32 CSF, --collective-tuning runtime with gb202_pcie_tp4.json. The PoC changes:

  1. FlashInfer: FI 443d8fad plus full_phased, payload_rsag_phased and bulk_push_phased. They move the same data as their base modes, but every CTA of a rank writes one peer at a time in rank-staggered order. The joint compile key goes to 211 so stale cache entries are never reused.
  2. Flare: the three names are added to ignition/preparation/collective_ladder.py.
  3. Ladder: ignition/preparation/ladders/frank2_hostbridge_tp4_forced.json, 255 buckets from 16 B to 8 MiB, one profile per bucket, so the startup race has one candidate.
    • It is built from a b12x.tools.collective_ladder sweep on 0,4,8,12 with pull and bulk_pull excluded: 84,200 configurations, 1200 s.
    • Write-only modes have no stall tail, so each bucket keeps the lowest mean among profiles within 3% of the best median.

The resulting ladder:

payload forced mode mean
<= ~12 KiB full / payload_rsag (fan-out is harmless when latency-bound) 3.6-6 us
12-110 KiB full_phased 7.4 us at 16 KiB, 10.4 at 64 KiB
115 KiB-1.6 MiB payload_rsag_phased 13.9 at 128 KiB, 26 at 512 KiB, 45 at 1 MiB
>= 1.7 MiB bulk_push_phased 82 at 2 MiB, 294 at 8 MiB

For comparison, the existing modes on 0,4,8,12 at 32 rows (320 KiB) take 32.5 us (pull, with stalls) and 28.9 us (bulk pull); the phased choice takes ~20 us.

Export and tests.

  • Same command and environment as the reference package (frank1 GPUs 8-11), only adding the ladder. The export took ~18 min and resource release 14.7 s.
  • Weights are byte-identical to the reference; only 14 deployment/index files differ.
  • FI test_pcie_joint_gpu.py passes on 0,4,8,12 (13 passed, 12 skipped).
  • Flare tests/preparation/test_collective_ladder.py passes for the new ladder, including b12x accepting every pinned geometry at both ends of every bucket. The one remaining failure is a native-extension mismatch in the test environment, and it fails the same way on main.

Startup:

  • the selection is identical on every start: Full 2, full_phased 6, payload_rsag_phased 11, bulk_push_phased 2, Payload RSAG 1;
  • no pull of any kind;
  • startup tuning takes 1.33 s instead of 2.45 s.

Serving measurement.

  • frank2, --max-concurrency 32 --mtp-draft-tokens 7 --mtp-verification-token-budget 128 --embedding-table-storage ram --prefix-cache-memory 64G.
  • llm_decode_bench.py with 2 x 30 s cells per concurrency at temperature 1, after a warm-up.
  • Runs alternated: current, PoC, current, PoC, on 0,4,8,12 and again on the switched GPUs 0,1,2,3 (two PIX pairs in one quadrant). 12-15 has one run of the current package. Ratios use the mean of the cells.
tok/s current on 0,4,8,12 PoC on 0,4,8,12 current on 12-15 current on 0-3 PoC on 0-3
c1 193-248 308-374 323-331 319-380 318-329
c8 671-684 963-1008 1031-1039 1035-1042 976-986
c16 1001-1102 1466-1474 1524-1559 1558-1578 1482-1499
c32 1007-1764 2155-2204 2309-2312 2266-2306 2162-2239
ratio (cell means) c1 c8 c16 c32
PoC 0,4,8,12 vs current 0,4,8,12 +61% +45% +42% +24% (vs the good start)
PoC 0,4,8,12 vs current 0-3 (best switch result) +1% -6% -6% -5%
PoC 0-3 vs current 0-3 (cost of forcing phased on switches) -5% -5% -5% -4%
PoC 0,4,8,12 vs PoC 0-3 (cost of the host-bridge fabric) +6% -1% -1% -1%

c1 differences are within draft-acceptance noise at temperature 1.

GPU time per speculative round, from Flare's flare_speculative_gpu_nanoseconds_total / flare_speculative_rounds_total (slowest rank). This does not depend on draft acceptance.

graph current on 0,4,8,12 PoC on 0,4,8,12 current on 12-15 current on 0-3 PoC on 0-3 PoC 0,4,8,12 vs PoC 0-3
b1w8 13.91-14.07 ms 8.20-8.29 ms 7.62-7.65 ms 7.58-7.63 ms 8.03-8.09 ms +2%
b8w1 13.37 ms 8.23-8.25 ms 7.70 ms 7.67-7.71 ms 8.07-8.09 ms +2%
b16w1 14.20 ms 10.80-10.81 ms 10.28 ms 10.08-10.16 ms 10.62-10.66 ms +2%
b32w1 - 14.41-14.48 ms 13.89 ms 13.84-13.88 ms 14.36-14.41 ms +0.5%
b8w8 28.28-28.42 ms 19.8-21.8 ms 19.95 ms 19.86-19.87 ms 19.2-19.5 ms +2-13% (few rounds)

On 0-3 the current package's startup race picks Bulk Push 7, Full 4, Payload RSAG 11 (no reads), and that is the best result on this host.

Result.

  • On host bridges the PoC makes a decode round ~40% faster: 8.2-8.3 ms against 13.9-14.1 ms.
  • That is within 4-8% of the best switch rounds (7.6 ms at b1w8), and within 0.5-2% of the same forced phased ladder on switches.
  • Throughput lands within 5-6% of the switches at c8-c32.
  • It is stable across starts, which the current selection is not.
  • The remaining gap is the cost of the phased modes themselves, which are 4-6% slower than the switch-optimal fan-out modes on 0-3. It is not the host-bridge fabric. A per-fabric choice (switch modes on switches, phased modes on host bridges) therefore gives switch-level results on both.

7. Recommended direction

  1. Across quadrants, never read peers and never fan writes out. Use write-only transports with one destination per GPU at a time:
    • <= ~12 KiB: fan-out is fine (latency-bound);
    • ~12-110 KiB: phased one-shot push (or an LL-style write ring, which is what NCCL does);
    • ~115 KiB-1.6 MiB: phased RSAG (a write ring is within a few %);
    • = 1.7 MiB: phased bulk push.

  2. Select by fabric class, not by a short median race. Peer reads look best on median and worst on mean. Options:
    • ship a ladder per fabric class (switch-local vs host-bridge/quadrant-crossing) and choose it from PCI topology at load;
    • or keep the startup race but leave read submodes out of the candidates when ranks span IO-die quadrants.
  3. Kernels need a permitted author. The phased submodes in the PoC are Claude-written (FI Parse Gemma 4 reasoning channels and tool calls #62), so principle 14 rules them out. The measurements above are the spec; the branches show the shape that works.
  4. Do not force phased modes on switches. On 0-3 they cost 4-6% against the switch-optimal fan-out modes. On host bridges they are within 0.5-2% of their own switch result. So the remaining gap to switches is the phased modes' own cost (phase barriers), not the fabric. A per-fabric choice gets both fabrics to their best.

8. Caveats

  • One host (frank2) and one PoC export. The forced ladder is frank2-specific; on switches (0-3) it costs 4-6%, so it should only be used where ranks span IO-die quadrants.
  • Throughput varies with draft acceptance at temperature 1; use the per-round GPU times for the comparison.
  • Branches: the Flare branch is based on main 921afd5 (its ladder code is identical to 4f20ac5). The FI branch is based on 443d8fad. Neither carries a design sign-off. Do not merge.

Reproduce

  • Harness: FI branch benchmarks/experimental/b12x/pcie_fabric_stall/README.md.
  • PoC export, serving and A/B: Flare branch scripts/poc_hostbridge_collectives/README.md.
  • frank2 data: /data/ssd2/claude-ds41/work/p2pstall/ (harness runs: dumps-*) and /data/ssd2/claude-ds41/work/poc-phased/ (ladder report, forced ladder, bench/{base-1,base-2,poc-1,poc-2,sw-base-1,sw03-base-1,sw03-base-2,sw03-poc-1,sw03-poc-2}).
  • PoC package: /data/ssd2/claude-ds41/poc-package/.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions