Skip to content

Commit e119b54

Browse files
committed
feat(validator): data-driven NCCL benchmark runtime for --data recipes
Add the nccl-benchmark-runtime-ref performance constraint: a recipe ships a Kubeflow TrainingRuntime as a file in its --data tree (at the same validators/performance/testdata/{accelerator}/{service}/runtime.yaml layout the embedded templates use) and references it. This closes the gap the nccl-benchmark-profile escape hatch (#1719) left open — a private service+accelerator introduced only via --data, whose fabric matches no embedded template, can now be NCCL-performance-certified keyed on its own criteria with no compiled applicability entry. The orchestrator (resolveBenchmarkRuntimeRef, called from ValidatePhases/ ValidatePhase) reads the referenced file through the recipe DataProvider and lowers its content into an nccl-benchmark-runtime carrier constraint that rides to the pod via the existing ValidationInput ConfigMap. The in-pod validator renders it in place of a baked-in template, bypasses the compiled supportedNCCLCombinations gate, and skips all service-specific fabric plumbing (EFA/TCPXO/RDMA discovery, GB200-NVreg/GKE-TCPXO preflights, RoCE claim, NVLS/IMEX) — the supplied runtime owns its fabric. Trust-bounded to a TrainingRuntime GVR with forced name/namespace; fails closed on a malformed/traversal ref, missing/empty file, unresolved ref reaching the pod, or a non-TrainingRuntime carrier. Mutually exclusive with nccl-benchmark-profile. Fixes #1792 Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
1 parent 6548f17 commit e119b54

14 files changed

Lines changed: 1679 additions & 68 deletions

docs/contributor/validator.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,49 @@ already present in the compiled matrix, so the
433433
(advertised tuple ⇒ parseable template) covers every profile a recipe can
434434
name. See `validators/performance/nccl_benchmark_profile.go`.
435435

436+
#### NCCL: recipe-supplied runtime (`nccl-benchmark-runtime-ref`)
437+
438+
A profile still requires an embedded template to borrow, so it cannot cover a
439+
private service+accelerator whose fabric matches none of the shipped templates
440+
([#1792](https://github.com/NVIDIA/aicr/issues/1792)). The optional
441+
`nccl-benchmark-runtime-ref` performance constraint closes that gap: its value is
442+
a bare `{accelerator}/{service}` naming a Kubeflow `TrainingRuntime` the recipe
443+
ships in its `--data` tree at
444+
`validators/performance/testdata/{accelerator}/{service}/runtime.yaml` — the same
445+
layout the embedded templates use, so an external runtime is a drop-in for
446+
upstreaming.
447+
448+
Resolution is split across the two-stage design:
449+
450+
- **Orchestrator** (`pkg/validator/benchmark_runtime_ref.go`,
451+
`resolveBenchmarkRuntimeRef`, called at the top of `ValidatePhases` /
452+
`ValidatePhase`): reads the referenced file through the recipe `DataProvider`
453+
and lowers its content into the `nccl-benchmark-runtime` **carrier** constraint
454+
before the `/data/validation` ConfigMap is written. Fails closed
455+
(`ErrCodeInvalidRequest`) on a malformed/traversal ref, a missing/empty file,
456+
an absent `DataProvider`, or a ref set alongside an inline carrier. The two
457+
constraint names are defined once in `pkg/validator/v1` (`PerfConstraintNCCLBenchmarkRuntime*`)
458+
so the write side and the pod read side cannot drift.
459+
- **Pod** (`validators/performance/nccl_benchmark_runtime.go`): reads the
460+
`nccl-benchmark-runtime` carrier and renders it — via the same `${VAR}`
461+
substitution as the baked-in templates, through the shared `renderYAMLTemplate`
462+
core — in place of `testdata/{accelerator}/{service}`.
463+
`validateNcclAllReduceBw` bypasses the `ncclCombinationSupported` applicability
464+
gate entirely (applicability is granted by the recipe's explicit opt-in, keyed
465+
on its own criteria) and `applyNCCLResources` skips every service-specific step
466+
— EFA/TCPXO/RDMA discovery, the GB200-NVreg / GKE-TCPXO preflights, RoCE claim
467+
application, and NVLS/IMEX provisioning — because the supplied runtime owns its
468+
fabric wiring. Transport verification still runs (the `-net` / `-nvls` markers
469+
are transport-internal and fabric-agnostic), so a runtime paired with a named
470+
variant must still prove its transport rather than pass on bandwidth alone; the
471+
worker cohort is also sized against the runtime's own `nodeSelector` so
472+
`WorkerCount` matches placement. It fails
473+
closed on a carrier that is not a `trainer.kubeflow.org/v1alpha1`
474+
`TrainingRuntime` with a `node` replicatedJob. The runtime is applied only
475+
through `trainingRuntimeGVR` with a force-set name/namespace, so a recipe can
476+
supply a `TrainingRuntime` and nothing else — not an arbitrary resource kind.
477+
It is mutually exclusive with `nccl-benchmark-profile`.
478+
436479
#### `inference-perf`: model, concurrency, and weights cache
437480

438481
The `inference-perf` check warms vLLM before measuring, so the one-time

docs/integrator/data-extension.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ to opt into one of the embedded benchmarks. The profile selects the benchmark
130130
template and fabric handling; node identification still follows the recipe's
131131
own accelerator. See
132132
[Opting external recipes into a benchmark profile](../user/validation.md#opting-external-recipes-into-a-benchmark-profile).
133+
If the private service's fabric matches none of the embedded templates, ship the
134+
benchmark itself: put a Kubeflow `TrainingRuntime` in your `--data` tree at
135+
`validators/performance/testdata/{accelerator}/{service}/runtime.yaml` and point
136+
the `nccl-benchmark-runtime-ref` constraint at it (`{accelerator}/{service}`).
137+
Pass the same `--data <dir>` to `aicr validate` so it can resolve the referenced
138+
file; validation then gates it keyed on the recipe's own criteria with no
139+
compiled entry required, and the file is a drop-in should the runtime ever be
140+
upstreamed.
141+
See [Supplying a benchmark runtime for a private service](../user/validation.md#supplying-a-benchmark-runtime-for-a-private-service).
133142

134143
## Adding a component
135144

docs/integrator/recipe-development.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,22 @@ discovery, and preflights. See
290290
[Opting external recipes into a benchmark profile](../user/validation.md#opting-external-recipes-into-a-benchmark-profile)
291291
for the valid pairs and skip/fail semantics.
292292

293+
When no embedded pair matches — a genuinely private service+accelerator with a
294+
fabric none of the shipped templates cover — supply the benchmark yourself:
295+
ship a Kubeflow `TrainingRuntime` in your `--data` tree at
296+
`validators/performance/testdata/{accelerator}/{service}/runtime.yaml` and
297+
reference it with the `nccl-benchmark-runtime-ref` constraint (a bare
298+
`{accelerator}/{service}` value). Run `aicr validate --data <dir> ...` so the
299+
referenced file is resolvable; it is read and rendered in place of a baked-in
300+
template, keyed on the recipe's own criteria with no compiled applicability
301+
entry. The runtime owns its fabric wiring (the validator skips service-specific
302+
fabric setup — discovery, preflights, and NVLS/IMEX provisioning — but still
303+
asserts transport for the `-net`/`-nvls` variants), must
304+
declare a `node` replicatedJob, and is mutually exclusive with
305+
`nccl-benchmark-profile`. Laying the file at the embedded testdata path makes it
306+
a drop-in for upstreaming. See
307+
[Supplying a benchmark runtime for a private service](../user/validation.md#supplying-a-benchmark-runtime-for-a-private-service).
308+
293309
### Component Types
294310

295311
**Helm components** (most common):

docs/user/validation.md

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ ones) that match the target fabric:
5252
| `nccl-all-reduce-bw-nvls` | NVLS (MNNVL across an NVL72 IMEX domain) | GB200 + EKS, and GB200 + OKE. Asserts the NVLS communicator actually initialized — catches silent fallback to EFA (EKS) or Socket (OKE) when the IMEX domain is misconfigured. |
5353

5454
The applicability column is the *default*, derived from the recipe's
55-
`criteria`. A recipe whose criteria fall outside it can opt into one of these
56-
benchmarks explicitly — see
57-
[Opting external recipes into a benchmark profile](#opting-external-recipes-into-a-benchmark-profile).
55+
`criteria`. A recipe whose criteria fall outside it can still run these
56+
benchmarks explicitly — either by
57+
[borrowing an embedded benchmark profile](#opting-external-recipes-into-a-benchmark-profile)
58+
whose fabric matches its hardware, or, for a private service whose fabric
59+
matches no embedded template, by
60+
[supplying its own benchmark runtime](#supplying-a-benchmark-runtime-for-a-private-service).
5861

5962
The `-net` check defaults to the AWS EFA fabric. On a ConnectX **RoCE** cluster
6063
(e.g. DGXC GB300 `p6e-gb300r`), set `AICR_NCCL_FABRIC=roce` in the `aicr
@@ -180,6 +183,124 @@ check with a message naming the profile. When a profile is set it wins over
180183
criteria-derived applicability, and the pass/fail thresholds stay in the
181184
same-named check constraints as always.
182185

186+
### Supplying a benchmark runtime for a private service
187+
188+
A profile only helps when an **embedded** template already matches the
189+
cluster's hardware and fabric. A private service introduced entirely through
190+
`--data` — a `criteria.service` (and possibly `criteria.accelerator`) the
191+
validator ships no template for, with a fabric that reuses none of the
192+
embedded ones — has no compiled pair to point a profile at. Such a recipe
193+
supplies the benchmark itself: it ships a Kubeflow `TrainingRuntime` as a file
194+
in its `--data` tree and references it with the `nccl-benchmark-runtime-ref`
195+
performance constraint (a bare `{accelerator}/{service}` value). `aicr validate`
196+
reads that file and gates the benchmark keyed on the recipe's **own** criteria,
197+
with no compiled applicability entry required.
198+
199+
Lay the runtime out **exactly where the embedded templates live**, so it is a
200+
drop-in for upstreaming later:
201+
202+
```text
203+
mydata/ # your --data directory
204+
├── overlays/
205+
│ └── mycloud-training.yaml # the recipe overlay (below)
206+
└── validators/performance/testdata/
207+
└── gb200/mycloud/runtime.yaml # the TrainingRuntime, real file
208+
```
209+
210+
```yaml
211+
# overlays/mycloud-training.yaml
212+
validation:
213+
performance:
214+
checks:
215+
- nccl-all-reduce-bw
216+
constraints:
217+
- name: nccl-all-reduce-bw
218+
value: ">= 450"
219+
- name: nccl-benchmark-runtime-ref
220+
value: gb200/mycloud # -> validators/performance/testdata/gb200/mycloud/runtime.yaml
221+
```
222+
223+
```yaml
224+
# validators/performance/testdata/gb200/mycloud/runtime.yaml
225+
apiVersion: trainer.kubeflow.org/v1alpha1
226+
kind: TrainingRuntime
227+
metadata:
228+
name: nccl-all-reduce-runtime
229+
spec:
230+
template:
231+
spec:
232+
replicatedJobs:
233+
- name: node # required: the worker cohort
234+
template:
235+
spec:
236+
template:
237+
spec:
238+
containers:
239+
- name: node
240+
image: <your NCCL benchmark image>
241+
resources:
242+
limits:
243+
nvidia.com/gpu: "${GPU_COUNT_PER_NODE}"
244+
# ...command, fabric resources, sidecars...
245+
```
246+
247+
```bash
248+
aicr validate -r recipe.yaml -s snapshot.yaml \
249+
--data ./mydata --phase performance
250+
```
251+
252+
The supplied runtime **owns its fabric wiring end to end**: because the recipe
253+
opted in explicitly, the validator bypasses the compiled applicability gate and
254+
skips every service-specific *setup* step — EFA/TCPXO/RDMA NIC discovery, the
255+
GB200-NVreg / GKE-TCPXO preflights, and NVLS/IMEX auto-provisioning. It renders
256+
the runtime, sizes it (against the runtime's own `nodeSelector` when it pins
257+
one, so `WorkerCount` matches placement), applies it alongside the shared
258+
`TrainJob`, and evaluates the bandwidth threshold from the launcher logs.
259+
Transport verification is **not** skipped: pairing the runtime with the `-net`
260+
or `-nvls` check still asserts that transport actually carried traffic (the NCCL
261+
markers are fabric-agnostic), so a named variant can't pass on bandwidth alone.
262+
What it must honor:
263+
264+
- It must be a Kubeflow `TrainingRuntime` (`apiVersion:
265+
trainer.kubeflow.org/v1alpha1`); any other kind/apiVersion is rejected. The
266+
validator force-sets its name and namespace, so a recipe can supply a
267+
`TrainingRuntime` and nothing else — never an arbitrary resource.
268+
- It must declare a `node` replicatedJob (the worker cohort the shared
269+
`TrainJob` sizes and the validator injects scheduling into and reads logs
270+
from), mirroring the embedded templates.
271+
- The generic template variables `${NAMESPACE}`, `${WORKER_COUNT}`,
272+
`${GPU_COUNT_PER_NODE}`, `${GPU_COUNT}`, `${TEST_TYPE}`,
273+
`${MIN_MESSAGE_SIZE}`, and `${MAX_MESSAGE_SIZE}` are substituted; the
274+
service-specific ones (EFA/RDMA/GKE) are not, so the runtime must pin its own
275+
fabric resources.
276+
- The runtime should pin its own worker `nodeSelector`/`tolerations`, or pass
277+
`--node-selector` to `aicr validate`, so workers land on the intended GPU
278+
nodes.
279+
280+
`nccl-benchmark-runtime-ref` and `nccl-benchmark-profile` are mutually
281+
exclusive — a recipe supplies its own runtime **or** borrows an embedded one,
282+
never both. An absent or blank ref falls back to criteria/profile-derived
283+
applicability. A malformed ref, or a file missing from `--data`, is rejected by
284+
`aicr validate` **before any validator Job is deployed** (a resolution error on
285+
stderr). A resolved file that is not a `TrainingRuntime` with a `node`
286+
replicatedJob **fails the check itself** (in the pod). Either way it fails
287+
rather than silently skipping.
288+
289+
> **Sizing note.** The worker cohort is sized against the runtime's own
290+
> `nodeSelector` when it pins one, but richer scheduling constraints
291+
> (`nodeAffinity`, un-tolerated node taints) are the Kubernetes scheduler's
292+
> job, not the validator's — a runtime that restricts placement below the sized
293+
> cohort surfaces as a launcher timeout, not a false pass. Pass an explicit
294+
> `--node-selector` when you want the sized and scheduled cohorts to match
295+
> exactly.
296+
297+
**Upstreaming.** Because the file already sits at
298+
`validators/performance/testdata/{accelerator}/{service}/runtime.yaml`, adopting
299+
it into AICR is a copy: move the file into the repo, add the accelerator/service
300+
tuple to the compiled `supportedNCCLCombinations` matrix, and drop the
301+
`nccl-benchmark-runtime-ref` constraint — the benchmark then runs from the
302+
recipe's plain criteria like any other embedded one.
303+
183304
## Inference performance validation
184305

185306
Inference performance runs the `inference-perf` check — deploys a
@@ -776,8 +897,11 @@ Common reasons and their cause:
776897
| `no inference-throughput or inference-ttft-p99 constraint in recipe` | `stdout` | Check was invoked but recipe is missing the matching constraints | Re-generate the recipe or add the constraints |
777898
| `dynamo-platform not in recipe components` | `stdout` | Inference check selected but `dynamo-platform` absent from `componentRefs` | Use `--platform dynamo` when generating the recipe |
778899
| `DynamoGraphDeployment CRD not installed` | `stdout` | Recipe declares `dynamo-platform` but the operator is not deployed | Run `aicr bundle` + `./deploy.sh` first, or wait for bootstrap to complete |
779-
| `requires Service + Accelerator to be implemented` | `stdout` | The recipe's `criteria` are outside the NCCL benchmark's default applicability (e.g. a service registered via `--data`) | Add an `nccl-benchmark-profile` constraint — see [Opting external recipes into a benchmark profile](#opting-external-recipes-into-a-benchmark-profile) |
900+
| `requires Service + Accelerator to be implemented` | `stdout` | The recipe's `criteria` are outside the NCCL benchmark's default applicability (e.g. a service registered via `--data`) | Add an `nccl-benchmark-profile` constraint — see [Opting external recipes into a benchmark profile](#opting-external-recipes-into-a-benchmark-profile) — or, if no embedded template fits, an `nccl-benchmark-runtime-ref` constraint — see [Supplying a benchmark runtime for a private service](#supplying-a-benchmark-runtime-for-a-private-service) |
780901
| `benchmark profile … does not implement the … NCCL variant` | `stdout` | The declared `nccl-benchmark-profile` is valid but has no template for this check variant | Drop the non-applicable check from `validation.performance.checks`, or pick a profile that implements it |
902+
| `failed to resolve nccl-benchmark-runtime-ref=… expected … in the --data tree` | `stderr` | The referenced runtime file is missing from `--data` (or `--data` wasn't passed) | Place the `TrainingRuntime` at `validators/performance/testdata/{accelerator}/{service}/runtime.yaml` in your `--data` dir — see [Supplying a benchmark runtime for a private service](#supplying-a-benchmark-runtime-for-a-private-service) |
903+
| `nccl-benchmark-runtime … must be a … TrainingRuntime … must declare a "node" replicatedJob` | `stdout` | The referenced file is not a Kubeflow `trainer.kubeflow.org/v1alpha1` `TrainingRuntime`, or lacks the `node` replicatedJob | Fix the runtime file to be a valid `TrainingRuntime` with a `node` replicatedJob |
904+
| `nccl-benchmark-runtime and nccl-benchmark-profile are mutually exclusive` | `stdout` | The recipe declares both escape hatches at once | Keep only one: borrow an embedded profile **or** supply your own runtime |
781905
| `skipped - no-cluster mode` | `message` | `--no-cluster` was passed — the runner short-circuits every phase before dispatching any Job | Remove the flag to run behavioral checks |
782906
| `skipped due to previous phase failure` | `message` | `--fail-fast` was set and an earlier phase failed, so subsequent phases were skipped | Fix the earlier phase first, or drop `--fail-fast` to run all phases regardless |
783907

0 commit comments

Comments
 (0)