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

如何在香港服务器的 Debian 集群里用「节点亲和性」把 GPU 虚拟化任务调顺

发布人:Minchunlin 发布时间:2025-08-30 09:38 阅读量:672


凌晨 3:10,我站在香港数据中心那个熟悉的机柜前,冷风从地板缝里往上钻。两台 A100 的风扇正对着我叫;而 Slack 里,模型推理队列又堆到 900+。同事说“GPU 明明空了,为啥 Pod 不上?”,我打开 kubectl get pods -A -o wide,一眼望去,全是 Pending,理由“node(s) didn't match node affinity/selector”。
那晚我把从 Debian 裸机到 Kubernetes 调度链路又撸了一遍,最后靠节点亲和性(Node Affinity)+ MIG/时间片共享把 GPU 虚拟化任务调顺了。第二天早上 6:40,落地窗外维港开始发白,队列清空,整整齐齐。

本文就是那晚(以及后续一周复盘)沉淀下来的完整实操教程。我会从硬件、系统、驱动、K8s、GPU Operator 到调度策略,一步步把坑和解法摆在你面前;既保证新手能复刻,也能让老手挑到细节。

现场环境与目标

机房与硬件(实配)

节点名 机架/可用区 CPU & 内存 GPU MIG 系统盘/数据盘 NIC 备注
hk-gpu-01 hk1a / R42 2×Xeon Gold 6348, 512GB 2×A100 40GB PCIe 支持 2×1.92TB NVMe(RAID1) 2×25GbE 机柜顶端,进风好
hk-gpu-02 hk1a / R43 2×Xeon Gold 6348, 512GB 2×A100 40GB PCIe 支持 2×1.92TB NVMe(RAID1) 2×25GbE 与 01 同交换机
hk-gpu-03 hk1b / R05 2×AMD EPYC 7643, 1TB 4×A10 24GB 不支持 MIG 2×3.84TB NVMe(RAID1) 2×25GbE 专做时间片共享

OS 统一:Debian 12 (bookworm),内核 6.1;容器运行时 containerd 1.7;K8s v1.29(双控平面);CNI 用 Cilium;集群里安装了 NVIDIA GPU Operator(负责驱动、GFD、DCGM、Device Plugin)。

我们的目标

  1. 在 Debian 上稳定启用 A100 的 MIG(如 1g.5gb / 2g.10gb / 3g.20gb / 4g.20gb / 7g.40gb),实现“硬切片”多租并发推理。
  2. 在 A10 节点上启用 时间片共享(time-slicing) 做“软切片”。
  3. 用 Node Affinity/Anti-Affinity + 自定义标签 把不同 SLA/任务类型调度到合适的节点/机架/可用区。
  4. 用 Taints/Tolerations 保证 GPU 节点不被非 GPU 任务污染。
  5. 用 度量对比 验证优化效果。

基础安装:Debian + containerd + NVIDIA 组件

如果你也用 GPU Operator(强烈推荐),以下两条路二选一:Operator 自动装驱动 或 节点预装驱动。我实践下来,生产更偏向 Operator 托管,版本一致性更好。

1)containerd 与基础内核参数

# 基础组件
apt-get update && apt-get install -y curl gnupg2 software-properties-common jq

# containerd
apt-get install -y containerd

# systemd cgroup(很关键,否则 nvidia-container-runtime/cgroups 会出幺蛾子)
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
systemctl enable --now containerd

# 内核参数(NUMA/大页按需):以下示例保持保守
echo 'vm.max_map_count=262144' > /etc/sysctl.d/99-sysctl.conf
sysctl --system

2)安装 Kubernetes 组件(略写关键步)

# 关闭 swap(kubelet 要求)
swapoff -a && sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# kubelet/kubeadm/kubectl(按你集群管理方式走)
apt-get install -y kubelet kubeadm kubectl
systemctl enable kubelet

3)安装 NVIDIA GPU Operator(Helm)

helm repo add nvidia https://nvidia.github.io/gpu-operator
helm repo update

# hk-gpu 命名空间
kubectl create ns gpu-operator

# 基础安装(驱动、GFD、DCGM、Device Plugin 都交给它)
helm install gpu-operator nvidia/gpu-operator -n gpu-operator \
  --set driver.enabled=true \
  --set toolkit.enabled=true \
  --set devicePlugin.enabled=true \
  --set migManager.enabled=true \
  --set node-feature-discovery.enabled=true

坑 1:驱动版本与内核头文件

Debian 12 下如果你自己装 DEB 驱动包,linux-headers 要与 running kernel 完全匹配,否则 DKMS 会失败。Operator 模式把这事打包好了,建议生产用它。

让 GPU“会说话”:MIG 与时间片共享

A100 开启 MIG(硬切片)

在 A100 节点(hk-gpu-01/02)上:

# 开启 MIG 模式(每块卡)
nvidia-smi -i 0 -mig 1
nvidia-smi -i 1 -mig 1
# 生效通常需要重置 GPU(会短暂中断,生产请维护窗操作)
nvidia-smi -i 0 -rgc
nvidia-smi -i 1 -rgc

# 例:在每块卡上创建 7×1g.5gb(小切片满铺)
nvidia-smi mig -i 0 -cgi 19,19,19,19,19,19,19 -C
nvidia-smi mig -i 1 -cgi 19,19,19,19,19,19,19 -C
# 19 是 A100 40GB 上 1g.5gb 的 profile ID(不同卡/容量 ID 不同,请以节点上 `nvidia-smi mig -lgip` 为准)

GPU Operator 的 MIG Manager + GFD(GPU Feature Discovery) 会把切片后的资源暴露成 K8s 资源与标签,例如:

资源名:nvidia.com/mig-1g.5gb、nvidia.com/mig-2g.10gb 等

标签:nvidia.com/mig.capable=true、nvidia.com/gpu.product=NVIDIA-A100-40GB-PCIe 等

坑 2:MIG 改动后 Pod 看不见新资源

经验:做完 MIG 分区后,等 GFD 重新发现(或手动滚动 GFD/Device Plugin DaemonSet)。极端情况下需要重启 kubelet。

A10 开启时间片共享(软切片)

在 A10 节点(hk-gpu-03)上,我们用 Device Plugin 的 time-slicing:

创建/更新 ConfigMap(Operator 会挂载给 Device Plugin):

apiVersion: v1
kind: ConfigMap
metadata:
  name: nvidia-device-plugin-config
  namespace: gpu-operator
data:
  config.yaml: |
    version: v1
    flags:
      migStrategy: none
    sharing:
      timeSlicing:
        resources:
          - name: nvidia.com/gpu
            replicas: 4  # 每块卡切 4 份时间片

让 Device Plugin 使用它(Helm values):

helm upgrade gpu-operator nvidia/gpu-operator -n gpu-operator \
  --set devicePlugin.config.name=nvidia-device-plugin-config \
  --set devicePlugin.config.default=config.yaml

时间片心法:Pod 仍然 requests: nvidia.com/gpu: 1,只是 Device Plugin 让“同一物理卡并发跑多个 Pod”。吞吐拉高,但单个推理延迟会升高——适合批量/吞吐型。

给节点贴上“会说话的标签”:区域/机架/型号/SLA

我采用了一套自定义标签,再配合 Operator/GFD 自动的硬件标签:

# 可用区与机架
kubectl label node hk-gpu-01 topology.kubernetes.io/zone=hk1a rack=r42
kubectl label node hk-gpu-02 topology.kubernetes.io/zone=hk1a rack=r43
kubectl label node hk-gpu-03 topology.kubernetes.io/zone=hk1b rack=r05

# 角色与 GPU 能力
kubectl label node hk-gpu-01 gpu.role=online gpu.mig=enabled
kubectl label node hk-gpu-02 gpu.role=online gpu.mig=enabled
kubectl label node hk-gpu-03 gpu.role=batch  gpu.timeslicing=enabled

# Taint 防止被通用工作负载占用
kubectl taint nodes hk-gpu-01 hk-gpu-02 hk-gpu-03 gpu-only=true:NoSchedule

坑 3:标签漂移

手工标签容易丢。我后面用 DaemonSet + init 脚本在节点启动时校正标签,或者把标签写进 kubelet --node-labels(自动化/不可变)。

调度策略:Node Affinity / Anti-Affinity / Taints / 拓扑

1)在线推理(低延迟)→ A100 + MIG-1g.5gb,固定 hk1a

apiVersion: apps/v1
kind: Deployment
metadata:
  name: infer-online-a100-1g
  namespace: ai
spec:
  replicas: 12
  selector:
    matchLabels: { app: infer-online }
  template:
    metadata:
      labels:
        app: infer-online
        tier: online
    spec:
      tolerations:
        - key: "gpu-only"
          operator: "Equal"
          value: "true"
          effect: "NoSchedule"
      nodeSelector:   # 最硬的筛选先走 here(简单场景够用)
        nvidia.com/mig.capable: "true"
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values: ["hk1a"]
                  - key: gpu.mig
                    operator: In
                    values: ["enabled"]
                  - key: nvidia.com/gpu.product
                    operator: In
                    values: ["NVIDIA-A100-40GB-PCIe"]
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 80
              preference:
                matchExpressions:
                  - key: rack
                    operator: In
                    values: ["r42","r43"]  # 同交换机,跨机架少
            - weight: 20
              preference:
                matchExpressions:
                  - key: gpu.role
                    operator: In
                    values: ["online"]
      containers:
        - name: infer
          image: registry.local/serving:cuda12.2
          resources:
            limits:
              nvidia.com/mig-1g.5gb: 1
              cpu: "2"
              memory: "6Gi"
          ports:
            - containerPort: 8080

要点

  • limits: nvidia.com/mig-1g.5gb: 1:请求一个 1g.5gb 实体 MIG 切片。
  • requiredDuringScheduling… 把“必须条件”写实:区域、MIG 可用、GPU 产品型号。
  • preferred… 把“更优”表达出来:同交换机机架优先,online 角色优先。

2)离线批处理(吞吐优先)→ A10 时间片,尽量不与 online 混

apiVersion: batch/v1
kind: Job
metadata:
  name: batch-emb-a10
  namespace: ai
spec:
  backoffLimit: 1
  parallelism: 16
  template:
    metadata:
      labels: { app: emb-build, tier: batch }
    spec:
      tolerations:
        - key: "gpu-only"
          operator: "Equal"
          value: "true"
          effect: "NoSchedule"
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: gpu.timeslicing
                    operator: In
                    values: ["enabled"]
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values: ["hk1b"]
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 50
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: tier
                      operator: In
                      values: ["online"]
                topologyKey: "kubernetes.io/hostname"
      containers:
        - name: emb
          image: registry.local/offline:cuda12.2
          resources:
            limits:
              nvidia.com/gpu: 1   # time-slicing 下仍是 1,设备插件会“并发”
              cpu: "4"
              memory: "12Gi"

要点

  • gpu.timeslicing=enabled 把任务锁进支持时间片的节点;
  • Anti-Affinity 尽量避免与 tier=online 同宿主机;
  • parallelism 结合 time-slicing 的 replicas 调大吞吐。

3)统一的 RuntimeClass(避免容器运行时踩坑)

创建 NVIDIA runtimeclass:

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: nvidia
handler: nvidia

在 Pod 里引用:

spec:
  runtimeClassName: nvidia

坑 4:could not select device driver "nvidia"

99% 是 nvidia-container-toolkit 没注入或 containerd 没开 SystemdCgroup=true。Operator 模式下一般由 toolkit 组件搞定,自己装时注意 /etc/containerd/config.toml 的 runtimes.nvidia 配置是否存在。

验证与回归:资源发现、拓扑与监控

1)节点与标签快检

kubectl get nodes -L topology.kubernetes.io/zone,rack,gpu.mig,gpu.timeslicing,nvidia.com/gpu.product

2)资源是否被 K8s 识别

# MIG 资源
kubectl describe node hk-gpu-01 | grep -A2 "Capacity" | grep mig
# 时间片配置(看 Device Plugin 日志)
kubectl -n gpu-operator logs ds/nvidia-device-plugin-daemonset | grep time-slicing

3)DCGM + Prometheus 指标(核心几个)

  • DCGM_FI_DEV_GPU_UTIL(GPU 利用率)
  • DCGM_FI_DEV_FB_USED(显存使用)

自定义:队列长度、P95 推理延迟、作业等待时长(从队列/网关采)

优化前后对照(真实数据一夜复盘)

指标 优化前(散落调度) 优化后(亲和性 + MIG/时间片)
在线推理 P95 延迟 120ms 78ms
在线 Pod Pending 平均时长 4m12s 28s
A100 平均 GPU 利用率 41% 71%
A10 节点吞吐(req/s) 1.0× 2.7×
同机架跨交换机流量 降 36%
事故:误上非 GPU 节点 偶发 0(taint 生效)

真实踩坑与解决清单

MIG Profile ID 对不上

先跑 nvidia-smi mig -lgip 看本卡支持的具体 ID 和 profile 名,再下命令。不同驱动/卡/容量 ID 可能不同。

GFD/GPU Device Plugin 没感知新切片

滚动这两个 DaemonSet 最快;极端再滚 kubelet。

Debian 12 + cgroup v2

containerd SystemdCgroup=true 必开;RuntimeClass 用 nvidia;否则偶发 cgroup permission。

镜像 CUDA/驱动不匹配

统一用一个 CUDA 基础镜像族(如 cuda:12.2),Operator 装的驱动与它匹配;不混搭。

标签失真/丢失

用 DaemonSet 初始化脚本或 --node-labels 固化;不要全靠手贴。

队列优先级

给在线/离线两个 PriorityClass,搭配 preemptionPolicy,确保 SLA。

跨可用区回源慢

preferredDuringScheduling 倾向就近(同 zone、同 rack),把远端作为次选,必要时加 topologySpreadConstraints。

额外的“老鸟手感”建议

  • 把亲和性策略做成“模板”:在线、离线、训练三类各一套,CI/CD 里按应用类型自动注入。
  • 不要一次把 MIG 切满:先从 1g.5gb/2g.10gb 做 AB 测出最优切片,再铺满。迁移切片要维护窗。
  • 把 GPU 节点当“小集群”管理:独立 namespace、独立网段(必要时)、单独 taint。
  • 配合 Descheduler:避免“初调好后来又漂”,比如启用 RemoveDuplicates、LowNodeUtilization 等策略(按需)。

结尾:凌晨的风停了

那天早上,我在机房门口把工单状态改成“已恢复”。手机屏幕上,在线延迟曲线像是被人按住了脑袋,稳稳地贴在 80ms 附近;A10 的吞吐飙上去,队列也逐步清空。
其实我们做的并不神秘:把资源切清楚(MIG/时间片)、把节点标明白(标签/taint),再把调度说准确(亲和性/反亲和/拓扑)。
当这些原则在 Debian + Kubernetes 的链路里逐个落位,GPU 就不再是“凭缘分”的黑盒,而是可预期、可复用、可度量的生产力。

如果你也在香港的机房里吹过半夜的冷风,希望这份笔记能让你少走几步回头路。祝你今天的队列,也能在天亮之前清空。

目录结构
全文