AFD Plugin Explained

The MoE handoff: where the gate runs

AdvancedAFD conceptsabout 5 min read + 2 min deep dives

Code version: afd-plugin @ 7452d52 · 2026-10-27 · vLLM @ ced6857

In one sentence

A MoE layer has a gate and experts. The compute_gate_on_attention flag decides which AFD role runs the gate, and so what crosses the connector.

The idea#

A mixture-of-experts layer has two parts: a small gate that picks experts per token, and the large experts themselves. AFD always runs the experts on the FFN role, because the expert weights are large. The gate is small, so AFD can run it on either side.

One flag chooses: compute_gate_on_attention. When it is false (the default), the gate stays with the experts on the FFN side. When it is true, the gate runs on the attention role, and the attention side sends the router output along with the hidden states.

An everyday comparison. Think of a mailroom. You can write the delivery route on the parcel before you hand it over (gate on the attention side), or let the mailroom read the address and decide (gate on the FFN side). Either way the parcel reaches the same shelf. The comparison stops here: the gate output is a tensor, not a label, and it must match the experts the FFN side runs.

The picture#

Figure: the flag moves the gate. With the gate on the attention side the FFN runs experts only, and router_logits cross the wire.
Open full size ↗

Read panels 1 and 2 as the two settings. The blue arrow is the data plane. Panel 3 lists the three wire payloads. The order on the wire is always hidden states, then router logits, then input ids.

In the code#

Step 1: The default: the gate runs on the FFN side#

With the flag false, the attention side ships the whole FFN or MoE block through a remote FFN proxy. The FFN side runs the native block, gate and all. On the CUDA MoE path the attention role still builds a remote-experts shell, but its gate is None, so the shell ships only the hidden states (afd_plugin/model_executor/models/deepseek_v2.py:319-335).

        if compute_gate_on_attention:
            self.gate = native.GateLinear(
                config.hidden_size,
                config.n_routed_experts,
                out_dtype=router_dtype,
                prefix=f"{prefix}.gate",
            )
            if getattr(config, "topk_method", None) == "noaux_tc":
                self.gate.e_score_correction_bias = nn.Parameter(
                    torch.empty(config.n_routed_experts, dtype=torch.float32),
                )
            else:
                self.gate.e_score_correction_bias = None
        else:
            self.gate = None

        ep_size = native.get_ep_group().device_group.size()
        self.n_routed_experts = int(config.n_routed_experts)
        self.n_shared_experts = int(config.n_shared_experts)
        self.n_redundant_experts = parallel_config.eplb_config.num_redundant_experts
        self.n_logical_experts = self.n_routed_experts
        self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts
        self.n_local_physical_experts = self.n_physical_experts // ep_size
        # No local shared-expert MLP is constructed, so fused shared experts
        # can never be enabled for this shell.
        self.is_fused_shared_expert_enabled = False
        self.experts = AFDAttentionFusedMoE(
            layer_idx=layer_idx,
            external_routing=compute_gate_on_attention,
        )
The shell builds a gate only when compute_gate_on_attention is set; else gate is None.

Step 2: Gate on the attention side: compute, then send the router logits#

With the flag true, the shell builds its own gate (afd_plugin/model_executor/models/deepseek_v2.py:306-318). Its forward computes the router logits first, then calls the experts proxy with them (afd_plugin/model_executor/models/deepseek_v2.py:347-371).

        # ### PATCH START: compute the Attention-side gate before transport.
        num_tokens, hidden_dim = hidden_states.shape
        hidden_states = hidden_states.view(-1, hidden_dim)
        if self.is_sequence_parallel and not already_sequence_parallel:
            hidden_states = native.sequence_parallel_chunk(hidden_states)
        router_logits, _ = self.gate(hidden_states)
        final_hidden_states = self.experts(
            hidden_states=hidden_states,
            router_logits=router_logits,
        )
The attention-side gate runs before transport; the proxy carries the router logits.

The experts proxy is AFDAttentionFusedMoE, a RemoteFFNProxy that adds router_logits to the send only when the experts use external routing (afd_plugin/model_executor/models/deepseek_v2.py:178-191).

Step 3: The FFN side runs experts only, for expert layers only#

The FFN server loop drives the FFN model runner. Inside _ffn_forward, the set of layers depends on the flag. With the gate on the attention side, the FFN runs only the expert layers; otherwise it runs all layers (afd_plugin/v1/worker/ffn_model_runner.py:211-215).

        num_layers = max(int(self.num_layers or 0), 1)
        experts_layer_indices = frozenset(
            model.get_experts_layer_indices(),
        )
        layer_indices = (
            tuple(sorted(experts_layer_indices))
            if self.afd_config.compute_gate_on_attention
            else tuple(range(num_layers))
        )
layer_indices: expert layers only when the gate runs on the attention side.

For each received layer, the runner picks the compute call. An expert layer with the gate on the attention side calls compute_experts_output with the transported router logits; any other layer calls compute_ffn_output (afd_plugin/v1/worker/ffn_model_runner.py:251-267).

                    if (
                        uses_remote_experts
                        and self.afd_config.compute_gate_on_attention
                    ):
                        router_logits = payload.router_logits
                        assert router_logits is not None
                        rank_ffn_output = model.compute_experts_output(
                            hidden_states,
                            layer_idx,
                            router_logits,
                        )
                    else:
                        rank_ffn_output = self._execute_eager_mode(
                            hidden_states,
                            layer_idx,
                            input_ids=payload.input_ids,
                        )
compute_experts_output (gate on attention) vs compute_ffn_output (gate on FFN).

Step 4: compute_experts_output needs the gate cleared#

compute_experts_output runs the native experts runner with the transported router logits. It checks that the runner gate is None, which is the v0.30 contract for external routing (afd_plugin/model_executor/models/deepseek_v2.py:702-721).

        # v0.30 runner contract: gate is None means the runner consumes the
        # router logits transported from Attention (external routing).
        if self.mlp.experts.gate is not None:
            raise RuntimeError("FFN native runner must use external routing")
        return self.mlp.experts(
            hidden_states=hidden_states,
            router_logits=router_logits,
        )
The FFN experts runner must use external routing (gate cleared).

Deep dive#

Deep dive

What crosses the wire, by case

The send order is fixed: hidden states, then router logits (if any), then input ids (if any) (afd_plugin/connectors/gpu/p2p.py:336-338).

CaseOn the wire (A to F)FFN compute
dense layer, any flaghidden_statescompute_ffn_output (native MLP)
MoE layer, gate on FFNhidden_statescompute_ffn_output (native MoE, native gate)
MoE layer, gate on attentionhidden_states + router_logitscompute_experts_output (external routing)
DeepSeek-V4 hash MoEhidden_states + int32 input_idscompute_ffn_output with ids

The input_ids are a 1-D int32 tensor, token-aligned with the hidden states; they are used only for the DeepSeek-V4 hash MoE (afd_plugin/connectors/gpu/p2p.py:210, afd_plugin/model_executor/models/deepseek_v4.py:89-92). The Data on the wire: tensors per layer and stage page reads the tensor shapes and the per-peer split.

Deep dive

The three attention-side MoE classes

Three classes can stand in for the MoE block on the attention role. They differ by device and by what they send.

ClassRole / deviceSendsLine
AFDDeepseekV2RemoteExpertsMoECUDA MoE shellhidden states; builds the gate when the flag is setafd_plugin/model_executor/models/deepseek_v2.py:277-335
AFDAttentionFusedMoEthe shell's experts proxyhidden states + router logits (external routing)afd_plugin/model_executor/models/deepseek_v2.py:161-191
GateOnlyRemoteMoENPU gate-on-attentionhidden states + gate outputsafd_plugin/model_executor/models/deepseek_v2.py:197-274

The NPU path is covered on Ascend NPU overview: the same contract, different transports; this page reads the CUDA classes.

Deep dive

The afd_balanced routing strategy (benchmarks only)

For controlled MoE benchmarks, the plugin registers a deterministic routing strategy, afd_balanced (afd_plugin/model_executor/routing_simulator.py:166-171). It replaces the gate's choice with uniform weights and fixed, EP-balanced expert ids, so every rank gets an equal share (afd_plugin/model_executor/routing_simulator.py:122-163).

The strategy is registered in register_afd, outside the patch try/except (afd_plugin/__init__.py:183-187).

What can go wrong#

Why it matters. The two sides must agree on where the gate runs. If the attention side sends router logits but the FFN side keeps its own gate, the experts would route twice and the output would be wrong.

Check yourself#

  1. 1
    What does compute_gate_on_attention decide?
    easy
    Show answer

    Which role runs the MoE gate. False (the default) keeps the gate with the experts on the FFN side; true runs it on the attention side (afd_plugin/model_executor/models/deepseek_v2.py:306-320).

  2. 2
    When the gate runs on the attention side, which layers does the FFN run?
    easy
    Show answer

    Only the expert layers. layer_indices is the sorted expert-layer set when the flag is set, else all layers (afd_plugin/v1/worker/ffn_model_runner.py:211-215).

  3. 3
    What extra tensor crosses the wire when the gate runs on the attention side, and when is it consumed?
    medium
    Show answer

    router_logits. The FFN experts runner consumes it only when its gate is None (external routing), which AFD arranges by clearing the runner gate (afd_plugin/model_executor/models/deepseek_v2.py:548-553, afd_plugin/model_executor/models/deepseek_v2.py:714-721).

  4. 4
    What does DeepSeek-V4 add to the handoff, and why?
    medium
    Show answer

    A 1-D int32 input_ids tensor, because its hash router needs the token ids to pick experts (afd_plugin/connectors/gpu/p2p.py:210, afd_plugin/model_executor/models/deepseek_v4.py:89-92).

  5. 5
    What does the afd_balanced strategy return, and why is it not for production?
    hard
    Show answer

    Uniform weights (1 / top_k) and deterministic, EP-balanced expert ids (afd_plugin/model_executor/routing_simulator.py:99-119). It replaces the real routing, so it changes model outputs; it is for benchmark and profiling runs only (afd_plugin/model_executor/routing_simulator.py:10-11).