
前两天帮朋友搭一套 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分布式任务跑在哪个 WorkerGPU 怎么分组毫秒到秒级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 节点打上gpurtx4090或gpua100标签然后在模型服务 Deployment 里指定 nodeSelector。这样不同型号的卡可以分开调度避免出现“要跑大模型却被分到小显存卡”的尴尬。如果还想做软规则用nodeAffinity的preferredDuringScheduling表示能分到一起尽量分到一起分不到也不阻塞。第二是污点和容忍。在 GPU 节点上加 taint比如nvidia.com/gputrue: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: 32Gi2.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_gpus1)装饰一个函数或类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 GroupRay 的“资源套餐”单任务或者单个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}, ], strategySPREAD, ) # 等资源到位再启动 actor ray.get(pg.ready()) ray.remote(num_gpus2) 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 来跑 vLLMvLLM 的每个 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 这块还有两个常见坑。一个是任务长时间 Pendingray 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。一个很长的 promptprefill 阶段可能要算几万 token如果把它当成一个整体塞进 GPU这一轮迭代就会非常慢同 batch 里的 decode 请求被迫等它。Chunked Prefill 会把长 prompt 拆成多个 chunk每算完一个 chunk 就穿插几个 decode 请求避免长 prefill 把整台 GPU 卡死。这个功能对线上服务尤其重要我强烈建议默认开启。4.3 关键参数怎么定以 GPU 显存 90% 为例vLLM 的调度参数本质上都是在“显存预算、并发上限、单步计算量”三个值之间找平衡。我常用的一组配置长这样参数建议值作用gpu_memory_utilization0.90 - 0.95控制模型权重之外预留多少显存给 KV cachemax_num_seqs128 - 256最多同时处理多少序列超过就排队max_num_batched_tokens4096 - 8192单次前向计算最多多少 token防止单步过慢enable_chunked_prefilltrue长 prompt 分块避免卡死 decodemax_prefill_tokens2048每个 chunk 里 prefill token 上限越小越平滑block_size16KV cache 分块大小默认 16一般不用动怎么理解这个组合比如一张 24GB 的卡模型权重占 8GBgpu_memory_utilization0.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、进入runningExecutor 把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: 64GiRay 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_gpus1整数分配用 placement group 隔离vLLM 长请求卡死整批vLLM观察 prefill 耗时开启 chunked prefill调低max_prefill_tokensvLLM 队列涨但 GPU 没打满vLLM看 KV cache usage 和 max_num_seqs提高max_num_seqs控制序列最大长度升级 vLLM 后性能下降配置漂移benchmark 前后对比回退默认参数或版本逐项对照分析再说一个很多人没注意的细节vLLM 的日志里其实会周期打印显存和 KV cache 的分配情况比如gpu_cache_usage0.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 分块不够用了。如果你后续想再往深走可以把三层调度器的关键指标串到统一监控面做“三窗口”联动告警这样大多数调度问题都能在用户侧感知之前被系统先标记出来。