
那天是周三,香港葵涌机房冷通道显示 18°C。风口噪声跟服务器告警声混在一起,我披着工单夹克蹲在 42U 柜前,盯着 A100 的指示灯怔了十几秒。白天高峰我们的文生图接口被打穿,Stable Diffusion XL 的队列像春运,一度 p95 超过 12 秒,用户在群里刷屏“慢”。运营说要“今晚缓一口”,我知道靠“再加一台”已经治标不治本了。
这次我决定把一张 A100 80GB PCIe 切成 MIG(Multi-Instance GPU) 的多份小“显卡”,把并发跑开、把尾延迟压下去。以下就是这次在 香港服务器 + Ubuntu LTS 上从零到落地的全过程:环境、分区、容器/K8s 调度、Diffusers/TensorRT 优化、监控,以及所有踩过的坑和应对手法。我尽量把每一步写到“新手能照抄、老手能复用”,同时保留一线场景的细节与温度。
1. 现场环境与目标
1.1 硬件与系统(实配)
| 项目 | 规格 |
|---|---|
| 机房 | 香港,10Gbps 上联,低时延对内地/东南亚访问友好 |
| 主机 | 2× Intel Xeon Gold 6426Y / 512GB DDR4 / 2× NVMe 3.84TB(RAID1) |
| GPU | NVIDIA A100 80GB PCIe × 1(目标也适用于 H100/A30/A100 40G,对应的 MIG 配置不同) |
| 网卡 | 2× 10GbE(Bonding active-backup) |
| 系统 | Ubuntu 22.04 LTS(内核 5.15+) |
| 驱动/工具 | NVIDIA Driver 550.x、CUDA 12.4、cuDNN 9.x、nvidia-container-toolkit 1.16+ |
目标:把一张 A100 切成多块“独立小 GPU”,让 高并发小作业(如 SD1.5/SD-Turbo 512×512)走小分区;把 大模型/高分辨(如 SDXL 1024×1024、复杂 ControlNet)放中/大分区。提升并发下的稳定吞吐,压低 p95。
2. 为什么 MIG 能救命(简明但不失深度)
- 硬隔离:MIG 把 SM、HBM、Cache 等硬件切成多个“实例”。每个实例有独立资源,互不抢占,尾延迟稳定。
- 粒度匹配:小请求不再和“大单子”抢整卡。以前 1 个大请求就能把整卡的显存与计算拉满,小请求在队列里等;有了 MIG,小请求各跑各的。
- 可预测:每个分区的算力大致是“整卡的 1/n”,延迟更可预期,调度更简单。
- 经验法则:A100 80GB 的 MIG 切片以“10GB 一档”。1g.10gb 适合 SD1.5/SD-Turbo 类;3g.40gb 能稳跑 SDXL;业务高峰时用 “2×3g.40gb + 1×1g.10gb” 或 “3×2g.20gb + 1×1g.10gb” 是比较通用的混合布局。
3. 系统与驱动准备(Ubuntu)
维护窗口内完成;开启 MIG 会触发 GPU reset,务必先 drain 流量。
# 0) 基础包
sudo apt update && sudo apt -y install build-essential dkms pciutils jq curl gnupg lsb-release
# 1) 安装 NVIDIA 驱动(示例:550)
# 建议使用官方 repo 或数据中心镜像,确保与目标 CUDA 版本匹配
sudo ubuntu-drivers autoinstall
# 或者手动装 nvidia-driver-550
sudo apt -y install nvidia-driver-550
# 2) CUDA & cuDNN(与驱动匹配)
# 这里略去 repo 配置,按官方流程安装
sudo apt -y install cuda-toolkit-12-4
# 3) 容器运行时 + NVIDIA 容器工具包
sudo apt -y install docker.io
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update && sudo apt -y install nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
A100 SXM 机型通常需要 nvidia-fabricmanager 服务;PCIe 机型一般也可安装并启动,兼容性更好:
sudo apt -y install nvidia-fabricmanager-550
sudo systemctl enable --now nvidia-fabricmanager
4. 启用 MIG 并创建分区(nvidia-smi 实操)
强提示:启用 MIG 会重置 GPU,终止现有上下文。请先停止业务容器或切走流量。
# 启用 MIG
sudo nvidia-smi -i 0 -mig 1
# 观察 MIG 状态
nvidia-smi -i 0
# 列出可用的 GPU Instance Profile(不同 GPU 型号输出不同)
sudo nvidia-smi mig -i 0 -lgip
# 列出可用的 Compute Instance Profile
sudo nvidia-smi mig -i 0 -lcip
以 A100 80GB 为例,常见的 GI/CI 组合(简表,用于理解):
| GI(GPU Instance) | 可创建数量(满载) | 典型用途 |
|---|---|---|
| 1g.10gb | 7 | SD1.5/SD-Turbo 512×512 单请求、轻 ControlNet |
| 2g.20gb | 3(+余 1g) | SD1.5 大分辨率、轻 SDXL |
| 3g.40gb | 2(+余 1g) | SDXL 1024×1024、较重 ControlNet |
| 7g.80gb | 1 | 整卡直用,高批次推理/训练 |
不同驱动版本中 Profile ID 数字可能不同,不要硬背 ID。用脚本通过名称匹配最稳。
4.1 一键脚本:创建“2×3g.40gb + 1×1g.10gb”混合布局
这个布局能同时兼顾 SDXL(两个 3g 分区)与 轻任务(一个 1g 分区)。
#!/usr/bin/env bash
set -euo pipefail
GPU_IDX=${1:-0}
echo "[*] Enabling MIG on GPU $GPU_IDX (if not already)..."
sudo nvidia-smi -i $GPU_IDX -mig 1 >/dev/null
echo "[*] Deleting old compute & gpu instances..."
# 删除旧的 CI/GI
for gi in $(sudo nvidia-smi mig -i $GPU_IDX -lgi | awk '/GPU instance ID/ {print $4}'); do
for ci in $(sudo nvidia-smi mig -i $GPU_IDX -lci | awk '/Compute Instance ID/ {print $4}'); do
sudo nvidia-smi mig -i $GPU_IDX -dci -gi $gi -ci $ci || true
done
sudo nvidia-smi mig -i $GPU_IDX -dgi -gi $gi || true
done
# 根据名称找到 GI/CI Profile ID
gi_id_by_name() {
local name="$1"
sudo nvidia-smi mig -i $GPU_IDX -lgip | awk -v n="$name" '$0 ~ n {print $5; exit}'
}
ci_id_by_name() {
local name="$1"
sudo nvidia-smi mig -i $GPU_IDX -lcip | awk -v n="$name" '$0 ~ n {print $6; exit}'
}
GI_1G=$(gi_id_by_name "1g.10gb")
GI_3G=$(gi_id_by_name "3g.40gb")
CI_1G=$(ci_id_by_name "1c.10gb")
CI_3G=$(ci_id_by_name "3c.40gb")
echo "[*] Create 3g.40gb x2 ..."
for _ in 1 2; do
sudo nvidia-smi mig -i $GPU_IDX -cgi $GI_3G -C
done
echo "[*] Create 1g.10gb x1 ..."
sudo nvidia-smi mig -i $GPU_IDX -cgi $GI_1G -C
echo "[*] Create compute instances in each GPU instance..."
for gi in $(sudo nvidia-smi mig -i $GPU_IDX -lgi | awk '/GPU instance ID/ {print $4}'); do
# 检测该 GI 是 3g 还是 1g
if sudo nvidia-smi mig -i $GPU_IDX -lgi | sed -n "/GPU instance ID\s\+$gi/,/UUID/p" | grep -q "3g.40gb"; then
sudo nvidia-smi mig -i $GPU_IDX -gi $gi -cci $CI_3G -C
else
sudo nvidia-smi mig -i $GPU_IDX -gi $gi -cci $CI_1G -C
fi
done
echo "[*] MIG layout done:"
sudo nvidia-smi -L
运行后 nvidia-smi -L 能看到形如 MIG-GPU-xxxxxxxx/xx/… 的 MIG 设备 UUID。这就是容器/K8s 调度时要用到的“显卡编号”。
4.2 开机自动应用(systemd)
# /usr/local/sbin/mig-layout.sh 放上述脚本
sudo install -m 0755 mig-layout.sh /usr/local/sbin/mig-layout.sh
cat | sudo tee /etc/systemd/system/mig-layout.service >/dev/null <<'EOF'
[Unit]
Description=Apply NVIDIA MIG Layout
After=nvidia-persistenced.service nvidia-fabricmanager.service docker.service
Requires=nvidia-persistenced.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/mig-layout.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mig-layout.service
5. 容器化接入:Docker 直连与 Kubernetes 调度
5.1 Docker:按 MIG UUID 绑定设备
# 列出 MIG UUID
nvidia-smi -L
# 绑定到某个 MIG 设备(示例)
MIG_UUID="MIG-GPU-3e7d0c2c-.../1/0"
docker run --rm \
--gpus "device=${MIG_UUID}" \
-e NVIDIA_VISIBLE_DEVICES=${MIG_UUID} \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
nvcr.io/nvidia/pytorch:24.08-py3 nvidia-smi -L
要点:--gpus "device=..." 与 NVIDIA_VISIBLE_DEVICES 同步使用最省心,容器内部只看到绑定的那块 MIG 设备。
5.2 Kubernetes:GPU Operator + Device Plugin(推荐)
部署 NVIDIA GPU Operator(包含驱动、Device Plugin、DCGM Exporter 等),并开启 MIG mixed 策略;
Pod 里按 资源名 申请,如 nvidia.com/mig-1g.10gb: 1。
示例(仅展示关键段落):
# values for gpu-operator (Helm)
mig:
strategy: mixed
# 工作负载 Deployment(SDXL 服务请求 1 个 3g.40gb)
apiVersion: apps/v1
kind: Deployment
metadata:
name: sdxl-service
spec:
replicas: 2
template:
spec:
containers:
- name: app
image: yourrepo/sdxl-serving:latest
resources:
limits:
nvidia.com/mig-3g.40gb: "1"
env:
- name: NVIDIA_VISIBLE_DEVICES
valueFrom:
fieldRef:
fieldPath: status.allocatable # GPU Operator 会注入正确的 MIG 设备
提示:不同版本的 Device Plugin 资源名可能略有差异,部署时以集群实际导出的资源名为准(kubectl describe node <node> 查看)。
6. 模型侧优化:Diffusers / TensorRT / 内存技巧(实战配方)
6.1 PyTorch + Diffusers(轻量快起)
# app/pipeline.py
import torch, os
from diffusers import StableDiffusionPipeline
model_id = os.getenv("MODEL_ID", "runwayml/stable-diffusion-v1-5") # 或 SDXL
device = "cuda"
pipe = StableDiffusionPipeline.from_pretrained(
model_id, torch_dtype=torch.float16
).to(device)
pipe.enable_model_cpu_offload(False)
pipe.enable_vae_slicing()
pipe.enable_xformers_memory_efficient_attention()
# PyTorch 2.4+ 编译加速(谨慎开启,首次会有编译开销)
pipe.unet = torch.compile(pipe.unet)
torch.backends.cuda.matmul.allow_tf32 = True
torch.set_float32_matmul_precision("high")
def infer(prompt: str, h=512, w=512, steps=20, seed=42):
g = torch.Generator(device=device).manual_seed(seed)
with torch.inference_mode(), torch.autocast("cuda"):
img = pipe(prompt, height=h, width=w, num_inference_steps=steps, generator=g).images[0]
return img
小技巧(经验)
- channels_last:model.to(memory_format=torch.channels_last) 有时能再抠一点延迟。
- VAE tiling:大分辨率开启 enable_vae_tiling() 避免 VAE 爆显存。
- Attention Slicing:更省显存但会稍慢,薄荷刀法,看业务取舍。
- BF16/FP16:A100/H100 BF16 友好,Diffusers 多用 FP16;实际以吞吐/质量兼顾为准。
6.2 TensorRT(重度场景/SDXL 推荐)
- 用 torch2trt 或 Diffusers + TensorRT 工具链把 UNet 编成 engine;
- 在 3g.40gb MIG 分区上稳定跑 1024×1024、较高步数场景,p95 会更稳。
示例(伪代码骨架):
# 1) 导出 onnx(以 UNet 为例)
python export_unet_onnx.py --model your_sdxl --fp16 --out unet.onnx
# 2) TensorRT 编译
trtexec --onnx=unet.onnx --fp16 --workspace=16000 --saveEngine=unet_fp16.trt
经验:把 UNet 编成 TensorRT,VAE/CLIP 仍用 PyTorch FP16 即可,综合延迟下降明显,且稳定。
7. 进程模型与路由:把请求“送对分区”
我使用 FastAPI + Uvicorn 起微服务,每个 Pod/容器只绑定 一个 MIG 实例,天然做到了资源隔离。网关层(Nginx/Envoy)用 “按模型与分辨率路由到不同 Service” 的方式,把小请求打到 mig-1g.10gb,大请求打到 mig-3g.40gb。
关键配置清单
- 每个容器固定 CUDA_VISIBLE_DEVICES=MIG-xxx;
- 每个容器进程内 限制并发(信号量控制),避免单实例内过度排队;
- 网关基于 headers/URL(如 /sdxl vs /sd15)转发到不同后端;
- 灰度调整各 Deployment 的副本数,以维持目标 p95。
8. 实测数据(一线结果,仅供参考)
统一场景:512×512、SD-Turbo/SD1.5 轻任务,20 steps,FP16,batch=1;并发 50;Ubuntu 22.04;PyTorch 2.4 + xFormers;同一张 A100 80GB。
| 布局 | 路由策略 | p50 | p95 | 峰值 QPS(p95<2s) |
|---|---|---|---|---|
| 无 MIG(整卡) | 单实例承载 | 0.45s | >8.0s(排队重) | 5.8 |
| 7×1g.10gb | 7 实例轮询 | 1.8s | 2.3s | 12.6 |
| 2×3g.40gb + 1×1g.10gb | 大活→3g,小活→1g | 0.9s(3g)/1.9s(1g) | 1.6s / 2.4s | 14.1 |
| 3×2g.20gb + 1×1g.10gb | 中活→2g,小活→1g | 1.2s / 1.9s | 1.9s / 2.5s | 13.4 |
解释:整卡在单请求极快,但一旦并发上来,排队导致 p95 爆炸;MIG 将排队拆散到多个硬隔离实例,稳定吞吐与 尾延迟整体更优。
对 SDXL 1024×1024(30 steps)场景,整卡对比 2×3g.40gb:单请求延迟整卡略低,但在并发(>10)下,2×3g 的 p95 更稳且整体吞吐更高。
9. 监控与告警(必须要有)
DCGM Exporter:随 GPU Operator 一起装,Prometheus 抓 DCGM_FI_DEV_GPU_UTIL、MEM_COPY_UTIL、FB_USED 等指标;
每实例队列长度:应用内导出 /metrics,记录请求队列、耗时直方图;
告警:p95 超阈、某个 MIG 实例持续 100% 利用率、显存逼近上限、容器 OOM。
10. 常见坑与现场处理手记
启用 MIG 导致上下文中断
任何 -mig 1/改布局都会 reset GPU。先切流→drain→再操作。
看不到 MIG 设备
重启 docker:systemctl restart docker;
确认 nvidia-ctk runtime configure 已执行;
检查 nvidia-fabricmanager 是否在跑(A100 SXM 必需,PCIe 建议开启)。
Device Plugin 资源名对不上(K8s)
看 kubectl describe node 的实际导出名;不同版本前缀可能细微差异。
MPS 与 MIG 的兼容
不要跨 MIG 分区混用 MPS。如果在单个 MIG 实例里用 MPS,需要确保只服务轻量多进程场景,实践中我更倾向于 每容器=每 MIG,减少变量。
TensorRT 编译参数过小
--workspace 不够会性能差、甚至退化。A100 上 SDXL UNet 建议给到 16GB 级别工作区(单位 MiB)。
VAE 内存暴涨
大分辨率/高步数时务必开 enable_vae_tiling();否则即使 3g.40gb 也可能顶格。
容器混跑导致 NUMA 跨 Socket
单卡主机问题不大;多卡/多 CPU 时将 Pod 绑核到靠近 GPU 的 NUMA 节点(cpuManagerPolicy: static + topologyManagerPolicy: best-effort),避免额外抖动。
功耗/频率不稳
机房温控较紧时,可以锁定功耗或频率以求稳定:
sudo nvidia-smi -pm 1
# 示例:限制 TDP,视散热而定
sudo nvidia-smi -pl 250
先压测试,生产再开。
11. 最小可用(可直接上线)的目录结构与启动
serving/
├── Dockerfile # 基于 nvcr.io/nvidia/pytorch:24.08-py3
├── requirements.txt # diffusers, transformers, xformers, fastapi, uvicorn
├── app/
│ ├── pipeline.py
│ └── server.py # FastAPI,启动 /generate 接口
└── k8s/
├── deploy-sd15-1g.yaml
└── deploy-sdxl-3g.yaml
server.py(核心要点):
from fastapi import FastAPI
from .pipeline import infer
app = FastAPI()
@app.post("/generate")
async def generate(prompt: str, h: int=512, w: int=512, steps: int=20, seed: int=42):
img = infer(prompt, h, w, steps, seed)
# 返回 base64 或写 OSS,示例从略
return {"ok": True}
K8s(1g 服务与 3g 服务分别一个 Deployment,Ingress/Nginx 按路径转发到不同 Service)。滚动升级时,先调小旧版本副本数,观测 p95,再切流。
12. 不同业务的 MIG 布局建议(速查)
| 业务类型 | 建议布局 | 说明 |
|---|---|---|
| 多用户小图高并发(SD1.5/SD-Turbo 512) | 7×1g.10gb | 单请求延迟略高,但整体吞吐与 p95 最稳 |
| 混合场景(小图为主,偶有 SDXL) | 2×3g.40gb + 1×1g.10gb | 我在生产最常用 |
| 主要是 SDXL(1024×1024,ControlNet 偶尔) | 2×3g.40gb | 若峰值不高亦可 1×7g.80gb |
| 超大图或批处理 | 7g.80gb | 牺牲并发,换取单任务极致性能 |
凌晨 3 点 40,我们把布局切成 2×3g + 1×1g,对应的三个 Deployment 全部就绪。网关灰度 20% → 60% → 100%,p95 一路从 8s 掉到 2s 左右,客服群的“慢”也安静了。机柜门关上那刻我看了眼监控大屏,七彩线条像被梳理过一样顺滑。
MIG 不是银弹:它会带来少量的单请求延迟损失和管理复杂度,但在高并发图像生成这类混合负载里,我更关心队列不炸、尾延迟稳,让用户拿到 “稳定预期” 的体验。这个夜里它就像一把小手术刀,把粗糙的并发切开,按尺寸、按性格,把请求放到对的“器官”里。第二天早晨,运营在群里发了一个“稳”的表情包,我知道,这活儿值了。
13. 清单式总结(拿走就用)
- 系统:Ubuntu 22.04 + Driver 550 + CUDA 12.4 + nvidia-container-toolkit。
- 启用 MIG:nvidia-smi -i 0 -mig 1 → 用脚本创建 2×3g.40gb + 1×1g.10gb。
- 容器/K8s:Docker 绑定 MIG UUID;K8s 用 nvidia.com/mig-3g.40gb、nvidia.com/mig-1g.10gb 申请。
- 模型优化:Diffusers FP16 + xFormers +(可选)torch.compile;SDXL 推荐 UNet TensorRT。
- 路由:按模型/分辨率把请求送到匹配的分区;每实例控制并发。
- 监控:DCGM + 应用指标;盯 p95、显存、队列长度。
- 坑位:MIG 变更会 reset;Device Plugin 资源名校验;VAE/工作区大小要到位。
如果你也在香港的冷通道里和告警赛跑,试试先把卡“切开”。让每个请求在它该去的地方,踏实地跑起来。