ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes+vLLM大模型推理服务部署实战:从GPU调度到弹性扩缩容

Kubernetes+vLLM大模型推理服务部署实战:从GPU调度到弹性扩缩容 1. 整体架构设计与部署思路1.1 为什么选择KubernetesvLLM这个组合先说个真事。前段时间公司内部有个AI服务要上线业务方给的需求很简单把Qwen系列模型部署成HTTP服务支撑内部工具链的调用。一开始图省事直接在GPU服务器上nohup python -m vllm.entrypoints.openai.api_server一把梭服务确实起来了但还没撑到一周就暴露出一堆问题——业务线之间环境互相污染、某条业务流量一上来GPU显存直接被占满、扩容只能靠手动复制命令再起一个进程、模型版本更新时回滚特别痛苦。这就是典型的“从单机脚本走向生产环境”的痛点。而KubernetesvLLM这个组合正好把两个最关键的环节拆开了vLLM负责把大模型高效跑起来Kubernetes负责把vLLM容器当成一种可调度的资源管起来。说白了K8s不关心你容器里跑的是Nginx还是Qwen它只保证“你声明的副本数有几份就给你跑几份节点挂了自动在旁边重新拉起来”。这个解耦思路正是大多数大模型应用从实验走向生产必须迈过的一步。1.2 这套方案能解决什么问题资源隔离每个模型服务独立容器显存、CPU、内存互不干扰不会出现一个业务把GPU吃光、另一个业务直接OOM的情况。弹性扩缩容流量高峰时自动扩容Pod副本低峰时缩容回最小副本不用人盯着监控手动操作。故障自愈容器崩溃、节点宕机后自动重启或迁移配合探针还能在服务真正可用之前不给它派流量。版本化与灰度发布镜像Tag就是模型版本滚动更新失败后可以秒级回滚。统一入口通过Ingress或LoadBalancer暴露服务调用方只认一个地址Pod IP怎么变都无所谓。1.3 适合谁来参考这篇文章主要面向两类人一类是运维或平台工程师团队里有大模型推理服务要上容器平台你需要知道vLLM有哪些关键参数、怎么配合K8s的GPU调度另一类是算法或后端工程师本地脚本跑通了想上生产但对K8s生态不熟需要一套能直接抄作业的落地路径。2. vLLM核心机制与关键参数拆解2.1 vLLM为什么能扛住高并发以前用HuggingFace Transformers直接加载模型做推理每个并发请求都会占用一整份KV Cache显存很快就爆了而且多个请求之间是串行的吞吐量上不去。vLLM的核心创新是PagedAttention——它把KV Cache切成固定大小的块像操作系统管理内存一样按页分配内存利用率大幅提升同一个请求序列里还能共享部分页。这个思路在Kubernetes调度下尤其重要。因为你在yaml里给Pod声明的nvidia.com/gpu: 1是整卡粒度卡就一张能不能把这张卡的显存榨干就全看推理引擎了。同样的Qwen2.5-7B用原始Transformers部署可能并发8个请求就OOM用vLLM加合理配置能扛到几十甚至上百并发这决定了你底层的GPU数量可以少买多少。2.2 continuous batching与max-num-seqs的配合vLLM默认开启continuous batching意思是调度器会不断把新请求插入到当前正在执行的batch中只要显存里有空位就塞进去。但“塞进去”不是无限制的--max-num-seqs参数就限制了同一个batch里最多同时处理多少个序列。这个参数怎么定我一般这样估算推理可用显存 GPU总显存 × gpu-memory-utilization(默认0.9) - 模型权重占用模型权重占用可以启动日志里看到然后根据序列长度的上限max-model-len和KV Cache每token的字节数去反推能容纳多少条序列同时要给前向计算留下余量。实际操作中7B模型在A100 80G上max-num-seqs设在64到128之间通常比较合理如果业务方反馈延迟敏感就调小到16或32换来每条请求排队时间更短。这个参数不像量化那样能“一顿操作猛如虎”但它是吞吐和延迟之间的核心旋钮。2.3 max-model-len与显存估算--max-model-len代表模型能接受的最大序列长度上下文生成长度它不直接限制单条请求而是预分配KV Cache的空间。很多人一上来就设32768甚至65536结果模型权重还没加载完就OOM了。以Qwen2.5-7B为例FP16权重约15GBKV Cache在32K长度、batch为64时差不多要七八个GB再加上激活值、CUDA context一张24GB的卡就很吃紧。所以我的建议是先根据业务最长上下文需求算一个保守值比如业务实际最长也就8K那就设8192不要盲目追长。显存还有富余的话再逐步调大max-model-len或max-num-seqs。2.4 量化方式怎么选FP8H100、L20、A30等支持FP8的卡上推荐显存直接减半吞吐明显提升但要注意部分算子精度损失有些场景有延迟波动。AWQ/GPTQ老卡V100、A100上的常见4bit量化方案模型体积小精度损失可控。bitsandbytes只推荐做试验生产环境不太建议因为推理速度比较慢。这里补一句热词里提到的“fp8部署后偶发卡顿延迟”是很典型的场景。原因通常是FP8在某些算子比如自定义Attention上没有完全走专用kernel或者max_num_batched_tokens配太大导致batch过大。如果遇到这种情况我一般先把--max-num-seqs降一半再开--enforce-eager对比看是不是CUDA Graph与FP8 kernel之间的兼容性问题实在不行就换回BF16显存多占一点但延迟更稳定。2.5 --enforce-eager到底是什么默认情况下vLLM会启用CUDA Graph来加速小batch场景代价是启动时会捕获一组固定shape的CUDA kernel占用额外的显存。--enforce-eager就是禁用CUDA Graph每一次前向都走eager mode。什么情况下用一是模型跑在低显存边缘时比如24GB卡上跑14B量化模型省出来的那份图内存可能就是压垮骆驼的最后一根稻草二是某些新模型架构与CUDA Graph捕获不兼容不关掉直接启动报错。但注意eager mode下吞吐会有明显下降所以它不是“优化参数”而是“兜底开关”能不开就不开。3. Kubernetes上的GPU调度与vLLM部署实操3.1 GPU设备插件让K8s认识显卡K8s默认不知道节点上有GPU需要安装NVIDIA Device Plugin它会把GPU资源以nvidia.com/gpu的形式上报给kubelet。从K8s的视角看GPU和CPU、内存一样是一种可量化的资源只是只能以“整数”方式分配——你没法声明0.5张卡。安装方式很常规kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/main/nvidia-device-plugin.yml装完之后kubectl get node -o json里能看到节点的capacity.nvidia.com/gpu字段这就是调度依据。如果你用容器运行时是containerd设备插件同样适用它通过CDI或环境变量把显卡设备注入到容器里。3.2 GPU共享与切分一张卡怎么分给多个模型默认情况下一个Pod独占一张显卡。这在生产环境中非常浪费——显存够大的卡完全可以同时跑两个小模型。有两种常见方案Time-Slicing时间切片NVIDIA官方方案把一张GPU按时间片分给多个Pod适合多模型共享但显存不能严格隔离的场合。配置一个ConfigMap定义每个节点的GPU切分数再给设备插件加参数启动。缺点是多个Pod同时高负载时会互相争抢算力。MIG多实例GPUA100、H100等卡支持硬件级别的切分显存和算力物理隔离一个实例挂了不影响其他实例。但MIG对vLLM的影响是显存单实例变小了大模型放不下。我这边的经验是7B以下的小模型优先Time-Slicing每张卡分2到4个实例超过13B的模型直接独占整卡不要硬切。3.3 部署vLLM服务的完整yaml模板下面是一套我实际在用的部署文件参考了群里很多踩坑经历后逐渐稳定下来的版本apiVersion: apps/v1 kind: Deployment metadata: name: vllm-qwen25-7b namespace: ai-serving spec: replicas: 2 selector: matchLabels: app: vllm-qwen25-7b strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 template: metadata: labels: app: vllm-qwen25-7b spec: containers: - name: vllm image: vllm/vllm-openai:latest command: [python3, -m, vllm.entrypoints.openai.api_server] args: - --model/models/qwen2.5-7b-instruct - --served-model-nameqwen25-7b - --max-model-len8192 - --max-num-seqs64 - --gpu-memory-utilization0.9 - --port8000 - --enable-metrics ports: - containerPort: 8000 resources: limits: nvidia.com/gpu: 1 memory: 64Gi cpu: 16 requests: nvidia.com/gpu: 1 memory: 64Gi cpu: 16 readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 120 periodSeconds: 30 volumeMounts: - name: model-cache mountPath: /models volumes: - name: model-cache persistentVolumeClaim: claimName: model-pvc几个写yaml时容易忽略的点initialDelaySeconds要足够大。vLLM加载7B模型可能需要一到两分钟探针设置太短会导致Pod反复重启。requests和limits的nvidia.com/gpu必须一致且都是整数1这是K8s的硬性规定不支持小数GPU请求。--enable-metrics一定要加后面接Prometheus做HPA和监控全靠它不加的话连Pod里有没有正常服务都只能靠日志猜。滚动更新的maxUnavailable: 0保证新版本Pod就绪前旧Pod不会被杀掉这对在线服务很关键。3.4 模型文件的存储与加载方式模型文件动辄几十GB放镜像里显然不合理。我通常固定在节点上挂载一块NVMe SSD通过NFS或者JuiceFS把模型数据同步到本地然后以hostPath或PVC方式挂进容器。生产环境我更推荐用hostPath Local PersistentVolume因为NFS的IOPS对模型加载这种大文件顺序读场景不友好——从NFS上一次性读50GB模型文件往往要等好几秒甚至十几秒模型才完全加载完。但hostPath有个问题调度器不知道Pod应该调度到哪台机器上所以需要用nodeSelector或nodeAffinity把副本固定到有模型文件的节点或者配合本地PV的延迟绑定特性做到自动匹配。3.5 入口暴露与自动扩缩容3.5.1 Service与IngressDeployment就绪后创建ServiceapiVersion: v1 kind: Service metadata: name: vllm-qwen25-7b-svc namespace: ai-serving spec: selector: app: vllm-qwen25-7b ports: - port: 80 targetPort: 8000 type: ClusterIP然后用Ingress对外暴露如果公司内网已经有一套Ingress Controller比如Nginx Ingress直接配置一条路由规则即可。注意vLLM服务是流式输出Ingress和前端代理层的超时时间要放宽Nginx的proxy-read-timeout至少调到600秒不然长回答生成到一半连接就被掐断。3.5.2 基于指标的水平扩缩容vLLM暴露的/metrics端点里有几个重点指标vllm:num_requests_running当前正在处理的请求数vllm:num_requests_waiting排队中的请求数vllm:gpu_cache_usage_percKV Cache使用率vllm:generation_tokens_total生成token总数结合Prometheus Adapter可以做一套很实用的HPA策略排队请求数超过一定阈值就扩容排队清空则缩容。下面是一个简化的HPAapiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-qwen25-7b-hpa namespace: ai-serving spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-qwen25-7b minReplicas: 2 maxReplicas: 8 metrics: - type: Pods pods: metric: name: vllm_num_requests_waiting target: type: AverageValue averageValue: 16这里有个细节大模型服务扩容不是瞬间完成的vLLM从启动到加载完模型可能要几分钟所以HPA扩容阈值要比理想值再保守一些。我一般把目标排队数控制在“单副本吞吐量的一半”左右留出加载时间余量。4. 常见问题与排查技巧实录4.1 请求一直排队但GPU利用率不高这个现象我很长一段时间没想明白后来才发现是max-num-seqs设得太小了。比如业务方一下来100个并发请求但max-num-seqs只有8那么同时只能处理8条其他92条全在等待队列里排队GPU其实没有跑满。排查方法拉Prometheus看vllm:num_requests_running和vllm:gpu_cache_usage_perc如果running一直等于max-num-seqscache使用率却不到70%说明batch太小可以上调max-num-seqs。反之如果cache使用率已经到90%以上那就是显存真的撑不住了要么缩序列长度要么降量化精度。4.2 Qwen3系列重复输出与温度参数用vLLM部署Qwen3-8B时不少人会遇到生成内容反复出现同一段话的情况。这个跟vLLM没有直接关系是Qwen3模型默认的enable_thinking机制在起作用——模型在推理时会先输出一段内部思考再生成答案如果采样温度设太高思考部分容易陷入循环。解决方式客户端显式传temperature0.7以下并设置top_p不超过0.9服务端启动时检查是否关闭thinking模式或使用官方推荐的采样参数模板实在不行在请求参数里加上与“阅读并输出最终答案”配套的提示词模板。4.3 embedding和reranker模型在vLLM上启动报错热词里提到“昇腾910B/A2上不能通过vLLM启动embedding向量和reranker模型”这个其实不是昇腾的问题。vLLM核心支持的是生成式Decoder模型虽然新版本对部分embedding模型做了试点支持但大部分向量模型和双塔结构模型都不在vLLM支持列表内报错几乎是必然的。这类需求我建议直接用专门的工具embedding用TEI或Xinferencereranker同样用TEI它们和vLLM一样都能和K8s无缝集成只是镜像不同。不用勉强在一条路上硬怼。4.4 双卡推理为什么只看见一张卡在工作很多人想用两张L20跑模型理由很简单——单卡显存不够了。但部署后发现第二张卡的利用率几乎为零或者干脆起不来。这涉及到vLLM的张量并行tensor parallel机制只有当启动参数里加了--tensor-parallel-size 2时模型才会真正切分到两张卡上协同计算。不加的话vLLM只认一张卡其余卡资源空转。另外注意节点上的NVIDIA_VISIBLE_DEVICES环境变量如果设置了某张卡也会限制vLLM只看得见那部分设备。L20显存48GB跑Qwen2.5-14B量化后单卡完全够用不必强行双卡如果跑32B或72B级别的模型才建议考虑TP2或TP4同时要确认机器NVLINK拓扑是否支持高效通信——没有NVLINK时TP的通信开销会让性能严重缩水。4.5 显存明明够加载时还是OOM模型权重确实是按gpu-memory-utilization0.9来估算的但有几个隐藏显存占用很容易忽略CUDA context本身可能吃几百MB、CUDA Graph会预分配一部分显存、某些注意力算子的临时buffer也和序列长度成正比。我排查这类问题时会优先做三步启动前先nvidia-smi确认没有其他进程占着显存把gpu-memory-utilization降到0.8再试看是否是预留比例问题如果仍然OOM从日志里找具体是哪个tensor分配失败一般会提示是在权重加载阶段还是KV Cache分配阶段从而针对性调参数。4.6 节点上的卡被Pod调度但nvidia-smi里看不到这个问题常见于容器运行时未启用GPU支持。如果你用containerd且通过CDI方式使用GPU需要在containerd配置里启用CDI并且在Pod spec里加上cdi.k8s.io/nvidia.com: 设备名注解。另外检查Device Plugin是否与你的K8s版本兼容插件日志通常会打印“unable to find device plugin”之类的信息。还有一种情况Pod已经Running了但请求一直卡在ContainerCreatingdescribe pod显示nvidia.com/gpu无法满足——那通常是节点上的卡都被占满了但没有节点标签去约束调度或者Device Plugin把其中某张卡设为不支持分配状态。5. 上线前后的监控与运维细节我的习惯是服务上线第一天就把监控、日志、告警三件套全部接好不然等出故障才补会非常被动。监控Prometheus抓取vLLM的/metrics重点盯vllm:gpu_cache_usage_perc、vllm:num_requests_waiting、请求延迟分位数P50/P95/P99和GPU卡的温度、功耗。P99延迟比平均延迟更能反映真实体验。日志在K8s里推荐用Filebeat或Fluent Bit把标准输出和日志文件收集到ES或Loki。vLLM启动日志里有显存分配详情报错时能直接定位请求日志则方便做调用链追踪。告警至少设置三档——排队数超过阈值、GPU缓存使用率超过95%、P99延迟超过业务容忍线。注意告警规则要有持续时间避免因为某个瞬时抖动频繁骚扰值班同事。6. 踩过坑之后的一些额外建议最后再分享几个小经验都是真实项目中累积出来的第一镜像版本要锁死。不要用latest标签vLLM升级频率很快改了默认参数的行为就可能发生变化。我习惯在镜像名里带上release版本号比如vllm/vllm-openai:v0.6.6升级时在测试环境验证完再推生产。第二最好通过自定义API Key做租户区分vLLM原生不支持多租户鉴权但可以在前面加一层网关来做。K8s里做这层代理最简单的方式是塞一个Sidecar容器或通过Ingress插件实现不需要单独起一套服务。第三热更新模型时不要直接删Pod。可以先停流量用新版本镜像的Deployment滚动更新等readiness探针通过后再把Service流量切过去。如果你用的是蓝绿发布注意两套资源同时运行时的显存总量不能超过节点容量否则新Pod永远Pending旧Pod流量又切不过来就卡死了。第四定期做容量评估。K8s能自动调度但它不能凭空变出显卡。每季度结合业务增长算一下GPU总量够不够不够就提前采购或规划降级方案。这点很多团队容易忽略结果大促一来调度器全是Pending比显存不足还尴尬。这套“KubernetesvLLM”的组合是我目前在生产环境落地大模型推理服务的主推方案——K8s负责韧性vLLM负责性能两者各司其职配合起来确实省心。如果你也正在把大模型服务从单机脚本往容器平台迁移希望这篇文章能帮你少踩几个坑。
返回列表