Skip to content
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f9cd2e8
feat: port Qwen Flash Next training and AWEX to main
dingzhiqiang Sep 16, 2026
eb9804b
fix(engine): retain Qwen diagnostic SGLang launch override
dingzhiqiang Sep 16, 2026
d75a763
fix: preserve Qwen experiment semantics on upstream main
dingzhiqiang Sep 16, 2026
cf474a7
fix(examples): load SWE SFT config with OmegaConf
dingzhiqiang Sep 16, 2026
b590a82
fix(engine): forward reward normalization mode through remote engines
dingzhiqiang Sep 16, 2026
587d7c5
fix(engine): preserve AWEX reader device and bounded transport
dingzhiqiang Sep 16, 2026
84a116e
fix(engine): serialize AWEX metadata with scheduler GC scans
dingzhiqiang Sep 16, 2026
4f74684
fix(experimental): preserve proxy chat template options
dingzhiqiang Sep 16, 2026
940d6a7
fix(utils): restore AWEX inference after checkpoint recovery
dingzhiqiang Sep 16, 2026
81c3d78
fix(examples): align SWE inference memory with validated run
dingzhiqiang Sep 16, 2026
7d98a51
fix(examples): support fresh ten-step SWE training
dingzhiqiang Sep 17, 2026
3426e93
fix(examples): retry Arena gateway failures and support full SWE pool
dingzhiqiang Sep 17, 2026
64093ad
docs(examples): record aligned reward in ten-step 256K SWE run
dingzhiqiang Sep 18, 2026
c85d029
fix(examples): package validated SGLang QSA bounds patch
dingzhiqiang Sep 18, 2026
fc9b258
refactor(examples): separate Qwen recipes from experiment diagnostics
dingzhiqiang Sep 18, 2026
7f1d254
test: consolidate Qwen regression coverage and remove recipe notes
dingzhiqiang Sep 18, 2026
6052ad3
fix(examples): ship required SGLang QSA patches with RL recipes
dingzhiqiang Sep 18, 2026
4cfff49
refactor(models): delegate Qwen checkpoint logic to mcore-bridge
dingzhiqiang Sep 18, 2026
5abc9cc
fix: reconcile Qwen AWEX integration with main
dingzhiqiang Sep 18, 2026
b0c3ff4
fix(engine): synchronize AWEX slices before weight transfer
dingzhiqiang Sep 18, 2026
a926944
chore: merge main and reconcile proxy template options
dingzhiqiang Sep 20, 2026
ab5422b
fix(models): pin bridge with safe long-context PLE indexing
dingzhiqiang Sep 20, 2026
097e146
fix(models): pin bridge with long-context QSA indexing fix
dingzhiqiang Sep 20, 2026
8ac2497
perf(examples): persist Qwen 256K chunked CP2 recipe
dingzhiqiang Sep 21, 2026
b777e82
fix(examples): restore compatible Qwen AWEX IPC allocator
dingzhiqiang Sep 21, 2026
249b5b1
fix(engine): isolate CUDA IPC buffers from expandable training storage
dingzhiqiang Sep 21, 2026
7f30178
feat: support frozen Qwen4Exp vision with AWEX and chunked loss
dingzhiqiang Sep 21, 2026
f9e0639
fix(openai): preserve image data URIs across inference backends
dingzhiqiang Sep 21, 2026
eec0ef0
fix(examples): validate native model failure receipts for RL
dingzhiqiang Sep 21, 2026
d691d0d
fix(openai): fill unscored concat leaves with episode rewards
dingzhiqiang Sep 21, 2026
f5861c2
feat(examples): add Qwen SWE evaluation-only recipe
dingzhiqiang Sep 21, 2026
ed0daca
fix(examples): preserve pinned SWE evaluation env versions
dingzhiqiang Sep 21, 2026
a848768
fix(swe): reject nonfinite gradient norms before clipping
dingzhiqiang Sep 21, 2026
abe292c
fix(swe): allow native tasks to wait through cold training
dingzhiqiang Sep 22, 2026
5713996
docs(examples): clarify native RL timeout precedence
dingzhiqiang Sep 22, 2026
3ef85e5
fix(examples): pin selected Arena versions during RL
dingzhiqiang Sep 22, 2026
105a6d6
feat(examples): capture Qwen visual RL diagnostic batches
dingzhiqiang Sep 22, 2026
2a639f4
fix(examples): preserve rollout metadata in batch snapshots
dingzhiqiang Sep 22, 2026
d0a505f
feat(examples): replay one visual RL batch for diagnostics
dingzhiqiang Sep 22, 2026
85b55bb
fix(examples): allow replay of later visual RL batches
dingzhiqiang Sep 22, 2026
6ea0b8f
feat(examples): replay visual RL batches across weight updates
dingzhiqiang Sep 22, 2026
e11bf3d
fix(examples): pin bridge and freeze multimodal evaluation recipes
dingzhiqiang Sep 22, 2026
562c01c
fix(examples): align Arena rollout defaults and failure handling
dingzhiqiang Sep 22, 2026
8567037
build: pin official mcore-bridge for Qwen experiments
dingzhiqiang Sep 22, 2026
3a46db4
fix: backport v1 PRM fallback and explicit tool result status
dingzhiqiang Sep 22, 2026
d081449
fix(swe): reject terminal Arena evaluator errors
dingzhiqiang Sep 22, 2026
dddaa87
fix(swe): renew active Arena gateway registrations
dingzhiqiang Sep 23, 2026
8887ee0
fix(engine): preserve generated vision token provenance
dingzhiqiang Sep 23, 2026
09e431e
fix(trainer): retain input token mask before label alignment
dingzhiqiang Sep 23, 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
34 changes: 31 additions & 3 deletions areal/api/cli_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ class GenerationHyperparameters:
)
},
)
reward_normalization_use_std: bool = field(
default=True,
metadata={
"help": "Divide grouped rollout rewards by their standard deviation. "
"Set False to subtract only the mean when reward_normalization is enabled."
},
)
Comment on lines +263 to +269

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.

reward_normalization_use_std changes the relative scale of training signals across prompt groups and is not specific to Qwen Flash Next. Could we move it to a separate PR with the intended use case and tests, keeping this PR focused on model integration?

drop_incomplete_group: bool = field(
default=False,
metadata={
Expand Down Expand Up @@ -325,6 +332,7 @@ def to_openai_agents_model_settings_dict(
# OpenAI client kwargs even when users enable them.
_WORKFLOW_ONLY_ARGS: ClassVar[set[str]] = {
"reward_normalization",
"reward_normalization_use_std",
"drop_incomplete_group",
}

Expand Down Expand Up @@ -929,6 +937,15 @@ class MegatronEngineConfig:
)
},
)
language_model_only: bool = field(
default=False,
metadata={
"help": (
"For multimodal ModelScope bridges, build only the language model. "
"Set True for text-only SWE inputs and False for vision inputs."
)
},
)
# Don't use MegatronOptimizerConfig here because OmegaConf
# does not recognize the annotation "torch.dtype"
overlap_param_gather_with_optimizer_step: bool = False
Expand Down Expand Up @@ -1074,8 +1091,8 @@ class MegatronEngineConfig:
bridge_type: str = field(
default="mbridge",
metadata={
"help": "Bridge backend for MegatronEngine. Choices: 'mbridge' or 'megatron-bridge'.",
"choices": ["mbridge", "megatron-bridge"],
"help": "Bridge backend for MegatronEngine. Choices: 'mbridge', 'megatron-bridge', or 'mcore-bridge'.",
"choices": ["mbridge", "megatron-bridge", "mcore-bridge"],
},
)

Expand All @@ -1089,7 +1106,7 @@ class MegatronEngineConfig:
use_bridge_for_update_weights: bool = field(
default=False,
metadata={
"help": "When True and bridge_type='megatron-bridge', delegate live "
"help": "When True and bridge_type is 'megatron-bridge' or 'mcore-bridge', delegate live "
"weight sync to bridge.export_hf_weights instead of the hand-rolled "
"convert_to_hf registry. Required for models without a registry entry "
"(e.g. Qwen3.5). FP8 paths fall back to the registry automatically.",
Expand All @@ -1114,6 +1131,17 @@ class MegatronEngineConfig:
},
)

freeze_ple_table: bool = field(
default=True,
metadata={
"help": (
"Freeze the Qwen4-Exp PLE/N-gram table while keeping the PLE "
"projection, norm, convolution, and token embedding trainable. "
"Set False to train and export the full PLE table."
)
},
)

enable_mtp_training: bool = field(
default=False,
metadata={
Expand Down
5 changes: 5 additions & 0 deletions areal/api/engine_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def rollout_batch(
reward_normalization: bool = False,
drop_incomplete_group: bool = False,
min_usable_group_size: int = 1,
reward_normalization_use_std: bool = True,
) -> list[dict[str, Any]]:
"""Submit a batch of requests and wait for results.

Expand Down Expand Up @@ -247,6 +248,7 @@ def prepare_batch(
reward_normalization: bool = False,
drop_incomplete_group: bool = False,
min_usable_group_size: int = 1,
reward_normalization_use_std: bool = True,
) -> list[dict[str, Any]]:
"""Prepare a batch of data for training from a dataloader.

Expand Down Expand Up @@ -751,6 +753,7 @@ def submit(
reward_normalization: bool = False,
drop_incomplete_group: bool = False,
min_usable_group_size: int = 1,
reward_normalization_use_std: bool = True,
) -> int:
"""Submit a request to the inference engine and return immediately.

Expand Down Expand Up @@ -860,6 +863,7 @@ def rollout_batch(
reward_normalization: bool = False,
drop_incomplete_group: bool = False,
min_usable_group_size: int = 1,
reward_normalization_use_std: bool = True,
) -> list[dict[str, Any]]:
"""Submit a batch of requests to the inference engine and wait for the results.

Expand Down Expand Up @@ -911,6 +915,7 @@ def prepare_batch(
reward_normalization: bool = False,
drop_incomplete_group: bool = False,
min_usable_group_size: int = 1,
reward_normalization_use_std: bool = True,
) -> list[dict[str, Any]]:
"""Asynchronously submit and wait until a full batch is ready with controlled staleness.

Expand Down
37 changes: 33 additions & 4 deletions areal/dataset/gsm8k.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
# SPDX-License-Identifier: Apache-2.0

import os
from pathlib import Path

from datasets import load_dataset


def _rank_local_cache_dir(cache_dir: str | None) -> str | None:
if cache_dir is None:
return None
return str(Path(cache_dir) / f"rank-{os.getenv('RANK', '0')}")


def get_gsm8k_sft_dataset(
path: str,
split: str,
tokenizer,
max_length: int | None = None,
cache_dir: str | None = None,
**_: object,
):
dataset = load_dataset(path=path, name="main", split=split)
dataset = load_dataset(
path=path,
name="main",
split=split,
cache_dir=_rank_local_cache_dir(cache_dir),
keep_in_memory=True,

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.

This changes the default for every GSM8K caller by forcing the dataset, map, and filter results into memory, even when cache_dir is not configured. The new Qwen RL recipe does not set cache_dir, and the added test only exercises the SFT loader with an explicit cache path, so the global default behavior and RL path are untested. Why is this dataset-wide change part of the model integration?

)

def process(sample):
seq_token = tokenizer.encode(
Expand All @@ -19,11 +36,15 @@ def process(sample):
loss_mask = [0] * len(prompt_token) + [1] * (len(seq_token) - len(prompt_token))
return {"input_ids": seq_token, "loss_mask": loss_mask}

dataset = dataset.map(process).remove_columns(["question", "answer"])
dataset = dataset.map(process, keep_in_memory=True).remove_columns(
["question", "answer"]
)

if max_length is not None:
# Filter out sequences longer than max_length
dataset = dataset.filter(lambda x: len(x["input_ids"]) <= max_length)
dataset = dataset.filter(
lambda x: len(x["input_ids"]) <= max_length, keep_in_memory=True
)

return dataset

Expand All @@ -33,8 +54,16 @@ def get_gsm8k_rl_dataset(
split: str,
tokenizer,
max_length: int | None = None,
cache_dir: str | None = None,
**_: object,
):
dataset = load_dataset(path=path, name="main", split=split)
dataset = load_dataset(
path=path,
name="main",
split=split,
cache_dir=_rank_local_cache_dir(cache_dir),
keep_in_memory=True,
)

def process(sample):
messages = [
Expand Down
86 changes: 76 additions & 10 deletions areal/engine/awex/colocate_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@

from __future__ import annotations

import os
from typing import Any

import torch

from areal.engine.awex.memory_saver import patch_tms_hook_mode
from areal.engine.awex.metadata import serialize_metadata_gc

# Must run before any awex import: awex.models.registry auto-imports model
# modules at module load, and the BailingMoe module's transitive megatron import
Expand Down Expand Up @@ -60,13 +62,25 @@ def __init__(self, *args, model: torch.nn.Module, **kwargs):
self._model_device = device
super().__init__(*args, model=model, **kwargs)

def _init_reader_in_colocate_mode(self):

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.

This overrides the AWEX colocate transport for every existing model, not only Qwen4Exp, and defaults expert packing on. The pinned AWEX 0.8.1 already has an AWEX_CHUNK_MB bounded-memory path, while the added repository tests mock P2P and cannot regression-test real NCCL for existing models. Why does a Qwen recipe PR need to replace the generic transport instead of scoping or reusing the upstream path?

from areal.engine.awex.colocate_transport import (
_BoundedMemoryNcclColocateStreamBatchTransport,
)

super()._init_reader_in_colocate_mode()
self.colocate_transport = _BoundedMemoryNcclColocateStreamBatchTransport(
self.transfer_rank, self.infer_world_size
)
Comment on lines +65 to +73

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.

Could we move the shared transport changes into a separate PR? This replaces the transport for existing AWEX colocated models too, and changes buffer lifetimes and CUDA stream synchronization beyond the new model integration.

Could we also add a regression test covering multiple peers and consecutive weight updates, checking received weight values rather than only successful completion?


def _set_device(self) -> None:
# TODO(agent): This adapter is CUDA-only; physical ids remain metadata
# identities and must not be used as CUDA_VISIBLE_DEVICES indices.
torch.cuda.set_device(self._model_device)
self.barrier_device = self._model_device.index
self.backend = "nccl"
self.ready_tensor = torch.tensor(1, device=self._model_device)
self.ready_tensor = torch.tensor(
1, dtype=torch.int64, device=self._model_device
)
logger.info(
"Bound AWEX reader rank %s to model device %s",
self.transfer_rank,
Expand Down Expand Up @@ -228,6 +242,7 @@ def _select_rank0(raw_meta_list):
def get_model_arch_name(self) -> str:
return self._model_arch_name

@serialize_metadata_gc
def get_parameters_meta(self):
return self._build_params_meta()

Expand Down Expand Up @@ -267,6 +282,9 @@ def __init__(self, scheduler: Any):
self._infer_params_meta = None
self._infer_conf: dict | None = None
self._initialized = False
self._qwen4_frozen_contract = None
self._qwen4_frozen_binder = None
self._qwen4_parameter_identity = None

# ── model / context helpers ───────────────────────────────────────

Expand All @@ -289,16 +307,10 @@ def _build_model_context(self) -> dict[str, Any]:
pp_size = int(getattr(server_args, "pp_size", 1))
dp_size = int(getattr(server_args, "dp_size", 1))

from areal.engine.sglang_fork_contract import resolve_scheduler_parallel_attr

def rank_attr(name: str) -> int | None:
for obj in (
scheduler,
getattr(scheduler, "ps", None),
getattr(scheduler, "tp_worker", None),
):
value = getattr(obj, name, None) if obj is not None else None
if value is not None:
return int(value)
return None
return resolve_scheduler_parallel_attr(scheduler, name)

tp_rank = rank_attr("tp_rank")
if tp_rank is None and self._instance_local_rank is not None:
Expand Down Expand Up @@ -418,12 +430,45 @@ def _build_instance_params_meta(self):
)
return resolver.get_parameters_meta()

def _bind_qwen4_frozen_contract(self, timeout_s: float) -> None:
from pathlib import Path

from sglang.srt.managers.scheduler_components import weight_updater

from areal.models.mcore.qwen4_exp_awex import register_qwen4_exp_awex
from areal.models.mcore.qwen4_exp_awex_binding import SglangFrozenBinder
from areal.models.mcore.qwen4_exp_awex_contract import load_frozen_contract

manifest = os.environ.get("QWEN_AWEX_FROZEN_CONTRACT")
if not manifest:
raise ValueError("Qwen4Exp AWEX requires QWEN_AWEX_FROZEN_CONTRACT")
contract = load_frozen_contract(
Path(manifest), Path(self._scheduler.server_args.model_path)
)
train_info = self._meta_server_client.get_object(
"awex_train_info", timeout=timeout_s
)
if (
train_info.get("train_world_size") != self._train_world_size
or train_info.get("qwen4_exp_frozen_contract") != contract.to_dict()
):
raise ValueError("Training and inference frozen contracts differ")
binder = SglangFrozenBinder(self._get_model, contract, weight_updater)
register_qwen4_exp_awex(sglang_binder=binder)
self._qwen4_frozen_contract = contract
self._qwen4_frozen_binder = binder

def get_weight_metadata(self):
"""Inference-side parameters_meta for ONE engine instance."""
if self._engine_rank is None:
raise RuntimeError(
"AwexColocateReader must be initialized before getting weight metadata"
)
if (
type(self._get_model()).__name__ == "Qwen4ExpForConditionalGeneration"
and self._qwen4_frozen_binder is None
):
raise RuntimeError("Qwen4Exp metadata requires matching frozen contracts")
if self._infer_params_meta is None:
self._infer_params_meta = self._build_instance_params_meta()
return self._infer_params_meta
Expand Down Expand Up @@ -487,6 +532,8 @@ def initialize(

host, port = meta_server_addr.rsplit(":", 1)
self._meta_server_client = MetaServerClient(host, int(port))
if type(self._get_model()).__name__ == "Qwen4ExpForConditionalGeneration":
self._bind_qwen4_frozen_contract(timeout_s)

# Compute single-instance parameters_meta (also reused as the native
# reader's constructor arg later).
Expand All @@ -511,6 +558,10 @@ def initialize(
# awex.
"router_dtype": _get_router_dtype(awex_hf_config),
}
if self._qwen4_frozen_contract is not None:
infer_conf["qwen4_exp_frozen_contract"] = (
self._qwen4_frozen_contract.to_dict()
)
self._infer_conf = infer_conf

# Only one rank publishes the engine-instance-wide info the writer waits
Expand Down Expand Up @@ -569,6 +620,11 @@ def _ensure_reader(self) -> NCCLWorkerWeightsReader:
reader.meta_server_client, self._local_gpu_id
)
reader.initialize()
if self._qwen4_frozen_contract is not None:
self._qwen4_parameter_identity = tuple(
(name, id(parameter))
for name, parameter in self._get_model().named_parameters()
)
self._reader = reader
logger.info(
"Constructed native NCCLWorkerWeightsReader (transfer_rank=%d, "
Expand All @@ -591,6 +647,16 @@ def update_weights(self, version: int) -> None:
if not self._initialized:
raise RuntimeError("AwexColocateReader not initialized")
reader = self._ensure_reader()
if self._qwen4_frozen_contract is not None:
identity = tuple(
(name, id(parameter))
for name, parameter in self._get_model().named_parameters()
)
if identity != self._qwen4_parameter_identity:
raise RuntimeError(
"Qwen4Exp Parameters changed; reconstruct the AWEX reader after recovery"
)
reader.weight_converter.refresh_frozen_contract()
reader.update_weights(step_id=version)
self._rebuild_derived_weights()
logger.info("Colocate weight update completed: version=%d", version)
Expand Down
Loading
Loading