Skip to content

feat(examples): add a Fabric load client for GPU autoscaling runs - #14192

Merged
cv merged 1 commit into
v1from
feat/gpu-autoscaling-load-client
Nov 4, 2026
Merged

cv merged 1 commit into
v1from
feat/gpu-autoscaling-load-client

Conversation

@cv

@cv cv commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Failure

The GPU autoscaling example has no way to load the chart's GPUs through the agents, which is what makes its autoscaler scale.
#14009 had load clients, but they sent each prompt with kubectl exec into the sandbox pods, around the gateway's authentication, and read pod names and runtime IDs from NemoClaw's terraform.tfstate.
With the development profile that example uses, nobody but NemoClaw can authenticate to the gateway, so no supported client could reach the agents.

Decision

Follows #14191 (merged), on which this was stacked.

  • examples/kubernetes/gpu-autoscaling/load.sh, a Bash script with the OpenShell CLI, curl and jq. It reads nemoclaw apply -o json and sends PROMPTS prompts across the agents, at most CONCURRENCY at a time through xargs -P, then prints how many it sent and how many replied, the median, 95th-percentile and longest latency overall and per sandbox, up to 10 failures, and one reply per sandbox. It exits nonzero if any request failed. Each sandbox takes one of the two paths that need no state files or kubectl exec:
    • Hermes's API server, when apply lists an api interface and an api service URL for the sandbox: OpenAI chat completions to serviceUrls.SANDBOX.api, through the gateway's port forward and with its client certificate. The script reads the interface token once, with openshell sandbox exec … cat and the reported tokenPath, into a private temporary file.
    • fabric-agent-ctl invoke for any other sandbox, such as OpenClaw or Deep Agents, through openshell sandbox exec with agents.SANDBOX.runtimeId (feat(sdk): report each agent's runtime ID in plan and apply results #14186).
  • two-runtimes.yaml: a Hermes sandbox on the chart's vLLM release and an OpenClaw sandbox on its Ollama release, the two that install-two-runtimes.sh installs (docs(examples): run two GPU runtimes and read runtime IDs from apply #14191). It uses the oidc profile, and declares Hermes's api interface with a serviceExposures entry using BEARER_PASSTHROUGH, because OpenShell's default STRIP drops the token Hermes checks. Its Envoy Service names are the ones Envoy Gateway v1.8.3 created for those Gateways on kind; it shortens a Gateway's name to 48 characters before the hash.
  • README.md: Drive Load Through the Agents, with the apply and run commands, the variables, and the limits: the gateway's one CPU for sandbox exec, Fabric's one invocation at a time per runtime, Hermes's 10 concurrent runs per sandbox, what a service URL skips, and that the CLI doesn't renew a client-credentials token (bug(cli): client-credentials gateway login stops working when the access token expires OpenShell#4287).

From #14009's e2e-fabric-load-test.py and client scripts, this keeps the prompts, the per-request timeout, the success check on result.status and result.output.response, and per-sandbox counts. It leaves out the HPA polling and the stop-at-ceiling logic, which belong to the chart and kubectl get hpa --watch; the OpenClaw tool pinning, which edited Fabric's generated configuration inside the sandbox; and the timeline and CSV output.

Validation

  • cargo ci on c6a581a9cf: all selected steps passed in 135 s; test ran 1,827 tests and lifecycle 101. cargo test -p nemoclaw-onboarding two_runtimes ran the new file's onboarding journey.
  • shellcheck 0.11 (koalaman/shellcheck:stable) on load.sh: no findings.
  • Live run on a local kind cluster (kind v0.33.0, kindest/node:v1.37.0, Agent Sandbox 1.0.3, linux/arm64, no GPU), with resources I created and removed afterwards:
    • Model: the chart from nemoclaw-community cb46db83ab installed with helm, with Ollama and qwen3:0.6b on CPU, no GPU request or node selector, no ServiceMonitor or HPA, arm64 digests for Ollama 0.40.11 and node:22-bookworm-slim, behind Envoy Gateway v1.8.3. Both sandboxes used its one Envoy Service, with model: qwen3:0.6b.

    • Issuer: Keycloak 26.4 in the cluster, at https://<kind node IP>:30443/realms/openshell with a test CA, so that the gateway pod and the host see the same issuer URL, with a confidential client whose service account has the admin role in a roles claim and audience openshell. The kind HCL cases' issuer serves only discovery and keys, with no token endpoint, so the OpenShell CLI can't sign in through it.

    • Deployment: two-runtimes.yaml with the context, issuer, endpoints, model and image digests replaced, and openclaw and hermes built for linux/arm64 and pushed to a registry on the kind network. nemoclaw apply -o json with the bundle from this branch succeeded, and reported serviceUrls.user-0000.api as https://nc-…--user-0000--api.openshell.localhost:8080/, both runtime IDs under agents, and the Hermes token path under interfaces.

    • CLI: the port forward and client certificate from docs/kubernetes.md, then openshell gateway add … --oidc-client-id with OPENSHELL_OIDC_CLIENT_SECRET (OpenShell 0.1.6): Authenticated via client credentials; openshell status showed Authenticated (OIDC).

    • Load: PROMPTS=12 CONCURRENCY=4 load.sh apply.json:

      Sending 12 prompts to 2 sandboxes, at most 4 at a time:
        user-0000: Hermes API at https://nc-…--user-0000--api.openshell.localhost:8080/
        user-0001: fabric-agent-ctl invoke, runtime runtime-1793794887745-82-2
      
      Sent 12 prompts in 238 s: 12 replied, 0 failed.
      Latency of replies: p50 51.9 s, p95 156 s, max 156 s.
      
        user-0000 (api): 6/6 replied; p50 16.9 s, p95 87.6 s, max 87.6 s
        user-0001 (invoke): 6/6 replied; p50 80.7 s, p95 156 s, max 156 s
      
        reply #1 user-0000: A Kubernetes HorizontalPodAutoscaler is a service that automatically scales its pods based on metrics.
        reply #2 user-0001: The current system state shows no active exec sessions or subagents, indicating no ongoing operations are in progress. To proceed, please check your system or c
      

      An earlier run of 8 prompts also got 8 replies. OpenClaw's replies don't answer the prompts, likely because of qwen3:0.6b with OpenClaw's tools; each invocation still reported succeeded with a response.

    • Failures: with TIMEOUT=5, both requests were reported as failed (curl: (28) Operation timed out and timed out after 5 s), and the script exited 1.

  • Not run: GPUs, autoscaling, vLLM, NIM, and Deep Agents. The Deep Agents path is the same fabric-agent-ctl invoke command as OpenClaw's.
  • Deferred to CI: cargo ci live-kind and live-docker; the change adds no image or Kubernetes code.

load.sh sends prompts to every agent that apply -o json reports, at most
CONCURRENCY at a time, and reports each request's latency and errors.
Hermes sandboxes get chat completions at their api service URL with the
interface token read once through openshell sandbox exec; other sandboxes
get fabric-agent-ctl invoke through openshell sandbox exec with the
reported runtime ID. two-runtimes.yaml declares a Hermes and an OpenClaw
sandbox on the chart's two releases, with the oidc profile and Hermes's
API published through a BEARER_PASSTHROUGH service exposure.

Co-authored-by: maggiezha <[email protected]>
@cv

cv commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author
Authoring session — opening
Field Status Value Evidence or limit
Harness recorded Pi 1.5.0 Session metadata and installed release
Skills recorded nemoclaw-contributor-create-pr (complete read) Skill reads to cutoff
Instruction files recorded AGENTS.md (injected); WRITING.md (complete read); .github/PULL_REQUEST_TEMPLATE.md (complete read); .github/SESSION_REPORT_TEMPLATE.md (complete read) Startup injection and reads to cutoff
Tool calls partial typescript: 129 Whole session, which also wrote #14191; includes failed calls; the typescript wrapper hides its nested shell, git and workspace calls
Session window recorded 2026-11-04T12:03Z to 2026-11-04T12:43Z; 41 min First event to cutoff, including idle time
Compactions recorded 0 Complete record to cutoff
Models recorded claude-opus-5-5 Model change events; no changes
Reasoning levels recorded high Thinking-level events; no changes

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

@cv
cv enabled auto-merge (squash) November 4, 2026 12:55
@cv
cv merged commit b5489e5 into v1 Nov 4, 2026
56 of 57 checks passed
@cv
cv deleted the feat/gpu-autoscaling-load-client branch November 4, 2026 13:00
@wscurran wscurran added area: docs Documentation, examples, guides, or docs build area: performance Latency, throughput, resource use, benchmarks, or scaling area: security Security controls, permissions, secrets, or hardening feature PR adds or expands user-visible functionality integration: hermes Hermes integration behavior integration: openclaw OpenClaw integration behavior platform: k8s Affects Kubernetes deployments v1 NemoClaw v1 branch labels Nov 4, 2026
cv added a commit that referenced this pull request Nov 11, 2026
…ms (#14413)

The release notes now list every user-visible change on `v1` since the
`v0.1.0-rc9` tag, including the breaking ones, and four example pages
drop claims that later changes made false.

## Problem

`docs/about/release-notes.md` listed three breaking changes and none of
the features, fixes, and onboarding changes that reached `v1` after
`v0.1.0-rc9`; it didn't name the configuration shapes the parser now
rejects or the intent record version `v0.1.0-rc9` writes.
Some example claims went stale:

- `examples/spark/README.md` said the pinned OpenShell has the Podman
TLS initialization blocker, NVIDIA/OpenShell#3427; its fix merged before
OpenShell 0.1.0, and `v1` pins 0.1.7.
- `examples/station/README.md` and `examples/gtc-dc/README.md` required
exactly one visible GPU on the host; since #13929,
`container.gpuDevices` lets a service on a host with more GPUs request
one.
- `examples/hcl/travel-team` said Fabric's Deep Agents adapter can't
turn reasoning off, NVIDIA/NeMo-Fabric#433; NVIDIA/NeMo-Fabric#439 fixed
that, and the pinned catalog's Deep Agents `reasoning_effort` accepts
`none`.
- The travel team cited #13313 for the booking key its service doesn't
receive, but #13313 closed when `openshell_api` landed.

## Change

- `docs/about/release-notes.md` gains "Changes Since v0.1.0-rc9",
grouped by area, from the commits in `v0.1.0-rc9..origin/v1`; where a
later commit changed an earlier one, the entry describes the current
code, and changes added and removed after the tag aren't listed:
- Agents and interfaces: #14037, #14038, #14269, #13981, #14024, #14186,
#14052, #14385, #13971, #13858, #13947, #14039, #14042, #14044, #14343.
- Inference: #13929, #14391, #14396, #14398, #14400, #13963, #13912,
#13964, #14388, #14390, #14397.
- Gateways and Kubernetes: #13909, #13970, #13922 (an existing
namespace, refused at plan by #13756, is now bound; cluster-wide reads
dropped), #14014, #14169, #13921.
- Plan, apply, and diagnostics: #14224, #13756, #14141, #14133, #14135,
#13775, #13744, #13701.
- Onboarding: #13693, #13695, #13741, #13757, #13851, #13978, #14228,
#14233, #14334, #14336.
- Examples and builds: #14166, #14184, #14188, #14191, #14192, #14200,
#13889, #14267, #14312, #14315, #14355, b30e9b4, #13781, #13813,
#14246, and the pins in `versions.json` and `image/fabric/Dockerfile`.
- Its breaking changes since the tag:
- the intent record moving from 15, which `v0.1.0-rc9` writes, through
16 (#14266), 17 (#14343), 18 (#13929), 19 (#14391) and 20 (#14394);
  - name-keyed `sandboxes` and `inferenceProviders` (#13703, #13705);
- a recipe `compatibility.gpu` outside every hardware profile (#13912);
- model tuning the adapter doesn't accept (#14111), and Hermes's
`max_tokens` losing its default (#14296);
  - `skills.paths` resolution (#14296);
  - endpoint and `networkCIDR` validation (#13847, #14081);
- the HCL `gateway_values` attribute (#14014), and Kubernetes ownership
receipts from before #13922 and #14014.
  The two earlier items stay, under their own heading.
- `examples/spark/README.md` replaces the closed OpenShell#3427 blocker
with the Podman and remote-model limits in `docs/resources/limits.md`.
- `examples/station/README.md` and `examples/gtc-dc/README.md` require
one GPU visible in the model container, and point to
`container.gpuDevices` for a host with more.
- `examples/hcl/travel-team/README.md` and `WALKTHROUGH.md` drop the
NeMo-Fabric#433 limitation, and the travel team's booking-key statements
link #14417 instead of #13313.

Not listed, because both changes landed after the tag: Deep Agents Code
(#14043, #14250), `compatibility.gpuCount` (#14391, #14394), and the
"Creating infrastructure state" progress step (#14169, #14248).

Checked and left unchanged: the root `README.md`,
`crates/nemoclaw-onboarding/README.md`,
`crates/nemoclaw-cli/assets/README.md`, `internal-docs/README.md`, the
other example READMEs, and every example YAML header comment. The GPU
autoscaling README and `load.sh` read the JSON fields `OperationResult`
serializes, and NVIDIA/OpenShell#4287 is fixed only after the pinned
0.1.7. The two pre-tag release-note items still match the schema and
SDK. `docs/README.md` is left to #14412, which retires the stale field
reference snapshot it describes.

Not verified: whether Hermes sends the booking key's placeholder to MCP
servers; #14417 tracks the live check.

## Beyond CI

Each relative link and anchor in the changed files resolves to an
existing file and heading.
@wscurran wscurran added area: providers Inference provider integrations and provider behavior provider: ollama Ollama local model provider behavior provider: vllm vLLM local or hosted provider behavior labels Nov 13, 2026
cv pushed a commit that referenced this pull request Nov 13, 2026
…14427)

For Kubernetes users: `examples/kubernetes/hosted-nvidia.tf` now applies
with its default workspace, the Kubernetes guide lists the NetworkPolicy
enforcement that OpenShell requires, and the new
`examples/kubernetes/managed-oidc.yaml` is a complete document for a
managed gateway with the `oidc` profile and a registry pull Secret.

## Problem

- `hosted-nvidia.tf`, which `examples/gtc-dc/kubernetes-amd64.tf` links
to, defaults `workspace` to `gtc-dc-kubernetes-cloud`, 23 characters.
The pinned OpenShell refuses a workspace name over 19 characters
(`MAX_ROUTABLE_NAME_LEN`), and only when apply creates the workspace,
after the gateway is installed. The run recorded on #14315 hit this. The
HCL plan scenario passed, because plan doesn't create the workspace.
- The guide's prerequisites didn't say that OpenShell requires a network
plugin that enforces NetworkPolicy. OpenShell's NetworkPolicies are what
keep a sandbox's workload pod off the network. Kubernetes accepts them
without such a plugin, so nothing reports one that's missing.
- No example used the `oidc` profile on its own, or `imagePullSecrets`.
The only OIDC example, `gpu-autoscaling/two-runtimes.yaml`, depends on
the GPU chart. The guide also didn't say:
  - that the gateway token needs the admin role;
  - how to create the `caConfigMap`;
  - how the OpenShell CLI signs in where no browser can open;
  - which role lets the CLI reach the deployment's workspace.

## Change

- **`hosted-nvidia.tf`:**
  - Default `workspace` to `gtc-dc-k8s-cloud`.
- Add a `validation` block with OpenShell's name rule: at most 19
characters, a DNS label, and no `--`.
- `gtc-dc-kubernetes-amd64-hcl.tftest.hcl` plans the old default and
expects that validation to fail.
  - `examples/gtc-dc/README.md` states the limit.
- **Prerequisites (`kubernetes.md`):** add a network plugin that
enforces ingress and egress NetworkPolicy, linked to OpenShell's
requirement. Say that nothing reports a plugin that ignores
NetworkPolicy. Add it to the threat model's "Relies on" column.
- **`examples/kubernetes/managed-oidc.yaml`:**
- An OpenClaw and a Hermes agent on the isolated network tier, sharing
hosted NVIDIA inference.
- The `oidc` profile with `caConfigMap` and a token, and
`imagePullSecrets`.
  - The OIDC and registry sections of the guide link it.
- **OIDC section:**
  - Add the `kubectl create configmap` command for `caConfigMap`.
- The token needs the role `admin_role` names; without it, plan fails
with `observation permission denied`.
- When `scopes_claim` is set, the token also needs the same scopes as an
external gateway's token.
- **"Reach the Gateway from the OpenShell CLI":**
- Add the client-credentials sign-in (`OPENSHELL_OIDC_CLIENT_SECRET`),
with `openshell gateway login` to sign in again, because the pinned CLI
doesn't renew that token (NVIDIA/OpenShell#4287).
  - Cite #14192's kind run of it beside #14131.
- A token with the role `admin_role` names reaches the deployment's
workspace. A token with only the role `user_role` names needs a platform
admin to add its subject to the workspace first, and with only that role
`openshell status` reports `authorization denied`.

Left out:
- An OpenShift copy of the OIDC example, because #13828 plans to fold
the OpenShift examples into the Kubernetes ones.
- Agent interfaces in the example, which no run here checked.
- A `managed-oidc.yaml` row in `examples/kubernetes/README.md`, which
#14428 adds; whichever of the two merges second adds the row.
- Correcting the guide where it says the gateway requires its client
certificate. The pinned gateway asks for one but doesn't require it: in
the run below, a call with only an OIDC token succeeded, and the CLI
worked with only the gateway's CA in its `mtls` directory. That fix
touches text outside this change.

## Beyond CI

- **`managed-oidc.yaml` on k3s:** applied on single-node k3s
v1.37.1+k3s1 with Agent Sandbox 1.0.3, the `local-path` StorageClass,
and k3s's NetworkPolicy controller.
  - Setup:
    - Images built from c5bf74d and pushed to a loopback registry.
    - Hosted NVIDIA inference.
- The e2e test issuer in another namespace, with its CA in the
`issuer-ca` ConfigMap.
- The example with only its UID, context, namespace, issuer settings and
image digests changed.
  - Results:
- Plan and apply succeeded, and both agents replied through the gateway.
- `registry-credentials` was on the gateway, supervisor and sandbox
pods.
    - A second plan found no changes.
- Destroy succeeded and kept the namespace, the gateway's volume and its
Secrets.
- **Token without the admin role:** on the same deployment, a token
re-signed with only the user role made plan fail with `observation
permission denied`.
- **`hosted-nvidia.tf` on k3s, with its new default workspace:** the
same cluster and images, with OpenTofu and the providers from a bundle
built at c5bf74d.
- Setup: the two claims prepared and labelled as
`examples/gtc-dc/README.md` says. Only the provider versions, UID,
context, namespace and images were set; `workspace` kept its default.
- Init, validate, plan and apply succeeded, and OpenShell created the
workspace `gtc-dc-k8s-cloud`.
- OpenClaw and Hermes each replied `READY` through `fabric-agent-ctl
invoke`, and each had its claim's `workspace/` and `artifacts/` mounted
and writable.
  - A second plan found no changes.
- The README's targeted destroy removed the gateway and both agents, and
kept the storage resource and both claims.
- **OpenShell CLI sign-in on a three-node cluster:** k3d v5.9.0 with k3s
v1.37.1+k3s1 and Agent Sandbox 1.0.3, with Keycloak 26.4 in the cluster
as the issuer and three confidential clients. The gateway was #14428's
`published-nodeport.yaml`, which uses the same `oidc` profile;
NemoClaw's own token came from one of the clients.
- The guide's steps with the pinned CLI 0.1.7: the client certificate
copied from `<release>-client-tls`, `SSL_CERT_FILE` set to the issuer's
CA, then `openshell gateway add` with `OPENSHELL_OIDC_CLIENT_SECRET`.
The CLI printed `Authenticated via client credentials`, and `openshell
status` reported `Connected` and `Authenticated (OIDC)`.
- This worked both through the port forward to the gateway pod and at
the published endpoint.
- With the admin role, `openshell sandbox list` listed the agent, and
`openshell sandbox exec … fabric-agent-ctl invoke` got `READY`.
- With only the user role, `openshell status` reported `Authenticated
(OIDC; authorization denied)`, and sandbox commands failed with `not a
member of workspace`. After `openshell workspace member add … --role
user`, the same client listed and invoked the agent, and a NemoClaw plan
still found no changes.
- **Not run:**
  - The browser sign-in: no browser here. #14131 records one.
  - A cluster whose network plugin doesn't enforce NetworkPolicy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Hai Nguyen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Documentation, examples, guides, or docs build area: performance Latency, throughput, resource use, benchmarks, or scaling area: providers Inference provider integrations and provider behavior area: security Security controls, permissions, secrets, or hardening feature PR adds or expands user-visible functionality integration: hermes Hermes integration behavior integration: openclaw OpenClaw integration behavior platform: k8s Affects Kubernetes deployments provider: ollama Ollama local model provider behavior provider: vllm vLLM local or hosted provider behavior v1 NemoClaw v1 branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants