Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ce6c118
feat(deepep-efa): NeMo-RL GRPO + Megatron Shape-Y MoE all-to-all over…
dmvevents Aug 25, 2026
6c14c2e
fix(deepep-efa): NeMo-RL image builds + passes its own verify-image g…
dmvevents Aug 26, 2026
33ee8e7
fix(nemo-rl/deepep-efa): train-step MoE gate — sync replicated params…
dmvevents Aug 26, 2026
4052250
fix(nemo-rl/deepep-efa): security — .dockerignore + data-prep pod har…
dmvevents Aug 26, 2026
5df9552
fix(nemo-rl/deepep-efa): raycluster manifest — pod env, FSx default, …
dmvevents Aug 26, 2026
c455bb1
fix(nemo-rl/deepep-efa): recipe probes — per-token scoring, all-step …
dmvevents Aug 26, 2026
9e841d8
fix(nemo-rl/deepep-efa): recipe shells — EFA counter families, timeou…
dmvevents Aug 26, 2026
55a3f6b
nemo-rl/deepep-v2-efa: per-change patch probes + loader-resolved NCCL…
dmvevents Aug 26, 2026
316c127
nemo-rl/deepep-v2-efa: scope `|| true` to removal, reset OPAL_PREFIX,…
dmvevents Aug 26, 2026
72d66ad
docs(nemo-rl/deepep-v2-efa): correct provenance + substrate claims fr…
dmvevents Aug 26, 2026
af2bf8f
chmod +x nemo-rl/deepep-v2-efa shell entrypoints (PR #1242 review)
dmvevents Aug 26, 2026
9bbea80
build(nemo-rl/deepep-v2-efa): digest-pin NGC base, commit-pin NCCL, d…
dmvevents Aug 26, 2026
f30b929
docs(nemo-rl/deepep-v2-efa): make requirements.txt truthful vs NEMO_R…
dmvevents Aug 26, 2026
8956fcc
Merge remote-tracking branch 'upstream/main' into migrate/nemo-rl-to-…
dmvevents Sep 3, 2026
a2e017c
refactor(training): migrate nemo-rl/deepep-v2-efa to examples/trainin…
dmvevents Sep 3, 2026
fe0250a
refactor(training/nemo-rl/deepep-v2-efa): pin DeepEP source to amazon…
dmvevents Sep 3, 2026
4dc63a6
docs(nemo-rl/deepep-v2-efa): scope the 'baseline' framing — it is not…
dmvevents Sep 5, 2026
5bd1da8
docs(nemo-rl/deepep-v2-efa): env_vars NVSHMEM note — build-time link …
dmvevents Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/training/nemo-rl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ NVIDIA Resiliency Extension provides process-level fault tolerance:
- **Straggler Detector**: Monitors GPU kernel timing across ranks to detect slow GPUs
- **Checkpoint integration**: Saves model state to FSx for resume-on-restart

## Variants

This directory nests a distinct NeMo-RL example that shares the framework but targets a different
communication mechanism and scale:

| Variant | Focus | Scale |
|---------|-------|-------|
| [`deepep-v2-efa/`](./deepep-v2-efa) | NeMo-RL (GRPO) + Megatron-LM MoE expert-parallel all-to-all over **DeepEP V2's NCCL-GIN CPU-proxy** on **AWS EFA** (`Qwen3-30B-A3B`); built NGC-from-scratch. See its README for the measured-vs-staged breakdown. | 2× p5.48xlarge (16× H100) or p5en (H200) |

## Architecture

```
Expand Down
11 changes: 11 additions & 0 deletions examples/training/nemo-rl/deepep-v2-efa/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
# .gitignore does NOT apply to a Docker build context — without this file the
# filled-in `env_vars` (HF_TOKEN + AWS account id) is uploaded to the daemon,
# and to any remote/CI builder, on every `docker build .` in this directory.
# No COPY in nemo-rl.Dockerfile is broad (each names a specific file), so nothing
# lands in an image layer today; this closes the context-upload exposure and
# guards against a future broad COPY.
env_vars
*.log
__pycache__/
9 changes: 9 additions & 0 deletions examples/training/nemo-rl/deepep-v2-efa/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

# Local environment file with filled-in secrets/values — never commit
env_vars

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No .dockerignore while the build context is the directory holding env_vars

Observation. env_vars is correctly gitignored and carries HF_TOKEN plus your AWS account
id. README §2 then builds with the context set to this directory
(docker build -f nemo-rl.Dockerfile -t ${FULL_IMAGE} .). .gitignore does not apply to Docker
build contexts, so the filled-in env_vars is uploaded to the daemon — and to any remote/CI
builder — on every build.

Impact, scoped honestly: there is no COPY . . in this Dockerfile (every COPY names a
specific file), so nothing lands in an image layer and nothing is pushed to ECR. The exposure is
the context upload itself, which matters for a remote builder and for anyone who later adds a
broad COPY.

Suggestion. Add a .dockerignore beside the Dockerfile:

env_vars
*.log
__pycache__/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4052250. Added .dockerignore (your point exactly: .gitignore does not apply to a Docker build context, so the filled-in env_vars — HF token + AWS account id — would upload to the daemon and any remote/CI builder on every docker build .). It ignores env_vars, *.log, __pycache__/. No COPY in the Dockerfile is broad today, but this closes the context-upload exposure and guards a future broad COPY.


# Run artifacts
*.log
__pycache__/
287 changes: 287 additions & 0 deletions examples/training/nemo-rl/deepep-v2-efa/README.md

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions examples/training/nemo-rl/deepep-v2-efa/env_vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
# ============================================================
# NeMo-RL + DeepEP V2 over EFA - Environment Variables
#
# Copy this file to env_vars (gitignored) and fill in your values:
# cp env_vars.example env_vars && vim env_vars
# Source it before building or applying manifests:
# source env_vars
# ============================================================

# ----- AWS / ECR -----
# Region and account are derived from your current AWS credentials/config so
# nothing environment-specific is hard-coded. Override AWS_REGION if needed.
export AWS_REGION="${AWS_REGION:-$(aws configure get region)}"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/"
export IMAGE="nemo-rl-deepep-efa"
# Immutable tag - never "latest": with imagePullPolicy: IfNotPresent a node
# that cached "latest" silently keeps running the OLD image after a rebuild.
export TAG="v1-20260825"
export FULL_IMAGE="${REGISTRY}${IMAGE}:${TAG}"

# ----- Opt-in draft-PR image flavor -----
# 1 = bake the 2 draft upstream PRs (NeMo-RL#2410, Megatron-LM#4632) that the
# full GRPO rollout-over-DeepEP path needs; 0 = the upstream-only baseline.
# (DeepEP needs no patch: the amazon-contributing/DeepEP fork the image pins
# carries the former draft DeepEP#612 fixes in-code, on both flavors.) The ":-"
# default keeps a value pre-set on the command line (APPLY_DRAFT_ROLLOUT_PATCHES=1
# docker build ...) from being clobbered when this file is sourced afterwards.
export APPLY_DRAFT_ROLLOUT_PATCHES="${APPLY_DRAFT_ROLLOUT_PATCHES:-0}"

# ----- HuggingFace (full GRPO path only; the recipe gates need no weights) -----
export HF_TOKEN="" # <-- set your HuggingFace token here if the model is gated

# ----- Model / shape (the Wave-28 measured shape twin) -----
# Qwen3-30B-A3B: 128 routed experts, top-k 8, hidden 2048. Any MoE whose
# routed-expert count divides by the EP size works; the probe asserts it.
export MODEL_NAME="Qwen/Qwen3-30B-A3B"
export MODEL_LOCAL="/fsx/models/Qwen3-30B-A3B"

# ----- Cluster -----
# HOW THESE REACH THE GATES: sourcing this file sets vars in YOUR shell only — a
# client-side `source` does NOT cross `kubectl exec`; only the pod's own env
# does. So `envsubst < kubernetes/raycluster.yaml` substitutes NUM_NODES / the
# EP_* shape into the worker pod's `env:` block, and the pod carries them.
# run-rollout-probe.sh / train-step.sh read NNODES (not NUM_NODES); the manifest
# bridges the names by emitting `{ name: NNODES, value: "${NUM_NODES}" }`. Set
# NUM_NODES here, and both the RayCluster replica count and the in-pod NNODES the
# launchers read move together.
export NAMESPACE="nemo-rl-deepep"
export FSX_CLAIM="fsx-claim"
export NUM_NODES=2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

NUM_NODES here is never read by anything — the launchers read NNODES, and neither crosses kubectl exec

Observation. Two independent breaks in the same chain:

  1. Name mismatch. This file exports NUM_NODES; run-rollout-probe.sh:19 and
    train-step.sh:19 read NNODES="${NNODES:-2}". NUM_NODES is consumed only by
    raycluster.yaml's replicas/minReplicas/maxReplicas. No file bridges the two names.
  2. No propagation. This file's own header says these are exported "so ad-hoc shells
    (kubectl exec) carry the same contract" — but a client-side export does not cross
    kubectl exec; only the container's own environment does. I grepped raycluster.yaml: it
    defines zero of NNODES, GPUS_PER_NODE, EP_EXPERTS, EP_TOPK, EP_HIDDEN,
    EP_TOKENS, EP_NUM_SMS, EP_NUM_QPS. Only the transport-contract vars are in the pod env.

Impact. Every documented gate runs on the hardcoded script defaults, whatever the operator put
in env_vars. Set NUM_NODES=4 and you get four worker pods, while the leader still opens a
--nnodes 2 --nproc-per-node 8 rendezvous — ranks on workers 2 and 3 never join, and the probe
either hangs in init_process_group or silently certifies 2 of your 4 nodes. The EP_* shape
knobs this file documents as tunable are dead on the kubectl exec path for the same reason —
including EP_NUM_QPS, which trap 2 says is load-bearing on EFA.

Suggestion. Put them in the container env so the pod carries the contract (matching how the
transport vars are already handled), and rename to the name the scripts actually read:

                - { name: NNODES,          value: "${NUM_NODES}" }
                - { name: GPUS_PER_NODE,   value: "${GPUS_PER_NODE}" }
                - { name: EP_EXPERTS,      value: "${EP_EXPERTS}" }
                - { name: EP_TOPK,         value: "${EP_TOPK}" }
                - { name: EP_HIDDEN,       value: "${EP_HIDDEN}" }
                - { name: EP_TOKENS,       value: "${EP_TOKENS}" }
                - { name: EP_NUM_SMS,      value: "${EP_NUM_SMS}" }
                - { name: EP_NUM_QPS,      value: "${EP_NUM_QPS}" }

and correct the header line here, since sourcing this file cannot affect a pod.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5df9552. Root cause: the launchers read NNODES while env_vars exported NUM_NODES (only consumed by the manifest replica count), and nothing bridged them across kubectl exec. raycluster.yaml now emits { name: NNODES, value: "${NUM_NODES}" } so both the replica count and the in-pod NNODES derive from the single NUM_NODES you set; env_vars.example:44-48 documents the bridge.

export GPUS_PER_NODE=8
# Per-node EFA NIC count differs by instance type:
# p5.48xlarge (H100): 32 | p5en.48xlarge (H200): 16
export EFA_PER_NODE=32
export INSTANCE_TYPE="p5.48xlarge"

# ----- Probe / train-step shape knobs (defaults mirror the model above) -----
export EP_EXPERTS=128
export EP_TOPK=8
export EP_HIDDEN=2048
export EP_TOKENS=128
# Explicit SM/QP counts for the probe. WHY: the amazon-contributing/DeepEP fork
# carries the former #612 EFA fixes in-code (get_rdma_gbs() sysfs link-rate +
# the auto-QP overflow clamp), so its auto-sizers are EFA-aware — but the probe
# pins these anyway so it is deterministic and auto-sizer-independent. 2 QPs is
# the value the p5en evidence validated; it survives the fork's clamp unchanged.
export EP_NUM_SMS=8
export EP_NUM_QPS=2

# ----- EFA / NCCL-GIN transport contract -----
# Baked into the image ENV and repeated in kubernetes/raycluster.yaml's pod env,
# so a `kubectl exec` shell already inherits them from the container. Exported
# here too only so a LOCAL run (docker run on an EFA host, outside k8s) carries
# the same contract — a client-side `source` does NOT propagate across
# `kubectl exec`.
export FI_PROVIDER=efa
export FI_EFA_USE_DEVICE_RDMA=1
export FI_EFA_FORK_SAFE=1
export FI_EFA_ENABLE_SHM_TRANSFER=0
export NCCL_GIN_TYPE=2 # 2 = CPU-proxy GIN (the EFA-viable path)
export NCCL_GIN_ENABLE=1
export OFI_NCCL_GIN_GDAKI=0 # GPU-initiated GIN is not the shipped path on EFA
export OFI_NCCL_PROTOCOL=RDMA
export NCCL_NVLS_ENABLE=0 # prevents NVLS init failures on H100/H200
export NCCL_NET_PLUGIN=/opt/aws-ofi-nccl/lib/libnccl-net-ofi.so
# NOTE: EP_EFA_MAX_QPS / EP_EFA_RDMA_GBS are intentionally absent. They were the
# knobs the old draft DeepEP#612 patch read; the amazon-contributing/DeepEP fork
# resolves both structurally (get_rdma_gbs() sysfs link-rate + the C++
# _C.{min,max}_unordered_gin_qps clamp), so nothing in deep_ep reads them. The one
# live QP knob is EP_NUM_QPS above (the probe's explicit num_allocated_qps).

# ----- DeepEP V2 selection -----
export DEEP_EP_USE_V2_SHIM=0 # V2-native path, no compatibility shim
export HAVE_DEEP_EP_V2=True # rollout bridge feature flag (draft-PR rollout path)

# ----- NVSHMEM contract (INERT at run time on this image - kept for the rebuild case) -----
# deep_ep links NVSHMEM as a BUILD-TIME dependency (upstream setup.py links it
# unconditionally; see requirements.txt), but the V2 NCCL-GIN backend does NOT
# USE NVSHMEM at run time - the network path is NCCL-GIN - so these values are
# inert here. If you rebuild with the legacy NVSHMEM backend instead, these are
# the required values on EFA - note IBGDA must be 0 (proxy-based), despite what
# most upstream DeepEP manifests say.
export NVSHMEM_REMOTE_TRANSPORT=libfabric
export NVSHMEM_LIBFABRIC_PROVIDER=efa
export NVSHMEM_IB_ENABLE_IBGDA=0
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
# Lightweight CPU pod for staging the model + dataset onto FSx (full GRPO path
# only — the recipe gates in recipe/ need NO weights and NO dataset).
#
# Create the HF token Secret first (same Secret raycluster.yaml consumes):
# kubectl create secret generic hf-token \
# --from-literal=HF_TOKEN=$HF_TOKEN -n ${NAMESPACE}
#
# envsubst < kubernetes/data-prep-pod.yaml | kubectl apply -f -
# kubectl -n ${NAMESPACE} exec -it data-prep -- bash
# # inside: pip install "huggingface_hub[cli]==1.28.0" && \
# # hf download ${MODEL_NAME} --local-dir ${MODEL_LOCAL}
# # NOTE: the GRPO recipe consumes the hub id (policy.model_name:
# # Qwen/Qwen3-30B-A3B), not ${MODEL_LOCAL} — set HF_HOME on the Ray pods to
# # ${MODEL_LOCAL}'s parent, or override model_name, if you stage to a path.
apiVersion: v1
kind: Pod
metadata:
name: data-prep
namespace: ${NAMESPACE}
labels:
app: nemo-rl-deepep-data-prep
spec:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The data-prep pod can land on a GPU node and hold it (and installs an unpinned dep at run time)

Observation. Two smaller things in this spec, both by contrast with raycluster.yaml:

  • Scheduling. The Ray head carries a nvidia.com/gpu.present NotIn ["true"] nodeAffinity with
    the rationale "keep the head off GPU nodes so it never blocks a GPU worker from scheduling."
    This pod is the same kind of CPU-only workload, requests 4–8 CPU and 16–32 Gi, and runs
    sleep infinity — but has no such constraint, so on a cluster with schedulable capacity it can
    park on a p5 node for as long as the operator leaves it up. Copying the head's affinity block
    would settle it.
  • Runtime install. The header comment stages via
    pip install "huggingface_hub[cli]" — unpinned, at run time, inside the pod. On an air-gapped or
    egress-restricted cluster that step is the one that fails, and it's the only thing this pod
    exists to do. A pinned huggingface_hub[cli]==x.y.z (or an image that already carries it) makes
    the staging step reproducible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4052250. The CPU-only data-prep pod now carries the same nvidia.com/gpu.present NotIn true nodeAffinity as the Ray head (data-prep-pod.yaml:29-34), so it cannot land on and hold a GPU node, and it is pinned to python:3.12.14-slim (line 42) instead of installing an unpinned dependency at runtime.

# CPU-only staging workload — keep it off GPU nodes so a `sleep infinity` pod
# can never park on (and hold) a p5 node a GPU worker needs. Same label +
# rationale as raycluster.yaml's head group.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.present
operator: NotIn
values:
- "true"
automountServiceAccountToken: false
containers:
- name: data-prep
# Pinned patch release, never a floating tag — same "never latest"
# reasoning env_vars.example states for the training image.
image: python:3.12.14-slim
command: ["sleep", "infinity"]
env:
# Reuse the hf-token Secret (created above); a plaintext env value would
# sit in etcd for the life of this sleep-infinity pod and is visible in
# `kubectl get pod -o yaml`.
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: HF_TOKEN
resources:
requests:
cpu: "4"
memory: "16Gi"
limits:
cpu: "8"
memory: "32Gi"
volumeMounts:
- name: fsx
mountPath: /fsx
volumes:
- name: fsx
persistentVolumeClaim:
claimName: ${FSX_CLAIM}
restartPolicy: Never
terminationGracePeriodSeconds: 30
Loading