进阶推理:为什么您的单节点 vLLM 设置需要预填充 (Prefill) 与解码 (Decode) 解耦

22 分钟阅读
AMD 与嵌入式 LLM

简而言之: 预填充和解码阶段在 GPU 上争夺资源,导致高负载下的 ITL 激增。我们展示了如何利用 AMD 的 MORI-IO 连接器在单台 8-GPU MI300X 节点上实现它们的分离(解耦),与同等 GPU 数量下的标准同址部署相比,有效吞吐量 (Goodput) 提升了 2.5 倍,并保持了稳定的 token 生成。基准测试使用了 Qwen3-235B-A22B-FP8 模型,请求速率为 8 req/s,提示词 2000 tokens,输出 1000 tokens —— 请参阅表 3 和实验详情了解完整配置。


简介

在我们之前对 MoE 优化的探索 [1] 中,我们讨论了如何使用张量、流水线、数据和专家并行技术,在 8-GPU AMD Instinct MI300X 节点上部署大规模模型。本篇博客中,我们将展示由 AMD MORI-IO 实现的预填充-解码(PD)解耦如何解决这一瓶颈,在无需多节点集群的情况下,提供更高的有效吞吐量和更可预测的性能。

虽然你的 HBM(高带宽内存)已充分利用,计算负载平衡良好,vLLM 部署运行平稳,但一旦并发量增加,问题就会出现:间跳延迟 (ITL) 会出现不可预测的激增。根本原因很简单——预填充和解码本质上是不同的负载,它们在竞争相同的 GPU 资源。

预填充是计算密集型:它使用大规模 GEMM 并行处理整个提示词,成本直接随输入长度扩展。

解码是内存带宽密集型:它逐个生成 token,重复从 HBM 加载模型权重,单位字节的计算量相对较低。

当这两个阶段共享同一个实例时,它们会相互干扰。预填充请求会阻塞数十个正在进行的解码流,导致明显的卡顿,而解码负载也会延迟新预填充任务的调度。结果就是系统在两个阶段都无法高效且可预测地运行。


核心亮点

  • 在相同硬件上实现 2.5 倍的有效吞吐量。通过分离预填充和解码,在单台 8-GPU MI300X 节点上实现显著更高的符合 SLO 的吞吐量。

  • 消除高负载下的 ITL 抖动。专用的解码 GPU 通过消除预填充干扰,确保了稳定、可预测的 token 生成。

  • 单节点解耦——无需集群。在单节点内完整实现预填充-解码 (PD) 解耦,释放未被利用的性能。

  • 用于快速 KV 缓存传输的 MORI-IO。基于 RDMA 的 KV 数据迁移实现了各阶段间的高效切换。

  • 灵活的模式与权衡。写入模式提供最佳性能(较低的 TTFT),而读取模式提供更简单的编排——两者均远优于标准部署。


误区:“解耦仅适用于数据中心集群”

当推理工程师听到“预填充-解码 (PD) 解耦”时,通常会想到多节点数据中心设置——专用的预填充节点、专用的解码节点,以及连接它们的 RDMA 网络架构。自然的推论是:“我只有一个 8-GPU 节点,这不适用。”

这种假设会导致显著的性能浪费。PD 解耦可以完全在单台 8-GPU 系统内实现,如果你在意满足严格的延迟 SLO,这通常是正确的方法。

思路很简单:将两个阶段分离到专门的实例中。例如,四个 GPU 处理预填充,其余四个处理解码。每个实例都可以独立进行规模调整、并行化和调度,消除了限制单体部署的队头阻塞问题。

挑战在于切换过程。预填充阶段生成的 KV 缓存必须传输到解码实例——这可能涉及数 GB 的数据。如果处理不当,传输过程本身可能成为新的瓶颈,抵消解耦带来的收益。

AMD 通过 MORI-IO 解决了这个问题。这是一个贡献给 vLLM 的基于 RDMA 的 KV 缓存连接器 [4],构建在开源的 MORI(模块化 RDMA 接口)[5] 框架之上。

范围: 本博客专注于单节点 PD 解耦,部署在单台 8-GPU 设备上,旨在提高现有硬件上的有效吞吐量。


架构:使用 PD 解耦进行服务部署

拆分节点需要从单体部署转向轻量级微服务架构,包含下表 1 所示的三个组件。

组件作用
预填充实例处理输入提示并生成 KV 缓存(GPU 0–3)
解码实例利用传输过来的 KV 缓存逐个生成输出 token(GPU 4–7)
代理服务器客户端请求入口;先路由至预填充,再路由至解码

概括来说,两种模式都会将 KV 缓存(预填充输出)从预填充实例传输到解码实例,但区别在于谁启动传输以及在何时传输

  • 读取模式: 代理等待预填充完成,然后将 KV 块位置转发给解码实例。解码实例在开始生成之前通过 RDMA 拉取 KV 数据。
  • 写入模式: 代理同时分发任务给预填充和解码实例。预填充在计算每一层时,直接将 KV 数据推送到解码实例的内存中——这样解码实例在预填充完成后即可立即开始生成。

详细请求流程

MORI-IO 支持两种传输模式,区别在于谁发起 RDMA 传输以及代理如何编排两个阶段。模式由环境变量 VLLM_MORIIO_CONNECTOR_READ_MODE 设置。

读取模式 — 解码阶段拉取 KV 缓存

启用方式:export VLLM_MORIIO_CONNECTOR_READ_MODE=1

在读取模式下,代理串行分发至预填充和解码:它等待预填充完成,提取远程块 ID,然后转发给解码实例。解码实例利用这些 ID 通过 RDMA 从预填充实例拉取 KV 缓存。请求流程见图 1。


图 1:读取模式请求流程。代理串行分发——步骤 3(预填充响应)必须在步骤 4(分发至解码)之前完成。

单个请求的时间顺序:

  1. 客户端 → 代理:客户端发送推理请求。
  2. 代理 → 预填充:代理将提示词路由至预填充实例(max_tokens=1)。
  3. 预填充 → 代理(响应):预填充返回 remote_block_idsremote_engine_id,标识 KV 缓存所在位置。
  4. 代理 → 解码:代理将请求转发给解码实例,包含远程块 ID。
  5. 解码拉取 KV 缓存 (WAITING_FOR_REMOTE_KVS):解码实例对预填充实例的内存发起 RDMA 读取。调度器在传输完成前跳过该请求。
  6. 解码 → 预填充(清理):一旦所有 KV 块传输完成,解码通知预填充释放其占用的块。
  7. 解码 → 代理 → 客户端:生成的 token 通过 SSE 流回。

写入模式 — 预填充阶段推送 KV 缓存(默认)

启用方式:VLLM_MORIIO_CONNECTOR_READ_MODE 不设置(或 =0

在写入模式下,代理并发分发至预填充和解码——无需等待预填充完成。预填充实例在计算每一层时,直接将 KV 缓存一层一层推送到解码实例预分配的内存中。请求流程见图 2。


图 2:写入模式请求流程。代理并发发起预填充和解码任务(步骤 2);预填充在计算的同时通过 RDMA WRITE 逐层推送 KV 数据(步骤 3),而解码在等待。

单个请求的时间顺序:

  1. 客户端 → 代理:客户端发送推理请求。
  2. 代理 → 预填充 AND 代理 → 解码(并发):代理并行发起两个请求。预填充请求包含解码的连接详情;解码请求包含预填充的连接详情。代理不会阻塞等待预填充响应。
  3. 预填充推送 KV 缓存:每计算完一层,save_kv_layer 都会发起一次 RDMA 写入,直接进入解码实例预分配的 KV 块内存。对于分块预填充,块会在最后一块计算完之前累积,之后触发写入。
  4. 解码等待写入完成 (WAITING_FOR_REMOTE_KVS):解码调度器在每一步轮询 pop_finished_write_req_ids,直到收到所有块。
  5. 解码生成:一旦所有 KV 块到达,解码立即将请求移至就绪队列并开始自回归生成。
  6. 解码 → 代理 → 客户端:生成的 token 通过 SSE 流回。

代理层面的关键代码区别仅在于一个条件判断。

# examples/online_serving/disaggregated_serving/moriio_toy_proxy_server.py
 
if TRANSFER_TYPE == "READ":
    # Serial: wait for prefill to finish, extract block IDs for decode to pull.
    prefill_response = await send_prefill_task
    req_data["kv_transfer_params"]["remote_engine_id"] = prefill_response[
        "kv_transfer_params"
    ]["remote_engine_id"]
    req_data["kv_transfer_params"]["remote_block_ids"] = prefill_response[
        "kv_transfer_params"
    ]["remote_block_ids"]
 
# In WRITE mode, execution falls through here immediately —
# no await on send_prefill_task. Both phases are already in flight.
decode_request_task = asyncio.create_task(
    start_decode_request(decode_instance_endpoint["request_address"], req_data, request_id)
)

在读取模式下,remote_block_ids 必须通过代理中继,因为解码实例需要知道拉取哪些特定的块。在写入模式下,预填充主动推送至解码地址——无需中继块 ID。

读取模式 vs. 写入模式:一览

在底层,MORI-IO(在 vLLM 中作为 MoRIIOConnector 公开)管理 KV 缓存的切换。无论传输模式如何,在实例对之间进行首次 RDMA 传输前,MORI-IO 会通过 ZMQ 执行一次元数据交换——共享 KV 缓存基地址、块大小和每层张量跨度。此握手在后台线程中异步执行,不阻塞引擎循环,产生的 RDMA 会话会被缓存以供后续请求使用。

两种模式共享相同的握手和 RDMA 传输——区别完全在于代理分发层和传输方向。表 2 总结了关键区别:

属性读取模式写入模式
VLLM_MORIIO_CONNECTOR_READ_MODE=1未设置(或 =0
RDMA 方向解码从预填充拉取预填充向解码推送
代理分发串行(等待预填充 → 分发至解码)并发(预填充和解码并行)
remote_block_ids 通过代理中继需要不需要
KV 清理信号解码拉取后通知预填充释放块预填充按请求跟踪写入完成

结果:Goodput(有效吞吐量)提升 2.5 倍

在深入配置细节之前,让我们看看解耦实际能带来什么。

为什么是 Goodput 而非吞吐量

单纯的原始吞吐量具有误导性——系统可能维持高请求速率,同时默默违背大多数用户的延迟目标。我们遵循 DistServe 方法论 [3],使用 有效吞吐量 (Goodput) 作为主要指标。

有效吞吐量 = 同时满足 TTFT < T_ttft 和 ITL < T_itl 的最大请求速率(req/s)。

这在一个数字中同时捕捉了成本(每秒请求数)和服务质量(延迟 SLO 达成情况)。我们的 SLO 目标:TTFT < 1 秒ITL < 50 ms/token。只有同时满足这两个条件的请求才计入有效吞吐量。

主要结果

图 3 显示请求速率 = 8 时的有效吞吐量

指标标准 (1× TP8)标准 (2× TP4)MORI-IO 读取 (1P+1D)MORI-IO 写入 (1P+1D)
满足双重 SLO 的请求26/10030/10070/10073/100
主要失败模式ITL 激增 (P99 ITL >> 50 ms)ITL 激增(双峰:约 30ms 和 150ms)部分请求 TTFT 超过 1s部分请求 TTFT 超过 1s
相对有效吞吐量0.9x1x2.4x2.5x

标准部署失败是因为 ITL 集中在两个集群中——高延迟集群(约 150ms)远超 50ms 阈值。两种解耦模式都彻底消除了 ITL 违规;剩余的失败主要为随请求速率攀升而出现的 TTFT 超时。写入模式(73)略优于读取模式(70),因为代理并发分发降低了 TTFT,使更多请求保持在 1s 阈值内。

1.00 s
50 ms
图 3:有效吞吐量测量。每个条形代表一个请求——灰色条形表示违反了至少一个 SLO 阈值。调整滑块以探索不同的 SLO 目标。默认:TTFT < 1s,ITL < 50ms。

不同请求速率下的 SLO 达成率

图 4 显示了请求速率从 0.5 到 10 时的 SLO 达成率

  • 标准服务 (1× TP8):从低请求速率就开始出现 ITL 违规,且在所有测试速率下均占主导地位。在速率为 8 时达成率为 26/100。
  • 标准服务 (2× TP4): 表现急剧下降——从速率 0.5 的 100% 下降到速率 1 的约 60%,到速率 2 时崩溃至约 25% 并进入平稳期。ITL 违规提前饱和。
  • MORI-IO 读取 (1P+1D): 在速率约为 5 之前保持 100% 达成率,随后因 TTFT 开始超标,在速率 10 时逐渐降至约 44%。
  • MORI-IO 写入 (1P+1D): 在速率约为 5.5 之前保持 100% 达成率,随后在速率 10 时逐渐降至约 46%。

图 4:不同请求速率下 SLO 达成率(满足 TTFT 和 ITL 目标的请求百分比)。两种解耦模式在所有测试速率下均表现出比所有标准服务配置更高的 SLO 达成率。

理解权衡

为什么 ITL(间跳延迟)得到改善

在标准部署中,预填充和解码共享同一个 vLLM 引擎,并在批次内竞争调度资源。单次预填充(在一个前向传递中处理所有输入 token)耗时远长于解码步骤。批次中的每个解码请求都必须等待预填充完成后才能生成下一个 token,直接推高了 ITL。

通过解耦,你的解码引擎专门处理解码批次。没有计算密集型的预填充任务打断步骤节奏,因此无论有多少新请求进入系统,ITL 都变得稳定且可预测。无论读取模式还是写入模式,这一收益相同——解码引擎在两种情况下均与预填充隔离。

为什么 TTFT(首字延迟)变差

反之,解耦增加了通往首字(TTFT)的开销。在标准服务中

TTFT = queue + prefill_forward_pass + sample_T1 + detokenize + SSE_encode + network

在读取模式下,增加了两个额外步骤(图 5)

TTFT = queue(prefill) + prefill_forward_pass
     + [proxy serialization: await prefill, dispatch to decode]  <- Overhead 1
     + RDMA transfer (WAITING_FOR_REMOTE_KVS)                   <- Overhead 2
     + queue(decode) + sample_T1 + detokenize + SSE_encode + network

图 5:读取模式时序。开销 1(代理序列化)和开销 2(RDMA READ)是 TTFT 的累加贡献因素。

在写入模式下(图 6)

TTFT ≈ max(
           queue(prefill) + prefill_forward_pass + RDMA_write_time,
           queue(decode)
       ) + sample_T1 + detokenize + SSE_encode + network

图 6:写入模式时序。RDMA WRITE 与预填充计算重叠,因此开销 2 不会增加时钟 TTFT。

写入模式消除了开销 1。由于代理并发分发至两个实例,解码队列等待与预填充计算重叠。剩余成本——RDMA 传输本身——在结构上等同于读取模式中的 RDMA 读取。

开销 1:代理序列化(仅限读取模式)

在读取模式下,代理在分发给解码前必须等待完整的预填充响应。这增加了完整的预填充计算时间加上一次代理往返,从而增加了 TTFT。在写入模式下,此区块被跳过——解码请求在预填充完成前就已经在处理中了。

# examples/online_serving/disaggregated_serving/moriio_toy_proxy_server.py
 
if TRANSFER_TYPE == "READ":
    # In read mode, prefill and decode are executed serially.
    prefill_response = await send_prefill_task
    req_data["kv_transfer_params"]["remote_engine_id"] = prefill_response[
        "kv_transfer_params"
    ]["remote_engine_id"]
    req_data["kv_transfer_params"]["remote_block_ids"] = prefill_response[
        "kv_transfer_params"
    ]["remote_block_ids"]

开销 2:RDMA 传输等待

一旦解码实例收到请求,它进入 WAITING_FOR_REMOTE_KVS 状态。调度器在 RDMA 传输完成前每一步都会跳过该请求,完成后立即将其移至就绪队列进行调度。

# vllm/v1/request.py
 
WAITING_FOR_REMOTE_KVS = enum.auto()
 
# vllm/v1/core/sched/scheduler.py
# KVTransfer: skip request if still waiting for remote kvs.
 
if request.status == RequestStatus.WAITING_FOR_REMOTE_KVS:
    is_ready = self._update_waiting_for_remote_kv(request)
    if is_ready:
        request.status = RequestStatus.WAITING
    else:
        logger.debug("%s is still in WAITING_FOR_REMOTE_KVS state.",
                     request.request_id)
        self.waiting.pop_request()
        skipped_waiting_requests.prepend_request(request)
        continue

在读取模式下,此等待在预填充完成后开始。在写入模式下,此等待在解码请求到达时立即开始——与另一实例上进行的预填充计算重叠。

总结: 解耦以更长的首字等待时间为代价,换取了稳定、可预测的 ITL。延长多少取决于模式。在读取模式下,TTFT 至少增加一个完整的前向预填充传递(代理序列化)加上 RDMA 传输时间。在写入模式下,代理序列化被消除——TTFT 仅增加 RDMA 传输时间,且该时间与预填充计算重叠,因此净惩罚较小。无论哪种方式,ITL 的收益都是一样的。

什么时候应该使用这种方法?

表 4 总结了何时选择哪种部署方法。

你的情况建议
生产负载下 ITL p99 超过 SLO解耦——这是主要应用场景
TTFT 是你的硬性限制(例如聊天机器人 UX)标准服务部署可能更合适
长提示词下的高并发解耦——预填充干扰在此最严重
短提示词下的低请求速率标准服务足够使用

如何设置

现在你已经看到了结果,以下是如何进行部署。你需要配置三个组件:预填充实例、解码实例和代理服务器。有关完整的 vLLM 解耦预填充文档,请参见 [2]

预填充实例

预填充实例充当 KV 生产者(kv_role: kv_producer)。它处理输入提示词,计算 KV 缓存,并使其可供解码实例通过 RDMA 读取。

vllm serve <model> \
  ...
  --gpu_memory_utilization 0.9 \
  --kv-transfer-config '{
    "kv_connector": "MoRIIOConnector",
    "kv_role": "kv_producer",
    "kv_connector_extra_config": {
      "proxy_ip": "127.0.0.1",
      "proxy_ping_port": "36367",
      "http_port": "20005",
      "handshake_port": "6301",
      "notify_port": "6105"
    }
  }'

启动时,实例通过 ZMQ 向代理注册自己,发送其角色、HTTP 地址、握手和通知端口以及并行配置。它会持续发送定期注册消息,以便代理检测到不可用状态。

解码实例

解码实例充当 KV 消费者(kv_role: kv_consumer)。它在预填充完成后从代理接收请求,然后通过 RDMA 拉取 KV 缓存。

vllm serve <model> \
  ...
  --gpu_memory_utilization 0.9 \
  --kv-transfer-config '{
    "kv_connector": "MoRIIOConnector",
    "kv_role": "kv_consumer",
    "kv_connector_extra_config": {
      "proxy_ip": "127.0.0.1",
      "proxy_ping_port": "36367",
      "http_port": "40005",
      "handshake_port": "7301",
      "notify_port": "7501"
    }
  }'

代理服务器

代理是一个轻量级 HTTP 服务器,编排两阶段流程。它通过 ZMQ 在 proxy_ping_port 上监听实例注册,并使用轮询调度路由每个请求。

python examples/online_serving/disaggregated_serving/moriio_toy_proxy_server.py

在读取模式下,代理等待预填充实例完成,从响应中提取 remote_block_ids,并将其传递给解码实例,以便它明确知道要拉取哪些 KV 块。

端口参考

每个实例使用多个端口进行不同的通信通道,见表 5。秩(rank)偏移应用于 MoRIIOConfig(见 moriio_common.py)。

端口用途
proxy_ping_portZMQ 端点,用于各实例向代理注册
http_portvLLM HTTP 服务器端口;代理将推理请求转发至此
handshake_port一次性元数据交换:消费者获取生产者的 KV 缓存布局
notify_port单请求同步:预填充在 KV 块就绪时向解码发出信号

实验详情

设置

环境可以通过提供的 Dockerfile 构建来复现—— Dockerfile.rocm_base(使用来自 ROCm/mori 的 MORI 提交 2d02c6a9)和 Dockerfile.rocm(使用来自 vllm-project/vllm 的 vLLM 主分支)。

硬件

  • GPU:8× AMD Instinct MI300X (gfx942)
  • CPU:2× AMD EPYC 9654 96-核处理器

软件栈

  • ROCm 驱动:6.10.5 (AMDGPU)
  • 容器:rocm/vllm-dev (ROCm 7.0.51831-a3e329ad8)
  • vLLM:0.16.0rc1.dev1+gc46b0cd0a (git sha: c46b0cd0a)
  • PyTorch:2.9.1+git8907517 (ROCm 7.0.51831-a3e329ad8)
  • MORI 库:提交 c365eaed

基准测试配置

  • 模型:Qwen/Qwen3-235B-A22B-FP8
  • 输入序列长度:2000 tokens
  • 输出序列长度:1000 tokens
  • 数据集:随机
  • 工作负载:共 100 个请求
  • 请求速率:0.5 至 10(步长 0.5)

基准配置

本博客比较的四种配置描述于表 6。

配置描述
标准 (1× TP8)单 vLLM 实例使用所有 8× MI300X GPU (TP=8),具备专家并行能力。在单一引擎上处理混合的预填充和解码负载。
标准 (2× TP4)两个相同的 vLLM 实例,每个使用 4× MI300X GPU (TP=4),具备专家并行能力。代理使用轮询方式均衡分发请求。两个实例都处理混合的预填充和解码负载。
MORI-IO 读取 (1P+1D)一个预填充实例 (GPU 0–3) 和一个解码实例 (GPU 4–7),每个 TP=4,具备专家并行能力。两者均配置 VLLM_MORIIO_CONNECTOR_READ_MODE=1。代理串行分发:等待预填充返回 remote_block_ids,然后转发至解码。解码通过 RDMA 拉取 KV 缓存。前缀缓存已禁用。
MORI-IO 写入 (1P+1D)一个预填充实例 (GPU 0–3) 和一个解码实例 (GPU 4–7),每个 TP=4,具备专家并行能力。KV 缓存通过 MORI-IO 写入模式传输。状态感知型代理编排两阶段路由。按 MORI-IO 连接器要求,已禁用前缀缓存。

为什么选择这个基准? 标准 (2× TP4) 和解耦配置都使用了相同的总 GPU 数量 (8× MI300X),拆分为两个 4-GPU 组,确保了公平的比较。唯一的区别在于每组是运行混合的预填充+解码负载(标准)还是专用的预填充或解码负载(解耦)。标准 (1× TP8) 作为利用单引擎使用所有 8 个 GPU 的附加参考点。

通用性说明: 这些结果使用了混合专家模型 (MoE, Qwen3-235B-A22B-FP8)。预填充/解码干扰模式是 Transformer 推理的基础,也适用于稠密模型。MoE 模型倾向于放大该效应,因为专家路由增加了每步计算的可变性,使得 ITL 抖动更为明显。


结论与未来展望

本文演示了 PD 解耦不仅是数据中心规模的技术,它在单台 8-GPU 节点上也能带来显著收益。通过将 GPU 专用于每个阶段并使用 MORI-IO 进行高效的基于 RDMA 的 KV 缓存传输,我们实现了 2.5 倍的有效吞吐量提升,并消除了困扰同址部署的 ITL 违规问题。

后续计划

  • 多节点部署: 在生产环境中,预填充和解码实例可以跨越多个节点——MORI-IO 已经使用跨网络架构的 RDMA,因此相同的连接器可以在无需更改代码的情况下跨主机工作。
  • 分阶段调优: 使用专用实例,预填充实例可以配置为高计算吞吐量(更大的 token 预算、分块预填充),而解码实例可以针对低延迟(更小的批次大小、更严格的调度)进行调优。这种独立的旋钮调节在同址部署中是不可能实现的。

附录:可复现配置

若要复现这些结果,可从 rocm/vllm-dev 获取预构建的 nightly 镜像,或使用 vLLM 仓库中的 Dockerfile.rocm_baseDockerfile.rocm 从源码构建(MORI 提交 2d02c6a9,vLLM 提交 c46b0cd0a)。

下方提供了所有基准测试的完整 vLLM 命令行配置。每个命令都包含针对 AMD Instinct MI300X GPU 上 Qwen3-235B-A22B-FP8 模型的环境变量、并行标志和部署参数。

标准服务部署

# Instance 1 (GPU 0-3)
CUDA_VISIBLE_DEVICES=0,1,2,3 VLLM_ROCM_USE_AITER=1 vllm serve Qwen/Qwen3-235B-A22B-FP8 \
  -tp 4 \
  --enable-expert-parallel \
  --max-model-len 16384 \
  --max-num-batched-tokens 8192 \
  --distributed-executor-backend mp \
  --no-enable-prefix-caching \
  --port 8100
 
# Instance 2 (GPU 4-7)
CUDA_VISIBLE_DEVICES=4,5,6,7 VLLM_ROCM_USE_AITER=1 vllm serve Qwen/Qwen3-235B-A22B-FP8 \
  -tp 4 \
  --enable-expert-parallel \
  --max-model-len 16384 \
  --max-num-batched-tokens 8192 \
  --distributed-executor-backend mp \
  --no-enable-prefix-caching \
  --port 8200
 
# Proxy
cd <path_to>/vllm
python benchmarks/disagg_benchmarks/round_robin_proxy.py

解耦服务部署

# Prefill instance (GPU 0-3)
export VLLM_MORIIO_CONNECTOR_READ_MODE=1    # unset for write mode
export VLLM_ROCM_USE_AITER=1
export CUDA_VISIBLE_DEVICES=0,1,2,3
export HIP_VISIBLE_DEVICES=0,1,2,3
export MORI_DISABLE_AUTO_XGMI=1
export MORI_IO_ENABLE_NOTIFICATION=0
 
vllm serve Qwen/Qwen3-235B-A22B-FP8 \
  -tp 4 \
  --enable-expert-parallel \
  --port 20005 \
  --max-num-batched-tokens 4096 \
  --distributed-executor-backend mp \
  --gpu_memory_utilization 0.9 \
  --max-model-len 16384 \
  --max_num_seqs 64 \
  --no-enable-prefix-caching \
  --kv-transfer-config '{
    "kv_connector": "MoRIIOConnector",
    "kv_role": "kv_producer",
    "kv_connector_extra_config": {
      "proxy_ip": "127.0.0.1",
      "proxy_ping_port": "36367",
      "http_port": "20005",
      "handshake_port": "6301",
      "notify_port": "6105"
    }
  }'
 
# Decode instance (GPU 4-7)
export VLLM_MORIIO_CONNECTOR_READ_MODE=1    # unset for write mode
export VLLM_ROCM_USE_AITER=1
export CUDA_VISIBLE_DEVICES=4,5,6,7
export HIP_VISIBLE_DEVICES=4,5,6,7
export MORI_DISABLE_AUTO_XGMI=1
export MORI_IO_ENABLE_NOTIFICATION=0
 
vllm serve Qwen/Qwen3-235B-A22B-FP8 \
  -tp 4 \
  --enable-expert-parallel \
  --port 40005 \
  --no-enable-prefix-caching \
  --max-num-batched-tokens 4096 \
  --distributed-executor-backend mp \
  --gpu_memory_utilization 0.9 \
  --max-model-len 16384 \
  --max_num_seqs 64 \
  --kv-transfer-config '{
    "kv_connector": "MoRIIOConnector",
    "kv_role": "kv_consumer",
    "kv_connector_extra_config": {
      "proxy_ip": "127.0.0.1",
      "http_port": "40005",
      "proxy_ping_port": "36367",
      "handshake_port": "7301",
      "notify_port": "7501"
    }
  }'
 
# Proxy
cd <path_to>/vllm
python examples/online_serving/disaggregated_serving/moriio_toy_proxy_server.py

致谢

我们要感谢为此次合作做出贡献的许多杰出人才:

AMD: Hongxia Yang, Gilbert Lei, Mingzhi Liu, Niko Ma, Tian Di, Randall Smith, Feiyue Zhai, Peng Sun 和 MORI 团队。

Embedded LLM: Pin Siang Tan, Jun Kang Chow, Ye Hur Cheong, Vensen Mu, Jeff Aw, Tun Jian Tan 和 Embedded LLM 团队。

参考文献

  1. AMD 和 Embedded LLM,“vLLM MoE 剧本:TP、DP、PP 和专家并行的实用指南” https://rocm.blogs.amd.com/software-tools-optimization/vllm-moe-guide/README.html
  2. vLLM 解耦预填充文档 https://docs.vllm.com.cn/en/latest/features/disagg_prefill/
  3. DistServe:最大化 LLM 服务中的有效吞吐量 https://haoailab.com/blogs/distserve/
  4. MORI-IO 连接器 PR #29304 https://github.com/vllm-project/vllm/pull/29304
  5. MORI(模块化 RDMA 接口) https://github.com/ROCm/mori

免责声明

测试于 2026 年 3 月 12 日,在 AMD Instinct MI300X 平台上测量推理有效吞吐量。

硬件配置

  • MI300X:AMD EPYC 9654 96-核处理器服务器,配备 8× AMD Instinct MI300X (192GB, 750W) GPU,NPS1 (每个插槽 1 个 NUMA),2.2TiB (24 DIMMs, 4800 MT/s 内存, 96 GiB/DIMM)

软件配置

Ubuntu 22.04 LTS,Linux 内核 5.15.0-153-generic,ROCm 驱动 6.10.5 (AMDGPU),ROCm 7.0.51831-a3e329ad8,PyTorch 2.9.1+git8907517,vLLM 0.16.0rc1.dev1+gc46b0cd0a,MORI 库提交 c365eaed

服务器制造商的配置可能有所不同,从而导致不同结果。性能可能因配置、软件、vLLM 版本以及是否使用最新驱动程序和优化而异。