☰
K8s、Ray、vLLM三种调度的分工与协作:LLM推理平台的完整调度链
2026/9/29 18:42:46 网站建设 项目流程

前两天帮朋友搭一套 GPU 推理平台,启动会上三套系统的负责人都在说“我们做了调度”——Kubernetes 管 Pod 调度,Ray 管分布式任务调度,vLLM 管请求调度。第一反应是三家的活重了,但真正跑起来才发现,这三兄弟根本不是同一个岗位,而是服务大厅里的三道窗口。你让前台去替厨师炒菜,或者让厨师去管客房,都会乱套。

这篇文章我想直接用一次部署大模型推理服务的视角,把 K8s、Ray、vLLM 各自的“调度”拆清楚:它们到底决定了什么,分别用什么粒度做事,以及问题出在哪一层时该看谁的日志。适合正在做 LLM 推理平台、分布式训练编排,或者被“GPU 看着没满但请求一直排队”折磨的同学。读完你至少能快速判断:一个 Pending 的 Pod、一个等待分配的任务、一个在 vLLM 队列里排队的请求,各自属于哪一层的责任范围。

1. 三个“调度”到底在争什么?——先把边界画清楚

1.1 调度是分层的,不是同一件事

很多人刚接触这套技术栈时会有一个直觉:Kubernetes 管集群资源,Ray 也管集群资源,vLLM 里面还套一个 scheduler,这不就是重复造轮子吗?实际上它们调度的对象完全不是一个东西。

Kubernetes 调度的决策单位是 Pod。它解决的是“这个 Pod 里的容器应该放到集群里哪台物理机或虚拟机上”,决策依据是节点剩余 CPU、内存、GPU 卡数、亲和性、污点容忍这类粗粒度资源。它根本不关心 Pod 里跑的是不是大模型,也不关心模型权重多大、显存还剩多少。

Ray 调度的决策单位是 task、actor 和 placement group。它解决的是“这个分布式计算任务应该运行在 Ray 集群里哪个 Worker 进程上”,决策依据是每个 Worker 上报的 CPU/GPU 余量,以及任务自己声明的资源需求。Ray 不知道什么是 Transformer、什么是 KV cache,它只认“你要几张卡、几核 CPU、多大内存”。

vLLM 的调度决策单位是请求和序列。它解决的是“这批 HTTP 请求里,哪些先进入模型计算,哪些等在队列里,哪些被抢占重算”,决策依据是 GPU 显存里的 KV cache 分页表、当前 running 队列长度、prefill 和 decode 的状态。这是最微观的一层,直接决定线上请求的吞吐和延迟。

用一个离谱一点的类比:K8s 是酒店前台,只负责把你带进房间;Ray 是楼层主管,负责给服务员派活;vLLM 是后厨厨师,负责决定同一时间先炒哪个菜。三层各干各的,但一个顾客从进门到吃上饭,三个环节都跑不脱。

调度器调度对象决策内容典型时间尺度
KubernetesPod / 容器组Pod 放到哪台 Node,资源账本是否够秒级
Raytask / actor / placement group分布式任务跑在哪个 Worker,GPU 怎么分组毫秒到秒级
vLLM请求序列 / KV cache page每步迭代先算哪些请求,是否抢占、是否交换几十毫秒一轮

1.2 为什么不能只用一套调度到底

要是只留 K8s,不用 vLLM 那层调度,最直接的问题是:每个请求都要起一个 Pod、加载一遍模型权重,然后才能推理。模型加载几十秒,请求根本等不起。就算用常驻 Pod,请求还是要在应用层排队,而应用层的队列并不知道 GPU 显存里 KV cache 还剩多少,排了队也调度不精准。

要是只留 Ray,没有 K8s,分布式任务能跑,但机器的节点生命周期、容器隔离、日志采集、网络插件这些平台层能力都要自己搭。Ray 的 autoscaler 能开云主机,但做不到 K8s 那种丰富的节点资源管理和故障恢复编排。

要是只留 vLLM,不装 K8s 和 Ray,单机几块卡以内的场景是够用的,卡一多、实例一多,模型副本分散到哪台机器、几个副本之间怎么负载均衡、故障了怎么重新拉起,全凭手工,根本维护不过来。

所以这不是“谁替代谁”的问题,而是“接力赛”的问题。调度粒度越细,反应越快,能做的优化越多;调度范围越大,越能宏观控制成本。K8s 管住机器层,Ray 管住分布式计算层,vLLM 管住请求级微调度,三个窗口环环相扣。

2. Kubernetes:只决定“Pod 住在哪台机器”

2.1 它对什么负责,对什么不负责

Kubernetes 默认调度器的工作流程其实是两段:先做过滤,再做打分。过滤阶段把资源不够、端口冲突、不满足亲和性约束的节点全部剔除;打分阶段在剩下合格的节点里按负载均衡、拓扑分布等规则排序,选出一个最优解。

这听起来很合理,但注意一个关键点:Kubernetes 调度器做的是“资源账本调度”。它只按你申报的 Requests 和 Limits 记账,不查你实际花了多少。

比如 Pod 里申请了nvidia.com/gpu: 1,调度器知道这张卡可以分配,但它不知道这张卡上已经加载的模型占了 12GB 显存,也不知道剩余显存能不能放下新模型。NVIDIA 设备插件默认只上报卡数,不上报显存细粒度。两个 Pod 都申请同一块 GPU 资源,可能被分到同一张物理卡上,其中一个因为显存不足直接 OOM,另一个也会被连累重启。

也就是说,K8s 层调度决定了“Pod 能否启动、启动在哪台机器”,但它不决定“模型是否真能在 GPU 上跑起来”。后者必须靠 Ray 的资源分组、vLLM 的显存管理,以及你配置文件里的资源申报习惯一起兜底。

提示:K8s 的调度本质是“资源账本调度”,它只按你申报的数字记账,不查你实际花多少。申报太省,节点超卖;申报太满,节点饿死。

2.2 用亲和性、污点和拓扑分布把“账本”做细

既然 K8s 只看申报,那我们能做的就是让申报更贴近真实。我常用的手段有这么几个。

第一是 nodeSelector 和节点亲和性。给 GPU 节点打上gpu=rtx4090或gpu=a100标签,然后在模型服务 Deployment 里指定 nodeSelector。这样不同型号的卡可以分开调度,避免出现“要跑大模型却被分到小显存卡”的尴尬。如果还想做软规则,用nodeAffinity的preferredDuringScheduling,表示能分到一起尽量分到一起,分不到也不阻塞。

第二是污点和容忍。在 GPU 节点上加 taint,比如nvidia.com/gpu=true:NoSchedule,普通业务 Pod 没有对应 toleration 就不会被调度上去。这个做法尤其适合 GPU 资源稀缺的集群,防止那些没申请 GPU 的 Pod 因为 CPU 便宜就偷偷跑到 GPU 机器上挤占资源。

第三是 Pod 反亲和和拓扑分布约束。推理服务通常要多个副本,如果所有副本都挤在同一台物理机上,一旦这台机器宕机,整个服务就全挂了。用podAntiAffinity让同一服务的多个副本尽量分散到不同节点;用topologySpreadConstraints让副本在机架、可用区间也尽量均匀。

第四是扩展资源上报。如果集群里用的是第三方显存设备插件,可以给每个节点上报nvidia.com/gpu-mem这类扩展资源。这样调度器就像多了一本显存账本,申请资源时写上nvidia.com/gpu-mem: 24Gi,调度器才会把显存需求纳入过滤条件。

下面是个简化的 Deployment 骨架,把“申请一张卡、副本尽量分散”表达出来:

apiVersion: apps/v1 kind: Deployment metadata: name: llm-inference spec: replicas:3 selector: matchLabels: app: llm-inference template: metadata: labels: app: llm-inference spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: llm-inference topologyKey: kubernetes.io/hostname containers: - name: vllm image: vllm/vllm-openai:v0.7.3 resources: requests: nvidia.com/gpu: "1" memory: "32Gi" limits: nvidia.com/gpu: "1" memory: "32Gi"

2.3 我踩过的 K8s 调度坑

先说我踩得最狠的一个:早期部署两个不同模型服务,各自申请 1 张 A100,两个 Pod 都被调度到了同一台物理机。从 K8s 眼里看,这台机器有 8 张卡,资源完全够;但两张卡都被之前残留的进程占了大半显存,新模型一加载就 OOM。后来我把 GPU 节点的资源申报改成整卡可分配,模型启动前用脚本检查显存,才把这类事故压下去。

第二个坑是 requests 和 limits 不一致。CPU 这类资源适度超卖没问题,但 GPU 千万不要超卖。我只见过 CPU 超卖换来一点利用率提升,没见过 GPU 超卖不闯祸的。想上 GPU 超卖,必须有完整的位运迁移和抢占机制,否则老老实实requests == limits。

第三个坑是 Pending 时间过长。排障顺序一般是kubectl describe pod <name>,看 Events 里的0/4 nodes available提示。看到这个提示别急着加机器,先用kubectl get nodes --show-labels确认标签、污点是否匹配,再看是不是反亲和规则把自己给堵死了。我遇到过明明资源充足、却有 100 分反亲和权重让两个副本永远无法共存的情况,最后是放宽 topologyKey 范围解决的。

3. Ray:决定“分布式任务跑在哪个 Worker 上”

3.1 Ray 的调度对象不再是“进程”

Ray 集群由 head 节点和若干 worker 节点构成,每个 worker 上报自己的 CPU、GPU、内存资源。当你用@ray.remote(num_gpus=1)装饰一个函数或类,Ray 调度器会在集群里找一个满足资源要求的节点,把任务或 actor 放过去。

和 K8s 相比,Ray 最大的不同是调度对象的生命周期。K8s 的 Pod 是长期运行的,Pod 起来了就占着资源;Ray 的 task 可能几秒钟就结束,actor 可以长期驻留,也可以通过 placement group 把一组 actor 固定在一个资源集合里。也就是说,Ray 做的是分布式计算层面的“动态编排”,同一个 Ray 集群里可以同时跑数据处理、模型训练、模型推理多种任务。

Ray 还有自己的 autoscaler。如果没有 K8s,它可以直接调用云厂商接口开机器;如果跑在 K8s 上,一般用 KubeRay Operator 把 Ray 集群的管理交给 K8s。这里有一个非常常见的误区:同时启用 K8s 的 Cluster Autoscaler 和 Ray 的 autoscaler,两边都会为资源不足创建新节点,最后集群扩了一堆机器,账单翻倍,任务还是没跑起来。

提示:K8s 和 Ray 的自动扩缩不要同时抢资源创建节点。用 KubeRay 让 K8s 管 Pod 生命周期,Ray 只负责分布式任务调度,职责才清晰。

3.2 Placement Group:Ray 的“资源套餐”

单任务或者单个@ray.remote的调度很简单,难的是多个 actor 之间有依赖关系。比如大模型张量并行,4 个 GPU 上的 4 个 worker 需要互相通信,最好落在同一台机器或者同一个高带宽域内;而数据并行副本之间则希望分散到不同节点,降低同时故障的风险。

Ray 的 placement group 就是干这个的。它把一组“资源套餐”打包,再用策略决定这些套餐怎么分布。PACK策略把所有 bundle 放在同一节点,适合张量并行;SPREAD策略把 bundle 分散到不同节点,适合数据并行;STRICT_PACK和STRICT_SPREAD是硬约束,不满足就宁可等。

from ray.util.placement_group import placement_group pg = placement_group( [ {"GPU": 2, "CPU": 8}, {"GPU": 2, "CPU": 8}, ], strategy="SPREAD", ) # 等资源到位再启动 actor ray.get(pg.ready()) @ray.remote(num_gpus=2) class ShardWorker: def __init__(self, rank): self.rank = rank ... workers = [ShardWorker.remote(i) for i in range(2)]

这段逻辑在单机上看似没什么,但放到一个几十节点的 Ray 集群里,placement group 直接决定了你的模型分片能不能以最优拓扑启动。说得再直白点:K8s 管“Pod 放在哪台机器”,Ray 的 placement group 管“这一组有逻辑关系的 actor 如何在任意 RM 的列表里摆放”。

3.3 Ray 调度直接影响 vLLM 的多卡行为

如果你用 Ray 来跑 vLLM,vLLM 的每个 GPU worker 实际上会被封装成 Ray actor。Ray 给每个 actor 分配num_gpus,再通过CUDA_VISIBLE_DEVICES告诉具体是哪张卡。这里一旦 Ray 调度不准,就会出现两个很典型的故障。

一是两个 actor 被分配到同一张物理卡,报“CUDA error: device not available”或者直接卡死。二是张量并行需要的几个 worker 被分散到跨节点,虽然还能跑,但通信开销大了,吞吐和延迟双双恶化。

我的做法是:vLLM 的分布式推理任务,一定要通过 placement group 声明每个 bundle 的 GPU 数量和工作类型,而且给每个 actor 配上整卡资源,不允许共享。同时启动前用ray status检查实际分布,确保每个 GPU 对应的 actor 数量为 1。

3.4 Ray 调度层的常见坑

除了双 autoscaler 的问题,Ray 这块还有两个常见坑。一个是任务长时间 Pending,ray list tasks里能看到任务状态是PENDING,但集群明明有空闲 GPU。这个多半是 placement group 的STRICT_SPREAD或STRICT_PACK约束太死,必须等一次性满足所有约束才放行。解决办法是把策略换成软约束,或者拆成小 bundle 分步调度。

另一个坑是 actor 泄漏。Ray 的 actor 不像 task 一样结束就释放资源,如果业务逻辑里反复创建 actor 却没有销毁,Ray 集群的资源会被慢慢吃光,表现为新任务全部 Pending。排查时可以看ray status里每个节点的“ACTIVE ACTORS”数量,如果持续增长,基本就是代码里少了ray.kill()或 actor 退出清理逻辑。

4. vLLM:决定“同一秒内先算哪个请求、在哪张卡上排队”

4.1 vLLM 的调度循环:engine core、scheduler、executor 是怎么串起来的

vLLM 最核心的抽象是LLMEngine,它内部维护几个组件:Scheduler、模型 Executor、KV Cache Manager。每次模型前向计算之前,LLMEngine都会调用 Scheduler 的schedule()方法,让调度器决定这一轮迭代放哪些请求进来。

我把一次调度的流程拆成四步。

第一步,Scheduler 从waiting队列里取出排队的请求,检查它们能否分配足够的 KV cache 块。能分配就进入running状态,不能分配就继续等,或者触发抢占。

第二步,Scheduler 把本轮允许执行的序列组装成SequenceGroupMetadata列表,交给 Executor。Executor 可能是单卡,也可能是多卡;多卡场景下每个 GPU worker 拿到各自的数据分片。

第三步,Executor 执行 Transformer 前向计算,返回新生成的 token 和每个请求的最新状态。

第四步,Scheduler 根据返回结果更新队列:完成的请求释放 KV cache,没完成的继续留在running,被抢占的进入swapped或等待重算。

这四步每一轮迭代都会执行一遍,vLLM 就是靠这个循环实现了 continuous batching。传统的静态 batching 要等整个 batch 全部生成完才能放新请求,vLLM 每一轮都在动态增删,所以 GPU 不会因为某个长序列生成到一半就空转。

不过调度循环的前提是显存预算算得准。KV cache 实际上就是显存里的一块分页空间,vLLM 用 PagedAttention 把每个序列的 KV 切成固定大小的块,调度器在分配 KV cache 时基本等效于操作系统在做内存分页。这也是为什么 vLLM 的调度器既像进程调度器,又像内存管理器的原因。

提示:vLLM 的调度成本其实很低,瓶颈通常不是调度算法本身,而是显存预算算得太紧,序列不停地被抢占重算,GPU 在做大量重复计算。

4.2 连续批处理、抢占与 Chunked Prefill

先说抢占。当显存里的 KV cache 块不够了,Scheduler 必须把某个running序列请出去,给新请求腾地方。vLLM 有两种抢占方式:RECOMPUTE和SWAP。RECOMPUTE 是把被抢占序列的中间结果全部丢弃,等显存有空间了再从头重算;SWAP 是把这一段的 KV cache 拷贝到 CPU 内存,等需要时再拷回来。

SWAP 听起来更文明,但 CPU 内存和 GPU 显存之间的拷贝也很贵,而且如果 CPU 内存不够,SWAP 反而会失败。所以实际使用中我一般会优先考虑 RECOMPUTE,同时启动方配置好序列级别的超时时间,避免某个大请求反复被抢占导致迟迟算不完。

再说 Chunked Prefill。一个很长的 prompt,prefill 阶段可能要算几万 token,如果把它当成一个整体塞进 GPU,这一轮迭代就会非常慢,同 batch 里的 decode 请求被迫等它。Chunked Prefill 会把长 prompt 拆成多个 chunk,每算完一个 chunk 就穿插几个 decode 请求,避免长 prefill 把整台 GPU 卡死。这个功能对线上服务尤其重要,我强烈建议默认开启。

4.3 关键参数怎么定:以 GPU 显存 90% 为例

vLLM 的调度参数本质上都是在“显存预算、并发上限、单步计算量”三个值之间找平衡。我常用的一组配置长这样:

参数建议值作用
gpu_memory_utilization0.90 - 0.95控制模型权重之外预留多少显存给 KV cache
max_num_seqs128 - 256最多同时处理多少序列,超过就排队
max_num_batched_tokens4096 - 8192单次前向计算最多多少 token,防止单步过慢
enable_chunked_prefilltrue长 prompt 分块,避免卡死 decode
max_prefill_tokens2048每个 chunk 里 prefill token 上限,越小越平滑
block_size16KV cache 分块大小,默认 16,一般不用动

怎么理解这个组合?比如一张 24GB 的卡,模型权重占 8GB,gpu_memory_utilization=0.92表示最多用 22GB,剩下的 14GB 就是 KV cache 预算。每个序列的 KV cache 大小取决于上下文长度、层数、注意力头数。用一个小模型部署 embedding 服务时,KV cache 压力小,max_num_seqs可以顶得比较高;用一个大模型服务长文本时,max_num_seqs反而要降,否则每个序列分到的 KV cache 块太少,调度器会频繁抢占。

启动命令大概是:

python -m vllm.entrypoints.openai.api_server \ --model /models/qwen3-embedding-0.6b \ --dtype float16 \ --gpu-memory-utilization 0.92 \ --max-num-seqs 128 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --max-prefill-tokens 2048

这里有个容易忽略的点:vLLM 的 Docker 镜像默认是不带模型文件的。很多人以为镜像里已经封装好了模型权重,拉下来直接跑就报错“找不到 model”。实际上要把模型放在共享存储或本地目录,用 volume 挂载进去,再通过--model指定路径。

4.4 “新版本性能下降”到底锅在谁

经常看到有人升级 vLLM 后反馈“吞吐变低了、延迟变高了”,然后开始怀疑新 scheduler 改坏了。我在实际对比里发现,很多时候不是调度器退步,而是几个默认参数变化导致的。

比如新版本可能默认开启了disable_async_output_proc、修改了默认的调度版本、调整了 preemption 模式,或者对 chunked prefill 的默认行为变了。你升级后如果没有保留一份压测配置,很难判断是代码优化还是配置漂移。

我的习惯是升级前先用vllm.entrypoints.llm或者benchmark_serving跑一轮压测,记录num_prompts、total_tokens、吞吐、TTFT、TPOT 几个核心指标。升级后再用同样的脚本跑一遍,逐项对比。如果发现吞吐下降,先回退参数,再回退版本,一步步定位,而不是直接骂调度器不行。

5. 三层调度如何协同:一次真实部署的调度链

5.1 一次请求从进来到出结果,三层分别做了什么

把三层调度拼在一起看,一整条链路是这样的。

用户发起请求后,先经过 Nginx 或 API 网关,路由到某个 vLLM Pod 的 Service 上。这个“路由到哪个 Pod”看似负载均衡,底层其实早被 K8s 调度决定了——Pod 副本分布在哪几台机器、每个副本的副本数,都是 K8s 在创建时排好的。

请求进入 vLLM 后走的则是另一条逻辑:vLLM 的 HTTP Server 把请求转成Sequence,放进waiting队列;Scheduler 每轮迭代决定哪些waiting请求分配 KV cache、进入running;Executor 把running序列喂给 GPU kernel,生成 token;生成完毕后,KV cache 释放,结果返回给用户。

如果这个 vLLM 实例是通过 Ray 拉起的分布式推理,中间还插着一层:Ray 决定每个 GPU Worker 的 actor 数量、放置策略、分片方式。K8s 只保证一个“Ray Pod”在这台机器上活着,但张量并行到底是 2 卡还是 4 卡,走 PACK 还是 SPREAD,由 Ray 的 placement group 决定。

三层链路可以概括成:宏观调度决定副本分布,中观调度决定分布式计算拓扑,微观调度决定每个请求每轮迭代的去留。任何一层出问题,现象都可能表现为“响应慢”或“资源利用率低”,但定位手段完全不同。

5.2 最小可复现的 K8s + Ray + vLLM 部署骨架

如果你的集群已经有一套 K8s,最省心的方案是先用 KubeRay Operator 把 Ray 集群跑起来,然后在 Ray 集群里用 placement group 拉起 vLLM 的分布式 actor。Deployment 这部分和普通应用类似,关键是把 GPU 资源写对:

apiVersion: apps/v1 kind: Deployment metadata: name: ray-worker spec: replicas: 4 template: spec: containers: - name: ray-worker image: rayproject/ray:latest resources: requests: nvidia.com/gpu: "1" cpu: "8" memory: "64Gi" limits: nvidia.com/gpu: "1" cpu: "8" memory: "64Gi"

Ray Worker 稳定后,再在 Python 入口里创建 placement group,声明模型分片需要的资源套餐。这里有一个经验:不要把num_gpus写成小数,比如 0.5。虽然 Ray 支持,但在 GPU 整卡分配模式下,小数意味着多任务共享一张卡,一旦某个任务因为显存不够崩掉,其他共享任务也会跟着受影响。做推理服务,一张卡一个任务最稳。

5.3 排查顺序:先微观、再中观、最后宏观

很多人遇到“请求排队上不去”会直接冲进 K8s 看节点监控,但节点监控只能看资源水位,看不到请求到底卡在哪一环。我推荐的排查顺序是“从现象倒着一层层往外查”。

先看 vLLM 自己的指标。如果queue_time和avg_prompt_processing_time一直飙升,说明微观调度层出问题,比如 KV cache 预算不够导致等待,或者 prefill 过长。这一步可以用 vLLM 暴露的 Prometheus metrics 或者日志里的queue time、token throughput直接确认。

再查 Ray。如果 worker 之间负载不均,某些 actor 的 GPU 利用率特别高、某些特别低,多半是 placement group 策略选得不对,或者任务被反复迁移。此时看ray status的每节点资源余量和ray list actors的分布。

最后才看 K8s。如果 Pod 在多个节点上分布不均,或者某个节点出现 CPU 抢占、磁盘 IO 抖动,再回头调整亲和性和污点规则。K8s 这一层解决的问题频率最低,通常只在 Pod 创建或节点故障时才需要人工介入,日常排障可以先放一放。

6. 常见问题与排查技巧实录

6.1 “调度冲突”典型症状与处置

调度问题最常见的统称就是“资源不够但说不上哪里不够”。我把平时遇到最多的现象列出来,方便你按图索骥。

K8s 层最常见的是 Pod 一直 Pending。用kubectl describe pod看事件,如果提示0/8 nodes available,接着看是不是 CPU 不足、GPU 不足、标签不匹配、污点没容忍。注意区分Insufficient nvidia.com/gpu和Insufficient memory,前者是 GPU 卡资源不够,后者是节点内存不足。前者解决办法是调整副本数或加节点,后者先看是不是有内存超卖。

Ray 层最常见的是任务 PENDING。ray status里能看到资源余量,如果是0.0/1.0 GPU,说明 GPU 全被占用;如果明明有余量但任务还是 PENDING,多半是 placement group 的硬约束卡住了。此时要么改策略,要么等存量 actor 释放。

vLLM 层最常见的现象是 GPU 利用率不高但请求队列持续增长。先查max_num_seqs是否设低了,再查 KV cache 是不是被某个长序列占满。一个 32K 上下文的长会话,KV cache 可能吃掉十几个普通请求的量。遇到这种场景,要么限制单序列最大长度,要么降低gpu_memory_utilization留出更多显存余量。

6.2 经验速查表

我把上面提到的坑整理成一个速查表,建议收藏到团队文档里,下次排障直接对号入座。

现象责任层快速定位方式常见解法
Pod 一直 PendingK8skubectl describe pod看 Event检查资源申报、标签、污点、亲和性
Pod 启动了但容器内看不到 GPUK8s / 设备插件nvidia-smi是否为空检查 DevicePlugin 是否成功上报,环境变量是否注入
Ray 任务一直 PENDINGRayray status看资源余量检查 placement group 策略、actor 泄漏
两个 GPU worker 挤到一张卡Raynvidia-smi看进程限制num_gpus=1整数分配,用 placement group 隔离
vLLM 长请求卡死整批vLLM观察 prefill 耗时开启 chunked prefill,调低max_prefill_tokens
vLLM 队列涨但 GPU 没打满vLLM看 KV cache usage 和 max_num_seqs提高max_num_seqs,控制序列最大长度
升级 vLLM 后性能下降配置漂移benchmark 前后对比回退默认参数或版本,逐项对照分析

再说一个很多人没注意的细节:vLLM 的日志里其实会周期打印显存和 KV cache 的分配情况,比如gpu_cache_usage=0.85。我习惯在监控面板里同时挂上gpu_cache_usage、num_requests_running、num_requests_waiting三个指标,它们的变化趋势能非常直观地反映微观调度层的工作状态。

这里要提一嘴,网上有些文章会把“调度”这个词用在完全不同的语境里,比如操作系统处理机调度、AGV 小车调度、物流调度、生产调度甘特图,甚至还有人争论“DolphinScheduler 和 Airflow 哪个好”。这些跟本文讨论的不是一回事。操作系统处理机调度解决的是 CPU 核上线程怎么切换;vLLM 解决的是 GPU 显存分页和 token 序列怎么排队。两者名字相近,但一个偏底层硬件,一个偏应用推理,不要混为一谈。

最后说一点个人体会。我在实际部署中见过很多平台团队,把 K8s 里的 nodeSelector 和 vLLM 里的max_num_seqs当成同一个东西来调,结果就是 GPU 看着没满、请求却在排队,问题定位得七拐八绕。调度这件事,宏观、中观、微观三层各盯各的指标,问题反而简单了。还有一个小心得:改 vLLM 调度参数时,一定要保留不同配置的压测记录,直接对比benchmark_serving的输出,别凭感觉调。我因为在max_num_seqs上多调了 64,吞吐没涨、preempt 反而变多,回头对比日志才明白是 KV cache 分块不够用了。如果你后续想再往深走,可以把三层调度器的关键指标串到统一监控面,做“三窗口”联动告警,这样大多数调度问题都能在用户侧感知之前被系统先标记出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询