2.2 用 Deployment 把 vLLM 跑起来:逐行拆解那几个会埋雷的参数 读者读完这节能带走什么:你会拿到一份能直接改用的 vLLM Deployment,并且真正理解每个关键参数背后的物理约束——为什么探针延时设短会把服务拖进重启死循环、为什么张量并行数必须和 GPU 申请数严格一致、为什么上下文长度不能一上来就拉满。看完你能自己判断「照抄的这份 YAML 哪里会炸」。 上一节我们把权重稳稳放到了持久化存储里。现在权重就位、镜像预热,终于可以动手让 vLLM 真正跑起来。这一节是整章的核心,因为它决定了你的推理服务是「跑得起、稳得住」还是「一重启就崩」。我会给一份完整的 Deployment,然后逐个拆解那几个 90% 的人会设错的参数。
读者读完这节能带走什么:你会拿到一份能直接改用的 vLLM Deployment,并且真正理解每个关键参数背后的物理约束——为什么探针延时设短会把服务拖进重启死循环、为什么张量并行数必须和 GPU 申请数严格一致、为什么上下文长度不能一上来就拉满。看完你能自己判断「照抄的这份 YAML 哪里会炸」。
上一节我们把权重稳稳放到了持久化存储里。现在权重就位、镜像预热,终于可以动手让 vLLM 真正跑起来。这一节是整章的核心,因为它决定了你的推理服务是「跑得起、稳得住」还是「一重启就崩」。我会给一份完整的 Deployment,然后逐个拆解那几个 90% 的人会设错的参数。请务必读到最后,因为最致命的坑往往藏在最不起眼的一行里。
我们先把完整的骨架摆出来,建立整体印象,再逐段深入。这份配置假设权重已经通过上一节的方式挂载到了 /models/glm-5.2,单机单卡起步:
apiVersion: apps/v1 kind: Deployment metadata: name: glm52-vllm labels: app: glm52-vllm spec: replicas: 1 selector: matchLabels: app: glm52-vllm template: metadata: labels: app: glm52-vllm spec: containers: - name: vllm image: vllm/vllm-openai:latest args: - "--model" - "/models/glm-5.2" # 指向已就位的本地权重路径 - "--served-model-name" - "glm-5.2" # 对外 API 里暴露的模型名 - "--tensor-parallel-size" - "1" # 必须与下方 GPU 申请数一致 - "--max-model-len" - "32768" # 先用较短长度跑通, 再逐步放开 - "--gpu-memory-utilization" - "0.90" # 显存使用上限比例 resources: limits: nvidia.com/gpu: 1 # 与 tensor-parallel-size 对齐 ports: - containerPort: 8000 volumeMounts: - name: weights mountPath: /models readOnly: true readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 300 # 大模型加载慢, 别设太短 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 600 # 比 readiness 更宽松 periodSeconds: 20 volumes: - name: weights persistentVolumeClaim: claimName: glm52-weights
这份配置看起来平平无奇,但每一个我加了注释的地方,都是真实部署里能把人坑到怀疑人生的点。下面逐个拆。
我把这个放在第一位,因为它是最高频、也最隐蔽的坑。
Kubernetes 有两种健康探针:readinessProbe(就绪探针)决定 Pod 是否可以接收流量,livenessProbe(存活探针)决定 Pod 是否需要被杀掉重启。它们都会周期性地去访问你配置的健康检查地址(这里是 /health)。
问题来了:GLM-5.2 从磁盘加载到显存需要相当长的时间,可能是几十秒到几分钟,取决于模型体积和存储读速。而探针的 initialDelaySeconds(首次检查前的等待时间)如果设得太短——比如很多教程里默认的 10 秒或 30 秒——就会发生这样的惨剧:
模型还在加载,/health 还没起来,存活探针检查失败,Kubernetes 认为这个 Pod 坏了,把它杀掉重启;重启后模型又要重新加载,探针又在加载完成前失败,再杀再重启……于是你的 Pod 陷入 CrashLoopBackOff,永远起不来。你去看日志,甚至看不到明显报错,因为进程根本是被「误杀」的。
正确做法:initialDelaySeconds 必须留足模型加载时间。宁可设保守一点,比如 readiness 给 300 秒、liveness 给 600 秒(存活探针要比就绪探针更宽松,避免刚就绪就被误判)。具体数值要根据你实测的加载时间来定:观察一次正常加载要多久,然后在此基础上留出充分余量。记住一个原则:探针延时宁长勿短,短了会死循环,长了最多是就绪慢一点。
第二个高频坑,藏在 --tensor-parallel-size 和 resources.limits.nvidia.com/gpu 这两个值的关系里。
--tensor-parallel-size(张量并行数)告诉 vLLM 要把模型切成几份、分摊到几张 GPU 上并行计算。而 nvidia.com/gpu 告诉 Kubernetes 给这个 Pod 分配几张卡。这两个数字必须严格相等,否则会出两类问题:
tensor-parallel-size 大于分配的 GPU 数:vLLM 想用 4 张卡切模型,但 Pod 只拿到 2 张,启动直接失败,报找不到足够的 GPU。tensor-parallel-size 小于分配的 GPU 数:比如申请了 4 张卡但只设了并行数 1,那么 vLLM 只用其中 1 张,剩下 3 张被这个 Pod 独占却闲置——白白浪费了昂贵的显卡资源,而且别的 Pod 还调度不进来。所以规则很简单:单机单卡就都填 1;如果一张卡显存放不下整个模型,需要跨多卡张量并行,就把两个值同时改成对应的卡数,一一对齐。这是最不该错、却经常因为改一处忘改另一处而翻车的地方。
第三个坑关乎 --max-model-len,也就是模型支持的最大上下文长度。
GLM-5.2 的一大卖点是超长上下文(据官方为百万级 token 量级,具体以官方为准)。看到这个数字,很多人第一反应就是「那我就把 --max-model-len 设成最大值,把能力全开出来」。这是个代价惨重的误会。
原因在于 KV Cache(键值缓存)的显存开销。推理时,模型需要为上下文里的每一个 token 缓存中间结果,这块缓存的显存占用随着上下文长度线性甚至更陡地增长。当你把 max-model-len 拉到百万级,vLLM 会预留出能支撑这么长上下文的显存空间,结果很可能是——模型刚加载完,显存就被 KV Cache 预留吃光,直接 OOM(显存溢出),服务起都起不来。
正确做法是「先短后长,按余量放开」:先用一个较小的值(比如 32k)把服务跑通,观察显存实际占用;确认稳定后,再根据剩余显存逐步往上调。同时配合 --gpu-memory-utilization(显存使用上限比例,比如 0.90 表示最多用 90% 显存,留 10% 缓冲)来防止把显存吃到爆。
这里我要给一个明确主张:绝大多数真实业务并不需要百万级上下文。你的用户问答、文档分析可能几千到几万 token 就够了。为了一个用不上的极限能力,牺牲服务的稳定性和并发能力,是典型的本末倒置。先问清楚业务真实需要多长的上下文,再据此设定,才是专业做法。
还有两个不致命但很膈应人的细节,顺带说清楚。
一是 image 里的 tag。示例用了 :latest,这在学习阶段没问题,但生产环境强烈建议锁定具体版本号。:latest 会随着上游更新而变化,某天节点重新拉镜像时可能拉到一个行为不同、甚至不兼容当前配置的新版本,让你的服务在毫无预警的情况下出问题。锁版本是生产的基本素养。
二是 --served-model-name。这个参数决定了你对外 OpenAI 兼容 API 里那个 model 字段的名字。如果不设,默认会用 --model 的完整路径(比如 /models/glm-5.2),客户端调用时就得写这么一长串,很别扭。显式设成一个干净的名字(如 glm-5.2),客户端体验会好很多,后续换模型路径时也不影响调用方。
还有一个容易被新手忽略、却会影响调度行为的点:GPU 这种扩展资源和 CPU、内存不一样。
对 CPU 和内存,你可以分别设 requests(调度时保证的量)和 limits(运行时的上限),两者可以不相等,允许超卖。但 nvidia.com/gpu 这类扩展资源不支持超卖:它的 requests 和 limits 必须相等(你只写 limits 时 Kubernetes 会自动把 requests 设成相同值)。一张卡要么整张给一个 Pod,要么不给,不存在「半张卡」的默认分法(MIG 和时间切片是另外的共享手段,留到第三章详谈)。
这带来一个现实后果:GPU 是整张独占的昂贵资源,你的副本数直接受限于集群总卡数。如果你总共只有 4 张卡,那单卡副本最多跑 4 个,多一个副本就会因为抢不到 GPU 而一直 Pending。新手常常因为看到 Pod 卡在 Pending 而四处排查,其实只是卡不够了。部署前先用 kubectl describe node 看清楚每个节点的 nvidia.com/gpu 总量和已分配量,心里先算好账,能避开很多无谓的困惑。
配置写好、kubectl apply 之后,别急着接流量。养成一个检查节奏:
先用 kubectl get pods 看 Pod 状态是不是 Running 且就绪;再用 kubectl logs 看 vLLM 的启动日志,确认权重加载成功、没有 OOM、GPU 被正确识别;然后在集群内用一个临时 Pod 或端口转发,向 /v1/chat/completions 发一个最简单的测试请求,确认能返回结果。这三步都过了,才说明服务真正跑通了,可以进入下一步的暴露和路由。
这个「先看状态、再看日志、最后打一枪测试」的节奏,能帮你把问题拦在接入流量之前,而不是等真实用户请求进来才发现服务是坏的。
再补一个实用技巧:验证时用的测试请求,最好带上一个稍长的 prompt,而不是只发「你好」这种一两个 token 的短句。原因是短请求几乎不占 KV Cache,就算你的 --max-model-len 设得不合理,短请求也照样能返回,给你一种「服务很健康」的错觉;等真实的长上下文请求进来,才暴露出显存不够的问题。用一个接近真实业务长度的请求去测,才能提前把显存相关的坑试出来。
这一节我们跑通的是单机单卡单副本。在你急着把 replicas 从 1 改成更多之前,有三件事必须先想清楚,否则多副本只会把问题放大。
第一,显卡够不够。前面说过 GPU 整卡独占,副本数直接受限于总卡数,先确认你有足够的卡,再谈扩副本。
第二,权重存储能不能支撑多副本同时读。如果你用的是 ReadWriteOnce 的 PVC,多个节点上的副本根本挂载不了同一块卷,这时要么每个副本各自准备权重,要么改用支持多读的共享存储——这正是上一节讲的存储选型会反过来约束扩展性的地方。
第三,会话上下文会不会丢。多副本意味着请求会被分发到不同的 Pod,如果一个多轮对话的前后请求落到了不同副本,而副本之间不共享上下文缓存,就会出现「AI 突然忘了刚才说过什么」的诡异现象。解决它需要在前面加一层带会话亲和的路由,这也是下一节要展开的重点。
把这三件事想清楚,你的多副本才是「稳步扩展」,而不是「把单点问题复制了好几份」。
initialDelaySeconds 宁长勿短:短了会因模型加载未完成被误杀,陷入重启死循环;这是最高频的坑。--tensor-parallel-size 必须与 nvidia.com/gpu 申请数严格一致:大了启动失败,小了浪费显卡。--max-model-len 先短后长:百万级上下文会让 KV Cache 吃光显存导致 OOM,绝大多数业务用不上极限长度,按显存余量逐步放开。latest;显式设 --served-model-name 让 API 调用更干净。现在服务已经在集群内跑起来了,但集群外还够不着它。下一节我们讲怎么用 Service、Ingress 把这个推理服务安全地暴露出去,以及多副本时怎么做请求路由和会话亲和。