1. 背景
早期在开发 flagcx connector 的时候参考 vllm 版本为0.13.0,现在的 vllm 适配国产网卡的过程中主要 vllm 版本分别为 dcu 的 0.20.0 和沐曦的 0.20.2,故需要一个 connector 能够统一这两个版本的 vllm 以及QWEN3.6(暂时)。
2. 参考与调研
由attention + Mamba 混合架构的问题内的 9 条 bug 记录,总结为需要升级增加的功能包括:
- 支持导入新版本的 vllm 的 attention 模块 和 支持新版本的 vllm topo 库(传 list 的 backend),支持
get_attn_backend等等函数的新签名 ✅done - 支持HMA ✅done
- 从 router 进来之后的一个请求获取的 block_ids变成 2 维之后的完整透传以及原来所有一维索引变成 二维来对应。✅done
- 支持 prefill 的 tp > decode 的 tp ✅done
- 支持混合架构模型(mamba 层 kv cach、GDN 层的 kv cache等 ),见下面 2.1 的描述 ✅done
2.1 Hybrid attention and Hybrid KV Cache Manager
参考 https://huggingface.co/Qwen/Qwen3.6-35B-A3B/blob/main/config.json 官方的 config.json,看到模型的 layers 组成为[linear_attn x 3 + full_attn] x 10来组成 40 个 layers。这需要 connector 调用Hybrid KV Cache Manager去支持这类Hybrid model。
在 vllm 0.20 内的 nixl 支持了 mamba,vllm 0.22 内的nixl 支持了 GDN(qwen3.6需要)。上周的 mooncake 的 pr 支持了 GDN。所谓 GDN 就是:
/Users/joker/Desktop/project/baai/vllm/vllm/v1/worker/gpu_model_runner.py对一分为 2, (注:gdn,sw 等等都归于 mamba 类别)。
- AttentionSpec的话,当前就是 kv_caches[layer_name] = kv_cache.permute(*inv_order)
- MambaSpec的话,当前就是for 循环变量后的里面装了 conv 和 ssm 的 list

GDN 通过当前输入进入一个 conv 卷积看到局部上下文,再通过学习到的 delta 去 ssm 全局记忆里面拿出想要的 cache 去 decode 下一个 token。
故我们的传输 kv cache 就是需要传输 conv+ssm,其中 ssm 在 nixl/mooncake 中都传输 N-2 个 token 更新后的 ssm,由decode 自己算N-1 的状态。然后 vllm 内把这两个存 cache 的 [conv,ssm]放在一个 pages 里面,从过第一个 conv 的 lens 去索引 ssm 的 base addr。ssm 的尾巴后面自动 padding 到整页末尾。因此,connector 内的 get_num_new_matched_tokens 函数内对这种情况需要对应 -1 个 token。
当然注册的时候,直接对当前这个 list 内整个pages 注册。如图

┌───────────────────────┬─────────────┬─────────────┬──────────────┬────────────────┐
│ 层类型 / 场景 │ split_k_and │ blocks_firs │ 拆分路径 │ 每层 region 数 │
│ │ _v │ t │ │ │
├───────────────────────┼─────────────┼─────────────┼──────────────┼────────────────┤
│ attention(纯注意力模 │ │ │ 机制 │ │
│ 型, layer-first │ True │ False │ A:[K,V] │ 2 │
│ 后端) │ │ │ 两条 │ │
├───────────────────────┼─────────────┼─────────────┼──────────────┼────────────────┤
│ attention(GDN │ │ │ 机制 B:登记 │ │
│ 混合模型里, 因含 │ False │ True │ 1 条,展开劈 │ 2 │
│ mamba→blocks-first ) │ │ │ K/V │ │
├───────────────────────┼─────────────┼─────────────┼──────────────┼────────────────┤
│ │ │ │ 都不拆([con │ 1(整页 │
│ Mamba / GDN 层 │ False │ True │ v] + split=F │ conv+ssm) │
│ │ │ │ alse) │ │
├───────────────────────┼─────────────┼─────────────┼───────────── ─┼────────────────┤
│ MLA 层 │ False │ False(is_ml │ 都不拆 │ 1(整页 page_s │
│ │ │ a) │ │ ize_bytes) │
└───────────────────────┴─────────────┴─────────────┴──────────────┴────────────────┘
3. 测试与验证
basic
# step 0. 直接拉
docker pull vllm/vllm-openai:v0.20.2
docker run -d -it --network=host --gpus all --privileged --ipc=host --entrypoint bash --ulimit memlock=-1 --ulimit stack=67108864 -v /public-flash/zhiyuan/flagcx/:/workspace/ --name 20-liuda vllm-openai:v0.20.2-liuda
apt update && apt install -y --no-install-recommends net-tools vim git zoxide wget
apt-get update && apt-get install -y libhwloc15
pip install -U scikit-build-core==0.11 pybind11 ninja cmake
# step 1. vllm下载与编译(pass)
# git clone --branch v0.20.2 --depth 1 https://github.com/vllm-project/vllm.git
# cd vllm
# uv venv --python 3.12 .venv
# source /workspace/liuda/vllm/.venv/bin/activate
# VLLM_USE_PRECOMPILED=1 uv pip install -e . --torch-backend=auto -v放弃了 这个源码安装慢
# uv pip install vllm==0.20.2
# deactivate
# step 2. 模型
modelscope download --model Qwen/Qwen3.6-35B-A3B
/workspace/liuda/models/qwen/Qwen3.6-35B-A3B
# step 3. 仓库, plugin内的改动放在自己的仓库内的
https://github.com/leoda1/vllm-plugin-FL.git
cd vllm-plugin-FL
git checkout new
pip install --no-build-isolation -e .
git clone https://github.com/flagos-ai/FlagCX.git
git submodule update --init --recursive
export CPATH=/usr/local/lib/python3.12/site-packages/nvidia/cu13/include:$CPATH
export LD_LIBRARY_PATH=/usr/local/cuda:$LD_LIBRARY_PATH
pip install . -v --no-build-isolation
git clone https://github.com/flagos-ai/FlagGems
cd FlagGems
pip install --no-build-isolation -e .pd step(确保前面 0 error后执行这里的)
step1. 在节点 1 上起 prefill,注意这里多了一个环境变量 --no-disable-hybrid-kv-cache-manager
export NCCL_SOCKET_IFNAME=bond0
export TORCH_DEVICE_BACKEND_AUTOLOAD=0
export NCCL_IB_GID_INDEX=3
export FLAGCX_DEBUG=version
export VLLM_RPC_TIMEOUT=600000
export FLAGCX_PATH=/workspace/liuda/FlagCX
export VLLM_ENGINE_ITERATION_TIMEOUT_S=600
export PYTHONPATH=/workspace/liuda/vllm-plugin-FL:${PYTHONPATH}
export VLLM_FL_PREFER_ENABLED=false
export FLAGCX_P2P_SLICE_SIZE=65536
export VLLM_PLUGINS=fl
vllm serve /workspace/liuda/models/qwen/Qwen3.6-35B-A3B \
--host 0.0.0.0 \
--port 20001 \
--tensor-parallel-size 8 \
--seed 1024 \
--max-model-len 40960 \
--served-model-name base_model \
--max-num-batched-tokens 65536 \
--max-num-seqs 256 \
--trust-remote-code \
--no-disable-hybrid-kv-cache-manager \
--kv-cache-dtype fp8 \
--gpu-memory-utilization 0.8 \
--kv-transfer-config \
'{"kv_connector":"FlagCXConnector","kv_role":"kv_producer"}' > prefill-1.log 2>&1 &step2. 在节点 2上起 decode
export NCCL_SOCKET_IFNAME=bond0
export GLOO_SOCKET_IFNAME=bond0
export NCCL_IB_GID_INDEX=3
export VLLM_RPC_TIMEOUT=600000
export FLAGCX_DEBUG=version
export FLAGCX_DEBUG_SUBSYS=
export FLAGCX_PATH=/workspace/liuda/FlagCX
export VLLM_ENGINE_ITERATION_TIMEOUT_S=600
export PYTHONPATH=/workspace/liuda/vllm-plugin-FL:${PYTHONPATH}
export VLLM_FL_PREFER_ENABLED=false
export VLLM_PLUGINS=fl
export FLAGCX_P2P_SLICE_SIZE=65536
vllm serve /workspace/liuda/models/qwen/Qwen3.6-35B-A3B \
--host 0.0.0.0 \
--port 20002 \
--tensor-parallel-size 8 \
--seed 1024 \
--max-model-len 40960 \
--served-model-name base_model \
--max-num-batched-tokens 65536 \
--max-num-seqs 256 \
--trust-remote-code \
--no-disable-hybrid-kv-cache-manager \
--kv-cache-dtype fp8 \
--gpu-memory-utilization 0.8 \
--kv-transfer-config \
'{"kv_connector":"FlagCXConnector","kv_role":"kv_consumer"}'> decode.log 2>&1 &step3. 起 router,这里面的 router.py在 vllm-plugin-FL/examples/disaggregated_serving_xpyd/router.py
python3 router.py \
--host 0.0.0.0 \
--port 8000 \
--prefill http://10.0.27.1:20001 8998 \
--decode http://10.0.27.10:20002 > router.log 2>&1 &
step 4. 测试:
curl -v http://127.0.0.1:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "base_model",
"prompt": "100 + 100 =",
"max_tokens": 128,
"stream": true
}'#!/bin/bash
SLEEP_BETWEEN=5
unset http_proxy
unset https_proxy
unset HTTP_PROXY
unset HTTPS_PROXY
export VLLM_SERVER_DEV_MODE=1
BASE_ARGS="vllm bench serve \
--base-url http://10.0.27.10:8000 \
--endpoint /v1/completions \
--model /workspace/liuda/models/qwen/Qwen3.6-35B-A3B \
--served-model-name base_model \
--dataset-name random \
--num-warmups 20 \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,90,99"
run_test() {
local input_len=$1
local output_len=$2
local rps=$3
local num_prompts=$(( rps * 5 ))
echo "========================================"
echo "Testing input=${input_len} output=${output_len} rps=${rps} num_prompts=${num_prompts}"
echo "========================================"
$BASE_ARGS \
--random-input-len "$input_len" \
--random-output-len "$output_len" \
--request-rate "$rps" \
--num-prompts "$num_prompts"
# echo "Sleeping ${SLEEP_BETWEEN}s before next test..."
# curl -s -X POST http://10.0.26.138:8000/reset_prefix_cache && echo "OK"
# sleep "$SLEEP_BETWEEN"
}
# input output rps
run_test 1024 1024 75
#run_test 4096 1024 70
#run_test 8192 1024 65
#run_test 16384 1024 60
#run_test 32768 1024 45
echo "All tests done."
bug
1. flashinfer报错(放弃,直接用官方的镜像跑起来更容易)
如果报错:
WARNING 07-08 03:03:45 [gdn_linear_attn.py:733] /workspace/liuda/vllm/.venv/lib/python3.12/site-packages/flashinfer/data/cutlass/include/cutlass/cuda_host_adapter.hpp(150): error: identifier "PFN_cuTensorMapEncodeTiled" is undefined
需要去下一个正确的头文件:
FILE=/workspace/liuda/vllm/.venv/lib/python3.12/site-packages/flashinfer/data/cutlass/include/cutlass/cuda_host_adapter.hpp
cp "$FILE" "$FILE.bak"
curl -sL https://raw.githubusercontent.com/NVIDIA/cutlass/main/include/cutlass/cuda_host_adapter.hpp -o "$FILE"
rm -rf /root/.cache/flashinferflashinfer 的 gdn jit 零时编译的各类头文件都是 hardcode 的,没法虚拟环境里面每次报错少一个外部的头文件就 export 一个。。。