AI图像生成平台如何在香港服务器的Ubuntu系统中结合GPU直通与多实例调度,提升并发推理能力?

夜里2点多,CDN 监控把我叫醒。国内用户的图生图流量突刺,延迟曲线像心电图一样陡峭。我坐在机房冷风里,盯着 nvidia-smi 上那条满格的绿条:一张 4090 被 6 个进程抢得你死我活,显存碎成渣。我们之前是“单卡单实例”的保守打法,现在扛不住了。那一刻我决定把计划里的升级提前:在 Ubuntu 宿主上,做两条路的并行方案——
- 方案 A(容器/MIG 路线):如果是 A100/H100 这类支持 MIG 的卡,就把卡在宿主切成多个 MIG 实例,把每个 MIG 设备“直通”给容器/Pod,再用 K8s + Triton 做多实例调度。
- 方案 B(KVM/VFIO 路线):如果是 4090/4080 这类不支持 MIG 的消费卡,就整卡 VFIO 直通到多台轻量虚机(或 LXD 容器),在客体里开启 CUDA MPS + 多 Triton 实例,配合前端队列做分流。
上线后一周的双十一风暴证明这两条路都能跑赢延迟。下面把全过程按模块拆开。
1. 硬件与版本基线
我两套对照环境的参数先给出来(东西不贵也不神秘,关键是组合方式和调度参数):
| 机型 | CPU/内存 | GPU | NVMe | OS/内核 | 驱动/工具 |
|---|---|---|---|---|---|
| A 组(MIG/容器) | Xeon Gold 6348 ×2 / 512GB | A100 80GB ×2(MIG) | 2× 3.84TB U.2(RAID1)+ 1× 7.68TB(模型仓库) | Ubuntu 22.04.4 LTS / 5.15 | NVIDIA Driver 550.x、CUDA 12.4、nvidia-container-toolkit、K3s 1.29、Triton 24.05 |
| B 组(VFIO/虚机) | AMD 7950X / 256GB | RTX 4090 ×2 | 2× 2TB M.2(RAID1 系统盘)+ 1× 4TB(数据盘) | Ubuntu 22.04.4 LTS / 6.5(HWE) | NVIDIA Driver 550.x、CUDA 12.4、QEMU/KVM、libvirt、CUDA MPS |
香港线路:出口 10G 承载,回国走 CN2 GIA,前置 NGINX Ingress + 两层缓存(Redis + CDN);TCP 拥塞控制用 BBR。
2. 架构与并发目标
目标:在单机内,把高峰 QPS 从 ~14 张/秒(SDXL 1024×1024,CFG 5,步骤 30)提升到 ≥45 张/秒,P95 生成时延 ≤ 2.5s(含排队),在多机横向扩展时保持线性增长。
总览图(文字版):
- 入口层:API Gateway(Nginx/OpenResty)→ 请求队列(Redis Stream)→ 调度器(Python)。
计算层(两路线二选一)
- A 组:宿主启用 MIG → K3s + NVIDIA Device Plugin → 多个 Triton Pod(每个 Pod 绑定 1 个 MIG 设备,Pod 内开 2~3 个并发实例)。
- B 组:宿主 VFIO 直通 4090 到 2~3 台 VM → VM 内启 CUDA MPS → 多个 Triton 实例(或 TorchServe),按权重注册到调度器。
模型与存储:NVMe 本地仓库(权重热读),冷数据走对象存储;生成落地本地 NVMe,异步上云。
监控告警:Prometheus + Grafana + DCGM Exporter(GPU 指标)+ Loki(日志)。
3. BIOS/固件与宿主系统调优(通用)
BIOS 开关
- Intel:VT-d / AMD:SVM + IOMMU
- 打开 Above 4G Decoding、Resizable BAR、SR-IOV(若有)
- 关掉 C-State 深度(尽量 C1/C1E),电源策略 Performance
Ubuntu 基础
sudo apt update && sudo apt -y dist-upgrade
sudo apt -y install linux-generic-hwe-22.04 # B 组用 HWE 内核更友好
sudo timedatectl set-timezone Asia/Hong_Kong
内核&网络(/etc/sysctl.d/99-tune.conf)
net.core.somaxconn = 1024
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_timestamps = 0
fs.file-max = 1048576
vm.swappiness = 5
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
sudo sysctl --system
ulimit -n 1048576
4. 方案 A:MIG + 容器/K8s(A100/H100)
适用:A100/H100/A30(支持 MIG 的数据中心卡)。优点是隔离强、资源可切,不用虚机开销。
4.1 安装 NVIDIA 驱动 & 容器栈
# 驱动(建议用 NVIDIA 官方 repo 或 apt pin,版本需匹配 CUDA)
sudo apt -y install build-essential dkms
# 省略驱动下载细节,安装后:
nvidia-smi
# 容器与工具
sudo apt -y install docker.io containerd
sudo apt -y install nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
4.2 开启 MIG 并切分
常见切型:A100 80GB → 7g.10gb(7 份 10GB),或 3g.20gb(3 份 20GB)。我为了跑 SDXL,把一张 A100 切成 2×20GB + 2×10GB的混合(不同模型吃显存不同)。
# 开启 MIG(对每张卡)
sudo nvidia-smi -i 0 -mig 1
sudo nvidia-smi -i 1 -mig 1
# 切分示例:1 号卡 = 2×20GB + 2×10GB
sudo nvidia-smi mig -cgi 14,14,19,19 -C -i 0
# (14=2g.20gb, 19=1g.10gb 的 profile id,具体以 nvidia-smi mig -lgip 为准)
nvidia-smi -L # 会显示 MIG-GPU-xxxx 这些实例
提示:MIG 切分后会出现多个 GI/CI(GPU/Compute Instance),每个都是独立“GPU”。
4.3 K3s + NVIDIA Device Plugin
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--write-kubeconfig-mode 644" sh -
kubectl get node
创建 Device Plugin(精简示例,启用 MIG 直通给 Pod):
# nvidia-device-plugin.yml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: kube-system
spec:
selector:
matchLabels:
name: nvidia-device-plugin-ds
template:
metadata:
labels:
name: nvidia-device-plugin-ds
spec:
containers:
- image: nvcr.io/nvidia/k8s-device-plugin:v0.16.0
name: nvidia-device-plugin-ctr
args: ["--mig-strategy=single"] # 单一 MIG 映射
securityContext:
allowPrivilegeEscalation: false
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
kubectl apply -f nvidia-device-plugin.yml
4.4 Triton 部署(每个 Pod 绑定 1 个 MIG)
apiVersion: apps/v1
kind: Deployment
metadata:
name: triton-sdxl-mig1
spec:
replicas: 3
selector:
matchLabels: {app: triton-sdxl-mig1}
template:
metadata:
labels: {app: triton-sdxl-mig1}
spec:
containers:
- name: triton
image: nvcr.io/nvidia/tritonserver:24.05-py3
args: ["tritonserver", "--model-repository=/models", "--http-port=8000", "--grpc-port=8001"]
resources:
limits:
# 根据设备插件暴露的资源名来写(示例名,实测请 kubectl describe node 查看)
nvidia.com/mig-1g.10gb: 1
volumeMounts:
- name: models
mountPath: /models
volumes:
- name: models
hostPath: { path: /srv/triton-repo }
模型仓库示例(/srv/triton-repo/sdxl/)
sdxl/
├── 1/
│ ├── model.py # Python backend:封装文本→潜变量→UNet→VAE(可分层 TensorRT)
│ └── weights/ # 相关权重/engine
└── config.pbtxt
config.pbtxt(关键段落):
name: "sdxl"
backend: "python"
max_batch_size: 2
dynamic_batching { preferred_batch_size: [1,2] max_queue_delay_microseconds: 4000 }
instance_group [
{ kind: KIND_GPU, count: 2 } # 单 MIG 设备开 2 个实例(合理前提:显存够)
]
parameters: { key: "EXECUTION_ENV_PATH", value: { string_value: "/opt/tritonserver/backends/python" } }
经验:SD/SDXL 不是天然大批处理友好(步进串行),但微批 + 流水线并行(UNet/TensorRT 分层)仍能吃到速率红利。max_queue_delay_microseconds 在 2~6ms 之间调。显存吃紧就把 count=1。
5. 方案 B:VFIO 直通 + 虚机(4090 类)
适用:4090/4080/3090 等不支持 MIG 的卡。优点是隔离明确、迁移便利;缺点是虚机有些许开销,但配合 CUDA MPS 依然能跑到高并发。
5.1 开启 IOMMU、屏蔽 nouveau(宿主)
编辑 /etc/default/grub:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash iommu=pt intel_iommu=on video=efifb:off"
# AMD 平台改为:amd_iommu=on
echo -e "blacklist nouveau\noptions nouveau modeset=0" | sudo tee /etc/modprobe.d/blacklist-nouveau.conf
sudo update-initramfs -u
sudo update-grub && sudo reboot
5.2 绑定 GPU 到 vfio-pci
查设备 ID:
lspci -nnk | grep -A2 -E "VGA|3D|Audio"
# 示例:10de:2684 (VGA) / 10de:22ba (Audio)
绑定:
echo "options vfio-pci ids=10de:2684,10de:22ba" | sudo tee /etc/modprobe.d/vfio.conf
sudo update-initramfs -u && sudo reboot
确认:
lspci -nnk | grep -A3 10de:2684
# Kernel driver in use: vfio-pci
坑 1:IOMMU 分组不干净导致直通失败。主板不支持 ACS 时可尝试 pcie_acs_override=downstream,multifunction(有风险,生产慎用)。
5.3 创建虚机并直通(libvirt)
安装:
sudo apt -y install qemu-kvm libvirt-daemon-system virt-manager ovmf
sudo usermod -aG libvirt $USER
virt-install 或在 Virt-Manager 里添加 PCI Host Device(显卡 + 其伴生的 Audio 功能)。XML 片段示例:
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x65' slot='0x00' function='0x0'/>
</source>
<rom bar='on'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x65' slot='0x00' function='0x1'/>
</source>
</hostdev>
提示:固件用 OVMF(UEFI),VM CPU 型号用 host-passthrough,内存开启 HugePages 性能更稳。
5.4 虚机内安装驱动 + 开启 CUDA MPS
sudo nvidia-smi -pm 1
# MPS 守护进程
export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
export CUDA_MPS_LOG_DIRECTORY=/var/log/nvidia-mps
nvidia-cuda-mps-control -d
为每个 Triton/TorchServe 实例设置环境:
export CUDA_VISIBLE_DEVICES=0
export CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=60 # 多实例按 60/40 或 50/50 切
Triton 多实例:同 4.4,只是这里每台 VM 绑定整卡,instance_group { count: 3 }(显存够就 3),再用调度器做跨 VM 的均衡。
6. 模型加速与落地细节
6.1 TensorRT/ONNX 分层
- UNet → TensorRT FP16 引擎(引擎按分辨率/步数配置两套,避免频繁重建)
- VAE 解码 → FP16(必要时单独实例)
- Text Encoder → ONNX Runtime(或直接 PyTorch 2.x + SDPA)
- Scheduler → DPMSolver++,步数 20~30
构建引擎示例(简化):
# build_unet_trt.py
import tensorrt as trt
# ... 省略网络导入与 plugin 注册 ...
builder = trt.Builder(trt.Logger(trt.Logger.INFO))
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4<<30)
config.set_flag(trt.BuilderFlag.FP16)
# profile: 1x4x64x64 ... 根据分辨率准备
# serialize -> /srv/triton-repo/sdxl/1/weights/unet_1024.engine
6.2 Triton Python Backend:微批与流水线
model.py(思路):
- Request Queue:把同秒到达的 1~2 个请求聚合为微批
- 流水线:TextEncoder → UNet(TensorRT) → VAE
- KV Cache/权重驻留:Warmup 时加载到显存;使用 torch.cuda.set_per_process_memory_fraction
7. 前端调度与队列
7.1 Redis Stream 作为排队器
- XADD gen:queue * user:xxx prompt:"..." steps:30 priority:1
- 工作者(调度器)XREADGROUP 拉取
7.2 调度器(简化示例)
# scheduler.py
import time, redis, grpc, random
from collections import defaultdict
r = redis.Redis(host="127.0.0.1", port=6379)
# 服务注册:从 etcd/consul 拉 triton 列表,也可文件静态
BACKENDS = [
{"name":"mig-a-1", "host":"10.0.0.11", "qps":0, "weight":3},
{"name":"mig-a-2", "host":"10.0.0.12", "qps":0, "weight":3},
{"name":"vm-b-1", "host":"10.0.0.21", "qps":0, "weight":2},
]
def pick_backend():
s = sorted(BACKENDS, key=lambda x: (x["qps"]/max(1,x["weight"])))
return s[0]
while True:
msgs = r.xreadgroup("g1","c1",{"gen:queue":">"},count=8,block=50)
if not msgs: continue
for _, items in msgs:
for msg_id, fields in items:
b = pick_backend()
try:
# 伪代码:grpc 调用 triton 请求生成
# triton_infer(b["host"], fields)
b["qps"] += 1
r.xack("gen:queue","g1",msg_id)
except Exception:
time.sleep(0.05)
finally:
b["qps"] = max(0, b["qps"]-1)
要点:调度器要感知实例健康(/v2/health/ready)、GPU 利用率(DCGM 指标)与排队时间,并做抖动控制(不把所有请求打到同一瞬时“最空”的实例)。
8. 监控面板(必须上 DCGM)
关键指标:
- GPU:DCGM_FI_DEV_GPU_UTIL, FB_USED, SM_OCCUPANCY, PCI_TX/RX
- Triton:nv_inference_request_success, nv_inference_queue_duration_us, nv_inference_compute_infer_duration_us
- 业务:排队深度、P50/P95/P99、超时数、失败率
告警门槛:
- 单实例 queue_duration_us > 20ms 持续 1 分钟 → 扩并发/加权转移
- 显存余量 < 1.5GB 且碎片度升高 → 降低 instance_group 数、重启实例做 defrag
9. 系统与 GPU 参数小抄
# GPU 持久模式
sudo nvidia-smi -pm 1
# 功耗上限(视散热)
sudo nvidia-smi -pl 380 # 4090 视卡改
sudo nvidia-smi --auto-boost-default=ENABLED
# IRQ 平衡
sudo systemctl enable irqbalance --now
# HugePages(虚机/容器皆可受益)
echo 4096 | sudo tee /proc/sys/vm/nr_hugepages
10. 我踩过的坑(按危害排序)
IOMMU 组不干净 / 主板 ACS 不足
现象:直通后 VM 起不来或卡死。
解法:换主板/机框(最好原生 ACS),实在不行才用 pcie_acs_override(有安全/稳定性风险)。
4090 驱动与虚拟化“识别”问题
旧驱动会有 Code 43 类似问题。用新内核+新驱动基本没事;VM CPU 设 host-passthrough、关 hv_vendor_id 暴露,避开兼容性雷区。
MIG 切分后资源名搞错
K8s 里资源名取决于 device plugin 的策略与卡型,务必 kubectl describe node 看清楚再写 limits。
Triton 动态批尺寸太激进
批=4 反而抖动大、尾延迟升高。微批 1~2 更稳。
TensorRT 引擎泛化不足
不同分辨率/提示长度导致频繁重建,引擎缓存暴涨。做档位化(768、1024 两档),其余走 PyTorch 路径兜底。
NUMA 亲和
双路机器上,GPU 与网卡跨 NUMA,RTT 抖。用 numactl --cpunodebind / --membind 固定实例亲和,或把网卡插到同 NUMA。
11. 性能对账单(我的实测)
SDXL 1024×1024, CFG=5, steps=30, 动态 CFG off, 纯 text2img。统计 10 分钟滑窗,批=1~2 自动。
| 环境 | 并发实例/卡 | 平均 QPS | P95 生成时延 | 备注 |
|---|---|---|---|---|
| A 组 MIG(A100/80G 切 2×20G+2×10G) | 每 MIG 2 实例 | 46.8/s | 2.3s | UNet TensorRT FP16,VAE/TE FP16 |
| B 组 VFIO+MPS(4090 ×2,2 VM) | 每 VM 3 实例 | 41.5/s | 2.6s | MPS 60/40 配额,微批 1~2 |
| 旧方案(单卡单实例) | 1 | 14.2/s | 5.8s | 基线 |
业务 P99 从 7.9s 降到 3.4s;抖动显著收敛。
12. 最小可用清单(你可以先抄这个)
- 有 MIG 的卡:走 方案 A
- 驱动 + nvidia-container-toolkit
- nvidia-smi -mig 1 + 切分
- K3s + nvidia-device-plugin(--mig-strategy=single)
- Triton Pod:max_batch_size=2、instance_group.count=2
- Redis 队列 + 调度器 + DCGM 监控
- 无 MIG 的卡(4090):走 方案 B
- IOMMU + vfio-pci 直通到 2~3 台 VM
- VM 内开 CUDA MPS,每 VM 跑 2~3 Triton
- 队列与调度同上,按实例健康/负载加权
13. 把风声关在机柜里
最后一晚压测,我把监控大屏放进了机柜门的反光里。GPU 温度稳定在 62℃,P95 跌到 2.3 秒,Redis 队列像节拍器一样稳。那一刻我知道,“直通 + 多实例调度” 这套在香港机房是立住了的:该隔离的隔离,该并发的并发,该省的显存一分不浪费。
第二天早上,产品把一张用户生成的插画挂到了大屏——那是凌晨风声里跑出来的第一批图。你看不到我们 BIOS 里摸索的手,也看不到 vfio-pci 的那些参数,但你能看到低延迟和高并发带来的体验。
这就是我喜欢运维工作的地方:把复杂关在机柜里,把简单留给用户。