Skip to content

infra: K8s GPU cluster setup with KAI scheduler, KubeRay, and JobSet - #2238

Closed
terrykong wants to merge 25 commits into
mainfrom
tk/infra
Closed

infra: K8s GPU cluster setup with KAI scheduler, KubeRay, and JobSet#2238
terrykong wants to merge 25 commits into
mainfrom
tk/infra

Conversation

@terrykong

@terrykong terrykong commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add K8s infrastructure for deploying nemo-rl GPU workloads:

  • nvkind cluster setup: scripts for local GPU-enabled K8s cluster with NVIDIA GPU support
  • Helmfile: KAI scheduler (gang scheduling + fairshare), KubeRay operator, JobSet controller, nvidia-device-plugin (kind) / GPU operator (prod)
  • Workload examples: monolithic RayJob, disaggregated RL+Gym via KubeRay RayClusters, disaggregated RL+Gym via JobSet
  • Disaggregated architecture: ConfigMap endpoint registry for vLLM/Gym URL exchange, peer-watcher sidecar for failure cascading, standalone Gym server
  • Code changes: K8sEndpointRegistry for service discovery, remote_gym_url in NemoGymConfig, standalone Gym server moved to nemo_rl/distributed/

Architecture

Two deployment patterns for disaggregated RL+Gym:

  1. KubeRay RayClusters — two independent RayClusters with peer-watcher sidecars for failure cascading
  2. JobSet — single JobSet with native failurePolicy and init containers (no peer-watcher needed)

Both use a ConfigMap endpoint registry for dynamic vLLM URL exchange.

Test plan

  • nvkind cluster creation with 2 GPUs
  • Helmfile sync (KAI, KubeRay, JobSet, device plugin)
  • KAI queue creation (high-prio/low-prio hierarchy)
  • Disagg RayClusters: both clusters reach Ready, peer-watcher failure cascading works both directions
  • Disagg JobSet: all 6 pods gang-scheduled, init containers work, failurePolicy tears down on crash, successPolicy triggers on driver exit 0
  • GRPO initialization: config loaded, vLLM workers started (CUDA graphs captured), Megatron workers started, endpoint registry created, vLLM URLs published
  • dependsOn + KAI deadlock identified and documented (use init containers instead)
  • Full E2E training (blocked by Gym standalone server handshake — needs matching container image)

@copy-pr-bot

copy-pr-bot Bot commented Apr 9, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@terrykong terrykong added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Apr 19, 2026
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test ab15151

@terrykong terrykong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR adds K8s infrastructure for disaggregated RL-Gym deployments, including:

  • K8sEndpointRegistry — ConfigMap-backed service discovery between RL and Gym clusters
  • standalone_gym_server — standalone NeMo Gym server for independent deployment
  • Remote mode in NemoGym — connect to an external Gym HTTP service instead of spawning local subprocesses
  • Kind cluster setup scripts, Helm charts, and K8s manifest examples (JobSet + KubeRay approaches)
  • Gym submodule bump (23cdeb3801a9765f) for disaggregated mode support

Action Items

⚠️ Merge conflicts: This PR is currently in a CONFLICTING state with main. Please rebase and resolve conflicts before merging.

📝 PR description: The PR body is currently the template placeholder. It would be helpful to fill in a description of the motivation, architecture decisions, and test plan — this helps reviewers and future readers understand the context.

📖 Documentation: Per CONTRIBUTING.md, new key features should include a documentation update. This PR introduces a significant new deployment architecture — consider adding a doc page covering the disaggregated mode, K8s prerequisites, and configuration options (env.remote_gym_url, env.disagg_job_id).

Additional Notes (not tied to specific diff lines)

  • Internal image references: All example manifests use nvcr.io/nvidian/nemo-rl:e5a729c-47084432 which is only accessible within NVIDIA networks. If these examples are intended for external contributors too, consider adding a comment noting users should substitute their own image.
  • Missing nvcr-secret setup: SETUP.md Quick Start doesn't mention creating the nvcr-secret imagePullSecret that all manifests require. First-time users will hit ImagePullBackOff.
  • Shell script hardening: create-cluster.sh uses || true after nvkind cluster create which silently swallows all failures (not just the expected /proc/driver/nvidia patching issue). install-nvkind.sh installs nvkind at @latest without version pinning.

See inline comments for specific code-level findings.

Generated by Claude Code

import time
from pathlib import Path

from kubernetes import client, config

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k8s_endpoint_registry.py:37

kubernetes is imported at module level but isn't declared in pyproject.toml (not in [project.dependencies] or any optional extra). Users importing K8sEndpointRegistry will get an ImportError unless they manually pip install kubernetes.

The disagg-jobset.yaml works around this with a runtime uv pip install kubernetes (line 212), but it would be cleaner to declare it as an optional dependency — e.g., a k8s extra in pyproject.toml.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Added k8s extra to pyproject.toml.

cm.data[key] = value
self._v1.patch_namespaced_config_map(
name=self.configmap_name, namespace=self.namespace, body=cm
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k8s_endpoint_registry.py:118-126

The read-modify-patch pattern here is susceptible to lost updates under concurrency. If both RL and Gym call set() on the same ConfigMap near-simultaneously, one side may read a stale resourceVersion. The patch_namespaced_config_map call will then return a 409 Conflict, which isn't caught (only 404 is handled in the outer except), so the write silently fails with an unhandled exception.

Consider catching 409 in the outer handler with a retry loop, or using a strategic merge patch that targets only the specific key rather than replacing the full data dict.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Added 409 handling on the patch path with retry.

except ApiException as create_err:
if create_err.status == 409:
# Another process created it between our read and create — retry patch.
self.set(key, value)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k8s_endpoint_registry.py:141-144

The recursive self.set(key, value) on a 409 during the fallback create has no depth limit. In a tight race between RL and Gym both hitting this path, this could recurse until stack overflow. A simple iterative retry with a cap (e.g., for attempt in range(3): ...) would be safer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Converted to iterative retry with _max_retries=5. Raises RuntimeError if exhausted.

Comment thread nemo_rl/environments/nemo_gym.py Outdated
url = remote_url.removeprefix("http://").removeprefix("https://")
if ":" in url:
host, port_str = url.rsplit(":", 1)
port = int(port_str.rstrip("/"))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nemo_gym.py:51-56

The URL parsing using rsplit(":", 1) works for simple host:port inputs but breaks on URLs with path components. For example, http://gym-service:8080/v1host="gym-service", port_str="8080/v1"int("8080/v1".rstrip("/")) raises ValueError.

Consider using urllib.parse.urlparse for robustness:

from urllib.parse import urlparse
parsed = urlparse(remote_url if "://" in remote_url else f"http://{remote_url}")
host = parsed.hostname or remote_url.rstrip("/")
port = parsed.port or 8080

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Using urllib.parse.urlparse now.

initial_global_config_dict = cfg.get("initial_global_config_dict") or {}
self.rollout_max_attempts_to_avoid_lp_nan = initial_global_config_dict.pop(
"rollout_max_attempts_to_avoid_lp_nan", 1
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nemo_gym.py:63-66

Two things here:

  1. Mutation leak: .pop("rollout_max_attempts_to_avoid_lp_nan", 1) mutates the caller's initial_global_config_dict (potentially the live config from run_grpo_nemo_gym.py). Consider using .get() instead of .pop(), or copying the dict first.

  2. Missing validation: Unlike the colocated path (line 120), there's no assert >= 1 check here. If a user sets this to 0, run_rollouts() silently produces no results (the while trial < max_attempts loop at line 154 is never entered).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Changed .pop() to .get() and added assert >= 1 matching the colocated path.

Comment thread examples/nemo_gym/run_grpo_nemo_gym.py Outdated

# Wait for the Gym cluster to register its head server address.
print("Waiting for Gym head server to register in endpoint registry...")
remote_gym_url = registry.get("gym_head_server")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run_grpo_nemo_gym.py:231-234

registry.get("gym_head_server") blocks for up to 600s and raises TimeoutError on failure. This exception isn't caught — it will crash the training job after all setup (Ray init, model loading, etc.) is complete, wasting GPU time.

Consider wrapping this in a try/except with a user-friendly error message, e.g.:

try:
    remote_gym_url = registry.get("gym_head_server")
except TimeoutError:
    raise RuntimeError(
        f"Timed out waiting for Gym cluster to register. "
        f"Check that the Gym cluster is running with --job-id={disagg_job_id}"
    )

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Wrapped in try/except TimeoutError with a message pointing to the Gym cluster.

k: _strip_interpolations(v)
for k, v in d.items()
if not (isinstance(v, str) and "${" in v)
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

standalone_gym_server.py:125-136

_strip_interpolations silently removes any config key whose value contains ${. This is meant to handle unresolvable OmegaConf interpolations from the full GRPO config, but it could silently drop legitimate values — the server would then start with missing settings and fail later in an opaque way.

Consider logging each dropped key so users can diagnose config issues:

if isinstance(v, str) and "${" in v:
    print(f"Dropping unresolvable interpolation: {k}={v}")
    continue

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Now logs each dropped key with its path and value.

verbs: ["get", "create", "update", "patch", "delete"]
- apiGroups: ["ray.io"]
resources: ["rayclusters"]
verbs: ["get", "delete"]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endpoint-registry-rbac.yaml:13-19

This Role grants delete on all ConfigMaps and all RayClusters in the namespace. A few observations:

  • delete on ConfigMaps is only needed by the peer-watcher teardown path. The endpoint registry itself only needs get, create, update, patch.
  • delete on RayClusters is only needed by the peer-watcher sidecar on head pods, but this SA is also used by GPU worker pods (disagg-rayclusters.yaml lines 167, 200) which don't need it.

Consider splitting into two ServiceAccounts — one for head pods (full permissions) and one for workers (ConfigMap read/write only). This follows the principle of least privilege.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a NOTE comment acknowledging this is an example and suggesting production hardening. Kept as-is for simplicity.

print(f"[peer-watcher] Peer {PEER} {state or 'unreachable'} ({fails}/{MAX_FAIL})", flush=True)
if fails >= MAX_FAIL:
teardown(f"Peer {PEER} failed {MAX_FAIL}x")
continue

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disagg-rayclusters.yaml:131-140

The continue on line 136 skips the ConfigMap error-flag check (lines 137-140). If the peer cluster is unhealthy and signals an error via ConfigMap simultaneously, the error flag won't trigger teardown until either the peer recovers or MAX_FAIL is reached.

Moving the ConfigMap check before the continue would catch both conditions:

if state in ("failed", "suspended") or (code != 0 and state == ""):
    fails += 1
    ...
    if fails >= MAX_FAIL:
        teardown(...)
    # Check ConfigMap error even when peer is failing
    if JOB_ID:
        cm = kube(f"/api/v1/...")
        if cm.get("data", {}).get("error", ""):
            teardown(f"Error via ConfigMap: {cm['data']['error']}")
    continue

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Moved ConfigMap check before the continue in both peer-watcher sidecars.

- devices: all
extraMounts:
- hostPath: /home/terryk/nemo-rl
containerPath: /workspace/nemo-rl

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvkind-config-values-dev.yaml:6-7

Nit: /home/terryk/nemo-rl is a developer-specific path checked into the repo. Other contributors using this file will silently get an empty DirectoryOrCreate mount. Consider adding a comment noting this path must be edited per-user, or adding this file to .gitignore and providing a .example template instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0881933. Added TODO comment noting the path must be edited per-user.

terrykong added a commit that referenced this pull request Apr 19, 2026
- Add `k8s` optional extra with `kubernetes` dependency to pyproject.toml
- Fix read-modify-patch race in K8sEndpointRegistry.set(): handle 409
  on both patch and create paths, convert recursive retry to iterative
  loop with max attempts
- Use urllib.parse.urlparse for robust URL parsing in NemoGym remote mode
- Fix .pop() caller-dict mutation: use .get() instead, add validation
  assert matching the colocated code path
- Wrap registry.get() TimeoutError with actionable diagnostic message
- Log dropped interpolation keys in standalone_gym_server
- Add production-hardening comment to example RBAC manifest
- Move ConfigMap error check before `continue` in both peer-watcher
  sidecars so errors aren't skipped during failure-counting
- Add TODO comment about hardcoded hostPath in nvkind dev config

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong terrykong changed the title Tk/infra infra: K8s GPU cluster setup with KAI scheduler, KubeRay, and JobSet Apr 20, 2026
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>
Add optional remote_gym_url to NemoGymConfig. When set, the NemoGym
Ray actor connects to an external Gym HTTP service instead of spawning
local subprocesses. Colocated mode (default) is unchanged.

- nemo_gym.py: split __init__ into remote/colocated paths
- run_grpo_nemo_gym.py: support env.remote_gym_url and env.disagg_job_id
- Gym submodule: standalone_server.py entry point with K8s endpoint
  registry integration, use_absolute_ip for cross-pod communication
- gym_standalone_config.yaml: example config for standalone server

Tested: disaggregated GRPO completed 3 training steps with RL on one
RayCluster (2 GPU) and Gym on a separate RayCluster (CPU only).

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

Each (RL, Gym) job pair shares a ConfigMap for dynamic address exchange.
Both sides register their IP:port and poll for the peer's address.
The ConfigMap has an ownerReference to the RL RayCluster for automatic
garbage collection on teardown.

- k8s_endpoint_registry.py: create/set/get/get_nowait methods with race
  condition handling (409 retry) and proper error propagation
- endpoint-registry-rbac.yaml: ServiceAccount + Role + RoleBinding
- disagg_rl_raycluster.yaml: RL cluster with serviceAccountName
- disagg_gym_raycluster.yaml: Gym cluster with serviceAccountName

Tested: ConfigMap CRUD verified in-cluster, bidirectional URL exchange
between RL and Gym clusters confirmed working.

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>
Allows iterating on the server without committing to the Gym repo.
The script imports from nemo_gym at runtime (same container image).

Run with: uv run --extra nemo_gym python -m nemo_rl.distributed.standalone_gym_server

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>
- Add `k8s` optional extra with `kubernetes` dependency to pyproject.toml
- Fix read-modify-patch race in K8sEndpointRegistry.set(): handle 409
  on both patch and create paths, convert recursive retry to iterative
  loop with max attempts
- Use urllib.parse.urlparse for robust URL parsing in NemoGym remote mode
- Fix .pop() caller-dict mutation: use .get() instead, add validation
  assert matching the colocated code path
- Wrap registry.get() TimeoutError with actionable diagnostic message
- Log dropped interpolation keys in standalone_gym_server
- Add production-hardening comment to example RBAC manifest
- Move ConfigMap error check before `continue` in both peer-watcher
  sidecars so errors aren't skipped during failure-counting
- Add TODO comment about hardcoded hostPath in nvkind dev config

Signed-off-by: Terry Kong <terryk@nvidia.com>
The standalone_gym_server has been moved to nemo_rl/distributed/ so
the Gym submodule change from tk/standalone-server is no longer needed.
The standalone server could potentially be upstreamed to Gym in the
future, but for now it lives in nemo-rl since it's highly in flux.

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

Copy link
Copy Markdown
Collaborator Author

Superseded by #2321 which includes all the infra work from this branch plus nrl-k8s CLI improvements (dev pod, DRA auto-management, per-user cluster names, stale RayJob detection).

@terrykong terrykong closed this Apr 23, 2026
terrykong added a commit that referenced this pull request Apr 23, 2026
- Add `k8s` optional extra with `kubernetes` dependency to pyproject.toml
- Fix read-modify-patch race in K8sEndpointRegistry.set(): handle 409
  on both patch and create paths, convert recursive retry to iterative
  loop with max attempts
- Use urllib.parse.urlparse for robust URL parsing in NemoGym remote mode
- Fix .pop() caller-dict mutation: use .get() instead, add validation
  assert matching the colocated code path
- Wrap registry.get() TimeoutError with actionable diagnostic message
- Log dropped interpolation keys in standalone_gym_server
- Add production-hardening comment to example RBAC manifest
- Move ConfigMap error check before `continue` in both peer-watcher
  sidecars so errors aren't skipped during failure-counting
- Add TODO comment about hardcoded hostPath in nvkind dev config

Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 26, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 27, 2026
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong added a commit that referenced this pull request Apr 27, 2026
Signed-off-by: Terry Kong <terryk@nvidia.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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant