AFD Plugin Explained

Tour: Run a recipe

IntermediateGuided toursabout 7 min read

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

In one sentence

This tour reads one GPU recipe line by line. It shows what each flag does inside the plugin and vLLM, and how to send a request.

The idea#

A recipe is a shell script that starts the two servers of an AFD run. This tour reads 2a2f_graph_dbo_dp1tp2.sh, the 2A2F graph recipe for DeepSeek-V2-Lite (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:1-61). It starts one attention server on two GPUs and one FFN server on two GPUs, with CUDA graphs and DBO on.

Both servers use the same --additional-config block; only role and the GPUs differ. The Recipes reference page lists every recipe; the Tour: Life of a request tour follows a request through this same recipe.

The picture#

Figure: the recipe starts two servers with the same afd block, waits for both to come up, then sends traffic to the attention HTTP port only.
Open full size ↗

Read panel 1 as the two serve commands, side by side. Read panel 2 as the order: background both, wait for startup, then send requests to the attention port.

1Turn the V2 runner on#

The script exports one environment variable before it starts a server (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:5).

MODEL_PATH=${MODEL_PATH:-/path/model_weights/DeepSeek-V2-Lite}
export VLLM_USE_V2_MODEL_RUNNER=1
The model path and the V2 runner switch.

AFD runs on Model Runner V2. Setting VLLM_USE_V2_MODEL_RUNNER=1 turns the V2 runner on explicitly. vLLM also picks V2 by default when the variable is unset (vllm:vllm/config/vllm.py:692-694); the recipe sets it so the choice is clear. The validator raises only when V2 resolves to off (afd_plugin/validation.py:90-91).

2The attention serve command and the parallel sizes#

The attention server runs on GPUs 0 and 1. The parallel flags set the per-role rank count (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:7-10).

CUDA_VISIBLE_DEVICES=0,1 uv run vllm serve "$MODEL_PATH" \
    --data-parallel-size 1 \
    --tensor-parallel-size 2 \
    --enable-expert-parallel \
GPUs 0,1; DP1 TP2; expert parallel on.
  • CUDA_VISIBLE_DEVICES=0,1 picks the two GPUs for this role.
  • --data-parallel-size 1 --tensor-parallel-size 2 gives two attention ranks, because the rank count must equal DP × TP (afd_plugin/validation.py:107-117).
  • --enable-expert-parallel turns on static expert parallelism, which AFD requires (afd_plugin/validation.py:118-125).

See Deployment topologies: how to place the roles for the rank rule.

3The afd block in --additional-config#

The --additional-config JSON carries the AFD settings under the "afd" key (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:11-20).

    --additional-config '{
        "afd": {
            "role": "attention",
            "connector": "P2pNcclAFDConnector",
            "host": "127.0.0.1",
            "port": 6269,
            "num_attention_ranks": 2,
            "num_ffn_ranks": 2
        }
    }' \
The afd block: role, connector, host, port and rank counts.

The plugin reads this inner mapping and builds an AFDConfig (afd_plugin/config.py:42). The role is attention here; the connector is the GPU P2P connector; host and port set the rendezvous where the two roles meet; num_attention_ranks and num_ffn_ranks are both 2. See AFD config reference: the additional_config["afd"] keys.

4The batch, DBO and CUDA graph flags#

The next flags set the batch limits, DBO and the CUDA graph (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:21-29).

    --max-num-seqs 64 \
    --max-num-batched-tokens 64 \
    --enable-dbo \
    --dbo-decode-token-threshold 2 \
    --dbo-prefill-token-threshold 12 \
    --max-cudagraph-capture-size 64 \
    --compilation-config '{
        "cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes":[64]
    }' \
DBO thresholds and the FULL_DECODE_ONLY graph capture.

5The HTTP port and backgrounding#

The attention server binds an HTTP port and runs in the background (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:30-32).

    --host 127.0.0.1 \
    --port 18305 \
    --trust-remote-code > attn.log 2>&1 &
The HTTP host and port; logs to attn.log; runs in the background.

Clients connect to this HTTP port, 18305. It is a different port from the AFD 6269: the HTTP port carries requests, the AFD port carries the connector setup. The command ends with &, so it runs in the background.

6The FFN server: the same block, role ffn#

The FFN server is the same command with two changes: GPUs 2 and 3, and role: ffn (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:34-59).

CUDA_VISIBLE_DEVICES=2,3 uv run vllm serve "$MODEL_PATH" \
    --data-parallel-size 1 \
    --tensor-parallel-size 2 \
    --enable-expert-parallel \
    --additional-config '{
        "afd": {
            "role": "ffn",
            "connector": "P2pNcclAFDConnector",
            "host": "127.0.0.1",
            "port": 6269,
            "num_attention_ranks": 2,
            "num_ffn_ranks": 2
        }
    }' \
The FFN server: GPUs 2,3, role ffn, same host, port and ranks.

The FFN server reads the same host, port and rank counts, so the two roles meet at the same rendezvous. It runs no KV cache and no real scheduler, only a no-op stand-in; the connector drives it (afd_plugin/compat/patches/engine_core.py:599-623). Send no client traffic here. See Attention and FFN roles: what each server owns and The FFN runtime: a connector-driven server loop.

7Start order: background both, then wait#

The script backgrounds both servers and ends with wait (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:53-61).

    --trust-remote-code > ffn.log 2>&1 &

wait
The FFN server backgrounds, then the script waits for both.

Both roles start together and meet at the AFD rendezvous. Wait for Application startup complete in each log before you send traffic (recipe/gpu/P2pNcclAFDConnector/README.md:39-43).

Deep dive

Run it from the repository root

Set the model path and the V2 runner, then run the script (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/README.md:88-92).

export MODEL_PATH=/path/model_weights/DeepSeek-V2-Lite
export VLLM_USE_V2_MODEL_RUNNER=1
bash recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh

The script writes attn.log and ffn.log in the current directory.

8Send a request, then benchmark it#

Send requests only to the attention HTTP server (README.md:258-260). The FFN server has no request path; a request there fails fast (afd_plugin/v1/worker/ffn_worker.py:122-131).

curl http://127.0.0.1:18305/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{"model": "'"$MODEL_PATH"'", "prompt": "Hello", "max_tokens": 16}'

To benchmark, drive load against the same port with the repository's wrapper around vllm bench serve (tools/benchmarks/request_generator.sh:44-57). The README shows the benchmark command for this model (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/README.md:104-120). See Tuning: the knobs the repo exposes.

What can go wrong#

Why it matters. The recipe is the full launch contract. One wrong flag, port, or target stops the run or hangs the rendezvous.

Check yourself#

  1. 1
    Which port do clients connect to, and which port is the AFD rendezvous?
    easy
    Show answer
  2. 2
    What changes between the attention and FFN serve commands?
    easy
    Show answer
  3. 3
    Why does the recipe set VLLM_USE_V2_MODEL_RUNNER=1 if vLLM picks V2 by default?
    medium
    Show answer

    To be explicit. vLLM picks V2 by default when the variable is unset (vllm:vllm/config/vllm.py:692-694), and AFD requires the V2 runner (afd_plugin/validation.py:90-91).

  4. 4
    The recipe turns on DBO with thresholds 2 and 12. What constraint must the run then meet?
    medium
    Show answer

    Exactly two microbatches. AFD raises when DBO or ubatching is on and num_ubatches != 2 (afd_plugin/validation.py:126-131).

  5. 5
    Which two flags switch this recipe from eager to graph, and which graph mode do they set?
    hard
    Show answer

    --max-cudagraph-capture-size 64 and --compilation-config with cudagraph_mode: FULL_DECODE_ONLY (recipe/gpu/P2pNcclAFDConnector/deepseek_v2_lite/prefill_decode_colocation/2a2f_graph_dbo_dp1tp2.sh:26-28). GPU AFD allows only that mode (afd_plugin/v1/worker/cuda_graph.py:20-21).