Data on the wire: tensors per layer and stage
Code version: afd-plugin @ 7452d52 · 2026-10-27 · vLLM @ ced6857
In one sentence
The data plane moves hidden-state tensors between the roles once per layer and stage. The FFN rank fans in its peers, computes, and splits the output back.
The idea#
For each layer and stage, the attention rank sends its hidden states to its FFN rank. The FFN rank may own more than one attention peer, so it concatenates the inputs along the token dimension, runs the feed-forward, then splits the output back and returns each slice to the rank that produced it.
Each transfer carries a small metadata record: the layer index, the stage index and the per-peer token
lengths. The token lengths set the expected tensor shape and the split sizes. The transfers go through custom
ops, so they stay usable under torch.compile and CUDA graph capture.
An everyday comparison. Think of parcels with shipping labels. The label says how many boxes and for which address, so the depot can combine shipments and later separate them again. The comparison stops here: the "parcels" also carry optional extras next to the hidden states, the router logits and the token IDs, which only some models send.
The picture#
Read panel 1 as one exchange for one layer and stage. Read panel 2 as the fan-in when one FFN rank owns two attention peers.
In the code#
Step 1: What the attention rank sends#
send_attn_output sends the hidden states first, then the optional router logits, then the
optional token IDs (afd_plugin/connectors/gpu/p2p.py:323-390). The hidden states are a CUDA tensor of
shape (num_tokens, hidden_size); the hidden size comes from the model config
(afd_plugin/connectors/gpu/p2p.py:195-197).
metadata = context.metadata
if not torch.compiler.is_compiling() and not metadata.validate_tensor_shape(
tuple(hidden_states.shape),
):
raise ValueError(
f"hidden_states shape {hidden_states.shape!r} does not match "
f"AFD metadata token count {metadata.total_tokens}",
)
router_logits: torch.Tensor | None = kwargs.get("router_logits")
input_ids: torch.Tensor | None = kwargs.get("input_ids")
if (
router_logits is not None
and router_logits.shape[0] != hidden_states.shape[0]
):
raise ValueError(
"router_logits and hidden_states must have equal token counts",
)
if input_ids is not None:
if input_ids.ndim != 1 or input_ids.shape[0] != hidden_states.shape[0]:
raise ValueError(
"input_ids must be one-dimensional and token-aligned with "
"hidden_states",
)
if input_ids.dtype != _INPUT_IDS_DTYPE:
raise ValueError("P2P AFD input_ids must use torch.int32")The leading dimension must equal total_tokens, unless torch.compile is tracing
(afd_plugin/connectors/gpu/p2p.py:347-353). The token IDs must be a one-dimensional
int32 tensor (afd_plugin/connectors/gpu/p2p.py:363-370).
Step 2: The transfer metadata#
Each transfer carries an AFDTransferMetadata: the layer index, the stage index and the
per-peer sequence lengths (afd_plugin/connectors/metadata.py:188-190). The sum of the lengths is the
expected leading dimension (afd_plugin/connectors/metadata.py:198-200).
@property
def total_tokens(self) -> int:
return sum(self.seq_lens)The shape check compares shape[0] with total_tokens
(afd_plugin/connectors/metadata.py:230-231).
Step 3: The FFN rank fans in its peers#
recv_attn_output receives one tensor from every attention peer in the subgroup, concatenates
them along the token dimension, and records each peer's length in the returned metadata
(afd_plugin/connectors/gpu/p2p.py:428-552).
hidden_states = (
torch.cat(hidden_states_list, dim=0)
if len(hidden_states_list) > 1
else hidden_states_list[0]
)
router_logits = None
if routing_spec is not None:
router_logits = (
torch.cat(router_logits_list, dim=0)
if len(router_logits_list) > 1
else router_logits_list[0]
)
input_ids = None
if input_ids_list:
input_ids = (
torch.cat(input_ids_list, dim=0)
if len(input_ids_list) > 1
else input_ids_list[0]
)
metadata = AFDTransferMetadata.create_ffn_metadata(
layer_idx=0,
stage_idx=ubatch_idx,
seq_lens=[tensor.shape[0] for tensor in hidden_states_list],
)With one peer the single tensor is the whole input (afd_plugin/connectors/gpu/p2p.py:520-524). All
of a step's receives run in one NCCL group, so the peers' transfers proceed together
(afd_plugin/connectors/gpu/p2p.py:514-519).
Step 4: The FFN rank splits the output back#
send_ffn_output splits the output along the token dimension by the recorded sequence lengths,
then sends each slice to the attention rank that produced it
(afd_plugin/connectors/gpu/p2p.py:554-613).
output_slices: list[torch.Tensor] = []
dsts: list[int] = []
start = 0
for dst, token_count in zip(
range(1, self.group_size),
split_sizes,
strict=False,
):
end = start + token_count
output_slices.append(ffn_output[start:end])
dsts.append(dst)
start = end
self._send_tensors(output_slices, dsts, self.e2a_group, self.e2a_comm_id)When the recorded lengths do not cover every peer, the split falls back to an even split, which raises if
the tokens do not divide evenly (afd_plugin/connectors/gpu/p2p.py:590-599).
Step 5: The transfers are custom ops#
The sends and receives are wrapped in four custom ops: afd_p2p_send,
afd_p2p_recv, and the _group variants
(afd_plugin/connectors/gpu/p2p.py:979-1122). The wrapping keeps the transfers traceable by
torch.compile and capturable by CUDA graphs
(afd_plugin/connectors/gpu/p2p.py:983-986). Each op runs on the current CUDA stream
(afd_plugin/connectors/gpu/p2p.py:1002-1006). The connectors/gpu/p2p.py: the GPU P2P connector page reads the custom ops in
full.
Deep dive#
Deep diveWorked example: two peers into one FFN rank (4A2F)
Take a 4A2F topology, so FFN rank 0 owns attention ranks A0 and A1
(afd_plugin/distributed/topology.py:136). The hidden size is
2048 for DeepSeek-V2-Lite; this value is inferred from the checkpoint config, not
stored in either repository. Take one decode step with 8 tokens on A0 and 8 on A1, in bf16
(inferred from the recipe).
- A0 sends:
hidden_statesof shape[8, 2048], metadataseq_lens = [8]. - A1 sends:
hidden_statesof shape[8, 2048], metadataseq_lens = [8]. - On the wire: each tensor is
8 × 2048 × 2 bytes = 32,768 bytes, per layer, per direction. - F0 receives and concatenates:
[16, 2048], withseq_lens = [8, 8](afd_plugin/connectors/gpu/p2p.py:540-544). - F0 computes and splits back: two slices of
[8, 2048]; the first returns to A0, the second to A1 (afd_plugin/connectors/gpu/p2p.py:601-613).
The FFN rank derives the per-peer shapes from the DP metadata before it receives
(afd_plugin/connectors/gpu/p2p.py:776-807); see The control plane: DP metadata once per step.
Deep diveThe two optional tensors
Two tensors cross next to the hidden states, in this wire order: hidden states, then router logits, then
token IDs (afd_plugin/connectors/gpu/p2p.py:336-338).
| Tensor | Shape / dtype | When it is sent |
|---|---|---|
hidden_states | [n, hidden_size], model dtype | always |
router_logits | [n, num_experts] | when the gate runs on the attention side (afd_plugin/v1/worker/ffn_model_runner.py:225-229) |
input_ids | int32 [n] | for the DeepSeek-V4 hash MoE (afd_plugin/connectors/gpu/p2p.py:210-214) |
The FFN side asks for the router logits only when compute_gate_on_attention is set, through a
routing spec (afd_plugin/v1/worker/ffn_model_runner.py:225-229). It asks for the token IDs only when
the model needs them (afd_plugin/v1/worker/ffn_model_runner.py:221). The router logits width and
dtype come from the model (afd_plugin/connectors/gpu/p2p.py:481-491). The The MoE handoff: where the gate runs
page reads the gate placement.
Deep diveReceive buffers and the current stream
Each receive writes into a buffer. In eager mode the connector allocates a fresh tensor per receive. When
CUDA graphs are on, the receive reuses a buffer preallocated during the control-plane update, so the
allocation stays stable for capture and replay
(afd_plugin/connectors/gpu/p2p.py:472-477,
afd_plugin/connectors/gpu/p2p.py:911-936). The attention side's recv_ffn_output takes a
preallocated ref_tensor for the same reason
(afd_plugin/connectors/gpu/p2p.py:392-426). The custom ops issue the transfers on the current CUDA
stream, inside one NCCL group when there is more than one peer
(afd_plugin/connectors/gpu/p2p.py:1037-1070).
What can go wrong#
- Shape mismatch. A hidden-state tensor whose leading dimension is not
total_tokensraises, unlesstorch.compileis tracing (afd_plugin/connectors/gpu/p2p.py:347-353). - An uneven split. When the recorded lengths do not cover every peer and the tokens do
not divide evenly, the split raises
(
afd_plugin/connectors/gpu/p2p.py:590-599). - A CPU tensor. The hidden states must be on the GPU; a CPU tensor is rejected
(
afd_plugin/connectors/gpu/p2p.py:634-635). - Wrong token-ID dtype. The token IDs must be
int32and one-dimensional (afd_plugin/connectors/gpu/p2p.py:363-370).
Why it matters. The two roles must agree on the shapes. The recorded sequence lengths are the only record of which tokens belong to which peer, so the FFN rank uses them to split the output back.
Check yourself#
- 1What crosses from the attention rank to the FFN rank for one layer and stage?easy
Show answer
The hidden states
[n, hidden_size], then optional router logits and optionalint32token IDs, in that order (afd_plugin/connectors/gpu/p2p.py:336-338). - 2How does the FFN rank know how to split its output back?easy
Show answer
From the recorded sequence lengths.
recv_attn_outputrecords each peer's token length (afd_plugin/connectors/gpu/p2p.py:540-544), andsend_ffn_outputsplits by those lengths (afd_plugin/connectors/gpu/p2p.py:590-613). - 3Why are the sends and receives wrapped in custom ops?medium
Show answer
So the transfers stay traceable by
torch.compileand capturable by CUDA graphs (afd_plugin/connectors/gpu/p2p.py:983-986). The connector registers four ops, with fake implementations for tracing (afd_plugin/connectors/gpu/p2p.py:979-1122). - 4When does the attention side send themedium
int32token IDs?Show answer
For the DeepSeek-V4 hash MoE, when the model asks for them (
afd_plugin/connectors/gpu/p2p.py:210-214,afd_plugin/v1/worker/ffn_model_runner.py:221). - 5In 4A2F with 8 tokens on A0 and 8 on A1, what shape does F0 run the FFN on, and what does it send back?hard
Show answer
F0 concatenates to
[16, 2048]withseq_lens = [8, 8](afd_plugin/connectors/gpu/p2p.py:540-544), then splits the output into two[8, 2048]slices, one per peer (afd_plugin/connectors/gpu/p2p.py:601-613). The 2048 is inferred from the DeepSeek-V2-Lite checkpoint.