RDMA Bandwidth Utilization in Dispatch/Combine Kernels
Overview
This document explains how RDMA bandwidth is used in the internode_v1 (and v1_ll) dispatch/combine kernels, and how to measure or verify that the RDMA link is being driven at full capacity.
Kernel Execution Path
Each dispatch+combine iteration consists of the following phases (as seen in the profiler trace):
ep_dispatch_copy_to_staging [serial, separate kernel]
↓
EpDispatchInterNodeV1Kernel [one kernel launch, two block groups]
├─ RDMA blocks (blockId < rdmaBlockNum)
│ dispatch_inter_node_send → fire RDMA puts to other nodes
│ dispatch_inter_node_recv → poll flags, copy received tokens into XGMI mem
└─ XGMI blocks (blockId >= rdmaBlockNum)
dispatch_intra → direct XGMI writes to intra-node peers
[all blocks] dispatch_sync → barrier, signal recv counts, shmem_quiet
↓
EpCombineSync [serial, separate kernel]
combine_sync → copy local expert outputs into combineInp staging;
accumulate routing weights; reset dispatch counters
↓
EpCombineSyncBarrier [serial, 1 block × 1 warp]
ep_combine_sync_barrier → cross-device barrier via RDMA AMO to every peer node;
spins until all nodes acknowledge before combine starts
↓
EpCombineInterNodeV1Kernel [one kernel launch, two block groups]
├─ RDMA blocks
│ combine_inter_node → poll flags, accumulate, RDMA-send results back
└─ XGMI blocks
combine_intra_node → accumulate intra-node contributions
↓
EpCombineAll [serial, separate kernel]
ep_combine_all → final weighted accumulation across all nodes into output
The low-latency variant (v1_ll) has the same structure but uses
dispatch_inter_node_ll_send / _ll_recv / combine_inter_node_ll / combine_intra_node_ll.
What RDMA Actually Transfers
Dispatch send (DispatchInterNodeSend / DispatchInterNodeLLSend)
Each RDMA block iterates over chunks of warpSize (64) local tokens and fires one
ShmemPutMemNbiSignalThread call per chunk per remote node:
bytes per put = tokenNum * xferBytes
xferBytes = hiddenBytes + indexBytes + weightBytes + scaleBytes + sizeof(index_t)
The put is a 1-sided non-blocking RDMA write + remote atomic signal. QP selection rotates
across numQpPerPe queue pairs:
int qpId = (tokenId / warpSize) % config.numQpPerPe;
Total bytes injected per rank per iteration (dispatch direction):
dispatch_rdma_bytes = curRankNumToken × xferBytes × (nNodes - 1)
Combine send (CombineInterNodeTyped / CombineInterNodeLLTyped)
After accumulating the expert outputs, the combine RDMA blocks fire one
ShmemPutTypeNbiWarp per completed chunk back to the originating node:
bytes per put = thisChunkTokenNum * tokCombXferBytes
tokCombXferBytes = hiddenBytes (or fp8HiddenBytes for FP8 paths)
+ weightBytes (if weightsBuf != nullptr)
Total bytes per rank per iteration (combine direction):
combine_rdma_bytes = curRankNumToken × tokCombXferBytes × (nNodes - 1)
Theoretical Peak vs. Observed Throughput
Computing theoretical peak
Given a profiler trace (times in microseconds):
dispatch_send_duration_us = wall-clock span of dispatch_inter_node_send
combine_inter_duration_us = wall-clock span of combine_inter_node
Theoretical RDMA throughput check:
# Example calculation
hidden_dim = 7168 # model hidden dim
dtype_bytes = 2 # bf16
index_bytes = topk * 4 # int32 per expert slot
weight_bytes = topk * 4 # float32
scale_bytes = scale_dim * scale_type_size # optional
xfer_bytes_tok = hidden_dim * dtype_bytes + index_bytes + weight_bytes + scale_bytes + 4
total_send_bytes = cur_rank_num_token * xfer_bytes_tok * (n_nodes - 1)
# From trace
send_dur_sec = dispatch_send_duration_us * 1e-6
observed_bw_GBps = total_send_bytes / send_dur_sec / 1e9
# Compare to link rating, e.g. 400 Gb/s = 50 GB/s per port
# With numQpPerPe QPs the effective ceiling is still the NIC BW
print(f"Observed RDMA send BW: {observed_bw_GBps:.1f} GB/s")
Understanding the receive-side timing
The dispatch_inter_node_recv / combine_inter_node spans include both:
Waiting (spinning) on
chunkFlagornodeRecvTokenNumflags until the remote data arrives.Copying from the RDMA staging buffer into XGMI-accessible memory.
A long recv span relative to the send span indicates the receiver is
blocked waiting for data — the NIC/fabric is the bottleneck.
A short recv span means the data arrived before or soon after the receiver started polling.
How to Check RDMA Bandwidth from the Trace
Step 1: Collect a profiler trace
Enable the kernel profiler (requires ENABLE_PROFILER build flag) and run:
python analyze_trace_internode.py <trace_rank_0_XXXXXX.json> --out timeline.png
The trace gives wall-clock spans for each kernel on each warp, merged into a single timeline per iteration.
Step 2: Read key durations from the text summary
The script prints:
Kernel start end dur
dispatch_inter_node_send 0.00 120.00 120.00 ← RDMA send phase
dispatch_inter_node_recv 10.00 350.00 340.00 ← poll + copy received tokens
dispatch_intra 0.00 180.00 180.00 ← concurrent XGMI
dispatch_sync 350.00 360.00 10.00 ← barrier + shmem_quiet
combine_sync 360.00 400.00 40.00 ← copy outputs into staging
ep_combine_sync_barrier 400.00 420.00 20.00 ← cross-node AMO barrier
combine_inter_node 420.00 760.00 340.00 ← RDMA combine (concurrent)
combine_intra_node 420.00 560.00 140.00 ← XGMI combine (concurrent)
ep_combine_all 760.00 810.00 50.00 ← final accumulation
Step 3: Compute observed RDMA bandwidth
# Plug in values from your config and trace
send_dur_us = 120.0 # dispatch_inter_node_send wall-clock span
combine_dur_us = 340.0 # combine_inter_node wall-clock span
cur_rank_num_token = 2048
n_nodes = 4
hidden_dim = 7168
dtype_bytes = 2 # bf16
topk = 4
xfer_bytes_tok = hidden_dim * dtype_bytes + topk * 4 + topk * 4 + 4
dispatch_bytes = cur_rank_num_token * xfer_bytes_tok * (n_nodes - 1)
combine_bytes = cur_rank_num_token * hidden_dim * dtype_bytes * (n_nodes - 1)
dispatch_bw_GBps = dispatch_bytes / (send_dur_us * 1e-6) / 1e9
combine_bw_GBps = combine_bytes / (combine_dur_us * 1e-6) / 1e9
print(f"Dispatch RDMA BW: {dispatch_bw_GBps:.1f} GB/s")
print(f"Combine RDMA BW: {combine_bw_GBps:.1f} GB/s")
Step 4: Interpret results
Observation |
Likely cause |
|---|---|
|
Send side is BW-saturated (good) |
|
Receiver spins waiting; fabric or sender is the bottleneck |
|
Receiver catches up quickly; may be underloaded (small batch) |
|
Poll loop stall or NIC contention |
Iteration time dominated by |
Barrier overhead, not BW |
Key Tuning Parameters That Affect RDMA Utilization
Parameter |
Effect |
|---|---|
|
More RDMA blocks → more warps driving sends concurrently |
|
More QPs → more in-flight RDMA operations, reduces head-of-line blocking |
|
Larger batches → longer RDMA transfers → easier to saturate |
|
Larger transfer size per token → higher bandwidth per put |
|
Chunk size = 64 tokens; larger chunks = fewer puts with less overhead |
v1 vs. v1_ll (low-latency) tradeoff
v1: Each RDMA block handles a contiguous range of chunks per remote node. The recv polling is simple but may stall if chunks are out of order across nodes.
v1_ll: Work is distributed per
(expert, token, node)triplet. Each warp picks up work as soon as any chunk flag signals. Better latency at small batch sizes; similar peak BW at large batches.
Sanity Checks from Hardware Counters (optional)
If you have access to rocprof or equivalent:
rocprof --stats --hip-trace \
--counter "TCC_EA_RDREQ_sum,TCC_EA_WRREQ_sum" \
python run_dispatch_combine.py
Cross-check the TCC_EA_WRREQ write request count against
total_sends = curRankNumToken / warpSize * nQpPerPe * (nNodes - 1).
For RDMA-specific counters (NIC-side), use the vendor tool (e.g., perfquery for InfiniBand or
the AMD NIC perf tools) to observe port-level PortXmitData and PortRcvData during the run.
If PortXmitData / iteration_time ≈ rated NIC bandwidth, the link is saturated.
Summary
To determine if RDMA is fully utilized:
Collect a trace with the built-in kernel profiler.
Read
dispatch_inter_node_sendduration from the text summary.Compute theoretical bytes sent (tokens × xferBytes × (nNodes − 1)).
Divide to get observed GB/s; compare to the NIC’s rated bandwidth.
If observed < rated by >20%, look for: too few QPs, too small batch, or the recv side blocking the send side (check
recvspan vs.sendspan ratio).