推理资源的预算方法
阅读说明:本文以推理服务中的典型故障链路说明排查和设计方法。文中的告警、数字与“线上”叙述如未给出来源,均应视为示例条件;落地前请在自己的版本、负载和资源约束下复测。
1. 业务高峰期的卡顿告警:8 卡 A100 显存全红与并发暴跌
下面用一个假设场景说明 推理服务 中应先检查哪些信号,以及如何验证判断。
晚高峰 20:30,Prometheus 告警钉钉群连续推送了 12 条 GPU 资源耗尽消息。线上部署的 Llama-3-70B 推理集群突然出现严重的 P99 延迟飙升,原本维持在 200ms 左右的首字延迟(TTFT)迅速冲到 4.5 秒,并发 Token 吞吐量从 3500 tokens/s 断崖式跌至 400 tokens/s。
登跳板机查看 kubectl top node,发现 4 台配备了 8 卡 A100-80G 的节点显存利用率全部处于 99.2% 的饱和状态。然而,用nvidia-smi深度观察,GPU-Util(算力利用率)却奇特地徘徊在 28%~35% 之间。这意味着算力没有用满,显存却被明显卡死。更糟糕的是,Kubernetes 的 HPA(Horizontal Pod Autoscaler)因为仅仅依赖 CPU 和通用显存占用指标,在并发骤增时触发了在未验证前扩容。由于大模型镜像高达 30GB 且权重加载耗时超过 3 分钟,新 Pod 还没准备好,原有的推理节点就已经因为 KV Cache 内存溢出(OOM)频繁重启。
解决这个问题的关键,在于必须搞清楚 LLM 推理过程中显存到底被哪些组件吃掉了,以及如何通过确定性的指标来驱动 GPU Pod 的弹性伸缩。
2. 算力成本拆解:KV Cache 动态占用与 Token 吞吐的数学账
评估大模型推理算力成本,不能照搬传统 Web 应用的 CPU/RAM 预算逻辑。LLM 显存消耗由三部分硬性组成:模型权重静态显存、上下文 KV Cache 动态显存、以及 CUDA Context 与临时激活值(Activation)。
以 FP16 精度的 70B 模型为例,其静态显存占用是固定的:
$$ Memory_{static} = 70 \times 10^9 \times 2 \text{ Bytes} \approx 140 \text{ GB} $$
若采用 4 卡 Tensor Parallelism(TP=4),每张 A100 占用 35GB 静态显存。剩下 45GB 显存由 PagedAttention 划分为物理 Block 管理。此时,决定单卡最大并发能力的是 KV Cache 的理论极限。假设模型 Layer 数量为 $L=80$,Key/Value 头数为 $H=8$,隐层维度为 $D=128$,最大上下文长度 $S=4096$,则单个 Request 满上下文时的 KV Cache 占用为:
$$ Memory_{KV} = 2 \times L \times H \times D \times S \times 2 \text{ Bytes} = 2 \times 80 \times 8 \times 128 \times 4096 \times 2 \approx 1.34 \text{ GB} $$
这意味着剩余 45GB 显存理论上最多只能同时容纳大约 33 个满上下文请求。如果线上出现突发长文本并发,KV Cache 短时间内填满,推理引擎就不得不将物理 Block 换出到 CPU 内存(Swap-out)或者强行抢占(Preemption)中断请求,导致整体吞吐下降 80% 以上。
成本算账的逻辑显而易见:在未验证前增加 GPU 节点是昂贵的。按单张 A100 每小时 2.5 美元计算,扩容一个 8 卡节点意味着每月增加 1.4 万美元成本。因此,必须将 KV Cache 的实际利用率(vllm:gpu_cache_usage_perc)与请求队列长度(vllm:num_requests_waiting)作为 K8s HPA 的核心 Metric,实现精准伸缩。
3. vLLM 与 K8s HPA 结合的弹性伸缩架构设计
传统的 HPA 基于 CPU 利用率扩容在大模型场景下完全失效。必须引入 Custom Metrics API,将 vLLM 暴露的 Prometheus 业务指标暴露给 K8s 伸缩控制器。
伸缩决策不能采用简单的线性比例算法。如果只凭“显存超过 80%”就扩容,冷启动的 3 分钟窗口期足以让服务明显级联故障。我们设计的弹性架构分为三层:
- 入口闸门(Ingress Gate):在 vLLM 前置 Nginx/Envoy 限制最大等待队列。超过安全阈值的请求直接返回 429 降级响应,保护现有推理节点不崩溃。
- 指标采集层(Prometheus + Prometheus-Adapter):每 5 秒抓取一次
vllm:gpu_cache_usage_perc和vllm:num_requests_waiting。 - 阶梯式 HPA 策略:当
num_requests_waiting > 5且持续 15 秒时,触发 Pod 数量加倍;当gpu_cache_usage_perc < 30%持续 10 分钟时,才平滑缩容,避免频繁冷启动振荡。
4. 生产级 Prometheus 监控指标提取与 Python 弹性扩缩防线代码
下面的 Python 服务展示了如何安全地作为 K8s Custom Metrics 适配器,获取 vLLM 暴露的指标并实施包含熔断与预热检查的扩缩容防线计算。
import time import requests from typing import Dict, Tuple class LLMInferenceAutoscalerGuard: def __init__( self, vllm_metrics_url: str, max_gpu_cache_threshold: float = 0.85, max_waiting_queue: int = 10, warmup_cooldown_seconds: int = 180 ): self.vllm_metrics_url = vllm_metrics_url self.max_gpu_cache_threshold = max_gpu_cache_threshold self.max_waiting_queue = max_waiting_queue self.warmup_cooldown_seconds = warmup_cooldown_seconds self.last_scale_time = 0.0 def parse_prometheus_metrics(self, raw_text: str) -> Dict[str, float]: metrics = {} for line in raw_text.splitlines(): if line.startswith("#") or not line.strip(): continue parts = line.split() if len(parts) >= 2: key, val = parts[0], parts[1] try: metrics[key] = float(val) except ValueError: continue return metrics def fetch_inference_health(self) -> Tuple[bool, float, int]: try: resp = requests.get(self.vllm_metrics_url, timeout=2.0) if resp.status_code != 200: # 监控端点异常,触发安全降级防御 return False, 1.0, 999 parsed = self.parse_prometheus_metrics(resp.text) cache_usage = parsed.get('vllm:gpu_cache_usage_perc', 0.0) waiting_reqs = int(parsed.get('vllm:num_requests_waiting', 0)) return True, cache_usage, waiting_reqs except Exception as err: # 网络超时或解析故障,必须阻止危险的缩容操作 return False, 1.0, 999 def evaluate_scale_decision(self, current_replicas: int) -> Tuple[str, int]: now = time.time() is_healthy, cache_usage, waiting_reqs = self.fetch_inference_health() if not is_healthy: # 指标异常时保持副本数,拒绝在未验证前缩容 return "HOLD_UNHEALTHY_METRICS", current_replicas # 防频繁冷却震荡检查 if now - self.last_scale_time < self.warmup_cooldown_seconds: return "COOLDOWN_WAITING", current_replicas # 紧急扩容条件:KV Cache 接近红线 或 等待队列堆积 if cache_usage >= self.max_gpu_cache_threshold or waiting_reqs >= self.max_waiting_queue: target_replicas = min(current_replicas * 2, 16) # 单次最多翻倍,上限16 Pod if target_replicas != current_replicas: self.last_scale_time = now return "SCALE_UP_EMERGENCY", target_replicas # 平滑缩容条件:显存占用低于 30% 且无排队 if cache_usage < 0.30 and waiting_reqs == 0: target_replicas = max(current_replicas - 1, 2) # 保持最小2副本保底 if target_replicas != current_replicas: self.last_scale_time = now return "SCALE_DOWN_SMOOTH", target_replicas return "HOLD_STABLE", current_replicas if __name__ == "__main__": guard = LLMInferenceAutoscalerGuard( vllm_metrics_url="http://127.0.0.1:8000/metrics", max_gpu_cache_threshold=0.80, max_waiting_queue=5, warmup_cooldown_seconds=180 ) # 模拟一次检测 action, replicas = guard.evaluate_scale_decision(current_replicas=4) print(f"[Autoscaler Decision] Action: {action}, Target Replicas: {replicas}")5. 压测回归:从 1200 QPS 到 P99 降至 180ms 的成本效益对比
实施该方案后,我们在 Staging 环境使用 Vegeta 模拟了包含长短文本混合的突发流量(QPS 从 200 短时间内飙升至 1200)。
对比数据非常清晰:在使用传统 CPU HPA 策略时,推理集群在第 40 秒出现大规模 429 拒答,P99 延迟突破 5000ms,整体 GPU 显存频繁引发 vLLM 强行 Swap,实际完成的吞吐仅为 850 tokens/s。
采用基于gpu_cache_usage_perc与等待队列梯度的自定义扩缩容方案后,当等待队列首次突破 5 个请求时,系统立即拦截溢出流量并平滑触发扩容。因为设置了 2 副本的温备基线,流量被有效平摊。最终 P99 延迟稳定在 180ms,无任何节点发生显存 OOM。集群在突发流量退去 10 分钟后顺畅缩容,月度 GPU 算力成本相比固定配额方案降低了 42%。
小结:把结论留给可复现的结果
本文的场景用于说明推理服务的检查顺序,不代表某个环境的既成事故或固定收益。变更前应记录基线、版本与配置,控制流量或样本,并比较尾延迟、错误率和资源占用;未达到预设门槛时,应保留或回退原方案。