Skip to content

k8s-aibom: validate h100-gke-cos-inference end to end on real GKE #2310

Closed

Description

@mchmarny

Part of #2271.

Goal

Prove the full AICR workflow for k8s-aibom end to end on a real GKE cluster with H100 GPUs, using h100-gke-cos-inference as the target environment: generate the recipe, render the bundle, deploy it, pass validation, and produce a genuine CycloneDX ML-BOM from a real GPU inference workload.

This satisfies ADR-019's Follow-Up Decisions requirement for managed-cluster qualification and measured controller/API-server cost, which is the one requirement in that list that cannot be met by inspection or by a simulated cluster.

Why this is not covered by what already exists

  • tools/k8s-aibom-test/ runs on Kind. Kind is a useful behavioral proof and it is what qualified the component, but it is a simulation of a managed cluster, not one. The control plane, node images, GPU stack, and API-server behavior all differ.
  • The operational envelope recorded in ADR-019 (14s convergence at 1,000 workloads, 230.4 mCPU / 47.5 MiB burst) was measured on Kind. GKE's managed control plane is the thing that actually needs qualifying.
  • Registry adoption ([Epic]: k8s-aibom — registry-only component adoption (ADR-019) #2234) deliberately proved the component is safe to offer. It proved nothing about a real cluster running it under a real workload.

Explicitly not a stock-recipe change

Per the ADR-019 amendment, registry presence asserts the component is qualified and offerable; stock recipe presence asserts AICR ships it by default, and that is gated on requalification plus the remaining Follow-Up requirements.

This issue runs from a custom overlay or an unmerged branch. It does not modify recipes/overlays/h100-gke-cos-inference.yaml on main, and no stock recipe changes. That keeps the demonstration honest about what has and has not been committed to.

Scope

On a live GKE cluster with H100 nodes:

  1. aicr recipe for service: gke, accelerator: h100, os: cos, intent: inference, with k8s-aibom declared in a custom overlay.
  2. aicr bundle renders through every supported deployer; confirm the rendered output is what actually deploys.
  3. Deploy the bundle and confirm the component installs cleanly against the managed control plane, including CRD application (see below).
  4. aicr validate passes, including the k8s-aibom health check — the Deployment rollout terms, the AIBOMControllerConfig Ready condition, and the CRD storage-version assertions added in feat(recipes): assert k8s-aibom CRD storage version in the health check #2305.
  5. Label a namespace aibom.k8saibom.dev/enabled=true, run a real GPU inference workload, and confirm a correct CycloneDX 1.6 ML-BOM is produced for it. Inline vs. truncated behavior at the 256 KiB threshold should be observed rather than assumed.
  6. Measure controller CPU/memory and API-server impact on GKE, and compare against the Kind-measured envelope in ADR-019. A material divergence is a finding, not a footnote.

Depends on

  • k8s-aibom: requalify and re-pin against the v1beta1 graduation release #2282 (requalify and re-pin to v1.3.0) — run against the qualified pin, not a stale one.
  • The CRD-apply step: the registry now pins chart 1.3.0, whose CRDs differ from 1.2.0. On Helm, Helmfile, and Flux the CRDs must be applied before the bundle upgrade (see the component catalog). Whether that step is needed here depends on whether the cluster is fresh; a fresh install applies crds/ automatically.

Out of scope

  • Merging k8s-aibom into the stock h100-gke-cos-inference overlay. That is the epic's later step and needs the remaining Follow-Up requirements.
  • Upgrade, rollback, and uninstall evidence — tracked separately; it gates the overlay merge, not this.
  • Any AIBOM-as-AICR-evidence integration (ADR-007). Still a separate decision.

Done when

  • Every step above is executed on a real GKE cluster and the commands and outputs are recorded on this issue.
  • A real ML-BOM produced from a real inference workload is attached or quoted.
  • Measured controller and API-server cost is recorded and compared to the ADR-019 envelope.
  • Any divergence from the Kind-based expectations is written down, including "none observed" if that is the result.

Activity

  1. self-assigned this
    on Aug 20, 2026
  2. added theissue type on Aug 20, 2026
  3. mchmarny commented on Aug 20, 2026

    @mchmarny
    MemberAuthor

    Workload runtime: use Triton or vLLM, not NIM

    Recording a decision so it is not rediscovered on the cluster.

    Upstream flagged that k8s-aibom's runtime attribution has named patterns for Triton (including the trtllm variants) and vLLM, but not for NIM (nvcr.io/nim/*). A NIM-served workload would produce weaker runtime attribution than their 1,000-workload qualification showed. That is a pattern gap on their side, not a detection failure, and they have offered a same-day pattern addition if we hit it.

    This does not constrain the recipe, because h100-gke-cos-inference ships no serving runtime. Its resolved components are gpu-operator, nvsentinel, nvidia-dra-driver-gpu, nodewright-operator, nodewright-customizations, kube-prometheus-stack, and nfd. It is the GPU platform. The inference workload in step 5 above is one we choose and deploy, so the choice is entirely ours and costs nothing.

    Decision: serve the pilot workload with Triton or vLLM. Both are covered patterns, so runtime attribution in the resulting ML-BOM reflects the component working as intended rather than a known pattern gap. There is no nvcr.io/nim image anywhere in this recipe's rendered BOM, so nothing has to be avoided — just chosen deliberately.

    If the workload changes to NIM for any reason, expect weaker attribution and send the image refs upstream rather than filing it as a defect.

    Related gaps, tracked here so they are not lost

    Neither blocks this issue.

    • NIM attribution matters to AICR beyond this pilot. Four recipes use k8s-nim-operator: h100-eks-ubuntu-inference-nim, rtx-pro-6000-eks-ubuntu-inference-nim, ocp-inference-nim, and ocp. Anyone pairing k8s-aibom with those hits the same gap regardless of what happens here. Worth asking upstream to add the pattern on its own merits.
    • Dynamo coverage is unverified and has a larger footprint. dynamo-platform appears in 13 overlays, more than three times NIM's. It orchestrates vLLM, TRT-LLM, or SGLang backends, but the served container is selected at runtime rather than chart-pinned, so our BOM cannot tell us whether a Dynamo-served pod matches upstream's existing patterns. Upstream's heads-up covered Triton and vLLM as runtimes and did not mention Dynamo either way.
  4. mchmarny commented on Aug 20, 2026

    @mchmarny
    MemberAuthor

    Live GKE run: core claim proven

    Executed on a real GKE cluster, aicr-uat-day-gh1-0-32392558596 (us-central1), Kubernetes v1.35.6-gke.1250000, 6 nodes with 2 × NVIDIA-H100-80GB-HBM3 (nvidia-h100-mega-80gb, 8 GPUs each). AICR inference stack already deployed: gpu-operator, kai-scheduler, nvsentinel, nfd, nvidia-dra-driver, cert-manager, kube-prometheus-stack, dynamo-system.

    Steps 1-3: generation, bundling, install

    k8s-aibom was added through a custom overlay, not a stock recipe change, exactly as the ADR-019 amendment requires. The stock recipe resolved 14 components; adding a narrow overlay scoped to service: gke, accelerator: h100, os: cos, intent: inference produced 15.

    The resolved pin is the requalified set from #2282:

    CHART='oci://ghcr.io/googlecloudplatform/charts/k8s-aibom'
    VERSION='1.3.0'
    image digest: sha256:f8e48d4edc44e6ee8e40a2ac6c5f60b190aa18d411a75702dc5798a77a039e8d
    

    Install pulled chart digest sha256:4ffa933e272a977e0b60f2eca1c4326176e6196e2ee69e1bf4f72c8b5a511c90, matching the qualified value exactly. Rendered values carried the secure defaults: no sinks, empty discovery map, 256 KiB inline threshold. Deployment reached 1/1.

    Step 4: the CRD storage-version assertion, verified against a real cluster

    This is the first real-world exercise of the assertion added in #2305 and flipped in #2309:

    aiboms.aibom.k8saibom.dev                  storage=v1beta1  served=v1alpha1,v1beta1
    aibomcontrollerconfigs.aibom.k8saibom.dev  storage=v1beta1  served=v1alpha1,v1beta1
    

    Both match what the health check asserts. AIBOMControllerConfig/default reported Ready=True with observedGeneration == generation.

    Step 5: a real ML-BOM from a real GPU workload

    Namespace aibom-pilot labeled aibom.k8saibom.dev/enabled=true, running vLLM on an H100 (vllm/vllm-openai:v0.6.3, facebook/opt-125m, 1 GPU, pod 1/1 Running).

    NAME                           WORKLOAD     NAME           CATEGORY    RUNTIME   READY
    apps-deployment-vllm-opt125m   Deployment   vllm-opt125m   inference   vllm      True
    

    The emitted CycloneDX 1.6 document (inline, 2776 bytes, sha256:3c249239…):

    Field Value
    Tool k8s-aibom v1.3.0
    Workload ref workload:aibom-pilot/Deployment/vllm-opt125m
    Confidence inferred
    Scraper inference.spec

    Components:

    application               vllm                  v0.6.3
    container                 vllm/vllm-openai      v0.6.3
    machine-learning-model    facebook/opt-125m
    

    The model was extracted from the container args rather than declared, which is the substantive capability this pilot set out to demonstrate. Runtime attribution resolved to vllm, consistent with upstream's statement that vLLM is a covered pattern.

    One behavioral note worth recording: the AIBOM was generated while the pod was still Pending on a scheduling failure. Generation reads the workload spec, not running pods, so a workload that never schedules still produces inventory.

    Finding: aicr validate cannot schedule its orchestrator Job on tainted GPU nodes

    aicr validate against the full recipe failed:

    [TIMEOUT] job failed (auto-injected node selector nvidia.com/gpu.present=true — if no GPU
    nodes are schedulable, target a GPU node explicitly, e.g. --node-selector ...): [TIMEOUT]
    job completion timeout: context deadline exceeded
    

    The GPU nodes on this cluster carry dedicated=gpu-workload:NoSchedule. The orchestrator Job gets an auto-injected nvidia.com/gpu.present=true node selector, so it targets exactly those tainted nodes and cannot schedule. My vLLM Deployment hit the same wall until given a matching toleration.

    The suggested remedy does not apply to the failure. aicr validate --help states for both flags:

    --node-selector ... Does not affect the validator orchestrator Job.
    --toleration ... Does not affect the validator orchestrator Job.

    So the error recommends --node-selector for a failure in the one component that flag explicitly cannot influence, and there is no flag that adds a toleration to the orchestrator Job. Tainting GPU nodes is a common pattern, so this is likely to be hit by real users rather than being specific to this UAT cluster.

    Worth filing separately. It does not block this issue: steps 1-5 were verified directly with kubectl and helm, and the health-check assertions were confirmed by hand against live cluster state.

    Remaining for this issue

    • Step 6: measured controller and API-server cost on GKE, compared against ADR-019's Kind-measured envelope.
    • Inline vs truncated behavior at the 256 KiB threshold, observed rather than assumed.
  5. mchmarny commented on Aug 20, 2026

    @mchmarny
    MemberAuthor

    Step 6: measured cost on GKE, and the truncation threshold

    Same cluster, same install. All figures from kubectl top against the live controller.

    Controller cost

    State CPU Memory
    1 workload (quiet) 1m 23-24Mi
    51 workloads 10m 32Mi
    Peak, during large-BOM generation 2m 48Mi

    Convergence: 51 AIBOMs in 24 seconds from a single kubectl apply, all reaching Ready=True, all attributed runtime=vllm.

    Deployed requests and limits match ADR-019 exactly: requests 50m/128Mi, limits 1 CPU/256Mi.

    Compared to the ADR-019 Kind envelope

    ADR-019 recorded quiet CPU mean/p95/max of 17.82/24.88/32.10 mCPU with memory 47.98/48.98/48.98 MiB, and a 1,000-workload run converging in 14s at 230.4 mCPU / 47.5 MiB burst.

    No material divergence. Every GKE measurement sits at or below the corresponding Kind figure, and peak memory (48Mi) lands exactly on the Kind quiet memory number. Extrapolating the 51-workload CPU figure linearly to 1,000 workloads gives roughly 200 mCPU against Kind's measured 230.4 mCPU burst, so the envelope holds on a managed control plane. Observed usage stays well inside the configured requests, so the 50m/128Mi request is not tight.

    Scale here was 51 workloads rather than 1,000, so this confirms the envelope rather than re-deriving it. Recording the difference rather than implying equivalence.

    Inline vs truncated at the 256 KiB threshold

    Observed directly by growing a workload's spec until the document crossed the boundary:

    BOM size Result
    2,776 bytes inline
    137,859 bytes inline
    228,072 bytes inline
    286,902 bytes truncated

    Above the threshold the inline key is absent and the status carries truncated: true, the byte size, the sha256 of the full document, and a reason:

    BOM size 286902 bytes exceeds inline threshold 262144 bytes and no external sink is configured. Configure a GCS or webhook sink in AIBOMControllerConfig to retrieve the full BOM.

    Conditions stay honest about what happened rather than reporting failure:

    Ready=True       reason=BOMGenerated    msg=BOM generated successfully
    Synced=True      reason=CRDStatusOnly   msg=no external sinks configured; CR status is the sole sink
    SinkFailed=False reason=AllSinksOK      msg=no sink failures
    Stale=False      reason=BOMGenerated    msg=controller reconciled this AIBOM during the current cycle
    

    The document is still identified by digest and size when truncated, so an operator can tell that inventory exists and is unretrievable, rather than it silently appearing absent. This is ADR-019's documented behavior for the sinkless secure default, now observed rather than assumed.

    Garbage collection

    Deleting the 52 synthetic workloads dropped the AIBOM count from 53 to 1 without intervention, so controller-owned resources are reclaimed when their workload goes away.

    Status of this issue

    Steps 1-6 are complete and evidenced. Remaining known gap: aicr validate could not run on this cluster because its orchestrator Job cannot tolerate tainted GPU nodes (detailed in the previous comment). Health-check assertions were verified by hand against live cluster state instead.

  6. mchmarny commented on Aug 20, 2026

    @mchmarny
    MemberAuthor

    Correction: the model is declared, not inferred

    Upstream flagged a terminology error in my write-up above, and the BOM I captured proves them right. I wrote:

    The model was extracted from the container args rather than declared

    That is backwards in k8s-aibom's confidence model. Reading the emitted document back rather than relying on the summary row:

    Component aibom.confidence aibom.evidence.source Locator
    machine-learning-model facebook/opt-125m declared container_arg spec.template.spec.containers[0].args[0 1](--model)
    application vllm (runtime) inferred image_pattern ^vllm/.*
    container vllm/vllm-openai unresolved image_reference —

    A --model flag is the workload declaring its own model, the same tier as HF_MODEL_ID, and the component carries the arg locator as evidence. inferred is reserved for what the controller deduces, such as runtime from an image pattern — which is exactly what the vllm application component shows.

    The document-level aibom.confidence = inferred is the aggregate across components and is what I misread as applying to the model.

    The capability demonstrated is still the interesting one, just named correctly: the controller resolved a declared model identity out of the workload spec with a precise evidence locator, and separately inferred the runtime from the image. Two different tiers, and the document distinguishes them.

    No AICR documentation carries this error; it was confined to the comment above.

  7. mchmarny commented on Aug 21, 2026

    @mchmarny
    MemberAuthor

    Evidence: end to end on real GKE

    GKE aicr-uat-day-gh1, K8s v1.35.6-gke.1250000, 2x a3-megagpu-8g (16x H100), converted to the h100-gke-cos-inference component set. Run from unmerged branch feat/aibom-stock-overlay-gke-inference (#2328). No stock recipe on main changed.

    1. Recipe selection. --service gke --accelerator h100 --os cos --intent inference resolves 15 components including k8s-aibom. --runtime-inventory disabled records configuration.runtimeInventory.mode: disabled, sets install: false, and the bundler then omits the component.

    2. Deployer rendering. All five staged and version-pinned correctly. Deployed and observed on helm, helmfile, flux. argocd and argocd-helm rendered but not deployed — neither controller was installed here.

    3. Install. Fresh install of chart 1.3.0 via the stock helm bundle applied crds/ automatically: both CRDs v1beta1 storage, storedVersions ["v1beta1"], controller 1/1. An upgraded cluster shows ["v1alpha1","v1beta1"]; a fresh install shows ["v1beta1"] alone.

    4. Validation.

    rc=0
    [chainsaw] k8s-aibom: health check passed
    phase=deployment status=passed / phase=conformance status=passed
    

    5. ML-BOM from a real GPU workload. Namespace aibom-inference labeled aibom.k8saibom.dev/enabled=true, vLLM serving Qwen/Qwen2.5-0.5B-Instruct on one H100. AIBOM/apps-deployment-vllm-qwen: Ready/Synced True, SinkFailed/Stale False.

    CycloneDX 1.6 validation passed (3 components)
    published sha256 == sha256(emitted bytes)   # b9aafbef...082d98
    
    Type Name
    application vllm
    container vllm/vllm-openai:latest
    machine-learning-model Qwen/Qwen2.5-0.5B-Instruct

    Inline vs truncated, observed rather than assumed: 2,885 bytes against the 262,144-byte threshold.

    6. Cost vs the ADR-019 envelope. Replicas-zero fixtures, ADR-019's methodology.

    Workloads GKE CPU GKE memory Kind (ADR-019)
    1 1 mCPU 26 MiB
    251 1 mCPU 42 MiB
    1,001 1-2 mCPU steady 67-69 MiB 230.4 mCPU / 47.5 MiB
    • Convergence faster than Kind: all 1,001 AIBOMs present by t+10s, vs Kind's 14s.
    • Memory diverges ~45% high at 1,000 workloads, 68 vs 47.5 MiB. Still ~53% of the declared 128Mi request, so the envelope holds, but the Kind figure understates a managed cluster. Growth is sublinear across the three points; three samples do not support extrapolation.
    • No CPU peak claimed. The first sample landed post-convergence, and metrics-server cannot resolve a sub-15s burst. Steady state is 1-2 mCPU, and no sample approached the 50m request.
    • No leak: deleting the 1,000 fixtures garbage-collected all 1,000 AIBOMs and memory returned 68 to 29 MiB.

    Not covered: argocd/argocd-helm deployment; AIBOM as ADR-007 evidence, out of scope per ADR-019.

  8. mchmarny commented on Aug 21, 2026

    @mchmarny
    MemberAuthor

    API-server impact (completes step 6)

    Measured via apiserver_request_total{resource=~"aiboms|aibomcontrollerconfigs"}, which is directly attributable since nothing else on the cluster touches those resources. Read through Prometheus rather than kubectl get --raw /metrics, because the latter round-robins across apiserver replicas and its per-replica counters make deltas meaningless.

    Across a 0 to 1,001 workload burst:

    Requests on aibom resources 1,999 (~2 per inventoried workload)
    Total apiserver requests, same window 17,183
    Controller's attributable share 11.6%
    Long-running watches on aibom resources 4

    The remaining ~88% is the deployment and replicaset controllers reacting to 1,000 new Deployments, not this component.

    Two limits worth stating: the controller's informer watches on Deployments, Pods, Jobs and DaemonSets are not separable from kube-controller-manager's, since those resources are shared. And the shipped chart exposes no metrics endpoint and no option to enable one, so per-client attribution is unavailable by design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions