ARTICLE DETAIL

资讯详情

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

从单卡到千卡:大模型推理集群架构设计与 vLLM 生产实践

从单卡到千卡:大模型推理集群架构设计与 vLLM 生产实践 1. 为什么单卡推理撑不起生产环境一切的起点1.1 单卡推理的“作业模式”是怎么来的很多团队的起点其实都很朴素在手头的一台工作站上用一张消费级显卡或者一块 A100先跑起一个开源大模型测测效果。所谓单卡推理就是模型权重、KV Cache、和整个推理进程全都挤在一张 GPU 上。配合 Ollama 这类工具本地部署大模型让个人电脑智能化的体验很顺甚至一条命令就能把 7B 级模型拉起来输完提示词后看着 token 一个接一个蹦出来。单卡推理的优势非常明显零网络规划、零分布式概念、开发调试直观。但它本质上是一种“单人体验”。服务一个用户和服务一百个用户是完全不同的两件事。我之前见过不少团队用单卡做 demo感觉“效果不错”然后直接被线上流量教育了一顿——并发用户一多本来的流畅对话变得像拨号上网每输出一个字都要等很久。做生产级的推理服务我们要的不是“单条请求多快”而是整体吞吐、排队等待时间、尾部时延、以及多模型多版本下的稳定隔离。单卡推理适合个人项目、模型验证和离线评测一旦要收口给业务方调用就必须认真考虑大模型推理集群架构设计这件事。1.2 让我下决心做集群的三个信号我在实际监控后台里见过不少翻车现场。总结起来下面三个信号一旦出现基本说明单卡推理已经到瓶颈了。第一个信号是并发掉链子。单卡能承载的并发请求往往只是个位数因为每一个活跃序列都要占住一块 KV Cache。比如一个 7B 模型光权重在 BF16 下就要约 14GB如果上下文设到 32K几个并发用户就能把剩余显存吃光后面的请求只能在队列里苦等。第二个信号是上下文长度失控。业务方不会按你 demo 时的短文本长度来使用总结、Agent 多轮对话、长文档问答动辄上万 token。上下文越长KV Cache 占用越大单卡很快连一个请求都放不下。第三个信号是多模型、多版本并存。你不可能每次微调出一个新模型就把旧的停掉你需要灰度、回滚、不同模型入口单卡只布一个实例连滚动更新都不好做。所以从单卡到千卡本质上是把一个“能跑”的单机脚本改造成一个“可靠”的在线服务系统。集群化之后你还得考虑负载均衡策略、GPU 调度、容器化部署、指标监控这些原本不需要碰的东西。这也是我这篇文章想完整拆开讲的事情。2. 推理集群架构设计的核心拆解2.1 引擎选型为什么很多生产环境都用 vLLM集群里真正跑模型的是推理引擎不是裸的 PyTorch 脚本。当前主流选项主要是 vLLM、TensorRT-LLM、SGLang以及一部分人仍然在用的 transformers 直接跑。对我来说vLLM 在绝大多数场景下是最省心的起点。vLLM 的厉害之处在于 PagedAttention 和 Continuous Batching。PagedAttention 把 KV Cache 切成固定大小的块按需分配不再要求一整段连续显存显存碎片问题被压下去很多Continuous Batching 则让引擎每步迭代都动态调整 batch 里的序列有序列结束就补新序列显存利用率和吞吐量比静态 batch 高一大截。它还提供 OpenAI 兼容接口部署完业务方直接替换 base_url 就能接入这对企业大模型私有化部署来说非常友好。TensorRT-LLM 性能确实能更高尤其用上 FP4、FP8 和定制算子优化之后但它要求你对 CUDA、网络结构和模型导出有很深的理解迭代部署模型的速度也慢不少。SGLang 在多轮对话和 Agent 高频上下文复用场景里有优势它的 RadixAttention 能复用前缀 KV Cache但社区资料相对少遇到坑时排查难度高。引擎上手难度吞吐优化亮点典型适用场景vLLM低PagedAttention、Continuous Batching、内置 metrics大多数开源模型、API 服务、集群扩展TensorRT-LLM高图优化、量化、算子融合固定模型、追求极致吞吐、长期稳定运行SGLang中高RadixAttention 前缀缓存、Agent 场景优化多轮对话、多 Agent 调度选型建议很简单团队技术栈不深先用 vLLM性能确实上不去再逐步优化引擎不要一上来就挑战 TensorRT-LLM 的构建流程。后面我讲的参数调优也会一直拿 vLLM 举例。2.2 接入层与网关别把裸引擎直接暴露给客户推理集群的一个常见误区是所有人拿着 vLLM Pod IP 直接连。这样副本本身没有任何流量管理能力副本一重启 IP 就变客户端还得自己做故障转移更别提限流、鉴权、灰度这些能力。所以在 vLLM 之前必须有一层网关。网关要干的事包括统一入口、负载均衡、超时控制、鉴权与限流。LLM 请求是长连接且计算量大网关不能只做一个简单转发。我曾经见过 Nginx 默认轮询把所有长请求平分结果一台 GPU 上全是大 context 长任务其他 GPU 全是短任务看起来“连接数均匀”实际算力明显歪掉。正确的负载均衡至少要考虑两种维度一是连接数或请求数二是每个副本当前的排队状态。vLLM 会在/metrics暴露vllm:num_requests_waiting、vllm:num_requests_running这类指标网关定期拉取选排队最少的副本转发效果远好于静态的随机和轮询。策略原理适用场景注意事项轮询请求均匀分到每个副本请求长度均匀、副本规格一致长尾请求会导致负载倾斜最少连接选择连接数最少的副本中等并发下比较简单不反映队列深度和显存占用基于队列深度轮询副本指标选排队数最少者LLM 集群推荐需要网关能访问/metrics自适应加权根据历史时延和错误率动态加权复杂生产环境实现成本高优先做前几种对于中小规模用 K8s Service 加 Nginx 的 least_conn 已经能打到了多节点、上百副本规模我建议在执行 Nginx 或 Envoy 之前先做一层“调度路由服务”专门轮询各个 vLLM 实例的排队指标再精确转发。2.3 部署形态一个模型一个 Deployment在 K8s 上我会按“一个模型一个 Deployment”的组织方式来管理。每个 Deployment 的副本就是一个 vLLM 实例占用一张 GPU或者一组 TP 的几张 GPU。模型通过 Service 暴露业务方只认 Service 域名不感知背后的副本增减。一个非常重要但容易被忽略的原则一块物理 GPU 默认只跑一个推理 Pod。不要在同一个 GPU 里塞两个 vLLM 进程否则显存互挤CUDA 上下文切换和 KV Cache 碎片会让你排查到头秃。多模型要共享一台机器应该通过 K8s 调度器把它们分配到不同 GPU 上依靠nvidia.com/gpu: 1的资源限制完成绑定。企业私有化部署时还会有“独享 GPU 节点”的需求因为有的客户要求模型数据不出内网。此时可以通过节点标签nodeSelector把某一组 Deployment 固定到特定 GPU 节点池再从网关层面做租户隔离。这个架构和上一节的网关设计天然打通一道网关认证再按模型路由到不同 Deployment。3. 从单卡到多节点的关键架构模式3.1 并行怎么选张量并行不是越多越好从单卡到集群很多人第一反应是把模型切到几十张卡上行不行这就要说到张量并行Tensor Parallelism和数据并行Data Parallelism的区别。张量并行是把一个 Transformer 层的矩阵权重切成多份分别放在不同 GPU 上前向计算时通过集合通信合并结果。模型权重超过单卡显存时TP 几乎是必须的比如一个 72B 模型在 80G 显存上连 BF16 加载都危险可以考虑用 8 卡 TP。但 TP 会显著增加通信量每层 Attention 和 MLP 都需要一次 AllReduce层数越多通信放大越明显。跨节点做 TP 时网络带宽是关键。单机内卡间走 NVLink跨节点走 InfiniBand 或 RoCE。如果只有普通 25G 以太网强行把 TP 扩展到跨节点你会发现 8 卡反而比 4 卡更慢。所以实操里可以定一条铁律TP 尽量限制在单节点内跨节点扩展用“数据并行加副本”的方式也就是一个模型多个 Pod各自用整卡或单机 TP前面靠负载均衡分流。并行方式干什么用通信开销适合场景数据并行同模型多副本各处理不同请求低推理时基本无同步在线推理主力张量并行单模型拆到多卡高每层都通信模型超过单卡显存流水线并行按层切分微批次流水中高离线训练多推理少用千卡集群最终形态通常是“数据并行为主TP 只在节点内使用”。你可以在一个节点上放一个 TP8 的巨型副本也可以放四个 TP2 的小副本完全取决于模型大小和请求并发。对大多数开源 7B 到 32B 级别模型TP1 或 TP2 就够副本数量可以拉很多负载均衡压力主要交给网关。3.2 连续批处理与显存调度让每张卡都忙起来单卡推理时很多框架采取静态 batch一批请求必须一起开始、一起结束中间如果有短请求先处理完整批还要等最长的那个。vLLM 的 Continuous Batching 打破了这种限制把批处理颗粒度缩小到迭代级每步解码后都可以把完成的序列剔出把新请求拉进来。这个机制对集群的直接影响是每个副本的吞吐上限不是由“最大并发数”决定而是由显存里能塞下的 KV Cache 总量决定。因此调参数时不光要看模型权重占多少显存更要看上下文长度上限和最大并发序列数。我在一次压测里遇到过有趣的情况单副本 max-num-seqs 设置得非常大外层网关把所有请求都堆到一个 Pod结果这个 Pod 频繁做抢占和重算吞吐反而比限制并发数时更低。后来我把 max-num-seqs 调到合理档位多余流量自动被网关分配到其他副本整体 QPS 反而上去了。这也说明推理集群的负载均衡不能只看网络层连接数得把显存里的计算队列也考虑进来。3.3 LLM 专属的负载均衡短请求和长请求根本不是一类流量通用负载均衡器通常会根据连接数、CPU 使用率等指标做分配。但大模型推理有一个显著特点一条请求的执行代价未知可能只有几十个 token也可能要生成几千个 token两者消耗的时间相差一两个数量级。如果只用“请求次数”做负载均衡就会遇到前面说的“连接数均匀、算力不均”问题。更靠谱的做法是引入队列深度指标每个 vLLM 副本暴露 running 请求数和 waiting 请求数网关选择一个总排队长度最小的副本转发。这就像餐馆的引导员不会只看每一桌坐了人没有还要看哪些桌刚点完大菜、哪些桌快吃完正收拾——把新客人带进最快能接单的那一桌。还有一个更进阶的方向是 prefill 和 decode 分离。Prefill 阶段是密集矩阵运算主要吃算力Decode 阶段是自回归一步步出 token更多消耗显存带宽和 KV Cache。大厂在超大规模集群上会把这两个阶段拆到不同 GPU 池子通过专门的调度器协调。对大多数 27B 以下模型组成的集群这个复杂度暂时不值得先用排队深度调度就能获得 80% 的收益。3.4 自动伸缩要做“读秒级”不要做“分钟级”千卡集群的难度不在于部署一千张卡而在于让资源跟着流量变化自动伸缩。如果业务有典型高峰你不可能人肉半夜爬起来扩容。K8s HPA 默认只消费 CPU 和内存指标对 GPU 推理没有感觉我在生产集群用的办法是直接采集 vLLM 的队列指标做扩缩容。判断扩容的条件有三个常用指标单副本的vllm:num_requests_waiting、请求在队列中的排队时间、以及尾部时延。比如当某个 Deployment 所有副本平均 waiting 数量超过 20 持续 30 秒就加一个副本当 waiting 降回 5 以内持续 5 分钟再缩一个副本。缩容比扩容更需要谨慎。GPU 上有正在执行的请求不能像 Web 应用那样随意杀掉 Pod否则所有用户都会在生成半截时断线。K8s 滚动更新和 HPA 缩容默认会优雅终止 Pod但如果 vLLM 没有在几十秒内完成当前请求Pod 还是会被强杀。我建议把容器的 terminationGracePeriodSeconds 设到 120 秒以上并且让网关在发现 Pod 进入 Terminating 状态时不再给这个副本派发新流量。4. 实操过程从单卡到千卡的落地细节4.1 先用一张卡测清上限别拍脑袋设计集群任何集群设计都该从基准开始否则后面全是拍脑袋。我会先拿一张 GPU 部署目标模型用真实业务的长文本和短文本混合作压测记录单卡吞吐和数据点。比如用 vLLM 部署一个 7B 模型到 A100 80Gvllm serve Qwen/Qwen2.5-7B-Instruct \ --port 8000 \ --tensor-parallel-size 1 \ --max-model-len 32768 \ --gpu-memory-utilization 0.85 \ --max-num-seqs 16这个配置下权重占掉 14GB 左右剩下的显存基本留给 KV Cache 和 CUDA context。短文本并发压测单副本输出吞吐大致在 1000 token/s 上下输入输出混合场景会降到几百 token/s。如果把 max-num-seqs 继续加大到 64吞吐不一定线性上涨因为显存和计算资源会互相挤兑。并发数每请求平均生成长度单卡吞吐token/s尾时延p954512700-10002-3s85121000-15003-5s165121200-18006-10s32512可能因抢占反而下降15s这些数字只是量级参考。拿到单卡上限就可以倒推集群规模比如目标峰值是每秒 20 个请求每请求平均生成 500 token那纯 token 吞吐就要 10000 token/s至少需要 8-10 个上述副本。所谓千卡架构不是一开始就铺 1000 张卡而是先算清增长曲线再决定节点容量和网关配置。4.2 K8s 里跑 vLLM 的 Deployment 配置集群化第一步是把 vLLM 镜像打成容器以 Deployment 方式跑在 K8s 上。一个 GPU Pod 只布一个推理引擎资源声明里指定nvidia.com/gpu: 1K8s 的 NVIDIA Device Plugin 会把物理 GPU 调度进容器。apiVersion: apps/v1 kind: Deployment metadata: name: qwen-7b-infer spec: replicas: 3 selector: matchLabels: app: qwen-7b-infer template: metadata: labels: app: qwen-7b-infer spec: terminationGracePeriodSeconds: 120 containers: - name: vllm image: vllm/vllm-openai:latest args: - --model - Qwen/Qwen2.5-7B-Instruct - --port - 8000 - --gpu-memory-utilization - 0.85 - --max-num-seqs - 32 ports: - containerPort: 8000 resources: limits: nvidia.com/gpu: 1资源字段我只写 limits 不写 requests因为 GPU 不适合和 CPU 一样做超卖。不要在一张卡上 requests 写 0.5 然后塞两个 Pod那会让 CUDA 的并发上下文争抢显存行为非常不可控。更稳妥的方式是给节点打 label比如gpu-typeA100、gpu-size80G然后在 Deployment 里用 nodeSelector 明确选择。副本怎么暴露最开始的方案是建一个 Service把多个副本统一到一个虚拟 IP 上。普通 ClusterIP Service 默认走轮询压测阶段够用生产环境最好在前面再加一层流量管理网关。4.3 网关加载多副本的标准配置多副本架起来以后我通常会在 vLLM 前面加两层一层做负载均衡和健康检查一层做统一 API 网关。负载均衡层可以直接用 Nginx 或 Envoy健康检查路径用/health它返回 200 时表示引擎活着且能接收请求。kubectl apply -f deployment.yaml kubectl expose deployment qwen-7b-infer --port 8000 --target-port 8000Nginx 配置里用least_conn而不是默认轮询并且开启被动健康检查。请求打进 Service 时Nginx 把连接数当成当前在途请求数来调度这个方案部署简单、也能撑住大多数场景。但要是遇到明显的长短请求不均就得升级成动态队列深度调度。动态调度的做法并不神秘写一个轻量服务定期请求每个 Pod 的/metrics解析出vllm:num_requests_waiting和vllm:num_requests_running然后把当前 HTTP 请求转发给综合负载最小的实例。这个路由服务本身无状态可以多副本跑。我在压测过千 QPS 时这个调度器延迟几乎可以忽略反而解决了很多长尾倾斜问题。4.4 千卡集群里反复调过的 vLLM 参数当节点变多光用默认参数已经不够了。我梳理了几个生产环境中影响最大的 vLLM 参数它们对集群整体表现影响最大。gpu-memory-utilization默认通常 0.9但我建议留 0.85 到 0.9 之间。留出的空间给 CUDA context、调度器和量化临时 buffer别把显存塞满否则模型加载或显存碎片稍微变大就直接 OOM。max-num-seqs决定每个引擎最多并发处理的序列数。调大能提高吞吐但会挤占 KV Cache极端情况下触发抢占重算吞吐反而下降。实际运行中我会观察指标再调整从 16 开始逐步往上加压测曲线出现拐点就停下来。max-model-len决定允许的最大上下文长度。设得过高会让每个序列的显存分配留很大冗余设得过低又会直接拒绝长文本请求。必须根据业务分布设置不能拍脑袋写 128K。enable-prefix-caching值得关注。如果模型是聊天和 Agent 场景建议开启前缀缓存。相同 system prompt、相同历史上下文的请求会复用 KV Cache能明显降低多副本负载。开启后要留意显存占用率因为缓存也占显存。所有这些配置都不应该手工改一遍再重启一遍。生产环境建议用 Helm Chart 或 GitOps 管理参数变更走评审和灰度发布先在少量副本验证再全量滚动。推理集群的“千卡”不是多点几次扩容按钮而是把参数、镜像、流量调优都沉淀成可重复的流程。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因处理建议GPU 显存剩余不少却 OOMmax-model-len 或 max-num-seqs 过大KV Cache 预分配过多降并发数或缩小 max-model-len多副本下某台 GPU 总是排队负载均衡只看连接数没看队列深度改用基于 queue depth 的调度器TP 跨节点训练经常超时网络带宽不足或 NCCL 配置有问题避免跨节点 TP配置 NCCL 超时参数扩容后吞吐不升反降模型并行度太高通信成为瓶颈检查 TP 大小减小并行度增加副本Pod 缩容导致用户断流优雅终止时间太短请求没跑完terminationGracePeriodSeconds 设到 120s模型加载慢且 GPU 利用率低权重加载瓶颈或者 CPU 瓶颈预热权重开启 prefix caching减并发排查时不要一上来就查模型代码。先看 GPU 利用率、显存占用、网络收发速率和num_requests_waiting四个基础指标大部分问题都出在配置层面而不是模型本身。5.2 两个最容易忽视的坑第一个坑是健康检查只看“进程存活”不看“引擎可用”。vLLM 的/health只能告诉你进程没挂但引擎可能正在被一批超长请求塞满新请求进去要排很久。网关层要额外加一个“过热熔断”逻辑当队列深度超过阈值时暂时把副本从负载池摘掉等队列恢复后再放进来。第二个坑是 CCU context 和显存碎片的隐性消耗。多副本部署后模型权重、CUDA module、TensorRT 缓存都会占一部分显存这些并不体现在模型参数量里。所以 gpu-memory-utilization 留 15% 左右非常必要尤其在 4060、3090 这类消费级显卡上显存管理比 A100 更敏感。另外很多团队搞出千卡以后第一眼看的指标还是“显卡利用率”。但在 LLM 推理里算力利用率只是参考真正体现服务质量的指标是 p95 时延、队列深度、吞吐 token/s、以及抢占重算次数。把监控面板按这个思路去搭问题定位会快非常多。6. 一点个人经验千卡不是目的稳定吞吐才是从单卡推理一路走到千卡规模我最大的体会是架构设计的核心不是把一千张卡接起来而是让每一张卡都在合适的负载区间工作。我自己的建议是第一版集群不要超过 8 个副本先把网关、队列指标、健康检查、参数调优这些基础能力跑顺。等业务流量真的把 8 个副本打满你再按数据扩容到几十、几百张卡。扩容途中会发现绝大部门性能问题不是网卡不够快而是 KV Cache 调度不合理或者负载均衡器选择了“看起来平均、实际上倾斜”的方式。还有一点要提醒微调完的新模型上线千万别直接替换正在服务的 Deployment。哪怕是同结构模型也应该走新 Deployment 灰度发布流量逐步切换。否则一旦新模型生成质量有问题回滚起来很痛苦。这套流程和 Web 应用发布很像但 GPU 资源更贵发布失误的代价也更大。最后再分享一个小技巧给每个副本加上标准的 startup 和 readiness 探针但不要把所有流量依赖都放在 K8s 自带的探针上。自己写一个轻量调度器用 vLLM 的 metrics 做真实路由判断是我做过最值的一项投资。它能让你在“千卡负载均衡”这件事上真正掌握控制权。
返回列表