vLLM Code Reading (Section 3): Ray Core for vLLM

Ray 在 vLLM 里的定位更接近分布式控制面,而不是推理数据面。它负责把一组长期运行的 Python actor 放到合适的节点和 GPU 上,并管理这些 actor 的生命周期。

在 vLLM 的服务链路里,Ray 的边界可以先概括成:

1
2
3
4
5
6
7
Ray in vLLM:
负责启动 actor、放置 GPU rank、管理跨节点资源和生命周期

not Ray in vLLM:
不负责每个 token 的高频调度
不负责 TP/PP 的核心通信
不替代 EngineCore scheduler

几个 Ray 概念和 vLLM 组件的对应关系如下:

1
2
3
4
5
6
ray.remote             -> 把 RayWorkerProc / EngineCoreActor 变成 actor
.options(...) -> 给 Ray scheduler 的资源和放置约束
.remote(...) -> 真正创建 actor,参数传给 __init__
Placement Group -> 固定 rank 到 GPU / node 的布局
runtime_env -> 给 Ray worker 注入运行环境
ObjectRef / ray.get -> 等待 actor 初始化和远程方法返回

1. Ray 是什么

Ray 是一个分布式 Python runtime,用来把 Python 函数、类、任务调度到本机或多机集群上执行。

核心能力:

1
2
3
4
5
并行执行函数
启动有状态 Actor
跨进程/跨机器传递对象
按 CPU/GPU 资源调度任务
用 Placement Group 控制资源布局

在 vLLM 里,Ray 主要用于:

1
2
3
启动多机 GPU worker
管理 actor 生命周期
控制 rank 到 GPU/节点的放置

Ray 不是 vLLM 高频推理通信的核心数据面。vLLM 高频通信主要还是:

1
2
3
4
ZMQ
MessageQueue
torch.distributed
NCCL

2. Ray 的基础对象

ray.init()

启动或连接 Ray runtime。

1
2
3
import ray

ray.init()

本地练习时,ray.init() 会自动启动一个本地 Ray runtime。

@ray.remote

把普通函数或类变成 Ray 可调度对象。

函数:

1
2
3
@ray.remote
def f(x):
return x * x

类:

1
2
3
4
@ray.remote
class Worker:
def ping(self):
return "pong"

也可以不用装饰器,动态写:

1
RemoteWorker = ray.remote(Worker)

vLLM 常用这种动态写法,因为它要动态配置资源、placement group、runtime env。

.remote()

真正提交执行。

1
ref = f.remote(3)

这不会立刻返回结果,而是返回一个 ObjectRef

ObjectRef

Ray 的 future-like 对象。

1
ref = f.remote(3)

ref 代表一个未来结果。

取结果:

1
result = ray.get(ref)

批量取:

1
results = ray.get([ref1, ref2, ref3])

ray.wait()

等待一部分任务先完成。

1
ready, remaining = ray.wait(refs, num_returns=1)

适合:

1
2
谁先完成先处理谁
监控 actor 是否异常退出

vLLM 里监控 Ray worker actor 健康时会用类似模式。

3. Ray Task:无状态函数并行

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import ray

ray.init(num_cpus=4)

@ray.remote
def work(x):
time.sleep(1)
return x * x

refs = [work.remote(i) for i in range(8)]
print(ray.get(refs))

ray.shutdown()

含义:

1
2
work.remote(i) 提交任务
ray.get(refs) 等所有任务结束

如果有 4 个 CPU,8 个任务大约分两轮跑完。

4. Ray Actor:有状态远程对象

Actor 是常驻对象,有自己的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ray

ray.init()

@ray.remote
class Counter:
def __init__(self):
self.x = 0

def inc(self):
self.x += 1
return self.x

counter = Counter.remote()

print(ray.get(counter.inc.remote()))
print(ray.get(counter.inc.remote()))

ray.shutdown()

Actor 的特点:

1
2
3
4
有状态
常驻
方法调用异步
适合 long-running worker

vLLM 的这些对象都可以按 Actor 理解:

1
2
3
RayWorkerProc
EngineCoreActor
DPMoEEngineCoreActor

5. ray.remote(...).options(...).remote(...) 是什么

vLLM 里常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
actor = (
ray.remote(RayWorkerProc)
.options(
name=actor_name,
num_cpus=0,
num_gpus=1,
scheduling_strategy=scheduling_strategy,
runtime_env=runtime_env,
)
.remote(
vllm_config=vllm_config,
rank=rank,
)
)

可以拆成三步:

1
2
3
4
5
6
7
8
9
10
11
RemoteClass = ray.remote(RayWorkerProc)

ConfiguredClass = RemoteClass.options(
num_gpus=1,
scheduling_strategy=scheduling_strategy,
)

actor = ConfiguredClass.remote(
vllm_config=vllm_config,
rank=rank,
)

区别:

1
2
3
4
5
6
7
8
9
10
ray.remote(Class)
把普通 class 变成 Ray actor class

.options(...)
设置 Ray 调度参数
比如 CPU/GPU、placement group、runtime_env、actor name

.remote(...)
真正创建 actor
参数传给 Class.__init__

所以:

1
2
.options(...) 是给 Ray scheduler 的
.remote(...) 是给你的 Python class 构造函数的

6. Placement Group 是什么

Placement Group 是 Ray 的资源预留机制。

普通调度:

1
2
我需要 1 个 GPU
Ray 随便找一张空 GPU 放我

Placement Group:

1
2
3
4
5
6
7
8
我需要一组资源:
bundle 0: 1 GPU
bundle 1: 1 GPU
bundle 2: 1 GPU
bundle 3: 1 GPU

Ray 先整体预留这组资源
然后我可以指定 actor 放到哪个 bundle

7. Bundle 是什么

一个 placement group 由多个 bundle 组成:

1
2
3
4
5
6
7
8
9
pg = placement_group(
bundles=[
{"GPU": 1},
{"GPU": 1},
{"GPU": 1},
{"GPU": 1},
],
strategy="PACK",
)

可以理解为:

1
2
3
4
5
pg
bundle 0: 1 GPU
bundle 1: 1 GPU
bundle 2: 1 GPU
bundle 3: 1 GPU

创建 actor 时指定 bundle:

1
2
3
4
5
6
Worker.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
placement_group_bundle_index=0,
)
).remote()

含义:

1
这个 Worker 必须放到 pg 的 bundle 0 上

8. Placement Group 的策略

常见策略:

1
2
3
4
5
6
7
8
9
10
11
PACK
尽量把 bundle 放近,比如同一个节点

STRICT_PACK
必须全部放到同一个节点,否则等待/失败

SPREAD
尽量分散到不同节点

STRICT_SPREAD
必须分散到不同节点

PACKSTRICT_PACK 的区别尤其容易误解。

PACK 是偏好,不是硬约束。Ray 会尽量把 placement group 里的 bundles 放到更少的节点上,比如优先塞到同一个节点;但如果一个节点放不下,Ray 可以把剩余 bundle 放到别的节点上,只要整个 placement group 的资源能被满足。

1
2
3
4
5
6
7
PACK:
node0 has 4 GPUs
request 6 bundles, each {"GPU": 1}

possible placement:
node0: bundle 0,1,2,3
node1: bundle 4,5

STRICT_PACK 是硬约束。所有 bundle 必须放到同一个节点上。如果没有任何单个节点能同时满足全部 bundle,placement group 就会一直等待,或者最终调度失败。

1
2
3
4
5
6
7
8
STRICT_PACK:
node0 has 4 GPUs
node1 has 4 GPUs
request 6 bundles, each {"GPU": 1}

result:
cannot place
because no single node has 6 GPUs

对 vLLM 来说,STRICT_PACK 适合“这些 ranks 必须在同一台机器里”的场景,例如小规模 TP 想强制落在同节点,避免跨节点 TP all-reduce。PACK 更像“尽量放近,但允许资源不够时跨节点”,适合希望提高 locality、但不想因为单节点资源不足而完全起不来的情况。

所以如果你希望:

1
8 个 TP ranks 必须在同一台 8 卡机器上

应该用 STRICT_PACK

如果你希望:

1
尽量把 ranks 放在一起,但可以跨节点补齐资源

PACK 更合理。

对 vLLM 来说:

1
2
3
TP 通信密集,通常希望同节点
PP 可以跨节点
EP / DeepEP 可能对 rank 拓扑更敏感

所以 placement group 很重要。

9. Placement Group 为什么对 vLLM 重要

vLLM 中每个 GPU rank 是一个 worker:

1
2
3
4
5
rank 0
rank 1
rank 2
...
rank 15

Ray 如果随便调度,只知道:

1
每个 actor 需要 1 张 GPU

但 vLLM 需要知道:

1
2
3
4
5
rank 0 放哪张卡
rank 1 放哪张卡
rank 0..7 是否在同一节点
rank 8..15 是否在另一节点
哪个 rank 是最后一个 PP stage 的 TP0

这些关系影响:

1
2
3
4
5
6
NCCL 初始化
TP group
PP group
模型 shard
输出 rank
通信拓扑

所以 vLLM 用 placement group 固定资源布局。

10. runtime_env

runtime_env 用来给 Ray actor 配置运行环境。

例如:

1
2
3
4
5
6
7
actor = Worker.options(
runtime_env={
"env_vars": {
"MY_ENV": "1",
}
}
).remote()

vLLM 会用它给 Ray worker 传环境变量,例如:

1
2
3
禁用 Ray 自动设置 CUDA_VISIBLE_DEVICES
传递 profiling / nsight 配置
传递必要的 vLLM 环境变量

11. Ray 在 vLLM 里的两条路径

Ray 在 vLLM 里主要出现在两条路径:

1
2
3
4
5
6
7
path A:
distributed_executor_backend = ray
Ray 用来启动 GPU worker actors

path B:
data_parallel_backend = ray
Ray 用来启动多个 EngineCore actors

这两条路径解决的是不同层级的进程编排问题。

distributed_executor_backend=ray 负责模型执行 worker 的放置:

1
模型并行的每个 GPU rank 放在哪个节点、哪张卡上

data_parallel_backend=ray 负责 EngineCore 副本的生命周期:

1
多个 data parallel EngineCore 的启动、管理和健康检查

Ray 不替代 vLLM 的 scheduler。即使 worker 是 Ray actor,token 级调度仍然发生在 EngineCore / Scheduler 里;TP/PP 通信也主要由 torch.distributed、NCCL 和 vLLM 自己的通信机制负责。

12. 路径一:Ray 启动 GPU Worker

这是多机 TP/PP 推理最常见的 Ray 路径:

1
2
3
4
vllm serve /path/to/model \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--distributed-executor-backend ray

这条路径里,API Server 和 EngineCore 的关系仍然和前两节一样:

1
2
3
4
API Server
-> AsyncLLM
-> AsyncMPClient
-> EngineCoreProc

变化发生在 executor 层。EngineCore 需要执行模型时,不再用本地 multiprocess worker,而是选择 Ray executor:

1
2
3
Executor.get_class
-> RayExecutorV2
-> ray.remote(RayWorkerProc)

对于:

1
2
TP = 8
PP = 2

world size 是:

1
world_size = TP * PP = 16

所以这条路径会创建 16 个 RayWorkerProc actor。每个 actor 对应一个模型执行 rank。它们不是 16 个 API Server,也不是 16 个 EngineCore;它们是执行层 worker。

核心代码形态是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
actor = (
ray.remote(RayWorkerProc)
.options(
name=actor_name,
num_cpus=0,
**resource_kwargs,
scheduling_strategy=scheduling_strategy,
runtime_env=runtime_env,
)
.remote(
vllm_config=self.vllm_config,
rank=bundle["rank"],
distributed_init_method=distributed_init_method,
input_shm_handle=scheduler_output_handle,
is_driver_worker=is_driver_worker,
is_driver_node=is_driver_node,
)
)

这段链式调用可以拆成三层:

1
2
3
4
5
6
7
8
ray.remote(RayWorkerProc)
把 RayWorkerProc 变成 Ray actor class

.options(...)
告诉 Ray scheduler:这个 actor 要什么资源,放在哪个 placement group bundle 上,用什么 runtime env

.remote(...)
真正创建 actor,把 vllm_config、rank、distributed_init_method 等参数传给 RayWorkerProc.__init__

这些参数对应 vLLM 的执行语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rank:
这个 actor 是全局第几个模型执行 rank

distributed_init_method:
后续初始化 torch.distributed / NCCL group 要用

scheduling_strategy:
把 rank 绑定到 placement group 的某个 bundle

runtime_env:
给 actor 注入 vLLM 需要的环境变量

is_driver_worker / is_driver_node:
标记这个 rank 是否承担 driver 侧职责

所以这条路径可以总结成:

1
2
Ray is used to place and launch model workers.
vLLM still owns scheduling and model execution logic.

13. 路径二:Ray 启动 EngineCore

第二条路径是 data_parallel_backend=ray。这时 Ray 不只是启动 GPU worker,还会启动 EngineCore 本身。

代码路径可以简化成:

1
2
3
4
5
launch_core_engines
-> CoreEngineActorManager
-> ray.remote(EngineCoreActor)
-> actor.wait_for_init.remote()
-> actor.run.remote()

这里的 EngineCoreActor 本质上仍然是 EngineCoreProc:

1
2
class EngineCoreActor(EngineCoreActorMixin, EngineCoreProc):
...

也就是说,Ray 管的是进程/actor 生命周期,但 EngineCore 的主逻辑没有变:

1
2
3
4
5
6
7
ZMQ input thread
-> input_queue
-> run_busy_loop
-> scheduler.schedule
-> executor.execute_model
-> output_queue
-> ZMQ output thread

这条路径适合理解成:

1
2
3
每个 DP rank 有自己的 EngineCore
Ray 负责把这些 EngineCore 作为 actors 拉起来
每个 EngineCore 内部仍然有 scheduler / executor / output loop

为什么要这么做?因为 data parallel 场景下,vLLM 不再只有一个 EngineCore 管所有请求。它可能需要多个 EngineCore 副本,每个副本管理一部分请求或一组执行资源。Ray 提供的价值是 actor 生命周期、跨节点调度、异常监控和资源放置。

这里也要避免一个误解:EngineCoreActor 不是 Ray 帮 vLLM 重写了 scheduler。Ray 只是把 EngineCore 这个 long-running component 放进 Ray actor 管理体系里。

14. Ray 负责什么,不负责什么

把两条路径放在一起看,Ray 在 vLLM 里负责的是 control plane。

Ray 负责:

1
2
3
4
5
6
7
启动 RayWorkerProc / EngineCoreActor
跨节点调度 actor
按 CPU/GPU 资源约束放置 actor
用 placement group 固定 rank 拓扑
传递 runtime_env
管理 actor 生命周期
监控 actor 是否异常退出

Ray 不主要负责:

1
2
3
4
5
6
决定下一个 token 算哪个 request
管理 KV cache block 分配
执行 TP all-reduce
执行 PP hidden state 传递
执行 CUDA kernel
承载每个 token 的高频数据面

这些分别由 vLLM 里的其他层负责:

工作 主要负责者
请求调度 Scheduler
KV cache 分配 KVCacheManager / block pool
模型 forward GPUModelRunner / worker
TP/PP 通信 torch.distributed / NCCL
API Server 和 EngineCore IPC ZMQ + msgpack
worker 放置和生命周期 Ray

这个边界很重要。Ray 是一个很强的分布式 runtime,但在 vLLM 里它不是 token scheduler,也不是 NCCL 的替代品。它更多是在启动时和故障管理时发挥作用。

15. 从 vllm serve 到 Ray actors 的完整路径

最后把前面的内容合起来。

多机 TP/PP,且使用 Ray executor 时,主路径是:

flowchart TB
    API["API Server / FastAPI"]
    ALLM["AsyncLLM"]
    MP["AsyncMPClient"]
    ECP["EngineCoreProc"]
    SCH["Scheduler"]
    EX["RayExecutorV2"]
    PG["Placement Group"]
    RW0["RayWorkerProc rank 0"]
    RW1["RayWorkerProc rank 1"]
    RWN["RayWorkerProc rank N"]
    MR["GPUModelRunner"]

    API --> ALLM --> MP --> ECP --> SCH --> EX
    EX --> PG
    PG --> RW0
    PG --> RW1
    PG --> RWN
    RW0 --> MR
    RW1 --> MR
    RWN --> MR

这条图里,Ray 参与的是:

1
2
3
RayExecutorV2
-> placement group
-> RayWorkerProc actors

EngineCore 仍然是调度中心。RayWorkerProc 是被 Ray 放置和管理的执行 rank。

如果使用 Ray 管理 data parallel EngineCore,结构变成:

flowchart TB
    API["API Server"]
    MGR["CoreEngineActorManager"]
    EC0["EngineCoreActor DP rank 0"]
    EC1["EngineCoreActor DP rank 1"]
    ECN["EngineCoreActor DP rank N"]
    S0["Scheduler / Executor"]
    S1["Scheduler / Executor"]
    SN["Scheduler / Executor"]

    API --> MGR
    MGR --> EC0 --> S0
    MGR --> EC1 --> S1
    MGR --> ECN --> SN

这条图里,Ray 参与的是:

1
2
CoreEngineActorManager
-> EngineCoreActor per DP rank

每个 EngineCoreActor 内部仍然是 vLLM 自己的 scheduler / executor 逻辑。

最终边界可以概括为:

1
2
Ray gives vLLM distributed process placement.
vLLM keeps request scheduling, KV management, and model execution semantics.

这也是 RayExecutorV2CoreEngineActorManager 的区别:

1
2
3
4
5
RayExecutorV2:
uses Ray to place model execution ranks

CoreEngineActorManager:
uses Ray to manage EngineCore replicas

两者都依赖 Ray actor,但控制的对象不同:前者控制 worker rank,后者控制 EngineCore。vLLM 的调度语义仍然留在 EngineCore、Scheduler、Executor 和 Worker 这一套内部结构里。


vLLM Code Reading (Section 3): Ray Core for vLLM
https://jeremyguo.space/2026/06/24/vllm-code-reading-section-3-ray-core-for-vllm/
作者
郭俊毅 / JeremyGuo
发布于
2026年6月24日
许可协议