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。
本地练习时,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()
真正提交执行。
这不会立刻返回结果,而是返回一个 ObjectRef。
ObjectRef
Ray 的 future-like 对象。
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 必须分散到不同节点
|
PACK 和 STRICT_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 如果随便调度,只知道:
但 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)
|
对于:
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.
|
这也是 RayExecutorV2 和 CoreEngineActorManager 的区别:
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 这一套内部结构里。