Skip to content

feat(nrl-k8s): Kubernetes infrastructure and CLI tooling - #2330

Merged
terrykong merged 76 commits into
mainfrom
hemil/k8s-infra-cp-part1
Apr 28, 2026
Merged

feat(nrl-k8s): Kubernetes infrastructure and CLI tooling#2330
terrykong merged 76 commits into
mainfrom
hemil/k8s-infra-cp-part1

Conversation

@terrykong

@terrykong terrykong commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • infra/: Kind cluster setup, Helm values (KAI, KubeRay, JobSet), K8s example manifests (monolithic RayJob, disagg RayClusters, JobSets, KAI topology/queues)
  • tools/nrl_k8s/: Full CLI for launching NeMo-RL recipes on Kubernetes — schema validation, manifest building, DRA auto-management, dev pods, secret management, stale resource detection
  • skills/: Claude Code skill for launching/monitoring K8s workloads

Closes RL-417

Test plan

  • cd tools/nrl_k8s && uv run --extra test pytest -x -q
  • nrl-k8s run <recipe> --infra <infra> --dry-run produces valid manifest
  • nrl-k8s dev connect / nrl-k8s dev stop lifecycle works

terrykong and others added 30 commits April 23, 2026 19:30
Local K8s GPU dev environment using nvkind (NVIDIA's kind wrapper):
- nvkind cluster setup scripts (install-nvkind.sh, create-cluster.sh)
- Custom config template with extraMounts for dev code mounting
- Helmfile with kind/prod environments (device plugin vs GPU operator)
- KAI scheduler for gang scheduling, KubeRay for RayCluster management
- Example manifests: gang-scheduled pods, RayClusters, SFT RayJobs
- SETUP.md with prerequisites, quick start, and architecture docs

Tested: SFT RayJob (train/loss 4.06 < 5.9), KAI all-or-nothing
gang scheduling, two simultaneous 1-GPU SFT jobs.

Signed-off-by: Terry Kong <terryk@nvidia.com>
…overy (infra part)

Signed-off-by: Terry Kong <terryk@nvidia.com>
When RL and Gym run on separate RayClusters, either cluster failing or
being deleted triggers teardown of both clusters to release resources.

- peer-watcher.py: pure Python sidecar (no deps beyond stdlib), deployed
  as a ConfigMap volume mount on each head pod
- Monitors peer RayCluster status via K8s API (polls every 10s)
- Tears down after MAX_PEER_FAILURES (default 3) consecutive failures
- Also monitors ConfigMap "error" key for application-level error signaling
- Handles transient K8s API errors as failures (not false-healthy)
- Added signal_error() to K8sEndpointRegistry
- Updated disagg manifests with peer-watcher sidecar containers
- Updated RBAC with "delete" verb for rayclusters

Tested: deleting either cluster triggers teardown of both within ~10s.
Signed-off-by: Terry Kong <terryk@nvidia.com>
…share configs

- Kyverno policy: RayCluster/RayJob must have kai.scheduler/queue label.
  Validates at CRD level (not pod) since KubeRay operator creates pods.
  Optional Policy 2 for user→queue access control via ConfigMap.
- kube-prometheus-stack: Prometheus + Grafana for fairshare monitoring.
  Pre-built Grafana dashboard showing GPU allocation vs fair share,
  preemption events, and scheduling latency per queue.
- ServiceMonitors for KAI scheduler, binder, and queue-controller.
- Example queue configs:
  - kai-queue.yaml: 2-GPU kind cluster (2 teams, equal quotas)
  - kai-queue-prod.yaml: 256-GPU prod (3 departments, 6 teams)
  - preemptMinRuntime: 4h (protect long training runs from priority preemption)
  - reclaimMinRuntime: 15m (fast fairness reclaim of over-quota resources)
- SETUP.md: fairshare docs, preempt vs reclaim explanation, Grafana access.

Tested: Kyverno rejects RayCluster without queue label, accepts with.
Team A 2-GPU job reclaimed when Team B submitted to its guaranteed quota.

Signed-off-by: Terry Kong <terryk@nvidia.com>
- Upgrade KAI scheduler v0.13.4 → v0.14.0 (adds Ray topology-aware
  scheduling, segment-size annotation support for PyTorchJob)
- Update chart URL from NVIDIA/KAI-Scheduler to kai-scheduler/KAI-Scheduler
- Fix Grafana dashboard metric names (add kai_ prefix to match actual
  Prometheus metric names). Verified: Grafana queries return live data.
- New: extensions/k8s_cli/ — standalone Python CLI (pip installable):
  - nrl-k8s fairshare — show queue config (quota, limit, weight, priority)
  - nrl-k8s occupancy — show GPU allocation per node and per queue
  - nrl-k8s submit — submit gang-scheduled RayJob with optional
    --segment-size for topology-aware scheduling
  - 6 unit tests (mocked K8s API), all passing
- Add TODO for NVL72 topology testing with links to relevant PRs/issues

Tested: KAI v0.14.0 gang scheduling works, CLI commands verified against
live cluster, Grafana dashboard loads and queries return data.

Signed-off-by: Terry Kong <terryk@nvidia.com>
- Merge disagg_rl_raycluster.yaml + disagg_gym_raycluster.yaml into
  single disagg-rayclusters.yaml (always deployed together)
- Inline peer-watcher Python script directly in sidecar container args
  (eliminates ConfigMap setup step, each deployment is self-contained)
- Remove 7 redundant workload YAMLs (sft_rayjob, kai_scheduled_*,
  raycluster-blocker, standalone peer-watcher.py)
- Update SETUP.md: simplified quick start, updated architecture tree,
  removed ConfigMap peer-watcher setup step

15 files → 8 files in examples/. Infrastructure configs unchanged.

Tested: inlined peer-watcher works — deleting either cluster triggers
teardown of both within 10s.

Signed-off-by: Terry Kong <terryk@nvidia.com>
- Remove extensions/k8s_cli/ (not needed for now)
- Rename queues: org → root-org, priority-team → high-prio,
  community → low-prio
- Simplify Kyverno policy comments
- Update SETUP.md to remove CLI section

Signed-off-by: Terry Kong <terryk@nvidia.com>
Not needed right now — queue enforcement can be added back later
if required.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Signed-off-by: Terry Kong <terryk@nvidia.com>
Signed-off-by: Terry Kong <terryk@nvidia.com>
…a part)

Signed-off-by: Terry Kong <terryk@nvidia.com>
Three workload deployment patterns:
1. rayjob-monolithic.yaml — single-cluster RayJob (1 GPU, KubeRay)
2. disagg-rayclusters.yaml — two KubeRay RayClusters + peer-watcher
3. disagg-jobset.yaml — single JobSet with native failure/startup policies

Also adds JobSet controller (v0.11.1) to the helmfile.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Shows the full Ray cluster pattern for Gym: separate head and worker
pods within the JobSet, with dependsOn ordering and DNS discovery.

Signed-off-by: Terry Kong <terryk@nvidia.com>
KAI gang-schedules all pods in a JobSet together (one PodGroup with
minMember=total pods). This deadlocks with dependsOn: KAI waits for
all pods to exist, but JobSet won't create dependent pods until the
head is Ready. Fix: drop dependsOn, use init containers that poll
ray health-check (same pattern KubeRay uses).

Tested: all 6 pods schedule, init containers wait for heads, driver
submits a Ray job successfully, successPolicy triggers on driver
exit 0, failurePolicy tears down everything on gym-head crash.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Signed-off-by: Terry Kong <terryk@nvidia.com>
- Add git safe.directory for Gym submodule (uv build fails otherwise)
- Add uv pip install kubernetes (needed by endpoint registry)
- Increase readiness probe failureThreshold (uv install takes time)

Signed-off-by: Terry Kong <terryk@nvidia.com>
Replace --working-dir and --runtime-env-json with a simple cd wrapper.
Both --working-dir and runtime_env.working_dir cause Ray to zip and
upload the entire directory to GCS, which is extremely slow for large
repos (1GB+). Since the code is already on all nodes via hostPath,
wrapping the entrypoint with cd avoids the upload entirely.

Before: ray job submit --working-dir /workspace/nemo-rl -- python ...
  → scans entire tree, uploads 1GB+ to GCS, takes minutes

After: ray job submit -- bash -c "cd /workspace/nemo-rl && python ..."
  → instant submission, no upload
Signed-off-by: Terry Kong <terryk@nvidia.com>
…pefail

- Use timestamp-based submission_id to avoid GCS collision across redeploys
- Disable wandb (not configured in kind dev cluster)
- Add set -eo pipefail for proper exit code propagation through tee
- Persist driver logs to hostPath for post-mortem debugging

Tested: GRPO training loads config, connects to Ray cluster, loads
datasets, initializes compute cluster. Fails with "Not enough GPUs"
(expected — kind cluster has 2 GPUs, config expects 8).

Signed-off-by: Terry Kong <terryk@nvidia.com>
- Add active development disclaimer (GitHub admonition)
- Add production guidance (adapt manifests, use Terraform, not helmfile)
- Document colocated vs disaggregated architecture with diagrams
- Compare KubeRay RayClusters vs JobSet for disagg deployment
- Explain why ConfigMap is still needed for JobSet (vLLM URL exchange)
- Document dependsOn + KAI deadlock and init container workaround
- Add local kind testing instructions
- Add comparison table (failure cascading, gang scheduling, discovery)

Signed-off-by: Terry Kong <terryk@nvidia.com>
Add all parallelism overrides needed for Qwen3-0.6B on a 2-GPU cluster:
- tensor_model_parallel_size=1, pipeline=1, expert=1, context=1
- sequence_parallel=false (requires TP>1)
- colocated.enabled=false, gpus_per_node=1
- max_new_tokens=512, max_total_sequence_length=512
- max_num_steps=2 for quick smoke testing

Tested: GRPO initializes vLLM workers, captures CUDA graphs, starts
Megatron LM workers. Fails at k8s_endpoint_registry import because
the container image predates the tk/infra branch — the hostPath mount
has newer code than the baked-in worker venvs. Will work with a
matching container build.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Signed-off-by: Terry Kong <terryk@nvidia.com>
Mount the local nemo-rl source at both /workspace/nemo-rl and
/opt/nemo-rl. The container's editable install points to /opt/nemo-rl,
so this ensures all imports (Ray job driver, worker venvs) use the
same code from the hostPath mount.

Tested: GRPO setup completes (471s), vLLM workers initialized with
CUDA graphs, Megatron workers started, endpoint registry created and
vLLM URLs published. Full disagg E2E validated up to Gym handshake.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Signed-off-by: Terry Kong <terryk@nvidia.com>
…part)

Signed-off-by: Terry Kong <terryk@nvidia.com>
…hboard

Adds a production-grade launch path, a dedicated dashboard command, and
an onboarding guide. All opt-in — existing interactive dev flows keep
working unchanged.

`--mode {interactive, batch}` on `launch` / `run`:
  interactive (default): port-forward + working_dir upload + tail logs.
  batch: kubectl exec + code from image + nohup/disown + no wait.
Each axis is independently overridable via `--submitter`,
`--code-source`, `--code-path`, `--run-id`, `--wait/--no-wait`.

New submitters package (`src/nrl_k8s/submitters/`):
  * JobSubmitter Protocol with submit/follow/status/stop.
  * PortForwardSubmitter (Ray Job SDK) — inlines env vars as shell
    `export`s instead of `runtime_env.env_vars` to sidestep Ray's
    "Failed to merge the Job's runtime env" error when ray.init captures
    os.environ.
  * ExecSubmitter — `kubectl cp` launcher script, `nohup bash ... &
    disown` via Popen, bounded-timeout pidfile poll (~17 s wall time
    vs 14 min previously for detach-clean return on flaky EKS).
    tail -F, kill -0 / exitcode sentinel, SIGTERM/SIGKILL stop.
  * SubmissionHandle cached under ~/.cache/nrl-k8s/runs/<id>.json so
    job logs / job stop dispatch on transport without re-probing.

Schema additions (back-compat — every default preserved):
  * SubmitSpec.submitter: portForward | exec
  * SubmitSpec.execTmpDir: /tmp
  * LaunchSpec.codeSource: upload | image | lustre
  * LaunchSpec.codePath + validator requiring it for image / lustre
  * LaunchSpec.runMode: interactive | batch
  * orchestrate.submit_training always injects NRL_K8S_RUN_ID into the
    entrypoint env so recipes can reference it for wandb name etc.

`nrl-k8s cluster dashboard <name>` command:
  Port-forwards svc/<name>-head-svc:8265 to localhost:8265 and opens a
  browser. `--fix` (default) runs a one-time reinstall of ray[default]
  with `--link-mode=copy` on the head pod when dashboard static assets
  are symlinks into the uv cache (aiohttp's follow_symlinks=False else
  404s every JS/CSS file → blank page). The permanent fix is in the
  image build (`ENV UV_LINK_MODE=copy` before the ray install).

Docs + examples:
  * docs/onboarding.md — step-by-step for standing up nrl-k8s on a
    fresh cluster (RBAC SA, pull secrets, wandb secret, node pool
    mapping, first run).
  * examples/qwen3_4b_if_gym_disagg.prod.infra.yaml — production
    variant of the canonical gym-disagg example:
    submitter=exec, codeSource=image, codePath=/opt/nemo-rl.
  * README: "Modes: interactive vs batch" section with the full batch
    walkthrough, env-var conventions, and blank-dashboard remediation.

Testing:
  * 137 unit tests pass (was 134). New coverage: submitter protocol,
    exec command shape + env-escape + pidfile plumbing, port-forward
    back-compat, mode resolution, cluster dashboard invocation.

Verified end-to-end against the live raycluster-single-qwen3-4b:
  * exec-mode run → SUCCEEDED, wandb: nemorl-single-k8s/runs/f9oisnfd
  * portForward+image run → SUCCEEDED, wandb: runs/wgm3txe7
Both submit wall-time 9–17 s, laptop detachable as soon as the driver
is running.

Signed-off-by: Hemil Desai <hemild@nvidia.com>
Signed-off-by: Terry Kong <terryk@nvidia.com>
The GPU Operator is pre-installed by NKX on production clusters, so we
should not deploy our own. Rename prod gpu_backend to
existing-nkx-gpu-operator, remove the gpu-operator helm release and
values file, and update SETUP.md accordingly.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Increase binder memory from 200Mi to 1Gi — the default OOMs on large
clusters (78+ nodes), which breaks DRA resource claim allocation.

Set admission.gpuPodRuntimeClassName to "" to prevent the KAI admission
webhook from injecting runtimeClassName: nvidia onto GPU pods. GPU
Operator v25.10.0+ with cdi.enabled=true installs a nvidia RuntimeClass
that triggers the management.nvidia.com CDI path, which fails with
"unresolvable CDI devices" on clusters where CDI spec files are not
fully configured. Requires KAI >= v0.13.0 (PR #1035).

Signed-off-by: Terry Kong <terryk@nvidia.com>
Add two JobSet variants for running monolithic (colocated) training on
GB300 nodes with DRA topology-aware scheduling:

- monolithic-jobset.yaml: uses KAI scheduler with queue support
- monolithic-jobset-no-kai.yaml: uses default scheduler (for comparing
  DRA behavior between schedulers)

Both use ComputeDomain + RoCE ResourceClaimTemplates for topology-aware
placement and run SFT with Qwen3-0.6B as a smoke test.

Signed-off-by: Terry Kong <terryk@nvidia.com>
Add step-by-step onboarding for the aws-cmh (GB300) and nemo-ci-h100
EKS clusters, covering access requests, nvsec/AWS SSO auth, kubeconfig
setup, shared PVC workspace, and running jobs with nrl-k8s.

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong terrykong added CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) and removed CI:L1 Run doctests, unit tests, and functional tests labels Apr 27, 2026
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test 3d846a6

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test debb067

The v4.34.0 versioned URL returns 500; the unversioned URL works.

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong
terrykong requested a review from a team as a code owner April 27, 2026 23:58
@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Apr 27, 2026
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test 5c29d48

The chat_templating page was split; "how to create" content moved to
chat_templating_writing. Also add the nsight-systems InstallationGuide
URL to false positives (transient connection reset).

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test 0b05895

hemildesai
hemildesai previously approved these changes Apr 28, 2026
HuggingFace docs intermittently return 500. Drop the `main/` version
pin from the chat_templating link and add all HF doc URLs to the
linkcheck false-positives list to stop transient failures.

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test feabeea

@terrykong
terrykong merged commit 66e7149 into main Apr 28, 2026
44 of 46 checks passed
@terrykong
terrykong deleted the hemil/k8s-infra-cp-part1 branch April 28, 2026 01:30
snivertynv pushed a commit to snivertynv/RL that referenced this pull request May 5, 2026
…#2330)

Signed-off-by: Terry Kong <terryk@nvidia.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Hemil Desai <hemild@nvidia.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants