Kubernetes、Ray、vLLM 都在调度,它们各自决定了什么?

0 阅读14分钟

Kubernetes、Ray、vLLM 都在调度,它们各自决定了什么?

一台服务器的 GPU 还有空闲,模型服务却在排队。负责容器的人说资源已经给了,负责 Python 运行时的人说任务已经调度,负责推理的人说这一轮还放不进更多请求。三个人可能都没有说错。

问题在于,“已经调度”后面省略了对象。Pod 被分配到节点、Actor 获准运行、请求获得本轮执行机会,是三件不同的事。把它们画成一条从 Kubernetes 通往 GPU 的流水线,会让人误以为上层一旦放行,下层就必然获得足够容量。

理解推理基础设施,可以从一个具体动作开始:把“资源已分配”改写成“哪个系统把什么资源分给了哪个对象”。下面用 Kubernetes、Ray 和 vLLM 的公开机制追踪这件事,最后形成一份能用于排障交接的对象对应记录。场景与记录是教学示例,没有对应的本地 GPU 压测。

Pod 获得 GPU 配额,模型还没有获得执行计划

假设团队已有 Kubernetes,用普通整卡资源部署模型服务;不讨论 MIG、时间切片或其他 GPU 共享机制。Pod 中的容器声明需要两张 NVIDIA GPU,常见资源名是 nvidia.com/gpu。这里的整数首先表达设备资源需求,并没有携带“把 Transformer 分成两份”的意思。

Kubernetes 的调度器为待调度 Pod 寻找满足资源和其他约束的节点,再完成绑定。GPU 驱动、设备插件及容器运行时负责相应的设备暴露链路。官方 GPU 调度说明将设备资源声明放在容器资源配置中,而不是模型并行配置中。GPU 调度调度器职责分别说明了这两个边界。

因此,Pod 的两张卡需求得到满足,只能支持继续启动应用。它没有指定哪个进程加载哪部分权重,也没有保证应用会使用全部可见设备。应用如果只创建单卡执行实例,另外一张卡不会因为资源声明是二而自动承担另一半模型。

这个反例的价值不在于提醒人“还要配参数”,而在于改变排查方向:当设备已正确暴露、应用却只使用一张卡时,应核对进程与模型执行配置;继续增加 Pod 的 GPU 数量,可能只会让更多资源无法被其他工作负载申请。反过来,Pod 尚未找到可行节点时,修改推理批量参数也不能满足节点亲和性或设备资源约束。

一次排障交接至少要带上 Pod、Node、容器内可见设备及应用启动参数。仅转发一张宿主机 GPU 利用率截图,无法证明这些对象已经对应起来。

图01 图02

Ray 的资源申请决定谁能运行,不替进程兑现用量

如果应用使用 Ray,设备进入容器后,还会被运行时用于任务与 Actor 的资源调度。Actor 可以理解为常驻的有状态工作进程,远程方法在它持有的状态上执行;这里不必先把它当成一个模型副本。

Ray 官方文档给出一个很直观的例子:一台物理上有八个 CPU 的机器,可以用 --num-cpus=0 启动 Ray 节点,让需要 CPU 逻辑资源的任务不被安排到这里。物理硬件没有消失,改变的是运行时准入条件。Ray 逻辑资源说明

这也解释了为什么不能把 Ray 面板里的资源数字直接当作硬件利用率。申请一个 CPU 的任务,并不会因此被硬性限制为只使用一个物理核。GPU 方面,Ray 通过设置 CUDA_VISIBLE_DEVICES 控制常见框架看到的设备;这仍不等于替模型设置显存预算、划分权重或执行张量并行。同一官方说明

于是,“Actor 创建成功,但模型加载时显存不足”并不构成逻辑矛盾。运行时批准的是它声明的资源需求,模型加载检查面对的是权重和实际运行时开销。若需求表达不充分或模型配置不合适,错误会出现在后面的环节。重新提交同一个 Actor 不能凭空补足显存。

另一个容易混淆的地方是设备编号。容器、Ray 进程和宿主机看到的序号可能不同,进程中的设备 0 不应直接等同于宿主机列表里的第一张卡。需要把进程的可见设备与实际设备身份对应起来,再判断有没有重复占用或遗漏使用。这是诊断建议,不是要求修改逻辑资源数字来绕过真实硬件不足。

图03

vLLM 调度请求时,GPU 进程可以早已存在

进入推理引擎后,“调度”又换了对象。vLLM v0.28.0 的 V1 架构说明把 API Server、Engine Core 和 GPU Worker 分开:API 进程接收输入并返回结果;Engine Core 运行调度器、管理 KV Cache、协调模型执行;GPU Worker 加载权重并执行前向计算。vLLM 架构说明

KV Cache 保存生成过程会继续使用的注意力键和值。它会随着活跃序列和长度产生容量需求,所以“进程已启动”与“这个请求现在能执行”之间,仍有请求调度与内存管理。

前面的 Pod 和运行时调度,主要解决工作进程在哪里、是否允许启动;引擎内部则要持续决定哪些工作进入执行。正常生成过程中,不需要每输出一个 token 就让 Kubernetes 重新选择节点,也不需要为每个 token 重新创建一个 Ray Actor。请求排队而相关 Worker 一直存活,完全可能是正常的引擎侧容量约束,不能据此断言集群调度器卡住了。

并行策略参与的是模型执行布局。Tensor Parallel 将层内计算分摊到多个设备,Pipeline Parallel 将模型阶段分开;这些配置需要相应引擎和模型实现支持。vLLM 的并行部署文档说明了模型切分与执行后端的组合方式,不能从 Pod 或 Actor 数量直接反推模型副本数。vLLM 并行部署

NCCL 则提供 GPU 间的集合通信及点对点通信操作。它让参与计算的设备交换数据,不接管 HTTP 请求,也不替集群决定启动多少服务副本。NCCL 概览

由此可以判断两类表面相似的等待:Worker 还没成功建立通信组时,请求等不到完整执行环境;通信已经建立而请求没有进入本轮时,应继续查入口排队、引擎预算和实际负载。两者都表现为客户端没有收到 token,但需要的证据不同。本文只确定应追踪哪个对象,不承诺仅凭一个指标就能定位根因。

图04

先把排队位置写出来,再决定缺少哪一层

“服务有队列”仍然不够具体。请求可能尚未进入推理引擎,停在网关或服务路由层。如果采用 Ray Serve,HTTP Proxy、请求路由与模型 Deployment Replica 之间还存在应用服务关系;这些角色与 Ray Core 的 Task、Actor 资源调度不能混为一谈。Ray Serve 架构

对一条慢请求,先确定它已经到达哪个入口、被路由给哪个服务实例,再关联该实例的 Engine 和 Worker。随后才有可能把请求等待与 Pod、运行时资源、模型执行放进同一条因果链。不同实现未必自动贯通全部 ID,需要应用日志或追踪补充对应关系。

拿一条慢请求,把三层对象真正连起来

下面沿一条请求走完定位过程。它是作者设计的教学案例,部署关系与事件均为说明性假设,不是 Kubernetes、Ray 或 vLLM 默认提供的一条完整 trace,也不是生产测试记录。

假设服务部署了两个可提供相同模型 revision 的副本 A、B,每个副本有一个 vLLM Engine。GPU Worker 由本例采用的 Ray 执行方式管理,运行在 Kubernetes Pod 内;这些进程在用户发请求之前已经启动。不要把这个部署图套给不使用 Ray 的 vLLM,也不要把 Engine Core 本身直接等同于一个 Ray Actor。

用户请求 req-101 提交后迟迟没有首 token,运维面板同时显示部分 GPU 负载不高。排障的第一步不是扩 Pod,而是找到这条请求实际选择了谁。假设应用记录显示,路由层因本例配置的会话粘滞规则选择了副本 A。这个规则是案例条件,不是宣称 vLLM 的前缀缓存自动强制所有会话去固定副本。

先把身份关系记录下来:

本例对象对应身份信息应从哪里得到这一层回答什么
用户请求req-101,目标模型revision m1应用入口与路由记录用户想调用谁,实际被分给谁
服务副本与引擎replica-A / engine-A部署配置及副本启动记录哪个引擎拥有这条请求的调度状态
GPU执行进程worker-A0、worker-A1;本例由两个Ray Actor承载执行器、Actor及Worker启动记录执行工作被交给哪几个常驻进程
容器与GPUpod-A0、pod-A1的UID及进程可见GPUKubernetes对象、容器和运行时记录这些进程实际落在哪些资源上

表中的名称都是教学别名。真实记录要保留进程和部署的生命周期身份,不能把重启后复用的副本名当成同一个进程。上表也不意味着各层ID天然相同;缺少映射时应补启动关联记录,而不是按时间接近就认定两个事件属于同一条请求。

接着核对时间顺序。假设本例在请求到达之前已经观察到相关Pod就绪、Ray Actor成功启动、模型Worker完成初始化。请求到达之后,没有新增Actor安排请求,也没有相关Pod重启。这样可以缩小当前问题的范围:这条请求不是正在等“为它创建一个新Pod”。但仍不能用历史就绪记录证明所有执行过程都正常,后面还要看当前引擎和Worker事件。

现在到 engine-A 的记录中寻找 req-101。假设采集显示它已被引擎接收,但尚未进入模型执行计划;同一时刻,活跃请求持有的状态导致本次KV分配检查没有通过。这个条件对应固定 v0.28.0 Scheduler 源码中等待请求分配失败、未进入当前计划的分支;本例取普通同步调度,不涉及异步在途状态、推测解码或外部KV传输。这里的逐请求调度原因需要对应版本的日志、tracing或额外观测支持,不能把一个聚合队列数字直接翻译为“req-101因为KV不足”。如果没有这条关联证据,结论只能先写为等待原因待查。

在这些案例条件成立时,优先方向已经明确:请求位于引擎侧等待,单纯看到节点有空CPU、Ray没有待安排Actor或GPU利用率不高,都不能解释它为什么没拿到状态。Kubernetes完成了Pod资源放置,Ray已经安排好了执行进程;当前尚未完成的是引擎内这条请求的接纳或调度。给同一个Pod增加CPU申请,不会自动为既定GPU上的KV池增加空间。

还要追问一次:副本B既然空闲,为什么不帮忙?查看同一请求的路由结果后,答案可能只是本例的粘滞规则一直选择A。引擎A不会仅凭集群里另一张卡空闲,就把一条有状态生成请求任意搬到引擎B。B是否能接收新请求,仍需确认模型revision、接口、容量与业务会话约束;本例没有跨引擎迁移机制;请求已经产生输出后的迁移则是另一个更复杂的问题,不能把重试写成透明迁移。

下一轮实验可以保持两个引擎的运行参数不变,只调整本例允许的新请求路由条件,再重放同一到达轨迹。比较A、B各自的接收量、引擎等待、失败和用户端时限达标率。如果等待只是从A移到B,总体仍超载,就没有获得新容量;如果改变路由破坏了必需的会话或业务状态约束,延迟下降也不能视为合格结果。这个实验是为检验路由假设,不是给所有服务推荐取消粘滞。

如果证据换了,定位也要跟着换

同样一条请求,如果应用记录显示它尚未拿到任何可用副本,而Ray中对应副本的资源组仍待安排,就检查资源组的 bundle 与放置策略是否可行:每个 bundle 必须在一个节点上放得下,不代表整个 placement group 都必须位于同一节点;还要核对集群可用资源。Ray placement group 官方说明。若相关Worker Pod还没有被调度到节点,再检查Pod请求与节点条件。此时继续调整vLLM单轮token预算没有击中等待位置。

另一种情况是req-101已经进入模型执行计划,但对应Worker的执行或通信迟迟没有完成。那就保留这次计划和Worker关联,追当前GPU执行、通信或进程状态;不能再用“引擎有队列”作为唯一解释。若服务端已经输出而客户端迟迟收不到,还应继续查后续传输与应用缓冲。

这些分支都不要求把所有日志堆在一起。真正有用的是保存最少而足够的证据:请求实际去了哪个副本,该副本对应哪些执行进程,这些进程在哪些Pod和GPU上,以及等待发生在接收前、执行计划前、执行期间还是输出之后。不同机器的时间戳需要校准或注明误差;延迟优先在各自时钟域内计算,再用身份关系连接。

这样,文章开头的“GPU有空闲、请求仍排队”就不再只是职责定义。它会落成一条有条件的定位结论:在本例已取得的证据下,应先处理路由或引擎侧状态竞争,而不是把三层都扩一遍。若下一条证据推翻了这一假设,就沿对应对象回到正确层级,而不是继续维护一个凭仪表盘猜出来的答案。

按缺少的具体职责决定组件

现在再看技术选型,会少掉许多伪问题。团队只是需要容器发布和设备配额时,可以由 Kubernetes 直接部署 vLLM;已有单机固定服务,也可以先用简单的进程管理方案。如果应用确实需要分布式 Task、Actor 或 Serve 应用组合,再评估 Ray 对应的能力和维护成本。是否采用某个执行后端,还要服从所选引擎版本和部署拓扑的支持范围。

这些不是必须逐级购买的能力。删除 Ray 后,进程组织职责仍要由其他启动方式承担;删除 Kubernetes 后,机器、设备和进程生命周期也不会自己得到管理。判断组件是否必要,要看删掉它以后哪项具体职责无人承担,而不是它在架构图中属于第几层。

因此,下次听到“GPU 已经调度好了”,可以先补全一句话:哪个对象获准使用了哪些设备,现在等待的是创建进程、初始化模型,还是执行请求?这份对应关系建立起来以后,才知道应该调整资源声明、运行时申请、模型布局,还是请求容量。

资料于 2026-09-08 初核,新增案例与来源于 2026-09-12 复核;vLLM 引用固定为 v0.28.0,Ray 页面当日显示 2.58.0。这里讨论公开机制及作者的排障推导,不构成特定组件组合的兼容性验证。 图05 图06