> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/aiperf/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/aiperf/_mcp/server.

# Kubernetes Configuration Reference

This guide covers all the ways to configure AIPerf benchmarks on Kubernetes -- from the AIPerfJob custom resource fields to CLI flags and Helm chart settings.

---

## AIPerfJob Custom Resource

An `AIPerfJob` is a Kubernetes custom resource that tells the operator what benchmark to run. Here is the full structure:

```yaml
apiVersion: aiperf.nvidia.com/v1alpha1
kind: AIPerfJob
metadata:
  name: my-benchmark
  namespace: my-benchmarks  # optional; kubectl uses your current context's namespace when omitted
spec:
  # Benchmark configuration (what to measure)
  benchmark:
    models: ["Qwen/Qwen3-0.6B"]
    endpoint:
      urls: ["http://dynamo-agg-frontend.dynamo-server.svc:8000/v1"]
      streaming: true
    datasets:
      - name: main
        type: synthetic
        entries: 1000
        prompts:
          isl: { mean: 512, stddev: 0 }
          osl: { mean: 128, stddev: 0 }
    phases:
      - name: profiling
        type: concurrency
        concurrency: 50
        requests: 500
    artifacts:
      autoPlot: true
      plotRequired: false

  # Config-v2 envelope field (a sibling of benchmark, not nested inside it)
  plot:
    visualization:
      single_run_defaults: [ttft_over_time]
      single_run_plots:
        ttft_over_time:
          type: scatter
          x: request_number
          y: time_to_first_token
          title: TTFT over time

  # Container image (defaults to the chart/AIPerf image when omitted)
  image: "nvcr.io/nvidia/aiperf:latest"

  # Pod resource mode
  resourceMode: burstable        # "burstable" (default), "guaranteed", or "none"

  # Worker scaling
  connectionsPerWorker: 100       # max concurrent connections per worker process

  # Lifecycle
  ttlSecondsAfterFinished: 300    # seconds to keep pods after completion
  timeoutSeconds: 0               # benchmark timeout (0 = no timeout)

  # Cancel a running benchmark
  cancel: false                   # set to true to cancel

  # Pod customization
  podTemplate:
    nodeSelector:
      nvidia.com/gpu.product: "A100"
    tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
    imagePullSecrets:
      - {name: my-registry-secret}
    env:
      - name: AIPERF_HTTP_CONNECTION_LIMIT
        value: "200"
    volumes:
      - name: model-cache
        persistentVolumeClaim:
          claimName: model-cache
    volumeMounts:
      - name: model-cache
        mountPath: /root/.cache/huggingface

  # Kueue scheduling
  scheduling:
    queueName: my-queue
    priorityClass: high-priority
```

---

## Spec Fields Reference

### Benchmark Configuration (`spec.benchmark`)

The `benchmark` section mirrors the standard AIPerf YAML config. Any field you use in a local `aiperf profile` run works here.

| Field | Type | Description |
|-------|------|-------------|
| `models` | list[string] | Model name(s) served by the endpoint |
| `endpoint.urls` | list[string] | Inference server URLs |
| `endpoint.streaming` | bool | Enable streaming responses |
| `endpoint.type` | string | Endpoint type. The CRD deliberately carries **no** `default:` here so an omitted `type` stays absent and `endpoint.template` can infer `type: template`; Pydantic resolves an otherwise-omitted `type` to `chat` |
| `datasets` | list | Named dataset configurations (each entry has a `name`) |
| `phases` | list | Ordered load phases (warmup, profiling, etc.), each with a `name` |

See the [YAML Config Reference](/aiperf/dev/tutorials/configuration/yaml-configuration-files) for the complete set of benchmark fields.

> **Shorthand siblings.** The apiserver also accepts the singular shortcuts
> from AIPerf CLI YAML — `model:` (string/list/object), `dataset:` (single
> dict), and top-level `warmup:` / `profiling:` (phase dicts) — and the
> operator hoists them into the canonical `models`/`datasets`/`phases`
> shapes before validation. Mixing the canonical and shorthand form for the
> same slot (e.g. both `datasets:` and `dataset:`) is rejected at admission.
> Full rule catalog: [CRD Validation Rules](/aiperf/dev/kubernetes/crd-validation-rules).

`datasets` and `phases` are lists, not maps. Kubernetes alphabetizes the keys
of object-typed CRD fields at storage time, so phase ordering is only
preserved because it is expressed as a list.

### Plot envelope (`spec.plot`)

Kubernetes preserves the Config-v2 `plot:` envelope field and runs it after
the benchmark's exporters have finished, before the results-sidecar ready
marker is written. The resolved envelope is saved as
`.aiperf-plot-config.yaml` beside the run artifacts, so a later
`aiperf plot <run-directory>` uses the same visualization configuration.

Setting `plot:` implies `spec.benchmark.artifacts.autoPlot: true` unless
`autoPlot: false` was explicitly authored. With the default
`plotRequired: false`, rendering failures produce a warning and the exported
benchmark artifacts still become ready. With `plotRequired: true`, rendering
is part of the completion transaction: a failure leaves results unready and
the controller exits non-zero. Inline plot mappings are the portable form for
hand-authored CRs; `aiperf kube generate -f config.yaml` resolves a file-backed
`plot: ./plots/config.yaml` into the inline form when it emits the CR.

### Load Phases (`spec.benchmark.phases`)

Each phase defines a load pattern:

```yaml
phases:
  - name: warmup
    kind: warmup
    type: concurrency
    concurrency: 10
    requests: 10
  - name: profiling
    kind: profiling
    type: concurrency
    concurrency: 50
    requests: 500
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | required | Unique phase identity used in status and artifact paths. Must match `^[A-Za-z_][A-Za-z0-9_-]*$` and be unique case-insensitively |
| `kind` | string | inferred for canonical names | Semantic role: `warmup` or `profiling`; warmup metrics are kept phase-scoped and excluded from profiling aggregates |
| `type` | string | required | Load type: `concurrency`, `constant`, `poisson`, `gamma`, `user_centric`, or `fixed_schedule` |
| `concurrency` | int | - | Number of concurrent requests (>= 1) |
| `requests` | int | - | Total requests to send (>= 1) |
| `duration` | float \| string | - | Phase duration, alternative to `requests`. Seconds, or a suffixed string (`300`, `'5m'`, `'2h'`) |
| `sessions` | int | - | Stop after this many sessions complete (>= 1) |

### Deployment Options (spec top level)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `image` | string | Installed chart image (operator) or `nvcr.io/nvidia/aiperf:latest` (direct mode) | AIPerf container image |
| `imagePullPolicy` | string | - | `Always`, `IfNotPresent`, or `Never` (Helm default: `IfNotPresent`) |
| `resourceMode` | string | `burstable` | Pod CPU/memory mode. `burstable` (default) sets requests only, no limits (Burstable QoS) so the controller can grow during aggregation without being OOM-killed by cgroup; `guaranteed` keeps requests==limits (Guaranteed QoS); `none` omits CPU/memory requests and limits for both controller and worker pods. |
| `connectionsPerWorker` | int | 100 | Max concurrent connections per worker process |
| `ttlSecondsAfterFinished` | int | 300 | Seconds to keep pods after completion |
| `timeoutSeconds` | int | 0 | Benchmark timeout in seconds (0 = no timeout) |
| `cancel` | bool | `false` | Set to `true` to cancel a running benchmark |
| `keepFailedPods` | bool | `false` | Preserve pods on failure for debugging (overrides `ttlSecondsAfterFinished`) |
| `resultsTtlDays` | int | - | Override operator-level `AIPERF_RESULTS_TTL_DAYS` for this job or sweep archive (1-365) |
| `skipEndpointCheck` | bool | `false` | Skip the operator-side endpoint reachability probe before deploying |
| `failurePolicy.onChildFailure` | string | `continue` | `continue` or `abort`. On AIPerfJob governs the single benchmark; on AIPerfSweep whether a failed child aborts the sweep |
| `failurePolicy.maxFailures` | int | 0 | Hard failure budget for the whole sweep; `0` = unbounded. When >0 the sweep stops scheduling children once `failedRuns >= maxFailures` and ends `Failed` |
| `schemaVersion` | string | `2.0` | Config-v2 schema version; `2.0` is the only accepted value |

`spec` also carries the remaining Config-v2 envelope fields as siblings of
`benchmark`: `plot` (below), plus `sweep`, `multiRun`, `variables`,
`randomSeed`, and `noSweepTable`. `sweep` and multi-run orchestration
(`multiRun.numRuns > 1` or `multiRun.convergence`) are rejected on `AIPerfJob`
and required/allowed on `AIPerfSweep` — see
[CRD Validation Rules](/aiperf/dev/kubernetes/crd-validation-rules). `AIPerfSweep` additionally accepts
`childMetadata` (labels/annotations stamped onto every child `AIPerfJob`).

### Pod Template (`spec.podTemplate`)

Customize the pods that run your benchmark. Every field is optional; typed
fields are preferred over `extraPodSpec` because preflight checks, env merging,
and securityContext merging only apply to typed fields.

| Field | Type | Description |
|-------|------|-------------|
| `nodeSelector` | map | Node labels to constrain scheduling |
| `tolerations` | list | Tolerations for tainted nodes |
| `affinity` | map | K8s `Affinity` (nodeAffinity, podAffinity, podAntiAffinity) |
| `topologySpreadConstraints` | list | K8s `TopologySpreadConstraint` entries |
| `imagePullSecrets` | list[object] | Secret references for private registries, K8s `LocalObjectReference` shape: `[{name: my-secret}]` |
| `env` | list | Extra environment variables (K8s `EnvVar` format) |
| `volumes` | list | Additional volume definitions |
| `volumeMounts` | list | Additional volume mounts |
| `annotations` | map | Extra pod annotations |
| `labels` | map | Extra pod labels |
| `serviceAccountName` | string | Custom service account |
| `containerSecurityContext` | map | SecurityContext applied to every container in the controller and worker pods |
| `podSecurityContext` | map | Pod-level `PodSecurityContext` (fsGroup, runAsUser, sysctls, ...) |
| `priorityClassName` | string | Native K8s PriorityClass. Distinct from `scheduling.priorityClass` (Kueue) |
| `runtimeClassName` | string | K8s RuntimeClass (e.g. `nvidia`, `kata`) |
| `schedulerName` | string | Alternate scheduler to dispatch the pod to |
| `hostAliases` | list | Extra `/etc/hosts` entries (`{ip, hostnames}`) |
| `dnsPolicy` | string | `ClusterFirst`, `ClusterFirstWithHostNet`, `Default`, or `None` |
| `dnsConfig` | map | K8s `PodDNSConfig`; typically paired with `dnsPolicy: None` |
| `terminationGracePeriodSeconds` | int | Grace period before SIGKILL (>= 0) |
| `initContainers` | list | InitContainers run to completion before the main containers |
| `shareProcessNamespace` | bool | Share a PID namespace across containers (chaos testing; keep `false` in production) |
| `extraPodSpec` | map | Escape hatch: raw PodSpec keys merged last, overriding the typed fields above |

### Scheduling (`spec.scheduling`)

For clusters using [Kueue](https://kueue.sigs.k8s.io/) for resource management:

| Field | Type | Description |
|-------|------|-------------|
| `queueName` | string | Kueue LocalQueue name for gang-scheduling |
| `priorityClass` | string | Kueue WorkloadPriorityClass for scheduling priority |

---

## CLI Flags

When using `aiperf kube profile`, you can set deployment options via CLI flags. These override values in a config file:

| Flag | Maps To | Default | Description |
|------|---------|---------|-------------|
| `--image` | `spec.image` | YAML, installed chart, or direct-mode default | Explicit container-image override; an image authored in workload YAML remains authoritative when this flag is omitted |
| `--image-pull-policy` | `spec.imagePullPolicy` | - | Image pull policy (Helm default: `IfNotPresent`) |
| `--total-workers` | `spec.benchmark.runtime.workers` | 10 | Exact worker target (distributed across pods); when omitted, a YAML-authored `runtime.workers` wins before automatic sizing |
| `--name` | `metadata.name` | auto-generated | Job name (DNS label, max 40 chars) |
| `--namespace`, `-n` | `metadata.namespace` | kubeconfig context namespace | Target namespace; required when your context does not set one |
| `--ttl-seconds` | `spec.ttlSecondsAfterFinished` | 300 | TTL after completion |
| `--node-selector` | `spec.podTemplate.nodeSelector` | `{}` | Node selector labels |
| `--tolerations` | `spec.podTemplate.tolerations` | `[]` | Pod tolerations |
| `--queue-name` | `spec.scheduling.queueName` | - | Kueue queue name |
| `--priority-class` | `spec.scheduling.priorityClass` | - | Kueue priority class |
| `--image-pull-secrets` | `spec.podTemplate.imagePullSecrets` | `[]` | Pull secret names |
| `--env-vars` | `spec.podTemplate.env` | `{}` | Non-sensitive extra env vars |
| `--env-from-secrets` | `spec.podTemplate.env` | `{}` | Env vars from Kubernetes Secrets as `ENV_NAME=secret_name/key`; required for endpoint API keys, sensitive headers, and credentialed URLs |
| `--secret-mounts` | `spec.podTemplate.volumes` + `volumeMounts` | `[]` | Secret volume mounts (`{name, mount_path, sub_path}`) as a JSON object, a JSON array, or the flag repeated |
| `--annotations` | `spec.podTemplate.annotations` | `{}` | Extra pod annotations |
| `--labels` | `spec.podTemplate.labels` | `{}` | Extra pod labels |
| `--service-account` | `spec.podTemplate.serviceAccountName` | - | Pod service account |
| `--kubeconfig` | - | `~/.kube/config` or `KUBECONFIG` | Path to kubeconfig file |
| `--kube-context` | - | current context | Kubernetes context to use |
| `--detach`, `-d` | - | `false` | Exit after deploying. Auto-enabled in non-interactive environments |
| `--dry-run` | - | `false` | Print CR without submitting |
| `--operator` | - | `false` | Deploy through the operator without probing the cluster-scoped AIPerfJob CRD |
| `--no-operator` | - | `false` | Deploy without operator |
| `--skip-endpoint-check` | - | `false` | Skip endpoint health check |
| `--no-wait` | - | `false` | Don't wait for pods ready |
| `--attach-port` | - | 0 (ephemeral) | Local port for port-forward |

The four map-valued flags — `--annotations`, `--labels`, `--env-vars`, and
`--env-from-secrets` — each accept three equivalent spellings: `--labels
tier=gold`, `--labels.tier gold`, and `--labels '{"tier": "gold"}'`. Repeat the
flag for additional entries.

Benchmark CLI flags use the same precedence for plain AIPerf config files and
`AIPerfJob` CR input: explicitly passed flags override YAML, while omitted CLI
defaults do not rewrite authored values. This applies to `profile` and
`generate`; `kube sweep` applies the same benchmark overrides before it builds
the `AIPerfSweep` template. Kubernetes deployment flags merge into the CR
deployment subtree, so, for example, `--node-selector gpu=true` does not erase
an unrelated YAML `podTemplate.affinity` or `podTemplate.volumes` block. An
explicit list-valued flag still replaces the corresponding YAML list.

Operator mode is auto-detected when neither mode flag is passed. On a
multi-tenant cluster where users can create namespaced `AIPerfJob` resources
but cannot read cluster-scoped CRDs, pass `--operator` explicitly. When an
explicit `--namespace` is also supplied, `profile` assumes the namespace was
pre-provisioned and does not attempt to create it. `--operator` and
`--no-operator` are mutually exclusive.

When an `AIPerfJob` CR is passed to `profile --no-operator` or
`generate --no-operator`, direct mode preserves the fields that JobSet can
represent, including `imagePullPolicy`, `resourceMode`, `keepFailedPods`,
`ttlSecondsAfterFinished`, `podTemplate`, and `scheduling`. The operator still
owns CR lifecycle fields such as `timeoutSeconds`, `resultsTtlDays`, `cancel`,
and `failurePolicy`; they have no direct-mode reconciler.

Some benchmark runtime fields are intentionally Kubernetes-managed:
`artifacts.dir` is fixed to the mounted `/results` volume, and the operator
sets the service run type, API bind, dataset-service URL, UI, and ZMQ transport
needed for cross-pod operation. Other runtime fields, including
`workers`, `workersPerPod`, `recordProcessors`,
`recordProcessorsPerPod`, and `statsInterval`, remain user-configurable. A
total `recordProcessors` value must divide evenly across identical worker pods;
otherwise set `recordProcessorsPerPod` explicitly. These fields also drive the
preflight memory estimate, so a pod layout you set here is the layout the
estimate is sized against — see
[Memory estimator](/aiperf/dev/kubernetes/memory-estimator#topology).

---

## Helm Chart Configuration

The operator Helm chart is configured via `values.yaml`. Key settings:

### Operator

```yaml
operator:
  replicas: 1
  id: ""                            # "" = cluster-wide operator; a unique id
                                    # claims operator.watchNamespaces so the
                                    # cluster-wide operator steps aside
  resources:
    requests: { cpu: 250m, memory: 256Mi }
    # No limits set by default (burstable QoS) so the operator can scale
    # memory/CPU with high-concurrency runs.
  env:
    monitorInterval: "10.0"         # seconds between status checks
    monitorInitialDelay: "5.0"      # delay before first status check
    jobTimeoutSeconds: "0"          # 0 = no timeout
    podRestartThreshold: "3"        # restarts before warning events
    resultsTtlDays: "30"            # days to keep results on PVC
    resultsMaxRetries: "5"          # retries for fetching results
    resultsRetryDelay: "2.0"        # delay between result fetch retries
    endpointCheckTimeout: "10.0"    # endpoint health check timeout
    resultsCompressOnDisk: "true"   # store results as zstd on PVC
```

`operator.replicas` is fixed at `1`. The kopf process and runs index have one
authoritative writer and do not use leader election, so the chart rejects
multi-replica values instead of presenting unsafe pseudo-HA.

`operator.id` decides namespace ownership between installs. Leave it empty for
the cluster-wide operator. Set it, together with `operator.watchNamespaces`, to
run a scoped operator that leases those namespaces away from the cluster-wide
one — see
[Operator Scope and Namespace Ownership](/aiperf/dev/kubernetes/production-deployment#operator-scope-and-namespace-ownership).

### Storage

Results are stored on a PVC so they survive pod deletion:

```yaml
storage:
  enabled: true           # default — PVC-backed; set false for ephemeral emptyDir
  size: 1Ti
  storageClassName: ""    # empty = cluster default
  mountPath: "/data"      # mount path inside the operator pod
  accessMode: "ReadWriteOnce"
  emptyDirSizeLimit: ""   # bounds the fallback emptyDir when enabled=false; empty = unbounded
```

### Results Server

A sidecar that serves stored results via HTTP (used by `aiperf kube results` by default):

```yaml
resultsServer:
  port: 8081
  resources:
    requests: { cpu: 100m, memory: 512Mi }
    limits: { cpu: 500m, memory: 1Gi }
```

The `resultsServer` chart block only exposes `port` and `resources` today.

The results-server also hosts optional POST routes that create or cancel
`AIPerfJob` resources. These are governed by two environment variables read on
the results-server container: `AIPERF_OPERATOR_MUTATING_ROUTES_ENABLED`
(default `false`) and `AIPERF_OPERATOR_MUTATING_ROUTES_TOKEN` (default empty —
fails closed). When disabled, the read-only APIs stay exposed while those
mutating POSTs return 403, so serving the results-server does not grant write
access through the operator ServiceAccount. The index-rebuild route is mounted
read-only on the results-server, so even with the routes enabled and a valid
token it returns 503; restart the operator pod to run the single-writer startup
rebuild.

The bundled chart does **not** template these two variables (there is no `resultsServer.mutatingRoutes` value and no token-secret projection). To turn the routes on, set both env vars directly on the `results-server` container — e.g. via a deployment patch or a customized chart template — then have clients send `Authorization: Bearer <token>` on protected POST requests. The browser dashboard never receives this token and keeps create/cancel controls disabled; use `aiperf kube` or `kubectl` from an authenticated terminal for those mutations.

### `dashboard`

Optional Plotly Dash sidecar for the operator Pod. Default off.

| Key                              | Default      | Description                                                                   |
|----------------------------------|--------------|-------------------------------------------------------------------------------|
| `dashboard.enabled`              | `false`      | Whether to add the dashboard container and surface the "Plots ↗" SPA link.   |
| `dashboard.port`                 | `8082`       | Pod-local HTTP port. `results-server` reverse-proxies `/dashboard/*` here.    |
| `dashboard.resources.requests`   | `cpu: 100m, memory: 1Gi` | Resource requests. Leave generous so the build has memory.        |
| `dashboard.resources.limits`     | `{}`         | Empty by default = no limit. Set `memory:` to enforce a ceiling.             |

See [`dashboard-ui.md`](/aiperf/dev/kubernetes/web-dashboard#isolated-plotly-dashboard-sidecar-opt-in) for the full architecture.

### Benchmark RBAC Namespaces

```yaml
benchmarkRbacNamespaces: []
```

The benchmark `Role` and `RoleBinding` are installed in the chart's release
namespace whenever `rbac.create` is `true` (the default). List additional
namespaces in `benchmarkRbacNamespaces` to install the same pair there — for
example when each team runs benchmarks in its own namespace. The chart does
not create any of these namespaces; they must already exist.

### Operator ServiceAccount

```yaml
serviceAccount:
  create: true
  name: "" # auto-generated from the release name when create=true
  annotations: {}
```

With `create: false` the chart provisions no ServiceAccount and no RBAC of its
own, so `serviceAccount.name` is **required** and must name a pre-provisioned
account already bound to the operator's `ClusterRole`. Omitting it fails the
render; it does not fall back to the namespace `default` account, which carries
none of the operator's permissions. See
[`rbac-security.md`](/aiperf/dev/kubernetes/rbac-and-security#rbaccreatefalse-workflow) for the
out-of-band RBAC tree.

### Default Image

The default image used for benchmark jobs if not specified in the CR:

```yaml
defaults:
  image: "" # empty = "<image.repository>:<image.tag|Chart.AppVersion>"
  imagePullPolicy: "IfNotPresent"
```

When `defaults.image` is empty (the chart default), the chart computes the
benchmark image as `<image.repository>:<image.tag | Chart.AppVersion>`, so
overriding `image.tag` automatically propagates to benchmark pods. Set
`defaults.image` explicitly to decouple the benchmark image from the operator
image.

### Ingress

Expose the results-server HTTP API outside the cluster via a Kubernetes `Ingress`. Disabled by default -- results are reachable via `ClusterIP` + `kubectl port-forward`.

```yaml
ingress:
  enabled: false
  className: ""                    # IngressClass name (e.g. "nginx"); empty uses cluster default
  annotations: {}                  # annotations applied to the Ingress
  hosts:
    - host: aiperf.example.com
      paths:
        - path: /
          pathType: Prefix         # backend port defaults to resultsServer.port; override with portNumber
  tls: []                          # optional list of {hosts, secretName}
```

### NetworkPolicy

Restrict pod traffic to/from the operator. Disabled by default -- no restrictions applied. When enabled, ingress is allowed from the release namespace, the benchmark namespace, `benchmarkRbacNamespaces`, and `allowedNamespaces` on the health (8080), results (`resultsServer.port`), and metrics (`operator.metrics.port`, when non-zero) ports. Egress allows DNS, the K8s API server (443/6443), the benchmark namespace, `benchmarkRbacNamespaces`, and `allowedNamespaces`.

```yaml
networkPolicy:
  enabled: false
  allowedNamespaces: []            # extra namespaces allowed to reach the operator and reachable on egress
  allowedIngressCIDRs: []          # CIDR allow-list for external scrapers (Prometheus, ingress controllers)
```

### Kueue

```yaml
kueue:
  # When set, the benchmark namespace is annotated with
  # kueue.x-k8s.io/default-queue-name so all AIPerf jobs are admitted through
  # Kueue even without an explicit --queue-name flag. Left empty, it falls
  # back to kueue.localQueueName when createQueues is true.
  defaultQueueName: ""

  # Optionally let the chart provision the Kueue objects themselves
  # (ResourceFlavor + ClusterQueue + LocalQueue). Off by default so the
  # chart renders on clusters without Kueue CRDs.
  createQueues: false
  flavorName: "default-flavor"
  clusterQueueName: "aiperf-cluster-queue"
  localQueueName: "aiperf-local-queue"
  resources:
    cpu: "1000"
    memory: "4Ti"
    gpu: ""            # empty = omit nvidia.com/gpu from the quota entirely
```

See [Kueue Integration](/aiperf/dev/kubernetes/kueue-integration) for the full gang-scheduling walkthrough.

### `helm test` hooks

```yaml
tests:
  # Renders the two `helm test` hook Pods (CRD check, operator health check)
  # and the dedicated ServiceAccount / Role / RoleBinding / ClusterRole /
  # ClusterRoleBinding they run under. All five RBAC objects and both Pods are
  # gated together, so disabling this never leaves a Pod pointing at a
  # ServiceAccount that was not created.
  enabled: true
```

Set `tests.enabled=false` when cluster policy forbids chart-managed
cluster-scoped RBAC. `test-crd-installed` reads cluster-scoped
CustomResourceDefinitions, so its `ClusterRole` cannot be narrowed to a
namespace — this is the only way to make the chart emit zero `ClusterRole` and
`ClusterRoleBinding` objects. `helm test` then prints `TEST SUITE: None` and
exits 0. It is a separate flag from `rbac.create` on purpose; see
[Eliminating all cluster-scoped RBAC](/aiperf/dev/kubernetes/rbac-and-security#eliminating-all-cluster-scoped-rbac).

---

## Configuration Patterns

### Combining CLI Flags with Config Files

CLI flags override config file values. This is useful for changing deployment settings without editing the YAML:

```bash
# Use config file for benchmark settings, override image and workers
aiperf kube profile \
  --config benchmark.yaml \
  --image my-registry/aiperf:v2.0 \
  --total-workers 20 \
  --namespace production
```

### Validating Before Deploying

`aiperf kube validate` checks `AIPerfJob` and `AIPerfSweep` CR YAML — files
with `apiVersion: aiperf.nvidia.com/v1alpha1`, a `kind:`, `metadata.name`, and
`spec.benchmark`. A plain benchmark config file is not a CR and will fail these
structural checks; generate a CR first with
`aiperf kube generate --operator --config benchmark.yaml`.

```bash
# Validate CR structure and fields
aiperf kube validate aiperfjob.yaml

# Strict mode fails on unknown spec fields
aiperf kube validate --strict aiperfjob.yaml

# JSON output for CI
aiperf kube validate -o json aiperfjob.yaml
```

Preview what will be submitted without deploying:

```bash
aiperf kube profile --config benchmark.yaml --image aiperf:latest --dry-run
```

### Memory Estimation

AIPerf prints a memory estimate before deploying. This helps you right-size your pods:

```bash
aiperf kube generate --operator --config benchmark.yaml --image aiperf:latest
```

The memory estimate is printed to stderr. It accounts for dataset size, number of workers, connection pools, and record buffers.

### Multiple Phases

Use multiple phases to warm up before measuring:

```yaml
phases:
  - name: warmup
    kind: warmup
    type: concurrency
    concurrency: 10
    requests: 20
  - name: low_load
    kind: profiling
    type: concurrency
    concurrency: 25
    requests: 250
  - name: high_load
    kind: profiling
    type: concurrency
    concurrency: 100
    requests: 500
```

Phases run in order. Every phase keeps phase-scoped results; only phases with
`kind: profiling` contribute to profiling aggregates. Canonical names
`warmup` and `profiling` infer their matching kinds, while custom names require
an explicit `kind`.

---

## Resource Mode

`spec.resourceMode` controls the QoS class Kubernetes assigns to benchmark pods. The three modes differ only in how `requests` and `limits` are emitted onto the manifest — the underlying resource budget is the same in every case.

| Mode | Behavior | K8s QoS class | When to use |
|---|---|---|---|
| `burstable` (default) | `requests` only; no `limits`. | Burstable | Default. Cost-sensitive clusters, development, and any benchmark where the controller's aggregation phase may temporarily allocate beyond the request — limits-free pods are not OOM-killed by cgroup. Controller pods stay Burstable by default in the operator's own `values.yaml` for the same reason. |
| `guaranteed` | `requests == limits` for CPU and memory. | Guaranteed | Production benchmarks where pods must not be evicted under pressure and noisy-neighbor behavior is unacceptable. Use this mode when you have measured the controller's peak memory and want a hard ceiling. |
| `none` | Neither `requests` nor `limits`. | BestEffort | Environments where CPU/memory admission control is disabled (e.g. CI `kind` clusters with tight node budgets, or when an external scheduler handles admission). The resource-dependent preflight checks ("Node Resources", "Per-Node Schedulability", "Resource Quotas", "Memory Estimation") are auto-skipped. |

The mode applies to both controller-pod and worker-pod containers; there is no per-container override. On an `AIPerfSweep` it also covers the sweep-controller pod (both its `sweep-controller` and `results-sidecar` containers), which is why `burstable` matters there: under `sweep.type: adaptive_search` that pod imports torch/BoTorch and grows to roughly 350 MiB after the first GP fit, well past its 512Mi request. OOMKill semantics follow the QoS class — `guaranteed` pods will not be evicted for resource pressure, `burstable` pods may be throttled, and `none`/BestEffort pods can be evicted first.

---

## Tunable Environment Variables (`AIPERF_K8S_*`)

These variables tune the operator and individual benchmark pods. `operator.env` is a fixed key map, not an arbitrary passthrough: only the keys it declares reach the container. For any other variable, set it on the live Deployment (`kubectl -n aiperf-system set env deployment/aiperf-operator -c operator KEY=VALUE` — `-c operator` matters, the Deployment also runs the `results-server` and `dashboard` containers) to affect every subsequent job, or use `spec.podTemplate.env` to affect one CR only.

Resource-sizing and JobSet variables are read by the process that *renders* the JobSet, so `spec.podTemplate.env` has no effect on them; they must be set on the operator.

### Resource sizing (per-container CPU / memory)

Every control-plane container, the event-bus proxy sidecar, the results sidecar, and the worker pod have a paired `_CPU` / `_MEMORY` variable. Defaults are low burstable requests so many tiny jobs can start concurrently; raise them for very large concurrency or high-token workloads.

| Variable | Default | Applies to |
|---|---|---|
| `AIPERF_K8S_SYSTEM_CONTROLLER_CPU` / `_MEMORY` | `75m` / `192Mi` | SystemController container |
| `AIPERF_K8S_SWEEP_CONTROLLER_CPU` / `_MEMORY` | `75m` / `512Mi` | Sweep-controller container (higher memory: adaptive search imports torch/BoTorch here) |
| `AIPERF_K8S_TIMING_MANAGER_CPU` / `_MEMORY` | `50m` / `192Mi` | TimingManager container |
| `AIPERF_K8S_DATASET_MANAGER_CPU` / `_MEMORY` | `50m` / `256Mi` | DatasetManager container |
| `AIPERF_K8S_RECORDS_MANAGER_CPU` / `_MEMORY` | `75m` / `256Mi` | RecordsManager container (raise to 4000m+ for >500k concurrency) |
| `AIPERF_K8S_API_CPU` / `_MEMORY` | `75m` / `256Mi` | API container (WebSocket + HTTP) |
| `AIPERF_K8S_GPU_TELEMETRY_MANAGER_CPU` / `_MEMORY` | `25m` / `192Mi` | GPU telemetry container |
| `AIPERF_K8S_SERVER_METRICS_MANAGER_CPU` / `_MEMORY` | `25m` / `192Mi` | Server-metrics container |
| `AIPERF_K8S_RESULTS_SIDECAR_CPU` / `_MEMORY` | `25m` / `192Mi` | Results sidecar (fallback retrieval path) |
| `AIPERF_K8S_EVENT_BUS_PROXY_CPU` / `_MEMORY` | `50m` / `64Mi` | Event-bus XPUB/XSUB proxy sidecar |
| `AIPERF_K8S_WORKER_POD_CPU` / `_MEMORY` | `3350m` / `6Gi` | Worker pod (workers + record processors + WPM) |

### Architecture toggles

| Variable | Default | Purpose |
|---|---|---|
| `AIPERF_K8S_EVENT_BUS_SIDECAR_ENABLED` | `true` | Run the XPUB/XSUB event-bus proxy as a dedicated sidecar container. Set to `false` to revert to the pre-sidecar behavior where `SystemController` hosts the proxy in-process. Only disable if you are explicitly testing the legacy path. |
| `AIPERF_K8S_RECORD_PROCESSOR_SCALE_FACTOR` | `1` | Workers per record processor inside each worker pod. `1` means one RP per worker (maximum fairness); higher values amortize RP overhead across more workers. |
| `AIPERF_K8S_RECORD_PROCESSOR_CPU_REQUEST` | (unset) | Optional per-RP CPU request override. When unset, RP CPU is derived from the worker-pod budget. |

### JobSet and lifecycle

| Variable | Default | Purpose |
|---|---|---|
| `AIPERF_K8S_JOBSET_TTL_SECONDS_AFTER_FINISHED` | `300` | Seconds to keep pods after JobSet completion. Override per-CR via `spec.ttlSecondsAfterFinished`. |
| `AIPERF_K8S_JOBSET_DIRECT_MODE_TTL_SECONDS` | `28800` (8h) | TTL applied when `--no-operator` is used, giving you time to pull results from pod-local storage. |
| `AIPERF_K8S_JOBSET_CONTROLLER_BACKOFF_LIMIT` | `0` | Controller-job retry count. Default `0` — fail fast when the controller crashes. |
| `AIPERF_K8S_JOBSET_WORKER_BACKOFF_LIMIT` | `20` | Worker-job retry count. Higher than controller to absorb transient pod-startup flakes. |
| `AIPERF_K8S_JOBSET_WORKER_CONNECTION_PROBE_TIMEOUT` | `60.0` | Seconds a worker waits for the PUB/SUB connection probe before exiting so K8s restarts it. |

### Health probes

| Variable | Default | Purpose |
|---|---|---|
| `AIPERF_K8S_HEALTH_STARTUP_PERIOD_SECONDS` | `5` | Startup probe interval. |
| `AIPERF_K8S_HEALTH_STARTUP_FAILURE_THRESHOLD` | `30` | Consecutive failures before the container is killed during startup. Raise for slow-starting workloads (large tokenizers, cold container images). |
| Other `AIPERF_K8S_HEALTH_*` | see code | Liveness/readiness intervals, timeouts, thresholds. |

### Ports

All health and service ports are overridable via `AIPERF_K8S_PORT_*` (e.g. `AIPERF_K8S_PORT_API_SERVICE=9090`, `AIPERF_K8S_PORT_RESULTS_SIDECAR=9091`, `AIPERF_K8S_PORT_SYSTEM_CONTROLLER_HEALTH=8080`). Consult `src/aiperf/kubernetes/environment.py::_PortSettings` for the full list — changing these is rarely necessary.

The complete, generated reference for every `AIPERF_*` variable (including non-k8s ones) lives in [`../environment-variables.md`](/aiperf/dev/reference/environment-variables).

---

## Related Documentation

- [Getting Started](/aiperf/dev/kubernetes/getting-started-on-kubernetes) -- First benchmark walkthrough
- [Monitoring and Troubleshooting](/aiperf/dev/kubernetes/monitoring-and-observability) -- Live monitoring and debugging
- [Production Deployments](/aiperf/dev/kubernetes/production-deployment) -- CI/CD, Kueue, and GitOps workflows
- [CRD Validation Rules](/aiperf/dev/kubernetes/crd-validation-rules) -- Apiserver-side CEL invariants and shorthand acceptance
- [Preflight Checks](/aiperf/dev/kubernetes/preflight-checks) -- What the operator validates before admitting a CR
- [Memory Estimator](/aiperf/dev/kubernetes/memory-estimator) -- How per-component memory estimates drive resource requests
- [Direct Mode](/aiperf/dev/kubernetes/direct-mode-no-operator) -- Trade-offs when running `--no-operator`
- [User-defined output files](/aiperf/dev/kubernetes/user-defined-output-files) -- `artifacts.user_files` for templated sidecar files
- [YAML Config Reference](/aiperf/dev/tutorials/configuration/yaml-configuration-files) -- Complete benchmark configuration options