首页 / 资讯中心 / 文章详情

K8s集群部署DeepSeek推理服务:GPU配置与调度优化实战

K8s集群部署DeepSeek推理服务:GPU配置与调度优化实战 ★ FEATURED ARTICLE
K8s 集群上跑 DeepSeek 推理服务这套配置和调度优化方案我磨合了大半年总算稳定了。先说结论真正决定线上效果的往往不是模型本身而是 Pod 部署细节和 GPU 资源调度的精细度。这篇就从我的实际维护经验出发把关键配置、调度策略、遇到的问题和排查思路完整放出来给正在折腾或准备折腾这事的朋友一个参考。1. 先想清楚在 K8s 上跑 DeepSeek 到底图什么1.1 不是所有场景都需要上 K8s先说个实话如果只是自己开发机或者一台固定服务器上跑 DeepSeek 做实验K8s 纯属给自己找麻烦。单机部署拉起一个 vLLM 或者 Ollama 服务环境变量一配端口一开几十行命令就完事了运维成本最低。一旦进入团队协作、多模型并存或多业务复用的阶段裸机的短板就非常明显。我自己经历过的冲突是A 业务线要部署一个 70B 蒸馏模型做代码补全B 业务线要跑一个 7B 小模型做语义检索C 团队还要定期起批量任务做评测。三拨人盯着同一批 GPU 节点手动分配根本分配不过来。谁先抢到算谁的资源利用率低不说还经常出现某个团队把节点显存占满另一个团队的任务在排队等死的情况。这时用 K8s 的价值就出来了统一资源池、按命名空间隔离、配额管控、基于镜像的标准化部署、故障节点自动腾挪都是一次配置长期受益的事。尤其 DeepSeek 这类开源模型权重可以在内部统一存储和管理多环境基于同一个基础镜像起服务版本回滚也方便。1.2 部署 DeepSeek 的两种主流姿势我见过两种常见的部署方式先区分开后面所有配置都跟这个选择有关。一种是直接把 Hugging Face 格式的权重下载到一个存储位置然后用 vLLM、TGI、SGLang 这类推理框架加载权重并启动 OpenAI 兼容的 API 服务。这种方式适合生产环境吞吐量高、支持连续批处理、可以配合张量并行在多卡上跑大模型。这也是我主要采用的方式。另一种是用 Ollama 这类轻量运行时把模型封装成单机可用的服务。这种方式胜在部署极简但并发能力和高级调度能力弱一些适合内网小规模试用、开发联调不适合高并发的线上推理业务。两种方式在 K8s 里的部署形态也不同前者建议用 Deployment 配合 Service 暴露多副本加 HPA 弹性伸缩后者一般当成单实例服务不太需要复杂调度。提示决定用哪种方式之前先确认你们要部署的具体模型规模。如果是蒸馏后的小模型比如 7B、14B、32B 级别单卡甚至 CPU 都能跑如果是满血版那种千亿参数级别就必须考虑多机多卡、张量并行加流水线并行基础设施的复杂度会完全不一样。2. 部署前的硬件规划与容量评估2.1 显存估算别等 Pod 建起来才发现卡不够显存是 DeepSeek 部署里最容易踩坑的地方没有之一。很多人看到模型文件多大就觉得显存需求就是文件大小这个理解不完整。模型权重占的显存只是基础推理时还要额外留出 KV Cache、CUDA 上下文、中间激活值的空间。以 FP16 精度为例权重显存大约等于参数量乘以 2 字节。比如 70B 参数的模型FP16 权重就是 140GB 左右单张 80GB 的卡放不下至少要两张 80G 卡才能装权重而 KV Cache 和激活值还需要额外预留。这时候就必须做量化比如 INT8 或 INT4或者直接用多卡张量并行把权重切分到多张卡上。我在实际做容量规划时的估算公式很简单权重显存 参数量 × 精度字节数至少再额外预留 20%-30% 给 KV Cache。上线前先用单卡跑一个小并发压测观察显存峰值再按这个峰值配置 Pod 的 GPU 请求数量。千万不要只看理论值因为 vLLM 的 KV Cache 是按 max_num_seqs 和 max_model_len 参数动态预分配的你设置得越大它吃掉的显存越多。2.2 不要忽略 CPU、内存与磁盘这三个隐形瓶颈GPU 只是门槛CPU 和内存同样会卡脖子。DeepSeek 服务的计算链路里除了模型推理的算子还有 tokenizer 编解码、请求调度、以及 vLLM 的调度器线程这些如果 CPU 资源给不够GPU 利用率会被明显拖累。我的经验是每个 GPU 至少要配 8-16 个 vCPU 和 32-64GB 内存。不然后端日志里会频繁出现 CPU starvation 类似的警告GPU 利用率却始终上不去。内存方面特别要说一下 vLLM 的机制。它的 KV cache 有很大一部分会落在 CPUhost memory上作为 swap 空间尤其是当 GPU 显存不足时。如果容器内存 limit 给太小进程不会像传统应用那样优雅报错而是直接触发 OOMKilled。我个人碰到过好几次GPU 显存只用了 70%容器却被内核杀了排查了好久才发现是内存 limit 不够CPU offload 占掉了大量宿主机内存。磁盘更不能忽略。DeepSeek 权重文件大尤其是 70B 以上模型safetensors 文件动辄几十 GB 起步。如果每次 Pod 重建都要从对象存储下载权重启动时间可以拖到十几分钟甚至更久这对滚动更新和故障恢复来说不可接受。最好的做法是把权重放在多节点共享存储如企业内部的对象存储、分布式文件系统里Pod 启动时读取或者提前在每个 GPU 节点的本地磁盘上缓存好权重Pod 用 hostPath 或本地 PV 挂载。2.3 节点池划分GPU 节点最好打上标记在集群规划初期GPU 节点就应该和 CPU 节点分开管理。原因很简单GPU 节点上跑的是高优先级、高资源消耗的推理任务一旦混入普通 CPU 负载可能抢占 CPU 核和心理内存进而影响 GPU 推理线程的稳定性。我的做法是新建独立的 GPU 节点池给所有 GPU 节点打上专门标签比如 gpu-nodetrue同时给它们加上污点让普通业务 Pod 默认调度不上去。只有配置了对应容忍度的推理 Pod 才能真正使用 GPU 节点。这个隔离看起来简单但能避免大量莫名其妙的性能波动。后面讲 Pod 配置时也会再提到。3. Pod 部署配置一套稳定复用的 YAML 模板3.1 镜像选择与拉取策略的坑DeepSeek 的推理服务镜像我建议直接基于官方或社区维护的推理框架镜像来构建不要自己从零装环境。vLLM 这类框架对 CUDA、驱动、自定义算子的版本要求非常严格自己折腾依赖容易在集群里复制出各种诡异问题。我自己是把启动脚本、模型路径配置、健康检查逻辑打包进一个自定义镜像基础镜像则固定某个经过验证的框架版本不会频繁升级。镜像拉取策略有个细节推理镜像通常比较大如果 imagePullPolicy 设置不当滚动更新时每个节点都会重新拉一遍既占带宽又拖慢启动。我一般设置为 IfNotPresent并把镜像内容尽量做精简。模型权重不放进镜像否则镜像可能变成几十上百 GB拉取和存储都是灾难。权重统一走存储卷挂载。注意镜像里不要包含任何模型权重文件。权重单独放在存储中。否则每次镜像构建、推送、拉取都是一次大文件的拷机之旅集群规模稍大一点就非常痛苦。3.2 Request 与 Limit写错一个数就翻车很多人在写 Deployment 里的 resources 时CPU 和内存都是随便填这在 CPU 业务里问题不大但在 GPU 推理场景里这些数值直接影响 vLLM 内部的线程池配置和内存预分配策略还影响调度器对节点的选择。我推荐在推理服务里将 CPU 和内存的 request 与 limit 设置一致即使用 Guaranteed QoS 等级。原因有两点第一limit 与 request 一致后Kubernetes 调度器会优先把该 Pod 调度到资源充足的节点避免与其他 Pod 争抢第二vLLM 启动时根据容器的 CPU limit 决定并行 worker 数量如果 limit 写得过小实际可用 CPU 核数会小于预期GPU 利用率直接上不去。GPU 资源则不要写具体显存大小而是用 nvidia.com/gpu: 1 这样的整数卡数来申请。K8s 的设备插件机制只支持整卡资源不认 我只要 20GB 显存 这种请求。如果你确实需要只占部分显存得走 MIG、GPU 共享或虚拟化方案后面调度部分会展开讲。3.3 亲和性、反亲和性与拓扑管理把 Pod 放到正确的位置同为推理 Pod也有相互干扰的问题。比如两个模型服务同时调度到同一张 GPU 卡上其中一个显存需求特别大另一个很小即便加起来没超显存也会因为 GPU 的算力争抢互相拖慢。更常见的是多副本场景同一服务的多个副本如果都堆在同一台节点上一旦节点故障所有副本同时挂掉服务直接不可用。我的解决思路是双管齐下。第一使用 podAntiAffinity 让同一个服务的多个副本尽量分散到不同节点上实现高可用。第二对不同优先级的业务做节点级隔离核心业务打上专属调度标签只允许调度到特定的 GPU 节点组非核心业务则在另一组节点上运行。拓扑管理也需要提一下。在开启 CPU 管理策略的节点上如果对延迟极其敏感还需要考虑把 GPU 与 CPU 的 NUMA 拓扑对齐。对大多数推理场景来说这个不是必须的但如果是多卡张量并行跑大模型卡之间的通信频率非常高这时就要确保 Multi-NIC 和 RDMA 能力可用普通的千兆网卡会成为延迟瓶颈。3.4 存储、安全上下文与环境变量权重存储我前面已经强调了建议走 StorageClass 对应的 PVC 挂载到固定目录。有一点要注意PVC 的访问模式一般用 ReadOnlyMany多个副本可以同时挂载同一份权重避免每个副本单独复制一份。如果集群的存储不支持 ReadOnlyMany本地 SSD 加 DaemonSet 预加载也是一种务实方案。安全上下文方面推理服务通常只需要读取权重、监听端口所以设置 readOnlyRootFilesystem: true、drop 掉所有 capabilities、以非 root 用户运行都是合理的加固手段。不过要注意vLLM 等框架有时需要写临时目录记得单独给一个 emptyDir 挂载到 /tmp否则容器启动就会报权限错误。环境变量建议集中管理比如配置 model 路径、max_model_len、gpu_memory_utilization、tensor_parallel_size 这些关键参数不要写死在镜像里而是用 ConfigMap 管理。原因很简单修改参数不需要重新构建镜像只需重启 Pod 即可。对于频繁调整 max_model_len 这类参数的同学来说这个习惯能省大量时间。4. 资源调度优化让每一块 GPU 发挥最大价值4.1 默认调度器的问题只会拿整卡分配Kubernetes 默认调度器在 GPU 调度上非常简单粗暴按整数卡分配。它不感知显存大小、不感知 GPU 型号、不感知利用率。这意味着如果有两种 GPU一种 16GB 显存一种 80GB 显存默认调度器都认为是一张卡实际申请时可能把大模型任务调度到小显存节点上然后 Pod 一直 Pending。解决这个问题的通常做法是给 GPU 节点打标签比如 gpu-typea100-80g、gpu-typea10-24g并在 Pod 的 nodeSelector 里明确指定需要哪种节点。这样虽然笨但最直接有效。另一个问题是碎片化。假设节点有 4 张 80G 卡先有一个 Pod 申请了 1 张卡接下来一个任务需要 3 张卡才能跑默认调度器会怎么处理它不会自动把任务挤到同一个节点上完成 3 卡需求也不具备紧凑打包的算法结果可能是新 Pod 被调度到另一个有 4 张卡的节点前面那台节点的剩余 3 张卡反而被浪费。这种碎片问题在大模型多卡推理场景尤其突出。4.2 调度器增强从 Scheduler Framework 到自定义评分针对官方调度器的不足社区的常见思路有两条。一条是给调度器增加自定义扩展点让它在评分阶段优先选择能塞下整个 Pod 所需 GPU 的节点另一条是使用支持设备拓扑感知的调度器。我个人最常用的还是节点标签加 nodeSelector 的组合配合节点亲和性和 Pod 反亲和性能覆盖大部分场景。如果你的集群规模很大、GPU 型号很多、业务复杂才需要考虑引入更复杂的调度方案。这里的原则是能用简单办法解决就不要引入复杂组件每多一个控制面组件就多一份运维成本。4.3 GPU 共享、MIG 与时间切片到底怎么选在生产里你一定会遇到这种问题有一个小模型一张全卡根本用不满但独占一张卡又很浪费。这时 GPU 共享技术就派上用场了。三种主流方案的区别我先用一张表说明方案工作原理适用场景限制点NVIDIA MIG物理切分算力与显存硬隔离A100/A30/H100 等型号固定切分切分粒度固定无法动态调整时间切片多个 Pod 共用一张卡按时间片轮转低优先级、离线小任务隔离性弱显存共享容易互相干扰自定义 GPU 共享通过扩展设备插件实现显存细粒度分配内部工具化部署、测试环境依赖特定插件实现兼容性需验证我的实际建议是线上高优推理服务不要用时间切片因为延迟不可控两个任务同时跑在一个 GPU 上会导致推理耗时成倍增加。MIG 适合把大卡切成几个独立的小卡跑小模型但前提是模型权重和显存切分能够匹配。如果是开发测试环境时间切片倒是省钱利器反正对延迟不敏感。4.4 弹性伸缩HPA 与集群自动扩容的协同在线推理服务的流量不会一直平稳。白天办公时段请求量大晚间基本空闲。如果用固定副本数要么浪费 GPU要么扛不住高峰。我目前的方案是HPA 基于自定义指标每秒请求数、GPU 利用率、排队请求数做扩容同时配合集群级自动扩缩容。HPA 扩容后如果节点资源不足新 Pod 会 Pending这时集群自动扩缩容会为新 Pod 创建新的 GPU 节点。这里有个关键的坑大模型推理 Pod 的启动时间很长因为要加载几十 GB 的权重冷启动可能需要好几分钟甚至更久。HPA 的反应速度根本追不上突发的流量高峰。所以对核心服务我会设置一个 minReplicas保证至少 2 个或 4 个常驻副本再通过 PDB 保证滚动更新时不会全部中断。另外可以把 HPA 的扩容指标阈值调低一点比如 QPS 达到最大承载的 40% 就开始扩容用资源换稳定。4.5 多团队共用集群的配额与优先级管理当多个团队共用同一个 GPU 集群时协调就变得微妙起来。内部最容易出现的场景是某个团队提交了一个超大批处理任务把所有 GPU 都占满导致另一个团队的核心推理服务无法扩容。我吃过这种亏之后把资源配额和优先级管理纳入了日常规范。常规做法是在命名空间级别设置 ResourceQuota 和 LimitRange限制每个团队最多能申请多少 GPU、多少内存防止某个团队一次性把集群资源全占掉。同时引入 PriorityClass核心在线服务设置为高优先级离线批处理任务设置为低优先级。这样当资源不足时调度器会优先保证高优先级 Pod 的资源必要时可以驱逐低优先级的批处理任务。一个重要经验配额管的不是能有多少 Pod而是团队最多能占用多少资源。规划时要根据业务实际需求动态调整不要一刀切。我见过因为配额设得太死导致某个团队高峰期完全扩不了容的例子。5. 实操记录一份经过验证的 DeepSeek 推理部署方案5.1 基础组件安装任何 GPU Pod 要正常识别显卡前提是集群里已经安装了 NVIDIA 的 device plugin 组件。没有它你永远等不到 nvidia.com/gpu 这个资源。这个组件以 DaemonSet 形式运行在每个 GPU 节点上负责向 kubelet 上报 GPU 设备信息并完成设备分配。安装完成后用一个临时 Pod 验证一下资源识别是否正常kubectl run gpu-test --imagenvidia/cuda:12.4.0-base-ubuntu22.04 \ --restartNever --limits{nvidia.com/gpu:1} \ --command -- nvidia-smi如果输出里能看到显卡信息说明设备插件工作正常。这一步很基础但很多部署失败的问题都出在这里。5.2 核心编排一个可复用的 Deployment 示例下面这个 YAML 是我在实际项目里沉淀出来的核心模板去掉部分敏感配置后可以直接套用apiVersion: apps/v1 kind: Deployment metadata: name: deepseek-inference namespace: ai-service labels: app: deepseek-inference spec: replicas: 2 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app: deepseek-inference template: metadata: labels: app: deepseek-inference spec: priorityClassName: high-priority nodeSelector: gpu-type: a100-80g tolerations: - key: gpu-node operator: Exists effect: NoSchedule affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - deepseek-inference topologyKey: kubernetes.io/hostname containers: - name: inference image: registry.internal/ai/deepseek-runtime:stable imagePullPolicy: IfNotPresent resources: requests: cpu: 16 memory: 64Gi nvidia.com/gpu: 1 limits: cpu: 16 memory: 64Gi nvidia.com/gpu: 1 ports: - containerPort: 8000 name: http env: - name: MODEL_PATH value: /models/deepseek-70b - name: MAX_MODEL_LEN value: 8192 - name: GPU_MEMORY_UTILIZATION value: 0.9 - name: TENSOR_PARALLEL_SIZE value: 1 volumeMounts: - name: model-weights mountPath: /models readOnly: true - name: tmp mountPath: /tmp readinessProbe: httpGet: path: /health port: http initialDelaySeconds: 60 periodSeconds: 10 livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 120 periodSeconds: 30 securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: [ALL] volumes: - name: model-weights persistentVolumeClaim: claimName: deepseek-weights-pvc - name: tmp emptyDir: {}说一下这份 YAML 的设计思路。副本数设为 2 是我对在线推理服务的最低要求宁可资源利用率低一点也要求有冗余。podAntiAffinity 使用 preferred 而不是 required也是刻意的——required 会在节点数不足时直接导致 Pod 无法调度preferred 则会让调度器尽量分散实在没节点了也能将就。对可用性要求极高的服务可以把 required 和足够多的节点数配合使用。readinessProbe 的 initialDelaySeconds 设为 60是因为 vLLM 启动时需要加载权重这个过程通常要几十秒如果太早开始探测健康检查会失败导致 Pod 反复重启。livenessProbe 更保守120 秒后才开始探测避免启动期间的误杀。5.3 验证、暴露与压测部署完成后通过 Service 暴露服务。如果集群内部访问用 ClusterIP 就行外部访问可以用 Ingress 或者 Gateway。这里不太推荐把推理服务直接用 NodePort 暴露因为大模型推理的并发连接数很高NodePort 的负载均衡能力有限而且容易暴露节点 IP。压测时我会分两步走。第一步用一个小脚本打健康检查接口确认服务是真的可用而不是只起了容器。第二步用请求生成工具模拟真实并发流量观察 P99 延迟和 GPU 利用率。# 从 Pod 内确认 GPU 识别与加载状态 kubectl exec -it pod-name -- nvidia-smi # 模拟简单请求验证服务可用 curl -X POST http://service-ip:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {prompt: 你好介绍一下你自己, max_tokens: 256}常见压测错误是只看平均延迟不看长尾。大模型推理的 P99 延迟代表用户体验的下限如果 P99 明显偏高说明调度策略或资源分配还有问题。我会重点观察 GPU 利用率曲线和显存占用是否平稳而不是单纯看 QPS 高低。6. 常见问题与排查技巧实录6.1 Pod 一直 Pending怎么快速定位Pod 卡在 Pending 是最常见的现象。可能原因很多节点资源不足、找不到匹配的 GPU 型号、污点没消除、配额不够、PVC 无法挂载。快速定位的通用步骤是# 查看事件这是第一手信息 kubectl describe pod pod-name # 查看调度器是否给出了可选择节点 kubectl get nodes -l gpu-typea100-80g # 查看当前配额和已使用量 kubectl describe resourcequota -n ai-service事件里如果显示 0/N nodes available: insufficient nvidia.com/gpu说明 GPU 资源不足如果显示 node(s) didnt match nodeSelector说明节点标签不匹配如果显示 unexpected PVC说明存储没准备好。这些提示基本能覆盖九成的问题。6.2 容器 OOMKilled是内存不够还是显存爆了在 GPU 推理场景里OOMKilled 的原因通常不是 GPU 显存不足而是 CPU 内存不足这是个容易混淆的地方。GPU 显存不足时vLLM 会在日志里输出显存分配失败的报错进程不会立刻被 kill而 CPU 内存不足会触发内核 OOM容器直接被杀掉重启后的日志里只留下 exit code 137。我之前排查过一个反复重启的 Podkubectl describe 里看到 OOMKilled第一反应是加大显存结果发现 vLLM 日志里根本没有显存错误反而看到大量的 CPU swap 动作。最后把内存 limit 从 32G 提到 64G问题立刻消失。这个案例值得记住遇到 OOMKilled先看容器的内存 limit再排查显存。6.3 服务启动了但推理速度极慢服务能响应但慢得离谱通常是三个原因。第一GPU 利用率很高但实际只跑到几十瓦说明算力没有充分发挥。第二共享 GPU 导致的互相抢占尤其在使用时间切片时另一个任务把 GPU 算力吃满了。第三CPU 资源被限流vLLM 的调度线程等不到 CPU 时间片。排查时先看 nvidia-smi 里进程列表和 GPU 利用率再看容器 CPU 使用率是不是撞到了 limit。如果 CPU 使用率长期贴近 limit就得考虑放大 CPU 配额或者减少同节点上的密集任务。6.4 模型权重加载缓慢导致的滚动更新超时滚动更新时新 Pod 拉 PVC 权重可能需要好几分钟而 maxUnavailable 如果设置成 1会出现新旧版本交替过程中只有一个副本在用整体并发能力下降。如果这个时间暴露在业务高峰期会直接反映为请求超时。我的建议是给核心服务设置 maxUnavailable: 0宁可多占用一份资源也要保证滚动过程里始终有副本在服务。同时冷启动时间长的服务健康检查的 initialDelaySeconds 一定要给够防止出现旧 Pod 还没退、新 Pod 一直不 Ready的尴尬局面。6.5 排查工具与信息收集模板碰到疑难问题时我习惯一次性收集这些信息再定位# 1. 当前服务状态 kubectl get deploy,rs,pods -n ai-service -o wide # 2. 详细事件 kubectl describe pod pod-name # 3. 日志注意带上前一个容器的日志 kubectl logs pod-name --previous # 4. 节点状态确认 GPU 是否健康 kubectl describe node node-name | grep -A5 nvidia.com/gpu # 5. 如果启用了自动扩缩容看 HPA 配置 kubectl get hpa -n ai-service -o yaml这套组合基本能覆盖从调度失败、资源不足到运行异常的排查链路。结尾几个小建议最后分享点个人体会。跟 DeepSeek 这类大模型在 K8s 上的磨合过程真正花时间的其实不是写 YAML而是容量规划和调度策略的反复权衡。模型权重多大、跑在什么精度的卡上、要不要共享 GPU、副本数留多少、弹性伸缩怎么设这些问题没有标准答案只能根据自己集群的规模和业务场景慢慢调。我建议新上线的服务先从简单方案开始独立 GPU 节点、request 与 limit 一致、两副本起步、HPA 留够余量。稳定运行后再逐步引入 GPU 共享、重调度等高级特性。还有一个小技巧是上线前用模拟调度工具或者直接临时建几个测试 Pod把调度器选择的节点提前看一遍避免真正发布时才发现节点标签配错或者资源不足。这套思路可以复用到其他开源大模型的部署上希望这篇能帮大家少走点弯路。
阅读完成 · 觉得有帮助?
咨询建站