| name | known-issues |
| description | Known environment issues that make MORI slow or behave oddly without being MORI bugs — currently: HIP VMM peer traffic silently falling off XGMI onto PCIe/host memory on Linux kernels built without CONFIG_DMABUF_MOVE_NOTIFY / CONFIG_PCI_P2PDMA, which hits mori-cco (EPv2) but not mori-shmem (EPv1). Use when EPv2 / dispatch_combine_v2 / cco is much slower than expected, when EPv2 is far slower than EPv1 on the same box, when a2a bandwidth does not scale with the number of peers, or when the user asks whether a perf problem is a MORI bug or an environment/toolchain problem. Also: (2) on ROCm 7.0-7.2.x a clr hipMemSetAccess sub-buffer bug makes mori-cco 2-node ccoDevCommCreate fail with "hipMemSetAccess ... invalid argument"; (3) on ROCm 7.2.x clr routes uncached VMM allocations to the coarse-grained pool, making cco combine ~10-15us slower. |
MORI Known Issues
Environment-level problems that look like MORI bugs but are not. Check here before
debugging MORI itself when performance is unexpectedly bad on a specific machine.
Issue 1 — VMM peer traffic falls off XGMI onto PCIe / host memory
Affects: mori-cco (EPv2, dispatch_combine_v2) and anything else built on the
HIP VMM API. Does not affect mori-shmem (EPv1), which uses IPC handles.
Root cause: the Linux kernel was compiled without
CONFIG_DMABUF_MOVE_NOTIFY=y (and usually also without CONFIG_PCI_P2PDMA=y).
This is the case for the stock Ubuntu 22.04 GA 5.15 kernel. It is a kernel build
configuration problem, not a kernel version bug and not an amdgpu regression — a 5.15
rebuilt with those options works fine, and the Ubuntu 6.8 HWE kernel already has them.
Impact is silent. Nothing errors, correctness still passes; the a2a path is just
~9x slower.
Symptoms
- EPv2 dispatch/combine bandwidth is a fraction of EPv1's on the same machine.
- Per-rank bandwidth plateaus at roughly a single XGMI link (~55 GB/s on MI355X),
or below it — even though the box has 7 peers.
- Aggregate bandwidth does not increase with the number of peers: 1 peer and 7 peers
give the same total.
rocm-smi --showtopotype shows XGMI everywhere and TransferBench reports full
aggregate bandwidth — because those use the non-VMM path and are unaffected.
Measured on an 8x MI355X box (HIDDEN=7168 TOPK=8 EPR=32, bf16, 4096 tok/rank, 8 ranks):
| dispatch | combine |
|---|
| EPv1 (shmem / IPC handle) | 345 GB/s | 377 GB/s |
| EPv2 (cco / VMM) — affected kernel | 36 GB/s | 54 GB/s |
| EPv2 (cco / VMM) — after fixing the kernel | 340 GB/s | 390 GB/s |
Detection (30 seconds, no GPU needed)
grep -E 'CONFIG_(PCI_P2PDMA|DMABUF_MOVE_NOTIFY)=' /boot/config-$(uname -r)
Both must print =y. If either is missing (# CONFIG_... is not set or no output),
the machine is affected.
Also useful:
uname -r
grep -E 'CONFIG_IOMMU_DEFAULT' /boot/config-$(uname -r)
Runtime confirmation
vmm_peer_probe.cpp in this skill directory gives a direct verdict — it allocates VMM
memory on one GPU, grants peer access from another, and checks whether the memory stayed
in VRAM:
hipcc -O3 --offload-arch=$(rocminfo | grep -m1 -o 'gfx[0-9a-f]*') \
vmm_peer_probe.cpp -o vmm_peer_probe
./vmm_peer_probe
VERDICT: OK — memory stayed in VRAM, the machine is fine.
VERDICT: AFFECTED — granting peer access evicted the buffer out of VRAM.
Mechanism (what actually happens)
-
hipMemSetAccess() → ROCr hsa_amd_vmem_set_access() imports the allocation into
each peer as a dma-buf (ImportMemoryHandle(DMABUF_FD) → hsaKmtHandleImport
→ KFD import_obj_create() → amdgpu_gem_prime_import() →
dma_buf_dynamic_attach()).
-
The dma-buf core then pins the exporter's buffer, calling amdgpu_dma_buf_pin()
(amd/amdgpu/amdgpu_dma_buf.c):
u32 domains = bo->allowed_domains;
if (!IS_ENABLED(CONFIG_DMABUF_MOVE_NOTIFY)) {
domains &= ~AMDGPU_GEM_DOMAIN_VRAM;
} else {
list_for_each_entry(attach, &dmabuf->attachments, node)
if (!attach->peer2peer)
domains &= ~AMDGPU_GEM_DOMAIN_VRAM;
}
return amdgpu_bo_pin(bo, domains);
IS_ENABLED() is a compile-time constant taken from the kernel's own
autoconf.h. Without move-notify support the kernel cannot ask an importer to drop
its mapping when a buffer must move, so amdgpu conservatively refuses to pin in VRAM.
-
The buffer is therefore pinned into host system memory. Note this is stronger than
"peer access falls back to PCIe": the data is not in peer VRAM at all. Every subsequent
"peer" store is a write to host DRAM across the writing GPU's own PCIe link, which is
why aggregate bandwidth is capped at ~one PCIe link no matter how many peers there are.
-
amdgpu_dma_buf_map() contains a peer2peer/VRAM placement check, but it is guarded
by if (!bo->tbo.pin_count) and the pin already happened — that code never runs.
Why EPv1 is immune: hsa_amd_agents_allow_access() maps the same BO into every
GPU's page tables in one hsaKmtMapMemoryToGPUNodes() call. No dma-buf, no attach, no pin.
Why there is no application-level workaround: MORI runs one process per rank, so peer
memory must cross process boundaries, which requires exportable handles — and ROCr
unconditionally does the per-peer dma-buf import (it even lazily exports a dma-buf fd for
locally created handles). cco also cannot fall back to IPC handles because it needs a flat
symmetric VA at computed addresses, which only
hipMemAddressReserve() + hipMemMap() can provide.
Fix
Preferred — install a kernel that already has the options (Ubuntu 22.04):
sudo apt install linux-generic-hwe-22.04
sudo reboot
If the machine must stay on 5.15 — rebuild it with two options. All their Kconfig
dependencies (ZONE_DEVICE, MEMORY_HOTPLUG, SPARSEMEM_VMEMMAP, GENERIC_ALLOCATOR,
DMA_SHARED_BUFFER, …) are already satisfied in the stock config:
sudo apt build-dep linux-image-unsigned-$(uname -r)
apt-get source linux-image-unsigned-$(uname -r)
cd linux-5.15.0-*/
cp /boot/config-$(uname -r) .config
scripts/config -e DMABUF_MOVE_NOTIFY -e PCI_P2PDMA
scripts/config -d DEBUG_INFO -d DEBUG_INFO_BTF
make olddefconfig
grep -E 'CONFIG_(DMABUF_MOVE_NOTIFY|PCI_P2PDMA)=' .config
make -j$(nproc) bindeb-pkg
sudo dpkg -i ../linux-image-*.deb ../linux-headers-*.deb
sudo reboot
Check mokutil --sb-state first — with Secure Boot enabled a self-built kernel needs
signing. The old kernel stays in GRUB, so this is revertible. Note that a self-built
kernel leaves the distro's kernel maintenance path; prefer the HWE kernel for anything
long-lived.
CONFIG_DMABUF_MOVE_NOTIFY is the one that actually matters. CONFIG_PCI_P2PDMA is
enabled alongside it because upstream HSA_AMD_P2P depends on both and it costs nothing.
Third option — one-line amdgpu DKMS patch, no kernel change. Force the branch that
keeps VRAM (its per-attachment peer2peer check is already satisfied):
S=/usr/src/amdgpu-<version>/amd/amdgpu/amdgpu_dma_buf.c
sudo cp $S $S.orig
sudo sed -i 's|if (!IS_ENABLED(CONFIG_DMABUF_MOVE_NOTIFY)) {|if (0) { /* WAR: kernel lacks MOVE_NOTIFY */|' $S
sudo dkms build amdgpu/<version> -k $(uname -r) --force
sudo dkms install amdgpu/<version> -k $(uname -r) --force
Then either reboot, or reload the module (modprobe -r amdgpu && modprobe amdgpu) if
nothing holds the GPUs (lsmod | grep '^amdgpu' must show used_by=0 and
rocm-smi --showpids must show no KFD PIDs). Reloading invalidates every container's GPU
context, so it disturbs other users exactly as much as a reboot — it only saves time.
Roll back with sudo cp $S.orig $S and rebuild.
Verification after the fix
grep -E 'CONFIG_(PCI_P2PDMA|DMABUF_MOVE_NOTIFY)=' /boot/config-$(uname -r)
./vmm_peer_probe
Then re-run the EPv2 kernel bench; per-rank bandwidth should be in the same range as EPv1.
About IOMMU — related but not the cause
amd_iommu=on iommu=pt does not fix Issue 1. This was tested directly: with
passthrough confirmed active (dmesg showing
iommu: Default domain type: Passthrough, and
/sys/bus/pci/devices/<BDF>/iommu_group/type reading identity), bandwidth and IOMMU
transaction counters were unchanged. The buffer is pinned into host memory before any
address-translation question arises.
The tempting but wrong theory: IOMMU passthrough sets adev->ram_is_direct_mapped, which
KFD's reuse_dmamap() uses to choose KFD_MEM_ATT_SHARED over KFD_MEM_ATT_DMABUF.
That choice really does flip with iommu=pt — but both attachment types share the same
scatter-gather BO, whose pages were already placed in host memory by the pin.
Attachment type is not the physical route.
Still enable iommu=pt anyway. It is the standard configuration for AMD GPU / HPC
nodes, amdgpu uses ram_is_direct_mapped elsewhere, and it removes IOMMU translation
overhead. Just do not let it stand in for the kernel config fix. Note that Ubuntu kernels
default to translated mode (CONFIG_IOMMU_DEFAULT_DMA_LAZY=y,
CONFIG_IOMMU_DEFAULT_PASSTHROUGH not set), so passthrough must be requested explicitly
on the kernel command line.
IOMMU is, however, an excellent diagnostic instrument for this issue, because host-DRAM
DMA is translated by the IOMMU while XGMI peer traffic never touches it:
EV=""; for i in 0 1 2 3 4 5 6 7; do EV="$EV -e amd_iommu_$i/mem_trans_total/"; done
sudo perf stat -a $EV -- sleep 8
On an affected machine one IOMMU counts billions of transactions during VMM peer traffic
(64 B each — multiply out and it matches the observed bandwidth exactly). After the fix
the same measurement drops by ~380x, down to the same noise level as non-VMM peer traffic.
amd-smi metric --pcie is a coarser cross-check: on an affected box exactly one card —
the one issuing the writes — shows sustained multi-GB/s PCIe traffic while all its peers
stay idle, which is itself the tell that the peers are not the destination.
Also worth knowing
-
CONFIG_HSA_AMD_P2P cannot be relied on. amd/dkms/dkms-config.sh tests for the
misspelled CONFIG_DMABUF_MOVENOTIFY (missing underscore), so the amdgpu DKMS build
derives CONFIG_HSA_AMD_P2P=0 even on kernels where both real options are =y. Present
in at least amdgpu DKMS 6.14.14 and 6.16.13. Do not use CONFIG_HSA_AMD_P2P as a health
signal, and do not expect code under #ifdef CONFIG_HSA_AMD_P2P to be compiled in.
-
KFD may report max_bandwidth=0 for XGMI io-links
(/sys/class/kfd/kfd/topology/nodes/*/io_links/*/properties) on some hosts while others
report 64000, with no functional difference. It is unexplained but unrelated to
Issue 1 — kfd_mem_attach() never consults link bandwidth. Do not chase it.
-
Micro-benchmarking peer bandwidth is noisy. Use ≥1024 blocks per stream and take the
best of several repetitions. An under-parallelised harness produced a 1.5x spread between
consecutive identical runs, which is more than enough to invent or hide a regression.
Issue 2 — ROCm 7.0–7.2.x: hipMemSetAccess sub-buffer bug (cco 2-node cannot start)
Affects: mori-cco (EPv2) multi-node. A clr/HIP-runtime bug present in all ROCm
7.0 → 7.2.x releases (regression from 6.4.x; fixed on clr develop 2026-01-26 but not
back-ported to any 7.2.x release branch). ROCm 7.14+ already has the fix. mori-shmem
(EPv1) is unaffected.
Symptom: ccoDevCommCreate fails during window setup:
[shmem] [error] ccoMemAlloc: hipMemSetAccess failed after retries: 1 (invalid argument)
[shmem] [error] ccoDevCommCreate: resource window MemAlloc failed
RuntimeError: ccoDevCommCreate failed: -1
Single-node cco usually still passes; the failure shows up cross-node. CCO_GDR_CAPABLE=0
does not help — the trigger is sub-buffer geometry, not GPUDirect-RDMA.
Root cause: hipMemSetAccess validates coverage by walking the parent reservation's
sub-buffers from sub-buffer 0, ignoring the ptr argument, and returns InvalidValue
whenever size is not a prefix-sum of sub-buffer sizes starting at sub-buffer 0. cco
reserves one flat VA (hipMemAddressReserve) and maps several different-sized windows
(sub-buffers) into it, each with its own hipMemSetAccess, so every window after the first
fails. (SWDEV-568260 / ROCm/rocm-systems#2451, commit be8bcd059.)
Fix — upgrade, or rebuild clr with the upstream fix and swap libamdhip64.so.
Prefer ROCm 7.14+. If you must stay on 7.2.x, rebuild clr for your exact version and
cherry-pick the fix (this is what docker/Dockerfile.cco did before the 7.14 move —
git log -S be8bcd059 -- docker/ for the original recipe):
V=$(cat /opt/rocm/.info/version | head -c5)
git clone --depth 1 --branch rocm-$V https://github.com/ROCm/clr.git /tmp/clr
git clone --depth 1 --branch rocm-$V https://github.com/ROCm/HIP.git /tmp/hip
cd /tmp/clr && git fetch origin develop && git cherry-pick be8bcd059
cmake -B build -DHIP_COMMON_DIR=/tmp/hip -DCMAKE_PREFIX_PATH=/opt/rocm \
-DCLR_BUILD_HIP=ON -DCLR_BUILD_OCL=OFF -DHIP_PLATFORM=amd \
-D__HIP_ENABLE_RTC=OFF -D__HIP_ENABLE_PCH=OFF
make -C build amdhip64 -j$(nproc)
T=$(readlink -f /opt/rocm/lib/libamdhip64.so.7)
cp "$T" "$T.orig" && cp build/hipamd/lib/libamdhip64.so.*-* "$T"
The rebuilt lib links the system libhsa-runtime64.so.1 (ROCR is not touched). Deploy by
replacing the file (or LD_PRELOAD) and restarting the process; it only works on the
exact matching ROCm version (soname must match, e.g. libamdhip64.so.7.2.70203 for
7.2.3).
Issue 3 — ROCm 7.2.x: uncached VMM allocations land in the coarse-grained pool (cco combine slow)
Affects: mori-cco (EPv2) combine latency on ROCm 7.2.x (fixed on clr
develop, not in 7.2.x release; 7.14+ is fine). Correctness is unaffected — this is a
pure perf issue, and it usually rides along with Issue 2 on the same 7.2.x box.
Symptom: cco combine is ~10–15 µs slower than on ROCm 7.14 for the same shape, and
requesting uncached memory (the default) is no faster than forcing cached
(CCO_UNCACHED_WINDOW=0 gives the same time). Example: v1_ll combine, tok16, hidden 6144,
fp8/bf16, EP16 → ~65 µs on 7.2.x vs ~54 µs on 7.14; dispatch is roughly unchanged.
Root cause: rocclr's VMM path (deviceVmemAlloc) always allocates from the
coarse-grained gpuvm_segment_, ignoring hipMemAllocationTypeUncached. cco's
symmetric window therefore stays cached, so a GPU spinning on an RDMA "done" flag hits a
dirty L2 line and waits for coarse-grained coherence. The non-VMM path (deviceLocalAlloc