大模型服务自动扩缩容:基于Token吞吐与GPU利用率的HPA策略设计
大模型服务自动扩缩容:基于Token吞吐与GPU利用率的HPA策略设计
一、模型服务扩缩容的指标困境
大模型推理服务的扩缩容远比传统微服务复杂。传统服务通常以CPU使用率或QPS作为核心指标,但GPU场景下这两个指标都存在明显缺陷。CPU使用率无法反映GPU的计算饱和度——模型在做矩阵运算时CPU可能很闲;QPS则忽略了请求复杂度的差异,一次处理512 token的请求和一次处理8192 token的请求成本完全不同。
真正有效的指标需要同时覆盖两个维度:请求处理速率和硬件资源利用率。Token吞吐量(tokens per second)衡量实际计算产出,GPU利用率反映硬件使用效率。这两者结合才能准确描述模型服务的健康状态。
二、KEDA Prometheus Scaler配置实践
KEDA(Kubernetes Event-Driven Autoscaling)提供了比原生HPA更灵活的扩缩容策略。对于GPU推理服务,我们通过Prometheus Scaler来对接自定义指标。
以下是基于Token吞吐量和GPU利用率的ScaledObject配置:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llm-inference-scaler
namespace: ai-serving
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: llm-inference-deployment
pollingInterval: 15
cooldownPeriod: 300
minReplicaCount: 1
maxReplicaCount: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
metricName: token_throughput_utilization
threshold: "0.8"
query: |
sum(rate(http_tokens_per_second{service="llm-inference"}[2m]))
/ sum(kube_deployment_spec_replicas{deployment="llm-inference-deployment"})
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
metricName: gpu_utilization_percentage
threshold: "85"
query: |
avg(DCGM_FI_DEV_GPU_UTIL{pod=~"llm-inference-deployment-.*"})
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
metricName: request_queue_depth
threshold: "10"
query: |
avg(inference_request_queue_depth{pod=~"llm-inference-deployment-.*"})
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 30
多指标触发器的设计遵循"任一触发即扩容"的原则。实际压测数据显示,Token吞吐量指标通常最先触发扩容——当单Pod处理能力接近饱和时,Token生产速率会出现明显拐点。GPU利用率作为辅助指标用于防止资源浪费,队列深度则提供最终兜底。
三、冷启动延迟与扩容窗口的博弈
模型服务的最大痛点是冷启动。一个70B参数的模型从镜像拉取到权重加载完成,可能需要3-8分钟。这意味着在流量突增场景下,新Pod启动期间老Pod已经在承受超额负载。
解决冷启动问题的策略分层:
第一层:模型预热池。维持一定数量的预热Pod,权重已加载到GPU显存中,处于待命状态。这类似于数据库连接池的思路——用空间换时间。
# 模型预热管理器(Python侧示例)
import asyncio
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
class ModelWarmPool:
"""模型预热池,确保最小数量的Pod处于就绪状态"""
def __init__(self, model_path: str, min_warm_pods: int = 2):
self.model_path = model_path
self.min_warm_pods = min_warm_pods
self._ready_event = asyncio.Event()
async def preload_weights(self) -> None:
"""在Pod启动时预加载模型权重到GPU"""
torch.cuda.synchronize()
model = AutoModelForCausalLM.from_pretrained(
self.model_path,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
# 触发初次推理以完成KV Cache预热
tokenizer = AutoTokenizer.from_pretrained(self.model_path)
inputs = tokenizer("warmup", return_tensors="pt").to("cuda")
with torch.no_grad():
_ = model.generate(**inputs, max_new_tokens=1)
self._ready_event.set()
# 向就绪探针暴露状态
await self._mark_ready()
async def _mark_ready(self) -> None:
import os
ready_file = "/tmp/model_ready"
os.makedirs(os.path.dirname(ready_file), exist_ok=True)
with open(ready_file, "w") as f:
f.write("1")
第二层:渐进式引流。新Pod就绪后不立即接收全量流量,而是通过Service Mesh逐步增加权重,让模型在前几个请求中完成KV Cache的热身。
第三层:溢出队列。当所有Pod都在处理请求时,新请求进入有界队列等待,而非直接返回503。队列长度也是KEDA的触发指标之一。
四、缩容时的优雅退出与请求排空
缩容比扩容更容易出问题。粗暴地终止Pod会导致正在推理中的请求被中断,已消耗的计算资源白白浪费。对于自回归生成任务,一个请求可能持续数十秒甚至数分钟。
优雅退出流程的核心实现:
// Go侧:推理网关的优雅退出处理器
package gateway
import (
"context"
"os/signal"
"sync"
"syscall"
"time"
)
type GracefulShutdown struct {
inflightRequests sync.WaitGroup
draining atomic.Bool
drainTimeout time.Duration
}
func (gs *GracefulShutdown) OnSIGTERM(ctx context.Context, server *http.Server) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh
gs.draining.Store(true)
// Step 1: 标记节点为不可调度,停止接收新请求
// 通过更新Endpoint的ready状态实现
// Step 2: 等待正在处理的请求完成
done := make(chan struct{})
go func() {
gs.inflightRequests.Wait()
close(done)
}()
select {
case <-done:
// 所有请求正常完成
case <-time.After(gs.drainTimeout):
// 超时强制退出,但记录未完成请求数量
log.Warn("drain timeout, forcing shutdown",
"remaining", gs.remainingCount())
}
// Step 3: 释放GPU资源
gs.releaseGPUResources()
// Step 4: 关闭HTTP服务器
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
}
func (gs *GracefulShutdown) WrapHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if gs.draining.Load() {
// 已经处于排空状态,将请求转发到其他Pod
w.Header().Set("X-Draining", "true")
http.Error(w, "node draining", http.StatusServiceUnavailable)
return
}
gs.inflightRequests.Add(1)
defer gs.inflightRequests.Done()
next.ServeHTTP(w, r)
})
}
func (gs *GracefulShutdown) releaseGPUResources() {
// 通知设备插件释放GPU
// 避免GPU资源泄漏导致后续调度问题
}
排空超时的设定需要权衡。对于推理服务,建议排空超时设置在60-120秒之间——足以让大部分请求完成生成,但不至于让Kubernetes等待过久导致Pod卡在Terminating状态。
五、总结
大模型推理服务的扩缩容策略核心在于指标选择和生命周期管理。Token吞吐量配合GPU利用率构成更准确的饱和度衡量体系;KEDA的Prometheus Scaler提供了灵活的指标对接能力。冷启动问题需要通过权重预热、渐进式引流和溢出队列三层策略来缓解。缩容时的优雅退出尤其重要——推理请求的生命周期远比HTTP请求长,粗暴终止的成本不可忽视。合理的排空超时和流量转发机制是保障服务稳定性的最后一道防线。
更多推荐




所有评论(0)