Skip to content

[Don't Merge] Add miles grpo test case - #1225

Draft
littlemex wants to merge 15 commits into
awslabs:mainfrom
littlemex:add-miles-grpo-test-case
Draft

[Don't Merge] Add miles grpo test case#1225
littlemex wants to merge 15 commits into
awslabs:mainfrom
littlemex:add-miles-grpo-test-case

Conversation

@littlemex

Copy link
Copy Markdown
Contributor

Purpose

Relates to #1224

Adds a test case for GRPO post-training with miles on Amazon EKS with EFA, as a companion to the existing 3.test_cases/pytorch/slime case. miles is a fork of slime built for CUDA 13 and Blackwell: SGLang for rollout, Megatron-LM for training, weight sync over Ray actor methods.

The reason to carry both is that the two forks diverge exactly where porting is expensive. miles ships on an nvidia/cuda base rather than NGC, and that difference alone produces three failures that do not occur in the slime case and are not obvious from either repository's documentation. docs/PORT_NOTES.md records them so the next person does not rediscover them.

Relates to the slime test case's layout and env-var conventions, which this case mirrors deliberately so the two can be read side by side.

Changes

  • miles.Dockerfile — takes the upstream radixark/miles image (PyTorch 2.11 / CUDA 13.0.1, SGLang and Megatron-LM prebuilt) and adds only the AWS EFA layer: GDRCopy, the EFA installer, and NCCL/EFA runtime defaults. Rebuilding that stack on an NGC base is not viable because of wheel ABI mismatch, so the image is taken as-is. Pinned by digest — see Notes below.
  • kubernetes/ — KubeRay RayCluster, an in-cluster BuildKit Job for clusters with no local Docker daemon, a data-prep Pod, and the reward-service Deployment for the disaggregated overlay. All rendered with envsubst; every placeholder is defined in an env_vars.*.example.
  • recipe/run_grpo_qwen3_4b.sh, recipe/run_grpo_qwen3_30b_a3b.sh — dense and MoE GRPO recipes, submitting through recipe/launcher/grpo_launch.sh. The 4B recipe builds either GPU layout from COLOCATE; the MoE recipe is colocated only, and says so rather than accepting the flag and ignoring it.
  • env_vars.colocated.example, env_vars.disaggregated.example — all tunables, with the reasoning attached to the values that are not free choices (EFA count, memory fractions, the MoE flag set). No account, region, cluster or namespace values: region and account are derived from the caller's AWS credentials at source time.
  • scripts/convert_checkpoint.sh, scripts/evaluate.sh — HF/Megatron conversion and AIME-2024 evaluation.
  • reward_service/ + reward_service.Dockerfile — CPU reward service for the disaggregated overlay. Present, not exercised; marked as such.
  • docs/PORT_NOTES.md — the three nvidia/cuda-vs-NGC failures and the porting decisions.
  • docs/EFA_2NODE.md — the 2-node EFA result, the security-group prerequisite that has to be right for it, and the failure signature when it is not.
  • docs/VERIFICATION_LOG.md — what was run, on what, and what came back, read out of the trainer's TensorBoard event files rather than retyped.
  • .github/workflows/miles-pin-freshness.yml — see Notes below.

Test Plan

Environment:

  • AWS Service: Amazon EKS + KubeRay, FSx for Lustre at /fsx
  • Instance type: p5en.48xlarge (H200 141GB x8)
  • Number of nodes: 1 for the dense baseline, 2 (16 GPU) for the multi-node and MoE runs

Test commands:

# The reader's path: the shipped example with only cluster-specific values filled in
# (model and data paths, checkpoint and TensorBoard directories, namespace, FSx claim),
# and NUM_ROLLOUT lowered to keep the run short. No edits to the recipe.
cp env_vars.colocated.example env_vars
# ... fill in the cluster-specific values ...
envsubst < kubernetes/raycluster.yaml | kubectl apply -f -
bash recipe/run_grpo_qwen3_4b.sh

# Metrics were read back from the event files the trainer wrote, not from terminal output:
python3 -c "
from tensorboard.backend.event_processing.event_accumulator import EventAccumulator
a = EventAccumulator('/fsx/tb/<run>'); a.Reload()
print({t: a.Scalars(t)[0].value for t in ['train/mis_kl','rollout/raw_reward']})"

Test Results

Single-node dense baseline, shipped recipe unmodified, Ray job SUCCEEDED:

metric value reading
rollout/raw_reward 0.508 the reward model is scoring real answers
rollout/repetition_frac 0.0 generation is not degenerate
rollout/truncated_ratio 0.484 about half the responses hit the length cap, expected for a thinking model at 8192
train/grad_norm 0.614 finite and unremarkable
train/train_rollout_logprob_abs_diff 0.0129 trainer and rollout agree on the same tokens to ~1e-2
perf/step_time 356s one rollout plus one optimizer step

Two nodes (16 GPU) over EFA, 3 rollout cycles, SUCCEEDED in 676s:

rollout rollout/raw_reward actor_train_tflops perf/step_time
0 0.477 100.4 119.2s
1 0.523 245.4 73.1s
2 0.492 235.7 81.6s

Three optimizer steps evidence a closed loop across the node boundary, not convergence, and the
log says so. Fabric measured separately: NCCL all_reduce busbw 190-257 GB/s with efa-direct
and GPUDirect RDMA, no TCP fallback.

What is not verified, listed in the README as its own rows rather than left implicit:

  • Qwen3-30B-A3B MoE produces a usable model. The layout fits and every cycle completes, but
    across 4 runs generation falls into a repetition loop and reward stays 0.0
    (repetition_frac 0.48-0.70, truncated 0.97-0.99, against 0.0 / 0.43-0.58 on dense
    Qwen3-4B). Root cause unresolved; four candidate causes ruled out by measurement. Filed
    separately upstream. The ALTERNATE block that configures it is commented out with the issue
    stated inline, so it cannot be enabled without reading it.
  • The disaggregated 30B actor-8 layout — needs B300-class 288GB HBM; only the colocated 16-GPU
    layout was run.
  • Any B300 hardware. Expected to work (nothing here carries a GPU-generation literal outside the
    pinned base image) but not run.
  • The disaggregated reward service. Present, never deployed.
  • The shipped raycluster.yaml with the head on a CPU node. That is the shape that ships, but
    the validation runs used a head-on-GPU overlay because the validation cluster had no
    large-disk CPU node at the time.
  • Checkpoint save-back. save_model() fails inside Megatron's distributed-checkpoint save, so
    SAVE_INTERVAL ships above the step count and no save is triggered. This means a default run
    produces no checkpoint at all, which the README now states at the top of the conversion
    step rather than leaving the reader to discover it after hours of training.

Directory Structure

Mirrors the sibling 3.test_cases/pytorch/slime case so the two read side by side:

3.test_cases/pytorch/miles/
├── miles.Dockerfile                 # radixark/miles base (digest-pinned) + AWS EFA layer
├── reward_service.Dockerfile        # CPU reward service (disaggregated overlay)
├── README.md                        # prerequisites, walkthrough, verification status, known issues
├── requirements.txt
├── env_vars.colocated.example       # the verified default
├── env_vars.disaggregated.example   # overlay: reward model on a CPU pool
├── docs/
│   ├── PORT_NOTES.md                # nvidia/cuda-vs-NGC failures and porting decisions
│   ├── EFA_2NODE.md                 # 2-node EFA result and the SG prerequisite
│   └── VERIFICATION_LOG.md          # runs, job ids, flags, metrics
├── kubernetes/                      # RayCluster, BuildKit Job, data-prep Pod, reward Deployment
├── recipe/                          # GRPO recipes + launcher
├── scripts/                         # checkpoint conversion, evaluation
└── reward_service/                  # FastAPI reward app

Checklist

  • I have read the contributing guidelines.
  • I am working against the latest main branch.
  • I have searched existing open and recently merged PRs to confirm this is not a duplicate.
  • The contribution is self-contained with documentation and scripts.
  • External dependencies are pinned to a specific version or tag (no latest) — base image by
    digest, EFA installer and GDRCopy by version, Python packages pinned.
  • A README is included or updated with prerequisites, instructions, and known issues.
  • New test cases follow the expected directory structure — matching the sibling slime case.

littlemex and others added 15 commits July 20, 2026 08:42
Adds a PyTorch test case running GRPO post-training with miles (a direct fork of
SLIME targeting CUDA 13 / NVIDIA Blackwell sm_103) on Amazon EKS / SageMaker HyperPod,
mirroring the sibling 3.test_cases/pytorch/slime test case so users can run the same
GRPO workloads on the miles stack (SGLang rollout + Megatron-LM training via Ray).

Verified on p5en.48xlarge (H200): Qwen3-4B dense colocated GRPO on 1 and 2 nodes
(16 GPU over EFA, 3 cycles), and Qwen3-30B-A3B MoE colocated on 2 nodes (16 GPU) with
--use-distributed-optimizer + triton MoE runner. 2-node EFA NCCL busbw 190-257 GB/s.
Components that mirror slime but were not run on this hardware (head-on-CPU manifest,
30B disaggregated which needs B300 HBM, the CPU reward service) are marked UNVERIFIED.
Three miles-base pitfalls (CUDA compat shadowing / libcuda in the SGLang subprocess /
Ray driver on the head) are fixed in miles.Dockerfile + the manifests and documented in
docs/PORT_NOTES.md. Image tags pinned; no secrets or account-specific values committed.
- env_vars: SAVE_INTERVAL default 1000 (> NUM_ROLLOUT) so the shipped default does not
  hit the known save_model() pickle-truncation crash; comment explains why not to lower it.
- 4B recipe: add TP/PP/CP/EP to the required-variable validation (parity with 30B recipe);
  make --sglang-mem-fraction-static a variable (SGLANG_MEM_FRACTION:-0.8) like the 30B recipe;
  align the EXTRA_TRAIN_ARGS comment with the shipped --use-tensorboard default.
- miles.Dockerfile: correct the weight-sync note (miles uses Ray actor methods, not an
  SGLang-fork HTTP path); mark NCCL_VERSION/AWS_OFI_NCCL_VERSION as reference-only ARGs
  (nothing installs from them; they come from the base image / EFA installer).
- raycluster.yaml: head /dev/shm sizeLimit 64Gi -> 8Gi to stay within the 16Gi memory limit
  (medium:Memory counts against it); comment added.
…reshness CI

The 30B MoE configuration was written up as "Verified" on the strength of the job
completing. It does complete: the actor spans all 16 GPU without OOM and every rollout
cycle finishes. But its generation is degenerate -- across four runs the model loops and
never reaches an answer (repetition_frac 0.48-0.70 vs 0.0 on dense 4B, reward 0.0 vs
0.42-0.55). Four candidate causes were tested and refuted: response-length cap,
trainer/rollout weight divergence, sampling temperature, and checkpoint corruption. Root
cause unresolved.

Split the conflated claim into "completes without OOM/crash" (Verified) and "produces a
usable trained model" (Known Issue), and rescope the same overclaim everywhere it appears:
README table, quick-start note, launch-command comment, parallelism section, recipe STATUS
banner and runtime warning, env_vars ALTERNATE block, and the RayCluster manifest comment.

Also: re-pin MILES_BASE_TAG (the old tag is now 404 on Docker Hub), parameterise the
buildkit Job namespace, drop a docs reference to a path that does not exist in this repo,
annotate env vars that are exported but never read, and add a GPU-free workflow that fails
when a pinned external artifact stops resolving. That workflow deliberately does not build
or train -- no runner here has 8-16 H200s -- and its header says so.
… parameterising

evaluate.sh parsed its CLI flags into shell variables but never exported them, and the
Python it runs is inside a quoted heredoc, so it read the environment and fell back to
defaults. Passing --num-samples or --temperature therefore changed nothing while the run
still wrote a results file that looked authoritative. Export the parsed values, and give
the server port a single definition instead of three copies.

The buildkit Job wrote its push target as a literal account/region/tag string built from
variables the pod has no way to expand, so applying it without envsubst pushed to a
malformed registry path; it also pinned a tag that nothing else tracks. Use ${FULL_IMAGE},
which is the same value the RayCluster already consumes. Give that Job and the data-prep
pod the ${NAMESPACE} and ${FSX_CLAIM} treatment the other manifests already had.

Both recipes dereference EVAL_DATA but left it out of the required-variable check, which is
exactly the opaque mid-construction failure that check exists to prevent.

Also: document that the checkpoint conversion has to run inside a miles-image pod with the
shared filesystem mounted, since the script needs both and the quick start had it before
any such pod exists; and drop a stale NUM_NODES line the PR body already claimed was gone.
The conversion step needs the miles image, the shared filesystem, and real GPUs when
--num-gpus is passed. The quick start put it before any such pod exists, and the earlier fix
for that pointed at a path inside the image that does not exist. Copy the script into a Ray
GPU worker and run it there. Not the head: the head runs with num-gpus 0 and
NVIDIA_VISIBLE_DEVICES=void, which is the environment docs/PORT_NOTES.md already documents
as one where importing CUDA fails.

evaluate.sh passed --log-level WARN to uvicorn, which only accepts lowercase names and
raises KeyError on that value. The recipes already used "warning"; this was the same bug
left behind in one more place.

The data-prep pod carried HF_TOKEN as an empty literal in the manifest, inviting a reader to
paste a token into a file they might commit. Read it from the same hf-token Secret the
RayCluster already uses, and apply the manifest through envsubst now that it references
${NAMESPACE} and ${FSX_CLAIM}.
…in CI

radixark publishes dev-* as mutable snapshots and has already deleted a tag this file
pinned, so a dated tag gives neither reproducibility nor availability. Resolve the tag to
its digest and build FROM that, keeping the tag alongside so a reader can still tell which
snapshot it is. The Dockerfile carries the curl invocation for resolving a new one.

The freshness workflow was checking the tag, which the build no longer uses. It now checks
the digest, and separately warns when the documented tag has drifted off it -- expected for
a mutable tag, harmless for the build, but it means the comment has gone stale. Verified
against the live registry: the digest resolves 200 and currently matches the tag.
Records what was run, on what, and what came back: the submission command, the Ray job id,
the flags the job actually received (read out of the job log, not the recipe source), and the
metric values read back from the trainer's TensorBoard event files.

The run documented here is the reader's path: the shipped env example with only the
cluster-specific paths filled in, launched with the shipped recipe unmodified. It came back
SUCCEEDED with reward 0.516, repetition 0.0, truncation 0.484, and a finite grad norm.

The log also states why reward and repetition are the metrics to check rather than the exit
code, with the 30B MoE numbers next to the dense ones: both exit SUCCEEDED, and only one of
them trained anything. That distinction is the reason the verification table now has separate
rows for completing and for producing a usable model, and an earlier version of the table got
it wrong for want of exactly this log.
…d stale prose

- kubernetes/raycluster.yaml: parameterize EFA count (${EFA_PER_NODE}) and worker
  replicas (${ACTOR_NUM_NODES}). Both were hardcoded (8 of 15 allocatable EFA devices;
  replicas: 1) with a comment telling the reader to edit them for 2-node runs. A
  partial EFA request breaks point-to-point NCCL (PP/EP) while passing an all-reduce
  smoke test; a missed replica edit leaves the job waiting on placement it cannot get.
- env_vars.colocated.example: add EFA_PER_NODE=15, matching the node's actual
  allocatable count (README said 16; node reports 15).
- env_vars.disaggregated.example: define REWARD_IMAGE/REWARD_TAG, referenced by
  kubernetes/reward-service.yaml via envsubst but never defined -- rendered
  `image: ""` and the Deployment was rejected. Also correct the Spot-tolerance
  comment, which contradicted the README's own "miles lacks slime's retry" note.
- scripts/evaluate.sh: fix three scoring bugs found by independent review and
  confirmed against the real aime-2024.jsonl and a synthetic pass@k case --
  pass@k keyed on a JSONL "idx" field the data does not have (all 30 prompts
  collapsed onto key 0; reproduced: reports 1.0000 where the true value is 0.0333);
  \boxed{} extraction stopped at the first brace, misreading nested-brace answers
  like \frac{1}{2}; unbounded concurrency under a 300s timeout counted most
  requests as wrong answers once MAX_TOKENS made generation slower than the queue.
  Added bounded concurrency, a generation-sized timeout, and a non-zero exit above
  a 5% error rate.
- README.md: fix the head-vs-worker contradiction in step 4 (prose said "exec into
  its head", code block correctly targeted a worker); make step 8 explicit that the
  shipped SAVE_INTERVAL/NUM_ROLLOUT produce no checkpoint at all; replace fragile
  `ray job list | head -3` parsing and an undefined ${HEAD_POD} in the monitoring
  block; make the hf-token Secret's required-ness match the manifest instead of the
  "skip if public" prose; align the EFA hardware row with allocatable (15, not 16);
  document ACTOR_NUM_NODES driving both topology and replica count, drop the two
  variables (NUM_NODES/GPUS_PER_NODE) the doc showed that env_vars never defines.
- docs/EFA_2NODE.md: correct prose that called a partial EFA request "a throughput
  improvement, not a correctness issue" -- it is a correctness issue on P2P paths.
- docs/VERIFICATION_LOG.md: record this round -- what was found by reading rather
  than re-running, and what was re-verified (all four manifests rendered and
  validated with kubectl apply --dry-run=server against a live EKS cluster at both
  ACTOR_NUM_NODES=1 and =2; the boxed{} extractor and pass@k indexing checked
  against the real aime-2024.jsonl; every script parses with bash -n).
…al server on abort

Four defects from an adversarial review pass, all cases of a stated setting or claim not
matching what the code does:

- scripts/evaluate.sh leaked the SGLang server. The high-error-rate abort added in the
  previous commit exits non-zero from inside the heredoc, and `set -e` then skips the
  cleanup `kill` at the end of the script -- leaving a multi-GPU server holding the node.
  Cleanup now runs from an EXIT trap. Verified with a standalone reproduction: the trap
  fires on a non-zero exit, the child is reaped, and the exit code is preserved.
- Both recipes validate COLOCATE, print it, and branch on it, then pass --colocate
  unconditionally. COLOCATE=false printed "Colocated: false" (4B) or a warning (30B) and
  ran the colocated layout anyway. Neither recipe builds a disaggregated actor layout, so
  they now refuse rather than silently disagree with the env.
- env_vars.disaggregated.example twice pointed the reader at a README "Prerequisites"
  section for an `aws sagemaker update-cluster` command. No such section exists, and the
  command is HyperPod-specific while this test case targets EKS. Replaced with what is
  actually true: the pool must exist, and how you create it depends on your cluster.
- The 2-node "3 rollout cycles / Verified" row cited only docs/EFA_2NODE.md, which measures
  fabric bandwidth and says nothing about a training loop. The GRPO metrics behind that row
  (3 cycles SUCCEEDED in 676s, raw_reward 0.477/0.523/0.492) are now in the verification
  log, with an explicit note that three optimizer steps evidence a closed loop, not
  convergence.
A review pass on comment quality found that the development history had leaked out of
docs/VERIFICATION_LOG.md and into the recipes, manifests and env examples: comments narrating
what an earlier revision got wrong, what the original value was, and what a since-removed flag
used to do. That is git history and log material. What belongs next to the code is why the
current shape is correct.

- scripts/evaluate.sh: fixed a regression from the previous commit. Moving cleanup into an
  EXIT trap left a bare `wait ${SGLANG_PID}` behind in Step 3, so the SUCCESS path blocked
  forever on a server nothing had killed yet. Verified both paths now: success exits 0
  immediately, the high-error abort still exits 2, and neither leaks the server.
- Trimmed the post-mortem prose in evaluate.sh's extract_boxed docstring, the semaphore
  comment and the pass@k comment to the current-tense reason.
- Collapsed the 6-line uvicorn log-level explanation duplicated verbatim in both recipes to
  one line each, with the detail left where it already lived in docs/PORT_NOTES.md.
- Replaced the removed-flag history in the 30B recipe with the reusable trap it illustrates:
  miles registers --sglang-* from SGLang's live ServerArgs with ignore_unknown_args, so a
  flag SGLang has dropped is accepted silently and does nothing.
- Cut the COLOCATE guard from 16 lines making the same point three times to 2 comment lines
  and a 3-line error.
- raycluster.yaml, env_vars.*.example, README: removed the duplicated head-vs-worker
  explanation, the four-way-duplicated B300 portability discussion, the "do not parse ray job
  list" warning against a usage nobody suggested, and a stale NOTE claiming worker replicas are
  not driven by a variable (they are, since ACTOR_NUM_NODES was wired up).
- Rewrote "SPOT IS NOT FREE HERE", which read as a contradiction rather than a warning, and
  disambiguated "0.85 (the original hardcode)", which named no identifiable original.
- Unified British spellings to American to match the rest of the repository.
The RayCluster mounts the hf-token Secret without optional: true, so the Ray pods never start
without it and report CreateContainerConfigError rather than anything mentioning HuggingFace.
That made it a real prerequisite, but the Prerequisites list did not carry it.

Adds it as item 8, and gives step 1 the empty-value form for public models, since the pods only
need the key to exist. Verified against a live cluster that
--from-literal=HF_TOKEN= produces the HF_TOKEN key that secretKeyRef resolves.
…e one it was copied from

reward-service.yaml kept slime-reward for the Deployment, the Service and every app label, and
env_vars.disaggregated.example pointed RM_URL at that DNS name -- left over from the sibling
test case this file was adapted from, while raycluster.yaml had been renamed to miles-ray.

Beyond the inconsistency, both test cases claim the same Service name, so deploying the
disaggregated overlays of miles and slime into one namespace would collide on miles-reward's
predecessor rather than coexisting.

Renamed to miles-reward throughout, matching REWARD_IMAGE_NAME which was already miles-reward.
Verified that the rendered Service name and the RM_URL host now agree and that the manifest is
accepted by a live cluster.
… step 4

Step 8 gave the ls check and the megatron2hf conversion as bare shell commands with no
kubectl exec/cp wrapper, unlike step 4's identical script which explicitly runs it on a GPU
worker pod because the script and /fsx only exist there. A reader following step 8 literally
from their workstation hits convert_checkpoint.sh's cd ${SLIME_DIR} (default /root/miles)
failing under set -euo pipefail, since that path only exists inside the miles image.
COLOCATE named a choice the recipe could not make. --colocate was passed
unconditionally, so the recipe refused COLOCATE=false rather than honor it, and
the disaggregated layout was unreachable from here.

That layout is not a variant of the same run. miles selects the weight sync
implementation from --colocate alone: present gives UpdateWeightFromTensor,
moving weights by CUDA IPC between processes that share a device; absent gives
UpdateWeightFromDistributed, broadcasting them over NCCL, across EFA when the
actor and rollout pools sit on different nodes. A user comparing the two
placements needs both, and until now this recipe offered one.

Emit --colocate from an array so the disaggregated case contributes zero argv
tokens, and enforce the sizing rule each layout implies: colocated shares
devices, so ROLLOUT_NUM_GPUS must equal the actor GPU count; disaggregated needs
separate pools, so actor + rollout must fit CLUSTER_GPUS when the caller states
it. Over-subscribing the latter does not fail at submit time -- Ray waits on a
placement group that never becomes ready, which reads as a hang -- so refuse
while the numbers are still on screen.

Reject COLOCATE values other than the two spellings. "True", "1" and "yes" would
have fallen through to the disaggregated branch and chosen a layout nobody asked
for, which is the failure the previous refusal existed to prevent, pointing the
other way. Likewise type-check the GPU counts first: -ne against a non-numeric
value errors inside the `if`, which reads as false, so the layout check passes
without having run.

--sglang-mem-fraction-static becomes SGLANG_MEM_FRACTION, defaulting to the 0.8
this recipe has always used. Disaggregated engines do not share their devices
with the trainer and can take more, and a study that varies it needs the knob;
the value is resolved once and printed next to the layout so the banner cannot
describe a run the argv did not request.

A colocated invocation renders a byte-identical argv: 104 tokens, unchanged.
…le "disaggregated"

The Known Issue said the root cause was unresolved and that confirming it would mean
isolating the MoE path outside the GRPO loop. Did that: served the same checkpoint from
SGLang directly, no miles, no Megatron, no GRPO. It reproduces, so nothing in training
is involved.

Varying one axis at a time locates it in expert parallelism. EP=1 is clean at TP 1, 4
and 8; EP>1 degenerates at every TP tried, and the rate grows with EP (0.594 at EP=2,
0.844 at EP=4 with TP held at 8). The model's own recommended sampling still gives
0.875 and the default auto runner backend still gives 0.844, so neither the temperature
nor the triton selection was responsible, which is worth stating because both were
plausible and both were wrong.

There is no way out inside this test case. EP=1 fixes generation and cannot train: each
rank then holds all 128 experts and asks for 108.76 GiB against 139.80 GiB. Raising TP
from 8 to 16 moves that by 0.3%, since tensor parallelism does not shard expert weights.
The README now says so rather than leaving a reader to discover it.

Separately, the README used "disaggregated" for two unrelated things -- splitting the
actor and rollout GPU pools, and moving reward scoring to CPU -- and the file named
env_vars.disaggregated.example is the second one. Said which is which, and added the
4B COLOCATE=false row to the verification table as UNVERIFIED: the recipe builds that
layout now, but only the rendered argv was checked, not a completed run.
@littlemex

Copy link
Copy Markdown
Contributor Author

TODO before merge

  • Squash and clean up commit history into logical units
  • Include actual output logs from test case runs (TensorBoard metrics, job completion output)
  • Address upstream issues: SGLang expert-parallel repetition bug blocks 30B MoE validation

@KeitaW
KeitaW marked this pull request as draft August 5, 2026 01:39
@KeitaW

KeitaW commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Thank you! Feel free to open the PR once you clear the remaining TODO items.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants