跳到内容

专家并行部署

vLLM 支持专家并行 (EP),它允许将混合专家 (MoE) 模型中的专家部署在不同的 GPU 上,从而提高局部性、效率和整体吞吐量。

EP 通常与数据并行 (DP) 结合使用。虽然 DP 可以独立于 EP 使用,但 EP 与 DP 结合使用时效率更高。您可以在此处阅读有关数据并行的更多信息。

先决条件

在使用 EP 之前,您需要安装必要的依赖项。我们正在积极努力使这一过程在未来变得更简单。

  1. 安装 DeepEP:按照 vLLM 关于 EP 内核的指南 在此设置主机环境。
  2. 安装 DeepGEMM 库:遵循官方说明
  3. 对于分离式服务:通过运行 install_gdrcopy.sh 脚本(例如 install_gdrcopy.sh "${GDRCOPY_OS_VERSION}" "12.8" "x64")安装 gdrcopy。您可以在此处找到可用的操作系统版本。

后端选择指南

vLLM 为 EP 提供了多种通信后端。使用 --all2all-backend 进行选择

Backend Use Case 特性 最佳适用场景
allgather_reducescatter 默认后端 使用 allgather/reducescatter 原语的标准 all2all 通用,适用于任何 EP+DP 配置
deepep_high_throughput 多节点预填充 带连续布局的分组 GEMM,针对预填充优化 预填充主导的工作负载,高吞吐量场景
deepep_low_latency 多节点解码 CUDA 图支持,掩码布局,针对解码优化 解码主导的工作负载,低延迟场景
flashinfer_nvlink_one_sided MNNVL 系统 FlashInfer 用于多节点 NVLink 的单向 A2A 策略 高吞吐量工作负载
flashinfer_nvlink_two_sided MNNVL 系统 FlashInfer 用于多节点 NVLink 的双向 A2A 策略 跨节点具备 NVLink 的系统

单节点部署

警告

EP 是一个实验性功能。参数名称和默认值可能会在未来发生变化。

配置

通过设置 --enable-expert-parallel 标志启用 EP。EP 大小自动计算如下:

EP_SIZE = TP_SIZE × DP_SIZE

其中

  • TP_SIZE:张量并行大小
  • DP_SIZE:数据并行大小
  • EP_SIZE:专家并行大小(自动计算)

启用 EP 后的层行为

启用 EP 后,MoE 模型中的不同层行为不同

层类型 行为 使用的并行化方式
专家 (MoE) 层 跨所有 EP rank 分片 大小为 TP × DP 的专家并行 (EP)
注意力层 行为取决于 TP 大小 见下文

注意力层并行化

  • TP = 1:注意力权重在所有 DP rank 之间复制(数据并行)
  • TP > 1:注意力权重在每个 DP 组内的 TP rank 之间使用张量并行分片

例如,TP=2, DP=4(总共 8 个 GPU)

  • 专家层形成一个大小为 8 的 EP 组,专家分布在所有 GPU 上
  • 注意力层在 4 个 DP 组的每一组内使用 TP=2

与数据并行部署的关键区别

如果不使用 --enable-expert-parallel,MoE 层将使用张量并行(形成大小为 TP × DP 的 TP 组),类似于密集模型。启用 EP 后,专家层切换到专家并行,这可以为 MoE 模型提供更好的效率和局部性。

示例命令

以下命令服务于 DeepSeek-V3-0324 模型,采用 1 路张量并行、8 路(注意力)数据并行和 8 路专家并行。注意力权重在所有 GPU 上复制,而专家权重在 GPU 之间拆分。它可以在具有 8 个 GPU 的 H200(或 H20)节点上运行。对于 H100,您可以尝试服务较小的模型或参考多节点部署部分。

# Single node EP deployment
vllm serve deepseek-ai/DeepSeek-V3-0324 \
    --tensor-parallel-size 1 \       # Tensor parallelism across 1 GPU
    --data-parallel-size 8 \         # Data parallelism across 8 processes
    --enable-expert-parallel         # Enable expert parallelism

多节点部署

对于多节点部署,请使用 DeepEP 通信内核,并采用两种模式之一(参见上方的后端选择指南)。

部署步骤

  1. 每个节点运行一个命令 - 每个节点都需要自己的启动命令
  2. 配置网络 - 确保正确的 IP 地址和端口配置
  3. 设置节点角色 - 第一个节点处理请求,额外节点以无头 (headless) 模式运行

示例:2 节点部署

以下示例使用 deepep_low_latency 模式在 2 个节点上部署 DeepSeek-V3-0324

# Node 1 (Primary - handles incoming requests)
vllm serve deepseek-ai/DeepSeek-V3-0324 \
    --all2all-backend deepep_low_latency \
    --tensor-parallel-size 1 \               # TP size per node
    --enable-expert-parallel \               # Enable EP
    --data-parallel-size 16 \                # Total DP size across all nodes
    --data-parallel-size-local 8 \           # Local DP size on this node (8 GPUs per node)
    --data-parallel-address 192.168.1.100 \  # Replace with actual IP of Node 1
    --data-parallel-rpc-port 13345 \         # RPC communication port, can be any port as long as reachable by all nodes
    --api-server-count=8                     # Number of API servers for load handling (scaling this out to # local ranks is recommended)

# Node 2 (Secondary - headless mode, no API server)
vllm serve deepseek-ai/DeepSeek-V3-0324 \
    --all2all-backend deepep_low_latency \
    --tensor-parallel-size 1 \               # TP size per node
    --enable-expert-parallel \               # Enable EP
    --data-parallel-size 16 \                # Total DP size across all nodes
    --data-parallel-size-local 8 \           # Local DP size on this node
    --data-parallel-start-rank 8 \           # Starting rank offset for this node
    --data-parallel-address 192.168.1.100 \  # IP of primary node (Node 1)
    --data-parallel-rpc-port 13345 \         # Same RPC port as primary
    --headless                               # No API server, worker only

关键配置注意事项

  • 无头模式:辅助节点以 --headless 标志运行,这意味着所有客户端请求均由主节点处理
  • Rank 计算--data-parallel-start-rank 应等于之前节点的累积本地 DP 大小
  • 负载扩展:调整主节点上的 --api-server-count 以处理更高的请求负载

网络配置

InfiniBand 集群

在 InfiniBand 网络集群上,设置此环境变量以防止初始化挂起

export GLOO_SOCKET_IFNAME=eth0
这确保了 torch 分布式组发现使用以太网而不是 InfiniBand 进行初始设置。

专家并行负载均衡器 (EPLB)

虽然 MoE 模型通常经过训练以使每个专家接收相似数量的 token,但在实践中,token 在专家之间的分布可能高度倾斜。vLLM 提供了专家并行负载均衡器 (EPLB) 来重新分配跨 EP rank 的专家映射,从而平衡专家之间的负载。

配置

通过 --enable-eplb 标志启用 EPLB。

启用后,vLLM 会在每次前向传递时收集负载统计信息,并定期重新平衡专家分布。

EPLB 参数

使用 --eplb-config 参数配置 EPLB,该参数接受 JSON 字符串。可用的键及其描述如下:

参数 描述 默认值
window_size 用于跟踪重新平衡决策的引擎步数 1000
step_interval 重新平衡的频率(每 N 个引擎步) 3000
log_balancedness 记录平衡度指标(每个专家的平均 token ÷ 每个专家的最大 token) false
num_redundant_experts 除平均分布外,每个 EP rank 的额外全局专家数量 0
use_async 使用非阻塞 EPLB 以减少延迟开销 false
policy 专家并行负载均衡的策略类型 "default"

例如

vllm serve Qwen/Qwen3-30B-A3B \
  --enable-eplb \
  --eplb-config '{"window_size":1000,"step_interval":3000,"num_redundant_experts":2,"log_balancedness":true}'
倾向于使用独立参数而不是 JSON?
vllm serve Qwen/Qwen3-30B-A3B \
        --enable-eplb \
        --eplb-config.window_size 1000 \
        --eplb-config.step_interval 3000 \
        --eplb-config.num_redundant_experts 2 \
        --eplb-config.log_balancedness true

专家分布公式

  • 默认:每个 EP rank 有 总专家数 ÷ EP rank 数 个专家
  • 带冗余:每个 EP rank 有 (总专家数 + 冗余专家数) ÷ EP rank 数 个专家

内存占用开销

EPLB 使用需要放入 GPU 内存的冗余专家。这意味着 EPLB 可能不适合内存受限的环境,或者 KV 缓存空间非常宝贵的情况。

此开销等于 MoE 层数 * 每个专家字节数 * (总专家数 + 冗余专家数) ÷ EP rank 数。对于 DeepSeekV3,每个 EP rank 一个冗余专家大约为 2.4 GB

示例命令

启用 EPLB 的单节点部署

# Single node with EPLB load balancing
vllm serve deepseek-ai/DeepSeek-V3-0324 \
    --tensor-parallel-size 1 \       # Tensor parallelism
    --data-parallel-size 8 \         # Data parallelism
    --enable-expert-parallel \       # Enable EP
    --enable-eplb \                  # Enable load balancer
    --eplb-config '{"window_size":1000,"step_interval":3000,"num_redundant_experts":2,"log_balancedness":true}'

对于多节点部署,请将这些 EPLB 标志添加到每个节点的命令中。我们建议在大型用例中将 --eplb-config '{"num_redundant_experts":32}' 设置为 32,以便最受欢迎的专家始终可用。

高级配置

性能优化

  • DeepEP 内核high_throughputlow_latency 内核针对分离式服务进行了优化,在混合工作负载下表现可能不佳
  • 双批重叠 (Dual Batch Overlap):使用 --enable-dbo 将 all-to-all 通信与计算重叠。详见双批重叠
  • 异步调度(实验性):尝试 --async-scheduling 以将调度与模型执行重叠。

故障排除

  • non-zero status: 7 cannot register cq buf:使用 Infiniband/RoCE 时,确保主机 VM 和 pod 的 ulimit -l 显示为 "unlimited"。
  • init failed for transport: IBGDA:缺少 InfiniBand GDA 内核模块。在每个 GPU 节点上运行 tools/ep_kernels/configure_system_drivers.sh 并重启。这也修复了错误 NVSHMEM API called before NVSHMEM initialization has completed
  • NVSHMEM 对等断开连接:通常是网络配置错误。如果通过 Kubernetes 部署,请验证每个 pod 运行时的 hostNetwork: true, securityContext.privileged: true,以访问 Infiniband。

基准测试

  • 使用模拟器标志 VLLM_MOE_ROUTING_SIMULATION_STRATEGY=uniform_randomVLLM_RANDOMIZE_DP_DUMMY_INPUTS=1,以便 token 路由在 EP rank 之间保持平衡。

  • 增加 VLLM_MOE_DP_CHUNK_SIZE 可以通过增加 rank 间 token 传输的最大批大小来提高吞吐量。这可能会导致 DeepEP 抛出 assert self.nvshmem_qp_depth >= (num_max_dispatch_tokens_per_rank + 1) * 2,这可以通过增加环境变量 NVSHMEM_QP_DEPTH 来修复。

分离式服务(预填充/解码分离)

对于需要对首字延迟 (TTFT) 和字间延迟提供严格 SLA 保证的生产部署,分离式服务允许对预填充和解码操作进行独立扩展。

架构概述

  • 预填充实例:使用 deepep_high_throughput 后端以实现最佳预填充性能
  • 解码实例:使用 deepep_low_latency 后端以实现最小解码延迟
  • KV 缓存传输:通过 NIXL 或其他 KV 连接器连接实例

设置步骤

  1. 安装 gdrcopy/ucx/nixl:为了获得最高性能,请运行 install_gdrcopy.sh 脚本以安装 gdrcopy(例如 install_gdrcopy.sh "${GDRCOPY_OS_VERSION}" "12.8" "x64")。您可以在此处找到可用的操作系统版本。如果未安装 gdrcopy,使用普通的 pip install nixl 仍然可以工作,但性能会降低。nixlucx 作为依赖项通过 pip 安装。对于非 CUDA 平台,要安装带有非 CUDA UCX 构建的 nixl,请运行 install_nixl_from_source_ubuntu.py 脚本。

  2. 配置两个实例:将此标志添加到预填充和解码实例 --kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_both"}'。注意,您还可以指定一个或多个 NIXL_Backend。例如:--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_both", "kv_connector_extra_config":{"backends":["UCX", "GDS"]}}'

  3. 客户端编排:使用下面的客户端脚本协调预填充/解码操作。我们正在积极致力于路由解决方案。

客户端编排示例

from openai import OpenAI
import uuid

try:
    # 1: Set up clients for prefill and decode instances
    openai_api_key = "EMPTY"  # vLLM doesn't require a real API key

    # Replace these IP addresses with your actual instance addresses
    prefill_client = OpenAI(
        api_key=openai_api_key,
        base_url="http://192.168.1.100:8000/v1",  # Prefill instance URL
    )
    decode_client = OpenAI(
        api_key=openai_api_key,
        base_url="http://192.168.1.101:8001/v1",  # Decode instance URL  
    )

    # Get model name from prefill instance
    models = prefill_client.models.list()
    model = models.data[0].id
    print(f"Using model: {model}")

    # 2: Prefill Phase
    # Generate unique request ID to link prefill and decode operations
    request_id = str(uuid.uuid4())
    print(f"Request ID: {request_id}")

    prefill_response = prefill_client.completions.create(
        model=model,
        # Prompt must exceed vLLM's block size (16 tokens) for PD to work
        prompt="Write a detailed explanation of Paged Attention for Transformers works including the management of KV cache for multi-turn conversations",
        max_tokens=1,  # Force prefill-only operation
        extra_body={
            "kv_transfer_params": {
                "do_remote_decode": True,     # Enable remote decode
                "do_remote_prefill": False,   # This is the prefill instance
                "remote_engine_id": None,     # Will be populated by vLLM
                "remote_block_ids": None,     # Will be populated by vLLM
                "remote_host": None,          # Will be populated by vLLM
                "remote_port": None,          # Will be populated by vLLM
            }
        },
        extra_headers={"X-Request-Id": request_id},
    )

    print("-" * 50)
    print("✓ Prefill completed successfully")
    print(f"Prefill response: {prefill_response.choices[0].text}")

    # 3: Decode Phase
    # Transfer KV cache parameters from prefill to decode instance
    decode_response = decode_client.completions.create(
        model=model,
        prompt="This prompt is ignored during decode",  # Original prompt not needed
        max_tokens=150,  # Generate up to 150 tokens
        extra_body={
            "kv_transfer_params": prefill_response.kv_transfer_params  # Pass KV cache info
        },
        extra_headers={"X-Request-Id": request_id},  # Same request ID
    )

    print("-" * 50)
    print("✓ Decode completed successfully")
    print(f"Final response: {decode_response.choices[0].text}")

except Exception as e:
    print(f"❌ Error during disaggregated serving: {e}")
    print("Check that both prefill and decode instances are running and accessible")

基准测试

  • 要模拟分离式服务的解码部署,请将 --kv-transfer-config '{"kv_connector":"DecodeBenchConnector","kv_role":"kv_both"}' 传递给 vllm serve 调用。连接器用随机值填充 KV 缓存,以便可以独立对解码进行分析。

  • CUDAGraph 捕获:使用 --compilation_config '{"cudagraph_mode": "FULL_DECODE_ONLY"}' 仅为解码启用 CUDA 图捕获并保存 KV 缓存。