news 2026/9/28 16:57:46

Kubernetes、Ray、vLLM 三层调度分工与协同优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kubernetes、Ray、vLLM 三层调度分工与协同优化

1. 三类调度器不是“谁管谁”,而是各守一段技术栈的边界

“Kubernetes、Ray、vLLM 都在调度,它们各自决定了什么?”——这个问题背后藏着一个普遍误解:很多人下意识把这三者当成同一层级的“资源分配工具”,甚至试图比较“哪个调度更强”。实则不然。它们根本不在同一个抽象层上工作,更像铁路系统里的不同角色:Kubernetes 是修轨道、建车站、管列车时刻表的基建与运力调度方;Ray 是设计车厢编组、定义乘客上下车流程、协调多节车厢协同运行的任务执行框架调度方;而 vLLM 则是坐在某节特定车厢里、专精于“如何让每位乘客(token)以最短路径、最少等待时间通过检票口和座位引导区”的模型推理粒度调度方。三者不重叠、不替代,而是纵向嵌套、职责分明。

我第一次在生产环境同时部署这三者时,就踩过典型误区:以为把模型镜像塞进 Kubernetes Pod 就万事大吉,结果发现 GPU 显存利用率长期卡在 30% 以下,QPS 上不去。排查三天才发现,Kubernetes 确实把 1 张 A100 分配给了 Pod,但 vLLM 的 PagedAttention 内存管理没启用,Ray 的 Actor 并发数设得过高导致请求排队,而 Kubernetes 的 Horizontal Pod Autoscaler(HPA)又因指标采集延迟,根本没触发扩缩容。问题不在“谁没调度”,而在“谁该调度什么”没理清。

这种分层本质,直接决定了你调试时的排查路径。当你遇到“GPU 利用率低但延迟高”这类典型症状,必须按顺序问三个问题:

  • Kubernetes 层:Pod 是否真拿到了独占的 GPU 设备?nvidia-smi在容器内是否可见?设备插件(NVIDIA Device Plugin)是否注册成功?
  • Ray 层:Actor 实例是否被正确放置在有 GPU 的节点上?ray.get_gpu_ids()返回值是否匹配?是否存在跨节点数据传输瓶颈?
  • vLLM 层:Scheduler 是否启用了enable_chunked_prefill?KV Cache 是否因block_size=16过小导致频繁换页?max_num_seqs是否远低于硬件并发能力?

关键词Kubernetes、Ray、vLLM、调度、GPU在这里不是并列关系,而是纵向责任链:Kubernetes 决定“物理资源归谁用”,Ray 决定“逻辑任务怎么分发与协同”,vLLM 决定“单次推理请求内部 token 如何高效流转”。忽略这个分层,所有优化都是隔靴搔痒。

提示:很多团队在压测时发现 vLLM 吞吐上不去,第一反应是调大--tensor-parallel-size。但若 Kubernetes 没给 Pod 绑定足够多的 GPU,或 Ray 的 Placement Group 没预留对应数量的 GPU 资源,这个参数调得再大也毫无意义——它只会触发 vLLM 的 fallback 逻辑,降级为单卡运行。

2. Kubernetes:决定“谁能在哪台机器上用多少块 GPU”

Kubernetes 的调度,核心是解决“资源供给”问题。它不关心你跑的是大模型推理、训练还是数据库,只认三样东西:CPU 核心数、内存字节数、GPU 设备数(由 device plugin 抽象为nvidia.com/gpu这类扩展资源)。它的调度器(默认是 kube-scheduler)在每次创建 Pod 时,执行一个确定性决策:从集群所有 Node 中,筛选出满足resources.requests.nvidia.com/gpu: 1的节点,并确保该节点剩余 GPU 数量 ≥ 请求量。

但这只是起点。真实场景中,Kubernetes 的调度决策远比“有没有 GPU”复杂得多。我们曾在线上遇到一个经典案例:集群有 8 台 A100 服务器,每台 8 卡,总 GPU 数 64。vLLM 服务配置了requests: {nvidia.com/gpu: 2},理论上可部署 32 个 Pod。但实际部署到第 25 个时,新 Pod 就卡在Pending状态。kubectl describe pod显示0/8 nodes are available: 8 node(s) didn't match Pod's node affinity/selector。排查发现,问题出在Topology-aware 调度上——我们启用了 NVIDIA GPU Operator 的 Topology Manager,要求 CPU Core、PCIe Root Port、GPU Device 必须在同一个 NUMA Node 上。而部分 A100 服务器的 BIOS 设置中,GPU 被映射到了非对称的 NUMA 域,导致虽然总 GPU 数充足,但符合拓扑约束的“可用 GPU 对”只有 24 个。

这就引出了 Kubernetes 调度的四个关键决策维度,每个都直接影响 vLLM 的实际性能:

2.1 GPU 设备绑定:从“共享”到“独占”的硬隔离

默认情况下,Kubernetes 仅保证 Pod 请求的 GPU 数量可用,但不阻止其他 Pod 共享同一张卡(通过 CUDA_VISIBLE_DEVICES 控制)。这对 vLLM 是灾难性的。vLLM 的 PagedAttention 依赖稳定的显存布局,若另一进程突然申请大量显存,会导致 KV Cache Block 被强制换出,引发严重抖动。解决方案是启用GPU 设备插件的device-plugin模式,配合nvidia.com/gpu: 1的 requests/limits,实现物理卡级独占。验证方法很简单:进入 Pod 执行nvidia-smi -L,应只看到 1 行输出;再执行nvidia-smi dmon -s u,观察util列是否稳定在 70%~95%,而非忽高忽低。

2.2 拓扑亲和性:让计算离数据更近

vLLM 的推理延迟对 PCIe 带宽极其敏感。一次典型的 Prefill 阶段,需将整个 prompt embedding 从 CPU 内存拷贝到 GPU 显存,再经 Transformer Layer 计算。若 GPU 与 CPU 不在同一 NUMA Node,跨 NUMA 访问延迟可达 100ns 以上,远超 PCIe 4.0 的 32GB/s 带宽限制。Kubernetes 通过topologySpreadConstraints和nodeAffinity强制调度器将 Pod 放置在 GPU 与 CPU 拓扑一致的节点上。我们的配置如下:

topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: vllm-inference nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: nvidia.com/gpu.present operator: Exists - key: topology.kubernetes.io/region operator: In values: ["cn-north-1"]

这段配置确保:1)Pod 不会跨可用区调度(避免网络延迟);2)只调度到有 GPU 的节点;3)同区域内的节点间 GPU 分布尽可能均匀(防止单点过载)。

2.3 资源预留:为 vLLM 的“突发流量”留出缓冲带

vLLM 的 Scheduler 会动态调整max_num_seqs(最大并发请求数),其理论峰值受max_model_len(最大序列长度)和block_size(KV Cache Block 大小)共同制约。公式为:
max_num_seqs ≈ (total_gpu_memory - model_weights_memory) / (block_size * 2 * sizeof(float16))
其中2是 Key 和 Value 各占一份。以 80GB A100 为例,加载 LLaMA-3-8B(约 16GB 权重),剩余显存约 64GB。若block_size=16,则max_num_seqs ≈ 64 * 1024^3 / (16 * 2 * 2) ≈ 1048576。但这是理想值。实际中,CUDA Context、PyTorch Autograd Graph、临时 Buffer 都会占用额外显存。因此,我们在 Kubernetes 中为 vLLM Pod 设置limits.nvidia.com/gpu: 1,但requests.nvidia.com/gpu: 0.5——这不是为了“超卖”,而是向调度器声明:“我需要 1 整张卡,但启动时只预占一半显存,为后续动态增长留出空间”。这避免了因requests==limits导致的过度保守调度。

2.4 自动扩缩容:用 HPA 抓住真实的业务脉搏

Kubernetes 的 HPA 默认基于 CPU/Memory 指标,这对 vLLM 几乎无效。一个空闲的 vLLM Pod,CPU 利用率可能只有 5%,但 GPU 利用率已满载;反之,当请求队列堆积时,CPU 可能飙升至 90%,但 GPU 仍在等数据。我们必须自定义指标。我们采用 Prometheus + kube-state-metrics 方案,采集 vLLM 暴露的/metrics端点中的vllm:gpu_cache_usage_ratio(GPU Cache 使用率)和vllm:request_queue_size(请求队列长度)。HPA 配置如下:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-deployment minReplicas: 2 maxReplicas: 20 metrics: - type: Pods pods: metric: name: vllm_request_queue_size target: type: AverageValue averageValue: 10 - type: Pods pods: metric: name: vllm_gpu_cache_usage_ratio target: type: AverageValue averageValue: "0.8"

这意味着:当平均队列长度 >10 或平均 Cache 使用率 >80% 时,HPA 开始扩容。实践证明,这套组合指标比单一 CPU 指标响应快 3 倍,且误扩容率低于 2%。

注意:不要迷信nvidia.com/gpu这个资源名。它由 NVIDIA Device Plugin 注册,但插件版本必须与 Kubernetes 版本兼容。我们曾升级到 Kubernetes v1.26 后,旧版 Device Plugin(v0.9.0)无法正确识别 A100 的 MIG(Multi-Instance GPU)切片,导致nvidia-smi -L在节点上能看到 7 个 MIG 实例,但kubectl get nodes -o wide却只显示nvidia.com/gpu: 0。最终回退到 Device Plugin v0.11.0 并重启 kubelet 才解决。版本兼容性表必须查 NVIDIA 官方文档,不能凭经验猜测。

3. Ray:决定“多个 vLLM 实例如何协同完成一个推理任务”

如果说 Kubernetes 解决了“硬件资源在哪”,那么 Ray 解决的是“逻辑任务怎么分”。vLLM 本身是一个单进程服务,但生产环境往往需要处理高并发、长尾延迟、模型路由等复杂需求。这时,Ray 的 Actor 模型就成为天然的 glue layer。它不调度 GPU,而是调度“使用 GPU 的 Python 对象”。

3.1 Actor 放置:让计算靠近数据,而非远离

Ray 的核心概念是 Actor——一个有状态的、远程可调用的 Python 类实例。当我们部署 vLLM 时,通常会创建一个VLLMActor,其__init__方法中初始化AsyncLLMEngine。关键在于:这个 Actor 必须被放置在有 GPU 的节点上,且其生命周期必须与 GPU 资源绑定。否则,Actor 可能被调度到 CPU 节点,导致torch.cuda.is_available()返回 False,初始化直接失败。

Ray 提供了PlacementGroup机制来精确控制资源分配。我们定义一个pg = ray.util.placement_group([{"CPU": 4, "GPU": 1}], strategy="STRICT_PACK"),然后用VLLMActor.options(placement_group=pg).remote()创建 Actor。STRICT_PACK策略确保所有资源(4 CPU + 1 GPU)必须来自同一个物理节点,彻底避免跨节点通信开销。这与 Kubernetes 的topologySpreadConstraints形成双重保障:Kubernetes 确保 Pod 在正确节点,Ray 确保 Actor 在 Pod 内的正确位置。

3.2 并发模型:Actor vs. Task,选错就是性能黑洞

Ray 提供两种并行原语:无状态的@ray.remoteTask 和有状态的 Actor。初学者常犯的错误是,为每个请求都创建一个新 Task:await generate_text.remote(prompt)。这看似简单,但代价巨大:每次 Task 启动都要重新加载模型权重、初始化 CUDA Context、重建 KV Cache 结构,耗时可达 500ms~2s。而 vLLM 的核心价值恰恰在于复用这些昂贵资源。

正确做法是:用一个 Actor 承载所有请求,用异步方法暴露服务。代码结构如下:

@ray.remote(num_gpus=1) class VLLMActor: def __init__(self): self.engine = AsyncLLMEngine.from_engine_args(engine_args) async def generate(self, prompt: str, sampling_params: SamplingParams): results_generator = self.engine.generate(prompt, sampling_params) final_output = None async for request_output in results_generator: final_output = request_output return final_output # 调用方 actor = VLLMActor.remote() result = await actor.generate.remote("Hello", SamplingParams(temperature=0.7))

这里,num_gpus=1是 Ray 的资源声明,它会触发 Ray 的调度器去寻找有 1 块 GPU 的节点。Actor 初始化后,所有generate调用都复用同一个engine实例,真正实现了“一次加载,多次推理”。

3.3 资源隔离:防止一个模型拖垮整个集群

生产环境中,我们常需同时部署多个模型(如 LLaMA-3、Qwen、GLM),每个模型对 GPU 显存、计算能力的需求不同。若所有 Actor 共享同一份 GPU 资源池,一个模型的 OOM 可能导致整个 Ray Cluster 不稳定。Ray 的解决方案是Resource Customization。我们为每个模型 Actor 声明专属资源标签:

@ray.remote(resources={"llama3_8b_gpu": 1}) class Llama3Actor: ... @ray.remote(resources={"qwen2_7b_gpu": 1}) class Qwen2Actor: ...

然后在启动 Ray Cluster 时,为每个节点指定其支持的资源:

ray start --head --resources='{"llama3_8b_gpu": 2, "qwen2_7b_gpu": 2}'

这样,Llama3Actor只会被调度到声明了llama3_8b_gpu资源的节点,且不会与Qwen2Actor争抢同一块 GPU。这是一种轻量级的、应用层的资源隔离,比 Kubernetes 的 Namespace 级隔离更细粒度,比 vLLM 的模型并行更灵活。

3.4 故障恢复:Actor 的状态持久化与自动重启

vLLM 的AsyncLLMEngine是有状态的:它维护着正在运行的请求队列、KV Cache 的 Block Table、生成过程中的中间结果。如果 Actor 所在的节点宕机,这些状态会丢失,导致请求失败。Ray 提供了checkpointing机制,但 vLLM 官方并不推荐对 Engine 做全量 Checkpoint(因为显存状态难以序列化)。我们的实践方案是:将 Actor 设计为“无状态核心 + 有状态外部存储”。

具体来说,VLLMActor本身不保存任何请求中间状态,所有generate调用都立即转发给一个中心化的RequestManager(部署为独立的 Ray Service)。RequestManager使用 Redis 存储请求元数据(prompt、sampling_params、request_id),并将request_id返回给客户端。vLLM Actor 只负责根据request_id从 Redis 读取 prompt,调用engine.generate,再将结果写回 Redis。这样,即使 Actor 重启,只要RequestManager和 Redis 健在,客户端就能通过轮询request_id获取结果。这是一种典型的“计算与状态分离”架构,牺牲了极少量延迟(Redis 读写约 1~2ms),换来了 99.99% 的故障恢复能力。

实操心得:Ray 的@ray.remote装饰器有一个易被忽视的参数max_restarts。默认为-1(无限重启),这在开发时很友好,但在生产环境可能导致“雪崩”——一个有 Bug 的 Actor 不断崩溃重启,消耗大量 CPU 资源。我们强制设置max_restarts=3,并在第 3 次失败后,由监控系统触发告警并人工介入。同时,在 Actor 的__init__中加入try...except,捕获torch.cuda.OutOfMemoryError并主动调用ray.actor.exit_actor(),避免陷入无限 OOM 循环。

4. vLLM:决定“单个请求内部,每个 token 如何被最高效地计算”

当 Kubernetes 把 GPU 给了 Pod,Ray 把 Actor 放到了正确的 GPU 上,vLLM 才真正开始它最核心的工作:在单张 GPU 上,以微秒级精度调度每一个 token 的计算。这与前两层的“宏观调度”截然不同,是真正的“微观调度”。

4.1 PagedAttention:GPU 显存的“虚拟内存管理”

传统推理框架(如 HuggingFace Transformers)将 KV Cache 存储为连续的 Tensor,其大小由max_seq_len决定。例如,max_seq_len=32768时,一个 32 层的模型,KV Cache 显存占用约为32 * 2 * 32768 * hidden_size * sizeof(float16)。对于 LLaMA-3-8B(hidden_size=4096),这轻松突破 40GB,远超单卡容量。

vLLM 的破局点是PagedAttention,它借鉴操作系统虚拟内存思想,将 KV Cache 拆分为固定大小的Block(默认 16 个 token),每个 Block 存储在显存的离散物理页中。逻辑上连续的序列,其 KV Cache Block 可以分散在显存各处。这带来三大优势:

  1. 显存碎片容忍:即使显存被划分为许多小块,只要总空闲 Block 数够,就能容纳长序列。
  2. 零拷贝共享:多个请求的 prefix(如 system prompt)可共享同一组 Block,无需复制。
  3. 动态扩容:新请求到来时,只需分配新 Block,无需 realloc 整个 Tensor。

block_size是 PagedAttention 的核心参数。我们做过一组对比实验:在 A100 80GB 上部署 LLaMA-3-8B,max_model_len=32768,测试不同block_size下的吞吐(req/s)和首 token 延迟(ms):

block_size吞吐 (req/s)首 token 延迟 (ms)显存碎片率
412.318512%
1628.7923%
6431.2880.5%
25629.595<0.1%

结论清晰:block_size=16是黄金平衡点。过小(4)导致 Block Table 过大,元数据开销占比高;过大(256)虽碎片率低,但单个 Block 未填满时浪费显存,且 Prefill 阶段的内存带宽压力增大。block_size不是越大越好,必须结合模型尺寸和典型 prompt 长度综合选择。

4.2 Scheduler:请求队列的“交通管制员”

vLLM 的 Scheduler 是整个引擎的“大脑”,它决定:1)新请求何时被接纳(Admission Control);2)已接纳请求的 token 何时被计算(Scheduling Policy);3)KV Cache Block 如何被复用与回收(Memory Management)。

其核心数据结构是ScheduledSequenceGroup,每个代表一个待处理的请求。Scheduler 维护两个队列:

  • Waiting Queue:新请求在此排队,等待显存和计算资源。
  • Running Queue:正在被计算的请求,其seq_group已被分配 Block。

关键决策逻辑在schedule()函数中。它每轮循环执行:

  1. Prefill 阶段检查:遍历 Waiting Queue,对每个请求计算prefill_tokens_needed = len(prompt)。若available_blocks >= prefill_tokens_needed / block_size,则将其移入 Running Queue,并为其分配 Block。
  2. Decode 阶段调度:对 Running Queue 中所有请求,计算decode_tokens_needed = len(running_seqs)(即当前正在生成的序列数)。若available_blocks >= decode_tokens_needed,则允许本轮 Decode。
  3. Block 回收:对已完成的请求,将其占用的所有 Block 标记为free。

这个看似简单的逻辑,却隐藏着深刻权衡。例如,“是否允许新请求抢占正在 Decode 的请求的 Block?”答案是否。vLLM 采用FIFO + Preemption策略:新请求只能等待,或当显存不足时,强制中断(Preempt)一个老请求,将其 Block 换出到 CPU 内存(Swap-out),腾出空间。这会导致被中断请求的延迟飙升,但保障了新请求的公平性。我们线上将preemption_mode="recompute"(而非"swap"),即中断后不换出,而是下次继续 Prefill,避免 Swap 的 I/O 开销。

4.3 EngineCore 与 Scheduler/Executor 的交互:一场精密的“流水线协作”

vLLM 的架构图常被简化为“Scheduler → Executor”,但真实交互远比这复杂。AsyncLLMEngine是顶层入口,它内部持有Scheduler和ModelRunner(Executor 的封装)。三者协作流程如下:

  1. Client 发起请求:engine.generate(prompt)创建一个Request对象,放入Scheduler.waiting队列。
  2. Scheduler 调度:Scheduler.schedule()返回一个SchedulerOutput,包含:
    • seq_groups:本轮要计算的请求列表。
    • blocks_to_swap_in/out:需要换入/换出的 Block ID 列表。
    • num_lookahead_slots:为下一个 Prefill 预留的 slot 数。
  3. Executor 执行:ModelRunner.execute_model()接收SchedulerOutput,执行三步:
    • Swap-in/out:调用 CUDA kernel 将 Block 从 CPU 内存拷贝到 GPU 显存,或反之。
    • Prefill:对新请求,执行完整 Transformer 前向传播,生成第一个 token。
    • Decode:对已有请求,执行单步 Transformer,生成下一个 token。
  4. 结果返回:ModelRunner将生成的 token、logprobs 等打包为SamplerOutput,交还给Scheduler。Scheduler更新seq_group状态,并决定下一轮调度。

这个流水线的关键在于异步与重叠。Swap-in和Prefill可以在不同 CUDA Stream 上并发执行;Prefill的输出可以直接作为Decode的输入,无需 CPU-GPU 数据拷贝。vLLM 通过精细的 CUDA Stream 管理,将这些操作重叠起来,将 GPU 利用率推至极限。这也是为什么 vLLM 的吞吐能比 Transformers 高 2~4 倍——它不是更快地算一个 token,而是让 GPU 几乎没有空闲时刻。

4.4 参数调优:那些文档里没写的“经验值”

vLLM 的命令行参数众多,但真正影响生产性能的,只有几个关键项。以下是我们在 10+ 个线上模型服务中验证过的“必调参数”:

  • --max-num-seqs 256:这是max_num_seqs的硬上限。不要设为理论最大值(如 1048576),那会导致 Scheduler 队列过长,增加延迟。256 是一个安全起点,可根据vllm:request_queue_size监控指标逐步上调。
  • --block-size 16:如前所述,16 是 A100/H100 上的黄金值。对于 L4(24GB 显存),建议--block-size 8以降低碎片。
  • --enable-chunked-prefill:当prompt极长(>8192 tokens)时,启用此选项可将 Prefill 分块执行,避免单次 kernel launch 时间过长导致 GPU 超时。但会略微增加显存开销(约 5%)。
  • --gpu-memory-utilization 0.9:显存利用率阈值。默认 0.9,意味着当显存使用率 >90% 时,Scheduler 会拒绝新请求。我们线上设为0.85,为 CUDA Context 留出缓冲。
  • --enforce-eager:仅在调试时开启。它禁用 vLLM 的图优化(CUDA Graph),让每个 kernel 都单独 launch,便于用 Nsight Compute 分析性能瓶颈。生产环境必须关闭,否则吞吐下降 30%+。

踩坑实录:我们曾将--max-num-seqs设为 1024,认为“越大越好”。结果发现,当请求队列长度超过 500 时,Scheduler.schedule()函数本身(Python 代码)的 CPU 占用飙升至 90%,成为瓶颈。这是因为schedule()需遍历所有seq_group计算资源需求,O(n) 复杂度。最终我们改用--max-num-batched-tokens 4096(限制每轮最多处理的 token 总数),将 CPU 负担转移到 C++ 层,问题迎刃而解。这印证了一个原则:vLLM 的调优,本质是在 Python 层(Scheduler)和 C++/CUDA 层(Executor)之间找到负载平衡点。

5. 三层联动:一次请求的完整生命周期与排障地图

理解了 Kubernetes、Ray、vLLM 各自的职责,最终要回归到一个最朴素的问题:当用户在前端点击“发送”,到收到第一个 token,这中间到底发生了什么?下面,我们以一次典型的 LLaMA-3-8B 推理请求为例,绘制完整的端到端生命周期图谱,并附上每一环节的排障要点。

5.1 生命周期:从 HTTP 请求到 token 流

  1. HTTP 入口层(Kubernetes Ingress):用户请求POST /v1/completions到 Kubernetes Service。Ingress Controller(如 Nginx)根据host和path将流量路由到后端vllm-deployment的 Pod IP。

    • 排障点:若返回503 Service Unavailable,先检查kubectl get endpoints vllm-service,确认 Endpoint 列表非空。若为空,说明 Pod 未就绪(Readiness Probe 失败),需看 Pod 日志。
  2. Kubernetes Pod 层:请求到达 Pod 内的vllm-api-server(FastAPI 进程)。此时,nvidia-smi应显示该 Pod 独占 1 张 GPU,且CUDA_VISIBLE_DEVICES环境变量应为0。

    • 排障点:若vllm-api-server启动报错CUDA driver version is insufficient,说明容器内 NVIDIA 驱动版本(nvidia-smi输出)与宿主机驱动不匹配。必须使用与宿主机驱动兼容的nvcr.io/nvidia/pytorch基础镜像。
  3. Ray Actor 层:vllm-api-server调用ray.get_actor("vllm_actor").generate.remote(...)。Ray Client 通过 GCS(Global Control Store)定位到vllm_actor所在的 Worker Node 和进程。

    • 排障点:若报错ActorDiedError或RayTaskError,执行ray status查看集群状态,重点检查Used Resources中GPU是否为 0(说明 Actor 未成功获取 GPU)。
  4. vLLM Engine 层:VLLMActor.generate()调用self.engine.generate()。AsyncLLMEngine的add_request()将请求加入Scheduler.waiting队列。

    • 排障点:若请求长时间卡在waiting状态,curl http://localhost:8000/metrics | grep vllm_request_queue_size查看队列长度。若 >0 且持续增长,说明Scheduler无法为其分配 Block,需检查vllm:gpu_cache_usage_ratio是否已达 100%。
  5. PagedAttention 执行层:Scheduler.schedule()返回SchedulerOutput,ModelRunner.execute_model()启动 CUDA kernel。此时,nvidia-smi dmon -s u应显示util列稳定在 80%~95%,mem列缓慢上升。

    • 排障点:若util波动剧烈(如 10% ↔ 90%),说明存在严重的 kernel launch 间隔,可能是block_size过小导致 Block Table 查找开销过大,或max_num_seqs过大导致 Python 层调度瓶颈。
  6. 结果返回层:ModelRunner生成SamplerOutput,Scheduler更新seq_group状态,并将首个 token 通过AsyncLLMEngine的 callback 机制,经vllm-api-server的 SSE(Server-Sent Events)流式返回给前端。

    • 排障点:若收到首个 token 后,后续 token 停滞,检查vllm:gpu_cache_usage_ratio是否在生成过程中骤降至 0%——这表明 KV Cache 被意外清空,常见于max_model_len设置过小,导致序列被 Truncate。

5.2 排障地图:按现象反推故障层

生产中最常见的 5 类现象,及其精准的定位路径:

现象可能根因层关键检查命令快速验证方法
Pod 一直 PendingKuberneteskubectl describe pod <pod-name>查看 Events 中是否有0/8 nodes are available: ...,确认 GPU 资源请求是否被满足
Pod Running 但nvidia-smi无 GPUKuberneteskubectl exec -it <pod-name> -- nvidia-smi -L若报错NVIDIA-SMI has failed...,检查nvidia-device-plugin-daemonset是否在对应节点 Running
API 返回 500,日志报CUDA out of memoryvLLMkubectl logs <pod-name> | grep "CUDA"检查--gpu-memory-utilization是否设得过高,或--block-size是否过小导致碎片
吞吐极低(<5 req/s),GPU util <20%Ray 或 vLLMray status+curl http://localhost:8000/metrics | grep vllm若vllm_request_queue_size高而vllm_gpu_cache_usage_ratio低,说明 Ray Actor 未正确绑定 GPU;若两者都低,说明 vLLM 未收到请求,检查 API Server 日志
首 token 延迟高(>500ms),后续 token 快vLLMnvidia-smi dmon -s u+cat /proc/<pid>/status | grep VmRSS首次 Prefill 需加载模型权重,若VmRSS(内存占用)在 Prefill 后激增,说明模型加载正常;若util在 Prefill 阶段很低,可能是--enable-chunked-prefill未启用,导致长 prompt 单次 kernel 过大

这张地图的价值在于:它把模糊的“性能差”转化为可执行的、分层的诊断指令。工程师不再需要“大海捞针”,而是按图索骥,5 分钟内即可定位到具体层级。

最后分享一个血泪教训:我们曾上线一个新模型服务,压测时一切正常,但上线后用户反馈“偶尔卡顿”。监控显示vllm_request_queue_size会周期性飙升到 200+,然后在 1 秒内回落。排查数日无果,最终发现是 Kubernetes 的livenessProbe配置了initialDelaySeconds: 30,而 vLLM 的首次 Prefill 耗时恰好 32 秒。Probe 失败导致 Pod 被反复重启,每次重启都丢弃了所有请求,形成“卡顿”假象。解决方案是将initialDelaySeconds改为60,并添加failureThreshold: 3。这提醒我们:Kubernetes 的运维配置,与 vLLM 的性能特征,必须深度耦合。脱离 vLLM 的实际行为去配置 K8s,注定失败。


我在实际部署 vLLM 的三年里,见过太多团队把问题归咎于“vLLM 不够快”,结果花两周优化 vLLM 参数,却发现根源是 Kubernetes 的topologySpreadConstraints配置错误,导致 80% 的请求被调度到跨 NUMA 的节点上。真正的性能工程,从来不是单点优化,而是对

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 16:56:45

DeepSeek实操手册:从状态流管理到生产部署全链路

1. 这不是“教程”&#xff0c;而是一份能直接上手跑通的DeepSeek实操日志2026年&#xff0c;DeepSeek系列模型已不再是实验室里的概念验证&#xff0c;而是真正嵌入到产品线、风控系统、内容生成流水线里的“生产级组件”。我从去年底开始在三个不同规模的团队里落地DeepSeek—…

作者头像 李华
网站建设 2026/9/28 16:56:25

agent-native智能体应用架构落地实践:从LLM补丁到自主执行体

过去一年里&#xff0c;我见过太多号称"AI应用"的项目&#xff0c;本质上是老系统打了个AI补丁&#xff1a;数据库表结构照旧&#xff0c;业务流程照旧&#xff0c;只是在某个角落塞了一个LLM接口&#xff0c;生成一段文字或做一次意图分类。这种方案不能说没用&…

作者头像 李华
网站建设 2026/9/28 16:56:17

Substrate区块链开发框架详解:模块化架构与Runtime升级实战

1. 项目概述&#xff1a;Substrate 到底是什么 我第一次听到 Substrate 这个词&#xff0c;是两三年前在朋友的项目讨论里。当时他说"我们用 Substrate 搭了一条链"&#xff0c;我脑子里的第一反应是&#xff1a;这不就是用 Polkadot 的框架改一改嘛&#xff0c;和用…

作者头像 李华
网站建设 2026/9/28 16:56:15

Superpowers使用指南:用技能让AI编程遵循工程工作流

最近聊AI编程的人&#xff0c;越来越多地提到 Superpowers 这个词。一开始我以为是某个新出的大模型名&#xff0c;或者又是营销号在炒作“AI超能力”概念&#xff0c;直到我把这套东西真正装起来跑了一周&#xff0c;才发现它值得单独写一篇使用指南。如果你正在用 Claude Cod…

作者头像 李华
网站建设 2026/9/28 16:55:56

STM32H750调试:5个高频Flash下载失败原因与解决套路

玩STM32H750VBT6的人&#xff0c;十个里至少有七个被“Flash Download Failed”折磨过。这个错误在Keil5里有一堆变体&#xff0c;今天可能报target dll has been cancelled&#xff0c;明天又变成could not load file xxx.axf&#xff0c;再过几天甚至冒出一个莫名其妙的"…

作者头像 李华
网站建设 2026/9/28 16:55:38

Agent-Native架构:从AI套壳到智能体原生的生产实践

很长时间里&#xff0c;我一直有种说不出的别扭感。市面上所有号称"AI应用"的产品&#xff0c;绝大多数只是给传统业务系统套了一个Chat窗口&#xff1a;用户在对话框里提问&#xff0c;系统通过RAG去知识库里检索几段文字&#xff0c;再把答案拼装成一段话吐出来。用…

作者头像 李华