Skip to content

fix(rollout): train safely on incomplete groups - #6

Open
EazyReal wants to merge 21 commits into
vmax/ragged-transport-safetyfrom
vmax/mask-incomplete-rollout-groups
Open

fix(rollout): train safely on incomplete groups#6
EazyReal wants to merge 21 commits into
vmax/ragged-transport-safetyfrom
vmax/mask-incomplete-rollout-groups

Conversation

@EazyReal

@EazyReal EazyReal commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Purpose

Fork-local focused review for upstream areal-project/AReaL#1563. Do not merge this PR; the upstream PR is the landing authority.

Stack

Summary

  • represent unusable rollout slots as semantic None and retain every usable slot exactly once
  • derive min_usable_group_size from group-relative reward or advantage normalization; batch-relative estimators retain a usable singleton
  • preserve actual ragged group boundaries through normalization, redistribution, and sequence packing
  • backfill DP-dispatchable batches and synchronize optimizer minibatches without dropping a rank-local split
  • propagate deterministic workflow-contract violations terminally while keeping objective rejection retryable
  • retain the existing globally token-weighted PPO actor objective

There is deliberately no requires_peer protocol noun. The estimator owns its minimum usable sample count; the rollout boundary carries only that normalized integer contract.

Exact range

b0dbd4c423de3be706a3978fd9d1ced4678298e6..6b11d22af742baf18e3226dceb0ef6a2fd154a6c

Verification

  • affected CPU surface: 310 passed, 3 environment-dependent skips
  • distributed ragged/collective-safety surface: 2 passed, 3 environment-dependent skips
  • full pre-commit passed locally and in hosted CI
  • independent exact-head Megatron and Archon reviews found no actionable issue
  • CodeRabbit's collision and timeout findings are fixed; both retry-policy findings were withdrawn after the semantic contract was clarified; all four threads are resolved

A real multi-GPU Megatron/Archon pipeline canary is not available on this host.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7b6df2a7-4ed7-467f-a481-a0d0dc986d1c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds min_usable_group_size across rollout APIs and implementations, strengthens distributed rollout preparation and error synchronization, supports ragged grouping, introduces synchronized PPO microbatch scheduling, and adds group-level training metrics with corresponding tests and documentation.

Changes

Rollout API and propagation

Layer / File(s) Summary
Minimum usable group size propagation
areal/api/engine_api.py, areal/engine/*, areal/experimental/engine/*, areal/infra/controller/*, tests/test_train_controller.py
Public rollout methods, engine adapters, and controllers accept and forward min_usable_group_size.
Distributed rollout preparation and redistribution
areal/infra/dist_rollout.py, tests/test_data_redistribution.py, tests/torchrun/redistribute.py
Ragged trajectory gathering, head-only preparation, retry handling, and collective error synchronization are added.
Grouped rollout usability and metrics
areal/infra/remote_inf_engine.py, tests/test_grouped_rollout_workflow.py, tests/test_incomplete_rollout_groups.py, docs/*/reference/rollout_workflow.md
Grouped workflows enforce usable-slot thresholds, validate merged slot cardinality, and record group statistics; documentation reflects the new behavior.
PPO group metrics and synchronized microbatches
areal/utils/data.py, areal/trainer/ppo/*, areal/trainer/rl_trainer.py, tests/test_utils.py, tests/test_incomplete_rollout_groups.py
PPO updates use synchronized microbatch schedules, transport-only padding, group metrics, and trainable-batch collection constraints.
Ragged partitioning and validation
areal/utils/seqpack.py, areal/utils/data.py, areal/infra/controller/train_controller.py, tests/test_seqpack.py, tests/test_eval_dispatch.py, tests/test_reward_norm_variable_group.py, tests/v2/training_service/test_data_proxy_unit.py
Uneven partitions are supported while invalid rank counts and missing divisibility metadata remain rejected.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: garrett4wade, dingzhiqiang

Poem

A rabbit hopped through rollout lanes,
Sorting groups through distributed trains.
Tiny slots found their proper place,
Microbatches marched in pace.
“Usable!” cried the bunny bright—
Metrics bloomed by lantern light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: making rollout training safe for incomplete groups.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vmax/mask-incomplete-rollout-groups

Comment @coderabbitai help to get the list of available commands.

@EazyReal
EazyReal force-pushed the vmax/mask-incomplete-rollout-groups branch 6 times, most recently from 50b6ffa to 682003a Compare July 24, 2026 11:06
@EazyReal
EazyReal force-pushed the vmax/mask-incomplete-rollout-groups branch from 8850bb4 to 18b64a8 Compare July 29, 2026 05:57
@EazyReal EazyReal changed the title feat(rollout): mask untrainable incomplete groups fix(rollout): train safely on incomplete groups Jul 29, 2026
@EazyReal
EazyReal changed the base branch from review/issue-1559-base to vmax/ragged-transport-safety July 29, 2026 10:18
@EazyReal

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EazyReal

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
areal/api/engine_api.py (1)

205-226: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound min_usable_group_size before rollout retries.

A grouped rollout can provide at most group_size usable slots. If min_usable_group_size > group_size, arun_episode rejects every group, and dynamic preparation can retry forever. Validate 1 <= min_usable_group_size <= group_size at the shared rollout boundary and document the constraint.

Also applies to: 247-269, 750-784, 858-887, 908-954

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@areal/api/engine_api.py` around lines 205 - 226, Validate
min_usable_group_size at the shared rollout boundary before any rollout retries,
requiring 1 <= min_usable_group_size <= group_size; reject invalid values early
so arun_episode and dynamic preparation cannot retry indefinitely. Update the
parameter documentation and all corresponding rollout entry points to state this
constraint, including the methods covering the referenced grouped rollout paths.
🧹 Nitpick comments (3)
tests/test_train_controller.py (1)

579-608: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover non-default forwarding.

Both tests exercise only the default value. Call the controller methods with group_size=2 and min_usable_group_size=2, then assert that 2 reaches the rollout mock.

Also applies to: 610-638

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_train_controller.py` around lines 579 - 608, Update the
prepare_batch delegation tests, including the additional test covering the same
path, to pass group_size=2 and min_usable_group_size=2 to the controller and
assert mock_rollout.prepare_batch receives both values as 2 instead of the
defaults.
areal/infra/remote_inf_engine.py (1)

724-730: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting min_usable_group_size on rollout_batch/prepare_batch too.

Only submit gained the parameter docs (Lines 1250-1251); the other two public entrypoints now accept the same argument with no docstring entry.

Also applies to: 751-751, 849-849, 1233-1233, 1250-1251, 1275-1275, 1328-1328, 1361-1361, 1415-1415

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@areal/infra/remote_inf_engine.py` around lines 724 - 730, Document the
min_usable_group_size parameter in the docstrings for the public rollout_batch
and prepare_batch entrypoints, matching the existing description and behavior
documented for submit. Ensure each entrypoint’s parameter list explains the
valid range and purpose without changing the validation or implementation.
tests/torchrun/redistribute.py (1)

14-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_HybridTrainEngine omits cpu_group/config, which the coordinator touches on the success path.

Both assertions in _test_hybrid_error raise before _broadcast_and_redistribute_trajectories reaches dist.barrier(group=self.train_engine.cpu_group), so this passes today. Adding a cpu_group (and a config stub) makes the fake engine resilient if the error path ever shifts, instead of failing with a confusing AttributeError.

♻️ Suggested hardening
 class _HybridTrainEngine:
     def __init__(self, rank, dp_group, model_group):
         self.rank = rank
         self.data_parallel_group = dp_group
         self.context_and_model_parallel_group = model_group
+        self.cpu_group = model_group
+        self.config = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/torchrun/redistribute.py` around lines 14 - 24, Update
_HybridTrainEngine to define the cpu_group attribute and a config stub expected
by _broadcast_and_redistribute_trajectories, initializing them in __init__
alongside the existing rank and process-group fields. Preserve the current test
behavior while ensuring the fake engine can safely reach the coordinator’s
success path without raising AttributeError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@areal/infra/dist_rollout.py`:
- Around line 346-390: Bound the retry behavior in _prepare_until_dispatchable
when dynamic_bs is enabled: track attempts, emit progress diagnostics comparable
to _collect_trainable_rollout_batch, and apply the established retry/backoff
approach if available. After the configured limit is reached without a
dispatchable batch, raise a clear RuntimeError instead of looping indefinitely;
preserve the existing synchronization and fixed-batch behavior.

In `@areal/trainer/rl_trainer.py`:
- Around line 102-128: Bound retries in _collect_trainable_rollout_batch when
dynamic_bs is enabled by enforcing a finite attempt limit (or equivalent
timeout) while collecting undersized batches. After the limit is exceeded, raise
a clear actionable error that includes the collected and required group counts;
preserve the current successful return and fixed-batch RuntimeError behavior.

In `@areal/utils/data.py`:
- Around line 934-989: Update split_training_batch_into_microbatches so
effective_n_mbs provides at least enough slots for the largest local microbatch
contribution, while preserving the requested n_mbs limit when it is sufficient.
Replace the scheduled-slot assert with explicit collision handling that fails
loudly rather than overwriting data. Add coverage for a rank whose split count
exceeds n_mbs, such as by forcing a small max_tokens_per_mb, including the
synchronized schedule path used by PPO actor and critic updates.

In `@tests/test_data_redistribution.py`:
- Around line 52-78: Add timeout=60 to the subprocess.run invocation in
test_redistribute_ragged_cpu, matching the existing hybrid redistribution test
while preserving the current command and output assertions.

---

Outside diff comments:
In `@areal/api/engine_api.py`:
- Around line 205-226: Validate min_usable_group_size at the shared rollout
boundary before any rollout retries, requiring 1 <= min_usable_group_size <=
group_size; reject invalid values early so arun_episode and dynamic preparation
cannot retry indefinitely. Update the parameter documentation and all
corresponding rollout entry points to state this constraint, including the
methods covering the referenced grouped rollout paths.

---

Nitpick comments:
In `@areal/infra/remote_inf_engine.py`:
- Around line 724-730: Document the min_usable_group_size parameter in the
docstrings for the public rollout_batch and prepare_batch entrypoints, matching
the existing description and behavior documented for submit. Ensure each
entrypoint’s parameter list explains the valid range and purpose without
changing the validation or implementation.

In `@tests/test_train_controller.py`:
- Around line 579-608: Update the prepare_batch delegation tests, including the
additional test covering the same path, to pass group_size=2 and
min_usable_group_size=2 to the controller and assert mock_rollout.prepare_batch
receives both values as 2 instead of the defaults.

In `@tests/torchrun/redistribute.py`:
- Around line 14-24: Update _HybridTrainEngine to define the cpu_group attribute
and a config stub expected by _broadcast_and_redistribute_trajectories,
initializing them in __init__ alongside the existing rank and process-group
fields. Preserve the current test behavior while ensuring the fake engine can
safely reach the coordinator’s success path without raising AttributeError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24659cf9-e44a-4e85-bf21-a9c4bf2c4632

📥 Commits

Reviewing files that changed from the base of the PR and between b0dbd4c and 18b64a8.

📒 Files selected for processing (27)
  • areal/api/engine_api.py
  • areal/engine/fsdp_engine.py
  • areal/engine/megatron_engine.py
  • areal/engine/sglang_remote.py
  • areal/engine/vllm_remote.py
  • areal/experimental/engine/archon_engine.py
  • areal/infra/controller/rollout_controller.py
  • areal/infra/controller/train_controller.py
  • areal/infra/dist_rollout.py
  • areal/infra/remote_inf_engine.py
  • areal/trainer/ppo/actor.py
  • areal/trainer/ppo/critic.py
  • areal/trainer/rl_trainer.py
  • areal/utils/data.py
  • areal/utils/seqpack.py
  • docs/en/reference/rollout_workflow.md
  • docs/zh/reference/rollout_workflow.md
  • tests/test_data_redistribution.py
  • tests/test_eval_dispatch.py
  • tests/test_grouped_rollout_workflow.py
  • tests/test_incomplete_rollout_groups.py
  • tests/test_reward_norm_variable_group.py
  • tests/test_seqpack.py
  • tests/test_train_controller.py
  • tests/test_utils.py
  • tests/torchrun/redistribute.py
  • tests/v2/training_service/test_data_proxy_unit.py

Comment thread areal/infra/dist_rollout.py
Comment thread areal/trainer/rl_trainer.py
Comment thread areal/utils/data.py
Comment thread tests/test_data_redistribution.py Outdated
@EazyReal
EazyReal force-pushed the vmax/mask-incomplete-rollout-groups branch from 18b64a8 to 6b11d22 Compare July 29, 2026 19:21
@EazyReal

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EazyReal

Copy link
Copy Markdown
Owner Author

CodeRabbit disposition on current head 6b11d22:

  • fixed the microbatch-slot collision and added the forced local_count > n_mbs regression
  • added the distributed subprocess timeout
  • centralized 1 <= min_usable_group_size <= group_size validation before retries
  • covered non-default forwarding and documented the constraint on all rollout entry points
  • kept dynamic objective rejection open-ended by design; terminal contract/configuration failures now propagate immediately across local and remote execution
  • did not add unused cpu_group/config fields to an error-path-only test fake, or filler docstrings solely to raise a generic coverage percentage

Current evidence: 310 affected tests passed with 3 environment skips, distributed ragged tests passed, and full pre-commit passed.

wuallen57730 and others added 8 commits July 31, 2026 10:29
…roject#1569)

agenerate read the weight version twice: once when building the
request and again when recording output_versions. A weight update
landing in between labelled tokens with a version that did not
generate them, which skews staleness checks such as the decoupled
loss and rejection sampling.

Pin the version before the request goes out and reuse it when
recording, so each segment of a trajectory carries the version that
actually served it.
…real-project#1544)

use_deterministic_algorithms previously set deterministic_mode only on
the built model's config. Several Megatron-Core and TransformerEngine
code paths consume determinism settings at module construction and
cache them in instance state:

- VocabParallelEmbedding copies config.deterministic_mode at __init__;
  without it the nondeterministic F.embedding backward is used.
- TEDotProductAttention validates NVTE_ALLOW_NONDETERMINISTIC_ALGO
  against deterministic_mode only at __init__, and TE snapshots its
  deterministic flag from that env var and the global torch switch at
  construction.

Setting the flag after the model was built therefore engaged only
runtime consumers (loss fusions, schedules) and silently left the
layer-level kernels nondeterministic.

Changes:
- Set deterministic_mode on the TransformerConfig before the model is
  built, keeping the post-build call for runtime consumers.
- Select AttnBackend.flash under deterministic mode: Megatron-Core owns
  the NVTE_*_ATTN selection env vars and asserts they match the config,
  and the cuDNN fused-attention deterministic backward needs workspaces
  that grow prohibitively with context length.
- Export NVTE_ALLOW_NONDETERMINISTIC_ALGO=0 to trainer processes from
  the launchers and to megatron worker specs in single-controller mode,
  so it is in place before TransformerEngine reads it regardless of TE
  version; warn when the setting may have come too late.

With this, repeated runs of the same batch produce bitwise-identical
training stats, including grad_norm.
…(incl. areal-project#1577) (areal-project#1579)

* fix: emit PEFT-standard disk LoRA adapter keys so vLLM can load them, and align sglang best-effort unload test assertion (areal-project#1577)

* test(inference-service): raise VLM controller init timeout to 600s to avoid vLLM startup timeout
…real-project#1573)

Upstream SGLang does not enable the mamba cache path for the Bailing
hybrid MoE family, so radix cache stays off and prefix reuse is lost.
Add an opt-in script that applies the same eight edits the Ling hybrid
fork carries, covering SGLang 0.5.9 and 0.5.10.post1, which moved the
DP-attention padding helper behind a dp_size prologue.

The script is reversible and supports a dry run, so the target tree can
be checked before anything is written.
…ndering (areal-project#1499)

* fix(openai): align proxy tool schemas with sglang chat-completions rendering

The proxy path renders the request `tools` block through LiteLLM-style
dicts, while sglang's native /v1/chat/completions route round-trips
tools through its pydantic `Tool` model before applying the chat
template. The two renderings differ in field order and default-field
presence (e.g. sglang's `Function` dumps `strict: false` while the
proxy omits it), so the same trajectory produces different prompt
token ids on the two paths and training rewards drift from evaluation
results.

Round-trip tools through sglang's `Tool` model on the proxy path so
the dicts fed to `apply_chat_template` byte-match sglang's own
rendering. Flat Responses `FunctionToolParam` entries (top-level
`name`/`parameters`, no `function` key) are first normalized to the
nested Chat shape that both sglang's model and the chat template
expect. The alignment only runs when the engine class name identifies
an sglang backend; other backends are left unaligned, and per-tool
validation failures fall back to the original dict.

Ported from a verified internal fix.

* test(openai): cover tool schema alignment against sglang rendering

Assert the aligned dicts match what sglang's own Tool model produces, so
a regression that reintroduces the prompt-token drift fails here rather
than showing up as a train/eval reward mismatch.

Also pin the documented fallbacks: flat Responses tools normalize to the
nested Chat shape, a single invalid tool does not fail the batch, and
unsupported entries pass through unchanged.
)

* feat(mcore): apply fp32 lm head forward when enabled

enable_fp32_lm_head only reached the model through mbridge extra args, so
it was dropped for configs whose TransformerConfig rejects the field and
the flag silently had no effect. Patch the lm_head/output_layer forward
after the model is built instead, so the existing flag applies on every
Megatron construction path.

The patch is skipped when the flag is off, when Megatron already provides
a native fp32 column-parallel head, and when a module was already
patched, and it never runs for critic models, which replace the output
layer with a value head.

* fix(mcore): forward the tensor-parallel group from the fp32 lm head

The patched forward mirrors ColumnParallelLinear, which passes self.tp_group to
copy_to_tensor_model_parallel_region and
gather_from_tensor_model_parallel_region. Both calls omitted it, so they fell
back to the default tensor-model-parallel group and ran the collectives on the
wrong ranks whenever the head was built with a non-default group. The sequence
parallel path already forwarded the group, so the two behaved inconsistently.

Take the group once and route all three calls through a helper that drops the
keyword on megatron-core releases that do not accept it. Runs on the default
group are unaffected: the wrappers resolve None through
get_tensor_model_parallel_group_if_none, so passing it explicitly is identical
to omitting it.

Document the patch on _fp32_lm_head_forward_impl: what it fixes, the
megatron-core version it was verified against, and how it drifts if upstream
changes the forward or the collective signatures.
…l-project#1572)

Rejection sampling narrows the loss mask, so the existing token counters no
longer describe what the update actually trained on. Report the total, valid and
masked token counts alongside the masked ratio, and split the log-prob drift
into signed and absolute forms so a run's staleness is visible per step.

prompt_len is derived from the first trained position rather than the difference
of the two mask sums, which stops reporting the prompt as longer than it is once
rejection removes generated tokens from the loss mask. The mask reaching
_ppo_update is rolled left by one, so the roll is undone before the lookup.
Le8r0nJames and others added 6 commits August 6, 2026 22:43
areal-project#1545)

* fix(infra): fail fast when a local inference server dies during launch

_wait_for_server already receives the server process handle but never
inspects it: when the server crashes at startup (port conflict,
scheduler init error, OOM kill), the launcher keeps polling the health
endpoint until setup_timeout expires and then raises a bare
TimeoutError. Poll the process each iteration and raise immediately
with the exit code and a pointer to the server traceback instead,
which also makes the scheduler-level launch retries practical since a
failed attempt now surfaces in seconds rather than after the full
timeout. Log a heartbeat every 60s while waiting and include the
address and timeout in the TimeoutError message.

* test(infra): cover fail-fast on dead inference server process

Requested in review: mock process.poll() returning an exit code and
assert that _wait_for_server raises RuntimeError carrying the
returncode instead of waiting for the health-check timeout.

* fix(infra): shut the server down on any launch failure

The fail-fast path raises RuntimeError, which escaped the TimeoutError
handler in launch_server and skipped _shutdown_one_server. The skip happens
to be harmless today because the address is not registered yet and the dead
process needs no kill, but the two failure modes should not diverge.

Catch both and report the underlying reason instead of assuming a timeout.
* perf: reduce Megatron training memory peaks

Add an SFT profiling workflow and use its memory snapshots to remove full-sequence vocabulary and optimizer gradient peaks from Megatron training.

Key changes:
- Add rank-aware kernel and memory profiling for packed SFT workloads
- Fuse FP32 vocab-parallel logprob storage with LM head backward
- Add optional true chunked LM head loss with recomputed backward
- Configure precision-aware optimizer fields before Megatron validation
- Cover BF16/FP32 numerical parity and distributed TP/SP behavior

* fix(models): avoid private storage identity checks

Track the LM head output tensor weakly and compare storage through the public data_ptr API. This preserves allocator-address reuse protection without depending on PyTorch's private storage _cdata field.

* test: compare parameter storage without object identity

Parameter.data may return a fresh Tensor wrapper on each access. Verify that replicated parameters retain their data pointer and storage offset instead of comparing transient Python objects.

* test: make recycled CUDA storage check deterministic

Construct the replacement tensor from the original storage instead of relying on the caching allocator to immediately reuse a freed address after the full CI suite.

* fix(engine): guard AReaL LM Head storage reuse

Keep entropy differentiable unless its gradients are disabled, and make the optimized LM Head path opt-in. Warn when destructive storage reuse makes entropy non-differentiable, while rejecting unsupported NPU and tree-training combinations.

* fix(engine): export standard FSDP LoRA adapter keys

PEFT keeps the adapter name in live parameter FQNs, while serving engines expect serialized LoRA keys without it. Normalize per-parameter FSDP exports and align the SGLang best-effort assertion with its load behavior.

* feat(engine): support chunked logits for padded models

Enable chunked LM Head loss for text-only padded BSHD models such as Qwen3.5 and rename the public toggle to enable_chunked_logits so the configuration reflects its behavior.

Key changes:
- Add padded label construction and output repacking
- Add Qwen3.5 and updated Qwen3 MoE profile recipes
- Update CLI docs, validation, and regression coverage

* fix(engine): configure logprob chunking explicitly

Replace the profile-only environment override with a validated train-engine option so FSDP, Megatron, Archon, and tree paths use the same explicit value.

Key changes:
- add and document TrainEngineConfig.logprobs_chunk_size
- pass the setting through every engine logprob path
- translate the profile guide and remove out-of-scope FSDP LoRA changes
- add config, launcher, and explicit chunk-size tests

Refs: areal-project#1555
…project#1500)

* feat(colocate): support AWEX colocated actor-rollout training

Add shared-GPU colocated training: the Megatron actor and the SGLang
rollout engine time-share the same devices, coordinated through AWEX
weight transfer.

- AWEX weight-sync writer with tag-based offload/onload and an SGLang
  engine plugin implementing the colocate reader protocol.
- Colocation scheduling support in the Slurm scheduler and controllers
  (pause/resume generation around the train phase, engine-level
  offload orchestration, recover handling).
- Megatron model registration and fast HF checkpoint loading for the
  Bailing hybrid MoE family.

Both sides key the CUDA IPC handoff on physical GPU ids, mapping through
the device mask. Deriving them differently only agrees when the mask
starts at zero, so a process pinned to any other GPU would await a key
its peer never publishes.

Pausing for a weight update keeps SGLang's default mode, which aborts
in-flight requests and returns their partial output so the client
resumes them by extending the prompt. A second in-place pause raises
the scheduler's paused flag so the colocate loop services awex work.
Splitting it this way leaves the scheduler fully idle, which SGLang
requires before releasing memory, and other backends keep their
single-request pause.

Supports SGLang 0.5.9 and 0.5.10, whose removed decode-stat hooks are
now optional.

Verified on multi-node colocated RL runs: training statistics align
with the separated-GPU baseline, and single-batch replays produce
bitwise-identical training stats. A single-node example test covers the
colocate path end to end.

* fix(colocate): gate colocation-only steps on the v1 AWEX colocate setup

weight_update_mode alone does not identify the colocated v1 run. Controller v2
selects AWEX from use_lora and never reads that field, so a v2 separation run
may legitimately carry weight_update_mode="awex" and would then take the v1
colocation handover: its offload was disabled, a stray meta server started,
awex_colocate_mode was forced onto its SGLang servers, and both checkpoint
branches were skipped so it wrote nothing at all.

Gate on _is_v1_awex_colocate, which also requires controller v1 and an
actor-rollout colocation strategy. The weight-update meta dispatch keeps its
comparison: it sits in an elif chain that v2 short-circuits earlier.

Behaviour is unchanged for every configuration that exists today; the three
colocated example configs still select the path and the separation config still
does not.

* refactor(colocate): configure the allocator per role instead of at import

AWEX_ACTOR_ALLOC_CONF existed because the colocate examples point rollout at
${actor.scheduling_spec}, so both roles shared one env_vars mapping while the
actor wants expandable_segments and SGLang's memory saver cannot tolerate it.
Applying it required rewriting PYTORCH_CUDA_ALLOC_CONF from the first lines of
the top-level package, because the `from .infra` chain initializes CUDA and
freezes the allocator config; that put argv sniffing and an environment mutation
into every `import areal`, and the same block was duplicated in rpc_server.

Give each colocated role its own scheduling_spec env_vars instead: the env then
reaches the process through `srun --env`, before it starts, so no import-time
hook is needed. Drop the mechanism and both copies.

The mirror of it in the SGLang plugin never worked. It ran from the __main__
block, long after the module-level `from areal.utils import ...` had already
frozen the allocator config, so it rewrote the variable while allocations stayed
expandable. Replace it with an assertion that runs before any areal import and
fails loudly, since a silently self-disabled memory saver surfaces much later as
a colocate OOM or an invalid CUDA IPC target.

* perf(mcore): build the gloo mirror group only when the engine can offload

resolve_broadcast_target reads cpu_model_parallel_group only after an offloaded
engine has handed the accelerator to rollout and device collectives are
unusable. Creating it unconditionally cost one gloo new_group per data-parallel
group at startup on every run, including separation runs that never offload.

Gate it on the engine's offload config. Nothing changes on the consumer side:
the attribute already defaults to None and resolve_broadcast_target falls back
to the device group in that case.

* fix(colocate): do not require a colocation strategy to select v1 AWEX

The gate added earlier also demanded an actor-rollout colocation scheduling
strategy. AWEX runs opt in through weight_update_mode and leave actor and
rollout on the default separation strategy, so the gate went false for every
such run: the controller never started the AWEX meta server nor passed its
address to the rollout, each training worker then started a server of its own,
and the rollout registered against a different one. The run waited on
'infer_conf' until the timeout with no error.

Keep the controller-version check, which is what stops a v2 separation run from
taking this path, and drop the strategy check. The truth table in the test was
asserting the broken behaviour, so it is corrected alongside.

* docs(examples): add the colocated AWEX GRPO config

Mirrors the two-GPU setup the example test exercises: a Megatron actor and an
SGLang rollout time-sharing both GPUs, with weights handed over through AWEX.

The TMS switches live in the actor's own scheduling_spec env_vars rather than a
global toggle: SGLang opens its own memory-saver regions for the colocated
rollout, and an auto-opened region on the training side would nest inside them.

* refactor: drop the unrelated rpc_server import reformat

The parenthesised import left behind by removing the AWEX allocator
environment knob carries no semantic change, so rpc_server.py no longer
needs to appear in this change set at all.

* refactor(recover): fold the colocate helpers into RecoverHandler

Both helpers are only reachable from RecoverHandler.load(), so keeping
them at module level widened the public surface of areal.utils.recover
for no caller. They become private static methods next to the existing
_ensure_recover_supported/_normalize_recover_engines pair.
…eal-project#1583)

Workers are long-lived rpc_server processes, so any terminal state means they
are gone. The status check only reacted to FAILED and CANCELLED, so a job that
reached COMPLETED - for instance the batch script exiting 0 after a container
FATAL - was still treated as healthy and the controller waited on workers that
no longer existed. NODE_FAIL was not mapped at all.

squeue also exits non-zero once a job leaves its window ("Invalid job id
specified" right after completion), which is indistinguishable from a transient
slurmctld error at the call site. Ask sacct in that case: a terminal state means
the workers are gone, anything else is treated as transient and retried.
Address review on the incomplete-group transport and collection paths:

- Replace the serial per-item broadcast fallback for ragged trajectory
  lists with one metadata all-gather plus one padded all-gather per
  dtype/device bucket, so unequal group counts cost a handful of
  collectives instead of sum(lengths) container broadcasts. Ranks with
  zero trajectories join the same collectives via the gathered metadata,
  and gathered tensors are cloned out of the padded buffers so a skewed
  distribution does not pin world_size x max-payload memory.
- Split redistribute_trajectories into its gather phase and a pure-local
  packing phase, and recover only the latter: every head holds the same
  gathered data there, so failures are symmetric and always reach the
  error sync; a failure inside a collective now propagates instead of
  posting mismatched operations at peers.
- Abort dynamic preparation only on a sustained stall: at least eight
  consecutive rounds that add no trainable group AND thirty minutes
  without progress. Both signals ride the existing per-round all-gather,
  so every head raises on the same iteration and the coordinated error
  path turns a silent SPMD spin into a terminal error on all ranks,
  while fast legitimate all-reject bursts (e.g. a staleness flush after
  a weight update) stay alive.

Refs: areal-project#1563
Signed-off-by: EazyReal <8047065+EazyReal@users.noreply.github.com>
Address review on estimator-owned group minimums and the tightened
workflow contract:

- NormConfig gains uses_group_statistics and PPOActorConfig owns
  minimum_usable_group_size, replacing the trainer-side free function
  and its ad-hoc shape duck-typing.
- A singleton target group is complete by definition, so group
  statistics no longer hard-fail n_samples=1 configs; the previous
  ValueError broke examples/openclaw on startup while the v2 rollout
  path skipped the check entirely. Config-level degeneracy stays a
  UserWarning, as on main.
- The trainer collection loop shares the empty-round streak plus
  wall-clock stall bound and rate-limits its progress warnings.
- WorkflowContractError now names the two remedies (one sample per
  episode or batch-level normalization), and the one-sample-per-slot
  contract is documented in the RolloutWorkflow docstring and the
  grouped-rollout reference (EN/ZH), scoped honestly to grouped
  rollouts: n_samples=1 installs no group wrapper and is not checked.
- Correct docstrings that still described divisibility-based dispatch:
  balanced_greedy_partition, _pad_eval_batch, and the v2 data-proxy
  dispatcher module.

Refs: areal-project#1563
Signed-off-by: EazyReal <8047065+EazyReal@users.noreply.github.com>
@EazyReal
EazyReal force-pushed the vmax/mask-incomplete-rollout-groups branch from 3e7fa87 to 9392e20 Compare August 9, 2026 23:04
Reviewer request on areal-project#1563: expose the partial-group trainability
threshold instead of only deriving it. PPOActorConfig gains
min_usable_group_size (default None); None keeps the derived value
(2 when reward_norm/adv_norm uses group statistics, 1 for singleton
target groups, else 1), an explicit value replaces it and is still
validated against group_size at workflow setup.

Signed-off-by: Max Wang <maxwill@vmax.ai>
With n_samples=1 every prompt group is a singleton, so group mean
centering erases the task reward. Batch centering keeps a live signal;
singleton group std is already pinned to 1 and stays a no-op.

Signed-off-by: Max Wang <maxwill@vmax.ai>
…e-rollout-groups

# Conflicts:
#	areal/trainer/ppo/actor.py
Reviewer request on areal-project#1563: an explicit min_usable_group_size below 2
combined with group-relative normalization would train lone survivors
that have no group peers to normalize against, so PPOActorConfig now
rejects it at construction. The derived default is unaffected.

Signed-off-by: Max Wang <maxwill@vmax.ai>
std_level: group is not the no-op I claimed on the review thread: the
OpenAI-proxy agent exports one row per interaction, so compute_advantages
passes per-episode row counts as group sizes and a k-turn episode's
identical rewards collapse to sign(r - mean) * sqrt((k-1)/k), discarding
reward magnitude. Batch std matches adv_norm and keeps the signal; the
group_size line had no remaining consumer.

Signed-off-by: Max Wang <maxwill@vmax.ai>
Two exactness gaps around the new config field. The v2 rollout path never
consumes it, so an explicit setting now fails fast there (matching
RolloutControllerV2's rejection of reward_normalization and
drop_incomplete_group) and the help text names the v1 scope. The
one-sample-per-slot contract is armed by the resolved minimum, not by
group normalization itself, so the error message, arun_episode docstring,
and reference docs now name the real trigger and include unsetting
actor.min_usable_group_size among the remedies; the agent tutorial and
export-style reference gain the same caveat where they teach
export_style: individual.

Signed-off-by: Max Wang <maxwill@vmax.ai>
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.

5 participants