Skip to content
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
c1df884
feat(trainer): add multi-objective policy distillation
yulangz Aug 11, 2026
a51aa18
fix: propagate role env to forked workers
yulangz Aug 11, 2026
3794efa
feat: offload SGLang CUDA graphs during AWEX training
yulangz Aug 11, 2026
a0be494
Merge branch 'main' into feature/mopd
yulangz Aug 11, 2026
e249e32
Merge remote-tracking branch 'github/main' into feature/mopd
yulangz Aug 12, 2026
a05754b
fix: restore MOPD CI compatibility
yulangz Aug 12, 2026
47f36e0
chore: merge main into feature/mopd
yulangz Aug 12, 2026
627a4f4
chore: merge main into feature/mopd
yulangz Aug 12, 2026
476fc62
test: stabilize MOPD CI fixtures
yulangz Aug 12, 2026
7cad29b
fix(engine): stabilize AWEX colocated weight updates
yulangz Aug 13, 2026
ce6b7a7
feat(engine): add GPU-staged optimizer support
yulangz Aug 17, 2026
f7f7f7c
feat(examples): add CPU-staged Qwen3 MoE example
yulangz Aug 17, 2026
62a7b4a
test: consolidate staged optimizer coverage
yulangz Aug 18, 2026
07fda30
feat: add DAPO-Math CPU-staged training example
Aug 20, 2026
8fd2a5e
fix(dataset): use compatible chat template helper
Aug 21, 2026
7dd2bed
fix: release colocated actor memory between phases
Aug 24, 2026
0365518
feat(engine): integrate CPU-staged optimizer residency
Aug 24, 2026
e1fc363
fix(engine): use native AWEX Qwen3 converter
Aug 24, 2026
d5bd29b
feat(engine): decouple staged offload from optimizer choice
Aug 25, 2026
e97a42d
test(engine): add staged optimizer parity coverage
Aug 27, 2026
3c90fba
refactor(engine): share staged optimizer runtime
Aug 27, 2026
eb63993
refactor(engine): reuse native staged update rules
Aug 27, 2026
e9ecd7e
perf(engine): select staged AdamW update backend
Aug 27, 2026
08826c7
test(engine): expand staged optimizer parallel coverage
Aug 28, 2026
f4c94a2
fix(engine): handle staged optimizer schedules and empty owners
Sep 7, 2026
e851361
chore: merge main into optimizer offload branch
Sep 7, 2026
551b48a
fix(engine): use physical GPU ranks for AWEX TP servers
Sep 7, 2026
27b110c
fix(models): disable bridge wgrad fusion without native extension
Sep 7, 2026
812992a
refactor: simplify staged optimizers and add Qwen3.5 example
Sep 7, 2026
335cf93
fix(engine): budget padded microbatches by rectangular input size
Sep 8, 2026
6fc019c
fix(ci): provision Muon dependency and correct test fixtures
Sep 8, 2026
5da34c0
fix(ci): install Muon dependency in a runner-writable directory
Sep 8, 2026
b8a97bd
test: reduce tree training memory and always release engines
Sep 9, 2026
8c909e3
fix(engine): avoid ephemeral ports for SGLang rendezvous
Sep 10, 2026
25d91c2
chore: merge main into optimizer offload branch
Sep 18, 2026
d745230
Merge remote-tracking branch 'origin/main' into feature/optimizer_off…
Sep 20, 2026
fbc070f
test: align unit tests with runtime backends
Sep 20, 2026
fff0213
Merge remote-tracking branch 'origin/main' into feature/optimizer_off…
Sep 21, 2026
1d6f822
refactor(engine): split Muon from staged AdamW
Sep 21, 2026
193decb
feat(engine): add distributed Muon optimizer offload
Sep 21, 2026
8228c45
chore: merge main and align CI fixtures
Sep 22, 2026
0186f36
test(engine): align optimizer fixture with model config
Sep 22, 2026
8c8eddd
chore: sync Muon branch with optimizer offload
Sep 22, 2026
163773d
fix(engine): keep AWEX transfer ranks dense
Sep 22, 2026
eea70a1
chore: sync review fixes into Muon branch
Sep 22, 2026
e9e05d4
test: defer VLM model path resolution during collection
Sep 22, 2026
c5f294a
chore: sync lazy VLM fixture into Muon branch
Sep 22, 2026
b63aeae
fix(ci): preserve checkout path for worker processes
Sep 22, 2026
02acb1e
fix(ci): preserve Muon dependency path for workers
Sep 22, 2026
b00bd98
chore: merge main into optimizer offload branch
Sep 29, 2026
339495d
fix(engine): adapt Muon offload to Megatron Core 0.19
Sep 29, 2026
8221943
refactor(engine): build Muon through Megatron 0.19 API
Sep 29, 2026
bac678a
fix(engine): merge main and refresh offload test fixtures
Oct 8, 2026
aa585fe
fix(engine): sync Muon with optimizer offload and main
Oct 8, 2026
231e92b
fix(ci): align AWEX with latest main before unit tests
Oct 9, 2026
8a65023
chore: sync AWEX 0.9 and CI dependency setup into Muon branch
Oct 9, 2026
4a2a677
fix(engine): rebuild staged optimizer state after HF reload
Oct 12, 2026
37431a6
fix(engine): avoid double-offset AWEX transfer ranks
Oct 12, 2026
be24155
refactor(engine): split activation tracking from optimizer offload
Oct 12, 2026
d62bc7e
chore: sync optimizer offload review fixes into Muon branch
Oct 12, 2026
773c99a
fix(engine): merge main and fix Muon resume validation
Oct 16, 2026
386337d
chore: merge latest main into Muon optimizer offload
Nov 6, 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
18 changes: 17 additions & 1 deletion .github/workflows/test-areal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,22 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Sync AWEX dependency
run: |
AWEX_REQUIREMENT=$("$VIRTUAL_ENV/bin/python" - <<'PY'
import tomllib
with open("pyproject.toml", "rb") as f:
dependencies = tomllib.load(f)["project"]["dependencies"]
print(next(dep for dep in dependencies if dep.startswith("awex==")))
PY
)
uv pip install --python "$VIRTUAL_ENV/bin/python" --no-deps "$AWEX_REQUIREMENT"

- name: Install Muon dependency in the prebuilt environment
run: |
export PATH="/opt/.venv/bin:$PATH"
uv pip install --python /opt/.venv/bin/python --target "$RUNNER_TEMP/muon-deps" --no-deps "emerging-optimizers==0.3.0"

- name: Validate Docker installation
run: |
export PATH="/opt/.venv/bin:$PATH"
Expand All @@ -316,7 +332,7 @@ jobs:
env:
CI: true
HF_TOKEN: ${{ secrets.HF_TOKEN }}
PYTHONPATH: ${{ github.workspace }}
PYTHONPATH: ${{ github.workspace }}:${{ runner.temp }}/muon-deps
TOKENIZERS_PARALLELISM: false
AREAL_IS_IN_CI: 1
VIRTUAL_ENV: /opt/.venv
Expand Down
102 changes: 100 additions & 2 deletions areal/api/cli_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,55 @@ def to_openai_args_dict(
# Train Engine Configs


@dataclass
class MuonOptimizerConfig:
"""Algorithm settings for Megatron's distributed Muon optimizer."""

momentum: float = 0.95
use_nesterov: bool = False
fp32_matmul_prec: str = field(
default="medium",
metadata={"choices": ["low", "medium", "high"]},
)
coefficient_type: str = "quintic"
num_ns_steps: int = 5
scale_mode: str = field(
default="spectral",
metadata={"choices": ["spectral", "unit_rms_norm", "shape_scaling"]},
)
split_qkv: bool = True
tp_mode: str = field(
default="duplicated",
metadata={"choices": ["blockwise", "duplicated", "distributed"]},
)
extra_scale_factor: float = 1.0

def __post_init__(self) -> None:
if not 0.0 <= self.momentum < 1.0:
raise ValueError("optimizer.muon.momentum must be in [0, 1)")
if self.fp32_matmul_prec not in {"low", "medium", "high"}:
raise ValueError(
"optimizer.muon.fp32_matmul_prec must be low, medium, or high"
)
if self.num_ns_steps < 1:
raise ValueError("optimizer.muon.num_ns_steps must be at least 1")
if self.scale_mode not in {
"spectral",
"unit_rms_norm",
"shape_scaling",
}:
raise ValueError(
"optimizer.muon.scale_mode must be spectral, unit_rms_norm, "
"or shape_scaling"
)
if self.tp_mode not in {"blockwise", "duplicated", "distributed"}:
raise ValueError(
"optimizer.muon.tp_mode must be blockwise, duplicated, or distributed"
)
if self.extra_scale_factor <= 0:
raise ValueError("optimizer.muon.extra_scale_factor must be positive")


@dataclass
class OptimizerConfig:
"""Configuration for model optimization during training."""
Expand All @@ -399,8 +448,9 @@ class OptimizerConfig:
metadata={
"help": "Optimizer type. For FSDP Engine, adam_bf16 enables memory-efficient BF16 optimizer states. "
"For Megatron Engine, adam_bf16 requires dtype=bfloat16 and is automatically converted to adam "
"with precision-aware optimizer enabled.",
"choices": ["adam", "sgd", "adam_bf16"],
"with precision-aware optimizer enabled. dist_muon selects Megatron's layer-wise distributed "
"Muon optimizer and uses AdamW for scalar/non-matrix parameters.",
"choices": ["adam", "sgd", "adam_bf16", "dist_muon"],
},
)
lr: float = field(default=1e-3, metadata={"help": "Learning rate"})
Expand All @@ -423,6 +473,10 @@ class OptimizerConfig:
"help": "Adam epsilon parameter. Only effective when optimizer_type is adam/adam_bf16"
},
)
muon: MuonOptimizerConfig = field(
default_factory=MuonOptimizerConfig,
metadata={"help": "Muon algorithm settings used when type=dist_muon."},
)
min_lr_ratio: float = field(
default=0.0,
metadata={
Expand Down Expand Up @@ -914,6 +968,36 @@ class FP8EngineConfig:
)


@dataclass
class CPUStagedOffloadConfig:
"""Bounded GPU staging for CPU-resident Megatron optimizer state."""

enabled: bool = field(
default=False,
metadata={
"help": (
"Keep optimizer state in pinned CPU memory and stream bounded "
"update units through GPU staging buffers. The optimizer algorithm "
"is selected independently by optimizer.type. Megatron only."
)
},
)
buffer_count: int = field(
default=2,
metadata={"help": "Number of reusable GPU optimizer staging buffers."},
)
bucket_size_mb: float = field(
default=128.0,
metadata={"help": "Maximum size in MiB of one GPU optimizer staging unit."},
)

def __post_init__(self) -> None:
if self.buffer_count < 1:
raise ValueError("cpu_staged_offload.buffer_count must be at least 1")
if self.bucket_size_mb <= 0:
raise ValueError("cpu_staged_offload.bucket_size_mb must be positive")


@dataclass
class MegatronEngineConfig:
"""Configuration for Megatron-LM training framework.
Expand Down Expand Up @@ -962,6 +1046,15 @@ class MegatronEngineConfig:
main_params_dtype: str = "float32"
exp_avg_dtype: str = "float32"
exp_avg_sq_dtype: str = "float32"
cpu_staged_offload: CPUStagedOffloadConfig = field(
default_factory=CPUStagedOffloadConfig,
metadata={
"help": (
"Keep the selected optimizer's state CPU-resident and use bounded "
"GPU staging buffers for optimizer steps."
)
},
)

# Checkpointing Configuration
async_save: bool = field(
Expand Down Expand Up @@ -1174,6 +1267,11 @@ class MegatronEngineConfig:
)

def __post_init__(self) -> None:
if self.cpu_staged_offload.enabled and self.async_save:
raise ValueError(
"megatron.async_save is not supported with "
"megatron.cpu_staged_offload.enabled=true"
)
if self.enable_mtp_training and not self.enable_mtp:
raise ValueError("enable_mtp_training requires enable_mtp=True")
if self.mtp_only:
Expand Down
11 changes: 11 additions & 0 deletions areal/dataset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"virl39k",
"hh-rlhf",
"torl_data",
"dapo-math-17k",
"swe_sft",
]

Expand Down Expand Up @@ -174,6 +175,16 @@ def _get_custom_dataset(
max_length=max_length,
**kwargs,
)
elif "dapo-math-17k" in path.lower() and type == "rl":
from .dapo_math_17k import get_dapo_math_17k_rl_dataset

return get_dapo_math_17k_rl_dataset(
path=path,
split=split,
tokenizer=tokenizer,
max_length=max_length,
**kwargs,
)
elif (
_SWE_PATH_PATTERN.search(path.lower()) or _is_swe_sft_artifact(path)
) and type == "sft":
Expand Down
60 changes: 60 additions & 0 deletions areal/dataset/dapo_math_17k.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0

from pathlib import Path

from datasets import Dataset, load_dataset

from areal.utils.hf_utils import apply_chat_template


def get_dapo_math_17k_rl_dataset(
path: str,
split: str,
tokenizer,
max_length: int | None = None,
) -> Dataset:
"""Load local DAPO-Math-17k parquet data for RL training."""
del split
dataset_root = Path(path)
parquet_files = sorted((dataset_root / "data").glob("*.parquet"))
json_files = sorted(dataset_root.glob("*.jsonl")) + sorted(
dataset_root.glob("*.json")
)
if parquet_files:
dataset = load_dataset(
"parquet",
data_files=[str(data_file) for data_file in parquet_files],
split="train",
)
elif json_files:
dataset = load_dataset(
"json",
data_files=[str(data_file) for data_file in json_files],
split="train",
)
else:
raise FileNotFoundError(
f"No parquet or JSON data files found under {dataset_root}"
)

def process(sample):
answer = sample.get("label")
if answer is None:
answer = sample["reward_model"]["ground_truth"]
return {"messages": sample["prompt"], "answer": str(answer)}

dataset = dataset.map(process, remove_columns=dataset.column_names)

if max_length is not None:
if tokenizer is None:
raise ValueError("tokenizer is required when max_length is set")

def filter_length(sample):
prompt_ids = apply_chat_template(
tokenizer, sample["messages"], add_generation_prompt=True, tokenize=True
)
return len(prompt_ids) <= max_length

dataset = dataset.filter(filter_length)

return dataset
Loading
Loading