Skip to content

Commit 3c6aaff

Browse files
authored
docs: describe how a training workload attaches to the cluster fabric (#2306)
Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
1 parent 21b538d commit 3c6aaff

3 files changed

Lines changed: 333 additions & 5 deletions

File tree

docs/index.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ navigation:
2828
path: user/cli-reference.md
2929
- page: Generating Bundles
3030
path: user/bundling.md
31+
- page: Fabric-Attached Training
32+
path: user/fabric-attached-training.md
3133
- page: CLI Configuration File
3234
path: user/cli-config.md
3335
- page: Artifact Verification

docs/integrator/gke-tcpxo-networking.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ spec:
132132
hostNetwork: false
133133
containers:
134134
- name: tcpxo-daemon
135-
image: us-docker.pkg.dev/gce-ai-infra/gpudirect-tcpxo/tcpgpudmarxd-dev:v1.0.20
135+
image: us-docker.pkg.dev/gce-ai-infra/gpudirect-tcpxo/tcpgpudmarxd-dev:v1.0.21
136136
securityContext:
137137
capabilities:
138138
add: [NET_ADMIN, NET_BIND_SERVICE]
@@ -175,18 +175,39 @@ Key properties:
175175
- NRI annotations inject GPU devices and multi-NIC interfaces
176176
- Requires NRI device injector DaemonSet deployed on GPU nodes
177177

178-
See [`demos/workloads/training/gke-nccl-test-tcpxo.yaml`](https://github.com/NVIDIA/aicr/blob/main/demos/workloads/training/gke-nccl-test-tcpxo.yaml) for a complete 2-node NCCL benchmark example.
178+
Running a **Kubeflow TrainJob** rather than a bare Pod? A TrainJob cannot add
179+
the `tcpxo-daemon` sidecar, so the wiring must live in a `TrainingRuntime` — see
180+
[Attaching a Training Workload to the Cluster Fabric](../user/fabric-attached-training.md).
181+
182+
See [`demos/workloads/training/gke-nccl-test-tcpxo.yaml`](https://github.com/NVIDIA/aicr/blob/main/demos/workloads/training/gke-nccl-test-tcpxo.yaml) for a complete 2-node NCCL benchmark example. (pinned to an earlier coupled pair, plugin `v1.0.14` with daemon `v1.0.20`)
179183

180184
## NCCL Plugin Version Matching
181185

182-
The NCCL test container image must match the cluster's installed TCPXO plugin version. Check with:
186+
Google publishes the plugin installer and the `tcpxo-daemon` sidecar as a
187+
**coupled release pair**. Running a mismatched pair is unsupported. The pair
188+
AICR currently ships is:
189+
190+
| Component | Image | Version |
191+
|---|---|---|
192+
| Plugin installer (DaemonSet, cluster-side) | `nccl-plugin-gpudirecttcpx-dev` | `v1.0.15` |
193+
| Sidecar (workload-side) | `tcpgpudmarxd-dev` | `v1.0.21` |
194+
195+
Check what your cluster actually runs:
183196

184197
```shell
185198
kubectl get ds nccl-tcpxo-installer -n kube-system \
186-
-o jsonpath='{.spec.template.spec.containers[?(@.name=="nccl-tcpxo-installer")].image}'
199+
-o jsonpath='{.spec.template.spec.initContainers[?(@.name=="nccl-tcpxo-installer")].image}'
187200
```
188201

189-
Update the `nccl-plugin-gpudirecttcpx-dev` image tag in your workload to match.
202+
Then set your workload's `tcpxo-daemon` image to the daemon version paired with
203+
it, per [Google's release notes][tcpxo-releases].
204+
205+
**Upgrade in order:** upgrade the plugin installer first, then the workload's
206+
daemon. Google also advises that workloads should not be running while the
207+
installer is upgraded. This is a sequence, not a statement that a mismatched
208+
pair is supported to run.
209+
210+
[tcpxo-releases]: https://github.com/GoogleCloudPlatform/container-engine-accelerators/blob/master/gpudirect-tcpxo/README.md
190211

191212
## Running the NCCL Benchmark
192213

@@ -237,6 +258,8 @@ NRI profile (recommended, no `hostNetwork`):
237258

238259
```shell
239260
kubectl create ns nccl-test
261+
# Note: this manifest is pinned to the earlier v1.0.14 / v1.0.20 pair.
262+
# Update both images to your cluster's pair before applying.
240263
kubectl apply -f demos/workloads/training/gke-nccl-test-tcpxo.yaml -n nccl-test
241264
242265
# Wait for pods to be 2/2 Running
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# Attaching a Training Workload to the Cluster Fabric
2+
3+
AICR recipes deliver the cluster side of high-speed inter-node networking — the
4+
NCCL plugin, device injection, node labels and taints. They do **not** attach
5+
your workload to it. That part is yours, and this page describes what it
6+
involves.
7+
8+
Without it a multi-node job still runs: NCCL falls back to TCP over the primary
9+
interface. Intra-node NVLink is unaffected, so nothing errors and the job
10+
completes — just far slower than the hardware allows. Check for it rather than
11+
assuming it.
12+
13+
## Which layer owns what
14+
15+
Kubeflow Trainer splits a workload across two objects, and the split decides
16+
where fabric wiring can live.
17+
18+
A **TrainJob** can supply pod annotations and labels (`PodTemplatePatch.Metadata`)
19+
and volumes (`PodSpecPatch.Volumes`) through `spec.runtimePatches`, plus
20+
`image`, `command`, `args`, `env` and `resourcesPerNode` for the trainer
21+
through `spec.trainer`.
22+
23+
Note where worker **environment** goes: `ContainerPatch.env` is rejected for the
24+
`node`, `dataset-initializer` and `model-initializer` containers, so worker
25+
variables belong in `spec.trainer.env`, not in a runtime patch. `ContainerPatch`
26+
can still set `volumeMounts` and `securityContext` on `node`.
27+
28+
A **TrainJob cannot add a container.** `ContainerPatch` carries only `name`,
29+
`env`, `volumeMounts` and `securityContext` — no `image`, no `command` — and the
30+
runtime rejects a patch naming a container it does not already define.
31+
32+
That single limitation decides the rest:
33+
34+
| Fabric | Platform | Needs a sidecar? | Where the wiring goes |
35+
|---|---|---|---|
36+
| GPUDirect TCPXO | GKE, A3 Mega | yes (`tcpxo-daemon`) | a `TrainingRuntime` you author |
37+
| EFA | EKS | no | your `TrainJob` |
38+
| InfiniBand / RDMA | AKS | no | your `TrainJob` |
39+
40+
## GKE GPUDirect TCPXO
41+
42+
TCPXO needs the `tcpxo-daemon` sidecar, so the wiring cannot live in a TrainJob.
43+
`TrainingRuntime` is an ordinary namespaced resource: author one in your
44+
namespace and reference it from `runtimeRef`.
45+
46+
**What that runtime must carry.** The annotations and sidecar are specified in
47+
[Workload Pod Configuration](../integrator/gke-tcpxo-networking.md#workload-pod-configuration-nri-profile);
48+
the `dshm` volume, worker `IPC_LOCK`, daemon `args` and NCCL settings are not in
49+
that section — take them from AICR's validator runtime, cited below.
50+
51+
- the `networking.gke.io/interfaces` and `devices.gke.io/container.tcpxo-daemon`
52+
annotations, on the pod template metadata
53+
- the `tcpxo-daemon` native sidecar, at the version paired with the plugin your
54+
cluster runs
55+
- four hostPath volumes plus a memory-backed `dshm` at `/dev/shm`, and `IPC_LOCK`
56+
on the worker
57+
- the NCCL configuration for your plugin release. The ~40 `NCCL_FASTRAK_*`
58+
tuning variables ship as `/usr/local/nvidia/lib64/nccl-env-profile.sh`, laid
59+
down by the plugin installer and version-matched to it. Source that file in
60+
your container's startup rather than transcribing the variables — a job that
61+
skips it still attaches to the fabric, but runs well under its bandwidth
62+
63+
**A placement sketch**`"<...>"` marks a value you must fill in. It shows
64+
where each piece goes; it is not a manifest, abridged or otherwise, and the
65+
authoritative field list is the integrator page linked below:
66+
67+
```yaml
68+
apiVersion: trainer.kubeflow.org/v1alpha1
69+
kind: TrainingRuntime # namespaced — yours to create
70+
metadata:
71+
name: torch-tcpxo
72+
labels:
73+
trainer.kubeflow.org/framework: torch
74+
spec:
75+
mlPolicy: {numNodes: 2, torch: {}}
76+
template:
77+
spec:
78+
replicatedJobs:
79+
- name: node
80+
template:
81+
metadata:
82+
labels:
83+
trainer.kubeflow.org/trainjob-ancestor-step: trainer # required
84+
spec:
85+
template:
86+
metadata:
87+
annotations:
88+
devices.gke.io/container.tcpxo-daemon: "<NRI device list — see reference>"
89+
networking.gke.io/default-interface: eth0
90+
networking.gke.io/interfaces: "<JSON array, 9 entries — see reference>"
91+
spec:
92+
nodeSelector: "<carry over from your bundle>"
93+
tolerations: "<carry over from your bundle>"
94+
initContainers:
95+
- name: tcpxo-daemon
96+
restartPolicy: Always # native sidecar — without this the
97+
# pod stalls in initialization
98+
image: "<registry>/tcpgpudmarxd-dev:<paired-with-your-plugin>"
99+
args: "<see reference>" # + capabilities, volumeMounts
100+
containers:
101+
- name: node
102+
image: <your training image>
103+
resources:
104+
limits: {nvidia.com/gpu: 8} # TCPXO needs all 8
105+
env: "<incl. NCCL_FASTRAK_LLCM_DEVICE_DIRECTORY>"
106+
securityContext: "<IPC_LOCK>" # + volumeMounts
107+
volumes: "<4 hostPaths + dshm>"
108+
```
109+
110+
Then reference it from the TrainJob, which carries no fabric configuration at all:
111+
112+
```yaml
113+
apiVersion: trainer.kubeflow.org/v1alpha1
114+
kind: TrainJob
115+
spec:
116+
runtimeRef:
117+
name: torch-tcpxo
118+
kind: TrainingRuntime
119+
apiGroup: trainer.kubeflow.org
120+
trainer:
121+
numNodes: 2
122+
image: my-registry/my-trainer:latest
123+
```
124+
125+
**The authoritative wiring** is
126+
[Workload Pod Configuration](../integrator/gke-tcpxo-networking.md#workload-pod-configuration-nri-profile)
127+
together with the version table in
128+
[NCCL Plugin Version Matching](../integrator/gke-tcpxo-networking.md#nccl-plugin-version-matching).
129+
Take the annotation, sidecar, volume and capability requirements from there.
130+
131+
AICR's performance validator applies a runtime with the same wiring
132+
([`validators/performance/testdata/h100/gke/runtime.yaml`](https://github.com/NVIDIA/aicr/blob/main/validators/performance/testdata/h100/gke/runtime.yaml)),
133+
which is useful to read for shape. It is **not** a template to copy: it is an
134+
MPI benchmark rather than a training job, it carries validator-only
135+
placeholders substituted at apply time, and its own image pins are not
136+
guaranteed to match the pair the recipe currently ships.
137+
138+
Two details are easy to miss because they are not in the pod spec:
139+
140+
- Your runtime's `node` replicated job needs the label
141+
`trainer.kubeflow.org/trainjob-ancestor-step: trainer`. Without it Trainer
142+
applies none of the TrainJob's `trainer` block — image, command, `numNodes`,
143+
or the `PET_*` rendezvous variables.
144+
- AICR's bundler injects `nodeSelector` and `tolerations` into the runtime it
145+
ships, from `--accelerated-node-selector` and `--accelerated-node-toleration`.
146+
**A runtime you author inherits nothing**, so carry your bundle's resolved
147+
values across or the job may stay Pending — or land on another 8-GPU pool
148+
without TCPXO.
149+
150+
### The network names are yours to supply
151+
152+
This is the part no example can fill in for you. The
153+
`networking.gke.io/interfaces` annotation must name the eight GPU NIC `Network`
154+
objects **as they exist on your cluster**. AICR requires only that each name
155+
contain `gpu-nic`; the rest is chosen by whoever provisioned it, so prefixed
156+
forms such as `aicr-demo2-gpu-nic-0` are common.
157+
158+
```shell
159+
kubectl get networks.networking.gke.io \
160+
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | grep gpu-nic
161+
```
162+
163+
That prints bare names, which is what the annotation takes. `-o name` would
164+
prefix them with `network.networking.gke.io/` — not the form to paste.
165+
166+
`Network` is cluster-scoped. If your role is namespace-only you will not be able
167+
to list them; ask whoever provisions the cluster for the eight names, or for the
168+
`GKENetworkParamSet` mapping.
169+
170+
### Do not set `resourcesPerNode`
171+
172+
Let the runtime own the resource shape. On the pinned Kubeflow Trainer
173+
**v2.2.0**, `resourcesPerNode` on a TrainJob is not merged into the runtime's —
174+
a value carrying `limits` or `requests` **replaces**
175+
the worker's resource requirements outright, so a job that sets it to ask for
176+
memory silently loses the runtime's `nvidia.com/gpu: 8` and TCPXO stops working.
177+
178+
Setting it at all — even to `{}` — also feeds Torch's process-count inference,
179+
which prefers the TrainJob's value and can yield `PET_NPROC_PER_NODE=1`.
180+
181+
If you must set it, repeat *every* resource in it, including the GPU request.
182+
183+
## EKS EFA
184+
185+
EFA needs no sidecar, so a TrainJob can attach to it against a generic runtime
186+
such as the `torch-distributed` runtime AICR ships. It needs three things:
187+
188+
**The device request**, alongside every other resource. `torch-distributed`
189+
carries **no `resources` block at all**, so `resourcesPerNode` is the only
190+
source of GPUs here — omitting `nvidia.com/gpu` yields a pod with none:
191+
192+
```yaml
193+
apiVersion: trainer.kubeflow.org/v1alpha1
194+
kind: TrainJob
195+
spec:
196+
runtimeRef:
197+
name: torch-distributed
198+
kind: ClusterTrainingRuntime # cluster-scoped, unlike the GKE example
199+
apiGroup: trainer.kubeflow.org
200+
trainer:
201+
numNodes: 2 # torch-distributed defaults to 1
202+
resourcesPerNode:
203+
limits:
204+
nvidia.com/gpu: 8
205+
vpc.amazonaws.com/efa: "<EFA_COUNT>" # per node — read it, see below
206+
requests:
207+
nvidia.com/gpu: 8
208+
vpc.amazonaws.com/efa: "<EFA_COUNT>"
209+
```
210+
211+
**`IPC_LOCK` and `FI_EFA_FORK_SAFE=1`.** `torch-distributed` grants neither —
212+
AICR's tested EFA runtime sets both. Add `FI_EFA_FORK_SAFE=1` alongside the
213+
other `FI_*` variables in `spec.trainer.env`, and `IPC_LOCK` via a
214+
`runtimePatches` entry setting `securityContext` on the `node` container
215+
(`securityContext` is patchable there even though `env` is not). Without
216+
`IPC_LOCK`, NCCL may fail to register pinned buffers.
217+
218+
**`NCCL_SOCKET_IFNAME=eth0`.** AICR's tested EFA and InfiniBand runtimes both pin
219+
this so NCCL's bootstrap uses the control interface. On a multi-NIC node — p5
220+
carries several EFA ENIs — NCCL may otherwise pick a secondary, non-routable NIC
221+
for rendezvous and hang during initialization, before any transport is chosen.
222+
223+
**An image carrying the EFA stack.** AICR installs the device plugin, which
224+
exposes the devices — it does not put `libfabric` or `aws-ofi-nccl` into your
225+
training image. An ordinary PyTorch image will fall back to sockets no matter
226+
what `LD_LIBRARY_PATH` says. Build from a base that includes them, and point
227+
`LD_LIBRARY_PATH` at wherever *that image* installs the plugin; AICR's tested
228+
image uses `/opt/amazon/ofi-nccl/lib/x86_64-linux-gnu`.
229+
230+
**The count, read from the nodes you will actually run on.** It varies by
231+
instance type — 4 on p4d, 32 on p5, and on g7e by size, with some sizes having
232+
none. Check every eligible node and fail if they disagree, rather than trusting
233+
the first one:
234+
235+
```shell
236+
kubectl get nodes -l <your-gpu-pool-selector> \
237+
-o custom-columns='NODE:.metadata.name,EFA:.status.allocatable.vpc\.amazonaws\.com/efa'
238+
```
239+
240+
## AKS InfiniBand and RDMA
241+
242+
Also TrainJob-expressible. AICR fixes the resource name and the value is always
243+
`1`:
244+
245+
```yaml
246+
resourcesPerNode:
247+
limits:
248+
nvidia.com/gpu: 8
249+
rdma/hca_shared_devices_a: 1
250+
requests:
251+
nvidia.com/gpu: 8
252+
rdma/hca_shared_devices_a: 1
253+
env:
254+
- name: NCCL_IB_DISABLE
255+
value: "0"
256+
- name: NCCL_NET_PLUGIN
257+
value: none
258+
```
259+
260+
The same repeat-every-resource caveat applies.
261+
262+
**`IPC_LOCK` is required here too, and allocating the RDMA device does not grant
263+
it.** NCCL's IB transport registers pinned (memlocked) buffers via ibverbs, and
264+
`IPC_LOCK` is what lifts `RLIMIT_MEMLOCK` — without it the job can fail *after*
265+
the device is allocated, which reads as an NCCL bug rather than a missing
266+
capability. Add it the same way as for EFA: a `runtimePatches` entry setting
267+
`securityContext` on the `node` container.
268+
269+
**An image carrying the IB verbs stack.** As with EFA, allocating the device is
270+
not enough: NCCL's IB transport dlopens `libibverbs` and the rest of rdma-core at
271+
runtime. An image without them logs `NET/IB : No device found` and falls back to
272+
sockets — the job still completes, just over TCP. Build from a base that carries
273+
rdma-core, or install it in the image.
274+
275+
**A memory-backed `/dev/shm` is also expected.** AICR's tested runtime mounts a
276+
`dshm` volume (`emptyDir: {medium: Memory}`) there; the default 64 MiB `/dev/shm`
277+
is small for multi-process NCCL.
278+
279+
See [AKS GPU Setup](../integrator/aks-gpu-setup.md) for the cluster-side
280+
prerequisites.
281+
282+
## Verifying the fabric is in use
283+
284+
Run a short job with `NCCL_DEBUG=INFO` and check which transport NCCL selected.
285+
Every runtime AICR ships sets `NCCL_DEBUG=WARN`, at which this line is
286+
suppressed — so grepping an ordinary run finds nothing, which is not evidence of
287+
socket fallback:
288+
289+
```shell
290+
kubectl logs <worker-pod> -c node | grep -i 'NCCL INFO.*Using network'
291+
```
292+
293+
Expect the plugin name — `FasTrak` for TCPXO, `AWS Libfabric` for EFA, `IB` for
294+
InfiniBand. **`Socket` means the fabric is not in use** and the job is running
295+
over TCP.
296+
297+
`NCCL_DEBUG=INFO` is verbose. Prefer a short dedicated run for the check rather
298+
than leaving it on for a full training job.
299+
300+
## Related
301+
302+
- [GKE TCPXO Networking Prerequisites](../integrator/gke-tcpxo-networking.md) — cluster-side setup and the full pod-level wiring
303+
- [AKS GPU Setup](../integrator/aks-gpu-setup.md) — RDMA prerequisites

0 commit comments

Comments
 (0)