AI图像生成平台如何在香港服务器的 Ubuntu 中配置 GPU MIG 分区,提升并发推理效率
技术教程 2025-09-16 11:28 212


那天是周三,香港葵涌机房冷通道显示 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/工作区大小要到位。

如果你也在香港的冷通道里和告警赛跑,试试先把卡“切开”。让每个请求在它该去的地方,踏实地跑起来。