大模型推理的热度,这两年几乎全被模型参数和刷分霸屏。可真到了业务落地的阶段,大家才会在同一个地方停下来挠头:模型拿回来了,怎么稳定、弹性、安全地跑成一个线上服务?我前阵子把一套7B对话模型完整部署到容器服务VKE上,再配合vLLM做推理加速,整整跑了两周生产流量,今天就把这次踩出来的经验原原本本写下来。这是一篇偏实战的内容,也会把VKE与大模型推理相关的六大能力模块完整拆一遍。
我建议你按自己的角色挑着看:如果你偏平台或SRE,重点看能力模块和调度细节;如果你偏算法工程,重点看部署流程和问题排查。但不管你属于哪类,只要你的服务准备上容器、要用GPU、要对外提供大模型推理API,这篇文章都会比官方文档多给一层“现场感”。
1. 为什么大模型推理偏偏需要容器服务VKE
1.1 大模型推理部署的三个典型痛感
没亲手从零部署过大模型推理服务的人,很难理解为什么一个“模型加载然后调用”的过程会搞出那么多幺蛾子。首先,环境依赖极其脆弱。PyTorch版本、CUDA驱动、Python包、推理框架,任何一个版本对不上,服务要么直接起不来,要么跑几步就崩。同一个团队里,A用1.x版本的transformers,B用2.x,最后合并代码的时候光修依赖就够喝一壶。
其次,GPU资源不好管。一张A100上到底能塞几个模型,显存够不够,并发上来之后会不会OOM,这些不是靠肉眼看docker ps能解决的。更麻烦的是,不同模型对显存和算力的要求差异很大,直接把一整张卡独占给一个小模型,成本高到离谱,但如果把多个模型塞进一张卡,又得操心隔离和互相干扰。
第三,上线链路太长。以前部署一个模型服务,得单独写部署脚本、配服务发现、挂监控、设告警、打通网络策略,零零散散搞一两天都很正常。而且这些配置千奇百怪,换一个人就很难维护。我跟不少做AI平台的朋友聊过,大家的感觉出奇一致:模型的训练和微调倒是推进得挺快,反而是在“把模型变成服务”这一段,被各种工程问题拖慢了速度。
1.2 VKE这类托管容器服务的底层逻辑
容器服务VKE本质上就是一个托管的Kubernetes集群,但它不是简单帮你装好K8s就完事。它把Kubernetes的声明式API能力做成了云上的一等公民——节点不用你自己买自己装,弹性伸缩可以按配置自动扩缩,LB、存储、监控这些周边依赖都能用云产品原生对接。
这样带来的最大好处是,你不需要关心集群控制面怎么维护、etcd怎么备份、Node怎么驱逐重建。算法同学只要把一个推理镜像推上去,在VKE里写一份Deployment + Service的YAML,一个可对外提供服务的推理应用就上线了。从“我有一堆模型权重”到“模型在跑且能扛流量”,中间的距离被压缩到几十分钟。对大模型推理这种形态来说,这个压缩特别关键,因为在生产环境里你还需要面对显存争抢、冷启动、峰值流量、节点故障这些问题,不是一个模型文件就能解决的。
2. 六大能力模块拆解:VKE体系的骨架
2.1 弹性伸缩:把GPU节点按流量“吹大”和“捏小”
大模型推理服务的流量曲线通常不是平的,早晚高峰、运营活动、临时任务都可能带来突发流量。弹性伸缩这个模块,管的就是当流量变大时副本数能不能自动加上去,流量降下来时资源能不能收回来。
VKE的弹性伸缩我实际用下来,主要是两层结构:一层是Pod层面的HPA,根据CPU、内存或者自定义指标调整Deployment副本数;另一层是节点池层面的Cluster Autoscaler,当Pod调度不上时,自动在新节点池里拉起一批GPU节点,Pod跑完后没活干了再缩掉。
这里的重点在于“拿什么指标做伸缩”。纯CPU水位对大模型推理基本不适用,因为请求计算主要发生在GPU上,CPU利用率往往不高。我实际用的是vLLM暴露出来的自定义指标,比如vllm:num_requests_waiting(排队请求数)和vllm:num_generating(正在生成的请求数),结合Prometheus Adapter把指标喂给HPA。下面这个配置就是基于排队请求数做扩缩容的:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: llama2-7b-inference minReplicas: 2 maxReplicas: 8 metrics: - type: Pods pods: metric: name: vllm_num_requests_waiting target: type: AverageValue averageValue: "8" behavior: scaleDown: stabilizationWindowSeconds: 600把stabilizationWindowSeconds设成600秒,是为了防止流量抖动导致副本频繁缩下去又扩上来。推理服务跟普通Web服务不太一样,Pod起来以后加载模型到显存需要一两分钟甚至更久,缩容太激进,模型的热数据刚准备好就被干掉了,相当浪费。宁可多留几个副本兜底,也别卡在扩容的等待时间上。
2.2 异构算力:一张卡跑多个模型的调度艺术
VKE底层接的是Kubernetes的Device Plugin机制,节点上有GPU的时候,它会向上报一个nvidia.com/gpu资源,Pod里声明limits: nvidia.com/gpu: "1",调度器就会把Pod调度到有GPU的节点上。
但真正让推理成本降下来的,是GPU共享与切分能力。一个7B模型在FP16下权重大概14GB,如果用一张80GB的A100独占,显存利用率只有20%不到,算力更是浪费不少。我这边做过几轮优化后,一般让两个到三个小型对话模型共享一张大卡,或者用NVIDIA MIG做硬隔离,效果比独占模式好得多。
调度策略上,有两个方向要提前想清楚。Kubernetes调度器默认会把Pod尽量打散到不同节点,这在普通服务里是高可用的好习惯,但在GPU推理里不一定合适。如果你的业务高峰明确、节点池要缩容,那就希望负载尽量集中到少数节点上,缩容时才能腾出空节点。VKE这类托管服务通常允许配置Binpack(集中)或Spread(打散)策略。
如果跑的是70B以上模型,单卡肯定放不下,需要做张量并行。这时调度要额外关注节点亲和性,两个Pod要落在同一台多卡机器上,而且要预留足够CPU内存。我见过因为调度策略没配好,两个并行分片被调度到不同机器,走网络做张量同步,推理时延直接翻了好几倍。所以部署前就要把nodeSelector、PodTopologySpread这些字段规划清楚,宁可多加两条约束,也别让调度器自由发挥。
2.3 存储与模型加速:几十GB权重文件怎么快速就位
部署大模型推理,千万别把模型权重打进容器镜像。7B模型权重十几GB,70B上百GB,镜像仓库会被压垮,节点拉镜像也会拉到怀疑人生。正确做法是:镜像里只放推理框架和依赖代码,权重文件放到外部存储,通过PV/PVC挂载进容器。
VKE支持的存储类型很丰富,按场景分大概这么几类:
- 云盘(ESSD这类):单节点读写性能最好,适合模型固定挂载在一个Pod上的情况,但多副本之间没法共享。
- 文件存储:多个副本可以同时挂载同一份模型文件,适合先共享读模型,每个Pod再写自己的本地缓存。
- 对象存储:适合做模型文件的归档和分发,尤其模型版本多的时候,把历史版本放对象存储,需要时再拉到计算节点。
模型加载最怕冷启动。第一次启动时从远端拉十几GB文件,几十秒到几分钟就过去了。我在VKE里经常用的优化手段是:节点池制作自定义镜像时,提前把常用模型缓存到节点本地目录,Pod启动时直接从本地加载,而不是每次从对象存储拉。还有一个做法是给模型目录做一个简单的缓存层,同一节点上多个副本都是读同一份本地权重文件,热启动基本能控制在几秒内。
2.4 网络与流量接入:让请求低延迟抵达推理后端
推理服务对网络的要求比普通Web服务高,因为一次推理请求可能要持续几十秒甚至更久,且涉及到SSE流式输出。VKE的网络模式一般推荐VPC-CNI直通,Pod直接拿VPC地址,包转发路径短,性能优于传统的叠加网络。在压测里,这个差异在长连接高并发场景下会非常明显。
服务暴露方面,集群内部访问用ClusterIP,外部访问有四层LoadBalancer和七层Ingress两种选择。对推理API这类需要灵活的请求路由的业务,我一般会走Ingress。但注意,默认的Nginx Ingress配置对推理场景不太友好,响应体稍微大一点就会被缓冲,SSE流式输出会被卡住。需要在Ingress上追加类似这样的注解:
nginx.ingress.kubernetes.io/proxy-buffering: "off" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600"关闭缓冲后,模型生成一个token就能立即返回一个token,用户体验会好很多。同时把超时时间拉长,因为大模型生成几百个token确实可能需要一分钟量级,直接用默认的60秒超时,业务稍微长一点就会被网关掐断。
2.5 可观测性:从GPU温度到首token时延
部署完服务只是开始,真正考验人的是线上出问题的时候能不能快速定位。VKE配套的可观测体系里,我重点盯四类指标。
第一类是节点和Pod的常规指标,CPU、内存、磁盘。第二类是GPU指标,显存占用、GPU利用率、温度、功耗,这些在云监控里直接能看到。第三类是框架级指标,vLLM、TGI这类推理框架都会暴露自定义metrics,比如当前请求数、排队数、平均生成token数。第四类是业务指标,最重要的就是TTFT,首token时延,也就是用户发出请求到收到第一个token的时间。
我自己常用的告警阈值整理成了表格,方便直接抄:
| 指标项 | 含义 | 参考告警阈值 |
|---|---|---|
| GPU 利用率 | 计算单元占用比例 | 持续超过90%且副本数已达上限 |
| 显存占用率 | 显存水位 | 超过85%持续5分钟 |
| TTFT P99 | 首token时延 | 超过2秒 |
| 请求排队数 | 等待中的请求数量 | 平均值超过8 |
指标要联动起来看才有效。比如GPU利用率高但TTFT也高,说明算力饱和,应该扩容;GPU利用率低但TTFT高,那可能是网络或者并发调度出了问题,先把瓶颈找到再动资源。
2.6 安全与多租户隔离:非root跑服务才是基线
容器安全里有一条讲究:运行用户不要用root。大模型推理服务如果以root跑在容器里,一旦应用被攻破,攻击者拿到的就是容器内的最高权限,逃逸风险会显著升高。VKE底层的Kubernetes本身就有完整的安全原语,关键是你得愿意用起来。
在镜像里,我用Dockerfile显式创建一个普通用户,并把模型目录的属主交给他。直接在Dockerfile里固定用户和目录权限,比在运行参数里单独指定更不容易忘。
FROM vllm/vllm-openai:latest RUN useradd -m -u 1000 modeluser \ && mkdir -p /workspace /models \ && chown -R modeluser:modeluser /workspace /models USER modeluser除了非root,还有几个安全项也值得检查:容器内开启runAsNonRoot: true、allowPrivilegeEscalation: false,避免提权;镜像推上去之前做一次漏洞扫描,别把带高危漏洞的基础镜像带到生产;团队共享集群时,用Namespace加上ResourceQuota限制每个团队的CPU、内存和GPU额度,防止某个团队把集群资源吃空。
3. 大模型推理的真实业务落地:vLLM部署全流程
3.1 前置准备与集群规划
我这次选的是7B规模对话模型,规格上单卡A10也就是24GB显存就能跑起来,但为了容错和并发,我最终用了A100 40G节点。选实例规格的时候要提前估算显存:FP16的7B权重约14GB,再加上KV Cache、激活值、CUDA上下文,留出40%甚至50%余量比较稳妥。下面这个表是我常用的规划参考:
| 模型规模 | 约估显存占用 | 建议实例 |
|---|---|---|
| 7B | 16GB~24GB | 单卡A10/A100 40G |
| 13B | 28GB~40GB | 单卡A100 40G/80G |
| 70B | 130GB~160GB | 2×A100 80G 或 4×A100 40G |
创建VKE集群时,我开了两个节点池:CPU节点池用来跑业务网关和运维组件,GPU节点池专门跑推理服务。GPU节点池需要打开弹性伸缩,并且设置最小2台、最大6台,这样高峰期节点能自动扩出来,低谷期也能缩回去控制成本。网络选择VPC-CNI直通模式,方便后续挂负载均衡和腾讯云那类安全组策略。
3.2 镜像构建与推送
模型权重不进镜像,所以镜像本身不大。我用vLLM官方镜像作为基础层,把非root用户的配置写进去,然后构建并推送到镜像仓库。这里命令是通用的,无论你用的是VKE配套的镜像仓库,还是其他任意Registry,动作都是先tag再push:
docker build -t registry.example.com/llm/vllm_llama2_7b:1.0 . docker push registry.example.com/llm/vllm_llama2_7b:1.0有一类镜像不要做进容器:体积巨大的模型权重、虚拟环境、临时缓存文件。镜像只保留Python代码、依赖和推理框架,这样拉起容器快、镜像仓库压力小、安全扫描也容易做。
3.3 在VKE上部署推理服务
部署推理服务我直接用一份Deployment YAML,把GPU资源、模型数据卷、安全配置一次性声明好:
apiVersion: apps/v1 kind: Deployment metadata: name: llama2-7b-inference namespace: llm spec: replicas: 2 selector: matchLabels: app: llama2-7b-inference template: metadata: labels: app: llama2-7b-inference spec: containers: - name: vllm image: registry.example.com/llm/vllm_llama2_7b:1.0 args: - "--model" - "/models/llama-2-7b-chat-hf" - "--served-model-name" - "llama2-7b" - "--tensor-parallel-size" - "1" - "--max-model-len" - "4096" ports: - containerPort: 8000 resources: limits: nvidia.com/gpu: "1" volumeMounts: - name: model-storage mountPath: /models securityContext: runAsNonRoot: true allowPrivilegeEscalation: false volumes: - name: model-storage persistentVolumeClaim: claimName: model-pvcnvidia.com/gpu: "1"是让调度器从可用的GPU节点池找一台有卡的节点。max-model-len控制最大上下文长度,如果业务里长文本对话多,这个值要适当放大,但对应的KV Cache也会吃更多显存。我建议先用小并发验证一次,再逐步上调,别一上来就把显存打满。
3.4 让服务能被业务访问:Service与Ingress
Deployment跑起来之后,还需要一个Service把Pod的端口暴露出来。集群内部调用用ClusterIP就够,但要走外部API的话,我一般再挂Ingress做域名和路由管理。
apiVersion: v1 kind: Service metadata: name: llama2-7b-inference-svc namespace: llm spec: selector: app: llama2-7b-inference ports: - port: 8000 targetPort: 8000Ingress部分除了前面提到的关闭缓冲、调大超时,还可以按路径把多个模型路由到不同的后端Service。比如/v1/llama2-7b指向7B模型,/v1/llama3-70b指向70B模型,对外看来就是一个API网关,实际上背后是多个推理服务在跑。这个路由方式对多模型平台非常实用。
3.5 弹性伸缩配置:按推理负载自适应扩展
Deployment部署完,我接着配置HPA和节点池弹性,让服务能在流量上来时自动扩容。HPA的配置在2.1里已经给了,这里再补一个关键点:GPU节点池的Cluster Autoscaler不要设置得太敏感,否则一次短时流量抖动就可能触发节点扩容,导致成本飙升。我一般把扩缩容的冷却时间拉到5分钟,节点空闲静默期也调长。
还有一个细节:HPA的minReplicas不要设置成1。大模型推理Pod缩到一个副本的时候,万一它挂了,新的Pod冷启动加载模型要好几分钟,服务直接断档。我习惯最低保底两个副本,既保证故障时还能有流量承接,也避免频繁扩容缩容。
3.6 轻量边缘推理可以作为页边补充:Jetson上跑llama.cpp
VKE承载的是云端集中式算力,但大模型推理不一定只在云上。像Jetson AGX Orin这类带GPU的边缘设备,配合llama.cpp这类针对CPU和轻量GPU优化的推理引擎,也能跑起来量化后的小模型。之前我把一个7B模型做4-bit量化后在Jetson上跑,推理速度虽然远不如A100,但对一些数据不出局的边缘场景已经够用。这类边缘设备的好处是部署在机房或现场,可以和云上VKE集群通过控制面打通,形成“云上做大模型、边缘做轻量推理”的混合架构。
4. 常见问题与排查技巧实录
4.1 GPU显存不足导致Pod一直Pending
这是新手上路最容易遇到的情况,现象是Pod一直处于Pending,登录VKE控制台看到事件里写着Insufficient nvidia.com/gpu。原因很简单:节点上的卡已经被其他Pod占满,或者剩余显存不够你声明的资源。排查时先看节点GPU分配情况,再看有没有其他模型把卡吃满了。有时候不是卡不够,而是只声明GPU数量、没声明显存,两个小模型同时被调度到同一张卡上,显存会爆。针对这种情况,我一般会约束同一张卡上的Pod数量,或者在业务低峰期手动缩到指定的副本数。
4.2 镜像拉取慢、容器启动慢
启动慢的原因分两种:镜像太大,或模型权重冷启动。镜像问题可以通过给推理镜像瘦身解决,把用不到的包全部清掉,基础镜像尽量选官方精简版。模型权重冷启动问题刚才也提到,最有效的办法是节点池预缓存。如果已经是预缓存了还是慢,检查一下文件存储或对象存储的带宽,带宽不够时大量并发读取会把存储打满,启动速度自然上不去。
4.3 非root用户没法写模型缓存目录
用非root用户跑容器之后,会遇到一个典型问题:模型缓存想写到某个目录,但权限不够,直接Permission denied。根源是Dockerfile里虽然创建了用户,但目录属主没切。解法就是在Dockerfile里把需要写入的目录chown给这个用户,或者用initContainer先改一遍数据卷的属主。千万不要因为懒得查权限就把用户改回root,安全底线会被突破。
4.4 扩容后冷启动时间太长
流量高峰时HPA触发扩容,新Pod拉起来后要加载十几GB的模型,前几分钟几乎无法应答请求。这会让扩容效果大打折扣。我的经验是双管齐下:一方面像前面说的,节点本地做模型缓存,甚至直接在节点池预加载;另一方面,给HPA设置更早的触发点,比如排队请求数超过4就扩容,趁流量还没完全打满,先把新副本预热起来。宁可多花一点GPU运行时间,也别让用户体验到“排队五分钟,超时一片”的情况。
4.5 推理时延抖动排查
时延抖动是线上最难缠的问题之一。有一次某模型P99时延突然从1秒飙到5秒,查了一圈发现是另一个团队的批量任务在同一时间挤到了同一张GPU卡上。因为GPU共享会引入算力争抢,推荐的做法是给不同模型或者不同团队打上不同的调度标签,让关键推理服务独占一部分算力节点,把批量任务和在线任务隔离开。还有一个隐蔽因素是CPU绑核,推理服务如果没绑定CPU,上下文切换也可能带来时延波动,在压测阶段就要留意。
4.6 常见问题速查表
| 问题现象 | 可能原因 | 优先排查命令/方法 |
|---|---|---|
| Pod 一直 Pending | GPU 资源不足 | kubectl describe pod <pod>看事件 |
| 显存 OOM | 并发过高或上下文过长 | 降低 max-model-len,控制并发 |
| 镜像拉取很慢 | 镜像过大 | 把模型权重移出镜像 |
| 目录 Permission denied | 非root用户无权限 | 检查Dockerfile USER与chown |
| 扩容后仍超时 | 模型冷启动 | 节点预热 + HPA前移 |
| 响应被缓冲导致断流 | Ingress开启buffer | 关闭proxy-buffering |
| GPU利用率低但时延高 | 算力争抢或网络瓶颈 | 隔离节点,检查VPC-CNI模式 |
5. 写在最后:经验沉淀比功能堆叠更重要
从集群规划到vLLM跑通,再到现在稳定扛住线上流量,我最大的体会是:VKE这类容器服务确实把Kubernetes的复杂度做了很好的封装,但工具只是给了你一套框架,真正决定稳定性的还是使用姿势。弹性伸缩的指标选得对不对、GPU共享的隔离做得好不好、模型权重的分发链路通不通,这些才是大模型推理落地成败的关键。
如果让我给正准备做这件事的人一个建议,那就是先把“最小可用的推理链路”跑通,再逐步加弹性、加多模型、加GPU共享。一上来就追求复杂的调度和优化,反而容易在生产里埋坑。先把一个7B模型稳定地变成API,你就已经跑赢了大多数还在文档里打转的团队了。