十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

云原生大模型部署实战:K8s GPU调度与vLLM弹性伸缩全攻略

云原生大模型部署实战:K8s GPU调度与vLLM弹性伸缩全攻略 把大模型服务从“脚本启动”搬到云原生环境这个事我最近刚好完整做了一轮踩了不少坑也理顺了不少逻辑。今天这篇就围绕云原生环境中的大模型部署策略展开把我实际用到的方案、踩过的雷、反复调过的参数全部整理出来。这篇内容适合三类人一是正在做AI平台或模型服务化的工程师二是想把本地Ollama部署升级成生产级API服务的同学三是刚接触Kubernetes但想搞清楚“GPU调度到底怎么玩”的运维。我会从部署形态选型、K8s GPU调度、弹性伸缩、常见故障排查这几个维度讲尽量做到可以直接抄作业。先说一个核心结论云原生部署大模型真正的难点不是“把模型塞进容器”而是显存调度、弹性伸缩、流量治理、模型更新这些工程化的事情。模型推理快不快取决于GPU和推理引擎模型服务稳不稳、省不省成本、能不能扛住流量波动才取决于云原生这套架构。1. 为什么我建议把大模型推理服务迁到Kubernetes1.1 单机部署大模型的三个真实痛点很多人最开始部署大模型都是在一台GPU服务器上跑脚本用nohup或者systemd把推理服务拉起来。这个方法在开发验证阶段完全够用但一旦要面对多个模型、多个业务方、流量忽高忽低的情况问题就接踵而来。第一个痛点是显存利用率低。我团队早期有两台GPU服务器一台跑了LLM推理一台跑了Embedding模型显存分配都是写死的。结果LLM那台晚高峰排队Embedding这台一整个白天几乎闲置。后来想把Embedding模型挪过去和LLM混布又担心相互干扰最后只能继续闲置着。第二个痛点是模型更新必须停服。旧模型要换新版本先杀掉旧进程再拉新镜像再启动服务。如果是工作日白天操作业务方直接投诉“API又挂了”。这种更新方式在单机环境里几乎没法做到平滑。第三个痛点是故障恢复全靠人。GPU卡偶发异常、显存被某个进程吃满、进程僵死都需要人工登服务器排查。晚上两三点被告警叫醒然后手动重启服务这类经历相信不少人都体会过。1.2 云原生到底解决了什么问题云原生给大模型部署带来的核心能力可以概括成四个词编排、调度、弹性、可观测。编排解决的是“多副本如何管理”。同一个推理服务可以跑多个副本滚动更新时先起新Pod、再杀旧Pod请求不中断。调度解决的是“模型跑到哪块GPU上”。通过节点标签、亲和性、资源请求可以把不同模型合理地分配到不同GPU节点上。弹性解决的是“流量多了怎么办、流量少了怎么办”。Kubernetes自带的HPA加上自定义指标可以实现基于GPU利用率或请求队列长度的自动伸缩。业务高峰多拉起几个推理副本低峰自动缩容省下的GPU资源可以给其他任务用。可观测解决的是“服务出了什么状况”。通过Prometheus采集指标、Grafana展示面板、Loki收集日志GPU利用率、请求延迟、显存占用这些指标都可以一目了然。但这里我必须泼一盆冷水云原生本身不是性能加速器。它不会让你的模型推理变得更快它的价值在于让已有的GPU算力被更高效地组织、更稳定地输出。如果你只有一台服务器、一个模型、几十个日活调用方直接上K8s反而增加运维复杂度不一定划算。1.3 部署策略的整体链路我习惯把云原生大模型部署拆成三条线来看资源层、服务层、流量层。资源层解决的是GPU怎么管。包括GPU设备插件、显存切分方案、节点池划分、资源配额。服务层解决的是模型怎么跑。包括推理引擎选型、模型加载方式、副本管理、更新策略。流量层解决的是请求怎么进。包括Ingress网关、路由规则、限流熔断、灰度发布。三层各司其职但又是联动的。比如某业务方流量大增流量层先做限流保护然后HPA根据指标扩容服务层副本服务层扩容前资源层要保证节点池里有可调度的GPU资源。这三条线都理顺了整个部署策略才算闭环。2. 选型对比Ollama和vLLM到底该怎么选2.1 先分清你处在哪个部署阶段现在开源推理工具非常多很多人一上来就懵。我给大家一个判断框架先分清你是“本地私有部署”还是“生产对外服务”。本地私有部署典型场景是个人电脑或内网服务器上跑一个私有模型自己用、小团队用、或接Open WebUI这类前端。这时候追求的是快速启动、操作简单、模型管理方便。Ollama是这类场景的不二之选一条命令就能拉模型、起服务API也简单还有配套的模型仓库管理。生产对外服务典型场景是多个业务方调用API有并发、有SLA要求、需要压测、需要监控。这时候追求的是高吞吐、低延迟、稳定的显存控制和高级调度能力。Ollama就略显吃力vLLM、TensorRT-LLM、TGI这类专门的推理引擎才是正解。我见过不少团队把Ollama直接暴露到生产环境前期没问题流量一起来就各种超时、OOM、排队。不是说Ollama不行而是它设计的定位就不是高并发推理服务。工具选型一定要匹配场景这一点非常重要。2.2 主流推理引擎横向对比这里我列一个实际对比表格都是我自己部署过的引擎参数和表现基于实测引擎定位核心优势主要短板适用场景Ollama本地/私有快速部署安装简单、模型管理方便、依赖少高并发吞吐偏弱、调度能力有限开发验证、内网小范围使用、个人电脑vLLM生产级高吞吐推理PagedAttention显存效率高、兼容OpenAI API、支持Prefix Caching部署配置项多、学习曲线稍陡对外API服务、GPU集群、高并发场景HuggingFace TGI生产级推理HuggingFace生态紧密、动态批处理成熟部分新模型适配慢、调优依赖经验与HF生态深度绑定的团队TensorRT-LLM极致性能推理NVIDIA硬件上性能天花板最高模型编译复杂、构建时间长、灵活性低固定模型固定硬件、极致性能诉求SGLang高性能结构化推理结构化输出支持好、调度灵活社区相对新、资料较少需要复杂结构化输出的场景从这个表能看出来没有哪个引擎是绝对王者关键看你的调用模式和硬件事态。2.3 我的选型建议如果你是刚开始我的建议是分两步走。第一步先用Ollama在本地把模型跑通验证模型效果、调prompt、测业务逻辑。第二步确认模型效果没问题、要上生产了再用vLLM搭建正式API服务配合K8s做云原生部署。整个流程里模型权重是共享的不会因为换了推理引擎就需要重新下载模型。关于vLLM它最打动我的一点是PagedAttention它把KV Cache拆成块来管理大幅度减少了显存碎片这让它在大并发场景下能塞入更多请求。另外它还提供了兼容OpenAI的API接口迁移成本很低原来的openai客户端改一下base_url就能直接对接。如果你用的是NVIDIA A100/H100这类专业卡TensorRT-LLM确实能榨出更高性能但它引入的复杂度不容小觑。模型需要编译优化每次换版本都要重新构建。个人建议除非你对单卡吞吐有极致的追求否则先用vLLM把业务跑起来性价比最高。3. K8s部署大模型推理服务一份能直接抄的YAML3.1 GPU资源管理整卡、MIG、共享显存怎么选Kubernetes默认只认识“整张GPU卡”通过nvidia.com/gpu这个资源名来调度。一台8卡机器nvidia.com/gpu: 1就是分配一张完整的显卡给某个Pod。这种方式简单粗暴适合大模型这种独占显存需求明显的场景。但实际部署中我发现一个问题并不是所有模型都需要一整张卡。一个7B模型量化后显存占用可能只要8GB左右但整卡40GB显存A100大部分时间是闲置的。这时候有两个选择一个是NVIDIA MIG把物理GPU切成多个独立的GPU实例另一个是GPU Sharing用第三方插件实现显存共享。MIG只支持A100、H100这类高端卡切分后每个实例有独立的显存和计算资源隔离性最好但配置稍麻烦而且不同CUDA版本对MIG的支持不一致。GPU Sharing方案比如阿里云的gpu-share、NVIDIA的time-slicing配置更灵活但隔离性弱可能出现显存被打爆影响同卡其他Pod的情况。我的实操经验是生产环境优先整卡分配用节点池来做资源划分GPU Sharing适合测试环境或非核心服务。不要一上来就追求极致利用率先把稳定性和隔离性保住。3.2 vLLM推理服务的Deployment配置示例下面这份YAML是我在实际项目里用过的去掉了业务相关字段保留了核心配置apiVersion: apps/v1 kind: Deployment metadata: name: vllm-llama-server namespace: ai-service spec: replicas: 1 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: vllm-llama template: metadata: labels: app: vllm-llama spec: terminationGracePeriodSeconds: 120 containers: - name: vllm image: vllm/vllm-openai:latest command: [python3, -m, vllm.entrypoints.openai.api_server] args: - --model - /models/llama-7b-chat - --tensor-parallel-size - 1 - --max-model-len - 32768 - --gpu-memory-utilization - 0.9 - --port - 8000 - --enable-prefix-caching ports: - containerPort: 8000 name: http resources: limits: nvidia.com/gpu: 1 memory: 32Gi cpu: 8 requests: nvidia.com/gpu: 1 memory: 24Gi cpu: 4 readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 60 periodSeconds: 30 volumeMounts: - name: model-storage mountPath: /models volumes: - name: model-storage persistentVolumeClaim: claimName: model-pvc这里有几个参数我必须单独拿出来讲因为它们直接决定了服务能不能稳定跑。--max-model-len这个参数设置模型最大上下文长度。它直接影响显存的KV Cache预分配。设得太大启动时直接把显存占满设得太小超过长度的用户请求被截断。我建议结合业务实际输入输出来估算一般场景16384或32768比较常用。--gpu-memory-utilization设置KV Cache可用的显存比例。默认是0.9也就是90%显存会预留给KV Cache。这个值不是越高越好留出的10%要覆盖激活参数和临时计算的开销。如果设成0.99在部分模型上启动就会报CUDA Out of Memory。--tensor-parallel-size是多卡张量并行参数。如果模型单卡放不下需要设置成2、4等但这要求模型本身支持张量并行且多卡之间通过NVLink通讯。没有NVLink的机器性能会明显下降。--enable-prefix-caching是vLLM的缓存前缀复用开关后面还会具体讲。3.3 模型文件加载三种方式该怎么选大模型权重文件动不动几十GB怎么让Pod读到模型文件是个不能回避的问题。我试过三种方式各有优缺点。第一种是把模型文件打包装进容器镜像。优点是Pod启动就能用不依赖外部存储缺点是镜像体积巨大、构建和推送慢、更新模型要重新构建镜像。我建议除非模型很小几GB以内且很少更新否则不要用这个方式。第二种是用PVCPersistentVolumeClaim挂载模型文件。在K8s集群里先创建一块存储把模型文件放进去Pod启动时直接挂载。优点是模型更新只需替换存储里的文件Pod不用重新拉镜像缺点是存储要准备足够空间多副本同时读时要注意存储的IO能力。第三种是用Init Container从对象存储拉取模型到临时目录。Pod启动时先跑一个Init Container把模型从对象存储下载到emptyDir再启动主容器加载。优点是不需要创建持久化存储对象存储管模型版本很方便缺点是每次Pod重建都要重新拉模型冷启动时间会比较长。我目前用的是方案二PVC挂载。在本地测试时把模型文件传到PVC里生产环境通过对象存储同步到PVC。这样做的好处是模型版本更新时我只需要在PVC里切换软链接不用重启Pod就能完成模型灰度。3.4 模型平滑更新一个很容易翻车的地方K8s的滚动更新默认行为是“先起新的再杀旧的”但对大模型推理服务来说这个默认行为会引发严重问题。原因在于新Pod启动到Ready通常需要几分钟甚至十几分钟因为要加载几十GB的模型文件、分配显存、预热缓存。如果在旧Pod还没退出时新Pod还没Ready流量就不会切换但如果你没配置maxUnavailable: 0旧Pod可能会被立刻杀掉服务就出现了空白窗口。我的做法是三点配合strategy.rollingUpdate.maxSurge: 1表示最多多出1个副本maxUnavailable: 0表示旧副本不能减少再配合readinessProbe让新Pod就绪后才接流量。这个组合保证了更新过程中始终至少有1个可用的推理副本。另外terminationGracePeriodSeconds一定要设置足够长。vLLM接到SIGTERM后需要把正在生成的请求处理完再退出如果这个时间设得太短默认30秒进程还没处理完就被强杀线上就会看到大量客户端报连接重置。我设成了120秒实测下来比较稳。4. 弹性伸缩与流量治理让GPU跟着业务节奏走4.1 推理场景为什么不建议用CPU指标做伸缩K8s默认的HPA通常基于CPU使用率来做伸缩但对大模型推理服务来说这几乎是无效的。原因很简单推理服务的主要瓶颈是GPU利用率和显存CPU使用率往往很低数据预处理、调度、tokenize甚至可能一直低于20%。按CPU伸缩的话只有流量已经高到把CPU打满才会扩容但那时GPU早就打满了请求已经在排队了。正确的做法是基于自定义指标做伸缩。核心指标有两个一是GPU利用率二是推理服务的请求队列长度。GPU利用率高说明当前算力吃紧需要扩容推理队列长说明进来的请求超出处理能力需要增加副本。实现上vLLM会暴露Prometheus格式的监控指标包括running_requests、waiting_requests、gpu_utilization等然后用Prometheus Adapter把它们暴露给HPA使用。4.2 基于vLLM队列长度的KEDA自动伸缩我用的是KEDAKubernetes Event-driven Autoscaling它比原生HPA灵活可以直接从Prometheus查询指标来做伸缩。下面是一个ScaledObject示例apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: vllm-llama-scaler namespace: ai-service spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-llama-server minReplicaCount: 1 maxReplicaCount: 4 triggers: - type: prometheus metadata: serverAddress: http://prometheus.monitoring.svc:9090 query: | sum(avg_over_time(vllm:waiting_requests{namespaceai-service}[2m])) threshold: 5这个配置的含义是每2分钟采样一次vLLM的等待请求数如果超过5个就触发扩容最多扩到4个副本请求数降下来后会自动缩回1个副本。这个方案运行时一定要关注冷启动时间。大模型推理Pod的冷启动可能长达10分钟如果一个突发流量过来扩容的数字上去了但新Pod还加载不完模型集群就会陷入“扩容没用”的状态。为了缓解这个我把minReplicaCount至少设为1并且让核心模型始终保持一个“热副本”。4.3 缓存命中率与vLLM Prefix Caching调优关于“vLLM如何优化大模型的缓存命中率”这个问题实际上指的是vLLM的Prefix Caching功能。它会把请求的公共前缀部分比如system prompt、固定指令的KV Cache缓存起来后续相同前缀的请求可以直接复用这部分计算结果不需要重新跑一遍注意力计算显著提升吞吐。影响命中率的关键因素有三个。一是请求必须共享相同前缀。如果每个请求的system prompt都不一样或者prompt构建时随机性太强缓存很难命中。二是缓存容量有限如果max-model-len设置过大导致cache区域碎片化命中率也会下降。三是调度策略不同请求的prefix长度不一vLLM需要高效匹配缓存块。实操调优建议所有请求统一使用固定的system prompt模板不要逐票拼接动态前缀。启动参数加--enable-prefix-caching开启前缀缓存功能。对于模型内部prompt把“工具定义”、“系统角色”这些固定内容放在最前面因为前缀匹配是从开头对齐的。监控缓存命中情况可以通过vLLM暴露的metrics来观察命中率低的业务再反过来优化prompt结构。4.4 多模型统一入口与限流策略生产环境通常不止一个模型服务。LLM一个服务、Embedding一个服务、多模态一个服务如果每个服务都暴露独立域名调用方会非常痛苦。我建议用统一的API网关做入口比如APISIX或Nginx Ingress。按路径前缀把请求路由到不同后端服务/v1/llm/chat路由到vLLM服务/v1/embedding路由到Embedding服务。网关层面还要做限流。原因是大模型推理资源很贵一个业务方的突发流量随时可能打爆整个服务。限流策略要区分“并发限流”和“QPS限流”。大模型场景更建议限制并发数因为一个请求可能持续几十秒QPS限制反而不好把握。我用APISIX做过一个策略每个业务方API Key对应一个限流规则默认单个Key最多同时3个视频对话请求超出直接返回429。同时每个后端服务设一个最大并发数超过后多余请求进入队列或返回“稍后重试”。这套限流规则上线后服务再没出现过集体雪崩。5. 常见问题与排查实录那些我踩过的坑5.1 显存OOM排查启动时报错和运行时报错是两回事显存相关的坑我踩过无数回大致可以分成两类。第一种是启动阶段就OOM。最常见的诱因是--max-model-len设置过大导致KV Cache预分配超出显存。排查方法是看启动日志如果是指KV Cache size的报错先把--max-model-len调小一半试试。另一个诱因是--gpu-memory-utilization设得过高和模型权重本身抢显存。正常情况保持在0.85到0.92之间比较安全。第二种是运行过程中OOM。这类问题更隐蔽表现为服务跑一段时间后突然无响应或Pod被杀。常见原因包括并发请求数超过模型显存上限vLLM的--max-num-seqs参数控制的是同时处理的最大序列数如果设得太大每个序列的KV Cache累积起来就会爆显存。另一个原因是显存碎片化长时间运行不重启虽然空闲总量够但连续分配不出大块显存。查看报错我用三个命令kubectl describe pod看事件、kubectl logs看推理日志、nvidia-smi看显存实时状态。一般三步就能定位。5.2 一次滚动更新引发的服务不可用这里分享一个真实事故。有一次我从7B模型切换到13B模型直接kubectl set image更新Deployment。结果新Pod加载模型需要8分钟旧Pod在滚动更新开始后就被K8s标记为Terminating仅过了默认的30秒宽限期旧Pod就没了。那段时间所有请求全部失败业务方炸了锅。事后复盘问题出在三处第一没设maxUnavailable: 0旧Pod被提前销毁第二terminationGracePeriodSeconds太短旧Pod还没处理完流量就被强杀第三没有为模型目录做灰度切换新Pod起来后要重新读模型文件。现在我的标准流程是先在PVC里把新模型文件放好手动调整Deployment的args指向新模型路径用maxSurge: 1, maxUnavailable: 0策略滚动更新等全部副本Ready后再观察10分钟确认没问题再收缩旧副本。这个小流程帮我避免了很多次线上事故。5.3 幻觉、上下文长度与温度参数对线上服务的影响模型部署除了技术池还要处理模型本身的“能力边界”。上线前就要跟业务方对齐三个参数temperature、max_tokens、上下文长度。temperature控制随机性设置在0到2之间。客服、问答、代码生成这类对准确性要求高的场景我建议temperature设为0.1到0.3避免模型自由发挥。创意写作、头脑风暴场景可以设到0.8以上。很多问题反馈“模型胡说八道”往往就是temperature没调。max_tokens是单次生成的最大token数。它决定返回长度上限但要注意输出超长会直接截断用户看到的回答可能是半截话。业务方如果经常要长文输出要在prompt里要求完整回答并在服务端检测是否触发了截断标志。上下文长度是部署时定的max-model-len。它决定模型能“记住”多少会话内容。设得太短长对话被截断记忆丢失设得太长显存不够。我做过一个长文本总结服务上线初期设了8192结果用户输入几千字后模型只回复“内容太长无法处理”后来调成32768才正常。5.4 模型安全与合规检查别省略从公开渠道拿到的开源模型权重不代表可以直接放心上生产。我建议部署前至少做三件事第一对模型做一轮基础安全测试。包括对抗性prompt、越狱尝试、敏感话题探测。这不是为了刁难模型而是提前知道它有哪些偏向和漏洞方便在网关层做补充防护。第二网关层增加输入输出过滤。对大模型的输入做长度限制、敏感词拦截对输出做敏感内容检测。不要指望模型自律工程上兜底才可靠。第三私有化部署要考虑权重的来源安全。“投毒测试”在业界并不是新鲜事一个被恶意微调过的模型可能在某些特定触发词下输出异常内容。从非官方渠道下载的模型建议先校验哈希、对比官方发布信息再在内网隔离环境测试一轮。5.5 监控与成本控制最后说监控。一个合格的大模型推理服务至少要有以下指标GPU利用率、显存占用、请求延迟P50/P95/P99、每秒请求数、排队请求数、缓存命中率、Token吞吐量。vLLM自带Prometheus指标导出配合Grafana面板基本够用。成本控制方面我的经验是GPU节点池要区分“热池”和“冷池”。热池常驻核心模型的服务副本保证低延迟冷池按需扩容业务低峰时缩容到0。如果云厂商支持抢占式实例可以用它跑非核心的批量推理任务成本能降不少但不要让它承担线上核心API。6. 一点体会把大模型部署到云原生环境本质上不是为了追新而是让GPU资源跟着业务节奏走。我最大的感受是不要把问题复杂化。先用Ollama跑通模型再用vLLM稳定服务最后上K8s做编排和管理每一步都是在前一步的基础上升级。如果一开始就直接上全家桶配置项多到会让你失去耐心。最后分享一个小技巧给每个推理服务保留一个“逃生通道”。也就是在K8s集群之外留一台能直连模型的备机里面用最轻量的脚本方式把核心模型服务跑起来。正常情况它不对外服务一旦K8s集群出现大规模故障可以手动切换流量到这台备机顶上。这个设计在关键业务场景下救过我一次成本不高但很值得。
返回列表