Day 10:MIG 硬切片 + AWQ 量化 + HPA Custom Metrics
LLM 推理上生产时最头疼的三件事:一张 A100/A800 怎么给多个模型分着用、7B/13B 模型怎么塞进切片后的小显存、QPS 上来时怎么扩 Pod 才不会反应慢半拍。这一天把这三件事一次性拉通:A800 切 3 个 MIG slice,单 slice 上跑 Qwen2.5-7B-AWQ Q4 量化模型,HPA 不看 CPU 而是看 vLLM 的 num_requests_waiting 队列深度做弹性。
整篇按 A → C 3 段走,每段把命令、原理、真坑串在一起。
环境前置
GPU 节点单台:A800 40GB PCIe(Ampere,支持 MIG)、Ubuntu 22.04、nvidia-driver 535+、K3s v1.30 单节点、containerd(K3s 内置)。nvidia-container-toolkit 和 default_runtime_name = "nvidia" 已经在 Day 8 配好,本文从 MIG 启用开始。
A. MIG 硬切片:A800 切 3 × 2g.10gb
A.1 三种 GPU 共享方式对比
| 方案 | 隔离 | 支持硬件 | 故障隔离 | 性能可预测性 |
|---|---|---|---|---|
| MIG | 硬件级(独立 SM + 显存 + L2 + 带宽) | A100/A800/H100/H200 等数据中心卡 | 有 | 强 |
| MPS | 软件级(共享 CUDA context) | 任意 NVIDIA GPU | 无(client 崩拖死 server) | 弱 |
| time-slicing | 调度级(device-plugin 假装多 GPU) | 任意 | 无 | 最弱(串行排队) |
生产 LLM 多租户几乎只能选 MIG:MPS 没故障隔离,一个 vLLM OOM 把整张卡上其他模型也带挂;time-slicing 串行排队,推理 p99 直接爆。
A800-40G 切片 profile 候选:1g.5gb × 7、2g.10gb × 3、3g.20gb × 2、7g.40gb × 1。本文选 2g.10gb × 3 —— 刚好对应「baseline 模型 + 量化大模型 + HPA 扩容预留」,每个 10GB 够装 AWQ Q4 的 7B 模型 + KV cache。
A.2 启 MIG + 创建 instance
MIG 启用要求 GPU 上无活进程,先清场:
# 清掉所有用 GPU 的负载
k3s kubectl delete deploy -n vllm --all
k3s kubectl delete ds -n kube-system nvidia-device-plugin-daemonset
k3s kubectl delete ds -n gpu-monitoring dcgm-exporter
# 开 MIG mode(pending,需要 GPU reset 才生效)
nvidia-smi -i 0 -mig 1
nvidia-smi --gpu-reset
# 创 3 个 GPU Instance + 自动建配套 Compute Instance(-C flag)
nvidia-smi mig -cgi 2g.10gb,2g.10gb,2g.10gb -C
nvidia-smi -L 应看到 3 个独立 MIG device、各自 UUID:
MIG 2g.10gb Device 0: UUID: MIG-25148f1b-...
MIG 2g.10gb Device 1: UUID: MIG-ede38a71-...
MIG 2g.10gb Device 2: UUID: MIG-ab84bd1e-...
GPU Instance(GI)和 Compute Instance(CI)的区别:GI 是显存 + SM 的硬件分区,CI 是 GI 之上的 CUDA context。-C 让 nvidia-smi 自动 1:1 建 CI,正常推理一个 GI 配一个 CI 即可。如要在一个 GI 里再分多 CUDA context(少见)用 nvidia-smi mig -cci。
A.3 真坑 1:Xorg 占住 /dev/nvidia0,MIG 启不了
第一次 nvidia-smi -mig 1 报:
Unable to enable MIG Mode for GPU 00000000:00:08.0: In use by another client
lsof /dev/nvidia0 暴露 Xorg 持有。Ubuntu Desktop 默认 graphical.target,GDM 启 Xorg、Xorg mmap GPU。修法:
systemctl set-default multi-user.target # 改默认 runlevel = 无 GUI
systemctl isolate multi-user.target # 立刻切
pkill -9 Xorg # 杀残留
生产 GPU 节点直接用 Ubuntu Server(无 GUI),装机时选 nvidia-driver-server 包,不要 Desktop 系统改。
A.4 装 MIG-aware Device Plugin
NVIDIA Device Plugin 通过 MIG_STRATEGY env 控制 3 种 MIG 模式:
| 值 | 行为 | 资源名 |
|---|---|---|
none | 不识别 MIG,整卡当 1 GPU | nvidia.com/gpu: 1 |
single | 所有 slice 同 profile,统一暴露 | nvidia.com/gpu: 3 |
mixed | 不同 profile 共存 | nvidia.com/mig-2g.10gb: 3 |
3 个 slice 全 2g.10gb,选 single —— Deployment 写 nvidia.com/gpu: 1 就拿一个 slice,不用动 Pod spec。
# nvidia-device-plugin DaemonSet 关键片段
env:
- {name: MIG_STRATEGY, value: "single"}
- {name: NVIDIA_VISIBLE_DEVICES, value: "all"}
securityContext:
privileged: true # <- 不加这个 Pod CrashLoopBackOff
A.5 真坑 2:Device Plugin 没开 privileged → Insufficient Permissions
Plugin Pod 起来后 CrashLoopBackOff:
error getting parent memory info: Insufficient Permissions
MIG 模式下 device-plugin 要读 GPU 的 parent device(物理卡)枚举 slice 拓扑,这个 ioctl 需要 CAP_SYS_ADMIN。非 MIG 模式不需要 privileged,所以网上很多老 yaml 没开,抄过来用在 MIG 节点必踩。
验证 K8s 看到 3 个 GPU 资源:
k3s kubectl get node ubuntu22 -o jsonpath='{.status.allocatable}' | jq '."nvidia.com/gpu"'
# "3"
3 个 slice 各占 28 SM(A800 总 108 SM,留 24 SM 给 system reserved)、各 9.75 GiB 显存(10GB 标称扣 250 MiB driver header)。
B. Qwen2.5-7B-AWQ 量化 + 双模型同卡
B.1 量化方法对比
7B 模型 BF16 原始 14GB,单 MIG slice 10GB 装不下,必须量化。Qwen2.5-7B 在 MIG 2g.10gb(9.75 GiB 可用)上的实测:
| 方法 | 位宽 | 权重 | 总占用(含 KV) | 精度损失 | calibration | 硬件 |
|---|---|---|---|---|---|---|
| BF16(原始) | 16 | 14.0 GB | ~16.0 GB → 装不下 | 0 | — | 任意 |
| AWQ Q4 | int4 | 3.6 GB | ~5.6 GB ✓ | < 5% | 不需要 | Ampere+ |
| GPTQ Q4 | int4 | 3.7 GB | ~5.7 GB ✓ | < 5% | 需要 128 样本 | Ampere+ |
| int8 W8A8 | int8 | 7.0 GB | ~9.0 GB 勉强 | < 2% | 不需要 | 任意 |
| FP8 | fp8 | 7.0 GB | ~9.0 GB 勉强 | < 1% | 不需要 | 仅 H100/H200 |
选 AWQ:免 calibration、HuggingFace 现成 Qwen2.5-7B-Instruct-AWQ 仓库直接拉、vLLM 原生支持。
量化对 latency 的影响:AWQ INT4 矩阵乘在 Ampere Tensor Core 有专门 INT4 kernel,batch=1 比 BF16 反而快 10-15%(计算访存比变小);大 batch 下因 dequant overhead 可能略输 BF16。
B.2 双 vLLM Deployment:baseline + 量化
# vllm-3b: 标准 BF16 baseline
apiVersion: apps/v1
kind: Deployment
metadata: {name: vllm-3b, namespace: vllm}
spec:
template:
spec:
containers:
- name: vllm
image: docker.m.daocloud.io/vllm/vllm-openai:v0.6.5
args: ["--model", "Qwen/Qwen2.5-3B-Instruct",
"--served-model-name", "qwen2.5-3b",
"--gpu-memory-utilization", "0.85",
"--max-model-len", "2048"]
resources:
limits: {nvidia.com/gpu: 1}
---
# vllm-7b-awq: AWQ 量化 7B,args 加 --quantization awq
# model 换成 Qwen/Qwen2.5-7B-Instruct-AWQ,其他配置不变
K8s scheduler 把每个 nvidia.com/gpu: 1 自动分到独立 MIG slice,第 3 个 slice 留给 HPA 扩容。不需要 Pod spec 指定 slice UUID,device-plugin 自动分配。
B.3 真坑 3:vLLM bool flag 不能接 =false
加 --disable-log-stats=false 想保留 stats,vLLM 直接退出:
error: argument --disable-log-stats: ignored explicit argument 'false'
vLLM 用 argparse store_true,flag 只能写 --disable-log-stats(出现 = True),不接显式 value。修法:删掉这个 arg,默认就 enable stats(HPA 需要 /metrics)。
argparse store_true 是 Python 圈最反直觉的陷阱,docker yaml 写惯 key=value 必踩。
B.4 MIG slice 容量精确验证
vLLM 启动 log 自报显存分配(Qwen2.5-3B BF16):
total_gpu_memory (9.75GiB) × gpu_memory_utilization (0.85) = 8.29GiB
model weights take 5.79GiB
non_torch_memory takes 0.14GiB
activation peak memory takes 1.39GiB
KV Cache reserved 0.96GiB
MIG 2g.10gb 实测可用 9.75 GiB。KV cache 0.96 GiB 折算 ~14 个 2048-token 并发 —— 单 slice 跑 3B 的瓶颈是 KV cache 不是计算,这也是 HPA 该看 num_requests_waiting(队列堆积)而非 GPU util 的根因。
B.5 推理验证 + 关键洞察
curl http://vllm-3b-svc.vllm:8000/v1/chat/completions \
-d '{"model":"qwen2.5-3b","messages":[{"role":"user","content":"用一句话说 Kubernetes 是什么"}],"max_tokens":50}'
# Kubernetes 是一个开源的容器编排系统,用于自动化部署、扩展和管理容器化应用程序。
单并发 tok/s 实测:
| 模型 | 显存占用 | tok/s(batch=1) | 质量主观分 |
|---|---|---|---|
| Qwen2.5-3B BF16 | 5.8 GB | ~115 tok/s | 良 |
| Qwen2.5-7B-AWQ Q4 | 3.8 GB | ~80 tok/s | 优 |
7B-AWQ 显存比 3B-BF16 还小(3.8 vs 5.8 GiB),因为 AWQ 4× 压缩比超过 7B/3B 的 2.3× 参数比。生产直觉应该改成「能跑量化大模型就不跑全精度小模型」—— 7B 量化版的能力远高于 3B 全精度,~5% 精度损失业务场景里基本看不出来。
B.6 vLLM 暴露的 HPA-friendly 指标
/metrics endpoint 给出 14+ Prometheus 指标,HPA 角度最重要的几个:
vllm:num_requests_waiting ← HPA 黄金 signal
vllm:num_requests_running # 当前并发处理中
vllm:gpu_cache_usage_perc # KV cache 占用 %(次选 signal)
vllm:num_preemptions_total # KV cache 满抢占次数(告警)
vllm:avg_generation_throughput_toks_per_s
vllm:request_success_total{finished_reason="stop"}
num_requests_waiting 是 HPA 金标准:队列空 = 容量富裕、队列长 = 容量不够,跟弹性目标 1:1 对应。
C. HPA Custom Metrics:从 vLLM 队列深度做弹性
C.1 为什么 LLM HPA 不能用 CPU / GPU util
普通 web 服务 HPA 看 CPU 利用率就够,LLM 推理完全不通:
| signal | 问题 |
|---|---|
| CPU 利用率 | LLM CPU 永远低(GPU 才是瓶颈)。70% 阈值 HPA 永不触发,GPU 早就排队 |
| GPU 利用率 | Pod 在跑就持续 80%+,没法区分「忙得过来」和「忙不过来」 |
| QPS | prompt / output 长度差异巨大,QPS 不反映真实负载 |
num_requests_waiting | 队列堆积 = 后端处理不过来,1:1 对应「该扩容了」 |
生产 LLM HPA 的黄金组合:
- 主信号:
vllm:num_requests_waiting> 5 → scale up - 次信号:
vllm:gpu_cache_usage_perc> 80% → scale up(防 KV cache 抢占) - scale down:两个信号连续 5 分钟为 0 → 缩 1 个 replica
C.2 HPA Custom Metrics 完整链路
vLLM num_requests_waiting 进 HPA 有两条路:
路径 1(标准): vLLM /metrics → Prometheus scrape → prometheus-adapter
→ 注册成 custom.metrics.k8s.io / external.metrics.k8s.io
→ HPA controller 读 metrics.k8s.io → 扩缩决策
路径 2(推荐): vLLM /metrics → Prometheus → KEDA operator
→ 直接 patch Deployment replicas → 扩缩决策
| 维度 | Prometheus Adapter | KEDA |
|---|---|---|
| 安装复杂度 | 中(写 rules 映射 metric 名) | 低(CRD 配置) |
| 数据源 | 仅 Prometheus | 30+(Prom、Kafka、Redis、SQS...) |
| scale to 0 | 不支持 | 支持 |
| 生产推荐 | 同质化大集群 | LLM / 事件驱动场景 |
LLM 场景推荐 KEDA:scale to 0 对夜间空闲尤其值钱(H100 ¥30+/小时,闲置 0 副本省钱)。
C.3 KEDA 方案
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: {name: vllm-3b-scaler, namespace: vllm}
spec:
scaleTargetRef:
name: vllm-3b
minReplicaCount: 1
maxReplicaCount: 3 # 不超过 MIG slice 数
pollingInterval: 10
cooldownPeriod: 300 # 5min scale-down stabilization
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.prom:9090
threshold: '5'
query: sum(vllm:num_requests_waiting{model_name="qwen2.5-3b"})
maxReplicaCount: 3 必须跟 MIG slice 数对齐 —— 扩到 4 个 Pod 时第 4 个 Pending 因为没有 GPU 资源。
C.4 Prometheus Adapter 方案
# HPA 用 External metric(K8s metric 名不能含冒号,adapter rule 里 rename)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: vllm-3b-hpa, namespace: vllm}
spec:
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: vllm-3b}
minReplicas: 1
maxReplicas: 3
metrics:
- type: External
external:
metric:
name: vllm_num_requests_waiting # 冒号 → 下划线
selector: {matchLabels: {model_name: qwen2.5-3b}}
target: {type: AverageValue, averageValue: "5"}
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies: [{type: Pods, value: 2, periodSeconds: 30}]
scaleDown:
stabilizationWindowSeconds: 300
policies: [{type: Pods, value: 1, periodSeconds: 60}]
C.5 真坑 4:HPA 反应慢的根因是 scrape interval
HPA 默认 --horizontal-pod-autoscaler-sync-period=15s(每 15s 决策一次),但实际反应速度由最慢的一环决定:
vLLM /metrics 更新(实时)
→ Prometheus scrape(默认 15-60s) ← 最慢
→ prometheus-adapter / KEDA 查询(10s)
→ HPA 决策(15s)
→ Pod 启动(30s-2min for LLM)
把 Prometheus scrape 调到 5s(仅对 vLLM job),HPA 反应能从 60s+ 压到 20s 内:
scrape_configs:
- job_name: vllm
scrape_interval: 5s # 仅此 job 5s,不动全局
static_configs:
- targets: ['vllm-3b:8000', 'vllm-7b-awq:8000']
线上反向坑:HPA 配 stabilizationWindowSeconds: 0 想立即扩,但 Prometheus 默认 60s scrape,实际还是 1 分钟才反应过来。盯着 HPA 调半天没用,根因在 Prometheus。
C.6 LLM 弹性的冷启动陷阱
num_requests_waiting > 5 触发扩容 → KEDA patch replicas → K8s 调度新 Pod → 拉 image + load 权重 + warmup:image pull 6GB(10-60s)+ 模型权重 load 4-15GB(15-30s)+ CUDA graph capture(5-15s),总冷启动 30-120s。期间老 Pod 排队,扩容反而可能让 p99 更糟(请求路由到没 warmup 完的新 Pod)。
三个保命做法:
- 预拉镜像:DaemonSet 跑
crictl pull把镜像预热到所有 GPU 节点 - 模型 hostPath / PVC 缓存:权重落本地 SSD,新 Pod mount 现成的
- readinessProbe 严格化:不只
/health200,要真跑一次 mini 推理才 Ready(initialDelaySeconds: 30 + failureThreshold: 12)
最痛反面教材:HPA 扩容后新 Pod 5 秒就 Ready(其实还在 warmup),Service 把请求打过去,第一波几十个请求 p99 飙 30s+,触发更多扩容,雪崩。
面试常见题
Q1:MIG vs MPS vs time-slicing 三种 GPU 共享方案怎么选?
3 个维度对比:
| 维度 | MIG | MPS | time-slicing |
|---|---|---|---|
| 隔离 | 硬件级 | 软件级 | 调度级(串行) |
| 故障隔离 | 有 | 无 | 无 |
| 性能可预测性 | 强 | 弱 | 最弱 |
| 硬件支持 | Ampere+ 数据中心卡 | 任意 | 任意 |
选型:生产多租户 / 多模型 → MIG;自家服务能容忍干扰 → MPS;dev / 测试凑合 → time-slicing。RTX 4090 不支持 MIG,只能 MPS。
深问:MIG 切片粒度能动态调吗? 不能,换 profile 必须全删 GI 再重建,期间 GPU 不可用。生产做切片决策一次到位。
Q2:AWQ / GPTQ / FP8 / int8 量化方法对推理 latency / 精度 / 显存有什么影响?
显存:AWQ / GPTQ Q4 4× 压缩;FP8 / int8 2× 压缩。latency:AWQ batch=1 比 BF16 反而快 10-15%(INT4 Tensor Core),大 batch 略输;FP8 与 BF16 持平。精度损失:AWQ/GPTQ < 5%、int8 < 2%、FP8 < 1%。硬件:AWQ/GPTQ 需要 Ampere+、FP8 仅 H100/H200、int8 任意卡。
工程角度:A100/A800 选 AWQ(免 calibration),H100 选 FP8(精度最好且硬件加速)。「能跑量化大模型就不跑全精度小模型」是 2024+ 生产共识 —— 7B-AWQ 能力远超 3B 全精度。
Q3:HPA 的 Resource / Pods / Object / External 四种 metric 类型有什么区别?
Resource:CPU / 内存,数据从 metrics-server 来Pods:每个 Pod 一个值(如 RPS),HPA 对所有 Pod 求平均后跟阈值比Object:关联到某个具体 K8s 对象(如 Ingress 的 latency),单个值External:跟 K8s 对象无关的外部指标(如 SQS 队列长度、Kafka lag)
vLLM num_requests_waiting 用 Pods(per-Pod 求平均)或 External(Prometheus query 里 sum 过)都行,生产更常用 External。
Q4:HPA Custom Metrics 完整链路是什么?慢了从哪里查?
6 跳:
应用 /metrics 暴露
→ Prometheus scrape(scrape_interval)
→ prometheus-adapter 注册成 metrics.k8s.io API
→ HPA controller 查 API(sync-period 15s)
→ HPA 算 desiredReplicas
→ Deployment 扩缩 + Pod warmup
慢的根因 90% 在 Prometheus scrape_interval(默认 15-60s)。把 vLLM job 单独调到 5s,HPA 反应从 60s+ 压到 20s 内。进阶:KEDA 替代 prometheus-adapter,少一跳且支持 scale to 0。
Q5:生产 LLM 弹性怎么避免冷启动炸 GPU?
冷启动 4 个慢点:image pull 6GB(10-60s)+ 权重 load 4-15GB(15-60s)+ CUDA graph capture(5-15s)+ 首请求 warmup(5-10s)。
4 个保命做法:
- 预拉镜像:DaemonSet 跑
crictl pull预热所有节点 - 模型 hostPath / PVC 缓存:权重落本地 SSD,新 Pod mount 现成的
- readinessProbe 严格化:不只
/health200,要真跑一次 mini 推理才 Ready - 预扩容 standby:HPA 触发后留 1 个 warmup 中的 standby Pod,warmup 完才接流量
最痛反面教材:新 Pod 5 秒就 Ready(其实还在 warmup),Service 把请求打过去,第一波 p99 飙 30s+,触发更多扩容雪崩。
下一步
Day 10 结束后这台 A800 已具备生产 LLM 推理集群的全部要素:MIG 硬切片隔离、AWQ 量化让 7B 跑在 10GB slice 上、HPA 看队列深度做智能弹性。
Day 11 进入多模型路由:一台 GPU 上同时跑 3-5 个不同模型,用 vLLM 的 LoRA adapter 切换或上层 router(LiteLLM / 自建 OpenRouter)做 model-as-a-service,再叠 token 计费 / quota / 多租户隔离。