Structural repair: - 13 pages were the same document twice (each exactly 2x its HEAD size), a second frontmatter block whose closing fence had been glued onto a body line by a frontmatter rewrite that re-read its own output. Un-doubled by cutting at the second frontmatter and keeping the copy whose body is a superset; the orphan census reports 0 lost tokens against HEAD. - log.md: one entry per date, bare `## YYYY-MM-DD` headings, newest first — 66 dated headings merged into 34, titles demoted to bold leads, every content line verified present. The file had been descending at the head and ascending at the tail. Ownership moves: - New `LLM/frank-medium.md` is the locator for the retired Dankserver endpoint: era windows quoted from each era page's own banner, no numbers restated, and the two unresolved date conflicts recorded instead of silently resolved. `frank-archon.md` links to it instead of restating the pre-rename window. - The four Pipeline-Parallelism stubs (frontmatter only, no body) are gone; `Multi-Node Runtimes.md` owns runtime construction, launch, mixed-vendor verdicts and cross-split spec decode, and the four links that pointed at the stubs were repointed at it or at the drafter page. - Authentik's three stub pages fold into `Authentik.md`, with the process detail and the outpost reference split out so the parent stays under the length hint. Verified corrections (host census + live probe, 2026-10-19): - The MXFP4 checkpoint two pages called the "on-disk rollback" is gone: /home/dank/models/ holds StillDeadcode, rad and tpl.jinja. Stated once, on the era page that owns that payload; the swap page now links to it. - frank-medium-radiance-mxfp4: the 4.24/7 accept rate belongs to the DFlash2 arm measured on the FP8 target, not to the MXFP4 era; the 3-family soak run census is kept once (155 requests) instead of twice (~120 and 155). - Fleet shapes re-confirmed against the live endpoints before touching any page: 3x200000 fp8 KV, 2x200704, 2x262144. Index rows are regenerated, not hand-carved. Net -1,849 vault lines.
19 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| reference | Dual R9700 Cooperation | Why two R9700s could not run tensor-parallel on the old board, and what the board swap unblocked. |
|
2026-09-16 |
Dual R9700 Cooperation
Dankserver's two R9700s run one tensor-parallel engine across both cards —
today that is frank-archon under radiance 1.0.13, TP=2
(frank-archon). When this page was written the endpoint
was frank-medium: first llama.cpp's layer split
(frank-medium-llamacpp-dflash2, 2026-09-16 → 09-20), then from 2026-09-20
one vLLM-radiance process at tensor_parallel_size 2
(frank-medium-radiance-mxfp4, 2026-09-20 → 09-26). This page records why
true tensor parallelism was impossible on the previous board (TUF GAMING B650-E
WIFI), and what the 2026-09-20 board swap changed.
The short answer
The wall is gone. On the B650-E, card #2 sat behind the chipset, whose bridges could not route PCIe atomic operations. RCCL's kernels carry a hidden hostcall-buffer parameter and ROCr refuses to supply it to a device without atomics, so no RCCL collective or P2P operation could be dispatched on that card — see the measured matrix and the 2026-09-16 table below.
On 2026-09-20 the board became an ASUS ProArt B850-Creator WiFi NEO, which
bifurcates the CPU's GPP ports x8/x8: the board-swap path this page
recommended, at B850 rather than X870E money. Both cards now hang off CPU root
ports (00:01.1, 00:01.3), every bridge on both chains reads
AtomicOpsCap: Routing+, and both endpoints read AtomicOpsCtl: ReqEn+ —
the exact precondition this page's own test plan named, and it is now
measured rather than assumed.
What that bought:
- Both cooperation modes now work, and they were both run. vLLM TP=2 booted
and served (its R4D allreduce needs no RCCL collective at all), and llama.cpp
--split-mode tensorbooted and served against an RCCL-backed build — so a collective HAS been dispatched on this host since the swap. - TP=2 is the production shape. It wins on every axis measured, by 2.1x on
decode and 3.6x at 4-way concurrency, and it replaced the layer split on the day
of measurement. The payload has moved since: the MXFP4
frank-mediumshape is retired 2026-09-26, and what runs today is the iq4r publisher under radiance 1.0.13 (frank-archon). - The drafter, not the collective, is what llama.cpp lost. Its tensor mode could not carry the DFlash2 drafter, which is the honest caveat on the comparison below.
- The bandwidth story moved too. Both cards ride Gen5 x8 (~31.5 GB/s each):
card 0 effectively unchanged (was Gen4 x16), card 1 up ~16x (was Gen4 x1,
~1.97 GB/s). The M.2→PCIe riser plan is dead —
PCIEX16_2holds card #2 now and M.2_2 is free.
The gate, in source
void Device::checkAtomicSupport() { // rocclr/device/rocm/rocdevice.cpp:181
std::vector<amd::Device::LinkAttrType> link_attrs;
link_attrs.push_back(std::make_pair(LinkAttribute::kLinkAtomicSupport, 0));
if (findLinkInfo(cpu_agent_info_->fine_grain_pool, &link_attrs)) {
if (link_attrs[0].second == 1) {
info_.pcie_atomics_ = true; // no else branch
}
}
}
The flag is a queried hardware property, consumed at
rocvirtual.cpp:3954 (the hostcall/AQL dispatch path), in rocblit.cpp (SDMA
copy paths) and exported as hipDeviceProp.hostNativeAtomicSupported. There is
no environment variable, module parameter, or sysfs toggle around it — verified
by grepping ROCclr for getenv/HSA_*/ROCR_*/AMD_* in the gate's
neighborhood: none. Writing the endpoint's Device Control 2 bit 6 by hand
(setpci -s 0b:00.0 CAP_EXP+28.w=0440) does nothing because the value is
consumed at driver probe time.
What has to be true upstream of it: every PCIe bridge on the GPU's path to the
CPU must advertise AtomicOpsCap: Routing+. On this board card #2's path
includes 05:00.0 and 06:09.0, both Routing- — a read-only capability
bit, so no software can enable it.
The same gate, as this board reports it (2026-09-16)
lspci -vv on dankserver. Card 0's chain is root port 00:01.1 → 01:00.0 →
02:00.0 → 03:00.0; card 1's is 00:02.1 → 05:00.0 → 06:00.0 →
06:09.0 → 09:00.0 → 0a:00.0 → 0b:00.0:
| Device | Role | AtomicOpsCap |
AtomicOpsCtl |
Link |
|---|---|---|---|---|
00:01.1 |
CPU root port → PCIEX16_1 | Routing+ | — | x16 |
03:00.0 |
card 0 | 32bit+ 64bit+ | ReqEn+ | x16 |
00:02.1 |
CPU root port → chipset | Routing+ | — | x4 |
05:00.0, 06:00.0, 06:09.0 |
B650 bridges | Routing− | — | x1 at 06:09.0 |
09:00.0, 0a:00.0 |
PCIe switch ports | Routing+ | — | x1 (downgraded) |
0b:00.0 |
card 1 | 32bit+ 64bit+ | ReqEn− | x16 |
Three things fall out of that table, none of them in the source alone:
- The CPU side is fine —
00:02.1isRouting+. The wall is inside the chipset, not the platform, which is why a slot on CPU lanes is the fix. - The endpoint's own bit differs between the cards, and that is the kernel's
doing.
pci_enable_atomic_ops_to_root()walks the bridge chain and refuses to set Requester Enable when any bridge cannot route atomics. ROCclr'spcie_atomics_and the endpoint'sReqEnare two faces of one cause, and it makeslspci -vv -s <bdf> | grep AtomicOpsCtla one-command diagnosis: on this host card 0 readsReqEn+and card 1ReqEn−. - Card 1's effective link is x1, not the x16 its own capability block
advertises:
06:09.0trained at x1, so ~2 GB/s is the real ceiling. That is a bandwidth fact rather than an atomics one, and it only matters for collectives — a 27B TP=2 allreduce is ≈5.6 ms/token at x1 against ≈1.4 ms at x4. The layer split's boundary copy does not care.
Re-measured 2026-09-20 (ProArt B850-Creator WiFi NEO)
lspci -vv after the board swap. Card 0's chain is 00:01.1 → 01:00.0 →
02:00.0 → 03:00.0; card 1's is 00:01.3 → 05:00.0 → 06:00.0 →
07:00.0. No chipset bridge is in either path — 00:02.1 now carries only
LAN/SATA/WiFi:
| Device | Role | AtomicOpsCap |
AtomicOpsCtl |
Link |
|---|---|---|---|---|
00:01.1 |
CPU GPP → PCIEX16_1 | Routing+ | — | Gen5 x8 |
01:00.0 |
card 0 switch upstream | Routing+ | — | Gen5 x8 |
02:00.0 |
card 0 switch downstream | Routing+ | — | Gen5 x16 |
03:00.0 |
card 0 | 32bit+ 64bit+ | ReqEn+ | Gen5 x16 (on-card) |
00:01.3 |
CPU GPP → PCIEX16_2 | Routing+ | — | Gen5 x8 |
05:00.0 |
card 1 switch upstream | Routing+ | — | Gen5 x8 |
06:00.0 |
card 1 switch downstream | Routing+ | — | Gen5 x16 |
07:00.0 |
card 1 | 32bit+ 64bit+ | ReqEn+ | Gen5 x16 (on-card) |
- Both endpoints read
CAP_EXP+28.w = 0x1440— bit 6 set, which is the kernel'spci_enable_atomic_ops_to_root()having walked a chain with noRouting-bridge. Same one-command diagnosis, opposite answer. dmesgagrees on bandwidth:252.056 Gb/s available PCIe bandwidth, limited by 32.0 GT/s PCIe x8 link at 0000:00:01.1(and.13) — ~31.5 GB/s per card. DMI confirms the bifurcation:PCIEX16(G5)_2, "8x or x8".rocm-smi --showtoporeportedPCIEbetween the cards (Navi 48 has no XGMI), they were in separate IOMMU groups (15 and 20 — re-read 2026-10-18: still 15 and 20), and both root ports had ACSReqRedir+, so this page inferred P2P would hairpin at the root complex. That inference is superseded: the live engine reports the two cards reaching each other's memory directly, which frank-archon and Dankserver System Specs record. Plain P2P traffic measured 45.27 GB/s bidirectional on the old board — DMA was never the gate, atomics were.- Kernel 7.0.0-31-generic with
iommu=ptthen; this host runs 7.0.0-38-generic withiommu=pttoday (/proc/cmdline, read 2026-10-18). Both were ruled out as causes (above) and this host's IOMMU posture is unchanged.
Still unproven (as of 2026-09-20). The gate that matters is ROCclr's
checkAtomicSupport() reading the kLinkAtomicSupport link attribute; the ReqEn
bit is the same cause seen from the kernel side, but no RCCL kernel had been
launched since the swap. Until the 2-rank probe runs — or a --split-mode row
boot that does not log internal AllReduce init failed ... falling back to meta-backend butterfly — "collectives work here" was a strong expectation, not a
measurement. RCCL never got that measurement; what settled the operational
question instead is production traffic — radiance's TP=2 wire carries cross-rank
all-reduces over the direct P2P path and prints its own precision, and the
measured wht6 banner with its exact-step bound is on frank-archon.
Measured matrix
Bare 2-rank torch.distributed (backend=nccl) on the two R9700s, no vLLM, no
model. Rank 0 = card #1 (CPU lanes), rank 1 = card #2 (chipset):
| Operation | rank 0 (card #1) | rank 1 (card #2) |
|---|---|---|
broadcast |
OK | FAIL DistBackendError |
all_reduce |
OK | FAIL |
barrier |
OK | FAIL |
send / recv (P2P) |
blocked, then timeout | FAIL |
all_gather (after failure) |
group unusable | — |
The AMD_LOG_LEVEL=3 trace names the cause: Pcie atomics not enabled, hostcall not supported → AQL dispatch failed! → hipLaunchKernel returns
hipErrorIllegalState for ncclDevKernel_Generic_4(...). Nothing in vLLM or
RCCL mentions atomics; the runtime log is the only place it appears.
send/recv failing is the decisive result: it kills pipeline parallelism
as an option, because vLLM's PP stage handoff is exactly that primitive.
Software escapes: closed, and the last one is now moot
Every row below was written under the old board. The last row — the only door that was ever open — stopped mattering on 2026-09-20, because the hardware gate it existed to route around no longer applies to this host.
| Idea | Finding |
|---|---|
| vLLM's IPC custom allreduce | Gated off by a table keyed to CUDA sm versions — {9.0, 10.0, 10.3, 10.7}; ROCm reports capability 12.0 for gfx1201, so the gate is False. custom_ar itself is compiled into the image. |
| ...and if that table were patched | Still insufficient: broadcast, barrier, and the TP logits all_gather have no non-RCCL path. |
| The radiance fork's own allreduce | Already present and on by default (RADIANCE_USE_R4D_AR, size gate RADIANCE_AR_MAX_KB): a one-shot P2P push allreduce wrapping CudaCommunicator.all_reduce, exchanging IPC handles over the gloo group. With the gate raised it covers prefill and decode, and ncclDevKernel disappears from the profile. But it does not intercept PyNcclCommunicator.__init__'s warmup allreduce, nor pynccl_comm.all_gather — both direct RCCL calls. |
| NCCL symmetric memory | Unavailable: the shipped RCCL advertises NCCL 2.30.4 but exposes no ncclDevComm* API, so torch's symmetric-memory path cannot run. |
| Prefill/decode disaggregation | Needs a KV-transfer connector plus coordination collectives; both out. |
| gloo for GPU work | CPU/TCP collectives — orders of magnitude too slow for per-layer TP. |
| Rebuild RCCL so its kernels do not declare a hostcall buffer | OPEN — unverified. The claim is that the hostcall requirement is triggered by device-side printf/assert, and that -mprintf-kind=buffered avoids it. Counter-evidence: RCCL's device source at src/device/ contains no live printf, assert, or __assertfail (checked), so what makes its kernels declare HiddenHostcallBuffer is unexplained. The claim traces to search-result summaries, not a primary source. Cheap to settle once a host is available: recompile librccl with -mprintf-kind=buffered, dump a kernel's declared arguments (roc-obj-extract + llvm-readelf --notes) and look for hidden_hostcall_buffer; if it is gone, a 2-rank probe should dispatch. Stop and mask the service first — see the note below. |
Bandwidth: why x4 is enough
A 27B TP=2 allreduce at batch 4 moves roughly
2 \times \text{batch} \times \text{hidden} \times 2\ \text{bytes} \approx 160\ \text{KB}
per layer per forward pass; across ~62 layers that is ≈10 MB per token-step,
or ~1.4 ms at PCIe 4.0 x4 (~7.9 GB/s), and under 0.4 ms at the Gen5 x8
~31.5 GB/s each card now has. TP is compute-bound here, not interconnect-bound.
That is why the cheap option (the M.2 riser) and the expensive one (the board
swap) were both considered adequate on bandwidth alone — and why the board that
landed, at x8/x8, clears the arithmetic with room to spare.
Ruled out empirically (do not re-derive)
- Kernel versions 7.0.0-28 / -30 / -31 — byte-identical failure.
iommu=pt, andamd_iommu=offis unrelated (that was a Dankclaw prefill knob).- P2P hardware: 45.274 GB/s bidirectional between the cards works fine — plain memory traffic is not the problem; atomic routing is.
- A from-source librccl rebuild: identical failure at the identical call path.
- Every NCCL transport/algorithm knob.
- A one-card test proves nothing: NCCL short-circuits at
world_size=1, which is exactly why the two replicas work at all. - Independent corroboration: StillDeadcode/vllm-radiance Codeberg issue #22 —
different host, same image, kernel 7.0.0-30, same
enqueue.ccfailure.
Measured verdict (2026-09-20)
Both cooperation modes were run on this host on 2026-09-20 — the same salted
10-prompt battery (scripts/bench-conversation.py), the service stopped and
masked between arms. The arm table is frank-medium-radiance-mxfp4's bench of
record (arms R = radiance TP=2 + DFlash2 as deployed, I = the llama.cpp layer
split that preceded it, L = llama.cpp --split-mode tensor against RCCL); this
page keeps only the verdict. TP=2 won on every axis — roughly 2.1× decode and
3.6× at 4-way concurrency over the layer split — and replaced the layer split the
day of measurement.
Arm L carries a caveat no flag can remove: llama.cpp's tensor mode could not load
the DFlash2 drafter (it aborts at ggml-backend.cpp:941 — a draft weight
pre-allocated on the non-primary device has no backend once n_copies == 1), so
it decoded without speculation while the other arms decoded with it; and part
of R's margin over I is the 4-bit MXFP4 checkpoint, not the parallelism.
Two numbers elsewhere on this page are therefore not interchangeable:
- The 272 t/s recorded 2026-09-16 is prefill, on a butterfly allreduce, with RCCL off, on the old board. It was never a TP decode number, and its paired decode figure is in "The third path" below.
- The butterfly tensor arm and the RCCL tensor arm a day later are the same shape family on different collective paths; the measured pair lives with frank-medium-radiance-mxfp4's arms.
RCCL needs PCIe atomics on every participating GPU — the gate the old board
failed. vLLM's R4D one-shot allreduce dispatches no collective at all, which is
why TP=2 runs today rather than merely "being unblocked": the image ships TheRock
RCCL, and TheRock carries no gfx1201 device code (invalid device function), so
radiance could not have used RCCL on this host even with atomics routed. Ubuntu's
RCCL 2.27 does carry gfx1201 kernels, and that is what arm L linked against.
Rule for any future multi-GPU collective experiment: stop and mask the
production service first, per the benchmarking policy, or run it on a host that
is not serving users. The production service is frank-archon, a root unit,
so on Dankserver that is sudo systemctl stop frank-archon && sudo systemctl mask frank-archon (scripts/frank-archon.service; never disable — that
deletes the unit file, and mask is what the bench policy prescribes). The
2026-09-20 wording below — systemctl --user stop frank-medium && systemctl --user mask frank-medium, frank-medium being a rootless quadlet — is retired
with that service, which today reports not-found (read 2026-10-18).
This is not theoretical: on 2026-09-16 a bare RCCL P2P probe was run against the live service; the host went hard down ~20 minutes later (no ARP, no ICMP, required a power cycle). The mechanism is not proven, but a hung AQL dispatch wedging the GPU driver is a plausible cause and this host has precedent for going unreachable until a power cycle.
The third path (production in the layer-split era): llama.cpp's layer split
Everything above is about collectives, and vLLM cannot proceed without them
in any of its parallelisms — TP needs a per-layer allreduce, and PP's stage
handoff is a p2p send/recv of the hidden state, the primitive this host
also fails. llama.cpp's --split-mode layer needs no collective at all:
layers are distributed across the cards and the boundary activation is a
plain per-backend copy, once per forward pass. No RCCL kernel is ever launched
on the second card, so the atomics gate never comes up.
That is what frank-medium ran from 2026-09-16 to 09-20
(frank-medium-llamacpp-dflash2): one process, one model, both cards,
262144 context. Its measured layer-split rates are that page's; this host's
tensor arm under the butterfly fallback measured 272 t/s prefill / 26.3 tok/s
decode. That tensor number is not a prediction for an RCCL-backed build — it
is what tensor split costs when the allreduce degrades to a meta-backend
butterfly, and it is the number the board-swap path exists to retire.
The ranking therefore changed twice. On 2026-09-16: software already gets both
cards onto one model, so the hardware fix stopped being a capability question
and became a performance one — tensor parallelism, ~40 % decode, plus one
pooled KV instead of two independent 122k-token pools. On 2026-09-20 the
hardware half of that question was answered by the board swap; the llama.cpp
measurement half never ran, and GGML_HIP_RCCL still defaults OFF in
scripts/build-llama-rdna4.sh:41 (re-read 2026-10-18) for exactly the reason
this page documents — moot while no llama.cpp shape is deployed on this host.
What would change the answer
An RCCL build whose kernels stop declaring a hostcall buffer— moot since 2026-09-20. That door only mattered while the hardware gate could not be passed; the board swap passes it. The-mprintf-kind=bufferedhypothesis and its counter-evidence stay in the escape table for the record.- The symmetric-kernel path inside RCCL, if a release ever ships the device API
(
ncclDevComm*) that torch's symmetric-memory path needs. The shipped TheRock nightly (~/.local/share/rocm-7.14.0a20260624/lib/librccl.so.1) still does not. Firmware or an AGESA update that makes the chipset bridges advertise atomic routing— arrived as new silicon instead.- A GPU-side workaround: none found in ROCclr, and the gate is per-device, queried once at probe.
See Also
- Pipeline Parallelism/index — two-machine research (the PP variant of this problem)
- Dankserver System Specs — hardware inventory
- Observability — the scrape/dashboard/alert stack around these endpoints