网络拓扑感知调度

简介

在大规模 AI 训练场景中,尤其是大语言模型(LLMs)训练,高效的 Pod 间通信对训练性能至关重要。模型并行技术,如张量并行(TP)、流水线并行(PP)和数据并行(DP),需要在 GPU 之间进行频繁的高带宽数据交换——通常跨越多个节点。在这些工作负载下,网络拓扑成为关键的性能瓶颈,通信延迟和带宽受物理网络层次结构(如 NVLink、block、spine)的严重影响。

为了优化训练效率,Koordinator 提供了网络拓扑感知调度能力,该能力确保:

  • 当集群资源充足时,具有网络拓扑调度需求的 Pod 将根据用户指定的策略被调度到性能更好的拓扑域(例如,更低的延迟、更高的带宽)。
  • 当集群资源不足时,调度器将基于网络拓扑约束通过作业级抢占为 GangGroup 抢占资源,并在 .status.nominatedNode 字段中记录资源预留,以确保一致的放置。

此功能与 PodGroup/GangGroup 语义无缝协作。

前置条件

  • Kubernetes >= 1.18
  • Koordinator >= 1.7.0

验证网络拓扑支持是否启用

虽然从 Koordinator >= 1.7.0 开始,网络拓扑感知调度默认启用,但建议在继续之前确认配置。

检查 koord-scheduler 启动参数

  1. 验证 koord-scheduler 已启用网络拓扑管理器:
1. kubectl -n koordinator-system get deployment koord-scheduler -o yaml | grep enable-network-topology-manager

预期输出:

1. - --enable-network-topology-manager=true

如果参数缺失或设置为 false,请更新 koord-scheduler 部署以添加此标志。

检查 Coscheduling 插件配置

  1. 验证 Coscheduling 插件已启用网络拓扑感知:
1. kubectl -n koordinator-system get cm koord-scheduler-config -o yaml

确保 Coscheduling 插件配置包含 awareNetworkTopology: true:

1. apiVersion: v1
2. kind: ConfigMap
3. metadata:
4. name: koord-scheduler-config
5. namespace: koordinator-system
6. data:
7. koord-scheduler-config: |
8. apiVersion: kubescheduler.config.k8s.io/v1
9. kind: KubeSchedulerConfiguration
10. profiles:
11. - pluginConfig:
12. - name: Coscheduling
13. args:
14. apiVersion: kubescheduler.config.k8s.io/v1
15. kind: CoschedulingArgs
16. awareNetworkTopology: true
17. enablePreemption: true

如果 awareNetworkTopology 缺失或设置为 false,请更新 ConfigMap 并重启 koord-scheduler Pod:

1. kubectl delete po -l koord-app=koord-scheduler -n koordinator-system

配置网络拓扑

为节点添加拓扑信息标签

首先,使用 NVIDIA 的 topograph 等工具为节点标记其网络拓扑位置:

1. apiVersion: v1
2. kind: Node
3. metadata:
4. name: node-0
5. labels:
6. network.topology.nvidia.com/block: b1
7. network.topology.nvidia.com/spine: s1

创建 ClusterNetworkTopology

管理员通过名为 default 的 ClusterNetworkTopology CR 定义拓扑层次结构:

1. apiVersion: scheduling.koordinator.sh/v1alpha1
2. kind: ClusterNetworkTopology
3. metadata:
4. name: default
5. spec:
6. networkTopologySpec:
7. - labelKey:
8. - network.topology.nvidia.com/spine
9. topologyLayer: SpineLayer
10. - labelKey:
11. - network.topology.nvidia.com/block
12. parentTopologyLayer: SpineLayer
13. topologyLayer: BlockLayer
14. - parentTopologyLayer: BlockLayer
15. topologyLayer: NodeTopologyLayer

拓扑形成树状结构,其中每一层代表网络中的聚合级别(例如,Node → Block → Spine)。

验证拓扑配置

应用 ClusterNetworkTopology 后,检查其状态:

1. kubectl get clusternetworktopology default -o yaml

示例输出:

1. status:
2. detailStatus:
3. - childTopologyLayer: SpineLayer
4. childTopologyNames:
5. - spine-0
6. - spine-1
7. - spine-2
8. nodeNum: 12
9. topologyInfo:
10. topologyLayer: ClusterTopologyLayer
11. topologyName: ""
12. - childTopologyLayer: BlockLayer
13. childTopologyNames:
14. - block-0
15. - block-1
16. nodeNum: 5
17. parentTopologyInfo:
18. topologyLayer: ClusterTopologyLayer
19. topologyName: ""
20. topologyInfo:
21. topologyLayer: SpineLayer
22. topologyName: spine-0
23. - childTopologyLayer: NodeTopologyLayer
24. nodeNum: 3
25. parentTopologyInfo:
26. topologyLayer: SpineLayer
27. topologyName: spine-0
28. topologyInfo:
29. topologyLayer: BlockLayer
30. topologyName: block-0
31. - childTopologyLayer: NodeTopologyLayer
32. nodeNum: 2
33. parentTopologyInfo:
34. topologyLayer: SpineLayer
35. topologyName: spine-0
36. topologyInfo:
37. topologyLayer: BlockLayer
38. topologyName: block-1
39. - childTopologyLayer: BlockLayer
40. childTopologyNames:
41. - block-2
42. - block-3
43. nodeNum: 4
44. parentTopologyInfo:
45. topologyLayer: ClusterTopologyLayer
46. topologyName: ""
47. topologyInfo:
48. topologyLayer: SpineLayer
49. topologyName: spine-1
50. - childTopologyLayer: NodeTopologyLayer
51. nodeNum: 2
52. parentTopologyInfo:
53. topologyLayer: SpineLayer
54. topologyName: spine-1
55. topologyInfo:
56. topologyLayer: BlockLayer
57. topologyName: block-2
58. - childTopologyLayer: NodeTopologyLayer
59. nodeNum: 2
60. parentTopologyInfo:
61. topologyLayer: SpineLayer
62. topologyName: spine-1
63. topologyInfo:
64. topologyLayer: BlockLayer
65. topologyName: block-3
66. - childTopologyLayer: BlockLayer
67. childTopologyNames:
68. - block-4
69. nodeNum: 3
70. parentTopologyInfo:
71. topologyLayer: ClusterTopologyLayer
72. topologyName: ""
73. topologyInfo:
74. topologyLayer: SpineLayer
75. topologyName: spine-2
76. - childTopologyLayer: NodeTopologyLayer
77. nodeNum: 3
78. parentTopologyInfo:
79. topologyLayer: SpineLayer
80. topologyName: spine-2
81. topologyInfo:
82. topologyLayer: BlockLayer
83. topologyName: block-4

基于上述 status,集群具有两层 spine-block 架构:

1. ClusterTopologyLayer
2. ├── SpineLayer: spine-0
3. │   ├── BlockLayer: block-0
4. │   │   └── NodeTopologyLayer: 3 nodes
5. │   └── BlockLayer: block-1
6. │       └── NodeTopologyLayer: 2 nodes
7. │
8. ├── SpineLayer: spine-1
9. │   ├── BlockLayer: block-2
10. │   │   └── NodeTopologyLayer: 2 nodes
11. │   └── BlockLayer: block-3
12. │       └── NodeTopologyLayer: 2 nodes
13. │
14. └── SpineLayer: spine-2
15. └── BlockLayer: block-4
16. └── NodeTopologyLayer: 3 nodes

我们可能已经注意到,此网络拓扑架构与网络拓扑算法演示中使用的架构一致。

使用示例

环境设置

为了演示网络拓扑感知调度,我们假设集群具有验证拓扑配置部分所示的拓扑配置:

  • 总共 12 个工作节点(node-0 到 node-11)
  • 3 个 Spine:
  • spine-0: 5 个节点(block-0 有 3 个节点:node-0、node-1、node-2;block-1 有 2 个节点:node-3、node-4)
  • spine-1: 4 个节点(block-2 有 2 个节点:node-5、node-6;block-3 有 2 个节点:node-7、node-8)
  • spine-2: 3 个节点(block-4 有 3 个节点:node-9、node-10、node-11)
  • 每个节点配置: 8 个 CPU 核心,32 GiB 内存
1. ClusterTopologyLayer
2. ├── SpineLayer: spine-0 (5 nodes)
3. │   ├── BlockLayer: block-0 (node-0, node-1, node-2)
4. │   └── BlockLayer: block-1 (node-3, node-4)
5. ├── SpineLayer: spine-1 (4 nodes)
6. │   ├── BlockLayer: block-2 (node-5, node-6)
7. │   └── BlockLayer: block-3 (node-7, node-8)
8. └── SpineLayer: spine-2 (3 nodes)
9. └── BlockLayer: block-4 (node-9, node-10, node-11)

我们的演示将包括:

  1. 示例 1: 使用 PreferGather 策略和 Binpack 算法的拓扑感知调度
  2. 示例 2: 当约束无法满足时的 MustGather 调度和拓扑感知抢占

示例 1: 使用 PreferGather 和 Binpack 的拓扑感知调度

此示例演示了 Koordinator 如何使用 PreferGather 策略调度 Pod,以实现最优网络性能和最小资源浪费。PreferGather 策略确保 Pod 聚集在同一拓扑域内以获得更好的网络局部性,而 Binpack 算法选择最紧凑的域以最小化资源碎片。

场景

我们将在所有 12 个节点空闲时创建一个 4 Pod 训练作业:

  • 每个 Pod 需要 8 个 CPU 核心(占用整个节点)
  • 总需求: 4 个 Pod = 4 个节点
  • 调度器应选择提供以下内容的最紧凑拓扑域:
  • 最佳网络性能: Pod 聚集在同一 Spine 中以获得更低的延迟和更高的带宽
  • 最少资源浪费: 能够容纳所有 Pod 的最小拓扑域

预期结果: Pod 调度到 spine-1(node-5 到 node-8),这是最优选择,因为:

  • 它恰好有 4 个节点(完美匹配,零浪费)
  • 所有 Pod 都在同一 Spine 中以获得最佳网络性能
  • 其他 Spine 中没有资源碎片

步骤 1: 创建具有 PreferGather 策略的 PodGroup

1. apiVersion: scheduling.sigs.k8s.io/v1alpha1
2. kind: PodGroup
3. metadata:
4. name: topology-demo-job
5. namespace: default
6. annotations:
7. gang.scheduling.koordinator.sh/network-topology-spec: |
8. {
9. "gatherStrategy": [
10. {
11. "layer": "BlockLayer",
12. "strategy": "PreferGather"
13. },
14. {
15. "layer": "SpineLayer",
16. "strategy": "PreferGather"
17. }
18. ]
19. }
20. spec:
21. minMember: 4
22. scheduleTimeoutSeconds: 300

步骤 2: 创建 4 Pod 训练作业

1. apiVersion: v1
2. kind: Pod
3. metadata:
4. name: training-pod-0
5. namespace: default
6. labels:
7. pod-group.scheduling.sigs.k8s.io: topology-demo-job
8. annotations:
9. gang.scheduling.koordinator.sh/network-topology-index: "0"
10. spec:
11. schedulerName: koord-scheduler
12. containers:
13. - name: curlimage
14. image: busybox
15. imagePullPolicy: IfNotPresent
16. command:
17. - sleep
18. - 365d
19. resources:
20. requests:
21. cpu: 8
22. memory: 32Gi
23. limits:
24. cpu: 8
25. memory: 32Gi
26. terminationMessagePath: /dev/termination-log
27. terminationMessagePolicy: File
28. restartPolicy: Always
29. ---
30. apiVersion: v1
31. kind: Pod
32. metadata:
33. name: training-pod-1
34. namespace: default
35. labels:
36. pod-group.scheduling.sigs.k8s.io: topology-demo-job
37. annotations:
38. gang.scheduling.koordinator.sh/network-topology-index: "1"
39. spec:
40. schedulerName: koord-scheduler
41. containers:
42. - name: curlimage
43. image: busybox
44. imagePullPolicy: IfNotPresent
45. command:
46. - sleep
47. - 365d
48. resources:
49. requests:
50. cpu: 8
51. memory: 32Gi
52. limits:
53. cpu: 8
54. memory: 32Gi
55. terminationMessagePath: /dev/termination-log
56. terminationMessagePolicy: File
57. restartPolicy: Always
58. ---
59. apiVersion: v1
60. kind: Pod
61. metadata:
62. name: training-pod-2
63. namespace: default
64. labels:
65. pod-group.scheduling.sigs.k8s.io: topology-demo-job
66. annotations:
67. gang.scheduling.koordinator.sh/network-topology-index: "2"
68. spec:
69. schedulerName: koord-scheduler
70. containers:
71. - name: curlimage
72. image: busybox
73. imagePullPolicy: IfNotPresent
74. command:
75. - sleep
76. - 365d
77. resources:
78. requests:
79. cpu: 8
80. memory: 32Gi
81. limits:
82. cpu: 8
83. memory: 32Gi
84. terminationMessagePath: /dev/termination-log
85. terminationMessagePolicy: File
86. restartPolicy: Always
87. ---
88. apiVersion: v1
89. kind: Pod
90. metadata:
91. name: training-pod-3
92. namespace: default
93. labels:
94. pod-group.scheduling.sigs.k8s.io: topology-demo-job
95. annotations:
96. gang.scheduling.koordinator.sh/network-topology-index: "3"
97. spec:
98. schedulerName: koord-scheduler
99. containers:
100. - name: curlimage
101. image: busybox
102. imagePullPolicy: IfNotPresent
103. command:
104. - sleep
105. - 365d
106. resources:
107. requests:
108. cpu: 8
109. memory: 32Gi
110. limits:
111. cpu: 8
112. memory: 32Gi
113. terminationMessagePath: /dev/termination-log
114. terminationMessagePolicy: File
115. restartPolicy: Always

步骤 3: 应用并验证调度结果

1. kubectl apply -f topology-demo-job.yaml

几秒钟后,检查 Pod 调度结果:

1. kubectl get pods -o wide | grep training-pod

预期输出:

1. NAME            READY   STATUS    RESTARTS   AGE   IP            NODE      NOMINATED NODE
2. training-pod-0  1/1     Running   0          30s   10.244.5.10   node-5    <none>
3. training-pod-1  1/1     Running   0          30s   10.244.6.11   node-6    <none>
4. training-pod-2  1/1     Running   0          30s   10.244.7.10   node-7    <none>
5. training-pod-3  1/1     Running   0          30s   10.244.8.11   node-8    <none>

Binpack 算法如何工作

步骤 1: 自下而上计算 offerslots

对于每个节点(每个 Pod 需要 8 个 CPU 核心以占用整个节点):

  • 所有节点(node-0 到 node-11): offerslots = 1 (8 个可用核心 / 每个 Pod 8 个核心)

对于每个 Block:

  • block-0 (node-0, node-1, node-2): offerslots = 3
  • block-1 (node-3, node-4): offerslots = 2
  • block-2 (node-5, node-6): offerslots = 2
  • block-3 (node-7, node-8): offerslots = 2
  • block-4 (node-9, node-10, node-11): offerslots = 3

对于每个 Spine:

  • spine-0 (block-0, block-1): offerslots = 5
  • spine-1 (block-2, block-3): offerslots = 4
  • spine-2 (block-4): offerslots = 3

步骤 2: 查找候选拓扑节点

我们需要 4 个 Pod(4 个 offerslots)。只有 spine-0(5 个槽位)和 spine-1(4 个槽位)可以容纳该作业。

在 Block 级别,没有单个 block 可以容纳所有 4 个 Pod,因此候选者在 Spine 级别。

步骤 3: 应用 Binpack 算法

在候选 Spine 中:

  • spine-0: offerslots = 5, 需要 = 4, 浪费 = 1
  • spine-1: offerslots = 4, 需要 = 4, 浪费 = 0 ← 被选中(最小浪费)

Binpack 算法选择 spine-1(node-5 到 node-8),放置所有 4 个 Pod 后零浪费。

最终放置: 所有 4 个 Pod 都调度到 spine-1(node-5、node-6、node-7、node-8),实现了:

  • 最优网络性能(PreferGather 效果): 所有 Pod 都聚集在同一 Spine(spine-1)内,最小化跨 Spine 流量并最大化 Pod 间通信的网络带宽
  • 最小资源浪费(Binpack 效果): spine-1 恰好有 4 个槽位供 4 个 Pod 使用(零浪费),而 spine-0 会有 1 个槽位浪费。这使得 spine-0 的所有 5 个节点都可用于未来的工作负载,防止资源碎片

示例 2: MustGather 和拓扑感知抢占

此示例演示了为什么某些工作负载需要 MustGather 以及抢占如何遵循网络拓扑约束。

为什么需要 MustGather: 在延迟敏感的分布式训练场景中,网络通信模式对延迟极其敏感。如果 Pod 分散在不同的 Spine 上,跨 Spine 通信开销会使训练时间增加 3-5 倍,使作业在经济上不可行。因此,MustGather 不仅仅是优化——它是这些工作负载的硬性要求。

抢占如何遵循拓扑: 当由于资源不足而无法满足 MustGather 约束时,Koordinator 执行拓扑感知抢占。这意味着:

  1. 调度器识别需要抢占哪些低优先级 Pod
  2. 抢占决策遵循与调度相同的拓扑约束
  3. 调度器确保释放的资源可以满足 MustGather 要求
  4. 通过 nominatedNode 预留资源以防止其他 Pod 窃取它们

场景

此示例展示了以下场景:

  1. 低优先级 Pod 占用 node-0、node-1 和 node-5(每个 Pod 占用整个节点)
  2. 提交一个具有 MustGather 到 SpineLayer 的高优先级 4 Pod 作业(每个 Pod 占用整个节点)
  3. 没有单个 Spine 在不抢占的情况下拥有 4 个可用节点
  4. Koordinator 执行拓扑感知抢占:抢占 node-5 上的 Pod 并将所有 4 个高优先级 Pod 调度到 node-5 到 node-8(在 spine-1 内),在整个抢占过程中维护 MustGather 约束

  5. 3 个低优先级 Pod 已在 node-0、node-1 和 node-5 上运行(每个占用 8 核心的整个节点)

  6. 一个具有严格网络要求的高优先级 4 Pod 训练作业:
  7. 需要 MustGather 到 SpineLayer:跨 Spine 延迟会使性能降低 3-5 倍
  8. 每个 Pod 需要 8 个 CPU 核心(占用整个节点)
  9. 当前状态: 没有单个 Spine 拥有 4 个可用节点
  10. spine-0: 3 个可用节点(node-2、node-3、node-4) - node-0、node-1 被占用
  11. spine-1: 3 个可用节点(node-6、node-7、node-8) - node-5 被占用
  12. spine-2: 3 个可用节点(node-9、node-10、node-11)

预期结果: 调度器执行拓扑感知抢占,抢占 node-5 上的低优先级 Pod 以满足 MustGather 约束,并将所有 4 个高优先级 Pod 调度到 spine-1(node-5 到 node-8)。

步骤 1: 定义 PriorityClass

1. apiVersion: scheduling.k8s.io/v1
2. kind: PriorityClass
3. metadata:
4. name: high-priority
5. value: 1000000
6. preemptionPolicy: PreemptLowerPriority
7. description: "High priority for critical training jobs"
8. ---
9. apiVersion: scheduling.k8s.io/v1
10. kind: PriorityClass
11. metadata:
12. name: low-priority
13. value: 1000
14. preemptionPolicy: PreemptLowerPriority
15. description: "Low priority for best-effort jobs"
1. kubectl apply -f priorityclasses.yaml

步骤 2: 在 node-0、node-1 和 node-5 上创建低优先级 Pod

1. apiVersion: v1
2. kind: Pod
3. metadata:
4. name: low-priority-pod-0
5. namespace: default
6. spec:
7. schedulerName: koord-scheduler
8. priorityClassName: low-priority
9. nodeName: node-0
10. containers:
11. - name: curlimage
12. image: busybox
13. imagePullPolicy: IfNotPresent
14. command:
15. - sleep
16. - 365d
17. resources:
18. requests:
19. cpu: 8
20. memory: 32Gi
21. limits:
22. cpu: 8
23. memory: 32Gi
24. terminationMessagePath: /dev/termination-log
25. terminationMessagePolicy: File
26. restartPolicy: Always
27. ---
28. apiVersion: v1
29. kind: Pod
30. metadata:
31. name: low-priority-pod-1
32. namespace: default
33. spec:
34. schedulerName: koord-scheduler
35. priorityClassName: low-priority
36. nodeName: node-1
37. containers:
38. - name: curlimage
39. image: busybox
40. imagePullPolicy: IfNotPresent
41. command:
42. - sleep
43. - 365d
44. resources:
45. requests:
46. cpu: 8
47. memory: 32Gi
48. limits:
49. cpu: 8
50. memory: 32Gi
51. terminationMessagePath: /dev/termination-log
52. terminationMessagePolicy: File
53. restartPolicy: Always
54. ---
55. apiVersion: v1
56. kind: Pod
57. metadata:
58. name: low-priority-pod-5
59. namespace: default
60. spec:
61. schedulerName: koord-scheduler
62. priorityClassName: low-priority
63. nodeName: node-5
64. containers:
65. - name: curlimage
66. image: busybox
67. imagePullPolicy: IfNotPresent
68. command:
69. - sleep
70. - 365d
71. resources:
72. requests:
73. cpu: 8
74. memory: 32Gi
75. limits:
76. cpu: 8
77. memory: 32Gi
78. terminationMessagePath: /dev/termination-log
79. terminationMessagePolicy: File
80. restartPolicy: Always
1. kubectl apply -f low-priority-pods.yaml

验证 Pod 正在运行:

1. kubectl get pods -o wide
1. NAME                  READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE
2. low-priority-pod-0    1/1     Running   0          10s   10.244.0.10   node-0   <none>
3. low-priority-pod-1    1/1     Running   0          10s   10.244.1.10   node-1   <none>
4. low-priority-pod-5    1/1     Running   0          10s   10.244.5.10   node-5   <none>

步骤 3: 创建具有 MustGather 到 SpineLayer 的高优先级作业

1. apiVersion: scheduling.sigs.k8s.io/v1alpha1
2. kind: PodGroup
3. metadata:
4. name: high-priority-training
5. namespace: default
6. annotations:
7. gang.scheduling.koordinator.sh/network-topology-spec: |
8. {
9. "gatherStrategy": [
10. {
11. "layer": "SpineLayer",
12. "strategy": "MustGather"
13. }
14. ]
15. }
16. spec:
17. minMember: 4
18. scheduleTimeoutSeconds: 300
1. apiVersion: v1
2. kind: Pod
3. metadata:
4. name: hp-training-pod-0
5. namespace: default
6. labels:
7. pod-group.scheduling.sigs.k8s.io: high-priority-training
8. annotations:
9. gang.scheduling.koordinator.sh/network-topology-index: "0"
10. spec:
11. schedulerName: koord-scheduler
12. priorityClassName: high-priority
13. preemptionPolicy: PreemptLowerPriority
14. containers:
15. - name: curlimage
16. image: busybox
17. imagePullPolicy: IfNotPresent
18. command:
19. - sleep
20. - 365d
21. resources:
22. requests:
23. cpu: 8
24. memory: 32Gi
25. limits:
26. cpu: 8
27. memory: 32Gi
28. terminationMessagePath: /dev/termination-log
29. terminationMessagePolicy: File
30. restartPolicy: Always
31. ---
32. apiVersion: v1
33. kind: Pod
34. metadata:
35. name: hp-training-pod-1
36. namespace: default
37. labels:
38. pod-group.scheduling.sigs.k8s.io: high-priority-training
39. annotations:
40. gang.scheduling.koordinator.sh/network-topology-index: "1"
41. spec:
42. schedulerName: koord-scheduler
43. priorityClassName: high-priority
44. preemptionPolicy: PreemptLowerPriority
45. containers:
46. - name: curlimage
47. image: busybox
48. imagePullPolicy: IfNotPresent
49. command:
50. - sleep
51. - 365d
52. resources:
53. requests:
54. cpu: 8
55. memory: 32Gi
56. limits:
57. cpu: 8
58. memory: 32Gi
59. terminationMessagePath: /dev/termination-log
60. terminationMessagePolicy: File
61. restartPolicy: Always
62. ---
63. apiVersion: v1
64. kind: Pod
65. metadata:
66. name: hp-training-pod-2
67. namespace: default
68. labels:
69. pod-group.scheduling.sigs.k8s.io: high-priority-training
70. annotations:
71. gang.scheduling.koordinator.sh/network-topology-index: "2"
72. spec:
73. schedulerName: koord-scheduler
74. priorityClassName: high-priority
75. preemptionPolicy: PreemptLowerPriority
76. containers:
77. - name: curlimage
78. image: busybox
79. imagePullPolicy: IfNotPresent
80. command:
81. - sleep
82. - 365d
83. resources:
84. requests:
85. cpu: 8
86. memory: 32Gi
87. limits:
88. cpu: 8
89. memory: 32Gi
90. terminationMessagePath: /dev/termination-log
91. terminationMessagePolicy: File
92. restartPolicy: Always
93. ---
94. apiVersion: v1
95. kind: Pod
96. metadata:
97. name: hp-training-pod-3
98. namespace: default
99. labels:
100. pod-group.scheduling.sigs.k8s.io: high-priority-training
101. annotations:
102. gang.scheduling.koordinator.sh/network-topology-index: "3"
103. spec:
104. schedulerName: koord-scheduler
105. priorityClassName: high-priority
106. preemptionPolicy: PreemptLowerPriority
107. containers:
108. - name: curlimage
109. image: busybox
110. imagePullPolicy: IfNotPresent
111. command:
112. - sleep
113. - 365d
114. resources:
115. requests:
116. cpu: 8
117. memory: 32Gi
118. limits:
119. cpu: 8
120. memory: 32Gi
121. terminationMessagePath: /dev/termination-log
122. terminationMessagePolicy: File
123. restartPolicy: Always

步骤 4: 应用并观察抢占

1. kubectl apply -f high-priority-training.yaml

提交后立即检查 Pod 状态:

1. kubectl get pods -o wide

抢占期间的预期输出:

1. NAME                  READY   STATUS        RESTARTS   AGE   IP            NODE     NOMINATED NODE
2. low-priority-pod-0    1/1     Running       0          2m    10.244.0.10   node-0   <none>
3. low-priority-pod-1    1/1     Running       0          2m    10.244.1.10   node-1   <none>
4. low-priority-pod-5    0/1     Terminating   0          2m    10.244.5.10   node-5   <none>
5. hp-training-pod-0     0/1     Pending       0          10s   <none>        <none>   node-5
6. hp-training-pod-1     0/1     Pending       0          10s   <none>        <none>   node-6
7. hp-training-pod-2     0/1     Pending       0          10s   <none>        <none>   node-7
8. hp-training-pod-3     0/1     Pending       0          10s   <none>        <none>   node-8

理解拓扑感知抢占期间的调度器消息

在抢占进行中(在受害 Pod 完全终止之前),高优先级 Pod 将处于 Pending 状态。您可以检查详细的调度器消息:

1. kubectl get pod hp-training-pod-0 -o yaml

Pod 状态将包含说明为什么调度正在等待的信息性消息:

1. status:
2. conditions:
3. - lastProbeTime: null
4. lastTransitionTime: "2025-10-23T07:35:30Z"
5. message: '0/12 nodes are available: no candidate topology nodes can accommodate
6. job, desiredOfferSlot: 4, topology topologyNode SpineLayer/spine-0: 3;topology
7. topologyNode SpineLayer/spine-1: 3;topology topologyNode SpineLayer/spine-2:
8. 3. preemption: not eligible due to terminating pod on the nominated node..'
9. reason: Unschedulable
10. status: "False"
11. type: PodScheduled
12. nominatedNodeName: node-5
13. phase: Pending

消息中的关键信息:

  1. 拓扑约束评估: no candidate topology nodes can accommodate job, desiredOfferSlot: 4

  2. 作业需要在单个拓扑域内有 4 个槽位(节点)

  3. 每个 Spine 的可用性:

  4. topology topologyNode SpineLayer/spine-0: 3 - 只有 3 个可用节点

  5. topology topologyNode SpineLayer/spine-1: 3 - 只有 3 个可用节点
  6. topology topologyNode SpineLayer/spine-2: 3 - 只有 3 个可用节点
  7. 没有任何 Spine 可以在不抢占的情况下满足 MustGather 要求
  8. 抢占状态: preemption: not eligible due to terminating pod on the nominated node

  9. 已为 node-5 上的 low-priority-pod-5 触发抢占

  10. 调度器正在等待受害 Pod 完全终止
  11. 资源已通过 nominatedNodeName: node-5 预留
  12. GangGroup 协调: 其他成员 Pod(hp-training-pod-1、hp-training-pod-2、hp-training-pod-3)将显示类似消息,表明它们由于 GangGroup 约束而等待:
1. message: 'GangGroup "default/high-priority-training" gets rejected due to member Pod
2. "default/hp-training-pod-0" is unschedulable with reason "no candidate topology nodes
3. can accommodate job, desiredOfferSlot: 4...", alreadyWaitForBound: 0. preemption:
4. preemption already attempted by default/hp-training-pod-0 with message not eligible
5. due to terminating pod on the nominated node..'

此消息确认:

  • GangGroup 中的所有 Pod 一起等待(gang 调度语义)
  • 抢占在作业级别协调
  • 在整个抢占过程中强制执行拓扑约束

步骤 5: 验证最终调度结果

在 node-5 上的低优先级 Pod 终止后,再次检查:

1. kubectl get pods -o wide

预期的最终输出:

1. NAME                  READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE
2. low-priority-pod-0    1/1     Running   0          3m    10.244.0.10   node-0   <none>
3. low-priority-pod-1    1/1     Running   0          3m    10.244.1.10   node-1   <none>
4. hp-training-pod-0     1/1     Running   0          45s   10.244.5.20   node-5   <none>
5. hp-training-pod-1     1/1     Running   0          45s   10.244.6.20   node-6   <none>
6. hp-training-pod-2     1/1     Running   0          45s   10.244.7.20   node-7   <none>
7. hp-training-pod-3     1/1     Running   0          45s   10.244.8.20   node-8   <none>

拓扑感知抢占如何工作

步骤 1: 识别拓扑要求并验证 MustGather 约束

高优先级作业要求所有 4 个 Pod 都在同一个 Spine 中(MustGather 到 SpineLayer)。这是一个硬约束——如果不满足此要求,作业将无法运行。每个 Pod 需要 8 个 CPU 核心(整个节点)。

步骤 2: 评估每个 Spine 的可用资源(遵循拓扑)

当前集群状态:

  • spine-0 (5 个节点):
  • node-0: 被 low-priority-pod-0 占用(8 核心已使用,0 核心可用)
  • node-1: 被 low-priority-pod-1 占用(8 核心已使用,0 核心可用)
  • node-2: 可用(8 核心)
  • node-3: 可用(8 核心)
  • node-4: 可用(8 核心)
  • 总计: 3 个节点可用(作业需要 4 个)
  • spine-1 (4 个节点):
  • node-5: 被 low-priority-pod-5 占用(8 核心已使用,0 核心可用)
  • node-6: 可用(8 核心)
  • node-7: 可用(8 核心)
  • node-8: 可用(8 核心)
  • 总计: 3 个节点可用(作业需要 4 个)
  • spine-2 (3 个节点):
  • node-9: 可用(8 核心)
  • node-10: 可用(8 核心)
  • node-11: 可用(8 核心)
  • 总计: 3 个节点可用(即使抢占也无法容纳 4 个 Pod)

关键观察: 没有单个 Spine 在不抢占的情况下拥有足够的可用资源。调度器必须执行拓扑感知抢占以满足 MustGather 约束。

步骤 3: 确定拓扑感知抢占策略

调度器在遵循 SpineLayer 拓扑约束的同时评估最小抢占选项:

  • spine-0: 需要抢占 1 个节点(node-0 或 node-1),结果有 4 个可用节点
  • 抢占数量: 1 个 Pod
  • 抢占后: 可以满足 MustGather 到 SpineLayer
  • spine-1: 需要抢占 1 个节点(node-5),结果有 4 个可用节点
  • 抢占数量: 1 个 Pod
  • 抢占后: 可以满足 MustGather 到 SpineLayer
  • spine-2: 无法容纳 4 个 Pod(总共只有 3 个节点)
  • 不是有效候选者

spine-0 和 spine-1 都需要相同数量的抢占(1 个 Pod)。Binpack 算法选择 spine-1,因为:

  1. 它恰好有 4 个节点(完美匹配,零浪费)
  2. 抢占 node-5 后: spine-1 中的所有 4 个节点都变为可用
  3. 这以最小的干扰满足了 MustGather 约束

步骤 4: 执行拓扑感知抢占

Koordinator 在维护拓扑约束的同时执行抢占:

  1. 标记 low-priority-pod-5 进行抢占(设置 DisruptionTarget 条件)
  2. 为所有 4 个高优先级 Pod 设置 nominatedNode 到 spine-1 内的节点(node-5 到 node-8)
  3. 这确保在资源预留期间保留拓扑约束
  4. 删除 low-priority-pod-5
  5. 将所有高优先级 Pod 调度到 spine-1,满足 MustGather 约束

关键要点:

  1. MustGather 是硬性要求: 对于张量并行训练等延迟敏感的工作负载,MustGather 不是可选的——它对于可接受的性能至关重要。跨 Spine 通信会使作业速度降低 3-5 倍。
  2. 抢占遵循拓扑约束: 当可用资源无法满足 MustGather 时,调度器执行拓扑感知抢占。它在每个拓扑域(Spine)内分别评估抢占候选者,确保最终放置满足拓扑要求。
  3. 最小干扰: 调度器仅抢占了 1 个 Pod(满足 MustGather 约束所需的最小数量),选择 spine-1 是因为它是完美匹配(4 个节点供 4 个 Pod 使用)。
  4. 资源预留维护拓扑: nominatedNode 机制在所选拓扑域(spine-1)内预留资源,防止其他 Pod 在抢占过程中破坏拓扑约束。
  5. 端到端拓扑保证: 从抢占决策通过资源预留到最终调度,整个过程都维护 MustGather 约束,确保所有 4 个 Pod 都放置在 spine-1 内。

高级配置

拓扑层命名约定

建议使用清晰的命名约定来识别拓扑层:

1. # 推荐的层命名
2. NodeTopologyLayer       # 节点层
3. AcceleratorLayer        # 加速器互连层(例如 NVLink)
4. BlockLayer              # Block 交换机层
5. SpineLayer              # Spine 交换机层
6. DatacenterLayer         # 数据中心层

拓扑策略选择指南

策略 用例 行为
--- --- ---
PreferGather 拓扑层需要良好的网络性能但可以容忍一些分散 尽可能聚集,但当资源不足时允许分散
MustGather 拓扑层具有严格的网络带宽要求 必须在指定层中聚集,否则调度失败

PodCountMultiple 配置

在一些分布式训练场景中,放置在每个拓扑域内的 Pod 数量必须是特定值 X 的整数倍,其中 X 与域内的通信参数(例如张量并行度)相关。例如,如果 TP=8,则每个 Block 应精确包含 0 或 8 个 Pod——不允许部分组。

可以通过聚集策略中的 podCountMultiple 字段进行配置:

1. apiVersion: scheduling.sigs.k8s.io/v1alpha1
2. kind: PodGroup
3. metadata:
4. name: tp-training-job
5. namespace: default
6. annotations:
7. gang.scheduling.koordinator.sh/network-topology-spec: |
8. {
9. "gatherStrategy": [
10. {
11. "layer": "SpineLayer",
12. "strategy": "PreferGather"
13. },
14. {
15. "layer": "BlockLayer",
16. "strategy": "PreferGather",
17. "podCountMultiple": 8
18. }
19. ]
20. }
21. spec:
22. minMember: 16
23. scheduleTimeoutSeconds: 300

在上面的示例中,BlockLayer 的 podCountMultiple: 8 表示分配到每个 Block 的 Pod 数量必须是 8 的整数倍。在计算 offerslots 时,算法会将每个拓扑节点的 offerslots 向下取整为该层指定的 podCountMultiple 值的最近整数倍。例如,如果一个 Block 有 10 个可用槽位但 podCountMultiple 为 8,则该 Block 的有效 offerslots 将为 8。

多 PodGroup 协调调度

对于包含 master 和 worker 的复杂训练作业,使用 GangGroup 语义:

1. apiVersion: scheduling.sigs.k8s.io/v1alpha1
2. kind: PodGroup
3. metadata:
4. annotations:
5. gang.scheduling.koordinator.sh/groups: |
6. ["default/llm-master", "default/llm-worker"]
7. gang.scheduling.koordinator.sh/network-topology-spec: |
8. {
9. "gatherStrategy": [
10. {
11. "layer": "SpineLayer",
12. "strategy": "MustGather"
13. }
14. ]
15. }

确保所有相关的 PodGroup 使用相同的 gang.scheduling.koordinator.sh/groups 和 gang.scheduling.koordinator.sh/network-topology-spec 注解。

故障排查

Pod 卡在 Pending 状态

可能原因: 网络拓扑约束过于严格,没有足够的节点满足要求

解决方案:

  • 检查集群拓扑配置: kubectl get clusternetworktopology default -o yaml
  • 放宽拓扑策略,将 MustGather 更改为 PreferGather
  • 添加集群节点或调整节点拓扑标签

拓扑感知调度未生效

故障排查步骤:

  1. 验证 Koord-Scheduler 是否启用了网络拓扑插件:
1. kubectl -n koordinator-system get cm koord-scheduler-config -o yaml
  1. 检查节点是否具有正确的拓扑标签:
1. kubectl get nodes --show-labels | grep network.topology
  1. 检查 ClusterNetworkTopology 状态:
1. kubectl get clusternetworktopology default -o yaml

抢占未按预期发生

检查:

  1. 确认 PriorityClass 配置正确
  2. 检查 Pod 的 preemptionPolicy 设置
  3. 查看调度器日志:
1. kubectl -n koordinator-system logs -l component=koord-scheduler --tail=100

最佳实践

  1. 从 PreferGather 策略开始: 对于初始部署,使用 PreferGather 了解 Pod 放置模式,然后再应用严格的 MustGather 约束
  2. 为分布式训练使用拓扑索引: 始终分配 gang.scheduling.koordinator.sh/network-topology-index 注解,以在数据并行作业中建立清晰的通信模式
  3. 选择适当的拓扑层:

  4. 对于延迟敏感的推理服务: MustGather + BlockLayer

  5. 对于大规模 LLM 训练: MustGather + SpineLayer
  6. 对于一般分布式训练: 跨多个层的 PreferGather 组合
  7. 为关键工作负载结合优先级: 为生产训练作业设置高优先级,以确保它们可以在维护拓扑约束的同时抢占低优先级作业
  8. 监控拓扑分布: 定期检查 Pod 放置模式,并根据实际网络性能指标调整拓扑策略
  9. 规划抢占场景: 使用 MustGather 时,确保至少一个拓扑域中有足够的容量,以避免调度失败

参考资料