vLLM + PyTorch:在K8s集群中实现LLM推理服务的高效弹性扩缩容
引言:LLM推理的“性能与成本”困局
部署大语言模型(LLM)推理服务时,我们常常面临一个经典的“跷跷板”困境:用户流量忽高忽低,如果长期保持大量GPU副本在线,成本高昂;但如果副本太少,突发流量一来,服务立马超时甚至崩溃。静态配置的扩缩容策略,在LLM场景下显得格外笨拙。
为什么Kubernetes自带的基于CPU或内存的HPA(Horizontal Pod Autoscaler)不奏效?原因很简单:LLM推理是GPU密集型任务。你可能CPU利用率才30%,GPU显存已经爆满;内存还有余量,新的推理请求已经排起长队。指标选错了,扩容就永远慢半拍。
将vLLM推理引擎与Kubernetes HPA深度结合,配合自定义指标(如队列等待数、KV Cache利用率),是当前生产级LLM服务的主流解法。其底层逻辑可以概括为:单实例极致性能(内功)+ 集群级精准弹性(外功)。本文将带您从原理到实战,拆解这套方案的落地路径。
一、vLLM的“内功”:PagedAttention与Continuous Batching
在谈弹性之前,必须先理解vLLM为何能扛住高并发。答案藏在它的两项核心创新中。
1.1 PagedAttention:把显存利用率从40%拉到90%+
传统推理框架为每个请求预分配一整块连续显存来存储KV Cache,无论它最终输出多少token。这导致两个问题:一是内部碎片严重,二是显存利用率常常低于40%。
vLLM借鉴操作系统虚拟内存分页思想,将KV Cache拆成固定大小的“页”(Block),按需分配、非连续存储。每个请求维护一张Block Table(逻辑页→物理页映射表),CUDA kernel在计算attention时通过查表动态拼接数据。
# 模拟PagedAttention核心数据结构(简化版)
class PagedAttentionManager:
def __init__(self, num_blocks=1024, block_size=16):
# 物理显存池:预先分配的总块数 × 每块可存token数
self.physical_pool = [None] * num_blocks # 实际场景中是连续显存
self.free_blocks = list(range(num_blocks))
# 每个请求的页表:{request_id: [物理块ID列表]}
self.block_tables = {}
def allocate_pages(self, request_id, num_pages):
"""为新请求分配物理页"""
if len(self.free_blocks) < num_pages:
raise RuntimeError("显存不足,触发扩容信号!")
allocated = self.free_blocks[:num_pages]
self.free_blocks = self.free_blocks[num_pages:]
self.block_tables[request_id] = allocated
return allocated
def get_kv_cache(self, request_id):
"""根据页表拼接KV Cache"""
physical_ids = self.block_tables.get(request_id, [])
# 实际vLLM中,这里会调用CUDA kernel聚合并计算attention
return physical_ids
这套机制将显存利用率从不足40%提升到90%以上,单卡支持的并发请求数翻了数倍。显存利用率更高,意味着同样硬件下单个Pod能扛更多并发,HPA的扩容阈值可以设得更从容。
1.2 Continuous Batching:让GPU不再“等慢车”
传统静态批处理(Static Batching)有个致命缺陷:一批请求必须全部完成后才能释放资源。假如9个短请求配1个长请求,其他9个早已跑完,GPU只能干等着那个“拖油瓶”。
vLLM的Continuous Batching打破了这种僵局——它在每个Step(迭代)粒度上动态重组Batch。某个请求生成完了,立刻从队列里塞一个新请求进来继续算,GPU几乎没有空转时间。实测吞吐可达Hugging Face Transformers的10倍以上。
这两项能力的意义:单实例性能越强,HPA扩缩容的“基线”就越稳定。扩容不会过于频繁,缩容也更有底气。
二、弹性“外功”:Kubernetes HPA与自定义指标
单实例再强,面对流量洪峰终究有限。外部弹性靠Kubernetes HPA实现,但关键问题始终是指标选什么。
2.1 为什么CPU/内存指标不靠谱?
用CPU或内存利用率触发扩缩容,在AI推理场景下相当危险。CPU可能才30%,GPU已经满载;内存还有余量,显存早就溢出了。基于错误指标的扩缩容,要么扩得太慢导致服务超时,要么缩得太狠引发雪崩。
2.2 vLLM暴露的关键指标
vLLM会暴露一系列精准的业务指标,可通过Prometheus抓取,再经Prometheus Adapter转换为Kubernetes自定义指标,供HPA使用。
以下是最常用的几个指标:
| 指标名 | 含义 | 适用场景 |
|---|---|---|
vllm:num_requests_waiting |
队列中等待的请求数 | 吞吐优先,队列堆积时扩容 |
vllm:num_requests_running |
当前正在处理的请求数 | 直接负载指标 |
vllm:gpu_cache_usage_perc |
KV Cache利用率(0-1) | 延迟敏感型,提前扩容 |
关于指标选择,有两条经验法则:
- 吞吐优先:使用
num_requests_waiting。当队列开始堆积时扩容,让单实例充分消化后再加副本。 - 延迟敏感:使用
gpu_cache_usage_perc。KV Cache接近满载意味着新请求可能排队,需提前扩容。
HPA扩缩容的计算公式为:
期望Pod数 = ceil[当前Pod数 × (当前指标值 / 目标指标值)]
例如,当前每Pod平均有10个请求在等待,目标设为5,则HPA会扩容至ceil(1 × 10/5)=2个Pod。
2.3 将vLLM指标暴露给Prometheus
在K8s集群中,先通过PodMonitor配置让Prometheus抓取vLLM的/metrics端点:
apiVersion: monitoring.googleapis.com/v1
kind: PodMonitoring
metadata:
name: vllm-pod-monitoring
spec:
selector:
matchLabels:
app: vllm-tpu
endpoints:
- path: /metrics
port: 8000
interval: 15s
然后配置Prometheus Adapter,将vLLM指标注册为Kubernetes自定义指标:
rules:
# 将 vllm:num_requests_waiting 转为自定义指标
- seriesQuery: '{__name__=~"^vllm:num_requests_waiting$"}'
resources:
overrides:
namespace:
resource: "namespace"
name:
matches: ""
as: "vllm_num_requests_waiting"
metricsQuery: sum by(namespace) (vllm:num_requests_waiting)
2.4 实战HPA配置:基于等待队列数扩容
以下是一个推荐的HPA配置示例,使用num_requests_waiting作为主要扩缩依据:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-hpa
namespace: llm-services
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-deployment
minReplicas: 2
maxReplicas: 20
metrics:
# 主指标:平均每个Pod等待队列长度
- type: Pods
pods:
metric:
name: vllm_num_requests_waiting
target:
type: AverageValue
averageValue: 5 # 当每个Pod平均等待>5个请求时扩容
# 辅助指标:KV Cache利用率(作为二次保险)
- type: Pods
pods:
metric:
name: vllm_gpu_cache_usage_perc
target:
type: AverageValue
averageValue: 70 # 百分比
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 30 # 每30秒最多翻倍
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 120 # 保守缩容,防止震荡
关键参数解读:
- 扩容稳定窗口(60秒):快速响应负载增加。
- 缩容稳定窗口(300秒):避免处理长请求时副本被过早缩减。
- 扩容策略:每30秒最多扩容100%,快速拉起副本应对洪峰。
- 缩容策略:每120秒最多缩容10%,非常保守,防止反复震荡。
三、高级弹性方案:KEDA + 外部指标
标准的HPA有一个局限:它主要依赖Pod级别的指标(如Pods类型)。但有时我们需要基于聚合后的全局指标来扩容,比如整个服务所有Pod的等待请求总数。
这时KEDA(Kubernetes Event-driven Autoscaling)就派上了用场。KEDA扩展了HPA的能力,支持从Prometheus等外部数据源拉取指标,并执行更灵活的扩缩规则。
3.1 KEDA配置示例
以下是一个KEDA ScaledObject配置,基于聚合的num_requests_waiting总和进行扩容:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: vllm-keda-scaler
namespace: llm-services
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-deployment
minReplicaCount: 2
maxReplicaCount: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitoring.svc:9090
metricName: vllm_waiting_total
query: sum(vllm:num_requests_waiting{namespace="llm-services"})
threshold: '10' # 所有Pod等待总数超过10则扩容
activationThreshold: '3' # 低于3则缩容
KEDA的优势:
- 支持从0扩容(适合serverless场景)。
- 支持更丰富的触发源(Prometheus、Kafka、RabbitMQ等)。
- 可以通过
activationThreshold设置“死区”,避免小流量时频繁震荡。
四、冷启动优化:让扩容真正“快起来”
弹性扩缩最大的挑战是冷启动时间——新Pod从启动到模型加载完成,往往需要数十秒甚至数分钟。如果扩容信号发出了,但新Pod迟迟不能服务,扩容就形同虚设。
以下四招能显著缩短冷启动时间:
4.1 预构建镜像与模型量化
将vLLM依赖、Python环境、甚至模型权重预置在镜像中,避免每次拉取。同时使用GPTQ/AWQ量化将模型体积压缩30%-50%,加载速度提升3-5倍。
4.2 共享缓存卷(Cloud Storage FUSE / NFS)
将模型权重挂载为共享卷,多个Pod复用同一份缓存,避免每个Pod重复下载上百GB的模型文件。Google Cloud的GKE提供了GCSFuse CSI驱动,可将Cloud Storage桶挂载为PV,支持并行下载,极大加速模型加载。
volumes:
- name: gcs-fuse-csi-ephemeral
csi:
driver: gcsfuse.csi.storage.gke.io
volumeAttributes:
bucketName: my-model-bucket
mountOptions: "file-cache:enable-parallel-downloads:true,file-cache:parallel-downloads-per-file:100"
4.3 就绪探针(Readiness Probe)
配置TCP探针,仅当模型完全加载后才将Pod纳入服务流量:
readinessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 15
periodSeconds: 10
4.4 HAMi:让单卡跑多个模型,减少扩容需求
如果GPU资源紧张,还可以考虑用HAMi(GPU虚拟化调度器)在单张GPU上切分显存运行多个模型。vLLM Production Stack已原生支持HAMi参数。配置示例:
requestGPU: 1
limitGPU: 1
requestGPUMem: "14000" # 申请14GB显存
limitGPUMem: "14000"
podAnnotations:
hami.io/gpu-scheduler-policy: "binpack" # 尽量堆叠到同一张卡
这种方式可以通过提高单卡利用率来降低扩容的迫切性,间接优化弹性效率。
五、行业实践:Pinterest与Uber的验证
这套技术栈并非纸上谈兵,已在多家头部公司生产环境中得到验证。
Pinterest将Kubernetes + Ray + PyTorch + vLLM组合用于批量推理,实现了4.5倍吞吐量提升、30倍成本下降,每月运行约1800个推理作业。
Uber将其LLM训练与推理工作负载迁移至Kubernetes,基于此架构实现了2-3倍吞吐量提升,覆盖从7B到70B模型的微调与推理,批量推理规模达每月数千个作业。
总结
vLLM + PyTorch在K8s集群中实现高效弹性扩缩容,核心有三层:
- 底层(内功):依赖PagedAttention和Continuous Batching提升单实例性能上限,让每个Pod能扛更多并发。
- 中层(决策):通过HPA + 自定义指标(
num_requests_waiting、gpu_cache_usage_perc)或KEDA外部指标,实现精准的负载感知扩缩容,避免基于CPU/内存的“瞎扩”。 - 上层(提速):通过预构建镜像、模型量化、共享缓存卷、就绪探针等手段优化冷启动时间,让扩容真正“快起来”。
最终达到的效果是:单机扛得住、集群扩得快、成本压得低——这正是生产级LLM服务架构的理想状态。
更多推荐

所有评论(0)