上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-13 10:40 阅读量:1167


夜里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 的那些参数,但你能看到低延迟和高并发带来的体验。

这就是我喜欢运维工作的地方:把复杂关在机柜里,把简单留给用户。

目录结构
全文