Skip to content

Commit 7ba3ac9

Browse files
authored
feat(recipes): adopt k8s-aibom in the h100-gke-cos-inference recipe (#2328)
Signed-off-by: Mark Chmarny <mark@chmarny.com>
1 parent 4cae5aa commit 7ba3ac9

12 files changed

Lines changed: 373 additions & 39 deletions

docs/design/019-k8s-aibom-runtime-inventory.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -620,11 +620,42 @@ the workflow without asserting stock stability.
620620

621621
### C. Recipe in scope
622622

623-
`h100-gke-cos-inference`, a single leaf overlay. Not a recipe family, not
624-
`gke-cos-inference`, not `gke-cos`, not a mixin, and not a shared base.
625-
Inference is chosen because an AI BOM inventories models and AI workloads, so
626-
a served model exercises the component's actual purpose. Substituting a
627-
different recipe requires amending this section.
623+
`h100-gke-cos-inference`. Not a recipe family, not `gke-cos-inference`, not
624+
`gke-cos`, not a mixin, and not a shared base. Inference is chosen because an
625+
AI BOM inventories models and AI workloads, so a served model exercises the
626+
component's actual purpose. Substituting a different recipe requires amending
627+
this section.
628+
629+
**Correction, 2026-08-21.** This section originally called
630+
`h100-gke-cos-inference` "a single leaf overlay." It is not a leaf:
631+
`h100-gke-cos-inference-dynamo` declares it as its `base`, so a componentRef
632+
added here is inherited by that recipe and the component would ship in two
633+
stock recipes rather than one. The scope decision is unchanged — one recipe —
634+
so `h100-gke-cos-inference-dynamo` declines the inherited component with
635+
`overrides.install: false`, the same gate `--runtime-inventory disabled` sets.
636+
The declined ref is retained rather than omitted so the resolved recipe records
637+
the decision.
638+
639+
Two consequences worth stating, because neither is obvious from the overlay
640+
files:
641+
642+
- Declining it there is a scope decision, not a judgment about Dynamo. AICR has
643+
qualified `k8s-aibom` on its own, not alongside `grove` and
644+
`dynamo-platform`. Widening adoption to that recipe is a later decision that
645+
needs its own evidence.
646+
- The stock render golden covers *leaves only*, so it pins
647+
`h100-gke-cos-inference-dynamo` and not `h100-gke-cos-inference`. The target
648+
recipe of this amendment therefore has no rendered-bytes parity coverage. The
649+
golden that does move is the resolution golden, recording the declined ref on
650+
the descendant, while the render golden stays byte-identical.
651+
652+
That unchanged render golden is **supporting evidence, not the proof**: it
653+
establishes only that the descendant's rendered bytes did not change. It
654+
cannot establish the one-recipe blast radius, precisely because the target
655+
recipe has no render golden of its own. The scope assertion lives in
656+
`TestK8sAIBOMStockAdoption` (`pkg/recipe`), which pins that the target
657+
enables the component, the descendant declines it, the generation-time
658+
opt-out declines it, and a sibling recipe does not declare it at all.
628659

629660
### D. User-demand case
630661

docs/user/component-catalog.md

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The source of truth is [`recipes/registry.yaml`](https://github.com/NVIDIA/aicr/
2828
| **prometheus-adapter** | Exposes custom metrics from Prometheus to the Kubernetes metrics API. Enables HPA scaling based on GPU utilization and other custom metrics. | [prometheus-adapter](https://github.com/kubernetes-sigs/prometheus-adapter) |
2929
| **aws-ebs-csi-driver** | CSI driver for Amazon EBS volumes. Provides persistent storage for workloads on EKS. EKS-specific. **Cluster-wide default StorageClass:** AICR enables `defaultStorageClass.enabled`, so this component provisions a **cluster-default** gp3 StorageClass (`ebs-csi-default-sc`) on **every** EKS cluster that includes it — not just inference recipes; training overlays inherit it too. EKS ships no default SC of its own, so this makes dynamic provisioning (e.g. the inference-perf model cache) work zero-config. Two consequences to note: (1) if the cluster already has a default SC, Kubernetes treats multiple defaults as ambiguous — unset the other; (2) a PVC that previously failed-fast on "no default SC" will now silently bind gp3, which can mask a misconfiguration. | [AWS EBS CSI Driver](https://github.com/kubernetes-sigs/aws-ebs-csi-driver) |
3030
| **k8s-ephemeral-storage-metrics** | Exports ephemeral storage usage metrics per pod. Useful for monitoring scratch space consumption on GPU nodes. | [k8s-ephemeral-storage-metrics](https://github.com/jmcgrath207/k8s-ephemeral-storage-metrics) |
31-
| **k8s-aibom** | Optional runtime AI workload inventory. Produces namespace-scoped CycloneDX 1.6 ML-BOM resources for explicitly opted-in namespaces. Registry-only: no stock recipe installs it. CLI aliases: `k8saibom`, `aibom`. See [k8s-aibom Runtime Inventory](#k8s-aibom-runtime-inventory). | [k8s-aibom](https://github.com/GoogleCloudPlatform/k8s-aibom) |
31+
| **k8s-aibom** | Optional runtime AI workload inventory. Produces namespace-scoped CycloneDX 1.6 ML-BOM resources for explicitly opted-in namespaces. Installed by one stock recipe, `h100-gke-cos-inference`; every other stock recipe leaves it out. Decline it with `aicr recipe --runtime-inventory disabled`. CLI aliases: `k8saibom`, `aibom`. See [k8s-aibom Runtime Inventory](#k8s-aibom-runtime-inventory). | [k8s-aibom](https://github.com/GoogleCloudPlatform/k8s-aibom) |
3232
| **kai-scheduler** | Gang scheduler with hierarchical queues and topology-aware placement; works with device-plugin (`nvidia.com/gpu`) and DRA GPU allocation alike. Ensures distributed training jobs land on nodes with optimal interconnect topology. | [KAI Scheduler](https://github.com/kai-scheduler/KAI-Scheduler) |
3333
| **grove** | Pod lifecycle management for Dynamo inference platform. Installed as a standalone component. | [Grove](https://github.com/ai-dynamo/grove) |
3434
| **dynamo-platform** | NVIDIA Dynamo inference serving platform with bundled CRDs. Distributed inference with KV-cache-aware routing, Dynamo request-plane traffic, a NATS-backed Kubernetes event plane for KV-cache events, and disaggregated prefill/decode. | [Dynamo](https://github.com/ai-dynamo/dynamo) |
@@ -291,9 +291,17 @@ AICR enforces and surfaces inference-gateway exposure in two places:
291291
## k8s-aibom Runtime Inventory
292292

293293
AICR qualifies k8s-aibom v1.3.0 as an optional Helm component. It is not in
294-
the base, a mixin, or any stock overlay. To enable it, add this reference to a
295-
custom or external overlay and keep that overlay's criteria as narrow as the
296-
intended rollout:
294+
the base or a mixin. Exactly one stock recipe installs it,
295+
`h100-gke-cos-inference`, under [ADR-019](https://github.com/NVIDIA/aicr/blob/main/docs/design/019-k8s-aibom-runtime-inventory.md)'s
296+
stock-adoption amendment. Decline it at generation time with
297+
`aicr recipe --runtime-inventory disabled`, described below.
298+
299+
`h100-gke-cos-inference-dynamo` inherits from that recipe and deliberately
300+
declines the component, so the Dynamo platform recipe deploys exactly what it
301+
did before. Adoption beyond the one recipe is a later decision.
302+
303+
To enable it anywhere else, add this reference to a custom or external overlay
304+
and keep that overlay's criteria as narrow as the intended rollout:
297305

298306
```yaml
299307
spec:
@@ -366,21 +374,28 @@ storage. So the check catches a stranded upgrade that crosses a
366374
storage-version boundary, such as the 1.2.0 to 1.3.0 move this pin made, and
367375
does not catch one within a boundary, such as 1.0.0 to 1.2.0.
368376

369-
**Declining the component.** No stock recipe declares `k8s-aibom` today, so the
370-
flag applies to a recipe that adds it through a custom overlay — the shape shown
371-
above. Point `--data` at the directory holding that overlay and generate with
372-
`--runtime-inventory disabled`:
377+
**Declining the component.** `h100-gke-cos-inference` installs `k8s-aibom` by
378+
default. Decline it at generation time:
379+
380+
```bash
381+
aicr recipe --service gke --accelerator h100 --os cos --intent inference \
382+
--runtime-inventory disabled -o recipe.yaml
383+
```
384+
385+
The same flag works for a recipe that adds the component through a custom
386+
overlay — the shape shown above. Point `--data` at the directory holding that
387+
overlay:
373388

374389
```bash
375390
aicr recipe --service gke --accelerator h100 --os cos --intent inference \
376391
--data ./my-recipes --runtime-inventory disabled -o recipe.yaml
377392
```
378393

379394
Passing the flag against a recipe that does not declare the component is an
380-
error, not a silent no-op:
395+
error, not a silent no-op. Training recipes do not, so:
381396

382397
```console
383-
$ aicr recipe --service gke --accelerator h100 --os cos --intent inference \
398+
$ aicr recipe --service gke --accelerator h100 --os cos --intent training \
384399
--runtime-inventory disabled
385400
[INVALID_REQUEST] runtime inventory mode "disabled" requires the recipe to
386401
declare component "k8s-aibom"; this recipe does not resolve it
@@ -519,12 +534,25 @@ underneath it, so it reconciles against resources that are disappearing.
519534

520535
1. Remove the component reference from the custom overlay and regenerate the
521536
recipe and bundle.
522-
2. Uninstall the release with the deployer-appropriate procedure in
523-
[Bundle Uninstall](cli-reference.md#bundle-uninstall) — `helm uninstall
524-
k8s-aibom -n k8s-aibom-system` for the `helm` deployer, `helmfile destroy`
525-
for Helmfile bundles, deleting the owning `Application` for Argo CD, and the
526-
`HelmRelease` for Flux. Confirm the controller Deployment is gone before
527-
continuing.
537+
2. Uninstall the release, scoped to this component only:
538+
539+
```bash
540+
# helm and helmfile bundles alike: helmfile installs through Helm, so the
541+
# release is an ordinary Helm release and this removes exactly one.
542+
helm uninstall k8s-aibom -n k8s-aibom-system
543+
```
544+
545+
For Argo CD, delete the owning `Application`; for Flux, the `HelmRelease`.
546+
Confirm the controller Deployment is gone before continuing.
547+
548+
**Do not use `helmfile destroy` for this.** It tears down *every* release in
549+
the bundle in reverse dependency order, not just this component. It is also
550+
ineffective here: step 1 regenerated the bundle without `k8s-aibom`, so the
551+
release is no longer declared in it and `destroy` would not remove the one
552+
release you actually want gone while removing all the ones you do not. If
553+
you prefer a Helmfile-native command, run it against a bundle that still
554+
declares the component and scope it explicitly with
555+
`helmfile destroy --selector name=k8s-aibom`.
528556
3. Only then delete retained AIBOMs and, last, the CRDs.
529557

530558
Deleting the CRDs cascades to every AIBOM stored cluster-wide, including any

pkg/bundler/k8s_aibom_render_parity_test.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,23 @@ import (
3535
"github.com/NVIDIA/aicr/pkg/recipe"
3636
)
3737

38-
// ADR-019 admits k8s-aibom registry-only: no stock recipe declares it, so no
39-
// stock-recipe render path exercises it and the KWOK deployer lanes never see
40-
// it. These tests are the substitute — they build the same single-component
41-
// recipe an adopter would, from the live registry entry rather than from
42-
// hardcoded coordinates, so a registry edit flows into the assertions instead
43-
// of silently diverging from them.
38+
// One stock recipe declares k8s-aibom, h100-gke-cos-inference, under ADR-019's
39+
// stock-adoption amendment. That recipe is not a leaf — h100-gke-cos-inference-dynamo
40+
// bases on it and declines the component — and the stock render golden covers
41+
// leaves only, so no golden pins this component's rendered bytes and no KWOK
42+
// deployer lane exercises it per deployer.
43+
//
44+
// These tests cover the per-deployer render: they build the same
45+
// single-component recipe an adopter would, from the live registry entry rather
46+
// than from hardcoded coordinates, so a registry edit flows into the assertions
47+
// instead of silently diverging from them.
48+
//
49+
// They deliberately do not assert the stock-adoption contract — that the target
50+
// recipe enables the component, the Dynamo descendant declines it, and the
51+
// generation-time opt-out works. A synthetic single-component fixture cannot
52+
// see any of that, so flipping the target ref to `install: false` would leave
53+
// every assertion here green. That contract is pinned separately by
54+
// TestK8sAIBOMStockAdoption in pkg/recipe.
4455
const (
4556
k8sAIBOMComponentName = "k8s-aibom"
4657
k8sAIBOMValuesFile = "components/k8s-aibom/values.yaml"

pkg/cli/query_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,14 +342,21 @@ func TestRecipeAndQueryCommandsRejectInvalidRuntimeInventoryMode(t *testing.T) {
342342
// TestRecipeCommandRejectsRuntimeInventoryWithoutComponent covers the flag-set
343343
// branch reaching a successful parse and then failing closed at build time,
344344
// which is the path an operator hits after a typo in --service.
345+
//
346+
// The criteria must name a recipe that does not declare k8s-aibom. Inference on
347+
// gke/h100/cos is the stock-adoption target under ADR-019's amendment and now
348+
// declares it, so this asserts against a training recipe instead. Mirrors the
349+
// same precondition in pkg/client/v1's TestResolveRecipeRuntimeInventoryMode.
345350
func TestRecipeCommandRejectsRuntimeInventoryWithoutComponent(t *testing.T) {
346351
err := recipeCmd().Run(t.Context(), []string{
347352
"recipe", "--service", "gke", "--accelerator", "h100",
348-
"--os", "cos", "--intent", "inference",
353+
"--os", "cos", "--intent", "training",
349354
"--runtime-inventory", "disabled",
350355
})
351356
if err == nil {
352-
t.Fatal("command error = nil, want rejection for a recipe that does not declare the component")
357+
t.Fatal("command error = nil, want rejection for a recipe that does not declare " +
358+
"the component; if these criteria now declare k8s-aibom, pick criteria that " +
359+
"do not rather than relaxing this assertion")
353360
}
354361
if !stderrors.Is(err, errors.New(errors.ErrCodeInvalidRequest, "")) {
355362
t.Errorf("command error = %v, want ErrCodeInvalidRequest", err)

pkg/client/v1/aicr_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,8 +2573,10 @@ func TestResolveRecipeRuntimeInventoryMode(t *testing.T) {
25732573
{name: "invalid mode is rejected", mode: "off", wantErr: true},
25742574
{name: "empty mode is rejected", mode: "", wantErr: true},
25752575
{
2576-
// Valid value, but no stock recipe declares the component, so the
2577-
// build must refuse rather than record a mode it cannot apply.
2576+
// Valid value, but these criteria do not declare the component,
2577+
// so the build must refuse rather than record a mode it cannot
2578+
// apply. The baseline assertion above fails loudly if a future
2579+
// overlay change makes these criteria declare it.
25782580
name: "valid mode on a recipe without the component is rejected",
25792581
mode: "disabled", wantErr: true,
25802582
},

0 commit comments

Comments
 (0)