NUMA 感知的 GPU 调度:三个管理器与调度器失明
草稿
两个完全相同的 Pod,一个达标、一个慢 35%,没有报错也没有 OOM,唯一的区别是 GPU 和 CPU 落在了哪个 NUMA 节点上。
无声的性能杀手
下面这个场景每天都在配置不当的 GPU 集群里上演:两个完全相同的 Pod,同样的模型、同样的 GPU 型号、同样的资源请求。一个达到预期吞吐,另一个慢 35%。这就是 NUMA 问题:除非专门配置拓扑管理器栈,它对标准 Kubernetes 完全不可见。物理层面的背景见作为拓扑的服务器。
三个管理器:CPU、内存、设备
Kubernetes 通过 kubelet 上三个相互独立、由拓扑管理器(Topology Manager)协调的管理器来控制 NUMA 对齐:
- CPU 管理器(
cpuManagerPolicy: static): 默认none策略下,任何容器都可跑在任何核心上。启用static后,Guaranteed QoS 等级(CPU 的 request == limit 且为整数值)的 Pod 被独占式地钉在特定核心上,这些核心从共享池移除。状态记录在/var/lib/kubelet/cpu_manager_state。 - 内存管理器(
memoryManagerPolicy: Static): 强制 NUMA 本地内存分配,与拓扑管理器通信报告哪些 NUMA 节点有足够预留内存。关键点:必须同时配置reservedMemory覆盖操作系统在各 NUMA 节点上的内存,否则内存管理器会拒绝本应放得下的工作负载。 - 设备管理器(带拓扑提示的设备插件): NVIDIA 设备插件通过 ListAndWatch 的 TopologyInfo 字段报告每块 GPU 的 NUMA 亲和性,拓扑管理器收集这些提示。
拓扑管理器:协调者与策略
拓扑管理器在 Pod 准入时(先于容器创建)运行,并基于 NUMA 对齐能否达成来决定接纳还是拒绝 Pod。它不会传送内存,也不会修复糟糕的硬件设计;它协调的是必须全部给出有用提示的各个组件。
# 追求最严格 NUMA 对齐的 kubelet 配置
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cpuManagerPolicy: static
topologyManagerPolicy: single-numa-node # 最严格:全有或全无
topologyManagerScope: pod # 作用于整个 Pod(而非逐容器)
memoryManagerPolicy: Static
reservedMemory: # 内存管理器必配
- numaNode: 0
limits:
memory: 4Gi # NUMA 0 上为 OS/系统预留
- numaNode: 1
limits:
memory: 4Gi| 策略 | 行为 | 适用场景 |
|---|---|---|
none | 默认。不强制 NUMA 对齐 | 开发集群、非延迟敏感工作负载 |
best-effort | 偏好对齐,无法对齐也继续 | 有 NUMA 收益即好的混合负载 |
restricted | 对齐失败则拒绝 Pod,报 TopologyAffinityError | 性能敏感的生产推理 |
single-numa-node | 最严格:所有资源必须来自同一 NUMA 节点 | 延迟关键型推理、多路 HPC |
调度器失明问题与修复
这是每个团队都会踩的根本缺口:拓扑管理器运行在 kubelet 上,只在节点层面强制对齐,而此时调度器已经选好了节点。如果调度器挑了一个无法满足 NUMA 对齐的节点,Pod 被调度过去后立刻被拓扑管理器以 TopologyAffinityError 拒绝。Pod 卡在 Pending 永不启动,集群陷入“调度器反复尝试同样几个节点、节点反复拒绝”的循环。
真实的生产故障模式
集群有 4 个 GPU 节点,每个 8 块 GPU 按 4+4 分布在 2 个 NUMA 节点上。你请求一个 4 GPU、single-numa-node 策略的 Pod。调度器看到 8 块可分配 GPU,随意挑了一个节点;但准入时,拓扑管理器发现匹配的 NUMA 节点上只剩 2 块空闲 GPU。Pod 被拒,如此循环,直到某个 NUMA 节点被完全腾空,你的训练作业永远开不了工。
修复需要三个部件协同:
- NFD 拓扑更新器(Node Feature Discovery Topology Updater): DaemonSet,从 kubelet 的 PodResources API 读取按 NUMA 划分的资源可用性,发布为 NodeResourceTopology(NRT)自定义资源,约每 60 秒更新。
- NRT 调度器插件: 二级调度器或调度器插件,读取 NRT 对象作为过滤和打分依据,只在准入前把 Pod 放到 NUMA 对齐可满足的节点上。
- KAI Scheduler(或类似物): NVIDIA 的开源调度器,读取 NRT、实现成组调度,在集群层面做出 NUMA 感知放置(深入介绍见NVIDIA × CNCF)。
# 检查 NodeResourceTopology 对象
kubectl get noderesourcetopologies.topology.node.k8s.io
kubectl get nrt gpu-node-01 -o yaml | head -60
# Zones:
# - name: node-0 # NUMA 节点 0
# resources:
# - name: nvidia.com/gpu
# capacity: '4'
# available: '2' # 2 空闲,2 已分配总结
NUMA 治理分两半:kubelet 内的三个管理器保证“放进来就对齐”,NRT 与拓扑感知调度器保证“别放到对不齐的节点”。只配前者不配后者,就会收获 Pending 循环;两者都配齐,拓扑才从性能陷阱变成可声明的约束。排障入口见故障模式与排障手册的 TopologyAffinityError 一节。
参考资料
- Kubernetes 文档:Control Topology Management Policies on a node。