尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从单卡到千卡:大模型推理负载均衡与架构演进实录

从单卡到千卡:大模型推理负载均衡与架构演进实录 今年有个项目让我印象很深第一版推理服务就是一张 A100 顶一个实例前面挂个 Nginx 做轮询听起来挺正常对吧灰度放量那天下午p95 首 token 延迟从 800ms 直接飙到 9 秒监控告警还没响工单电话先到了。后来复盘才发现问题根本不在算力而在调度——大模型推理的请求根本不像传统 Web 请求那样短平快负载均衡的策略、KV Cache 的复用、实例之间的互踢每一项都能把集群拖垮。这篇文章我想把从单卡推理到千卡负载均衡这条路上踩过的坑、算过的账、验证过的方案完整梳理一遍。写出来主要是给两类人看一是手里已经有模型、正准备做推理服务化的工程师二是已经在跑多卡集群但被延迟毛刺和扩容事故折磨的运维/架构同学。文里的估算逻辑、架构分层和调度策略都是我实际部署中验证过的东西有数据、有对比、有翻车记录可以直接拿去参考。1. 先算明白一张卡能扛什么显存、带宽与算力的三角账很多人上来就问这张卡能不能跑 Qwen 27B或者K100 单卡跑量化版 27B 速度快不快。但能跑和能扛流量是两码事。我在实测中见过有人用单卡 4-bit 量化跑通 27B 模型generate 速度看着还行结果一旦并发上来延迟立刻崩。原因很简单大模型推理不是只有权重那么点显存需求KV Cache、并发槽位、prefill/decode 的资源争抢这些才是真正的瓶颈。1.1 速度到底怎么算prefill 看算力decode 看带宽生成式推理分成两个阶段资源瓶颈完全不一样prefill处理输入把用户的 prompt 一次性算完属于典型的计算密集型。每 token 的计算量可以用 2×N 来估算N 是模型参数量。27B 模型每处理一个 token 大约要算 54 GFLOPs。单张 H100 的 FP16 稠密算力约 990 TFLOPS按 60% 的实际利用率算每秒能 prefill 约 11000 token一个 2000 token 的长 prompt 大约 0.18 秒。如果是量化模型prefill 速度还会被反量化开销拖累但总体仍然算力够用。decode逐 token 生成每生成一个新 token 都要把整个权重读一遍做矩阵向量乘瓶颈彻底变成显存带宽。H100 显存带宽约 3.35 TB/s4-bit 量化后的 27B 权重约 13.5GB理论极限是每秒 248 token但实际还要算上 GQA、kernel 启动、多 batch 混跑的开销单用户能拿到 30~60 token/s 已经是正常水平。A100 80G 带宽约 2TB/s理论上限约 148 token/s实测单请求大多在 20~40 token/s 这个区间。所以判断单卡快不快的第一件事是搞清楚你测的是哪个阶段。很多人只报生成速度但没告诉你 prompt 有多长、并发是多少、是不是用了投机采样。我在压测时习惯分开记录 TTFT首 token 延迟和 TPOT每 token 间隔这两个数字混在一起啥也看不出来。1.2 显存账权重只是门票KV Cache 才是大头权重能不能塞进显存只是第一关真正的资源大头是 KV Cache。每个请求在处理过程中都需要保存历史 token 的 Key 和 Value这套缓存的大小可以用下面这个公式粗算def estimate_kv_cache_bytes_per_token( num_layers: int, kv_heads: int, head_dim: int, bytes_per_element: int 2, # fp16 ) - float: elements_per_token num_layers * kv_heads * head_dim * 2 # K和V各一份 return elements_per_token * bytes_per_element / 1024 # 单位KB # 以27B模型为例64层、GQA 8个KV头、head_dim 128 per_token estimate_kv_cache_bytes_per_token(64, 8, 128, 2) print(f每token KV Cache: {per_token:.1f} KB) # 实际输出约 256 KB/token注意这个例子是一个偏保守的 GQA 配置。如果模型用的是 MHA每个注意力头都是独立 KV这个数字要乘好几倍。按上面的保守估算一个 2K context 的请求要吃掉约 512 MB不对——256 KB × 2048 ≈ 512 MB我重新算一下256KB × 2048 524288 KB 512 MB。是的单个 2K context 的请求约 512MB KV Cache。8K context 就是 2GB。这一点非常反直觉推理服务往往会同时维持几十上百个并发请求每个请求的 context 还在不断边长KV Cache 的累计占用很容易把显存吃穿权重反而只占一小块地方。更具体的账一张 80G 的 H1004-bit 27B 权重 13.5GB剩下的约 66GB 理论上能放约 130 个 2K context 请求的 KV Cache512MB × 128 个 ≈ 64GB。但如果 context 涨到 8K同样的 66GB 只能放约 33 个请求。这就是为什么有些单卡服务并发一高就 OOM——不是你显存不够是每个请求的 KV Cache 比你想象的大得多。1.3 单卡并发槽位的真相为什么 20 路并发就卡每张卡能同时服务的请求数受两个因素限制KV Cache 总预算和 decode 阶段的总带宽。H100 单卡如果保持每请求 40 token/s 的生成速度20 个并发意味着每秒要产出 800 token每个 token 都要读一遍 13.5GB 权重总读取量约 10.8TB/s已经远超 3.35TB/s 的显存带宽。所以单卡 80G 显存看着很大真正能稳定支撑的并发其实非常有限。连续批处理模型continuous batching能把每个 batch 的 GPU 利用率抬高但也只是把带宽总量这个硬上限的作用发挥到极致不可能突破物理限制。我自己的经验单张 H100 跑 4-bit 27B在线业务如果要求每请求 30 token/s 以上稳定并发基本在 4~8 路顶天了。所以判断什么时候该上集群其实很简单用你业务高峰期的并发数除以单卡能扛的并发数结果大于 1就得开始琢磨集群了。2. 集群架构的分层逻辑网关、路由、推理实例与 KV Cache 池集群不是把一堆卡堆在一起就完事了。我见过不少团队直接把训练集群的那套思路搬过来做推理结果发现完全不适用——训练任务以小时为单位做一次资源申请、等到全部就位没问题在线推理的延迟以毫秒计请求不会等你的任务调度器慢慢排队。2.1 第一阶段多副本加负载均衡大多数业务到此就够了如果业务规模不大最简单的架构就是多实例副本 前置负载均衡。每个推理实例都是独立进程各自加载权重、各自维护 KV Cache实例之间没有共享状态。请求进来由负载均衡层分发。这个阶段真正要花心思的是网关层。我一开始以为网关就是 Nginx 转发后来发现推理场景至少还要处理三件事排队与容量保护实例有最大并发上限超出的请求不能无限堆积否则延迟会瀑布式恶化。网关必须对每个实例维护一个并发计数器超过阈值就返回 503 或重试到其他实例。超时分级首 token 等待、总生成时长、流式空闲超时三类超时用不同的阈值不能一刀切。优雅摘除实例要升级或重启时先从网关摘流、等存量请求结束、再做优雅停机。这一条做不好扩容回滚时必出事故。网关层还有一个容易忽略的细节流式响应SSE/流式 HTTP下网关如果不把连接和上游实例绑定好用户会拿到吐一半断流的结果。长连接的超时设置、心跳机制都要在压测里专门验证。2.2 推理引擎层连续批处理与 PagedAttention 是吞吐的地基单实例内部用什么引擎直接决定集群要多少张卡。现在主流推理引擎vLLM、SGLang、TensorRT-LLM都实现了连续批处理和 PagedAttention。就我的实测感受PagedAttention 把 KV Cache 按页管理显存利用率比静态分配高得多内存碎片基本消失OOM 概率大幅下降。连续批处理让请求可以在 token 粒度上动态进出 batch而不是等整个序列生成完。一个请求生成完一个词立刻腾出算力给下一个请求GPU 利用率能稳定在 70%~90%比传统的静态 batching 高 30~50%。选引擎的时候还得分清张量并行TP和流水线并行PP的适用场景。TP 把单卡装不下的模型切到多卡卡间通信量很大需要高速互联PP 适合超长序列场景把层切分成流水段通信开销相对小但会牺牲设备利用率。千卡推理集群里90% 的实例跑的是单卡能装下的模型量化后真正需要 TP 的通常是超大模型或极长 context 的专用场景。2.3 进阶形态PD 分离与 KV Cache 池千卡架构的分水岭当集群规模到几百卡以上我强烈建议考虑 prefill 和 decode 分离PD 分离。原因在上面算过账prefill 吃算力decode 吃带宽。混跑时两者互相干扰长 prompt 请求会抢占大量 GPU 算力做 prefill同时把显存带宽占满导致正在生成 token 的短请求集体变慢表现为 p99 延迟剧烈抖动。PD 分离的核心是把请求拆成两段prefill 节点负责读入整个 prompt、计算 KV Cache算力密集型适合用高算力卡并行度可以开得很高。decode 节点接收 prefill 阶段算好的 KV Cache逐 token 生成带宽密集型卡上只需要保住 KV Cache 和权重即可。两段之间要做 KV Cache 的跨节点传输。这一层设计得好集群的利用率天花板会显著提高设计不好会出现prefill 算完等 decode 接收的死锁。工程上建议先用成熟的调度框架如结合 Ray 或 K8s 的 PD 分离部署把 prefill 和 decode 的实例配比、传输队列长度先用监控数据迭代出来不要一上来就手工调参。KV Cache 池化是另一个关键升级。实践中大量请求存在公共前缀——系统提示词、文档、对话历史。如果在 prefill 阶段用 prefix cache 命中可以直接跳过一大段重复计算。集群层面则需要把相同前缀的请求路由到同一个有缓存前缀的节点上这是KV Cache 局部性我在下一节细说。2.4 一张表看清四层架构各自职责层级核心职责常见误区关键指标网关层路由、排队、超时、熔断、容量保护只做转发不管排队QPS、排队深度、503率路由层按缓存局部性和实例负载做调度只按轮询分发KV Cache命中率、剩余时间均方差推理实例层连续批处理、KV Cache管理、生成忽视显存带宽瓶颈TTFT、TPOT、GPU利用率KV Cache层prefix cache、PD间KV传输混跑长短请求缓存命中率、传输时延这张表是给架构评审用的每层只有一两个核心指标能真正反映健康度剩下的都是噪声。3. 负载均衡策略的演进为什么轮询在生成式推理上必然翻车我见过很多团队的第一版负载均衡策略是 Nginx 轮询或者简单的 least-connection压测一上就露馅。原因在于大模型推理请求根本不是短事务——一个请求可能运行几秒钟甚至几十秒而且运行时资源占用是动态变化的。长 prompt 的 prefill 可能瞬间吃掉几 GB 显存和大量算力短请求的 decode 则长期占用显存带宽。用传统负载均衡的思路去处理这种长尾、异构、高相互影响的流量结果必然是部分实例过载、部分实例空转。3.1 问题本质请求不是均匀的短事务假设集群有 4 个实例网关用 least-connection 分发。第 1 个实例接到一个 8K prompt 的长文档总结请求正在做 prefill算力几乎占满第 2 个实例上有一批正在逐 token 生成的短请求显存带宽吃紧。这时新的请求进来网关看连接数可能选择发给第 1 个实例结果第 1 个实例的 prefill 还没结束新请求只能排队TTFT 直接飙升。这不是调度算法坏了而是调度算法根本不了解实例内部正在干什么。3.2 等开销负载均衡让每个实例的预计剩余时间趋于相等我在实际项目里最终采用的方案可以归纳为等开销负载均衡。核心思想很简单不是看谁连接数少而是预估每个实例上正在处理的请求预计还要跑多久 排队中的请求预计要跑多久然后选择预计剩余时间最小的实例。用伪代码表达def choose_instance(request, instances): def estimated_finish_time(inst, req): # 正在运行的请求预计还需要多少时间 running sum(r.remaining_latency() for r in inst.running_requests) # 排队中的请求预计需要多少时间保守估计 queued sum(q.estimated_latency() for q in inst.queue) # 新请求本身的预计处理时间 new_req estimate_prefill(req.prompt_len) estimate_decode(req.max_tokens) return running queued new_req return min(instances, keylambda inst: estimated_finish_time(inst, request))关键在 estimate 函数怎么算。prefill 时长可以用 prompt 长度除以实例的 prefill 吞吐每秒处理多少 tokendecode 时长可以用 max_tokens 除以每 token 生成速度。这两个数字不需要很精确能分出这个实例明显更忙就够了。我实测这么做之后p95 TTFT 比轮询稳定了 3~4 倍吞吐也有 20% 以上的提升。等开销调度的本质是把实例的忙碌程度从离散的连接数升级为时间维度上的负载估计。传统负载均衡解决的是谁闲着等开销解决的是谁最快能空出来。3.3 Cache-aware 路由把局部性变成调度的一等公民等开销调度的局限是它不知道实例里有哪些 KV Cache。一个实例可能非常空闲但它缓存的前缀和你请求的前缀完全无关把请求发过去意味着要重新做一遍 prefill。另一个实例稍微忙一点但它缓存了某个长文档的前缀你的大部分 prefill 计算可以直接复用缓存结果。cache-aware 路由的做法是在调度打分函数里加入缓存命中因子def score(instance, request): load_score instance.estimated_finish_time(request) cache_ratio instance.prefix_cache_hit_ratio(request.prompt_prefix) # 缓存命中率越高负载惩罚越小α用于权衡 return load_score * (1 - alpha * cache_ratio)α 是个可调参数我推荐从 0.3 起调观察集群整体缓存命中率和延迟曲线的平衡。cache-aware 路由做得好长 prompt 场景的 prefill 计算量能降 40%~70%对 TTFT 的改善是立竿见影的。但这里有个容易被忽视的副作用如果调度策略过于强调缓存局部性特定前缀的请求会被反复送到同一两个实例造成热点。热点实例的 KV Cache 命中率很高但排队时间也最长。所以局部性和负载均衡永远是矛盾的两端需要不断用监控数据平衡。我的经验是低负载时优先局部性高负载时优先等开销中间用 QoS 参数平滑过渡。3.4 四种策略的压测对比与选型建议策略实现成本p95 TTFT缓存命中率长尾抖动适用规模轮询极低差低严重10卡以内least-connection低中低中等50卡以内等开销调度中良中小百卡级cache-aware 等开销较高优高小千卡级选型建议直接给结论50 卡以下可以先上 least-connection一旦规模过百或者开始做 PD 分离必须上等开销调度如果业务有明显的长公共前缀特性比如 RAG 系统用固定系统提示词cache-aware 路由带来的收益会让你非常惊讶。4. 千卡规模下的隐形灾害互踢、羊群效应与故障雪崩千卡集群和百卡集群的差别不只是数量级的提升而是会冒出一批小规模集群根本遇不到的系统性灾害。这些灾害几乎都源于局部最优不等于全局最优——单张卡跑得好好的合在一起反而互相拖累。4.1 冷启动互踢新实例上线为什么老实例的延迟反而变高有一次扩容我加了 8 个推理实例结果存量实例的 p99 延迟反而涨了 30%。排查了好久才找到原因新实例上线后是冷的——KV Cache 全空、显存里只有权重。网关把一部分流量切过去之后新实例面对长 prompt 请求只能老老实实重新做 prefillprefill 过程极其消耗算力导致新实例处理缓慢。同时新实例的慢请求不断积压网关的健康检查连续失败后把新实例摘除流量又弹回老实例老实例瞬间过载连锁反应。这就是冷启动互踢。解决思路有两个方向一是给新实例一段预热期在正式接流之前先灌入一批真实流量样本把频繁访问的前缀缓存热身二是对新实例的调度权重动态上调从 0 开始逐步升高让它慢慢吸收流量而不是一上来就接受全量请求。我在生产环境两种方法都试过预热期的效果更直接但需要你提前攒一批压测流量脚本权重渐进则适合无法预知业务前缀的场景。4.2 故障放大链路一次抖动如何演变成全集群雪崩千卡规模的集群单实例故障是常态但真正怕的是故障被层层放大。最经典的链路是这样的某台实例所在宿主机网络抖动 → 该实例上的在线请求变慢排队越来越多 → 健康检查连续探测失败网关将该实例摘除 → 该实例积压的请求全部断开、客户端自动重试 → 重试流量被均匀打向其他实例 → 其他实例瞬时过载延迟上升 → 新一轮健康检查失败更多实例被摘除……整套崩塌过程可能只需要两三分钟。核心教训是健康检查的判定阈值必须和业务延迟指标关联而不是简单地连接能否建立同时网关侧必须做熔断——当某实例的 p99 延迟超过阈值时不是把实例摘除而是先让它半开只接收低优先级流量避免重试风暴。我还强烈建议在所有客户端 SDK 里做重试退避exponential backoff。很多重试风暴根本不来自用户而是来自内部 SDK 的默认行为。一次 5 分钟的故障可能被重试逻辑放大成 20 分钟的全集群抖动。4.3 排队与过载保护宁可拒绝也不要让延迟失控千卡集群的容量规划永远赶不上流量突刺这种时候最关键的是刹车系统。我在网关层给每个实例配置了最大并发数和最大排队长度超过上限直接返回 503 或者让客户端走降级链路。你的第一反应可能是拒绝请求会影响用户体验但数据不会骗人当排队深度超过实例处理能力的 2 倍时队列里的请求几乎全部超时对用户来说等 30 秒拿到一段错误信息比立刻收到 503 再重试要恶劣得多。用一张表说明我踩过的阈值经验参数推荐初值调整依据过小的影响过大的影响实例最大并发单卡带宽上限/单请求解码速度实测decode吞吐利用率低延迟崩溃实例最大排队并发数的1~2倍压测p95延迟拐点频繁拒绝超时堆积网关全局并发所有实例并发的80%容量余量保护不足雪崩这个表的本质是给每个实例一个即将过载的红线让它在崩掉之前先说不。4.4 网络与拓扑千卡集群真正难调的往往是通信千卡 H100 级别的集群典型配置是 InfiniBand 或 RoCE 高速网络 集中式存储 K8s/GPU 调度平台。硬件连接需求并不复杂——按机架做 ToR 交换机、机架间再做 Spine 互联重点在于网络隔离和 QoS。推理集群有个特点大部分流量是东西向的短小控制流但 PD 分离架构下 prefill 节点向 decode 节点传输 KV Cache 时会产生大量跨节点数据流这种流量如果和训练任务的集合通信混在同一张网络上分分钟把网络打爆。建议不管规模多大都要把推理集群的存储网络、KV Cache 传输网络、外部访问网络做逻辑隔离。跨节点 TP 或 KV 传输一定要走 RDMA避免 TCP 协议栈在千卡规模下成为明显瓶颈。实测中很多 GPU 利用率上不去不是算力问题而是通信等待时间太长GPU 一直在等数据。GPU 上的 Compute 和网络上的 Transfer 必须重叠起来否则再强的卡也白搭。5. 可观测性清单监控、日志与 Trace 三位一体千卡集群没有全局可观测性就像蒙眼开车——你凭感觉觉得还行直到压死最后一根稻草才看到全貌。推理场景的监控体系和传统 Web 服务不一样重点指标完全不同。5.1 关键指标TTFT、TPOT、排队深度、KV Cache 命中率基础监控至少要有下面这些TTFT首 token 延迟用户可感知的第一个指标p50/p95/p99 都要看。长 prompt 场景下 TTFT 直接被 prefill 调度质量左右。TPOT 或 ITL每 token 间隔反映生成阶段是否稳定持续上涨通常意味着带宽吃紧或排队恶化。排队深度网关和实例两个维度都要看排队深度是预测延迟拐点的先行指标。GPU 利用率、显存占用、显存带宽利用率带宽利用率比算力利用率更关键推理场景瓶颈大多是带宽。KV Cache 命中率prefix cache 是否生效能直接解释 prefill 计算量的波动。温度/功耗千卡集群的散热和电力是硬约束功耗曲线异常往往先于性能劣化出现。对于 GPU 利用率我要多说一句推理场景只盯着算力利用率会误判。一张卡算力利用率 30% 但显存带宽利用率 90%它其实是满载的再加请求就会延迟崩溃。判断负载高低优先看带宽利用率和排队深度。5.2 Token 级日志与全链路 Trace传统服务用 request ID 串日志就能定位问题推理服务最好能记录到 token 粒度。我的做法是网关生成全局 request ID转发到推理实例时带上推理实例每生成一个 token 输出一条带时间戳的日志记录 token 序号、累积耗时、当前 KV Cache 大小。这样一旦某个请求出现中间卡住的情况可以从 token 粒度精确复现是哪一秒开始变慢、当时实例在干什么。再往上走就是全链路 Trace——网格从网关的排队开始穿过负载均衡决策、实例的 GPU kernel 执行再到 KV Cache 复用情况。你可能会觉得 token 级日志数据量太大但实际上用采样方式例如只保留 p99 请求和异常请求的 token 级日志就能覆盖 95% 的排障场景。磁盘便宜用户的困惑贵。5.3 容量规划靠数据而不是靠直觉扩容有了监控数据扩容决策就不再是拍脑袋。我通常用三个信号判定扩容时机网关全局排队深度持续超过阈值的 30 分钟均值、p95 TTFT 超过业务 SLO 的 70% 且仍在上升、以及预计未来 1~2 小时内的配额估计比如下班前的高峰。数据驱动扩容最大的好处是提前量——等延迟打满再扩容扩容的冷启动问题会让你先经历一轮劣化不如在拐点之前动手。6. 从 8 卡到 512 卡的演进路线与我的最后建议最后把我实际走过的一条演进路线完整复盘一下包含每个阶段对应的架构动作和容易踩的坑给正在规划的同学一个参考。6.1 分阶段的架构动作与踩坑记录阶段规模架构动作我踩过的坑第一阶段8~16卡多副本least-connection统一网关健康检查阈值只看连接故障时雪崩第二阶段16~64卡等开销调度上线prefix cache 开启冷启动没做预热扩容引发生存量实例延迟抖第三阶段64~256卡PD 分离、KV Cache 池化cache-aware 路由KV 跨节点传输走 TCP网络成为瓶颈第四阶段256~512卡网络隔离、token级可观测性、熔断半开全局监控缺失容量规划全靠经验每个阶段过渡都伴随至少一次线上事故这是正常的。架构升级和大版本重构一样不可能平滑无感。关键是每次事故都要变成监控指标和调度策略的增量进步而不是只修一个临时补丁。6.2 如果重来我会在第一天就做好的三件事第一压测脚本和监控面板在写第一个推理服务时就要搭好而不是等集群到一百卡再补。没有压测基线你后面做任何调度策略优化都无法量化收益。第二网关层从第一天就预留切换调度算法的开关能通过配置中心动态切换轮询、等开销、cache-aware 三种策略。我见过太多团队因为网关写死轮询想换策略只能改代码重新发布白白错过黄金调优窗口。第三所有客户端 SDK 统一重试退避策略这是在千卡集群里花最少的钱避免最大灾难的工程决策。6.3 最后一个可落地的小技巧分享一个很多人忽视的实践给负载均衡策略加一个金丝雀池。任何调度算法的改动先让 5% 的流量走新策略和存量策略并行跑一两天对比 TTFT 和吞吐。我在做 cache-aware 调参时就是靠金丝雀池避免了一次线上事故——新策略在某类流量上出现严重热点金丝雀池数据立刻暴露了问题存量流量完全没受影响。小规模集群可能觉得这是多余动作但规模上来之后这个习惯能救你很多次。
返回列表