Skip to content

Commit 78b97cd

Browse files
authored
feat(validation): detect missing GKE GPU NIC networks at deployment phase (NVIDIA#2248)
Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
1 parent def13e0 commit 78b97cd

11 files changed

Lines changed: 753 additions & 41 deletions

File tree

docs/integrator/gke-tcpxo-networking.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,83 @@ GKE clusters must have multi-NIC networking configured before deploying AICR bun
1313
- `nccl-tcpxo-installer` DaemonSet on GPU nodes (included in AICR bundle)
1414
- `nri-device-injector` DaemonSet on GPU nodes (included in AICR bundle)
1515

16+
The last two ship in the AICR bundle. The first two are **cluster provisioning**
17+
— AICR detects them but does not create them.
18+
19+
### Provisioning multi-NIC networking
20+
21+
These four steps are ordered. Step 2 is the one that cannot be undone later;
22+
steps 3 and 4 must both complete before any TCPXO workload — or `aicr validate`
23+
will work.
24+
25+
1. **Create the VPCs and subnets** — one dedicated VPC + subnet per GPU NIC,
26+
eight in total, in the cluster's region.
27+
2. **Create the cluster** with `--enable-multi-networking`, plus its two
28+
prerequisites `--enable-dataplane-v2` and `--enable-ip-alias`.
29+
3. **Create the GPU node pool** on an `a3-megagpu-8g` machine type with
30+
`--enable-gvnic`, attaching the eight VPC/subnet pairs as repeated
31+
`--additional-node-network` entries, one per pair, each in the form
32+
`network=NETWORK,subnetwork=SUBNET`.
33+
4. **Apply the `Network` and `GKENetworkParamSet` CRs** — one pair per GPU NIC,
34+
binding each additional node network into the cluster so pods can reference
35+
it by name. **Each `Network` name must contain `gpu-nic`** — for example
36+
`gpu-nic-0` through `gpu-nic-7`, optionally with a cluster prefix such as
37+
`aicr-demo2-gpu-nic-0`. The paired `GKENetworkParamSet` is referenced by the
38+
`Network` through `spec.parametersRef`, so its own name is unconstrained.
39+
40+
> **The `gpu-nic` naming is a requirement, not a convention.** AICR discovers
41+
> these networks by matching `gpu-nic` in the `Network` object's
42+
> `metadata.name` — both the `gke-gpu-nic-networks` deployment check and the
43+
> NCCL benchmark's own interface mapping. Google's sample manifests name the
44+
> Device networks `vpc1``vpc8`; applied verbatim those are invisible to AICR,
45+
> and the deployment check reports 0 of 8 on a cluster that is otherwise
46+
> correctly provisioned. Rename them when following that procedure.
47+
>
48+
> Beyond containing `gpu-nic`, the exact names are yours to choose — but the
49+
> workload annotation below must reference the names your cluster actually has.
50+
> The example there uses `gpu-nic0``gpu-nic7`; if you provisioned
51+
> `gpu-nic-0``gpu-nic-7`, use those instead.
52+
53+
> **Multi-networking cannot be enabled after cluster creation.** `--enable-multi-networking`
54+
> is a create-time flag; there is no `gcloud container clusters update` equivalent,
55+
> so a cluster created without it must be **recreated**. Steps 3 and 4, by
56+
> contrast, can be done on an existing multi-networking cluster — a node pool can
57+
> be added later, and the CRs can be applied at any point.
58+
59+
AICR installs the TCPXO DaemonSets and detects the CRs; it does not provision any
60+
of this networking. These steps are a summary of the prerequisite AICR depends
61+
on, not a complete provisioning runbook — for the full procedure, including the
62+
per-VPC firewall rules and the supported GKE version floors, follow Google's
63+
[GPUDirect and multi-networking guide](https://cloud.google.com/kubernetes-engine/docs/how-to/gpu-bandwidth-gpudirect-tcpx).
64+
65+
Completing steps 1–3 without step 4 is the failure mode worth knowing: the VMs
66+
come up with all nine NICs attached (the node's primary interface plus the eight
67+
GPU NICs) and the AICR TCPXO DaemonSets roll out cleanly, but with no `Network`
68+
objects bound into the cluster no pod can reference a GPU NIC and TCPXO cannot
69+
function.
70+
71+
### Verifying
72+
73+
```shell
74+
kubectl get network.networking.gke.io
75+
```
76+
77+
Expect eight GPU NIC entries (plus the `default` network). Match on the
78+
`gpu-nic` substring rather than an exact name: the rest of each name is chosen
79+
at provisioning time and may carry a local prefix, such as
80+
`aicr-demo2-gpu-nic-0`.
81+
82+
Fewer than eight means the prerequisite is incomplete. AICR's
83+
`gke-gpu-nic-networks` deployment check asserts this same count, so
84+
`aicr validate --phase deployment` reports the shortfall by name rather than
85+
letting it surface later as a performance-phase abort with no bandwidth number.
86+
1687
**Important:** The GPU node pool must be provisioned with only the 8 GPU NIC
1788
networks (`gpu-nic-0` through `gpu-nic-7`). Do **not** include a gVNIC additional
1889
network — it takes a GPU NIC PCI slot (`0000:06:00.0`), leaving only 7/8 GPUs
19-
available for TCPXO.
90+
available for TCPXO. This is distinct from the `--enable-gvnic` node-pool flag,
91+
which selects the gVNIC driver and **is** required: pass the flag, but do not add
92+
a ninth `--additional-node-network` entry for it.
2093

2194
## Workload Pod Configuration (NRI Profile)
2295

recipes/overlays/h100-gke-cos-training.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ spec:
8181
- expected-resources
8282
- gpu-operator-version
8383
- check-nvidia-smi
84+
# Fails closed when the cluster lacks the Network / GKENetworkParamSet
85+
# objects GPUDirect TCPXO needs. The gke-nccl-tcpxo DaemonSets roll out
86+
# cleanly without them, so without this the gap only surfaces as a
87+
# performance-phase abort with no bandwidth number produced (#2216).
88+
- gke-gpu-nic-networks
8489
constraints:
8590
- name: Deployment.gpu-operator.version
8691
value: ">= v24.6.0"

recipes/validators/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Applied by `catalog.Load` (`pkg/validator/catalog/catalog.go`) in order:
4141
| `expected-resources` | Verify expected Kubernetes resources exist and are healthy (runs ExpectedResources + Chainsaw assert paths side-by-side) | 8m |
4242
| `gpu-operator-version` | Validate GPU Operator version against recipe constraints | 2m |
4343
| `check-nvidia-smi` | Verify nvidia-smi works on all schedulable GPU nodes (cordoned nodes are disclosed, not silently skipped) | 10m |
44+
| `gke-gpu-nic-networks` | Verify the GKE cluster has the GPU NIC networks GPUDirect TCPXO requires (skipped unless the recipe declares `gke-nccl-tcpxo`) | 2m |
4445

4546
### Performance Phase
4647

recipes/validators/catalog.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ validators:
6565
timeout: 10m
6666
args: ["check-nvidia-smi"]
6767
env: []
68+
- name: gke-gpu-nic-networks
69+
phase: deployment
70+
description: "Verify the GKE cluster has the GPU NIC networks GPUDirect TCPXO requires (skipped unless the recipe declares gke-nccl-tcpxo)"
71+
image: ghcr.io/nvidia/aicr-validators/deployment:latest
72+
timeout: 2m
73+
args: ["gke-gpu-nic-networks"]
74+
env: []
6875
# Performance phase
6976
#
7077
# The NCCL checks (nccl-all-reduce-bw and the -net/-nvls variants at the
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"log/slog"
20+
21+
"github.com/NVIDIA/aicr/pkg/errors"
22+
"github.com/NVIDIA/aicr/validators"
23+
"github.com/NVIDIA/aicr/validators/internal/gkenet"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
)
26+
27+
// tcpxoComponent is the recipe componentRef that supplies GPUDirect TCPXO.
28+
const tcpxoComponent = "gke-nccl-tcpxo"
29+
30+
// checkGKEGPUNICNetworks verifies the cluster has the GKE multi-NIC networking
31+
// objects GPUDirect TCPXO depends on.
32+
//
33+
// The gke-nccl-tcpxo component ships two DaemonSets, and both roll out cleanly
34+
// on a cluster that has zero Network / GKENetworkParamSet objects — so the
35+
// component's health check reports Synced+Healthy while TCPXO cannot function.
36+
// Without this check the gap surfaces hours later as a performance-phase abort
37+
// in the NCCL benchmark's own discovery, with no bandwidth number produced.
38+
//
39+
// The Network CRs are infrastructure: creating and binding them belongs to
40+
// cluster provisioning, not AICR. This check only detects their absence and
41+
// names the prerequisite, at the deployment phase where it is actionable.
42+
func checkGKEGPUNICNetworks(ctx *validators.Context) error {
43+
if ctx.DynamicClient == nil {
44+
return errors.New(errors.ErrCodeInvalidRequest, "dynamic client is not available")
45+
}
46+
47+
slog.Info("listing GKE networks", "gvr", gkenet.NetworkGVR.String())
48+
49+
gpuNICs, listErr := gkenet.DiscoverGPUNICNetworks(ctx.Ctx, ctx.DynamicClient)
50+
51+
capability := validators.Capability{
52+
Component: tcpxoComponent,
53+
Subject: "GKE Networks (networks.networking.gke.io)",
54+
AbsentMsg: absentPrerequisiteMsg("the cluster does not serve the networks.networking.gke.io " +
55+
"API at all, so it has 0"),
56+
InapplicableMsg: tcpxoComponent + " not declared in recipe and the cluster has no GKE Network " +
57+
"API — cluster does not use GPUDirect TCPXO",
58+
}
59+
60+
// An ABSENT Network API is clean absence, not an infrastructure failure: the
61+
// CRD arrives with --enable-multi-networking, so a cluster created without it
62+
// legitimately does not serve this GVR. Route that shape through Require,
63+
// which is declaration-gated — an undeclared recipe skips, a declared one gets
64+
// the actionable message. RequireList would classify it as a blocking INTERNAL
65+
// error, which both false-fails an undeclared recipe and hides the missing
66+
// prerequisite behind "failed to read" on a declared one.
67+
if apierrors.IsNotFound(listErr) {
68+
// present is unused here: Require consults it only when probeErr is nil.
69+
return capability.Require(ctx, listErr, false)
70+
}
71+
72+
// Every other list error blocks regardless of declaration — an RBAC denial or
73+
// an apiserver hiccup is not evidence that TCPXO is inapplicable.
74+
if err := capability.RequireList(listErr); err != nil {
75+
return err
76+
}
77+
78+
// The prerequisite belongs to gke-nccl-tcpxo: a recipe that does not declare
79+
// the component is not asking for TCPXO, so its cluster's networking is not
80+
// this check's business. This also covers the #1327 standalone-run boundary,
81+
// where there is no recipe context at all.
82+
if !validators.RecipeDeclares(ctx, tcpxoComponent) {
83+
return validators.Skip(
84+
tcpxoComponent + " not declared in recipe — GPUDirect TCPXO networking is inapplicable")
85+
}
86+
87+
// Evidence to stdout.
88+
fmt.Printf("Found %d GPU NIC network(s) (need %d):\n", len(gpuNICs), gkenet.RequiredGPUNICNetworks)
89+
for _, name := range gpuNICs {
90+
fmt.Printf(" %s\n", name)
91+
}
92+
93+
if len(gpuNICs) < gkenet.RequiredGPUNICNetworks {
94+
return errors.New(errors.ErrCodeNotFound, absentPrerequisiteMsg(fmt.Sprintf(
95+
"the cluster has %d of %d", len(gpuNICs), gkenet.RequiredGPUNICNetworks)))
96+
}
97+
98+
return nil
99+
}
100+
101+
// absentPrerequisiteMsg builds the operator-facing message for a missing GPU NIC
102+
// networking prerequisite. detail names what was actually observed; the rest is
103+
// the constant remediation, kept in one place so the absent-API path and the
104+
// short-count path cannot drift.
105+
//
106+
// The message names the required naming convention because it is a real way to
107+
// hit a zero count on an otherwise correctly provisioned cluster: discovery
108+
// matches the substring against the NETWORK name only, and Google's own sample
109+
// manifests name the Device networks vpc1..vpc8.
110+
func absentPrerequisiteMsg(detail string) string {
111+
return fmt.Sprintf(
112+
"recipe declares %s but %s GPU NIC networks — GPUDirect TCPXO requires one Network "+
113+
"per GPU NIC, each bound to a GKENetworkParamSet and each with %q in its own "+
114+
"metadata.name (this check counts Network names; it does not verify the "+
115+
"GKENetworkParamSet binding or readiness). "+
116+
"These are provisioned with the cluster, not by AICR, and multi-networking "+
117+
"(--enable-multi-networking) cannot be enabled after cluster creation. "+
118+
"Verify with: kubectl get network.networking.gke.io "+
119+
"(see docs/integrator/gke-tcpxo-networking.md)",
120+
tcpxoComponent, detail, gkenet.GPUNICNameSubstring)
121+
}

0 commit comments

Comments
 (0)