Skip to content

Commit d550567

Browse files
authored
Merge branch 'main' into feat/runtime-inventory-selection
2 parents 7d3bf22 + db8dcb9 commit d550567

6 files changed

Lines changed: 104 additions & 35 deletions

File tree

docs/user/cli-reference.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -965,8 +965,8 @@ aicr validate [flags]
965965
| `--image-pull-secret` | | string[] | | Image pull secrets for private registries (repeatable) |
966966
| `--job-name` | | string | aicr-validate | Name for the validation Job |
967967
| `--service-account-name` | | string | aicr | ServiceAccount name for validation Job |
968-
| `--node-selector` | | string[] | | Override GPU node selection for validation workloads. Replaces platform-specific selectors (e.g., `cloud.google.com/gke-accelerator`, `node.kubernetes.io/instance-type`) on inner workloads like NCCL benchmark pods. Use when GPU nodes have non-standard labels. Does not affect the validator orchestrator Job. (format: key=value, repeatable) |
969-
| `--toleration` | | string[] | | Override tolerations for validation workloads. Replaces the default tolerate-all policy on inner workloads like NCCL benchmark pods and conformance test pods. Does not affect the validator orchestrator Job. (format: key=value:effect, repeatable) |
968+
| `--node-selector` | | string[] | | Override GPU node selection for the live snapshot agent (when `--snapshot` is omitted) and inner validation workloads. Replaces platform-specific selectors (e.g., `cloud.google.com/gke-accelerator`, `node.kubernetes.io/instance-type`) on inner workloads like NCCL benchmark pods. Use when GPU nodes have non-standard labels. Does not affect the validator orchestrator Job. (format: key=value, repeatable) |
969+
| `--toleration` | | string[] | | Override tolerations for the live snapshot agent (when `--snapshot` is omitted) and inner validation workloads. When omitted, the snapshot agent tolerates all taints. Does not affect the validator orchestrator Job. (format: key=value:effect, repeatable) |
970970
| `--timeout` | | duration | 5m | Timeout for validation Job completion |
971971
| `--no-cleanup` | | bool | false | Skip removal of Job and RBAC resources on completion |
972972
| `--require-gpu` | | bool | false | Require GPU resources on the validation pod |
@@ -1174,7 +1174,7 @@ spec:
11741174
serviceAccountName: aicr
11751175
nodeSelector:
11761176
my-org/gpu-pool: "true"
1177-
tolerations:
1177+
tolerations: # [] clears the live snapshot agent's tolerate-all default
11781178
- "gpu-type=h100:NoSchedule"
11791179
requireGpu: true
11801180
execution:
@@ -1211,7 +1211,7 @@ aicr validate --config validate-cluster-a.yaml
12111211
aicr validate --config validate-cluster-b.yaml
12121212
```
12131213

1214-
The `--node-selector` and `--toleration` flags control scheduling for the inner validation workloads (NCCL benchmark workers, conformance test pods), not the validator orchestrator Job. For when to use them with non-standard GPU labels or taints, see [Validation](validation.md#non-standard-gpu-labels-or-taints).
1214+
The `--node-selector` and `--toleration` flags control scheduling for the inner validation workloads (NCCL benchmark workers, conformance test pods). When `--snapshot` is omitted, they also configure the preliminary live snapshot agent. They do not configure the validator orchestrator Job. For when to use them with non-standard GPU labels or taints, see [Validation](validation.md#non-standard-gpu-labels-or-taints).
12151215

12161216
**Output Structure ([CTRF](https://ctrf.io/) JSON):**
12171217

docs/user/validation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,9 @@ aicr validate \
910910
--toleration dedicated=worker-workload:NoExecute
911911
```
912912

913-
These flags affect the inner benchmark pods that run on GPU nodes (NCCL workers, Dynamo workers), not the validator orchestrator Job itself. For `inference-perf` specifically, `--node-selector` narrows the pool of candidate GPU nodes — the validator then picks the candidate with the most free GPUs (subtracting same-ledger occupancy only — DRA allocations from DRA capacity, device-plugin requests from device-plugin capacity — and skipping DRA candidates that carry scalar `nvidia.com/gpu` workloads) and pins all Dynamo Frontend + worker pods to that node via `kubernetes.io/hostname`. The AIPerf benchmark runner pod is CPU-only, uses a tolerate-all / no-nodeSelector pod spec, and is unaffected by these flags.
913+
These flags affect the inner benchmark pods that run on GPU nodes (NCCL workers, Dynamo workers). The example above supplies `--snapshot`, so it does not launch the live snapshot agent. When `--snapshot` is omitted, the flags also configure that preliminary agent. With no toleration override, the agent tolerates all taints; an explicit `spec.validate.agent.tolerations: []` clears that default. Neither flag affects the validator orchestrator Job itself.
914+
915+
For `inference-perf` specifically, `--node-selector` narrows the pool of candidate GPU nodes — the validator then picks the candidate with the most free GPUs (subtracting same-ledger occupancy only — DRA allocations from DRA capacity, device-plugin requests from device-plugin capacity — and skipping DRA candidates that carry scalar `nvidia.com/gpu` workloads) and pins all Dynamo Frontend + worker pods to that node via `kubernetes.io/hostname`. The AIPerf benchmark runner pod is CPU-only, uses a tolerate-all / no-nodeSelector pod spec, and is unaffected by these flags.
914916

915917
### A check reports `skipped` unexpectedly
916918

pkg/cli/validate.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ func resolveValidateNodeSelector(cmd *cli.Command, resolved *config.ValidateReso
135135
// inference-perf that want to mirror the target node's taints by default
136136
// must distinguish "operator opted into tolerate-all" from "operator said
137137
// nothing". Returning nil here when neither CLI nor config set the field
138-
// keeps the env var unset, so the inner validator context sees nil.
138+
// keeps the env var unset, so the inner validator context sees nil. The live
139+
// snapshot path consumes that same nil as its signal to apply the agent's
140+
// tolerate-all default at the Job projection boundary.
139141
func resolveValidateTolerations(cmd *cli.Command, resolved *config.ValidateResolved) ([]corev1.Toleration, error) {
140142
if cmd.IsSet("toleration") {
141143
tols, err := snapshotter.ParseTolerations(cmd.StringSlice("toleration"))
@@ -465,12 +467,12 @@ func validateCmdFlags() []cli.Flag {
465467
},
466468
&cli.StringSliceFlag{
467469
Name: "node-selector",
468-
Usage: "Override GPU node selection for validation workloads (format: key=value, can be repeated). Replaces platform-specific selectors on inner workloads (e.g., NCCL benchmark pods). Use when GPU nodes have non-standard labels. Does not affect the validator orchestrator Job.",
470+
Usage: "Override GPU node selection for the live snapshot agent (when --snapshot is omitted) and inner validation workloads (format: key=value, can be repeated). Replaces platform-specific selectors on inner workloads (e.g., NCCL benchmark pods). Does not affect the validator orchestrator Job.",
469471
Category: catScheduling,
470472
},
471473
&cli.StringSliceFlag{
472474
Name: "toleration",
473-
Usage: "Override tolerations for validation workloads (format: key=value:effect, can be repeated). Replaces the default tolerate-all policy on inner workloads. Does not affect the validator orchestrator Job.",
475+
Usage: "Override tolerations for the live snapshot agent (when --snapshot is omitted) and inner validation workloads (format: key=value:effect, can be repeated). When omitted, the snapshot agent tolerates all taints. Does not affect the validator orchestrator Job.",
474476
Category: catScheduling,
475477
},
476478
&cli.DurationFlag{

pkg/client/v1/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ func (s *Snapshot) Unwrap() *snapshotter.Snapshot {
113113
// collection Job passed to Client.CollectSnapshot. Facade-owned;
114114
// field-for-field mirror of pkg/snapshotter.AgentConfig. Tolerations
115115
// keep k8s.io/api/core/v1.Toleration since kubernetes/api is itself
116-
// stable.
116+
// stable. Nil Tolerations use a tolerate-all default; a non-nil empty
117+
// slice explicitly disables that default.
117118
//
118119
// The mirror is enforced, not conventional: TestAgentConfigMirrorsInternal
119120
// fails when either struct gains, drops, or retypes a field, and every

pkg/snapshotter/agent.go

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ type AgentConfig struct {
6868
// NodeSelector for targeting specific nodes
6969
NodeSelector map[string]string
7070

71-
// Tolerations for scheduling on tainted nodes
71+
// Tolerations for scheduling on tainted nodes. Nil uses
72+
// DefaultTolerations; a non-nil empty slice explicitly disables that default.
7273
Tolerations []corev1.Toleration
7374

7475
// Timeout for waiting for Job completion
@@ -147,6 +148,32 @@ type AgentConfig struct {
147148
Limits corev1.ResourceList
148149
}
149150

151+
// buildAgentConfig projects snapshotter configuration onto the deployer's
152+
// Job configuration. Keep scheduling defaults at this projection boundary so
153+
// every snapshot-agent caller gets the same nil-versus-empty behavior.
154+
func buildAgentConfig(config *AgentConfig, agentOutput string) agent.Config {
155+
return agent.Config{
156+
Namespace: config.Namespace,
157+
ServiceAccountName: config.ServiceAccountName,
158+
JobName: config.JobName,
159+
Image: config.Image,
160+
ImagePullSecrets: config.ImagePullSecrets,
161+
NodeSelector: config.NodeSelector,
162+
Tolerations: effectiveAgentTolerations(config.Tolerations),
163+
Output: agentOutput,
164+
Debug: config.Debug,
165+
Privileged: config.Privileged,
166+
RequireGPU: config.RequireGPU,
167+
RuntimeClassName: config.RuntimeClassName,
168+
MaxNodesPerEntry: config.MaxNodesPerEntry,
169+
OS: config.OS,
170+
ClusterConfigPath: config.ClusterConfigPath,
171+
DiscoverNetwork: config.DiscoverNetwork,
172+
Requests: config.Requests,
173+
Limits: config.Limits,
174+
}
175+
}
176+
150177
// deployAndWaitForResult handles the common deploy-wait-retrieve lifecycle for an agent Job.
151178
// It creates the deployer, deploys RBAC and the Job, streams logs, waits for completion,
152179
// and retrieves the snapshot data from the result ConfigMap.
@@ -169,26 +196,7 @@ func deployAndWaitForResult(ctx context.Context, clientset k8sclient.Interface,
169196
// name the injected selector (TOCTOU: node may be cordoned after detection).
170197
autoInjectedGPUSelector := maybeInjectGPUNodeSelector(ctx, clientset, config)
171198

172-
agentConfig := agent.Config{
173-
Namespace: config.Namespace,
174-
ServiceAccountName: config.ServiceAccountName,
175-
JobName: config.JobName,
176-
Image: config.Image,
177-
ImagePullSecrets: config.ImagePullSecrets,
178-
NodeSelector: config.NodeSelector,
179-
Tolerations: config.Tolerations,
180-
Output: agentOutput,
181-
Debug: config.Debug,
182-
Privileged: config.Privileged,
183-
RequireGPU: config.RequireGPU,
184-
RuntimeClassName: config.RuntimeClassName,
185-
MaxNodesPerEntry: config.MaxNodesPerEntry,
186-
OS: config.OS,
187-
ClusterConfigPath: config.ClusterConfigPath,
188-
DiscoverNetwork: config.DiscoverNetwork,
189-
Requests: config.Requests,
190-
Limits: config.Limits,
191-
}
199+
agentConfig := buildAgentConfig(config, agentOutput)
192200

193201
deployer := agent.NewDeployer(clientset, agentConfig)
194202

@@ -267,18 +275,24 @@ func deployAndWaitForResult(ctx context.Context, clientset k8sclient.Interface,
267275
fmt.Fprintln(logWriter(), logs)
268276
fmt.Fprintln(logWriter(), "--- end logs ---")
269277
}
278+
isTransient := errors.IsTransient(waitErr)
270279
msg := "job failed"
271280
if autoInjectedGPUSelector {
272281
msg = "job failed (auto-injected node selector nvidia.com/gpu.present=true — " +
273-
"if no GPU nodes are schedulable, target a GPU node explicitly, e.g. " +
274-
"--node-selector kubernetes.io/hostname=<gpu-node> " +
275-
"(repeat the flag per key=value), or pass --require-gpu to schedule onto " +
276-
"a node advertising the nvidia.com/gpu resource)"
282+
"verify matching GPU nodes are Ready and schedulable; if tolerations were " +
283+
"explicitly cleared or replaced, pass a matching --toleration " +
284+
"key=value:effect. To override placement, pass --node-selector " +
285+
"kubernetes.io/hostname=<gpu-node>; --require-gpu selects a node " +
286+
"advertising the nvidia.com/gpu resource)"
287+
} else if isTransient {
288+
msg = "job failed (verify target nodes are Ready and schedulable; if " +
289+
"tolerations were explicitly cleared or replaced, pass a matching " +
290+
"--toleration key=value:effect)"
277291
}
278292
// A wait that exceeded the deadline (pending pod, image pull, no schedulable
279293
// node) is transient and retryable — classify it as ErrCodeTimeout rather
280294
// than masking it as a deterministic ErrCodeInternal failure.
281-
if errors.IsTransient(waitErr) {
295+
if isTransient {
282296
return nil, errors.Wrap(errors.ErrCodeTimeout, msg, waitErr)
283297
}
284298
return nil, errors.Wrap(errors.ErrCodeInternal, msg, waitErr)
@@ -680,6 +694,15 @@ func DefaultTolerations() []corev1.Toleration {
680694
}
681695
}
682696

697+
// effectiveAgentTolerations applies the snapshot agent's scheduling default
698+
// without collapsing an explicit empty override into that default.
699+
func effectiveAgentTolerations(tolerations []corev1.Toleration) []corev1.Toleration {
700+
if tolerations == nil {
701+
return DefaultTolerations()
702+
}
703+
return tolerations
704+
}
705+
683706
func validateTaintEffect(effect corev1.TaintEffect) error {
684707
switch effect {
685708
case corev1.TaintEffectNoSchedule:

pkg/snapshotter/agent_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
stderrors "errors"
1919
"os"
2020
"path/filepath"
21+
"reflect"
2122
"strings"
2223
"testing"
2324

@@ -51,6 +52,46 @@ func TestDefaultTolerations(t *testing.T) {
5152
}
5253
}
5354

55+
func TestBuildAgentConfigTolerations(t *testing.T) {
56+
explicit := []corev1.Toleration{
57+
{
58+
Key: "dedicated",
59+
Operator: corev1.TolerationOpEqual,
60+
Value: "gpu-workload",
61+
Effect: corev1.TaintEffectNoSchedule,
62+
},
63+
}
64+
tests := []struct {
65+
name string
66+
input []corev1.Toleration
67+
want []corev1.Toleration
68+
}{
69+
{
70+
name: "omitted uses tolerate-all default",
71+
want: DefaultTolerations(),
72+
},
73+
{
74+
name: "explicit empty disables default",
75+
input: []corev1.Toleration{},
76+
want: []corev1.Toleration{},
77+
},
78+
{
79+
name: "explicit tolerations are preserved",
80+
input: explicit,
81+
want: explicit,
82+
},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
got := buildAgentConfig(&AgentConfig{Tolerations: tt.input}, "snapshot.yaml").Tolerations
88+
if !reflect.DeepEqual(got, tt.want) {
89+
t.Errorf("buildAgentConfig().Tolerations = %#v, want %#v", got, tt.want)
90+
}
91+
})
92+
}
93+
}
94+
5495
func TestAgentConfig_Defaults(t *testing.T) {
5596
// Test that AgentConfig can be instantiated with zero values
5697
cfg := AgentConfig{}

0 commit comments

Comments
 (0)