大模型服务调度困局:LLM 推理集群的负载均衡策略与架构实践
大模型服务调度困局:LLM 推理集群的负载均衡策略与架构实践
一、Token 洪峰与推理延迟的双重挤压:LLM 服务负载均衡的核心痛点
当企业将大语言模型(LLM)从实验阶段推向生产环境时,最先暴露的往往不是模型精度问题,而是服务层的调度瓶颈。一个典型的场景:线上对话系统在晚高峰迎来请求洪峰,GPU 集群中部分节点因 KV Cache 占满而排队,另一部分节点却因模型版本不同而处于空闲状态——请求在错误的节点上堆积,用户面对的却是超时和降级。
传统微服务的负载均衡策略(轮询、加权轮询、最少连接数)在 LLM 推理场景下几乎全部失效。原因在于 LLM 推理具有三个独特特征:第一,请求的算力消耗与输入输出 Token 数强相关,而非简单的连接数;第二,推理过程分为 Prefill(预填充)和 Decode(解码)两个阶段,两阶段对 GPU 资源的占用模式截然不同;第三,不同模型实例可能承载不同规格的模型,算力异构性极大。如果仍然用"连接数最少"来路由请求,结果就是:一个正在处理长上下文请求的实例被持续分配新请求,最终 OOM 崩溃。
这正是 LLM 服务负载均衡需要独立架构设计的根本原因。本文将从推理引擎的底层调度机制出发,逐步构建一套面向生产环境的 LLM 负载均衡方案。
二、从 Prefill 到 Decode:LLM 推理调度的底层机制与路由模型
要设计合理的负载均衡策略,必须先理解 LLM 推理引擎内部的资源消耗模型。以 vLLM 为例,其核心调度机制基于 PagedAttention,将 KV Cache 分页管理,每个请求的 KV Cache 占用量与序列长度成正比。
flowchart TB subgraph 请求生命周期 A[客户端请求] --> B[API Gateway] B --> C{负载均衡路由决策} C -->|策略1: Token感知| D[推理实例 A] C -->|策略2: 队列深度| E[推理实例 B] C -->|策略3: 模型匹配| F[推理实例 C] end subgraph 推理实例内部 D --> G[Prefill 阶段] G --> H[Decode 阶段] H --> I[KV Cache 释放] end subgraph 资源监控 D --> J[GPU 利用率] D --> K[KV Cache 使用率] D --> L[请求队列深度] J & K & L --> M[指标上报至路由中心] M --> C end上图展示了 LLM 请求从入口到路由再到推理实例的完整链路。关键点在于:路由决策不能仅依赖静态权重,而必须结合实例的实时资源状态。
Prefill 阶段是计算密集型的,需要一次性处理所有输入 Token,GPU 计算利用率瞬间拉满。Decode 阶段则是访存密集型的,每次只生成一个 Token,但需要反复读取 KV Cache,GPU 计算利用率反而较低。这意味着:如果一个实例正在执行 Prefill,此时再分配一个 Prefill 请求,GPU 显存带宽将成为瓶颈;而如果分配一个 Decode 请求,两者可以一定程度上复用计算资源。
因此,理想的调度策略应该感知每个实例当前所处的推理阶段,实现 Prefill 与 Decode 请求的交错调度。这种策略被称为"阶段感知路由"(Phase-Aware Routing),是 LLM 负载均衡区别于传统负载均衡的核心差异。
三、生产级负载均衡实现:Token 感知与阶段感知的路由策略
下面给出一个基于 Java 实现的 LLM 推理集群负载均衡器。该实现融合了 Token 感知、KV Cache 使用率感知和请求阶段感知三种策略,并支持优雅降级。
/** * LLM 推理实例的健康状态与资源快照 * 每个实例定期上报,路由器基于此做决策 */ public class InferenceNodeSnapshot { private final String nodeId; private final String modelId; // GPU 资源指标 private final double gpuUtilization; // GPU 计算利用率 0~1 private final double kvCacheUsage; // KV Cache 使用率 0~1 private final int availableKvBlocks; // 可用 KV Cache 分页数 // 请求队列指标 private final int pendingPrefillCount; // 排队中的 Prefill 请求 private final int activeDecodeCount; // 正在执行的 Decode 请求 private final int waitingQueueSize; // 等待队列总长度 // 延迟指标(滑动窗口均值) private final double avgPrefillLatencyMs; private final double avgDecodeTpsPerToken; // 每 Token 解码延迟 private final long timestamp; // 快照时间戳 /** * 计算该实例的负载评分,分数越低越适合接收新请求 * 综合考虑 KV Cache 余量、队列深度和 GPU 利用率 */ public double calculateLoadScore(RequestPhase incomingPhase) { // KV Cache 余量权重最高,防止 OOM double kvCachePenalty = (1.0 - kvCacheUsage) * 40.0; // 队列深度惩罚 double queuePenalty = waitingQueueSize * 5.0; // Prefill 请求的交错调度:若实例正在 Prefill,降低优先级 double phasePenalty = 0.0; if (incomingPhase == RequestPhase.PREFILL && pendingPrefillCount > 0) { phasePenalty = 30.0; // 避免多个 Prefill 同时竞争 GPU } // GPU 利用率过高时降权 double gpuPenalty = gpuUtilization > 0.85 ? (gpuUtilization - 0.85) * 100.0 : 0.0; return kvCachePenalty + queuePenalty + phasePenalty + gpuPenalty; } /** * 判断实例是否可以接受新请求 * KV Cache 使用率超过阈值时拒绝,防止 OOM 导致实例崩溃 */ public boolean isAcceptable() { return kvCacheUsage < 0.90 && waitingQueueSize < 100; } } /** * 请求阶段枚举:Prefill(首 Token 生成前)与 Decode(流式解码中) */ public enum RequestPhase { PREFILL, // 预填充阶段,计算密集 DECODE // 解码阶段,访存密集 } /** * LLM 推理集群负载均衡路由器 * 基于 Token 感知与阶段感知的加权最少负载策略 */ public class LlmLoadBalancer { private final Map<String, InferenceNodeSnapshot> nodeSnapshots = new ConcurrentHashMap<>(); private final ScheduledExecutorService healthChecker = Executors.newSingleThreadScheduledExecutor(); // 降级策略:当所有节点指标过期时回退到加权轮询 private final AtomicInteger roundRobinIndex = new AtomicInteger(0); public LlmLoadBalancer() { // 每 2 秒检查一次节点健康状态,过期节点标记为不可用 healthChecker.scheduleAtFixedRate(this::pruneStaleNodes, 2, 2, TimeUnit.SECONDS); } /** * 更新节点快照(由各推理实例定期上报调用) */ public void updateSnapshot(InferenceNodeSnapshot snapshot) { nodeSnapshots.put(snapshot.getNodeId(), snapshot); } /** * 核心路由方法:为给定请求选择最优推理实例 * * @param modelId 目标模型标识 * @param inputTokens 输入 Token 数(用于估算 Prefill 资源需求) * @param phase 请求阶段 * @return 选中的节点 ID */ public String route(String modelId, int inputTokens, RequestPhase phase) { // 筛选:模型匹配 + 可接受新请求 + 快照未过期 List<InferenceNodeSnapshot> candidates = nodeSnapshots.values().stream() .filter(s -> s.getModelId().equals(modelId)) .filter(InferenceNodeSnapshot::isAcceptable) .filter(s -> System.currentTimeMillis() - s.getTimestamp() < 5000) .collect(Collectors.toList()); // 降级:无可用节点时回退到加权轮询 if (candidates.isEmpty()) { return fallbackRoundRobin(modelId); } // 按负载评分排序,选择评分最低(负载最轻)的节点 candidates.sort(Comparator.comparingDouble(s -> s.calculateLoadScore(phase))); InferenceNodeSnapshot selected = candidates.get(0); // 二次校验:输入 Token 数是否超出该节点剩余 KV Cache 容量 // 估算每个 Token 约占用 1 个 KV Block(简化模型,实际需按模型维度计算) if (selected.getAvailableKvBlocks() < inputTokens) { // 容量不足,尝试次优节点 if (candidates.size() > 1) { selected = candidates.get(1); } else { throw new LlmRoutingException( "所有候选节点 KV Cache 不足以承载请求,inputTokens=" + inputTokens); } } return selected.getNodeId(); } /** * 降级策略:加权轮询 * 当所有节点指标过期时使用,保证服务可用性 */ private String fallbackRoundRobin(String modelId) { List<String> modelNodes = nodeSnapshots.values().stream() .filter(s -> s.getModelId().equals(modelId)) .map(InferenceNodeSnapshot::getNodeId) .collect(Collectors.toList()); if (modelNodes.isEmpty()) { throw new LlmRoutingException("无可用推理节点,modelId=" + modelId); } int index = roundRobinIndex.getAndIncrement() % modelNodes.size(); return modelNodes.get(index); } /** * 清理过期节点快照,防止路由到已下线实例 */ private void pruneStaleNodes() { long now = System.currentTimeMillis(); nodeSnapshots.entrySet().removeIf( e -> now - e.getValue().getTimestamp() > 10000 ); } }上述实现的关键设计决策有三点。第一,负载评分以 KV Cache 余量为核心权重,因为 LLM 推理的 OOM 风险远高于 CPU 饱和。第二,阶段感知机制在实例已有 Prefill 请求时降低新 Prefill 请求的优先级,避免 GPU 计算资源争抢。第三,降级策略保证在指标采集链路异常时系统仍可运行,这是生产环境的基本要求。
四、KV Cache 争抢与冷启动延迟:负载均衡策略的架构权衡
任何架构决策都有代价。Token 感知与阶段感知的负载均衡策略同样存在必须正视的边界条件。
第一,指标采集的时效性开销。路由决策依赖每个推理实例的实时快照,这意味着实例需要以 12 秒的频率上报 GPU 利用率、KV Cache 使用率等指标。在高并发场景下,指标上报本身会占用推理实例的 CPU 和网络带宽。实测数据显示,在 A100 集群上,vLLM 的 metrics 端点每次采集约增加 0.30.5ms 的延迟开销。当实例数量超过 50 个时,集中式路由器的指标聚合也可能成为瓶颈。
第二,KV Cache 容量估算的不确定性。代码中简化了 Token 到 KV Block 的映射关系,但实际场景中,不同模型的 KV Cache 占用量差异极大。例如,Llama-3-70B 的每个 Token 约占用 128KB 的 KV Cache,而 Qwen-2-7B 仅需约 16KB。如果路由器维护的容量模型不准确,可能导致请求被错误地路由到"看似有余量但实际不足"的实例,引发运行时 OOM。
第三,冷启动与模型加载延迟。当集群需要水平扩容时,新实例加载模型权重到 GPU 显存通常需要 30~120 秒(取决于模型大小和存储带宽)。在此期间,新实例无法接收请求,而路由器可能已经将其纳入候选池。必须在快照中加入"模型就绪"状态位,否则会引发大量路由失败。
适用边界总结:该策略适用于请求量相对稳定、模型规格统一的推理集群。对于多模型混部、请求量剧烈波动的场景,需要额外引入预测性扩缩容和请求优先级队列机制,复杂度将显著上升。
五、总结
LLM 推理服务的负载均衡,本质上是在 GPU 显存约束与请求延迟约束之间寻找最优解。传统微服务的负载均衡策略忽略了推理过程的阶段差异和 KV Cache 的资源竞争,直接套用会导致请求堆积与实例崩溃。
本文提出的 Token 感知与阶段感知路由策略,通过实时采集推理实例的 KV Cache 使用率、GPU 利用率和请求队列深度,结合请求阶段进行差异化调度,在保证实例稳定性的前提下提升了集群整体吞吐。降级到加权轮询的设计则确保了指标链路异常时的服务可用性。
落地路线建议:第一步,在现有推理集群上部署指标采集 Agent,验证 KV Cache 使用率与实际负载的相关性;第二步,实现基于 KV Cache 余量的最简路由策略,灰度上线观察;第三步,引入阶段感知和 Token 感知逻辑,逐步提升调度精度;第四步,建立容量预测模型,为水平扩缩容提供决策依据。
