From 9a3b29711d2ca8e8bc789a1fdc0a44c839623c35 Mon Sep 17 00:00:00 2001 From: Ratish P <114130421+Ratish1@users.noreply.github.com> Date: Wed, 31 Dec 2025 07:40:25 +0400 Subject: [PATCH 01/57] feat: Implement lazy data loading for Dataset (#246) Co-authored-by: zhaochenyang20 Co-authored-by: PopSoda2002 --- miles/ray/rollout_data_source.py | 1 + miles/rollout/data_source.py | 1 + miles/rollout/sglang_rollout.py | 1 + miles/utils/arguments.py | 6 ++ miles/utils/data.py | 144 +++++++++++++++++++++++-------- 5 files changed, 115 insertions(+), 38 deletions(-) diff --git a/miles/ray/rollout_data_source.py b/miles/ray/rollout_data_source.py index c9df08f4f..e962a29f8 100644 --- a/miles/ray/rollout_data_source.py +++ b/miles/ray/rollout_data_source.py @@ -43,6 +43,7 @@ def __init__(self, args): apply_chat_template=args.apply_chat_template, apply_chat_template_kwargs=args.apply_chat_template_kwargs, seed=args.rollout_seed, + num_proc=args.num_proc, ) if self.args.rollout_shuffle: self.dataset.shuffle(self.epoch_id) diff --git a/miles/rollout/data_source.py b/miles/rollout/data_source.py index 613319d34..ca9b80f94 100644 --- a/miles/rollout/data_source.py +++ b/miles/rollout/data_source.py @@ -75,6 +75,7 @@ def __init__(self, args): apply_chat_template=args.apply_chat_template, apply_chat_template_kwargs=args.apply_chat_template_kwargs, seed=args.rollout_seed, + num_proc=args.num_proc, ) if self.args.rollout_shuffle: self.dataset.shuffle(self.epoch_id) diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 2e33542a5..6ed83092b 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -478,6 +478,7 @@ async def eval_rollout_single_dataset( tool_key=dataset_cfg.tool_key, apply_chat_template=args.apply_chat_template, apply_chat_template_kwargs=args.apply_chat_template_kwargs, + num_proc=args.num_proc, ) dataset = EVAL_PROMPT_DATASET[cache_key] diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index ce6e47161..07b03fcbc 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -577,6 +577,12 @@ def add_data_arguments(parser): "and should be set to a larger value than `max_tokens_per_gpu` if you want better performance. " ), ) + parser.add_argument( + "--num-proc", + type=int, + default=8, + help="Number of processes for dataset initialization and filtering.", + ) return parser def add_eval_arguments(parser): diff --git a/miles/utils/data.py b/miles/utils/data.py index c36902c81..e26cee33c 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -1,9 +1,10 @@ import json import logging import os -import random import re +from functools import partial +import datasets import numpy as np import pandas as pd import ray @@ -16,6 +17,16 @@ logger = logging.getLogger(__name__) +_FILE_TYPE_MAP = { + ".jsonl": "json", + ".parquet": "parquet", +} + + +def _filter_func(example, tokenizer, processor, max_length, prompt_key, multimodal_keys, apply_chat_template_kwargs): + prompt = _build_messages(example, prompt_key, multimodal_keys) + return not _should_skip_prompt(prompt, tokenizer, processor, max_length, apply_chat_template_kwargs) + # TODO: don't read the whole file into memory. def read_file(path): @@ -124,53 +135,110 @@ def __init__( seed=42, apply_chat_template=False, apply_chat_template_kwargs=None, + num_proc=8, ): - self.origin_samples = [] - for data in read_file(path): - prompt = _build_messages(data, prompt_key, multimodal_keys) - - metadata = data.get(metadata_key) or {} - if tool_key is not None and tool_key in data: - tools = data[tool_key] - if isinstance(tools, str): - tools = json.loads(tools) - elif isinstance(tools, np.ndarray): - tools = tools.tolist() - assert isinstance(tools, list), f"tools must be a list, got {type(tools)} instead" - metadata["tools"] = tools - - # TODO: this is slow. - if _should_skip_prompt(prompt, tokenizer, processor, max_length, apply_chat_template_kwargs): - continue + # 1. Store basic config + self.tokenizer = tokenizer + self.processor = processor + self.max_length = max_length + self.prompt_key = prompt_key + self.multimodal_keys = multimodal_keys + self.label_key = label_key + self.tool_key = tool_key + self.metadata_key = metadata_key + self.apply_chat_template_kwargs = apply_chat_template_kwargs or {} + self.seed = seed + self.epoch_id = -1 - self.origin_samples.append( - Sample( - prompt=prompt, - label=data[label_key] if label_key is not None else None, - metadata=metadata, - ) - ) + # 2. Load and process dataset + self.hf_dataset = self._load_and_filter_dataset(path, num_proc) + self.origin_hf_dataset = self.hf_dataset - self.epoch_id = -1 - self.seed = seed - self.samples = self.origin_samples + def _get_file_type(self, path: str) -> str: + _, ext = os.path.splitext(path) + + try: + return _FILE_TYPE_MAP[ext] + except KeyError: + raise ValueError(f"Unsupported format: {ext}. Supported: {list(_FILE_TYPE_MAP.keys())}") from None + + def _load_and_filter_dataset(self, path, num_proc): + raw_file_path, row_slice = _parse_generalized_path(path) + + if not os.path.exists(raw_file_path): + raise FileNotFoundError(f"Prompt dataset path '{raw_file_path}' does not exist.") + + logger.info(f"Loading dataset from {raw_file_path} using Hugging Face datasets.") + + # Determine file type and load using datasets library for memory-mapped access + file_type = self._get_file_type(raw_file_path) + ds = datasets.load_dataset(file_type, data_files=raw_file_path, split="train") + + # Apply row slicing if specified + if row_slice: + num_rows = len(ds) + indices = range(num_rows)[row_slice] + ds = ds.select(indices) + logger.info(f"Applied slice {row_slice}, dataset size: {len(ds)}") + + filter_kwargs = { + "tokenizer": self.tokenizer, + "processor": self.processor, + "max_length": self.max_length, + "prompt_key": self.prompt_key, + "multimodal_keys": self.multimodal_keys, + "apply_chat_template_kwargs": self.apply_chat_template_kwargs, + } + + original_size = len(ds) + + ds = ds.filter(partial(_filter_func, **filter_kwargs), num_proc=num_proc, desc="Filtering invalid samples") + + new_size = len(ds) + logger.info(f"Filtered dataset from {original_size} to {new_size} samples.") + + return ds + + def __len__(self): + return len(self.hf_dataset) + + def __getitem__(self, idx): + # The underlying HF dataset handles lazy fetching + data = self.hf_dataset[idx] + + # Process the data using existing logic + prompt = _build_messages(data, self.prompt_key, self.multimodal_keys) + + metadata = data.get(self.metadata_key) or {} + if self.tool_key is not None and self.tool_key in data: + tools = data[self.tool_key] + if isinstance(tools, str): + tools = json.loads(tools) + # TODO (chenyang): If the JSON parsing is heavy, we might need + # to use hf_dataset.map() during init to pre-process these + # fields into a more efficient format (Arrow-native), rather + # than parsing raw strings on the fly. + elif isinstance(tools, np.ndarray): + tools = tools.tolist() + assert isinstance(tools, list), f"tools must be a list, got {type(tools)} instead" + metadata["tools"] = tools + + sample = Sample( + prompt=prompt, + label=data.get(self.label_key) if self.label_key is not None else None, + metadata=metadata, + ) + + return sample def shuffle(self, new_epoch_id): if self.epoch_id == new_epoch_id: return - random.seed(self.seed + new_epoch_id) - permutation = list(range(len(self.samples))) - random.shuffle(permutation) - self.samples = [self.origin_samples[i] for i in permutation] + logger.info(f"Shuffling dataset for epoch {new_epoch_id} with seed {self.seed + new_epoch_id}") + self.hf_dataset = self.origin_hf_dataset.shuffle(seed=self.seed + new_epoch_id) self.epoch_id = new_epoch_id - def __getitem__(self, idx): - return self.samples[idx] - - def __len__(self): - return len(self.samples) - def get_minimum_num_micro_batch_size(total_lengths, max_tokens_per_gpu): # use first fit to get the number of micro batches From b0d3341be1771e1ed63583ab9f62f739da472c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=99=A8=E9=98=B3?= Date: Tue, 30 Dec 2025 20:09:35 -0800 Subject: [PATCH 02/57] Revert "feat: Implement lazy data loading for Dataset" (#372) --- miles/ray/rollout_data_source.py | 1 - miles/rollout/data_source.py | 1 - miles/rollout/sglang_rollout.py | 1 - miles/utils/arguments.py | 6 -- miles/utils/data.py | 144 ++++++++----------------------- 5 files changed, 38 insertions(+), 115 deletions(-) diff --git a/miles/ray/rollout_data_source.py b/miles/ray/rollout_data_source.py index e962a29f8..c9df08f4f 100644 --- a/miles/ray/rollout_data_source.py +++ b/miles/ray/rollout_data_source.py @@ -43,7 +43,6 @@ def __init__(self, args): apply_chat_template=args.apply_chat_template, apply_chat_template_kwargs=args.apply_chat_template_kwargs, seed=args.rollout_seed, - num_proc=args.num_proc, ) if self.args.rollout_shuffle: self.dataset.shuffle(self.epoch_id) diff --git a/miles/rollout/data_source.py b/miles/rollout/data_source.py index ca9b80f94..613319d34 100644 --- a/miles/rollout/data_source.py +++ b/miles/rollout/data_source.py @@ -75,7 +75,6 @@ def __init__(self, args): apply_chat_template=args.apply_chat_template, apply_chat_template_kwargs=args.apply_chat_template_kwargs, seed=args.rollout_seed, - num_proc=args.num_proc, ) if self.args.rollout_shuffle: self.dataset.shuffle(self.epoch_id) diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 6ed83092b..2e33542a5 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -478,7 +478,6 @@ async def eval_rollout_single_dataset( tool_key=dataset_cfg.tool_key, apply_chat_template=args.apply_chat_template, apply_chat_template_kwargs=args.apply_chat_template_kwargs, - num_proc=args.num_proc, ) dataset = EVAL_PROMPT_DATASET[cache_key] diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 07b03fcbc..ce6e47161 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -577,12 +577,6 @@ def add_data_arguments(parser): "and should be set to a larger value than `max_tokens_per_gpu` if you want better performance. " ), ) - parser.add_argument( - "--num-proc", - type=int, - default=8, - help="Number of processes for dataset initialization and filtering.", - ) return parser def add_eval_arguments(parser): diff --git a/miles/utils/data.py b/miles/utils/data.py index e26cee33c..c36902c81 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -1,10 +1,9 @@ import json import logging import os +import random import re -from functools import partial -import datasets import numpy as np import pandas as pd import ray @@ -17,16 +16,6 @@ logger = logging.getLogger(__name__) -_FILE_TYPE_MAP = { - ".jsonl": "json", - ".parquet": "parquet", -} - - -def _filter_func(example, tokenizer, processor, max_length, prompt_key, multimodal_keys, apply_chat_template_kwargs): - prompt = _build_messages(example, prompt_key, multimodal_keys) - return not _should_skip_prompt(prompt, tokenizer, processor, max_length, apply_chat_template_kwargs) - # TODO: don't read the whole file into memory. def read_file(path): @@ -135,110 +124,53 @@ def __init__( seed=42, apply_chat_template=False, apply_chat_template_kwargs=None, - num_proc=8, ): - # 1. Store basic config - self.tokenizer = tokenizer - self.processor = processor - self.max_length = max_length - self.prompt_key = prompt_key - self.multimodal_keys = multimodal_keys - self.label_key = label_key - self.tool_key = tool_key - self.metadata_key = metadata_key - self.apply_chat_template_kwargs = apply_chat_template_kwargs or {} - self.seed = seed - self.epoch_id = -1 - - # 2. Load and process dataset - self.hf_dataset = self._load_and_filter_dataset(path, num_proc) - self.origin_hf_dataset = self.hf_dataset - - def _get_file_type(self, path: str) -> str: - _, ext = os.path.splitext(path) - - try: - return _FILE_TYPE_MAP[ext] - except KeyError: - raise ValueError(f"Unsupported format: {ext}. Supported: {list(_FILE_TYPE_MAP.keys())}") from None - - def _load_and_filter_dataset(self, path, num_proc): - raw_file_path, row_slice = _parse_generalized_path(path) - - if not os.path.exists(raw_file_path): - raise FileNotFoundError(f"Prompt dataset path '{raw_file_path}' does not exist.") - - logger.info(f"Loading dataset from {raw_file_path} using Hugging Face datasets.") - - # Determine file type and load using datasets library for memory-mapped access - file_type = self._get_file_type(raw_file_path) - ds = datasets.load_dataset(file_type, data_files=raw_file_path, split="train") - - # Apply row slicing if specified - if row_slice: - num_rows = len(ds) - indices = range(num_rows)[row_slice] - ds = ds.select(indices) - logger.info(f"Applied slice {row_slice}, dataset size: {len(ds)}") - - filter_kwargs = { - "tokenizer": self.tokenizer, - "processor": self.processor, - "max_length": self.max_length, - "prompt_key": self.prompt_key, - "multimodal_keys": self.multimodal_keys, - "apply_chat_template_kwargs": self.apply_chat_template_kwargs, - } - - original_size = len(ds) - - ds = ds.filter(partial(_filter_func, **filter_kwargs), num_proc=num_proc, desc="Filtering invalid samples") - - new_size = len(ds) - logger.info(f"Filtered dataset from {original_size} to {new_size} samples.") - - return ds + self.origin_samples = [] + for data in read_file(path): + prompt = _build_messages(data, prompt_key, multimodal_keys) + + metadata = data.get(metadata_key) or {} + if tool_key is not None and tool_key in data: + tools = data[tool_key] + if isinstance(tools, str): + tools = json.loads(tools) + elif isinstance(tools, np.ndarray): + tools = tools.tolist() + assert isinstance(tools, list), f"tools must be a list, got {type(tools)} instead" + metadata["tools"] = tools + + # TODO: this is slow. + if _should_skip_prompt(prompt, tokenizer, processor, max_length, apply_chat_template_kwargs): + continue - def __len__(self): - return len(self.hf_dataset) + self.origin_samples.append( + Sample( + prompt=prompt, + label=data[label_key] if label_key is not None else None, + metadata=metadata, + ) + ) - def __getitem__(self, idx): - # The underlying HF dataset handles lazy fetching - data = self.hf_dataset[idx] - - # Process the data using existing logic - prompt = _build_messages(data, self.prompt_key, self.multimodal_keys) - - metadata = data.get(self.metadata_key) or {} - if self.tool_key is not None and self.tool_key in data: - tools = data[self.tool_key] - if isinstance(tools, str): - tools = json.loads(tools) - # TODO (chenyang): If the JSON parsing is heavy, we might need - # to use hf_dataset.map() during init to pre-process these - # fields into a more efficient format (Arrow-native), rather - # than parsing raw strings on the fly. - elif isinstance(tools, np.ndarray): - tools = tools.tolist() - assert isinstance(tools, list), f"tools must be a list, got {type(tools)} instead" - metadata["tools"] = tools - - sample = Sample( - prompt=prompt, - label=data.get(self.label_key) if self.label_key is not None else None, - metadata=metadata, - ) - - return sample + self.epoch_id = -1 + self.seed = seed + self.samples = self.origin_samples def shuffle(self, new_epoch_id): if self.epoch_id == new_epoch_id: return - logger.info(f"Shuffling dataset for epoch {new_epoch_id} with seed {self.seed + new_epoch_id}") - self.hf_dataset = self.origin_hf_dataset.shuffle(seed=self.seed + new_epoch_id) + random.seed(self.seed + new_epoch_id) + permutation = list(range(len(self.samples))) + random.shuffle(permutation) + self.samples = [self.origin_samples[i] for i in permutation] self.epoch_id = new_epoch_id + def __getitem__(self, idx): + return self.samples[idx] + + def __len__(self): + return len(self.samples) + def get_minimum_num_micro_batch_size(total_lengths, max_tokens_per_gpu): # use first fit to get the number of micro batches From 8ba715e5714dd83d6335b6136d8d59e1f61ba396 Mon Sep 17 00:00:00 2001 From: Ying Sheng Date: Tue, 30 Dec 2025 20:48:59 -0800 Subject: [PATCH 03/57] [MISC] add codeowners (#373) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..5d094d3de --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +.github/ @fzyzcjy @yushengsu-thu @Ying1123 +/miles/ @fzyzcjy @yueming-yuan From bc61a7d55fcd2739dbba565949022ae808fa7ed0 Mon Sep 17 00:00:00 2001 From: Ying Sheng Date: Tue, 30 Dec 2025 21:01:23 -0800 Subject: [PATCH 04/57] [Misc] update codeowners (#374) --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5d094d3de..dc0cc7cbc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ -.github/ @fzyzcjy @yushengsu-thu @Ying1123 +.github/CODEOWNERS @fzyzcjy @Ying1123 +.github/workflows/ @yushengsu-thu /miles/ @fzyzcjy @yueming-yuan From 8a988dc3d2b1a752a31bda078caecf74bd32891c Mon Sep 17 00:00:00 2001 From: miles-code-angel Date: Fri, 2 Jan 2026 18:42:42 -0800 Subject: [PATCH 05/57] [auto-sync] update code (#383) --- build_conda.sh | 4 - docker/Dockerfile | 21 +- docker/Dockerfile.rocm_MI350-5 | 504 +++++------ docker/Dockerfile_20250810_9a48ba0.rocm | 2 +- docker/Dockerfile_20250810_c22f55b.rocm | 2 +- docker/justfile | 11 + docker/patch/latest/megatron.patch | 816 +++++++----------- docker/patch/latest/sglang.patch | 610 ++++++++----- docker/version.txt | 2 +- docs/README.md | 2 +- docs/en/advanced/pd-disaggregation.md | 5 + docs/en/advanced/speculative-decoding.md | 2 +- docs/en/examples/deepseek-r1.md | 4 +- docs/en/examples/glm4-9B.md | 4 +- docs/en/examples/glm4.5-355B-A32B.md | 6 +- docs/en/examples/qwen3-30B-A3B.md | 4 +- docs/en/examples/qwen3-4B.md | 4 +- docs/en/examples/qwen3-4b-base-openhermes.md | 4 +- docs/en/get_started/customization.md | 409 +++++++++ docs/en/get_started/qa.md | 2 +- docs/en/get_started/quick_start.md | 8 + docs/en/get_started/usage.md | 90 +- docs/en/index.rst | 7 +- docs/en/platform_support/amd_tutorial.md | 29 +- examples/DrGRPO/README.md | 50 ++ examples/DrGRPO/custom_reducer.py | 67 ++ examples/geo3k_vlm/README.md | 75 +- examples/geo3k_vlm/fsdp_vs_megatron.png | Bin 0 -> 102247 bytes examples/geo3k_vlm/rewards.png | Bin 1475375 -> 0 bytes examples/geo3k_vlm/run_geo3k_vlm.py | 132 --- examples/geo3k_vlm/run_geo3k_vlm.sh | 225 +++++ examples/geo3k_vlm/run_geo3k_vlm_sft.sh | 186 ++++ examples/geo3k_vlm_multi_turn/README.md | 41 + examples/geo3k_vlm_multi_turn/__init__.py | 1 + examples/geo3k_vlm_multi_turn/base_env.py | 25 + examples/geo3k_vlm_multi_turn/env_geo3k.py | 273 ++++++ .../geo3k_vlm_multi_turn_config.yaml | 2 + examples/geo3k_vlm_multi_turn/rollout.py | 371 ++++++++ .../run_geo3k_vlm_multi_turn.py | 167 ++++ .../vlm_multi_turn_geo3k_reward.png | Bin 0 -> 53073 bytes examples/multi_agent/agent_system.py | 20 +- examples/on_policy_distillation/README.md | 58 ++ .../on_policy_distillation.py | 3 +- .../run-qwen3-8B-opd.sh | 1 + examples/retool/generate_with_retool.py | 20 +- examples/search-r1/generate_with_search.py | 17 +- examples/train_infer_mismatch_helper/mis.py | 126 ++- .../run-qwen3-4b-fsdp-mis.sh | 148 ++++ miles/backends/fsdp_utils/actor.py | 97 ++- miles/backends/fsdp_utils/data_packing.py | 16 +- .../fsdp_utils/kernels/fused_experts.py | 165 +++- .../fused_moe_triton_backward_kernels.py | 540 ++++++++++++ .../fsdp_utils/update_weight_utils.py | 16 +- miles/backends/megatron_utils/__init__.py | 32 +- miles/backends/megatron_utils/actor.py | 38 +- miles/backends/megatron_utils/arguments.py | 2 + miles/backends/megatron_utils/checkpoint.py | 5 +- miles/backends/megatron_utils/ci_utils.py | 84 ++ .../megatron_utils/config_mapping/__init__.py | 12 - .../predefined_config_mappers.py | 128 --- .../megatron_utils/config_mapping/registry.py | 55 -- miles/backends/megatron_utils/data.py | 100 +++ miles/backends/megatron_utils/initialize.py | 5 + .../kernels/int4_qat/fake_int4_quant_cuda.cu | 368 ++++++++ .../megatron_utils/kernels/int4_qat/setup.py | 39 + miles/backends/megatron_utils/loss.py | 47 +- .../megatron_to_hf/deepseekv3.py | 18 + .../megatron_to_hf/qwen3_next.py | 7 +- miles/backends/megatron_utils/model.py | 116 ++- .../backends/megatron_utils/model_provider.py | 38 + .../hf_weight_iterator_bridge.py | 3 +- miles/backends/sglang_utils/sglang_engine.py | 90 +- miles/ray/actor_group.py | 11 +- miles/ray/rollout.py | 82 +- miles/ray/rollout_data_source.py | 186 ---- miles/ray/train_actor.py | 2 +- miles/rollout/rm_hub/math_utils.py | 2 +- miles/rollout/sft_rollout.py | 16 +- miles/rollout/sglang_rollout.py | 107 ++- miles/utils/arguments.py | 201 +++-- miles/utils/data.py | 101 ++- miles/utils/eval_config.py | 3 + miles/utils/flops_utils.py | 54 +- miles/utils/health_monitor.py | 28 +- miles/utils/http_utils.py | 80 +- miles/utils/mask_utils.py | 70 +- miles/utils/megatron_bridge_utils.py | 9 +- miles/utils/metric_utils.py | 2 +- miles/utils/misc.py | 4 + miles/utils/ppo_utils.py | 69 +- miles/utils/processing_utils.py | 55 +- miles/utils/seqlen_balancing.py | 6 +- miles/utils/types.py | 7 +- miles_plugins/mbridge/qwen3_next.py | 9 +- miles_plugins/models/glm4.py | 10 +- miles_plugins/models/hf_attention.py | 3 +- miles_plugins/models/qwen3_next.py | 5 +- scripts/models/qwen3-1.7B.sh | 2 +- scripts/models/qwen3-235B-A22B.sh | 2 +- scripts/models/qwen3-30B-A3B.sh | 2 +- scripts/models/qwen3-8B.sh | 2 +- scripts/models/qwen3-next-80B-A3B.sh | 4 + scripts/run-qwen3-235B-A22B-sft.sh | 1 + scripts/run-qwen3-4B-base-sft.sh | 1 + scripts/run-qwen3-next-80B-A3B-8gpus.sh | 192 +++++ scripts/run-qwen3-next-80B-A3B-fsdp.sh | 181 ++++ tools/convert_fsdp_to_hf.py | 178 ++++ tools/convert_hf_to_torch_dist.py | 8 +- train.py | 8 +- train_async.py | 6 +- 110 files changed, 6266 insertions(+), 2024 deletions(-) create mode 100644 docs/en/advanced/pd-disaggregation.md create mode 100644 docs/en/get_started/customization.md create mode 100644 examples/DrGRPO/README.md create mode 100644 examples/DrGRPO/custom_reducer.py create mode 100644 examples/geo3k_vlm/fsdp_vs_megatron.png delete mode 100644 examples/geo3k_vlm/rewards.png delete mode 100644 examples/geo3k_vlm/run_geo3k_vlm.py create mode 100644 examples/geo3k_vlm/run_geo3k_vlm.sh create mode 100644 examples/geo3k_vlm/run_geo3k_vlm_sft.sh create mode 100644 examples/geo3k_vlm_multi_turn/README.md create mode 100644 examples/geo3k_vlm_multi_turn/__init__.py create mode 100644 examples/geo3k_vlm_multi_turn/base_env.py create mode 100644 examples/geo3k_vlm_multi_turn/env_geo3k.py create mode 100644 examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_config.yaml create mode 100644 examples/geo3k_vlm_multi_turn/rollout.py create mode 100644 examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py create mode 100644 examples/geo3k_vlm_multi_turn/vlm_multi_turn_geo3k_reward.png create mode 100644 examples/on_policy_distillation/README.md create mode 100644 examples/train_infer_mismatch_helper/run-qwen3-4b-fsdp-mis.sh create mode 100644 miles/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py create mode 100644 miles/backends/megatron_utils/ci_utils.py delete mode 100644 miles/backends/megatron_utils/config_mapping/__init__.py delete mode 100644 miles/backends/megatron_utils/config_mapping/predefined_config_mappers.py delete mode 100644 miles/backends/megatron_utils/config_mapping/registry.py create mode 100644 miles/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu create mode 100644 miles/backends/megatron_utils/kernels/int4_qat/setup.py delete mode 100644 miles/ray/rollout_data_source.py create mode 100644 scripts/run-qwen3-next-80B-A3B-8gpus.sh create mode 100644 scripts/run-qwen3-next-80B-A3B-fsdp.sh create mode 100644 tools/convert_fsdp_to_hf.py diff --git a/build_conda.sh b/build_conda.sh index 46fc12df6..b45c2202e 100644 --- a/build_conda.sh +++ b/build_conda.sh @@ -46,10 +46,6 @@ NVCC_APPEND_FLAGS="--threads 4" \ --no-build-isolation \ --config-settings "--build-option=--cpp_ext --cuda_ext --parallel 8" git+https://github.com/NVIDIA/apex.git@10417aceddd7d5d05d7cbf7b0fc2daad1105f8b4 -git clone https://github.com/NVIDIA/Megatron-LM.git --recursive && \ - cd Megatron-LM && git checkout ${MEGATRON_COMMIT} && \ - pip install -e . - pip install git+https://github.com/fzyzcjy/torch_memory_saver.git@dc6876905830430b5054325fa4211ff302169c6b --no-cache-dir --force-reinstall pip install git+https://github.com/fzyzcjy/Megatron-Bridge.git@dev_rl --no-build-isolation pip install nvidia-modelopt[torch]>=0.37.0 --no-build-isolation diff --git a/docker/Dockerfile b/docker/Dockerfile index cfffa422f..94c2dbde6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang # ======================================== Arguments ============================================= ARG PATCH_VERSION=latest -ARG MEGATRON_COMMIT=core_v0.14.0 +ARG MEGATRON_COMMIT=3714d81d418c9f1bca4594fc35f9e8289f652862 ARG ENABLE_CUDA_13=0 @@ -71,25 +71,14 @@ RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ python3 -m pip install https://github.com/sgl-project/whl/releases/download/v${SGL_KERNEL_VERSION}/sgl_kernel-${SGL_KERNEL_VERSION}+cu130-cp310-abi3-manylinux2014_$(uname -m).whl --force-reinstall --no-deps; \ fi -# AMEM -# we need to create a fake libcuda.so.1 to make the linker happy when building AMEM -ENV CUDA_DIR=/usr/local/cuda -ENV CUDA_STUBS=${CUDA_DIR}/lib64/stubs -RUN ln -s ${CUDA_STUBS}/libcuda.so ${CUDA_STUBS}/libcuda.so.1 && \ - echo "${CUDA_STUBS}" > /etc/ld.so.conf.d/z-cuda-stubs.conf && \ - ldconfig -RUN git clone https://github.com/inclusionAI/asystem-amem.git && \ - cd asystem-amem && git checkout 6483bb17c9a98b51c3a94b7048467d5b50fbad4b && \ - git submodule init && git submodule update && \ - MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi/ ./build.sh && \ - mv /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2 /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2.bak && \ - cp -r third_party/nccl/build/lib/* /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/ - # https://github.com/pytorch/pytorch/issues/168167 RUN pip install nvidia-cudnn-cu12==9.16.0.29 +# reinstall numpy 1.x for megatron +RUN pip install "numpy<2" + RUN rm /root/.tmux.conf -RUN rm -rf /root/.cache/pip /root/asystem-amem /root/flash-attention +RUN rm -rf /root/.cache/pip /root/flash-attention # ====================================== Patches ============================================ diff --git a/docker/Dockerfile.rocm_MI350-5 b/docker/Dockerfile.rocm_MI350-5 index 6dc1353f0..dd32f32c5 100644 --- a/docker/Dockerfile.rocm_MI350-5 +++ b/docker/Dockerfile.rocm_MI350-5 @@ -1,252 +1,252 @@ -#### Use the base image for ROCm 7 / gfx950 (MI355) - -# The Docker image built with this Dockerfile: -# Base image: ROCm 7 with vllm pre-built for gfx950 -# Target GPU: MI355 (gfx950) - - -FROM rocm/sgl-dev:rocm7-vllm-20250904 - -SHELL ["/bin/bash", "-ceuxo", "pipefail"] - -ARG MAX_JOBS=128 -ENV MAX_JOBS=${MAX_JOBS} - -# Set environment variables for gfx950 -ENV GPU_ARCH=gfx950 -ENV PYTORCH_ROCM_ARCH=gfx950 -ENV GPU_ARCH_LIST=gfx950 -ENV AMDGPU_TARGET=gfx950 - - -########################################### -##############1. Install AITER############# -########################################### -WORKDIR /app - -RUN pip uninstall -y aiter || true -RUN rm -rf aiter -RUN git clone https://github.com/ROCm/aiter.git \ - && cd aiter \ - && git checkout v0.1.7.post2 \ - && git submodule update --init --recursive \ - && GPU_ARCHS=gfx950 python setup.py develop -########################################### -########################################### -########################################### - - -########################################### -####2. Install TransformerEngine for gfx950 -########################################### -WORKDIR /app - -RUN rm -rf TransformerEngine -RUN git clone https://github.com/ROCm/TransformerEngine.git \ - && cd TransformerEngine \ - && git checkout 90c04bcdc3c109505b318f40a39680263af55edf \ - && git submodule update --init --recursive - -ENV NVTE_FRAMEWORK=pytorch -ENV NVTE_ROCM_ARCH=gfx950 -ENV NVTE_USE_HIPBLASLT=1 -ENV NVTE_USE_ROCM=1 -ENV CMAKE_PREFIX_PATH="/opt/rocm:/opt/rocm/hip:/usr/local:/usr" - -RUN cd TransformerEngine && pip install . -v -########################################### -########################################### -########################################### - - -######################################### -####3. Install Megatron-LM (NVIDIA version) -######################################### -WORKDIR /app - -RUN pip install "numpy>=1.21.0,<2.0" --force-reinstall - -RUN pip uninstall -y megatron-core || true -RUN rm -rf Megatron-LM -RUN git clone https://github.com/NVIDIA/Megatron-LM \ - && cd Megatron-LM \ - && git checkout 48406695c4efcf1026a7ed70bb390793918dd97b \ - && pip install -e . -######################################### -######################################### -######################################### - - -######################################## -############ 4. Install mbridge######### -######################################## -RUN pip install git+https://github.com/ISEEKYAN/mbridge.git --no-deps -######################################## -######################################## -######################################## - - -######################################## -######5. Install Ray#################### -######################################## -RUN pip uninstall ray -y || true -RUN pip install "ray[data,train,tune,serve]==2.47.1" -######################################## -######################################## -######################################## - - -######################################### -###6. Install torch_memory_saver######### -######################################### -RUN pip install torch_memory_saver -######################################### -######################################### - - -####################################### -####7. Install Apex for ROCm########### -####################################### -WORKDIR /app - -RUN pip uninstall -y apex || true -RUN rm -rf apex -RUN git clone https://github.com/ROCm/apex.git \ - && cd apex \ - && python setup.py install -####################################### -####################################### -####################################### - - -######################################## -###8. Install slime agent framework deps -######################################## -RUN pip install pydra_config==0.0.15 -RUN pip install together -RUN pip install google-generativeai -RUN pip install tensorboard -######################################## -######################################## -######################################## - - -######################################## -###9. Set performance environment vars## -######################################## -ENV HIP_FORCE_DEV_KERNARG=1 -ENV HSA_NO_SCRATCH_RECLAIM=1 -ENV SGLANG_USE_AITER=1 -ENV SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 -ENV SGLANG_MOE_PADDING=1 -ENV SGLANG_SET_CPU_AFFINITY=1 -ENV SGLANG_ROCM_FUSED_DECODE_MLA=1 -ENV SGLANG_USE_ROCM700A=1 -ENV NCCL_MIN_NCHANNELS=112 -ENV VLLM_FP8_PADDING=1 -ENV VLLM_FP8_ACT_PADDING=1 -ENV VLLM_FP8_WEIGHT_PADDING=1 -ENV VLLM_FP8_REDUCE_CONV=1 -ENV TORCHINDUCTOR_MAX_AUTOTUNE=1 -ENV TORCHINDUCTOR_MAX_AUTOTUNE_POINTWISE=1 -######################################## -######################################## -######################################## - - -########################################### -##############Install SGLang############### -########################################### -WORKDIR /app - -# Install prerequisites -RUN pip install IPython orjson python-multipart torchao==0.9.0 pybind11 - -# Clone SGLang -RUN pip uninstall -y sgl_kernel sglang || true -RUN rm -rf sglang -RUN git clone https://github.com/sgl-project/sglang.git \ - && cd sglang \ - && git checkout v0.5.6 - -# Build sgl-kernel for gfx950 -RUN cd sglang/sgl-kernel \ - && rm -f pyproject.toml \ - && mv pyproject_rocm.toml pyproject.toml \ - && AMDGPU_TARGET=gfx950 python setup_rocm.py install - -# Install SGLang -RUN cd sglang \ - && rm -rf python/pyproject.toml \ - && mv python/pyproject_other.toml python/pyproject.toml \ - && pip install -e "python[all_hip]" - -# Test SGLang installation -RUN python -c "import sglang; import sgl_kernel; print('SGLang + sgl_kernel: OK')" - -RUN python -m pip cache purge -########################################### -########################################### -########################################### - - -########################################### -#### APPLY PATCHES (gfx950/MI355) ######### -########################################### - -# Copy patches from slime repo -COPY amd_patch/latest /app/patch - -# Apply Megatron patches -RUN cd /app/Megatron-LM \ - && git apply /app/patch/amd_megatron_fused_kernels_init.patch \ - && git apply /app/patch/megatron.patch --3way \ - && if grep -R -n '^<<<<<<< ' .; then \ - echo "Patch failed to apply cleanly. Please resolve conflicts." && \ - exit 1; \ - fi \ - && pip install -e . -v - -# Apply SGLang patch -RUN cd /app/sglang \ - && git apply /app/patch/sglang.patch || echo "Check patch compatibility with v0.5.6" \ - && if grep -R -n '^<<<<<<< ' .; then \ - echo "Patch failed to apply cleanly. Please resolve conflicts." && \ - exit 1; \ - fi - -# Copy MOE configs for gfx950/MI355 -RUN find /app/sglang/python/sglang/srt/layers/quantization/configs/ \ - /app/sglang/python/sglang/srt/layers/moe/fused_moe_triton/configs/ \ - -type f -name '*MI300X*' 2>/dev/null | while read f; do \ - cp "$f" "$(echo $f | sed 's/MI300X/MI300X_VF/')" 2>/dev/null || true; \ - cp "$f" "$(echo $f | sed 's/MI300X/MI355/')" 2>/dev/null || true; \ -done - -########################################### -########################################### -########################################### - - -######################################## -#### Install additional packages######## -######################################## -RUN pip install sglang-router --force-reinstall -######################################## -######################################## -######################################## - - -######################################## -# Fix click/ray incompatibility with Python 3.10 -######################################## -RUN pip install click==8.2.1 -######################################## -######################################## -######################################## - - -WORKDIR /app - -CMD ["/usr/bin/bash"] - +#### Use the base image for ROCm 7 / gfx950 (MI355) + +# The Docker image built with this Dockerfile: +# Base image: ROCm 7 with vllm pre-built for gfx950 +# Target GPU: MI355 (gfx950) + + +FROM rocm/sgl-dev:rocm7-vllm-20250904 + +SHELL ["/bin/bash", "-ceuxo", "pipefail"] + +ARG MAX_JOBS=128 +ENV MAX_JOBS=${MAX_JOBS} + +# Set environment variables for gfx950 +ENV GPU_ARCH=gfx950 +ENV PYTORCH_ROCM_ARCH=gfx950 +ENV GPU_ARCH_LIST=gfx950 +ENV AMDGPU_TARGET=gfx950 + + +########################################### +##############1. Install AITER############# +########################################### +WORKDIR /app + +RUN pip uninstall -y aiter || true +RUN rm -rf aiter +RUN git clone https://github.com/ROCm/aiter.git \ + && cd aiter \ + && git checkout v0.1.7.post2 \ + && git submodule update --init --recursive \ + && GPU_ARCHS=gfx950 python setup.py develop +########################################### +########################################### +########################################### + + +########################################### +####2. Install TransformerEngine for gfx950 +########################################### +WORKDIR /app + +RUN rm -rf TransformerEngine +RUN git clone https://github.com/ROCm/TransformerEngine.git \ + && cd TransformerEngine \ + && git checkout 90c04bcdc3c109505b318f40a39680263af55edf \ + && git submodule update --init --recursive + +ENV NVTE_FRAMEWORK=pytorch +ENV NVTE_ROCM_ARCH=gfx950 +ENV NVTE_USE_HIPBLASLT=1 +ENV NVTE_USE_ROCM=1 +ENV CMAKE_PREFIX_PATH="/opt/rocm:/opt/rocm/hip:/usr/local:/usr" + +RUN cd TransformerEngine && pip install . -v +########################################### +########################################### +########################################### + + +######################################### +####3. Install Megatron-LM (NVIDIA version) +######################################### +WORKDIR /app + +RUN pip install "numpy>=1.21.0,<2.0" --force-reinstall + +RUN pip uninstall -y megatron-core || true +RUN rm -rf Megatron-LM +RUN git clone https://github.com/NVIDIA/Megatron-LM \ + && cd Megatron-LM \ + && git checkout 48406695c4efcf1026a7ed70bb390793918dd97b \ + && pip install -e . +######################################### +######################################### +######################################### + + +######################################## +############ 4. Install mbridge######### +######################################## +RUN pip install git+https://github.com/ISEEKYAN/mbridge.git --no-deps +######################################## +######################################## +######################################## + + +######################################## +######5. Install Ray#################### +######################################## +RUN pip uninstall ray -y || true +RUN pip install "ray[data,train,tune,serve]==2.47.1" +######################################## +######################################## +######################################## + + +######################################### +###6. Install torch_memory_saver######### +######################################### +RUN pip install torch_memory_saver +######################################### +######################################### + + +####################################### +####7. Install Apex for ROCm########### +####################################### +WORKDIR /app + +RUN pip uninstall -y apex || true +RUN rm -rf apex +RUN git clone https://github.com/ROCm/apex.git \ + && cd apex \ + && python setup.py install +####################################### +####################################### +####################################### + + +######################################## +###8. Install miles agent framework deps +######################################## +RUN pip install pydra_config==0.0.15 +RUN pip install together +RUN pip install google-generativeai +RUN pip install tensorboard +######################################## +######################################## +######################################## + + +######################################## +###9. Set performance environment vars## +######################################## +ENV HIP_FORCE_DEV_KERNARG=1 +ENV HSA_NO_SCRATCH_RECLAIM=1 +ENV SGLANG_USE_AITER=1 +ENV SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 +ENV SGLANG_MOE_PADDING=1 +ENV SGLANG_SET_CPU_AFFINITY=1 +ENV SGLANG_ROCM_FUSED_DECODE_MLA=1 +ENV SGLANG_USE_ROCM700A=1 +ENV NCCL_MIN_NCHANNELS=112 +ENV VLLM_FP8_PADDING=1 +ENV VLLM_FP8_ACT_PADDING=1 +ENV VLLM_FP8_WEIGHT_PADDING=1 +ENV VLLM_FP8_REDUCE_CONV=1 +ENV TORCHINDUCTOR_MAX_AUTOTUNE=1 +ENV TORCHINDUCTOR_MAX_AUTOTUNE_POINTWISE=1 +######################################## +######################################## +######################################## + + +########################################### +##############Install SGLang############### +########################################### +WORKDIR /app + +# Install prerequisites +RUN pip install IPython orjson python-multipart torchao==0.9.0 pybind11 + +# Clone SGLang +RUN pip uninstall -y sgl_kernel sglang || true +RUN rm -rf sglang +RUN git clone https://github.com/sgl-project/sglang.git \ + && cd sglang \ + && git checkout v0.5.6 + +# Build sgl-kernel for gfx950 +RUN cd sglang/sgl-kernel \ + && rm -f pyproject.toml \ + && mv pyproject_rocm.toml pyproject.toml \ + && AMDGPU_TARGET=gfx950 python setup_rocm.py install + +# Install SGLang +RUN cd sglang \ + && rm -rf python/pyproject.toml \ + && mv python/pyproject_other.toml python/pyproject.toml \ + && pip install -e "python[all_hip]" + +# Test SGLang installation +RUN python -c "import sglang; import sgl_kernel; print('SGLang + sgl_kernel: OK')" + +RUN python -m pip cache purge +########################################### +########################################### +########################################### + + +########################################### +#### APPLY PATCHES (gfx950/MI355) ######### +########################################### + +# Copy patches from miles repo +COPY amd_patch/latest /app/patch + +# Apply Megatron patches +RUN cd /app/Megatron-LM \ + && git apply /app/patch/amd_megatron_fused_kernels_init.patch \ + && git apply /app/patch/megatron.patch --3way \ + && if grep -R -n '^<<<<<<< ' .; then \ + echo "Patch failed to apply cleanly. Please resolve conflicts." && \ + exit 1; \ + fi \ + && pip install -e . -v + +# Apply SGLang patch +RUN cd /app/sglang \ + && git apply /app/patch/sglang.patch || echo "Check patch compatibility with v0.5.6" \ + && if grep -R -n '^<<<<<<< ' .; then \ + echo "Patch failed to apply cleanly. Please resolve conflicts." && \ + exit 1; \ + fi + +# Copy MOE configs for gfx950/MI355 +RUN find /app/sglang/python/sglang/srt/layers/quantization/configs/ \ + /app/sglang/python/sglang/srt/layers/moe/fused_moe_triton/configs/ \ + -type f -name '*MI300X*' 2>/dev/null | while read f; do \ + cp "$f" "$(echo $f | sed 's/MI300X/MI300X_VF/')" 2>/dev/null || true; \ + cp "$f" "$(echo $f | sed 's/MI300X/MI355/')" 2>/dev/null || true; \ +done + +########################################### +########################################### +########################################### + + +######################################## +#### Install additional packages######## +######################################## +RUN pip install sglang-router --force-reinstall +######################################## +######################################## +######################################## + + +######################################## +# Fix click/ray incompatibility with Python 3.10 +######################################## +RUN pip install click==8.2.1 +######################################## +######################################## +######################################## + + +WORKDIR /app + +CMD ["/usr/bin/bash"] + diff --git a/docker/Dockerfile_20250810_9a48ba0.rocm b/docker/Dockerfile_20250810_9a48ba0.rocm index 073fb3891..db3d25a21 100644 --- a/docker/Dockerfile_20250810_9a48ba0.rocm +++ b/docker/Dockerfile_20250810_9a48ba0.rocm @@ -82,7 +82,7 @@ RUN pip install setuptools==75.8.0 ########################################### -############build sgalng################### +############build sglang################### ########################################### # Set environment variables ENV BASE_DIR=/workspace diff --git a/docker/Dockerfile_20250810_c22f55b.rocm b/docker/Dockerfile_20250810_c22f55b.rocm index 468a17c37..de6d93422 100644 --- a/docker/Dockerfile_20250810_c22f55b.rocm +++ b/docker/Dockerfile_20250810_c22f55b.rocm @@ -92,7 +92,7 @@ RUN pip install setuptools==75.8.0 ########################################### -############build sgalng################### +############build sglang################### ########################################### # Set environment variables ENV BASE_DIR=/workspace diff --git a/docker/justfile b/docker/justfile index a064fb426..aac41e6b2 100644 --- a/docker/justfile +++ b/docker/justfile @@ -24,3 +24,14 @@ _release-raw: docker tag radixark/miles:$IMAGE_TAG radixark/miles:latest docker push radixark/miles:latest fi + +debug: + #!/bin/bash + set -euxo pipefail + cd .. + + VERSION="$(cat docker/version.txt | tr -d '\n')" + IMAGE_TAG=${VERSION} + + docker build -f docker/Dockerfile . --build-arg HTTP_PROXY="$http_proxy" --build-arg HTTPS_PROXY="$https_proxy" --build-arg NO_PROXY="localhost,127.0.0.1" -t radixark/miles-test:$IMAGE_TAG + docker push radixark/miles-test:$IMAGE_TAG diff --git a/docker/patch/latest/megatron.patch b/docker/patch/latest/megatron.patch index 3a56ff4c2..a337b19fb 100644 --- a/docker/patch/latest/megatron.patch +++ b/docker/patch/latest/megatron.patch @@ -12,38 +12,10 @@ index 41c21d93d..ef80f72d6 100644 err_msg = f'Common file {load_path} does not exist' if MultiStorageClientFeature.is_enabled(): diff --git a/megatron/core/dist_checkpointing/strategies/torch.py b/megatron/core/dist_checkpointing/strategies/torch.py -index ccf5242a2..9b6d3e31f 100644 +index 5a1ea308d..aa701237f 100644 --- a/megatron/core/dist_checkpointing/strategies/torch.py +++ b/megatron/core/dist_checkpointing/strategies/torch.py -@@ -427,6 +427,15 @@ def _restore_dict_types(x: Union[dict, list, Any], keys_template: Union[dict, li - _restore_dict_types(x_val, templ_val) - - -+@dataclass -+class MCoreMetadata(Metadata): -+ """Metadata with mcore specific data.""" -+ -+ # holds data related to flattened_range -+ # TODO: remove when flattened_range is properly removed -+ mcore_data: Optional[Dict[str, Dict[str, Any]]] = None # Mcore related data about each tensor -+ -+ - @dataclass(frozen=True) - class MCoreSavePlan(SavePlan): - """SavePlan with MCore specific data.""" -@@ -499,9 +508,10 @@ class MCoreSavePlanner(DefaultSavePlanner): - def create_global_plan(self, all_plans: List[MCoreSavePlan]) -> Tuple[List[SavePlan], Metadata]: - """Merges MCore data for all plans.""" - global_plan, metadata = super().create_global_plan(all_plans) -- metadata.mcore_data = dict( -+ mcore_data = dict( - ChainMap(*(plan.mcore_data for plan in all_plans)) # type: ignore[arg-type] - ) -+ metadata = MCoreMetadata(mcore_data=mcore_data, **vars(metadata)) - return global_plan, metadata - - def create_decentralized_global_plan(self, local_plan: SavePlan) -> SavePlan: -@@ -556,10 +566,12 @@ class MCoreLoadPlanner(DefaultLoadPlanner): +@@ -597,10 +597,12 @@ class MCoreLoadPlanner(DefaultLoadPlanner): def _validate_global_shapes(self, metadata, sharded_tensors): for sh_ten in sharded_tensors: if sh_ten.key not in metadata.state_dict_metadata: @@ -60,7 +32,7 @@ index ccf5242a2..9b6d3e31f 100644 loaded_shape = metadata.state_dict_metadata[sh_ten.key].size expected_shape = self._expected_shape(sh_ten) if loaded_shape != expected_shape: -@@ -589,7 +601,7 @@ class MCoreLoadPlanner(DefaultLoadPlanner): +@@ -630,7 +632,7 @@ class MCoreLoadPlanner(DefaultLoadPlanner): tensor_metadata = self.metadata.state_dict_metadata metadata_with_sizes = [ (tensor_metadata[key], tensor_metadata[key].size, sharded_tensor) @@ -69,7 +41,7 @@ index ccf5242a2..9b6d3e31f 100644 ] try: # Temporarily set sizes to expected shapes -@@ -918,6 +930,7 @@ class TorchDistLoadShardedStrategy(LoadShardedStrategy): +@@ -959,6 +961,7 @@ class TorchDistLoadShardedStrategy(LoadShardedStrategy): planner=MCoreLoadPlanner( shapes_validation_sharded_tensors=flexible_shape_sharded_tensors, allow_shape_mismatch_sharded_tensors=allow_shape_mismatch_sharded_tensors, @@ -77,31 +49,11 @@ index ccf5242a2..9b6d3e31f 100644 ), ) -diff --git a/megatron/core/distributed/__init__.py b/megatron/core/distributed/__init__.py -index fe26e8b43..4451f2776 100644 ---- a/megatron/core/distributed/__init__.py -+++ b/megatron/core/distributed/__init__.py -@@ -11,3 +11,15 @@ from .finalize_model_grads import finalize_model_grads - from .fsdp.mcore_fsdp_adapter import FullyShardedDataParallel - from .torch_fully_sharded_data_parallel import TorchFullyShardedDataParallel - from .torch_fully_sharded_data_parallel_config import TorchFullyShardedDataParallelConfig -+ -+# Backward compatibility patch for FSDP module reorganization -+import sys -+import importlib.util -+ -+spec = importlib.util.find_spec('megatron.core.distributed.fsdp.src.megatron_fsdp') -+if spec: -+ custom_fsdp = importlib.util.module_from_spec(spec) -+ spec.loader.exec_module(custom_fsdp) -+ sys.modules['megatron.core.distributed.custom_fsdp'] = custom_fsdp -+ if hasattr(custom_fsdp, 'MegatronFSDP'): -+ custom_fsdp.FullyShardedDataParallel = custom_fsdp.MegatronFSDP diff --git a/megatron/core/extensions/transformer_engine.py b/megatron/core/extensions/transformer_engine.py -index 7727efe1e..966fe652a 100644 +index acb93ef78..20ee977b0 100644 --- a/megatron/core/extensions/transformer_engine.py +++ b/megatron/core/extensions/transformer_engine.py -@@ -366,6 +366,7 @@ class TELinear(te.pytorch.Linear): +@@ -408,6 +408,7 @@ class TELinear(te.pytorch.Linear): ) for param in self.parameters(): @@ -109,44 +61,245 @@ index 7727efe1e..966fe652a 100644 if is_expert: # Reduce the gradient on the expert_data_parallel group for expert linear layers setattr(param, "allreduce", not self.expert_parallel) +diff --git a/megatron/core/fusions/fused_mla_yarn_rope_apply.py b/megatron/core/fusions/fused_mla_yarn_rope_apply.py +index 1fd5dcfae..c9aeef1f0 100644 +--- a/megatron/core/fusions/fused_mla_yarn_rope_apply.py ++++ b/megatron/core/fusions/fused_mla_yarn_rope_apply.py +@@ -385,6 +385,7 @@ def rotary_fwd_kv_kernel( + SIN, + emb_dim: tl.constexpr, + k_dim: tl.constexpr, ++ k_dim_ceil: tl.constexpr, + v_dim: tl.constexpr, + head_num: tl.constexpr, + batch_size, +@@ -434,21 +435,27 @@ def rotary_fwd_kv_kernel( + cos_right = tl.load(COS + token_idx * emb_dim + emb_dim // 2 + tl.arange(0, emb_dim // 2)) + sin_right = tl.load(SIN + token_idx * emb_dim + emb_dim // 2 + tl.arange(0, emb_dim // 2)) + +- KV_ptr = KV + pid_m * stride_kv_seq + pid_head * BLOCK_H * stride_kv_nheads +- kv_off = tl.arange(0, BLOCK_H)[:, None] * stride_kv_nheads +- mask = kv_off < head_num * stride_kv_nheads +- k_in_off = kv_off + tl.arange(0, k_dim)[None, :] +- v_in_off = kv_off + k_dim + tl.arange(0, v_dim)[None, :] +- k = tl.load(KV_ptr + k_in_off, mask=mask) +- v = tl.load(KV_ptr + v_in_off, mask=mask) ++ KV_ptr = KV + pid_m * stride_kv_seq # + pid_head * BLOCK_H * stride_kv_nheads ++ ki_range = tl.arange(0, BLOCK_H)[:, None] + pid_head * BLOCK_H ++ kj_range = tl.arange(0, k_dim_ceil)[None, :] ++ mask_k = (ki_range < head_num) & (kj_range < k_dim) ++ mask_v = ki_range < head_num ++ k_off = ki_range * stride_kv_nheads + kj_range ++ if v_dim > 0: ++ v_off = ki_range * stride_kv_nheads + k_dim + tl.arange(0, v_dim)[None, :] ++ v = tl.load(KV_ptr + v_off, mask=mask_v) ++ else: ++ v = tl.zeros((BLOCK_H, 1), dtype=KV.dtype.element_ty) ++ k = tl.load(KV_ptr + k_off, mask=mask_k) + +- K_ptr = O_KEY + pid_m * stride_k_seq + pid_head * BLOCK_H * stride_k_nheads +- V_ptr = O_VALUE + pid_m * stride_v_seq + pid_head * BLOCK_H * stride_v_nheads ++ K_ptr = O_KEY + pid_m * stride_k_seq # + pid_head * BLOCK_H * stride_k_nheads ++ V_ptr = O_VALUE + pid_m * stride_v_seq # + pid_head * BLOCK_H * stride_v_nheads + +- k_out_off = tl.arange(0, BLOCK_H)[:, None] * stride_k_nheads + tl.arange(0, k_dim)[None, :] +- v_out_off = tl.arange(0, BLOCK_H)[:, None] * stride_v_nheads + tl.arange(0, v_dim)[None, :] +- tl.store(K_ptr + k_out_off, k, mask=mask) +- tl.store(V_ptr + v_out_off, v, mask=mask) ++ k_out_off = ki_range * stride_k_nheads + kj_range ++ tl.store(K_ptr + k_out_off, k, mask=mask_k) ++ if v_dim > 0: ++ v_out_off = ki_range * stride_v_nheads + tl.arange(0, v_dim)[None, :] ++ tl.store(V_ptr + v_out_off, v, mask=mask_v) + + EMB = K_POS_EMB + pid_m * stride_emb_seq + # x1 = t[..., 0::2], x2 = t[..., 1::2] +@@ -460,14 +467,16 @@ def rotary_fwd_kv_kernel( + x_left = x_left.expand_dims(0).broadcast_to(BLOCK_H, emb_dim // 2) + x_right = x_right.expand_dims(0).broadcast_to(BLOCK_H, emb_dim // 2) + ++ x_range = tl.arange(0, BLOCK_H)[:, None] + pid_head * BLOCK_H ++ mask_x = x_range < head_num + x_left_off = ( +- tl.arange(0, BLOCK_H)[:, None] * stride_k_nheads ++ x_range * stride_k_nheads + + k_dim + + tl.arange(0, emb_dim // 2)[None, :] + ) + x_right_off = x_left_off + emb_dim // 2 +- tl.store(K_ptr + x_left_off, x_left, mask=mask) +- tl.store(K_ptr + x_right_off, x_right, mask=mask) ++ tl.store(K_ptr + x_left_off, x_left, mask=mask_x) ++ tl.store(K_ptr + x_right_off, x_right, mask=mask_x) + + + @triton.autotune( +@@ -493,6 +502,7 @@ def rotary_bwd_kv_kernel( + SIN, + emb_dim: tl.constexpr, + k_dim: tl.constexpr, ++ k_dim_ceil: tl.constexpr, + v_dim: tl.constexpr, + head_num: tl.constexpr, + batch_size, +@@ -533,27 +543,32 @@ def rotary_bwd_kv_kernel( + else: + token_idx = _get_thd_token_idx(cu_seqlens_kv, pid_m, seq_num, cp_rank, cp_size) + +- dKV_ptr = dKV + pid_m * stride_dkv_seq + pid_head * BLOCK_H * stride_dkv_nheads +- dkv_off = tl.arange(0, BLOCK_H)[:, None] * stride_dkv_nheads +- mask = dkv_off < head_num * stride_dkv_nheads +- dk_out_off = dkv_off + tl.arange(0, k_dim)[None, :] +- dv_out_off = dkv_off + k_dim + tl.arange(0, v_dim)[None, :] +- +- dK_ptr = dK + pid_m * stride_dk_seq + pid_head * BLOCK_H * stride_dk_nheads +- dV_ptr = dV + pid_m * stride_dv_seq + pid_head * BLOCK_H * stride_dv_nheads +- dk_in_off = tl.arange(0, BLOCK_H)[:, None] * stride_dk_nheads + tl.arange(0, k_dim)[None, :] +- dv_in_off = tl.arange(0, BLOCK_H)[:, None] * stride_dv_nheads + tl.arange(0, v_dim)[None, :] +- dk = tl.load(dK_ptr + dk_in_off, mask=mask) +- dv = tl.load(dV_ptr + dv_in_off, mask=mask) +- tl.store(dKV_ptr + dk_out_off, dk, mask=mask) +- tl.store(dKV_ptr + dv_out_off, dv, mask=mask) ++ dKV_ptr = dKV + pid_m * stride_dkv_seq # + pid_head * BLOCK_H * stride_dkv_nheads ++ ki_range = tl.arange(0, BLOCK_H)[:, None] + pid_head * BLOCK_H ++ kj_range = tl.arange(0, k_dim_ceil)[None, :] ++ mask_k = (ki_range < head_num) & (kj_range < k_dim) ++ mask_v = ki_range < head_num ++ dk_out_off = ki_range * stride_dkv_nheads + kj_range ++ ++ dK_ptr = dK + pid_m * stride_dk_seq # + pid_head * BLOCK_H * stride_dk_nheads ++ dV_ptr = dV + pid_m * stride_dv_seq # + pid_head * BLOCK_H * stride_dv_nheads ++ dk_in_off = ki_range * stride_dk_nheads + kj_range ++ ++ dk = tl.load(dK_ptr + dk_in_off, mask=mask_k) ++ tl.store(dKV_ptr + dk_out_off, dk, mask=mask_k) ++ ++ if v_dim > 0: ++ dv_out_off = ki_range * stride_dkv_nheads + k_dim + tl.arange(0, v_dim)[None, :] ++ dv_in_off = ki_range * stride_dv_nheads + tl.arange(0, v_dim)[None, :] ++ dv = tl.load(dV_ptr + dv_in_off, mask=mask_v) ++ tl.store(dKV_ptr + dv_out_off, dv, mask=mask_v) + + if pid_head == 0: + x_left_accum = tl.zeros((BLOCK_H, emb_dim // 2), dtype=tl.float32) + x_right_accum = tl.zeros((BLOCK_H, emb_dim // 2), dtype=tl.float32) + for i in tl.static_range(triton.cdiv(head_num, BLOCK_H)): +- dK_ptr = dK + pid_m * stride_dk_seq + i * BLOCK_H * stride_dk_nheads +- x_off = tl.arange(0, BLOCK_H)[:, None] * stride_dk_nheads + k_dim ++ dK_ptr = dK + pid_m * stride_dk_seq # + i * BLOCK_H * stride_dk_nheads ++ x_off = tl.arange(0, BLOCK_H)[:, None] * stride_dk_nheads + k_dim + i * BLOCK_H * stride_dk_nheads + mask = x_off < head_num * stride_dk_nheads + x_left_off = x_off + tl.arange(0, emb_dim // 2)[None, :] + x_right_off = x_left_off + emb_dim // 2 +@@ -632,6 +647,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + + o_key = kv.new_empty(total_seqlen, nheads, emb_dim + k_dim) + o_value = kv.new_empty(total_seqlen, nheads, v_dim) ++ k_dim_ceil = triton.next_power_of_2(k_dim) + + grid = lambda META: (total_seqlen, triton.cdiv(nheads, META["BLOCK_H"])) + rotary_fwd_kv_kernel[grid]( +@@ -643,6 +659,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + sin, + emb_dim, + k_dim, ++ k_dim_ceil, + v_dim, + nheads, + batch_size, +@@ -700,6 +717,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + + d_kv = dk.new_empty(total_seqlen, nheads, ctx.k_dim + ctx.v_dim) + d_emb = dk.new_empty(total_seqlen, 1, ctx.emb_dim) ++ k_dim_ceil = triton.next_power_of_2(ctx.k_dim) + + grid = lambda META: (total_seqlen, triton.cdiv(nheads, META["BLOCK_H"])) + rotary_bwd_kv_kernel[grid]( +@@ -711,6 +729,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + sin, + ctx.emb_dim, + ctx.k_dim, ++ k_dim_ceil, + ctx.v_dim, + nheads, + batch_size, +diff --git a/megatron/core/models/common/language_module/language_module.py b/megatron/core/models/common/language_module/language_module.py +index 13d74aa52..060898a7a 100644 +--- a/megatron/core/models/common/language_module/language_module.py ++++ b/megatron/core/models/common/language_module/language_module.py +@@ -184,7 +184,15 @@ class LanguageModule(MegatronModule): + assert ( + column_parallel_linear is not None + ), "column_parallel_linear cannot be None when not using fused linear cross entropy." +- logits, _ = column_parallel_linear(hidden, **col_linear_kwargs) ++ # output ++ output_layer_params = {k: v.detach() for k, v in column_parallel_linear.named_parameters()} ++ output_layer_buffers = dict(column_parallel_linear.named_buffers()) ++ logits, _ = torch.func.functional_call( ++ column_parallel_linear, ++ {**output_layer_params, **output_layer_buffers}, ++ (hidden,), ++ col_linear_kwargs, ++ ) + + return self.compute_language_model_loss(labels, logits) + diff --git a/megatron/core/models/gpt/gpt_layer_specs.py b/megatron/core/models/gpt/gpt_layer_specs.py -index 860ee64a9..80944b702 100755 +index e21127b87..712793853 100755 --- a/megatron/core/models/gpt/gpt_layer_specs.py +++ b/megatron/core/models/gpt/gpt_layer_specs.py -@@ -79,6 +79,8 @@ def get_gpt_layer_with_transformer_engine_spec( - qk_l2_norm: Optional[bool] = False, - use_te_op_fuser: Optional[bool] = False, +@@ -188,6 +188,8 @@ def get_gpt_layer_with_transformer_engine_spec( use_kitchen: bool = False, + use_te_activation_func: bool = False, + fallback_to_eager_attn: bool = False, + post_self_attn_layernorm: bool = False, + post_mlp_layernorm: bool = False, ) -> ModuleSpec: """Use this spec to use lower-level Transformer Engine modules (required for fp8 training). -@@ -178,9 +180,11 @@ def get_gpt_layer_with_transformer_engine_spec( - ), - ), - self_attn_bda=get_bias_dropout_add, -+ post_self_attn_layernorm=TENorm if post_self_attn_layernorm else IdentityOp, - pre_mlp_layernorm=backend.layer_norm() if num_experts else IdentityOp, - mlp=mlp, - mlp_bda=get_bias_dropout_add, -+ post_mlp_layernorm=TENorm if post_mlp_layernorm else IdentityOp, - sharded_state_dict_keys_map={ - "mlp.0.weight": "mlp.linear_fc1.layer_norm_weight", - "mlp.0.bias": "mlp.linear_fc1.layer_norm_bias", +@@ -260,6 +262,8 @@ def get_gpt_layer_with_transformer_engine_spec( + mlp=mlp, + sharded_state_dict_keys_map=sharded_state_dict_keys_map, + normalization=normalization, ++ post_self_attn_layernorm=post_self_attn_layernorm, ++ post_mlp_layernorm=post_mlp_layernorm, + ) + + +@@ -349,6 +353,8 @@ def get_transformer_layer_spec_for_backend( + mlp: ModuleSpec, + sharded_state_dict_keys_map: Optional[dict] = None, + normalization: Optional[str] = None, ++ post_self_attn_layernorm: bool = False, ++ post_mlp_layernorm: bool = False, + ) -> ModuleSpec: + """Helper function to get module spec for TransformerLayer""" + +@@ -371,9 +377,11 @@ def get_transformer_layer_spec_for_backend( + input_layernorm=input_layernorm, + self_attention=attention, + self_attn_bda=get_bias_dropout_add, ++ post_self_attn_layernorm=TENorm if post_self_attn_layernorm else IdentityOp, + pre_mlp_layernorm=pre_mlp_layernorm, + mlp=mlp, + mlp_bda=get_bias_dropout_add, ++ post_mlp_layernorm=TENorm if post_mlp_layernorm else IdentityOp, + sharded_state_dict_keys_map=sharded_state_dict_keys_map, + ), + ) diff --git a/megatron/core/models/gpt/gpt_model.py b/megatron/core/models/gpt/gpt_model.py -index 6aec66e6d..6ca48b55f 100644 +index a1230568c..1fd52f65a 100644 --- a/megatron/core/models/gpt/gpt_model.py +++ b/megatron/core/models/gpt/gpt_model.py -@@ -355,6 +355,7 @@ class GPTModel(LanguageModule): +@@ -446,6 +446,7 @@ class GPTModel(LanguageModule): *, inference_params: Optional[BaseInferenceContext] = None, loss_mask: Optional[Tensor] = None, + mtp_kwargs: Optional[dict] = {}, ) -> Tensor: """Forward function of the GPT Model This function passes the input tensors - through the embedding layer, and then the decoeder and finally into the post -@@ -410,6 +411,7 @@ class GPTModel(LanguageModule): + through the embedding layer, and then the decoder and finally into the post +@@ -508,6 +509,7 @@ class GPTModel(LanguageModule): runtime_gather_output=runtime_gather_output, extra_block_kwargs=extra_block_kwargs, inference_context=inference_context, @@ -154,7 +307,7 @@ index 6aec66e6d..6ca48b55f 100644 ) def _postprocess( -@@ -431,6 +433,7 @@ class GPTModel(LanguageModule): +@@ -529,6 +531,7 @@ class GPTModel(LanguageModule): runtime_gather_output=None, extra_block_kwargs=None, inference_context=None, @@ -162,22 +315,23 @@ index 6aec66e6d..6ca48b55f 100644 ): """Postprocesses decoder hidden states to generate logits or compute loss. -@@ -446,7 +449,7 @@ class GPTModel(LanguageModule): +@@ -543,7 +546,8 @@ class GPTModel(LanguageModule): + output_weight = None if self.share_embeddings_and_output_weights: output_weight = self.shared_embedding_or_output_weight() - - if mtp_in_postprocess: ++ + if mtp_in_postprocess and mtp_kwargs.get('mtp_labels', None) is not None: hidden_states = self.mtp( input_ids=input_ids, position_ids=position_ids, -@@ -465,25 +468,37 @@ class GPTModel(LanguageModule): - if not self.post_process: +@@ -563,13 +567,18 @@ class GPTModel(LanguageModule): return hidden_states -- if self.mtp_process: + # Skip when mtp_num_layers is None or 0 +- if self.config.mtp_num_layers: - mtp_labels = labels.clone() -+ if self.mtp_process and mtp_kwargs.get('mtp_labels', None) is not None: ++ if self.config.mtp_num_layers and mtp_kwargs.get('mtp_labels', None) is not None: + mtp_labels = mtp_kwargs['mtp_labels'].clone() + mtp_labels, _ = roll_tensor(mtp_labels, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params) + @@ -190,39 +344,22 @@ index 6aec66e6d..6ca48b55f 100644 + # Otherwise, roll the loss_mask to keep up with the mtp_labels + loss_mask, _ = roll_tensor(loss_mask, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params) for mtp_layer_number in range(self.config.mtp_num_layers): - # output -- mtp_logits, _ = self.output_layer( -- hidden_states_list[mtp_layer_number + 1], -- weight=output_weight, -- runtime_gather_output=runtime_gather_output, -+ output_layer_params = {k: v.detach() for k, v in self.output_layer.named_parameters()} -+ output_layer_buffers = dict(self.output_layer.named_buffers()) -+ mtp_logits, _ = torch.func.functional_call( -+ self.output_layer, -+ {**output_layer_params, **output_layer_buffers}, -+ (hidden_states_list[mtp_layer_number + 1],), -+ { -+ "weight": output_weight.detach() if output_weight else None, -+ "runtime_gather_output": runtime_gather_output, -+ }, - ) # Calc loss for the current Multi-Token Prediction (MTP) layers. -- mtp_labels, _ = roll_tensor(mtp_labels, shifts=-1, dims=-1, cp_group=self.cp_group) -- loss_mask, num_tokens = roll_tensor( -- loss_mask, shifts=-1, dims=-1, cp_group=self.cp_group -+ mtp_labels, _ = roll_tensor(mtp_labels, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params) -+ new_loss_mask, num_tokens = roll_tensor( -+ loss_mask, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params + mtp_labels, _ = roll_tensor( +@@ -595,7 +604,7 @@ class GPTModel(LanguageModule): + sequence_parallel_enabled=self.output_layer.sequence_parallel, + column_parallel_linear=self.output_layer, + col_linear_kwargs={ +- 'weight': output_weight, ++ 'weight': output_weight.detach() if output_weight else None, + 'runtime_gather_output': runtime_gather_output, + }, ) -+ loss_mask = new_loss_mask * loss_mask - mtp_loss = self.compute_language_model_loss(mtp_labels, mtp_logits) - mtp_loss = loss_mask * mtp_loss - if self.training: diff --git a/megatron/core/optimizer/distrib_optimizer.py b/megatron/core/optimizer/distrib_optimizer.py -index a36b67364..ed8883e32 100644 +index 6e093f96f..eac21a3ea 100644 --- a/megatron/core/optimizer/distrib_optimizer.py +++ b/megatron/core/optimizer/distrib_optimizer.py -@@ -657,6 +657,8 @@ class DistributedOptimizer(MixedPrecisionOptimizer): +@@ -677,6 +677,8 @@ class DistributedOptimizer(MixedPrecisionOptimizer): # TE FusedAdam will not accumulate step for empty param groups, so we need to # align the step across param groups. param_group["step"] = int(step) @@ -231,11 +368,20 @@ index a36b67364..ed8883e32 100644 # Grad scaler state. if self.grad_scaler: +@@ -1646,6 +1648,8 @@ class DistributedOptimizer(MixedPrecisionOptimizer): + if key == 'padding': + tensors[key] = LocalNonpersistentObject(tensors[key]) + continue ++ if key == 'step': ++ continue + assert tensors[key].shape == (gbuf_local_end - gbuf_local_start,), ( + tensors[key].shape, + gbuf_local_start, diff --git a/megatron/core/parallel_state.py b/megatron/core/parallel_state.py -index a40c85a88..86688c331 100644 +index a273002b9..4f821cfd5 100644 --- a/megatron/core/parallel_state.py +++ b/megatron/core/parallel_state.py -@@ -9,6 +9,7 @@ from typing import Callable, List, Optional +@@ -11,6 +11,7 @@ from typing import Callable, List, Optional import numpy as np import torch @@ -244,7 +390,7 @@ index a40c85a88..86688c331 100644 from .utils import GlobalMemoryBuffer, is_torch_min_version diff --git a/megatron/core/pipeline_parallel/p2p_communication.py b/megatron/core/pipeline_parallel/p2p_communication.py -index 63ee9d1f5..b90b744c1 100644 +index ac839c21f..f18309217 100644 --- a/megatron/core/pipeline_parallel/p2p_communication.py +++ b/megatron/core/pipeline_parallel/p2p_communication.py @@ -26,22 +26,22 @@ def _batched_p2p_ops( @@ -274,153 +420,11 @@ index 63ee9d1f5..b90b744c1 100644 ) ops.append(recv_next_op) if len(ops) > 0: -diff --git a/megatron/core/transformer/attention.py b/megatron/core/transformer/attention.py -index c749bac43..dde8d50e7 100644 ---- a/megatron/core/transformer/attention.py -+++ b/megatron/core/transformer/attention.py -@@ -670,7 +670,10 @@ class Attention(MegatronModule, ABC): - # Get the query, key and value tensors based on the type of attention - - # self or cross attn. - nvtx_range_push(suffix="qkv") -- query, key, value = self.get_query_key_value_tensors(hidden_states, key_value_states) -+ if self.config.use_gated_attention: -+ query, gate, key, value = self.get_query_gate_key_value_tensors(hidden_states, key_value_states) -+ else: -+ query, key, value = self.get_query_key_value_tensors(hidden_states, key_value_states) - nvtx_range_pop(suffix="qkv") - - # =================================================== -@@ -842,6 +845,11 @@ class Attention(MegatronModule, ABC): - # Output. [sq, b, h] - # ================= - -+ if self.config.use_gated_attention: -+ nvtx_range_push(suffix="sigmoid_gate") -+ core_attn_out = core_attn_out * torch.sigmoid(gate) -+ nvtx_range_pop(suffix="sigmoid_gate") -+ - nvtx_range_push(suffix="linear_proj") - output, bias = self.linear_proj(core_attn_out) - nvtx_range_pop(suffix="linear_proj") -@@ -879,19 +887,34 @@ class SelfAttention(Attention): - model_comm_pgs=model_comm_pgs, - ) - -- self.linear_qkv = build_module( -- submodules.linear_qkv, -- self.config.hidden_size, -- self.query_projection_size + 2 * self.kv_projection_size, -- config=self.config, -- init_method=self.config.init_method, -- gather_output=False, -- bias=self.config.add_bias_linear or self.config.add_qkv_bias, -- skip_bias_add=False, -- is_expert=False, -- tp_comm_buffer_name='qkv', -- tp_group=self.model_comm_pgs.tp, -- ) -+ if self.config.use_gated_attention: -+ self.linear_qgkv = build_module( -+ submodules.linear_qkv, -+ self.config.hidden_size, -+ 2 * (self.query_projection_size + self.kv_projection_size), -+ config=self.config, -+ init_method=self.config.init_method, -+ gather_output=False, -+ bias=self.config.add_bias_linear or self.config.add_qkv_bias, -+ skip_bias_add=False, -+ is_expert=False, -+ tp_comm_buffer_name='qkv', -+ tp_group=self.model_comm_pgs.tp, -+ ) -+ else: -+ self.linear_qkv = build_module( -+ submodules.linear_qkv, -+ self.config.hidden_size, -+ self.query_projection_size + 2 * self.kv_projection_size, -+ config=self.config, -+ init_method=self.config.init_method, -+ gather_output=False, -+ bias=self.config.add_bias_linear or self.config.add_qkv_bias, -+ skip_bias_add=False, -+ is_expert=False, -+ tp_comm_buffer_name='qkv', -+ tp_group=self.model_comm_pgs.tp, -+ ) - - if submodules.q_layernorm is not None: - self.q_layernorm = build_module( -@@ -1036,6 +1059,65 @@ class SelfAttention(Attention): - - return query, key, value - -+ # adapt from https://github.com/alibaba/Pai-Megatron-Patch/blob/8e6cbb0556ba09933ab4a4edb23c0af1d19d9960/megatron_patch/model/qwen3_next/gated_attention.py#L192 -+ def get_query_gate_key_value_tensors(self, hidden_states, key_value_states=None): -+ """ -+ Derives `query`, `key` and `value` tensors from `hidden_states`. -+ """ -+ # Attention heads [sq, b, h] --> [sq, b, ng * 2 * (np/ng + 1) * hn)] -+ mixed_qgkv, _ = self.linear_qgkv(hidden_states) -+ -+ # [sq, b, hp] --> [sq, b, ng, 2 * (np/ng + 1) * hn] -+ new_tensor_shape = mixed_qgkv.size()[:-1] + ( -+ self.num_query_groups_per_partition, -+ ( -+ 2 * (self.num_attention_heads_per_partition // self.num_query_groups_per_partition + 1) -+ * self.hidden_size_per_attention_head -+ ), -+ ) -+ mixed_qgkv = mixed_qgkv.view(*new_tensor_shape) -+ -+ split_arg_list = [ -+ ( -+ self.num_attention_heads_per_partition -+ // self.num_query_groups_per_partition -+ * self.hidden_size_per_attention_head -+ ), -+ ( -+ self.num_attention_heads_per_partition -+ // self.num_query_groups_per_partition -+ * self.hidden_size_per_attention_head -+ ), -+ self.hidden_size_per_attention_head, -+ self.hidden_size_per_attention_head, -+ ] -+ -+ if SplitAlongDim is not None: -+ -+ # [sq, b, ng, (np/ng + 2) * hn] -+ # --> [sq, b, ng, np/ng * hn], [sq, b, ng, hn], [sq, b, ng, hn] -+ (query, gate, key, value) = SplitAlongDim(mixed_qgkv, 3, split_arg_list) -+ else: -+ -+ # [sq, b, ng, (np/ng + 2) * hn] -+ # --> [sq, b, ng, np/ng * hn], [sq, b, ng, hn], [sq, b, ng, hn] -+ (query, gate, key, value) = torch.split(mixed_qgkv, split_arg_list, dim=3) -+ -+ # [sq, b, ng, np/ng * hn] -> [sq, b, np, hn] -+ query = query.reshape(query.size(0), query.size(1), -1, self.hidden_size_per_attention_head) -+ gate = gate.reshape(query.size(0), query.size(1), -1) -+ -+ if self.q_layernorm is not None: -+ query = self.q_layernorm(query) -+ -+ if self.k_layernorm is not None: -+ key = self.k_layernorm(key) -+ -+ if self.config.test_mode: -+ self.run_realtime_tests() -+ -+ return query, gate, key, value -+ - def backward_dw(self) -> NoReturn: - """Execute weight update operations""" - self._backward_qkv_proj() diff --git a/megatron/core/transformer/moe/moe_utils.py b/megatron/core/transformer/moe/moe_utils.py -index 235b6f6af..fbcffe278 100644 +index 28cff06f5..58dc4bb70 100644 --- a/megatron/core/transformer/moe/moe_utils.py +++ b/megatron/core/transformer/moe/moe_utils.py -@@ -566,6 +566,9 @@ def topk_routing_with_score_function( +@@ -587,6 +587,9 @@ def topk_routing_with_score_function( else: return torch.topk(scores, k=topk, dim=1) @@ -431,12 +435,12 @@ index 235b6f6af..fbcffe278 100644 if use_pre_softmax: scores = torch.softmax(logits, dim=-1, dtype=torch.float32).type_as(logits) diff --git a/megatron/core/transformer/moe/router.py b/megatron/core/transformer/moe/router.py -index 6b20b8622..459e65921 100644 +index 16fc9d9af..517944f25 100644 --- a/megatron/core/transformer/moe/router.py +++ b/megatron/core/transformer/moe/router.py -@@ -156,6 +156,9 @@ class TopKRouter(Router): - self.local_tokens_per_expert = None - self.expert_bias = None +@@ -201,6 +201,9 @@ class TopKRouter(Router): + self.global_tokens_per_expert = None + self.ga_steps = None + from miles.utils.routing_replay import register_routing_replay + register_routing_replay(self) @@ -445,7 +449,7 @@ index 6b20b8622..459e65921 100644 """ Maintain the expert bias in float32. diff --git a/megatron/core/transformer/multi_token_prediction.py b/megatron/core/transformer/multi_token_prediction.py -index b7884e18e..f0104f861 100755 +index a8f4abfcd..f33f6f05e 100755 --- a/megatron/core/transformer/multi_token_prediction.py +++ b/megatron/core/transformer/multi_token_prediction.py @@ -6,6 +6,7 @@ from typing import Callable, List, Optional, Union @@ -454,186 +458,27 @@ index b7884e18e..f0104f861 100755 from torch import Tensor +import warnings - from megatron.core import InferenceParams, mpu, parallel_state, tensor_parallel + from megatron.core import InferenceParams, parallel_state, tensor_parallel from megatron.core.dist_checkpointing.mapping import ShardedStateDict -@@ -105,17 +106,21 @@ def tie_output_layer_state_dict( - ) - - --def roll_tensor(tensor, shifts=-1, dims=-1, cp_group=None): -- """Roll the tensor input along the sequence dimension with Context Parallelism (CP) support. - -- This function extends the original roll_tensor to support Context Parallelism, which allows -- MTP to work with CP > 1. When CP is enabled, the sequence dimension is split across CP ranks, -- and tensor rolling requires communication between adjacent CP ranks to properly handle the -- boundary conditions. -+def roll_tensor(tensor, shifts=-1, dims=-1, cp_group=None, packed_seq_params=None): -+ """Roll the tensor input along the sequence dimension with Context Parallelism (CP) and Packed Sequence support. -+ -+ This function extends the original roll_tensor to support Context Parallelism and Packed Sequences. -+ When CP is enabled, the sequence dimension is split across CP ranks, and tensor rolling requires -+ communication between adjacent CP ranks to properly handle the boundary conditions. -+ When packed sequences are used, rolling is performed within each individual sequence boundary -+ to prevent mixing tokens between different packed sequences. - - For CP=1 (default behavior): Uses standard torch.roll with zero padding - For CP>1: Splits tensor into chunks, performs rolling within each chunk, then exchanges - boundary elements between adjacent CP ranks to maintain sequence continuity. -+ For packed sequences: Rolls tensors within sequence boundaries defined by cu_seqlens. -+ - - Args: - tensor (Tensor): The input tensor to roll. -@@ -123,9 +128,15 @@ def roll_tensor(tensor, shifts=-1, dims=-1, cp_group=None): - dims (int): The dimension to roll (typically -1 for sequence dimension). - cp_group (ProcessGroup): The context parallelism process group. If None or size=1, - falls back to standard rolling behavior. -+ packed_seq_params (PackedSeqParams): Parameters for packed sequence processing. -+ If provided, rolling respects sequence boundaries. - Returns: - tuple: (rolled_tensor, sum_of_rolled_tensor) - """ -+ -+ if packed_seq_params is not None: -+ return _roll_tensor_packed_seq(tensor, shifts, dims, packed_seq_params, cp_group) -+ - # Standard rolling behavior when CP is not enabled (cp_group is None or size=1) - if cp_group is None or cp_group.size() == 1: - rolled_tensor = torch.roll(tensor, shifts=shifts, dims=dims) -@@ -193,6 +204,103 @@ def roll_tensor(tensor, shifts=-1, dims=-1, cp_group=None): - - return rolled_tensor, rolled_tensor.sum() - -+def _roll_tensor_packed_seq(tensor, shifts, dims, packed_seq_params, cp_group=None): -+ """Roll tensor with packed sequence support. -+ -+ This function handles rolling for packed sequences by respecting sequence boundaries -+ defined in packed_seq_params.cu_seqlens. Rolling is performed within each individual -+ sequence to prevent mixing tokens between different packed sequences. When Context -+ Parallelism (CP) is enabled, each CP rank still receives the full `cu_seqlens` metadata -+ so we slice out the portion of every packed sequence that lives on the current rank and -+ reuse the standard CP boundary exchange to populate the rolling window. -+ -+ Args: -+ tensor (Tensor): The input tensor to roll. -+ shifts (int): The shift of the tensor (typically -1 for MTP). -+ dims (int): The dimension to roll (typically -1 for sequence dimension). -+ packed_seq_params (PackedSeqParams): Parameters for packed sequence processing. -+ cp_group (ProcessGroup): The context parallelism process group. -+ -+ Returns: -+ tuple: (rolled_tensor, sum_of_rolled_tensor) -+ """ -+ -+ # Notice: This is a naive implementation to test the correctness, a better solution will only sync the boundary tokens once. -+ assert dims == -1 or dims == tensor.dim() - 1, "Packed sequence roll only supports the last dimension." -+ assert shifts == -1, "Packed sequence roll only supports a single-token left shift." -+ cu_seqlens = packed_seq_params.cu_seqlens_q -+ assert cu_seqlens is not None, "Packed sequence parameters must provide cu_seqlens_q." -+ -+ rolled_tensor = tensor.clone() -+ -+ cp_size = cp_group.size() if cp_group is not None else 1 -+ if cp_size == 1: -+ # CP disabled: simply roll inside each packed sequence boundary. -+ for i in range(len(cu_seqlens) - 1): -+ start_idx = cu_seqlens[i] -+ end_idx = cu_seqlens[i + 1] -+ seq_slice = tensor[..., start_idx:end_idx] -+ rolled_seq = torch.roll(seq_slice, shifts=shifts, dims=dims) -+ rolled_seq[..., shifts:] = 0 -+ rolled_tensor[..., start_idx:end_idx] = rolled_seq -+ return rolled_tensor, rolled_tensor.sum() -+ -+ # CP enabled: each rank owns two chunks per sequence (front and mirrored tail). -+ local_rank = torch.distributed.get_rank(group=cp_group) -+ global_ranks = torch.distributed.get_process_group_ranks(group=cp_group) -+ next_rank = global_ranks[(local_rank + 1) % cp_size] -+ prev_rank = global_ranks[(local_rank - 1) % cp_size] -+ -+ # iterate over each sequence individually -+ for i in range(len(cu_seqlens) - 1): -+ start_idx = cu_seqlens[i] -+ end_idx = cu_seqlens[i + 1] -+ -+ # the idx has been multiplied by cp_size, so we need to divide it by cp_size to get the local idx -+ local_start_idx = start_idx // cp_size -+ local_end_idx = end_idx // cp_size -+ tensor_slice = rolled_tensor[..., local_start_idx:local_end_idx].clone() -+ -+ # The following code is very similar as the code in roll_tensor function -+ local_chunks = tensor_slice.chunk(2, dim=dims) -+ rolled_chunks = [ -+ torch.roll(chunk, shifts=shifts, dims=dims) for chunk in local_chunks -+ ] -+ -+ tensor_send_list = [] -+ tensor_recv_list = [] -+ for chunk in rolled_chunks: -+ boundary = chunk.select(dims, shifts).contiguous().clone() -+ tensor_send_list.append(boundary) -+ tensor_recv_list.append(torch.empty_like(boundary)) -+ -+ ops = [] -+ if local_rank != 0: -+ ops.append(torch.distributed.isend(tensor=tensor_send_list[0], dst=prev_rank)) -+ ops.append(torch.distributed.irecv(tensor=tensor_recv_list[1], src=prev_rank)) -+ else: -+ tensor_recv_list[1].zero_() -+ -+ if local_rank != cp_size - 1: -+ ops.append(torch.distributed.irecv(tensor=tensor_recv_list[0], src=next_rank)) -+ ops.append(torch.distributed.isend(tensor=tensor_send_list[1], dst=next_rank)) -+ else: -+ tensor_recv_list[0].copy_(tensor_send_list[1]) -+ -+ for op in ops: -+ op.wait() -+ -+ index = [slice(None)] * rolled_chunks[0].dim() -+ index[dims] = shifts -+ for chunk, recv in zip(rolled_chunks, tensor_recv_list): -+ chunk[tuple(index)] = recv -+ -+ seq_result = torch.cat(rolled_chunks, dim=dims) -+ -+ # update the rolled tensor -+ rolled_tensor[..., local_start_idx:local_end_idx] = seq_result -+ -+ return rolled_tensor, rolled_tensor.sum() - - class MTPLossLoggingHelper: - """Helper class for logging MTP losses.""" -@@ -480,9 +588,10 @@ class MultiTokenPredictionLayer(MegatronModule): - def _get_embeddings( - self, - input_ids: torch.Tensor, -- position_ids: torch.Tensor, - embedding: Callable, - hidden_states: torch.Tensor, -+ position_ids: Optional[torch.Tensor] = None, -+ packed_seq_params: Optional[PackedSeqParams] = None, - ): - """ - Preprocesses input data for the Multi-Token Prediction (MTP) layers. -@@ -499,12 +608,23 @@ class MultiTokenPredictionLayer(MegatronModule): - sequence length, b is the batch size, and h is the hidden size. - """ - # Calc logits for the current Multi-Token Prediction (MTP) layers. -- input_ids, _ = roll_tensor(input_ids, shifts=-1, dims=-1, cp_group=self.cp_group) -- position_ids, _ = roll_tensor(position_ids, shifts=-1, dims=-1, cp_group=self.cp_group) -+ input_ids, _ = roll_tensor(input_ids, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params) -+ -+ # Prepare/roll position ids only when applicable. -+ if position_ids is None: -+ # Fallback position ids for learned absolute embedding. -+ seq_len = input_ids.size(-1) -+ position_ids = torch.arange(seq_len, dtype=torch.long, device=input_ids.device) -+ position_ids = position_ids.unsqueeze(0).expand_as(input_ids) -+ -+ position_ids, _ = roll_tensor( -+ position_ids, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params -+ ) +@@ -714,17 +715,19 @@ class MultiTokenPredictionLayer(MegatronModule): + cp_group=self.cp_group, + packed_seq_params=packed_seq_params, + ) +- position_ids, _ = roll_tensor( +- position_ids, +- shifts=-1, +- dims=-1, +- cp_group=self.cp_group, +- packed_seq_params=packed_seq_params, +- ) ++ if position_ids is not None: ++ position_ids, _ = roll_tensor( ++ position_ids, ++ shifts=-1, ++ dims=-1, ++ cp_group=self.cp_group, ++ packed_seq_params=packed_seq_params, ++ ) # embedding decoder_input = embedding(input_ids=input_ids, position_ids=position_ids) + decoder_input = decoder_input.detach() @@ -643,7 +488,7 @@ index b7884e18e..f0104f861 100755 return input_ids, position_ids, decoder_input, hidden_states -@@ -604,22 +724,66 @@ class MultiTokenPredictionLayer(MegatronModule): +@@ -826,6 +829,51 @@ class MultiTokenPredictionLayer(MegatronModule): return hidden_states def _checkpointed_forward(self, forward_func, *args, **kwargs): @@ -693,14 +538,9 @@ index b7884e18e..f0104f861 100755 + tensor_args_tuple = tuple(tensor_args) + def checkpoint_handler(): -- """Determines whether to use the `te_checkpoint` or `tensor_parallel.checkpoint`""" -+ """Determines whether to use the `te_checkpoint` or `tensor_parallel.checkpoint`.""" + """Determines whether to use the `te_checkpoint` or `tensor_parallel.checkpoint`""" if self.config.fp8: - from megatron.core.extensions.transformer_engine import te_checkpoint - - return te_checkpoint( -- forward_func, -+ run, +@@ -836,12 +884,11 @@ class MultiTokenPredictionLayer(MegatronModule): self.config.distribute_saved_activations, tensor_parallel.random.get_cuda_rng_tracker, parallel_state.get_tensor_model_parallel_group(), @@ -715,43 +555,25 @@ index b7884e18e..f0104f861 100755 ) if self.config.recompute_method == 'uniform': -@@ -681,15 +845,13 @@ class MultiTokenPredictionLayer(MegatronModule): - [s, b, h], and optionally the updated context tensor if cross-attention is used. - """ - assert context is None, f"multi token prediction + cross attention is not yet supported." -- assert ( -- packed_seq_params is None -- ), f"multi token prediction + sequence packing is not yet supported." - - input_ids, position_ids, decoder_input, hidden_states = self._get_embeddings( - input_ids=input_ids, - position_ids=position_ids, - embedding=embedding, - hidden_states=hidden_states, -+ packed_seq_params=packed_seq_params, - ) - - if self.config.recompute_granularity == 'full' and self.training: diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py -index d55bebe7e..1eecbbd38 100644 +index e2705bd9f..a0aa109b5 100644 --- a/megatron/core/transformer/transformer_config.py +++ b/megatron/core/transformer/transformer_config.py -@@ -173,6 +173,10 @@ class TransformerConfig(ModelParallelConfig): - qk_layernorm: bool = False - """Whether to apply `normalization` type of normalization to the query and key embeddings.""" +@@ -210,6 +210,9 @@ class TransformerConfig(ModelParallelConfig): + attention_output_gate: bool = False + """Whether to apply output gate to the attention layers.""" + post_self_attn_layernorm: bool = False + post_mlp_layernorm: bool = False -+ use_gated_attention: bool = False + test_mode: bool = False """Whether to run real-time tests.""" diff --git a/megatron/core/transformer/transformer_layer.py b/megatron/core/transformer/transformer_layer.py -index 84f22bdea..f0f3f8e86 100644 +index 3ea405770..5a42001b9 100644 --- a/megatron/core/transformer/transformer_layer.py +++ b/megatron/core/transformer/transformer_layer.py -@@ -224,6 +224,7 @@ class TransformerLayerSubmodules: +@@ -223,6 +223,7 @@ class TransformerLayerSubmodules: input_layernorm: Union[ModuleSpec, type] = IdentityOp self_attention: Union[ModuleSpec, type] = IdentityOp self_attn_bda: Union[ModuleSpec, type] = IdentityFuncOp @@ -759,7 +581,7 @@ index 84f22bdea..f0f3f8e86 100644 pre_cross_attn_layernorm: Union[ModuleSpec, type] = IdentityOp cross_attention: Union[ModuleSpec, type] = IdentityOp -@@ -232,6 +233,7 @@ class TransformerLayerSubmodules: +@@ -231,6 +232,7 @@ class TransformerLayerSubmodules: pre_mlp_layernorm: Union[ModuleSpec, type] = IdentityOp mlp: Union[ModuleSpec, type] = IdentityOp mlp_bda: Union[ModuleSpec, type] = IdentityFuncOp @@ -767,7 +589,7 @@ index 84f22bdea..f0f3f8e86 100644 # Mapping for sharded tensor keys to be applied in `sharded_state_dict` method sharded_state_dict_keys_map: Dict[str, str] = field(default_factory=dict) -@@ -336,6 +338,13 @@ class TransformerLayer(MegatronModule, BaseTransformerLayer): +@@ -310,6 +312,13 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): # [Module 3: BiasDropoutFusion] self.self_attn_bda = build_module(submodules.self_attn_bda) @@ -781,9 +603,9 @@ index 84f22bdea..f0f3f8e86 100644 # [Module 4: Post SelfAttention] Optional Layernorm after self-attn self.pre_cross_attn_layernorm = build_module( submodules.pre_cross_attn_layernorm, -@@ -399,6 +408,13 @@ class TransformerLayer(MegatronModule, BaseTransformerLayer): - # [Module 9: BiasDropoutFusion] - self.mlp_bda = build_module(submodules.mlp_bda) +@@ -375,6 +384,13 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): + + self.is_moe_layer = isinstance(self.mlp, MoELayer) + self.post_mlp_layernorm = build_module( + submodules.post_mlp_layernorm, @@ -795,7 +617,7 @@ index 84f22bdea..f0f3f8e86 100644 self.recompute_input_layernorm = False self.recompute_pre_mlp_layernorm = False self.recompute_mlp = False -@@ -535,6 +551,10 @@ class TransformerLayer(MegatronModule, BaseTransformerLayer): +@@ -551,6 +567,10 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): attention_output_with_bias[0] ) @@ -806,7 +628,7 @@ index 84f22bdea..f0f3f8e86 100644 # TODO: could we move `bias_dropout_add_exec_handler` itself # inside the module provided in the `bias_dropout_add_spec` module? nvtx_range_push(suffix="self_attn_bda") -@@ -635,6 +655,10 @@ class TransformerLayer(MegatronModule, BaseTransformerLayer): +@@ -677,6 +697,10 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): else: mlp_output_with_bias = self.mlp(pre_mlp_layernorm_output) @@ -818,30 +640,20 @@ index 84f22bdea..f0f3f8e86 100644 # discard the output of the pre-mlp layernorm and register the recompute # as a gradient hook of mlp_output_with_bias[0] diff --git a/megatron/training/arguments.py b/megatron/training/arguments.py -index e3459c5ee..7346bf35b 100644 +index b267c8a81..83736acdc 100644 --- a/megatron/training/arguments.py +++ b/megatron/training/arguments.py -@@ -937,8 +937,6 @@ def validate_args(args, defaults={}): - # MoE Spec check - if args.num_experts == 0: - args.num_experts = None -- if args.num_experts is not None: -- assert args.spec is None, "Model Spec must be None when using MoEs" - if args.num_experts is not None and args.moe_ffn_hidden_size is None: - args.moe_ffn_hidden_size = args.ffn_hidden_size - print("Warning: moe_ffn_hidden_size is not set, using ffn_hidden_size for MoE instead.") -@@ -1198,6 +1196,10 @@ def core_transformer_config_from_args(args, config_class=None): - if args.is_hybrid_model: - kw_args['is_hybrid_model'] = args.is_hybrid_model +@@ -1398,6 +1398,9 @@ def core_transformer_config_from_args(args, config_class=None): + + kw_args['inference_sampling_seed'] = args.seed + kw_args['post_self_attn_layernorm'] = args.post_self_attn_layernorm + kw_args['post_mlp_layernorm'] = args.post_mlp_layernorm -+ kw_args['use_gated_attention'] = args.use_gated_attention + # handle quantization config # NOTE: Kitchen arguments are only added to the namespace when # Kitchen library is available. -@@ -1488,6 +1490,12 @@ def _add_network_size_args(parser): +@@ -1764,6 +1767,12 @@ def _add_network_size_args(parser): action='store_true', help='If set, use original BERT residula connection ' 'ordering.') @@ -855,15 +667,15 @@ index e3459c5ee..7346bf35b 100644 help='Use OpenAIs GeLU implementation. This option' 'should not be used unless for backward compatibility' diff --git a/megatron/training/tokenizer/tokenizer.py b/megatron/training/tokenizer/tokenizer.py -index 5cf222ccc..d1554ca4c 100644 +index 13b7526ca..6c590f653 100644 --- a/megatron/training/tokenizer/tokenizer.py +++ b/megatron/training/tokenizer/tokenizer.py -@@ -138,6 +138,8 @@ class _HuggingFaceTokenizer(MegatronTokenizer): - f"The transformers library must be installed to use huggingface_tokenizer_provider" - ) - -+ if "trust_remote_code" not in kwargs: -+ kwargs["trust_remote_code"] = True +@@ -136,7 +136,7 @@ class _HuggingFaceTokenizer(MegatronLegacyTokenizer): # TODO(bnorick): download tokenizer once to lustre and use force offline to make sure all tasks read it from there self._tokenizer = transformers.AutoTokenizer.from_pretrained( - pretrained_model_name_or_path=pretrained_model_name_or_path, **kwargs + pretrained_model_name_or_path=pretrained_model_name_or_path, +- trust_remote_code=trust_remote_code, ++ trust_remote_code=True, + **kwargs, + ) + self._vocab = self._tokenizer.get_vocab() diff --git a/docker/patch/latest/sglang.patch b/docker/patch/latest/sglang.patch index de12cdd43..3522294fc 100644 --- a/docker/patch/latest/sglang.patch +++ b/docker/patch/latest/sglang.patch @@ -16,6 +16,18 @@ index ef52bda7f..537d892dc 100644 def add(self, req: Req, is_retracted: bool = False) -> None: """Add a request to the pending queue.""" if self._check_if_req_exceed_kv_capacity(req): +diff --git a/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py b/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py +index efa979460..d2d049a20 100644 +--- a/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py ++++ b/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py +@@ -83,6 +83,7 @@ class ScheduleBatchDisaggregationDecodeMixin: + seq_lens, dtype=torch.int32, device=self.device + ) + self.out_cache_loc = out_cache_loc ++ self.out_cache_loc_cpu = out_cache_loc.to("cpu", non_blocking=True) + self.seq_lens_sum = sum(seq_lens) + + if self.return_logprob: diff --git a/python/sglang/srt/disaggregation/mooncake/conn.py b/python/sglang/srt/disaggregation/mooncake/conn.py index d4414d084..c5fb10155 100644 --- a/python/sglang/srt/disaggregation/mooncake/conn.py @@ -58,55 +70,6 @@ index 952374ed5..239ac2571 100644 class SchedulerDisaggregationPrefillMixin: """ -diff --git a/python/sglang/srt/distributed/device_communicators/pynccl.py b/python/sglang/srt/distributed/device_communicators/pynccl.py -index 86c53f26b..52acf95b9 100644 ---- a/python/sglang/srt/distributed/device_communicators/pynccl.py -+++ b/python/sglang/srt/distributed/device_communicators/pynccl.py -@@ -380,3 +380,9 @@ class PyNcclCommunicator: - - self.disabled = old_disable - self.stream = old_stream -+ -+ def nccl_pause(self): -+ self.nccl.ncclPause(self.comm) -+ -+ def nccl_resume(self): -+ self.nccl.ncclResume(self.comm) -diff --git a/python/sglang/srt/distributed/device_communicators/pynccl_wrapper.py b/python/sglang/srt/distributed/device_communicators/pynccl_wrapper.py -index 6b12f2922..7028a4e46 100644 ---- a/python/sglang/srt/distributed/device_communicators/pynccl_wrapper.py -+++ b/python/sglang/srt/distributed/device_communicators/pynccl_wrapper.py -@@ -304,6 +304,17 @@ class NCCLLibrary: - Function("ncclGroupEnd", ncclResult_t, []), - ] - -+ if os.environ.get("AMEM_ENABLE", "0") == "1": -+ exported_functions.extend( -+ [ -+ # ncclResult_t ncclPause(ncclComm_t comm); -+ Function("ncclPause", ncclResult_t, [ncclComm_t]), -+ # ncclResult_t ncclResume(ncclComm_t comm); -+ Function("ncclResume", ncclResult_t, [ncclComm_t]), -+ Function("ncclSetGroupID", ncclResult_t, [ctypes.c_int]), -+ ] -+ ) -+ - exported_functions_symm_mem = [ - # ncclResult_t ncclCommWindowRegister(ncclComm_t comm, void* buff, size_t size, ncclWindow_t* win, int winFlags); - Function( -@@ -551,6 +562,12 @@ class NCCLLibrary: - def ncclGroupEnd(self) -> None: - self.NCCL_CHECK(self._funcs["ncclGroupEnd"]()) - -+ def ncclPause(self, comm: ncclComm_t) -> None: -+ self.NCCL_CHECK(self._funcs["ncclPause"](comm)) -+ -+ def ncclResume(self, comm: ncclComm_t) -> None: -+ self.NCCL_CHECK(self._funcs["ncclResume"](comm)) -+ - - __all__ = [ - "NCCLLibrary", diff --git a/python/sglang/srt/distributed/parallel_state.py b/python/sglang/srt/distributed/parallel_state.py index cf90f6fe0..11d26df81 100644 --- a/python/sglang/srt/distributed/parallel_state.py @@ -192,17 +155,26 @@ index 9f556a885..992843285 100644 bsz, s, _ = x_shape head = self.num_attention_heads_per_partition diff --git a/python/sglang/srt/layers/communicator.py b/python/sglang/srt/layers/communicator.py -index 932f52aeb..79c6b664f 100644 +index 932f52aeb..ee52f4c94 100644 --- a/python/sglang/srt/layers/communicator.py +++ b/python/sglang/srt/layers/communicator.py @@ -372,6 +372,7 @@ class LayerCommunicator: residual: torch.Tensor, forward_batch: ForwardBatch, quant_format: str = "", -+ post_residual_addition: Optional[torch.Tensor] = None, ++ **kwargs, ): if get_attn_tp_context().input_scattered: hidden_states, residual = self._tp_reduce_scatter( +@@ -421,7 +422,7 @@ class LayerCommunicator: + ) + + else: +- hidden_states = self.input_layernorm(hidden_states) ++ hidden_states = self.input_layernorm(hidden_states, **kwargs) + else: + + if _use_aiter and _is_gfx95_supported and ("mxfp4" in quant_format): @@ -453,7 +454,9 @@ class LayerCommunicator: ) else: @@ -210,12 +182,12 @@ index 932f52aeb..79c6b664f 100644 - hidden_states, residual + hidden_states, + residual, -+ post_residual_addition, ++ **kwargs, ) hidden_states = self._communicate_simple_fn( diff --git a/python/sglang/srt/layers/layernorm.py b/python/sglang/srt/layers/layernorm.py -index 3293a8a59..a075b71ce 100644 +index 3293a8a59..1dac03e4c 100644 --- a/python/sglang/srt/layers/layernorm.py +++ b/python/sglang/srt/layers/layernorm.py @@ -84,15 +84,12 @@ class RMSNorm(CustomOp): @@ -236,45 +208,73 @@ index 3293a8a59..a075b71ce 100644 self.variance_epsilon = eps self.hidden_size = hidden_size self.variance_size_override = ( -@@ -105,21 +102,26 @@ class RMSNorm(CustomOp): +@@ -105,21 +102,29 @@ class RMSNorm(CustomOp): self, x: torch.Tensor, residual: Optional[torch.Tensor] = None, -+ post_residual_addition: Optional[torch.Tensor] = None, ++ **kwargs, ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: if self.variance_size_override is not None: - return self.forward_native(x, residual) -+ return self.forward_native(x, residual, post_residual_addition) ++ return self.forward_native(x, residual, **kwargs) if is_batch_invariant_mode_enabled(): if ( residual is not None or get_global_server_args().rl_on_policy_target == "fsdp" ): - return self.forward_native(x, residual) -+ return self.forward_native(x, residual, post_residual_addition) ++ return self.forward_native(x, residual, **kwargs) return rms_norm_batch_invariant( x, self.weight.data, self.variance_epsilon, ) if residual is not None: -+ # TODO: Ideally we want to have (a+b)+c. but right now we can only have a+(b+c). -+ # (a+b)+c != a+(b+c), we probably need to add another parameter to fused_add_rmsnorm ++ # TODO: Ideally we want to have (hidden_states+residual)+post_residual_addition. ++ # but right now we can only have hidden_states+(residual+post_residual_addition). ++ # (hidden_states+residual)+post_residual_addition != hidden_states+(residual+post_residual_addition), ++ # we probably need to add another parameter to fused_add_rmsnorm ++ post_residual_addition = kwargs.get("post_residual_addition") + if post_residual_addition is not None: + residual = residual + post_residual_addition fused_add_rmsnorm(x, residual, self.weight.data, self.variance_epsilon) return x, residual out = rmsnorm(x, self.weight.data, self.variance_epsilon) -@@ -179,17 +181,35 @@ class RMSNorm(CustomOp): +@@ -129,6 +134,7 @@ class RMSNorm(CustomOp): self, x: torch.Tensor, residual: Optional[torch.Tensor] = None, -+ post_residual_addition: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if residual is not None: + out, _, residual_out = torch_npu.npu_add_rms_norm( +@@ -141,6 +147,7 @@ class RMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if residual is not None: + residual_out = torch.empty_like(x) +@@ -160,6 +167,7 @@ class RMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if not x.is_contiguous(): + # NOTE: Remove this if aiter kernel supports discontinuous input +@@ -179,17 +187,36 @@ class RMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: if not x.is_contiguous(): x = x.contiguous() - orig_dtype = self.override_orig_dtype or x.dtype + orig_dtype = x.dtype ++ post_residual_addition = kwargs.get("post_residual_addition") + + if residual is not None and not self.fp32_residual: + x = ( @@ -308,6 +308,148 @@ index 3293a8a59..a075b71ce 100644 hidden_size = x.shape[-1] if hidden_size != self.hidden_size: +@@ -226,6 +253,7 @@ class RMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if _is_cpu_amx_available: + if residual is not None: +@@ -237,15 +265,16 @@ class RMSNorm(CustomOp): + x, self.weight.data, self.variance_epsilon + ) + else: +- return self.forward_native(x, residual) ++ return self.forward_native(x, residual, **kwargs) + + def forward_xpu( + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if self.variance_size_override is not None: +- return self.forward_native(x, residual) ++ return self.forward_native(x, residual, **kwargs) + if residual is not None: + fused_add_rmsnorm(x, residual, self.weight.data, self.variance_epsilon) + return x, residual +@@ -307,6 +336,7 @@ class LayerNorm(CustomOp): + def forward_cuda( + self, + x: torch.Tensor, ++ **kwargs, + ) -> torch.Tensor: + if ( + _flashinfer_layernorm_available +@@ -315,11 +345,12 @@ class LayerNorm(CustomOp): + ): + return layernorm(x, self.weight, self.bias, self.variance_epsilon) + else: +- return self.forward_native(x) ++ return self.forward_native(x, **kwargs) + + def forward_native( + self, + x: torch.Tensor, ++ **kwargs, + ) -> torch.Tensor: + weight = self.weight if self.elementwise_affine else None + bias = self.bias if self.use_bias else None +@@ -336,12 +367,14 @@ class LayerNorm(CustomOp): + def forward_hip( + self, + x: torch.Tensor, ++ **kwargs, + ) -> torch.Tensor: +- return self.forward_native(x) ++ return self.forward_native(x, **kwargs) + + def forward_npu( + self, + x: torch.Tensor, ++ **kwargs, + ) -> torch.Tensor: + orig_dtype = x.dtype + x = x.to(self.dtype) +@@ -360,8 +393,9 @@ class LayerNorm(CustomOp): + def forward_cpu( + self, + x: torch.Tensor, ++ **kwargs, + ) -> torch.Tensor: +- return self.forward_native(x) ++ return self.forward_native(x, **kwargs) + + + class GemmaRMSNorm(CustomOp): +@@ -382,6 +416,7 @@ class GemmaRMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if residual is not None: + gemma_fused_add_rmsnorm( +@@ -395,6 +430,7 @@ class GemmaRMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + orig_dtype = x.dtype + if residual is not None: +@@ -412,13 +448,15 @@ class GemmaRMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: +- return self._forward_impl(x, residual) ++ return self._forward_impl(x, residual, **kwargs) + + def forward_npu( + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if residual is not None: + x = x + residual +@@ -431,8 +469,9 @@ class GemmaRMSNorm(CustomOp): + self, + x: torch.Tensor, + residual: Optional[torch.Tensor] = None, ++ **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: +- return self._forward_impl(x, residual) ++ return self._forward_impl(x, residual, **kwargs) + + + class Gemma3RMSNorm(CustomOp): +@@ -445,17 +484,17 @@ class Gemma3RMSNorm(CustomOp): + def _norm(self, x): + return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) + +- def forward_native(self, x): ++ def forward_native(self, x, **kwargs): + output = self._norm(x.float()) + # Llama does x.to(float16) * w whilst Gemma3 is (x * w).to(float16) + # See https://github.com/huggingface/transformers/pull/29402 + output = output * (1.0 + self.weight.float()) + return output.type_as(x) + +- def forward_cuda(self, x): +- return self.forward_native(x) ++ def forward_cuda(self, x, **kwargs): ++ return self.forward_native(x, **kwargs) + +- def forward_npu(self, x): ++ def forward_npu(self, x, **kwargs): + output, _ = torch_npu.npu_gemma_rms_norm(x, self.weight, self.eps) + return output + diff --git a/python/sglang/srt/layers/logits_processor.py b/python/sglang/srt/layers/logits_processor.py index 522865765..733bad5f2 100644 --- a/python/sglang/srt/layers/logits_processor.py @@ -350,10 +492,10 @@ index e7d5a67cc..639e47163 100644 out_hidden_states[begin_chunk_idx:end_chunk_idx], diff --git a/python/sglang/srt/layers/moe/routed_experts_capturer.py b/python/sglang/srt/layers/moe/routed_experts_capturer.py new file mode 100644 -index 000000000..e16817f1f +index 000000000..11adcaa77 --- /dev/null +++ b/python/sglang/srt/layers/moe/routed_experts_capturer.py -@@ -0,0 +1,279 @@ +@@ -0,0 +1,305 @@ +import logging +from abc import ABC +from contextlib import contextmanager @@ -364,12 +506,17 @@ index 000000000..e16817f1f + +from sglang.srt.configs.model_config import ModelConfig +from sglang.srt.layers.dp_attention import ( ++ attn_tp_all_gather_into_tensor, + get_attention_dp_rank, ++ get_attention_tp_size, + get_dp_local_info, + is_dp_attention_enabled, +) +from sglang.srt.mem_cache.memory_pool import ReqToTokenPool +from sglang.srt.server_args import get_global_server_args ++from sglang.srt.layers.moe import ( ++ get_moe_a2a_backend, ++) + +logger = logging.getLogger(__name__) + @@ -446,9 +593,6 @@ index 000000000..e16817f1f + assert hasattr(self, "buffer") + return get_tensor_size_bytes(self.buffer) + -+ def set_experts_buffer(self, layer_id: int, loc: torch.Tensor, top_k: torch.Tensor): -+ self.buffer[layer_id, loc, :] = top_k.to(device="cpu", non_blocking=True) -+ + def _finalize_allocation_log(self): + """Common logging and memory usage computation for captured experts buffers.""" + buffer_size_GB = self.get_buffer_size_bytes() / _GB @@ -539,7 +683,24 @@ index 000000000..e16817f1f + device=device, + ) + ++ if get_moe_a2a_backend().is_deepep(): ++ attn_tp_size = get_attention_tp_size() if is_dp_attention_enabled() else 1 ++ self.gather_buffer = torch.empty( ++ ( ++ self.device_cache.buffer.shape[0] * attn_tp_size, ++ self.device_cache.buffer.shape[2], ++ ), ++ dtype=torch.int32, ++ device=device, ++ ) ++ + def capture(self, layer_id: int, topk_ids: torch.Tensor): ++ if get_moe_a2a_backend().is_deepep(): ++ local_topk_ids = topk_ids ++ topk_ids = self.gather_buffer[ ++ : local_topk_ids.size(0) * get_attention_tp_size() ++ ] ++ attn_tp_all_gather_into_tensor(topk_ids, local_topk_ids) + self.device_cache.capture_fwd_routed_experts(layer_id, topk_ids) + + def sync_fwd_experts_buffer_DtoH( @@ -549,7 +710,9 @@ index 000000000..e16817f1f + can_run_graph: bool, + cuda_graph_batch: int, + ): -+ if is_dp_attention_enabled(): ++ # When DeepEP is enabled, capture() already does all_gather, so device_cache.buffer ++ # contains data from all DP ranks. We should not slice by DP rank in this case. ++ if is_dp_attention_enabled() and not get_moe_a2a_backend().is_deepep(): + local_start_pos, local_num_tokens = get_dp_local_info(self.forward_batch) + # handle with cuda graph padding + if can_run_graph: @@ -561,6 +724,11 @@ index 000000000..e16817f1f + local_start_pos = 0 + local_end_pos = device_loc.shape[0] + ++ if self.forward_batch.num_token_non_padded is not None: ++ assert local_end_pos - local_start_pos >= self.forward_batch.num_token_non_padded ++ local_end_pos = local_start_pos + self.forward_batch.num_token_non_padded ++ cpu_loc = cpu_loc[: self.forward_batch.num_token_non_padded] ++ + self.host_cache.buffer[cpu_loc] = self.device_cache.buffer[ + local_start_pos:local_end_pos, :, : self.num_experts_per_tok + ].cpu() @@ -838,27 +1006,23 @@ index 7f6f6a010..c4a673145 100644 if not get_global_server_args().sampling_backend == "ascend" or ( return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB diff --git a/python/sglang/srt/managers/detokenizer_manager.py b/python/sglang/srt/managers/detokenizer_manager.py -index 87922077e..8cb6bad8d 100644 +index 87922077e..6507d8bf5 100644 --- a/python/sglang/srt/managers/detokenizer_manager.py +++ b/python/sglang/srt/managers/detokenizer_manager.py -@@ -247,6 +247,16 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): +@@ -247,6 +247,12 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): s.sent_offset = len(output_str) output_strs.append(incremental_output) + output_routed_experts = [] + if recv_obj.output_routed_experts is not None: + output_routed_experts = [ -+ ( -+ output_routed_experts.tolist() -+ if output_routed_experts is not None -+ else [] -+ ) ++ output_routed_experts + for output_routed_experts in recv_obj.output_routed_experts + ] return BatchStrOutput( rids=recv_obj.rids, http_worker_ipcs=recv_obj.http_worker_ipcs, -@@ -272,6 +282,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): +@@ -272,6 +278,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): output_token_ids_logprobs_idx=recv_obj.output_token_ids_logprobs_idx, output_token_entropy_val=recv_obj.output_token_entropy_val, output_hidden_states=recv_obj.output_hidden_states, @@ -927,7 +1091,7 @@ index e34736cc4..5e5997a1a 100644 # idx is the index of the token in the prompt after expansion. # val is the length of padded tokens after expansion. diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py -index c4c5a9ebb..1450c5fd8 100644 +index c4c5a9ebb..3650ba881 100644 --- a/python/sglang/srt/managers/schedule_batch.py +++ b/python/sglang/srt/managers/schedule_batch.py @@ -450,6 +450,7 @@ class Req: @@ -977,7 +1141,15 @@ index c4c5a9ebb..1450c5fd8 100644 is_prefill_only=all(req.is_prefill_only for req in reqs), chunked_req=chunked_req, dllm_config=dllm_config, -@@ -1457,6 +1469,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1282,6 +1294,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): + ) + else: + self.out_cache_loc = torch.cat(decoder_out_cache_loc) ++ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) + + if not encoder_out_cache_loc: + self.encoder_out_cache_loc = torch.zeros(0, dtype=torch.int64).to( +@@ -1457,6 +1470,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): self.req_pool_indices = req_pool_indices_tensor self.orig_seq_lens = orig_seq_lens_tensor self.out_cache_loc = out_cache_loc @@ -985,7 +1157,7 @@ index c4c5a9ebb..1450c5fd8 100644 self.input_embeds = ( torch.tensor(input_embeds).to(self.device, non_blocking=True) if input_embeds -@@ -1508,10 +1521,14 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1508,10 +1522,14 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): input_ids = torch.cat([self.input_ids, running_batch.input_ids]) out_cache_loc = torch.cat([self.out_cache_loc, running_batch.out_cache_loc]) @@ -1000,7 +1172,7 @@ index c4c5a9ebb..1450c5fd8 100644 # For overlap scheduler, the output_ids has one step delay delta = 0 if self.enable_overlap else -1 -@@ -1677,6 +1694,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1677,6 +1695,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): self.seq_lens_cpu = torch.empty(0, dtype=torch.int64) self.orig_seq_lens = torch.empty(0, dtype=torch.int32, device=self.device) self.out_cache_loc = torch.empty(0, dtype=torch.int64, device=self.device) @@ -1008,7 +1180,7 @@ index c4c5a9ebb..1450c5fd8 100644 self.req_pool_indices = torch.empty(0, dtype=torch.int32, device=self.device) self.seq_lens_sum = 0 self.extend_num_tokens = 0 -@@ -1736,6 +1754,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1736,6 +1755,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): # Allocate memory self.out_cache_loc = alloc_for_decode(self, token_per_req=1) @@ -1016,7 +1188,7 @@ index c4c5a9ebb..1450c5fd8 100644 # Update req-level memory management fields for req in self.reqs: -@@ -1807,6 +1826,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1807,6 +1827,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): self.seq_lens_cpu = self.seq_lens_cpu[keep_indices] self.orig_seq_lens = self.orig_seq_lens[keep_indices_device] self.out_cache_loc = None @@ -1024,7 +1196,7 @@ index c4c5a9ebb..1450c5fd8 100644 self.seq_lens_sum = self.seq_lens.sum().item() self.output_ids = self.output_ids[keep_indices_device] self.return_logprob = any(req.return_logprob for req in self.reqs) -@@ -1852,6 +1872,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1852,6 +1873,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): self.seq_lens_cpu = torch.cat([self.seq_lens_cpu, other.seq_lens_cpu]) self.orig_seq_lens = torch.cat([self.orig_seq_lens, other.orig_seq_lens]) self.out_cache_loc = None @@ -1032,7 +1204,7 @@ index c4c5a9ebb..1450c5fd8 100644 self.seq_lens_sum += other.seq_lens_sum if self.output_ids is not None: self.output_ids = torch.cat([self.output_ids, other.output_ids]) -@@ -1903,6 +1924,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1903,6 +1925,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): seq_lens=self.seq_lens, orig_seq_lens=self.orig_seq_lens, out_cache_loc=self.out_cache_loc, @@ -1040,7 +1212,7 @@ index c4c5a9ebb..1450c5fd8 100644 seq_lens_cpu=seq_lens_cpu, seq_lens_sum=self.seq_lens_sum, return_logprob=self.return_logprob, -@@ -1983,7 +2005,8 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -1983,7 +2006,8 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): def __str__(self): return ( f"ScheduleBatch(forward_mode={self.forward_mode.name if self.forward_mode else 'None'}, " @@ -1050,7 +1222,7 @@ index c4c5a9ebb..1450c5fd8 100644 ) -@@ -2038,6 +2061,9 @@ class ModelWorkerBatch: +@@ -2038,6 +2062,9 @@ class ModelWorkerBatch: # Sampling info sampling_info: SamplingBatchInfo @@ -1140,7 +1312,7 @@ index c48f5f893..a9796c25f 100644 placeholder_tokens_val=None, retraction_counts=retraction_counts, diff --git a/python/sglang/srt/managers/scheduler_update_weights_mixin.py b/python/sglang/srt/managers/scheduler_update_weights_mixin.py -index f8ebfc1f4..a05449fac 100644 +index f8ebfc1f4..48b9a1a3b 100644 --- a/python/sglang/srt/managers/scheduler_update_weights_mixin.py +++ b/python/sglang/srt/managers/scheduler_update_weights_mixin.py @@ -1,6 +1,7 @@ @@ -1175,49 +1347,7 @@ index f8ebfc1f4..a05449fac 100644 if GPU_MEMORY_TYPE_WEIGHTS in tags: self.stashed_model_static_state = _export_static_state( self.tp_worker.model_runner.model -@@ -137,6 +148,20 @@ class SchedulerUpdateWeightsMixin: - if GPU_MEMORY_TYPE_CUDA_GRAPH in tags: - self.memory_saver_adapter.pause(GPU_MEMORY_TYPE_CUDA_GRAPH) - -+ if os.environ.get("AMEM_ENABLE", "0") == "1": -+ tp_group = get_tp_group() -+ if tp_group is not None and tp_group.pynccl_comm is not None: -+ tp_group.pynccl_comm.nccl_pause() -+ attn_tp_group = get_attention_tp_group() -+ if attn_tp_group is not None and attn_tp_group.pynccl_comm is not None: -+ attn_tp_group.pynccl_comm.nccl_pause() -+ moe_ep_group = get_moe_ep_group() -+ if moe_ep_group is not None and moe_ep_group.pynccl_comm is not None: -+ moe_ep_group.pynccl_comm.nccl_pause() -+ moe_tp_group = get_moe_tp_group() -+ if moe_tp_group is not None and moe_tp_group.pynccl_comm is not None: -+ moe_tp_group.pynccl_comm.nccl_pause() -+ - torch.get_device_module().synchronize() - - return ReleaseMemoryOccupationReqOutput() -@@ -155,6 +180,20 @@ class SchedulerUpdateWeightsMixin: - if GPU_MEMORY_TYPE_CUDA_GRAPH in tags: - self.memory_saver_adapter.resume(GPU_MEMORY_TYPE_CUDA_GRAPH) - -+ if os.environ.get("AMEM_ENABLE", "0") == "1": -+ tp_group = get_tp_group() -+ if tp_group is not None and tp_group.pynccl_comm is not None: -+ tp_group.pynccl_comm.nccl_resume() -+ attn_tp_group = get_attention_tp_group() -+ if attn_tp_group is not None and attn_tp_group.pynccl_comm is not None: -+ attn_tp_group.pynccl_comm.nccl_resume() -+ moe_ep_group = get_moe_ep_group() -+ if moe_ep_group is not None and moe_ep_group.pynccl_comm is not None: -+ moe_ep_group.pynccl_comm.nccl_resume() -+ moe_tp_group = get_moe_tp_group() -+ if moe_tp_group is not None and moe_tp_group.pynccl_comm is not None: -+ moe_tp_group.pynccl_comm.nccl_resume() -+ - if GPU_MEMORY_TYPE_WEIGHTS in tags: - self.memory_saver_adapter.resume(GPU_MEMORY_TYPE_WEIGHTS) - torch.distributed.barrier(self.tp_cpu_group) -@@ -167,6 +206,13 @@ class SchedulerUpdateWeightsMixin: +@@ -167,6 +178,13 @@ class SchedulerUpdateWeightsMixin: if GPU_MEMORY_TYPE_KV_CACHE in tags: self.memory_saver_adapter.resume(GPU_MEMORY_TYPE_KV_CACHE) @@ -1231,11 +1361,47 @@ index f8ebfc1f4..a05449fac 100644 return ResumeMemoryOccupationReqOutput() def check_weights(self: Scheduler, recv_req: CheckWeightsReqInput): +diff --git a/python/sglang/srt/managers/tokenizer_communicator_mixin.py b/python/sglang/srt/managers/tokenizer_communicator_mixin.py +index edbc52526..2cdc42755 100644 +--- a/python/sglang/srt/managers/tokenizer_communicator_mixin.py ++++ b/python/sglang/srt/managers/tokenizer_communicator_mixin.py +@@ -421,6 +421,11 @@ class TokenizerCommunicatorMixin: + result = (await self.update_weights_from_distributed_communicator(obj))[ + 0 + ] ++ if result.success and obj.weight_version is not None: ++ self._update_weight_version_if_provided(obj.weight_version) ++ result.message += ( ++ f" Weight version updated to {obj.weight_version}." ++ ) + return result.success, result.message + + # This means that weight sync +@@ -480,6 +485,11 @@ class TokenizerCommunicatorMixin: + async with self.is_pause_cond: + if self.is_pause: + result = (await self.update_weights_from_tensor_communicator(obj))[0] ++ if result.success and obj.weight_version is not None: ++ self._update_weight_version_if_provided(obj.weight_version) ++ result.message += ( ++ f" Weight version updated to {obj.weight_version}." ++ ) + return result.success, result.message + + # This means that weight sync diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py -index b90cf0616..98d71d896 100644 +index b90cf0616..8a5cbdbed 100644 --- a/python/sglang/srt/managers/tokenizer_manager.py +++ b/python/sglang/srt/managers/tokenizer_manager.py -@@ -888,6 +888,7 @@ class TokenizerManager(TokenizerCommunicatorMixin): +@@ -20,6 +20,7 @@ import logging + import math + import os + import pickle ++import pybase64 + import signal + import sys + import threading +@@ -888,6 +889,7 @@ class TokenizerManager(TokenizerCommunicatorMixin): session_params=session_params, custom_logit_processor=obj.custom_logit_processor, return_hidden_states=obj.return_hidden_states, @@ -1243,17 +1409,22 @@ index b90cf0616..98d71d896 100644 data_parallel_rank=obj.data_parallel_rank, priority=obj.priority, extra_key=obj.extra_key, -@@ -1621,6 +1622,9 @@ class TokenizerManager(TokenizerCommunicatorMixin): +@@ -1621,6 +1623,14 @@ class TokenizerManager(TokenizerCommunicatorMixin): if getattr(recv_obj, "output_hidden_states", None): meta_info["hidden_states"] = recv_obj.output_hidden_states[i] + if getattr(recv_obj, "output_routed_experts", None): -+ meta_info["routed_experts"] = recv_obj.output_routed_experts[i] ++ if recv_obj.output_routed_experts[i] is not None: ++ meta_info["routed_experts"] = pybase64.b64encode( ++ recv_obj.output_routed_experts[i].contiguous().numpy().tobytes(order="C") ++ ).decode("ascii") ++ else: ++ meta_info["routed_experts"] = None + if isinstance(recv_obj, BatchStrOutput): state.text += recv_obj.output_strs[i] if self.server_args.stream_output and state.obj.stream: -@@ -1747,12 +1751,13 @@ class TokenizerManager(TokenizerCommunicatorMixin): +@@ -1747,12 +1757,13 @@ class TokenizerManager(TokenizerCommunicatorMixin): return if len(recv_obj.input_token_logprobs_val) > 0: @@ -1274,7 +1445,7 @@ index b90cf0616..98d71d896 100644 recv_obj.output_token_logprobs_val[recv_obj_index] ) diff --git a/python/sglang/srt/model_executor/forward_batch_info.py b/python/sglang/srt/model_executor/forward_batch_info.py -index 3a85e6a7e..2859dafa1 100644 +index 3a85e6a7e..5d74adca6 100644 --- a/python/sglang/srt/model_executor/forward_batch_info.py +++ b/python/sglang/srt/model_executor/forward_batch_info.py @@ -51,6 +51,7 @@ from sglang.srt.layers.dp_attention import ( @@ -1327,19 +1498,25 @@ index 3a85e6a7e..2859dafa1 100644 # text only mrope_positions = torch.tensor( [ -@@ -823,6 +834,10 @@ class ForwardBatch: +@@ -823,6 +834,8 @@ class ForwardBatch: ) self.out_cache_loc = self._pad_tensor_to_size(self.out_cache_loc, num_tokens) + if self.out_cache_loc_cpu is not None: -+ self.out_cache_loc_cpu = self._pad_tensor_to_size( -+ self.out_cache_loc_cpu, num_tokens -+ ) ++ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) if self.encoder_lens is not None: self.encoder_lens = self._pad_tensor_to_size(self.encoder_lens, bs) self.positions = self._pad_tensor_to_size(self.positions, num_tokens) +@@ -906,6 +919,7 @@ class ForwardBatch: + self.spec_info.hidden_states = self.hidden_states_backup + if hasattr(self, "output_cache_loc_backup"): + self.out_cache_loc = self.output_cache_loc_backup ++ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) + + elif self.forward_mode.is_decode() or self.forward_mode.is_idle(): + logits_output.next_token_logits = logits_output.next_token_logits[:bs] diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py -index 4d58278b7..8f50dc430 100644 +index 4d58278b7..81c6a5c7c 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py @@ -94,6 +94,11 @@ from sglang.srt.layers.dp_attention import ( @@ -1354,18 +1531,19 @@ index 4d58278b7..8f50dc430 100644 from sglang.srt.layers.pooler import EmbeddingPoolerOutput from sglang.srt.layers.sampler import Sampler from sglang.srt.layers.torchao_utils import apply_torchao_config_to_model -@@ -502,6 +507,10 @@ class ModelRunner: +@@ -502,6 +507,11 @@ class ModelRunner: server_args.max_running_requests, server_args.max_total_tokens, ) + + # Init routed experts capturer -+ self.init_routed_experts_capturer() ++ if not self.is_draft_worker: ++ self.init_routed_experts_capturer() + if self.device == "cuda": self.init_cublas() self.init_attention_backend() -@@ -545,6 +554,40 @@ class ModelRunner: +@@ -545,6 +555,40 @@ class ModelRunner: # Initialize piecewise CUDA graph self.init_piecewise_cuda_graphs() @@ -1406,20 +1584,7 @@ index 4d58278b7..8f50dc430 100644 def model_specific_adjustment(self): server_args = self.server_args -@@ -792,7 +835,11 @@ class ModelRunner: - ) - with self.memory_saver_adapter.region( - GPU_MEMORY_TYPE_WEIGHTS, -- enable_cpu_backup=enable_cpu_backup, -+ enable_cpu_backup=( -+ self.server_args.enable_weights_cpu_backup -+ if not self.is_draft_worker -+ else True -+ ), - ): - self.model = get_model( - model_config=self.model_config, -@@ -2645,9 +2692,12 @@ class ModelRunner: +@@ -2645,9 +2689,12 @@ class ModelRunner: ) -> Tuple[Union[LogitsProcessorOutput, PPProxyTensors], bool]: self.forward_pass_id += 1 @@ -1435,22 +1600,23 @@ index 4d58278b7..8f50dc430 100644 ): output = self._forward_raw( forward_batch, -@@ -2656,6 +2706,13 @@ class ModelRunner: +@@ -2656,6 +2703,14 @@ class ModelRunner: reinit_attn_backend, split_forward_count, ) + # Copy cached routing experts' buffers back to CPU cache -+ get_global_experts_capturer().sync_fwd_experts_buffer_DtoH( -+ device_loc=forward_batch.out_cache_loc, -+ cpu_loc=forward_batch.out_cache_loc_cpu, -+ can_run_graph=output[1], -+ cuda_graph_batch=getattr(self.graph_runner, "bs", None), -+ ) ++ if not self.is_draft_worker: ++ get_global_experts_capturer().sync_fwd_experts_buffer_DtoH( ++ device_loc=forward_batch.out_cache_loc, ++ cpu_loc=forward_batch.out_cache_loc_cpu, ++ can_run_graph=output[1], ++ cuda_graph_batch=getattr(self.graph_runner, "bs", None), ++ ) if self.eplb_manager is not None: self.eplb_manager.on_forward_pass_end() diff --git a/python/sglang/srt/models/deepseek_v2.py b/python/sglang/srt/models/deepseek_v2.py -index dc30b4f0a..f29dc4b71 100644 +index dc30b4f0a..57625cdeb 100644 --- a/python/sglang/srt/models/deepseek_v2.py +++ b/python/sglang/srt/models/deepseek_v2.py @@ -667,6 +667,7 @@ class DeepseekV2MoE(nn.Module): @@ -1461,6 +1627,19 @@ index dc30b4f0a..f29dc4b71 100644 renormalize=config.norm_topk_prob, use_grouped_topk=True, num_expert_group=config.n_group, +@@ -2641,7 +2642,11 @@ class DeepseekV2AttentionMLA(nn.Module): + ): + k = k_nope.new_empty(*k_shape) + concat_mla_k(k=k, k_nope=k_nope, k_rope=k_pe) +- elif _is_cuda: ++ elif _is_cuda and all( ++ # (i.bit_count() == 1) == (is_power_of_two(i)) ++ i.bit_count() == 1 ++ for i in (k_shape[1], k_nope.shape[-1], k_pe.shape[-1]) ++ ): + # fa3 mha support fp8 inputs + if ( + self.current_attention_backend == "fa3" diff --git a/python/sglang/srt/models/ernie4.py b/python/sglang/srt/models/ernie4.py index ab1b6576b..dffd8f09a 100644 --- a/python/sglang/srt/models/ernie4.py @@ -1474,27 +1653,17 @@ index ab1b6576b..dffd8f09a 100644 use_grouped_topk=False, correction_bias=self.gate.e_score_correction_bias, diff --git a/python/sglang/srt/models/glm4_moe.py b/python/sglang/srt/models/glm4_moe.py -index a9689b8f2..bc8538da8 100644 +index a9689b8f2..0a6c467b1 100644 --- a/python/sglang/srt/models/glm4_moe.py +++ b/python/sglang/srt/models/glm4_moe.py -@@ -379,6 +379,17 @@ class Glm4MoeSparseMoeBlock(nn.Module): - - self.gate = Glm4MoeGate(config=config, prefix=add_prefix("gate", prefix)) +@@ -393,6 +393,7 @@ class Glm4MoeSparseMoeBlock(nn.Module): -+ self.topk = TopK( -+ top_k=self.top_k, + self.topk = TopK( + top_k=self.top_k + self.num_fused_shared_experts, + layer_id=self.layer_id, -+ renormalize=config.norm_topk_prob, -+ use_grouped_topk=True, -+ num_expert_group=config.n_group, -+ topk_group=config.topk_group, -+ correction_bias=self.gate.e_score_correction_bias, -+ routed_scaling_factor=self.routed_scaling_factor, -+ ) -+ - self.experts = get_moe_impl_class(quant_config)( - num_experts=config.n_routed_experts + self.num_fused_shared_experts, - num_fused_shared_experts=self.num_fused_shared_experts, + renormalize=config.norm_topk_prob, + use_grouped_topk=True, + num_expert_group=config.n_group, diff --git a/python/sglang/srt/models/gpt_oss.py b/python/sglang/srt/models/gpt_oss.py index 9474700c4..398d622ff 100644 --- a/python/sglang/srt/models/gpt_oss.py @@ -1613,7 +1782,7 @@ index ea33e81ef..561934dce 100644 self.norm = PPMissingLayer(return_tuple=True) diff --git a/python/sglang/srt/models/qwen3.py b/python/sglang/srt/models/qwen3.py -index 30b92acbd..a0d14895f 100644 +index 30b92acbd..0d28e0f2b 100644 --- a/python/sglang/srt/models/qwen3.py +++ b/python/sglang/srt/models/qwen3.py @@ -90,8 +90,8 @@ class Qwen3Attention(nn.Module): @@ -1642,7 +1811,7 @@ index 30b92acbd..a0d14895f 100644 hidden_states: torch.Tensor, forward_batch: ForwardBatch, residual: Optional[torch.Tensor], -+ post_residual_addition: Optional[torch.Tensor] = None, ++ **kwargs, ) -> Tuple[torch.Tensor, torch.Tensor]: # Self Attention hidden_states, residual = self.layer_communicator.prepare_attn( @@ -1650,7 +1819,7 @@ index 30b92acbd..a0d14895f 100644 + hidden_states, + residual, + forward_batch, -+ post_residual_addition=post_residual_addition, ++ **kwargs, ) if hidden_states.shape[0] != 0: hidden_states = self.self_attn( @@ -1975,10 +2144,18 @@ index 370aec2b6..47666d8f3 100644 elif processor.__class__.__name__ not in { "Qwen2_5_VLProcessor", diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py -index 8e7753dab..323788f39 100644 +index 8e7753dab..15a0823fd 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py -@@ -535,6 +535,7 @@ class ServerArgs: +@@ -489,6 +489,7 @@ class ServerArgs: + cuda_graph_max_bs: Optional[int] = None + cuda_graph_bs: Optional[List[int]] = None + disable_cuda_graph: bool = False ++ disable_draft_cuda_graph: bool = False + disable_cuda_graph_padding: bool = False + enable_profile_cuda_graph: bool = False + enable_cudagraph_gc: bool = False +@@ -535,6 +536,7 @@ class ServerArgs: disable_fast_image_processor: bool = False keep_mm_feature_on_device: bool = False enable_return_hidden_states: bool = False @@ -1986,7 +2163,7 @@ index 8e7753dab..323788f39 100644 scheduler_recv_interval: int = 1 numa_node: Optional[List[int]] = None enable_deterministic_inference: bool = False -@@ -1966,6 +1967,9 @@ class ServerArgs: +@@ -1966,6 +1968,9 @@ class ServerArgs: "Enable deterministic inference because of rl_on_policy_target." ) self.enable_deterministic_inference = True @@ -1996,7 +2173,19 @@ index 8e7753dab..323788f39 100644 # TODO remove this environment variable as a whole os.environ["SGLANG_ENABLE_DETERMINISTIC_INFERENCE"] = "1" -@@ -3705,6 +3709,11 @@ class ServerArgs: +@@ -3462,6 +3467,11 @@ class ServerArgs: + action="store_true", + help="Disable cuda graph.", + ) ++ parser.add_argument( ++ "--disable-draft-cuda-graph", ++ action="store_true", ++ help="Disable cuda graph for draft model in speculative decoding.", ++ ) + parser.add_argument( + "--disable-cuda-graph-padding", + action="store_true", +@@ -3705,6 +3715,11 @@ class ServerArgs: action="store_true", help="Enable returning hidden states with responses.", ) @@ -2009,10 +2198,34 @@ index 8e7753dab..323788f39 100644 "--scheduler-recv-interval", type=int, diff --git a/python/sglang/srt/speculative/eagle_info.py b/python/sglang/srt/speculative/eagle_info.py -index b3d72df05..ddfe0b178 100644 +index b3d72df05..09a1634e0 100644 --- a/python/sglang/srt/speculative/eagle_info.py +++ b/python/sglang/srt/speculative/eagle_info.py -@@ -746,6 +746,10 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): +@@ -135,6 +135,7 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin): + len(batch.input_ids), + ) + self.last_loc = last_loc ++ batch.out_cache_loc_cpu = batch.out_cache_loc.to("cpu", non_blocking=True) + + bs = batch.batch_size() + assign_req_to_token_pool_func( +@@ -492,6 +493,7 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin): + batch.out_cache_loc = tgt_cache_loc + batch.seq_lens.add_(accept_length + 1) + batch.seq_lens_cpu.add_(accept_length_cpu + 1) ++ batch.out_cache_loc_cpu = batch.out_cache_loc.to("cpu", non_blocking=True) + + draft_input = EagleDraftInput( + hidden_states=batch.spec_info.hidden_states[accept_index], +@@ -575,6 +577,7 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin): + topk=self.topk, + capture_hidden_mode=CaptureHiddenMode.LAST, + ) ++ batch.out_cache_loc_cpu = batch.out_cache_loc.to("cpu", non_blocking=True) + + return EagleVerifyOutput( + draft_input=draft_input, +@@ -746,6 +749,10 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): self.topk_index = self.topk_index[: len(new_indices)] self.hidden_states = self.hidden_states[: len(new_indices)] self.verified_id = self.verified_id[: len(new_indices)] @@ -2023,7 +2236,7 @@ index b3d72df05..ddfe0b178 100644 else: # in some cases(e.g draft_extend), we have not filtered the batch by `unfinished_index` self.topk_p = self.topk_p[new_indices] -@@ -777,6 +781,27 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): +@@ -777,6 +784,27 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): self.verified_id = torch.cat([self.verified_id, spec_info.verified_id], axis=0) self.topk_p = torch.cat([self.topk_p, spec_info.topk_p]) self.topk_index = torch.cat([self.topk_index, spec_info.topk_index]) @@ -2051,3 +2264,16 @@ index b3d72df05..ddfe0b178 100644 @dataclass +diff --git a/python/sglang/srt/speculative/eagle_worker.py b/python/sglang/srt/speculative/eagle_worker.py +index 07e3798f1..dede648ee 100644 +--- a/python/sglang/srt/speculative/eagle_worker.py ++++ b/python/sglang/srt/speculative/eagle_worker.py +@@ -222,7 +222,7 @@ class EAGLEWorker(TpModelWorker): + self.cuda_graph_runner = None + self.cuda_graph_runner_for_draft_extend = None + +- if self.server_args.disable_cuda_graph: ++ if self.server_args.disable_cuda_graph or self.server_args.disable_draft_cuda_graph: + return + + Device2DraftCudaGraphRunner = { diff --git a/docker/version.txt b/docker/version.txt index b480e0254..874b9fb07 100644 --- a/docker/version.txt +++ b/docker/version.txt @@ -1 +1 @@ -nightly-dev-20251212a \ No newline at end of file +nightly-dev-20251229a \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 9e4971a1f..5251aa5ab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Miles Documentation -We recommend new contributors start from writing documentation, which helps you quickly understand SGLang codebase. +We recommend new contributors start from writing documentation, which helps you quickly understand miles codebase. Most documentation files are located under the `docs/` folder. ## Docs Workflow diff --git a/docs/en/advanced/pd-disaggregation.md b/docs/en/advanced/pd-disaggregation.md new file mode 100644 index 000000000..a4bca69bb --- /dev/null +++ b/docs/en/advanced/pd-disaggregation.md @@ -0,0 +1,5 @@ +# PD Disaggregation + +Miles supports Prefill and Decode disaggregation (PD Disaggregation). + +You can set the number of servers used for Prefill by setting the `--prefill-num-servers` argument. diff --git a/docs/en/advanced/speculative-decoding.md b/docs/en/advanced/speculative-decoding.md index f85d0ca1f..2c37b6aa4 100644 --- a/docs/en/advanced/speculative-decoding.md +++ b/docs/en/advanced/speculative-decoding.md @@ -4,7 +4,7 @@ Speculative decoding is a key optimization for speeding up rollouts. Instead of ## Accelerating Inference with Speculative Decoding -For models with MTP layers (e.g., GLM-4.6, DeepSeek-V3/R1), simply add: +For models with MTP layers (e.g., GLM-4.7, DeepSeek-V3/R1), simply add: ```bash --sglang-speculative-algorithm EAGLE diff --git a/docs/en/examples/deepseek-r1.md b/docs/en/examples/deepseek-r1.md index e1c24e3ad..19b418e97 100644 --- a/docs/en/examples/deepseek-r1.md +++ b/docs/en/examples/deepseek-r1.md @@ -11,7 +11,7 @@ Regarding parallelism, for sglang we will enable EP64, activate dp attention, an ## Environment Setup -For instructions on setting up the environment and downloading data, please refer to [Example: Qwen3-4B](./qwen3-4B.md). +For instructions on setting up the environment and downloading data, please refer to [Example: Qwen3-4B](qwen3-4B.md). To prepare the DeepSeek R1 checkpoint, first you will need to download DeepSeek-R1 to a directory accessible by all machines (hereinafter referred to as `$BASE_DIR`): @@ -85,7 +85,7 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" source "${SCRIPT_DIR}/models/deepseek-v3.sh" ``` -This reads the model's config from [scripts/models/deepseek-v3.sh](../../../scripts/models/deepseek-v3.sh). These configs are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](../../../scripts/models/). +This reads the model's config from [scripts/models/deepseek-v3.sh](https://github.com/radixark/miles/blob/main/scripts/models/deepseek-v3.sh). These configs are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](https://github.com/radixark/miles/tree/main/scripts/models/). #### CKPT\_ARGS diff --git a/docs/en/examples/glm4-9B.md b/docs/en/examples/glm4-9B.md index f46e9f373..8f9bff6e7 100644 --- a/docs/en/examples/glm4-9B.md +++ b/docs/en/examples/glm4-9B.md @@ -49,7 +49,7 @@ bash scripts/run-glm4-9B.sh ### Parameter Introduction -Here, we will briefly introduce the various components of the [run-glm4-9B.sh](../../../scripts/run-glm4-9B.sh) script: +Here, we will briefly introduce the various components of the [run-glm4-9B.sh](https://github.com/radixark/miles/blob/main/scripts/run-glm4-9B.sh) script: #### MODEL\_ARGS @@ -58,7 +58,7 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" source "${SCRIPT_DIR}/models/glm4-9B.sh" ``` -Reads the model's config from [scripts/models/glm4-9B.sh](../../../scripts/models/glm4-9B.sh). These configs are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](../../../scripts/models/). +Reads the model's config from [scripts/models/glm4-9B.sh](https://github.com/radixark/miles/blob/main/scripts/models/glm4-9B.sh). These configs are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](https://github.com/radixark/miles/tree/main/scripts/models/). ⚠️ Ensure that settings such as `--rotary-base` in the model configuration file match the settings of the model you are currently training. This is because different models, even with the same architecture, might use different values. If needed, you can override these parameters in your script after loading the model weights. For instance: diff --git a/docs/en/examples/glm4.5-355B-A32B.md b/docs/en/examples/glm4.5-355B-A32B.md index 50b7c4921..b5336ee17 100644 --- a/docs/en/examples/glm4.5-355B-A32B.md +++ b/docs/en/examples/glm4.5-355B-A32B.md @@ -5,12 +5,12 @@ This is an example of doing GLM-4.5 RL training using 64xH100 GPUs. ## Environment Setup -For instructions on setting up the environment and downloading data, please refer to [Example: Qwen3-4B](./qwen3-4B.md). +For instructions on setting up the environment and downloading data, please refer to [Example: Qwen3-4B](qwen3-4B.md). First, you will need to download GLM-4.5 to a directory accessible by all machines (hereinafter referred to as `$BASE_DIR`): ```bash -huggingface-cli download zai-org/GLM-4.5 --local-dir $BASE_DIR/GLM-4.5-355B-A32B +hf download zai-org/GLM-4.5 --local-dir $BASE_DIR/GLM-4.5-355B-A32B ``` Next, we need to convert the huggingface checkpoint into the torch_dist format with 2 nodes, each with 8 GPUs: @@ -66,7 +66,7 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" source "${SCRIPT_DIR}/models/glm4.5-355B-A32B.sh" ``` -This reads the model's config from [scripts/models/glm4.5-355B-A32B.sh](../../../scripts/models/glm4.5-355B-A32B.sh). These configs are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](../../../scripts/models/). +This reads the model's config from [scripts/models/glm4.5-355B-A32B.sh](https://github.com/radixark/miles/blob/main/scripts/models/glm4.5-355B-A32B.sh). These configs are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](https://github.com/radixark/miles/tree/main/scripts/models/). #### PERF\_ARGS diff --git a/docs/en/examples/qwen3-30B-A3B.md b/docs/en/examples/qwen3-30B-A3B.md index 965ef7eb5..35be2773a 100644 --- a/docs/en/examples/qwen3-30B-A3B.md +++ b/docs/en/examples/qwen3-30B-A3B.md @@ -3,7 +3,7 @@ ## Environment Preparation -The environment setup, model download, data, and checkpoint conversion are the same as for the Qwen3-4B model. You can refer to [Example: Qwen3-4B Model](./qwen3-4B.md), replacing mentions of Qwen3-4B with Qwen3-30B-A3B. +The environment setup, model download, data, and checkpoint conversion are the same as for the Qwen3-4B model. You can refer to [Example: Qwen3-4B Model](qwen3-4B.md), replacing mentions of Qwen3-4B with Qwen3-30B-A3B. To convert huggingface checkpoint to torch_dist, please try: @@ -29,7 +29,7 @@ bash scripts/run-qwen3-30B-A3B.sh ### Parameter Introduction -Here, we will briefly introduce the MoE-related parts in the [run-qwen3-30B-A3B.sh](../../../scripts/run-qwen3-30B-A3B.sh) script. +Here, we will briefly introduce the MoE-related parts in the [run-qwen3-30B-A3B.sh](https://github.com/radixark/miles/blob/main/scripts/run-qwen3-30B-A3B.sh) script. 1. To support running Qwen3-30B-A3B in an 8xH800 environment, we need to enable Megatron's CPU Adam to save GPU memory. The corresponding configuration is: diff --git a/docs/en/examples/qwen3-4B.md b/docs/en/examples/qwen3-4B.md index 1966fd823..d66df38f3 100644 --- a/docs/en/examples/qwen3-4B.md +++ b/docs/en/examples/qwen3-4B.md @@ -49,7 +49,7 @@ bash scripts/run-qwen3-4B.sh ### Parameter Introduction -Here, we will briefly introduce the various components of the [run-qwen3-4B.sh](../../../scripts/run-qwen3-4B.sh) script: +Here, we will briefly introduce the various components of the [run-qwen3-4B.sh](https://github.com/radixark/miles/blob/main/scripts/run-qwen3-4B.sh) script: #### MODEL\_ARGS @@ -58,7 +58,7 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" source "${SCRIPT_DIR}/models/qwen3-4B.sh" ``` -This reads the model's configuration from [scripts/models/qwen3-4B.sh](../../../scripts/models/qwen3-4B.sh). These are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](../../../scripts/models/). +This reads the model's configuration from [scripts/models/qwen3-4B.sh](https://github.com/radixark/miles/blob/main/scripts/models/qwen3-4B.sh). These are all Megatron parameters. When training with Megatron, it cannot read the model config from the checkpoint, so we need to configure it ourselves. We provide some examples in [scripts/models](https://github.com/radixark/miles/tree/main/scripts/models/). ⚠️ Ensure that settings such as `--rotary-base` in the model configuration file match the settings of the model you are currently training. This is because different models, even with the same architecture, might use different values. If needed, you can override these parameters in your script after loading the model weights. For instance: diff --git a/docs/en/examples/qwen3-4b-base-openhermes.md b/docs/en/examples/qwen3-4b-base-openhermes.md index cab853008..a4b6237b6 100644 --- a/docs/en/examples/qwen3-4b-base-openhermes.md +++ b/docs/en/examples/qwen3-4b-base-openhermes.md @@ -3,7 +3,7 @@ ## Environment Preparation -First, we need to create a mirror environment and convert the `Qwen3-4B-Base` model by following the [Example: Qwen3-4B Model](./models/qwen3-4B.md). +First, we need to create a mirror environment and convert the `Qwen3-4B-Base` model by following the [Example: Qwen3-4B Model](qwen3-4B.md). After that, we will process the SFT data. Here, we use the classic [OpenHermes-2.5](https://huggingface.co/datasets/teknium/OpenHermes-2.5) as an example. First, we process the data into a format suitable for `miles` to load. You can use the following script to add a column that conforms to the OpenAI message format and save it to `/root/openhermes2_5.parquet`. @@ -50,7 +50,7 @@ bash script/run-qwen3-4B-base-sft.sh ### Parameter Introduction -You can compare [run-qwen3-4B-base-sft.sh](../../scripts/run-qwen3-4B.sh) with [run-qwen3-4B.sh](../../scripts/run-qwen3-4B.sh). You will find that besides changing the model from the instruct version to the base model, the main adjustments are as follows: +You can compare [run-qwen3-4B-base-sft.sh](https://github.com/radixark/miles/blob/main/scripts/run-qwen3-4B-base-sft.sh) with [run-qwen3-4B.sh](https://github.com/radixark/miles/blob/main/scripts/run-qwen3-4B.sh). You will find that besides changing the model from the instruct version to the base model, the main adjustments are as follows: 1. Removed `SGLANG_ARGS` and `GRPO_ARGS`. This is because it is not necessary to start SGLang or configure GRPO-related settings during the SFT process. diff --git a/docs/en/get_started/customization.md b/docs/en/get_started/customization.md new file mode 100644 index 000000000..341dac62d --- /dev/null +++ b/docs/en/get_started/customization.md @@ -0,0 +1,409 @@ +# Customization Guide + +miles provides extensive customization capabilities through function path arguments. These allow you to inject custom logic at various stages of the training and rollout pipeline without modifying the core codebase. + +## Overview of Customization Interfaces + +Below is a summary of all available customization interfaces and their purposes. + +| Interface Argument | Purpose | +| :--- | :--- | +| [`--rollout-function-path`](#1-rollout-function---rollout-function-path) | Override the entire rollout generation logic. | +| [`--custom-generate-function-path`](#2-custom-generate-function---custom-generate-function-path) | Override only the generation step (e.g., for RAG or tool use). | +| [`--custom-rm-path`](#3-reward-model---custom-rm-path) | Implement custom reward computation logic. | +| [`--dynamic-sampling-filter-path`](#4-dynamic-sampling-filter---dynamic-sampling-filter-path) | Filter samples during dynamic sampling (e.g., DAPO). | +| [`--buffer-filter-path`](#5-buffer-filter---buffer-filter-path) | Filter samples in the rollout buffer before training. | +| [`--rollout-sample-filter-path`](#6-rollout-sample-filter---rollout-sample-filter-path) | Determine if individual samples participate in loss calculation. | +| [`--rollout-all-samples-process-path`](#7-rollout-all-samples-process---rollout-all-samples-process-path) | Process all samples (including filtered ones) after rollout. | +| [`--rollout-data-postprocess-path`](#8-rollout-data-postprocess---rollout-data-postprocess-path) | Post-process rollout data after log probs are computed. | +| [`--custom-loss-function-path`](#9-custom-loss-function---custom-loss-function-path) | Implement custom training loss computation. | +| [`--custom-tis-function-path`](#10-custom-tisrs-function---custom-tis-function-path) | Implement custom importance sampling for off-policy correction. | +| [`--custom-pg-loss-reducer-function-path`](#11-custom-pg-loss-reducer---custom-pg-loss-reducer-function-path) | Customize pg_loss reduction (e.g., for Dr.GRPO). | +| [`--custom-reward-post-process-path`](#12-reward-post-processing---custom-reward-post-process-path) | Custom post-processing of rewards before advantage computation. | +| [`--custom-convert-samples-to-train-data-path`](#13-samples-to-train-data-conversion---custom-convert-samples-to-train-data-path) | Override the conversion of samples to training data format. | +| [`--custom-rollout-log-function-path`](#14-logging-functions) | Custom logging for training rollouts. | +| [`--custom-eval-rollout-log-function-path`](#14-logging-functions) | Custom logging for evaluation rollouts. | +| [`--data-source-path`](#15-data-source---data-source-path) | Override the data source for rollout prompts. | +| [`--eval-function-path`](#16-evaluation-function---eval-function-path) | Override the rollout function specifically for evaluation. | +| [`--custom-megatron-init-path`](#17-megatron-hooks) | Custom initialization after Megatron setup. | +| [`--custom-megatron-before-log-prob-hook-path`](#17-megatron-hooks) | Custom logic before log probability computation. | +| [`--custom-megatron-before-train-step-hook-path`](#17-megatron-hooks) | Custom logic before each training step. | +| [`--miles-router-middleware-paths`](#18-miles-router-middleware---miles-router-middleware-paths) | Add custom middleware to miles router. | + +## Detailed Interface Reference + +### 1. Rollout Function (`--rollout-function-path`) + +**Default**: `miles.rollout.sglang_rollout.generate_rollout` + +**Purpose**: Override the entire rollout generation logic. + +**Signature**: +```python +async def generate_rollout(args, rollout_id, *, evaluation=False) -> RolloutFnTrainOutput | RolloutFnEvalOutput +``` + +**Use Cases**: +- Implementing complex multi-turn conversations +- Adding custom sampling strategies +- Integrating external tools or APIs during generation + +**Example**: See [examples/multi_agent/rollout_with_multi_agents.py](../../../examples/multi_agent/rollout_with_multi_agents.py) + +--- + +### 2. Custom Generate Function (`--custom-generate-function-path`) + +**Default**: `None` (uses built-in generate function) + +**Purpose**: Override only the generation step within the default rollout function. + +**Signature**: +```python +async def custom_generate(args, sample: Sample, sampling_params: dict) -> Sample +``` + +**Use Cases**: +- Implementing tool-calling or function-calling capabilities +- Adding retrieval-augmented generation (RAG) +- Multi-turn conversation handling + +**Example**: See [examples/search-r1/generate_with_search.py](../../../examples/search-r1/generate_with_search.py) + +--- + +### 3. Reward Model (`--custom-rm-path`) + +**Default**: `None` (uses built-in reward models based on `--rm-type`) + +**Purpose**: Implement custom reward computation logic. + +**Signature** (single sample mode): +```python +async def custom_rm(args, sample: Sample) -> float +``` + +**Signature** (batch mode, when `--group-rm` is enabled): +```python +async def batched_custom_rm(args, samples: list[Sample]) -> list[float] +``` + +**Use Cases**: +- Custom rule-based rewards +- Integration with external reward model services +- Multi-dimensional reward signals + +**Built-in Options** (`--rm-type`): +- `math`: Mathematical answer verification +- `dapo`: DAPO-style scoring +- `deepscaler`: DeepScaler rule-based reward +- `f1`: F1 score computation +- `gpqa`: GPQA reward computation +- `ifbench`: IFBench reward computation +- `remote_rm`: Remote reward model service (requires `--rm-url`) + +--- + +### 4. Dynamic Sampling Filter (`--dynamic-sampling-filter-path`) + +**Default**: `None` + +**Purpose**: Filter samples during dynamic sampling (e.g., DAPO-style filtering). + +**Signature**: +```python +def filter_function(args, samples: list[Sample], **kwargs) -> DynamicFilterOutput +``` + +**Return Type**: +```python +@dataclass +class DynamicFilterOutput: + keep: bool # Whether to keep this sample group + reason: str | None # Reason for filtering (for logging) +``` + +**Use Cases**: +- Filtering out samples where all responses have the same reward +- Implementing curriculum learning strategies +- Quality-based sample selection + +**Example**: `miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std` + +--- + +### 5. Buffer Filter (`--buffer-filter-path`) + +**Default**: `None` + +**Purpose**: Filter samples in the rollout buffer before training. + +**Signature**: +```python +def buffer_filter(samples: list[list[Sample]]) -> list[list[Sample]] +``` + +**Use Cases**: +- Removing low-quality samples before training +- Implementing priority-based sample selection +- Balancing sample distributions + +--- + +### 6. Rollout Sample Filter (`--rollout-sample-filter-path`) + +**Default**: `None` + +**Purpose**: Determine whether individual samples participate in loss calculation. + +**Signature**: +```python +def filter_function(args, samples: list[Sample]) -> None +``` + +**Note**: This function should directly modify the `remove_sample` attribute of each `Sample` object. + +**Use Cases**: +- Filtering samples based on response quality +- Implementing selective training strategies + +--- + +### 7. Rollout All Samples Process (`--rollout-all-samples-process-path`) + +**Default**: `None` + +**Purpose**: Process all samples (including filtered ones) after rollout. + +**Signature**: +```python +def process_function(args, samples: list[list[Sample]]) -> None +``` + +**Use Cases**: +- Logging and analysis of all generated samples +- Computing statistics across filtered and kept samples + +--- + +### 8. Rollout Data Postprocess (`--rollout-data-postprocess-path`) + +**Default**: `None` + +**Purpose**: Post-process rollout data after log probabilities are computed. + +**Signature**: +```python +def postprocess_function(args, samples: list[list[Sample]]) -> None +``` + +**Use Cases**: +- Updating loss masks based on computed values +- Adding additional metadata to samples + +--- + +### 9. Custom Loss Function (`--custom-loss-function-path`) + +**Default**: `None` (requires `--loss-type custom_loss`) + +**Purpose**: Implement custom training loss computation. + +**Use Cases**: +- Novel RL objectives +- Multi-objective optimization +- Custom regularization terms + +--- + +### 10. Custom TIS/RS Function (`--custom-tis-function-path`) + +**Default**: `None` + +**Purpose**: Implement custom importance sampling for off-policy correction. + +**Use Cases**: +- Custom importance sampling ratio computation +- Advanced off-policy correction methods + +**Example**: `examples/train_infer_mismatch_helper/mis.py:compute_mis_weights_with_cp` + +--- + +### 11. Custom pg_loss Reducer (`--custom-pg-loss-reducer-function-path`) + +**Default**: `None` + +**Purpose**: Customize the reduction of pg_loss while other metrics (pg_clipfrac, ppo_kl, entropy_loss, etc.) still use the default sum_of_sample_mean. + +**Signature**: +```python +def get_pg_loss_reducer( + total_lengths: list[int], + response_lengths: list[int], + loss_masks: list[torch.Tensor], + calculate_per_token_loss: bool = False, +) -> Callable[[torch.Tensor], torch.Tensor] +``` + +**Use Cases**: +- Dr.GRPO: Divide by a constant instead of effective token count +- Custom loss normalization strategies + +**Example**: `examples/DrGRPO/custom_reducer.py:get_pg_loss_reducer` + +--- + +### 12. Reward Post-Processing (`--custom-reward-post-process-path`) + +**Default**: `None` (uses default GRPO normalization) + +**Purpose**: Custom post-processing of rewards before advantage computation. + +**Use Cases**: +- Custom reward normalization strategies +- Reward shaping + +--- + +### 13. Samples to Train Data Conversion (`--custom-convert-samples-to-train-data-path`) + +**Default**: `None` (uses built-in conversion logic) + +**Purpose**: Override the conversion of samples to training data format. + +**Signature**: +```python +def convert_samples_to_train_data( + args, + samples: list[Sample] | list[list[Sample]], +) -> dict +``` + +**Return Type**: +```python +dict: { + "tokens": list[list[int]], # Token IDs for each sample + "response_lengths": list[int], # Response lengths + "rewards": list[float], # Normalized rewards + "raw_reward": list[float], # Raw rewards + "truncated": list[int], # Truncation flags (0 or 1) + "sample_indices": list[int], # Sample indices + "loss_masks": list[list[int]], # Loss masks for each sample + # Optional fields: + "round_number": list[int], # Round numbers (for rollout buffer) + "rollout_log_probs": list, # Log probs (for off-policy correction) + "rollout_routed_experts": list, # Routed experts (for MoE) + "metadata": list, # Train metadata + "multimodal_train_inputs": list, # Multimodal tensors (for VLM) + "teacher_log_probs": list, # Teacher log probs (for distillation) +} +``` + +**Use Cases**: +- Handling `list[list[Sample]]` inputs +- Custom data format requirements for training + +--- + +### 14. Logging Functions + +#### Training Rollout Logging (`--custom-rollout-log-function-path`) + +**Signature**: +```python +def log_rollout_data(rollout_id, args, samples, rollout_extra_metrics, rollout_time) -> bool +``` + +**Return**: `True` to skip default logging, `False` to continue with default logging. + +#### Evaluation Rollout Logging (`--custom-eval-rollout-log-function-path`) + +**Signature**: +```python +def log_eval_rollout_data(rollout_id, args, data, extra_metrics) -> bool +``` + +**Return**: `True` to skip default logging, `False` to continue with default logging. + +--- + +### 15. Data Source (`--data-source-path`) + +**Default**: `miles.rollout.data_source.RolloutDataSourceWithBuffer` + +**Purpose**: Override the data source for rollout prompts. + +**Base Class**: `miles.rollout.data_source.DataSource` + +**Required Methods**: +```python +class CustomDataSource(DataSource): + def get_samples(self, num_samples: int) -> list[list[Sample]]: + """Return num_samples samples""" + + def add_samples(self, samples: list[list[Sample]]): + """Add samples back to the data source""" + + def save(self, rollout_id): + """Save state for checkpointing""" + + def load(self, rollout_id=None): + """Load state from checkpoint""" +``` + +--- + +### 16. Evaluation Function (`--eval-function-path`) + +**Default**: Same as `--rollout-function-path` + +**Purpose**: Override the rollout function specifically for evaluation. + +**Use Cases**: +- Different sampling parameters for evaluation +- Evaluation-specific logic + +--- + +### 17. Megatron Hooks + +#### Megatron Initialization (`--custom-megatron-init-path`) + +**Signature**: +```python +def custom_init(args) -> None +``` + +**Purpose**: Custom initialization after Megatron setup. + +#### Before Log Prob Hook (`--custom-megatron-before-log-prob-hook-path`) + +**Signature**: +```python +def custom_hook(args, model, store_prefix) -> None +``` + +**Purpose**: Custom logic before log probability computation. + +#### Before Train Step Hook (`--custom-megatron-before-train-step-hook-path`) + +**Signature**: +```python +def custom_hook(args, rollout_id, step_id, model, optimizer, opt_param_scheduler) -> None +``` + +**Purpose**: Custom logic before each training step. + +--- + +### 18. miles Router Middleware (`--miles-router-middleware-paths`) + +**Purpose**: Add custom middleware to the miles router for request processing. + +**Use Cases**: +- Request/response transformation +- Custom routing logic +- Caching and optimization + + diff --git a/docs/en/get_started/qa.md b/docs/en/get_started/qa.md index b3163f215..2552b5859 100644 --- a/docs/en/get_started/qa.md +++ b/docs/en/get_started/qa.md @@ -49,7 +49,7 @@ 9. **My gradient norm is very high and the training crashes. What should I do?** - First, ensure that your data and model are compatible. For example, if your data already uses a chat template, check if this template matches the one used by the original model. If the data is correct, please refer to our [Debug Guide](./debug.md) for a more in-depth analysis. + First, ensure that your data and model are compatible. For example, if your data already uses a chat template, check if this template matches the one used by the original model. If the data is correct, please refer to our [Debug Guide](../developer_guide/debug.md) for a more in-depth analysis. 10. **My sglang generation takes an extremely long time, GPU power is maxed out, and there's no output for a long while. Why?** diff --git a/docs/en/get_started/quick_start.md b/docs/en/get_started/quick_start.md index db07ab705..d00fbb67d 100644 --- a/docs/en/get_started/quick_start.md +++ b/docs/en/get_started/quick_start.md @@ -105,6 +105,14 @@ PYTHONPATH=/root/Megatron-LM python tools/convert_torch_dist_to_hf.py \ Note that as Megatron will do padding to embedding for better performance, it may happen that the converted embedding is not correct. In that case, please manually set `--vocab-size` during convertion. +For FSDP checkpoints (without `common.pt`), use the dedicated conversion script. Point `--input-dir` to the checkpoint directory (e.g. `iter_xxx` or `iter_xxx/model`) and provide the original Hugging Face directory: + +```bash +python tools/convert_fsdp_to_hf.py \ + --input-dir /path/to/fsdp_ckpt/iter_xxx \ + --output-dir /root/fsdp-converted \ + --origin-hf-dir /root/GLM-Z1-9B-0414 +``` ## Training Script and Parameter Overview diff --git a/docs/en/get_started/usage.md b/docs/en/get_started/usage.md index 0c6d8b098..dd756cf35 100644 --- a/docs/en/get_started/usage.md +++ b/docs/en/get_started/usage.md @@ -6,7 +6,7 @@ When using miles, parameters are primarily passed for the following purposes: 1. To allocate a portion of the GPUs in the cluster for training and another portion for inference. -2. To load Megatron for the training portion. +2. To load Megatron or FSDP for the training portion. 3. To load SGLang for the inference portion. 4. To configure the hyperparameters required for RL training. @@ -28,6 +28,15 @@ For co-located training and inference, you also need to configure: - `--colocate`: Enables co-located training and inference. When enabled, it ignores `--rollout-num-gpus` and makes the number of GPUs for training and inference equal. +Additionally, miles supports Prefill and Decode disaggregation (PD Disaggregation). You can set the number of servers used for Prefill by setting the `--prefill-num-servers` argument. + +### Choosing Training Backend + +miles supports multiple training backends, which can be selected via the `--train-backend` parameter: + +- `megatron` (default): Uses Megatron-LM as the training backend, supporting efficient training of large-scale models. +- `fsdp`: Uses PyTorch FSDP as the training backend, allowing direct loading of HuggingFace format weights without conversion. + ### Loading Megatron Unlike tools such as SGLang, vLLM, or Hugging Face Trainer, Megatron cannot directly read Hugging Face checkpoints. Instead, the user must configure the parameters for the model to be trained and load Megatron's own checkpoint format. @@ -67,7 +76,7 @@ MODEL_ARGS=( ) ``` -We provide configurations for common models in [scripts/models](../../scripts/models), which you can reuse directly. If you are also using Megatron for pre-training/SFT, you can directly reuse the model configurations from your pre-training/SFT setup. +We provide configurations for common models in [scripts/models](../../../scripts/models), which you can reuse directly. If you are also using Megatron for pre-training/SFT, you can directly reuse the model configurations from your pre-training/SFT setup. Note: @@ -99,7 +108,7 @@ Megatron supports several of its custom checkpoint formats. Here are two of the The `torch` format is Megatron's older storage format. Its structure consists of directories like `mp_rank_xxx`, where each directory corresponds to the checkpoint stored by each rank under a specific parallel partitioning. Because of this, when loading a `torch` format checkpoint, you must ensure that the checkpoint's parallelism strategy matches that of the training task. -We recommend using the `torch_dist` format because it supports automatic parallel sharding, meaning that training tasks with different parallelism settings can share the same checkpoint, which is much more convenient. `torch_dist` is also the default format in the open-source Megatron. A `torch_dist` format checkpoint typically contains a set of `.distcp` files. When using `torch_dist`, you can convert from Hugging Face to `torch_dist` and vice versa using the checkpoint conversion method described in the [README](../../README.md). +We recommend using the `torch_dist` format because it supports automatic parallel sharding, meaning that training tasks with different parallelism settings can share the same checkpoint, which is much more convenient. `torch_dist` is also the default format in the open-source Megatron. A `torch_dist` format checkpoint typically contains a set of `.distcp` files. When using `torch_dist`, you can convert from Hugging Face to `torch_dist` and vice versa using the checkpoint conversion method described in the [README](../../../README.md). In terms of storage structure, a Megatron checkpoint typically looks like this, assuming the storage path is `/ckpt/`: @@ -138,6 +147,7 @@ Note: - Before the first training step, miles will synchronize the parameters from Megatron to SGLang. Therefore, the `--hf-checkpoint` does not need to contain the latest training parameters, and you do not need to change the HF checkpoint when resuming training. - By default, SGLang reads the maximum context length from the `config.json` in the Hugging Face checkpoint. You can use the `--sglang-context-length` parameter to override this value to support longer inference. - During co-located training and inference, although Megatron and SGLang will offload sequentially, they still need to leave some memory for each other. You need to adjust SGLang's total VRAM usage by reducing `--sglang-mem-fraction-static`. + - miles supports passing through sgl-router parameters by adding a `router` prefix to the original parameter name. For example, sgl-router's `--balance-abs-threshold` parameter should be set as `--router-balance-abs-threshold`. Since sgl-router uses cache-aware routing by default, it may cause uneven request distribution. You can set `--router-balance-abs-threshold 0` to force balanced distribution, but this may affect prefix cache hit rate in multi-turn conversation scenarios. For details on some of SGLang's customizations and the principles behind how miles incorporates SGLang, please see the "How to Use SGLang" section. @@ -176,14 +186,16 @@ Additionally, we provide a `metadata_key`, which defaults to `"metadata"`. When - `gspo` ([https://arxiv.org/abs/2507.18071](https://arxiv.org/abs/2507.18071)) - `reinforce_plus_plus` and `reinforce_plus_plus_baseline` ([https://arxiv.org/abs/2501.03262](https://arxiv.org/abs/2501.03262)) - `ppo` ([https://arxiv.org/abs/1707.06347](https://arxiv.org/abs/1707.06347)) + - `on_policy_distillation` - `--calculate-per-token-loss`: By default, Miles calculates loss on a per-sample basis, i.e., `mean(sum(sample_i) / len(sample_i))`. Enable this flag to calculate loss on a per-token basis, i.e., `sum(sum(sample_i)) / sum(len(sample_i))`. - `--use-tis`: Enable this setting to use TIS (Truncated Importance Sampling) (https://fengyao.notion.site/off-policy-rl). +- `--true-on-policy-mode`: Enable True On-Policy mode, which strictly ensures that data is generated by the current policy during training. ## Custom Rollout Function miles supports customizing data generation (rollout) to various degrees. - - By default, it uses the `generate_rollout` function from [miles/rollout/sglang\_example.py](../../miles/rollout/sglang_rollout.py) for data generation. This file implements an asynchronous (asyncio) data generation flow based on SGLang and supports features like dynamic sampling and partial rollout. + - By default, it uses the `generate_rollout` function from [miles/rollout/sglang_rollout.py](https://github.com/radixark/miles/blob/main/miles/rollout/sglang_rollout.py) for data generation. This file implements an asynchronous (asyncio) data generation flow based on SGLang and supports features like dynamic sampling and partial rollout. - You can completely replace the `generate_rollout` in sglang\_example.py by using the `--rollout-function-path` parameter. You just need to ensure that the function signature passed via `--rollout-function-path` is as follows: @@ -213,7 +225,7 @@ miles supports customizing data generation (rollout) to various degrees. - `evaluation`: A boolean indicating if the rollout is for evaluation. You can configure a separate evaluation function using `--eval-function-path`. - - The returned `Sample` type is defined in [miles/utils/types.py](../../miles/utils/types.py). When implementing, you need to ensure the following fields are correctly set: + - The returned `Sample` type is defined in [miles/utils/types.py](https://github.com/radixark/miles/blob/main/miles/utils/types.py). When implementing, you need to ensure the following fields are correctly set: - `tokens`: The tokens for the prompt + response. - `response_length`: The total length of the response. For multi-turn tasks, this is the length of the tokens remaining after the first-turn prompt. @@ -254,7 +266,7 @@ miles supports customizing data generation (rollout) to various degrees. return sample ``` - For a more complete version, please refer to [miles/rollout/sglang\_example.py](../../miles/rollout/sglang_rollout.py). + For a more complete version, please refer to [miles/rollout/sglang_rollout.py](https://github.com/radixark/miles/blob/main/miles/rollout/sglang_rollout.py). - Sometimes, you may also need to support a custom reward model. This can be configured by setting `--custom-rm-path`. @@ -275,7 +287,7 @@ Some parameters related to miles's resource scheduling are configured by miles i - `--tp-size` in miles is set using `--rollout-num-gpus-per-engine`. - `--model-path` in miles is set using `--hf-checkpoint`. -The way SGLang parameters are integrated into miles can be found in [miles/backends/sglang\_utils/arguments.py](../../miles/backends/sglang_utils/arguments.py). +The way SGLang parameters are integrated into miles can be found in [miles/backends/sglang_utils/arguments.py](https://github.com/radixark/miles/blob/main/miles/backends/sglang_utils/arguments.py). ### How to Use the Router @@ -291,7 +303,7 @@ miles supports different and lightly modified versions of Megatron by reusing co ### Parameter Configuration -miles directly imports all parameters of the Megatron in the current environment by using `from megatron.training.arguments import parse_args`. If the version of Megatron you are using has parameters defined outside of `parse_args`, you can configure them by passing them in, similar to how it's done in [train.py](../../train.py), for example: +miles directly imports all parameters of the Megatron in the current environment by using `from megatron.training.arguments import parse_args`. If the version of Megatron you are using has parameters defined outside of `parse_args`, you can configure them by passing them in, similar to how it's done in [train.py](https://github.com/radixark/miles/blob/main/train.py), for example: ```python if __name__ == "__main__": @@ -309,4 +321,64 @@ In some customized Megatron implementations, special operations need to be perfo - `--custom-megatron-init-path`: Adds some initialization calls. - `--custom-megatron-before-log-prob-hook-path`: Is called before calculating the log probability. - - `--custom-megatron-before-train-step-hook-path`: Is called before each training step. You could use this to mix in special training losses, for example. \ No newline at end of file + - `--custom-megatron-before-train-step-hook-path`: Is called before each training step. You could use this to mix in special training losses, for example. + +## How to Use FSDP + +miles also support FSDP2 as the training backend, docs [here](https://lmsys.org/blog/2025-12-03-miles-fsdp/). + +> FSDP automatically reads all architecture information via `AutoModelForCausalLM.from_pretrained()`, without manual specification. Megatron requires manual configuration of parameters to read model architecture information. FSDP can read entirely from `config.json`, directly avoiding the weight format conversion step. + +To run FSDP as the training backend, pass `--train-backend fsdp` to enable. + +### Parameters + +Parameters that FSDP used are shown as below in comparison to Megatron, more supports are coming on the way. + +| Configuration Category | Megatron Parameter | FSDP Parameter | Description | +| --- | --- | --- | --- | +| **Model Loading** | `--load` (Megatron checkpoint) + architecture args (`--num-layers`, `--hidden-size` etc.) | `--hf-checkpoint` (Required) | **FSDP**: Directly uses HuggingFace format, no weight conversion needed, architecture inferred via `AutoConfig` | +| **Tensor Parallel** | `--tensor-model-parallel-size` | Coming Soon | | +| **Pipeline Parallel** | `--pipeline-model-parallel-size` | Coming Soon | | +| **Expert Parallel** | `--expert-model-parallel-size` | Coming Soon | | +| **Context Parallel** | `--context-parallel-size` | `--context-parallel-size` | Both support CP | +| **Initial Learning Rate** | `--lr` | `--lr` | Same parameter | +| **Learning Rate Decay** | `--lr-decay-style` (linear/cosine etc.) | `--lr-decay-style` | Same parameter | +| **Warmup** | `--lr-warmup-iters` (steps) | `--lr-warmup-iters` | Same parameter | +| **Min Learning Rate** | `--min-lr` | `--min-lr` | Same parameter | +| **Optimizer Type** | `--optimizer` (adam/sgd etc.) | `--optimizer` (default adam) | Basically same | +| **Distributed Optimizer** | `--use-distributed-optimizer` | Built-in to FSDP | FSDP uses distributed optimizer by default | +| **Gradient Checkpoint** | `--recompute-granularity`, `--recompute-method` | `--gradient-checkpointing` | **FSDP**: Simplified to boolean switch | +| **CPU Offload** | Implemented via distributed optimizer | `--fsdp-cpu-offload` | **FSDP**: Offload parameters/gradients/optimizer states to CPU | +| **CPU Backend** | Implemented via distributed optimizer | `--fsdp-cpu-backend` | **FSDP**: Specify CPU backend and use hybrid backend when CPU offload is enabled | +| **Attention Backend** | Decided by Megatron Core | `--attn-implementation` (flash_attention_2/sdpa/eager) | **FSDP**: Directly passed to HuggingFace | +| **Mixed Precision** | `--fp16` or `--bf16` | `--fp16` (bf16 inferred automatically) | Basically same | +| **Training Backend** | Default or `--train-backend megatron` | `--train-backend fsdp` (Required) | Used to switch backend | +| **Config** | | `--config` | **FSDP**: Set additional parameters for FSDP backend | + +### Quick Start + +```bash +# If you need to use WANDB, you need to set the environment variable WANDB_API_KEY in advance +# Download model weights (Qwen3-4B) +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B + +# Download training dataset (dapo-math-17k) +hf download --repo-type dataset zhuzilin/dapo-math-17k \ + --local-dir /root/dapo-math-17k + +# Download evaluation dataset (aime-2024) +hf download --repo-type dataset zhuzilin/aime-2024 \ + --local-dir /root/aime-2024 + +# Clone code and install dependencies +git clone https://github.com/radixark/miles.git +cd miles +pip install -e . + + +# FSDP does not require weight conversion, natively supports huggingface format +# Enable reference model, train Qwen3-4B in colocate mode +source /root/miles/scripts/run-qwen3-4B-fsdp.sh +``` + diff --git a/docs/en/index.rst b/docs/en/index.rst index be1427733..afafc6796 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -6,7 +6,7 @@ miles is an LLM post-training framework for RL scaling, providing two core capab - High-Performance Training: Supports efficient training in various modes by connecting Megatron with SGLang; - Flexible Data Generation: Enables arbitrary training data generation workflows through custom data generation interfaces and server-based engines. -miles is the RL-framework behind GLM-4.5 and GLM-4.6. Apart from models from Z.ai, we also supports the following models: +miles is the RL-framework behind GLM-4.7, GLM-4.6 and GLM-4.5. Apart from models from Z.ai, we also supports the following models: - Qwen3 series (Qwen3Next, Qwen3MoE, Qwen3), Qwen2.5 series; - DeepSeek V3 series (DeepSeek V3, V3.1, DeepSeek R1); @@ -18,6 +18,7 @@ miles is the RL-framework behind GLM-4.5 and GLM-4.6. Apart from models from Z.a get_started/quick_start.md get_started/usage.md + get_started/customization.md get_started/qa.md .. toctree:: @@ -43,6 +44,7 @@ miles is the RL-framework behind GLM-4.5 and GLM-4.6. Apart from models from Z.a advanced/speculative-decoding.md advanced/fault-tolerance.md advanced/arch-support-beyond-megatron.md + advanced/pd-disaggregation.md .. toctree:: :maxdepth: 1 @@ -52,7 +54,8 @@ miles is the RL-framework behind GLM-4.5 and GLM-4.6. Apart from models from Z.a _examples_synced/search-r1/README.md _examples_synced/fully_async/README.md _examples_synced/retool/README.md - _examples_synced/multi_agent/README.md + _examples_synced/multi_agent/README.md + _examples_synced/on_policy_distillation/README.md .. toctree:: :maxdepth: 1 diff --git a/docs/en/platform_support/amd_tutorial.md b/docs/en/platform_support/amd_tutorial.md index 790aede5d..53df91b09 100644 --- a/docs/en/platform_support/amd_tutorial.md +++ b/docs/en/platform_support/amd_tutorial.md @@ -50,13 +50,27 @@ docker run --rm -it \ /bin/bash ``` -Then, download and install miles. +Then, download and install miles: ```bash git clone https://github.com/radixark/miles.git cd miles pip install -e . ``` +Download the model and data: + +```bash +# hf checkpoint +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B + +# train data +hf download --repo-type dataset zhuzilin/dapo-math-17k \ + --local-dir /root/dapo-math-17k + +# eval data +hf download --repo-type dataset zhuzilin/aime-2024 \ + --local-dir /root/aime-2024 +``` ### Checkpoint Format Conversion @@ -73,8 +87,8 @@ MEGATRON_LM_PATH=$(pip list | grep megatron-core | awk '{print $NF}') PYTHONPATH=${MEGATRON_LM_PATH} python tools/convert_hf_to_torch_dist.py \ ${MODEL_ARGS[@]} \ --no-gradient-accumulation-fusion \ - --hf-checkpoint model/Qwen3-4B \ - --save model/Qwen3-4B_torch_dist + --hf-checkpoint /root/Qwen3-4B \ + --save /root/Qwen3-4B_torch_dist ``` Note: We implemented a dedicated AMD conversion script that forces a CPU-only conversion workflow using the Gloo backend to bypass hardware-specific issues. A GPU-based script for ROCm is currently in development. @@ -85,7 +99,14 @@ Note: We implemented a dedicated AMD conversion script that forces a CPU-only co ### Example: Qwen3-4B We provide examples to use [Qwen3-4B](https://huggingface.co/Qwen/Qwen3-4B), please refer to: -- [Example: Qwen3-4B Model](../../../scripts/run-qwen3-4B-amd.sh): Just run `scripts/run-qwen3-4B-amd.sh` +- [Example: Qwen3-4B Model](https://github.com/radixark/miles/blob/main/scripts/run-qwen3-4B-amd.sh): Just run + +```bash +MILES_DIR=/root \ +MODEL_DIR=/root \ +DATA_DIR=/root \ +bash scripts/run-qwen3-4B-amd.sh +``` ⚠️ TODO: ROCM seems to not support `apex` yet. Thus, we need to disable gradient accumulation fusionby adding the `--no-gradient-accumulation-fusion` flag in the training script currently. We will continue investigating how to enable this. diff --git a/examples/DrGRPO/README.md b/examples/DrGRPO/README.md new file mode 100644 index 000000000..56bd39848 --- /dev/null +++ b/examples/DrGRPO/README.md @@ -0,0 +1,50 @@ +# Dr.GRPO Custom Reducer + +This example demonstrates how to use a custom reducer function for Dr.GRPO algorithm. + +## Overview + +By default, miles divides the policy gradient loss by the number of effective tokens in each sample. This custom implementation allows you to divide by a constant value (default: 1000) instead. + +## Usage + +Use `--custom-pg-loss-reducer-function-path` to apply the custom reducer **only to pg_loss**, while other metrics (pg_clipfrac, ppo_kl, entropy_loss, etc.) still use the default sum_of_sample_mean: + +```bash +--custom-pg-loss-reducer-function-path examples.Dr.GRPO.custom_reducer.get_pg_loss_reducer +``` + +## Customization + +You can modify the `DIVISOR` constant in `custom_reducer.py` to use a different value: + +```python +# In custom_reducer.py +DIVISOR = 1000.0 # Change this to your desired constant +``` + +## How It Works + +The custom function has the same signature as the default `get_sum_of_sample_mean`: + +```python +def get_pg_loss_reducer( + total_lengths: list[int], + response_lengths: list[int], + loss_masks: list[torch.Tensor], + calculate_per_token_loss: bool = False, +) -> Callable[[torch.Tensor], torch.Tensor]: +``` + +Instead of dividing by `loss_mask_i.sum()` (the number of effective tokens), it divides by the constant `DIVISOR`. + +## Example + +```bash +GRPO_ARGS=( + --advantage-estimator grpo + --custom-pg-loss-reducer-function-path examples.Dr.GRPO.custom_reducer:get_pg_loss_reducer + # ... other arguments +) +``` + diff --git a/examples/DrGRPO/custom_reducer.py b/examples/DrGRPO/custom_reducer.py new file mode 100644 index 000000000..565125a38 --- /dev/null +++ b/examples/DrGRPO/custom_reducer.py @@ -0,0 +1,67 @@ +"""Custom pg_loss reducer for Dr.GRPO. + +This module provides a custom reducer that divides by a constant instead of +the number of effective tokens. This is useful for Dr.GRPO algorithm. + +Usage: + --custom-pg-loss-reducer-function-path examples.Dr.GRPO.custom_reducer:get_pg_loss_reducer +""" + +from collections.abc import Callable + +import torch +from megatron.core import mpu + +# Constant divisor instead of effective token count +DIVISOR = 1000.0 + + +def get_pg_loss_reducer( + total_lengths: list[int], + response_lengths: list[int], + loss_masks: list[torch.Tensor], + calculate_per_token_loss: bool = False, +) -> Callable[[torch.Tensor], torch.Tensor]: + """ + Custom reducer for pg_loss only. Divides by a constant (DIVISOR) + instead of the number of effective tokens. + + This function is designed to be used with --custom-pg-loss-reducer-function-path + so that only pg_loss uses this custom reducer, while other metrics + (pg_clipfrac, ppo_kl, entropy_loss, etc.) still use the default sum_of_sample_mean. + + Note: This implementation only supports cp_size == 1 (no context parallelism). + + Args: + total_lengths: List of total sequence lengths (prompt + response). Unused but kept for API compatibility. + response_lengths: List of response lengths. + loss_masks: List of loss masks for each sample. + calculate_per_token_loss: If True, return sum_of_token (no division). + If False, return sum_of_sample_mean with constant divisor. + + Returns: + A callable function that takes a tensor and returns a scalar tensor. + """ + assert mpu.get_context_parallel_world_size() == 1, "This custom reducer only supports cp_size == 1" + + if calculate_per_token_loss: + + def sum_of_token(x: torch.Tensor) -> torch.Tensor: + return sum( + [ + (x_i * loss_mask_i).sum() + for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False) + ] + ) + + return sum_of_token + + def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor: + return sum( + [ + (x_i * loss_mask_i).sum() / DIVISOR + for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False) + ] + ) + + return sum_of_sample_mean diff --git a/examples/geo3k_vlm/README.md b/examples/geo3k_vlm/README.md index 1946999dd..6ef751228 100644 --- a/examples/geo3k_vlm/README.md +++ b/examples/geo3k_vlm/README.md @@ -1,19 +1,84 @@ -# FSDP + VLM Single-Turn RL +# VLM Single-Turn RL (FSDP & Megatron) -Training VLMs with FSDP on single-turn reasoning task using GRPO on the [GEO3K dataset](https://huggingface.co/datasets/hiyouga/geometry3k). We used processed version [here](https://huggingface.co/datasets/chenhegu/geo3k_imgurl). +Training VLMs with FSDP or Megatron on single-turn reasoning task using GRPO on the [GEO3K dataset](https://huggingface.co/datasets/hiyouga/geometry3k). We used processed version [here](https://huggingface.co/datasets/chenhegu/geo3k_imgurl).

- Reward Plot + FSDP vs Megatron Reward Plot

+## Data Preparation (For SFT Training) + +The [geo3k_imgurl](https://huggingface.co/datasets/chenhegu/geo3k_imgurl) dataset contains: +- `problem`: The math problem text (string) +- `answer`: The answer (string, e.g., "270") +- `images`: Image data (list) + +For SFT training, we need to format the `answer` field for `\boxed{}` format and the messages. You can use the following script to format the answer field: + +```python +from datasets import load_dataset +import pandas as pd + +ds = load_dataset("chenhegu/geo3k_imgurl", split="train") + +def format_answer(answer: str) -> str: + """Format answer to include \\boxed{} format.""" + return f"Answer: \\boxed{{{answer}}}" + +def process_sample(sample): + formatted_answer = f"Answer: \\boxed{{{sample['answer']}}}" + + sample["messages"] = [ + {"role": "user", "content": sample["problem"]}, + {"role": "assistant", "content": formatted_answer} + ] + return sample + +ds = ds.map(process_sample) +ds.to_parquet("/root/datasets/geo3k_imgurl/train_formatted.parquet") +``` + ## Reproduce ```bash export WANDB_API_KEY=your_wandb_api_key -MILES_SCRIPT_MODEL_NAME=Qwen3-VL-2B-Instruct MILES_SCRIPT_NUM_GPUS=8 python examples/geo3k_vlm/run_geo3k_vlm.py 2>&1 | tee run_simple.log +# Megatron backend (default -> Qwen3-VL-2B-Instruct + Megatron) +./examples/geo3k_vlm/run_geo3k_vlm.sh + +# FSDP backend +MILES_SCRIPT_TRAIN_BACKEND=fsdp ./examples/geo3k_vlm/run_geo3k_vlm.sh + +# With different model +MILES_SCRIPT_MODEL_NAME=Qwen3-VL-4B-Instruct ./examples/geo3k_vlm/run_geo3k_vlm.sh + +# SFT +./examples/geo_3k_vlm/run_geo3k_vlm_sft.sh ``` +### Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `MILES_SCRIPT_TRAIN_BACKEND` | `megatron` | Training backend (`megatron` or `fsdp`) | +| `MILES_SCRIPT_MODEL_NAME` | `Qwen3-VL-8B-Instruct` | Model name | +| `MILES_SCRIPT_DATASET_NAME` | `chenhegu/geo3k_imgurl` | HuggingFace dataset name | +| `MILES_SCRIPT_NUM_GPUS` | `8` | Number of GPUs | +| `MILES_SCRIPT_EXTERNAL_RAY` | `0` | Use external Ray cluster (`1` to enable) | + +### Supported Models + +- `Qwen3-VL-2B-Instruct` +- `Qwen3-VL-4B-Instruct` +- `Qwen3-VL-8B-Instruct` +- `Qwen3-VL-30B-A3B-Instruct` +- `Qwen3-VL-235B-A22B-Instruct` +- `Qwen3-VL-2B-Thinking` +- `Qwen3-VL-4B-Thinking` +- `Qwen3-VL-8B-Thinking` +- `Qwen3-VL-30B-A3B-Thinking` +- `Qwen3-VL-235B-A22B-Thinking` + ## Notes ### Reward Model Configuration @@ -32,4 +97,4 @@ Our initial geo3k-specific verifier produced "format scores" (**0 and 0.9**) ins We fixed this by switching to the default math RM with clean **binary 0/1 rewards**. If you encounter similar precision issues with non-binary rewards, you can change the reward tensor dtype from `torch.float` to `torch.float16` in `miles/ray/rollout.py` (`_post_process_rewards` method) to truncate precision artifacts. ## B200 -Blackwell currently does not support fa3, we need to use `--sglang-mm-attention-backend sdpa` and `--attn-implementation flash_attention_2` \ No newline at end of file +Blackwell currently does not support fa3, we need to use `--sglang-mm-attention-backend sdpa` and `--attn-implementation flash_attention_2` diff --git a/examples/geo3k_vlm/fsdp_vs_megatron.png b/examples/geo3k_vlm/fsdp_vs_megatron.png new file mode 100644 index 0000000000000000000000000000000000000000..5c32e414b84f4ea43b94c55f0f2081abfd0b407c GIT binary patch literal 102247 zcmeFYqv4Qf zdjqqKr3f?}N<)zn@buu7j(tPXoM+-CDw+~{{09dc>sefOuq z(c}hGs4Ulu{7HUQ9AB@meoQRyeZSDt#!DPRTlu0S38U^`z&p{?efR*=&c6KG(%cG- zY3eznRCqJ?`lekSNZkMh^&VmKUAAsC#swABxw@aKIuw)=ihFo6yfQ6Ih^7<*Szx_- zSe0tMS{R4w{biP%C@H~aA5`?rgO?UQ6tQ=-IUY)77CMJ>*So!)p`BTooo_U0!8dE?J>Bj`wtNW_AG2ruAY-RyrMi+wmjg6K? z8oOCiU0=ggSG*?KW?k&hKNg`i(87)t4&(Y^$qPTm#p4C67^4A3qwBn|z#M+o46g1z zo#@?aU5|>%`RH<>52aa7=tNp(mdBmPM zT5ZM=^Iy#+?5L&kqb$wC-NYV~RFqsq*}GLkVNrX&Dq&g+lhTewU{F+kWgOgV>UT#^Oa0+!0dmSgRHe&ZOuBy5t&l!^ z{PvN;$v)XU_)o|TLKj7(KAJxVDZD5`p>#2kG=-t95t7iIeCu=T3JF#VJ^Z05`i1c^ zq5G)J+{7;pqu}(nM6mA3sIj7>exec{7~$%9G%U{8b>o&^(?nu_#K)OKrPO(mBQG$)z&yE!~zY=4zP!AC?cIdW_p4Vp; zr_eCky|Z{#dz0&_9J=*3_(_*BMvoD+PNUZbOTdzrZ_NJX>K6Txo8Ls}J#axkPWEGn zZ;9<_7zTAg=@jwhL^Kb(k=H*nGN06LgTQDMwqf4kWZk1i1z=Q7##uPcdE^oz>%K_( zi~dMY{`#$8^Kg=DI)cNWs~iUE>d9Xl04;#1D=9StKMQrgOQUW1#LZG(NQmZojU{C*|N0k)r5QX|hTR9{)5^7RvN~+imE{ zfakDgsZXk8Ga?a6)0zvxX<+*zMfNBcZ zB;wxgVK%9s5snoWOn5`xCi7k;xW(;OV1yF*vMaR`)T5r&4e>O;DhH|h0I>^kUEi_x zy{(}OhGu;G?y+@^3q&`H*JNQDLM&1weK>L>Y^rd!I7QJvl*g2$AJT%xl({y(#7LW9 z+lG>#Yki~I#oWV8#Nooa!&boN3yTVUd5=BdyDfZ4B@uU@2cwK^32Vu90C#{=n0lR1 zBFRbH6df>7XoB33q${#2At>J}>Lc?iJdvlV)ckP*m&%mWl-86lKgcO;Qtm-`CTCI6 z`w#Lo=^?chZH=Etf=A+`jGWwv?uZJrx?D-EVyUui=AwHIqSe`S&CJP6!=dFN$_?Hj z-;6@hnmR}#ccR4dwOUmh+7`Yv(j~d{Lie#bbZc+zkIgN1kZKh zHMIx)3*`%Ti$+|IW$D1ow)N;-$t-`4yGoToc=i{4Mr}p|>xNmDQdI4+>aUBliz17n zC$x)8)e6=^#)j`u5fZ5knapg=(@cR!n9eppH(nlIQ(jLXFYpmq5}}q+h9|TnC>_lO9uk$PLLUB)#@dnmTH_m&nW#sb?m~X#Gj#FU&O(C0zv`w_G8#t{LusSo#v^B5=4M00PxMK3z zIPH_l5+3oWv)7s(%Sky)MSjoz-m(Qt_gf`Ur9vfcD%>$)OS!L<+|_Q`+?;EVfA3_} zwbwcdgT#Qp)$}=&<(35|(JIjz#{$QnjwxxJeWG;uSSN2uxsJ0=;gtN8 z{SM)d978q=7!^UzwZMB!cP^*!+6#ay@#py7OX_Ga^3G zeS$;rko0u!UHb@psPvlhZo50V7he*WFxJ5qpl2>*IgoQQyiD0H`?~dEBo+9n zhfN}FGEGKCPG%)$QHaOlvTUx8yH7ovK}h?kZ7oxe8kfd`a@s3i505 zuQ3!OD$QmSb87ydaNPlZ=+6`O8uJXTO8g(MnM+u__&6+gJG8W`-d8P}FOBJQK3OU`Oke=cdHi{7 z0VBICDNlyv=Bu+0{m(xiY*Cr;nY6}R*i4L0s1I|NC4>n02+HYStsO5n6KH#7@mbuj z<@&j9Xo+bnV^d{Y673%F-_)ON`zn!#IGLVw8JJ!QhZ{0W6TPM+Yc|++jg<=^Ko|ZNv#HK1i1qb#b~d^+;>+w8yrs}3 zlsbWhjOv$VjAM#J7sAtzOBa>}CgvKpx+lrJQH_myl1=Kfxi>!^Ggd5{SkI7`rU;ORs!N97DRsng9oOKhxWAeb-+;|L_h9>0s z+P6-jkhzt{=eWA(bp&6ID&=GRHVvG3G5+49^R$t<;v=}cu{z;4u%&!uIyS_dj>8va zaM9#+Xth2=QGQXT*G%r*awWMLp~E$0=dyym>fyq7b~|K%0(x*T-_Yw^nel&Q zDSwzboomrM)$1dN6SM#kzMYTKbmZjy3ND%2f+3(*zGxvf%AKM6fHoLHMRHqq=z9PiR5dOX;rX(3 zdI>KDI=;B`Z!+<9A)^^0CvZLNIMmz0ch8Nr5jm(^Tf}za!>K8#&}``Fb>wkm9Ld9T zExYsOLMCxH&U?XK6-gh2doBs*4=;mf6U>T5js6oSZBV_*0f?OE%%_k5L*7*LtC^e} z6a%D=1O*383Dbz27oB@FddTtw9k`ZyiYO|@sP7vl?Z%bgYC z@y=9R3qgBXs9uGk1RO3ZH4Hv3F1&a*3xf;hamyY0$Iwa7a;j>ty>zzJ1}>dbGuJr1 z%h9XorQOltmyu=DRZp|gUrUvyvaL3#LkZ>4YQ?kty%8j|Lcm%5odh^(-~TRIpOJns z(kVAMnHFkQHBOhR@TmqmVFEE70DH{8nwulCQJ1Npu_^z%NDIMeX~pMjO-V|w6N%id z{alb7-q|*heerP4^SH4((6CAf{qM*4!VR+deX*V$jLjdS<#G)Wo<13G4Em+hY-0Jk z*)s%Zj7~@RPiVeylZjC8oG_iyh^OP7-o9nVL}e#1r*LJPvUnf;q^0?9Oq9@KPzj9- zfL#(sy@mmm-@9GVGEKEjm>9^>Q2$+o5ipbZO;g&HWE#H46%d0GaDhaM)=bp@eHIk* zM`$EoI27IhKhQssQ38+@$4%w+D)Qb7!~9oSrxF&~>*#>}XOH!h!r%gvRWz9B|M?U* z$gSKR&i^46UpV#^f*Np#&<^QDde@gYAS>gZ2#y{qf{~H_se;pg`E|TkyrvmR;S;zR*qE1&#>hxT1 z_>D@AGWv;t4o_sRgLwlg+k=cOap-jENg}l7N>hIAU0ss>+w>~ml;jWQMR;8-GSPdC ziT)abtZ9hhi6{0j`v&t~aTb}>`S>C>wW-69@b+BHsr~iIJ-WN`G%Sq@5fj{(F|N;P zyZD|p(p}xfVue>sX4LpMs^6)A$AIhFo8b?@gK6U6jvk9#Von9=UvsF)0QYfU2A+?D z_OFeO(e!7Yy_qz-Kfd~IGSw?L2{wY|(PE|*f#KcUkDe>m(fH7MV-~GL7y`!+xsIRP z?O#Tf*UBg*ThVTEyYOM&dd`Kw_}_lWM`)M|IGvIsTE*^At@cir`Ix?>BhS-@5Pm@? z@$UN5AR`*`cmjP=M!Ix2|7G{SlT+H$IAKbN&GcjYgpC|6Ohhd#q7lrH8U>Mav_70M za;@f^?3Lv%t0tv+nsX$*D8K({XLwV;&{To!nqb{m&(TcA5r*OUX8L{h+CxnTY)U^| ztgI|qEX@zHFz>v{{ zdc9XEYkt&@b5RtjGwwmL1YKIfNaFOhQQaKQpsLnt4qpt%GioF_?F_=pu&&Vbc&sw( z)Fw&&QOp)C6hb+4+Uku!u4&L5J8E0K`n`I2!?l)TzDL@qY!AZ(vG>Bnm=H_>GKN?X z;ZBCP=SD@uevSJr{eZG#TF=Y405%HXzgi}IxQ|mXIc;`#dMqp$9BJ_gL}_t8Qg=VO z;o6=WYm2s=di`{vQ)5N%d0NrLYqL-7d^C&PoKW?q17;>*`VHK&DGBlkkbL+M(-OS-$%!j#Ac-_ZbpUfF(*ICiBww#m72Z{0z z0|0=X5y!2axiz1}x7SoEFus|&x$MzwX&#`UYUh3{5-mSG{Z8m!afp8+ZOMI)Y^Rr` z(Y@^L=5CGM=3W|5=JVp=G?(t`R#vW6CF!xY*q2tUURtyaS`)}P>fs69bB_^RoDctQ zkigpdEc07F-Aw#Kp6`?rkqnpxY}hs~)T}W6L-;n5L=_P7?n@sYgL-tf7&)fKvk2wN z{E^v0qiJy%U~J_jUwtGc`>A3Iv@WnHh@TAbwri6}`D{np__~)jzfOCsV3jBkFiEDn$XJwr}QHCB1FFKv+;3igfvWk?wk9jD<95IhREH}9>c6n{pICQ-cUY4Y8N~z{$TnGA`+AOhUiGjm+gd9``Fw_{+#}u zzr)p8^LH@+HMsdpws6TkhcMmiW;hF*KGaYm>sf=>Xahli$wJjg10V1{7xdtpsLJ0i zz~4#UR2Mgqivl+`uc05Gje)_jNAp6&c`d@T?)wt1+xoQnWYXE~x}7F(j)$ctl1%ef zkl~}QUaqhA-rIAt888-OH;1Q*;dnY{0E#OvXT)?=D^iV#S_Mhzl1ram&}DqC>l(Z} z?Y+YY@LyF!cfY;^#}vX16dNIg!1|4ME5l)uM$=J?hpOV{$O>F=Kp}76h-y z19pnbTR%A(1@#xD;AK$yFtJob$ z9jNFdWsYZ1k7aFplzCMyX6hUuTN*m4Tg7HTo@J}pQ`KcoV6A3)(221B z!n1MzeP(cBZu$FLz5nU@8`=9cpSPAUgBA~G%V}^nWoDU%u3b(S@BL`g*@8~Q7Yuwu z&Azv1Kx~ElVmK}^0j>^W-At1qftgmBw;&%xqM9&v<|J5#w)=pkQr1^ zG-4rv!Q94#i*haY@tHoaJ<9>3XE;$pyA|(xux%#F6dsQ38;%FdvgNOB>z0`c(w0A| zwqnsx#O?JpTSR_(?^J^BSYpuTv+v;upDqUlgD8qf)ld32x|J>%-Am2(qiE(+GNcEH zDei)0a=JhXKcc;F7x;d`1^2i#?=mIC{1iFS=u zk){@{d74j(xioW$C-{JjS3wbrNpfPgcM5sfZg zIsU6^%eN$#n?YIDK&J0wQSKs=KbbIF!L1!A<^g!%TlY`=ZpWzb{&&vvm<4&z1}0DpnUzF%AC^1Ul^u4S3?vC1H@T`H60m{yqE0fHBmtvY0N4i_mBb ze>7x?rq%K{VfV8ajBh^s`%#3qU^;tEmr>q5-DDwOAyJ;ABwrHnS=6?@C zzdW=qFRrNwYJQ$11d!z*`(ub==e)eQesB|SkP}+99p9@_m&<#5WMmC%BWv6o!n*+z z8%*X4BQ;pRHo)Km=!`DJ-JVJvvl}wzRvdDlucyX#o7Yk-<%1t!p%LW4n^gB9?v2g? zM=y`JF*pj`Gz19!v6gmePAT68_4FaE_Y)T5Iv{fim6N3Kt|)odA8xre^j2ac9i?DtyR=IF++cXB0&y_&rWVYDa>Q|LLUB(X;Ry^FrV8GyC;juc zrd8z8^Q~RkK2NyJM{#dD5aj_9Z9ZCxxLz^W5rvR%1hNj|1~xD^t%#<5dpKg4^1b#&_HN}{=+ zoz(q6V$cZ<;>9?%cOGYdSp&(DwO{3>*X>c%9-{Cz(}6z(U&$P{vJ(2cAJ7sk9E<9p zG-9Hh{*57hsf1C&lU$CDkf<< zXa1gdh2s23yhwa}&zNGL$9T7XMz{MF4B*vwQhrp?HnRG5nZnVdoCzrP73O|B=<*7!&&ONfgrCr^YmD~A)-G_ae<&hpkU4e5 zLV;vgJ(0Y(fr{oH+o494W2>Hc%iGW1I(uKx<~hreGoU|}j0VTz0?$X?B<6}oE)kN1 z5cOX%19?t6yeh1loC!3gvDVYJ&b!cKR)FpZB>;2gSRav)dhf?u)P|-I_yfohw2{mz z{34u1*I#PI>ZuQ*;NjKTf^siNr_}4|yPXnkWp$Chzo+ZJYP4mNc6n_8+3!!H-wCmG zFI$ysL3=J17W}!J(RUiM_1@;VS+|03BO?w^_1M3JNgw6q`1M8nLf{UKJwxVXK+knW z0)5_Ih@%`6aEo6!FZnStD?pR~Iy5)JLY(++Re-X@RTG7G+~{w3?k5UUJ&sqoPX1TR z2w_Bcm(C5t9^^Q`5#BZS9fS&i(tlta5#}Go^9CMoc2b$dz@@fuE#7dJI&CvhiyXA}{z@OBzDv5z5cDub&!dP7=ry ze3kCCNF?d7VfN0TMJ8Drkx?`CT@D&mT2|L3`rWbyTls0{)fmpNK9)$kqw+cwQYIy6 zjFj@9-Xf#Nz_MYLvmcr|XhTGSBp)o;e_km>?*-9bR&$^A*jvDgi_mmM@La z>2{T<+mH6QXru^H37fqaIj;tb%Lf9)C_XuY=lK%seRps+;M3b`x6n&R1Ydu;U(gZ> z<`_PpFri>{1l1aK@$4Na6IRtS2Pu6+U9mLJAk%&uW&LO?_&?_Qy`r$MedeaSJ(py>o9rb^{RrMPYZhc1PwTOLc8^Ml6>{Ov(NBBdYs z8TqnV`gMOxo(cGw-9vSLhcX9mOBF8=oYGMYQFt-0w zB-Z3Ea-WZ%O>pm4efrX2Frtt);K+92jeRI#CP5nM=F>%5Q>0CxbzRue@6q^a4ul6EzAWV7qDAy`rxViZ1rB9cMizTtkcjMQ}89H+HTwmFPuh)PFyzlUK*5+s3x z-|#_>c!;FD)h0)@BxC&5WQ!l(8n2TVQPzU{lW7l?-L+?7;1ywsUY*?vZ=9WDusHE{n@YmfxpREpLBEPbcwM@e234x#8NyC^8EvM`=r zCfv)U91DN{DH9`sm1#EuN+JH-3^@jNh&6#uSkF~yPhf*T-RFEmYO>A%1St0==VtU< zuY~TTpkCGQdq}lEq&H7(~HQdHYg8cHAb5a?yel1;~-f$k@m?hJ*X)*-Gh z*&%Ef%UqE&3ACwyRmums)cu@I&|uSZ%&vTpF(_vj$1ElO!& zPQumc2+yRpA1aA5OBM9iXrP+DT+y=ZW9^rZaDT_+elVATrvx#V9WL( z{_>H{rZW&*&N~WB>L3KPL)46}GY7MW%bm$rlXBAu*4o~~%^k5iW_`P1)bW_i5z>pT} zuAN9G3z$bXEZ2_MnTeQ@aLqb*cb(VAeoBGN+c#ZZywih z>3eEH_!k?D9=%))x(*z4<<6iv=LY|cD8b*h%yYWEVCiA0A_tUph?Mz`9WQw~|C`?=i2IFe!2QGMpNd_Yg25oDZij*0hhm z^B*m{1Dr#xWTUw0UHplhXb8Kgn+!WTF|FIwSZ1R9-JRa7yzM6{*J%Pl@&@czeSO$o zm=-II`%N*!UP<_W9SD=ZO7X2sAOXSk z-i7+VJN{FNUWGuOZ1U0M{bx!xRNB{{g_>CZANdbTzv~Z>JnHIA*JRAUas2O6So|-b ziSvIs`+pJsPs!tg^yqaNMMC61v)PW_`T_?26EJ2BgkBK; z-26Wc``_aSauB>NMl!Po#_z8YcBWK!p!* zl0ZWzv3FxalfZsh)PTm%gmrc3CV zGldAuk z0ndCw3Jkwk=^~EQV_#efsqq+bU-Z811bomL@A&cgyHUos^cE4$B263Ry2|4w0A9^C zu6LO;N$p^$W4J(6$Q!;PeTiPF|8a{7-a?Ig?cU?93zQ@W{8{0i6N()* znx8e*@8MR?-pUfQ%UKgr)EJQhNdtY3dbKA1$U8j};sTd+P+1=Nae)%a=kJi+kV<^+ zytQ7JuVULAbN9T08lCfv%E@DEhDI~42KYagWr-yx3f%ncpJmS2G^w+uL-Sbq(}ig5 zHkybh5|W)il;JKv>vf*J_u%O3VFO%ZmRw)>%ejezPnJ&w@47u3wk$+M8 zMS--K%e`tlk;u80cxM>^_E^DhFzP0yFVB4-o@Ky%r&GIo^r8dQ)Nc)Il&xYqaCD&=K%qfesb+V4v&U=6 zUDKx?nsa*tbyv9vHb#;*B)YnJe?@8}5YlI%cx~2}=c)gl=40Vh;$*j?INtENcFY^4 z@uW)sLG>$j^wW`jrNg>_vU?v{K7@spy|rr^{LKR}y$PWhXe@6{K&i2otuhiDS+06- zdhqo(x4#`kot^Kuxd-f))U7g>J8ZaE^@?(UKeu0Bn^Xn14K2rWo9HqtsUb|3y1=nw zq0jJya)YVt0-I%Q39*pKLE~4!C+`Taw>jRz{DeYJR(oyZtMFLbeV^1;eaW8&B|GI7 zOnjcFiV~cKHK%a~8bh8!CHSF@etIb$OwQ`#Gs@QEVso5#svRW~Bs&cl34wQ--X&IxW2}zalRT&X9WWQc z5bIk>;;A`jbc;(#EN~bX_*E*!1zCAKUoRdkd?0DK;<}a^>C^B7PfS8g3Us@pXL__) zGr2#?*NHxdDj{!J!0nITg*4~&95xN{CZcHKnHkmlj~UA*9RnXGHwXG=VwD+H*%^Vg zcRav9bvHt9)M^;KY}d#015f6S!)-g99BH*P;bTF;lgi`nY!U*#@SeL31W8d8xIiVP z3uAh$d&@=M1s%Sj({mZ5ViJn<+^*BwLAi8dnI&}|TIEI)WM1wgx38sq=447s@8$rg zpe39ula5O{vsHrbP_zj+NDQLUM_MPuqd+`e^f92!#>{@nt`-2nUmg`lOtwUFgg;oP8`5m6-}voN*C|`IWG_9f z>H31sOxUpZGYqt9=DzS*K;y6u#hK2eQ3(Bx{9Hl>fGQe>}g$G(56!MHTv z>8j2qh89DBdf{YICL&HgdRgf8SB-&JA%4n$a;pqp;U(xv_C(&?xJ;>lq*K)DLwN>wmrbO+>sI`n6SL|8f_&-L9ceZQXq}%rlQOFNMAVr$8Wk} z-3!pd?O!I{IT{A-4%6GBhouhI+aqa(&Prl?+@~f?+plY=c}=%N6B9kR?r#*l6{1j`2Qr0JHpB`jZ?@C?uk_D#p$P${HX@*@4x8DjgR>2RJh;_vu2C#vzUdvlt zZQ9w+4MqKIt(yMmS3`|OUHH=n>&4gvK-p@RK2VGD>y&%AwR0=+?%m8E<@+}qv>IG} zhmtw$`vz@!P)4o(F4Mxtn`+XeDc3=&nxCd4-jYoi9WWh&o)?lF80c4HyyU7?(oNMD zUA96mwh@+fea9+#EZk*a{t37hjkwuC3f?!ggjSlF93p$oKmJcG6dIPq4VA_x_}-&T zDzCq97>5cG3A}*tZFTJmd(Rs$Hf3|Si=X;56^EXP8(zCpvs1y#qQhJhJUNIprDm%f z)qpmYUp#}>{@Nd+7Mp^#X&Hx}gh~_WtDH?%UyEDc*tNirBpi0vonW^kWZqM53 zvsJmG12(R}{3rnyYhb@|i**@$*Q!lMY9ebQ517)@*SXNF5B)YaxsCIxQ#MaI0KqIY zw*~wPNgEDyb(s{qe6CumRx(QXPU_e0q<#lUGWR0)E&RzIbsyFDJn$+^SM_bpzRCyk zK-DyNcmyftzI;`_tCSVH6JHeB1L{ZVOB&X?J%$O3vCO?)7Be+I>320MQPdzjbROOb zMVm5MSf_g@OrI_f=*rb*>2ROx54_xHSmuarHj`b#Kp;LHf4?74ceZa9V_aw!K2i$$ z00-iHl;_Tq4{j#v}-mY$r5P)tY~W0S(sx2#&|_`RwWFagi-8-wlXVw%p* zmr|SECU8AKRN@6nu|l3~;OWl54|71tXc4>@L4IM+j(lZ)RcXq;H)#Fdx=t@|U6mzfWlA9sOv_Wh?Lz>U!-fm<{Aud9@cUkDe*ZdC=YU51fI+t<}spyST z=Sr{?DDCD`ITE}WPQ@&=F|;F+{5FuRu#mp~#WbRi0X_z?%^^h_8m5F>0oO=s!Pz%j z%jvdqle5NR`q8xFvUuiV4U$ubMiu?^bIM;+7_`jf6qdKlLkx(=_M|AxWUqf7qWx=U zwlkoWmDX&hmo}-yW_~FHBs#7*{sTp(aDo<0*l zOmnIWGMm`8_TL<8YHPsC!kO5bh;P9B$0E4m;-mmctqF9kj z<-uvL-^e^=yHC{dLR|R;*7&bz=Dx7rMh|HS>ad(0V)j+dD9^sOL(+0~ztumlE;qzf zTgK>7`=6$tX&`rB@kq@k;JlTCzg57kl=EJnVUfXvHkc4(Dt@$C-a~H0=%cx7tsduR z6(`zd6#bJ;raHpHI#aWH_{Xs6Ak8169`h!uwMNrN!!KYX2h(imIu|-^{^9G8{(20rzT%!VX%heMIy8~>ydn|WA&Fvx$Y=u`fPg>iAq9n)DqxE zTvl@RE97;Wq@zfWeL+IYS*R-e0qnTPaTO8Wb;YIfiAy$pjcNLB6D&}a97TPb%(-6) zN7%??I*Z_ZPcvC*l+a)w)2dPv&$s*T5kB3!wndF{bcU+maHZ17RI41{noXQrE$G_v zF^cLxA*whNSqV0vQ9pMM^#kPlHo|k5T^td^9o@TU_wXGt$vqC0z!4 zx~;}|$0hjQkILWu8=C6=0{C`n%yb}BO0Ex`tWvUW&5D)Cda<(!fsj#hWvD6X3X>R6 zerCsO6LDHUqUqxRZEV+}2RoAZwo4NdLh3t@PP=KSSNF)JYu_xmdsM!^IMy1S9~JpI zy&qp83f);6YmTCwJiXu9ipW>3C!vpG4D=4mBG|I%-s0ick9I44=$7@-rk^$71sXB< zv8ZTOk%BRvD^a}S#xlJj0-$DgRayE8ZZ3!Z(J+UqYKCPX%OH3&KXIn=x>40;UNHp& z#0`cAtZ=Pa?LAZB1(()z39^0u>DYtHIxxt!`pZMZIsUMZEP&}-Xq_Nck>c9}30Gmz zo;C*;CbGyfb0Y|BggitI_r2P;wh1_G;RjzTa(5(luui^Ywrq-e$z`7a?|l}_f8(*3 zW$s$&asHM=OVMZsPbS}9YzB*$iunl>C`<0s-7IF2=)f>}{##%9t3YArBSJ@`(ID>k z>>}^ghV1Lj9Xxw)8@+M^OA4=kO>;eK7N!fv-d3A#?_54RjCi`UHX`=7->X<5y1|-< zifg3}4ZTHLC;c?TPu4gUCzzw2#J8t-kNts_tnP00wb?&*1JHNN77fojKKZB|x-?9e z5Fw$iD1)D#?58rUT?PAAv0XebzSBwJ0&WpP8_mm83^C#=^~%)K^=bA#93<+hYO2M_ zDqKf;At@_9E3mIOJRH(H<~}RHla!ddSZvm*%nqVbFmrWb>xqO!@}xNmuQ&J~S=VSA zx)Jbh6E3=JX`zWp2qWJ5B@zsLdMd&*NmWB|U%TprnX;7PMXaU2w^c2r9V=qIV|~>u zM&~6SBrin`7Sk395vM{aoAz|DEq(=Ek8C?%{@RTg6@b%L|MjJ=o!ozqujaI22N{nu zl1wx9v53tkKYfFbmF51WwoVKPBoSN!-iPR}EOQgrjztfeq8?**E{$5}0T56HE3>|- zk9Wwpywy_0r>Y{BYu9PLj#Gu;wCUxOvkncRnScJQFMI;PH4o2Dy;pRI&=$g&H+u=( zx&5sKh@iEC+P$;XTR2}NVB9`AQzsHxefU^YX;&qjEhlg>s!WqWm0~OWB2s_)8)pBJ zgE&wekEzVTOZjU{;wr$sin@#O$nA?WzqP1oKN zALuXv=dsDsp`W)Wd1ZNC# zukY6-rg((D-^aAiOm`ZNMF1yJw_ojsUdY0ctDa3g?&Aia|Ih+al^#|12Ed zHrMR+u(Q*g3IGr(i!tj*FA@kLQUqV<8eH+=v*&g;b-K6a809hKO(WOGDwfvacllNq z-G3B2L&K@e2z^4RG?+6z8x>WXxo6)MThBlJ9w+dyKCsS8$SrCN^d)fuJ?Y`2*Xx8h z>zf4oY^Izjb~cU!_YGCd?lRoDZm~6SXi6U-pPpJ1a@M;KV1JJ(F7Co9otcoMTYRmg z%S@O#Yi0z0P!)Roqc{zTg9_)HCf^a*KDK*jUk%^_`w1R2_zwHW*|<|*7i6+Skg;Oe99 z12*=k+d_p=JDfQV+@UjU zf@$sa9cQKT_Bz2sS_B6n&?o)X#C#tb_U7iLqN8@jfg=C7k4DpX!i5I@T@~qTN0xM=z+O3DtE+N| zL8t%3o4clE9L}vOYC2<<|8JB&%BTajUdbB9!Ied57rtugt&1?(VX&me_|D;Z`BCsl z0x^&N9&sq220VJnJ3DQqPfL%LpegLGkDEy?rZ_QhGdPV*Sa;`Y6-0b z?h<&XMBHY6T0l>ey9Dmf>N7fD1QRmb#}(gTPSlUdiV{kOu3>|OAW13(mAOX|f^B0aWvF=k_!2`nwtY{-AI*%)Am-TW3An`Z9aY*B4dCl=In8UbCyHg}&b%ysuHA=-l%Q864 zM*RGm@ay=3kq+3YL!?vkX3|NT6gChuhHw$S?#97@g1$Yab~iW5q=&IQ*10#SA9I$=fwkcxWqeeCpft=?WI3?0X*b5m3;;t&RDUDnpx z{po8q23+lk@Oa0=h`-Ov%yXTDRgv=mZ`r2)r zu>d)j+Xt#2{pGPA|=_Sn4)dGj$S@(^=WnHMYWlsX-jtOF?Qx6zpurb9i0ZpX*MZ%h%)||19RBVh&oK z-7fh|!FIpHhi`~{aL}@7l>s+(_H{NV#{tWJwTTf6*4`bru(Cv30fk_IQb}l6Vi27+Okd23W`B z-RdP#=TjfXi@k9ACL;g*o{MJiN`!BM?6$dCAzj2**7)q2L@tl8=Zc<_-3?J^gNY+W z*sAcv95S3ff|rzZ10grK+lTdXmQ2agSQWvJKjX<1d@SnJ9nuOj-VczM$Wy%=oeeX% zITinKv|k)nR9B$*sSd~JYRc~@Bg}+(O6ay>0!&@7hVdVuyS` zzw#>Cy#aN_@Y-NL$OLvTJ-)+?g%%+bYl7en?G2SsD{c($VMz((`*ZD7d+V{?CDH8n zQWv~*^8v-x&B~KJyPZLn$zuBSIUT#kXeZSW^$p&#`)6~m_b>(>HzuwH3z6Kqh1aCu zedM=IDUcnaQS%VR5(c0b^e z4mz=sZaK>+a0#@nM3#<*0}UDqtEa1ao`{Ou@+CeZ;6m75P7wq)d244FOpKeYVb3l~ zyX72>TZb9Wtt6tv-c)5Wy);@`^~rVV%$z2{szl7^{N$LY{Rtd)Zv7nG^4%UXhZpEN z!!ANr#SJpYpuWW$o~na2%}^%^wXj{;BHvqIIt#Ww z?Q3)m64R0bu%R2;;+?(8d0~;E*8Tl1d3M@Z8H7~PQJLvD0R-}lfHorAN6@vLb2HWS zmxBO>aSSWM_Tn(pH+QO^!S$RE>v8&^OsRfEeVvh;I(Hcn)IoJ(L)+~c?$1nD< zaAa+q1E`R?bd4Qj>u9MqI9YsTPftV76Q3P4n(Vr@(d+9QQTSpo*Bht*gR8S?`)*p( zhwUjk42Q|!@LtX^f5c*GL^IH7g||CIG6~PyR09)P)aH3x0$`NBZ_jDI}#wZGTt%;#duOx{N$HN$)IJ(nkwJwBbsc*6AcM^hZ36Ys5fLq~E| zZ%k)@j|_agd4pDwe`rL_-67Yo9=C#_NOxH98NlD^;0k7tSI0cWG>E6sFif!+w--n) z$fiJ_&rz|jWy5M!I&f|NyBEOPMu_+74-#=j{&X`cGYi$~>>90QYPz|dmOH~?Vw4!>0X=~D>Z`inv;-1E z-+D!-m&o&RBMeG6?X{c^U?***mP+@Sy5Go^!g096h^jI>dm}6(nzRm+brcjdmIx>3 z5bN*)vgf0n)Kki3QOO$CVGxKl)X z&rT*%ZNdWQY*y+SZmacri)u0*UHI=yYiDE_2!iYH$30r5VKo^$V%uIgLY0F^?9z&A z1BuV%AAiq~Gw4)z4}?bSmJYhHQoqLsrAcjmjsD%_&tQ#8-Z{@EmhPeb>F%$?2HX-e zGe@ivfOgJ*@weG|HmWdSXpQy*R7vDf%qOf>rpiCzg~zwzYr8%-5c*T$3>&lZii_1j z=4nbI&tC?xMiW`o5FsDb_;fz`O}eWz%DlYn3Kixw2YRNc+76>F>Wi zFZ7*Vkd%wQ#+@jLtAc&n^p$Xe;$mcqLU|9U%4e4wdt=AVtPS!thT3-)Y4+Nn?khz& z?tV?Pky$w$CHCXj$xDtm6}K4+V*VTYWXQ=^N$sdJ>0=RSMq0(9RM+AI2VZD=O*`lB zR5fYt)XYofX)!l!i%bbNFr6Kvy3Wbs6+3EtOf58bMi{x2l5sqJbFGZHUR{YvZRF;7 zeJ>-TF39bRyTyv{8D%)c^s;?(t_61au&Le&<>O*!f0mhPb9DTrLF$9CXWftXGEdJ; zA19MpW)+&_4^V-P^zVDT29-Q4t^SbE{3id?XVYPJEH+YCKMnd^nwj)qbq9!fBa!BY zhqL~ulD8MQrwdG@(%lR6UCAU#o5liGFHlGZ9-=Fc@9aFn)9o28^}kLO`zQ_cYkka6 zlk%3EJF)F=(v5F*E%&cH=OEWteSE(TE|-U{has`)G`+s9Kf9d{Tq>^4MC|82t{Mx1 z6K~_i>NjK;g>C0{hks}GX@!NS{;~I0_s@E8ZwFbwZv52z18Tu+b4P7q%IE7__%6B9 zvnj!cO-#Mv(zp6q{uj$s7uu3Sg17A#G`YP-b#67Di~C)cijjpbgRj%?uTdyOlCA$F zJK*VWKN_38*q#-*HRhe35NrA%J>Tj-0)6I5{LU!OrsNQ6ok{^r;rr(LRqEI9Kf`&? zHZQZ22>c5jne_A#!nTP`mcy4nQN0WU6c@{LqD z`c}uuLT0|%5BI&DK7~8%=<#-LimKC~uFobnFM(>6qdbSKy*QDd(>1;aOXt;x8Kq@rB8-GbROA-@IU{^we{>q~;rU zgnb94Z>h3nP+fhuSx!)hlg;9bx*3BtLmO3TWqvADkrU)T_+=I^ZoQk7h9p1%Oz~nE zn$@0u4HM29in7e;6Qc9+?`~4J_i2u?z^R-^+dSk-~LJ&wzT-&t@iM;zp!TKP%@Pl8#Z@pc4eesparc2o9ptVSie}koZhT zqJ++?Qa80TV=Lw~)Fy2cI=NBo0eyrR;BvZ_EN`%NQ;X{Vaygm`f2j1*30@3 zAYnj<`L~?BtsK4Nvd)dxsRk3L5^-f)%$j|7<%6-0#43AGWcY4bgw|E1ZaeCT=p@E1 zN_Y1-lhUxOX<5p{mC6KF2^ZQFrgPIORLz8UAU;u-C*hti-6P<17Ow61(TUY3pJZ!=G`SkNn6RC}vVM*6w z^prwU6`tVcS!X_kX1SLhDoEPkZdo_J6)P;#F}@0I|6;K~U)jx=(cG%7~mz}Q_Os$KU9idk_TBT)t7%eMG*0l4Lp$7 zWzcf?j3)^W$J+^DSI@2D0Vj_X(fMR3H)xhi<@?DIDjQcAy)oVT84F`X?~-^r|65OV z!FySXq4Pve?B$x7tY^EXLwd{6b#Xg;+=alun352sW~~xDMp4AR zr~A#y^?VXUB;k%>(LKMoC(5`<^~pazy4yTJ=qS2?rHHl1f_XVSEQi4SI$4w z5li;W*MYW8AB<3O-xc>fnRtMXLFL&|73uflcz!M$cEWUaUat{p8uHnA?@&HUCiL;h z%>^a-t}a(3IC%vs?mhn5texOgNx%{Hea@kqzykb89m_E zb5)Tf6A4+4 zNS;XQ1GmvpuXy-Zyvh{(@ap&2xe%{hOpgLNIB(i@8VGk9P2V1MfpZ+e`;~|bwck@I zIZif{G$x|5VXCa@S>`*w2yhHyYJ9B{J(G83hSlAy&B=G4Psl!tTA4CZYb@l*Lnn_` zmA?`pvdQK9{XHT5sUtCyk!%?FNhxxAtLa0s9*yf|U`__1>r-Ubq@5%MOJ90kGdLya zBLqu6cSVHm67N$8latA5EVPecnyMa2Q6`_Vx=wh6%mzLAq|-zFaTxq^8D}H%*~R=a zXs{}fm|n`Rk6~=(9yVd~_qCW&S?o()XsUy~m+aYIx?CyQ=gS}@6;|yxy-6t-NeZT@l~b4*vs9vBM<32T!tP@VtC-Ktat)}Su`&*I z32`aD9z+CZ&UgR1lC4S5CcsWe3 zr{y(I3-LjYh_x;us^P(fdJ!VGaI$gOGc+cGn#u1x!G^4|VGa&clR0$1t|_NoaCUmFwf8 z`#p~In1p(zCgPJ-t4(}xyTZn;prELLU>C~S;_^zn_j_1ya4{uTF1cj;**`}kkuU(($Z&Y`;5;fxz_bK z?9%W#5bu~uG!KMXOe%xX69qm~s52pAjR70@L2{@oWL{)@jvNW><1&`zHQUSE-$(*( zW@5}I^~t{AL81gd4CKm^B-c?B+H+Y-xX3l?CjymX`<-(EIy)pbN`X%?5*s_3ab)2n zf1%$NqLZ-SCC%}xHpD+gqgm)9OW&T8v-)&qjb}bg>#DBz_YzPbqHe171|<4fR1uDM zBj^y+vGM}2h-5b+VQ_5KheVoQpc1BcOBzcwu0tZv5#8q4^a9I6yB3|IOMXr>A%Ud) zM{bD;u^zA$u+CxgEr*IOPB*d=?YM-1?Ci zYm@c46$8PK)p-CfCyQcI6*r?i53fi0A4}n#sZYuDe~PN*UDTQAyA>@WNA% zcdvzHewlTi1d23SeI#{rd|oph9YWzd)EV5Bumm=>*)vKGaIoD2w}MK^EJIpVh!z#H z{2Wp`dQkOQ_mDiK-$}u!!?RtY?G)FL>+eQ}3(m4=8Q&vPnvvp84nnfHW7sxS1L5^_ z){D9A_fTthC$$|17OnzV+eUcqUz3;%gA;aKi@xe+O#6?>?^{ZqX(%8 z0b5eVccSvChkLJ(vS?@TZ#p6-y!TKrir_x6&_4i|KdH?%!HKPl8S02`Y*c33OZ{Dq zRFCjAwrop;V;>B4R0>7Jqfy7rTV2=>Jnpe!(Ea@Vy zsU6>$LG^6+O~$$$^(z06SP-S&rp8F$_u#WFl6&~xaR+kZRpKYMm~Cw%uKV=V!Ytz+ z{2q>Hw|a4r*B4-*Tz5 zMoN)q#iSOt2_17Iw0Rt?`DX^3La9^}7k%+1EX9~sVX!2xA}7*=qZ8{OZ|8NdF~#SJ zo{zrJ;lU&d5Ur42P4)XpJ|}dW)7_r5NbI_tO|pm_X$SfwdY~+S+{~EWxxTBOSr;*Y z68VqUbP`JX539d>}?J<$Ml#UwHuRtt3E*~kvc8QIdv?J=(6 zL&pPUxGx^-=Um1dr-gycCMVf_B{y1b1foY^)?8Uik^mBKX;k2xm(8s)N#~{0tf~i< z0=1Wpl;t3E=;$beay%b76BM!m^_8J~P8=6lIt=}mdQ<8=%7M`GGRoTMgEwK1+$1Zr z&w?Yg^)5oWBW~w|Af-K7kWz)%$}p&RkPAg=SePj!?%c$0-Ed?$X#t#U@38b_f$<{G zEWMlrDuAR4`nqRJ_kciAE}sgh982(XMiyi%^QjeVvH&gYFx5keSLT@==7e0Mooe6B z`b+oESx^d=I<1xJen64a+CJ=)x1y3uo=~*#WhJ*vQ7+}d2g>WguHQOb4P+@L)p|#3 z#|K1y1^wK9IC^N#V|REis#K=wI1{MwQ*Gj8&doO0#&i3+UlVk2&t}uKjbgLKbmf>G zl;`Y?S1CN_=~I=7D{tFyvGj%fnZ-0GLOry;o2($4+i86$_TNm30Ic}C;3GzxLh!#? z4qrt9{XbBI-UG`2|INM-1B&J1yW)QQKll&fcif);|0VyQ6ITS;MId&Bq`Q#)e22@> zKieyQ+RN%#a&fj8RVEw`+Mi-p^m4bay^$8mdcq4(Y*^6wZ^A@c+$T$_qdfkLC;*U{zw2#qi-L z!}Y`Aba%v zZ(bAY>yAB9=BEUZ;5Y$*K$eWQG#Gqe;1es*Q!0Xt727bql ztRK?FR8{%=ge^I&~BMVz`lGSsy@mV`4w2{Pps<$3n#3MrAwo9p$C4X?6F6H@voN3{d@lZYiyqA zM@T|Y8w_7tIR$8_#0ULY_-_?1Ihhyoo8|!v2Hxh+&xv^OVgT#^ZM_e{_$(Gcx3MSa zx86#!Nl(<--~8Vc*|FsG_nH_#1>>JDDgU+DPw($2WMs7eRtzed1>5lF%M<`?uRCsy z{$A|E1b{l0z6`7ms~7P%Zw#O8(4AIIvavqh&8jT;H!ulOl25W`lmkmOF2lUnMU1Qd z{1IKjs&7}VKSk1n1OLw|a388Ms({w@`|lArSpZ?uYnWxGd+ONrTTk;@cnX+4I3#nV z?BI*`I4Vrq>O-AXi@tu05NIXOQ@r2=WQ+U1W!o2I*~)K8XQcfx%f;=dwIKQfyjKB% zQ00NUse4`=7Oex#=z9870koJzs@_rhI2i*f=GNMcEYuh4bWh!+uSb`hhTh4B!IMGm zK|dNaNPckMAxL(|B>o4$xQ4Us+$o{O$K%~yA@r)b3INe3F93!n(5;QsF^qHXZ_n^G z&|sWH$-nPu0`|hWgVv);l$U(QoJ@|TQr>i8AXJ|L1%Mnn7^cXkcn967|AFpHbO2{( z`DP0tDXlvzl#Ul(lX|0z_kDRsJN1%ugmzT5ZA^*E1ZKZEx z!-AhZ(3NTG6%|E(kb(`&#_J#1uK_-ml7u%(`CmE-L67JEL2O4r&-NxB6%DWv*2l>S zAA+mWLV#>~jGcJ=CU%pzgqh6-N#4=#5KlhM)VYY%^I9l!JM@PDbu zpQ9)bRLkk{RYvJjJerLYVPc^)mFjQb+LWl-t{EA~=T_s`qp4KqFEYZo%C>l3l?7^j zf4($0h-{Cgn;L!+QtAqwjsIP4IE2xBfW-`m|9LN-DInR9PY?DUVjBv9T3+12lTE&3 z37-p?e!qm_n87t&Upxgk$VktMVB*#%N!k5z@sj)|}2h&<%rHr~oN^ zZ@Rc=wQaIc4{LI(H%w-VR^)7)0xbL7T`6g7BLN|`%`p|*{|*llf31`7m}N0b>Muu0 z_^)xf1!5(z4G9<>l*cVHin5XJ0d}oTo{X^PNTuM=r3V&+$uCn^_6?7YORCjxnM~R= z?QSU59$Qzf!>c6;K@%#ZaeEFU$+vF1foiXxQMmsCmLTDPzNQDnwZ!Kgyx4{^ZzBJa zFemFWCEAZ|aN*(o)mL*%PTj*PRyj!NuriU`i}8nGR6V)5m8rqECSGUeKq!B?y5@_s zrfX08gFxaDdS&w~m|Zp2!iEMz(v(BX>MC3c|2eQqzk+A@1`Qe2Qd=7Am6(W~REAuo`YeAf8;_S_57eCjop^_vbv$ zo#4Js)O-9Bpy>XJ#XJO{Qcu@i^?H(D6$uUNCxfeT(4a`SU{k#dim0jcjtUprZyjm;!8n z^KJ9$+x8`Q$rfONy2$_a<}ZPK*s%?4DX*tcwdFR=uZ#C5%ZGsT+VbfeDP66W>o2>F z_8%CJmZvIVVtp6SyW_ce;297_L!xACLAfiLla zxs=>W)8nROGxR)QkI*?dIDSQAd#+CaHeSn^Hzx-tpc8N5`|RO}-ekQj0rWH~JijV$ zBvR)9q6Y@TsPgdT8y z=YAins9QhEi-4&KzfOH1M;9Iv6?;z>e#C_ah<`ahfqiJ)Q?B0+Aw$N-<%}kQ~dQZJty(0#!SvYKBJr+f2g_3y*}YF z7dKS8N?0H?epz_ouUuLFUxIFb0#qfUuiGi9k4`53CNBFX)Zv$-F&D^Jr1{aC61;yc zraucy$adL@p#bT3!A25+GzpUTFZe0o{Fkp70i{rYhQ#E~3pVg!&fGI!KZO0>JI@vB zujk2^8)f?$5m=r*d=Zwp%7bl)PJ>eMzr*qrz#cL7wF=wCAMSN!OGAB>ov9c^~$q_z`1 zmF9=O9sMMC90WU3*zTubM*R`g9PN34ox+Z|{z>6#JQrxM6-8%$JzetV;cT6*+fYvX zLokkJ%5{ReY=hUZ-i1T2t!}MHxRCvXjGOI(I?w=BooCc$x;%lZX7krIc0}HblaJ#q zx@6W+m<~XpiOT%DtSV^_{ao)EMph%IC$^5~(j1hHj50Z<_U+ui{Zg5%!ouDLw1%IlHBB6 z?_yOg!Cw=3)B6ATTT;Ibby+CiVt!`xj^y38H2V!(MT^v1XXont8+=8jlEh()J;=%N zO-%cM|Lt&md6F@yXqgOE@8{>Tl$fOa3BZQ}9L3)~1poN)B+023YUkv_uN=siUWCyQ zUpX2wD1JX@qoUy@1xUh}`C=Khiqo*i9F@H{-O4{FX)lpSBHOSANqI`=$XHH<@(NGP zYUP-B8!|Ii+RsWD%AYE^jnE_ZZ!+81ipknKnmM-v7{Ui01fczmLQ6M}kAc2RN&I-|;#V>s3I&6@xy+UEmd(;;U*$s={34G9}I! zNPyF`<00F&RMK9{h4_K+&YtXOXZZ7y$=?wkP2GZX@P-k6b^Z2)FTnEbkYm6%zO+*V zp>_^c11tRQJbXvJ{68+EzN(4JQVv#y50Y#_RNn^jQ5-lpyu?Lv#hv8_iF<`kz3}4L zb$DL8B- z0p7V3_hl+hX_%~lUGuET7C!7YbuFeodJ)bFpIPc8p7UD}j)|YSL?#U|@F&I|FU6So zQ_Bt_)CTVsb)R+Qr5*VNMMES?pm19>1B-6^eS0)676rK8KKF)rPBi5W<&WaEJ+Xy6 zz^G)XgpaLfEr6!OxkxC_Py2}kGjr(tg|kbHO{7Z#^Zj%({x%8Kf@?{Y@^(msknNk1 zCUvjohv59aOtA!+YP`nVs<)$T5yc{ONlsiKXM4qmVv0MzJd_g`7580qWl4V|yp~N$ zh47STBbO+Ds6Y;IG8q$N3C_Nvgh>19lN^t_GLQ`=Iva;@s{Pi}!mUjjzBcdH+A}~j z%{a;a%y&{QxE0aJ^hA-*I%G*sn)Zh)417?Sd!}u~szR;k9xbqXWQmQjT`na#Af5z|mb>v!MM(J3i^#B=<;MmtJf9J_YZ6|R`;_oXco?Rcv6ydlsO*AKlO1Ek*wI`eJbK#6cP+qvAW!J}Z>)R2;Tz2JBf{11+Nl?wMBDd1CmKDQz za1i$B@&54!x8%`wQRBS_ug1wNV~?K@9pOn z6?HxIZ;67;%iem^3+zm7z;^<_h#xR|?~lE325$xR`>P~Wv|Y#+YnQMuR5ZWl(pL4E z#c1CcHqLw=!>wKZ)#^b@7I0Lfg!Q7WMipiFrZR8f=*oQX=;{Lprvvt zl;7;tce`7|N?qW&DKZQ1BE?%5-~bqT+R22aD(27dqj87t^xb#^!*#W+)VXUA$3h8A zJAsZy`%gNd*XWMYf#R4|AXLhKv^Iil#J3IjLp<=nWWDD3Re;dmh0Ydk>j=XL0fhRy zV$T3Gc!9S;l8T?-pL$A8J70UG(r&hr5QBMLH>=6qU#{Nok%_$H4$DDa$^3ov zzR9$nGq<~JHNL3)X^ZVz|NO4-zF)V*^CK>0qPTv8`guavi7l_b1>)CHKPy&v6%Pjv zqh2L<#7-v;)HY$?hPz8thNfS)Q?nTa?nyV7%fr3so!^~($Xt6)TrfPGz*NC|#A>=T zHY}3j=_Q~%_UX3C_TmO^cS?$pQ6Z{RJxAe{P(OWKaLuNCMIsc|XjI|A;LTaS7wn)t z*nObwy}#wDj{TTE%6Ok=fibRYVF8TQZqt9Uwizs zR7MI+8&DovqtEEB0;3+d>?Fco5UanSq15*mE6tYI$Db~^KI)P*ZgifN=;~>he&u2_ zXq&aft6b~OG4CCIYKv`H>%AaxeSlS-!{eP*Tfrky^c~*7XF%6{^4=eJ;m=;1uPSY! zjp0mnL6`9Fdry@p!8IR7zKyB1KlVPDRfU3D<>Nq}ryC$97cwNDV8 ziaGE+_Ys>f&#zVrm)6S~q+@Sk{tjaKT}KM#h1Ue_YIY^4A8ncVY4uhPy}L~c`LCXaR*r>w6ui2t@EUH{H0HVMB5MX5WI;0t z>|8k167RM^(LvA+{Ds}!#;)33YkfX%+L5UxzFd3=u0D#jZqn78E9mxORCQqqH_biv zbjM$Ko@QUl|59soL-M+G7&^2?KxYe+6|Y=5(Df7m4l++iZM!x$!k5Jlle!2}UtsRv zgWU;}2!=$0Ywl4lwHoPN)en_Fav2=IjMAR(8ue>D8&_dLcvF@yWb@6{l{1%=Bj}JK z!>!VDx0a`3dzjrYqg~(m0uI2xfkgSJjt?IL`C+(0euUpuSF9p^63~`VDN7%s)o09DD;9QH93 zCQsZ?+rpH_jC&IA1;j~KK)}9nF?5{(WDq{Vd9tYa=hqVc^GI_7P+pj-qiEq{oe1vu z#tz?pJQQ4B+|{4KmR@$QqJ4O6(XsS;OaK1pZMLj%bLBxqduh%um|Ni2vDQB0MZmM) z)-+w@mt?yP+EO)cME}_jzM`vk4ZliYLbOS@H7hn=?aVB(=ic~l#_XsK`hM8Ayz}TF z#z&|b!Co4RgG}u-PkV~NRkp$PIK`~5Q=o}s0Xk}zdcWiS*A~EM5aFmF1+j&LD)1B6P`e12E_w@%jlYt@^ z!0VV!QMGIX(6Vus)i4qOH6taYTUFftI#@;`hM{`Cd2g0h@s+t6Z)spx8F%%AJ@x@m7(&wi3wGeg zic4Qvkv`w%D0`@Vo7L)$-dtb2-Rhve#)xa&j~Vv7whaGKD#R6c&XQ@De{=woPAZ|k zFzUIvk3BDU17vu7=nKh0!343wLe4h|hXQLOuEy^j%;-IDp1*yv&X^luPXtpD-9Ewk24ohR!S*m~b^}Lk_bp4N^U;4zekVt6_ zIX-BF_akI$NfZ`%)}WL+yV;XyA`T6Vlt`t4 zuQj>*)`AXy?y)Rh?_eUb;pQo|_XGF_7HhK<=9qkstvWWA3O<@J1?)#V_}XAKmbadA zPqIv*%>|M8%ABNdR}@QmZWR{emVc_qx@{s;lvd1+yFIzOEx$ZpG8VclYcjrZC{&gB z;@PQpaW%Dl;GK0q zO`_4c;CeW9B+L?m^O$^B>-fDrU9a+!V$O+)5V1(zq;~6}_WkZ^Xl581*r(z+_@<&{ zj=m|9;jz%3rIUT=pZ@RRk8g!FF$vMZk%)SjgRwVk!&ob;d7E(3(EY~y#Bk}va8kbQ zKz1@*&ENTCVP!a$(swm}Thjjo^$}L@tyy@xiY#9ENfPRRT&-4d^YABbiGv?9jm4sK zr|9-i^4Rx8_h#6F@Viggqjp6QWlIH2!ZDzbLxsj<3A%>UUSgm8mIUGS($M_6K_DC1 zM^AU{*oq(n8FTBA$YX+zqAou8gb|BF-tQ4L9v$_AuA@TaAI7x@4s|lecjr))9z*FT5)swBh?RQxi`(eyshf=(I&c$G?(95^T;WUXuQo7 zpe)^I+H>SYJTW*vX50bx=brt6J;0YhBBhZpk40`RI{zGgr;9ebH_F-`(|`t9`9s7p z&3on=pz4AiRmhHpX*k7KZ>pN*F^pT>AbpK(HZ|)NUL76oZeZ@KK)qnuLP3N;h|?sQ zZzo*gFEFEJ&`T>6fC6ZyOV*9o@iRu8ru={VZC=q=6F;M*cJCL_#w5fAr-`?$c=HF? zdt^{VD`3);p)bv(3U{hFFdI+`AfEfG9v5qr*$E^i0?O#J&;HCt}=a`(W-rT z#W9!P5;nb76krmK_Br|f@#c>fN^gD}ATlMONz+>(8%lJ#+DKO`nZ6lOJ61{Xp2+F- zI(sxtDh6Worx!KGXlFlz;zFkv*Whl>ew=bc;Seg-C-cA??`fV|CiOec8AqarGSeCx z*rO-!beI6cmIGieyly7r6ttizGQ^|vN5i6y!Zr3~Cp`C2kY;BqE63kf>XVUuy6)KL36lVL0+U{A^? z(E5rp7V0IZ2m@v)r$m$U2?OEvu?Y>5Nx;3XfT>)BzSLosEf~}Iv;$CJp|DwHQ%%9G zCPid9S2nWh`4grLvEdh(kb6Ea%i9Dgrxm9bT`TW%fcFVw`>xzCs-L=3jZ|YmMD2UI zNHuUx1oRO|I?&~#0S-Ru(JJV<5zCMLd~R1{g(9D|I7znn=3ILTpV-ZCT})iFaW=pD z2Zx_xopV%e>a0RhPiKJz9v*WP4YfjUTM$j@g zDZC`TPUExMZc7*V@-kxB0>d>(nsdBHMaqk%oJSLSWew56U8+G>f$Y$fn%>|2SigCs{Y&-wM2c?R70#I ztE+mQ_KUAH3ew8nXrUR7=<)=V7ycyh9P4=Su>-TXfT*$p@w5X5pyiQhb{`Mpeu?pz zIG#Ec@};v(Uen1%rZzGBS@-p&PVDr?lC2Eoo-PC`^JP2r805EDK$z_!m1+hbgM(@& zOMO3xNV)6M35_+0tGTTS*ZP*g6f5fY>0zFOuJWfkb)CZc#Az> zZrk9^N2_c=*T{GOMBX{zla0qDALs08Ej+=W$Fnj+D?Ebu<#+MOXc{g_Y^R(KvGk+I zZBA`i6H+L?{)n24Y6xx7sKV9#wR7wsobA%uWLvZSWSgJsGh^bP4LcJ0jY#RM*+9|T z>gKoKGj9!URaB^Iq@l`LZ=*|nCp0sN-CSkhNK#jd69}Mkdz5Q+*t{J>-o>I&kDW z=$nQ^kZtq(O%Zx$xJ=s3vd|WIm2z6=-|=_7=N?v#Iv<|V(mP z$o1T0D5XnfPRmby_XdrO7W}q0gs2*hZaUzyG1>c0-KJY%?DjS+q2j12d-T9OPc;Oe z=xJ>@iL8%W|7DEulLznn+C|~^Bv6~glf9d-JHumtIX9z!K+IwJFNpbSi=dmS6*$$G&OF;7-|@7a=*pCn^$MSb3>J>{%Ux1}I@#$ZTGz&X1N<<`x^ z_#>VNqfJuzW|zCw^@jFy6Koz$c2Cb1lNwy+9(PFuK=)>@e!K2+M-QKTJ<_9)q3p{! z;slZ>{6+|$j*El;IpGUKC5AM71k?+G?BfW<+i*#qHBQ*im2A8{hOpD82F3Gd0l?c4 z+ht)H67*?VjHHfkzU$RH_@S4lVm>!al$tOH zkcp3atZ(4K+OJKomoo5briwETI*om=O(|tQcdd$QD`S-a;v2l{7gTq49*o#!o~9xEi%)ORKvtGRT1rH|D>3nZ-m1M|(Us z#AO*ImKA$rl!?(1Xx}f(<9n5*66 zL^Dj6J~K9&F1QtC&|g{XJMhxYuOgS3cBuyz^`lbGNT@QH+7T`Z=mj$svTT8`Ij+kJ zE)0;O;g1xMDHP5S|H$}fSyROlu3pm^7 zp=RYb&EIgqL+DF}tXo}pTa8M$x&6l&h^e~O06zVsx9+YsBXC(8mioz7e-c#-L$z$W z(XqsXM$aYvi(qJ9|IC8Fr(qh(x_L4_X6dB*)f(W=;I_bAfk5HICM47GJ#Sg#O#i9- z-E22LJ@G0bG}6*61U20k9qb*PA;^Y_bs=8H9Cim#Ui|{+PYn_v{J=~6nj;byR^eg; zzP2d~eTsZ*;SXO!(ntGoT3x1|IBc?9xZl*)w?++2Ec$*LfUHN}kDWPXlH27kPl2=v z(AD>ULHQvYqN!>k|3C$&#;g{BZ240a>B>)wL#`x_?V+tkC|X7v38nR#FBTZBQ=|Ui z89NR2XdSvr!*CU;4P|P?IUVT2$VyQE<9NnS@+k)2+fa7;TG*F>XvDGU*Smy#qPI+&9@lDeAc9F8DczE7aBXy6I@UhwS`}i%t~T&0(IpSqRbll zj(g4o=08f6>HA<7Yl>Q}ljD2QsBQB^o-}NPLHjH|orRkJPZXIn$Ff~@lM!onLfd;j z@sj{ei`*!e+~fN6y4H^YP&gY+gUoZBXoIkF8njsY2v-_M>UOuR&nmI2YPj8E$EODT z4@QqXj<^}lQk|0KcgBXCR-z*iB=2HW|0E6qp45rf?s8hRL4AKGC&Jjcw~7+Lm)76- zYI{I^7&Rku^NrLGhiSo+q{*`4a=UzWaxVG3zGHmDNsK)G(jD;u8(aZ`&mEnup^;pc zr(cHhZ{3CUff~Qu;V3*B)KkU#k>@QJIy5t{HJ_n_RG*x9dyh>)2KdvCqG9F=h3%*z z%ok|kZgGpg-lai)+F0O#>E9K8LR}2`OlTRq=cD>kA}GI{^FE2Wg6(ING){7qcIv_r zwmw#!W&?S;Qdqw|5+%=0V#``Bk{B-TT!iv}^yrbjord}^J=S>+cYG*Tnt!D2YL-kr z6OAz&4yNd9E%lT+j6`? zwk){)8lpkZkd`MQuKBPPUd3DJz`t;n_q3lhKy^r`#LF~t;60E=ULZ-tVl9C10Q(3_ z?{+GiYlW4=5L{8;fz+l(^Y6IxZ8QMGHJCTR{*)6*H`|6K|JFD<5Ek&{wk8M7~0}>{Y- zQKS=$FUFu)?ObkzkQ(9_28F-7>4Mjs`IwxXhP4L_zFoe}Rh$<(Qh#`$AuPzecr@aB zPoV|_@~iLaS2_M`UtZZY^ol_`mObmo6Cl|3a5Rl2909#tLehtvx zO4LHvEKerqKM!FpK2EawWfrjKN<)NLePlq2*r0ie4sM}BLY6#2NIb!<#R1S8 zu2X)P9APxS7x^Ad3Uknj zY$=^HT`>u2^OCCXr!a6Z;=Ro6Id5;GcmY_Lxt&*D#3fG?anT4G-Nrb{bQO_>;co#< zh{l1F-`>{STJHvqC|@m^Y3(ekY^0~nY6G4O+@>I0`tpbjf@sJCJ`h}1NY%PKzLR^; zM<%w_q#Ndu?s_)yZN=W z_xvg+1gZ)fd86>B_^vSt`D5y#AV!M-K0gyCn3JFW257GSDom5w>`q4||Iv~ApQn%X z!_ohc3V;rnM7WmH?_1Lwf-0CSc5{nM9UAWUCtyE}6uTnMm;-OMNA+6tTWyu>9rY?; zfXv4|1a+H#;l`BRL*VU7`y5uasv!h?b4ws!vC9Z%a``_?;2kQVHf6^qW<7BTBsxx? zkzslqu^Y3=a0E{ivq*2ftyXn?N75zyM1n#lu{B=S~xq&+5kLph*~}XN;c=%5<_<=YQE{tHUF1CLcAX@5ui{)tfj% z-9~M|_6bF$NE)LQDUl^I7^6ZeyOJ0xglu8PK7;fmStd)#Iz;v**|$N-zKnJ3WF5vf zwi$-;{XEb6zTfx#2Q%k)?)zNVb*^)6VYjr6PQjx2p6{FI4V$EvKBAkA6p#K-Kj)*} z%){4x-wzeemlMMVZCaKj`qJ!C+os;jL`8<3NeNax(#s=~_Zf4Q*UOV@3Br|Uu*>}# z8(ypKg=PJ|gwC2BrPNTLAWIYYLZ)#^yO)7Q?ANp-D%2Q!JigqqD^U*u7asvlxXqC? zWMNvu&*||Y%r#V0m|bg}FT z-YFnL!DOYHv7?1q(~-?WBcx7d`>Y8ewv`kRWFPz?Kg5}*00 z$D5Z^ewsrWD}_#j{1>WjE|SV~u-kFx!eOr}rVM<5)X0g0y(Zj%t@^2zrMB$N^eJKk zbD(q08Vd=(KQz-^7)pgrmv-0g1&eXu+nGpJ9dhm^K$4@mk5x#aBUDa$K>m_{kGkoM zO9t@J>xKP0cm=5N9*eUjIYvC7=mcJaT%tbb zz@P&-A`U+~I731j);=zNW<<3s((pBZ?s|_|t6V+P`OR%j=-?A_si_n|54q4DpZXjH zLPx@wg`CCK6~UiAZOAAC6G(g7xc-`u{}W4b(8^D}KUO#{WAFt)y>~a;pq#qu{%czN zbWs>ZIV-ic8=Zuy#Q_(UP%(~ht20fv+0R+bcidY%VZu~)H~QSQLRO@YTt<)-#gP@s z60{Vgg3)i_Oxm1y%#0{2Q*h(mP>$=3d%2{&eN3M=0F|%Vq zI{JhKYHp$%xTTWzx0sOykf!UxiFPV$C&a6{dL7DNuos_fsxK#VB1)`K=Np9PiX>T} z5Fz(RuUS_Cj_&d6j-Ui^{ztzeRgQ|#LI?UEGLaZuK38SG967kgdxGcw(c@D^W$GwNX}+*fYTO zTcb-9mh4L7rifzI(fkBY?{wEt(arpna^ClpGWAhgp%zLs;MuBI!*dVd~ieOH0Va z7jj0C*0WeJc;iw8brMNvq;d?1Es3@f1PGOhFrrlSo>(x(4z6XHnCB#)LNB#Em>N>} zNs%4QRC{q4GU?(IoqVL0b{SB9`jKA*Z&Ln&)FplhI zeoOa3d=CWVy#HmC0OEh61&FZ+F_p=(nUEv4Pp+K5`06oz zg_(B++$nw9@7qs27omxx4=+~hsTh)<-*Fn_`03N*9RZ^=Rf$_{28KYkx-3Sf0v%)S zHb|xX2tBbo*3C`&CRq@CY$7YvW=M}O{v3z!&AkUT-+wmJPJZq zhQ;+!pP$>^TaTC-<_d^NSzh7wZ?Lo5B>h6oY=XKaKiN8uyp1_d*xmnwI=R3aH^yNu z6{~3RTj0l#7-Z#`G0P7oG*ip9pS zS8U^Y31dFWNHFs;8=%EQy@-dMtINzNQU(*HlvNaG-Z_ghkN;UXn0YsT69Xenb;kJ$ zW4w&JX&4h` zp&Jz-Xz1AOX)ko2gHUn%S0yy>`@aLhp3BP4vZ4Lj%cH<|l_k7WDZsV_`SLzZllmG= zGmHz3dTXeC}(LMvlEUY{-2ARg1@`w=p}&%Ze5AlJm`0Uy&)KGVv_%o&?%~PtU@|H6-l;Nv=BtC{lpPzWZOCtPI|eUUAlo;;MXkTS_ntkv4U$)=b4Y8{x@R$%%pQTVbBI64)VQY!jJLE=?PqmiQj*V*gVcoXt*o= z?oH)o|1U1`DR(*8SK>M~1rZ>wSE;Qmmi~>M#P`dvkss9WOt)E}cE*Ejuls|&d=9UP zTJpmeM7Sz{1ko+mV`qvC=dKTxWM+s(UaW(#bg}lx5A}HS??c-lWCJpGh0q^ z%wPzH#%x!qJ0CHM ze1ua!7L_fe%E=-YY0*{_J0nlVdm`Y7Ooy>P$mGC=$n9kLDvaG-!}>|!Je;_m^}bm$ zpv$}AH(!nRPu>D6Q@*T_G6UPtQ}qPw$X=qxb5^j;zFleieeXx{0@75YMuTC81=bg+K+R96 znc?jc#%+o4O2B?Sh$FW(@S>8feVL$`0CRC`c0y_0T<>-kT6B|}#@eeQj>I=r(*$Cj zfM?8)w_HB_A-RG24DjcI_z`uJ|48U}A{@De5@E~CT+5SusW1HLhMQFji>Q;&a7BuV zp?)J;%xdi15>f)A;~i71-6RHcLUpK3eM}b!Y;?6&Ip=RC0@u%Y{#8UTKo>gh2-lj* z)v7#~h86N274{ary~0I0gG}So<^KMGur?cT1u972 z&;QYkw$6WuuxQXdlh~Q|5PohHRZH!(k36sr)Gjl)SWXsm{u&qNYL(SzBan1YshCl< zZl6ZMltlgk0`)2L1mTpmcl|ZhK5;cdG8+D=b>^UX&wmWlWBf)-%~@~4g2+~Iir_88 zj)q=rMfQRS>JO(L)!%@l*N__EY8WeZ=ODIKoCd9TT;5+K&2fxao+lDzM*i~z2~$szV7>aC#EDO zB5jZVP~3RuY!JX>wi#>l!$V=?T?B*X9lXpY-qv(*QXC0owknuO%;{PH);O>jUh7*F z5=R1B(&e*Q_hoJQnLE|%+o5Q{S|O3Tmk1*KDXTd%x6>{q6sXl0B3q>vjXhb><(Eg- zvCKs=wCH-~X)P>=FmWWIdF@uU1#oco#y`wfMe;1|ZOg!mAI)S%D_sY=RecZcnld9# zv`IX^p3OX|e=fk~-1$#-vcXdE-WwIe$i-lV2d>cu-d`LfEld{H)66Ht-1l6q0`L*# zn{5SYi2zB5#9oY++4T+bCOERvjks5@_?y7R zg@K1GHI|f6UgQ_znapM7S{(jJ9ToFTwx3avWx!#w6!2Y#-%oj{^0E%7Ke|8f(}lh7 zp{6S{b=VI9?3vX0GJ%mKvloSRNBnty9@LMw8rfvT#>K;Ls!XNRyazfRTpW=} zfN+jO{(A!vxY&+;szmny+E~{%`Jh#7Ml_bisU;RDY`pR^A%$(Gay8nU?>!UEl1fMU zi~%C;=*k$1}LQAQd#0HbPD`vHPJ&pSFexdJd z96JF~yB4ImBC8x!PEBfE^JH4=C4zv|{pzMZOku%zWnp*>?Rt&bWV{hHPM4=4-x zk%+5L7kX-0rN>~~&rtqmnPl;g;^U2ObyQbpwiKgDpAspK!%!w9SINNuqXQcxHT%u+ zEECvmDHBvN@0)arFu=Zg>fW)F!s~6Z4zD#UM>UqEeI?qCI@9)dDoTYM&kB8X3+*}x zoQJ?y>Z4p`zIe5LQFUu|8QHb!dv?8@gH+;T?JI~vd0ea=n42d1tV~-j(M}*%cSg!U z^mCs?M?K$vaXGbrbUO|FC%Q=2a=c{vs$`?2qC`mromEVbI@od@l5)8qjY9FLwaxj- zW=g9nA;2z~tp%cmQqVwbdAoHO+UDO=%VYCy{&noIU`_4X zBMh$YI3YWHGqNL@uVU4^@$X{h!8g(StOV^>pSz>3KQ$iBXg;M8#G}nlsP#(+;rc3s zYn<=Z%s@Q&^+;z302O81bpG9CMjYw|`tce8?0_5SG({bS&*M(nEF(!N-&*y(L(J%_ zuwZh4i7i@qgHJw2mtctyw;s~$a*fcc8uNPq4{f>j?1VN?7N_wtS7ilz-4RgFY(Km3 z3!T^W7t=?r?*R2_k{cKFYq9|Mhg-M0;>g|f%*U-q%?zXe5y5=`mKz4_v$^l(bYbLz zG0a%_C`k9ktVyi#(_RT9T9tk^09NiaEkRow32n6GhU&hoex$jU7BPVmT12fevk@h-Obb;_Pr&LhvNQSoGY~MDFBdeMvWitird-A4+ zEmy$zIw^qXWxA?*C3!_z*@ki;T_v}K^e|((k?0mJDFHxqi?{N_Tsn5_BRpgEUBs?e z{|*s5iQz7k{IfkRUQW>Rea(vO$?Rk$nM0xw3MaP#;)E#OVGBZr7K~-~#?o~lMw;BU zgW6@W28z-tp|aM}Rd*u7b)`&3g_lcOvQ@3qY?90CfMj|7a7{wiq03+HLf-N1Ep9uC0xg%t-8&BXUpQ~{&Epm_?Zt0Z$%}PrgpG?IR12i z85)=d9X7|b&dAdCZ4Pq6Hz_++t6cxe8sfT?slsS&%2HG_(*T5=F4{rl!+$eIx2C12_!#M`cnW0coo<7Q-{&tU2p3Jt3TCqPIl`vngJR*q%TUG7S&?QVe>btvj+yPdEv?=gztCc_GK|gL<_)lDJuN|e#-TzA& z+NMaozec92w$^4&p%L`3Cr+}NYqB8)5#>wdjdSwI9?qH? zL}GiMbSClZPuIpWVfabE$TPF9`2ih4K5wt0ZS!@@9tqPVb`uG~I6rur%xy^Nw%#^p z^8LLel^!qMPxFCF|FVe%GJ3DH{o!t|i>mLLb9>h*g3Yhc!Mc;uVPt_y&I|?B5`b! zmW31^ZJ4Yx$x>QkCH>q@f#_}hZpB3v3Jhq#1?HZZZIJrjvmnE8TKZSW1uUdlTSVc; zB8@fRlD4#-;CRPw_d(cRj@6=@hUKZz+c!|>&C<{z02&)kgus3AfS5aR3IA(-` zGsGyvy^vZdKF?Y@F)XCsYC`KhptT~2b#;d$ftw@CUzx6P}EF{iTV6$2CQi>>XK zm|T_rXwqSB!}5ha1;HFhXTChnDA+N37G%vgoB;Or{brX>v&x!^t!k9VkNd?pe?|s$ z`8(Gy++jMWWGY@0Vk8~<@^~U)5y^9&$WC3=N~@zl?LL>hM_Vk}&GF6%b4xG1QcaC^ z@erH}am4d$XpPsjHrIQ#bB?ELa;GHpYmotySN^qYY}YfGJ@Fre!QWjhMlQS;{su(N zbFo=5N@^=gDI(U*>C-oZqy_TOZSHMQFKnRmiQ7;Zgv zmHWNw`a7+R1hL&BK3@3LLZGY)>f;=<$lT>M9J!MT$)XkKM>ZThBo4*vlFa=DGaoJJ zF1SmlT#5q^<#DB0IkF9I0{pX_{D9aSz1oV#jIU-{-H_%^x$6PJ!T)SdypjDD5jD!D z;5Gbs(vb}rCQv`XPN;+!_|KjqXb38WCx8e^K0_x4g7B`7w^y+fHz^l+-E1?%bpj$L zSJINx%V}<^+$q(oS(#ya(PU)Wt>sNatz5L|XnU5s|FhOL)AmCorwVG**KbC~;af6EPyB#(Hh|ETUA`2Z`Uc&aVi*zPaB%vGFUWwC>MSX8!) zFc=k!dG-1F=+GA3&R{j{iF{n2XWTtv$>ImG_L&;%gVNGa_qd+wj-}u2KNVtj=W&6e z>?j_Vm(OpESE$n0Y&3S`u-6MJ(ffkdJ6x65+cpiuAB%pwLaN zL0`YH`Ov)Vc0@+$`IQJf;d>p2TGuW4pD#dsG0idIS0a8hU->y%;^FpZ@iBPq_D+U( zxCdD8?DyP9S}m)xDInp7{mx0~EBQy%&b($@1S5lU^m8I3NqpKn z&EsCSr{p=fy87sE%N2r6_v~NGJya7O+l~aHsd5{oB=>B9d!Lj%W@G3L8Sa0t()Dl< z*GC?)S+6ukcO+S$y#Rn)3%bPihEnoSJqZ;0I@dffgSz0CwoiJO{`@BQue0XO^&uWI z>)#(e4<~=UX18%Izc{9Z=P$f_d0qrK#!ruXkZI>rsWM zFS~Mk>$loeZZTIaH&5>3XD=~}J%q!04MbS(^E)l1K~mZETgtI{y#Dn{(>1Z7NU0YDzBp`gbX|@g@gPnPufi9k z!=6hW@U7fV&Y%Mw`k8qo+w;HBe_U{URbswdsJSXOLga-GT0F(0mwDTnx25vfzmD1q zfywR@NCwT|R%?n1%e6M8an-|5GffH?oH)-9-^2<Sb6kWTX>LkdgsEb5oT~9%juU z%Q@LEpO9?qd50)KXdt^&RZ5_n8wZsa7DLVPtiJu8T~|%+@Gya~_x6G{Vpv7A0s5if z%ryQYc-1s%vk4on*5BjTERaBeI6NPXKuzHVtN;G1ZEh?MTWEr{A^Mdr;K%a&(TK`5 zAek~r=bxeqK3WkNkuq;8i*>5b&gek(?^%FCAg&Z|{3JLwb!LU(gg1&V+lro-u zcu0BYe4Q1lR@4`-DL$-{YH4}B_wyE+aoR!)&)DUY-XX;2DyOotdDr1Fks+8_a=+r7 zx!OEU%cR`G#~w8*E)6a;+C3}$CrrBZuYu9O-rAqp~7Zx^3flsrn` zqqruqkTTAS**?vh#8GC!1HNy#xh> z?D>y1o?v@+g&5uT4S#h_G=cU>OKBAOt<2!P0{x9ocx!uOxrD$|*Yz)-HH3S^slmth zwk$RXQVHC9>*e~_hJq%7j90b$x$A)dACScMgI*Z~MDz&@w~y`^i9dnVY)?_X1|6F* zL(0gfxF@}|ta)kP@RbF{Mi^Q5-nhj_6m`0kG7}R-xyI~FHZ*}}uBA3KLtMlBz*n;X z9Y6Yn!~Pa=`(2lR-OZiu_?Ttth)zSO2U(`i88lzxNWO!B;qt!dze$*znfZo zn%)FKlwKE%OSoAskUnYK^9xRYUD`8%MB=gIe<;8DSjD&1g%wX0x~pcZEQM~seunS@ zGMtY1YatuEkuAHKt@F{bAudJ*^#Ui^nJ%5OK3$^7p4Y2`@Q(svhRq4mAu3IG&o_R!7 z=%!5S^?DbM8u`QCBCk(I3(|4Zu#J{ByX%w7R^Kli{xk^aLJOjO-74cr=GfPTMT?ln zfI{HY1(#6@I7o@f6cVVj_<5v{|7@>1b~dM74zMd&VCF|+0gB8^YvhAM+y}cQ#gR1ub1^Hv zIzRY6rJVd^Kt)_*mL%=5Kt~!e5~4l+Dyf_JZ!Tdw1|U*@*8;zgDO*Z4ttM1+vS?Gu zDq@(}!i+i}J$R}rqHdSJj$Brdx^H0A1T=&TS|QHt@L&t@nb-NRrRH15`TvRvb$V4L zDfU!ap2A(U;qMM_0eDXK+|h|2ME!kKE_tQcP6R6A;Q zn~b}?XJH;;6r+V(j^Kov-xD@f>k{iN4<8-%(a+fc{HYqMvQ)q~@-+w=aM|pg*ocG; z*J?$C1P85O>yvsMi$;OfrTWVu94Fzut?i_Y8MW4`$N*jDpRA^qW_bSTvA!oFk0xKZ z`!uCRhjf_SLxh?Lpa3TA=dUZ#Ro+yv_m!9ZKjWkpdMfM`9q6g_K$=k{j{sA!@#a4W zDb|#`!t=3b{8#UgpBq>1bfY8>mRi|&PxtuXkHJCy$osvZy0nB+f7)A9J6C_q6u)$DSL6%Xbh z4iqMtN9IgQNb{?V-A#0eDDHFIHJ${?Ke03u&9UDexw12OPr!G=L5dydsfbwO>b2)6=af!eiP@>gGiHaKvhCV)pd#Lub@MeXeY^m^v``#$~s z#4n={5`Ed1O3tQGN`HHuX?cbJ739uNTVIL-{Ijb5b#dA(>R&oJ=zH3jf$m2vz;!9SYk@* z4)n6#32|Ox0`IIk-1Y1CEYUkG5Gk49$Nk-!{yYcBa|HCb=jJC|?Ug;}5xWBUrvHha z)%XNIH53Z<(t6f*?ygemT|P6Fvv5V#hSi#%C#Ekp!J-v_Q{|cz?f~@ivjmz((-ZD@YbtQ3%xJ~$T72y#07jDFy%!Tp zhWt}5Vkyyi3CyH@&l;7du9G^IAQzrrU%-S@jaL4N@4NuHh^4aR1>ils8bG8erINWw zQ;U0Z7Mzgbx$jrMxv*p+0LAk9F1J@aQ*{b1s& z1Ng5Me&eoq81p}~Brejo+CFTzr&!?m}HrkoGyVp^VTLPpN2X&;5`=xkEY|T%`%j$|qgUOOO4WGSpECh-AjKhrP z@K52h>Wx{?o7KWAyXMc1c?%(c3^G0LS1Epr{ea-5dZ2+96$3N0pDc$V$6ab(#-v?8 z)a}Z;xf@)tw_XVw-G{rftk81i!5E8^zi$&Hq~j>O zXXBS66apdu`rPXefd|*1wB7S|QP6y}zjqj}2{4T-_{5F0yHrj9pdwECvlLUJ!2Zr~ zT0ybveKEKEG;ijl&HhrR(NE&Ybw?q-0|%+9nmO6M&I*rP$z_R3$sj@xe%1BS+EL@T zX}X|YG*%U5#x!2?OXVs-?557+2k(0WTz*7mZt;d+C~f;H?vygowV*e5d&-fOU%Dq! zOaCTt2U)f4*)JIdhW7O%Y-cXrzZ&3t5Z!9Zl=L?5(Spp6SI%eI-mJJh30tu#98Odp z?RR0bkXfIzNV&RW=Bei&=>m3=jLj3`Bq0r_Wglj?2oUkz;Ujl4`L5wWECbR;j$JkqUePUSo7uN@Co{uV-rr7*X0ZPpnT@GIRR#p$&WAj~KSdj?PU$$+ zK62`jfiQLxH?78~j2WVCUSL?TV>3Eu(*V-@CodpkZK3jnTrfh9bU6svW6)(HpW}(> zv+d&qJ5{k9`TrP=3CaU59VRCT+rzk-rw0>grn#R6nSY^8mvs)bl1pL{ZnY1N^Z>y5 z0t9S~lD~B4L)yzAgI-b5Dof*u((VpsZ84?WnFfNJF`d|M9PQ*W>$&MG&<&Ex*?YHj zMoZhdod&U+AJAnzzN3;qo*$*(mSFx;)zZ&U$1p8zmy6+h-Frqta|Y5eicYU0lVR^U zkOCKcpKYg_@thAi=C8(nM-jn<=>Mmwy=LGFNQQ9IYflt0Ce1~V>7uEgZav@7419MO zE=;ekeWB`&@<$&V7+cxdn6zpeRwPv7jzZL|`&2JS%=EsTT7fw2T7baeaRTQo9MW*F zUs}Gm&bqVy3=+XNGZoXR{(huQAXmB#9rik*RYYkVwDF2?)TMa2c95#P05}zKOMaJe zd+@>l*H|5S86x!b2RCr8?&kM0SAPTV4m3WgkYo&LmNLD!;MDUEjxAJdzT=My02o}F z@yKo;BB(B=$b{B+x&uphC^cGT+_5p8C;{1Qp=?}*7u8^{_spn4V~X1l?>o}fvp$~< zkL)MFO=bU2<>s@2e@<%638lC^h;(l8Rej6aU7_x!7RPE>dm*v!rnuXVE9Tn_AK>pA zycNlQvDd%^@mjZEsvV8Lw?_V`P*fMDr+`B%$d!K%Q`hH?Qyw?*)3|;J2(@(A5O2=V1-({D}T;QZYSDdk!sn{ z&e)Mk|Hr0Z=zhwUJvkFL>3Xe~pf1&I0X+WjC_Mr#rn*kw^o!z(x*!@6H+o5aYm%KX z(9@n3ENnKSvXTK!p4v$igXa_^wzv&H6 z?^~;bYxbC`u3}+lzdS;0wA_{L`w{>=Mm&9XZm{C160=RaE({p?c zFA?A_crd~$&3r+Xafa2$#l%y`Uxw6eCpQuG`MHt!>5`pO5k88o0q}5W>@QbX9 zOXT|GJrU`EuF|3kG63(F%Kl5K4IS%vH`If&(@hNI|Dz{_Nu<=ea=0U#QFqgWc#lPJli{Wo^WcJbUA>D z@yp8cHV@0Ee2M2GEtE<9yP8*cSB?4Id23Q|036vqG8RKatO%;=J*#!dUeaJg+bb4m zV3M8Iu@F3x<$c(`lh3)Ru!is9}e2=sB;~(ON|doc=|^I z4O6to9T4vhGc{009Ldwj=C^-Wc7fF;%6iM&9Y!`*K6J>7v6~pz14fKnSIvoA5~i24 z+h@t}ZBrhE30z>be0K%tVD1;P-u3J7m8Ojh4UX))Mwvq@{_O1 z(n=)zoPle@uLE>Pu$$4$Om?pI3~dC$x6SINVk2kLo+wtpmyTiY@}(w7_E_4rzib@dmdb1P=I6g@eRH$^|R;e)0d#*H(Txm4Cq*TxQ%w!ghu zIw=Wnt}=X%2lS_zC+i<0wvHtfg4Jq20{X*aX4u?N(CJKf7Wy?&s#5RB79TXWGsqsq zQ6Rd2@Vs$z3e7smqm1mP&T&iYD`!JeqxRu3C#YHn*iMF#?3D4cU9^D=m3D@;z; z_oBb}PJL##G8LjlA*(K*-zKsXdQ8|-S&nlluy;+ZiI^Px@w0$0h(q$$Zd11ws=nZ_ z#&!tn13kQwuBH0--3fykhnRTWnH#4GHsai9rX(`AYE((YcIpd(sgQ#0taWH?(xFXk zf!yoTRWh1pwewSFigEos) zQKe2FZN}uN*5O0${E*uot(OWAbiP_IwF6GN38gbh*T^$m$4LX8srtC=!gfySneA^L z*GzoJeTBUZO`3_LH)c>rcl-CFgQ(HGpuQ@<{W`|3SmPP*IerN=5mp>-;MVGDqstXf z%KA3r4wHG)Jz~T^7iYr|#l|L*t+@n z1HP9yVYzYZn!oCkIssCwz_TnYNH!C$K~kD@N}A3Wm5TNyiOu@!+)Y0{)~^6<#-&7Q zstkapvrn?UCd~JrLI?YMWHSby zJLR`prTMgmg)Q+Jm7$(mzUhEp@R^^Z`}UCHDRfaU9?}Ib87&BmHbjZ<@l*csdQ)fP zRF@*x?B-=*Q~p7OTB#U%eCO6c(i&an;@M8)Y|p<{#FWG*7J?jA;#F@7w;F4pk8w3i8#+K1;np)!A)ec40Q#hNpu( z`1Fgw*lmu82{Z8CHRqD4$?KV|u!qtb6jVCp7j8CCcJpfzA>XLr(Bw@XQ>-ENdcPt% zSZYQij>WYg7EG|gFW;zJ&j{%wh657^{v=)P&E+y(unP-%zE#U)lLhL(#TY10R>g#b zw+G#DWJ~FLDu`RYIx?$*HRp#ba+22hE|&)ysBd)7yn8xs>!0DoX8n=buOQV(iJ1J7 zp1}~+qxDm^VP7a!M~Lp<^ZPO&(dlhp{+H_X>lNqS&LHSpDK~bcwPKVcdiM7FDbK{< z!DCW#RD)Hq;1ji(c7^>;Ht=8_TDA}sg-d5qS+cnEHW@HB#_y$dSIvrKM>GYQvjE|KtGx{iGVGwkx5M#5I^U8QA@bxvGlNQUHE_Q9RRL zt9Ca1GdmIwk`az-YD4j*7=e+Osk_7bg+}>m*{QrYf2tZERP&m?=Yt{#wlC{gUBN<| zCeUMmrKDz`{dy6O_EN48ON3eW5B#}x4Z8xg!InQ(>$m+4R=vFRYMyR}$2e?P=Hm6y zbCU&yJQauNToAe!TF|zoC<@OkFspIKdq))lwVtsMc2GlI`x}Xo<{os6d0wph#KO)7kv{LjvP&1zJnq}j>WRn6XyOj<6zqjgE`5B zqU2x}B>DX4uG-tQq+8J@E&3qRv~Yi!CEc$lbVncOAdQQ$-g{S=UDr|^;<9Wzyei;v z$8%tzPQP{=?Ehrkdm(8?X051Mc))+Rk}o%i$6T!mY$9)7Gd)nJ*zjAM*}a=Ctwz@1 zu9FG?HXgfDPHp{}JdyBU^i@(!)B6$hjtie=bKL!M#Hk{GSmiStm;tprQy1AdX)nsV zu!htjMrx&ptB9hbe*@$DwT5~kpO~w{26r!-^!dRP7CXG(fW7M#$-X@SJf_Fur@2+H ziszqBu^LbIUg(*bLNug+mdR<)4!cz%R}YG)VH;VjrI|k?nzR0ns4d%z>XB&LOaaA( z+_3_c7f#ycJtG3MLF~<|hnEh4?Ma!Ale;f|sO<>^c``RbQ~EhbOwkX9g|-xcDPi=d z6z>97q^o5shT2_6%qfbd!|H3vC8R3D_jT}6qD8%z6#IOdU-{!127#c zeTXuTKYR+Jz2tROzFQ8=Nx%FQzVAAIQK+nT;Wsu*7`>mCfv^5v)ca&}7VjJ4+a~~gzs^A<9%)R*C80Tn*o21gO z2`m3)=ckku+ZXFk0UtnDnME|bnK?umO%VWl&&ss#YYBJZ$Srccib~5Z*GH~z1Kq}N zM;kBRMq~gM+Jlo%8~Y@BZt_yNH_*)Bz^~ZyY_;u#IpMKflD)TkIdwVkfIt*h#V3c1wL^>Atv_{7$I&Y>x{JUbuR;vjvny|B*miA6qi)MGcidxC=dnHJz{Dkw zF?GWBu@X;gBJ5DjT_(wgI?w*WoXIHGUlAzv1in~YgFyr1F@hNn;TBA3&V+u z^N(WPz6wgSV|3(Kvo76i^YS?<&CnH~w^@7aEm!G8{P&H9EP*ou^u)5iOHMt`AAQ?a zb_pn4IW7@u{}6AheoX{=Z0HdBIgs5Fuq#t}_gJQu6tC<6@Crp-`-$!Y~@lltk-&QZv3| zTz3@?s=!R&f}Eps1g;F>+TZ0}w)>9Tj%qz@>C%>WdDgX)bz5Mf3Q*fLnUU29DB+=j z7jxSoRqfDZ`_tk2)RL(hy+r#);rPK{NRknn5-)8c4LgW=J$dUnF$TZ>NBXe~W2W3h z>mak)_>fwp=Xh!S2jHvDcnnU$DlghEy?9vT0=l-j^R1H|CQRsPFVC)3u$cTRhj^9n z5AE8z!#CGt;gk@qj#OfH7)@Dcx&7aYoNyu1Hobc`hurO?DessOikUqfN46L{ZR3{o zP;!m22Xrr)hwaRVP)(~|RcO`vkU#{4-O4Y~oF*Epv!711VM6xk$!L~~u99cdBhD2SLfY!Q2wP*hI4-J@gnaWmnMzTnAjT3hhP32%3VE?@>eVWvRi9RG}?`)!N6 zD3|;6i~6VRwE~+yCG00DnQPfnhg-HeM#+bHOZBYrmJx1$>M&(Kqa&e@g{ct8;}vvw zZH_-sYzC4YT*_v{O+$I zZ{FWU@#bkCu5l^wmDBa8huJr~rOU9{vhf;4CeNKfp6w%e# z9X@fTvv77Ft%wPv3_PDEKU zp)SawleMzmxU5_WQ4^s;ewiiZcZC%V@2*|b!c3L`n5wcyIM~Zu%zgIn&z>(n(>F88 zIoxZ$!v!D^@(|f-Gk~kL_1&hW37dgEJ9zcNsnU!kmz+>Fiu6Ete$oW6d+to3#KC5r zxq?q5m%Q(42VIA_-mpbZ#qA{-W6_{|a(YI45>_%9XD0J}OSQG1V!Po-uB#n^Sv7a zPHy)S?(mKt{v}%#oTpZ#{Jk`CJ>4-wi?N{fgihA2En#9hLDjyeZmhDB&38lYnRxxY zZ2Ym56sxcfoiCSoo5(Xp3AX+Vq61c#U0bAbmlfUhPekZdy*w&i?AaEu2rjvTDv;ak zzy8Qp20uZ!S5xZEF3j{jY@?l}lV>NW_;Z@H6k2YadSQgcyyuwRRRbqs$TTZ!JO&ZwgJ*F5WN_Ey+& zwW&DS;u<%;APYI#^>Y@V;&kvU)LTo83PxtjWc$vqh=Nq@9S_6SK*?JlmUM8HlONud zGWymqsV$i_?ZV*;y;qC>I_Ayr{Q9HDm{87D!;ktn21$0;l0~gXqib`uw~S)i`R^Ht zXD*4V&N=(%P%9tDA$5iFL+k`#b);1zlSzfrCVR*>hGOrM_%&@A@DDqbgK3d|g@hUd zDOJt7`7J`Ec{6Nv98$($aIBdqJghD}>@uA}hx?xP5JBIb+#W%JRp!xkYJ*tX?(Njh zLDrx4;U94eLvuYYyO6YRd8&IR7yhnPeDVLV_Yp~fZRCZfR;_PWsxxLF)zo*GNCV&V z2ZJmBz$~St_s{;9`4iB)R0l7dxRP0Pg*FE#4v|{sWw`R2l zEikA1cItNZxBaCOF)tpZ&oIhHoeN?6Jy`67bt^DjyIL`0HUOZr=i7R*=Fi60Fw|^a z?}=hfm#zzsyC~AhHJuIDoT^^GkL0-|QL{0koU52zF8Aa8Yz1Vj7`kdeE@aY+KmOzl z9dob=NX~0!{tKvqay9gy50m3{hp7V-hbtO#o0#7_g(kRzjlFKqM7LkC0V?6ripvuY#!Ox{DNk z_#X6_HO&IoAB3+IFa|`bjGXcDIuswQQ{&8U0*~5Ic@hZ>%9Z1&mZo3y9Hpuy- zGK@ntaw~}w`3ss|+|v?7*`xI@XnSHRsuuKMHoxAFVeSdI&qhiXNlg%Y3|Fy2*-3A& zj{D;2ST*l-BTGHsI-15nsG1K>#uVXi^%g7;*+u`3Q=Xuk%!mQkXY$7EQj}tiuX)o< z&}CjfEp~^)F}yRTYm=XYG!{Fy6YH<^WtchMdc%C=r4l__?_`8G_L}E@ zd(-MR=EkQP+3I{52ouy#eRS_10c6V>K8PVNR$wEI#|uvWPWHODt%XxQ7S;3Y(4Vot z)pP6clG^Yyu>Na678!H}{bga8_)#JZ7KPOqF-z)`6|eIgS_{QsJfADa4WEeHf>q}b z%wUI=K_cF{@qxn~>hzk`m(|2N(t1$=m0M5AYB}v(^qLuomO5EYZ^SQ_i6-MsvRszZ zn@Ivm7Aoe=z4;?|XAat4NgOWaP5`g>R*1jI>#KZw8k#HiI`q=+SZbPT zfBVb2oKTf?$bcwBCa2G)YN*U<0fc>B-LX3ru-e;Rn8l!0PnaDLW`q$3L#}GFw{|@9 z50DL49K>C^VT+l$Ak}qdqiqD2*I%y5zL|wn$^W|#*u#GC{_l44s&sN8?s&|aJI+~T zdeyVeS70aM!EZ{j;kZ>`j>)Rl;I+`^(Q6PW7HzAwR=l%_{m^h1o8IxO8-n|Q<@wd! z-0=Rc;W~KHJLV$n{LRI81r((5(_fn-CvB-V{wklI z=D!;H$pBx?bM;UCvyGnrhpq4ar}_{7ZW_r-CF2l^q=>SPeeB{$h|G|zj(Lu zqHu7KJ6BM<;{ad*<-0AJW}4TiGZv`dsD_V?{$ zy_+7Q5|p#tzW^*o1bc% ze2f#T^Vb}FXQdL`7oHq6HW&TK3NXlNTC*)<*^{(p30}wtubDa|XfLDc=awXHY{c4k zz0yBSk^bi-kHTGxY7fg_8w07?{kg$J;Eu)7?rYN%zqy4X8(R2+@!o#M;YN;l*MRN> zqsneB)f46WB1Je9-}%zucEA4iQN#)YKzur~n1AfEoBb6TCjAch4dus4_~lP=9{_u5^Vh!q^~+v1 zS+G>FUGPl*qhv4Q9C+(Z&a2O-(5K$fw|_ACAt)vGW}bAKdr$HgE5o6jQll=giiqMp z^qMD&!^%QDa8kW8^xEuwVb? zGjeMu!hUIK6a&x#=gz7-CKtN&;h6VYVelqpVU(Zd7q6t@w<6R#ICHA>es!dHaee?W zvv80r@q{NU=eQsx{Xurz0Z({yBr0k-^H6;#B_TH@!7 zeSz~spMv*dWld$rwyUYc;JGRn6wu~X-Z|nw)Y++I66b9?`cV7a&G`0PwAu_d6hxqyaPUUpe=xKEomy_@v4l|-rji;PM z&zCI#CY+p7)$Z*-NaR%xS|gX=jOywr1 zdr3yhuJ2`TDFeG%KIN7zhPmmeYFEr(fA)h%ssx->e3m~p?uJU%b70TV>zQvhipq=T z3yMc$8Xq0sBEfs-vteBhvVZYp@rGC1=05p-U88x<6ZCk-YeS1kp7qQPAcs@^4s+)I zXNetNc2XT{ z9pVfSBiB!#l%`mNl?ffF*YX(e&svbOqZ!8=Z~m|zx*g1ZnPGiyyykunITJz0pLK&R z+kc;)AGOQd|H3!-*Y+cYh3>#tz;&@&hncs@#>ABJBShEE32@?K*nKvnF-WlU@Rn{$ zLNGeOr8X9D;YhWN{XCVzvV*Q2!lUB}PUC~+rY(o-Cq75a1TOc(94fwA`)=kpU{cEh zyHwUtgqq#io&!MtiK$4YNK)M!VT+5eIngg`DjZkz(_}(8%FhvV z*uJfMJKn-A1x(8|_AaJT8kv!|c6FN+#G8~`eoYN%c`qIU+f;kC?U$s)e}(Af-g;eP zlb?E=`A5tcpjTZp7@LvqTuRgDJnGML6|C4WAQPacQS_}tu&Y^Hs+<+qD?;@yP#R;s zo?I=#u02kZgRV&omwH>DWG?E4@5FKLeVo>T1U(b&be---GDtxRziq3=!@UWK-zgtbC zerb3W*I72DZUfBO?%99n+oCMo_uC5|2uJ<#)kzunxTejWNxgwPUoP(-0FJc*_&lEA z(vhDF@y(=D=!07+X*HXy*5fsi_+FzyK5Bq-;)c(D8xc`1HTTEzNlGbK7gvtmwbhSb zgT{n`b?55hA5M}Wvo<0V1ydi6Ef}&6R8H2TT@FYwkN6Hg@t0&A+T&s)kIPh4%CWPF zDW-jIsEmXJR+U$lL0d#OuBOA;5LUT5zgg0ry3V1-4~4!!_noS0oD8e7$L0d_@xG0? zjsL-%FQNHh)u?HGXyNHZAnI3&UM!cyq#eLew+ZlKgWdjp)y7g0ruecH6(eIY@@;cC z57#mOqwS8px8tWpX=JN|4^0^C9xAk{-2W(`O7(5;qLfJK2G|N%I0xjXLM92BqE@bNRhwymWH^v&I@cXr*SH zAl?`_C>_%C_$^d~e8G;A^1M43nx%WSTs=Ra4no?BY87b)mNaE~^YoKYr&H)d-IbPS z&A*p^$CJhI+t2$A9+BSpss0}1Yk>t|rNpuhHtkXT^!Wp@DwB`jdUg4Dz1Li04r*=$ zc3ve>3iqVd{P%tU{l{~D@f>5LZX%DYfL)7GySX*>sO>7}ucdXvhaxYX#17_DXSa$g zVdsI3>DL;_#qdG|1@YC0(4R3|6J?|9*TcHl;NijnQ`q`N!@OP`S%?n;b}lg`(yS$( zo!Hch(5!FK3W7;A#<8bBS*&vE)Txsb?biWO|92hSa+Cig%;cH4V*LcX6{+z&G%Y6c zQcBf#du>%z-Uz2fLqYDB0Zsj11t^L3Wvz@v&3wgQQ=c#kVh>v@dK2C~ysVTLpZtth zhIjosPgHT#_hUAhH&Z2!cQbbUhqwH3pS;VYi7R_NGbrhK-Wkx^8{B=jwQn7k-YZo* zBp$wEzo}0-nGsIe>zz5Mr)wuUF+~b%jSYVD_mQPY#Y0g7r~Bwrt8Y8OCXq1ZD%u7y z{pkv}Tf7x~Ov}kED5EVe6kGW@xBN|(1@6>TY9o#~-Alx=e?K%|>{6V=>k~LKDh#Su z<}Hd#iPknNRfAB1LF2A=kr0c2*Wv59PA)^VqE6I&7TCaKkkgk0>^Li_Wrxb=kGewnL~DVh?w_a2L9`v;ykdPIy2jA zqm)mo+`$IXf|O!|7i?r|YM4JYulE{kBbI%a`_%(=RF%@d)oFt$kMz~SKMyjGBTNu2 z3qz3+wAGVutdU;VgtUWJ<}V;lHu~N5b`8QfVA_8|$33AoC#^(li?JQCmhqP30p&;& ze#KS&qb17OYr%WT91)xRDwOlBm2>vY_M5)7!6kMImYKW!9e-TQ{oN5OHIYvVC|{)N zokH9K-=LA?#-y3fFK28oW0K0NgE*m;IXMq<)=abY31)>ouS!$$`9xO9F6(NJ>QSfq zk%He*XJz1I%Sp9MxjmBW>Y&fPgqf(l1oUt9l1<*dgrh37_uZd#v;b%UFZpUu!rGXJ zKVo`rK~0lCpWZrn*JF*o3_JXM8J`gpf>VJXX@4kNyp3}Ox%QJ7<*iOeQk*!>44*1Z zv`?)m3_>!up9NR3+P3O8At*-*N}SKGbpIWBnnruqgnA7-4w&GxD+y%aDpBKAaN+D; zN-c*$l=tBJIb~qOsj&AF+XkH=BX^Kq1tb`(%l4jRNgyP`Fw`x-OUu^ zhP4_p154(~PXa;vn3_ZAh&wQGJX&5lc)@&TFJNw*x+!*CMw>oqE~L%Nw#eV{URR@k zDxtKaNf1b5;m+%!X0u@pa}kyHNCUpS*C6jFPu#1r?v=xS>$Ap)%P@lHXR7o$Tb(vG zd6qh`_@%Ae5HfoW;^c5?-lG)DS5s^PA7>uwCF1*k z`Q*QTu1_-}@>+!!vfjL6;F8^1a!nFtZ+4J;bb$*@OG4REwfU%-G8iF7wZID2*@VET z2h(a~$$4AOU#DL@@Nl0RkJnuJv-##{%vXLYy(`;yVco0)$Q7#se$4x~a@WI4sO^DOYlVUF zYEdM~PA-_lAe(EjOl2C(U{MuY8|PCP`f~#0Vku0shqo zs}1*kO#=E%o6T2h*EovDVwcs}M1pLNsDW4DN{hM&ra-o}SG4)9)%6>Ywt|J~yG54-KWX ziB-O9eXz)gCK)W~flR;-<`Uf=)?0jLG6HbwMe+NXv!B(zJB!Ur(pY-SKk&|eYihgX z%+S2b@?{AN2USPZrIi4;%c6YR@Z;G%_TtVg9PG0M{_I%QrA3#=< z?DZnkHxn;`DI4zQ4V`a4nV5*dbS+MTbO6!_eZ9hPaPTJ zt9#I1UBwp%@HmFQA7*B2(aLs09g1g&7po)f)i)?id&=-T??$XE1C3N-KK&i>NxS~L zW;2F2)BSg32`SwF+>ZMYU(X=C#FaZ+ME8M8xWo%%iBGL>3IfuWG=<>rb*&WMHi?_n5zj+8RX@gDHOSSE0mu=pTh+l$Ylw8YrzGn> zd(by9Mm-QL`y8c`@i5J+vgTLib9uR2TQ}hF)aR&6V!1YcYay)?x=!v^dJDhi%uwJW zcvqEmvbW=vu**Mm3l%KkC597H9N=`zseZ$AH{pKSnhRH@+Aa|p3O8grt&XCDT2n#; zhTzp2>DjIv@byR1AbPhn*C(LPL7)0Nt4rHz5xJ3@Tf%Tf+gEl9uZDep%B&lD*XZNU zykHX?1x4@CcN)$NNEI#h;Y7dr#%3Az7XU zV?+iX^!tw_=8JW*MQFWl+b~7C>ICOl6N%Yu3A<|LWN(GfNHcQI;`josy`pQwd=HXW z$dc^0yR-=7R` zonR{r0_*GKV*aaC|sqg0K(Dz^LLBV@Cu)#YCTTUQ*-U)RhSrmv5} z7Y5!6&6=YV70XOe^r~K2H=#nW5;8G9gpddB7GdY_4T;30KJTJky$L|pcm4lGk?&9b zAANl7W@K21?`4)b6QmGJBiHD!#9rm^A?{A4ZZ@kLvi!Y%i~TIivA)p-qf#EMG3$1A zkRq~%A&jNw-e1A;84dD;*)d{$q{cQ>kc~ro;LJl-yjBiF|JLO*kywjsII!$VL?dDBV=) zb8ZjZ&9FtGuCO|)#_wXR$W3@fWSB|tx5XOYF=bqp)omuF>o5Ixt(|@j-tHl~f{gCa zOGRbPGVB?)e0VF6`D5YoAiMYI6!#9MWyC-yrx|?bdQ8QwFetGi#GzOv#Ad8dLNFG? z7+BhJca9dWcnJ*Z=H+2Wf|2kZgE$x=hE`SD&xoE^=Y#!Bl^Jz=AyKMYHR~c=kdh7jv4DU>X?GB~&#a0d={_if6LCS+s_ReNePJq^AH3Q_ z&!W$OCebyp1z@y~V8HfKhHvg(FoKUJv7&#)NZt;!VyC(^=&GGlZo6tm*3o|@Z+>S@U#_|x`ssr?p`Q&EEP z>(2wEl`ukT*JTtxW4vSFuRV;sqri%E9afjXm#z)92JM1<*Syuqnp|y8LI#W28*u!J zOuIMXtFI(JK$ocC(41V%^W)DiT?Vm(x{|KaZRYfgnrgom=H8lzMHpf z-xPB#g#(-1^n_vT*7N#blDGdHLPc_*8pKH+2bVKX_^(Fhg{=mvY>f*iVMHSc-HN!}9m2-tf1SVD#-&Im$-5r-prm-8yrTB5RG+&MO@M}slNEzQA$o!3mc ztOo@K22am^VL)FxE%NkXQ9+CN%Oi2^1wH=y*}vOtKNuWMu#H_x5{2X_!{H4SkVp7G$U`P$9_8^ zq@dqZ{+Yv%i~e3Pn~M)Yn%2uJd$Ty_Ni4fp`(r-IfY7 z@~=WalzOYX?GR24&nPHOe2#IzWFCl>EkQ1ncWO}wXxE8JczKqg$ZLKS5f2G=iC?*(d~Ek2*0ep+jrCycw1 z_ybU(PmNHKY>^RS$}l9ZvJ*!pTpST9+JobyTKPUqPmhC*%egoQ1m=<}QGaO~n+d32Wrtd@iIat31r zv~ecg8asYy5?>OGB;0vCAQ2QmnWWOiDBf{8eEDlhqnWODkW;^p__Zz~V7n1K7Zq1? zMdn`}uu&!(8-GpihRCipeUAkFlQb|)LoYqG(?!1N>8LcC*!C00y7??PDe~|c2R27$ zO>#iHLg0#=TS7UKpM2T&y%g;rK}QxheItxzr_r~z?;^;&RJ&!-$F5!bz_@ueHEdwc z%s_)OwEt56k~G!mojO;Wn-B=nDJzc&P0tut6@j%^M_>_I-5lVO#Q^wyLAcYN2&?=T zHt@>z`e8|FR|rlvoa$%r?i06#o!`lm1;$G{y&k1o+tY)+JgeTFG1TZ`zD{v4M(8P) z-vjNyNBWVq^2!Ucu=Uwy+K}IFMZK&&HM3#(Yy6CLyjZJZZQP{=i9{vL242eqlj5M|O!{!i3)M`mWss2#Jpo1R9DOQhmw@BC_PR zPFZw^vA{I=C8&&`Xz68RsdxUY&ErlgMXwT-LN>v99RhF{6$F)2I=!K@RaE7~?hK;m zJsu-|;>yy?c~g-}`F1xdB^+;i^0QlewVtA`UQ!+SDiN6jp}oI=p9 zgxz0OaK_^Og5TLToDkyI$O{;r^LOU(22xxBKl=@dsaABpBOcrRd{$uzeLafi6$yRu zE*x7Q&4#K65tx$WMeM>imX3G>G`>@6~|v(9xB^D@PH%u<7HRm9%qAcbU+mHYUMT5-i8EOrQlr;XiHgJS#?U6L0j?k9AE`tI(x>+rsomCg0LX%(kKKFvZY_kw`r*T5@_ zqgUlD|F8BYg#@5}Adb=2<16Znpa+#u%hy}#gjNgs7PWa@#(}XvK#=AYi(h(&$FQoV z?n5pPsXck`IxAF>Ss^Tn8B`A8rD=l9as_fdJ|*FQB$Qcr(_f7ZD)Rm!PyHlT|Cr0V zSP1#Rid8hU4vOlU2LEJTOyH|kThZn^XO*U#PA4yx`H)gkKIx!!Ee$O0E7kPGW#?m` zX7%SVmcXEK=>ycV0#@sZ>yUPv9M#*(sIvA!pf1-^jNOF25ErRUu~wg7eMd2=-^Xrj zHmF3I9wFqDmR$pV7oMychzMeMacxv4Pg_RHfNUVoPt-qpKppzz&puEED`5I@f>k|G z?cNRd=Px?ze0#<>wS47Os3`mnpdZTZ!M){!`Xya3O0YxzN|I=jcy&miV~|v1-<;S2lHf zQ#(bm3z&;r)o2Od-XKDr`$Q8d;QJ~eWTJ-0L;`NfVAZ|)j1Gd+j{j#CPgkIu%+Or$+0GM&uU7UuE$ljA$3F@;J(7jShON-|H>(mpNJ zh$d%5N|B%?4Rc{`EhaY92Rnl!^nBkMkqwtI>evs8$zPy^9}l`Td~d*8Hhw4+^SDp^ z5G05ztQoJ=EcFr?4JI0B3Bk3Mem|`nE^b}j04l}*O|=W#lP0EPAIdfiwsYKEkC46d zmBx_Lt?I@OR3dMlt1k$1&*j|Jf9Lju`eMm0Uy7<~?>UGyVscCofjb#(`=Axqbs#AsKM9vfp@~DIwJfV z|6DrVGNJx=($k%a>!+tRGGlUb3hniAoYcv>UE1~|;rYckFUvj*yHx*1itfYrd_iEGAoW^rL1)g0TGg*MGzgXM{1_2JVq{0YbClG8M9!4wE&NA ztC-`H@R}<+i<@x3?L3r9x>FDf#_KbJWFd&@DKpeo=gsAz@MkL;%IKF{* zI>Lva3Y<6`wkLea49t4HXQsl;udoND_^hGvE6uJZE6wCyLJTt44$;MUz_P`;^3Up( zzFrLLZ3!7<2G@2{&&_4?-gUe1M=mBb8C4eeQS>l3x}=#16G3H<=$ZSV}HwfM$+6E zl43FVyanu97peGDkORE-NN$|LvRmYc8Et=NO!Q3JPfl>z*zV)q%XPm}W3V}FrBmVf zCOd|Yt6MrHzGuR?{^0?d8)x!3ti0tGrArs9 zp-xH8K%)CnEx?uM?u}6=FCLpBB~fy{&OQD)uETb2J)3ioy$mD%B4xjcys27Cip062 zVWz!u|K&AWi}aI7{xmaA5%~As7GHJr44s>ca!^4zBWA|^+4W+3HP9<1<}Yq2q0Jeo zex;6EjF-2n-EtRoi-RqGoA#&!MO37=ocs8?aY6BVnqs4&0qz|+87yfi^0mBV599YK zIVM`PlR27sR)}72r{J5GZ<{`liESt7j@lo6+7(Jcx)Q^Bf1M&QDX|H3e*?P*KTf$as;NfyVM_r0|{rhI}OxS8Ay2#2;<#Q|Jn46MRt^Zo(qqeof zLjC3A@R)yExcQ$Jn6TiSLPlkCaPuG|bF@=GoU7CGTZRZ`F#J*sdq%=*;9eXgpXEUi z>qhy362ghB$N=I!*Ul6ur<>Kv9>+}441R7j%}PQv+AUA^e|EkEzG+kFQ>33XRX5+X zD>TT~AT)9wGuU$Dxz$6bv6>xK%47jGMW8KQQZj@$N(AjVyO3^)W*SlhuAXnMGDX@% z{QdMp@$pY~!s3flw5iUTn{cPhpdXVyKV@Puc`wwv6)>uNJ5ylIta#j{t6JEST#aaR ziP#q$>^f#J$dD@&Cwep!;h^B;`1C_x)+9c(9eX*wsIbcD4!4HqVIG+^g zJuF9AEi1u-gz^y@z;+>OS^@8%V53gKEhA1bVbS2emEViGECljROw9HmU)7Pl&4{&9 z)!CNJN11roh+_)X)H;wY=C0+xY(ZQ!%Im_swk0L5J(O9xM_+ z$9o#a3LidiQ-Hu|Sii@6elyy~VihxaLmGbptFOYh2 zLn&^5JTyj>YdQ9s?|BF44Fr`(1_JUXJ=UiD>q8y5fLM`tU)&xJoSF=+3o#aWwPd?9 zcCyFVYTC2M5z0E`ZBlOYz-df}*!)<2xX@YNkp~vAX_E31=q-&HqDeuuu&ZG#NLQl| z=NiWLJ4V%`pp*)k^=;Oj>rPg6+WW#`EM|3sJ-)0;d~k8OGI0fM^Q)`RCp=sL^c!bSp7(G_CAK>xc|@P;T7)5I4}sDw;U3WS`>C(XG$ja9piNxwqOFtoD67 z#>+kUfw)j+s2^Lf!+~s(SgrENQtB1RLJP)!_x19i!Xikq=YcanG$#P)78&0Crv|i~ zS?7M;H4%fchW(g4u0^V%S_ zj7S*-LsG!LIXMM1uU?sTpikV-9Ulf*dA9a*HHK^s>}8A)rZY|wyf+ii{Mx+V2s=MF z?73T1e%?GoYG$OP#)y6zh;1QTLyQJPBJxLWp-Y2b2eC*%PWyLO3d!1n{MpRHAO)p21P1AnR119c(9$&_OOHc?s{}a`Q8x5uRgz_sbVLB>~rymx69Dxfp|h> z&||Ax^qJ>2nh%!En~siZ_i~>yppPTmP~$Vn?u$)FG~I*U@%XP<7$Am$jQ%xA!JdMU z{KWjl>^u(cYTN!cu{l;hoP2AS6&=@T2@{3~g%70Fid$+5zim2kd!KeKNIR@TFAN!W z$w$ohr!-&vG7V_(+yjtX+fe_GhVk!35tRz$X6LP>o8X-sdl}>1zvp>P&#BaR$&Vy_ z<0kCCxnkRfIJL91XpWw_Cf{73il~58VIDyiR(k&wx!xp7k>)mXMc{6n+xaxaB?!)E zmb3z!37oz7g#U%tOdL2M6Tg|H%Pu*uU5OIy%-t?v2Iuo878q2yrM%`*v;h`IfT=LE(dvB8^5C5uGi1RBpI{*aq5oa6E;ltb?*){mtSz?hUXinUe*s_k zSAUH2cz#P4l20>~2zrj>-_#a~H&l2F3~5G&y^pEgLm1aMb8|Y3u{~xr5Nndavt&d} zai5HJoWMfwoH-B=M|8==K4Y_G_W0Y2b=VG0f51-!Ixkgjd72uqKMZK3XpI=Bx6&Ep z!wxT3?rlw^#DEF?Dx$PiDgA*&419>Vo$%=OfOiemGK$8FxDQvO=vWrsCye{DgP*_d zaPS{&oZ|G+qrAt@(ZUeE%SZ1IllUft_OE^G)&^Hwr|eZO?wk9V6BAmSuQH-{F?P=w z#wHUiR~Ozhm*s3sNj2uPjJ4z_fi`uB`LJ8sR)xo2{BK2k?ySvOum5Hy*wU{VDlJd~ zX~9y^qZsagf^;0_!5F>CcE64|^ITw0Tb!WlOuPFbS?!1&Gv`iNq(#H)b5gOQ_VWsarQ?_CHKZC{$0CiY@mqb*oH@?gm=@Qsegb(E@I zHXvpOP-nD6eCU&66@l8P3|7;L8E7`a|R%iP%48>T7hL?f)f%3>q50+N^4vFZD)PWGzgg4+UuR&=WSk9IMIYs#LotfZdZB~ zTbXmBAz(LrP7LgS+~YZ+24g;=Q=gSca~0O^U~;F{>_v@D6GA7#N*U3&E2yF=RaXzu z0+s7Bj`b_0O|%Spo8FBt5V-&(h30RBKr^`*>x{39((uJM?b2s(5;P41{4I1-8rtSN7EA_hUhvr@F5 z^qUL`-~8r89a$E>tKL2-^iJ`~(Lm>1m2bW!s}I4c@a*~ksf3lnlaW@pOI#nd!S8za z)E?4iqxQKKo_0Q(>aN*0GX_YoWn;I?iFlMmQ-S)Hhd!^O)mmU6Mm*LeNgJS4R0U8| zVZ_7BP;9=_NnJp$(T+mIxM@bYLp}l|zx*nv*4&M&ue{&2!n z2^KSe;G^FCky;g7KwgXH_=h5O70jH6&_g=b%X%`)j}O;YtsKrqM}rUX^>+XsNsIFQ zFPucb8>qZaYw~Zq9ZJ=A`fb(a;ZFOw^nrhZVv$WqZk>Tc-Z@T`Nlmav|D-yyi$mbN8 zFx@-D+?`@!tLX{WLyT{GKCnoJz0{|`r=B)wT}fIv)R%BVxxZ+I{mSUbMS#YX7G4e*(YG=k5&fC6dX@#8A=#n!d0#? zg5Y1pshMGTL+!*7wEGC^F~gY4Hm{gYF0XeN^;)Yr=_Y1N`3aE1AFOwZ?gRNat#wav zHNVg^Cvb#-aN`eAavxv0FC<~&YT@3M`AG!hN~Zg`r>vf_#(bOKN=&%$YdBuBQ(vMq z9bet%^9N`#act|dK=MK+F1n^dUF6<&12~MWqXm%(STO7Ih;omvsvM!GLF`tgxQ(w5 zu{#>$=%eqF@?P9_5gRbhkou&u5PX!I0FF1+rU+WOoW+SR^_!t#xm!F2UGXmfg#OlA zua6rnL~-e-G$gS$h~)--rjuh8U7;RafBHwGOjvI>guY7xm?!1IfnD5-6kn( zw<^mvQxG`#^$Q!;r_(PJQ?;#wxpKX~ zwP|JhRL0_fXxACmi@xcW?vw)lOM-qh%pW^&F$7MnJ|hp$a@zyQeo7)heoI8&VTfwh ze|+$E*L(2o{XK(}7!|Fq46mpvU%kWX4Y)fa>B?}z&f zzRS9{!!vfE3RtZWcMhuI%e>myh*^J)*A?5A4-IBv;(MB*tD4^<-Ro!NhGZ72Q3ffm z13Ca8lV^{#XIIAKwpK?=OX`+iuZV1MhGmO==vN*JDCybqn|-q4dZea?$$ zJcSQjMYG^a3E`D}^y-ES=Vo?R7}8-ujq=3(6FhC~*>neEy)!!)c^dK8P~zP#OhsTz zaurxVi_yDco9xbZI~TiS?(jzcUBm%{>l)AWQ*Fw&#m~!_*2X`h2anE+JeCVo0QjQ6 zCa(P$9%V#>!$>W+B2Ve9AKY|5JD-va(kVZVV;$sb2nCBlB5Bob;M7#Q&z%C$>aApE z?Tf&q|1)Oltgr8I>1N}wv_6h!i!1R>k>XYzaw_8}xj1p|T0@ggxmbB`Ro~efp`Xc! zyy?!XA8)dx&J&wiJ@ioasHNczj+MA1Vw1^1K8q=V)k=KrFT7uvG%CF7c}>0xwjC!_ zY>)5G>DzcZPidL9ltHY`M0s`3uMfJd-OrWX=_|-_<|TZ*y>uYGfjATb2z&vj%^$Yp zY0nHT*&dk#0O`M4Tp=8fzW?qM^wgbnfD|`&XvV%xlYP}M_@n0%kBwVMzBOtp;MJ16 z3oQD5GoA8Ru_EnY^;&BNkjLb^?)Iv(O#B{<)>_%oZJm7*)M|E`D|;2|NBIOfxdA*b znVax8>FuCK2VuC!+l`ZC!-+Tin|wFq7vzk@-(OJxccey0C8#e$@6V}H){~_h(O1=i zWg{woy00`TF0FEYWwla(H+~*jH&p+af03vxc0I1SwXlnCpfeG zvwqocTL`M_gU-1j`yyU0T03V9Lb=OyU@`6P`AJqmbWxZ(|2 zKo)dOa|cG?f~L&0kVABDLkbLZzzdFJg1)7tcF1+KI9*Df2AzQ zh`?7pp0L_i2Y+Tn*Kk0Os>yuZzIs>f!vQFg=;Qiq`llKC31wX+FP}GBuWI-??`A@? zCvSiVW5IpL`1{G@J-^R>Dav0R|TpR1knKf zdu*#tE0dg&NsG6UfG4BNxNQ&sI`M*V3YkGEc9tIsf!Gxwgzw0e;mf>d9)a|%s$YtT zK5q;P99^0pe}O?QPYRdtWI_E%4;3maEL1$zD!*yd?CGd@E?-HvMT}yYljw8^lng-yJZk6x zV-+=|>zk@!kJ7NwI+kVv zfx4~#kOFP^`+GeJ#q#;s@tVw|u+ivbQk<)e$A;rbdC-imP|VkqA5HdGe&r6l)TPL^ zj3xtR62B?<< z!SbTrZo)k-|D#W;Ln5X0-+{lhDc{z4LrZ)#6eoSCUFKcsQrftW1;jgr&>r*uJ}=nK{Pe{>Ndy8 z(iLYI1DaNX-`ZB+y%V;ImDt6MIWJYqDn=CBBZJIUFad20m;d>td)QC*-!vu*d3jwB zw6r_hVBKfzvbJtc2^U{1ez&(z!!cTQiPwZmlGJ14>9h$T_b!)-o@|-RugZ z`8xcxIj%K7^-lozh*Vx8#>ok(eTUpl?2UF#$M4rYWDnQ84sW(Dxn3hxnR1M;J!IlqzIl3_x7HL_M%;~AOv*TG1- zcG}w*`w~}k&N}ISj5qxzfR&eRRy9VK=?GWXN{|^@$X63eJc)OydhLc*Nt!AG@k2l8 zj(gY!uGkPl;VX#{%G;+VA33n6S>jYuqVbC}Pe(hSup~f{3>W2g`}4A6wNJCg~w z4~Lptd^?3oqFmLF=0cddD2!HR<$qENcOES{`<3qIF2By_!q&4)r9x|a#dbM2$>$Ic zqXUgIkPTqDq{00%AE_4I(Q}+cj;UO!84?84_IWQV`kyEIXE9~^UDz_+HS%;34B3%y zuPoXzQ`09o{68;iaa8SD^=Uc%&AOZ0UjkTe^2S>vvz3Bo$}A8tB-QRSgc7A8>j^w0 zPr!E4+1ucfk@rJX>3N}_GG-})3fUTN+2M)M3$i6Tbhjznw%-7L?b*lCAc3d zz*^L#4{x$qSZ@d=bv2J8No^0NzaQKhK4RQ&tJeLa zGjYRNzax!Gl+iw#{ABRHa(HZ>v^OJs5WsV!|Dq|AKsO0w&9~n7lO$KM(nY>IR>quP zfWI1hpYAXYg>3MIQUs;Sz=ozb)OoH-{v%YCmpDS`3jzl5;g@^Qok72lvwa8z<3fPW zC)rR})ZF}aoF097cqI|(1*cU!liYBp|whZDHa+A6>xdnah)QmjAS^l00PL+P90-04H+?D}>v=wOiAdh~GGO z$;S>U7=L{REK5I<5~D>K5k{K=(lI-5tAlBuw5+Um7&nSuK{h77{*cgVI8fQSb7`c8 z5uqVCU?MAws}4sL>gx%yG@|S##Ohwki1I9^-Owkqu|FVMiLCt-5^-XCT=Jv&xsNMyiC=sLSY6AuBW2o@c!fD0PYNurfs4byfNYt_5;{7QMWyTr@YS1>*Dj>&UC z0v|*>J7(6o_qTpfJu>y&cboUw8tC0QyUDQcwU0gT1gs`y^5<#JHT5Lu-M$-HiZUT2 z2*R%gA0}IY-hiWvLW$R69-4nbewz9044-U71>_*Kk#jk903X+zYv(Naqo85inVq~B zf@z=TS2}x6@6O5XR4Aj;NabGmsiTMohvGpO}N|ZFfI>QZaG=wW=7F1>Cg69 z-Djo6F|IIm0l3w5YL@G8ee(2MY;?iGM{aX;<=#xEV`71cli15fjil`fYi6(}uM86mZ&`OGRF7 zYJ-QQy$hGHo|T?QuvPN3Ibbqg5_M_6DUEWn5Fk6u)VY&Y+7T!|OQaIi-7)9$n zkpJ)*VN+dL&W4q7+Ihi=#xH7|e>SOmL6+vsfo-jwc(7@f^^?i}@5DU-qG-M+gvyQN zNj3lj^)J9Je=+n!omv;RTCB1^Og&uMwJ{u>e9t7J0hA`$i(##V-cbAxu1Q_NmDB7$ z|K@zt-~8>XDUV7C!Q1NB0pmDY&0Lxal*7E?i*~2~2=_|QKB!Yq_8$i9?{u_j5;BFoqZ$-ZXG5K?3dV>cudqU=jyFo^8gnQTQ7#n|_K z8)M79jN!R--}m?RdtT4$`NyB*x~|VT=Q`Ip@Avzxtg+5(_8{d}lQiy?G?9Xf9RG-Q zXR9tiw|_w+`XlTrKK`aguh#v!)ZsrDXkuFqAvn*T(-nSRyz6!)wnt{I_CFY0q#5DK zYx2rp9bsl3GP1kzO!;J#iUg) zLW#@{QSI_B-8(3*4>%-b2}G!?h|d!x3Tb>wq@wU@XzsM!n~-9$+1MR;pk7nado(ocyCt?Y^}btz=Rj(YK(9cB>p{_Zz=^b{G}487p4ouc$C zx%4D>C_Z-{sLOa)x0%p)lyS4?dH6wSLHO+hRj!(5nFbB?1x%B_&(C-U)RQivx9R)O zBzVhfT!j+C3`QyN*0Nh%SXY~MN$E9mL!_@?MOLE&ZS{GqP_*==Tw7V(5T|-DNb)HSz!c=?zB=)96jr{WIgiZM#(Q! zgqv)|)5f3U>wDOK*PxB)1Bkqe82M95QqP8p+9?1c3;9L;r_)Vau96!perXhN-HsdUi&V&AYCr{ zOleeHL_$Feug6|463Q3mD~?zNH`ULu-^@I7FM9;tAS761fRe{&4woK}=j<3O@F?4% zUblzg54R|7*goJe@2+S&JnJ#0Mq3Bf)l8TO&}fhPD~k9 z>oh858L|8tQsp3F)yJ0@=Q!r)4pB;Q}T5%rOy{ocEH zT*adAN;}BPGYgSJ6a!Z`!uG34b;I2|g3t@3H?MYvhZA2kSYFQ38=<0cFSd1F_f!8g zabMjp&GI12#emHnxHty=SyjTsCR`UIYN=EGI>(aMUxMO30xh-2kmxz{kaC8MbowqH$k;YC8U@zop3alNJOE{q9pHjHg-`Coa5{mav== z_j=LK8`I?D{p}6$+KWJW^NylVH@cIU*{v8Cd4z}A;;b9iWuGr)p;kZr_AP*pDIkZ% z+aKV2-(->B&!No;20#rxYpRaI2zW$%g+I7>`oF8;ukxt5Z!7I{IREN7a05FuuC(KL zN4#otUZ5lK>ReLmC(Sr#Seu`jn~Z~A!nQR*$@)Jn0K|EF3$d>`Z)&Y%BgL55A>rXK zVB&P_{To2Y|MqfEkrjl!h=qh8-HfDdU<`&F(ga-|cU|4zEXNHhAOWlXBe8E;;@zQaWJ8w>ao9yKyI+P#tl z5bOc7k{f^1V`W7}V8N40~VZ$T+9% zCNrk#0M#=efZgI!slPbA_e|~+si)qsK!0hW++ory)88?`slIbqYcQuW4_%IoF<#)^ z7Uj8BFW@?62TlxezUq5Up`VdQM5ap1v}mgVT9d#c?{QhomK}%yoUgXXzSUmo1s@k5 z_H0DxQjOi&ahAl`OVWTIckLW=EMs6KZpud&aLPVZ$xR{N_cCZ5L_L5}Kf4L7c=eW< zc~nI~pbpBQ*#_f+JoKEd6K?Z{t}dqReS0Gfy;XhxkI&-(78s>odQK|2W6(P{x3%@o z<3E8?Aq@jhQmtP4FjgD)2v>#JT=G=L*GB3d^Q;|iKNJWbP9rFFD}0QtFrVV$`O zU-AnJo&R7#1y|onjK_Vg@gJjOdwY0RF}WyKt;hTKJ4SZ*EwVGM3TJt?TW#bv*BO|v zYc{!yD%eS(DJuGYUZ#2(LlGwfio5E^t^5$v7Mfjz7Vq+)^NpNAZaSJ#;7&h{;HIRG z28U%7PLLXfbVqf}=>{u|jDxCP{mD~lW9yw*x^r)|HPq_uf?XOen){;`9>4cRk%IVA z2F9{bCL-}8B!-_gN2a>J#ZKa=<}7!Bp@O3U@u_Y@BxEIg)Gw_9-3W7B!KU@|VUoY^ znF_h@9n586!aedPP9xoC)5D=TADadu>?*%6ZPe?USyDD^Ia3~AQQ^4-q z$3f!0!U&bVPNgz{M$*6GFiC$a%!tiRzd;rdl-RAT()Z@V_T6lbwR=|qsw#S4?RnJv zcH@yS6@-(0t4jBIvMvJ|oBiq^6@2HT>}lNtar*@g+Q|-?BLqaOuWAtD2PN|-Sd$mEIVeD zX|?b!oHXG*&tV?1qgH>bOgw@h_sr3ZP)1(qv)MF98O%^xPkwXbR#gY|Ryv(TpYqF~ z1fN-E0R-q~;0=A_a3%h~Lu${)4VEywn7;@8~^qJ7TKkB#Ie)0BKz*_ci5X_hU{X#pw zkai7vzZHPB4`;d)00F7aau5vfUl;(zG@$uVgEyG4Kp_`8W!C+n%Ng;)^`nS%UTck0r%}bu{FiNCa)aNLp#AgzHfj$U7n|(^jY#qzB@6DK-Y!h} z>jFMXP>&rn0qS8XUee;8x6+y{$_h(V!L=kZGkLb9zq#Vai4vZO`RPhR6=@PLyvAt; z%L}3v8wZN*7@@tz*13#Wv&e$k+=gI-HTK_>*;`CWlkA0oLidX6ldihJ;UmlX;1%@7 zn++AAWze2y|J~iH`WB?T_x^h!8aN_iYIMq_4D?e1GUqs6496!K=Te_jnIn{)%~9U_ zdR?H7<)aG~T8dbHTjhem*&JigeRxZayeqJMH9*7(SR#e2?(z$T084xX;uxLep;@JZcB6di@rr-P_-9!%Jsx(2&*p zOWNV{AcQng9u9clNCl@OB_ijrM3~?{(G9GX29r#?4sw=hacWFvtN|7Lfbx4i?z8V_ zCKZ7IwaeV>OxT49kNvz-35?XV{m8di!TzCpm@nQ<`XM>m1wr)p^mI?pV9}?Lgjvj8x(d~|N#v;my~sMS zH*o<(?)O`~Kn1;s8nOj(+Z#td!{)O<;nnh@jC&H-Rt0zvR|H;MV~V4|{p>j)!s-|j zf`VE~57@|!&Gy+PS}=kbzIEMisDpTiKwC$zA0(y)T;RTF z+F0^w|Hj=FiEmEB?s@@5tF0ddPZEq~Xv9l~mg6v`8dDDnKT^^KOplD%T+aNCg2?iYZOx%** zSFDdM%tq1MFufe2SvzbF)eK5e7^^<2)DCzuc%e=IDjRu~2@_Nfo({*i^ispe1f!q_ zy=>n(xUP4WgZk@pwq=2@%@xpTj&NuM&)x8JG3GBOz2Q(-_b>Wq_d#j&n=Q`er0gYT zKi9Ea5Bi+_L3PfTj&t6UR_sG!Z?w5H4I9$~1OQ!c}1< zN=twqeQYN~AIm;kAT<{?+eXIAe-?fi9Py-aYT&ogrcMAMTAg1lN-_P$Fhs`n=Z?Gw z$f2JoDQ@jjm6KcZ?y^JMN{%07ct!eTypEphWqN877f4(rxS8$oN0i{KJzr@f=_9inDQpDD%)1pP+z3o!XWncuzEA`_U!5Zmobk`qj8d$k;@MV-Y>FAw~-p zRdQv)w^K~(U0mMVvH>#)B}_9j#fIX#P6zdH)FDuPUFOg`h%GOM8e2;Pl3&Sd=zT}( zu@;NXex3ww!$StCUJDgGx1w}~%zc4 z_3QDCm$Qz`Q!gj2LuM{}|Lwm*RBwI$;4vw!BBOQxGqoO?gw~I zHocB;W`VJxDY_#-c}WQdVV~%IoKIMLh5MSz`_;4^6|qOA`2FJUWdp4qZy5&AUW>2& zNJZta@cvIplgm4gUtu`O4npYCfnxHT_3|00-o4)knXwOxwN)VtzB~w_Yl{+Ova%h9 zSK9kZc_){Qco6%kDFOh!v0k4!r>WJ0Sh21#Th0lR9J2W(bW(W*JE20;UxL;P!g4y? z6H)YHFU@k@m~XyC{OJ`OaK>6zLEjyC@u6*d&qz|l*+_k!+|d3Rt&$O9PZ8xPbLots zTJ1XfeTmeDO%e7GAcQF2{=Ci%-{;gO_D=_fddoDlX zpD1m|;O=#O-yrO{n(=A3(A#_otAI3zWBWK$@vaD}FwCufv@Xn~w;X~WO38JW7PYX$<$bpY0% z*7dCda1()CvEXKMyXc?Lo zJOI3EEpi?%R(XPax|*SY%dhAmY{5Fnv0wISQp<2ik;+;!KEsJ$hohl2yq_lFgJ{1; zb>VY{mhYmDsueaSKzm}%`1yFLxi$=+Q1Scw>Z?Xk#uovds)mW~Sc_?QZ`ma$7+%jN zw`pdh^&?+t#~QfLA($qenKuj^8b>+F^j`s?e)3ZudA?)wy*D4Eo^!ijdEPbs!<9gl z&mtp?3DS(LC4P%;kbW`!8XMnuT{R~tVh2wR>A|FZ-FC$osU(+bTK6XV&g!{K&6|fv zC2xS3`ikEky?v4ZE(@=ef0qWLMH_wL zOw1h#or4h=w0X5$NbCkp-RCAYlfuZse#y9T`UGphz=rq_779Ss8VAn;of&tI=xl7c z-hwJ=jTpgr{--1RTUYGJtYE+UX7RgmVH!4OQ@}@G^T2v6Duc9{+ z?4wgP6M>)@e!Vj({(-@1%v8qjcmI?+$&FajoamA<+SU!g6GPH8+B*i5@yYX~`! zo#}BBxKJM7kq0fi7I}c>Ml^;y8wHt@+*~3kvzj8zxNzgxl9`pql9ep(xlL(&2t>v< z#RG^6zLyxTE#RXQ$D9=|?|Pf%ygGO*cuCVU zO@Fih+qMDq*v|3sS3w6wYJ|k#P`p~Z)ZQojW!X$- zC07MBdQ!dtn6rCSO9{`z`vP`X6a}EJVT87ri(f54)w{BKv8ww59NVlg25DH+!6xvJQX;fV()B*R+j|qFS9<;sN}_s=SGY$;kXKt4FRCyW_qO zodeoC>D@aCR?IL;_Nm8_Gqv~Z(zLoC$hxiy8W|UT1xhAs39QX31(8Gp`n(p9Z|0x0l`U1XaG!E`@#l$rn7jZqTuKz8s93I zC!|g(;yP-=w*7^?!!Hu)3eY+sP`B;%WF-HEE9zJDKC7cmMmJH*K`nLERYRxPlTvXQ zts~A5HM@|V(~eoRGth78P@Q082D1JED&mQ%D%D8aATcr0YPRjO11rm#?cK?%8CW~( zEE>D(j|SubYIwZiFzs6$Nn=2=aX&&DA{B>zxa{@2C&8q`JH-dE6!Mc}XC523u~PKY zfbF};p$$inY3>aS+k|mNsgsQ4yo!x}sqUEIkc~!K8}-=705|3Bp@6*{LV03c)8GcaE(U;X&Nx^FYh(`vi!c(vKmCpKJa4g2MQv`bQT@9pu4oZ2qpLT+`z@U+#~ zK#1h7!IaX=#kZ=rk{F@KH_y9!Eyt>}U6P2?ewG`Kbomac$+!gd$?}C5#u~ zWdTbn!i(<(1z|Fm0ik#J!i24kfH+e{h*+McPhqJ$DabX9XZsUegI)YAwN-5+EF=Gc~y9N^k8P5Hksw0OA`?a`; zsVRt5-o#<#f|0i0G)tbvUQJq&$FQBiQyXjP=liKN!Ud~!yY++x|M?uBfq+q=N9SSB zKK0Qpx7OsG6Cl%grt#`@NCoJvfke+&3u)Ut)T{h-959$vjxAKA3DX>yo{0hl2DJAI z`Si**h!}l=rMhXLNY^VSLDC&lV|^knH1eB}hkWxZDFgM_DEG zOOz3rw6^Pz@}umJP1NwOcBT*5K!BW6kw+GqfcIB3Vh!T}95=&zR1(ufqjR*mc5Jy_ ze!SF5sE|7-eVB0gHGm|)U0gM$Ten?sH;+!nYT%0BVQgCNQOgAUA#y^nvsmk(sxxnV zJ-NYX1-m4yu7LMT{}zo-h@EwptVCpF{vJoWh^W6hJwlpei~D#4^seB^au$o?h1k|| z9>l@?gW+%QBXXH(DwTgmTZ^m1RHVuoZ23mMzAvraHFoEY1b$L9OMUP|2GfJZ$mKn9 zG4dYnL*cxSUCCVq47Z8yq6@Alr=%+|_nwYc+43RJ%A&h&-B|ChV;pbPz(!50Lr8 zJ2?S6(FE*LgE4wNc)*a7%i!2GPHGh>53doAv!GjGAm9LLFaIFNr{SpM7^o7AILW|v z=N}&Apm94NugU66wf0M@z7UOsB8vMp*Rf`&(RkZ3&u63UF^?N}im1kCL>$0%|p_xW4ir zJkksdI-D-cXKVQ5oquD0EnvHm z8>yA};C>vA$-0k{M9UGr93=K{)wp2}6+Myh^{kBD6PZwxknAZeNLQ5*^h!v%>+sbJ z)PXSkgp03hJ;b4VRUDDA>%F$7PM9n$ zx&c899@$Uq=;ycb1s=%#32JyWh8PfsUD8DLc>U5o*5P8v${f29QtQYb@%}nSktw)V+7@Lu%6Ggn4dw)i*)CnH@5WUtlR1)EG3#}^ETNFEX$SbTjXb^ z2K>5Ct3PMbqBAwacG133oqT?bsZQj1d1uCchRv{fzEc8!aBZ4OfxUe1?&jy*-XYr4 zX^HU2p{Itdxv%6;9Fp_&n6GitKhZX!bg(`j~$izs9IGr%P#N*?$df|NCpjhKG zM%Nc2>!-S%vL=Z+IAUvf{7_wPl(_UN|9GLIPdoLfjS$+DT6#T=+H=&~Xic}B!9azx zqy|cAG58=At!b3@v~wt zPxq#V{xq*|c868Y-kKH9v<1k9f4YJ~_MmUe555pF!ID!ZO~(y_^rqqtAL0Oa={}^Y ze)^%#5CglGuSvy(kh%F7vZJAxvt|byEm{%iC1>t%5rShEZ?=DbjV+p<%UQ~2+rP$0 zj%>r0ah#b+Jzho|j zk(!ZO&8;UABQ3xj6X~Uk*ZmI3tm(Y$8f$$IFuoD^ewU2W8S^E8#j|Q9dN*Uw0>5BL zRzv@0{Np{e{oe7Za`bg7R*G|ED@-~KuRqrOlT8SQ*O?*hvc=!657@V}LIn_2qdt$e z>*Wr62p=ZAHMTdnpzh}{u=)S#%s)oXXp*WP$}W!dEVDt=?PvUnhI@*xbbHn=wLF_Y zDQP)9rDt-qi<9a^^ef(a{*IMq##$g(Feb|WakMIiT$bVIGT+vlpWoGtNUIYb&=fG` za?8?yn?UhV#ctgvkW4~M{%R({{}*=qwcv}(+I}O8P2z9@Ut()A&<&(U_qR_x&J8pb z?>_MgQ!f44nKuWaJY3WU?5xfev874b16|gUvg3e$dNbU9a4FK+&bw3b(lBe^it{Iw zG+7II`*lw8H}Tq_V$PQc&8|ZhSS!y=o+S0o=E{@K92F};da(fnuXM2pn(CPxf!$kWStS~cdc0yF@id+GHZ z;)lJFg_l6bK%df$2azA#b?Y<`y!Ma6faZhuYq>Km;YN&G%ECYhE)Eg(0{=~v0j9Dr zhE~ibSLsv)tK|RNod|CDpig`pX_db-WlFw~lwJ4?7INhKVyT}nWna0Y{W&n)I^Q^c z>AON>10!fQx*TL1>w)ZPmPuzKB!)nK_hwjTX&hj);lDbEQMF2}@2`F<=koJ~ZK1 z>$6i<|HbF$E(6E&Q-@Q>9(L~A_XZvo?@it8yBooN=yYyzSMGD&!IQOsc`!hLr_WPn z#`ua&lMgrI5Y2`j3PoLe0619^jnB*~H<&N<*9nxcF{E$#co_+Aj4AKzwraZF=)0X9%y3=NoNzAC+e&+H=}<1+{y;{mNQ zWh%J;zrhRF<-`t`BK^Z1o`dhQ=AFa7G(Y>} z?VSe$XRJYqvn0p5Qr;?xdHAB5m^8zka>2YWKyl^@j}6O~ z8~xq_gpmDS27y7br|u?r?)%Pt2!q7uyqCV6!Xv z2_-G{4_9h8JNkN3eU#jX+b#4~#)CtI{`m1P^mbYC+Oe$GCg}v}0eK27cM@3T z3%Ol274u$K96f{${OKA^r;%Ak*y)JHK^o>v`hJYRp?YO?=21g=rzL6PYnOi{gl|5U z)=tX38wjR(u{-EQn?dwrTQKvR57&>OK-2a9_g*v_bMZXuqeKE(IdG5i&49}K6zE6Nk!ZF;R2+_-QOzs>~qQGE&s z9&J^51b-eiZS%0G!`X1C;)TpTn92)I@7XTGs&Bm>#H+E&=*wFDMgZflXMX=zWd3%5 zt`&*y?>3{~sp&;AWWcScqA!NrjjTp=Ya z=gNZ;fpu;kU6?4q$GJ4FUwT&s#D1R0EzR%sxqO$-D!EajBWb>=PB$)u38xBKvVzxrN`?t(? zXA*9z%GJnWLR2*V$%Bzxn)~I(wz90cA*nb^7h5&IiZsAzPS#-vJ>T+bM`kJH?OBmx z@9KUR`Ds>3ub!)j@&-oe8s;z=W~AOJru^pgbD%$7xfl5;+5ZjG?_HPe1lN%S2U7EBW}GNzHKKn6DNXl;^>{XxZmO@I7({=qFL{*quhYs`e?hMysF%T;2WU-7~MDSP4u}Y;ET5 zRb2{3R9%GRQ{yUT=vE7Oy68AI_}IG-?y!d(pAiFdQ)XMK=Xz@tO=l-HgK)|_o(lNG zs<>tLa!nxiM>T%L@$@y0?$*pR@b0y$v3k2dm2Bd>L=%u$YgTxz{{k@dC8pE64X)am zlClTNr3+muTU!-ACqSE2G(X0z2Mg-WZzkQvYqXi5$Spvri8Iq1hj>>7#y)I^1=9y~ zM0>&`&bR{si`t^mx>%+}$L^~Gw}oJokLp$dX@3xdx))*s0fx%ArOgZ!T2kJ{_V!hR zSGXgS(8k|yoz8uWnv4+8RcxQ9-R|?D5?gy%I(CAFX}J)On>*{?tK>)CU8#{3Vt2)| zI$(B;&2Yh7)NAP6>IX_I0Bxm@d~WXAhqTpmuNn7M+M1p);Qrm*!2VbxU5&uUJW2a{l;x3Kl~**{vGI=X z-upIRfa%#pg0c=6N|#6AAdODG^(QKCy3Jk7Jkj5~Nu20Aru$%}OeV1kBx3yqE)V-R zcpMID`@pIasNhP!9*jyyE;jz!jGC7KJg6YJ<|}4sgj8HpB7;<=dE%9~nuwey^40rZ z95#DWYU7OYOs0)Y10!}rkClssZ`b}cZfafg&^&+f z5xa^!l~eB}CpTCkAA*~?MoxO+$-_HTJ=w~5`B&Zt=h4!FH?E+xJPfX4vyZMZ*lBJH z=IX)j*D>9mu}fS4iC?~N%H40OE$2pJ?W`BqP7P=j4+br@uA2zN3t6VS6iy-{T!`i0zNgf={==3GB zr_?wL8c-GkEKf&eedy~A_Y&wLpv1NLwam-p3vl>4xcAP>1V`HGvAsZxW4`Q_Zw711 zfl)$0CM?m7Fu;N#H-T$Ep(mFa*ll{FBYYB*|GuO?pQ2Lo7yI3||0~W!{e%GIt4|FH zn=XHBLXAG^cy~)f4L?aC_vfH2WR})P6&ID$fU!o0D%`STm|LQPM9w2&n-n2Aa;DmZ zVQQB!ugxohP;b4l+>Prj;2w?(DbIFH0=YEWN9N2UQP==$Bx`C73wG3bP46HDxCD+Vh%y5%DH(RRV3}2J7d`gn0@)&ivm%oIe=zt(suOG-LqmMAuk^X(J4*8 zZQn6}$kW!D|TKC=v}x*K~LQrkU%8X|Nsa+@DByjY_mJhD|n zm<7M7WX0$^!3Gl#)2}MkucoX%Gxb@js|IX^H*2a~#L%?c-Vl9>iioSOet6-Gu^l^Z#T5uNqNQ zvs93YvRjtt)%9xuP6&RF0hW{V6lkybNMPvX`m+8%FxCLf@g>{4Ya>6fOn=m=@y5kUKeR&! zgy zrj4TareKB&n2~;shcJu8VWc-fH&k$-*k=h*yBbV$;GD*>5Ut)<7zu`1sc~$_?_Mib zf>DPNwBSV8=Vg>I81yA|TfbIXyCrV{D6HGETb)4qSwAd-c8WOMFMQ{3WYJ-K&q+p5 zWP=%L;Y;}(IZ>-%P{ZFrT*xmeFS?BRWgV#@s(c9aVPy~Tc;&*PjVY^CSGVdJl1S|) z3rX&=b{(s-JAs|I0v@&QQ=SrYivQa6SS$$Zb8=HD<$$OkC^~&ivsQywB&{|X!)!-d zNU$BIclx2W$%U;+^`OFI1|2FgV`jK)1w_*8Q$I1ggztE@VVa^qFYO(5dCBJRKW zI^V%yx+ej*E_xm6s9TqRJ)T0q6q9#WBZ$LgBwpF z9mI&?e<;Ahasaco+!_gLx?S}fqckex9hBdt(V-mt{O7ryPDHW^t;J)YFq77leIM^t zdUm8Yw?*a^^LGWcLsgk(n85iFT9W-#v^rzLW`n<;JE%@s-}1@jMm6&a#T^b1x54=TJpp8EwL&Bgeb zQBi(G3rD@DdrQNR9y(y`2zj1c>qG}8zJXEcjZrcSbeMb&fP~{{?Y=zbdt?11Kb2N_ z|61BPwQQbPDa@lKl<-0?5Zl*(OJW#_Zn>Sf#8(*Ns)cCb|Mfgg_^6y_zw5Bq)qWc~ zuxnrO`y>3nyGD!=Cj(%LFnnnymJUN~^p&k6IL0C_^M&ZDzGh~V>Ei`iqi?|46^hfv zALW+7*f@{ax7?W3X!T)o!11Vl`U3VH5osrac0d~xkUQ4fG%@i-ieWiN!59r*X2!+H zZdTvnml9CS%N}2#m}WFE0o_}ReUMhk)7JS>(gky)WR7&DL~3<-H#O(twI844F1I`% zs7BF4A5)TZ-u5uVHu7E0`BYW}FSjrJUQ=&j?|5_NzYt&X`sM24YutHQB4n)WDnjek zgOn)$f#SmhT)(GZXHAnmGgP8Baol(Ex}GHKGCaxwF#LVm5SsYO_e&^inae_#Tw#qn zDwWrVLN_Yt8$j<>r;?t{F=*hS=UVgY4LIXPf@=$WeWMm=s13Ibx-610K*FJHC!C%l zWT*c%m%mPRV%Zm%PLK7_V1!V@Quo=qS2?qRReqoGbcgtNqfqv?T zYD{k~oSYUSF4tm{7pWFp>))SAfr9q)V)#-Wtc^-y#dhend{d#@VYi=DaIDIB9rL8k z@DLP5iUI0S@N!R0$2JM^{D$e@obGQOn2wBKapA9D8ED3WzM*;!8-1B<_9s*}P%P?^ z7KOTY^ksA0Di5bn5{R$-xCjQDAdAzsZ&^6%l`_rcJ0J?hSlIaQnm#oYKs4(wtxgX^ zJXd@>^=VnH2Ql8!@bQ4CsYEJAVkk2APhD;DVg=B*g*G6BLFN{56j3-p=lD(wKN*+Tf%$9}Rn4u6_6h5a z2O=C7s6(}`JF0{1TpWBZ1Rr9TH0lINvyb7?tUu}AsUq!4EL-hiTV%&p;cEtwvm7G(=+ z(6#>4-~5JuUZfW!Z?#T-Ingz?vtzNu_H{y;``U*^0qLWXWguxC%&f9POwpe>0~+I0 zxv~Z++omhZJ|i1>;RC~w9FJPnx24((`y~E9uBBs9^N(u{Q=`uvPd#E-n;^Ud5?MWV z29!y6)XdGa+&z@>#*|CNWp7+{HQtj2%nb@Lg%aesn8L5g zC1#6hEP>{xz07b&ihf#q_;8kW(*8-G>3jId&f0WEy0O83r*i)EHlS3U7$gH|_UPxN zax2kGZi-&D<9nm~t*N#`2~T*j7=~lg{*KLG0?#UDk8+IT$X3AF7D2A5?3uWRl^`=w zwu}w?o1m^&(VzKI=5NG!OyUNVuZRDP6;({+(g(48RvVxvaWSK|HfqcdUg3=i+$)3= zvGc*NSUoX2tVNGGbdhlFZlq&>Wi{utqRf#eS?_m05xK>>7!G{cEd}h(`BgAJ4AV3d zOUv%-tZCCKxm_cjFfd4rB+-1RL>g0}d8N=xl_V*MP+0%Jx9T;ja?phBbp31f6gR;L z6~2>_P9J5YufCird7l|l^8UP_9lB*e^F*Nu#3r8?&wnu1 zDP=`ef4%{6uT&rTR(n+d5%)2bmFKbS(%SWY{d5k?+$I2FREW$vV5o5<>JXo%Y>12Q%{WlRjap2FNPVQwbd>a@iol?aA7p!i!?MXB4B{2)e1vcK=2fcqz z^QG4;w@=~*5cZl8w0raoCcPc^!VJ=TPpcqD^jh)k{Y_U?;x<4Iw~*T`O~dd6#_Rk4 z#(8k}H&A_AGx>Y>e*hY^qk--=lFuwCc~u$Fi$&V_$1Nh#oAqh|Qgf9D$RbA|l01^J zvIRAZ99Y~p_wV-Qr<42^BL#HVTv&*p9wK=bxKf1u6#5hNW~vm2-Tg zu9P5-1-L1&v^(3>b(13EpC_S&p|59D72;Gf zGZQoZR2i-6l+kLPI_aOpr`6w*3)9DPtMmTwF){y1J@N_=oI`O0KfwY6D z44N^1y>S9B%mtP#{aL<@v;s(@m~CHs#_=Dol@pB5$>h(zyW`uzmY7Q9CnPz)V>^RT zCCDIXqg)^T8zs&J@+-+xL;OKW1mkj(?h z2F&Zpj4`RK6MB|<@s444Ix1=ZZ98A0QU<&rC-TI=&w@v6S}7P&$$E$XzXTlgY=T}M zw1G#*1jzbVGZ>*4f6B2K1(7J)wX3;lK7eY+Lt^5Yyu<;uqNEhuNAvV(pV>RS4yMoI zv`4U^@qlPEa3&)Y3U1u*ng7>eaCG2x2VOGlky!u_J|f0ZLZFMbrJ#mzv?i^zXU}c0}80Eo0PvWLWl2u1y!Gzuj%Skv5<# z$7>9LW&bm-TiO(D&Un*|23nLBSI%24Kruyd-}w*ONSLS7E<_5dL29+|dl!`4cYsiM zZw;s>73~f$kXrzmdp^p^= zc|8o4>r%bONiv;MYg#hCif2l~GhSXZ{G85v{ndlS>)n(D$PL_yZDYjWS$ytNMD@=<(a|HLz`pF}zl0y1YK8bQGlAeG$Q^jsi7imrwZ~LS z^h>b?qqXM?9^6W7S-^wOiL47!4r_`Qbwu~$sA=IAX|T}0tBr~{`5_~I)AS`bKTUh* zu2f79nJLX&-H_)%mhw4@ae21f5cRRD{y4ZFQv6xti(P;By1KRLQQ8slMyT1VepW)S zUk_}tgb@C@)28t4MkLYA?)Gy$EcpU6eh2BjyoRpJl9;gcXtd-mNOfG&+H=(0VvN_X>Y&Zu|^{m!0H@GkRkhMtZBdre2s zc&=W|$BPVpPM9o2>C|ZoM)^~L6u#D4HhKN6pjr4Q`Lj~(7!wa7||L;K<3VG7W|IMt&kIwJo(atWE9Xx-xlmu zDWJ|blT(u>xsy&q7zKKRORw?l|yznsEl3uE}%v9?ly+7U6M=?z!cf zVo>|eP+4Kss;Q|jDe!ljBLRJ~5|Px!@a3vaH1I z4#+RmEovFKNfm!2N_4>#?~tQ+Q@k3CUdp$KpZwW#qC)x`3A4k*Li-x+9-sH(rw#uf? zTyjjsJI{KML!GyLc9-1Ui>u^RbinxB_Y8l3{xeV(`e%{g^BvMN;$dHF6@+XO@18RSoIToSPdV34RvXjsrU+3dLq8C%hl_7tFVwtg($PxJ|>OvV19m7bZPv>h^K$A{+se&dkQ=SzQ!qIQ&Ge8@V-&K zU8dZhHW_4Np~^(WV@ zvc<@=(HoA+=lCMlf}Y{Oy)!ATdiL;(O5Zuhk>Eb77W-LbaI6b{I{t~&zlZ$bHSq3q zXrM61FUXJST)>pW?kc3yh(A@`prAthEVJqS?>zlZ&QrujI?P=fDnY-)uMhhpdg~T>!`_xS9wIAv z{gU(G|7q_#gPPpBwhtg8Qk5nhMVb_qDm{uw4JarbL6Ke}p@$j~P>~|N7ikCSO{6!G zP9nXd(h?vbolxJ6N1vnf%s2D?`Tl(KF_Q_ibKm>kYwfkyy4JN211;7IqW*Ds3qis;@;(0ejx8mrG*(Y2ddRlvUFV zE9I0@JdWu2jH}DZm!}OA#B2q~7z8VtlbVjlJ#vm7xxNwy61Cl{}^3Yf+`8^{Ixsu={xE6z@$2}2d zxJ2iWJ-#pVo`3kIX|J6OFR8Em)?=A(jAVD;hP!n5M81aXd}$xNSm9>8Qa>;(;%uN$ zSw42N*KaQOUAo$HNuyCc zveX=8Vd5uRa-C|>~+KDSAqy89wcLMhgt zx+R5eHiNSH<{jZSI}U5hmsaeBucNIg(qy-=TE=6dxmFF(L|r$qMkQ+_4z5jU+_Qq~ zN9?_nrAG>tq%2eXT!X4Q9AA+RU%zFouiLIF2W5<_G?ZzQ?TfYdv z8ch~>w>8S!)Vu>7mPt|#f(xs@UCPz2iK(l;{cwiV8BsP}?MUq53ompDopQiMPR^Fz zHhtwmO=x_bU{uBdsBgmA?!`~_S#tpONqB#<^MEb8V~4ULHy0frk0`NhH1Vu&l$*?* z*Y+G98~BEg7yanHw&fNPTdBXkv5^H`@>izp27Gq8xgUh$Z2HY4@D-uMWz@Mx(>Zx!Z_FrgT$(+*VbX*kgDWF6yv0?KJ<+h@$g zp9^frDY!k&Scls-BwREXp6iMEY?h>5(ASVElG-b>vXs3|rn6kEvGY3g!ECq2+Mry{ z#=@its=#zbro;QobZ=YPu`gD@^z=_%Qt~2Iqx*`)49m%YgggG&-C3gkH5IphEO_Bx zx-lJh+{PaTeW%o}ots##csZ9tQy(K}_&(lp<7`%;hijqQg4t}`Hg2Nzl^ATe7C)&H z4jFDfRXr5a*})?ZD|3aBRNJ^sbD58q#~22#kb%RiY#L-_mPP3cC-^i&y2$hkTiCru zKjifDb>5`t>VR76lJT`9&XRs8N*udtq;JZ)oL!dJ1wP_lZCsX2CzM|3iG?0}w2xY5 z9v@5c$KU+v#}6N-C4^1w_LE)&wlS217lse)!)r;6EH@X~w)K_b9p@|#qzq4+AnMEQ zrbO+$%ZWX^I~6T<{a>1RTc)%aKBCZ1Q((CKxYTwEY-JR7ke$aEBw7xY2lkH7mNtJ4 z(x0iEoW_F_OqnnSuYKA)U?}(0(Y`q*Ow4^->}y3OH~f2@j@oJxJ9bCi;0Q@xC6y0; zl^y0_kX#38%hbOhiE=oknJAf(@aWqMiMhXB?ad(#A}tyl(GpY9#AFOzf)Yj-U6uwY z4OcCjLRpD*+W46_#N!XiLp|kvLgxg{hG+4l&eir6oK%%5sr??Xw0?6b&|=Wb%CRYn z)S%Ons45~s3b_oYe^BB^)9HreexeGFXhKpd=PKrn**82Xv7bViIxBP^vTTpCd8`w6 z^z=kFOnH~x=QWYDtD7dyX!926&qhTZd27UOE3tlcD|;Nf!Hik9h!*RW*0o7E4mot? zL>7+hVJnCTjSKh|Bz-N4dguCPt%47p%A z?GtbmZP*)^Z)#mtKI>N4V~Qhn0};(6c9R{G1Y!^CS&LuJi0D+rc})GWW5VlW_h-(F zBM*C}GJ;yt%-XC;cow!-1(nzw6YngRRy zG3M+-W~pVD(TcpgN6zCFj$xJyNf5ty09W=IZ{P?vI`LO;C<$src2U+z8;JS!_Qu`( z3)u+bP2&^0f1>7}?S<^AH-V*f=dXehkK>+28kOorFolS{p!GT&=rWi#U{ znfkEiJkAk5eBRr&SI+${(d66Cp1BatLa@AtC+G%eyAHc!9MWVTkD)N53vK*_XHEem zPAp+Y_6s9YAv1}k_GhfaqHKo>VwVvvnMPIc?Qg}yE=v;1A&U>P@_Cl6n+UaMV#^+7 z*hyTxr(m+RIQRrMOEwPeW7wr|9wRc(D^TOwXZMYKWk0TZ4=P0JtkUp=bve%=K_Ka`RoG4o+*qFfj@Kp7T@9}*^{$BG~nXy6+ zYy#CdiW>3ota`moKIzY?+jRnad~gPkK*8c0yClEH50X8;;esHW)%eZI*vWL~zOreD0-<6TBdb zuDEa*>U2sTDnycW*U%=#t*|V_Gtd$Jp4P8rbPUdMgbDX69KSK}wedxP6PTN>%X2sMlZPpEq zxnL%*Up`gcRaKb-c#Ho~A}5JDN%o0&F8?R95}zal-loovJ>Yj4Il{mG5b|0yQbW^a ziXGp}C?SIS^1yC<$OA=A_?z{y=$%|RJWlCMW4RpFYU`$khnr_<#xClhVoF@+d`=5# zjp~84V=U18x4Srg@+h9t=+Q(mtGlR_5d0z!+NEk=%VzSis@8836P&t#q9xC}Nf3$c z@sRbS*@;VGf1k|DB@aE2kp9~%z@o_{mxl}qjRh_a{cZpd6;fbQsyrg-Non&JPmX6` zqE+PXP~Omg?`}k10U4M39H}!w)4%O3X@vOk%C42>Nv)E{#C#9lMP=O8)8ielTn+Ct zS5*sGpG%&@%9VMd*!U(Y)H-7X6IXH&kYskC@Hy^3*6~xnv*fJX)dD6ZS`M|_!X1ST zMVxiSWU=l1u-6d^<17LU$-`I8oaK(cF~>{U2TpjmQA9*XM_*s4Ye?k<-O>G5cgry3 zO2@(-<(WI9eaFlFRA~N$6!t`*scInWvw!XE286gz7A=ckR@2H-HD{E;)W+%Zx|yZO ztbg2qf5soqp@&FX1y%*AXZ5D+J=l$bvL|`$>`RG^gFml6XN@HOSHbZ-;Va0MdJ?v1 zke;=>=u*6TDecjDA8+^VGQP`LqIHYhINYpNhVkrku()sCc-+9Jk(RilR4=nTw0MbX z{e)!LsC5c9LY@8Tuz`c=KBv3@sqA-lSa+krFo|_<(_sP|gtay8s^+r;Sc4nK5x z8Ff{+SMp=IiACpfRYS+SLE9|&oNC{PM?{h(hL*Ya>Jqd+rMv`A`mcmd998KPtZ+w1 zNAy^Qt!nT*boRMW@};5BI_YiB(F$;uDQ4QRGfK#weF~Ah-tVk$v+Z=?$eLw66F4MX zU_D-ZQP-j;{wm#-`{Diu;$<E@1nX5$4>hXF`+!iveg$1m~Ux9%Cr?lH_ay+1`Rdo67`{t+ss zl?F4kU>82D6#8as$te>UQnp)nb*TkTYP8xVaUj=NO{hMtVQZ1Q?sz&(?dDD70WM{{ znJ_i5&O{v}1#29I-yRnqvN#q)HDix9MXX`bb4Lt%6{lAU#|xcS#}ba`h9%dhM%Hvz zW0)S}uo;>S*S@;FtMKNhIZJldwX;-Sr~M?CjA1SeHxRRZk!n_DZ#(q2P>4EjSJ(LE zlk^XS@#s9EnntF*Cb^Z9#t8B?*fP{aJ0#rINk3$R!cHbmdMulxN4 z!v;OqU8(|&3iM^xU$kr%h_y<^3|-LJbA81!tRlam(i`V(u)mW+nmMW7^Q4H4XRL;1 z-@xjEY{|GIypOa^QY@0D{$Oa}y+%v|xB8T8{{yYXzb-sPLKeKl4tl6D2a@;B{)(^w zvq+=CcBE5lYu~Idlm@pk%*Mv6uEGYmnGG3@gYETq*ey z!HA(35cW@Nkn0ppuhm){$~8}YHJHENq?9TKW!Wew9xmuAxrkztBJNQyhggnpFFHo# zzLXxSix)fo+D7xFGpzqfJL|A?MXIZx+)TOl?5+lD^ZOFp14Pn9k5pwb;r=KgFherc zG_o^y9Dlh*jvioqZdQr@1@lo;{ULPJlQJRBBpRQV9r8? z7`xyD`tq&KY}r?QH}T~R4-qYkyE9|_JA+@iM9?A3$5jFbwF5Tw7&(UIjm858aACXU*n!b-J0r&jZ7+p$?^cs>b{2b*5w8F6-L}^MbD~( zjsbx}vE<4Z9_z(sL7a1T{hZl{X=diR~m&nL|;Z|fO*~hcat|4KrV~+FI zlP$fLZJ~ttHeWUgF?G5EWF?O9Cefr}_Qso03tl7{YKbQ(e(ct&sN2yk53^Y-$z`;{ zY%|h3dC)a&zo7=^BE?gpduMS@Ygmy?fBQXH;Nq+~wO4c#m|Et~*^uMl+e>d-8@l2g zQ`&?t1C86CdS3lD;mMsz zkJE*h0g-G4FqbLqe}G}15r2mmg**_EbvdSxpXff1*}Q%k}$bq7QB9{X2+!U)h5wv4bX)zzG4mZau^l7Mp(6 z5Gs$tdIq{hV@ziZ68|O&S^7?}{Um7I)neos(jv_%`7x^+QSxbN`=QaTU3BO@*Z7Cx zE^w+i2j9Z?K^5;$->Pi;+z&VLk<}M{VbCINNMY@ z5;%9B97ezi<49egaNfIl(U#Qt5Db`m=@_I4d6~ll7o$zI60?6}F+f zREqfX!l%8>sU5Srl>AABnUXdZLD_1zXw__$Yz*^=xV>l+q)x)BBoR{ndKVhE|9(#LPz3Gw1S%if2aC})zZ=tjb z5<&<`eb|}v?~RPf{?wQ9mQ~GO>|lOj!EnK|y{RDcE#m?VSIyE@xlw{sxK+-Iah@K0 z_n_iat#(%cH?OUICZ&Mf2+Oqe!EfwuA{nol3~Q@iLy{OUx^BY8FWLH zRjnswG43J9e)1syt?@^%#_kIiX4DtG8ubYv{@v;Zsh;fwe7 z%Zfv`*bZq~g3>KB%KYB!H;ChI7G`U`c8s#7+mTsxWSMIzOGd9R}#U;$^MdMl(>kZ~4W>vkl4Rx9Z zo?@dSBW+nTyzuaO#>VeMrUpwnZ6h%a#*0IRT+uz2eb_ABY4IH@^C#$l1-;db!!q+F z9etS1sA#vfW^|Sj^xEV&or{1Q<5u64uvoYC3qjl7CTXCD9jxqa+tvJ~M@P(WQB*qD z#au|nP8Lrq%{dHwnvILO=hzC)NS*o(2^a`u#|TFNNKI+KIbk5+9b!bQQ3A<_Fkm;| zCtB_vZK3igdl+LXtmW`(8E9;u819Zt$QX6L{bf5`3A)sG5_Vxq+i^(`Ak z0HtxN)f?oIUf1vOLO~L44ZnCJen%!za-oGdDv@04Ekw)!+sBPVEEd&W84h5V(o5oP7K_ z$i<#<38S&;x~@{%HYPXFQo-U-Ot$owxioija1%L)PEItqxIl=6hei=(l*B;~ta-o= zV~twGcbZFR;>%Fyw7jRgkNUnG?GvUDx1ReBKHXTjox4fRfzE1k`6BQbusn~M?_Kf+ zs>V7AM!0Z;ftUU>_*~F+)&Yx58h8h=Pn#R`%qU9|ssNruVi0J5B1%&&SQzs-5}ZFVIT!hnv2Fi^k#ExSDPBpf`& zUr47mC*j3ihSic$%3o0VgbR!~kt)T0Y&jkhD<1od=I1`a$t4`=K-bUV*$chPkXUO5 zX<0GJyeDVTe5%4Oqi%1BF|=Ht-!5O(Cv<)lA)vpVNKOED{R3?fxJXOPw-0Qms&Al2 zE_0nsTd@=R2C!(y<`VsvKr5C9MTH(hINz?0~lh($ra44y>qY*EmF^iR5#Ch;= z)e%XnB=GPXUq|vk0x7vPdI^F-(F*_Rhkv|y%^6w_bSr6_ z1sB*M=XZot>Ydoz_3y$@v_YQ?uYdtCC1##1tn;pvOgTVGjT_?pj^LV?kxq?|K_{<} zu@@810~O!>U#mFwBEzfxpXE9~C%+a{zZ9KW{RJR@v?9sD44AhEd=vennV<+%Yw$OF z{%&*-4h5hOMtJ%S=pW6E{9qZp`p)*diBY#xgW(4n@oLcg(fl1uNA0CQ*g=9*!Y88t zzcF0psGXXf<5mK};<1Ax;e_RX}yxRnaX+6j(EQ;#!Da#X8{e0U0);hc|>rVMB z|K$WKPohVyo`ZjGcE z#(m5SV9SGc@zHsu7frEAJkeJkkn0!1p2bj&6Od6~BRtvMo4h_Vw`1NV%ckF#et2$v zb=vyT^zaXPzoXa19}$+#cgF%UqjGzA|Fw2j8J>A5`PmfNDZy1)wpHyDPj7uZH4>Je z80D(O!ky!~%DtC2)t2hR=d^G&T&T?@s^2qZ*-4XDf#XACOBh8on4Tnx4B~2+C8yc= z1(P>@S!!L4M|D~HWuJ5|3@|eoyKZdxXCW%y_12dUt#()js`UmMN#X_~Gh066p47#& zi|r|l7C*``a`uszEZ@4R;bB4j;&So0SJMqD#;2kPRp z4pC*OWxh5xr~B>m{XX$r8Pmtgi-{(2D6CW91L=dqu>RSuq6U!)#3+1n(EpahJO3Ce z$qO**r=&8!RdwK*-HPw$YKdJXx&?_KH8Z~`Gz;_sPivqv%N(8-eH_uGw#@7 zb>tq$UdGK#2sq4%!wP@fhi&@-C}&EhY7sQ7>{JMo<8fIW04!M5ebv}2=Jr_??3ywg z5q4du*qYT34?Cmz7tCO^H#t7|`Lt~>PjT)Udo5O-yJw4$J8*xij$gB6bnn&gTlub# z><`6TS9|S#UVyK^G@gx$tiI7@i8_Q%AuRKqm!D>CVkf)w>s==i9_ypAnN`+%u5;!L zH@f0)rtuOK#E(}w>FQY}N_wVPf4$bb$BbB_(0y)KIK2RN*}&$WiFtse=bYhu?84|g z1BPEn*2F**W^?s@{qfCCX=Y)6KP3n_SiD-$Btg`Mx^zBqRXt?wc~PcyqGbv%JoAkZO+rxX*+~ByLfgvOKKhi@8|W`nD2xp&gU7P ztH`OHp51pl%WLOQQ>Xv6S$9#N`kBkx5MME$?{6%F$(?IlaS_LHTLy-3l(B>gqc*3d z<_Wdm!0u!4PN(3KTf%&TtH@W~DR*6Z0VBJ~4;YHqo`~%LUGuqjgYa>vt_sdTY%(uS zsj(7*#IG%xP|2G6s7wj&9L|a7W$Q^itbZI02>yg}VU)j(lj)A=ai!w=nBi*2o9qW4 ztFp88bJFG}_s!S{8VL9Nn`ltvkqDP1Jwc{r+_R&C*{PBn+f+|jrCH1kZVtSm zl24(g$NX3{sqLti`tzJ%)qLH}&aI0xv$<8Lun7P$X^szns8xD1c!^KR?Y_a zHitFMDx~cqNoB-Mh7=mtD`OuqVRQU*h2urBHrylFO+S|mW8P!89YKxuGKm9#Ipa5R zv`c{ou>5%H7fKN6fZYY(l;V05Usib8hHQU!^h{6B3-K)+;-KVW%@FhJeKvP>o56P% zD-+piozEJ`JqfD4*_h)emzNLi)m(N>+IkUCr;l!ko{%1a*K(_xF6s0StdD-x%2sEr z!dEw~Z)}<<^3&Hsh5Sz(Lh9eB7N7Gc9^nzbSq-LFXLy8tzjP}_whyXXq|c*C#d@)T z%9<?#x9pDD`4k%KU@WBqNZr8X z&NwQ$H>o*?S7PJFYKF4uVP;d@_|+MOtP+pF*)jgBr2MdBt%3?3gW%@p5E;U4XuG60 ziWK5R`2n!Dg=A#-KzeN;-X%iwSyC5{c6TdK-kcmz`t6%gQS}fZN%QcpGJnb zN?qHb-e>?ljq#Mu-7vumU2J2o?I(j#(*t}~Wu^p+-cbO534Tv7z z#+!ZeevJ4eGqw48RLoA6jQRz4+KiWV4iDIJEKVr07BSh`Y*M0`4NA?9PrtsW#KJU_ z-j)*C1H_y@Mg&C(*O8G3uFBDGocVZu#O$nq@Qk;{!J_m={@SbXi!Xmf90dW#p?x~{ zmV;s`*wL-3#dWfRDP0`y)mi78tHjS@D#K8=djKYT83o5a^fshVL?&-Ba(WN7%Z{P~ zS*G5HB*}lV+qp7-PWi)$w=wgpyYUUWA){ru0e(Z~QCp>_bxo@?tI05VOS;50-~BdD zt-;p~n>pi!6#THAKGzsV`TR)BgW}?50Z1cPcxr=jIZeuzB=g7d0YC;uGp^bKhEf?& z@e(L+wntZ8AFg_SxyNTxpLBrlWiBuW%TW69X|U@n^*+{BD62;RR`uxV#0k(kmw!4j|1Eb z^Yigz^NGo0^T%>WwV1LrUq*Oj)XwXy8;B&2Pu6?gmu&AgQ-V63*M8e~C7YdlGbBhq zABm6oKUYG`i@^FVJVpl4V(o>nkmKCqn_W)VsbA>XuuB#x|;Nti1kTqaO7LIgly`Dl|i8a zo~5Z+G&0uZFAMHd5hp@mOH$ePN(c8B!=(#F#9*F5+~_$)St8-1$gBBC6_kirw^Ce! zg=Zhzw6l6mvIThwAa|Rr%V0$dbVy~w8g*1|QG=KPeZoa!XH!pBg+io6(w7E;S<3Fyo70qdHp-k3g1e6j3f@YeJv)2C zYqInkZS)!zQ7{T|Tj8zREGCisX}&uT#m(Bcag$wph1FcnlVFo8F=75z9$&Srh2@Ws z_u)ajW+0^eCwzP2#kS4Z0L&~WS-fCio<{7QZe(xUdxnC#I6DN{z{3)t6P|BGhjMPv z>!;qHwW?eRjy-x5{duf(G+Vb3o^3t*=y-onPZtyClm*Q>Cxz|#UK~3e@gQ%g{a$wP zE~ugIgnGCt=XyG(l|R&%!YgL2B0@g=5p7K513h(8b##wvG4p;Ty@5g0em6 zt2}WRO((Sf(alzzX8C_~bK94GTUQ3;wSP67?1HzYdYdCoPgvMLXm!4v4!uTY>-5yG z?k+9Tlu)W2H()LD)?t>F^IlN@1xcGEY`0~+i`iS*Q;RoDKz$iq2{v;I7Iy6Zw(8B% zpv~i4l01{5!Y#7^%8sVzC;Vs0*VSW$fX?+?`+Chvk3e_*3PgbwjYPqleih$h^iwxb zBz$g%q1@|Za`AUR2K2*s@+UQE2~$K{K_s4il-yveZqUQE_hDhtKjBZX-RlzZTVg3$ z4YVgRZH@-7Kcc$^(s_cz%`m)_$aCZrhf@YKb=Ws9p|rY+g6q@6pb{-VaaND#u2lkq zZBNS3?IkLD^)peKey+V4RCtMNEmoN65k0VWO19hfh_urh%Wc|6i$9YWl^EM3q~zx% zHu&ma=ly{cFP`Sji@ZYpBR|MYk&)iv0*k-(_QgMF1dc;69IPAk2In8mjj_NO8E682 z)-e84WP{uU-#^Nl7W?}8k7fcBkeHgN)BawXMR2MQd>1X&XZX3upUqzX@SHnUFznrQbb(&utEmO+GJqp?#3%~Vi8|`PsQ!X7E5*y7G zQDOQuLRQ+9R=J;>BFkp*O0Fz_=x@cWtI83Rqb;^9) zdbY1rvHx%8((`{zJA|WxFgR8q^56icRrJulMk&BXgcd;Ae}=8*Gc2&9hX<(6u4^ literal 0 HcmV?d00001 diff --git a/examples/geo3k_vlm/rewards.png b/examples/geo3k_vlm/rewards.png deleted file mode 100644 index 4b1c6c0ce3eee3740465c7691aa7e44939e2a20c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1475375 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

m{(c-fS_2zXZv>2 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

cT*^tIAd7EBX$Q9*PQJ5IRchhS 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
o;Mf*6V!LCu#n#tnb1#egWXp+LKW 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 z ysFYI_3W-q8Lh-H5E0aCz3=-N585hxxNGmE-peVsLD@xl)DL{gUFGayBJCAuc zr`}pfZIT5(T6R0kS)}G$ZVadjt{3IpR++pQ zqQyuoIo$2t`0~2=({|Q2PqrOwErw)kV=G_zS-{)c!euRQ)=UfjrKs$%uE6YCe0ccq zm50jit(W%I*D82s%2CVvVN)m`uWtCo#lQ68s|71;4e<|u_!v>(jHlSaYCP@Fouk~d zB7QpT?_*oPX9@D$R@JOyPn>ju%H|I+@8oUd?C5GuZlTs1YBeRr%S0=!#mX9^^K7yq8wC~p^*~#?zSdN zQMY42u(-oSI~cQ-X2C>#y-SQs(n`@%m`pL5RM{;Tw$I%j47t1$T{csv7|{yV?5#?d zlmF3g4YSmMub$rd1xlD;0o6f&@XT}1UA}x7v|s$<7g;c;-PCJ>U}=Tk2K4U)GUB3j z$o;EJZ@&2^I-CFW;?=8&ytAEpMJ_5eC33$>cw^JKAd92q4)1UT`30t8+xM!Mr10u8P0yMfB#j8QMd&JK+2yt{TUP2-A@@ z@t&&kO7l!`C`q%5bb({whg-5nwdEh^Y4kh)^I!jKJeutAX0JOsn7;PToB!Y+|D!Kw zH)YF))pZtKIycQ`o`xYd**evem=2aSeFF#7xt$&eW1{!POvTu;9<2*{8k}^%AYD4H znywRSJIKmx=1!wYucO_j)YN1n8BSG@buyMLomSX+-div51l-X1yPEJtw;6NoRqm`= zolsUYZcFgq0Zs!@4qFkVy?r}Pd+v1=qC29E^1`S^ZT60{30#IB~mf( z?2Se`PVnE^*s&*py0qz2%iI@#Pf}n-$deE1ceBT~ZZh$XvJ($_sueypvzv6eDf?pb zPwCy-1o3`~VAm6F`scn7q!|ZU=+o369_&eQ!aS+Sd$aNl7)*7T;K>6%^ zj@oSfWTxw4uJA@S*TTAtdKxdJ4jzavn%sz&KY5;GJf) zM(trVJ<1*ZWVOtdTeA+Or(|PofN3u7DqSPfl`sx)eE!7=@y~uV! zfd`HPX(}$#1qHsf6lhL}Gfns+y|`m>7I*KOOXzMYRqmrz zvs0sJErBS9foeLc)yTEx^lrd@>++_%kR5>H)rFiUyp80)ZHTrG4I~XS65}aOw+_es zM3wx6=Q>w&`w#i>@b|22n{vij-eS3ojXSczZk#VV&FMGaMIu0vAu>#U^I5v~`DxN` zpD5O9xs0wWW8FR#e&|zuI~f~UaJF@8{KQ{ug`zl$-9Srj&AeBzhzn2E$U`<^UY1nx6WUFXkAe)q>zU*8vN0CRBw1^0i$(2-4vaLNd-u3(5XfT zCbvP%O?kM*_jheO78g4=ZDPVXFxAeaok@bZTeFD`R5$y>4J$Ge$w^9u8nOl|bzQel z`yx32W@(p&q~&JfT}p^k=heH*b0y=nl2gaFMnq_7o_CGW8qhc_eM0L>ikEUPOe#K= zA0JPzZ04jm+i%4+oOPmNQ8TBQ7=}fL`+Xiyq|c(1)^jHW#gV3oU|L1m;Xs4)=CBez71vJ4eyY4{T_uIA<|NESpO)ajBG=TDKR~ zH-s!M;=kW}N-~Y4! z0+BGPk$b#rf_Bzyne%QBWs;3JqKf|Jo8QuUBe;93&WzEeW=RuBPE2da-HvYz=5MOGew=+Kci1_uh-sw0x%Z zHtL0ziB3j_t&|>S86OQAucK7%B7oya~M6BU7+}+=^uW|@06@n#EK{8k_h@DDR z!o>*qO5_%*A#gCW`Befu2{J$bY@x7x>BQeiV*mnFgv#UXogJk1x_RQ`152xCTU*1# z&Rc3DLj?=#+C0|sVC=Tumcpv@8XQ<7Bbpz~rg@atV~I5sA6@%^wX_=YVDC!{IJY)t zVx>g6R4>~`tBZ}AL4N#&t08ZhP|zX(y}^%u^rPljU!Zh1e+rG`FQD!;YiAqN6&?Sb z)=Y7x8gJO5Is(i7sNL4qwlMC-)+J$4Ii0{H0;g@74+mdN_xqOyrUi`mM-S~>5?w&X zCtiNZ%MpxTcl*)~$W(&9#AYO|xLH!zXcQ@p_;|5pqNT(G4r;x7{f2o4N@=FMq^@!$ z_HKm*)eeP`x2c$-DG`}KdPS#dNdzi|t*C*0{bA?DmtPQan2x7!{p|f=XQwls{qZ0F zCfpOyleyoU<|yEU0JQykw68arT9uw)E!LQMBYqX=Sq_|arKAV~k3IIoVxTAIkf4)P zxT}7Hs5loe@uXI^1pz~M3%pmV7pT0QglGmUQVzWc79xSln>cHU0MfZ}AGdYA#%;r)rVT8Krb7R_)TVAjT>NGaW$B z^MNF3(G7`8>W-AoMpbKrTs)H4iY0*4{+3WR2$+}-wy8x{q}2`?(X+BYelG3Xtw0u} zI2T6T3jO4rBPl!i7_rK1lZLZlta3f*7u9Ukq%``|l0N%Jr@y@+gsC}8 zsKz*JF>3hQ)l~?Z1Jk&`EF-dso3$+g{Y+2Bo$$1HflZV7gud{ExS``}rYC%B_VW_s zuZ`6OfODrnY+AZcQ7sJ3Ec|Mnwc5(U)^wap63(mWCN>PIek0&X=WA3mCWz5!DMGA> z>fkIUBu@GHG811){q8a~Y6Nq=A^nYX-J=Lbj?tQBR}<9K?Ch@BT^4(h{elADQVLvj ziN2+bxj^fP0v2FpUp)a!U7B4>E*NXZn-x#rQghTF@=x;=#fM)pkG#9`D=jaM0IpwE zCpN82`abcew{e2TRl^|F__y4D0h!^=H=PQ@^D5L483uLgrTsIObCC{WIoVnfND*p$ z^U2_{IxE#*P6HekSk+>+%CZx4zYz<&yz%OjLT8T??wvL72x`5f=9#N04J@gpq7WSnLR7aonsyG=nu_uR~w?l2=+a(rz{u z=Puehp{geI!6&0@QLc&&0NYdsDE($kEc)dgo!T0@I~8*5WV1NfQxjkFci(t3_4P){ z0G#qmEJ~?P21Oj1l#p>@<3f-LCXwmzpox4j{RsW!S!7GDqw9kMekH^NInMcwVP>@x zt@+`N{k`5`crXy5H!mSOAjx*w4yEitN$(QQ!dHlE=I@hu`yzRy_Q4iQl}p(8CDpQ! z^Lkr{k+}!!SOsEI!?x86=XogFyRBT&&&{Wv?&XM)?%S`wA$xQoT2v&T@__~cgQBbz zE@%;Z2YMzLnL8S=$d%auVo|jgj(9YL!w51-@nxN4yIlhko3j=BBW;jtK9q)em`s){ z^t;%*HBtL)JDmH6*&)1?1=|)w{ZHO~*CHGm$O+()<*)LfT0o(OFuCwt)D7FYc!tT4 zNh;$HKKLLoJK00b1@dMcUVO_~6gpx)5_zP8!nH2W6%~#I0%^YKn=53yY7o(V!aTx#$(w0l2Wx;>@%{NnLrIw|Xm0Ed% z8nJ|4E|CLpIvH(cp2ujzIiB;eD0He|j_<*g&-;j>MZG3?DJzyPlCY>t723YCH{0zK zD3t<6Vkl~4OOP60P$NYkm_lpoFu$^rm95S;+*$X>L$yDmLH)!CvkidV1Fe{uJ9TY@ zl5G=Lj1V@p$TF1$j60(DhY&m;z)7*9YINWv44CBzyx8+uy@ zuE@i&mFnm-mJbe=hXN*6%fhAN3aqGx0M+K8f8ZFvqGV%0RZf<~0vo*nadUM)pq9yX zt4NWz)Oq=+{aw%8?gZzOD34q>$S%}NKSaIM)MSS@-g*-y<$2-h=jC^kT&0w{86gdn zG+C}+d+oI)S*&ok+)azn-5Y0G#`gAbYIpn2Vx^kbqYg0^RSOWCG&X<%8JI0qwoto~ zE4Z2xG~NdUahi}vFDf;ZWGRqgpWE%4Vz&+|6U1cD80V*mN)pZBwEP5>*{5wnOEi=eZXx}Tcv79ZszI1;%)f77anWp0)* zho|*)RTlWAO0miINY~saPo+lFmJ>|(S8v_(#d>uBO!Mtur*rk{Ro{yBEe|B?!dLFr zpVPlW!KG{qddlQ=5Sx&o@Ct3BmC`IE-8;Bsr-sAcOV3?hOvP9UdEWI#CjICYAetty z@dj?#N#kR}tGDVv=8=8hr}KaN&;S1K{>gvwcYgfa9*;ZIzwx*KW{RicC{cqJ?2X?((aFdFrvJ5E-1G|NQ3-{s}Or-aL`# z!TQ;1L?EAe@)<0U-g@h;X7^YF)NlvEDIo2|$Km!C%}txa5*OK>UznkHIxjsP57cYR zj_)@eR#v)I;nkO3HZaqIyz%y1ilz^l3{*R=FNP=g8=-F#e0_s-n2r1@h;jloY{erg zndygmK$^9t;A#6x`%6_1Y8R8p1Pf!lSyLGVo=-qq|M|)*uOxe?^WKN=RXeiSS=XJf zqn_(UJP_B27*ikGg&bv9W1(Zh{9yu1Up6)~lxThId#M+rS3{ijcXbzIZ)-&9X|-b* zt+~&PTUtvmAVmhsz^But!PigrJBHcL9o0Ws!Thyi+^W{!h6_sBnzs|k7$vZ8tS;6_ z>Cx2~*|iI1ncX{jwQiOiMltjMtCDVDdmX@%-w1M(3X=t6?Muf`fLY|cqY_TMdhBPz zP!)VgqpHLM(yx;U;(5E50#?)riF9zlsoqq&-G&={7v+SZIC+$w+C{pcz;~GfXAbSY z%ay*U+uc)uSzo2QfBS2^!6%}_#Qw+gHQ&w=dhy7`{AU1?W`aX&mKGv@@4IXkPB-y7 zOd3_YrKFdp16?&&kjC$8)Z|<7>Tq*t8&IflDRM;eXlwk`?^}V~5{X|OHr*^6(?b>? z?SFar{V!$@f4|t9UT|$Y6r;Q2D zE`h6#Q#jsaGmh;zckP{bMXQV!b&uLx>*`?apu+@qx}ga!_TGNuO@RVEazCEYEE>Bi_oCD$8xZM8xe@#G8PI=xG&`B!uSR z#!Hvy8=E%;7oU8RTT`qKb)D@ILh+mH1a;xq%11H;}G|{b@J^_RKnDO!bbop&fxO+a3X|I*3geX z`Q+m^&RjzFx5Pb-Z;*NKz4xraYfhrnnp|vHgeM@Gh&5%gZE5&rfp;jAx1$RD(K%!- zh*<#)2m5lDL#mGqgU#i9)p)FI$_e`m=l>f4G)JS?y*y z2o3EG;Ge^ix3f3VDlIZDfBV@!s-T`o)e>rBmTOC8tuI91#gl^>f#aq}3Pji*61)AK z?tx&M4R{Ga#82t0P3Yh|@YWP3!q$pt@bk1x5s_;5PyLQKff52P&cj2aXz}R@=gk*^ zg>}ND@zB~$(NL^1?X1%q_*+r*Y`1JW&qx$~&=aDp-8?dWmc%e4oR(Lr7k+MT&{CEj zFLT)CdV=@@_lSL=8d5TSFlG-4bT)(!Ig`Acoe%02LXvk8f5X1Mq$&G0%Y%k$+OGYS z$;XWn%M~*`N#=&lGtUfr_GzBQEHirc*cGqO(@epM^{50PMJGbj%!_KukDHPVWmU0_ zMDniA*(L$qav22Ftw{yhD4PkB`E767?%oUbiQ(GbCcM|djoO*auu63S|5x2&u0 zhB;_zk!-bux)o|khiq>8j$M@{-b_}aiNXrTd0iaUG^8t67tlzpLg;DnV)Uw{PF+yX z)6?j1@A#yk<=eqwouh@r^5M~6G=WSR&N#D>u1zl}h06OhJfaf)Iu7m##ywtD(LtD@ zPC;q2?u8x=u@czsrOTI_enC5vEUIr6#Tn^`vOwJ(X9M(ylk0od_1GzTf9zo3{-`D} zmhwmat%`;Wl-T0>q*sQR7+iK1qCN4^d=9%0Z$13%_nx@i+4?vBr9YodT{ik}y#Mx3 z-u@}0Sf4 zI@X~QrA$AmCLSr*b^?$7;1L0~)=r_YN7bKYB^LGpXLT(W6Oqpg*Y7NxWy2IJoBrNH z49ilj%jAN%_dti`o2%wE1z7^Fe`~B%Q(a3L)2B*{Sq)j5(?U+H{OM&-vL zwJJm`k6Z8KzmC6i$G_5js?4;5UeFXu(ey&O`s{ZM=_2C=1s+HWtej{MB=#3be`6F_ z@2Pxag(|3eDd~zBFvs;fvkhmNXl+8X&vDodob#8KsamOfMXx$PX%Jd-lVH^z54Cn7 zfXl^jF3rxp;&(3ZO%5g>e)=J94mJnRJoC)!ufNH`!SkcuCprtQB|dTUTyXd{1L<#e z4)!|-TL>JJY8!~gW(d;Z65nb%j> zDHi<&DenwbL}7Bs#+T zZ@xl}9|DK z8+NxXW1H-a`a7M@XTng~#Ih7VxUIl|+vM{IchBnXbT<9)mmf12<=qN$1BZtCFcEA@ zy#U45@xz+No%AvV6oCy~o}P@-RCNj8r2s`Mv#_XabyZp0i~Ed*TpaH9X9gshY~%CE z_3IWU2$;l}6m^_6#%$S&*Rnxd^4g6{2c9c%(FtvETOF%X)!km+b${UjOs_c+*4Bvy zIl;K}UQ;{%^r^@rBjzrTnR$l7zz47W;%C<;)8G3~UUcs3cz3jY>5@=`m3f7B`o%G` z*1v+%-58HX1afZINkQf28B9%65pMp@jhmexA(bbgjr;44xkCI1=Uj!jL7i~cufTz; znZn8>EoicLFh1P*zKEaI&kDwrIC{mCAn5gkltR^N2{P2-4HQDnftTgE4%jemOj+cM(%W;dg`fIsZj(V!7pcI z?J0C6y_p@&`wduKD z5W%UjPe1*1|HF4Eo0%knT+txq-I5y7@Xluyy$csAg&*SjPGg++zWCzHg@&rm26fKq zaYAd>>sh6+)1UUHU*g^8pMDz8zeQ0AJcrpOBnyD`Cu9d0-nnR_Zny*M$AWC@`H!xJ z6a0u=mbfJM?O(tVA?Vt(dT=u5|Ar zw!0ixDzmk-lZdFBk(umWxd0U*;q4NQQ}8Y2wbHvCCf0%4FTVWTYyaQJ+nbhA(zDdq z8!0Gu(!1!K`uSvP!~F2dLzlFA&EPqo6fh_#Hxk)aglBhR3mq`@^k}sF2+};!>T4G{ zG!F@?r9MuZ+x=o^GE+Rn<#yMshZqZ~yWM+vwv^ert~D|GF5*e-=WsC06s5{y`3v0K zv}a&RPiEh<|3#OtDr5QF4zc#8BUup!TPSoUmqeeMNSr1Q-m88>Xu{>Bw*^}hL|{U- zJcS!%8rko#zDr#0;_r$?xUT46l`wW%sQk=HWY9^V6fzBN%XSxKd{x#3+{y+Yk{^-0 zC3)n&a0`6w*WHj5gJ~Od??n`=#azC4zIcWL=^R82L0){5#M4k+qHw#vji(Pk{O}i_ z@A56a`_X$>AA6QIdWS&nY;V(a@r?c1{(tx1{I^?a*RW%@Gu+m*HSaU>QJCP%Lzl0x zoM{Ode1=VobTKS3dOUl69R*cSzl+|-+z*^U(1||z;#pDt{yD3zMfX`ybl9S;N< zepN`KX4y1ET_hLLh&1-O6H|i;^*H?;ozC1oj|AG~%MTUj@maSvKp9@;ruV0%rw-ZG z$7LGRrNNI|z|V8TBgI~%Z;ApAnR2v-E}X+wY@=1F zeev*u0uL|+&P#0{U=m!Q{w=3~afVd^89(;pCgrTjoJ=W>-6W37awlpwTFcRH-}!gF zb-Y>8-_y*wq$IwIvDZc(Uma(^}O6FInKK#Q{QpU!5VfA&dME*i4w zRh7>-zP%OBi5#4+GJqI4ioW;=+VS{Y9$o@dm@2@fbktI*^b512$eR8jO3tmz!dt@) z8H8^he&{m4u=kYb4tJ?HK@HCYuF$8lVS$h16;q~p z?-h6PA_|0p^#?1%LFRYAmiih-Dq)Fj`p+k1Yl(0b5!PS-^2>^CRYc5g$i3cZW}D>! zg4n_Ki5ISB3(nz|Qn)ou%68ETwRx#Hh+5DWQF~h`Kk>v94nZ7jiKs6c1(~HIf|dJ2 z>`=v9SOs?y@xN_qQ_CZzYG#aJvF{Wg0Lbx^DoZ=)m1yNFFTB#(%KAu&-*x*EFK!l< z8bY~A0};au(7z`ns$3cJdyta(i!Im*V^ zm4iU3Rjw1@yt1I)8FaIZwfTapn%zrRE=O2uWCaL*QA44Z0F(#}+hngpLrYOlMSk*2 z1WeT+?&s|A%B4%ix++g7dNQI%gN^?CKmVhHaV+PSfv#1g3m5RMXqE6QN>CO}auEs1 z4%ox3*aS2p;r34Mbs@j%RLFBo-|Hu&-DE$TKPA6nOj9qk+P%4NX2}8rnmADrq0&vE za~5W+JK31t6cNwL;Fe67P)|6;ngSD=EGW}5z&6g-L}ivY^7o`ESbvtsNo;C{wL~|o zrPGHwJ%K#wy!rZDH?QyZZ9OoXZ(qJ_17dAP?XsMPiMNkwu!Qtr*yrnIj?JZrP*tpG zk|rmG?%hm4%S{Clrehedmt72rEh&)}qogk>g_>EeY+eZ%mA{sdhB{5DvxMERhpuQC zMDk6_lP7YNUDOavKYvDrJaG(e>vLF`O!xo#ivC{xBQ_s-epfTrbA^Hm%^R9Ohgm{ zqY65zZ$cmyVn3VQ3OLzf%B*7HQm6Cu6HiigO85Tz@3%%z5zW5E_*7bOs{J_QyOmMj znTFEav`V{ev71$@C`pVn3*na3NTvwXh|$2)7(3D5*(aZT;-~rw_T(b!w&Kq%fWF?1 zTRGRtDlw>2b=kzSt?+>-^b`misIr$|ukbs4n@CrQNBNn`NITM`Sx9ub&gX`y4Mc*c zJ9$OApLn8)&I1IFFuUWcyt`z&rUA;3C6I2amfA^fQXdt$+`qEb$$raJ%J&9`{eSJx{JG42n0FrTTzdA2XArBuwQ+Or z1~^u$2S83f23eLskX*G?bFdcIC;q^{Rb<#1Qu(F})apdG_7l7Yy4Vz~TR~0P-OPq4 ztJxSOgr~yEiz^E}?{4+mDHvzvy|{n=6fjL^tFy!65gAO^yV-N>Xf@*6dfVpUEKxQ= znRqlIKYt|FAQIZOUyIAAK=s}k2cM`0p(59%iMQte4&Ona)*WryiWn44eND`Brx66L zw6jP=f8A-VHv8Yg1MX`#o1INRro~6AvG@+t?YydndO-PJ*w(esqdV<$US+Mrp#mn{ z{NM*BW~Xs!eUEUttb6rn)1aimZ5S?0avJ(mG-@66#r3zC0=W6v_0Q6m=yo4_?6G^s zv~_hd!#ENfUlu_EGXhgv(K@^{;?(*e8B3o-s@~s8I4n-<=Tz4@dX&P-inU(E$rtYF zx2}kK1#qQ%f^qTE1qB{F3N+<<@bJFC_}7F2h7{$-tUfDu&|qh@yBFt|kG@N~G$_G$ zCj7u>5s&BjP^O&3V@{%K7+PQXd?5cyp`ceA9qA?TSm_!{TK5G~OX7V)E}uK&))J|2 zf^e6(P!BE18roKG(m&YcOV1FfZ@;7jRH_HPwb9?QZ{lR~`fIP9ic^yo-|eF1ah+k8 zwJw{zO{d?@Cx;ue&SegtCGmyv`knD+cdtMHT+D+5rgha%E#r)v&2zmd@$f+GM7bHl z(TzB}7q3Nwj(JYwC0>G*DZ!P*lPFnVEGI()J ze{;8a3Otn~`&ceZPuaEUYodDGTQuC7Hk)VyleKWOfBkp=e2f}-_@|Blf z_RyJ#PK*Qj^tz?^qlAy?l-IC4k-R!v#a4uJWX&bkdx{+@r4p0LM<0D;A7hRi;Q{BI zO+LJqK=eizZaX`1sO1$e%3dii#;gKmJLoJ!73>M2Dg%i&EC2WbClwZNZzZyCO z+{q+YC9Ix{6;TMh55T$2D(4A`v=blu8^eblLbv@f|7X^!3iH_cT;f*}H&v3ilons) zOk8=(64$Zm-jacBNDLiyUdmjBDAdY=OK%0+s%69G*6*rFaUdeGSOP+PL_^7_h+ec{ zHs9ad%Wk@vOkin%gBl&rX|b(!xt2CgHxjhwgqb%rj1-2zMS}{+kx!v4t7IJZIhl{L zCu&HUxv8>+ETt0x5{hC;E%QC>y#M|O*%P;tovA}nTT7nU@w zu33kyQSElEpygfitHd(85cR{)!?~DoY>RmqktJ)_HXr-Z6H(c)GPjhny|p#k-@i22 z%7S%~xv?{!ilk@Zw7g$cTl}zlbE|~|Q*Z8nIi4j}U6JNx?pyxCKh~8{Wui zp$teqcI8H5qWfOXxac^$# zbIl1E^)p%!quF;XpeRBu5wux%-`eC>K;lMcliQV8v$bN0Cf zt7=iISb$|lq8LtuikhO&Lh2xq88ng4i2A9CiQee^h2Q(Lv;KYpjFw-gdB|qSk$QZH zF62iQoJIi)%s0=oNNV8vPZvCLc&DdAQS8sKwwXA!OgFDyy_%e}nG@`+7YD}A#@4f5 zIrTpN0+OvnMQPJPsJRq0BuFokx(C@s3g!GqmUg2(qLuY?4wQ;8<90H|PT9~CRkhtP zl!r0DH$00op^vhFO|Qokgfr4gP$ARgMr*yhzlhO3^2j49>s5IAj_J71DbB7X4I|~H zQ|i)Eh!z?za;%X5`be!YuEcCv(NzAl6enDvie*S=bAy^)Q%jt?-N@~NRsnFHc$S$$ zB89egf!H9>0 z2aFnYUtfIkd6s`X8HUd_&c~Z82th zJ@MG%v=2M7cMB_8h8;l+qnaje^iAGP(=WLX9&_Xy^?WnGUSavtJ*(E!O}bA2?#&*0 z{nshQtd?z#IgNp>%Ev<2r;)(~SaqLj zU>`hVq%YC~MgeR#%8RmZY~ViZ$L9A(j-^Gv!TIwT+>pju0cK!Nit;{w(N1-^Y0xDfGgA5ShYTBiWN zsIA}_=l;fcYWJZzZ*saT=``t~R_`V4#H>H(AFl|<`s??4t$U=OB&9LZKhh-d&;AOA zn)h0PGv2eUoJP(-oZmr~LENBeWPgQ859KS+?TJ{b3h8|DAC;*TRW z?^w6gLAHcg;+Glpx8olwkH)Pa)5Tt+ZC!I+1&UZ4FYHio)Oxt;a3lm`0y}w&Gj9Ib zXP=!NhPbt;qdR(Oi^T)+CuWcqn0iii(pwzC`h#m&wKs3=?(6gEL^k(OT%5-G69L=| zq`l4su||ah-}_2gVr!BTuzKuwIK zrRq_Gbf|xAU9y6P+glvp%@wFnbo18nDm7eg_Z@_hz4$F(qf7{`I{MKqw9@S+2$a2l z=bKwY5TGSpZxMOr;zeX$6ZqJW!t(` zt61or+N$Yn_T`sfBCpq_(5IpY;5YenlgBFKjT)K5#r+cb@Fl2u#YY0rr&f_ho~w59 zgssWY(>}5y!gcXEA5VTOIAST|c<Qcu^9*9#X25jfkT4V{Z zO{x<~BDJoA^Kr;0ezi);se&7W0U)#w)%>h8Bp0hl)ubdLVONgWb=10_VjEJhno= zqH_3FW^f4=HC(m=xoXfY>!KDnN~V_A%Quex=G`(NxPWUB5)sy;)k{YdIC{qngN>mj zq=)+lIV^2!`0T4MT{gqod9 zLn{?vo%VJ|j89@1EAAgFG(Wt^?7)_tw_aRT;C!1<$1MOBH$d7xB;OT{7OF5FXAb}=t$gU6kc^)(K?z6(c3PG@PDRPk&6eZrGa#eU-iM!FyE)#C zz%0|z)$$I)N>vs4Vp09{iquw-wu~Nyi}yb%w>e6UF)rCQt#ue`x=dD|SQo3sDayd} ze9>9ck|V&)5c%Dty5$kTc4yU&H3H^XaVymO{qKMO`|u>uc-v;jh#ZXP^|5nLJ}JWhJKA@pV{Up!ROD?Z zDc@qyg!AFcS0G4|w>y_iMkEY7!>ueZ?PZS*8@HDE$E1^H`5UNK6IzxzO;YDzZ@PEw z+BGWX=Tkp?>RuH18$kNL6+uVVSCL|wqN>$9RwQeX`8~#e>3wUKw6?JX+$pPk-ja>> zp(L%t^m}OESI~Y_L@{7%)7}W#xy?9&}`+RAVZfCl9*`tV7;wtvJW8&WS zPC}>pfpIUUiHAty;d-V=I5bpO_#HjCxcc@|pml4b!ZqlQ&bF@6U83AN$$)@hI~i}+ zSaKl^&5Xnvta@g53IEjW>&AI;9odXg@{=heoj$qeeR6vq!ntTKIyE#_R=Qy+e5JXk zqIz-jf&$+S3aoedzZ*5WsKqxy0rs(_`&(F1L z=Ek=UCgcBI>%@EwJwF*jKzct}Ujry{m`Yk~4SB!$- zi}2{sZi3~aGr#ucjnP|gzLzCbyumwLQ!&kccQhLJI+rG+V}0gZQAcNLvbxW?5or~z zv2oU9)V1O#$6d7b9Ld!?ICT2r)~q}E(wTg#_wM3?penHhmdMU?grr_vqGHGTONGhp zz~*?ua&|UomdfdJf#yYG^Tiim=>I8Y1wuWkimE2Are9cSh^n?wjeHgt${fK1qUIk& zxy;4|1YxaZtWnCu_zxqs^>|0w|=BWnaqgKBq8kw|C*mh_2t;+~n z3aUL%1ZJAR2eDVhf;+w3&)vwijx#;)7{PwMI-t>Q4oF#l<{q79ilAL3W4Q~ zB`*sxPhl%i5C>>+BzRy)*)CZQ;L?COSS9D&CSGN^iGtqy;9Azel{egwzPV5rFD@#d zYPKGYI5#!(=ATIK@q8!`0Df$(=q1IxwS)*7G{A>`At4GcDq87vl~<&|JuRt- z{1M$)RKX9v zXu_5=8x|ntJS|0!mQMtPau#Vx0{RId9K-6g*F*bdy_+<~o~dGYCIYHPYN3R@3W1t? z1U>uE+6;PcJl+}fE;;+M7I~>ntBbtI#ZsLp5a|mH8Hpl0`)ZZpY}zMF1nFkh2fI*u zfS1sO>8B)Y&0T?K>#Mf1-|)u%!DR1X(z$%4jBfl#aG<01zZJafTB0=^L@IEnPUjjO z%-;L(BUMHLaI!u)RM6<)*C|D!jsn#ptgb&=6Zt|zi`28QG~o{~LttUJ>Yb(3MfHeQ zJvy{fC#aNJh>!qzTl<3=R${;SDkDA7=&;Ir0>c}%fF2IzE=k!XC%)BDxr_!%qGuE1 zVC!dGY?<5;dqpziLpw8}jXa>q;lG%4?FW#BZk=q3+rXaj z#?kEvaDIg=c`N#sXe((k=SfBUB#Z7qwRw0GrYe(i1m(`Iyu}ElEt@UYKyXm?y*8}`bmUBy}a*ArSB3kK6eCVNvaM;ZV`QhPu$QB}z_WPD@U)40e zI$ii=PEGn?h!e^~($B>P#qchQoiCm%Xf7Biv|3r$yC4RJmQ*y3!?XQts)6av!jne# z)sDiZFRE!ZRc4$Shij^rg};ztg&LexJA=aoI_U^A4)Wx~Y#+Yx#M3s!cWk(iZD|FQ znJ|_+Dxfo7YsFS-i^ut-h{eTis>vom7#Aq1Y!GQ}&H5|rlGh}(`BXi|=Z$Q1?95WxcSA>su($Y}el2O=gu0{Lx2$q+PQ~OnO9f8wUyY zn_QPwq+Y!@@BA%g-tLBCV%_{XZb$4~qNi$#%%#n2Z4K3C|8EEDyZFq`HPoDDsa$O4 zGEJx13&)+tDTI3LhuILN<&-OSrbyeE*j{y$xsX^bX?57)66(d@lN4whhBS+c18G8Z z`fI>ilrLkwU_xx~*GWoQOKB{0#N26imCaP5dzh%X&(DRuO-s6LL(+Nj`I?xlp>c8f zjZz?^+8ols(-sYFP3<}ccg|VT(%M**-n4Hn{9vPbTvhUz#cVmD6 z06+jqL_t&nUG<`Ya z)r;E~6!?x%fbd_W3kuvZ1sGz>Q1CZDZkq?J zH(xWSmz{pTZtk*snhMmsMT_sWC9S<_N|8{IW;EXa8i(H#_V@VGqRY7I)4^??ZCP(g zcdfr_n1wT(r*YygmXO9{*V3ZIcpln~lzgJpwOj4-D8td!iFa=IGcEC|ri+Y6^(xF( zp3>XF`TEabv9+Z?Je~K)2OXO}io4s6LiJ0E`}VvtAW^vEuSvrd*FCUgo)2wF2K zb=c8qNtPv@{cfD%`bj?tv$%d*0FtBma5@u{v+n9x4_~fCb^J+Mr+m4FQ%;<2eY-1e z&bK?0jq$FvJ$jX7Fn(UbbMxl)_0AumYLWz>s7#Iq?7j8In>p7|70mcI{awUlQI=jL z&uctCK_JWJ4o9NJhY5oT=>4(Z{Np(vuiJU)rKi)$OQ5Kot+*VS=y@Vapk;|bJ_{j@ zmoPA^T8?@YZWVj27)$2%!m2*bIn{v-5Nv_o_V%R;3C~A+mehm_we}!%3aBHx`&H45 zsz@W0Od-5F6%y#vJZo`V09-($zfRF@wmr0^XVxAHI3XPt>qf_H@|d`^)@FOhN>1Kn zMME&5Qybc*3RTR~t-Sb|$}LmztI9^CY2wWVR!+V)EH;ffp#-t*&3ZfWD!QLa34Qdo zdq4TfKi#bLwLCbML&er5tdcHp28^kgGZo#-U;x^SLE|OcPT~PD{)%sxZWNvisaSGB zj>uWn!{1zDR+304K?OFV2}5}Z1T?bU?%cYqnT=G{=$Wfm69B0+8ihN2Pre!a1ne{O zcitZiv+xm6_@@luNCLo>LdxLUdHgZ?ClfRJvm{6ev)yjFPJG8F8_JdnPN$!I{3!-$ zce)$HguL2@qV|y7uyAvRZDRnt7@C#V0+zE$&VbC69m$%`T8ZLPjL;kbSwNOl873J> zF*nmmVLx9S10iB#C2HB2&=Jx^#OC)*>_eQmeCqMZoA< z`xLQ5i_Y$z(|?DPNE?_VZwMtiQ`Tmf zjcF^q#Fm+Y5KmS2^`HHW%;W3c?xeHrsKN-E@5c26&>9LzT00MMe_QSC(`L1>fV~05 zw0}CFOnPs)W1j5FFh-qQ6`O?tIS9_?9?`SNf4 z);=jP4`$9Ic_^61MCRShHc7-K*Qi74nSO-G@8I5K7*t89gV3}L^|V!Xk-5e58lIZ~ zUb|*>0~%i6W4N^=OGOBcVlWrA0MDD*qk}GrIZ(RkXgXpLBuMU)NKrkL6*_Z^M<+Jg z?RT6OOlX|QE08I+KOZC_&Lf;1(Sfkyx@VcAf>N>AxD=NfNimq7X-e#)oWDrHxJ*C& z_?MIm9GOY{fvF@cb9P+s2+C$(UpYgL2E`RM5E@E}*RH+CXop6$tUWI>VtV^oEn|B% z+7Jfy50|UbU4nQC*Y2+_1Baw@IY=CNyh^k99(vg+6n%ql8 zDAX`lqhOq*v@8v)vv4cCzT)fHyVaHj z9Z{#G8YMtVyfLKjGEk1?ju_sW3hkrU7a0~eil1)zoBH`sG*@tTWUwCFYp=bA@%D>& z{_$sLPXBFcUlnoUw!Z|$pV*Va$E*2KG|EN87NJUUQc~HYO}&PL?(hAp|7J>z@Ho&k z?WwKZ(=4YkKzPxf5@slCv9P8rg*o6)`PK1ziut9165>ul4fYx2MQgIr&s|JTor7u z$D}2NqoIG*x4qNn`$g!v+TMcMyB;#Par z`VRM6!L*MpE&s;g(o9A#2A@i13!-Kvvwg+oOZG`hJF;M|i&$u>F>nFx;?K960=F_N z6P5HTarR(h1Ai9SP9dCBE$1TtiPxC)MKwzTO4D_-GBHt;4Z>!3^U+5i&DI_Cy81# ze(OYVgl_0sXxr_mlL2h1p4wle3krOvDX>)j?{pie-_L&WddC8en049N&0Ks9>wFrTx-s^sJ1|Jz;b}Eu{x@#d#p z6l*yX%DFu_PQ7ZblxmgiJMZ#9sH9u+q*ATV*1qqoe6C<4ZG$*VL7e@_RccUFpm;o0 zK36UK-n#vpd}wT;h;FI2iREOA4ioeCMY{*}M{PyXuvu>XfY>x`!Jk*VrYrrF!?W^}qgHhk__wc4B`Tse9m zgDnTJkv%6wf+J8Rk`u_qgQB|1BR;~!iGx#ebgf+!ukCC)&{N{zCa~fw^kHDccQ8xH zEusWo*j=dl-Ntlv{t6brt+!$LXIPl%nqLPSzyGiNJOAZ>{9paftADzGxWPxqtqdL= zOlT)ddARNRJU7u5ea!?>kf=OPPL#`VPxOeZ>u|g=Js4!F!Ee6$V+e5iM7GpZH&O+3 zC!si-$+Fqmdhz*}Rd{c@o4(%6&c=PQ9OD)nRf=YI2tvXh@U5z+=*7^qWH0IuH;eD+ z^3K-QWHd7M*8-`7F~2x~*}1n!5uxo0eRnr3E>vMB(8g=z(@X4%Cn}+tkPXQ=t=Jc? z;ugHj?L`8RF1`J>!B$3D`jPq6nUP#vI5ipN$V4UgvYBD^h@-q_DR(ypBVObupN-Rh z-g)coH|(1@A7dOv4^>N=kiD+r1kY?f_9#-9zf~_M+wnr9Rp{YC%#@FWm8hCweu|f& zw>j7m6G5J_`dK^In{hMF#v^QX5TK5~w@|OxBirmyG;X6avc-An4}hOmrGY{Mc^&edTVJ~8Pj*FO;=os;24=h!)gKf!-G*! zMKl5axPcP;1pzn5CfUz;`i2=I39Aqp(v6lWteJf{wG^o_P}EFs4=&-J;i$q+hiA#~foS*$lz;16}Pdx&bKs^*0BCR^LR z$sy}EnA(at@q?KWd1l+RwS)7CVz4uM2PHAThHE*fHcu->#}II}XH~DO_{#m1 zTDF*Xut?7QU<+Fg?DcIA?a9WK;V%1WbtXo|m`1Oo8ny#KdhcizpdUlS7#&Uog^o34b+vjMc1Pr1$r~QMlq6|-!`t> zNtS@y$0F8Evunx^%g&g}<1oB*)hj^h=(}PtIGCO`(~42cW^@|>QSP8)S_}o*jQrA| zScepf`6Oc7`2rnhkZ)+n<6N)W#I{$G%?%$GBG`5|2yz6sd-&#CuMsE4IV2*$)Q0FI zSlFNHq=-mPQY@8x>0oFNnFR_rs6aHmBSTp}=p7nDCn2B}UmtIx+W=wb%9+CtspkmI#q>tV~#uJ6&Rw0!NpLkTIocWda z61Nbm$S&?w)D1#AVcEhi9|?E_YgI{PlT_m~5 z4~~?vPuw*x`;#kLfbaMcG1UsC17FdC7q7k)bJjA$C(=uHlC9kf)iOy+aJN*TFiJkZ zYBH9-D|qgN*F-0{ zZH~LY^U807C6oz2+(a=dLS{E|<`!jgY~(K6GCr7SP4F5%jrbF`M6;8llGQC>>ru=$ zUwb1+Fhd09gK^l_)tOY6K!fKWIE|ZWnFF*a5Q>Ea*dFs}XjWuQZ&}T!9a}GCwi=nt zuETjHDi1Yc8RNR<2$zqQZ(#qd`MT}|;cje913Wj3ur9|{y(ktBa{ z-{cS3qJ~&po1?Hv^b48X)%7GBEC)r%0m<=BR3_rzNa*7&O*7jU@Te1w&*F z9ll5zpi`b)mYw%(krqu-{i>|4npJFOXPNK8{HvbJN-acAyQRyX;dTbn=p2z`6zl%o zR&r^7{ACVft6ZV}G-J$IU(-p=z>eJtYuus&nL~!3=ClzDK9jVZG(tZvDV&ihy%yH@ z2>E(=@%1aBKp0#pf12jjH*}UP3bSxocT_)5PdFpUv~9)KMT;7Fp;pJFvLNS+2sAm2 z-H10*YR&mj7katJ3}vrgL3+iEizYLUwTMh?h*!7>NRcKoBTA2Io3BE$j6nKDu><)Z ztVsW&3VxL;ME!FW{b1nRcQi%&ZJw{;K>Qo+>9X>GePxQ|QxYB`m~R*jGEve-oOz2D z!7wagW}rgmi*!MO?>Yry?k~~>1#YE4MwLnJJMPZ>Vixx&d+)sa*-w7@3y}*7I!qBaVjTi{K|3Be%<9a8cnl?qT1CQsuyP{XL@6Fm3c(;^cTH=%xSBS z?w?s^TJyV08rqB>w8>dUq~~Fn$w3=0hVDofx(>6}K~Dk#-$bj-!<Y1M>wW66RsM>TijVEXx+Z+`lRfB4x?e)7fJ?-`kPg&ByGfs-y1_ewD`>a%>i z#$?J_m*FrZ{koRN6D7C8(+|u*y*g>SLk7HD@w{YC7aQl?l-Z&EH>ab0t3!13b*khP z4-i7(;g7dPihl;|#&+e*Z{EDg?dzs47?1EfufMnR(BtBxB8UW%&t@r>?VaF7?Y!>^ zeCSS0M0Mb*Bbui(PMh9ge`r}yR$yRacwJ?t@Tw6+Kt`F(!;SsjeX9p7zC1V>+eeUA zX3BG~w{1H_zV-NR1tXVKOEr!qB}xv7dH>yat!aDc$|X+dLkFpPkYJt;WWV#~TOo9> z_x#nXR#WQ*5;O=$z@l6o3yf+=)*z>>aG`pui<;+2^DiMmdX|2-qVpu|g-7UHYuw`$ z&zjL%os&}PtX8DX_PcTJhv{)dD}ON{3z&94SIfj}hLedt931R^#8{0HlXRx&s`xVF z5*g!g-SYUg@Na09r;y}N&tis`lIKvrO*j}UBvr-yGD*Zji|KUA!n zviTE-O9uF&XsgoJsN&}GKBBbG@<#G6M5q)$ivo8B4?XcC>mRSa_yP|f=@siVg4uQM z@0H8y6&GY1GhWGbb2qM25LOzG_H^sEZYmBOR7OOqA z=VaFC;Z)38kuR$aYNO+kXx5J9xiQU%;Hg0uX7FeR$dV<6v$zF_-jAI$dx@beLVGuC z0#E=QWbgaQkfPjZfAZn`*GdemYOU?X4l>OmOXw(G>UUJzCOxUn=J3juhsXPNx(~I% zj*b^mBeLYqODO{~0TMgF`#{T?8KIGQp4DDl({ONujG^FD&pr>Mj01t_b)LQY0!U(q zqSxf*-8bIIDq>VxmN{{UnkVI)q7*2W9dKp z8c{FB-`?Iy@IxG>lOwdHjZFbq2c5E1F%h+Iw)L-O1E{Y&$C#KP;K~$2ftvHSdm#dYalK36@9O$v^__X%>T|^=ekdRI8R=lRI>ctx)eoN7CNW#Q{#x zSE*{0`pPV#1BjG17pKDLNEV!ZmiUzLM6!ZblP;I+LzxIm=wl+h)_5vkbN;SqlGnof zmh#tJexB&&W&+*mz|p`IdrE4pZDF+jw6I#gac9>JjbkM#H+_K^Ns6C&gfNP8JQhG& z=cb?a?izrYjbvN9m7jN#yhksH<<8c&7#%BUG3CzXZNw~kHW;g#Vyh$Vb7CdZMT?W- z#;c1}`MKXvs{42G1w?~F$CqI9dW)h}1H^?KD@LRuDCPK_m?f5MJAtPj zeR7l1Z)~wYz;apIbcJT5`Aq+KVqbxqH*YY}(TAp4Ce>_j$6TjPisb7}rJ`jV(#~n` zAk&$@{PL?@pqi)mKY!oJrW6)~bTZe;a72)*^{HR!4rEV-2~7-FOCutd>nXS}Qmr1* zUJ6fM=zAMEEaalfe)idC?GrGX?0@jlH5R)m=8{uzUm>-i|B^e!lWt6n!dMXwc+R$9rD?Te^R!F>HjTgp zrSP%v{{}Gob`*gZs6mX`H>0scNn5KH^uqd(EHqi^#p+J!+fqHSxhCD2)1~{NOYiLj z7>-^Gz-AIlm0~6Q)`{>J{E%IDCYja?Yrpfo#+PP4j~;!uu8L&7+UM)nE&a@vmgM!z z1T-B*wh~z>FGHPi*^=^0d$@RQ#h?Y7nq(|wqvg5y{WVrZn3Ki0+&k)Nd|OEMkaN~>Kx zhnRsy|ILk(T~C%GoLj5ZG^Bmk&678(=vDSRyx2@Y7kbHVfIJyZhnw-a`B_7s;NlbN zor={R7ITs9f&vc$1y0(L2La~`9KQ()oRACyyo?Umc3z~3#bk$~4&MYGEb&=}{^U%#$j+j>jxXv}iITCCRmv~1th_wDPYdw*wFXEKWz;j{HbbW)*BH<4h6qJGH~bJ`!09!va|+=vM>5WtNnD|pV-@GqzfadnCydMF`r*E{K9WhSR!q7LZCv(^ zhz8~HOQ$my=TRtW+`r;u_0M2N{##|af_Y$Q&Uq_Q!`UFHM3$jcb4>ALh?662sw}D|MVZ({cJuFd}%p9 z1U82Idj}2e`R;DR8U9FgES774TxvF|?3KV>^mtoAh6=}2wl#L2R8kr-~7~`Go|L!2HlR;nEjR-h+7??3=_ z6%?VuTuzYUVDo)MmR2e6o>z;kscflSZikEcfr;z2Et|*kA>lqoaKpN}@ziymZMfkP zl)`byEbLvqTeS*`O7@=%>&DKt^6anh$VJ*{S@iP5wU9oW<)(7IGIXxVf9^d{LLD1K z^Voia_?lI`ka3hor1{t&$ucX($P_7*s3J;<5de8qBnV)bSkc;CG&!nC{^7IV3pjq~ zop<~M5FF65`z6nfSjI|0F^#L6&(|KmSi9f?R6WWdZa%bvr9}v-a$Zxe(ouJh*BM(& zyjvgVTLr??j)|BaeUgu+Qyy#nQ9;=JU3MMizG7ZRa3x^#C}k8YZX%;j6Hq z=R@97WtKQ^cve=zyiA>zj-sh#KvHCZEg)2bijzos;#yGz_0y9U&?6u`(=@p4WI*g=K!4b`;_? zaxSkq!0dvqF36%q*hWmIGsx23N(bW8v-%{q{%ZILb-=q{lpN0bTlOd5B!Z{4Rf32& z26uM%nOP$_F$FHRuip^r+jG7eKH3|bV}jG&_EWsV__%ED$7-Qiq^uNdd=-VD(pC)- z%+x*v3@<}tKKb!{p%2q1CvIM<9q?3;oc#LjU(1)n-G;-be)Nng#FP!FHcGwp`p-ZA ze5<>DhpU>eH0QtpKkWVKfADv!=_ft<|a%w2Sw@c1SVkX5m^Z9f`cxM0y>7rylwC zdWIQ=^LmCyc@4p3Qf#|y0@Bpc^5KR5g0-eD9`yA_lM+3{2ijL4j0_Q{y$(Xc*3-qOvJz@Raww_>H`*BTegMDY&l~B2WQ|?X zv)xfy^z^ggj}-1up=Pf)Z{AG*cy#oecYZ6golzo(*>Aw)HVbjQ^-#3GFsW7oNi<<% zccG|joTf5iHV?(MX?8|v|$m1t#BQB|@x5#0Rb17KI@ zw`<0`u<4Q$TaSg+c2wg!vn!E!k{C!w<5>2QyuK4bXMZi0LqRX+B%v4;^VIeYHN)oC zl6_?cLa+67>gW2Slx{9t(TcndxZV*hREqgh@W)rTzY#`*v#-4J3Z5!O=}vO;loy2C z$!-~=EndnHHuWfb4T>@-$1Yd2b!twN3S%RQu>MrEhYCy80ZSAOO;9c_ zuNTKpBH4bO-}H)MYsN(GW>(-;BJ-jb|FK|M_Ykt|m0AjD7#XKo+u{{jST?mmDKu@SoK*oQ zh+={Sj=)JJy#4Mw@jYoh8lRG@Y+otfkGerBbe!B%dIRyPfLn+vgVw?f4ULFmX{YeU zgGBSjc?q{PM!SacwB?PYAl_0Fv+OIdsg81HW$Sz~Hh;VMa_{=JF5BJhwR$=yFDjyW zeYp1mw!iW6%P%+m4An#X-L_Lsk*HBAs!yF_L6ZktV}0k0qp6!VPru^weXF>o~r@z`v)-r^lu(T~Qf=0SPry5K*H}-AAn-FJKzx63=gzdpxjqFR;L9HXL=%NF8q~loZQyT(8So zaix@0q>d?~m|rkCVjn0~p^LGQpFSRqKRv)y)_{gQ51cg#^CBYo~I*I$15CE<7d z>Z`BHJW0H|Q$o|w;@GIb?bhg8|9IaTDHpV~ebnKPss0^Gh24F=^3F0fH&U>fObTZ( zJ0Rzbir#g+Oj;agxT71zkg?M*Fc3# zF3am}jj}YS{nX%P)JB@9w3{e{DS88mzuArTN0Vnz#Thm$h=3puaz;u-N zPVy)ysnpTBz_{kxL>9OqOR`)rwO6Q)_Bv!t*1#CqPrkgu4L1exx3yv91xxsDWk<{v zLWri~f(0d_>!&&rAv6h_OJk_Gxmatw)b2a)z2_7TBipxLe`6$QvsTBUzij>!vdd0N z2+I^!-#BlE8||L8|QQg1i!Run5yJVVP|Cauz0)JWc7!@z^Z zPpa`}N`1`8TxLbDU|&(-fhe$Qi4T=>}e zLy{WIpHYyOs2&&YlEE#qQ0Gxv!ZIo5XbAH9N`hPk<@!EQI3xcmLbd*^f)b#dY! z^rr8B_@ODc^u`-+sH_^W9k+U%hVO1H`1y3n9Tv(m1h;7(@--y_mr$?)df;#KG_Rm> zW)(&1>HOdipMPd#0Y(NK^Etm=v&cL!)Q8NNdf9|>wBgjaiPKN}IT?v_3w3Nl7u&Y) zC5^HTV4Y@gc40*bTWe;wG;N5OMV4ZCX{n7xj^AAxzH2~#eA}kK(?k0TLTzS0wK0Uq zC+}3FG=MQBYXb`*Vz?O^0$t05N$*&cu$!A38~}$5;eu(hjd7W$K`<{gZ)p6hF`QTV zKNQc>ad0rPu`pJReA)5O}6ZcVsR3WYHvx2 z9C1G@KHjxzs!L+70RF{4`3ndnbkR(e6PIytGP$$A2Qg{S&fdY3{iQrJA?K2yKvmTn zC{JR-(7c@~!dlEH$hGnLS8Ps!Z90_T$b+q&ZACD$Cmt%%u6FFiV@!sq8!No-e-UkY zR{rLYc=OFS4TYIbgekoD?gza)p=Vb>;*|owYH(c8U9?F|-~y>9~XP%DP9i zf^bA`F%+2VaaoL0>*^`0%&JB;-u82)A5uLTVXp?z=98K^fUZLExEd$UOxelB>csV|i z|E;1uas+90WLOMr;v*@dYWQ$~Z2@k)p}#|m;wK{Ilbv?#OAN20!{OGDH(jWjlT)E7 zHy69Ivit%>yoxS|T|q0VZeQCXE{vrXN!VJBRNZEDF7*Ksn@Y~MayOmF}GQ%gNG9JS+a4?ZE9|HXh}!KwylUPM806XvWv>FZ}k41&HmS;jW0)ouSdOu(Nq*E zUcLOvn*pLLAxT1HuJ4SlUFYc)CY$qIDF^$#hZiS`TCar%5Q|+v%e6 z_^Y4Q;mVoAWQhu=Kn@JG?>j+K8|vm;tgE60>lTi&1{ZN}{=(c#W;Y+v27)c1%w zr@-FyfafqW*YCajCROj`K*2qeoMKx_qA3$gqkN1yaNeC_T;xy-s+~n^ZBf)pBHBAy zOu&9^)7<%*1zW5?yzcKieKlu}03YM42Vgk=53@-4tp@6(9(_+7f z2iXV@Z6RC!lEh?h^!9uEBAruBBaPaRQ6rrpT@4A&smiJs_9EaF=zmW3j@aut6zV55 zV2-e^wZpXiyHaD+UAMpOjT2z2PiYX*N0$XL>L6aM7GFpl9wsrbgG7Y1MbhEod!rw zqn=)Be_!ud6S|($9=5o!4)>==2Q$4g+HrZtYFHZEO{X4I%KkXhx)3GVL@o4AnmlFx_a?JF1${KYMVCfbuE2nveu(I zCLezCF>UMw0n0BCO6liEpFc=(-<>=wA;s%ep!QzxKs8rL5()a#PrD0Lx84rK)l2DS z7dAtZS#y>l;m+BLWA*A~T|h2uE-zpW={GEd%6774R~NidphRI{j|*8{$PmF?0fu<8Ue<}^WB6{JJ3gx!u{Of-kt zb8E}eNx^fg3|ALd6u3AATHCMTVkb)etklaoJi`Y4NXy@bdF_QZmRQgEIIoR$ps~HV z{j;C_Oux{2D;0R0I9!!=MS<@Q1ulKc_uZ-L6($#?z)9I?S~B|H%1NhH4R)NZxPQF5 z_JY7Ko;{wRb2Rnw;O9c1qi;~~A6hQrVmvz;e~Wlx0M8M0tg&UqVUJcz z>;t0wg?V?aPN$(Uu;We6N?Qz>-@n=I?e3545|$8{q`Y14FeNhOiUws(R^pqly`JOu z7K>l~>Q`(fiIU_cxX{VKa>V60UkBEcRM|J2AhFX$x@swY}Yt2eKQw*t$& zore-SsE&yxYifQoAaij#94`MW()I7Nkh)H3Is;`UEtWbZ3Q{r;oLaf6OdOYJIYZ3b_S*_1&Z(u zEAazq^@v?2FgCXn7ST1cX%*tD@Q+@q>$*6pG~MsKbjWbXx0gMSW4seIsYI#X?Y=_E z(^ZMcwaDJEpwgU^rPi$|A)sAhp z0Cg)8*c;0@$Vt(et&vM&p?m5Ew^mnb72Tw<6`Jo7&f}lUt9l<*NJFfXvPIrfib?8_ z2kDz9x4yWsvHRrK9hg8~8MqP|%hs_zX^UCW5=UA@qBeKMNWrnGUnBU6St~14WE6+u zqE}jc?NzlUUJv>=0VTD<;BYS|R)XpbCkCdNNx_eoFg3C@Vx%*IMxsTU4t&Y~o^5t;7Y5V+FTxi#>Ur(c6A;XwjnOT7|A5-mEqzk2z zZ8qq@ty=qw{+@cph5U+*PNOJXRUMOgmFg{0qJmPSC7p|neAC}KZH{tvFxzNr_{!@q zxBYlae(->}71xB0MHxDsB_+hpT`3)>)iV{%BX`+ea*!ui9*TsLv-$RDn}GL+JMVw; z#jV@ojCl1t2YO3R2qCh^yW{Vnx;V z9i3b$A_BW$$lqId*8RP`gHeB&RS&#{Bw^E1oSxv(TU)(Pw)-Dn8+^7k-_JqDvw!sW z|Gd|?^X?|MZ=mT@di`5pf9>!{&Uihzb0|?=lWv4+<+g8lhE&x|h^;j!^DnVW2{JkbL+cmOJ!J=`y?Q-7&f_tP8>#e)LI>K|bMH{`L}iTB?f(HFe1P#qhn?fBNaCDaj_BD^xFfu`1}4 z$VH>Br8C8G#Tk;b3aO`8^l=g9Aen?x)7{C=#`vH8*=X3#hiA z$ffA}F0DAHpzp{d6>L(CuC9%fXw+w)eO9Z))~~+)N^?W*$XMxN;%BX#AybH>N3)K{ z5NpwU2qq3uWxO?0)sems`;$_Q5=Ikxgha;GorJhmP7c3Q1pWez!SvzPqYj zE;ipi=pjgTTezG}t1?uIz6nw8m%Ml?OM_ObAcL%{^xdGqSQUe)S>D9{ulO`VTa z=Akyej@^)|oH1TyCpSS7w?6*pF^uvb2=9l@WX{snF5}(f;WBvox7*)*rHg2=Jva7! z*m54td#&#btj;lWVX!*CHk~srw6k2kvz-GlbIC_>G? z(+JxLEwD`L_-ysq5Ir&;g-hgTr!(*TbVd6HK-umm`yz{^Q zUmyPO|M!=F{k90au%!=|SaC6qi-3W9XxL&`C(DFuD@zL`O~uX06LDfG^Jp`%Yc-kA zMx#mH_^gDy24^gTLTfl4EOO4$k#p|NXjZcGntD0w)#Hnu-AA$#iSCu8|%2<$v|#a}Hi-l7`cucUM_Gor73x|mzm zO02r9{Z1`>x>VIf0aYMFfU01AM#1tI4L+YwKiFCP<<|cHv-!>2{cst}w!(W~x+V214CqLo!f9B^uBXu#8Wide8fp$|1 zMSMR2bp8kVj7L^LD&WW#2`_@$F!{@`8q-^2+|tD2-X=e!3u$nTy(@lD?YQis0LP;o zP-#ScPF$t9SzwC=xWG+|nBf%0>SKKC=c%$ID)HID_=&F$UbuDm)8Fs?d~b1M(i?O6 zC$OfU7+=K)J!0b%zHs|qyK%$ov=#AYlSPr6uf9e(GD@U%iORG3gN2zNc{Dl@BUq_3 zvx;}w7(IF8iN>W88+?oaXD`uIh{+HA;HX9^u|B?nOP5N(P@aU4kBX8ECDVeWQqFsm z-SIXZm0(;8JMySl8FH6}REf&+8aR1NEoFd0wTpHqt46Xtd(%~eFW!4UdU!bc@e41D z*IsO1OHk~guLr$1kSsSE|r;M;epB-lv~_KC#^GaGIEV#`PDThbJB9&GmZHge=G8?5l3dC__EIHU38oek#>zPt(aM58g z|C2X=`4@lwPyfk3_{VR(^4gHef^_uWPf7jU` z2+OjMO9_2)s@6)AJpj~uW>MUFiTBLcFezk843jbPTxXG<6h#so#X+RC{3mZb_2+;0 zA5sNA5H{A#keT`#jt_c{X0WdVEumBCfAGTym_;=py;ipy`pt;y*0=>LZqn0 z1TIUr)lwc!)gc0UdB~Uo#A(_eC&xunt4MnTCaR!`HTR^O^!`c|5Up`0$P5Ng-FTvT zy!_OLQ&aM!H?Iu_WQZmyp}GhGE+ zqt#p|($;NQO_iIr#Mr&F)@cWQ4fW7aX_0vS91tOyXl#tF$BJ?e)w5=$k#5zLlI_xx zLXE*P2h396#Zyl_L5#vCJ!3^(_j1M6Zr4klY+mW$CACxT{94*DG>d)Y*5BpI=>FQQ-Sd zfd`$8eBbMNh5tiQU`;$|cJ3pi3!BCzN`Zmz$lhuX1@_y#be|#YMM0a?shQA$yqMnW zC?>`lD*2n`+nq^2y+P~$E?UH+&7XbW80mDf?Q_2|{i)J*ex~axqN`T-c<*ZY`KfHj zKh?Wz0lBm65hE6+3T3rlcK5MsB=7N?4|HX?&_)b3cu0tt{WneH%LbVFhG9fXt77DN zAfVqOlllIhO;9;PD%j3`esE$suc|RbB7T`$A}a>p6^(MDxaAD0tmh0f>{}U)OD0ep z9`$@RYCl1*<*TQj#>{^xQuhGX2S)~Q(C3Y%1ug~~UZ)2%N7G9lUB-|LpEvn}ULx4@6ob;*t8WrgFU^lWDc z2PVp#8Pc30esd>eI^z*rI&3BA*XzrM*(nE^-Das_OTq%N0=Sv5KNxXYl}(597{o}_ zaQO2VUj#w^Tc3T-lu;GYK#I&MrsO?Uy-!LL=uleKs(XcXA|ZVufGfJ!-7M2nmjWKC z@^1Uly>a#*?KioAuZ@DCoBeGY002M$Nkl!MG`xfD_bdY zncH!f^~8pwv1`kd{W-H?UExB}$J>lDt)PsX6@qg8 zTGQ5Yf*ya#sK5Kn*2g=&TR*w>`<>obiW&`%@-(KVFj{ZZ?GTyjgF{-lNO(a4IKCEz z#Q`k&URe+8w+hK37HGqIc}LNt78g(CRcq49_yu|AtiDe(3}G=oT?y~Loj+s5q-JF! zZ2S`Cj6_DSC%i+^<3wG^5XLaWCoI;Q{=~J!okLtU2K&=V?2iSxzLPUNvg0&nC44Xu z$-olk$TmY*kvC*XGl|#(hz_2mJ@P?4}OBKN0#^rzsU|yb^%2Q*cY^vnwZQkP z2mqpCv!OJPw0<9)jgjM)Z^O1#wvn9$WYdT|R{E z`1tzZi=BZO6JFIfZob~zej>8BHAX#5(bMQ4e)qdSOb!m5^T$al(RM4jI$51yoBePF z9ub4+zld&{2Hj-yUI2DLiNDR$w#@Moi6FO!^AUHMMzXOLCr4l|j<&Y5>K{XOWU9Zj zbB+AiF3q-S)YiB^`Un61-}}dZ`se@f-~0Qc-mn5m!_3hzhX`(Fl~VC+Ad2)k2a!&W z>@*lk*ufi7M;i&)%J3-L_%iov?5H%jwtc+~c!Fv=C!KHqlTSVgG;hmp*?1)MauZ6a zMe~v9PC^cPt^%6tY{l@KnV!TVk<<;GBFb^R$&b#6X7|x8D-c8kTbh7z70?WI`G5cW z-?uw2JoiF--F+M@rk$=GWb(9}0cnhS?B&9X(^=2@-MVj2(k0ZkrRsvV?!fk}Hy+{5U9bg+aw+dc12FmiW^(!{DM%2F7Z24Uih6&0~?U^5GjiN?I?kfYEw3bJaqCh&% zAM;;v3i92Q4+IpIHb|vEY*OWOUR|lJ^Q#4j=#7R3+z0fedt$Nl@j zxeGsB4;lu5y?A_GOZYA$sdM3{+N9@xO`H&6_vvHT|s%{GI{GI*B%}aOOok0qK2?)$57cbh5na zv+{7+B-bcVbYjTz$fmuY;1~fw(r%^RzZ*{Y+wL*zMuF`00fb7YyYlDe_jWbQw`O}Ol_t22* z8LzIdDDXp|!11)_hftfVI(%yibPC!e52$YVPDmmr{lr>jA-b6(8|8+9g~7)q5Ap9= zp-eZ@pP01O-|mo)x_f~yJ(en)M+Z)A-st5v#x?P2Bi-CNF)qFU{D;e$iB0?<@pyHk z@a%7#`KO7QsTVUk6K$EiF2;b?F*B<*`#Vr=rnio#T1(*i?(Wwe*Sf}#yFFrWje#x> zDdu9{tB0`5z(=J_PQYxkSEq%g2shm^a?MtXk1MH7nPfQ|7{(I9BH4hoCdkh7%y1$2 z3`WD@jroyHS%X=TC**JunMAw8sX!^ldr>v-yz?7bl8-5TKKuCH_tcAw#X`Xag2l_a zPxoV`Edg}mQN6_U%C$B<+}@x5fXjP)0=?%~;G>oysb=G(=<% z>jPlf3?r72)v}@yjKtwAEkoJVHldRxa<#9fLaQ!H56GBW3oH1v749K*p98P`G^T7g z15&n{R9;Db=BjY(F5C{a=W%Zr!f7(_Wsz56g;oFy10|Nbx;fP>W7;p z4P9}}&hy2`@4ep~u13ge7h<9V=w(JMHr2?u+As5jQbaU%L*yetbz%{6dh6maI>#Uz zT7_APhEeJlUZ(=NF+De>qC*5Q{+&R`7&kGBXv=swN*HC<{>Xe3p9uU6OyfJQ8#iYOa_J$ z)AsS5d{ogUthYAiUtJq~I-GqqvcYRbM%Fg4HvByj;kU9HD}st-YywK5wNEk>C<{*C z5G?9kL$E|%#ce)Kic)^dsdWgT&#AN$OirccozQRDC|aw*A7qT@$)fxiyn?@*Bn?cB z)Pj6uv;y-EfBa}{u}5Ib)5JTNmDL;F%6cn8GfOc-jM;DBd8aiL+!sLqc$3 z?JQJ%D}q``uy-N|wefTWMU#jLigQ%O?8R(twzerFl|>TLUnBM5#5b8Np2y=C+I-v{ z&I+bXaA4td0dmm8*2BpSyA~cCK6B^j+1;b3X1)F2zL$m_{a*qJNM24zUo6XIn|7ij zgDi)un~B%qr;)j`r7fqKgppAeEtR#=bH7xr@vEti5}Q~Un}v6OcMtyB8aHniQbf-3 z)^P$DRNH=+bCe!nA#X~izN25>dgD#)!y1_PK6%f1$)M3bX>Vv3VDY2F6F$~?*zVf} ztQg%@$K&v(8lY>3nnH~2c!hjgy*A=|cum;9^Nu{SWP|YKa81o2~PZ% zi`kBS)TuikVXfIiJ@es+aezi|?+q4T?hHQL8GO3kzq7sRO%hPIWRBphLnnu81z_IhbiwtH`5l*j=m|9r9egdm=Nw<3C?hD8RuAXnc{(6lQy5Sk7tG;QE zcGB&H)+}6BcyQm-9U3zFN6~aCG7vgOiq`KhpWo-NS4+A&4Wsmi0~RB%>&6Hd?of%1 z?5@}0qIpE3zFU4zT|5Ln>tNTf8`#EcNTLtFi@t0<&-vGpr>xq_fM{f)nznbxvx_sqE=*ShLtkpD;Bh$5B?oK)_^<|Fl;D2 z>B0~$c&%WTHVD|-SHJP4e7ETrnqnd?*aTXwEXoCK5X>vZ)t9aR;$?4l&isZCB#_Qi z*MNiIC&-|=BY8v-o%#Cr>t;x|Y{f{0GKRc9`X=`YDh6Z3JMX*`I`sRmzy5kP-V9=* z)2i_jCrtyRYTY5NtUoI4`&eiZ=0m&2x^;Rz#%#Q4bA{2SfzxN-+`2PyoOY@yVIo$Z zF90p)59wQD!4J)WQdO@|AVQ2LUDIflEn!GYZ+h4a!vxB(skJqELnSIN--b0;9 zD{#8v^qv_=DlEmd=F{##LjM9<>t)V_b9M7R6hOq%NXXcyv7Nmky>a ziCr_DJRDMO?!xk2<~UId30X540$(XQzlQ_+2;NG^$(oRf8Qd~1zjZ72%?qYBD8yy< zcb%~dpzg(VB?deSlq-D2X(Hm#nLI@=vp<2w43WkfWx%=!Mh)Z&@QGIiMXr9=$aVGW z5-(Wf*z<+jT3*gIkiF}CXTF=Mrs>r@JmE_^CBvS!I%t7}J%C0PQyeS&TgtuF5QbxgBl@j8pV2>U>Z z)hxAYh^ev>CzJ)I8+6(@^23im%D%_m+1`5UnWx=E1mt69OonAb=k-DeVF->gW$X^s zLeDTIwW|u|dC}X3TI7UBCpdWJX!iN7FL^qHC(*Ef^JL=nR#ef`WyI7?$|0oi)};68 zAAYw)C-+LW*i+IVA|WlUE>>BOFOD1X;UUX12r+y!N2)J{1`pNAv9-gEqpcCo$!sXT z6y(8k&p&S#i}g0uWNhN(Aa(D)^EMB3;uu)v5(XS67UPp)eVKtGHk!`1c+CRRv9*MO zE{8OlsN}lL93xv--6LR2$2Kat$s1ILRi41PgMrn;BWBo_UV9Bdo(A!qGO(wA^s8UR z-;Z7)S(jjvhAqW~8Se#E!n2ClBQD|U&e-JXQ9vqe+6xh*2B+gu$i8ggY^Fy?Ir@~C zq(&X7qcubaREO165AiFHlxCF;vB?IR3G$u(FGs8*Yo*1;=7^U7kA)Lvi(}X0%$jVb zdTrL{6B|3-IAtkkS-1MjHA{Mdou)5C<1ZQ@5}WI)o0tE66$`tUCHb%MILr(@D^ zPeN@)MAE?7+uv7xTZvL`gy)T4{-OzF%XIp?%(x(XQK1s@W@GKJg?~D(pxI*j+8Zxd z3_cDQx6~I_(sI7)b;d8GZa}uS$C?HcRsr-dn;&`_`)NRP_eI+jf$YwgATO&{)M~sD zFX^U+KFDstoX1m#ys}{8RLA{P(~J}}6~xgdWs^9Hj1xH+8Xzsc*6F)kaSwMHT>vIdR2-H z08I1W`sVgFDBQX7=3m7^O0(Sh#;X{-1%6iEtA8TC}Td2hH=w(u#USLXe~g?;24PB2B8=j z94;v}%EFM|rURmZ4ZFgwxH{q#4L#CDYpSqyA4ef$@2qyeA(l#3emQ^slb;gJJA1ny ze)LhZ(#o^m{2GPtTO;o_v260`8`X4n3UZyF<%78w9GDQl1_3CUCyR-AR(I6G;eV{p z-UY`u$oGQtsO~F>9#arXYK$7^WG@|eqiVO)W|NsnLgUOMC>$U)1yEUvR(BT!S7+fT z0mIPtG=#09{VdVVSP|*`Y*cy#*xVaLH{Q$eCBcIDrPZ`Ep6~tA8Bf$=brW zcT3(x(pyugeL)6QRjp-ReV1nLM%Tv`SUnE(&P{@AZ~zA%TVq_yb^l9^0`gv`cFZ`? z0Y0h31yhECsbG)=L`wrXU0+?esR8=7_rLE4Oti#Oxjx_<-?MD#F;*z2FJApP`$7gC zL3$r6N+*e_O)U7L_xjCOB3jFrj$tnET;5;oB=G6z(>x?C-)h4JWBK%{G|H`hzgI9Nsl(w2DF_GHgIhdRyL7 z-SyU8Us|6Q9koVn)}S#1yeijyEQ456<~t59s~ZnWfh9lF7Q%F0%W!g(Qwa<~ zheE**ZSS;g?VzV*pz*~w)!nrBgBEm^?{7Q>qD!^j>KhFqSDp1z2G1bSs%KehnKSY{ zo?C4l59FaOm4?ews#WRd)MAzA>f(w5KNt%9(VOHChLS(DiWncPs&sNLlH z<$25(DLEtDj4_)k4>D`2{Gst_-4(z92Bv96O>yJ9Iwr|OgTKIwN{g+k(Ku0pqA8Y} zNA(AXj+1<-`o>xy=AaC|DySC(xs4s~g&a)Uye|4e|50%m=gl9NdG3Ygx4sX3pN+%$z}|=l8;hA(mHv@Eqp}fmYz*N?Z?gB*_Gn>`zQg9I zU$&GJG+gw=IgurEW|o_nWn_!gX7+Qp+HGE~Z@&H-YveJ^k}2ZH=EGo^Gt~FkkcWf$ zXEyf}__Ut*aV!8A=Lc@=W}WTEjBQmGFqoCvHF0XMyY5gavadagt;w-E7FNnIo9|B# zIfBd#-G$rNx4&6621qtt(cKBoCUu=bfuk#yTuBKbKrBa*w@SIMcB~0(SaBF6N-Q3f z_(!WDmV}el$1AGzCliQ#!P*y^;FivVU6x#N=q$M_v2U)S2yl|QPvaa z7PS|94%m}IhhWk)i3@Z~9TVCW>-{(1e5L7ybdJWnb}bx_hFM}25ND)D>O2?>a#|kk zkX@E*oj6UUP(SRZym~d@x!l^V{?}iBjqp78v&rliufM@9B|Ch|dKh7iHMrgmJ}D(7 zWWVtwWBZUs+=FQ!^N%7%jG!`oY&jgR(BDgn}>6D zMD0(|&_)F{Wpqvw>SSN2ut_*_H?ldjTHqjixGomA_w4$5^zzFui*ju630O?qsG-=_ z`V`vlIW$pyg-c{l%vQl*bMFLOqPUb*mClFZOXY$6=63qQCjS7o0Pc#VP`cgx5^}Ii zA=a@MIf$X~NM*?XD)OUZgLpd93T!&snz@2jG+jbl{f+18s7G&<vbM!-t;u>&B0f@UmryPe0X=~rENlA z?|3n?H|XEl-POOa?lk#HJ*y{e9x?^&T)XZL%mOoQBpGBrsLgC`-wKU3C9A_S0XQQ$ zN_KB~+HnqYRlx-$OSDd9FVB|Bs*_TbmEx$+Y$cqIPm|ofHk$WIOX3kzUWifUl8|bl z-bE~urh?@xt1{|c;g}T@62I&2ZD#%Acrmn5IFce#Iu6)4boD9LP*}X)oAP5tjc;s? zhR?rHf_2R1*s?2Fu;I)lvtBLuto=M)Yo%2Tv5NMtKjDDk%HWmF-m}j=ITB+!KU`Rj zK(G@+Sp4KaQ2n+JEF88ofMbBYsQz?>I;j6vZ@#^UCKjwrMIuvMesXmV|KGR&ia(V+ zD3Xwiv99jS2r9DDnq=s%x^UVaE8F5TcI3$FVsYz>FBDA>1hZ=NtHP1KB2qNKGHHVm`jSkyv!{Sf9x^tHR8+1MwQkOBY=2 z&UN$gN4U;{-RzQZ2BCQY$rnGFpKd<+$*EDze7@v!H$Zdq>kcc~p$Pr8$nb z>}Ah}Mru`i>ZzyFLN6zYMmjh;0Gn!3B2H?hEztvwn<+VMfMABpi0m6;(ce)sr(Jr< z|435=EX78t61nuI#xGRL1yjS^OVzdZ9pa@He*WPPf0*AM}A6hYj`%ing#>>qN)r%+UMf+`yRZp z&bIrud6JfX;b4NW$g6`ez#C+p&2c(z!pHY6>ni&+x;r3 z%tIOOXD$En$nvcB&%bx#-L#6bre^$VOGe*%gXKu;d_b4=w0Hq{^{+8-7x7K}FHoSo z2I8&H(|@ZG!mZvv=JM)RE;2*ZYx?#&8mEuXDDr61o9*}P+?Sov>^d1lE^Z?6(lV`2 zMVI%wzvnzUmGj&FR5fOU;n8IKaQgJY;nN4RYllZ8=aj^+$s(DU(r-R~XKVW9X!iBy zq6Ksw)9|=>p_aFABXwvM^KT@I6v^ODqiQnOja;~y;I6r|OXDi5J27@P2IY*y+RfAM zd4gcpd}%e};w4EC4@Xt(W|iW+sj?J@(WV3*-II>tD!1bG!mHI-cUmy`$!Ka1RmR89iWTT{vi(#Q1Un{Qamz0N*$ z{pD9Uhr9xy>3vWdWyiRxQ&B6;92>ej?f!`$b!$35sJ9dxdwePsiY%$qUh9Cyp5GvJ zfBcB&XFZ+8;`QgtYn8fl`|Hp_Dvi`sC>>>@g`2F#(Nq)`*IZ#-eA@Eo(38~$(fuD$lE#_vwHyD!>i8{{*Sbsmd( zd}aqCjctGKWYgK^wVgm(p@Amrw8}IuHHN9GdBwq~@jw>`)tWPXdNg_E^_viAXm=$8 zC{Jk&k4T-!QRMP^h))A&D|Fu3@R8zD#Ft-wrQyWlMwdFnMKex2T!P(nQs;1fH031? zr7Fyhq7y|LpdC^Me~( zcZQ43KN?{u2}F`6i%S$6R-pzBV>j$-7abWFz1z2MqcH5~puyIbr9j_o4sH+oxBH8| z+3et8@^An0CP*bi{uVD0tT-OgJ(+Sp0T@0lM?GFr45Rk;4)j|#98}^j|Mu&X>7CbJ z-sBF*cj$TO> zTDA+JU&U%)X=JV&pX(S4_w|VpCO*NuJP?a~m5f(C!pwKlCDrS??osAu( zC{8QJ>tFx+*Nr$&Kl>vrlfB7gyYL+}{9jV$+P1Scla@`Uetes+Rkw8vB<(8#R)yk7 z+_z1s9_M+mD{5oOPlRpn)Xuc2RV29Nid5XUX@vw8fA25$5j;)z#x?6pGGYv-)e5@) zC_m6CYM0WZjxg7{4IL%Hwb8EgM8Q!h68I2oZVgj!RlP$-xAB5J7)e&SRKd08+-d|F zE5lOD3nt|7hDHhk%18|WP=LGH$yMFG5d3qP^2o&Rf|Y$d*)tMaON7e}BBXU*+un&F zWs{hU@knlw;&IahDl}H+0FC?6#0g4%@ww+`HkY5z{`#F?TLNtP7IjAq)@t)N<&wJ4 z-pRknT8Fr6AhlWjdi76mEestDe)Oa0VQ)1>42T!}(7?s4x0huDc^%ODHRTYkQkzmV z&$KS4HFgR=%Bt6QaEFV(nS5EN)wjBCsxtg(&9hx(7ZBt%Cf?o{U1M>`iYPSC7&|#D z;UE@EZIAuDd6ljxa6bw(v7riN)O(Yk^WP}hJe}p#&E>H4bhbzS+Y|=E8K|^vFv{Lj zcWS=BR^EMTwU+bh@`?iAUkaS){=dJ~xWfFhDPTwu!z#^vLV2GvKYFwn-6g0rbd)q# zOl%njm$9Un2tRyET~>2-Jw$bJ3x-cd8Z+%S$$WV5-}+4`fuXXQa@#G3dm|>fizP$A z%#)6enlS=2<9NP`6I?wDWJmAzDCf5^^v-|#`;UI}`w#!k`)@O8AKCcr!WMwDN;>;q z3EWc_c7 zAmhIh%j3$_ym6MzFe7RFEIe~L;YdnIUZ#AKMQRCOYt!Bg`&6#ew7Ton zQ#ZoA#bS!@vAL;%;>z_{`P8D}6Y9;6?Dm59HBMVJVs3rRdqZ@5t(w?YC*-J#3R-*Bc zfZ1SfG9SN3R8UA?6^*S{hRRc=Ro)X=pZHOE65}hNDAwPB^N?_K>cp{aL0^NXe){7C zP=dcTJQyq{obpaj;=r!*Z0YQCoJcXcN2yR z1C8>|{?_4qbTntn87}78Ae-C0knxqjB6@G%)`}?wKH3r>hNK|TB4|~*%nhkZuTYye z^H1Y*_}lV&#Rp0GZgVFK8lHdQ1!vTO27wsA>b%B)Cn;JU;_XhmO34xTMimki;@O?X zi%M+g*`TA@hGgD?)MAX4}#abIxM8ycbU9;@#0j36%H%2rJvRj^}Cu2$^ssv)dFdicu{ z&n7C7j};)~<>gf_Qkz%!sF7MAU2BysC*01F$Q85;T@W~aYp<-f!AHP0La>U+I3~KO zl00n>M!%x3s;?Gp`O}~NbXDSUl~%VN-Gy=?;2TqZ_LH9?&V^xDWf~cATw|}AMaPn7 z&^(gRJT~k#=xNbw$y>%PL*vSN%M-Fx-2}v*jan*=T0^p0%qK@9SUR}+kuGZf+Ray5 zaqqtW9xthJZ`xRvnni!KgaMt{EbrX?H#DgRWf$=#Fi|^e0(SQ+XWn_^jW@s{cz^k; zUqQo=k1#ZRPUTmLb9jx+nZIlAbt*yVNo`g80tcaFsAZ1UQ>C)^P4}h1id^>v_5I3l ze!YkvF@Eud7jTUZNz0u%{x*kmufKRodM}Rxy0cCy^yaaZ(7R6q2cv|E96|2Ds4B`nU*`)Dj7F+bS-V=6|M!-DNDd~6X>Zy`F;@B zT2psgpP+fy^xlcVULoe`3cag~i%|e?O8bX`^mcxQr+$uxMtq~UUU>Cyzxs<`y!h(4 zcA3^rPr=uOfA%wjrK@yBf%{XS=?BeURytOTZr}HPlVsj2)%YY2AA=UqM`T2oQ^~Ul zKXug7I&Ld<^ce72FZb&DiUN(-BP6_u}}NzpdujY(RQ+ zpir-?)$;GDN}4+zJC`btJ8ysR&VTnW|A+tfU;g+1tTr{~pr>j2&^g({E z87nLM<F_b)av8B%EKiNF^`QYp4w|Af1yaTIH z?;fW47*oovQnN1ELOUG9$J2E3n~S*&aK1Yui0tevT!~ID*&2$rBj}Uey(|S_$u3~N z^0!hmTd-40DWX2@k?++=tGgpxL&A3q!)r#Mgc_4Gv&U`Q(eN{IMR0gnA*(uE0=^rN zW68Xsc@iBX^(PRg$ZB;tU$y<@A(fGAj zUuC`WDKR7iD(1TrhE??&N>z{k!8E6}p}%bL^rA(Cv(Uc#g;Z+wc3rGm#L3&Ie>M4x z52;m-x2)u$;Rfsy4r-YCsyMp|{({qp5P)b=4l|xD<_fR045!Pg-?2vKmAti{lWj2y zp^8&WB~;NNK%jfU7w496Xx=nVnf=3G{_<~eIeJmTiQmcd71%bJ#yuamfAQzI}bs{ADShzZ!Yjdlk=eU_DO+~a^_X5cQeGg*n# zpwB7AE=)Y+gpGF*5Qd+;|Gt|M()rP=uia#f_~x5$&@K4H^p_PgK}p;3T_#RNrfT5y zg$HpB1d(%6UiSe|7tmEP)lXH;x?O47DAIyk#$!2e-n?mXgB`XA<(TpC}9D zu(4D@`2fE9>dW44wrB=kmEoPDTdK_6b^Y<6o^gJ{f}_?iqL)w97X#EWv3}_8MX}_?{?>e9N7}}PC7dOO?Ki@d_bViF z4J$j;f?keB7v0V3!n{#fyPf+Y#$@~O-FMwz`To07*8ad22T80~uvlN_X8~Hvw0_z6 zw4!7k5G4q}E0D{D+Kq?rfA}7NQ1(A>w{KZ;SZ|0%>S#e_dC&F7AAiW5+%#cm#o&kw zTs$;jTKD|mFdZT*WZF$Nc3~UZwz^ttQX5lmh*NaB9=l~u=xp+k%-~^8qOx2lTA?W8 zPPO5z5;$iZl6SK?SeC@sf|hDWZQTRw=r{pMGRz!nt&Fi*E4y*DJ)G{?0XzE~^cMf|fBB#P4L zs1sBV%1!HUm6Zmz>?hpIbzXMJKyqyMUVr1|g{589dTPDd%*wO2w#IleWG`(TmUtn2 z#6i?*=Vwz?+0H%UU9pd;)+MKsw6Iv3*sPgEZEot|%E^u2ay{I*Oa{ z^p?ykoZ;pX#H^+Yf2tWaS8KYeN1E;i*XXRVL`sc8P-!wH`S5|E1ieR!LI*5*Hr>kL z=FOXw8`cQGRckkOjv;J`Gm=W)X-v$^a+Kb1rgIhZK$j8xw761hIuVUgfUfbl;o(2})4%sG z{*(XgzxwC@#Tzfas{>6ar3 zy7aP>S;(24gJF*QW^^&fbgNww%JYbl9wNilvWJz0Jq7vsh``?y%)P4HhId|r7@JYW z{qW|+vU#DT+m1VA&`pXA0nNZ$r+zma$W(OE84NJ%8t#maHW#za#pk=@vkXU8@7-QXvb=)7G1J8EzXa@k9*(VXwiVszeeNt$YrbOxPihcdC+O!cMj3mp z?1^&5^K>%ZHF(R;z^f6P`CBY;HXoc$iHd9JN$O=3g;cGA?ljZ$3ij^pJF4rcipcIP zvCsqtB1+45W#Gk)YO@<203RFewj$czqy}dxS2Afim_Vt0f>_ltKQq-iP0!S?GJqiY z)i8vFK_63YU{*S E`y<9fZzbb&u!E7Dn~-|<^zLoNt}WJTzPOb|2sVzC+~0~A{W ztXwG`sawf+8-ckvf9}j z+vBab|N7Ty?8`ZSVo3`EDnqzo4sI@j1OZqQv#Qn*exPsk5$!0NL2+e6S5W4nsdw2O z#AV`DYGJcnfo}Yo4kzq3ghbzRP07uA*cafBZU`7XCf1oFKSu5O{{9}5eWWX3I&RAu zpJ5r_bN}4b)l}^&TGvmI8;0nPItUSgZ1LO)jU8~z%f?;CQOc$X#Z?I~sK$6~d}*L8 z+H8E^<3_N7Cute!@#=UP@rT%^Q=o8S_J7mu`rD;*d6Z9Uds{$ZA1Pja^Hu)3j^Ha0BxQFuwJ1XsmDO-#0&^{r zG6)mm@}g-jJZCB&I`kHwe)=igJR≺Lum(-yDr8?lc`Rhg+bjNW>MiRIHOy%>#%V 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 z�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%0As4L{w~dV93au#x|i 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%0AcIVUA?#c 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ÐMO-A=y@Nps0=dtrHF zW4^PMlw{)^xW`$gsnsPOI20qH+I)&*x#;A2h0H5FND-V+_V1iv9`@F;<3HYNS;Qxu z*FAtb>cBQGNobsGVHq{)Vjn(`>m1g+^XHRz-eX_57p zj+4|}0tH?RBBK%f-rpFU_plXwF#xY&ofIuFuV6%kNJI4QaoH~q=yVGe922QYPcN)z zmK9VqRVCuWrI-*hC+_)b7ng!U>s7_ife{g zb}g+IkXLp}b?m_iVJW1OR-RR;)?F(o)X;G@Uv%&PX_AYF8+3O zQ#^C>!kUmleM@`&9g}~hEOY#b5K1%61X0J^27j5GaJ|kyyfGlRiplP^G%*kLbnu@i zEULC|3FbY@{(?>OpVQ-Qf(Dp5{94zf2I=wu+m91Dg%f`7ePC~l4_3-*jnDf_Ao$p1 z)HG0ILM3X_f?DaJq7-yn{2e^O;_pYsf9~;gik(L{H~ydnwR=8hTNzC#GE!zyIgzd~ z^`5jnr?H~>7}V(OIX2-b(kq$l^KicAAUWV^3)HD(q)+>U+wmHmfrV+qdYLwQS|zH^ zKN4KLmX+3v6-k3gC<*4Tr0@oaCEEadc? ze;q+PSClA^*TQ3Lom<>54+Ny%7iE9cI##f5FVT8l`*sO~>gO4plM@IT#T|>{JZjJ# z&e_wZD5mMlv_NCL{-*8Z`$^<=tX~K`06TZ1wm$sw2g1mZWgM)fHL!%Cs|xs>Iy@H8 zmE0s`!blr;rnr*GCCL1?o<)zk!mZYX7!mH(N z%YTXpFiugrDwe6zytHt!1??uYlmni(PNrF@+5>XA_o7=Fn8)ZZ6U$AdcN$QM3l7H* z+J2A3kVNI4I0yiWxJQwOy16G9AbAJ?cfSoPU(;2aroYBq++{14;L|fwthJpyY#Q%GD_J2AzbIQKED@0~nI&H`$AeH&O zw)(k6EAV`Oj9}?y`gjK@2pxElI<=`wF)>XJ3h=hTdy72x=5{_q?|$E0Epvi7JcHoU zqBh;Cw(_-NX3~9Dh=ABnMBQ z%G%@59r_YTYC{UWO*`Hmj{I_sZv7?!0nBQz4`8Q~mzxT#mxkudNjQmoV8j6=5x$a4 zUsbs=yf&AVXbwIL1&%1b+#~!2J5AF_ih%AdaY6d6GqbI7l}3$WGER>wm7!QxlB;9- zPcrVpoS>%r_5A8s{3AMZkW>|_>m&|6@c?k2Q}$)_o&N%W=nUWQi;Amr+gogAheA9| z75*#KHBz7!sz%yoiI>8mtW1Difh5gYmsrv5{$-Z;(+T6#hwH-^qxKG~uHtA&aZ%LE zJ)z>gLK-2qq85vbX>NRuZvDk|*u<&t?%4U}Kp=l$;$r)im-=k&Ywl$o_znP>NK>2&Ai__Eu?(1kU&1ZJ~YQ6hUVlZi@9 zx{%IRR{+HhtR4l?xyyRd=v1$X3Z|*^xRl#s`PclnPKf)?C=ZkPpVz zqTg*ol&I#=`T9Rzj10_HF+yab%|8~q%KT|*=1@}FaaM)W-rH|EmvZ+*oJg5Q0|K2L z_HlPr;rDiBXA>1-*j%LGou(8w;t1;6jHhjqr5_LsyMmJGM?S?jji=t-dlud*y7I7PqT>*ws{^1|Nrc361=_G~4?-YYOSCN-ew;57eSVgnlWe2^XVOVKy=p zk-8lZZlkh=@O5mGDI<1mcaXIW5T9 zw3o%U95x~9$Nzcjs-nI06U{Y>pqMnYH4jDd@$#bmirnug2W)JFPIX*9+y`g`TN_sE zjGl|$-WuSFWa&*&nMZX0OGz|z0~F0n3?KKZ*&2y^5X;UcMapgHEmFUvA# zV12M=FV9Oi80Au+_$y%wGYY%x^5!e3wdZRw^>(I6Qiy0sLN`i;D^QI(G$qGyp2}>G zcXe4e7t%r`!0}@()7~OeDjz<8MQYPVJW`#;qRevFhqz>8!mYqWiA9cvFN*}jt~<3_ zFI`BfK^4bu(6RxwH07nNVM(jEeS1QDy&hYMi!^Ou#E&5>q3ft2Wo|a9gGm_Zw{U3Z z@*k}0fY&s{CU&VJ^rV?3zV#VuZM@x)sGoio)Cg_HkyNT)O^Zo5dn8QsW|Csb^;*Ue9$(7>$&T-RONjg&2^$5y=VlX^Qk9t(nXUD%1XiKyFdOC!%P zzuBU6`8R%x{ec#x3Efjg#noPnc-5voem=@zRdMU8H~RxtVKTH+*T8(Ghn;!zFH1tp zB_&s;A?C}wD&3*UO&5##*!Wp1ofiu{0`YvD9kX4oYp$%Vz~L`^XY9>j3DpR0e$p>~ z(PyT8)34xxVO(LD7?{yz{dJY7uUIw~&N4`Qw`?p=OAoC3lO}PEtzj@Y$hzlQb^Tg= zJ|p{M?y>NZYWy806tfw(+0Id?xh=T|c(%|p*Ri#!>BdaJkBj((9Z0F|c}pe}OKIte z+v@fe(_rY%;*j2`mZs4juWXL}vPtdc|4=mlcR>wM4A@NGAn>Oh8H39U7QLN0*eteE zD|YQL9vq{&QC#Zg7%u0o#I^AF?Aq!!W;nHceAaOFSvH*jLy}`U72d9m%4<=p zGNZ}{T8y?LZ6Knov)FzFg&mTW-MfSa^PNVqxPFgqo$1nFMY(79w{;stW0QJf#Bmit z+ZV{uXn#pQLe~JT*~$Zx57xfq1RWRrF+4EMq=5jfj9w|FjBk&vloiL4;&~Ld->QyU z_AM7}@ex%>sIw~?dz@NPF|C8qeg9vy2wNIVg*L2aDn~(o*3i+g-a`GdWMUYtX{kLqih#Y!UyH6dBpCaq#4= z*_p*cMY0-D(@M>*zPwD8F;-=_E?3h&wFWA}E&1CMeUp$gPe&!j(EdUS+woqim@VP@ zGJ8&bC%i?M%PH_1UCYQhvMcFlIxBr!RI zVRzETs)?ZC8ydEYJ=YNEGEINuJRQ9|X6$B4plc|SU-mwhev6^05A#wngeEb z1WlcxU0WxAEGVFhgkb{oz;(3>7WOK$Kp3=dWp3AO-~uvPWB%pFtI|AGIZ9yjGD&XnI7rdBJy zA#!%m*eRfo)R$5p6d&0@Cu5&T91HmDRE>+XBwrQyuJZb@P)v$)D(N~fY;j-PcF%4> zW`^I}40J1VT%NwmPdm1O@g}bBq*C$RdviYqn}^i&=wS+a=$aFd=@h>2oX2Qg_>X$& zYXMWWY~@D@zfG99i6M{l8$oD&w6mT5GiuSuY9r>LeVBmUz@gnqvz$Xf;>yStrwLSU z@Tl5p^zLLcx9fd(^VOEnOqZ@d1sWlU%1f@H0x?e_B8!Kiv8iUSMic+n>;pj4sJ)q( z{<@M1+{m=0oOz((Y4C0*J}>!s+b6ECRCwLQCq{UuN(LbopO1?ht!e#VPY4=)t-Kl< zCp+|$0y!f|grR=(t3{*(9$pPRe6pOFG|FSrg+}f=Kb|}cs+3jPJ@Yj#JOAZc@y2d2 z#bI#;8ZLzbmt<1v4TVb@x~cl#T<0D6jJQhL9hIEi6htH{kC;-b3G`qV26OwaTZrD> z%p0CNJYz&P5@Bm;dH|~vGb1J!+9pUj#)IWg*;a-OK5>vnz(4J zx$gQU!_ghB8mizO5a1@SZ{abFWtU}%yZ-WV8kQi<5Ki3AfYCT&Z_jS7BLFXa2uuVk zw7X5s!_pwpj~505RnRlqtcy#j~Thg68Um@cEN54KLldJG=vw(7h1Ag||Z%0O3c<`Q zAWT5{@MUE;dqndm%G>J7ZdLi$^v5z*q(EB5q7YOGG0lv9}p5tqEap zU0iUw4f94mzCdJ!s*8t*bAk`5(W!jyR< zr~K+Eb7pFIC!~_?`5)IhcQ0Tgq)uFLb;Xgui|fPQtDi2&m#CL9h)*^#m5ft^F(LWA zuo#OJCxap#VM;C^lnp=j5LueY&2brU*s$5l_5&W3ZGJljJsGl5Vu}x+*8%`P`0L^l z^&O*pbq50nRJ~6(Nuurn3VVU1Y2!`#HB&Cr8l<|f%HU{U46iYQXSCf$3I%9B@SFU$ zqQ;>uxEmXVoq7~MDqtA6JRv~VDzj#IQO$v(pChqah9(-53iWH(I2KS5PtA!i3Aar# z(RKh`UZm7~PF#IN@e6Y0R}>e7e~b}$S@h(N!!0$uChWMk)*{*v#m@*4j12)a`3#Ma z6%JyohIKIg_LahBMYc&F8uX!>o$8Z<1O1fB;$5T`?)M#t}j1>0}jh3WQc%({CM+oV%dUf!P&kikCaW-SY}?) zR$QX1%zm@{)t!*)$0A}ttx`0_8X#*%iKs_zQlB2T#H6$>*c@p{Y}Uw4qbAwD(OFoU z8QP?a-8;xPy%BHU3xpdTO;AFr>-l`83r@X)owE|&r8N5A0~&@55bKxQ0&KU&Cky2% zStDTG^DYBSaB`ocVcnF8klGgOIu>!EQIk<2XUxK~l6U*BsP8+s^_sOKg7499MVLfTF5QM?vv?SL`IEuuffi6I)6TjDxSYiqqTawWnSQdoF9DkW^ z?%r>R7*>2LhkR9EAB&%~+I%@&-~ewKF@snfl0Bi5H7x%*RuQ4@=OY?u?WNd}-%$S; zC|J+|>f67Kq2Q%Y)7gU$8?sg?#|8+_pEYVdOFxei<%vofySe@bj+Jzrk_p!CoPj4g z?G;cnC_O>hWW09qYAsRAt|Y>;@?T}7oHvv>#G`|vc=7+Ec8y{Ggzti9T2)&*N91C}_xU_>gq0U2cqiWi)4)!yuUFf1BL?xI3=hyi~7z zt^VW?_)wq+$X0J=M$Hqnv-;n@?tH3$-PL{}6SZu6edFKL5v8=j?HQfO9I?S~ePQn6 zdFkIsA4NzTClsY!O3KOqPtuM5{mxq)jngOlnaAjZEsYARUT#F>(6`L>DuT2&qoXac32uakb&8pE1bHp+P(C8V1B19JFAnM5Ft*&vjIsKuB+~ zEDx#-+L#IE#EZlPQ0j<3yu(hQp3Qj2#O$sUxJ!PA8XSmUO%hbR_W@T)NsRQ%f|phK zPRz_^hZpb0_&?2g@mSwLtD8Z?>!l}%l+YRljjK*l_QWJi9OKBocA5cKa_pMIi^M-P zvf)0J-4f`bm?knBi*VCg`prcX|GyW&RlfGAf|kPaW0sNw5XQN!$$HJ=yj%qsqHY|` z7*qtp`_%6Z3#!;b9QuA@JPDRM?i#N&YL!S*1#9Qn!CQ571;IhL$WZ4+sA2^s4BoII zJl1=W*v|ZliZq835*cp2Uvqy`vo7+mlP?UYjBNk%#l`hGP!wp!$sV!cDCpk9K=^UY#%<1#(pal~bihwIPvC`0e_ zjVZk0TJbY$l8l1fY(Pe}%6k@R%bMK@EVgB&8{`x;gI)Z4wD>tZ_QB%+tQh(^b`FN% zOD2Z{?%vNVFh3FUpKH36?7!=h_tOz-H4OvkwSz)W`ap>INJUMW(KH6v^n4L!>RV{; zt0v~#uK%dLMp#3~1pOBJw{}&`x~WYM2r4|e-#$6`0rrM_j+98~Dx#A7ZKkd<-Vc#F zlptWqq~fmNASUW`Fq;>J=dZ zbZ|B|R~7(zhx23S_Lx7EzB(7kx?AY&Y$B+w17$@ed62~Mi|<@@*WgTlwcM3yS%n019d%ntpoMPYtKSVk8#Uu6%O zl@l0y^6&HM4yTEdu19m9uiuboXbW}7zt=6P_LE!eH*G4;S`iv}OB;6z=a4jS5L*}r zv|rs3!AF+KZ0M1_HQclZAcULNmfREfUkSjeRO84Ix1w1ioD1BH4~4m18q0DKk@Hwh z4D;B$u}E4DtL5fJaMxK+l2WKgxyH`++(I|DwsJ;fmWR_Jc3n%5Cfq7TLJ1K1|7Z^; zXc94cgGRKHJs=?w5C77+rnH!o&DhtO@B(S`ZtVgJ5TMHU@1$48^Y>-SwEh>O4j&&~ z6m}|yLQYE7x3Jh|d4T}aJj)px2QgD{AJgPHI%56R@35#1TmQw>aa7RF`9Dsf|JOhz zrxawl8|{nNw!5SO6i`1fQf&1L;Hna}Xg$(b&`#&3JtFNB^Bm(2>e7T{N2UyZ*vKxh z=+umJ0JOHhKkCGL5DAZ_@Av82c;jKO01ALGSXg=YpZ~B=1?#d3}acqv+3lW z=c7ui9p7l*q&=A{qarcLh&r~q-kNf0*e31YC}E&nf_`gK`p(JBS*46^reuZ3^=?qu zb!AFNi0h~R_whFqO%t@K+`mo21g-=eE{(yS>aT(gy5R||LFxu-(j;^=Il$f}!Dife z;3XRq&@)2-e>enI1?Y$9P*(_M3v|l)Nd3YL!_r(m@0SfstM`Yz?xx65P~Tf;9AfQzQ{DSR#CVA&i4dbl8^7yncVCl+Fm@U6@p-lm*KhGQ z?}2-ifr5fgxj7tH5)=whra(tbgs!N1U}$6B+T#!tIN_vw;bn_aH7*RRXr`S513rY1 z^10*t)U_b0ejDcxc$;;ZMJQJ3^gStmk%Mh^)A>#9Cnsj_7UvwV=s)3TG)+u7^s^7!rx$ZGJ>0Q<+$?Zlw#!S*3@37XDyio8`J;xrredjEgNGBpNJm z*+EQa5TnZSh3Mt!aM5XEbCyCa+Ncuc3WB@kz z`xPEc8Fiv(R&arP7c#}1L~AXgLJof|SQeqIvI|#*`(R6zl=s@Lzut`h0*qf4;-|7IP{wHEzR9J<&GJb$6!}bD9a6 z6!>#lVM~Ff?tJ|*ffdr4ds%8DbM3CYWay~%;-mk%h3e(up+ak9J6Bq3u>^@;%H#~0 zPwI|Aq~J?x&GPzi$BW_Fz}Ol-Cv-EAz96+mxcu-#LR$%&MV2y-5}|qq85c-C=h30C zJe=%~Xr>8z8H&UZOs^sAIj8en_8$k`X4kTKT*#&m3IZRbM^^&WftCp4NR|0Ta5W+^ zV`B`aXl8I3vKohFRC$lp1u&P8+Jn6TY;TnSUPEWu7zEvd-4_YIN!fxs;ifD#D`m zQBfUXb)&?1vMTPD>a&QEo$GAkkRdvl%BY!D;n)ugx=*_XyyVkws8f1-xNhY236V@l z%^r>TZmt5xaVk8u2s;*M_iHO78kRt_bfNkIF_qTV``QF9|+h zNWkIPAM9jwNz`RqbuB&h^Hg(T-14QVby|{tSGLIU?h|nf+lwYFm`~7hQ)QsPMBrSt z%vrM%jxd2nwgJXwkyJnP5ZTwzj>)-;4V)@r5C2pk9-fwtWaGG-X+0-y7W1q6uU6G& zdg|3_*TrCOW%Uh#aj1F|O}eMuV?G-5S6jg|x&}N3s#ZkU(H9YFyf!5|2sdHmSbtj< z*G|uRK0z1$@yX~}^z0@^YK)+qeoILON`ul}IP`aeKqrb(6f2CD3=Z_4?2>+t3w4q> z;+1XSq-jVfRZ?M#9FKbM$%)2kH_TEB_4z+z&PskUV`W=_T) z74ZD9zs#O`)Bn$#rN=>>gE| zic)W#wfTke%UfMWD^6bY-061^YKB;4Lb|ALo4EI}*fCB(NmIS#jVg zp}OF7Z$P6i(L@jJ<71E_H)*I|<~~6NX^L1M_*aT_R+9*Zk_IDB3W!C^{0jd(s|G9W z)2-03%6)60XoHq55+W?YM9ZYYrIEUsA~u<=>i+b&U62@y<}oHe^43-b-NOcDpb)H7 zc8#z|fS7~7-dDadIRHtb;w+VJ#k3|JaL7z^S5oQs?(Vm|-vfnI&lD<5s)d}cLH3;A zy%NV}ta?y@brWa-&4X*Duasu=G}o;jVO(V^eyR}wbJk49hTYlbh8_jgmt10+{__>! z9Zk2rUizW>Mgj*3Xms5`#l70i_GmU1Ys5%BvD6D+$wPZmNrQ(H9RrWW7df{&NPAGEb;T`gDmEiI9E?vq!C%Zz*csy6Tz@-2ZIDs z?Llm(qBypFdE|j_7QO9>_QXe7@?056%gUwaMf7 zZ1#z=TFyok`@Neyhq%ZUa$Cl{C7ut~aniQcgdTuNAh{Mg>O=OYsW6LI;&IQDyxlC0 zOJ&{wmnF2Zr$XuF#*^%t!9Ek-pG{rk-+M*Dl*4Jo&v0&`2MYkR7PHkT;|A@$|71SP z`jV-#X*?vsHT=U6U_Se+Z-@s@VfDsa@_^#bB}?h^mf4CL&TssTCiM*UDW-%jZ5Ay% z1sWbnZpj6{4r}#ZhxPxVIb;cUaCJPDL@a-&_R+eFxCJ&9y72P5-Q8R8HOq{8}sI{PS3d zSh)Uf+bneq+XpJ)o^q%?q*h`}u>&lp)@nDV5DT3_B`HtJyJ8{4Ph;3WuyElzrdA+| zzdKXzmhLql(OGjZ0(mI)UNU~qj9V}&pA$}?{vgG%o_kLr>s+N2!|4J79 znu@E|)}}(TlstwhU%(a#An|LW5D8RnvMNyoL@&;;fr;Ouffs3>L%;L6KMwy6Wt9Ijr{vhR&%QDCZ!n* zGplRHH6WdR`g+qC=hxLRgC$TVVZOK=?!J1n0*hDp)z*Mxr+p^ZPu31^ol|6)piXGP zM);z*p`ekQgK4)_2*ZHS$gsF@l`Hp_41KeYg~5f74ojtrqVUufksxx;`bN?HA8VpM zYrqi8Cejn@xXV6x0*ta!0J#fiXl=s7&IpU%kJ__M_IuOa$fcCxKdYcl01een_=8k~ z?g61$M20#V3Bw&65dR<5pEo6R)sOOJhXM4nc=S=B&UdYK{cdZPcfQy3SX*H!zA zt^W#4*z~F#@u8TNEf={!i1`x^vs7kx`TnJ(-ZLer`{oj%yr#lkulGCmXY5xLqh+?w zOWizK@et0;XsIa?iK=*#*}T z>XeR5f!w*~B(>TUZk|16-6+Z6vx6}uT$-^OV6i);-Ak| zyUdsH+oWS)DI42g_^E1yB07f#zh7FL!REcM-g>|8a2H^`1Mw7iQu=0_W*8pw3G&Eo z|BND}h|}0+m`6S7Fx%B3kYFR>!HoGW@b4ws6l2#X?p3dQ42r(9v^bFsSnt#}DO!|X zib@(Ybs6>eoAbZi_<)1iW~#b68hz#E#=K~NaD zSH{&@1d&i~|Kte%Y2YDw3Ozy+M(?~ZZ*%St&S)&$pG^qD7(`hp8-r~bK1QKz9wcwI z>JAlFVKdk0$N~F?h4h{_W4W+g@m+xG=m6cHJaWXFqjh#x0hN3+qfj)eLkZCk4Z*KE z;els1X1C#naVJV+$xer>EYVvOLE8$A?JS&vN#Rc^3kUO1Q+1=t@{`*h;fm;!$yYkF?nGm_wkuobJJMzXjIu+g9UtrdiKo+%4Kk$ zX~`H(%UbYgA7FHG5N5N*rQxp?V7g2FA&wrgF2C?ZbE?3>>7O4eox9w%LqzT$b@Z*a zZ3r+oj+1tn>xyg;?qlm!N#UdPe6YM$`4g}!uA_5M|IViDu-78lLw^$pxvOZCl~SPE zLDuy^x8c#EaRI30$kLIIxLNPj{hwuM|9b!+6mYq?Z%#R4&nF8!w8;F{hBtlJsq`0+ zd55!qo}+f}5nKEYC+P22;t3hrI0C^4ECZNIUAsV-8@b;0dGb_glsv2X2|j@H@YDxlruF+>eT=2}AZo93&$qSG=z4r>Mo*?`}2AgA!Y>g8^J`Z@K}mn|uN zk+k=HDN|&1xEjMrk}6m(8FNST%Q@5%Sy>_M+aytkad5W04mGUnhjQ6y4-&81p`2*R zV)IGGk!5Nmn=XHb*sOViryMCo){3sgZ_ewu$rou?xQVyE;@T;M^{*sQYR3pgEw}jd z$#&UUW;EcGHXRt6j=Rw$gg7VRkIqEACOZRWuxSpe5H|XOly#k?z6Y?}D;vR>EvqLZ zr1ooq-JILh^LD_2tHGu-3q1OW+<<;r#1vW!%>^TnH`Tg zVt<4L1pU}M*2Nvo*%;cvB|1nT#UZ}k8d0qo`0zaBt}Vp9K(a_8eF^-JSDzso?3}Vz}+eP84#a zZ@hH|HUKZe%<}Z+Mq&97gp)o>08u&+Q*U$m{uSRpfsPX$1QW_%n5gn|Mgh)(hq#Bm zvZ7M0^{(Li0W}SJ2EK9TG|iEAeczcHb1oijz||#Zzz=3w1_Cu$Jm!y+;E(V;cAT@j zam9X}KbHZ&>pA{k{vfUWPKi(NvRG?e2LXC(M@3D-Nk79^G&fVjzo?YR9WX9C8)aSN zWFCQXycBV{`x=(tz=pWaat8xYkZvG5Z7WFsF!8g5N;B@h*5j?{0}8-`#XOid z!pDyeOykJw3XJZ8yOl$@ks!ZkArbMxjG5WR$YwP>k||?+PXT9o##nYuGzk$C|F%)G zIi=TFJ-r=B+meeszIW9D5`!cx6>F3XTfWbH>?C}6dq(wPH`H%UBC8TY$DyEBSTsCz z_iQ=yl*J`AonFsZPVFKD;e<)g8tB%@tkk!4Bwwf8VUNGya`?CJdEe@a4VwenRMoNA&AOdsXG__^6c{LE#ex#c8HtwL!LwhCnI3e!;T6^%v8sZ` zJLdC(GZuF;?>7S!JwBS34cfc@*Px~k{f8OB)N37T+ZuF~{AFc~7J6vsspwX@c!sF& zi%qAFY`udNUP^zaKT!dl`wNgwn=st1u1~`(#Q864LsE1=BWRj#h2f^@keadN&jaNy zsT#E`WkWwOfLxtLbeT+sEW|0Xh0P>rrUBRuBSh3&)xdBii&z|CR!DbP zQ?F>%p{@h-Fe;h4<9i>G#NQ%^Ty$)@4+=VKd@3eCC5CU`o^alyXbB3C(cgB0-Jx+G zN62s@o*$ZTuok4srA%GmjrZ1cU(q96tn9k{lzKCMAqsOZ?A^!JPKV=v- z%YE_UvTvBjHrUrt>xO;gN5`WS2ef)GPZ@9e<7tS{mljO!ZNE&-k2CiMl+eUg86#@U zH8YN_6_m*U&Y|{U8UvQ!GZ_@y-!~R4i|@6CzH_%C7a9_Ja&M|gJiQ=}3s~(s#lji% z_S@$5Kja;!q#9Qe4qO@ZRAGo??VWS+^F!=hl8<5`e=nLV{vviwp}Tidor*oNM?;Vg z%}X;HE5J+yt!!`md#jY}O_R<6dZ6)mv|4MJcYo&gRiv4(SnAMOa0n;~!9H%iV#f$u z7aKn4jm97kBbSh1FW_W-ElI@uvK~@gHo*NdzjydIRdJ;? z)Q2>!lGJ!(4aL-95yX!VUK}F-3*iYN*{OVZf>o(i1n_r-Hkal&KJjGFCx+)*3lXA4 zN{o7WSpj+@ioG=Q9`7-!`r_)wu=$w_i+G`6^U3tpzjPU%<62(+eqUkz*2yYLV?3)M z2K$UXR!Kx+WHD4ojbwzu(cTaAthkD;_Z^?+f7A$z%v%5Y^U&f-vkCN?Ei$_r$J1? zbLDQgk}eXe2#m1j%!+JsW0zNy`XV09p}SxRS{oE#i?ouT6odjx>e>&EbxClQ8zPJL zel2cOM>EP*ApJ2lV+MI9lSmj*h3r4lgUBDB*{Nqu;rjZsB!H8Iv6c}EdOW`&k$s64 zbzde+)tF1N#mh*=sPJSR4`A(Sm2JpXua^qBi6{ul_hP2$%{dI~Dq+ku#I(0R$p+@X zYg8TW%ep+FM&DfFdmxd-i2>DNGv-&UiTI4UkinkXX{?%m>H!k~7qw--on*^d{X@H7 z@T=5bz&rOda=)sIaP_{s!isoHw9K zjM2&gaT_`6r%7~SSTw0mr7Ygv`~b19mt#eW+|d#(VCDRDOX%*TO(RdRh)GyZAqgg0 zDUg7*vLzFEnopq89(v6C14-wX7fDC+FV)GMD22H)F)Z*PQ<8-zH8oy6cEI|o_4kng z2qdz4!X)c%OA^sCTcpg5E?;-&!o+1+?_0NI-pQ&FeZO#lPPi)uhbr9{OcqFWb;>R= z8-Fh;65E6Q_sQ43{8x(qNT9AN3n0p)O!S1<7HqwGA{EV7Q}xc*=Dkfuw81cplt}w? zlgarLv`^BRFc9fVE)UbQ(v8y0RAqN6^YcUg`!e^t@;kD_tEKc(vQdY>+ohX(!T@R6 zzXpn7|1Aris@mcI=VrTI?#Cn``3Lhc5q1IbIX)| zuPsD^{pZW*_m$sQ?rDVkyIin)^TkZCzO|!VSehoo73MxR$oP#~(B8q-FZ@OOz9jDLzrEqDYvrO`UY(MTGHJJe#3&Xp!zjk<9 z@&Zz?h~J;zc@a4HQ>}CBy#4pIkjnR0zelvmbN*uw<$pB*HNpYN1Ull1+cC@_!F_5N zLxbWwz#brlNOH4kf2|^X^?ou<`32K!ygnvL9Mw2;752y@yrW(1<@9d;<)r{oOfbA4 z2LcDO9FC!!{49p^dw=`9Ac7$t_9 zP&oS9o`*^_6p$GV!y)q9N=fCPvpz+r*7Hk#HP(%DPb(wrapq17vbtb@NXk_%nTAV_ z{#D`E8BaLuXLGT**|~5m99~_f4>sglV%3V7IGtG!gLQOq07C#tYk5GYBe8Hy7c=E2 zWXKi8o-{i^1x6Z&Rf0wo6P z3~lr^C(V$pY8URYG#@>c@|l!gR`hB1?O+cUo(o2 zBAfk3jjS}g6i=4LhZQ~j4|UixT7#y_MX~c1jiI?#H)y}7ljnbuaee4*FKuno*Siw9f&bIcIU#a;6@l75!t&-5)zWIZ{}lP zxGbkk)Zz9Go)AJ}&x~?Epk%&LR7_%j3dCi{B8QUkkI+pNzJG~2B5`3b!s ztS@CHi5>|m|MrKQ)5P_5O<#LihGGZRJPkuO6FdwErE1MQ;H2Pa1U!{h25#ms>}`L58%Bdw}~d#u>d>o*GH(IFK| z?(jij0e|P5m5QJ|oDP!_2BUZjCX_YL!A+ac)E-m5-B+5LOFPoN|bVnhLlar+K71yB;W5@zu-~0Tk`-G2f~t%nn+vB0V?ViO70eRgf^LR z@X8ZgfVb4RPV3IK-S~7hrl^EU=e>aRLSchOq(h%#f)517+JZ?n!J=_XksbKup7^LM zfuh`D-olWIKMcjC#0LVi_9Ig+XM}3Okpq$jc#1AskdRR)G24uc^;N?wlnT>NcFF85 zN1LKCV6J9EVx6ol9RcW?$yUf^R`W1@y8Rkj#!0LWtfhPi8y6=FgFIiAw;4k*1C58| zi7eFn8nR^uo6b7-lVj$6_|su|Y&=a;SuI6noh;TknHkMTGPvajGHz()u*6$?2qfVI zA2G*(tCROGaQpi|i3Gp=3mPa41NRi5p1>E|TB+J;aCD3P%(8kkcQ#MQKF2pWZ!NR# ze}IJ6$Ax6R{gFHOkKPCB-VnHyJ;?$~rj6-&mlokBzI?wQg5r4H;RY(3Z%yb^GZ&* zu-4!!ONJ;laoKDu7u*$N5n2B__3(H!NgYHT9voR`zY&@TzXK@R2$UoMenIrQKoZU< z2&Y-j1ezy7RjK zu-+{y)fuh+nUnsgJmIt@`{W>Lj1!M)vT+Q7HaG7uNl7hxdG!ObD?ify&4jD)^0Kxf zD<9pP&L#u;U1q0w=%5BZU6Pk*_s(3EI8FZH{lYG~23@HD;Mg>E{%dF&AlfL|RJrs^TvQy5!zZ*toxrw`+0-<~G{vIMn z08>h?pN~uyGwcrX8o0)sKa5pac6B|~hCLz2((g31X`kh6{>QGwZ&uo~2DQF>x}4nY zrEW2;F|E(T<>M-R@)b}TU~*P3z19GMuiR5h1c(^}%XKu)K$~6#SEIJ?koI>4j97H! zk149{{r2>i{)Y+4U*y?N1Ppy8E-o%A$)m*#`MvIXJ8+vg>=rUmp#J&t^Yr6x_N>`y z7$^mhhzeyp>4-Rr2q?kOLYnL&kOWcEml`HmA)f`1ZBKI*%KIr>7$)^VW985dBwnK9 z<3&%!MCG+mb>96+fZtOAw}`3elRQ*OaXv9FwB*RH@T)dT36QMn?l(wE8+3-kT9|*G zuUVRu%Fo|_&FBTxcjzj58{yJdG|>eR*;i`4-U=C-1GT5bi5adY-m?0`Ky@3N_v^(_ z&%5N*Lh2}v?f~PHio6po_R#m~&r`MMnsrQL+;K=+Y_fkEuTR)GswzVk;m(=H0K>VJ zHJluAXe!ug&6LB^Pl@(f1YE)9ACZ}i)W#}jQ@7Z(Sebl_ox&QxB9WRH9X|~oua3Ec zrWX|;8INj;frYE(p)30xdKGW6V0dG=SxT`^^%^!J`}Rpl7SViKmO$%8!{ ztXg2`4}tCKHgsWk!yd#=1f^#smjEj6YIGb4lXFT;Tc?7Ny1a`Z7pvC5q+NV;_~Rn^ zK0o`TUW@AttOX6;CJUN*&$02@W-eTsInw)XWE8NkkwofKcZsuop97qX7JPRRnR5B| zKjh~bH2|uIL1_a-&(3WER;Oi+eK~Y;PAwax-MJa-(~dDlAO8MuS-3z=1@hrG^?Pst z6MmPnVP_}OtQqY``k2`JYZ`)VAPnV(wawZoa42{oYLm-F0N^r<`nAdnRE3bd44dr} z3$oYo1+Rj&jEJg@CzQzJr}>11%|g@o8AF}cqG;yhFv=R_41Mgp^Ni1_Jh+vbThdsL z?^v_p&)acA?A!+OvVl*y!OR=}p-hM1NzEKJ#ignH-kXX*(eJ(3Z*xinh#eG|46&m} zH)AY9)pKVi4Aa&iVbC+!sKQmc>Q_+1#$d){G0e1*^|?nc6GdZAV~!BYo-Z~H$Xcn; za!>%25WzTm8sCdp^}740QbWIlj<{U3BUhg8x5k-qT$N_^OXP`03(XLXRrV;Fwdd~E zo6)(0o=vSOWxGYOpvNP-)tSk4tJ*HCWE0cOo(6UzQoDa}D~7IlEQMcNqj()4|40jQ z{080G>XuIxl@F+=>elB(0;8bjHye@9vF*D2&0)yiks_RbZQAb|+Z6h)%#uRJm>5!B+VlH4<2;33mc^G@NZlN`=d}emH#zG)p3irX!osSbYr>rr8WTz#!`sOT ze2H#?!O8LIJSCVCMAi{?@SyEdUf#6=cO_s7U+wzoyEF17Qt!0}mAodZ6W^I;)0@ZWlBo-I&LX#iqHEp!%qtqp} zcxxp^nto*Wmg)^Aqro`}+lzqCzRo=)4ubaktYnWmC91j!EM9$CK>guaq<@)X(P0?` zq4vrB?T^{uH>VnuP$_^7DVGIC>e3nPR zv=L!j)OGzj{mLr+`)h=0Mg;b-nH+X$&|BOMAR;qdEVZ{q%@V|emcuA-Kjl783><%Yx-Kc-`@!LX+xvW)eLu&PE=*pJyvyS0 zrtcfDH(1V&3}wt$DgOSL;ggyu(Zch+9qUDUH!_l-H{kFH|1%mm0us0=!X!ciRhtLOpX~P}oW+z7LL&XoIIa_D zK~uo6;!M-8<9^$3bz?34WUwNiw#gc=52F^4OrQMEYcQMzQ<6K88N)kFvz6y;O~h`i z!$4d7w_|BDH_HP?2?uBkHDX=bxBRl_h^89y-!@DxFp@qW322-=LbHsnd?A>+*p4Tx zL|)}8=naS~6PmwCciwY(u+{aHeIl#~o%)n-X}>if}1at{|w<59_T80HbR(0hyu z1J;ab_jyG;z8LVSlpkdG5vRzd8NRSAa5hEuEG`oiy*b>-ZoQC&U2y+dW2P(eOVRT! z8%LX2jb>*XY~b{X62ns;?tS!p>0sd2XU!fq6jB|?DSD`p&Z(Z(bL7=Q6O4i(b->{P z)d~w^O@piH$~^>2jXQBbaMdgM zzH9b{?-?-EaGk61q$H7m@l}sCxLTsKi7-aBF<}y_LD#eP#0Xc#q-!0%c`VFH4fUxZ zIy*2D_}Wr}$y-xlQjc~g9%EznTF`NYEbcm{{CwBMin=tw`Z3^Nyn1LMGsDB zvj_h#<+c83@-_{}NeP{7?uMv;G=q4vMCk0VWj-Hs=7S)5^8IH*Tbop<$A+^q_BuW~ zQ>eDKCN{c^pLq9M$}5jn2Ob_{R#dPPbZ}YxBEAFXHeipS=9_$?IC&bA$3RQfxVNgG z?J1?IT5$|S@Lp|NXj17B_tShR}Y=|4w7F1{%XLdNLnkX1|;(91MkvCNM0S6O@W zqw&cmMfCaKj@5^U`n>+dVP$%>#v=Kr3gcqBz58$gqr47s0`45xccH!OUp9%VRmCH7 zKG%(;w*X&CC!Oy*t(8Q#iFgV}7aVlS4!xQ5QCHwcMO8z@p9=|a^}sGnxngkP9va;b z9Tk#?O;4*J#(~1A`%tscXquf`4>W$`i8{lo`!g$VK>hc(fbZhYTBmZE9u$Qu0M z8?A-IU={}VaRRULv21Pr9MK{JXy~s8DnrO%Fc;Ph5@>4{&a%Kt7m8ykpv-y;-Iw}S z-C$SwRu=Ent{pJSAhsTAfRDpUQAMj#1)YazRl?MK^{SVTYhFiMQ!XHfDbEkStWIf8 zUo2R(gtQkighZ#5;9lR}qfs9Kt0bd#oMA1l3P&WQ80yG)YP+>%KD>WO?id3Kan5!z zNUc+09jHHZ94oX8vc|LKO2w5)>uE;it-q=*sIIfhq zj#S9rq}Uf3+o2l{>`q!vqxd7XtlYzOR&wuMXwB4_Z?{A&}7x&8+2>^m9VxRW2c)9 zaqRxJI5r&CV^YI_39B++V0*xTKTWPDDtqG@UgVp04?%Eq^Qv+zls%e5B+u~BZQo6_ za;5_cNp*R)y{c3p@BHvuP_+yBk7*$9T@(m}^N*E^3-j4VO6KYmVJi@8iBYtJafaje zq~?}9c&+p)qwx1F(laLdi+ng|1pTxWPOo^^-ZvTOb>cE&ATmNyL1+En?@7dsxAO-i zQT+~PJ@#V9;$$}tc2?8vWWeI?3{QCRrG-_b;d_{##wFOe4gxjw@_uDwFjb*kPq03lDjFtX6<`@Yt#N$rMiYDz$2p#==sgE79+p$Yp)nztKEA5iAVWMmnBV779;o>8szVFS z$%s#LX*B`8U}`u^)p&UCjWfF_Z}q5Mfq~g_zS*=b)#-eN*OK!t%v}UV@889sX%jam zogaFyq`L|cWz(^Vy1qK(K${9xyQryxL6cQys9%swW;DIsEqH2MI^{{c0PuB7n_3{q!A+Dg#r2DX z*Gh3|Jh0tPIG*ePV(BjwVjYO+B)$3E=qTaH)^NVrv3kXg=q81toHC6uE2;L1#|S4W zT!Y$aP!gs4ZH*$Bwx4I9ZA-xN#SD?M0U-wMmIvU`NF%a_2=gn4N^{C?l*Lh#bSBvz zX8gG>?LH(>7!58kL7JYYXSOLFIDHgG@&i8(i)>Ob^eDQz$>iR!O*SWzo?JJ`;LS;u zcAxTATm>-~ZJeLfD7L`S45UPR(E={B9-8#fh08-&Dv4;h%Aa@9S^T@F6j36)>-D0u z?JI0S>|HkZ50}dC*$apIZ1%0XDjL7E5tSZuEV4<$zi(wWgW?&J+b+{}H|{O9awO!? zQ71#MLG{stVLs$j7#PVSkKLcn-_dmkV>ot`<+~(Ug z+xa?eFAL^&%>qCgZTBFkIwM<2eYW6e2uw_FSD&5g!?92Kt+RFxB%QTbPcoIn>!K$Un zHMQ|tLGRA%NW(G44m<`4rsKJvi+bMQg&(=9sX>mUM)%%?=P+DAhyJ-9c zO(@|aw`d!v&4f{#LP$onSe6TXE{Jgq=W~MsE3$stXxG?HxSvz^I<~+PCG$7*Wy4LC z=fZ$SgRoUWmhE~>^_1RSRMbu+A!!qHNl~Lg!3?Fu9YX5b4$>Kq~XnQax+C4t9AYzykipW5I=PoOThkw*JIH29=51M{u8dDKGT z6iPSks4RBo@>OvI!$uGd)AS&{;_umGzJDnr553>L{06?p_WW`+7V(YXbU8(RUwnqx znh6^vB&ThlN1SR^(FttER7r)c97#)L%)qNZ@vp~g0~dOe4Zifc#Wt@4qEb=?>3l>HO}$1qA$ zaAod~iilT`Nmf-Y0ftjx*4X=r{*%)c)4$osD@G<4Rgx57hg91ZW zRAC2sg|kaqb2?pvU43pmgbm*s;Fi93AhMoL-HyQEkbHbI>*eN(S`_`*Bg`L2#$fX- zu$-7j%#Xv)W$jP0`^YSXU5qm8;%$3P9a_>iwvBcixuJM$wW(gc`Op2079jq);B6bo zOLwi%vsVrE`6d$YlY)pUcuU%D+ig*oa#0+&`dyfgOhS%;K!S1%AW4Vk?!y!o2l

  • T_2 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ξ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 UTHKGq0e)1TK36DzV)FJs0OiSjU;qFB diff --git a/examples/geo3k_vlm/run_geo3k_vlm.py b/examples/geo3k_vlm/run_geo3k_vlm.py deleted file mode 100644 index 0106d2beb..000000000 --- a/examples/geo3k_vlm/run_geo3k_vlm.py +++ /dev/null @@ -1,132 +0,0 @@ -import os - -import miles.utils.misc as U -from miles.utils.external_utils.command_utils import execute_train, get_default_wandb_args - -MODEL_NAME = os.environ.get("MILES_SCRIPT_MODEL_NAME", "Qwen3-VL-2B-Instruct") -assert MODEL_NAME in {"Qwen2.5-VL-3B-Instruct", "Qwen3-VL-2B-Instruct", "Qwen3-VL-4B-Instruct", "Qwen3-VL-8B-Instruct"} - -NUM_GPUS = int(os.environ.get("MILES_SCRIPT_NUM_GPUS", "1")) -EXTERNAL_RAY = int(os.environ.get("MILES_SCRIPT_EXTERNAL_RAY", "0")) - - -def prepare(): - U.exec_command("mkdir -p /root/models /root/datasets") - U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") - dataset_name = "chenhegu/geo3k_imgurl" - _, partial_name = dataset_name.split("/") - U.exec_command(f"hf download --repo-type dataset {dataset_name} --local-dir /root/datasets/{partial_name}") - - -def execute(): - - ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " - - rollout_args = ( - "--prompt-data /root/datasets/geo3k_imgurl/train.parquet " - "--input-key problem " - "--label-key answer " - '--multimodal-keys \'{"image": "images"}\' ' - "--apply-chat-template " - "--rollout-shuffle " - "--rm-type math " - "--num-rollout 3000 " - "--rollout-batch-size 64 " - "--n-samples-per-prompt 8 " - "--rollout-max-response-len 4096 " - "--rollout-temperature 1 " - "--global-batch-size 512 " - ) - - eval_args = ( - "--eval-interval 20 " - "--eval-prompt-data geo3k /root/datasets/geo3k_imgurl/test.parquet " - "--n-samples-per-eval-prompt 1 " - "--eval-max-response-len 4096 " - "--eval-top-k 1 " - ) - - grpo_args = ( - "--advantage-estimator grpo " - # "--use-kl-loss " - "--kl-loss-coef 0.00 " - "--kl-loss-type low_var_kl " - "--kl-coef 0.00 " - "--entropy-coef 0.00 " - "--eps-clip 0.2 " - "--eps-clip-high 0.28 " - ) - - optimizer_args = ( - "--optimizer adam " - "--lr 1e-6 " - "--lr-decay-style constant " - "--weight-decay 0.1 " - "--adam-beta1 0.9 " - "--adam-beta2 0.98 " - ) - - sglang_args = ( - "--rollout-num-gpus-per-engine 1 " - "--sglang-mem-fraction-static 0.6 " - f"--sglang-cuda-graph-bs {' '.join(map(str, [1, 2, 4, 8] + list(range(16, 257, 8))))} " - ) - - fsdp_args = ( - # Set to true for FULL_STATE_DICT mode, false for SHARDED_STATE_DICT mode (default) - # "--fsdp-full-params " # Uncomment this line to enable full params mode - # Set the bucket size for weight update - "--update-weight-buffer-size 536870912 " # 512MB - "--train-backend fsdp " - "--gradient-checkpointing " - "--sglang-attention-backend fa3 " - "--attn-implementation flash_attention_3 " - ) - - misc_args = "--actor-num-nodes 1 " f"--actor-num-gpus-per-node {NUM_GPUS} " "--colocate " - - # misc_args += ( - # "--use-dynamic-batch-size " - # # TODO pick a good value - # "--max-tokens-per-gpu 2048 " - # ) - - # true_on_policy_args = ( - # "--sglang-enable-deterministic-inference " - # "--sglang-rl-on-policy-target fsdp " - # "--deterministic-mode " - # "--true-on-policy-mode " - # ) - # true_on_policy_envs = { - # # TODO note: "Ring" in original RL PR, "allreduce:tree" in SGLang - # # "NCCL_ALGO": "Ring", - # "NCCL_ALGO": "allreduce:tree", - # "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", - # "CUBLAS_WORKSPACE_CONFIG": ":4096:8", - # } - - train_args = ( - f"{ckpt_args} " - f"{rollout_args} " - f"{optimizer_args} " - f"{grpo_args} " - f"{sglang_args} " - f"{fsdp_args} " - f"{eval_args} " - f"{misc_args} " - f"{get_default_wandb_args(__file__)} " - # f"{true_on_policy_args} " - ) - - # Submit Ray job - execute_train( - train_args=train_args, - num_gpus_per_node=NUM_GPUS, - megatron_model_type=None, - extra_env_vars={}, - ) - - -if __name__ == "__main__": - prepare() - execute() diff --git a/examples/geo3k_vlm/run_geo3k_vlm.sh b/examples/geo3k_vlm/run_geo3k_vlm.sh new file mode 100644 index 000000000..051efc285 --- /dev/null +++ b/examples/geo3k_vlm/run_geo3k_vlm.sh @@ -0,0 +1,225 @@ +#!/bin/bash + +# Qwen3 VL RL training on geo3k dataset +# Supports both megatron and fsdp training backends +# Usage: +# MILES_SCRIPT_TRAIN_BACKEND=fsdp ./run_geo3k_vlm.sh +# MILES_SCRIPT_MODEL_NAME=Qwen3-VL-2B-Instruct ./run_geo3k_vlm.sh + +# Configuration +TRAIN_BACKEND=${MILES_SCRIPT_TRAIN_BACKEND:-"megatron"} +MODEL_NAME=${MILES_SCRIPT_MODEL_NAME:-"Qwen3-VL-8B-Instruct"} +DATASET_NAME=${MILES_SCRIPT_DATASET_NAME:-"chenhegu/geo3k_imgurl"} +NUM_GPUS=${MILES_SCRIPT_NUM_GPUS:-8} +DATASET_LOCAL_NAME=$(basename "$DATASET_NAME") + +# Validate MODEL_NAME +VALID_MODELS=" + Qwen3-VL-2B-Instruct + Qwen3-VL-4B-Instruct + Qwen3-VL-8B-Instruct + Qwen3-VL-30B-A3B-Instruct + Qwen3-VL-235B-A22B-Instruct + Qwen3-VL-2B-Thinking + Qwen3-VL-4B-Thinking + Qwen3-VL-8B-Thinking + Qwen3-VL-30B-A3B-Thinking + Qwen3-VL-235B-A22B-Thinking +" +if ! echo "$VALID_MODELS" | grep -qw "$MODEL_NAME"; then + echo "Error: MODEL_NAME must be one of: $VALID_MODELS" + exit 1 +fi + +MODEL_NAME_LOWER=$(echo "$MODEL_NAME" | tr '[:upper:]' '[:lower:]') + +# External Ray flag +if [ -z "$MILES_SCRIPT_EXTERNAL_RAY" ] || [ "$MILES_SCRIPT_EXTERNAL_RAY" = "0" ]; then + USE_EXTERNAL_RAY=0 +else + USE_EXTERNAL_RAY=1 +fi + +# Cleanup +pkill -9 sglang +sleep 3 +if [ "$USE_EXTERNAL_RAY" = "0" ]; then + ray stop --force + pkill -9 ray +fi +pkill -9 miles +sleep 3 +if [ "$USE_EXTERNAL_RAY" = "0" ]; then + pkill -9 ray +fi +pkill -9 miles +pkill -9 redis + +set -ex + +export PYTHONBUFFERED=16 + +# Detect NVLink +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +# Download model and dataset +mkdir -p /root/models /root/datasets +if [ ! -d "/root/models/${MODEL_NAME}" ]; then + hf download Qwen/${MODEL_NAME} --local-dir /root/models/${MODEL_NAME} +fi +if [ ! -d "/root/datasets/${DATASET_LOCAL_NAME}" ]; then + hf download --repo-type dataset ${DATASET_NAME} --local-dir /root/datasets/${DATASET_LOCAL_NAME} +fi + +# Common args +CKPT_ARGS=( + --hf-checkpoint /root/models/${MODEL_NAME} +) + +ROLLOUT_ARGS=( + --prompt-data /root/datasets/${DATASET_LOCAL_NAME}/train.parquet + --input-key problem + --label-key answer + --apply-chat-template + --rollout-shuffle + --rm-type math + --num-rollout 3000 + --rollout-batch-size 64 + --n-samples-per-prompt 8 + --rollout-max-response-len 4096 + --rollout-temperature 0.8 + --global-batch-size 512 +) + +# required for vlm datasets +MULTIMODAL_KEYS='{"image": "images"}' + +EVAL_ARGS=( + --eval-interval 20 + --eval-prompt-data ${DATASET_LOCAL_NAME} /root/datasets/${DATASET_LOCAL_NAME}/test.parquet + --n-samples-per-eval-prompt 1 + --eval-max-response-len 4096 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.6 + --sglang-cuda-graph-bs 1 2 4 8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 136 144 152 160 168 176 184 192 200 208 216 224 232 240 248 256 +) + +# Wandb args (only if WANDB_API_KEY is set) +if [ -n "$WANDB_API_KEY" ]; then + WANDB_ARGS=( + --use-wandb + --wandb-project miles-geo3k-vlm + --wandb-group ${MODEL_NAME_LOWER}-${TRAIN_BACKEND} + --wandb-key ${WANDB_API_KEY} + --disable-wandb-random-suffix + ) +else + WANDB_ARGS=() +fi + +MISC_ARGS=( + --colocate +) + +# Backend-specific args +if [ "$TRAIN_BACKEND" = "fsdp" ]; then + BACKEND_ARGS=( + --train-backend fsdp + --gradient-checkpointing + --sglang-attention-backend fa3 + --attn-implementation flash_attention_3 + --update-weight-buffer-size 536870912 + ) + MODEL_ARGS=() +else + # megatron backend (default) + BACKEND_ARGS=( + --train-backend megatron + --load /root/models/${MODEL_NAME} + --tensor-model-parallel-size 4 + --sequence-parallel + --pipeline-model-parallel-size 1 + --context-parallel-size 1 + --expert-model-parallel-size 1 + --expert-tensor-parallel-size 1 + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 4096 + --attention-dropout 0.0 + --hidden-dropout 0.0 + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + --attention-backend flash + --megatron-to-hf-mode bridge + ) + + # get MODEL_ARGS from scripts/models for megatron backend + MILES_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." &>/dev/null && pwd)" + MODEL_ARGS_FILE=$(echo "$MODEL_NAME" | sed 's/-Instruct//g; s/-Thinking//g; s/Qwen3-VL-/qwen3-/g; s/-2B/-1.7B/g') + # VL models require rotary-base 5000000 + MODEL_ARGS_ROTARY_BASE=5000000 source "${MILES_DIR}/scripts/models/${MODEL_ARGS_FILE}.sh" + +fi + +# Start Ray if not using external Ray +if [ "$USE_EXTERNAL_RAY" = "0" ]; then + export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} + export no_proxy="127.0.0.1,${MASTER_ADDR}" + ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus ${NUM_GPUS} --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 +fi + +# Build runtime env +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node ${NUM_GPUS} \ + --multimodal-keys "${MULTIMODAL_KEYS}" \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${BACKEND_ARGS[@]} \ + ${MISC_ARGS[@]} \ No newline at end of file diff --git a/examples/geo3k_vlm/run_geo3k_vlm_sft.sh b/examples/geo3k_vlm/run_geo3k_vlm_sft.sh new file mode 100644 index 000000000..764a7df39 --- /dev/null +++ b/examples/geo3k_vlm/run_geo3k_vlm_sft.sh @@ -0,0 +1,186 @@ +TRAIN_BACKEND=${MILES_SCRIPT_TRAIN_BACKEND:-"megatron"} +MODEL_NAME=${MILES_SCRIPT_MODEL_NAME:-"Qwen3-VL-8B-Instruct"} +DATASET_NAME=${MILES_SCRIPT_DATASET_NAME:-"chenhegu/geo3k_imgurl"} +NUM_GPUS=${MILES_SCRIPT_NUM_GPUS:-8} +DATASET_LOCAL_NAME=$(basename "$DATASET_NAME") + +# Validate MODEL_NAME +VALID_MODELS=" + Qwen3-VL-2B-Instruct + Qwen3-VL-4B-Instruct + Qwen3-VL-8B-Instruct + Qwen3-VL-2B-Thinking + Qwen3-VL-4B-Thinking + Qwen3-VL-8B-Thinking + Qwen3-VL-30B-A3B-Instruct + Qwen3-VL-235B-A22B-Instruct + Qwen3-VL-30B-A3B-Thinking + Qwen3-VL-235B-A22B-Thinking +" +if ! echo "$VALID_MODELS" | grep -qw "$MODEL_NAME"; then + echo "Error: MODEL_NAME must be one of: $VALID_MODELS" + exit 1 +fi + +MODEL_NAME_LOWER=$(echo "$MODEL_NAME" | tr '[:upper:]' '[:lower:]') + +# External Ray flag +if [ -z "$MILES_SCRIPT_EXTERNAL_RAY" ] || [ "$MILES_SCRIPT_EXTERNAL_RAY" = "0" ]; then + USE_EXTERNAL_RAY=0 +else + USE_EXTERNAL_RAY=1 +fi + +# Cleanup +pkill -9 sglang +sleep 3 +if [ "$USE_EXTERNAL_RAY" = "0" ]; then + ray stop --force + pkill -9 ray +fi +pkill -9 miles +sleep 3 +if [ "$USE_EXTERNAL_RAY" = "0" ]; then + pkill -9 ray +fi +pkill -9 miles +pkill -9 redis + +set -ex + +export PYTHONBUFFERED=16 + +# Detect NVLink +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +# Download model and dataset +mkdir -p /root/models /root/datasets +if [ ! -d "/root/models/${MODEL_NAME}" ]; then + hf download Qwen/${MODEL_NAME} --local-dir /root/models/${MODEL_NAME} +fi +if [ ! -d "/root/datasets/${DATASET_LOCAL_NAME}" ]; then + hf download --repo-type dataset ${DATASET_NAME} --local-dir /root/datasets/${DATASET_LOCAL_NAME} +fi + +# Common args +CKPT_ARGS=( + --hf-checkpoint /root/models/${MODEL_NAME} + --load /root/models/${MODEL_NAME} +) + +SFT_ARGS=( + --rollout-function-path miles.rollout.sft_rollout.generate_rollout + --prompt-data /root/datasets/${DATASET_LOCAL_NAME}/train_formatted.parquet + --input-key messages + --apply-chat-template + --rollout-shuffle + --num-epoch 3000 + --rollout-batch-size 128 + --global-batch-size 128 + + --loss-type sft_loss + --calculate-per-token-loss + --disable-compute-advantages-and-returns + --debug-train-only +) + +# required for vlm datasets +MULTIMODAL_KEYS='{"image": "images"}' + + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-5 + --lr-decay-style cosine + --min-lr 1e-6 + --lr-warmup-fraction 0.1 + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.95 +) + +if [ -n "$WANDB_API_KEY" ]; then + WANDB_ARGS=( + --use-wandb + --wandb-project miles-geo3k-vlm-sft + --wandb-group ${MODEL_NAME_LOWER}-${TRAIN_BACKEND} + --wandb-key ${WANDB_API_KEY} + --disable-wandb-random-suffix + ) +else + WANDB_ARGS=() +fi + +# Backend-specific args +if [ "$TRAIN_BACKEND" = "fsdp" ]; then + BACKEND_ARGS=( + --train-backend fsdp + --gradient-checkpointing + --attn-implementation flash_attention_3 + --update-weight-buffer-size 536870912 + ) +else + # megatron backend (default) + BACKEND_ARGS=( + --train-backend megatron + --tensor-model-parallel-size 4 + --sequence-parallel + --pipeline-model-parallel-size 1 + --context-parallel-size 1 + --expert-model-parallel-size 1 + --expert-tensor-parallel-size 1 + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 4096 + --attention-dropout 0.0 + --hidden-dropout 0.0 + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + --attention-backend flash + --megatron-to-hf-mode bridge + ) + + # get MODEL_ARGS from scripts/models for megatron backend + MILES_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." &>/dev/null && pwd)" + MODEL_ARGS_FILE=$(echo "$MODEL_NAME" | sed 's/-Instruct//g; s/-Thinking//g; s/Qwen3-VL-/qwen3-/g; s/-2B/-1.7B/g') + # VL models require rotary-base 5000000 + MODEL_ARGS_ROTARY_BASE=5000000 source "${MILES_DIR}/scripts/models/${MODEL_ARGS_FILE}.sh" +fi + +# Start Ray if not using external Ray +if [ "$USE_EXTERNAL_RAY" = "0" ]; then + export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} + export no_proxy="127.0.0.1,${MASTER_ADDR}" + ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus ${NUM_GPUS} --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 +fi + +# Build runtime env +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train_async.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node ${NUM_GPUS} \ + --multimodal-keys "${MULTIMODAL_KEYS}" \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${SFT_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${BACKEND_ARGS[@]} diff --git a/examples/geo3k_vlm_multi_turn/README.md b/examples/geo3k_vlm_multi_turn/README.md new file mode 100644 index 000000000..9dca4d6b6 --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/README.md @@ -0,0 +1,41 @@ +# VLM Multi-Turn (FSDP backend, geo3k dataset) +Training VLM with FSDP on [geo3k dataset](https://huggingface.co/datasets/hiyouga/geometry3k) with multi-turn reasoning with interactive environment feedback, using GRPO. For dataset, we used the [processed version](https://huggingface.co/datasets/VeraIsHere/geo3k_imgurl_processed). + +The multi-turn rollout is implemented through a custom generate function `examples.geo3k_vlm_multi_turn.rollout.generate`, overriding the original generate function. + +In terms of the environment interaction, this example initializes a custom interactive environment in `examples/geo3k_vlm_multi_turn/env_geo3k.py` with the APIs below. +
    +Environment API (geo3k) + +- `build_env(sample: Sample | None = None, args: Any | None = None, **_) -> Geo3kEnv`: constructs the env. +- `reset() -> tuple[dict, dict]`: clears internal state. +- `step(response_text: str) -> tuple[dict, bool, dict]`: parses the actor's response text and update the state. Return new observation, a flag that marks whether the task is done, and step_info. +- `format_observation(observation: dict) -> dict`: converts an env observation into a chat message. +

    + + +The reward model is the default math RM. + +![VLM multi-turn geo3k reward](vlm_multi_turn_geo3k_reward.png) + +## Reproduce +```bash +# 1) Set environment variable +export WANDB_API_KEY=... +export MILES_SCRIPT_MODEL_NAME=Qwen3-VL-2B-Instruct +export MILES_SCRIPT_NUM_GPUS=4 +export MILES_SCRIPT_TRAIN_BACKEND=fsdp + +# 2) Download the dataset +hf download --repo-type dataset VeraIsHere/geo3k_imgurl_processed --local-dir /root/datasets/geo3k_imgurl_processed + +# 3) Run the script: +cd /root/miles +python examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py +``` + +## What each file does +- `examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py`: downloads model, sets training/rollout args, and launches the run. +- `examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_config.yaml`: specifies `max_turns` and `rollout_interaction_env_path` for the multi-turn rollout. +- `examples/geo3k_vlm_multi_turn/rollout.py`: custom multi-turn rollout that calls SGLang for token generation, builds loss masks/log_probs, enforces max_turns, and early-stops on max_new_tokens. +- `examples/geo3k_vlm_multi_turn/env_geo3k.py`: geo3k tool-calling env that parses {...}, scores math answers, and returns tool feedback per turn. diff --git a/examples/geo3k_vlm_multi_turn/__init__.py b/examples/geo3k_vlm_multi_turn/__init__.py new file mode 100644 index 000000000..526e3ae7c --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/__init__.py @@ -0,0 +1 @@ +# Multi-turn VLM Sokoban example package diff --git a/examples/geo3k_vlm_multi_turn/base_env.py b/examples/geo3k_vlm_multi_turn/base_env.py new file mode 100644 index 000000000..05d9632b3 --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/base_env.py @@ -0,0 +1,25 @@ +class BaseInteractionEnv: + """ + Base class that defines the explicit contract for interaction environments. + """ + + def reset(self): + raise NotImplementedError + + def step(self, response_text: str): + raise NotImplementedError + + def close(self): + pass + + def format_observation(self, observation: dict) -> dict: + observation = observation or {} + content = [] + multimodal = observation.get("multi_modal_data") or {} + + for _, images in multimodal.items(): + for image in images: + content.append({"type": "image", "image": image}) + + content.append({"type": "text", "text": observation.get("obs_str", "")}) + return {"role": "user", "content": content} diff --git a/examples/geo3k_vlm_multi_turn/env_geo3k.py b/examples/geo3k_vlm_multi_turn/env_geo3k.py new file mode 100644 index 000000000..be089df9b --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/env_geo3k.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +import json +import logging +import re +from copy import deepcopy +from typing import Any + +try: + import orjson # type: ignore +except Exception: # pragma: no cover - optional dependency + orjson = None +from examples.geo3k_vlm_multi_turn.base_env import BaseInteractionEnv + +from miles.rollout.rm_hub import grade_answer_verl +from miles.rollout.rm_hub.math_utils import extract_answer as extract_boxed_answer +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + +# Matches the JSON payload emitted between ... tags. +TOOL_CALL_RE = re.compile(r"\s*(\{.*?\})\s*", re.DOTALL) +# Accept either name; verl uses `calc_geo3k_reward` while the instruction refers to `calc_score`. +SUPPORTED_TOOL_NAMES = {"calc_score", "calc_geo3k_reward"} + + +class Geo3kEnv(BaseInteractionEnv): + """ + Minimal interaction environment for multi-turn geo3k with a scoring tool. + + The model is expected to emit a {...} payload that includes + an `answer` argument. We run the math reward checker against the ground truth and + return the score as the next observation. The episode ends immediately after each + step; responses are provided but no further turns are taken. + """ + + def __init__(self, *, ground_truth: str | None = None, max_turns: int | None = None): + self.ground_truth = str(ground_truth) if ground_truth is not None else None + self.tool_calls: list[dict[str, Any]] = [] + self.last_tool_score: float | None = None + self.turn = 0 + self.max_turns = max_turns + + def reset(self): + self.tool_calls.clear() + self.last_tool_score = None + self.turn = 0 + # No initial observation is needed; the question lives in the prompt. + observation: dict[str, Any] = {} + reset_info = {"ground_truth_available": self.ground_truth is not None} + return observation, reset_info + + def close(self): + """No resources to release.""" + return + + def _extract_tool_call(self, text: str) -> dict[str, Any] | None: + """ + Parse the latest tool call payload from the assistant response. + Supports the {...} convention used in the + SGLang multi-turn templates. Tool tags are mandatory. + """ + matches = list(TOOL_CALL_RE.finditer(text)) + raw_json = None + if matches: + raw_json = matches[-1].group(1).strip() + + if raw_json is None: + return None + + payload = self._parse_tool_payload(raw_json) + if payload is None: + return None + + name = payload.get("name") or payload.get("function", {}).get("name") + arguments = payload.get("arguments") or payload.get("function", {}).get("arguments") or {} + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError: + logger.warning("Tool call arguments are not valid JSON; rejecting tool call.") + return None + + if not name: + return None + return {"name": name, "arguments": arguments} + + def _score_answer(self, answer: str) -> float: + """ + Use the same logic as the single-turn math reward model. + We accept either boxed or raw numeric strings by retrying with a boxed wrapper. + """ + if not self.ground_truth: + return 0.0 + + answer = answer.strip() + candidates = [answer] + if "\\boxed" not in answer: + candidates.append(f"\\boxed{{{answer}}}") + + for candidate in candidates: + try: + if grade_answer_verl(candidate, self.ground_truth): + return 1.0 + except Exception as exc: # pragma: no cover - defensive + logger.debug("grade_answer_verl failed on %s: %s", candidate, exc) + continue + return 0.0 + + def _extract_answer_from_text(self, text: str) -> str | None: + """ + Prefer a concise answer by pulling the last \\boxed{} chunk; fall back to the last + non-empty line (capped) to avoid echoing the whole response body. + """ + boxed = extract_boxed_answer(text) + if boxed: + return str(boxed).strip() + for line in reversed(text.splitlines()): + cleaned = line.strip() + if cleaned: + return cleaned[:512] + trimmed = text.strip() + return trimmed[:512] if trimmed else None + + def _extract_balanced_json(self, text: str, start: int) -> str | None: + """ + Best-effort balanced brace extraction starting at `start` (index of an opening '{'). + Keeps string-awareness to avoid terminating inside quoted braces. + """ + depth = 0 + in_string = False + escaped = False + for idx in range(start, len(text)): + ch = text[idx] + if ch == "\\" and not escaped: + escaped = True + continue + if ch == '"' and not escaped: + in_string = not in_string + if not in_string: + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return text[start : idx + 1] + escaped = False + return None + + def _build_tool_feedback(self, score: float, parsed_answer: str) -> str: + """ + Provide concise feedback for the model to continue reasoning. + """ + turn_idx = self.turn - 1 # zero-based + # Send the final reminder one turn before the true last turn so the model sees it in time. + last_warning_turn = None + if self.max_turns is not None: + if self.max_turns >= 2: + last_warning_turn = self.max_turns - 2 + else: + last_warning_turn = self.max_turns - 1 + is_final_turn = last_warning_turn is not None and turn_idx >= last_warning_turn + + if score == 1.0: + return ( + f"calc_score result: {score}. Parsed answer '{parsed_answer}' matches the reference. " + "You can now stop reasoning and provide the final solution in \\boxed{}." + ) + if score == 0.0: + if is_final_turn: + return ( + f"calc_score result: {score}. Parsed answer '{parsed_answer}' does not match the reference. " + "Your answer is wrong. You may need to reason in a different way. Don't repeat your answer unless necessary. " + "Since you only have one chance to answer, don't call tool again. You should provide your final answer in the form below Answer: \\boxed{$Answer} where $Answer is your fiinal answer to this problem." + ) + return ( + f"calc_score result: {score}. Parsed answer '{parsed_answer}' does not match the reference. " + "Your answer is wrong. You may need to reason in a different way. Don't repeat your answer unless necessary." + ) + + # Called during rollout after receiving a model response + def step(self, response_text: str): + self.turn += 1 + is_final_turn = self.max_turns is not None and self.turn >= self.max_turns + tool_call = self._extract_tool_call(response_text) + info: dict[str, Any] = {"tool_call": deepcopy(tool_call)} + + if not tool_call: + info["tool_executed"] = False + obs = { + "obs_str": "No tool call detected; ending the episode.", + "role": "tool", + } + return obs, True, info + + name = (tool_call.get("name") or "").strip() + arguments = tool_call.get("arguments") or {} + if name not in SUPPORTED_TOOL_NAMES: + obs = { + "obs_str": ( + f"Tool `{name}` is not supported. " + 'Call `calc_score` (or `calc_geo3k_reward`) via {"name": "calc_score", "arguments": {"answer": ""}} (format must be (JSON))' + "to check your solution." + ), + "role": "tool", + } + info["tool_executed"] = False + return obs, is_final_turn, info + + raw_answer = arguments.get("answer", None) + parsed_answer = "" if raw_answer is None else str(raw_answer) + if not parsed_answer.strip(): + obs = { + "obs_str": ( + "Tool call detected but no `answer` was provided. " + 'Call `calc_score` (or `calc_geo3k_reward`) via {"name": "calc_score", "arguments": {"answer": ""}} ' + "to check your solution." + ), + "role": "tool", + } + info["tool_executed"] = False + info["answer_missing"] = True + return obs, is_final_turn, info + + score = self._score_answer(parsed_answer) + self.last_tool_score = score + tool_record = {"name": name, "answer": parsed_answer, "score": score} + self.tool_calls.append(tool_record) + info.update(tool_record) + info["tool_executed"] = True + + obs = { + "obs_str": self._build_tool_feedback(score, parsed_answer), + "role": "tool", + "tool_score": score, + } + + return obs, is_final_turn, info + + def _parse_tool_payload(self, raw_json: str) -> dict[str, Any] | None: + """Parse tool payload strictly as JSON. Malformed payloads are rejected.""" + loader = orjson.loads if orjson is not None else json.loads + try: + return loader(raw_json) + except Exception as exc: + logger.warning("Failed to decode tool call payload: %s", exc) + return None + + +def _extract_ground_truth(sample: Sample | None) -> str | None: + """Resolve the ground-truth answer from label or metadata.""" + if sample is None: + return None + if sample.label is not None: + return str(sample.label) + # metadata = sample.metadata + # for key in ("answer", "ground_truth", "label"): + # if key in metadata and metadata[key] is not None: + # return str(metadata[key]) + return None + + +def build_env(sample: Sample | None = None, args: Any | None = None, **_: Any) -> Geo3kEnv: + """ + Construct a Geo3kEnv. Ground truth is pulled from sample.label or metadata. + """ + ground_truth = _extract_ground_truth(sample) + max_turns = args.max_turns + if max_turns is None: + raise ValueError("max_turns must be set via --custom-config-path in the custom config file.") + if ground_truth is None: + logger.warning("Ground truth answer missing; calc_score tool will always return 0.") + return Geo3kEnv(ground_truth=ground_truth, max_turns=max_turns) diff --git a/examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_config.yaml b/examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_config.yaml new file mode 100644 index 000000000..ad2dd6fef --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_config.yaml @@ -0,0 +1,2 @@ +max_turns: 3 +rollout_interaction_env_path: examples.geo3k_vlm_multi_turn.env_geo3k diff --git a/examples/geo3k_vlm_multi_turn/rollout.py b/examples/geo3k_vlm_multi_turn/rollout.py new file mode 100644 index 000000000..733c2aea1 --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/rollout.py @@ -0,0 +1,371 @@ +from __future__ import annotations + +import importlib +import importlib.util +import sys +from pathlib import Path +from typing import Any + +import torch +from examples.geo3k_vlm_multi_turn.base_env import BaseInteractionEnv + +# When executed as a module: python -m examples.vlm_multi_turn.rollout +from miles.rollout.sglang_rollout import GenerateState +from miles.utils.http_utils import post +from miles.utils.processing_utils import encode_image_for_rollout_engine +from miles.utils.types import Sample + +DEFAULT_ENV_MODULE = "examples.vlm_multi_turn.env_geo3k" + + +def _load_env_module(env_path: str | None): + """Load the interaction environment module from a module path or a file path.""" + target = env_path or DEFAULT_ENV_MODULE + module_path = Path(target) + if module_path.suffix == ".py" and module_path.exists(): + spec = importlib.util.spec_from_file_location(f"rollout_env_{module_path.stem}", module_path) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot import environment module from {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + return importlib.import_module(target) + + +def _build_env(env_module, sample: Sample, args: Any): + """Instantiate the interaction environment using the provided module.""" + build_fn = env_module.build_env + if not callable(build_fn): + raise ValueError("Environment module must expose a callable `build_env(sample, args)`.") + try: + return build_fn(sample=sample, args=args) + except TypeError: + # Fallback to positional signature + return build_fn(sample, args) + + +def _encode_observation_for_generation( + tokenizer, + processor, + message: dict, + metadata: dict | None, + apply_chat_template: bool, + apply_chat_template_kwargs: dict | None, +): + """ + Encode a single observation turn that may include images/videos in the content list. + Trim out the system/tool preamble added by the chat template so only the observation tokens remain. + """ + tools = metadata.get("tools") if metadata else None + apply_kwargs = apply_chat_template_kwargs or {} + + trim_length = 0 + dummy_messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "I am a user."}, + ] + + if apply_chat_template: + dummy_prompt = tokenizer.apply_chat_template( + dummy_messages, + tools=tools, + tokenize=False, + add_generation_prompt=False, + **apply_kwargs, + ) + formatted_prompt = tokenizer.apply_chat_template( + dummy_messages + [message], + tools=tools, + tokenize=False, + add_generation_prompt=True, + **apply_kwargs, + ) + trim_length = len(tokenizer.encode(dummy_prompt, add_special_tokens=False)) + else: + formatted_prompt = [message] + + multimodal_inputs = None + multimodal_train_inputs = None + if processor: + # Convert content-embedded images/videos into multimodal inputs for the processor. + from qwen_vl_utils import process_vision_info + + images, videos = process_vision_info([message]) + multimodal_inputs = {"images": images, "videos": videos} + processor_output = processor(text=formatted_prompt, **multimodal_inputs) + prompt_ids = processor_output["input_ids"][0] + multimodal_train_inputs = { + k: v for k, v in processor_output.items() if k not in ["input_ids", "attention_mask"] + } or None + else: + prompt_ids = tokenizer.encode(formatted_prompt, add_special_tokens=False) + + if trim_length: + prompt_ids = prompt_ids[trim_length:] + + image_data = [] + if multimodal_inputs and multimodal_inputs.get("images"): + image_data = [encode_image_for_rollout_engine(img) for img in multimodal_inputs["images"]] + return prompt_ids, image_data, multimodal_inputs, multimodal_train_inputs + + +def _merge_multimodal_train_inputs(chunks: list[dict | None]) -> dict | None: + """ + Merge per-turn multimodal_train_inputs with a single concat per key. + + Note: Only torch.Tensor values are merged; non-tensor fields are ignored by design. + """ + if not chunks: + return None + + values_by_key = {} + for chunk in chunks: + if not chunk: + continue + for key, val in chunk.items(): + if val is None: + continue + values_by_key.setdefault(key, []).append(val) + + merged = {} + for key, values in values_by_key.items(): + if all(isinstance(v, torch.Tensor) for v in values): + merged[key] = torch.cat(values, dim=0) + + return merged + + +def _initialize_resources(args: Any, sample: Sample): + env_module = _load_env_module(args.rollout_interaction_env_path) + max_turns = args.max_turns + if max_turns is None: + raise ValueError("max_turns must be set via --custom-config-path in the custom config file.") + state = GenerateState(args) + url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" + sample.metadata = sample.metadata or {} + env = _build_env(env_module, sample, args) + config = {"max_turns": max_turns} + return env, env_module, config, state, url + + +def _prepare_initial_inputs(sample: Sample, processor, tokenizer): + if processor: + processor_output = processor(text=sample.prompt, **(sample.multimodal_inputs or {})) + prompt_ids = processor_output["input_ids"][0] + sample.multimodal_train_inputs = { + k: v for k, v in processor_output.items() if k not in ["input_ids", "attention_mask"] + } or None + else: + prompt_ids = tokenizer.encode(sample.prompt, add_special_tokens=False) + + image_data = [] + if sample.multimodal_inputs and sample.multimodal_inputs.get("images"): + image_data = [encode_image_for_rollout_engine(img) for img in sample.multimodal_inputs["images"]] + return prompt_ids, image_data, sample.multimodal_train_inputs + + +def _prepare_start_state(sample: Sample, state, args: Any, sampling_params: dict): + prompt_ids, image_data, init_mm_train = _prepare_initial_inputs(sample, state.processor, state.tokenizer) + current_image_data = image_data + multimodal_train_inputs_buffer: list[dict | None] = [] + if init_mm_train: + multimodal_train_inputs_buffer.append(init_mm_train) + + if not sample.tokens: + sample.tokens = list(prompt_ids) + response_tokens: list[int] = sample.tokens[len(prompt_ids) :] if len(sample.tokens) >= len(prompt_ids) else [] + sample.loss_mask = sample.loss_mask or [] + sample.rollout_log_probs = sample.rollout_log_probs or [] + sample.response_length = len(response_tokens) + + budget = None + if args.rollout_max_context_len is not None: + budget = args.rollout_max_context_len - len(sample.tokens) + elif sampling_params.get("max_new_tokens") is not None: + budget = sampling_params["max_new_tokens"] - len(sample.tokens) + return current_image_data, response_tokens, budget, multimodal_train_inputs_buffer + + +async def _run_inference_step(url: str, tokens: list[int], sampling_params: dict, image_data, tokenizer): + payload = { + "input_ids": tokens, + "sampling_params": sampling_params, + "return_logprob": True, + } + if image_data: + payload["image_data"] = image_data + + output = await post(url, payload) + response_text = output["text"] + if "output_token_logprobs" in output["meta_info"]: + new_tokens = [item[1] for item in output["meta_info"]["output_token_logprobs"]] + new_log_probs = [item[0] for item in output["meta_info"]["output_token_logprobs"]] + else: + new_tokens, new_log_probs = [], [] + finish_type = output["meta_info"]["finish_reason"]["type"] + return response_text, new_tokens, new_log_probs, finish_type + + +def _process_env_step(env: BaseInteractionEnv, response_text: str, tokenizer, processor, args, sample_metadata): + observation, done, _ = env.step(response_text) + if done: + return None, None, None, None, True + + next_user_message = env.format_observation(observation) + obs_prompt_ids, obs_image_data, obs_multimodal_inputs, obs_multimodal_train_inputs = ( + _encode_observation_for_generation( + tokenizer, + processor, + next_user_message, + sample_metadata, + args.apply_chat_template, + args.apply_chat_template_kwargs, + ) + ) + + bos_id = tokenizer.bos_token_id + if bos_id is not None and obs_prompt_ids and obs_prompt_ids[0] == bos_id: + obs_prompt_ids = obs_prompt_ids[1:] + + return obs_prompt_ids, obs_image_data, obs_multimodal_inputs, obs_multimodal_train_inputs, False + + +def _append_to_sample( + sample: Sample, + response_tokens: list[int], + tokens_to_add: list[int], + logprobs: list[float], + loss_mask_val: int, +) -> None: + sample.tokens.extend(tokens_to_add) + response_tokens.extend(tokens_to_add) + sample.loss_mask.extend([loss_mask_val] * len(tokens_to_add)) + sample.rollout_log_probs.extend(logprobs) + sample.response_length = len(response_tokens) + + +def _update_multimodal_state( + sample: Sample, + current_image_data, + obs_image_data, + obs_multimodal_inputs, + obs_multimodal_train_inputs, + multimodal_train_inputs_buffer: list[dict | None], +): + if obs_image_data: + current_image_data = (current_image_data or []) + obs_image_data + + if obs_multimodal_inputs: + if not sample.multimodal_inputs: + sample.multimodal_inputs = obs_multimodal_inputs + elif isinstance(sample.multimodal_inputs, dict) and isinstance(obs_multimodal_inputs, dict): + for key, val in obs_multimodal_inputs.items(): + if val is None: + continue + if ( + key in sample.multimodal_inputs + and isinstance(sample.multimodal_inputs[key], list) + and isinstance(val, list) + ): + sample.multimodal_inputs[key].extend(val) + else: + sample.multimodal_inputs = obs_multimodal_inputs + + if obs_multimodal_train_inputs: + multimodal_train_inputs_buffer.append(obs_multimodal_train_inputs) + + return current_image_data + + +def _should_stop_on_finish(sample: Sample, finish_type: str) -> bool: + match finish_type: + case "length": + sample.status = Sample.Status.TRUNCATED + return True + case "abort": + sample.status = Sample.Status.ABORTED + return True + return False + + +def _update_budget(budget, consumed: int): + if budget is None: + return None + return budget - consumed + + +def _finalize_sample(sample: Sample, tokenizer, response_tokens, multimodal_train_inputs_buffer): + sample.multimodal_train_inputs = _merge_multimodal_train_inputs(multimodal_train_inputs_buffer) + sample.response = tokenizer.decode(response_tokens, skip_special_tokens=False) + sample.response_length = len(response_tokens) + if sample.status is None: + sample.status = Sample.Status.COMPLETED + return sample + + +async def generate(args: Any, sample: Sample, sampling_params) -> Sample: + """Custom multi-turn rollout that interacts with a pluggable environment.""" + assert not args.partial_rollout, "Partial rollout is not supported for interaction rollouts." + + env, env_module, config, state, url = _initialize_resources(args, sample) + sampling_params = sampling_params.copy() + current_image_data, response_tokens, budget, multimodal_train_inputs_buffer = _prepare_start_state( + sample, state, args, sampling_params + ) + try: + env.reset() + if budget is not None and budget <= 0: + sample.status = Sample.Status.TRUNCATED + return sample + + cur_sampling_params = sampling_params + for turn_idx in range(config["max_turns"]): + if budget is not None: + cur_sampling_params["max_new_tokens"] = budget + + response_text, new_response_tokens, new_response_log_probs, finish_type = await _run_inference_step( + url, sample.tokens, cur_sampling_params, current_image_data, state.tokenizer + ) + _append_to_sample(sample, response_tokens, new_response_tokens, new_response_log_probs, loss_mask_val=1) + budget = _update_budget(budget, len(new_response_tokens)) + + if _should_stop_on_finish(sample, finish_type): + break + if budget is not None and budget <= 0: + sample.status = Sample.Status.TRUNCATED + break + + obs_prompt_ids, obs_image_data, obs_multimodal_inputs, obs_multimodal_train_inputs, done = ( + _process_env_step(env, response_text, state.tokenizer, state.processor, args, sample.metadata) + ) + if done: + sample.status = Sample.Status.COMPLETED + break + + obs_log_probs = [float("-inf")] * len(obs_prompt_ids) + _append_to_sample(sample, response_tokens, obs_prompt_ids, obs_log_probs, loss_mask_val=0) + budget = _update_budget(budget, len(obs_prompt_ids)) + + current_image_data = _update_multimodal_state( + sample, + current_image_data, + obs_image_data, + obs_multimodal_inputs, + obs_multimodal_train_inputs, + multimodal_train_inputs_buffer, + ) + + if budget is not None and budget <= 0: + sample.status = Sample.Status.TRUNCATED + break + if turn_idx + 1 >= config["max_turns"]: + sample.status = Sample.Status.COMPLETED + break + + return _finalize_sample(sample, state.tokenizer, response_tokens, multimodal_train_inputs_buffer) + finally: + try: + env.close() + except Exception: + pass diff --git a/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py b/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py new file mode 100644 index 000000000..4c8798e4d --- /dev/null +++ b/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py @@ -0,0 +1,167 @@ +import os + +import miles.utils.misc as U +from miles.utils.external_utils.command_utils import execute_train + +MODEL_NAME = os.environ.get("MILES_SCRIPT_MODEL_NAME", "Qwen3-VL-2B-Instruct") +assert MODEL_NAME in { + "Qwen3-VL-2B-Instruct", + "Qwen3-VL-4B-Instruct", + "Qwen3-VL-8B-Instruct", + "Qwen3-VL-2B-Thinking", + "Qwen3-VL-4B-Thinking", + "Qwen3-VL-8B-Thinking", +} + +NUM_GPUS = int(os.environ.get("MILES_SCRIPT_NUM_GPUS", "4")) +EXTERNAL_RAY = int(os.environ.get("MILES_SCRIPT_EXTERNAL_RAY", "0")) +TRAIN_BACKEND = os.environ.get("MILES_SCRIPT_TRAIN_BACKEND", "fsdp").lower() +assert TRAIN_BACKEND in {"fsdp", "megatron"} + +DATASET_NAME = "VeraIsHere/geo3k_imgurl_processed" +DATA_ROOT = "/root/datasets/geo3k_imgurl_processed" +TRAIN_DATA_PATH = os.path.join(DATA_ROOT, "train.parquet") + + +def get_megatron_model_type(model_name: str) -> str: + model_type = model_name.replace("-Instruct", "").replace("-Thinking", "") + model_type = model_type.replace("Qwen3-VL-", "qwen3-") + return model_type.replace("-2B", "-1.7B") + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + data_missing = not os.path.exists(TRAIN_DATA_PATH) + if data_missing: + U.exec_command(f"hf download --repo-type dataset {DATASET_NAME} --local-dir {DATA_ROOT}") + if not os.path.exists(TRAIN_DATA_PATH): + raise FileNotFoundError(f"Dataset not found. Expected local dataset at {TRAIN_DATA_PATH}; ") + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " + + wandb_args = ( + "--use-wandb " + "--wandb-project miles-dev " + "--wandb-group geo3k_vlm_multi_turn " + "--wandb-key ${WANDB_API_KEY} " + ) + + rollout_args = ( + f"--prompt-data {TRAIN_DATA_PATH} " + "--input-key problem " + "--label-key answer " + '--multimodal-keys \'{"image": "images"}\' ' + "--rm-type math " + "--apply-chat-template " + "--custom-generate-function-path examples.geo3k_vlm_multi_turn.rollout.generate " + "--custom-config-path examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_config.yaml " + "--rollout-shuffle " + "--num-rollout 3000 " + "--rollout-batch-size 64 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 4096 " + "--rollout-temperature 1 " + "--global-batch-size 512 " + ) + + # eval_args = ( + # "--eval-interval 20 " + # f"--eval-prompt-data geo3k_eval {TRAIN_DATA_PATH}@[0:64] " + # "--n-samples-per-eval-prompt 1 " + # "--eval-max-response-len 4096 " + # "--eval-top-k 1 " + # ) + + grpo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 1 " + "--sglang-mem-fraction-static 0.6 " + f"--sglang-cuda-graph-bs {' '.join(map(str, [1, 2, 4, 8] + list(range(16, 257, 8))))} " + ) + + fsdp_args = ( + "--train-backend fsdp " + "--gradient-checkpointing " + "--sglang-attention-backend fa3 " + "--attn-implementation flash_attention_3 " + "--update-weight-buffer-size 536870912 " + ) + + megatron_args = ( + "--train-backend megatron " + f"--load /root/models/{MODEL_NAME} " + "--tensor-model-parallel-size 4 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 1 " + "--expert-model-parallel-size 1 " + "--expert-tensor-parallel-size 1 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 4096 " + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--megatron-to-hf-mode bridge " + ) + + misc_args = ( + "--actor-num-nodes 1 " f"--actor-num-gpus-per-node {NUM_GPUS} " f"--rollout-num-gpus {NUM_GPUS} " "--colocate " + ) + + if TRAIN_BACKEND == "megatron": + backend_args = megatron_args + megatron_model_type = get_megatron_model_type(MODEL_NAME) + os.environ["MODEL_ARGS_ROTARY_BASE"] = "5000000" + else: + backend_args = fsdp_args + megatron_model_type = None + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{sglang_args} " + f"{backend_args} " + f"{misc_args} " + f"{wandb_args} " + # f"{get_default_wandb_args(__file__)} " + ) + + execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=megatron_model_type, + extra_env_vars=({"WANDB_API_KEY": os.environ["WANDB_API_KEY"]} if os.environ.get("WANDB_API_KEY") else {}), + ) + + +if __name__ == "__main__": + prepare() + execute() diff --git a/examples/geo3k_vlm_multi_turn/vlm_multi_turn_geo3k_reward.png b/examples/geo3k_vlm_multi_turn/vlm_multi_turn_geo3k_reward.png new file mode 100644 index 0000000000000000000000000000000000000000..0f675e3bca3e244352c0d720459af10de7ee3628 GIT binary patch literal 53073 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 Ck8*zi literal 0 HcmV?d00001 diff --git a/examples/multi_agent/agent_system.py b/examples/multi_agent/agent_system.py index a0d937358..e1376bcd7 100644 --- a/examples/multi_agent/agent_system.py +++ b/examples/multi_agent/agent_system.py @@ -20,11 +20,21 @@ async def generate_response(args, prompt, key): url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" - prompt_token_ids = tokenizer.encode(prompt, add_special_tokens=False) + if args.apply_chat_template: + assert isinstance(prompt, list), "prompt should be a list when apply_chat_template is True" + prompt_text = tokenizer.apply_chat_template( + prompt, + tokenize=False, + add_generation_prompt=True, # Add generation prompt for the assistant + **(args.apply_chat_template_kwargs or {}), + ) + sample.prompt = prompt_text + else: + assert isinstance(prompt, str), "prompt should be a string when apply_chat_template is False" + sample.prompt = prompt + prompt_token_ids = tokenizer(sample.prompt, add_special_tokens=False)["input_ids"] sample.tokens = prompt_token_ids - sample.prompt = prompt - input_token_ids = prompt_token_ids - prompt_length = len(input_token_ids) + prompt_length = len(prompt_token_ids) current_sampling_params = deepcopy(sampling_params) current_sampling_params["max_new_tokens"] = min( sampling_params["max_new_tokens"], max_context_length - prompt_length @@ -33,7 +43,7 @@ async def generate_response(args, prompt, key): if current_sampling_params["max_new_tokens"] <= 0: return None - payload = {"input_ids": input_token_ids, "sampling_params": current_sampling_params, "return_logprob": True} + payload = {"input_ids": prompt_token_ids, "sampling_params": current_sampling_params, "return_logprob": True} output = await post(url, payload) diff --git a/examples/on_policy_distillation/README.md b/examples/on_policy_distillation/README.md new file mode 100644 index 000000000..bab6340d6 --- /dev/null +++ b/examples/on_policy_distillation/README.md @@ -0,0 +1,58 @@ +# On-Policy Distillation Example + +This example shows how to run **on-policy distillation** using Miles. A small student (Qwen3-8B) is aligned to imitate a larger teacher (Qwen3-32B) by training only on the student's own rollouts and matching the teacher's token-level log-probabilities. + +In this example, the teacher model acts as a reward model (RM) by providing teacher log probabilities as the supervision signal. + +## Components + +- `on_policy_distillation.py` implements:: + - `reward_func` calls the teacher server (via `args.rm_url`) with every sample to obtain token-level logprobs. + - `post_process_rewards` trims the teacher logprobs to the generated response span and writes the tensors back to each `Sample` to compute advantages. +- `run-qwen3-8B-opd.sh` launches an SGLang teacher server, then submits a Ray job that runs `train.py`. + +## Running the example + +1. Download or prepare the required checkpoints and data. +```bash +hf download Qwen/Qwen3-32B --local-dir /root/Qwen3-32B +hf download Qwen/Qwen3-8B --local-dir /root/Qwen3-8B +hf download zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +``` + +2. Run the hf to mcore for student model conversion: +```bash +source "${HOME_DIR}/miles/scripts/models/qwen3-8B.sh" + +PYTHONPATH=/root/Megatron-LM:${HOME_DIR}/miles python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint ${HOME_DIR}/checkpoints/Qwen/Qwen3-8B \ + --save ${HOME_DIR}/checkpoints/Qwen/Qwen3-8B_torch_dist +``` +3. run on-policy distillation: +```bash +bash examples/on_policy_distillation/run-qwen3-8B-opd.sh +``` + + +# Preliminary Results +Using Qwen3-8B-Base model sfted on part of the [OpenThoughts3-1.2M](https://huggingface.co/datasets/open-thoughts/OpenThoughts3-1.2M) dataset, we performed on-policy distillation with a Qwen3-32B teacher on the remaining data. Evaluation on Math500 shows: + +| | Pass@1 | +|-----------------------------------------------|--------| +| Qwen3-8B-Base + SFT | 76% | +| Qwen3-8B-Base + SFT + On-Policy Distillation | 94% | + + + + + +# FAQ +1. **Why are teacher logits computed via a sglang server instead of inside the training backend?** +The teacher runs on an independent SGLang server that Miles treats as a reward model. Hosting it inside Megatron/FSDP would require maintaining a second, fully configured training stack for the teacher. + + +# References +1. https://thinkingmachines.ai/blog/on-policy-distillation/ +2. https://arxiv.org/abs/2306.13649 +3. https://arxiv.org/abs/2306.08543 \ No newline at end of file diff --git a/examples/on_policy_distillation/on_policy_distillation.py b/examples/on_policy_distillation/on_policy_distillation.py index 929e2d64c..94a7c29a7 100644 --- a/examples/on_policy_distillation/on_policy_distillation.py +++ b/examples/on_policy_distillation/on_policy_distillation.py @@ -6,7 +6,8 @@ async def reward_func(args, sample, **kwargs): payload = { - "text": sample.prompt + sample.response, + # "text": sample.prompt + sample.response, + "input_ids": sample.tokens, "sampling_params": { "temperature": 0, "max_new_tokens": 0, diff --git a/examples/on_policy_distillation/run-qwen3-8B-opd.sh b/examples/on_policy_distillation/run-qwen3-8B-opd.sh index c57b9eef4..f45c2634b 100644 --- a/examples/on_policy_distillation/run-qwen3-8B-opd.sh +++ b/examples/on_policy_distillation/run-qwen3-8B-opd.sh @@ -29,6 +29,7 @@ until curl -sf http://$TEACHER_IP:$TEACHER_PORT/health_generate > /dev/null; do sleep 5 done +curl http://$TEACHER_IP:$TEACHER_PORT/get_model_info echo "Teacher model server is up and running at $TEACHER_IP:$TEACHER_PORT." sleep 10 diff --git a/examples/retool/generate_with_retool.py b/examples/retool/generate_with_retool.py index 068ca07f9..9fb782edb 100644 --- a/examples/retool/generate_with_retool.py +++ b/examples/retool/generate_with_retool.py @@ -230,9 +230,20 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: tool_call_count = 0 # Track actual tool call rounds for turn in range(TOOL_CONFIGS["max_turns"]): - # Simple: just send prompt + response + # Check if total length exceeds max context length + total_length = len(prompt_tokens_ids) + len(response_token_ids) + if args.rollout_max_context_len is not None: + max_context_length = args.rollout_max_context_len + else: + max_context_length = args.context_parallel_size * args.max_tokens_per_gpu + if total_length >= max_context_length: + sample.status = Sample.Status.TRUNCATED + break + + # Use token IDs instead of text + current_token_ids = prompt_tokens_ids + response_token_ids payload = { - "text": prompt + response, + "input_ids": current_token_ids, "sampling_params": sampling_params, "return_logprob": True, # Request log probabilities for training } @@ -265,15 +276,16 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: sample.status = Sample.Status.ABORTED return sample - cur_response = output["text"] - if "output_token_logprobs" in output["meta_info"]: cur_response_token_ids = [item[1] for item in output["meta_info"]["output_token_logprobs"]] + cur_response = state.tokenizer.decode(cur_response_token_ids) cur_log_probs = [item[0] for item in output["meta_info"]["output_token_logprobs"]] if sample.rollout_log_probs is None: sample.rollout_log_probs = [] sample.rollout_log_probs += cur_log_probs + else: + cur_response = output["text"] cur_response = postprocess_responses(cur_response) cur_response_token_ids = state.tokenizer(cur_response, add_special_tokens=False)["input_ids"] diff --git a/examples/search-r1/generate_with_search.py b/examples/search-r1/generate_with_search.py index 2549e2a68..bcf94f0fd 100644 --- a/examples/search-r1/generate_with_search.py +++ b/examples/search-r1/generate_with_search.py @@ -3,6 +3,7 @@ import asyncio import re +import numpy as np from qa_em_format import compute_score_em @@ -151,7 +152,18 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: # Handle partial rollout samples: continue generation from existing response prompt = sample.prompt - prompt_tokens_ids = state.tokenizer(sample.prompt, add_special_tokens=False)["input_ids"] + if args.apply_chat_template: + assert isinstance(prompt, np.ndarray), "prompt should be a np.ndarray when apply_chat_template is True" + prompt_text = state.tokenizer.apply_chat_template( + prompt, + tokenize=False, + add_generation_prompt=True, # Add generation prompt for the assistant + **(args.apply_chat_template_kwargs or {}), + ) + else: + assert isinstance(prompt, str), "prompt should be a string when apply_chat_template is False" + prompt_text = prompt + prompt_tokens_ids = state.tokenizer(prompt_text, add_special_tokens=False)["input_ids"] response = "" response_token_ids = [] loss_mask = [] @@ -159,7 +171,7 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: for _turn_idx in range(SEARCH_R1_CONFIGS["max_turns"]): payload = { - "text": prompt + response, + "text": prompt_text + response, "sampling_params": sampling_params, } # Add log probability collection if enabled @@ -230,6 +242,7 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: sample.response_length = len(response_token_ids) sample.response = response sample.loss_mask = loss_mask + sample.prompt = prompt_text # Store log probs if enabled if SEARCH_R1_CONFIGS["return_logprob"]: diff --git a/examples/train_infer_mismatch_helper/mis.py b/examples/train_infer_mismatch_helper/mis.py index 19c666bf1..5d14ceb6b 100644 --- a/examples/train_infer_mismatch_helper/mis.py +++ b/examples/train_infer_mismatch_helper/mis.py @@ -2,7 +2,11 @@ import torch -from miles.backends.megatron_utils.cp_utils import all_gather_with_cp, slice_log_prob_with_cp +# NOTE: +# - `compute_mis_weights` is a lightweight, standalone function that is useful to unit-test on CPU. +# - `compute_mis_weights_with_cp` depends on Megatron context-parallel utilities, which are heavy and may not be +# available in minimal environments. +# To keep `mis.py` importable for unit tests, we lazily import CP utilities inside `compute_mis_weights_with_cp`. def masked_sum(x: torch.Tensor, loss_mask: torch.Tensor, expand: bool = False) -> torch.Tensor: @@ -15,6 +19,26 @@ def masked_mean(x: torch.Tensor, loss_mask: torch.Tensor, expand: bool = False) return result.expand_as(x) if expand else result +def masked_min(x: torch.Tensor, loss_mask: torch.Tensor, expand: bool = False) -> torch.Tensor: + """Masked min over valid tokens (loss_mask == 1). Returns 0 when mask is empty.""" + mask = loss_mask.bool() + if mask.any(): + result = x[mask].min() + else: + result = torch.tensor(0.0, device=x.device, dtype=x.dtype) + return result.expand_as(x) if expand else result + + +def masked_max(x: torch.Tensor, loss_mask: torch.Tensor, expand: bool = False) -> torch.Tensor: + """Masked max over valid tokens (loss_mask == 1). Returns 0 when mask is empty.""" + mask = loss_mask.bool() + if mask.any(): + result = x[mask].max() + else: + result = torch.tensor(0.0, device=x.device, dtype=x.dtype) + return result.expand_as(x) if expand else result + + def metrics_append(metrics: dict[str, list[torch.Tensor]], key: str, value: torch.Tensor) -> None: """ @@ -60,6 +84,8 @@ def calculate_veto_mask( loss_mask: torch.Tensor, veto_threshold: float | None, metrics: dict[str, list[torch.Tensor]], + *, + metric_prefix: str = "", ) -> torch.Tensor: if veto_threshold is None: return torch.ones_like(log_ratio) @@ -69,16 +95,21 @@ def calculate_veto_mask( has_catastrophic = catastrophic_tokens.any() veto_mask = (~has_catastrophic).float().expand_as(log_ratio) - metrics_append(metrics, "catastrophic_token_fraction", catastrophic_tokens.int()) - metrics_append(metrics, "catastrophic_seq_fraction", has_catastrophic.int().expand_as(loss_mask)) + metrics_append(metrics, f"{metric_prefix}catastrophic_token_fraction", catastrophic_tokens.int()) + metrics_append(metrics, f"{metric_prefix}catastrophic_seq_fraction", has_catastrophic.int().expand_as(loss_mask)) return veto_mask def truncate( - weights: torch.Tensor, loss_mask: torch.Tensor, metrics: dict[str, list[torch.Tensor]], upper_bound: float + weights: torch.Tensor, + loss_mask: torch.Tensor, + metrics: dict[str, list[torch.Tensor]], + upper_bound: float, + *, + metric_prefix: str = "", ) -> torch.Tensor: assert upper_bound is not None - metrics_append(metrics, "truncate_fraction", (weights > upper_bound).int()) + metrics_append(metrics, f"{metric_prefix}truncate_fraction", (weights > upper_bound).int()) return weights.clamp(0, upper_bound) * loss_mask @@ -88,10 +119,12 @@ def clip( metrics: dict[str, list[torch.Tensor]], lower_bound: float, upper_bound: float, + *, + metric_prefix: str = "", ) -> torch.Tensor: assert lower_bound is not None and upper_bound is not None and lower_bound < upper_bound - metrics_append(metrics, "clip_fraction_low", (weights < lower_bound).int()) - metrics_append(metrics, "clip_fraction_high", (weights > upper_bound).int()) + metrics_append(metrics, f"{metric_prefix}clip_fraction_low", (weights < lower_bound).int()) + metrics_append(metrics, f"{metric_prefix}clip_fraction_high", (weights > upper_bound).int()) return weights.clamp(lower_bound, upper_bound) * loss_mask @@ -101,10 +134,12 @@ def mask( metrics: dict[str, list[torch.Tensor]], lower_bound: float, upper_bound: float, + *, + metric_prefix: str = "", ) -> tuple[torch.Tensor, torch.Tensor]: assert lower_bound is not None and upper_bound is not None and lower_bound < upper_bound - metrics_append(metrics, "mask_fraction_low", (weights < lower_bound).int()) - metrics_append(metrics, "mask_fraction_high", (weights > upper_bound).int()) + metrics_append(metrics, f"{metric_prefix}mask_fraction_low", (weights < lower_bound).int()) + metrics_append(metrics, f"{metric_prefix}mask_fraction_high", (weights > upper_bound).int()) in_range = (weights >= lower_bound) & (weights <= upper_bound) modified_mask = loss_mask * in_range.float() # Zero out padding in weights but preserve values at non-rejected positions @@ -189,11 +224,15 @@ def compute_log_ratio(raw_log_diff: torch.Tensor, mask: torch.Tensor, level: str metrics_append(metrics, "tis_weight_before_bound", weights) if args.tis_mode == "truncate": - weights = truncate(weights, loss_mask, metrics, args.tis_upper_bound) + weights = truncate(weights, loss_mask, metrics, args.tis_upper_bound, metric_prefix="tis_") elif args.tis_mode == "clip": - weights = clip(weights, loss_mask, metrics, tis_lower_bound, args.tis_upper_bound) + weights = clip( + weights, loss_mask, metrics, tis_lower_bound, args.tis_upper_bound, metric_prefix="tis_" + ) elif args.tis_mode == "mask": - weights, modified_mask = mask(weights, loss_mask, metrics, tis_lower_bound, args.tis_upper_bound) + weights, modified_mask = mask( + weights, loss_mask, metrics, tis_lower_bound, args.tis_upper_bound, metric_prefix="tis_" + ) else: raise ValueError(f"Unsupported tis_mode: {args.tis_mode}") @@ -212,14 +251,18 @@ def compute_log_ratio(raw_log_diff: torch.Tensor, mask: torch.Tensor, level: str rs_weights = torch.exp(log_ratio_safe_rs) # Apply mask-based rejection sampling - _, modified_mask = mask(rs_weights, modified_mask, metrics, rs_lower_bound, rs_upper_bound) + _, modified_mask = mask( + rs_weights, modified_mask, metrics, rs_lower_bound, rs_upper_bound, metric_prefix="rs_" + ) # Veto on raw per-token ratios (sequence-wise rejection) if args.rs_veto_threshold is not None: - veto_mask = calculate_veto_mask(raw_log_ratio_diff, loss_mask, args.rs_veto_threshold, metrics) + veto_mask = calculate_veto_mask( + raw_log_ratio_diff, loss_mask, args.rs_veto_threshold, metrics, metric_prefix="rs_" + ) modified_mask = modified_mask * veto_mask - metrics_append(metrics, "ratio_mean_after_tis", weights) + metrics_append(metrics, "is_ratio_mean_after_tis_rs", weights) weights = weights.detach() modified_mask = modified_mask.detach() @@ -253,6 +296,14 @@ def compute_log_ratio(raw_log_diff: torch.Tensor, mask: torch.Tensor, level: str for w in all_weights: metrics_append(metrics, "batch_norm_factor", torch.ones_like(w)) + # Final weight stats (after optional batch normalization). + # NOTE: These are expanded to token-shape so that the existing mean-reducer can aggregate them. + for w, m in zip(all_weights, loss_masks, strict=False): + m = m.float() + metrics_append(metrics, "is_ratio_mean_final", masked_mean(w, m, expand=True)) + metrics_append(metrics, "is_ratio_min_final", masked_min(w, m, expand=True)) + metrics_append(metrics, "is_ratio_max_final", masked_max(w, m, expand=True)) + return all_weights, all_modified_masks, metrics @@ -280,6 +331,9 @@ def compute_mis_weights_with_cp( modified_masks: List of modified response masks with rejection applied (one per sequence). is_metrics: The metrics for the importance sampling weights, a dict of flattened tensors. """ + # Lazy import to avoid importing Megatron dependencies when only `compute_mis_weights` is used. + from miles.backends.megatron_utils.cp_utils import all_gather_with_cp, slice_log_prob_with_cp + # Gather cp slice from other cp ranks full_rollout_log_probs = [ all_gather_with_cp(log_prob, total_length, response_length) @@ -395,3 +449,45 @@ def add_ppl_metrics( rho_squared_seq = torch.exp(2.0 * log_ratio_sum_safe) # (Π ρ_t)² chi2_seq = rho_squared_seq - 1.0 metrics_append(metrics, "chi2_seq", chi2_seq) + + +def compute_mis_weights_fsdp( + args, + *, + pg_loss: torch.Tensor, + train_log_probs: list[torch.Tensor], + rollout_log_probs: list[torch.Tensor], + loss_masks: list[torch.Tensor], + **kwargs: Any, +) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]: + """Compute masked importance sampling weights for FSDP. No context parallelism. + + Args: + args: Arguments containing MIS settings (use_tis, tis_mode, etc.) + pg_loss: Policy gradient loss, flattened tensor [total_tokens] + train_log_probs: Training log probs, list of 1D tensors per sequence + rollout_log_probs: Rollout log probs, list of 1D tensors per sequence + loss_masks: Loss masks, list of 1D tensors per sequence + **kwargs: Additional arguments (cp_rank, cp_size, etc.) for compatibility + + Returns: + pg_loss: Policy gradient loss with IS weights applied + modified_masks: Modified loss masks after rejection sampling + mis_metrics: Metrics dict with flattened tensors + """ + is_weights, modified_masks, is_metrics = compute_mis_weights( + args=args, + train_log_probs=train_log_probs, + rollout_log_probs=rollout_log_probs, + loss_masks=loss_masks, + ) + + result_metrics = {} + if is_weights is not None: + is_weights_flat = torch.cat(is_weights, dim=0) + pg_loss = pg_loss * is_weights_flat + + for key, values in is_metrics.items(): + result_metrics[f"mis_{key}"] = torch.cat(values, dim=0) + + return pg_loss, modified_masks, result_metrics diff --git a/examples/train_infer_mismatch_helper/run-qwen3-4b-fsdp-mis.sh b/examples/train_infer_mismatch_helper/run-qwen3-4b-fsdp-mis.sh new file mode 100644 index 000000000..df3848038 --- /dev/null +++ b/examples/train_infer_mismatch_helper/run-qwen3-4b-fsdp-mis.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + + + + +set -ex + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +NVLINK_COUNT=$(nvidia-smi | grep -o "NVLink" | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + + + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" + +RUN_ID=${RUN_ID:-"run_$(date +%Y%m%d_%H%M%S)"} +LOAD_SAVE_PATH="/root/shared_data/${RUN_ID}/checkpoints" + +CKPT_ARGS=( + --hf-checkpoint /root/Qwen3-4B + --load /root/Qwen3-4B + --ref-load /root/Qwen3-4B +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --balance-data + --rm-type deepscaler + --num-rollout 100 + --rollout-batch-size 8 + --n-samples-per-prompt 8 + --rollout-max-response-len 4096 + --rollout-temperature 0.8 + --global-batch-size 64 +) + +GRPO_ARGS=( + --use-kl-loss + --advantage-estimator grpo + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 + --use-tis +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +WANDB_ARGS=( + --use-wandb + --wandb-project miles-dev-mcore-fsdp + --wandb-group qwen3-4B-fsdp-1130-ref + --wandb-key ${WANDB_API_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.75 + --sglang-decode-log-interval 1000 + --sglang-chunked-prefill-size 4096 + --sglang-attention-backend fa3 +) + +TRAIN_BACKEND_ARGS=( + --train-backend fsdp + --update-weight-buffer-size 536870912 + --gradient-checkpointing + --attn-implementation flash_attention_3 + --train-env-vars '{"PYTORCH_CUDA_ALLOC_CONF":"expandable_segments:True"}' +) + +PERF_ARGS=( + --use-dynamic-batch-size + --max-tokens-per-gpu 9216 +) + +MISC_ARGS=( + --actor-num-nodes 1 + --actor-num-gpus-per-node 8 + --colocate + --use-fault-tolerance + --dump-details /root/shared_data/qwen3-4B-fsdp-1116-noref/dump_details + # --fsdp-cpu-offload +) + +CUSTOM_ARGS=( + --custom-config-path examples/train_infer_mismatch_helper/mis.yaml + --custom-tis-function-path examples.train_infer_mismatch_helper.mis.compute_mis_weights_fsdp +) + +# launch the master node of ray in container - 8 GPUs for training +export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats + + +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/:${SCRIPT_DIR}\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\" + } +}" + + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${TRAIN_BACKEND_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${MISC_ARGS[@]} \ + ${CUSTOM_ARGS[@]} + + diff --git a/miles/backends/fsdp_utils/actor.py b/miles/backends/fsdp_utils/actor.py index 1e3e5b3ae..2cd96723a 100644 --- a/miles/backends/fsdp_utils/actor.py +++ b/miles/backends/fsdp_utils/actor.py @@ -1,5 +1,6 @@ import logging import os +import random from argparse import Namespace from itertools import accumulate @@ -18,7 +19,14 @@ from miles.utils.distributed_utils import get_gloo_group from miles.utils.memory_utils import clear_memory, print_memory from miles.utils.metric_utils import compute_rollout_step -from miles.utils.ppo_utils import compute_approx_kl, compute_gspo_kl, compute_opsm_mask, compute_policy_loss +from miles.utils.misc import load_function +from miles.utils.ppo_utils import ( + compute_approx_kl, + compute_gspo_kl, + compute_opsm_mask, + compute_policy_loss, + vanilla_tis_function, +) from miles.utils.processing_utils import load_processor, load_tokenizer from miles.utils.ray_utils import Box from miles.utils.timer import Timer, inverse_timer, timer @@ -317,12 +325,13 @@ def wake_up(self) -> None: dist.barrier(group=get_gloo_group()) print_memory("after wake_up model") - def save_model(self, iteration: int) -> None: + def save_model(self, rollout_id: int, force_sync: bool = False) -> None: """Delegate checkpoint saving to the shared checkpoint utilities.""" if self.args.debug_rollout_only or self.args.save is None: return - checkpoint.save(self, iteration) + assert not self.args.async_save, "FSDPTrainRayActor does not support async_save yet." + checkpoint.save(self, rollout_id) def _compute_log_prob( self, @@ -454,8 +463,10 @@ def _packed_data( rollout_log_probs=( rollout_data["rollout_log_probs"][start:end] if "rollout_log_probs" in rollout_data else None ), - multimodal_inputs=( - rollout_data["multimodal_inputs"][start:end] if "multimodal_inputs" in rollout_data else None + multimodal_train_inputs=( + rollout_data["multimodal_train_inputs"][start:end] + if "multimodal_train_inputs" in rollout_data + else None ), num_packs=mbs_size, ) @@ -654,26 +665,41 @@ def _has_rollout_log_probs(batch) -> bool: else None ) - # Apply TIS before sample mean calculation + # Apply off-policy correction using importance sampling if enabled if self.args.use_tis: - # Apply TIS off-policy correction using importance sampling assert ( has_rollout_log_probs and rollout_log_probs is not None - ), "rollout_log_probs must be provided as non-empty torch.Tensor for TIS" + ), "rollout_log_probs must be provided as non-empty torch.Tensor for TIS/MIS" - tis = torch.exp(old_log_probs - rollout_log_probs) + train_log_probs_list = list(log_probs.split(response_lengths, dim=0)) + rollout_log_probs_list = list(rollout_log_probs.split(response_lengths, dim=0)) ois = (-ppo_kl).exp() - tis_clip = torch.clamp( - tis, min=getattr(self.args, "tis_clip_low", 0.1), max=getattr(self.args, "tis_clip", 2.0) - ) - tis_clipfrac = tis_clip != tis - - pg_loss = pg_loss * tis_clip - - assert not self.args.calculate_per_token_loss, "calculate_per_token_loss not yet implemented" - pg_loss = sum_of_sample_mean(pg_loss, response_lengths, loss_masks) - pg_clipfrac = sum_of_sample_mean(pg_clipfrac, response_lengths, loss_masks) - ppo_kl = sum_of_sample_mean(ppo_kl.abs(), response_lengths, loss_masks) + tis_kwargs = { + "args": self.args, + "pg_loss": pg_loss, + "train_log_probs": train_log_probs_list, + "rollout_log_probs": rollout_log_probs_list, + "loss_masks": loss_masks, + "response_lengths": response_lengths, + "cp_rank": self.cp_rank, + "cp_size": self.cp_size, + "cp_group": self.cp_group, + } + + if self.args.custom_tis_function_path is not None: + tis_func = load_function(self.args.custom_tis_function_path) + else: + tis_func = vanilla_tis_function + pg_loss, loss_masks, tis_metrics = tis_func(**tis_kwargs) + + if self.args.calculate_per_token_loss: + pg_loss = sum_of_token(pg_loss, response_lengths, loss_masks) + pg_clipfrac = sum_of_token(pg_clipfrac, response_lengths, loss_masks) + ppo_kl = sum_of_token(ppo_kl.abs(), response_lengths, loss_masks) + else: + pg_loss = sum_of_sample_mean(pg_loss, response_lengths, loss_masks) + pg_clipfrac = sum_of_sample_mean(pg_clipfrac, response_lengths, loss_masks) + ppo_kl = sum_of_sample_mean(ppo_kl.abs(), response_lengths, loss_masks) # Only compare rollout vs. train log probs when they originate from different stages. train_rollout_logprob_abs_diff = None @@ -720,10 +746,13 @@ def _has_rollout_log_probs(batch) -> bool: if self.args.use_opsm: reported["opsm_clipfrac"] = opsm_clipfrac - if self.args.use_tis and tis is not None: - reported["tis"] = sum_of_sample_mean(tis, response_lengths, loss_masks).detach() + if self.args.use_tis and tis_metrics: reported["ois"] = sum_of_sample_mean(ois, response_lengths, loss_masks).detach() - reported["tis_clipfrac"] = sum_of_sample_mean(tis_clipfrac.float(), response_lengths, loss_masks).detach() + for k, v in tis_metrics.items(): + if self.args.calculate_per_token_loss: + reported[k] = sum_of_token(v, response_lengths, loss_masks).detach() + else: + reported[k] = sum_of_sample_mean(v, response_lengths, loss_masks).detach() # Scale loss for gradient accumulation loss = loss * self.dp_size / self.args.global_batch_size @@ -791,6 +820,15 @@ def update_weights(self) -> None: # type: ignore[override] dist.barrier(group=get_gloo_group()) self.weight_updater.update_weights() + + if self.args.ci_test and len(rollout_engines) > 0: + engine = random.choice(rollout_engines) + engine_version = ray.get(engine.get_weight_version.remote()) + if str(engine_version) != str(self.weight_updater.weight_version): + raise RuntimeError( + f"Weight version mismatch! Engine: {engine_version}, Updater: {self.weight_updater.weight_version}" + ) + clear_memory() def _create_ref_model(self, ref_load_path: str | None): @@ -854,8 +892,8 @@ def _get_model_inputs_args(self, packed_sequence: dict) -> dict: "position_ids": position_ids, "attention_mask": None, } - if packed_sequence.get("multimodal_inputs"): - model_args.update(packed_sequence["multimodal_inputs"]) + if packed_sequence.get("multimodal_train_inputs"): + model_args.update(packed_sequence["multimodal_train_inputs"]) return model_args @@ -1093,3 +1131,12 @@ def apply_fsdp2(model, mesh=None, cpu_offload=False, args=None): fully_shard(model, **fsdp_kwargs) return model + + +def sum_of_token(x: torch.Tensor, response_lengths: list[int], loss_masks: list[torch.Tensor]) -> torch.Tensor: + return sum( + [ + (x_i * loss_mask_i).sum() + for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False) + ] + ) diff --git a/miles/backends/fsdp_utils/data_packing.py b/miles/backends/fsdp_utils/data_packing.py index 8318f0c53..676d242ec 100644 --- a/miles/backends/fsdp_utils/data_packing.py +++ b/miles/backends/fsdp_utils/data_packing.py @@ -17,7 +17,7 @@ def pack_sequences( advantages: list[float], returns: list[float], rollout_log_probs: list[list[float]] | None = None, - multimodal_inputs: list[dict] | None = None, + multimodal_train_inputs: list[dict] | None = None, max_tokens_per_gpu: int | None = None, num_packs: int | None = None, ) -> list[dict]: @@ -33,7 +33,7 @@ def pack_sequences( advantages: List of advantages per sequence returns: List of returns per sequence rollout_log_probs: List of rollout log probabilities per sequence - multimodal_inputs: List of dict of multimodal tokens per sequence + multimodal_train_inputs: List of dict of multimodal tensors for training per sequence max_tokens_per_gpu: Maximum tokens per GPU pack num_packs: Explicit number of packs to create @@ -100,19 +100,19 @@ def pack_sequences( ), } - # Collect and add multimodal inputs for this partition - if multimodal_inputs: + # Collect and add multimodal training tensors for this partition + if multimodal_train_inputs: multimodal_data = {} # key -> concatenated tensor multimodal_num_items = {} # key -> list of item counts per sequence for i in indices: - for key, mm_tensor in multimodal_inputs[i].items(): + for key, mm_tensor in multimodal_train_inputs[i].items(): if key not in multimodal_data: multimodal_data[key] = mm_tensor multimodal_num_items[key] = [mm_tensor.size(0)] else: multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0) multimodal_num_items[key].append(mm_tensor.size(0)) - packed_batch["multimodal_inputs"] = multimodal_data + packed_batch["multimodal_train_inputs"] = multimodal_data packed_batch["multimodal_num_items"] = multimodal_num_items result.append(packed_batch) @@ -157,8 +157,8 @@ def unpack_sequences(packed_batch: dict) -> list[dict]: # Skip multimodal_num_items - it's metadata if key == "multimodal_num_items": continue - # Handle multimodal_inputs dict: split each tensor using multimodal_num_items - elif key == "multimodal_inputs" and isinstance(value, dict): + # Handle multimodal_train_inputs dict: split each tensor using multimodal_num_items + elif key == "multimodal_train_inputs" and isinstance(value, dict): instance[key] = {} for mm_key, mm_tensor in value.items(): if mm_key in multimodal_num_items: diff --git a/miles/backends/fsdp_utils/kernels/fused_experts.py b/miles/backends/fsdp_utils/kernels/fused_experts.py index a7970994b..d1c02aae8 100644 --- a/miles/backends/fsdp_utils/kernels/fused_experts.py +++ b/miles/backends/fsdp_utils/kernels/fused_experts.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import torch import triton.language as tl from sglang.srt.layers.moe.fused_moe_triton.fused_moe import ( @@ -7,6 +9,8 @@ silu_and_mul, ) +from .fused_moe_triton_backward_kernels import invoke_fused_moe_backward_kernel + class GateUpProjFunction(torch.autograd.Function): @staticmethod @@ -81,14 +85,89 @@ def forward( filter_expert=True, ) - ctx.save_for_backward(hidden_states, w1, topk_weights) + ctx.save_for_backward(hidden_states, w1, topk_weights, topk_ids) + ctx.config = config + ctx.num_tokens = num_tokens + ctx.topk = topk return intermediate_cache1 @staticmethod def backward(ctx, grad_output): - hidden_states, w1, topk_weights = ctx.saved_tensors - return torch.zeros_like(hidden_states), torch.zeros_like(w1), torch.zeros_like(topk_weights), None + """ + Backward pass for GateUpProjFunction using Triton kernels. + + Args: + grad_output: shape (num_tokens * topk, N) + + Returns: + (grad_hidden_states, grad_w1, grad_topk_weights, None) + """ + + hidden_states, w1, topk_weights, topk_ids = ctx.saved_tensors + config = ctx.config + num_tokens = ctx.num_tokens + topk = ctx.topk + + E, N, D_in = w1.shape + CHUNK_SIZE = 64 * 1024 + + # Initialize gradient tensors + grad_hidden_states = torch.zeros_like(hidden_states) + grad_w1 = torch.zeros_like(w1) + # GateUpProj stage doesn't need topk_weights gradient + grad_topk_weights = torch.zeros_like(topk_weights) + + # Process in chunks to match forward pass + for chunk in range((num_tokens // CHUNK_SIZE) + 1): + begin_chunk_idx, end_chunk_idx = ( + chunk * CHUNK_SIZE, + min((chunk + 1) * CHUNK_SIZE, num_tokens), + ) + + curr_num_tokens = end_chunk_idx - begin_chunk_idx + if curr_num_tokens == 0: + continue + + curr_hidden_states = hidden_states[begin_chunk_idx:end_chunk_idx] + curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx] + curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx] + curr_grad_output = grad_output[begin_chunk_idx * topk : end_chunk_idx * topk] + + # Get aligned metadata + sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size( + curr_topk_ids, config["BLOCK_SIZE_M"], E + ) + + # Prepare gradient buffer for this chunk + curr_grad_hidden_states = torch.zeros_like(curr_hidden_states) + curr_grad_w1 = torch.zeros_like(w1) + + # Call Triton backward kernel with MUL_ROUTED_WEIGHT=False + # Use chunk of hidden_states to match sorted_token_ids indices + invoke_fused_moe_backward_kernel( + grad_output=curr_grad_output, + input=curr_hidden_states, # Use chunk of hidden_states to match sorted_token_ids + weight=w1, + grad_input=curr_grad_hidden_states, + grad_weight=curr_grad_w1, + grad_topk_weights=None, # Not needed for GateUpProj + topk_weights=curr_topk_weights, + topk_ids=curr_topk_ids, + sorted_token_ids=sorted_token_ids, + expert_ids=expert_ids, + num_tokens_post_padded=num_tokens_post_padded, + mul_routed_weight=False, + top_k=topk, + config=config, + compute_type=tl.bfloat16, + ) + + # Accumulate gradients + grad_hidden_states[begin_chunk_idx:end_chunk_idx] += curr_grad_hidden_states + grad_w1 += curr_grad_w1 + + return grad_hidden_states, grad_w1, grad_topk_weights, None class SiluAndMulFunction(torch.autograd.Function): @@ -193,15 +272,89 @@ def forward( b_use_tma=False, ) - ctx.save_for_backward(intermediate_cache2, w2, topk_weights) + ctx.save_for_backward(intermediate_cache2, w2, topk_weights, topk_ids) + ctx.config = config + ctx.num_tokens = num_tokens + ctx.topk = topk return intermediate_cache3 @staticmethod def backward(ctx, grad_output): - intermediate_cache2, w2, topk_weights = ctx.saved_tensors + """ + Backward pass for DownProjFunction using Triton kernels. + + Args: + grad_output: shape (num_tokens, topk, hidden_size) + + Returns: + (grad_intermediate_cache2, grad_w2, grad_topk_weights, None) + """ + intermediate_cache2, w2, topk_weights, topk_ids = ctx.saved_tensors + config = ctx.config + num_tokens = ctx.num_tokens + topk = ctx.topk + + E, hidden_size, intermediate_size = w2.shape + CHUNK_SIZE = 64 * 1024 + + # Initialize gradient tensors + grad_intermediate_cache2 = torch.zeros_like(intermediate_cache2) + grad_w2 = torch.zeros_like(w2) + grad_topk_weights = torch.zeros_like(topk_weights) + + # Process in chunks to match forward pass + for chunk in range((num_tokens // CHUNK_SIZE) + 1): + begin_chunk_idx, end_chunk_idx = ( + chunk * CHUNK_SIZE, + min((chunk + 1) * CHUNK_SIZE, num_tokens), + ) + + curr_num_tokens = end_chunk_idx - begin_chunk_idx + if curr_num_tokens == 0: + continue + + curr_intermediate_cache2 = intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk] + curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx] + curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx] + curr_grad_output = grad_output[begin_chunk_idx:end_chunk_idx] + + # Get aligned metadata + sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size( + curr_topk_ids, config["BLOCK_SIZE_M"], E + ) + + # Prepare gradient buffers for this chunk + curr_grad_intermediate_cache2 = torch.zeros_like(curr_intermediate_cache2) + curr_grad_w2 = torch.zeros_like(w2) + curr_grad_topk_weights = torch.zeros_like(curr_topk_weights) + + # Call Triton backward kernel with MUL_ROUTED_WEIGHT=True + # Note: Use top_k=1 to match forward pass indexing + invoke_fused_moe_backward_kernel( + grad_output=curr_grad_output, + input=curr_intermediate_cache2, + weight=w2, + grad_input=curr_grad_intermediate_cache2, + grad_weight=curr_grad_w2, + grad_topk_weights=curr_grad_topk_weights, + topk_weights=curr_topk_weights, + topk_ids=curr_topk_ids, + sorted_token_ids=sorted_token_ids, + expert_ids=expert_ids, + num_tokens_post_padded=num_tokens_post_padded, + mul_routed_weight=True, + top_k=1, + config=config, + compute_type=tl.bfloat16, + ) + + # Accumulate gradients + grad_intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk] = curr_grad_intermediate_cache2 + grad_w2 += curr_grad_w2 + grad_topk_weights[begin_chunk_idx:end_chunk_idx] = curr_grad_topk_weights - return torch.zeros_like(intermediate_cache2), torch.zeros_like(w2), torch.zeros_like(topk_weights), None + return grad_intermediate_cache2, grad_w2, grad_topk_weights, None class MoeSumReduceFunction(torch.autograd.Function): diff --git a/miles/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py b/miles/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py new file mode 100644 index 000000000..333347849 --- /dev/null +++ b/miles/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py @@ -0,0 +1,540 @@ +from __future__ import annotations + +from typing import Any + +import torch +import triton +import triton.language as tl + + +@triton.jit +def fused_moe_backward_input_kernel( + # Pointers to matrices + grad_output_ptr, + weight_ptr, + grad_input_ptr, + grad_topk_weights_ptr, + topk_weights_ptr, + sorted_token_ids_ptr, + expert_ids_ptr, + num_tokens_post_padded_ptr, + # Matrix dimensions + N, + K, + EM, + num_valid_tokens, + # Strides + stride_gom, + stride_gon, + stride_we, + stride_wn, + stride_wk, + stride_gim, + stride_gik, + # Meta-parameters + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, + MUL_ROUTED_WEIGHT: tl.constexpr, + top_k: tl.constexpr, + compute_type: tl.constexpr, +): + """ + Backward kernel for computing grad_input. + + Forward: output = input @ weight.T (optionally multiplied by topk_weights) + Backward: grad_input = grad_output @ weight (optionally multiplied by topk_weights) + + This kernel computes: grad_input[token] = sum_over_N(grad_output[token, n] * weight[expert, n, :]) + If MUL_ROUTED_WEIGHT: grad_input[token] *= topk_weights[token] + + Parallelization: Similar to forward, parallel over M and N dimensions, loop over K. + """ + # Map program ids to blocks (parallel over M and N, similar to forward) + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + # Check bounds + num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr) + + # Only process if this block is valid + if pid_m * BLOCK_SIZE_M < num_tokens_post_padded: + # Load token information + offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64) + offs_token = tl.load(sorted_token_ids_ptr + offs_token_id) + offs_token = offs_token.to(tl.int64) + token_mask = offs_token < num_valid_tokens + + # Get expert ID for this block + off_experts = tl.load(expert_ids_ptr + pid_m).to(tl.int64) + + # Only process if expert is valid + if off_experts != -1: + # Initialize offsets for N dimension (current block) + offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64) + offs_k = tl.arange(0, BLOCK_SIZE_K) + + # Load grad_output block: shape (BLOCK_SIZE_M, BLOCK_SIZE_N) + grad_output_ptrs = grad_output_ptr + (offs_token[:, None] * stride_gom + offs_n[None, :] * stride_gon) + grad_out = tl.load( + grad_output_ptrs, + mask=token_mask[:, None] & (offs_n[None, :] < N), + other=0.0, + ) + + # Apply topk_weights to grad_output if needed + if MUL_ROUTED_WEIGHT: + moe_weight = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0) + grad_out = grad_out * moe_weight[:, None] + + # Iterate over K dimension + for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): + # Current K offsets + curr_offs_k = k * BLOCK_SIZE_K + offs_k + + # Load weight block: shape (BLOCK_SIZE_N, BLOCK_SIZE_K) + # weight: shape (E, N, K) + weight_ptrs = ( + weight_ptr + + off_experts * stride_we + + offs_n[:, None] * stride_wn + + curr_offs_k[None, :] * stride_wk + ) + w = tl.load( + weight_ptrs, + mask=(offs_n[:, None] < N) & (curr_offs_k[None, :] < K), + other=0.0, + ) + + # Compute contribution: grad_out @ weight + # grad_out: (BLOCK_SIZE_M, BLOCK_SIZE_N) + # w: (BLOCK_SIZE_N, BLOCK_SIZE_K) + # result: (BLOCK_SIZE_M, BLOCK_SIZE_K) + contribution = tl.dot(grad_out, w) + + # Atomic add to grad_input because different N blocks contribute to same K + grad_input_ptrs = grad_input_ptr + ( + (offs_token[:, None] // top_k) * stride_gim + curr_offs_k[None, :] * stride_gik + ) + grad_input_mask = token_mask[:, None] & (curr_offs_k[None, :] < K) + tl.atomic_add(grad_input_ptrs, contribution.to(compute_type), mask=grad_input_mask) + + +@triton.jit +def fused_moe_backward_weight_kernel( + # Pointers to matrices + grad_output_ptr, + input_ptr, + grad_weight_ptr, + topk_weights_ptr, + sorted_token_ids_ptr, + expert_ids_ptr, + num_tokens_post_padded_ptr, + # Matrix dimensions + N, + K, + EM, + num_valid_tokens, + # Strides + stride_gom, + stride_gon, + stride_im, + stride_ik, + stride_gwe, + stride_gwn, + stride_gwk, + # Meta-parameters + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + GROUP_SIZE_M: tl.constexpr, + MUL_ROUTED_WEIGHT: tl.constexpr, + top_k: tl.constexpr, + compute_type: tl.constexpr, +): + """ + Backward kernel for computing grad_weight. + + Forward: output = input @ weight.T (optionally multiplied by topk_weights) + Backward: grad_weight = input.T @ grad_output (optionally multiplied by topk_weights) + + This kernel computes: grad_weight[expert, n, k] = sum_over_tokens(input[token, k] * grad_output[token, n]) + If MUL_ROUTED_WEIGHT: the accumulation is weighted by topk_weights[token] + + Parallelization: Parallel over M and N dimensions with grouping, loop over K. + """ + # Map program ids to blocks (parallel over M and N with grouping, similar to forward and backward_input) + pid = tl.program_id(axis=0) + num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M) + num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) + num_pid_in_group = GROUP_SIZE_M * num_pid_n + group_id = pid // num_pid_in_group + first_pid_m = group_id * GROUP_SIZE_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M) + pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m) + pid_n = (pid % num_pid_in_group) // group_size_m + + # Check bounds + num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr) + + # Only process if this block is valid + if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded: + return + + # Get expert ID for this M block + expert_id = tl.load(expert_ids_ptr + pid_m).to(tl.int64) + + # Only process if expert is valid + if expert_id == -1: + return + + # Load token information for this M block + offs_m = tl.arange(0, BLOCK_SIZE_M) + offs_token_id = pid_m * BLOCK_SIZE_M + offs_m.to(tl.int64) + offs_token = tl.load( + sorted_token_ids_ptr + offs_token_id, mask=offs_token_id < num_tokens_post_padded, other=num_valid_tokens + ) + offs_token = offs_token.to(tl.int64) + token_mask = (offs_token_id < num_tokens_post_padded) & (offs_token < num_valid_tokens) + + # Clamp offs_token to valid range + offs_token_clamped = tl.where(token_mask, offs_token, 0) + + # Determine input token indices based on MUL_ROUTED_WEIGHT + if MUL_ROUTED_WEIGHT: + input_token_idx = offs_token_clamped + input_mask = token_mask + else: + input_token_idx = offs_token_clamped // top_k + num_input_tokens = num_valid_tokens // top_k + input_mask = token_mask & (input_token_idx < num_input_tokens) + + # Load topk_weights if needed + if MUL_ROUTED_WEIGHT: + moe_weight = tl.load(topk_weights_ptr + offs_token_clamped, mask=token_mask, other=0.0) + + # Current N offset for this program + offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64) + + # Load grad_output for this N block: shape (M, BLOCK_SIZE_N) + # grad_output is always indexed by sorted_token_ids (offs_token_clamped) + # because it has shape (num_tokens * topk, N) + grad_output_ptrs = grad_output_ptr + (offs_token_clamped[:, None] * stride_gom + offs_n[None, :] * stride_gon) + grad_out = tl.load( + grad_output_ptrs, + mask=token_mask[:, None] & (offs_n[None, :] < N), + other=0.0, + ) + + # Apply topk_weights if needed + if MUL_ROUTED_WEIGHT: + grad_out = grad_out * moe_weight[:, None] + + # Zero out padding tokens + token_mask_col = token_mask[:, None] + grad_out = grad_out * token_mask_col + + # Iterate over K blocks and accumulate + for k_block in range(tl.cdiv(K, BLOCK_SIZE_K)): + offs_k = k_block * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K).to(tl.int64) + + # Load input for this K block + input_ptrs = input_ptr + (input_token_idx[:, None] * stride_im + offs_k[None, :] * stride_ik) + inp = tl.load( + input_ptrs, + mask=input_mask[:, None] & (offs_k[None, :] < K), + other=0.0, + ) + + # Zero out padding tokens - use input_mask for input, token_mask for grad_output + input_mask_col = input_mask[:, None] + inp = inp * input_mask_col + + # Compute grad_weight contribution: grad_out.T @ inp + grad_w_contribution = tl.dot(grad_out.T, inp) + + # Write back using atomic add + grad_weight_ptrs = ( + grad_weight_ptr + expert_id * stride_gwe + offs_n[:, None] * stride_gwn + offs_k[None, :] * stride_gwk + ) + grad_weight_mask = (offs_n[:, None] < N) & (offs_k[None, :] < K) + tl.atomic_add(grad_weight_ptrs, grad_w_contribution.to(compute_type), mask=grad_weight_mask) + + +@triton.jit +def fused_moe_backward_topk_weights_kernel( + # Pointers to matrices + grad_output_ptr, + input_ptr, + weight_ptr, + grad_topk_weights_ptr, + sorted_token_ids_ptr, + expert_ids_ptr, + num_tokens_post_padded_ptr, + # Matrix dimensions + N, + K, + EM, + num_valid_tokens, + # Strides + stride_gom, + stride_gon, + stride_im, + stride_ik, + stride_we, + stride_wn, + stride_wk, + # Meta-parameters + BLOCK_SIZE_M: tl.constexpr, + BLOCK_SIZE_N: tl.constexpr, + BLOCK_SIZE_K: tl.constexpr, + top_k: tl.constexpr, + compute_type: tl.constexpr, +): + """ + Backward kernel for computing grad_topk_weights. + + Forward: output = topk_weights * (input @ weight.T) + Backward: grad_topk_weights = sum(grad_output * (input @ weight.T)) + + This kernel computes the gradient of topk_weights by computing the dot product + of grad_output with the forward output before weight multiplication. + """ + # Map program id to token block + pid = tl.program_id(axis=0) + + # Check bounds + num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr) + + # Only process if this block is valid + if pid * BLOCK_SIZE_M < num_tokens_post_padded: + # Load token information + offs_token_id = pid * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64) + offs_token = tl.load( + sorted_token_ids_ptr + offs_token_id, mask=offs_token_id < num_tokens_post_padded, other=num_valid_tokens + ) + offs_token = offs_token.to(tl.int64) + token_mask = (offs_token_id < num_tokens_post_padded) & (offs_token < num_valid_tokens) + + # Clamp offs_token to valid range for safe pointer arithmetic + offs_token_clamped = tl.where(token_mask, offs_token, 0) + + # Get expert ID for this block + off_experts = tl.load(expert_ids_ptr + pid).to(tl.int64) + + # Only process if expert is valid + if off_experts != -1: + # Initialize offsets + offs_n = tl.arange(0, BLOCK_SIZE_N) + offs_k = tl.arange(0, BLOCK_SIZE_K) + + # Accumulator for grad_topk_weights + accumulator = tl.zeros((BLOCK_SIZE_M,), dtype=tl.float32) + + # Iterate over N and K dimensions to compute forward output and gradient + for n in range(0, tl.cdiv(N, BLOCK_SIZE_N)): + # Current N offset + curr_offs_n = n * BLOCK_SIZE_N + offs_n + + # Load grad_output block: (M, N) + grad_output_ptrs = grad_output_ptr + ( + offs_token_clamped[:, None] * stride_gom + curr_offs_n[None, :] * stride_gon + ) + grad_out = tl.load( + grad_output_ptrs, + mask=token_mask[:, None] & (curr_offs_n[None, :] < N), + other=0.0, + ) + + # Compute forward output for this N block: input @ weight[:, n, :].T + forward_output_n = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) + + for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)): + # Current K offset + curr_offs_k = k * BLOCK_SIZE_K + offs_k + + # Load input block: (M, K) + input_ptrs = input_ptr + ( + (offs_token_clamped[:, None] // top_k) * stride_im + curr_offs_k[None, :] * stride_ik + ) + inp = tl.load( + input_ptrs, + mask=token_mask[:, None] & (curr_offs_k[None, :] < K), + other=0.0, + ) + + # Load weight block: (N, K) + weight_ptrs = ( + weight_ptr + + off_experts * stride_we + + curr_offs_n[:, None] * stride_wn + + curr_offs_k[None, :] * stride_wk + ) + w = tl.load( + weight_ptrs, + mask=(curr_offs_n[:, None] < N) & (curr_offs_k[None, :] < K), + other=0.0, + ) + + # Accumulate forward output: input @ weight.T + # inp: (M, K), w.T: (K, N) -> (M, N) + forward_output_n += tl.dot(inp, w.T) + + # Compute contribution to grad_topk_weights: sum(grad_out * forward_output) + # Sum over N dimension + accumulator += tl.sum(grad_out * forward_output_n, axis=1) + + # Write back grad_topk_weights using atomic add with clamped token indices + tl.atomic_add(grad_topk_weights_ptr + offs_token_clamped, accumulator.to(compute_type), mask=token_mask) + + +def invoke_fused_moe_backward_kernel( + grad_output: torch.Tensor, + input: torch.Tensor, + weight: torch.Tensor, + grad_input: torch.Tensor, + grad_weight: torch.Tensor, + grad_topk_weights: torch.Tensor | None, + topk_weights: torch.Tensor, + topk_ids: torch.Tensor, + sorted_token_ids: torch.Tensor, + expert_ids: torch.Tensor, + num_tokens_post_padded: torch.Tensor, + mul_routed_weight: bool, + top_k: int, + config: dict[str, Any], + compute_type: tl.dtype, +) -> None: + """ + Invoke the fused MOE backward kernels to compute gradients. + + Args: + grad_output: Gradient of output, shape (num_tokens * topk, N) or (num_tokens, topk, N) + input: Input tensor, shape (num_tokens, K) + weight: Weight tensor, shape (E, N, K) + grad_input: Output gradient for input, shape (num_tokens, K) + grad_weight: Output gradient for weight, shape (E, N, K) + grad_topk_weights: Output gradient for topk_weights, shape (num_tokens, topk) or None + topk_weights: Top-K routing weights, shape (num_tokens, topk) + topk_ids: Top-K expert IDs, shape (num_tokens, topk) + sorted_token_ids: Sorted token IDs + expert_ids: Expert IDs for each block + num_tokens_post_padded: Number of tokens after padding + mul_routed_weight: Whether to multiply by routing weights + top_k: Number of experts per token + config: Kernel configuration + compute_type: Computation data type + """ + assert topk_weights.stride(1) == 1 + assert sorted_token_ids.stride(0) == 1 + + # Flatten grad_output if needed + # Before: (num_tokens, topk, hidden_size) + # After: (num_tokens * topk, hidden_size) + if grad_output.ndim == 3: + grad_output = grad_output.reshape(-1, grad_output.shape[-1]) + + E, N, K = weight.shape + + # ===================== Compute grad_input ===================== + def grid_input(META): + return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]),) + + fused_moe_backward_input_kernel[grid_input]( + grad_output, + weight, + grad_input, + grad_topk_weights if grad_topk_weights is not None else grad_input, # dummy pointer + topk_weights, + sorted_token_ids, + expert_ids, + num_tokens_post_padded, + N, + K, + sorted_token_ids.shape[0], + grad_output.shape[0], + grad_output.stride(0), + grad_output.stride(1), + weight.stride(0), + weight.stride(1), + weight.stride(2), + grad_input.stride(0), + grad_input.stride(1), + MUL_ROUTED_WEIGHT=mul_routed_weight, + top_k=top_k, + compute_type=compute_type, + **config, + ) + + # ===================== Compute grad_weight ===================== + # Initialize grad_weight to zero + grad_weight.zero_() + + # Use same grid configuration as forward kernel: encode both M and N dimensions + def grid_weight(META): + return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]),) + + fused_moe_backward_weight_kernel[grid_weight]( + grad_output, + input, + grad_weight, + topk_weights, + sorted_token_ids, + expert_ids, + num_tokens_post_padded, + N, + K, + sorted_token_ids.shape[0], + grad_output.shape[0], + grad_output.stride(0), + grad_output.stride(1), + input.stride(0), + input.stride(1), + grad_weight.stride(0), + grad_weight.stride(1), + grad_weight.stride(2), + MUL_ROUTED_WEIGHT=mul_routed_weight, + top_k=top_k, + compute_type=compute_type, + **config, + ) + + # ===================== Compute grad_topk_weights (if needed) ===================== + if mul_routed_weight and grad_topk_weights is not None: + + def grid_topk(META): + return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]),) + + fused_moe_backward_topk_weights_kernel[grid_topk]( + grad_output, + input, + weight, + grad_topk_weights.view(-1), + sorted_token_ids, + expert_ids, + num_tokens_post_padded, + N, + K, + sorted_token_ids.shape[0], + grad_output.shape[0], + grad_output.stride(0), + grad_output.stride(1), + input.stride(0), + input.stride(1), + weight.stride(0), + weight.stride(1), + weight.stride(2), + top_k=top_k, + compute_type=compute_type, + BLOCK_SIZE_M=config["BLOCK_SIZE_M"], + BLOCK_SIZE_N=config["BLOCK_SIZE_N"], + BLOCK_SIZE_K=config["BLOCK_SIZE_K"], + ) diff --git a/miles/backends/fsdp_utils/update_weight_utils.py b/miles/backends/fsdp_utils/update_weight_utils.py index c8dcbd810..d0f2360ab 100644 --- a/miles/backends/fsdp_utils/update_weight_utils.py +++ b/miles/backends/fsdp_utils/update_weight_utils.py @@ -33,6 +33,7 @@ class UpdateWeight(abc.ABC): def __init__(self, args: Namespace, model: torch.nn.Module) -> None: self.args = args self.model = model + self.weight_version = 0 @abc.abstractmethod def connect_rollout_engines( @@ -43,6 +44,7 @@ def connect_rollout_engines( pass def update_weights(self) -> None: + self.weight_version += 1 bucket = [] bucket_size = 0 for name, param in self.model.state_dict().items(): @@ -71,10 +73,10 @@ def update_weights(self) -> None: def wait_and_update_bucket_weights(self, bucket): bucket = [(name, param.wait()) if hasattr(param, "wait") else (name, param) for name, param in bucket] - self.update_bucket_weights(bucket) + self.update_bucket_weights(bucket, weight_version=self.weight_version) @abc.abstractmethod - def update_bucket_weights(self, named_tensors) -> None: + def update_bucket_weights(self, named_tensors, weight_version=None) -> None: pass @@ -114,7 +116,7 @@ def connect_rollout_engines( # Calculate TP rank within this SGLang engine group self.tp_rank = dist.get_rank() - start_rank - def update_bucket_weights(self, named_tensors) -> None: + def update_bucket_weights(self, named_tensors, weight_version=None) -> None: monkey_patch_torch_reductions() # Use flattened bucket approach similar to Megatron logger.info("Using flattened tensor bucket") @@ -162,6 +164,7 @@ def update_bucket_weights(self, named_tensors) -> None: "serialized_named_tensors": [tensors[i] for tensors in gathered_serialized_batches], "load_format": "flattened_bucket", "flush_cache": False, + "weight_version": str(weight_version), } ref = self._ipc_engine.update_weights_from_tensor.remote(**kwargs) ray.get(ref) @@ -174,10 +177,6 @@ def update_bucket_weights(self, named_tensors) -> None: class UpdateWeightFromDistributed(UpdateWeight): """Broadcast weights via a temporary NCCL group to rollout engines.""" - def __init__(self, args: Namespace, model: torch.nn.Module) -> None: - self.args = args - self.model = model - def connect_rollout_engines( self, rollout_engines: Sequence[ActorHandle], @@ -220,7 +219,7 @@ def connect_rollout_engines( ) ray.get(refs) - def update_bucket_weights(self, named_tensors) -> None: + def update_bucket_weights(self, named_tensors, weight_version=None) -> None: """Send names/dtypes/shapes metadata to engines, then broadcast tensors. Ensures tensors are contiguous; when `world_size == 1`, converts DTensors @@ -235,6 +234,7 @@ def update_bucket_weights(self, named_tensors) -> None: dtypes=[param.dtype for _, param in named_tensors], shapes=[param.shape for _, param in named_tensors], group_name=self._group_name, + weight_version=str(weight_version), ) for engine in self.rollout_engines ] diff --git a/miles/backends/megatron_utils/__init__.py b/miles/backends/megatron_utils/__init__.py index d67804568..9ca75eb41 100644 --- a/miles/backends/megatron_utils/__init__.py +++ b/miles/backends/megatron_utils/__init__.py @@ -20,23 +20,23 @@ def new_init(self, *args, **kwargs): except ImportError: logging.warning("deep_ep is not installed, some functionalities may be limited.") +try: + from megatron.bridge.models.qwen_vl.modelling_qwen3_vl.text_model import ( + Qwen3VLMoETextRotaryEmbedding, + Qwen3VLTextRotaryEmbedding, + ) -from .actor import MegatronTrainRayActor -from .arguments import parse_args, set_default_megatron_args, validate_args -from .checkpoint import load_checkpoint, save_checkpoint -from .initialize import init -from .model import initialize_model_and_optimizer + def patch_rotary_embedding(cls): + _original_forward = cls.forward -logging.getLogger().setLevel(logging.WARNING) + def _patched_forward(self, *args, packed_seq_params=None, **kwargs): + return _original_forward(self, *args, **kwargs) + cls.forward = _patched_forward -__all__ = [ - "parse_args", - "validate_args", - "load_checkpoint", - "save_checkpoint", - "set_default_megatron_args", - "MegatronTrainRayActor", - "init", - "initialize_model_and_optimizer", -] + patch_rotary_embedding(Qwen3VLTextRotaryEmbedding) + patch_rotary_embedding(Qwen3VLMoETextRotaryEmbedding) +except ImportError: + pass + +logging.getLogger().setLevel(logging.WARNING) diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index bcbdd1a42..9f869415c 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -1,5 +1,6 @@ import logging import os +import random import socket from argparse import Namespace from contextlib import nullcontext @@ -192,6 +193,16 @@ def _get_rollout_data(self, rollout_data_ref: Box) -> RolloutBatch: rollout_data["loss_masks"] = [ torch.tensor(t, dtype=torch.int, device=torch.cuda.current_device()) for t in rollout_data["loss_masks"] ] + if "multimodal_train_inputs" in rollout_data: + # Move multimodal training tensors to GPU in advance + rollout_data["multimodal_train_inputs"] = [ + ( + {key: tensor.to(device=torch.cuda.current_device()) for key, tensor in mm_dict.items()} + if mm_dict is not None + else None + ) + for mm_dict in rollout_data["multimodal_train_inputs"] + ] if self.args.qkv_format == "bshd": # TODO: micro-batch wise dynamic, possibly move to @data.py:get_data_iterator @@ -467,11 +478,26 @@ def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None: log_perf_data(rollout_id, self.args) @timer - def save_model(self, iteration: int) -> None: + def save_model(self, rollout_id: int, force_sync: bool = False) -> None: if self.args.debug_rollout_only: return - save(iteration, self.model, self.optimizer, self.opt_param_scheduler) + # torch dist may trigger nccl communication during saving. + if self.args.offload_train: + reload_process_groups() + + if self.args.async_save: + from megatron.training.async_utils import maybe_finalize_async_save + + maybe_finalize_async_save(blocking=True) + + save(rollout_id, self.model, self.optimizer, self.opt_param_scheduler) + + if force_sync and self.args.async_save: + maybe_finalize_async_save(blocking=True) + + if self.args.offload_train: + destroy_process_groups() @timer def update_weights(self) -> None: @@ -493,6 +519,14 @@ def update_weights(self) -> None: self.weight_updater.update_weights() print_memory("after update_weights") + if self.args.ci_test and len(rollout_engines) > 0: + engine = random.choice(rollout_engines) + engine_version = ray.get(engine.get_weight_version.remote()) + if str(engine_version) != str(self.weight_updater.weight_version): + raise RuntimeError( + f"Weight version mismatch! Engine: {engine_version}, Updater: {self.weight_updater.weight_version}" + ) + if getattr(self.args, "keep_old_actor", False): if self.args.update_weights_interval == 1: logger.info("updating model queue: rollout_actor -> old_actor, actor -> rollout_actor") diff --git a/miles/backends/megatron_utils/arguments.py b/miles/backends/megatron_utils/arguments.py index 5d5090116..aea72ceb8 100644 --- a/miles/backends/megatron_utils/arguments.py +++ b/miles/backends/megatron_utils/arguments.py @@ -16,6 +16,8 @@ def set_default_megatron_args(args): # placeholders args.seq_length = 4096 args.max_position_embeddings = args.seq_length + # megatron(dev) optimizer-cpu-offload save ckpt bugs + args.dist_ckpt_save_pre_mcore_014 = True # compatible for megatron if hasattr(args, "rope_type") and args.rope_type is None: args.rope_type = "yarn" if args.multi_latent_attention else "rope" diff --git a/miles/backends/megatron_utils/checkpoint.py b/miles/backends/megatron_utils/checkpoint.py index 6bd77d4a4..35d712910 100644 --- a/miles/backends/megatron_utils/checkpoint.py +++ b/miles/backends/megatron_utils/checkpoint.py @@ -7,6 +7,7 @@ from megatron.training.checkpointing import load_checkpoint as _load_checkpoint_megatron from megatron.training.checkpointing import save_checkpoint from megatron.training.global_vars import get_args + from miles.utils import megatron_bridge_utils logger = logging.getLogger(__name__) @@ -47,13 +48,15 @@ def _is_megatron_checkpoint(path: str | Path) -> bool: def _load_checkpoint_hf(ddp_model, optimizer, args, load_path: str): + assert args.megatron_to_hf_mode == "bridge", "Only bridge mode is supported for loading HF checkpoint" from megatron.bridge import AutoBridge + import miles_plugins.megatron_bridge # noqa: F401 logger.info(f"Load checkpoint from HuggingFace model into Megatron (path={load_path})") - bridge = AutoBridge.from_hf_pretrained(load_path, trust_remote_code=True) with megatron_bridge_utils.patch_megatron_model(ddp_model): + bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True) bridge.load_hf_weights(ddp_model) # Copied from Megatron-core :: load_checkpoint (with simplifications) diff --git a/miles/backends/megatron_utils/ci_utils.py b/miles/backends/megatron_utils/ci_utils.py new file mode 100644 index 000000000..e6ce784ca --- /dev/null +++ b/miles/backends/megatron_utils/ci_utils.py @@ -0,0 +1,84 @@ +"""CI utilities for Megatron backend testing.""" + +import logging +from collections.abc import Sequence + +from megatron.core.distributed import DistributedDataParallel as DDP + +logger = logging.getLogger(__name__) + + +def check_mtp_only_grad(model: Sequence[DDP], step_id: int) -> None: + """Check that only MTP parameters have non-zero gradients. + + This is used for CI testing to verify that when all outputs are truncated, + only the MTP layers receive gradients (since only mtp_loss contributes). + + Args: + model: Sequence of DDP-wrapped model chunks. + step_id: Current step index for logging. + + Raises: + AssertionError: If any non-MTP parameter has a non-zero gradient. + """ + non_mtp_nonzero_grads = [] + mtp_nonzero_grads = [] + + for model_chunk in model: + for name, param in model_chunk.named_parameters(): + # Get the main_grad from the distributed optimizer if available + grad = getattr(param, "main_grad", None) + if grad is None: + grad = param.grad + if grad is None: + continue + + grad_norm = grad.abs().max().item() + is_mtp = ".mtp." in name + + if is_mtp: + if grad_norm > 0: + mtp_nonzero_grads.append((name, grad_norm)) + else: + if grad_norm > 0: + non_mtp_nonzero_grads.append((name, grad_norm)) + + # Log the results + logger.info( + f"[CI MTP Grad Check] Step {step_id}: " + f"MTP params with non-zero grad: {len(mtp_nonzero_grads)}, " + f"non-MTP params with non-zero grad: {len(non_mtp_nonzero_grads)}" + ) + + if non_mtp_nonzero_grads: + # Log the first few non-MTP params with non-zero gradients for debugging + for name, grad_norm in non_mtp_nonzero_grads[:5]: + logger.error(f"[CI MTP Grad Check] Non-MTP param with non-zero grad: {name}, max_grad={grad_norm}") + + assert len(non_mtp_nonzero_grads) == 0, ( + f"Expected all non-MTP parameters to have zero gradients, " + f"but found {len(non_mtp_nonzero_grads)} with non-zero gradients. " + f"First few: {non_mtp_nonzero_grads[:5]}" + ) + + # Also verify that MTP params do have gradients (otherwise the test is not valid) + assert len(mtp_nonzero_grads) > 0, ( + "Expected MTP parameters to have non-zero gradients, but all were zero. " + "This may indicate the MTP loss is not being computed." + ) + + +def check_mtp_loss(mtp_loss: float, max_mtp_loss: float = 1.0) -> None: + """Check that MTP loss is within expected bounds. + + Args: + mtp_loss: The computed MTP loss value. + max_mtp_loss: Maximum allowed MTP loss (default: 1.0). + + Raises: + AssertionError: If MTP loss exceeds the maximum allowed value. + """ + assert mtp_loss < max_mtp_loss, ( + f"MTP loss {mtp_loss} exceeds maximum allowed value {max_mtp_loss}. " + "This may indicate an issue with MTP training." + ) diff --git a/miles/backends/megatron_utils/config_mapping/__init__.py b/miles/backends/megatron_utils/config_mapping/__init__.py deleted file mode 100644 index cc8ebc132..000000000 --- a/miles/backends/megatron_utils/config_mapping/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .registry import mapper_registry, register_mapper - - -def get_mapper(name: str): - return mapper_registry.get_mapper(name) - - -__all__ = [ - "register_mapper", - "mapper_registry", - "get_mapper", -] diff --git a/miles/backends/megatron_utils/config_mapping/predefined_config_mappers.py b/miles/backends/megatron_utils/config_mapping/predefined_config_mappers.py deleted file mode 100644 index 8f092d9ae..000000000 --- a/miles/backends/megatron_utils/config_mapping/predefined_config_mappers.py +++ /dev/null @@ -1,128 +0,0 @@ -from collections import namedtuple -import torch.nn.functional as F -from transformers import PretrainedConfig -from .registry import register_mapper - - -MegatronModelConfig = namedtuple("MegatronModelConfig", ["transformer_config", "gpt_model_args"]) - - -def _get_activation_func(name: str): - if name == "silu": - return F.silu - elif name == "gelu": - return F.gelu - else: - raise ValueError(f"Unsupported activation function: {name}") - - -def _to_n_args(value): - if isinstance(value, list): - return value - return [value] - - -def _map_common_configs(hf_config: PretrainedConfig) -> MegatronModelConfig: - rope_scaling_args = {} - if "rope_scaling" in hf_config and hf_config.rope_scaling is not None: - rope_scaling_args["seq_len_interpolation_factor"] = hf_config.rope_scaling["factor"] - return MegatronModelConfig( - transformer_config={ - # Model architecture parameters - "num_layers": hf_config.num_hidden_layers, - "hidden_size": hf_config.hidden_size, - "num_attention_heads": hf_config.num_attention_heads, - "num_query_groups": hf_config.num_key_value_heads, - "ffn_hidden_size": hf_config.intermediate_size, - "kv_channels": getattr(hf_config, "head_dim", None), - "layernorm_epsilon": hf_config.rms_norm_eps, - # Activation and normalization - "activation_func": _get_activation_func(hf_config.hidden_act), - "normalization": "RMSNorm", - "gated_linear_unit": True, - }, - gpt_model_args={ - "vocab_size": hf_config.vocab_size, - "rotary_base": hf_config.rope_theta, - "position_embedding_type": "rope", - "untie_embeddings_and_output_weights": not hf_config.tie_word_embeddings, - }, - ) - - -@register_mapper("qwen2") -def qwen2_config_mapper(hf_config: PretrainedConfig) -> MegatronModelConfig: - mapped_config = _map_common_configs(hf_config) - mapped_config.transformer_config.update( - { - "add_bias_linear": False, - "add_qkv_bias": hf_config.attention_bias, - } - ) - - return mapped_config - - -@register_mapper("qwen3") -def qwen3_config_mapper(hf_config: PretrainedConfig) -> MegatronModelConfig: - mapped_config = _map_common_configs(hf_config) - mapped_config.transformer_config.update( - { - "add_bias_linear": False, - "add_qkv_bias": hf_config.attention_bias, - "qk_layernorm": True, - } - ) - - return mapped_config - - -@register_mapper("qwen3_moe") -def qwen3_moe_config_mapper(hf_config: PretrainedConfig) -> MegatronModelConfig: - mapped_config = _map_common_configs(hf_config) - mapped_config.transformer_config.update( - { - "add_bias_linear": False, - "add_qkv_bias": hf_config.attention_bias, - "moe_ffn_hidden_size": hf_config.moe_intermediate_size, - "moe_router_topk": hf_config.num_experts_per_tok, - "num_moe_experts": hf_config.num_experts, - "moe_aux_loss_coeff": _to_n_args(hf_config.router_aux_loss_coef), - "moe_router_load_balancing_type": _to_n_args("none"), # turn off aux_loss as it hurts perf in RL - "moe_router_score_function": "softmax", - "moe_router_pre_softmax": False, - "qk_layernorm": True, - } - ) - - return mapped_config - - -@register_mapper("glm4_moe") -def glm4_moe_config_mapper(hf_config: PretrainedConfig) -> MegatronModelConfig: - moe_layer_freq = [1] * hf_config.num_hidden_layers - for i in range(min(hf_config.first_k_dense_replace, hf_config.num_hidden_layers)): - moe_layer_freq[i] = 0 - - mapped_config = _map_common_configs(hf_config) - mapped_config.transformer_config.update( - { - "add_bias_linear": False, - "qk_layernorm": hf_config.use_qk_norm, - "add_qkv_bias": hf_config.attention_bias, - "moe_ffn_hidden_size": hf_config.moe_intermediate_size, - "moe_router_topk": hf_config.num_experts_per_tok, - "moe_router_topk_scaling_factor": hf_config.routed_scaling_factor, - "moe_router_dtype": "fp32", - "num_moe_experts": hf_config.num_experts, - "moe_router_enable_expert_bias": True, - "moe_layer_freq": moe_layer_freq, - "moe_router_bias_update_rate": 0.0, - "moe_aux_loss_coeff": _to_n_args(hf_config.router_aux_loss_coef), - "moe_router_load_balancing_type": _to_n_args("seq_aux_loss"), - "moe_router_score_function": "sigmoid", - "rotary_percent": hf_config.partial_rotary_factor, - } - ) - - return mapped_config diff --git a/miles/backends/megatron_utils/config_mapping/registry.py b/miles/backends/megatron_utils/config_mapping/registry.py deleted file mode 100644 index ebc2677f2..000000000 --- a/miles/backends/megatron_utils/config_mapping/registry.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -from collections.abc import Callable - -logger = logging.getLogger(__name__) - - -class MapperRegistry: - """ - Registry for config mappers. - """ - - def __init__(self): - self._mappers: dict[str, Callable] = {} - - def register(self, model_types: list[str], mapper_func: Callable): - if not callable(mapper_func): - raise TypeError(f"Mapper for {model_types} must be callable") - - for name in model_types: - if name in self._mappers: - logger.warning(f"Mapper for {name} is being overridden") - self._mappers[name] = mapper_func - logger.info(f"Registered config mapper for model type: {name}") - - def get_mapper(self, name: str) -> Callable: - """ - Get the mapper by model_type. - """ - if name not in self._mappers: - raise ValueError(f"Mapper for {name} is not registered.") - return self._mappers[name] - - def list_registered_mappers(self) -> list[str]: - return list(self._mappers.keys()) - - -# Global registry instance -mapper_registry = MapperRegistry() - - -def register_mapper(*args): - """ - Decorator: register config mapper. - - Args: suppotred model_types. - """ - - def decorator(func: Callable): - mapper_registry.register( - model_types=list(args), - mapper_func=func, - ) - return func - - return decorator diff --git a/miles/backends/megatron_utils/data.py b/miles/backends/megatron_utils/data.py index f94d1b7e0..8aa01e126 100644 --- a/miles/backends/megatron_utils/data.py +++ b/miles/backends/megatron_utils/data.py @@ -50,6 +50,7 @@ def get_batch( batch = data_iterator.get_next(keys) packed_seq_params = None + max_seqlen = None tokens = batch["tokens"] # use 0 as the pad token id should be fine? pad_token_id = 0 @@ -99,6 +100,46 @@ def get_batch( batch["tokens"] = tokens batch["packed_seq_params"] = packed_seq_params + + # loss masks + loss_masks = [] + for loss_mask, total_length, response_length in zip( + batch["loss_masks"], + batch["total_lengths"], + batch["response_lengths"], + strict=True, + ): + prompt_length = total_length - response_length + loss_mask = F.pad(loss_mask, (prompt_length - 1, 1), value=0) + loss_mask = slice_with_cp(loss_mask, 0, qkv_format, max_seqlen) + loss_masks.append(loss_mask) + + if qkv_format == "bshd": + loss_masks = torch.stack(loss_masks) + elif qkv_format == "thd": + loss_masks = torch.cat(loss_masks) + loss_masks = F.pad(loss_masks, (0, pad), value=0).unsqueeze(0) + + assert loss_masks.shape == tokens.shape, f"loss_masks.shape: {loss_masks.shape}, tokens.shape: {tokens.shape}" + batch["full_loss_masks"] = loss_masks + + # Process multimodal training tensors if present + multimodal_train_inputs = batch.get("multimodal_train_inputs", None) + if multimodal_train_inputs is not None: + multimodal_data = {} # key -> concatenated tensor + multimodal_num_items = {} # key -> list of item counts per sequence + for mm_input_dict in multimodal_train_inputs: + if mm_input_dict is not None: + for key, mm_tensor in mm_input_dict.items(): + if key not in multimodal_data: + multimodal_data[key] = mm_tensor + multimodal_num_items[key] = [mm_tensor.size(0)] + else: + multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0) + multimodal_num_items[key].append(mm_tensor.size(0)) + batch["multimodal_train_inputs"] = multimodal_data + batch["multimodal_num_items"] = multimodal_num_items + return batch @@ -325,6 +366,7 @@ def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatc for key, val in rollout_data.items(): if key in [ "tokens", + "multimodal_train_inputs", "loss_masks", "sample_indices", "rollout_routed_experts", @@ -376,6 +418,64 @@ def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatc if args.log_passrate: log_passrate(rollout_id, args, rollout_data) + if args.log_correct_samples: + if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage(): + cp_size = mpu.get_context_parallel_world_size() + log_dict = {} + response_lengths = rollout_data["response_lengths"] + loss_masks = rollout_data["loss_masks"] + total_lengths = rollout_data["total_lengths"] + + def quantile(total_value, n_quantiles, data) -> dict: + import math + + assert n_quantiles > 1, f"n_quantiles({n_quantiles}) must be greater than 1." + + quantiles = [((i + 1) / n_quantiles) for i in range(n_quantiles)] + cut_points = [total_value * q for q in quantiles] + cut_points[-1] = total_value + + count = [0] * n_quantiles + for d in data: + for i, point in enumerate(cut_points): + if d <= point: + count[i] += 1 + break + + total = sum(count) + 1e-9 + percentile = [c / total for c in count] + + percentile = {f"p{min(math.ceil(q*100),100)}": p for q, p in zip(quantiles, percentile, strict=True)} + return percentile + + raw_rewards = rollout_data["raw_reward"] + # Additional metrics for correct cases are calculated separately below. + correct_response_lengths = [] + correct_total_lengths = [] + correct_loss_masks = [] + correct_entropy = [] + for i, raw_reward in enumerate(raw_rewards): + if raw_reward == 1: + correct_response_lengths.append(response_lengths[i]) + correct_total_lengths.append(total_lengths[i]) + correct_loss_masks.append(loss_masks[i]) + correct_entropy.append(-rollout_data["log_probs"][i]) + num_correct_responses = len(correct_total_lengths) + rollout_data["correct_response_lengths"] = correct_response_lengths + correct_response_length_percentile = quantile( + args.rollout_max_response_len, 4, rollout_data["correct_response_lengths"] + ) + for p, val in correct_response_length_percentile.items(): + rollout_data[f"correct_length/{p}"] = [val] * num_correct_responses + if len(correct_entropy) > 0: + sum_of_sample_mean = get_sum_of_sample_mean( + correct_total_lengths, correct_response_lengths, correct_loss_masks + ) + correct_entropy = sum_of_sample_mean(torch.cat(correct_entropy, dim=0)) + rollout_data["correct_entropy"] = [correct_entropy.item()] * num_correct_responses + else: + rollout_data["correct_entropy"] = [0] * num_correct_responses + def log_multi_turn_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None: """ diff --git a/miles/backends/megatron_utils/initialize.py b/miles/backends/megatron_utils/initialize.py index e9f062c11..cbe981632 100644 --- a/miles/backends/megatron_utils/initialize.py +++ b/miles/backends/megatron_utils/initialize.py @@ -4,6 +4,7 @@ import numpy as np import torch from megatron.core import mpu, tensor_parallel +from megatron.core.config import set_experimental_flag from megatron.core.num_microbatches_calculator import init_num_microbatches_calculator from megatron.training.global_vars import _build_tokenizer, set_args @@ -54,6 +55,10 @@ def _initialize_distributed(args, get_embedding_ranks=None, get_position_embeddi def init(args): set_args(args) + if args.enable_experimental: + logger.info("Enable megatron experimental") + set_experimental_flag(True) + # Pytorch distributed. _initialize_distributed(args) diff --git a/miles/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu b/miles/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu new file mode 100644 index 000000000..a6e955490 --- /dev/null +++ b/miles/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu @@ -0,0 +1,368 @@ +#include +#include + +#define FINAL_MASK 0xFFFFFFFF + +__device__ __host__ __forceinline__ +int ceil_div(int a, int b) { + return (a + b - 1) / b; +} + +__device__ __forceinline__ +float warpReduceMax(float val) { +#pragma unroll + for (int mask = 16; mask > 0; mask >>= 1) + val = fmaxf(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32)); + return val; +} + + +__device__ __forceinline__ +float warpReduceMin(float val) { +#pragma unroll + for (int mask = 16; mask > 0; mask >>= 1) + val = fminf(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32)); + return val; +} + +// almost all int4 use blocksize = [1, 32] +template +__global__ +void int4_quant_1x32_kernel( + const scalar_t* __restrict__ x, + scalar_t* __restrict__ out, + scalar_t* out_scale, + scalar_t* out_zero, + const int M, const int N, + const int stride_xm, const int stride_xn, + const int stride_om, const int stride_on, + const int stride_osm, const int stride_osn, + const int stride_ozm, const int stride_ozn, + bool sym +) { + constexpr int WARPS_PER_BLOCK = 8; + const int needed_warps = ceil_div(N, 32); + + const int tid = threadIdx.x; + const int warp_id = tid >> 5; + const int lane_id = tid & 0x1F; + constexpr float SYM_CONS = 1.0f / 7.0f; + constexpr float ASYM_CONS = 1.0f / 15.0f; + + const int row = blockIdx.x; + + for (int item = warp_id; item < needed_warps; item += WARPS_PER_BLOCK) { + const int col = item * 32 + lane_id; + float val = 0.0f; + + if (col < N) { + val = static_cast(x[row * stride_xm + col * stride_xn]); + } + + float scale = 0.0f; + float zero = 0.0f; + + if (sym) { + float abs_val = fabsf(val); + + float block_max = warpReduceMax(abs_val); + + scale = fmaxf(block_max * SYM_CONS, 1e-5f); + + val = rintf(val / scale); + } else { + float block_min = warpReduceMin(val); + float block_max = warpReduceMax(val); + + scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f); + zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f); + + val = rintf(val / scale) + zero; + } + + if (col < N) { + out[row * stride_om + col * stride_on] = static_cast(val); + out_scale[row * stride_osm + item * stride_osn] = static_cast(scale); + if(!sym) { + out_zero[row * stride_ozm + item * stride_ozn] = static_cast(zero); + } + } + } +} + +// for some transpose case, blocksize = [32, 1] +template +__global__ +void int4_quant_32x1_kernel( + const scalar_t* __restrict__ x, + scalar_t* __restrict__ out, + scalar_t* out_scale, + scalar_t* out_zero, + const int M, const int N, + const int stride_xm, const int stride_xn, + const int stride_om, const int stride_on, + const int stride_osm, const int stride_osn, + const int stride_ozm, const int stride_ozn, + bool sym +) { + constexpr int WARPS_PER_BLOCK = 8; + const int start_row = blockIdx.x * 32; + const int end_row = min((blockIdx.x + 1) * 32, M); + + const int tid = threadIdx.x; + const int warp_id = tid >> 5; + const int lane_id = tid & 0x1F; + constexpr float SYM_CONS = 1.0f / 7.0f; + constexpr float ASYM_CONS = 1.0f / 15.0f; + + for (int item = warp_id; item < N; item += WARPS_PER_BLOCK) { + const int col = item; + const int row = start_row + lane_id; + + float val = 0.0f; + + if (row < end_row) { + val = static_cast(x[row * stride_xm + col * stride_xn]); + } + + float scale = 0.0f; + float zero = 0.0f; + + if (sym) { + float abs_val = fabsf(val); + + float block_max = warpReduceMax(abs_val); + + scale = fmaxf(block_max * SYM_CONS, 1e-5f); + + val = rintf(val / scale); + } else { + float block_min = warpReduceMin(val); + float block_max = warpReduceMax(val); + + scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f); + zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f); + + val = rintf(val / scale) + zero; + } + + if (row < end_row) { + out[row * stride_om + col * stride_on] = static_cast(val); + out_scale[blockIdx.x * stride_osm + item * stride_osn] = static_cast(scale); + if (!sym) { + out_zero[blockIdx.x * stride_ozm + item * stride_ozn] = static_cast(zero); + } + } + } +} + +template +__global__ void int4_quant_common_kernel( + const scalar_t* __restrict__ x, + scalar_t* __restrict__ out, + scalar_t* out_scale, + scalar_t* out_zero, + const int M, const int N, + const int stride_xm, const int stride_xn, + const int stride_om, const int stride_on, + const int stride_osm, const int stride_osn, + const int stride_ozm, const int stride_ozn, + const int BLOCK_M, const int BLOCK_N, + bool sym +) { + const int start_row = blockIdx.x * BLOCK_M; + const int WARPS_PER_BLOCK = blockDim.x >> 5; + + const int warp_id = threadIdx.x >> 5; + const int lane_id = threadIdx.x & 0x1F; + constexpr float SYM_CONS = 1.0f / 7.0f; + constexpr float ASYM_CONS = 1.0f / 15.0f; + constexpr int WARP_SIZE = 32; + + const int needed_warps = ceil_div(N, BLOCK_N); + const int iters = ceil_div(BLOCK_M * BLOCK_N, 32); + int warp_rows = 1; + + if (BLOCK_N <= WARP_SIZE) { + warp_rows = WARP_SIZE / BLOCK_N; + } + + for (int item = warp_id; item < needed_warps; item += WARPS_PER_BLOCK) { + float local_max = -INFINITY; + float local_min = INFINITY; + + float val = 0.0f; + float scale, zero = 0.0f; + + const int row_off = lane_id / BLOCK_N; + const int col_off = lane_id % BLOCK_N; + int row, col = 0; + + for (int i = 0; i < iters; ++i) { + if (BLOCK_N <= WARP_SIZE) { + row = start_row + i * warp_rows + row_off; + col = item * BLOCK_N + col_off; + } else { + row = start_row; + col = item * BLOCK_N + i * WARP_SIZE + col_off; + } + + if (row < M && col < N) { + val = static_cast(x[row * stride_xm + col * stride_xn]); + } else { + val = 0.0f; + } + + if (sym) { + local_max = fmaxf(local_max, fabsf(val)); + } else { + local_max = fmaxf(local_max, val); + local_min = fminf(local_min, val); + } + } + + if (sym) { + float block_max = warpReduceMax(local_max); + scale = fmaxf(block_max * SYM_CONS, 1e-5f); + } else { + float block_max = warpReduceMax(local_max); + float block_min = warpReduceMin(local_min); + scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f); + zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f); + } + + for (int i = 0; i < iters; ++i) { + if (BLOCK_N <= WARP_SIZE) { + row = start_row + i * warp_rows + row_off; + col = item * BLOCK_N + col_off; + } else { + row = start_row; + col = item * BLOCK_N + i * WARP_SIZE + col_off; + } + + if (row < M && col < N) { + float val = static_cast(x[row * stride_xm + col * stride_xn]); + if (sym) { + val = rintf(val / scale); + } else { + val = rintf(val / scale) + zero; + } + + out[row * stride_om + col * stride_on] = static_cast(val); + out_scale[blockIdx.x * stride_osm + item * stride_osn] = static_cast(scale); + if (!sym) { + out_zero[blockIdx.x * stride_ozm + item * stride_ozn] = static_cast(zero); + } + } + } + } +} + +// dispatch +template +void launch_int4_quant_kernel( + const scalar_t* x, + scalar_t* out, + scalar_t* out_scale, + scalar_t* out_zero, + int M, int N, + const int stride_xm, const int stride_xn, + const int stride_om, const int stride_on, + const int stride_osm, const int stride_osn, + const int stride_ozm, const int stride_ozn, + int block_m, int block_n, + bool sym, + cudaStream_t stream +) { + constexpr int WARPS_PER_BLOCK = 8; + constexpr int THREADS_PER_BLOCK = WARPS_PER_BLOCK * 32; // 256 + + if (block_m == 1 && block_n == 32) { + dim3 grid(M); + dim3 block(THREADS_PER_BLOCK); + + int4_quant_1x32_kernel<<>>( + x, out, out_scale, out_zero, M, N, + stride_xm, stride_xn, + stride_om, stride_on, + stride_osm, stride_osn, + stride_ozm, stride_ozn, + sym + ); + } else if (block_m == 32 && block_n == 1) { + dim3 grid(ceil_div(M, block_m)); + dim3 block(THREADS_PER_BLOCK); + + int4_quant_32x1_kernel<<>>( + x, out, out_scale, out_zero, M, N, + stride_xm, stride_xn, + stride_om, stride_on, + stride_osm, stride_osn, + stride_ozm, stride_ozn, + sym + ); + } else { + dim3 grid(ceil_div(M, block_m)); + dim3 block(THREADS_PER_BLOCK); + int4_quant_common_kernel<<>>( + x, out, out_scale, out_zero, M, N, + stride_xm, stride_xn, + stride_om, stride_on, + stride_osm, stride_osn, + stride_ozm, stride_ozn, + block_m, block_n, + sym + ); + } +} + +std::tuple +fake_int4_quant_cuda( + torch::Tensor& x, + std::vector& block_size, + bool sym +) { + TORCH_CHECK(x.dim() == 2, "Input must be 2D"); + TORCH_CHECK(x.is_cuda(), "Input must be on CUDA"); + + int M = x.size(0); + int N = x.size(1); + int block_m = block_size[0]; + int block_n = block_size[1]; + + TORCH_CHECK(block_m > 0 && block_n > 0, "Block sizes must be positive, got block_m=", block_m, ", block_n=", block_n); + TORCH_CHECK((block_m * block_n) % 32 == 0, + "block_m * block_n (", block_m * block_n, ") must be divisible by 32. " + "But got a ", block_m, "x", block_n, " block."); + + auto out = torch::empty_like(x); + auto out_scale = torch::empty({ceil_div(M, block_m), ceil_div(N, block_n)}, x.options()); + auto out_zero = torch::empty_like(out_scale); + + const cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + AT_DISPATCH_FLOATING_TYPES_AND( + at::ScalarType::BFloat16, + x.scalar_type(), "int4_quant_cuda", [&] { + launch_int4_quant_kernel( + x.const_data_ptr(), + out.data_ptr(), + out_scale.data_ptr(), + out_zero.data_ptr(), + M, N, + x.stride(0), x.stride(1), + out.stride(0), out.stride(1), + out_scale.stride(0), out_scale.stride(1), + out_zero.stride(0), out_zero.stride(1), + block_m, block_n, + sym, + stream + ); + }); + + return std::make_tuple(out, out_scale, out_zero); +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("fake_int4_quant_cuda", &fake_int4_quant_cuda, "fake INT4 quantization cuda"); +} diff --git a/miles/backends/megatron_utils/kernels/int4_qat/setup.py b/miles/backends/megatron_utils/kernels/int4_qat/setup.py new file mode 100644 index 000000000..b27967bc9 --- /dev/null +++ b/miles/backends/megatron_utils/kernels/int4_qat/setup.py @@ -0,0 +1,39 @@ +from setuptools import setup +from torch.utils.cpp_extension import BuildExtension, CUDAExtension +import torch + +# Get CUDA arch list +arch_list = [] +if torch.cuda.is_available(): + for i in range(torch.cuda.device_count()): + major, minor = torch.cuda.get_device_capability(i) + arch_list.append(f"{major}.{minor}") + arch_list = sorted(set(arch_list)) + +setup( + name="fake_int4_quant_cuda", + ext_modules=[ + CUDAExtension( + name="fake_int4_quant_cuda", + sources=["fake_int4_quant_cuda.cu"], + extra_compile_args={ + "cxx": [ + "-O3", + "-std=c++17", + ], + "nvcc": [ + "-O3", + "-std=c++17", + "--expt-relaxed-constexpr", + "-Xcompiler", + "-fPIC", + ] + + [ + f'-gencode=arch=compute_{arch.replace(".", "")},code=sm_{arch.replace(".", "")}' + for arch in arch_list + ], + }, + ) + ], + cmdclass={"build_ext": BuildExtension}, +) diff --git a/miles/backends/megatron_utils/loss.py b/miles/backends/megatron_utils/loss.py index d7b72a512..5f69ffa15 100644 --- a/miles/backends/megatron_utils/loss.py +++ b/miles/backends/megatron_utils/loss.py @@ -153,7 +153,11 @@ def get_log_probs_and_entropy( max_seq_lens=max_seq_lens, ): log_prob, entropy = calculate_log_probs_and_entropy( - logits_chunk, tokens_chunk, mpu.get_tensor_model_parallel_group(), with_entropy=with_entropy + logits_chunk, + tokens_chunk, + mpu.get_tensor_model_parallel_group(), + with_entropy=with_entropy, + chunk_size=args.log_probs_chunk_size, ) log_probs_list.append(log_prob.squeeze(-1)) @@ -269,12 +273,12 @@ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) advantages = [r for r in returns] elif args.advantage_estimator == "ppo": - # TODO: optimize this old_rewards = rewards rewards = [] + kl_coef = -args.kl_coef + cp_rank = mpu.get_context_parallel_rank() for reward, k in zip(old_rewards, kl, strict=False): - k *= -args.kl_coef - cp_rank = mpu.get_context_parallel_rank() + k *= kl_coef if cp_rank == 0: k[-1] += reward rewards.append(k) @@ -445,7 +449,7 @@ def policy_loss_function( Computes current log-probabilities and entropy from model logits, then calculates PPO-style clipped policy gradient loss. For GSPO, gathers full sequences via context-parallel all-gather before computing per-sample - KL. Optionally applies TIS (Temporal Importance Sampling) correction and + KL. Optionally applies TIS (Truncated Importance Sampling) correction and adds KL loss term if configured. Args: @@ -534,6 +538,16 @@ def policy_loss_function( # Apply off-policy correction using importance sampling if enabled if args.get_mismatch_metrics or args.use_tis: + # NOTE: + # `tis_func` may apply rejection-sampling style masking (RS) and return `modified_response_masks`. + # We rebuild `sum_of_sample_mean` with those masks to correct denominators for loss/backprop. + # + # However, mismatch/TIS/RS metrics (e.g., "truncate_fraction") are often defined over the + # *pre-RS* valid tokens. If we aggregate metrics with `modified_response_masks`, the rejected + # tokens are excluded from the denominator and the metric can be artificially driven to 0. + # Keep a copy of the original reducer (based on `batch["loss_masks"]`) for metric aggregation. + sum_of_sample_mean_for_mismatch_metrics = sum_of_sample_mean + assert "rollout_log_probs" in batch, "rollout_log_probs must be provided for TIS" ois = (-ppo_kl).exp() @@ -561,10 +575,21 @@ def policy_loss_function( modified_response_masks, args.calculate_per_token_loss, args.qkv_format, - batch.get("max_seq_lens", None), + max_seq_lens, + ) + + # Determine pg_loss reducer: use custom if specified, otherwise default + if getattr(args, "custom_pg_loss_reducer_function_path", None) is not None: + custom_pg_loss_reducer_func = load_function(args.custom_pg_loss_reducer_function_path) + # Determine which loss_masks to use for pg_loss reducer + pg_loss_masks = modified_response_masks if (args.get_mismatch_metrics or args.use_tis) else batch["loss_masks"] + pg_loss_reducer = custom_pg_loss_reducer_func( + total_lengths, response_lengths, pg_loss_masks, args.calculate_per_token_loss ) + else: + pg_loss_reducer = sum_of_sample_mean - pg_loss = sum_of_sample_mean(pg_loss) + pg_loss = pg_loss_reducer(pg_loss) pg_clipfrac = sum_of_sample_mean(pg_clipfrac) ppo_kl = sum_of_sample_mean(ppo_kl) @@ -615,11 +640,13 @@ def policy_loss_function( reported_loss["kl_loss"] = kl_loss.clone().detach() if args.get_mismatch_metrics or args.use_tis: - reported_loss["ois"] = sum_of_sample_mean(ois).clone().detach() + # Aggregate mismatch/TIS/RS related metrics with the *pre-RS* masks. + # See comment above where `sum_of_sample_mean_for_mismatch_metrics` is defined. + reported_loss["ois"] = sum_of_sample_mean_for_mismatch_metrics(ois).clone().detach() # Assume all metrics are already cloned and detached for metric_key, metric_value in tis_metrics.items(): key_name = f"{metric_key}" - reported_loss[key_name] = sum_of_sample_mean(metric_value) + reported_loss[key_name] = sum_of_sample_mean_for_mismatch_metrics(metric_value) if args.use_opsm: reported_loss["opsm_clipfrac"] = opsm_clipfrac @@ -807,7 +834,7 @@ def loss_function( return ( loss, - num_tokens if args.calculate_per_token_loss else 1, + torch.tensor(num_tokens if args.calculate_per_token_loss else 1, device=logits.device), { "keys": list(log.keys()), "values": torch.tensor( diff --git a/miles/backends/megatron_utils/megatron_to_hf/deepseekv3.py b/miles/backends/megatron_utils/megatron_to_hf/deepseekv3.py index 1200b18d9..3f29f197e 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/deepseekv3.py +++ b/miles/backends/megatron_utils/megatron_to_hf/deepseekv3.py @@ -108,4 +108,22 @@ def convert_deepseekv3_to_hf(args, name, param): elif rest == "mlp.router.expert_bias": return [(f"model.layers.{layer_idx}.mlp.gate.e_score_correction_bias", param)] + mtp_layer_pattern = r"module\.module\.mtp\.layers\.(\d+)\.(.+)" + match = re.match(mtp_layer_pattern, name) + if match: + layer_idx, rest = match.groups() + layer_idx = int(layer_idx) + args.num_layers + if rest == "eh_proj.weight": + return [(f"model.layers.{layer_idx}.eh_proj.weight", param)] + elif rest == "enorm.weight": + return [(f"model.layers.{layer_idx}.enorm.weight", param)] + elif rest == "hnorm.weight": + return [(f"model.layers.{layer_idx}.hnorm.weight", param)] + elif rest == "final_layernorm.weight": + return [(f"model.layers.{layer_idx}.shared_head.norm.weight", param)] + else: + name = f"module.module.decoder.layers.{layer_idx}.{rest}" + name = name.replace("transformer_layer.", "") + return convert_deepseekv3_to_hf(args, name, param) + raise ValueError(f"Unknown parameter name: {name}") diff --git a/miles/backends/megatron_utils/megatron_to_hf/qwen3_next.py b/miles/backends/megatron_utils/megatron_to_hf/qwen3_next.py index 91e9ba88a..6cb7a2169 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/qwen3_next.py +++ b/miles/backends/megatron_utils/megatron_to_hf/qwen3_next.py @@ -62,8 +62,7 @@ def convert_qwen3_next_to_hf(args, name, param): if rest == "self_attention.linear_proj.weight": return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)] - elif rest == "self_attention.linear_qgkv.weight": - + elif rest == "self_attention.linear_qkv.weight": param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size) q_param, k_param, v_param = torch.split( param, split_size_or_sections=[2 * value_num_per_group, 1, 1], dim=1 @@ -80,7 +79,7 @@ def convert_qwen3_next_to_hf(args, name, param): (f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param), (f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param), ] - elif rest == "self_attention.linear_qgkv.bias": + elif rest == "self_attention.linear_qkv.bias": param = param.view(args.num_query_groups, -1) q_bias, k_bias, v_bias = torch.split( param, @@ -103,7 +102,7 @@ def convert_qwen3_next_to_hf(args, name, param): ] elif rest == "mlp.linear_fc2.weight": return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)] - elif rest == "self_attention.linear_qgkv.layer_norm_weight": + elif rest == "self_attention.linear_qkv.layer_norm_weight": return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)] elif rest == "mlp.linear_fc1.layer_norm_weight": return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)] diff --git a/miles/backends/megatron_utils/model.py b/miles/backends/megatron_utils/model.py index c4e182797..304bb9f3f 100644 --- a/miles/backends/megatron_utils/model.py +++ b/miles/backends/megatron_utils/model.py @@ -25,7 +25,6 @@ from miles.utils.memory_utils import clear_memory from .checkpoint import load_checkpoint, save_checkpoint -from .cp_utils import slice_with_cp from .data import DataIterator, get_batch from .loss import loss_function from .model_provider import get_model_provider_func @@ -84,9 +83,6 @@ def get_optimizer_param_scheduler(args: Namespace, optimizer: MegatronOptimizer) def setup_model_and_optimizer( args: Namespace, role: str = "actor", - no_wd_decay_cond: Callable[..., bool] | None = None, - scale_lr_cond: Callable[..., bool] | None = None, - lr_mult: float = 1.0, ) -> tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler]: """Build model(s), wrap with DDP, and construct optimizer and scheduler. @@ -119,11 +115,8 @@ def setup_model_and_optimizer( config.timers = None optimizer = get_megatron_optimizer( - config, - model, - no_wd_decay_cond, - scale_lr_cond, - lr_mult, + config=config, + model_chunks=model, use_gloo_process_groups=args.enable_gloo_process_groups, ) opt_param_scheduler = get_optimizer_param_scheduler(args, optimizer) @@ -211,7 +204,14 @@ def forward_step( # Get the batch. batch = get_batch( data_iterator, - ["tokens", "total_lengths", "response_lengths", "max_seq_lens"], + [ + "tokens", + "loss_masks", + "multimodal_train_inputs", + "total_lengths", + "response_lengths", + "max_seq_lens", + ], args.data_pad_size_multiplier, args.qkv_format, ) @@ -226,6 +226,8 @@ def forward_step( attention_mask=None, labels=None, packed_seq_params=packed_seq_params, + loss_mask=batch["full_loss_masks"], + **(batch["multimodal_train_inputs"] if batch["multimodal_train_inputs"] is not None else {}), ) return output_tensor, partial( @@ -355,6 +357,7 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p data_iterator, [ "tokens", + "multimodal_train_inputs", "packed_seq_params", "total_lengths", "response_lengths", @@ -375,36 +378,6 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p old_stage = os.environ["ROUTING_REPLAY_STAGE"] os.environ["ROUTING_REPLAY_STAGE"] = "replay_forward" - def build_loss_mask_for_mtp(batch: dict[str, object]) -> torch.Tensor | None: - tokens_tensor: torch.Tensor = batch["tokens"] - - mask_chunks: list[torch.Tensor] = [] - for total_len, response_len, resp_mask in zip( - batch["total_lengths"], batch["response_lengths"], batch["loss_masks"], strict=False - ): - assert ( - resp_mask.numel() == response_len - ), f"Unexpected loss mask size {resp_mask.numel()} (expected {response_len} or {total_len})." - prompt_len = total_len - response_len - full_mask = resp_mask.new_zeros(total_len) - full_mask[prompt_len:] = resp_mask - - mask_chunks.append(slice_with_cp(full_mask, 0.0)) - - flattened_mask = torch.cat(mask_chunks, dim=0) - seq_len = tokens_tensor.size(-1) - assert ( - flattened_mask.numel() <= seq_len - ), f"MTP loss mask ({flattened_mask.numel()}) exceeds token length ({seq_len})." - - # token tensor may be padded by 128, so pad loss mask to the same length - loss_mask_tensor = flattened_mask.new_zeros(seq_len) - loss_mask_tensor[: flattened_mask.numel()] = flattened_mask - return loss_mask_tensor.unsqueeze(0) - - loss_mask = None - mtp_kwargs = None - if return_schedule_plan: assert not args.enable_mtp_training, "MTP training should not be enabled when using combined 1f1b" output_tensor = model.build_schedule_plan( @@ -413,29 +386,25 @@ def build_loss_mask_for_mtp(batch: dict[str, object]) -> torch.Tensor | None: attention_mask=None, labels=None, packed_seq_params=batch["packed_seq_params"], + loss_mask=batch["full_loss_masks"], ) else: - # If enabling MTP training: trigger MTP loss inside Megatron while returning logits - # for the target model's loss. + forward_kwargs = { + "input_ids": batch["tokens"], + "position_ids": None, + "attention_mask": None, + "labels": None, + "packed_seq_params": batch["packed_seq_params"], + "loss_mask": batch["full_loss_masks"], + } + if args.enable_mtp_training: - loss_mask = build_loss_mask_for_mtp(batch) - assert ( - loss_mask.shape == batch["tokens"].shape - ), f"loss_mask shape {loss_mask.shape} mismatches token shape {batch['tokens'].shape}" - mtp_kwargs = { - # We have to set labels to tokens for MTP training, to point out samples to train. - "mtp_labels": batch["tokens"], - } - - output_tensor = model( - input_ids=batch["tokens"], - position_ids=None, - attention_mask=None, - labels=None, - packed_seq_params=batch["packed_seq_params"], - loss_mask=loss_mask, - **(dict(mtp_kwargs=mtp_kwargs) if mtp_kwargs is not None else {}), - ) + forward_kwargs["mtp_kwargs"] = {"mtp_labels": batch["tokens"]} + + if batch["multimodal_train_inputs"] is not None: + forward_kwargs.update(batch["multimodal_train_inputs"]) + + output_tensor = model(**forward_kwargs) if os.environ.get("ENABLE_ROUTING_REPLAY", "0") == "1": os.environ["ROUTING_REPLAY_STAGE"] = old_stage @@ -467,6 +436,13 @@ def build_loss_mask_for_mtp(batch: dict[str, object]) -> torch.Tensor | None: else: valid_step = not (math.isnan(grad_norm) or math.isinf(grad_norm)) + # CI check: verify only MTP parameters have non-zero gradients when truncation happens + # This check must happen before optimizer.step() as gradients may be modified during step + if args.ci_test and args.enable_mtp_training: + from miles.backends.megatron_utils.ci_utils import check_mtp_only_grad + + check_mtp_only_grad(model, step_id) + if valid_step: # Update parameters. update_successful, grad_norm, num_zeros_in_grad = optimizer.step() @@ -506,6 +482,16 @@ def should_disable_forward_pre_hook(args: Namespace) -> bool: return args.use_distributed_optimizer and args.overlap_param_gather +def finalize_model_grads_with_empty_cache(*args, **kwargs): + # trigger empty cache when there are less than 10% free memory before the final reduce scatter. + # TODO: this is an ad-hoc method and we should figure out why the oom happens in the first place. + device = torch.cuda.current_device() + free, total = torch.cuda.mem_get_info(device) + if free / total < 0.1: + clear_memory() + return finalize_model_grads(*args, **kwargs) + + def train( rollout_id: int, model: Sequence[DDP], @@ -556,7 +542,7 @@ def train( config.param_sync_func = [model_chunk.start_param_sync for model_chunk in model] if len(model) == 1: config.param_sync_func = config.param_sync_func[0] - config.finalize_model_grads_func = finalize_model_grads + config.finalize_model_grads_func = finalize_model_grads_with_empty_cache pre_hook_enabled = False @@ -619,6 +605,12 @@ def train( mtp_losses = (tracker["values"] * mtp_loss_scale).item() MTPLossLoggingHelper.clean_loss_in_tracker() + # CI check: verify MTP loss is within expected bounds + if args.ci_test: + from miles.backends.megatron_utils.ci_utils import check_mtp_loss + + check_mtp_loss(mtp_losses) + # per train step log. if ( mpu.get_data_parallel_rank(with_context_parallel=True) == 0 @@ -740,4 +732,6 @@ def initialize_model_and_optimizer( ) clear_memory() + opt_param_scheduler.step(increment=iteration * args.global_batch_size) + return model, optimizer, opt_param_scheduler, iteration diff --git a/miles/backends/megatron_utils/model_provider.py b/miles/backends/megatron_utils/model_provider.py index 5b7b3dd74..7834f1101 100644 --- a/miles/backends/megatron_utils/model_provider.py +++ b/miles/backends/megatron_utils/model_provider.py @@ -16,6 +16,8 @@ from megatron.core.transformer.transformer_config import TransformerConfig from megatron.training.arguments import core_transformer_config_from_args +from miles.utils.misc import load_function + # Adapt from https://github.com/volcengine/verl/blob/c3b20575d2bc815fcccd84bddb4c0401fc4b632b/verl/models/llama/megatron/layers/parallel_linear.py#L82 class LinearForLastLayer(torch.nn.Linear): @@ -53,6 +55,42 @@ def get_model_provider_func( args: argparse.Namespace, role: Literal["actor", "critic"] = "actor", ): + # Support custom model provider path (similar to --custom-rm-path for reward models) + if getattr(args, "custom_model_provider_path", None): + + def wrapped_model_provider( + pre_process: bool = True, post_process: bool = True, vp_stage: int | None = None + ) -> GPTModel: + custom_model_provider = load_function(args.custom_model_provider_path) + # Check if the custom provider supports vp_stage parameter + has_vp_stage = "vp_stage" in inspect.signature(custom_model_provider).parameters + if has_vp_stage: + model = custom_model_provider(pre_process=pre_process, post_process=post_process, vp_stage=vp_stage) + else: + model = custom_model_provider(pre_process=pre_process, post_process=post_process) + # Apply critic output layer if needed + if post_process and role == "critic": + model.output_layer = LinearForLastLayer( + input_size=model.config.hidden_size, output_size=1, config=model.config + ) + return model + + return wrapped_model_provider + + if args.megatron_to_hf_mode == "bridge": + from megatron.bridge import AutoBridge + + bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True) + provider = bridge.to_megatron_provider(load_weights=False) + # TODO: we should not manually set this... + provider.tensor_model_parallel_size = args.tensor_model_parallel_size + provider.pipeline_model_parallel_size = args.pipeline_model_parallel_size + provider.expert_model_parallel_size = args.expert_model_parallel_size + provider.expert_tensor_parallel_size = args.expert_tensor_parallel_size + provider.sequence_parallel = args.sequence_parallel + provider.finalize() + return provider.provide + def model_provider(pre_process: bool = True, post_process: bool = True, vp_stage: int | None = None) -> GPTModel: """Builds the model. diff --git a/miles/backends/megatron_utils/update_weight/hf_weight_iterator_bridge.py b/miles/backends/megatron_utils/update_weight/hf_weight_iterator_bridge.py index a88d18b36..7e0a4817e 100644 --- a/miles/backends/megatron_utils/update_weight/hf_weight_iterator_bridge.py +++ b/miles/backends/megatron_utils/update_weight/hf_weight_iterator_bridge.py @@ -13,9 +13,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from megatron.bridge import AutoBridge + import miles_plugins.megatron_bridge # noqa: F401 - self._bridge = AutoBridge.from_hf_pretrained(self.args.hf_checkpoint) + self._bridge = AutoBridge.from_hf_pretrained(self.args.hf_checkpoint, trust_remote_code=True) def get_hf_weight_chunks(self, megatron_local_weights): # TODO support quantization (e.g. modify megatron-bridge to provide megatron param name) diff --git a/miles/backends/sglang_utils/sglang_engine.py b/miles/backends/sglang_utils/sglang_engine.py index 2e1afe625..479197b9f 100644 --- a/miles/backends/sglang_utils/sglang_engine.py +++ b/miles/backends/sglang_utils/sglang_engine.py @@ -1,7 +1,9 @@ import dataclasses +import ipaddress import logging import multiprocessing import time +from urllib.parse import quote import requests import sglang_router @@ -91,28 +93,39 @@ def __init__(self, args, rank: int, worker_type: str = "regular"): self.rank = rank self.worker_type = worker_type - def init(self, dist_init_addr, port, nccl_port, host=None): + def init(self, dist_init_addr, port, nccl_port, host=None, disaggregation_bootstrap_port=None): self.router_ip = self.args.sglang_router_ip self.router_port = self.args.sglang_router_port host = host or get_host_info()[1] - # support ipv6 address - if ":" in host and not host.startswith("["): - host = f"[{host}]" + def _format_v6_uri(addr): + if not addr or addr.startswith("["): + return addr + try: + if ipaddress.ip_address(addr).version == 6: + return f"[{addr}]" + except ValueError: + pass + return addr - # dist_init_addr may be 2605:...:10163, should split port - *addr_parts, port_str = dist_init_addr.split(":") - ipv6_addr = ":".join(addr_parts) - if ":" in ipv6_addr and not ipv6_addr.startswith("["): - dist_init_addr = f"[{ipv6_addr}]:{port_str}" + host = _format_v6_uri(host) + ip_part, port_part = dist_init_addr.rsplit(":", 1) + dist_init_addr = f"{_format_v6_uri(ip_part)}:{port_part}" server_args_dict, external_engine_need_check_fields = _compute_server_args( - self.args, self.rank, dist_init_addr, nccl_port, host, port, self.worker_type + self.args, + self.rank, + dist_init_addr, + nccl_port, + host, + port, + self.worker_type, + disaggregation_bootstrap_port, ) self.node_rank = server_args_dict["node_rank"] - self.server_host = server_args_dict["host"] + self.server_host = server_args_dict["host"] # with [] if ipv6 self.server_port = server_args_dict["port"] if self.args.rollout_external: @@ -157,12 +170,15 @@ def _init_normal(self, server_args_dict): f"http://{self.router_ip}:{self.router_port}/add_worker?url=http://{self.server_host}:{self.server_port}" ) else: + payload = { + "url": f"http://{self.server_host}:{self.server_port}", + "worker_type": self.worker_type, + } + if self.worker_type == "prefill": + payload["bootstrap_port"] = server_args_dict["disaggregation_bootstrap_port"] response = requests.post( f"http://{self.router_ip}:{self.router_port}/workers", - json={ - "url": f"http://{self.server_host}:{self.server_port}", - "worker_type": self.worker_type, - }, + json=payload, ) response.raise_for_status() @@ -261,13 +277,31 @@ def shutdown(self): logger.info(f"Shutdown engine {self.server_host}:{self.server_port}...") if self.node_rank == 0: worker_url = f"http://{self.server_host}:{self.server_port}" + response = None if parse(sglang_router.__version__) <= parse("0.2.1") or self.args.use_miles_router: response = requests.post( f"http://{self.router_ip}:{self.router_port}/remove_worker?url=http://{self.server_host}:{self.server_port}" ) - else: + elif parse(sglang_router.__version__) < parse("0.3.0"): + worker_url = quote(worker_url, safe="") response = requests.delete(f"http://{self.router_ip}:{self.router_port}/workers/{worker_url}") - response.raise_for_status() + else: + try: + all_workers = requests.get(f"http://{self.router_ip}:{self.router_port}/workers").json()["workers"] + for worker in all_workers: + if worker["url"] == worker_url: + worker_id = worker["id"] + response = requests.delete( + f"http://{self.router_ip}:{self.router_port}/workers/{worker_id}" + ) + break + else: + logger.warning(f"Worker {worker_url} not found in router during shutdown.") + except Exception as e: + logger.warning(f"Failed to fetch workers list or remove worker: {e}") + + if response is not None: + response.raise_for_status() kill_process_tree(self.process.pid) def get_weight_version(self): @@ -292,7 +326,7 @@ def resume_memory_occupation(self, tags: list[str] = None): ) def check_weights(self, action: str): - return self._make_request("check_weights", {"action": action}) + return self._make_request("weights_checker", {"action": action}) def init_weights_update_group(self, master_address, master_port, rank_offset, world_size, group_name, backend): return self._make_request( @@ -381,7 +415,16 @@ def stop_profile(self): return response -def _compute_server_args(args, rank, dist_init_addr, nccl_port, host, port, worker_type: str = "regular"): +def _compute_server_args( + args, + rank, + dist_init_addr, + nccl_port, + host, + port, + worker_type: str = "regular", + disaggregation_bootstrap_port: int | None = None, +): nnodes = max(1, args.rollout_num_gpus_per_engine // args.num_gpus_per_node) node_rank = rank % nnodes kwargs = { @@ -406,11 +449,17 @@ def _compute_server_args(args, rank, dist_init_addr, nccl_port, host, port, work "ep_size": args.sglang_ep_size, # always skip warmup to prevent warmup timeout. "skip_server_warmup": True, + # always enable draft weights cpu backup so that we run training without mtp weights. + "enable_draft_weights_cpu_backup": True, } if worker_type == "prefill": kwargs["disaggregation_mode"] = "prefill" kwargs["load_balance_method"] = "round_robin" + assert ( + disaggregation_bootstrap_port is not None + ), "disaggregation_bootstrap_port must be set for prefill worker" + kwargs["disaggregation_bootstrap_port"] = disaggregation_bootstrap_port elif worker_type == "decode": kwargs["disaggregation_mode"] = "decode" kwargs["prefill_round_robin_balance"] = True @@ -419,11 +468,12 @@ def _compute_server_args(args, rank, dist_init_addr, nccl_port, host, port, work kwargs["enable_return_routed_experts"] = True if args.fp16: kwargs["dtype"] = "float16" - external_engine_need_check_fields = [k for k in kwargs.keys() if k not in _EXTERNAL_ENGINE_SKIP_CHECK_FIELDS] unused_keys = set(kwargs.keys()) for attr in dataclasses.fields(ServerArgs): + if worker_type == "decode" and attr.name == "enable_hierarchical_cache": + continue if hasattr(args, f"sglang_{attr.name}") and attr.name not in kwargs: kwargs[attr.name] = getattr(args, f"sglang_{attr.name}") unused_keys.discard(attr.name) diff --git a/miles/ray/actor_group.py b/miles/ray/actor_group.py index 21a26abbe..30a82b891 100644 --- a/miles/ray/actor_group.py +++ b/miles/ray/actor_group.py @@ -72,12 +72,13 @@ def _allocate_gpus_for_actor(self, pg, num_gpus_per_actor): env_vars["TMS_INIT_ENABLE"] = "1" env_vars["TMS_INIT_ENABLE_CPU_BACKUP"] = "1" - if self.args.use_routing_replay: + # We cannot do routing replay for critic. + if self.args.use_routing_replay and self.role == "actor": env_vars["ENABLE_ROUTING_REPLAY"] = "1" backend = self.args.train_backend if backend == "megatron": - from miles.backends.megatron_utils import MegatronTrainRayActor + from miles.backends.megatron_utils.actor import MegatronTrainRayActor actor_impl = MegatronTrainRayActor @@ -115,9 +116,9 @@ def async_train(self, rollout_id, rollout_data_ref): """Do one rollout training""" return [actor.train.remote(rollout_id, rollout_data_ref) for actor in self._actor_handlers] - def save_model(self, step_id): - """Save actor model on rank 0.""" - return ray.get([actor.save_model.remote(step_id) for actor in self._actor_handlers]) + def save_model(self, rollout_id, force_sync=False): + """Save actor model""" + return ray.get([actor.save_model.remote(rollout_id, force_sync=force_sync) for actor in self._actor_handlers]) def update_weights(self): """Broadcast weights from rank 0 to all other ranks.""" diff --git a/miles/ray/rollout.py b/miles/ray/rollout.py index 9ee0fbb8a..40338f7e5 100644 --- a/miles/ray/rollout.py +++ b/miles/ray/rollout.py @@ -1,9 +1,7 @@ import logging import multiprocessing -import os import random import time -from glob import glob from pathlib import Path from typing import Any @@ -58,6 +56,11 @@ def __init__(self, args, pg): self.custom_reward_post_process_func = None if self.args.custom_reward_post_process_path is not None: self.custom_reward_post_process_func = load_function(self.args.custom_reward_post_process_path) + self.custom_convert_samples_to_train_data_func = None + if self.args.custom_convert_samples_to_train_data_path is not None: + self.custom_convert_samples_to_train_data_func = load_function( + self.args.custom_convert_samples_to_train_data_path + ) logger.info(f"import {self.args.rollout_function_path} as generate_rollout function.") logger.info(f"import {self.args.eval_function_path} as eval_generate_rollout function.") @@ -218,6 +221,9 @@ def _convert_samples_to_train_data(self, samples: list[Sample] | list[list[Sampl """ Convert inference generated samples to training data. """ + if self.custom_convert_samples_to_train_data_func is not None: + return self.custom_convert_samples_to_train_data_func(self.args, samples) + raw_rewards, rewards = self._post_process_rewards(samples) assert len(raw_rewards) == len(samples) @@ -268,8 +274,8 @@ def _convert_samples_to_train_data(self, samples: list[Sample] | list[list[Sampl if samples[0].train_metadata is not None: train_data["metadata"] = [sample.train_metadata for sample in samples] - if samples[0].multimodal_inputs is not None: - train_data["multimodal_inputs"] = [sample.multimodal_inputs for sample in samples] + if samples[0].multimodal_train_inputs is not None: + train_data["multimodal_train_inputs"] = [sample.multimodal_train_inputs for sample in samples] if "teacher_log_probs" in samples[0].__dict__: train_data["teacher_log_probs"] = [sample.teacher_log_probs for sample in samples] @@ -302,7 +308,7 @@ def _split_train_data_by_dp(self, data, dp_size): rollout_data["partition"] = partition for key in [ "tokens", - "multimodal_inputs", + "multimodal_train_inputs", "response_lengths", "rewards", "truncated", @@ -332,7 +338,7 @@ def _split_train_data_by_dp(self, data, dp_size): def init_rollout_engines(args, pg, all_rollout_engines): if args.debug_train_only: - return 0, None + return 0 num_gpu_per_engine = min(args.rollout_num_gpus_per_engine, args.num_gpus_per_node) num_engines = args.rollout_num_gpus // num_gpu_per_engine @@ -369,27 +375,9 @@ def init_rollout_engines(args, pg, all_rollout_engines): "SGLANG_MEMORY_SAVER_CUDA_GRAPH": "true", "SGLANG_BATCH_INVARIANT_OPS_ENABLE_MM_FALLBACK_VARIANT": "true", "SGLANG_ENABLE_HEALTH_ENDPOINT_GENERATION": "false", + "SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_IDLE": "false", } - # TODO: currently the amem position is hardcoded, change to a better way later. - # note that amem does not work with update weights from distributed. - if ( - args.offload_rollout - and args.actor_num_nodes * args.actor_num_gpus_per_node >= args.rollout_num_gpus - and len(glob("/usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libamem_nccl.so*")) > 0 - ): - logger.info("Enable AMEM for rollout engine.") - ld_library_path = ( - os.environ.get("LD_LIBRARY_PATH", "") + ":/usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib" - ) - env_vars |= { - "LD_LIBRARY_PATH": ld_library_path, - "NCCL_CUMEM_ENABLE": "1", - "AMEM_ENABLE": "1", - "AMEM_GROUPID": "0", - "GMM_LOG": "2", - } - worker_type = "regular" if args.prefill_num_servers is not None: if i < prefill_num_servers: @@ -412,7 +400,7 @@ def init_rollout_engines(args, pg, all_rollout_engines): num_new_engines = len(rollout_engines) if num_new_engines == 0: - return num_new_engines, None + return num_new_engines if args.rollout_external: addr_and_ports = _allocate_rollout_engine_addr_and_ports_external(args=args, rollout_engines=rollout_engines) @@ -456,6 +444,12 @@ def _allocate_rollout_engine_addr_and_ports_normal(*, args, num_engines, rollout ) addr_and_ports = [{} for _ in range(num_engines)] + # Calculate prefill limit to identify prefill engines + prefill_limit = 0 + if args.prefill_num_servers is not None: + num_gpu_per_engine = min(args.rollout_num_gpus_per_engine, args.num_gpus_per_node) + prefill_limit = args.prefill_num_servers * args.rollout_num_gpus_per_engine // num_gpu_per_engine + visited_nodes = set() for rank, engine in rollout_engines: if rank // num_engines_per_node in visited_nodes: @@ -490,20 +484,24 @@ def addr(): get_addr, get_port = get_addr_and_ports(engine) for i in range(num_engines_on_this_node): - addr_and_ports[rank + i]["host"] = get_addr() - addr_and_ports[rank + i]["port"] = get_port() - addr_and_ports[rank + i]["nccl_port"] = get_port() + current_rank = rank + i + addr_and_ports[current_rank]["host"] = get_addr() + addr_and_ports[current_rank]["port"] = get_port() + addr_and_ports[current_rank]["nccl_port"] = get_port() + + if args.prefill_num_servers is not None and current_rank < prefill_limit: + addr_and_ports[current_rank]["disaggregation_bootstrap_port"] = get_port() if args.rollout_num_gpus_per_engine > args.num_gpus_per_node: num_node_per_engine = args.rollout_num_gpus_per_engine // args.num_gpus_per_node if rank % num_node_per_engine == 0: # this is the first node in the engine, we need to allocate the dist_init_addr port - dist_init_addr = f"{get_addr()}:{get_port(6 + args.sglang_dp_size)}" + dist_init_addr = f"{get_addr()}:{get_port(30 + args.sglang_dp_size)}" for i in range(num_node_per_engine): addr_and_ports[rank + i]["dist_init_addr"] = dist_init_addr else: for i in range(num_engines_on_this_node): - addr_and_ports[rank + i]["dist_init_addr"] = f"{get_addr()}:{get_port(6 + args.sglang_dp_size)}" + addr_and_ports[rank + i]["dist_init_addr"] = f"{get_addr()}:{get_port(30 + args.sglang_dp_size)}" for i, _ in rollout_engines: for key in ["port", "nccl_port", "dist_init_addr"]: @@ -513,7 +511,7 @@ def addr(): return addr_and_ports -def _start_router(args, prefill_and_decode_urls=None): +def _start_router(args): """start sgl router and miles router""" if args.sglang_router_ip is not None: return @@ -560,6 +558,11 @@ def _start_router(args, prefill_and_decode_urls=None): def _log_eval_rollout_data(rollout_id, args, data, extra_metrics: dict[str, Any] | None = None): + if args.custom_eval_rollout_log_function_path is not None: + custom_log_func = load_function(args.custom_eval_rollout_log_function_path) + if custom_log_func(rollout_id, args, data, extra_metrics): + return + log_dict = extra_metrics or {} for key in data.keys(): rewards = data[key]["rewards"] @@ -588,15 +591,28 @@ def _log_eval_rollout_data(rollout_id, args, data, extra_metrics: dict[str, Any] def _log_rollout_data(rollout_id, args, samples, rollout_extra_metrics, rollout_time): + if args.custom_rollout_log_function_path is not None: + custom_log_func = load_function(args.custom_rollout_log_function_path) + if custom_log_func(rollout_id, args, samples, rollout_extra_metrics, rollout_time): + return + if args.load_debug_rollout_data: return log_dict = {**(rollout_extra_metrics or {})} - response_lengths = [sample.effective_response_length for sample in samples] + response_lengths = [sample.response_length for sample in samples] log_dict["perf/rollout_time"] = rollout_time if args.rollout_num_gpus: log_dict["perf/tokens_per_gpu_per_sec"] = sum(response_lengths) / rollout_time / args.rollout_num_gpus log_dict["perf/longest_sample_tokens_per_sec"] = max(response_lengths) / rollout_time + + response_lengths = [sample.effective_response_length for sample in samples] + if args.rollout_num_gpus: + log_dict["perf/effective_tokens_per_gpu_per_sec"] = ( + sum(response_lengths) / rollout_time / args.rollout_num_gpus + ) + log_dict["perf/longest_effective_sample_tokens_per_sec"] = max(response_lengths) / rollout_time + log_dict |= dict_add_prefix(compute_metrics_from_samples(args, samples), "rollout/") logger.info(f"perf {rollout_id}: {log_dict}") step = compute_rollout_step(args, rollout_id) diff --git a/miles/ray/rollout_data_source.py b/miles/ray/rollout_data_source.py deleted file mode 100644 index c9df08f4f..000000000 --- a/miles/ray/rollout_data_source.py +++ /dev/null @@ -1,186 +0,0 @@ -import copy -import logging -import os -from pathlib import Path - -import torch -from transformers import AutoTokenizer - -from miles.utils.data import Dataset -from miles.utils.misc import load_function -from miles.utils.types import Sample - -logger = logging.getLogger(__name__) - - -# TODO may further refactor data-loading part later -class RolloutDataSource: - def __init__(self, args): - self.args = args - - self.epoch_id = 0 - self.sample_group_index = 0 - self.sample_index = 0 - self.sample_offset = 0 - # TODO remove this - self.metadata = {} - - if args.rollout_global_dataset: - tokenizer = AutoTokenizer.from_pretrained(args.hf_checkpoint, trust_remote_code=True) - - # TODO move (during the refactor) - if (d := args.dump_details) is not None: - tokenizer.save_pretrained(Path(d) / "tokenizer") - - self.dataset = Dataset( - args.prompt_data, - tokenizer=tokenizer, - max_length=args.rollout_max_prompt_len, - prompt_key=args.input_key, - label_key=args.label_key, - metadata_key=args.metadata_key, - tool_key=args.tool_key, - apply_chat_template=args.apply_chat_template, - apply_chat_template_kwargs=args.apply_chat_template_kwargs, - seed=args.rollout_seed, - ) - if self.args.rollout_shuffle: - self.dataset.shuffle(self.epoch_id) - else: - self.dataset = None - - def get_samples(self, num_samples): - # TODO further improve code - if self.dataset is not None: - if self.sample_offset + num_samples <= len(self.dataset): - prompt_samples = self.dataset.samples[self.sample_offset : self.sample_offset + num_samples] - self.sample_offset += num_samples - else: - prompt_samples = self.dataset.samples[self.sample_offset :] - num_samples -= len(prompt_samples) - self.epoch_id += 1 - if self.args.rollout_shuffle: - self.dataset.shuffle(self.epoch_id) - prompt_samples += self.dataset.samples[:num_samples] - self.sample_offset = num_samples - else: - prompt_samples = [Sample() for _ in range(num_samples)] - - samples = [] - for prompt_sample in prompt_samples: - group = [] - for _ in range(self.args.n_samples_per_prompt): - sample = copy.deepcopy(prompt_sample) - sample.group_index = self.sample_group_index - sample.index = self.sample_index - self.sample_index += 1 - group.append(sample) - self.sample_group_index += 1 - samples.append(group) - return samples - - def add_samples(self, samples: list[list[Sample]]): - raise RuntimeError(f"Cannot add samples to {self.__class__.__name__}. This is a read-only data source.") - - def save(self, rollout_id): - if not self.args.rollout_global_dataset: - return - - state_dict = { - "sample_offset": self.sample_offset, - "epoch_id": self.epoch_id, - "sample_group_index": self.sample_group_index, - "sample_index": self.sample_index, - "metadata": self.metadata, - } - path = os.path.join(self.args.save, f"rollout/global_dataset_state_dict_{rollout_id}.pt") - os.makedirs(os.path.dirname(path), exist_ok=True) - torch.save(state_dict, path) - - def load(self, rollout_id=None): - if not self.args.rollout_global_dataset: - return - - if self.args.load is None: - return - - path = os.path.join(self.args.load, f"rollout/global_dataset_state_dict_{rollout_id}.pt") - if not os.path.exists(path): - logger.info(f"Checkpoint {path} does not exist.") - return - - logger.info(f"load metadata from {path}") - logger.info(f"load metadata: {self.metadata}") - state_dict = torch.load(path) - self.sample_offset = state_dict.get("sample_offset", 0) - self.epoch_id = state_dict.get("epoch_id", 0) - self.sample_group_index = state_dict.get("sample_group_index", 0) - self.sample_index = state_dict.get("sample_index", 0) - self.metadata = state_dict.get("metadata", {}) - - if self.args.rollout_global_dataset and self.args.rollout_shuffle: - self.dataset.shuffle(self.epoch_id) - - -class RolloutDataSourceWithBuffer(RolloutDataSource): - def __init__(self, args): - super().__init__(args) - self.buffer = [] - if self.args.buffer_filter_path is None: - self.buffer_filter = pop_first - else: - self.buffer_filter = load_function(self.args.buffer_filter_path) - - def get_samples(self, num_samples: int) -> list[list[Sample]]: - """ - Return num_samples samples - """ - - samples = self._get_samples_from_buffer(num_samples) - num_samples -= len(samples) - - if num_samples == 0: - return samples - - samples += super().get_samples(num_samples=num_samples) - return samples - - def _get_samples_from_buffer(self, num_samples: int) -> list[list[Sample]]: - if len(self.buffer) == 0 or num_samples == 0: - return [] - - samples = self.buffer_filter(self.args, None, self.buffer, num_samples) - return samples - - def add_samples(self, samples: list[list[Sample]]): - """ - Add a sample group to buffer. - """ - if not samples: - return - assert isinstance(samples, list), f"samples must be a list, got {type(samples)}" - assert isinstance(samples[0], list), f"the elements of samples must be list, got {type(samples[0])}" - for i in range(0, len(samples)): - assert ( - len(samples[i]) == self.args.n_samples_per_prompt - ), f"the length of the elements of samples must be equal to n_samples_per_prompt, got {len(samples[i])} != {self.args.n_samples_per_prompt}" - group = samples[i] # type: ignore - self.buffer.append(group) - - # TODO remove - def update_metadata(self, metadata: dict): - self.metadata.update(metadata) - - # TODO remove - def get_metadata(self): - return self.metadata - - def get_buffer_length(self): - return len(self.buffer) - - -def pop_first(args, rollout_id, buffer: list[list[Sample]], num_samples: int) -> list[list[Sample]]: - num_to_pop = min(len(buffer), num_samples) - samples = buffer[:num_to_pop] - del buffer[:num_to_pop] - return samples diff --git a/miles/ray/train_actor.py b/miles/ray/train_actor.py index 3d3923e9c..81e3fae00 100644 --- a/miles/ray/train_actor.py +++ b/miles/ray/train_actor.py @@ -113,7 +113,7 @@ def train(self, rollout_id, rollout_data_ref): raise NotImplementedError @abc.abstractmethod - def save_model(self, iteration): + def save_model(self, rollout_id, force_sync=False): raise NotImplementedError @abc.abstractmethod diff --git a/miles/rollout/rm_hub/math_utils.py b/miles/rollout/rm_hub/math_utils.py index cab786797..e67253891 100644 --- a/miles/rollout/rm_hub/math_utils.py +++ b/miles/rollout/rm_hub/math_utils.py @@ -220,7 +220,7 @@ def _str_is_int(x: str) -> bool: return False -def _str_to_int(x: str) -> bool: +def _str_to_int(x: str) -> int: x = x.replace(",", "") x = float(x) return int(x) diff --git a/miles/rollout/sft_rollout.py b/miles/rollout/sft_rollout.py index 1e8a96c85..8669e380f 100644 --- a/miles/rollout/sft_rollout.py +++ b/miles/rollout/sft_rollout.py @@ -1,8 +1,7 @@ import logging -from transformers import AutoTokenizer - from miles.utils.mask_utils import MultiTurnLossMaskGenerator +from miles.utils.processing_utils import load_processor, load_tokenizer __all__ = ["generate_rollout"] @@ -10,6 +9,7 @@ TOKENIZER = None +PROCESSOR = None MASK_GENERATOR = None SAMPLE_PRINTED = False @@ -29,9 +29,12 @@ def generate_rollout(args, rollout_id, data_buffer, evaluation=False): assert not evaluation assert args.rollout_global_dataset - global TOKENIZER, MASK_GENERATOR, SAMPLE_PRINTED + global TOKENIZER, PROCESSOR, MASK_GENERATOR, SAMPLE_PRINTED if TOKENIZER is None: - TOKENIZER = AutoTokenizer.from_pretrained(args.hf_checkpoint, trust_remote_code=True) + TOKENIZER = load_tokenizer(args.hf_checkpoint, trust_remote_code=True) + + if PROCESSOR is None: + PROCESSOR = load_processor(args.hf_checkpoint, trust_remote_code=True) if MASK_GENERATOR is None: MASK_GENERATOR = MultiTurnLossMaskGenerator(TOKENIZER, tokenizer_type=args.loss_mask_type) @@ -41,7 +44,10 @@ def generate_rollout(args, rollout_id, data_buffer, evaluation=False): for i, sample in enumerate(samples): (sample,) = sample messages = sample.prompt - token_ids, loss_mask = MASK_GENERATOR.get_loss_mask(messages) + tools = sample.metadata.get("tools", None) + + token_ids, loss_mask = MASK_GENERATOR.get_loss_mask(messages, tools=tools) + response_length = MASK_GENERATOR.get_response_lengths([loss_mask])[0] sample.tokens = token_ids diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 2e33542a5..2f3734657 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -4,9 +4,11 @@ from argparse import Namespace from collections import defaultdict from collections.abc import Callable +from contextlib import contextmanager from typing import Any import numpy as np +import pybase64 import sglang_router from packaging.version import parse from tqdm import tqdm @@ -19,12 +21,7 @@ from miles.utils.http_utils import get, post from miles.utils.mask_utils import get_response_lengths from miles.utils.misc import SingletonMeta, load_function -from miles.utils.processing_utils import ( - encode_image_for_rollout_engine, - load_processor, - load_tokenizer, - prepare_model_inputs, -) +from miles.utils.processing_utils import encode_image_for_rollout_engine, load_processor, load_tokenizer from miles.utils.types import Sample from .rm_hub import async_rm, batched_async_rm @@ -64,8 +61,24 @@ def __init__(self, args: Namespace) -> None: sampling_seed_base = args.rollout_seed self.group_sampling_seeds = [sampling_seed_base + i for i in range(args.n_samples_per_prompt)] + # dp rank balancing + self.dp_counts = [0] * (args.sglang_dp_size or 1) + self.dp_rank = 0 + self.reset() + @contextmanager + def dp_rank_context(self): + candidates = [i for i, count in enumerate(self.dp_counts) if count == min(self.dp_counts)] + dp_rank = int(np.random.choice(candidates)) + self.dp_counts[dp_rank] += 1 + self.dp_rank = dp_rank + try: + yield dp_rank + finally: + self.dp_counts[dp_rank] -= 1 + assert self.dp_counts[dp_rank] >= 0 + def reset(self) -> None: self.remaining_batch_size = 0 self.pendings = set() @@ -89,6 +102,9 @@ def submit_generate_tasks(self, samples: list[list[Sample]]) -> None: async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, Any]) -> Sample: """Generate using traditional SGLang router with token-based workflow""" + if args.ci_test: + assert isinstance(sample.prompt, str) + state = GenerateState(args) url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" @@ -96,17 +112,14 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A sample.status == Sample.Status.PENDING or sample.status == Sample.Status.ABORTED ), f"Sample status is {sample.status}" - prompt_ids, extra_info = prepare_model_inputs( - sample.prompt, - state.tokenizer, - state.processor, - sample.metadata, - args.apply_chat_template_kwargs, - ) - - image_data = extra_info.get("images", []) - video_data = extra_info.get("videos", []) - multimodal_inputs = extra_info.get("multimodal_inputs", None) + if state.processor: + processor_output = state.processor(text=sample.prompt, **sample.multimodal_inputs) + prompt_ids = processor_output["input_ids"][0] + sample.multimodal_train_inputs = { + k: v for k, v in processor_output.items() if k not in ["input_ids", "attention_mask"] + } or None + else: + prompt_ids = state.tokenizer.encode(sample.prompt, add_special_tokens=False) if len(sample.response) > 0: sampling_params["max_new_tokens"] -= len(sample.tokens) - len(prompt_ids) @@ -127,12 +140,9 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A if args.use_rollout_routing_replay: payload["return_routed_experts"] = True - if image_data: + if sample.multimodal_inputs and sample.multimodal_inputs["images"]: + image_data = sample.multimodal_inputs["images"] payload["image_data"] = [encode_image_for_rollout_engine(image) for image in image_data] - sample.multimodal_inputs = multimodal_inputs - - if video_data: - raise NotImplementedError("Video data is not supported yet") # Use existing tokens for multi-turn or tokenize the new prompt if len(sample.response) > 0: @@ -147,7 +157,7 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A # Extract new response tokens if args.use_miles_router and "RadixTreeMiddleware" in args.miles_router_middleware_paths: - assert not args.partial_rollout, "Currently parital rollout is not suppurted when using miles router" + assert not args.partial_rollout, "Currently partial rollout is not supported when using miles router" retrieve_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/retrieve_from_text" retrieve_payload = {"text": sample.prompt + output["text"], "return_logp": True} retrieve_output = await post(retrieve_url, retrieve_payload) @@ -185,8 +195,14 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A sample.weight_versions.append(output["meta_info"]["weight_version"]) if "routed_experts" in output["meta_info"]: - assert len(output["meta_info"]["routed_experts"]) == len(sample.tokens) - 1 - sample.rollout_routed_experts = np.array(output["meta_info"]["routed_experts"]) + sample.rollout_routed_experts = np.frombuffer( + pybase64.b64decode(output["meta_info"]["routed_experts"].encode("ascii")), + dtype=np.int32, + ).reshape( + len(sample.tokens) - 1, + args.num_layers, + args.moe_router_topk, + ) match output["meta_info"]["finish_reason"]["type"]: case "length": @@ -205,6 +221,10 @@ async def generate_and_rm( sampling_params: dict[str, Any], evaluation: bool = False, ) -> Sample | list[Sample]: + # mask previous off-policy generation for partial rollout + if args.partial_rollout and args.mask_offpolicy_in_partial_rollout and sample.response_length > 0: + sample.loss_mask = [0] * sample.response_length + # For samples with existing response, check if they're complete if sample.status == Sample.Status.COMPLETED or sample.status == Sample.Status.TRUNCATED: assert sample.response is not None @@ -220,11 +240,12 @@ async def generate_and_rm( sample.status = Sample.Status.ABORTED return sample - if args.custom_generate_function_path is not None: - custom_generate_func = load_function(args.custom_generate_function_path) - sample = await custom_generate_func(args, sample, sampling_params) - else: - sample = await generate(args, sample, sampling_params) + with state.dp_rank_context() as _: + if args.custom_generate_function_path is not None: + custom_generate_func = load_function(args.custom_generate_function_path) + sample = await custom_generate_func(args, sample, sampling_params) + else: + sample = await generate(args, sample, sampling_params) # for the rm that need the whole group, we will not do the rm here if args.group_rm: @@ -266,7 +287,9 @@ async def generate_and_rm_group( if getattr(args, "sglang_enable_deterministic_inference", False): seed = state.group_sampling_seeds[idx] current_sampling_params["sampling_seed"] = seed - tasks.append(generate_and_rm(args, sample, current_sampling_params, evaluation=evaluation)) + tasks.append( + asyncio.create_task(generate_and_rm(args, sample, current_sampling_params, evaluation=evaluation)) + ) group = await asyncio.gather(*tasks) @@ -349,6 +372,7 @@ async def generate_rollout_async( target_data_size = args.rollout_batch_size data = [] + all_data = [] do_print = True pbar = tqdm(total=target_data_size * args.n_samples_per_prompt, desc="Rollout generation") while len(data) < target_data_size: @@ -370,6 +394,7 @@ async def generate_rollout_async( do_print = False assert len(group) == args.n_samples_per_prompt + all_data.append(group) dynamic_filter_output = _call_dynamic_filter(dynamic_filter, args, group) if not dynamic_filter_output.keep: metric_gatherer.on_dynamic_filter_drop(reason=dynamic_filter_output.reason) @@ -393,6 +418,7 @@ async def generate_rollout_async( assert len(data) == args.rollout_batch_size, f"Got {len(data)} samples, expected {args.rollout_batch_size}" data = sorted(data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index) + all_samples = sorted(data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index) # reset the global state to prevent effects on the next rollout or eval. state.reset() @@ -400,6 +426,11 @@ async def generate_rollout_async( filter_func = load_function(args.rollout_sample_filter_path) filter_func(args, data) + # There can be circumstances where users want to process all samples including filtered ones. + if args.rollout_all_samples_process_path is not None: + process_func = load_function(args.rollout_all_samples_process_path) + process_func(args, all_samples, data_source) + return RolloutFnTrainOutput(samples=data, metrics=metric_gatherer.collect()), aborted_samples @@ -508,17 +539,19 @@ async def eval_rollout_single_dataset( sampling_params = base_sampling_params.copy() sampling_params["sampling_seed"] = args.rollout_seed + j tasks.append( - generate_and_rm( - args, - sample, - sampling_params=sampling_params, - evaluation=True, + asyncio.create_task( + generate_and_rm( + args, + sample, + sampling_params=sampling_params, + evaluation=True, + ) ) ) data = [] do_print = True - pbar = tqdm(total=len(tasks), desc="Rollout generation", disable=not do_print) + pbar = tqdm(total=len(tasks), desc=f"Eval {dataset_cfg.name}", disable=not do_print) for coro in asyncio.as_completed(tasks): sample = await coro if do_print: diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index ce6e47161..824b3a028 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -11,7 +11,6 @@ from miles.backends.sglang_utils.arguments import add_sglang_arguments from miles.backends.sglang_utils.arguments import validate_args as sglang_validate_args from miles.utils.eval_config import EvalDatasetConfig, build_eval_dataset_configs, ensure_dataset_list - from miles.utils.logging_utils import configure_logger logger = logging.getLogger(__name__) @@ -155,11 +154,26 @@ def add_train_arguments(parser): default="raw", help="The method to convert megatron weights to hugging face weights for SGLang.", ) + parser.add_argument( + "--custom-model-provider-path", + type=str, + default=None, + help=( + "Path to a custom model provider function. " + "If set, we will use this function instead of the default model provider. " + "The function should have the signature " + "`def custom_model_provider(pre_process: bool, post_process: bool, vp_stage: int | None = None) -> GPTModel`. " + "Example: 'my_module.my_model_provider'." + ), + ) parser.add_argument( "--recompute-loss-function", action="store_true", help="Whether to disable recompute loss function to save memory during training.", ) + parser.add_argument( + "--log-probs-chunk-size", type=int, default=-1, help="Chunk size to compute log probs to save memory" + ) return parser @@ -177,11 +191,6 @@ def add_rollout_arguments(parser): "It doesn't necessary need to contain the most up-to-date parameters." ), ) - parser.add_argument( - "--use-hf-config-for-megatron", - action="store_true", - help="Whether to use HF config for Megatron core to define the model architecture.", - ) parser.add_argument( "--model-name", type=str, @@ -240,7 +249,7 @@ def add_rollout_arguments(parser): parser.add_argument( "--rollout-max-response-len", type=int, - default=1024, + default=None, help=( "The maximum length of the response for the inference engine during rollout. " "It is basically `max_tokens` in sglang." @@ -329,6 +338,15 @@ def add_rollout_arguments(parser): "This is useful for long responses." ), ) + parser.add_argument( + "--mask-offpolicy-in-partial-rollout", + action="store_true", + default=False, + help=( + "Whether to mask previous generation in partial rollout. " + "If set, only on-policy generated tokens will be used in training" + ), + ) parser.add_argument( "--custom-generate-function-path", type=str, @@ -338,6 +356,26 @@ def add_rollout_arguments(parser): "This should be useful if you need to implement some special rollout logic, e.g. multi-turn, function calling." ), ) + parser.add_argument( + "--custom-rollout-log-function-path", + type=str, + default=None, + help=( + "The custom function for logging rollout data. The signature of the functions is: " + "def log_rollout_data(rollout_id, args, samples, rollout_extra_metrics, rollout_time) -> bool. " + "The return value indicates whether to skip the default logging. " + ), + ) + parser.add_argument( + "--custom-eval-rollout-log-function-path", + type=str, + default=None, + help=( + "The custom function for logging eval rollout data. " + "def log_eval_rollout_data(rollout_id, args, data, extra_metrics) -> bool. " + "The return value indicates whether to skip the default logging. " + ), + ) parser.add_argument( "--buffer-filter-path", @@ -417,8 +455,8 @@ def add_fault_tolerance_arguments(parser): parser.add_argument( "--rollout-health-check-first-wait", type=float, - default=300.0, - help="Time to wait for the compilation before the actual health check.", + default=0, + help="Initial grace period (in seconds) before starting health checks. This allows time for model compilation and initialization. Increase this value significantly when using deepgemm.", ) return parser @@ -613,6 +651,12 @@ def add_eval_arguments(parser): "When provided, this overrides --eval-prompt-data." ), ) + parser.add_argument( + "--skip-eval-before-train", + action="store_true", + default=False, + help="Whether to skip evaluation before training.", + ) # The following keys are used to override the rollout version during eval. parser.add_argument("--eval-input-key", type=str, default=None, help="JSON dataset key") @@ -650,6 +694,7 @@ def add_algo_arguments(parser): reset_arg(parser, "--load", type=str, default=None) reset_arg(parser, "--save", type=str, default=None) reset_arg(parser, "--save-interval", type=int, default=None) + reset_arg(parser, "--async-save", action="store_true") reset_arg(parser, "--seed", type=int, default=1234) reset_arg(parser, "--clip-grad", type=float, default=1.0) reset_arg(parser, "--calculate-per-token-loss", action="store_true") @@ -816,6 +861,12 @@ def add_algo_arguments(parser): default=None, help="Path to the custom TIS/RS function (e.g., examples/train_infer_mismatch_helper/mis.py:compute_mis_weights_with_cp).", ) + parser.add_argument( + "--custom-pg-loss-reducer-function-path", + type=str, + default=None, + help="Path to a custom reducer function for pg_loss only. When set, pg_loss will use this custom reducer while other metrics (pg_clipfrac, ppo_kl, entropy_loss, etc.) still use the default sum_of_sample_mean. (e.g., examples/Dr.GRPO/custom_reducer.py:get_pg_loss_reducer).", + ) parser.add_argument( "--use-routing-replay", @@ -934,6 +985,12 @@ def add_wandb_arguments(parser): "Specify the key in the reward dict using this argument.", ), ) + parser.add_argument( + "--log-correct-samples", + action="store_true", + default=False, + help="Whether to turn on passrate logging, which will log the pass@n of the responses in the rollout.", + ) parser.add_argument("--wandb-run-id", type=str, default=None) return parser @@ -1092,6 +1149,16 @@ def add_reward_model_arguments(parser): "Path to the custom function that will post process reward, by default it will be the normalization for grpo. " ), ) + parser.add_argument( + "--custom-convert-samples-to-train-data-path", + type=str, + default=None, + help=( + "Path to a custom function that converts samples to training data. " + "If set, this function will replace the default _convert_samples_to_train_data. " + "The function should have the signature `def convert_samples_to_train_data(args, samples) -> dict`." + ), + ) return parser def add_rollout_buffer_arguments(parser): @@ -1144,6 +1211,15 @@ def add_rollout_buffer_arguments(parser): "Note: This attribute does not determine whether the sample participates in advantage normalization." ), ) + parser.add_argument( + "--rollout-all-samples-process-path", + type=str, + default=None, + help=( + "Path to the rollout all samples process function that " + "can process all samples including filtered ones." + ), + ) parser.add_argument( "--disable-rollout-trim-samples", action="store_true", @@ -1257,21 +1333,17 @@ def add_sglang_tp_size(): parser = add_mtp_training_arguments(parser) parser = add_prefill_decode_disaggregation_arguments(parser) parser = add_ci_arguments(parser) - parser.set_defaults(sglang_tensor_parallel_size=add_sglang_tp_size()) - - # For megatron parser = add_custom_megatron_plugins_arguments(parser) - try: - parser.add_argument( - "--custom-config-path", - type=str, - default=None, - help="Path to the YAML config for custom function arguments.", - ) - parser.add_argument("--padded-vocab-size", type=int, default=None) - except argparse.ArgumentError: - pass + reset_arg( + parser, + "--custom-config-path", + type=str, + default=None, + help="Path to the YAML config for custom function arguments.", + ) + reset_arg(parser, "--padded-vocab-size", type=int, default=None) + parser.set_defaults(sglang_tensor_parallel_size=add_sglang_tp_size()) return parser return add_miles_arguments @@ -1285,19 +1357,13 @@ def parse_args(add_custom_arguments=None): backend = parse_args_train_backend() if backend == "megatron": - from miles.backends.megatron_utils import parse_args as megatron_parse_args - from miles.backends.megatron_utils import set_default_megatron_args - from miles.backends.megatron_utils import validate_args as megatron_validate_args + from miles.backends.megatron_utils.arguments import parse_args as megatron_parse_args + from miles.backends.megatron_utils.arguments import set_default_megatron_args + from miles.backends.megatron_utils.arguments import validate_args as megatron_validate_args args = megatron_parse_args(extra_args_provider=add_miles_arguments) if args.hf_checkpoint: hf_config = AutoConfig.from_pretrained(args.hf_checkpoint, trust_remote_code=True) - if args.use_hf_config_for_megatron: - from miles.backends.megatron_utils.config_mapping import get_mapper - - megatron_config_from_hf = get_mapper(hf_config.model_type)(hf_config) - _validate_and_update_megatron_args_from_hf(args, megatron_config_from_hf.transformer_config) - _validate_and_update_megatron_args_from_hf(args, megatron_config_from_hf.gpt_model_args) hf_validate_args(args, hf_config) args.rank = 0 @@ -1396,18 +1462,23 @@ def miles_validate_args(args): ) # TODO: During loading, we need to set the start_rollout_id here. - if ( - args.load is None - or not os.path.exists(args.load) - or not os.path.exists(os.path.join(args.load, "latest_checkpointed_iteration.txt")) - ): - args.no_load_optim = True - args.no_load_rng = True - args.finetune = True - args.load = args.ref_load - if args.ref_ckpt_step is not None: - args.ckpt_step = args.ref_ckpt_step + if args.megatron_to_hf_mode == "bridge": + if args.load is None: + args.load = args.ref_load or args.hf_checkpoint args.start_rollout_id = 0 + else: + if ( + args.load is None + or not os.path.exists(args.load) + or not os.path.exists(os.path.join(args.load, "latest_checkpointed_iteration.txt")) + ): + args.no_load_optim = True + args.no_load_rng = True + args.finetune = True + args.load = args.ref_load + if args.ref_ckpt_step is not None: + args.ckpt_step = args.ref_ckpt_step + args.start_rollout_id = 0 if args.eval_interval is not None: assert args.eval_datasets, "Evaluation datasets must be configured when eval_interval is set." @@ -1567,35 +1638,26 @@ def miles_validate_args(args): logger.info(f"Warning: Argument {k} is already set to {getattr(args, k)}, will override with {v}.") setattr(args, k, v) - if args.rollout_max_context_len is None: - logger.info( - f"args.rollout_max_context_len is not set. Use args.rollout_max_response_len {args.rollout_max_response_len} as default value." - ) - args.rollout_max_context_len = args.rollout_max_response_len - if args.eval_max_context_len is None: logger.info( f"args.eval_max_context_len is not set. Use args.rollout_max_context_len {args.rollout_max_context_len} as default value." ) args.eval_max_context_len = args.rollout_max_context_len - if args.rollout_max_prompt_len is None: - logger.info( - f"args.rollout_max_prompt_len is not set. Use args.rollout_max_context_len - 1 ({args.rollout_max_context_len} - 1) as default value so that there is at least one generated token to compute loss." - ) - args.rollout_max_prompt_len = args.rollout_max_context_len - 1 - - assert ( - args.rollout_max_prompt_len <= args.rollout_max_context_len - 1 - ), f"args.rollout_max_prompt_len ({args.rollout_max_prompt_len}) must be smaller than args.rollout_max_context_len ({args.rollout_max_context_len}) so that there is at least one generated token to compute loss." + if args.rollout_max_context_len is not None: + if args.rollout_max_prompt_len is None: + args.rollout_max_prompt_len = args.rollout_max_context_len - 1 + logger.info( + f"args.rollout_max_prompt_len is not set. Use args.rollout_max_context_len - 1 ({args.rollout_max_context_len} - 1) as default value so that there is at least one generated token to compute loss." + ) + assert ( + args.rollout_max_prompt_len <= args.rollout_max_context_len - 1 + ), f"args.rollout_max_prompt_len ({args.rollout_max_prompt_len}) must be smaller than args.rollout_max_context_len ({args.rollout_max_context_len}) so that there is at least one generated token to compute loss." - if args.prefill_num_servers is not None: - assert not args.use_fault_tolerance, "fault tolerance is not supported when prefill_num_servers is set." + assert not ( + args.prefill_num_servers is not None and args.rollout_external + ), "prefill_num_servers cannot be set when rollout_external is set." - assert args.qkv_format in [ - "thd", - "bshd", - ], f"qkv_format {args.qkv_format} is not supported. (only 'thd' and 'bshd' are supported)" if args.qkv_format == "bshd": assert args.train_backend == "megatron", "bshd format is only supported for megatron backend." assert ( @@ -1609,6 +1671,10 @@ def equal(x, y): errors = [] + # multimodal models have different config structure + if hasattr(hf_config, "text_config"): + hf_config = hf_config.text_config + for hf_config_name, megatron_config_name, compare_fn in [ ("hidden_size", "hidden_size", equal), ("num_attention_heads", "num_attention_heads", equal), @@ -1627,12 +1693,3 @@ def equal(x, y): if len(errors) > 0: raise AssertionError("hf_validate_args failed: " + "; ".join(errors)) - - -def _validate_and_update_megatron_args_from_hf(args, args_from_hf_config: dict[str, Any]): - for key, value in args_from_hf_config.items(): - if hasattr(args, key) and getattr(args, key) != value: - raise ValueError( - f"Argument {key} is not consistent. {key} in args is {getattr(args, key)}, but from HF config is {value}." - ) - setattr(args, key, value) diff --git a/miles/utils/data.py b/miles/utils/data.py index c36902c81..fea0d4c46 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -1,3 +1,4 @@ +import itertools import json import logging import os @@ -5,9 +6,13 @@ import re import numpy as np -import pandas as pd import ray +try: + import pyarrow.parquet as pq +except ImportError: + pq = None + from miles.utils.types import MultimodalTypes, Sample from .timer import Timer @@ -17,26 +22,50 @@ logger = logging.getLogger(__name__) -# TODO: don't read the whole file into memory. def read_file(path): path, row_slice = _parse_generalized_path(path) + reader = None if not os.path.exists(path): raise FileNotFoundError(f"Prompt dataset path '{path}' does not exist.") if path.endswith(".jsonl"): - df = pd.read_json(path, lines=True, dtype={"label": str}) + + def jsonl_reader(p): + with open(p, encoding="utf-8") as f: + for line_num, line in enumerate(f): + line = line.strip() + if not line: + continue + try: + yield json.loads(line) + except json.JSONDecodeError as e: + print(f"JSON decode error at line {line_num}: {e}") + continue + + reader = jsonl_reader(path) + elif path.endswith(".parquet"): - df = pd.read_parquet(path, dtype_backend="pyarrow") + if pq is None: + raise ImportError("pyarrow is required for parquet support") + + def parquet_reader(p): + pf = pq.ParquetFile(p) + + for batch in pf.iter_batches(): + yield from batch.to_pylist() + + reader = parquet_reader(path) + else: raise ValueError(f"Unsupported file format: {path}. Supported formats are .jsonl and .parquet.") if row_slice is not None: - logger.info(f"read_file path={path} slice {len(df)=} rows into {row_slice=}") - df = df.iloc[row_slice] - for _, row in df.iterrows(): - yield row.to_dict() + logger.info("read_file path=%s applying slice row_slice=%s", path, row_slice) + reader = itertools.islice(reader, row_slice.start, row_slice.stop, row_slice.step) + + yield from reader def _parse_generalized_path(s: str): @@ -49,23 +78,31 @@ def _parse_generalized_path(s: str): return s, None -def _should_skip_prompt(prompt, tokenizer, processor, max_length, apply_chat_template_kwargs): +def _should_skip_prompt(formatted_prompt: str, tokenizer, processor, max_length, multimodal_inputs=None): if max_length is None: return False - from miles.utils.processing_utils import prepare_model_inputs + if processor: + processor_output = processor(text=formatted_prompt, **multimodal_inputs) + input_ids = processor_output["input_ids"][0] + else: + input_ids = tokenizer.encode(formatted_prompt, add_special_tokens=False) - input_ids, _ = prepare_model_inputs(prompt, tokenizer, processor, None, apply_chat_template_kwargs) return len(input_ids) > max_length -def _build_messages(data: dict, prompt_key: str, multimodal_keys: dict = None): - messages = data.get(prompt_key) +def _build_messages(data: dict, prompt_key: str, as_conversation: bool, multimodal_keys: dict = None): + prompt = data.get(prompt_key) - if isinstance(messages, str): - messages = [{"role": "user", "content": messages}] + if isinstance(prompt, str): + # If prompt is a string and we don't apply chat template, return the prompt as is. + if not as_conversation: + return prompt + else: + prompt = [{"role": "user", "content": prompt}] if multimodal_keys: + assert as_conversation, "as_conversation must be True when multimodal_keys is not None" # Build mapping: placeholder -> (MultimodalType, content_list) multimodals = {} for type_name, data_key in multimodal_keys.items(): @@ -75,7 +112,7 @@ def _build_messages(data: dict, prompt_key: str, multimodal_keys: dict = None): pattern = "(" + "|".join(re.escape(p) for p in multimodals.keys()) + ")" - for message in messages: + for message in prompt: if isinstance(message["content"], str): content_list = [] for segment in re.split(pattern, message["content"]): @@ -105,7 +142,7 @@ def _build_messages(data: dict, prompt_key: str, multimodal_keys: dict = None): f"Unsupported content type: {type(message['content'])}, expected str or list of dicts" ) - return messages + return prompt class Dataset: @@ -127,9 +164,11 @@ def __init__( ): self.origin_samples = [] for data in read_file(path): - prompt = _build_messages(data, prompt_key, multimodal_keys) + as_conversation = apply_chat_template + prompt = _build_messages(data, prompt_key, as_conversation, multimodal_keys) metadata = data.get(metadata_key) or {} + tools = None if tool_key is not None and tool_key in data: tools = data[tool_key] if isinstance(tools, str): @@ -139,15 +178,37 @@ def __init__( assert isinstance(tools, list), f"tools must be a list, got {type(tools)} instead" metadata["tools"] = tools + if apply_chat_template: + formatted_prompt = tokenizer.apply_chat_template( + prompt, + tools=tools, + tokenize=False, + add_generation_prompt=True, + **(apply_chat_template_kwargs or {}), + ) + else: + formatted_prompt = prompt + + if processor: + from miles.utils.processing_utils import process_vision_info + + assert isinstance( + prompt, list + ), f"prompt must be a list when processor is not None, got {type(prompt)} instead" + multimodal_inputs = process_vision_info(prompt, processor) + else: + multimodal_inputs = None + # TODO: this is slow. - if _should_skip_prompt(prompt, tokenizer, processor, max_length, apply_chat_template_kwargs): + if _should_skip_prompt(formatted_prompt, tokenizer, processor, max_length, multimodal_inputs): continue self.origin_samples.append( Sample( - prompt=prompt, + prompt=formatted_prompt, label=data[label_key] if label_key is not None else None, metadata=metadata, + multimodal_inputs=multimodal_inputs, ) ) diff --git a/miles/utils/eval_config.py b/miles/utils/eval_config.py index 4a7c1e912..69b4464b4 100644 --- a/miles/utils/eval_config.py +++ b/miles/utils/eval_config.py @@ -111,6 +111,9 @@ class EvalDatasetConfig: top_p: float | None = None top_k: int | None = None max_response_len: int | None = None + stop: list[str] | None = None + stop_token_ids: list[int] | None = None + min_new_tokens: int | None = None metadata_overrides: dict[str, Any] = field(default_factory=dict) diff --git a/miles/utils/flops_utils.py b/miles/utils/flops_utils.py index 71cdd4c65..75afccc05 100644 --- a/miles/utils/flops_utils.py +++ b/miles/utils/flops_utils.py @@ -6,20 +6,43 @@ def calculate_lm_head_flops(seqlen, hidden_size, vocab_size): return 2 * seqlen * hidden_size * vocab_size -def calculate_qkv_projection_flops(seqlen, hidden_size, num_attention_heads, num_query_groups): - head_dim = hidden_size // num_attention_heads - n_q_heads = num_attention_heads - n_kv_heads = num_query_groups - q_flops = 2 * seqlen * hidden_size * n_q_heads * head_dim - kv_flops = 2 * seqlen * hidden_size * n_kv_heads * head_dim * 2 +def calculate_qkv_projection_flops(args, seqlen, hidden_size, num_attention_heads, num_query_groups): + if args.q_lora_rank is None: + q_flops = 2 * seqlen * hidden_size * num_attention_heads * args.kv_channels + else: + q_flops = ( + 2 + * seqlen + * args.q_lora_rank + * (args.hidden_size + args.num_attention_heads * (args.qk_head_dim + args.qk_pos_emb_head_dim)) + ) + if args.kv_lora_rank is None: + kv_flops = 2 * 2 * seqlen * hidden_size * num_query_groups * args.kv_channels + else: + kv_flops = ( + 2 + * seqlen + * ( + args.kv_lora_rank + * (args.hidden_size + args.num_attention_heads * (args.qk_head_dim + args.v_head_dim)) + + args.hidden_size * args.qk_pos_emb_head_dim + ) + ) + return q_flops + kv_flops -def calculate_attention_flops(seqlen, num_attention_heads, head_dim): +def calculate_attention_flops(args, seqlen, num_attention_heads): # QK^T with causal - flops = 2 * num_attention_heads * seqlen * seqlen * head_dim // 2 + if args.qk_pos_emb_head_dim: + flops = 2 * num_attention_heads * seqlen * seqlen * (args.qk_head_dim + args.qk_pos_emb_head_dim) / 2 + else: + flops = 2 * num_attention_heads * seqlen * seqlen * args.kv_channels / 2 # A*V - flops += 2 * num_attention_heads * seqlen * seqlen * head_dim + if args.v_head_dim: + flops += num_attention_heads * seqlen * seqlen * args.v_head_dim + else: + flops += num_attention_heads * seqlen * seqlen * args.kv_channels return flops @@ -31,12 +54,10 @@ def calculate_mlp_flops(seqlen, hidden_size, ffn_hidden_size): return 2 * seqlen * hidden_size * ffn_hidden_size * 3 -def calculate_layer_flops(seqlen, hidden_size, num_attention_heads, num_query_groups, ffn_hidden_size, head_dim): - if head_dim is None: - head_dim = hidden_size // num_attention_heads +def calculate_layer_flops(args, seqlen, hidden_size, num_attention_heads, num_query_groups, ffn_hidden_size): return ( - calculate_qkv_projection_flops(seqlen, hidden_size, num_attention_heads, num_query_groups) - + calculate_attention_flops(seqlen, num_attention_heads, head_dim) + calculate_qkv_projection_flops(args, seqlen, hidden_size, num_attention_heads, num_query_groups) + + calculate_attention_flops(args, seqlen, num_attention_heads) + calculate_output_flops(seqlen, hidden_size) + calculate_mlp_flops(seqlen, hidden_size, ffn_hidden_size) ) @@ -50,7 +71,6 @@ def calculate_fwd_flops( num_attention_heads = args.num_attention_heads num_query_groups = args.num_query_groups vocab_size = args.vocab_size - kv_channels = args.kv_channels total_flops = 0 @@ -79,12 +99,12 @@ def calculate_fwd_flops( if num_dense_layers > 0: total_flops += ( calculate_layer_flops( + args, seqlen, hidden_size, num_attention_heads, num_query_groups, dense_ffn, - kv_channels, ) * num_dense_layers ) @@ -92,12 +112,12 @@ def calculate_fwd_flops( if num_moe_layers > 0: total_flops += ( calculate_layer_flops( + args, seqlen, hidden_size, num_attention_heads, num_query_groups, moe_ffn, - kv_channels, ) * num_moe_layers ) diff --git a/miles/utils/health_monitor.py b/miles/utils/health_monitor.py index 5757a1675..eea834c99 100644 --- a/miles/utils/health_monitor.py +++ b/miles/utils/health_monitor.py @@ -24,6 +24,7 @@ def start(self) -> bool: assert self._thread is None, "Health monitor thread is already running." + logger.info("Starting RolloutHealthMonitor...") self._stop_event = threading.Event() self._thread = threading.Thread( target=self._health_monitor_loop, @@ -31,26 +32,32 @@ def start(self) -> bool: daemon=True, ) self._thread.start() + logger.info("RolloutHealthMonitor started.") return True def stop(self) -> None: if not self._thread: return + logger.info("Stopping RolloutHealthMonitor...") assert self._stop_event is not None self._stop_event.set() timeout = self._check_timeout + self._check_interval + 5 self._thread.join(timeout=timeout) if self._thread.is_alive(): logging.warning("Rollout health monitor thread did not terminate within %.1fs", timeout) + else: + logger.info("RolloutHealthMonitor stopped.") self._thread = None self._stop_event = None def _health_monitor_loop(self) -> None: assert self._stop_event is not None + logger.info(f"Health monitor loop started. Waiting for first wait: {self._check_first_wait}s") # TODO: need to be waiting for the large moe to be ready. this is hacky. if self._stop_event.wait(self._check_first_wait): + logger.info("Health monitor stopped during first wait.") return while not self._stop_event.is_set(): self._run_health_checks() @@ -65,25 +72,32 @@ def _run_health_checks(self) -> None: def _check_engine_health(self, rollout_engine_id, engine) -> None: if engine is None: + logger.info(f"Skipping health check for engine {rollout_engine_id} (None)") return try: ray.get(engine.health_generate.remote(timeout=self._check_timeout)) except Exception as e: - logger.info( - f"Health check timed out for rollout engine {rollout_engine_id} (ray timeout). Killing actor. (original exception: {e})" + logger.error( + f"Health check failed for rollout engine {rollout_engine_id} (ray timeout or error). Killing actor. Exception: {e}" ) self._kill_engine(rollout_engine_id=rollout_engine_id) def _kill_engine(self, rollout_engine_id: int): + logger.info(f"Killing engine group {rollout_engine_id}...") for i in range( rollout_engine_id * self._rollout_manager.nodes_per_engine, (rollout_engine_id + 1) * self._rollout_manager.nodes_per_engine, ): engine = self._rollout_manager.all_rollout_engines[i] - try: - ray.get(engine.shutdown.remote()) - ray.kill(engine) - except Exception as e: - logger.info(f"Fail to kill engine and skip (e: {e})") + if engine: + logger.info(f"Shutting down and killing engine at index {i}") + try: + ray.get(engine.shutdown.remote()) + ray.kill(engine) + logger.info(f"Successfully killed engine at index {i}") + except Exception as e: + logger.warning(f"Fail to kill engine at index {i} (e: {e})") + else: + logger.info(f"Engine at index {i} is already None") self._rollout_manager.all_rollout_engines[i] = None diff --git a/miles/utils/http_utils.py b/miles/utils/http_utils.py index 04b7a677e..2b3e6e192 100644 --- a/miles/utils/http_utils.py +++ b/miles/utils/http_utils.py @@ -45,34 +45,64 @@ def get_host_info(): if env_overwrite_local_ip := os.getenv(MILES_HOST_IP_ENV, None): return hostname, env_overwrite_local_ip - # try DNS - try: - return hostname, socket.gethostbyname(hostname) - except socket.gaierror: - pass + def _is_loopback(ip): + return ip.startswith("127.") or ip == "::1" - # try IPv4 - try: - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_sock: - udp_sock.connect(("8.8.8.8", 80)) # Google DNS - return hostname, udp_sock.getsockname()[0] - except OSError: - pass + def _resolve_ip(family, test_target_ip): + """ + Attempt to get the local LAN IP for the specific family (IPv4/IPv6). + Strategy: UDP Probe (Preferred) -> Hostname Resolution (Fallback) -> None + """ - # try IPv6 - try: - with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s6: - s6.connect(("2001:4860:4860::8888", 80)) - return hostname, s6.getsockname()[0] - except OSError: - pass + # Strategy 1: UDP Connect Probe (Most accurate, relies on routing table) + # Useful when the machine has a default gateway or internet access. + try: + with socket.socket(family, socket.SOCK_DGRAM) as s: + # The IP doesn't need to be reachable, but the routing table must exist. + s.connect((test_target_ip, 80)) + ip = s.getsockname()[0] + if not _is_loopback(ip): + return ip + except Exception: + pass # Route unreachable or network error, move to next strategy. + + # Strategy 2: Hostname Resolution (Fallback for offline clusters) + # Useful for offline environments where UDP connect fails but /etc/hosts is configured. + try: + # getaddrinfo allows specifying the family (AF_INET or AF_INET6) + # Result format: [(family, type, proto, canonname, sockaddr), ...] + infos = socket.getaddrinfo(hostname, None, family=family, type=socket.SOCK_STREAM) + + for info in infos: + ip = info[4][0] # The first element of sockaddr is the IP + # Must filter out loopback addresses to avoid "127.0.0.1" issues + if not _is_loopback(ip): + return ip + except Exception: + pass - # hostname -I - try: - local_ip = os.popen("hostname -I | awk '{print $1}'").read().strip() - return hostname, local_ip or "::1" - except Exception: - return hostname, "::1" + return None + + prefer_ipv6 = os.getenv("MILES_PREFER_IPV6", "0").lower() in ("1", "true", "yes", "on") + local_ip = None + final_fallback = "127.0.0.1" + + if prefer_ipv6: + # [Strict Mode] IPv6 Only + # 1. Try UDP V6 Probe + # 2. Try Hostname Resolution (V6) + # If failed, fallback to V6 loopback. Never mix with V4. + local_ip = _resolve_ip(socket.AF_INET6, "2001:4860:4860::8888") + final_fallback = "::1" + else: + # [Strict Mode] IPv4 Only (Default) + # 1. Try UDP V4 Probe + # 2. Try Hostname Resolution (V4) + # If failed, fallback to V4 loopback. Never mix with V6. + local_ip = _resolve_ip(socket.AF_INET, "8.8.8.8") + final_fallback = "127.0.0.1" + + return hostname, local_ip or final_fallback def _wrap_ipv6(host): diff --git a/miles/utils/mask_utils.py b/miles/utils/mask_utils.py index 36fc75aac..a5bc90ab4 100644 --- a/miles/utils/mask_utils.py +++ b/miles/utils/mask_utils.py @@ -2,7 +2,7 @@ def get_response_lengths(loss_masks: list[list[int]]) -> list[int]: - return [len(mask[mask.index(1) :]) if 1 in mask else 0 for mask in loss_masks] + return [mask.count(1) if 1 in mask else 0 for mask in loss_masks] class MultiTurnLossMaskGenerator: @@ -44,12 +44,17 @@ def get_system_message_length(self) -> tuple[int, int]: system_message_length = idx_1 - ((idx_2 - idx_1) - end_interval - len(raw_token_ids)) return system_message_length, gen_token_length - def gen_multi_turn_loss_mask_qwen(self, messages: list[dict]) -> tuple[list[int], list[int]]: + def gen_multi_turn_loss_mask_qwen( + self, messages: list[dict], tools: list[dict] = None + ) -> tuple[list[int], list[int]]: all_loss_masks = [] all_token_ids = [] for i, message in enumerate(messages): - message_ids = self.tokenizer.apply_chat_template([message], tokenize=True) + if i == 0: + message_ids = self.tokenizer.apply_chat_template([message], tokenize=True, tools=tools) + else: + message_ids = self.tokenizer.apply_chat_template([message], tokenize=True) if message["role"] != "system" and i > 0: message_ids = message_ids[self.system_message_length :] @@ -67,7 +72,9 @@ def gen_multi_turn_loss_mask_qwen(self, messages: list[dict]) -> tuple[list[int] return all_token_ids, all_loss_masks - def gen_multi_turn_loss_mask_qwen3(self, messages: list[dict]) -> tuple[list[int], list[int]]: + def gen_multi_turn_loss_mask_qwen3( + self, messages: list[dict], tools: list[dict] = None + ) -> tuple[list[int], list[int]]: all_loss_masks = [] all_token_ids = [] @@ -75,8 +82,14 @@ def gen_multi_turn_loss_mask_qwen3(self, messages: list[dict]) -> tuple[list[int prefix_token_ids = self.tokenizer.apply_chat_template([prefix_message], tokenize=True) for i, message in enumerate(messages): - prefixed_message_ids = self.tokenizer.apply_chat_template([prefix_message, message], tokenize=True) - message_ids = prefixed_message_ids[len(prefix_token_ids) :] + if i == 0: + tailed_message_ids = self.tokenizer.apply_chat_template( + [message, prefix_message], tokenize=True, tools=tools + ) + message_ids = tailed_message_ids[: -len(prefix_token_ids)] + else: + prefixed_message_ids = self.tokenizer.apply_chat_template([prefix_message, message], tokenize=True) + message_ids = prefixed_message_ids[len(prefix_token_ids) :] if message["role"] != "system" and i > 0: message_ids = message_ids[self.system_message_length :] @@ -94,8 +107,12 @@ def gen_multi_turn_loss_mask_qwen3(self, messages: list[dict]) -> tuple[list[int return all_token_ids, all_loss_masks - def gen_multi_turn_loss_mask_distill_qwen(self, messages: list[dict]) -> tuple[list[int], list[int]]: - prompt = self.tokenizer.apply_chat_template(messages[:1], tokenize=False, add_generation_prompt=True) + def gen_multi_turn_loss_mask_distill_qwen( + self, messages: list[dict], tools: list[dict] = None + ) -> tuple[list[int], list[int]]: + prompt = self.tokenizer.apply_chat_template( + messages[:1], tokenize=False, add_generation_prompt=True, tools=tools + ) response = messages[-1]["content"] prompt_tokens = self.tokenizer(prompt, add_special_tokens=False)["input_ids"] response_tokens = self.tokenizer(response, add_special_tokens=False)["input_ids"] @@ -108,19 +125,46 @@ def gen_multi_turn_loss_mask_distill_qwen(self, messages: list[dict]) -> tuple[l loss_mask = [0] * len(token_ids) return token_ids, loss_mask - def get_loss_mask(self, messages: list[dict]) -> list[int]: + def get_loss_mask(self, messages: list[dict], tools: list[dict] = None) -> tuple[list[int], list[int]]: if self.tokenizer_type == "qwen": if "<|Assistant|>" in self.tokenizer.get_added_vocab(): - return self.gen_multi_turn_loss_mask_distill_qwen(messages) + return self.gen_multi_turn_loss_mask_distill_qwen(messages, tools) - return self.gen_multi_turn_loss_mask_qwen(messages) + return self.gen_multi_turn_loss_mask_qwen(messages, tools) elif self.tokenizer_type == "qwen3": - return self.gen_multi_turn_loss_mask_qwen3(messages) + return self.gen_multi_turn_loss_mask_qwen3(messages, tools) elif self.tokenizer_type == "distill_qwen": - return self.gen_multi_turn_loss_mask_distill_qwen(messages) + return self.gen_multi_turn_loss_mask_distill_qwen(messages, tools) else: raise ValueError(f"Unsupported tokenizer type: {self.tokenizer_type}") + def get_loss_mask_with_multimodal_alignment( + self, messages: list[dict], input_ids: list[int], tools: list[dict] = None + ) -> tuple[list[int], list[int]]: + text = [] + for msg in messages: + if isinstance(msg.get("content"), list): + text_parts = [] + for item in msg["content"]: + if isinstance(item, dict) and item.get("type") == "text": + text_parts.append(item.get("text", "")) + elif isinstance(item, str): + text_parts.append(item) + text.append({"role": msg["role"], "content": " ".join(text_parts)}) + else: + text.append(msg) + + _, loss_mask_text = self.get_loss_mask(text, tools=tools) + + diff = len(input_ids) - len(loss_mask_text) + assert diff >= 0, ( + f"input_ids (length={len(input_ids)}) is shorter than text loss_mask (length={len(loss_mask_text)}) " + f"Please check if processor and tokenizer tokenization are consistent." + ) + loss_mask = [0] * diff + loss_mask_text + + return input_ids, loss_mask + def get_text_from_loss_mask(self, token_ids: list[int], loss_masks: list[int]) -> list[str]: selected_texts = [] current_tokens = [] diff --git a/miles/utils/megatron_bridge_utils.py b/miles/utils/megatron_bridge_utils.py index d8bc8060b..9e5f065cd 100644 --- a/miles/utils/megatron_bridge_utils.py +++ b/miles/utils/megatron_bridge_utils.py @@ -10,10 +10,13 @@ def patch_megatron_model(model): unwrapped_model = unwrap_model(model)[0] model_config = unwrapped_model.config - assert not hasattr(model_config, "share_embeddings_and_output_weights") - model_config.share_embeddings_and_output_weights = unwrapped_model.share_embeddings_and_output_weights + attribute_was_added = False + if not hasattr(model_config, "share_embeddings_and_output_weights"): + model_config.share_embeddings_and_output_weights = unwrapped_model.share_embeddings_and_output_weights + attribute_was_added = True try: yield finally: - delattr(model_config, "share_embeddings_and_output_weights") + if attribute_was_added: + delattr(model_config, "share_embeddings_and_output_weights") diff --git a/miles/utils/metric_utils.py b/miles/utils/metric_utils.py index fe8d6df50..fce4107a0 100644 --- a/miles/utils/metric_utils.py +++ b/miles/utils/metric_utils.py @@ -105,7 +105,7 @@ def compression_ratio( return ratio, savings_pct -def has_repetition(text: str = None): +def has_repetition(text: str): if len(text) > 10000 and compression_ratio(text[-10000:])[0] > 10: return True else: diff --git a/miles/utils/misc.py b/miles/utils/misc.py index 2fe825812..23375a60b 100644 --- a/miles/utils/misc.py +++ b/miles/utils/misc.py @@ -71,6 +71,7 @@ def should_run_periodic_action( rollout_id: int, interval: int | None, num_rollout_per_epoch: int | None = None, + num_rollout: int | None = None, ) -> bool: """ Return True when a periodic action (eval/save/checkpoint) should run. @@ -83,5 +84,8 @@ def should_run_periodic_action( if interval is None: return False + if num_rollout is not None and rollout_id == num_rollout - 1: + return True + step = rollout_id + 1 return (step % interval == 0) or (num_rollout_per_epoch is not None and step % num_rollout_per_epoch == 0) diff --git a/miles/utils/ppo_utils.py b/miles/utils/ppo_utils.py index c301ef624..d2d44c6be 100644 --- a/miles/utils/ppo_utils.py +++ b/miles/utils/ppo_utils.py @@ -644,21 +644,72 @@ def chunked_gae( return advantages, returns -def calculate_log_probs_and_entropy(logits, tokens, tp_group, with_entropy: bool = False): +def calculate_log_probs_and_entropy(logits, tokens, tp_group, with_entropy: bool = False, chunk_size: int = -1): logits = logits.contiguous() # TODO: not sure why we need to clone the logits here. # Without the clone, the backward will trigger inplace edit error. # It seems that the function with tp will modify the logits inplace. + entropy = None if logits.size(0) != 0: - log_prob = compute_log_probs(logits.clone(), tokens, tp_group) + if chunk_size > 0: + num_chunks = (logits.size(0) - 1) // chunk_size + 1 + tokens_chunks = tokens.chunk(num_chunks, dim=0) + logits_chunks = logits.chunk(num_chunks, dim=0) + log_probs = [] + for tokens_chunk, logits_chunk in zip(tokens_chunks, logits_chunks, strict=True): + log_prob = compute_log_probs(logits_chunk.clone(), tokens_chunk, tp_group) + log_probs.append(log_prob) + log_prob = torch.cat(log_probs, dim=0) + if with_entropy: + entropys = [] + for _, logits_chunk in zip(tokens_chunks, logits_chunks, strict=True): + entropy = compute_entropy_from_logits(logits_chunk.clone(), tp_group) + entropys.append(entropy) + entropy = torch.cat(entropys, dim=0) + else: + log_prob = compute_log_probs(logits.clone(), tokens, tp_group) + if with_entropy: + entropy = compute_entropy_from_logits(logits.clone(), tp_group) else: log_prob = logits.new_zeros((0,)) - - if with_entropy: - if logits.size(0) != 0: - entropy = compute_entropy_from_logits(logits.clone(), tp_group) - else: + if with_entropy: entropy = logits.new_zeros((0,)) - else: - entropy = None + return log_prob, entropy + + +def vanilla_tis_function( + args, + *, + pg_loss: torch.Tensor, + train_log_probs: list[torch.Tensor], + rollout_log_probs: list[torch.Tensor], + loss_masks: list[torch.Tensor], + **kwargs, +) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]: + """Apply TIS off-policy correction using importance sampling. + + Parameters: + args: Arguments containing TIS settings. + pg_loss: Policy gradient loss tensor of shape [total_seq_len - 1]. + train_log_probs: List of tensors containing training log-probabilities + for each sequence. + rollout_log_probs: List of tensors containing rollout log-probabilities + for each sequence. + loss_masks: List of tensors containing loss masks for each sequence. + """ + rollout_log_probs = torch.cat(rollout_log_probs, dim=0) + old_log_probs = torch.cat(train_log_probs, dim=0) + tis = torch.exp(old_log_probs - rollout_log_probs) + tis_abs = (tis - 1).abs() + tis_clip_low = args.tis_clip_low if args.tis_clip_low is not None else 0.1 + tis_clip_high = args.tis_clip if args.tis_clip is not None else 2.0 + tis_weights = torch.clamp(tis, min=tis_clip_low, max=tis_clip_high) + tis_clipfrac = (tis_weights != tis).float() + metrics = { + "tis": tis.clone().detach(), + "tis_clipfrac": tis_clipfrac.clone().detach(), + "tis_abs": tis_abs.clone().detach(), + } + pg_loss = pg_loss * tis_weights + return pg_loss, loss_masks, metrics diff --git a/miles/utils/processing_utils.py b/miles/utils/processing_utils.py index 60cd8f255..f36f93c1b 100644 --- a/miles/utils/processing_utils.py +++ b/miles/utils/processing_utils.py @@ -6,6 +6,11 @@ logger = logging.getLogger(__name__) +# Default image patch size for vision-language models +# Note: Qwen3-VL uses 16, Qwen2.5-VL uses 14 +# Reference: https://github.com/QwenLM/Qwen3-VL/blob/main/qwen-vl-utils/README.md +DEFAULT_PATCH_SIZE = 14 + def load_tokenizer(name_or_path: str, **kwargs): return AutoTokenizer.from_pretrained(name_or_path, **kwargs) @@ -25,50 +30,22 @@ def load_processor(name_or_path: str, **kwargs): return proc -def prepare_model_inputs(prompt, tokenizer, processor=None, metadata=None, apply_chat_template_kwargs=None): - """Prepare all inputs for model inference. - - Returns: - tuple: (input_ids, extra_info) - - input_ids: Token IDs for the prompt - - extra_info: Dict with 'images', 'videos', 'multimodal_inputs' (or empty dict) - """ - tools = metadata.get("tools") if metadata else None - text_prompt = tokenizer.apply_chat_template( - prompt, - tools=tools, - tokenize=False, - add_generation_prompt=True, - **(apply_chat_template_kwargs or {}), - ) +def process_vision_info(prompt, processor): + # temporary solution, will write image utils for miles later + from qwen_vl_utils import process_vision_info - if not processor: - input_ids = tokenizer.encode(text_prompt, add_special_tokens=False) - return input_ids, {} + if hasattr(processor.image_processor, "patch_size"): + image_patch_size = processor.image_processor.patch_size else: - # temporary solution, will write image utils for miles later - from qwen_vl_utils import process_vision_info - - images, videos = process_vision_info(prompt) - - # Get input IDs with full prompt (text + multimodal) - processor_output = processor(text=text_prompt, images=images, videos=videos) - input_ids = processor_output["input_ids"][0] - - # Extract multimodal tokens (exclude text-related tokens) - multimodal_inputs = {k: v for k, v in processor_output.items() if k not in ["input_ids", "attention_mask"]} - - extra_info = { - "images": images, - "videos": videos, - "multimodal_inputs": multimodal_inputs, - } - - return input_ids, extra_info + logger.info(f"Using default patch size: {DEFAULT_PATCH_SIZE}") + image_patch_size = DEFAULT_PATCH_SIZE + images, videos = process_vision_info(prompt, image_patch_size=image_patch_size) + multimodal_inputs = {"images": images, "videos": videos} + return multimodal_inputs def encode_image_for_rollout_engine(image) -> str: - """Load an image from path, ensure RGB, encode as JPEG base64 string.""" + """Load an image from path, ensure RGB, encode as PNG base64 string.""" buffer = io.BytesIO() if image.mode != "RGB": image = image.convert("RGB") diff --git a/miles/utils/seqlen_balancing.py b/miles/utils/seqlen_balancing.py index 5bee97c6e..a5dd71f94 100644 --- a/miles/utils/seqlen_balancing.py +++ b/miles/utils/seqlen_balancing.py @@ -165,11 +165,11 @@ def _check_and_sort_partitions(partitions): assert len(partitions) == k_partitions, f"{len(partitions)} != {k_partitions}" seen_idx = set() sorted_partitions = [None] * k_partitions - for _i, partition in enumerate(partitions): - assert len(partition) > 0, f"the {_i}-th partition is empty" + for i, partition in enumerate(partitions): + assert len(partition) > 0, f"the {i}-th partition is empty" for idx in partition: seen_idx.add(idx) - sorted_partitions[_i] = sorted(partition) + sorted_partitions[i] = sorted(partition) assert seen_idx == set(range(len(seqlen_list))) return sorted_partitions diff --git a/miles/utils/types.py b/miles/utils/types.py index 9003050a2..7821cf505 100644 --- a/miles/utils/types.py +++ b/miles/utils/types.py @@ -14,7 +14,8 @@ class Sample: # prompt prompt: str | list[dict[str, str]] = "" tokens: list[int] = field(default_factory=list) - multimodal_inputs: dict[str, Any] = None + multimodal_inputs: dict[str, Any] = None # raw multimodal data, e.g. images, videos, etc. + multimodal_train_inputs: dict[str, Any] = None # processed multimodal data, e.g. pixel_values, etc. # response response: str = "" response_length: int = 0 @@ -31,6 +32,10 @@ class Status(Enum): COMPLETED = "completed" TRUNCATED = "truncated" ABORTED = "aborted" + # Indicates a recoverable or non-critical failure during generation (e.g., tool call failure, + # external API error, parsing error). Unlike ABORTED, FAILED samples may still contain partial + # valid output and can be retried or handled gracefully. + FAILED = "failed" status: Status = Status.PENDING diff --git a/miles_plugins/mbridge/qwen3_next.py b/miles_plugins/mbridge/qwen3_next.py index 377ba18f6..8a86dcc57 100644 --- a/miles_plugins/mbridge/qwen3_next.py +++ b/miles_plugins/mbridge/qwen3_next.py @@ -29,8 +29,8 @@ class Qwen3NextBridge(Qwen2MoEBridge): ] } | { - "self_attention.linear_qgkv.layer_norm_weight": ["model.layers.{layer_number}.input_layernorm.weight"], - "self_attention.linear_qgkv.weight": [ + "self_attention.linear_qkv.layer_norm_weight": ["model.layers.{layer_number}.input_layernorm.weight"], + "self_attention.linear_qkv.weight": [ "model.layers.{layer_number}.self_attn.q_proj.weight", "model.layers.{layer_number}.self_attn.k_proj.weight", "model.layers.{layer_number}.self_attn.v_proj.weight", @@ -41,7 +41,7 @@ class Qwen3NextBridge(Qwen2MoEBridge): def _weight_to_mcore_format( self, mcore_weights_name: str, hf_weights: list[torch.Tensor] ) -> tuple[list[str], list[torch.Tensor]]: - if "self_attention.linear_qgkv." in mcore_weights_name and "layer_norm" not in mcore_weights_name: + if "self_attention.linear_qkv." in mcore_weights_name and "layer_norm" not in mcore_weights_name: # merge qkv assert len(hf_weights) == 3 num_key_value_heads = self.hf_config.num_key_value_heads @@ -96,5 +96,6 @@ def _build_config(self): moe_router_pre_softmax=False, qk_layernorm=True, # Qwen3 Next specific - use_gated_attention=True, + attention_output_gate=True, + moe_shared_expert_gate=True, ) diff --git a/miles_plugins/models/glm4.py b/miles_plugins/models/glm4.py index d3e920efd..ba42ea1a6 100644 --- a/miles_plugins/models/glm4.py +++ b/miles_plugins/models/glm4.py @@ -3,11 +3,11 @@ def get_glm_spec(args, config, vp_stage): transformer_layer_spec = get_gpt_layer_with_transformer_engine_spec( - args.num_experts, - args.moe_grouped_gemm, - args.qk_layernorm, - args.multi_latent_attention, - args.moe_use_legacy_grouped_gemm, + num_experts=args.num_experts, + moe_grouped_gemm=args.moe_grouped_gemm, + qk_layernorm=args.qk_layernorm, + multi_latent_attention=args.multi_latent_attention, + moe_use_legacy_grouped_gemm=args.moe_use_legacy_grouped_gemm, post_self_attn_layernorm=args.post_self_attn_layernorm, post_mlp_layernorm=args.post_mlp_layernorm, ) diff --git a/miles_plugins/models/hf_attention.py b/miles_plugins/models/hf_attention.py index 77c15074b..c353ae7b2 100644 --- a/miles_plugins/models/hf_attention.py +++ b/miles_plugins/models/hf_attention.py @@ -22,7 +22,7 @@ def __init__( config, layer_number: int, cp_comm_type: str = "p2p", - model_comm_pgs=None, + pg_collection=None, ): super().__init__(config=config) self.args = args @@ -43,6 +43,7 @@ def forward( rotary_pos_emb: torch.Tensor | tuple[torch.Tensor, torch.Tensor] | None = None, rotary_pos_cos: torch.Tensor | None = None, rotary_pos_sin: torch.Tensor | None = None, + rotary_pos_cos_sin: torch.Tensor | None = None, attention_bias: torch.Tensor | None = None, packed_seq_params: PackedSeqParams | None = None, sequence_len_offset: int | None = None, diff --git a/miles_plugins/models/qwen3_next.py b/miles_plugins/models/qwen3_next.py index 71e94b922..0a42e4d57 100644 --- a/miles_plugins/models/qwen3_next.py +++ b/miles_plugins/models/qwen3_next.py @@ -169,14 +169,14 @@ def __init__( config, layer_number: int, cp_comm_type: str = "p2p", - model_comm_pgs=None, + pg_collection=None, ): super().__init__( args, config, layer_number, cp_comm_type, - model_comm_pgs, + pg_collection, ) if Qwen3NextAttention is None: raise ImportError("Please install transformers>=4.35.0 to use Qwen3NextAttention.") @@ -223,5 +223,4 @@ def get_qwen3_next_spec(args, config, vp_stage): params={"args": args}, ) transformer_layer_spec.layer_specs[layer_id] = layer_specs - transformer_layer_spec.layer_specs[layer_id].submodules.mlp.submodules.shared_experts.params = {"gate": True} return transformer_layer_spec diff --git a/scripts/models/qwen3-1.7B.sh b/scripts/models/qwen3-1.7B.sh index 769e2716e..743599633 100644 --- a/scripts/models/qwen3-1.7B.sh +++ b/scripts/models/qwen3-1.7B.sh @@ -10,7 +10,7 @@ MODEL_ARGS=( --disable-bias-linear --normalization "RMSNorm" --norm-epsilon 1e-6 - --rotary-base 1000000 + --rotary-base "${MODEL_ARGS_ROTARY_BASE:-1000000}" --vocab-size 151936 --kv-channels 128 --qk-layernorm diff --git a/scripts/models/qwen3-235B-A22B.sh b/scripts/models/qwen3-235B-A22B.sh index 40c7ca0ff..32a98679f 100644 --- a/scripts/models/qwen3-235B-A22B.sh +++ b/scripts/models/qwen3-235B-A22B.sh @@ -32,7 +32,7 @@ MODEL_ARGS=( --untie-embeddings-and-output-weights --vocab-size 151936 - --rotary-base 1000000 + --rotary-base "${MODEL_ARGS_ROTARY_BASE:-1000000}" # moe --moe-ffn-hidden-size 1536 diff --git a/scripts/models/qwen3-30B-A3B.sh b/scripts/models/qwen3-30B-A3B.sh index bc8278abc..624623d8f 100644 --- a/scripts/models/qwen3-30B-A3B.sh +++ b/scripts/models/qwen3-30B-A3B.sh @@ -32,7 +32,7 @@ MODEL_ARGS=( --untie-embeddings-and-output-weights --vocab-size 151936 - --rotary-base 1000000 + --rotary-base "${MODEL_ARGS_ROTARY_BASE:-1000000}" # moe --moe-ffn-hidden-size 768 diff --git a/scripts/models/qwen3-8B.sh b/scripts/models/qwen3-8B.sh index c7f21ff9c..fc573adb3 100644 --- a/scripts/models/qwen3-8B.sh +++ b/scripts/models/qwen3-8B.sh @@ -10,7 +10,7 @@ MODEL_ARGS=( --disable-bias-linear --normalization "RMSNorm" --norm-epsilon 1e-6 - --rotary-base 1000000 + --rotary-base "${MODEL_ARGS_ROTARY_BASE:-1000000}" --vocab-size 151936 --kv-channels 128 --qk-layernorm diff --git a/scripts/models/qwen3-next-80B-A3B.sh b/scripts/models/qwen3-next-80B-A3B.sh index 56595d6c1..6e3700b76 100644 --- a/scripts/models/qwen3-next-80B-A3B.sh +++ b/scripts/models/qwen3-next-80B-A3B.sh @@ -51,4 +51,8 @@ MODEL_ARGS=( --moe-router-dtype fp32 --moe-permute-fusion --moe-aux-loss-coeff 0 + + # qwen3 specific + --attention-output-gate + --moe-shared-expert-gate ) \ No newline at end of file diff --git a/scripts/run-qwen3-235B-A22B-sft.sh b/scripts/run-qwen3-235B-A22B-sft.sh index 54598d27d..a2839910d 100644 --- a/scripts/run-qwen3-235B-A22B-sft.sh +++ b/scripts/run-qwen3-235B-A22B-sft.sh @@ -49,6 +49,7 @@ SFT_ARGS=( --rollout-function-path miles.rollout.sft_rollout.generate_rollout --prompt-data ${BASE_FOLDER}/openhermes2_5.parquet --input-key messages + --apply-chat-template --rollout-shuffle --num-epoch 3 --rollout-batch-size 128 diff --git a/scripts/run-qwen3-4B-base-sft.sh b/scripts/run-qwen3-4B-base-sft.sh index 64cd055bd..d5dd181cf 100644 --- a/scripts/run-qwen3-4B-base-sft.sh +++ b/scripts/run-qwen3-4B-base-sft.sh @@ -38,6 +38,7 @@ SFT_ARGS=( --rollout-function-path miles.rollout.sft_rollout.generate_rollout --prompt-data /root/openhermes2_5.parquet --input-key messages + --apply-chat-template --rollout-shuffle --num-epoch 3 --rollout-batch-size 128 diff --git a/scripts/run-qwen3-next-80B-A3B-8gpus.sh b/scripts/run-qwen3-next-80B-A3B-8gpus.sh new file mode 100644 index 000000000..7e36e1944 --- /dev/null +++ b/scripts/run-qwen3-next-80B-A3B-8gpus.sh @@ -0,0 +1,192 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# if base folder not set raise error +if [ -z "${BASE_FOLDER}" ]; then + echo "BASE_FOLDER is not set. Please set it to the base directory of your checkpoints." + exit 1 +fi + +if [ -z "${MASTER_ADDR}" ]; then + echo "MASTER_ADDR is not set. Please set it to the master node address." + exit 1 +fi + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 + +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/models/qwen3-next-80B-A3B.sh" + +CKPT_ARGS=( + --hf-checkpoint ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking + --ref-load ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking_torch_dist + --load ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking_miles/ + --save ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking_miles/ + --save-interval 20 +) + +ROLLOUT_ARGS=( + --prompt-data ${BASE_FOLDER}/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 300 + --rollout-batch-size 16 + --n-samples-per-prompt 4 + --rollout-max-response-len 8192 + --rollout-temperature 0.8 + + --global-batch-size 64 + --balance-data +) + +EVAL_ARGS=( + --eval-interval 20 + --eval-prompt-data aime ${BASE_FOLDER}/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 2 + --eval-max-response-len 16384 + --eval-top-p 0.7 +) + +PERF_ARGS=( + --tensor-model-parallel-size 1 + --sequence-parallel + --pipeline-model-parallel-size 6 + --context-parallel-size 1 + --expert-model-parallel-size 1 + --expert-tensor-parallel-size 1 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + # --micro-batch-size 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 2048 +) + +GRPO_ARGS=( + --advantage-estimator gspo + #--use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 4e-4 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 + + --optimizer-cpu-offload + --overlap-cpu-optimizer-d2h-h2d + --use-precision-aware-optimizer +) + +WANDB_ARGS=( +# --use-wandb +# --wandb-project miles-dev +# --wandb-group qwen3-next-80B-A3B-test +# --wandb-key ${WANDB_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 2 + --rollout-num-gpus 2 + --sglang-mem-fraction-static 0.8 + --sglang-ep-size 1 + + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 128) + + # mtp +# --sglang-speculative-algorithm EAGLE +# --sglang-speculative-num-steps 2 +# --sglang-speculative-eagle-topk 1 +# --sglang-speculative-num-draft-tokens 3 +# --sglang-enable-draft-weights-cpu-backup +# +# --sglang-max-running-requests 512 +) + +MISC_ARGS=( + # default dropout in megatron is 0.1 + --attention-dropout 0.0 + --hidden-dropout 0.0 + # should be good for model performance + --accumulate-allreduce-grads-in-fp32 +# --grad-reduce-in-bf16 + --attention-softmax-in-fp32 + # need to comment this when using model with MLA + --attention-backend flash + + --moe-token-dispatcher-type alltoall +# --moe-enable-deepep +# --debug-rollout-only +) + +# launch the master node of ray in container +export no_proxy="127.0.0.1,${MASTER_ADDR}" +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 +for WORKER_IP in $(awk '{print $1}' /root/mpi_rack_hostfile); do + if [[ "$WORKER_IP" == "$MLP_WORKER_0_HOST" ]]; then + continue + fi + echo "Starting Ray worker on ${WORKER_IP}" + ssh root@"${WORKER_IP}" \ + "pkill -9 sglang ; ray stop --force ; pkill -9 python ; ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 --node-ip-address ${WORKER_IP} --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265" & +done +wait + +# Build the runtime environment JSON with proper variable substitution +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + \"no_proxy\": \"${no_proxy}\", + \"MASTER_ADDR\": \"${MASTER_ADDR}\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 6 \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} diff --git a/scripts/run-qwen3-next-80B-A3B-fsdp.sh b/scripts/run-qwen3-next-80B-A3B-fsdp.sh new file mode 100644 index 000000000..786db35c0 --- /dev/null +++ b/scripts/run-qwen3-next-80B-A3B-fsdp.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# if base folder not set raise error +if [ -z "${BASE_FOLDER}" ]; then + echo "BASE_FOLDER is not set. Please set it to the base directory of your checkpoints." + exit 1 +fi + +if [ -z "${MASTER_ADDR}" ]; then + echo "MASTER_ADDR is not set. Please set it to the master node address." + exit 1 +fi + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 + +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" + +CKPT_ARGS=( + --hf-checkpoint ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking +# --ref-load ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking + --load ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking_miles/ + --save ${BASE_FOLDER}/Qwen3-Next-80B-A3B-Thinking_miles/ + --save-interval 20 +) + + +ROLLOUT_ARGS=( + --prompt-data ${BASE_FOLDER}/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 300 + --rollout-batch-size 4 + --n-samples-per-prompt 3 + --rollout-max-response-len 8192 + --rollout-temperature 0.8 + + --global-batch-size 12 +# --balance-data +) + +EVAL_ARGS=( + --eval-interval 10 + --eval-prompt-data aime ${BASE_FOLDER}/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 1 + --eval-max-response-len 16384 + --eval-top-p 0.7 +) + +PERF_ARGS=( + --micro-batch-size 1 +# --use-dynamic-batch-size + --max-tokens-per-gpu 1 +) + +GRPO_ARGS=( + --advantage-estimator gspo + #--use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 4e-4 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +WANDB_ARGS=( +# --use-wandb +# --wandb-project miles-dev +# --wandb-group qwen3-next-80B-A3B-test +# --wandb-key ${WANDB_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 2 + --rollout-num-gpus 2 + --sglang-mem-fraction-static 0.8 + --sglang-ep-size 1 + + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 128) + + # mtp +# --sglang-speculative-algorithm EAGLE +# --sglang-speculative-num-steps 2 +# --sglang-speculative-eagle-topk 1 +# --sglang-speculative-num-draft-tokens 3 +# --sglang-enable-draft-weights-cpu-backup +# +# --sglang-max-running-requests 512 +) + +TRAIN_BACKEND_ARGS=( + --train-backend fsdp +# --update-weight-buffer-size 536870912 + --gradient-checkpointing +# --fp16 +# --attn-implementation flash_attention_3 + --train-env-vars '{"PYTORCH_CUDA_ALLOC_CONF":"expandable_segments:True"}' +) + +MISC_ARGS=( + # default dropout in megatron is 0.1 +# --accumulate-allreduce-grads-in-fp32 +# --attention-softmax-in-fp32 + # need to comment this when using model with MLA + +# --moe-enable-deepep +# --debug-train-only +) + +# launch the master node of ray in container +export no_proxy="127.0.0.1,${MASTER_ADDR}" +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 +for WORKER_IP in $(awk '{print $1}' /root/mpi_rack_hostfile); do + if [[ "$WORKER_IP" == "$MLP_WORKER_0_HOST" ]]; then + continue + fi + echo "Starting Ray worker on ${WORKER_IP}" + ssh root@"${WORKER_IP}" \ + "pkill -9 sglang ; ray stop --force ; pkill -9 python ; ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 --node-ip-address ${WORKER_IP} --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265" & +done +wait + +# Build the runtime environment JSON with proper variable substitution +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/:${SCRIPT_DIR}\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + \"no_proxy\": \"${no_proxy}\", + \"MASTER_ADDR\": \"${MASTER_ADDR}\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 6 \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${TRAIN_BACKEND_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} diff --git a/tools/convert_fsdp_to_hf.py b/tools/convert_fsdp_to_hf.py new file mode 100644 index 000000000..142730c81 --- /dev/null +++ b/tools/convert_fsdp_to_hf.py @@ -0,0 +1,178 @@ +import argparse +import os +import pickle +import shutil +import time + +import torch +import torch.distributed.checkpoint as dist_cp +from transformers import AutoConfig, AutoModelForCausalLM +from typing_extensions import override + + +class UnpicklerWrapper(pickle.Unpickler): + @override + def find_class(self, mod_name, name): + class DummyClass: + def __init__(self, *args, **kwargs): + pass + + if mod_name.startswith("megatron") or mod_name.startswith("glm"): + return DummyClass + return super().find_class(mod_name, name) + + +class WrappedStorageReader(dist_cp.FileSystemReader): + @override + def read_metadata(self): + path = self.fs.concat_path(self.path, ".metadata") + with self.fs.create_stream(path, "rb") as metadata_file: + metadata = UnpicklerWrapper(metadata_file).load() + if getattr(metadata, "storage_meta", None) is None: + metadata.storage_meta = dist_cp.StorageMeta() + metadata.storage_meta.load_id = self.load_id + if metadata.planner_data is None: + metadata.planner_data = {} + return metadata + + +class EmptyStateDictLoadPlanner(dist_cp.default_planner.DefaultLoadPlanner): + @override + def set_up_planner( + self, + state_dict: dist_cp.metadata.STATE_DICT_TYPE, + metadata: dist_cp.metadata.Metadata | None = None, + is_coordinator: bool = False, + ) -> None: + for k, v in metadata.state_dict_metadata.items(): + if "optimizer" in k: + continue + print(f"find {k} in torch_dist ckpt") + if isinstance(v, dist_cp.metadata.TensorStorageMetadata): + v = torch.empty(v.size, dtype=v.properties.dtype) # type: ignore[assignment] + state_dict[k] = v + super().set_up_planner(state_dict, metadata, is_coordinator) + + +def _detect_model_dir(input_dir: str) -> str: + model_dir = os.path.join(input_dir, "model") + return model_dir if os.path.isdir(model_dir) else input_dir + + +def _load_fsdp_state_dict(input_dir: str) -> dict[str, torch.Tensor]: + state_dict: dict[str, torch.Tensor] = {} + dist_cp.state_dict_loader._load_state_dict( + state_dict, + storage_reader=WrappedStorageReader(input_dir), + planner=EmptyStateDictLoadPlanner(), + no_dist=True, + ) + return state_dict + + +def _get_candidate_prefixes(keys: list[str]) -> list[str]: + predefined = [ + "model_state.model.", + "model_state.", + "model.", + "module.", + "", + ] + + detected: set[str] = set() + for key in keys: + for prefix in predefined: + if prefix and key.startswith(prefix): + detected.add(prefix) + + # Always keep empty string as a fall back option for exact match. + detected.add("") + # Preserve predefined order while keeping only detected prefixes. + return [p for p in predefined if p in detected] + + +def _strip_best_prefix(keys: list[str], target_keys: set[str]) -> tuple[str, int]: + best_prefix = "" + best_match = -1 + + for prefix in _get_candidate_prefixes(keys): + mapped_keys = {k.removeprefix(prefix) for k in keys} + match_count = len(mapped_keys & target_keys) + if match_count > best_match: + best_match = match_count + best_prefix = prefix + + return best_prefix, best_match + + +def _convert_fsdp_to_hf( + origin_hf_dir: str, + input_dir: str, + output_dir: str, +) -> None: + print(f"loading FSDP model from {input_dir}") + t = time.time() + state_dict = _load_fsdp_state_dict(input_dir) + print(f"FSDP model loaded in {time.time()-t:.2f} sec.") + + tensor_items = {k: v for k, v in state_dict.items() if isinstance(v, torch.Tensor)} + + config = AutoConfig.from_pretrained(origin_hf_dir, trust_remote_code=True) + hf_model = AutoModelForCausalLM.from_config(config) + target_keys = set(hf_model.state_dict().keys()) + + best_prefix, best_match = _strip_best_prefix(list(tensor_items.keys()), target_keys) + total_keys = len(tensor_items) + + print(f"Using prefix '{best_prefix}' for key mapping. " f"Matched {best_match}/{total_keys} parameter keys.") + + model_state = {k.removeprefix(best_prefix): v for k, v in tensor_items.items()} + + if not model_state: + raise ValueError( + "No model weights found in checkpoint. " + "Please pass the checkpoint directory (e.g. iter_xxx or iter_xxx/model)." + ) + + missing, unexpected = hf_model.load_state_dict(model_state, strict=False) + print(f"Missing keys: {missing}\nUnexpected keys: {unexpected}") + + os.makedirs(output_dir, exist_ok=True) + hf_model.save_pretrained(output_dir, safe_serialization=True) + print(f"Model weights saved to {output_dir}") + + +def copy_assets(origin_hf_dir: str, output_dir: str) -> None: + for filename in os.listdir(origin_hf_dir): + if filename == "model.safetensors.index.json" or filename.endswith(".safetensors"): + continue + origin_filename = os.path.join(origin_hf_dir, filename) + if not os.path.isfile(origin_filename): + print(f"Skip {filename}, not a file.") + continue + src, dst = origin_filename, os.path.join(output_dir, filename) + print(f"copy from {src} to {dst}") + shutil.copy(src, dst) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input-dir", type=str, required=True) + parser.add_argument("--output-dir", type=str, required=True) + parser.add_argument( + "--origin-hf-dir", + type=str, + required=True, + help="The original Hugging Face model directory to load config/tokenizer assets.", + ) + parser.add_argument( + "-f", "--force", action="store_true", help="Force overwrite the output directory if it exists." + ) + args = parser.parse_args() + + if os.path.exists(args.output_dir) and not args.force: + raise ValueError(f"Output directory {args.output_dir} already exists. Use --force to overwrite it.") + + model_dir = _detect_model_dir(args.input_dir) + _convert_fsdp_to_hf(args.origin_hf_dir, model_dir, args.output_dir) + copy_assets(args.origin_hf_dir, args.output_dir) diff --git a/tools/convert_hf_to_torch_dist.py b/tools/convert_hf_to_torch_dist.py index 10faa6824..d6fddf386 100644 --- a/tools/convert_hf_to_torch_dist.py +++ b/tools/convert_hf_to_torch_dist.py @@ -11,7 +11,7 @@ import miles_plugins.mbridge # noqa: F401 from mbridge import AutoBridge -from miles.backends.megatron_utils import set_default_megatron_args +from miles.backends.megatron_utils.arguments import set_default_megatron_args from miles.backends.megatron_utils.initialize import init from miles.backends.megatron_utils.model_provider import get_model_provider_func from miles.utils.logging_utils import configure_logger @@ -21,6 +21,12 @@ def add_convertion_args(parser): """Add conversion arguments to the parser""" parser.add_argument("--hf-checkpoint", type=str, required=True, help="HuggingFace model path") + parser.add_argument( + "--megatron-to-hf-mode", + choices=["raw", "bridge"], + default="raw", + help="The method to convert megatron weights to hugging face weights for SGLang.", + ) try: parser.add_argument("--padded-vocab-size", type=int, default=None) except Exception: diff --git a/train.py b/train.py index 9fb480eda..e5f6da596 100644 --- a/train.py +++ b/train.py @@ -62,7 +62,7 @@ def onload_rollout(): # train loop. # note that for async training, one can change the position of the sync operation(ray.get). for rollout_id in range(args.start_rollout_id, args.num_rollout): - if args.eval_interval is not None and rollout_id == 0: + if args.eval_interval is not None and rollout_id == 0 and not args.skip_eval_before_train: ray.get(rollout_manager.eval.remote(rollout_id)) rollout_data_ref = ray.get(rollout_manager.generate.remote(rollout_id)) @@ -78,11 +78,11 @@ def onload_rollout(): else: ray.get(actor_model.async_train(rollout_id, rollout_data_ref)) - if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch): + if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch, args.num_rollout): if (not args.use_critic) or (rollout_id >= args.num_critic_only_steps): - actor_model.save_model(rollout_id) + actor_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) if args.use_critic: - critic_model.save_model(rollout_id) + critic_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) if args.rollout_global_dataset: ray.get(rollout_manager.save.remote(rollout_id)) diff --git a/train_async.py b/train_async.py index a43464aaf..24f471887 100644 --- a/train_async.py +++ b/train_async.py @@ -47,10 +47,10 @@ def train(args): else: ray.get(actor_model.async_train(rollout_id, rollout_data_curr_ref)) - if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch): - actor_model.save_model(rollout_id) + if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch, args.num_rollout): + actor_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) if args.use_critic: - critic_model.save_model(rollout_id) + critic_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) if args.rollout_global_dataset: ray.get(rollout_manager.save.remote(rollout_id)) From 9dfd3392471b72ed97054d70980fd0409612fbe8 Mon Sep 17 00:00:00 2001 From: miles-code-angel Date: Sat, 3 Jan 2026 20:09:56 -0800 Subject: [PATCH 06/57] update code (#385) --- docker/patch/latest/sglang.patch | 26 + docs/en/get_started/quick_start.md | 2 +- miles/backends/sglang_utils/sglang_engine.py | 7 +- miles/ray/actor_group.py | 4 +- miles/ray/placement_group.py | 18 +- miles/ray/rollout.py | 9 +- miles/router/middleware_hub/radix_tree.py | 5 - .../middleware_hub/radix_tree_middleware.py | 4 +- miles/utils/data.py | 24 +- scripts/run-qwen3-4B-base-sft.sh | 2 +- tests/test_chunked_gae.py | 63 ++ tests/test_fsdp_gptoss_20b.sh | 130 ++++ tests/test_fsdp_import.py | 9 + tests/test_fused_experts_backward.py | 593 ++++++++++++++++++ tests/test_gspo.sh | 79 +++ tests/test_mimo_7B_mtp_only_grad.py | 142 +++++ tests/test_moonlight_16B_A3B.py | 125 ++++ tests/test_quick_start_glm4_9B.py | 5 + tests/test_qwen2.5_0.5B_gsm8k.py | 5 + tests/test_qwen2.5_0.5B_gsm8k_async.py | 5 + tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py | 5 + tests/test_qwen3_0.6B_fsdp_distributed.py | 5 + tests/test_qwen3_0.6B_parallel_check.py | 138 ++++ tests/test_qwen3_30B_A3B.py | 24 +- tests/test_qwen3_4B_ckpt.py | 139 ++++ tests/test_qwen3_4B_fsdp_true_on_policy.py | 114 ++++ tests/test_qwen3_4B_ppo.py | 135 ++++ tests/test_qwen3_vl_4B_fsdp.py | 111 ++++ tests/utils/test_mask_utils.py | 99 +++ 29 files changed, 1990 insertions(+), 37 deletions(-) create mode 100644 tests/test_chunked_gae.py create mode 100644 tests/test_fsdp_gptoss_20b.sh create mode 100644 tests/test_fsdp_import.py create mode 100644 tests/test_fused_experts_backward.py create mode 100644 tests/test_gspo.sh create mode 100644 tests/test_mimo_7B_mtp_only_grad.py create mode 100644 tests/test_moonlight_16B_A3B.py create mode 100644 tests/test_qwen3_0.6B_parallel_check.py create mode 100644 tests/test_qwen3_4B_ckpt.py create mode 100644 tests/test_qwen3_4B_fsdp_true_on_policy.py create mode 100644 tests/test_qwen3_4B_ppo.py create mode 100644 tests/test_qwen3_vl_4B_fsdp.py create mode 100644 tests/utils/test_mask_utils.py diff --git a/docker/patch/latest/sglang.patch b/docker/patch/latest/sglang.patch index 3522294fc..600706288 100644 --- a/docker/patch/latest/sglang.patch +++ b/docker/patch/latest/sglang.patch @@ -2197,6 +2197,32 @@ index 8e7753dab..15a0823fd 100644 parser.add_argument( "--scheduler-recv-interval", type=int, +diff --git a/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py b/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +index cd9d171fe..71e2f7063 100644 +--- a/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py ++++ b/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +@@ -335,6 +335,10 @@ class EAGLEDraftCudaGraphRunner: + self.seq_lens.fill_(self.seq_len_fill_value) + self.out_cache_loc.zero_() + self.positions.zero_() ++ self.topk_p.zero_() ++ self.topk_index.zero_() ++ self.hidden_states.zero_() ++ self.req_pool_indices.zero_() + + num_tokens = bs * self.num_tokens_per_bs + +@@ -344,8 +348,8 @@ class EAGLEDraftCudaGraphRunner: + forward_batch.out_cache_loc + ) + self.positions[:raw_num_token].copy_(forward_batch.positions) +- self.topk_p[:raw_bs].copy_(forward_batch.spec_info.topk_p) +- self.topk_index[:raw_bs].copy_(forward_batch.spec_info.topk_index) ++ self.topk_p[:raw_bs].copy_(forward_batch.spec_info.topk_p.clamp(0, 1)) ++ self.topk_index[:raw_bs].copy_(forward_batch.spec_info.topk_index.clamp(0, self.model_runner.model_config.vocab_size - 1)) + self.hidden_states[:raw_bs].copy_(forward_batch.spec_info.hidden_states) + self.req_pool_indices[:raw_bs].copy_(forward_batch.req_pool_indices) + diff --git a/python/sglang/srt/speculative/eagle_info.py b/python/sglang/srt/speculative/eagle_info.py index b3d72df05..09a1634e0 100644 --- a/python/sglang/srt/speculative/eagle_info.py diff --git a/docs/en/get_started/quick_start.md b/docs/en/get_started/quick_start.md index d00fbb67d..08c41caf2 100644 --- a/docs/en/get_started/quick_start.md +++ b/docs/en/get_started/quick_start.md @@ -39,7 +39,7 @@ docker run --rm --gpus all --ipc=host --shm-size=16g \ ### Install miles -miles is already installed in the docker image. To update to the latest version, please execute the following command: +miles is already installed in the docker image. To update to the latest verison, please execute the following command: ```bash # Path can be adjusted according to actual situation diff --git a/miles/backends/sglang_utils/sglang_engine.py b/miles/backends/sglang_utils/sglang_engine.py index 479197b9f..2995bbc32 100644 --- a/miles/backends/sglang_utils/sglang_engine.py +++ b/miles/backends/sglang_utils/sglang_engine.py @@ -88,10 +88,11 @@ def _wait_server_healthy(base_url, api_key, is_process_alive): class SGLangEngine(RayActor): - def __init__(self, args, rank: int, worker_type: str = "regular"): + def __init__(self, args, rank: int, worker_type: str = "regular", base_gpu_id: int | None = None): self.args = args self.rank = rank self.worker_type = worker_type + self.base_gpu_id = base_gpu_id def init(self, dist_init_addr, port, nccl_port, host=None, disaggregation_bootstrap_port=None): self.router_ip = self.args.sglang_router_ip @@ -122,6 +123,7 @@ def _format_v6_uri(addr): port, self.worker_type, disaggregation_bootstrap_port, + base_gpu_id=self.base_gpu_id, ) self.node_rank = server_args_dict["node_rank"] @@ -424,6 +426,7 @@ def _compute_server_args( port, worker_type: str = "regular", disaggregation_bootstrap_port: int | None = None, + base_gpu_id: int | None = None, ): nnodes = max(1, args.rollout_num_gpus_per_engine // args.num_gpus_per_node) node_rank = rank % nnodes @@ -441,7 +444,7 @@ def _compute_server_args( "node_rank": node_rank, "dist_init_addr": dist_init_addr, "gpu_id_step": 1, - "base_gpu_id": get_base_gpu_id(args, rank), + "base_gpu_id": base_gpu_id if base_gpu_id is not None else get_base_gpu_id(args, rank), # parallel "tp_size": args.rollout_num_gpus_per_engine, "dp_size": args.sglang_dp_size, diff --git a/miles/ray/actor_group.py b/miles/ray/actor_group.py index 30a82b891..0b11df312 100644 --- a/miles/ray/actor_group.py +++ b/miles/ray/actor_group.py @@ -31,7 +31,7 @@ def __init__( args, num_nodes, num_gpus_per_node, - pg: tuple[PlacementGroup, list[int]], + pg: tuple[PlacementGroup, list[int], list[int]], num_gpus_per_actor: float = 1, role: str = "actor", ) -> None: @@ -48,7 +48,7 @@ def _allocate_gpus_for_actor(self, pg, num_gpus_per_actor): # Use placement group to lock resources for models of same type assert pg is not None - pg, reordered_bundle_indices = pg + pg, reordered_bundle_indices, _reordered_gpu_ids = pg env_vars = { # because sglang will always set NCCL_CUMEM_ENABLE to 0 diff --git a/miles/ray/placement_group.py b/miles/ray/placement_group.py index b6fb7a20b..78639b62c 100644 --- a/miles/ray/placement_group.py +++ b/miles/ray/placement_group.py @@ -60,7 +60,11 @@ def _create_placement_group(num_gpus): ray.kill(actor) bundle_infos = [(i, gpu_ids[i][0], gpu_ids[i][1]) for i in range(num_bundles)] - pg_reordered_bundle_indices = [bundle_info[0] for bundle_info in sorted(bundle_infos, key=sort_key)] + sorted_bundle_infos = sorted(bundle_infos, key=sort_key) + pg_reordered_bundle_indices = [info[0] for info in sorted_bundle_infos] + # Map from logical index -> physical GPU ID + pg_reordered_gpu_ids = [gpu_ids[info[0]][1] for info in sorted_bundle_infos] + for i in range(num_bundles): actual_bundle_index = pg_reordered_bundle_indices[i] logger.info( @@ -68,7 +72,7 @@ def _create_placement_group(num_gpus): f"node: {gpu_ids[actual_bundle_index][0]}, gpu: {gpu_ids[actual_bundle_index][1]}" ) - return pg, pg_reordered_bundle_indices + return pg, pg_reordered_bundle_indices, pg_reordered_gpu_ids def create_placement_groups(args): @@ -99,16 +103,18 @@ def create_placement_groups(args): rollout_offset += args.critic_num_nodes * args.critic_num_gpus_per_node logger.info(f"Creating placement group with {num_gpus} GPUs...") - pg, actor_pg_reordered_bundle_indices = _create_placement_group(num_gpus) + pg, actor_pg_reordered_bundle_indices, actor_pg_reordered_gpu_ids = _create_placement_group(num_gpus) rollout_pg_reordered_bundle_indices = actor_pg_reordered_bundle_indices[rollout_offset:] + rollout_pg_reordered_gpu_ids = actor_pg_reordered_gpu_ids[rollout_offset:] if args.use_critic: critic_pg_reordered_bundle_indices = actor_pg_reordered_bundle_indices[critic_offset:] + critic_pg_reordered_gpu_ids = actor_pg_reordered_gpu_ids[critic_offset:] return { - "actor": (pg, actor_pg_reordered_bundle_indices), - "critic": (pg, critic_pg_reordered_bundle_indices) if args.use_critic else None, - "rollout": (pg, rollout_pg_reordered_bundle_indices), + "actor": (pg, actor_pg_reordered_bundle_indices, actor_pg_reordered_gpu_ids), + "critic": (pg, critic_pg_reordered_bundle_indices, critic_pg_reordered_gpu_ids) if args.use_critic else None, + "rollout": (pg, rollout_pg_reordered_bundle_indices, rollout_pg_reordered_gpu_ids), } diff --git a/miles/ray/rollout.py b/miles/ray/rollout.py index 40338f7e5..7b3f8ce76 100644 --- a/miles/ray/rollout.py +++ b/miles/ray/rollout.py @@ -142,7 +142,7 @@ def check_weights(self, action: str): def _get_rollout_data(self, rollout_id): if self.args.load_debug_rollout_data: data = torch.load( - open(self.args.load_debug_rollout_data.format(rollout_id=rollout_id), "rb"), + self.args.load_debug_rollout_data.format(rollout_id=rollout_id), weights_only=False, )["samples"] data = [Sample.from_dict(sample) for sample in data] @@ -349,7 +349,7 @@ def init_rollout_engines(args, pg, all_rollout_engines): num_engines > prefill_num_servers ), f"num_engines {num_engines} should be larger than prefill_num_servers {prefill_num_servers}" - pg, reordered_bundle_indices = pg + pg, reordered_bundle_indices, reordered_gpu_ids = pg RolloutRayActor = ray.remote(SGLangEngine) @@ -361,6 +361,9 @@ def init_rollout_engines(args, pg, all_rollout_engines): num_gpus = 0.2 num_cpus = num_gpus + # Get the base GPU ID from placement group + base_gpu_id = int(reordered_gpu_ids[i * num_gpu_per_engine]) + scheduling_strategy = PlacementGroupSchedulingStrategy( placement_group=pg, placement_group_capture_child_tasks=True, @@ -392,7 +395,7 @@ def init_rollout_engines(args, pg, all_rollout_engines): runtime_env={ "env_vars": env_vars, }, - ).remote(args, rank=i, worker_type=worker_type) + ).remote(args, rank=i, worker_type=worker_type, base_gpu_id=base_gpu_id) rollout_engines.append((i, rollout_engine)) all_rollout_engines[i] = rollout_engine diff --git a/miles/router/middleware_hub/radix_tree.py b/miles/router/middleware_hub/radix_tree.py index 3c5dc769a..6e722f1e2 100644 --- a/miles/router/middleware_hub/radix_tree.py +++ b/miles/router/middleware_hub/radix_tree.py @@ -621,11 +621,6 @@ def retrieve_from_text(self, text: str, return_logprob: bool = True): # Create trie instance for testing trie = StringRadixTrie(max_cache_size=100, verbose=True) - # Test token retrieval - print("\nTesting token retrieval:") - test_tokens = trie.retrieve_from_text("Hello world") - print(f"Tokens for 'Hello world': {test_tokens}") - # Example usage with simplified insert test_cases = [ ("Hello world", [1, 2, 3], [-0.1, -0.2, -0.3]), diff --git a/miles/router/middleware_hub/radix_tree_middleware.py b/miles/router/middleware_hub/radix_tree_middleware.py index a173cd0d6..961e19d53 100644 --- a/miles/router/middleware_hub/radix_tree_middleware.py +++ b/miles/router/middleware_hub/radix_tree_middleware.py @@ -1,5 +1,5 @@ +import asyncio import json -from time import sleep from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware @@ -108,7 +108,7 @@ async def dispatch(self, request: Request, call_next): ): break # await 30 seconds for aborted responses - sleep(30) + await asyncio.sleep(30) if isinstance(response_data, dict) and "text" in response_data and "output_ids" in response_data: generated_text = response_data["text"] diff --git a/miles/utils/data.py b/miles/utils/data.py index fea0d4c46..f1a8a9586 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -78,15 +78,21 @@ def _parse_generalized_path(s: str): return s, None -def _should_skip_prompt(formatted_prompt: str, tokenizer, processor, max_length, multimodal_inputs=None): +def _should_skip_prompt(output_prompt: str | list, tokenizer, processor, max_length, multimodal_inputs=None): if max_length is None: return False + if isinstance(output_prompt, list): + logger.warning( + "Skipping max_length check for list prompt. Set apply_chat_template=True to enable length filtering." + ) + return False + if processor: - processor_output = processor(text=formatted_prompt, **multimodal_inputs) + processor_output = processor(text=output_prompt, **multimodal_inputs) input_ids = processor_output["input_ids"][0] else: - input_ids = tokenizer.encode(formatted_prompt, add_special_tokens=False) + input_ids = tokenizer.encode(output_prompt, add_special_tokens=False) return len(input_ids) > max_length @@ -102,7 +108,6 @@ def _build_messages(data: dict, prompt_key: str, as_conversation: bool, multimod prompt = [{"role": "user", "content": prompt}] if multimodal_keys: - assert as_conversation, "as_conversation must be True when multimodal_keys is not None" # Build mapping: placeholder -> (MultimodalType, content_list) multimodals = {} for type_name, data_key in multimodal_keys.items(): @@ -164,7 +169,8 @@ def __init__( ): self.origin_samples = [] for data in read_file(path): - as_conversation = apply_chat_template + # Both chat templates and multimodal inputs require conversation format (list of message dicts) + as_conversation = apply_chat_template or (multimodal_keys is not None) prompt = _build_messages(data, prompt_key, as_conversation, multimodal_keys) metadata = data.get(metadata_key) or {} @@ -179,7 +185,7 @@ def __init__( metadata["tools"] = tools if apply_chat_template: - formatted_prompt = tokenizer.apply_chat_template( + output_prompt = tokenizer.apply_chat_template( prompt, tools=tools, tokenize=False, @@ -187,7 +193,7 @@ def __init__( **(apply_chat_template_kwargs or {}), ) else: - formatted_prompt = prompt + output_prompt = prompt if processor: from miles.utils.processing_utils import process_vision_info @@ -200,12 +206,12 @@ def __init__( multimodal_inputs = None # TODO: this is slow. - if _should_skip_prompt(formatted_prompt, tokenizer, processor, max_length, multimodal_inputs): + if _should_skip_prompt(output_prompt, tokenizer, processor, max_length, multimodal_inputs): continue self.origin_samples.append( Sample( - prompt=formatted_prompt, + prompt=output_prompt, label=data[label_key] if label_key is not None else None, metadata=metadata, multimodal_inputs=multimodal_inputs, diff --git a/scripts/run-qwen3-4B-base-sft.sh b/scripts/run-qwen3-4B-base-sft.sh index d5dd181cf..6086313e0 100644 --- a/scripts/run-qwen3-4B-base-sft.sh +++ b/scripts/run-qwen3-4B-base-sft.sh @@ -38,7 +38,7 @@ SFT_ARGS=( --rollout-function-path miles.rollout.sft_rollout.generate_rollout --prompt-data /root/openhermes2_5.parquet --input-key messages - --apply-chat-template + # --apply-chat-template --rollout-shuffle --num-epoch 3 --rollout-batch-size 128 diff --git a/tests/test_chunked_gae.py b/tests/test_chunked_gae.py new file mode 100644 index 000000000..6640df8c2 --- /dev/null +++ b/tests/test_chunked_gae.py @@ -0,0 +1,63 @@ +import time +import pytest +import torch + +from miles.utils.ppo_utils import chunked_gae, vanilla_gae + + +@pytest.mark.parametrize( + "B,T", + [ + (16, 4096), + (32, 8192), + (256, 128 * 1024), + ], +) +@pytest.mark.parametrize("chunk_size", [64, 128, 256]) +def test_gae_parallel_matches_serial(B, T, chunk_size): + """ + Test that chunked_gae (parallel-scan) matches vanilla_gae (batch-serial) + under various shapes, chunk sizes and dtypes. + """ + device = "cuda" if torch.cuda.is_available() else "cpu" + torch.manual_seed(0) + + rewards = torch.randn(B, T, device=device, dtype=torch.float32) + values = torch.randn(B, T, device=device, dtype=torch.float32) + + gamma, lam = 0.99, 0.95 + + # ---------- Serial ---------- + if device == "cuda": + torch.cuda.synchronize() + t0 = time.time() + adv_s, ret_s = vanilla_gae(rewards, values, gamma, lam) + if device == "cuda": + torch.cuda.synchronize() + t1 = time.time() + serial_time = t1 - t0 + + # ---------- Parallel-scan ---------- + if device == "cuda": + torch.cuda.synchronize() + t0 = time.time() + adv_p, ret_p = chunked_gae(rewards, values, gamma, lam, chunk_size=chunk_size) + if device == "cuda": + torch.cuda.synchronize() + t1 = time.time() + parallel_time = t1 - t0 + + # ---------- Accuracy ---------- + adv_err = (adv_s - adv_p).abs().max().item() + ret_err = (ret_s - ret_p).abs().max().item() + + atol = 1e-5 + assert adv_err < atol, f"adv error too large: {adv_err}" + assert ret_err < atol, f"ret error too large: {ret_err}" + + # ---------- logging ---------- + print(f"\n[GAE Test] B={B}, T={T}, chunk={chunk_size}") + print(f" Serial : {serial_time:.6f} s") + print(f" Parallel : {parallel_time:.6f} s") + print(f" Speedup : x{serial_time / parallel_time:.2f}") + print(f" Max diff adv={adv_err:.3e}, ret={ret_err:.3e}") diff --git a/tests/test_fsdp_gptoss_20b.sh b/tests/test_fsdp_gptoss_20b.sh new file mode 100644 index 000000000..90cab2317 --- /dev/null +++ b/tests/test_fsdp_gptoss_20b.sh @@ -0,0 +1,130 @@ +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# ref link in verl: https://github.com/volcengine/verl/pull/3212/files +cat > convert_model.py << EOF +import torch +import os +from transformers import AutoModelForCausalLM, AutoTokenizer, Mxfp4Config + +model_id = "openai/gpt-oss-20b" +output_dir = "/root/models/gpt-oss-20b-bf16" + +if os.path.exists(output_dir): + print(f"Model already exists at {output_dir}, skipping conversion.") +else: + print(f"Converting model from {model_id} to {output_dir}...") + + quantization_config = Mxfp4Config(dequantize=True) + model_kwargs = dict( + attn_implementation="eager", + torch_dtype=torch.bfloat16, + quantization_config=quantization_config, + use_cache=False, + device_map="auto", + ) + + model = AutoModelForCausalLM.from_pretrained(model_id, **model_kwargs) + + # Patch config + model.config.attn_implementation = "eager" + + model.save_pretrained(output_dir) + tokenizer = AutoTokenizer.from_pretrained(model_id) + tokenizer.save_pretrained(output_dir) + print("Conversion done.") +EOF + +python3 convert_model.py + + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 +export CUDA_VISIBLE_DEVICES=4,5,6,7 + +CKPT_ARGS=( + --hf-checkpoint /root/models/gpt-oss-20b-bf16 +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 1000 + --rollout-batch-size 4 + --n-samples-per-prompt 4 + --rollout-max-response-len 2048 + --rollout-temperature 0.8 + + --global-batch-size 16 +) + +GRPO_ARGS=( + --advantage-estimator grpo + # --use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +SGLANG_ARGS=( + # Set equal to the number of GPUs per node for colocated mode + --rollout-num-gpus-per-engine 4 + --sglang-tensor-parallel-size 1 + --sglang-dtype bfloat16 + --sglang-decode-log-interval 1000 +) + + +WANDB_ARGS=( + --use-wandb + --wandb-project "miles-fsdp-gpt" + --wandb-group "20b-bf16" + --wandb-key ${WANDB_API_KEY} +) + +# launch the master node of ray in container +ray start --head --node-ip-address 127.0.0.1 --num-gpus 4 --disable-usage-stats + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json='{ + "env_vars": { + "no_proxy": "localhost,127.0.0.1,0.0.0.0,${MASTER_ADDR}" + } + }' \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 4 \ + --colocate \ + --train-backend fsdp \ + --bf16 \ + --attn-implementation eager \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${WANDB_ARGS[@]} \ diff --git a/tests/test_fsdp_import.py b/tests/test_fsdp_import.py new file mode 100644 index 000000000..66b6861ed --- /dev/null +++ b/tests/test_fsdp_import.py @@ -0,0 +1,9 @@ +import pytest + + +def test_fsdp_import(): + try: + from torch.distributed.fsdp import FullyShardedDataParallel as FSDP + except ImportError: + pytest.skip("FSDP not available in this environment") + assert FSDP is not None diff --git a/tests/test_fused_experts_backward.py b/tests/test_fused_experts_backward.py new file mode 100644 index 000000000..e2a94897b --- /dev/null +++ b/tests/test_fused_experts_backward.py @@ -0,0 +1,593 @@ +""" +Test script to compare Triton backward implementation with Python backward implementation. + +This test compares: +1. Triton implementation (from fused_experts.py) - uses invoke_fused_moe_backward_kernel +2. Python reference implementation (defined in this file) - uses pure PyTorch operations +""" + +import pytest +import torch + +# ============================================================================ +# Python Reference Implementation (Pure PyTorch) +# ============================================================================ + + +class GateUpProjFunctionPython(torch.autograd.Function): + @staticmethod + def forward( + ctx, + hidden_states: torch.Tensor, + w1: torch.Tensor, + topk_weights: torch.Tensor, + topk_ids: torch.Tensor, + ): + num_tokens, D_in = hidden_states.shape + E, N, K = w1.shape + assert D_in == K, f"hidden_states dim {D_in} != w1 dim {K}" + + topk = topk_ids.shape[1] + + # Output: (num_tokens * topk, N) + intermediate_cache1 = torch.empty( + (num_tokens * topk, N), + device=hidden_states.device, + dtype=hidden_states.dtype, + ) + + # Python implementation: iterate over tokens and their topk experts + # For each token t and expert k: + # intermediate_cache1[t*topk + k] = hidden_states[t] @ w1[expert_id].T + for t in range(num_tokens): + for k in range(topk): + expert_id = topk_ids[t, k].item() + x_t = hidden_states[t] # shape: (D_in,) + W1_e = w1[expert_id] # shape: (N, K) + intermediate_cache1[t * topk + k] = x_t @ W1_e.T + + ctx.save_for_backward(hidden_states, w1, topk_weights, topk_ids) + ctx.num_tokens = num_tokens + ctx.topk = topk + + return intermediate_cache1 + + @staticmethod + def backward(ctx, grad_output): + """ + Backward pass for GateUpProjFunction - Pure Python implementation. + + Forward: output = input @ w1 (without topk_weight multiplication) + Backward: + - grad_hidden_states = grad_output @ w1 + - grad_w1 = grad_output.T @ input (note: transposed) + - grad_topk_weights = zeros (not needed in this stage) + + Args: + grad_output: shape (num_tokens * topk, N) + + Returns: + (grad_hidden_states, grad_w1, grad_topk_weights, None) + """ + hidden_states, w1, topk_weights, topk_ids = ctx.saved_tensors + topk = ctx.topk + + num_tokens, D_in = hidden_states.shape + E, N, _ = w1.shape + CHUNK_SIZE = 64 * 1024 + + # Initialize gradient tensors + grad_hidden_states = torch.zeros_like(hidden_states) + # Use float32 for grad_w1 accumulation to avoid bfloat16 precision loss + grad_w1 = torch.zeros(w1.shape, dtype=torch.float32, device=w1.device) + # GateUpProj stage doesn't compute topk_weights gradient + grad_topk_weights = torch.zeros_like(topk_weights) + + # Process in chunks to match forward pass + for chunk in range((num_tokens // CHUNK_SIZE) + 1): + begin_chunk_idx, end_chunk_idx = ( + chunk * CHUNK_SIZE, + min((chunk + 1) * CHUNK_SIZE, num_tokens), + ) + + curr_num_tokens = end_chunk_idx - begin_chunk_idx + if curr_num_tokens == 0: + continue + + curr_hidden_states = hidden_states[begin_chunk_idx:end_chunk_idx] + curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx] + curr_grad_output = grad_output[begin_chunk_idx * topk : end_chunk_idx * topk] + + # 1. Calculate grad_hidden_states: grad_output @ w1 + # For each token t and expert k: + # grad_hidden_states[t] += grad_output[t*topk+k] @ w1[expert_id] + for t in range(curr_num_tokens): + for k in range(topk): + expert_id = curr_topk_ids[t, k].item() + grad_y_tk = curr_grad_output[t * topk + k] # shape: (N,) + W1_e = w1[expert_id] # shape: (N, D_in) + # grad_x: (N,) @ (N, D_in) -> (D_in,) + grad_hidden_states[begin_chunk_idx + t] += grad_y_tk @ W1_e + + # 2. Calculate grad_w1: input.T @ grad_output + # For each token t and expert k: + # grad_w1[expert_id] += input[t].T @ grad_output[t*topk+k] + # Which is: grad_w1[expert_id] += outer(grad_output[t*topk+k], input[t]) + for t in range(curr_num_tokens): + for k in range(topk): + expert_id = curr_topk_ids[t, k].item() + x_t = curr_hidden_states[t] # shape: (D_in,) + grad_y_tk = curr_grad_output[t * topk + k] # shape: (N,) + # grad_W1: outer(grad_y_tk, x_t) -> (N, D_in) + # Accumulate in float32 + grad_w1[expert_id] += torch.outer(grad_y_tk, x_t).to(torch.float32) + + # Convert grad_w1 back to original dtype (bfloat16) + grad_w1 = grad_w1.to(hidden_states.dtype) + + return grad_hidden_states, grad_w1, grad_topk_weights, None + + +class DownProjFunctionPython(torch.autograd.Function): + @staticmethod + def forward( + ctx, + intermediate_cache2: torch.Tensor, + w2: torch.Tensor, + topk_weights: torch.Tensor, + topk_ids: torch.Tensor, + ): + total_tokens, intermediate_size = intermediate_cache2.shape + topk = topk_ids.shape[1] + num_tokens = total_tokens // topk + E, hidden_size, K = w2.shape + assert intermediate_size == K, f"intermediate_cache2 dim {intermediate_size} != w2 dim {K}" + + # Output: (num_tokens, topk, hidden_size) + intermediate_cache3 = torch.empty( + (num_tokens, topk, hidden_size), + device=intermediate_cache2.device, + dtype=intermediate_cache2.dtype, + ) + + # Python implementation: iterate over tokens and their topk experts + # For each token t and expert k: + # intermediate_cache3[t, k] = topk_weights[t, k] * (intermediate_cache2[t*topk+k] @ w2[expert_id].T) + for t in range(num_tokens): + for k in range(topk): + expert_id = topk_ids[t, k].item() + x_tk = intermediate_cache2[t * topk + k] # shape: (intermediate_size,) + W2_e = w2[expert_id] # shape: (hidden_size, intermediate_size) + weight_tk = topk_weights[t, k] # scalar + + intermediate_cache3[t, k] = weight_tk * (x_tk @ W2_e.T) + + ctx.save_for_backward(intermediate_cache2, w2, topk_weights, topk_ids) + ctx.num_tokens = num_tokens + ctx.topk = topk + + return intermediate_cache3 + + @staticmethod + def backward(ctx, grad_output): + """ + Backward pass for DownProjFunction - Pure Python implementation. + + Forward: output = topk_weights * (input @ w2) (with topk_weight multiplication) + Backward: + - grad_intermediate_cache2 = topk_weights * (grad_output @ w2) + - grad_w2 = topk_weights * (grad_output.T @ intermediate_cache2) + - grad_topk_weights = dot(grad_output, forward_output_before_weighting) + + Args: + grad_output: shape (num_tokens, topk, hidden_size) + + Returns: + (grad_intermediate_cache2, grad_w2, grad_topk_weights, None) + """ + intermediate_cache2, w2, topk_weights, topk_ids = ctx.saved_tensors + num_tokens = ctx.num_tokens + topk = ctx.topk + + E, hidden_size, intermediate_size = w2.shape + CHUNK_SIZE = 64 * 1024 + + # Initialize gradient tensors + grad_intermediate_cache2 = torch.zeros_like(intermediate_cache2) + # Use float32 for grad_w2 accumulation to avoid bfloat16 precision loss + grad_w2 = torch.zeros(w2.shape, dtype=torch.float32, device=w2.device) + # Compute grad_topk_weights in DownProjFunction backward + grad_topk_weights = torch.zeros_like(topk_weights) + + # Process in chunks to match forward pass + for chunk in range((num_tokens // CHUNK_SIZE) + 1): + begin_chunk_idx, end_chunk_idx = ( + chunk * CHUNK_SIZE, + min((chunk + 1) * CHUNK_SIZE, num_tokens), + ) + + curr_num_tokens = end_chunk_idx - begin_chunk_idx + if curr_num_tokens == 0: + continue + + curr_intermediate_cache2 = intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk] + curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx] + curr_grad_output = grad_output[begin_chunk_idx:end_chunk_idx] + curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx] + + # 1. Calculate grad_intermediate_cache2: topk_weights * (grad_output @ w2) + for t in range(curr_num_tokens): + for k in range(topk): + expert_id = curr_topk_ids[t, k].item() + grad_y_tk = curr_grad_output[t, k] # shape: (hidden_size,) + W2_e = w2[expert_id] # shape: (hidden_size, intermediate_size) + weight_tk = curr_topk_weights[t, k] # scalar + + grad_intermediate_cache2[(begin_chunk_idx + t) * topk + k] = weight_tk * (grad_y_tk @ W2_e) + + # 2. Calculate grad_w2: topk_weights * (grad_output.T @ intermediate_cache2) + for t in range(curr_num_tokens): + for k in range(topk): + expert_id = curr_topk_ids[t, k].item() + grad_y_tk = curr_grad_output[t, k] # shape: (hidden_size,) + x_tk = curr_intermediate_cache2[t * topk + k] # shape: (intermediate_size,) + weight_tk = curr_topk_weights[t, k] # scalar + + # Accumulate in float32 + grad_w2[expert_id] += (weight_tk * torch.outer(grad_y_tk, x_tk)).to(torch.float32) + + # 3. Calculate grad_topk_weights: dot(grad_output, forward_output_before_weighting) + for t in range(curr_num_tokens): + for k in range(topk): + expert_id = curr_topk_ids[t, k].item() + grad_y_tk = curr_grad_output[t, k] # shape: (hidden_size,) + x_tk = curr_intermediate_cache2[t * topk + k] # shape: (intermediate_size,) + W2_e = w2[expert_id] # shape: (hidden_size, intermediate_size) + + # Compute forward output before weighting + forward_output_unweighted = x_tk @ W2_e.T # shape: (hidden_size,) + + # grad_topk_weights: dot product + grad_topk_weights[begin_chunk_idx + t, k] += torch.sum(grad_y_tk * forward_output_unweighted) + + # Convert grad_w2 back to original dtype (bfloat16) + grad_w2 = grad_w2.to(intermediate_cache2.dtype) + + return grad_intermediate_cache2, grad_w2, grad_topk_weights, None + + +# ============================================================================ +# Import Triton Implementation +# ============================================================================ + +from miles.backends.fsdp_utils.kernels.fused_experts import DownProjFunction as DownProjFunctionTriton +from miles.backends.fsdp_utils.kernels.fused_experts import GateUpProjFunction as GateUpProjFunctionTriton + +# ============================================================================ +# Test Fixtures and Utilities +# ============================================================================ + + +@pytest.fixture +def setup_moe_params(): + """Setup MOE parameters for testing.""" + torch.manual_seed(42) + + # Small parameters for easier debugging + num_tokens = 64 + hidden_size = 128 + intermediate_size = 256 + num_experts = 4 + topk = 2 + + device = "cuda" if torch.cuda.is_available() else "cpu" + dtype = torch.bfloat16 + + # Create input tensors with random values for better testing + hidden_states = torch.randn(num_tokens, hidden_size, device=device, dtype=dtype) + + # Create expert weights + w1 = torch.randn(num_experts, intermediate_size * 2, hidden_size, device=device, dtype=dtype) + w2 = torch.randn(num_experts, hidden_size, intermediate_size, device=device, dtype=dtype) + + # Create router outputs + topk_weights = torch.rand(num_tokens, topk, device=device, dtype=dtype) + topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) # normalize + + # Random expert selection + topk_ids = torch.stack([torch.randperm(num_experts, device=device)[:topk] for _ in range(num_tokens)], dim=0).to( + torch.int32 + ) + + return { + "hidden_states": hidden_states, + "w1": w1, + "w2": w2, + "topk_weights": topk_weights, + "topk_ids": topk_ids, + "device": device, + "dtype": dtype, + } + + +# ============================================================================ +# Test Cases +# ============================================================================ + + +class TestGateUpProjBackward: + """Test GateUpProjFunction backward pass comparison.""" + + def test_forward_consistency(self, setup_moe_params): + """Test that Triton and Python implementations produce same forward output.""" + params = setup_moe_params + + # Python implementation + out_python = GateUpProjFunctionPython.apply( + params["hidden_states"].clone(), + params["w1"].clone(), + params["topk_weights"].clone(), + params["topk_ids"].clone(), + ) + + # Triton implementation + out_triton = GateUpProjFunctionTriton.apply( + params["hidden_states"].clone(), + params["w1"].clone(), + params["topk_weights"].clone(), + params["topk_ids"].clone(), + ) + + # Check outputs are close + torch.testing.assert_close(out_python, out_triton, rtol=1, atol=1) + print("✓ GateUpProjFunction forward test passed") + + def test_backward_consistency(self, setup_moe_params): + """Test that Triton and Python implementations produce same gradients.""" + params = setup_moe_params + + # Prepare inputs with requires_grad + hidden_states_python = params["hidden_states"].clone().requires_grad_(True) + w1_python = params["w1"].clone().requires_grad_(True) + topk_weights_python = params["topk_weights"].clone().requires_grad_(True) + topk_ids_python = params["topk_ids"].clone() + + hidden_states_triton = params["hidden_states"].clone().requires_grad_(True) + w1_triton = params["w1"].clone().requires_grad_(True) + topk_weights_triton = params["topk_weights"].clone().requires_grad_(True) + topk_ids_triton = params["topk_ids"].clone() + + # Python implementation + out_python = GateUpProjFunctionPython.apply( + hidden_states_python, + w1_python, + topk_weights_python, + topk_ids_python, + ) + + # Triton implementation + out_triton = GateUpProjFunctionTriton.apply( + hidden_states_triton, + w1_triton, + topk_weights_triton, + topk_ids_triton, + ) + + # Create gradient for backward + grad_output = torch.randn_like(out_python) + + # Backward pass + out_python.backward(grad_output.clone()) + out_triton.backward(grad_output.clone()) + + # Check hidden_states gradients + print("\n" + "=" * 80) + print("GateUpProjFunction Backward - hidden_states gradients:") + print("=" * 80) + if hidden_states_python.grad is not None and hidden_states_triton.grad is not None: + diff = hidden_states_python.grad - hidden_states_triton.grad + max_diff = torch.max(torch.abs(diff)) + print(f"Max absolute difference: {max_diff:.6f}") + torch.testing.assert_close(hidden_states_python.grad, hidden_states_triton.grad, rtol=1, atol=1) + print("✓ hidden_states gradient matches") + print("=" * 80 + "\n") + + # Check w1 gradients + print("\n" + "=" * 80) + print("GateUpProjFunction Backward - w1 gradients:") + print("=" * 80) + if w1_python.grad is not None and w1_triton.grad is not None: + diff = w1_python.grad - w1_triton.grad + max_diff = torch.max(torch.abs(diff)) + print(f"Max absolute difference: {max_diff:.6f}") + torch.testing.assert_close(w1_python.grad, w1_triton.grad, rtol=1, atol=1) + print("✓ w1 gradient matches") + print("=" * 80 + "\n") + + print("✓ GateUpProjFunction backward test passed") + + +class TestDownProjBackward: + """Test DownProjFunction backward pass comparison.""" + + def test_forward_consistency(self, setup_moe_params): + """Test that Triton and Python implementations produce same forward output.""" + params = setup_moe_params + + # Create intermediate input (after SiluAndMul) + num_tokens = params["hidden_states"].shape[0] + topk = params["topk_ids"].shape[1] + intermediate_size = params["w2"].shape[2] + intermediate_cache2 = torch.randn( + num_tokens * topk, intermediate_size, device=params["device"], dtype=params["dtype"] + ) + + # Python implementation + out_python = DownProjFunctionPython.apply( + intermediate_cache2.clone(), + params["w2"].clone(), + params["topk_weights"].clone(), + params["topk_ids"].clone(), + ) + + # Triton implementation + out_triton = DownProjFunctionTriton.apply( + intermediate_cache2.clone(), + params["w2"].clone(), + params["topk_weights"].clone(), + params["topk_ids"].clone(), + ) + + # Check outputs are close + torch.testing.assert_close(out_python, out_triton, rtol=1, atol=1) + print("✓ DownProjFunction forward test passed") + + def test_backward_consistency(self, setup_moe_params): + """Test that Triton and Python implementations produce same gradients.""" + params = setup_moe_params + + # Create intermediate input + num_tokens = params["hidden_states"].shape[0] + topk = params["topk_ids"].shape[1] + intermediate_size = params["w2"].shape[2] + + intermediate_cache2_base = torch.randn( + num_tokens * topk, intermediate_size, device=params["device"], dtype=params["dtype"] + ) + + intermediate_cache2_python = intermediate_cache2_base.clone().requires_grad_(True) + intermediate_cache2_triton = intermediate_cache2_base.clone().requires_grad_(True) + + w2_python = params["w2"].clone().requires_grad_(True) + w2_triton = params["w2"].clone().requires_grad_(True) + + topk_weights_python = params["topk_weights"].clone().requires_grad_(True) + topk_weights_triton = params["topk_weights"].clone().requires_grad_(True) + + # Python implementation + out_python = DownProjFunctionPython.apply( + intermediate_cache2_python, + w2_python, + topk_weights_python, + params["topk_ids"], + ) + + # Triton implementation + out_triton = DownProjFunctionTriton.apply( + intermediate_cache2_triton, + w2_triton, + topk_weights_triton, + params["topk_ids"], + ) + + # Create gradient for backward + grad_output = torch.randn_like(out_python) + + # Backward pass + out_python.backward(grad_output.clone()) + out_triton.backward(grad_output.clone()) + + # Check intermediate_cache2 gradients + print("\n" + "=" * 80) + print("DownProjFunction Backward - intermediate_cache2 gradients:") + print("=" * 80) + if intermediate_cache2_python.grad is not None and intermediate_cache2_triton.grad is not None: + diff = intermediate_cache2_python.grad - intermediate_cache2_triton.grad + max_diff = torch.max(torch.abs(diff)) + print(f"Max absolute difference: {max_diff:.6f}") + torch.testing.assert_close( + intermediate_cache2_python.grad, intermediate_cache2_triton.grad, rtol=1, atol=1 + ) + print("✓ intermediate_cache2 gradient matches") + print("=" * 80 + "\n") + + # Check topk_weights gradients + print("\n" + "=" * 80) + print("DownProjFunction Backward - topk_weights gradients:") + print("=" * 80) + if topk_weights_python.grad is not None and topk_weights_triton.grad is not None: + diff = topk_weights_python.grad - topk_weights_triton.grad + max_diff = torch.max(torch.abs(diff)) + print(f"Max absolute difference: {max_diff:.6f}") + torch.testing.assert_close(topk_weights_python.grad, topk_weights_triton.grad, rtol=1, atol=1) + print("✓ topk_weights gradient matches") + print("=" * 80 + "\n") + + # Check w2 gradients + print("\n" + "=" * 80) + print("DownProjFunction Backward - w2 gradients:") + print("=" * 80) + if w2_python.grad is not None and w2_triton.grad is not None: + diff = w2_python.grad - w2_triton.grad + max_diff = torch.max(torch.abs(diff)) + print(f"Max absolute difference: {max_diff:.6f}") + torch.testing.assert_close(w2_python.grad, w2_triton.grad, rtol=1, atol=1) + print("✓ w2 gradient matches") + print("=" * 80 + "\n") + + print("✓ DownProjFunction backward test passed") + + +# ============================================================================ +# Main Test Runner +# ============================================================================ + + +def run_all_tests(): + """Run all tests.""" + print("=" * 80) + print("Running Fused Experts Backward Tests") + print("Testing: Triton Implementation vs Python Reference") + print("=" * 80) + + if not torch.cuda.is_available(): + print("WARNING: CUDA not available, skipping tests") + return + + # Setup parameters + torch.manual_seed(42) + params_dict = {} + + # Small parameters for testing + num_tokens = 64 + hidden_size = 128 + intermediate_size = 256 + num_experts = 4 + topk = 2 + + device = "cuda" + dtype = torch.bfloat16 + + # Create input tensors + params_dict["hidden_states"] = torch.randn(num_tokens, hidden_size, device=device, dtype=dtype) + params_dict["w1"] = torch.randn(num_experts, intermediate_size * 2, hidden_size, device=device, dtype=dtype) + params_dict["w2"] = torch.randn(num_experts, hidden_size, intermediate_size, device=device, dtype=dtype) + params_dict["topk_weights"] = torch.rand(num_tokens, topk, device=device, dtype=dtype) + params_dict["topk_weights"] = params_dict["topk_weights"] / params_dict["topk_weights"].sum(dim=-1, keepdim=True) + params_dict["topk_ids"] = torch.stack( + [torch.randperm(num_experts, device=device)[:topk] for _ in range(num_tokens)], dim=0 + ).to(torch.int32) + params_dict["device"] = device + params_dict["dtype"] = dtype + + print("\n" + "=" * 80) + print("Testing GateUpProjFunction Backward") + print("=" * 80) + test_gate_up = TestGateUpProjBackward() + test_gate_up.test_forward_consistency(params_dict) + test_gate_up.test_backward_consistency(params_dict) + + print("\n" + "=" * 80) + print("Testing DownProjFunction Backward") + print("=" * 80) + test_down = TestDownProjBackward() + test_down.test_forward_consistency(params_dict) + test_down.test_backward_consistency(params_dict) + + print("\n" + "=" * 80) + print("All Backward Tests Passed! ✓") + print("=" * 80) + + +if __name__ == "__main__": + run_all_tests() diff --git a/tests/test_gspo.sh b/tests/test_gspo.sh new file mode 100644 index 000000000..6e915ca65 --- /dev/null +++ b/tests/test_gspo.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 + +CKPT_ARGS=( + --hf-checkpoint /root/Qwen3-0.6B +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 2 + --rollout-batch-size 4 + --n-samples-per-prompt 4 + --rollout-max-response-len 8192 + --rollout-temperature 0.8 + + --global-batch-size 16 +) + +GSPO_ARGS=( + --advantage-estimator gspo + #--use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 3.5e-4 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 +) + +# launch the master node of ray in container +ray start --head --node-ip-address 127.0.0.1 --num-gpus 4 --disable-usage-stats + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json='{ + "env_vars": { + "no_proxy": "localhost,127.0.0.1,0.0.0.0,${MASTER_ADDR}" + } + }' \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 4 \ + --colocate \ + --train-backend fsdp \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GSPO_ARGS[@]} \ + ${SGLANG_ARGS[@]} diff --git a/tests/test_mimo_7B_mtp_only_grad.py b/tests/test_mimo_7B_mtp_only_grad.py new file mode 100644 index 000000000..bbd9b2e03 --- /dev/null +++ b/tests/test_mimo_7B_mtp_only_grad.py @@ -0,0 +1,142 @@ +"""End-to-end test for MTP-only gradient verification. + +This test verifies that when MTP training is enabled and all outputs are truncated +(due to very short max response length), only MTP parameters receive non-zero +gradients while all other model parameters have zero gradients. + +This validates that the MTP loss computation correctly isolates gradient flow +to only the MTP layers when the main model loss is zero (due to truncation). +""" + +import os + +import miles.utils.external_utils.command_utils as U + + +MODEL_NAME = "MiMo-7B-RL" +MODEL_TYPE = "mimo-7B-rl" +NUM_GPUS = 8 + + +def prepare(): + """Download model and convert checkpoint with MTP layers.""" + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download XiaomiMiMo/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + + # Convert checkpoint with MTP layers enabled + U.convert_checkpoint( + model_name=MODEL_NAME, + megatron_model_type=MODEL_TYPE, + num_gpus_per_node=NUM_GPUS, + extra_args=" --mtp-num-layers 1", + dir_dst="/root/models", + ) + + +def execute(): + """Run training with MTP enabled and very short output length to cause truncation.""" + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/ " f"--ref-load /root/models/{MODEL_NAME}_torch_dist " + + # Use very short rollout-max-response-len to ensure all outputs are truncated + # This should result in zero loss for the main model, leaving only MTP loss + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + "--num-rollout 1 " + "--rollout-batch-size 4 " + "--n-samples-per-prompt 2 " + # Very short max response length to cause all outputs to be truncated + "--rollout-max-response-len 128 " + "--rollout-temperature 0.8 " + "--global-batch-size 8 " + ) + + perf_args = ( + "--tensor-model-parallel-size 2 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 1 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 4096 " + ) + + grpo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 2 " + "--rollout-num-gpus 8 " + "--sglang-mem-fraction-static 0.8 " + "--sglang-enable-metrics " + ) + + # Enable MTP training with loss scaling + mtp_args = "--mtp-num-layers 1 " "--enable-mtp-training " "--mtp-loss-scaling-factor 0.2 " + + ci_args = ( + "--ci-test " + "--ci-disable-kl-checker " + # MTP grad check is automatically triggered when ci_test and enable_mtp_training are both set + ) + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 8 " + "--colocate " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{sglang_args} " + f"{mtp_args} " + f"{ci_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + prepare() + # Remove proxy settings that might interfere with local operations + for key in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]: + os.environ.pop(key, None) + execute() diff --git a/tests/test_moonlight_16B_A3B.py b/tests/test_moonlight_16B_A3B.py new file mode 100644 index 000000000..d41a37477 --- /dev/null +++ b/tests/test_moonlight_16B_A3B.py @@ -0,0 +1,125 @@ +import os +import miles.utils.external_utils.command_utils as U + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) + +MODEL_NAME = "Moonlight-16B-A3B-Instruct" +MODEL_TYPE = "moonlight" +NUM_GPUS = 8 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command( + "hf download moonshotai/Moonlight-16B-A3B-Instruct --local-dir /root/models/Moonlight-16B-A3B-Instruct" + ) + U.hf_download_dataset("zhuzilin/dapo-math-17k") + U.hf_download_dataset("zhuzilin/aime-2024") + + U.convert_checkpoint(model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS) + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " f"--ref-load /root/{MODEL_NAME}_torch_dist " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 4096 " + "--rollout-temperature 1 " + "--global-batch-size 32 " + ) + + eval_args = ( + f"{'--eval-interval 20 ' if ENABLE_EVAL else ''}" + "--eval-prompt-data aime /root/datasets/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 4096 " + "--eval-top-k 1 " + ) + + perf_args = ( + "--tensor-model-parallel-size 2 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 2 " + "--expert-model-parallel-size 8 " + "--expert-tensor-parallel-size 1 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + f"--max-tokens-per-gpu {2048 if TIGHT_HOST_MEMORY else 2048} " + ) + + grpo_args = ( + "--advantage-estimator gspo " + f"{'' if TIGHT_HOST_MEMORY else '--use-kl-loss '}" + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 4e-4 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 2 " "--sglang-mem-fraction-static 0.8 " "--sglang-max-running-requests 512 " + ) + + ci_args = "--ci-test " + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 8 " + "--colocate " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute() diff --git a/tests/test_quick_start_glm4_9B.py b/tests/test_quick_start_glm4_9B.py index f18888c22..cf99af919 100644 --- a/tests/test_quick_start_glm4_9B.py +++ b/tests/test_quick_start_glm4_9B.py @@ -1,3 +1,4 @@ +import os import miles.utils.external_utils.command_utils as U ENABLE_EVAL = U.get_bool_env_var("MILES_TEST_ENABLE_EVAL", "1") @@ -120,4 +121,8 @@ def execute(): if __name__ == "__main__": # TODO also use typer prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") execute() diff --git a/tests/test_qwen2.5_0.5B_gsm8k.py b/tests/test_qwen2.5_0.5B_gsm8k.py index 6302aadb6..31efaa9d4 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k.py +++ b/tests/test_qwen2.5_0.5B_gsm8k.py @@ -1,3 +1,4 @@ +import os import miles.utils.external_utils.command_utils as U @@ -124,4 +125,8 @@ def execute(): if __name__ == "__main__": prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") execute() diff --git a/tests/test_qwen2.5_0.5B_gsm8k_async.py b/tests/test_qwen2.5_0.5B_gsm8k_async.py index 1c55ccb20..a8177263c 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_async.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_async.py @@ -1,3 +1,4 @@ +import os import miles.utils.external_utils.command_utils as U FEW_GPU = U.get_bool_env_var("MILES_TEST_FEW_GPU", "1") @@ -124,4 +125,8 @@ def execute(): if __name__ == "__main__": prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") execute() diff --git a/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py b/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py index 6967f9145..eff8b6583 100644 --- a/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py +++ b/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py @@ -1,3 +1,4 @@ +import os import miles.utils.external_utils.command_utils as U MODEL_NAME = "Qwen3-0.6B" @@ -97,4 +98,8 @@ def execute(): if __name__ == "__main__": prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") execute() diff --git a/tests/test_qwen3_0.6B_fsdp_distributed.py b/tests/test_qwen3_0.6B_fsdp_distributed.py index b3eb416b3..0feecde8e 100644 --- a/tests/test_qwen3_0.6B_fsdp_distributed.py +++ b/tests/test_qwen3_0.6B_fsdp_distributed.py @@ -1,3 +1,4 @@ +import os import miles.utils.external_utils.command_utils as U MODEL_NAME = "Qwen3-0.6B" @@ -99,4 +100,8 @@ def execute(): if __name__ == "__main__": prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") execute() diff --git a/tests/test_qwen3_0.6B_parallel_check.py b/tests/test_qwen3_0.6B_parallel_check.py new file mode 100644 index 000000000..3c9c51204 --- /dev/null +++ b/tests/test_qwen3_0.6B_parallel_check.py @@ -0,0 +1,138 @@ +import os + +import miles.utils.external_utils.command_utils as U + + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) + +MODEL_NAME = "Qwen3-0.6B" +MODEL_TYPE = "qwen3-0.6B" +NUM_GPUS = 8 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + + U.convert_checkpoint( + model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS, dir_dst="/root/models" + ) + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/ " f"--ref-load /root/models/{MODEL_NAME}_torch_dist " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + "--num-rollout 1 " + "--rollout-batch-size 4 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 8192 " + "--rollout-temperature 0.8 " + "--global-batch-size 32 " + ) + + ppo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type k1 " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 4e-4 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = "--rollout-num-gpus-per-engine 2 " "--rollout-num-gpus 8 " "--sglang-mem-fraction-static 0.8 " + + ci_args = "--ci-test " + + misc_args = ( + # default dropout in megatron is 0.1 + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + # should be good for model performance + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + # need to comment this when using model with MLA + "--attention-backend flash " + "--actor-num-nodes 1 " + "--colocate " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{ppo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + for i in range(2): + U.execute_train( + train_args=train_args + + ( + f"--save-debug-rollout-data data-{i}.pt " + f"--ci-save-grad-norm grad_norms-{i}.pt " + f"--actor-num-gpus-per-node {NUM_GPUS} " + ), + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + # 8 GPU CPU 1 + for num_gpus in [8, 4, 2]: + remaining_gpus = num_gpus + for tp_size in [1, 2, 4, 8]: + remaining_gpus /= tp_size + for pp_size in [1, 2, 4]: + if remaining_gpus < pp_size: + continue + remaining_gpus /= pp_size + for cp_size in [1, 2, 4, 8]: + if remaining_gpus < cp_size: + continue + args = train_args + ( + f"--load-debug-rollout-data data-{i}.pt " + f"--ci-load-grad-norm grad_norms-{i}.pt " + f"--context-parallel-size {cp_size} " + f"--tensor-model-parallel-size {tp_size} " + f"--pipeline-model-parallel-size {pp_size} " + "--sequence-parallel " + f"--actor-num-gpus-per-node {num_gpus} " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 8192 " + ) + + U.execute_train( + train_args=args, + num_gpus_per_node=num_gpus, + megatron_model_type=MODEL_TYPE, + ) + train_args += "--calculate-per-token-loss " + + +if __name__ == "__main__": + # TODO also use typer + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute() diff --git a/tests/test_qwen3_30B_A3B.py b/tests/test_qwen3_30B_A3B.py index 6b5f6b889..af832dd4e 100644 --- a/tests/test_qwen3_30B_A3B.py +++ b/tests/test_qwen3_30B_A3B.py @@ -5,6 +5,8 @@ ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) +USE_DEEPEP = bool(int(os.environ.get("MILES_TEST_USE_DEEPEP", "1"))) +USE_FP8_ROLLOUT = bool(int(os.environ.get("MILES_TEST_USE_FP8_ROLLOUT", "1"))) MODEL_NAME = "Qwen3-30B-A3B" MODEL_TYPE = "qwen3-30B-A3B" @@ -22,7 +24,10 @@ def prepare(): def execute(): - ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-FP8 " f"--ref-load /root/{MODEL_NAME}_torch_dist " + if USE_FP8_ROLLOUT: + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-FP8 " f"--ref-load /root/{MODEL_NAME}_torch_dist " + else: + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " f"--ref-load /root/{MODEL_NAME}_torch_dist " rollout_args = ( "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " @@ -89,13 +94,13 @@ def execute(): sglang_args = ( "--rollout-num-gpus-per-engine 8 " "--sglang-mem-fraction-static 0.8 " - "--sglang-moe-a2a-backend deepep " - "--sglang-deepep-mode auto " "--sglang-max-running-requests 512 " - "--sglang-disable-radix-cache " "--sglang-enable-metrics " ) + if USE_DEEPEP: + sglang_args += "--sglang-moe-a2a-backend deepep --sglang-deepep-mode auto " + ci_args = "--ci-test " misc_args = ( @@ -107,13 +112,16 @@ def execute(): "--attention-softmax-in-fp32 " # need to comment this when using model with MLA "--attention-backend flash " - "--moe-token-dispatcher-type flex " - "--moe-enable-deepep " "--actor-num-nodes 1 " "--actor-num-gpus-per-node 8 " "--colocate " ) + if USE_DEEPEP: + misc_args += "--moe-token-dispatcher-type flex --moe-enable-deepep " + else: + misc_args += "--moe-token-dispatcher-type alltoall " + train_args = ( f"{ckpt_args} " f"{rollout_args} " @@ -137,4 +145,8 @@ def execute(): if __name__ == "__main__": # TODO also use typer prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") execute() diff --git a/tests/test_qwen3_4B_ckpt.py b/tests/test_qwen3_4B_ckpt.py new file mode 100644 index 000000000..0e632578e --- /dev/null +++ b/tests/test_qwen3_4B_ckpt.py @@ -0,0 +1,139 @@ +import os +from argparse import ArgumentParser + +import miles.utils.external_utils.command_utils as U + + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) + +MODEL_NAME = "Qwen3-4B" +MODEL_TYPE = "qwen3-4B" +NUM_GPUS = 8 + + +parser = ArgumentParser() +parser.add_argument("--async-save", action="store_true", help="Whether to test async save/load.") + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.exec_command(f"rm -rf /root/models/{MODEL_NAME}_miles") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + U.hf_download_dataset("zhuzilin/aime-2024") + + U.convert_checkpoint( + model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS, dir_dst="/root/models" + ) + + +def execute(mode: str = ""): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/ " f"--ref-load /root/models/{MODEL_NAME}_torch_dist " + if mode == "save": + ckpt_args += f"--save /root/models/{MODEL_NAME}_miles " + ckpt_args += "--save-interval 2 " + elif mode == "async_save": + ckpt_args += f"--save /root/models/{MODEL_NAME}_miles " + ckpt_args += "--save-interval 2 " + ckpt_args += "--async-save " + elif mode == "load": + ckpt_args += f"--load /root/models/{MODEL_NAME}_miles " + ckpt_args += "--ckpt-step 1 " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + "--num-rollout 3 " + "--rollout-batch-size 4 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 1024 " + "--rollout-temperature 0.8 " + "--global-batch-size 32 " + "--balance-data " + ) + + perf_args = ( + "--tensor-model-parallel-size 2 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 2 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + f"--max-tokens-per-gpu {2048 if TIGHT_HOST_MEMORY else 16384} " + ) + + ppo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type k1 " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + "--optimizer-cpu-offload " + "--overlap-cpu-optimizer-d2h-h2d " + "--use-precision-aware-optimizer " + ) + + sglang_args = "--rollout-num-gpus-per-engine 2 --sglang-mem-fraction-static 0.8 --sglang-cuda-graph-bs 1 2 4 8 16 " + + ci_args = "--ci-test " + + misc_args = ( + # default dropout in megatron is 0.1 + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + # should be good for model performance + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + # need to comment this when using model with MLA + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 8 " + "--colocate " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{ppo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + args = parser.parse_args() + # TODO also use typer + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute("save" if not args.async_save else "async_save") + execute("load") diff --git a/tests/test_qwen3_4B_fsdp_true_on_policy.py b/tests/test_qwen3_4B_fsdp_true_on_policy.py new file mode 100644 index 000000000..9f404cb77 --- /dev/null +++ b/tests/test_qwen3_4B_fsdp_true_on_policy.py @@ -0,0 +1,114 @@ +import os +import miles.utils.external_utils.command_utils as U + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +NUM_GPUS = 2 + +MODEL_NAME = "Qwen3-4B" + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + U.hf_download_dataset("zhuzilin/aime-2024") + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 4096 " + "--rollout-temperature 1 " + "--global-batch-size 32 " + ) + + eval_args = ( + f"{'--eval-interval 20 ' if ENABLE_EVAL else ''}" + "--eval-prompt-data aime /root/datasets/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 4096 " + "--eval-top-p 0.7 " + ) + + fsdp_args = "--train-backend fsdp " "--update-weight-buffer-size 536870912 " + + grpo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 1 " + "--sglang-decode-log-interval 1000 " + "--sglang-enable-metrics " + "--sglang-enable-deterministic-inference " + "--sglang-rl-on-policy-target fsdp " + "--sglang-attention-backend fa3 " + "--attn-implementation flash_attention_3 " + "--deterministic-mode " + "--true-on-policy-mode " + ) + + ci_args = "--ci-test " + + misc_args = "--actor-num-nodes 1 " f"--actor-num-gpus-per-node {NUM_GPUS} " "--colocate " + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{fsdp_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + extra_env_vars = { + "NCCL_ALGO": "allreduce:tree", + "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", + "CUBLAS_WORKSPACE_CONFIG": ":4096:8", + "CUDA_DEVICE_MAX_CONNECTIONS": "1", + } + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=None, + extra_env_vars=extra_env_vars, + ) + + +if __name__ == "__main__": + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute() diff --git a/tests/test_qwen3_4B_ppo.py b/tests/test_qwen3_4B_ppo.py new file mode 100644 index 000000000..0eb6c381c --- /dev/null +++ b/tests/test_qwen3_4B_ppo.py @@ -0,0 +1,135 @@ +import os + +import miles.utils.external_utils.command_utils as U + + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) + +MODEL_NAME = "Qwen3-4B" +MODEL_TYPE = "qwen3-4B" +NUM_GPUS = 8 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command("hf download Qwen/Qwen3-4B --local-dir /root/models/Qwen3-4B") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + U.hf_download_dataset("zhuzilin/aime-2024") + + U.convert_checkpoint(model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS) + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/ " f"--ref-load /root/{MODEL_NAME}_torch_dist " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 8192 " + "--rollout-temperature 0.8 " + "--global-batch-size 32 " + "--balance-data " + ) + + eval_args = ( + f"{'--eval-interval 20 ' if ENABLE_EVAL else ''}" + "--eval-prompt-data aime24 /root/datasets/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 16384 " + "--eval-top-k 1 " + ) + + perf_args = ( + "--tensor-model-parallel-size 2 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 2 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + f"--max-tokens-per-gpu {2048 if TIGHT_HOST_MEMORY else 16384} " + ) + + ppo_args = ( + "--advantage-estimator ppo " + f"{'' if TIGHT_HOST_MEMORY else '--use-kl-loss '}" + "--kl-loss-coef 0.00 " + "--kl-loss-type k1 " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 4e-4 " + "--num-critic-only-steps 1 " + "--normalize-advantages " + "--critic-lr 1e-5 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 2 " + "--rollout-num-gpus 8 " + "--sglang-mem-fraction-static 0.8 " + "--sglang-max-running-requests 512 " + "--sglang-enable-metrics " + ) + + ci_args = "--ci-test " + + misc_args = ( + # default dropout in megatron is 0.1 + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + # should be good for model performance + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + # need to comment this when using model with MLA + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 4 " + "--colocate " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{ppo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + # TODO also use typer + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute() diff --git a/tests/test_qwen3_vl_4B_fsdp.py b/tests/test_qwen3_vl_4B_fsdp.py new file mode 100644 index 000000000..fbdffd237 --- /dev/null +++ b/tests/test_qwen3_vl_4B_fsdp.py @@ -0,0 +1,111 @@ +import os +import miles.utils.external_utils.command_utils as U + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +NUM_GPUS = 8 + +MODEL_NAME = "Qwen3-VL-4B-Instruct" +DATASET_NAME = "chenhegu/geo3k_imgurl" + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset(DATASET_NAME) + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " + + rollout_args = ( + "--prompt-data /root/datasets/geo3k_imgurl/train.parquet " + "--input-key problem " + "--label-key answer " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 4096 " + "--rollout-temperature 1 " + "--global-batch-size 32 " + ) + + # multimodal keys required for vlm datasets + multimodal_args = '--multimodal-keys \'{"image": "images"}\' ' + + eval_args = ( + f"{'--eval-interval 20 ' if ENABLE_EVAL else ''}" + "--eval-prompt-data geo3k /root/datasets/geo3k_imgurl/test.parquet " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 4096 " + ) + + fsdp_args = "--train-backend fsdp " "--gradient-checkpointing " "--update-weight-buffer-size 536870912 " + + grpo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 1 " + "--sglang-mem-fraction-static 0.6 " + "--sglang-decode-log-interval 1000 " + "--sglang-enable-metrics " + "--sglang-attention-backend fa3 " + "--attn-implementation flash_attention_3 " + ) + + ci_args = "--ci-test " + + misc_args = "--actor-num-nodes 1 " f"--actor-num-gpus-per-node {NUM_GPUS} " "--colocate " + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{multimodal_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{fsdp_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + extra_env_vars = { + "CUDA_DEVICE_MAX_CONNECTIONS": "1", + } + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=None, + extra_env_vars=extra_env_vars, + ) + + +if __name__ == "__main__": + prepare() + os.environ.pop("http_proxy", None) + os.environ.pop("https_proxy", None) + os.environ.pop("HTTP_PROXY", None) + os.environ.pop("HTTPS_PROXY", None) + execute() diff --git a/tests/utils/test_mask_utils.py b/tests/utils/test_mask_utils.py new file mode 100644 index 000000000..f54304b96 --- /dev/null +++ b/tests/utils/test_mask_utils.py @@ -0,0 +1,99 @@ +from transformers import AutoTokenizer + +from miles.utils.mask_utils import MultiTurnLossMaskGenerator + + +def test_loss_mask_qwen3_simple(model_name: str = "Qwen/Qwen3-8B"): + tokenizer = AutoTokenizer.from_pretrained(model_name) + mask_generator = MultiTurnLossMaskGenerator(tokenizer, tokenizer_type="qwen3") + messages = [ + {"role": "system", "content": "SYSTEM MESSAGE FOR TESTING ONLY"}, + {"role": "user", "content": "USER CONTENT FOR TESTING ONLY"}, + {"role": "assistant", "content": "ASSISTANT RESPONSE FOR TESTING ONLY"}, + ] + all_token_ids, all_loss_masks = mask_generator.gen_multi_turn_loss_mask_qwen3(messages) + assert len(all_token_ids) == len(all_loss_masks), f"{len(all_token_ids)} != {len(all_loss_masks)}" + selected_texts = mask_generator.get_text_from_loss_mask(all_token_ids, all_loss_masks) + assert len(selected_texts) == 1, f"Expected 1 text, got {len(selected_texts)}" + + print(f"==== Single Turn Test {model_name} ====") + print("text = ", [tokenizer.decode(all_token_ids)]) + print("token_ids = ", all_token_ids) + print("loss_mask = ", all_loss_masks) + print("selected_texts = ", selected_texts) + + +def test_loss_mask_qwen3_tools(model_name: str = "Qwen/Qwen3-8B"): + tokenizer = AutoTokenizer.from_pretrained(model_name) + mask_generator = MultiTurnLossMaskGenerator(tokenizer, tokenizer_type="qwen3") + messages = [ + {"role": "system", "content": "SYSTEM MESSAGE FOR TESTING ONLY"}, + {"role": "user", "content": "USER CONTENT FOR TESTING ONLY"}, + { + "role": "assistant", + "content": "I WILL CALL terminal", + "tool_calls": [ + {"function": {"name": "terminal", "arguments": {"command": "ls"}}, "id": "call_0", "type": "function"}, + {"function": {"name": "terminal", "arguments": {"command": "ls"}}, "id": "call_0", "type": "function"}, + ], + }, + {"role": "tool", "name": "terminal", "content": "LICENSE README.md README_zh.md"}, + {"role": "tool", "name": "terminal", "content": "LICENSE README.md README_zh.md"}, + {"role": "assistant", "content": "ASSISTANT RESPONSE FOR TESTING ONLY"}, + ] + tools = [ + { + "type": "function", + "function": { + "name": "terminal", + "description": "Perform operations from the terminal.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to execute as `bash -c `", + }, + "description": { + "type": "string", + "description": "Brief description of the command for the user.", + }, + }, + "required": ["command"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read the content of a file given its path.", + "parameters": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "The absolute path to the file to be read.", + } + }, + "required": ["file_path"], + }, + }, + }, + ] + + all_token_ids, all_loss_masks = mask_generator.gen_multi_turn_loss_mask_qwen3(messages, tools) + assert len(all_token_ids) == len(all_loss_masks), f"{len(all_token_ids)} != {len(all_loss_masks)}" + selected_texts = mask_generator.get_text_from_loss_mask(all_token_ids, all_loss_masks) + assert len(selected_texts) == 2, f"Expected 2 texts, got {len(selected_texts)}" + + print(f"==== Multi-turn with Tools Test {model_name} ====") + print("text = ", [tokenizer.decode(all_token_ids)]) + print("token_ids = ", all_token_ids) + print("loss_mask = ", all_loss_masks) + print("selected_texts = ", selected_texts) + + +if __name__ == "__main__": + test_loss_mask_qwen3_simple("Qwen/Qwen3-Coder-30B-A3B-Instruct") + test_loss_mask_qwen3_tools("Qwen/Qwen3-Coder-30B-A3B-Instruct") From 927d653e027183d840c1ec359c306eef2914a78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=99=A8=E9=98=B3?= Date: Sun, 4 Jan 2026 21:52:18 -0800 Subject: [PATCH 07/57] Cherry Pick commits to local fix CI unit tests (#393) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../formal_math/single_round/run_minimal.py | 12 ++++--- .../run_geo3k_vlm_multi_turn.py | 12 ++++--- miles/backends/fsdp_utils/arguments.py | 1 + miles/backends/sglang_utils/arguments.py | 1 - miles/backends/sglang_utils/sglang_engine.py | 4 +++ miles/ray/rollout.py | 5 +-- miles/utils/external_utils/command_utils.py | 5 +-- scripts/run-qwen3-4B-fsdp.sh | 34 +++++++++++-------- tests/test_fsdp_gptoss_20b.sh | 28 ++++++++------- tests/test_moonlight_16B_A3B.py | 6 ++-- tests/test_quick_start_glm4_9B.py | 6 ++-- tests/test_qwen2.5_0.5B_gsm8k.py | 6 ++-- tests/test_qwen2.5_0.5B_gsm8k_async.py | 6 ++-- tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py | 6 ++-- tests/test_qwen3_0.6B_fsdp_distributed.py | 6 ++-- tests/test_qwen3_0.6B_parallel_check.py | 6 ++-- tests/test_qwen3_30B_A3B.py | 6 ++-- tests/test_qwen3_4B_ckpt.py | 6 ++-- tests/test_qwen3_4B_fsdp_true_on_policy.py | 6 ++-- tests/test_qwen3_4B_ppo.py | 6 ++-- 20 files changed, 84 insertions(+), 84 deletions(-) diff --git a/examples/formal_math/single_round/run_minimal.py b/examples/formal_math/single_round/run_minimal.py index 469d6b833..69d092884 100644 --- a/examples/formal_math/single_round/run_minimal.py +++ b/examples/formal_math/single_round/run_minimal.py @@ -96,10 +96,14 @@ ) wandb_args = ( - "--use-wandb " - "--wandb-project miles-formal-math-run-minimal " - "--wandb-group demo " - "--wandb-key ${WANDB_API_KEY} " + ( + "--use-wandb " + "--wandb-project miles-formal-math-run-minimal " + "--wandb-group demo " + f"--wandb-key '{wandb_api_key}' " + ) + if (wandb_api_key := os.environ.get("WANDB_API_KEY")) + else "" ) train_args = ( diff --git a/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py b/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py index 4c8798e4d..5c32d33d3 100644 --- a/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py +++ b/examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py @@ -43,10 +43,14 @@ def execute(): ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " wandb_args = ( - "--use-wandb " - "--wandb-project miles-dev " - "--wandb-group geo3k_vlm_multi_turn " - "--wandb-key ${WANDB_API_KEY} " + ( + "--use-wandb " + "--wandb-project miles-dev " + "--wandb-group geo3k_vlm_multi_turn " + f"--wandb-key '{wandb_api_key}' " + ) + if (wandb_api_key := os.environ.get("WANDB_API_KEY")) + else "" ) rollout_args = ( diff --git a/miles/backends/fsdp_utils/arguments.py b/miles/backends/fsdp_utils/arguments.py index a319fe6e5..c2015a1f7 100644 --- a/miles/backends/fsdp_utils/arguments.py +++ b/miles/backends/fsdp_utils/arguments.py @@ -35,6 +35,7 @@ class FSDPArgs: # Precision gradient_checkpointing: bool = False fp16: bool = False + bf16: bool = False # FSDP configuration fsdp_state_dict_cpu_offload: bool = True # If True, offload full state dict to CPU during collection. diff --git a/miles/backends/sglang_utils/arguments.py b/miles/backends/sglang_utils/arguments.py index 1311350cb..49ca74a36 100644 --- a/miles/backends/sglang_utils/arguments.py +++ b/miles/backends/sglang_utils/arguments.py @@ -41,7 +41,6 @@ def add_sglang_arguments(parser): skipped_args = [ "model_path", - "dtype", "trust_remote_code", "random_seed", # memory diff --git a/miles/backends/sglang_utils/sglang_engine.py b/miles/backends/sglang_utils/sglang_engine.py index 2995bbc32..510899093 100644 --- a/miles/backends/sglang_utils/sglang_engine.py +++ b/miles/backends/sglang_utils/sglang_engine.py @@ -471,6 +471,8 @@ def _compute_server_args( kwargs["enable_return_routed_experts"] = True if args.fp16: kwargs["dtype"] = "float16" + elif args.bf16: + kwargs["dtype"] = "bfloat16" external_engine_need_check_fields = [k for k in kwargs.keys() if k not in _EXTERNAL_ENGINE_SKIP_CHECK_FIELDS] unused_keys = set(kwargs.keys()) @@ -497,4 +499,6 @@ def _compute_server_args( "nccl_port", "dist_init_addr", "skip_server_warmup", + "enable_draft_weights_cpu_backup", + "mem_fraction_static", ] diff --git a/miles/ray/rollout.py b/miles/ray/rollout.py index 7b3f8ce76..1bff43124 100644 --- a/miles/ray/rollout.py +++ b/miles/ray/rollout.py @@ -423,10 +423,11 @@ def init_rollout_engines(args, pg, all_rollout_engines): def _allocate_rollout_engine_addr_and_ports_external(args, rollout_engines): addr_and_ports = [] for rank, _ in rollout_engines: - [host, port] = args.rollout_external_engine_addrs[rank].split(":") + addr = args.rollout_external_engine_addrs[rank] + [host, port] = addr.split(":") addr_and_ports.append( dict( - dist_init_addr=None, + dist_init_addr=addr, nccl_port=None, host=host, port=int(port), diff --git a/miles/utils/external_utils/command_utils.py b/miles/utils/external_utils/command_utils.py index ac7f3ce46..8c7c9316b 100644 --- a/miles/utils/external_utils/command_utils.py +++ b/miles/utils/external_utils/command_utils.py @@ -213,12 +213,13 @@ def get_default_wandb_args(test_file: str, run_name_prefix: str | None = None, r if (x := run_name_prefix) is not None: wandb_run_name = f"{x}_{wandb_run_name}" - # do not put wandb_api_key value here to avoid leaking to logs explicitly + # Use the actual key value from environment to avoid shell expansion issues + wandb_key = os.environ.get("WANDB_API_KEY") return ( "--use-wandb " f"--wandb-project miles-{test_name} " f"--wandb-group {wandb_run_name} " - f"--wandb-key ${{WANDB_API_KEY}} " + f"--wandb-key '{wandb_key}' " "--disable-wandb-random-suffix " ) diff --git a/scripts/run-qwen3-4B-fsdp.sh b/scripts/run-qwen3-4B-fsdp.sh index 3c95442d5..9fa339178 100644 --- a/scripts/run-qwen3-4B-fsdp.sh +++ b/scripts/run-qwen3-4B-fsdp.sh @@ -75,12 +75,16 @@ OPTIMIZER_ARGS=( --adam-beta2 0.98 ) -WANDB_ARGS=( - --use-wandb - --wandb-project miles-dev-mcore-fsdp - --wandb-group qwen3-4B-fsdp-1130-ref - --wandb-key ${WANDB_API_KEY} -) +if [ -z "${WANDB_API_KEY}" ]; then + WANDB_ARGS=() +else + WANDB_ARGS=( + --use-wandb + --wandb-project miles-dev-mcore-fsdp + --wandb-group qwen3-4B-fsdp-1130-ref + --wandb-key "${WANDB_API_KEY}" + ) +fi SGLANG_ARGS=( --rollout-num-gpus-per-engine 1 @@ -128,15 +132,15 @@ RUNTIME_ENV_JSON="{ ray job submit --address="http://127.0.0.1:8265" \ --runtime-env-json="${RUNTIME_ENV_JSON}" \ -- python3 train.py \ - ${CKPT_ARGS[@]} \ - ${ROLLOUT_ARGS[@]} \ - ${OPTIMIZER_ARGS[@]} \ - ${GRPO_ARGS[@]} \ - ${WANDB_ARGS[@]} \ - ${SGLANG_ARGS[@]} \ - ${TRAIN_BACKEND_ARGS[@]} \ - ${PERF_ARGS[@]} \ - ${MISC_ARGS[@]} + "${CKPT_ARGS[@]}" \ + "${ROLLOUT_ARGS[@]}" \ + "${OPTIMIZER_ARGS[@]}" \ + "${GRPO_ARGS[@]}" \ + "${WANDB_ARGS[@]}" \ + "${SGLANG_ARGS[@]}" \ + "${TRAIN_BACKEND_ARGS[@]}" \ + "${PERF_ARGS[@]}" \ + "${MISC_ARGS[@]}" diff --git a/tests/test_fsdp_gptoss_20b.sh b/tests/test_fsdp_gptoss_20b.sh index 90cab2317..b68671b5e 100644 --- a/tests/test_fsdp_gptoss_20b.sh +++ b/tests/test_fsdp_gptoss_20b.sh @@ -99,12 +99,16 @@ SGLANG_ARGS=( ) -WANDB_ARGS=( - --use-wandb - --wandb-project "miles-fsdp-gpt" - --wandb-group "20b-bf16" - --wandb-key ${WANDB_API_KEY} -) +if [ -z "${WANDB_API_KEY}" ]; then + WANDB_ARGS=() +else + WANDB_ARGS=( + --use-wandb + --wandb-project "miles-fsdp-gpt" + --wandb-group "20b-bf16" + --wandb-key "${WANDB_API_KEY}" + ) +fi # launch the master node of ray in container ray start --head --node-ip-address 127.0.0.1 --num-gpus 4 --disable-usage-stats @@ -122,9 +126,9 @@ ray job submit --address="http://127.0.0.1:8265" \ --train-backend fsdp \ --bf16 \ --attn-implementation eager \ - ${CKPT_ARGS[@]} \ - ${ROLLOUT_ARGS[@]} \ - ${OPTIMIZER_ARGS[@]} \ - ${GRPO_ARGS[@]} \ - ${SGLANG_ARGS[@]} \ - ${WANDB_ARGS[@]} \ + "${CKPT_ARGS[@]}" \ + "${ROLLOUT_ARGS[@]}" \ + "${OPTIMIZER_ARGS[@]}" \ + "${GRPO_ARGS[@]}" \ + "${SGLANG_ARGS[@]}" \ + "${WANDB_ARGS[@]}" diff --git a/tests/test_moonlight_16B_A3B.py b/tests/test_moonlight_16B_A3B.py index d41a37477..b1255982e 100644 --- a/tests/test_moonlight_16B_A3B.py +++ b/tests/test_moonlight_16B_A3B.py @@ -118,8 +118,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_quick_start_glm4_9B.py b/tests/test_quick_start_glm4_9B.py index cf99af919..15ca8ce5f 100644 --- a/tests/test_quick_start_glm4_9B.py +++ b/tests/test_quick_start_glm4_9B.py @@ -121,8 +121,6 @@ def execute(): if __name__ == "__main__": # TODO also use typer prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen2.5_0.5B_gsm8k.py b/tests/test_qwen2.5_0.5B_gsm8k.py index 31efaa9d4..dcdbd5834 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k.py +++ b/tests/test_qwen2.5_0.5B_gsm8k.py @@ -125,8 +125,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen2.5_0.5B_gsm8k_async.py b/tests/test_qwen2.5_0.5B_gsm8k_async.py index a8177263c..dcaaf5e1f 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_async.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_async.py @@ -125,8 +125,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py b/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py index eff8b6583..3d19b48ce 100644 --- a/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py +++ b/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py @@ -98,8 +98,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen3_0.6B_fsdp_distributed.py b/tests/test_qwen3_0.6B_fsdp_distributed.py index 0feecde8e..3d70f3e4c 100644 --- a/tests/test_qwen3_0.6B_fsdp_distributed.py +++ b/tests/test_qwen3_0.6B_fsdp_distributed.py @@ -100,8 +100,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen3_0.6B_parallel_check.py b/tests/test_qwen3_0.6B_parallel_check.py index 3c9c51204..44f5c42fa 100644 --- a/tests/test_qwen3_0.6B_parallel_check.py +++ b/tests/test_qwen3_0.6B_parallel_check.py @@ -131,8 +131,6 @@ def execute(): if __name__ == "__main__": # TODO also use typer prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen3_30B_A3B.py b/tests/test_qwen3_30B_A3B.py index af832dd4e..adff10804 100644 --- a/tests/test_qwen3_30B_A3B.py +++ b/tests/test_qwen3_30B_A3B.py @@ -145,8 +145,6 @@ def execute(): if __name__ == "__main__": # TODO also use typer prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen3_4B_ckpt.py b/tests/test_qwen3_4B_ckpt.py index 0e632578e..22fb2b5fc 100644 --- a/tests/test_qwen3_4B_ckpt.py +++ b/tests/test_qwen3_4B_ckpt.py @@ -131,9 +131,7 @@ def execute(mode: str = ""): args = parser.parse_args() # TODO also use typer prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute("save" if not args.async_save else "async_save") execute("load") diff --git a/tests/test_qwen3_4B_fsdp_true_on_policy.py b/tests/test_qwen3_4B_fsdp_true_on_policy.py index 9f404cb77..7c975c7cc 100644 --- a/tests/test_qwen3_4B_fsdp_true_on_policy.py +++ b/tests/test_qwen3_4B_fsdp_true_on_policy.py @@ -107,8 +107,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen3_4B_ppo.py b/tests/test_qwen3_4B_ppo.py index 0eb6c381c..962f610fa 100644 --- a/tests/test_qwen3_4B_ppo.py +++ b/tests/test_qwen3_4B_ppo.py @@ -128,8 +128,6 @@ def execute(): if __name__ == "__main__": # TODO also use typer prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() From 47a5bdf02199d12d9800dcc55d83895b74409c64 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 4 Jan 2026 23:55:37 -0600 Subject: [PATCH 08/57] [example] Add SWE-agent example (#367) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: zijiexia Co-authored-by: zhaochenyang20 --- .gitmodules | 8 + examples/swe-agent/README.md | 130 ++++++++++ .../swe-agent/download_and_process_data.py | 85 ++++++ examples/swe-agent/generate_with_swe_agent.py | 242 ++++++++++++++++++ examples/swe-agent/mini-swe-agent | 1 + examples/swe-agent/nemo-gym | 1 + examples/swe-agent/run-qwen3-4b-instruct.sh | 166 ++++++++++++ 7 files changed, 633 insertions(+) create mode 100644 .gitmodules create mode 100644 examples/swe-agent/README.md create mode 100755 examples/swe-agent/download_and_process_data.py create mode 100644 examples/swe-agent/generate_with_swe_agent.py create mode 160000 examples/swe-agent/mini-swe-agent create mode 160000 examples/swe-agent/nemo-gym create mode 100755 examples/swe-agent/run-qwen3-4b-instruct.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..7885d8a54 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "examples/swe-agent/nemo-gym"] + path = examples/swe-agent/nemo-gym + url = https://github.com/yueming-yuan/Gym + branch = miles-swe-agent +[submodule "examples/swe-agent/mini-swe-agent"] + path = examples/swe-agent/mini-swe-agent + url = https://github.com/yueming-yuan/nv-mini-swe-agent + branch = miles-swe-agent diff --git a/examples/swe-agent/README.md b/examples/swe-agent/README.md new file mode 100644 index 000000000..8648c23ee --- /dev/null +++ b/examples/swe-agent/README.md @@ -0,0 +1,130 @@ +### Introduction + +This is an example for SWE-agent training. This example uses NVIDIA's Nemo-Gym as the Gym environment implement, SWE-Gym as the training data, and SWE-bench as the evaluation. + +This implementation of this example is partially in submodules below: +- Nemo-Gym: https://github.com/yueming-yuan/Gym/tree/miles-swe-agent +- mini-swe-agent: https://github.com/yueming-yuan/nv-mini-swe-agent/tree/miles-swe-agent + + +### Prepare environment +#### Update submodules +```bash +git submodule update --init --recursive . +``` +#### Docker settings +```bash +# 1. create a docker network +docker network create swe-net + +# 2. create environment docker +docker run -itd \ + --name swe_env \ + --shm-size 16g \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /mnt/data:/data \ + -v /home/sglang-rl/:/workspace \ + --ipc=host \ + --ulimit nofile=65536:65536 \ + --ulimit memlock=-1 \ + --ulimit stack=67108864 \ + --network swe-net \ + ubuntu:latest \ + /bin/bash + +# 3. create miles docker +docker run -itd \ + --shm-size 32g \ + --gpus all \ + -v /mnt/data/cache/huggingface:/root/.cache/huggingface \ + -v /mnt/data:/data \ + -v /home/sglang-rl/:/workspace \ + --ipc=host \ + --ulimit nofile=65536:65536 \ + --ulimit memlock=-1 \ + --ulimit stack=67108864 \ + --privileged \ + --network swe-net \ + --name miles_ \ + slimerl/slime:latest \ + /bin/zsh + +# 4. install utils in environment docker +docker exec -it swe_env /bin/bash +apt update && apt install -y zsh curl git python3 python3-pip docker.io +``` +note: `-v /var/run/docker.sock:/var/run/docker.sock` is required for Docker-in-Docker SWE environment execution; use `--network swe-net` to enable communication between training & environment. + +#### Installation + +In **environment docker**, install Gym +```bash +git clone https://github.com/yueming-yuan/Gym +cd Gym + +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env +uv venv --python 3.12 && source .venv/bin/activate +uv sync --extra dev --group docs + +# configure env.yaml +echo "policy_base_url: https://api.openai.com/v1 +policy_api_key: your-openai-api-key +policy_model_name: gpt-4.1-2025-04-14 +default_host: 0.0.0.0" > env.yaml +``` +note: set host IP to `0.0.0.0` to enable communications between dockers. + +then set up for SWE-agent server: +```bash +cd responses_api_agents/mini_swe_agent +uv pip install -r requirements.txt +``` +Now you should be able to run the SWE-agent server. + +For **miles docker** setup, please follow the standard setup process. + +### Preparing data +In **miles docker**, download **SWE-Gym** data from huggingface and convert it to Miles' prompt data format with this script. +``` +cd miles/examples/swe-agent +python download_and_process_data.py --input SWE-Gym/SWE-Gym --output /root/swe_train.jsonl +``` + +### Running train +1. In environment docker, launch the agent server +```bash +cd Gym +source .venv/bin/activate +cd responses_api_agents/mini_swe_agent +./start_server.sh +``` + + +2. In miles docker, +(1) export `SWE_AGENT_GYM_URL` to be the port of the second server you started in Gym in environment docker, whose `server_type` is `responses_api_agents`. `swe_env` is the environment docker's name; replace it if you changed the name. +(minor TODO: modify the port selections to avoid setting this every time.) (2) launch the training. +```bash +export SWE_AGENT_GYM_URL="http://swe_env:" +bash examples/swe-agent/run-qwen3-4b-instruct.sh +``` + + +### Troubleshooting +1. The first time of every SWE environment can be slow, and may need to wait before generation, because each SWE-Gym task has a specific docker, and `docker pull` takes time. +2. Sometimes the environment may also be slow at evaluation. The timeout of evaluation is 10 minutes by default. If the server is stuck at `[EVAL] Running eval`, you may need to wait for it. + +## Metrics +``` +agent/turns_mean, agent/turns_sum - Turn counts +agent/tool_calls_mean, agent/tool_calls_sum - Tool call counts +agent/total_time_mean/max/min - Total time statistics +agent/model_query_time_sum_mean - Avg total model time per rollout +agent/env_execution_time_sum_mean - Avg total env time per rollout +agent/eval_time_mean - Avg evaluation time +agent/overhead_time_mean - Avg overhead time +agent/time_per_turn - Avg time per turn +agent/model_query_time_avg - Avg model query time per turn +agent/env_execution_time_avg - Avg env execution time per turn +agent/model_time_ratio, agent/env_time_ratio - Time ratios +``` diff --git a/examples/swe-agent/download_and_process_data.py b/examples/swe-agent/download_and_process_data.py new file mode 100755 index 000000000..3512bf3d4 --- /dev/null +++ b/examples/swe-agent/download_and_process_data.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Download and process data to Miles format.""" + +import argparse +import json +import tempfile +from pathlib import Path +from datasets import load_dataset + + +def convert_to_miles_format(input_path: str, output_path: str, limit: int = None, split: str = "train"): + """Convert JSONL to Miles format. + + Args: + input_path: Path to input JSONL file + output_path: Path to output JSONL file in Miles format + limit: Optional limit on number of samples + split: Dataset split name (used in metadata) + """ + count = 0 + with open(input_path) as fin, open(output_path, "w") as fout: + for line in fin: + if limit and count >= limit: + break + + instance = json.loads(line) + + # Add subset and split to metadata for Gym API + metadata = dict(instance) + metadata["subset"] = "gym" + metadata["split"] = split + + miles_sample = { + "prompt": instance.get("problem_statement", ""), + "metadata": metadata, + } + + fout.write(json.dumps(miles_sample) + "\n") + count += 1 + + print(f"Converted {count} samples: {input_path} -> {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Download HuggingFace dataset and convert to Miles format") + parser.add_argument("--input", type=str, required=True, help="HuggingFace dataset path or local JSONL file") + parser.add_argument("--output", type=str, required=True, help="Output JSONL file path") + parser.add_argument( + "--split", type=str, default="train", help="Dataset split (default: train, only for HF datasets)" + ) + parser.add_argument("--limit", type=int, help="Limit number of samples") + + args = parser.parse_args() + + input_path = Path(args.input) + + if input_path.exists() and input_path.suffix == ".jsonl": + print(f"Processing local file: {args.input}") + convert_to_miles_format(args.input, args.output, args.limit, args.split) + else: + print(f"Loading HuggingFace dataset: {args.input} (split={args.split})") + ds = load_dataset(args.input, split=args.split) + + if args.limit: + ds = ds.select(range(min(args.limit, len(ds)))) + + tmp_path = None + try: + with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as tmp: + tmp_path = tmp.name + + print(f"Downloading to temporary file: {tmp_path}") + ds.to_json(tmp_path) + + print(f"Converting to Miles format: {args.output}") + convert_to_miles_format(tmp_path, args.output, split=args.split) + finally: + if tmp_path and Path(tmp_path).exists(): + Path(tmp_path).unlink() + + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/examples/swe-agent/generate_with_swe_agent.py b/examples/swe-agent/generate_with_swe_agent.py new file mode 100644 index 000000000..b0dbbd612 --- /dev/null +++ b/examples/swe-agent/generate_with_swe_agent.py @@ -0,0 +1,242 @@ +import logging +import os +from argparse import Namespace +from collections.abc import Callable +from typing import Any + +from miles.rollout.base_types import RolloutFnEvalOutput, RolloutFnTrainOutput +from miles.rollout.filter_hub.base_types import DynamicFilterOutput +from miles.rollout.sglang_rollout import GenerateState, eval_rollout +from miles.utils.async_utils import run +from miles.utils.http_utils import post +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + + +def build_tokens_and_mask_from_messages( + messages: list[dict], + tokenizer, +) -> tuple[list[int], list[int], str, int]: + + if not messages or len(messages) < 2: + return [], [], "", 0 + + prompt_msgs = messages[:2] + response_msgs = messages[2:] + + prompt_tokens = [] + for msg in prompt_msgs: + content = msg.get("content", "") + if content: + prompt_tokens.extend(tokenizer(content, add_special_tokens=False)["input_ids"]) + + response_tokens = [] + loss_mask = [] + response_text_parts = [] + + for msg in response_msgs: + content = msg.get("content", "") + if not content: + continue + + tokens = tokenizer(content, add_special_tokens=False)["input_ids"] + token_len = len(tokens) + + response_tokens.extend(tokens) + response_text_parts.append(content) + + mask_val = 1 if msg.get("role") == "assistant" else 0 + loss_mask.extend([mask_val] * token_len) + + all_tokens = prompt_tokens + response_tokens + response_text = "".join(response_text_parts) + response_length = len(response_tokens) + + return all_tokens, loss_mask, response_text, response_length + + +async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, Any]) -> Sample: + """ + Custom generation function for SWE-Agent integration. + + Orchestrates the interaction with the external Gym environment: + 1. Sends prompt/metadata to Gym. + 2. Receives execution trace (messages) and rewards. + 3. Formats data for Miles training format. + + Note: Performs in-place modification of `sample` for memory efficiency. + """ + # Prepare request for Gym /run endpoint + request = { + "responses_create_params": { + "input": [], + }, + "sampling_params": sampling_params, + **sample.metadata, + "sglang_url": f"http://{args.sglang_router_ip}:{args.sglang_router_port}/v1", + } + + gym_url = os.getenv("SWE_AGENT_GYM_URL", "http://localhost:11000") + response = await post(f"{gym_url}/run", request) + + exit_status = response.get("info", {}).get("exit_status", "") + logger.debug(f"exit_status: {exit_status}, reward: {response.get('reward', 0.0)}") + + messages = response.get("messages", []) + + if len(messages) >= 2: + sample.prompt = messages[:2] + + state = GenerateState(args) + tokens, loss_mask, response_text, response_length = build_tokens_and_mask_from_messages( + messages=messages, + tokenizer=state.tokenizer, + ) + + sample.rollout_log_probs = None # TODO + sample.tokens = tokens + sample.loss_mask = loss_mask + sample.response = response_text + sample.response_length = response_length + sample.metadata["reward"] = response.get("reward", 0.0) + sample.metadata["eval_report"] = response.get("metadata", {}) + sample.metadata["messages"] = messages + + agent_metrics = response.get("info", {}).get("agent_metrics", {}) + sample.metadata["agent_metrics"] = agent_metrics + + if exit_status == "Submitted": + sample.status = Sample.Status.COMPLETED + elif exit_status in ("RolloutTruncated", "LimitsExceeded", "CollapseContinued"): + sample.status = Sample.Status.TRUNCATED + else: + sample.status = Sample.Status.ABORTED + sample.reward = 0.0 + + return sample + + +async def reward_func(args, sample: Sample, **kwargs) -> float: + """Reward function - already computed in generate()""" + reward = sample.metadata.get("reward", 0.0) + return reward + + +def dynamic_filter(args, samples: list[Sample], **kwargs) -> DynamicFilterOutput: + """Filter out groups with any aborted samples from training""" + has_aborted = any(sample.status == Sample.Status.ABORTED for sample in samples) + if has_aborted: + return DynamicFilterOutput(keep=False, reason="group_has_aborted") + return DynamicFilterOutput(keep=True) + + +def aggregate_agent_metrics(samples: list[Sample]) -> dict: + """Aggregate agent metrics across samples for logging""" + metrics = {} + + all_metrics = [] + for sample in samples: + if hasattr(sample, "metadata") and sample.metadata: + agent_metrics = sample.metadata.get("agent_metrics", {}) + if agent_metrics: + all_metrics.append(agent_metrics) + + if not all_metrics: + return {} + + # Count metrics - mean and sum + for key in ["turns", "tool_calls"]: + values = [m.get(key, 0) for m in all_metrics] + if values: + metrics[f"agent/{key}_mean"] = sum(values) / len(values) + metrics[f"agent/{key}_sum"] = sum(values) + + # Time sum metrics - mean across rollouts + for key in ["model_query_time_sum", "env_execution_time_sum", "eval_time", "agent_run_time"]: + values = [m.get(key, 0) for m in all_metrics] + if values: + metrics[f"agent/{key}_mean"] = sum(values) / len(values) + + # Time avg metrics - mean of means + for key in ["time_per_turn", "model_query_time_avg", "env_execution_time_avg"]: + values = [m.get(key, 0) for m in all_metrics] + if values: + metrics[f"agent/{key}"] = sum(values) / len(values) + + # Ratio metrics (all based on total_time which includes eval) + for key in ["model_time_ratio", "env_time_ratio", "eval_time_ratio"]: + values = [m.get(key, 0) for m in all_metrics] + if values: + metrics[f"agent/{key}"] = sum(values) / len(values) + + # Total time stats + values = [m.get("total_time", 0) for m in all_metrics] + if values: + metrics["agent/total_time_mean"] = sum(values) / len(values) + metrics["agent/total_time_max"] = max(values) + metrics["agent/total_time_min"] = min(values) + + return metrics + + +async def generate_rollout_async( + args: Namespace, rollout_id: int, data_source: Callable[[int], list[list[Sample]]] +) -> tuple[RolloutFnTrainOutput, list[list[Sample]]]: + """ + Custom rollout function that wraps sglang_rollout.generate_rollout_async + and adds agent metrics aggregation. + """ + from miles.rollout.sglang_rollout import generate_rollout_async as base_generate_rollout_async + + rollout_output, aborted_samples = await base_generate_rollout_async(args, rollout_id, data_source) + + all_samples = [] + for group in rollout_output.samples: + if isinstance(group[0], list): + for sample_list in group: + all_samples.extend(sample_list) + else: + all_samples.extend(group) + + agent_metrics = aggregate_agent_metrics(all_samples) + + metrics = rollout_output.metrics or {} + metrics.update(agent_metrics) + + logger.info(f"Aggregated agent metrics for rollout {rollout_id}: {agent_metrics}") + + return RolloutFnTrainOutput(samples=rollout_output.samples, metrics=metrics), aborted_samples + + +def generate_rollout( + args: Namespace, rollout_id: int, data_buffer: Any, evaluation: bool = False +) -> RolloutFnTrainOutput | RolloutFnEvalOutput: + """An example to implement the generate_rollout function for an rule based rm rollout generation. + + Args: + args: the whole args + rollout_id: int, the id of the rollout, used for deterministic data generation + data_buffer: the data buffer to store the generated samples + evaluation: bool, whether the rollout is for evaluation or not + + Returns: + list[list[Sample]]: a list of list of samples generated by the rollout + """ + output, aborted_samples = generate_abortable_samples( + args, rollout_id, data_buffer.get_samples, evaluation=evaluation + ) + data_buffer.add_samples(aborted_samples) + return output + + +def generate_abortable_samples( + args: Namespace, + rollout_id: int, + data_source: Callable[[int], list[list[Sample]]], + evaluation: bool = False, +) -> tuple[Any, list[list[Sample]]]: + assert args.rollout_global_dataset + if evaluation: + return run(eval_rollout(args, rollout_id)) + return run(generate_rollout_async(args, rollout_id, data_source)) diff --git a/examples/swe-agent/mini-swe-agent b/examples/swe-agent/mini-swe-agent new file mode 160000 index 000000000..8d74eee82 --- /dev/null +++ b/examples/swe-agent/mini-swe-agent @@ -0,0 +1 @@ +Subproject commit 8d74eee82036bc1c30f17c18b67c1e6984ad4f0b diff --git a/examples/swe-agent/nemo-gym b/examples/swe-agent/nemo-gym new file mode 160000 index 000000000..4fce289f9 --- /dev/null +++ b/examples/swe-agent/nemo-gym @@ -0,0 +1 @@ +Subproject commit 4fce289f9bbee420ebc9a7ac2f8884437d3a93ea diff --git a/examples/swe-agent/run-qwen3-4b-instruct.sh b/examples/swe-agent/run-qwen3-4b-instruct.sh new file mode 100755 index 000000000..d9c9dd953 --- /dev/null +++ b/examples/swe-agent/run-qwen3-4b-instruct.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=1 + +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" + +export SWE_AGENT_GYM_URL="${SWE_AGENT_GYM_URL:-http://swe_env:11000}" + +source "${SCRIPT_DIR}/../../scripts/models/qwen3-4B-Instruct-2507.sh" + +CKPT_ARGS=( + --hf-checkpoint /root/qwen3-4B-Instruct-2507 + --ref-load /root/qwen3-4B-Instruct-2507_torch_dist + # --load /path/to/checkpoint/ + --save /root/qwen3-4B-Instruct-2507_miles/ + --save-interval 100 +) + +PERF_ARGS=( + --tensor-model-parallel-size 2 + --pipeline-model-parallel-size 1 + --context-parallel-size 1 + --expert-model-parallel-size 1 + --expert-tensor-parallel-size 1 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + # --micro-batch-size 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 2048 +) + +ROLLOUT_ARGS=( + --prompt-data /root/swe_train.jsonl + --input-key prompt + --metadata-key metadata + --rollout-shuffle + --num-rollout 3000 + --rollout-batch-size 8 + --n-samples-per-prompt 8 + --rollout-temperature 0.8 + --rollout-max-response-len 8192 + + --global-batch-size 64 + --balance-data +) + +EVAL_ARGS=( + # --eval-interval 50 + # --eval-prompt-data /workspace/data/swe_gym_val.jsonl + # --eval-input-key prompt + # --eval-metadata-key metadata + # --n-samples-per-eval-prompt 1 + # --eval-max-response-len 4096 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.01 + --kl-loss-type low_var_kl + --entropy-coef 0.0 + --eps-clip 0.2 + --eps-clip-high 0.28 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +WANDB_ARGS=() +if [ -n "$WANDB_KEY" ]; then + WANDB_ARGS=( + --use-wandb + --wandb-project miles-swe-agent + --wandb-group swe-agent-qwen2.5-3b + --wandb-key ${WANDB_KEY} + ) +fi + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 +) + +MISC_ARGS=( + # default dropout in megatron is 0.1 + --attention-dropout 0.0 + --hidden-dropout 0.0 + # should be good for model performance + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + # need to comment this when using model with MLA + --attention-backend flash +) + +CUSTOM_ARGS=( + --custom-generate-function-path generate_with_swe_agent.generate + --custom-rm-path generate_with_swe_agent.reward_func + --rollout-function-path generate_with_swe_agent.generate_rollout + --dynamic-sampling-filter-path generate_with_swe_agent.dynamic_filter +) + +export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} +echo "Starting Ray cluster at ${MASTER_ADDR}..." +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 4 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 --port=8899 + +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/:${SCRIPT_DIR}:/root/miles\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"SWE_AGENT_GYM_URL\": \"${SWE_AGENT_GYM_URL}\" + } +}" +# \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + +echo "Launching training..." +echo " SWE Agent URL: ${SWE_AGENT_GYM_URL}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 4 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} \ + ${CUSTOM_ARGS[@]} + +echo "Training completed!" From 7434e9a4574afce384b3e80354ed1dcebd7f7dc6 Mon Sep 17 00:00:00 2001 From: Ratish P <114130421+Ratish1@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:56:06 +0400 Subject: [PATCH 09/57] add background health check to miles native router (#260) Co-authored-by: zhaochenyang20 --- miles/router/router.py | 106 ++++++++++++++++++++++++++++++++------- miles/utils/arguments.py | 6 +++ 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/miles/router/router.py b/miles/router/router.py index 88179a293..2e8ecfc41 100644 --- a/miles/router/router.py +++ b/miles/router/router.py @@ -1,5 +1,7 @@ import argparse +import asyncio import json +import logging import httpx import uvicorn @@ -9,6 +11,8 @@ from miles.utils.misc import load_function +logger = logging.getLogger(__name__) + def run_router(args): """ @@ -28,9 +32,14 @@ def __init__(self, args, verbose=False): self.verbose = verbose self.app = FastAPI() - - # Worker information - self.worker_urls: dict[str, int] = {} + self.app.add_event_handler("startup", self._start_background_health_check) + + # URL -> Active Request Count (load state) + self.worker_request_counts: dict[str, int] = {} + # URL -> Consecutive Failures + self.worker_failure_counts: dict[str, int] = {} + # Quarantined workers excluded from routing pool + self.dead_workers: set[str] = set() self.max_weight_version = None max_connections = getattr(args, "miles_router_max_connections", None) @@ -63,9 +72,61 @@ def _setup_routes(self): # Catch-all route for proxying to SGLang - must be registered LAST self.app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])(self.proxy) - async def health_check(self, request: Request): - # TODO: do health check in background - pass + async def _start_background_health_check(self): + asyncio.create_task(self._health_check_loop()) + + async def _check_worker_health(self, url): + """Encapsulated health check logic for better maintainability.""" + try: + response = await self.client.get(f"{url}/health", timeout=5.0) + if response.status_code == 200: + return url, True + logger.debug(f"[miles-router] Worker {url} is unhealthy (Status: {response.status_code})") + except Exception as e: + logger.debug(f"[miles-router] Worker {url} health check failed: {e}") + return url, False + + async def _health_check_loop(self): + """Background loop to monitor worker health and adjust routing pool.""" + interval = self.args.rollout_health_check_interval + threshold = self.args.miles_router_health_check_failure_threshold + + while True: + try: + await asyncio.sleep(interval) + + urls = [u for u in self.worker_request_counts if u not in self.dead_workers] + if not urls: + continue + + results = await asyncio.gather(*(self._check_worker_health(url) for url in urls)) + + for url, is_healthy in results: + if not is_healthy: + failures = self.worker_failure_counts.get(url, 0) + 1 + self.worker_failure_counts[url] = failures + + if failures >= threshold: + logger.warning( + f"[miles-router] Worker {url} failed {threshold} consecutive health checks. Marking as DEAD." + ) + self.dead_workers.add(url) + # TODO (chenyang): Connect back 'dead' workers requires a mechanism to sync + # model versions to avoid off-policy issues from stale weights, since these + # dead workers' parameters may not be refitted. + else: + self.worker_failure_counts[url] = 0 + + logger.debug( + f"[miles-router] Health check complete. {len(self.worker_request_counts) - len(self.dead_workers)} workers healthy." + ) + + except asyncio.CancelledError: + logger.warning("[miles-router] Background health check loop is being cancelled.") + raise + except Exception as e: + logger.error(f"[miles-router] Unexpected error in health check loop: {e}", exc_info=True) + await asyncio.sleep(5) async def proxy(self, request: Request, path: str): """Proxy all other requests to the SGLang router""" @@ -124,16 +185,17 @@ async def add_worker(self, request: Request): ) # Add if new, keep a simple request count per worker - if worker_url not in self.worker_urls: - self.worker_urls[worker_url] = 0 + if worker_url not in self.worker_request_counts: + self.worker_request_counts[worker_url] = 0 + self.worker_failure_counts[worker_url] = 0 if self.verbose: print(f"[miles-router] Added new worker: {worker_url}") - return {"status": "success", "worker_urls": self.worker_urls} + return {"status": "success", "worker_urls": self.worker_request_counts} async def list_workers(self, request: Request): """List all registered workers""" - return {"urls": list(self.worker_urls.keys())} + return {"urls": list(self.worker_request_counts.keys())} async def retrieve_from_text(self, request: Request): """Get token information from text input""" @@ -158,19 +220,27 @@ async def retrieve_from_text(self, request: Request): return result def _use_url(self): - """Select a worker URL using round-robin strategy""" - assert len(self.worker_urls) > 0, "No workers available" + """Select worker URL with minimal active requests.""" + + if not self.dead_workers: + # Healthy path: select from all workers + url = min(self.worker_request_counts, key=self.worker_request_counts.get) + else: + # Degraded path: select from workers not in dead_workers + valid_workers = (w for w in self.worker_request_counts if w not in self.dead_workers) + try: + url = min(valid_workers, key=self.worker_request_counts.get) + except ValueError: + raise RuntimeError("No healthy workers available in the pool") from None - # get the url with mininal count - url = min(self.worker_urls, key=self.worker_urls.get) - self.worker_urls[url] += 1 + self.worker_request_counts[url] += 1 return url def _finish_url(self, url): """Mark the request to the given URL as finished""" - assert url in self.worker_urls, f"URL {url} not recognized" - self.worker_urls[url] -= 1 - assert self.worker_urls[url] >= 0, f"URL {url} count went negative" + assert url in self.worker_request_counts, f"URL {url} not recognized" + self.worker_request_counts[url] -= 1 + assert self.worker_request_counts[url] >= 0, f"URL {url} count went negative" if __name__ == "__main__": diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 824b3a028..ea2ec0a33 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -919,6 +919,12 @@ def add_router_arguments(parser): default=None, help="Max connections for MilesRouter HTTP client.", ) + parser.add_argument( + "--miles-router-health-check-failure-threshold", + type=int, + default=3, + help="Number of consecutive failures before marking a worker as unhealthy.", + ) RouterArgs.add_cli_args(parser, use_router_prefix=True, exclude_host_port=True) return parser From c77bdcf2a7b2877ef4d10c3a70016a7b0865305a Mon Sep 17 00:00:00 2001 From: miles-code-angel Date: Mon, 5 Jan 2026 13:32:45 -0800 Subject: [PATCH 10/57] update code (#401) Co-authored-by: Ratish1 " --- .github/workflows/conda-ci.yml | 90 + .github/workflows/pr-test.yml | 158 +- .github/workflows/pr-test.yml.j2 | 48 +- .github/workflows/release-docs.yaml | 53 + docker/Dockerfile | 2 +- docker/justfile | 3 + docker/patch/latest/sglang.patch | 1817 ++--------------- docker/version.txt | 2 +- examples/swe-agent/README.md | 2 +- miles/backends/fsdp_utils/arguments.py | 1 - miles/backends/megatron_utils/actor.py | 5 + miles/backends/megatron_utils/data.py | 15 +- miles/backends/megatron_utils/loss.py | 6 +- miles/backends/megatron_utils/model.py | 55 + miles/backends/sglang_utils/arguments.py | 5 - miles/backends/sglang_utils/sglang_engine.py | 25 +- miles/ray/placement_group.py | 1 + miles/ray/rollout.py | 133 +- miles/rollout/filter_hub/base_types.py | 30 + miles/rollout/rm_hub/math_utils.py | 14 +- miles/rollout/sglang_rollout.py | 102 +- .../middleware_hub/radix_tree_middleware.py | 18 + miles/utils/arguments.py | 29 +- miles/utils/data.py | 40 +- .../debug_utils/display_debug_rollout_data.py | 4 +- miles/utils/metric_utils.py | 2 + miles/utils/misc.py | 3 + miles/utils/types.py | 91 +- miles/utils/wandb_utils.py | 13 - .../run-gptoss-20b-fsdp.sh | 28 +- scripts/run-qwen3-235B-A22B-sft.sh | 2 +- scripts/run-qwen3-4B-amd.sh | 8 +- tests/test_mimo_7B_mtp_only_grad.py | 4 + train.py | 10 +- train_async.py | 10 +- 35 files changed, 972 insertions(+), 1857 deletions(-) create mode 100644 .github/workflows/conda-ci.yml create mode 100644 .github/workflows/release-docs.yaml rename tests/test_fsdp_gptoss_20b.sh => scripts/run-gptoss-20b-fsdp.sh (88%) diff --git a/.github/workflows/conda-ci.yml b/.github/workflows/conda-ci.yml new file mode 100644 index 000000000..332ced7f2 --- /dev/null +++ b/.github/workflows/conda-ci.yml @@ -0,0 +1,90 @@ +name: conda CI + +on: + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-conda: + if: contains(github.event.pull_request.title, '[release]') + runs-on: self-hosted + container: + image: lmsysorg/sglang:v0.5.0rc0-cu126 + options: --gpus all --ipc=host --shm-size=16g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 -v /mnt/nvme0n1/models:/root/models -v /mnt/nvme0n1/datasets:/root/datasets + + defaults: + run: + working-directory: ${{ github.workspace }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Construct Conda + run: | + echo "📦 Installing miles..." + cd $GITHUB_WORKSPACE + echo "Current directory: $(pwd)" + + mkdir -p /root/ + BASE_DIR=/root bash build_conda.sh + shell: bash + + - name: Download model and dataset + run: | + echo "🔗 Downloading up model and dataset..." + + # Create cache directories if they don't exist + mkdir -p /root/models /root/datasets + + echo "Downloading Qwen3-30B-A3B..." + hf download Qwen/Qwen3-30B-A3B --local-dir /root/models/Qwen3-30B-A3B + hf download Qwen/Qwen3-30B-A3B-FP8 --local-dir /root/models/Qwen3-30B-A3B-FP8 + + hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/datasets/dapo-math-17k + + hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/datasets/aime-2024 + shell: bash + + - name: Convert checkpoint + run: | + echo "🔄 Converting model checkpoint..." + cd $GITHUB_WORKSPACE + echo "Current directory: $(pwd)" + + source ~/.bashrc + micromamba activate miles + export CUDA_HOME="$CONDA_PREFIX" + + source scripts/models/qwen3-30B-A3B.sh + PYTHONPATH=/root/Megatron-LM torchrun --nproc-per-node 8 tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/models/Qwen3-30B-A3B \ + --save /root/Qwen3-30B-A3B_torch_dist + shell: bash + + - name: Run tests + run: | + echo "🧪 Running tests..." + cd $GITHUB_WORKSPACE + echo "Current directory: $(pwd)" + + source ~/.bashrc + micromamba activate miles + export CUDA_HOME="$CONDA_PREFIX" + + MILES_TEST_USE_DEEPEP=0 MILES_TEST_USE_FP8_ROLLOUT=0 python tests/test_qwen3_30B_A3B.py + shell: bash + + - name: Cleanup + if: always() + run: | + echo "🧹 Cleaning up..." + pkill -9 ray || true + ray stop --force || true + pkill -9 python || true + shell: bash diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index e649da717..ce9c2daaa 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -38,13 +38,17 @@ jobs: --ulimit stack=67108864 --memory=0 --memory-swap=0 - -v /data/miles_ci:/data/miles_ci - -v /data/miles_ci/models:/root/models - -v /data/miles_ci/datasets:/root/datasets + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets strategy: fail-fast: false matrix: - info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}] + info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}] defaults: run: working-directory: ${{ github.workspace }} @@ -59,7 +63,7 @@ jobs: - name: Install shell: bash - run: cd $GITHUB_WORKSPACE && pip install -e . + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages - name: Execute shell: bash @@ -78,9 +82,13 @@ jobs: --ulimit stack=67108864 --memory=0 --memory-swap=0 - -v /data/miles_ci:/data/miles_ci - -v /data/miles_ci/models:/root/models - -v /data/miles_ci/datasets:/root/datasets + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets strategy: fail-fast: false matrix: @@ -99,7 +107,139 @@ jobs: - name: Install shell: bash - run: cd $GITHUB_WORKSPACE && pip install -e . + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages + + - name: Execute + shell: bash + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + + e2e-test-precision: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-precision')) + runs-on: self-hosted + container: + image: radixark/miles:latest + options: > + --gpus all + --ipc=host + --shm-size=16g + --ulimit memlock=-1 + --ulimit stack=67108864 + --memory=0 + --memory-swap=0 + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + strategy: + fail-fast: false + matrix: + info: [{"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}] + defaults: + run: + working-directory: ${{ github.workspace }} + env: + GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} + WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} + MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install + shell: bash + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages + + - name: Execute + shell: bash + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + + e2e-test-ckpt: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-ckpt')) + runs-on: self-hosted + container: + image: radixark/miles:latest + options: > + --gpus all + --ipc=host + --shm-size=16g + --ulimit memlock=-1 + --ulimit stack=67108864 + --memory=0 + --memory-swap=0 + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + strategy: + fail-fast: false + matrix: + info: [{"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py --async-save"}] + defaults: + run: + working-directory: ${{ github.workspace }} + env: + GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} + WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} + MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install + shell: bash + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages + + - name: Execute + shell: bash + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + + e2e-test-image: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-image')) + runs-on: self-hosted + container: + image: radixark/miles-test:latest + options: > + --gpus all + --ipc=host + --shm-size=16g + --ulimit memlock=-1 + --ulimit stack=67108864 + --memory=0 + --memory-swap=0 + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + strategy: + fail-fast: false + matrix: + info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py --async-save"}] + defaults: + run: + working-directory: ${{ github.workspace }} + env: + GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} + WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} + MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install + shell: bash + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages - name: Execute shell: bash diff --git a/.github/workflows/pr-test.yml.j2 b/.github/workflows/pr-test.yml.j2 index 06d6ed570..12bfae9fe 100644 --- a/.github/workflows/pr-test.yml.j2 +++ b/.github/workflows/pr-test.yml.j2 @@ -4,6 +4,11 @@ 'tests': [ {'test_file': 'test_quick_start_glm4_9B.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_ppo.py', 'num_gpus': 8}, + {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_fsdp_true_on_policy.py', 'num_gpus': 2}, + {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_vl_4B_fsdp.py', 'num_gpus': 8}, ], }, 'e2e-test-long': { @@ -15,6 +20,35 @@ {'test_file': 'test_qwen3_0.6B_fsdp_distributed.py', 'num_gpus': 2}, ], }, + 'e2e-test-precision': { + 'label': 'run-ci-precision', + 'tests': [ + {'test_file': 'test_qwen3_0.6B_parallel_check.py', 'num_gpus': 8}, + ], + }, + 'e2e-test-ckpt': { + 'label': 'run-ci-ckpt', + 'tests': [ + {'test_file': 'test_qwen3_4B_ckpt.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_ckpt.py --async-save', 'num_gpus': 8}, + ], + }, + 'e2e-test-image': { + 'label': 'run-ci-image', + 'image': 'radixark/miles-test:latest', + 'tests': [ + {'test_file': 'test_quick_start_glm4_9B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_ppo.py', 'num_gpus': 8}, + {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_fsdp_true_on_policy.py', 'num_gpus': 2}, + {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_vl_4B_fsdp.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_0.6B_parallel_check.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_ckpt.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_ckpt.py --async-save', 'num_gpus': 8}, + ], + }, } %> name: PR Test @@ -43,7 +77,7 @@ jobs: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, '<< config.label >>')) runs-on: self-hosted container: - image: radixark/miles:latest + image: << config.image if config.image else 'radixark/miles:latest' >> options: > --gpus all --ipc=host @@ -52,9 +86,13 @@ jobs: --ulimit stack=67108864 --memory=0 --memory-swap=0 - -v /data/miles_ci:/data/miles_ci - -v /data/miles_ci/models:/root/models - -v /data/miles_ci/datasets:/root/datasets + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets strategy: fail-fast: false matrix: @@ -73,7 +111,7 @@ jobs: - name: Install shell: bash - run: cd $GITHUB_WORKSPACE && pip install -e . + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages - name: Execute shell: bash diff --git a/.github/workflows/release-docs.yaml b/.github/workflows/release-docs.yaml new file mode 100644 index 000000000..1da468e00 --- /dev/null +++ b/.github/workflows/release-docs.yaml @@ -0,0 +1,53 @@ +name: Release Documentation + +on: + push: + branches: + - main + paths: + - "docs/**" + - "examples/**" + - "version.txt" + workflow_dispatch: + +concurrency: + group: release-docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + if: github.repository == 'radixark/miles' + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + + - name: Install dependencies + run: | + apt-get update && apt-get install -y pandoc parallel retry + pip install -r docs/requirements.txt + + - name: Build documentation + run: | + cd docs + bash ./build.sh en + bash ./build.sh zh + mv ./build/zh ./build/en/ + env: + LC_ALL: "en_US.UTF-8" + LC_CTYPE: "en_US.UTF-8" + + + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/build/en \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 94c2dbde6..1f63e3db4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG SGLANG_IMAGE_TAG=nightly-dev-20251208-5e2cda61 +ARG SGLANG_IMAGE_TAG=nightly-dev-20260103-24c91001 FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang # ======================================== Arguments ============================================= diff --git a/docker/justfile b/docker/justfile index aac41e6b2..d8a512d3e 100644 --- a/docker/justfile +++ b/docker/justfile @@ -35,3 +35,6 @@ debug: docker build -f docker/Dockerfile . --build-arg HTTP_PROXY="$http_proxy" --build-arg HTTPS_PROXY="$https_proxy" --build-arg NO_PROXY="localhost,127.0.0.1" -t radixark/miles-test:$IMAGE_TAG docker push radixark/miles-test:$IMAGE_TAG + + docker tag radixark/miles-test:$IMAGE_TAG radixark/miles-test:latest + docker push radixark/miles-test:latest diff --git a/docker/patch/latest/sglang.patch b/docker/patch/latest/sglang.patch index 600706288..42d23ed65 100644 --- a/docker/patch/latest/sglang.patch +++ b/docker/patch/latest/sglang.patch @@ -1,8 +1,8 @@ diff --git a/python/sglang/srt/disaggregation/decode.py b/python/sglang/srt/disaggregation/decode.py -index ef52bda7f..537d892dc 100644 +index 199885244..742ad0639 100644 --- a/python/sglang/srt/disaggregation/decode.py +++ b/python/sglang/srt/disaggregation/decode.py -@@ -296,6 +296,13 @@ class DecodePreallocQueue: +@@ -314,6 +314,13 @@ class DecodePreallocQueue: ) return kv_manager @@ -16,23 +16,11 @@ index ef52bda7f..537d892dc 100644 def add(self, req: Req, is_retracted: bool = False) -> None: """Add a request to the pending queue.""" if self._check_if_req_exceed_kv_capacity(req): -diff --git a/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py b/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py -index efa979460..d2d049a20 100644 ---- a/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py -+++ b/python/sglang/srt/disaggregation/decode_schedule_batch_mixin.py -@@ -83,6 +83,7 @@ class ScheduleBatchDisaggregationDecodeMixin: - seq_lens, dtype=torch.int32, device=self.device - ) - self.out_cache_loc = out_cache_loc -+ self.out_cache_loc_cpu = out_cache_loc.to("cpu", non_blocking=True) - self.seq_lens_sum = sum(seq_lens) - - if self.return_logprob: diff --git a/python/sglang/srt/disaggregation/mooncake/conn.py b/python/sglang/srt/disaggregation/mooncake/conn.py -index d4414d084..c5fb10155 100644 +index 32e8c0b69..df913da7b 100644 --- a/python/sglang/srt/disaggregation/mooncake/conn.py +++ b/python/sglang/srt/disaggregation/mooncake/conn.py -@@ -1074,6 +1074,19 @@ class MooncakeKVManager(CommonKVManager): +@@ -1079,6 +1079,19 @@ class MooncakeKVManager(CommonKVManager): f"Losing connection with prefill instance (bootstrap_addr: {failed_bootstrap_addr}), {len(affected_rooms)} requests affected" ) @@ -53,10 +41,10 @@ index d4414d084..c5fb10155 100644 class MooncakeKVSender(CommonKVSender): diff --git a/python/sglang/srt/disaggregation/prefill.py b/python/sglang/srt/disaggregation/prefill.py -index 952374ed5..239ac2571 100644 +index ac11013f8..478e469f6 100644 --- a/python/sglang/srt/disaggregation/prefill.py +++ b/python/sglang/srt/disaggregation/prefill.py -@@ -305,6 +305,13 @@ class PrefillBootstrapQueue: +@@ -309,6 +309,13 @@ class PrefillBootstrapQueue: else: return bootstrapped_reqs, failed_reqs @@ -71,10 +59,10 @@ index 952374ed5..239ac2571 100644 class SchedulerDisaggregationPrefillMixin: """ diff --git a/python/sglang/srt/distributed/parallel_state.py b/python/sglang/srt/distributed/parallel_state.py -index cf90f6fe0..11d26df81 100644 +index 0478526ef..cfb1aa669 100644 --- a/python/sglang/srt/distributed/parallel_state.py +++ b/python/sglang/srt/distributed/parallel_state.py -@@ -1780,7 +1780,10 @@ def get_tensor_model_parallel_world_size(): +@@ -1797,7 +1797,10 @@ def get_tensor_model_parallel_world_size(): def get_tensor_model_parallel_rank(): """Return my rank for the tensor model parallel group.""" @@ -86,111 +74,11 @@ index cf90f6fe0..11d26df81 100644 def get_pipeline_model_parallel_world_size(): -diff --git a/python/sglang/srt/entrypoints/engine.py b/python/sglang/srt/entrypoints/engine.py -index 67a082ea6..390365864 100644 ---- a/python/sglang/srt/entrypoints/engine.py -+++ b/python/sglang/srt/entrypoints/engine.py -@@ -183,6 +183,7 @@ class Engine(EngineBase): - lora_path: Optional[List[Optional[str]]] = None, - custom_logit_processor: Optional[Union[List[str], str]] = None, - return_hidden_states: bool = False, -+ return_routed_experts: bool = False, - stream: bool = False, - bootstrap_host: Optional[Union[List[str], str]] = None, - bootstrap_port: Optional[Union[List[int], int]] = None, -@@ -218,6 +219,7 @@ class Engine(EngineBase): - lora_path=lora_path, - custom_logit_processor=custom_logit_processor, - return_hidden_states=return_hidden_states, -+ return_routed_experts=return_routed_experts, - stream=stream, - bootstrap_host=bootstrap_host, - bootstrap_port=bootstrap_port, -diff --git a/python/sglang/srt/layers/attention/vision.py b/python/sglang/srt/layers/attention/vision.py -index 9f556a885..992843285 100644 ---- a/python/sglang/srt/layers/attention/vision.py -+++ b/python/sglang/srt/layers/attention/vision.py -@@ -518,11 +518,25 @@ class VisionAttention(nn.Module): - self.dummy_dim = (num_dummy_heads + num_heads) * self.head_size - - if self.qk_normalization: -+ norm_kwargs = ( -+ dict( -+ weight_dtype=torch.float32, -+ cast_x_before_out_mul=True, -+ ) -+ if get_global_server_args().rl_on_policy_target is not None -+ else {} -+ ) - self.q_norm = RMSNorm( -- self.dummy_dim, eps=layer_norm_eps, var_hidden_size=embed_dim -+ self.dummy_dim, -+ eps=layer_norm_eps, -+ var_hidden_size=embed_dim, -+ **norm_kwargs, - ) - self.k_norm = RMSNorm( -- self.dummy_dim, eps=layer_norm_eps, var_hidden_size=embed_dim -+ self.dummy_dim, -+ eps=layer_norm_eps, -+ var_hidden_size=embed_dim, -+ **norm_kwargs, - ) - - # Select attention backend via a unified method -@@ -648,6 +662,15 @@ class VisionAttention(nn.Module): - if x.dim() == 2: - x = x.unsqueeze(0) - assert x.dim() == 3, x.shape -+ if ( -+ get_global_server_args().rl_on_policy_target is not None -+ and position_embeddings is not None -+ ): -+ assert isinstance(position_embeddings, tuple), ( -+ "expected position_embeddings to be a tuple of two tensors,\n" -+ f"but got {type(position_embeddings)}, change if needed" -+ ) -+ position_embeddings = tuple(p.to(x.dtype) for p in position_embeddings) - x_shape = x.shape - bsz, s, _ = x_shape - head = self.num_attention_heads_per_partition -diff --git a/python/sglang/srt/layers/communicator.py b/python/sglang/srt/layers/communicator.py -index 932f52aeb..ee52f4c94 100644 ---- a/python/sglang/srt/layers/communicator.py -+++ b/python/sglang/srt/layers/communicator.py -@@ -372,6 +372,7 @@ class LayerCommunicator: - residual: torch.Tensor, - forward_batch: ForwardBatch, - quant_format: str = "", -+ **kwargs, - ): - if get_attn_tp_context().input_scattered: - hidden_states, residual = self._tp_reduce_scatter( -@@ -421,7 +422,7 @@ class LayerCommunicator: - ) - - else: -- hidden_states = self.input_layernorm(hidden_states) -+ hidden_states = self.input_layernorm(hidden_states, **kwargs) - else: - - if _use_aiter and _is_gfx95_supported and ("mxfp4" in quant_format): -@@ -453,7 +454,9 @@ class LayerCommunicator: - ) - else: - hidden_states, residual = self.input_layernorm( -- hidden_states, residual -+ hidden_states, -+ residual, -+ **kwargs, - ) - - hidden_states = self._communicate_simple_fn( diff --git a/python/sglang/srt/layers/layernorm.py b/python/sglang/srt/layers/layernorm.py -index 3293a8a59..1dac03e4c 100644 +index b07164c53..8e6722ce0 100644 --- a/python/sglang/srt/layers/layernorm.py +++ b/python/sglang/srt/layers/layernorm.py -@@ -84,15 +84,12 @@ class RMSNorm(CustomOp): +@@ -83,15 +83,12 @@ class RMSNorm(MultiPlatformOp): eps: float = 1e-6, var_hidden_size: Optional[int] = None, cast_x_before_out_mul: bool = False, @@ -208,73 +96,13 @@ index 3293a8a59..1dac03e4c 100644 self.variance_epsilon = eps self.hidden_size = hidden_size self.variance_size_override = ( -@@ -105,21 +102,29 @@ class RMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if self.variance_size_override is not None: -- return self.forward_native(x, residual) -+ return self.forward_native(x, residual, **kwargs) - if is_batch_invariant_mode_enabled(): - if ( - residual is not None - or get_global_server_args().rl_on_policy_target == "fsdp" - ): -- return self.forward_native(x, residual) -+ return self.forward_native(x, residual, **kwargs) - return rms_norm_batch_invariant( - x, - self.weight.data, - self.variance_epsilon, - ) - if residual is not None: -+ # TODO: Ideally we want to have (hidden_states+residual)+post_residual_addition. -+ # but right now we can only have hidden_states+(residual+post_residual_addition). -+ # (hidden_states+residual)+post_residual_addition != hidden_states+(residual+post_residual_addition), -+ # we probably need to add another parameter to fused_add_rmsnorm -+ post_residual_addition = kwargs.get("post_residual_addition") -+ if post_residual_addition is not None: -+ residual = residual + post_residual_addition - fused_add_rmsnorm(x, residual, self.weight.data, self.variance_epsilon) - return x, residual - out = rmsnorm(x, self.weight.data, self.variance_epsilon) -@@ -129,6 +134,7 @@ class RMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if residual is not None: - out, _, residual_out = torch_npu.npu_add_rms_norm( -@@ -141,6 +147,7 @@ class RMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if residual is not None: - residual_out = torch.empty_like(x) -@@ -160,6 +167,7 @@ class RMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if not x.is_contiguous(): - # NOTE: Remove this if aiter kernel supports discontinuous input -@@ -179,17 +187,36 @@ class RMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, +@@ -194,10 +191,22 @@ class RMSNorm(MultiPlatformOp): ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: if not x.is_contiguous(): x = x.contiguous() - orig_dtype = self.override_orig_dtype or x.dtype + orig_dtype = x.dtype -+ post_residual_addition = kwargs.get("post_residual_addition") + post_residual_addition = kwargs.get("post_residual_addition") + + if residual is not None and not self.fp32_residual: + x = ( @@ -289,172 +117,27 @@ index 3293a8a59..1dac03e4c 100644 + residual = x.clone() x = x.to(torch.float32) - if residual is not None: -- x = x + residual.to(torch.float32) ++ if residual is not None and self.fp32_residual: + x = ( + x + + residual.to(torch.float32) +@@ -207,10 +216,7 @@ class RMSNorm(MultiPlatformOp): + else 0.0 + ) + ) - if self.fp32_residual: - residual = x.clone() - else: - residual = x.to(orig_dtype) -+ if residual is not None and self.fp32_residual: -+ x = ( -+ x -+ + residual.to(torch.float32) -+ + ( -+ post_residual_addition.to(torch.float32) -+ if post_residual_addition is not None -+ else 0.0 -+ ) -+ ) + residual = x.to(orig_dtype) hidden_size = x.shape[-1] if hidden_size != self.hidden_size: -@@ -226,6 +253,7 @@ class RMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if _is_cpu_amx_available: - if residual is not None: -@@ -237,15 +265,16 @@ class RMSNorm(CustomOp): - x, self.weight.data, self.variance_epsilon - ) - else: -- return self.forward_native(x, residual) -+ return self.forward_native(x, residual, **kwargs) - - def forward_xpu( - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if self.variance_size_override is not None: -- return self.forward_native(x, residual) -+ return self.forward_native(x, residual, **kwargs) - if residual is not None: - fused_add_rmsnorm(x, residual, self.weight.data, self.variance_epsilon) - return x, residual -@@ -307,6 +336,7 @@ class LayerNorm(CustomOp): - def forward_cuda( - self, - x: torch.Tensor, -+ **kwargs, - ) -> torch.Tensor: - if ( - _flashinfer_layernorm_available -@@ -315,11 +345,12 @@ class LayerNorm(CustomOp): - ): - return layernorm(x, self.weight, self.bias, self.variance_epsilon) - else: -- return self.forward_native(x) -+ return self.forward_native(x, **kwargs) - - def forward_native( - self, - x: torch.Tensor, -+ **kwargs, - ) -> torch.Tensor: - weight = self.weight if self.elementwise_affine else None - bias = self.bias if self.use_bias else None -@@ -336,12 +367,14 @@ class LayerNorm(CustomOp): - def forward_hip( - self, - x: torch.Tensor, -+ **kwargs, - ) -> torch.Tensor: -- return self.forward_native(x) -+ return self.forward_native(x, **kwargs) - - def forward_npu( - self, - x: torch.Tensor, -+ **kwargs, - ) -> torch.Tensor: - orig_dtype = x.dtype - x = x.to(self.dtype) -@@ -360,8 +393,9 @@ class LayerNorm(CustomOp): - def forward_cpu( - self, - x: torch.Tensor, -+ **kwargs, - ) -> torch.Tensor: -- return self.forward_native(x) -+ return self.forward_native(x, **kwargs) - - - class GemmaRMSNorm(CustomOp): -@@ -382,6 +416,7 @@ class GemmaRMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if residual is not None: - gemma_fused_add_rmsnorm( -@@ -395,6 +430,7 @@ class GemmaRMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - orig_dtype = x.dtype - if residual is not None: -@@ -412,13 +448,15 @@ class GemmaRMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: -- return self._forward_impl(x, residual) -+ return self._forward_impl(x, residual, **kwargs) - - def forward_npu( - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - if residual is not None: - x = x + residual -@@ -431,8 +469,9 @@ class GemmaRMSNorm(CustomOp): - self, - x: torch.Tensor, - residual: Optional[torch.Tensor] = None, -+ **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: -- return self._forward_impl(x, residual) -+ return self._forward_impl(x, residual, **kwargs) - - - class Gemma3RMSNorm(CustomOp): -@@ -445,17 +484,17 @@ class Gemma3RMSNorm(CustomOp): - def _norm(self, x): - return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) - -- def forward_native(self, x): -+ def forward_native(self, x, **kwargs): - output = self._norm(x.float()) - # Llama does x.to(float16) * w whilst Gemma3 is (x * w).to(float16) - # See https://github.com/huggingface/transformers/pull/29402 - output = output * (1.0 + self.weight.float()) - return output.type_as(x) - -- def forward_cuda(self, x): -- return self.forward_native(x) -+ def forward_cuda(self, x, **kwargs): -+ return self.forward_native(x, **kwargs) - -- def forward_npu(self, x): -+ def forward_npu(self, x, **kwargs): - output, _ = torch_npu.npu_gemma_rms_norm(x, self.weight, self.eps) - return output - diff --git a/python/sglang/srt/layers/logits_processor.py b/python/sglang/srt/layers/logits_processor.py -index 522865765..733bad5f2 100644 +index fa7431048..cd33ea735 100644 --- a/python/sglang/srt/layers/logits_processor.py +++ b/python/sglang/srt/layers/logits_processor.py -@@ -841,11 +841,6 @@ class LogitsProcessor(nn.Module): +@@ -878,11 +878,6 @@ class LogitsProcessor(nn.Module): None, # bias True, # is_vnni ) @@ -467,7 +150,7 @@ index 522865765..733bad5f2 100644 logits = torch.matmul( hidden_states.to(lm_head.weight.dtype), lm_head.weight.T diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py b/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py -index e7d5a67cc..639e47163 100644 +index a1885fade..14d692365 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py @@ -14,6 +14,7 @@ import torch.nn.functional as F @@ -477,8 +160,8 @@ index e7d5a67cc..639e47163 100644 +from sglang.srt.server_args import get_global_server_args from sglang.srt.utils import ( cpu_has_amx_support, - direct_register_custom_op, -@@ -626,7 +627,10 @@ def fused_experts_impl( + get_bool_env_var, +@@ -573,7 +574,10 @@ def fused_experts_impl( ).squeeze(dim=1) else: # According to micro benchmark results, torch.compile can get better performance for small token. @@ -491,198 +174,39 @@ index e7d5a67cc..639e47163 100644 intermediate_cache3.view(*intermediate_cache3.shape), out_hidden_states[begin_chunk_idx:end_chunk_idx], diff --git a/python/sglang/srt/layers/moe/routed_experts_capturer.py b/python/sglang/srt/layers/moe/routed_experts_capturer.py -new file mode 100644 -index 000000000..11adcaa77 ---- /dev/null +index 00bd68755..5a3ca8a67 100644 +--- a/python/sglang/srt/layers/moe/routed_experts_capturer.py +++ b/python/sglang/srt/layers/moe/routed_experts_capturer.py -@@ -0,0 +1,305 @@ -+import logging -+from abc import ABC +@@ -1,5 +1,6 @@ + import logging + from abc import ABC +from contextlib import contextmanager -+from typing import Optional -+ -+import numpy as np -+import torch -+ -+from sglang.srt.configs.model_config import ModelConfig -+from sglang.srt.layers.dp_attention import ( + from typing import Optional + + import numpy as np +@@ -8,13 +9,18 @@ import torch + + from sglang.srt.configs.model_config import ModelConfig + from sglang.srt.layers.dp_attention import ( + attn_tp_all_gather_into_tensor, -+ get_attention_dp_rank, + get_attention_dp_rank, + get_attention_tp_size, -+ get_dp_local_info, -+ is_dp_attention_enabled, -+) -+from sglang.srt.mem_cache.memory_pool import ReqToTokenPool -+from sglang.srt.server_args import get_global_server_args + get_dp_local_info, + is_dp_attention_enabled, + ) + from sglang.srt.mem_cache.memory_pool import ReqToTokenPool + from sglang.srt.model_executor.forward_batch_info import ForwardBatch + from sglang.srt.server_args import get_global_server_args +from sglang.srt.layers.moe import ( + get_moe_a2a_backend, +) -+ -+logger = logging.getLogger(__name__) -+ -+_GB = 1024 * 1024 * 1024 -+_MB = 1024 * 1024 -+ -+ -+def get_tensor_size_bytes(t: torch.Tensor): -+ return np.prod(t.shape) * t.dtype.itemsize -+ -+ -+class _RoutedExpertsDeviceCache: -+ def __init__( -+ self, -+ max_running_requests: int, -+ num_hidden_layers: int, -+ num_experts_per_tok: int, -+ num_fused_shared_experts: int, -+ device: str, -+ ) -> None: -+ self.buffer = torch.zeros( -+ ( -+ max( -+ get_global_server_args().chunked_prefill_size -+ * get_global_server_args().dp_size, -+ max_running_requests, -+ ), -+ num_hidden_layers, -+ num_experts_per_tok + num_fused_shared_experts, -+ ), -+ dtype=torch.int32, -+ device=device, -+ ) -+ self._finalize_allocation_log() -+ -+ def get_buffer_size_bytes(self): -+ assert hasattr(self, "buffer") -+ return get_tensor_size_bytes(self.buffer) -+ -+ def capture_fwd_routed_experts(self, layer_id: int, topk_ids: torch.Tensor): -+ assert layer_id is not None, "capturing routing experts but get layer_id None" -+ batch, _ = topk_ids.shape -+ self.buffer[:batch, layer_id, :] = topk_ids -+ -+ def _finalize_allocation_log(self): -+ """Common logging and memory usage computation for captured experts buffers.""" -+ buffer_size_MB = self.get_buffer_size_bytes() / _MB -+ logger.info( -+ f"Routing experts device buffer allocated. #shape: {tuple(self.buffer.shape)}, size: {buffer_size_MB:.2f} MB" -+ ) -+ -+ -+class _RoutedExpertsHostCache: -+ def __init__( -+ self, -+ num_tokens: int, -+ num_hidden_layers: int, -+ num_experts_per_tok: int, -+ ) -> None: -+ self.num_tokens = num_tokens -+ self.buffer = torch.zeros( -+ ( -+ num_tokens, -+ num_hidden_layers, -+ num_experts_per_tok, -+ ), -+ dtype=torch.int32, -+ device="cpu", -+ pin_memory=True, -+ ) -+ self._finalize_allocation_log() -+ -+ def get_buffer_size_bytes(self): -+ assert hasattr(self, "buffer") -+ return get_tensor_size_bytes(self.buffer) -+ -+ def _finalize_allocation_log(self): -+ """Common logging and memory usage computation for captured experts buffers.""" -+ buffer_size_GB = self.get_buffer_size_bytes() / _GB -+ logger.info( -+ f"Routing experts host buffer allocated. #tokens: {self.num_tokens}, size: {buffer_size_GB:.2f} GB" -+ ) -+ -+ -+class RoutedExpertsCapturer(ABC): -+ @staticmethod -+ def create( -+ enable: bool, -+ model_config: ModelConfig, -+ num_fused_shared_experts: int, -+ num_tokens: int, -+ max_running_requests: int, -+ device: str, -+ ): -+ if enable: -+ return _RoutedExpertsCapturerReal( -+ model_config, -+ num_tokens=num_tokens, -+ max_running_requests=max_running_requests, -+ num_fused_shared_experts=num_fused_shared_experts, -+ device=device, -+ ) -+ else: -+ return _RoutedExpertsCapturerNoop() -+ -+ def capture(self, layer_id: int, topk_ids: torch.Tensor): -+ raise NotImplementedError -+ -+ def get_routed_experts( -+ self, -+ req_pool_idx: int, -+ seqlen: int, -+ req_to_token_pool: ReqToTokenPool, -+ ): -+ raise NotImplementedError -+ -+ def sync_fwd_experts_buffer_DtoH( -+ self, -+ device_loc: torch.Tensor, -+ cpu_loc: torch.Tensor, -+ can_run_graph: bool, -+ cuda_graph_batch: int, -+ ): -+ raise NotImplementedError -+ -+ @contextmanager -+ def with_forward(self, forward_batch): -+ yield -+ -+ def get_host_cache(self): -+ raise NotImplementedError -+ -+ def get_device_cache(self): -+ raise NotImplementedError -+ -+ -+class _RoutedExpertsCapturerReal(RoutedExpertsCapturer): -+ """Capturer for routed experts with host buffer""" -+ -+ def __init__( -+ self, -+ model_config: ModelConfig, -+ num_tokens: int, -+ max_running_requests: int, -+ num_fused_shared_experts: int, -+ device: str, -+ ): -+ self.forward_batch = None -+ self.num_fused_shared_experts = num_fused_shared_experts -+ self.num_hidden_layers = model_config.hf_text_config.num_hidden_layers -+ self.num_experts_per_tok = model_config.hf_text_config.num_experts_per_tok -+ -+ self.host_cache = _RoutedExpertsHostCache( -+ num_tokens=num_tokens, -+ num_hidden_layers=self.num_hidden_layers, -+ num_experts_per_tok=self.num_experts_per_tok, -+ ) -+ -+ self.device_cache = _RoutedExpertsDeviceCache( -+ max_running_requests=max_running_requests, -+ num_hidden_layers=self.num_hidden_layers, -+ num_experts_per_tok=self.num_experts_per_tok, -+ num_fused_shared_experts=self.num_fused_shared_experts, -+ device=device, -+ ) -+ + + logger = logging.getLogger(__name__) + +@@ -181,13 +187,26 @@ class _RoutedExpertsCapturerReal(RoutedExpertsCapturer): + device=device, + ) + + if get_moe_a2a_backend().is_deepep(): + attn_tp_size = get_attention_tp_size() if is_dp_attention_enabled() else 1 + self.gather_buffer = torch.empty( @@ -694,212 +218,37 @@ index 000000000..11adcaa77 + device=device, + ) + -+ def capture(self, layer_id: int, topk_ids: torch.Tensor): + def _sync_fwd_experts_buffer_DtoH( + self, + forward_batch: ForwardBatch, + can_run_graph: bool, + cuda_graph_batch: int, + ): +- if is_dp_attention_enabled(): ++ # When DeepEP is enabled, capture() already does all_gather, so device_cache.buffer ++ # contains data from all DP ranks. We should not slice by DP rank in this case. ++ if is_dp_attention_enabled() and not get_moe_a2a_backend().is_deepep(): + local_start_pos, local_num_tokens = get_dp_local_info(forward_batch) + # handle with cuda graph padding + if can_run_graph: +@@ -206,6 +225,12 @@ class _RoutedExpertsCapturerReal(RoutedExpertsCapturer): + ].cpu() + + def capture(self, layer_id: int, topk_ids: torch.Tensor): + if get_moe_a2a_backend().is_deepep(): + local_topk_ids = topk_ids + topk_ids = self.gather_buffer[ + : local_topk_ids.size(0) * get_attention_tp_size() + ] + attn_tp_all_gather_into_tensor(topk_ids, local_topk_ids) -+ self.device_cache.capture_fwd_routed_experts(layer_id, topk_ids) -+ -+ def sync_fwd_experts_buffer_DtoH( -+ self, -+ device_loc: torch.Tensor, -+ cpu_loc: torch.Tensor, -+ can_run_graph: bool, -+ cuda_graph_batch: int, -+ ): -+ # When DeepEP is enabled, capture() already does all_gather, so device_cache.buffer -+ # contains data from all DP ranks. We should not slice by DP rank in this case. -+ if is_dp_attention_enabled() and not get_moe_a2a_backend().is_deepep(): -+ local_start_pos, local_num_tokens = get_dp_local_info(self.forward_batch) -+ # handle with cuda graph padding -+ if can_run_graph: -+ local_start_pos = get_attention_dp_rank() * cuda_graph_batch -+ local_end_pos = local_start_pos + local_num_tokens -+ else: -+ local_end_pos = local_start_pos + local_num_tokens -+ else: -+ local_start_pos = 0 -+ local_end_pos = device_loc.shape[0] -+ -+ if self.forward_batch.num_token_non_padded is not None: -+ assert local_end_pos - local_start_pos >= self.forward_batch.num_token_non_padded -+ local_end_pos = local_start_pos + self.forward_batch.num_token_non_padded -+ cpu_loc = cpu_loc[: self.forward_batch.num_token_non_padded] -+ -+ self.host_cache.buffer[cpu_loc] = self.device_cache.buffer[ -+ local_start_pos:local_end_pos, :, : self.num_experts_per_tok -+ ].cpu() -+ -+ def get_routed_experts( -+ self, -+ req_pool_idx: int, -+ seqlen: int, -+ req_to_token_pool: ReqToTokenPool, -+ ): -+ cache_pool_idx = ( -+ req_to_token_pool.req_to_token[req_pool_idx][: seqlen - 1].cpu().clone() -+ ) -+ return self.get_host_cache().buffer[cache_pool_idx] -+ -+ @contextmanager -+ def with_forward(self, forward_batch): -+ self.forward_batch = forward_batch -+ yield -+ -+ def get_host_cache(self): -+ return self.host_cache -+ -+ def get_device_cache(self): -+ return self.device_cache -+ -+ -+class _RoutedExpertsCapturerNoop(RoutedExpertsCapturer): -+ def __init__(self): -+ pass -+ -+ def capture(self, layer_id: int, topk_ids: torch.Tensor): -+ pass -+ -+ def get_routed_experts( -+ self, -+ req_pool_idx: int, -+ seqlen: int, -+ req_to_token_pool: ReqToTokenPool, -+ ): -+ pass -+ -+ def sync_fwd_experts_buffer_DtoH( -+ self, -+ device_loc: torch.Tensor, -+ cpu_loc: torch.Tensor, -+ can_run_graph: bool, -+ cuda_graph_batch: int, -+ ): -+ pass -+ -+ @contextmanager -+ def with_forward(self, forward_batch): -+ yield -+ -+ def get_host_cache(self): -+ pass -+ -+ def get_device_cache(self): -+ pass -+ -+ -+_global_expert_capturer: Optional[RoutedExpertsCapturer] = _RoutedExpertsCapturerNoop() -+ -+ -+def get_global_experts_capturer(): -+ return _global_expert_capturer -+ -+ -+def set_global_experts_capturer(capturer: RoutedExpertsCapturer): -+ global _global_expert_capturer -+ _global_expert_capturer = capturer -\ No newline at end of file -diff --git a/python/sglang/srt/layers/moe/topk.py b/python/sglang/srt/layers/moe/topk.py -index a802647e8..0fd550c0c 100644 ---- a/python/sglang/srt/layers/moe/topk.py -+++ b/python/sglang/srt/layers/moe/topk.py -@@ -48,6 +48,7 @@ from sglang.srt.eplb.expert_location_dispatch import ( - ) - from sglang.srt.layers.dp_attention import is_allocation_symmetric - from sglang.srt.layers.moe import get_moe_runner_backend -+from sglang.srt.layers.moe.routed_experts_capturer import get_global_experts_capturer - from sglang.srt.utils import ( - cpu_has_amx_support, - get_bool_env_var, -@@ -212,6 +213,7 @@ class TopK(CustomOp): - self, - top_k: int, - *, -+ layer_id: Optional[int] = None, - use_grouped_topk: bool = False, - topk_group: Optional[int] = None, - num_expert_group: Optional[int] = None, -@@ -233,6 +235,7 @@ class TopK(CustomOp): - if use_grouped_topk: - assert num_expert_group is not None and topk_group is not None - -+ self.layer_id = layer_id - self.topk_config = TopKConfig( - top_k=top_k, - use_grouped_topk=use_grouped_topk, -@@ -260,6 +263,7 @@ class TopK(CustomOp): - self.topk_config.torch_native = True - return select_experts( - hidden_states=hidden_states, -+ layer_id=self.layer_id, - router_logits=router_logits, - topk_config=self.topk_config, - num_token_non_padded=num_token_non_padded, -@@ -309,6 +313,7 @@ class TopK(CustomOp): - ): - topk_output = select_experts( - hidden_states=hidden_states, -+ layer_id=self.layer_id, - router_logits=router_logits, - topk_config=self.topk_config, - num_token_non_padded=num_token_non_padded, -@@ -326,6 +331,7 @@ class TopK(CustomOp): - ) -> TopKOutput: - return select_experts( - hidden_states=hidden_states, -+ layer_id=self.layer_id, - router_logits=router_logits, - topk_config=self.topk_config, - num_token_non_padded=num_token_non_padded, -@@ -856,6 +862,7 @@ def select_experts( - router_logits: torch.Tensor, - topk_config: TopKConfig, - *, -+ layer_id: Optional[int] = None, - num_token_non_padded: Optional[torch.Tensor] = None, - expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None, - ) -> StandardTopKOutput: -@@ -983,7 +990,10 @@ def select_experts( - ) - - get_global_expert_distribution_recorder().on_select_experts(topk_ids=topk_ids) -- -+ get_global_experts_capturer().capture( -+ layer_id=layer_id, -+ topk_ids=topk_ids, -+ ) - return StandardTopKOutput(topk_weights, topk_ids, router_logits) - + self.device_cache.capture_fwd_routed_experts(layer_id, topk_ids) -diff --git a/python/sglang/srt/layers/moe/utils.py b/python/sglang/srt/layers/moe/utils.py -index 70466bb20..cd85fc2f2 100644 ---- a/python/sglang/srt/layers/moe/utils.py -+++ b/python/sglang/srt/layers/moe/utils.py -@@ -284,7 +284,7 @@ def speculative_moe_a2a_backend_context(): - global MOE_A2A_BACKEND - original_backend = MOE_A2A_BACKEND - try: -- MOE_A2A_BACKEND = MoeA2ABackend.NONE -+ MOE_A2A_BACKEND = get_speculative_moe_a2a_backend() - yield - finally: - MOE_A2A_BACKEND = original_backend + def get_routed_experts( diff --git a/python/sglang/srt/layers/rotary_embedding.py b/python/sglang/srt/layers/rotary_embedding.py -index 0cdb7e1ae..df8860409 100644 +index 56516b41b..cb2ebca60 100644 --- a/python/sglang/srt/layers/rotary_embedding.py +++ b/python/sglang/srt/layers/rotary_embedding.py -@@ -15,7 +15,6 @@ from sglang.srt.server_args import get_global_server_args - from sglang.srt.utils import ( - cpu_has_amx_support, - get_bool_env_var, -- get_compiler_backend, - is_cpu, - is_cuda, - is_hip, -@@ -132,9 +131,7 @@ class RotaryEmbedding(CustomOp): +@@ -135,9 +135,7 @@ class RotaryEmbedding(MultiPlatformOp): if get_global_server_args().rl_on_policy_target is not None: self._forward_method = self.forward_native @@ -910,82 +259,21 @@ index 0cdb7e1ae..df8860409 100644 self.position_cos, self.position_sin = None, None def _compute_inv_freq(self, base: Union[int, float]) -> torch.Tensor: -@@ -1423,6 +1420,9 @@ class MRotaryEmbedding(RotaryEmbedding): - f"Corrected mrope_section: {self.mrope_section} (sum={sum(self.mrope_section)})" - ) - -+ if get_global_server_args().rl_on_policy_target is not None: -+ self._forward_method = self.forward_native -+ - def _match_cos_sin_cache_dtype(self, query: torch.Tensor) -> None: - # __setattr__ in nn.Module (called by `self.cos_sin_cache = ...`) - # is expensive, so avoid calling it if possible -@@ -1432,8 +1432,7 @@ class MRotaryEmbedding(RotaryEmbedding): - ): - self.cos_sin_cache = self.cos_sin_cache.to(query.device, dtype=query.dtype) - -- @torch.compile(dynamic=True, backend=get_compiler_backend()) -- def _forward_native( -+ def forward_native( - self, - positions: torch.Tensor, - query: torch.Tensor, -@@ -1490,7 +1489,7 @@ class MRotaryEmbedding(RotaryEmbedding): - key = torch.cat((key_rot, key_pass), dim=-1).reshape(key_shape) - return query, key - -- def forward( -+ def forward_cuda( - self, - positions: torch.Tensor, - query: torch.Tensor, -@@ -1507,14 +1506,12 @@ class MRotaryEmbedding(RotaryEmbedding): - """ - assert positions.ndim == 1 or positions.ndim == 2 - -- if positions.ndim == 2 and self.mrope_section and _is_cuda: -- return self._forward_triton(positions, query, key) -- elif _is_npu: -- return self._forward_npu(positions, query, key) -- else: -- return self._forward_native(positions, query, key) -+ # Use Triton kernel for multimodal (2D positions) with mrope -+ if positions.ndim == 2 and self.mrope_section: -+ return self.forward_triton(positions, query, key) -+ return self.forward_native(positions, query, key, fused_set_kv_buffer_arg) - -- def _forward_triton( -+ def forward_triton( - self, - positions: torch.Tensor, - query: torch.Tensor, -@@ -1563,15 +1560,19 @@ class MRotaryEmbedding(RotaryEmbedding): - key = torch.cat((key_rot, key_pass), dim=-1).reshape(key_shape) - return query, key - -- def _forward_npu( -+ def forward_npu( - self, - positions: torch.Tensor, - query: torch.Tensor, +@@ -1577,6 +1575,9 @@ class MRotaryEmbedding(RotaryEmbedding): key: torch.Tensor, -+ fused_set_kv_buffer_arg: Optional[FusedSetKVBufferArg] = None, + fused_set_kv_buffer_arg: Optional[FusedSetKVBufferArg] = None, ) -> Tuple[torch.Tensor, torch.Tensor]: + assert ( + fused_set_kv_buffer_arg is None + ), "fused_set_kv_buffer_arg is not supported for npu implementation" # TODO: remove this when npu_mrope supports QNumHeads * QHeadSize > 4096 - if query.shape[1] > 4096: -- return self._forward_native(positions, query, key) -+ return self.forward_native(positions, query, key, fused_set_kv_buffer_arg) - rotary_mode = "half" - if self.is_neox_style: - rotary_mode = "half" + assert ( + fused_set_kv_buffer_arg is None diff --git a/python/sglang/srt/layers/sampler.py b/python/sglang/srt/layers/sampler.py -index 7f6f6a010..c4a673145 100644 +index 55bef5652..35ad68b1c 100644 --- a/python/sglang/srt/layers/sampler.py +++ b/python/sglang/srt/layers/sampler.py -@@ -105,16 +105,11 @@ class Sampler(nn.Module): +@@ -108,16 +108,11 @@ class Sampler(nn.Module): if return_logprob and SGLANG_RETURN_ORIGINAL_LOGPROB: probs_without_temp_scaling = torch.softmax(logits, dim=-1) @@ -1005,214 +293,11 @@ index 7f6f6a010..c4a673145 100644 # For ascend backend, softmax is not needed before sampling if not get_global_server_args().sampling_backend == "ascend" or ( return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB -diff --git a/python/sglang/srt/managers/detokenizer_manager.py b/python/sglang/srt/managers/detokenizer_manager.py -index 87922077e..6507d8bf5 100644 ---- a/python/sglang/srt/managers/detokenizer_manager.py -+++ b/python/sglang/srt/managers/detokenizer_manager.py -@@ -247,6 +247,12 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): - s.sent_offset = len(output_str) - output_strs.append(incremental_output) - -+ output_routed_experts = [] -+ if recv_obj.output_routed_experts is not None: -+ output_routed_experts = [ -+ output_routed_experts -+ for output_routed_experts in recv_obj.output_routed_experts -+ ] - return BatchStrOutput( - rids=recv_obj.rids, - http_worker_ipcs=recv_obj.http_worker_ipcs, -@@ -272,6 +278,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): - output_token_ids_logprobs_idx=recv_obj.output_token_ids_logprobs_idx, - output_token_entropy_val=recv_obj.output_token_entropy_val, - output_hidden_states=recv_obj.output_hidden_states, -+ output_routed_experts=output_routed_experts, - placeholder_tokens_idx=None, - placeholder_tokens_val=None, - retraction_counts=recv_obj.retraction_counts, -diff --git a/python/sglang/srt/managers/io_struct.py b/python/sglang/srt/managers/io_struct.py -index e34736cc4..5e5997a1a 100644 ---- a/python/sglang/srt/managers/io_struct.py -+++ b/python/sglang/srt/managers/io_struct.py -@@ -23,6 +23,8 @@ from dataclasses import dataclass, field - from enum import Enum - from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union - -+import torch -+ - from sglang.srt.lora.lora_registry import LoRARef - from sglang.srt.managers.schedule_batch import BaseFinishReason - from sglang.srt.multimodal.mm_utils import has_valid_data -@@ -175,6 +177,8 @@ class GenerateReqInput(BaseReq): - log_metrics: bool = True - # Whether to return hidden states - return_hidden_states: Union[List[bool], bool] = False -+ # Whether to return captured routed experts -+ return_routed_experts: bool = False - - # The modalities of the image data [image, multi-images, video] - modalities: Optional[List[str]] = None -@@ -592,6 +596,7 @@ class GenerateReqInput(BaseReq): - if isinstance(self.return_hidden_states, list) - else self.return_hidden_states - ), -+ return_routed_experts=self.return_routed_experts, - modalities=self.modalities[i] if self.modalities else None, - session_params=self.session_params, - lora_path=self.lora_path[i] if self.lora_path is not None else None, -@@ -655,6 +660,9 @@ class TokenizedGenerateReqInput(BaseReq): - # Whether to return hidden states - return_hidden_states: bool = False - -+ # Whether to return captured routed experts -+ return_routed_experts: bool = False -+ - # The input embeds - input_embeds: Optional[Union[List[List[List[float]]], List[List[float]]]] = None - -@@ -910,6 +918,9 @@ class BatchTokenIDOutput( - # Hidden states - output_hidden_states: List[List[float]] - -+ # The routed experts for each output token -+ output_routed_experts: List[torch.Tensor] -+ - # The information of placeholder tokens (e.g., image token) - # idx is the index of the token in the prompt after expansion. - # val is the length of padded tokens after expansion. -@@ -989,6 +1000,9 @@ class BatchStrOutput( - # Hidden states - output_hidden_states: List[List[float]] - -+ # The routed experts for each output token -+ output_routed_experts: List[List[int]] -+ - # The information of placeholder tokens (e.g., image token) - # idx is the index of the token in the prompt after expansion. - # val is the length of padded tokens after expansion. diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py -index c4c5a9ebb..3650ba881 100644 +index 468d8fb8a..229a9a2dc 100644 --- a/python/sglang/srt/managers/schedule_batch.py +++ b/python/sglang/srt/managers/schedule_batch.py -@@ -450,6 +450,7 @@ class Req: - session_id: Optional[str] = None, - custom_logit_processor: Optional[str] = None, - return_hidden_states: bool = False, -+ return_routed_experts: bool = False, - eos_token_ids: Optional[Set[int]] = None, - bootstrap_host: Optional[str] = None, - bootstrap_port: Optional[int] = None, -@@ -629,6 +630,12 @@ class Req: - self.output_topk_p = None - self.output_topk_index = None - -+ # capture routed experts -+ self.return_routed_experts = return_routed_experts -+ self.routed_experts: Optional[torch.Tensor] = ( -+ None # cpu tensor: shape (seqlen, topk) -+ ) -+ - # Embedding (return values) - self.embedding = None - -@@ -992,6 +999,7 @@ class Req: - self.retraction_count += 1 - - self.prefix_indices = torch.empty((0,), dtype=torch.int64) -+ self.routed_experts = [] - self.last_node = None - self.swa_uuid_for_lock = None - self.extend_input_len = 0 -@@ -1159,6 +1167,9 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - # Whether to return hidden states - return_hidden_states: bool = False - -+ # Whether to return captured experts -+ return_routed_experts: bool = False -+ - # Whether this batch is prefill-only (no token generation needed) - is_prefill_only: bool = False - -@@ -1206,6 +1217,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - device=req_to_token_pool.device, - spec_algorithm=spec_algorithm, - return_hidden_states=any(req.return_hidden_states for req in reqs), -+ return_routed_experts=any(req.return_routed_experts for req in reqs), - is_prefill_only=all(req.is_prefill_only for req in reqs), - chunked_req=chunked_req, - dllm_config=dllm_config, -@@ -1282,6 +1294,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - ) - else: - self.out_cache_loc = torch.cat(decoder_out_cache_loc) -+ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) - - if not encoder_out_cache_loc: - self.encoder_out_cache_loc = torch.zeros(0, dtype=torch.int64).to( -@@ -1457,6 +1470,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - self.req_pool_indices = req_pool_indices_tensor - self.orig_seq_lens = orig_seq_lens_tensor - self.out_cache_loc = out_cache_loc -+ self.out_cache_loc_cpu = out_cache_loc.cpu() - self.input_embeds = ( - torch.tensor(input_embeds).to(self.device, non_blocking=True) - if input_embeds -@@ -1508,10 +1522,14 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - - input_ids = torch.cat([self.input_ids, running_batch.input_ids]) - out_cache_loc = torch.cat([self.out_cache_loc, running_batch.out_cache_loc]) -+ out_cache_loc_cpu = torch.cat( -+ [self.out_cache_loc_cpu, running_batch.out_cache_loc_cpu] -+ ) - - self.merge_batch(running_batch) - self.input_ids = input_ids - self.out_cache_loc = out_cache_loc -+ self.out_cache_loc_cpu = out_cache_loc_cpu - - # For overlap scheduler, the output_ids has one step delay - delta = 0 if self.enable_overlap else -1 -@@ -1677,6 +1695,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - self.seq_lens_cpu = torch.empty(0, dtype=torch.int64) - self.orig_seq_lens = torch.empty(0, dtype=torch.int32, device=self.device) - self.out_cache_loc = torch.empty(0, dtype=torch.int64, device=self.device) -+ self.out_cache_loc_cpu = torch.empty(0, dtype=torch.int64, device="cpu") - self.req_pool_indices = torch.empty(0, dtype=torch.int32, device=self.device) - self.seq_lens_sum = 0 - self.extend_num_tokens = 0 -@@ -1736,6 +1755,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - - # Allocate memory - self.out_cache_loc = alloc_for_decode(self, token_per_req=1) -+ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) - - # Update req-level memory management fields - for req in self.reqs: -@@ -1807,6 +1827,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - self.seq_lens_cpu = self.seq_lens_cpu[keep_indices] - self.orig_seq_lens = self.orig_seq_lens[keep_indices_device] - self.out_cache_loc = None -+ self.out_cache_loc_cpu = None - self.seq_lens_sum = self.seq_lens.sum().item() - self.output_ids = self.output_ids[keep_indices_device] - self.return_logprob = any(req.return_logprob for req in self.reqs) -@@ -1852,6 +1873,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - self.seq_lens_cpu = torch.cat([self.seq_lens_cpu, other.seq_lens_cpu]) - self.orig_seq_lens = torch.cat([self.orig_seq_lens, other.orig_seq_lens]) - self.out_cache_loc = None -+ self.out_cache_loc_cpu = None - self.seq_lens_sum += other.seq_lens_sum - if self.output_ids is not None: - self.output_ids = torch.cat([self.output_ids, other.output_ids]) -@@ -1903,6 +1925,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): - seq_lens=self.seq_lens, - orig_seq_lens=self.orig_seq_lens, - out_cache_loc=self.out_cache_loc, -+ out_cache_loc_cpu=self.out_cache_loc_cpu, - seq_lens_cpu=seq_lens_cpu, - seq_lens_sum=self.seq_lens_sum, - return_logprob=self.return_logprob, -@@ -1983,7 +2006,8 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): +@@ -2181,7 +2181,8 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): def __str__(self): return ( f"ScheduleBatch(forward_mode={self.forward_mode.name if self.forward_mode else 'None'}, " @@ -1222,97 +307,20 @@ index c4c5a9ebb..3650ba881 100644 ) -@@ -2038,6 +2062,9 @@ class ModelWorkerBatch: - # Sampling info - sampling_info: SamplingBatchInfo - -+ # cpu copy of out_cache_loc -+ out_cache_loc_cpu: Optional[torch.Tensor] = None -+ - # The original sequence lengths, Qwen-1M related - orig_seq_lens: Optional[torch.Tensor] = None - -diff --git a/python/sglang/srt/managers/scheduler.py b/python/sglang/srt/managers/scheduler.py -index b801fd8f8..9e27cc825 100644 ---- a/python/sglang/srt/managers/scheduler.py -+++ b/python/sglang/srt/managers/scheduler.py -@@ -1305,6 +1305,7 @@ class Scheduler( - input_embeds=recv_req.input_embeds, - custom_logit_processor=recv_req.custom_logit_processor, - return_hidden_states=recv_req.return_hidden_states, -+ return_routed_experts=recv_req.return_routed_experts, - eos_token_ids=self.model_config.hf_eos_token_id, - bootstrap_host=recv_req.bootstrap_host, - bootstrap_port=recv_req.bootstrap_port, diff --git a/python/sglang/srt/managers/scheduler_output_processor_mixin.py b/python/sglang/srt/managers/scheduler_output_processor_mixin.py -index c48f5f893..a9796c25f 100644 +index e40586c24..32d98aee4 100644 --- a/python/sglang/srt/managers/scheduler_output_processor_mixin.py +++ b/python/sglang/srt/managers/scheduler_output_processor_mixin.py -@@ -9,6 +9,7 @@ import torch - from sglang.srt.disaggregation.utils import DisaggregationMode +@@ -10,6 +10,7 @@ from sglang.srt.disaggregation.utils import DisaggregationMode from sglang.srt.environ import envs from sglang.srt.layers.logits_processor import LogitsProcessorOutput -+from sglang.srt.layers.moe.routed_experts_capturer import get_global_experts_capturer + from sglang.srt.layers.moe.routed_experts_capturer import get_global_experts_capturer ++ from sglang.srt.managers.io_struct import ( AbortReq, BatchEmbeddingOutput, -@@ -112,6 +113,14 @@ class SchedulerOutputProcessorMixin: - req.check_finished() - - if req.finished(): -+ req.routed_experts = ( -+ get_global_experts_capturer().get_routed_experts( -+ req_pool_idx=req.req_pool_idx, -+ seqlen=req.seqlen, -+ req_to_token_pool=self.req_to_token_pool, -+ ) -+ ) -+ - release_kv_cache(req, self.tree_cache) - req.time_stats.completion_time = time.perf_counter() - elif not batch.decoding_reqs or req not in batch.decoding_reqs: -@@ -362,6 +371,12 @@ class SchedulerOutputProcessorMixin: - req.check_finished(new_accepted_len) - - if req.finished(): -+ req.routed_experts = get_global_experts_capturer().get_routed_experts( -+ req_pool_idx=req.req_pool_idx, -+ seqlen=req.seqlen, -+ req_to_token_pool=self.req_to_token_pool, -+ ) -+ - if self.server_args.disaggregation_decode_enable_offload_kvcache: - # Asynchronously offload KV cache; release_kv_cache will be called after Device->Host transfer completes - if not self.decode_offload_manager.offload_kv_cache(req): -@@ -756,6 +771,7 @@ class SchedulerOutputProcessorMixin: - spec_accepted_tokens = [] - retraction_counts = [] - output_hidden_states = None -+ output_routed_experts = None - - queue_times = [] - forward_entry_times = [] -@@ -946,6 +962,10 @@ class SchedulerOutputProcessorMixin: - if output_hidden_states is None: - output_hidden_states = [] - output_hidden_states.append(req.hidden_states) -+ if req.return_routed_experts: -+ if output_routed_experts is None: -+ output_routed_experts = [] -+ output_routed_experts.append(req.routed_experts) - - if ( - req.finished() -@@ -994,6 +1014,7 @@ class SchedulerOutputProcessorMixin: - output_token_ids_logprobs_idx=output_token_ids_logprobs_idx, - output_token_entropy_val=None, - output_hidden_states=output_hidden_states, -+ output_routed_experts=output_routed_experts, - placeholder_tokens_idx=None, - placeholder_tokens_val=None, - retraction_counts=retraction_counts, diff --git a/python/sglang/srt/managers/scheduler_update_weights_mixin.py b/python/sglang/srt/managers/scheduler_update_weights_mixin.py -index f8ebfc1f4..48b9a1a3b 100644 +index 293a84350..0947f77e0 100644 --- a/python/sglang/srt/managers/scheduler_update_weights_mixin.py +++ b/python/sglang/srt/managers/scheduler_update_weights_mixin.py @@ -1,6 +1,7 @@ @@ -1333,7 +341,7 @@ index f8ebfc1f4..48b9a1a3b 100644 from sglang.srt.managers.io_struct import ( CheckWeightsReqInput, CheckWeightsReqOutput, -@@ -127,6 +131,13 @@ class SchedulerUpdateWeightsMixin: +@@ -137,6 +141,13 @@ class SchedulerUpdateWeightsMixin: self.memory_saver_adapter.pause(GPU_MEMORY_TYPE_KV_CACHE) self.flush_cache() @@ -1347,7 +355,7 @@ index f8ebfc1f4..48b9a1a3b 100644 if GPU_MEMORY_TYPE_WEIGHTS in tags: self.stashed_model_static_state = _export_static_state( self.tp_worker.model_runner.model -@@ -167,6 +178,13 @@ class SchedulerUpdateWeightsMixin: +@@ -177,6 +188,13 @@ class SchedulerUpdateWeightsMixin: if GPU_MEMORY_TYPE_KV_CACHE in tags: self.memory_saver_adapter.resume(GPU_MEMORY_TYPE_KV_CACHE) @@ -1361,70 +369,11 @@ index f8ebfc1f4..48b9a1a3b 100644 return ResumeMemoryOccupationReqOutput() def check_weights(self: Scheduler, recv_req: CheckWeightsReqInput): -diff --git a/python/sglang/srt/managers/tokenizer_communicator_mixin.py b/python/sglang/srt/managers/tokenizer_communicator_mixin.py -index edbc52526..2cdc42755 100644 ---- a/python/sglang/srt/managers/tokenizer_communicator_mixin.py -+++ b/python/sglang/srt/managers/tokenizer_communicator_mixin.py -@@ -421,6 +421,11 @@ class TokenizerCommunicatorMixin: - result = (await self.update_weights_from_distributed_communicator(obj))[ - 0 - ] -+ if result.success and obj.weight_version is not None: -+ self._update_weight_version_if_provided(obj.weight_version) -+ result.message += ( -+ f" Weight version updated to {obj.weight_version}." -+ ) - return result.success, result.message - - # This means that weight sync -@@ -480,6 +485,11 @@ class TokenizerCommunicatorMixin: - async with self.is_pause_cond: - if self.is_pause: - result = (await self.update_weights_from_tensor_communicator(obj))[0] -+ if result.success and obj.weight_version is not None: -+ self._update_weight_version_if_provided(obj.weight_version) -+ result.message += ( -+ f" Weight version updated to {obj.weight_version}." -+ ) - return result.success, result.message - - # This means that weight sync diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py -index b90cf0616..8a5cbdbed 100644 +index f4fc29e29..5ef12cca6 100644 --- a/python/sglang/srt/managers/tokenizer_manager.py +++ b/python/sglang/srt/managers/tokenizer_manager.py -@@ -20,6 +20,7 @@ import logging - import math - import os - import pickle -+import pybase64 - import signal - import sys - import threading -@@ -888,6 +889,7 @@ class TokenizerManager(TokenizerCommunicatorMixin): - session_params=session_params, - custom_logit_processor=obj.custom_logit_processor, - return_hidden_states=obj.return_hidden_states, -+ return_routed_experts=obj.return_routed_experts, - data_parallel_rank=obj.data_parallel_rank, - priority=obj.priority, - extra_key=obj.extra_key, -@@ -1621,6 +1623,14 @@ class TokenizerManager(TokenizerCommunicatorMixin): - if getattr(recv_obj, "output_hidden_states", None): - meta_info["hidden_states"] = recv_obj.output_hidden_states[i] - -+ if getattr(recv_obj, "output_routed_experts", None): -+ if recv_obj.output_routed_experts[i] is not None: -+ meta_info["routed_experts"] = pybase64.b64encode( -+ recv_obj.output_routed_experts[i].contiguous().numpy().tobytes(order="C") -+ ).decode("ascii") -+ else: -+ meta_info["routed_experts"] = None -+ - if isinstance(recv_obj, BatchStrOutput): - state.text += recv_obj.output_strs[i] - if self.server_args.stream_output and state.obj.stream: -@@ -1747,12 +1757,13 @@ class TokenizerManager(TokenizerCommunicatorMixin): +@@ -1652,12 +1652,13 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi return if len(recv_obj.input_token_logprobs_val) > 0: @@ -1444,190 +393,43 @@ index b90cf0616..8a5cbdbed 100644 state.output_token_logprobs_val.extend( recv_obj.output_token_logprobs_val[recv_obj_index] ) -diff --git a/python/sglang/srt/model_executor/forward_batch_info.py b/python/sglang/srt/model_executor/forward_batch_info.py -index 3a85e6a7e..5d74adca6 100644 ---- a/python/sglang/srt/model_executor/forward_batch_info.py -+++ b/python/sglang/srt/model_executor/forward_batch_info.py -@@ -51,6 +51,7 @@ from sglang.srt.layers.dp_attention import ( - set_dp_buffer_len, - set_is_extend_in_batch, - ) -+from sglang.srt.server_args import get_global_server_args - from sglang.srt.utils import get_compiler_backend, is_npu, support_triton - from sglang.srt.utils.common import ceil_align - -@@ -214,6 +215,9 @@ class ForwardBatch: - # The sum of all sequence lengths - seq_lens_sum: int - -+ # cpu copy of out_cache_loc -+ out_cache_loc_cpu: Optional[torch.Tensor] = None -+ - # The original sequence length without being chunked. Qwen-1M related. - orig_seq_lens: Optional[torch.Tensor] = None - -@@ -368,6 +372,7 @@ class ForwardBatch: - req_pool_indices=batch.req_pool_indices, - seq_lens=batch.seq_lens, - out_cache_loc=batch.out_cache_loc, -+ out_cache_loc_cpu=batch.out_cache_loc_cpu, - mm_inputs=batch.multimodal_inputs, - encoder_cached=batch.encoder_cached, - encoder_lens=batch.encoder_lens, -@@ -623,7 +628,10 @@ class ForwardBatch: - mm_input = batch.multimodal_inputs[batch_idx] - if self.forward_mode.is_decode(): - # 3 * N -- if mm_input is None: -+ if ( -+ mm_input is None -+ or get_global_server_args().rl_on_policy_target is not None -+ ): - mrope_positions_list[batch_idx] = torch.full( - (3, 1), - self.seq_lens[batch_idx] - 1, -@@ -640,7 +648,10 @@ class ForwardBatch: - batch.extend_seq_lens[batch_idx], - batch.extend_prefix_lens[batch_idx], - ) -- if mm_input is None: -+ if ( -+ mm_input is None -+ or get_global_server_args().rl_on_policy_target is not None -+ ): - # text only - mrope_positions = torch.tensor( - [ -@@ -823,6 +834,8 @@ class ForwardBatch: - ) - - self.out_cache_loc = self._pad_tensor_to_size(self.out_cache_loc, num_tokens) -+ if self.out_cache_loc_cpu is not None: -+ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) - if self.encoder_lens is not None: - self.encoder_lens = self._pad_tensor_to_size(self.encoder_lens, bs) - self.positions = self._pad_tensor_to_size(self.positions, num_tokens) -@@ -906,6 +919,7 @@ class ForwardBatch: - self.spec_info.hidden_states = self.hidden_states_backup - if hasattr(self, "output_cache_loc_backup"): - self.out_cache_loc = self.output_cache_loc_backup -+ self.out_cache_loc_cpu = self.out_cache_loc.to("cpu", non_blocking=True) - - elif self.forward_mode.is_decode() or self.forward_mode.is_idle(): - logits_output.next_token_logits = logits_output.next_token_logits[:bs] diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py -index 4d58278b7..81c6a5c7c 100644 +index 1d69c0582..9027374be 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py -@@ -94,6 +94,11 @@ from sglang.srt.layers.dp_attention import ( - set_is_extend_in_batch, - ) - from sglang.srt.layers.logits_processor import LogitsProcessorOutput -+from sglang.srt.layers.moe.routed_experts_capturer import ( -+ RoutedExpertsCapturer, -+ get_global_experts_capturer, -+ set_global_experts_capturer, -+) - from sglang.srt.layers.pooler import EmbeddingPoolerOutput - from sglang.srt.layers.sampler import Sampler - from sglang.srt.layers.torchao_utils import apply_torchao_config_to_model -@@ -502,6 +507,11 @@ class ModelRunner: - server_args.max_running_requests, - server_args.max_total_tokens, +@@ -558,7 +558,8 @@ class ModelRunner(ModelRunnerKVCacheMixin): ) -+ -+ # Init routed experts capturer + + # Init routed experts capturer +- self.init_routed_experts_capturer() + if not self.is_draft_worker: + self.init_routed_experts_capturer() -+ + if self.device == "cuda": self.init_cublas() - self.init_attention_backend() -@@ -545,6 +555,40 @@ class ModelRunner: - # Initialize piecewise CUDA graph - self.init_piecewise_cuda_graphs() - -+ def init_routed_experts_capturer(self): -+ # TODO: the redundant logic with TpModelWorker -+ max_running_requests = min( -+ ( -+ self.max_total_num_tokens // 2 -+ if self.server_args.max_running_requests is None -+ else self.server_args.max_running_requests -+ // ( -+ self.server_args.dp_size -+ if self.server_args.enable_dp_attention -+ else 1 -+ ) -+ ), -+ self.req_to_token_pool.size, -+ ) -+ -+ if not self.server_args.disable_shared_experts_fusion and hasattr( -+ self.model, "num_fused_shared_experts" -+ ): -+ num_fused_shared_experts = self.model.num_fused_shared_experts -+ else: -+ num_fused_shared_experts = 0 -+ -+ set_global_experts_capturer( -+ RoutedExpertsCapturer.create( -+ enable=get_global_server_args().enable_return_routed_experts, -+ model_config=self.model_config, -+ num_fused_shared_experts=num_fused_shared_experts, -+ num_tokens=self.max_total_num_tokens + self.page_size, -+ max_running_requests=max_running_requests, -+ device=self.device, +@@ -2224,11 +2225,12 @@ class ModelRunner(ModelRunnerKVCacheMixin): + output.expert_distribution_metrics = recorder_outputs.get("metrics") + + # Copy cached routing experts' buffers back to CPU cache +- get_global_experts_capturer().on_forward_end( +- forward_batch=forward_batch, +- can_run_graph=output.can_run_graph, +- cuda_graph_batch=getattr(self.graph_runner, "bs", None), +- ) ++ if not self.is_draft_worker: ++ get_global_experts_capturer().on_forward_end( ++ forward_batch=forward_batch, ++ can_run_graph=output.can_run_graph, ++ cuda_graph_batch=getattr(self.graph_runner, "bs", None), + ) -+ ) -+ - def model_specific_adjustment(self): - server_args = self.server_args - -@@ -2645,9 +2689,12 @@ class ModelRunner: - ) -> Tuple[Union[LogitsProcessorOutput, PPProxyTensors], bool]: - self.forward_pass_id += 1 - -- with get_global_expert_distribution_recorder().with_forward_pass( -- self.forward_pass_id, -- forward_batch, -+ with ( -+ get_global_expert_distribution_recorder().with_forward_pass( -+ self.forward_pass_id, -+ forward_batch, -+ ), -+ get_global_experts_capturer().with_forward(forward_batch), - ): - output = self._forward_raw( - forward_batch, -@@ -2656,6 +2703,14 @@ class ModelRunner: - reinit_attn_backend, - split_forward_count, - ) -+ # Copy cached routing experts' buffers back to CPU cache -+ if not self.is_draft_worker: -+ get_global_experts_capturer().sync_fwd_experts_buffer_DtoH( -+ device_loc=forward_batch.out_cache_loc, -+ cpu_loc=forward_batch.out_cache_loc_cpu, -+ can_run_graph=output[1], -+ cuda_graph_batch=getattr(self.graph_runner, "bs", None), -+ ) if self.eplb_manager is not None: self.eplb_manager.on_forward_pass_end() diff --git a/python/sglang/srt/models/deepseek_v2.py b/python/sglang/srt/models/deepseek_v2.py -index dc30b4f0a..57625cdeb 100644 +index 2918461d3..2bcc67087 100644 --- a/python/sglang/srt/models/deepseek_v2.py +++ b/python/sglang/srt/models/deepseek_v2.py -@@ -667,6 +667,7 @@ class DeepseekV2MoE(nn.Module): - - self.topk = TopK( - top_k=config.num_experts_per_tok + self.num_fused_shared_experts, -+ layer_id=self.layer_id, - renormalize=config.norm_topk_prob, - use_grouped_topk=True, - num_expert_group=config.n_group, -@@ -2641,7 +2642,11 @@ class DeepseekV2AttentionMLA(nn.Module): +@@ -2704,7 +2704,11 @@ class DeepseekV2AttentionMLA(nn.Module): ): k = k_nope.new_empty(*k_shape) concat_mla_k(k=k, k_nope=k_nope, k_rope=k_pe) @@ -1640,78 +442,6 @@ index dc30b4f0a..57625cdeb 100644 # fa3 mha support fp8 inputs if ( self.current_attention_backend == "fa3" -diff --git a/python/sglang/srt/models/ernie4.py b/python/sglang/srt/models/ernie4.py -index ab1b6576b..dffd8f09a 100644 ---- a/python/sglang/srt/models/ernie4.py -+++ b/python/sglang/srt/models/ernie4.py -@@ -87,6 +87,7 @@ class Ernie4Moe(nn.Module): - - self.topk = TopK( - top_k=config.moe_k, -+ layer_id=layer_id, - renormalize=True, - use_grouped_topk=False, - correction_bias=self.gate.e_score_correction_bias, -diff --git a/python/sglang/srt/models/glm4_moe.py b/python/sglang/srt/models/glm4_moe.py -index a9689b8f2..0a6c467b1 100644 ---- a/python/sglang/srt/models/glm4_moe.py -+++ b/python/sglang/srt/models/glm4_moe.py -@@ -393,6 +393,7 @@ class Glm4MoeSparseMoeBlock(nn.Module): - - self.topk = TopK( - top_k=self.top_k + self.num_fused_shared_experts, -+ layer_id=self.layer_id, - renormalize=config.norm_topk_prob, - use_grouped_topk=True, - num_expert_group=config.n_group, -diff --git a/python/sglang/srt/models/gpt_oss.py b/python/sglang/srt/models/gpt_oss.py -index 9474700c4..398d622ff 100644 ---- a/python/sglang/srt/models/gpt_oss.py -+++ b/python/sglang/srt/models/gpt_oss.py -@@ -113,6 +113,7 @@ class GptOssSparseMoeBlock(nn.Module): - self.topk = TopK( - top_k=config.num_experts_per_tok, - renormalize=True, -+ layer_id=layer_id, - ) - - self.top_k = config.num_experts_per_tok -diff --git a/python/sglang/srt/models/grok.py b/python/sglang/srt/models/grok.py -index fd513060a..a089475b7 100644 ---- a/python/sglang/srt/models/grok.py -+++ b/python/sglang/srt/models/grok.py -@@ -142,6 +142,7 @@ class Grok1MoE(nn.Module): - self.topk = TopK( - top_k=top_k, - renormalize=False, -+ layer_id=layer_id, - custom_routing_function=custom_routing_function, - ) - -diff --git a/python/sglang/srt/models/hunyuan.py b/python/sglang/srt/models/hunyuan.py -index 7c6fd9e48..b20d28544 100644 ---- a/python/sglang/srt/models/hunyuan.py -+++ b/python/sglang/srt/models/hunyuan.py -@@ -150,6 +150,7 @@ class HunYuanSparseMoeBlock(nn.Module): - - self.topk = TopK( - top_k=top_k, -+ layer_id=layer_id, - renormalize=True if top_k > 1 else False, - ) - -diff --git a/python/sglang/srt/models/longcat_flash.py b/python/sglang/srt/models/longcat_flash.py -index 3530609ba..01c89e893 100644 ---- a/python/sglang/srt/models/longcat_flash.py -+++ b/python/sglang/srt/models/longcat_flash.py -@@ -245,6 +245,7 @@ class LongcatFlashMoE(nn.Module): - renormalize=False, - use_grouped_topk=False, - correction_bias=self.router.e_score_correction_bias.data, -+ layer_id=layer_id, - ) - self.topk.forward = self.topk.forward_native - diff --git a/python/sglang/srt/models/qwen2.py b/python/sglang/srt/models/qwen2.py index a7dbadec6..c83a41338 100644 --- a/python/sglang/srt/models/qwen2.py @@ -1751,18 +481,10 @@ index a7dbadec6..c83a41338 100644 if get_global_server_args().rl_on_policy_target is not None else {} diff --git a/python/sglang/srt/models/qwen2_moe.py b/python/sglang/srt/models/qwen2_moe.py -index ea33e81ef..561934dce 100644 +index 3ad9f6736..0b9c7f499 100644 --- a/python/sglang/srt/models/qwen2_moe.py +++ b/python/sglang/srt/models/qwen2_moe.py -@@ -161,6 +161,7 @@ class Qwen2MoeSparseMoeBlock(nn.Module): - self.topk = TopK( - top_k=config.num_experts_per_tok, - renormalize=config.norm_topk_prob, -+ layer_id=layer_id, - ) - - self.experts = get_moe_impl_class(quant_config)( -@@ -581,7 +582,17 @@ class Qwen2MoeModel(nn.Module): +@@ -586,7 +586,17 @@ class Qwen2MoeModel(nn.Module): prefix=add_prefix("layers", prefix), ) if self.pp_group.is_last_rank: @@ -1782,7 +504,7 @@ index ea33e81ef..561934dce 100644 self.norm = PPMissingLayer(return_tuple=True) diff --git a/python/sglang/srt/models/qwen3.py b/python/sglang/srt/models/qwen3.py -index 30b92acbd..0d28e0f2b 100644 +index 9220831f6..47a1a4e4c 100644 --- a/python/sglang/srt/models/qwen3.py +++ b/python/sglang/srt/models/qwen3.py @@ -90,8 +90,8 @@ class Qwen3Attention(nn.Module): @@ -1795,7 +517,7 @@ index 30b92acbd..0d28e0f2b 100644 ) if get_global_server_args().rl_on_policy_target is not None else {} -@@ -256,10 +256,8 @@ class Qwen3DecoderLayer(nn.Module): +@@ -242,10 +242,8 @@ class Qwen3DecoderLayer(nn.Module): norm_kwargs = ( dict( @@ -1807,24 +529,8 @@ index 30b92acbd..0d28e0f2b 100644 ) if get_global_server_args().rl_on_policy_target is not None else {} -@@ -289,10 +287,14 @@ class Qwen3DecoderLayer(nn.Module): - hidden_states: torch.Tensor, - forward_batch: ForwardBatch, - residual: Optional[torch.Tensor], -+ **kwargs, - ) -> Tuple[torch.Tensor, torch.Tensor]: - # Self Attention - hidden_states, residual = self.layer_communicator.prepare_attn( -- hidden_states, residual, forward_batch -+ hidden_states, -+ residual, -+ forward_batch, -+ **kwargs, - ) - if hidden_states.shape[0] != 0: - hidden_states = self.self_attn( diff --git a/python/sglang/srt/models/qwen3_moe.py b/python/sglang/srt/models/qwen3_moe.py -index 9737ac719..09c756918 100644 +index e11678a9e..e277d46f2 100644 --- a/python/sglang/srt/models/qwen3_moe.py +++ b/python/sglang/srt/models/qwen3_moe.py @@ -22,6 +22,7 @@ import math @@ -1844,17 +550,15 @@ index 9737ac719..09c756918 100644 from sglang.srt.layers.moe.utils import RoutingMethodType from sglang.srt.layers.quantization.base_config import QuantizationConfig from sglang.srt.layers.radix_attention import RadixAttention -@@ -227,7 +228,9 @@ class Qwen3MoeSparseMoeBlock(nn.Module): - top_k=config.num_experts_per_tok, - renormalize=config.norm_topk_prob, +@@ -229,6 +230,7 @@ class Qwen3MoeSparseMoeBlock(nn.Module): use_grouped_topk=False, -+ layer_id=layer_id, + layer_id=layer_id, ) + self.top_k = config.num_experts_per_tok self.experts = get_moe_impl_class(quant_config)( num_experts=config.num_experts -@@ -293,7 +296,22 @@ class Qwen3MoeSparseMoeBlock(nn.Module): +@@ -294,7 +296,22 @@ class Qwen3MoeSparseMoeBlock(nn.Module): # router_logits: (num_tokens, n_experts) router_logits, _ = self.gate(hidden_states) @@ -1878,7 +582,7 @@ index 9737ac719..09c756918 100644 final_hidden_states = self.experts(hidden_states, topk_output) if ( self.tp_size > 1 -@@ -474,13 +492,14 @@ class Qwen3MoeAttention(nn.Module): +@@ -475,13 +492,14 @@ class Qwen3MoeAttention(nn.Module): ) self.compatible_with_fused_kv_buffer = ( False if isinstance(self.rotary_emb, MRotaryEmbedding) else True @@ -1894,7 +598,7 @@ index 9737ac719..09c756918 100644 ) self._used_fused_qk_norm_rope_last_call = False -@@ -493,8 +512,16 @@ class Qwen3MoeAttention(nn.Module): +@@ -494,8 +512,16 @@ class Qwen3MoeAttention(nn.Module): prefix=add_prefix("attn", prefix), ) @@ -1912,8 +616,8 @@ index 9737ac719..09c756918 100644 + self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps, **norm_kwargs) self.alt_stream = alt_stream - def _apply_qk_norm( -@@ -751,9 +778,19 @@ class Qwen3MoeDecoderLayer(nn.Module): + def op_prepare(self, state): +@@ -736,9 +762,19 @@ class Qwen3MoeDecoderLayer(nn.Module): quant_config=quant_config, prefix=add_prefix("mlp", prefix), ) @@ -1936,90 +640,44 @@ index 9737ac719..09c756918 100644 self.layer_communicator = LayerCommunicator( diff --git a/python/sglang/srt/models/qwen3_vl.py b/python/sglang/srt/models/qwen3_vl.py -index ed52f7ff4..8ce9fab9d 100644 +index 891913078..c9dbecd23 100644 --- a/python/sglang/srt/models/qwen3_vl.py +++ b/python/sglang/srt/models/qwen3_vl.py -@@ -18,7 +18,6 @@ import re - from functools import lru_cache, partial - from typing import Callable, Iterable, List, Optional, Tuple, Union - --import numpy as np - import torch - import torch.nn as nn - from einops import rearrange -@@ -349,83 +348,65 @@ class Qwen3VLMoeVisionModel(nn.Module, RotaryPosMixin): - return rotary_pos_emb +@@ -397,28 +397,68 @@ class Qwen3VLMoeVisionModel(nn.Module, RotaryPosMixin): + return cos_combined, sin_combined def fast_pos_embed_interpolate(self, grid_thw): +- patch_pos_embeds_permute = [] +- m_size = self.spatial_merge_size + grid_ts, grid_hs, grid_ws = grid_thw[:, 0], grid_thw[:, 1], grid_thw[:, 2] - num_grid_per_side = int(self.num_position_embeddings**0.5) ++ num_grid_per_side = int(self.num_position_embeddings**0.5) + device = self.pos_embed.weight.device - - idx_list = [[] for _ in range(4)] - weight_list = [[] for _ in range(4)] - -- # TODO: use torch instand of np -- for t, h, w in grid_thw: -- h_idxs = np.linspace(0, num_grid_per_side - 1, h) -- w_idxs = np.linspace(0, num_grid_per_side - 1, w) ++ ++ idx_list = [[] for _ in range(4)] ++ weight_list = [[] for _ in range(4)] ++ + for t, h, w in zip(grid_ts, grid_hs, grid_ws): + h_idxs = torch.linspace(0, num_grid_per_side - 1, h) + w_idxs = torch.linspace(0, num_grid_per_side - 1, w) - -- h_idxs_floor = h_idxs.astype(int) -- w_idxs_floor = w_idxs.astype(int) -- h_idxs_ceil = (h_idxs.astype(int) + 1).clip(max=num_grid_per_side - 1) -- w_idxs_ceil = (w_idxs.astype(int) + 1).clip(max=num_grid_per_side - 1) ++ + h_idxs_floor = h_idxs.int() + w_idxs_floor = w_idxs.int() + h_idxs_ceil = (h_idxs.int() + 1).clip(max=num_grid_per_side - 1) + w_idxs_ceil = (w_idxs.int() + 1).clip(max=num_grid_per_side - 1) - - dh = h_idxs - h_idxs_floor - dw = w_idxs - w_idxs_floor - -- idx_list[0].extend( -- ((h_idxs_floor * num_grid_per_side)[None].T + w_idxs_floor[None]) -- .flatten() -- .tolist() -- * t -- ) -- idx_list[1].extend( -- ((h_idxs_floor * num_grid_per_side)[None].T + w_idxs_ceil[None]) -- .flatten() -- .tolist() -- * t -- ) -- idx_list[2].extend( -- ((h_idxs_ceil * num_grid_per_side)[None].T + w_idxs_floor[None]) -- .flatten() -- .tolist() -- * t -- ) -- idx_list[3].extend( -- ((h_idxs_ceil * num_grid_per_side)[None].T + w_idxs_ceil[None]) -- .flatten() -- .tolist() -- * t -- ) ++ ++ dh = h_idxs - h_idxs_floor ++ dw = w_idxs - w_idxs_floor ++ + base_h = h_idxs_floor * num_grid_per_side + base_h_ceil = h_idxs_ceil * num_grid_per_side - -- weight_list[0].extend( -- ((1 - dh)[None].T * (1 - dw)[None]).flatten().tolist() * t -- ) -- weight_list[1].extend(((1 - dh)[None].T * dw[None]).flatten().tolist() * t) -- weight_list[2].extend((dh[None].T * (1 - dw)[None]).flatten().tolist() * t) -- weight_list[3].extend((dh[None].T * dw[None]).flatten().tolist() * t) ++ + indices = [ + (base_h[None].T + w_idxs_floor[None]).flatten(), + (base_h[None].T + w_idxs_ceil[None]).flatten(), + (base_h_ceil[None].T + w_idxs_floor[None]).flatten(), + (base_h_ceil[None].T + w_idxs_ceil[None]).flatten(), + ] - -- device = self.pos_embed.weight.device -- dtype = self.pos_embed.weight.dtype ++ + weights = [ + ((1 - dh)[None].T * (1 - dw)[None]).flatten(), + ((1 - dh)[None].T * dw[None]).flatten(), @@ -2027,17 +685,11 @@ index ed52f7ff4..8ce9fab9d 100644 + (dh[None].T * dw[None]).flatten(), + ] -- p0 = ( -- self.pos_embed(torch.tensor(idx_list[0], dtype=torch.long, device=device)) -- * torch.tensor(weight_list[0], dtype=dtype, device=device)[:, None] -- ) -- p1 = ( -- self.pos_embed(torch.tensor(idx_list[1], dtype=torch.long, device=device)) -- * torch.tensor(weight_list[1], dtype=dtype, device=device)[:, None] -- ) -- p2 = ( -- self.pos_embed(torch.tensor(idx_list[2], dtype=torch.long, device=device)) -- * torch.tensor(weight_list[2], dtype=dtype, device=device)[:, None] +- embeds = torch.arange(self.num_grid, device=self.pos_embed.weight.device) +- embeds = ( +- self.pos_embed(embeds) +- .permute(1, 0) +- .reshape(1, -1, self.num_grid_per_side, self.num_grid_per_side) + for i in range(4): + idx_list[i].extend(indices[i].tolist()) + weight_list[i].extend(weights[i].tolist()) @@ -2046,33 +698,40 @@ index ed52f7ff4..8ce9fab9d 100644 + weight_tensor = torch.tensor( + weight_list, dtype=self.pos_embed.weight.dtype, device=device ) -- p3 = ( -- self.pos_embed(torch.tensor(idx_list[3], dtype=torch.long, device=device)) -- * torch.tensor(weight_list[3], dtype=dtype, device=device)[:, None] +- for t, h, w in grid_thw: +- pos_embed = torch.nn.functional.interpolate( +- embeds, size=(h, w), mode="bilinear", align_corners=self.align_corners +- ) +- pos_embed = pos_embed.reshape( +- -1, +- h // self.spatial_merge_size, +- self.spatial_merge_size, +- w // self.spatial_merge_size, +- self.spatial_merge_size, + pos_embeds = self.pos_embed(idx_tensor).to(device) * weight_tensor[:, :, None] + patch_pos_embeds = pos_embeds[0] + pos_embeds[1] + pos_embeds[2] + pos_embeds[3] + + patch_pos_embeds = patch_pos_embeds.split( + [h * w for h, w in zip(grid_hs, grid_ws)] - ) - -- patch_pos_embeds = p0 + p1 + p2 + p3 -- patch_pos_embeds = patch_pos_embeds.split([t * h * w for t, h, w in grid_thw]) - patch_pos_embeds_permute = [] -- m_size = self.spatial_merge_size -- for pos_embed, (t, h, w) in zip(patch_pos_embeds, grid_thw): ++ ) ++ ++ patch_pos_embeds_permute = [] + merge_size = self.spatial_merge_size + for pos_embed, t, h, w in zip(patch_pos_embeds, grid_ts, grid_hs, grid_ws): + pos_embed = pos_embed.repeat(t, 1) - pos_embed = ( -- pos_embed.view(t, h // m_size, m_size, w // m_size, m_size, -1) ++ pos_embed = ( + pos_embed.view( + t, h // merge_size, merge_size, w // merge_size, merge_size, -1 + ) - .permute(0, 1, 3, 2, 4, 5) - .flatten(0, 4) ++ .permute(0, 1, 3, 2, 4, 5) ++ .flatten(0, 4) ) -@@ -555,21 +536,27 @@ class Qwen3LLMModel(Qwen3Model): +- pos_embed = pos_embed.permute(1, 3, 2, 4, 0) +- pos_embed = pos_embed.flatten(0, 3).repeat(t, 1) + patch_pos_embeds_permute.append(pos_embed) + return torch.cat(patch_pos_embeds_permute) + +@@ -607,14 +647,19 @@ class Qwen3LLMModel(Qwen3Model): hidden_states + residual if residual is not None else hidden_states ) @@ -2085,69 +744,22 @@ index ed52f7ff4..8ce9fab9d 100644 + :, sep : sep + self.hidden_size + ] + -+ # SGLang applies residual at the START of the next layer, not at the END like HuggingFace. -+ # See: https://github.com/huggingface/transformers/blob/v5.0.0rc0/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L549 -+ # To match HF behavior, deepstack must be added AFTER residual: (hidden_states + residual) + deepstack -+ # The order matters because addition with different tensors is not associative in practice. + # SGLang applies residual at the START of the next layer, not at the END like HuggingFace. + # See: https://github.com/huggingface/transformers/blob/v5.0.0rc0/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L549 + # To match HF behavior, deepstack must be added AFTER residual: (hidden_states + residual) + deepstack + # The order matters because addition with different tensors is not associative in practice. +- # Deepstack for prev_layer is applied at the start of current layer via post_residual_addition. +- deepstack_embeds = self.get_deepstack_embeds( +- layer_idx - 1, input_deepstack_embeds +- ) hidden_states, residual = layer( positions, hidden_states, - forward_batch, - residual, -+ post_residual_addition=deepstack_embeds, - ) - -- # process deepstack -- if ( -- input_deepstack_embeds is not None -- and layer_idx in self.deepstack_embed_to_decoder_layer -- ): -- sep = self.hidden_size * layer_idx -- hidden_states += input_deepstack_embeds[:, sep : sep + self.hidden_size] -- - if not self.pp_group.is_last_rank: - return PPProxyTensors( - { -diff --git a/python/sglang/srt/models/step3_vl.py b/python/sglang/srt/models/step3_vl.py -index 4474f62d5..0e537c398 100644 ---- a/python/sglang/srt/models/step3_vl.py -+++ b/python/sglang/srt/models/step3_vl.py -@@ -129,6 +129,7 @@ class Step3TextMoEMLP(nn.Module): - top_k=config.moe_top_k, - renormalize=config.norm_expert_weight, - use_grouped_topk=False, -+ layer_id=layer_id, - ) - - self.experts = get_moe_impl_class(quant_config)( -diff --git a/python/sglang/srt/multimodal/processors/base_processor.py b/python/sglang/srt/multimodal/processors/base_processor.py -index 370aec2b6..47666d8f3 100644 ---- a/python/sglang/srt/multimodal/processors/base_processor.py -+++ b/python/sglang/srt/multimodal/processors/base_processor.py -@@ -13,6 +13,7 @@ from PIL import Image - from transformers import BaseImageProcessorFast - - from sglang.srt.managers.schedule_batch import Modality, MultimodalDataItem -+from sglang.srt.server_args import get_global_server_args - from sglang.srt.utils import ( - get_bool_env_var, - is_npu, -@@ -260,7 +261,9 @@ class BaseMultimodalProcessor(ABC): - and isinstance(processor.image_processor, BaseImageProcessorFast) - and not self.server_args.disable_fast_image_processor - ): -- if not _is_npu: -+ if get_global_server_args().rl_on_policy_target is not None: -+ kwargs["device"] = "cpu" -+ elif not _is_npu: - kwargs["device"] = "cuda" - elif processor.__class__.__name__ not in { - "Qwen2_5_VLProcessor", diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py -index 8e7753dab..15a0823fd 100644 +index 54d4e415a..de7620c20 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py -@@ -489,6 +489,7 @@ class ServerArgs: +@@ -523,6 +523,7 @@ class ServerArgs: cuda_graph_max_bs: Optional[int] = None cuda_graph_bs: Optional[List[int]] = None disable_cuda_graph: bool = False @@ -2155,25 +767,7 @@ index 8e7753dab..15a0823fd 100644 disable_cuda_graph_padding: bool = False enable_profile_cuda_graph: bool = False enable_cudagraph_gc: bool = False -@@ -535,6 +536,7 @@ class ServerArgs: - disable_fast_image_processor: bool = False - keep_mm_feature_on_device: bool = False - enable_return_hidden_states: bool = False -+ enable_return_routed_experts: bool = False - scheduler_recv_interval: int = 1 - numa_node: Optional[List[int]] = None - enable_deterministic_inference: bool = False -@@ -1966,6 +1968,9 @@ class ServerArgs: - "Enable deterministic inference because of rl_on_policy_target." - ) - self.enable_deterministic_inference = True -+ -+ # For VLM -+ os.environ["SGLANG_VLM_CACHE_SIZE_MB"] = "0" - # TODO remove this environment variable as a whole - os.environ["SGLANG_ENABLE_DETERMINISTIC_INFERENCE"] = "1" - -@@ -3462,6 +3467,11 @@ class ServerArgs: +@@ -3951,6 +3952,11 @@ class ServerArgs: action="store_true", help="Disable cuda graph.", ) @@ -2185,34 +779,23 @@ index 8e7753dab..15a0823fd 100644 parser.add_argument( "--disable-cuda-graph-padding", action="store_true", -@@ -3705,6 +3715,11 @@ class ServerArgs: - action="store_true", - help="Enable returning hidden states with responses.", - ) -+ parser.add_argument( -+ "--enable-return-routed-experts", -+ action="store_true", -+ help="Enable returning routed experts of each layer with responses.", -+ ) - parser.add_argument( - "--scheduler-recv-interval", - type=int, diff --git a/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py b/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py -index cd9d171fe..71e2f7063 100644 +index 5fe45086c..c95fbd0f6 100644 --- a/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +++ b/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py -@@ -335,6 +335,10 @@ class EAGLEDraftCudaGraphRunner: +@@ -341,7 +341,10 @@ class EAGLEDraftCudaGraphRunner: self.seq_lens.fill_(self.seq_len_fill_value) self.out_cache_loc.zero_() self.positions.zero_() +- + self.topk_p.zero_() + self.topk_index.zero_() + self.hidden_states.zero_() + self.req_pool_indices.zero_() - num_tokens = bs * self.num_tokens_per_bs -@@ -344,8 +348,8 @@ class EAGLEDraftCudaGraphRunner: + # Common inputs +@@ -350,8 +353,8 @@ class EAGLEDraftCudaGraphRunner: forward_batch.out_cache_loc ) self.positions[:raw_num_token].copy_(forward_batch.positions) @@ -2224,34 +807,10 @@ index cd9d171fe..71e2f7063 100644 self.req_pool_indices[:raw_bs].copy_(forward_batch.req_pool_indices) diff --git a/python/sglang/srt/speculative/eagle_info.py b/python/sglang/srt/speculative/eagle_info.py -index b3d72df05..09a1634e0 100644 +index 1bf3816e9..b5b41dba4 100644 --- a/python/sglang/srt/speculative/eagle_info.py +++ b/python/sglang/srt/speculative/eagle_info.py -@@ -135,6 +135,7 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin): - len(batch.input_ids), - ) - self.last_loc = last_loc -+ batch.out_cache_loc_cpu = batch.out_cache_loc.to("cpu", non_blocking=True) - - bs = batch.batch_size() - assign_req_to_token_pool_func( -@@ -492,6 +493,7 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin): - batch.out_cache_loc = tgt_cache_loc - batch.seq_lens.add_(accept_length + 1) - batch.seq_lens_cpu.add_(accept_length_cpu + 1) -+ batch.out_cache_loc_cpu = batch.out_cache_loc.to("cpu", non_blocking=True) - - draft_input = EagleDraftInput( - hidden_states=batch.spec_info.hidden_states[accept_index], -@@ -575,6 +577,7 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin): - topk=self.topk, - capture_hidden_mode=CaptureHiddenMode.LAST, - ) -+ batch.out_cache_loc_cpu = batch.out_cache_loc.to("cpu", non_blocking=True) - - return EagleVerifyOutput( - draft_input=draft_input, -@@ -746,6 +749,10 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): +@@ -778,6 +778,10 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): self.topk_index = self.topk_index[: len(new_indices)] self.hidden_states = self.hidden_states[: len(new_indices)] self.verified_id = self.verified_id[: len(new_indices)] @@ -2262,7 +821,7 @@ index b3d72df05..09a1634e0 100644 else: # in some cases(e.g draft_extend), we have not filtered the batch by `unfinished_index` self.topk_p = self.topk_p[new_indices] -@@ -777,6 +784,27 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): +@@ -809,6 +813,27 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): self.verified_id = torch.cat([self.verified_id, spec_info.verified_id], axis=0) self.topk_p = torch.cat([self.topk_p, spec_info.topk_p]) self.topk_index = torch.cat([self.topk_index, spec_info.topk_index]) @@ -2291,10 +850,10 @@ index b3d72df05..09a1634e0 100644 @dataclass diff --git a/python/sglang/srt/speculative/eagle_worker.py b/python/sglang/srt/speculative/eagle_worker.py -index 07e3798f1..dede648ee 100644 +index a702df4f8..61d9ae366 100644 --- a/python/sglang/srt/speculative/eagle_worker.py +++ b/python/sglang/srt/speculative/eagle_worker.py -@@ -222,7 +222,7 @@ class EAGLEWorker(TpModelWorker): +@@ -231,7 +231,7 @@ class EAGLEWorker(TpModelWorker): self.cuda_graph_runner = None self.cuda_graph_runner_for_draft_extend = None diff --git a/docker/version.txt b/docker/version.txt index 874b9fb07..4cc406789 100644 --- a/docker/version.txt +++ b/docker/version.txt @@ -1 +1 @@ -nightly-dev-20251229a \ No newline at end of file +nightly-dev-20260105a \ No newline at end of file diff --git a/examples/swe-agent/README.md b/examples/swe-agent/README.md index 8648c23ee..a7c3ee74a 100644 --- a/examples/swe-agent/README.md +++ b/examples/swe-agent/README.md @@ -46,7 +46,7 @@ docker run -itd \ --privileged \ --network swe-net \ --name miles_ \ - slimerl/slime:latest \ + radixark/miles:latest \ /bin/zsh # 4. install utils in environment docker diff --git a/miles/backends/fsdp_utils/arguments.py b/miles/backends/fsdp_utils/arguments.py index c2015a1f7..a319fe6e5 100644 --- a/miles/backends/fsdp_utils/arguments.py +++ b/miles/backends/fsdp_utils/arguments.py @@ -35,7 +35,6 @@ class FSDPArgs: # Precision gradient_checkpointing: bool = False fp16: bool = False - bf16: bool = False # FSDP configuration fsdp_state_dict_cpu_offload: bool = True # If True, offload full state dict to CPU during collection. diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index 9f869415c..7cc7f2619 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -496,6 +496,11 @@ def save_model(self, rollout_id: int, force_sync: bool = False) -> None: if force_sync and self.args.async_save: maybe_finalize_async_save(blocking=True) + if self.args.save_hf is not None and self.role == "actor": + from miles.backends.megatron_utils.model import save_hf_model + + save_hf_model(self.args, rollout_id, self.model) + if self.args.offload_train: destroy_process_groups() diff --git a/miles/backends/megatron_utils/data.py b/miles/backends/megatron_utils/data.py index 8aa01e126..e709bf950 100644 --- a/miles/backends/megatron_utils/data.py +++ b/miles/backends/megatron_utils/data.py @@ -49,8 +49,9 @@ def get_batch( assert "tokens" in keys batch = data_iterator.get_next(keys) - packed_seq_params = None - max_seqlen = None + if "dynamic_global_batch_size" in data_iterator.rollout_data: + batch["dynamic_global_batch_size"] = data_iterator.rollout_data["dynamic_global_batch_size"] + tokens = batch["tokens"] # use 0 as the pad token id should be fine? pad_token_id = 0 @@ -286,9 +287,16 @@ def get_data_iterator( cp_size = mpu.get_context_parallel_world_size() num_local_samples = len(rollout_data["total_lengths"]) - num_local_gbs = args.global_batch_size // dp_size + global_batch_size = rollout_data.get("dynamic_global_batch_size", args.global_batch_size) + num_local_gbs = global_batch_size // dp_size num_steps_per_rollout = num_local_samples // num_local_gbs + if global_batch_size != args.global_batch_size: + logger.info( + f"Using dynamic global_batch_size={global_batch_size} (original={args.global_batch_size}), " + f"num_local_samples={num_local_samples}, num_steps_per_rollout={num_steps_per_rollout}" + ) + def _generate_data_iterator(rollout_data, micro_batch_size, micro_batch_indices=None): data_iterator = [] for _ in range(vpp_size): @@ -371,6 +379,7 @@ def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatc "sample_indices", "rollout_routed_experts", "max_seq_lens", + "dynamic_global_batch_size", ]: continue # Upload per sample mean for each rollout value diff --git a/miles/backends/megatron_utils/loss.py b/miles/backends/megatron_utils/loss.py index 5f69ffa15..3afff1f49 100644 --- a/miles/backends/megatron_utils/loss.py +++ b/miles/backends/megatron_utils/loss.py @@ -822,12 +822,10 @@ def loss_function( loss, log = func(args, batch, logits, sum_of_sample_mean) # Here we need to divide by cp_size because to cancel the multiply in Megatron. + global_batch_size = batch.get("dynamic_global_batch_size", args.global_batch_size) if not args.calculate_per_token_loss: loss = ( - loss - * num_microbatches - / args.global_batch_size - * mpu.get_data_parallel_world_size(with_context_parallel=True) + loss * num_microbatches / global_batch_size * mpu.get_data_parallel_world_size(with_context_parallel=True) ) else: loss = loss * mpu.get_context_parallel_world_size() diff --git a/miles/backends/megatron_utils/model.py b/miles/backends/megatron_utils/model.py index 304bb9f3f..780370453 100644 --- a/miles/backends/megatron_utils/model.py +++ b/miles/backends/megatron_utils/model.py @@ -6,6 +6,7 @@ from argparse import Namespace from collections.abc import Callable, Sequence from functools import partial +from pathlib import Path import torch from megatron.core import mpu @@ -546,6 +547,23 @@ def train( pre_hook_enabled = False + if args.reset_optimizer_states: + if ( + mpu.get_data_parallel_rank(with_context_parallel=True) == 0 + and mpu.get_tensor_model_parallel_rank() == 0 + and mpu.get_pipeline_model_parallel_rank() == mpu.get_pipeline_model_parallel_world_size() - 1 + ): + print("Reset optimizer states") + for chained_optimizer in optimizer.chained_optimizers: + for group in chained_optimizer.optimizer.param_groups: + if "step" in group: + group["step"] = 0 + for state in chained_optimizer.optimizer.state.values(): + if "exp_avg" in state: + state["exp_avg"].zero_() + if "exp_avg_sq" in state: + state["exp_avg_sq"].zero_() + if args.manual_gc: # Disable the default garbage collector and perform the collection manually. # This is to align the timing of garbage collection across ranks. @@ -699,6 +717,43 @@ def save( enable_forward_pre_hook(model) +def save_hf_model(args, rollout_id: int, model: Sequence[DDP]) -> None: + """Save Megatron model in HuggingFace format. + + Args: + model (Sequence[DDP]): Sequence of DDP-wrapped model chunks. + rollout_id (int): Rollout ID for path formatting. + """ + should_log = ( + mpu.get_data_parallel_rank(with_context_parallel=True) == 0 and mpu.get_tensor_model_parallel_rank() == 0 + ) + + try: + from megatron.bridge import AutoBridge + from miles.utils.megatron_bridge_utils import patch_megatron_model + + path = Path(args.save_hf.format(rollout_id=rollout_id)) + + if should_log: + logger.info(f"Saving model in HuggingFace format to {path}") + + bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True) + + path.mkdir(parents=True, exist_ok=True) + + with patch_megatron_model(model): + bridge.save_hf_pretrained( + model, + path=path, + ) + + if should_log: + logger.info(f"Successfully saved HuggingFace model to {path}") + except Exception as e: + if should_log: + logger.error(f"Failed to save HuggingFace format: {e}") + + def initialize_model_and_optimizer( args: Namespace, role: str = "actor" ) -> tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler, int]: diff --git a/miles/backends/sglang_utils/arguments.py b/miles/backends/sglang_utils/arguments.py index 49ca74a36..3b8697545 100644 --- a/miles/backends/sglang_utils/arguments.py +++ b/miles/backends/sglang_utils/arguments.py @@ -1,5 +1,3 @@ -import sglang -from packaging.version import parse from sglang.srt.server_args import ServerArgs from miles.utils.http_utils import _wrap_ipv6 @@ -114,9 +112,6 @@ def new_add_argument_wrapper(*name_or_flags, **kwargs): def validate_args(args): - if parse(sglang.__version__) == parse("0.4.10") and getattr(args, "sglang_enable_ep_moe", False): - args.sglang_expert_parallel_size = args.rollout_num_gpus_per_engine - args.sglang_tp_size = args.rollout_num_gpus_per_engine args.sglang_dp_size = args.sglang_data_parallel_size args.sglang_pp_size = args.sglang_pipeline_parallel_size diff --git a/miles/backends/sglang_utils/sglang_engine.py b/miles/backends/sglang_utils/sglang_engine.py index 510899093..9bb9b1287 100644 --- a/miles/backends/sglang_utils/sglang_engine.py +++ b/miles/backends/sglang_utils/sglang_engine.py @@ -2,6 +2,7 @@ import ipaddress import logging import multiprocessing +import os import time from urllib.parse import quote @@ -31,6 +32,24 @@ def get_base_gpu_id(args, rank): return start_index +def _to_local_gpu_id(physical_gpu_id: int) -> int: + cvd = os.environ.get("CUDA_VISIBLE_DEVICES") + if not cvd: + return physical_gpu_id # no remapping + # CUDA_VISIBLE_DEVICES can be like "4,5,6,7" + visible = [int(x) for x in cvd.split(",") if x.strip() != ""] + # In a remapped process, valid torch device indices are 0..len(visible)-1 + if physical_gpu_id in visible: + return visible.index(physical_gpu_id) + # If we're already getting local IDs, allow them + if 0 <= physical_gpu_id < len(visible): + return physical_gpu_id + raise RuntimeError( + f"GPU id {physical_gpu_id} is not valid under CUDA_VISIBLE_DEVICES={cvd}. " + f"Expected one of {visible} (physical) or 0..{len(visible)-1} (local)." + ) + + def launch_server_process(server_args: ServerArgs) -> multiprocessing.Process: from sglang.srt.entrypoints.http_server import launch_server @@ -430,6 +449,8 @@ def _compute_server_args( ): nnodes = max(1, args.rollout_num_gpus_per_engine // args.num_gpus_per_node) node_rank = rank % nnodes + base = base_gpu_id if base_gpu_id is not None else get_base_gpu_id(args, rank) + base = _to_local_gpu_id(base) kwargs = { "model_path": args.hf_checkpoint, "trust_remote_code": True, @@ -444,7 +465,7 @@ def _compute_server_args( "node_rank": node_rank, "dist_init_addr": dist_init_addr, "gpu_id_step": 1, - "base_gpu_id": base_gpu_id if base_gpu_id is not None else get_base_gpu_id(args, rank), + "base_gpu_id": base, # parallel "tp_size": args.rollout_num_gpus_per_engine, "dp_size": args.sglang_dp_size, @@ -471,8 +492,6 @@ def _compute_server_args( kwargs["enable_return_routed_experts"] = True if args.fp16: kwargs["dtype"] = "float16" - elif args.bf16: - kwargs["dtype"] = "bfloat16" external_engine_need_check_fields = [k for k in kwargs.keys() if k not in _EXTERNAL_ENGINE_SKIP_CHECK_FIELDS] unused_keys = set(kwargs.keys()) diff --git a/miles/ray/placement_group.py b/miles/ray/placement_group.py index 78639b62c..eb232b161 100644 --- a/miles/ray/placement_group.py +++ b/miles/ray/placement_group.py @@ -1,5 +1,6 @@ import logging import socket + import ray from ray.util.placement_group import placement_group from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy diff --git a/miles/ray/rollout.py b/miles/ray/rollout.py index 1bff43124..4b22c5ddc 100644 --- a/miles/ray/rollout.py +++ b/miles/ray/rollout.py @@ -1,3 +1,4 @@ +import itertools import logging import multiprocessing import random @@ -116,7 +117,6 @@ def eval(self, rollout_id): # if debug train only, we don't generate evaluation data return - # TODO: add fault tolerance to eval result = call_rollout_fn(self.eval_generate_rollout, self.args, rollout_id, self.data_source, evaluation=True) data = result.data self._save_debug_rollout_data(data, rollout_id=rollout_id, evaluation=True) @@ -160,17 +160,56 @@ def _get_rollout_data(self, rollout_id): data = data.samples # flatten the data if it is a list of lists while isinstance(data[0], list): - data = sum(data, []) - - if self.args.disable_rollout_trim_samples: - logger.info(f"Collectd {len(data)} samples from rollout to train") - elif len(data) % self.args.global_batch_size != 0: - trim_len = (len(data) // self.args.global_batch_size) * self.args.global_batch_size - origin_data_length = len(data) - data = data[:trim_len] - logger.info(f"trim number of samples from {origin_data_length} to {trim_len}") + data = list(itertools.chain.from_iterable(data)) + + if not self.args.disable_rollout_trim_samples: + global_batch_size = self.args.global_batch_size + if self.args.use_dynamic_global_batch_size: + logger.info(f"Collected {len(data)} samples from rollout to train with dynamic global batch size") + # TODO: this is a temporary solution, we should directly save dynamic_global_batch_size to rollout data + self._dynamic_global_batch_size = self._compute_dynamic_global_batch_size(len(data)) + global_batch_size = self._dynamic_global_batch_size + + if len(data) % global_batch_size != 0: + trim_len = (len(data) // self.args.global_batch_size) * global_batch_size + if trim_len == 0: + raise ValueError(f"Not enough samples {len(data)} for global_batch_size {global_batch_size}") + origin_data_length = len(data) + data = data[:trim_len] + logger.info(f"trim number of samples from {origin_data_length} to {trim_len}") + logger.info(f"Final collected {len(data)} samples from rollout to train") + return data, metrics + def _compute_dynamic_global_batch_size(self, num_samples: int) -> int: + """Calculate dynamic global_batch_size to ensure only one training step. + + Strategy: global_batch_size = num_samples rounded down to a multiple of dp_size + This ensures num_steps_per_rollout = num_samples // global_batch_size = 1 + """ + dp_size = self.train_parallel_config["dp_size"] + original_gbs = self.args.global_batch_size + + # Round down to a multiple of dp_size to ensure only one training step + dynamic_gbs = (num_samples // dp_size) * dp_size + + if dynamic_gbs == 0: + # Too few samples, use at least dp_size + dynamic_gbs = dp_size + logger.warning(f"num_samples={num_samples} < dp_size={dp_size}, using dp_size as global_batch_size") + + # Calculate how many samples will be discarded + wasted = num_samples - dynamic_gbs + + if dynamic_gbs != original_gbs or wasted > 0: + logger.info( + f"Dynamic global_batch_size: {original_gbs} -> {dynamic_gbs} " + f"(num_samples={num_samples}, dp_size={dp_size}, " + f"num_steps=1, wasted={wasted})" + ) + + return dynamic_gbs + def _save_debug_rollout_data(self, data, rollout_id, evaluation: bool): # TODO to be refactored (originally Buffer._set_data) if (path_template := self.args.save_debug_rollout_data) is not None: @@ -332,6 +371,9 @@ def _split_train_data_by_dp(self, data, dp_size): if key not in data: continue rollout_data[key] = data[key] + # Pass dynamic global_batch_size to training side + if hasattr(self, "_dynamic_global_batch_size"): + rollout_data["dynamic_global_batch_size"] = self._dynamic_global_batch_size rollout_data_refs.append(Box(ray.put(rollout_data))) return rollout_data_refs @@ -540,13 +582,11 @@ def _start_router(args): router_args.port = args.sglang_router_port router_args.prometheus_port = find_available_port(random.randint(4000, 5000)) router_args.log_level = "warn" + router_args.request_timeout_secs = args.sglang_router_request_timeout_secs if args.prefill_num_servers is not None: router_args.pd_disaggregation = True - if hasattr(router_args, "request_timeout_secs"): - router_args.request_timeout_secs = args.sglang_router_request_timeout_secs - logger.info(f"Launch router with args: {router_args}") process = multiprocessing.Process( @@ -604,20 +644,8 @@ def _log_rollout_data(rollout_id, args, samples, rollout_extra_metrics, rollout_ return log_dict = {**(rollout_extra_metrics or {})} - response_lengths = [sample.response_length for sample in samples] - log_dict["perf/rollout_time"] = rollout_time - if args.rollout_num_gpus: - log_dict["perf/tokens_per_gpu_per_sec"] = sum(response_lengths) / rollout_time / args.rollout_num_gpus - log_dict["perf/longest_sample_tokens_per_sec"] = max(response_lengths) / rollout_time - - response_lengths = [sample.effective_response_length for sample in samples] - if args.rollout_num_gpus: - log_dict["perf/effective_tokens_per_gpu_per_sec"] = ( - sum(response_lengths) / rollout_time / args.rollout_num_gpus - ) - log_dict["perf/longest_effective_sample_tokens_per_sec"] = max(response_lengths) / rollout_time - log_dict |= dict_add_prefix(compute_metrics_from_samples(args, samples), "rollout/") + log_dict |= dict_add_prefix(compute_perf_metrics_from_samples(args, samples, rollout_time), "perf/") logger.info(f"perf {rollout_id}: {log_dict}") step = compute_rollout_step(args, rollout_id) log_dict["rollout/step"] = step @@ -630,13 +658,45 @@ def compute_metrics_from_samples(args, samples): log_dict = {} log_dict |= dict_add_prefix(compute_statistics(response_lengths), "response_len/") log_dict |= _compute_zero_std_metrics(args, samples) - log_dict |= _compute_spec_metrics(args, samples) log_dict |= _compute_reward_cat_metrics(args, samples) log_dict["repetition_frac"] = np.mean([int(has_repetition(s.response)) for s in samples]).item() log_dict["truncated_ratio"] = np.mean([int(s.status == Sample.Status.TRUNCATED) for s in samples]).item() return log_dict +def compute_perf_metrics_from_samples(args, samples, rollout_time): + non_generation_time = [sample.non_generation_time for sample in samples] + + log_dict = {} + log_dict["rollout_time"] = rollout_time + if max(non_generation_time) > 0: + log_dict |= dict_add_prefix(compute_statistics(non_generation_time), "non_generation_time/") + + def token_perf(response_lengths, non_generation_time, key=""): + max_response_length = max(response_lengths) + if args.rollout_num_gpus: + log_dict[f"{key}tokens_per_gpu_per_sec"] = sum(response_lengths) / rollout_time / args.rollout_num_gpus + log_dict[f"longest_{key}sample_tokens_per_sec"] = max_response_length / rollout_time + + if max(non_generation_time) == 0: + return + + non_generation_time = [ + t for t, length in zip(non_generation_time, response_lengths, strict=True) if length == max_response_length + ] + mean_non_generation_time = sum(non_generation_time) / len(non_generation_time) + + log_dict[f"longest_{key}sample_non_generation_time"] = mean_non_generation_time + log_dict[f"longest_{key}sample_tokens_per_sec_without_non_generation"] = max_response_length / ( + rollout_time - mean_non_generation_time + ) + + token_perf([sample.response_length for sample in samples], non_generation_time, key="") + token_perf([sample.effective_response_length for sample in samples], non_generation_time, key="effective_") + + return log_dict + + def _compute_zero_std_metrics(args, all_samples: list[Sample]): # only compute in GRPO-like algorithms where one prompt has multiple responses if args.advantage_estimator == "ppo": @@ -659,12 +719,19 @@ def _compute_spec_metrics(args, all_samples: list[Sample]): return {} num_samples = len(all_samples) metrics = {} - metrics["rollout/spec_accept_rate"] = ( - sum(sample.spec_info.spec_accept_rate for sample in all_samples) / num_samples - ) - metrics["rollout/spec_accept_length"] = ( - sum(sample.spec_info.spec_accept_length for sample in all_samples) / num_samples - ) + metrics["spec_accept_rate"] = sum(sample.spec_info.spec_accept_rate for sample in all_samples) / num_samples + metrics["spec_accept_length"] = sum(sample.spec_info.spec_accept_length for sample in all_samples) / num_samples + return metrics + + +def _compute_prefix_cache_metrics(args, all_samples: list[Sample]): + num_samples = len(all_samples) + metrics = {} + total_cached_tokens = sum(sample.prefix_cache_info.cached_tokens for sample in all_samples) + total_prompt_tokens = sum(sample.prefix_cache_info.total_prompt_tokens for sample in all_samples) + + metrics["prefix_cache_hit_rate"] = total_cached_tokens / total_prompt_tokens if total_prompt_tokens > 0 else 0.0 + metrics["avg_cached_tokens_per_sample"] = total_cached_tokens / num_samples return metrics diff --git a/miles/rollout/filter_hub/base_types.py b/miles/rollout/filter_hub/base_types.py index ba1a4441c..2937273bd 100644 --- a/miles/rollout/filter_hub/base_types.py +++ b/miles/rollout/filter_hub/base_types.py @@ -1,3 +1,4 @@ +from collections import defaultdict from dataclasses import dataclass @@ -5,3 +6,32 @@ class DynamicFilterOutput: keep: bool reason: str | None = None + + +def call_dynamic_filter(fn, *args, **kwargs): + if fn is None: + return DynamicFilterOutput(keep=True) + + output = fn(*args, **kwargs) + + # compatibility for legacy version + if not isinstance(output, DynamicFilterOutput): + output = DynamicFilterOutput(keep=output) + + return output + + +class MetricGatherer: + def __init__(self): + self._dynamic_filter_drop_reason_count = defaultdict(lambda: 0) + + def on_dynamic_filter_drop(self, reason: str | None): + if not reason: + return + self._dynamic_filter_drop_reason_count[reason] += 1 + + def collect(self): + return { + f"rollout/dynamic_filter/drop_{reason}": count + for reason, count in self._dynamic_filter_drop_reason_count.items() + } diff --git a/miles/rollout/rm_hub/math_utils.py b/miles/rollout/rm_hub/math_utils.py index e67253891..94ec98b92 100644 --- a/miles/rollout/rm_hub/math_utils.py +++ b/miles/rollout/rm_hub/math_utils.py @@ -18,7 +18,7 @@ def mathd_normalize_answer(answer: str | None) -> str | None: answer = answer.strip() try: # Remove enclosing `\text{}`. - m = re.search("^\\\\text\{(?P.+?)\}$", answer) + m = re.search(r"^\\text\{(?P.+?)\}$", answer) if m is not None: answer = m.group("text").strip() return _strip_string(answer) @@ -124,7 +124,7 @@ def _fix_sqrt(string): # remove percentage string = string.replace("\\%", "") - string = string.replace("\%", "") + string = string.replace(r"\%", "") # " 0." equivalent to " ." and "{0." equivalent to "{." Alternatively, add "0" if "." is the start of the string string = string.replace(" .", " 0.") @@ -161,7 +161,7 @@ def _fix_sqrt(string): # sympy might hang -- we don't care about trying to be lenient in these cases BAD_SUBSTRINGS = ["^{", "^("] -BAD_REGEXES = ["\^[0-9]+\^", "\^[0-9][0-9]+"] +BAD_REGEXES = [r"\^[0-9]+\^", r"\^[0-9][0-9]+"] TUPLE_CHARS = "()[]" @@ -238,7 +238,7 @@ def _inject_implicit_mixed_number(step: str): def _strip_properly_formatted_commas(expr: str): # We want to be careful because we don't want to strip tuple commas - p1 = re.compile("(\d)(,)(\d\d\d)($|\D)") + p1 = re.compile(r"(\d)(,)(\d\d\d)($|\D)") while True: next_expr = p1.sub("\\1\\3\\4", expr) if next_expr == expr: @@ -253,7 +253,7 @@ def _normalize(expr: str) -> str: return None # Remove enclosing `\text{}`. - m = re.search("^\\\\text\{(?P.+?)\}$", expr) + m = re.search(r"^\\text\{(?P.+?)\}$", expr) if m is not None: expr = m.group("text") @@ -286,8 +286,8 @@ def _normalize(expr: str) -> str: "inch", "yard", ]: - expr = re.sub(f"{unit}(es)?(s)? *(\^[0-9]+)?", "", expr) - expr = re.sub("\^ *\\\\circ", "", expr) + expr = re.sub(rf"{unit}(es)?(s)? *(\^[0-9]+)?", "", expr) + expr = re.sub(r"\^ *\\circ", "", expr) if len(expr) > 0 and expr[0] == "{" and expr[-1] == "}": expr = expr[1:-1] diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 2f3734657..77c540d60 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -1,8 +1,8 @@ import asyncio import copy +import inspect import logging from argparse import Namespace -from collections import defaultdict from collections.abc import Callable from contextlib import contextmanager from typing import Any @@ -14,12 +14,11 @@ from tqdm import tqdm from miles.rollout.base_types import RolloutFnEvalOutput, RolloutFnTrainOutput -from miles.rollout.filter_hub.base_types import DynamicFilterOutput +from miles.rollout.filter_hub.base_types import MetricGatherer, call_dynamic_filter from miles.utils.async_utils import run from miles.utils.data import Dataset from miles.utils.eval_config import EvalDatasetConfig from miles.utils.http_utils import get, post -from miles.utils.mask_utils import get_response_lengths from miles.utils.misc import SingletonMeta, load_function from miles.utils.processing_utils import encode_image_for_rollout_engine, load_processor, load_tokenizer from miles.utils.types import Sample @@ -154,20 +153,10 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A output = await post(url, payload) - # Extract new response tokens - if args.use_miles_router and "RadixTreeMiddleware" in args.miles_router_middleware_paths: - assert not args.partial_rollout, "Currently partial rollout is not supported when using miles router" - retrieve_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/retrieve_from_text" - retrieve_payload = {"text": sample.prompt + output["text"], "return_logp": True} - retrieve_output = await post(retrieve_url, retrieve_payload) - sample.tokens = retrieve_output["tokens"] - sample.response += output["text"] - sample.loss_mask = retrieve_output["loss_mask"] - sample.response_length = get_response_lengths([sample.loss_mask])[0] - sample.loss_mask = sample.loss_mask[-sample.response_length :] - sample.rollout_log_probs = retrieve_output["rollout_logp"][-sample.response_length :] - # Notice: currently cannot get the spec info from radix router output. + from miles.router.middleware_hub.radix_tree_middleware import postprocess_sample_with_radix_tree + + sample = await postprocess_sample_with_radix_tree(args, sample, output) else: if "output_token_logprobs" in output["meta_info"]: new_response_tokens = [item[1] for item in output["meta_info"]["output_token_logprobs"]] @@ -184,16 +173,6 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A sample.rollout_log_probs = [] sample.rollout_log_probs += new_response_log_probs - if args.sglang_speculative_algorithm: - # cannot directly use spec info from sglang because of partial rollout. - sample.spec_info.add( - meta_info=output["meta_info"], - response_length=sample.response_length, - ) - - if "weight_version" in output["meta_info"]: - sample.weight_versions.append(output["meta_info"]["weight_version"]) - if "routed_experts" in output["meta_info"]: sample.rollout_routed_experts = np.frombuffer( pybase64.b64decode(output["meta_info"]["routed_experts"].encode("ascii")), @@ -204,13 +183,7 @@ async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, A args.moe_router_topk, ) - match output["meta_info"]["finish_reason"]["type"]: - case "length": - sample.status = Sample.Status.TRUNCATED - case "abort": - sample.status = Sample.Status.ABORTED - case "stop": - sample.status = Sample.Status.COMPLETED + sample.update_from_meta_info(args, output["meta_info"]) return sample @@ -243,7 +216,11 @@ async def generate_and_rm( with state.dp_rank_context() as _: if args.custom_generate_function_path is not None: custom_generate_func = load_function(args.custom_generate_function_path) - sample = await custom_generate_func(args, sample, sampling_params) + # if signature has evaluation, pass evaluation + if "evaluation" in inspect.signature(custom_generate_func).parameters: + sample = await custom_generate_func(args, sample, sampling_params, evaluation=evaluation) + else: + sample = await custom_generate_func(args, sample, sampling_params) else: sample = await generate(args, sample, sampling_params) @@ -366,7 +343,7 @@ async def generate_rollout_async( load_function(args.dynamic_sampling_filter_path) if args.dynamic_sampling_filter_path is not None else None ) - metric_gatherer = _MetricGatherer() + metric_gatherer = MetricGatherer() # target_data_size is the total number of valid samples to get target_data_size = args.rollout_batch_size @@ -395,7 +372,7 @@ async def generate_rollout_async( assert len(group) == args.n_samples_per_prompt all_data.append(group) - dynamic_filter_output = _call_dynamic_filter(dynamic_filter, args, group) + dynamic_filter_output = call_dynamic_filter(dynamic_filter, args, group) if not dynamic_filter_output.keep: metric_gatherer.on_dynamic_filter_drop(reason=dynamic_filter_output.reason) state.remaining_batch_size -= 1 @@ -434,35 +411,6 @@ async def generate_rollout_async( return RolloutFnTrainOutput(samples=data, metrics=metric_gatherer.collect()), aborted_samples -def _call_dynamic_filter(fn, *args, **kwargs): - if fn is None: - return DynamicFilterOutput(keep=True) - - output = fn(*args, **kwargs) - - # compatibility for legacy version - if not isinstance(output, DynamicFilterOutput): - output = DynamicFilterOutput(keep=output) - - return output - - -class _MetricGatherer: - def __init__(self): - self._dynamic_filter_drop_reason_count = defaultdict(lambda: 0) - - def on_dynamic_filter_drop(self, reason: str | None): - if not reason: - return - self._dynamic_filter_drop_reason_count[reason] += 1 - - def collect(self): - return { - f"rollout/dynamic_filter/drop_{reason}": count - for reason, count in self._dynamic_filter_drop_reason_count.items() - } - - EVAL_PROMPT_DATASET = {} @@ -580,9 +528,8 @@ async def eval_rollout_single_dataset( } -# TODO remove this temp function def generate_rollout( - args: Namespace, rollout_id: int, data_buffer: Any, evaluation: bool = False + args: Namespace, rollout_id: int, data_source: Any, evaluation: bool = False ) -> RolloutFnTrainOutput | RolloutFnEvalOutput: """An example to implement the generate_rollout function for an rule based rm rollout generation. @@ -595,20 +542,11 @@ def generate_rollout( Returns: list[list[Sample]]: a list of list of samples generated by the rollout """ - output, aborted_samples = generate_abortable_samples( - args, rollout_id, data_buffer.get_samples, evaluation=evaluation - ) - data_buffer.add_samples(aborted_samples) - return output - - -def generate_abortable_samples( - args: Namespace, - rollout_id: int, - data_source: Callable[[int], list[list[Sample]]], - evaluation: bool = False, -) -> tuple[Any, list[list[Sample]]]: assert args.rollout_global_dataset if evaluation: - return run(eval_rollout(args, rollout_id)) - return run(generate_rollout_async(args, rollout_id, data_source)) + output, _ = run(eval_rollout(args, rollout_id)) + return output + + output, aborted_samples = run(generate_rollout_async(args, rollout_id, data_source.get_samples)) + data_source.add_samples(aborted_samples) + return output diff --git a/miles/router/middleware_hub/radix_tree_middleware.py b/miles/router/middleware_hub/radix_tree_middleware.py index 961e19d53..db57f6456 100644 --- a/miles/router/middleware_hub/radix_tree_middleware.py +++ b/miles/router/middleware_hub/radix_tree_middleware.py @@ -7,6 +7,10 @@ from starlette.responses import Response from transformers import AutoTokenizer +from miles.utils.http_utils import post +from miles.utils.mask_utils import get_response_lengths +from miles.utils.types import Sample + from .radix_tree import StringRadixTrie # Hop-by-hop headers that should not be forwarded @@ -149,3 +153,17 @@ async def dispatch(self, request: Request, call_next): if getattr(self.router, "verbose", False): print(f"[miles-router] Warning: Failed to cache trajectory: {e}") return response + + +async def postprocess_sample_with_radix_tree(args, sample: Sample, output: dict): + assert not args.partial_rollout, "Currently partial rollout is not supported when using miles router" + retrieve_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/retrieve_from_text" + retrieve_payload = {"text": sample.prompt + output["text"], "return_logp": True} + retrieve_output = await post(retrieve_url, retrieve_payload) + sample.tokens = retrieve_output["tokens"] + sample.response += output["text"] + sample.loss_mask = retrieve_output["loss_mask"] + sample.response_length = get_response_lengths([sample.loss_mask])[0] + sample.loss_mask = sample.loss_mask[-sample.response_length :] + sample.rollout_log_probs = retrieve_output["rollout_logp"][-sample.response_length :] + return sample diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index ea2ec0a33..595542c58 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -695,6 +695,15 @@ def add_algo_arguments(parser): reset_arg(parser, "--save", type=str, default=None) reset_arg(parser, "--save-interval", type=int, default=None) reset_arg(parser, "--async-save", action="store_true") + parser.add_argument( + "--save-hf", + type=str, + default=None, + help=( + "Path to save the model in HuggingFace format when using Megatron backend. " + "The model will be saved to `save_hf.format(rollout_id)`. " + ), + ) reset_arg(parser, "--seed", type=int, default=1234) reset_arg(parser, "--clip-grad", type=float, default=1.0) reset_arg(parser, "--calculate-per-token-loss", action="store_true") @@ -827,6 +836,15 @@ def add_algo_arguments(parser): default=False, help="Whether to calculate the mismatch metrics.", ) + parser.add_argument( + "--reset-optimizer-states", + action="store_true", + default=False, + help=( + "Whether to reset optimizer states after each rollout. " + "If enabled, the optimizer's history will be cleared at the end of each rollout, which can sometimes help with training stability or fulfill specific experiment requirements." + ), + ) parser.add_argument( "--use-rollout-logprobs", action="store_true", @@ -1232,6 +1250,12 @@ def add_rollout_buffer_arguments(parser): default=False, help="disable trim samples in rollout buffer when converting samples to train data", ) + parser.add_argument( + "--use-dynamic-global-batch-size", + action="store_true", + default=False, + help="enable dynamic global batch size, disable trim samples in rollout buffer when converting samples to train data", + ) return parser def add_custom_megatron_plugins_arguments(parser): @@ -1599,11 +1623,6 @@ def miles_validate_args(args): ) args.global_batch_size = global_batch_size - assert args.rollout_batch_size * args.n_samples_per_prompt % args.global_batch_size == 0, ( - f"rollout_batch_size {args.rollout_batch_size} * n_samples_per_prompt {args.n_samples_per_prompt} " - f"is not a multiple of global_batch_size {args.global_batch_size}" - ) - if args.n_samples_per_prompt == 1: args.grpo_std_normalization = False logger.info("n_samples_per_prompt is set to 1, grpo_std_normalization will be set to False.") diff --git a/miles/utils/data.py b/miles/utils/data.py index f1a8a9586..6e64ef678 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -78,23 +78,38 @@ def _parse_generalized_path(s: str): return s, None -def _should_skip_prompt(output_prompt: str | list, tokenizer, processor, max_length, multimodal_inputs=None): +def filter_long_prompt(origin_samples: list[Sample], tokenizer, processor, max_length: int | None) -> list[Sample]: if max_length is None: return False - if isinstance(output_prompt, list): + if not isinstance(origin_samples[0].prompt, str): logger.warning( "Skipping max_length check for list prompt. Set apply_chat_template=True to enable length filtering." ) return False if processor: - processor_output = processor(text=output_prompt, **multimodal_inputs) - input_ids = processor_output["input_ids"][0] + filtered_samples = [] + for sample in origin_samples: + from miles.utils.processing_utils import process_vision_info + + multimodal_inputs = process_vision_info(sample.prompt, processor) + processor_output = processor(text=sample.prompt, **multimodal_inputs) + input_ids = processor_output["input_ids"][0] + if len(input_ids) <= max_length: + filtered_samples.append(sample) else: - input_ids = tokenizer.encode(output_prompt, add_special_tokens=False) + prompts = [sample.prompt for sample in origin_samples] + input_ids_list = tokenizer(prompts, add_special_tokens=False)["input_ids"] + filtered_samples = [ + sample + for sample, input_ids in zip(origin_samples, input_ids_list, strict=True) + if len(input_ids) <= max_length + ] - return len(input_ids) > max_length + logger.info(f"Filtered {len(origin_samples) - len(filtered_samples)} samples longer than max_length={max_length}.") + + return filtered_samples def _build_messages(data: dict, prompt_key: str, as_conversation: bool, multimodal_keys: dict = None): @@ -167,7 +182,7 @@ def __init__( apply_chat_template=False, apply_chat_template_kwargs=None, ): - self.origin_samples = [] + origin_samples = [] for data in read_file(path): # Both chat templates and multimodal inputs require conversation format (list of message dicts) as_conversation = apply_chat_template or (multimodal_keys is not None) @@ -205,11 +220,7 @@ def __init__( else: multimodal_inputs = None - # TODO: this is slow. - if _should_skip_prompt(output_prompt, tokenizer, processor, max_length, multimodal_inputs): - continue - - self.origin_samples.append( + origin_samples.append( Sample( prompt=output_prompt, label=data[label_key] if label_key is not None else None, @@ -218,6 +229,11 @@ def __init__( ) ) + if max_length is not None: + self.origin_samples = filter_long_prompt(origin_samples, tokenizer, processor, max_length) + else: + self.origin_samples = origin_samples + self.epoch_id = -1 self.seed = seed self.samples = self.origin_samples diff --git a/miles/utils/debug_utils/display_debug_rollout_data.py b/miles/utils/debug_utils/display_debug_rollout_data.py index 3036e16ea..5775877b5 100644 --- a/miles/utils/debug_utils/display_debug_rollout_data.py +++ b/miles/utils/debug_utils/display_debug_rollout_data.py @@ -6,7 +6,7 @@ import torch import typer -from miles.ray.rollout import compute_metrics_from_samples +from miles.ray.rollout import compute_perf_metrics_from_samples from miles.utils.types import Sample _WHITELIST_KEYS = [ @@ -47,7 +47,7 @@ def main( log_reward_category=None, ) sample_objects = [Sample.from_dict(s) for s in sample_dicts] - metrics = compute_metrics_from_samples(args, sample_objects) + metrics = compute_perf_metrics_from_samples(args, sample_objects) print("metrics", metrics) if show_samples: diff --git a/miles/utils/metric_utils.py b/miles/utils/metric_utils.py index fce4107a0..66292c79e 100644 --- a/miles/utils/metric_utils.py +++ b/miles/utils/metric_utils.py @@ -58,6 +58,8 @@ def compute_statistics(values: list[float]) -> dict[str, float]: return { "mean": np.mean(values).item(), "median": np.median(values).item(), + "max": np.max(values).item(), + "min": np.min(values).item(), } diff --git a/miles/utils/misc.py b/miles/utils/misc.py index 23375a60b..c0a96d636 100644 --- a/miles/utils/misc.py +++ b/miles/utils/misc.py @@ -30,6 +30,9 @@ def __call__(cls, *args, **kwargs): cls._instances[cls] = instance return cls._instances[cls] + def clear_instances(cls): + cls._instances = {} + def exec_command(cmd: str, capture_output: bool = False) -> str | None: print(f"EXEC: {cmd}", flush=True) diff --git a/miles/utils/types.py b/miles/utils/types.py index 7821cf505..ccf569da2 100644 --- a/miles/utils/types.py +++ b/miles/utils/types.py @@ -43,31 +43,35 @@ class Status(Enum): # metadata used during training, e.g., what loss to use for this sample. train_metadata: dict | None = None + non_generation_time: float = 0.0 # time spent in non-generation steps + + @dataclass class SpecInfo: spec_accept_token_num: int = 0 spec_draft_token_num: int = 0 spec_verify_ct: int = 0 - spec_accept_rate: float = 0.0 - spec_accept_length: float = 0.0 - - def add(self, meta_info: dict, response_length: int): - self.spec_accept_token_num += meta_info["spec_accept_token_num"] - self.spec_draft_token_num += meta_info["spec_draft_token_num"] - self.spec_verify_ct += meta_info["spec_verify_ct"] - if self.spec_draft_token_num > 0: - # Notice: this does not iclude the bonus token generated by verify step. - self.spec_accept_rate = self.spec_accept_token_num / self.spec_draft_token_num - # self.spec_accept_rate = meta_info["spec_accept_rate"] # - if self.spec_verify_ct > 0: - self.spec_accept_length = response_length / self.spec_verify_ct + completion_token_num: int = 0 + + @property + def spec_accept_rate(self) -> float: + return self.spec_accept_token_num / self.spec_draft_token_num if self.spec_draft_token_num > 0 else 0.0 + + @property + def spec_accept_length(self) -> float: + return self.completion_token_num / self.spec_verify_ct if self.spec_verify_ct > 0 else 0.0 + + def add(self, meta_info: dict): + self.spec_accept_token_num += meta_info.get("spec_accept_token_num", 0) + self.spec_draft_token_num += meta_info.get("spec_draft_token_num", 0) + self.spec_verify_ct += meta_info.get("spec_verify_ct", 0) + self.completion_token_num += meta_info.get("completion_tokens", 0) def to_dict(self): return { "spec_accept_token_num": self.spec_accept_token_num, "spec_draft_token_num": self.spec_draft_token_num, "spec_verify_ct": self.spec_verify_ct, - "spec_accept_rate": self.spec_accept_rate, - "spec_accept_length": self.spec_accept_length, + "completion_token_num": self.completion_token_num, } @staticmethod @@ -76,22 +80,52 @@ def from_dict(data: dict): info.spec_accept_token_num = data.get("spec_accept_token_num", 0) info.spec_draft_token_num = data.get("spec_draft_token_num", 0) info.spec_verify_ct = data.get("spec_verify_ct", 0) - info.spec_accept_rate = data.get("spec_accept_rate", 0.0) - info.spec_accept_length = data.get("spec_accept_length", 0.0) + info.completion_token_num = data.get("completion_token_num", 0) return info spec_info: SpecInfo = field(default_factory=SpecInfo) + @dataclass + class PrefixCacheInfo: + cached_tokens: int = 0 + total_prompt_tokens: int = 0 + + @property + def prefix_cache_hit_rate(self) -> float: + return self.cached_tokens / self.total_prompt_tokens if self.total_prompt_tokens > 0 else 0.0 + + def add(self, meta_info: dict): + self.cached_tokens += meta_info.get("cached_tokens", 0) + # new_tokens = input_tokens - cached_tokens + self.total_prompt_tokens += meta_info.get("prompt_tokens", 0) + + def to_dict(self): + return { + "cached_tokens": self.cached_tokens, + "total_prompt_tokens": self.total_prompt_tokens, + } + + @staticmethod + def from_dict(data: dict): + info = Sample.PrefixCacheInfo() + info.cached_tokens = data.get("cached_tokens", 0) + info.total_prompt_tokens = data.get("total_prompt_tokens", 0) + return info + + prefix_cache_info: PrefixCacheInfo = field(default_factory=PrefixCacheInfo) + def to_dict(self): value = self.__dict__.copy() value["status"] = self.status.value value["spec_info"] = self.spec_info.to_dict() + value["prefix_cache_info"] = self.prefix_cache_info.to_dict() return value @staticmethod def from_dict(data: dict): data["status"] = Sample.Status(data["status"]) data["spec_info"] = Sample.SpecInfo.from_dict(data.get("spec_info", {})) + data["prefix_cache_info"] = Sample.PrefixCacheInfo.from_dict(data.get("prefix_cache_info", {})) return Sample(**data) def get_reward_value(self, args) -> float: @@ -101,6 +135,29 @@ def get_reward_value(self, args) -> float: def effective_response_length(self): return sum(self.loss_mask) if self.loss_mask is not None else self.response_length + def update_from_meta_info(self, args, meta_info: dict): + """ + Update the sample with new information from meta_info returned by the rollout engine. + And extract + """ + if args.sglang_speculative_algorithm: + # cannot directly use spec info from sglang because of partial rollout. + self.spec_info.add(meta_info=meta_info) + + # Collect prefix cache statistics + self.prefix_cache_info.add(meta_info=meta_info) + + if "weight_version" in meta_info: + self.weight_versions.append(meta_info["weight_version"]) + + match meta_info["finish_reason"]["type"]: + case "length": + self.status = Sample.Status.TRUNCATED + case "abort": + self.status = Sample.Status.ABORTED + case "stop": + self.status = Sample.Status.COMPLETED + @dataclass(frozen=True) class ParamInfo: diff --git a/miles/utils/wandb_utils.py b/miles/utils/wandb_utils.py index 4b2bfbfc5..e890a8771 100644 --- a/miles/utils/wandb_utils.py +++ b/miles/utils/wandb_utils.py @@ -157,16 +157,3 @@ def _init_wandb_common(): wandb.define_metric("eval/step") wandb.define_metric("eval/*", step_metric="eval/step") wandb.define_metric("perf/*", step_metric="rollout/step") - - -def get_wandb_offline_dir(args): - """Get the directory where offline W&B data is stored.""" - if _is_offline_mode(args): - if args and hasattr(args, "wandb_dir") and args.wandb_dir: - # Use custom directory if specified - return args.wandb_dir - else: - # Default offline directory is ~/wandb/offline-run- - # This will be created automatically by wandb - return os.path.expanduser("~/wandb") - return None diff --git a/tests/test_fsdp_gptoss_20b.sh b/scripts/run-gptoss-20b-fsdp.sh similarity index 88% rename from tests/test_fsdp_gptoss_20b.sh rename to scripts/run-gptoss-20b-fsdp.sh index b68671b5e..90cab2317 100644 --- a/tests/test_fsdp_gptoss_20b.sh +++ b/scripts/run-gptoss-20b-fsdp.sh @@ -99,16 +99,12 @@ SGLANG_ARGS=( ) -if [ -z "${WANDB_API_KEY}" ]; then - WANDB_ARGS=() -else - WANDB_ARGS=( - --use-wandb - --wandb-project "miles-fsdp-gpt" - --wandb-group "20b-bf16" - --wandb-key "${WANDB_API_KEY}" - ) -fi +WANDB_ARGS=( + --use-wandb + --wandb-project "miles-fsdp-gpt" + --wandb-group "20b-bf16" + --wandb-key ${WANDB_API_KEY} +) # launch the master node of ray in container ray start --head --node-ip-address 127.0.0.1 --num-gpus 4 --disable-usage-stats @@ -126,9 +122,9 @@ ray job submit --address="http://127.0.0.1:8265" \ --train-backend fsdp \ --bf16 \ --attn-implementation eager \ - "${CKPT_ARGS[@]}" \ - "${ROLLOUT_ARGS[@]}" \ - "${OPTIMIZER_ARGS[@]}" \ - "${GRPO_ARGS[@]}" \ - "${SGLANG_ARGS[@]}" \ - "${WANDB_ARGS[@]}" + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${WANDB_ARGS[@]} \ diff --git a/scripts/run-qwen3-235B-A22B-sft.sh b/scripts/run-qwen3-235B-A22B-sft.sh index a2839910d..50b46c048 100644 --- a/scripts/run-qwen3-235B-A22B-sft.sh +++ b/scripts/run-qwen3-235B-A22B-sft.sh @@ -49,7 +49,7 @@ SFT_ARGS=( --rollout-function-path miles.rollout.sft_rollout.generate_rollout --prompt-data ${BASE_FOLDER}/openhermes2_5.parquet --input-key messages - --apply-chat-template + # --apply-chat-template --rollout-shuffle --num-epoch 3 --rollout-batch-size 128 diff --git a/scripts/run-qwen3-4B-amd.sh b/scripts/run-qwen3-4B-amd.sh index 321a9712d..83af90156 100755 --- a/scripts/run-qwen3-4B-amd.sh +++ b/scripts/run-qwen3-4B-amd.sh @@ -15,13 +15,13 @@ set -euxo pipefail ### AMD Support ### -MILES_DIR="${MILES_DIR:-/home/yushensu/projects/miles}" # Default path if not set in environment +MILES_DIR="${MILES_DIR:-/root}" # Default path if not set in environment export MILES_DIR -MODEL_DIR="${MODEL_DIR:-/home/yushensu/projects/model}" # Default path if not set in environment +MODEL_DIR="${MODEL_DIR:-/root}" # Default path if not set in environment export MODEL_DIR -DATA_DIR="${DATA_DIR:-/home/yushensu/projects/data}" # Default path if not set in environment +DATA_DIR="${DATA_DIR:-/root}" # Default path if not set in environment export DATA_DIR # For AMD GPU @@ -153,6 +153,8 @@ ray job submit --address="http://127.0.0.1:8265" \ --actor-num-nodes 1 \ --actor-num-gpus-per-node 8 \ --colocate \ + --no-offload-train \ + --no-offload-rollout \ ${MODEL_ARGS[@]} \ ${CKPT_ARGS[@]} \ ${ROLLOUT_ARGS[@]} \ diff --git a/tests/test_mimo_7B_mtp_only_grad.py b/tests/test_mimo_7B_mtp_only_grad.py index bbd9b2e03..97c76ace5 100644 --- a/tests/test_mimo_7B_mtp_only_grad.py +++ b/tests/test_mimo_7B_mtp_only_grad.py @@ -92,6 +92,10 @@ def execute(): "--rollout-num-gpus 8 " "--sglang-mem-fraction-static 0.8 " "--sglang-enable-metrics " + "--sglang-speculative-algorithm EAGLE " + "--sglang-speculative-num-steps 2 " + "--sglang-speculative-eagle-topk 1 " + "--sglang-speculative-num-draft-tokens 3 " ) # Enable MTP training with loss scaling diff --git a/train.py b/train.py index e5f6da596..a4f6824cc 100644 --- a/train.py +++ b/train.py @@ -80,9 +80,15 @@ def onload_rollout(): if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch, args.num_rollout): if (not args.use_critic) or (rollout_id >= args.num_critic_only_steps): - actor_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) + actor_model.save_model( + rollout_id, + force_sync=rollout_id == args.num_rollout - 1, + ) if args.use_critic: - critic_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) + critic_model.save_model( + rollout_id, + force_sync=rollout_id == args.num_rollout - 1, + ) if args.rollout_global_dataset: ray.get(rollout_manager.save.remote(rollout_id)) diff --git a/train_async.py b/train_async.py index 24f471887..bef1d98ab 100644 --- a/train_async.py +++ b/train_async.py @@ -48,9 +48,15 @@ def train(args): ray.get(actor_model.async_train(rollout_id, rollout_data_curr_ref)) if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch, args.num_rollout): - actor_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) + actor_model.save_model( + rollout_id, + force_sync=rollout_id == args.num_rollout - 1, + ) if args.use_critic: - critic_model.save_model(rollout_id, force_sync=rollout_id == args.num_rollout - 1) + critic_model.save_model( + rollout_id, + force_sync=rollout_id == args.num_rollout - 1, + ) if args.rollout_global_dataset: ray.get(rollout_manager.save.remote(rollout_id)) From 43b954353fea60bc49ff2880485ca6fcb5ae672f Mon Sep 17 00:00:00 2001 From: zijiexia <37504505+zijiexia@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:24:03 -0800 Subject: [PATCH 11/57] Update example dir (#345) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: zhaochenyang20 --- docs/en/get_started/qa.md | 2 +- docs/en/get_started/usage.md | 2 +- examples/README.md | 24 ++++ examples/eval/README.md | 108 ++++++++++++++++-- examples/eval/scripts/run-qwen3-4B.sh | 12 +- examples/formal_math/single_round/README.md | 58 ++++++++-- .../single_round/kimina_wrapper.py | 23 ++-- examples/formal_math/single_round/run.py | 2 +- examples/formal_math/single_round/run_sft.py | 2 +- examples/fully_async/README.md | 14 +-- examples/geo3k_vlm/README.md | 2 +- examples/low_precision/README.md | 12 +- examples/multi_agent/agent_system.py | 15 +-- examples/on_policy_distillation/README.md | 11 +- examples/reproducibility/README.md | 6 +- examples/search-r1/README.md | 4 +- examples/search-r1/generate_with_search.py | 14 +-- examples/strands-agents/README.md | 2 +- examples/swe-agent/README.md | 18 +-- examples/true_on_policy/README.md | 8 +- examples/true_on_policy/run_simple.py | 2 +- scripts/run_qwen3_4b.py | 24 ++-- 22 files changed, 245 insertions(+), 120 deletions(-) create mode 100644 examples/README.md diff --git a/docs/en/get_started/qa.md b/docs/en/get_started/qa.md index 2552b5859..c9e8dad21 100644 --- a/docs/en/get_started/qa.md +++ b/docs/en/get_started/qa.md @@ -57,7 +57,7 @@ 11. **Sglang shows an `an illegal memory access was encountered` error.** - According to the sglang documentation ([https://docs.sglang.ai/references/troubleshooting.html](https://docs.sglang.ai/references/troubleshooting.html)), this could be an OOM error. Consider reducing the value of `--sglang-mem-fraction-static`. + According to [SGLang documentation](https://docs.sglang.io/references/faq.html), this could be an OOM error. Consider reducing the value of `--sglang-mem-fraction-static`. 12. **A `JSONDecodeError` occurs related to torch compile/inductor.** diff --git a/docs/en/get_started/usage.md b/docs/en/get_started/usage.md index dd756cf35..9738131c7 100644 --- a/docs/en/get_started/usage.md +++ b/docs/en/get_started/usage.md @@ -291,7 +291,7 @@ The way SGLang parameters are integrated into miles can be found in [miles/backe ### How to Use the Router -miles uses [sglang-router](https://github.com/sgl-project/sglang/tree/main/sgl-router) to manage the SGLang servers during the training process. You can configure the address of the [sglang-router](https://github.com/sgl-project/sglang/tree/main/sgl-router) using `--sglang-router-ip` and `--sglang-router-port`. If not configured, a router will be started by default within the cluster. +miles uses [sglang-router](https://github.com/sgl-project/sglang/tree/main/sgl-model-gateway) to manage the SGLang servers during the training process. You can configure the address of the [sglang-router](https://github.com/sgl-project/sglang/tree/main/sgl-model-gateway) using `--sglang-router-ip` and `--sglang-router-port`. If not configured, a router will be started by default within the cluster. After starting, all SGLang servers will register with the router via the `/add_worker` endpoint. When actually generating data, you only need to send HTTP requests to the router, which will perform load balancing and forward the requests to the servers. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..c38642fbd --- /dev/null +++ b/examples/README.md @@ -0,0 +1,24 @@ +# Examples + +These examples provide concrete examples to leverage Miles in your own RL workflow. Some examples are just demonstrative, but most of them are verifiable with a concrete performance score. + +## Directory Structure + +- **[DrGRPO](./DrGRPO)**: Custom reducer for Dr.GRPO algorithm. +- **[eval](./eval)**: Documentation and setup for evaluation environments using NeMo-Skills. +- **[eval_multi_task](./eval_multi_task)**: Example for supporting OOD evaluation tasks, e.g., GPQA, IFBench. +- **[formal_math](./formal_math)**: Examples related to formal math reasoning tasks, including a single round demo. +- **[fully_async](./fully_async)**: Demonstrates fully asynchronous rollout generation for higher efficiency. +- **[geo3k_vlm](./geo3k_vlm)**: Training VLMs with FSDP on a single-turn reasoning task using GRPO on the GEO3K dataset. +- **[geo3k_vlm_multi_turn](./geo3k_vlm_multi_turn)**: VLM multi-turn training (FSDP backend) on Geo3k dataset. +- **[low_precision](./low_precision)**: Examples of FP8 training and inference for improved throughput and stability. +- **[multi_agent](./multi_agent)**: Example of running multi-agent RL with `miles`. +- **[on_policy_distillation](./on_policy_distillation)**: Example implementation for on-policy distillation, extending the reinforcement learning pipeline to support teacher–student distillation directly within on-policy training. +- **[reproducibility](./reproducibility)**: Guides on achieving bitwise experiment reproduction using deterministic modes. +- **[retool](./retool)**: Demonstrates the retool functionality for tool-enabled language model generation. +- **[search-r1](./search-r1)**: A minimal reproduction of Search-R1, featuring multi-turn conversation and tool-calling. +- **[strands-agents](./strands-agents)**: Integration example with the Strands-Agents scaffolding framework. +- **[tau-bench](./tau-bench)**: Training in an agentic multi-turn tool use environment (Tau-bench). +- **[train_infer_mismatch_helper](./train_infer_mismatch_helper)**: Algorithmic methods for rollout correction (e.g., TIS, MIS). +- **[true_on_policy](./true_on_policy)**: Ensures strictly equal log probabilities between inference (SGLang) and training engines. +- **[true_on_policy_vlm](./true_on_policy_vlm)**: "True On-Policy" training demonstration for VLM (Qwen3-VL). diff --git a/examples/eval/README.md b/examples/eval/README.md index 4e7e0b4c0..adafe9a5b 100644 --- a/examples/eval/README.md +++ b/examples/eval/README.md @@ -1,15 +1,33 @@ -# Docs +# Evaluation with Nemo Skills + +This directory contains configuration and utilities for offloading complex evaluation benchmarks to a separate environment using the `eval_delegate` mechanism. It is designed to integrate with [Nemo Skills](https://github.com/NVIDIA/NeMo-Skills) for running benchmarks like AIME25, Arena-Hard, and HLE, which may require specific environments distinct from the main training setup. + +## Overview + +The setup allows Miles to delegate evaluation tasks to a dedicated "Skills" server. This creates a clear separation of concerns: + +1. **Miles Container**: Runs the main training loop and hosts the model using SGLang. +2. **Skills Container**: Hosts the `nemo_skills` environment, runs the evaluation logic, and queries the model running in the Miles container. ## Prerequisites -- A writable host directory for cached data (`/data/.cache`) -- Choose descriptive container names to replace the placeholders (``, ``). -## 1) Prepare host network +- A writable host directory for cached data (e.g., `/data/.cache`). +- Docker installed with NVIDIA GPU support. + +## Setup Instructions + +### Prepare Host Network + +Create a Docker network to allow communication between the Miles and Skills containers. + ```bash docker network create skills-net ``` -## 2) Launch the miles container +### Launch the Miles Container + +Start the main container where Miles and the model will run. Replace `` with your desired name (e.g., `miles_main`). + ```bash docker run \ -itd \ @@ -25,7 +43,10 @@ docker run \ /bin/bash ``` -## 3) Launch the Skills container +### Launch the Skills Container + +Start the container that will run the evaluation benchmarks. Replace `` with your desired name (e.g., `skills_env`). + ```bash docker run \ -itd \ @@ -42,17 +63,26 @@ docker run \ /bin/bash ``` -## 4) Inside the Skills container -Clone repos and install the Skills package: +### Configure the Skills Container + +Enter the **Skills container** and set up the environment. + +**a) Install Dependencies** + ```bash +# Clone repositories git clone -b miles_skills https://github.com/guapisolo/miles.git /opt/miles git clone -b miles https://github.com/guapisolo/Skills.git /opt/Skills +# Install Skills package cd /opt/Skills pip install -e . ``` -Download/prepare datasets: +**b) Prepare Datasets** + +Download and prepare the datasets you intend to use. + ```bash cd /opt/Skills/nemo_skills/dataset python3 aime25/prepare.py @@ -60,7 +90,10 @@ python3 hle/prepare.py python3 arena-hard/prepare.py ``` -Start the skills server: +**c) Start the Evaluation Server** + +Start the server that listens for evaluation requests from Miles. + ```bash cd /opt/miles python examples/eval/nemo_skills/skills_server.py \ @@ -72,5 +105,58 @@ python examples/eval/nemo_skills/skills_server.py \ --max-concurrent-requests 512 \ --openai-model-name miles-openai-model ``` +*Note: You can now connect to the server at `skills_server:9050` from within the `skills-net` Docker network. The server always proxies evaluation traffic to an OpenAI-compatible sglang router (Miles starts and manage the router), so adjust `--openai-model-name` and `--max-concurrent-requests` as needed for your deployment. + +## Running Evaluation + +The example scripts are located in `examples/eval/scripts`. Here is an example workflow for training Qwen3-4B with delegated evaluation. + +### Prepare Miles Container + +Enter the **Miles container** and install the package. + +```bash +cd /root/miles +git pull +pip install -e . +``` + +### Download Model and Data + +```bash +# Download model weights (Qwen3-4B) +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B + +# Download training dataset (dapo-math-17k) +hf download --repo-type dataset zhuzilin/dapo-math-17k \ + --local-dir /root/dapo-math-17k +``` + +### Convert Model to Megatron-LM Format + +You need to convert the HF model to the format required by Megatron-LM. Ensure you load the correct model arguments first. + +```bash +# Source model arguments +source scripts/models/qwen3-4B.sh + +# Convert model +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen3-4B \ + --save /root/Qwen3-4B_torch_dist +``` + +### Run the Training Script + +Run the training script. + +```bash +bash examples/eval/scripts/run-qwen3-4B.sh +``` + +## Configuration -You can now connect to the server at `skills_server:9050` from within the `skills-net` Docker network. The server always proxies evaluation traffic to an OpenAI-compatible sglang router (Miles starts and manage the router), so adjust `--openai-model-name` and `--max-concurrent-requests` as needed for your deployment. +The evaluation configuration is defined in `examples/eval/scripts/multi_tasks.yaml`. It specifies: +- `delegate`: Configurations for the external skills server (URL, timeouts). +- `datasets`: List of datasets to evaluate on (e.g., `aime25`, `arena-hard`). diff --git a/examples/eval/scripts/run-qwen3-4B.sh b/examples/eval/scripts/run-qwen3-4B.sh index 4343377f1..34891126d 100644 --- a/examples/eval/scripts/run-qwen3-4B.sh +++ b/examples/eval/scripts/run-qwen3-4B.sh @@ -36,10 +36,10 @@ source "${REPO_ROOT}/scripts/models/qwen3-4B.sh" EVAL_CONFIG_PATH=${SKILLS_EVAL_CONFIG_PATH:-"${REPO_ROOT}/examples/eval/scripts/multi_tasks.yaml"} CKPT_ARGS=( - --hf-checkpoint /root/shared/Qwen3-4B - --ref-load /root/shared/Qwen3-4B_torch_dist - --load /root/shared/Qwen3-4B_miles/ - --save /root/shared/Qwen3-4B_miles/ + --hf-checkpoint /root/Qwen3-4B + --ref-load /root/Qwen3-4B_torch_dist + --load /root/Qwen3-4B_miles/ + --save /root/Qwen3-4B_miles/ --save-interval 20 ) @@ -122,7 +122,9 @@ MISC_ARGS=( ) export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} -export CUDA_VISIBLE_DEVICES=6,7 +# export CUDA_VISIBLE_DEVICES=0,1 +# Set Up Your GPUs for Training + ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 2 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 RUNTIME_ENV_JSON="{ diff --git a/examples/formal_math/single_round/README.md b/examples/formal_math/single_round/README.md index e66931566..706d7714a 100644 --- a/examples/formal_math/single_round/README.md +++ b/examples/formal_math/single_round/README.md @@ -1,23 +1,65 @@ -# Usage +# Single-Round Formal Math with Lean 4 and RL -For the minimal demo: +This directory contains an example of training a model to solve formal math problems using Lean 4. It leverages Reinforcement Learning (GRPO) with a "verifier-in-the-loop" approach, where generated proofs are verified for correctness using the [Kimina](https://github.com/project-numina/kimina-lean-server) verifier. -```shell -# install dependencies +## Overview + +- **Task**: Given a formal math statement in Lean 4, generate a valid proof. +- **Method**: Single-turn reinforcement learning (GRPO). The model generates a full proof (including thoughts/plans), and the reward is determined by whether the proof compiles and is valid. +- **Verifier**: Uses `kimina-lean-server` running in a Docker container to verify the generated Lean code. + +## Prerequisites + +### Docker Setup +You need Docker installed and a specific network for communication between the training process and the Kimina verifier: + +```bash +# Create a docker network for kimina and miles to communicate +docker network create formal_math +``` + +**Note**: The training script will launch a `kimina-lean-server` container. It requires mounting the host Docker socket (`/var/run/docker.sock`) so the script can manage sibling containers. Connect miles container to the same docker network. + +### Install Dependencies + +```bash apt update && apt install -y docker-cli pip install kimina-client polars +``` + +## Quick Start: Minimal Demo -# prepare data +This minimal demo (`run_minimal.py`) runs a self-contained training loop on a small dataset. + +### Prepare Data +Download and process the data (e.g., FineLeanCorpus, MiniF2F). + +```bash python examples/formal_math/single_round/prepare_data.py --output-name minimal_demo +``` -# prepare ray, model, test dataset, etc -# normally just use this script, but here we want to demonstrate run_minimal.py, thus skip ray-submit part +### Prepare Models & Environment +Use `run.py` to download the base model (e.g., Qwen3-8B) and set up the environment. We skip the actual training submission here (`MILES_SCRIPT_ENABLE_RAY_SUBMIT=0`) as we will use the minimal runner next. + +```bash MILES_SCRIPT_ENABLE_RAY_SUBMIT=0 python examples/formal_math/single_round/run.py +``` -# run +### Run Training +Launch the minimal training script. + +```bash python examples/formal_math/single_round/run_minimal.py ``` +## Advanced Usage + +For full-scale training or standard runs, use `run.py`. This script leverages `miles.utils.external_utils.command_utils` to handle cluster setup and execution. + +```bash +python examples/formal_math/single_round/run.py +``` + The code also support more complicated cases, e.g.: * SFT + RL diff --git a/examples/formal_math/single_round/kimina_wrapper.py b/examples/formal_math/single_round/kimina_wrapper.py index 35a40f922..1f400fa15 100644 --- a/examples/formal_math/single_round/kimina_wrapper.py +++ b/examples/formal_math/single_round/kimina_wrapper.py @@ -56,30 +56,33 @@ def _create_actor_per_node(actor_cls) -> list: @ray.remote class _KiminaServerActor: def __init__(self): - self.addr = _get_current_node_host_ip() self.port = get_free_port() if _KILL_PREVIOUS_KIMINA_DOCKER: _docker_stop_all() - _docker_start(port=self.port) + self.docker_name = _docker_start(port=self.port) _wait_server_ready(base_url=self.get_api_url()) def get_api_url(self): - return f"http://{self.addr}:{self.port}" + return f"http://{self.docker_name}:8000" def _docker_start(port: int): - name = f"kimina_lean_server_auto_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{random.randint(0, 1000000)}" + docker_name = ( + f"kimina_lean_server_auto_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{random.randint(0, 1000000)}" + ) exec_command( "docker run " "-d " - f"--name {name} " + f"--name {docker_name} " "--restart unless-stopped " + "--network formal_math " # "--env-file .env " # do not use env yet f"-p {port}:8000 " f"projectnumina/kimina-lean-server:2.0.0" ) + return docker_name def _wait_server_ready(base_url: str): @@ -101,13 +104,3 @@ def _docker_stop_all(): '[ -n "$ids" ] && docker stop $ids && docker rm $ids; ' "true" ) - - -def _get_current_node_host_ip(): - # when RL container uses network=host - return "127.0.0.1" - - # when RL container does not use network=host - # https://stackoverflow.com/questions/22944631 - # out = exec_command("ip route show default | awk '/default/ {print $3}'", capture_output=True) - # return out.strip() diff --git a/examples/formal_math/single_round/run.py b/examples/formal_math/single_round/run.py index 8cbb9d738..25f9ad07f 100644 --- a/examples/formal_math/single_round/run.py +++ b/examples/formal_math/single_round/run.py @@ -23,7 +23,7 @@ def prepare(): U.exec_command("mkdir -p /root/models /root/datasets") - U.exec_command(f"huggingface-cli download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") if arg_ref_load is None: U.convert_checkpoint( model_name=MODEL_NAME, diff --git a/examples/formal_math/single_round/run_sft.py b/examples/formal_math/single_round/run_sft.py index d294b133a..f24f79e3a 100644 --- a/examples/formal_math/single_round/run_sft.py +++ b/examples/formal_math/single_round/run_sft.py @@ -13,7 +13,7 @@ def prepare(): U.exec_command("mkdir -p /root/models /root/datasets") - U.exec_command(f"huggingface-cli download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") U.convert_checkpoint(model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS) diff --git a/examples/fully_async/README.md b/examples/fully_async/README.md index 7c7bdf343..53f08b3ca 100644 --- a/examples/fully_async/README.md +++ b/examples/fully_async/README.md @@ -1,15 +1,15 @@ -## Fully Asynchronous Rollout Example +# Fully Asynchronous Rollout Example This example shows a simple way to make rollout generation **fully asynchronous**: a single global worker is created once and then keeps running in the background, continuously pulling prompts and launching generation tasks. Training only needs to fetch already finished results. This removes the per‑step wait that happens in the normal synchronous style. -### Files +## Files * `fully_async_rollout.py`: global async worker + `generate_rollout_fully_async` entry. * `run-qwen3-4b-fully_async.sh`: example launch script with Qwen3‑4B. -### Prerequisite +## Prerequisite First set up model & environment following the Qwen3-4B example. -### Quick Start +## Quick Start ```bash cd miles bash examples/fully_async/run-qwen3-4b-fully_async.sh @@ -20,18 +20,18 @@ Creating new global async worker... Continuous async rollout worker started ``` -### How It Works (Very Short) +## How It Works (Very Short) * First call: create `AsyncRolloutWorker` (thread + asyncio loop). * Loop keeps up to `--rollout-batch-size` tasks in flight using `generate_and_rm_group`. * Completed groups are pushed into a queue; caller drains until it has enough samples. * Worker is stopped automatically at process exit. -### Limitations +## Limitations * No evaluation mode. * Ordering is best effort (sorted at the end by index). * Minimal error handling. -### Config Differences (2 Key Points) +## Config Differences (2 Key Points) To enable the fully async pattern there are only two changes compared to a normal run: 1. Use the async training driver: `train_async.py` (not `train.py`). diff --git a/examples/geo3k_vlm/README.md b/examples/geo3k_vlm/README.md index 6ef751228..a71e0d760 100644 --- a/examples/geo3k_vlm/README.md +++ b/examples/geo3k_vlm/README.md @@ -43,7 +43,7 @@ ds.to_parquet("/root/datasets/geo3k_imgurl/train_formatted.parquet") ```bash export WANDB_API_KEY=your_wandb_api_key -# Megatron backend (default -> Qwen3-VL-2B-Instruct + Megatron) +# Megatron backend (default -> Qwen3-VL-8B-Instruct + Megatron) ./examples/geo3k_vlm/run_geo3k_vlm.sh # FSDP backend diff --git a/examples/low_precision/README.md b/examples/low_precision/README.md index 12ceed735..9fd4d115f 100644 --- a/examples/low_precision/README.md +++ b/examples/low_precision/README.md @@ -1,14 +1,14 @@ -## FP8 training examples +# FP8 training examples -This is an example of FP8 training and FP8 inference. Under FP8 training and inference, it can achieve more efficient inference throughput and lower training-inference mismatch, resulting in more stable training. +This is an example of FP8 training and FP8 inference. Under FP8 training and inference, it can achieve more efficient inference throughput and lower training-inference mismatch, resulting in more stable training. More details can be found in [this blog](https://lmsys.org/blog/2025-11-25-fp8-rl/). -### Files +## Files * `run-qwen3-4b-fp8.sh`: example launch script with Qwen3‑4B in FP8. * `run-qwen3-30b-a3b-fp8-two-nodes.sh`: example launch script for running Qwen3‑30B‑A3B in FP8 across two nodes. -### Quick Start +## Quick Start 1. Check if your training script is properly configured. @@ -44,7 +44,7 @@ Following the above command will launch FP8 training. Note that TransformerEngine does not specifically save FP8 quantized weights; the saved torch dist remains in original precision (usually bf16). If you want to evaluate under FP8, you need to convert the checkpoint from `torch_dist` to HuggingFace format, then convert to FP8 HuggingFace format. -### Quick Explanation +## Quick Explanation Here's a quick explanation of how FP8 training is currently implemented in miles: @@ -57,7 +57,7 @@ Here's a quick explanation of how FP8 training is currently implemented in miles 4. Save checkpoint: Similar to weight updates, if checkpoints need to be saved from the training engine, they will also be dequantized back to bf16 and saved to `torch_dist` format checkpoints. -### TODO +## TODO Currently, FP8 is far from being a complete feature and still has the following bugs, for examples: diff --git a/examples/multi_agent/agent_system.py b/examples/multi_agent/agent_system.py index e1376bcd7..46db66143 100644 --- a/examples/multi_agent/agent_system.py +++ b/examples/multi_agent/agent_system.py @@ -20,18 +20,7 @@ async def generate_response(args, prompt, key): url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" - if args.apply_chat_template: - assert isinstance(prompt, list), "prompt should be a list when apply_chat_template is True" - prompt_text = tokenizer.apply_chat_template( - prompt, - tokenize=False, - add_generation_prompt=True, # Add generation prompt for the assistant - **(args.apply_chat_template_kwargs or {}), - ) - sample.prompt = prompt_text - else: - assert isinstance(prompt, str), "prompt should be a string when apply_chat_template is False" - sample.prompt = prompt + sample.prompt = prompt prompt_token_ids = tokenizer(sample.prompt, add_special_tokens=False)["input_ids"] sample.tokens = prompt_token_ids prompt_length = len(prompt_token_ids) @@ -160,7 +149,7 @@ async def select(self, args, problem_statement, candidate_solutions: list[str]) def extract_selected_solution_idx(self, response: str, candidate_solutions: list[str]) -> int: """Extracts the selected solution ID from the response.""" - PATTERN = re.compile("Judgment:\s*(\d+)") + PATTERN = re.compile(r"Judgment:\s*(\d+)") matched = PATTERN.findall(response) try: selected_id = int(matched[0]) - 1 diff --git a/examples/on_policy_distillation/README.md b/examples/on_policy_distillation/README.md index bab6340d6..ff7a8207b 100644 --- a/examples/on_policy_distillation/README.md +++ b/examples/on_policy_distillation/README.md @@ -17,17 +17,18 @@ In this example, the teacher model acts as a reward model (RM) by providing teac ```bash hf download Qwen/Qwen3-32B --local-dir /root/Qwen3-32B hf download Qwen/Qwen3-8B --local-dir /root/Qwen3-8B -hf download zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k ``` 2. Run the hf to mcore for student model conversion: ```bash -source "${HOME_DIR}/miles/scripts/models/qwen3-8B.sh" +cd /root/miles +source scripts/models/qwen3-8B.sh -PYTHONPATH=/root/Megatron-LM:${HOME_DIR}/miles python tools/convert_hf_to_torch_dist.py \ +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ ${MODEL_ARGS[@]} \ - --hf-checkpoint ${HOME_DIR}/checkpoints/Qwen/Qwen3-8B \ - --save ${HOME_DIR}/checkpoints/Qwen/Qwen3-8B_torch_dist + --hf-checkpoint /root/Qwen3-8B \ + --save /root/Qwen3-8B_torch_dist ``` 3. run on-policy distillation: ```bash diff --git a/examples/reproducibility/README.md b/examples/reproducibility/README.md index 84fbb028a..7005f3cd9 100644 --- a/examples/reproducibility/README.md +++ b/examples/reproducibility/README.md @@ -29,8 +29,8 @@ For data and checkpoint preparation, please run: ```bash # download -huggingface-cli download --repo-type dataset zhuzilin/gsm8k --local-dir /root/gsm8k -huggingface-cli download Qwen/Qwen2.5-0.5B-Instruct --local-dir /root/Qwen2.5-0.5B-Instruct +hf download --repo-type dataset zhuzilin/gsm8k --local-dir /root/gsm8k +hf download Qwen/Qwen2.5-0.5B-Instruct --local-dir /root/Qwen2.5-0.5B-Instruct # convert ckpt cd miles/ @@ -48,4 +48,4 @@ And to run training, bash examples/reproducibility/run-qwen2.5-0.5B-gsm8k.sh ``` -For screen shots of the wandb, please refer to [pull#370](https://github.com/radixark/miles/pull/370). +For screen shots of the wandb, please refer to [pull#370](https://github.com/THUDM/slime/pull/370). diff --git a/examples/search-r1/README.md b/examples/search-r1/README.md index 46b1ef85c..b9c426f8b 100644 --- a/examples/search-r1/README.md +++ b/examples/search-r1/README.md @@ -20,6 +20,8 @@ Download and prepare the training data: cd /root/ git clone https://github.com/PeterGriffinJin/Search-R1.git cd Search-R1/ +pip install -e . --no-deps +pip install tensordict # Set your working directory WORK_DIR=/root/Search-R1 @@ -45,7 +47,7 @@ Initialize the Qwen2.5-3B model: ```bash # hf checkpoint -huggingface-cli download Qwen/Qwen2.5-3B --local-dir /root/Qwen2.5-3B +hf download Qwen/Qwen2.5-3B --local-dir /root/Qwen2.5-3B # mcore checkpoint cd /root/miles diff --git a/examples/search-r1/generate_with_search.py b/examples/search-r1/generate_with_search.py index bcf94f0fd..a1096b745 100644 --- a/examples/search-r1/generate_with_search.py +++ b/examples/search-r1/generate_with_search.py @@ -3,7 +3,6 @@ import asyncio import re -import numpy as np from qa_em_format import compute_score_em @@ -151,18 +150,7 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" # Handle partial rollout samples: continue generation from existing response - prompt = sample.prompt - if args.apply_chat_template: - assert isinstance(prompt, np.ndarray), "prompt should be a np.ndarray when apply_chat_template is True" - prompt_text = state.tokenizer.apply_chat_template( - prompt, - tokenize=False, - add_generation_prompt=True, # Add generation prompt for the assistant - **(args.apply_chat_template_kwargs or {}), - ) - else: - assert isinstance(prompt, str), "prompt should be a string when apply_chat_template is False" - prompt_text = prompt + prompt_text = sample.prompt prompt_tokens_ids = state.tokenizer(prompt_text, add_special_tokens=False)["input_ids"] response = "" response_token_ids = [] diff --git a/examples/strands-agents/README.md b/examples/strands-agents/README.md index 3a785f31d..ad597645e 100644 --- a/examples/strands-agents/README.md +++ b/examples/strands-agents/README.md @@ -16,7 +16,7 @@ This is a running example that connects the [Strands-Agents](https://github.com/ ```bash # hf checkpoint -huggingface-cli download Qwen/Qwen3-4B-Instruct-2507 --local-dir /root/models/Qwen/Qwen3-4B-Instruct-2507 +hf download Qwen/Qwen3-4B-Instruct-2507 --local-dir /root/models/Qwen/Qwen3-4B-Instruct-2507 # mcore checkpoint cd /root/miles diff --git a/examples/swe-agent/README.md b/examples/swe-agent/README.md index a7c3ee74a..d71ad3ecd 100644 --- a/examples/swe-agent/README.md +++ b/examples/swe-agent/README.md @@ -1,4 +1,6 @@ -### Introduction +# SWE-agent training Example + +## Introduction This is an example for SWE-agent training. This example uses NVIDIA's Nemo-Gym as the Gym environment implement, SWE-Gym as the training data, and SWE-bench as the evaluation. @@ -7,12 +9,12 @@ This implementation of this example is partially in submodules below: - mini-swe-agent: https://github.com/yueming-yuan/nv-mini-swe-agent/tree/miles-swe-agent -### Prepare environment -#### Update submodules +## Prepare environment +### Update submodules ```bash git submodule update --init --recursive . ``` -#### Docker settings +### Docker settings ```bash # 1. create a docker network docker network create swe-net @@ -55,7 +57,7 @@ apt update && apt install -y zsh curl git python3 python3-pip docker.io ``` note: `-v /var/run/docker.sock:/var/run/docker.sock` is required for Docker-in-Docker SWE environment execution; use `--network swe-net` to enable communication between training & environment. -#### Installation +### Installation In **environment docker**, install Gym ```bash @@ -84,14 +86,14 @@ Now you should be able to run the SWE-agent server. For **miles docker** setup, please follow the standard setup process. -### Preparing data +## Preparing data In **miles docker**, download **SWE-Gym** data from huggingface and convert it to Miles' prompt data format with this script. ``` cd miles/examples/swe-agent python download_and_process_data.py --input SWE-Gym/SWE-Gym --output /root/swe_train.jsonl ``` -### Running train +## Running train 1. In environment docker, launch the agent server ```bash cd Gym @@ -110,7 +112,7 @@ bash examples/swe-agent/run-qwen3-4b-instruct.sh ``` -### Troubleshooting +## Troubleshooting 1. The first time of every SWE environment can be slow, and may need to wait before generation, because each SWE-Gym task has a specific docker, and `docker pull` takes time. 2. Sometimes the environment may also be slow at evaluation. The timeout of evaluation is 10 minutes by default. If the server is stuck at `[EVAL] Running eval`, you may need to wait for it. diff --git a/examples/true_on_policy/README.md b/examples/true_on_policy/README.md index 918f5f3a8..620564d41 100644 --- a/examples/true_on_policy/README.md +++ b/examples/true_on_policy/README.md @@ -1,6 +1,6 @@ # True On-Policy between Training and Inference -True on-policy ensures that the log probs generated by inference engine (SGLang) is strictly equal to the one generated by the training Engine. +True on-policy ensures that the log probs generated by inference engine (SGLang) is strictly equal to the one generated by the training Engine. Here's our [blog](https://lmsys.org/blog/2025-12-03-miles-fsdp/) for more details. ## Examples @@ -17,7 +17,7 @@ python examples/true_on_policy/run_simple.py This script contains more features for various use cases, and one flag is about the true on policy feature. ```bash -python scripts/run_qwen3_4b_fsdp.py --true-on-policy +python scripts/run_qwen3_4b.py --train-backend fsdp --true-on-policy ``` In order to quickly see the curve, you may use `--mode debug_minimal`, which will skip evaluation and run generation with a very short output sequence length. Since true on policy is unrelated to OSL or answer correctness, this can be used for quick experiments. @@ -45,7 +45,7 @@ Detailed reproduction refers to [this](https://gist.github.com/fzyzcjy/46f9fc096 ## How it is Implemented -The core idea is to make each and every operation in training and inference be bitwise equal. The main code is implemented in [#566](https://github.com/radixark/miles/pull/566) and [SGLang#12058](https://github.com/sgl-project/sglang/pull/12058). +The core idea is to make each and every operation in training and inference be bitwise equal. The main code is implemented in [#566](https://github.com/THUDM/slime/pull/566) and [SGLang#12058](https://github.com/sgl-project/sglang/pull/12058). Briefly speaking, we handled the following components to make them aligned: @@ -53,7 +53,7 @@ Briefly speaking, we handled the following components to make them aligned: * GEMM: We use [DeepGEMM](https://github.com/deepseek-ai/DeepGEMM) for fast matrix multiplication while preserving true-on-policy, thanks to its algorithm to pick things like tensor core instructions ([SGLang#12142](https://github.com/sgl-project/sglang/pull/12142)). * Batch invariant kernels: This is a prerequisite for true on-policy, and we use [the ones](https://github.com/thinking-machines-lab/batch_invariant_ops) from the Thinking Machines Lab. * Torch compile: We also utilize [`torch.compile`](https://docs.pytorch.org/docs/stable/generated/torch.compile.html) to speed up by avoiding many tiny kernels. -* We align numeric operation details between the two systems for simplicity, such as op dtype, detailed kernels, etc. Some operations can also be compiled to speedup ([#603](https://github.com/radixark/miles/pull/603), [SGLang#12161](https://github.com/sgl-project/sglang/pull/12161)). +* We align numeric operation details between the two systems for simplicity, such as op dtype, detailed kernels, etc. Some operations can also be compiled to speedup ([#603](https://github.com/THUDM/slime/pull/603), [SGLang#12161](https://github.com/sgl-project/sglang/pull/12161)). In order to more easily align the two parts, we use SGLang's [dumper](https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/debug_utils/dumper.py) tool for quick comparisons. (Need [#12622](https://github.com/sgl-project/sglang/pull/12622) and [#12623](https://github.com/sgl-project/sglang/pull/12623) for most convenience.) diff --git a/examples/true_on_policy/run_simple.py b/examples/true_on_policy/run_simple.py index 1b472b806..7e317195d 100644 --- a/examples/true_on_policy/run_simple.py +++ b/examples/true_on_policy/run_simple.py @@ -14,7 +14,7 @@ def prepare(): U.exec_command("mkdir -p /root/models /root/datasets") - U.exec_command(f"huggingface-cli download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") U.hf_download_dataset("zhuzilin/gsm8k") diff --git a/scripts/run_qwen3_4b.py b/scripts/run_qwen3_4b.py index d1aa63301..393b8a12e 100644 --- a/scripts/run_qwen3_4b.py +++ b/scripts/run_qwen3_4b.py @@ -40,7 +40,7 @@ def __post_init__(self): def prepare(args: ScriptArgs): U.exec_command("mkdir -p /root/models /root/datasets") - U.exec_command(f"huggingface-cli download Qwen/{args.model_name} --local-dir /root/models/{args.model_name}") + U.exec_command(f"hf download Qwen/{args.model_name} --local-dir /root/models/{args.model_name}") U.hf_download_dataset("zhuzilin/dapo-math-17k") U.hf_download_dataset("zhuzilin/aime-2024") @@ -49,9 +49,7 @@ def prepare(args: ScriptArgs): U.hf_download_dataset("zyzshishui0627/IFBench") if args.rollout_fp8: - U.exec_command( - f"huggingface-cli download Qwen/{args.model_name}-FP8 --local-dir /root/models/{args.model_name}-FP8" - ) + U.exec_command(f"hf download Qwen/{args.model_name}-FP8 --local-dir /root/models/{args.model_name}-FP8") if (args.train_backend == "megatron") and not args.enable_megatron_bridge: U.convert_checkpoint( @@ -64,23 +62,21 @@ def prepare(args: ScriptArgs): def execute(args: ScriptArgs): load_save_path = f"/root/shared_data/{args.run_id}/checkpoints" + + ref_load_path = f"/root/models/{args.model_name}" + if args.train_backend == "megatron" and not args.enable_megatron_bridge: + ref_load_path = f"/root/models/{args.model_name}_torch_dist" + ckpt_args = ( f"--hf-checkpoint /root/models/{args.model_name}{'-FP8' if args.rollout_fp8 else ''} " f"--load {load_save_path} " + f"--ref-load {ref_load_path} " f"--save {load_save_path} " f"--save-interval {2 if args.mode == 'debug_minimal' else 20} " - f"--save-retain-interval {2 if args.mode == 'debug_minimal' else 20} " ) + if args.train_backend == "megatron": - ref_load_path = ( - f"/root/models/{args.model_name}/" - if args.enable_megatron_bridge - else f"/root/models/{args.model_name}_torch_dist" - ) - ckpt_args += ( - # FSDP does not support this - f"--ref-load {ref_load_path} " - ) + ckpt_args += f"--save-retain-interval {2 if args.mode == 'debug_minimal' else 20} " rollout_args = ( "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " From 236b640bcc454b0ade8ed09738d4d944de3b0179 Mon Sep 17 00:00:00 2001 From: miles-code-angel Date: Wed, 7 Jan 2026 21:48:25 -0800 Subject: [PATCH 12/57] update code (#411) --- .github/workflows/pr-test.yml | 98 ++- .github/workflows/pr-test.yml.j2 | 44 +- build_conda.sh | 16 +- docker/README.md | 4 +- docker/patch/v0.5.7/megatron.patch | 681 +++++++++++++++ docker/patch/v0.5.7/sglang.patch | 864 +++++++++++++++++++ docker/version.txt | 2 +- miles/backends/fsdp_utils/actor.py | 2 + miles/backends/fsdp_utils/checkpoint.py | 9 +- miles/backends/megatron_utils/actor.py | 12 +- miles/backends/megatron_utils/arguments.py | 2 +- miles/backends/megatron_utils/checkpoint.py | 79 ++ miles/backends/sglang_utils/sglang_engine.py | 11 + miles/ray/rollout.py | 103 ++- miles/utils/arguments.py | 12 +- miles/utils/health_monitor.py | 93 +- miles/utils/mask_utils.py | 3 +- tests/test_qwen2.5_0.5B_gsm8k_async_short.py | 130 +++ tests/test_qwen2.5_0.5B_gsm8k_short.py | 129 +++ train.py | 49 +- 20 files changed, 2251 insertions(+), 92 deletions(-) create mode 100644 docker/patch/v0.5.7/megatron.patch create mode 100644 docker/patch/v0.5.7/sglang.patch create mode 100644 tests/test_qwen2.5_0.5B_gsm8k_async_short.py create mode 100644 tests/test_qwen2.5_0.5B_gsm8k_short.py diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index ce9c2daaa..8ea939739 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -48,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}] + info: [{"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_async_short.py"}, {"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_short.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_colocated_2xGPU.py"}] defaults: run: working-directory: ${{ github.workspace }} @@ -69,8 +69,52 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} - e2e-test-long: - if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-long')) + e2e-test-fsdp: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-fsdp')) + runs-on: self-hosted + container: + image: radixark/miles:latest + options: > + --gpus all + --ipc=host + --shm-size=16g + --ulimit memlock=-1 + --ulimit stack=67108864 + --memory=0 + --memory-swap=0 + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + strategy: + fail-fast: false + matrix: + info: [{"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}] + defaults: + run: + working-directory: ${{ github.workspace }} + env: + GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} + WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} + MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install + shell: bash + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages + + - name: Execute + shell: bash + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + + e2e-test-megatron: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-megatron')) runs-on: self-hosted container: image: radixark/miles:latest @@ -92,7 +136,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k.py"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k_async.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_colocated_2xGPU.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}] + info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}] defaults: run: working-directory: ${{ github.workspace }} @@ -201,6 +245,50 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + e2e-test-long: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-long')) + runs-on: self-hosted + container: + image: radixark/miles:latest + options: > + --gpus all + --ipc=host + --shm-size=16g + --ulimit memlock=-1 + --ulimit stack=67108864 + --memory=0 + --memory-swap=0 + -e http_proxy=$http_proxy + -e https_proxy=$https_proxy + -e HTTP_PROXY=$HTTP_PROXY + -e HTTPS_PROXY=$HTTPS_PROXY + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + strategy: + fail-fast: false + matrix: + info: [{"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k.py"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k_async.py"}] + defaults: + run: + working-directory: ${{ github.workspace }} + env: + GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} + WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} + MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install + shell: bash + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages + + - name: Execute + shell: bash + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + e2e-test-image: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-image')) runs-on: self-hosted @@ -224,7 +312,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py --async-save"}] + info: [{"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_async_short.py"}, {"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_short.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_colocated_2xGPU.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}, {"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py --async-save"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k.py"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k_async.py"}] defaults: run: working-directory: ${{ github.workspace }} diff --git a/.github/workflows/pr-test.yml.j2 b/.github/workflows/pr-test.yml.j2 index 12bfae9fe..84cac9114 100644 --- a/.github/workflows/pr-test.yml.j2 +++ b/.github/workflows/pr-test.yml.j2 @@ -2,22 +2,27 @@ 'e2e-test-short': { 'label': 'run-ci-short', 'tests': [ - {'test_file': 'test_quick_start_glm4_9B.py', 'num_gpus': 8}, - {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8}, - {'test_file': 'test_qwen3_4B_ppo.py', 'num_gpus': 8}, - {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen2.5_0.5B_gsm8k_async_short.py', 'num_gpus': 4}, + {'test_file': 'test_qwen2.5_0.5B_gsm8k_short.py', 'num_gpus': 4}, + {'test_file': 'test_qwen3_0.6B_fsdp_colocated_2xGPU.py', 'num_gpus': 2}, + ], + }, + 'e2e-test-fsdp': { + 'label': 'run-ci-fsdp', + 'tests': [ {'test_file': 'test_qwen3_4B_fsdp_true_on_policy.py', 'num_gpus': 2}, - {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_vl_4B_fsdp.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_0.6B_fsdp_distributed.py', 'num_gpus': 2}, ], }, - 'e2e-test-long': { - 'label': 'run-ci-long', + 'e2e-test-megatron': { + 'label': 'run-ci-megatron', 'tests': [ - {'test_file': 'test_qwen2.5_0.5B_gsm8k.py', 'num_gpus': 2}, - {'test_file': 'test_qwen2.5_0.5B_gsm8k_async.py', 'num_gpus': 2}, - {'test_file': 'test_qwen3_0.6B_fsdp_colocated_2xGPU.py', 'num_gpus': 2}, - {'test_file': 'test_qwen3_0.6B_fsdp_distributed.py', 'num_gpus': 2}, + {'test_file': 'test_quick_start_glm4_9B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_4B_ppo.py', 'num_gpus': 8}, + {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, ], }, 'e2e-test-precision': { @@ -33,20 +38,33 @@ {'test_file': 'test_qwen3_4B_ckpt.py --async-save', 'num_gpus': 8}, ], }, + 'e2e-test-long': { + 'label': 'run-ci-long', + 'tests': [ + {'test_file': 'test_qwen2.5_0.5B_gsm8k.py', 'num_gpus': 2}, + {'test_file': 'test_qwen2.5_0.5B_gsm8k_async.py', 'num_gpus': 2}, + ], + }, 'e2e-test-image': { 'label': 'run-ci-image', 'image': 'radixark/miles-test:latest', 'tests': [ + {'test_file': 'test_qwen2.5_0.5B_gsm8k_async_short.py', 'num_gpus': 4}, + {'test_file': 'test_qwen2.5_0.5B_gsm8k_short.py', 'num_gpus': 4}, + {'test_file': 'test_qwen3_0.6B_fsdp_colocated_2xGPU.py', 'num_gpus': 2}, + {'test_file': 'test_qwen3_4B_fsdp_true_on_policy.py', 'num_gpus': 2}, + {'test_file': 'test_qwen3_vl_4B_fsdp.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_0.6B_fsdp_distributed.py', 'num_gpus': 2}, {'test_file': 'test_quick_start_glm4_9B.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_4B_ppo.py', 'num_gpus': 8}, {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, - {'test_file': 'test_qwen3_4B_fsdp_true_on_policy.py', 'num_gpus': 2}, {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, - {'test_file': 'test_qwen3_vl_4B_fsdp.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_0.6B_parallel_check.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_4B_ckpt.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_4B_ckpt.py --async-save', 'num_gpus': 8}, + {'test_file': 'test_qwen2.5_0.5B_gsm8k.py', 'num_gpus': 2}, + {'test_file': 'test_qwen2.5_0.5B_gsm8k_async.py', 'num_gpus': 2}, ], }, } %> diff --git a/build_conda.sh b/build_conda.sh index b45c2202e..b9787aef4 100644 --- a/build_conda.sh +++ b/build_conda.sh @@ -12,6 +12,8 @@ source ~/.bashrc micromamba create -n miles python=3.12 pip -c conda-forge -y micromamba activate miles export CUDA_HOME="$CONDA_PREFIX" +export SGLANG_COMMIT="24c91001cf99ba642be791e099d358f4dfe955f5" +export MEGATRON_COMMIT="3714d81d418c9f1bca4594fc35f9e8289f652862" export BASE_DIR=${BASE_DIR:-"/root"} cd $BASE_DIR @@ -27,7 +29,7 @@ pip install torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1 --index-url https # install sglang git clone https://github.com/sgl-project/sglang.git cd sglang -git checkout 5e2cda6158e670e64b926a9985d65826c537ac82 +git checkout ${SGLANG_COMMIT} # Install the python packages pip install -e "python[all]" @@ -53,12 +55,9 @@ pip install nvidia-modelopt[torch]>=0.37.0 --no-build-isolation # megatron cd $BASE_DIR git clone https://github.com/NVIDIA/Megatron-LM.git --recursive && \ - cd Megatron-LM/ && git checkout core_v0.14.0 && \ + cd Megatron-LM/ && git checkout ${MEGATRON_COMMIT} && \ pip install -e . -# https://github.com/pytorch/pytorch/issues/168167 -pip install nvidia-cudnn-cu12==9.16.0.29 - # install miles and apply patches # if miles does not exist locally, clone it @@ -73,8 +72,11 @@ else pip install -e . fi +# https://github.com/pytorch/pytorch/issues/168167 +pip install nvidia-cudnn-cu12==9.16.0.29 + # apply patch cd $BASE_DIR/sglang -git apply $MILES_DIR/docker/patch/v0.5.6/sglang.patch +git apply $MILES_DIR/docker/patch/v0.5.7/sglang.patch cd $BASE_DIR/Megatron-LM -git apply $MILES_DIR/docker/patch/v0.5.6/megatron.patch \ No newline at end of file +git apply $MILES_DIR/docker/patch/v0.5.7/megatron.patch \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 156169c72..29929d282 100644 --- a/docker/README.md +++ b/docker/README.md @@ -5,10 +5,12 @@ We will publish 2 kinds of docker images: 2. latest version, which aligns to `lmsysorg/sglang:latest`. current stable version is: -- sglang nightly-dev-20251208-5e2cda61 (5e2cda6158e670e64b926a9985d65826c537ac82), megatron v0.14.0 (23e00ed0963c35382dfe8a5a94fb3cda4d21e133) +- sglang v0.5.7 nightly-dev-20260103-24c91001 (24c91001cf99ba642be791e099d358f4dfe955f5), megatron dev 3714d81d418c9f1bca4594fc35f9e8289f652862 history versions: +- sglang v0.5.6 nightly-dev-20251208-5e2cda61 (5e2cda6158e670e64b926a9985d65826c537ac82), megatron v0.14.0 (23e00ed0963c35382dfe8a5a94fb3cda4d21e133) - sglang v0.5.5.post1 (303cc957e62384044dfa8e52d7d8af8abe12f0ac), megatron v0.14.0 (23e00ed0963c35382dfe8a5a94fb3cda4d21e133) +- sglang v0.5.0rc0-cu126 (8ecf6b9d2480c3f600826c7d8fef6a16ed603c3f), megatron 48406695c4efcf1026a7ed70bb390793918dd97b The command to build: diff --git a/docker/patch/v0.5.7/megatron.patch b/docker/patch/v0.5.7/megatron.patch new file mode 100644 index 000000000..a337b19fb --- /dev/null +++ b/docker/patch/v0.5.7/megatron.patch @@ -0,0 +1,681 @@ +diff --git a/megatron/core/dist_checkpointing/strategies/common.py b/megatron/core/dist_checkpointing/strategies/common.py +index 41c21d93d..ef80f72d6 100644 +--- a/megatron/core/dist_checkpointing/strategies/common.py ++++ b/megatron/core/dist_checkpointing/strategies/common.py +@@ -86,7 +86,7 @@ class TorchCommonLoadStrategy(LoadCommonStrategy): + msc = MultiStorageClientFeature.import_package() + return msc.torch.load(load_path, map_location='cpu') + else: +- return torch.load(load_path, map_location='cpu') ++ return torch.load(load_path, map_location='cpu', weights_only=False) + except FileNotFoundError as e: + err_msg = f'Common file {load_path} does not exist' + if MultiStorageClientFeature.is_enabled(): +diff --git a/megatron/core/dist_checkpointing/strategies/torch.py b/megatron/core/dist_checkpointing/strategies/torch.py +index 5a1ea308d..aa701237f 100644 +--- a/megatron/core/dist_checkpointing/strategies/torch.py ++++ b/megatron/core/dist_checkpointing/strategies/torch.py +@@ -597,10 +597,12 @@ class MCoreLoadPlanner(DefaultLoadPlanner): + def _validate_global_shapes(self, metadata, sharded_tensors): + for sh_ten in sharded_tensors: + if sh_ten.key not in metadata.state_dict_metadata: +- raise KeyError( +- f"{sh_ten.key} from model not in state dict:" +- f" {sorted(metadata.state_dict_metadata.keys())}" +- ) ++ # raise KeyError( ++ # f"{sh_ten.key} from model not in state dict:" ++ # f" {sorted(metadata.state_dict_metadata.keys())}" ++ # ) ++ print(f"{sh_ten.key} from model not in state dict, will skip") ++ continue + loaded_shape = metadata.state_dict_metadata[sh_ten.key].size + expected_shape = self._expected_shape(sh_ten) + if loaded_shape != expected_shape: +@@ -630,7 +632,7 @@ class MCoreLoadPlanner(DefaultLoadPlanner): + tensor_metadata = self.metadata.state_dict_metadata + metadata_with_sizes = [ + (tensor_metadata[key], tensor_metadata[key].size, sharded_tensor) +- for key, sharded_tensor in self.allow_shape_mismatch_sharded_tensors.items() ++ for key, sharded_tensor in self.allow_shape_mismatch_sharded_tensors.items() if key in tensor_metadata + ] + try: + # Temporarily set sizes to expected shapes +@@ -959,6 +961,7 @@ class TorchDistLoadShardedStrategy(LoadShardedStrategy): + planner=MCoreLoadPlanner( + shapes_validation_sharded_tensors=flexible_shape_sharded_tensors, + allow_shape_mismatch_sharded_tensors=allow_shape_mismatch_sharded_tensors, ++ allow_partial_load=True, + ), + ) + +diff --git a/megatron/core/extensions/transformer_engine.py b/megatron/core/extensions/transformer_engine.py +index acb93ef78..20ee977b0 100644 +--- a/megatron/core/extensions/transformer_engine.py ++++ b/megatron/core/extensions/transformer_engine.py +@@ -408,6 +408,7 @@ class TELinear(te.pytorch.Linear): + ) + + for param in self.parameters(): ++ setattr(param, "parallel_mode", parallel_mode) + if is_expert: + # Reduce the gradient on the expert_data_parallel group for expert linear layers + setattr(param, "allreduce", not self.expert_parallel) +diff --git a/megatron/core/fusions/fused_mla_yarn_rope_apply.py b/megatron/core/fusions/fused_mla_yarn_rope_apply.py +index 1fd5dcfae..c9aeef1f0 100644 +--- a/megatron/core/fusions/fused_mla_yarn_rope_apply.py ++++ b/megatron/core/fusions/fused_mla_yarn_rope_apply.py +@@ -385,6 +385,7 @@ def rotary_fwd_kv_kernel( + SIN, + emb_dim: tl.constexpr, + k_dim: tl.constexpr, ++ k_dim_ceil: tl.constexpr, + v_dim: tl.constexpr, + head_num: tl.constexpr, + batch_size, +@@ -434,21 +435,27 @@ def rotary_fwd_kv_kernel( + cos_right = tl.load(COS + token_idx * emb_dim + emb_dim // 2 + tl.arange(0, emb_dim // 2)) + sin_right = tl.load(SIN + token_idx * emb_dim + emb_dim // 2 + tl.arange(0, emb_dim // 2)) + +- KV_ptr = KV + pid_m * stride_kv_seq + pid_head * BLOCK_H * stride_kv_nheads +- kv_off = tl.arange(0, BLOCK_H)[:, None] * stride_kv_nheads +- mask = kv_off < head_num * stride_kv_nheads +- k_in_off = kv_off + tl.arange(0, k_dim)[None, :] +- v_in_off = kv_off + k_dim + tl.arange(0, v_dim)[None, :] +- k = tl.load(KV_ptr + k_in_off, mask=mask) +- v = tl.load(KV_ptr + v_in_off, mask=mask) ++ KV_ptr = KV + pid_m * stride_kv_seq # + pid_head * BLOCK_H * stride_kv_nheads ++ ki_range = tl.arange(0, BLOCK_H)[:, None] + pid_head * BLOCK_H ++ kj_range = tl.arange(0, k_dim_ceil)[None, :] ++ mask_k = (ki_range < head_num) & (kj_range < k_dim) ++ mask_v = ki_range < head_num ++ k_off = ki_range * stride_kv_nheads + kj_range ++ if v_dim > 0: ++ v_off = ki_range * stride_kv_nheads + k_dim + tl.arange(0, v_dim)[None, :] ++ v = tl.load(KV_ptr + v_off, mask=mask_v) ++ else: ++ v = tl.zeros((BLOCK_H, 1), dtype=KV.dtype.element_ty) ++ k = tl.load(KV_ptr + k_off, mask=mask_k) + +- K_ptr = O_KEY + pid_m * stride_k_seq + pid_head * BLOCK_H * stride_k_nheads +- V_ptr = O_VALUE + pid_m * stride_v_seq + pid_head * BLOCK_H * stride_v_nheads ++ K_ptr = O_KEY + pid_m * stride_k_seq # + pid_head * BLOCK_H * stride_k_nheads ++ V_ptr = O_VALUE + pid_m * stride_v_seq # + pid_head * BLOCK_H * stride_v_nheads + +- k_out_off = tl.arange(0, BLOCK_H)[:, None] * stride_k_nheads + tl.arange(0, k_dim)[None, :] +- v_out_off = tl.arange(0, BLOCK_H)[:, None] * stride_v_nheads + tl.arange(0, v_dim)[None, :] +- tl.store(K_ptr + k_out_off, k, mask=mask) +- tl.store(V_ptr + v_out_off, v, mask=mask) ++ k_out_off = ki_range * stride_k_nheads + kj_range ++ tl.store(K_ptr + k_out_off, k, mask=mask_k) ++ if v_dim > 0: ++ v_out_off = ki_range * stride_v_nheads + tl.arange(0, v_dim)[None, :] ++ tl.store(V_ptr + v_out_off, v, mask=mask_v) + + EMB = K_POS_EMB + pid_m * stride_emb_seq + # x1 = t[..., 0::2], x2 = t[..., 1::2] +@@ -460,14 +467,16 @@ def rotary_fwd_kv_kernel( + x_left = x_left.expand_dims(0).broadcast_to(BLOCK_H, emb_dim // 2) + x_right = x_right.expand_dims(0).broadcast_to(BLOCK_H, emb_dim // 2) + ++ x_range = tl.arange(0, BLOCK_H)[:, None] + pid_head * BLOCK_H ++ mask_x = x_range < head_num + x_left_off = ( +- tl.arange(0, BLOCK_H)[:, None] * stride_k_nheads ++ x_range * stride_k_nheads + + k_dim + + tl.arange(0, emb_dim // 2)[None, :] + ) + x_right_off = x_left_off + emb_dim // 2 +- tl.store(K_ptr + x_left_off, x_left, mask=mask) +- tl.store(K_ptr + x_right_off, x_right, mask=mask) ++ tl.store(K_ptr + x_left_off, x_left, mask=mask_x) ++ tl.store(K_ptr + x_right_off, x_right, mask=mask_x) + + + @triton.autotune( +@@ -493,6 +502,7 @@ def rotary_bwd_kv_kernel( + SIN, + emb_dim: tl.constexpr, + k_dim: tl.constexpr, ++ k_dim_ceil: tl.constexpr, + v_dim: tl.constexpr, + head_num: tl.constexpr, + batch_size, +@@ -533,27 +543,32 @@ def rotary_bwd_kv_kernel( + else: + token_idx = _get_thd_token_idx(cu_seqlens_kv, pid_m, seq_num, cp_rank, cp_size) + +- dKV_ptr = dKV + pid_m * stride_dkv_seq + pid_head * BLOCK_H * stride_dkv_nheads +- dkv_off = tl.arange(0, BLOCK_H)[:, None] * stride_dkv_nheads +- mask = dkv_off < head_num * stride_dkv_nheads +- dk_out_off = dkv_off + tl.arange(0, k_dim)[None, :] +- dv_out_off = dkv_off + k_dim + tl.arange(0, v_dim)[None, :] +- +- dK_ptr = dK + pid_m * stride_dk_seq + pid_head * BLOCK_H * stride_dk_nheads +- dV_ptr = dV + pid_m * stride_dv_seq + pid_head * BLOCK_H * stride_dv_nheads +- dk_in_off = tl.arange(0, BLOCK_H)[:, None] * stride_dk_nheads + tl.arange(0, k_dim)[None, :] +- dv_in_off = tl.arange(0, BLOCK_H)[:, None] * stride_dv_nheads + tl.arange(0, v_dim)[None, :] +- dk = tl.load(dK_ptr + dk_in_off, mask=mask) +- dv = tl.load(dV_ptr + dv_in_off, mask=mask) +- tl.store(dKV_ptr + dk_out_off, dk, mask=mask) +- tl.store(dKV_ptr + dv_out_off, dv, mask=mask) ++ dKV_ptr = dKV + pid_m * stride_dkv_seq # + pid_head * BLOCK_H * stride_dkv_nheads ++ ki_range = tl.arange(0, BLOCK_H)[:, None] + pid_head * BLOCK_H ++ kj_range = tl.arange(0, k_dim_ceil)[None, :] ++ mask_k = (ki_range < head_num) & (kj_range < k_dim) ++ mask_v = ki_range < head_num ++ dk_out_off = ki_range * stride_dkv_nheads + kj_range ++ ++ dK_ptr = dK + pid_m * stride_dk_seq # + pid_head * BLOCK_H * stride_dk_nheads ++ dV_ptr = dV + pid_m * stride_dv_seq # + pid_head * BLOCK_H * stride_dv_nheads ++ dk_in_off = ki_range * stride_dk_nheads + kj_range ++ ++ dk = tl.load(dK_ptr + dk_in_off, mask=mask_k) ++ tl.store(dKV_ptr + dk_out_off, dk, mask=mask_k) ++ ++ if v_dim > 0: ++ dv_out_off = ki_range * stride_dkv_nheads + k_dim + tl.arange(0, v_dim)[None, :] ++ dv_in_off = ki_range * stride_dv_nheads + tl.arange(0, v_dim)[None, :] ++ dv = tl.load(dV_ptr + dv_in_off, mask=mask_v) ++ tl.store(dKV_ptr + dv_out_off, dv, mask=mask_v) + + if pid_head == 0: + x_left_accum = tl.zeros((BLOCK_H, emb_dim // 2), dtype=tl.float32) + x_right_accum = tl.zeros((BLOCK_H, emb_dim // 2), dtype=tl.float32) + for i in tl.static_range(triton.cdiv(head_num, BLOCK_H)): +- dK_ptr = dK + pid_m * stride_dk_seq + i * BLOCK_H * stride_dk_nheads +- x_off = tl.arange(0, BLOCK_H)[:, None] * stride_dk_nheads + k_dim ++ dK_ptr = dK + pid_m * stride_dk_seq # + i * BLOCK_H * stride_dk_nheads ++ x_off = tl.arange(0, BLOCK_H)[:, None] * stride_dk_nheads + k_dim + i * BLOCK_H * stride_dk_nheads + mask = x_off < head_num * stride_dk_nheads + x_left_off = x_off + tl.arange(0, emb_dim // 2)[None, :] + x_right_off = x_left_off + emb_dim // 2 +@@ -632,6 +647,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + + o_key = kv.new_empty(total_seqlen, nheads, emb_dim + k_dim) + o_value = kv.new_empty(total_seqlen, nheads, v_dim) ++ k_dim_ceil = triton.next_power_of_2(k_dim) + + grid = lambda META: (total_seqlen, triton.cdiv(nheads, META["BLOCK_H"])) + rotary_fwd_kv_kernel[grid]( +@@ -643,6 +659,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + sin, + emb_dim, + k_dim, ++ k_dim_ceil, + v_dim, + nheads, + batch_size, +@@ -700,6 +717,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + + d_kv = dk.new_empty(total_seqlen, nheads, ctx.k_dim + ctx.v_dim) + d_emb = dk.new_empty(total_seqlen, 1, ctx.emb_dim) ++ k_dim_ceil = triton.next_power_of_2(ctx.k_dim) + + grid = lambda META: (total_seqlen, triton.cdiv(nheads, META["BLOCK_H"])) + rotary_bwd_kv_kernel[grid]( +@@ -711,6 +729,7 @@ class ApplyMLARotaryEmbKV(torch.autograd.Function): + sin, + ctx.emb_dim, + ctx.k_dim, ++ k_dim_ceil, + ctx.v_dim, + nheads, + batch_size, +diff --git a/megatron/core/models/common/language_module/language_module.py b/megatron/core/models/common/language_module/language_module.py +index 13d74aa52..060898a7a 100644 +--- a/megatron/core/models/common/language_module/language_module.py ++++ b/megatron/core/models/common/language_module/language_module.py +@@ -184,7 +184,15 @@ class LanguageModule(MegatronModule): + assert ( + column_parallel_linear is not None + ), "column_parallel_linear cannot be None when not using fused linear cross entropy." +- logits, _ = column_parallel_linear(hidden, **col_linear_kwargs) ++ # output ++ output_layer_params = {k: v.detach() for k, v in column_parallel_linear.named_parameters()} ++ output_layer_buffers = dict(column_parallel_linear.named_buffers()) ++ logits, _ = torch.func.functional_call( ++ column_parallel_linear, ++ {**output_layer_params, **output_layer_buffers}, ++ (hidden,), ++ col_linear_kwargs, ++ ) + + return self.compute_language_model_loss(labels, logits) + +diff --git a/megatron/core/models/gpt/gpt_layer_specs.py b/megatron/core/models/gpt/gpt_layer_specs.py +index e21127b87..712793853 100755 +--- a/megatron/core/models/gpt/gpt_layer_specs.py ++++ b/megatron/core/models/gpt/gpt_layer_specs.py +@@ -188,6 +188,8 @@ def get_gpt_layer_with_transformer_engine_spec( + use_kitchen: bool = False, + use_te_activation_func: bool = False, + fallback_to_eager_attn: bool = False, ++ post_self_attn_layernorm: bool = False, ++ post_mlp_layernorm: bool = False, + ) -> ModuleSpec: + """Use this spec to use lower-level Transformer Engine modules (required for fp8 training). + +@@ -260,6 +262,8 @@ def get_gpt_layer_with_transformer_engine_spec( + mlp=mlp, + sharded_state_dict_keys_map=sharded_state_dict_keys_map, + normalization=normalization, ++ post_self_attn_layernorm=post_self_attn_layernorm, ++ post_mlp_layernorm=post_mlp_layernorm, + ) + + +@@ -349,6 +353,8 @@ def get_transformer_layer_spec_for_backend( + mlp: ModuleSpec, + sharded_state_dict_keys_map: Optional[dict] = None, + normalization: Optional[str] = None, ++ post_self_attn_layernorm: bool = False, ++ post_mlp_layernorm: bool = False, + ) -> ModuleSpec: + """Helper function to get module spec for TransformerLayer""" + +@@ -371,9 +377,11 @@ def get_transformer_layer_spec_for_backend( + input_layernorm=input_layernorm, + self_attention=attention, + self_attn_bda=get_bias_dropout_add, ++ post_self_attn_layernorm=TENorm if post_self_attn_layernorm else IdentityOp, + pre_mlp_layernorm=pre_mlp_layernorm, + mlp=mlp, + mlp_bda=get_bias_dropout_add, ++ post_mlp_layernorm=TENorm if post_mlp_layernorm else IdentityOp, + sharded_state_dict_keys_map=sharded_state_dict_keys_map, + ), + ) +diff --git a/megatron/core/models/gpt/gpt_model.py b/megatron/core/models/gpt/gpt_model.py +index a1230568c..1fd52f65a 100644 +--- a/megatron/core/models/gpt/gpt_model.py ++++ b/megatron/core/models/gpt/gpt_model.py +@@ -446,6 +446,7 @@ class GPTModel(LanguageModule): + *, + inference_params: Optional[BaseInferenceContext] = None, + loss_mask: Optional[Tensor] = None, ++ mtp_kwargs: Optional[dict] = {}, + ) -> Tensor: + """Forward function of the GPT Model This function passes the input tensors + through the embedding layer, and then the decoder and finally into the post +@@ -508,6 +509,7 @@ class GPTModel(LanguageModule): + runtime_gather_output=runtime_gather_output, + extra_block_kwargs=extra_block_kwargs, + inference_context=inference_context, ++ mtp_kwargs=mtp_kwargs, + ) + + def _postprocess( +@@ -529,6 +531,7 @@ class GPTModel(LanguageModule): + runtime_gather_output=None, + extra_block_kwargs=None, + inference_context=None, ++ mtp_kwargs={}, + ): + """Postprocesses decoder hidden states to generate logits or compute loss. + +@@ -543,7 +546,8 @@ class GPTModel(LanguageModule): + output_weight = None + if self.share_embeddings_and_output_weights: + output_weight = self.shared_embedding_or_output_weight() +- if mtp_in_postprocess: ++ ++ if mtp_in_postprocess and mtp_kwargs.get('mtp_labels', None) is not None: + hidden_states = self.mtp( + input_ids=input_ids, + position_ids=position_ids, +@@ -563,13 +567,18 @@ class GPTModel(LanguageModule): + return hidden_states + + # Skip when mtp_num_layers is None or 0 +- if self.config.mtp_num_layers: +- mtp_labels = labels.clone() ++ if self.config.mtp_num_layers and mtp_kwargs.get('mtp_labels', None) is not None: ++ mtp_labels = mtp_kwargs['mtp_labels'].clone() ++ mtp_labels, _ = roll_tensor(mtp_labels, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params) ++ + hidden_states_list = torch.chunk(hidden_states, 1 + self.config.mtp_num_layers, dim=0) + hidden_states = hidden_states_list[0] + if loss_mask is None: + # if loss_mask is not provided, use all ones as loss_mask + loss_mask = torch.ones_like(mtp_labels) ++ else: ++ # Otherwise, roll the loss_mask to keep up with the mtp_labels ++ loss_mask, _ = roll_tensor(loss_mask, shifts=-1, dims=-1, cp_group=self.cp_group, packed_seq_params=packed_seq_params) + for mtp_layer_number in range(self.config.mtp_num_layers): + # Calc loss for the current Multi-Token Prediction (MTP) layers. + mtp_labels, _ = roll_tensor( +@@ -595,7 +604,7 @@ class GPTModel(LanguageModule): + sequence_parallel_enabled=self.output_layer.sequence_parallel, + column_parallel_linear=self.output_layer, + col_linear_kwargs={ +- 'weight': output_weight, ++ 'weight': output_weight.detach() if output_weight else None, + 'runtime_gather_output': runtime_gather_output, + }, + ) +diff --git a/megatron/core/optimizer/distrib_optimizer.py b/megatron/core/optimizer/distrib_optimizer.py +index 6e093f96f..eac21a3ea 100644 +--- a/megatron/core/optimizer/distrib_optimizer.py ++++ b/megatron/core/optimizer/distrib_optimizer.py +@@ -677,6 +677,8 @@ class DistributedOptimizer(MixedPrecisionOptimizer): + # TE FusedAdam will not accumulate step for empty param groups, so we need to + # align the step across param groups. + param_group["step"] = int(step) ++ if "step" in param_group and param_group["step"] is None: ++ del param_group["step"] + + # Grad scaler state. + if self.grad_scaler: +@@ -1646,6 +1648,8 @@ class DistributedOptimizer(MixedPrecisionOptimizer): + if key == 'padding': + tensors[key] = LocalNonpersistentObject(tensors[key]) + continue ++ if key == 'step': ++ continue + assert tensors[key].shape == (gbuf_local_end - gbuf_local_start,), ( + tensors[key].shape, + gbuf_local_start, +diff --git a/megatron/core/parallel_state.py b/megatron/core/parallel_state.py +index a273002b9..4f821cfd5 100644 +--- a/megatron/core/parallel_state.py ++++ b/megatron/core/parallel_state.py +@@ -11,6 +11,7 @@ from typing import Callable, List, Optional + + import numpy as np + import torch ++import torch.distributed as dist + + from .utils import GlobalMemoryBuffer, is_torch_min_version + +diff --git a/megatron/core/pipeline_parallel/p2p_communication.py b/megatron/core/pipeline_parallel/p2p_communication.py +index ac839c21f..f18309217 100644 +--- a/megatron/core/pipeline_parallel/p2p_communication.py ++++ b/megatron/core/pipeline_parallel/p2p_communication.py +@@ -26,22 +26,22 @@ def _batched_p2p_ops( + ops = [] + if tensor_send_prev is not None: + send_prev_op = torch.distributed.P2POp( +- torch.distributed.isend, tensor_send_prev, prev_pipeline_rank, group ++ torch.distributed.isend, tensor_send_prev, prev_pipeline_rank, + ) + ops.append(send_prev_op) + if tensor_recv_prev is not None: + recv_prev_op = torch.distributed.P2POp( +- torch.distributed.irecv, tensor_recv_prev, prev_pipeline_rank, group ++ torch.distributed.irecv, tensor_recv_prev, prev_pipeline_rank, + ) + ops.append(recv_prev_op) + if tensor_send_next is not None: + send_next_op = torch.distributed.P2POp( +- torch.distributed.isend, tensor_send_next, next_pipeline_rank, group ++ torch.distributed.isend, tensor_send_next, next_pipeline_rank, + ) + ops.append(send_next_op) + if tensor_recv_next is not None: + recv_next_op = torch.distributed.P2POp( +- torch.distributed.irecv, tensor_recv_next, next_pipeline_rank, group ++ torch.distributed.irecv, tensor_recv_next, next_pipeline_rank, + ) + ops.append(recv_next_op) + if len(ops) > 0: +diff --git a/megatron/core/transformer/moe/moe_utils.py b/megatron/core/transformer/moe/moe_utils.py +index 28cff06f5..58dc4bb70 100644 +--- a/megatron/core/transformer/moe/moe_utils.py ++++ b/megatron/core/transformer/moe/moe_utils.py +@@ -587,6 +587,9 @@ def topk_routing_with_score_function( + else: + return torch.topk(scores, k=topk, dim=1) + ++ from miles.utils.routing_replay import get_routing_replay_compute_topk ++ compute_topk = get_routing_replay_compute_topk(compute_topk) ++ + if score_function == "softmax": + if use_pre_softmax: + scores = torch.softmax(logits, dim=-1, dtype=torch.float32).type_as(logits) +diff --git a/megatron/core/transformer/moe/router.py b/megatron/core/transformer/moe/router.py +index 16fc9d9af..517944f25 100644 +--- a/megatron/core/transformer/moe/router.py ++++ b/megatron/core/transformer/moe/router.py +@@ -201,6 +201,9 @@ class TopKRouter(Router): + self.global_tokens_per_expert = None + self.ga_steps = None + ++ from miles.utils.routing_replay import register_routing_replay ++ register_routing_replay(self) ++ + def _maintain_float32_expert_bias(self): + """ + Maintain the expert bias in float32. +diff --git a/megatron/core/transformer/multi_token_prediction.py b/megatron/core/transformer/multi_token_prediction.py +index a8f4abfcd..f33f6f05e 100755 +--- a/megatron/core/transformer/multi_token_prediction.py ++++ b/megatron/core/transformer/multi_token_prediction.py +@@ -6,6 +6,7 @@ from typing import Callable, List, Optional, Union + + import torch + from torch import Tensor ++import warnings + + from megatron.core import InferenceParams, parallel_state, tensor_parallel + from megatron.core.dist_checkpointing.mapping import ShardedStateDict +@@ -714,17 +715,19 @@ class MultiTokenPredictionLayer(MegatronModule): + cp_group=self.cp_group, + packed_seq_params=packed_seq_params, + ) +- position_ids, _ = roll_tensor( +- position_ids, +- shifts=-1, +- dims=-1, +- cp_group=self.cp_group, +- packed_seq_params=packed_seq_params, +- ) ++ if position_ids is not None: ++ position_ids, _ = roll_tensor( ++ position_ids, ++ shifts=-1, ++ dims=-1, ++ cp_group=self.cp_group, ++ packed_seq_params=packed_seq_params, ++ ) + # embedding + decoder_input = embedding(input_ids=input_ids, position_ids=position_ids) ++ decoder_input = decoder_input.detach() + +- hidden_states = make_viewless_tensor(inp=hidden_states, requires_grad=True, keep_graph=True) ++ hidden_states = make_viewless_tensor(inp=hidden_states, requires_grad=True, keep_graph=False) + + return input_ids, position_ids, decoder_input, hidden_states + +@@ -826,6 +829,51 @@ class MultiTokenPredictionLayer(MegatronModule): + return hidden_states + + def _checkpointed_forward(self, forward_func, *args, **kwargs): ++ """Wrap `forward_func` with activation checkpointing while only passing tensors. ++ ++ Non-tensor arguments (e.g., configuration objects, None) are captured via closure so ++ that checkpoint implementations never receive them directly, avoiding save_for_backward ++ issues with non-tensor inputs. ++ """ ++ ++ # TODO(jiajun): Is there any better implementation here? ++ positional_specs = [] ++ kw_specs = [] ++ tensor_args: List[torch.Tensor] = [] ++ ++ for arg in args: ++ if torch.is_tensor(arg): ++ positional_specs.append(('tensor', len(tensor_args))) ++ tensor_args.append(arg) ++ else: ++ positional_specs.append(('const', arg)) ++ ++ for key, value in kwargs.items(): ++ if torch.is_tensor(value): ++ kw_specs.append((key, ('tensor', len(tensor_args)))) ++ tensor_args.append(value) ++ else: ++ kw_specs.append((key, ('const', value))) ++ ++ def run(*flat_tensor_args): ++ rebuilt_args = [] ++ for spec_type, payload in positional_specs: ++ if spec_type == 'tensor': ++ rebuilt_args.append(flat_tensor_args[payload]) ++ else: ++ rebuilt_args.append(payload) ++ ++ rebuilt_kwargs = {} ++ for key, (spec_type, payload) in kw_specs: ++ if spec_type == 'tensor': ++ rebuilt_kwargs[key] = flat_tensor_args[payload] ++ else: ++ rebuilt_kwargs[key] = payload ++ ++ return forward_func(*rebuilt_args, **rebuilt_kwargs) ++ ++ tensor_args_tuple = tuple(tensor_args) ++ + def checkpoint_handler(): + """Determines whether to use the `te_checkpoint` or `tensor_parallel.checkpoint`""" + if self.config.fp8: +@@ -836,12 +884,11 @@ class MultiTokenPredictionLayer(MegatronModule): + self.config.distribute_saved_activations, + tensor_parallel.random.get_cuda_rng_tracker, + parallel_state.get_tensor_model_parallel_group(), +- *args, +- **kwargs, ++ *tensor_args_tuple, + ) + else: + return tensor_parallel.checkpoint( +- forward_func, self.config.distribute_saved_activations, *args, *kwargs.values() ++ run, self.config.distribute_saved_activations, *tensor_args_tuple + ) + + if self.config.recompute_method == 'uniform': +diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py +index e2705bd9f..a0aa109b5 100644 +--- a/megatron/core/transformer/transformer_config.py ++++ b/megatron/core/transformer/transformer_config.py +@@ -210,6 +210,9 @@ class TransformerConfig(ModelParallelConfig): + attention_output_gate: bool = False + """Whether to apply output gate to the attention layers.""" + ++ post_self_attn_layernorm: bool = False ++ post_mlp_layernorm: bool = False ++ + test_mode: bool = False + """Whether to run real-time tests.""" + +diff --git a/megatron/core/transformer/transformer_layer.py b/megatron/core/transformer/transformer_layer.py +index 3ea405770..5a42001b9 100644 +--- a/megatron/core/transformer/transformer_layer.py ++++ b/megatron/core/transformer/transformer_layer.py +@@ -223,6 +223,7 @@ class TransformerLayerSubmodules: + input_layernorm: Union[ModuleSpec, type] = IdentityOp + self_attention: Union[ModuleSpec, type] = IdentityOp + self_attn_bda: Union[ModuleSpec, type] = IdentityFuncOp ++ post_self_attn_layernorm: Union[ModuleSpec, type] = IdentityOp + + pre_cross_attn_layernorm: Union[ModuleSpec, type] = IdentityOp + cross_attention: Union[ModuleSpec, type] = IdentityOp +@@ -231,6 +232,7 @@ class TransformerLayerSubmodules: + pre_mlp_layernorm: Union[ModuleSpec, type] = IdentityOp + mlp: Union[ModuleSpec, type] = IdentityOp + mlp_bda: Union[ModuleSpec, type] = IdentityFuncOp ++ post_mlp_layernorm: Union[ModuleSpec, type] = IdentityOp + + # Mapping for sharded tensor keys to be applied in `sharded_state_dict` method + sharded_state_dict_keys_map: Dict[str, str] = field(default_factory=dict) +@@ -310,6 +312,13 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): + # [Module 3: BiasDropoutFusion] + self.self_attn_bda = build_module(submodules.self_attn_bda) + ++ self.post_self_attn_layernorm = build_module( ++ submodules.post_self_attn_layernorm, ++ config=self.config, ++ hidden_size=self.config.hidden_size, ++ eps=self.config.layernorm_epsilon, ++ ) ++ + # [Module 4: Post SelfAttention] Optional Layernorm after self-attn + self.pre_cross_attn_layernorm = build_module( + submodules.pre_cross_attn_layernorm, +@@ -375,6 +384,13 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): + + self.is_moe_layer = isinstance(self.mlp, MoELayer) + ++ self.post_mlp_layernorm = build_module( ++ submodules.post_mlp_layernorm, ++ config=self.config, ++ hidden_size=self.config.hidden_size, ++ eps=self.config.layernorm_epsilon ++ ) ++ + self.recompute_input_layernorm = False + self.recompute_pre_mlp_layernorm = False + self.recompute_mlp = False +@@ -551,6 +567,10 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): + attention_output_with_bias[0] + ) + ++ attention_output, attention_output_bias = attention_output_with_bias ++ attention_output = self.post_self_attn_layernorm(attention_output) ++ attention_output_with_bias = (attention_output, attention_output_bias) ++ + # TODO: could we move `bias_dropout_add_exec_handler` itself + # inside the module provided in the `bias_dropout_add_spec` module? + nvtx_range_push(suffix="self_attn_bda") +@@ -677,6 +697,10 @@ class TransformerLayer(GraphableMegatronModule, BaseTransformerLayer): + else: + mlp_output_with_bias = self.mlp(pre_mlp_layernorm_output) + ++ mlp_output, mlp_output_bias = mlp_output_with_bias ++ mlp_output = self.post_mlp_layernorm(mlp_output) ++ mlp_output_with_bias = (mlp_output, mlp_output_bias) ++ + if self.recompute_pre_mlp_layernorm: + # discard the output of the pre-mlp layernorm and register the recompute + # as a gradient hook of mlp_output_with_bias[0] +diff --git a/megatron/training/arguments.py b/megatron/training/arguments.py +index b267c8a81..83736acdc 100644 +--- a/megatron/training/arguments.py ++++ b/megatron/training/arguments.py +@@ -1398,6 +1398,9 @@ def core_transformer_config_from_args(args, config_class=None): + + kw_args['inference_sampling_seed'] = args.seed + ++ kw_args['post_self_attn_layernorm'] = args.post_self_attn_layernorm ++ kw_args['post_mlp_layernorm'] = args.post_mlp_layernorm ++ + # handle quantization config + # NOTE: Kitchen arguments are only added to the namespace when + # Kitchen library is available. +@@ -1764,6 +1767,12 @@ def _add_network_size_args(parser): + action='store_true', + help='If set, use original BERT residula connection ' + 'ordering.') ++ group.add_argument('--post-self-attn-layernorm', action='store_true', ++ help='If set, use post self attention layernorm.') ++ group.add_argument('--post-mlp-layernorm', action='store_true', ++ help='If set, use post MLP layernorm.') ++ group.add_argument('--use-gated-attention', action='store_true', ++ help='If set, use gated attention as in Qwen3Next') + group.add_argument('--openai-gelu', action='store_true', + help='Use OpenAIs GeLU implementation. This option' + 'should not be used unless for backward compatibility' +diff --git a/megatron/training/tokenizer/tokenizer.py b/megatron/training/tokenizer/tokenizer.py +index 13b7526ca..6c590f653 100644 +--- a/megatron/training/tokenizer/tokenizer.py ++++ b/megatron/training/tokenizer/tokenizer.py +@@ -136,7 +136,7 @@ class _HuggingFaceTokenizer(MegatronLegacyTokenizer): + # TODO(bnorick): download tokenizer once to lustre and use force offline to make sure all tasks read it from there + self._tokenizer = transformers.AutoTokenizer.from_pretrained( + pretrained_model_name_or_path=pretrained_model_name_or_path, +- trust_remote_code=trust_remote_code, ++ trust_remote_code=True, + **kwargs, + ) + self._vocab = self._tokenizer.get_vocab() diff --git a/docker/patch/v0.5.7/sglang.patch b/docker/patch/v0.5.7/sglang.patch new file mode 100644 index 000000000..42d23ed65 --- /dev/null +++ b/docker/patch/v0.5.7/sglang.patch @@ -0,0 +1,864 @@ +diff --git a/python/sglang/srt/disaggregation/decode.py b/python/sglang/srt/disaggregation/decode.py +index 199885244..742ad0639 100644 +--- a/python/sglang/srt/disaggregation/decode.py ++++ b/python/sglang/srt/disaggregation/decode.py +@@ -314,6 +314,13 @@ class DecodePreallocQueue: + ) + return kv_manager + ++ def release_memory_occupation(self): ++ if hasattr(self.kv_manager, "close"): ++ self.kv_manager.close() ++ ++ def resume_memory_occupation(self): ++ self.kv_manager = self._init_kv_manager() ++ + def add(self, req: Req, is_retracted: bool = False) -> None: + """Add a request to the pending queue.""" + if self._check_if_req_exceed_kv_capacity(req): +diff --git a/python/sglang/srt/disaggregation/mooncake/conn.py b/python/sglang/srt/disaggregation/mooncake/conn.py +index 32e8c0b69..df913da7b 100644 +--- a/python/sglang/srt/disaggregation/mooncake/conn.py ++++ b/python/sglang/srt/disaggregation/mooncake/conn.py +@@ -1079,6 +1079,19 @@ class MooncakeKVManager(CommonKVManager): + f"Losing connection with prefill instance (bootstrap_addr: {failed_bootstrap_addr}), {len(affected_rooms)} requests affected" + ) + ++ def close(self): ++ # Batch deregister KV data buffers ++ if self.kv_args.kv_data_ptrs: ++ self.engine.batch_deregister(self.kv_args.kv_data_ptrs) ++ ++ # Batch deregister auxiliary data buffers ++ if self.kv_args.aux_data_ptrs: ++ self.engine.batch_deregister(self.kv_args.aux_data_ptrs) ++ ++ # Batch deregister state/extra pool data buffers ++ if self.kv_args.state_data_ptrs: ++ self.engine.batch_deregister(self.kv_args.state_data_ptrs) ++ + + class MooncakeKVSender(CommonKVSender): + +diff --git a/python/sglang/srt/disaggregation/prefill.py b/python/sglang/srt/disaggregation/prefill.py +index ac11013f8..478e469f6 100644 +--- a/python/sglang/srt/disaggregation/prefill.py ++++ b/python/sglang/srt/disaggregation/prefill.py +@@ -309,6 +309,13 @@ class PrefillBootstrapQueue: + else: + return bootstrapped_reqs, failed_reqs + ++ def release_memory_occupation(self): ++ if hasattr(self.kv_manager, "close"): ++ self.kv_manager.close() ++ ++ def resume_memory_occupation(self): ++ self.kv_manager = self._init_kv_manager() ++ + + class SchedulerDisaggregationPrefillMixin: + """ +diff --git a/python/sglang/srt/distributed/parallel_state.py b/python/sglang/srt/distributed/parallel_state.py +index 0478526ef..cfb1aa669 100644 +--- a/python/sglang/srt/distributed/parallel_state.py ++++ b/python/sglang/srt/distributed/parallel_state.py +@@ -1797,7 +1797,10 @@ def get_tensor_model_parallel_world_size(): + + def get_tensor_model_parallel_rank(): + """Return my rank for the tensor model parallel group.""" +- return get_tp_group().rank_in_group ++ try: ++ return get_tp_group().rank_in_group ++ except Exception: ++ return 0 + + + def get_pipeline_model_parallel_world_size(): +diff --git a/python/sglang/srt/layers/layernorm.py b/python/sglang/srt/layers/layernorm.py +index b07164c53..8e6722ce0 100644 +--- a/python/sglang/srt/layers/layernorm.py ++++ b/python/sglang/srt/layers/layernorm.py +@@ -83,15 +83,12 @@ class RMSNorm(MultiPlatformOp): + eps: float = 1e-6, + var_hidden_size: Optional[int] = None, + cast_x_before_out_mul: bool = False, +- fp32_residual: bool = False, +- weight_dtype: Optional = None, +- override_orig_dtype: Optional = None, ++ fp32_residual: bool = True, + ) -> None: + super().__init__() + self.cast_x_before_out_mul = cast_x_before_out_mul + self.fp32_residual = fp32_residual +- self.override_orig_dtype = override_orig_dtype +- self.weight = nn.Parameter(torch.ones(hidden_size, dtype=weight_dtype)) ++ self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + self.hidden_size = hidden_size + self.variance_size_override = ( +@@ -194,10 +191,22 @@ class RMSNorm(MultiPlatformOp): + ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + if not x.is_contiguous(): + x = x.contiguous() +- orig_dtype = self.override_orig_dtype or x.dtype ++ orig_dtype = x.dtype + post_residual_addition = kwargs.get("post_residual_addition") ++ ++ if residual is not None and not self.fp32_residual: ++ x = ( ++ x ++ + residual ++ + ( ++ post_residual_addition ++ if post_residual_addition is not None ++ else 0.0 ++ ) ++ ) ++ residual = x.clone() + x = x.to(torch.float32) +- if residual is not None: ++ if residual is not None and self.fp32_residual: + x = ( + x + + residual.to(torch.float32) +@@ -207,10 +216,7 @@ class RMSNorm(MultiPlatformOp): + else 0.0 + ) + ) +- if self.fp32_residual: +- residual = x.clone() +- else: +- residual = x.to(orig_dtype) ++ residual = x.to(orig_dtype) + + hidden_size = x.shape[-1] + if hidden_size != self.hidden_size: +diff --git a/python/sglang/srt/layers/logits_processor.py b/python/sglang/srt/layers/logits_processor.py +index fa7431048..cd33ea735 100644 +--- a/python/sglang/srt/layers/logits_processor.py ++++ b/python/sglang/srt/layers/logits_processor.py +@@ -878,11 +878,6 @@ class LogitsProcessor(nn.Module): + None, # bias + True, # is_vnni + ) +- elif get_global_server_args().rl_on_policy_target is not None: +- # Due to tie-weight, we may not be able to change lm_head's weight dtype +- logits = torch.matmul( +- hidden_states.bfloat16(), lm_head.weight.T.bfloat16() +- ) + else: + logits = torch.matmul( + hidden_states.to(lm_head.weight.dtype), lm_head.weight.T +diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py b/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py +index a1885fade..14d692365 100644 +--- a/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py ++++ b/python/sglang/srt/layers/moe/fused_moe_triton/fused_moe.py +@@ -14,6 +14,7 @@ import torch.nn.functional as F + import triton.language as tl + + from sglang.srt.layers.moe.moe_runner import MoeRunnerConfig ++from sglang.srt.server_args import get_global_server_args + from sglang.srt.utils import ( + cpu_has_amx_support, + get_bool_env_var, +@@ -573,7 +574,10 @@ def fused_experts_impl( + ).squeeze(dim=1) + else: + # According to micro benchmark results, torch.compile can get better performance for small token. +- if tokens_in_chunk <= 32: ++ if ( ++ not get_global_server_args().enable_deterministic_inference ++ and tokens_in_chunk <= 32 ++ ): + moe_sum_reduce_torch_compile( + intermediate_cache3.view(*intermediate_cache3.shape), + out_hidden_states[begin_chunk_idx:end_chunk_idx], +diff --git a/python/sglang/srt/layers/moe/routed_experts_capturer.py b/python/sglang/srt/layers/moe/routed_experts_capturer.py +index 00bd68755..5a3ca8a67 100644 +--- a/python/sglang/srt/layers/moe/routed_experts_capturer.py ++++ b/python/sglang/srt/layers/moe/routed_experts_capturer.py +@@ -1,5 +1,6 @@ + import logging + from abc import ABC ++from contextlib import contextmanager + from typing import Optional + + import numpy as np +@@ -8,13 +9,18 @@ import torch + + from sglang.srt.configs.model_config import ModelConfig + from sglang.srt.layers.dp_attention import ( ++ attn_tp_all_gather_into_tensor, + get_attention_dp_rank, ++ get_attention_tp_size, + get_dp_local_info, + is_dp_attention_enabled, + ) + from sglang.srt.mem_cache.memory_pool import ReqToTokenPool + from sglang.srt.model_executor.forward_batch_info import ForwardBatch + from sglang.srt.server_args import get_global_server_args ++from sglang.srt.layers.moe import ( ++ get_moe_a2a_backend, ++) + + logger = logging.getLogger(__name__) + +@@ -181,13 +187,26 @@ class _RoutedExpertsCapturerReal(RoutedExpertsCapturer): + device=device, + ) + ++ if get_moe_a2a_backend().is_deepep(): ++ attn_tp_size = get_attention_tp_size() if is_dp_attention_enabled() else 1 ++ self.gather_buffer = torch.empty( ++ ( ++ self.device_cache.buffer.shape[0] * attn_tp_size, ++ self.device_cache.buffer.shape[2], ++ ), ++ dtype=torch.int32, ++ device=device, ++ ) ++ + def _sync_fwd_experts_buffer_DtoH( + self, + forward_batch: ForwardBatch, + can_run_graph: bool, + cuda_graph_batch: int, + ): +- if is_dp_attention_enabled(): ++ # When DeepEP is enabled, capture() already does all_gather, so device_cache.buffer ++ # contains data from all DP ranks. We should not slice by DP rank in this case. ++ if is_dp_attention_enabled() and not get_moe_a2a_backend().is_deepep(): + local_start_pos, local_num_tokens = get_dp_local_info(forward_batch) + # handle with cuda graph padding + if can_run_graph: +@@ -206,6 +225,12 @@ class _RoutedExpertsCapturerReal(RoutedExpertsCapturer): + ].cpu() + + def capture(self, layer_id: int, topk_ids: torch.Tensor): ++ if get_moe_a2a_backend().is_deepep(): ++ local_topk_ids = topk_ids ++ topk_ids = self.gather_buffer[ ++ : local_topk_ids.size(0) * get_attention_tp_size() ++ ] ++ attn_tp_all_gather_into_tensor(topk_ids, local_topk_ids) + self.device_cache.capture_fwd_routed_experts(layer_id, topk_ids) + + def get_routed_experts( +diff --git a/python/sglang/srt/layers/rotary_embedding.py b/python/sglang/srt/layers/rotary_embedding.py +index 56516b41b..cb2ebca60 100644 +--- a/python/sglang/srt/layers/rotary_embedding.py ++++ b/python/sglang/srt/layers/rotary_embedding.py +@@ -135,9 +135,7 @@ class RotaryEmbedding(MultiPlatformOp): + + if get_global_server_args().rl_on_policy_target is not None: + self._forward_method = self.forward_native +- self._apply_rotary_emb_wrapped = torch.compile(dynamic=True)( +- self._apply_rotary_emb_wrapped +- ) ++ + self.position_cos, self.position_sin = None, None + + def _compute_inv_freq(self, base: Union[int, float]) -> torch.Tensor: +@@ -1577,6 +1575,9 @@ class MRotaryEmbedding(RotaryEmbedding): + key: torch.Tensor, + fused_set_kv_buffer_arg: Optional[FusedSetKVBufferArg] = None, + ) -> Tuple[torch.Tensor, torch.Tensor]: ++ assert ( ++ fused_set_kv_buffer_arg is None ++ ), "fused_set_kv_buffer_arg is not supported for npu implementation" + # TODO: remove this when npu_mrope supports QNumHeads * QHeadSize > 4096 + assert ( + fused_set_kv_buffer_arg is None +diff --git a/python/sglang/srt/layers/sampler.py b/python/sglang/srt/layers/sampler.py +index 55bef5652..35ad68b1c 100644 +--- a/python/sglang/srt/layers/sampler.py ++++ b/python/sglang/srt/layers/sampler.py +@@ -108,16 +108,11 @@ class Sampler(nn.Module): + if return_logprob and SGLANG_RETURN_ORIGINAL_LOGPROB: + probs_without_temp_scaling = torch.softmax(logits, dim=-1) + +- if get_global_server_args().rl_on_policy_target is not None: +- logits_div_temperature = ( +- logits.bfloat16().div(sampling_info.temperatures).bfloat16() +- ) +- logprobs_via_logsoftmax_kernel = torch.log_softmax( +- logits_div_temperature, dim=-1 +- ) +- + # Post process logits + logits.div_(sampling_info.temperatures) ++ if get_global_server_args().rl_on_policy_target is not None: ++ logprobs_via_logsoftmax_kernel = torch.log_softmax(logits, dim=-1) ++ + # For ascend backend, softmax is not needed before sampling + if not get_global_server_args().sampling_backend == "ascend" or ( + return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB +diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py +index 468d8fb8a..229a9a2dc 100644 +--- a/python/sglang/srt/managers/schedule_batch.py ++++ b/python/sglang/srt/managers/schedule_batch.py +@@ -2181,7 +2181,8 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): + def __str__(self): + return ( + f"ScheduleBatch(forward_mode={self.forward_mode.name if self.forward_mode else 'None'}, " +- f"#req={(len(self.reqs))})" ++ f"#req={(len(self.reqs))}), " ++ f"#out_cache_loc={self.out_cache_loc})" + ) + + +diff --git a/python/sglang/srt/managers/scheduler_output_processor_mixin.py b/python/sglang/srt/managers/scheduler_output_processor_mixin.py +index e40586c24..32d98aee4 100644 +--- a/python/sglang/srt/managers/scheduler_output_processor_mixin.py ++++ b/python/sglang/srt/managers/scheduler_output_processor_mixin.py +@@ -10,6 +10,7 @@ from sglang.srt.disaggregation.utils import DisaggregationMode + from sglang.srt.environ import envs + from sglang.srt.layers.logits_processor import LogitsProcessorOutput + from sglang.srt.layers.moe.routed_experts_capturer import get_global_experts_capturer ++ + from sglang.srt.managers.io_struct import ( + AbortReq, + BatchEmbeddingOutput, +diff --git a/python/sglang/srt/managers/scheduler_update_weights_mixin.py b/python/sglang/srt/managers/scheduler_update_weights_mixin.py +index 293a84350..0947f77e0 100644 +--- a/python/sglang/srt/managers/scheduler_update_weights_mixin.py ++++ b/python/sglang/srt/managers/scheduler_update_weights_mixin.py +@@ -1,6 +1,7 @@ + from __future__ import annotations + + import logging ++import os + import traceback + from typing import TYPE_CHECKING, Tuple + +@@ -12,6 +13,9 @@ from sglang.srt.constants import ( + GPU_MEMORY_TYPE_KV_CACHE, + GPU_MEMORY_TYPE_WEIGHTS, + ) ++from sglang.srt.disaggregation.utils import DisaggregationMode ++from sglang.srt.distributed import get_moe_ep_group, get_moe_tp_group, get_tp_group ++from sglang.srt.layers.dp_attention import get_attention_tp_group + from sglang.srt.managers.io_struct import ( + CheckWeightsReqInput, + CheckWeightsReqOutput, +@@ -137,6 +141,13 @@ class SchedulerUpdateWeightsMixin: + self.memory_saver_adapter.pause(GPU_MEMORY_TYPE_KV_CACHE) + self.flush_cache() + ++ if self.disaggregation_mode == DisaggregationMode.DECODE: ++ if hasattr(self, "disagg_decode_prealloc_queue"): ++ self.disagg_decode_prealloc_queue.release_memory_occupation() ++ elif self.disaggregation_mode == DisaggregationMode.PREFILL: ++ if hasattr(self, "disagg_prefill_bootstrap_queue"): ++ self.disagg_prefill_bootstrap_queue.release_memory_occupation() ++ + if GPU_MEMORY_TYPE_WEIGHTS in tags: + self.stashed_model_static_state = _export_static_state( + self.tp_worker.model_runner.model +@@ -177,6 +188,13 @@ class SchedulerUpdateWeightsMixin: + if GPU_MEMORY_TYPE_KV_CACHE in tags: + self.memory_saver_adapter.resume(GPU_MEMORY_TYPE_KV_CACHE) + ++ if self.disaggregation_mode == DisaggregationMode.DECODE: ++ if hasattr(self, "disagg_decode_prealloc_queue"): ++ self.disagg_decode_prealloc_queue.resume_memory_occupation() ++ elif self.disaggregation_mode == DisaggregationMode.PREFILL: ++ if hasattr(self, "disagg_prefill_bootstrap_queue"): ++ self.disagg_prefill_bootstrap_queue.resume_memory_occupation() ++ + return ResumeMemoryOccupationReqOutput() + + def check_weights(self: Scheduler, recv_req: CheckWeightsReqInput): +diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py +index f4fc29e29..5ef12cca6 100644 +--- a/python/sglang/srt/managers/tokenizer_manager.py ++++ b/python/sglang/srt/managers/tokenizer_manager.py +@@ -1652,12 +1652,13 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi + return + + if len(recv_obj.input_token_logprobs_val) > 0: +- state.input_token_logprobs_val.extend( +- recv_obj.input_token_logprobs_val[recv_obj_index] +- ) +- state.input_token_logprobs_idx.extend( +- recv_obj.input_token_logprobs_idx[recv_obj_index] +- ) ++ if recv_obj.input_token_logprobs_val[recv_obj_index]: ++ state.input_token_logprobs_val.extend( ++ recv_obj.input_token_logprobs_val[recv_obj_index] ++ ) ++ state.input_token_logprobs_idx.extend( ++ recv_obj.input_token_logprobs_idx[recv_obj_index] ++ ) + state.output_token_logprobs_val.extend( + recv_obj.output_token_logprobs_val[recv_obj_index] + ) +diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py +index 1d69c0582..9027374be 100644 +--- a/python/sglang/srt/model_executor/model_runner.py ++++ b/python/sglang/srt/model_executor/model_runner.py +@@ -558,7 +558,8 @@ class ModelRunner(ModelRunnerKVCacheMixin): + ) + + # Init routed experts capturer +- self.init_routed_experts_capturer() ++ if not self.is_draft_worker: ++ self.init_routed_experts_capturer() + + if self.device == "cuda": + self.init_cublas() +@@ -2224,11 +2225,12 @@ class ModelRunner(ModelRunnerKVCacheMixin): + output.expert_distribution_metrics = recorder_outputs.get("metrics") + + # Copy cached routing experts' buffers back to CPU cache +- get_global_experts_capturer().on_forward_end( +- forward_batch=forward_batch, +- can_run_graph=output.can_run_graph, +- cuda_graph_batch=getattr(self.graph_runner, "bs", None), +- ) ++ if not self.is_draft_worker: ++ get_global_experts_capturer().on_forward_end( ++ forward_batch=forward_batch, ++ can_run_graph=output.can_run_graph, ++ cuda_graph_batch=getattr(self.graph_runner, "bs", None), ++ ) + + if self.eplb_manager is not None: + self.eplb_manager.on_forward_pass_end() +diff --git a/python/sglang/srt/models/deepseek_v2.py b/python/sglang/srt/models/deepseek_v2.py +index 2918461d3..2bcc67087 100644 +--- a/python/sglang/srt/models/deepseek_v2.py ++++ b/python/sglang/srt/models/deepseek_v2.py +@@ -2704,7 +2704,11 @@ class DeepseekV2AttentionMLA(nn.Module): + ): + k = k_nope.new_empty(*k_shape) + concat_mla_k(k=k, k_nope=k_nope, k_rope=k_pe) +- elif _is_cuda: ++ elif _is_cuda and all( ++ # (i.bit_count() == 1) == (is_power_of_two(i)) ++ i.bit_count() == 1 ++ for i in (k_shape[1], k_nope.shape[-1], k_pe.shape[-1]) ++ ): + # fa3 mha support fp8 inputs + if ( + self.current_attention_backend == "fa3" +diff --git a/python/sglang/srt/models/qwen2.py b/python/sglang/srt/models/qwen2.py +index a7dbadec6..c83a41338 100644 +--- a/python/sglang/srt/models/qwen2.py ++++ b/python/sglang/srt/models/qwen2.py +@@ -90,9 +90,6 @@ class Qwen2MLP(nn.Module): + self.act_fn = SiluAndMul() + + def forward(self, x): +- if get_global_server_args().rl_on_policy_target is not None: +- x = x.bfloat16() +- + gate_up, _ = self.gate_up_proj(x) + x = self.act_fn(gate_up) + x, _ = self.down_proj(x) +@@ -279,11 +276,6 @@ class Qwen2Model(nn.Module): + quant_config=quant_config, + enable_tp=not is_dp_attention_enabled(), + prefix=add_prefix("embed_tokens", prefix), +- params_dtype=( +- torch.float32 +- if get_global_server_args().rl_on_policy_target is not None +- else None +- ), + ) + else: + self.embed_tokens = PPMissingLayer() +@@ -306,10 +298,8 @@ class Qwen2Model(nn.Module): + if self.pp_group.is_last_rank: + norm_kwargs = ( + dict( +- weight_dtype=torch.float32, + cast_x_before_out_mul=True, +- override_orig_dtype=torch.float32, +- fp32_residual=True, ++ fp32_residual=False, + ) + if get_global_server_args().rl_on_policy_target is not None + else {} +diff --git a/python/sglang/srt/models/qwen2_moe.py b/python/sglang/srt/models/qwen2_moe.py +index 3ad9f6736..0b9c7f499 100644 +--- a/python/sglang/srt/models/qwen2_moe.py ++++ b/python/sglang/srt/models/qwen2_moe.py +@@ -586,7 +586,17 @@ class Qwen2MoeModel(nn.Module): + prefix=add_prefix("layers", prefix), + ) + if self.pp_group.is_last_rank: +- self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) ++ norm_kwargs = ( ++ dict( ++ cast_x_before_out_mul=True, ++ fp32_residual=False, ++ ) ++ if get_global_server_args().rl_on_policy_target is not None ++ else {} ++ ) ++ self.norm = RMSNorm( ++ config.hidden_size, eps=config.rms_norm_eps, **norm_kwargs ++ ) + else: + self.norm = PPMissingLayer(return_tuple=True) + +diff --git a/python/sglang/srt/models/qwen3.py b/python/sglang/srt/models/qwen3.py +index 9220831f6..47a1a4e4c 100644 +--- a/python/sglang/srt/models/qwen3.py ++++ b/python/sglang/srt/models/qwen3.py +@@ -90,8 +90,8 @@ class Qwen3Attention(nn.Module): + + norm_kwargs = ( + dict( +- weight_dtype=torch.float32, + cast_x_before_out_mul=True, ++ fp32_residual=False, + ) + if get_global_server_args().rl_on_policy_target is not None + else {} +@@ -242,10 +242,8 @@ class Qwen3DecoderLayer(nn.Module): + + norm_kwargs = ( + dict( +- weight_dtype=torch.float32, + cast_x_before_out_mul=True, +- override_orig_dtype=torch.float32, +- fp32_residual=True, ++ fp32_residual=False, + ) + if get_global_server_args().rl_on_policy_target is not None + else {} +diff --git a/python/sglang/srt/models/qwen3_moe.py b/python/sglang/srt/models/qwen3_moe.py +index e11678a9e..e277d46f2 100644 +--- a/python/sglang/srt/models/qwen3_moe.py ++++ b/python/sglang/srt/models/qwen3_moe.py +@@ -22,6 +22,7 @@ import math + from typing import Any, Dict, Iterable, List, Optional, Tuple, TypeVar + + import torch ++import torch.nn.functional as F + from torch import nn + from transformers import PretrainedConfig + +@@ -50,7 +51,7 @@ from sglang.srt.layers.moe import ( + ) + from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class + from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE +-from sglang.srt.layers.moe.topk import TopK ++from sglang.srt.layers.moe.topk import StandardTopKOutput, TopK + from sglang.srt.layers.moe.utils import RoutingMethodType + from sglang.srt.layers.quantization.base_config import QuantizationConfig + from sglang.srt.layers.radix_attention import RadixAttention +@@ -229,6 +230,7 @@ class Qwen3MoeSparseMoeBlock(nn.Module): + use_grouped_topk=False, + layer_id=layer_id, + ) ++ self.top_k = config.num_experts_per_tok + + self.experts = get_moe_impl_class(quant_config)( + num_experts=config.num_experts +@@ -294,7 +296,22 @@ class Qwen3MoeSparseMoeBlock(nn.Module): + + # router_logits: (num_tokens, n_experts) + router_logits, _ = self.gate(hidden_states) +- topk_output = self.topk(hidden_states, router_logits) ++ ++ if get_global_server_args().rl_on_policy_target is not None: ++ routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float) ++ routing_weights, selected_experts = torch.topk( ++ routing_weights, self.top_k, dim=-1 ++ ) ++ routing_weights /= routing_weights.sum(dim=-1, keepdim=True) ++ routing_weights = routing_weights.to(hidden_states.dtype) ++ topk_output = StandardTopKOutput( ++ topk_weights=routing_weights, ++ topk_ids=selected_experts, ++ router_logits=router_logits, ++ ) ++ else: ++ topk_output = self.topk(hidden_states, router_logits) ++ + final_hidden_states = self.experts(hidden_states, topk_output) + if ( + self.tp_size > 1 +@@ -475,13 +492,14 @@ class Qwen3MoeAttention(nn.Module): + ) + self.compatible_with_fused_kv_buffer = ( + False if isinstance(self.rotary_emb, MRotaryEmbedding) else True +- ) ++ ) and (get_global_server_args().rl_on_policy_target is None) + self.compatible_with_fused_qk_norm_rope = ( + not isinstance(self.rotary_emb, MRotaryEmbedding) + ) and self.head_dim in (64, 128, 256) + self.use_fused_qk_norm_rope = ( + get_global_server_args().enable_fused_qk_norm_rope + and self.compatible_with_fused_qk_norm_rope ++ and (get_global_server_args().rl_on_policy_target is None) + ) + self._used_fused_qk_norm_rope_last_call = False + +@@ -494,8 +512,16 @@ class Qwen3MoeAttention(nn.Module): + prefix=add_prefix("attn", prefix), + ) + +- self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps) +- self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps) ++ norm_kwargs = ( ++ dict( ++ cast_x_before_out_mul=True, ++ fp32_residual=False, ++ ) ++ if get_global_server_args().rl_on_policy_target is not None ++ else {} ++ ) ++ self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps, **norm_kwargs) ++ self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps, **norm_kwargs) + self.alt_stream = alt_stream + + def op_prepare(self, state): +@@ -736,9 +762,19 @@ class Qwen3MoeDecoderLayer(nn.Module): + quant_config=quant_config, + prefix=add_prefix("mlp", prefix), + ) +- self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) ++ norm_kwargs = ( ++ dict( ++ cast_x_before_out_mul=True, ++ fp32_residual=False, ++ ) ++ if get_global_server_args().rl_on_policy_target is not None ++ else {} ++ ) ++ self.input_layernorm = RMSNorm( ++ config.hidden_size, eps=config.rms_norm_eps, **norm_kwargs ++ ) + self.post_attention_layernorm = RMSNorm( +- config.hidden_size, eps=config.rms_norm_eps ++ config.hidden_size, eps=config.rms_norm_eps, **norm_kwargs + ) + + self.layer_communicator = LayerCommunicator( +diff --git a/python/sglang/srt/models/qwen3_vl.py b/python/sglang/srt/models/qwen3_vl.py +index 891913078..c9dbecd23 100644 +--- a/python/sglang/srt/models/qwen3_vl.py ++++ b/python/sglang/srt/models/qwen3_vl.py +@@ -397,28 +397,68 @@ class Qwen3VLMoeVisionModel(nn.Module, RotaryPosMixin): + return cos_combined, sin_combined + + def fast_pos_embed_interpolate(self, grid_thw): +- patch_pos_embeds_permute = [] +- m_size = self.spatial_merge_size ++ grid_ts, grid_hs, grid_ws = grid_thw[:, 0], grid_thw[:, 1], grid_thw[:, 2] ++ num_grid_per_side = int(self.num_position_embeddings**0.5) ++ device = self.pos_embed.weight.device ++ ++ idx_list = [[] for _ in range(4)] ++ weight_list = [[] for _ in range(4)] ++ ++ for t, h, w in zip(grid_ts, grid_hs, grid_ws): ++ h_idxs = torch.linspace(0, num_grid_per_side - 1, h) ++ w_idxs = torch.linspace(0, num_grid_per_side - 1, w) ++ ++ h_idxs_floor = h_idxs.int() ++ w_idxs_floor = w_idxs.int() ++ h_idxs_ceil = (h_idxs.int() + 1).clip(max=num_grid_per_side - 1) ++ w_idxs_ceil = (w_idxs.int() + 1).clip(max=num_grid_per_side - 1) ++ ++ dh = h_idxs - h_idxs_floor ++ dw = w_idxs - w_idxs_floor ++ ++ base_h = h_idxs_floor * num_grid_per_side ++ base_h_ceil = h_idxs_ceil * num_grid_per_side ++ ++ indices = [ ++ (base_h[None].T + w_idxs_floor[None]).flatten(), ++ (base_h[None].T + w_idxs_ceil[None]).flatten(), ++ (base_h_ceil[None].T + w_idxs_floor[None]).flatten(), ++ (base_h_ceil[None].T + w_idxs_ceil[None]).flatten(), ++ ] ++ ++ weights = [ ++ ((1 - dh)[None].T * (1 - dw)[None]).flatten(), ++ ((1 - dh)[None].T * dw[None]).flatten(), ++ (dh[None].T * (1 - dw)[None]).flatten(), ++ (dh[None].T * dw[None]).flatten(), ++ ] + +- embeds = torch.arange(self.num_grid, device=self.pos_embed.weight.device) +- embeds = ( +- self.pos_embed(embeds) +- .permute(1, 0) +- .reshape(1, -1, self.num_grid_per_side, self.num_grid_per_side) ++ for i in range(4): ++ idx_list[i].extend(indices[i].tolist()) ++ weight_list[i].extend(weights[i].tolist()) ++ ++ idx_tensor = torch.tensor(idx_list, dtype=torch.long, device=device) ++ weight_tensor = torch.tensor( ++ weight_list, dtype=self.pos_embed.weight.dtype, device=device + ) +- for t, h, w in grid_thw: +- pos_embed = torch.nn.functional.interpolate( +- embeds, size=(h, w), mode="bilinear", align_corners=self.align_corners +- ) +- pos_embed = pos_embed.reshape( +- -1, +- h // self.spatial_merge_size, +- self.spatial_merge_size, +- w // self.spatial_merge_size, +- self.spatial_merge_size, ++ pos_embeds = self.pos_embed(idx_tensor).to(device) * weight_tensor[:, :, None] ++ patch_pos_embeds = pos_embeds[0] + pos_embeds[1] + pos_embeds[2] + pos_embeds[3] ++ ++ patch_pos_embeds = patch_pos_embeds.split( ++ [h * w for h, w in zip(grid_hs, grid_ws)] ++ ) ++ ++ patch_pos_embeds_permute = [] ++ merge_size = self.spatial_merge_size ++ for pos_embed, t, h, w in zip(patch_pos_embeds, grid_ts, grid_hs, grid_ws): ++ pos_embed = pos_embed.repeat(t, 1) ++ pos_embed = ( ++ pos_embed.view( ++ t, h // merge_size, merge_size, w // merge_size, merge_size, -1 ++ ) ++ .permute(0, 1, 3, 2, 4, 5) ++ .flatten(0, 4) + ) +- pos_embed = pos_embed.permute(1, 3, 2, 4, 0) +- pos_embed = pos_embed.flatten(0, 3).repeat(t, 1) + patch_pos_embeds_permute.append(pos_embed) + return torch.cat(patch_pos_embeds_permute) + +@@ -607,14 +647,19 @@ class Qwen3LLMModel(Qwen3Model): + hidden_states + residual if residual is not None else hidden_states + ) + ++ deepstack_embeds = None ++ if input_deepstack_embeds is not None: ++ prev_layer_idx = layer_idx - 1 ++ if prev_layer_idx in self.deepstack_embed_to_decoder_layer: ++ sep = self.hidden_size * prev_layer_idx ++ deepstack_embeds = input_deepstack_embeds[ ++ :, sep : sep + self.hidden_size ++ ] ++ + # SGLang applies residual at the START of the next layer, not at the END like HuggingFace. + # See: https://github.com/huggingface/transformers/blob/v5.0.0rc0/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L549 + # To match HF behavior, deepstack must be added AFTER residual: (hidden_states + residual) + deepstack + # The order matters because addition with different tensors is not associative in practice. +- # Deepstack for prev_layer is applied at the start of current layer via post_residual_addition. +- deepstack_embeds = self.get_deepstack_embeds( +- layer_idx - 1, input_deepstack_embeds +- ) + hidden_states, residual = layer( + positions, + hidden_states, +diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py +index 54d4e415a..de7620c20 100644 +--- a/python/sglang/srt/server_args.py ++++ b/python/sglang/srt/server_args.py +@@ -523,6 +523,7 @@ class ServerArgs: + cuda_graph_max_bs: Optional[int] = None + cuda_graph_bs: Optional[List[int]] = None + disable_cuda_graph: bool = False ++ disable_draft_cuda_graph: bool = False + disable_cuda_graph_padding: bool = False + enable_profile_cuda_graph: bool = False + enable_cudagraph_gc: bool = False +@@ -3951,6 +3952,11 @@ class ServerArgs: + action="store_true", + help="Disable cuda graph.", + ) ++ parser.add_argument( ++ "--disable-draft-cuda-graph", ++ action="store_true", ++ help="Disable cuda graph for draft model in speculative decoding.", ++ ) + parser.add_argument( + "--disable-cuda-graph-padding", + action="store_true", +diff --git a/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py b/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +index 5fe45086c..c95fbd0f6 100644 +--- a/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py ++++ b/python/sglang/srt/speculative/eagle_draft_cuda_graph_runner.py +@@ -341,7 +341,10 @@ class EAGLEDraftCudaGraphRunner: + self.seq_lens.fill_(self.seq_len_fill_value) + self.out_cache_loc.zero_() + self.positions.zero_() +- ++ self.topk_p.zero_() ++ self.topk_index.zero_() ++ self.hidden_states.zero_() ++ self.req_pool_indices.zero_() + num_tokens = bs * self.num_tokens_per_bs + + # Common inputs +@@ -350,8 +353,8 @@ class EAGLEDraftCudaGraphRunner: + forward_batch.out_cache_loc + ) + self.positions[:raw_num_token].copy_(forward_batch.positions) +- self.topk_p[:raw_bs].copy_(forward_batch.spec_info.topk_p) +- self.topk_index[:raw_bs].copy_(forward_batch.spec_info.topk_index) ++ self.topk_p[:raw_bs].copy_(forward_batch.spec_info.topk_p.clamp(0, 1)) ++ self.topk_index[:raw_bs].copy_(forward_batch.spec_info.topk_index.clamp(0, self.model_runner.model_config.vocab_size - 1)) + self.hidden_states[:raw_bs].copy_(forward_batch.spec_info.hidden_states) + self.req_pool_indices[:raw_bs].copy_(forward_batch.req_pool_indices) + +diff --git a/python/sglang/srt/speculative/eagle_info.py b/python/sglang/srt/speculative/eagle_info.py +index 1bf3816e9..b5b41dba4 100644 +--- a/python/sglang/srt/speculative/eagle_info.py ++++ b/python/sglang/srt/speculative/eagle_info.py +@@ -778,6 +778,10 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): + self.topk_index = self.topk_index[: len(new_indices)] + self.hidden_states = self.hidden_states[: len(new_indices)] + self.verified_id = self.verified_id[: len(new_indices)] ++ if self.accept_length is not None: ++ self.accept_length = self.accept_length[: len(new_indices)] ++ if self.accept_length_cpu is not None: ++ self.accept_length_cpu = self.accept_length_cpu[: len(new_indices)] + else: + # in some cases(e.g draft_extend), we have not filtered the batch by `unfinished_index` + self.topk_p = self.topk_p[new_indices] +@@ -809,6 +813,27 @@ class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): + self.verified_id = torch.cat([self.verified_id, spec_info.verified_id], axis=0) + self.topk_p = torch.cat([self.topk_p, spec_info.topk_p]) + self.topk_index = torch.cat([self.topk_index, spec_info.topk_index]) ++ if self.accept_length is not None and spec_info.accept_length is not None: ++ self.accept_length = torch.cat( ++ [self.accept_length, spec_info.accept_length] ++ ) ++ self.accept_length_cpu = self.accept_length.tolist() ++ elif self.accept_length is not None: ++ zeros = torch.zeros( ++ [spec_info.verified_id.shape[0]], ++ dtype=self.accept_length.dtype, ++ device=self.accept_length.device, ++ ) ++ self.accept_length = torch.cat([self.accept_length, zeros]) ++ self.accept_length_cpu = self.accept_length.tolist() ++ elif spec_info.accept_length is not None: ++ zeros = torch.zeros( ++ [self.verified_id.shape[0]], ++ dtype=self.accept_length.dtype, ++ device=self.accept_length.device, ++ ) ++ self.accept_length = torch.cat([zeros, spec_info.accept_length]) ++ self.accept_length_cpu = self.accept_length.tolist() + + + @dataclass +diff --git a/python/sglang/srt/speculative/eagle_worker.py b/python/sglang/srt/speculative/eagle_worker.py +index a702df4f8..61d9ae366 100644 +--- a/python/sglang/srt/speculative/eagle_worker.py ++++ b/python/sglang/srt/speculative/eagle_worker.py +@@ -231,7 +231,7 @@ class EAGLEWorker(TpModelWorker): + self.cuda_graph_runner = None + self.cuda_graph_runner_for_draft_extend = None + +- if self.server_args.disable_cuda_graph: ++ if self.server_args.disable_cuda_graph or self.server_args.disable_draft_cuda_graph: + return + + Device2DraftCudaGraphRunner = { diff --git a/docker/version.txt b/docker/version.txt index 4cc406789..7ebac80c9 100644 --- a/docker/version.txt +++ b/docker/version.txt @@ -1 +1 @@ -nightly-dev-20260105a \ No newline at end of file +nightly-dev-20260106a \ No newline at end of file diff --git a/miles/backends/fsdp_utils/actor.py b/miles/backends/fsdp_utils/actor.py index 2cd96723a..f22a95546 100644 --- a/miles/backends/fsdp_utils/actor.py +++ b/miles/backends/fsdp_utils/actor.py @@ -818,6 +818,8 @@ def update_weights(self) -> None: # type: ignore[override] if num_new_engines > 0: self.weight_updater.connect_rollout_engines(rollout_engines, rollout_engine_lock) dist.barrier(group=get_gloo_group()) + if dist.get_rank() == 0: + ray.get(self.rollout_manager.clear_num_new_engines.remote()) self.weight_updater.update_weights() diff --git a/miles/backends/fsdp_utils/checkpoint.py b/miles/backends/fsdp_utils/checkpoint.py index 3c49a10f8..6daf7f982 100644 --- a/miles/backends/fsdp_utils/checkpoint.py +++ b/miles/backends/fsdp_utils/checkpoint.py @@ -214,14 +214,15 @@ def save(actor: Any, iteration: int) -> None: state_dict = {"model_state": model_state} dcp.save(state_dict, checkpoint_id=str(model_dir)) - # Save optimizer state - if hasattr(actor, "optimizer") and actor.optimizer is not None: + # Save optimizer state (skip if --no-save-optim is set) + save_optimizer_state = not getattr(actor.args, "no_save_optim", False) + if save_optimizer_state and hasattr(actor, "optimizer") and actor.optimizer is not None: optimizer_state = OptimizerState(actor.model, actor.optimizer) optim_state_dict = {"optim_state": optimizer_state} dcp.save(optim_state_dict, checkpoint_id=str(optimizer_dir)) - # Save LR scheduler state - if hasattr(actor, "lr_scheduler") and actor.lr_scheduler is not None: + # Save LR scheduler state (skip if --no-save-optim is set) + if save_optimizer_state and hasattr(actor, "lr_scheduler") and actor.lr_scheduler is not None: lr_scheduler_state = LRSchedulerState(actor.lr_scheduler) lr_scheduler_state_dict = {"lr_scheduler_state": lr_scheduler_state} dcp.save(lr_scheduler_state_dict, checkpoint_id=str(lr_scheduler_dir)) diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index 7cc7f2619..1e6af26b7 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -509,15 +509,23 @@ def update_weights(self) -> None: if self.args.debug_train_only or self.args.debug_rollout_only: return - if self.args.offload_train: - reload_process_groups() + if self.args.use_fault_tolerance: + if dist.get_rank() == 0: + ray.get(self.rollout_manager.recover_rollout_engines.remote()) + dist.barrier(group=get_gloo_group()) rollout_engines, rollout_engine_lock, num_new_engines = ray.get( self.rollout_manager.get_rollout_engines_and_lock.remote() ) + + if self.args.offload_train: + reload_process_groups() + if num_new_engines > 0: self.weight_updater.connect_rollout_engines(rollout_engines, rollout_engine_lock) dist.barrier(group=get_gloo_group()) + if dist.get_rank() == 0: + ray.get(self.rollout_manager.clear_num_new_engines.remote()) with torch_memory_saver.disable() if self.args.offload_train else nullcontext(): print_memory("before update_weights") diff --git a/miles/backends/megatron_utils/arguments.py b/miles/backends/megatron_utils/arguments.py index aea72ceb8..0eb2bcd44 100644 --- a/miles/backends/megatron_utils/arguments.py +++ b/miles/backends/megatron_utils/arguments.py @@ -16,7 +16,7 @@ def set_default_megatron_args(args): # placeholders args.seq_length = 4096 args.max_position_embeddings = args.seq_length - # megatron(dev) optimizer-cpu-offload save ckpt bugs + # TODO: revisit this when megatron(dev) have solved the optimizer-cpu-offload ckpt saving bug args.dist_ckpt_save_pre_mcore_014 = True # compatible for megatron if hasattr(args, "rope_type") and args.rope_type is None: diff --git a/miles/backends/megatron_utils/checkpoint.py b/miles/backends/megatron_utils/checkpoint.py index 35d712910..87495b0d0 100644 --- a/miles/backends/megatron_utils/checkpoint.py +++ b/miles/backends/megatron_utils/checkpoint.py @@ -10,6 +10,85 @@ from miles.utils import megatron_bridge_utils +try: + # Here we patch out the `validate_non_overlapping_shards_metadata` in both functions + # because it is really slow for large models with many shards. + # TODO: find a less hacky way to do this. + import torch.distributed as dist + import torch.distributed._shard.sharding_spec as shard_spec + from torch.distributed._shard.sharded_tensor import ShardedTensor + from torch.distributed._shard.sharded_tensor.metadata import ShardedTensorMetadata + from torch.distributed._shard.sharded_tensor.shard import Shard + from torch.distributed._shard.sharded_tensor.utils import _parse_and_validate_remote_device + from torch.distributed._shard.sharding_spec.api import EnumerableShardingSpec + + def __post_init__(self): + pass + + EnumerableShardingSpec.__post_init__ = __post_init__ + + @classmethod + def _init_from_local_shards_and_global_metadata( # type: ignore[override] + cls, + local_shards: list[Shard], + sharded_tensor_metadata: ShardedTensorMetadata, + process_group=None, + init_rrefs=False, + sharding_spec=None, + ) -> ShardedTensor: + """ + Initialize a ShardedTensor with local shards and a global + ShardedTensorMetadata built on each rank. + + Warning: This API is experimental and subject to change. It does + not do cross rank validations, and fully rely on the user + for the correctness of sharded_tensor_metadata on each rank + """ + process_group = cls._normalize_pg(process_group) + current_rank = dist.get_rank() # intentional to get global rank + + shards_metadata = sharded_tensor_metadata.shards_metadata + + local_shard_metadatas = [] + + # collect local shard metadatas from the global sharded_tensor_metadata + for shard_metadata in shards_metadata: # type: ignore[attr-defined] + rank, local_device = _parse_and_validate_remote_device(process_group, shard_metadata.placement) + + if current_rank == rank: + local_shard_metadatas.append(shard_metadata) + + shards_metadata = sharded_tensor_metadata.shards_metadata + tensor_properties = sharded_tensor_metadata.tensor_properties + + if sharding_spec is None: + spec = shard_spec._infer_sharding_spec_from_shards_metadata(shards_metadata) + else: + spec = sharding_spec + + sharded_tensor = ShardedTensor.__new__( + ShardedTensor, + spec, + sharded_tensor_metadata.size, + dtype=tensor_properties.dtype, + layout=tensor_properties.layout, + pin_memory=tensor_properties.pin_memory, + requires_grad=tensor_properties.requires_grad, + ) + + # done validation, add local_shards + sharded_tensor._local_shards = local_shards + sharded_tensor._prepare_init(process_group=process_group, init_rrefs=init_rrefs) + + # run post initialization, i.e. map registration, rpc initialization + sharded_tensor._post_init() + return sharded_tensor + + ShardedTensor._init_from_local_shards_and_global_metadata = _init_from_local_shards_and_global_metadata + +except ImportError: + pass + logger = logging.getLogger(__name__) __all__ = ["save_checkpoint"] diff --git a/miles/backends/sglang_utils/sglang_engine.py b/miles/backends/sglang_utils/sglang_engine.py index 9bb9b1287..179306023 100644 --- a/miles/backends/sglang_utils/sglang_engine.py +++ b/miles/backends/sglang_utils/sglang_engine.py @@ -435,6 +435,17 @@ def stop_profile(self): response.raise_for_status() return response + def simulate_crash(self): + if self.args.rollout_external or not getattr(self, "process", None): + logger.info( + "simulate_crash called but no local engine process exists (rollout_external=%s); skip kill", + self.args.rollout_external, + ) + return + + logger.info(f"Simulating crash on engine {self.server_host}:{self.server_port}...") + self.shutdown() + def _compute_server_args( args, diff --git a/miles/ray/rollout.py b/miles/ray/rollout.py index 4b22c5ddc..79c6649be 100644 --- a/miles/ray/rollout.py +++ b/miles/ray/rollout.py @@ -10,6 +10,7 @@ import ray import torch from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy +from sglang.srt.constants import GPU_MEMORY_TYPE_CUDA_GRAPH, GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_WEIGHTS from miles.backends.sglang_utils.sglang_engine import SGLangEngine from miles.rollout.base_types import call_rollout_fn @@ -74,14 +75,41 @@ def __init__(self, args, pg): self.num_new_engines = init_rollout_engines(args, pg, self.all_rollout_engines) self.nodes_per_engine = max(1, args.rollout_num_gpus_per_engine // args.num_gpus_per_node) self.rollout_engine_lock = Lock.options(num_cpus=1, num_gpus=0).remote() + self.rollout_id = -1 self._metric_checker = MetricChecker.maybe_create(args) + self._health_monitor = None if self.args.use_fault_tolerance: self._health_monitor = RolloutHealthMonitor(self, args) + self._health_monitor.start() # Start the monitor thread (in paused state) + self._ci_fault_injection_pending = self.args.ci_test # Flag for CI fault injection + + def _try_ci_fault_injection(self): + """Try to inject fault during generate (when health monitor is running).""" + if not self._ci_fault_injection_pending: + return + + # Only inject fault once + self._ci_fault_injection_pending = False + + if self.all_rollout_engines and self.all_rollout_engines[0]: + logger.info("CI Fault Injection: Simulating crash on engine 0 during generate") + try: + # This will cause the ray actor to exit + self.all_rollout_engines[0].simulate_crash.remote() + # Wait for health monitor to detect the crash and mark engine as None + # health_check_interval + health_check_timeout + buffer + wait_time = self.args.rollout_health_check_interval + self.args.rollout_health_check_timeout + 5 + logger.info(f"CI Fault Injection: Waiting {wait_time}s for health monitor to detect crash") + time.sleep(wait_time) + except Exception as e: + logger.warning(f"CI Fault Injection failed: {e}") def dispose(self): if self._metric_checker is not None: self._metric_checker.dispose() + if self._health_monitor is not None: + self._health_monitor.stop() # TODO maybe rename "rollout_engines" and "all_rollout_engines" later @property @@ -97,25 +125,22 @@ def get_num_rollout_per_epoch(self): return len(self.data_source.dataset) // self.args.rollout_batch_size def generate(self, rollout_id): - monitor_started = self.args.use_fault_tolerance and self._health_monitor.start() start_time = time.time() - try: - data, metrics = self._get_rollout_data(rollout_id=rollout_id) - self._save_debug_rollout_data(data, rollout_id=rollout_id, evaluation=False) - _log_rollout_data(rollout_id, self.args, data, metrics, time.time() - start_time) - data = self._convert_samples_to_train_data(data) - return self._split_train_data_by_dp(data, self.train_parallel_config["dp_size"]) - finally: - if monitor_started: - self._health_monitor.stop() - self.num_new_engines = init_rollout_engines(self.args, self.pg, self.all_rollout_engines) - else: - self.num_new_engines = 0 + self.rollout_id = rollout_id + self.health_monitoring_resume() + if self.args.ci_test and self.args.use_fault_tolerance and rollout_id >= 2: + self._try_ci_fault_injection() + data, metrics = self._get_rollout_data(rollout_id=rollout_id) + self._save_debug_rollout_data(data, rollout_id=rollout_id, evaluation=False) + _log_rollout_data(rollout_id, self.args, data, metrics, time.time() - start_time) + data = self._convert_samples_to_train_data(data) + return self._split_train_data_by_dp(data, self.train_parallel_config["dp_size"]) def eval(self, rollout_id): if self.args.debug_train_only: # if debug train only, we don't generate evaluation data return + self.health_monitoring_resume() result = call_rollout_fn(self.eval_generate_rollout, self.args, rollout_id, self.data_source, evaluation=True) data = result.data @@ -131,10 +156,54 @@ def load(self, rollout_id=None): self.data_source.load(rollout_id) def offload(self): - return ray.get([engine.release_memory_occupation.remote() for engine in self.rollout_engines]) + self.health_monitoring_pause() + return ray.get( + [engine.release_memory_occupation.remote() for engine in self.rollout_engines if engine is not None] + ) + + def onload(self, tags: list[str] | None = None): + return ray.get( + [ + engine.resume_memory_occupation.remote(tags=tags) + for engine in self.rollout_engines + if engine is not None + ] + ) + + def onload_weights(self): + self.onload(tags=[GPU_MEMORY_TYPE_WEIGHTS]) + + def onload_kv(self): + self.onload(tags=[GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_CUDA_GRAPH]) + + def recover_rollout_engines(self): + """Restart any dead rollout engines and update num_new_engines for update_weights detection.""" + self.health_monitoring_pause() + if self.rollout_id == -1: + return self.rollout_engines, self.rollout_engine_lock, self.num_new_engines + + dead_indices = [i for i, engine in enumerate(self.all_rollout_engines) if engine is None] + self.num_new_engines = init_rollout_engines(self.args, self.pg, self.all_rollout_engines) + logger.info(f"Recovered {self.num_new_engines} dead rollout engines") + assert self.num_new_engines == len(dead_indices), "num_new_engines does not match dead_indices length" + if self.args.offload_rollout and dead_indices: + new_engines = [self.all_rollout_engines[i] for i in dead_indices] + ray.get([engine.release_memory_occupation.remote() for engine in new_engines]) + ray.get([engine.resume_memory_occupation.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS]) for engine in new_engines]) + + return self.rollout_engines, self.rollout_engine_lock, self.num_new_engines + + def clear_num_new_engines(self): + # when fault tolerance is not enabled, we need to manually clear num_new_engines after update_weights + self.num_new_engines = 0 + + def health_monitoring_pause(self) -> None: + if self._health_monitor is not None: + self._health_monitor.pause() - def onload(self, tags: list[str] = None): - return ray.get([engine.resume_memory_occupation.remote(tags=tags) for engine in self.rollout_engines]) + def health_monitoring_resume(self) -> None: + if self._health_monitor is not None: + self._health_monitor.resume() def check_weights(self, action: str): return ray.get([engine.check_weights.remote(action=action) for engine in self.rollout_engines]) @@ -171,7 +240,7 @@ def _get_rollout_data(self, rollout_id): global_batch_size = self._dynamic_global_batch_size if len(data) % global_batch_size != 0: - trim_len = (len(data) // self.args.global_batch_size) * global_batch_size + trim_len = (len(data) // global_batch_size) * global_batch_size if trim_len == 0: raise ValueError(f"Not enough samples {len(data)} for global_batch_size {global_batch_size}") origin_data_length = len(data) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 595542c58..51b3d970b 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -529,7 +529,7 @@ def add_data_arguments(parser): parser.add_argument( "--tool-key", type=str, - default=None, + default="tools", help=( "When need to add tools during apply_chat_template, you should provide the key for the tools in the prompt dataset." ), @@ -695,6 +695,16 @@ def add_algo_arguments(parser): reset_arg(parser, "--save", type=str, default=None) reset_arg(parser, "--save-interval", type=int, default=None) reset_arg(parser, "--async-save", action="store_true") + reset_arg( + parser, + "--no-save-optim", + action="store_true", + default=False, + help=( + "If set, do not save the optimizer state when saving checkpoints. " + "This reduces checkpoint size but disables training resumption from the saved checkpoint." + ), + ) parser.add_argument( "--save-hf", type=str, diff --git a/miles/utils/health_monitor.py b/miles/utils/health_monitor.py index eea834c99..e95367e95 100644 --- a/miles/utils/health_monitor.py +++ b/miles/utils/health_monitor.py @@ -8,40 +8,68 @@ class RolloutHealthMonitor: + """Health monitor for rollout engines. + + The monitor runs continuously once started, but can be paused/resumed + based on whether the engines are offloaded (cannot health check when offloaded). + + Lifecycle: + - start(): Start the monitor thread (called once during initialization) + - pause(): Pause health checking (called when offloading engines) + - resume(): Resume health checking (called when onloading engines) + - stop(): Stop the monitor thread completely (called during dispose) + """ + def __init__(self, rollout_manager, args): # TODO may remove this dependency after refactoring self._rollout_manager = rollout_manager self._thread = None self._stop_event = None + self._pause_event = None # When set, health checking is paused self._check_interval = args.rollout_health_check_interval self._check_timeout = args.rollout_health_check_timeout self._check_first_wait = args.rollout_health_check_first_wait + self._need_first_wait = True # Need to wait after each resume + self._is_checking_enabled = False # Track if health checking should be active def start(self) -> bool: - if not self._rollout_manager.rollout_engines: + """Start the health monitor thread. Called once during initialization. + + Returns: + True if the monitor was started, False if there are no engines to monitor. + """ + if not self._rollout_manager.all_rollout_engines: return False - assert self._thread is None, "Health monitor thread is already running." + if self._thread is not None: + logger.warning("Health monitor thread is already running.") + return True logger.info("Starting RolloutHealthMonitor...") self._stop_event = threading.Event() + self._pause_event = threading.Event() + self._pause_event.set() # Start in paused state until resume() is called self._thread = threading.Thread( target=self._health_monitor_loop, name="RolloutHealthMonitor", daemon=True, ) self._thread.start() - logger.info("RolloutHealthMonitor started.") + logger.info("RolloutHealthMonitor started (in paused state).") return True def stop(self) -> None: + """Stop the health monitor thread completely. Called during dispose.""" if not self._thread: return logger.info("Stopping RolloutHealthMonitor...") assert self._stop_event is not None self._stop_event.set() + # Also clear pause to let the thread exit + if self._pause_event: + self._pause_event.clear() timeout = self._check_timeout + self._check_interval + 5 self._thread.join(timeout=timeout) if self._thread.is_alive(): @@ -51,16 +79,59 @@ def stop(self) -> None: self._thread = None self._stop_event = None + self._pause_event = None + self._is_checking_enabled = False + + def pause(self) -> None: + """Pause health checking. Called when engines are offloaded.""" + if self._pause_event is None: + return + logger.info("Pausing health monitor...") + self._pause_event.set() + self._is_checking_enabled = False + + def resume(self) -> None: + """Resume health checking. Called when engines are onloaded.""" + if self._pause_event is None: + return + logger.info("Resuming health monitor...") + self._need_first_wait = True # Need to wait after each resume + self._pause_event.clear() + self._is_checking_enabled = True + + def is_checking_enabled(self) -> bool: + """Return whether health checking is currently enabled (not paused).""" + return self._is_checking_enabled def _health_monitor_loop(self) -> None: assert self._stop_event is not None - logger.info(f"Health monitor loop started. Waiting for first wait: {self._check_first_wait}s") - # TODO: need to be waiting for the large moe to be ready. this is hacky. - if self._stop_event.wait(self._check_first_wait): - logger.info("Health monitor stopped during first wait.") - return + assert self._pause_event is not None + while not self._stop_event.is_set(): - self._run_health_checks() + # Wait while paused + while self._pause_event.is_set() and not self._stop_event.is_set(): + self._stop_event.wait(timeout=0.5) + + if self._stop_event.is_set(): + break + + # Do first wait after each resume (for large MoE models to be ready) + if self._need_first_wait: + logger.info(f"Health monitor doing first wait after resume: {self._check_first_wait}s") + if self._stop_event.wait(self._check_first_wait): + logger.info("Health monitor stopped during first wait.") + break + if self._pause_event.is_set(): + # Got paused during first wait, skip this round and wait again next resume + logger.info("Health monitor paused during first wait, will wait again next resume.") + continue + self._need_first_wait = False + + # Run health checks + if not self._pause_event.is_set() and not self._stop_event.is_set(): + self._run_health_checks() + + # Wait for next check interval if self._stop_event.wait(self._check_interval): break @@ -68,6 +139,8 @@ def _run_health_checks(self) -> None: for rollout_engine_id, engine in enumerate(self._rollout_manager.rollout_engines): if self._stop_event is not None and self._stop_event.is_set(): break + if self._pause_event is not None and self._pause_event.is_set(): + break self._check_engine_health(rollout_engine_id, engine) def _check_engine_health(self, rollout_engine_id, engine) -> None: @@ -82,6 +155,8 @@ def _check_engine_health(self, rollout_engine_id, engine) -> None: f"Health check failed for rollout engine {rollout_engine_id} (ray timeout or error). Killing actor. Exception: {e}" ) self._kill_engine(rollout_engine_id=rollout_engine_id) + else: + logger.debug(f"Health check passed for rollout engine {rollout_engine_id}") def _kill_engine(self, rollout_engine_id: int): logger.info(f"Killing engine group {rollout_engine_id}...") diff --git a/miles/utils/mask_utils.py b/miles/utils/mask_utils.py index a5bc90ab4..0ddb3a141 100644 --- a/miles/utils/mask_utils.py +++ b/miles/utils/mask_utils.py @@ -2,7 +2,8 @@ def get_response_lengths(loss_masks: list[list[int]]) -> list[int]: - return [mask.count(1) if 1 in mask else 0 for mask in loss_masks] + # return the lengths starting from the first occurrence of 1 to the end of each loss mask + return [len(mask[mask.index(1) :]) if 1 in mask else 0 for mask in loss_masks] class MultiTurnLossMaskGenerator: diff --git a/tests/test_qwen2.5_0.5B_gsm8k_async_short.py b/tests/test_qwen2.5_0.5B_gsm8k_async_short.py new file mode 100644 index 000000000..d55262cd0 --- /dev/null +++ b/tests/test_qwen2.5_0.5B_gsm8k_async_short.py @@ -0,0 +1,130 @@ +import os +import miles.utils.external_utils.command_utils as U + +TIGHT_DEVICE_MEMORY = U.get_bool_env_var("MILES_TEST_TIGHT_DEVICE_MEMORY", "1") + +MODEL_NAME = "Qwen2.5-0.5B-Instruct" +MODEL_TYPE = "qwen2.5-0.5B" +NUM_GPUS = 4 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"huggingface-cli download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset("zhuzilin/gsm8k") + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/ " f"--ref-load /root/models/{MODEL_NAME}/ " + + rollout_args = ( + "--prompt-data /root/datasets/gsm8k/train.parquet " + "--input-key messages " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 4 " + "--rollout-max-response-len 1024 " + "--rollout-temperature 0.8 " + "--over-sampling-batch-size 16 " + "--dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std " + "--global-batch-size 32 " + ) + + eval_args = ( + "--eval-interval 8 " + "--eval-prompt-data gsm8k /root/datasets/gsm8k/test.parquet " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 1024 " + "--eval-top-k 1 " + ) + + perf_args = ( + "--tensor-model-parallel-size 1 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 1 " + "--expert-model-parallel-size 1 " + "--expert-tensor-parallel-size 1 " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 9216 " + ) + + grpo_args = ( + "--advantage-estimator grpo " + "--use-kl-loss " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 1 " + f"--sglang-mem-fraction-static {0.55 if TIGHT_DEVICE_MEMORY else 0.65} " + "--sglang-enable-metrics " + ) + + ci_args = "--ci-test " + + fault_tolerance_args = ( + "--use-fault-tolerance " + "--rollout-health-check-interval 5 " + "--rollout-health-check-timeout 10 " + "--rollout-health-check-first-wait 0 " + ) + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 1 " + "--rollout-num-gpus 3 " + "--megatron-to-hf-mode bridge " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{fault_tolerance_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + train_script="train_async.py", + ) + + +if __name__ == "__main__": + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute() diff --git a/tests/test_qwen2.5_0.5B_gsm8k_short.py b/tests/test_qwen2.5_0.5B_gsm8k_short.py new file mode 100644 index 000000000..afbffbc56 --- /dev/null +++ b/tests/test_qwen2.5_0.5B_gsm8k_short.py @@ -0,0 +1,129 @@ +import os +import miles.utils.external_utils.command_utils as U + +TIGHT_DEVICE_MEMORY = U.get_bool_env_var("MILES_TEST_TIGHT_DEVICE_MEMORY", "1") + +MODEL_NAME = "Qwen2.5-0.5B-Instruct" +MODEL_TYPE = "qwen2.5-0.5B" +NUM_GPUS = 4 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"huggingface-cli download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset("zhuzilin/gsm8k") + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/ " f"--ref-load /root/models/{MODEL_NAME}/ " + + rollout_args = ( + "--prompt-data /root/datasets/gsm8k/train.parquet " + "--input-key messages " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 4 " + "--rollout-max-response-len 1024 " + "--rollout-temperature 0.8 " + "--over-sampling-batch-size 16 " + "--dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std " + "--global-batch-size 32 " + ) + + eval_args = ( + "--eval-interval 20 " + "--eval-prompt-data gsm8k /root/datasets/gsm8k/test.parquet " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 1024 " + "--eval-top-k 1 " + ) + + perf_args = ( + "--tensor-model-parallel-size 1 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 1 " + "--expert-model-parallel-size 1 " + "--expert-tensor-parallel-size 1 " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 9216 " + ) + + grpo_args = ( + "--advantage-estimator grpo " + "--use-kl-loss " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 1 " + f"--sglang-mem-fraction-static {0.6 if TIGHT_DEVICE_MEMORY else 0.7} " + "--sglang-enable-metrics " + ) + + ci_args = "--ci-test " + + fault_tolerance_args = ( + "--use-fault-tolerance " + "--rollout-health-check-interval 5 " + "--rollout-health-check-timeout 10 " + "--rollout-health-check-first-wait 0 " + ) + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 4 " + "--colocate " + "--megatron-to-hf-mode bridge " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{fault_tolerance_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + prepare() + os.environ.pop("http_proxy") + os.environ.pop("https_proxy") + os.environ.pop("HTTP_PROXY") + os.environ.pop("HTTPS_PROXY") + execute() diff --git a/train.py b/train.py index a4f6824cc..745dcbed6 100644 --- a/train.py +++ b/train.py @@ -1,10 +1,4 @@ import ray -from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_WEIGHTS - -try: - from sglang.srt.constants import GPU_MEMORY_TYPE_CUDA_GRAPH -except ImportError: - GPU_MEMORY_TYPE_CUDA_GRAPH = None from miles.ray.placement_group import create_placement_groups, create_rollout_manager, create_training_models from miles.utils.arguments import parse_args @@ -27,7 +21,7 @@ def train(args): actor_model, critic_model = create_training_models(args, pgs, rollout_manager) if args.offload_rollout: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) + ray.get(rollout_manager.onload_weights.remote()) # always update weight first so that sglang has the loaded weights from training. actor_model.update_weights() @@ -36,9 +30,7 @@ def train(args): ray.get(rollout_manager.check_weights.remote(action="compare")) if args.offload_rollout: - if GPU_MEMORY_TYPE_CUDA_GRAPH is not None: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_CUDA_GRAPH])) - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_KV_CACHE])) + ray.get(rollout_manager.onload_kv.remote()) # special case for eval-only if args.num_rollout == 0 and args.eval_interval is not None: @@ -55,9 +47,19 @@ def offload_train(): else: actor_model.clear_memory() - def onload_rollout(): - if args.offload_rollout: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) + def save(rollout_id): + if (not args.use_critic) or (rollout_id >= args.num_critic_only_steps): + actor_model.save_model( + rollout_id, + force_sync=rollout_id == args.num_rollout - 1, + ) + if args.use_critic: + critic_model.save_model( + rollout_id, + force_sync=rollout_id == args.num_rollout - 1, + ) + if args.rollout_global_dataset: + ray.get(rollout_manager.save.remote(rollout_id)) # train loop. # note that for async training, one can change the position of the sync operation(ray.get). @@ -79,27 +81,14 @@ def onload_rollout(): ray.get(actor_model.async_train(rollout_id, rollout_data_ref)) if should_run_periodic_action(rollout_id, args.save_interval, num_rollout_per_epoch, args.num_rollout): - if (not args.use_critic) or (rollout_id >= args.num_critic_only_steps): - actor_model.save_model( - rollout_id, - force_sync=rollout_id == args.num_rollout - 1, - ) - if args.use_critic: - critic_model.save_model( - rollout_id, - force_sync=rollout_id == args.num_rollout - 1, - ) - if args.rollout_global_dataset: - ray.get(rollout_manager.save.remote(rollout_id)) + save(rollout_id) offload_train() - onload_rollout() + if args.offload_rollout: + ray.get(rollout_manager.onload_weights.remote()) actor_model.update_weights() - if args.offload_rollout: - if GPU_MEMORY_TYPE_CUDA_GRAPH is not None: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_CUDA_GRAPH])) - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_KV_CACHE])) + ray.get(rollout_manager.onload_kv.remote()) if should_run_periodic_action(rollout_id, args.eval_interval, num_rollout_per_epoch): ray.get(rollout_manager.eval.remote(rollout_id)) From 60fc56ec71829e35a6541c9f5e97ca7de5806729 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 12 Jan 2026 14:08:46 -0800 Subject: [PATCH 13/57] move swe to experimental (#421) --- .gitmodules | 4 ++-- examples/experimental/README.md | 1 + examples/{ => experimental}/swe-agent/README.md | 0 .../{ => experimental}/swe-agent/download_and_process_data.py | 0 .../{ => experimental}/swe-agent/generate_with_swe_agent.py | 0 examples/{ => experimental}/swe-agent/mini-swe-agent | 0 examples/{ => experimental}/swe-agent/nemo-gym | 0 .../{ => experimental}/swe-agent/run-qwen3-4b-instruct.sh | 0 8 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 examples/experimental/README.md rename examples/{ => experimental}/swe-agent/README.md (100%) rename examples/{ => experimental}/swe-agent/download_and_process_data.py (100%) rename examples/{ => experimental}/swe-agent/generate_with_swe_agent.py (100%) rename examples/{ => experimental}/swe-agent/mini-swe-agent (100%) rename examples/{ => experimental}/swe-agent/nemo-gym (100%) rename examples/{ => experimental}/swe-agent/run-qwen3-4b-instruct.sh (100%) diff --git a/.gitmodules b/.gitmodules index 7885d8a54..21ea17aba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,8 @@ [submodule "examples/swe-agent/nemo-gym"] - path = examples/swe-agent/nemo-gym + path = examples/experimental/swe-agent/nemo-gym url = https://github.com/yueming-yuan/Gym branch = miles-swe-agent [submodule "examples/swe-agent/mini-swe-agent"] - path = examples/swe-agent/mini-swe-agent + path = examples/experimental/swe-agent/mini-swe-agent url = https://github.com/yueming-yuan/nv-mini-swe-agent branch = miles-swe-agent diff --git a/examples/experimental/README.md b/examples/experimental/README.md new file mode 100644 index 000000000..efc6363d9 --- /dev/null +++ b/examples/experimental/README.md @@ -0,0 +1 @@ +The examples under this directory are not fully verified, only for experimental use/develop purpose. diff --git a/examples/swe-agent/README.md b/examples/experimental/swe-agent/README.md similarity index 100% rename from examples/swe-agent/README.md rename to examples/experimental/swe-agent/README.md diff --git a/examples/swe-agent/download_and_process_data.py b/examples/experimental/swe-agent/download_and_process_data.py similarity index 100% rename from examples/swe-agent/download_and_process_data.py rename to examples/experimental/swe-agent/download_and_process_data.py diff --git a/examples/swe-agent/generate_with_swe_agent.py b/examples/experimental/swe-agent/generate_with_swe_agent.py similarity index 100% rename from examples/swe-agent/generate_with_swe_agent.py rename to examples/experimental/swe-agent/generate_with_swe_agent.py diff --git a/examples/swe-agent/mini-swe-agent b/examples/experimental/swe-agent/mini-swe-agent similarity index 100% rename from examples/swe-agent/mini-swe-agent rename to examples/experimental/swe-agent/mini-swe-agent diff --git a/examples/swe-agent/nemo-gym b/examples/experimental/swe-agent/nemo-gym similarity index 100% rename from examples/swe-agent/nemo-gym rename to examples/experimental/swe-agent/nemo-gym diff --git a/examples/swe-agent/run-qwen3-4b-instruct.sh b/examples/experimental/swe-agent/run-qwen3-4b-instruct.sh similarity index 100% rename from examples/swe-agent/run-qwen3-4b-instruct.sh rename to examples/experimental/swe-agent/run-qwen3-4b-instruct.sh From 5d7a21c3e10dba58025473a68ac9c602b9232cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=99=A8=E9=98=B3?= Date: Mon, 12 Jan 2026 17:06:21 -0800 Subject: [PATCH 14/57] feat: add int4 reinforcement learning training support (Part1) (#422) --- docker/patch/latest/megatron.patch | 93 +++++++- docker/patch/latest/sglang.patch | 359 ++++++++++++++++++++++++++++- 2 files changed, 447 insertions(+), 5 deletions(-) diff --git a/docker/patch/latest/megatron.patch b/docker/patch/latest/megatron.patch index a337b19fb..8504c1885 100644 --- a/docker/patch/latest/megatron.patch +++ b/docker/patch/latest/megatron.patch @@ -50,7 +50,7 @@ index 5a1ea308d..aa701237f 100644 ) diff --git a/megatron/core/extensions/transformer_engine.py b/megatron/core/extensions/transformer_engine.py -index acb93ef78..20ee977b0 100644 +index acb93ef78..d239db4ab 100644 --- a/megatron/core/extensions/transformer_engine.py +++ b/megatron/core/extensions/transformer_engine.py @@ -408,6 +408,7 @@ class TELinear(te.pytorch.Linear): @@ -61,6 +61,97 @@ index acb93ef78..20ee977b0 100644 if is_expert: # Reduce the gradient on the expert_data_parallel group for expert linear layers setattr(param, "allreduce", not self.expert_parallel) +@@ -1161,6 +1162,61 @@ class TEDotProductAttention(te.pytorch.DotProductAttention): + + + if HAVE_TE and is_te_min_version("1.9.0.dev0"): ++ def ceil_div(x: int, y: int) -> int: ++ return (x + y - 1) // y ++ ++ class _FakeInt4QuantizationSTE(torch.autograd.Function): ++ @staticmethod ++ def forward(ctx, x, group_size): ++ m, n = x.shape ++ block_size_m, block_size_n = 1, group_size ++ ++ ++ m_padded = ceil_div(m, block_size_m) * block_size_m ++ n_padded = ceil_div(n, block_size_n) * block_size_n ++ ++ x_padded = torch.zeros( ++ (m_padded, n_padded), ++ dtype=x.dtype, device=x.device ++ ) ++ x_padded[:m, :n] = x ++ ++ x_view = x_padded.view( ++ m_padded // block_size_m, ++ block_size_m, ++ n_padded // block_size_n, ++ block_size_n ++ ) ++ ++ x_max = x_view.abs().float().amax(dim=(1, 3), keepdim=True) ++ q_max = 7 ++ x_scale = x_max / q_max ++ ++ x_scale = x_scale.clamp(min=1e-5) ++ ++ x_div = x_view / x_scale ++ x_round = torch.round(x_div) ++ ++ x_q_clamped = x_round.clamp(-q_max, q_max) ++ ++ x_dequant_view = x_q_clamped * x_scale ++ ++ x_dequant_full = x_dequant_view.view_as(x_padded) ++ x_out = x_dequant_full[:m, :n].contiguous().to(x.dtype) ++ ++ return x_out ++ ++ @staticmethod ++ def backward(ctx, grad_output): ++ return grad_output, None ++ ++ def fake_int4_quantization_ste(x, group_size): ++ x_out = _FakeInt4QuantizationSTE.apply(x, group_size) ++ ++ if hasattr(x, 'main_grad'): ++ x_out.main_grad = x.main_grad ++ ++ return x_out + + class TEGroupedLinear(te.pytorch.GroupedLinear): + """ +@@ -1351,6 +1407,7 @@ if HAVE_TE and is_te_min_version("1.9.0.dev0"): + _is_first_microbatch = ( + None if self.disable_parameter_transpose_cache else self.is_first_microbatch + ) ++ + out = super().forward(x, m_splits, is_first_microbatch=_is_first_microbatch) + self.is_first_microbatch = False + +@@ -1361,6 +1418,20 @@ if HAVE_TE and is_te_min_version("1.9.0.dev0"): + return out + return out, None + ++ def _get_weight_tensors(self): ++ """Get the weight tensors of the module.""" ++ weight_tensors = super()._get_weight_tensors() ++ ++ if os.getenv("OPEN_TRAINING_INT4_FAKE_QAT_FLAG", "0") == "1": ++ group_size = int(os.getenv("OPEN_TRAINING_INT4_GROUP_SIZE", "128")) ++ ++ weight_tensors = [ ++ fake_int4_quantization_ste(w, group_size) ++ for w in weight_tensors ++ ] ++ ++ return weight_tensors ++ + def _encode_extra_state(self, state): + # TE 2.0 changed the format of extra_state to be a byte tensor + if is_te_min_version("2.0.0"): diff --git a/megatron/core/fusions/fused_mla_yarn_rope_apply.py b/megatron/core/fusions/fused_mla_yarn_rope_apply.py index 1fd5dcfae..c9aeef1f0 100644 --- a/megatron/core/fusions/fused_mla_yarn_rope_apply.py diff --git a/docker/patch/latest/sglang.patch b/docker/patch/latest/sglang.patch index 42d23ed65..7ed339363 100644 --- a/docker/patch/latest/sglang.patch +++ b/docker/patch/latest/sglang.patch @@ -74,6 +74,77 @@ index 0478526ef..cfb1aa669 100644 def get_pipeline_model_parallel_world_size(): +diff --git a/python/sglang/srt/entrypoints/engine.py b/python/sglang/srt/entrypoints/engine.py +index 21909706b..8fac5f162 100644 +--- a/python/sglang/srt/entrypoints/engine.py ++++ b/python/sglang/srt/entrypoints/engine.py +@@ -49,6 +49,7 @@ from sglang.srt.managers.io_struct import ( + InitWeightsUpdateGroupReqInput, + LoadLoRAAdapterReqInput, + MultimodalDataInputFormat, ++ PostProcessWeightsReqInput, + ReleaseMemoryOccupationReqInput, + ResumeMemoryOccupationReqInput, + RpcReqInput, +@@ -593,6 +594,24 @@ class Engine(EngineBase): + self.tokenizer_manager.update_weights_from_ipc(obj, None) + ) + ++ def post_process_weights( ++ self, ++ restore_weights_before_load: bool = False, ++ post_process_quantization: bool = False, ++ ): ++ """ ++ Optional post-processing for updated weights (e.g., Marlin conversion). ++ Should be called after weight update is finished. ++ """ ++ obj = PostProcessWeightsReqInput( ++ restore_weights_before_load=restore_weights_before_load, ++ post_process_quantization=post_process_quantization, ++ ) ++ ++ return self.loop.run_until_complete( ++ self.tokenizer_manager.post_process_weights(obj, None) ++ ) ++ + def get_weights_by_name(self, name: str, truncate_size: int = 100): + """Get weights by parameter name.""" + obj = GetWeightsByNameReqInput(name=name, truncate_size=truncate_size) +diff --git a/python/sglang/srt/entrypoints/http_server.py b/python/sglang/srt/entrypoints/http_server.py +index 88705cc35..c8dc052f1 100644 +--- a/python/sglang/srt/entrypoints/http_server.py ++++ b/python/sglang/srt/entrypoints/http_server.py +@@ -107,6 +107,7 @@ from sglang.srt.managers.io_struct import ( + OpenSessionReqInput, + ParseFunctionCallReq, + PauseGenerationReqInput, ++ PostProcessWeightsReqInput, + ProfileReqInput, + ReleaseMemoryOccupationReqInput, + ResumeMemoryOccupationReqInput, +@@ -957,6 +958,21 @@ async def update_weights_from_ipc(obj: UpdateWeightsFromIPCReqInput, request: Re + else: + return ORJSONResponse(content, status_code=HTTPStatus.BAD_REQUEST) + ++@app.post("/post_process_weights") ++async def post_process_weights(req: PostProcessWeightsReqInput, request: Request): ++ """ ++ Optional post-processing for updated weights (e.g., Marlin conversion). ++ This should be called selectively after `update_weights_from_distributed/update_weights_from_tensor`. ++ """ ++ success, message = await _global_state.tokenizer_manager.post_process_weights( ++ req, request ++ ) ++ ++ content = {"success": success, "message": message} ++ return ORJSONResponse( ++ content, status_code=200 if success else HTTPStatus.BAD_REQUEST ++ ) ++ + + @app.post("/update_weight_version") + async def update_weight_version(obj: UpdateWeightVersionReqInput, request: Request): diff --git a/python/sglang/srt/layers/layernorm.py b/python/sglang/srt/layers/layernorm.py index b07164c53..8e6722ce0 100644 --- a/python/sglang/srt/layers/layernorm.py @@ -244,6 +315,102 @@ index 00bd68755..5a3ca8a67 100644 self.device_cache.capture_fwd_routed_experts(layer_id, topk_ids) def get_routed_experts( +diff --git a/python/sglang/srt/layers/quantization/compressed_tensors/compressed_tensors_moe.py b/python/sglang/srt/layers/quantization/compressed_tensors/compressed_tensors_moe.py +index c5e5a11fc..6b788fb1d 100644 +--- a/python/sglang/srt/layers/quantization/compressed_tensors/compressed_tensors_moe.py ++++ b/python/sglang/srt/layers/quantization/compressed_tensors/compressed_tensors_moe.py +@@ -1016,13 +1016,38 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod): + layer.a2_scale = None + layer.marlin_state = GPTQMarlinState.REPACK + ++ if not hasattr(layer, "_original_shapes"): ++ layer._original_shapes = {} ++ ++ # Force record: these are the target GPTQ shapes for rollback. ++ layer._original_shapes["w13_weight_packed"] = tuple(w13_weight.shape) ++ layer._original_shapes["w2_weight_packed"] = tuple(w2_weight.shape) ++ ++ # Also record the shapes of the scales. ++ layer._original_shapes["w2_weight_scale"] = tuple(w2_scale.shape) ++ layer._original_shapes["w13_weight_scale"] = tuple(w13_scale.shape) ++ + def process_weights_after_loading(self, layer: torch.nn.Module) -> None: ++ ++ # Skip if the layer is already converted to Marlin format to prevent double-packing. ++ if getattr(layer, "is_marlin_converted", False): ++ return ++ ++ if not hasattr(layer, "_original_shapes"): ++ layer._original_shapes = {} + + def replace_tensor(name, new_t): ++ target_attr = getattr(layer, name) ++ ++ # Only save if the key doesn't exist to prevent overwriting with Marlin shapes. ++ if name not in layer._original_shapes: ++ # This is a safety check; `create_weights` usually handles this already. ++ layer._original_shapes[name] = tuple(target_attr.shape) ++ + # It is important to use resize_() here since it ensures + # the same buffer is reused +- getattr(layer, name).resize_(new_t.shape) +- getattr(layer, name).copy_(new_t) ++ target_attr.resize_(new_t.shape) ++ target_attr.copy_(new_t) + del new_t + + num_experts = layer.w13_weight_g_idx.shape[0] +@@ -1078,7 +1103,7 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod): + layer.w13_weight_packed.shape[2], + self.num_bits, + ) +- replace_parameter(layer, "w13_weight_packed", marlin_w13_qweight) ++ replace_tensor("w13_weight_packed", marlin_w13_qweight) + marlin_w2_qweight = gptq_marlin_moe_repack( + layer.w2_weight_packed, + layer.w2_g_idx_sort_indices, +@@ -1086,7 +1111,7 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod): + layer.w2_weight_packed.shape[2], + self.num_bits, + ) +- replace_parameter(layer, "w2_weight_packed", marlin_w2_qweight) ++ replace_tensor("w2_weight_packed", marlin_w2_qweight) + # Repack scales + marlin_w13_scales = marlin_moe_permute_scales( + layer.w13_weight_scale, +@@ -1094,7 +1119,7 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod): + layer.w13_weight_scale.shape[2], + self.group_size, + ) +- replace_parameter(layer, "w13_weight_scale", marlin_w13_scales) ++ replace_tensor("w13_weight_scale", marlin_w13_scales) + + marlin_w2_scales = marlin_moe_permute_scales( + layer.w2_weight_scale, +@@ -1103,7 +1128,22 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod): + layer.w2_weight_scale.shape[2], + self.group_size, + ) +- replace_parameter(layer, "w2_weight_scale", marlin_w2_scales) ++ replace_tensor("w2_weight_scale", marlin_w2_scales) ++ ++ layer.is_marlin_converted = True ++ ++ def restore_weights_before_loading(self, layer: torch.nn.Module): ++ """Forcibly resize parameters back to their original shapes (e.g., GPTQ format) before loading weights.""" ++ if not hasattr(layer, "_original_shapes"): ++ return ++ ++ for name, orig_shape in layer._original_shapes.items(): ++ param = getattr(layer, name, None) ++ ++ if param is not None and param.shape != orig_shape: ++ param.resize_(orig_shape) ++ ++ layer.is_marlin_converted = False + + def create_moe_runner( + self, layer: torch.nn.Module, moe_runner_config: MoeRunnerConfig diff --git a/python/sglang/srt/layers/rotary_embedding.py b/python/sglang/srt/layers/rotary_embedding.py index 56516b41b..cb2ebca60 100644 --- a/python/sglang/srt/layers/rotary_embedding.py @@ -293,6 +460,30 @@ index 55bef5652..35ad68b1c 100644 # For ascend backend, softmax is not needed before sampling if not get_global_server_args().sampling_backend == "ascend" or ( return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB +diff --git a/python/sglang/srt/managers/io_struct.py b/python/sglang/srt/managers/io_struct.py +index 879e1bfa6..de52085fa 100644 +--- a/python/sglang/srt/managers/io_struct.py ++++ b/python/sglang/srt/managers/io_struct.py +@@ -1286,6 +1286,19 @@ class UpdateWeightsFromIPCReqOutput(BaseReq): + success: bool + message: str + ++@dataclass ++class PostProcessWeightsReqInput(BaseReq): ++ # Whether to restore weights before loading new weights ++ restore_weights_before_load: bool = False ++ # Whether to enable quantization post-processing ++ post_process_quantization: bool = False ++ ++ ++@dataclass ++class PostProcessWeightsReqOutput(BaseReq): ++ success: bool ++ message: str ++ + + @dataclass + class InitWeightsSendGroupForRemoteInstanceReqOutput(BaseReq): diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py index 468d8fb8a..229a9a2dc 100644 --- a/python/sglang/srt/managers/schedule_batch.py @@ -307,6 +498,26 @@ index 468d8fb8a..229a9a2dc 100644 ) +diff --git a/python/sglang/srt/managers/scheduler.py b/python/sglang/srt/managers/scheduler.py +index bca1c31e6..0c82e37a4 100644 +--- a/python/sglang/srt/managers/scheduler.py ++++ b/python/sglang/srt/managers/scheduler.py +@@ -97,6 +97,7 @@ from sglang.srt.managers.io_struct import ( + OpenSessionReqInput, + OpenSessionReqOutput, + PauseGenerationReqInput, ++ PostProcessWeightsReqInput, + ProfileReq, + ReleaseMemoryOccupationReqInput, + ResumeMemoryOccupationReqInput, +@@ -1055,6 +1056,7 @@ class Scheduler( + ), + (UpdateWeightsFromTensorReqInput, self.update_weights_from_tensor), + (UpdateWeightsFromIPCReqInput, self.update_weights_from_ipc), ++ (PostProcessWeightsReqInput, self.post_process_weights), + (GetWeightsByNameReqInput, self.get_weights_by_name), + (ReleaseMemoryOccupationReqInput, self.release_memory_occupation), + (ResumeMemoryOccupationReqInput, self.resume_memory_occupation), diff --git a/python/sglang/srt/managers/scheduler_output_processor_mixin.py b/python/sglang/srt/managers/scheduler_output_processor_mixin.py index e40586c24..32d98aee4 100644 --- a/python/sglang/srt/managers/scheduler_output_processor_mixin.py @@ -320,7 +531,7 @@ index e40586c24..32d98aee4 100644 AbortReq, BatchEmbeddingOutput, diff --git a/python/sglang/srt/managers/scheduler_update_weights_mixin.py b/python/sglang/srt/managers/scheduler_update_weights_mixin.py -index 293a84350..0947f77e0 100644 +index 293a84350..68911c433 100644 --- a/python/sglang/srt/managers/scheduler_update_weights_mixin.py +++ b/python/sglang/srt/managers/scheduler_update_weights_mixin.py @@ -1,6 +1,7 @@ @@ -341,7 +552,28 @@ index 293a84350..0947f77e0 100644 from sglang.srt.managers.io_struct import ( CheckWeightsReqInput, CheckWeightsReqOutput, -@@ -137,6 +141,13 @@ class SchedulerUpdateWeightsMixin: +@@ -21,6 +25,8 @@ from sglang.srt.managers.io_struct import ( + GetWeightsByNameReqOutput, + InitWeightsUpdateGroupReqInput, + InitWeightsUpdateGroupReqOutput, ++ PostProcessWeightsReqInput, ++ PostProcessWeightsReqOutput, + ReleaseMemoryOccupationReqInput, + ReleaseMemoryOccupationReqOutput, + ResumeMemoryOccupationReqInput, +@@ -113,6 +119,11 @@ class SchedulerUpdateWeightsMixin: + logger.error(message) + torch.distributed.barrier(group=self.tp_cpu_group) + return UpdateWeightsFromIPCReqOutput(success, message) ++ ++ def post_process_weights(self, recv_req: PostProcessWeightsReqInput): ++ """Optional post-processing for updated weights (e.g., Marlin conversion).""" ++ success, message = self.tp_worker.post_process_weights(recv_req) ++ return PostProcessWeightsReqOutput(success, message) + + def get_weights_by_name(self: Scheduler, recv_req: GetWeightsByNameReqInput): + parameter = self.tp_worker.get_weights_by_name(recv_req) +@@ -137,6 +148,13 @@ class SchedulerUpdateWeightsMixin: self.memory_saver_adapter.pause(GPU_MEMORY_TYPE_KV_CACHE) self.flush_cache() @@ -355,7 +587,7 @@ index 293a84350..0947f77e0 100644 if GPU_MEMORY_TYPE_WEIGHTS in tags: self.stashed_model_static_state = _export_static_state( self.tp_worker.model_runner.model -@@ -177,6 +188,13 @@ class SchedulerUpdateWeightsMixin: +@@ -177,6 +195,13 @@ class SchedulerUpdateWeightsMixin: if GPU_MEMORY_TYPE_KV_CACHE in tags: self.memory_saver_adapter.resume(GPU_MEMORY_TYPE_KV_CACHE) @@ -369,6 +601,58 @@ index 293a84350..0947f77e0 100644 return ResumeMemoryOccupationReqOutput() def check_weights(self: Scheduler, recv_req: CheckWeightsReqInput): +diff --git a/python/sglang/srt/managers/tokenizer_communicator_mixin.py b/python/sglang/srt/managers/tokenizer_communicator_mixin.py +index e5d42bed8..412293b30 100644 +--- a/python/sglang/srt/managers/tokenizer_communicator_mixin.py ++++ b/python/sglang/srt/managers/tokenizer_communicator_mixin.py +@@ -49,6 +49,8 @@ from sglang.srt.managers.io_struct import ( + LoadLoRAAdapterReqOutput, + LoRAUpdateOutput, + OpenSessionReqInput, ++ PostProcessWeightsReqInput, ++ PostProcessWeightsReqOutput, + ProfileReq, + ProfileReqOutput, + ProfileReqType, +@@ -177,6 +179,9 @@ class TokenizerCommunicatorMixin: + self.update_weights_from_ipc_communicator = _Communicator( + self.send_to_scheduler, server_args.dp_size + ) ++ self.post_process_weights_communicator = _Communicator( ++ self.send_to_scheduler, server_args.dp_size ++ ) + self.get_weights_by_name_communicator = _Communicator( + self.send_to_scheduler, server_args.dp_size + ) +@@ -250,6 +255,10 @@ class TokenizerCommunicatorMixin: + UpdateWeightsFromIPCReqOutput, + self.update_weights_from_ipc_communicator.handle_recv, + ), ++ ( ++ PostProcessWeightsReqOutput, ++ self.post_process_weights_communicator.handle_recv, ++ ), + ( + GetWeightsByNameReqOutput, + self.get_weights_by_name_communicator.handle_recv, +@@ -433,6 +442,17 @@ class TokenizerCommunicatorMixin: + + return success, message + ++ async def post_process_weights( ++ self: TokenizerManager, ++ obj: PostProcessWeightsReqInput, ++ request: Optional[fastapi.Request] = None, ++ ) -> Tuple[bool, str]: ++ """Trigger post-processing hooks for weights after loading (e.g., Marlin conversion).""" ++ self.auto_create_handle_loop() ++ async with self.model_update_lock.writer_lock: ++ results = await self.post_process_weights_communicator(obj) ++ return _Communicator.merge_results(results) ++ + async def init_weights_send_group_for_remote_instance( + self, + obj: InitWeightsSendGroupForRemoteInstanceReqInput, diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py index f4fc29e29..5ef12cca6 100644 --- a/python/sglang/srt/managers/tokenizer_manager.py @@ -393,8 +677,32 @@ index f4fc29e29..5ef12cca6 100644 state.output_token_logprobs_val.extend( recv_obj.output_token_logprobs_val[recv_obj_index] ) +diff --git a/python/sglang/srt/managers/tp_worker.py b/python/sglang/srt/managers/tp_worker.py +index 1f1875254..51d8651ce 100644 +--- a/python/sglang/srt/managers/tp_worker.py ++++ b/python/sglang/srt/managers/tp_worker.py +@@ -27,6 +27,7 @@ from sglang.srt.managers.io_struct import ( + InitWeightsSendGroupForRemoteInstanceReqInput, + InitWeightsUpdateGroupReqInput, + LoadLoRAAdapterReqInput, ++ PostProcessWeightsReqInput, + SendWeightsToRemoteInstanceReqInput, + UnloadLoRAAdapterReqInput, + UpdateWeightFromDiskReqInput, +@@ -175,6 +176,11 @@ class BaseTpWorker(ABC): + success, message = self.model_runner.update_weights_from_ipc(recv_req) + return success, message + ++ def post_process_weights(self, recv_req: PostProcessWeightsReqInput): ++ """Perform optional post-processing on the updated model weights (e.g., Marlin conversion).""" ++ success, message = self.model_runner.post_process_weights(recv_req) ++ return success, message ++ + def get_weights_by_name(self, recv_req: GetWeightsByNameReqInput): + parameter = self.model_runner.get_weights_by_name( + recv_req.name, recv_req.truncate_size diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py -index 1d69c0582..9027374be 100644 +index 1d69c0582..aa9aefec6 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py @@ -558,7 +558,8 @@ class ModelRunner(ModelRunnerKVCacheMixin): @@ -425,6 +733,49 @@ index 1d69c0582..9027374be 100644 if self.eplb_manager is not None: self.eplb_manager.on_forward_pass_end() +@@ -2436,6 +2438,42 @@ class ModelRunner(ModelRunnerKVCacheMixin): + logger.error(f"IPC weight update failed: {e}") + return False, str(e) + ++ def post_process_weights(self, recv_req): ++ """ ++ Execute post-processing logic for model weights, such as Marlin quantization format conversion. ++ """ ++ from sglang.srt.model_loader.loader import device_loading_context ++ ++ target_device = torch.device("cuda", torch.cuda.current_device()) ++ ++ if recv_req.restore_weights_before_load: ++ for _, module in self.model.named_modules(): ++ quant_method = getattr(module, "quant_method", None) ++ ++ # Check if the module supports restoring weights ++ if quant_method is not None and hasattr( ++ quant_method, "restore_weights_before_loading" ++ ): ++ ++ with device_loading_context(module, target_device): ++ quant_method.restore_weights_before_loading(module) ++ ++ if recv_req.post_process_quantization: ++ ++ # Iterate through all modules to apply specific post-loading processing ++ for _, module in self.model.named_modules(): ++ quant_method = getattr(module, "quant_method", None) ++ ++ # Check if the module supports quantization post-processing ++ if quant_method is not None and hasattr( ++ quant_method, "process_weights_after_loading" ++ ): ++ ++ # Apply the post-processing (e.g., repacking weights for Marlin kernel) ++ with device_loading_context(module, target_device): ++ quant_method.process_weights_after_loading(module) ++ ++ return True, "Success" + + def _model_load_weights_direct(model, named_tensors: List[Tuple[str, torch.Tensor]]): + params_dict = dict(model.named_parameters()) diff --git a/python/sglang/srt/models/deepseek_v2.py b/python/sglang/srt/models/deepseek_v2.py index 2918461d3..2bcc67087 100644 --- a/python/sglang/srt/models/deepseek_v2.py From f68eef8cdc1288b68c189a8e0e3c894fa33d025e Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 12 Jan 2026 19:20:36 -0800 Subject: [PATCH 15/57] refactor [1/X]: unify training backends by general utils, tested Megatron & FSDP alignment (#412) --- .github/workflows/pr-test.yml | 6 +- .github/workflows/pr-test.yml.j2 | 3 + examples/train_infer_mismatch_helper/mis.py | 9 +- miles/backends/fsdp_utils/actor.py | 811 ++++-------------- miles/backends/fsdp_utils/parallel.py | 58 ++ miles/backends/megatron_utils/actor.py | 105 +-- miles/backends/megatron_utils/data.py | 614 ------------- miles/backends/megatron_utils/model.py | 121 ++- miles/backends/megatron_utils/parallel.py | 67 ++ miles/backends/training_utils/ci_utils.py | 55 ++ .../cp_utils.py | 33 +- miles/backends/training_utils/data.py | 433 ++++++++++ miles/backends/training_utils/log_utils.py | 408 +++++++++ .../loss.py | 62 +- miles/backends/training_utils/parallel.py | 27 + miles/utils/arguments.py | 2 + miles/utils/ppo_utils.py | 64 +- tests/test_qwen3_0.6B_megatron_fsdp_align.py | 152 ++++ 18 files changed, 1552 insertions(+), 1478 deletions(-) create mode 100644 miles/backends/fsdp_utils/parallel.py delete mode 100644 miles/backends/megatron_utils/data.py create mode 100644 miles/backends/megatron_utils/parallel.py create mode 100644 miles/backends/training_utils/ci_utils.py rename miles/backends/{megatron_utils => training_utils}/cp_utils.py (90%) create mode 100644 miles/backends/training_utils/data.py create mode 100644 miles/backends/training_utils/log_utils.py rename miles/backends/{megatron_utils => training_utils}/loss.py (94%) create mode 100644 miles/backends/training_utils/parallel.py create mode 100644 tests/test_qwen3_0.6B_megatron_fsdp_align.py diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 8ea939739..f00faa5a6 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -92,7 +92,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}] + info: [{"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}, {"num_gpus": 4, "test_file": "test_qwen3_0.6B_megatron_fsdp_align.py"}] defaults: run: working-directory: ${{ github.workspace }} @@ -180,7 +180,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}] + info: [{"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}, {"num_gpus": 4, "test_file": "test_qwen3_0.6B_megatron_fsdp_align.py"}] defaults: run: working-directory: ${{ github.workspace }} @@ -312,7 +312,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_async_short.py"}, {"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_short.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_colocated_2xGPU.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}, {"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py --async-save"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k.py"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k_async.py"}] + info: [{"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_async_short.py"}, {"num_gpus": 4, "test_file": "test_qwen2.5_0.5B_gsm8k_short.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_colocated_2xGPU.py"}, {"num_gpus": 2, "test_file": "test_qwen3_4B_fsdp_true_on_policy.py"}, {"num_gpus": 8, "test_file": "test_qwen3_vl_4B_fsdp.py"}, {"num_gpus": 2, "test_file": "test_qwen3_0.6B_fsdp_distributed.py"}, {"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}, {"num_gpus": 8, "test_file": "test_qwen3_0.6B_parallel_check.py"}, {"num_gpus": 4, "test_file": "test_qwen3_0.6B_megatron_fsdp_align.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ckpt.py --async-save"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k.py"}, {"num_gpus": 2, "test_file": "test_qwen2.5_0.5B_gsm8k_async.py"}] defaults: run: working-directory: ${{ github.workspace }} diff --git a/.github/workflows/pr-test.yml.j2 b/.github/workflows/pr-test.yml.j2 index 84cac9114..25bb2bce2 100644 --- a/.github/workflows/pr-test.yml.j2 +++ b/.github/workflows/pr-test.yml.j2 @@ -13,6 +13,7 @@ {'test_file': 'test_qwen3_4B_fsdp_true_on_policy.py', 'num_gpus': 2}, {'test_file': 'test_qwen3_vl_4B_fsdp.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_0.6B_fsdp_distributed.py', 'num_gpus': 2}, + {'test_file': 'test_qwen3_0.6B_megatron_fsdp_align.py', 'num_gpus': 4}, ], }, 'e2e-test-megatron': { @@ -29,6 +30,7 @@ 'label': 'run-ci-precision', 'tests': [ {'test_file': 'test_qwen3_0.6B_parallel_check.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_0.6B_megatron_fsdp_align.py', 'num_gpus': 4}, ], }, 'e2e-test-ckpt': { @@ -61,6 +63,7 @@ {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_0.6B_parallel_check.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_0.6B_megatron_fsdp_align.py', 'num_gpus': 4}, {'test_file': 'test_qwen3_4B_ckpt.py', 'num_gpus': 8}, {'test_file': 'test_qwen3_4B_ckpt.py --async-save', 'num_gpus': 8}, {'test_file': 'test_qwen2.5_0.5B_gsm8k.py', 'num_gpus': 2}, diff --git a/examples/train_infer_mismatch_helper/mis.py b/examples/train_infer_mismatch_helper/mis.py index 5d14ceb6b..98fbe44b4 100644 --- a/examples/train_infer_mismatch_helper/mis.py +++ b/examples/train_infer_mismatch_helper/mis.py @@ -2,6 +2,8 @@ import torch +from miles.backends.training_utils.parallel import ParallelState + # NOTE: # - `compute_mis_weights` is a lightweight, standalone function that is useful to unit-test on CPU. # - `compute_mis_weights_with_cp` depends on Megatron context-parallel utilities, which are heavy and may not be @@ -316,6 +318,7 @@ def compute_mis_weights_with_cp( loss_masks: list[torch.Tensor], total_lengths: list[int], response_lengths: list[int], + parallel_state: ParallelState, **kwargs: Any, ) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]: """ @@ -336,13 +339,13 @@ def compute_mis_weights_with_cp( # Gather cp slice from other cp ranks full_rollout_log_probs = [ - all_gather_with_cp(log_prob, total_length, response_length) + all_gather_with_cp(log_prob, total_length, response_length, parallel_state) for log_prob, total_length, response_length in zip( rollout_log_probs, total_lengths, response_lengths, strict=False ) ] full_old_log_probs = [ - all_gather_with_cp(old_log_prob, total_length, response_length) + all_gather_with_cp(old_log_prob, total_length, response_length, parallel_state) for old_log_prob, total_length, response_length in zip( train_log_probs, total_lengths, response_lengths, strict=False ) @@ -362,7 +365,7 @@ def slice_cp_and_concat( ) -> torch.Tensor: values = [ # TODO: A rename of this function? - slice_log_prob_with_cp(values[i], total_lengths[i], response_lengths[i]) + slice_log_prob_with_cp(values[i], total_lengths[i], response_lengths[i], parallel_state) for i in range(len(values)) ] return torch.cat(values, dim=0) diff --git a/miles/backends/fsdp_utils/actor.py b/miles/backends/fsdp_utils/actor.py index f22a95546..536fdf970 100644 --- a/miles/backends/fsdp_utils/actor.py +++ b/miles/backends/fsdp_utils/actor.py @@ -2,41 +2,37 @@ import os import random from argparse import Namespace -from itertools import accumulate import ray import torch import torch.distributed as dist -import torch.nn.functional as F -from ring_flash_attn import substitute_hf_flash_attn, update_ring_flash_attn_params +from ring_flash_attn import update_ring_flash_attn_params from tqdm import tqdm from transformers import AutoConfig from miles.ray.train_actor import TrainRayActor from miles.utils import train_dump_utils, train_metric_utils from miles.utils.context_utils import with_defer -from miles.utils.data import get_minimum_num_micro_batch_size, process_rollout_data from miles.utils.distributed_utils import get_gloo_group from miles.utils.memory_utils import clear_memory, print_memory -from miles.utils.metric_utils import compute_rollout_step -from miles.utils.misc import load_function -from miles.utils.ppo_utils import ( - compute_approx_kl, - compute_gspo_kl, - compute_opsm_mask, - compute_policy_loss, - vanilla_tis_function, -) from miles.utils.processing_utils import load_processor, load_tokenizer from miles.utils.ray_utils import Box from miles.utils.timer import Timer, inverse_timer, timer from miles.utils.tracking_utils import init_tracking -from ...utils import tracking_utils from ...utils.profile_utils import TrainProfiler +from ..training_utils.ci_utils import check_grad_norm +from ..training_utils.data import DataIterator, get_batch, get_data_iterator, get_rollout_data +from ..training_utils.log_utils import ( + aggregate_forward_results, + aggregate_train_losses, + log_rollout_data, + log_train_step, +) +from ..training_utils.loss import compute_advantages_and_returns, get_log_probs_and_entropy, loss_function from . import checkpoint -from .data_packing import pack_sequences, pad_packed_sequence_with_cp, unpack_sequences from .lr_scheduler import get_lr_scheduler +from .parallel import create_fsdp_parallel_state from .update_weight_utils import UpdateWeightFromDistributed, UpdateWeightFromTensor logger = logging.getLogger(__name__) @@ -59,12 +55,13 @@ class FSDPTrainRayActor(TrainRayActor): def init(self, args: Namespace, role: str, with_ref: bool = False) -> int: # type: ignore[override] super().init(args, role, with_ref) - # Setup device mesh for parallelism (handles both CP and non-CP cases) - self._setup_device_mesh() + # Setup ParallelState for both CP and non-CP cases + self.parallel_state = create_fsdp_parallel_state(args) + torch.manual_seed(args.seed) self.train_parallel_config = { - "dp_size": self.dp_size, + "dp_size": self.parallel_state.dp_size, } if self.args.debug_rollout_only: @@ -106,10 +103,10 @@ def init(self, args: Namespace, role: str, with_ref: bool = False) -> int: # ty full_state = model.state_dict() - model = apply_fsdp2(model, mesh=self.dp_mesh, cpu_offload=self.fsdp_cpu_offload, args=self.args) + model = apply_fsdp2(model, mesh=self.parallel_state.dp_mesh, cpu_offload=self.fsdp_cpu_offload, args=self.args) model = self._fsdp2_load_full_state_dict( - model, full_state, self.dp_mesh, cpu_offload=True if self.fsdp_cpu_offload else None + model, full_state, self.parallel_state.dp_mesh, cpu_offload=True if self.fsdp_cpu_offload else None ) self.model = model @@ -189,53 +186,6 @@ def _enable_true_on_policy_optimizations(self, args): apply_fsdp_moe_patch() - def _setup_device_mesh(self) -> None: - """Setup device mesh for parallelism (always called, handles both CP and non-CP cases). - - Creates 2D mesh (dp_size, cp_size) for all cases: - - When context_parallel_size > 1: hybrid CP + DP - - When context_parallel_size = 1: pure DP (equivalent to 1D mesh) - - This ensures consistent group management across all parallelism modes. - """ - from torch.distributed.device_mesh import init_device_mesh - - world_size = dist.get_world_size() - rank = dist.get_rank() - - # Use context_parallel_size directly (defaults to 1 for pure DP) - self.cp_size = self.args.context_parallel_size - self.dp_size = world_size // self.cp_size - - # Create 2D device mesh: (dp_size, cp_size) - # Ranks laid out in row-major: mesh[dp_idx, cp_idx] = dp_idx * cp_size + cp_idx - # - CP groups: consecutive ranks along dim 1, e.g., [0,1], [2,3], [4,5], [6,7] - # - DP groups: striped ranks along dim 0, e.g., [0,2,4,6], [1,3,5,7] - # When cp_size=1, this degenerates to pure DP - self.mesh = init_device_mesh("cuda", mesh_shape=(self.dp_size, self.cp_size), mesh_dim_names=("dp", "cp")) - - # Extract process groups from mesh - self.dp_group = self.mesh.get_group("dp") # For FSDP gradient sync, metric reduction - self.cp_group = self.mesh.get_group("cp") # For Ring Flash Attention, logit gathering - self.dp_mesh = self.mesh["dp"] # For FSDP - - # Compute local ranks within each dimension - self.dp_rank = rank // self.cp_size - self.cp_rank = rank % self.cp_size - - logger.info( - f"[Rank {rank}] Device mesh (2D): world_size={world_size}, " - f"cp_size={self.cp_size}, dp_size={self.dp_size}" - ) - logger.info(f"[Rank {rank}] Mesh shape: {self.mesh.shape}, " f"dp_rank={self.dp_rank}, cp_rank={self.cp_rank}") - - # Setup Ring Flash Attention with CP group from mesh (only when cp_size > 1) - if self.cp_size > 1: - substitute_hf_flash_attn(self.cp_group, heads_k_stride=1) - logger.info(f"[Rank {rank}] CP initialized via device mesh") - else: - logger.info(f"[Rank {rank}] Pure DP mode (cp_size=1)") - def _get_init_weight_context_manager(self): """Get context manager for model initialization. @@ -336,22 +286,20 @@ def save_model(self, rollout_id: int, force_sync: bool = False) -> None: def _compute_log_prob( self, model_tag: str, - packed_batches: list[dict[str, torch.Tensor]], + data_iterator: DataIterator, + num_microbatches: list[int], store_prefix: str = "", ) -> dict[str, list[torch.Tensor]]: - """Compute token log-probabilities for a list of packed batches. + """Compute token log-probabilities using data iterator. Parameters: model_tag: Which parameters to use, e.g. "actor" or "ref". - packed_batches: A list of packed batch dictionaries produced by - `pack_sequences`, each containing at least `tokens` and - `position_ids`; may also include multimodal keys like `pixel_values`. + data_iterator: DataIterator providing micro-batches. + num_microbatches: List of number of microbatches per step. store_prefix: Prefix to use for keys in outputs (e.g., "ref_"). Returns: - A lightweight dictionary keyed by f"{store_prefix}log_probs". The - actual per-sequence results are written in-place into each element of - `packed_batches` under the same key and can be read back by callers. + A lightweight dictionary keyed by f"{store_prefix}log_probs". Note: Uses separate ref model when model_tag == "ref". The ref model is @@ -370,26 +318,59 @@ def _compute_log_prob( active_model = self.model try: - rollout_data = {f"{store_prefix}log_probs": []} + forward_data_store = [] + data_iterator.reset() + with timer(f"{store_prefix}log_probs"), torch.no_grad(): - for batch in self.prof.iterate_train_log_probs( - tqdm(packed_batches, desc=f"{store_prefix}log_probs", disable=dist.get_rank() != 0) - ): - model_args = self._get_model_inputs_args(batch) - logits = active_model(**model_args).logits.squeeze(0).float() - log_probs_result, entropy_result = get_logprob_and_entropy_with_cp( - logits=logits, - target_tokens=batch["tokens"], - cp_rank=self.cp_rank, - cp_size=self.cp_size, - cp_group=self.cp_group, - model_input_ids=model_args["input_ids"], - allow_compile=not self.args.true_on_policy_mode, - temperature=self.args.rollout_temperature, - ) - batch[f"{store_prefix}log_probs"] = log_probs_result - if store_prefix == "": - batch["entropy"] = entropy_result + num_steps_per_rollout = len(num_microbatches) + for step_id in range(num_steps_per_rollout): + for _ in self.prof.iterate_train_log_probs( + tqdm( + range(num_microbatches[step_id]), + desc=f"{store_prefix}log_probs", + disable=dist.get_rank() != 0, + ) + ): + forward_only_keys = [ + "tokens", + "loss_masks", + "multimodal_train_inputs", + "total_lengths", + "response_lengths", + "max_seq_lens", + ] + batch = get_batch( + data_iterator, + forward_only_keys, + self.parallel_state, + self.args.data_pad_size_multiplier, + self.args.qkv_format, + get_position_ids=True, + ) + + model_args = self._get_model_inputs_args(batch) + logits = active_model(**model_args).logits.float() + + result = get_log_probs_and_entropy( + logits=logits, + args=self.args, + parallel_state=self.parallel_state, + unconcat_tokens=batch["unconcat_tokens"], + total_lengths=batch["total_lengths"], + response_lengths=batch["response_lengths"], + with_entropy=(store_prefix == ""), + max_seq_lens=batch.get("max_seq_lens", None), + ) + + batch_result = { + f"{store_prefix}log_probs": result["log_probs"], + } + if store_prefix == "" and "entropy" in result: + batch_result["entropy"] = result["entropy"] + forward_data_store.append(batch_result) + + rollout_data = aggregate_forward_results(forward_data_store, data_iterator, self.args, store_prefix) + return rollout_data finally: @@ -402,80 +383,6 @@ def _compute_log_prob( self.model.cuda() dist.barrier(group=get_gloo_group()) - def _packed_data( - self, rollout_data: dict[str, list[torch.Tensor]] - ) -> tuple[list[dict[str, torch.Tensor]], list[int]]: - """Pack variable-length sequences for efficient processing. - - Parameters: - rollout_data: Dictionary of lists containing sequence-level tensors - such as `tokens`, `loss_masks`, `rewards`, `response_lengths`, - `advantages`, `returns`, and optional `rollout_log_probs`. - - Returns: - A pair `(packed_batches, grad_accum)` where `packed_batches` is a list - of packed batch dictionaries and `grad_accum` lists the micro-batch - indices at which to perform optimizer steps. - """ - # Pack sequences efficiently - tokens = rollout_data["tokens"] - - packed_batches = [] - mbs_size_list = [] - local_batch_size = self.args.global_batch_size // self.dp_size - assert ( - self.args.global_batch_size % self.dp_size == 0 - ), f"global_batch_size {self.args.global_batch_size} is not divisible by dp_world_size {self.dp_size}" - # Use global_batch_size for splitting when max_tokens_per_gpu is enabled - if self.args.use_dynamic_batch_size: - # In CP mode, CP group shares sequences, so total capacity is max_tokens_per_gpu * cp_size - max_tokens = self.args.max_tokens_per_gpu - if self.cp_size > 1: - max_tokens = max_tokens * self.cp_size - - for i in range(0, len(tokens), local_batch_size): - mbs_size_list.append( - get_minimum_num_micro_batch_size( - [len(t) for t in rollout_data["tokens"][i : i + local_batch_size]], - max_tokens, - ) - ) - num_microbatches = torch.tensor(mbs_size_list, dtype=torch.int, device=torch.cuda.current_device()) - dist.all_reduce(num_microbatches, op=dist.ReduceOp.MAX, group=self.dp_group) - num_microbatches = num_microbatches.tolist() - else: - num_microbatches = [self.args.global_batch_size // (self.args.micro_batch_size * self.dp_size)] * ( - len(tokens) // local_batch_size - ) - - start = 0 - for mbs_size in num_microbatches: - end = start + local_batch_size - packed_batches.extend( - pack_sequences( - rollout_data["tokens"][start:end], - rollout_data["loss_masks"][start:end], - rollout_data["rewards"][start:end], - rollout_data["raw_reward"][start:end], - rollout_data["response_lengths"][start:end], - rollout_data["advantages"][start:end], - rollout_data["returns"][start:end], - rollout_log_probs=( - rollout_data["rollout_log_probs"][start:end] if "rollout_log_probs" in rollout_data else None - ), - multimodal_train_inputs=( - rollout_data["multimodal_train_inputs"][start:end] - if "multimodal_train_inputs" in rollout_data - else None - ), - num_packs=mbs_size, - ) - ) - start = end - grad_accum = list(accumulate(num_microbatches)) - - return packed_batches, grad_accum - def train(self, rollout_id: int, rollout_data_ref: Box) -> None: """Run one training update over a rollout batch. @@ -491,7 +398,7 @@ def train(self, rollout_id: int, rollout_data_ref: Box) -> None: self.wake_up() with inverse_timer("train_wait"), timer("train"): - rollout_data = process_rollout_data(self.args, rollout_data_ref, self.dp_rank, self.dp_size) + rollout_data = get_rollout_data(self.args, rollout_data_ref, self.parallel_state) if self.args.debug_rollout_only: return self._train_core(rollout_id=rollout_id, rollout_data=rollout_data) @@ -503,79 +410,101 @@ def train(self, rollout_id: int, rollout_data_ref: Box) -> None: compute_total_fwd_flops=None, ) - def _log_rollout_data(self, rollout_id: int, rollout_data, packed_batches): - log_dict = {} - if "raw_reward" in rollout_data and dist.get_rank() == 0: - raw_reward_list = rollout_data["raw_reward"] - if raw_reward_list: - log_dict["rollout/raw_reward"] = sum(raw_reward_list) / len(raw_reward_list) - - for metric_key in ["log_probs", "rollout_log_probs", "ref_log_probs", "advantages", "returns"]: - if metric_key not in packed_batches[0]: - continue - val = torch.tensor([0.0], device=torch.cuda.current_device()) - for _mbs_id, batches in enumerate(packed_batches): - unpacked_batches = unpack_sequences(batches) - for unpacked_batch in unpacked_batches: - if isinstance(unpacked_batch[metric_key], torch.Tensor): - loss_masks_tensor = unpacked_batch["loss_masks"].to(device=torch.cuda.current_device()) - metric_tensor = unpacked_batch[metric_key].to(device=torch.cuda.current_device()) - val += (metric_tensor * loss_masks_tensor).sum() / loss_masks_tensor.sum().clamp_min(1) - else: - val += unpacked_batch[metric_key] - dist.all_reduce(val, op=dist.ReduceOp.SUM, group=self.dp_group) - log_dict[f"rollout/{metric_key}"] = ( - val / (self.args.n_samples_per_prompt * self.args.rollout_batch_size) - ).item() - if dist.get_rank() == 0: - logger.info(f"rollout {rollout_id}: {log_dict}") - log_dict["rollout/step"] = compute_rollout_step(self.args, rollout_id) - tracking_utils.log(self.args, log_dict, step_key="rollout/step") - - if self.args.ci_test and self.args.true_on_policy_mode: - assert log_dict["rollout/log_probs"] == log_dict["rollout/rollout_log_probs"], ( - f"CI check failed: true_on_policy_mode is enabled, but log_probs " - f"({log_dict['rollout/log_probs']}) != rollout_log_probs " - f"({log_dict['rollout/rollout_log_probs']})" - ) - def _train_core(self, rollout_id: int, rollout_data) -> None: - if self.args.advantage_estimator in ["grpo", "gspo"]: - rollout_data["advantages"] = rollout_data["returns"] = [ - torch.tensor([rollout_data["rewards"][i]] * rollout_data["response_lengths"][i]) - for i in range(len(rollout_data["rewards"])) - ] - else: - raise NotImplementedError(f"Unsupported advantage_estimator {self.args.advantage_estimator}") - - packed_batches, grad_accum = self._packed_data(rollout_data) + data_iterator, num_microbatches = get_data_iterator(self.args, self.model, self.parallel_state, rollout_data) + data_iterator = data_iterator[0] assert ( - len(grad_accum) > 0 - ), f"Invalid grad_accum {grad_accum} for micro_batch_size {self.args.micro_batch_size} and global_batch_size {self.args.global_batch_size}" + len(num_microbatches) > 0 + ), f"Invalid num_microbatches {num_microbatches} for micro_batch_size {self.args.micro_batch_size} and global_batch_size {self.args.global_batch_size}" if self.ref_model is not None: - self._compute_log_prob("ref", packed_batches, store_prefix="ref_") + ref_results = self._compute_log_prob("ref", data_iterator, num_microbatches, store_prefix="ref_") + rollout_data.update(ref_results) + + actor_results = self._compute_log_prob("actor", data_iterator, num_microbatches) + rollout_data.update(actor_results) - self._compute_log_prob("actor", packed_batches) - self._log_rollout_data(rollout_id, rollout_data, packed_batches) + compute_advantages_and_returns(self.args, self.parallel_state, rollout_data) + + log_rollout_data(rollout_id, self.args, rollout_data, self.parallel_state) with timer("actor_train"): - reported_accum: dict[str, list[torch.Tensor]] = {} - self.optimizer.zero_grad(set_to_none=True) - for mbs_id, packed_batch in self.prof.iterate_train_actor( - enumerate(tqdm(packed_batches, desc="actor_train", disable=dist.get_rank() != 0)) - ): - self._train_step( - packed_batch=packed_batch, - reported_accum=reported_accum, - mbs_id=mbs_id, - grad_accum=grad_accum, + data_iterator.reset() + num_steps_per_rollout = len(num_microbatches) + + for step_id in range(num_steps_per_rollout): + self.optimizer.zero_grad(set_to_none=True) + + losses_reduced = [] + for _ in self.prof.iterate_train_actor( + tqdm(range(num_microbatches[step_id]), desc="actor_train", disable=dist.get_rank() != 0) + ): + batch = get_batch( + data_iterator, + [ + "tokens", + "loss_masks", + "multimodal_train_inputs", + "total_lengths", + "response_lengths", + "max_seq_lens", + "log_probs", + "advantages", + "returns", + "ref_log_probs", + "rollout_log_probs", + ], + self.parallel_state, + self.args.data_pad_size_multiplier, + self.args.qkv_format, + get_position_ids=True, + ) + + log_dict = self._train_step( + batch=batch, + step_id=step_id, + num_microbatches=num_microbatches[step_id], + ) + losses_reduced.append(log_dict) + + grad_norm = torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.clip_grad) + grad_norm = grad_norm.full_tensor().item() + + self.optimizer.step() + self.lr_scheduler.step() + + if self.args.ci_test: + check_grad_norm( + args=self.args, + grad_norm=grad_norm, + rollout_id=rollout_id, + step_id=step_id, + role="actor", + rank=self.parallel_state.dp_cp_rank, + ) + + loss_dict = aggregate_train_losses(losses_reduced, self.parallel_state) + + extra_metrics = {} + for param_group_id, param_group in enumerate(self.optimizer.param_groups): + extra_metrics[f"lr-pg_{param_group_id}"] = param_group["lr"] + + log_train_step( + args=self.args, + loss_dict=loss_dict, + grad_norm=grad_norm, + rollout_id=rollout_id, + step_id=step_id, + num_steps_per_rollout=num_steps_per_rollout, + role="actor", + extra_metrics=extra_metrics, ) self.prof.step(rollout_id=rollout_id) - train_dump_utils.save_debug_train_data(self.args, rollout_id=rollout_id, rollout_data=rollout_data) + if self.args.save_debug_train_data is not None: + train_dump_utils.save_debug_train_data(self.args, rollout_id=rollout_id, rollout_data=rollout_data) # Update ref model if needed (copy actor weights to ref) if ( @@ -590,217 +519,23 @@ def _train_core(self, rollout_id: int, rollout_data) -> None: self.ref_model.load_state_dict(actor_state) self.ref_model.cpu() - def _train_step(self, packed_batch, reported_accum, mbs_id, grad_accum): + def _train_step(self, batch, step_id, num_microbatches): # Prepare model inputs - model_args = self._get_model_inputs_args(packed_batch) - logits = self.model(**model_args).logits.squeeze(0).float() + model_args = self._get_model_inputs_args(batch) + logits = self.model(**model_args).logits.float() - # Compute log probs and entropy (unified for both CP and non-CP modes) - log_probs, entropy_result = get_logprob_and_entropy_with_cp( + loss, normalizer, log_dict = loss_function( + args=self.args, + parallel_state=self.parallel_state, + batch=batch, + num_microbatches=num_microbatches, logits=logits, - target_tokens=packed_batch["tokens"], - cp_rank=self.cp_rank, - cp_size=self.cp_size, - cp_group=self.cp_group, - model_input_ids=model_args["input_ids"], - allow_compile=not self.args.true_on_policy_mode, - temperature=self.args.rollout_temperature, - ) - packed_batch["cur_log_probs"] = log_probs - packed_batch["entropy"] = entropy_result - - unpacked_batches = unpack_sequences(packed_batch) - - old_log_prob_key = "rollout_log_probs" if self.args.use_rollout_logprobs else "log_probs" - missing_old_log_probs = [ - idx - for idx, batch in enumerate(unpacked_batches) - if old_log_prob_key not in batch or not isinstance(batch[old_log_prob_key], torch.Tensor) - ] - if missing_old_log_probs: - raise KeyError( - f"{old_log_prob_key} must be provided as torch.Tensor for all microbatches when " - f"use_rollout_logprobs is set to {self.args.use_rollout_logprobs}. Missing in batches: {missing_old_log_probs}" - ) - old_log_probs = torch.cat([batch[old_log_prob_key] for batch in unpacked_batches], dim=0) - log_probs = torch.cat([batch["cur_log_probs"] for batch in unpacked_batches], dim=0) - advantages = torch.cat([batch["advantages"] for batch in unpacked_batches], dim=0) - loss_masks = [batch["loss_masks"].to(device=log_probs.device) for batch in unpacked_batches] - response_lengths = [batch["response_lengths"] for batch in unpacked_batches] - - advantages = advantages.to(device=log_probs.device) - old_log_probs = old_log_probs.to(device=log_probs.device) - ppo_kl = old_log_probs - log_probs - - if self.args.use_opsm: - opsm_mask, opsm_clipfrac = compute_opsm_mask( - args=self.args, - full_log_probs=[batch["cur_log_probs"] for batch in unpacked_batches], - full_old_log_probs=[batch[old_log_prob_key] for batch in unpacked_batches], - advantages=[batch["advantages"] for batch in unpacked_batches], - loss_masks=loss_masks, - ) - - if self.args.advantage_estimator == "gspo": - ppo_kl = compute_gspo_kl( - full_log_probs=[batch["cur_log_probs"] for batch in unpacked_batches], - full_old_log_probs=[batch[old_log_prob_key] for batch in unpacked_batches], - local_log_probs=[batch["cur_log_probs"] for batch in unpacked_batches], - loss_masks=loss_masks, - ) - - pg_loss, pg_clipfrac = compute_policy_loss(ppo_kl, advantages, self.args.eps_clip, self.args.eps_clip_high) - - if self.args.use_opsm: - pg_loss = pg_loss * opsm_mask - - def _has_rollout_log_probs(batch) -> bool: - rollout_tensor = batch.get("rollout_log_probs") - return isinstance(rollout_tensor, torch.Tensor) and rollout_tensor.numel() > 0 - - has_rollout_log_probs = all(_has_rollout_log_probs(batch) for batch in unpacked_batches) - rollout_log_probs = ( - torch.cat([batch["rollout_log_probs"] for batch in unpacked_batches], dim=0) - if has_rollout_log_probs - else None + apply_megatron_loss_scaling=False, ) - # Apply off-policy correction using importance sampling if enabled - if self.args.use_tis: - assert ( - has_rollout_log_probs and rollout_log_probs is not None - ), "rollout_log_probs must be provided as non-empty torch.Tensor for TIS/MIS" - - train_log_probs_list = list(log_probs.split(response_lengths, dim=0)) - rollout_log_probs_list = list(rollout_log_probs.split(response_lengths, dim=0)) - ois = (-ppo_kl).exp() - tis_kwargs = { - "args": self.args, - "pg_loss": pg_loss, - "train_log_probs": train_log_probs_list, - "rollout_log_probs": rollout_log_probs_list, - "loss_masks": loss_masks, - "response_lengths": response_lengths, - "cp_rank": self.cp_rank, - "cp_size": self.cp_size, - "cp_group": self.cp_group, - } - - if self.args.custom_tis_function_path is not None: - tis_func = load_function(self.args.custom_tis_function_path) - else: - tis_func = vanilla_tis_function - pg_loss, loss_masks, tis_metrics = tis_func(**tis_kwargs) - - if self.args.calculate_per_token_loss: - pg_loss = sum_of_token(pg_loss, response_lengths, loss_masks) - pg_clipfrac = sum_of_token(pg_clipfrac, response_lengths, loss_masks) - ppo_kl = sum_of_token(ppo_kl.abs(), response_lengths, loss_masks) - else: - pg_loss = sum_of_sample_mean(pg_loss, response_lengths, loss_masks) - pg_clipfrac = sum_of_sample_mean(pg_clipfrac, response_lengths, loss_masks) - ppo_kl = sum_of_sample_mean(ppo_kl.abs(), response_lengths, loss_masks) - - # Only compare rollout vs. train log probs when they originate from different stages. - train_rollout_logprob_abs_diff = None - if not self.args.use_rollout_logprobs and rollout_log_probs is not None: - train_rollout_logprob_abs_diff = (old_log_probs - rollout_log_probs).abs() - train_rollout_logprob_abs_diff = sum_of_sample_mean( - train_rollout_logprob_abs_diff, response_lengths, loss_masks - ).detach() - - entropy = torch.cat([batch["entropy"] for batch in unpacked_batches], dim=0) - entropy_loss = sum_of_sample_mean(entropy, response_lengths, loss_masks) - - loss = pg_loss - self.args.entropy_coef * entropy_loss - - if self.args.use_kl_loss: - ref_log_probs = torch.cat([batch["ref_log_probs"] for batch in unpacked_batches], dim=0) - importance_ratio = None - if self.args.use_unbiased_kl: - importance_ratio = torch.exp(log_probs - old_log_probs) - kl = compute_approx_kl( - log_probs, - ref_log_probs, - kl_loss_type=self.args.kl_loss_type, - importance_ratio=importance_ratio, - ) - kl_loss = sum_of_sample_mean(kl, response_lengths, loss_masks) - - loss = loss + self.args.kl_loss_coef * kl_loss - - reported = { - "loss": loss.detach(), - "pg_loss": pg_loss.detach(), - "pg_clipfrac": pg_clipfrac.detach(), - "ppo_kl": ppo_kl.detach(), - "entropy_loss": entropy_loss.detach(), - } - - if train_rollout_logprob_abs_diff is not None: - reported["train_rollout_logprob_abs_diff"] = train_rollout_logprob_abs_diff - - if self.args.use_kl_loss: - reported["kl_loss"] = kl_loss.detach() - - if self.args.use_opsm: - reported["opsm_clipfrac"] = opsm_clipfrac - - if self.args.use_tis and tis_metrics: - reported["ois"] = sum_of_sample_mean(ois, response_lengths, loss_masks).detach() - for k, v in tis_metrics.items(): - if self.args.calculate_per_token_loss: - reported[k] = sum_of_token(v, response_lengths, loss_masks).detach() - else: - reported[k] = sum_of_sample_mean(v, response_lengths, loss_masks).detach() - - # Scale loss for gradient accumulation - loss = loss * self.dp_size / self.args.global_batch_size loss.backward() - # Accumulate reported metrics (store tensors for later mean) - for k, v in reported.items(): - reported_accum.setdefault(k, []).append(v) - - if (mbs_id + 1) in grad_accum: - # TODO: check if the grad norm is global grad norm. - grad_norm = torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.clip_grad) - # the grad norm used to be of DTensor - grad_norm = float(grad_norm) - - self.optimizer.step() - # Update learning rate - self.lr_scheduler.step() - self.optimizer.zero_grad(set_to_none=True) - # Aggregate logs - aggregated = {k: torch.stack(v).sum().item() for k, v in reported_accum.items()} - # TODO: change this, this is slow. - reduced_aggregated = [None] * self.dp_size - dist.all_gather_object(reduced_aggregated, aggregated, group=self.dp_group) - aggregated = {} - for k in reported_accum.keys(): - aggregated[k] = sum([r[k] for r in reduced_aggregated]) / (self.args.global_batch_size) - reported_accum.clear() - if dist.get_rank() == 0: - log_dict = { - f"train/{k}": (val.item() if torch.is_tensor(val) else val) for k, val in aggregated.items() - } - log_dict["train/grad_norm"] = grad_norm - - # Log learning rate per parameter group; use scheduler's last computed LRs - lr_values = self.lr_scheduler.get_last_lr() - for gid, _group in enumerate(self.optimizer.param_groups): - log_dict[f"train/lr-pg_{gid}"] = lr_values[gid] - - kl_info = "" - if self.args.use_kl_loss and "kl_loss" in aggregated: - kl_info = f", kl_loss: {aggregated['kl_loss']:.4f}, kl_penalty: {aggregated['kl_loss'] * self.args.kl_loss_coef:.4f}" - logger.info(kl_info) - logger.info(f"step {self.global_step}: {log_dict}") - - log_dict["train/step"] = self.global_step - tracking_utils.log(self.args, log_dict, step_key="train/step") - self.global_step += 1 + return log_dict @timer def update_weights(self) -> None: # type: ignore[override] @@ -866,203 +601,40 @@ def _create_ref_model(self, ref_load_path: str | None): full_state = ref_model.state_dict() # Always use CPUOffloadPolicy for reference, let FSDP2 handle the offload. It is faster than model.cpu(). - ref_model = apply_fsdp2(ref_model, mesh=self.dp_mesh, cpu_offload=True, args=self.args) - ref_model = self._fsdp2_load_full_state_dict(ref_model, full_state, self.dp_mesh, cpu_offload=True) + ref_model = apply_fsdp2(ref_model, mesh=self.parallel_state.dp_mesh, cpu_offload=True, args=self.args) + ref_model = self._fsdp2_load_full_state_dict( + ref_model, full_state, self.parallel_state.dp_mesh, cpu_offload=True + ) logger.info(f"[Rank {dist.get_rank()}] Reference model created with FSDP2 CPUOffloadPolicy") return ref_model else: raise NotImplementedError(f"Loading from checkpoint file {ref_load_path} not yet implemented") - def _get_model_inputs_args(self, packed_sequence: dict) -> dict: - input_ids = packed_sequence["tokens"].unsqueeze(0) - position_ids = packed_sequence["position_ids"].unsqueeze(0) - if self.cp_size > 1: - - packed_sequence = pad_packed_sequence_with_cp(packed_sequence, self.cp_size) + def _get_model_inputs_args(self, batch: dict) -> dict: + input_ids = batch["tokens"] + position_ids = batch["position_ids"] - if not packed_sequence["cu_seqlens"].is_cuda: - packed_sequence["cu_seqlens"] = packed_sequence["cu_seqlens"].cuda() - cu_seqlens = packed_sequence["cu_seqlens"] - update_ring_flash_attn_params(cu_seqlens, self.cp_group) + if self.parallel_state.cp_size > 1: + if "cu_seqlens" in batch: + cu_seqlens = batch["cu_seqlens"] + if not cu_seqlens.is_cuda: + cu_seqlens = cu_seqlens.cuda() + update_ring_flash_attn_params(cu_seqlens, self.cp_group) - input_ids = torch.chunk(packed_sequence["tokens"].unsqueeze(0), self.cp_size, dim=1)[self.cp_rank] - position_ids = torch.chunk(packed_sequence["position_ids"].unsqueeze(0), self.cp_size, dim=1)[self.cp_rank] + input_ids = torch.chunk(input_ids, self.parallel_state.cp_size, dim=1)[self.parallel_state.cp_rank] + position_ids = torch.chunk(position_ids, self.parallel_state.cp_size, dim=1)[self.parallel_state.cp_rank] model_args = { "input_ids": input_ids, "position_ids": position_ids, "attention_mask": None, } - if packed_sequence.get("multimodal_train_inputs"): - model_args.update(packed_sequence["multimodal_train_inputs"]) - return model_args - - -def selective_log_softmax_raw(logits: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor: - """Fused version of the common `log_softmax -> gather` operation. - - The fused version of this operation avoids the (potentially large) memory overhead - of allocating a new tensor to store the full logprobs. - - Parameters: - logits: Tensor of shape [..., V] containing model logits. - input_ids: Tensor of shape [...] of token indices whose log-probabilities are gathered. - - Returns: - Tensor of shape [...] containing the log-probabilities corresponding to `input_ids`. - """ - logprobs = logits.log_softmax(dim=-1) - return torch.gather(logprobs, dim=-1, index=input_ids.unsqueeze(-1)).squeeze(-1) - -selective_log_softmax_compiled = torch.compile(dynamic=True)(selective_log_softmax_raw) + if batch.get("multimodal_train_inputs"): + model_args.update(batch["multimodal_train_inputs"]) - -def gather_log_probs_packed( - shifted_logits: torch.Tensor, - input_ids: torch.Tensor, - allow_compile: bool, - cu_seqlens: torch.Tensor | float | None = None, - temperature: torch.Tensor | None = None, -) -> torch.Tensor: - """Gather next-token log probabilities for packed sequences. - - Parameters: - logits: Model logits of shape [B, T, V] or [T, V]. - input_ids: Token ids of shape [B, T] or [T]. - cu_seqlens: Optional cumulative sequence lengths (unused here). Present - for API compatibility with callers. - - Returns: - A tensor of shape [T-1] (or [B, T-1]) with log-probabilities of targets. - """ - # Handle batch dimension - logits should be [batch_size, seq_len, vocab_size] - if shifted_logits.dim() == 3: - # Remove batch dimension for packed sequences - shifted_logits = shifted_logits.squeeze(0) - input_ids = input_ids.squeeze(0) - - if temperature is not None: - shifted_logits = shifted_logits.div(temperature) - - targets = input_ids[1:].to(device=shifted_logits.device) - - # Gather log probs for targets - selective_log_softmax = selective_log_softmax_compiled if allow_compile else selective_log_softmax_raw - return selective_log_softmax(shifted_logits, targets) - - -def get_logprob_and_entropy_with_cp( - logits: torch.Tensor, - target_tokens: torch.Tensor, - cp_rank: int, - cp_size: int, - cp_group, - model_input_ids: torch.Tensor, - allow_compile: bool, - temperature: float | None = None, -) -> tuple[torch.Tensor, torch.Tensor]: - """Compute log probabilities and entropy in Context Parallel mode. - - Parameters: - logits: Model output logits with shape [chunk_size, vocab_size] - target_tokens: Target tokens with shape [total_seq_len] - cp_rank: Current CP rank - cp_size: CP world size - cp_group: CP communication group - model_input_ids: Model input_ids (used for the last rank) - allow_compile: Whether to allow compilation - temperature: Temperature parameter (optional) - - Returns: - log_probs: Aggregated log probabilities with shape [total_seq_len - 1] - entropy: Aggregated entropy with shape [total_seq_len - 1] - """ - # Fast path for non-CP mode (cp_size=1): avoid unnecessary communication - if cp_size == 1: - shifted_logits = logits[:-1, :] - local_log_probs = gather_log_probs_packed( - shifted_logits, target_tokens, allow_compile=allow_compile, temperature=temperature - ) - log_probs_full = torch.log_softmax(shifted_logits, dim=-1) - probs = torch.softmax(shifted_logits, dim=-1) - entropy = -(probs * log_probs_full).sum(dim=-1) - return local_log_probs, entropy - - chunk_size = logits.shape[0] - tokens_start_index = chunk_size * cp_rank - tokens_end_index = ( - tokens_start_index + chunk_size + 1 if cp_rank < cp_size - 1 else tokens_start_index + chunk_size - ) - - # For the last rank, remove the last logit - logits = logits if cp_rank < cp_size - 1 else logits[:-1, :] - - # Get local tokens for current rank - local_tokens = ( - target_tokens[tokens_start_index:tokens_end_index] if cp_rank < cp_size - 1 else model_input_ids.squeeze(0) - ) - - # Compute local log probs - local_log_probs = gather_log_probs_packed( - logits, local_tokens, allow_compile=allow_compile, temperature=temperature - ) - - # Pad for the last rank - if cp_rank == cp_size - 1: - local_log_probs = F.pad(local_log_probs, (0, chunk_size - local_log_probs.shape[0]), value=0) - - # Compute entropy - shifted_logits = logits[:-1, :] if cp_rank == cp_size - 1 else logits - log_probs_full = torch.log_softmax(shifted_logits, dim=-1) - probs = torch.softmax(shifted_logits, dim=-1) - entropy = -(probs * log_probs_full).sum(dim=-1) - - # Pad entropy for the last rank - if cp_rank == cp_size - 1: - entropy = F.pad(entropy, (0, chunk_size - entropy.shape[0]), value=0) - - # Merge with a single all_gather: stack as [2, chunk_size] - stacked_local = torch.stack([local_log_probs, entropy], dim=0) - gathered_stacked = torch.distributed.nn.functional.all_gather(stacked_local, group=cp_group) - - # Concatenate by effective length (non-last rank=chunk_size, last rank=chunk_size-1) - lp_parts, ent_parts = [], [] - for r in range(cp_size): - eff_len = chunk_size if r < cp_size - 1 else max(0, chunk_size - 1) - if eff_len > 0: - lp_parts.append(gathered_stacked[r][0][:eff_len]) - ent_parts.append(gathered_stacked[r][1][:eff_len]) - - log_probs = torch.cat(lp_parts, dim=0) if lp_parts else local_log_probs.new_zeros((0,)) - entropy_result = torch.cat(ent_parts, dim=0) if ent_parts else entropy.new_zeros((0,)) - - # Truncate to global effective length T-1 (packed tokens length is T) - log_probs = log_probs[: len(target_tokens) - 1] - entropy_result = entropy_result[: len(target_tokens) - 1] - - return log_probs, entropy_result - - -def sum_of_sample_mean(x: torch.Tensor, response_lengths: list[int], loss_masks: list[torch.Tensor]) -> torch.Tensor: - """Compute sum of per-sample means across variable-length responses. - - Parameters: - x: Flat tensor containing concatenated per-token values across samples. - response_lengths: Lengths of each sample's response segment in `x`. - loss_masks: Per-sample masks aligned with `response_lengths`. - - Returns: - A scalar tensor equal to the sum over samples of the mean value within - each sample's response segment. - """ - return sum( - [ - (x_i * loss_mask_i).sum() / torch.clamp_min(loss_mask_i.sum(), 1) - for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False) - ] - ) + return model_args @torch.no_grad() @@ -1133,12 +705,3 @@ def apply_fsdp2(model, mesh=None, cpu_offload=False, args=None): fully_shard(model, **fsdp_kwargs) return model - - -def sum_of_token(x: torch.Tensor, response_lengths: list[int], loss_masks: list[torch.Tensor]) -> torch.Tensor: - return sum( - [ - (x_i * loss_mask_i).sum() - for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False) - ] - ) diff --git a/miles/backends/fsdp_utils/parallel.py b/miles/backends/fsdp_utils/parallel.py new file mode 100644 index 000000000..d87aa2b6e --- /dev/null +++ b/miles/backends/fsdp_utils/parallel.py @@ -0,0 +1,58 @@ +import logging +from argparse import Namespace + +import torch.distributed as dist +from ring_flash_attn import substitute_hf_flash_attn +from torch.distributed.device_mesh import init_device_mesh + +from miles.utils.distributed_utils import get_gloo_group + +from ..training_utils.parallel import ParallelState + +logger = logging.getLogger(__name__) + + +def create_fsdp_parallel_state(args: Namespace) -> ParallelState: + """Create a ParallelState instance for FSDP configuration.""" + world_size = dist.get_world_size() + rank = dist.get_rank() + + cp_size = args.context_parallel_size + dp_rank = rank // cp_size + cp_rank = rank % cp_size + + mesh = init_device_mesh("cuda", mesh_shape=(world_size // cp_size, cp_size), mesh_dim_names=("dp", "cp")) + + logger.info( + f"[Rank {rank}] Device mesh (2D): world_size={world_size}, " + f"cp_size={cp_size}, dp_size={world_size // cp_size}" + ) + logger.info(f"[Rank {rank}] Mesh shape: {mesh.shape}, " f"dp_rank={dp_rank}, cp_rank={cp_rank}") + + # Setup Ring Flash Attention with CP group from mesh (only when cp_size > 1) + if cp_size > 1: + substitute_hf_flash_attn(mesh.get_group("cp"), heads_k_stride=1) + logger.info(f"[Rank {rank}] CP initialized via device mesh") + else: + logger.info(f"[Rank {rank}] Pure DP mode (cp_size=1)") + + parallel_state = ParallelState( + dp_rank=dp_rank, + dp_src_rank=dp_rank // world_size, + dp_size=world_size // cp_size, + cp_rank=cp_rank, + cp_size=cp_size, + dp_cp_rank=rank, + dp_cp_size=world_size, + dp_group=mesh.get_group("dp"), + dp_cp_group=dist.group.WORLD, + dp_cp_group_gloo=get_gloo_group(), + cp_group=mesh.get_group("cp"), + tp_size=1, + tp_rank=0, + tp_group=dist.new_group([rank]), + ) + + parallel_state.dp_mesh = mesh["dp"] + + return parallel_state diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index 1e6af26b7..f19616487 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -16,7 +16,6 @@ from miles.ray.train_actor import TrainRayActor from miles.utils import train_dump_utils from miles.utils.context_utils import with_defer -from miles.utils.data import process_rollout_data from miles.utils.distributed_utils import get_gloo_group, init_process_group from miles.utils.memory_utils import clear_memory, print_memory from miles.utils.ray_utils import Box @@ -28,12 +27,14 @@ from ...utils.profile_utils import TrainProfiler from ...utils.tensor_backper import TensorBackuper +from ..training_utils.cp_utils import slice_with_cp +from ..training_utils.data import DataIterator, get_data_iterator, get_rollout_data, sync_actor_critic_data +from ..training_utils.log_utils import log_perf_data, log_rollout_data +from ..training_utils.loss import compute_advantages_and_returns, get_log_probs_and_entropy, get_values from .checkpoint import load_checkpoint -from .cp_utils import slice_log_prob_with_cp, slice_with_cp -from .data import DataIterator, get_data_iterator, log_perf_data, log_rollout_data, sync_actor_critic_data from .initialize import init, is_megatron_main_rank -from .loss import compute_advantages_and_returns, get_log_probs_and_entropy, get_values from .model import forward_only, initialize_model_and_optimizer, save, train +from .parallel import create_megatron_parallel_state from .update_weight.common import named_params_and_buffers from .update_weight.update_weight_from_distributed import UpdateWeightFromDistributed from .update_weight.update_weight_from_tensor import UpdateWeightFromTensor @@ -92,6 +93,8 @@ def init( args, role ) + self.parallel_state = create_megatron_parallel_state(model=self.model) + if role == "critic": if self.args.offload_train: self.sleep() @@ -176,72 +179,6 @@ def wake_up(self) -> None: reload_process_groups() print_memory("after wake_up model") - def _get_rollout_data(self, rollout_data_ref: Box) -> RolloutBatch: - # Fetch data through ray on CPU, not sure if this will be performance bottleneck. - # Both first pp stage and the last pp stage will receive the data. - rollout_data = process_rollout_data( - self.args, - rollout_data_ref, - mpu.get_data_parallel_rank(with_context_parallel=False), - mpu.get_data_parallel_world_size(with_context_parallel=False), - ) - # TODO: this is ugly, move to somewhere else? - # move tokens to GPU in advance - rollout_data["tokens"] = [ - torch.tensor(t, dtype=torch.long, device=torch.cuda.current_device()) for t in rollout_data["tokens"] - ] - rollout_data["loss_masks"] = [ - torch.tensor(t, dtype=torch.int, device=torch.cuda.current_device()) for t in rollout_data["loss_masks"] - ] - if "multimodal_train_inputs" in rollout_data: - # Move multimodal training tensors to GPU in advance - rollout_data["multimodal_train_inputs"] = [ - ( - {key: tensor.to(device=torch.cuda.current_device()) for key, tensor in mm_dict.items()} - if mm_dict is not None - else None - ) - for mm_dict in rollout_data["multimodal_train_inputs"] - ] - - if self.args.qkv_format == "bshd": - # TODO: micro-batch wise dynamic, possibly move to @data.py:get_data_iterator - max_seq_len = max(rollout_data["total_lengths"]) - - # pad to reduce memory fragmentation and maybe make the computation faster - pad_size = mpu.get_tensor_model_parallel_world_size() * self.args.data_pad_size_multiplier - max_seq_len = (max_seq_len + pad_size - 1) // pad_size * pad_size - - rollout_data["max_seq_lens"] = [max_seq_len] * len(rollout_data["tokens"]) - - if "rollout_log_probs" in rollout_data: - rollout_data["rollout_log_probs"] = [ - torch.tensor( - slice_log_prob_with_cp( - log_prob, - total_length, - response_length, - self.args.qkv_format, - rollout_data["max_seq_lens"][i] if self.args.qkv_format == "bshd" else None, - ), - device=torch.cuda.current_device(), - dtype=torch.float32, - ) - for i, (log_prob, total_length, response_length) in enumerate( - zip( - rollout_data["rollout_log_probs"], - rollout_data["total_lengths"], - rollout_data["response_lengths"], - strict=False, - ) - ) - ] - if "rollout_routed_experts" in rollout_data: - rollout_data["rollout_routed_experts"] = [ - torch.from_numpy(r) for r in rollout_data["rollout_routed_experts"] - ] - return rollout_data - def _switch_model(self, target_tag: str) -> None: if target_tag not in self.weights_backuper.backup_tags: raise ValueError(f"Cannot switch to unknown model tag: {target_tag}") @@ -262,8 +199,8 @@ def fill_routing_replay(self, data_iterator, num_microbatches, rollout_data): for iterator in data_iterator: iterator.reset() - tp_rank = mpu.get_tensor_model_parallel_rank() - tp_size = mpu.get_tensor_model_parallel_world_size() + tp_rank = self.parallel_state.tp_rank + tp_size = self.parallel_state.tp_size def pad_func(experts, pad): _, num_layers, topk = experts.shape @@ -289,9 +226,9 @@ def pad_func(experts, pad): # TODO: fuse this padding with the following slice_with_cp to reduce memory copy. rollout_routed_experts = [pad_func(r, 1) for r in rollout_routed_experts] # TODO: maybe extract a common process function for here and get_batch? - rollout_routed_experts = [slice_with_cp(r, pad_func) for r in rollout_routed_experts] + rollout_routed_experts = [slice_with_cp(r, pad_func, self.parallel_state) for r in rollout_routed_experts] rollout_routed_experts = torch.cat(rollout_routed_experts, dim=0) - pad_size = mpu.get_tensor_model_parallel_world_size() * self.args.data_pad_size_multiplier + pad_size = self.parallel_state.tp_size * self.args.data_pad_size_multiplier pad = (pad_size - rollout_routed_experts.size(0) % pad_size) % pad_size if pad != 0: rollout_routed_experts = pad_func(rollout_routed_experts, pad) @@ -340,6 +277,7 @@ def compute_log_prob( self.model, data_iterator, num_microbatches, + self.parallel_state, store_prefix=store_prefix, ) @@ -348,9 +286,9 @@ def train(self, rollout_id: int, rollout_data_ref: Box) -> None: self.wake_up() with timer("data_preprocess"): - rollout_data = self._get_rollout_data(rollout_data_ref) + rollout_data = get_rollout_data(self.args, rollout_data_ref, self.parallel_state) if self.args.debug_rollout_only: - log_rollout_data(rollout_id, self.args, rollout_data) + log_rollout_data(rollout_id, self.args, rollout_data, self.parallel_state) return if self.role == "critic": @@ -360,7 +298,7 @@ def train(self, rollout_id: int, rollout_data_ref: Box) -> None: def train_critic(self, rollout_id: int, rollout_data: RolloutBatch) -> None: # Create data iterator for log_probs and train. - data_iterator, num_microbatches = get_data_iterator(self.args, self.model, rollout_data) + data_iterator, num_microbatches = get_data_iterator(self.args, self.model, self.parallel_state, rollout_data) rollout_data.update( forward_only( get_values, @@ -368,13 +306,14 @@ def train_critic(self, rollout_id: int, rollout_data: RolloutBatch) -> None: self.model, data_iterator, num_microbatches, + self.parallel_state, ) ) if rollout_id >= self.args.num_critic_only_steps: sync_actor_critic_data(self.args, rollout_data, self._actor_critic_groups) - compute_advantages_and_returns(self.args, rollout_data) + compute_advantages_and_returns(self.args, self.parallel_state, rollout_data) self.args.loss_type = "value_loss" train( @@ -384,11 +323,12 @@ def train_critic(self, rollout_id: int, rollout_data: RolloutBatch) -> None: self.opt_param_scheduler, data_iterator, num_microbatches, + self.parallel_state, ) def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None: # Create data iterator for log_probs and train. - data_iterator, num_microbatches = get_data_iterator(self.args, self.model, rollout_data) + data_iterator, num_microbatches = get_data_iterator(self.args, self.model, self.parallel_state, rollout_data) if self.args.use_rollout_routing_replay: self.fill_routing_replay(data_iterator, num_microbatches, rollout_data) @@ -434,12 +374,12 @@ def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None: # Calculate adv and returns. Need to performed before training (instead of on the fly), # because we may need normalize the whole rollout. - compute_advantages_and_returns(self.args, rollout_data) + compute_advantages_and_returns(self.args, self.parallel_state, rollout_data) if self.rollout_data_postprocess is not None: self.rollout_data_postprocess(self.args) - log_rollout_data(rollout_id, self.args, rollout_data) + log_rollout_data(rollout_id, self.args, rollout_data, self.parallel_state) # Train if self.args.use_routing_replay: @@ -452,6 +392,7 @@ def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None: self.opt_param_scheduler, data_iterator, num_microbatches, + self.parallel_state, ) self.prof.step(rollout_id=rollout_id) @@ -475,7 +416,7 @@ def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None: logger.info(f"Updating ref model at rollout_id {rollout_id}") self.weights_backuper.backup("ref") - log_perf_data(rollout_id, self.args) + log_perf_data(rollout_id, self.args, self.parallel_state) @timer def save_model(self, rollout_id: int, force_sync: bool = False) -> None: diff --git a/miles/backends/megatron_utils/data.py b/miles/backends/megatron_utils/data.py deleted file mode 100644 index e709bf950..000000000 --- a/miles/backends/megatron_utils/data.py +++ /dev/null @@ -1,614 +0,0 @@ -import logging -from argparse import Namespace -from collections.abc import Sequence - -import numpy as np -import torch -import torch.distributed as dist -import torch.nn.functional as F -from megatron.core import mpu -from megatron.core.packed_seq_params import PackedSeqParams - -from miles.utils import train_metric_utils -from miles.utils.data import get_minimum_num_micro_batch_size -from miles.utils.flops_utils import calculate_fwd_flops -from miles.utils.metric_utils import compute_pass_rate, compute_rollout_step -from miles.utils.seqlen_balancing import get_seqlen_balanced_partitions -from miles.utils.types import RolloutBatch - -from ...utils import tracking_utils -from .cp_utils import get_sum_of_sample_mean, slice_with_cp - -logger = logging.getLogger(__name__) - - -def get_batch( - data_iterator: "DataIterator", keys: Sequence[str], pad_multiplier: int = 128, qkv_format: str = "thd" -) -> dict[str, torch.Tensor | PackedSeqParams | list[torch.Tensor] | None]: - """ - Generate a CP-ready micro-batch with packed sequence parameters. - - Steps: - - Fetch raw fields via iterator. - - Save original token tensors under "unconcat_tokens". - - Slice tokens into two chunks for Context Parallelism (CP), concatenate, and pad to a configurable multiple. - - Build cu_seqlens and `PackedSeqParams` with T-H-D layout (T: sequence length, H: attention heads, D: head dimension). - - Args: - data_iterator: Iterator providing micro-batch data. - keys: List of keys to fetch from the iterator. - pad_multiplier: Multiplier for padding size calculation (default: 128). - - Returns a dict including: - - "tokens": torch.LongTensor of shape [1, T_padded] on the current CUDA device - - "unconcat_tokens": list[torch.LongTensor] for the micro-batch before CP slicing/concat - - "packed_seq_params": PackedSeqParams with T-H-D settings (cu_seqlens on CUDA, dtype=int) - Plus any other requested keys forwarded from the iterator. - """ - - assert "tokens" in keys - batch = data_iterator.get_next(keys) - - if "dynamic_global_batch_size" in data_iterator.rollout_data: - batch["dynamic_global_batch_size"] = data_iterator.rollout_data["dynamic_global_batch_size"] - - tokens = batch["tokens"] - # use 0 as the pad token id should be fine? - pad_token_id = 0 - pad_size = mpu.get_tensor_model_parallel_world_size() * pad_multiplier - - # for cp, we need all tokens to calculate logprob - batch["unconcat_tokens"] = tokens - - cp_size = mpu.get_context_parallel_world_size() - - if qkv_format == "bshd": - max_seqlen = batch["max_seq_lens"][0] - assert max([t.size(0) for t in tokens]) <= max_seqlen - tokens = [slice_with_cp(t, pad_token_id, qkv_format, max_seqlen) for t in tokens] - tokens = torch.stack(tokens) - - elif qkv_format == "thd": - tokens = [slice_with_cp(t, pad_token_id, qkv_format) for t in tokens] - - cu_seqlens = [0] - for t in tokens: - cu_seqlens.append(cu_seqlens[-1] + t.size(0)) - - tokens = torch.cat(tokens) - - # Always pad to reduce memory fragmentation and maybe make the computation faster - pad = (pad_size - tokens.size(0) % pad_size) % pad_size - if pad != 0: - tokens = F.pad(tokens, (0, pad), value=pad_token_id) - cu_seqlens.append(cu_seqlens[-1] + pad) - - # thd requires the cu_seqlens to be of the origin length - cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int).cuda() * cp_size - max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item() - - packed_seq_params = PackedSeqParams( - cu_seqlens_q=cu_seqlens, - cu_seqlens_kv=cu_seqlens, - max_seqlen_q=max_seqlen, - max_seqlen_kv=max_seqlen, - qkv_format="thd", - ) - - tokens = tokens.unsqueeze(0) - else: - raise ValueError(f"Unsupported qkv_format: {qkv_format}") - - batch["tokens"] = tokens - batch["packed_seq_params"] = packed_seq_params - - # loss masks - loss_masks = [] - for loss_mask, total_length, response_length in zip( - batch["loss_masks"], - batch["total_lengths"], - batch["response_lengths"], - strict=True, - ): - prompt_length = total_length - response_length - loss_mask = F.pad(loss_mask, (prompt_length - 1, 1), value=0) - loss_mask = slice_with_cp(loss_mask, 0, qkv_format, max_seqlen) - loss_masks.append(loss_mask) - - if qkv_format == "bshd": - loss_masks = torch.stack(loss_masks) - elif qkv_format == "thd": - loss_masks = torch.cat(loss_masks) - loss_masks = F.pad(loss_masks, (0, pad), value=0).unsqueeze(0) - - assert loss_masks.shape == tokens.shape, f"loss_masks.shape: {loss_masks.shape}, tokens.shape: {tokens.shape}" - batch["full_loss_masks"] = loss_masks - - # Process multimodal training tensors if present - multimodal_train_inputs = batch.get("multimodal_train_inputs", None) - if multimodal_train_inputs is not None: - multimodal_data = {} # key -> concatenated tensor - multimodal_num_items = {} # key -> list of item counts per sequence - for mm_input_dict in multimodal_train_inputs: - if mm_input_dict is not None: - for key, mm_tensor in mm_input_dict.items(): - if key not in multimodal_data: - multimodal_data[key] = mm_tensor - multimodal_num_items[key] = [mm_tensor.size(0)] - else: - multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0) - multimodal_num_items[key].append(mm_tensor.size(0)) - batch["multimodal_train_inputs"] = multimodal_data - batch["multimodal_num_items"] = multimodal_num_items - - return batch - - -def gather_log_data( - metric_name: str, - args: Namespace, - rollout_id: int, - log_dict: dict[str, float], -) -> dict[str, float] | None: - """ - Gather per-rank metrics, reduce by mean on the DP source rank, and log. - - Expects `log_dict` to contain plain scalars. The DP source rank prints and - optionally logs to WandB/TensorBoard with a step derived from `rollout_id` and - batch sizes. Returns the reduced dict on the DP source rank; returns None on others. - """ - - if mpu.get_data_parallel_rank(with_context_parallel=True) == 0: - dp_size = mpu.get_data_parallel_world_size(with_context_parallel=True) - - gathered_log_dict = [None] * dp_size - # Not sure if this will be a performance bottleneck. - dist.gather_object( - log_dict, - gathered_log_dict, - dst=mpu.get_data_parallel_src_rank(with_context_parallel=True), - group=mpu.get_data_parallel_group_gloo(with_context_parallel=True), - ) - - reduced_log_dict = { - f"{metric_name}/{key}": sum([d[key] for d in gathered_log_dict]) / dp_size for key in log_dict - } - logger.info(f"{metric_name} {rollout_id}: {reduced_log_dict}") - - # Calculate step once to avoid duplication - step = compute_rollout_step(args, rollout_id) - reduced_log_dict["rollout/step"] = step - tracking_utils.log(args, reduced_log_dict, step_key="rollout/step") - - return reduced_log_dict - else: - dist.gather_object( - log_dict, - None, - dst=mpu.get_data_parallel_src_rank(with_context_parallel=True), - group=mpu.get_data_parallel_group_gloo(with_context_parallel=True), - ) - return None - - -class DataIterator: - """Micro-batch iterator over rollout dicts. - - Supports either fixed contiguous micro-batches or an explicit per-step - index schedule (for dynamic batch sizing / sequence-length balancing). - """ - - def __init__( - self, - rollout_data: RolloutBatch, - micro_batch_size: int | None = None, - micro_batch_indices: list[list[int]] | None = None, - ) -> None: - """Initialize an iterator over `rollout_data`. - - Args: - rollout_data: Dict of per-sample fields for the local step. - micro_batch_size: Fixed contiguous slice size when not using dynamic scheduling. - micro_batch_indices: Explicit indices per micro-batch when using dynamic balancing. - Must be mutually exclusive with `micro_batch_size`. - """ - self.rollout_data = rollout_data - self.micro_batch_size = micro_batch_size - self.micro_batch_indices = micro_batch_indices - assert micro_batch_size is None or micro_batch_indices is None - self.offset = 0 - - def get_next(self, keys: Sequence[str]) -> dict[str, list[object] | None]: - """Return the next micro-batch for the requested keys. - - - If `micro_batch_indices` is provided, selects rows according to the current - index list for each requested key. - - Otherwise, slices a contiguous window of size `micro_batch_size` starting - at the current offset. - - Returns a dict mapping each key to a list subset (or None if absent). - """ - batch = {} - for key in keys: - vals = self.rollout_data.get(key, None) - if vals is None: - batch[key] = None - else: - if self.micro_batch_indices is not None: - indices = self.micro_batch_indices[self.offset] - batch[key] = [vals[i] for i in indices] - else: - assert self.offset + self.micro_batch_size <= len( - vals - ), f"offset: {self.offset}, micro_batch_size: {self.micro_batch_size}, len(vals): {len(vals)}" - batch[key] = vals[self.offset : self.offset + self.micro_batch_size] - - if self.micro_batch_indices is not None: - self.offset += 1 - else: - self.offset += self.micro_batch_size - return batch - - def reset(self) -> "DataIterator": - """Reset internal offset to the start and return self.""" - self.offset = 0 - return self - - -def get_data_iterator( - args: Namespace, - model: torch.nn.Module | Sequence[torch.nn.Module], - rollout_data: RolloutBatch, -) -> tuple[list[DataIterator], list[int]]: - """ - Create iterators and a micro-batch schedule for a rollout step. - - - If `use_dynamic_batch_size` is False, splits into fixed-size contiguous - micro-batches of `micro_batch_size`. - - If True, computes the number of micro-batches per local step based on - `max_tokens_per_gpu` and per-sample lengths, all-reduces to a DP-wide - maximum, optionally enforces divisibility for Virtual Pipeline Parallelism (VPP), and builds a balanced - index schedule to equalize token counts across micro-batches. - - Returns `(data_iterators, num_microbatches)` where: - - `data_iterators`: list of `DataIterator`, one per VPP stage (size 1 if VPP disabled) - - `num_microbatches`: list[int], one per local step in the rollout (length = steps) - """ - dp_size = mpu.get_data_parallel_world_size(with_context_parallel=False) - dp_group = mpu.get_data_parallel_group() - vpp_size = mpu.get_virtual_pipeline_model_parallel_world_size() - if vpp_size is None: - vpp_size = 1 - if vpp_size > 1: - from megatron.core.utils import get_model_config - - config = get_model_config(model[0]) - microbatch_group_size_per_vp_stage = config.microbatch_group_size_per_vp_stage - cp_size = mpu.get_context_parallel_world_size() - - num_local_samples = len(rollout_data["total_lengths"]) - global_batch_size = rollout_data.get("dynamic_global_batch_size", args.global_batch_size) - num_local_gbs = global_batch_size // dp_size - num_steps_per_rollout = num_local_samples // num_local_gbs - - if global_batch_size != args.global_batch_size: - logger.info( - f"Using dynamic global_batch_size={global_batch_size} (original={args.global_batch_size}), " - f"num_local_samples={num_local_samples}, num_steps_per_rollout={num_steps_per_rollout}" - ) - - def _generate_data_iterator(rollout_data, micro_batch_size, micro_batch_indices=None): - data_iterator = [] - for _ in range(vpp_size): - data_iterator.append(DataIterator(rollout_data, micro_batch_size, micro_batch_indices)) - return data_iterator - - if not args.use_dynamic_batch_size: - num_microbatches = [num_local_gbs // args.micro_batch_size for _ in range(num_steps_per_rollout)] - data_iterator = _generate_data_iterator(rollout_data, args.micro_batch_size) - else: - assert args.max_tokens_per_gpu is not None - # calculate the number of mirobatches for each step - samples = rollout_data["total_lengths"] - assert len(samples) == num_local_samples - num_microbatches = [] - for i in range(num_steps_per_rollout): - start, end = i * num_local_gbs, (i + 1) * num_local_gbs - num_microbatches.append( - get_minimum_num_micro_batch_size(samples[start:end], args.max_tokens_per_gpu * cp_size) - ) - - num_microbatches = torch.tensor(num_microbatches, dtype=torch.int, device=torch.cuda.current_device()) - dist.all_reduce(num_microbatches, op=dist.ReduceOp.MAX, group=dp_group) - - if vpp_size > 1: - # vpp requies the number of microbatches to be divisible by vpp_size - num_microbatches = torch.clamp( - num_microbatches // microbatch_group_size_per_vp_stage * microbatch_group_size_per_vp_stage, - min=1, - ) - - num_microbatches = num_microbatches.tolist() - - # balance the each micro batch - samples = rollout_data["total_lengths"] - # balance the number of mirobatches across steps - micro_batch_indices = [] - for i, num_mbs in enumerate(num_microbatches): - start, end = i * num_local_gbs, (i + 1) * num_local_gbs - samples = rollout_data["total_lengths"][start:end] - partitions = get_seqlen_balanced_partitions(samples, num_mbs, equal_size=False) - for j in range(num_mbs): - for k in range(len(partitions[j])): - partitions[j][k] += start - micro_batch_indices.extend(partitions) - - assert len(set(sum(micro_batch_indices, []))) == num_local_samples - - data_iterator = _generate_data_iterator(rollout_data, None, micro_batch_indices) - - return ( - data_iterator, - num_microbatches, - ) - - -def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None: - """ - Summarize rollout fields and log reduced metrics on PP last stage, TP rank 0. - - - Tensor-valued lists are concatenated and averaged. For token-level metrics - like log-probs/returns/advantages/values, computes a CP-correct sample mean - using `loss_masks` and total/response lengths. - - Non-tensor lists are averaged elementwise. - - Scalars are converted to Python numbers. - """ - if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage(): - cp_size = mpu.get_context_parallel_world_size() - log_dict = {} - response_lengths = rollout_data["response_lengths"] - loss_masks = rollout_data["loss_masks"] - total_lengths = rollout_data["total_lengths"] - max_seq_lens = rollout_data.get("max_seq_lens", None) - - for key, val in rollout_data.items(): - if key in [ - "tokens", - "multimodal_train_inputs", - "loss_masks", - "sample_indices", - "rollout_routed_experts", - "max_seq_lens", - "dynamic_global_batch_size", - ]: - continue - # Upload per sample mean for each rollout value - # There are the following assumptions: - # - Each dp rank has the same number of samples - if isinstance(val, (list, tuple)): - if isinstance(val[0], torch.Tensor): - # NOTE: Here we have to do the clone().detach(), otherwise the tensor will be - # modified in place and will cause problem for the next rollout. - val = torch.cat(val).clone().detach() - if key in ["log_probs", "ref_log_probs", "rollout_log_probs", "returns", "advantages", "values"]: - sum_of_sample_mean = get_sum_of_sample_mean( - total_lengths, - response_lengths, - loss_masks, - qkv_format=args.qkv_format, - max_seq_lens=max_seq_lens, - ) - val = cp_size * sum_of_sample_mean(val) / len(loss_masks) - else: - val = val.mean() * cp_size - else: - val = sum(val) / len(val) - elif isinstance(val, torch.Tensor): - val = val.float().mean() - else: - raise ValueError(f"Unsupported type: {type(val)} for key: {key}") - log_dict[key] = val.item() if isinstance(val, torch.Tensor) else val - - reduced_log_dict = gather_log_data("rollout", args, rollout_id, log_dict) - if args.ci_test and reduced_log_dict is not None: - if ( - rollout_id == 0 - and "rollout/log_probs" in reduced_log_dict - and "rollout/ref_log_probs" in reduced_log_dict - ): - assert reduced_log_dict["rollout/log_probs"] == reduced_log_dict["rollout/ref_log_probs"] - if "rollout/log_probs" in reduced_log_dict: - assert -0.5 < reduced_log_dict["rollout/log_probs"] < 0 - if "rollout/entropy" in reduced_log_dict: - assert 0 < reduced_log_dict["rollout/entropy"] < 0.5 - - if args.log_multi_turn: - log_multi_turn_data(rollout_id, args, rollout_data) - if args.log_passrate: - log_passrate(rollout_id, args, rollout_data) - - if args.log_correct_samples: - if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage(): - cp_size = mpu.get_context_parallel_world_size() - log_dict = {} - response_lengths = rollout_data["response_lengths"] - loss_masks = rollout_data["loss_masks"] - total_lengths = rollout_data["total_lengths"] - - def quantile(total_value, n_quantiles, data) -> dict: - import math - - assert n_quantiles > 1, f"n_quantiles({n_quantiles}) must be greater than 1." - - quantiles = [((i + 1) / n_quantiles) for i in range(n_quantiles)] - cut_points = [total_value * q for q in quantiles] - cut_points[-1] = total_value - - count = [0] * n_quantiles - for d in data: - for i, point in enumerate(cut_points): - if d <= point: - count[i] += 1 - break - - total = sum(count) + 1e-9 - percentile = [c / total for c in count] - - percentile = {f"p{min(math.ceil(q*100),100)}": p for q, p in zip(quantiles, percentile, strict=True)} - return percentile - - raw_rewards = rollout_data["raw_reward"] - # Additional metrics for correct cases are calculated separately below. - correct_response_lengths = [] - correct_total_lengths = [] - correct_loss_masks = [] - correct_entropy = [] - for i, raw_reward in enumerate(raw_rewards): - if raw_reward == 1: - correct_response_lengths.append(response_lengths[i]) - correct_total_lengths.append(total_lengths[i]) - correct_loss_masks.append(loss_masks[i]) - correct_entropy.append(-rollout_data["log_probs"][i]) - num_correct_responses = len(correct_total_lengths) - rollout_data["correct_response_lengths"] = correct_response_lengths - correct_response_length_percentile = quantile( - args.rollout_max_response_len, 4, rollout_data["correct_response_lengths"] - ) - for p, val in correct_response_length_percentile.items(): - rollout_data[f"correct_length/{p}"] = [val] * num_correct_responses - if len(correct_entropy) > 0: - sum_of_sample_mean = get_sum_of_sample_mean( - correct_total_lengths, correct_response_lengths, correct_loss_masks - ) - correct_entropy = sum_of_sample_mean(torch.cat(correct_entropy, dim=0)) - rollout_data["correct_entropy"] = [correct_entropy.item()] * num_correct_responses - else: - rollout_data["correct_entropy"] = [0] * num_correct_responses - - -def log_multi_turn_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None: - """ - Log multi-turn auxiliary metrics such as raw/observed response lengths and rounds. - - Operates only on PP last stage and TP rank 0. Uses GPU tensors when available - to compute statistics without host transfers. - """ - if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage(): - log_dict = {} - for key, val in rollout_data.items(): - if key == "loss_masks": - if val: # Check if val is not empty - device = val[0].device # Get device from first tensor - - # Vectorized length calculation using torch - raw_response_lengths = torch.tensor([v.shape[0] for v in val], dtype=torch.float32, device=device) - log_dict["raw_response_length/response_length_mean"] = raw_response_lengths.mean().item() - log_dict["raw_response_length/response_length_max"] = raw_response_lengths.max().item() - log_dict["raw_response_length/response_length_min"] = raw_response_lengths.min().item() - log_dict["raw_response_length/response_length_clip_ratio"] = ( - (raw_response_lengths >= args.rollout_max_response_len).float().mean().item() - ) - - # Vectorized sum calculation using torch - stay on GPU - wo_obs_response_lengths = torch.tensor( - [v.sum().item() for v in val], dtype=torch.float32, device=device - ) - log_dict["wo_obs_response_length/response_length_mean"] = wo_obs_response_lengths.mean().item() - log_dict["wo_obs_response_length/response_length_max"] = wo_obs_response_lengths.max().item() - log_dict["wo_obs_response_length/response_length_min"] = wo_obs_response_lengths.min().item() - if key == "round_number": - # Use numpy for vectorized round number statistics - round_number_array = np.array(val) - log_dict["multi_turn_metric/round_number_mean"] = np.mean(round_number_array) - log_dict["multi_turn_metric/round_number_max"] = np.max(round_number_array) - log_dict["multi_turn_metric/round_number_min"] = np.min(round_number_array) - gather_log_data("multi_turn", args, rollout_id, log_dict) - - -def log_passrate(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None: - """ - Compute pass@k metrics from `raw_reward` groups and log the results. - - `raw_reward` is reshaped to `[group_number, group_size]`, then pass@k is - estimated per problem and averaged. - """ - if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage(): - log_dict = {} - for key, val in rollout_data.items(): - if key != "raw_reward": - continue - - log_dict |= compute_pass_rate( - flat_rewards=val, - group_size=args.n_samples_per_prompt, - num_groups=args.rollout_batch_size, - ) - - gather_log_data("passrate", args, rollout_id, log_dict) - - -def log_perf_data(rollout_id: int, args: Namespace) -> None: - train_metric_utils.log_perf_data_raw( - rollout_id=rollout_id, - args=args, - is_primary_rank=( - mpu.get_tensor_model_parallel_rank() == 0 - and mpu.is_pipeline_last_stage() - and mpu.get_data_parallel_rank(with_context_parallel=True) == 0 - ), - compute_total_fwd_flops=lambda seq_lens: calculate_fwd_flops(seqlens=seq_lens, args=args) - / dist.get_world_size() - / 1e12, - ) - - -def sync_actor_critic_data( - args: Namespace, - rollout_data: RolloutBatch | None = None, - group: dist.ProcessGroup | None = None, -) -> None: - """ - Broadcast `values` (from critic) and optionally `log_probs`/`ref_log_probs` - (from actor) across PP ranks to align data dependencies. - - - Values are broadcast from src=1. - - Log-probs and ref-log-probs are broadcast from src=0 when KL is used. - Updates `rollout_data` in place with the synchronized tensors. - """ - log_probs_key = "log_probs" if not args.use_rollout_logprobs else "rollout_log_probs" - values, log_probs, ref_log_probs = map(rollout_data.get, ("values", log_probs_key, "ref_log_probs")) - - # return when not the pp last stage - if not values and not log_probs: - return - - handles = [] - - if not values: - values = [torch.empty_like(log_prob) for log_prob in log_probs] - for value in values: - handles.append(dist.broadcast(value, src=1, group=group, async_op=True)) - - if args.kl_coef != 0 or args.use_kl_loss: - if not log_probs: - log_probs = [torch.empty_like(value) for value in values] - if not ref_log_probs: - ref_log_probs = [torch.empty_like(value) for value in values] - for ref_log_prob, log_prob in zip(ref_log_probs, log_probs, strict=False): - handles.append(dist.broadcast(log_prob, src=0, group=group, async_op=True)) - handles.append(dist.broadcast(ref_log_prob, src=0, group=group, async_op=True)) - - for handle in handles: - handle.wait() - - rollout_data.update( - { - k: v - for k, v in { - "values": values, - log_probs_key: log_probs, - "ref_log_probs": ref_log_probs, - }.items() - if v is not None - } - ) diff --git a/miles/backends/megatron_utils/model.py b/miles/backends/megatron_utils/model.py index 780370453..fea58feec 100644 --- a/miles/backends/megatron_utils/model.py +++ b/miles/backends/megatron_utils/model.py @@ -22,13 +22,16 @@ from megatron.training.global_vars import get_args from megatron.training.training import get_model -from miles.utils import tracking_utils from miles.utils.memory_utils import clear_memory +from ..training_utils.ci_utils import check_grad_norm, check_kl +from ..training_utils.data import DataIterator, get_batch +from ..training_utils.log_utils import aggregate_forward_results, aggregate_train_losses, log_train_step +from ..training_utils.loss import loss_function +from ..training_utils.parallel import ParallelState from .checkpoint import load_checkpoint, save_checkpoint -from .data import DataIterator, get_batch -from .loss import loss_function from .model_provider import get_model_provider_func +from .parallel import get_packed_seq_params logger = logging.getLogger(__name__) @@ -154,6 +157,7 @@ def forward_only( model: Sequence[DDP], data_iterator: Sequence[DataIterator], num_microbatches: Sequence[int], + parallel_state: ParallelState, store_prefix: str = "", ) -> dict[str, list[torch.Tensor]]: """Run forward passes only and collect non-loss outputs (e.g., logprobs). @@ -213,12 +217,13 @@ def forward_step( "response_lengths", "max_seq_lens", ], + parallel_state, args.data_pad_size_multiplier, args.qkv_format, ) unconcat_tokens = batch["unconcat_tokens"] tokens = batch["tokens"] - packed_seq_params = batch["packed_seq_params"] + packed_seq_params = get_packed_seq_params(batch, args) total_lengths = batch["total_lengths"] response_lengths = batch["response_lengths"] output_tensor = model( @@ -234,6 +239,7 @@ def forward_step( return output_tensor, partial( f, args=args, + parallel_state=parallel_state, unconcat_tokens=unconcat_tokens, total_lengths=total_lengths, response_lengths=response_lengths, @@ -276,22 +282,9 @@ def forward_step( rollout_data = {} # Store the results on the last stage if mpu.is_pipeline_last_stage(): - keys = forward_data_store[0].keys() - for key in keys: - values = [] - for value in forward_data_store: - assert isinstance(value[key], list) - values += value[key] - - if args.use_dynamic_batch_size: - # TODO: This is ugly... Find a better way to make the data have the same order. - # TODO: move this out of the loop. - origin_values = [None] * len(values) - origin_indices = sum(data_iterator[0].micro_batch_indices, []) - for value, origin_index in zip(values, origin_indices, strict=False): - origin_values[origin_index] = value - values = origin_values - rollout_data[f"{store_prefix}{key}"] = values + aggregated = aggregate_forward_results(forward_data_store, data_iterator[0], args, store_prefix="") + for key, value in aggregated.items(): + rollout_data[f"{store_prefix}{key}"] = value return rollout_data @@ -304,6 +297,7 @@ def train_one_step( optimizer: MegatronOptimizer, opt_param_scheduler: OptimizerParamScheduler, num_microbatches: int, + parallel_state: ParallelState, ) -> tuple[dict[str, float], float]: """Execute a single pipeline-parallel training step. @@ -371,6 +365,7 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p "rollout_log_probs", "max_seq_lens", ], + parallel_state, args.data_pad_size_multiplier, args.qkv_format, ) @@ -386,7 +381,7 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p position_ids=None, attention_mask=None, labels=None, - packed_seq_params=batch["packed_seq_params"], + packed_seq_params=get_packed_seq_params(batch, args), loss_mask=batch["full_loss_masks"], ) else: @@ -395,7 +390,7 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p "position_ids": None, "attention_mask": None, "labels": None, - "packed_seq_params": batch["packed_seq_params"], + "packed_seq_params": get_packed_seq_params(batch, args), "loss_mask": batch["full_loss_masks"], } @@ -410,7 +405,9 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p if os.environ.get("ENABLE_ROUTING_REPLAY", "0") == "1": os.environ["ROUTING_REPLAY_STAGE"] = old_stage - return output_tensor, partial(loss_function, args, batch, num_microbatches) + return output_tensor, partial( + loss_function, args, parallel_state, batch, num_microbatches, apply_megatron_loss_scaling=True + ) # Forward pass. forward_backward_func = get_forward_backward_func() @@ -458,22 +455,7 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p optimizer.zero_grad() if mpu.is_pipeline_last_stage(ignore_virtual=True): - # Average loss across microbatches. - keys = losses_reduced[0]["keys"] - values = None - for x in losses_reduced: - if values is None: - values = x["values"] - else: - values += x["values"] - assert len(keys) + 1 == values.numel() - torch.distributed.all_reduce(values, group=mpu.get_data_parallel_group(with_context_parallel=True)) - - loss_reduced = {} - values = values.tolist() - num_samples_or_tokens = values[0] - for key, value in zip(keys, values[1:], strict=False): - loss_reduced[key] = value * mpu.get_context_parallel_world_size() / num_samples_or_tokens + loss_reduced = aggregate_train_losses(losses_reduced, parallel_state) return loss_reduced, grad_norm return {}, grad_norm @@ -500,6 +482,7 @@ def train( opt_param_scheduler: OptimizerParamScheduler, data_iterator: Sequence[DataIterator], num_microbatches: Sequence[int], + parallel_state: ParallelState, ) -> None: """Run training over a rollout consisting of multiple steps. @@ -597,6 +580,7 @@ def train( optimizer, opt_param_scheduler, num_microbatches[step_id], + parallel_state, ) if step_id == 0: @@ -638,52 +622,41 @@ def train( accumulated_step_id = rollout_id * num_steps_per_rollout + step_id role = getattr(model[0], "role", "actor") role_tag = "" if role == "actor" else f"{role}-" - log_dict = { - f"train/{role_tag}{key}": val.mean().item() if isinstance(val, torch.Tensor) else val - for key, val in loss_dict.items() - } - log_dict[f"train/{role_tag}grad_norm"] = grad_norm + + extra_metrics = {} if args.enable_mtp_training: - log_dict[f"train/{role_tag}mtp_loss"] = mtp_losses + extra_metrics["mtp_loss"] = mtp_losses for param_group_id, param_group in enumerate(optimizer.param_groups): - log_dict[f"train/{role_tag}lr-pg_{param_group_id}"] = opt_param_scheduler.get_lr(param_group) - - log_dict["train/step"] = accumulated_step_id - tracking_utils.log(args, log_dict, step_key="train/step") + extra_metrics[f"lr-pg_{param_group_id}"] = opt_param_scheduler.get_lr(param_group) + + log_dict = log_train_step( + args=args, + loss_dict=loss_dict, + grad_norm=grad_norm, + rollout_id=rollout_id, + step_id=step_id, + num_steps_per_rollout=num_steps_per_rollout, + role=role, + extra_metrics=extra_metrics, + should_log=True, + ) if args.ci_test and not args.ci_disable_kl_checker: - if step_id == 0 and "train/ppo_kl" in log_dict and "train/pg_clipfrac" in log_dict: - if args.multi_latent_attention: - # TODO: mla currently have non-zero kl, need further investigation - assert log_dict["train/ppo_kl"] < 1e-8, f"{log_dict=}" - else: - assert log_dict["train/ppo_kl"] == 0.0 and log_dict["train/pg_clipfrac"] == 0.0, f"{log_dict=}" - if accumulated_step_id == 0 and "train/kl_loss" in log_dict: - assert log_dict["train/kl_loss"] == 0.0, f"{log_dict=}" + check_kl(args, log_dict, step_id, accumulated_step_id) logger.info(f"{role_tag}step {accumulated_step_id}: {log_dict}") - if args.ci_save_grad_norm is not None: - ci_save_grad_norm_path = args.ci_save_grad_norm.format( - role=role, + if args.ci_test: + check_grad_norm( + args=args, + grad_norm=grad_norm, rollout_id=rollout_id, step_id=step_id, - ) - torch.save(grad_norm, ci_save_grad_norm_path) - elif args.ci_load_grad_norm is not None: - ci_load_grad_norm_path = args.ci_load_grad_norm.format( role=role, - rollout_id=rollout_id, - step_id=step_id, + rank=mpu.get_data_parallel_rank(), ) - expected_grad_norm = torch.load(ci_load_grad_norm_path) - assert math.isclose( - grad_norm, - expected_grad_norm, - rel_tol=0.01, - abs_tol=0.01, - ), f"grad norm mismatch: {grad_norm} != {expected_grad_norm}" + # Close out pre-hooks if using distributed optimizer and overlapped param gather. if pre_hook_enabled: disable_forward_pre_hook(model) @@ -730,6 +703,7 @@ def save_hf_model(args, rollout_id: int, model: Sequence[DDP]) -> None: try: from megatron.bridge import AutoBridge + from miles.utils.megatron_bridge_utils import patch_megatron_model path = Path(args.save_hf.format(rollout_id=rollout_id)) @@ -770,6 +744,7 @@ def initialize_model_and_optimizer( if torch.version.hip: import megatron.core.dist_checkpointing.strategies.filesystem_async as filesystem_async_module + from miles.utils.rocm_checkpoint_writer import ROCmFileSystemWriterAsync filesystem_async_module.FileSystemWriterAsync = ROCmFileSystemWriterAsync diff --git a/miles/backends/megatron_utils/parallel.py b/miles/backends/megatron_utils/parallel.py new file mode 100644 index 000000000..e3d99fc46 --- /dev/null +++ b/miles/backends/megatron_utils/parallel.py @@ -0,0 +1,67 @@ +import logging +from argparse import Namespace +from collections.abc import Sequence + +import torch +from megatron.core import mpu +from megatron.core.packed_seq_params import PackedSeqParams +from megatron.core.utils import get_model_config + +from ..training_utils.parallel import ParallelState + +logger = logging.getLogger(__name__) + + +def create_megatron_parallel_state( + model: torch.nn.Module | Sequence[torch.nn.Module] | None = None, +) -> ParallelState: + vpp_size_value = mpu.get_virtual_pipeline_model_parallel_world_size() + if vpp_size_value is None: + vpp_size = 1 + microbatch_group_size_per_vp_stage = None + elif vpp_size_value > 1: + assert model is not None + model_to_check = model[0] if isinstance(model, Sequence) else model + config = get_model_config(model_to_check) + vpp_size = vpp_size_value + microbatch_group_size_per_vp_stage = config.microbatch_group_size_per_vp_stage + else: + vpp_size = 1 + microbatch_group_size_per_vp_stage = None + + parallel_state = ParallelState( + dp_rank=mpu.get_data_parallel_rank(with_context_parallel=False), + dp_src_rank=mpu.get_data_parallel_src_rank(with_context_parallel=True), + dp_size=mpu.get_data_parallel_world_size(with_context_parallel=False), + cp_rank=mpu.get_context_parallel_rank(), + cp_size=mpu.get_context_parallel_world_size(), + dp_cp_rank=mpu.get_data_parallel_rank(with_context_parallel=True), + dp_cp_size=mpu.get_data_parallel_world_size(with_context_parallel=True), + dp_group=mpu.get_data_parallel_group(with_context_parallel=False), + dp_cp_group=mpu.get_data_parallel_group(with_context_parallel=True), + dp_cp_group_gloo=mpu.get_data_parallel_group_gloo(with_context_parallel=True), + cp_group=mpu.get_context_parallel_group(), + tp_size=mpu.get_tensor_model_parallel_world_size(), + tp_rank=mpu.get_tensor_model_parallel_rank(), + tp_group=mpu.get_tensor_model_parallel_group(), + is_pp_last_stage=mpu.is_pipeline_last_stage(), + vpp_size=vpp_size, + microbatch_group_size_per_vp_stage=microbatch_group_size_per_vp_stage, + ) + + return parallel_state + + +def get_packed_seq_params(batch: dict[str, torch.Tensor], args: Namespace) -> PackedSeqParams: + if args.qkv_format == "thd": + packed_seq_params = PackedSeqParams( + cu_seqlens_q=batch["cu_seqlens"], + cu_seqlens_kv=batch["cu_seqlens"], + max_seqlen_q=batch["max_seqlen"], + max_seqlen_kv=batch["max_seqlen"], + qkv_format="thd", + ) + batch["packed_seq_params"] = packed_seq_params + return packed_seq_params + else: + return None diff --git a/miles/backends/training_utils/ci_utils.py b/miles/backends/training_utils/ci_utils.py new file mode 100644 index 000000000..ee5563f14 --- /dev/null +++ b/miles/backends/training_utils/ci_utils.py @@ -0,0 +1,55 @@ +"""CI utilities for training backend testing.""" + +import logging +import math +from argparse import Namespace + +import torch + +logger = logging.getLogger(__name__) + + +def check_kl(args: Namespace, log_dict: dict[str, float], step_id: int, accumulated_step_id: int) -> None: + if step_id == 0 and "train/ppo_kl" in log_dict and "train/pg_clipfrac" in log_dict: + if args.multi_latent_attention: + # TODO: mla currently have non-zero kl, need further investigation + assert log_dict["train/ppo_kl"] < 1e-8, f"{log_dict=}" + else: + assert log_dict["train/ppo_kl"] == 0.0 and log_dict["train/pg_clipfrac"] == 0.0, f"{log_dict=}" + if accumulated_step_id == 0 and "train/kl_loss" in log_dict: + assert log_dict["train/kl_loss"] == 0.0, f"{log_dict=}" + + +def check_grad_norm( + args: Namespace, + grad_norm: float, + rollout_id: int, + step_id: int, + role: str = "actor", + rank: int = 0, +) -> None: + + if rank != 0: + return + + if args.ci_save_grad_norm is not None: + ci_save_grad_norm_path = args.ci_save_grad_norm.format( + role=role, + rollout_id=rollout_id, + step_id=step_id, + ) + torch.save(grad_norm, ci_save_grad_norm_path) + + elif args.ci_load_grad_norm is not None: + ci_load_grad_norm_path = args.ci_load_grad_norm.format( + role=role, + rollout_id=rollout_id, + step_id=step_id, + ) + expected_grad_norm = torch.load(ci_load_grad_norm_path, weights_only=False) + assert math.isclose( + grad_norm, + expected_grad_norm, + rel_tol=0.03, + abs_tol=0.03, + ), f"grad norm mismatch: {grad_norm} != {expected_grad_norm}" diff --git a/miles/backends/megatron_utils/cp_utils.py b/miles/backends/training_utils/cp_utils.py similarity index 90% rename from miles/backends/megatron_utils/cp_utils.py rename to miles/backends/training_utils/cp_utils.py index 2e795d3d3..7d3f4b3e1 100644 --- a/miles/backends/megatron_utils/cp_utils.py +++ b/miles/backends/training_utils/cp_utils.py @@ -3,20 +3,22 @@ import torch import torch.distributed as dist import torch.nn.functional as F -from megatron.core import mpu + +from .parallel import ParallelState def get_logits_and_tokens_offset_with_cp( total_length: int, response_length: int, + parallel_state: ParallelState, qkv_format: str = "thd", max_seq_len: int | None = None, ): """ All offsets start from the begining of the prompt. """ - cp_rank = mpu.get_context_parallel_rank() - cp_size = mpu.get_context_parallel_world_size() + cp_rank = parallel_state.cp_rank + cp_size = parallel_state.cp_size assert cp_size > 1 prompt_length = total_length - response_length @@ -54,6 +56,7 @@ def get_sum_of_sample_mean( total_lengths: list[int], response_lengths: list[int], loss_masks: list[torch.Tensor], + parallel_state: ParallelState, calculate_per_token_loss: bool = False, qkv_format: str = "thd", max_seq_lens: list[int] | None = None, @@ -61,7 +64,7 @@ def get_sum_of_sample_mean( """ Calculate correct sample mean for CP """ - cp_size = mpu.get_context_parallel_world_size() + cp_size = parallel_state.cp_size if cp_size == 1: def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor: @@ -89,7 +92,7 @@ def sum_of_token(x: torch.Tensor) -> torch.Tensor: max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None prompt_length = total_length - response_length _, _, _, tokens_offset = get_logits_and_tokens_offset_with_cp( - total_length, response_length, qkv_format, max_seq_len + total_length, response_length, parallel_state, qkv_format, max_seq_len ) loss_mask_0 = loss_mask[tokens_offset[0][0] - prompt_length : tokens_offset[0][1] - prompt_length] loss_mask_1 = loss_mask[tokens_offset[1][0] - prompt_length : tokens_offset[1][1] - prompt_length] @@ -119,18 +122,20 @@ def sum_of_token(x: torch.Tensor) -> torch.Tensor: return sum_of_sample_mean if not calculate_per_token_loss else sum_of_token -def all_gather_with_cp(tensor: torch.Tensor, total_length: int, response_length: int) -> torch.Tensor: +def all_gather_with_cp( + tensor: torch.Tensor, total_length: int, response_length: int, parallel_state: ParallelState +) -> torch.Tensor: """ Gather tensors across all ranks in the context parallel group. The first dimension of the output tensor will be the `response_length`. """ - cp_group = mpu.get_context_parallel_group() - cp_size = mpu.get_context_parallel_world_size() + cp_group = parallel_state.cp_group + cp_size = parallel_state.cp_size if cp_size == 1: return tensor - _, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp(total_length, response_length) + _, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp(total_length, response_length, parallel_state) prompt_length = total_length - response_length @@ -174,11 +179,12 @@ def zero(len: int) -> torch.Tensor: def slice_with_cp( tokens: torch.Tensor, pad_value: tuple[int, float, Callable], + parallel_state: ParallelState, qkv_format: str = "thd", max_seq_len: int | None = None, ) -> torch.Tensor: - cp_rank = mpu.get_context_parallel_rank() - cp_size = mpu.get_context_parallel_world_size() + cp_rank = parallel_state.cp_rank + cp_size = parallel_state.cp_size if qkv_format == "bshd": assert max_seq_len is not None @@ -219,19 +225,20 @@ def slice_log_prob_with_cp( log_prob: list[float] | torch.Tensor, total_length: int, response_length: int, + parallel_state: ParallelState, qkv_format: str = "thd", max_token_len: int | None = None, ) -> list[float] | torch.Tensor: assert len(log_prob) == response_length - cp_size = mpu.get_context_parallel_world_size() + cp_size = parallel_state.cp_size if cp_size == 1: return log_prob prompt_length = total_length - response_length _, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp( - total_length, response_length, qkv_format, max_token_len + total_length, response_length, parallel_state, qkv_format, max_token_len ) chunk_1 = log_prob[logits_offset[0][0] - (prompt_length - 1) : logits_offset[0][1] - (prompt_length - 1)] diff --git a/miles/backends/training_utils/data.py b/miles/backends/training_utils/data.py new file mode 100644 index 000000000..67bb30108 --- /dev/null +++ b/miles/backends/training_utils/data.py @@ -0,0 +1,433 @@ +import logging +from argparse import Namespace +from collections.abc import Sequence + +import torch +import torch.distributed as dist +import torch.nn.functional as F + +from miles.utils.data import get_minimum_num_micro_batch_size +from miles.utils.seqlen_balancing import get_seqlen_balanced_partitions +from miles.utils.types import RolloutBatch + +from ...utils.data import process_rollout_data +from ...utils.ray_utils import Box +from .cp_utils import slice_log_prob_with_cp, slice_with_cp +from .parallel import ParallelState + +logger = logging.getLogger(__name__) + + +def get_rollout_data(args: Namespace, rollout_data_ref: Box, parallel_state: ParallelState) -> RolloutBatch: + # Fetch data through ray on CPU, not sure if this will be performance bottleneck. + # Both first pp stage and the last pp stage will receive the data. + rollout_data = process_rollout_data( + args, + rollout_data_ref, + parallel_state.dp_rank, + parallel_state.dp_size, + ) + # move tokens to GPU in advance + rollout_data["tokens"] = [ + torch.tensor(t, dtype=torch.long, device=torch.cuda.current_device()) for t in rollout_data["tokens"] + ] + rollout_data["loss_masks"] = [ + torch.tensor(t, dtype=torch.int, device=torch.cuda.current_device()) for t in rollout_data["loss_masks"] + ] + if "multimodal_train_inputs" in rollout_data: + # Move multimodal training tensors to GPU in advance + rollout_data["multimodal_train_inputs"] = [ + ( + {key: tensor.to(device=torch.cuda.current_device()) for key, tensor in mm_dict.items()} + if mm_dict is not None + else None + ) + for mm_dict in rollout_data["multimodal_train_inputs"] + ] + + if args.qkv_format == "bshd": + # TODO: micro-batch wise dynamic, possibly move to @data.py:get_data_iterator + max_seq_len = max(rollout_data["total_lengths"]) + + # pad to reduce memory fragmentation and maybe make the computation faster + pad_size = parallel_state.tp_size * args.data_pad_size_multiplier + max_seq_len = (max_seq_len + pad_size - 1) // pad_size * pad_size + + rollout_data["max_seq_lens"] = [max_seq_len] * len(rollout_data["tokens"]) + + if "rollout_log_probs" in rollout_data: + rollout_data["rollout_log_probs"] = [ + torch.tensor( + slice_log_prob_with_cp( + log_prob, + total_length, + response_length, + parallel_state, + args.qkv_format, + rollout_data["max_seq_lens"][i] if args.qkv_format == "bshd" else None, + ), + device=torch.cuda.current_device(), + dtype=torch.float32, + ) + for i, (log_prob, total_length, response_length) in enumerate( + zip( + rollout_data["rollout_log_probs"], + rollout_data["total_lengths"], + rollout_data["response_lengths"], + strict=False, + ) + ) + ] + if "rollout_routed_experts" in rollout_data: + rollout_data["rollout_routed_experts"] = [torch.from_numpy(r) for r in rollout_data["rollout_routed_experts"]] + return rollout_data + + +def get_batch( + data_iterator: "DataIterator", + keys: Sequence[str], + parallel_state: ParallelState, + pad_multiplier: int = 128, + qkv_format: str = "thd", + get_position_ids: bool = False, +) -> dict[str, torch.Tensor | list[torch.Tensor] | None]: + """ + Generate a CP-ready micro-batch with packed sequence parameters. + + Steps: + - Fetch raw fields via iterator. + - Save original token tensors under "unconcat_tokens". + - Slice tokens into two chunks for Context Parallelism (CP), concatenate, and pad to a configurable multiple. + - Build cu_seqlens and `PackedSeqParams` with T-H-D layout (T: sequence length, H: attention heads, D: head dimension). + + Args: + data_iterator: Iterator providing micro-batch data. + keys: List of keys to fetch from the iterator. + pad_multiplier: Multiplier for padding size calculation (default: 128). + + Returns a dict including: + - "tokens": torch.LongTensor of shape [1, T_padded] on the current CUDA device + - "unconcat_tokens": list[torch.LongTensor] for the micro-batch before CP slicing/concat + - "packed_seq_params": PackedSeqParams with T-H-D settings (cu_seqlens on CUDA, dtype=int) + Plus any other requested keys forwarded from the iterator. + """ + + assert "tokens" in keys + batch = data_iterator.get_next(keys) + + if "dynamic_global_batch_size" in data_iterator.rollout_data: + batch["dynamic_global_batch_size"] = data_iterator.rollout_data["dynamic_global_batch_size"] + + tokens = batch["tokens"] + # use 0 as the pad token id should be fine? + pad_token_id = 0 + pad_size = parallel_state.dp_size * pad_multiplier + + # for cp, we need all tokens to calculate logprob + batch["unconcat_tokens"] = tokens + + cp_size = parallel_state.cp_size + + if qkv_format == "bshd": + max_seqlen = batch["max_seq_lens"][0] + assert max([t.size(0) for t in tokens]) <= max_seqlen + tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format, max_seqlen) for t in tokens] + tokens = torch.stack(tokens) + + elif qkv_format == "thd": + tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format) for t in tokens] + + cu_seqlens = [0] + for t in tokens: + cu_seqlens.append(cu_seqlens[-1] + t.size(0)) + + tokens = torch.cat(tokens) + + # Always pad to reduce memory fragmentation and maybe make the computation faster + pad = (pad_size - tokens.size(0) % pad_size) % pad_size + if pad != 0: + tokens = F.pad(tokens, (0, pad), value=pad_token_id) + cu_seqlens.append(cu_seqlens[-1] + pad) + + # thd requires the cu_seqlens to be of the origin length + cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int).cuda() * cp_size + max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item() + + tokens = tokens.unsqueeze(0) + + batch["cu_seqlens"] = cu_seqlens + batch["max_seqlen"] = max_seqlen + else: + raise ValueError(f"Unsupported qkv_format: {qkv_format}") + + batch["tokens"] = tokens + + if get_position_ids: + position_ids_list = [] + for t in batch["unconcat_tokens"]: + seq_len = t.size(0) + pos_ids = torch.arange(seq_len, device=t.device, dtype=torch.long) + position_ids_list.append(pos_ids) + + if qkv_format == "bshd": + position_ids = [slice_with_cp(p, 0, parallel_state, qkv_format, max_seqlen) for p in position_ids_list] + position_ids = torch.stack(position_ids) + elif qkv_format == "thd": + position_ids = [slice_with_cp(p, 0, parallel_state, qkv_format) for p in position_ids_list] + position_ids = torch.cat(position_ids) + if pad != 0: + position_ids = F.pad(position_ids, (0, pad), value=0) + position_ids = position_ids.unsqueeze(0) + + batch["position_ids"] = position_ids + + # loss masks + loss_masks = [] + for loss_mask, total_length, response_length in zip( + batch["loss_masks"], + batch["total_lengths"], + batch["response_lengths"], + strict=True, + ): + prompt_length = total_length - response_length + loss_mask = F.pad(loss_mask, (prompt_length - 1, 1), value=0) + loss_mask = slice_with_cp(loss_mask, 0, parallel_state, qkv_format, max_seqlen) + loss_masks.append(loss_mask) + + if qkv_format == "bshd": + loss_masks = torch.stack(loss_masks) + elif qkv_format == "thd": + loss_masks = torch.cat(loss_masks) + loss_masks = F.pad(loss_masks, (0, pad), value=0).unsqueeze(0) + + assert loss_masks.shape == tokens.shape, f"loss_masks.shape: {loss_masks.shape}, tokens.shape: {tokens.shape}" + batch["full_loss_masks"] = loss_masks + + # Process multimodal training tensors if present + multimodal_train_inputs = batch.get("multimodal_train_inputs", None) + if multimodal_train_inputs is not None: + multimodal_data = {} # key -> concatenated tensor + multimodal_num_items = {} # key -> list of item counts per sequence + for mm_input_dict in multimodal_train_inputs: + if mm_input_dict is not None: + for key, mm_tensor in mm_input_dict.items(): + if key not in multimodal_data: + multimodal_data[key] = mm_tensor + multimodal_num_items[key] = [mm_tensor.size(0)] + else: + multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0) + multimodal_num_items[key].append(mm_tensor.size(0)) + batch["multimodal_train_inputs"] = multimodal_data + batch["multimodal_num_items"] = multimodal_num_items + + return batch + + +class DataIterator: + """Micro-batch iterator over rollout dicts. + + Supports either fixed contiguous micro-batches or an explicit per-step + index schedule (for dynamic batch sizing / sequence-length balancing). + """ + + def __init__( + self, + rollout_data: RolloutBatch, + micro_batch_size: int | None = None, + micro_batch_indices: list[list[int]] | None = None, + ) -> None: + """Initialize an iterator over `rollout_data`. + + Args: + rollout_data: Dict of per-sample fields for the local step. + micro_batch_size: Fixed contiguous slice size when not using dynamic scheduling. + micro_batch_indices: Explicit indices per micro-batch when using dynamic balancing. + Must be mutually exclusive with `micro_batch_size`. + """ + self.rollout_data = rollout_data + self.micro_batch_size = micro_batch_size + self.micro_batch_indices = micro_batch_indices + assert micro_batch_size is None or micro_batch_indices is None + self.offset = 0 + + def get_next(self, keys: Sequence[str]) -> dict[str, list[object] | None]: + """Return the next micro-batch for the requested keys. + + - If `micro_batch_indices` is provided, selects rows according to the current + index list for each requested key. + - Otherwise, slices a contiguous window of size `micro_batch_size` starting + at the current offset. + + Returns a dict mapping each key to a list subset (or None if absent). + """ + batch = {} + for key in keys: + vals = self.rollout_data.get(key, None) + if vals is None: + batch[key] = None + else: + if self.micro_batch_indices is not None: + indices = self.micro_batch_indices[self.offset] + batch[key] = [vals[i] for i in indices] + else: + assert self.offset + self.micro_batch_size <= len( + vals + ), f"offset: {self.offset}, micro_batch_size: {self.micro_batch_size}, len(vals): {len(vals)}" + batch[key] = vals[self.offset : self.offset + self.micro_batch_size] + + if self.micro_batch_indices is not None: + self.offset += 1 + else: + self.offset += self.micro_batch_size + return batch + + def reset(self) -> "DataIterator": + """Reset internal offset to the start and return self.""" + self.offset = 0 + return self + + +def get_data_iterator( + args: Namespace, + model: torch.nn.Module | Sequence[torch.nn.Module], + parallel_state: ParallelState, + rollout_data: RolloutBatch, +) -> tuple[list[DataIterator], list[int]]: + """ + Create iterators and a micro-batch schedule for a rollout step. + + - If `use_dynamic_batch_size` is False, splits into fixed-size contiguous + micro-batches of `micro_batch_size`. + - If True, computes the number of micro-batches per local step based on + `max_tokens_per_gpu` and per-sample lengths, all-reduces to a DP-wide + maximum, optionally enforces divisibility for Virtual Pipeline Parallelism (VPP), and builds a balanced + index schedule to equalize token counts across micro-batches. + + Returns `(data_iterators, num_microbatches)` where: + - `data_iterators`: list of `DataIterator`, one per VPP stage (size 1 if VPP disabled) + - `num_microbatches`: list[int], one per local step in the rollout (length = steps) + """ + dp_size = parallel_state.dp_size + dp_group = parallel_state.dp_group + vpp_size = parallel_state.vpp_size + microbatch_group_size_per_vp_stage = parallel_state.microbatch_group_size_per_vp_stage + + cp_size = parallel_state.cp_size + + num_local_samples = len(rollout_data["total_lengths"]) + global_batch_size = rollout_data.get("dynamic_global_batch_size", args.global_batch_size) + num_local_gbs = global_batch_size // dp_size + num_steps_per_rollout = num_local_samples // num_local_gbs + + if global_batch_size != args.global_batch_size: + logger.info( + f"Using dynamic global_batch_size={global_batch_size} (original={args.global_batch_size}), " + f"num_local_samples={num_local_samples}, num_steps_per_rollout={num_steps_per_rollout}" + ) + + def _generate_data_iterator(rollout_data, micro_batch_size, micro_batch_indices=None): + data_iterator = [] + for _ in range(vpp_size): + data_iterator.append(DataIterator(rollout_data, micro_batch_size, micro_batch_indices)) + return data_iterator + + if not args.use_dynamic_batch_size: + num_microbatches = [num_local_gbs // args.micro_batch_size for _ in range(num_steps_per_rollout)] + data_iterator = _generate_data_iterator(rollout_data, args.micro_batch_size) + else: + assert args.max_tokens_per_gpu is not None + # calculate the number of mirobatches for each step + samples = rollout_data["total_lengths"] + assert len(samples) == num_local_samples + num_microbatches = [] + for i in range(num_steps_per_rollout): + start, end = i * num_local_gbs, (i + 1) * num_local_gbs + num_microbatches.append( + get_minimum_num_micro_batch_size(samples[start:end], args.max_tokens_per_gpu * cp_size) + ) + + num_microbatches = torch.tensor(num_microbatches, dtype=torch.int, device=torch.cuda.current_device()) + dist.all_reduce(num_microbatches, op=dist.ReduceOp.MAX, group=dp_group) + + if vpp_size > 1: + # vpp requies the number of microbatches to be divisible by vpp_size + num_microbatches = torch.clamp( + num_microbatches // microbatch_group_size_per_vp_stage * microbatch_group_size_per_vp_stage, + min=1, + ) + + num_microbatches = num_microbatches.tolist() + + # balance the each micro batch + samples = rollout_data["total_lengths"] + # balance the number of mirobatches across steps + micro_batch_indices = [] + for i, num_mbs in enumerate(num_microbatches): + start, end = i * num_local_gbs, (i + 1) * num_local_gbs + samples = rollout_data["total_lengths"][start:end] + partitions = get_seqlen_balanced_partitions(samples, num_mbs, equal_size=False) + for j in range(num_mbs): + for k in range(len(partitions[j])): + partitions[j][k] += start + micro_batch_indices.extend(partitions) + + assert len(set(sum(micro_batch_indices, []))) == num_local_samples + + data_iterator = _generate_data_iterator(rollout_data, None, micro_batch_indices) + + return ( + data_iterator, + num_microbatches, + ) + + +def sync_actor_critic_data( + args: Namespace, + rollout_data: RolloutBatch | None = None, + group: dist.ProcessGroup | None = None, +) -> None: + """ + Broadcast `values` (from critic) and optionally `log_probs`/`ref_log_probs` + (from actor) across PP ranks to align data dependencies. + + - Values are broadcast from src=1. + - Log-probs and ref-log-probs are broadcast from src=0 when KL is used. + Updates `rollout_data` in place with the synchronized tensors. + """ + log_probs_key = "log_probs" if not args.use_rollout_logprobs else "rollout_log_probs" + values, log_probs, ref_log_probs = map(rollout_data.get, ("values", log_probs_key, "ref_log_probs")) + + # return when not the pp last stage + if not values and not log_probs: + return + + handles = [] + + if not values: + values = [torch.empty_like(log_prob) for log_prob in log_probs] + for value in values: + handles.append(dist.broadcast(value, src=1, group=group, async_op=True)) + + if args.kl_coef != 0 or args.use_kl_loss: + if not log_probs: + log_probs = [torch.empty_like(value) for value in values] + if not ref_log_probs: + ref_log_probs = [torch.empty_like(value) for value in values] + for ref_log_prob, log_prob in zip(ref_log_probs, log_probs, strict=False): + handles.append(dist.broadcast(log_prob, src=0, group=group, async_op=True)) + handles.append(dist.broadcast(ref_log_prob, src=0, group=group, async_op=True)) + + for handle in handles: + handle.wait() + + rollout_data.update( + { + k: v + for k, v in { + "values": values, + log_probs_key: log_probs, + "ref_log_probs": ref_log_probs, + }.items() + if v is not None + } + ) diff --git a/miles/backends/training_utils/log_utils.py b/miles/backends/training_utils/log_utils.py new file mode 100644 index 000000000..1a2f17602 --- /dev/null +++ b/miles/backends/training_utils/log_utils.py @@ -0,0 +1,408 @@ +import logging +from argparse import Namespace +from math import isclose + +import numpy as np +import torch +import torch.distributed as dist + +from miles.utils import train_metric_utils +from miles.utils.flops_utils import calculate_fwd_flops +from miles.utils.metric_utils import compute_pass_rate, compute_rollout_step +from miles.utils.types import RolloutBatch + +from ...utils import tracking_utils +from .cp_utils import get_sum_of_sample_mean +from .data import DataIterator +from .parallel import ParallelState + +logger = logging.getLogger(__name__) + + +def gather_log_data( + metric_name: str, + args: Namespace, + rollout_id: int, + log_dict: dict[str, float], + parallel_state: ParallelState, +) -> dict[str, float] | None: + """ + Gather per-rank metrics, reduce by mean on the DP source rank, and log. + + Expects `log_dict` to contain plain scalars. The DP source rank prints and + optionally logs to WandB/TensorBoard with a step derived from `rollout_id` and + batch sizes. Returns the reduced dict on the DP source rank; returns None on others. + """ + + if parallel_state.dp_cp_rank == 0: + dp_size = parallel_state.dp_cp_size + + gathered_log_dict = [None] * dp_size + # Not sure if this will be a performance bottleneck. + dist.gather_object( + log_dict, + gathered_log_dict, + dst=parallel_state.dp_src_rank, + group=parallel_state.dp_cp_group_gloo, + ) + + reduced_log_dict = { + f"{metric_name}/{key}": sum([d[key] for d in gathered_log_dict]) / dp_size for key in log_dict + } + logger.info(f"{metric_name} {rollout_id}: {reduced_log_dict}") + + # Calculate step once to avoid duplication + step = compute_rollout_step(args, rollout_id) + reduced_log_dict["rollout/step"] = step + tracking_utils.log(args, reduced_log_dict, step_key="rollout/step") + + return reduced_log_dict + else: + dist.gather_object( + log_dict, + None, + dst=parallel_state.dp_src_rank, + group=parallel_state.dp_cp_group_gloo, + ) + return None + + +def aggregate_forward_results( + forward_data_store: list[dict[str, list]], + data_iterator: DataIterator, + args: Namespace, + store_prefix: str = "", +) -> dict[str, list]: + rollout_data = {} + if not forward_data_store: + return rollout_data + + keys = forward_data_store[0].keys() + for key in keys: + values = [] + for batch_result in forward_data_store: + assert isinstance(batch_result[key], list), f"Expected list for key {key}, got {type(batch_result[key])}" + values += batch_result[key] + + # Handle dynamic batch size: restore original order + if args.use_dynamic_batch_size and hasattr(data_iterator, "micro_batch_indices"): + origin_values = [None] * len(values) + origin_indices = sum(data_iterator.micro_batch_indices, []) + for value, origin_index in zip(values, origin_indices, strict=False): + origin_values[origin_index] = value + values = origin_values + + rollout_data[key] = values + + return rollout_data + + +def log_rollout_data( + rollout_id: int, args: Namespace, rollout_data: RolloutBatch, parallel_state: ParallelState +) -> None: + """ + Summarize rollout fields and log reduced metrics on PP last stage, TP rank 0. + + - Tensor-valued lists are concatenated and averaged. For token-level metrics + like log-probs/returns/advantages/values, computes a CP-correct sample mean + using `loss_masks` and total/response lengths. + - Non-tensor lists are averaged elementwise. + - Scalars are converted to Python numbers. + """ + if parallel_state.tp_rank == 0 and parallel_state.is_pp_last_stage: + cp_size = parallel_state.cp_size + log_dict = {} + response_lengths = rollout_data["response_lengths"] + loss_masks = rollout_data["loss_masks"] + total_lengths = rollout_data["total_lengths"] + max_seq_lens = rollout_data.get("max_seq_lens", None) + + for key, val in rollout_data.items(): + if key in [ + "tokens", + "multimodal_train_inputs", + "loss_masks", + "sample_indices", + "rollout_routed_experts", + "max_seq_lens", + "dynamic_global_batch_size", + ]: + continue + # Upload per sample mean for each rollout value + # There are the following assumptions: + # - Each dp rank has the same number of samples + if isinstance(val, (list, tuple)): + if isinstance(val[0], torch.Tensor): + # NOTE: Here we have to do the clone().detach(), otherwise the tensor will be + # modified in place and will cause problem for the next rollout. + val = torch.cat(val).clone().detach() + if key in ["log_probs", "ref_log_probs", "rollout_log_probs", "returns", "advantages", "values"]: + sum_of_sample_mean = get_sum_of_sample_mean( + total_lengths, + response_lengths, + loss_masks, + parallel_state, + qkv_format=args.qkv_format, + max_seq_lens=max_seq_lens, + ) + val = cp_size * sum_of_sample_mean(val) / len(loss_masks) + else: + val = val.mean() * cp_size + else: + val = sum(val) / len(val) + elif isinstance(val, torch.Tensor): + val = val.float().mean() + else: + raise ValueError(f"Unsupported type: {type(val)} for key: {key}") + log_dict[key] = val.item() if isinstance(val, torch.Tensor) else val + + reduced_log_dict = gather_log_data("rollout", args, rollout_id, log_dict, parallel_state) + if args.ci_test and reduced_log_dict is not None: + if ( + rollout_id == 0 + and "rollout/log_probs" in reduced_log_dict + and "rollout/ref_log_probs" in reduced_log_dict + ): + assert reduced_log_dict["rollout/log_probs"] == reduced_log_dict["rollout/ref_log_probs"] + if "rollout/log_probs" in reduced_log_dict and "rollout/rollout_log_probs" in reduced_log_dict: + assert isclose( + reduced_log_dict["rollout/log_probs"], reduced_log_dict["rollout/rollout_log_probs"], abs_tol=0.03 + ) + if "rollout/entropy" in reduced_log_dict: + assert 0 < reduced_log_dict["rollout/entropy"] < 0.7 + + if args.log_multi_turn: + log_multi_turn_data(rollout_id, args, rollout_data, parallel_state) + if args.log_passrate: + log_passrate(rollout_id, args, rollout_data) + + if args.log_correct_samples: + if parallel_state.tp_rank == 0 and parallel_state.is_pp_last_stage: + cp_size = parallel_state.cp_size + log_dict = {} + response_lengths = rollout_data["response_lengths"] + loss_masks = rollout_data["loss_masks"] + total_lengths = rollout_data["total_lengths"] + + def quantile(total_value, n_quantiles, data) -> dict: + import math + + assert n_quantiles > 1, f"n_quantiles({n_quantiles}) must be greater than 1." + + quantiles = [((i + 1) / n_quantiles) for i in range(n_quantiles)] + cut_points = [total_value * q for q in quantiles] + cut_points[-1] = total_value + + count = [0] * n_quantiles + for d in data: + for i, point in enumerate(cut_points): + if d <= point: + count[i] += 1 + break + + total = sum(count) + 1e-9 + percentile = [c / total for c in count] + + percentile = {f"p{min(math.ceil(q*100),100)}": p for q, p in zip(quantiles, percentile, strict=True)} + return percentile + + raw_rewards = rollout_data["raw_reward"] + # Additional metrics for correct cases are calculated separately below. + correct_response_lengths = [] + correct_total_lengths = [] + correct_loss_masks = [] + correct_entropy = [] + for i, raw_reward in enumerate(raw_rewards): + if raw_reward == 1: + correct_response_lengths.append(response_lengths[i]) + correct_total_lengths.append(total_lengths[i]) + correct_loss_masks.append(loss_masks[i]) + correct_entropy.append(-rollout_data["log_probs"][i]) + num_correct_responses = len(correct_total_lengths) + rollout_data["correct_response_lengths"] = correct_response_lengths + correct_response_length_percentile = quantile( + args.rollout_max_response_len, 4, rollout_data["correct_response_lengths"] + ) + for p, val in correct_response_length_percentile.items(): + rollout_data[f"correct_length/{p}"] = [val] * num_correct_responses + if len(correct_entropy) > 0: + sum_of_sample_mean = get_sum_of_sample_mean( + correct_total_lengths, correct_response_lengths, correct_loss_masks, parallel_state + ) + correct_entropy = sum_of_sample_mean(torch.cat(correct_entropy, dim=0)) + rollout_data["correct_entropy"] = [correct_entropy.item()] * num_correct_responses + else: + rollout_data["correct_entropy"] = [0] * num_correct_responses + + +def log_multi_turn_data( + rollout_id: int, args: Namespace, rollout_data: RolloutBatch, parallel_state: ParallelState +) -> None: + """ + Log multi-turn auxiliary metrics such as raw/observed response lengths and rounds. + + Operates only on PP last stage and TP rank 0. Uses GPU tensors when available + to compute statistics without host transfers. + """ + if parallel_state.tp_rank == 0 and parallel_state.is_pp_last_stage: + log_dict = {} + for key, val in rollout_data.items(): + if key == "loss_masks": + if val: # Check if val is not empty + device = val[0].device # Get device from first tensor + + # Vectorized length calculation using torch + raw_response_lengths = torch.tensor([v.shape[0] for v in val], dtype=torch.float32, device=device) + log_dict["raw_response_length/response_length_mean"] = raw_response_lengths.mean().item() + log_dict["raw_response_length/response_length_max"] = raw_response_lengths.max().item() + log_dict["raw_response_length/response_length_min"] = raw_response_lengths.min().item() + log_dict["raw_response_length/response_length_clip_ratio"] = ( + (raw_response_lengths >= args.rollout_max_response_len).float().mean().item() + ) + + # Vectorized sum calculation using torch - stay on GPU + wo_obs_response_lengths = torch.tensor( + [v.sum().item() for v in val], dtype=torch.float32, device=device + ) + log_dict["wo_obs_response_length/response_length_mean"] = wo_obs_response_lengths.mean().item() + log_dict["wo_obs_response_length/response_length_max"] = wo_obs_response_lengths.max().item() + log_dict["wo_obs_response_length/response_length_min"] = wo_obs_response_lengths.min().item() + if key == "round_number": + # Use numpy for vectorized round number statistics + round_number_array = np.array(val) + log_dict["multi_turn_metric/round_number_mean"] = np.mean(round_number_array) + log_dict["multi_turn_metric/round_number_max"] = np.max(round_number_array) + log_dict["multi_turn_metric/round_number_min"] = np.min(round_number_array) + gather_log_data("multi_turn", args, rollout_id, log_dict, parallel_state) + + +def log_passrate(rollout_id: int, args: Namespace, rollout_data: RolloutBatch, parallel_state: ParallelState) -> None: + """ + Compute pass@k metrics from `raw_reward` groups and log the results. + + `raw_reward` is reshaped to `[group_number, group_size]`, then pass@k is + estimated per problem and averaged. + """ + if parallel_state.tp_rank == 0 and parallel_state.is_pp_last_stage: + log_dict = {} + for key, val in rollout_data.items(): + if key != "raw_reward": + continue + + log_dict |= compute_pass_rate( + flat_rewards=val, + group_size=args.n_samples_per_prompt, + num_groups=args.rollout_batch_size, + ) + + gather_log_data("passrate", args, rollout_id, log_dict, parallel_state) + + +def log_perf_data(rollout_id: int, args: Namespace, parallel_state: ParallelState) -> None: + train_metric_utils.log_perf_data_raw( + rollout_id=rollout_id, + args=args, + is_primary_rank=( + parallel_state.tp_rank == 0 and parallel_state.is_pp_last_stage and parallel_state.dp_cp_rank == 0 + ), + compute_total_fwd_flops=lambda seq_lens: calculate_fwd_flops(seqlens=seq_lens, args=args) + / dist.get_world_size() + / 1e12, + ) + + +def aggregate_train_losses( + losses_reduced: list[dict[str, list[str] | torch.Tensor]], + parallel_state: ParallelState, +) -> dict[str, float]: + """Aggregate loss metrics across micro-batches. + + Sums loss values across all micro-batches, performs all-reduce across + the data-parallel group, and computes per-sample/token averages. + + Args: + losses_reduced: List of log_dict from each micro-batch. + Each log_dict has format: {"keys": list[str], "values": torch.Tensor} + parallel_state: Parallel state containing dp_group and cp_size. + + Returns: + Dictionary mapping metric names to averaged values. + """ + if not losses_reduced: + return {} + + keys = losses_reduced[0]["keys"] + + values = None + for log_dict in losses_reduced: + if values is None: + values = log_dict["values"].clone() + else: + values += log_dict["values"] + + assert len(keys) + 1 == values.numel(), f"Expected {len(keys) + 1} values, got {values.numel()}" + + dist.all_reduce(values, op=dist.ReduceOp.SUM, group=parallel_state.dp_cp_group) + + loss_reduced = {} + values = values.tolist() + num_samples_or_tokens = values[0] + + for key, value in zip(keys, values[1:], strict=False): + loss_reduced[key] = value * parallel_state.cp_size / num_samples_or_tokens + + return loss_reduced + + +def log_train_step( + args: Namespace, + loss_dict: dict[str, float], + grad_norm: float, + rollout_id: int, + step_id: int, + num_steps_per_rollout: int, + role: str = "actor", + extra_metrics: dict[str, float] | None = None, + should_log: bool | None = None, +) -> dict[str, float]: + """Log training metrics for one step. + + Formats loss metrics, gradient norm, and extra metrics (e.g., learning rates, MTP loss) for tracking. + + Args: + args: Configuration. + loss_dict: Dictionary of loss metrics from aggregate_train_losses. + grad_norm: Gradient norm after clipping. + rollout_id: Rollout ID. + step_id: Step ID within the rollout. + num_steps_per_rollout: Total number of steps per rollout. + role: Role name (e.g., "actor", "critic"). + extra_metrics: Optional extra metrics to log (e.g., learning rates, MTP loss). + should_log: Optional override for logging condition. If None, uses rank == 0. + + Returns: + The formatted log_dict (for CI tests or other uses). + """ + accumulated_step_id = rollout_id * num_steps_per_rollout + step_id + role_tag = "" if role == "actor" else f"{role}-" + + log_dict_out = { + f"train/{role_tag}{key}": val.mean().item() if isinstance(val, torch.Tensor) else val + for key, val in loss_dict.items() + } + log_dict_out[f"train/{role_tag}grad_norm"] = float(grad_norm) + + if extra_metrics: + for key, val in extra_metrics.items(): + log_dict_out[f"train/{role_tag}{key}"] = val + + log_dict_out["train/step"] = accumulated_step_id + + if should_log is None: + should_log = dist.get_rank() == 0 + + if should_log: + tracking_utils.log(args, log_dict_out, step_key="train/step") + logger.info(f"{role_tag}step {accumulated_step_id}: {log_dict_out}") + + return log_dict_out diff --git a/miles/backends/megatron_utils/loss.py b/miles/backends/training_utils/loss.py similarity index 94% rename from miles/backends/megatron_utils/loss.py rename to miles/backends/training_utils/loss.py index 3afff1f49..a7f88d137 100644 --- a/miles/backends/megatron_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -3,7 +3,6 @@ from typing import Any import torch -from megatron.core import mpu from torch.utils.checkpoint import checkpoint from miles.utils.distributed_utils import distributed_masked_whiten @@ -22,12 +21,14 @@ from miles.utils.types import RolloutBatch from .cp_utils import all_gather_with_cp, get_logits_and_tokens_offset_with_cp, get_sum_of_sample_mean +from .parallel import ParallelState def get_responses( logits: torch.Tensor, *, args: Namespace, + parallel_state: ParallelState, unconcat_tokens: list[torch.Tensor], total_lengths: list[int], response_lengths: list[int], @@ -68,7 +69,7 @@ def get_responses( logits = logits.div(args.rollout_temperature) - cp_size = mpu.get_context_parallel_world_size() + cp_size = parallel_state.cp_size end = 0 for i, (tokens, total_length, response_length) in enumerate( zip(unconcat_tokens, total_lengths, response_lengths, strict=False) @@ -87,7 +88,7 @@ def get_responses( else: # TODO: this is super ugly... do better abstraction. chunk_size, chunks_offset, logits_offset, tokens_offset = get_logits_and_tokens_offset_with_cp( - total_length, response_length, qkv_format, max_seq_len + total_length, response_length, parallel_state, qkv_format, max_seq_len ) logits_0, logits_1 = logits[end : end + chunk_size], logits[end + chunk_size : end + 2 * chunk_size] @@ -112,6 +113,7 @@ def get_log_probs_and_entropy( logits: torch.Tensor, *, args: Namespace, + parallel_state: ParallelState, unconcat_tokens: list[torch.Tensor], total_lengths: list[int], response_lengths: list[int], @@ -147,6 +149,7 @@ def get_log_probs_and_entropy( for logits_chunk, tokens_chunk in get_responses( logits, args=args, + parallel_state=parallel_state, unconcat_tokens=unconcat_tokens, total_lengths=total_lengths, response_lengths=response_lengths, @@ -155,7 +158,7 @@ def get_log_probs_and_entropy( log_prob, entropy = calculate_log_probs_and_entropy( logits_chunk, tokens_chunk, - mpu.get_tensor_model_parallel_group(), + parallel_state.tp_group, with_entropy=with_entropy, chunk_size=args.log_probs_chunk_size, ) @@ -175,6 +178,7 @@ def get_values( logits: torch.Tensor, *, args: Namespace, + parallel_state: ParallelState, unconcat_tokens: list[torch.Tensor], total_lengths: list[int], response_lengths: list[int], @@ -205,6 +209,7 @@ def get_values( for logits_chunk, _ in get_responses( logits, args=args, + parallel_state=parallel_state, unconcat_tokens=unconcat_tokens, total_lengths=total_lengths, response_lengths=response_lengths, @@ -218,7 +223,7 @@ def get_values( } -def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) -> None: +def compute_advantages_and_returns(args: Namespace, parallel_state: ParallelState, rollout_data: RolloutBatch) -> None: """Compute advantages and returns in-place based on `args.advantage_estimator`. This function extracts rewards, log-probs, values, and masks from @@ -276,14 +281,14 @@ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) old_rewards = rewards rewards = [] kl_coef = -args.kl_coef - cp_rank = mpu.get_context_parallel_rank() + cp_rank = parallel_state.cp_rank for reward, k in zip(old_rewards, kl, strict=False): k *= kl_coef if cp_rank == 0: k[-1] += reward rewards.append(k) advantages, returns = get_advantages_and_returns_batch( - total_lengths, response_lengths, values, rewards, args.gamma, args.lambd + total_lengths, response_lengths, values, rewards, args.gamma, args.lambd, parallel_state ) elif args.advantage_estimator == "reinforce_plus_plus": @@ -296,6 +301,7 @@ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) total_lengths=total_lengths, kl_coef=args.kl_coef, gamma=args.gamma, + parallel_state=parallel_state, ) advantages = [r for r in returns] @@ -331,7 +337,7 @@ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) # TODO: OpenRLHF always does advantages normalization but veRL doesn't seem to do it. if args.normalize_advantages: all_advs = torch.cat(advantages) - cp_size = mpu.get_context_parallel_world_size() + cp_size = parallel_state.cp_size if cp_size == 1: all_masks = torch.cat(loss_masks) else: @@ -343,7 +349,7 @@ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None _, _, _, token_offsets = get_logits_and_tokens_offset_with_cp( - total_len, response_len, args.qkv_format, max_seq_len + total_len, response_len, parallel_state, args.qkv_format, max_seq_len ) # Convert global offsets to response-space offsets @@ -373,7 +379,7 @@ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) assert ( all_advs.size() == all_masks.size() ), f"Shape mismatch before whitening: advantages {all_advs.size()}, masks {all_masks.size()}" - dp_group = mpu.get_data_parallel_group() + dp_group = parallel_state.dp_group whitened_advs_flat = distributed_masked_whiten( all_advs, @@ -440,6 +446,7 @@ def icepop_function( def policy_loss_function( args: Namespace, + parallel_state: ParallelState, batch: RolloutBatch, logits: torch.Tensor, sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor], @@ -478,6 +485,7 @@ def policy_loss_function( log_probs_and_entropy = get_log_probs_and_entropy( logits, args=args, + parallel_state=parallel_state, unconcat_tokens=batch["unconcat_tokens"], total_lengths=total_lengths, response_lengths=response_lengths, @@ -494,13 +502,13 @@ def policy_loss_function( full_old_log_probs = None if need_full_log_probs: full_log_probs = [ - all_gather_with_cp(log_prob, total_length, response_length) + all_gather_with_cp(log_prob, total_length, response_length, parallel_state) for log_prob, total_length, response_length in zip( log_probs, total_lengths, response_lengths, strict=False ) ] full_old_log_probs = [ - all_gather_with_cp(old_log_prob, total_length, response_length) + all_gather_with_cp(old_log_prob, total_length, response_length, parallel_state) for old_log_prob, total_length, response_length in zip( old_log_probs, total_lengths, response_lengths, strict=False ) @@ -559,6 +567,7 @@ def policy_loss_function( "loss_masks": batch["loss_masks"], "total_lengths": total_lengths, "response_lengths": response_lengths, + "parallel_state": parallel_state, } if args.custom_tis_function_path is not None: @@ -573,6 +582,7 @@ def policy_loss_function( total_lengths, response_lengths, modified_response_masks, + parallel_state, args.calculate_per_token_loss, args.qkv_format, max_seq_lens, @@ -656,6 +666,7 @@ def policy_loss_function( def value_loss_function( args: Namespace, + parallel_state: ParallelState, batch: RolloutBatch, logits: torch.Tensor, sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor], @@ -682,6 +693,7 @@ def value_loss_function( values = get_values( logits, args=args, + parallel_state=parallel_state, unconcat_tokens=batch["unconcat_tokens"], total_lengths=batch["total_lengths"], response_lengths=batch["response_lengths"], @@ -714,6 +726,7 @@ def value_loss_function( def sft_loss_function( args: Namespace, + parallel_state: ParallelState, batch: RolloutBatch, logits: torch.Tensor, sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor], @@ -740,6 +753,7 @@ def sft_loss_function( log_probs_and_entropy = get_log_probs_and_entropy( logits, args=args, + parallel_state=parallel_state, unconcat_tokens=batch["unconcat_tokens"], total_lengths=total_lengths, response_lengths=response_lengths, @@ -765,9 +779,11 @@ def sft_loss_function( def loss_function( args: Namespace, + parallel_state: ParallelState, batch: RolloutBatch, num_microbatches: int, logits: torch.Tensor, + apply_megatron_loss_scaling: bool = False, ) -> tuple[torch.Tensor, int | torch.Tensor, dict[str, list[str] | torch.Tensor]]: """Dispatch to the configured loss and rescale for Megatron integration. @@ -799,6 +815,7 @@ def loss_function( batch["total_lengths"], batch["response_lengths"], batch["loss_masks"], + parallel_state, args.calculate_per_token_loss, args.qkv_format, batch.get("max_seq_lens", None), @@ -817,18 +834,27 @@ def loss_function( raise ValueError(f"Unknown loss type: {args.loss_type}") if args.recompute_loss_function: - loss, log = checkpoint(func, args, batch, logits, sum_of_sample_mean) + loss, log = checkpoint( + func, + args, + parallel_state, + batch, + logits, + sum_of_sample_mean, + ) else: - loss, log = func(args, batch, logits, sum_of_sample_mean) + loss, log = func(args, parallel_state, batch, logits, sum_of_sample_mean) # Here we need to divide by cp_size because to cancel the multiply in Megatron. global_batch_size = batch.get("dynamic_global_batch_size", args.global_batch_size) if not args.calculate_per_token_loss: - loss = ( - loss * num_microbatches / global_batch_size * mpu.get_data_parallel_world_size(with_context_parallel=True) - ) + if apply_megatron_loss_scaling: + loss = loss * num_microbatches / global_batch_size * parallel_state.dp_cp_size + else: + loss = loss / global_batch_size * parallel_state.dp_size else: - loss = loss * mpu.get_context_parallel_world_size() + if apply_megatron_loss_scaling: + loss = loss * parallel_state.cp_size return ( loss, diff --git a/miles/backends/training_utils/parallel.py b/miles/backends/training_utils/parallel.py new file mode 100644 index 000000000..4283e8731 --- /dev/null +++ b/miles/backends/training_utils/parallel.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +import torch.distributed as dist + + +@dataclass +class ParallelState: + """Core parallel state shared across all backends. + Required by the general training utils. + """ + + dp_rank: int + dp_src_rank: int + dp_size: int + cp_rank: int + cp_size: int + dp_cp_rank: int + dp_cp_size: int + dp_group: dist.ProcessGroup | None + dp_cp_group: dist.ProcessGroup | None + dp_cp_group_gloo: dist.ProcessGroup | None + cp_group: dist.ProcessGroup | None + tp_size: int + tp_rank: int + tp_group: dist.ProcessGroup | None + is_pp_last_stage: bool = True + vpp_size: int | None = 1 + microbatch_group_size_per_vp_stage: int | None = None diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 51b3d970b..79b2c419c 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1416,6 +1416,8 @@ def parse_args(add_custom_arguments=None): args.rank = 0 # Primary process rank for wandb initialization args.world_size = args.actor_num_nodes * args.actor_num_gpus_per_node + assert args.context_parallel_size == 1, "Context parallelism is not supported for FSDP backend." + miles_validate_args(args) if backend == "megatron": diff --git a/miles/utils/ppo_utils.py b/miles/utils/ppo_utils.py index d2d44c6be..3883fa423 100644 --- a/miles/utils/ppo_utils.py +++ b/miles/utils/ppo_utils.py @@ -6,6 +6,7 @@ import torch import torch.distributed as dist import torch.nn.functional as F +from miles.backends.training_utils.parallel import ParallelState @torch.compile(dynamic=True) @@ -149,6 +150,7 @@ def compute_policy_loss( def compute_log_probs(logits: torch.Tensor, tokens: torch.Tensor, process_group: dist.ProcessGroup | None): + # TODO: when megatron is not installed, fall back to naive implementation from megatron.core.fusions.fused_cross_entropy import fused_vocab_parallel_cross_entropy # convert to [seq_len, batch_size, vocab_size] as expected by fused_vocab_parallel_cross_entropy @@ -215,6 +217,7 @@ def get_reinforce_plus_plus_returns( total_lengths: list[int], kl_coef: float, gamma: float, + parallel_state: ParallelState, ) -> list[torch.Tensor]: """ Calculates discounted returns for REINFORCE++ (https://arxiv.org/pdf/2501.03262) @@ -245,7 +248,7 @@ def get_reinforce_plus_plus_returns( # Step 1,2:Gather all chunks and token_offsets from all ranks and reconstruct the full response tensor by splitting and placing each part from miles.backends.megatron_utils.cp_utils import all_gather_with_cp - full_kl_response = all_gather_with_cp(local_kl_chunk, total_len, response_len) + full_kl_response = all_gather_with_cp(local_kl_chunk, total_len, response_len, parallel_state) else: full_kl_response = local_kl_chunk @@ -267,7 +270,7 @@ def get_reinforce_plus_plus_returns( if cp_size > 1: from miles.backends.megatron_utils.cp_utils import slice_log_prob_with_cp - local_returns_chunk = slice_log_prob_with_cp(returns_for_seq, total_len, response_len) + local_returns_chunk = slice_log_prob_with_cp(returns_for_seq, total_len, response_len, parallel_state) else: local_returns_chunk = returns_for_seq @@ -313,6 +316,7 @@ def get_advantages_and_returns( rewards: torch.Tensor, gamma: float, lambd: float, + parallel_state: ParallelState, ) -> tuple[torch.Tensor, torch.Tensor]: """Function that computes advantages and returns from rewards and values. Calculated as in the original PPO paper: https://arxiv.org/abs/1707.06347 @@ -340,8 +344,8 @@ def get_advantages_and_returns( if cp_size > 1: from miles.backends.megatron_utils.cp_utils import all_gather_with_cp - full_rewards = all_gather_with_cp(rewards, total_len, response_len) - full_values = all_gather_with_cp(values, total_len, response_len) + full_rewards = all_gather_with_cp(rewards, total_len, response_len, parallel_state) + full_values = all_gather_with_cp(values, total_len, response_len, parallel_state) else: full_rewards = rewards full_values = values @@ -360,8 +364,8 @@ def get_advantages_and_returns( if cp_size > 1: from miles.backends.megatron_utils.cp_utils import slice_log_prob_with_cp - advantages = slice_log_prob_with_cp(full_advantages, total_len, response_len) - returns = slice_log_prob_with_cp(full_returns, total_len, response_len) + advantages = slice_log_prob_with_cp(full_advantages, total_len, response_len, parallel_state) + returns = slice_log_prob_with_cp(full_returns, total_len, response_len, parallel_state) else: advantages = full_advantages returns = full_returns @@ -376,6 +380,7 @@ def get_advantages_and_returns_batch( rewards_list, gamma, lambd, + parallel_state: ParallelState, chunked: bool = True, ): """ @@ -410,8 +415,8 @@ def get_advantages_and_returns_batch( for total_len, resp_len, v, r in zip( total_lengths, response_lengths, values_list, rewards_list, strict=False ): - full_v = all_gather_with_cp(v, total_len, resp_len) - full_r = all_gather_with_cp(r, total_len, resp_len) + full_v = all_gather_with_cp(v, total_len, resp_len, parallel_state) + full_r = all_gather_with_cp(r, total_len, resp_len, parallel_state) full_values_list.append(full_v) full_rewards_list.append(full_r) @@ -450,7 +455,7 @@ def get_advantages_and_returns_batch( returns_list = [] if cp_size > 1: - from miles.backends.megatron_utils.cp_utils import slice_log_prob_with_cp + from miles.backends.training_utils.cp_utils import slice_log_prob_with_cp for total_len, resp_len, adv_row, ret_row in zip( total_lengths, @@ -462,8 +467,8 @@ def get_advantages_and_returns_batch( adv_full = adv_row # shape = [resp_len_i padded to max_len] ret_full = ret_row - adv_sliced = slice_log_prob_with_cp(adv_full[:resp_len], total_len, resp_len) - ret_sliced = slice_log_prob_with_cp(ret_full[:resp_len], total_len, resp_len) + adv_sliced = slice_log_prob_with_cp(adv_full[:resp_len], total_len, resp_len, parallel_state) + ret_sliced = slice_log_prob_with_cp(ret_full[:resp_len], total_len, resp_len, parallel_state) advantages_list.append(adv_sliced) returns_list.append(ret_sliced) @@ -676,40 +681,3 @@ def calculate_log_probs_and_entropy(logits, tokens, tp_group, with_entropy: bool entropy = logits.new_zeros((0,)) return log_prob, entropy - - -def vanilla_tis_function( - args, - *, - pg_loss: torch.Tensor, - train_log_probs: list[torch.Tensor], - rollout_log_probs: list[torch.Tensor], - loss_masks: list[torch.Tensor], - **kwargs, -) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]: - """Apply TIS off-policy correction using importance sampling. - - Parameters: - args: Arguments containing TIS settings. - pg_loss: Policy gradient loss tensor of shape [total_seq_len - 1]. - train_log_probs: List of tensors containing training log-probabilities - for each sequence. - rollout_log_probs: List of tensors containing rollout log-probabilities - for each sequence. - loss_masks: List of tensors containing loss masks for each sequence. - """ - rollout_log_probs = torch.cat(rollout_log_probs, dim=0) - old_log_probs = torch.cat(train_log_probs, dim=0) - tis = torch.exp(old_log_probs - rollout_log_probs) - tis_abs = (tis - 1).abs() - tis_clip_low = args.tis_clip_low if args.tis_clip_low is not None else 0.1 - tis_clip_high = args.tis_clip if args.tis_clip is not None else 2.0 - tis_weights = torch.clamp(tis, min=tis_clip_low, max=tis_clip_high) - tis_clipfrac = (tis_weights != tis).float() - metrics = { - "tis": tis.clone().detach(), - "tis_clipfrac": tis_clipfrac.clone().detach(), - "tis_abs": tis_abs.clone().detach(), - } - pg_loss = pg_loss * tis_weights - return pg_loss, loss_masks, metrics diff --git a/tests/test_qwen3_0.6B_megatron_fsdp_align.py b/tests/test_qwen3_0.6B_megatron_fsdp_align.py new file mode 100644 index 000000000..1431d8c3d --- /dev/null +++ b/tests/test_qwen3_0.6B_megatron_fsdp_align.py @@ -0,0 +1,152 @@ +import os + +import miles.utils.external_utils.command_utils as U + +MODEL_NAME = "Qwen3-0.6B" +MODEL_TYPE = "qwen3-0.6B" +NUM_GPUS = 4 +CP_SIZE = 1 +MEGATRON_TP_SIZE = 1 +MEGATRON_PP_SIZE = 1 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + + U.convert_checkpoint( + model_name=MODEL_NAME, + megatron_model_type=MODEL_TYPE, + num_gpus_per_node=NUM_GPUS, + dir_dst="/root/models", + ) + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}/" + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + "--num-rollout 1 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 8192 " + "--rollout-temperature 1 " + "--global-batch-size 64 " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 8192 " + ) + + ppo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type k1 " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 4e-4 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 1 " "--sglang-chunked-prefill-size 4096 " "--sglang-mem-fraction-static 0.75 " + ) + + ci_args = "--ci-test " + + misc_args = "--actor-num-nodes 1 " "--colocate " f"--actor-num-gpus-per-node {NUM_GPUS} " + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{ppo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + debug_data_path = "test_rollout_data_megatron_fsdp_align.pt" + grad_norm_path = "grad_norm_fsdp.pt" + + fsdp_args = ( + "--train-backend fsdp " + "--attn-implementation flash_attention_2 " + "--gradient-checkpointing " + f"--context-parallel-size {CP_SIZE} " + f"--update-weight-buffer-size {512 * 1024 * 1024} " + """--train-env-vars '{"PYTORCH_CUDA_ALLOC_CONF":"expandable_segments:True"}' """ + ) + + try: + U.execute_train( + train_args=train_args + (f"{fsdp_args}" f"--save-debug-rollout-data {debug_data_path} "), + num_gpus_per_node=NUM_GPUS, + megatron_model_type=None, + ) + + U.execute_train( + train_args=train_args + + ( + f"{fsdp_args}" + f"--load-debug-rollout-data {debug_data_path} " + f"--ci-save-grad-norm {grad_norm_path} " + "--debug-train-only " + ), + num_gpus_per_node=NUM_GPUS, + megatron_model_type=None, + ) + + U.execute_train( + train_args=train_args + + ( + f"--ref-load /root/models/{MODEL_NAME}_torch_dist " + f"--tensor-model-parallel-size {MEGATRON_TP_SIZE} " + "--sequence-parallel " + f"--pipeline-model-parallel-size {MEGATRON_PP_SIZE} " + f"--context-parallel-size {CP_SIZE} " + "--expert-model-parallel-size 1 " + "--expert-tensor-parallel-size 1 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--train-memory-margin-bytes 3221225472 " + f"--load-debug-rollout-data {debug_data_path} " + f"--ci-load-grad-norm {grad_norm_path} " + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--debug-train-only " + ), + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + finally: + if os.path.exists(grad_norm_path): + os.remove(grad_norm_path) + if os.path.exists(debug_data_path): + os.remove(debug_data_path) + + +if __name__ == "__main__": + prepare() + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) + execute() From 20ab4f24e847829df492d1418da4e3b520b06be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=99=A8=E9=98=B3?= Date: Mon, 12 Jan 2026 22:41:17 -0800 Subject: [PATCH 16/57] [squashed] Support VLM Multi-turn Training with Megatron, Support INT4 training, bug fix, etc. (#426) --- docker/Dockerfile | 1 + docker/patch/latest/sglang.patch | 107 ++++++- docker/version.txt | 2 +- docs/en/advanced/pd-disaggregation.md | 2 +- docs/en/examples/glm4-9B.md | 2 +- docs/en/examples/qwen3-30B-A3B.md | 2 +- docs/en/examples/qwen3-4B.md | 2 +- docs/en/get_started/customization.md | 10 + docs/en/get_started/quick_start.md | 2 +- docs/en/get_started/usage.md | 4 +- docs/en/platform_support/amd_tutorial.md | 4 +- examples/README.md | 2 +- examples/eval/README.md | 24 +- examples/geo3k_vlm/README.md | 5 + examples/geo3k_vlm_multi_turn/README.md | 16 +- .../geo3k_vlm_multi_turn_reward.png | Bin 0 -> 228749 bytes examples/geo3k_vlm_multi_turn/rollout.py | 16 +- .../rollout_experiment_result_megatron.png | Bin 0 -> 208317 bytes .../vlm_multi_turn_geo3k_reward.png | Bin 53073 -> 0 bytes examples/low_precision/README.md | 76 ++++- .../run-kimi-k2-Thinking-int4.sh | 189 +++++++++++++ .../run-moonlight-16B-A3B-int4.sh | 165 +++++++++++ .../low_precision/run-qwen3-235B-A22B-int4.sh | 171 +++++++++++ .../low_precision/run-qwen3-30B-A3B-int4.sh | 165 +++++++++++ examples/low_precision/run-qwen3-4b-fp8.sh | 11 +- examples/on_policy_distillation/README.md | 4 +- examples/retool/README.md | 2 +- examples/search-r1/README.md | 2 +- examples/strands-agents/README.md | 56 ---- .../strands-agents/generate_with_strands.py | 267 ------------------ examples/strands_sglang/README.md | 70 +++++ .../strands_sglang/generate_with_strands.py | 117 ++++++++ .../requirements.txt | 1 + .../strands_qwen3_8b.sh} | 42 +-- examples/tau-bench/README.md | 4 +- examples/train_infer_mismatch_helper/mis.py | 2 +- miles/backends/megatron_utils/__init__.py | 2 +- .../megatron_utils/megatron_to_hf/__init__.py | 6 +- .../megatron_to_hf/processors/__init__.py | 15 + .../quantizer_compressed_tensors.py | 189 +++++++++++++ .../{quantizer.py => quantizer_fp8.py} | 4 +- .../update_weight_from_distributed.py | 34 ++- .../update_weight_from_tensor.py | 15 + miles/backends/sglang_utils/sglang_engine.py | 19 ++ miles/backends/training_utils/__init__.py | 0 miles/utils/ppo_utils.py | 6 +- miles/utils/types.py | 12 +- tools/convert_hf_to_fp8.py | 8 +- tools/convert_hf_to_hf_int4.py | 103 +++++++ 49 files changed, 1542 insertions(+), 416 deletions(-) create mode 100644 examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_reward.png create mode 100644 examples/geo3k_vlm_multi_turn/rollout_experiment_result_megatron.png delete mode 100644 examples/geo3k_vlm_multi_turn/vlm_multi_turn_geo3k_reward.png create mode 100644 examples/low_precision/run-kimi-k2-Thinking-int4.sh create mode 100644 examples/low_precision/run-moonlight-16B-A3B-int4.sh create mode 100644 examples/low_precision/run-qwen3-235B-A22B-int4.sh create mode 100644 examples/low_precision/run-qwen3-30B-A3B-int4.sh delete mode 100644 examples/strands-agents/README.md delete mode 100644 examples/strands-agents/generate_with_strands.py create mode 100644 examples/strands_sglang/README.md create mode 100644 examples/strands_sglang/generate_with_strands.py rename examples/{strands-agents => strands_sglang}/requirements.txt (75%) rename examples/{strands-agents/strands_qwen3_4b.sh => strands_sglang/strands_qwen3_8b.sh} (73%) create mode 100644 miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_compressed_tensors.py rename miles/backends/megatron_utils/megatron_to_hf/processors/{quantizer.py => quantizer_fp8.py} (96%) create mode 100644 miles/backends/training_utils/__init__.py create mode 100644 tools/convert_hf_to_hf_int4.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 1f63e3db4..a48dc5987 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -35,6 +35,7 @@ RUN git clone https://github.com/Dao-AILab/flash-attention.git && \ RUN pip install git+https://github.com/ISEEKYAN/mbridge.git@89eb10887887bc74853f89a4de258c0702932a1c --no-deps RUN pip install flash-linear-attention==0.4.0 +RUN pip install tilelang -f https://tile-ai.github.io/whl/nightly/cu128/ # TE does not have wheel on cuda 13 yet, thus need to install from source RUN if [ "${ENABLE_CUDA_13}" = "1" ]; then \ diff --git a/docker/patch/latest/sglang.patch b/docker/patch/latest/sglang.patch index 7ed339363..b801e1162 100644 --- a/docker/patch/latest/sglang.patch +++ b/docker/patch/latest/sglang.patch @@ -1,3 +1,20 @@ +diff --git a/python/sglang/srt/configs/model_config.py b/python/sglang/srt/configs/model_config.py +index f807deedb..4c0407dec 100644 +--- a/python/sglang/srt/configs/model_config.py ++++ b/python/sglang/srt/configs/model_config.py +@@ -269,6 +269,12 @@ class ModelConfig: + ): + self.hf_config.architectures[0] = "DeepseekV3ForCausalLMNextN" + ++ if ( ++ is_draft_model ++ and self.hf_config.architectures[0] == "DeepseekV32ForCausalLM" ++ ): ++ self.hf_config.architectures[0] = "DeepseekV3ForCausalLMNextN" ++ + if is_draft_model and self.hf_config.architectures[0] == "Glm4MoeForCausalLM": + self.hf_config.architectures[0] = "Glm4MoeForCausalLMNextN" + diff --git a/python/sglang/srt/disaggregation/decode.py b/python/sglang/srt/disaggregation/decode.py index 199885244..742ad0639 100644 --- a/python/sglang/srt/disaggregation/decode.py @@ -145,6 +162,50 @@ index 88705cc35..c8dc052f1 100644 @app.post("/update_weight_version") async def update_weight_version(obj: UpdateWeightVersionReqInput, request: Request): +diff --git a/python/sglang/srt/layers/attention/nsa/nsa_indexer.py b/python/sglang/srt/layers/attention/nsa/nsa_indexer.py +index c9e82e4b1..58270e34a 100644 +--- a/python/sglang/srt/layers/attention/nsa/nsa_indexer.py ++++ b/python/sglang/srt/layers/attention/nsa/nsa_indexer.py +@@ -3,6 +3,7 @@ from __future__ import annotations + from abc import ABC, abstractmethod + from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple + ++import os + import torch + from einops import rearrange + +@@ -188,6 +189,9 @@ class Indexer(MultiPlatformOp): + @torch.compile(dynamic=True) + def _get_logits_head_gate(self, x: torch.Tensor, q_scale: torch.Tensor): + weights, _ = self.weights_proj(x.float()) ++ if weights.shape[1] < 32: ++ assert 32 % weights.shape[1] == 0 ++ weights = weights.repeat_interleave(32 // weights.shape[1], dim=1) + weights = weights * self.n_heads**-0.5 + weights = weights.unsqueeze(-1) * q_scale * self.softmax_scale + return weights +@@ -278,7 +282,10 @@ class Indexer(MultiPlatformOp): + key, [self.rope_head_dim, self.head_dim - self.rope_head_dim], dim=-1 + ) + +- _, k_rope = self.rotary_emb(positions, k_rope, k_rope) ++ if os.environ.get("USE_FIRST_HALF_ROPE", "0") == "1": ++ k_rope, _ = self.rotary_emb(positions, k_rope, k_rope) ++ else: ++ _, k_rope = self.rotary_emb(positions, k_rope, k_rope) + key[..., : self.rope_head_dim] = k_rope + key = rotate_activation(key) + +@@ -837,6 +844,9 @@ class Indexer(MultiPlatformOp): + query, key = self._get_q_k_bf16( + q_lora, x, positions, enable_dual_stream, forward_batch=forward_batch + ) ++ if query.shape[1] < 32: ++ assert 32 % query.shape[1] == 0 ++ query = query.repeat_interleave(32//query.shape[1], dim=1) + + if enable_dual_stream: + current_stream = torch.cuda.current_stream() diff --git a/python/sglang/srt/layers/layernorm.py b/python/sglang/srt/layers/layernorm.py index b07164c53..8e6722ce0 100644 --- a/python/sglang/srt/layers/layernorm.py @@ -702,7 +763,7 @@ index 1f1875254..51d8651ce 100644 parameter = self.model_runner.get_weights_by_name( recv_req.name, recv_req.truncate_size diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py -index 1d69c0582..aa9aefec6 100644 +index 1d69c0582..d984c2e12 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py @@ -558,7 +558,8 @@ class ModelRunner(ModelRunnerKVCacheMixin): @@ -715,7 +776,7 @@ index 1d69c0582..aa9aefec6 100644 if self.device == "cuda": self.init_cublas() -@@ -2224,11 +2225,12 @@ class ModelRunner(ModelRunnerKVCacheMixin): +@@ -2224,11 +2225,19 @@ class ModelRunner(ModelRunnerKVCacheMixin): output.expert_distribution_metrics = recorder_outputs.get("metrics") # Copy cached routing experts' buffers back to CPU cache @@ -725,15 +786,22 @@ index 1d69c0582..aa9aefec6 100644 - cuda_graph_batch=getattr(self.graph_runner, "bs", None), - ) + if not self.is_draft_worker: ++ # In speculative decoding, num_tokens_per_bs > 1, so we need to pass ++ # the actual number of tokens per dp rank in cuda graph, not batch size. ++ cuda_graph_num_tokens = None ++ if getattr(self.graph_runner, "bs", None): ++ cuda_graph_num_tokens = ( ++ self.graph_runner.bs * self.graph_runner.num_tokens_per_bs ++ ) + get_global_experts_capturer().on_forward_end( + forward_batch=forward_batch, + can_run_graph=output.can_run_graph, -+ cuda_graph_batch=getattr(self.graph_runner, "bs", None), ++ cuda_graph_batch=cuda_graph_num_tokens, + ) if self.eplb_manager is not None: self.eplb_manager.on_forward_pass_end() -@@ -2436,6 +2438,42 @@ class ModelRunner(ModelRunnerKVCacheMixin): +@@ -2436,6 +2445,41 @@ class ModelRunner(ModelRunnerKVCacheMixin): logger.error(f"IPC weight update failed: {e}") return False, str(e) @@ -758,7 +826,6 @@ index 1d69c0582..aa9aefec6 100644 + quant_method.restore_weights_before_loading(module) + + if recv_req.post_process_quantization: -+ + # Iterate through all modules to apply specific post-loading processing + for _, module in self.model.named_modules(): + quant_method = getattr(module, "quant_method", None) @@ -777,7 +844,7 @@ index 1d69c0582..aa9aefec6 100644 def _model_load_weights_direct(model, named_tensors: List[Tuple[str, torch.Tensor]]): params_dict = dict(model.named_parameters()) diff --git a/python/sglang/srt/models/deepseek_v2.py b/python/sglang/srt/models/deepseek_v2.py -index 2918461d3..2bcc67087 100644 +index 2918461d3..d44c8aaa0 100644 --- a/python/sglang/srt/models/deepseek_v2.py +++ b/python/sglang/srt/models/deepseek_v2.py @@ -2704,7 +2704,11 @@ class DeepseekV2AttentionMLA(nn.Module): @@ -793,6 +860,34 @@ index 2918461d3..2bcc67087 100644 # fa3 mha support fp8 inputs if ( self.current_attention_backend == "fa3" +@@ -3997,16 +4001,17 @@ class DeepseekV2ForCausalLM(nn.Module): + f"model.layers.{nextn_layer_id}.mlp.{expert_sub_name}.{stem}" + ) + +- for partial_name in tqdm.tqdm( +- partial_names, +- desc="quant weights to fp8 ue8m0", +- ): +- original_weight = weights_dict[f"{partial_name}.weight"] +- out_w, out_s = quant_weight_ue8m0( +- original_weight, weight_block_size=weight_block_size +- ) +- weights_dict[f"{partial_name}.weight"] = out_w +- weights_dict[f"{partial_name}.weight_scale_inv"] = out_s ++ if len(partial_names) > 0: ++ for partial_name in tqdm.tqdm( ++ partial_names, ++ desc="quant weights to fp8 ue8m0", ++ ): ++ original_weight = weights_dict[f"{partial_name}.weight"] ++ out_w, out_s = quant_weight_ue8m0( ++ original_weight, weight_block_size=weight_block_size ++ ) ++ weights_dict[f"{partial_name}.weight"] = out_w ++ weights_dict[f"{partial_name}.weight_scale_inv"] = out_s + + if is_nextn and enable_nextn_moe_bf16_cast_to_fp8(self.quant_config): + self._mark_nextn_moe_weights_as_ue8m0() diff --git a/python/sglang/srt/models/qwen2.py b/python/sglang/srt/models/qwen2.py index a7dbadec6..c83a41338 100644 --- a/python/sglang/srt/models/qwen2.py diff --git a/docker/version.txt b/docker/version.txt index 7ebac80c9..f072c7789 100644 --- a/docker/version.txt +++ b/docker/version.txt @@ -1 +1 @@ -nightly-dev-20260106a \ No newline at end of file +nightly-dev-20260113a diff --git a/docs/en/advanced/pd-disaggregation.md b/docs/en/advanced/pd-disaggregation.md index a4bca69bb..f509d72d8 100644 --- a/docs/en/advanced/pd-disaggregation.md +++ b/docs/en/advanced/pd-disaggregation.md @@ -1,5 +1,5 @@ # PD Disaggregation -Miles supports Prefill and Decode disaggregation (PD Disaggregation). +miles supports Prefill and Decode disaggregation (PD Disaggregation). You can set the number of servers used for Prefill by setting the `--prefill-num-servers` argument. diff --git a/docs/en/examples/glm4-9B.md b/docs/en/examples/glm4-9B.md index 8f9bff6e7..36629568f 100644 --- a/docs/en/examples/glm4-9B.md +++ b/docs/en/examples/glm4-9B.md @@ -8,7 +8,7 @@ After pulling the `radixark/miles:latest` image, initialize the image environmen cd /root/ git clone https://github.com/radixark/miles.git cd miles/ -pip install -e . +pip install -e . --no-deps ``` Download the model and data: diff --git a/docs/en/examples/qwen3-30B-A3B.md b/docs/en/examples/qwen3-30B-A3B.md index 35be2773a..2f731d461 100644 --- a/docs/en/examples/qwen3-30B-A3B.md +++ b/docs/en/examples/qwen3-30B-A3B.md @@ -9,7 +9,7 @@ To convert huggingface checkpoint to torch_dist, please try: ```bash cd miles/ -pip install -e . +pip install -e . --no-deps source scripts/models/qwen3-30B-A3B.sh PYTHONPATH=/root/Megatron-LM/ torchrun --nproc-per-node 8 \ tools/convert_hf_to_torch_dist.py \ diff --git a/docs/en/examples/qwen3-4B.md b/docs/en/examples/qwen3-4B.md index d66df38f3..8374de4be 100644 --- a/docs/en/examples/qwen3-4B.md +++ b/docs/en/examples/qwen3-4B.md @@ -8,7 +8,7 @@ After pulling the `radixark/miles:latest` image, initialize the image environmen cd /root/ git clone https://github.com/radixark/miles.git cd miles/ -pip install -e . +pip install -e . --no-deps ``` Download the model and data: diff --git a/docs/en/get_started/customization.md b/docs/en/get_started/customization.md index 341dac62d..b1088ce64 100644 --- a/docs/en/get_started/customization.md +++ b/docs/en/get_started/customization.md @@ -406,4 +406,14 @@ def custom_hook(args, rollout_id, step_id, model, optimizer, opt_param_scheduler - Custom routing logic - Caching and optimization +--- + +### 19. MoE Routing Replay + +Stabilize MoE RL training by recording and replaying expert routing decisions to ensure consistency. + +| Argument | Description | +| --- | --- | +| `--use-routing-replay` | Forward-backward routing consistency in training. ([arXiv:2507.18071](https://arxiv.org/abs/2507.18071)) | +| `--use-rollout-routing-replay` | R3: Replay routing from rollout during training. **Requires `--use-miles-router`**. ([arXiv:2510.11370](https://arxiv.org/abs/2510.11370)) | diff --git a/docs/en/get_started/quick_start.md b/docs/en/get_started/quick_start.md index 08c41caf2..180075ed3 100644 --- a/docs/en/get_started/quick_start.md +++ b/docs/en/get_started/quick_start.md @@ -45,7 +45,7 @@ miles is already installed in the docker image. To update to the latest verison, # Path can be adjusted according to actual situation cd /root/miles git pull -pip install -e . +pip install -e . --no-deps ``` ## Model and Dataset Download diff --git a/docs/en/get_started/usage.md b/docs/en/get_started/usage.md index 9738131c7..97f6449fe 100644 --- a/docs/en/get_started/usage.md +++ b/docs/en/get_started/usage.md @@ -187,7 +187,7 @@ Additionally, we provide a `metadata_key`, which defaults to `"metadata"`. When - `reinforce_plus_plus` and `reinforce_plus_plus_baseline` ([https://arxiv.org/abs/2501.03262](https://arxiv.org/abs/2501.03262)) - `ppo` ([https://arxiv.org/abs/1707.06347](https://arxiv.org/abs/1707.06347)) - `on_policy_distillation` -- `--calculate-per-token-loss`: By default, Miles calculates loss on a per-sample basis, i.e., `mean(sum(sample_i) / len(sample_i))`. Enable this flag to calculate loss on a per-token basis, i.e., `sum(sum(sample_i)) / sum(len(sample_i))`. +- `--calculate-per-token-loss`: By default, miles calculates loss on a per-sample basis, i.e., `mean(sum(sample_i) / len(sample_i))`. Enable this flag to calculate loss on a per-token basis, i.e., `sum(sum(sample_i)) / sum(len(sample_i))`. - `--use-tis`: Enable this setting to use TIS (Truncated Importance Sampling) (https://fengyao.notion.site/off-policy-rl). - `--true-on-policy-mode`: Enable True On-Policy mode, which strictly ensures that data is generated by the current policy during training. @@ -374,7 +374,7 @@ hf download --repo-type dataset zhuzilin/aime-2024 \ # Clone code and install dependencies git clone https://github.com/radixark/miles.git cd miles -pip install -e . +pip install -e . --no-deps # FSDP does not require weight conversion, natively supports huggingface format diff --git a/docs/en/platform_support/amd_tutorial.md b/docs/en/platform_support/amd_tutorial.md index 53df91b09..c22fdb2ae 100644 --- a/docs/en/platform_support/amd_tutorial.md +++ b/docs/en/platform_support/amd_tutorial.md @@ -54,7 +54,7 @@ Then, download and install miles: ```bash git clone https://github.com/radixark/miles.git cd miles -pip install -e . +pip install -e . --no-deps ``` Download the model and data: @@ -93,7 +93,7 @@ PYTHONPATH=${MEGATRON_LM_PATH} python tools/convert_hf_to_torch_dist.py \ Note: We implemented a dedicated AMD conversion script that forces a CPU-only conversion workflow using the Gloo backend to bypass hardware-specific issues. A GPU-based script for ROCm is currently in development. -⚠️ If you encounter an issue where miles cannot be found, please run `pip install -e .` in the miles directory. +⚠️ If you encounter an issue where miles cannot be found, please run `pip install -e . --no-deps` in the miles directory. ### Example: Qwen3-4B diff --git a/examples/README.md b/examples/README.md index c38642fbd..88f135241 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # Examples -These examples provide concrete examples to leverage Miles in your own RL workflow. Some examples are just demonstrative, but most of them are verifiable with a concrete performance score. +These examples provide concrete examples to leverage miles in your own RL workflow. Some examples are just demonstrative, but most of them are verifiable with a concrete performance score. ## Directory Structure diff --git a/examples/eval/README.md b/examples/eval/README.md index adafe9a5b..4271a8c7d 100644 --- a/examples/eval/README.md +++ b/examples/eval/README.md @@ -4,10 +4,10 @@ This directory contains configuration and utilities for offloading complex evalu ## Overview -The setup allows Miles to delegate evaluation tasks to a dedicated "Skills" server. This creates a clear separation of concerns: +The setup allows miles to delegate evaluation tasks to a dedicated "Skills" server. This creates a clear separation of concerns: -1. **Miles Container**: Runs the main training loop and hosts the model using SGLang. -2. **Skills Container**: Hosts the `nemo_skills` environment, runs the evaluation logic, and queries the model running in the Miles container. +1. **miles Container**: Runs the main training loop and hosts the model using SGLang. +2. **Skills Container**: Hosts the `nemo_skills` environment, runs the evaluation logic, and queries the model running in the miles container. ## Prerequisites @@ -18,15 +18,15 @@ The setup allows Miles to delegate evaluation tasks to a dedicated "Skills" serv ### Prepare Host Network -Create a Docker network to allow communication between the Miles and Skills containers. +Create a Docker network to allow communication between the miles and Skills containers. ```bash docker network create skills-net ``` -### Launch the Miles Container +### Launch the miles Container -Start the main container where Miles and the model will run. Replace `` with your desired name (e.g., `miles_main`). +Start the main container where miles and the model will run. Replace `` with your desired name (e.g., `miles_main`). ```bash docker run \ @@ -76,7 +76,7 @@ git clone -b miles https://github.com/guapisolo/Skills.git /opt/Skills # Install Skills package cd /opt/Skills -pip install -e . +pip install -e . --no-deps ``` **b) Prepare Datasets** @@ -92,7 +92,7 @@ python3 arena-hard/prepare.py **c) Start the Evaluation Server** -Start the server that listens for evaluation requests from Miles. +Start the server that listens for evaluation requests from miles. ```bash cd /opt/miles @@ -105,20 +105,20 @@ python examples/eval/nemo_skills/skills_server.py \ --max-concurrent-requests 512 \ --openai-model-name miles-openai-model ``` -*Note: You can now connect to the server at `skills_server:9050` from within the `skills-net` Docker network. The server always proxies evaluation traffic to an OpenAI-compatible sglang router (Miles starts and manage the router), so adjust `--openai-model-name` and `--max-concurrent-requests` as needed for your deployment. +*Note: You can now connect to the server at `skills_server:9050` from within the `skills-net` Docker network. The server always proxies evaluation traffic to an OpenAI-compatible sglang router (miles starts and manage the router), so adjust `--openai-model-name` and `--max-concurrent-requests` as needed for your deployment. ## Running Evaluation The example scripts are located in `examples/eval/scripts`. Here is an example workflow for training Qwen3-4B with delegated evaluation. -### Prepare Miles Container +### Prepare miles Container -Enter the **Miles container** and install the package. +Enter the **miles container** and install the package. ```bash cd /root/miles git pull -pip install -e . +pip install -e . --no-deps ``` ### Download Model and Data diff --git a/examples/geo3k_vlm/README.md b/examples/geo3k_vlm/README.md index a71e0d760..751faec02 100644 --- a/examples/geo3k_vlm/README.md +++ b/examples/geo3k_vlm/README.md @@ -2,6 +2,11 @@ Training VLMs with FSDP or Megatron on single-turn reasoning task using GRPO on the [GEO3K dataset](https://huggingface.co/datasets/hiyouga/geometry3k). We used processed version [here](https://huggingface.co/datasets/chenhegu/geo3k_imgurl). +Note: Please make sure the cudnn version in the environment is 9.16.0.29 to prevent severe performance regression in conv3d in torch 2.9 mentioned in https://github.com/pytorch/pytorch/issues/168167. Otherwise, you can reinstall cudnn with: +```bash +pip install nvidia-cudnn-cu12==9.16.0.29 +``` +

    FSDP vs Megatron Reward Plot

    diff --git a/examples/geo3k_vlm_multi_turn/README.md b/examples/geo3k_vlm_multi_turn/README.md index 9dca4d6b6..dfbed9f67 100644 --- a/examples/geo3k_vlm_multi_turn/README.md +++ b/examples/geo3k_vlm_multi_turn/README.md @@ -1,9 +1,14 @@ -# VLM Multi-Turn (FSDP backend, geo3k dataset) -Training VLM with FSDP on [geo3k dataset](https://huggingface.co/datasets/hiyouga/geometry3k) with multi-turn reasoning with interactive environment feedback, using GRPO. For dataset, we used the [processed version](https://huggingface.co/datasets/VeraIsHere/geo3k_imgurl_processed). +# VLM Multi-Turn (geo3k dataset) +Training VLM on [geo3k dataset](https://huggingface.co/datasets/hiyouga/geometry3k) with multi-turn reasoning with interactive environment feedback, using GRPO. For the dataset, we used the [processed version](https://huggingface.co/datasets/VeraIsHere/geo3k_imgurl_processed). -The multi-turn rollout is implemented through a custom generate function `examples.geo3k_vlm_multi_turn.rollout.generate`, overriding the original generate function. +Note: Please make sure the cudnn version in the environment is 9.16.0.29 to prevent severe performance regression in conv3d in torch 2.9 mentioned in https://github.com/pytorch/pytorch/issues/168167. Otherwise, you can reinstall cudnn with: +```bash +pip install nvidia-cudnn-cu12==9.16.0.29 +``` + +The multi-turn rollout is implemented through a [custom generate function](rollout.py#L309), overriding the original generate function. -In terms of the environment interaction, this example initializes a custom interactive environment in `examples/geo3k_vlm_multi_turn/env_geo3k.py` with the APIs below. +In terms of the environment interaction, this example initializes a [custom interactive environment](env_geo3k.py) with the APIs below.
    Environment API (geo3k) @@ -16,7 +21,8 @@ In terms of the environment interaction, this example initializes a custom inter The reward model is the default math RM. -![VLM multi-turn geo3k reward](vlm_multi_turn_geo3k_reward.png) +![VLM multi-turn geo3k reward](geo3k_vlm_multi_turn_reward.png) +![Rollout megatron](rollout_experiment_result_megatron.png) ## Reproduce ```bash diff --git a/examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_reward.png b/examples/geo3k_vlm_multi_turn/geo3k_vlm_multi_turn_reward.png new file mode 100644 index 0000000000000000000000000000000000000000..88851bb461bda64034846c98c3f37e29fd76ebe8 GIT binary patch literal 228749 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%0AFFS4k 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 fsRdBw`i+#)4YKPNm$cL1FH-hf9k Sample: sample.status = Sample.Status.COMPLETED break - obs_log_probs = [float("-inf")] * len(obs_prompt_ids) + obs_log_probs = [0.0] * len(obs_prompt_ids) _append_to_sample(sample, response_tokens, obs_prompt_ids, obs_log_probs, loss_mask_val=0) budget = _update_budget(budget, len(obs_prompt_ids)) diff --git a/examples/geo3k_vlm_multi_turn/rollout_experiment_result_megatron.png b/examples/geo3k_vlm_multi_turn/rollout_experiment_result_megatron.png new file mode 100644 index 0000000000000000000000000000000000000000..dd249de2952016e460aa946cd28b7bd3f757d848 GIT binary patch literal 208317 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%0AwF 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

    PblTzPed&Nt#mx7Li#^S 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ïRH{bneU9eJZ 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≺L9w)Y|`w%Cxeb7 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 J5GQ}T`F}qX{(b-e literal 0 HcmV?d00001 diff --git a/examples/geo3k_vlm_multi_turn/vlm_multi_turn_geo3k_reward.png b/examples/geo3k_vlm_multi_turn/vlm_multi_turn_geo3k_reward.png deleted file mode 100644 index 0f675e3bca3e244352c0d720459af10de7ee3628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53073 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 Ck8*zi diff --git a/examples/low_precision/README.md b/examples/low_precision/README.md index 9fd4d115f..5bb90442a 100644 --- a/examples/low_precision/README.md +++ b/examples/low_precision/README.md @@ -63,4 +63,78 @@ Currently, FP8 is far from being a complete feature and still has the following - FP8 weights (`--fp8-param-gather`) can provide memory savings benefits, but currently FP8 weights must be used with TransformerEngine's FusedAdam, which conflicts with the commonly used Adam CPU offload technique in Megatron-LM. -The miles team will continue to collaborate with the NVIDIA team to contribute more complete FP8 training infrastructure to the community. \ No newline at end of file +The miles team will continue to collaborate with the NVIDIA team to contribute more complete FP8 training infrastructure to the community. + + +Here is a polished and professional version of your documentation. + +I have corrected grammatical errors, improved the flow, standardizes the terminology (e.g., capitalizing "STE"), and clarified the instructions. + +*** + +## INT4 Training Examples + +This guide provides examples for INT4 STE (Straight-Through Estimator) training and INT4 inference. Utilizing INT4 inference significantly improves throughput, thereby accelerating the training pipeline (specifically during the rollout generation phase). + +### Files + +* `run-moonlight-16B-A3B-int4.sh`: Launch script for **Moonlight-16B-A3B** (INT4) on 4x H200 GPUs. +* `run-qwen3‑30B‑A3B-int4.sh`: Launch script for **Qwen3‑30B‑A3B** (INT4) on 8x H200 GPUs. +* `run-qwen3-235B-A22B-int4.sh`: Launch script for **Qwen3-235B-A22B** (INT4) on 64x H200 GPUs. +* `run-kimi-k2-Thinking-int4.sh`: Launch script for **Kimi-k2-Thinking** (INT4) on 256x H200 GPUs. + +### Quick Start + +#### 1. Convert HuggingFace Weights to INT4 +First, download the PTQ (Post-Training Quantization) calibration dataset from HuggingFace: +[https://huggingface.co/datasets/Salesforce/wikitext/tree/main/wikitext-2-raw-v1](https://huggingface.co/datasets/Salesforce/wikitext/tree/main/wikitext-2-raw-v1) + +Next, use the `tools/convert_hf_to_hf_int4.py` script to convert BF16 weights to INT4 format. Ensure that the `--hf-checkpoint` parameter points to a directory where `config.json` contains the correct `quantization_config`. miles will automatically utilize INT4 quantization during weight updates. + +```bash +python tools/convert_hf_to_hf_int4.py \ + --input-dir /path/to/your/original/models \ + --output-dir /path/to/your/save/models \ + --data-dir /path/to/your/wikitext +``` + +#### 2. Start INT4 Training + +You need to configure the specific environment variables for quantization settings. + +**Environment Variables:** + +* **`OPEN_TRAINING_INT4_FAKE_QAT_FLAG`**: Enables fake quantization operations for INT4 training. +* **`OPEN_TRAINING_INT4_GROUP_SIZE`**: Specifies the block size (group size) for model quantization. + * Set to **128** for `moonlight-16B-A3B` 、 `qwen3-30B-A3B`and `qwen3-235B-A22B-int4`. + * Set to **32** for `kimi-k2-Thinking-int4`. + +**Configuration Example:** + +```json +RUNTIME_ENV_JSON="{ + \"env_vars\": { + ... + \"OPEN_TRAINING_INT4_FAKE_QAT_FLAG\": \"1\", + \"OPEN_TRAINING_INT4_GROUP_SIZE\": \"128\" + } +}" +``` + +**Launch Commands:** + +```bash +# Moonlight-16B-A3B Int4 training +bash examples/low_precision/run-moonlight-16B-A3B-int4.sh + +# Qwen3‑30B‑A3B Int4 training +bash examples/low_precision/run-qwen3‑30B‑A3B-int4.sh + +# Qwen3-235B-A22B Int4 training (8 nodes) +bash examples/low_precision/run-qwen3-235B-A22B-int4.sh + +# Kimi-k2-Thinking Int4 training (32 nodes) +bash examples/low_precision/run-kimi-k2-Thinking-int4.sh +``` + +- For multi-node environments, please start the Ray service according to your cluster configuration. \ No newline at end of file diff --git a/examples/low_precision/run-kimi-k2-Thinking-int4.sh b/examples/low_precision/run-kimi-k2-Thinking-int4.sh new file mode 100644 index 000000000..3bedbf88a --- /dev/null +++ b/examples/low_precision/run-kimi-k2-Thinking-int4.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 + +NVLINK_COUNT=$(nvidia-smi | grep -o "NVLink" | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/../../models/kimi-k2-thinking.sh" + +CKPT_ARGS=( + --hf-checkpoint /root/Kimi-K2-Thinking/ + --ref-load /root/Kimi-K2_thinking_torch_dist/ + --load /root/Kimi-K2-thinking_miles/ + --save /root/Kimi-K2-thinking_miles/ + --save-interval 20 +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + + --rm-type math + + --num-rollout 100 + --rollout-batch-size 128 + --n-samples-per-prompt 8 + --rollout-max-response-len 16384 + --rollout-temperature 0.8 + + # --global-batch-size 256 + + --over-sampling-batch-size 256 + --dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std + + --num-steps-per-rollout 4 + --balance-data +) + +EVAL_ARGS=( + --eval-interval 10 + --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 16 + --eval-max-response-len 16384 + --eval-top-p 0.7 +) + +PERF_ARGS=( + --tensor-model-parallel-size 8 + --sequence-parallel + --pipeline-model-parallel-size 8 + --context-parallel-size 4 + --expert-model-parallel-size 32 + --expert-tensor-parallel-size 1 + --decoder-last-pipeline-num-layers 5 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + --use-dynamic-batch-size + --max-tokens-per-gpu 16384 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + # --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 + --use-tis +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 + + --optimizer-cpu-offload + --overlap-cpu-optimizer-d2h-h2d + --use-precision-aware-optimizer +) + +WANDB_ARGS=( + # --use-wandb + # --wandb-project miles-dev + # --wandb-group kimi-k2-thinking-test + # --wandb-key ${WANDB_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-mem-fraction-static 0.7 + + # dp attention + # --sglang-enable-dp-attention + # --sglang-dp-size 8 + # --sglang-moe-dense-tp-size 1 + # --sglang-enable-dp-lm-head + # --sglang-disable-radix-cache + + --sglang-ep-size 8 + + # enable deepep for sglang + #--sglang-enable-deepep-moe + #--sglang-deepep-mode auto + + # make every dp rank has 128 concurrency + --sglang-server-concurrency 1024 + --use-miles-router +) + + +MISC_ARGS=( + # default dropout in megatron is 0.1 + --attention-dropout 0.0 + --hidden-dropout 0.0 + # should be good for model performance + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + # need to comment this when using model with MLA + --attention-backend flash + + # use deepep for megatron + # --moe-enable-deepep + # --moe-token-dispatcher-type flex + --no-check-for-nan-in-loss-and-grad +) + +# Build the runtime environment JSON with proper variable substitution +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + \"NCCL_TIMEOUT_MS\":\"360000000\", + \"no_proxy\": \"${no_proxy}\", + \"MASTER_ADDR\": \"${MASTER_ADDR}\", + \"OPEN_TRAINING_INT4_FAKE_QAT_FLAG\": \"1\", + \"OPEN_TRAINING_INT4_GROUP_SIZE\": \"32\" + } +}" + + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 /personal/miles/miles/train.py \ + --actor-num-nodes 32 \ + --actor-num-gpus-per-node 8 \ + --colocate \ + --update-weight-buffer-size $(( 4 * 512 * 1024 * 1024)) \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} \ No newline at end of file diff --git a/examples/low_precision/run-moonlight-16B-A3B-int4.sh b/examples/low_precision/run-moonlight-16B-A3B-int4.sh new file mode 100644 index 000000000..12ea3ee81 --- /dev/null +++ b/examples/low_precision/run-moonlight-16B-A3B-int4.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python +pkill -9 redis + +set -ex + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 + +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/../../models/moonlight.sh" + +CKPT_ARGS=( + --hf-checkpoint /root/Moonlight-16B-A3B-Instruct-INT4 + --ref-load /root/Moonlight-16B-A3B-Instruct-INT4_torch_dist + --load /root/Moonlight-16B-A3B_miles/ + --save /root/Moonlight-16B-A3B_miles/ + --save-interval 20 +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type math + --num-rollout 3000 + --rollout-batch-size 128 + --n-samples-per-prompt 8 + --rollout-max-response-len 4096 + --rollout-temperature 0.8 + + --over-sampling-batch-size 256 + --dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std + + --num-steps-per-rollout 4 + # --global-batch-size 256 + --balance-data +) + +EVAL_ARGS=( + --eval-interval 20 + --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 8 + --eval-max-response-len 4096 + --eval-top-p 0.7 +) + +PERF_ARGS=( + --tensor-model-parallel-size 2 + --sequence-parallel + --pipeline-model-parallel-size 1 + --context-parallel-size 1 + --expert-model-parallel-size 4 + --expert-tensor-parallel-size 1 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + # --micro-batch-size 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 8192 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 + + --optimizer-cpu-offload + --overlap-cpu-optimizer-d2h-h2d + --use-precision-aware-optimizer +) + +WANDB_ARGS=( + # --use-wandb + # --wandb-project miles-dev + # --wandb-group moomlight-16B-A3B-test + # --wandb-key ${WANDB_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 4 + --sglang-mem-fraction-static 0.7 + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) +) + +MISC_ARGS=( + # default dropout in megatron is 0.1 + --attention-dropout 0.0 + --hidden-dropout 0.0 + # should be good for model performance + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + # need to comment this when using model with MLA + # --attention-backend flash + + # use deepep for megatron + --moe-enable-deepep + --moe-token-dispatcher-type flex +) + +# launch the master node of ray in container +export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 4 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 + +# Build the runtime environment JSON with proper variable substitution +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + \"OPEN_TRAINING_INT4_FAKE_QAT_FLAG\": \"1\", + \"OPEN_TRAINING_INT4_GROUP_SIZE\": \"128\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 4 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} diff --git a/examples/low_precision/run-qwen3-235B-A22B-int4.sh b/examples/low_precision/run-qwen3-235B-A22B-int4.sh new file mode 100644 index 000000000..e0e3e6c6b --- /dev/null +++ b/examples/low_precision/run-qwen3-235B-A22B-int4.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# will prevent ray from buffering stdout/stderr +export PYTHONBUFFERED=16 + +NVLINK_COUNT=$(nvidia-smi | grep -o "NVLink" | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/../../models/qwen3-235B-A22B.sh" + +CKPT_ARGS=( + --hf-checkpoint /root/Qwen3-235B-A22B-INT4/ + --ref-load /root/Qwen3-235B-A22B_torch_dist/ + --load /root/Qwen3-235B-A22B-miles/ + --save /root/Qwen3-235B-A22B-miles/ + --save-interval 20 +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + + --rm-type deepscaler + + --num-rollout 300 + --rollout-batch-size 32 + --n-samples-per-prompt 8 + --rollout-max-response-len 8192 + --rollout-temperature 0.8 + + --global-batch-size 256 + --balance-data +) + +EVAL_ARGS=( + --eval-interval 10 + --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 16 + --eval-max-response-len 16384 + --eval-top-p 0.7 +) + +PERF_ARGS=( + --tensor-model-parallel-size 4 + --sequence-parallel + --pipeline-model-parallel-size 4 + --context-parallel-size 2 + --expert-model-parallel-size 16 + --expert-tensor-parallel-size 1 + --decoder-last-pipeline-num-layers 22 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + # --micro-batch-size 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 16384 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + # --kl-coef 0.00 + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 + --use-tis +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 + + --optimizer-cpu-offload + --overlap-cpu-optimizer-d2h-h2d + --use-precision-aware-optimizer +) + +WANDB_ARGS=( + # --use-wandb + # --wandb-project miles-dev + # --wandb-group qwen3-235B-A22B-test + # --wandb-key ${WANDB_KEY} +) + + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-mem-fraction-static 0.7 + # --sglang-enable-dp-attention + # --sglang-dp-size 4 + --sglang-ep-size 8 + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) + --use-miles-router +) + + +MISC_ARGS=( + # default dropout in megatron is 0.1 + --attention-dropout 0.0 + --hidden-dropout 0.0 + # should be good for model performance + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + # need to comment this when using model with MLA + --attention-backend flash + --no-check-for-nan-in-loss-and-grad + + # use deepep for megatron + # --moe-enable-deepep + # --moe-token-dispatcher-type flex +) + +# Build the runtime environment JSON with proper variable substitution +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + \"NCCL_TIMEOUT_MS\":\"360000000\", + \"no_proxy\": \"${no_proxy}\", + \"MASTER_ADDR\": \"${MASTER_ADDR}\", + \"OPEN_TRAINING_INT4_FAKE_QAT_FLAG\": \"1\", + \"OPEN_TRAINING_INT4_GROUP_SIZE\": \"128\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 8 \ + --actor-num-gpus-per-node 8 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} \ No newline at end of file diff --git a/examples/low_precision/run-qwen3-30B-A3B-int4.sh b/examples/low_precision/run-qwen3-30B-A3B-int4.sh new file mode 100644 index 000000000..eb0c870b3 --- /dev/null +++ b/examples/low_precision/run-qwen3-30B-A3B-int4.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# for rerun the task +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +# will prevent ray from buffering stdout/stderrs +export PYTHONBUFFERED=16 + +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/../../models/qwen3-30B-A3B.sh" + +CKPT_ARGS=( + --hf-checkpoint /root/Qwen3-30B-A3B-INT4/ + --ref-load /root/Qwen3-30B-A3B_torch_dist/ + --load /root/Qwen3-30B-A3B_miles/ + --save /root/Qwen3-30B-A3B_miles/ + --save-interval 20 +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + + --rm-type deepscaler + + --num-rollout 100 + --rollout-batch-size 32 + --n-samples-per-prompt 8 + --rollout-max-response-len 8192 + --rollout-temperature 0.8 + + --global-batch-size 256 + --balance-data + # --debug-rollout-only +) + +EVAL_ARGS=( + --eval-interval 10 + --eval-prompt-data /root/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 8 + --eval-max-response-len 16384 + --eval-top-p 0.7 +) + +PERF_ARGS=( + --tensor-model-parallel-size 4 + --sequence-parallel + --pipeline-model-parallel-size 1 + --context-parallel-size 1 + --expert-model-parallel-size 8 + --expert-tensor-parallel-size 1 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + # --micro-batch-size 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 8192 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 + --use-tis +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 + + --optimizer-cpu-offload + --overlap-cpu-optimizer-d2h-h2d + --use-precision-aware-optimizer +) + +WANDB_ARGS=( + # --use-wandb + # --wandb-project miles-dev + # --wandb-group qwen3-30B-A3B-test + # --wandb-key ${WANDB_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) + --use-miles-router +) + +MISC_ARGS=( + # default dropout in megatron is 0.1 + --attention-dropout 0.0 + --hidden-dropout 0.0 + # should be good for model performance + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + # need to comment this when using model with MLA + --attention-backend flash + # use deepep for megatron + # --moe-enable-deepep + # --moe-token-dispatcher-type flex +) + +# launch the master node of ray in container +export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 + +# Build the runtime environment JSON with proper variable substitution +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\", + \"NCCL_NVLS_ENABLE\": \"${HAS_NVLINK}\", + \"OPEN_TRAINING_INT4_FAKE_QAT_FLAG\": \"1\", + \"OPEN_TRAINING_INT4_GROUP_SIZE\": \"128\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 8 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} + \ No newline at end of file diff --git a/examples/low_precision/run-qwen3-4b-fp8.sh b/examples/low_precision/run-qwen3-4b-fp8.sh index b196ba606..89b7079ad 100644 --- a/examples/low_precision/run-qwen3-4b-fp8.sh +++ b/examples/low_precision/run-qwen3-4b-fp8.sh @@ -120,14 +120,6 @@ MISC_ARGS=( --attention-backend flash ) -PRECISE_ARGS=( - --transformer-impl transformer_engine - --bf16 - --fp8-format e4m3 - --fp8-recipe blockwise - --fp8-param-gather -) - # launch the master node of ray in container export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} @@ -159,5 +151,4 @@ ray job submit --address="http://127.0.0.1:8265" \ ${PERF_ARGS[@]} \ ${EVAL_ARGS[@]} \ ${SGLANG_ARGS[@]} \ - ${MISC_ARGS[@]} \ - ${PRECISE_ARGS[@]} \ No newline at end of file + ${MISC_ARGS[@]} \ No newline at end of file diff --git a/examples/on_policy_distillation/README.md b/examples/on_policy_distillation/README.md index ff7a8207b..a6b22c5b1 100644 --- a/examples/on_policy_distillation/README.md +++ b/examples/on_policy_distillation/README.md @@ -1,6 +1,6 @@ # On-Policy Distillation Example -This example shows how to run **on-policy distillation** using Miles. A small student (Qwen3-8B) is aligned to imitate a larger teacher (Qwen3-32B) by training only on the student's own rollouts and matching the teacher's token-level log-probabilities. +This example shows how to run **on-policy distillation** using miles. A small student (Qwen3-8B) is aligned to imitate a larger teacher (Qwen3-32B) by training only on the student's own rollouts and matching the teacher's token-level log-probabilities. In this example, the teacher model acts as a reward model (RM) by providing teacher log probabilities as the supervision signal. @@ -50,7 +50,7 @@ Using Qwen3-8B-Base model sfted on part of the [OpenThoughts3-1.2M](https://hugg # FAQ 1. **Why are teacher logits computed via a sglang server instead of inside the training backend?** -The teacher runs on an independent SGLang server that Miles treats as a reward model. Hosting it inside Megatron/FSDP would require maintaining a second, fully configured training stack for the teacher. +The teacher runs on an independent SGLang server that miles treats as a reward model. Hosting it inside Megatron/FSDP would require maintaining a second, fully configured training stack for the teacher. # References diff --git a/examples/retool/README.md b/examples/retool/README.md index b4e3f71eb..bd9af717b 100644 --- a/examples/retool/README.md +++ b/examples/retool/README.md @@ -21,7 +21,7 @@ The retool example provides: 1. Setup and download datasets: ```bash cd miles -pip install -e . +pip install -e . --no-deps # For SFT part, you can use later model to RL directly and skip SFT. hf download --repo-type dataset JoeYing/ReTool-SFT --local-dir /root/JoeYing/ReTool-SFT hf download Qwen/Qwen3-4B-Instruct-2507 --local-dir /root/Qwen/Qwen3-4B-Instruct-2507 diff --git a/examples/search-r1/README.md b/examples/search-r1/README.md index b9c426f8b..867ca504b 100644 --- a/examples/search-r1/README.md +++ b/examples/search-r1/README.md @@ -9,7 +9,7 @@ Use the `radixark/miles:latest` image and initialize the environment required fo ```bash cd /root/ git clone https://github.com/radixark/miles.git -pip install -e . +pip install -e . --no-deps # for Search R1 pip install chardet ``` diff --git a/examples/strands-agents/README.md b/examples/strands-agents/README.md deleted file mode 100644 index ad597645e..000000000 --- a/examples/strands-agents/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Miles x Strands-Agents - -This is a running example that connects the [Strands-Agents](https://github.com/strands-agents/sdk-python) agent scaffolding framework with Miles for RL training. - -## Install Dependencies - -1. Pull the `radixark/miles:latest` image and enter it -2. Goes to miles folder: `cd /root/miles` (Clone the repository if not already there: `cd /root && git clone https://github.com/radixark/miles.git`) -3. Install Miles: `pip install -e .` -4. Goes to the example folder: `cd /root/miles/examples/strands-agents` -5. Install other dependencies: `pip install -r requirements.txt` - -> NOTE: we use camel-ai's subprocess code interpreter for python code execution, which is NOT a good practice; it's just for convenience of this example and the dependencies for solving math problems are usually ready in `miles`'s docker - -## Prepare Model - -```bash -# hf checkpoint -hf download Qwen/Qwen3-4B-Instruct-2507 --local-dir /root/models/Qwen/Qwen3-4B-Instruct-2507 - -# mcore checkpoint -cd /root/miles -source scripts/models/qwen3-4B.sh -PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ - ${MODEL_ARGS[@]} \ - --hf-checkpoint /root/models/Qwen/Qwen3-4B-Instruct-2507 \ - --save /root/models/Qwen/Qwen3-4B-Instruct-2507_torch_dist -``` - -## Prepare Dataset - -Following [Retool](https://arxiv.org/abs/2504.11536), we used `dapo-math-17k` as training data: - -``` -from datasets import load_dataset -ds = load_dataset("zhuzilin/dapo-math-17k", split="train") -ds.to_json("/root/data/dapo-math-17k.jsonl", orient="records", lines=True) -``` - -and `aime-2024` as eval data: - -``` -from datasets import load_dataset -ds = load_dataset("zhuzilin/aime-2024", split="train") -ds.to_json("/root/data/aime-2024.jsonl", orient="records", lines=True) -``` - -## Run Training - -Assuming `/root/miles` is up-to-date (if this PR is not merged you may need to switch branch): - -``` -cd /root/miles -export WANDB_KEY=$your_wandb_key -bash examples/strands-agents/strands_qwen3_4b.sh -``` diff --git a/examples/strands-agents/generate_with_strands.py b/examples/strands-agents/generate_with_strands.py deleted file mode 100644 index 4f020614a..000000000 --- a/examples/strands-agents/generate_with_strands.py +++ /dev/null @@ -1,267 +0,0 @@ -import logging - -import openai -import wandb -from camel.interpreters import SubprocessInterpreter -from strands import Agent, tool -from strands.models.openai import OpenAIModel -from strands.types.exceptions import ContextWindowOverflowException, EventLoopException, MaxTokensReachedException - -from miles.rollout.rm_hub.math_dapo_utils import compute_score as math_dapo_compute_score -from miles.rollout.sglang_rollout import GenerateState -from miles.utils.types import Sample - -logging.basicConfig(level=logging.INFO) - -logger = logging.getLogger(__name__) - - -SYSTEM_PROMPT = """ -You are a helpful math-solving assistant with access to the `execute_python_code` tool. - -Guidelines: -- For any numerical or symbolic computation, always use the `execute_python_code` tool rather than performing calculations mentally. -- Break problems into clear steps, calling the Python tool whenever computation is required. -- After completing your reasoning, present the final result enclosed in \\boxed{}. -""".strip() - -MAX_NUM_MESSAGES = 16 # messages beyond this will be truncated - - -def create_strands_agent(args, sampling_params): - """Create a strands agent that connects to the SGLang rollout server""" - - # Create an OpenAI model from the SGLang server - model_params = { - "max_tokens": sampling_params["max_new_tokens"], - "temperature": sampling_params["temperature"], - "top_p": sampling_params["top_p"], - } - sglang_server_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/v1" - logger.info( - f"[Strands Agents] Creating OpenAIModel from SGLang server at {sglang_server_url}" - f" with parameters: {model_params}" - ) - model = OpenAIModel( - client_args={ - "api_key": "EMPTY", - "base_url": sglang_server_url, - "timeout": 300.0, # needed for tool calls - }, - model_id=args.hf_checkpoint.split("/")[-1], - params=model_params, - ) - - # Define the `execute_python_code` tool using camel-ai's subprocess interpreter - @tool - def execute_python_code(code: str) -> str: - r"""Execute a given Python code snippet. - - Args: - code (str): The input Python code to the Code Execution tool call. - - Returns: - str: The text output from the Code Execution tool call. - """ - interpreter = SubprocessInterpreter( - require_confirm=False, - print_stdout=False, - print_stderr=False, - execution_timeout=60.0, - ) - result = interpreter.run(code=code, code_type="python") - logger.info( - f"[Strands Agents] executing Python code: ```python\n{code}\n``` and get execution result: ```python\n{result}\n```" - ) - return result - - # Create the strands agent - agent = Agent( - model=model, - tools=[execute_python_code], - system_prompt=SYSTEM_PROMPT, - callback_handler=None, - ) - - return agent - - -async def run_strands_agent(agent: Agent, prompt: str) -> Sample.Status: - """Run the strands agent with the given prompt and set the sample status.""" - try: - logger.info(f"[Strands Agents] running agent with prompt: {prompt}") - await agent.invoke_async(prompt=prompt) - sample_status = Sample.Status.COMPLETED - except Exception as e: - truncated_conditions = [ - isinstance(e, MaxTokensReachedException), - isinstance(e, ContextWindowOverflowException), - isinstance(e, EventLoopException) - and isinstance(e.original_exception, openai.APIError) - and "context length" in str(e.original_exception).lower(), - ] - if any(truncated_conditions): - sample_status = Sample.Status.TRUNCATED - logger.warning(f"[Strands Agents] sample is TRUNCATED due to {type(e).__name__}: {e}") - else: - sample_status = Sample.Status.ABORTED - logger.error(f"[Strands Agents] sample is ABORTED due to {type(e).__name__}: {e}") - - return sample_status - - -def get_trajectory(agent: Agent) -> list[dict]: - """Get the chat template-compatible trajectory from strands agent's messages.""" - openai_model: OpenAIModel = agent.model - trajectory = openai_model.format_request_messages(messages=agent.messages, system_prompt=agent.system_prompt) - for message in trajectory: - if "content" in message and isinstance(message["content"], list): - if len(message["content"]) > 0 and "text" in message["content"][0]: - message["content"] = message["content"][0]["text"] - else: - message["content"] = "" - return trajectory - - -async def generate(args, sample: Sample, sampling_params) -> Sample: - """Generate function using strands-agents as agent scaffolding""" - assert not args.partial_rollout, "Partial rollout is not supported for this function at the moment." - - state = GenerateState(args) - - # Create strands agent - agent = create_strands_agent(args, sampling_params) - - # Run the strands agent - prompt_text = sample.prompt if isinstance(sample.prompt, str) else sample.prompt[0]["content"] - sample.status = await run_strands_agent(agent, prompt_text) - - # Early return if sample is aborted - if sample.status == Sample.Status.ABORTED: - agent.cleanup() - return sample - - # Get the trajectory from the agent and further truncate if necessary - trajectory = get_trajectory(agent) - if len(trajectory) > MAX_NUM_MESSAGES: - logger.warning( - f"[Strands Agents] sample is TRUNCATED due to number of messages (={len(trajectory)}) exceeding limit (={MAX_NUM_MESSAGES})" - ) - # This post-processing is not optimal but just for simplicity - # We should implement a hook in strands-agents to handle this truncation - trajectory = trajectory[:MAX_NUM_MESSAGES] - sample.status = Sample.Status.TRUNCATED - - # Get the initial prompt (system + user message) - initial_prompt_messages = [msg for msg in trajectory if msg["role"] in ["system", "user"]] - assert len(initial_prompt_messages) == 2, "Initial prompt messages must be exactly 2 for single-turn conversations" - prompt_text = state.tokenizer.apply_chat_template( - initial_prompt_messages, - tokenize=False, - add_generation_prompt=True, # Add generation prompt for the assistant - ) - prompt_tokens_ids = state.tokenizer(prompt_text, add_special_tokens=False)["input_ids"] - - # Build (re-tokenize) the response incrementally - response_token_ids = [] - loss_masks = [] - response_text = "" - - # Start with the initial prompt messages for progressive chat template application - current_messages = list(initial_prompt_messages) - prev_token_count = len(prompt_tokens_ids) - - # Iterate through remaining messages (assistant and tool messages) - for message in trajectory[len(initial_prompt_messages) :]: - # Add this message to the conversation - current_messages.append(message) - - # Apply chat template and tokenize up to this point - current_text = state.tokenizer.apply_chat_template( - current_messages, tokenize=False, add_generation_prompt=False - ) - current_token_ids = state.tokenizer(current_text, add_special_tokens=False)["input_ids"] - - # Calculate how many new tokens this message added - new_token_count = len(current_token_ids) - message_token_length = new_token_count - prev_token_count - - # Extract the new tokens for this message - message_tokens = current_token_ids[prev_token_count:] - assert len(message_tokens) == message_token_length, "Message tokens length mismatch" - response_token_ids.extend(message_tokens) - - # Align message tokens with loss masks - if message["role"] == "assistant": - # We train on assistant messages - loss_masks.extend([1] * message_token_length) - else: - # We don't train on tool messages - loss_masks.extend([0] * message_token_length) - - prev_token_count = new_token_count - - # Extract the response text (everything after the initial prompt) - full_conversation_text = state.tokenizer.apply_chat_template( - trajectory, tokenize=False, add_generation_prompt=False - ) - response_text = full_conversation_text[len(prompt_text) :] - - # Set sample attributes and some debug information - sample.tokens = prompt_tokens_ids + response_token_ids - sample.response_length = len(response_token_ids) - sample.response = response_text - sample.loss_mask = loss_masks - # Store tool call count for reward calculation - sample.tool_call_count = [message["role"] == "tool" for message in trajectory].count(True) - - # Log to wandb if available - if wandb.run is not None: - wandb.log( - { - "debug/response_length": sample.response_length, - "debug/available_tools": len(agent.tool_names), - "debug/tool_calls": sample.tool_call_count, - "debug/num_messages": len(trajectory), - "debug/truncated": sample.status == Sample.Status.TRUNCATED, - } - ) - - agent.cleanup() - return sample - - -async def reward_func(args, sample, **kwargs): - """Tool call reward function using math_dapo as primary reward model""" - if not isinstance(sample, Sample): - raise TypeError("Sample must be an instance of Sample class.") - - # Extract information from sample - solution_str = sample.response - ground_truth = sample.label if sample.label is not None else "" - tool_call_count = getattr(sample, "tool_call_count", 0) - - # Accept both Answer: ... and \\boxed{...} answer - result = math_dapo_compute_score(solution_str, ground_truth, strict_box_verify=False) - result_boxed = math_dapo_compute_score(solution_str, ground_truth, strict_box_verify=True) - if result["pred"] == "[INVALID]": - result = result_boxed - - # Encourage model to call tools - if result["score"] < 0: - tool_call_reward = (tool_call_count - 2) / 2 * 0.1 - result["score"] = min(-0.6, result["score"] + tool_call_reward) - - if result["pred"] is None: - result["pred"] = "" - - logger.info( - f"[Strands Agents] sample summary: " - f"status={sample.status} | " - f"tool_call_count={sample.tool_call_count} | " - f"response_length={sample.response_length} | " - f"reward={result} | " - f"ground_truth={ground_truth}" - ) - - return result diff --git a/examples/strands_sglang/README.md b/examples/strands_sglang/README.md new file mode 100644 index 000000000..ad310238a --- /dev/null +++ b/examples/strands_sglang/README.md @@ -0,0 +1,70 @@ +# miles x Strands-SGLang + +This example connects `miles` with [`strands-sglang`](https://github.com/horizon-rl/strands-sglang) (SGLang extension for the agentic scaffolding [`strands`](https://github.com/strands-agents/sdk-python)) for agentic RL training. + +## Why `strands-sglang`? + +| Component | Agent Loop | TITO Support | +| ------------------------------------------------------------------ | ----------------------------------- | -------------------------------------- | +| [Strands-Agents](https://github.com/strands-agents/sdk-python) | ✅ Handles agent loop, custom hooks | ❌ text-based, requires retokenization | +| [SGLang](https://github.com/sgl-project/sglang) | ❌ Single generation only | ✅ Native `input_ids` in/out | +| **[strands-sglang](https://github.com/horizon-rl/strands-sglang)** | ✅ Via Strands | ✅ Via SGLang's native API | + +`strands-sglang` bridges the gap by extending `strands` with SGLang's native `/generate` endpoint: + +- Captures exact token IDs during generation (no retokenization drift) +- Automatically tracks `loss_mask` via `token_manager` +- Provides `ToolIterationLimiter` for clean trajectory truncation + +## Install Dependencies + +1. Pull the `radixark/miles:latest` image and enter it +2. Go to miles folder: `cd /root/miles` +3. Install miles: `pip install -e . --no-deps` +4. Go to the example folder: `cd /root/miles/examples/strands_sglang` +5. Install other dependencies: `pip install -r requirements.txt` + +> NOTE: `strands-sglang` is under rapid development, so we recommend using the GitHub repo version: `strands-sglang @ git+https://github.com/horizon-rl/strands-sglang.git` + +> NOTE: We use camel-ai's subprocess code interpreter for python code execution, which is NOT a good practice; it's just for convenience of this example. + +## Prepare Model + +```bash +# hf checkpoint +huggingface-cli download Qwen/Qwen3-8B --local-dir /root/models/Qwen/Qwen3-8B + +# mcore checkpoint +cd /root/miles +source scripts/models/qwen3-8B.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/models/Qwen/Qwen3-8B \ + --save /root/models/Qwen/Qwen3-8B_torch_dist +``` + +## Prepare Dataset + +Following [Retool](https://arxiv.org/abs/2504.11536), we use `dapo-math-17k` as training data: + +```python +from datasets import load_dataset +ds = load_dataset("zhuzilin/dapo-math-17k", split="train") +ds.to_json("/root/data/dapo-math-17k.jsonl", orient="records", lines=True) +``` + +and `aime-2024` as eval data: + +```python +from datasets import load_dataset +ds = load_dataset("zhuzilin/aime-2024", split="train") +ds.to_json("/root/data/aime-2024.jsonl", orient="records", lines=True) +``` + +## Run Training + +```bash +cd /root/miles +export WANDB_KEY=$your_wandb_key +bash examples/strands_sglang/strands_qwen3_8b.sh +``` diff --git a/examples/strands_sglang/generate_with_strands.py b/examples/strands_sglang/generate_with_strands.py new file mode 100644 index 000000000..a7cf91819 --- /dev/null +++ b/examples/strands_sglang/generate_with_strands.py @@ -0,0 +1,117 @@ +import logging + +from camel.interpreters import SubprocessInterpreter +from strands import Agent, tool +from strands_sglang import SGLangClient, SGLangModel +from strands_sglang.tool_limiter import ToolIterationLimiter + +from miles.rollout.rm_hub.math_dapo_utils import compute_score as math_dapo_compute_score +from miles.rollout.sglang_rollout import GenerateState +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + +SYSTEM_PROMPT = """ +You are a helpful math-solving assistant with access to the `execute_python_code` tool. + +Guidelines: +- For any numerical or symbolic computation, always use the `execute_python_code` tool rather than performing calculations mentally. +- Break problems into clear steps, calling the Python tool whenever computation is required. +- After completing your reasoning, present the final result enclosed in \\boxed{}. +""".strip() + +MAX_TOOL_ITERATIONS = 5 + +_client_cache: dict[str, SGLangClient] = {} + + +def get_client(args) -> SGLangClient: + """Get shared client for connection pooling (like MILES).""" + base_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}" + if base_url not in _client_cache: + _client_cache[base_url] = SGLangClient.from_miles_args(args) + return _client_cache[base_url] + + +@tool +def execute_python_code(code: str) -> str: + """Execute Python code and return the output.""" + interpreter = SubprocessInterpreter( + require_confirm=False, + print_stdout=False, + print_stderr=False, + execution_timeout=60.0, + ) + result = interpreter.run(code, "python") + logger.info(f"Executing Python code: ```python\n{code}\n``` and get execution result: ```python\n{result}\n```") + return result + + +async def generate(args, sample: Sample, sampling_params) -> Sample: + """Generate with TITO: tokens captured during generation, no retokenization.""" + assert not args.partial_rollout, "Partial rollout not supported." + + state = GenerateState(args) + model = SGLangModel( + tokenizer=state.tokenizer, + client=get_client(args), + model_id=args.hf_checkpoint.split("/")[-1], + params={k: sampling_params[k] for k in ["max_new_tokens", "temperature", "top_p"]}, + ) + + limiter = ToolIterationLimiter(max_iterations=MAX_TOOL_ITERATIONS) + agent = Agent( + model=model, + tools=[execute_python_code], + hooks=[limiter], + callback_handler=None, + system_prompt=SYSTEM_PROMPT, + ) + + prompt = sample.prompt if isinstance(sample.prompt, str) else sample.prompt[0]["content"] + + try: + await agent.invoke_async(prompt) + sample.status = Sample.Status.COMPLETED + except Exception as e: + # Always use TRUNCATED instead of ABORTED because Miles doesn't properly + # handle ABORTED samples in reward processing. See: https://github.com/THUDM/slime/issues/200 + sample.status = Sample.Status.TRUNCATED + logger.warning(f"TRUNCATED: {type(e).__name__}: {e}") + + # TITO: extract trajectory from token_manager + tm = model.token_manager + prompt_len = len(tm.segments[0]) # system + user are first segment + sample.tokens = tm.token_ids + sample.loss_mask = tm.loss_mask[prompt_len:] + sample.rollout_log_probs = tm.logprobs[prompt_len:] + sample.response_length = len(sample.tokens) - prompt_len + sample.response = model.tokenizer.decode(sample.tokens[prompt_len:], skip_special_tokens=False) + # Tool iteration and tool call count are different because multiple parallel tool calls count as 1 iteration + sample.tool_iterations = limiter.iteration_count + trajectory = model.format_request_messages(agent.messages, None) + sample.tool_call_count = [message["role"] == "tool" for message in trajectory].count(True) + + model.reset() + agent.cleanup() + return sample + + +async def reward_func(args, sample: Sample, **kwargs): + """Reward function using math_dapo scoring.""" + ground_truth = sample.label or "" + tool_iterations = getattr(sample, "tool_iterations", 0) + + result = math_dapo_compute_score(sample.response, ground_truth, strict_box_verify=False) + if result["pred"] == "[INVALID]": + result = math_dapo_compute_score(sample.response, ground_truth, strict_box_verify=True) + + # Encourage tool use on failures + if result["score"] < 0: + result["score"] = min(-0.6, result["score"] + (tool_iterations - 2) / 2 * 0.1) + + result["pred"] = result["pred"] or "" + logger.info( + f"reward={result['score']:.2f} | status={sample.status.name} | tool_iters={tool_iterations} | tool_calls={getattr(sample, 'tool_call_count', 0)} | tokens={len(sample.tokens)} | resp_len={sample.response_length} | " + ) + return result["score"] diff --git a/examples/strands-agents/requirements.txt b/examples/strands_sglang/requirements.txt similarity index 75% rename from examples/strands-agents/requirements.txt rename to examples/strands_sglang/requirements.txt index 040fa471c..2c838bab3 100644 --- a/examples/strands-agents/requirements.txt +++ b/examples/strands_sglang/requirements.txt @@ -1,3 +1,4 @@ camel-ai strands-agents strands-agents-tools +strands-sglang diff --git a/examples/strands-agents/strands_qwen3_4b.sh b/examples/strands_sglang/strands_qwen3_8b.sh similarity index 73% rename from examples/strands-agents/strands_qwen3_4b.sh rename to examples/strands_sglang/strands_qwen3_8b.sh index 647c8e2f5..7d29eb279 100644 --- a/examples/strands-agents/strands_qwen3_4b.sh +++ b/examples/strands_sglang/strands_qwen3_8b.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Qwen3-8B Training with Strands-SGLang +# Note: 8B model requires ~2x memory of 4B, adjusted settings accordingly + # for rerun the task pkill -9 sglang sleep 3 @@ -23,39 +26,39 @@ else fi echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" -source "/root/miles/scripts/models/qwen3-4B.sh" +source "/root/miles/scripts/models/qwen3-8B.sh" # Generate timestamp suffix for save path TIMESTAMP_SUFFIX=$(date +%Y%m%d_%H%M%S) CKPT_ARGS=( - --hf-checkpoint /root/models/Qwen/Qwen3-4B-Instruct-2507 - --ref-load /root/models/Qwen/Qwen3-4B-Instruct-2507_torch_dist - # --load Qwen3-4B-Instruct-2507_strands_dapo_1129 - --save /root/models/Qwen/Qwen3-4B-Instruct-2507_strands_dapo_${TIMESTAMP_SUFFIX} + --hf-checkpoint /root/models/Qwen/Qwen3-8B + --ref-load /root/models/Qwen/Qwen3-8B_torch_dist + # --load Qwen3-8B_strands_dapo + --save /root/models/Qwen/Qwen3-8B_strands_dapo_${TIMESTAMP_SUFFIX} --save-interval 20 - --rotary-base 5000000 + --rotary-base 1000000 ) ROLLOUT_ARGS=( - --prompt-data /root/data/dapo-math-17k/dapo-math-17k.jsonl + --prompt-data /root/data/dapo-math-17k.jsonl --input-key prompt --label-key label --rollout-shuffle - --reward-key score + # --reward-key score --num-rollout 3000 - --rollout-batch-size 32 + --rollout-batch-size 16 # Reduced from 32 for 8B model memory --n-samples-per-prompt 8 - --rollout-max-response-len 8192 + --rollout-max-response-len 16384 --rollout-temperature 1 - --global-batch-size 256 + --global-batch-size 128 # Reduced from 256 for 8B model memory --balance-data ) EVAL_ARGS=( --eval-interval 20 - --eval-prompt-data aime /root/data/aime-2024/aime-2024.jsonl + --eval-prompt-data aime /root/data/aime-2024.jsonl --n-samples-per-eval-prompt 16 --eval-max-response-len 16384 --eval-top-p 1 @@ -75,7 +78,7 @@ PERF_ARGS=( # --micro-batch-size 1 --use-dynamic-batch-size - --max-tokens-per-gpu 9216 + --max-tokens-per-gpu 18432 ) GRPO_ARGS=( @@ -100,14 +103,15 @@ OPTIMIZER_ARGS=( WANDB_ARGS=( --use-wandb --wandb-project strands-miles - --wandb-group Qwen3-4B-Instruct-2507-strands-dapo + --wandb-group Qwen3-8B-strands-dapo --wandb-key ${WANDB_KEY} ) SGLANG_ARGS=( --rollout-num-gpus-per-engine 2 - --sglang-mem-fraction-static 0.7 - --sglang-tool-call-parser qwen # Enable tool call parsing for Strands Agent + --sglang-mem-fraction-static 0.4 + # Note: strands-sglang handles tool parsing internally (HermesToolCallParser) + # No need for --sglang-tool-call-parser ) MISC_ARGS=( @@ -122,8 +126,8 @@ MISC_ARGS=( ) CUSTOM_ARGS=( - --custom-generate-function-path examples.strands-agents.generate_with_strands.generate - --custom-rm-path examples.strands-agents.generate_with_strands.reward_func + --custom-generate-function-path examples.strands_sglang.generate_with_strands.generate + --custom-rm-path examples.strands_sglang.generate_with_strands.reward_func ) # launch the master node of ray in container @@ -155,4 +159,4 @@ ray job submit --address="http://127.0.0.1:8265" \ ${EVAL_ARGS[@]} \ ${SGLANG_ARGS[@]} \ ${MISC_ARGS[@]} \ - ${CUSTOM_ARGS[@]} \ No newline at end of file + ${CUSTOM_ARGS[@]} diff --git a/examples/tau-bench/README.md b/examples/tau-bench/README.md index 524157b75..417275c1b 100644 --- a/examples/tau-bench/README.md +++ b/examples/tau-bench/README.md @@ -9,13 +9,13 @@ Use the `zhuzilin/miles:latest` image and initialize the environment required fo cd /root/ git clone https://github.com/radixark/miles.git cd miles -pip install -e . +pip install -e . --no-deps # for tau bench cd /root/ git clone https://github.com/JD-ETH/tau-bench.git cd tau-bench git checkout feature/litellm-retry -pip install -e . +pip install -e . --no-deps ``` Use the following script to generate mock data for miles training. diff --git a/examples/train_infer_mismatch_helper/mis.py b/examples/train_infer_mismatch_helper/mis.py index 98fbe44b4..1e5f99275 100644 --- a/examples/train_infer_mismatch_helper/mis.py +++ b/examples/train_infer_mismatch_helper/mis.py @@ -335,7 +335,7 @@ def compute_mis_weights_with_cp( is_metrics: The metrics for the importance sampling weights, a dict of flattened tensors. """ # Lazy import to avoid importing Megatron dependencies when only `compute_mis_weights` is used. - from miles.backends.megatron_utils.cp_utils import all_gather_with_cp, slice_log_prob_with_cp + from miles.backends.training_utils.cp_utils import all_gather_with_cp, slice_log_prob_with_cp # Gather cp slice from other cp ranks full_rollout_log_probs = [ diff --git a/miles/backends/megatron_utils/__init__.py b/miles/backends/megatron_utils/__init__.py index 9ca75eb41..a4666fbeb 100644 --- a/miles/backends/megatron_utils/__init__.py +++ b/miles/backends/megatron_utils/__init__.py @@ -39,4 +39,4 @@ def _patched_forward(self, *args, packed_seq_params=None, **kwargs): except ImportError: pass -logging.getLogger().setLevel(logging.WARNING) +logging.getLogger("megatron").setLevel(logging.WARNING) diff --git a/miles/backends/megatron_utils/megatron_to_hf/__init__.py b/miles/backends/megatron_utils/megatron_to_hf/__init__.py index ba5a286a3..84ff899aa 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/__init__.py +++ b/miles/backends/megatron_utils/megatron_to_hf/__init__.py @@ -3,8 +3,7 @@ from .glm4moe import convert_glm4moe_to_hf from .llama import convert_llama_to_hf from .mimo import convert_mimo_to_hf -from .processors.padding_remover import remove_padding -from .processors.quantizer import quantize_params +from .processors import quantize_params, remove_padding from .qwen2 import convert_qwen2_to_hf from .qwen3_next import convert_qwen3_next_to_hf from .qwen3moe import convert_qwen3moe_to_hf @@ -23,9 +22,6 @@ def convert_to_hf(args, model_name, name, param, quantization_config=None): converted_named_tensors = _convert_to_hf_core(args, model_name, name, param) - if not quantization_config: - return converted_named_tensors - return quantize_params(args, name, converted_named_tensors, quantization_config) diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/__init__.py b/miles/backends/megatron_utils/megatron_to_hf/processors/__init__.py index e69de29bb..0141c3548 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/__init__.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/__init__.py @@ -0,0 +1,15 @@ +from .padding_remover import remove_padding +from .quantizer_compressed_tensors import quantize_params_compressed_tensors +from .quantizer_fp8 import quantize_params_fp8 + +__all__ = ["remove_padding", "quantize_param", "quantize_params_fp8", "quantize_params_compressed_tensors"] + + +def quantize_params(args, megatron_name, converted_named_params, quantization_config): + if quantization_config is None: + return converted_named_params + elif quantization_config["quant_method"] == "fp8": + return quantize_params_fp8(args, megatron_name, converted_named_params, quantization_config) + elif quantization_config["quant_method"] == "compressed-tensors": + # only int4 at the moment. + return quantize_params_compressed_tensors(converted_named_params, quantization_config) diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_compressed_tensors.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_compressed_tensors.py new file mode 100644 index 000000000..41712b33a --- /dev/null +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_compressed_tensors.py @@ -0,0 +1,189 @@ +import logging +import math +import re +from typing import Literal + +import torch + +logger = logging.getLogger(__name__) + + +__all__ = ["quantize_params_compressed_tensors"] + + +def pack_to_int32( + value: torch.Tensor, + num_bits: int, + packed_dim: Literal[0] | Literal[1] = 1, +) -> torch.Tensor: + """ + Packs a tensor of quantized weights stored in int8 into int32s with padding + + Pseudocode: + 1. Shift wrt num_bits to convert to unsigned. num_bits=8 + [1,2] -> [129, 130] + 2. Pad to fill in 32 bits + [129, 130] -> [129, 130, 0, 0] + 3. convert to binary align in order + [129, 130, 0, 0] -> 00000000 00000000 10000010 10000001 + 4. convert aligned binary to number + 00000000000000001000001010000001 -> 33409 + 5. covert back to uint32 + 33409 -> 33409 + + :param value: tensor to pack + :param num_bits: number of bits used to store underlying data, must be at least 1 + :returns: packed int32 tensor + """ + if value.dtype is not torch.int8: + raise ValueError("Tensor must be quantized to torch.int8 before packing") + + if num_bits > 8: + raise ValueError("Packing is only supported for less than 8 bits") + + if num_bits < 1: + raise ValueError(f"num_bits must be at least 1, got {num_bits}") + + # Convert to unsigned range for packing, matching quantization offset + offset = 1 << (num_bits - 1) + value = (value + offset).to(torch.uint8) + device = value.device + + pack_factor = 32 // num_bits + + if packed_dim == 0: + value = value.transpose(0, 1) + + rows, cols = value.shape + padded_cols = math.ceil(cols / pack_factor) * pack_factor + pad_len = padded_cols - cols + + if pad_len > 0: + value = torch.nn.functional.pad(value, (0, pad_len)) + + num_groups = padded_cols // pack_factor + + # Use int32 here + reshaped = value.view(rows, num_groups, pack_factor).to(torch.int32) + bit_shifts = torch.arange(pack_factor, device=device, dtype=torch.int32) * num_bits + packed = (reshaped << bit_shifts).sum(dim=2, dtype=torch.int32) + + if packed_dim == 0: + packed = packed.transpose(0, 1) + + return packed + + +def pack_int4_to_int32(q_weight: torch.Tensor) -> torch.Tensor: + """ + pack int4 to int32 + Args: + q_weight: [N, K] tensor, dtype=int8 or uint8 + Returns: + packed: [N, K // 8] tensor, dtype=int32 + """ + return pack_to_int32(q_weight, 4, -1) + + +def int4_block_quantize(x: torch.Tensor, group_size: int = 128) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + De-quantized = Scale * Quantized (Zero Point is always 0) + """ + N, K = x.shape + if group_size == -1: + group_size = K + + # Padding + if K % group_size != 0: + import torch.nn.functional as F + + x = F.pad(x, (0, group_size - (K % group_size))) + N, K = x.shape + + num_groups = K // group_size + x_reshaped = x.float().view(N, num_groups, group_size) + + # ========================================================= + # 1. Scale + # Range: [-7, 7] -> dividing by 7.0 + # ========================================================= + x_abs_max = x_reshaped.abs().amax(dim=-1, keepdim=True) + scale = x_abs_max / 7.0 + scale = scale.clamp(min=1e-5) + + # ========================================================= + # 2. Quantize + # ========================================================= + x_int_sym = (x_reshaped / scale).round().clamp(-8, 7) + + out = x_int_sym.to(torch.int8) + + # ========================================================= + # 3. Zero Point + # ========================================================= + zero_point = torch.zeros_like(scale) + out = out.view(N, K) + + scale_out = scale.squeeze(-1).contiguous() + zero_out = zero_point.squeeze(-1).contiguous() + + return out, scale_out, zero_out + + +def quantize_params_compressed_tensors(converted_named_params, quantization_config): + w_cfg = quantization_config["config_groups"]["group_0"]["weights"] + group_size = w_cfg["group_size"] + is_symmetric = w_cfg["symmetric"] + ignore_rules = quantization_config.get("ignore", []) + + results = [] + + for name, param in converted_named_params: + is_ignored = any((r.startswith("re:") and re.match(r[3:], name)) or r == name for r in ignore_rules) + + if is_ignored or not name.endswith(".weight") or param.dim() < 2: + results.append((name, param)) + continue + + input_tensor = param.view(-1, param.shape[-1]) if param.dim() > 2 else param + + if group_size != -1 and input_tensor.shape[-1] < group_size: + logger.warning(f"Skipping {name}, K-dim {input_tensor.shape[-1]} < group_size") + results.append((name, param)) + continue + + results.extend(_quantize_param_int4(name, input_tensor, group_size, param.shape, is_symmetric)) # origin shape + + return results + + +def _quantize_param_int4(name: str, weight: torch.Tensor, group_size: int, shape: torch.Tensor, is_symmetric: bool): + """ + Wraps the quantization function, handles renaming and packing. + """ + base_name = name.replace(".weight", "") + + new_base_name = base_name + + original_dtype = weight.dtype + + if group_size == -1: + group_size = weight.shape[1] + elif weight.shape[1] % group_size != 0: + logger.warning( + f"Weight {name} with shape {weight.shape} has K-dimension " + f"not divisible by group_size {group_size}. Skipping." + ) + return [(name, weight.to(original_dtype))] + + q_weight, scales, zeros = int4_block_quantize(weight, group_size) + + packed_q_weight = pack_int4_to_int32(q_weight) + + qweight_name = f"{new_base_name}.weight_packed" + scales_name = f"{new_base_name}.weight_scale" + qweight_shape = f"{new_base_name}.weight_shape" + + q_shape = torch.tensor(shape, dtype=torch.int32, device="cuda") + + return [(qweight_name, packed_q_weight), (scales_name, scales.to(original_dtype)), (qweight_shape, q_shape)] diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py similarity index 96% rename from miles/backends/megatron_utils/megatron_to_hf/processors/quantizer.py rename to miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py index 9876f6020..41495f396 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py @@ -7,9 +7,7 @@ from ...sglang import quant_weight_ue8m0, should_deepgemm_weight_requant_ue8m0, transform_scale_ue8m0 -def quantize_params(args, megatron_name, converted_named_params, quantization_config): - if quantization_config is None: - return converted_named_params +def quantize_params_fp8(args, megatron_name, converted_named_params, quantization_config): assert quantization_config["quant_method"] == "fp8" assert quantization_config["fmt"] == "e4m3" assert quantization_config["activation_scheme"] == "dynamic" diff --git a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed.py b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed.py index 801074553..f9c90bb1b 100644 --- a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed.py +++ b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed.py @@ -80,6 +80,13 @@ def update_weights(self) -> None: if dist.get_rank() == 0: ray.get([engine.pause_generation.remote() for engine in self.rollout_engines]) ray.get([engine.flush_cache.remote() for engine in self.rollout_engines]) + # int4/fp4 pre_process + if self.quantization_config and self.quantization_config["quant_method"] in ["compressed-tensors"]: + post_process_weights( + restore_weights_before_load=True, + post_process_quantization=False, + rollout_engines=self.rollout_engines, + ) dist.barrier(group=get_gloo_group()) buffer_size = 0 @@ -111,9 +118,15 @@ def update_weights(self) -> None: if named_tensors: self._update_expert_bucket_weights_from_distributed(named_tensors, pbar=pbar) - dist.barrier(group=get_gloo_group()) if dist.get_rank() == 0: ray.get([engine.continue_generation.remote() for engine in self.rollout_engines]) + # int4/fp4 post_process + if self.quantization_config and self.quantization_config["quant_method"] in ["compressed-tensors"]: + post_process_weights( + restore_weights_before_load=False, + post_process_quantization=True, + rollout_engines=self.rollout_engines, + ) dist.barrier(group=get_gloo_group()) def _update_weight_from_distributed( @@ -297,3 +310,22 @@ def update_weights_from_distributed( handle.wait() return refs + + +def post_process_weights( + restore_weights_before_load: bool, + post_process_quantization: bool, + rollout_engines: Sequence[ActorHandle], +): + """ + Trigger post-process for int4/fp4 quantization on all rollout engines. + """ + ray.get( + [ + engine.post_process_weights.remote( + restore_weights_before_load=restore_weights_before_load, + post_process_quantization=post_process_quantization, + ) + for engine in rollout_engines + ] + ) diff --git a/miles/backends/megatron_utils/update_weight/update_weight_from_tensor.py b/miles/backends/megatron_utils/update_weight/update_weight_from_tensor.py index 527d3cfe9..1acfabba3 100644 --- a/miles/backends/megatron_utils/update_weight/update_weight_from_tensor.py +++ b/miles/backends/megatron_utils/update_weight/update_weight_from_tensor.py @@ -16,6 +16,7 @@ from .update_weight_from_distributed import ( connect_rollout_engines_from_distributed, disconnect_rollout_engines_from_distributed, + post_process_weights, update_weights_from_distributed, ) @@ -112,6 +113,12 @@ def update_weights(self) -> None: rank = dist.get_rank() if rank == 0: ray.get([engine.flush_cache.remote() for engine in self.rollout_engines]) + if self.quantization_config and self.quantization_config["quant_method"] in ["compressed-tensors"]: + post_process_weights( + restore_weights_before_load=True, + post_process_quantization=False, + rollout_engines=self.rollout_engines, + ) dist.barrier(group=get_gloo_group()) megatron_local_weights = self.weights_getter() @@ -121,6 +128,14 @@ def update_weights(self) -> None: ray.get(refs) del long_lived_tensors + # int4/fp4 post_process + if rank == 0: + if self.quantization_config and self.quantization_config["quant_method"] in ["compressed-tensors"]: + post_process_weights( + restore_weights_before_load=False, + post_process_quantization=True, + rollout_engines=self.rollout_engines, + ) dist.barrier(group=get_gloo_group()) def _send_hf_params(self, hf_named_tensors) -> tuple[list[ObjectRef], Any]: diff --git a/miles/backends/sglang_utils/sglang_engine.py b/miles/backends/sglang_utils/sglang_engine.py index 179306023..f736cf97a 100644 --- a/miles/backends/sglang_utils/sglang_engine.py +++ b/miles/backends/sglang_utils/sglang_engine.py @@ -401,6 +401,25 @@ def continue_generation(self): response.raise_for_status() return response + def post_process_weights( + self, + restore_weights_before_load: bool = False, + post_process_quantization: bool = False, + ): + """ + Update model weights from tensor data. The HTTP server will only post meta data, and the real weights will be copied directly from GPUs. + Note: The model should be on GPUs rather than CPU for this functionality to work properly. + If you encounter issues, ensure your model is loaded on GPU devices rather than CPU. + """ + + return self._make_request( + "post_process_weights", + { + "restore_weights_before_load": restore_weights_before_load, + "post_process_quantization": post_process_quantization, + }, + ) + def start_profile( self, # The output directory diff --git a/miles/backends/training_utils/__init__.py b/miles/backends/training_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/miles/utils/ppo_utils.py b/miles/utils/ppo_utils.py index 3883fa423..34904477a 100644 --- a/miles/utils/ppo_utils.py +++ b/miles/utils/ppo_utils.py @@ -246,7 +246,7 @@ def get_reinforce_plus_plus_returns( if cp_size > 1: # Step 1,2:Gather all chunks and token_offsets from all ranks and reconstruct the full response tensor by splitting and placing each part - from miles.backends.megatron_utils.cp_utils import all_gather_with_cp + from miles.backends.training_utils.cp_utils import all_gather_with_cp full_kl_response = all_gather_with_cp(local_kl_chunk, total_len, response_len, parallel_state) else: @@ -342,7 +342,7 @@ def get_advantages_and_returns( cp_size = mpu.get_context_parallel_world_size() if cp_size > 1: - from miles.backends.megatron_utils.cp_utils import all_gather_with_cp + from miles.backends.training_utils.cp_utils import all_gather_with_cp full_rewards = all_gather_with_cp(rewards, total_len, response_len, parallel_state) full_values = all_gather_with_cp(values, total_len, response_len, parallel_state) @@ -407,7 +407,7 @@ def get_advantages_and_returns_batch( dtype = values_list[0].dtype if cp_size > 1: - from miles.backends.megatron_utils.cp_utils import all_gather_with_cp + from miles.backends.training_utils.cp_utils import all_gather_with_cp full_values_list = [] full_rewards_list = [] diff --git a/miles/utils/types.py b/miles/utils/types.py index ccf569da2..0a2531a7a 100644 --- a/miles/utils/types.py +++ b/miles/utils/types.py @@ -123,10 +123,20 @@ def to_dict(self): @staticmethod def from_dict(data: dict): + data = dict(data) data["status"] = Sample.Status(data["status"]) data["spec_info"] = Sample.SpecInfo.from_dict(data.get("spec_info", {})) data["prefix_cache_info"] = Sample.PrefixCacheInfo.from_dict(data.get("prefix_cache_info", {})) - return Sample(**data) + + field_names = set(Sample.__dataclass_fields__.keys()) + init_data = {k: v for k, v in data.items() if k in field_names} + sample = Sample(**init_data) + + for key, value in data.items(): + if key not in field_names: + setattr(sample, key, value) + + return sample def get_reward_value(self, args) -> float: return self.reward if not args.reward_key else self.reward[args.reward_key] diff --git a/tools/convert_hf_to_fp8.py b/tools/convert_hf_to_fp8.py index ee48582e2..7754e7dea 100644 --- a/tools/convert_hf_to_fp8.py +++ b/tools/convert_hf_to_fp8.py @@ -65,7 +65,7 @@ def block_fp8(weight, block_size): .to(torch.float8_e4m3fn) ) qweight = qweight[:shape_0, :shape_1].clone().detach() - scale = scale.squeeze() + scale = scale.reshape(n_tiles, k_tiles) return qweight, scale @@ -101,12 +101,15 @@ def __init__(self): self.weight_map = {} self.param_count = 0 self.modules_to_not_convert = [] + self.has_dsa_layers = False def add_result(self, filename, q_weights, module_names): with self.lock: for k, v in q_weights.items(): self.weight_map[k] = filename self.param_count += len(v) + if "indexer" in k: + self.has_dsa_layers = True self.modules_to_not_convert.extend(module_names) @@ -133,6 +136,7 @@ def process_file(input_path, output_path, filename, strategy, block_size, result and "norm" not in key and "lm_head" not in key and "eh_proj" not in key + and "weights_proj" not in key ): qw, s = quant_fp8(weights[key], strategy, block_size) q_weights[key] = qw @@ -181,6 +185,8 @@ def convert_fp8(input_path, output_path, strategy, block_size=None, max_workers= } if block_size: quantization_config["weight_block_size"] = block_size + if result_collector.has_dsa_layers: + quantization_config["scale_fmt"] = "ue8m0" if len(result_collector.modules_to_not_convert) > 0: quantization_config["modules_to_not_convert"] = list(set(result_collector.modules_to_not_convert)) else: diff --git a/tools/convert_hf_to_hf_int4.py b/tools/convert_hf_to_hf_int4.py new file mode 100644 index 000000000..ba76a987f --- /dev/null +++ b/tools/convert_hf_to_hf_int4.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +import argparse +import os +import random + +import torch +from datasets import Dataset, load_dataset +from llmcompressor import oneshot +from llmcompressor.modifiers.quantization.gptq import GPTQModifier +from transformers import AutoModelForCausalLM, AutoTokenizer + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--input-dir", type=str, required=True, help="local BF16 path") + parser.add_argument("--output-dir", type=str, required=True) + parser.add_argument("--data-dir", type=str, required=True, help="dataset path") + parser.add_argument("--quant-type", type=str, choices=["W4A16", "W8A16"], default="W4A16") + parser.add_argument("--num-calibration-samples", type=int, default=256, help="sample nums") + parser.add_argument("--max-sequence-length", type=int, default=2048) + parser.add_argument("--dampening-frac", type=float, default=0.01) + parser.add_argument("--trust-remote-code", action="store_true") + parser.add_argument("--quant-group-size", type=int, default=32, help="GPTQ Group Size") + return parser.parse_args() + + +def get_calibration_dataset(tokenizer, num_samples, seq_len, local_data_path): + + train_file = os.path.join(local_data_path, "train-00000-of-00001.parquet") + + if not os.path.exists(train_file): + print(f"can't find the localpath: {train_file}") + exit(1) + + try: + ds_raw = load_dataset("parquet", data_files={"train": train_file}, split="train") + except Exception as e: + print(f"load Parquet file failed: {e}") + exit(1) + + text_stream = "".join(ds_raw["text"]) + encoded = tokenizer(text_stream, return_tensors="pt").input_ids[0] + + data_list = [] + for _ in range(num_samples): + i = random.randint(0, encoded.shape[0] - seq_len - 1) + chunk = encoded[i : i + seq_len] + + data_list.append({"input_ids": chunk.tolist(), "attention_mask": torch.ones_like(chunk).tolist()}) + + ds_hf = Dataset.from_list(data_list) + return ds_hf + + +def main(): + args = parse_args() + + tokenizer = AutoTokenizer.from_pretrained(args.model_id, trust_remote_code=args.trust_remote_code) + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + + ds_hf = get_calibration_dataset( + tokenizer, args.num_calibration_samples, args.max_sequence_length, args.local_data_path + ) + + model = AutoModelForCausalLM.from_pretrained( + args.model_id, + device_map="auto", + torch_dtype=torch.bfloat16, + trust_remote_code=args.trust_remote_code, + low_cpu_mem_usage=True, + ) + + ignore_patterns = [ + "re:.*lm_head.*", + "re:.*norm.*", + "re:.*embed.*", + "re:.*self_attn.*", + "re:.*shared_experts.*", + "re:.*mlp\\.(gate|up|gate_up|down)_proj.*", + ] + + recipe = GPTQModifier( + targets="Linear", + scheme=args.quant_type, + ignore=ignore_patterns, + dampening_frac=args.dampening_frac, + block_size=32, + ) + + oneshot( + model=model, + dataset=ds_hf, # dataset + tokenizer=tokenizer, + recipe=recipe, + output_dir=args.output_dir, + max_seq_length=args.max_sequence_length, + num_calibration_samples=args.num_calibration_samples, + ) + + +if __name__ == "__main__": + main() From 636c9958f5c2970bdc9fe1aca103b9819c9e771c Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Tue, 13 Jan 2026 00:28:29 -0800 Subject: [PATCH 17/57] [minor] delete unused util file (#428) --- miles/backends/fsdp_utils/data_packing.py | 218 ---------------------- 1 file changed, 218 deletions(-) delete mode 100644 miles/backends/fsdp_utils/data_packing.py diff --git a/miles/backends/fsdp_utils/data_packing.py b/miles/backends/fsdp_utils/data_packing.py deleted file mode 100644 index 676d242ec..000000000 --- a/miles/backends/fsdp_utils/data_packing.py +++ /dev/null @@ -1,218 +0,0 @@ -"""Data packing utilities for FSDP backend to reduce padding overhead.""" - -import math - -import torch -import torch.nn.functional as F - -from miles.utils.seqlen_balancing import get_seqlen_balanced_partitions - - -def pack_sequences( - tokens: list[list[int]], - loss_masks: list[list[int]], - rewards: list[float], - raw_rewards: list, - response_lengths: list[int], - advantages: list[float], - returns: list[float], - rollout_log_probs: list[list[float]] | None = None, - multimodal_train_inputs: list[dict] | None = None, - max_tokens_per_gpu: int | None = None, - num_packs: int | None = None, -) -> list[dict]: - """ - Pack sequences into dense batches with cumulative sequence lengths. - - Args: - tokens: List of token sequences - loss_masks: List of loss masks - rewards: List of rewards per sequence - raw_rewards: List of raw rewards per sequence - response_lengths: List of response lengths per sequence - advantages: List of advantages per sequence - returns: List of returns per sequence - rollout_log_probs: List of rollout log probabilities per sequence - multimodal_train_inputs: List of dict of multimodal tensors for training per sequence - max_tokens_per_gpu: Maximum tokens per GPU pack - num_packs: Explicit number of packs to create - - Returns: - List of packed batches with tokens, masks, cu_seqlens, rewards, raw_rewards, response_lengths, advantages, returns - """ - if not tokens: - return [] - - seq_lengths = [len(t) for t in tokens] - - # Determine number of packs and use balanced partitioning - if num_packs: - k_partitions = num_packs - elif max_tokens_per_gpu: - total_tokens = sum(seq_lengths) - k_partitions = max(1, math.ceil(total_tokens / max_tokens_per_gpu)) - else: - k_partitions = 1 - - # Use balanced partitioning for optimal load distribution - partitions = get_seqlen_balanced_partitions( - seq_lengths, k_partitions=k_partitions, equal_size=False # Allow variable sizes for better balance - ) - - # Pack each partition - result = [] - for indices in partitions: - # Build cumulative sequence lengths - cu_seqlens = [0] - flat_tokens = [] - flat_masks = [] - flat_positionids = [] - flat_advantages = [] - flat_returns = [] - flat_rollout_log_probs = [] - - for i in indices: - seq_tokens = tokens[i] - seq_mask = loss_masks[i] - seq_positionids = list(range(len(seq_tokens))) - - flat_tokens.extend(seq_tokens) - flat_positionids.extend(seq_positionids) - flat_masks.extend(seq_mask) - flat_advantages.extend(advantages[i]) - flat_returns.extend(returns[i]) - if rollout_log_probs: - flat_rollout_log_probs.extend(rollout_log_probs[i]) - cu_seqlens.append(cu_seqlens[-1] + len(seq_tokens)) - - packed_batch = { - "tokens": torch.tensor(flat_tokens, dtype=torch.long), - "loss_masks": torch.tensor(flat_masks, dtype=torch.int), - "position_ids": torch.tensor(flat_positionids, dtype=torch.int), - "cu_seqlens": torch.tensor(cu_seqlens, dtype=torch.int32), - "rewards": torch.tensor([rewards[i] for i in indices], dtype=torch.float32), - "raw_reward": [raw_rewards[i] for i in indices], - "response_lengths": [response_lengths[i] for i in indices], - "advantages": torch.tensor(flat_advantages, dtype=torch.float32), - "returns": torch.tensor(flat_returns, dtype=torch.float32), - "rollout_log_probs": torch.tensor( - flat_rollout_log_probs, dtype=torch.float32, device=torch.cuda.current_device() - ), - } - - # Collect and add multimodal training tensors for this partition - if multimodal_train_inputs: - multimodal_data = {} # key -> concatenated tensor - multimodal_num_items = {} # key -> list of item counts per sequence - for i in indices: - for key, mm_tensor in multimodal_train_inputs[i].items(): - if key not in multimodal_data: - multimodal_data[key] = mm_tensor - multimodal_num_items[key] = [mm_tensor.size(0)] - else: - multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0) - multimodal_num_items[key].append(mm_tensor.size(0)) - packed_batch["multimodal_train_inputs"] = multimodal_data - packed_batch["multimodal_num_items"] = multimodal_num_items - - result.append(packed_batch) - - return result - - -def unpack_sequences(packed_batch: dict) -> list[dict]: - """ - Unpack sequences from a packed batch. - - Args: - packed_batch: Packed batch - - Returns: - List of unpacked batches - """ - - cu_seqlens = packed_batch["cu_seqlens"] - num_sequences = len(cu_seqlens) - 1 - response_lengths = packed_batch["response_lengths"] - multimodal_num_items = packed_batch.get("multimodal_num_items", {}) - - instances = [] - - # Calculate pad_length by counting trailing zeros - tokens = packed_batch["tokens"] - nonzero_indices = (tokens != 0).nonzero(as_tuple=True)[0] - if len(nonzero_indices) > 0: - # Last non-zero index, pad_length is everything after it - pad_length = len(tokens) - nonzero_indices[-1].item() - 1 - else: - pad_length = 0 # No padding if no non-zero tokens (or all zeros) - for i in range(num_sequences): - start_idx = cu_seqlens[i].item() - end_idx = cu_seqlens[i + 1].item() - instance = {} - - # Copy any additional attributes that might exist in the packed batch - for key, value in packed_batch.items(): - if key not in instance: - # Skip multimodal_num_items - it's metadata - if key == "multimodal_num_items": - continue - # Handle multimodal_train_inputs dict: split each tensor using multimodal_num_items - elif key == "multimodal_train_inputs" and isinstance(value, dict): - instance[key] = {} - for mm_key, mm_tensor in value.items(): - if mm_key in multimodal_num_items: - num_items_list = multimodal_num_items[mm_key] - start_mm_idx = sum(num_items_list[:i]) - end_mm_idx = start_mm_idx + num_items_list[i] - if num_items_list[i] > 0: - instance[key][mm_key] = mm_tensor[start_mm_idx:end_mm_idx] - # For tensor attributes, we need to slice them appropriately - elif isinstance(value, torch.Tensor): - if key in ["log_probs", "ref_log_probs", "cur_log_probs", "entropy"]: - # These are computed from logits[:-1] so they have length seq_len-1 - instance[key] = value[ - end_idx - 1 - response_lengths[i] - pad_length : end_idx - 1 - pad_length - ] - elif key == "rollout_log_probs": - # rollout_log_probs is packed based on response_lengths, so slice differently - instance[key] = value[sum(response_lengths[:i]) : sum(response_lengths[: i + 1])] - elif key in ["tokens", "position_ids"]: - # For other tensor attributes, try to slice them - if len(value) > start_idx: - instance[key] = value[start_idx:end_idx] - else: - raise ValueError(f"Attribute {key} is not found in the packed batch") - elif key in ["loss_masks", "advantages", "returns"]: - instance[key] = value[sum(response_lengths[:i]) : sum(response_lengths[: i + 1])] - elif isinstance(value, list): - instance[key] = value[i] - else: - raise ValueError(f"Attribute {key} is not found in the packed batch") - - instances.append(instance) - - return instances - - -def pad_packed_sequence_with_cp(packed_sequence: dict, cp_size: int) -> dict: - """Pad packed sequence to make total length divisible by cp_size. - - Args: - packed_sequence: Packed sequence dict containing tokens, position_ids, cu_seqlens, etc. - cp_size: Context parallelism world size - - Returns: - Padded packed sequence - """ - seq_length = len(packed_sequence["tokens"]) - # Calculate padding needed: (cp_size - seq_length % cp_size) % cp_size - remainder = seq_length % cp_size - pad_length = (cp_size - remainder) % cp_size - - if pad_length > 0: - packed_sequence["tokens"] = F.pad(packed_sequence["tokens"], (0, pad_length), value=0) - packed_sequence["position_ids"] = F.pad(packed_sequence["position_ids"], (0, pad_length), value=0) - packed_sequence["loss_masks"] = F.pad(packed_sequence["loss_masks"], (0, pad_length), value=0) - packed_sequence["cu_seqlens"][-1] += pad_length - return packed_sequence From e6571fa9cab5e7afda5a6f8725bfcd70a7218615 Mon Sep 17 00:00:00 2001 From: zijiexia <37504505+zijiexia@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:14:23 -0800 Subject: [PATCH 18/57] Remove AI response in the doc (#429) --- examples/low_precision/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/low_precision/README.md b/examples/low_precision/README.md index 5bb90442a..97389fd91 100644 --- a/examples/low_precision/README.md +++ b/examples/low_precision/README.md @@ -65,11 +65,6 @@ Currently, FP8 is far from being a complete feature and still has the following The miles team will continue to collaborate with the NVIDIA team to contribute more complete FP8 training infrastructure to the community. - -Here is a polished and professional version of your documentation. - -I have corrected grammatical errors, improved the flow, standardizes the terminology (e.g., capitalizing "STE"), and clarified the instructions. - *** ## INT4 Training Examples From 1f619e17587c8ffc56378c6ec67849a2df46d970 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:41:10 +0800 Subject: [PATCH 19/57] Fix rollout-all-samples (#431) --- miles/rollout/sglang_rollout.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 77c540d60..91918340a 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -395,7 +395,9 @@ async def generate_rollout_async( assert len(data) == args.rollout_batch_size, f"Got {len(data)} samples, expected {args.rollout_batch_size}" data = sorted(data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index) - all_samples = sorted(data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index) + all_samples = sorted( + all_data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index + ) # reset the global state to prevent effects on the next rollout or eval. state.reset() From 2cdc1f7189f0a37cb67f0420512d30d085344e06 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:42:07 +0800 Subject: [PATCH 20/57] Fix retool example incorrectly handling max_tool_calls (#462) --- examples/retool/generate_with_retool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/retool/generate_with_retool.py b/examples/retool/generate_with_retool.py index 9fb782edb..f5b8ad268 100644 --- a/examples/retool/generate_with_retool.py +++ b/examples/retool/generate_with_retool.py @@ -321,7 +321,7 @@ async def generate(args, sample: Sample, sampling_params) -> Sample: sample.rollout_log_probs ), f"Token/logp length mismatch at turn {turn}: {len(response_token_ids)} tokens vs {len(sample.rollout_log_probs)} logps" - if turn >= TOOL_CONFIGS["max_tool_calls"]: + if tool_call_count >= TOOL_CONFIGS["max_tool_calls"]: break # Set sample attributes From 5dd0044e569bf1b5c64292316849c16fbb619229 Mon Sep 17 00:00:00 2001 From: Zhiyao Jiang Date: Sat, 17 Jan 2026 02:29:39 -0500 Subject: [PATCH 21/57] Integrate Terminal Bench into Miles (#447) Co-authored-by: Xinyu Jiang Co-authored-by: Jiajun Li --- examples/eval/eval_delegate.py | 10 + examples/eval/{ => nemo_skills}/README.md | 0 examples/eval/scripts/eval_tb_example.yaml | 29 ++ examples/eval/scripts/run-eval-tb-qwen.sh | 159 +++++++ examples/eval/terminal_bench/README.md | 129 ++++++ examples/eval/terminal_bench/__init__.py | 1 + examples/eval/terminal_bench/requirements.txt | 3 + examples/eval/terminal_bench/tb_client.py | 104 +++++ examples/eval/terminal_bench/tb_config.py | 52 +++ examples/eval/terminal_bench/tb_server.py | 433 ++++++++++++++++++ 10 files changed, 920 insertions(+) rename examples/eval/{ => nemo_skills}/README.md (100%) create mode 100644 examples/eval/scripts/eval_tb_example.yaml create mode 100644 examples/eval/scripts/run-eval-tb-qwen.sh create mode 100644 examples/eval/terminal_bench/README.md create mode 100644 examples/eval/terminal_bench/__init__.py create mode 100644 examples/eval/terminal_bench/requirements.txt create mode 100644 examples/eval/terminal_bench/tb_client.py create mode 100644 examples/eval/terminal_bench/tb_config.py create mode 100644 examples/eval/terminal_bench/tb_server.py diff --git a/examples/eval/eval_delegate.py b/examples/eval/eval_delegate.py index fd6b9878d..1ecabe659 100644 --- a/examples/eval/eval_delegate.py +++ b/examples/eval/eval_delegate.py @@ -91,6 +91,12 @@ def _rebuild_delegate_config( env_cfg = build_skills_eval_env_config(args, env, defaults) if env_cfg is not None: envs.append(env_cfg) + elif env_name == "terminal_bench": + from examples.eval.terminal_bench.tb_config import build_terminal_bench_config + + env_cfg = build_terminal_bench_config(args, env, defaults) + if env_cfg is not None: + envs.append(env_cfg) else: raise ValueError(f"Unknown delegate environment: {env_name}") return envs @@ -151,6 +157,10 @@ def _create_delegate(env_cfg: EvalEnvConfig, router_addr: str): from examples.eval.nemo_skills.skills_client import SkillsEvalClient return SkillsEvalClient.from_config(env_cfg, router_addr) + elif env_name == "terminal_bench": + from examples.eval.terminal_bench.tb_client import TerminalBenchClient + + return TerminalBenchClient.from_config(env_cfg, router_addr) logger.warning("No delegate client registered for environment: %s", env_name) return None diff --git a/examples/eval/README.md b/examples/eval/nemo_skills/README.md similarity index 100% rename from examples/eval/README.md rename to examples/eval/nemo_skills/README.md diff --git a/examples/eval/scripts/eval_tb_example.yaml b/examples/eval/scripts/eval_tb_example.yaml new file mode 100644 index 000000000..2e2308981 --- /dev/null +++ b/examples/eval/scripts/eval_tb_example.yaml @@ -0,0 +1,29 @@ +eval: + defaults: + n_samples_per_eval_prompt: 1 + temperature: 0.6 + top_p: 0.95 + top_k: -1 + max_response_len: 24576 + datasets: # these eval tasks go through miles dataset config and default rollout function (miles.rollout.sglang_rollout.generate_rollout) + - name: gpqa # huggingface-cli download --repo-type dataset zyzshishui0627/gpqa_diamond --local-dir /root/gpqa + path: /root/gpqa/gpqa_eval.jsonl + rm_type: gpqa + n_samples_per_eval_prompt: 2 + - name: ifbench # huggingface-cli download --repo-type dataset zyzshishui0627/IFBench --local-dir /root/ifbench + path: /root/ifbench/IFBench_eval.jsonl + rm_type: ifbench + n_samples_per_eval_prompt: 1 + delegate: + - name: terminal_bench + url: http://172.17.0.1:9051 # Port must match the TB server running on the host machine + timeout_secs: 86400 # 24 hours + max_retries: 1 # HTTP request retries from Miles to the TB server + model_name: qwen3-8b + api_base: http://127.0.0.1:30005/v1 # Port must match the sglang router port set in run-eval-tb-qwen.sh + dataset_path: /mnt/data/xinyu/program/miles-tb/terminal-bench/tasks # Dataset path on the host machine + # task_ids: + # - hello-world + # n_tasks: 10 + n_attempts: 1 # TB task-level retries (per task within tb run) + n_concurrent: 8 \ No newline at end of file diff --git a/examples/eval/scripts/run-eval-tb-qwen.sh b/examples/eval/scripts/run-eval-tb-qwen.sh new file mode 100644 index 000000000..471a59d56 --- /dev/null +++ b/examples/eval/scripts/run-eval-tb-qwen.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# Example launcher that reuses the Qwen3-8B recipe but delegates evaluation to an +# external Terminal Bench server via the eval_delegate_rollout wrapper. + +# Clean up any stale processes from a previous run. +pkill -9 sglang +sleep 3 +ray stop --force +pkill -9 ray +pkill -9 python +sleep 3 +pkill -9 ray +pkill -9 python + +set -ex + +export PYTHONBUFFERED=16 +export MILES_HOST_IP=${MILES_HOST_IP:-"127.0.0.1"} + +MODEL_DIR="${MODEL_DIR:-/root/.cache}" +export MODEL_DIR + +NVLINK_COUNT=$(nvidia-smi topo -m 2>/dev/null | grep -o 'NV[0-9][0-9]*' | wc -l) +if [ "$NVLINK_COUNT" -gt 0 ]; then + HAS_NVLINK=1 +else + HAS_NVLINK=0 +fi +echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." &>/dev/null && pwd)" +source "${REPO_ROOT}/scripts/models/qwen3-8B.sh" + +# Store eval/delegate settings in a YAML config similar to examples/eval_multi_task. +EVAL_CONFIG_PATH=${TB_EVAL_CONFIG_PATH:-"${REPO_ROOT}/examples/eval/scripts/eval_tb_example.yaml"} + +CKPT_ARGS=( + --hf-checkpoint ${MODEL_DIR}/OpenThinker-Agent-v1 # huggingface-cli download open-thoughts/OpenThinker-Agent-v1 + --ref-load ${MODEL_DIR}/OpenThinker-Agent-v1_torch_dist + # --load ${MODEL_DIR}/OpenThinker-Agent-v1_miles/ + --save ${MODEL_DIR}/OpenThinker-Agent-v1_miles/ + --save-interval 20 +) + +ROLLOUT_ARGS=( + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 3000 + --rollout-batch-size 32 + --n-samples-per-prompt 8 + --rollout-max-response-len 8192 + --rollout-temperature 0.8 + --global-batch-size 256 + --balance-data +) + +EVAL_ARGS=( + --eval-interval 5 + --eval-config "${EVAL_CONFIG_PATH}" + --eval-function-path examples.eval.eval_delegate_rollout.generate_rollout +) + +PERF_ARGS=( + --tensor-model-parallel-size 1 + --pipeline-model-parallel-size 1 + --context-parallel-size 1 + --expert-model-parallel-size 1 + --expert-tensor-parallel-size 1 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + --use-dynamic-batch-size + --max-tokens-per-gpu 9216 +) + +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.00 + --kl-loss-type low_var_kl + --entropy-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 +) + +OPTIMIZER_ARGS=( + --optimizer adam + --lr 1e-6 + --lr-decay-style constant + --weight-decay 0.1 + --adam-beta1 0.9 + --adam-beta2 0.98 +) + +WANDB_ARGS=( + --use-wandb + --wandb-project miles-eval + --wandb-group qwen3-8b-eval + --wandb-key ${WANDB_KEY} # export WANDB_KEY="your_key" +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 + --sglang-router-port 30005 +) + +MISC_ARGS=( + --attention-dropout 0.0 + --hidden-dropout 0.0 + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + --attention-backend flash +) + +export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} +export CUDA_VISIBLE_DEVICES=0,1 + +ray start --head --node-ip-address ${MASTER_ADDR} --port 6380 --num-gpus 2 \ + --disable-usage-stats \ + --dashboard-host=0.0.0.0 \ + --dashboard-port=8266 \ + --dashboard-agent-listen-port 52366 \ + --dashboard-agent-grpc-port 52367 \ + --runtime-env-agent-port 52368 + + +RUNTIME_ENV_JSON="{ + \"env_vars\": { + \"PYTHONPATH\": \"/root/Megatron-LM/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\" + } +}" + +ray job submit --address="http://${MASTER_ADDR}:8266" \ + --working-dir "${REPO_ROOT}" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 2 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} diff --git a/examples/eval/terminal_bench/README.md b/examples/eval/terminal_bench/README.md new file mode 100644 index 000000000..341e543fc --- /dev/null +++ b/examples/eval/terminal_bench/README.md @@ -0,0 +1,129 @@ +# Terminal Bench Eval + +This folder wires Terminal Bench (TB) into Miles as an eval delegate. The TB run happens on the host via the `tb` CLI, and Miles reads back aggregated metrics such as `accuracy`, `n_resolved`, `n_unresolved`, `pass_at_k/*`, and token stats like `total_input_tokens_mean/median` and `total_output_tokens_mean/median`. + +## What runs where + +- Miles runs your training/eval loop inside the Docker container. +- Miles calls the TB delegate client. +- The TB delegate server (`tb_server.py`) runs `tb run ...` on the host. +- The server reads the latest TB JSON results and returns metrics to Miles. + +## 1) Get the code (host) + +```bash +mkdir miles-tb +cd miles-tb +git clone https://github.com/radixark/miles.git +git clone https://github.com/laude-institute/terminal-bench +``` + +## 2) Launch the Miles container + +```bash +docker run \ + -itd \ + --gpus all \ + --shm-size 32g \ + --network host \ + --ipc=host \ + --privileged \ + --ulimit memlock=-1 \ + --ulimit stack=67108864 \ + --ulimit nofile=65536:65536 \ + -v /mnt/data/.cache:/root/.cache \ + -v $(pwd):/shared/miles-tb \ + --name \ + radixark/miles:latest \ + /bin/bash +``` + +## 3) Inside the Miles container + +```bash +docker exec -it /bin/bash +``` + +## 4) Terminal Bench environment (host) + +Run on the machine that will host `tb_server.py` (where you cloned both repos): + +```bash +# Host machine terminal (outside Docker) +uv venv --python 3.13 .venv +source .venv/bin/activate + +uv pip install terminal-bench/. +uv pip install -r miles/examples/eval/terminal_bench/requirements.txt +``` + +Notes: +- Use your local repo paths if they are not `./miles` and `./terminal-bench`. + +## 5) Start the Terminal Bench server + +Run on the host (same machine where `tb` works): + +```bash +python miles/examples/eval/terminal_bench/tb_server.py \ + --host 0.0.0.0 --port 9051 \ + --output-root tb_eval_output +``` + +What it does: +- Uses `OPENAI_API_KEY=EMPTY` +- Runs `tb run -a terminus-2 -m openai/ ... --n-concurrent 8` +- Waits for completion, then returns `accuracy`, `n_resolved`, + `n_unresolved`, `pass_at_k/*`, and token stats such as + `total_input_tokens_mean/median` and `total_output_tokens_mean/median` + +## 6) Run the eval script (example) + +If you use the provided Qwen eval launcher (`run-eval-tb-qwen.sh`), follow the steps below to run Terminal-Bench evaluation. + +First, update the `dataset_path` in `eval_tb_example.yaml` to the local path of `terminal-bench/tasks` on your host (not an internal Docker-only path). + +Then download the HuggingFace model checkpoint inside the Miles container: + +```bash +huggingface-cli download open-thoughts/OpenThinker-Agent-v1 \ +--local-dir /root/.cache/OpenThinker-Agent-v1 +``` + +After downloading, convert the HuggingFace checkpoint to Miles's torch distributed format. From the Miles root directory, run: + +```bash +cd /shared/miles-tb/miles +source scripts/models/qwen3-8B.sh + +export PYTHONPATH=/root/Megatron-LM:/shared/miles-tb/miles + +python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/.cache/OpenThinker-Agent-v1 \ + --save /root/.cache/OpenThinker-Agent-v1_torch_dist +``` + +Finally, run the following command inside the Miles container: + +```bash +bash miles/examples/eval/scripts/run-eval-tb-qwen.sh 2>&1 | tee run.log +``` + +For convenience, you can restrict the evaluation scope in `eval_tb_example.yaml`, either by specifying a single task or multiple tasks (`task_ids`), or by limiting the number of tasks via `n_tasks`. + +## 7) Common Issues + +When running Miles inside a Docker container with `--network host`, Ray may encounter port conflicts due to shared networking with the host. + +In some cases, this manifests as Ray failing to start or reporting Redis- or session-related errors. This can usually be resolved by explicitly assigning unused ports when starting the Ray head node, for example by setting a non-default `--port` and `--dashboard-port`. + +In more severe cases, Ray job submission may fail with errors indicating that no available agent can accept jobs. This typically happens when the dashboard agent or runtime environment agent ports are also in conflict. In such situations, explicitly specifying the agent-related ports (e.g. `--dashboard-agent-listen-port`, `--dashboard-agent-grpc-port`, and `--runtime-env-agent-port`) when starting Ray can resolve the issue. + +If the TB server cannot connect to the Miles server through the sglang router (`InternalServerError`), check which address is actually listening on the router port (e.g. 30005 in this example) and update the `api_base` in `eval_tb_example.yaml` accordingly: + +```bash +ss -lntp | grep 30005 +``` + +You may see `Parser warnings`, `Context length exceeded`, `Command 1 should end with newline`, `Harness execution failed` in `tb_server.py` logs. They are warnings from Terminal Bench and can be ignored if runs proceed normally. \ No newline at end of file diff --git a/examples/eval/terminal_bench/__init__.py b/examples/eval/terminal_bench/__init__.py new file mode 100644 index 000000000..6d2704250 --- /dev/null +++ b/examples/eval/terminal_bench/__init__.py @@ -0,0 +1 @@ +"""Terminal Bench evaluation helpers.""" diff --git a/examples/eval/terminal_bench/requirements.txt b/examples/eval/terminal_bench/requirements.txt new file mode 100644 index 000000000..1a0006c93 --- /dev/null +++ b/examples/eval/terminal_bench/requirements.txt @@ -0,0 +1,3 @@ +flask +omegaconf +requests diff --git a/examples/eval/terminal_bench/tb_client.py b/examples/eval/terminal_bench/tb_client.py new file mode 100644 index 000000000..2a93b7161 --- /dev/null +++ b/examples/eval/terminal_bench/tb_client.py @@ -0,0 +1,104 @@ +import logging +import time +from typing import Any + +import requests +from examples.eval.eval_delegate import EvalClient, EvalDelegateError +from examples.eval.terminal_bench.tb_config import TerminalBenchConfig + +logger = logging.getLogger(__name__) + + +class TerminalBenchClient(EvalClient): + """HTTP client that proxies evaluation requests to the Terminal Bench server.""" + + def __init__(self, config: TerminalBenchConfig, router_url: str): + super().__init__(config.name or "terminal_bench") + self._config = config + endpoint = (config.url or "").rstrip("/") + if endpoint.endswith("/evaluate"): + base_endpoint = endpoint[: -len("/evaluate")] + else: + base_endpoint = endpoint + self._endpoint = f"{base_endpoint}/evaluate" if base_endpoint else "" + self._status_endpoint = f"{base_endpoint}/status" if base_endpoint else "" + self._timeout_secs = float(config.timeout_secs) + self._max_retries = max(1, int(config.max_retries)) + self._headers = dict(config.headers or {}) + self._session = requests.Session() + + @classmethod + def from_config(cls, config: TerminalBenchConfig, router_url: str): + if not config.url: + return None + return cls(config, router_url) + + def evaluate(self, args, rollout_id: int) -> tuple[dict[str, Any], dict[str, Any]]: + payload = self._build_payload(args, rollout_id) + response = self._request(payload) + metrics = response.get("raw_metrics", {}) + return metrics, response + + def _build_payload(self, args, rollout_id: int) -> dict[str, Any]: + payload = { + "model_name": self._config.model_name, + "api_base": self._config.api_base, + "n_tasks": self._config.n_tasks, + "n_concurrent": self._config.n_concurrent, + "metric_prefix": self._config.name, + } + if self._config.dataset_path: + payload["dataset_path"] = self._config.dataset_path + if self._config.task_ids: + payload["task_ids"] = list(self._config.task_ids) + if self._config.n_attempts is not None: + payload["n_attempts"] = self._config.n_attempts + return payload + + def _request(self, payload: dict[str, Any]) -> dict[str, Any]: + last_error: Exception | None = None + for attempt in range(1, self._max_retries + 1): + try: + response = self._session.post( + self._endpoint, + json=payload, + timeout=self._timeout_secs, + headers=self._headers, + ) + response.raise_for_status() + if not response.content: + return {} + body = response.json() + if body.get("status") == "completed": + return body + job_id = body.get("job_id") + if not job_id: + return body + return self._poll_status(job_id) + except requests.RequestException as exc: + last_error = exc + logger.warning( + "Terminal Bench delegate request failed (attempt %s/%s): %s", attempt, self._max_retries, exc + ) + if attempt < self._max_retries: + time.sleep(min(2**attempt, 30)) + raise EvalDelegateError("Terminal Bench evaluation request failed") from last_error + + def _poll_status(self, job_id: str) -> dict[str, Any]: + status_url = f"{self._status_endpoint}/{job_id}" + deadline = time.time() + self._timeout_secs + while time.time() < deadline: + response = self._session.get(status_url, timeout=min(self._timeout_secs, 30), headers=self._headers) + response.raise_for_status() + if not response.content: + time.sleep(2) + continue + body = response.json() + status = body.get("status") + if status == "completed": + return body + if status == "failed": + error = body.get("error") or "Terminal Bench job failed" + raise EvalDelegateError(error) + time.sleep(2) + raise EvalDelegateError("Terminal Bench evaluation timed out") diff --git a/examples/eval/terminal_bench/tb_config.py b/examples/eval/terminal_bench/tb_config.py new file mode 100644 index 000000000..f57b445dd --- /dev/null +++ b/examples/eval/terminal_bench/tb_config.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any + +from examples.eval.eval_delegate import EvalEnvConfig + + +@dataclass +class TerminalBenchConfig(EvalEnvConfig): + """Environment configuration shared by the Terminal Bench client/server.""" + + model_name: str = "qwen3-8b" + api_base: str = "http://127.0.1.1:30001/v1" + dataset_path: str | None = None + n_tasks: int | None = None + task_ids: list[str] = field(default_factory=list) + n_attempts: int | None = None + n_concurrent: int = 8 + + @classmethod + def parse(cls, args, raw_env_config: Mapping[str, Any], defaults: Mapping[str, Any]) -> TerminalBenchConfig: + clean_raw = dict(raw_env_config or {}) + clean_raw.pop("type", None) + base_cfg: TerminalBenchConfig = super().parse(clean_raw, defaults) + + field_casts = { + "model_name": str, + "api_base": str, + "n_attempts": int, + "n_tasks": int, + "n_concurrent": int, + "dataset_path": str, + } + + for key, caster in field_casts.items(): + value = clean_raw.get(key) + if value is not None: + setattr(base_cfg, key, caster(value)) + + task_ids = clean_raw.get("task_ids") + if isinstance(task_ids, (list, tuple)): + base_cfg.task_ids = [str(item) for item in task_ids if item] + elif task_ids is not None: + raise ValueError("task_ids must be a list") + + return base_cfg + + +def build_terminal_bench_config(args, raw_env_config: Mapping[str, Any], defaults: Mapping[str, Any]): + return TerminalBenchConfig.parse(args, raw_env_config, defaults) diff --git a/examples/eval/terminal_bench/tb_server.py b/examples/eval/terminal_bench/tb_server.py new file mode 100644 index 000000000..58c9d54ad --- /dev/null +++ b/examples/eval/terminal_bench/tb_server.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +""" +Simple HTTP server that proxies Miles evaluation requests to the `tb run` +command shipped with Terminal Bench. + +Usage: + python examples/eval/terminal_bench/tb_server.py \ + --host 0.0.0.0 --port 9050 \ + --output-root /opt/tb-eval + +Miles (or Miles-compatible runners) should POST the payload described in +`EvalRequestPayload` to http://:/evaluate. The server blocks until +`tb run` finishes, then returns aggregated metrics along with paths to the +generated artifacts (logs + raw metrics). +""" + +from __future__ import annotations + +import argparse +import json +import logging +import os +import shlex +import statistics +import subprocess +import sys +import threading +import time +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[3] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from flask import Flask, jsonify, request +from omegaconf import OmegaConf +from omegaconf.errors import OmegaConfBaseException + +logger = logging.getLogger("terminal_bench_server") +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + + +# --------------------------------------------------------------------------- +# Request payload helpers +# --------------------------------------------------------------------------- + + +@dataclass +class EvalRequestPayload: + model_name: str = "" + api_base: str = "" + n_tasks: int | None = None + n_concurrent: int | None = None + dataset_path: str | None = None + task_ids: list[str] | None = None + n_attempts: int | None = None + metric_prefix: str | None = None + + +@dataclass +class JobRecord: + job_id: str + status: str + run_id: str + command: str + output_dir: str + log_path: str + raw_metrics: dict[str, Any] | None = None + error: str | None = None + created_at: float = field(default_factory=time.time) + started_at: float | None = None + finished_at: float | None = None + + def to_dict(self) -> dict[str, Any]: + payload: dict[str, Any] = { + "job_id": self.job_id, + "status": self.status, + "run_id": self.run_id, + "command": self.command, + "output_dir": self.output_dir, + "log_path": self.log_path, + "created_at": self.created_at, + "started_at": self.started_at, + "finished_at": self.finished_at, + } + if self.raw_metrics is not None: + payload["raw_metrics"] = self.raw_metrics + if self.error: + payload["error"] = self.error + return payload + + +# --------------------------------------------------------------------------- +# Configuration + command helpers +# --------------------------------------------------------------------------- + + +def _normalize_model_name(model_name: str) -> str: + name = (model_name or "").strip() + if not name: + return "" + if "/" in name: + return name + return f"openai/{name}" + + +@dataclass +class ServerConfig: + output_root: Path + + @classmethod + def from_args(cls, args: argparse.Namespace) -> ServerConfig: + return cls(output_root=Path(args.output_root).expanduser().resolve()) + + +class TerminalBenchEvaluator: + def __init__(self, config: ServerConfig): + self._config = config + self._lock = threading.Lock() + self._jobs_lock = threading.Lock() + self._jobs: dict[str, JobRecord] = {} + self._config.output_root.mkdir(parents=True, exist_ok=True) + self._log_root = REPO_ROOT.parent / "tb_eval_logs" + self._log_root.mkdir(parents=True, exist_ok=True) + + def evaluate(self, payload: EvalRequestPayload) -> dict[str, Any]: + if not payload.model_name: + raise ValueError("Missing `model_name` in request payload.") + if not payload.api_base: + raise ValueError("Missing `api_base` in request payload.") + + job_id = uuid.uuid4().hex + run_id = f"{int(time.time())}-{job_id[:8]}" + run_dir = self._config.output_root / run_id + + command = self._build_command(payload, run_id) + command_str = " ".join(shlex.quote(part) for part in command) + log_path = self._log_root / f"{run_id}.log" + + record = JobRecord( + job_id=job_id, + status="queued", + run_id=run_id, + command=command_str, + output_dir=str(run_dir), + log_path=str(log_path), + ) + with self._jobs_lock: + self._jobs[job_id] = record + + thread = threading.Thread( + target=self._run_job, + args=(job_id, payload, run_dir, command, log_path), + daemon=True, + ) + thread.start() + + return { + "job_id": job_id, + "status": "queued", + "status_url": f"/status/{job_id}", + "run_id": run_id, + "command": command_str, + "output_dir": str(run_dir), + "log_path": str(log_path), + } + + def _run_job( + self, + job_id: str, + payload: EvalRequestPayload, + run_dir: Path, + command: list[str], + log_path: Path, + ) -> None: + with self._jobs_lock: + record = self._jobs.get(job_id) + if record is None: + return + record.status = "running" + record.started_at = time.time() + + env = self._build_env() + logger.info("Starting Terminal Bench run: %s", " ".join(shlex.quote(part) for part in command)) + try: + with self._lock: + self._run_command(command, env=env, log_path=log_path) + metrics = self._collect_metrics(run_dir) + if payload.metric_prefix: + metrics = {payload.metric_prefix: metrics} + with self._jobs_lock: + record = self._jobs.get(job_id) + if record is None: + return + record.status = "completed" + record.raw_metrics = metrics + record.finished_at = time.time() + except Exception as exc: # noqa: BLE001 + with self._jobs_lock: + record = self._jobs.get(job_id) + if record is None: + return + record.status = "failed" + record.error = str(exc) + record.finished_at = time.time() + + def get_job_status(self, job_id: str) -> dict[str, Any] | None: + with self._jobs_lock: + record = self._jobs.get(job_id) + if record is None: + return None + return record.to_dict() + + def _build_command(self, payload: EvalRequestPayload, run_id: str) -> list[str]: + # 1. Normalize model name (add openai/ prefix) + model_name = _normalize_model_name(payload.model_name) + + cmd = [ + "tb", + "run", + "-a", + "terminus-2", # Added Agent flag + "--output-path", + str(self._config.output_root), + "--run-id", + run_id, + ] + + # 2. Add model + if model_name: + cmd.extend(["--model", model_name]) + + # 3. Add Agent kwargs (Use api_base exactly like the CLI command) + if payload.api_base: + cmd.extend(["--agent-kwarg", f"api_base={payload.api_base}"]) + + if payload.dataset_path: + cmd.extend(["--dataset-path", payload.dataset_path]) + + if payload.n_attempts is not None: + cmd.extend(["--n-attempts", str(payload.n_attempts)]) + + # 4. Add n_tasks if present + task_ids = [] + if payload.task_ids: + task_ids.extend([str(item) for item in payload.task_ids if item]) + if task_ids: + for task_id in task_ids: + cmd.extend(["--task-id", task_id]) + elif payload.n_tasks is not None: + cmd.extend(["--n-tasks", str(payload.n_tasks)]) + + # 5. Add concurrency + n_concurrent = payload.n_concurrent + if n_concurrent is None: + n_concurrent = 1 + cmd.extend(["--n-concurrent", str(n_concurrent)]) + + return cmd + + def _build_env(self) -> dict[str, str]: + env = os.environ.copy() + # Inject env var to simulate "OPENAI_API_KEY=EMPTY" + env["OPENAI_API_KEY"] = "EMPTY" + return env + + @staticmethod + def _run_command(cmd: list[str], *, env: dict[str, str], log_path: Path): + with open(log_path, "w", encoding="utf-8") as log_file: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env, + text=True, + bufsize=1, + ) + assert process.stdout is not None + for line in process.stdout: + log_file.write(line) + log_file.flush() + sys.stdout.write(line) + sys.stdout.flush() + retcode = process.wait() + if retcode != 0: + with open(log_path, encoding="utf-8", errors="ignore") as log_file: + tail = "".join(log_file.readlines()[-200:]) + raise RuntimeError(f"`tb run` failed with exit code {retcode}. See {log_path}\n{tail}") + + @staticmethod + def _collect_metrics(run_dir: Path) -> dict[str, Any]: + metrics_path = run_dir / "results.json" + if not metrics_path.exists(): + logger.warning("Results file missing at %s", metrics_path) + return {} + + metrics = TerminalBenchEvaluator._extract_metrics(metrics_path) + if not metrics: + logger.warning("No accuracy/n_resolved metrics found in %s", metrics_path) + return metrics + + @staticmethod + def _extract_metrics(metrics_path: Path) -> dict[str, Any]: + try: + with open(metrics_path, encoding="utf-8") as fp: + metrics_data = json.load(fp) + except json.JSONDecodeError as exc: + logger.warning("Failed to parse %s: %s", metrics_path, exc) + return {} + + metrics: dict[str, Any] = {} + + # core metrics + accuracy = metrics_data.get("accuracy") + if isinstance(accuracy, (int, float)): + metrics["accuracy"] = float(accuracy) + + n_resolved = metrics_data.get("n_resolved") + if isinstance(n_resolved, (int, float)): + metrics["n_resolved"] = int(n_resolved) + + n_unresolved = metrics_data.get("n_unresolved") + if isinstance(n_unresolved, (int, float)): + metrics["n_unresolved"] = int(n_unresolved) + + # pass@k flatten + pass_at_k = metrics_data.get("pass_at_k") + if isinstance(pass_at_k, dict): + for k, v in pass_at_k.items(): + if isinstance(v, (int, float)): + metrics[f"pass_at_k/{k}"] = float(v) + + # token stats from per-task results + results = metrics_data.get("results") + if isinstance(results, list): + input_tokens = [ + r.get("total_input_tokens") + for r in results + if isinstance(r, dict) and isinstance(r.get("total_input_tokens"), (int, float)) + ] + output_tokens = [ + r.get("total_output_tokens") + for r in results + if isinstance(r, dict) and isinstance(r.get("total_output_tokens"), (int, float)) + ] + + if input_tokens: + metrics["total_input_tokens_mean"] = float(statistics.mean(input_tokens)) + metrics["total_input_tokens_median"] = float(statistics.median(input_tokens)) + if output_tokens: + metrics["total_output_tokens_mean"] = float(statistics.mean(output_tokens)) + metrics["total_output_tokens_median"] = float(statistics.median(output_tokens)) + + return metrics + + +# --------------------------------------------------------------------------- +# HTTP server +# --------------------------------------------------------------------------- + + +def build_app(evaluator: TerminalBenchEvaluator) -> Flask: + app = Flask(__name__) + + @app.get("/health") + def health_check(): + return jsonify({"status": "ok"}) + + @app.post("/evaluate") + def evaluate_endpoint(): + try: + raw_payload = request.get_json(force=True, silent=False) + cfg = OmegaConf.merge( + OmegaConf.structured(EvalRequestPayload), + OmegaConf.create(raw_payload or {}), + ) + payload = OmegaConf.to_object(cfg) + result = evaluator.evaluate(payload) + return jsonify(result) + except OmegaConfBaseException as exc: + logger.exception("Invalid request payload") + return jsonify({"error": str(exc)}), 400 + except Exception as exc: # noqa: BLE001 + logger.exception("Evaluation failed") + return jsonify({"error": str(exc)}), 500 + + @app.get("/status/") + def status_endpoint(job_id: str): + status = evaluator.get_job_status(job_id) + if status is None: + return jsonify({"error": "job not found"}), 404 + return jsonify(status) + + return app + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run the Terminal Bench evaluation HTTP server.") + parser.add_argument("--host", type=str, default="0.0.0.0") + parser.add_argument("--port", type=int, default=9050) + parser.add_argument( + "--output-root", + type=str, + default="./terminal-bench-output", + help="Directory to store `tb run` outputs.", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + config = ServerConfig.from_args(args) + evaluator = TerminalBenchEvaluator(config) + app = build_app(evaluator) + logger.info( + "Starting Terminal Bench evaluation server on %s:%s (output root=%s)", + args.host, + args.port, + config.output_root, + ) + app.run(host=args.host, port=args.port) + + +if __name__ == "__main__": + main() From dfd822cd945498b0d44f4e0a70efcf57e0065517 Mon Sep 17 00:00:00 2001 From: "Ethan (Yusheng) Su" Date: Mon, 19 Jan 2026 12:38:24 -0800 Subject: [PATCH 22/57] [CI] Fix and setup CI (#402) Co-authored-by: Yusheng Su --- .github/workflows/pr-test.yml | 210 +++++++++++++++---- .github/workflows/pr-test.yml.j2 | 30 ++- tests/test_qwen2.5_0.5B_gsm8k_async_short.py | 6 +- tests/test_qwen2.5_0.5B_gsm8k_short.py | 6 +- 4 files changed, 204 insertions(+), 48 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index f00faa5a6..60e37ec5e 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -33,18 +33,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -61,6 +60,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -69,6 +80,15 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + e2e-test-fsdp: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-fsdp')) runs-on: self-hosted @@ -77,18 +97,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -105,6 +124,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -113,6 +144,15 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + e2e-test-megatron: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-megatron')) runs-on: self-hosted @@ -121,18 +161,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -149,6 +188,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -157,6 +208,15 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + e2e-test-precision: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-precision')) runs-on: self-hosted @@ -165,18 +225,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -193,6 +252,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -201,6 +272,15 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + e2e-test-ckpt: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-ckpt')) runs-on: self-hosted @@ -209,18 +289,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -237,6 +316,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -245,6 +336,15 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + e2e-test-long: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-long')) runs-on: self-hosted @@ -253,18 +353,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -281,6 +380,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -289,6 +400,15 @@ jobs: shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + e2e-test-image: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-image')) runs-on: self-hosted @@ -297,18 +417,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -325,6 +444,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -332,3 +463,12 @@ jobs: - name: Execute shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true diff --git a/.github/workflows/pr-test.yml.j2 b/.github/workflows/pr-test.yml.j2 index 25bb2bce2..1d1022837 100644 --- a/.github/workflows/pr-test.yml.j2 +++ b/.github/workflows/pr-test.yml.j2 @@ -102,18 +102,17 @@ jobs: options: > --gpus all --ipc=host - --shm-size=16g + --shm-size=32g --ulimit memlock=-1 --ulimit stack=67108864 --memory=0 --memory-swap=0 - -e http_proxy=$http_proxy - -e https_proxy=$https_proxy - -e HTTP_PROXY=$HTTP_PROXY - -e HTTPS_PROXY=$HTTPS_PROXY -v /mnt/nvme0n1/miles_ci:/data/miles_ci -v /mnt/nvme0n1/miles_ci/models:/root/models -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + --privileged + --ulimit nofile=65535:65535 + -v /tmp:/tmp strategy: fail-fast: false matrix: @@ -130,6 +129,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cleanup Ray processes + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + pkill -9 -f gcs_server 2>/dev/null || true + pkill -9 -f 'ray-dashboard' 2>/dev/null || true + pkill -9 sglang 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true + sleep 3 + - name: Install shell: bash run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages @@ -137,4 +148,13 @@ jobs: - name: Execute shell: bash run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} + + - name: Post-test cleanup + if: always() + shell: bash + run: | + pkill -9 -f 'ray::' 2>/dev/null || true + pkill -9 -f raylet 2>/dev/null || true + ray stop --force 2>/dev/null || true + rm -rf /tmp/ray/* 2>/dev/null || true <% endfor %> \ No newline at end of file diff --git a/tests/test_qwen2.5_0.5B_gsm8k_async_short.py b/tests/test_qwen2.5_0.5B_gsm8k_async_short.py index d55262cd0..90cd15cb6 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_async_short.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_async_short.py @@ -123,8 +123,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() diff --git a/tests/test_qwen2.5_0.5B_gsm8k_short.py b/tests/test_qwen2.5_0.5B_gsm8k_short.py index afbffbc56..867fdcad6 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_short.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_short.py @@ -122,8 +122,6 @@ def execute(): if __name__ == "__main__": prepare() - os.environ.pop("http_proxy") - os.environ.pop("https_proxy") - os.environ.pop("HTTP_PROXY") - os.environ.pop("HTTPS_PROXY") + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) execute() From 38c152fe2d224e7cba220097104f0ba97f652c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=99=A8=E9=98=B3?= Date: Mon, 19 Jan 2026 18:16:09 -0800 Subject: [PATCH 23/57] [CI] R3 bug fix & add CI test for R3 (#496) --- .github/workflows/pr-test.yml | 23 +++- .github/workflows/pr-test.yml.j2 | 8 +- miles/backends/megatron_utils/actor.py | 2 +- miles/backends/training_utils/ci_utils.py | 2 +- tests/test_moonlight_16B_A3B_r3.py | 125 ++++++++++++++++++ tests/test_qwen3_30B_A3B_r3.py | 151 ++++++++++++++++++++++ 6 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 tests/test_moonlight_16B_A3B_r3.py create mode 100644 tests/test_qwen3_30B_A3B_r3.py diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 60e37ec5e..d34c823aa 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -55,6 +55,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository @@ -119,6 +122,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository @@ -175,7 +181,7 @@ jobs: strategy: fail-fast: false matrix: - info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}] + info: [{"num_gpus": 8, "test_file": "test_quick_start_glm4_9B.py"}, {"num_gpus": 8, "test_file": "test_qwen3_30B_A3B.py", "use_deepep": "1", "use_fp8_rollout": "1"}, {"enable_eval": "0", "num_gpus": 8, "test_file": "test_qwen3_30B_A3B_r3.py", "use_deepep": "1", "use_fp8_rollout": "1"}, {"enable_eval": "0", "num_gpus": 8, "test_file": "test_qwen3_30B_A3B_r3.py"}, {"num_gpus": 8, "test_file": "test_qwen3_4B_ppo.py"}, {"num_gpus": 8, "test_file": "test_moonlight_16B_A3B.py"}, {"enable_eval": "0", "num_gpus": 8, "test_file": "test_moonlight_16B_A3B_r3.py"}, {"num_gpus": 8, "test_file": "test_mimo_7B_mtp_only_grad.py"}] defaults: run: working-directory: ${{ github.workspace }} @@ -183,6 +189,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository @@ -247,6 +256,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository @@ -311,6 +323,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository @@ -375,6 +390,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository @@ -439,6 +457,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository diff --git a/.github/workflows/pr-test.yml.j2 b/.github/workflows/pr-test.yml.j2 index 1d1022837..37b6fa446 100644 --- a/.github/workflows/pr-test.yml.j2 +++ b/.github/workflows/pr-test.yml.j2 @@ -20,9 +20,12 @@ 'label': 'run-ci-megatron', 'tests': [ {'test_file': 'test_quick_start_glm4_9B.py', 'num_gpus': 8}, - {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_qwen3_30B_A3B.py', 'num_gpus': 8, 'use_deepep': '1', 'use_fp8_rollout': '1'}, + {'test_file': 'test_qwen3_30B_A3B_r3.py', 'num_gpus': 8, 'use_deepep': '1', 'use_fp8_rollout': '1', 'enable_eval': '0'}, + {'test_file': 'test_qwen3_30B_A3B_r3.py', 'num_gpus': 8, 'enable_eval': '0'}, {'test_file': 'test_qwen3_4B_ppo.py', 'num_gpus': 8}, {'test_file': 'test_moonlight_16B_A3B.py', 'num_gpus': 8}, + {'test_file': 'test_moonlight_16B_A3B_r3.py', 'num_gpus': 8, 'enable_eval': '0'}, {'test_file': 'test_mimo_7B_mtp_only_grad.py', 'num_gpus': 8}, ], }, @@ -124,6 +127,9 @@ jobs: GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + MILES_TEST_USE_DEEPEP: ${{ matrix.info.use_deepep || '0' }} + MILES_TEST_USE_FP8_ROLLOUT: ${{ matrix.info.use_fp8_rollout || '0' }} + MILES_TEST_ENABLE_EVAL: ${{ matrix.info.enable_eval || '1' }} steps: - name: Checkout repository diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index f19616487..a92198a67 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -228,7 +228,7 @@ def pad_func(experts, pad): # TODO: maybe extract a common process function for here and get_batch? rollout_routed_experts = [slice_with_cp(r, pad_func, self.parallel_state) for r in rollout_routed_experts] rollout_routed_experts = torch.cat(rollout_routed_experts, dim=0) - pad_size = self.parallel_state.tp_size * self.args.data_pad_size_multiplier + pad_size = self.parallel_state.dp_size * self.args.data_pad_size_multiplier pad = (pad_size - rollout_routed_experts.size(0) % pad_size) % pad_size if pad != 0: rollout_routed_experts = pad_func(rollout_routed_experts, pad) diff --git a/miles/backends/training_utils/ci_utils.py b/miles/backends/training_utils/ci_utils.py index ee5563f14..0080afba4 100644 --- a/miles/backends/training_utils/ci_utils.py +++ b/miles/backends/training_utils/ci_utils.py @@ -16,7 +16,7 @@ def check_kl(args: Namespace, log_dict: dict[str, float], step_id: int, accumula assert log_dict["train/ppo_kl"] < 1e-8, f"{log_dict=}" else: assert log_dict["train/ppo_kl"] == 0.0 and log_dict["train/pg_clipfrac"] == 0.0, f"{log_dict=}" - if accumulated_step_id == 0 and "train/kl_loss" in log_dict: + if accumulated_step_id == 0 and "train/kl_loss" in log_dict and not args.use_rollout_routing_replay: assert log_dict["train/kl_loss"] == 0.0, f"{log_dict=}" diff --git a/tests/test_moonlight_16B_A3B_r3.py b/tests/test_moonlight_16B_A3B_r3.py new file mode 100644 index 000000000..cdb898c19 --- /dev/null +++ b/tests/test_moonlight_16B_A3B_r3.py @@ -0,0 +1,125 @@ +import os +import miles.utils.external_utils.command_utils as U + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) + +MODEL_NAME = "Moonlight-16B-A3B-Instruct" +MODEL_TYPE = "moonlight" +NUM_GPUS = 8 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command( + "hf download moonshotai/Moonlight-16B-A3B-Instruct --local-dir /root/models/Moonlight-16B-A3B-Instruct" + ) + U.hf_download_dataset("zhuzilin/dapo-math-17k") + U.hf_download_dataset("zhuzilin/aime-2024") + + U.convert_checkpoint(model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS) + + +def execute(): + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " f"--ref-load /root/{MODEL_NAME}_torch_dist " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 4096 " + "--rollout-temperature 1 " + "--global-batch-size 32 " + ) + + eval_args = ( + f"{'--eval-interval 20 ' if ENABLE_EVAL else ''}" + "--eval-prompt-data aime /root/datasets/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 4096 " + "--eval-top-k 1 " + ) + + perf_args = ( + "--tensor-model-parallel-size 2 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 2 " + "--expert-model-parallel-size 8 " + "--expert-tensor-parallel-size 1 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + f"--max-tokens-per-gpu {2048 if TIGHT_HOST_MEMORY else 2048} " + ) + + grpo_args = ( + "--advantage-estimator gspo " + f"{'' if TIGHT_HOST_MEMORY else '--use-kl-loss '}" + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 4e-4 " + "--use-rollout-routing-replay " + "--use-miles-router " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 2 " "--sglang-mem-fraction-static 0.8 " "--sglang-max-running-requests 512 " + ) + + ci_args = "--ci-test " + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 8 " + "--colocate " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + prepare() + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) + execute() diff --git a/tests/test_qwen3_30B_A3B_r3.py b/tests/test_qwen3_30B_A3B_r3.py new file mode 100644 index 000000000..5a5b968aa --- /dev/null +++ b/tests/test_qwen3_30B_A3B_r3.py @@ -0,0 +1,151 @@ +import os + +import miles.utils.external_utils.command_utils as U + + +ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) +TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) +USE_DEEPEP = bool(int(os.environ.get("MILES_TEST_USE_DEEPEP", "1"))) +USE_FP8_ROLLOUT = bool(int(os.environ.get("MILES_TEST_USE_FP8_ROLLOUT", "1"))) + +MODEL_NAME = "Qwen3-30B-A3B" +MODEL_TYPE = "qwen3-30B-A3B" +NUM_GPUS = 8 + + +def prepare(): + U.exec_command("mkdir -p /root/models /root/datasets") + U.exec_command("hf download Qwen/Qwen3-30B-A3B --local-dir /root/models/Qwen3-30B-A3B") + U.exec_command("hf download Qwen/Qwen3-30B-A3B-FP8 --local-dir /root/models/Qwen3-30B-A3B-FP8") + U.hf_download_dataset("zhuzilin/dapo-math-17k") + U.hf_download_dataset("zhuzilin/aime-2024") + + U.convert_checkpoint(model_name=MODEL_NAME, megatron_model_type=MODEL_TYPE, num_gpus_per_node=NUM_GPUS) + + +def execute(): + if USE_FP8_ROLLOUT: + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-FP8 " f"--ref-load /root/{MODEL_NAME}_torch_dist " + else: + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " f"--ref-load /root/{MODEL_NAME}_torch_dist " + + rollout_args = ( + "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + "--num-rollout 3 " + "--rollout-batch-size 8 " + "--n-samples-per-prompt 8 " + "--rollout-max-response-len 8192 " + "--rollout-temperature 1 " + "--global-batch-size 32 " + "--balance-data " + ) + + eval_args = ( + f"{'--eval-interval 20 ' if ENABLE_EVAL else ''}" + "--eval-prompt-data aime24 /root/datasets/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 16384 " + "--eval-top-k 1 " + ) + + perf_args = ( + "--tensor-model-parallel-size 4 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 2 " + "--expert-model-parallel-size 8 " + "--expert-tensor-parallel-size 1 " + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + f"--max-tokens-per-gpu {2048 if TIGHT_HOST_MEMORY else 16384} " + ) + + grpo_args = ( + "--advantage-estimator gspo " + f"{'' if TIGHT_HOST_MEMORY else '--use-kl-loss '}" + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--kl-coef 0.00 " + "--entropy-coef 0.00 " + "--eps-clip 4e-4 " + "--use-tis " + "--use-rollout-routing-replay " + "--use-miles-router " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + "--optimizer-cpu-offload " + "--overlap-cpu-optimizer-d2h-h2d " + "--use-precision-aware-optimizer " + ) + + sglang_args = ( + "--rollout-num-gpus-per-engine 8 " + "--sglang-mem-fraction-static 0.8 " + "--sglang-max-running-requests 512 " + "--sglang-enable-metrics " + ) + + if USE_DEEPEP: + sglang_args += "--sglang-moe-a2a-backend deepep --sglang-deepep-mode auto " + + ci_args = "--ci-test " + + misc_args = ( + # default dropout in megatron is 0.1 + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + # should be good for model performance + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + # need to comment this when using model with MLA + "--attention-backend flash " + "--actor-num-nodes 1 " + "--actor-num-gpus-per-node 8 " + "--colocate " + ) + + if USE_DEEPEP: + misc_args += "--moe-token-dispatcher-type flex --moe-enable-deepep " + else: + misc_args += "--moe-token-dispatcher-type alltoall " + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{ci_args} " + f"{misc_args} " + ) + + U.execute_train( + train_args=train_args, + num_gpus_per_node=NUM_GPUS, + megatron_model_type=MODEL_TYPE, + ) + + +if __name__ == "__main__": + # TODO also use typer + prepare() + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) + execute() From fc1076f348a49bce83f5f16db866e512a01f673f Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 7 Dec 2025 21:51:50 -0600 Subject: [PATCH 24/57] first attempt in supporting deepseek v3.2 --- .../megatron_to_hf/deepseekv32.py | 124 +++++++++++ miles/utils/data.py | 14 ++ miles/utils/deepseek_v32_patch.py | 50 +++++ miles_plugins/mbridge/__init__.py | 24 +- miles_plugins/mbridge/deepseekv32.py | 51 +++++ scripts/run_deepseek_v3.2_5layer.py | 206 ++++++++++++++++++ scripts/train_dsv32.py | 122 +++++++++++ 7 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py create mode 100644 miles/utils/deepseek_v32_patch.py create mode 100644 miles_plugins/mbridge/deepseekv32.py create mode 100644 scripts/run_deepseek_v3.2_5layer.py create mode 100644 scripts/train_dsv32.py diff --git a/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py b/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py new file mode 100644 index 000000000..b271e5a1e --- /dev/null +++ b/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py @@ -0,0 +1,124 @@ +import re + +import sglang +import torch +from packaging.version import parse + + +def convert_deepseekv3_2_to_hf(args, name, param): + if name == "module.module.embedding.word_embeddings.weight": + return [("model.embed_tokens.weight", param)] + if name == "module.module.output_layer.weight": + return [("lm_head.weight", param)] + if name == "module.module.decoder.final_layernorm.weight": + return [("model.norm.weight", param)] + + try: + head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads + except AttributeError: + head_dim = args.hidden_size // args.num_attention_heads + value_num_per_group = args.num_attention_heads // args.num_query_groups + + decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)" + match = re.match(decoder_layers_pattern, name) + if match: + layer_idx, rest = match.groups() + + # experts + expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)" + match = re.match(expert_pattern, rest) + if match: + rest, expert_idx = match.groups() + if rest == "linear_fc1": + gate_weight, up_weight = param.chunk(2, dim=0) + outputs = [ + (f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.gate_proj.weight", gate_weight), + (f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.up_proj.weight", up_weight), + ] + return outputs + elif rest == "linear_fc2": + outputs = [ + (f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.weight", param), + ] + if parse(sglang.__version__) < parse("0.4.9.post5") and args.sglang_enable_ep_moe: + outputs += [ + ( + f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.input_scale", + torch.tensor(1.0, dtype=torch.float32, device=param.device), + ), + ( + f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.weight_scale", + torch.tensor(1.0, dtype=torch.float32, device=param.device), + ), + ] + return outputs + else: + raise ValueError(f"Unknown expert parameter name: {name}") + + # shared expert + shared_expert_pattern = r"mlp.shared_experts\.(.+)" + match = re.match(shared_expert_pattern, rest) + if match: + rest = match.groups()[0] + if rest == "linear_fc1.weight": + gate_weight, up_weight = param.chunk(2, dim=0) + return [ + (f"model.layers.{layer_idx}.mlp.shared_experts.gate_proj.weight", gate_weight), + (f"model.layers.{layer_idx}.mlp.shared_experts.up_proj.weight", up_weight), + ] + elif rest == "linear_fc2.weight": + return [(f"model.layers.{layer_idx}.mlp.shared_experts.down_proj.weight", param)] + else: + raise ValueError(f"Unknown shared expert parameter name: {name}") + + if rest == "self_attention.linear_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)] + elif rest == "self_attention.linear_q_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.q_proj.weight", param)] + elif rest == "self_attention.linear_q_down_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.q_a_proj.weight", param)] + elif rest == "self_attention.linear_q_up_proj.layer_norm_weight": + return [(f"model.layers.{layer_idx}.self_attn.q_a_layernorm.weight", param)] + elif rest == "self_attention.linear_q_up_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.q_b_proj.weight", param)] + elif rest == "self_attention.linear_qkv.bias": + param = param.view(args.num_query_groups, -1) + q_bias, k_bias, v_bias = torch.split( + param, + split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim], + dim=1, + ) + q_bias = q_bias.contiguous().flatten() + k_bias = k_bias.contiguous().flatten() + v_bias = v_bias.contiguous().flatten() + return [ + (f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias), + (f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias), + (f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias), + ] + elif rest == "mlp.linear_fc1.weight": + gate_weight, up_weight = param.chunk(2, dim=0) + return [ + (f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight), + (f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight), + ] + elif rest == "mlp.linear_fc2.weight": + return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)] + elif rest == "self_attention.linear_qkv.layer_norm_weight" or rest == "input_layernorm.weight": + return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)] + elif rest == "mlp.linear_fc1.layer_norm_weight": + return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)] + elif rest == "self_attention.linear_kv_down_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.kv_a_proj_with_mqa.weight", param)] + elif rest == "self_attention.linear_kv_up_proj.layer_norm_weight": + return [(f"model.layers.{layer_idx}.self_attn.kv_a_layernorm.weight", param)] + elif rest == "self_attention.linear_kv_up_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.kv_b_proj.weight", param)] + elif rest == "pre_mlp_layernorm.weight": + return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)] + elif rest == "mlp.router.weight": + return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)] + elif rest == "mlp.router.expert_bias": + return [(f"model.layers.{layer_idx}.mlp.gate.e_score_correction_bias", param)] + + raise ValueError(f"Unknown parameter name: {name}") diff --git a/miles/utils/data.py b/miles/utils/data.py index 6e64ef678..737246acd 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -207,6 +207,20 @@ def __init__( add_generation_prompt=True, **(apply_chat_template_kwargs or {}), ) + ### DSV32 + try: + prompt = tokenizer.apply_chat_template( + prompt, + tools, + tokenize=False, + add_generation_prompt=True, + **apply_chat_template_kwargs, + ) + except Exception as e: + from sglang.srt.entrypoints.openai.encoding_dsv32 import encode_messages + encode_config = dict(thinking_mode="thinking", drop_thinking=True, add_default_bos_token=True) + prompt = encode_messages(prompt, **encode_config) + ### DSV32 else: output_prompt = prompt diff --git a/miles/utils/deepseek_v32_patch.py b/miles/utils/deepseek_v32_patch.py new file mode 100644 index 000000000..940090706 --- /dev/null +++ b/miles/utils/deepseek_v32_patch.py @@ -0,0 +1,50 @@ +import os +import json +import tempfile +from transformers import AutoConfig + +_patched = False + + +def apply_deepseek_v32_patch(restore_model_type=False): + global _patched + + if _patched: + return + + _original_from_pretrained = AutoConfig.from_pretrained + + def _patched_from_pretrained(pretrained_model_name_or_path, *args, **kwargs): + if isinstance(pretrained_model_name_or_path, str) and os.path.isdir(pretrained_model_name_or_path): + config_file = os.path.join(pretrained_model_name_or_path, "config.json") + if os.path.exists(config_file): + try: + with open(config_file, "r") as f: + config_json = json.load(f) + + if config_json.get("model_type") == "deepseek_v32": + config_json["model_type"] = "deepseek_v3" + if "architectures" in config_json: + config_json["architectures"] = ["DeepseekV3ForCausalLM"] + + tmp_path = os.path.join(tempfile.gettempdir(), "_tmp_config_folder") + os.makedirs(tmp_path, exist_ok=True) + unique_path = os.path.join(tmp_path, f"deepseek_v32_{os.getpid()}.json") + + with open(unique_path, "w") as f: + json.dump(config_json, f) + + config = _original_from_pretrained(unique_path, *args, **kwargs) + + if restore_model_type: + object.__setattr__(config, "model_type", "deepseek_v32") + + return config + except Exception: + pass + + return _original_from_pretrained(pretrained_model_name_or_path, *args, **kwargs) + + AutoConfig.from_pretrained = _patched_from_pretrained + _patched = True + diff --git a/miles_plugins/mbridge/__init__.py b/miles_plugins/mbridge/__init__.py index f97c7f46e..0e259d6b0 100644 --- a/miles_plugins/mbridge/__init__.py +++ b/miles_plugins/mbridge/__init__.py @@ -1,6 +1,28 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) + +from miles.utils.deepseek_v32_patch import apply_deepseek_v32_patch +apply_deepseek_v32_patch(restore_model_type=True) + +from .deepseekv32 import DeepseekV32Bridge from .glm4 import GLM4Bridge from .glm4moe import GLM4MoEBridge from .mimo import MimoBridge from .qwen3_next import Qwen3NextBridge -__all__ = ["GLM4Bridge", "GLM4MoEBridge", "Qwen3NextBridge", "MimoBridge"] +__all__ = ["DeepseekV32Bridge", "GLM4Bridge", "GLM4MoEBridge", "Qwen3NextBridge", "MimoBridge"] + +from mbridge import AutoBridge + +_original_from_config = AutoBridge.from_config + +@classmethod +def _patched_from_config(cls, hf_config, **kwargs): + if hf_config.model_type == "deepseek_v32": + from mbridge.core.bridge import _MODEL_REGISTRY + return _MODEL_REGISTRY['deepseek_v32'](hf_config, **kwargs) + + return _original_from_config(hf_config, **kwargs) + +AutoBridge.from_config = _patched_from_config diff --git a/miles_plugins/mbridge/deepseekv32.py b/miles_plugins/mbridge/deepseekv32.py new file mode 100644 index 000000000..8bc94dd23 --- /dev/null +++ b/miles_plugins/mbridge/deepseekv32.py @@ -0,0 +1,51 @@ +from mbridge.core import register_model +from mbridge.models import DeepseekV3Bridge + + +@register_model("deepseek_v32") +class DeepseekV32Bridge(DeepseekV3Bridge): + + _ATTENTION_MAPPING = ( + DeepseekV3Bridge._ATTENTION_MAPPING.copy() + ) + + # Because the indexer needs the norm output, we cannot use the fused transformer engine impl and have to compute it separately. + if "self_attention.linear_q_up_proj.layer_norm_weight" in _ATTENTION_MAPPING: + del _ATTENTION_MAPPING["self_attention.linear_q_up_proj.layer_norm_weight"] + if "self_attention.linear_kv_up_proj.layer_norm_weight" in _ATTENTION_MAPPING: + del _ATTENTION_MAPPING["self_attention.linear_kv_up_proj.layer_norm_weight"] + + _ATTENTION_MAPPING.update({ + "self_attention.q_layernorm.weight": [ + "model.layers.{layer_number}.self_attn.q_a_layernorm.weight" + ], + "self_attention.kv_layernorm.weight": [ + "model.layers.{layer_number}.self_attn.kv_a_layernorm.weight" + ], + "self_attention.core_attention.indexer.linear_wq_b.weight": [ + "model.layers.{layer_number}.self_attn.indexer.wq_b.weight" + ], + "self_attention.core_attention.indexer.linear_wk.weight": [ + "model.layers.{layer_number}.self_attn.indexer.wk.weight" + ], + "self_attention.core_attention.indexer.k_norm.weight": [ + "model.layers.{layer_number}.self_attn.indexer.k_norm.weight" + ], + "self_attention.core_attention.indexer.k_norm.bias": [ + "model.layers.{layer_number}.self_attn.indexer.k_norm.bias" + ], + "self_attention.core_attention.indexer.linear_weights_proj.weight": [ + "model.layers.{layer_number}.self_attn.indexer.weights_proj.weight" + ], + }) + + def _build_config(self): + config = super()._build_config() + + config.experimental_attention_variant = "dsa" + config.dsa_indexer_n_heads = getattr(self.hf_config, 'dsa_indexer_n_heads', 64) + config.dsa_indexer_head_dim = getattr(self.hf_config, 'dsa_indexer_head_dim', 128) + config.dsa_indexer_topk = getattr(self.hf_config, 'dsa_indexer_topk', 2048) + + return config + diff --git a/scripts/run_deepseek_v3.2_5layer.py b/scripts/run_deepseek_v3.2_5layer.py new file mode 100644 index 000000000..0923e11c6 --- /dev/null +++ b/scripts/run_deepseek_v3.2_5layer.py @@ -0,0 +1,206 @@ +import re +from dataclasses import dataclass +from typing import Literal + +import typer + +import miles.utils.external_utils.command_utils as U + +app = typer.Typer() + + +@dataclass +class ScriptArgs(U.ExecuteTrainConfig): + run_id: str = U.create_run_id() + hf_checkpoint: str = "/root/.cache/dsv32-ckpt/DeepSeek-V3.2-5layer" + torch_dist_checkpoint: str = "/root/.cache/dsv32-ckpt/DeepSeek-V3-0324-5layer_torch_dist" + num_gpus_per_node: int = 8 + enable_eval: bool = False + enable_deepep: bool = False + extra_args: str = "" + task: Literal["dapo_aime", "gsm8k"] = "dapo_aime" + mode: Literal["normal", "debug_minimal"] = "debug_minimal" + + +@app.command() +@U.dataclass_cli +def train(args: ScriptArgs): + load_save_path = f"/root/shared_data/{args.run_id}/checkpoints" + ckpt_args = ( + f"--hf-checkpoint {args.hf_checkpoint} " + f"--ref-load {args.torch_dist_checkpoint} " + f"--load {load_save_path} " + f"--save {load_save_path} " + "--save-interval 20 " + "--save-retain-interval 20 " + ) + + rollout_args = ( + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type math " + "--num-rollout 3000 " + "--rollout-batch-size 128 " + "--n-samples-per-prompt 8 " + "--rollout-temperature 0.8 " + "--num-steps-per-rollout 4 " + "--balance-data " + ) + + if args.mode != "debug_minimal": + rollout_args += ( + "--over-sampling-batch-size 256 " + "--dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std " + ) + + eval_args = "" + if (args.mode != "debug_minimal") and args.enable_eval: + eval_args += "--eval-interval 20 " "--eval-top-p 0.7 " + + match args.task: + case "dapo_aime": + rollout_args += ( + "--prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + f"--rollout-max-response-len {100 if args.mode == 'debug_minimal' else 32768} " + ) + eval_args += ( + "--eval-prompt-data aime /root/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 8 " + "--eval-max-response-len 32768 " + ) + case "gsm8k": + rollout_args += ( + "--prompt-data /root/gsm8k/train.parquet " + "--input-key messages " + "--rollout-max-response-len 256 " + ) + eval_args += ( + "--eval-prompt-data gsm8k /root/gsm8k/test.parquet " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 256 " + ) + + if args.num_nodes <= 2: + perf_args = ( + "--tensor-model-parallel-size 1 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 8 " + "--expert-model-parallel-size 8 " + "--expert-tensor-parallel-size 1 " + ) + elif args.num_nodes <= 4: + perf_args = ( + "--tensor-model-parallel-size 4 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 8 " + "--expert-model-parallel-size 8 " + "--expert-tensor-parallel-size 1 " + ) + else: + perf_args = ( + "--tensor-model-parallel-size 4 " + "--sequence-parallel " + "--pipeline-model-parallel-size 1 " + "--context-parallel-size 8 " + "--expert-model-parallel-size 16 " + "--expert-tensor-parallel-size 1 " + ) + perf_args += ( + "--recompute-granularity full " + "--recompute-method uniform " + "--recompute-num-layers 1 " + "--use-dynamic-batch-size " + "--max-tokens-per-gpu 2048 " + ) + + grpo_args = ( + "--advantage-estimator grpo " + "--kl-loss-coef 0.00 " + "--kl-loss-type low_var_kl " + "--entropy-coef 0.00 " + "--eps-clip 0.2 " + "--eps-clip-high 0.28 " + ) + + optimizer_args = ( + "--optimizer adam " + "--lr 1e-6 " + "--lr-decay-style constant " + "--weight-decay 0.1 " + "--adam-beta1 0.9 " + "--adam-beta2 0.98 " + ) + + sglang_decode_max_bs = 256 + sglang_world_size = 8 if args.num_nodes <= 4 else 64 + sglang_attn_dp_size = 1 if args.num_nodes <= 4 else 8 + sglang_attn_tp_size = sglang_world_size // sglang_attn_dp_size + sglang_args = ( + f"--rollout-num-gpus-per-engine {sglang_world_size} " + "--sglang-mem-fraction-static 0.7 " + # f"--sglang-tp-size {sglang_world_size} " + f"--sglang-tp-size 1 " + f"--sglang-ep-size {sglang_world_size} " + "--sglang-enable-dp-attention " + f"--sglang-dp-size {sglang_attn_dp_size} " + "--sglang-moe-dense-tp-size 1 " + "--sglang-enable-dp-lm-head " + "--sglang-server-concurrency 1024 " + f"--sglang-max-running-requests {sglang_world_size * sglang_decode_max_bs // sglang_attn_tp_size} " + f"--sglang-chunked-prefill-size {sglang_world_size * sglang_decode_max_bs} " + f"--sglang-cuda-graph-max-bs {sglang_decode_max_bs} " + ) + if args.enable_deepep: + sglang_args += ( + "--sglang-moe-a2a-backend deepep " + "--sglang-deepep-mode low_latency " + ) + sglang_extra_env_vars = {} + if args.enable_deepep: + sglang_extra_env_vars["SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK"] = f"{sglang_decode_max_bs}" + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + f"--update-weight-buffer-size {4 * 1024 ** 3} " + f"--actor-num-nodes {args.num_nodes} " + f"--actor-num-gpus-per-node {args.num_gpus_per_node} " + f"--num-gpus-per-node {args.num_gpus_per_node} " + "--colocate " + "--use-fault-tolerance " + f"--dump-details /root/shared_data/{args.run_id}/dump_details " + "--disable-weights-backuper " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{U.get_default_wandb_args(__file__, run_id=args.run_id)} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{misc_args} " + f"{args.extra_args} " + ) + + U.execute_train( + train_args=train_args, + train_script="scripts/train_dsv32.py", + config=args, + num_gpus_per_node=args.num_gpus_per_node, + megatron_model_type="deepseek-v32-5layer", + extra_env_vars={**sglang_extra_env_vars}, + ) + + +if __name__ == "__main__": + app() + diff --git a/scripts/train_dsv32.py b/scripts/train_dsv32.py new file mode 100644 index 000000000..4216ec4be --- /dev/null +++ b/scripts/train_dsv32.py @@ -0,0 +1,122 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from miles.utils.deepseek_v32_patch import apply_deepseek_v32_patch +apply_deepseek_v32_patch() + +from turtle import mode +import ray +from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_WEIGHTS +from typing import Optional + +try: + from sglang.srt.constants import GPU_MEMORY_TYPE_CUDA_GRAPH +except ImportError: + GPU_MEMORY_TYPE_CUDA_GRAPH = None + +from miles.ray.placement_group import create_placement_groups, create_rollout_manager, create_training_models +from miles.utils.arguments import parse_args +from miles.utils.logging_utils import configure_logger +from miles.utils.tracking_utils import init_tracking + + +def train(args): + configure_logger() + # allocate the GPUs + pgs = create_placement_groups(args) + init_tracking(args) + + # create the rollout manager, with sglang engines inside. + # need to initialize rollout manager first to calculate num_rollout + rollout_manager, num_rollout_per_epoch = create_rollout_manager(args, pgs["rollout"]) + + # create the actor and critic models + actor_model, critic_model = create_training_models(args, pgs, rollout_manager) + + if args.offload_rollout: + ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) + + # always update weight first so that sglang has the loaded weights from training. + print("[DEBUG] train.py first update weights") + actor_model.update_weights() + + if args.check_weight_update_equal: + ray.get(rollout_manager.check_weights.remote(action="compare")) + + if args.offload_rollout: + if GPU_MEMORY_TYPE_CUDA_GRAPH is not None: + ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_CUDA_GRAPH])) + ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_KV_CACHE])) + + # special case for eval-only + if args.num_rollout == 0 and args.eval_interval is not None: + ray.get(rollout_manager.eval.remote(rollout_id=0)) + + def offload_train(): + if args.offload_train: + if args.use_critic: + critic_model.offload() + if rollout_id >= args.num_critic_only_steps: + actor_model.offload() + else: + actor_model.offload() + else: + actor_model.clear_memory() + + def onload_rollout(): + if args.offload_rollout: + ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) + + # train loop. + # note that for async training, one can change the position of the sync operation(ray.get). + for rollout_id in range(args.start_rollout_id, args.num_rollout): + # TODO extract the duplicated eval logic + if args.eval_interval is not None and rollout_id == 0: + ray.get(rollout_manager.eval.remote(rollout_id)) + + rollout_data_ref = ray.get(rollout_manager.generate.remote(rollout_id)) + + if args.offload_rollout: + ray.get(rollout_manager.offload.remote()) + + if args.use_critic: + critic_train_handle = critic_model.async_train(rollout_id, rollout_data_ref) + if rollout_id >= args.num_critic_only_steps: + ray.get(actor_model.async_train(rollout_id, rollout_data_ref)) + ray.get(critic_train_handle) + else: + ray.get(actor_model.async_train(rollout_id, rollout_data_ref)) + + if args.save_interval is not None and ( + (rollout_id + 1) % args.save_interval == 0 + or (num_rollout_per_epoch is not None and (rollout_id + 1) % num_rollout_per_epoch == 0) + ): + if (not args.use_critic) or (rollout_id >= args.num_critic_only_steps): + actor_model.save_model(rollout_id) + if args.use_critic: + critic_model.save_model(rollout_id) + if args.rollout_global_dataset: + ray.get(rollout_manager.save.remote(rollout_id)) + + offload_train() + onload_rollout() + actor_model.update_weights() + + if args.offload_rollout: + if GPU_MEMORY_TYPE_CUDA_GRAPH is not None: + ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_CUDA_GRAPH])) + ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_KV_CACHE])) + + if args.eval_interval is not None and ( + (rollout_id + 1) % args.eval_interval == 0 + or (num_rollout_per_epoch is not None and (rollout_id + 1) % num_rollout_per_epoch == 0) + ): + ray.get(rollout_manager.eval.remote(rollout_id)) + + ray.get(rollout_manager.dispose.remote()) + + +if __name__ == "__main__": + args = parse_args() + train(args) From a7373e7a441dd47abb8c869ed77ae74ace052dc9 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Wed, 10 Dec 2025 11:32:39 -0800 Subject: [PATCH 25/57] update --- miles/backends/megatron_utils/actor.py | 3 +++ .../megatron_utils/megatron_to_hf/__init__.py | 3 +++ .../megatron_to_hf/deepseekv32.py | 17 ++++++++++++++--- miles_plugins/mbridge/deepseekv32.py | 3 +++ scripts/run_deepseek_v3.2_5layer.py | 12 +++++++----- scripts/train_dsv32.py | 1 - 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index a92198a67..a12d1be4c 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -13,6 +13,9 @@ from torch_memory_saver import torch_memory_saver from transformers import AutoConfig, AutoTokenizer +from miles.utils.deepseek_v32_patch import apply_deepseek_v32_patch +apply_deepseek_v32_patch() + from miles.ray.train_actor import TrainRayActor from miles.utils import train_dump_utils from miles.utils.context_utils import with_defer diff --git a/miles/backends/megatron_utils/megatron_to_hf/__init__.py b/miles/backends/megatron_utils/megatron_to_hf/__init__.py index 84ff899aa..b9b394cbc 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/__init__.py +++ b/miles/backends/megatron_utils/megatron_to_hf/__init__.py @@ -1,4 +1,5 @@ from .deepseekv3 import convert_deepseekv3_to_hf +from .deepseekv32 import convert_deepseekv32_to_hf from .glm4 import convert_glm4_to_hf from .glm4moe import convert_glm4moe_to_hf from .llama import convert_llama_to_hf @@ -41,6 +42,8 @@ def _convert_to_hf_core(args, model_name, name, param): converted_named_tensors = convert_qwen3_next_to_hf(args, name, param) elif "qwen2" in model_name or "qwen3" in model_name: converted_named_tensors = convert_qwen2_to_hf(args, name, param) + elif "deepseekv32" in model_name: + converted_named_tensors = convert_deepseekv32_to_hf(args, name, param) elif "deepseekv3" in model_name: converted_named_tensors = convert_deepseekv3_to_hf(args, name, param) diff --git a/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py b/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py index b271e5a1e..3b6519ada 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py +++ b/miles/backends/megatron_utils/megatron_to_hf/deepseekv32.py @@ -5,7 +5,7 @@ from packaging.version import parse -def convert_deepseekv3_2_to_hf(args, name, param): +def convert_deepseekv32_to_hf(args, name, param): if name == "module.module.embedding.word_embeddings.weight": return [("model.embed_tokens.weight", param)] if name == "module.module.output_layer.weight": @@ -77,7 +77,7 @@ def convert_deepseekv3_2_to_hf(args, name, param): return [(f"model.layers.{layer_idx}.self_attn.q_proj.weight", param)] elif rest == "self_attention.linear_q_down_proj.weight": return [(f"model.layers.{layer_idx}.self_attn.q_a_proj.weight", param)] - elif rest == "self_attention.linear_q_up_proj.layer_norm_weight": + elif rest == "self_attention.q_layernorm.weight": return [(f"model.layers.{layer_idx}.self_attn.q_a_layernorm.weight", param)] elif rest == "self_attention.linear_q_up_proj.weight": return [(f"model.layers.{layer_idx}.self_attn.q_b_proj.weight", param)] @@ -110,12 +110,23 @@ def convert_deepseekv3_2_to_hf(args, name, param): return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)] elif rest == "self_attention.linear_kv_down_proj.weight": return [(f"model.layers.{layer_idx}.self_attn.kv_a_proj_with_mqa.weight", param)] - elif rest == "self_attention.linear_kv_up_proj.layer_norm_weight": + elif rest == "self_attention.kv_layernorm.weight": return [(f"model.layers.{layer_idx}.self_attn.kv_a_layernorm.weight", param)] elif rest == "self_attention.linear_kv_up_proj.weight": return [(f"model.layers.{layer_idx}.self_attn.kv_b_proj.weight", param)] elif rest == "pre_mlp_layernorm.weight": return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)] + # DSA Indexer parameters + elif rest == "self_attention.core_attention.indexer.linear_wq_b.weight": + return [(f"model.layers.{layer_idx}.self_attn.indexer.wq_b.weight", param)] + elif rest == "self_attention.core_attention.indexer.linear_wk.weight": + return [(f"model.layers.{layer_idx}.self_attn.indexer.wk.weight", param)] + elif rest == "self_attention.core_attention.indexer.k_norm.weight": + return [(f"model.layers.{layer_idx}.self_attn.indexer.k_norm.weight", param)] + elif rest == "self_attention.core_attention.indexer.k_norm.bias": + return [(f"model.layers.{layer_idx}.self_attn.indexer.k_norm.bias", param)] + elif rest == "self_attention.core_attention.indexer.linear_weights_proj.weight": + return [(f"model.layers.{layer_idx}.self_attn.indexer.weights_proj.weight", param)] elif rest == "mlp.router.weight": return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)] elif rest == "mlp.router.expert_bias": diff --git a/miles_plugins/mbridge/deepseekv32.py b/miles_plugins/mbridge/deepseekv32.py index 8bc94dd23..fb9355f5e 100644 --- a/miles_plugins/mbridge/deepseekv32.py +++ b/miles_plugins/mbridge/deepseekv32.py @@ -1,5 +1,6 @@ from mbridge.core import register_model from mbridge.models import DeepseekV3Bridge +from megatron.core.transformer.enums import AttnBackend @register_model("deepseek_v32") @@ -42,6 +43,8 @@ class DeepseekV32Bridge(DeepseekV3Bridge): def _build_config(self): config = super()._build_config() + config.attention_backend = AttnBackend.auto + config.experimental_attention_variant = "dsa" config.dsa_indexer_n_heads = getattr(self.hf_config, 'dsa_indexer_n_heads', 64) config.dsa_indexer_head_dim = getattr(self.hf_config, 'dsa_indexer_head_dim', 128) diff --git a/scripts/run_deepseek_v3.2_5layer.py b/scripts/run_deepseek_v3.2_5layer.py index 0923e11c6..bcbb3a891 100644 --- a/scripts/run_deepseek_v3.2_5layer.py +++ b/scripts/run_deepseek_v3.2_5layer.py @@ -13,8 +13,8 @@ class ScriptArgs(U.ExecuteTrainConfig): run_id: str = U.create_run_id() hf_checkpoint: str = "/root/.cache/dsv32-ckpt/DeepSeek-V3.2-5layer" - torch_dist_checkpoint: str = "/root/.cache/dsv32-ckpt/DeepSeek-V3-0324-5layer_torch_dist" - num_gpus_per_node: int = 8 + torch_dist_checkpoint: str = "/root/DeepSeek-V3.2-5layer_torch_dist" + num_gpus_per_node: int = 4 enable_eval: bool = False enable_deepep: bool = False extra_args: str = "" @@ -87,8 +87,8 @@ def train(args: ScriptArgs): "--tensor-model-parallel-size 1 " "--sequence-parallel " "--pipeline-model-parallel-size 1 " - "--context-parallel-size 8 " - "--expert-model-parallel-size 8 " + "--context-parallel-size 1 " + "--expert-model-parallel-size 4 " "--expert-tensor-parallel-size 1 " ) elif args.num_nodes <= 4: @@ -136,7 +136,7 @@ def train(args: ScriptArgs): ) sglang_decode_max_bs = 256 - sglang_world_size = 8 if args.num_nodes <= 4 else 64 + sglang_world_size = 4 if args.num_nodes <= 4 else 64 sglang_attn_dp_size = 1 if args.num_nodes <= 4 else 8 sglang_attn_tp_size = sglang_world_size // sglang_attn_dp_size sglang_args = ( @@ -168,6 +168,7 @@ def train(args: ScriptArgs): "--hidden-dropout 0.0 " "--accumulate-allreduce-grads-in-fp32 " "--attention-softmax-in-fp32 " + "--attention-backend auto " f"--update-weight-buffer-size {4 * 1024 ** 3} " f"--actor-num-nodes {args.num_nodes} " f"--actor-num-gpus-per-node {args.num_gpus_per_node} " @@ -176,6 +177,7 @@ def train(args: ScriptArgs): "--use-fault-tolerance " f"--dump-details /root/shared_data/{args.run_id}/dump_details " "--disable-weights-backuper " + "--model-name deepseekv32 " ) train_args = ( diff --git a/scripts/train_dsv32.py b/scripts/train_dsv32.py index 4216ec4be..848cef803 100644 --- a/scripts/train_dsv32.py +++ b/scripts/train_dsv32.py @@ -38,7 +38,6 @@ def train(args): ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) # always update weight first so that sglang has the loaded weights from training. - print("[DEBUG] train.py first update weights") actor_model.update_weights() if args.check_weight_update_equal: From b62966e0eb7fa1e404051b2444b5aa9d4d7f4e4b Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sat, 13 Dec 2025 19:06:18 -0800 Subject: [PATCH 26/57] add several fix, supported thd + CP on megatron's dsa, added dockerfile --- docker/deepseekv32/Dockerfile | 129 ++++ docker/deepseekv32/megatron_dsv32.patch | 552 ++++++++++++++++++ .../processors/quantizer_fp8.py | 7 +- miles/backends/training_utils/loss.py | 2 +- 4 files changed, 686 insertions(+), 4 deletions(-) create mode 100644 docker/deepseekv32/Dockerfile create mode 100644 docker/deepseekv32/megatron_dsv32.patch diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile new file mode 100644 index 000000000..5787b1ce4 --- /dev/null +++ b/docker/deepseekv32/Dockerfile @@ -0,0 +1,129 @@ +ARG SGLANG_IMAGE_TAG=dev +FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang + +# ======================================== Arguments ============================================= + +ARG PATCH_VERSION=latest +ARG MEGATRON_COMMIT=436065a86b749ca3b50eebca68f55c9e690a9f63 + +ARG ENABLE_CUDA_13=0 + +ARG ENABLE_SGLANG_PATCH=0 + +# ======================================== Setup ============================================= + +WORKDIR /root/ + +# ======================================== Apt dependencies ============================================= + +RUN apt update +RUN apt install -y nvtop rsync dnsutils + +# ====================================== Python dependencies ============================================ + +# The compilation is slow, thus should be put at top +# TransformerEngines does not support too high FA2 +RUN MAX_JOBS=64 pip -v install flash-attn==2.7.4.post1 --no-build-isolation + +# The compilation is slow, thus should be put at top +RUN git clone https://github.com/Dao-AILab/flash-attention.git && \ + cd flash-attention/ && git checkout fbf24f67cf7f6442c5cfb2c1057f4bfc57e72d89 && git submodule update --init && cd hopper/ && \ + MAX_JOBS=96 python setup.py install && \ + export python_path=`python -c "import site; print(site.getsitepackages()[0])"` && \ + mkdir -p $python_path/flash_attn_3 && \ + cp flash_attn_interface.py $python_path/flash_attn_3/flash_attn_interface.py && \ + rm -rf flash-attention/ + +RUN pip install git+https://github.com/ISEEKYAN/mbridge.git@89eb10887887bc74853f89a4de258c0702932a1c --no-deps + +RUN pip install flash-linear-attention==0.4.0 + +# TE does not have wheel on cuda 13 yet, thus need to install from source +RUN if [ "${ENABLE_CUDA_13}" = "1" ]; then \ + pip install nvidia-mathdx==25.6.0 && \ + pip -v install --no-build-isolation git+https://github.com/NVIDIA/TransformerEngine.git@release_v2.8; \ + else \ + pip -v install --no-build-isolation "transformer_engine[pytorch]==2.8.0"; \ + fi + +RUN NVCC_APPEND_FLAGS="--threads 4" \ + pip -v install --disable-pip-version-check --no-cache-dir \ + --no-build-isolation \ + --config-settings "--build-option=--cpp_ext --cuda_ext --parallel 8" git+https://github.com/NVIDIA/apex.git@10417aceddd7d5d05d7cbf7b0fc2daad1105f8b4 + +RUN git clone https://github.com/NVIDIA/Megatron-LM.git --recursive && \ + cd Megatron-LM && git checkout ${MEGATRON_COMMIT} && \ + pip install -e . + +RUN pip install git+https://github.com/fzyzcjy/torch_memory_saver.git@dc6876905830430b5054325fa4211ff302169c6b --no-cache-dir --force-reinstall +RUN pip install git+https://github.com/fzyzcjy/Megatron-Bridge.git@dev_rl --no-build-isolation +RUN pip install nvidia-modelopt[torch]>=0.37.0 --no-build-isolation + +# This patch from masahi will be included in later Triton releases +RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ + (cd /root && git clone -b feat/v350_plus_8045 https://github.com/fzyzcjy/triton.git && cd triton && pip install -r python/requirements.txt && pip install --verbose -e .); \ + fi + +COPY requirements.txt /tmp/requirements.txt +RUN pip install -r /tmp/requirements.txt + +# Temporarily install another sgl-kernel version for GB300 without rebuilding the whole image +RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ + SGL_KERNEL_VERSION=0.3.17.post2 && \ + python3 -m pip install https://github.com/sgl-project/whl/releases/download/v${SGL_KERNEL_VERSION}/sgl_kernel-${SGL_KERNEL_VERSION}+cu130-cp310-abi3-manylinux2014_$(uname -m).whl --force-reinstall --no-deps; \ + fi + +# This patch is merged into main, but we are using stable version, thus still need it +RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ + curl -L https://github.com/NVIDIA/TransformerEngine/pull/2286.patch -o /root/te2286.patch && (cd /usr/local/lib/python3.12/dist-packages/transformer_engine && (patch -p2 < /root/te2286.patch)); \ + fi + +# AMEM +# we need to create a fake libcuda.so.1 to make the linker happy when building AMEM +ENV CUDA_DIR=/usr/local/cuda +ENV CUDA_STUBS=${CUDA_DIR}/lib64/stubs +RUN ln -s ${CUDA_STUBS}/libcuda.so ${CUDA_STUBS}/libcuda.so.1 && \ + echo "${CUDA_STUBS}" > /etc/ld.so.conf.d/z-cuda-stubs.conf && \ + ldconfig +RUN git clone https://github.com/inclusionAI/asystem-amem.git && \ + cd asystem-amem && git checkout 6483bb17c9a98b51c3a94b7048467d5b50fbad4b && \ + git submodule init && git submodule update && \ + MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi/ ./build.sh && \ + mv /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2 /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2.bak && \ + cp -r third_party/nccl/build/lib/* /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/ + +RUN [ ! -f /root/.tmux.conf ] || rm /root/.tmux.conf + +# ====================================== Patches ============================================ + +COPY docker/deepseekv32/${PATCH_VERSION}/megatron_dsv32.patch /root/Megatron-LM/ +RUN cd Megatron-LM && \ + git update-index --refresh && \ + git apply megatron.patch --3way && \ + if grep -R -n '^<<<<<<< ' .; then \ + echo "Patch failed to apply cleanly. Please resolve conflicts." && \ + exit 1; \ + fi && \ + rm megatron.patch + +# TODO temporarily skip patching for GB200/GB300 (and require users to bring their own sglang version). should add back later. +COPY docker/patch/${PATCH_VERSION}/sglang.patch /sgl-workspace/sglang/ +RUN if [ "$ENABLE_SGLANG_PATCH" = "1" ]; then \ + cd /sgl-workspace/sglang && \ + git update-index --refresh && \ + git apply sglang.patch && \ + if grep -R -n '^<<<<<<< ' .; then \ + echo "Patch failed to apply cleanly. Please resolve conflicts." && \ + exit 1; \ + fi && \ + rm sglang.patch; \ +fi + +# ====================================== Install main package ============================================ + +# TODO may improve +ARG MILES_COMMIT=main +RUN git clone https://github.com/radixark/miles.git /root/miles && \ + cd /root/miles && \ + git checkout ${MILES_COMMIT} && \ + pip install -e . --no-deps diff --git a/docker/deepseekv32/megatron_dsv32.patch b/docker/deepseekv32/megatron_dsv32.patch new file mode 100644 index 000000000..5ad0d3afe --- /dev/null +++ b/docker/deepseekv32/megatron_dsv32.patch @@ -0,0 +1,552 @@ +diff --git a/megatron/core/transformer/dot_product_attention_context_parallel.py b/megatron/core/transformer/dot_product_attention_context_parallel.py +index 89659a1d7..f1d6855ee 100644 +--- a/megatron/core/transformer/dot_product_attention_context_parallel.py ++++ b/megatron/core/transformer/dot_product_attention_context_parallel.py +@@ -132,10 +132,10 @@ class AllGatherComm: + self.handles = [] + + +-def to_zz_mask_attn_bias(attention_mask, cp_size, nheads, nheads_k, heads_k_stride, device, dtype): ++def to_zz_mask_attn_bias(attention_mask, cp_size, nheads, nheads_k, heads_k_stride, device, dtype, if_zz_mask=False): + '''Convert the attention mask to the attention bias''' + +- if cp_size == 1: ++ if cp_size == 1 or if_zz_mask: + zz_mask = attention_mask + else: + chunked = attention_mask.chunk(dim=3, chunks=cp_size * 2) +@@ -143,7 +143,7 @@ def to_zz_mask_attn_bias(attention_mask, cp_size, nheads, nheads_k, heads_k_stri + zz_mask = torch.cat(zz_mask, dim=3) + attn_bias = torch.zeros(zz_mask.shape, device=device, dtype=dtype) + attn_bias.masked_fill_(zz_mask, float('-inf')) +- attn_bias = attn_bias.expand(-1, heads_k_stride * (nheads // nheads_k), -1, -1) ++ attn_bia = attn_bias.expand(-1, heads_k_stride * (nheads // nheads_k), -1, -1) + return attn_bias + + +@@ -151,7 +151,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + """Native attention function with context parallelism.""" + + @staticmethod +- def forward(ctx, q, k, v, attention_mask, attention_dropout, softmax_scale, pg): ++ def forward(ctx, q, k, v, attention_mask, attention_dropout, softmax_scale, pg, if_zz_mask=False): + '''Forward pass for the native attention function with context parallelism''' + + # Assert einops exists +@@ -171,12 +171,17 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + probs = [] + + # Initialize KV buffers +- kv_buffer = torch.empty( +- (2, k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), ++ # seperate KV buffer for MLA ++ kv_buffer = [torch.empty( ++ (k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), + dtype=k.dtype, + device=k.device, +- ) +- kv_buffer_copy = torch.empty_like(kv_buffer) ++ ), torch.empty( ++ (v.shape[0] * cp_size, v.shape[1], heads_k_stride, v.shape[3]), ++ dtype=v.dtype, ++ device=v.device, ++ )] ++ kv_buffer_copy = [torch.empty_like(kv_buffer[0]), torch.empty_like(kv_buffer[1])] + + # All-gather first chunk of KV buffers + k_0 = k[:, :, :heads_k_stride].contiguous() +@@ -186,7 +191,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + + # Prepare attention bias + attn_bias = to_zz_mask_attn_bias( +- attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype ++ attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype, if_zz_mask + ) + + # Iterate over heads +@@ -226,6 +231,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + + # Save contexts for backward pass + ctx.save_for_backward(q, k, v, attention_mask, *outs, *probs) ++ ctx.if_zz_mask = if_zz_mask + ctx.dropout = attention_dropout + ctx.scale = softmax_scale + ctx.heads_k_stride = heads_k_stride # TODO make it configurable +@@ -252,12 +258,16 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + comm = AllGatherComm(group=pg) + + # Initialize KV buffers +- kv_buffer = torch.empty( +- (2, k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), ++ kv_buffer = [torch.empty( ++ (k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), + dtype=k.dtype, + device=k.device, +- ) +- kv_buffer_copy = torch.empty_like(kv_buffer) ++ ), torch.empty( ++ (v.shape[0] * cp_size, v.shape[1], heads_k_stride, v.shape[3]), ++ dtype=v.dtype, ++ device=v.device, ++ )] ++ kv_buffer_copy = [torch.empty_like(kv_buffer[0]), torch.empty_like(kv_buffer[1])] + + # All-gather first chunk of KV buffers + dq = [] +@@ -270,7 +280,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + + # Prepare attention bias + attn_bias = to_zz_mask_attn_bias( +- attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype ++ attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype, ctx.if_zz_mask + ) + + # Iterate over heads +@@ -339,4 +349,4 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + dq = torch.cat(dq, dim=2) + dk = torch.cat(dk, dim=2) + dv = torch.cat(dv, dim=2) +- return dq, dk, dv, None, None, None, None ++ return dq, dk, dv, None, None, None, None, None +diff --git a/megatron/core/transformer/experimental_attention_variant/dsa.py b/megatron/core/transformer/experimental_attention_variant/dsa.py +index fc994490b..7bc9a485e 100644 +--- a/megatron/core/transformer/experimental_attention_variant/dsa.py ++++ b/megatron/core/transformer/experimental_attention_variant/dsa.py +@@ -6,6 +6,7 @@ from dataclasses import dataclass + from typing import Optional, Tuple, Union + + import torch ++import einops + + from megatron.core import parallel_state + from megatron.core.models.common.embeddings import ( +@@ -21,6 +22,8 @@ from megatron.core.transformer.module import MegatronModule + from megatron.core.transformer.spec_utils import ModuleSpec, build_module + from megatron.core.transformer.transformer_config import TransformerConfig + ++from megatron.core.transformer.dot_product_attention_context_parallel import AllGatherComm, AttentionFuncionWithContextParallel ++ + try: + from fast_hadamard_transform import hadamard_transform + except ImportError: +@@ -191,44 +194,72 @@ def compute_dsa_indexer_loss( + Returns: + index_loss: KL divergence loss (scalar). + """ +- sq, b, np, hn = query.size() +- sk = key.size(0) ++ cp_size = parallel_state.get_context_parallel_world_size() + +- # [sq, b, np, hn] -> [b, np, sq, hn] -> [b * np, sq, hn] +- query = query.permute(1, 2, 0, 3).reshape(b * np, sq, hn) +- # [sk, b, np, hn] -> [b, np, hn, sk] -> [b * np, hn, sk] +- key = key.permute(1, 2, 3, 0).reshape(b * np, hn, sk) +- # Compute attention scores [b * np, sq, sk] +- attention_scores = torch.bmm(query.float(), key.float()) * softmax_scale +- # Reshape to [b, np, sq, sk] +- attention_scores = attention_scores.reshape(b, np, sq, sk) ++ if cp_size > 1: ++ sq_local, b, np, hn = query.size() ++ sk_local = key.size(0) ++ sk_global = sk_local * cp_size + +- # causal_mask [sq, sk] +- causal_mask = torch.triu( +- torch.full((sq, sk), float('-inf'), dtype=torch.float32, device=attention_scores.device), +- diagonal=1, +- ) +- # index_mask [b, sq, sk] +- index_mask = torch.full( +- (b, sq, sk), float("-inf"), dtype=torch.float32, device=causal_mask.device +- ).scatter_(-1, topk_indices, 0) +- +- # [b, np, sq, skv] + [1, 1, sq, skv] -> [b, np, sq, skv] +- attention_scores += causal_mask.view(1, 1, sq, sk) +- if sparse_loss: +- # [b, np, sq, sk] + [b, 1, sq, sk] -> [b, np, sq, sk] +- attention_scores += index_mask.view(b, 1, sq, sk) +- # [b, sq, sk] + [b, sq, sk] -> [b, sq, sk] +- index_scores += index_mask +- +- # [b, np, sq, sk] -> [b, np, sq, sk] +- attention_scores = torch.nn.functional.softmax(attention_scores, dim=-1, dtype=torch.float32) +- # [b, sq, sk] -> [b, sq, sk] +- index_scores = torch.nn.functional.softmax(index_scores, dim=-1, dtype=torch.float32) ++ causal_mask = get_causal_mask(sq_local, sk_local, query.device) ++ float_mask = torch.zeros_like(causal_mask, dtype=torch.float32).masked_fill( ++ causal_mask, float('-inf') ++ ) + +- # Sum attention scores across heads. +- # [batch, heads, seqlen_q, seqlen_k] -> [batch, seqlen_q, seqlen_k] +- attention_scores = attention_scores.sum(dim=1) ++ index_mask = torch.full( ++ (b, sq_local, sk_global), float("-inf"), dtype=torch.float32, device=causal_mask.device ++ ).scatter_(-1, topk_indices, 0) ++ ++ float_mask = float_mask.view(1, 1, sq_local, sk_global) ++ float_mask = index_mask.view(b, 1, sq_local, sk_global) + float_mask if sparse_loss else float_mask ++ ++ # because the attention computation is more heavy in memory (has head dim), ++ # we apply cp (all-gather backend) on attention scores computation ++ attention_scores = compute_attention_scores_with_cp(query, key, float_mask, softmax_scale) # [b, sq_local, sk_global] ++ ++ index_scores = torch.nn.functional.softmax(index_scores, dim=-1, dtype=torch.float32) ++ ++ else: ++ sq, b, np, hn = query.size() ++ sk = key.size(0) ++ ++ # [sq, b, np, hn] -> [b, np, sq, hn] -> [b * np, sq, hn] ++ query = query.permute(1, 2, 0, 3).reshape(b * np, sq, hn) ++ # [sk, b, np, hn] -> [b, np, hn, sk] -> [b * np, hn, sk] ++ key = key.permute(1, 2, 3, 0).reshape(b * np, hn, sk) ++ # Compute attention scores [b * np, sq, sk] ++ attention_scores = torch.bmm(query.float(), key.float()) * softmax_scale ++ # Reshape to [b, np, sq, sk] ++ attention_scores = attention_scores.reshape(b, np, sq, sk) ++ ++ # causal_mask [sq, sk] ++ causal_mask = torch.triu( ++ torch.full((sq, sk), float('-inf'), dtype=torch.float32, device=attention_scores.device), ++ diagonal=1, ++ ) ++ # index_mask [b, sq, sk] ++ index_mask = torch.full( ++ (b, sq, sk), float("-inf"), dtype=torch.float32, device=causal_mask.device ++ ).scatter_(-1, topk_indices, 0) ++ ++ # [b, np, sq, skv] + [1, 1, sq, skv] -> [b, np, sq, skv] ++ attention_scores += causal_mask.view(1, 1, sq, sk) ++ if sparse_loss: ++ # [b, np, sq, sk] + [b, 1, sq, sk] -> [b, np, sq, sk] ++ attention_scores += index_mask.view(b, 1, sq, sk) ++ # [b, sq, sk] + [b, sq, sk] -> [b, sq, sk] ++ index_scores += index_mask ++ ++ # [b, np, sq, sk] -> [b, np, sq, sk] ++ attention_scores = torch.nn.functional.softmax(attention_scores, dim=-1, dtype=torch.float32) ++ # [b, sq, sk] -> [b, sq, sk] ++ index_scores = torch.nn.functional.softmax(index_scores, dim=-1, dtype=torch.float32) ++ ++ # Sum attention scores across heads. ++ # [batch, heads, seqlen_q, seqlen_k] -> [batch, seqlen_q, seqlen_k] ++ attention_scores = attention_scores.sum(dim=1) ++ ++ # Common part + if pg_collection.tp.size() > 1: + # attention scores are scattered to TP ranks in head dimension. + torch.distributed.all_reduce(attention_scores.contiguous(), group=pg_collection.tp) +@@ -252,6 +283,57 @@ def compute_dsa_indexer_loss( + return indexer_loss + + ++def compute_attention_scores_with_cp(q, k, attn_bias, scale, heads_k_stride = 1): ++ """ ++ compute attention scores of q_local @ k_global with CP all-gather backend ++ parallel on n_heads dimension ++ """ ++ pg = parallel_state.get_context_parallel_group() ++ cp_size = parallel_state.get_context_parallel_world_size() ++ ++ sq_local, b, nheads, hn_q = q.shape ++ sk_local, _, nheads_k, hn_k = k.shape ++ sk_global = sk_local * cp_size ++ ++ assert nheads % nheads_k == 0 and nheads_k % heads_k_stride == 0 ++ ++ comm = AllGatherComm(group=pg) ++ attns = torch.zeros(b, heads_k_stride, sq_local, sk_global, dtype=q.dtype, device=q.device) ++ ++ k_buffer = torch.empty( ++ (sk_global, b, heads_k_stride, hn_k), ++ dtype=k.dtype, ++ device=k.device ++ ) ++ k_buffer_copy = torch.empty_like(k_buffer) ++ k_0 = k[:, :, :heads_k_stride].contiguous() ++ comm.all_gather(k_buffer_copy, k_0) ++ ++ attn_bias = attn_bias.expand(-1, heads_k_stride * (nheads // nheads_k), -1, -1) ++ ++ for i in range(0, nheads_k, heads_k_stride): ++ comm.wait() ++ k_buffer, k_buffer_copy = k_buffer_copy, k_buffer ++ if i < nheads_k - heads_k_stride: ++ kvsl = i + heads_k_stride ++ kvsr = kvsl + heads_k_stride ++ send_k = k[:, :, kvsl:kvsr].contiguous() ++ comm.all_gather(k_buffer_copy, send_k) ++ q_i = q[:, :, i * nheads // nheads_k : (i + heads_k_stride) * nheads // nheads_k] ++ k_i = k_buffer ++ ++ _q_i = einops.rearrange(q_i, 's b h d -> b h s d') ++ _k_i = einops.rearrange(k_i, 's b h d -> b h d s') ++ attn_i = torch.matmul(_q_i.float(), _k_i.float()) * scale + attn_bias ++ attn_i = torch.nn.functional.softmax(attn_i, dim=-1, dtype=torch.float32) ++ ++ attns = attns + attn_i ++ ++ attns = torch.sum(attns, dim=1) ++ ++ return attns ++ ++ + class DSAIndexerLossAutoScaler(torch.autograd.Function): + """An AutoScaler that triggers the backward pass and scales the grad for indexer loss. + +@@ -496,7 +578,15 @@ class DSAIndexer(MegatronModule): + # Compute attention scores: q @ k^T + # [seqlen_q, batch, index_n_heads, index_head_dim] @ [seqlen_k, batch, index_head_dim]^T + # -> [seqlen_q, batch, index_n_heads, seqlen_k] +- index_scores = torch.einsum('sbhd,tbd->sbht', q.float(), k.float()) ++ cp_size = parallel_state.get_context_parallel_world_size() ++ if cp_size == 1: ++ index_scores = torch.einsum('sbhd,tbd->sbht', q.float(), k.float()) ++ else: ++ # because k is small (only 1 head), do just one all_gather ++ k_buffer = torch.cat(torch.distributed.nn.functional.all_gather(k, group=self.pg_collection.cp), dim=0) # k_buffer: [[chunk_0, chunk_3, chunk_1, chunk_2], batch, index_head_dim] ++ index_scores = torch.einsum('sbhd,tbd->sbht', q.float(), k_buffer.float()) # [s_q_local, batch, index_n_heads, s_k_global] ++ # rank 0: q [chunk_0, chunk_3], k[chunk_0, chunk_3, chunk_1, chunk_2] ++ # rank 1: q [chunk_1, chunk_2], k[chunk_0, chunk_3, chunk_1, chunk_2] + + # Apply ReLU activation. + index_scores = torch.relu(index_scores) +@@ -606,7 +696,10 @@ class DSAIndexer(MegatronModule): + # ========================================= + # Select top-k indices + # ========================================= +- topk_k = min(self.index_topk, seqlen) ++ cp_size = parallel_state.get_context_parallel_world_size() ++ ++ seqlen_k_global = k.shape[0] * cp_size ++ topk_k = min(self.index_topk, seqlen_k_global) + # [batch, seqlen, index_topk] + topk_indices = index_scores.topk(topk_k, dim=-1)[1] + +@@ -687,6 +780,57 @@ def unfused_dsa_fn(query, key, value, topk_indices, softmax_scale): + output = output.reshape(sq, b, np * hnv) + return output + ++def get_causal_mask(sq, skv, device): ++ cp_size = parallel_state.get_context_parallel_world_size() ++ cp_rank = parallel_state.get_context_parallel_rank() ++ skv_global = skv * cp_size ++ ++ if cp_size == 1: ++ causal_mask = torch.triu( ++ torch.ones((sq, skv), dtype=torch.bool, device=device), ++ diagonal=1, ++ ) ++ else: ++ sq_half = sq // 2 ++ global_q_positions = torch.cat([ ++ torch.arange(cp_rank * sq_half, (cp_rank + 1) * sq_half, device=device), ++ torch.arange(skv_global - (cp_rank + 1) * sq_half, skv_global - cp_rank * sq_half, device=device) ++ ]) ++ ++ global_k_positions = torch.arange(skv_global, device=device) ++ # [sq, 1] < [1, skv_global] -> [sq, skv_global] ++ causal_mask = global_q_positions.unsqueeze(1) < global_k_positions.unsqueeze(0) ++ # convert to zz mask ++ chunked = causal_mask.chunk(dim=1, chunks=cp_size * 2) ++ causal_mask = [_x for _p in zip(chunked[:cp_size], reversed(chunked[cp_size:])) for _x in _p] ++ causal_mask = torch.cat(causal_mask, dim=1) ++ ++ return causal_mask ++ ++def unfused_dsa_fn_with_cp(query, key, value, topk_indices, softmax_scale): ++ pg = parallel_state.get_context_parallel_group() ++ cp_size = parallel_state.get_context_parallel_world_size() ++ cp_rank = parallel_state.get_context_parallel_rank() ++ ++ sq, b, np, hn = query.size() ++ skv = key.size(0) ++ hnv = value.size(3) ++ ++ skv_global = skv * cp_size ++ ++ sparse_mask = torch.ones((b, sq, skv_global), dtype=torch.bool, device=query.device) ++ sparse_mask.scatter_(-1, topk_indices, False) ++ ++ causal_mask = get_causal_mask(sq, skv, query.device) ++ ++ combined_mask = sparse_mask | causal_mask.unsqueeze(0) ++ ++ attention_mask_for_cp = combined_mask.unsqueeze(1) # [b, 1, sq, skv_global] ++ output = AttentionFuncionWithContextParallel.apply( ++ query, key, value, attention_mask_for_cp, 0.0, softmax_scale, pg, True ++ ) ++ return output.reshape(sq, b, np * hnv) ++ + + class DSAttention(MegatronModule): + """ +@@ -768,18 +912,17 @@ class DSAttention(MegatronModule): + # Generate upper triangular mask with -inf above diagonal, 0 elsewhere + # torch.triu with diagonal=1 creates upper triangular matrix (excluding main diagonal) + # float_mask [sq, skv] +- float_mask = torch.triu( +- torch.full((sq, skv), float('-inf'), dtype=torch.float32, device=x.device), +- diagonal=1, +- ) ++ mask = get_causal_mask(sq, skv, x.device) + else: +- assert attention_mask.shape == (b, 1, sq, skv), 'attention_mask shape mismatch' ++ skv_global = skv * parallel_state.get_context_parallel_world_size() ++ assert attention_mask.shape == (b, 1, sq, skv_global), 'attention_mask shape mismatch' + # [b, 1, sq, skv] -> [b, sq, skv] + mask = attention_mask.squeeze() +- # float_mask [b, sq, skv] +- float_mask = torch.zeros_like(mask, dtype=torch.float32).masked_fill( +- mask, float('-inf') +- ) ++ ++ # float_mask [b, sq, skv] ++ float_mask = torch.zeros_like(mask, dtype=torch.float32).masked_fill( ++ mask, float('-inf') ++ ) + + # =================================== + # Get index scores and top-k indices +@@ -791,7 +934,7 @@ class DSAttention(MegatronModule): + # =================================== + # Run sparse attention kernel + # =================================== +- output = unfused_dsa_fn(query, key, value, topk_indices, self.softmax_scale) ++ output = unfused_dsa_fn_with_cp(query, key, value, topk_indices, self.softmax_scale) + + # =================================== + # Attach indexer loss +diff --git a/megatron/core/transformer/multi_latent_attention.py b/megatron/core/transformer/multi_latent_attention.py +index 3953d933b..0ec5029dd 100644 +--- a/megatron/core/transformer/multi_latent_attention.py ++++ b/megatron/core/transformer/multi_latent_attention.py +@@ -6,6 +6,7 @@ from dataclasses import dataclass + from typing import NoReturn, Optional, Union + + import torch ++import torch.nn.functional as F + + try: + from einops import rearrange +@@ -198,6 +199,64 @@ class MultiLatentAttention(Attention): + # the quantized tensor. + set_save_original_input(self.linear_proj) + ++ def convert_thd_and_bsnh(self, src, packed_seq_params, to_bsd): ++ pg = parallel_state.get_context_parallel_group() ++ cp_size = parallel_state.get_context_parallel_world_size() ++ cp_rank = parallel_state.get_context_parallel_rank() ++ ++ seq_len_global = packed_seq_params.max_seqlen_q ++ seq_len_local = seq_len_global // cp_size ++ cu_seqlens_local = packed_seq_params.cu_seqlens_q // cp_size ++ b = len(packed_seq_params.cu_seqlens_q) - 1 ++ t = cu_seqlens_local[-1].item() ++ d = src.shape[-1] ++ ++ if to_bsd: ++ dst = torch.zeros(seq_len_local, b, d, ++ device=src.device, dtype=src.dtype) ++ else: ++ dst = torch.empty((t, 1, d), device=src.device, dtype=src.dtype) ++ ++ if cp_size == 1: ++ for i in range(b): ++ start, end = cu_seqlens_local[i].item(), cu_seqlens_local[i+1].item() ++ if to_bsd: ++ dst[:end-start, i] = src[start:end, 0] ++ else: ++ dst[start:end, 0] = src[:end-start, i] ++ else: ++ gathered = torch.stack( # TODO, may be too large? largest size: cp_size * s * b * h ++ torch.distributed.nn.functional.all_gather(src, group=pg), dim=0 ++ ) ++ for i in range(b): ++ start, end = cu_seqlens_local[i].item(), cu_seqlens_local[i+1].item() ++ len_i = end - start ++ half_len_i = len_i // 2 ++ half = start + half_len_i ++ chunk_size = seq_len_local // 2 ++ s1, e1 = chunk_size * cp_rank, chunk_size * (cp_rank + 1) ++ s2, e2 = chunk_size * (2 * cp_size - cp_rank - 1), chunk_size * (2 * cp_size - cp_rank) ++ ++ if to_bsd: ++ first_half = gathered[:, start:half, 0].contiguous().view(cp_size * half_len_i, -1) ++ second_half = gathered[:, half:end, 0].flip(dims=[0]).contiguous().view(cp_size * half_len_i, -1) ++ padded = F.pad( ++ torch.cat([first_half, second_half], dim=0), ++ (0, 0, 0, seq_len_global - cp_size * len_i), value=0 ++ ) ++ dst[:, i] = torch.cat([padded[s1:e1], padded[s2:e2]], dim=0) ++ else: ++ first_chunk = gathered[:, :chunk_size, i] # s1, s2, ... ++ second_chunk = gathered[:, chunk_size:seq_len_local, i].flip(dims=[0]) # s_n, s_n-1 ... ++ ++ full_padded = torch.cat([first_chunk, second_chunk], dim=0).contiguous().view(seq_len_global, d) ++ ++ ++ dst[start:half, 0] = full_padded[half_len_i * cp_rank:half_len_i * (cp_rank + 1)] ++ dst[half:end, 0] = full_padded[half_len_i * (2 * cp_size - cp_rank - 1):half_len_i * (2 * cp_size - cp_rank)] ++ ++ return dst ++ + def forward( + self, + hidden_states, +@@ -237,6 +296,13 @@ class MultiLatentAttention(Attention): + if self.config.cache_mla_latents: + self.prepare_for_absorption() + ++ original_packed_seq_params = None ++ if (self.config.experimental_attention_variant == "dsa" and ++ packed_seq_params is not None and packed_seq_params.qkv_format == 'thd'): ++ original_packed_seq_params = packed_seq_params ++ hidden_states = self.convert_thd_and_bsnh(hidden_states, packed_seq_params, to_bsd=True) ++ packed_seq_params = None ++ + # ===================== + # Query, Key, and Value + # ===================== +@@ -306,8 +372,6 @@ class MultiLatentAttention(Attention): + attn_mask_type=attn_mask_type, + ) + elif self.config.experimental_attention_variant == "dsa": +- # For dsa we need to pass in the original hidden states and the compressed +- # query representation. + core_attn_out = self.core_attention( + query, + key, +@@ -358,11 +422,9 @@ class MultiLatentAttention(Attention): + # Flatten back: [seq, batch, num_heads * v_head_dim] + core_attn_out = core_attn_out.view(core_attn_out.size(0), core_attn_out.size(1), -1) + +- if packed_seq_params is not None and packed_seq_params.qkv_format == 'thd': +- # reshape to same output shape as unpacked case +- # (t, np, hn) -> (t, b=1, h=np*hn) +- # t is the pack size = sum (sq_i) +- # note that batch is a dummy dimension in the packed case ++ if original_packed_seq_params is not None: ++ core_attn_out = self.convert_thd_and_bsnh(core_attn_out, original_packed_seq_params, to_bsd=False) ++ elif packed_seq_params is not None and packed_seq_params.qkv_format == 'thd': + core_attn_out = core_attn_out.reshape(core_attn_out.size(0), 1, -1) + + if self.recompute_up_proj: +diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py +index a3a167549..98391fda6 100644 +--- a/megatron/core/transformer/transformer_config.py ++++ b/megatron/core/transformer/transformer_config.py +@@ -918,9 +918,9 @@ class TransformerConfig(ModelParallelConfig): + f" but got {self.context_parallel_size=}." + ) + elif self.experimental_attention_variant == "dsa": +- assert ( +- self.context_parallel_size == 1 +- ), "Currently context parallelism is not supported by DSAttention!" ++ # assert ( ++ # self.context_parallel_size == 1 ++ # ), "Currently context parallelism is not supported by DSAttention!" + assert not self.apply_rope_fusion, "RoPE fusion is not supported for DSAttention" + + if self.fp8: diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py index 41495f396..c7649cd8b 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py @@ -42,7 +42,8 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantizatio # TODO: find a clearer way. if converted_name.endswith("_scale"): continue - quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size)) + if_use_ue8m0_in_moe = True if args.sglang_moe_a2a_backend == "deepep" else False + quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size, if_use_ue8m0_in_moe=if_use_ue8m0_in_moe)) return quantize_named_params @@ -83,14 +84,14 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantizatio return converted_named_params -def _quantize_param(name, weight, weight_block_size): +def _quantize_param(name, weight, weight_block_size, if_use_ue8m0_in_moe=True): assert name.endswith(".weight"), f"Expected weight parameter, got {name}" FP8_MIN = torch.finfo(torch.float8_e4m3fn).min FP8_MAX = torch.finfo(torch.float8_e4m3fn).max if weight_block_size is not None: if should_deepgemm_weight_requant_ue8m0 and should_deepgemm_weight_requant_ue8m0( weight_block_size=weight_block_size - ): + ) and if_use_ue8m0_in_moe: qweight, scale = quant_weight_ue8m0(weight, weight_block_size=weight_block_size) scale = transform_scale_ue8m0(scale, mn=qweight.shape[-2]) else: diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index a7f88d137..abc790761 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -858,7 +858,7 @@ def loss_function( return ( loss, - torch.tensor(num_tokens if args.calculate_per_token_loss else 1, device=logits.device), + torch.tensor(num_tokens if args.calculate_per_token_loss else 1, dtype=torch.int, device=logits.device), { "keys": list(log.keys()), "values": torch.tensor( From 1a25680d7fb53ab26b076f5ed398c6e0ed1305e8 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sat, 13 Dec 2025 19:13:50 -0800 Subject: [PATCH 27/57] update dockerfile: TE version, fast-hadamard-transform --- docker/deepseekv32/Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile index 5787b1ce4..817bbbb31 100644 --- a/docker/deepseekv32/Dockerfile +++ b/docker/deepseekv32/Dockerfile @@ -38,12 +38,18 @@ RUN pip install git+https://github.com/ISEEKYAN/mbridge.git@89eb10887887bc74853f RUN pip install flash-linear-attention==0.4.0 +RUN git clone https://github.com/Dao-AILab/fast-hadamard-transform.git fast-hadamard-transform && \ + cd fast-hadamard-transform && \ + pip install -v . --no-build-isolation && \ + cd /root && \ + rm -rf fast-hadamard-transform + # TE does not have wheel on cuda 13 yet, thus need to install from source RUN if [ "${ENABLE_CUDA_13}" = "1" ]; then \ pip install nvidia-mathdx==25.6.0 && \ pip -v install --no-build-isolation git+https://github.com/NVIDIA/TransformerEngine.git@release_v2.8; \ else \ - pip -v install --no-build-isolation "transformer_engine[pytorch]==2.8.0"; \ + pip -v install --no-build-isolation "transformer_engine[pytorch]==2.10.0"; \ fi RUN NVCC_APPEND_FLAGS="--threads 4" \ From f1674e9c06cc863b9fc10cd5599ada60431723bc Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sat, 13 Dec 2025 19:36:27 -0800 Subject: [PATCH 28/57] update patches --- docker/deepseekv32/Dockerfile | 16 +++++- .../{megatron_dsv32.patch => megatron.patch} | 0 docker/deepseekv32/transformers.patch | 21 ++++++++ miles/backends/megatron_utils/actor.py | 3 -- miles/utils/deepseek_v32_patch.py | 50 ------------------- miles_plugins/mbridge/__init__.py | 3 -- 6 files changed, 36 insertions(+), 57 deletions(-) rename docker/deepseekv32/{megatron_dsv32.patch => megatron.patch} (100%) create mode 100644 docker/deepseekv32/transformers.patch delete mode 100644 miles/utils/deepseek_v32_patch.py diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile index 817bbbb31..5692d0e25 100644 --- a/docker/deepseekv32/Dockerfile +++ b/docker/deepseekv32/Dockerfile @@ -61,6 +61,10 @@ RUN git clone https://github.com/NVIDIA/Megatron-LM.git --recursive && \ cd Megatron-LM && git checkout ${MEGATRON_COMMIT} && \ pip install -e . +RUN git clone https://github.com/huggingface/transformers.git && \ + cd transformers && git checkout 40dc11cd3eb4126652aa41ef8272525affd4a636 && \ + pip install -e . + RUN pip install git+https://github.com/fzyzcjy/torch_memory_saver.git@dc6876905830430b5054325fa4211ff302169c6b --no-cache-dir --force-reinstall RUN pip install git+https://github.com/fzyzcjy/Megatron-Bridge.git@dev_rl --no-build-isolation RUN pip install nvidia-modelopt[torch]>=0.37.0 --no-build-isolation @@ -102,7 +106,7 @@ RUN [ ! -f /root/.tmux.conf ] || rm /root/.tmux.conf # ====================================== Patches ============================================ -COPY docker/deepseekv32/${PATCH_VERSION}/megatron_dsv32.patch /root/Megatron-LM/ +COPY docker/deepseekv32/megatron.patch /root/Megatron-LM/ RUN cd Megatron-LM && \ git update-index --refresh && \ git apply megatron.patch --3way && \ @@ -112,6 +116,16 @@ RUN cd Megatron-LM && \ fi && \ rm megatron.patch +COPY docker/deepseekv32/transformers.patch /root/transformers/ +RUN cd transformers && \ + git update-index --refresh && \ + git apply transformers.patch --3way && \ + if grep -R -n '^<<<<<<< ' .; then \ + echo "Patch failed to apply cleanly. Please resolve conflicts." && \ + exit 1; \ + fi && \ + rm transformers.patch + # TODO temporarily skip patching for GB200/GB300 (and require users to bring their own sglang version). should add back later. COPY docker/patch/${PATCH_VERSION}/sglang.patch /sgl-workspace/sglang/ RUN if [ "$ENABLE_SGLANG_PATCH" = "1" ]; then \ diff --git a/docker/deepseekv32/megatron_dsv32.patch b/docker/deepseekv32/megatron.patch similarity index 100% rename from docker/deepseekv32/megatron_dsv32.patch rename to docker/deepseekv32/megatron.patch diff --git a/docker/deepseekv32/transformers.patch b/docker/deepseekv32/transformers.patch new file mode 100644 index 000000000..61bc7b483 --- /dev/null +++ b/docker/deepseekv32/transformers.patch @@ -0,0 +1,21 @@ +diff --git a/src/transformers/models/auto/configuration_auto.py b/src/transformers/models/auto/configuration_auto.py +index 281bb0e773..6b8ae9f843 100644 +--- a/src/transformers/models/auto/configuration_auto.py ++++ b/src/transformers/models/auto/configuration_auto.py +@@ -1330,6 +1330,16 @@ class AutoConfig: + ) + config_dict["model_type"] = "ministral" + ++ if config_dict["model_type"] == "deepseek_v32": ++ logger.info( ++ "Detected deepseek_v32 model, treating as deepseek_v3 for compatibility." ++ ) ++ config_dict["model_type"] = "deepseek_v3" ++ if "architectures" in config_dict: ++ config_dict["architectures"] = [ ++ arch.replace("DeepseekV32", "DeepseekV3") for arch in config_dict["architectures"] ++ ] ++ + try: + config_class = CONFIG_MAPPING[config_dict["model_type"]] + except KeyError: diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index a12d1be4c..a92198a67 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -13,9 +13,6 @@ from torch_memory_saver import torch_memory_saver from transformers import AutoConfig, AutoTokenizer -from miles.utils.deepseek_v32_patch import apply_deepseek_v32_patch -apply_deepseek_v32_patch() - from miles.ray.train_actor import TrainRayActor from miles.utils import train_dump_utils from miles.utils.context_utils import with_defer diff --git a/miles/utils/deepseek_v32_patch.py b/miles/utils/deepseek_v32_patch.py deleted file mode 100644 index 940090706..000000000 --- a/miles/utils/deepseek_v32_patch.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import json -import tempfile -from transformers import AutoConfig - -_patched = False - - -def apply_deepseek_v32_patch(restore_model_type=False): - global _patched - - if _patched: - return - - _original_from_pretrained = AutoConfig.from_pretrained - - def _patched_from_pretrained(pretrained_model_name_or_path, *args, **kwargs): - if isinstance(pretrained_model_name_or_path, str) and os.path.isdir(pretrained_model_name_or_path): - config_file = os.path.join(pretrained_model_name_or_path, "config.json") - if os.path.exists(config_file): - try: - with open(config_file, "r") as f: - config_json = json.load(f) - - if config_json.get("model_type") == "deepseek_v32": - config_json["model_type"] = "deepseek_v3" - if "architectures" in config_json: - config_json["architectures"] = ["DeepseekV3ForCausalLM"] - - tmp_path = os.path.join(tempfile.gettempdir(), "_tmp_config_folder") - os.makedirs(tmp_path, exist_ok=True) - unique_path = os.path.join(tmp_path, f"deepseek_v32_{os.getpid()}.json") - - with open(unique_path, "w") as f: - json.dump(config_json, f) - - config = _original_from_pretrained(unique_path, *args, **kwargs) - - if restore_model_type: - object.__setattr__(config, "model_type", "deepseek_v32") - - return config - except Exception: - pass - - return _original_from_pretrained(pretrained_model_name_or_path, *args, **kwargs) - - AutoConfig.from_pretrained = _patched_from_pretrained - _patched = True - diff --git a/miles_plugins/mbridge/__init__.py b/miles_plugins/mbridge/__init__.py index 0e259d6b0..77741cb17 100644 --- a/miles_plugins/mbridge/__init__.py +++ b/miles_plugins/mbridge/__init__.py @@ -2,9 +2,6 @@ import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) -from miles.utils.deepseek_v32_patch import apply_deepseek_v32_patch -apply_deepseek_v32_patch(restore_model_type=True) - from .deepseekv32 import DeepseekV32Bridge from .glm4 import GLM4Bridge from .glm4moe import GLM4MoEBridge From 66d3b24299a0592cf7d6400ff762984deb80f8a7 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sat, 13 Dec 2025 19:46:26 -0800 Subject: [PATCH 29/57] update script --- scripts/run_deepseek_v3.2_5layer.py | 13 +-- scripts/train_dsv32.py | 121 ---------------------------- 2 files changed, 7 insertions(+), 127 deletions(-) delete mode 100644 scripts/train_dsv32.py diff --git a/scripts/run_deepseek_v3.2_5layer.py b/scripts/run_deepseek_v3.2_5layer.py index bcbb3a891..a6a841a8d 100644 --- a/scripts/run_deepseek_v3.2_5layer.py +++ b/scripts/run_deepseek_v3.2_5layer.py @@ -19,7 +19,7 @@ class ScriptArgs(U.ExecuteTrainConfig): enable_deepep: bool = False extra_args: str = "" task: Literal["dapo_aime", "gsm8k"] = "dapo_aime" - mode: Literal["normal", "debug_minimal"] = "debug_minimal" + mode: Literal["normal", "debug_minimal"] = "normal" @app.command() @@ -63,7 +63,7 @@ def train(args: ScriptArgs): rollout_args += ( "--prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl " "--input-key prompt " - f"--rollout-max-response-len {100 if args.mode == 'debug_minimal' else 32768} " + f"--rollout-max-response-len {100 if args.mode == 'debug_minimal' else 8192} " ) eval_args += ( "--eval-prompt-data aime /root/aime-2024/aime-2024.jsonl " @@ -87,8 +87,8 @@ def train(args: ScriptArgs): "--tensor-model-parallel-size 1 " "--sequence-parallel " "--pipeline-model-parallel-size 1 " - "--context-parallel-size 1 " - "--expert-model-parallel-size 4 " + "--context-parallel-size 8 " + f"--expert-model-parallel-size {args.num_gpus_per_node} " "--expert-tensor-parallel-size 1 " ) elif args.num_nodes <= 4: @@ -136,7 +136,7 @@ def train(args: ScriptArgs): ) sglang_decode_max_bs = 256 - sglang_world_size = 4 if args.num_nodes <= 4 else 64 + sglang_world_size = args.num_gpus_per_node if args.num_nodes <= 4 else 64 sglang_attn_dp_size = 1 if args.num_nodes <= 4 else 8 sglang_attn_tp_size = sglang_world_size // sglang_attn_dp_size sglang_args = ( @@ -178,6 +178,7 @@ def train(args: ScriptArgs): f"--dump-details /root/shared_data/{args.run_id}/dump_details " "--disable-weights-backuper " "--model-name deepseekv32 " + "--train-memory-margin-bytes 1073741824 " ) train_args = ( @@ -195,7 +196,7 @@ def train(args: ScriptArgs): U.execute_train( train_args=train_args, - train_script="scripts/train_dsv32.py", + train_script="train.py", config=args, num_gpus_per_node=args.num_gpus_per_node, megatron_model_type="deepseek-v32-5layer", diff --git a/scripts/train_dsv32.py b/scripts/train_dsv32.py deleted file mode 100644 index 848cef803..000000000 --- a/scripts/train_dsv32.py +++ /dev/null @@ -1,121 +0,0 @@ -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from miles.utils.deepseek_v32_patch import apply_deepseek_v32_patch -apply_deepseek_v32_patch() - -from turtle import mode -import ray -from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_WEIGHTS -from typing import Optional - -try: - from sglang.srt.constants import GPU_MEMORY_TYPE_CUDA_GRAPH -except ImportError: - GPU_MEMORY_TYPE_CUDA_GRAPH = None - -from miles.ray.placement_group import create_placement_groups, create_rollout_manager, create_training_models -from miles.utils.arguments import parse_args -from miles.utils.logging_utils import configure_logger -from miles.utils.tracking_utils import init_tracking - - -def train(args): - configure_logger() - # allocate the GPUs - pgs = create_placement_groups(args) - init_tracking(args) - - # create the rollout manager, with sglang engines inside. - # need to initialize rollout manager first to calculate num_rollout - rollout_manager, num_rollout_per_epoch = create_rollout_manager(args, pgs["rollout"]) - - # create the actor and critic models - actor_model, critic_model = create_training_models(args, pgs, rollout_manager) - - if args.offload_rollout: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) - - # always update weight first so that sglang has the loaded weights from training. - actor_model.update_weights() - - if args.check_weight_update_equal: - ray.get(rollout_manager.check_weights.remote(action="compare")) - - if args.offload_rollout: - if GPU_MEMORY_TYPE_CUDA_GRAPH is not None: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_CUDA_GRAPH])) - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_KV_CACHE])) - - # special case for eval-only - if args.num_rollout == 0 and args.eval_interval is not None: - ray.get(rollout_manager.eval.remote(rollout_id=0)) - - def offload_train(): - if args.offload_train: - if args.use_critic: - critic_model.offload() - if rollout_id >= args.num_critic_only_steps: - actor_model.offload() - else: - actor_model.offload() - else: - actor_model.clear_memory() - - def onload_rollout(): - if args.offload_rollout: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_WEIGHTS])) - - # train loop. - # note that for async training, one can change the position of the sync operation(ray.get). - for rollout_id in range(args.start_rollout_id, args.num_rollout): - # TODO extract the duplicated eval logic - if args.eval_interval is not None and rollout_id == 0: - ray.get(rollout_manager.eval.remote(rollout_id)) - - rollout_data_ref = ray.get(rollout_manager.generate.remote(rollout_id)) - - if args.offload_rollout: - ray.get(rollout_manager.offload.remote()) - - if args.use_critic: - critic_train_handle = critic_model.async_train(rollout_id, rollout_data_ref) - if rollout_id >= args.num_critic_only_steps: - ray.get(actor_model.async_train(rollout_id, rollout_data_ref)) - ray.get(critic_train_handle) - else: - ray.get(actor_model.async_train(rollout_id, rollout_data_ref)) - - if args.save_interval is not None and ( - (rollout_id + 1) % args.save_interval == 0 - or (num_rollout_per_epoch is not None and (rollout_id + 1) % num_rollout_per_epoch == 0) - ): - if (not args.use_critic) or (rollout_id >= args.num_critic_only_steps): - actor_model.save_model(rollout_id) - if args.use_critic: - critic_model.save_model(rollout_id) - if args.rollout_global_dataset: - ray.get(rollout_manager.save.remote(rollout_id)) - - offload_train() - onload_rollout() - actor_model.update_weights() - - if args.offload_rollout: - if GPU_MEMORY_TYPE_CUDA_GRAPH is not None: - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_CUDA_GRAPH])) - ray.get(rollout_manager.onload.remote(tags=[GPU_MEMORY_TYPE_KV_CACHE])) - - if args.eval_interval is not None and ( - (rollout_id + 1) % args.eval_interval == 0 - or (num_rollout_per_epoch is not None and (rollout_id + 1) % num_rollout_per_epoch == 0) - ): - ray.get(rollout_manager.eval.remote(rollout_id)) - - ray.get(rollout_manager.dispose.remote()) - - -if __name__ == "__main__": - args = parse_args() - train(args) From ccdff922d7c21c03a3a4d7d7824f2f3a4ac623d0 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 14 Dec 2025 16:26:39 -0800 Subject: [PATCH 30/57] minor fix --- docker/deepseekv32/megatron.patch | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index 5ad0d3afe..7a21a3c65 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -1,5 +1,5 @@ diff --git a/megatron/core/transformer/dot_product_attention_context_parallel.py b/megatron/core/transformer/dot_product_attention_context_parallel.py -index 89659a1d7..f1d6855ee 100644 +index 89659a1d7..38efa896c 100644 --- a/megatron/core/transformer/dot_product_attention_context_parallel.py +++ b/megatron/core/transformer/dot_product_attention_context_parallel.py @@ -132,10 +132,10 @@ class AllGatherComm: @@ -15,15 +15,6 @@ index 89659a1d7..f1d6855ee 100644 zz_mask = attention_mask else: chunked = attention_mask.chunk(dim=3, chunks=cp_size * 2) -@@ -143,7 +143,7 @@ def to_zz_mask_attn_bias(attention_mask, cp_size, nheads, nheads_k, heads_k_stri - zz_mask = torch.cat(zz_mask, dim=3) - attn_bias = torch.zeros(zz_mask.shape, device=device, dtype=dtype) - attn_bias.masked_fill_(zz_mask, float('-inf')) -- attn_bias = attn_bias.expand(-1, heads_k_stride * (nheads // nheads_k), -1, -1) -+ attn_bia = attn_bias.expand(-1, heads_k_stride * (nheads // nheads_k), -1, -1) - return attn_bias - - @@ -151,7 +151,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): """Native attention function with context parallelism.""" From fd6bea68c9e89dc5e41f352eaaf6760d2b72a33d Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 22 Dec 2025 23:06:48 -0800 Subject: [PATCH 31/57] fix --- .../run-qwen3-4b-mis.sh | 20 +- .../processors/quantizer_fp8.py | 3 + .../megatron_utils/update_weight/common.py | 5 + miles/utils/external_utils/command_utils.py | 8 +- miles_plugins/mbridge/__init__.py | 2 +- miles_plugins/mbridge/deepseekv32.py | 7 + scripts/models/deepseek-v32-5layer.sh | 1 + scripts/models/deepseek-v32.sh | 69 ++++ scripts/run_deepseek_v3.2_5layer.py | 2 +- scripts/run_deepseek_v32.py | 295 ++++++++++++++++++ 10 files changed, 397 insertions(+), 15 deletions(-) create mode 100644 scripts/models/deepseek-v32-5layer.sh create mode 100644 scripts/models/deepseek-v32.sh create mode 100644 scripts/run_deepseek_v32.py diff --git a/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh b/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh index 300e8ac75..a130caa58 100644 --- a/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh +++ b/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh @@ -24,37 +24,37 @@ fi echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" -source "/root/miles/scripts/models/qwen3-4B.sh" +source "/host_home/primary_synced/miles/scripts/models/qwen3-4B.sh" CKPT_ARGS=( - --hf-checkpoint /root/Qwen3-4B + --hf-checkpoint /host_home/models/Qwen3-4B #--hf-checkpoint /root/Qwen3-4B-FP8 - --ref-load /root/Qwen3-4B_torch_dist + --ref-load /root/models/Qwen3-4B_torch_dist # --load /root/Qwen3-4B_miles/ - --save /root/Qwen3-4B_miles/ + --save /root/models/Qwen3-4B_miles/ --save-interval 200 ) ROLLOUT_ARGS=( - --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl + --prompt-data /host_home/data/dapo-math-17k/dapo-math-17k.jsonl --input-key prompt --label-key label --apply-chat-template --rollout-shuffle --rm-type deepscaler --num-rollout 3000 - --rollout-batch-size 32 + --rollout-batch-size 8 --n-samples-per-prompt 8 --rollout-max-response-len 8192 --rollout-temperature 1 - --global-batch-size 256 + --global-batch-size 64 --balance-data ) EVAL_ARGS=( # --eval-interval 20 - --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl + --eval-prompt-data aime /host_home/data/aime-2024/aime-2024.jsonl --n-samples-per-eval-prompt 1 --eval-max-response-len 16384 --eval-top-p 1 @@ -127,7 +127,7 @@ CUSTOM_ARGS=( # launch the master node of ray in container export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} -ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 4 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 # Build the runtime environment JSON with proper variable substitution RUNTIME_ENV_JSON="{ @@ -142,7 +142,7 @@ ray job submit --address="http://127.0.0.1:8265" \ --runtime-env-json="${RUNTIME_ENV_JSON}" \ -- python3 train.py \ --actor-num-nodes 1 \ - --actor-num-gpus-per-node 8 \ + --actor-num-gpus-per-node 4 \ --colocate \ ${MODEL_ARGS[@]} \ ${CKPT_ARGS[@]} \ diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py index c7649cd8b..54bb1e676 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py @@ -73,6 +73,9 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantizatio "self_attention.linear_q_up_proj.weight", "self_attention.linear_kv_down_proj.weight", "self_attention.linear_kv_up_proj.weight", + # dsa indexer + "self_attention.core_attention.indexer.linear_wq_b.weight", + "self_attention.core_attention.indexer.linear_wk.weight", ]: quantize_named_params = [] for converted_name, param in converted_named_params: diff --git a/miles/backends/megatron_utils/update_weight/common.py b/miles/backends/megatron_utils/update_weight/common.py index 85fe76a1b..558a2e06f 100644 --- a/miles/backends/megatron_utils/update_weight/common.py +++ b/miles/backends/megatron_utils/update_weight/common.py @@ -202,6 +202,11 @@ def _named_params_and_buffers_global( expert_idx = int(expert_idx) + expert_offset yield f"module.module.mtp.layers.{layer_idx}.transformer_layer.mlp.experts.{rest}.weight{expert_idx}", param continue + + # TODO: a hacking here, need to be cleaner + duplicated = ['indexer.linear_weights_proj', 'indexer.linear_wk', 'indexer.linear_wq_b', 'linear_q_down_proj', 'linear_kv_down_proj'] + if any(dup in name for dup in duplicated): + param.parallel_mode = 'duplicated' layer_idx, rest = match.groups() layer_idx = int(layer_idx) + layer_offset diff --git a/miles/utils/external_utils/command_utils.py b/miles/utils/external_utils/command_utils.py index 8c7c9316b..e6bf01a1e 100644 --- a/miles/utils/external_utils/command_utils.py +++ b/miles/utils/external_utils/command_utils.py @@ -51,14 +51,15 @@ def convert_checkpoint( exec_command( f"source {repo_base_dir}/scripts/models/{megatron_model_type}.sh && " - f"PYTHONPATH=/root/Megatron-LM " + # Use installed Megatron instead of hardcoded path + f"PYTHONPATH=/host_home/primary_synced/Megatron-LM " f"torchrun " f"--nproc-per-node {num_gpus_per_node} " f"{multinode_args}" f"tools/convert_hf_to_torch_dist.py " "${MODEL_ARGS[@]} " f"--hf-checkpoint {hf_checkpoint} " - f"--save {path_dst}" + f"--save {path_dst} " f"{extra_args}" ) @@ -139,7 +140,8 @@ def execute_train( runtime_env_json = json.dumps( { "env_vars": { - "PYTHONPATH": "/root/Megatron-LM/", + # Use installed Megatron instead of hardcoded path + "PYTHONPATH": "/host_home/primary_synced/Megatron-LM/", # If setting this in FSDP, the computation communication overlapping may have issues **( {} diff --git a/miles_plugins/mbridge/__init__.py b/miles_plugins/mbridge/__init__.py index 77741cb17..cc42522ee 100644 --- a/miles_plugins/mbridge/__init__.py +++ b/miles_plugins/mbridge/__init__.py @@ -16,7 +16,7 @@ @classmethod def _patched_from_config(cls, hf_config, **kwargs): - if hf_config.model_type == "deepseek_v32": + if hasattr(hf_config, 'index_n_heads'): from mbridge.core.bridge import _MODEL_REGISTRY return _MODEL_REGISTRY['deepseek_v32'](hf_config, **kwargs) diff --git a/miles_plugins/mbridge/deepseekv32.py b/miles_plugins/mbridge/deepseekv32.py index fb9355f5e..aae07ee53 100644 --- a/miles_plugins/mbridge/deepseekv32.py +++ b/miles_plugins/mbridge/deepseekv32.py @@ -6,6 +6,13 @@ @register_model("deepseek_v32") class DeepseekV32Bridge(DeepseekV3Bridge): + # Weights with parallel_mode="duplicated" that should NOT be gathered across TP + _DUPLICATED_WEIGHTS = { + "self_attention.core_attention.indexer.linear_wq_b.weight", + "self_attention.core_attention.indexer.linear_wk.weight", + "self_attention.core_attention.indexer.linear_weights_proj.weight", + } + _ATTENTION_MAPPING = ( DeepseekV3Bridge._ATTENTION_MAPPING.copy() ) diff --git a/scripts/models/deepseek-v32-5layer.sh b/scripts/models/deepseek-v32-5layer.sh new file mode 100644 index 000000000..2466640af --- /dev/null +++ b/scripts/models/deepseek-v32-5layer.sh @@ -0,0 +1 @@ +MODEL_ARGS_NUM_LAYERS=5 source "$(dirname -- "${BASH_SOURCE[0]}")/deepseek-v32.sh" diff --git a/scripts/models/deepseek-v32.sh b/scripts/models/deepseek-v32.sh new file mode 100644 index 000000000..a98f2f561 --- /dev/null +++ b/scripts/models/deepseek-v32.sh @@ -0,0 +1,69 @@ +NLAYERS="${MODEL_ARGS_NUM_LAYERS:-61}" +FIRST_K_DENSE_REPLACE=3 + +arr=() +for ((i=0; i Date: Tue, 16 Dec 2025 23:11:30 +0000 Subject: [PATCH 32/57] update --- docker/deepseekv32/Dockerfile | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile index 5692d0e25..a52b35a67 100644 --- a/docker/deepseekv32/Dockerfile +++ b/docker/deepseekv32/Dockerfile @@ -6,7 +6,7 @@ FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang ARG PATCH_VERSION=latest ARG MEGATRON_COMMIT=436065a86b749ca3b50eebca68f55c9e690a9f63 -ARG ENABLE_CUDA_13=0 +ARG ENABLE_CUDA_13=1 ARG ENABLE_SGLANG_PATCH=0 @@ -47,7 +47,8 @@ RUN git clone https://github.com/Dao-AILab/fast-hadamard-transform.git fast-hada # TE does not have wheel on cuda 13 yet, thus need to install from source RUN if [ "${ENABLE_CUDA_13}" = "1" ]; then \ pip install nvidia-mathdx==25.6.0 && \ - pip -v install --no-build-isolation git+https://github.com/NVIDIA/TransformerEngine.git@release_v2.8; \ + pip install pybind11 && \ + pip -v install --no-build-isolation git+https://github.com/NVIDIA/TransformerEngine.git@release_v2.10; \ else \ pip -v install --no-build-isolation "transformer_engine[pytorch]==2.10.0"; \ fi @@ -83,11 +84,6 @@ RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ python3 -m pip install https://github.com/sgl-project/whl/releases/download/v${SGL_KERNEL_VERSION}/sgl_kernel-${SGL_KERNEL_VERSION}+cu130-cp310-abi3-manylinux2014_$(uname -m).whl --force-reinstall --no-deps; \ fi -# This patch is merged into main, but we are using stable version, thus still need it -RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ - curl -L https://github.com/NVIDIA/TransformerEngine/pull/2286.patch -o /root/te2286.patch && (cd /usr/local/lib/python3.12/dist-packages/transformer_engine && (patch -p2 < /root/te2286.patch)); \ - fi - # AMEM # we need to create a fake libcuda.so.1 to make the linker happy when building AMEM ENV CUDA_DIR=/usr/local/cuda From bb09c276aabd7ad6959c792de5998f6efac52b33 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Fri, 19 Dec 2025 11:36:52 -0800 Subject: [PATCH 33/57] init --- miles/utils/arguments.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 79b2c419c..7aed05451 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1701,6 +1701,9 @@ def miles_validate_args(args): args.use_dynamic_batch_size is False ), "Dynamic batch size is not supported for bshd format. Please specify --micro-batch-size instead." + if args.disable_thd_format: + assert args.train_backend == "megatron", "disable_thd_format is only supported for megatron backend." + def hf_validate_args(args, hf_config): def equal(x, y): From 8af1384dad48ad07628533ebf9b2b3682059c0cf Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 22 Dec 2025 20:28:44 -0800 Subject: [PATCH 34/57] supported bshd --- miles/backends/training_utils/data.py | 7 +++++++ miles/backends/training_utils/loss.py | 5 ++++- miles/utils/arguments.py | 5 +++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/miles/backends/training_utils/data.py b/miles/backends/training_utils/data.py index 67bb30108..b8ce38c97 100644 --- a/miles/backends/training_utils/data.py +++ b/miles/backends/training_utils/data.py @@ -134,6 +134,13 @@ def get_batch( tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format, max_seqlen) for t in tokens] tokens = torch.stack(tokens) + if qkv_format == "bshd": + max_seqlen = batch["max_seq_len"][0] + assert max([t.size(0) for t in tokens]) <= max_seqlen + tokens = [slice_with_cp(t, pad_token_id, qkv_format, max_seqlen) for t in tokens] + tokens = torch.stack(tokens) + # TODO: padding to multiples? + elif qkv_format == "thd": tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format) for t in tokens] diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index abc790761..1752044fe 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -87,6 +87,7 @@ def get_responses( tokens_chunk = tokens[-response_length:] else: # TODO: this is super ugly... do better abstraction. + _max_seq_len = max_seq_len[i] if max_seq_len is not None else None chunk_size, chunks_offset, logits_offset, tokens_offset = get_logits_and_tokens_offset_with_cp( total_length, response_length, parallel_state, qkv_format, max_seq_len ) @@ -101,7 +102,9 @@ def get_responses( tokens_1 = tokens[tokens_offset[1][0] : tokens_offset[1][1]] assert logits_0.size(0) == tokens_0.size(0), f"{logits_0.size(0)} vs {tokens_0.size(0)}" - assert logits_1.size(0) == tokens_1.size(0), f"{logits_1.size(0)} vs {tokens_1.size(0)}" + assert logits_1.size(0) == tokens_1.size(0), f"{logits_1.size(0)} vs {tokens_1.size(0)}, chunks_offset {chunks_offset}, logits_offset {logits_offset}, \ + tokens_offset {tokens_offset}, logits_1 range {(end + chunk_size, end + 2 * chunk_size)}, chunk_size {chunk_size}, max_seq_len {_max_seq_len}, \ + logits.shape {logits.shape}" logits_chunk = torch.cat([logits_0, logits_1], dim=0) tokens_chunk = torch.cat([tokens_0, tokens_1], dim=0) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 7aed05451..7139f0220 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1701,8 +1701,9 @@ def miles_validate_args(args): args.use_dynamic_batch_size is False ), "Dynamic batch size is not supported for bshd format. Please specify --micro-batch-size instead." - if args.disable_thd_format: - assert args.train_backend == "megatron", "disable_thd_format is only supported for megatron backend." + assert args.qkv_format in ['thd', 'bshd'], f"qkv_format {args.qkv_format} is not supported. (only 'thd' and 'bshd' are supported)" + if args.qkv_format == 'bshd': + assert args.train_backend == "megatron", "bshd format is only supported for megatron backend." def hf_validate_args(args, hf_config): From 42c680f224074e3f591b837e10a18eba46ade146 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 22 Dec 2025 21:08:10 -0800 Subject: [PATCH 35/57] lint --- miles/backends/training_utils/data.py | 4 ++-- miles/backends/training_utils/loss.py | 4 +--- miles/utils/arguments.py | 7 +++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/miles/backends/training_utils/data.py b/miles/backends/training_utils/data.py index b8ce38c97..4ce8ec468 100644 --- a/miles/backends/training_utils/data.py +++ b/miles/backends/training_utils/data.py @@ -138,9 +138,9 @@ def get_batch( max_seqlen = batch["max_seq_len"][0] assert max([t.size(0) for t in tokens]) <= max_seqlen tokens = [slice_with_cp(t, pad_token_id, qkv_format, max_seqlen) for t in tokens] - tokens = torch.stack(tokens) + tokens = torch.stack(tokens) # TODO: padding to multiples? - + elif qkv_format == "thd": tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format) for t in tokens] diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index 1752044fe..72f7526ed 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -102,9 +102,7 @@ def get_responses( tokens_1 = tokens[tokens_offset[1][0] : tokens_offset[1][1]] assert logits_0.size(0) == tokens_0.size(0), f"{logits_0.size(0)} vs {tokens_0.size(0)}" - assert logits_1.size(0) == tokens_1.size(0), f"{logits_1.size(0)} vs {tokens_1.size(0)}, chunks_offset {chunks_offset}, logits_offset {logits_offset}, \ - tokens_offset {tokens_offset}, logits_1 range {(end + chunk_size, end + 2 * chunk_size)}, chunk_size {chunk_size}, max_seq_len {_max_seq_len}, \ - logits.shape {logits.shape}" + assert logits_1.size(0) == tokens_1.size(0), f"{logits_1.size(0)} vs {tokens_1.size(0)}" logits_chunk = torch.cat([logits_0, logits_1], dim=0) tokens_chunk = torch.cat([tokens_0, tokens_1], dim=0) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 7139f0220..de5378d55 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1701,8 +1701,11 @@ def miles_validate_args(args): args.use_dynamic_batch_size is False ), "Dynamic batch size is not supported for bshd format. Please specify --micro-batch-size instead." - assert args.qkv_format in ['thd', 'bshd'], f"qkv_format {args.qkv_format} is not supported. (only 'thd' and 'bshd' are supported)" - if args.qkv_format == 'bshd': + assert args.qkv_format in [ + "thd", + "bshd", + ], f"qkv_format {args.qkv_format} is not supported. (only 'thd' and 'bshd' are supported)" + if args.qkv_format == "bshd": assert args.train_backend == "megatron", "bshd format is only supported for megatron backend." From 0791a9c8ae8d9eff7dd1037180ec365b93f4020f Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 22 Dec 2025 21:53:24 -0800 Subject: [PATCH 36/57] rename, add argument assert, lint --- miles/backends/training_utils/data.py | 2 +- miles/backends/training_utils/loss.py | 2 +- miles/utils/arguments.py | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/miles/backends/training_utils/data.py b/miles/backends/training_utils/data.py index 4ce8ec468..d38161e60 100644 --- a/miles/backends/training_utils/data.py +++ b/miles/backends/training_utils/data.py @@ -135,7 +135,7 @@ def get_batch( tokens = torch.stack(tokens) if qkv_format == "bshd": - max_seqlen = batch["max_seq_len"][0] + max_seqlen = batch["max_seq_lens"][0] assert max([t.size(0) for t in tokens]) <= max_seqlen tokens = [slice_with_cp(t, pad_token_id, qkv_format, max_seqlen) for t in tokens] tokens = torch.stack(tokens) diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index 72f7526ed..37e81eb53 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -87,7 +87,7 @@ def get_responses( tokens_chunk = tokens[-response_length:] else: # TODO: this is super ugly... do better abstraction. - _max_seq_len = max_seq_len[i] if max_seq_len is not None else None + max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None chunk_size, chunks_offset, logits_offset, tokens_offset = get_logits_and_tokens_offset_with_cp( total_length, response_length, parallel_state, qkv_format, max_seq_len ) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index de5378d55..bf7b0467e 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1707,6 +1707,9 @@ def miles_validate_args(args): ], f"qkv_format {args.qkv_format} is not supported. (only 'thd' and 'bshd' are supported)" if args.qkv_format == "bshd": assert args.train_backend == "megatron", "bshd format is only supported for megatron backend." + assert ( + args.use_dynamic_batch_size is False + ), "Dynamic batch size is not supported for bshd format. Please specify --micro-batch-size instead." def hf_validate_args(args, hf_config): From d8cb73a2fb6c8c39601a748b9a423f3597db877e Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sat, 27 Dec 2025 23:31:30 -0800 Subject: [PATCH 37/57] tmp fix --- miles/backends/megatron_utils/actor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index a92198a67..95803c73f 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -462,6 +462,9 @@ def update_weights(self) -> None: if self.args.offload_train: reload_process_groups() + if isinstance(num_new_engines, tuple): + num_new_engines = num_new_engines[0] + if num_new_engines > 0: self.weight_updater.connect_rollout_engines(rollout_engines, rollout_engine_lock) dist.barrier(group=get_gloo_group()) From 7009e11a966d347907d8e132d8a7b4db00370758 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 01:15:04 -0800 Subject: [PATCH 38/57] update megatron patch --- docker/deepseekv32/megatron.patch | 105 +----------------------------- 1 file changed, 1 insertion(+), 104 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index 7a21a3c65..e6a8a34a5 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -410,7 +410,7 @@ index fc994490b..7bc9a485e 100644 # =================================== # Attach indexer loss diff --git a/megatron/core/transformer/multi_latent_attention.py b/megatron/core/transformer/multi_latent_attention.py -index 3953d933b..0ec5029dd 100644 +index 3953d933b..84301ed54 100644 --- a/megatron/core/transformer/multi_latent_attention.py +++ b/megatron/core/transformer/multi_latent_attention.py @@ -6,6 +6,7 @@ from dataclasses import dataclass @@ -421,109 +421,6 @@ index 3953d933b..0ec5029dd 100644 try: from einops import rearrange -@@ -198,6 +199,64 @@ class MultiLatentAttention(Attention): - # the quantized tensor. - set_save_original_input(self.linear_proj) - -+ def convert_thd_and_bsnh(self, src, packed_seq_params, to_bsd): -+ pg = parallel_state.get_context_parallel_group() -+ cp_size = parallel_state.get_context_parallel_world_size() -+ cp_rank = parallel_state.get_context_parallel_rank() -+ -+ seq_len_global = packed_seq_params.max_seqlen_q -+ seq_len_local = seq_len_global // cp_size -+ cu_seqlens_local = packed_seq_params.cu_seqlens_q // cp_size -+ b = len(packed_seq_params.cu_seqlens_q) - 1 -+ t = cu_seqlens_local[-1].item() -+ d = src.shape[-1] -+ -+ if to_bsd: -+ dst = torch.zeros(seq_len_local, b, d, -+ device=src.device, dtype=src.dtype) -+ else: -+ dst = torch.empty((t, 1, d), device=src.device, dtype=src.dtype) -+ -+ if cp_size == 1: -+ for i in range(b): -+ start, end = cu_seqlens_local[i].item(), cu_seqlens_local[i+1].item() -+ if to_bsd: -+ dst[:end-start, i] = src[start:end, 0] -+ else: -+ dst[start:end, 0] = src[:end-start, i] -+ else: -+ gathered = torch.stack( # TODO, may be too large? largest size: cp_size * s * b * h -+ torch.distributed.nn.functional.all_gather(src, group=pg), dim=0 -+ ) -+ for i in range(b): -+ start, end = cu_seqlens_local[i].item(), cu_seqlens_local[i+1].item() -+ len_i = end - start -+ half_len_i = len_i // 2 -+ half = start + half_len_i -+ chunk_size = seq_len_local // 2 -+ s1, e1 = chunk_size * cp_rank, chunk_size * (cp_rank + 1) -+ s2, e2 = chunk_size * (2 * cp_size - cp_rank - 1), chunk_size * (2 * cp_size - cp_rank) -+ -+ if to_bsd: -+ first_half = gathered[:, start:half, 0].contiguous().view(cp_size * half_len_i, -1) -+ second_half = gathered[:, half:end, 0].flip(dims=[0]).contiguous().view(cp_size * half_len_i, -1) -+ padded = F.pad( -+ torch.cat([first_half, second_half], dim=0), -+ (0, 0, 0, seq_len_global - cp_size * len_i), value=0 -+ ) -+ dst[:, i] = torch.cat([padded[s1:e1], padded[s2:e2]], dim=0) -+ else: -+ first_chunk = gathered[:, :chunk_size, i] # s1, s2, ... -+ second_chunk = gathered[:, chunk_size:seq_len_local, i].flip(dims=[0]) # s_n, s_n-1 ... -+ -+ full_padded = torch.cat([first_chunk, second_chunk], dim=0).contiguous().view(seq_len_global, d) -+ -+ -+ dst[start:half, 0] = full_padded[half_len_i * cp_rank:half_len_i * (cp_rank + 1)] -+ dst[half:end, 0] = full_padded[half_len_i * (2 * cp_size - cp_rank - 1):half_len_i * (2 * cp_size - cp_rank)] -+ -+ return dst -+ - def forward( - self, - hidden_states, -@@ -237,6 +296,13 @@ class MultiLatentAttention(Attention): - if self.config.cache_mla_latents: - self.prepare_for_absorption() - -+ original_packed_seq_params = None -+ if (self.config.experimental_attention_variant == "dsa" and -+ packed_seq_params is not None and packed_seq_params.qkv_format == 'thd'): -+ original_packed_seq_params = packed_seq_params -+ hidden_states = self.convert_thd_and_bsnh(hidden_states, packed_seq_params, to_bsd=True) -+ packed_seq_params = None -+ - # ===================== - # Query, Key, and Value - # ===================== -@@ -306,8 +372,6 @@ class MultiLatentAttention(Attention): - attn_mask_type=attn_mask_type, - ) - elif self.config.experimental_attention_variant == "dsa": -- # For dsa we need to pass in the original hidden states and the compressed -- # query representation. - core_attn_out = self.core_attention( - query, - key, -@@ -358,11 +422,9 @@ class MultiLatentAttention(Attention): - # Flatten back: [seq, batch, num_heads * v_head_dim] - core_attn_out = core_attn_out.view(core_attn_out.size(0), core_attn_out.size(1), -1) - -- if packed_seq_params is not None and packed_seq_params.qkv_format == 'thd': -- # reshape to same output shape as unpacked case -- # (t, np, hn) -> (t, b=1, h=np*hn) -- # t is the pack size = sum (sq_i) -- # note that batch is a dummy dimension in the packed case -+ if original_packed_seq_params is not None: -+ core_attn_out = self.convert_thd_and_bsnh(core_attn_out, original_packed_seq_params, to_bsd=False) -+ elif packed_seq_params is not None and packed_seq_params.qkv_format == 'thd': - core_attn_out = core_attn_out.reshape(core_attn_out.size(0), 1, -1) - - if self.recompute_up_proj: diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py index a3a167549..98391fda6 100644 --- a/megatron/core/transformer/transformer_config.py From 4499325a72349cdecf7179ac9228e07c27565ea4 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 01:16:23 -0800 Subject: [PATCH 39/57] update transformers patch --- docker/deepseekv32/Dockerfile | 2 +- docker/deepseekv32/transformers.patch | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile index a52b35a67..e94e725ed 100644 --- a/docker/deepseekv32/Dockerfile +++ b/docker/deepseekv32/Dockerfile @@ -63,7 +63,7 @@ RUN git clone https://github.com/NVIDIA/Megatron-LM.git --recursive && \ pip install -e . RUN git clone https://github.com/huggingface/transformers.git && \ - cd transformers && git checkout 40dc11cd3eb4126652aa41ef8272525affd4a636 && \ + cd transformers && git checkout 8cb5963cc22174954e7dca2c0a3320b7dc2f4edc && \ pip install -e . RUN pip install git+https://github.com/fzyzcjy/torch_memory_saver.git@dc6876905830430b5054325fa4211ff302169c6b --no-cache-dir --force-reinstall diff --git a/docker/deepseekv32/transformers.patch b/docker/deepseekv32/transformers.patch index 61bc7b483..a7631aa00 100644 --- a/docker/deepseekv32/transformers.patch +++ b/docker/deepseekv32/transformers.patch @@ -1,11 +1,11 @@ diff --git a/src/transformers/models/auto/configuration_auto.py b/src/transformers/models/auto/configuration_auto.py -index 281bb0e773..6b8ae9f843 100644 +index f6a12e7cef..22129a86ee 100644 --- a/src/transformers/models/auto/configuration_auto.py +++ b/src/transformers/models/auto/configuration_auto.py -@@ -1330,6 +1330,16 @@ class AutoConfig: +@@ -1355,6 +1355,15 @@ class AutoConfig: + "Detected mistral model with layer_types, treating as ministral for alternating attention compatibility. " ) config_dict["model_type"] = "ministral" - + if config_dict["model_type"] == "deepseek_v32": + logger.info( + "Detected deepseek_v32 model, treating as deepseek_v3 for compatibility." @@ -15,7 +15,6 @@ index 281bb0e773..6b8ae9f843 100644 + config_dict["architectures"] = [ + arch.replace("DeepseekV32", "DeepseekV3") for arch in config_dict["architectures"] + ] -+ + try: config_class = CONFIG_MAPPING[config_dict["model_type"]] - except KeyError: From 6f1e1300372ef0cb3e655119ef91dc6b8e10d9d0 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 01:17:30 -0800 Subject: [PATCH 40/57] disable amem --- docker/deepseekv32/Dockerfile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile index e94e725ed..5ee2cd5a4 100644 --- a/docker/deepseekv32/Dockerfile +++ b/docker/deepseekv32/Dockerfile @@ -86,17 +86,17 @@ RUN if [ "$ENABLE_CUDA_13" = "1" ]; then \ # AMEM # we need to create a fake libcuda.so.1 to make the linker happy when building AMEM -ENV CUDA_DIR=/usr/local/cuda -ENV CUDA_STUBS=${CUDA_DIR}/lib64/stubs -RUN ln -s ${CUDA_STUBS}/libcuda.so ${CUDA_STUBS}/libcuda.so.1 && \ - echo "${CUDA_STUBS}" > /etc/ld.so.conf.d/z-cuda-stubs.conf && \ - ldconfig -RUN git clone https://github.com/inclusionAI/asystem-amem.git && \ - cd asystem-amem && git checkout 6483bb17c9a98b51c3a94b7048467d5b50fbad4b && \ - git submodule init && git submodule update && \ - MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi/ ./build.sh && \ - mv /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2 /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2.bak && \ - cp -r third_party/nccl/build/lib/* /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/ +# ENV CUDA_DIR=/usr/local/cuda +# ENV CUDA_STUBS=${CUDA_DIR}/lib64/stubs +# RUN ln -s ${CUDA_STUBS}/libcuda.so ${CUDA_STUBS}/libcuda.so.1 && \ +# echo "${CUDA_STUBS}" > /etc/ld.so.conf.d/z-cuda-stubs.conf && \ +# ldconfig +# RUN git clone https://github.com/inclusionAI/asystem-amem.git && \ +# cd asystem-amem && git checkout 6483bb17c9a98b51c3a94b7048467d5b50fbad4b && \ +# git submodule init && git submodule update && \ +# MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi/ ./build.sh && \ +# mv /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2 /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/libnccl.so.2.bak && \ +# cp -r third_party/nccl/build/lib/* /usr/local/lib/python3.12/dist-packages/nvidia/nccl/lib/ RUN [ ! -f /root/.tmux.conf ] || rm /root/.tmux.conf From cbd2e9f58e725b9e34a54eadc7a9c8b59b73f98b Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 01:18:15 -0800 Subject: [PATCH 41/57] add script --- scripts/run_deepseek_v32.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/run_deepseek_v32.py b/scripts/run_deepseek_v32.py index 5787f4ce2..86af2c42a 100644 --- a/scripts/run_deepseek_v32.py +++ b/scripts/run_deepseek_v32.py @@ -18,8 +18,8 @@ class ScriptArgs(U.ExecuteTrainConfig): mode: Literal["normal", "debug_minimal"] = "debug_minimal" run_id: str = U.create_run_id() model_org: str = "deepseek-ai" - model_name: Literal["DeepSeek-V3.2", "DeepSeek-V3.2-5layer"] = "DeepSeek-V3.2-5layer" - megatron_model_type: Literal["deepseek-v32", "deepseek-v32-5layer"] = "deepseek-v32-5layer" + model_name: Literal["DeepSeek-V3.2", "DeepSeek-V3.2-5layer"] = "DeepSeek-V3.2" + megatron_model_type: Literal["deepseek-v32", "deepseek-v32-5layer"] = "deepseek-v32" num_gpus_per_node: int = 4 enable_eval: bool = True extra_args: str = "" @@ -164,10 +164,10 @@ def train(args: ScriptArgs): else: # TODO choose a good config (currently randomly change to suit 64gpu) perf_args = ( - "--tensor-model-parallel-size 1 " + "--tensor-model-parallel-size 8 " "--sequence-parallel " f"--pipeline-model-parallel-size {1 if args.model_name == 'DeepSeek-V3.2-5layer' else 4} " - "--context-parallel-size 8 " + "--context-parallel-size 2 " "--expert-model-parallel-size 16 " "--expert-tensor-parallel-size 1 " ) @@ -179,7 +179,8 @@ def train(args: ScriptArgs): "--recompute-method uniform " "--recompute-num-layers 1 " # ------------ - "--use-dynamic-batch-size " + # "--use-dynamic-batch-size " + "--micro-batch-size 1 " # TODO temp use tiny value "--max-tokens-per-gpu 2048 " # "--max-tokens-per-gpu 16384 " @@ -266,6 +267,7 @@ def train(args: ScriptArgs): "--model-name deepseekv32 " # for mbridge load "--train-memory-margin-bytes 1073741824 " # "--check-weight-update-equal " + "--qkv-format bshd " ) train_args = ( From 9dc5258244a3f3c8782780c8457fb893e32b1704 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 19:44:37 -0800 Subject: [PATCH 42/57] update --- docker/deepseekv32/Dockerfile | 4 +- miles/utils/external_utils/command_utils.py | 10 ++-- scripts/run_deepseek_v32.py | 61 ++++++++++++++------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/docker/deepseekv32/Dockerfile b/docker/deepseekv32/Dockerfile index 5ee2cd5a4..38e9eadfa 100644 --- a/docker/deepseekv32/Dockerfile +++ b/docker/deepseekv32/Dockerfile @@ -1,4 +1,4 @@ -ARG SGLANG_IMAGE_TAG=dev +ARG SGLANG_IMAGE_TAG=v0.5.6.post2 FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang # ======================================== Arguments ============================================= @@ -6,7 +6,7 @@ FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang ARG PATCH_VERSION=latest ARG MEGATRON_COMMIT=436065a86b749ca3b50eebca68f55c9e690a9f63 -ARG ENABLE_CUDA_13=1 +ARG ENABLE_CUDA_13=0 ARG ENABLE_SGLANG_PATCH=0 diff --git a/miles/utils/external_utils/command_utils.py b/miles/utils/external_utils/command_utils.py index e6bf01a1e..bdfc864b4 100644 --- a/miles/utils/external_utils/command_utils.py +++ b/miles/utils/external_utils/command_utils.py @@ -26,6 +26,7 @@ def convert_checkpoint( extra_args: str = "", dir_dst: str = "/root", hf_checkpoint: str | None = None, + megatron_path: str = "/host_home/primary_synced/Megatron-LM", ): hf_checkpoint = hf_checkpoint or f"/root/models/{model_name}" @@ -52,7 +53,7 @@ def convert_checkpoint( exec_command( f"source {repo_base_dir}/scripts/models/{megatron_model_type}.sh && " # Use installed Megatron instead of hardcoded path - f"PYTHONPATH=/host_home/primary_synced/Megatron-LM " + f"PYTHONPATH={megatron_path} " f"torchrun " f"--nproc-per-node {num_gpus_per_node} " f"{multinode_args}" @@ -68,9 +69,9 @@ def rsync_simple(path_src: str, path_dst: str): exec_command(f"mkdir -p {path_dst} && rsync -a --info=progress2 {path_src}/ {path_dst}") -def hf_download_dataset(full_name: str): +def hf_download_dataset(full_name: str, data_dir: str = "/root/datasets"): _, partial_name = full_name.split("/") - exec_command(f"hf download --repo-type dataset {full_name} --local-dir /root/datasets/{partial_name}") + exec_command(f"hf download --repo-type dataset {full_name} --local-dir {data_dir}/{partial_name}") def fp8_cast_bf16(path_src, path_dst): @@ -99,6 +100,7 @@ def execute_train( before_ray_job_submit=None, extra_env_vars=None, config: ExecuteTrainConfig | None = None, + megatron_path: str = "/host_home/primary_synced/Megatron-LM", ): if extra_env_vars is None: extra_env_vars = {} @@ -141,7 +143,7 @@ def execute_train( { "env_vars": { # Use installed Megatron instead of hardcoded path - "PYTHONPATH": "/host_home/primary_synced/Megatron-LM/", + "PYTHONPATH": f"{megatron_path}", # If setting this in FSDP, the computation communication overlapping may have issues **( {} diff --git a/scripts/run_deepseek_v32.py b/scripts/run_deepseek_v32.py index 86af2c42a..0cbf9ca05 100644 --- a/scripts/run_deepseek_v32.py +++ b/scripts/run_deepseek_v32.py @@ -5,7 +5,7 @@ import re from dataclasses import dataclass from typing import Literal - +from pathlib import Path import typer import miles.utils.external_utils.command_utils as U @@ -25,6 +25,28 @@ class ScriptArgs(U.ExecuteTrainConfig): extra_args: str = "" task: Literal["dapo_aime", "gsm8k"] = "dapo_aime" enable_deepep: bool = True + data_dir: str = "/root" + model_dir: str = "/root/.cache/dsv32" + model_local_dir: str = "/root/.cache/dsv32" + save_dir: str = "/root/.cache/dsv32" + megatron_path: str = "/root/Megatron-LM" + + +@app.command() +@U.dataclass_cli +def prepare_single(args: ScriptArgs): + """This script only needs to be executed on one node.""" + match args.task: + case "dapo_aime": + U.hf_download_dataset("zhuzilin/dapo-math-17k", data_dir=args.data_dir) + U.hf_download_dataset("zhuzilin/aime-2024", data_dir=args.data_dir) + case "gsm8k": + U.hf_download_dataset("zhuzilin/gsm8k", data_dir=args.data_dir) + + U.fp8_cast_bf16( + path_src=f"{args.model_dir}/{args.model_name}", + path_dst=f"{args.model_dir}/{args.model_name}-bf16/", + ) @app.command() @@ -34,11 +56,6 @@ def prepare_spmd(args: ScriptArgs): extra_args = "--tensor-model-parallel-size 1 " "--expert-tensor-parallel-size 1 " if args.num_nodes == 1 and args.model_name == "DeepSeek-V3.2-5layer": extra_args += "--pipeline-model-parallel-size 1 " "--expert-model-parallel-size 1 " - elif args.model_name == "DeepSeek-V3.2-20layer": - extra_args += ( - "--expert-model-parallel-size 4 " - # PP info will be auto determined by converter script - ) else: extra_args += ( "--pipeline-model-parallel-size 8 " @@ -49,12 +66,13 @@ def prepare_spmd(args: ScriptArgs): U.convert_checkpoint( model_name=args.model_name, - hf_checkpoint=f"/root/models/{args.model_name}-bf16", + hf_checkpoint=f"{args.model_dir}/{args.model_name}-bf16", megatron_model_type=args.megatron_model_type, num_gpus_per_node=args.num_gpus_per_node, - multinode=True, + multinode=True if args.num_nodes > 1 else False, extra_args=extra_args, - dir_dst="/root/models", + dir_dst=f"{args.model_dir}", + megatron_path=args.megatron_path, ) @@ -66,12 +84,12 @@ def prepare_cp(args: ScriptArgs): def _prepare_cp(args: ScriptArgs): U.rsync_simple( - path_src=f"/root/models/{args.model_name}_torch_dist", - path_dst=f"/root/local_data/{args.model_name}_torch_dist", + path_src=f"{args.model_dir}/{args.model_name}_torch_dist", + path_dst=f"{args.model_local_dir}/{args.model_name}_torch_dist", ) U.rsync_simple( - path_src=f"/root/models/{args.model_name}", - path_dst=f"/root/local_data/{args.model_name}", + path_src=f"{args.model_dir}/{args.model_name}", + path_dst=f"{args.model_local_dir}/{args.model_name}", ) @@ -80,12 +98,12 @@ def _prepare_cp(args: ScriptArgs): def train(args: ScriptArgs): print("running on {args.num_nodes} nodes") # ensure files are there is it was not synced before - _prepare_cp(args) + # _prepare_cp(args) - load_save_path = f"/root/shared_data/{args.run_id}/checkpoints" + load_save_path = f"{args.save_dir}/{args.run_id}/checkpoints" ckpt_args = ( - f"--hf-checkpoint /root/local_data/{args.model_name} " - f"--ref-load /root/local_data/{args.model_name}_torch_dist " + f"--hf-checkpoint {args.model_local_dir}/{args.model_name} " + f"--ref-load {args.model_local_dir}/{args.model_name}_torch_dist " f"--load {load_save_path} " f"--save {load_save_path} " "--save-interval 20 " @@ -120,24 +138,24 @@ def train(args: ScriptArgs): match args.task: case "dapo_aime": rollout_args += ( - "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " + f"--prompt-data {args.data_dir}/dapo-math-17k/dapo-math-17k.jsonl " "--input-key prompt " f"--rollout-max-response-len {100 if args.mode == 'debug_minimal' else 8192} " ) eval_args += ( - "--eval-prompt-data aime /root/datasets/aime-2024/aime-2024.jsonl " + f"--eval-prompt-data aime {args.data_dir}/aime-2024/aime-2024.jsonl " "--n-samples-per-eval-prompt 8 " "--eval-max-response-len 8192 " ) case "gsm8k": rollout_args += ( - "--prompt-data /root/datasets/gsm8k/train.parquet " + f"--prompt-data {args.data_dir}/gsm8k/train.parquet " "--input-key messages " # Deliberately make it very short for this easy task "--rollout-max-response-len 256 " ) eval_args += ( - "--eval-prompt-data gsm8k /root/datasets/gsm8k/test.parquet " + f"--eval-prompt-data gsm8k {args.data_dir}/gsm8k/test.parquet " "--n-samples-per-eval-prompt 1 " "--eval-max-response-len 256 " ) @@ -290,6 +308,7 @@ def train(args: ScriptArgs): num_gpus_per_node=args.num_gpus_per_node, megatron_model_type=args.megatron_model_type, extra_env_vars={**sglang_extra_env_vars}, + megatron_path=args.megatron_path, ) From cab9686fa801a5f9455392a29ba390f84bb226ab Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 20:01:19 -0800 Subject: [PATCH 43/57] fix --- .../run-qwen3-4b-mis.sh | 20 +++++++++---------- miles/backends/training_utils/loss.py | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh b/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh index a130caa58..300e8ac75 100644 --- a/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh +++ b/examples/train_infer_mismatch_helper/run-qwen3-4b-mis.sh @@ -24,37 +24,37 @@ fi echo "HAS_NVLINK: $HAS_NVLINK (detected $NVLINK_COUNT NVLink references)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" -source "/host_home/primary_synced/miles/scripts/models/qwen3-4B.sh" +source "/root/miles/scripts/models/qwen3-4B.sh" CKPT_ARGS=( - --hf-checkpoint /host_home/models/Qwen3-4B + --hf-checkpoint /root/Qwen3-4B #--hf-checkpoint /root/Qwen3-4B-FP8 - --ref-load /root/models/Qwen3-4B_torch_dist + --ref-load /root/Qwen3-4B_torch_dist # --load /root/Qwen3-4B_miles/ - --save /root/models/Qwen3-4B_miles/ + --save /root/Qwen3-4B_miles/ --save-interval 200 ) ROLLOUT_ARGS=( - --prompt-data /host_home/data/dapo-math-17k/dapo-math-17k.jsonl + --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl --input-key prompt --label-key label --apply-chat-template --rollout-shuffle --rm-type deepscaler --num-rollout 3000 - --rollout-batch-size 8 + --rollout-batch-size 32 --n-samples-per-prompt 8 --rollout-max-response-len 8192 --rollout-temperature 1 - --global-batch-size 64 + --global-batch-size 256 --balance-data ) EVAL_ARGS=( # --eval-interval 20 - --eval-prompt-data aime /host_home/data/aime-2024/aime-2024.jsonl + --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl --n-samples-per-eval-prompt 1 --eval-max-response-len 16384 --eval-top-p 1 @@ -127,7 +127,7 @@ CUSTOM_ARGS=( # launch the master node of ray in container export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} -ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 4 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 # Build the runtime environment JSON with proper variable substitution RUNTIME_ENV_JSON="{ @@ -142,7 +142,7 @@ ray job submit --address="http://127.0.0.1:8265" \ --runtime-env-json="${RUNTIME_ENV_JSON}" \ -- python3 train.py \ --actor-num-nodes 1 \ - --actor-num-gpus-per-node 4 \ + --actor-num-gpus-per-node 8 \ --colocate \ ${MODEL_ARGS[@]} \ ${CKPT_ARGS[@]} \ diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index 37e81eb53..abc790761 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -87,7 +87,6 @@ def get_responses( tokens_chunk = tokens[-response_length:] else: # TODO: this is super ugly... do better abstraction. - max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None chunk_size, chunks_offset, logits_offset, tokens_offset = get_logits_and_tokens_offset_with_cp( total_length, response_length, parallel_state, qkv_format, max_seq_len ) From 8d51fe0bbb8f34d83174c4c4365d8cdfa967dfed Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Sun, 28 Dec 2025 20:02:05 -0800 Subject: [PATCH 44/57] rm unused script --- scripts/run_deepseek_v3.2_5layer.py | 209 ---------------------------- 1 file changed, 209 deletions(-) delete mode 100644 scripts/run_deepseek_v3.2_5layer.py diff --git a/scripts/run_deepseek_v3.2_5layer.py b/scripts/run_deepseek_v3.2_5layer.py deleted file mode 100644 index 1643087e7..000000000 --- a/scripts/run_deepseek_v3.2_5layer.py +++ /dev/null @@ -1,209 +0,0 @@ -import re -from dataclasses import dataclass -from typing import Literal - -import typer - -import miles.utils.external_utils.command_utils as U - -app = typer.Typer() - - -@dataclass -class ScriptArgs(U.ExecuteTrainConfig): - run_id: str = U.create_run_id() - hf_checkpoint: str = "/root/.cache/dsv32-ckpt/DeepSeek-V3.2-5layer" - torch_dist_checkpoint: str = "/root/DeepSeek-V3.2-5layer_torch_dist" - num_gpus_per_node: int = 4 - enable_eval: bool = False - enable_deepep: bool = False - extra_args: str = "" - task: Literal["dapo_aime", "gsm8k"] = "dapo_aime" - mode: Literal["normal", "debug_minimal"] = "debug_minimal" - - -@app.command() -@U.dataclass_cli -def train(args: ScriptArgs): - load_save_path = f"/root/shared_data/{args.run_id}/checkpoints" - ckpt_args = ( - f"--hf-checkpoint {args.hf_checkpoint} " - f"--ref-load {args.torch_dist_checkpoint} " - f"--load {load_save_path} " - f"--save {load_save_path} " - "--save-interval 20 " - "--save-retain-interval 20 " - ) - - rollout_args = ( - "--label-key label " - "--apply-chat-template " - "--rollout-shuffle " - "--rm-type math " - "--num-rollout 3000 " - "--rollout-batch-size 128 " - "--n-samples-per-prompt 8 " - "--rollout-temperature 0.8 " - "--num-steps-per-rollout 4 " - "--balance-data " - ) - - if args.mode != "debug_minimal": - rollout_args += ( - "--over-sampling-batch-size 256 " - "--dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std " - ) - - eval_args = "" - if (args.mode != "debug_minimal") and args.enable_eval: - eval_args += "--eval-interval 20 " "--eval-top-p 0.7 " - - match args.task: - case "dapo_aime": - rollout_args += ( - "--prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl " - "--input-key prompt " - f"--rollout-max-response-len {100 if args.mode == 'debug_minimal' else 8192} " - ) - eval_args += ( - "--eval-prompt-data aime /root/aime-2024/aime-2024.jsonl " - "--n-samples-per-eval-prompt 8 " - "--eval-max-response-len 32768 " - ) - case "gsm8k": - rollout_args += ( - "--prompt-data /root/gsm8k/train.parquet " - "--input-key messages " - "--rollout-max-response-len 256 " - ) - eval_args += ( - "--eval-prompt-data gsm8k /root/gsm8k/test.parquet " - "--n-samples-per-eval-prompt 1 " - "--eval-max-response-len 256 " - ) - - if args.num_nodes <= 2: - perf_args = ( - "--tensor-model-parallel-size 1 " - "--sequence-parallel " - "--pipeline-model-parallel-size 1 " - "--context-parallel-size 8 " - f"--expert-model-parallel-size {args.num_gpus_per_node} " - "--expert-tensor-parallel-size 1 " - ) - elif args.num_nodes <= 4: - perf_args = ( - "--tensor-model-parallel-size 4 " - "--sequence-parallel " - "--pipeline-model-parallel-size 1 " - "--context-parallel-size 8 " - "--expert-model-parallel-size 8 " - "--expert-tensor-parallel-size 1 " - ) - else: - perf_args = ( - "--tensor-model-parallel-size 4 " - "--sequence-parallel " - "--pipeline-model-parallel-size 1 " - "--context-parallel-size 8 " - "--expert-model-parallel-size 16 " - "--expert-tensor-parallel-size 1 " - ) - perf_args += ( - "--recompute-granularity full " - "--recompute-method uniform " - "--recompute-num-layers 1 " - "--use-dynamic-batch-size " - "--max-tokens-per-gpu 2048 " - ) - - grpo_args = ( - "--advantage-estimator grpo " - "--kl-loss-coef 0.00 " - "--kl-loss-type low_var_kl " - "--entropy-coef 0.00 " - "--eps-clip 0.2 " - "--eps-clip-high 0.28 " - ) - - optimizer_args = ( - "--optimizer adam " - "--lr 1e-6 " - "--lr-decay-style constant " - "--weight-decay 0.1 " - "--adam-beta1 0.9 " - "--adam-beta2 0.98 " - ) - - sglang_decode_max_bs = 256 - sglang_world_size = args.num_gpus_per_node if args.num_nodes <= 4 else 64 - sglang_attn_dp_size = 1 if args.num_nodes <= 4 else 8 - sglang_attn_tp_size = sglang_world_size // sglang_attn_dp_size - sglang_args = ( - f"--rollout-num-gpus-per-engine {sglang_world_size} " - "--sglang-mem-fraction-static 0.7 " - # f"--sglang-tp-size {sglang_world_size} " - f"--sglang-tp-size 1 " - f"--sglang-ep-size {sglang_world_size} " - "--sglang-enable-dp-attention " - f"--sglang-dp-size {sglang_attn_dp_size} " - "--sglang-moe-dense-tp-size 1 " - "--sglang-enable-dp-lm-head " - "--sglang-server-concurrency 1024 " - f"--sglang-max-running-requests {sglang_world_size * sglang_decode_max_bs // sglang_attn_tp_size} " - f"--sglang-chunked-prefill-size {sglang_world_size * sglang_decode_max_bs} " - f"--sglang-cuda-graph-max-bs {sglang_decode_max_bs} " - ) - if args.enable_deepep: - sglang_args += ( - "--sglang-moe-a2a-backend deepep " - "--sglang-deepep-mode low_latency " - ) - sglang_extra_env_vars = {} - if args.enable_deepep: - sglang_extra_env_vars["SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK"] = f"{sglang_decode_max_bs}" - - misc_args = ( - "--attention-dropout 0.0 " - "--hidden-dropout 0.0 " - "--accumulate-allreduce-grads-in-fp32 " - "--attention-softmax-in-fp32 " - "--attention-backend auto " - f"--update-weight-buffer-size {4 * 1024 ** 3} " - f"--actor-num-nodes {args.num_nodes} " - f"--actor-num-gpus-per-node {args.num_gpus_per_node} " - f"--num-gpus-per-node {args.num_gpus_per_node} " - "--colocate " - "--use-fault-tolerance " - f"--dump-details /root/shared_data/{args.run_id}/dump_details " - "--disable-weights-backuper " - "--model-name deepseekv32 " - "--train-memory-margin-bytes 1073741824 " - ) - - train_args = ( - f"{ckpt_args} " - f"{rollout_args} " - f"{optimizer_args} " - f"{grpo_args} " - f"{U.get_default_wandb_args(__file__, run_id=args.run_id)} " - f"{perf_args} " - f"{eval_args} " - f"{sglang_args} " - f"{misc_args} " - f"{args.extra_args} " - ) - - U.execute_train( - train_args=train_args, - train_script="train.py", - config=args, - num_gpus_per_node=args.num_gpus_per_node, - megatron_model_type="deepseek-v32-5layer", - extra_env_vars={**sglang_extra_env_vars}, - ) - - -if __name__ == "__main__": - app() - From f7beab4f93fe5fdea1c7e8bfeca1eea9d963354e Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 29 Dec 2025 00:23:32 -0800 Subject: [PATCH 45/57] fix --- .../megatron_utils/megatron_to_hf/processors/quantizer_fp8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py index 54bb1e676..87cf24992 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py @@ -42,7 +42,7 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantizatio # TODO: find a clearer way. if converted_name.endswith("_scale"): continue - if_use_ue8m0_in_moe = True if args.sglang_moe_a2a_backend == "deepep" else False + if_use_ue8m0_in_moe = True if args.sglang_moe_runner_backend == "deep_gemm" else False quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size, if_use_ue8m0_in_moe=if_use_ue8m0_in_moe)) return quantize_named_params From dd6870636290c5c00a156c21512fa755e8019eee Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 29 Dec 2025 13:42:29 -0800 Subject: [PATCH 46/57] add docs --- docker/deepseekv32/README.md | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docker/deepseekv32/README.md diff --git a/docker/deepseekv32/README.md b/docker/deepseekv32/README.md new file mode 100644 index 000000000..1ca43f1c2 --- /dev/null +++ b/docker/deepseekv32/README.md @@ -0,0 +1,41 @@ +## Usage + +### Docker +```bash +docker pull yueming11/miles:dsv32-dev + +docker run --gpus all --ipc=host --shm-size=16g --ulimit memlock=-1 --ulimit stack=67108864 --name miles_dsv32 yueming11/miles:dsv32-dev /bin/zsh + +git clone https://github.com/radixark/miles.git +git checkout dsv32 +cd dsv32 +pip install -e . + +# if shows Megatron does not support numpy 2.x +pip install numpy==1.26.4 +``` + +### Quick test with 5 layer model +#### model download + +``` +hf download Pinaster/DeepSeek-V3.2-5layer /root/models/DeepSeek-V3.2-5layer +``` + +#### Prepare model for training +Note: need to change the paths, for all commands below see `scripts/run_deepseek_v32.py` for details + +Step 1. download dataset & convert fp8 hf checkpoint to bf16 with one node +``` +python scripts/run_deepseek_v32.py prepare-single --model-name DeepSeek-V3.2-5layer --megatron-model-type deepseek-v32-5layer +``` + +Step 2. convert hf checkpoint to megatron checkpoint with multiple nodes +``` +python scripts/run_deepseek_v32.py prepare-spmd --model-name DeepSeek-V3.2-5layer --megatron-model-type deepseek-v32-5layer +``` + +#### Launch training +``` +python scripts/run_deepseek_v32.py train --model-name DeepSeek-V3.2-5layer --megatron-model-type deepseek-v32-5layer +``` \ No newline at end of file From f16e095957910bd6b353a5a8216870d911ef82f0 Mon Sep 17 00:00:00 2001 From: Zhihao Wang <101526713+xiuhu17@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:05:05 -0600 Subject: [PATCH 47/57] Fix torch native CP attention backend for DSA (#406) --- docker/deepseekv32/megatron.patch | 245 ++++++++++++++++++++++++++---- 1 file changed, 218 insertions(+), 27 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index e6a8a34a5..391460093 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -1,8 +1,77 @@ diff --git a/megatron/core/transformer/dot_product_attention_context_parallel.py b/megatron/core/transformer/dot_product_attention_context_parallel.py -index 89659a1d7..38efa896c 100644 +index 89659a1d7..77f1beb87 100644 --- a/megatron/core/transformer/dot_product_attention_context_parallel.py +++ b/megatron/core/transformer/dot_product_attention_context_parallel.py -@@ -132,10 +132,10 @@ class AllGatherComm: +@@ -6,6 +6,7 @@ + + import torch + from torch.nn import functional as F ++import torch.distributed as dist + + try: + import einops +@@ -53,15 +54,19 @@ def eager_attn_fwd(q, k, v, attn_bias, sinks, scale, dropout): + + + @torch.no_grad +-def eager_attn_bwd(q, k, v, attn_bias, sinks, scale, dropout, attn_output, probs, grad_output): ++def eager_attn_bwd(q, kv, attn_bias, sinks, scale, dim_short, dropout, attn_output, probs, grad_output): + """Backward pass for eager attention""" + + # Rearrange query, key, value to (b, h, s, d) + b, sq, h, d = q.shape +- sk = k.shape[1] ++ _, sk, _, _ = kv.shape ++ k = kv ++ v = kv[:,:,:,:dim_short] ++ q_tail = q[:,:,:,dim_short:] ++ _q_tail_T = einops.rearrange(q_tail, 'b s h d -> b h d s').contiguous() + _q_T = einops.rearrange(q, 'b s h d -> b h d s') + _k_T = einops.rearrange(k, 'b s h d -> b h s d') +- _v_T = einops.rearrange(v, ' b s h d -> b h d s') ++ _v_T = einops.rearrange(v, 'b s h d -> b h d s') + + # Backward pass for score @ value + if sinks is None: +@@ -70,9 +75,9 @@ def eager_attn_bwd(q, k, v, attn_bias, sinks, scale, dropout, attn_output, probs + attn_w = probs[..., :-1] # Drop the sink + grad_output = einops.rearrange(grad_output, 'b s h d -> b h s d') + attn_w_T = einops.rearrange(attn_w, ' b h sq sk -> b h sk sq') +- grad__v = torch.matmul(attn_w_T, grad_output) +- grad_attn_w = torch.matmul(grad_output, _v_T) +- ++ grad__v = torch.matmul(attn_w_T, grad_output).contiguous() # b h sk d ++ grad_attn_w = torch.matmul(grad_output, _v_T).contiguous() # b h s d || b h d sk -> b h s sk ++ + # Backward pass for softmax + if sinks is None: + grad_probs = grad_attn_w +@@ -95,15 +100,18 @@ def eager_attn_bwd(q, k, v, attn_bias, sinks, scale, dropout, attn_output, probs + + # Backward pass for q @ K^T + grad_attn_w *= scale +- grad__q = torch.matmul(grad_attn_w, _k_T) +- grad__k = torch.matmul(_q_T, grad_attn_w) ++ grad__q = torch.matmul(grad_attn_w, _k_T).contiguous() ++ grad__k = torch.matmul(_q_T, grad_attn_w).contiguous() # b h d sk ++ ++ grad__k_T = grad__k.transpose(2, 3).contiguous() # b h sk d ++ grad__kv = torch.zeros((b, h, sk, d), device=q.device, dtype=q.dtype) # b h sk d ++ grad__kv[:,:,:,:dim_short] = grad__v + grad__k_T[:,:,:,:dim_short] ++ grad__kv[:,:,:,dim_short:] = torch.matmul(_q_tail_T, grad_attn_w).contiguous().transpose(2, 3).contiguous() # b h sk d + + # Rearrange grads to (b, s, h, d) +- grad_v = einops.rearrange(grad__v, 'b h s d -> b s h d') +- grad_k = einops.rearrange(grad__k, 'b h d s -> b s h d') ++ grad__kv = grad__kv.transpose(1, 2).contiguous() + grad_q = einops.rearrange(grad__q, 'b h s d -> b s h d') +- return grad_q, grad_k, grad_v, grad_sinks +- ++ return grad_q, grad__kv, grad_sinks + + class AllGatherComm: + """All gather communication with async operations""" +@@ -132,10 +140,10 @@ class AllGatherComm: self.handles = [] @@ -15,7 +84,7 @@ index 89659a1d7..38efa896c 100644 zz_mask = attention_mask else: chunked = attention_mask.chunk(dim=3, chunks=cp_size * 2) -@@ -151,7 +151,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +@@ -151,7 +159,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): """Native attention function with context parallelism.""" @staticmethod @@ -24,7 +93,7 @@ index 89659a1d7..38efa896c 100644 '''Forward pass for the native attention function with context parallelism''' # Assert einops exists -@@ -171,12 +171,17 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +@@ -171,12 +179,17 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): probs = [] # Initialize KV buffers @@ -46,7 +115,7 @@ index 89659a1d7..38efa896c 100644 # All-gather first chunk of KV buffers k_0 = k[:, :, :heads_k_stride].contiguous() -@@ -186,7 +191,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +@@ -186,7 +199,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): # Prepare attention bias attn_bias = to_zz_mask_attn_bias( @@ -55,7 +124,18 @@ index 89659a1d7..38efa896c 100644 ) # Iterate over heads -@@ -226,6 +231,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +@@ -215,8 +228,9 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + + # Forward pass + out_i, probs_i = eager_attn_fwd( +- q_i, k_i, v_i, attn_bias, None, softmax_scale, attention_dropout ++ q_i, k_i, v_i, attn_bias.contiguous(), None, softmax_scale, attention_dropout + ) ++ + outs.append(out_i) + probs.append(probs_i) + +@@ -226,10 +240,13 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): # Save contexts for backward pass ctx.save_for_backward(q, k, v, attention_mask, *outs, *probs) @@ -63,42 +143,153 @@ index 89659a1d7..38efa896c 100644 ctx.dropout = attention_dropout ctx.scale = softmax_scale ctx.heads_k_stride = heads_k_stride # TODO make it configurable -@@ -252,12 +258,16 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): - comm = AllGatherComm(group=pg) + ctx.pg = pg ++ ctx.dim = q.shape[3] ++ ctx.dim_short = v.shape[3] + + return out + +@@ -238,13 +255,15 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + '''Backward pass for the native attention function with context parallelism''' + + # Initialize or resume constants and communication group +- q, k, v, attention_mask, *rest = ctx.saved_tensors ++ q, kv, _, attention_mask, *rest = ctx.saved_tensors ++ dim = ctx.dim ++ dim_short = ctx.dim_short + nheads = q.shape[2] +- nheads_k = k.shape[2] +- heads_k_stride = ctx.heads_k_stride +- assert nheads_k % heads_k_stride == 0 +- outs = rest[: nheads_k // heads_k_stride] +- probs = rest[nheads_k // heads_k_stride :] ++ nheads_kv = kv.shape[2] ++ heads_kv_stride = ctx.heads_k_stride ++ assert nheads_kv % heads_kv_stride == 0 ++ outs = rest[: nheads_kv // heads_kv_stride] ++ probs = rest[nheads_kv // heads_kv_stride :] + pg = ctx.pg + cp_size = 1 + if pg is not None: +@@ -253,30 +272,27 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): # Initialize KV buffers -- kv_buffer = torch.empty( + kv_buffer = torch.empty( - (2, k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), -+ kv_buffer = [torch.empty( -+ (k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), - dtype=k.dtype, - device=k.device, -- ) -- kv_buffer_copy = torch.empty_like(kv_buffer) -+ ), torch.empty( -+ (v.shape[0] * cp_size, v.shape[1], heads_k_stride, v.shape[3]), -+ dtype=v.dtype, -+ device=v.device, -+ )] -+ kv_buffer_copy = [torch.empty_like(kv_buffer[0]), torch.empty_like(kv_buffer[1])] +- dtype=k.dtype, +- device=k.device, ++ (kv.shape[0] * cp_size, kv.shape[1], heads_kv_stride, kv.shape[3]), ++ dtype=kv.dtype, ++ device=kv.device, + ) + kv_buffer_copy = torch.empty_like(kv_buffer) # All-gather first chunk of KV buffers dq = [] -@@ -270,7 +280,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +- dk = [] +- dv = [] +- k_0 = k[:, :, :heads_k_stride].contiguous() +- v_0 = v[:, :, :heads_k_stride].contiguous() +- comm.all_gather(kv_buffer_copy[0], k_0) +- comm.all_gather(kv_buffer_copy[1], v_0) ++ dkv = [] ++ kv_0 = kv[:, :, :heads_kv_stride].contiguous() ++ comm.all_gather(kv_buffer_copy, kv_0) # Prepare attention bias attn_bias = to_zz_mask_attn_bias( - attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype -+ attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype, ctx.if_zz_mask ++ attention_mask, cp_size, nheads, nheads_kv, heads_kv_stride, q.device, q.dtype, ctx.if_zz_mask ) # Iterate over heads -@@ -339,4 +349,4 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +- for i in range(0, nheads_k, heads_k_stride): ++ for i in range(0, nheads_kv, heads_kv_stride): + # Slice query and output for this iteration +- q_slice = slice(i * nheads // nheads_k, (i + heads_k_stride) * nheads // nheads_k) ++ q_slice = slice(i * nheads // nheads_kv, (i + heads_kv_stride) * nheads // nheads_kv) + q_i = q[:, :, q_slice] + dout_i = dout[:, :, q_slice] + +@@ -285,58 +301,45 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + kv_buffer, kv_buffer_copy = kv_buffer_copy, kv_buffer + + # All-gather the next portion of KV buffers if not the last iteration +- if i < nheads_k - heads_k_stride: +- kvsl = i + heads_k_stride +- kvsr = kvsl + heads_k_stride +- send_k = k[:, :, kvsl:kvsr].contiguous() +- send_v = v[:, :, kvsl:kvsr].contiguous() +- comm.all_gather(kv_buffer_copy[0], send_k) +- comm.all_gather(kv_buffer_copy[1], send_v) ++ if i < nheads_kv - heads_kv_stride: ++ kvsl = i + heads_kv_stride ++ kvsr = kvsl + heads_kv_stride ++ send_kv = kv[:, :, kvsl:kvsr].contiguous() ++ comm.all_gather(kv_buffer_copy, send_kv) + + # Prepare key, value for attention +- k_i = kv_buffer[0] +- v_i = kv_buffer[1] ++ kv_i = kv_buffer + + # Rearrange query, key, value to (b, s, h, d) + q_i = einops.rearrange(q_i, 's b h d -> b s h d') +- k_i = einops.rearrange(k_i, 's b h d -> b s h d') +- v_i = einops.rearrange(v_i, 's b h d -> b s h d') ++ kv_i = einops.rearrange(kv_i, 's b h d -> b s h d') + dout_i = einops.rearrange(dout_i, 's b h d -> b s h d') + + # Backward pass +- dq_i, _dk_i, _dv_i, _ = eager_attn_bwd( +- q_i, k_i, v_i, attn_bias, None, ctx.scale, ctx.dropout, outs[i], probs[i], dout_i ++ dq_i, _dkv_i, _ = eager_attn_bwd( ++ q_i, kv_i, attn_bias, None, ctx.scale, dim_short, ctx.dropout, outs[i], probs[i], dout_i + ) + + # Rearrange gradients to (s, b, h, d) + dq_i = einops.rearrange(dq_i, 'b s h d -> s b h d') +- _dk_i = einops.rearrange(_dk_i, 'b s h d -> s b h d') +- _dv_i = einops.rearrange(_dv_i, 'b s h d -> s b h d') ++ _dkv_i = einops.rearrange(_dkv_i, 'b s h d -> s b h d') ++ + if pg is None: +- dk_i = _dk_i +- dv_i = _dv_i ++ dkv_i = _dkv_i + else: + # Reduce-scatter gradients if CP > 1 +- dk_i = torch.zeros( +- (k_i.shape[1] // cp_size, k_i.shape[0], k_i.shape[2], k_i.shape[3]), +- device=k_i.device, +- dtype=k_i.dtype, +- ) +- dv_i = torch.zeros( +- (v_i.shape[1] // cp_size, v_i.shape[0], v_i.shape[2], v_i.shape[3]), +- device=v_i.device, +- dtype=v_i.dtype, ++ dkv_i = torch.zeros( ++ (kv_i.shape[1] // cp_size, kv_i.shape[0], kv_i.shape[2], kv_i.shape[3]), ++ device=kv_i.device, ++ dtype=kv_i.dtype, + ) +- torch.distributed.reduce_scatter_tensor(dk_i, _dk_i, group=pg) +- torch.distributed.reduce_scatter_tensor(dv_i, _dv_i, group=pg) ++ torch.distributed.reduce_scatter_tensor(dkv_i, _dkv_i, group=pg) + + # Collect gradients + dq.append(dq_i) +- dk.append(dk_i) +- dv.append(dv_i) ++ dkv.append(dkv_i) + + # Concatenate gradients and return dq = torch.cat(dq, dim=2) - dk = torch.cat(dk, dim=2) - dv = torch.cat(dv, dim=2) +- dk = torch.cat(dk, dim=2) +- dv = torch.cat(dv, dim=2) - return dq, dk, dv, None, None, None, None -+ return dq, dk, dv, None, None, None, None, None ++ dkv = torch.cat(dkv, dim=2) ++ return dq, dkv, dkv[:,:,:,:dim_short].detach().contiguous(), None, None, None, None, None diff --git a/megatron/core/transformer/experimental_attention_variant/dsa.py b/megatron/core/transformer/experimental_attention_variant/dsa.py index fc994490b..7bc9a485e 100644 --- a/megatron/core/transformer/experimental_attention_variant/dsa.py From e28d439e4ae6554de792844f455948d0dd4afc1c Mon Sep 17 00:00:00 2001 From: Zhihao Wang <101526713+xiuhu17@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:21:48 +0800 Subject: [PATCH 48/57] tilelang kernel + matrix absorb in megatron (#461) --- docker/deepseekv32/megatron.patch | 1296 ++++++++++++++--- .../processors/quantizer_fp8.py | 12 +- .../megatron_utils/update_weight/common.py | 12 +- miles_plugins/mbridge/__init__.py | 14 +- miles_plugins/mbridge/deepseekv32.py | 68 +- scripts/run_deepseek_v32.py | 5 +- 6 files changed, 1139 insertions(+), 268 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index 391460093..ac7a1be3c 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -1,220 +1,324 @@ diff --git a/megatron/core/transformer/dot_product_attention_context_parallel.py b/megatron/core/transformer/dot_product_attention_context_parallel.py -index 89659a1d7..77f1beb87 100644 +index 89659a1d7..1def27c69 100644 --- a/megatron/core/transformer/dot_product_attention_context_parallel.py +++ b/megatron/core/transformer/dot_product_attention_context_parallel.py -@@ -6,6 +6,7 @@ +@@ -3,9 +3,12 @@ + # Some of this code was adopted from https://github.com/zhuzilin/ring-flash-attention/ + # This source code is licensed under the MIT license found in the + # LICENSE file in the root directory of this source tree. ++# Kernel is adpoted from tilelang/examples/deepseek_v32 import torch - from torch.nn import functional as F +import torch.distributed as dist + from torch.nn import functional as F ++from .tilelang_kernel import sparse_mla_bwd, sparse_mla_fwd_interface try: import einops -@@ -53,15 +54,19 @@ def eager_attn_fwd(q, k, v, attn_bias, sinks, scale, dropout): +@@ -15,96 +18,6 @@ except ImportError: + HAVE_EINOPS = False - @torch.no_grad +-@torch.no_grad +-def eager_attn_fwd(q, k, v, attn_bias, sinks, scale, dropout): +- """Forward pass for eager attention""" +- +- # Rearrange query, key, value to (b, h, s, d) +- b, sq, h, d = q.shape +- sk = k.shape[1] +- _q = einops.rearrange(q, 'b s h d -> b h s d') +- _k = einops.rearrange(k, 'b s h d -> b h d s') +- _v = einops.rearrange(v, 'b s h d -> b h s d') +- +- # Compute attention weights +- attn_w = torch.matmul(_q, _k) * scale +- attn_w = attn_w + attn_bias +- +- # Add sinks to attention weights +- if sinks is None: +- logits = attn_w +- else: +- _sinks = sinks.reshape(1, h, 1, 1).expand(b, -1, sq, 1) +- logits = torch.cat([attn_w, _sinks], dim=-1) +- +- # Compute attention scores +- probs = F.softmax(logits, dim=-1, dtype=logits.dtype) +- if sinks is None: +- attn_w = probs +- else: +- attn_w = probs[..., :-1] # Drop the sink +- +- # Compute attention output +- attn_output = torch.matmul(attn_w, _v) +- attn_output = einops.rearrange(attn_output, 'b h s d -> b s h d') +- attn_output = attn_output.contiguous() +- +- return attn_output, probs +- +- +-@torch.no_grad -def eager_attn_bwd(q, k, v, attn_bias, sinks, scale, dropout, attn_output, probs, grad_output): -+def eager_attn_bwd(q, kv, attn_bias, sinks, scale, dim_short, dropout, attn_output, probs, grad_output): - """Backward pass for eager attention""" - - # Rearrange query, key, value to (b, h, s, d) - b, sq, h, d = q.shape +- """Backward pass for eager attention""" +- +- # Rearrange query, key, value to (b, h, s, d) +- b, sq, h, d = q.shape - sk = k.shape[1] -+ _, sk, _, _ = kv.shape -+ k = kv -+ v = kv[:,:,:,:dim_short] -+ q_tail = q[:,:,:,dim_short:] -+ _q_tail_T = einops.rearrange(q_tail, 'b s h d -> b h d s').contiguous() - _q_T = einops.rearrange(q, 'b s h d -> b h d s') - _k_T = einops.rearrange(k, 'b s h d -> b h s d') +- _q_T = einops.rearrange(q, 'b s h d -> b h d s') +- _k_T = einops.rearrange(k, 'b s h d -> b h s d') - _v_T = einops.rearrange(v, ' b s h d -> b h d s') -+ _v_T = einops.rearrange(v, 'b s h d -> b h d s') - - # Backward pass for score @ value - if sinks is None: -@@ -70,9 +75,9 @@ def eager_attn_bwd(q, k, v, attn_bias, sinks, scale, dropout, attn_output, probs - attn_w = probs[..., :-1] # Drop the sink - grad_output = einops.rearrange(grad_output, 'b s h d -> b h s d') - attn_w_T = einops.rearrange(attn_w, ' b h sq sk -> b h sk sq') +- +- # Backward pass for score @ value +- if sinks is None: +- attn_w = probs +- else: +- attn_w = probs[..., :-1] # Drop the sink +- grad_output = einops.rearrange(grad_output, 'b s h d -> b h s d') +- attn_w_T = einops.rearrange(attn_w, ' b h sq sk -> b h sk sq') - grad__v = torch.matmul(attn_w_T, grad_output) - grad_attn_w = torch.matmul(grad_output, _v_T) - -+ grad__v = torch.matmul(attn_w_T, grad_output).contiguous() # b h sk d -+ grad_attn_w = torch.matmul(grad_output, _v_T).contiguous() # b h s d || b h d sk -> b h s sk -+ - # Backward pass for softmax - if sinks is None: - grad_probs = grad_attn_w -@@ -95,15 +100,18 @@ def eager_attn_bwd(q, k, v, attn_bias, sinks, scale, dropout, attn_output, probs - - # Backward pass for q @ K^T - grad_attn_w *= scale +- # Backward pass for softmax +- if sinks is None: +- grad_probs = grad_attn_w +- else: +- dummy = torch.zeros((b, h, sq, 1), device=q.device, dtype=q.dtype) +- grad_probs = torch.cat([grad_attn_w, dummy], dim=3) +- del grad_attn_w +- grad_logits = torch._softmax_backward_data( +- grad_probs, probs, -1, probs.dtype +- ) # [b, h, sq, sk+1] +- +- # Backward pass for adding sinks +- if sinks is None: +- grad_sinks = None +- grad_attn_w = grad_logits +- else: +- grad__sinks = grad_logits[:, :, :, -1] # [b, h, sq] +- grad_sinks = einops.rearrange(grad__sinks, 'b h s -> h (b s)').sum(-1) +- grad_attn_w = grad_logits[:, :, :, :-1].contiguous() # [b, h, sq, sk] +- +- # Backward pass for q @ K^T +- grad_attn_w *= scale - grad__q = torch.matmul(grad_attn_w, _k_T) - grad__k = torch.matmul(_q_T, grad_attn_w) -+ grad__q = torch.matmul(grad_attn_w, _k_T).contiguous() -+ grad__k = torch.matmul(_q_T, grad_attn_w).contiguous() # b h d sk -+ -+ grad__k_T = grad__k.transpose(2, 3).contiguous() # b h sk d -+ grad__kv = torch.zeros((b, h, sk, d), device=q.device, dtype=q.dtype) # b h sk d -+ grad__kv[:,:,:,:dim_short] = grad__v + grad__k_T[:,:,:,:dim_short] -+ grad__kv[:,:,:,dim_short:] = torch.matmul(_q_tail_T, grad_attn_w).contiguous().transpose(2, 3).contiguous() # b h sk d - - # Rearrange grads to (b, s, h, d) +- +- # Rearrange grads to (b, s, h, d) - grad_v = einops.rearrange(grad__v, 'b h s d -> b s h d') - grad_k = einops.rearrange(grad__k, 'b h d s -> b s h d') -+ grad__kv = grad__kv.transpose(1, 2).contiguous() - grad_q = einops.rearrange(grad__q, 'b h s d -> b s h d') +- grad_q = einops.rearrange(grad__q, 'b h s d -> b s h d') - return grad_q, grad_k, grad_v, grad_sinks - -+ return grad_q, grad__kv, grad_sinks - +- class AllGatherComm: """All gather communication with async operations""" -@@ -132,10 +140,10 @@ class AllGatherComm: - self.handles = [] +@@ -131,212 +44,146 @@ class AllGatherComm: + handle.wait() + self.handles = [] +- -def to_zz_mask_attn_bias(attention_mask, cp_size, nheads, nheads_k, heads_k_stride, device, dtype): -+def to_zz_mask_attn_bias(attention_mask, cp_size, nheads, nheads_k, heads_k_stride, device, dtype, if_zz_mask=False): - '''Convert the attention mask to the attention bias''' - +- '''Convert the attention mask to the attention bias''' +- - if cp_size == 1: -+ if cp_size == 1 or if_zz_mask: - zz_mask = attention_mask - else: - chunked = attention_mask.chunk(dim=3, chunks=cp_size * 2) -@@ -151,7 +159,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): +- zz_mask = attention_mask +- else: +- chunked = attention_mask.chunk(dim=3, chunks=cp_size * 2) +- zz_mask = [_x for _p in zip(chunked[:cp_size], reversed(chunked[cp_size:])) for _x in _p] +- zz_mask = torch.cat(zz_mask, dim=3) +- attn_bias = torch.zeros(zz_mask.shape, device=device, dtype=dtype) +- attn_bias.masked_fill_(zz_mask, float('-inf')) +- attn_bias = attn_bias.expand(-1, heads_k_stride * (nheads // nheads_k), -1, -1) +- return attn_bias +- +- + class AttentionFuncionWithContextParallel(torch.autograd.Function): """Native attention function with context parallelism.""" ++ # q: [seq_len_shard, batch, nheads, dim] ++ # k: [seq_len_kv_shard, batch, 1, dim] ++ # v: [seq_len_kv_shard, batch, 1, dim_v] ++ # indices: [batch, 1, seq_len, topk] ++ # masks: [batch, 1, seq_len, seq_len_kv] @staticmethod - def forward(ctx, q, k, v, attention_mask, attention_dropout, softmax_scale, pg): -+ def forward(ctx, q, k, v, attention_mask, attention_dropout, softmax_scale, pg, if_zz_mask=False): ++ def forward(ctx, q, k, dim_v, indices, masks, attention_dropout, softmax_scale, pg): '''Forward pass for the native attention function with context parallelism''' - # Assert einops exists -@@ -171,12 +179,17 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): - probs = [] +- # Assert einops exists + if not HAVE_EINOPS: + raise ImportError("einops is required by the attention CP but cannot be imported.") - # Initialize KV buffers +- # Initialize communication group and constants + cp_size = 1 + if pg is not None: + cp_size = torch.distributed.get_world_size(pg) + comm = AllGatherComm(group=pg) +- nheads = q.shape[2] +- nheads_k = k.shape[2] +- heads_k_stride = 1 +- assert nheads % nheads_k == 0 and nheads_k % heads_k_stride == 0 +- outs = [] +- probs = [] +- +- # Initialize KV buffers - kv_buffer = torch.empty( - (2, k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), -+ # seperate KV buffer for MLA -+ kv_buffer = [torch.empty( -+ (k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), ++ s, b, heads, dim = q.shape ++ skv, _, kv_groups, _ = k.shape ++ ++ k_buffer = torch.empty( ++ (k.shape[0] * cp_size, k.shape[1], 1, k.shape[3]), dtype=k.dtype, device=k.device, -- ) + ) - kv_buffer_copy = torch.empty_like(kv_buffer) -+ ), torch.empty( -+ (v.shape[0] * cp_size, v.shape[1], heads_k_stride, v.shape[3]), -+ dtype=v.dtype, -+ device=v.device, -+ )] -+ kv_buffer_copy = [torch.empty_like(kv_buffer[0]), torch.empty_like(kv_buffer[1])] - - # All-gather first chunk of KV buffers - k_0 = k[:, :, :heads_k_stride].contiguous() -@@ -186,7 +199,7 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): - - # Prepare attention bias - attn_bias = to_zz_mask_attn_bias( +- +- # All-gather first chunk of KV buffers +- k_0 = k[:, :, :heads_k_stride].contiguous() +- v_0 = v[:, :, :heads_k_stride].contiguous() +- comm.all_gather(kv_buffer_copy[0], k_0) +- comm.all_gather(kv_buffer_copy[1], v_0) +- +- # Prepare attention bias +- attn_bias = to_zz_mask_attn_bias( - attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype -+ attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype, if_zz_mask - ) - - # Iterate over heads -@@ -215,8 +228,9 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): - - # Forward pass - out_i, probs_i = eager_attn_fwd( +- ) +- +- # Iterate over heads +- for i in range(0, nheads_k, heads_k_stride): +- # Wait for previous all-gather to complete +- comm.wait() +- kv_buffer, kv_buffer_copy = kv_buffer_copy, kv_buffer +- # All-gather the next portion of KV buffers if not the last iteration +- if i < nheads_k - heads_k_stride: +- kvsl = i + heads_k_stride +- kvsr = kvsl + heads_k_stride +- send_k = k[:, :, kvsl:kvsr].contiguous() +- send_v = v[:, :, kvsl:kvsr].contiguous() +- comm.all_gather(kv_buffer_copy[0], send_k) +- comm.all_gather(kv_buffer_copy[1], send_v) +- +- # Prepare query, key, value for attention +- q_i = q[:, :, i * nheads // nheads_k : (i + heads_k_stride) * nheads // nheads_k] +- k_i = kv_buffer[0] +- v_i = kv_buffer[1] +- +- # Rearrange query, key, value to (b, s, h, d) +- q_i = einops.rearrange(q_i, 's b h d -> b s h d') +- k_i = einops.rearrange(k_i, 's b h d -> b s h d') +- v_i = einops.rearrange(v_i, 's b h d -> b s h d') +- +- # Forward pass +- out_i, probs_i = eager_attn_fwd( - q_i, k_i, v_i, attn_bias, None, softmax_scale, attention_dropout -+ q_i, k_i, v_i, attn_bias.contiguous(), None, softmax_scale, attention_dropout - ) +- ) +- outs.append(out_i) +- probs.append(probs_i) +- +- # Concatenate outputs and rearrange to (s, b, h, d) +- out = torch.cat(outs, dim=2) +- out = einops.rearrange(out, 'b s h d -> s b h d') +- +- # Save contexts for backward pass +- ctx.save_for_backward(q, k, v, attention_mask, *outs, *probs) ++ comm.all_gather(k_buffer, k) ++ comm.wait() + - outs.append(out_i) - probs.append(probs_i) - -@@ -226,10 +240,13 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): - - # Save contexts for backward pass - ctx.save_for_backward(q, k, v, attention_mask, *outs, *probs) -+ ctx.if_zz_mask = if_zz_mask ++ zz_indices = indices.transpose(1, 2) ++ zz_masks = masks.transpose(1, 2) ++ ++ q_i = q ++ k_i = k_buffer ++ ++ s_, b_, h_, d_ = q_i.shape ++ q_i = einops.rearrange(q_i, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ s_, b_, h_, d_ = k_i.shape ++ k_i = einops.rearrange(k_i, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ zz_indices_i = zz_indices ++ b_, s_, g_, topk_ = zz_indices_i.shape ++ zz_indices_i = zz_indices_i.flatten().view(b_, s_, g_, topk_) ++ zz_masks_i = zz_masks ++ b_, s_, g_, skv_ = zz_masks_i.shape ++ zz_masks_i = zz_masks_i.flatten().view(b_, s_, g_, skv_) ++ ++ out_i, lse_i = sparse_mla_fwd_interface(q_i.contiguous(), k_i, zz_indices_i, zz_masks_i, dim_v, sm_scale = softmax_scale) ++ ++ # out: [B, seq_len_shard, h, dim] -> [seq_len, B, h, dim] ++ out_i = einops.rearrange(out_i, 'b s h d -> s b h d') ++ ++ # outs: [[B, seq_len_shard, nheads // kv_group, dim], ...., [B, seq_len_shard, nheads // kv_group, dim]], repeat kv_group // heads_kv_stride times ++ # lses: [[B, seq_len_shard, heads_kv_stride], ...., [B, seq_len_shard, heads_kv_stride]], repeat kv_group // heads_kv_stride times ++ ctx.save_for_backward(q, k, indices, masks, out_i, lse_i) ctx.dropout = attention_dropout - ctx.scale = softmax_scale - ctx.heads_k_stride = heads_k_stride # TODO make it configurable +- ctx.scale = softmax_scale +- ctx.heads_k_stride = heads_k_stride # TODO make it configurable ++ ctx.softmax_scale = softmax_scale ++ ctx.dim_v = dim_v ctx.pg = pg -+ ctx.dim = q.shape[3] -+ ctx.dim_short = v.shape[3] - return out +- return out ++ return out_i -@@ -238,13 +255,15 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + @staticmethod + def backward(ctx, dout): '''Backward pass for the native attention function with context parallelism''' - # Initialize or resume constants and communication group +- # Initialize or resume constants and communication group - q, k, v, attention_mask, *rest = ctx.saved_tensors -+ q, kv, _, attention_mask, *rest = ctx.saved_tensors -+ dim = ctx.dim -+ dim_short = ctx.dim_short - nheads = q.shape[2] +- nheads = q.shape[2] - nheads_k = k.shape[2] - heads_k_stride = ctx.heads_k_stride - assert nheads_k % heads_k_stride == 0 - outs = rest[: nheads_k // heads_k_stride] - probs = rest[nheads_k // heads_k_stride :] -+ nheads_kv = kv.shape[2] -+ heads_kv_stride = ctx.heads_k_stride -+ assert nheads_kv % heads_kv_stride == 0 -+ outs = rest[: nheads_kv // heads_kv_stride] -+ probs = rest[nheads_kv // heads_kv_stride :] ++ q, k, indices, masks, out, lse = ctx.saved_tensors ++ s, b, heads, dim = q.shape ++ dim_v = ctx.dim_v ++ softmax_scale = ctx.softmax_scale ++ pg = ctx.pg cp_size = 1 if pg is not None: -@@ -253,30 +272,27 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): + cp_size = torch.distributed.get_world_size(pg) + comm = AllGatherComm(group=pg) - # Initialize KV buffers - kv_buffer = torch.empty( +- # Initialize KV buffers +- kv_buffer = torch.empty( - (2, k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), -- dtype=k.dtype, -- device=k.device, -+ (kv.shape[0] * cp_size, kv.shape[1], heads_kv_stride, kv.shape[3]), -+ dtype=kv.dtype, -+ device=kv.device, ++ k_buffer = torch.empty( ++ (k.shape[0] * cp_size, k.shape[1], 1, k.shape[3]), + dtype=k.dtype, + device=k.device, ) - kv_buffer_copy = torch.empty_like(kv_buffer) - - # All-gather first chunk of KV buffers - dq = [] +- kv_buffer_copy = torch.empty_like(kv_buffer) +- +- # All-gather first chunk of KV buffers +- dq = [] - dk = [] - dv = [] - k_0 = k[:, :, :heads_k_stride].contiguous() - v_0 = v[:, :, :heads_k_stride].contiguous() - comm.all_gather(kv_buffer_copy[0], k_0) - comm.all_gather(kv_buffer_copy[1], v_0) -+ dkv = [] -+ kv_0 = kv[:, :, :heads_kv_stride].contiguous() -+ comm.all_gather(kv_buffer_copy, kv_0) - - # Prepare attention bias - attn_bias = to_zz_mask_attn_bias( +- +- # Prepare attention bias +- attn_bias = to_zz_mask_attn_bias( - attention_mask, cp_size, nheads, nheads_k, heads_k_stride, q.device, q.dtype -+ attention_mask, cp_size, nheads, nheads_kv, heads_kv_stride, q.device, q.dtype, ctx.if_zz_mask - ) +- ) - # Iterate over heads +- # Iterate over heads - for i in range(0, nheads_k, heads_k_stride): -+ for i in range(0, nheads_kv, heads_kv_stride): - # Slice query and output for this iteration +- # Slice query and output for this iteration - q_slice = slice(i * nheads // nheads_k, (i + heads_k_stride) * nheads // nheads_k) -+ q_slice = slice(i * nheads // nheads_kv, (i + heads_kv_stride) * nheads // nheads_kv) - q_i = q[:, :, q_slice] - dout_i = dout[:, :, q_slice] - -@@ -285,58 +301,45 @@ class AttentionFuncionWithContextParallel(torch.autograd.Function): - kv_buffer, kv_buffer_copy = kv_buffer_copy, kv_buffer - - # All-gather the next portion of KV buffers if not the last iteration +- q_i = q[:, :, q_slice] +- dout_i = dout[:, :, q_slice] +- +- # Wait for previous all-gather to complete +- comm.wait() +- kv_buffer, kv_buffer_copy = kv_buffer_copy, kv_buffer +- +- # All-gather the next portion of KV buffers if not the last iteration - if i < nheads_k - heads_k_stride: - kvsl = i + heads_k_stride - kvsr = kvsl + heads_k_stride @@ -222,76 +326,101 @@ index 89659a1d7..77f1beb87 100644 - send_v = v[:, :, kvsl:kvsr].contiguous() - comm.all_gather(kv_buffer_copy[0], send_k) - comm.all_gather(kv_buffer_copy[1], send_v) -+ if i < nheads_kv - heads_kv_stride: -+ kvsl = i + heads_kv_stride -+ kvsr = kvsl + heads_kv_stride -+ send_kv = kv[:, :, kvsl:kvsr].contiguous() -+ comm.all_gather(kv_buffer_copy, send_kv) - - # Prepare key, value for attention +- +- # Prepare key, value for attention - k_i = kv_buffer[0] - v_i = kv_buffer[1] -+ kv_i = kv_buffer - - # Rearrange query, key, value to (b, s, h, d) - q_i = einops.rearrange(q_i, 's b h d -> b s h d') +- +- # Rearrange query, key, value to (b, s, h, d) +- q_i = einops.rearrange(q_i, 's b h d -> b s h d') - k_i = einops.rearrange(k_i, 's b h d -> b s h d') - v_i = einops.rearrange(v_i, 's b h d -> b s h d') -+ kv_i = einops.rearrange(kv_i, 's b h d -> b s h d') - dout_i = einops.rearrange(dout_i, 's b h d -> b s h d') - - # Backward pass +- dout_i = einops.rearrange(dout_i, 's b h d -> b s h d') +- +- # Backward pass - dq_i, _dk_i, _dv_i, _ = eager_attn_bwd( - q_i, k_i, v_i, attn_bias, None, ctx.scale, ctx.dropout, outs[i], probs[i], dout_i -+ dq_i, _dkv_i, _ = eager_attn_bwd( -+ q_i, kv_i, attn_bias, None, ctx.scale, dim_short, ctx.dropout, outs[i], probs[i], dout_i - ) +- ) ++ comm.all_gather(k_buffer, k) ++ comm.wait() ++ ++ zz_indices = indices.transpose(1, 2) ++ zz_masks = masks.transpose(1, 2) ++ ++ k_i = k_buffer ++ ++ dq_list = [] ++ dk_list = [] ++ ++ s_, b_, h_, d_ = q.shape ++ q = einops.rearrange(q, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ s_, b_, h_, d_ = k_i.shape ++ k_i = einops.rearrange(k_i, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ s_, b_, h_, d_ = dout.shape ++ dout = einops.rearrange(dout, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ s_, b_, h_, d_ = out.shape ++ out = einops.rearrange(out, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ b_, s_, h_ = lse.shape ++ lse = lse.flatten().view(b_, s_, h_) ++ zz_indices_i = zz_indices ++ b_, s_, g_, topk_ = zz_indices_i.shape ++ zz_indices_i = zz_indices_i.flatten().view(b_, s_, g_, topk_) ++ zz_masks_i = zz_masks ++ b_, s_, g_, skv_ = zz_masks_i.shape ++ zz_masks_i = zz_masks_i.flatten().view(b_, s_, g_, skv_) ++ ++ heads_kv_stride = 16 ++ for i in range(0, heads, heads_kv_stride): ++ q_slice = slice(i, min(i + heads_kv_stride, heads)) ++ q_i = q[:, :, q_slice, :].contiguous() ++ dout_i = dout[:, :, q_slice, :].contiguous() ++ out_i = out[:, :, q_slice, :].contiguous() ++ lse_i = lse[:, :, q_slice].contiguous() ++ ++ # TODO: needs casual = True, may not be compatible with zz ++ dq_i, _dk_i = sparse_mla_bwd(q_i, k_i, out_i, dout_i, zz_indices_i, zz_masks_i, lse_i, dim_v, sm_scale = softmax_scale) - # Rearrange gradients to (s, b, h, d) +- # Rearrange gradients to (s, b, h, d) dq_i = einops.rearrange(dq_i, 'b s h d -> s b h d') -- _dk_i = einops.rearrange(_dk_i, 'b s h d -> s b h d') + _dk_i = einops.rearrange(_dk_i, 'b s h d -> s b h d') - _dv_i = einops.rearrange(_dv_i, 'b s h d -> s b h d') -+ _dkv_i = einops.rearrange(_dkv_i, 'b s h d -> s b h d') + if pg is None: -- dk_i = _dk_i + dk_i = _dk_i - dv_i = _dv_i -+ dkv_i = _dkv_i else: - # Reduce-scatter gradients if CP > 1 -- dk_i = torch.zeros( -- (k_i.shape[1] // cp_size, k_i.shape[0], k_i.shape[2], k_i.shape[3]), -- device=k_i.device, -- dtype=k_i.dtype, -- ) +- # Reduce-scatter gradients if CP > 1 + dk_i = torch.zeros( + (k_i.shape[1] // cp_size, k_i.shape[0], k_i.shape[2], k_i.shape[3]), + device=k_i.device, + dtype=k_i.dtype, + ) - dv_i = torch.zeros( - (v_i.shape[1] // cp_size, v_i.shape[0], v_i.shape[2], v_i.shape[3]), - device=v_i.device, - dtype=v_i.dtype, -+ dkv_i = torch.zeros( -+ (kv_i.shape[1] // cp_size, kv_i.shape[0], kv_i.shape[2], kv_i.shape[3]), -+ device=kv_i.device, -+ dtype=kv_i.dtype, - ) -- torch.distributed.reduce_scatter_tensor(dk_i, _dk_i, group=pg) +- ) + torch.distributed.reduce_scatter_tensor(dk_i, _dk_i, group=pg) - torch.distributed.reduce_scatter_tensor(dv_i, _dv_i, group=pg) -+ torch.distributed.reduce_scatter_tensor(dkv_i, _dkv_i, group=pg) - # Collect gradients - dq.append(dq_i) +- # Collect gradients +- dq.append(dq_i) - dk.append(dk_i) - dv.append(dv_i) -+ dkv.append(dkv_i) ++ dq_list.append(dq_i) ++ dk_list.append(dk_i) # Concatenate gradients and return - dq = torch.cat(dq, dim=2) +- dq = torch.cat(dq, dim=2) - dk = torch.cat(dk, dim=2) - dv = torch.cat(dv, dim=2) - return dq, dk, dv, None, None, None, None -+ dkv = torch.cat(dkv, dim=2) -+ return dq, dkv, dkv[:,:,:,:dim_short].detach().contiguous(), None, None, None, None, None ++ dq = torch.cat(dq_list, dim=2) ++ dk = sum(dk_list) ++ ++ return dq, dk, None, None, None, None, None, None diff --git a/megatron/core/transformer/experimental_attention_variant/dsa.py b/megatron/core/transformer/experimental_attention_variant/dsa.py -index fc994490b..7bc9a485e 100644 +index fc994490b..b23d2e9a8 100644 --- a/megatron/core/transformer/experimental_attention_variant/dsa.py +++ b/megatron/core/transformer/experimental_attention_variant/dsa.py @@ -6,6 +6,7 @@ from dataclasses import dataclass @@ -358,14 +487,14 @@ index fc994490b..7bc9a485e 100644 + float_mask = torch.zeros_like(causal_mask, dtype=torch.float32).masked_fill( + causal_mask, float('-inf') + ) ++ ++ index_mask = torch.full( ++ (b, sq_local, sk_global), float("-inf"), dtype=torch.float32, device=causal_mask.device ++ ).scatter_(-1, topk_indices, 0) - # Sum attention scores across heads. - # [batch, heads, seqlen_q, seqlen_k] -> [batch, seqlen_q, seqlen_k] - attention_scores = attention_scores.sum(dim=1) -+ index_mask = torch.full( -+ (b, sq_local, sk_global), float("-inf"), dtype=torch.float32, device=causal_mask.device -+ ).scatter_(-1, topk_indices, 0) -+ + float_mask = float_mask.view(1, 1, sq_local, sk_global) + float_mask = index_mask.view(b, 1, sq_local, sk_global) + float_mask if sparse_loss else float_mask + @@ -506,7 +635,7 @@ index fc994490b..7bc9a485e 100644 # [batch, seqlen, index_topk] topk_indices = index_scores.topk(topk_k, dim=-1)[1] -@@ -687,6 +780,57 @@ def unfused_dsa_fn(query, key, value, topk_indices, softmax_scale): +@@ -687,6 +780,48 @@ def unfused_dsa_fn(query, key, value, topk_indices, softmax_scale): output = output.reshape(sq, b, np * hnv) return output @@ -537,34 +666,56 @@ index fc994490b..7bc9a485e 100644 + + return causal_mask + -+def unfused_dsa_fn_with_cp(query, key, value, topk_indices, softmax_scale): ++def unfused_dsa_fn_with_cp(query, key, dim_v, topk_indices, softmax_scale): + pg = parallel_state.get_context_parallel_group() -+ cp_size = parallel_state.get_context_parallel_world_size() -+ cp_rank = parallel_state.get_context_parallel_rank() -+ + sq, b, np, hn = query.size() + skv = key.size(0) -+ hnv = value.size(3) -+ -+ skv_global = skv * cp_size -+ -+ sparse_mask = torch.ones((b, sq, skv_global), dtype=torch.bool, device=query.device) -+ sparse_mask.scatter_(-1, topk_indices, False) + -+ causal_mask = get_causal_mask(sq, skv, query.device) -+ -+ combined_mask = sparse_mask | causal_mask.unsqueeze(0) -+ -+ attention_mask_for_cp = combined_mask.unsqueeze(1) # [b, 1, sq, skv_global] ++ topk = topk_indices.shape[-1] ++ topk_indices = topk_indices.unsqueeze(1) ++ topk_indices = topk_indices.expand(-1, key.shape[2], -1, -1).contiguous().to(torch.int32) ++ causal_masks = get_causal_mask(sq, skv, query.device) ++ causal_masks = causal_masks[None, None, :, :] ++ causal_masks = causal_masks.expand(b, key.shape[2], -1, -1).contiguous() + output = AttentionFuncionWithContextParallel.apply( -+ query, key, value, attention_mask_for_cp, 0.0, softmax_scale, pg, True ++ query, key, dim_v, topk_indices, causal_masks, 0.0, softmax_scale, pg + ) -+ return output.reshape(sq, b, np * hnv) -+ ++ return output.reshape(sq, b, np * dim_v) class DSAttention(MegatronModule): """ -@@ -768,18 +912,17 @@ class DSAttention(MegatronModule): +@@ -729,7 +864,6 @@ class DSAttention(MegatronModule): + self, + query: torch.Tensor, + key: torch.Tensor, +- value: torch.Tensor, + x: torch.Tensor, + qr: torch.Tensor, + attention_mask: torch.Tensor, +@@ -743,7 +877,6 @@ class DSAttention(MegatronModule): + Args: + query: Query tensor [sq, b, np, hn]. + key: Key tensor [skv, b, np, hn]. +- value: Value tensor [skv, b, np, hnv]. + x: Original hidden states [sq, b, hidden_size]. + qr: Low-rank query representation [sq, b, q_lora_rank]. + attention_mask: Attention mask tensor [b, 1, sq, sk]. +@@ -754,9 +887,11 @@ class DSAttention(MegatronModule): + Returns: + output: Output tensor [sq, b, hidden_size] + """ +- sq, b, np, hn = query.size() +- skv = key.size(0) +- hnv = value.size(3) ++ dim_v = self.config.kv_lora_rank ++ # torch.Size([128, 1, 64, 576]) ++ sq, b, nheads, dim = query.size() ++ # torch.Size([128, 1, 1, 576]) ++ skv, _, kv_groups, _ = key.shape + + # Detach x and qr to prevent gradients of indexer from flowing back to the main model. + x = x.detach() +@@ -768,18 +903,17 @@ class DSAttention(MegatronModule): # Generate upper triangular mask with -inf above diagonal, 0 elsewhere # torch.triu with diagonal=1 creates upper triangular matrix (excluding main diagonal) # float_mask [sq, skv] @@ -591,17 +742,42 @@ index fc994490b..7bc9a485e 100644 # =================================== # Get index scores and top-k indices -@@ -791,7 +934,7 @@ class DSAttention(MegatronModule): +@@ -791,32 +925,6 @@ class DSAttention(MegatronModule): # =================================== # Run sparse attention kernel # =================================== - output = unfused_dsa_fn(query, key, value, topk_indices, self.softmax_scale) -+ output = unfused_dsa_fn_with_cp(query, key, value, topk_indices, self.softmax_scale) +- +- # =================================== +- # Attach indexer loss +- # =================================== +- if self.training and torch.is_grad_enabled(): +- # Compute KL divergence loss between indexer scores and true attention scores +- indexer_loss_coeff = getattr(self.config, 'dsa_indexer_loss_coeff', 0.0) +- indexer_loss = compute_dsa_indexer_loss( +- index_scores, +- topk_indices, +- query.detach(), +- key.detach(), +- self.softmax_scale, +- indexer_loss_coeff, +- getattr(self.config, "dsa_indexer_use_sparse_loss", False), +- self.indexer.pg_collection, +- ) +- # Save indexer loss for logging +- if indexer_loss_coeff > 0: +- DSAIndexerLossLoggingHelper.save_loss_to_tracker( +- loss=indexer_loss, +- layer_number=self.layer_number, +- num_layers=self.config.num_layers, +- ) +- # Attach loss to output +- output = DSAIndexerLossAutoScaler.apply(output, indexer_loss) ++ output = unfused_dsa_fn_with_cp(query, key, dim_v, topk_indices, self.softmax_scale) - # =================================== - # Attach indexer loss + return output diff --git a/megatron/core/transformer/multi_latent_attention.py b/megatron/core/transformer/multi_latent_attention.py -index 3953d933b..84301ed54 100644 +index 3953d933b..7d030ad02 100644 --- a/megatron/core/transformer/multi_latent_attention.py +++ b/megatron/core/transformer/multi_latent_attention.py @@ -6,6 +6,7 @@ from dataclasses import dataclass @@ -612,6 +788,692 @@ index 3953d933b..84301ed54 100644 try: from einops import rearrange +@@ -167,6 +168,7 @@ class MultiLatentAttention(Attention): + ) + + # Output. ++ # SP_Reduce scatter + TP_Row_par + self.linear_proj = build_module( + submodules.linear_proj, + self.query_projection_size, +@@ -311,7 +313,6 @@ class MultiLatentAttention(Attention): + core_attn_out = self.core_attention( + query, + key, +- value, + x=hidden_states, + qr=q_compressed, + attention_mask=attention_mask, +@@ -370,6 +371,19 @@ class MultiLatentAttention(Attention): + self.qkv_up_checkpoint.discard_output_and_register_recompute(core_attn_out) + self.qkv_up_checkpoint = None + ++ s_, b_ = core_attn_out.size(0), core_attn_out.size(1) ++ core_attn_out = core_attn_out.view( ++ s_, b_, ++ self.num_attention_heads_per_partition, ++ self.config.kv_lora_rank ++ ) ++ ++ # einsum: "sbhk,hdk->sbhd" ++ core_attn_out = torch.einsum("sbhk,hdk->sbhd", core_attn_out, self.up_v_weight_) ++ core_attn_out = core_attn_out.contiguous() ++ core_attn_out = core_attn_out.view(s_, b_, -1) ++ core_attn_out = core_attn_out.contiguous() ++ + # ================= + # Output. [sq, b, h] + # ================= +@@ -384,7 +398,6 @@ class MultiLatentAttention(Attention): + + return output, bias + +- + class MLASelfAttention(MultiLatentAttention): + """MLA Self-attention layer class + +@@ -753,7 +766,6 @@ class MLASelfAttention(MultiLatentAttention): + # [num_tokens, qk_pos_emb_head_dim] -> [num_tokens, 1, qk_pos_emb_head_dim] + k_pos_emb = torch.unsqueeze(k_pos_emb, -2) + +- # todo add assert about fusions and caching + if self.config.apply_rope_fusion: + cp_rank = self.pg_collection.cp.rank() + cp_size = self.pg_collection.cp.size() +@@ -844,6 +856,98 @@ class MLASelfAttention(MultiLatentAttention): + value = value.contiguous() + + return query, key, value ++ ++ def mla_absorb(q_compressed, kv_compressed, k_pos_emb, rotary_pos_emb): ++ if self.config.q_lora_rank is not None: ++ # q_compressed: [num_tokens, q_lora_rank] ++ # q: [num_tokens, n * (qk_head_dim + qk_pos_emb_head_dim)] ++ q, _ = self.linear_q_up_proj(q_compressed) ++ else: ++ # q_compressed: [num_tokens, hidden_size] ++ # q: [num_tokens, n * (qk_head_dim + qk_pos_emb_head_dim)] ++ q, _ = self.linear_q_proj(q_compressed) ++ ++ # q: [num_tokens, n, q_head_dim] ++ q = q.view(*q.size()[:-1], self.num_attention_heads_per_partition, self.q_head_dim) ++ ++ # [num_tokens, qk_pos_emb_head_dim] -> [num_tokens, 1, qk_pos_emb_head_dim] ++ k_pos_emb = torch.unsqueeze(k_pos_emb, -2) ++ ++ if self.config.apply_rope_fusion: ++ raise NotImplementedError( ++ "RoPE fusion is not yet supported with absorption training. " ++ "Please set apply_rope_fusion=False." ++ ) ++ else: ++ q_len = q.size()[0] ++ if inference_context is not None: ++ # add offset to the sequence start for inference ++ sequence_start = inference_context.sequence_len_offset ++ sequence_end = sequence_start + q_len ++ rotary_pos_emb = rotary_pos_emb[sequence_start:sequence_end] ++ elif packed_seq_params is None or self.config.context_parallel_size == 1: ++ rotary_pos_emb = rotary_pos_emb[0:q_len] ++ ++ # q_no_pe: [num_tokens, n, qk_head_dim] ++ # q_pos_emb: [num_tokens, n, qk_pos_emb_head_dim] ++ q_no_pe, q_pos_emb = torch.split( ++ q, [self.config.qk_head_dim, self.config.qk_pos_emb_head_dim], dim=-1 ++ ) ++ ++ # q_no_pe: [num_tokens, n, qk_head_dim] ++ # up_k_weight: [n, qk_head_dim, kv_lora_rank] ++ # q_absorbed: [num_tokens, n, kv_lora_rank] ++ q_absorbed = torch.einsum("...hd,hdk->...hk", q_no_pe, self.up_k_weight_) ++ ++ # TODO: Does it match ZZ? SP does not need but CP needs ++ if self.config.sequence_parallel: ++ kv_compressed = gather_from_sequence_parallel_region(kv_compressed) ++ ++ # kv_compressed: [num_tokens, kv_lora_rank] ++ if kv_compressed.ndim == 3: # [s, b, kv_lora_rank] ++ k_content = kv_compressed.unsqueeze(2).expand( ++ -1, -1, 1, -1 ++ ) ++ else: # [t, kv_lora_rank] for packed sequence ++ k_content = kv_compressed.unsqueeze(1).expand( ++ -1, 1, -1 ++ ) ++ ++ # q_pos_emb: [num_tokens, n, qk_pos_emb_head_dim] ++ q_pos_emb = apply_rotary_pos_emb( ++ q_pos_emb, ++ rotary_pos_emb, ++ config=self.config, ++ cu_seqlens=cu_seqlens_q, ++ mscale=mscale, ++ cp_group=self.pg_collection.cp, ++ ) ++ # k_pos_emb: [num_tokens, 1, qk_pos_emb_head_dim] ++ k_pos_emb = apply_rotary_pos_emb( ++ k_pos_emb, ++ rotary_pos_emb, ++ config=self.config, ++ cu_seqlens=cu_seqlens_kv, ++ mscale=mscale, ++ cp_group=self.pg_collection.cp, ++ ) ++ ++ # query: [num_tokens, n, kv_lora_rank + qk_pos_emb_head_dim] ++ query = torch.cat([q_absorbed, q_pos_emb], dim=-1) ++ ++ # key: [num_tokens, n, kv_lora_rank + qk_pos_emb_head_dim] ++ if k_pos_emb.ndim == 4: ++ k_pos_emb = k_pos_emb.expand(-1, -1, 1, -1) ++ else: ++ assert k_pos_emb.ndim == 3 ++ k_pos_emb = k_pos_emb.expand(-1, 1, -1) ++ ++ key = torch.cat([k_content, k_pos_emb], dim=-1) ++ ++ query = query.contiguous() ++ key = key.contiguous() ++ ++ return query, key + + if self.recompute_up_proj: + quantization = self.config.fp8 or self.config.fp4 +@@ -860,9 +964,10 @@ class MLASelfAttention(MultiLatentAttention): + q_compressed, kv_compressed, k_pos_emb, rotary_pos_emb + ) + else: +- query, key, value = qkv_up_proj_and_rope_apply( ++ query, key = mla_absorb( + q_compressed, kv_compressed, k_pos_emb, rotary_pos_emb + ) ++ value = None + + if return_compressed_tensors: + return query, key, value, q_compressed, kv_compressed +@@ -1104,5 +1209,27 @@ class MLASelfAttention(MultiLatentAttention): + * (self.config.qk_head_dim + self.config.v_head_dim), + -1, + ) +- + return weight_kv_updated ++ ++ @property ++ def up_k_weight_(self): ++ # linear_kv_up_proj.weight: [num_heads_per_partition * (qk_head_dim + v_head_dim), kv_lora_rank] ++ weight = self.linear_kv_up_proj.weight ++ weight_reshaped = weight.view( ++ self.num_attention_heads_per_partition, ++ self.config.qk_head_dim + self.config.v_head_dim, ++ self.config.kv_lora_rank, ++ ) ++ # [num_heads_per_partition, qk_head_dim, kv_lora_rank] ++ return weight_reshaped[:, :self.config.qk_head_dim, :] ++ ++ @property ++ def up_v_weight_(self): ++ weight = self.linear_kv_up_proj.weight ++ weight_reshaped = weight.view( ++ self.num_attention_heads_per_partition, ++ self.config.qk_head_dim + self.config.v_head_dim, ++ self.config.kv_lora_rank, ++ ) ++ # [num_heads_per_partition, v_head_dim, kv_lora_rank] ++ return weight_reshaped[:, self.config.qk_head_dim:, :] +diff --git a/megatron/core/transformer/tilelang_kernel/__init__.py b/megatron/core/transformer/tilelang_kernel/__init__.py +new file mode 100644 +index 000000000..d8f2425f0 +--- /dev/null ++++ b/megatron/core/transformer/tilelang_kernel/__init__.py +@@ -0,0 +1,10 @@ ++# Code is adopted from tilelang/examples/deepseek_v32 ++# transformer/tilelang_kernel/__init__.py ++ ++from .sparse_mla_fwd import sparse_mla_fwd_interface ++from .sparse_mla_bwd import sparse_mla_bwd ++ ++__all__ = [ ++ "sparse_mla_fwd_interface", ++ "sparse_mla_bwd", ++] +diff --git a/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py b/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py +new file mode 100644 +index 000000000..b8ea416dd +--- /dev/null ++++ b/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py +@@ -0,0 +1,274 @@ ++# ruff: noqa ++import tilelang ++from tilelang import language as T ++import torch ++ ++ ++@tilelang.jit(out_idx=[-1]) ++def preprocess( ++ B, ++ S, ++ H, ++ D, ++ block_ND=32, ++ num_stages=5, ++ dtype=T.bfloat16, ++ accum_dtype=T.float32, ++): ++ assert dtype == T.bfloat16 ++ assert accum_dtype == T.float32 ++ shape = [B, S, H, D] ++ ++ @T.prim_func ++ def preprocess_kernel( ++ O: T.Tensor(shape, dtype), ++ dO: T.Tensor(shape, dtype), ++ Delta: T.Tensor([B, S, H], accum_dtype), ++ ): ++ with T.Kernel(H, T.ceildiv(S, block_ND), B) as (bx, by, bz): ++ o = T.alloc_fragment([block_ND, block_ND], accum_dtype) ++ do = T.alloc_fragment([block_ND, block_ND], accum_dtype) ++ delta = T.alloc_fragment([block_ND], accum_dtype) ++ acc = T.alloc_fragment([block_ND, block_ND], accum_dtype) ++ T.clear(acc) ++ for k in T.Pipelined(T.ceildiv(D, block_ND), num_stages=num_stages): ++ T.copy(O[bz, by * block_ND : (by + 1) * block_ND, bx, k * block_ND : (k + 1) * block_ND], o) ++ T.copy(dO[bz, by * block_ND : (by + 1) * block_ND, bx, k * block_ND : (k + 1) * block_ND], do) ++ for i, j in T.Parallel(block_ND, block_ND): ++ acc[i, j] += o[i, j] * do[i, j] ++ T.reduce_sum(acc, delta, 1) ++ T.copy(delta, Delta[bz, by * block_ND : (by + 1) * block_ND, bx]) ++ ++ return preprocess_kernel ++ ++ ++@tilelang.jit(out_idx=[-1]) ++def postprocess( ++ B, ++ S_kv, ++ D, ++ D_tail, ++ kv_group=1, ++ block_N=64, ++ threads=256, ++ dtype=T.bfloat16, ++ accum_dtype=T.float32, ++): ++ assert dtype == T.bfloat16 ++ assert accum_dtype == T.float32 ++ dkv_shape = [B, S_kv, kv_group, D + D_tail] ++ ++ @T.prim_func ++ def postprocess_kernel( ++ dKV: T.Tensor(dkv_shape, accum_dtype), ++ dKV_out: T.Tensor(dkv_shape, dtype), ++ ): ++ with T.Kernel(T.ceildiv(S_kv, block_N), kv_group, B, threads=threads) as (bx, by, bz): ++ T.copy( ++ dKV[bz, bx * block_N : (bx + 1) * block_N, by, :], ++ dKV_out[bz, bx * block_N : (bx + 1) * block_N, by, :], ++ ) ++ ++ return postprocess_kernel ++ ++ ++@tilelang.jit( ++ out_idx=[-2], ++ pass_configs={ ++ tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True, ++ tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True, ++ tilelang.PassConfigKey.TL_ENABLE_AGGRESSIVE_SHARED_MEMORY_MERGE: True, ++ }, ++) ++def bwd( ++ B, ++ S, ++ S_kv, ++ H, ++ D, ++ D_tail, ++ topk, ++ kv_group=1, ++ sm_scale=None, ++ is_causal=True, ++ block_size=32, ++ num_stages=0, ++ threads=128, ++ indices_dtype=T.int32, ++ dtype=T.bfloat16, ++ accum_dtype=T.float32, ++ masks_dtype=T.bool, ++): ++ assert is_causal == True, "non-casual is not supported now" ++ assert topk % block_size == 0, "otherwise will load some index=0 thus causing wrong kv to be loaded" ++ assert dtype == T.bfloat16 ++ assert accum_dtype == T.float32 ++ assert indices_dtype == T.int32 ++ ++ if sm_scale is None: ++ sm_scale = (D + D_tail) ** (-0.5) ++ sm_scale_mul_reciprocal_log2 = sm_scale * 1.44269504 # log2(e) ++ ++ H_kv = H // kv_group ++ q_shape = [B, S, H, D + D_tail] ++ k_shape = [B, S_kv, kv_group, D + D_tail] ++ o_shape = [B, S, H, D] ++ indices_shape = [B, S, kv_group, topk] ++ delta_shape = [B, S, H] ++ lse_shape = [B, S, H] ++ masks_shape = [B, S, kv_group, S_kv] ++ assert indices_dtype == T.int32 ++ assert dtype == T.bfloat16 ++ assert accum_dtype == T.float32 ++ ++ H = H_kv ++ padded_H = max(tilelang.math.next_power_of_2(H_kv), 16) ++ block_H = min(64, padded_H) ++ assert padded_H % block_H == 0 ++ NH = padded_H // block_H ++ BS = block_size ++ NS = tilelang.cdiv(topk, block_size) ++ ++ split_store = 2 ++ ++ @T.prim_func ++ def sparse_mla_bwd_kernel( ++ Q: T.Tensor(q_shape, dtype), ++ KV: T.Tensor(k_shape, dtype), ++ dO: T.Tensor(o_shape, dtype), ++ Indices: T.Tensor(indices_shape, indices_dtype), ++ Masks: T.Tensor(masks_shape, masks_dtype), ++ Lse: T.Tensor(lse_shape, accum_dtype), ++ Delta: T.Tensor(delta_shape, accum_dtype), ++ dQ: T.Tensor(q_shape, dtype), ++ dKV: T.Tensor(k_shape, accum_dtype), ++ ): ++ with T.Kernel(S, B, kv_group * NH, threads=threads) as (s_i, by, bz): ++ Q_shared = T.alloc_shared([block_H, D], dtype) ++ Q_tail_shared = T.alloc_shared([block_H, D_tail], dtype) ++ KV_shared = T.alloc_shared([BS, D], dtype) ++ KV_tail_shared = T.alloc_shared([BS, D_tail], dtype) ++ dO_shared = T.alloc_shared([block_H, D], dtype) ++ mask = T.alloc_fragment([BS], "bool") ++ ++ P_shared_cast = T.alloc_shared([block_H, BS], dtype) ++ dP_shared_cast = T.alloc_shared([block_H, BS], dtype) ++ dQ_shared = T.alloc_shared([block_H, D], dtype) ++ dQ_tail_shared = T.alloc_shared([block_H, D_tail], dtype) ++ ++ acc_p = T.alloc_fragment([block_H, BS], accum_dtype) ++ acc_dp = T.alloc_fragment([block_H, BS], accum_dtype) ++ acc_dq = T.alloc_fragment([block_H, D], accum_dtype) ++ acc_dq_tail = T.alloc_fragment([block_H, D_tail], accum_dtype) ++ acc_dkv = T.alloc_fragment([BS, D], accum_dtype) ++ acc_dkv_tail = T.alloc_fragment([BS, D_tail], accum_dtype) ++ acc_dkv_shared = T.alloc_shared([BS // split_store, D], accum_dtype) ++ acc_dkv_tail_shared = T.alloc_shared([BS // split_store, D_tail], accum_dtype) ++ ++ T.copy(Q[by, s_i, bz * block_H : (bz + 1) * block_H, :D], Q_shared) ++ T.copy(Q[by, s_i, bz * block_H : (bz + 1) * block_H, D:], Q_tail_shared) ++ T.copy(dO[by, s_i, bz * block_H : (bz + 1) * block_H, :D], dO_shared) ++ ++ T.clear(acc_dq) ++ T.clear(acc_dq_tail) ++ ++ # Process each block of indices ++ for i_i in T.Pipelined(NS, num_stages=num_stages): ++ # Compute attention scores ++ for bi_i in T.Parallel(BS): ++ mask[bi_i] = Masks[by, s_i, bz // NH, Indices[by, s_i, bz // NH, i_i * BS + bi_i]] ++ ++ for h_i, bi_i in T.Parallel(block_H, BS): ++ acc_p[h_i, bi_i] = T.if_then_else(mask[bi_i], -T.infinity(acc_p.dtype), 0) ++ ++ # Load KV, V for this block of indices ++ for bi_i, d_i in T.Parallel(BS, D): ++ KV_shared[bi_i, d_i] = KV[by, Indices[by, s_i, bz // NH, i_i * BS + bi_i], bz // NH, d_i] ++ ++ T.gemm(Q_shared, KV_shared, acc_p, transpose_B=True, policy=T.GemmWarpPolicy.FullCol) ++ ++ for bi_i, d_i in T.Parallel(BS, D_tail): ++ KV_tail_shared[bi_i, d_i] = KV[by, Indices[by, s_i, bz // NH, i_i * BS + bi_i], bz // NH, D + d_i] ++ T.gemm(Q_tail_shared, KV_tail_shared[:, :D_tail], acc_p, transpose_B=True, policy=T.GemmWarpPolicy.FullCol) ++ ++ for h_i, bi_i in T.Parallel(block_H, BS): ++ acc_p[h_i, bi_i] = T.exp2(acc_p[h_i, bi_i] * sm_scale_mul_reciprocal_log2 - Lse[by, s_i, bz * block_H + h_i]) ++ ++ T.copy(acc_p, P_shared_cast) ++ ++ T.gemm(dO_shared, KV_shared, acc_dp, transpose_B=True, policy=T.GemmWarpPolicy.FullCol, clear_accum=True) ++ ++ for h_i, bi_i in T.Parallel(block_H, BS): ++ acc_dp[h_i, bi_i] = acc_p[h_i, bi_i] * (acc_dp[h_i, bi_i] - Delta[by, s_i, bz * block_H + h_i]) * sm_scale ++ ++ T.copy(acc_dp, dP_shared_cast) ++ T.gemm(dP_shared_cast, KV_shared, acc_dq, policy=T.GemmWarpPolicy.FullCol) ++ T.gemm(dP_shared_cast, KV_tail_shared, acc_dq_tail, policy=T.GemmWarpPolicy.FullCol) ++ ++ T.gemm(dP_shared_cast, Q_shared, acc_dkv, transpose_A=True, policy=T.GemmWarpPolicy.FullCol, clear_accum=True) ++ T.gemm(P_shared_cast, dO_shared, acc_dkv, transpose_A=True, policy=T.GemmWarpPolicy.FullCol) ++ ++ T.clear(acc_dkv_tail) ++ T.gemm(dP_shared_cast, Q_tail_shared, acc_dkv_tail, transpose_A=True, policy=T.GemmWarpPolicy.FullCol) ++ ++ for s in range(split_store): ++ for bi_i, d_i in T.Parallel(BS, D): ++ if bi_i < BS // split_store: ++ acc_dkv_shared[bi_i, d_i] = acc_dkv[bi_i + s * (BS // split_store), d_i] ++ ++ for bi_i, d_i in T.Parallel(BS, D_tail): ++ if bi_i < BS // split_store: ++ acc_dkv_tail_shared[bi_i, d_i] = acc_dkv_tail[bi_i + s * (BS // split_store), d_i] ++ ++ for bi_i, d_i in T.Parallel(BS // split_store, D // 4): ++ T.atomic_addx4( ++ dKV[by, Indices[by, s_i, bz // NH, i_i * BS + bi_i + s * (BS // split_store)], bz // NH, d_i * 4], ++ acc_dkv_shared[bi_i, d_i * 4], ++ ) ++ ++ # Atomically update dKV, dKV_tail tensors ++ for bi_i, d_i in T.Parallel(BS // split_store, D_tail // 4): ++ T.atomic_addx4( ++ dKV[by, Indices[by, s_i, bz // NH, i_i * BS + bi_i + s * (BS // split_store)], bz // NH, D + d_i * 4], ++ acc_dkv_tail_shared[bi_i, d_i * 4], ++ ) ++ ++ # Store the accumulated dQ ++ T.copy(acc_dq, dQ_shared) ++ T.copy(acc_dq_tail[:, :D_tail], dQ_tail_shared) ++ ++ T.copy(dQ_shared, dQ[by, s_i, bz * block_H : (bz + 1) * block_H, :D]) ++ T.copy(dQ_tail_shared, dQ[by, s_i, bz * block_H : (bz + 1) * block_H, D:]) ++ ++ return sparse_mla_bwd_kernel ++ ++ ++def sparse_mla_bwd(q, kv, o, do, indices, masks, lse, dim_v, sm_scale=None, is_casual=True, return_kernel=False, delta=None): ++ assert q.is_contiguous() ++ assert kv.is_contiguous() ++ assert indices.is_contiguous() ++ assert lse.is_contiguous() ++ B, S, H, dim_plus_tail_dim = q.shape ++ _, S_kv, kv_group, _ = kv.shape ++ assert kv.shape[-1] == dim_plus_tail_dim ++ assert kv.shape[0] == B ++ # dim should be assigned ++ D = dim_v ++ ++ D_tail = dim_plus_tail_dim - D ++ topk = indices.shape[-1] ++ assert indices.shape == (B, S, kv_group, topk) ++ assert lse.shape == (B, S, H) ++ ++ # Get kernels ++ preprocess_kernel = preprocess(B, S, H, D) ++ bwd_kernel = bwd(B, S, S_kv, H, D, D_tail, topk, kv_group, sm_scale, is_casual) ++ postprocess_kernel = postprocess(B, S_kv, D, D_tail, kv_group) ++ ++ if delta is None: ++ delta = preprocess_kernel(o, do) ++ dkv = torch.zeros_like(kv, dtype=torch.float32) ++ dq = bwd_kernel(q, kv, do, indices, masks, lse, delta, dkv) ++ dkv = postprocess_kernel(dkv) ++ ++ return dq, dkv +\ No newline at end of file +diff --git a/megatron/core/transformer/tilelang_kernel/sparse_mla_fwd.py b/megatron/core/transformer/tilelang_kernel/sparse_mla_fwd.py +new file mode 100644 +index 000000000..d338a2fa6 +--- /dev/null ++++ b/megatron/core/transformer/tilelang_kernel/sparse_mla_fwd.py +@@ -0,0 +1,190 @@ ++# ruff: noqa ++import torch ++import tilelang ++from tilelang import language as T ++ ++ ++@tilelang.jit( ++ out_idx=[-2, -1], ++ pass_configs={ ++ tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True, ++ tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True, ++ }, ++) ++def sparse_mla_fwd( ++ heads, ++ dim, ++ tail_dim, ++ topk, ++ kv_group=1, ++ sm_scale=None, ++ is_causal=True, ++ CP0=True, ++ block_I=64, ++ num_stages=2, ++ threads=256, ++): ++ assert dim == tilelang.math.next_power_of_2(dim), f"haven't check padding correctness yet, dim={dim}" ++ assert tail_dim == tilelang.math.next_power_of_2(tail_dim), f"haven't check padding correctness yet, dim={tail_dim}" ++ assert is_causal == True, "non-casual is not supported" ++ assert topk % block_I == 0, "otherwise will load some index=0 thus causing wrong kv to be loaded" ++ if sm_scale is None: ++ sm_scale = (1.0 / (dim + tail_dim)) ** 0.5 * 1.44269504 # log2(e) ++ else: ++ sm_scale = sm_scale * 1.44269504 # log2(e) ++ ++ batch = T.dynamic("batch") ++ seq_len = T.dynamic("seq_len") ++ seq_len_kv = T.dynamic("seq_len_kv") ++ ++ head_kv = heads // kv_group ++ q_shape = [batch, seq_len, heads, dim + tail_dim] ++ kv_shape = [batch, seq_len_kv, kv_group, dim + tail_dim] ++ o_shape = [batch, seq_len, heads, dim] ++ indices_shape = [batch, seq_len, kv_group, topk] ++ lse_shape = [batch, seq_len, heads] ++ masks_shape = [batch, seq_len, kv_group, seq_len_kv] ++ ++ masks_dtype = T.bool ++ indices_dtype = T.int32 ++ dtype = T.bfloat16 ++ accum_dtype = T.float32 ++ ++ G = kv_group ++ H = head_kv ++ padded_H = max(tilelang.math.next_power_of_2(head_kv), 16) ++ if padded_H != H: ++ assert kv_group == 1, ( ++ "here we solve the H padding automatically, other wise you should handle Q copy and Output copy with your mask (when kv_group == 1, use g_i * padded_H:(g_i+1) * padded_H would be handled automatically)" ++ ) ++ BI = block_I ++ NI = tilelang.cdiv(topk, block_I) ++ D = dim ++ D_tail = tail_dim ++ ++ if head_kv > 64: ++ assert head_kv % 64 == 0, "head_kv should be a multiple of 64" ++ REPLICATE_H = head_kv // 64 ++ else: ++ REPLICATE_H = 1 ++ ++ H_per_block = padded_H if REPLICATE_H == 1 else 64 ++ ++ @T.prim_func ++ def main( ++ Q: T.Tensor(q_shape, dtype), # type: ignore ++ KV: T.Tensor(kv_shape, dtype), # type: ignore ++ Indices: T.Tensor(indices_shape, indices_dtype), # type: ignore ++ Masks: T.Tensor(masks_shape, masks_dtype), # type: ignore ++ Output: T.Tensor(o_shape, dtype), # type: ignore ++ Lse: T.Tensor(lse_shape, accum_dtype), # type: ignore ++ ): ++ with T.Kernel(seq_len * REPLICATE_H, batch, kv_group, threads=threads) as ( ++ bx, ++ by, ++ bz, ++ ): ++ Q_shared = T.alloc_shared([H_per_block, D], dtype) ++ Q_tail_shared = T.alloc_shared([H_per_block, D_tail], dtype) ++ KV_shared = T.alloc_shared([BI, D], dtype) ++ K_tail_shared = T.alloc_shared([BI, D_tail], dtype) ++ O_shared = T.alloc_shared([H_per_block, D], dtype) ++ Lse_shared = T.alloc_shared([H_per_block], accum_dtype) ++ mask = T.alloc_fragment([BI], "bool") ++ ++ acc_o = T.alloc_fragment([H_per_block, D], accum_dtype) ++ acc_s = T.alloc_fragment([H_per_block, BI], accum_dtype) ++ S_shared = T.alloc_shared([H_per_block, BI], dtype) ++ sumexp = T.alloc_fragment([H_per_block], accum_dtype) ++ sumexp_i = T.alloc_fragment([H_per_block], accum_dtype) ++ alpha = T.alloc_fragment([H_per_block], accum_dtype) ++ m_i = T.alloc_fragment([H_per_block], accum_dtype) ++ m_i_prev = T.alloc_fragment([H_per_block], accum_dtype) ++ ++ T.fill(acc_o, 0) ++ T.fill(sumexp, 0) ++ T.fill(m_i, -(2**30)) # avoid -inf - inf to cause nan ++ ++ b_i, g_i = by, bz ++ s_i = bx if REPLICATE_H == 1 else (bx // REPLICATE_H) ++ q_i = s_i ++ ++ H0 = g_i * padded_H + (0 if REPLICATE_H == 1 else (bx % REPLICATE_H) * 64) ++ H1 = H0 + H_per_block ++ ++ T.copy(Q[b_i, s_i, H0:H1, :D], Q_shared) ++ T.copy(Q[b_i, s_i, H0:H1, D:], Q_tail_shared) ++ ++ for i_i in T.Pipelined(NI, num_stages=num_stages): ++ for bi_i in T.Parallel(BI): ++ mask[bi_i] = Masks[b_i, s_i, g_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i]] ++ for bi_i, d_i in T.Parallel(BI, D): ++ KV_shared[bi_i, d_i] = KV[b_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i], g_i, d_i] ++ for bi_i, d_i in T.Parallel(BI, D_tail): ++ K_tail_shared[bi_i, d_i] = KV[b_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i], g_i, D + d_i] ++ for h_i, bi_i in T.Parallel(H_per_block, BI): ++ acc_s[h_i, bi_i] = T.if_then_else(mask[bi_i], -T.infinity(acc_s.dtype), 0) ++ T.gemm( ++ Q_shared, ++ KV_shared, ++ acc_s, ++ transpose_B=True, ++ policy=T.GemmWarpPolicy.FullRow, ++ ) ++ T.gemm( ++ Q_tail_shared, ++ K_tail_shared, ++ acc_s, ++ transpose_B=True, ++ policy=T.GemmWarpPolicy.FullRow, ++ ) ++ T.copy(m_i, m_i_prev) ++ T.reduce_max(acc_s, m_i, dim=1, clear=False) ++ for h_i in T.Parallel(H_per_block): ++ m_i[h_i] = T.max(m_i[h_i], m_i_prev[h_i]) ++ for h_i in T.Parallel(H_per_block): ++ alpha[h_i] = T.exp2((m_i_prev[h_i] - m_i[h_i]) * sm_scale) ++ for h_i, bi_i in T.Parallel(H_per_block, BI): ++ acc_s[h_i, bi_i] = T.exp2(acc_s[h_i, bi_i] * sm_scale - m_i[h_i] * sm_scale) ++ T.reduce_sum(acc_s, sumexp_i, dim=1) # is this a accumulate operator? ++ for h_i in T.Parallel(H_per_block): ++ sumexp[h_i] = sumexp[h_i] * alpha[h_i] + sumexp_i[h_i] ++ for h_i, d_i in T.Parallel(H_per_block, D): ++ acc_o[h_i, d_i] = acc_o[h_i, d_i] * alpha[h_i] ++ ++ T.copy(acc_s, S_shared) ++ T.gemm(S_shared, KV_shared, acc_o, policy=T.GemmWarpPolicy.FullRow) ++ ++ # Rescale ++ for h_i, d_i in T.Parallel(H_per_block, D): ++ acc_o[h_i, d_i] /= sumexp[h_i] ++ for h_i in T.Parallel(H_per_block): ++ sumexp[h_i] = T.log2(sumexp[h_i]) + m_i[h_i] * sm_scale ++ ++ T.copy(acc_o, O_shared) ++ T.copy(acc_o, Output[b_i, s_i, H0:H1, :]) ++ T.copy(sumexp, Lse_shared) ++ T.copy(sumexp, Lse[b_i, s_i, H0:H1]) ++ ++ return main ++ ++ ++def sparse_mla_fwd_interface(q, kv, indices, masks, d_v, sm_scale=None, return_p_sum: bool = False, block_I=64, num_stages=2, threads=256): ++ is_casual = True ++ assert return_p_sum == False, "This kernel file is for fwd only" ++ assert q.is_contiguous() and kv.is_contiguous() and indices.is_contiguous() ++ batch, seq_len, heads, dim_plus_tail_dim = q.shape ++ _, seq_len_kv, kv_group, _ = kv.shape ++ ++ assert kv.shape[-1] == dim_plus_tail_dim ++ tail_dim = dim_plus_tail_dim - d_v ++ assert kv.shape[0] == batch ++ _, _, _, topk = indices.shape ++ assert indices.shape == (batch, seq_len, kv_group, topk) ++ assert masks.shape == (batch, seq_len, kv_group, seq_len_kv) ++ ++ kernel = sparse_mla_fwd( ++ heads, d_v, tail_dim, topk, kv_group, sm_scale, is_casual, block_I=block_I, num_stages=num_stages, threads=threads ++ ) ++ out, lse = kernel(q, kv, indices, masks) ++ return out, lse +\ No newline at end of file diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py index a3a167549..98391fda6 100644 --- a/megatron/core/transformer/transformer_config.py diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py index 87cf24992..da5b2b55a 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_fp8.py @@ -43,7 +43,9 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantizatio if converted_name.endswith("_scale"): continue if_use_ue8m0_in_moe = True if args.sglang_moe_runner_backend == "deep_gemm" else False - quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size, if_use_ue8m0_in_moe=if_use_ue8m0_in_moe)) + quantize_named_params.extend( + _quantize_param(converted_name, param, weight_block_size, if_use_ue8m0_in_moe=if_use_ue8m0_in_moe) + ) return quantize_named_params @@ -92,9 +94,11 @@ def _quantize_param(name, weight, weight_block_size, if_use_ue8m0_in_moe=True): FP8_MIN = torch.finfo(torch.float8_e4m3fn).min FP8_MAX = torch.finfo(torch.float8_e4m3fn).max if weight_block_size is not None: - if should_deepgemm_weight_requant_ue8m0 and should_deepgemm_weight_requant_ue8m0( - weight_block_size=weight_block_size - ) and if_use_ue8m0_in_moe: + if ( + should_deepgemm_weight_requant_ue8m0 + and should_deepgemm_weight_requant_ue8m0(weight_block_size=weight_block_size) + and if_use_ue8m0_in_moe + ): qweight, scale = quant_weight_ue8m0(weight, weight_block_size=weight_block_size) scale = transform_scale_ue8m0(scale, mn=qweight.shape[-2]) else: diff --git a/miles/backends/megatron_utils/update_weight/common.py b/miles/backends/megatron_utils/update_weight/common.py index 558a2e06f..e958566dc 100644 --- a/miles/backends/megatron_utils/update_weight/common.py +++ b/miles/backends/megatron_utils/update_weight/common.py @@ -202,11 +202,17 @@ def _named_params_and_buffers_global( expert_idx = int(expert_idx) + expert_offset yield f"module.module.mtp.layers.{layer_idx}.transformer_layer.mlp.experts.{rest}.weight{expert_idx}", param continue - + # TODO: a hacking here, need to be cleaner - duplicated = ['indexer.linear_weights_proj', 'indexer.linear_wk', 'indexer.linear_wq_b', 'linear_q_down_proj', 'linear_kv_down_proj'] + duplicated = [ + "indexer.linear_weights_proj", + "indexer.linear_wk", + "indexer.linear_wq_b", + "linear_q_down_proj", + "linear_kv_down_proj", + ] if any(dup in name for dup in duplicated): - param.parallel_mode = 'duplicated' + param.parallel_mode = "duplicated" layer_idx, rest = match.groups() layer_idx = int(layer_idx) + layer_offset diff --git a/miles_plugins/mbridge/__init__.py b/miles_plugins/mbridge/__init__.py index cc42522ee..67b824aa9 100644 --- a/miles_plugins/mbridge/__init__.py +++ b/miles_plugins/mbridge/__init__.py @@ -1,6 +1,7 @@ -import sys import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../..")) from .deepseekv32 import DeepseekV32Bridge from .glm4 import GLM4Bridge @@ -14,12 +15,15 @@ _original_from_config = AutoBridge.from_config + @classmethod def _patched_from_config(cls, hf_config, **kwargs): - if hasattr(hf_config, 'index_n_heads'): + if hasattr(hf_config, "index_n_heads"): from mbridge.core.bridge import _MODEL_REGISTRY - return _MODEL_REGISTRY['deepseek_v32'](hf_config, **kwargs) - + + return _MODEL_REGISTRY["deepseek_v32"](hf_config, **kwargs) + return _original_from_config(hf_config, **kwargs) + AutoBridge.from_config = _patched_from_config diff --git a/miles_plugins/mbridge/deepseekv32.py b/miles_plugins/mbridge/deepseekv32.py index aae07ee53..19e417fa0 100644 --- a/miles_plugins/mbridge/deepseekv32.py +++ b/miles_plugins/mbridge/deepseekv32.py @@ -1,6 +1,7 @@ +from megatron.core.transformer.enums import AttnBackend + from mbridge.core import register_model from mbridge.models import DeepseekV3Bridge -from megatron.core.transformer.enums import AttnBackend @register_model("deepseek_v32") @@ -13,49 +14,44 @@ class DeepseekV32Bridge(DeepseekV3Bridge): "self_attention.core_attention.indexer.linear_weights_proj.weight", } - _ATTENTION_MAPPING = ( - DeepseekV3Bridge._ATTENTION_MAPPING.copy() - ) - + _ATTENTION_MAPPING = DeepseekV3Bridge._ATTENTION_MAPPING.copy() + # Because the indexer needs the norm output, we cannot use the fused transformer engine impl and have to compute it separately. if "self_attention.linear_q_up_proj.layer_norm_weight" in _ATTENTION_MAPPING: del _ATTENTION_MAPPING["self_attention.linear_q_up_proj.layer_norm_weight"] if "self_attention.linear_kv_up_proj.layer_norm_weight" in _ATTENTION_MAPPING: del _ATTENTION_MAPPING["self_attention.linear_kv_up_proj.layer_norm_weight"] - - _ATTENTION_MAPPING.update({ - "self_attention.q_layernorm.weight": [ - "model.layers.{layer_number}.self_attn.q_a_layernorm.weight" - ], - "self_attention.kv_layernorm.weight": [ - "model.layers.{layer_number}.self_attn.kv_a_layernorm.weight" - ], - "self_attention.core_attention.indexer.linear_wq_b.weight": [ - "model.layers.{layer_number}.self_attn.indexer.wq_b.weight" - ], - "self_attention.core_attention.indexer.linear_wk.weight": [ - "model.layers.{layer_number}.self_attn.indexer.wk.weight" - ], - "self_attention.core_attention.indexer.k_norm.weight": [ - "model.layers.{layer_number}.self_attn.indexer.k_norm.weight" - ], - "self_attention.core_attention.indexer.k_norm.bias": [ - "model.layers.{layer_number}.self_attn.indexer.k_norm.bias" - ], - "self_attention.core_attention.indexer.linear_weights_proj.weight": [ - "model.layers.{layer_number}.self_attn.indexer.weights_proj.weight" - ], - }) + + _ATTENTION_MAPPING.update( + { + "self_attention.q_layernorm.weight": ["model.layers.{layer_number}.self_attn.q_a_layernorm.weight"], + "self_attention.kv_layernorm.weight": ["model.layers.{layer_number}.self_attn.kv_a_layernorm.weight"], + "self_attention.core_attention.indexer.linear_wq_b.weight": [ + "model.layers.{layer_number}.self_attn.indexer.wq_b.weight" + ], + "self_attention.core_attention.indexer.linear_wk.weight": [ + "model.layers.{layer_number}.self_attn.indexer.wk.weight" + ], + "self_attention.core_attention.indexer.k_norm.weight": [ + "model.layers.{layer_number}.self_attn.indexer.k_norm.weight" + ], + "self_attention.core_attention.indexer.k_norm.bias": [ + "model.layers.{layer_number}.self_attn.indexer.k_norm.bias" + ], + "self_attention.core_attention.indexer.linear_weights_proj.weight": [ + "model.layers.{layer_number}.self_attn.indexer.weights_proj.weight" + ], + } + ) def _build_config(self): config = super()._build_config() - + config.attention_backend = AttnBackend.auto - + config.experimental_attention_variant = "dsa" - config.dsa_indexer_n_heads = getattr(self.hf_config, 'dsa_indexer_n_heads', 64) - config.dsa_indexer_head_dim = getattr(self.hf_config, 'dsa_indexer_head_dim', 128) - config.dsa_indexer_topk = getattr(self.hf_config, 'dsa_indexer_topk', 2048) - - return config + config.dsa_indexer_n_heads = getattr(self.hf_config, "dsa_indexer_n_heads", 64) + config.dsa_indexer_head_dim = getattr(self.hf_config, "dsa_indexer_head_dim", 128) + config.dsa_indexer_topk = getattr(self.hf_config, "dsa_indexer_topk", 2048) + return config diff --git a/scripts/run_deepseek_v32.py b/scripts/run_deepseek_v32.py index 0cbf9ca05..6d76ade86 100644 --- a/scripts/run_deepseek_v32.py +++ b/scripts/run_deepseek_v32.py @@ -5,7 +5,6 @@ import re from dataclasses import dataclass from typing import Literal -from pathlib import Path import typer import miles.utils.external_utils.command_utils as U @@ -282,7 +281,7 @@ def train(args: ScriptArgs): "--use-fault-tolerance " f"--dump-details /root/shared_data/{args.run_id}/dump_details " "--disable-weights-backuper " - "--model-name deepseekv32 " # for mbridge load + "--model-name deepseekv32 " # for mbridge load "--train-memory-margin-bytes 1073741824 " # "--check-weight-update-equal " "--qkv-format bshd " @@ -313,4 +312,4 @@ def train(args: ScriptArgs): if __name__ == "__main__": - app() \ No newline at end of file + app() From 2c3534b66e616d8bd7eb2cd3f07cf241f841e704 Mon Sep 17 00:00:00 2001 From: zhihaow6 Date: Mon, 19 Jan 2026 00:21:41 -0800 Subject: [PATCH 49/57] update --- docker/deepseekv32/megatron.patch | 64 +++++++++++++++++-------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index ac7a1be3c..c1f80e1e5 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -1,8 +1,8 @@ diff --git a/megatron/core/transformer/dot_product_attention_context_parallel.py b/megatron/core/transformer/dot_product_attention_context_parallel.py -index 89659a1d7..1def27c69 100644 +index 89659a1d7..2c1464fb6 100644 --- a/megatron/core/transformer/dot_product_attention_context_parallel.py +++ b/megatron/core/transformer/dot_product_attention_context_parallel.py -@@ -3,9 +3,12 @@ +@@ -3,107 +3,12 @@ # Some of this code was adopted from https://github.com/zhuzilin/ring-flash-attention/ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. @@ -11,14 +11,15 @@ index 89659a1d7..1def27c69 100644 import torch +import torch.distributed as dist from torch.nn import functional as F -+from .tilelang_kernel import sparse_mla_bwd, sparse_mla_fwd_interface - - try: - import einops -@@ -15,96 +18,6 @@ except ImportError: - HAVE_EINOPS = False - - +- +-try: +- import einops +- +- HAVE_EINOPS = True +-except ImportError: +- HAVE_EINOPS = False +- +- -@torch.no_grad -def eager_attn_fwd(q, k, v, attn_bias, sinks, scale, dropout): - """Forward pass for eager attention""" @@ -108,11 +109,11 @@ index 89659a1d7..1def27c69 100644 - grad_q = einops.rearrange(grad__q, 'b h s d -> b s h d') - return grad_q, grad_k, grad_v, grad_sinks - -- ++from .tilelang_kernel import sparse_mla_bwd, sparse_mla_fwd_interface + class AllGatherComm: """All gather communication with async operations""" - -@@ -131,212 +44,146 @@ class AllGatherComm: +@@ -131,212 +36,145 @@ class AllGatherComm: handle.wait() self.handles = [] @@ -146,9 +147,9 @@ index 89659a1d7..1def27c69 100644 '''Forward pass for the native attention function with context parallelism''' - # Assert einops exists - if not HAVE_EINOPS: - raise ImportError("einops is required by the attention CP but cannot be imported.") - +- if not HAVE_EINOPS: +- raise ImportError("einops is required by the attention CP but cannot be imported.") +- - # Initialize communication group and constants cp_size = 1 if pg is not None: @@ -164,8 +165,6 @@ index 89659a1d7..1def27c69 100644 - # Initialize KV buffers - kv_buffer = torch.empty( - (2, k.shape[0] * cp_size, k.shape[1], heads_k_stride, k.shape[3]), -+ s, b, heads, dim = q.shape -+ skv, _, kv_groups, _ = k.shape + + k_buffer = torch.empty( + (k.shape[0] * cp_size, k.shape[1], 1, k.shape[3]), @@ -232,9 +231,9 @@ index 89659a1d7..1def27c69 100644 + k_i = k_buffer + + s_, b_, h_, d_ = q_i.shape -+ q_i = einops.rearrange(q_i, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ q_i = q_i.transpose(0, 1).flatten().view(b_, s_, h_, d_) + s_, b_, h_, d_ = k_i.shape -+ k_i = einops.rearrange(k_i, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ k_i = k_i.transpose(0, 1).flatten().view(b_, s_, h_, d_) + zz_indices_i = zz_indices + b_, s_, g_, topk_ = zz_indices_i.shape + zz_indices_i = zz_indices_i.flatten().view(b_, s_, g_, topk_) @@ -245,7 +244,8 @@ index 89659a1d7..1def27c69 100644 + out_i, lse_i = sparse_mla_fwd_interface(q_i.contiguous(), k_i, zz_indices_i, zz_masks_i, dim_v, sm_scale = softmax_scale) + + # out: [B, seq_len_shard, h, dim] -> [seq_len, B, h, dim] -+ out_i = einops.rearrange(out_i, 'b s h d -> s b h d') ++ b_, s_, h_, d_ = out_i.shape ++ out_i = out_i.transpose(0, 1).flatten().view(s_, b_, h_, d_).contiguous() + + # outs: [[B, seq_len_shard, nheads // kv_group, dim], ...., [B, seq_len_shard, nheads // kv_group, dim]], repeat kv_group // heads_kv_stride times + # lses: [[B, seq_len_shard, heads_kv_stride], ...., [B, seq_len_shard, heads_kv_stride]], repeat kv_group // heads_kv_stride times @@ -353,13 +353,13 @@ index 89659a1d7..1def27c69 100644 + dk_list = [] + + s_, b_, h_, d_ = q.shape -+ q = einops.rearrange(q, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ q = q.transpose(0, 1).flatten().view(b_, s_, h_, d_) + s_, b_, h_, d_ = k_i.shape -+ k_i = einops.rearrange(k_i, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ k_i = k_i.transpose(0, 1).flatten().view(b_, s_, h_, d_) + s_, b_, h_, d_ = dout.shape -+ dout = einops.rearrange(dout, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ dout = dout.transpose(0, 1).flatten().view(b_, s_, h_, d_) + s_, b_, h_, d_ = out.shape -+ out = einops.rearrange(out, 's b h d -> b s h d').flatten().view(b_, s_, h_, d_) ++ out = out.transpose(0, 1).flatten().view(b_, s_, h_, d_) + b_, s_, h_ = lse.shape + lse = lse.flatten().view(b_, s_, h_) + zz_indices_i = zz_indices @@ -379,12 +379,16 @@ index 89659a1d7..1def27c69 100644 + + # TODO: needs casual = True, may not be compatible with zz + dq_i, _dk_i = sparse_mla_bwd(q_i, k_i, out_i, dout_i, zz_indices_i, zz_masks_i, lse_i, dim_v, sm_scale = softmax_scale) ++ ++ b_, s_, h_, d_ = dq_i.shape ++ dq_i = dq_i.transpose(0, 1).flatten().view(s_, b_, h_, d_).contiguous() ++ b_, s_, h_, d_ = _dk_i.shape ++ _dk_i = _dk_i.transpose(0, 1).flatten().view(s_, b_, h_, d_).contiguous() - # Rearrange gradients to (s, b, h, d) - dq_i = einops.rearrange(dq_i, 'b s h d -> s b h d') - _dk_i = einops.rearrange(_dk_i, 'b s h d -> s b h d') +- dq_i = einops.rearrange(dq_i, 'b s h d -> s b h d') +- _dk_i = einops.rearrange(_dk_i, 'b s h d -> s b h d') - _dv_i = einops.rearrange(_dv_i, 'b s h d -> s b h d') -+ if pg is None: dk_i = _dk_i - dv_i = _dv_i @@ -416,9 +420,11 @@ index 89659a1d7..1def27c69 100644 - dv = torch.cat(dv, dim=2) - return dq, dk, dv, None, None, None, None + dq = torch.cat(dq_list, dim=2) -+ dk = sum(dk_list) ++ dk_ = torch.cat(dk_list, dim=2) ++ dk = torch.sum(dk_, dim=2, keepdim=True) + + return dq, dk, None, None, None, None, None, None +\ No newline at end of file diff --git a/megatron/core/transformer/experimental_attention_variant/dsa.py b/megatron/core/transformer/experimental_attention_variant/dsa.py index fc994490b..b23d2e9a8 100644 --- a/megatron/core/transformer/experimental_attention_variant/dsa.py From d81f29ca286a6ebf7c03ad3c01f3ae8ca8113bf1 Mon Sep 17 00:00:00 2001 From: zhihaow6 Date: Mon, 19 Jan 2026 23:32:19 -0800 Subject: [PATCH 50/57] update --- docker/deepseekv32/megatron.patch | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index c1f80e1e5..886b73e90 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -1,5 +1,5 @@ diff --git a/megatron/core/transformer/dot_product_attention_context_parallel.py b/megatron/core/transformer/dot_product_attention_context_parallel.py -index 89659a1d7..2c1464fb6 100644 +index 89659a1d7..c69859a04 100644 --- a/megatron/core/transformer/dot_product_attention_context_parallel.py +++ b/megatron/core/transformer/dot_product_attention_context_parallel.py @@ -3,107 +3,12 @@ @@ -397,13 +397,14 @@ index 89659a1d7..2c1464fb6 100644 dk_i = torch.zeros( (k_i.shape[1] // cp_size, k_i.shape[0], k_i.shape[2], k_i.shape[3]), device=k_i.device, - dtype=k_i.dtype, - ) +- dtype=k_i.dtype, +- ) - dv_i = torch.zeros( - (v_i.shape[1] // cp_size, v_i.shape[0], v_i.shape[2], v_i.shape[3]), - device=v_i.device, - dtype=v_i.dtype, -- ) ++ dtype=torch.float32, + ) torch.distributed.reduce_scatter_tensor(dk_i, _dk_i, group=pg) - torch.distributed.reduce_scatter_tensor(dv_i, _dv_i, group=pg) @@ -421,7 +422,7 @@ index 89659a1d7..2c1464fb6 100644 - return dq, dk, dv, None, None, None, None + dq = torch.cat(dq_list, dim=2) + dk_ = torch.cat(dk_list, dim=2) -+ dk = torch.sum(dk_, dim=2, keepdim=True) ++ dk = torch.sum(dk_, dim=2, keepdim=True).to(torch.bfloat16) + + return dq, dk, None, None, None, None, None, None \ No newline at end of file @@ -1004,10 +1005,10 @@ index 000000000..d8f2425f0 +] diff --git a/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py b/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py new file mode 100644 -index 000000000..b8ea416dd +index 000000000..83a259efa --- /dev/null +++ b/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py -@@ -0,0 +1,274 @@ +@@ -0,0 +1,272 @@ +# ruff: noqa +import tilelang +from tilelang import language as T @@ -1273,22 +1274,20 @@ index 000000000..b8ea416dd + # Get kernels + preprocess_kernel = preprocess(B, S, H, D) + bwd_kernel = bwd(B, S, S_kv, H, D, D_tail, topk, kv_group, sm_scale, is_casual) -+ postprocess_kernel = postprocess(B, S_kv, D, D_tail, kv_group) + + if delta is None: + delta = preprocess_kernel(o, do) + dkv = torch.zeros_like(kv, dtype=torch.float32) + dq = bwd_kernel(q, kv, do, indices, masks, lse, delta, dkv) -+ dkv = postprocess_kernel(dkv) + + return dq, dkv \ No newline at end of file diff --git a/megatron/core/transformer/tilelang_kernel/sparse_mla_fwd.py b/megatron/core/transformer/tilelang_kernel/sparse_mla_fwd.py new file mode 100644 -index 000000000..d338a2fa6 +index 000000000..e247038de --- /dev/null +++ b/megatron/core/transformer/tilelang_kernel/sparse_mla_fwd.py -@@ -0,0 +1,190 @@ +@@ -0,0 +1,191 @@ +# ruff: noqa +import torch +import tilelang @@ -1409,6 +1408,7 @@ index 000000000..d338a2fa6 + for i_i in T.Pipelined(NI, num_stages=num_stages): + for bi_i in T.Parallel(BI): + mask[bi_i] = Masks[b_i, s_i, g_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i]] ++ + for bi_i, d_i in T.Parallel(BI, D): + KV_shared[bi_i, d_i] = KV[b_i, Indices[b_i, s_i, g_i, i_i * BI + bi_i], g_i, d_i] + for bi_i, d_i in T.Parallel(BI, D_tail): From 474d542e33513f27f1103a032a65e38ea76453bd Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:52:46 +0800 Subject: [PATCH 51/57] Enable experimental rollout flag for CI tests (#492) Co-authored-by: Ethan (Yusheng) Su --- .github/workflows/pr-test.yml | 40 ++ .github/workflows/pr-test.yml.j2 | 19 +- miles/ray/rollout.py | 33 +- miles/rollout/base_types.py | 66 +- .../rollout/generate_hub/agentic_tool_call.py | 85 +++ miles/rollout/generate_hub/multi_turn.py | 88 +++ miles/rollout/generate_hub/single_turn.py | 46 ++ miles/rollout/generate_utils/__init__.py | 0 .../generate_utils/generate_endpoint_utils.py | 112 ++++ .../generate_utils/openai_endpoint_utils.py | 67 ++ miles/rollout/generate_utils/sample_utils.py | 115 ++++ .../rollout/generate_utils/tool_call_utils.py | 115 ++++ miles/rollout/inference_rollout/__init__.py | 2 + .../inference_rollout/compatibility.py | 84 +++ .../inference_rollout_common.py | 192 ++++++ .../inference_rollout_eval.py | 112 ++++ .../inference_rollout_train.py | 146 +++++ miles/rollout/rm_hub/__init__.py | 12 +- miles/router/router.py | 47 +- miles/router/sessions.py | 124 ++++ miles/utils/arguments.py | 24 +- miles/utils/environ.py | 14 + miles/utils/http_utils.py | 20 +- miles/utils/misc.py | 50 +- miles/utils/test_utils/__init__.py | 0 miles/utils/test_utils/mock_sglang_server.py | 248 ++++++++ miles/utils/test_utils/mock_tools.py | 268 ++++++++ .../utils/test_utils/uvicorn_thread_server.py | 49 ++ miles/utils/types.py | 18 + requirements.txt | 1 + tests/__init__.py | 1 + tests/ci/gpu_lock_exec.py | 11 +- tests/e2e/.gitkeep | 1 + tests/fast/__init__.py | 0 tests/fast/conftest.py | 15 + tests/fast/fixtures/__init__.py | 1 + tests/fast/fixtures/generation_fixtures.py | 274 +++++++++ tests/fast/fixtures/rollout_fixtures.py | 127 ++++ tests/fast/rollout/__init__.py | 0 tests/fast/rollout/generate_hub/__init__.py | 0 .../rollout/generate_hub/test_multi_turn.py | 572 ++++++++++++++++++ .../rollout/generate_hub/test_single_turn.py | 424 +++++++++++++ .../generate_hub/test_tool_call_utils.py | 99 +++ tests/fast/rollout/generate_utils/__init__.py | 0 .../generate_utils/test_sample_utils.py | 156 +++++ .../rollout/inference_rollout/__init__.py | 0 .../rollout/inference_rollout/conftest.py | 45 ++ .../inference_rollout/integration/__init__.py | 0 .../integration/test_basic.py | 69 +++ .../integration/test_deterministic.py | 37 ++ .../integration/test_dynamic_filter.py | 46 ++ .../integration/test_group_rm.py | 22 + .../integration/test_multi_sample.py | 65 ++ .../integration/test_multi_turn.py | 114 ++++ .../integration/test_over_sampling.py | 48 ++ .../integration/test_sample_filter.py | 67 ++ .../integration/test_semaphore.py | 33 + .../inference_rollout/integration/utils.py | 89 +++ .../inference_rollout/test_compatibility.py | 196 ++++++ tests/fast/rollout/rm_hub/__init__.py | 0 tests/fast/rollout/rm_hub/test_deepscaler.py | 26 + tests/fast/rollout/rm_hub/test_f1.py | 44 ++ tests/fast/rollout/rm_hub/test_gpqa.py | 86 +++ .../rollout/rm_hub/test_math_dapo_utils.py | 108 ++++ tests/fast/rollout/rm_hub/test_math_utils.py | 129 ++++ tests/fast/rollout/rm_hub/test_rm_hub.py | 126 ++++ tests/fast/router/__init__.py | 0 tests/fast/router/test_router.py | 204 +++++++ tests/fast/router/test_sessions.py | 195 ++++++ tests/fast/utils/__init__.py | 0 tests/fast/utils/test_arguments.py | 58 ++ tests/{ => fast}/utils/test_mask_utils.py | 0 tests/fast/utils/test_misc.py | 59 ++ tests/fast/utils/test_utils/__init__.py | 0 .../test_utils/test_mock_sglang_server.py | 409 +++++++++++++ .../fast/utils/test_utils/test_mock_tools.py | 111 ++++ tests/test_external_rollout.py | 1 + tests/test_mimo_7B_mtp_only_grad.py | 1 + tests/test_moonlight_16B_A3B.py | 1 + tests/test_quick_start_glm4_9B.py | 1 + tests/test_qwen2.5_0.5B_gsm8k.py | 1 + tests/test_qwen2.5_0.5B_gsm8k_async.py | 1 + tests/test_qwen2.5_0.5B_gsm8k_async_short.py | 1 + tests/test_qwen2.5_0.5B_gsm8k_short.py | 1 + tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py | 1 + tests/test_qwen3_0.6B_fsdp_distributed.py | 1 + tests/test_qwen3_0.6B_megatron_fsdp_align.py | 3 + tests/test_qwen3_0.6B_parallel_check.py | 2 + tests/test_qwen3_30B_A3B.py | 1 + tests/test_qwen3_4B_ckpt.py | 1 + tests/test_qwen3_4B_fsdp_true_on_policy.py | 1 + tests/test_qwen3_4B_ppo.py | 1 + tests/test_qwen3_vl_4B_fsdp.py | 1 + 93 files changed, 6230 insertions(+), 54 deletions(-) create mode 100644 miles/rollout/generate_hub/agentic_tool_call.py create mode 100644 miles/rollout/generate_hub/multi_turn.py create mode 100644 miles/rollout/generate_hub/single_turn.py create mode 100644 miles/rollout/generate_utils/__init__.py create mode 100644 miles/rollout/generate_utils/generate_endpoint_utils.py create mode 100644 miles/rollout/generate_utils/openai_endpoint_utils.py create mode 100644 miles/rollout/generate_utils/sample_utils.py create mode 100644 miles/rollout/generate_utils/tool_call_utils.py create mode 100644 miles/rollout/inference_rollout/__init__.py create mode 100644 miles/rollout/inference_rollout/compatibility.py create mode 100644 miles/rollout/inference_rollout/inference_rollout_common.py create mode 100644 miles/rollout/inference_rollout/inference_rollout_eval.py create mode 100644 miles/rollout/inference_rollout/inference_rollout_train.py create mode 100644 miles/router/sessions.py create mode 100644 miles/utils/environ.py create mode 100644 miles/utils/test_utils/__init__.py create mode 100644 miles/utils/test_utils/mock_sglang_server.py create mode 100644 miles/utils/test_utils/mock_tools.py create mode 100644 miles/utils/test_utils/uvicorn_thread_server.py create mode 100644 tests/__init__.py create mode 100644 tests/e2e/.gitkeep create mode 100644 tests/fast/__init__.py create mode 100644 tests/fast/conftest.py create mode 100644 tests/fast/fixtures/__init__.py create mode 100644 tests/fast/fixtures/generation_fixtures.py create mode 100644 tests/fast/fixtures/rollout_fixtures.py create mode 100644 tests/fast/rollout/__init__.py create mode 100644 tests/fast/rollout/generate_hub/__init__.py create mode 100644 tests/fast/rollout/generate_hub/test_multi_turn.py create mode 100644 tests/fast/rollout/generate_hub/test_single_turn.py create mode 100644 tests/fast/rollout/generate_hub/test_tool_call_utils.py create mode 100644 tests/fast/rollout/generate_utils/__init__.py create mode 100644 tests/fast/rollout/generate_utils/test_sample_utils.py create mode 100644 tests/fast/rollout/inference_rollout/__init__.py create mode 100644 tests/fast/rollout/inference_rollout/conftest.py create mode 100644 tests/fast/rollout/inference_rollout/integration/__init__.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_basic.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_deterministic.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_dynamic_filter.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_group_rm.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_multi_sample.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_multi_turn.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_over_sampling.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_sample_filter.py create mode 100644 tests/fast/rollout/inference_rollout/integration/test_semaphore.py create mode 100644 tests/fast/rollout/inference_rollout/integration/utils.py create mode 100644 tests/fast/rollout/inference_rollout/test_compatibility.py create mode 100644 tests/fast/rollout/rm_hub/__init__.py create mode 100644 tests/fast/rollout/rm_hub/test_deepscaler.py create mode 100644 tests/fast/rollout/rm_hub/test_f1.py create mode 100644 tests/fast/rollout/rm_hub/test_gpqa.py create mode 100644 tests/fast/rollout/rm_hub/test_math_dapo_utils.py create mode 100644 tests/fast/rollout/rm_hub/test_math_utils.py create mode 100644 tests/fast/rollout/rm_hub/test_rm_hub.py create mode 100644 tests/fast/router/__init__.py create mode 100644 tests/fast/router/test_router.py create mode 100644 tests/fast/router/test_sessions.py create mode 100644 tests/fast/utils/__init__.py create mode 100644 tests/fast/utils/test_arguments.py rename tests/{ => fast}/utils/test_mask_utils.py (100%) create mode 100644 tests/fast/utils/test_misc.py create mode 100644 tests/fast/utils/test_utils/__init__.py create mode 100644 tests/fast/utils/test_utils/test_mock_sglang_server.py create mode 100644 tests/fast/utils/test_utils/test_mock_tools.py diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index d34c823aa..4b8b5dc82 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -25,6 +25,46 @@ concurrency: jobs: + fast: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request) + runs-on: self-hosted + container: + image: radixark/miles:latest + options: > + --gpus all + --ipc=host + --shm-size=16g + --ulimit memlock=-1 + --ulimit stack=67108864 + --memory=0 + --memory-swap=0 + -v /mnt/nvme0n1/miles_ci:/data/miles_ci + -v /mnt/nvme0n1/miles_ci/models:/root/models + -v /mnt/nvme0n1/miles_ci/datasets:/root/datasets + strategy: + fail-fast: false + matrix: + info: [{"num_gpus": 0, "test_file": "fast"}] + defaults: + run: + working-directory: ${{ github.workspace }} + env: + GITHUB_COMMIT_NAME: ${{ github.sha }}_${{ github.event.pull_request.number || 'non-pr' }} + WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} + MILES_TEST_ENABLE_INFINITE_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.infinite_run) || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install + shell: bash + run: cd $GITHUB_WORKSPACE && pip install -e . --no-deps --break-system-packages + + - name: Execute + shell: bash + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- pytest tests/${{ matrix.info.test_file }} + e2e-test-short: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-short')) runs-on: self-hosted diff --git a/.github/workflows/pr-test.yml.j2 b/.github/workflows/pr-test.yml.j2 index 37b6fa446..c052b8494 100644 --- a/.github/workflows/pr-test.yml.j2 +++ b/.github/workflows/pr-test.yml.j2 @@ -1,4 +1,10 @@ <% set jobs = { + 'fast': { + 'test_executor': 'pytest', + 'tests': [ + {'test_file': 'fast', 'num_gpus': 0}, + ], + }, 'e2e-test-short': { 'label': 'run-ci-short', 'tests': [ @@ -98,7 +104,7 @@ concurrency: jobs: <% for job_name, config in jobs.items() %> << job_name >>: - if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, '<< config.label >>')) + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request<% if config.label %> && contains(github.event.pull_request.labels.*.name, '<< config.label >>')<% endif %>) runs-on: self-hosted container: image: << config.image if config.image else 'radixark/miles:latest' >> @@ -153,14 +159,5 @@ jobs: - name: Execute shell: bash - run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- python tests/${{ matrix.info.test_file }} - - - name: Post-test cleanup - if: always() - shell: bash - run: | - pkill -9 -f 'ray::' 2>/dev/null || true - pkill -9 -f raylet 2>/dev/null || true - ray stop --force 2>/dev/null || true - rm -rf /tmp/ray/* 2>/dev/null || true + run: python tests/ci/gpu_lock_exec.py --count ${{ matrix.info.num_gpus }} -- << config.test_executor | default('python') >> tests/${{ matrix.info.test_file }} <% endfor %> \ No newline at end of file diff --git a/miles/ray/rollout.py b/miles/ray/rollout.py index 79c6649be..27211845d 100644 --- a/miles/ray/rollout.py +++ b/miles/ray/rollout.py @@ -13,8 +13,15 @@ from sglang.srt.constants import GPU_MEMORY_TYPE_CUDA_GRAPH, GPU_MEMORY_TYPE_KV_CACHE, GPU_MEMORY_TYPE_WEIGHTS from miles.backends.sglang_utils.sglang_engine import SGLangEngine -from miles.rollout.base_types import call_rollout_fn +from miles.rollout.base_types import ( + RolloutFnConstructorInput, + RolloutFnEvalInput, + RolloutFnTrainInput, + call_rollout_fn, +) +from miles.rollout.inference_rollout.compatibility import call_rollout_function, load_rollout_function from miles.utils import tracking_utils +from miles.utils.environ import enable_experimental_rollout_refactor from miles.utils.health_monitor import RolloutHealthMonitor from miles.utils.http_utils import _wrap_ipv6, find_available_port, get_host_info, init_http_client from miles.utils.iter_utils import group_by @@ -53,8 +60,14 @@ def __init__(self, args, pg): data_source_cls = load_function(self.args.data_source_path) self.data_source = data_source_cls(args) - self.generate_rollout = load_function(self.args.rollout_function_path) - self.eval_generate_rollout = load_function(self.args.eval_function_path) + self.use_experimental_refactor = enable_experimental_rollout_refactor() + if self.use_experimental_refactor: + input = RolloutFnConstructorInput(args=args, data_source=self.data_source) + self.generate_rollout = load_rollout_function(input, self.args.rollout_function_path) + self.eval_generate_rollout = load_rollout_function(input, self.args.eval_function_path) + else: + self.generate_rollout = load_function(self.args.rollout_function_path) + self.eval_generate_rollout = load_function(self.args.eval_function_path) self.custom_reward_post_process_func = None if self.args.custom_reward_post_process_path is not None: self.custom_reward_post_process_func = load_function(self.args.custom_reward_post_process_path) @@ -142,7 +155,12 @@ def eval(self, rollout_id): return self.health_monitoring_resume() - result = call_rollout_fn(self.eval_generate_rollout, self.args, rollout_id, self.data_source, evaluation=True) + if self.use_experimental_refactor: + result = call_rollout_function(self.eval_generate_rollout, RolloutFnEvalInput(rollout_id=rollout_id)) + else: + result = call_rollout_fn( + self.eval_generate_rollout, self.args, rollout_id, self.data_source, evaluation=True + ) data = result.data self._save_debug_rollout_data(data, rollout_id=rollout_id, evaluation=True) metrics = _log_eval_rollout_data(rollout_id, self.args, data, result.metrics) @@ -224,7 +242,12 @@ def _get_rollout_data(self, rollout_id): ) metrics = None else: - data = call_rollout_fn(self.generate_rollout, self.args, rollout_id, self.data_source, evaluation=False) + if self.use_experimental_refactor: + data = call_rollout_function(self.generate_rollout, RolloutFnTrainInput(rollout_id=rollout_id)) + else: + data = call_rollout_fn( + self.generate_rollout, self.args, rollout_id, self.data_source, evaluation=False + ) metrics = data.metrics data = data.samples # flatten the data if it is a list of lists diff --git a/miles/rollout/base_types.py b/miles/rollout/base_types.py index faa85c726..c2644e87f 100644 --- a/miles/rollout/base_types.py +++ b/miles/rollout/base_types.py @@ -1,22 +1,86 @@ +from __future__ import annotations + +from argparse import Namespace from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any +from miles.rollout.data_source import DataSource from miles.utils.types import Sample +if TYPE_CHECKING: + from miles.rollout.inference_rollout.inference_rollout_common import GenerateState + + +@dataclass(frozen=True) +class RolloutFnConstructorInput: + args: Namespace + # TODO may refactor DataSource API + data_source: DataSource + + +@dataclass(frozen=True) +class RolloutFnBaseInput: + rollout_id: int + + @property + def evaluation(self): + raise NotImplementedError + + +# subclassing for different data in the future +@dataclass(frozen=True) +class RolloutFnTrainInput(RolloutFnBaseInput): + @property + def evaluation(self): + return False + +@dataclass(frozen=True) +class RolloutFnEvalInput(RolloutFnBaseInput): + @property + def evaluation(self): + return True + + +# TODO make it frozen @dataclass class RolloutFnTrainOutput: samples: list[list[Sample]] metrics: dict[str, Any] = None +# TODO make it frozen @dataclass class RolloutFnEvalOutput: data: dict[str, dict[str, Any]] metrics: dict[str, Any] = None +RolloutFnInput = RolloutFnTrainInput | RolloutFnEvalInput +RolloutFnOutput = RolloutFnTrainOutput | RolloutFnEvalOutput + + +@dataclass(frozen=True) +class GenerateFnInput: + state: GenerateState + sample: Sample + sampling_params: dict[str, Any] + evaluation: bool + + @property + def args(self) -> Namespace: + return self.state.args + + +@dataclass(frozen=True) +class GenerateFnOutput: + # One generate may lead to multiple samples, such as multi-agent, tree-like exploration, or + # multi-turn with removing thinking tokens. + samples: Sample | list[Sample] + + def call_rollout_fn(fn, *args, evaluation: bool, **kwargs): + """Legacy rollout function call interface. Used when MILES_EXPERIMENTAL_ROLLOUT_REFACTOR is disabled.""" output = fn(*args, **kwargs, evaluation=evaluation) # compatibility for legacy version diff --git a/miles/rollout/generate_hub/agentic_tool_call.py b/miles/rollout/generate_hub/agentic_tool_call.py new file mode 100644 index 000000000..05223a654 --- /dev/null +++ b/miles/rollout/generate_hub/agentic_tool_call.py @@ -0,0 +1,85 @@ +""" +Simple agentic demo with tool calling. +""" + +import argparse +from copy import deepcopy +from typing import Any + +from openai import AsyncOpenAI + +from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput +from miles.rollout.generate_utils.openai_endpoint_utils import ( + OpenAIEndpointTracer, + compute_samples_from_openai_records, +) +from miles.rollout.generate_utils.sample_utils import merge_samples +from miles.rollout.generate_utils.tool_call_utils import execute_tool_calls +from miles.utils.misc import load_function + + +async def generate(input: GenerateFnInput) -> GenerateFnOutput: + tracer = await OpenAIEndpointTracer.create(input.args) + + await _run_blackbox_tool_call_agent( + base_url=tracer.base_url, + prompt=input.sample.prompt, + max_turns=input.args.generate_max_turns, + tool_specs_path=input.args.generate_tool_specs_path, + execute_tool_function_path=input.args.generate_execute_tool_function_path, + ) + + records = await tracer.collect_records() + samples = compute_samples_from_openai_records(input.sample, records, input.state.tokenizer) + if not input.args.generate_multi_samples: + samples = merge_samples(samples, input.state.tokenizer) + return GenerateFnOutput(samples=samples) + + +def _add_arguments(parser: argparse.ArgumentParser): + parser.add_argument("--generate-max-turns", type=int, default=16) + parser.add_argument("--generate-tool-specs-path", type=str) + parser.add_argument("--generate-execute-tool-function-path", type=str) + parser.add_argument("--generate-multi-samples", action="store_true") + + +generate.add_arguments = _add_arguments + + +async def _run_blackbox_tool_call_agent( + base_url: str, + prompt: list[dict[str, Any]], + max_turns: int, + tool_specs_path: str, + execute_tool_function_path: str, +): + """ + Imagine this is a black-box agent, e.g. SWE-agent, which does arbitrarily complex work, + only understands OpenAI compatible API, and never understands Miles or the Sample data structure. + """ + + # ----------------------- Setup ------------------------- + + client = AsyncOpenAI(base_url=base_url, api_key="empty") + execute_tool_function = load_function(execute_tool_function_path) + tool_specs = load_function(tool_specs_path) + + # ----------------------- Initial prompts ------------------------- + + messages = deepcopy(prompt) + + for _turn in range(max_turns): + # ----------------------- Call inference endpoint ------------------------- + + response = await client.chat.completions.create(model="default", messages=messages, tools=tool_specs) + + choice = response.choices[0] + messages.append(choice.message.model_dump()) + + if choice.finish_reason in ("stop", "length"): + break + + # ----------------------- Execute tools ------------------------- + + if x := choice.message.tool_calls: + messages += await execute_tool_calls(x, execute_tool_function) diff --git a/miles/rollout/generate_hub/multi_turn.py b/miles/rollout/generate_hub/multi_turn.py new file mode 100644 index 000000000..97814ecb3 --- /dev/null +++ b/miles/rollout/generate_hub/multi_turn.py @@ -0,0 +1,88 @@ +""" +Simple multi-turn generation with tool calling. +""" + +import argparse +from copy import deepcopy + +from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput +from miles.rollout.generate_utils.generate_endpoint_utils import ( + compute_prompt_ids_from_sample, + compute_request_payload, + update_sample_from_response, +) +from miles.rollout.generate_utils.tool_call_utils import ( + create_tool_call_parser, + execute_tool_calls, + update_sample_with_tool_responses, +) +from miles.utils.http_utils import post +from miles.utils.misc import load_function + + +async def generate(input: GenerateFnInput) -> GenerateFnOutput: + # ----------------------- Setup ------------------------- + + args = input.args + sample = deepcopy(input.sample) + tokenizer = input.state.tokenizer + assert not args.partial_rollout, "Partial rollout is not supported" + + url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" + + execute_tool_function = load_function(args.generate_execute_tool_function_path) + + tool_specs = load_function(args.generate_tool_specs_path) + tool_call_parser = create_tool_call_parser(tool_specs, args.generate_tool_call_parser) + + multi_samples = [] + + # ----------------------- Initial prompts ------------------------- + + prompt_tokens_ids = compute_prompt_ids_from_sample(input.state, sample, tools=tool_specs) + + sample.tokens = prompt_tokens_ids.copy() + + for _turn in range(args.generate_max_turns): + # ----------------------- Call inference endpoint ------------------------- + + payload, halt_status = compute_request_payload(args, sample.tokens, input.sampling_params) + if payload is None: + sample.status = halt_status + if args.generate_multi_samples and multi_samples: + multi_samples[-1].status = halt_status + break + + if args.generate_multi_samples: + sample = deepcopy(input.sample) + + output = await post(url, payload) + await update_sample_from_response(args, sample, payload=payload, output=output, update_loss_mask=True) + + if args.generate_multi_samples: + multi_samples.append(deepcopy(sample)) + + if output["meta_info"]["finish_reason"]["type"] in ("abort", "length"): + break + + # ----------------------- Execute tools ------------------------- + + _, tool_calls = tool_call_parser.parse_non_stream(output["text"]) + if len(tool_calls) == 0: + break + + tool_messages = await execute_tool_calls(tool_calls, execute_tool_function) + update_sample_with_tool_responses(sample, tool_messages, tokenizer=tokenizer) + + return GenerateFnOutput(samples=multi_samples if args.generate_multi_samples else sample) + + +def _add_arguments(parser: argparse.ArgumentParser): + parser.add_argument("--generate-max-turns", type=int, default=16) + parser.add_argument("--generate-tool-specs-path", type=str) + parser.add_argument("--generate-tool-call-parser", type=str) + parser.add_argument("--generate-execute-tool-function-path", type=str) + parser.add_argument("--generate-multi-samples", action="store_true") + + +generate.add_arguments = _add_arguments diff --git a/miles/rollout/generate_hub/single_turn.py b/miles/rollout/generate_hub/single_turn.py new file mode 100644 index 000000000..5c0a15b5b --- /dev/null +++ b/miles/rollout/generate_hub/single_turn.py @@ -0,0 +1,46 @@ +""" +Simple single-turn generation. +""" + +from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput +from miles.rollout.generate_utils.generate_endpoint_utils import ( + compute_prompt_ids_from_sample, + compute_request_payload, + update_sample_from_response, +) +from miles.utils.http_utils import post +from miles.utils.types import Sample + + +async def generate(input: GenerateFnInput) -> GenerateFnOutput: + args = input.args + sample = input.sample + sampling_params = input.sampling_params + assert sample.status in {Sample.Status.PENDING, Sample.Status.ABORTED}, f"{sample.status=}" + url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate" + + prompt_ids = compute_prompt_ids_from_sample(input.state, sample) + + # Handle Partial Rollout resuming + if len(sample.response) > 0: + input_ids = sample.tokens + sampling_params["max_new_tokens"] -= len(sample.tokens) - len(prompt_ids) + + assert sampling_params["max_new_tokens"] >= 0 + if sampling_params["max_new_tokens"] == 0: + sample.status = Sample.Status.TRUNCATED + return GenerateFnOutput(samples=sample) + else: + input_ids = prompt_ids + + payload, halt_status = compute_request_payload( + args, input_ids=input_ids, sampling_params=sampling_params, multimodal_inputs=sample.multimodal_inputs + ) + if payload is None: + sample.status = halt_status + return GenerateFnOutput(samples=sample) + + output = await post(url, payload) + await update_sample_from_response(args, sample, payload=payload, output=output) + + return GenerateFnOutput(samples=sample) diff --git a/miles/rollout/generate_utils/__init__.py b/miles/rollout/generate_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/miles/rollout/generate_utils/generate_endpoint_utils.py b/miles/rollout/generate_utils/generate_endpoint_utils.py new file mode 100644 index 000000000..a91d71f1d --- /dev/null +++ b/miles/rollout/generate_utils/generate_endpoint_utils.py @@ -0,0 +1,112 @@ +""" +Utils to integrate SGLang's `/generate` endpoint with RL things like Sample. +""" + +from copy import deepcopy +from typing import Any + +import numpy as np +import pybase64 + +from miles.utils.processing_utils import encode_image_for_rollout_engine +from miles.utils.types import Sample + + +# Make this an isolated function because users may want to compute their own +def compute_prompt_ids_from_sample(state, sample, tools=None): + prompt = sample.prompt + + if state.processor: + processor_output = state.processor(text=prompt, **sample.multimodal_inputs) + prompt_ids = processor_output["input_ids"][0] + + # TODO shall we move it to other places? then can make this function immutable + sample.multimodal_train_inputs = { + k: v for k, v in processor_output.items() if k not in ["input_ids", "attention_mask"] + } or None + + return prompt_ids + else: + if not isinstance(prompt, str): + prompt = state.tokenizer.apply_chat_template( + prompt, tokenize=False, add_generation_prompt=True, tools=tools + ) + + return state.tokenizer.encode(prompt, add_special_tokens=False) + + +def compute_request_payload( + args, + input_ids: list[int], + sampling_params: dict, + multimodal_inputs: dict | None = None, +) -> tuple[dict[str, Any] | None, Sample.Status | None]: + sampling_params = deepcopy(sampling_params) + max_new_tokens = sampling_params.pop("max_new_tokens", args.rollout_max_response_len) + if x := args.rollout_max_context_len: + max_new_tokens = min(max_new_tokens, x - len(input_ids)) + if max_new_tokens <= 0: + return None, Sample.Status.TRUNCATED + + payload = { + "input_ids": input_ids, + "sampling_params": {**sampling_params, "max_new_tokens": max_new_tokens}, + "return_logprob": True, + "return_routed_experts": args.use_rollout_routing_replay, + } + if image_data := (multimodal_inputs or {}).get("images"): + payload["image_data"] = [encode_image_for_rollout_engine(image) for image in image_data] + + return payload, None + + +async def update_sample_from_response( + args, sample: Sample, payload: dict, output: dict, update_loss_mask: bool = False +): + # Initialize sample.tokens for the first turn + if (len(sample.response) == 0) and not sample.tokens: + sample.tokens = payload["input_ids"] + + if args.use_miles_router and "RadixTreeMiddleware" in args.miles_router_middleware_paths: + from miles.router.middleware_hub.radix_tree_middleware import postprocess_sample_with_radix_tree + + # TODO may rename to match + await postprocess_sample_with_radix_tree(args, sample, output) + + assert not update_loss_mask, "This code branch has not implemented update_loss_mask" + else: + if x := output["meta_info"].get("output_token_logprobs"): + new_response_tokens = [item[1] for item in x] + new_response_log_probs = [item[0] for item in x] + else: + new_response_tokens, new_response_log_probs = [], [] + + # Update sample with tokens directly - avoiding re-tokenization + sample.tokens = sample.tokens + new_response_tokens + sample.response_length += len(new_response_tokens) + sample.response += output["text"] + + if sample.rollout_log_probs is None: + sample.rollout_log_probs = [] + sample.rollout_log_probs += new_response_log_probs + + if update_loss_mask: + if sample.loss_mask is None: + sample.loss_mask = [] + sample.loss_mask += [1] * len(new_response_tokens) + + # TODO handle multi-turn cases (may need concat instead of assignment) + sample.rollout_routed_experts = _get_rollout_routed_experts_from_response(args, sample, output) + + # TODO may unify (currently there are both methods inside Sample and separate functions) + sample.update_from_meta_info(args, output["meta_info"]) + + +def _get_rollout_routed_experts_from_response(args, sample, output): + info = output["meta_info"].get("routed_experts") + if info is None: + return None + + x = np.frombuffer(pybase64.b64decode(info.encode("ascii")), dtype=np.int32) + x = x.reshape(len(sample.tokens) - 1, args.num_layers, args.moe_router_topk) + return x diff --git a/miles/rollout/generate_utils/openai_endpoint_utils.py b/miles/rollout/generate_utils/openai_endpoint_utils.py new file mode 100644 index 000000000..73ba8198b --- /dev/null +++ b/miles/rollout/generate_utils/openai_endpoint_utils.py @@ -0,0 +1,67 @@ +""" +Utilities for the OpenAI endpoint +""" + +import logging +from argparse import Namespace +from copy import deepcopy + +from miles.router.sessions import GetSessionResponse, SessionRecord +from miles.utils.http_utils import post +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + + +class OpenAIEndpointTracer: + def __init__(self, router_url: str, session_id: str): + self.router_url = router_url + self.session_id = session_id + self.base_url = f"{router_url}/sessions/{session_id}/v1" + + @staticmethod + async def create(args: Namespace): + router_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}" + session_id = (await post(f"{router_url}/sessions", {}))["session_id"] + return OpenAIEndpointTracer(router_url=router_url, session_id=session_id) + + async def collect_records(self) -> list[SessionRecord]: + response = await post(f"{self.router_url}/sessions/{self.session_id}", {}, action="get") + response = GetSessionResponse.model_validate(response) + records = response.records + + try: + await post(f"{self.router_url}/sessions/{self.session_id}", {}, action="delete") + except Exception as e: + logger.warning(f"Failed to delete session {self.session_id} after collecting records: {e}") + + return records + + +def compute_samples_from_openai_records(input_sample: Sample, records: list[SessionRecord], tokenizer) -> list[Sample]: + return [_compute_sample_from_openai_record(input_sample, record, tokenizer) for record in records] + + +def _compute_sample_from_openai_record(input_sample: Sample, record: SessionRecord, tokenizer) -> Sample: + # TODO may refine after @guapisolo's implementation + choice = record.response["choices"][0] + output_token_ids = [item["token_id"] for item in choice["logprobs"]["content"]] + output_log_probs = [item["logprob"] for item in choice["logprobs"]["content"]] + + sample = deepcopy(input_sample) + sample.tokens = record.request["input_ids"] + output_token_ids + sample.rollout_log_probs = output_log_probs + sample.response = tokenizer.decode(output_token_ids) + sample.response_length = len(output_token_ids) + sample.loss_mask = [1] * len(output_token_ids) + + # TODO unify with Sample.update_from_meta_info + match choice["finish_reason"]: + case "stop" | "tool_calls": + sample.status = Sample.Status.COMPLETED + case "length": + sample.status = Sample.Status.TRUNCATED + case "abort": + sample.status = Sample.Status.ABORTED + + return sample diff --git a/miles/rollout/generate_utils/sample_utils.py b/miles/rollout/generate_utils/sample_utils.py new file mode 100644 index 000000000..6a4e645be --- /dev/null +++ b/miles/rollout/generate_utils/sample_utils.py @@ -0,0 +1,115 @@ +from copy import deepcopy +from dataclasses import fields + +from miles.utils.types import Sample + + +def merge_samples(samples: list[Sample], tokenizer) -> Sample: + acc = samples[0] + for sample in samples[1:]: + acc = _merge_sample_pair(acc, sample, tokenizer=tokenizer) + return acc + + +def _merge_sample_pair(a: Sample, b: Sample, tokenizer) -> Sample: + """Merge two samples generated from sibling inference engine calls.""" + a, b = deepcopy(a), deepcopy(b) + + def _merge_equal_value(field): + x = getattr(a, field) + y = getattr(b, field) + assert x == y, f"{field} mismatch: a.{field}={x}, b.{field}={y}" + return x + + def _fill_defaults(sample: Sample): + if sample.loss_mask is None: + sample.loss_mask = [1] * sample.response_length + if sample.rollout_log_probs is None: + sample.rollout_log_probs = [0.0] * sample.response_length + + _fill_defaults(a) + _fill_defaults(b) + + obs_len = len(b.tokens) - len(a.tokens) - b.response_length + obs_tokens = b.tokens[len(a.tokens) : len(a.tokens) + obs_len] + # TODO: is this acceptable? + obs_text = tokenizer.decode(obs_tokens) + + try: + a.validate() + b.validate() + assert _startswith(short=a.prompt, long=b.prompt), "b.prompt must start with a.prompt" + assert _startswith(short=a.tokens, long=b.tokens), "b.tokens must start with a.tokens" + assert obs_len > 0, f"obs_len must be > 0, got {obs_len}" + if a.rollout_routed_experts is not None: + assert a.rollout_routed_experts.shape[0] <= b.rollout_routed_experts.shape[0] + assert a.status == Sample.Status.COMPLETED, f"a.status must be COMPLETED, got {a.status}" + + return _create_with_all_fields( + Sample, + group_index=_merge_equal_value("group_index"), + index=_merge_equal_value("index"), + prompt=b.prompt, + tokens=b.tokens, + multimodal_inputs=_merge_equal_value("multimodal_inputs"), + multimodal_train_inputs=_merge_equal_value("multimodal_train_inputs"), + response=a.response + obs_text + b.response, + response_length=a.response_length + obs_len + b.response_length, + label=_merge_equal_value("label"), + reward=_merge_equal_value("reward"), + loss_mask=a.loss_mask + [0] * obs_len + b.loss_mask, + weight_versions=a.weight_versions + b.weight_versions, + rollout_log_probs=a.rollout_log_probs + [0.0] * obs_len + b.rollout_log_probs, + rollout_routed_experts=b.rollout_routed_experts, + remove_sample=_merge_equal_value("remove_sample"), + status=b.status, + metadata=_merge_equal_value("metadata"), + train_metadata=_merge_equal_value("train_metadata"), + non_generation_time=_merge_equal_value("non_generation_time"), + spec_info=_merge_spec_info(a.spec_info, b.spec_info), + prefix_cache_info=_merge_prefix_cache_info(a.prefix_cache_info, b.prefix_cache_info), + ) + except AssertionError as e: + e.add_note(f"{a=} {b=}") + raise + + +def _merge_spec_info(a: Sample.SpecInfo, b: Sample.SpecInfo) -> Sample.SpecInfo: + def _merge_plus_value(field): + return getattr(a, field) + getattr(b, field) + + return _create_with_all_fields( + Sample.SpecInfo, + spec_accept_token_num=_merge_plus_value("spec_accept_token_num"), + spec_draft_token_num=_merge_plus_value("spec_draft_token_num"), + spec_verify_ct=_merge_plus_value("spec_verify_ct"), + completion_token_num=_merge_plus_value("completion_token_num"), + ) + + +def _merge_prefix_cache_info(a: Sample.PrefixCacheInfo, b: Sample.PrefixCacheInfo) -> Sample.PrefixCacheInfo: + def _merge_plus_value(field): + return getattr(a, field) + getattr(b, field) + + return _create_with_all_fields( + Sample.PrefixCacheInfo, + cached_tokens=_merge_plus_value("cached_tokens"), + total_prompt_tokens=_merge_plus_value("total_prompt_tokens"), + ) + + +def _create_with_all_fields(cls, **kwargs): + expected = {f.name for f in fields(cls)} + actual = set(kwargs.keys()) + assert ( + expected == actual + ), f"{cls.__name__} field mismatch. Missing: {expected - actual}, Extra: {actual - expected}" + return cls(**kwargs) + + +def _startswith(*, short, long) -> bool: + if isinstance(short, str) and isinstance(long, str): + return long.startswith(short) + if isinstance(short, list) and isinstance(long, list): + return (len(long) >= len(short)) and (long[: len(short)] == short) + raise NotImplementedError diff --git a/miles/rollout/generate_utils/tool_call_utils.py b/miles/rollout/generate_utils/tool_call_utils.py new file mode 100644 index 000000000..85ea87aea --- /dev/null +++ b/miles/rollout/generate_utils/tool_call_utils.py @@ -0,0 +1,115 @@ +""" +Utils to handle tool calls. +""" + +import json +import uuid +from collections.abc import Callable +from typing import Any + +from openai.types.chat import ChatCompletionMessageToolCall +from pydantic import TypeAdapter +from sglang.srt.entrypoints.openai.protocol import Tool +from sglang.srt.function_call.core_types import ToolCallItem +from sglang.srt.function_call.function_call_parser import FunctionCallParser + +from miles.utils.types import Sample + +_DUMMY_USER = {"role": "user", "content": "dummy"} + + +def create_tool_call_parser(tool_specs, tool_call_parser): + return FunctionCallParser( + tools=TypeAdapter(list[Tool]).validate_python(tool_specs), + tool_call_parser=tool_call_parser, + ) + + +async def execute_tool_calls( + tool_calls: list[ToolCallItem | ChatCompletionMessageToolCall], + execute_one: Callable, +) -> list[dict[str, Any]]: + tool_messages = [] + for call in tool_calls: + tool_messages.append(await _execute_tool_call(call, execute_one)) + return tool_messages + + +async def _execute_tool_call( + call: ToolCallItem | ChatCompletionMessageToolCall, execute_one: Callable +) -> dict[str, Any]: + if isinstance(call, ChatCompletionMessageToolCall): + name = call.function.name + params = json.loads(call.function.arguments) if call.function.arguments else {} + tool_call_id = call.id + elif isinstance(call, ToolCallItem): + name = call.name + params = json.loads(call.parameters) if call.parameters else {} + tool_call_id = f"call_{uuid.uuid4().hex[:24]}" + else: + raise TypeError(f"Unsupported tool call type: {type(call)}") + + result = await execute_one(name, params) + assert isinstance(result, str) + + return {"role": "tool", "tool_call_id": tool_call_id, "content": result, "name": name} + + +def update_sample_with_tool_responses(sample: Sample, tool_messages: list[dict[str, Any]], tokenizer): + next_obs_tokens_ids: list[int] = tokenize_tool_responses(tool_messages, tokenizer=tokenizer) + sample.response += tokenizer.decode(next_obs_tokens_ids) + sample.response_length += len(next_obs_tokens_ids) + sample.tokens += next_obs_tokens_ids + sample.loss_mask += [0] * len(next_obs_tokens_ids) + sample.rollout_log_probs += [0.0] * len(next_obs_tokens_ids) + + +# TODO: very naive implementation, need the to-be-implemented e2e test to validate. +def tokenize_tool_responses( + tool_messages: list[dict[str, Any]], + tokenizer, +) -> list[int]: + return _tokenize_postfix_messages(tool_messages, tokenizer) + + +def _tokenize_postfix_messages( + postfix_messages: list[dict[str, Any]], + tokenizer, +) -> list[int]: + dummy_assistant = _build_dummy_assistant(postfix_messages) + base_messages = [_DUMMY_USER, dummy_assistant] + + messages_without = base_messages + messages_with = base_messages + postfix_messages + + tokens_with = tokenizer.apply_chat_template(messages_with, tokenize=True, add_generation_prompt=True) + tokens_without = tokenizer.apply_chat_template(messages_without, tokenize=True, add_generation_prompt=False) + + assert tokens_with[: len(tokens_without)] == tokens_without, ( + f"Fail to tokenize_tool_responses caused by token prefix mismatch. " + f"This can happen for thinking model or models with special chat template, " + f"and this simple example does not support it yet, " + f"since this means we cannot have a append-only token id list. " + f"{tokens_with=} {tokens_without=} " + f"{tokenizer.decode(tokens_with)=} {tokenizer.decode(tokens_without)=} " + ) + return tokens_with[len(tokens_without) :] + + +def _build_dummy_assistant(tool_responses: list[dict[str, Any]]) -> dict[str, Any]: + return { + "role": "assistant", + "content": "", + "reasoning_content": " ", + "tool_calls": [ + { + "id": resp.get("tool_call_id", f"call0000{i}"), + "type": "function", + "function": { + "name": resp.get("name", "dummy_func"), + "arguments": {}, + }, + } + for i, resp in enumerate(tool_responses) + ], + } diff --git a/miles/rollout/inference_rollout/__init__.py b/miles/rollout/inference_rollout/__init__.py new file mode 100644 index 000000000..33ccf17bf --- /dev/null +++ b/miles/rollout/inference_rollout/__init__.py @@ -0,0 +1,2 @@ +# This is a refactor of the portions above generate-function in sglang_rollout.py, +# and is give a different name to ensure both code exist at the same time. diff --git a/miles/rollout/inference_rollout/compatibility.py b/miles/rollout/inference_rollout/compatibility.py new file mode 100644 index 000000000..7711e0dd3 --- /dev/null +++ b/miles/rollout/inference_rollout/compatibility.py @@ -0,0 +1,84 @@ +import inspect +from collections.abc import Callable + +from miles.rollout.base_types import ( + GenerateFnInput, + GenerateFnOutput, + RolloutFnConstructorInput, + RolloutFnEvalOutput, + RolloutFnInput, + RolloutFnOutput, + RolloutFnTrainOutput, +) +from miles.utils.async_utils import run +from miles.utils.misc import load_function + + +class LegacyRolloutFnAdapter: + def __init__(self, input: RolloutFnConstructorInput, fn: Callable): + self.args = input.args + self.data_source = input.data_source + self.fn = fn + + def __call__(self, input: RolloutFnInput) -> RolloutFnOutput: + output = self.fn(self.args, input.rollout_id, self.data_source, evaluation=input.evaluation) + + # compatibility for legacy version + if not isinstance(output, (RolloutFnTrainOutput, RolloutFnEvalOutput)): + output = RolloutFnEvalOutput(data=output) if input.evaluation else RolloutFnTrainOutput(samples=output) + + return output + + +def load_rollout_function(input: RolloutFnConstructorInput, path: str): + fn = load_function(path) + + if inspect.isclass(fn): + return fn(input) + else: + return LegacyRolloutFnAdapter(input, fn) + + +def call_rollout_function(fn, input: RolloutFnInput) -> RolloutFnOutput: + output = fn(input) + + if inspect.iscoroutine(output): + output = run(output) + + return output + + +class LegacyGenerateFnAdapter: + def __init__(self, fn: Callable): + self.fn = fn + self._has_evaluation_param = "evaluation" in inspect.signature(fn).parameters + + async def __call__(self, input: GenerateFnInput) -> GenerateFnOutput: + if self._has_evaluation_param: + output = await self.fn(input.args, input.sample, input.sampling_params, evaluation=input.evaluation) + else: + output = await self.fn(input.args, input.sample, input.sampling_params) + + if not isinstance(output, GenerateFnOutput): + output = GenerateFnOutput(samples=output) + + return output + + +def load_generate_function(path: str): + fn = load_function(path) + if fn is None: + return None + + if inspect.isclass(fn): + return fn() + elif _is_legacy_generate_fn(fn): + return LegacyGenerateFnAdapter(fn) + else: + return fn + + +def _is_legacy_generate_fn(fn: Callable) -> bool: + sig = inspect.signature(fn) + params = list(sig.parameters.keys()) + return len(params) >= 3 and params[0] != "input" diff --git a/miles/rollout/inference_rollout/inference_rollout_common.py b/miles/rollout/inference_rollout/inference_rollout_common.py new file mode 100644 index 000000000..8518c6e02 --- /dev/null +++ b/miles/rollout/inference_rollout/inference_rollout_common.py @@ -0,0 +1,192 @@ +import asyncio +import logging +from argparse import Namespace +from copy import deepcopy +from typing import Any + +from miles.rollout.base_types import ( + GenerateFnInput, + RolloutFnConstructorInput, + RolloutFnEvalInput, + RolloutFnEvalOutput, + RolloutFnInput, + RolloutFnOutput, + RolloutFnTrainInput, + RolloutFnTrainOutput, +) +from miles.rollout.generate_hub.single_turn import generate +from miles.rollout.inference_rollout.compatibility import load_generate_function +from miles.rollout.rm_hub import async_rm, batched_async_rm +from miles.utils.processing_utils import load_processor, load_tokenizer +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + + +class GenerateState: + def __init__(self, args: Namespace) -> None: + # persistent state for the generation process + self.args = args + self.tokenizer = load_tokenizer(args.hf_checkpoint, trust_remote_code=True) + self.processor = load_processor(args.hf_checkpoint, trust_remote_code=True) + + self.generate_fn_semaphore = asyncio.Semaphore( + args.sglang_server_concurrency * args.rollout_num_gpus // args.rollout_num_gpus_per_engine + ) + self.sampling_params: dict[str, Any] = compute_sampling_params( + args, + temperature=args.rollout_temperature, + top_p=args.rollout_top_p, + top_k=args.rollout_top_k, + max_new_tokens=args.rollout_max_response_len, + ) + + self.generate_function = load_generate_function(args.custom_generate_function_path) or generate + + self.reset() + + def reset(self) -> None: + self.aborted = False + + +async def generate_and_rm( + state: GenerateState, + sample: Sample | list[Sample], + sampling_params: dict[str, Any], + evaluation: bool = False, +) -> Sample | list[Sample]: + args = state.args + + # mask previous off-policy generation for partial rollout + if args.partial_rollout and args.mask_offpolicy_in_partial_rollout and sample.response_length > 0: + sample.loss_mask = [0] * sample.response_length + + # For samples with existing response, check if they're complete + if sample.status == Sample.Status.COMPLETED or sample.status == Sample.Status.TRUNCATED: + assert sample.response is not None + if not args.group_rm: + assert sample.reward is not None + return sample + + # generate + async with state.generate_fn_semaphore: + if state.aborted: + sample.status = Sample.Status.ABORTED + return sample + + output = await state.generate_function( + GenerateFnInput( + state=state, + sample=sample, + sampling_params=deepcopy(sampling_params), + evaluation=evaluation, + ) + ) + sample = output.samples + + # TODO change to `if not args.group_rm: do reward model` for more clarity after the refactor below + # for the rm that need the whole group, we will not do the rm here + if args.group_rm: + return sample + + # TODO: unify the two branches into one if we decide to use list as output type + # multi samples + if isinstance(sample, list): + samples = sample + if any([sample.status == Sample.Status.ABORTED for sample in samples]): + return samples + + # for multi agent system, the reward of some sample is calculated during generation. + samples_need_reward = [sample for sample in samples if sample.reward is None] + await batched_async_rm(args, samples_need_reward, inplace_set_reward_field=True) + return samples + else: + if sample.status == Sample.Status.ABORTED: + return sample + # for multi-turn environment, a reward could be assigned to the agent. + if sample.reward is None: + sample.reward = await async_rm(args, sample) + + return sample + + +async def generate_and_rm_group( + state: GenerateState, group: list[Sample], sampling_params: dict[str, Any], evaluation: bool = False +) -> list[Sample]: + args = state.args + + if state.aborted: + return group + + tasks = [] + for idx, sample in enumerate(group): + current_sampling_params = sampling_params.copy() + if getattr(args, "sglang_enable_deterministic_inference", False): + current_sampling_params["sampling_seed"] = args.rollout_seed + idx + tasks.append( + asyncio.create_task(generate_and_rm(state, sample, current_sampling_params, evaluation=evaluation)) + ) + + group = await asyncio.gather(*tasks) + if state.aborted: + return group + + if args.group_rm: + await batched_async_rm(args, group, inplace_set_reward_field=True) + + return group + + +def compute_sampling_params( + args, + *, + # after unifying configuration, this can be further refactored + temperature, + top_p, + top_k, + max_new_tokens, +): + return dict( + temperature=temperature, + top_p=top_p, + top_k=top_k, + max_new_tokens=max_new_tokens, + stop=args.rollout_stop, + stop_token_ids=args.rollout_stop_token_ids, + skip_special_tokens=args.rollout_skip_special_tokens, + no_stop_trim=True, + spaces_between_special_tokens=False, + ) + + +class InferenceRolloutFn: + def __init__(self, input: RolloutFnConstructorInput): + self.data_source = input.data_source + self.state = GenerateState(input.args) + self.eval_prompt_dataset_cache = {} + + async def __call__(self, input: RolloutFnInput) -> RolloutFnOutput: + if input.evaluation: + return await self._call_eval(input) + return await self._call_train(input) + + async def _call_train(self, input: RolloutFnTrainInput) -> RolloutFnTrainOutput: + from miles.rollout.inference_rollout.inference_rollout_train import generate_rollout_async + + output, aborted_samples = await generate_rollout_async( + self.state, input.rollout_id, self.data_source.get_samples + ) + self.data_source.add_samples(aborted_samples) + return output + + async def _call_eval(self, input: RolloutFnEvalInput) -> RolloutFnEvalOutput: + from miles.rollout.inference_rollout.inference_rollout_eval import eval_rollout_single_dataset + + assert not self.state.args.group_rm, "Group RM is not supported for eval rollout" + + coros = [] + for dataset_cfg in getattr(self.state.args, "eval_datasets", []) or []: + coros.append(eval_rollout_single_dataset(self.state, dataset_cfg, self.eval_prompt_dataset_cache)) + results_list = await asyncio.gather(*coros) + results = {k: v for r in results_list for k, v in r.items()} + return RolloutFnEvalOutput(data=results) diff --git a/miles/rollout/inference_rollout/inference_rollout_eval.py b/miles/rollout/inference_rollout/inference_rollout_eval.py new file mode 100644 index 000000000..2d052be0a --- /dev/null +++ b/miles/rollout/inference_rollout/inference_rollout_eval.py @@ -0,0 +1,112 @@ +import asyncio +import copy +import logging +from typing import Any + +from tqdm import tqdm + +from miles.rollout.inference_rollout.inference_rollout_common import ( + GenerateState, + compute_sampling_params, + generate_and_rm, +) +from miles.utils.data import Dataset +from miles.utils.eval_config import EvalDatasetConfig +from miles.utils.misc import as_completed_async +from miles.utils.processing_utils import load_processor, load_tokenizer +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + + +async def eval_rollout_single_dataset( + state: GenerateState, + dataset_cfg: EvalDatasetConfig, + prompt_dataset_cache: dict[Any, Dataset], +) -> dict[str, dict[str, list[Any]]]: + args = state.args + assert not args.group_rm, "Group RM is not supported for eval rollout" + + cache_key = dataset_cfg.cache_key + (args.hf_checkpoint, args.apply_chat_template) + if cache_key not in prompt_dataset_cache: + tokenizer = load_tokenizer(args.hf_checkpoint, trust_remote_code=True) + processor = load_processor(args.hf_checkpoint, trust_remote_code=True) + prompt_dataset_cache[cache_key] = Dataset( + path=dataset_cfg.path, + tokenizer=tokenizer, + processor=processor, + max_length=args.eval_max_prompt_len, + prompt_key=dataset_cfg.input_key, + label_key=dataset_cfg.label_key, + multimodal_keys=args.multimodal_keys, + metadata_key=dataset_cfg.metadata_key, + tool_key=dataset_cfg.tool_key, + apply_chat_template=args.apply_chat_template, + apply_chat_template_kwargs=args.apply_chat_template_kwargs, + ) + dataset = prompt_dataset_cache[cache_key] + + base_sampling_params = compute_sampling_params( + args, + temperature=dataset_cfg.temperature, + top_p=dataset_cfg.top_p, + top_k=dataset_cfg.top_k, + max_new_tokens=dataset_cfg.max_response_len, + ) + + tasks = [] + # do multiple samples for eval prompts + sample_index = 0 + for _i, prompt_sample in enumerate(dataset.samples): + for j in range(dataset_cfg.n_samples_per_eval_prompt): + # use the same prompt for multiple samples + sample = copy.deepcopy(prompt_sample) + sample.index = sample_index + sample_index += 1 + sample.metadata = dataset_cfg.inject_metadata(getattr(sample, "metadata", None)) + sampling_params = base_sampling_params + if getattr(args, "sglang_enable_deterministic_inference", False): + sampling_params = base_sampling_params.copy() + sampling_params["sampling_seed"] = args.rollout_seed + j + tasks.append( + asyncio.create_task( + generate_and_rm( + state, + sample, + sampling_params=sampling_params, + evaluation=True, + ) + ) + ) + + data = [] + do_print = True + pbar = tqdm(total=len(tasks), desc=f"Eval {dataset_cfg.name}", disable=not do_print) + async for sample in as_completed_async(tasks): + if do_print: + # TODO improve this after enhancing samples' type + s = (sample[0] if len(sample) > 0 else None) if isinstance(sample, list) else sample + if s is not None: + logger.info( + "eval_rollout_single_dataset example data: " + f"{[str(s.prompt) + s.response]} " + f"reward={s.reward}" + ) + do_print = False + if isinstance(sample, list): + data.extend(sample) + else: + data.append(sample) + pbar.update(1) + pbar.close() + + data.sort(key=lambda sample: sample.index) + + reward_key = args.eval_reward_key or args.reward_key + return { + dataset_cfg.name: { + "rewards": [sample.reward if not reward_key else sample.reward[reward_key] for sample in data], + "truncated": [sample.status == Sample.Status.TRUNCATED for sample in data], + "samples": data, + } + } diff --git a/miles/rollout/inference_rollout/inference_rollout_train.py b/miles/rollout/inference_rollout/inference_rollout_train.py new file mode 100644 index 000000000..bae94ec67 --- /dev/null +++ b/miles/rollout/inference_rollout/inference_rollout_train.py @@ -0,0 +1,146 @@ +import asyncio +import logging +from argparse import Namespace +from collections.abc import Callable + +import sglang_router +from packaging.version import parse +from tqdm import tqdm + +from miles.rollout.base_types import RolloutFnTrainOutput +from miles.rollout.filter_hub.base_types import MetricGatherer, call_dynamic_filter +from miles.rollout.inference_rollout.inference_rollout_common import GenerateState, generate_and_rm_group +from miles.utils.http_utils import get, post +from miles.utils.misc import as_completed_async, load_function +from miles.utils.types import Sample + +logger = logging.getLogger(__name__) + + +async def abort(state: GenerateState, pendings: set, rollout_id: int) -> list[list[Sample]]: + args = state.args + + assert not state.aborted + state.aborted = True + + urls = await get_worker_urls(args) + logger.info(f"Abort request for {urls}") + await asyncio.gather(*[post(f"{url}/abort_request", {"abort_all": True}) for url in urls]) + + # make sure all the pending tasks are finished + aborted_samples = [] + async for group in as_completed_async(pendings): + if not args.partial_rollout: + continue + + # for partial rollout, collect the partial samples into the data buffer + for sample in group: + if sample.response and "start_rollout_id" not in sample.metadata: + sample.metadata["start_rollout_id"] = rollout_id + aborted_samples.append(group) + + if args.partial_rollout: + logger.info(f"Collected {sum(len(x) for x in aborted_samples)} partial samples into the data buffer") + + return aborted_samples + + +async def get_worker_urls(args: Namespace): + if parse(sglang_router.__version__) <= parse("0.2.1") or args.use_miles_router: + response = await get(f"http://{args.sglang_router_ip}:{args.sglang_router_port}/list_workers") + return response["urls"] + else: + response = await get(f"http://{args.sglang_router_ip}:{args.sglang_router_port}/workers") + return [worker["url"] for worker in response["workers"]] + + +def submit_generate_tasks(state: GenerateState, samples: list[list[Sample]]): + return [ + asyncio.create_task( + # submit a group of samples as a single task. + generate_and_rm_group( + state, + group, + sampling_params=state.sampling_params.copy(), + evaluation=False, + ) + ) + for group in samples + ] + + +async def generate_rollout_async( + state: GenerateState, rollout_id: int, data_source: Callable[[int], list[list[Sample]]] +) -> tuple[RolloutFnTrainOutput, list[list[Sample]]]: + args = state.args + assert args.rollout_global_dataset + + # instantiate data filters + dynamic_filter = load_function(args.dynamic_sampling_filter_path) + + metric_gatherer = MetricGatherer() + + # target_data_size is the total number of valid samples to get + target_data_size = args.rollout_batch_size + + pendings = set() + data = [] + all_data = [] + do_print = True + pbar = tqdm(total=target_data_size * args.n_samples_per_prompt, desc="Rollout generation") + while len(data) < target_data_size: + while len(data) + len(pendings) < target_data_size: + # get samples from the buffer and submit the generation requests. + samples = data_source(args.over_sampling_batch_size) + pendings.update(submit_generate_tasks(state, samples)) + + # wait for the generation to finish + done, pendings = await asyncio.wait(pendings, return_when=asyncio.FIRST_COMPLETED) + for task in done: + group: list[Sample] = task.result() + + if do_print: + sample = group[0][0] if isinstance(group[0], list) else group[0] + logger.info( + f"First rollout sample: {[str(sample.prompt) + sample.response]}, label: {sample.label}, reward: {sample.reward}", + ) + do_print = False + + assert len(group) == args.n_samples_per_prompt + all_data.append(group) + dynamic_filter_output = call_dynamic_filter(dynamic_filter, args, group) + if not dynamic_filter_output.keep: + metric_gatherer.on_dynamic_filter_drop(reason=dynamic_filter_output.reason) + continue + + # add the samples to the data + # NOTE: here we have not stored all the unused samples back to the data buffer. + if len(data) < target_data_size: + data.append(group) + pbar.update(args.n_samples_per_prompt) + + pbar.close() + sample = data[-1][0][0] if isinstance(data[-1][0], list) else data[-1][0] + logger.info( + f"Finish rollout: {[str(sample.prompt) + sample.response]}, label: {sample.label}, reward: {sample.reward}", + ) + + # there are still some unfinished requests, abort them + aborted_samples = await abort(state, pendings, rollout_id) + + assert len(data) == args.rollout_batch_size, f"Got {len(data)} samples, expected {args.rollout_batch_size}" + data = sorted(data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index) + all_samples = sorted( + all_data, key=lambda group: group[0][0].index if isinstance(group[0], list) else group[0].index + ) + + # reset the global state to prevent effects on the next rollout or eval. + state.reset() + + if f := load_function(args.rollout_sample_filter_path): + f(args, data) + # There can be circumstances where users want to process all samples including filtered ones. + if f := load_function(args.rollout_all_samples_process_path): + f(args, all_samples, data_source) + + return RolloutFnTrainOutput(samples=data, metrics=metric_gatherer.collect()), aborted_samples diff --git a/miles/rollout/rm_hub/__init__.py b/miles/rollout/rm_hub/__init__.py index 62b253dde..e9ee29db4 100644 --- a/miles/rollout/rm_hub/__init__.py +++ b/miles/rollout/rm_hub/__init__.py @@ -69,8 +69,18 @@ async def async_rm(args, sample: Sample, **kwargs): async def batched_async_rm( args, samples: list[Sample], + inplace_set_reward_field: bool = False, **kwargs, -) -> list[int | float]: +) -> list[int | float] | None: + if inplace_set_reward_field: + rewards = await batched_async_rm(args, samples, **kwargs) + for sample, reward in zip(samples, rewards, strict=True): + assert ( + sample.reward is None + ), f"Overriding sample.reward from {sample.reward} to {reward}, is this intended?" + sample.reward = reward + return None + if args.custom_rm_path is not None: # Ensure the custom reward function is implemented in batch mode rm_function = load_function(args.custom_rm_path) diff --git a/miles/router/router.py b/miles/router/router.py index 2e8ecfc41..7d3ecd980 100644 --- a/miles/router/router.py +++ b/miles/router/router.py @@ -9,6 +9,7 @@ from fastapi.responses import JSONResponse from starlette.responses import Response +from miles.router.sessions import setup_session_routes from miles.utils.misc import load_function logger = logging.getLogger(__name__) @@ -69,6 +70,8 @@ def _setup_routes(self): self.app.post("/add_worker")(self.add_worker) self.app.get("/list_workers")(self.list_workers) self.app.post("/retrieve_from_text")(self.retrieve_from_text) + # Session routes - must be registered before catch-all + setup_session_routes(self.app, self) # Catch-all route for proxying to SGLang - must be registered LAST self.app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])(self.proxy) @@ -130,39 +133,41 @@ async def _health_check_loop(self): async def proxy(self, request: Request, path: str): """Proxy all other requests to the SGLang router""" - # Forward all other paths to SGLang router + result = await self._do_proxy(request, path) + return self._build_proxy_response(result) + + async def _do_proxy(self, request: Request, path: str) -> dict: + """Core proxy logic. Returns dict with request_body, response_body, status_code, headers.""" worker_url = self._use_url() url = f"{worker_url}/{path}" - # Get request body and headers body = await request.body() headers = dict(request.headers) try: response = await self.client.request(request.method, url, content=body, headers=headers) - # Eagerly read content so we can return JSON (not streaming) content = await response.aread() - content_type = response.headers.get("content-type", "") - try: - # Prefer parsing JSON if possible - data = json.loads(content) - return JSONResponse( - content=data, - status_code=response.status_code, - headers=dict(response.headers), - ) - except Exception: - # Fall back to raw body with original content type - return Response( - content=content, - status_code=response.status_code, - headers=dict(response.headers), - media_type=content_type or None, - ) - + return { + "request_body": body, + "response_body": content, + "status_code": response.status_code, + "headers": dict(response.headers), + } finally: self._finish_url(worker_url) + def _build_proxy_response(self, result: dict) -> Response: + """Build HTTP response from proxy result.""" + content = result["response_body"] + status_code = result["status_code"] + headers = result["headers"] + content_type = headers.get("content-type", "") + try: + data = json.loads(content) + return JSONResponse(content=data, status_code=status_code, headers=headers) + except Exception: + return Response(content=content, status_code=status_code, headers=headers, media_type=content_type) + async def add_worker(self, request: Request): """Add a new worker to the router. Supports providing the URL via query string or JSON body. diff --git a/miles/router/sessions.py b/miles/router/sessions.py new file mode 100644 index 000000000..9d753e597 --- /dev/null +++ b/miles/router/sessions.py @@ -0,0 +1,124 @@ +import json +import time +import uuid +from typing import TYPE_CHECKING + +from fastapi import Request +from fastapi.responses import JSONResponse, Response +from pydantic import BaseModel +from transformers import AutoTokenizer + +if TYPE_CHECKING: + from miles.router.router import MilesRouter + + +class SessionRecord(BaseModel): + timestamp: float + method: str + path: str + request: dict + response: dict + status_code: int + + +class GetSessionResponse(BaseModel): + session_id: str + records: list[SessionRecord] + + +class SessionManager: + def __init__(self): + self.sessions: dict[str, list[SessionRecord]] = {} + + def create_session(self) -> str: + session_id = uuid.uuid4().hex + self.sessions[session_id] = [] + return session_id + + def get_session(self, session_id: str) -> list[SessionRecord] | None: + return self.sessions.get(session_id) + + def delete_session(self, session_id: str) -> list[SessionRecord]: + assert session_id in self.sessions + return self.sessions.pop(session_id) + + def add_record(self, session_id: str, record: SessionRecord): + assert session_id in self.sessions + self.sessions[session_id].append(record) + + +def setup_session_routes(app, router: "MilesRouter"): + manager = SessionManager() + + # TODO temporary hack before @guapisolo implements TITO + # ============================= HACK START =============================== + # Lazy load tokenizer only when needed (for tests that don't have hf_checkpoint) + tokenizer = None + + def get_tokenizer(): + nonlocal tokenizer + if tokenizer is None: + tokenizer = AutoTokenizer.from_pretrained(router.args.hf_checkpoint, trust_remote_code=True) + return tokenizer + + # ============================= HACK END =============================== + + @app.post("/sessions") + async def create_session(): + session_id = manager.create_session() + return {"session_id": session_id} + + @app.get("/sessions/{session_id}") + async def get_session(session_id: str): + records = manager.get_session(session_id) + if records is None: + return JSONResponse(status_code=404, content={"error": "session not found"}) + return GetSessionResponse(session_id=session_id, records=records) + + @app.delete("/sessions/{session_id}") + async def delete_session(session_id: str): + if session_id not in manager.sessions: + return JSONResponse(status_code=404, content={"error": "session not found"}) + manager.delete_session(session_id) + return Response(status_code=204) + + @app.api_route("/sessions/{session_id}/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) + async def session_proxy(request: Request, session_id: str, path: str): + if session_id not in manager.sessions: + return JSONResponse(status_code=404, content={"error": "session not found"}) + + result = await router._do_proxy(request, path) + + request_body = json.loads(result["request_body"]) + response_body = json.loads(result["response_body"]) + + # TODO: remove this hack when @guapisolo implements the real TITO + # ============================= HACK START =============================== + if "messages" in request_body and "input_ids" not in request_body: + request_body["input_ids"] = get_tokenizer().apply_chat_template( + request_body["messages"], + add_generation_prompt=True, + add_special_tokens=False, + tools=request_body.get("tools"), + ) + if ( + "logprobs" in response_body.get("choices", [{}])[0] + and "content" in response_body["choices"][0]["logprobs"] + ): + logprobs_content = response_body["choices"][0]["logprobs"]["content"] + for item in logprobs_content: + if "token" in item and "token_id" not in item: + item["token_id"] = get_tokenizer().convert_tokens_to_ids(item["token"]) + # ============================= HACK END =============================== + + record = SessionRecord( + timestamp=time.time(), + method=request.method, + path=path, + request=request_body, + response=response_body, + status_code=result["status_code"], + ) + manager.add_record(session_id, record) + + return router._build_proxy_response(result) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 79b2c419c..071020292 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -10,8 +10,10 @@ from miles.backends.sglang_utils.arguments import add_sglang_arguments from miles.backends.sglang_utils.arguments import validate_args as sglang_validate_args +from miles.utils.environ import enable_experimental_rollout_refactor from miles.utils.eval_config import EvalDatasetConfig, build_eval_dataset_configs, ensure_dataset_list from miles.utils.logging_utils import configure_logger +from miles.utils.misc import load_function logger = logging.getLogger(__name__) @@ -204,7 +206,11 @@ def add_rollout_arguments(parser): parser.add_argument( "--rollout-function-path", type=str, - default="miles.rollout.sglang_rollout.generate_rollout", + default=( + "miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn" + if enable_experimental_rollout_refactor() + else "miles.rollout.sglang_rollout.generate_rollout" + ), help=( "Path to the rollout generation function." "You should use this model to create your own custom rollout function, " @@ -1344,6 +1350,20 @@ def add_ci_arguments(parser): ) return parser + def add_user_provided_function_arguments(parser): + args_partial, _ = parser.parse_known_args() + for path in [ + args_partial.rollout_function_path, + args_partial.custom_generate_function_path, + ]: + try: + fn = load_function(path) + except (ModuleNotFoundError, ValueError): + continue + if fn is not None and callable(getattr(fn, "add_arguments", None)): + fn.add_arguments(parser) + return parser + def add_sglang_tp_size(): temp_parser = argparse.ArgumentParser(add_help=False) temp_parser.add_argument("--rollout-num-gpus-per-engine", type=int, default=1) @@ -1374,6 +1394,8 @@ def add_sglang_tp_size(): parser = add_prefill_decode_disaggregation_arguments(parser) parser = add_ci_arguments(parser) parser = add_custom_megatron_plugins_arguments(parser) + if enable_experimental_rollout_refactor(): + parser = add_user_provided_function_arguments(parser) reset_arg( parser, "--custom-config-path", diff --git a/miles/utils/environ.py b/miles/utils/environ.py new file mode 100644 index 000000000..35d1f350e --- /dev/null +++ b/miles/utils/environ.py @@ -0,0 +1,14 @@ +import os + +_printed_experimental_rollout_refactor = False + + +def enable_experimental_rollout_refactor() -> bool: + result = bool(int(os.environ.get("MILES_EXPERIMENTAL_ROLLOUT_REFACTOR", "0"))) + + global _printed_experimental_rollout_refactor + if result and not _printed_experimental_rollout_refactor: + print("MILES_EXPERIMENTAL_ROLLOUT_REFACTOR=1 is enabled (experimental feature)") + _printed_experimental_rollout_refactor = True + + return result diff --git a/miles/utils/http_utils.py b/miles/utils/http_utils.py index 2b3e6e192..0abdbbf59 100644 --- a/miles/utils/http_utils.py +++ b/miles/utils/http_utils.py @@ -162,11 +162,15 @@ def _next_actor(): return actor -async def _post(client, url, payload, max_retries=60): +async def _post(client, url, payload, max_retries=60, action="post"): retry_count = 0 while retry_count < max_retries: try: - response = await client.post(url, json=payload or {}) + if action in ("delete", "get"): + assert not payload + response = await getattr(client, action)(url) + else: + response = await getattr(client, action)(url, json=payload or {}) response.raise_for_status() try: output = response.json() @@ -240,8 +244,8 @@ def __init__(self, concurrency: int): timeout=httpx.Timeout(None), ) - async def do_post(self, url, payload, max_retries=60): - return await _post(self._client, url, payload, max_retries) + async def do_post(self, url, payload, max_retries=60, action="post"): + return await _post(self._client, url, payload, max_retries, action=action) # Create actors per node created = [] @@ -265,7 +269,8 @@ async def do_post(self, url, payload, max_retries=60): _post_actors = created -async def post(url, payload, max_retries=60): +# TODO may generalize the name since it now contains http DELETE/GET etc (with retries and remote-execution) +async def post(url, payload, max_retries=60, action="post"): # If distributed mode is enabled and actors exist, dispatch via Ray. if _distributed_post_enabled and _post_actors: try: @@ -274,15 +279,16 @@ async def post(url, payload, max_retries=60): actor = _next_actor() if actor is not None: # Use a thread to avoid blocking the event loop on ray.get - obj_ref = actor.do_post.remote(url, payload, max_retries) + obj_ref = actor.do_post.remote(url, payload, max_retries, action=action) return await asyncio.to_thread(ray.get, obj_ref) except Exception as e: logger.info(f"[http_utils] Distributed POST failed, falling back to local: {e} (url={url})") # fall through to local - return await _post(_http_client, url, payload, max_retries) + return await _post(_http_client, url, payload, max_retries, action=action) +# TODO unify w/ `post` to add retries and remote-execution async def get(url): response = await _http_client.get(url) response.raise_for_status() diff --git a/miles/utils/misc.py b/miles/utils/misc.py index c0a96d636..bae72ec0d 100644 --- a/miles/utils/misc.py +++ b/miles/utils/misc.py @@ -1,17 +1,55 @@ +import asyncio import importlib import subprocess +from contextlib import contextmanager import ray from miles.utils.http_utils import is_port_available +# Mainly used for test purpose where `load_function` needs to load many in-flight generated functions +class FunctionRegistry: + def __init__(self): + self._registry: dict[str, object] = {} + + @contextmanager + def temporary(self, name: str, fn: object): + self._register(name, fn) + try: + yield + finally: + self._unregister(name) + + def get(self, name: str) -> object | None: + return self._registry.get(name) + + def _register(self, name: str, fn: object) -> None: + assert name not in self._registry + self._registry[name] = fn + + def _unregister(self, name: str) -> None: + assert name in self._registry + self._registry.pop(name) + + +function_registry = FunctionRegistry() + + +# TODO may rename to `load_object` since it can be used to load things like tool_specs def load_function(path): """ - Load a function from a module. + Load a function from registry or module. :param path: The path to the function, e.g. "module.submodule.function". :return: The function object. """ + if path is None: + return None + + registered = function_registry.get(path) + if registered is not None: + return registered + module_path, _, attr = path.rpartition(".") module = importlib.import_module(module_path) return getattr(module, attr) @@ -30,8 +68,9 @@ def __call__(cls, *args, **kwargs): cls._instances[cls] = instance return cls._instances[cls] - def clear_instances(cls): - cls._instances = {} + @staticmethod + def clear_all_instances(): + SingletonMeta._instances.clear() def exec_command(cmd: str, capture_output: bool = False) -> str | None: @@ -92,3 +131,8 @@ def should_run_periodic_action( step = rollout_id + 1 return (step % interval == 0) or (num_rollout_per_epoch is not None and step % num_rollout_per_epoch == 0) + + +async def as_completed_async(tasks): + for coro in asyncio.as_completed(tasks): + yield await coro diff --git a/miles/utils/test_utils/__init__.py b/miles/utils/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/miles/utils/test_utils/mock_sglang_server.py b/miles/utils/test_utils/mock_sglang_server.py new file mode 100644 index 000000000..2c0dddfe5 --- /dev/null +++ b/miles/utils/test_utils/mock_sglang_server.py @@ -0,0 +1,248 @@ +import asyncio +import re +import time +import uuid +from collections.abc import Callable +from contextlib import contextmanager +from dataclasses import asdict, dataclass + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from pydantic import TypeAdapter +from sglang.srt.entrypoints.openai.protocol import Tool +from sglang.srt.function_call.function_call_parser import FunctionCallParser +from transformers import AutoTokenizer + +from miles.utils.http_utils import find_available_port +from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer + + +@dataclass(frozen=True) +class ProcessResultMetaInfo: + weight_version: str | None = None + routed_experts: str | None = None + spec_accept_token_num: int | None = None + spec_draft_token_num: int | None = None + spec_verify_ct: int | None = None + + def to_dict(self) -> dict: + return {k: v for k, v in asdict(self).items() if v is not None} + + +@dataclass(frozen=True) +class ProcessResult: + text: str + finish_reason: str = "stop" + cached_tokens: int = 0 + meta_info: ProcessResultMetaInfo = ProcessResultMetaInfo() + + +ProcessFn = Callable[[str], ProcessResult] + + +class MockSGLangServer: + def __init__( + self, + model_name: str, + process_fn: ProcessFn, + host: str, + port: int, + latency: float = 0.0, + ): + self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) + self.process_fn = process_fn + self.host = host + self.port = port or find_available_port(30000) + self.latency = latency + + self.app = FastAPI() + self._server: UvicornThreadServer | None = None + + self.request_log: list[dict] = [] + self._concurrency = Counter() + + self._setup_routes() + + @property + def max_concurrent(self) -> int: + return self._concurrency.max_value + + def reset_stats(self): + self.request_log.clear() + self._concurrency.reset() + + def start(self): + self._server = UvicornThreadServer(self.app, host=self.host, port=self.port) + self._server.start() + + def stop(self): + if self._server is not None: + self._server.stop() + + @property + def url(self) -> str: + return f"http://{self.host}:{self.port}" + + def _setup_routes(self): + @self.app.post("/generate") + async def generate(request: Request): + return await self._handle_generate_like_request(request, self._compute_generate_response) + + @self.app.post("/v1/chat/completions") + async def chat_completions(request: Request): + return await self._handle_generate_like_request(request, self._compute_chat_completions_response) + + @self.app.get("/health") + async def health(): + return JSONResponse(content={"status": "ok"}) + + @self.app.post("/abort_request") + async def abort_request(_request: Request): + return JSONResponse(content={"status": "ok"}) + + async def _handle_generate_like_request(self, request: Request, compute_fn: Callable[[dict], dict]): + payload = await request.json() + self.request_log.append(payload) + with self._concurrency.track(): + if self.latency > 0: + await asyncio.sleep(self.latency) + response = compute_fn(payload) + return JSONResponse(content=response) + + def _compute_generate_response(self, payload: dict) -> dict: + assert payload.get("return_logprob", True) is True, "MockSGLangServer requires return_logprob=True" + input_ids = payload.get("input_ids", []) + + prompt_str = self.tokenizer.decode(input_ids, skip_special_tokens=False) + process_result = self.process_fn(prompt_str) + output_ids = self.tokenizer.encode(process_result.text, add_special_tokens=False) + + prompt_tokens = len(input_ids) + completion_tokens = len(output_ids) + + finish_reason_dict = {"type": process_result.finish_reason} + if process_result.finish_reason == "length": + finish_reason_dict["length"] = completion_tokens + + output_token_logprobs = [(-1 / 128 * i, token_id) for i, token_id in enumerate(output_ids)] + + meta_info = { + "finish_reason": finish_reason_dict, + "prompt_tokens": prompt_tokens, + "cached_tokens": process_result.cached_tokens, + "completion_tokens": completion_tokens, + "output_token_logprobs": output_token_logprobs, + **process_result.meta_info.to_dict(), + } + + return {"text": process_result.text, "meta_info": meta_info} + + def _compute_chat_completions_response(self, payload: dict) -> dict: + messages = payload.get("messages", []) + tools = payload.get("tools") + + prompt_str = self.tokenizer.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True, tools=tools + ) + + process_result = self.process_fn(prompt_str) + output_ids = self.tokenizer.encode(process_result.text, add_special_tokens=False) + + logprobs_content = [ + {"token": self.tokenizer.convert_ids_to_tokens(tid), "logprob": -1 / 128 * i} + for i, tid in enumerate(output_ids) + ] + + finish_reason = process_result.finish_reason + tool_calls = None + if tools and finish_reason == "stop": + parser = FunctionCallParser( + tools=TypeAdapter(list[Tool]).validate_python(tools), + tool_call_parser="qwen25", + ) + message_content, parsed_calls = parser.parse_non_stream(process_result.text) + if parsed_calls: + finish_reason = "tool_calls" + tool_calls = [ + { + "id": f"call{i:05d}", + "type": "function", + "function": {"name": call.name, "arguments": call.parameters or "{}"}, + } + for i, call in enumerate(parsed_calls) + ] + else: + message_content = process_result.text + + return { + "id": f"chatcmpl-{uuid.uuid4().hex[:8]}", + "object": "chat.completion", + "created": int(time.time()), + "model": "mock-model", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": message_content, + "tool_calls": tool_calls, + }, + "logprobs": {"content": logprobs_content}, + "finish_reason": finish_reason, + } + ], + } + + +class Counter: + def __init__(self): + self._current = 0 + self._max = 0 + + @property + def max_value(self) -> int: + return self._max + + def reset(self): + self._current = 0 + self._max = 0 + + @contextmanager + def track(self): + self._current += 1 + self._max = max(self._max, self._current) + try: + yield + finally: + self._current -= 1 + + +def default_process_fn(prompt: str) -> ProcessResult: + match = re.search(r"What is 1\+(\d+)\?", prompt) + if match: + num = int(match.group(1)) + ans = 1 + num + return ProcessResult(text=f"\\boxed{{{ans}}}", finish_reason="stop") + return ProcessResult(text="I don't understand.", finish_reason="stop") + + +@contextmanager +def with_mock_server( + model_name: str = "Qwen/Qwen3-0.6B", + process_fn: ProcessFn = default_process_fn, + host: str = "127.0.0.1", + port: int | None = None, + latency: float = 0.0, +): + server = MockSGLangServer( + model_name=model_name, + process_fn=process_fn, + host=host, + port=port, + latency=latency, + ) + try: + server.start() + yield server + finally: + server.stop() diff --git a/miles/utils/test_utils/mock_tools.py b/miles/utils/test_utils/mock_tools.py new file mode 100644 index 000000000..6b99e3673 --- /dev/null +++ b/miles/utils/test_utils/mock_tools.py @@ -0,0 +1,268 @@ +import json + +from transformers import AutoTokenizer + +from miles.utils.test_utils.mock_sglang_server import ProcessResult + +SAMPLE_TOOLS = [ + { + "type": "function", + "function": { + "name": "get_year", + "description": "Get current year", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_temperature", + "description": "Get temperature for a location", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + }, + }, +] + + +def _get_year(params: dict) -> str: + assert len(params) == 0 + return json.dumps({"year": 2026}) + + +def _get_temperature(params: dict) -> str: + temps = {"Mars": -60, "Earth": 15} + location = params.get("location") + assert location in temps, f"Unknown location: {location}" + return json.dumps({"temperature": temps[location]}) + + +TOOL_EXECUTORS = { + "get_year": _get_year, + "get_temperature": _get_temperature, +} + + +async def execute_tool_call(name: str, params: dict) -> str: + return TOOL_EXECUTORS[name](params) + + +_SYSTEM_PROMPT = ( + "<|im_start|>system\n" + "# Tools\n" + "\n" + "You may call one or more functions to assist with the user query.\n" + "\n" + "You are provided with function signatures within XML tags:\n" + "\n" + '{"type": "function", "function": {"name": "get_year", "description": "Get current year", "parameters": {"type": "object", "properties": {}, "required": []}}}\n' + '{"type": "function", "function": {"name": "get_temperature", "description": "Get temperature for a location", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}}\n' + "\n" + "\n" + "For each function call, return a json object with function name and arguments within XML tags:\n" + "\n" + '{"name": , "arguments": }\n' + "<|im_end|>\n" +) + + +_TOKENIZER = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B", trust_remote_code=True) + + +class TwoTurnStub: + """Stub for 2-turn: get_year + get_temperature(Mars) -> final answer""" + + USER_QUESTION = "What is 42 + year + temperature?" + + FIRST_RESPONSE = ( + "Let me get the year and temperature first.\n" + "\n" + '{"name": "get_year", "arguments": {}}\n' + "\n" + "\n" + '{"name": "get_temperature", "arguments": {"location": "Mars"}}\n' + "<|im_end|>\n" + ) + + FIRST_TOOL_RESPONSE = ( + "<|im_start|>user\n" + "\n" + '{"year": 2026}\n' + "\n" + "\n" + '{"temperature": -60}\n' + "<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + SECOND_RESPONSE = "The answer is: 42 + 2026 + -60 = 2008." + + FIRST_PROMPT = _SYSTEM_PROMPT + "<|im_start|>user\n" + USER_QUESTION + "<|im_end|>\n" + "<|im_start|>assistant\n" + SECOND_PROMPT = FIRST_PROMPT + FIRST_RESPONSE + FIRST_TOOL_RESPONSE + + PROMPT = [{"role": "user", "content": USER_QUESTION}] + + FIRST_PROMPT_TOKEN_IDS = _TOKENIZER(FIRST_PROMPT, add_special_tokens=False)["input_ids"] + SECOND_PROMPT_TOKEN_IDS = _TOKENIZER(SECOND_PROMPT, add_special_tokens=False)["input_ids"] + + FIRST_RESPONSE_CONTENT = "Let me get the year and temperature first." + FIRST_TOOL_CALLS_OPENAI_FORMAT = [ + {"id": "call00000", "function": {"arguments": "{}", "name": "get_year"}, "type": "function"}, + { + "id": "call00001", + "function": {"arguments": '{"location": "Mars"}', "name": "get_temperature"}, + "type": "function", + }, + ] + + OPENAI_MESSAGES_FIRST_TURN = [{"role": "user", "content": USER_QUESTION}] + + OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT = OPENAI_MESSAGES_FIRST_TURN + [ + { + "content": FIRST_RESPONSE_CONTENT, + "refusal": None, + "role": "assistant", + "annotations": None, + "audio": None, + "function_call": None, + "tool_calls": FIRST_TOOL_CALLS_OPENAI_FORMAT, + }, + {"role": "tool", "tool_call_id": "call00000", "content": '{"year": 2026}', "name": "get_year"}, + {"role": "tool", "tool_call_id": "call00001", "content": '{"temperature": -60}', "name": "get_temperature"}, + ] + + @staticmethod + def process_fn(prompt: str) -> ProcessResult: + prompt_response_pairs = { + TwoTurnStub.FIRST_PROMPT: TwoTurnStub.FIRST_RESPONSE, + TwoTurnStub.SECOND_PROMPT: TwoTurnStub.SECOND_RESPONSE, + } + + for expect_prompt, response in prompt_response_pairs.items(): + if prompt == expect_prompt: + return ProcessResult(text=response, finish_reason="stop") + + raise ValueError(f"Unexpected {prompt=}") + + +class ThreeTurnStub: + """Stub for 3-turn: get_year + get_temperature(Mars) -> get_temperature(Earth) -> final answer""" + + USER_QUESTION = "What is 42 + year + Mars temperature + Earth temperature?" + + FIRST_RESPONSE = ( + "Let me get the year and Mars temperature first.\n" + "\n" + '{"name": "get_year", "arguments": {}}\n' + "\n" + "\n" + '{"name": "get_temperature", "arguments": {"location": "Mars"}}\n' + "<|im_end|>\n" + ) + + SECOND_RESPONSE = ( + "Now let me get Earth temperature.\n" + "\n" + '{"name": "get_temperature", "arguments": {"location": "Earth"}}\n' + "<|im_end|>\n" + ) + + FIRST_TOOL_RESPONSE = ( + "<|im_start|>user\n" + "\n" + '{"year": 2026}\n' + "\n" + "\n" + '{"temperature": -60}\n' + "<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + SECOND_TOOL_RESPONSE = ( + "<|im_start|>user\n" + "\n" + '{"temperature": 15}\n' + "<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + THIRD_RESPONSE = "The answer is: 42 + 2026 + -60 + 15 = 2023." + + FIRST_PROMPT = _SYSTEM_PROMPT + "<|im_start|>user\n" + USER_QUESTION + "<|im_end|>\n" + "<|im_start|>assistant\n" + SECOND_PROMPT = FIRST_PROMPT + FIRST_RESPONSE + FIRST_TOOL_RESPONSE + THIRD_PROMPT = SECOND_PROMPT + SECOND_RESPONSE + SECOND_TOOL_RESPONSE + + PROMPT = [{"role": "user", "content": USER_QUESTION}] + + FIRST_PROMPT_TOKEN_IDS = _TOKENIZER(FIRST_PROMPT, add_special_tokens=False)["input_ids"] + SECOND_PROMPT_TOKEN_IDS = _TOKENIZER(SECOND_PROMPT, add_special_tokens=False)["input_ids"] + THIRD_PROMPT_TOKEN_IDS = _TOKENIZER(THIRD_PROMPT, add_special_tokens=False)["input_ids"] + + FIRST_RESPONSE_CONTENT = "Let me get the year and Mars temperature first." + FIRST_TOOL_CALLS_OPENAI_FORMAT = [ + {"id": "call00000", "function": {"arguments": "{}", "name": "get_year"}, "type": "function"}, + { + "id": "call00001", + "function": {"arguments": '{"location": "Mars"}', "name": "get_temperature"}, + "type": "function", + }, + ] + + SECOND_RESPONSE_CONTENT = "Now let me get Earth temperature." + SECOND_TOOL_CALLS_OPENAI_FORMAT = [ + { + "id": "call00000", + "function": {"arguments": '{"location": "Earth"}', "name": "get_temperature"}, + "type": "function", + }, + ] + + OPENAI_MESSAGES_FIRST_TURN = [{"role": "user", "content": USER_QUESTION}] + + OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT = OPENAI_MESSAGES_FIRST_TURN + [ + { + "content": FIRST_RESPONSE_CONTENT, + "refusal": None, + "role": "assistant", + "annotations": None, + "audio": None, + "function_call": None, + "tool_calls": FIRST_TOOL_CALLS_OPENAI_FORMAT, + }, + {"role": "tool", "tool_call_id": "call00000", "content": '{"year": 2026}', "name": "get_year"}, + {"role": "tool", "tool_call_id": "call00001", "content": '{"temperature": -60}', "name": "get_temperature"}, + ] + + OPENAI_MESSAGES_THIRD_TURN_FROM_CLIENT = OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT + [ + { + "content": SECOND_RESPONSE_CONTENT, + "refusal": None, + "role": "assistant", + "annotations": None, + "audio": None, + "function_call": None, + "tool_calls": SECOND_TOOL_CALLS_OPENAI_FORMAT, + }, + {"role": "tool", "tool_call_id": "call00000", "content": '{"temperature": 15}', "name": "get_temperature"}, + ] + + @staticmethod + def process_fn(prompt: str) -> ProcessResult: + prompt_response_pairs = { + ThreeTurnStub.FIRST_PROMPT: ThreeTurnStub.FIRST_RESPONSE, + ThreeTurnStub.SECOND_PROMPT: ThreeTurnStub.SECOND_RESPONSE, + ThreeTurnStub.THIRD_PROMPT: ThreeTurnStub.THIRD_RESPONSE, + } + + for expect_prompt, response in prompt_response_pairs.items(): + if prompt == expect_prompt: + return ProcessResult(text=response, finish_reason="stop") + + raise ValueError(f"Unexpected {prompt=}") diff --git a/miles/utils/test_utils/uvicorn_thread_server.py b/miles/utils/test_utils/uvicorn_thread_server.py new file mode 100644 index 000000000..904343c98 --- /dev/null +++ b/miles/utils/test_utils/uvicorn_thread_server.py @@ -0,0 +1,49 @@ +import asyncio +import socket +import threading +import time + +import uvicorn + + +class UvicornThreadServer: + def __init__(self, app, host: str, port: int): + self._app = app + self.host = host + self.port = port + self._server: uvicorn.Server | None = None + self._thread: threading.Thread | None = None + + @property + def url(self) -> str: + return f"http://{self.host}:{self.port}" + + def start(self) -> None: + config = uvicorn.Config(self._app, host=self.host, port=self.port, log_level="info") + self._server = uvicorn.Server(config) + + def run() -> None: + asyncio.run(self._server.serve()) + + self._thread = threading.Thread(target=run, daemon=True) + self._thread.start() + self._wait_for_port_open() + + def stop(self) -> None: + if self._server is not None: + self._server.should_exit = True + if self._thread is not None and self._thread.is_alive(): + self._thread.join(timeout=2.0) + + def _wait_for_port_open(self) -> None: + for _ in range(50): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + result = sock.connect_ex((self.host, self.port)) + sock.close() + if result == 0: + return + except Exception: + pass + time.sleep(0.1) + raise RuntimeError(f"Failed to start server on {self.url}") diff --git a/miles/utils/types.py b/miles/utils/types.py index 0a2531a7a..5200d625e 100644 --- a/miles/utils/types.py +++ b/miles/utils/types.py @@ -145,6 +145,24 @@ def get_reward_value(self, args) -> float: def effective_response_length(self): return sum(self.loss_mask) if self.loss_mask is not None else self.response_length + def validate(self): + assert self.response_length >= 0, f"response_length must be >= 0, got {self.response_length}" + assert ( + len(self.tokens) >= self.response_length + ), f"tokens length ({len(self.tokens)}) must be >= response_length ({self.response_length})" + if self.loss_mask is not None: + assert ( + len(self.loss_mask) == self.response_length + ), f"loss_mask length ({len(self.loss_mask)}) != response_length ({self.response_length})" + if self.rollout_log_probs is not None: + assert ( + len(self.rollout_log_probs) == self.response_length + ), f"rollout_log_probs length ({len(self.rollout_log_probs)}) != response_length ({self.response_length})" + if self.rollout_routed_experts is not None: + actual = len(self.rollout_routed_experts) + expect = len(self.tokens) - 1 + assert actual == expect, f"rollout_routed_experts length ({actual}) != len(tokens) - 1 ({expect})" + def update_from_meta_info(self, args, meta_info: dict): """ Update the sample with new information from meta_info returned by the rollout engine. diff --git a/requirements.txt b/requirements.txt index 2c20195fc..dacd51132 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ mcp[cli] memray # needed for debugging (but is lightweight), we can put it to dev mode when using pyproject.toml omegaconf pillow +pybase64 pylatexenc pyyaml qwen_vl_utils # for VLM diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/ci/gpu_lock_exec.py b/tests/ci/gpu_lock_exec.py index 9507e2e85..20379f76a 100644 --- a/tests/ci/gpu_lock_exec.py +++ b/tests/ci/gpu_lock_exec.py @@ -19,11 +19,14 @@ def main(): _execute_print_only(args) return - fd_locks = _try_acquire(args) + if args.count == 0 and not args.devices: + print("[gpu_lock_exec] Do not acquire GPU since count=0", flush=True) + else: + fd_locks = _try_acquire(args) - dev_list = ",".join(str(x.gpu_id) for x in fd_locks) - os.environ[args.target_env_name] = dev_list - print(f"[gpu_lock_exec] Acquired GPUs: {dev_list}", flush=True) + dev_list = ",".join(str(x.gpu_id) for x in fd_locks) + os.environ[args.target_env_name] = dev_list + print(f"[gpu_lock_exec] Acquired GPUs: {dev_list}", flush=True) _os_execvp(args) diff --git a/tests/e2e/.gitkeep b/tests/e2e/.gitkeep new file mode 100644 index 000000000..615f2b076 --- /dev/null +++ b/tests/e2e/.gitkeep @@ -0,0 +1 @@ +# TODO: may move e2e tests to this folder \ No newline at end of file diff --git a/tests/fast/__init__.py b/tests/fast/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/conftest.py b/tests/fast/conftest.py new file mode 100644 index 000000000..4cb30e91f --- /dev/null +++ b/tests/fast/conftest.py @@ -0,0 +1,15 @@ +import os + +import pytest + +from tests.fast.fixtures.generation_fixtures import generation_env +from tests.fast.fixtures.rollout_fixtures import rollout_env + +_ = rollout_env, generation_env + + +@pytest.fixture(autouse=True) +def enable_experimental_rollout_refactor(): + os.environ["MILES_EXPERIMENTAL_ROLLOUT_REFACTOR"] = "1" + yield + os.environ.pop("MILES_EXPERIMENTAL_ROLLOUT_REFACTOR", None) diff --git a/tests/fast/fixtures/__init__.py b/tests/fast/fixtures/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/fast/fixtures/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/fast/fixtures/generation_fixtures.py b/tests/fast/fixtures/generation_fixtures.py new file mode 100644 index 000000000..816371ee3 --- /dev/null +++ b/tests/fast/fixtures/generation_fixtures.py @@ -0,0 +1,274 @@ +""" +Fixtures to test custom-generate-function +""" + +from argparse import Namespace +from contextlib import contextmanager +from dataclasses import dataclass +from types import SimpleNamespace +from typing import Any +from unittest.mock import patch + +import pytest +import requests + +from miles.rollout.base_types import GenerateFnInput +from miles.rollout.inference_rollout.compatibility import load_generate_function +from miles.rollout.inference_rollout.inference_rollout_common import GenerateState +from miles.router.router import MilesRouter +from miles.utils.async_utils import run +from miles.utils.http_utils import find_available_port, init_http_client +from miles.utils.misc import SingletonMeta +from miles.utils.test_utils.mock_sglang_server import ProcessResult, ProcessResultMetaInfo, with_mock_server +from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer +from miles.utils.types import Sample + +MODEL_NAME = "Qwen/Qwen3-0.6B" +RESPONSE_TEXT = "\\boxed{8}" +DEFAULT_SAMPLING_PARAMS = {"max_new_tokens": 64, "temperature": 0.7} + +VARIANT_TO_GENERATE_FN_PATH = { + "old_sglang_rollout": "miles.rollout.sglang_rollout.generate", + "single_turn": "miles.rollout.generate_hub.single_turn.generate", + "multi_turn_single_sample": "miles.rollout.generate_hub.multi_turn.generate", + "multi_turn_multi_samples": "miles.rollout.generate_hub.multi_turn.generate", + "agentic_tool_call_single_sample": "miles.rollout.generate_hub.agentic_tool_call.generate", + "agentic_tool_call_multi_samples": "miles.rollout.generate_hub.agentic_tool_call.generate", +} + + +def extra_argv_for_variant( + variant: str, + *, + custom_generate_function_path: str | None = None, + generate_max_turns: int = 16, + generate_tool_specs_path: str = "miles.utils.test_utils.mock_tools.SAMPLE_TOOLS", + generate_tool_call_parser: str = "qwen25", + generate_execute_tool_function_path: str = "miles.utils.test_utils.mock_tools.execute_tool_call", +) -> list[str]: + argv = [ + "--custom-generate-function-path", + custom_generate_function_path or VARIANT_TO_GENERATE_FN_PATH[variant], + ] + + if variant in ( + "multi_turn_single_sample", + "multi_turn_multi_samples", + "agentic_tool_call_single_sample", + "agentic_tool_call_multi_samples", + ): + argv += [ + "--generate-max-turns", + str(generate_max_turns), + "--generate-tool-specs-path", + generate_tool_specs_path, + "--generate-execute-tool-function-path", + generate_execute_tool_function_path, + ] + if variant in ("multi_turn_single_sample", "multi_turn_multi_samples"): + argv += ["--generate-tool-call-parser", generate_tool_call_parser] + if variant in ("multi_turn_multi_samples", "agentic_tool_call_multi_samples"): + argv.append("--generate-multi-samples") + + return argv + + +def listify(x): + return x if isinstance(x, list) else [x] + + +def make_sample( + *, + prompt: str | list[dict] = "What is 1+7?", + tokens: list[int] | None = None, + response: str = "", + response_length: int = 0, + status: Sample.Status = Sample.Status.PENDING, + multimodal_inputs: dict | None = None, +) -> Sample: + return Sample( + prompt=prompt, + tokens=tokens or [], + response=response, + response_length=response_length, + status=status, + multimodal_inputs=multimodal_inputs, + ) + + +@dataclass +class GenerateEnv: + args: Namespace + mock_server: Any + + +@dataclass +class GenerateResult: + sample: Sample | list[Sample] + requests: list[dict] + + +def run_generate( + env: GenerateEnv, + sample: Sample, + sampling_params: dict[str, Any] | None = None, + *, + variant: str = "single_turn", +) -> GenerateResult: + env.mock_server.request_log.clear() + result_sample = run( + _call_generate( + env.args, + sample, + sampling_params or DEFAULT_SAMPLING_PARAMS, + variant=variant, + ) + ) + return GenerateResult(sample=result_sample, requests=list(env.mock_server.request_log)) + + +async def _call_generate( + args: Namespace, + sample: Sample, + sampling_params: dict[str, Any], + *, + variant: str = "single_turn", +) -> Sample: + generate_fn = load_generate_function(VARIANT_TO_GENERATE_FN_PATH[variant]) + state = GenerateState(args) + input = GenerateFnInput(state=state, sample=sample, sampling_params=sampling_params.copy(), evaluation=False) + output = await generate_fn(input) + return output.samples + + +def make_args( + *, + variant: str, + router_port: int, + use_rollout_routing_replay: bool = False, + sglang_speculative_algorithm: str | None = None, + model_name: str = MODEL_NAME, + extra_argv: list[str] | None = None, + custom_generate_function_path: str | None = None, + generate_max_turns: int = 16, + generate_tool_specs_path: str = "miles.utils.test_utils.mock_tools.SAMPLE_TOOLS", + generate_tool_call_parser: str = "qwen25", + generate_execute_tool_function_path: str = "miles.utils.test_utils.mock_tools.execute_tool_call", + rollout_max_context_len: int | None = None, +) -> Namespace: + argv = [ + "pytest", + "--train-backend", + "fsdp", + "--rollout-batch-size", + "1", + "--num-rollout", + "1", + "--rollout-num-gpus", + "1", + "--rollout-num-gpus-per-engine", + "1", + "--hf-checkpoint", + model_name, + "--prompt-data", + "/dev/null", + "--rm-type", + "math", + "--sglang-router-ip", + "127.0.0.1", + "--sglang-router-port", + str(router_port), + "--rollout-max-response-len", + "16", + ] + if use_rollout_routing_replay: + argv.append("--use-rollout-routing-replay") + if sglang_speculative_algorithm: + argv.extend(["--sglang-speculative-algorithm", sglang_speculative_algorithm]) + if rollout_max_context_len is not None: + argv.extend(["--rollout-max-context-len", str(rollout_max_context_len)]) + + argv.extend( + extra_argv_for_variant( + variant, + custom_generate_function_path=custom_generate_function_path, + generate_max_turns=generate_max_turns, + generate_tool_specs_path=generate_tool_specs_path, + generate_tool_call_parser=generate_tool_call_parser, + generate_execute_tool_function_path=generate_execute_tool_function_path, + ) + ) + + if extra_argv: + argv.extend(extra_argv) + + from miles.utils.arguments import parse_args + + with patch("sys.argv", argv): + args = parse_args() + + init_http_client(args) + return args + + +@contextmanager +def with_miles_router(backend_url: str, model_name: str): + router_args = SimpleNamespace( + miles_router_max_connections=10, + miles_router_timeout=30, + miles_router_middleware_paths=[], + rollout_health_check_interval=60, + miles_router_health_check_failure_threshold=3, + hf_checkpoint=model_name, + ) + router = MilesRouter(router_args) + + port = find_available_port(31000) + server = UvicornThreadServer(router.app, host="127.0.0.1", port=port) + server.start() + + url = f"http://127.0.0.1:{port}" + requests.post(f"{url}/add_worker", json={"url": backend_url}) + + try: + yield port + finally: + server.stop() + + +@pytest.fixture +def generation_env(request, variant): + SingletonMeta.clear_all_instances() + params = getattr(request, "param", {}) + args_kwargs = params.get("args_kwargs", {}) + model_name = args_kwargs.get("model_name", MODEL_NAME) + custom_generate_function_path = VARIANT_TO_GENERATE_FN_PATH[variant] + + def process_fn(_): + x = params.get("process_fn_kwargs", {}) + return ProcessResult( + text=x.get("response_text", RESPONSE_TEXT), + finish_reason=x.get("finish_reason", "stop"), + cached_tokens=x.get("cached_tokens", 0), + meta_info=ProcessResultMetaInfo( + weight_version=x.get("weight_version"), + routed_experts=x.get("routed_experts"), + spec_accept_token_num=x.get("spec_accept_token_num"), + spec_draft_token_num=x.get("spec_draft_token_num"), + spec_verify_ct=x.get("spec_verify_ct"), + ), + ) + + with with_mock_server(model_name=model_name, process_fn=process_fn) as mock_server: + with with_miles_router(mock_server.url, model_name) as router_port: + other_args_kwargs = {k: v for k, v in args_kwargs.items() if k != "model_name"} + args = make_args( + variant=variant, + router_port=router_port, + model_name=model_name, + custom_generate_function_path=custom_generate_function_path, + **other_args_kwargs, + ) + yield GenerateEnv(args=args, mock_server=mock_server) + + SingletonMeta.clear_all_instances() diff --git a/tests/fast/fixtures/rollout_fixtures.py b/tests/fast/fixtures/rollout_fixtures.py new file mode 100644 index 000000000..44d8a50d7 --- /dev/null +++ b/tests/fast/fixtures/rollout_fixtures.py @@ -0,0 +1,127 @@ +""" +Fixtures to test rollout-function +""" + +import json +from argparse import Namespace +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +from unittest.mock import patch + +import pytest +import requests + +from miles.rollout.data_source import DataSource, RolloutDataSourceWithBuffer +from miles.router.router import MilesRouter +from miles.utils.arguments import parse_args +from miles.utils.http_utils import find_available_port, init_http_client +from miles.utils.misc import SingletonMeta +from miles.utils.test_utils.mock_sglang_server import MockSGLangServer, with_mock_server +from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer + + +@dataclass(frozen=True) +class RolloutEnvConfig: + extra_argv: list[str] | None = None + data_rows: list[dict] | None = None + latency: float = 0.0 + + +@dataclass(frozen=True) +class RolloutEnv: + args: Namespace + data_source: DataSource + mock_server: MockSGLangServer + + +def _build_args(*, data_path: str, router_port: int, extra_argv: list[str] | None = None) -> Namespace: + argv = [ + "pytest", + "--train-backend", + "fsdp", + "--rollout-batch-size", + "1", + "--n-samples-per-prompt", + "1", + "--num-rollout", + "1", + "--rollout-num-gpus", + "1", + "--rollout-num-gpus-per-engine", + "1", + "--hf-checkpoint", + "Qwen/Qwen3-0.6B", + "--prompt-data", + data_path, + "--input-key", + "input", + "--label-key", + "label", + "--rm-type", + "math", + "--eval-prompt-data", + "toy", + data_path, + "--use-miles-router", + "--sglang-router-ip", + "127.0.0.1", + "--sglang-router-port", + str(router_port), + "--rollout-max-response-len", + "16", + ] + (extra_argv or []) + with patch("sys.argv", argv): + args = parse_args() + args.miles_router_middleware_paths = [] + init_http_client(args) + return args + + +@contextmanager +def _with_miles_router(args: Namespace) -> Iterator[UvicornThreadServer]: + router = MilesRouter(args, verbose=False) + server = UvicornThreadServer(router.app, host=args.sglang_router_ip, port=args.sglang_router_port) + try: + server.start() + yield server + finally: + server.stop() + + +def _write_jsonl(path: str, rows: list[dict]) -> None: + Path(path).write_text("".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows), encoding="utf-8") + + +DEFAULT_DATA_ROWS = [{"input": "What is 1+7?", "label": "8"}] + + +@pytest.fixture +def rollout_env(tmp_path, request) -> RolloutEnv: + config = request.param + assert isinstance(config, RolloutEnvConfig) + + data_rows = config.data_rows or DEFAULT_DATA_ROWS + + data_path = str(tmp_path / "data.jsonl") + _write_jsonl(data_path, data_rows) + + router_port = find_available_port(20000) + args = _build_args(data_path=data_path, router_port=router_port, extra_argv=config.extra_argv) + + SingletonMeta.clear_all_instances() + + with with_mock_server(model_name=args.hf_checkpoint, latency=config.latency) as mock_server: + with _with_miles_router(args) as router_server: + r = requests.post( + f"{router_server.url}/add_worker", + params={"url": mock_server.url}, + timeout=5.0, + ) + r.raise_for_status() + + data_source = RolloutDataSourceWithBuffer(args) + yield RolloutEnv(args=args, data_source=data_source, mock_server=mock_server) + + SingletonMeta.clear_all_instances() diff --git a/tests/fast/rollout/__init__.py b/tests/fast/rollout/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/rollout/generate_hub/__init__.py b/tests/fast/rollout/generate_hub/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/rollout/generate_hub/test_multi_turn.py b/tests/fast/rollout/generate_hub/test_multi_turn.py new file mode 100644 index 000000000..5d974aaad --- /dev/null +++ b/tests/fast/rollout/generate_hub/test_multi_turn.py @@ -0,0 +1,572 @@ +from copy import deepcopy +from dataclasses import dataclass, replace +from itertools import groupby + +import numpy as np +import pybase64 +import pytest +from tests.fast.fixtures.generation_fixtures import GenerateEnv, generation_env, listify, make_sample, run_generate +from transformers import AutoTokenizer + +from miles.utils.test_utils.mock_sglang_server import ProcessResult, ProcessResultMetaInfo +from miles.utils.test_utils.mock_tools import SAMPLE_TOOLS, ThreeTurnStub, TwoTurnStub +from miles.utils.types import Sample + +_ = generation_env, SAMPLE_TOOLS, TwoTurnStub, ThreeTurnStub + + +def is_agentic_variant(variant: str) -> bool: + return variant in ("agentic_tool_call_single_sample", "agentic_tool_call_multi_samples") + + +# ------------------------------------ fixtures and consts ---------------------------------------- + + +MODEL_NAME = "Qwen/Qwen3-0.6B" +DEFAULT_SAMPLING_PARAMS = {"max_new_tokens": 64, "temperature": 0.7} +TOKENIZER = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) + + +@pytest.fixture( + params=[ + "multi_turn_single_sample", + "multi_turn_multi_samples", + "agentic_tool_call_single_sample", + "agentic_tool_call_multi_samples", + ] +) +def variant(request): + return request.param + + +@dataclass(frozen=True) +class SampleParsedChunk: + tokens_decoded_str: str + loss_mask_value: int + rollout_log_probs: list[float] + + +@dataclass +class ExpectedSampleInfo: + chunks: list[SampleParsedChunk] + partial_sample: Sample + + +def token_len(text: str) -> int: + return len(TOKENIZER(text, add_special_tokens=False)["input_ids"]) + + +def expected_chunk(text: str, loss_mask: int) -> SampleParsedChunk: + n = token_len(text) + log_probs = [-1 / 128 * i for i in range(n)] if loss_mask else [0.0] * n + return SampleParsedChunk(text, loss_mask, log_probs) + + +def parse_sample_into_chunks(sample: Sample, tokenizer) -> list[SampleParsedChunk]: + prompt_len = len(sample.tokens) - sample.response_length + response_tokens = sample.tokens[prompt_len:] + loss_mask = sample.loss_mask or [] + log_probs = sample.rollout_log_probs or [] + + chunks = [] + idx = 0 + for mask_val, group in groupby(loss_mask): + group_len = len(list(group)) + sli = slice(idx, idx + group_len) + chunks.append( + SampleParsedChunk( + tokens_decoded_str=tokenizer.decode(response_tokens[sli]), + loss_mask_value=mask_val, + rollout_log_probs=log_probs[sli], + ) + ) + idx += group_len + return chunks + + +def expected_partial_sample( + *, + prompt: list[dict], + response: str, + response_length: int, + status: Sample.Status = Sample.Status.COMPLETED, +) -> Sample: + return Sample( + prompt=prompt, + response=response, + response_length=response_length, + status=status, + tokens=[], + loss_mask=[], + rollout_log_probs=[], + weight_versions=[], + spec_info=Sample.SpecInfo(), + prefix_cache_info=Sample.PrefixCacheInfo(), + ) + + +def verify_samples(actual: Sample | list[Sample], expected: list[ExpectedSampleInfo]): + actual = listify(actual) + assert len(actual) == len(expected) + + for actual_item, expected_item in zip(actual, expected, strict=True): + actual_chunks = parse_sample_into_chunks(actual_item, TOKENIZER) + assert actual_chunks == expected_item.chunks + + actual_partial = replace( + deepcopy(actual_item), + tokens=[], + loss_mask=[], + rollout_log_probs=[], + prefix_cache_info=Sample.PrefixCacheInfo(), + ) + assert actual_partial == expected_item.partial_sample + + +def _run_generate(variant: str, env: GenerateEnv, sample: Sample, sampling_params: dict | None = None): + return run_generate(env, sample, sampling_params, variant=variant) + + +def expected_request(input_ids: list[int], sampling_params: dict | None = None) -> dict: + return { + "input_ids": input_ids, + "sampling_params": sampling_params or DEFAULT_SAMPLING_PARAMS, + "return_logprob": True, + "return_routed_experts": False, + } + + +def expected_openai_request(messages: list[dict]) -> dict: + return {"messages": messages, "model": "default", "tools": SAMPLE_TOOLS} + + +SINGLE_TURN_PROMPT = [{"role": "user", "content": "What is 1+1?"}] +SINGLE_TURN_RESPONSE = "The answer is 2." +_SINGLE_TURN_PROMPT_TEXT = TOKENIZER.apply_chat_template( + SINGLE_TURN_PROMPT, tokenize=False, add_generation_prompt=True, tools=SAMPLE_TOOLS +) +SINGLE_TURN_PROMPT_TOKEN_IDS = TOKENIZER(_SINGLE_TURN_PROMPT_TEXT, add_special_tokens=False)["input_ids"] +SINGLE_TURN_PROMPT_TOKEN_LEN = len(SINGLE_TURN_PROMPT_TOKEN_IDS) + + +# ------------------------------------ tests ---------------------------------------- + + +class TestBasicMultiTurn: + def test_single_turn_no_tool_call(self, variant, generation_env): + generation_env.mock_server.process_fn = lambda _: ProcessResult( + text=SINGLE_TURN_RESPONSE, finish_reason="stop" + ) + + result = _run_generate(variant, generation_env, make_sample(prompt=SINGLE_TURN_PROMPT)) + + if is_agentic_variant(variant): + assert result.requests == [expected_openai_request(SINGLE_TURN_PROMPT)] + else: + assert result.requests == [expected_request(SINGLE_TURN_PROMPT_TOKEN_IDS)] + verify_samples( + result.sample, + [ + ExpectedSampleInfo( + chunks=[ + SampleParsedChunk( + tokens_decoded_str=SINGLE_TURN_RESPONSE, + loss_mask_value=1, + rollout_log_probs=[-1 / 128 * i for i in range(6)], + ), + ], + partial_sample=expected_partial_sample( + prompt=SINGLE_TURN_PROMPT, response=SINGLE_TURN_RESPONSE, response_length=6 + ), + ), + ], + ) + + def test_two_turns_with_tool_call(self, variant, generation_env): + generation_env.mock_server.process_fn = TwoTurnStub.process_fn + + S = TwoTurnStub + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT)) + + if is_agentic_variant(variant): + assert result.requests == [ + expected_openai_request(S.OPENAI_MESSAGES_FIRST_TURN), + expected_openai_request(S.OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT), + ] + else: + assert result.requests == [ + expected_request(S.FIRST_PROMPT_TOKEN_IDS), + expected_request(S.SECOND_PROMPT_TOKEN_IDS), + ] + if variant in ("multi_turn_single_sample", "agentic_tool_call_single_sample"): + full_response = S.FIRST_RESPONSE + S.FIRST_TOOL_RESPONSE + S.SECOND_RESPONSE + expected = [ + ExpectedSampleInfo( + chunks=[ + expected_chunk(S.FIRST_RESPONSE, 1), + expected_chunk(S.FIRST_TOOL_RESPONSE, 0), + expected_chunk(S.SECOND_RESPONSE, 1), + ], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=full_response, + response_length=token_len(full_response), + ), + ), + ] + else: + expected = [ + ExpectedSampleInfo( + chunks=[expected_chunk(S.FIRST_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.FIRST_RESPONSE, + response_length=token_len(S.FIRST_RESPONSE), + ), + ), + ExpectedSampleInfo( + chunks=[expected_chunk(S.SECOND_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.SECOND_RESPONSE, + response_length=token_len(S.SECOND_RESPONSE), + ), + ), + ] + verify_samples(result.sample, expected) + + +class TestExitConditions: + def test_partial_rollout_not_supported(self, variant, generation_env): + if is_agentic_variant(variant): + pytest.skip("agentic_tool_call does not check partial_rollout flag") + generation_env.args.partial_rollout = True + + with pytest.raises(AssertionError, match="Partial rollout is not supported"): + _run_generate(variant, generation_env, make_sample(prompt=SINGLE_TURN_PROMPT)) + + def test_abort_preserves_content(self, variant, generation_env): + if is_agentic_variant(variant): + pytest.skip("agentic_tool_call does not handle abort finish_reason") + generation_env.mock_server.process_fn = lambda _: ProcessResult( + text=SINGLE_TURN_RESPONSE, finish_reason="abort" + ) + + result = _run_generate(variant, generation_env, make_sample(prompt=SINGLE_TURN_PROMPT)) + + assert result.requests == [expected_request(SINGLE_TURN_PROMPT_TOKEN_IDS)] + verify_samples( + result.sample, + [ + ExpectedSampleInfo( + chunks=[ + SampleParsedChunk( + tokens_decoded_str=SINGLE_TURN_RESPONSE, + loss_mask_value=1, + rollout_log_probs=[-1 / 128 * i for i in range(6)], + ), + ], + partial_sample=expected_partial_sample( + prompt=SINGLE_TURN_PROMPT, + response=SINGLE_TURN_RESPONSE, + response_length=6, + status=Sample.Status.ABORTED, + ), + ), + ], + ) + + def test_finish_reason_length_exits_and_preserves_content(self, variant, generation_env): + S = TwoTurnStub + generation_env.mock_server.process_fn = lambda _: ProcessResult(text=S.FIRST_RESPONSE, finish_reason="length") + + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT)) + + if is_agentic_variant(variant): + assert result.requests == [expected_openai_request(S.OPENAI_MESSAGES_FIRST_TURN)] + else: + assert result.requests == [expected_request(S.FIRST_PROMPT_TOKEN_IDS)] + verify_samples( + result.sample, + [ + ExpectedSampleInfo( + chunks=[expected_chunk(S.FIRST_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.FIRST_RESPONSE, + response_length=token_len(S.FIRST_RESPONSE), + status=Sample.Status.TRUNCATED, + ), + ), + ], + ) + + @pytest.mark.parametrize("generation_env", [{"args_kwargs": {"generate_max_turns": 1}}], indirect=True) + def test_max_turns_reached(self, variant, generation_env): + S = TwoTurnStub + generation_env.mock_server.process_fn = lambda _: ProcessResult(text=S.FIRST_RESPONSE, finish_reason="stop") + + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT)) + + if is_agentic_variant(variant): + assert result.requests == [expected_openai_request(S.OPENAI_MESSAGES_FIRST_TURN)] + else: + assert result.requests == [expected_request(S.FIRST_PROMPT_TOKEN_IDS)] + if variant == "multi_turn_single_sample": + expected = [ + ExpectedSampleInfo( + chunks=[ + expected_chunk(S.FIRST_RESPONSE, 1), + expected_chunk(S.FIRST_TOOL_RESPONSE, 0), + ], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.FIRST_RESPONSE + S.FIRST_TOOL_RESPONSE, + response_length=token_len(S.FIRST_RESPONSE + S.FIRST_TOOL_RESPONSE), + ), + ), + ] + else: + expected = [ + ExpectedSampleInfo( + chunks=[expected_chunk(S.FIRST_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.FIRST_RESPONSE, + response_length=token_len(S.FIRST_RESPONSE), + ), + ), + ] + verify_samples(result.sample, expected) + + +class TestRespectMaxContextLen: + @pytest.mark.parametrize( + "generation_env", [{"args_kwargs": {"rollout_max_context_len": SINGLE_TURN_PROMPT_TOKEN_LEN}}], indirect=True + ) + def test_prompt_exceeds_max_context_len_returns_truncated(self, variant, generation_env): + if is_agentic_variant(variant): + pytest.skip("TODO: implement") + result = _run_generate(variant, generation_env, make_sample(prompt=SINGLE_TURN_PROMPT)) + assert result.requests == [] + if variant == "multi_turn_single_sample": + expected = [ + ExpectedSampleInfo( + chunks=[], + partial_sample=expected_partial_sample( + prompt=SINGLE_TURN_PROMPT, response="", response_length=0, status=Sample.Status.TRUNCATED + ), + ) + ] + else: + expected = [] + verify_samples(result.sample, expected) + + @pytest.mark.parametrize( + "generation_env", + [ + { + "args_kwargs": { + "rollout_max_context_len": len(TwoTurnStub.FIRST_PROMPT_TOKEN_IDS) + + token_len(TwoTurnStub.FIRST_RESPONSE) + + token_len(TwoTurnStub.FIRST_TOOL_RESPONSE) + } + } + ], + indirect=True, + ) + def test_second_turn_exceeds_max_context_len_returns_truncated(self, variant, generation_env): + if is_agentic_variant(variant): + pytest.skip("TODO: implement") + S = TwoTurnStub + generation_env.mock_server.process_fn = S.process_fn + + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT)) + + assert result.requests == [expected_request(S.FIRST_PROMPT_TOKEN_IDS)] + if variant == "multi_turn_single_sample": + partial_response = S.FIRST_RESPONSE + S.FIRST_TOOL_RESPONSE + expected = [ + ExpectedSampleInfo( + chunks=[ + expected_chunk(S.FIRST_RESPONSE, 1), + expected_chunk(S.FIRST_TOOL_RESPONSE, 0), + ], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=partial_response, + response_length=token_len(partial_response), + status=Sample.Status.TRUNCATED, + ), + ), + ] + else: + expected = [ + ExpectedSampleInfo( + chunks=[expected_chunk(S.FIRST_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.FIRST_RESPONSE, + response_length=token_len(S.FIRST_RESPONSE), + status=Sample.Status.TRUNCATED, + ), + ), + ] + verify_samples(result.sample, expected) + + @pytest.mark.parametrize( + "generation_env,expected_max_new_tokens", + [ + ( + {"args_kwargs": {"rollout_max_context_len": len(TwoTurnStub.SECOND_PROMPT_TOKEN_IDS) + 10}}, + 10, + ), + ( + {"args_kwargs": {"rollout_max_context_len": len(TwoTurnStub.SECOND_PROMPT_TOKEN_IDS) + 100}}, + 64, + ), + ], + indirect=["generation_env"], + ) + def test_second_turn_adjusts_max_new_tokens(self, variant, generation_env, expected_max_new_tokens): + if is_agentic_variant(variant): + pytest.skip("TODO: implement") + S = TwoTurnStub + generation_env.mock_server.process_fn = S.process_fn + + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT)) + + assert len(result.requests) >= 2 + assert result.requests[1]["sampling_params"]["max_new_tokens"] == expected_max_new_tokens + assert result.requests[1]["sampling_params"]["temperature"] == DEFAULT_SAMPLING_PARAMS["temperature"] + + +class TestThreeTurn: + """Need to test 3-turn case besides 2-turn, because e.g. merge_samples may behave differently.""" + + def test_three_turns_with_sequential_tool_calls(self, variant, generation_env): + generation_env.mock_server.process_fn = ThreeTurnStub.process_fn + + S = ThreeTurnStub + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT)) + + if is_agentic_variant(variant): + assert result.requests == [ + expected_openai_request(S.OPENAI_MESSAGES_FIRST_TURN), + expected_openai_request(S.OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT), + expected_openai_request(S.OPENAI_MESSAGES_THIRD_TURN_FROM_CLIENT), + ] + else: + assert result.requests == [ + expected_request(S.FIRST_PROMPT_TOKEN_IDS), + expected_request(S.SECOND_PROMPT_TOKEN_IDS), + expected_request(S.THIRD_PROMPT_TOKEN_IDS), + ] + if variant in ("multi_turn_single_sample", "agentic_tool_call_single_sample"): + full_response = ( + S.FIRST_RESPONSE + + S.FIRST_TOOL_RESPONSE + + S.SECOND_RESPONSE + + S.SECOND_TOOL_RESPONSE + + S.THIRD_RESPONSE + ) + expected = [ + ExpectedSampleInfo( + chunks=[ + expected_chunk(S.FIRST_RESPONSE, 1), + expected_chunk(S.FIRST_TOOL_RESPONSE, 0), + expected_chunk(S.SECOND_RESPONSE, 1), + expected_chunk(S.SECOND_TOOL_RESPONSE, 0), + expected_chunk(S.THIRD_RESPONSE, 1), + ], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=full_response, + response_length=token_len(full_response), + ), + ), + ] + else: + expected = [ + ExpectedSampleInfo( + chunks=[expected_chunk(S.FIRST_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.FIRST_RESPONSE, + response_length=token_len(S.FIRST_RESPONSE), + ), + ), + ExpectedSampleInfo( + chunks=[expected_chunk(S.SECOND_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.SECOND_RESPONSE, + response_length=token_len(S.SECOND_RESPONSE), + ), + ), + ExpectedSampleInfo( + chunks=[expected_chunk(S.THIRD_RESPONSE, 1)], + partial_sample=expected_partial_sample( + prompt=S.PROMPT, + response=S.THIRD_RESPONSE, + response_length=token_len(S.THIRD_RESPONSE), + ), + ), + ] + verify_samples(result.sample, expected) + + +class TestRoutedExpertsMultiTurn: + @pytest.mark.parametrize( + "generation_env", + [ + { + "args_kwargs": { + "use_rollout_routing_replay": True, + } + } + ], + indirect=True, + ) + def test_two_turns_routed_experts(self, variant, generation_env): + if is_agentic_variant(variant): + pytest.skip("TODO: implement") + + S = TwoTurnStub + num_layers, moe_router_topk = 2, 4 + generation_env.args.num_layers = num_layers + generation_env.args.moe_router_topk = moe_router_topk + + def make_routed_experts(prompt_token_ids, response_text): + total_tokens = len(prompt_token_ids) + token_len(response_text) + routed_experts_len = total_tokens - 1 + return np.arange(routed_experts_len * num_layers * moe_router_topk, dtype=np.int32).reshape( + routed_experts_len, num_layers, moe_router_topk + ) + + first_routed_experts = make_routed_experts(S.FIRST_PROMPT_TOKEN_IDS, S.FIRST_RESPONSE) + second_routed_experts = make_routed_experts(S.SECOND_PROMPT_TOKEN_IDS, S.SECOND_RESPONSE) + + def process_fn(prompt: str) -> ProcessResult: + if prompt == S.FIRST_PROMPT: + text, routed_experts = S.FIRST_RESPONSE, first_routed_experts + elif prompt == S.SECOND_PROMPT: + text, routed_experts = S.SECOND_RESPONSE, second_routed_experts + else: + raise ValueError(f"Unexpected prompt: {prompt}") + return ProcessResult( + text=text, + finish_reason="stop", + meta_info=ProcessResultMetaInfo( + routed_experts=pybase64.b64encode(routed_experts.tobytes()).decode("ascii") + ), + ) + + generation_env.mock_server.process_fn = process_fn + result = _run_generate(variant, generation_env, make_sample(prompt=S.PROMPT), DEFAULT_SAMPLING_PARAMS) + + sample = result.sample[-1] if isinstance(result.sample, list) else result.sample + assert sample.rollout_routed_experts is not None + assert sample.rollout_routed_experts.shape == second_routed_experts.shape + np.testing.assert_array_equal(sample.rollout_routed_experts, second_routed_experts) + assert len(sample.tokens) - 1 == second_routed_experts.shape[0] diff --git a/tests/fast/rollout/generate_hub/test_single_turn.py b/tests/fast/rollout/generate_hub/test_single_turn.py new file mode 100644 index 000000000..a58e6fb3c --- /dev/null +++ b/tests/fast/rollout/generate_hub/test_single_turn.py @@ -0,0 +1,424 @@ +import numpy as np +import pybase64 +import pytest +import torch +from PIL import Image +from tests.fast.fixtures.generation_fixtures import GenerateEnv, generation_env, listify, make_sample, run_generate +from transformers import AutoProcessor + +from miles.utils.processing_utils import encode_image_for_rollout_engine +from miles.utils.test_utils.mock_sglang_server import ProcessResult, ProcessResultMetaInfo +from miles.utils.types import Sample + +_ = generation_env + +# ------------------------------------ fixtures and consts ---------------------------------------- + + +MODEL_NAME = "Qwen/Qwen3-0.6B" +PROMPT = "What is 1+7?" +PROMPT_TOKENS = [3838, 374, 220, 16, 10, 22, 30] +PROMPT_TOKEN_LEN = len(PROMPT_TOKENS) +RESPONSE_TOKENS = [59, 79075, 90, 23, 92] +RESPONSE_TEXT = "\\boxed{8}" +RESPONSE_LOG_PROBS = [-0.0, -0.0078125, -0.015625, -0.0234375, -0.03125] +SAMPLING_PARAMS = {"max_new_tokens": 16, "temperature": 0.7} +DEFAULT_MAX_NEW_TOKENS = SAMPLING_PARAMS["max_new_tokens"] + + +@pytest.fixture(params=["old_sglang_rollout", "single_turn", "multi_turn_single_sample", "multi_turn_multi_samples"]) +def variant(request): + return request.param + + +def expected_request( + variant: str, + *, + input_ids: list[int] | None = None, + sampling_params: dict | None = None, + return_routed_experts: bool = False, + image_data: list[str] | None = None, +) -> dict: + result = { + "input_ids": input_ids or PROMPT_TOKENS, + "sampling_params": sampling_params or SAMPLING_PARAMS, + "return_logprob": True, + } + if variant in ("single_turn", "multi_turn_single_sample", "multi_turn_multi_samples") or return_routed_experts: + result["return_routed_experts"] = return_routed_experts + if image_data is not None: + result["image_data"] = image_data + return result + + +class _Unset: + pass + + +_UNSET = _Unset() + + +def expected_sample( + variant: str, + *, + prompt: str = PROMPT, + response: str = RESPONSE_TEXT, + response_length: int = 5, + tokens: list[int] | None | _Unset = _UNSET, + rollout_log_probs: list[float] | None | _Unset = _UNSET, + status: Sample.Status = Sample.Status.COMPLETED, + cached_tokens: int = 0, + prompt_tokens: int = 7, + weight_versions: list[str] | None = None, + rollout_routed_experts: np.ndarray | None = None, + spec_info: Sample.SpecInfo | None = None, + multimodal_inputs: dict | None = None, + multimodal_train_inputs: dict | None = None, + loss_mask: list[int] | None | _Unset = _UNSET, +) -> Sample: + actual_response_length = response_length if response_length is not None else len(RESPONSE_TOKENS) + if isinstance(loss_mask, _Unset): + loss_mask = ( + [1] * actual_response_length + if variant in ("multi_turn_single_sample", "multi_turn_multi_samples") + else None + ) + + return Sample( + group_index=None, + index=None, + prompt=prompt, + tokens=PROMPT_TOKENS + RESPONSE_TOKENS if isinstance(tokens, _Unset) else tokens, + multimodal_inputs=multimodal_inputs, + multimodal_train_inputs=multimodal_train_inputs, + response=response, + response_length=response_length, + label=None, + reward=None, + loss_mask=loss_mask, + weight_versions=weight_versions or [], + rollout_log_probs=RESPONSE_LOG_PROBS if isinstance(rollout_log_probs, _Unset) else rollout_log_probs, + rollout_routed_experts=rollout_routed_experts, + remove_sample=False, + status=status, + metadata={}, + train_metadata=None, + non_generation_time=0.0, + spec_info=spec_info or Sample.SpecInfo(), + prefix_cache_info=Sample.PrefixCacheInfo(cached_tokens=cached_tokens, total_prompt_tokens=prompt_tokens), + ) + + +def _make_sample(tokens=None, response="", response_length=0, status=Sample.Status.PENDING, multimodal_inputs=None): + return make_sample( + prompt=PROMPT, + tokens=tokens, + response=response, + response_length=response_length, + status=status, + multimodal_inputs=multimodal_inputs, + ) + + +def _run_generate(variant: str, env: GenerateEnv, sample: Sample | None = None, sampling_params: dict | None = None): + return run_generate(env, sample or _make_sample(), sampling_params or SAMPLING_PARAMS, variant=variant) + + +# ------------------------------------ tests ---------------------------------------- + + +class TestBasicGeneration: + def test_basic_generation(self, variant, generation_env): + result = _run_generate(variant, generation_env) + assert result.requests == [expected_request(variant)] + assert listify(result.sample) == [expected_sample(variant)] + + +class TestResumedSingleTurn: + def test_two_consecutive_calls_on_same_sample(self, variant, generation_env): + if variant in ("multi_turn_single_sample", "multi_turn_multi_samples"): + pytest.skip("not tested yet") + partial_text = "\\boxed" + partial_tokens = [59, 79075] + partial_log_probs = [-0.0, -0.0078125] + + remaining_text = "{8}" + remaining_tokens = [90, 23, 92] + remaining_log_probs = [-0.0, -0.0078125, -0.015625] + + generation_env.mock_server.process_fn = lambda _: ProcessResult(text=partial_text, finish_reason="abort") + sample = _make_sample() + result1 = _run_generate(variant, generation_env, sample) + assert result1.requests == [expected_request(variant)] + assert result1.sample == expected_sample( + variant, + response=partial_text, + response_length=2, + tokens=PROMPT_TOKENS + partial_tokens, + rollout_log_probs=partial_log_probs, + status=Sample.Status.ABORTED, + ) + + generation_env.mock_server.process_fn = lambda _: ProcessResult(text=remaining_text, finish_reason="stop") + result2 = _run_generate(variant, generation_env, result1.sample) + tokens_after_turn1 = PROMPT_TOKENS + partial_tokens + assert result2.requests == [ + expected_request( + variant, + input_ids=tokens_after_turn1, + sampling_params={"max_new_tokens": 14, "temperature": 0.7}, + ) + ] + assert result2.sample == expected_sample( + variant, + response=partial_text + remaining_text, + response_length=2 + 3, + tokens=tokens_after_turn1 + remaining_tokens, + rollout_log_probs=partial_log_probs + remaining_log_probs, + prompt_tokens=len(PROMPT_TOKENS) + len(tokens_after_turn1), + status=Sample.Status.COMPLETED, + ) + + +class TestFinishReason: + @pytest.mark.parametrize( + "generation_env,expected_status", + [ + ({"process_fn_kwargs": {"finish_reason": "stop"}}, Sample.Status.COMPLETED), + ({"process_fn_kwargs": {"finish_reason": "length"}}, Sample.Status.TRUNCATED), + ({"process_fn_kwargs": {"finish_reason": "abort"}}, Sample.Status.ABORTED), + ], + indirect=["generation_env"], + ) + def test_finish_reason_sets_status(self, variant, generation_env, expected_status): + result = _run_generate(variant, generation_env) + assert result.requests == [expected_request(variant)] + assert listify(result.sample) == [expected_sample(variant, status=expected_status)] + + +class TestRoutedExperts: + @pytest.mark.parametrize( + "generation_env", + [ + { + "args_kwargs": {"use_rollout_routing_replay": True}, + "process_fn_kwargs": {"routed_experts": "placeholder"}, + } + ], + indirect=True, + ) + def test_routed_experts_enabled_and_parsed(self, variant, generation_env): + num_layers, moe_router_topk = 2, 4 + num_tokens = len(PROMPT_TOKENS) + len(RESPONSE_TOKENS) + routed_experts_array = np.arange((num_tokens - 1) * num_layers * moe_router_topk, dtype=np.int32).reshape( + num_tokens - 1, num_layers, moe_router_topk + ) + + generation_env.args.num_layers = num_layers + generation_env.args.moe_router_topk = moe_router_topk + routed_experts_str = pybase64.b64encode(routed_experts_array.tobytes()).decode("ascii") + generation_env.mock_server.process_fn = lambda _: ProcessResult( + text=RESPONSE_TEXT, + finish_reason="stop", + meta_info=ProcessResultMetaInfo(routed_experts=routed_experts_str), + ) + + result = _run_generate(variant, generation_env) + assert result.requests == [expected_request(variant, return_routed_experts=True)] + sample = result.sample[0] if isinstance(result.sample, list) else result.sample + assert sample.rollout_routed_experts is not None + assert sample.rollout_routed_experts.shape == (num_tokens - 1, num_layers, moe_router_topk) + np.testing.assert_array_equal(sample.rollout_routed_experts, routed_experts_array) + + +class TestMetaInfo: + @pytest.mark.parametrize( + "generation_env", [{"process_fn_kwargs": {"cached_tokens": 3, "weight_version": "v1.0"}}], indirect=True + ) + def test_meta_info_fields_updated(self, variant, generation_env): + result = _run_generate(variant, generation_env) + assert result.requests == [expected_request(variant)] + assert listify(result.sample) == [expected_sample(variant, cached_tokens=3, weight_versions=["v1.0"])] + + @pytest.mark.parametrize( + "generation_env", + [ + { + "args_kwargs": {"sglang_speculative_algorithm": "EAGLE"}, + "process_fn_kwargs": {"spec_accept_token_num": 10, "spec_draft_token_num": 15, "spec_verify_ct": 3}, + } + ], + indirect=True, + ) + def test_spec_info_updated(self, variant, generation_env): + result = _run_generate(variant, generation_env) + assert result.requests == [expected_request(variant)] + assert listify(result.sample) == [ + expected_sample( + variant, + spec_info=Sample.SpecInfo( + spec_accept_token_num=10, spec_draft_token_num=15, spec_verify_ct=3, completion_token_num=5 + ), + ) + ] + + +class TestInputStatusValidation: + @pytest.mark.parametrize("status", [Sample.Status.PENDING, Sample.Status.ABORTED]) + def test_allowed_statuses(self, variant, generation_env, status): + result = _run_generate(variant, generation_env, _make_sample(status=status)) + assert result.requests == [expected_request(variant)] + assert listify(result.sample) == [expected_sample(variant)] + + @pytest.mark.parametrize("status", [Sample.Status.COMPLETED, Sample.Status.TRUNCATED]) + def test_rejected_statuses(self, variant, generation_env, status): + if variant in ("multi_turn_single_sample", "multi_turn_multi_samples"): + pytest.skip("not tested yet") + with pytest.raises(AssertionError): + _run_generate(variant, generation_env, _make_sample(status=status)) + + +class TestPayloadStructure: + def test_sampling_params_passed_through(self, variant, generation_env): + result = _run_generate( + variant, generation_env, sampling_params={"max_new_tokens": 16, "temperature": 0.5, "top_p": 0.9} + ) + assert result.requests == [ + expected_request(variant, sampling_params={"max_new_tokens": 16, "temperature": 0.5, "top_p": 0.9}) + ] + assert listify(result.sample) == [expected_sample(variant)] + + +class TestBoundaryConditions: + def test_max_new_tokens_zero_returns_truncated(self, variant, generation_env): + if variant in ("multi_turn_single_sample", "multi_turn_multi_samples"): + pytest.skip("not tested yet") + existing_tokens = [1, 2, 3, 4, 5, 6, 7] + list(range(100, 110)) + sample = _make_sample(tokens=existing_tokens, response="x" * 10, response_length=10) + + result = _run_generate(variant, generation_env, sample, {"max_new_tokens": 10, "temperature": 0.7}) + assert result.requests == [] + assert result.sample == expected_sample( + variant, + response="x" * 10, + response_length=10, + tokens=existing_tokens, + rollout_log_probs=None, + status=Sample.Status.TRUNCATED, + prompt_tokens=0, + ) + + @pytest.mark.parametrize("generation_env", [{"args_kwargs": {"rollout_max_context_len": 5}}], indirect=True) + def test_prompt_exceeds_max_context_len_returns_truncated(self, variant, generation_env): + if variant == "old_sglang_rollout": + pytest.skip("old_sglang_rollout does not support rollout_max_context_len") + if variant == "multi_turn_multi_samples": + pytest.skip("multi_turn_multi_samples returns empty list when first turn fails") + result = _run_generate(variant, generation_env) + assert result.requests == [] + tokens = PROMPT_TOKENS if variant in ("multi_turn_single_sample", "multi_turn_multi_samples") else [] + assert listify(result.sample) == [ + expected_sample( + variant, + response="", + response_length=0, + tokens=tokens, + rollout_log_probs=None, + status=Sample.Status.TRUNCATED, + prompt_tokens=0, + loss_mask=None if variant == "multi_turn_single_sample" else _UNSET, + ) + ] + + @pytest.mark.parametrize( + "generation_env,expected_max_new_tokens", + [ + ({"args_kwargs": {"rollout_max_context_len": 10}}, 10 - PROMPT_TOKEN_LEN), + ({"args_kwargs": {"rollout_max_context_len": 8}}, 8 - PROMPT_TOKEN_LEN), + ({"args_kwargs": {"rollout_max_context_len": 100}}, DEFAULT_MAX_NEW_TOKENS), + ], + indirect=["generation_env"], + ) + def test_moderate_length_input_adjusts_max_new_tokens(self, variant, generation_env, expected_max_new_tokens): + if variant == "old_sglang_rollout": + pytest.skip("old_sglang_rollout does not support rollout_max_context_len") + result = _run_generate(variant, generation_env) + assert len(result.requests) == 1 + assert result.requests[0]["sampling_params"]["max_new_tokens"] == expected_max_new_tokens + assert result.requests[0]["sampling_params"]["temperature"] == SAMPLING_PARAMS["temperature"] + assert listify(result.sample) == [expected_sample(variant)] + + @pytest.mark.parametrize( + "generation_env", + [{"args_kwargs": {"rollout_max_context_len": PROMPT_TOKEN_LEN}}], + indirect=True, + ) + def test_adjusted_max_new_tokens_zero_returns_truncated(self, variant, generation_env): + if variant == "old_sglang_rollout": + pytest.skip("old_sglang_rollout does not support rollout_max_context_len") + if variant == "multi_turn_multi_samples": + pytest.skip("multi_turn_multi_samples returns empty list when first turn fails") + result = _run_generate(variant, generation_env) + assert result.requests == [] + tokens = PROMPT_TOKENS if variant == "multi_turn_single_sample" else [] + assert listify(result.sample) == [ + expected_sample( + variant, + response="", + response_length=0, + tokens=tokens, + rollout_log_probs=None, + status=Sample.Status.TRUNCATED, + prompt_tokens=0, + loss_mask=None if variant == "multi_turn_single_sample" else _UNSET, + ) + ] + + +class TestEmptyResponse: + @pytest.mark.parametrize("generation_env", [{"process_fn_kwargs": {"response_text": ""}}], indirect=True) + def test_empty_response(self, variant, generation_env): + result = _run_generate(variant, generation_env) + assert result.requests == [expected_request(variant)] + assert listify(result.sample) == [ + expected_sample(variant, response="", response_length=0, tokens=PROMPT_TOKENS, rollout_log_probs=[]) + ] + + +VLM_MODEL_NAME = "Qwen/Qwen2-VL-2B-Instruct" + + +class TestMultimodal: + @pytest.mark.parametrize("generation_env", [{"args_kwargs": {"model_name": VLM_MODEL_NAME}}], indirect=True) + def test_multimodal_inputs_processed(self, variant, generation_env): + if variant in ("multi_turn_single_sample", "multi_turn_multi_samples"): + pytest.skip("not tested yet") + test_image = Image.new("RGB", (64, 64), color="red") + multimodal_inputs = {"images": [test_image]} + processor = AutoProcessor.from_pretrained(VLM_MODEL_NAME, trust_remote_code=True) + expected_mti = { + k: v + for k, v in processor(text=PROMPT, **multimodal_inputs).items() + if k not in ["input_ids", "attention_mask"] + } + + result = _run_generate(variant, generation_env, _make_sample(multimodal_inputs=multimodal_inputs)) + + assert result.requests == [ + expected_request( + variant, + input_ids=PROMPT_TOKENS, + image_data=[encode_image_for_rollout_engine(test_image)], + ) + ] + actual_mti = result.sample.multimodal_train_inputs + assert actual_mti is not None + assert set(actual_mti.keys()) == set(expected_mti.keys()) + assert torch.all(actual_mti["pixel_values"] == expected_mti["pixel_values"]) + assert torch.all(actual_mti["image_grid_thw"] == expected_mti["image_grid_thw"]) + assert result.sample == expected_sample( + variant, + tokens=PROMPT_TOKENS + RESPONSE_TOKENS, + multimodal_inputs=multimodal_inputs, + multimodal_train_inputs=actual_mti, + ) diff --git a/tests/fast/rollout/generate_hub/test_tool_call_utils.py b/tests/fast/rollout/generate_hub/test_tool_call_utils.py new file mode 100644 index 000000000..0f2305e75 --- /dev/null +++ b/tests/fast/rollout/generate_hub/test_tool_call_utils.py @@ -0,0 +1,99 @@ +import pytest + +from miles.rollout.generate_utils.tool_call_utils import _DUMMY_USER, _build_dummy_assistant, tokenize_tool_responses + +TOOL_CALL_TEST_MODELS = [ + "Qwen/Qwen2.5-0.5B-Instruct", + "Qwen/Qwen3-0.6B", + "Qwen/Qwen3-4B-Instruct-2507", + "Qwen/Qwen3-Coder-30B-A3B-Instruct", + # "meta-llama/Llama-3.2-1B-Instruct", # Skipped: gated repo, requires HF_TOKEN in CI + "mistralai/Mistral-7B-Instruct-v0.3", + "deepseek-ai/DeepSeek-V3", + "stepfun-ai/step3", + "MiniMaxAI/MiniMax-M2", + "internlm/internlm3-8b-instruct", + "THUDM/glm-4-9b-chat", + "moonshotai/Kimi-K2-Instruct", + "XiaomiMiMo/MiMo-7B-RL", +] + +SINGLE_TOOL_CALL_ONLY_MODELS = [ + # "meta-llama/Llama-3.2-1B-Instruct", # Skipped: gated repo +] + +# Models where tokenize->decode produces extra whitespace vs direct string diff +TOKENIZE_DECODE_WHITESPACE_DIFF_MODELS = [ + "THUDM/glm-4-9b-chat", +] + +SAMPLE_TOOL_RESPONSES = [ + { + "role": "tool", + "tool_call_id": "call00000", + "content": '{"year": 2026}', + "name": "get_year", + }, + { + "role": "tool", + "tool_call_id": "call00001", + "content": '{"temperature": 25}', + "name": "get_temperature", + }, +] + + +class TestTokenizeToolResponses: + @pytest.mark.parametrize("model_name", ["Qwen/Qwen3-0.6B"]) + def test_snapshot(self, model_name): + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) + token_ids = tokenize_tool_responses(SAMPLE_TOOL_RESPONSES, tokenizer) + decoded = tokenizer.decode(token_ids) + + assert decoded == ( + "<|im_start|>user\n" + "\n" + '{"year": 2026}\n' + "\n" + "\n" + '{"temperature": 25}\n' + "<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + @pytest.mark.parametrize("num_tools", [1, 2]) + @pytest.mark.parametrize("model_name", TOOL_CALL_TEST_MODELS) + def test_tokenize_tool_responses(self, model_name, num_tools): + if num_tools > 1 and model_name in SINGLE_TOOL_CALL_ONLY_MODELS: + pytest.skip(f"{model_name} only supports single tool call") + + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) + + tool_responses = SAMPLE_TOOL_RESPONSES[:num_tools] + assert len(tool_responses) == num_tools + + actual_token_ids = tokenize_tool_responses(tool_responses, tokenizer) + actual_str = tokenizer.decode(actual_token_ids) + + dummy_assistant = _build_dummy_assistant(tool_responses) + base_messages = [_DUMMY_USER, dummy_assistant] + expected_str = self._compute_chat_template_diff(base_messages, tool_responses, tokenizer) + + if model_name in TOKENIZE_DECODE_WHITESPACE_DIFF_MODELS: + # Some models produce whitespace differences between tokenize->decode and direct string diff + actual_str = actual_str.replace(" ", "") + expected_str = expected_str.replace(" ", "") + + assert actual_str == expected_str, f"{model_name=}" + + @staticmethod + def _compute_chat_template_diff(base_messages, extra_messages, tokenizer) -> str: + text_with = tokenizer.apply_chat_template( + base_messages + extra_messages, tokenize=False, add_generation_prompt=True + ) + text_without = tokenizer.apply_chat_template(base_messages, tokenize=False, add_generation_prompt=False) + return text_with[len(text_without) :] diff --git a/tests/fast/rollout/generate_utils/__init__.py b/tests/fast/rollout/generate_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/rollout/generate_utils/test_sample_utils.py b/tests/fast/rollout/generate_utils/test_sample_utils.py new file mode 100644 index 000000000..c53fbbb56 --- /dev/null +++ b/tests/fast/rollout/generate_utils/test_sample_utils.py @@ -0,0 +1,156 @@ +from unittest.mock import MagicMock + +import pytest + +from miles.rollout.generate_utils.sample_utils import _merge_sample_pair +from miles.utils.types import Sample + + +@pytest.fixture +def mock_tokenizer(): + tokenizer = MagicMock() + tokenizer.decode = lambda tokens: f"" + return tokenizer + + +def make_sample( + prompt="test_prompt", + tokens=None, + response="", + response_length=0, + loss_mask=None, + rollout_log_probs=None, + status=Sample.Status.COMPLETED, + label="test_label", + reward=1.0, + index=0, + group_index=0, +): + return Sample( + prompt=prompt, + tokens=tokens or [], + response=response, + response_length=response_length, + loss_mask=loss_mask, + rollout_log_probs=rollout_log_probs, + status=status, + label=label, + reward=reward, + index=index, + group_index=group_index, + ) + + +class TestMergeSamples: + def test_basic_merge(self, mock_tokenizer): + a = make_sample( + tokens=[1, 2, 3, 10, 11, 12], + response="response1", + response_length=3, + loss_mask=[1, 1, 1], + rollout_log_probs=[-0.1, -0.2, -0.3], + ) + b = make_sample( + tokens=[1, 2, 3, 10, 11, 12, 20, 21, 30, 31, 32], + response="response2", + response_length=3, + loss_mask=[1, 1, 1], + rollout_log_probs=[-0.4, -0.5, -0.6], + status=Sample.Status.TRUNCATED, + ) + + merged = _merge_sample_pair(a, b, mock_tokenizer) + + assert merged.tokens == b.tokens + assert merged.response_length == 3 + 2 + 3 + assert merged.loss_mask == [1, 1, 1, 0, 0, 1, 1, 1] + assert merged.rollout_log_probs == [-0.1, -0.2, -0.3, 0.0, 0.0, -0.4, -0.5, -0.6] + assert merged.prompt == a.prompt + assert merged.status == b.status + assert merged.label == a.label + assert merged.index == a.index + assert merged.group_index == a.group_index + assert "response1" in merged.response + assert "response2" in merged.response + assert "" in merged.response + + def test_loss_mask_none_defaults_to_all_ones(self, mock_tokenizer): + a = make_sample( + tokens=[1, 2, 10], + response_length=1, + loss_mask=None, + rollout_log_probs=None, + ) + b = make_sample( + tokens=[1, 2, 10, 20, 30], + response_length=1, + loss_mask=None, + rollout_log_probs=None, + ) + + merged = _merge_sample_pair(a, b, mock_tokenizer) + + assert merged.loss_mask == [1, 0, 1] + assert merged.rollout_log_probs == [0.0, 0.0, 0.0] + + def test_tokens_prefix_mismatch_raises(self, mock_tokenizer): + a = make_sample( + tokens=[1, 2, 3], + response_length=1, + loss_mask=[1], + ) + b = make_sample( + tokens=[1, 2, 99, 20, 30], + response_length=1, + loss_mask=[1], + ) + + with pytest.raises(AssertionError, match="b.tokens must start with a.tokens"): + _merge_sample_pair(a, b, mock_tokenizer) + + def test_field_mismatch_raises(self, mock_tokenizer): + a = make_sample( + tokens=[1, 2, 10], + response_length=1, + loss_mask=[1], + index=0, + ) + b = make_sample( + tokens=[1, 2, 10, 20, 30], + response_length=1, + loss_mask=[1], + index=1, + ) + + with pytest.raises(AssertionError, match="index mismatch"): + _merge_sample_pair(a, b, mock_tokenizer) + + def test_obs_len_invalid_raises(self, mock_tokenizer): + a = make_sample( + tokens=[1, 2, 10], + response_length=1, + loss_mask=[1], + ) + b = make_sample( + tokens=[1, 2, 10, 30], + response_length=1, + loss_mask=[1], + ) + + with pytest.raises(AssertionError, match="obs_len must be > 0"): + _merge_sample_pair(a, b, mock_tokenizer) + + def test_sample_validate_fails_raises(self, mock_tokenizer): + a = make_sample( + tokens=[1, 2, 10, 11], + response_length=2, + loss_mask=[1], + ) + b = make_sample( + tokens=[1, 2, 10, 11, 20, 30], + response_length=1, + loss_mask=[1], + ) + + with pytest.raises(AssertionError, match="loss_mask length"): + _merge_sample_pair(a, b, mock_tokenizer) diff --git a/tests/fast/rollout/inference_rollout/__init__.py b/tests/fast/rollout/inference_rollout/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/rollout/inference_rollout/conftest.py b/tests/fast/rollout/inference_rollout/conftest.py new file mode 100644 index 000000000..ca47edeeb --- /dev/null +++ b/tests/fast/rollout/inference_rollout/conftest.py @@ -0,0 +1,45 @@ +from unittest.mock import patch + +import pytest + +from miles.utils.arguments import parse_args + + +def _build_mock_args(extra_argv: list[str] | None = None): + argv = [ + "pytest", + "--train-backend", + "fsdp", + "--rollout-batch-size", + "2", + "--n-samples-per-prompt", + "1", + "--num-rollout", + "1", + "--rollout-num-gpus", + "4", + "--rollout-num-gpus-per-engine", + "2", + "--hf-checkpoint", + "Qwen/Qwen3-0.6B", + "--prompt-data", + "/dev/null", + "--input-key", + "input", + "--label-key", + "label", + "--rm-type", + "math", + "--use-miles-router", + "--sglang-router-ip", + "127.0.0.1", + "--sglang-router-port", + "30000", + ] + (extra_argv or []) + with patch("sys.argv", argv): + return parse_args() + + +@pytest.fixture +def mock_args(): + return _build_mock_args() diff --git a/tests/fast/rollout/inference_rollout/integration/__init__.py b/tests/fast/rollout/inference_rollout/integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/rollout/inference_rollout/integration/test_basic.py b/tests/fast/rollout/inference_rollout/integration/test_basic.py new file mode 100644 index 000000000..5b791829d --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_basic.py @@ -0,0 +1,69 @@ +import pytest +from tests.fast.fixtures.generation_fixtures import extra_argv_for_variant +from tests.fast.fixtures.rollout_fixtures import RolloutEnvConfig +from tests.fast.rollout.inference_rollout.integration.utils import ( + MODULAR_ROLLOUT_BASE_ARGV, + expected_sample, + load_and_call_train, +) + +from miles.rollout.base_types import RolloutFnConstructorInput, RolloutFnEvalInput +from miles.rollout.inference_rollout.compatibility import call_rollout_function, load_rollout_function + +_VARIANTS = [ + pytest.param( + RolloutEnvConfig( + extra_argv=[ + "--rollout-function-path", + "miles.rollout.sglang_rollout.generate_rollout", + "--eval-function-path", + "miles.rollout.sglang_rollout.generate_rollout", + "--custom-generate-function-path", + "miles.rollout.sglang_rollout.generate", + ] + ), + id="old_rollout_old_generate", + ), + pytest.param( + RolloutEnvConfig( + extra_argv=[ + "--rollout-function-path", + "miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn", + "--custom-generate-function-path", + "miles.rollout.sglang_rollout.generate", + ] + ), + id="new_rollout_old_generate", + ), + pytest.param( + RolloutEnvConfig(extra_argv=MODULAR_ROLLOUT_BASE_ARGV + extra_argv_for_variant("single_turn")), + id="new_rollout_new_generate", + ), +] + + +@pytest.mark.parametrize("rollout_env", _VARIANTS, indirect=True) +def test_train(rollout_env): + env = rollout_env + out = load_and_call_train(env.args, env.data_source) + + assert len(out.samples) == env.args.rollout_batch_size + group = out.samples[0] + assert len(group) == env.args.n_samples_per_prompt + assert group[0] == expected_sample(group_index=0) + + +@pytest.mark.parametrize("rollout_env", _VARIANTS, indirect=True) +def test_eval(rollout_env): + env = rollout_env + fn = load_rollout_function( + RolloutFnConstructorInput(args=env.args, data_source=env.data_source), env.args.eval_function_path + ) + out = call_rollout_function(fn, RolloutFnEvalInput(rollout_id=0)) + + assert "toy" in out.data + rewards = out.data["toy"]["rewards"] + samples = out.data["toy"]["samples"] + assert len(rewards) == len(samples) == env.args.n_samples_per_eval_prompt + assert rewards[0] == 1 + assert samples[0] == expected_sample(group_index=None) diff --git a/tests/fast/rollout/inference_rollout/integration/test_deterministic.py b/tests/fast/rollout/inference_rollout/integration/test_deterministic.py new file mode 100644 index 000000000..69a235911 --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_deterministic.py @@ -0,0 +1,37 @@ +import pytest + +from tests.fast.rollout.inference_rollout.integration.utils import integration_env_config, load_and_call_train + + +@pytest.mark.parametrize( + "rollout_env,expected_seeds", + [ + pytest.param( + integration_env_config( + [ + "--sglang-enable-deterministic-inference", + "--rollout-seed", + "42", + "--n-samples-per-prompt", + "3", + "--rollout-batch-size", + "1", + ] + ), + {42, 43, 44}, + id="enabled", + ), + pytest.param( + integration_env_config(["--n-samples-per-prompt", "2", "--rollout-batch-size", "1"]), + {None}, + id="disabled", + ), + ], + indirect=["rollout_env"], +) +def test_sampling_seeds(rollout_env, expected_seeds): + env = rollout_env + load_and_call_train(env.args, env.data_source) + + seeds = {req.get("sampling_params", {}).get("sampling_seed") for req in env.mock_server.request_log} + assert seeds == expected_seeds diff --git a/tests/fast/rollout/inference_rollout/integration/test_dynamic_filter.py b/tests/fast/rollout/inference_rollout/integration/test_dynamic_filter.py new file mode 100644 index 000000000..0ca5743ac --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_dynamic_filter.py @@ -0,0 +1,46 @@ +from contextlib import nullcontext + +import pytest +from tests.fast.rollout.inference_rollout.integration.utils import ( + MIXED_DATA_ROWS, + filter_by_reward, + integration_env_config, + load_and_call_train, +) + +from miles.utils.misc import function_registry + + +@pytest.mark.parametrize( + "rollout_env,use_filter,expect_all_correct", + [ + pytest.param( + integration_env_config(["--rollout-batch-size", "4"], data_rows=MIXED_DATA_ROWS), + False, + False, + id="no_filter", + ), + pytest.param( + integration_env_config( + ["--rollout-batch-size", "3", "--dynamic-sampling-filter-path", "test:filter_by_reward"], + data_rows=MIXED_DATA_ROWS, + ), + True, + True, + id="with_filter", + ), + ], + indirect=["rollout_env"], +) +def test_filter_effect(rollout_env, use_filter, expect_all_correct): + env = rollout_env + ctx = function_registry.temporary("test:filter_by_reward", filter_by_reward) if use_filter else nullcontext() + + with ctx: + out = load_and_call_train(env.args, env.data_source) + + rewards = {group[0].reward for group in out.samples} + if expect_all_correct: + assert rewards == {1}, "Filter should keep only correct samples" + else: + assert 0 in rewards, "Without filter, incorrect samples should be present" diff --git a/tests/fast/rollout/inference_rollout/integration/test_group_rm.py b/tests/fast/rollout/inference_rollout/integration/test_group_rm.py new file mode 100644 index 000000000..afd870c30 --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_group_rm.py @@ -0,0 +1,22 @@ +import pytest + +from tests.fast.rollout.inference_rollout.integration.utils import integration_env_config, load_and_call_train + + +@pytest.mark.parametrize( + "rollout_env", + [ + pytest.param( + integration_env_config(["--group-rm", "--n-samples-per-prompt", "2", "--rollout-batch-size", "1"]), + id="group_rm_enabled", + ), + ], + indirect=True, +) +def test_group_rm_rewards_set(rollout_env): + env = rollout_env + out = load_and_call_train(env.args, env.data_source) + + assert len(out.samples) == env.args.rollout_batch_size + rewards = [sample.reward for group in out.samples for sample in group] + assert all(r in (0, 1) for r in rewards) diff --git a/tests/fast/rollout/inference_rollout/integration/test_multi_sample.py b/tests/fast/rollout/inference_rollout/integration/test_multi_sample.py new file mode 100644 index 000000000..2b12d3d88 --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_multi_sample.py @@ -0,0 +1,65 @@ +import pytest +from tests.fast.fixtures.rollout_fixtures import DEFAULT_DATA_ROWS, RolloutEnvConfig +from tests.fast.rollout.inference_rollout.integration.utils import MODULAR_ROLLOUT_BASE_ARGV, load_and_call_train + +from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput +from miles.utils.misc import function_registry +from miles.utils.types import Sample + + +async def _multi_sample_generate(input: GenerateFnInput) -> GenerateFnOutput: + sample = input.sample + s1 = Sample( + prompt=sample.prompt, + response="\\boxed{8}", + response_length=5, + tokens=sample.tokens + [59, 79075, 90, 23, 92], + label=sample.label, + reward=None, + status=Sample.Status.COMPLETED, + ) + s2 = Sample( + prompt=sample.prompt, + response="\\boxed{8}", + response_length=5, + tokens=sample.tokens + [59, 79075, 90, 23, 92], + label=sample.label, + reward=0.5, + status=Sample.Status.COMPLETED, + ) + return GenerateFnOutput(samples=[s1, s2]) + + +@pytest.mark.parametrize( + "rollout_env", + [ + pytest.param( + RolloutEnvConfig( + extra_argv=MODULAR_ROLLOUT_BASE_ARGV + + [ + "--custom-generate-function-path", + "test:multi_sample_generate", + "--rollout-batch-size", + "1", + "--n-samples-per-prompt", + "1", + ], + data_rows=DEFAULT_DATA_ROWS, + ), + id="multi_sample_output", + ), + ], + indirect=True, +) +def test_multi_sample_output_preserves_existing_reward(rollout_env): + env = rollout_env + with function_registry.temporary("test:multi_sample_generate", _multi_sample_generate): + out = load_and_call_train(env.args, env.data_source) + + assert len(out.samples) == env.args.rollout_batch_size + group = out.samples[0] + assert isinstance(group[0], list) + samples = group[0] + assert len(samples) == 2 + assert samples[0].reward == 1 + assert samples[1].reward == 0.5 diff --git a/tests/fast/rollout/inference_rollout/integration/test_multi_turn.py b/tests/fast/rollout/inference_rollout/integration/test_multi_turn.py new file mode 100644 index 000000000..c41d71399 --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_multi_turn.py @@ -0,0 +1,114 @@ +from typing import Any + +import pytest +from tests.fast.fixtures.generation_fixtures import extra_argv_for_variant +from tests.fast.fixtures.rollout_fixtures import RolloutEnvConfig +from tests.fast.rollout.inference_rollout.integration.utils import MODULAR_ROLLOUT_BASE_ARGV, load_and_call_rollout + +from miles.utils.test_utils.mock_tools import TwoTurnStub +from miles.utils.types import Sample + + +TWO_TURN_DATA_ROWS = [{"input": [{"role": "user", "content": TwoTurnStub.USER_QUESTION}], "label": "2008"}] + +_VARIANT_NAMES = [ + "multi_turn_single_sample", + "multi_turn_multi_samples", + "agentic_tool_call_single_sample", + "agentic_tool_call_multi_samples", +] + +_BASE_EXTRA_ARGV = [ + "--rollout-batch-size", + "2", + "--n-samples-per-prompt", + "2", + "--n-samples-per-eval-prompt", + "2", + "--custom-rm-path", + "tests.fast.rollout.inference_rollout.integration.test_multi_turn._simple_reward_function", +] + + +def _config_for_variant(variant: str) -> RolloutEnvConfig: + return RolloutEnvConfig( + extra_argv=MODULAR_ROLLOUT_BASE_ARGV + extra_argv_for_variant(variant) + _BASE_EXTRA_ARGV, + data_rows=TWO_TURN_DATA_ROWS, + ) + + +@pytest.mark.parametrize( + "variant,rollout_env", + [pytest.param(variant, _config_for_variant(variant), id=variant) for variant in _VARIANT_NAMES], + indirect=["rollout_env"], +) +@pytest.mark.parametrize("test_type", ["train", "eval"]) +def test_rollout(rollout_env, variant, test_type): + env = rollout_env + env.mock_server.process_fn = TwoTurnStub.process_fn + + out = load_and_call_rollout(env.args, env.data_source, mode=test_type) + + if test_type == "train": + assert len(out.samples) == env.args.rollout_batch_size + group = out.samples[0] + _verify_samples(variant, group) + else: + assert "toy" in out.data + samples = out.data["toy"]["samples"] + _verify_samples(variant, samples) + + +def _verify_samples(variant: str, samples: list[Any]): + is_multi_samples = variant in ("multi_turn_multi_samples", "agentic_tool_call_multi_samples") + + if is_multi_samples: + if len(samples) > 0 and isinstance(samples[0], list): + # Train mode: list[list[Sample]], grouped by prompt + assert len(samples) == 2, f"n_samples_per_prompt=2, so group should have 2 samples, got {len(samples)}" + for group_sample in samples: + assert isinstance(group_sample, list), "multi_samples variant should return list[Sample] per generate" + _verify_group_samples(group_sample) + else: + # Eval mode: list[Sample], flattened + # n_samples_per_eval_prompt=2, and each generate returns 2 turns, so 2*2=4 samples + assert ( + len(samples) == 4 + ), f"n_samples_per_eval_prompt=2, each generate returns 2 turns, so should have 4 samples, got {len(samples)}" + # Group samples by prompt (every 2 samples form a group) + group_samples_list = [samples[i : i + 2] for i in range(0, len(samples), 2)] + for group_samples in group_samples_list: + _verify_group_samples(group_samples) + else: + assert len(samples) == 2, f"n_samples_per_prompt=2, so group should have 2 samples, got {len(samples)}" + for sample in samples: + assert isinstance(sample, Sample), "single_sample variant should return Sample, not list" + _verify_sample(sample) + + +def _verify_group_samples(group_samples: list[Sample], expected_count: int = 2): + assert len(group_samples) == expected_count, f"Group should have {expected_count} samples (one per turn)" + for i, sample in enumerate(group_samples): + _verify_sample(sample, expect_answer=(i == len(group_samples) - 1)) + + +def _verify_sample(sample: Sample, expected_reward: float = 1.0, expect_answer: bool = True): + assert sample.status == Sample.Status.COMPLETED + assert sample.reward == expected_reward, f"Sample should have reward={expected_reward}" + if expect_answer: + assert "2008" in sample.response, "Response should contain final answer '2008'" + + +async def _simple_reward_function(args, samples: Sample | list[Sample]) -> float | list[float]: + if isinstance(samples, list): + # For multi_samples variants, use the last sample's reward + if getattr(args, "generate_multi_samples", False): + return [_check_reward(samples[-1])] * len(samples) + else: + return [_check_reward(sample) for sample in samples] + else: + return _check_reward(samples) + + +def _check_reward(sample: Sample) -> float: + return float(sample.response and (str(sample.label) in sample.response)) diff --git a/tests/fast/rollout/inference_rollout/integration/test_over_sampling.py b/tests/fast/rollout/inference_rollout/integration/test_over_sampling.py new file mode 100644 index 000000000..0812962cc --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_over_sampling.py @@ -0,0 +1,48 @@ +import pytest +from tests.fast.rollout.inference_rollout.integration.utils import ( + filter_by_reward, + integration_env_config, + load_and_call_train, +) + +from miles.utils.misc import function_registry + +_DATA_ROWS = [ + {"input": "What is 1+7?", "label": "8"}, + {"input": "What is 1+8?", "label": "wrong"}, + {"input": "What is 1+9?", "label": "wrong"}, + {"input": "What is 1+6?", "label": "wrong"}, +] + +_BASE_ARGV = [ + "--over-sampling-batch-size", + "4", + "--dynamic-sampling-filter-path", + "test:filter_by_reward", +] + + +def _over_sampling_config(rollout_batch_size: int): + return integration_env_config(["--rollout-batch-size", str(rollout_batch_size)] + _BASE_ARGV, data_rows=_DATA_ROWS) + + +@pytest.mark.parametrize( + "rollout_env,expected_rounds", + [ + pytest.param(_over_sampling_config(1), 1, id="one_round"), + pytest.param(_over_sampling_config(2), 2, id="two_rounds"), + ], + indirect=["rollout_env"], +) +def test_over_sampling_rounds(rollout_env, expected_rounds): + env = rollout_env + + with function_registry.temporary("test:filter_by_reward", filter_by_reward): + out = load_and_call_train(env.args, env.data_source) + + assert len(out.samples) == env.args.rollout_batch_size + assert all(group[0].reward == 1 for group in out.samples) + + requests_count = len(env.mock_server.request_log) + expected_requests = expected_rounds * env.args.over_sampling_batch_size + assert requests_count == expected_requests, f"Expected {expected_rounds} round(s) = {expected_requests} requests" diff --git a/tests/fast/rollout/inference_rollout/integration/test_sample_filter.py b/tests/fast/rollout/inference_rollout/integration/test_sample_filter.py new file mode 100644 index 000000000..36e78c16c --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_sample_filter.py @@ -0,0 +1,67 @@ +from unittest.mock import Mock + +import pytest +from tests.fast.rollout.inference_rollout.integration.utils import ( + filter_by_reward, + integration_env_config, + load_and_call_train, +) + +from miles.utils.misc import function_registry + +# Data with only 2 reward=1 samples out of 4. +# This ensures all 4 samples must be generated to collect 2 valid ones. +_FILTER_TEST_DATA_ROWS = [ + {"input": "What is 1+7?", "label": "8"}, # reward=1 + {"input": "What is 1+8?", "label": "wrong"}, # reward=0 + {"input": "What is 1+9?", "label": "wrong"}, # reward=0 + {"input": "What is 1+6?", "label": "7"}, # reward=1 +] + + +@pytest.mark.parametrize( + "rollout_env", + [ + pytest.param( + integration_env_config( + [ + "--rollout-batch-size", + "2", + "--over-sampling-batch-size", + "4", + "--dynamic-sampling-filter-path", + "test:filter_by_reward", + "--rollout-sample-filter-path", + "test:sample_filter", + "--rollout-all-samples-process-path", + "test:all_samples_process", + ], + data_rows=_FILTER_TEST_DATA_ROWS, + ), + id="sample_filter_vs_all_samples", + ), + ], + indirect=True, +) +def test_sample_filter_and_all_samples_process(rollout_env): + env = rollout_env + sample_filter_mock = Mock() + all_samples_process_mock = Mock() + + with ( + function_registry.temporary("test:filter_by_reward", filter_by_reward), + function_registry.temporary("test:sample_filter", sample_filter_mock), + function_registry.temporary("test:all_samples_process", all_samples_process_mock), + ): + load_and_call_train(env.args, env.data_source) + + sample_filter_mock.assert_called_once() + _, filtered_data = sample_filter_mock.call_args[0] + rewards = [g[0][0].reward if isinstance(g[0], list) else g[0].reward for g in filtered_data] + assert all(r == 1 for r in rewards) + + all_samples_process_mock.assert_called_once() + _, all_samples, data_source = all_samples_process_mock.call_args[0] + assert data_source is not None + + assert len(all_samples) > len(filtered_data), "all_samples_process should see more samples than sample_filter" diff --git a/tests/fast/rollout/inference_rollout/integration/test_semaphore.py b/tests/fast/rollout/inference_rollout/integration/test_semaphore.py new file mode 100644 index 000000000..889a9ff8a --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/test_semaphore.py @@ -0,0 +1,33 @@ +import pytest + +from tests.fast.rollout.inference_rollout.integration.utils import integration_env_config, load_and_call_train + +_DATA_ROWS = [{"input": f"What is 1+{i}?", "label": str(1 + i)} for i in range(10)] +_BASE_ARGV = ["--rollout-batch-size", "4", "--n-samples-per-prompt", "2"] + + +@pytest.mark.parametrize( + "rollout_env,expected_range", + [ + pytest.param( + integration_env_config( + ["--sglang-server-concurrency", "1"] + _BASE_ARGV, data_rows=_DATA_ROWS, latency=0.05 + ), + (1, 1), + id="limit_1", + ), + pytest.param( + integration_env_config( + ["--sglang-server-concurrency", "999"] + _BASE_ARGV, data_rows=_DATA_ROWS, latency=0.05 + ), + (2, 999), + id="no_limit", + ), + ], + indirect=["rollout_env"], +) +def test_max_concurrent(rollout_env, expected_range): + env = rollout_env + load_and_call_train(env.args, env.data_source) + min_expected, max_expected = expected_range + assert min_expected <= env.mock_server.max_concurrent <= max_expected diff --git a/tests/fast/rollout/inference_rollout/integration/utils.py b/tests/fast/rollout/inference_rollout/integration/utils.py new file mode 100644 index 000000000..ad413cf94 --- /dev/null +++ b/tests/fast/rollout/inference_rollout/integration/utils.py @@ -0,0 +1,89 @@ +from tests.fast.fixtures.generation_fixtures import extra_argv_for_variant +from tests.fast.fixtures.rollout_fixtures import RolloutEnvConfig + +from miles.rollout.base_types import ( + RolloutFnConstructorInput, + RolloutFnEvalInput, + RolloutFnOutput, + RolloutFnTrainInput, +) +from miles.rollout.filter_hub.base_types import DynamicFilterOutput +from miles.rollout.inference_rollout.compatibility import call_rollout_function, load_rollout_function +from miles.utils.types import Sample + + +def expected_sample(*, group_index: int | None) -> Sample: + return Sample( + group_index=group_index, + index=0, + prompt="What is 1+7?", + tokens=[3838, 374, 220, 16, 10, 22, 30, 59, 79075, 90, 23, 92], + multimodal_inputs=None, + multimodal_train_inputs=None, + response="\\boxed{8}", + response_length=5, + label="8", + reward=1, + loss_mask=None, + weight_versions=[], + rollout_log_probs=[-0.0, -0.0078125, -0.015625, -0.0234375, -0.03125], + rollout_routed_experts=None, + remove_sample=False, + status=Sample.Status.COMPLETED, + metadata={}, + train_metadata=None, + non_generation_time=0.0, + spec_info=Sample.SpecInfo( + spec_accept_token_num=0, spec_draft_token_num=0, spec_verify_ct=0, completion_token_num=0 + ), + prefix_cache_info=Sample.PrefixCacheInfo(cached_tokens=0, total_prompt_tokens=7), + ) + + +MODULAR_ROLLOUT_BASE_ARGV = [ + "--rollout-function-path", + "miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn", +] + +MIXED_DATA_ROWS = [ + {"input": "What is 1+7?", "label": "8"}, + {"input": "What is 1+8?", "label": "9"}, + {"input": "What is 1+9?", "label": "wrong"}, + {"input": "What is 1+6?", "label": "7"}, +] + + +def integration_env_config( + extra_argv: list[str], + data_rows: list[dict] | None = None, + latency: float = 0.0, + variant: str = "single_turn", +): + return RolloutEnvConfig( + extra_argv=MODULAR_ROLLOUT_BASE_ARGV + extra_argv_for_variant(variant) + extra_argv, + data_rows=data_rows, + latency=latency, + ) + + +def load_and_call_rollout(args, data_source, mode: str = "train") -> RolloutFnOutput: + function_path = args.rollout_function_path if mode == "train" else args.eval_function_path + fn = load_rollout_function( + RolloutFnConstructorInput(args=args, data_source=data_source), + function_path, + ) + if mode == "train": + return call_rollout_function(fn, RolloutFnTrainInput(rollout_id=0)) + else: + return call_rollout_function(fn, RolloutFnEvalInput(rollout_id=0)) + + +def load_and_call_train(args, data_source): + return load_and_call_rollout(args, data_source, mode="train") + + +def filter_by_reward(args, samples, **kwargs): + reward = samples[0].reward if not isinstance(samples[0], list) else samples[0][0].reward + if reward == 1: + return DynamicFilterOutput(keep=True) + return DynamicFilterOutput(keep=False, reason="reward_zero") diff --git a/tests/fast/rollout/inference_rollout/test_compatibility.py b/tests/fast/rollout/inference_rollout/test_compatibility.py new file mode 100644 index 000000000..ddfecd067 --- /dev/null +++ b/tests/fast/rollout/inference_rollout/test_compatibility.py @@ -0,0 +1,196 @@ +import asyncio +from unittest.mock import MagicMock + +import pytest + +from miles.rollout.base_types import ( + GenerateFnInput, + GenerateFnOutput, + RolloutFnConstructorInput, + RolloutFnEvalInput, + RolloutFnEvalOutput, + RolloutFnTrainInput, + RolloutFnTrainOutput, +) +from miles.rollout.inference_rollout.compatibility import ( + LegacyGenerateFnAdapter, + LegacyRolloutFnAdapter, + call_rollout_function, + load_generate_function, + load_rollout_function, +) +from miles.utils.async_utils import run +from miles.utils.misc import function_registry + + +@pytest.fixture +def constructor_input(): + return RolloutFnConstructorInput(args="dummy_args", data_source="dummy_data_source") + + +@pytest.fixture +def make_generate_fn_input(): + def _make(evaluation: bool = False): + state = MagicMock() + state.args = MagicMock() + + return GenerateFnInput( + state=state, + sample={"text": "test prompt"}, + sampling_params={"temperature": 0.7}, + evaluation=evaluation, + ) + + return _make + + +class TestSupportedRolloutFormats: + """ + Documentation test to show various supported rollout function formats + """ + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_1_legacy_function_raw_output(self, constructor_input, evaluation): + def legacy_rollout_fn(args, rollout_id, data_source, evaluation=False): + if evaluation: + return {"metric": {"accuracy": 0.9}} + return [[{"text": "sample"}]] + + with function_registry.temporary("test:legacy_rollout", legacy_rollout_fn): + fn = load_rollout_function(constructor_input, "test:legacy_rollout") + + input_cls = RolloutFnEvalInput if evaluation else RolloutFnTrainInput + result = call_rollout_function(fn, input_cls(rollout_id=1)) + + assert isinstance(fn, LegacyRolloutFnAdapter) + if evaluation: + assert isinstance(result, RolloutFnEvalOutput) + assert result.data == {"metric": {"accuracy": 0.9}} + else: + assert isinstance(result, RolloutFnTrainOutput) + assert result.samples == [[{"text": "sample"}]] + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_2_legacy_function_typed_output(self, constructor_input, evaluation): + def legacy_rollout_fn(args, rollout_id, data_source, evaluation=False): + if evaluation: + return RolloutFnEvalOutput(data={"ds": {"acc": 0.95}}) + return RolloutFnTrainOutput(samples=[[{"text": "typed"}]]) + + with function_registry.temporary("test:legacy_typed", legacy_rollout_fn): + fn = load_rollout_function(constructor_input, "test:legacy_typed") + + input_cls = RolloutFnEvalInput if evaluation else RolloutFnTrainInput + result = call_rollout_function(fn, input_cls(rollout_id=1)) + + if evaluation: + assert isinstance(result, RolloutFnEvalOutput) + assert result.data == {"ds": {"acc": 0.95}} + else: + assert isinstance(result, RolloutFnTrainOutput) + assert result.samples == [[{"text": "typed"}]] + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_3_sync_class(self, constructor_input, evaluation): + class SyncRolloutFn: + def __init__(self, input: RolloutFnConstructorInput): + pass + + def __call__(self, input): + if input.evaluation: + return RolloutFnEvalOutput(data={"test": {"score": 1}}) + return RolloutFnTrainOutput(samples=[[{"text": "sync"}]]) + + with function_registry.temporary("test:sync_class", SyncRolloutFn): + fn = load_rollout_function(constructor_input, "test:sync_class") + + input_cls = RolloutFnEvalInput if evaluation else RolloutFnTrainInput + result = call_rollout_function(fn, input_cls(rollout_id=1)) + + assert isinstance(fn, SyncRolloutFn) + expected_type = RolloutFnEvalOutput if evaluation else RolloutFnTrainOutput + assert isinstance(result, expected_type) + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_4_async_class(self, constructor_input, evaluation): + class AsyncRolloutFn: + def __init__(self, input: RolloutFnConstructorInput): + pass + + async def __call__(self, input): + await asyncio.sleep(0.001) + if input.evaluation: + return RolloutFnEvalOutput(data={"benchmark": {"accuracy": 0.98}}) + return RolloutFnTrainOutput(samples=[[{"text": "async"}]]) + + with function_registry.temporary("test:async_class", AsyncRolloutFn): + fn = load_rollout_function(constructor_input, "test:async_class") + + input_cls = RolloutFnEvalInput if evaluation else RolloutFnTrainInput + result = call_rollout_function(fn, input_cls(rollout_id=1)) + + assert isinstance(fn, AsyncRolloutFn) + expected_type = RolloutFnEvalOutput if evaluation else RolloutFnTrainOutput + assert isinstance(result, expected_type) + + +class TestSupportedGenerateFormats: + """ + Documentation test similar to TestSupportedRolloutFormats + """ + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_1_legacy_function_with_evaluation_param(self, make_generate_fn_input, evaluation): + async def legacy_generate_fn(args, sample, sampling_params, evaluation=False): + return "my_sample" + + with function_registry.temporary("test:legacy_gen_eval", legacy_generate_fn): + fn = load_generate_function("test:legacy_gen_eval") + + result = run(fn(make_generate_fn_input(evaluation))) + + assert isinstance(fn, LegacyGenerateFnAdapter) + assert isinstance(result, GenerateFnOutput) + assert result.samples == "my_sample" + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_2_legacy_function_without_evaluation_param(self, make_generate_fn_input, evaluation): + async def legacy_generate_fn(args, sample, sampling_params): + return "my_sample" + + with function_registry.temporary("test:legacy_gen", legacy_generate_fn): + fn = load_generate_function("test:legacy_gen") + + result = run(fn(make_generate_fn_input(evaluation))) + + assert isinstance(fn, LegacyGenerateFnAdapter) + assert isinstance(result, GenerateFnOutput) + assert result.samples == "my_sample" + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_3_new_async_function_api(self, make_generate_fn_input, evaluation): + async def generate(input: GenerateFnInput) -> GenerateFnOutput: + return GenerateFnOutput(samples="my_sample") + + with function_registry.temporary("test:new_async", generate): + fn = load_generate_function("test:new_async") + + result = run(fn(make_generate_fn_input(evaluation))) + + assert isinstance(result, GenerateFnOutput) + assert result.samples == "my_sample" + + @pytest.mark.parametrize("evaluation", [False, True]) + def test_format_4_new_class_api(self, make_generate_fn_input, evaluation): + class MyGenerateFn: + async def __call__(self, input: GenerateFnInput) -> GenerateFnOutput: + return GenerateFnOutput(samples="my_sample") + + with function_registry.temporary("test:new_class", MyGenerateFn): + fn = load_generate_function("test:new_class") + + result = run(fn(make_generate_fn_input(evaluation))) + + assert isinstance(fn, MyGenerateFn) + assert isinstance(result, GenerateFnOutput) + assert result.samples == "my_sample" diff --git a/tests/fast/rollout/rm_hub/__init__.py b/tests/fast/rollout/rm_hub/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/rollout/rm_hub/test_deepscaler.py b/tests/fast/rollout/rm_hub/test_deepscaler.py new file mode 100644 index 000000000..bd4c606a6 --- /dev/null +++ b/tests/fast/rollout/rm_hub/test_deepscaler.py @@ -0,0 +1,26 @@ +import pytest + +from miles.rollout.rm_hub.deepscaler import get_deepscaler_rule_based_reward + + +class TestGetDeepscalerRuleBasedReward: + @pytest.mark.parametrize( + "response,label,expected", + [ + (r"Let me analyze...The answer is \boxed{42}", "42", 1), + (r"Thinking...The answer is \boxed{wrong}", "42", 0), + (r"###Response\boxed{42}", "42", 1), + (r"###Response\boxed{wrong}", "42", 0), + (r"The answer is \boxed{42}", "42", 0), + (r"The answer is 42", "42", 0), + (r"\boxed{42}", "", 0), + (r"\boxed{42}", r"\boxed{42}", 1), + (r"\boxed{123}", 123, 1), + (r"\boxed{3.14}", 3.14, 1), + (r"\boxed{1/2}", "0.5", 1), + (r"\boxed{\frac{1}{2}}", "0.5", 1), + (r"First thoughtSecond thought\boxed{42}", "42", 1), + ], + ) + def test_get_deepscaler_rule_based_reward(self, response, label, expected): + assert get_deepscaler_rule_based_reward(response, label) == expected diff --git a/tests/fast/rollout/rm_hub/test_f1.py b/tests/fast/rollout/rm_hub/test_f1.py new file mode 100644 index 000000000..c9ecf9614 --- /dev/null +++ b/tests/fast/rollout/rm_hub/test_f1.py @@ -0,0 +1,44 @@ +import pytest + +from miles.rollout.rm_hub.f1 import f1_score, normalize_answer + + +class TestNormalizeAnswer: + @pytest.mark.parametrize( + "input_str,expected", + [ + ("Hello World", "hello world"), + ("The quick brown fox", "quick brown fox"), + ("A cat and a dog", "cat and dog"), + ("Hello, world!", "hello world"), + (" multiple spaces ", "multiple spaces"), + ("An apple", "apple"), + ("UPPERCASE", "uppercase"), + ], + ) + def test_normalize_answer(self, input_str, expected): + assert normalize_answer(input_str) == expected + + +class TestF1Score: + @pytest.mark.parametrize( + "prediction,ground_truth,expected_f1,expected_prec,expected_recall", + [ + ("hello world", "hello world", 1.0, 1.0, 1.0), + ("hello world foo", "hello world bar", 2 / 3, 2 / 3, 2 / 3), + ("abc", "xyz", 0, 0, 0), + (None, "anything", 0, 0, 0), + ("yes", "no", 0, 0, 0), + ("no", "yes", 0, 0, 0), + ("yes", "yes", 1.0, 1.0, 1.0), + ("noanswer", "yes", 0, 0, 0), + ("the answer is correct", "answer is correct", 1.0, 1.0, 1.0), + ("hello, world!", "hello world", 1.0, 1.0, 1.0), + ("hello", "hello world", pytest.approx(2 / 3), 1.0, 0.5), + ], + ) + def test_f1_score(self, prediction, ground_truth, expected_f1, expected_prec, expected_recall): + f1, prec, recall = f1_score(prediction, ground_truth) + assert f1 == expected_f1 + assert prec == expected_prec + assert recall == expected_recall diff --git a/tests/fast/rollout/rm_hub/test_gpqa.py b/tests/fast/rollout/rm_hub/test_gpqa.py new file mode 100644 index 000000000..45cefd201 --- /dev/null +++ b/tests/fast/rollout/rm_hub/test_gpqa.py @@ -0,0 +1,86 @@ +import pytest + +from miles.rollout.rm_hub.gpqa import ( + _extract_letter_from_response, + _normalize_text, + _strip_chain_of_thought, + compute_gpqa_reward, +) + + +class TestStripChainOfThought: + @pytest.mark.parametrize( + "text,expected", + [ + ("Let me think...The answer is A", "The answer is A"), + ("The answer is A", "The answer is A"), + ("", ""), + (None, ""), + ], + ) + def test_strip_chain_of_thought(self, text, expected): + assert _strip_chain_of_thought(text) == expected + + +class TestNormalizeText: + @pytest.mark.parametrize( + "input_str,expected", + [ + ("Hello World", "hello world"), + ("Test-123", "test 123"), + ("A, B, C", "a b c"), + ("", ""), + ], + ) + def test_normalize_text(self, input_str, expected): + assert _normalize_text(input_str) == expected + + +class TestExtractLetterFromResponse: + @pytest.mark.parametrize( + "response,expected", + [ + ("The answer is A", "A"), + ("answer: B", "B"), + ("I think C is correct", "C"), + ("final answer: D", "D"), + ("Option A is the best choice", "A"), + ("The answer is B", "B"), + ("After analysis, my choice is C", "C"), + ("A B C D", "D"), + ("No valid letter here", None), + ("", None), + (None, None), + ("The answer is Z", None), + ], + ) + def test_extract_letter(self, response, expected): + assert _extract_letter_from_response(response, "ABCD") == expected + + +class TestComputeGpqaReward: + @pytest.mark.parametrize( + "response,label,metadata,expected", + [ + ("Answer: A", "A", None, 1.0), + ("Answer: A", "B", None, 0.0), + (None, "A", None, 0.0), + ("Answer: B", "ignored", {"correct_letter": "B"}, 1.0), + ("Answer: A", "ignored", {"correct_letter": "B"}, 0.0), + ("Answer: A", 0, {"choices": ["Option 1", "Option 2", "Option 3", "Option 4"]}, 1.0), + ("Answer: B", 1, {"choices": ["Option 1", "Option 2", "Option 3", "Option 4"]}, 1.0), + ("Answer: X", "X", {"valid_letters": ["X", "Y", "Z"]}, 1.0), + ("Answer: A", "X", {"valid_letters": ["X", "Y", "Z"]}, 0.0), + ( + "I believe the answer is Paris", + "", + {"choices": ["Paris", "London", "Berlin", "Rome"], "correct_letter": "A"}, + 1.0, + ), + ("Answer: A", "", {"choices": {"A": "Paris", "B": "London"}, "correct_letter": "A"}, 1.0), + ("The answer is Paris", "Paris", {"choices": ["Paris", "London", "Berlin", "Rome"]}, 1.0), + ("Let me think step by step...The answer is A", "A", None, 1.0), + ], + ) + def test_compute_gpqa_reward(self, response, label, metadata, expected): + assert compute_gpqa_reward(response, label, metadata=metadata) == expected diff --git a/tests/fast/rollout/rm_hub/test_math_dapo_utils.py b/tests/fast/rollout/rm_hub/test_math_dapo_utils.py new file mode 100644 index 000000000..56a7f6d1f --- /dev/null +++ b/tests/fast/rollout/rm_hub/test_math_dapo_utils.py @@ -0,0 +1,108 @@ +import pytest + +from miles.rollout.rm_hub.math_dapo_utils import ( + compute_score, + is_correct_minerva, + is_correct_strict_box, + last_boxed_only_string, + normalize_final_answer, + remove_boxed, +) + + +class TestLastBoxedOnlyString: + @pytest.mark.parametrize( + "input_str,expected", + [ + (r"The answer is \boxed{42}", r"\boxed{42}"), + (r"\boxed{x^2}", r"\boxed{x^2}"), + (r"No boxed", None), + (r"Multiple \boxed{1} and \boxed{2}", r"\boxed{2}"), + ], + ) + def test_last_boxed_only_string(self, input_str, expected): + assert last_boxed_only_string(input_str) == expected + + +class TestRemoveBoxed: + @pytest.mark.parametrize( + "input_str,expected", + [ + (r"\boxed{42}", "42"), + (r"\boxed{x + 1}", "x + 1"), + ], + ) + def test_remove_boxed_valid(self, input_str, expected): + assert remove_boxed(input_str) == expected + + def test_remove_boxed_invalid(self): + with pytest.raises(AssertionError): + remove_boxed("not boxed") + + +class TestNormalizeFinalAnswer: + @pytest.mark.parametrize( + "input_str,expected", + [ + ("42", "42"), + (" 42 ", "42"), + (r"\text{hello}", "hello"), + (r"\textbf{bold}", "bold"), + (r"x = 42", "42"), + (r"100 square", "100"), + (r"$50$ dollars", "50"), + (r"\boxed{42}", "42"), + (r"\frac12", r"\frac{1}{2}"), + (r"\sqrt3", r"\sqrt{3}"), + ("1,000", "1000"), + ("<|im_end|>", ""), + ], + ) + def test_normalize_final_answer(self, input_str, expected): + assert normalize_final_answer(input_str) == expected + + +class TestIsCorrectMinerva: + @pytest.mark.parametrize( + "solution,gt,gt_need_extract,expected_correct", + [ + ("Answer: 42", "42", False, True), + ("Answer: 100", "42", False, False), + ("Answer: wrong", "42", False, False), + ("Answer: 42", r"\boxed{42}", True, True), + ], + ) + def test_is_correct_minerva(self, solution, gt, gt_need_extract, expected_correct): + correct, pred = is_correct_minerva(solution, gt, gt_need_extract=gt_need_extract) + assert correct == expected_correct + + +class TestIsCorrectStrictBox: + @pytest.mark.parametrize( + "pred,gt,expected_score,expected_pred", + [ + (r"blah blah \boxed{42}", "42", 1, "42"), + (r"\boxed{wrong}", "42", -1, "wrong"), + ("no box here", "42", -1, None), + ], + ) + def test_is_correct_strict_box(self, pred, gt, expected_score, expected_pred): + score, extracted = is_correct_strict_box(pred, gt) + assert score == expected_score + assert extracted == expected_pred + + +class TestComputeScore: + @pytest.mark.parametrize( + "solution,gt,strict_box,expected_score,expected_acc", + [ + ("Answer: 42", "42", False, 1.0, True), + ("Answer: wrong", "42", False, -1.0, False), + (r"\boxed{42}", "42", True, 1.0, True), + ("x" * 500 + " Answer: 42", "42", False, 1.0, True), + ], + ) + def test_compute_score(self, solution, gt, strict_box, expected_score, expected_acc): + result = compute_score(solution, gt, strict_box_verify=strict_box) + assert result["score"] == expected_score + assert result["acc"] == expected_acc diff --git a/tests/fast/rollout/rm_hub/test_math_utils.py b/tests/fast/rollout/rm_hub/test_math_utils.py new file mode 100644 index 000000000..2423ed4ac --- /dev/null +++ b/tests/fast/rollout/rm_hub/test_math_utils.py @@ -0,0 +1,129 @@ +import pytest + +from miles.rollout.rm_hub.math_utils import ( + _normalize, + extract_answer, + grade_answer_mathd, + grade_answer_sympy, + grade_answer_verl, + last_boxed_only_string, + remove_boxed, +) + + +class TestLastBoxedOnlyString: + @pytest.mark.parametrize( + "input_str,expected", + [ + (r"The answer is \boxed{42}", r"\boxed{42}"), + (r"\boxed{x^2 + 1}", r"\boxed{x^2 + 1}"), + (r"So \boxed{\frac{1}{2}}", r"\boxed{\frac{1}{2}}"), + (r"No boxed here", None), + (r"Multiple \boxed{1} and \boxed{2}", r"\boxed{2}"), + (r"\boxed{nested {braces}}", r"\boxed{nested {braces}}"), + (r"\fbox{fbox content}", r"\fbox{fbox content}"), + ("", None), + ], + ) + def test_last_boxed_only_string(self, input_str, expected): + assert last_boxed_only_string(input_str) == expected + + +class TestRemoveBoxed: + @pytest.mark.parametrize( + "input_str,expected", + [ + (r"\boxed{42}", "42"), + (r"\boxed{x^2 + 1}", "x^2 + 1"), + (r"\boxed{\frac{1}{2}}", r"\frac{1}{2}"), + ("not boxed", None), + ], + ) + def test_remove_boxed(self, input_str, expected): + assert remove_boxed(input_str) == expected + + +class TestExtractAnswer: + @pytest.mark.parametrize( + "input_str,expected", + [ + (r"The answer is \boxed{42}", "42"), + (r"So \boxed{\frac{1}{2}}", r"\frac{1}{2}"), + (r"Multiple \boxed{1} then \boxed{final}", "final"), + (r"No boxed here", None), + ("", None), + ], + ) + def test_extract_answer(self, input_str, expected): + assert extract_answer(input_str) == expected + + +class TestNormalize: + @pytest.mark.parametrize( + "input_str,expected", + [ + ("1,000", "1000"), + (r"\text{hello}", "hello"), + (" 42 ", "42"), + (r"100%", "100"), + (r"\$50", "50"), + ("HELLO", "hello"), + ("1,234,567", "1234567"), + (None, None), + ], + ) + def test_normalize(self, input_str, expected): + assert _normalize(input_str) == expected + + +class TestGradeAnswerMathd: + @pytest.mark.parametrize( + "given,ground_truth,expected", + [ + ("42", "42", True), + (" 42 ", "42", True), + (r"\frac{1}{2}", r"\frac{1}{2}", True), + ("wrong", "42", False), + ("", "42", False), + ], + ) + def test_grade_answer_mathd(self, given, ground_truth, expected): + assert grade_answer_mathd(given, ground_truth) == expected + + +class TestGradeAnswerSympy: + @pytest.mark.parametrize( + "given,ground_truth,expected", + [ + ("42", "42", True), + ("x^2", "x^2", True), + ("1/2", "0.5", True), + (r"\frac{1}{2}", "0.5", True), + ("wrong", "42", False), + ("", "42", False), + ("(1,2)", "(1,2)", True), + ("(1,2,3)", "(1,2)", False), + ("42", None, False), + ], + ) + def test_grade_answer_sympy(self, given, ground_truth, expected): + assert grade_answer_sympy(given, ground_truth) == expected + + +class TestGradeAnswerVerl: + @pytest.mark.parametrize( + "solution,ground_truth,expected", + [ + (r"\boxed{42}", "42", True), + (r"The answer is \boxed{42}", "42", True), + (r"\boxed{1/2}", r"\frac{1}{2}", True), + (r"\boxed{wrong}", "42", False), + ("no boxed", "42", False), + (r"\boxed{42}", r"\boxed{42}", True), + ("", "42", False), + (r"\boxed{42}", "", False), + (r"\boxed{42}", None, False), + ], + ) + def test_grade_answer_verl(self, solution, ground_truth, expected): + assert grade_answer_verl(solution, ground_truth) == expected diff --git a/tests/fast/rollout/rm_hub/test_rm_hub.py b/tests/fast/rollout/rm_hub/test_rm_hub.py new file mode 100644 index 000000000..a3dadbdaf --- /dev/null +++ b/tests/fast/rollout/rm_hub/test_rm_hub.py @@ -0,0 +1,126 @@ +from unittest.mock import MagicMock + +import pytest + +from miles.rollout.rm_hub import async_rm, batched_async_rm +from miles.utils.async_utils import run +from miles.utils.types import Sample + + +@pytest.fixture +def mock_args(): + args = MagicMock() + args.custom_rm_path = None + args.rm_type = None + args.rm_url = None + return args + + +class TestAsyncRm: + @pytest.mark.parametrize( + "rm_type,response,label,expected", + [ + ("math", r"\boxed{42}", "42", 1), + ("math", r"\boxed{wrong}", "42", 0), + ("f1", "hello world", "hello world", 1.0), + ("dapo", "Answer: 42", "42", {"score": 1.0}), + ("deepscaler", r"\boxed{42}", "42", 1), + ("gpqa", "Answer: A", "A", 1.0), + ("boxed_f1", r"Final answer is \boxed{hello world}", "hello world", 1.0), + ], + ) + def test_rm_types(self, mock_args, rm_type, response, label, expected): + mock_args.rm_type = rm_type + sample = Sample(prompt="", response=response, label=label) + reward = run(async_rm(mock_args, sample)) + if isinstance(expected, dict): + for k, v in expected.items(): + assert reward[k] == v + else: + assert reward == expected + + def test_f1_rm_partial(self, mock_args): + mock_args.rm_type = "f1" + sample = Sample(prompt="", response="hello", label="hello world") + reward = run(async_rm(mock_args, sample)) + assert 0 < reward < 1 + + def test_random_rm(self, mock_args): + mock_args.rm_type = "random" + sample = Sample(prompt="", response="anything", label="anything") + reward = run(async_rm(mock_args, sample)) + assert reward in [0, 1] + + def test_rm_type_from_metadata(self, mock_args): + mock_args.rm_type = None + sample = Sample(prompt="", response=r"\boxed{42}", label="42", metadata={"rm_type": "math"}) + reward = run(async_rm(mock_args, sample)) + assert reward == 1 + + @pytest.mark.parametrize( + "rm_type,match", + [ + ("unknown_type", "not implemented"), + ("", "not specified"), + ], + ) + def test_invalid_rm_type_raises(self, mock_args, rm_type, match): + mock_args.rm_type = rm_type + sample = Sample(prompt="", response="test", label="test") + with pytest.raises(NotImplementedError, match=match): + run(async_rm(mock_args, sample)) + + +class TestBatchedAsyncRm: + @pytest.mark.parametrize( + "rm_type,samples_data,expected", + [ + ( + "math", + [(r"\boxed{42}", "42"), (r"\boxed{100}", "100"), (r"\boxed{wrong}", "42")], + [1, 1, 0], + ), + ( + "f1", + [("hello world", "hello world"), ("different", "something else")], + [1.0, 0], + ), + ], + ) + def test_batched_rm(self, mock_args, rm_type, samples_data, expected): + mock_args.rm_type = rm_type + samples = [Sample(prompt="", response=r, label=label) for r, label in samples_data] + rewards = run(batched_async_rm(mock_args, samples)) + assert rewards == expected + + def test_inplace_set_reward_field(self, mock_args): + mock_args.rm_type = "math" + samples = [ + Sample(prompt="", response=r"\boxed{42}", label="42"), + Sample(prompt="", response=r"\boxed{100}", label="100"), + ] + result = run(batched_async_rm(mock_args, samples, inplace_set_reward_field=True)) + assert result is None + assert samples[0].reward == 1 + assert samples[1].reward == 1 + + def test_inplace_raises_on_existing_reward(self, mock_args): + mock_args.rm_type = "math" + samples = [Sample(prompt="", response=r"\boxed{42}", label="42", reward=0.5)] + with pytest.raises(AssertionError, match="Overriding"): + run(batched_async_rm(mock_args, samples, inplace_set_reward_field=True)) + + def test_empty_samples(self, mock_args): + mock_args.rm_type = "math" + rewards = run(batched_async_rm(mock_args, [])) + assert rewards == [] + + def test_mixed_rm_types_via_metadata(self, mock_args): + mock_args.rm_type = None + samples = [ + Sample(prompt="", response=r"\boxed{42}", label="42", metadata={"rm_type": "math"}), + Sample(prompt="", response="hello", label="hello", metadata={"rm_type": "f1"}), + ] + rewards = run(batched_async_rm(mock_args, samples)) + assert rewards[0] == 1 + assert rewards[1] == 1.0 diff --git a/tests/fast/router/__init__.py b/tests/fast/router/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/router/test_router.py b/tests/fast/router/test_router.py new file mode 100644 index 000000000..7c645fe30 --- /dev/null +++ b/tests/fast/router/test_router.py @@ -0,0 +1,204 @@ +import asyncio +from argparse import Namespace + +import pytest +import requests + +from miles.router.router import MilesRouter +from miles.utils.http_utils import find_available_port +from miles.utils.test_utils.mock_sglang_server import MockSGLangServer, default_process_fn +from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer + + +def make_router_args(router_port: int, **overrides) -> Namespace: + defaults = dict( + sglang_router_ip="127.0.0.1", + sglang_router_port=router_port, + rollout_health_check_interval=1.0, + miles_router_health_check_failure_threshold=3, + miles_router_max_connections=100, + miles_router_timeout=None, + miles_router_middleware_paths=[], + ) + defaults.update(overrides) + return Namespace(**defaults) + + +def create_mock_worker(start_port: int = 30000) -> MockSGLangServer: + port = find_available_port(start_port) + return MockSGLangServer( + model_name="Qwen/Qwen3-0.6B", + process_fn=default_process_fn, + host="127.0.0.1", + port=port, + latency=0.0, + ) + + +class RouterEnv: + def __init__(self, router: MilesRouter, server: UvicornThreadServer): + self.router = router + self.server = server + + @property + def url(self) -> str: + return self.server.url + + +@pytest.fixture +def router_env(): + args = make_router_args(find_available_port(20000)) + router = MilesRouter(args, verbose=False) + server = UvicornThreadServer(router.app, host=args.sglang_router_ip, port=args.sglang_router_port) + server.start() + yield RouterEnv(router, server) + server.stop() + + +@pytest.fixture +def mock_worker(): + server = create_mock_worker() + server.start() + yield server + server.stop() + + +@pytest.fixture +def mock_worker_factory(): + servers = [] + + def _create(): + start_port = 30000 + len(servers) * 100 + server = create_mock_worker(start_port) + server.start() + servers.append(server) + return server + + yield _create + for s in servers: + s.stop() + + +@pytest.fixture +def router_factory(): + def _create(**overrides) -> MilesRouter: + args = make_router_args(find_available_port(20000), **overrides) + return MilesRouter(args, verbose=False) + + return _create + + +class TestWorkerManagement: + def test_add_worker_via_query_param(self, router_env: RouterEnv): + worker_url = "http://127.0.0.1:30001" + r = requests.post(f"{router_env.url}/add_worker", params={"url": worker_url}, timeout=5.0) + r.raise_for_status() + + assert r.json()["status"] == "success" + assert worker_url in router_env.router.worker_request_counts + assert router_env.router.worker_request_counts[worker_url] == 0 + + def test_add_worker_via_body(self, router_env: RouterEnv): + worker_url = "http://127.0.0.1:30002" + r = requests.post(f"{router_env.url}/add_worker", json={"url": worker_url}, timeout=5.0) + r.raise_for_status() + + assert r.json()["status"] == "success" + assert worker_url in router_env.router.worker_request_counts + + def test_add_worker_duplicate(self, router_env: RouterEnv): + worker_url = "http://127.0.0.1:30003" + requests.post(f"{router_env.url}/add_worker", params={"url": worker_url}, timeout=5.0).raise_for_status() + requests.post(f"{router_env.url}/add_worker", params={"url": worker_url}, timeout=5.0).raise_for_status() + + assert len(router_env.router.worker_request_counts) == 1 + assert worker_url in router_env.router.worker_request_counts + + def test_add_worker_missing_url(self, router_env: RouterEnv): + r = requests.post(f"{router_env.url}/add_worker", json={}, timeout=5.0) + assert r.status_code == 400 + assert "error" in r.json() + + def test_list_workers(self, router_env: RouterEnv): + worker_urls = ["http://127.0.0.1:30001", "http://127.0.0.1:30002"] + for url in worker_urls: + requests.post(f"{router_env.url}/add_worker", params={"url": url}, timeout=5.0) + + r = requests.get(f"{router_env.url}/list_workers", timeout=5.0) + r.raise_for_status() + assert set(r.json()["urls"]) == set(worker_urls) + + +class TestLoadBalancing: + def test_use_url_selects_min_load(self, router_factory): + router = router_factory() + router.worker_request_counts = {"http://w1:8000": 5, "http://w2:8000": 2, "http://w3:8000": 8} + + selected = router._use_url() + assert selected == "http://w2:8000" + assert router.worker_request_counts["http://w2:8000"] == 3 + + def test_use_url_excludes_dead_workers(self, router_factory): + router = router_factory() + router.worker_request_counts = {"http://w1:8000": 5, "http://w2:8000": 1, "http://w3:8000": 3} + router.dead_workers = {"http://w2:8000"} + + selected = router._use_url() + assert selected == "http://w3:8000" + assert router.worker_request_counts["http://w3:8000"] == 4 + + def test_use_url_raises_when_all_dead(self, router_factory): + router = router_factory() + router.worker_request_counts = {"http://w1:8000": 0} + router.dead_workers = {"http://w1:8000"} + + with pytest.raises(RuntimeError, match="No healthy workers"): + router._use_url() + + +# TODO: extract main body inside `_health_check_loop`, then can test that function +class TestHealthCheck: + def test_check_worker_health_success(self, router_factory, mock_worker: MockSGLangServer): + router = router_factory() + url, healthy = asyncio.run(router._check_worker_health(mock_worker.url)) + assert url == mock_worker.url + assert healthy is True + + def test_check_worker_health_failure(self, router_factory): + router = router_factory() + url, healthy = asyncio.run(router._check_worker_health("http://127.0.0.1:59999")) + assert url == "http://127.0.0.1:59999" + assert healthy is False + + +class TestProxyIntegration: + def test_proxy_forwards_request(self, router_env: RouterEnv, mock_worker: MockSGLangServer): + requests.post(f"{router_env.url}/add_worker", params={"url": mock_worker.url}, timeout=5.0).raise_for_status() + + payload = {"input_ids": [1, 2, 3], "return_logprob": True} + r = requests.post(f"{router_env.url}/generate", json=payload, timeout=10.0) + r.raise_for_status() + + assert "text" in r.json() + assert len(mock_worker.request_log) == 1 + assert mock_worker.request_log[0] == payload + + def test_proxy_multi_worker(self, router_env: RouterEnv, mock_worker_factory): + worker1, worker2 = mock_worker_factory(), mock_worker_factory() + requests.post(f"{router_env.url}/add_worker", params={"url": worker1.url}, timeout=5.0) + requests.post(f"{router_env.url}/add_worker", params={"url": worker2.url}, timeout=5.0) + + payload = {"input_ids": [1, 2, 3], "return_logprob": True} + for _ in range(4): + requests.post(f"{router_env.url}/generate", json=payload, timeout=10.0).raise_for_status() + + all_requests = worker1.request_log + worker2.request_log + assert len(all_requests) == 4 + assert all(req == payload for req in all_requests) + + def test_proxy_health_endpoint(self, router_env: RouterEnv, mock_worker: MockSGLangServer): + requests.post(f"{router_env.url}/add_worker", params={"url": mock_worker.url}, timeout=5.0) + + r = requests.get(f"{router_env.url}/health", timeout=5.0) + r.raise_for_status() + assert r.json()["status"] == "ok" diff --git a/tests/fast/router/test_sessions.py b/tests/fast/router/test_sessions.py new file mode 100644 index 000000000..5c6edafe2 --- /dev/null +++ b/tests/fast/router/test_sessions.py @@ -0,0 +1,195 @@ +from types import SimpleNamespace + +import pytest +import requests + +from miles.router.router import MilesRouter +from miles.router.sessions import SessionManager, SessionRecord +from miles.utils.http_utils import find_available_port +from miles.utils.test_utils.mock_sglang_server import ProcessResult, with_mock_server +from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer + + +class TestSessionManager: + def test_create_session(self): + manager = SessionManager() + session_id = manager.create_session() + assert session_id is not None + assert len(session_id) == 32 + assert session_id in manager.sessions + assert manager.sessions[session_id] == [] + + def test_get_session_exists(self): + manager = SessionManager() + session_id = manager.create_session() + records = manager.get_session(session_id) + assert records == [] + + def test_get_session_not_exists(self): + manager = SessionManager() + records = manager.get_session("nonexistent") + assert records is None + + def test_delete_session_exists(self): + manager = SessionManager() + session_id = manager.create_session() + records = manager.delete_session(session_id) + assert records == [] + assert session_id not in manager.sessions + + def test_delete_session_not_exists(self): + manager = SessionManager() + with pytest.raises(AssertionError): + manager.delete_session("nonexistent") + + def test_add_record(self): + manager = SessionManager() + session_id = manager.create_session() + record = SessionRecord( + timestamp=1234567890.0, + method="POST", + path="generate", + request={"prompt": "hello"}, + response={"text": "world"}, + status_code=200, + ) + manager.add_record(session_id, record) + assert len(manager.sessions[session_id]) == 1 + assert manager.sessions[session_id][0] == record + + def test_add_record_nonexistent_session(self): + manager = SessionManager() + record = SessionRecord( + timestamp=1234567890.0, + method="POST", + path="generate", + request={}, + response={}, + status_code=200, + ) + with pytest.raises(AssertionError): + manager.add_record("nonexistent", record) + + +@pytest.fixture(scope="class") +def router_url(): + def process_fn(prompt: str) -> ProcessResult: + return ProcessResult(text=f"echo: {prompt}", finish_reason="stop") + + with with_mock_server(process_fn=process_fn) as backend: + args = SimpleNamespace( + miles_router_max_connections=10, + miles_router_timeout=30, + miles_router_middleware_paths=[], + rollout_health_check_interval=60, + miles_router_health_check_failure_threshold=3, + hf_checkpoint="Qwen/Qwen3-0.6B", + ) + router = MilesRouter(args) + + port = find_available_port(31000) + server = UvicornThreadServer(router.app, host="127.0.0.1", port=port) + server.start() + + url = f"http://127.0.0.1:{port}" + requests.post(f"{url}/add_worker", json={"url": backend.url}) + + try: + yield url + finally: + server.stop() + + +class TestSessionRoutes: + def test_create_session(self, router_url): + response = requests.post(f"{router_url}/sessions") + assert response.status_code == 200 + data = response.json() + assert "session_id" in data + assert len(data["session_id"]) == 32 + + def test_get_session(self, router_url): + session_id = requests.post(f"{router_url}/sessions").json()["session_id"] + + get_resp = requests.get(f"{router_url}/sessions/{session_id}") + assert get_resp.status_code == 200 + data = get_resp.json() + assert data["session_id"] == session_id + assert data["records"] == [] + + def test_get_session_not_found(self, router_url): + response = requests.get(f"{router_url}/sessions/nonexistent") + assert response.status_code == 404 + assert response.json()["error"] == "session not found" + + def test_get_with_records(self, router_url): + session_id = requests.post(f"{router_url}/sessions").json()["session_id"] + + requests.post( + f"{router_url}/sessions/{session_id}/generate", + json={"input_ids": [1, 2, 3], "sampling_params": {}, "return_logprob": True}, + ) + + get_resp = requests.get(f"{router_url}/sessions/{session_id}") + assert get_resp.status_code == 200 + data = get_resp.json() + assert data["session_id"] == session_id + assert len(data["records"]) == 1 + + def test_delete_session(self, router_url): + session_id = requests.post(f"{router_url}/sessions").json()["session_id"] + + delete_resp = requests.delete(f"{router_url}/sessions/{session_id}") + assert delete_resp.status_code == 204 + assert delete_resp.text == "" + + assert requests.delete(f"{router_url}/sessions/{session_id}").status_code == 404 + + def test_delete_session_not_found(self, router_url): + response = requests.delete(f"{router_url}/sessions/nonexistent") + assert response.status_code == 404 + assert response.json()["error"] == "session not found" + + +class TestSessionProxy: + def test_proxy_session_not_found(self, router_url): + response = requests.post(f"{router_url}/sessions/nonexistent/generate", json={}) + assert response.status_code == 404 + assert response.json()["error"] == "session not found" + + def test_proxy_records_request_response(self, router_url): + session_id = requests.post(f"{router_url}/sessions").json()["session_id"] + + resp = requests.post( + f"{router_url}/sessions/{session_id}/generate", + json={"input_ids": [1, 2, 3], "sampling_params": {}, "return_logprob": True}, + ) + assert resp.status_code == 200 + assert "text" in resp.json() + + get_resp = requests.get(f"{router_url}/sessions/{session_id}") + records = get_resp.json()["records"] + assert len(records) == 1 + assert records[0]["method"] == "POST" + assert records[0]["path"] == "generate" + assert records[0]["request"]["input_ids"] == [1, 2, 3] + assert "text" in records[0]["response"] + + delete_resp = requests.delete(f"{router_url}/sessions/{session_id}") + assert delete_resp.status_code == 204 + + def test_proxy_accumulates_records(self, router_url): + session_id = requests.post(f"{router_url}/sessions").json()["session_id"] + + for _ in range(3): + requests.post( + f"{router_url}/sessions/{session_id}/generate", + json={"input_ids": [1], "sampling_params": {}, "return_logprob": True}, + ) + + get_resp = requests.get(f"{router_url}/sessions/{session_id}") + records = get_resp.json()["records"] + assert len(records) == 3 + + delete_resp = requests.delete(f"{router_url}/sessions/{session_id}") + assert delete_resp.status_code == 204 diff --git a/tests/fast/utils/__init__.py b/tests/fast/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/utils/test_arguments.py b/tests/fast/utils/test_arguments.py new file mode 100644 index 000000000..9bd1a620d --- /dev/null +++ b/tests/fast/utils/test_arguments.py @@ -0,0 +1,58 @@ +import argparse +import sys +from unittest.mock import patch + +import pytest + +from miles.utils.arguments import get_miles_extra_args_provider +from miles.utils.misc import function_registry + +PATH_ARGS = ["--rollout-function-path", "--custom-generate-function-path"] +REQUIRED_ARGS = ["--rollout-batch-size", "64"] + + +def make_class_with_add_arguments(): + class MyFn: + @classmethod + def add_arguments(cls, parser): + parser.add_argument("--my-custom-arg", type=int, default=42) + + return MyFn + + +def make_function_with_add_arguments(): + def my_fn(): + pass + + my_fn.add_arguments = lambda parser: parser.add_argument("--my-custom-arg", type=int, default=42) + return my_fn + + +def make_function_without_add_arguments(): + def my_fn(): + pass + + return my_fn + + +@pytest.mark.parametrize("path_arg", PATH_ARGS) +class TestAddArgumentsSupport: + + @pytest.mark.parametrize("fn_factory", [make_class_with_add_arguments, make_function_with_add_arguments]) + def test_add_arguments_is_called_and_arg_is_parsed(self, path_arg, fn_factory): + fn = fn_factory() + with function_registry.temporary("test:fn", fn), patch.object( + sys, "argv", ["test", path_arg, "test:fn", "--my-custom-arg", "100"] + REQUIRED_ARGS + ): + parser = argparse.ArgumentParser() + get_miles_extra_args_provider()(parser) + args, _ = parser.parse_known_args() + assert args.my_custom_arg == 100 + + def test_skips_function_without_add_arguments(self, path_arg): + fn = make_function_without_add_arguments() + with function_registry.temporary("test:fn", fn), patch.object( + sys, "argv", ["test", path_arg, "test:fn"] + REQUIRED_ARGS + ): + parser = argparse.ArgumentParser() + get_miles_extra_args_provider()(parser) diff --git a/tests/utils/test_mask_utils.py b/tests/fast/utils/test_mask_utils.py similarity index 100% rename from tests/utils/test_mask_utils.py rename to tests/fast/utils/test_mask_utils.py diff --git a/tests/fast/utils/test_misc.py b/tests/fast/utils/test_misc.py new file mode 100644 index 000000000..810c2b67c --- /dev/null +++ b/tests/fast/utils/test_misc.py @@ -0,0 +1,59 @@ +import os + +import pytest + +from miles.utils.misc import FunctionRegistry, function_registry, load_function + + +def _fn_a(): + return "a" + + +def _fn_b(): + return "b" + + +class TestFunctionRegistry: + def test_register_and_get(self): + registry = FunctionRegistry() + with registry.temporary("my_fn", _fn_a): + assert registry.get("my_fn") is _fn_a + + def test_register_duplicate_raises(self): + registry = FunctionRegistry() + with registry.temporary("my_fn", _fn_a): + with pytest.raises(AssertionError): + with registry.temporary("my_fn", _fn_b): + pass + + def test_unregister(self): + registry = FunctionRegistry() + with registry.temporary("my_fn", _fn_a): + assert registry.get("my_fn") is _fn_a + assert registry.get("my_fn") is None + + def test_temporary_cleanup_on_exception(self): + registry = FunctionRegistry() + with pytest.raises(RuntimeError): + with registry.temporary("temp_fn", _fn_a): + raise RuntimeError("test") + assert registry.get("temp_fn") is None + + +class TestLoadFunction: + def test_load_from_module(self): + import os.path + + assert load_function("os.path.join") is os.path.join + + def test_load_none_returns_none(self): + assert load_function(None) is None + + def test_load_from_registry(self): + with function_registry.temporary("test:my_fn", _fn_a): + assert load_function("test:my_fn") is _fn_a + + def test_registry_takes_precedence(self): + with function_registry.temporary("os.path.join", _fn_b): + assert load_function("os.path.join") is _fn_b + assert load_function("os.path.join") is os.path.join diff --git a/tests/fast/utils/test_utils/__init__.py b/tests/fast/utils/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fast/utils/test_utils/test_mock_sglang_server.py b/tests/fast/utils/test_utils/test_mock_sglang_server.py new file mode 100644 index 000000000..6633678da --- /dev/null +++ b/tests/fast/utils/test_utils/test_mock_sglang_server.py @@ -0,0 +1,409 @@ +import asyncio +import concurrent.futures +import time + +import pytest +import requests + +from miles.utils.test_utils.mock_sglang_server import ( + Counter, + ProcessResult, + ProcessResultMetaInfo, + default_process_fn, + with_mock_server, +) +from miles.utils.test_utils.mock_tools import SAMPLE_TOOLS, TwoTurnStub + + +def expected_logprobs(tokenizer, text: str) -> list[dict]: + output_ids = tokenizer.encode(text, add_special_tokens=False) + return [{"token": tokenizer.convert_ids_to_tokens(tid), "logprob": -i / 128} for i, tid in enumerate(output_ids)] + + +@pytest.fixture(scope="module") +def mock_server(): + with with_mock_server() as server: + yield server + + +class TestProcessResultMetaInfo: + def test_to_dict_empty(self): + assert ProcessResultMetaInfo().to_dict() == {} + + def test_to_dict_single_field(self): + assert ProcessResultMetaInfo(weight_version="v1").to_dict() == {"weight_version": "v1"} + + def test_to_dict_partial_fields(self): + assert ProcessResultMetaInfo(weight_version="v1", spec_accept_token_num=10).to_dict() == { + "weight_version": "v1", + "spec_accept_token_num": 10, + } + + def test_to_dict_all_fields(self): + assert ProcessResultMetaInfo( + weight_version="v1", + routed_experts="abc", + spec_accept_token_num=10, + spec_draft_token_num=15, + spec_verify_ct=3, + ).to_dict() == { + "weight_version": "v1", + "routed_experts": "abc", + "spec_accept_token_num": 10, + "spec_draft_token_num": 15, + "spec_verify_ct": 3, + } + + +class TestDefaultProcessFn: + def test_math_question(self): + assert default_process_fn("What is 1+5?") == ProcessResult(text="\\boxed{6}", finish_reason="stop") + assert default_process_fn("What is 1+10?") == ProcessResult(text="\\boxed{11}", finish_reason="stop") + + def test_unknown_question(self): + assert default_process_fn("Hello") == ProcessResult(text="I don't understand.", finish_reason="stop") + + +class TestCounter: + def test_tracks_max(self): + counter = Counter() + assert counter.max_value == 0 + + with counter.track(): + assert counter.max_value == 1 + with counter.track(): + assert counter.max_value == 2 + + counter.reset() + assert counter.max_value == 0 + + def test_concurrent_tasks(self): + counter = Counter() + + async def task(): + with counter.track(): + await asyncio.sleep(0.1) + + async def run_all(): + await asyncio.gather(task(), task(), task()) + + asyncio.run(run_all()) + assert counter.max_value == 3 + + +class TestMockServerBasic: + def test_start_stop(self, mock_server): + assert mock_server.port > 0 + assert f"http://{mock_server.host}:{mock_server.port}" == mock_server.url + + def test_request_log_and_reset_stats(self, mock_server): + mock_server.reset_stats() + assert len(mock_server.request_log) == 0 + + payload = {"input_ids": [1, 2, 3], "sampling_params": {"temperature": 0.5}, "return_logprob": True} + requests.post(f"{mock_server.url}/generate", json=payload, timeout=5.0) + assert len(mock_server.request_log) == 1 + assert mock_server.request_log[0] == payload + + mock_server.reset_stats() + assert len(mock_server.request_log) == 0 + assert mock_server.max_concurrent == 0 + + @pytest.mark.parametrize("latency,min_time,max_time", [(0.0, 0.0, 0.3), (0.5, 0.5, 1.0)]) + def test_latency(self, latency, min_time, max_time): + with with_mock_server(latency=latency) as server: + start = time.time() + requests.post(f"{server.url}/generate", json={"input_ids": [1], "sampling_params": {}}, timeout=5.0) + elapsed = time.time() - start + assert min_time <= elapsed < max_time + + def test_max_concurrent_with_latency(self): + with with_mock_server(latency=0.1) as server: + + def send_request(): + requests.post(f"{server.url}/generate", json={"input_ids": [1], "sampling_params": {}}, timeout=5.0) + + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [executor.submit(send_request) for _ in range(3)] + concurrent.futures.wait(futures) + + assert server.max_concurrent == 3 + + def test_health_endpoint(self, mock_server): + response = requests.get(f"{mock_server.url}/health", timeout=5.0) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + def test_abort_request_endpoint(self, mock_server): + response = requests.post(f"{mock_server.url}/abort_request", json={}, timeout=5.0) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +class TestGenerateEndpoint: + def test_basic(self, mock_server): + prompt = "What is 1+7?" + input_ids = mock_server.tokenizer.encode(prompt, add_special_tokens=False) + assert input_ids == [3838, 374, 220, 16, 10, 22, 30] + + response = requests.post( + f"{mock_server.url}/generate", + json={ + "input_ids": input_ids, + "sampling_params": {"temperature": 0.7, "max_new_tokens": 10}, + "return_logprob": True, + }, + timeout=5.0, + ) + assert response.status_code == 200 + assert response.json() == { + "text": "\\boxed{8}", + "meta_info": { + "finish_reason": {"type": "stop"}, + "prompt_tokens": len(input_ids), + "cached_tokens": 0, + "completion_tokens": 5, + "output_token_logprobs": [ + [-0.0, 59], + [-0.0078125, 79075], + [-0.015625, 90], + [-0.0234375, 23], + [-0.03125, 92], + ], + }, + } + + def test_with_meta_info(self): + def process_fn(_: str) -> ProcessResult: + return ProcessResult( + text="ok", + finish_reason="stop", + cached_tokens=5, + meta_info=ProcessResultMetaInfo( + weight_version="v2.0", + routed_experts="encoded_data", + spec_accept_token_num=10, + spec_draft_token_num=15, + spec_verify_ct=3, + ), + ) + + with with_mock_server(process_fn=process_fn) as server: + response = requests.post( + f"{server.url}/generate", + json={"input_ids": [1, 2, 3], "sampling_params": {}, "return_logprob": True}, + timeout=5.0, + ) + + assert response.json() == { + "text": "ok", + "meta_info": { + "finish_reason": {"type": "stop"}, + "prompt_tokens": 3, + "cached_tokens": 5, + "completion_tokens": 1, + "output_token_logprobs": [[-0.0, 562]], + "weight_version": "v2.0", + "routed_experts": "encoded_data", + "spec_accept_token_num": 10, + "spec_draft_token_num": 15, + "spec_verify_ct": 3, + }, + } + + def test_finish_reason_length(self): + def process_fn(_: str) -> ProcessResult: + return ProcessResult(text="truncated output", finish_reason="length") + + with with_mock_server(process_fn=process_fn) as server: + response = requests.post( + f"{server.url}/generate", + json={"input_ids": [1, 2, 3], "sampling_params": {}, "return_logprob": True}, + timeout=5.0, + ) + data = response.json() + + finish_reason = data["meta_info"]["finish_reason"] + assert finish_reason["type"] == "length" + assert finish_reason["length"] == data["meta_info"]["completion_tokens"] + + +class TestChatCompletionsEndpoint: + def test_basic(self, mock_server): + response = requests.post( + f"{mock_server.url}/v1/chat/completions", + json={ + "model": "test-model", + "messages": [{"role": "user", "content": "What is 1+5?"}], + }, + timeout=5.0, + ) + assert response.status_code == 200 + data = response.json() + + assert data["id"].startswith("chatcmpl-") + assert isinstance(data["created"], int) + assert data == { + "id": data["id"], + "object": "chat.completion", + "created": data["created"], + "model": "mock-model", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "\\boxed{6}", "tool_calls": None}, + "logprobs": {"content": expected_logprobs(mock_server.tokenizer, "\\boxed{6}")}, + "finish_reason": "stop", + } + ], + } + + def test_with_tool_calls(self): + tool_call_response = 'Let me check for you.\n\n{"name": "get_year", "arguments": {}}\n' + + def process_fn(_: str) -> ProcessResult: + return ProcessResult(text=tool_call_response, finish_reason="stop") + + with with_mock_server(process_fn=process_fn) as server: + response = requests.post( + f"{server.url}/v1/chat/completions", + json={ + "model": "test", + "messages": [{"role": "user", "content": "What year is it?"}], + "tools": SAMPLE_TOOLS, + }, + timeout=5.0, + ) + data = response.json() + + assert data["choices"][0] == { + "index": 0, + "message": { + "role": "assistant", + "content": "Let me check for you.", + "tool_calls": [ + {"id": "call00000", "type": "function", "function": {"name": "get_year", "arguments": "{}"}} + ], + }, + "logprobs": {"content": expected_logprobs(server.tokenizer, tool_call_response)}, + "finish_reason": "tool_calls", + } + + def test_with_tools_but_no_tool_call(self): + response_text = "The weather is sunny today." + + def process_fn(_: str) -> ProcessResult: + return ProcessResult(text=response_text, finish_reason="stop") + + with with_mock_server(process_fn=process_fn) as server: + response = requests.post( + f"{server.url}/v1/chat/completions", + json={ + "model": "test", + "messages": [{"role": "user", "content": "What's the weather?"}], + "tools": SAMPLE_TOOLS, + }, + timeout=5.0, + ) + data = response.json() + + assert data["choices"][0] == { + "index": 0, + "message": {"role": "assistant", "content": response_text, "tool_calls": None}, + "logprobs": {"content": expected_logprobs(server.tokenizer, response_text)}, + "finish_reason": "stop", + } + + def test_with_multiple_tool_calls(self): + multi_tool_response = ( + "I will get year and temperature.\n" + '\n{"name": "get_year", "arguments": {}}\n\n' + '\n{"name": "get_temperature", "arguments": {"location": "Shanghai"}}\n' + ) + + def process_fn(_: str) -> ProcessResult: + return ProcessResult(text=multi_tool_response, finish_reason="stop") + + with with_mock_server(process_fn=process_fn) as server: + response = requests.post( + f"{server.url}/v1/chat/completions", + json={ + "model": "test", + "messages": [{"role": "user", "content": "What year and temperature?"}], + "tools": SAMPLE_TOOLS, + }, + timeout=5.0, + ) + data = response.json() + + assert data["choices"][0] == { + "index": 0, + "message": { + "role": "assistant", + "content": "I will get year and temperature.", + "tool_calls": [ + {"id": "call00000", "type": "function", "function": {"name": "get_year", "arguments": "{}"}}, + { + "id": "call00001", + "type": "function", + "function": {"name": "get_temperature", "arguments": '{"location": "Shanghai"}'}, + }, + ], + }, + "logprobs": {"content": expected_logprobs(server.tokenizer, multi_tool_response)}, + "finish_reason": "tool_calls", + } + + +class TestMultiTurnToolCallProcessFn: + @pytest.mark.parametrize( + "prompt,expected_response", + [ + pytest.param(TwoTurnStub.FIRST_PROMPT, TwoTurnStub.FIRST_RESPONSE, id="first_turn"), + pytest.param(TwoTurnStub.SECOND_PROMPT, TwoTurnStub.SECOND_RESPONSE, id="second_turn"), + ], + ) + def test_generate_endpoint(self, prompt, expected_response): + with with_mock_server(process_fn=TwoTurnStub.process_fn) as server: + input_ids = server.tokenizer.encode(prompt, add_special_tokens=False) + response = requests.post( + f"{server.url}/generate", + json={"input_ids": input_ids, "sampling_params": {}, "return_logprob": True}, + timeout=5.0, + ) + assert response.status_code == 200 + data = response.json() + assert data["text"] == expected_response + assert data["meta_info"]["finish_reason"] == {"type": "stop"} + + @pytest.mark.parametrize( + "messages,expected_content,expected_tool_calls,expected_finish_reason", + [ + pytest.param( + TwoTurnStub.OPENAI_MESSAGES_FIRST_TURN, + TwoTurnStub.FIRST_RESPONSE_CONTENT, + TwoTurnStub.FIRST_TOOL_CALLS_OPENAI_FORMAT, + "tool_calls", + id="first_turn", + ), + pytest.param( + TwoTurnStub.OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT, + TwoTurnStub.SECOND_RESPONSE, + None, + "stop", + id="second_turn", + ), + ], + ) + def test_chat_completions_endpoint(self, messages, expected_content, expected_tool_calls, expected_finish_reason): + with with_mock_server(process_fn=TwoTurnStub.process_fn) as server: + response = requests.post( + f"{server.url}/v1/chat/completions", + json={"model": "test", "messages": messages, "tools": SAMPLE_TOOLS}, + timeout=5.0, + ) + assert response.status_code == 200 + data = response.json() + assert data["choices"][0]["message"]["content"] == expected_content + assert data["choices"][0]["message"]["tool_calls"] == expected_tool_calls + assert data["choices"][0]["finish_reason"] == expected_finish_reason diff --git a/tests/fast/utils/test_utils/test_mock_tools.py b/tests/fast/utils/test_utils/test_mock_tools.py new file mode 100644 index 000000000..3f2116ec0 --- /dev/null +++ b/tests/fast/utils/test_utils/test_mock_tools.py @@ -0,0 +1,111 @@ +import asyncio + +import pytest +from pydantic import TypeAdapter +from sglang.srt.entrypoints.openai.protocol import Tool +from sglang.srt.function_call.core_types import ToolCallItem +from sglang.srt.function_call.function_call_parser import FunctionCallParser + +from miles.utils.test_utils.mock_tools import SAMPLE_TOOLS, TwoTurnStub, execute_tool_call + + +class TestExecuteToolCall: + def test_execute_get_year(self): + result = asyncio.run(execute_tool_call("get_year", {})) + assert result == '{"year": 2026}' + + def test_execute_get_temperature(self): + result = asyncio.run(execute_tool_call("get_temperature", {"location": "Mars"})) + assert result == '{"temperature": -60}' + + +class TestApplyChatTemplateWithTools: + EXPECTED_PROMPT_WITHOUT_TOOLS = ( + "<|im_start|>user\n" "What's the weather in Paris?<|im_end|>\n" "<|im_start|>assistant\n" + ) + + EXPECTED_PROMPT_WITH_TOOLS = ( + "<|im_start|>system\n" + "# Tools\n\n" + "You may call one or more functions to assist with the user query.\n\n" + "You are provided with function signatures within XML tags:\n" + "\n" + '{"type": "function", "function": {"name": "get_year", "description": "Get current year", "parameters": {"type": "object", "properties": {}, "required": []}}}\n' + '{"type": "function", "function": {"name": "get_temperature", "description": "Get temperature for a location", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}}\n' + "\n\n" + "For each function call, return a json object with function name and arguments within XML tags:\n" + "\n" + '{"name": , "arguments": }\n' + "<|im_end|>\n" + "<|im_start|>user\n" + "What's the weather in Paris?<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + @pytest.mark.parametrize( + "tools,expected", + [ + pytest.param(None, EXPECTED_PROMPT_WITHOUT_TOOLS, id="without_tools"), + pytest.param(SAMPLE_TOOLS, EXPECTED_PROMPT_WITH_TOOLS, id="with_tools"), + ], + ) + def test_apply_chat_template(self, tools, expected): + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B", trust_remote_code=True) + messages = [{"role": "user", "content": "What's the weather in Paris?"}] + + prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools) + + assert prompt == expected + + +class TestSGLangFunctionCallParser: + """Test to demonstrate and ensure SGLang function call parser have features we need without breaking changes.""" + + @pytest.mark.parametrize( + "model_output,expected", + [ + pytest.param( + 'Let me check for you.\n\n{"name": "get_year", "arguments": {}}\n', + ( + "Let me check for you.", + [ToolCallItem(tool_index=-1, name="get_year", parameters="{}")], + ), + id="single_tool_call", + ), + pytest.param( + "I will get year and temperature.\n" + '\n{"name": "get_year", "arguments": {}}\n\n' + '\n{"name": "get_temperature", "arguments": {"location": "Shanghai"}}\n', + ( + "I will get year and temperature.", + [ + ToolCallItem(tool_index=-1, name="get_year", parameters="{}"), + ToolCallItem(tool_index=-1, name="get_temperature", parameters='{"location": "Shanghai"}'), + ], + ), + id="multi_tool_calls", + ), + pytest.param( + "The weather is sunny today.", + ("The weather is sunny today.", []), + id="no_tool_call", + ), + pytest.param( + TwoTurnStub.FIRST_RESPONSE, + ( + "Let me get the year and temperature first.", + [ + ToolCallItem(tool_index=-1, name="get_year", parameters="{}"), + ToolCallItem(tool_index=-1, name="get_temperature", parameters='{"location": "Mars"}'), + ], + ), + id="multi_turn_first_response", + ), + ], + ) + def test_parse_non_stream(self, model_output, expected): + tools = TypeAdapter(list[Tool]).validate_python(SAMPLE_TOOLS) + parser = FunctionCallParser(tools=tools, tool_call_parser="qwen25") + assert parser.parse_non_stream(model_output) == expected diff --git a/tests/test_external_rollout.py b/tests/test_external_rollout.py index c5c0838c5..9b6e69c29 100644 --- a/tests/test_external_rollout.py +++ b/tests/test_external_rollout.py @@ -126,6 +126,7 @@ def execute(): num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, before_ray_job_submit=_launch_background, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_mimo_7B_mtp_only_grad.py b/tests/test_mimo_7B_mtp_only_grad.py index 97c76ace5..d90a2d7a7 100644 --- a/tests/test_mimo_7B_mtp_only_grad.py +++ b/tests/test_mimo_7B_mtp_only_grad.py @@ -135,6 +135,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_moonlight_16B_A3B.py b/tests/test_moonlight_16B_A3B.py index b1255982e..c35943ec1 100644 --- a/tests/test_moonlight_16B_A3B.py +++ b/tests/test_moonlight_16B_A3B.py @@ -113,6 +113,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_quick_start_glm4_9B.py b/tests/test_quick_start_glm4_9B.py index 15ca8ce5f..ae3c383ae 100644 --- a/tests/test_quick_start_glm4_9B.py +++ b/tests/test_quick_start_glm4_9B.py @@ -115,6 +115,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen2.5_0.5B_gsm8k.py b/tests/test_qwen2.5_0.5B_gsm8k.py index dcdbd5834..4d7f034f6 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k.py +++ b/tests/test_qwen2.5_0.5B_gsm8k.py @@ -120,6 +120,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen2.5_0.5B_gsm8k_async.py b/tests/test_qwen2.5_0.5B_gsm8k_async.py index dcaaf5e1f..32b60f593 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_async.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_async.py @@ -120,6 +120,7 @@ def execute(): num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, train_script="train_async.py", + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen2.5_0.5B_gsm8k_async_short.py b/tests/test_qwen2.5_0.5B_gsm8k_async_short.py index 90cd15cb6..b1954a4e8 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_async_short.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_async_short.py @@ -118,6 +118,7 @@ def execute(): num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, train_script="train_async.py", + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen2.5_0.5B_gsm8k_short.py b/tests/test_qwen2.5_0.5B_gsm8k_short.py index 867fdcad6..86e21eac8 100644 --- a/tests/test_qwen2.5_0.5B_gsm8k_short.py +++ b/tests/test_qwen2.5_0.5B_gsm8k_short.py @@ -117,6 +117,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py b/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py index 3d19b48ce..3d4768e42 100644 --- a/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py +++ b/tests/test_qwen3_0.6B_fsdp_colocated_2xGPU.py @@ -93,6 +93,7 @@ def execute(): train_args=train_args, num_gpus_per_node=2, megatron_model_type=None, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen3_0.6B_fsdp_distributed.py b/tests/test_qwen3_0.6B_fsdp_distributed.py index 3d70f3e4c..fcd777288 100644 --- a/tests/test_qwen3_0.6B_fsdp_distributed.py +++ b/tests/test_qwen3_0.6B_fsdp_distributed.py @@ -95,6 +95,7 @@ def execute(): num_gpus_per_node=2 if FEW_GPU else 4, megatron_model_type=None, train_script="train_async.py", + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen3_0.6B_megatron_fsdp_align.py b/tests/test_qwen3_0.6B_megatron_fsdp_align.py index 1431d8c3d..b89a2f283 100644 --- a/tests/test_qwen3_0.6B_megatron_fsdp_align.py +++ b/tests/test_qwen3_0.6B_megatron_fsdp_align.py @@ -97,6 +97,7 @@ def execute(): train_args=train_args + (f"{fsdp_args}" f"--save-debug-rollout-data {debug_data_path} "), num_gpus_per_node=NUM_GPUS, megatron_model_type=None, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) U.execute_train( @@ -109,6 +110,7 @@ def execute(): ), num_gpus_per_node=NUM_GPUS, megatron_model_type=None, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) U.execute_train( @@ -135,6 +137,7 @@ def execute(): "--debug-train-only " ), num_gpus_per_node=NUM_GPUS, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, megatron_model_type=MODEL_TYPE, ) diff --git a/tests/test_qwen3_0.6B_parallel_check.py b/tests/test_qwen3_0.6B_parallel_check.py index 44f5c42fa..d0ad283d1 100644 --- a/tests/test_qwen3_0.6B_parallel_check.py +++ b/tests/test_qwen3_0.6B_parallel_check.py @@ -95,6 +95,7 @@ def execute(): ), num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) # 8 GPU CPU 1 for num_gpus in [8, 4, 2]: @@ -124,6 +125,7 @@ def execute(): train_args=args, num_gpus_per_node=num_gpus, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) train_args += "--calculate-per-token-loss " diff --git a/tests/test_qwen3_30B_A3B.py b/tests/test_qwen3_30B_A3B.py index adff10804..b30eeed8e 100644 --- a/tests/test_qwen3_30B_A3B.py +++ b/tests/test_qwen3_30B_A3B.py @@ -139,6 +139,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen3_4B_ckpt.py b/tests/test_qwen3_4B_ckpt.py index 22fb2b5fc..0df4492e1 100644 --- a/tests/test_qwen3_4B_ckpt.py +++ b/tests/test_qwen3_4B_ckpt.py @@ -124,6 +124,7 @@ def execute(mode: str = ""): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen3_4B_fsdp_true_on_policy.py b/tests/test_qwen3_4B_fsdp_true_on_policy.py index 7c975c7cc..03ba4094e 100644 --- a/tests/test_qwen3_4B_fsdp_true_on_policy.py +++ b/tests/test_qwen3_4B_fsdp_true_on_policy.py @@ -95,6 +95,7 @@ def execute(): "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", "CUBLAS_WORKSPACE_CONFIG": ":4096:8", "CUDA_DEVICE_MAX_CONNECTIONS": "1", + "MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1", } U.execute_train( diff --git a/tests/test_qwen3_4B_ppo.py b/tests/test_qwen3_4B_ppo.py index 962f610fa..d4c1ac273 100644 --- a/tests/test_qwen3_4B_ppo.py +++ b/tests/test_qwen3_4B_ppo.py @@ -122,6 +122,7 @@ def execute(): train_args=train_args, num_gpus_per_node=NUM_GPUS, megatron_model_type=MODEL_TYPE, + extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, ) diff --git a/tests/test_qwen3_vl_4B_fsdp.py b/tests/test_qwen3_vl_4B_fsdp.py index fbdffd237..bc4ef3293 100644 --- a/tests/test_qwen3_vl_4B_fsdp.py +++ b/tests/test_qwen3_vl_4B_fsdp.py @@ -92,6 +92,7 @@ def execute(): extra_env_vars = { "CUDA_DEVICE_MAX_CONNECTIONS": "1", + "MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1", } U.execute_train( From e1e23050bfcbbd5ee584fa4b08f6ae2e9b34ca59 Mon Sep 17 00:00:00 2001 From: zhihaow6 Date: Thu, 22 Jan 2026 13:58:34 -0800 Subject: [PATCH 52/57] update --- A.py | 1986 +++++++++++++++++++++++++++++ docker/deepseekv32/megatron.patch | 169 ++- 2 files changed, 2089 insertions(+), 66 deletions(-) create mode 100644 A.py diff --git a/A.py b/A.py new file mode 100644 index 000000000..811f1c692 --- /dev/null +++ b/A.py @@ -0,0 +1,1986 @@ +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +import warnings +from dataclasses import dataclass +from typing import Callable, List, Literal, Optional, Tuple, Union + +import torch +import torch.nn.functional as F + +from megatron.core.enums import Fp4Recipe, Fp8Recipe +from megatron.core.quantization.quant_config import RecipeConfig +from megatron.core.transformer.enums import AttnBackend, CudaGraphScope +from megatron.core.transformer.pipeline_parallel_layer_layout import PipelineParallelLayerLayout + +from ..fusions.fused_bias_geglu import quick_gelu +from ..model_parallel_config import ModelParallelConfig +from ..utils import ( + get_te_version, + init_method_normal, + is_te_min_version, + is_torch_min_version, + scaled_init_method_normal, +) + +try: + from packaging.version import Version as PkgVersion + + HAVE_PACKAGING = True +except ImportError: + HAVE_PACKAGING = False + + +@dataclass +class TransformerConfig(ModelParallelConfig): + """Configuration object for megatron-core transformers. + + The initialization function has an argument for each parameter, + including those in ModelParallelConfig. + """ + + #################### + # model architecture + #################### + + num_layers: int = 0 + """Number of transformer layers in a transformer block.""" + + mtp_num_layers: Optional[int] = None + """Number of Multi-Token Prediction (MTP) Layers.""" + + mtp_loss_scaling_factor: Optional[float] = None + """Weighting factor of Multi-Token Prediction (MTP) loss.""" + + num_layers_in_first_pipeline_stage: Optional[int] = None + """Number of transformer layers on first pipeline stage. + None implies equal layer division across PP ranks.""" + + num_layers_in_last_pipeline_stage: Optional[int] = None + """Number of transformer layers on last pipeline stage. + None implies equal layer division across PP ranks.""" + + pipeline_model_parallel_layout: Optional[Union[str, list, PipelineParallelLayerLayout]] = None + """Custom definition of the pipeline parallel partitioning. + Support type: + - str: e.g., 'Et*3|(tt|)*29,m|L'. Stages are split by '|', replicated stages or layers + can be described with multiplication. Commas can be used cosmetically. + - list: e.g., [['embedding', 'decoder'], ['decoder', 'decoder', 'decoder', 'loss']]. + - PipelineParallelLayerLayout: a PipelineParallelLayerLayout object. + If given either a string or a list, it will be transferred into a PipelineParallelLayerLayout + in post init. Let i = a * pp_size + b, then layout[i] gives a list of the layers + in the a-th vpp stage and the b-th pp stage, i.e., vpp(0)pp(0), vpp(0)pp(1), ..., + vpp(i)pp(j), vpp(i)pp(j+1), ..., vpp(-1)pp(-2), vpp(-1)pp(-1). + In the inner lists of layers, 'embedding' or 'E' denotes the embedding layer, 'loss' or 'L' + denotes the loss function, and 'decoder' or 't' denotes the transformer decoder layer. + Examples: + [['embedding', 'decoder'], ['decoder', 'decoder', 'decoder', 'loss']]: + pp = 2, vpp = None + pp rank 0 holds: embedding, decoder + pp rank 1 holds: decoder*3, loss + 'E|(tt|)*2,(t|)*4,mL': + pp = 2, vpp = 4 + vpp rank 0 pp rank 0 holds: embedding + vpp rank 0 pp rank 1~2 holds: decoder*2 + vpp rank 0 pp rank 3 holds: decoder + vpp rank 1 pp rank 0~2 holds: decoder + vpp rank 1 pp rank 3 holds: mtp, loss""" + + account_for_embedding_in_pipeline_split: bool = False + """If set, the embedding layer will be treated as a standard transformer + layer in the context of partition and placement for pipeline parallelism.""" + + account_for_loss_in_pipeline_split: bool = False + """If set, the loss layer will be treated as a standard transformer + layer in the context of partition and placement for pipeline parallelism.""" + + hidden_size: int = 0 + """Transformer hidden size.""" + + num_attention_heads: int = 0 + """Number of transformer attention heads.""" + + attention_backend: AttnBackend = AttnBackend.auto + """Attention backend to run. By default we let transformer engine + decide the best backend to run (except in the case of local). + If attention backend is local we use the local pytorch implementation in mcore. + Users can specify exact backend by changing this config. """ + + softmax_scale: Optional[float] = None + """Softmax scale for attention scaling.""" + + softmax_type: Literal['vanilla', 'off-by-one', 'learnable'] = 'vanilla' + """Applies modified softmax from https://www.evanmiller.org/attention-is-off-by-one.html. + Supports both TE FusedAttention and local unfused attention. Supports both a fixed offset and + and learnable offset.""" + + num_query_groups: Optional[int] = None + """Number of query groups for group query attention. If None, normal attention is used.""" + + ffn_hidden_size: Optional[int] = None + """Transformer Feed-Forward Network hidden size. This is set to 4*hidden_size + if not provided.""" + + kv_channels: Optional[int] = None + """Projection weights dimension in multi-head attention. This is set to hidden_size // + num_attention_heads if not provided.""" + + hidden_dropout: float = 0.1 + """Dropout probability for transformer hidden state.""" + + attention_dropout: float = 0.1 + """Post attention dropout probability.""" + + fp32_residual_connection: bool = False + """If true, move residual connections to fp32.""" + + # @jcasper should we keep this option? + apply_residual_connection_post_layernorm: bool = False + """If True, uses the original BERT residule connection ordering.""" + + layernorm_epsilon: float = 1e-5 + """Epsilon value for any LayerNorm operations.""" + + layernorm_zero_centered_gamma: bool = False + """If set to True, the LayerNorm is adjusted to center the gamma values around 0. This improves + numerical stability.""" + + add_bias_linear: bool = True + """Include a bias term in all linear layers (QKV projections, after core attention, and two in + MLP layer).""" + + add_qkv_bias: bool = False + """Add a bias term only for QKV projections.""" + + gated_linear_unit: bool = False + """Use a gated linear unit for the first linear layer in the MLP.""" + + activation_func: Callable = F.gelu + """Activation function to use for the non-linearity in the MLP.""" + + activation_func_fp8_input_store: bool = False + """Store the input of MLP activation function in FP8 for backprop to save memory. + The stored input is casted back to the original precision before backprop compuatation.""" + + glu_linear_offset: float = 0.0 + """Offset term in the GLU activation function: activation_func(x[0]) * (x[1] + offset). Only + used when gated_linear_unit is True""" + + activation_func_clamp_value: Optional[float] = None + """Clamp the output of the linear_fc1 in the activation function. Only used when activation_func + is quick_gelu.""" + + num_moe_experts: Optional[int] = None + """Number of experts to use for MoE layer. When set, it replaces MLP with MoE layer. Set to None + for no MoE.""" + + rotary_interleaved: bool = False + """True is rotate pairs of even and odd dimensions (RoFormer style), False is rotate pairs of + first half and second half (LLaMa style). Default to False.""" + + window_size: Optional[Tuple[int, int]] = None + """If not None, then will use sliding window attention. The size of the window is specified by + the numbers inside the tuple; -1 is special value meaning "infinite window size".""" + + window_attn_skip_freq: Optional[Union[int, List[int]]] = None + """Frequency of full attention layers among sliding window attention layers. Accepts either: + - An integer N: Represents a (N-1):1 ratio, one full attention layer after (N-1) SWA layers. + - A list that defines a custom pattern, e.g.: [1,1,1,1,0,0,0,0], where 1 represents SWA. """ + + normalization: str = "LayerNorm" + """Which norm to use for normalization layers, valid options are `LayerNorm` and `RMSNorm`.""" + + qk_layernorm: bool = False + """Whether to apply `normalization` type of normalization to the query and key embeddings.""" + + qk_clip: bool = False + """Whether to clip the query and key weights. Needed for Muon MLA Model training.""" + + qk_clip_alpha: float = 0.5 + """The balancing alpha for qk-clip. Q = Q * (eta ** alpha)""" + + qk_clip_threshold: float = 100 + """The balancing threshold for qk-clip. eta = min(threshold / max_attention_logits, 1.0)""" + + log_max_attention_logit: bool = False + """Whether to log the max attention logit across whole model. Decoupled from qk_clip, + defualts to False. Setting qk_clip will automatically log the max logit""" + + attention_output_gate: bool = False + """Whether to apply output gate to the attention layers.""" + + test_mode: bool = False + """Whether to run real-time tests.""" + + calculate_per_token_loss: bool = False + """Whether cross entropy loss is calculated over the actual number of non-padded tokens in the + global batch, versus the default behavior of assuming all tokens are non-padded.""" + + multi_latent_attention: bool = False + """Whether to use multi-latent attention.""" + + no_rope_freq: Optional[Union[int, List[int]]] = None + """Controls which layers perform Rotary Position Embedding (RoPE). Accepts either: + An integer N: Creates a pattern where RoPE is skipped every N-1 layers. For example, + no_rope=4 means RoPE is applied for 3 layers, then skipped for 1 layer, repeating this pattern. + A list of integers: Defines a custom pattern where 1 means skip RoPE and 0 means apply RoPE. + For example, [0,1,1,0] means: apply RoPE, skip RoPE, skip RoPE, apply RoPE.""" + + moe_deepep_num_sms: int = 20 + """Number of SMs to use for DeepEP.""" + + moe_hybridep_num_sms: int = 16 + """Number of SMs to use for HybridEP. In pure NVL scenarios, + 16 SMs can generally achieve good bandwidth.""" + + #################### + # attention variant + #################### + experimental_attention_variant: Optional[str] = None + """Type of attention variant to use. Currently support gated_delta_net and dsa.""" + + #################### + # attention variant: gated_delta_net + #################### + linear_attention_freq: Optional[Union[int, List[int]]] = None + """Frequency between LA (linear attention) layers + and SDPA (scaled dot-product attention) layers. + Accepts either: + - An integer N: Represents a (N-1):N ratio, meaning (N-1) LA layers for every 1 SDPA layer + - A list that defines a custom pattern, e.g.: [1,1,1,0,1,1,1,0,1,1,1,0]""" + + linear_conv_kernel_dim: Optional[int] = None + """Conv kernel dimension for the gated delta net.""" + + linear_key_head_dim: Optional[int] = None + """Query and key head dimension for the gated delta net.""" + + linear_value_head_dim: Optional[int] = None + """Value and gate head dimension for the gated delta net.""" + + linear_num_key_heads: Optional[int] = None + """Number of query and key heads for the gated delta net.""" + + linear_num_value_heads: Optional[int] = None + """Number of value and gate heads for the gated delta net.""" + + #################### + # attention variant: dsa + #################### + dsa_indexer_n_heads: Optional[int] = None + """Number of DSA indexer heads.""" + + dsa_indexer_head_dim: Optional[int] = None + """Dimension per DSA indexer head.""" + + dsa_indexer_topk: Optional[int] = None + """Number of top-k tokens to select in DSA indexer.""" + + dsa_indexer_loss_coeff: Optional[float] = None + """Coefficient for the DSA indexer KL divergence loss. Set to 0 to disable indexer loss.""" + + dsa_indexer_use_sparse_loss: Optional[bool] = None + """Whether to use sparse DSA indexer loss. If True, the indexer loss will be computed using the + top-k indices.""" + + #################### + # initialization + #################### + init_method: Optional[Callable] = None + """Method to initialize weights. Note that bias is always set to zero. Should be a function that + takes a single Tensor and initializes it. If None, will be set to + megatron.core.utils.init_method_normal(init_method_std) which is torch nn init normal with + mean=0.0 and std=init_method_std.""" + + output_layer_init_method: Optional[Callable] = None + """Method to initialize weights of the output layer of both attention and MLP blocks. If None, + will be set to megatron.core.utils.scaled_init_method_normal(init_method_std) which is torch nn + init normal with mean=0.0 and std=init_method_std / math.sqrt(2.0 * num_layers).""" + + init_method_std: float = 0.02 + """Standard deviation of the zero mean normal for the default initialization method, not used if + init_method and output_layer_init_method are provided.""" + + embedding_init_method: Optional[Callable] = None + """ + Method to initialize weights of the embedding layer. If None, will be set as described + in init_method above. + """ + + embedding_init_method_std: Optional[float] = None + """ + Standard deviation of the zero mean normal for the default initialization method for the + embedding layer. If None, will be set to init_method_std. + """ + + init_model_with_meta_device: bool = False + """ + If True, initializes the model with the meta device. This is helpful for + training of very large models. This feature is only works when megatron fsdp is turned on. + """ + + #################### + # mixed-precision + #################### + apply_query_key_layer_scaling: bool = False + """If true, scale Q * K^T by 1 / layer-number. This improve numeric stability when training with + fp16.""" + + attention_softmax_in_fp32: bool = True + """If True, run attention masking and softmax in fp32. This should be True if + apply_query_key_layer_scaling is True.""" + + disable_bf16_reduced_precision_matmul: bool = False + """If True, sets torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction=False to + prevent matmul from using reduced precision accumulation when using BF16.""" + + #################### + # fusion + #################### + bias_activation_fusion: bool = False + """If True, fuses bias addition and the activation function when possible.""" + + masked_softmax_fusion: bool = False + """If True, uses softmax fusion.""" + + persist_layer_norm: bool = False + """If True, uses the persistent fused layer norm kernel. This kernel only supports a fixed set + of hidden sizes.""" + + memory_efficient_layer_norm: bool = False + """If True, and using local layers (not from TransformerEngine), tells Apex to use the memory + efficient fused LayerNorm kernel. Ignored if not using LayerNorm.""" + + bias_dropout_fusion: bool = False # TODO: this should be bias_dropout_add_fusion? + """If True, uses bias dropout fusion.""" + + apply_rope_fusion: bool = False + """If True, use fused RoPE kernel.""" + + use_fused_weighted_squared_relu: bool = False + """If True, uses fused weighted squared relu kernel when using MoE.""" + + fused_single_qkv_rope: bool = False + """If set, avoid splitting QKV before ROPE forward and avoid concatenating ROPE dgrads.""" + + #################### + # activation recomputation + #################### + recompute_granularity: Optional[str] = None + """Determines which type of activation recompute to use. Megatron-core supports 'selective' + activation checkpointing where the submodules set in --recompute-modules is checkpointed. + The default is "core_attn" which is the memory intensive part of attention. + These memory intensive activations are also less compute intensive which makes activation + checkpointing more efficient for LLMs (20B+). See Reducing Activation Recomputation in Large + Transformer Models (https://arxiv.org/abs/2205.05198) for more details. 'full' will checkpoint + the entire transformer layer. If None, no recompute is performed and all activations are saved. + If set, must be 'selective' or 'full'. 'selective' always uses all layers. + """ + + recompute_method: Optional[str] = None + """Determines which transformer layers will be recomputed. uniform will uniformly divide the + total number of transformer layers in a transformer block and recompute the input activation of + each divided chunk at the specified granularity. block will recompute the input activations for + only a set number of transformer layers per pipeline stage. The rest of the layers in the + pipeline stage will not have any activations recomputed. If None, and recompute is enabled, all + layers will do recomputation. If set, must be 'uniform' or 'block'.""" + + recompute_num_layers: Optional[int] = None + """When recompute_method is uniform, recompute_num_layers is the number of transformer layers in + each uniformly divided recompute unit. When recompute_method is block, recompute_num_layers is + the number of transformer layers to recompute within each pipeline stage. Must be None for + 'selective' activation checkpointing.""" + + distribute_saved_activations: Optional[bool] = None + """If True, distribute recomputed activations across the model parallel group.""" + + recompute_modules: Optional[List[str]] = None + """The submodules to recompute. + choices: "core_attn", "moe_act", "layernorm", "mla_up_proj", "mlp", "moe", "shared_experts". + default: ["core_attn"]. + "core_attn": recompute the core attention part of the transformer layer. + "moe_act": recompute the MoE MLP activation function. + "layernorm": recompute the input_layernorm and pre_mlp_layernorm. + "mla_up_proj": recompute the MLA up projection and RoPE applying parts. + "mlp": recompute the dense MLP submodule. + "moe": recompute the MoE layer. + "shared_experts": recompute the shared experts in the MoE layer. + "moe_act", "layernorm", and "mla_up_proj" use output-discarding checkpointing, + "core_attn", "mlp", "moe", and "shared_experts" use normal checkpointing. + """ + + #################### + # fp8 related + #################### + fp8: Optional[str] = None + """If set, enables the use of FP8 precision through Transformer Engine. There are 2 predefined + choices (1) 'e4m3' uniformly uses e4m3 for all FP8 tensors, (2) 'hybrid' uses e4m3 for all FP8 + activation and weight tensors and e5m2 for all FP8 output activation gradient tensors.""" + + fp8_recipe: Optional[str] = "delayed" + """If set, enables the use of FP8 precision through Transformer Engine. There are 5 predefined + choices (1) 'tensorwise' uses per tensor current scaling recipe, (2) 'delayed' + uses delayed scaling recipe, 3) 'mxfp8' for Blackwell architecture only, + 4) 'blockwise' for blockwise scaling recipe, 5) 'custom' for custom quantization recipe.""" + + fp8_param: bool = False + """If set, keep the parameters in fp8 precision to save memory. This option must be used + together with fp8 mode (i.e., TransformerConfig.fp8 is not None). Note that not all parameters + will be converted to fp8; for example, biases will remain unchanged. The parameters affected are + primarily the weights of GEMMs. The specific parameters that will be converted to fp8 are + determined by TE.""" + + fp8_quantizer_factory: Optional[str] = None + """Python import path to a callable quantizer factory, e.g., package.module.quantizer_factory. + Required when fp8_recipe is custom.""" + + fp8_margin: int = 0 + """Margin for the scaling factor computation.""" + + fp8_interval: int = 1 + """DEPRECATED from TransformerEngine v1.8.0. This flag is ignored. + Controls how often the scaling factor is recomputed. + """ + + fp8_amax_history_len: int = 1 + """The length of the amax history window used for scaling factor computation.""" + + fp8_amax_compute_algo: str = "most_recent" + """Algorithm used for choosing the `amax` value for the scaling factor computation. There are 2 + predefined choices: `max` chooses the largest `amax` in the history window, while `most_recent` + always chooses the most recently seen value. + + """ + + fp8_wgrad: bool = True + """When set to False, override FP8 config options and do the wgrad computation + in higher precision.""" + + fp8_dot_product_attention: bool = False + """When set to True, use the FP8 implementation of Dot Product Attention.""" + + fp8_multi_head_attention: bool = False + """When set to True, use the FP8 implementation of Multi Head Attention.""" + + tp_only_amax_red: bool = False + """When set to True, reduce the FP8 AMAX only in the TP or TP-CP domain""" + + first_last_layers_bf16: bool = False + """If True, retains first and last N TransformerBlocks in BF16 as opposed to FP8.""" + + num_layers_at_start_in_bf16: int = 1 + """Number of layers at the start of the model to keep in BF16 precision when + first_last_layers_bf16 is True.""" + + num_layers_at_end_in_bf16: int = 1 + """Number of layers at the end of the model to keep in BF16 precision when + first_last_layers_bf16 is True.""" + + use_kitchen: bool = False + """Use the kitchen extension for transformer quantization.""" + + #################### + # fp4 related + #################### + fp4: Optional[str] = None + """If set, enables the use of FP4 precision through Transformer Engine. Currently only + supports 'nvfp4' which uses NVFP4BlockScaling recipe (requires TE >= 2.7.0.dev0).""" + + fp4_recipe: Optional[str] = "nvfp4" + """If set, enables the use of FP4 precision through Transformer Engine. Currently only + 'nvfp4' is supported which uses NVFP4BlockScaling recipe for Blackwell+ architecture.""" + + fp4_param: bool = False + """If set, keep the parameters in fp4 precision to save memory. This option must be used + together with fp4 mode (i.e., TransformerConfig.fp4 is not None). Note that not all parameters + will be converted to fp4; for example, biases will remain unchanged.""" + + fp4_quantizer_factory: Optional[str] = None + """Python import path to a callable quantizer factory, e.g., package.module.quantizer_factory. + Required when fp4_recipe is custom.""" + + #################### + # MoE related + #################### + moe_shared_expert_intermediate_size: Optional[int] = None + """Shared expert total ffn hidden size. + It should be equal to 'num_shared_experts * ffn_size_of_each_shared_expert' if + there are multiple shared experts. + None means no shared expert. + By default, the shared experts execute before the router. However, when + moe_shared_expert_overlap or overlap_moe_expert_parallel_comm is set, + the shared experts execute after the router, before the routed experts. + This makes the gradients from the router and the shared experts added in + different orders to the hidden_states, causing minor numerical differences + in the hidden_states gradient.""" + + moe_shared_expert_gate: bool = False + """Enable gate for shared expert.""" + + moe_shared_expert_overlap: bool = False + """Enable overlapping between shared expert computations and dispatcher communications. + Without this, the shared experts execute before the router.""" + + moe_layer_freq: Union[int, List[int]] = 1 + """Frequency between MoE layers and Dense layers. Accepts either: + - An integer N: Represents a 1:N ratio, meaning one expert layer for every N-1 dense layers. + - A list that defines a custom pattern, e.g.: [1,1,1,0,1,1,1,0,1,1,1,0]""" + + moe_ffn_hidden_size: Optional[int] = None + """MoE Feed-Forward Network hidden size""" + + moe_router_load_balancing_type: Union[str, List[str]] = "aux_loss" + """The load balancing strategy for the router. + Options: + - "aux_loss": Load balancing loss used in GShard and SwitchTransformer, calculated at + micro-batch level. + - "seq_aux_loss": Load balancing loss used in DeepSeekV2 and DeepSeekV3, computes loss + for each individual sample. + - "global_aux_loss": Load balancing loss calculated at global batch level. + - "sinkhorn": Balancing algorithm used in S-BASE. + - "none": No load balancing. + A list of strings can be provided to combine multiple aux-loss load balancing types. + The default is "aux_loss". + """ + + moe_router_topk: int = 2 + """Number of experts to route to for each token.""" + + moe_router_topk_limited_devices: Optional[int] = None + """Number of EP ranks to consider for each token in group-limited routing, + DEPRECATED and replaced by moe_router_num_groups and moe_router_group_topk. + """ + + moe_router_padding_for_quantization: Optional[bool] = False + """Whether to pad the routing_map to make sure the number of tokens each expert receives + is a multiple of 16/32 for quantized precision (e.g., FP8, FP4). This can remove the explicit + padding in the GroupedMLP layer.""" + + moe_router_padding_for_fp8: Optional[bool] = False + """[Compatibility alias for moe_router_padding_for_quantization] + Enabling this will also enable moe_router_padding_for_quantization.""" + + moe_router_num_groups: Optional[int] = None + """Number of groups to divide experts into for group-limited routing. + When using group-limited routing: + 1. Experts are divided into 'moe_router_num_groups' equal-sized groups + 2. For each token, 'moe_router_group_topk' groups are selected based on sum of + top-('moe_router_topk'/'moe_router_group_topk') routing scores within each group + 3. From these selected groups, 'moe_router_topk' individual experts are chosen + Two common use cases: + - Device-limited routing: Set 'moe_router_num_groups' equal to expert parallel size (EP) + to limit each token to experts on a subset of devices + (See DeepSeek-V2: https://arxiv.org/pdf/2405.04434) + - Node-limited routing: Set 'moe_router_num_groups' equal to number of nodes in EP group + to limit each token to experts on a subset of nodes + (See DeepSeek-V3: https://arxiv.org/pdf/2412.19437) + """ + + moe_router_group_topk: Optional[int] = None + """Number of selected groups for group-limited routing.""" + + moe_router_pre_softmax: bool = False + """Enable pre-softmax(pre-sigmoid) routing for MoE, which means softmax is before the + top-k selection. + By default, softmax is done after top-k.""" + + moe_router_topk_scaling_factor: Optional[float] = None + """Scaling factor for routing score in top-k selection, only works when moe_router_pre_softmax + enabled. Defaults to None, which means no scaling.""" + + moe_router_score_function: str = "softmax" + """Score function for MoE routing. Can be "softmax" or "sigmoid".""" + + moe_router_dtype: Optional[str] = None + """Data type for routing and expert output weighted averaging. Using fp32 or fp64 can + improve stability especially when the number of experts is large (e.g. finegrained-moe). + None means no changes for dtype.""" + + moe_router_enable_expert_bias: bool = False + """TopK routing with dynamic per-expert bias in the aux-loss-free load balancing strategy. + The routing decision is based on the sum of the routing scores and the expert bias. + See https://arxiv.org/abs/2408.15664 for details.""" + + moe_router_bias_update_rate: float = 1e-3 + """The expert bias is updated based on the number of assigned tokens to each expert + in a global batch, where the bias is increased for the experts with less assigned tokens + and decreased for the experts with more assigned tokens. + The default value 1e-3 is same as that used in DeepSeekV3.""" + + moe_router_force_load_balancing: bool = False + """[Experimental] Force load balancing with random logits for MoE router, supports naive topk + and group-limited topk. This is an experimental feature and only for benchmark.""" + + moe_grouped_gemm: bool = False + """When there are multiple experts per rank, compress multiple local (potentially small) gemms + in a single kernel launch to improve the utilization and performance by leveraging the Grouped + GEMM feature introduced since CUTLASS 2.8 (https://github.com/fanshiqing/grouped_gemm). + """ + + moe_use_legacy_grouped_gemm: bool = False + """Use legacy GroupedMLP rather than TEGroupedMLP. + Note: The legacy one will be deprecated soon.""" + + moe_aux_loss_coeff: Union[float, List[float]] = 0.0 + """Scaling coefficient for the aux loss. A starting value of 1e-2 is recommended. + If a list of load balancing types is provided for `moe_router_load_balancing_type`, + a corresponding list of coefficients should be provided here.""" + + moe_z_loss_coeff: Optional[float] = None # 1e-3 would be a good start value for z-loss + """Scaling coefficient for the z-loss. A starting value of 1e-3 is recommended.""" + + moe_input_jitter_eps: Optional[float] = None + """Add noise to the input tensor by applying jitter with a specified epsilon value.""" + + moe_token_dropping: bool = False + """This feature involves selectively dropping and padding tokens for each expert to achieve a + specified capacity, similar to GShard, Switch-Transformer, and DeepSpeed-MoE. Note that this is + currently unsupported so should remain False.""" + + moe_token_dispatcher_type: str = "allgather" + """The type of token dispatcher to use. The default is 'allgather'. + Options are 'allgather','alltoall' and 'flex'.""" + + moe_enable_deepep: bool = False + """[Experimental] Enable DeepEP for efficient token dispatching and combine in MoE models.""" + + moe_flex_dispatcher_backend: str = "deepep" + """[Experimental] The backend to use for flex token dispatcher. The default is "deepep". + Options are "deepep" and "hybridep". Currently only "hybridep" backend supports + the MNNVL case.""" + + moe_per_layer_logging: bool = False + """Enable per-layer logging for MoE, currently supports auxiliary loss and z loss.""" + + moe_expert_capacity_factor: Optional[float] = None + """moe_expert_capacity_factor (float): The capacity factor for each expert, None means no token + will be dropped. The default is None.""" + + moe_pad_expert_input_to_capacity: bool = False + """moe_pad_expert_input_to_capacity (bool): If True, pads the input for each expert to match + the expert capacity length, effective only after the moe_expert_capacity_factor is set. The + default setting is False.""" + + moe_token_drop_policy: str = "probs" + """The policy to drop tokens. Can be either "probs" or "position". If "probs", the tokens with + the lowest probabilities will be dropped. If "position", tokens at the end of each batch will + be dropped. + """ + + moe_layer_recompute: bool = False + """Memory optimization: checkpointing moe_layer to save actiavtion memory.""" + + moe_permute_fusion: bool = False + """Fuse token rearrangement ops during token dispatching.""" + + moe_router_fusion: bool = False + """Fuse ops in routing and aux loss calculation.""" + + moe_apply_probs_on_input: bool = False + """Apply probs on input of experts instead of applying after activation and glu.""" + + ################## + # Context Parallel + ################## + cp_comm_type: Optional[Union[str, List[str]]] = None + """Inter-gpu communication type for context parallelism. + str: all layers share same communication type. + List[str]: each layer has its separate communication type. + cp_comm_type of each layer can be "p2p" or "all_gather" or "a2a" or "a2a+p2p". + "p2p": Exchange KV chunks with P2P communications in ring topology. P2P is async and can be + overlapped with attention compute. + "all_gather": All-gather to get full sequence of KV before attention. The all-gather is not + async, and cannot be overlapped. + "a2a": Like DeepSpeed Ulysses, scatter attention heads across the CP group, and gather to get + full sequence of QKV. + "a2a+p2p": A hierarchical implementation of context parallelism to attention. + It uses A2A communications in low-level CP groups (e.g., via NVLink), + and P2P communications in high-level CP groups (e.g., via IBLink). + """ + + ################## + # Cuda Graphs + ################## + enable_cuda_graph: bool = False + """DEPRECATED and replaced by cuda_graph_impl. + When set to true, either partial CUDA graph (1/many CUDA graph per layer) or full iteration + CUDA graph (1 CUDA graph for whole iteration excluding optimizer) is enabled. --cuda-graph-scope + determines the scope of graph capture.""" + + cuda_graph_use_single_mempool: bool = False + """When set to true, cudagraphs will be captured inside a single mempool, in which all + cudagraphs may only be used once per step. If false, cudagraphs may be reused across + microbatches. Enabling may reduce cudagraph memory overheads due to memory fragmentation, + however may greatly increase the number of cudagraphs created when the number of microbatches + is high.""" + + cuda_graph_retain_backward_graph: bool = False + """When set to true, cudagraph backward passes will be graph captured with 'retain_grad=True' + This may enable cudagraphs for certain modules that are not completely cudagraph safe. For + more details, see: https://pytorch.org/docs/stable/generated/torch.Tensor.backward.html.""" + + cuda_graph_warmup_steps: int = 3 + """Number of warmup steps for CUDA graphs""" + + external_cuda_graph: bool = False + """DEPRECATED and replaced by cuda_graph_impl. + When set to true, TransformerLayer layers are swapped with user provided CUDA graphs.""" + + cuda_graph_impl: str = "none" + """Determines the CUDA graph capture implementation. + "none": no CUDA graph. + "local": capture the CUDA graph using MCore local implementation. Either partial CUDA graph + (1/many CUDA graph per layer) or full iteration CUDA graph (1 CUDA graph for whole iteration + excluding optimizer) is enabled. + "transformer_engine": capture the CUDA graph using TE make_graphed_callables().""" + + cuda_graph_scope: Optional[List[CudaGraphScope]] = None + """Determines the CUDA graphs capturing scope. + When cuda_graph_impl is set to "transformer_engine", valid values are "attn", "mlp", "moe", + "moe_router", "moe_preprocess", "mamba". None means the full layer. + When cuda_graph_impl is set to "local", "full_iteration" can be specified as cuda_graph_scope + to enable whole iteration CUDA graph. All other values enable layerwise CUDA graph.""" + + #################### + # miscellaneous + #################### + clone_scatter_output_in_embedding: bool = True + """When set to True, clone the output of scatter_to_sequence_parallel_region in embedding layer + to facilitate garbage collection of input.""" + + disable_parameter_transpose_cache: bool = False + """When set to true, the parameter transposes are not cached for subsequent iterations.""" + + config_logger_dir: str = "" + """When non-empty, dumps entry-point configs to config_logger_dir""" + + flash_decode: bool = False + """ Use the optimized flash decoding kernel during inference. """ + + use_te_activation_func: bool = False + """Whether to use ffn activation functions implemented by TransformerEngine""" + + use_te_rng_tracker: bool = False + """ Whether to use the TE or MCore version of the RNG tracker. """ + + inference_rng_tracker: bool = False + """ Whether we should instantiate a separate RNG tracker for inference. """ + + inference_sampling_seed: int = 42 + """ Random seed to use for sampling during inference. """ + + symmetric_ar_type: Optional[str] = None + """Type of symmetric all reduce to use""" + + mrope_section: Optional[List[int]] = None + """ Multimodal rope section is for channel dimension of temporal, height and width + in rope calculation. """ + + is_hybrid_model: bool = False + """ Indicates whether this is a hybrid model. """ + + mamba_state_dim: int = 128 + """The dimensionality of the state representation in Mamba layers.""" + + mamba_head_dim: int = 64 + """The dimensionality of the heads in the Mamba layers.""" + + mamba_num_groups: int = 8 + """The number of groups used in Mamba layers.""" + + mamba_num_heads: Optional[int] = None + """The number of heads used in Mamba layers. + If None, the number of heads will be hidden_size * expand // mamba_head_dim.""" + + use_mamba_mem_eff_path: bool = True + """If True, use the memory efficient path for Mamba layers.""" + + mlp_chunks_for_prefill: int = 1 + """The number of chunks along the sequence dimension to use for MLP computation + during prefill.""" + + heterogeneous_block_specs: bool = False + """Whether to use heterogeneous block specs (nemotron-nas architecture).""" + + hetereogenous_dist_checkpoint: bool = False + """Whether to use heterogenous layers in distributed checkpoint.""" + + #################### + # Quantization + #################### + quant_recipe: Optional[RecipeConfig] = None + """Configuration of any quantization to be applied to the model""" + + transformer_impl: str = "transformer_engine" + """Transformer implementation to use. + Options are 'transformer_engine' for Transformer Engine and 'local' for MCore.""" + + fallback_to_eager_attn: bool = False + """Whether to fallback to eager attention in TE implementation. + Suggested for when desired features are not available in TE implementation.""" + + ##################################### + # Fine-grained Activation Offloading + ##################################### + fine_grained_activation_offloading: bool = False + """If True, offload the input of the specified modules to the CPU. + Fine-grained activation offloading is a module-level offloading method + instead of a layer-level offloading method like cpu_offloading.""" + + offload_modules: Optional[list[str]] = None + """The submodules to offload its input. + choices: "attn_norm", "qkv_linear", "core_attn", "attn_proj", + "mlp_norm", "expert_fc1", "moe_act". + "attn_norm": offload the input of the normalization in the attention part. + "qkv_linear": offload the input of the qkv linear part. + "core_attn": offload the input of the core attention part. + "attn_proj": offload the input of the attn linear projection part. + "mlp_norm": offload the input of the normalization in the mlp part. + "expert_fc1": offload the input of the expert fc1 part. + "moe_act": offload the input of the moe act part. + """ + min_offloaded_tensor_size: int = 1024 * 1024 + """The minimum size of the tensor to be offloaded.""" + + def __post_init__(self): + """Python dataclass method that is used to modify attributes after initialization. + See https://docs.python.org/3/library/dataclasses.html#post-init-processing for more + details. + """ + super().__post_init__() + if self.fp16 and self.bf16: + raise ValueError( + f"Only one of self.fp16: {self.fp16} and self.bf16 {self.bf16} should be True." + ) + + # Apply BF16 matmul precision setting if needed + if self.bf16 and self.disable_bf16_reduced_precision_matmul: + torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = False + + if self.num_attention_heads % self.tensor_model_parallel_size != 0: + raise ValueError( + f"num_attention_heads ({self.num_attention_heads}) must be a multiple of " + f"tensor_model_parallel_size ({self.tensor_model_parallel_size})." + ) + + if self.ffn_hidden_size is None: + self.ffn_hidden_size = 4 * self.hidden_size + + if self.kv_channels is None: + self.kv_channels = self.hidden_size // self.num_attention_heads + + if self.num_query_groups is None: + self.num_query_groups = self.num_attention_heads + + if self.num_query_groups % self.tensor_model_parallel_size != 0: + raise ValueError( + f"num_query_groups ({self.num_query_groups}) must be a multiple of " + f"tensor_model_parallel_size ({self.tensor_model_parallel_size})." + ) + + if self.experimental_attention_variant in ["gated_delta_net"]: + assert ( + self.linear_attention_freq is not None + ), f"linear_attention_freq must be set for linear attention." + + if self.experimental_attention_variant == "gated_delta_net": + # Check required parameters + assert ( + self.linear_conv_kernel_dim is not None + ), "linear_conv_kernel_dim must be set for gated delta net." + assert ( + self.linear_key_head_dim is not None + ), "linear_key_head_dim must be set for gated delta net." + assert ( + self.linear_value_head_dim is not None + ), "linear_value_head_dim must be set for gated delta net." + assert ( + self.linear_num_key_heads is not None + ), "linear_num_key_heads must be set for gated delta net." + assert ( + self.linear_num_value_heads is not None + ), "linear_num_value_heads must be set for gated delta net." + assert self.linear_num_value_heads % self.linear_num_key_heads == 0, ( + f"linear_num_value_heads ({self.linear_num_value_heads}) must be a multiple of " + f"linear_num_key_heads ({self.linear_num_key_heads})." + ) + + # Check tensor parallelism compatibility + assert ( + self.linear_num_key_heads % self.tensor_model_parallel_size == 0 + ), "linear_num_key_heads must be a multiple of tensor_model_parallel_size." + assert ( + self.linear_num_value_heads % self.tensor_model_parallel_size == 0 + ), "linear_num_value_heads must be a multiple of tensor_model_parallel_size." + + # Do not support yet, but coming soon. + assert self.context_parallel_size == 1, ( + f"Gated delta net does not support context parallel for now," + f" but got {self.context_parallel_size=}." + ) + elif self.experimental_attention_variant == "dsa": + # assert ( + # self.context_parallel_size == 1 + # ), "Currently context parallelism is not supported by DSAttention!" + assert not self.apply_rope_fusion, "RoPE fusion is not supported for DSAttention" + + if self.fp8: + # cannot support first last layer bf16 with delayed scaling + if self.first_last_layers_bf16 and self.fp8_recipe == Fp8Recipe.delayed: + raise ValueError("Delayed scaling does not support first / last layer in BF16.") + + # max bf16 layers per pipeline stage + max_bf16_layers_per_pipeline_stage = ( + self.num_layers // self.pipeline_model_parallel_size + ) + + # check start/end bf16 layer counts are valid + if self.first_last_layers_bf16: + if ( + self.num_layers_at_start_in_bf16 < 0 + or self.num_layers_at_start_in_bf16 > max_bf16_layers_per_pipeline_stage + ): + raise ValueError( + f"num_layers_at_start_in_bf16 ({self.num_layers_at_start_in_bf16}) must be " + f"between 0 and number of layers per pipeline stage " + f"({max_bf16_layers_per_pipeline_stage})." + ) + if ( + self.num_layers_at_end_in_bf16 < 0 + or self.num_layers_at_end_in_bf16 > max_bf16_layers_per_pipeline_stage + ): + raise ValueError( + f"num_layers_at_end_in_bf16 ({self.num_layers_at_end_in_bf16}) must be " + f"between 0 and number of layers per pipeline stage " + f"({max_bf16_layers_per_pipeline_stage})." + ) + + if self.fp8_recipe == Fp8Recipe.custom: + if not self.fp8_quantizer_factory: + raise ValueError( + "fp8_quantizer_factory must be provided when fp8_recipe is 'custom'. " + "Specify a Python import path (e.g., package.module.quantizer_factory) " + "via --fp8-quantizer-factory." + ) + + if self.fp8_param and not self.fp8: + raise ValueError("fp8_param must be used together with fp8 mode.") + + # FP4 validation + if self.fp4_param and not self.fp4: + raise ValueError("fp4_param must be used together with fp4 mode.") + + if self.fp4 and self.fp8: + raise ValueError("fp4 and fp8 cannot be used simultaneously. Please choose one.") + + if self.fp4 and self.fp4_recipe == Fp4Recipe.custom: + if not self.fp4_quantizer_factory: + raise ValueError( + "fp4_quantizer_factory must be provided when fp4_recipe is 'custom'. " + "Specify a Python import path (e.g., package.module.quantizer_factory) " + "via --fp4-quantizer-factory." + ) + + if self.apply_query_key_layer_scaling: + self.attention_softmax_in_fp32 = True + + if self.expert_model_parallel_size > 1 and self.num_moe_experts is None: + raise ValueError("num_moe_experts must be non None to use expert-parallel.") + + if self.num_moe_experts is not None and self.num_moe_experts <= 0: + raise ValueError("num_moe_experts must be non-negative.") + + if self.num_moe_experts is not None and self.moe_ffn_hidden_size is None: + self.moe_ffn_hidden_size = self.ffn_hidden_size + warnings.warn("moe_ffn_hidden_size is not set, using ffn_hidden_size instead.") + + if self.num_moe_experts is None: + assert ( + self.moe_ffn_hidden_size is None + ), "moe_ffn_hidden_size must be None when num_experts is not set." + + if self.moe_enable_deepep: + if self.moe_token_dispatcher_type != "flex": + raise ValueError("DeepEP backend is only supported with flex token dispatcher.") + if self.moe_flex_dispatcher_backend == "hybridep": + raise ValueError("Only one backend is supported for flex token dispatcher.") + self.moe_flex_dispatcher_backend = "deepep" + warnings.warn( + "moe_enable_deepep is deprecated." + "Please use --moe-flex-dispatcher-backend=deepep instead." + ) + + if self.moe_token_dispatcher_type == "flex": + if self.moe_pad_expert_input_to_capacity and ( + self.moe_enable_deepep or self.moe_flex_dispatcher_backend == "deepep" + ): + raise ValueError( + "Flex token dispatcher with deepep backend does not support " + "moe_pad_expert_input_to_capacity" + ) + + if self.moe_shared_expert_intermediate_size is not None: + if self.moe_shared_expert_intermediate_size <= 0: + raise ValueError( + f"moe_shared_expert_intermediate_size must be " + f"num_shared_experts * ffn_size_of_each_shared_expert, " + f"but got {self.moe_shared_expert_intermediate_size}" + ) + if self.moe_shared_expert_overlap and self.moe_token_dispatcher_type not in [ + "alltoall" + ]: + raise ValueError( + f"moe_shared_expert_overlap only works with alltoall token dispatcher." + ) + + if isinstance(self.moe_router_load_balancing_type, list): + assert isinstance(self.moe_aux_loss_coeff, list) and len( + self.moe_aux_loss_coeff + ) == len(self.moe_router_load_balancing_type), ( + "moe_aux_loss_coeff must be a list of the same length as " + "moe_router_load_balancing_type" + ) + + if self.moe_expert_capacity_factor is not None: + if self.moe_expert_capacity_factor < 0: + self.moe_expert_capacity_factor = None + if isinstance(self.moe_router_load_balancing_type, list): + for load_balancing_type in self.moe_router_load_balancing_type: + if load_balancing_type not in [ + "aux_loss", + "seq_aux_loss", + "global_aux_loss", + "none", + ]: + raise ValueError( + "moe_expert_capacity_factor only works with aux_loss, " + "seq_aux_loss, global_aux_loss or none load balancing" + ) + elif self.moe_router_load_balancing_type not in [ + "aux_loss", + "seq_aux_loss", + "global_aux_loss", + "none", + ]: + raise ValueError( + "moe_expert_capacity_factor only works with aux_loss, " + "seq_aux_loss, global_aux_loss or none load balancing" + ) + + if self.moe_pad_expert_input_to_capacity: + if self.moe_expert_capacity_factor is None: + raise ValueError( + "moe_expert_capacity_factor must be set to use moe_pad_expert_input_to_capacity" + ) + + if self.cpu_offloading and ( + self.cpu_offloading_num_layers < 0 or self.cpu_offloading_num_layers >= self.num_layers + ): + raise ValueError( + f"CPU offloading can be done only for layers less than {self.num_layers}" + ) + + if self.cpu_offloading and self.pipeline_model_parallel_size > 1: + raise ValueError( + "Currently there is no support for Pipeline parallelism with CPU offloading" + ) + + if self.cpu_offloading and self.recompute_granularity is not None: + raise ValueError( + "CPU offloading does not work when activation recomputation is enabled" + ) + + if self.recompute_granularity is not None: + if self.recompute_granularity not in ["full", "selective"]: + raise ValueError( + f'When using recompute_granuarlity: {self.recompute_granularity} must be "full"' + 'or "selective".' + ) + + if self.recompute_method is not None: + if self.recompute_method not in ["block", "uniform"]: + raise ValueError( + f'recompute_method: {self.recompute_method} must be "block" or "uniform".' + ) + elif self.recompute_granularity != "selective": + raise ValueError( + f"Using recompute_granularity: {self.recompute_granularity} so " + 'recompute_method must be "block" or "uniform"' + ) + + if self.recompute_granularity != "selective" and self.recompute_num_layers is None: + raise ValueError( + f"When using recompute_granularity: {self.recompute_granularity} " + "recompute_num_layers must be between " + "1 and num_layers_per_pipeline_rank: " + f"{self.num_layers // self.pipeline_model_parallel_size}" + ) + elif ( + self.recompute_granularity == "selective" and self.recompute_num_layers is not None + ): + raise ValueError( + f"When using recompute_granularity: {self.recompute_granularity} " + "recompute_num_layers must be None." + ) + + if self.distribute_saved_activations and self.sequence_parallel: + raise ValueError( + f"distribute_saved_activations: {self.distribute_saved_activations} must be " + f"false when sequence parallel is enabled: {self.sequence_parallel}" + ) + + if self.recompute_modules is None: + self.recompute_modules = ["core_attn"] + + if self.recompute_granularity == "selective": + if len(self.recompute_modules) > 0: + allowed_modules = { + "core_attn", + "moe_act", + "layernorm", + "mla_up_proj", + "mlp", + "moe", + "shared_experts", + } + invalid_modules = set(self.recompute_modules) - allowed_modules + assert not invalid_modules, ( + f"Invalid choices for recompute_modules: {invalid_modules}. " + f"Allowed modules are: {allowed_modules}" + ) + + if "moe_act" in self.recompute_modules and not self.moe_grouped_gemm: + raise ValueError( + "moe_act in recompute_modules is only supported with moe_grouped_gemm." + ) + + if "mla_up_proj" in self.recompute_modules and not self.multi_latent_attention: + raise ValueError( + "mla_up_proj in recompute_modules is only supported with " + "multi_latent_attention." + ) + + if "core_attn" in self.recompute_modules: + warnings.warn( + "If you are using transformer_engine as the transformer implementation, " + "the core_attn is from transformer_engine and may be the fused version. " + "For fused attention, you have no need to set 'core_attn' to recompute. " + "Please check that the core_attn recompute is really needed." + ) + + if "shared_experts" in self.recompute_modules: + if ( + self.moe_shared_expert_intermediate_size is not None + and self.moe_shared_expert_overlap + ): + raise ValueError( + "shared_experts recompute cannot work with --moe-shared-expert-overlap." + ) + + if self.fp8: + if "moe_act" in self.recompute_modules or "layernorm" in self.recompute_modules: + if self.fp8_recipe == 'delayed': + raise ValueError( + "Delayed scaling does not support moe_act and layernorm recompute " + "for fp8." + ) + if not is_te_min_version("2.6.0dev0"): + raise ValueError( + "moe_act and layernorm recompute for fp8 needs " + "transformer-engine>=2.6.0dev0, " + f"but your version is {get_te_version()}." + ) + + if self.moe_layer_recompute: + warnings.warn( + "--moe-layer-recompute is deprecated. " + "Use --recompute-granularity selective --recompute-modules moe_layer instead." + ) + if self.recompute_granularity == "full": + raise ValueError( + "Do not set --moe-layer-recompute with full recompute granularity. " + ) + self.recompute_granularity = "selective" + if "moe" not in self.recompute_modules: + self.recompute_modules.append("moe") + + if self.fine_grained_activation_offloading: + assert ( + not self.cpu_offloading + ), "fine_grained_activation_offloading cannot be enabled with cpu_offloading." + assert self.offload_modules is not None and len(self.offload_modules) > 0 + allowed_modules = { + "core_attn", + "attn_proj", + "expert_fc1", + "moe_act", + "attn_norm", + "mlp_norm", + "qkv_linear", + } + invalid_modules = set(self.offload_modules) - allowed_modules + assert not invalid_modules, ( + f'Invalid choices for offload_modules: {invalid_modules}. ' + f'Allowed modules are: {allowed_modules}' + ) + if "attn_proj" in self.offload_modules and "core_attn" not in self.offload_modules: + raise ValueError( + "attn_proj cannot be set to offload_modules alone without core_attn " + "because the input of attn_proj is the output of core_attn, " + "which is needed in core_attn.backward()." + ) + + if ( + self.num_layers_in_first_pipeline_stage is not None + or self.num_layers_in_last_pipeline_stage is not None + ) and ( + self.account_for_embedding_in_pipeline_split or self.account_for_loss_in_pipeline_split + ): + raise ValueError( + "num_layers_in_first_pipeline_stage and num_layers_in_last_pipeline_stage cannot be" + "set at the same time with account_for_embedding_in_pipeline_split" + "and account_for_loss_in_pipeline_split" + ) + + # PP layout + if self.pipeline_model_parallel_layout is not None: + # If pipeline layout is set, we will check the conflicts + # with other pipeline layout arguments. + any_conflict = ( + self.num_layers_in_first_pipeline_stage is not None + or self.num_layers_in_last_pipeline_stage is not None + or self.account_for_embedding_in_pipeline_split + or self.account_for_loss_in_pipeline_split + ) + if any_conflict: + raise ValueError( + "pipeline_model_parallel_layout cannot be set" + " with other pipeline layout arguments." + f" {self.num_layers_in_first_pipeline_stage=}," + f" {self.num_layers_in_last_pipeline_stage=}," + f" {self.account_for_embedding_in_pipeline_split=}," + f" {self.account_for_loss_in_pipeline_split=}." + ) + + # Transfer pipeline_model_parallel_layout from str or list to + # PipelineParallelLayerLayout + if isinstance(self.pipeline_model_parallel_layout, str): + self.pipeline_model_parallel_layout = PipelineParallelLayerLayout.from_str( + layout=self.pipeline_model_parallel_layout, + pipeline_model_parallel_size=self.pipeline_model_parallel_size, + ) + elif isinstance(self.pipeline_model_parallel_layout, list): + # Since list is not hashable, the initialization will not be cached. + self.pipeline_model_parallel_layout = PipelineParallelLayerLayout( + layout=self.pipeline_model_parallel_layout, + pipeline_model_parallel_size=self.pipeline_model_parallel_size, + ) + + # Check whether the input VPP size conflicts with the PP layout + detected_vpp_size = ( + self.pipeline_model_parallel_layout.virtual_pipeline_model_parallel_size + ) + if self.virtual_pipeline_model_parallel_size is not None: + assert self.virtual_pipeline_model_parallel_size == detected_vpp_size, ( + f"virtual_pipeline_model_parallel_size conflicts with" + f" pipeline_model_parallel_layout," + f" ({self.virtual_pipeline_model_parallel_size=}, " + f" {detected_vpp_size=})" + ) + elif detected_vpp_size > 1: + self.virtual_pipeline_model_parallel_size = detected_vpp_size + + # Check whether the layout is valid. + self.mtp_standalone = self.pipeline_model_parallel_layout.validate_layer_layout( + num_layers=self.num_layers, mtp_num_layers=self.mtp_num_layers + ) + + # Uneven PP + elif ( + self.num_layers_in_first_pipeline_stage is not None + or self.num_layers_in_last_pipeline_stage is not None + ): + pipeline_parallel_size = self.pipeline_model_parallel_size + num_layers = self.num_layers + + if self.num_layers_in_first_pipeline_stage is not None: + if self.num_layers_in_first_pipeline_stage <= 0: + raise ValueError("num_layers_in_first_pipeline_stage must be larger than 0") + + if self.virtual_pipeline_model_parallel_size is not None: + if ( + self.num_layers_in_first_pipeline_stage + % self.virtual_pipeline_model_parallel_size + != 0 + ): + raise ValueError( + f"number of layers at first stage: " + f"{self.num_layers_in_first_pipeline_stage}" + f"must be divisible by virtual pipeline" + f"parallel degree {self.virtual_pipeline_model_parallel_size}" + ) + num_layers -= self.num_layers_in_first_pipeline_stage + pipeline_parallel_size -= 1 + + if self.num_layers_in_last_pipeline_stage is not None: + if self.num_layers_in_last_pipeline_stage <= 0: + raise ValueError("num_layers_in_last_pipeline_stage must be larger than 0") + + if self.virtual_pipeline_model_parallel_size is not None: + if ( + self.num_layers_in_last_pipeline_stage + % self.virtual_pipeline_model_parallel_size + != 0 + ): + raise ValueError( + f"number of layers at last stage: " + f"{self.num_layers_in_last_pipeline_stage}" + f"must be divisible by virtual pipeline" + f"parallel degree {self.virtual_pipeline_model_parallel_size}" + ) + num_layers -= self.num_layers_in_last_pipeline_stage + pipeline_parallel_size -= 1 + + # Here pipeline_parallel_size is the number of middle PP stages. If there are middle + # PP stages, check number of layers at middle stage is divisible by middle PP size. + if pipeline_parallel_size and not num_layers % pipeline_parallel_size == 0: + raise ValueError( + f"number of layers at middle stage: {num_layers} must be divisible by" + f"the middle pipeline model parallel size {pipeline_parallel_size}" + ) + + # If there are middle PP stages, check number of layers + # on each middle PP rank is divisible by VPP size. + if pipeline_parallel_size and self.virtual_pipeline_model_parallel_size is not None: + num_layers_per_middle_pipeline_rank = num_layers // pipeline_parallel_size + if ( + not num_layers_per_middle_pipeline_rank + % self.virtual_pipeline_model_parallel_size + == 0 + ): + raise ValueError( + f"number of layers on each middle pipeline rank:" + f"{num_layers_per_middle_pipeline_rank} must be divisible by virtual" + f"pipeline parallel degree {self.virtual_pipeline_model_parallel_size}" + ) + + elif ( + self.account_for_embedding_in_pipeline_split or self.account_for_loss_in_pipeline_split + ): + if self.virtual_pipeline_model_parallel_size is None: + num_layers = self.num_layers + + if self.account_for_embedding_in_pipeline_split: + num_layers += 1 + + if self.account_for_loss_in_pipeline_split: + num_layers += 1 + + if not num_layers % self.pipeline_model_parallel_size == 0: + raise ValueError( + f"number of middle layers: {num_layers} must be divisible by " + f"middle pipeline_model_parallel_size {self.pipeline_model_parallel_size}" + ) + else: + num_layers = self.num_layers + if self.account_for_embedding_in_pipeline_split: + num_layers += 1 + + if self.account_for_loss_in_pipeline_split: + num_layers += 1 + + if not num_layers % self.pipeline_model_parallel_size == 0: + raise ValueError( + f"num_layers: {num_layers} after enable" + f"account_for_embedding_in_pipeline_split or " + f"account_for_loss_in_pipeline_split must be divisible" + f"by pipeline_model_parallel_size " + f"{self.pipeline_model_parallel_size}" + ) + + num_layers_per_pipeline_rank = num_layers // self.pipeline_model_parallel_size + if ( + not num_layers_per_pipeline_rank % self.virtual_pipeline_model_parallel_size + == 0 + ): + raise ValueError( + f"number of layers on each pipeline rank: {num_layers_per_pipeline_rank}" + f"(after enable account_for_embedding_in_pipeline_split or " + f"account_for_loss_in_pipeline_split) must be divisible by" + f"virtual_pipeline_model_parallel_size" + f"{self.virtual_pipeline_model_parallel_size}" + ) + + if self.apply_query_key_layer_scaling: + self.attention_softmax_in_fp32 = True + + if self.bias_activation_fusion: + if self.activation_func not in [F.gelu, F.silu, quick_gelu]: + raise ValueError( + "When bias_activation_fusion is True, activation function should be either " + "gelu, swiglu, or quick_geglu" + ) + if ( + self.activation_func == F.gelu + and not self.gated_linear_unit + and not self.add_bias_linear + ): + raise ValueError( + "When bias_activation_fusion is True, gated_linear_unit is False " + "and activation function is gelu, add_bias_linear must also be True." + ) + if self.activation_func == quick_gelu and not self.gated_linear_unit: + raise ValueError( + "When bias_activation_fusion is True and activation function is quick_gelu, " + "gated_linear_unit must be True." + ) + if self.glu_linear_offset != 0.0 and self.activation_func != quick_gelu: + raise ValueError( + "When bias_activation_fusion is True and glu_linear_offset is non-zero, " + "activation function must be quick_gelu." + ) + + if self.use_te_activation_func: + raise ValueError( + "bias_activation_fusion and use_te_activation_func cannot be both true. " + "If you use bias in MLP FC1, we recommend setting bias_activation_fusion " + "to True and use_te_activation_func to False." + ) + + if self.use_te_activation_func: + if self.activation_func not in (F.gelu, F.silu, F.relu): + raise ValueError( + "TransformerEngine only support gelu, geglu, silu, swiglu, relu, reglu. " + "If you don't want to use TransformerEngine activation function, set " + "use_te_activation_func to False" + ) + + if self.activation_func_fp8_input_store: + if self.activation_func != F.silu or not self.gated_linear_unit: + raise ValueError("Storing activation input in FP8 is supported only for SwiGLU.") + + if self.apply_rope_fusion: + if self.multi_latent_attention: + warnings.warn( + "apply_rope_fusion for multi-latent attention only supports training. " + "It is experimental and may change in future versions." + ) + else: + if self.rotary_interleaved: + if not is_te_min_version("2.3.0"): + raise ValueError( + "rotary_interleaved does not work with apply_rope_fusion for " + "TE < 2.3.0. Please install TE >= 2.3.0" + ) + + from megatron.core.models.common.embeddings.rope_utils import ( + fused_apply_rotary_pos_emb, + fused_apply_rotary_pos_emb_thd, + ) + + if fused_apply_rotary_pos_emb is None and fused_apply_rotary_pos_emb_thd is None: + raise ValueError( + "apply_rope_fusion is not available. Please install TE >= 1.4." + ) + + if self.fused_single_qkv_rope: + if self.attention_output_gate: + raise ValueError("fused_single_qkv_rope does not support gated attention for now.") + + if self.multi_latent_attention and self.rotary_interleaved: + raise ValueError("rotary_interleaved does not work with multi_latent_attention.") + + # Set the embedding init method + if self.embedding_init_method_std is None: + # By default, use the same init std as you use for every other non-output layer. + self.embedding_init_method_std = self.init_method_std + + if self.embedding_init_method is None: + if self.init_method is None or (self.embedding_init_method_std != self.init_method_std): + # In this case, we set both the init method and the embedding init method to + # whatever std value requested (or defaulted) for the embedding_init_layer + self.embedding_init_method = init_method_normal(self.embedding_init_method_std) + else: + # Replicate the current behavior where if you are not changing the std of the + # embedding init differently and the init method is set, we fallback to the + # init method for this layer. Since we are here after an OR we know that + # init_method is not None + self.embedding_init_method = self.init_method + + if self.init_method is None: + self.init_method = init_method_normal(self.init_method_std) + + if self.output_layer_init_method is None: + self.output_layer_init_method = scaled_init_method_normal( + self.init_method_std, + self.num_layers, + multiplier=2.0 if not self.is_hybrid_model else 1.0, + ) + + if self.num_moe_experts is not None and self.add_bias_linear: + assert ( + self.expert_tensor_parallel_size == 1 + ), "Bias in Moe is only supported when ETP==1" + + if self.moe_router_enable_expert_bias and self.moe_router_score_function != "sigmoid": + raise ValueError( + "Expert bias for aux-loss-free routing only supports sigmoid score function." + "Please set --moe-router-score-function sigmoid for sigmoid score function." + ) + + if self.num_moe_experts and self.fp8: + # TE version below 1.7.0 will raise Error when handle zeros tokens for expert + if not is_te_min_version("1.7.0.dev0"): + raise ValueError( + "Only transformer-engine>=1.7.0 supports MoE FP8 training, " + f"but your version is {get_te_version()}." + ) + + if self.moe_grouped_gemm and not is_te_min_version("1.11.0"): + raise ValueError( + "Only transformer-engine>=1.11.0 supports FP8 grouped gemm, " + f"but your version is {get_te_version()}." + ) + + if self.moe_router_padding_for_fp8: + # enable moe_router_padding_for_quantization + warnings.warn( + "--moe-router-padding-for-fp8 is going to be deprecated. " + "Use --moe-router-padding-for-quantization instead." + ) + self.moe_router_padding_for_quantization = True + + if self.moe_router_padding_for_quantization: + if self.fp8 is None and self.fp4 is None: + raise ValueError( + "fp8/fp4 must be specified when moe_router_padding_for_quantization is True." + ) + + if self.moe_token_dispatcher_type in ["allgather", "alltoall_seq"]: + raise ValueError( + "allgather and alltoall_seq dispatcher does not support " + "moe_router_padding_for_quantization." + ) + + if ( + self.moe_router_topk == 1 + and self.moe_router_score_function == "softmax" + and not self.moe_router_pre_softmax + and self.moe_router_load_balancing_type != "sinkhorn" + ): + # Requires applying softmax before selecting the top-k when k is 1, + # since softmax on a [num_tokens, 1] would yield a zero gradient. + raise ValueError("Please use --moe-router-pre-softmax when topk is 1.") + + if self.moe_router_group_topk: + if self.moe_router_topk_limited_devices: + raise ValueError( + "moe_router_topk_limited_devices is deprecated and replaced by " + "moe_router_group_topk and moe_router_num_groups." + ) + if not self.moe_router_num_groups: + raise ValueError( + "When using group limited routing, moe_router_num_groups must be specified." + ) + else: + assert self.num_moe_experts % self.moe_router_num_groups == 0, ( + f"num_moe_experts ({self.num_moe_experts}) should be divisible by " + f"moe_router_num_groups ({self.moe_router_num_groups})." + ) + assert self.moe_router_group_topk <= self.moe_router_num_groups, ( + f"moe_router_group_topk ({self.moe_router_group_topk}) should be smaller than " + f"moe_router_num_groups ({self.moe_router_num_groups})." + ) + elif self.moe_router_topk_limited_devices: + warnings.warn( + "moe_router_topk_limited_devices is deprecated. Use moe_router_group_topk and " + "moe_router_num_groups instead." + ) + self.moe_router_group_topk = self.moe_router_topk_limited_devices + self.moe_router_num_groups = self.expert_model_parallel_size + + if self.enable_cuda_graph or self.external_cuda_graph: + assert ( + self.cuda_graph_impl == "none" + ), "Do not use enable_cuda_graph or external_cuda_graph with cuda_graph_impl." + assert ( + not self.enable_cuda_graph or not self.external_cuda_graph + ), "enable_cuda_graph and external_cuda_graph cannot be enabled at the same time." + + if self.enable_cuda_graph: + warnings.warn('enable_cuda_graph is deprecated, use cuda_graph_impl=local instead.') + self.cuda_graph_impl = "local" + if self.external_cuda_graph: + warnings.warn( + 'external_cuda_graph is deprecated, ' + 'use cuda_graph_impl=transformer_engine instead.' + ) + self.cuda_graph_impl = "transformer_engine" + + if self.cuda_graph_scope is None: + self.cuda_graph_scope = [] + elif not isinstance(self.cuda_graph_scope, list): + if isinstance(self.cuda_graph_scope, CudaGraphScope): + self.cuda_graph_scope = [self.cuda_graph_scope] + else: + assert isinstance(self.cuda_graph_scope, str), ( + "cuda_graph_scope must be a string that can be converted to a list of " + f"CudaGraphScope, got {self.cuda_graph_scope}." + ) + self.cuda_graph_scope = self.cuda_graph_scope.split(',') + if all(isinstance(scope, str) for scope in self.cuda_graph_scope): + # Backward compatibility for "full" scope. Now we use an empty list instead. + if "full" in self.cuda_graph_scope: + assert self.cuda_graph_scope == [ + "full" + ], "full scope cannot be used with other scopes." + warnings.warn( + "full scope is deprecated. " + "Use empty cuda_graph_scope to capture the whole layer." + ) + self.cuda_graph_scope = [] + else: + self.cuda_graph_scope = [CudaGraphScope[scope] for scope in self.cuda_graph_scope] + assert all( + isinstance(scope, CudaGraphScope) for scope in self.cuda_graph_scope + ), f"cuda_graph_scope must be a list of CudaGraphScope, got {self.cuda_graph_scope}." + + if self.cuda_graph_impl != "none": + assert self.cuda_graph_impl in [ + "transformer_engine", + "local", + ], f"Invalid cuda graph implementation: {self.cuda_graph_impl}" + + if self.cpu_offloading: + raise ValueError("CUDA graphs not supported with CPU offloading.") + + if self.cuda_graph_impl == "local": + assert not self.cuda_graph_scope or self.cuda_graph_scope == [ + CudaGraphScope.full_iteration + ], ( + "For local cuda graph implementation, the only valid value for " + "cuda_graph_scope is full_iteration, or an empty list to denote layerwise " + "graphs. To use other scopes, use cuda_graph_impl=transformer_engine." + ) + + if self.cuda_graph_impl == "transformer_engine": + assert CudaGraphScope.full_iteration not in self.cuda_graph_scope, ( + "To use full iteration cuda graph, please use " + "cuda_graph_impl=local instead of cuda_graph_impl=transformer_engine." + ) + assert ( + CudaGraphScope.moe not in self.cuda_graph_scope + or CudaGraphScope.moe_router not in self.cuda_graph_scope + ), 'cuda_graph_scope must not contain both moe and moe_router.' + if CudaGraphScope.moe_preprocess in self.cuda_graph_scope: + assert ( + CudaGraphScope.moe_router in self.cuda_graph_scope + ), 'moe_preprocess cuda graph is only supported with moe_router cuda graph.' + if self.num_moe_experts is None or self.num_moe_experts <= 1: + assert ( + CudaGraphScope.moe not in self.cuda_graph_scope + and CudaGraphScope.moe_router not in self.cuda_graph_scope + ), 'moe cuda graph is only supported for MoE.' + else: + if self.moe_layer_freq == 1 or ( + isinstance(self.moe_layer_freq, list) and 0 not in self.moe_layer_freq + ): + assert CudaGraphScope.mlp not in self.cuda_graph_scope, ( + 'mlp cuda graph is only supported for dense layers, ' + 'but not found in the model.' + ) + if ( + self.moe_expert_capacity_factor is None + or not self.moe_pad_expert_input_to_capacity + ): + assert ( + CudaGraphScope.moe not in self.cuda_graph_scope + ), 'moe cuda graph is only supported with drop-padding MoE.' + if self.moe_token_dispatcher_type == 'alltoall' and ( + self.moe_expert_capacity_factor is not None + or self.moe_router_padding_for_quantization + ): + assert CudaGraphScope.moe_preprocess not in self.cuda_graph_scope, ( + 'moe_preprocess cuda graph is not supported when there are ' + 'DtoH copies and synchronizations in the preprocess step.' + ) + + if self.recompute_granularity: + if self.recompute_granularity != "selective" or not self.cuda_graph_scope: + raise ValueError( + "Full-layer CUDA graphs not supported with activation recomputation." + ) + elif self.cuda_graph_scope != [CudaGraphScope.full_iteration]: + # For scoped CUDA graphs, only the non-graphed parts of the layer can be + # recomputed. So check if there are overlaps between the recomputed parts + # and the graphed parts. + if CudaGraphScope.attn in self.cuda_graph_scope: + for module in self.recompute_modules: + if module in ['core_attn', 'mla_up_proj']: + raise ValueError( + f'attn cuda graph is not supported with {module} recompute.' + ) + if ( + CudaGraphScope.mlp in self.cuda_graph_scope + and "mlp" in self.recompute_modules + ): + raise ValueError(f'mlp cuda graph is not supported with mlp recompute.') + if CudaGraphScope.moe in self.cuda_graph_scope: + for module in self.recompute_modules: + if module in ['moe_act', 'moe', 'shared_experts']: + raise ValueError( + f'moe cuda graph is not supported with {module} recompute.' + ) + if CudaGraphScope.moe_router in self.cuda_graph_scope: + for module in self.recompute_modules: + if module in ['moe', 'shared_experts']: + raise ValueError( + f'moe_router cuda graph is not supported with {module} ' + 'recompute.' + ) + if "layernorm" in self.recompute_modules: + if ( + CudaGraphScope.attn in self.cuda_graph_scope + and CudaGraphScope.mlp in self.cuda_graph_scope + and ( + CudaGraphScope.moe in self.cuda_graph_scope + or CudaGraphScope.moe_router in self.cuda_graph_scope + ) + ): + raise ValueError( + 'cuda graph is not supported with layernorm recompute.' + ) + if CudaGraphScope.attn in self.cuda_graph_scope: + warnings.warn( + "input_layernorm recompute is not supported with attention " + "cudagraph. Will only recompute the pre_mlp_layernorm." + ) + if ( + CudaGraphScope.mlp in self.cuda_graph_scope + or CudaGraphScope.moe in self.cuda_graph_scope + or CudaGraphScope.moe_router in self.cuda_graph_scope + ): + warnings.warn( + "pre_mlp_layernorm recompute is not supported with mlp/moe " + "cudagraph. Will only recompute the input_layernorm." + ) + + if self.moe_token_dispatcher_type in ["allgather"]: + if self.variable_seq_lengths is True: + raise ValueError( + f"Token dispatcher type: {self.moe_token_dispatcher_type} does not support " + f"variable sequence length, please use alltoall dispatcher instead." + ) + + if self.moe_permute_fusion: + from megatron.core.transformer.moe.moe_utils import ( + fused_permute, + fused_permute_with_probs, + fused_sort_chunks_by_index, + fused_sort_chunks_by_index_with_probs, + fused_unpermute, + ) + + if ( + fused_permute is None + or fused_permute_with_probs is None + or fused_sort_chunks_by_index is None + or fused_sort_chunks_by_index_with_probs is None + or fused_unpermute is None + ): + raise ValueError("fused permutation is not available. Please install TE >= 2.1.0.") + + if self.overlap_moe_expert_parallel_comm: + # TODO: remove this after we fix the hang issue with torch version < 2.6.0 + assert is_torch_min_version( + "2.6.0" + ), "A2A Overlap encounters hang issue with torch version < 2.6.0" + if self.pipeline_model_parallel_size > 1: + assert self.virtual_pipeline_model_parallel_size is not None, ( + "If enabling EP A2A overlap, virtual_pipeline_model_parallel_size " + "must be specified when pipeline_model_parallel_size > 1" + ) + # Expert model parallelism requirements + assert ( + self.expert_model_parallel_size > 1 + ), 'overlap_moe_expert_parallel_comm is only supported with expert model parallelism' + assert self.moe_token_dispatcher_type in [ + 'alltoall', + 'flex', + ], 'overlap_moe_expert_parallel_comm is supported with alltoall/flex token dispatcher' + + assert ( + self.recompute_granularity != 'full' + ), 'disable full recomputation when enabling overlap_moe_expert_parallel_comm' + assert ( + self.recompute_method is None + ), 'disable recomputation method when enabling overlap_moe_expert_parallel_comm' + assert ( + self.recompute_num_layers is None + ), 'recompute_num_layers must be None when enabling overlap_moe_expert_parallel_comm' + + # Check if bf16 or fp16 is used + assert ( + self.bf16 or self.fp16 + ), 'overlap_moe_expert_parallel_comm is only supported with bf16 or fp16 model' + + assert ( + not self.moe_shared_expert_overlap + ), 'disable moe_shared_expert_overlap when enabling overlap_moe_expert_parallel_comm' + assert ( + self.mtp_num_layers is None or self.mtp_num_layers == 1 + ), 'MTP layernum only supports 1 when enabling overlap_moe_expert_parallel_comm.' + + # Check delay_wgrad_compute compatibility + if self.delay_wgrad_compute: + assert ( + self.overlap_moe_expert_parallel_comm + ), 'overlap_moe_expert_parallel_comm must be enabled when enabling delay_wgrad_compute' + assert ( + not self.moe_use_legacy_grouped_gemm + ), 'delay_wgrad_compute is not supported with legacy groupedgemm implementation' + + if self.context_parallel_size > 1 and self.cp_comm_type is not None: + if isinstance(self.cp_comm_type, list): + assert len(self.cp_comm_type) == self.num_layers, ( + f"Length of cp_comm_type ({len(self.cp_comm_type)}) should equal to " + f"the total number of transformer layers ({self.num_layers})!" + ) + else: + assert isinstance( + self.cp_comm_type, str + ), "Unsupported communication type for context parallelism!" + + assert ( + self.pipeline_model_parallel_size > 0 + ), f"Pipeline model parallel size must be larger than 0 \ + when enable --standalone-embedding-stage and --standalone-loss-stage" + + if ( + self.num_moe_experts is not None + and self.num_moe_experts >= 32 + and not self.moe_router_dtype + ): + warnings.warn( + "Using a large number of experts (e.g. >=32) without fp32 routing. " + "Consider enabling moe_router_dtype for better numerical stability." + ) + if self.symmetric_ar_type is not None: + if not HAVE_PACKAGING: + raise ImportError( + "packaging is not installed. Please install it with `pip install packaging`." + ) + assert is_torch_min_version("2.7.0a0"), "Must have at least torch version 2.7 or higher" + assert is_te_min_version("2.3.0") or get_te_version() == PkgVersion( + "2.3.0.dev0+39c0e70" + ), "Must have at least TE version 2.3 or higher to use symmetric memory all reduce" + + if self.no_rope_freq: + assert not self.flash_decode, "flash_decode cannot be used with no_rope." + if isinstance(self.no_rope_freq, int): + assert self.num_layers % self.no_rope_freq == 0, ( + f"no_rope_freq={self.no_rope_freq} should be " + f"divisible by num_layers={self.num_layers}." + ) + # Convert integer pattern to list pattern + # e.g. no_rope=4 with num_layers=8 becomes [0,0,0,1,0,0,0,1] + pattern = [0] * (self.no_rope_freq - 1) + [1] + self.no_rope_freq = pattern * (self.num_layers // self.no_rope_freq) + else: + assert len(self.no_rope_freq) == self.num_layers, ( + f"Length of no_rope list ({len(self.no_rope_freq)}) must match " + f"the number of layers ({self.num_layers})" + ) + + if self.fallback_to_eager_attn: + assert self.transformer_impl == "transformer_engine", ( + f"fallback_to_eager_attn is only available with transformer_engine implementation," + f" but got {self.transformer_impl=}." + ) + + if self.fallback_to_eager_attn or self.transformer_impl == "local": + if self.context_parallel_size > 1 and self.cp_comm_type is not None: + all_cp_comm_types_are_all_gather = ( + all(item == "all_gather" for item in self.cp_comm_type) + if isinstance(self.cp_comm_type, list) + else self.cp_comm_type == "all_gather" + ) + if not all_cp_comm_types_are_all_gather: + raise ValueError( + f"fallback_to_eager_attn only supports all_gather communication type " + f"for context parallelism, but got {self.cp_comm_type=} instead." + ) + + +@dataclass +class MLATransformerConfig(TransformerConfig): + """Configuration object for megatron-core Multi-Latent Attention (MLA) transformers. + + The initialization function has an argument for each parameter, including those in + ModelParallelConfig. Included YaRN RoPE parameters that is fused in MLA. + """ + + multi_latent_attention: bool = True + """Whether to use Multi-Latent Attention.""" + + q_lora_rank: int = 512 + """Rank of Query tensor's low rank representation.""" + + kv_lora_rank: int = 512 + """Rank of Key and Value tensors' low rank representation.""" + + qk_head_dim: int = 128 + """Dimension of the head in the QK projection. q_head_dim = qk_head_dim + qk_pos_emb_head_dim""" + + qk_pos_emb_head_dim: int = 64 + """Dimension of the position embedding in the QK projection.""" + + v_head_dim: int = 128 + """Dimension of the head in the V projection.""" + + normalization: str = "RMSNorm" + """Default normalization layer for MLA models is RMSNorm.""" + + rope_type: str = "yarn" + """Type of RoPE to use. Default to yarn, options are rope and yarn.""" + + rotary_base: float = 10000 + """Rotary base for the rotary embeddings, used by rope and yarn.""" + + rotary_percent: float = 1.0 + """Rotary percent for the rotary embeddings, used by rope.""" + + rotary_scaling_factor: float = 40 + """Rotary scaling factor for the rotary embeddings, used by yarn.""" + + original_max_position_embeddings: int = 4096 + """Original maximum position embeddings for the original model, used by yarn.""" + + beta_fast: float = 32 + """Beta fast for YaRN RoPE, used by yarn.""" + + beta_slow: float = 1 + """Beta slow for YaRN RoPE, used by yarn.""" + + mscale: float = 1.0 + """Mscale for YaRN RoPE in Multi-Latent Attention, used by yarn.""" + + mscale_all_dim: float = 0.0 + """Mscale all dimensions for YaRN RoPE in Multi-Latent Attention, used by yarn.""" + + cache_mla_latents: bool = False + """Cache the low dimensional tensors for MLA rather than full KV cache. + This is only for the dynamic inference backend and requires that + Flash MLA is installed.""" + + def __post_init__(self): + super().__post_init__() + if self.multi_latent_attention and self.apply_rope_fusion and self.rope_type != "yarn": + raise ValueError("apply_rope_fusion for MLA only works with YARN RoPE.") + + if self.attention_output_gate: + raise NotImplementedError("Output gate is not supported for MLA yet.") + + if self.cache_mla_latents: + assert ( + self.apply_rope_fusion is False + ), "Rope Fusion is not compatible with caching latents" \ No newline at end of file diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index 886b73e90..b19c174e9 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -427,7 +427,7 @@ index 89659a1d7..c69859a04 100644 + return dq, dk, None, None, None, None, None, None \ No newline at end of file diff --git a/megatron/core/transformer/experimental_attention_variant/dsa.py b/megatron/core/transformer/experimental_attention_variant/dsa.py -index fc994490b..b23d2e9a8 100644 +index 353b31e9b..f5c55e429 100644 --- a/megatron/core/transformer/experimental_attention_variant/dsa.py +++ b/megatron/core/transformer/experimental_attention_variant/dsa.py @@ -6,6 +6,7 @@ from dataclasses import dataclass @@ -438,16 +438,15 @@ index fc994490b..b23d2e9a8 100644 from megatron.core import parallel_state from megatron.core.models.common.embeddings import ( -@@ -21,6 +22,8 @@ from megatron.core.transformer.module import MegatronModule +@@ -20,6 +21,7 @@ from megatron.core.transformer.enums import AttnMaskType + from megatron.core.transformer.module import MegatronModule from megatron.core.transformer.spec_utils import ModuleSpec, build_module from megatron.core.transformer.transformer_config import TransformerConfig - +from megatron.core.transformer.dot_product_attention_context_parallel import AllGatherComm, AttentionFuncionWithContextParallel -+ + try: from fast_hadamard_transform import hadamard_transform - except ImportError: -@@ -191,44 +194,72 @@ def compute_dsa_indexer_loss( +@@ -191,44 +193,72 @@ def compute_dsa_indexer_loss( Returns: index_loss: KL divergence loss (scalar). """ @@ -555,9 +554,9 @@ index fc994490b..b23d2e9a8 100644 if pg_collection.tp.size() > 1: # attention scores are scattered to TP ranks in head dimension. torch.distributed.all_reduce(attention_scores.contiguous(), group=pg_collection.tp) -@@ -252,6 +283,57 @@ def compute_dsa_indexer_loss( - return indexer_loss +@@ -251,6 +281,56 @@ def compute_dsa_indexer_loss( + return indexer_loss +def compute_attention_scores_with_cp(q, k, attn_bias, scale, heads_k_stride = 1): + """ @@ -609,11 +608,10 @@ index fc994490b..b23d2e9a8 100644 + + return attns + -+ + class DSAIndexerLossAutoScaler(torch.autograd.Function): """An AutoScaler that triggers the backward pass and scales the grad for indexer loss. - -@@ -496,7 +578,15 @@ class DSAIndexer(MegatronModule): +@@ -496,7 +576,15 @@ class DSAIndexer(MegatronModule): # Compute attention scores: q @ k^T # [seqlen_q, batch, index_n_heads, index_head_dim] @ [seqlen_k, batch, index_head_dim]^T # -> [seqlen_q, batch, index_n_heads, seqlen_k] @@ -630,19 +628,35 @@ index fc994490b..b23d2e9a8 100644 # Apply ReLU activation. index_scores = torch.relu(index_scores) -@@ -606,7 +696,10 @@ class DSAIndexer(MegatronModule): +@@ -546,14 +634,10 @@ class DSAIndexer(MegatronModule): + None, None, x, self.config, packed_seq_params + ) + if self.config.rope_type == "rope": +- rotary_pos_emb = self.rotary_pos_emb( +- rotary_seq_len, packed_seq_params=packed_seq_params +- ) ++ rotary_pos_emb = self.rotary_pos_emb(rotary_seq_len, packed_seq=False) + mscale = 1.0 + else: +- rotary_pos_emb, mscale = self.rotary_pos_emb( +- rotary_seq_len, packed_seq_params=packed_seq_params +- ) ++ rotary_pos_emb, mscale = self.rotary_pos_emb(rotary_seq_len, packed_seq=False) + + # ========================================= + # Gather inputs if sp is enabled +@@ -610,7 +694,9 @@ class DSAIndexer(MegatronModule): # ========================================= # Select top-k indices # ========================================= - topk_k = min(self.index_topk, seqlen) + cp_size = parallel_state.get_context_parallel_world_size() -+ + seqlen_k_global = k.shape[0] * cp_size + topk_k = min(self.index_topk, seqlen_k_global) # [batch, seqlen, index_topk] topk_indices = index_scores.topk(topk_k, dim=-1)[1] -@@ -687,6 +780,48 @@ def unfused_dsa_fn(query, key, value, topk_indices, softmax_scale): +@@ -691,6 +777,48 @@ def unfused_dsa_fn(query, key, value, topk_indices, softmax_scale): output = output.reshape(sq, b, np * hnv) return output @@ -691,7 +705,7 @@ index fc994490b..b23d2e9a8 100644 class DSAttention(MegatronModule): """ -@@ -729,7 +864,6 @@ class DSAttention(MegatronModule): +@@ -733,7 +861,6 @@ class DSAttention(MegatronModule): self, query: torch.Tensor, key: torch.Tensor, @@ -699,7 +713,7 @@ index fc994490b..b23d2e9a8 100644 x: torch.Tensor, qr: torch.Tensor, attention_mask: torch.Tensor, -@@ -743,7 +877,6 @@ class DSAttention(MegatronModule): +@@ -747,7 +874,6 @@ class DSAttention(MegatronModule): Args: query: Query tensor [sq, b, np, hn]. key: Key tensor [skv, b, np, hn]. @@ -707,7 +721,7 @@ index fc994490b..b23d2e9a8 100644 x: Original hidden states [sq, b, hidden_size]. qr: Low-rank query representation [sq, b, q_lora_rank]. attention_mask: Attention mask tensor [b, 1, sq, sk]. -@@ -754,9 +887,11 @@ class DSAttention(MegatronModule): +@@ -758,9 +884,11 @@ class DSAttention(MegatronModule): Returns: output: Output tensor [sq, b, hidden_size] """ @@ -722,7 +736,7 @@ index fc994490b..b23d2e9a8 100644 # Detach x and qr to prevent gradients of indexer from flowing back to the main model. x = x.detach() -@@ -768,18 +903,17 @@ class DSAttention(MegatronModule): +@@ -772,18 +900,17 @@ class DSAttention(MegatronModule): # Generate upper triangular mask with -inf above diagonal, 0 elsewhere # torch.triu with diagonal=1 creates upper triangular matrix (excluding main diagonal) # float_mask [sq, skv] @@ -741,7 +755,7 @@ index fc994490b..b23d2e9a8 100644 - float_mask = torch.zeros_like(mask, dtype=torch.float32).masked_fill( - mask, float('-inf') - ) -+ ++ + # float_mask [b, sq, skv] + float_mask = torch.zeros_like(mask, dtype=torch.float32).masked_fill( + mask, float('-inf') @@ -749,7 +763,7 @@ index fc994490b..b23d2e9a8 100644 # =================================== # Get index scores and top-k indices -@@ -791,32 +925,6 @@ class DSAttention(MegatronModule): +@@ -795,32 +922,6 @@ class DSAttention(MegatronModule): # =================================== # Run sparse attention kernel # =================================== @@ -780,30 +794,24 @@ index fc994490b..b23d2e9a8 100644 - ) - # Attach loss to output - output = DSAIndexerLossAutoScaler.apply(output, indexer_loss) +- + output = unfused_dsa_fn_with_cp(query, key, dim_v, topk_indices, self.softmax_scale) - ++ return output diff --git a/megatron/core/transformer/multi_latent_attention.py b/megatron/core/transformer/multi_latent_attention.py -index 3953d933b..7d030ad02 100644 +index ed90fdffa..4298e044b 100644 --- a/megatron/core/transformer/multi_latent_attention.py +++ b/megatron/core/transformer/multi_latent_attention.py -@@ -6,6 +6,7 @@ from dataclasses import dataclass - from typing import NoReturn, Optional, Union - - import torch -+import torch.nn.functional as F +@@ -15,7 +15,7 @@ except ImportError: + HAVE_EINOPS = False - try: - from einops import rearrange -@@ -167,6 +168,7 @@ class MultiLatentAttention(Attention): - ) - # Output. -+ # SP_Reduce scatter + TP_Row_par - self.linear_proj = build_module( - submodules.linear_proj, - self.query_projection_size, -@@ -311,7 +313,6 @@ class MultiLatentAttention(Attention): +-from megatron.core import tensor_parallel ++from megatron.core import parallel_state, tensor_parallel + from megatron.core.models.common.embeddings import ( + RotaryEmbedding, + YarnRotaryEmbedding, +@@ -312,7 +312,6 @@ class MultiLatentAttention(Attention): core_attn_out = self.core_attention( query, key, @@ -811,7 +819,7 @@ index 3953d933b..7d030ad02 100644 x=hidden_states, qr=q_compressed, attention_mask=attention_mask, -@@ -370,6 +371,19 @@ class MultiLatentAttention(Attention): +@@ -371,6 +370,19 @@ class MultiLatentAttention(Attention): self.qkv_up_checkpoint.discard_output_and_register_recompute(core_attn_out) self.qkv_up_checkpoint = None @@ -831,27 +839,53 @@ index 3953d933b..7d030ad02 100644 # ================= # Output. [sq, b, h] # ================= -@@ -384,7 +398,6 @@ class MultiLatentAttention(Attention): - - return output, bias - -- - class MLASelfAttention(MultiLatentAttention): - """MLA Self-attention layer class +@@ -555,11 +567,7 @@ class MLASelfAttention(MultiLatentAttention): + assert ( + hidden_states.ndim == 3 + ), f"hidden_states should be 3D, [s, b, n*h], got {hidden_states.ndim}D" +- if packed_seq_params is not None: +- assert ( +- packed_seq_params.local_cp_size is None +- ), "hybrid_context_parallel is not supported with MLA yet and is planned for future. \ +- Please disable hybrid_context_parallel." ++ -@@ -753,7 +766,6 @@ class MLASelfAttention(MultiLatentAttention): - # [num_tokens, qk_pos_emb_head_dim] -> [num_tokens, 1, qk_pos_emb_head_dim] - k_pos_emb = torch.unsqueeze(k_pos_emb, -2) + inference_context = deprecate_inference_params(inference_context, inference_params) -- # todo add assert about fusions and caching +@@ -576,13 +584,11 @@ class MLASelfAttention(MultiLatentAttention): + rotary_pos_sin = None + packed_seq = packed_seq_params is not None and packed_seq_params.qkv_format == 'thd' + if self.config.rope_type == "rope": +- rotary_pos_emb = self.rotary_pos_emb( +- rotary_seq_len, packed_seq_params=packed_seq_params +- ) ++ rotary_pos_emb = self.rotary_pos_emb(rotary_seq_len, packed_seq=packed_seq) + else: if self.config.apply_rope_fusion: - cp_rank = self.pg_collection.cp.rank() - cp_size = self.pg_collection.cp.size() -@@ -844,6 +856,98 @@ class MLASelfAttention(MultiLatentAttention): - value = value.contiguous() + rotary_pos_cos, rotary_pos_sin = self.rotary_pos_emb.get_cached_cos_sin( +- rotary_seq_len, dtype=hidden_states.dtype, packed_seq_params=packed_seq_params ++ rotary_seq_len, dtype=hidden_states.dtype, packed_seq=packed_seq + ) + rotary_pos_emb = None + assert inference_context is None, "Inference with MLA RoPE fusion is not supported" +@@ -591,11 +597,9 @@ class MLASelfAttention(MultiLatentAttention): + and fused_apply_mla_rope_for_kv is not None + ), "Fused MLA RoPE apply is not imported successfully" + else: +- rotary_pos_emb, mscale = self.rotary_pos_emb( +- rotary_seq_len, packed_seq_params=packed_seq_params +- ) ++ rotary_pos_emb, mscale = self.rotary_pos_emb(rotary_seq_len, packed_seq=packed_seq) + +- if packed_seq_params is not None and packed_seq_params.qkv_format == 'thd': ++ if packed_seq_params is not None: + if packed_seq_params.cu_seqlens_q_padded is not None: + cu_seqlens_q = packed_seq_params.cu_seqlens_q_padded + else: +@@ -867,6 +871,98 @@ class MLASelfAttention(MultiLatentAttention): return query, key, value -+ + + def mla_absorb(q_compressed, kv_compressed, k_pos_emb, rotary_pos_emb): + if self.config.q_lora_rank is not None: + # q_compressed: [num_tokens, q_lora_rank] @@ -896,7 +930,7 @@ index 3953d933b..7d030ad02 100644 + + # TODO: Does it match ZZ? SP does not need but CP needs + if self.config.sequence_parallel: -+ kv_compressed = gather_from_sequence_parallel_region(kv_compressed) ++ kv_compressed = gather_from_sequence_parallel_region(kv_compressed, group=self.tp_group) + + # kv_compressed: [num_tokens, kv_lora_rank] + if kv_compressed.ndim == 3: # [s, b, kv_lora_rank] @@ -943,10 +977,11 @@ index 3953d933b..7d030ad02 100644 + key = key.contiguous() + + return query, key - ++ if self.recompute_up_proj: quantization = self.config.fp8 or self.config.fp4 -@@ -860,9 +964,10 @@ class MLASelfAttention(MultiLatentAttention): + self.qkv_up_checkpoint = tensor_parallel.CheckpointWithoutOutput(fp8=quantization) +@@ -882,9 +978,10 @@ class MLASelfAttention(MultiLatentAttention): q_compressed, kv_compressed, k_pos_emb, rotary_pos_emb ) else: @@ -958,13 +993,11 @@ index 3953d933b..7d030ad02 100644 if return_compressed_tensors: return query, key, value, q_compressed, kv_compressed -@@ -1104,5 +1209,27 @@ class MLASelfAttention(MultiLatentAttention): - * (self.config.qk_head_dim + self.config.v_head_dim), - -1, +@@ -1128,3 +1225,26 @@ class MLASelfAttention(MultiLatentAttention): ) -- + return weight_kv_updated -+ ++ + @property + def up_k_weight_(self): + # linear_kv_up_proj.weight: [num_heads_per_partition * (qk_head_dim + v_head_dim), kv_lora_rank] @@ -987,9 +1020,10 @@ index 3953d933b..7d030ad02 100644 + ) + # [num_heads_per_partition, v_head_dim, kv_lora_rank] + return weight_reshaped[:, self.config.qk_head_dim:, :] +\ No newline at end of file diff --git a/megatron/core/transformer/tilelang_kernel/__init__.py b/megatron/core/transformer/tilelang_kernel/__init__.py new file mode 100644 -index 000000000..d8f2425f0 +index 000000000..c63794256 --- /dev/null +++ b/megatron/core/transformer/tilelang_kernel/__init__.py @@ -0,0 +1,10 @@ @@ -1003,6 +1037,7 @@ index 000000000..d8f2425f0 + "sparse_mla_fwd_interface", + "sparse_mla_bwd", +] +\ No newline at end of file diff --git a/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py b/megatron/core/transformer/tilelang_kernel/sparse_mla_bwd.py new file mode 100644 index 000000000..83a259efa @@ -1481,10 +1516,10 @@ index 000000000..e247038de + return out, lse \ No newline at end of file diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py -index a3a167549..98391fda6 100644 +index e2705bd9f..29a0ff9e0 100644 --- a/megatron/core/transformer/transformer_config.py +++ b/megatron/core/transformer/transformer_config.py -@@ -918,9 +918,9 @@ class TransformerConfig(ModelParallelConfig): +@@ -935,11 +935,10 @@ class TransformerConfig(ModelParallelConfig): f" but got {self.context_parallel_size=}." ) elif self.experimental_attention_variant == "dsa": @@ -1495,5 +1530,7 @@ index a3a167549..98391fda6 100644 + # self.context_parallel_size == 1 + # ), "Currently context parallelism is not supported by DSAttention!" assert not self.apply_rope_fusion, "RoPE fusion is not supported for DSAttention" - +- if self.fp8: + # cannot support first last layer bf16 with delayed scaling + if self.first_last_layers_bf16 and self.fp8_recipe == Fp8Recipe.delayed: From 72bafb1437e0fa676dd4136867f32d323e196323 Mon Sep 17 00:00:00 2001 From: lizamd <161388580+lizamd@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:50:49 -0800 Subject: [PATCH 53/57] Fix PYTHONPATH for AMD container Megatron-LM location (#506) --- scripts/run-qwen3-4B-amd.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/run-qwen3-4B-amd.sh b/scripts/run-qwen3-4B-amd.sh index 83af90156..998f06b7f 100755 --- a/scripts/run-qwen3-4B-amd.sh +++ b/scripts/run-qwen3-4B-amd.sh @@ -139,16 +139,16 @@ NUM_GPUS=$(echo ${HIP_VISIBLE_DEVICES} | tr ',' '\n' | wc -l) ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus ${NUM_GPUS} --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 -# "PYTHONPATH": "/workspace/Megatron-LM/", -MEGATRON_LM_PATH=$(pip list | grep megatron-core | awk '{print $NF}') +# Dynamically detect Megatron-LM installation path +MEGATRON_LM_PATH=$(python3 -c "import megatron; import os; print(os.path.dirname(os.path.dirname(megatron.__file__)))" 2>/dev/null || echo "/app/Megatron-LM") ray job submit --address="http://127.0.0.1:8265" \ - --runtime-env-json='{ - "env_vars": { - "PYTHONPATH": "/workspace/Megatron-LM/", - "CUDA_DEVICE_MAX_CONNECTIONS": "1" + --runtime-env-json="{ + \"env_vars\": { + \"PYTHONPATH\": \"${MEGATRON_LM_PATH}/\", + \"CUDA_DEVICE_MAX_CONNECTIONS\": \"1\" } - }' \ + }" \ -- python3 train.py \ --actor-num-nodes 1 \ --actor-num-gpus-per-node 8 \ From f8e4cd884217b47ceec431b5b4f513fcba51bfa5 Mon Sep 17 00:00:00 2001 From: zhihaow6 Date: Thu, 22 Jan 2026 15:12:17 -0800 Subject: [PATCH 54/57] update --- docker/deepseekv32/megatron.patch | 19 +++++++------------ miles/backends/training_utils/data.py | 7 ------- miles/utils/data.py | 8 +------- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index b19c174e9..bd5ef87c6 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -427,7 +427,7 @@ index 89659a1d7..c69859a04 100644 + return dq, dk, None, None, None, None, None, None \ No newline at end of file diff --git a/megatron/core/transformer/experimental_attention_variant/dsa.py b/megatron/core/transformer/experimental_attention_variant/dsa.py -index 353b31e9b..f5c55e429 100644 +index 353b31e9b..221e93500 100644 --- a/megatron/core/transformer/experimental_attention_variant/dsa.py +++ b/megatron/core/transformer/experimental_attention_variant/dsa.py @@ -6,6 +6,7 @@ from dataclasses import dataclass @@ -635,13 +635,13 @@ index 353b31e9b..f5c55e429 100644 - rotary_pos_emb = self.rotary_pos_emb( - rotary_seq_len, packed_seq_params=packed_seq_params - ) -+ rotary_pos_emb = self.rotary_pos_emb(rotary_seq_len, packed_seq=False) ++ rotary_pos_emb = self.rotary_pos_emb(rotary_seq_len, packed_seq_params=packed_seq_params) mscale = 1.0 else: - rotary_pos_emb, mscale = self.rotary_pos_emb( - rotary_seq_len, packed_seq_params=packed_seq_params - ) -+ rotary_pos_emb, mscale = self.rotary_pos_emb(rotary_seq_len, packed_seq=False) ++ rotary_pos_emb, mscale = self.rotary_pos_emb(rotary_seq_len, packed_seq_params=packed_seq_params) # ========================================= # Gather inputs if sp is enabled @@ -799,7 +799,7 @@ index 353b31e9b..f5c55e429 100644 + return output diff --git a/megatron/core/transformer/multi_latent_attention.py b/megatron/core/transformer/multi_latent_attention.py -index ed90fdffa..4298e044b 100644 +index ed90fdffa..7a7597d66 100644 --- a/megatron/core/transformer/multi_latent_attention.py +++ b/megatron/core/transformer/multi_latent_attention.py @@ -15,7 +15,7 @@ except ImportError: @@ -852,22 +852,17 @@ index ed90fdffa..4298e044b 100644 inference_context = deprecate_inference_params(inference_context, inference_params) -@@ -576,13 +584,11 @@ class MLASelfAttention(MultiLatentAttention): +@@ -576,9 +584,7 @@ class MLASelfAttention(MultiLatentAttention): rotary_pos_sin = None packed_seq = packed_seq_params is not None and packed_seq_params.qkv_format == 'thd' if self.config.rope_type == "rope": - rotary_pos_emb = self.rotary_pos_emb( - rotary_seq_len, packed_seq_params=packed_seq_params - ) -+ rotary_pos_emb = self.rotary_pos_emb(rotary_seq_len, packed_seq=packed_seq) ++ rotary_pos_emb = self.rotary_pos_emb(rotary_seq_len, packed_seq_params=packed_seq_params) else: if self.config.apply_rope_fusion: rotary_pos_cos, rotary_pos_sin = self.rotary_pos_emb.get_cached_cos_sin( -- rotary_seq_len, dtype=hidden_states.dtype, packed_seq_params=packed_seq_params -+ rotary_seq_len, dtype=hidden_states.dtype, packed_seq=packed_seq - ) - rotary_pos_emb = None - assert inference_context is None, "Inference with MLA RoPE fusion is not supported" @@ -591,11 +597,9 @@ class MLASelfAttention(MultiLatentAttention): and fused_apply_mla_rope_for_kv is not None ), "Fused MLA RoPE apply is not imported successfully" @@ -875,7 +870,7 @@ index ed90fdffa..4298e044b 100644 - rotary_pos_emb, mscale = self.rotary_pos_emb( - rotary_seq_len, packed_seq_params=packed_seq_params - ) -+ rotary_pos_emb, mscale = self.rotary_pos_emb(rotary_seq_len, packed_seq=packed_seq) ++ rotary_pos_emb, mscale = self.rotary_pos_emb(rotary_seq_len, packed_seq_params=packed_seq_params) - if packed_seq_params is not None and packed_seq_params.qkv_format == 'thd': + if packed_seq_params is not None: diff --git a/miles/backends/training_utils/data.py b/miles/backends/training_utils/data.py index d38161e60..67bb30108 100644 --- a/miles/backends/training_utils/data.py +++ b/miles/backends/training_utils/data.py @@ -134,13 +134,6 @@ def get_batch( tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format, max_seqlen) for t in tokens] tokens = torch.stack(tokens) - if qkv_format == "bshd": - max_seqlen = batch["max_seq_lens"][0] - assert max([t.size(0) for t in tokens]) <= max_seqlen - tokens = [slice_with_cp(t, pad_token_id, qkv_format, max_seqlen) for t in tokens] - tokens = torch.stack(tokens) - # TODO: padding to multiples? - elif qkv_format == "thd": tokens = [slice_with_cp(t, pad_token_id, parallel_state, qkv_format) for t in tokens] diff --git a/miles/utils/data.py b/miles/utils/data.py index 737246acd..eb512e514 100644 --- a/miles/utils/data.py +++ b/miles/utils/data.py @@ -200,13 +200,6 @@ def __init__( metadata["tools"] = tools if apply_chat_template: - output_prompt = tokenizer.apply_chat_template( - prompt, - tools=tools, - tokenize=False, - add_generation_prompt=True, - **(apply_chat_template_kwargs or {}), - ) ### DSV32 try: prompt = tokenizer.apply_chat_template( @@ -221,6 +214,7 @@ def __init__( encode_config = dict(thinking_mode="thinking", drop_thinking=True, add_default_bos_token=True) prompt = encode_messages(prompt, **encode_config) ### DSV32 + output_prompt = prompt else: output_prompt = prompt From b556aecfa7c61f9cf214f5d057ae6da5c164140a Mon Sep 17 00:00:00 2001 From: Zhihao Wang <101526713+xiuhu17@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:35:29 -0800 Subject: [PATCH 55/57] update --- docker/deepseekv32/megatron.patch | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docker/deepseekv32/megatron.patch b/docker/deepseekv32/megatron.patch index bd5ef87c6..f7204197c 100644 --- a/docker/deepseekv32/megatron.patch +++ b/docker/deepseekv32/megatron.patch @@ -798,6 +798,34 @@ index 353b31e9b..221e93500 100644 + output = unfused_dsa_fn_with_cp(query, key, dim_v, topk_indices, self.softmax_scale) + return output +diff --git a/megatron/core/transformer/moe/moe_utils.py b/megatron/core/transformer/moe/moe_utils.py +index 28cff06f5..befb5c124 100644 +--- a/megatron/core/transformer/moe/moe_utils.py ++++ b/megatron/core/transformer/moe/moe_utils.py +@@ -586,6 +586,9 @@ def topk_routing_with_score_function( + ) + else: + return torch.topk(scores, k=topk, dim=1) ++ ++ from miles.utils.routing_replay import get_routing_replay_compute_topk ++ compute_topk = get_routing_replay_compute_topk(compute_topk) + + if score_function == "softmax": + if use_pre_softmax: +diff --git a/megatron/core/transformer/moe/router.py b/megatron/core/transformer/moe/router.py +index 16fc9d9af..3c50a9516 100644 +--- a/megatron/core/transformer/moe/router.py ++++ b/megatron/core/transformer/moe/router.py +@@ -200,6 +200,9 @@ class TopKRouter(Router): + else: + self.global_tokens_per_expert = None + self.ga_steps = None ++ ++ from miles.utils.routing_replay import register_routing_replay ++ register_routing_replay(self) + + def _maintain_float32_expert_bias(self): + """ diff --git a/megatron/core/transformer/multi_latent_attention.py b/megatron/core/transformer/multi_latent_attention.py index ed90fdffa..7a7597d66 100644 --- a/megatron/core/transformer/multi_latent_attention.py From adf07aaec10d8ae53f3b9806cf841f16542a8bfe Mon Sep 17 00:00:00 2001 From: zhihaow6 Date: Fri, 23 Jan 2026 17:55:06 -0800 Subject: [PATCH 56/57] update --- A.py | 1986 ---------------------------------------------------------- 1 file changed, 1986 deletions(-) delete mode 100644 A.py diff --git a/A.py b/A.py deleted file mode 100644 index 811f1c692..000000000 --- a/A.py +++ /dev/null @@ -1,1986 +0,0 @@ -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - -import warnings -from dataclasses import dataclass -from typing import Callable, List, Literal, Optional, Tuple, Union - -import torch -import torch.nn.functional as F - -from megatron.core.enums import Fp4Recipe, Fp8Recipe -from megatron.core.quantization.quant_config import RecipeConfig -from megatron.core.transformer.enums import AttnBackend, CudaGraphScope -from megatron.core.transformer.pipeline_parallel_layer_layout import PipelineParallelLayerLayout - -from ..fusions.fused_bias_geglu import quick_gelu -from ..model_parallel_config import ModelParallelConfig -from ..utils import ( - get_te_version, - init_method_normal, - is_te_min_version, - is_torch_min_version, - scaled_init_method_normal, -) - -try: - from packaging.version import Version as PkgVersion - - HAVE_PACKAGING = True -except ImportError: - HAVE_PACKAGING = False - - -@dataclass -class TransformerConfig(ModelParallelConfig): - """Configuration object for megatron-core transformers. - - The initialization function has an argument for each parameter, - including those in ModelParallelConfig. - """ - - #################### - # model architecture - #################### - - num_layers: int = 0 - """Number of transformer layers in a transformer block.""" - - mtp_num_layers: Optional[int] = None - """Number of Multi-Token Prediction (MTP) Layers.""" - - mtp_loss_scaling_factor: Optional[float] = None - """Weighting factor of Multi-Token Prediction (MTP) loss.""" - - num_layers_in_first_pipeline_stage: Optional[int] = None - """Number of transformer layers on first pipeline stage. - None implies equal layer division across PP ranks.""" - - num_layers_in_last_pipeline_stage: Optional[int] = None - """Number of transformer layers on last pipeline stage. - None implies equal layer division across PP ranks.""" - - pipeline_model_parallel_layout: Optional[Union[str, list, PipelineParallelLayerLayout]] = None - """Custom definition of the pipeline parallel partitioning. - Support type: - - str: e.g., 'Et*3|(tt|)*29,m|L'. Stages are split by '|', replicated stages or layers - can be described with multiplication. Commas can be used cosmetically. - - list: e.g., [['embedding', 'decoder'], ['decoder', 'decoder', 'decoder', 'loss']]. - - PipelineParallelLayerLayout: a PipelineParallelLayerLayout object. - If given either a string or a list, it will be transferred into a PipelineParallelLayerLayout - in post init. Let i = a * pp_size + b, then layout[i] gives a list of the layers - in the a-th vpp stage and the b-th pp stage, i.e., vpp(0)pp(0), vpp(0)pp(1), ..., - vpp(i)pp(j), vpp(i)pp(j+1), ..., vpp(-1)pp(-2), vpp(-1)pp(-1). - In the inner lists of layers, 'embedding' or 'E' denotes the embedding layer, 'loss' or 'L' - denotes the loss function, and 'decoder' or 't' denotes the transformer decoder layer. - Examples: - [['embedding', 'decoder'], ['decoder', 'decoder', 'decoder', 'loss']]: - pp = 2, vpp = None - pp rank 0 holds: embedding, decoder - pp rank 1 holds: decoder*3, loss - 'E|(tt|)*2,(t|)*4,mL': - pp = 2, vpp = 4 - vpp rank 0 pp rank 0 holds: embedding - vpp rank 0 pp rank 1~2 holds: decoder*2 - vpp rank 0 pp rank 3 holds: decoder - vpp rank 1 pp rank 0~2 holds: decoder - vpp rank 1 pp rank 3 holds: mtp, loss""" - - account_for_embedding_in_pipeline_split: bool = False - """If set, the embedding layer will be treated as a standard transformer - layer in the context of partition and placement for pipeline parallelism.""" - - account_for_loss_in_pipeline_split: bool = False - """If set, the loss layer will be treated as a standard transformer - layer in the context of partition and placement for pipeline parallelism.""" - - hidden_size: int = 0 - """Transformer hidden size.""" - - num_attention_heads: int = 0 - """Number of transformer attention heads.""" - - attention_backend: AttnBackend = AttnBackend.auto - """Attention backend to run. By default we let transformer engine - decide the best backend to run (except in the case of local). - If attention backend is local we use the local pytorch implementation in mcore. - Users can specify exact backend by changing this config. """ - - softmax_scale: Optional[float] = None - """Softmax scale for attention scaling.""" - - softmax_type: Literal['vanilla', 'off-by-one', 'learnable'] = 'vanilla' - """Applies modified softmax from https://www.evanmiller.org/attention-is-off-by-one.html. - Supports both TE FusedAttention and local unfused attention. Supports both a fixed offset and - and learnable offset.""" - - num_query_groups: Optional[int] = None - """Number of query groups for group query attention. If None, normal attention is used.""" - - ffn_hidden_size: Optional[int] = None - """Transformer Feed-Forward Network hidden size. This is set to 4*hidden_size - if not provided.""" - - kv_channels: Optional[int] = None - """Projection weights dimension in multi-head attention. This is set to hidden_size // - num_attention_heads if not provided.""" - - hidden_dropout: float = 0.1 - """Dropout probability for transformer hidden state.""" - - attention_dropout: float = 0.1 - """Post attention dropout probability.""" - - fp32_residual_connection: bool = False - """If true, move residual connections to fp32.""" - - # @jcasper should we keep this option? - apply_residual_connection_post_layernorm: bool = False - """If True, uses the original BERT residule connection ordering.""" - - layernorm_epsilon: float = 1e-5 - """Epsilon value for any LayerNorm operations.""" - - layernorm_zero_centered_gamma: bool = False - """If set to True, the LayerNorm is adjusted to center the gamma values around 0. This improves - numerical stability.""" - - add_bias_linear: bool = True - """Include a bias term in all linear layers (QKV projections, after core attention, and two in - MLP layer).""" - - add_qkv_bias: bool = False - """Add a bias term only for QKV projections.""" - - gated_linear_unit: bool = False - """Use a gated linear unit for the first linear layer in the MLP.""" - - activation_func: Callable = F.gelu - """Activation function to use for the non-linearity in the MLP.""" - - activation_func_fp8_input_store: bool = False - """Store the input of MLP activation function in FP8 for backprop to save memory. - The stored input is casted back to the original precision before backprop compuatation.""" - - glu_linear_offset: float = 0.0 - """Offset term in the GLU activation function: activation_func(x[0]) * (x[1] + offset). Only - used when gated_linear_unit is True""" - - activation_func_clamp_value: Optional[float] = None - """Clamp the output of the linear_fc1 in the activation function. Only used when activation_func - is quick_gelu.""" - - num_moe_experts: Optional[int] = None - """Number of experts to use for MoE layer. When set, it replaces MLP with MoE layer. Set to None - for no MoE.""" - - rotary_interleaved: bool = False - """True is rotate pairs of even and odd dimensions (RoFormer style), False is rotate pairs of - first half and second half (LLaMa style). Default to False.""" - - window_size: Optional[Tuple[int, int]] = None - """If not None, then will use sliding window attention. The size of the window is specified by - the numbers inside the tuple; -1 is special value meaning "infinite window size".""" - - window_attn_skip_freq: Optional[Union[int, List[int]]] = None - """Frequency of full attention layers among sliding window attention layers. Accepts either: - - An integer N: Represents a (N-1):1 ratio, one full attention layer after (N-1) SWA layers. - - A list that defines a custom pattern, e.g.: [1,1,1,1,0,0,0,0], where 1 represents SWA. """ - - normalization: str = "LayerNorm" - """Which norm to use for normalization layers, valid options are `LayerNorm` and `RMSNorm`.""" - - qk_layernorm: bool = False - """Whether to apply `normalization` type of normalization to the query and key embeddings.""" - - qk_clip: bool = False - """Whether to clip the query and key weights. Needed for Muon MLA Model training.""" - - qk_clip_alpha: float = 0.5 - """The balancing alpha for qk-clip. Q = Q * (eta ** alpha)""" - - qk_clip_threshold: float = 100 - """The balancing threshold for qk-clip. eta = min(threshold / max_attention_logits, 1.0)""" - - log_max_attention_logit: bool = False - """Whether to log the max attention logit across whole model. Decoupled from qk_clip, - defualts to False. Setting qk_clip will automatically log the max logit""" - - attention_output_gate: bool = False - """Whether to apply output gate to the attention layers.""" - - test_mode: bool = False - """Whether to run real-time tests.""" - - calculate_per_token_loss: bool = False - """Whether cross entropy loss is calculated over the actual number of non-padded tokens in the - global batch, versus the default behavior of assuming all tokens are non-padded.""" - - multi_latent_attention: bool = False - """Whether to use multi-latent attention.""" - - no_rope_freq: Optional[Union[int, List[int]]] = None - """Controls which layers perform Rotary Position Embedding (RoPE). Accepts either: - An integer N: Creates a pattern where RoPE is skipped every N-1 layers. For example, - no_rope=4 means RoPE is applied for 3 layers, then skipped for 1 layer, repeating this pattern. - A list of integers: Defines a custom pattern where 1 means skip RoPE and 0 means apply RoPE. - For example, [0,1,1,0] means: apply RoPE, skip RoPE, skip RoPE, apply RoPE.""" - - moe_deepep_num_sms: int = 20 - """Number of SMs to use for DeepEP.""" - - moe_hybridep_num_sms: int = 16 - """Number of SMs to use for HybridEP. In pure NVL scenarios, - 16 SMs can generally achieve good bandwidth.""" - - #################### - # attention variant - #################### - experimental_attention_variant: Optional[str] = None - """Type of attention variant to use. Currently support gated_delta_net and dsa.""" - - #################### - # attention variant: gated_delta_net - #################### - linear_attention_freq: Optional[Union[int, List[int]]] = None - """Frequency between LA (linear attention) layers - and SDPA (scaled dot-product attention) layers. - Accepts either: - - An integer N: Represents a (N-1):N ratio, meaning (N-1) LA layers for every 1 SDPA layer - - A list that defines a custom pattern, e.g.: [1,1,1,0,1,1,1,0,1,1,1,0]""" - - linear_conv_kernel_dim: Optional[int] = None - """Conv kernel dimension for the gated delta net.""" - - linear_key_head_dim: Optional[int] = None - """Query and key head dimension for the gated delta net.""" - - linear_value_head_dim: Optional[int] = None - """Value and gate head dimension for the gated delta net.""" - - linear_num_key_heads: Optional[int] = None - """Number of query and key heads for the gated delta net.""" - - linear_num_value_heads: Optional[int] = None - """Number of value and gate heads for the gated delta net.""" - - #################### - # attention variant: dsa - #################### - dsa_indexer_n_heads: Optional[int] = None - """Number of DSA indexer heads.""" - - dsa_indexer_head_dim: Optional[int] = None - """Dimension per DSA indexer head.""" - - dsa_indexer_topk: Optional[int] = None - """Number of top-k tokens to select in DSA indexer.""" - - dsa_indexer_loss_coeff: Optional[float] = None - """Coefficient for the DSA indexer KL divergence loss. Set to 0 to disable indexer loss.""" - - dsa_indexer_use_sparse_loss: Optional[bool] = None - """Whether to use sparse DSA indexer loss. If True, the indexer loss will be computed using the - top-k indices.""" - - #################### - # initialization - #################### - init_method: Optional[Callable] = None - """Method to initialize weights. Note that bias is always set to zero. Should be a function that - takes a single Tensor and initializes it. If None, will be set to - megatron.core.utils.init_method_normal(init_method_std) which is torch nn init normal with - mean=0.0 and std=init_method_std.""" - - output_layer_init_method: Optional[Callable] = None - """Method to initialize weights of the output layer of both attention and MLP blocks. If None, - will be set to megatron.core.utils.scaled_init_method_normal(init_method_std) which is torch nn - init normal with mean=0.0 and std=init_method_std / math.sqrt(2.0 * num_layers).""" - - init_method_std: float = 0.02 - """Standard deviation of the zero mean normal for the default initialization method, not used if - init_method and output_layer_init_method are provided.""" - - embedding_init_method: Optional[Callable] = None - """ - Method to initialize weights of the embedding layer. If None, will be set as described - in init_method above. - """ - - embedding_init_method_std: Optional[float] = None - """ - Standard deviation of the zero mean normal for the default initialization method for the - embedding layer. If None, will be set to init_method_std. - """ - - init_model_with_meta_device: bool = False - """ - If True, initializes the model with the meta device. This is helpful for - training of very large models. This feature is only works when megatron fsdp is turned on. - """ - - #################### - # mixed-precision - #################### - apply_query_key_layer_scaling: bool = False - """If true, scale Q * K^T by 1 / layer-number. This improve numeric stability when training with - fp16.""" - - attention_softmax_in_fp32: bool = True - """If True, run attention masking and softmax in fp32. This should be True if - apply_query_key_layer_scaling is True.""" - - disable_bf16_reduced_precision_matmul: bool = False - """If True, sets torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction=False to - prevent matmul from using reduced precision accumulation when using BF16.""" - - #################### - # fusion - #################### - bias_activation_fusion: bool = False - """If True, fuses bias addition and the activation function when possible.""" - - masked_softmax_fusion: bool = False - """If True, uses softmax fusion.""" - - persist_layer_norm: bool = False - """If True, uses the persistent fused layer norm kernel. This kernel only supports a fixed set - of hidden sizes.""" - - memory_efficient_layer_norm: bool = False - """If True, and using local layers (not from TransformerEngine), tells Apex to use the memory - efficient fused LayerNorm kernel. Ignored if not using LayerNorm.""" - - bias_dropout_fusion: bool = False # TODO: this should be bias_dropout_add_fusion? - """If True, uses bias dropout fusion.""" - - apply_rope_fusion: bool = False - """If True, use fused RoPE kernel.""" - - use_fused_weighted_squared_relu: bool = False - """If True, uses fused weighted squared relu kernel when using MoE.""" - - fused_single_qkv_rope: bool = False - """If set, avoid splitting QKV before ROPE forward and avoid concatenating ROPE dgrads.""" - - #################### - # activation recomputation - #################### - recompute_granularity: Optional[str] = None - """Determines which type of activation recompute to use. Megatron-core supports 'selective' - activation checkpointing where the submodules set in --recompute-modules is checkpointed. - The default is "core_attn" which is the memory intensive part of attention. - These memory intensive activations are also less compute intensive which makes activation - checkpointing more efficient for LLMs (20B+). See Reducing Activation Recomputation in Large - Transformer Models (https://arxiv.org/abs/2205.05198) for more details. 'full' will checkpoint - the entire transformer layer. If None, no recompute is performed and all activations are saved. - If set, must be 'selective' or 'full'. 'selective' always uses all layers. - """ - - recompute_method: Optional[str] = None - """Determines which transformer layers will be recomputed. uniform will uniformly divide the - total number of transformer layers in a transformer block and recompute the input activation of - each divided chunk at the specified granularity. block will recompute the input activations for - only a set number of transformer layers per pipeline stage. The rest of the layers in the - pipeline stage will not have any activations recomputed. If None, and recompute is enabled, all - layers will do recomputation. If set, must be 'uniform' or 'block'.""" - - recompute_num_layers: Optional[int] = None - """When recompute_method is uniform, recompute_num_layers is the number of transformer layers in - each uniformly divided recompute unit. When recompute_method is block, recompute_num_layers is - the number of transformer layers to recompute within each pipeline stage. Must be None for - 'selective' activation checkpointing.""" - - distribute_saved_activations: Optional[bool] = None - """If True, distribute recomputed activations across the model parallel group.""" - - recompute_modules: Optional[List[str]] = None - """The submodules to recompute. - choices: "core_attn", "moe_act", "layernorm", "mla_up_proj", "mlp", "moe", "shared_experts". - default: ["core_attn"]. - "core_attn": recompute the core attention part of the transformer layer. - "moe_act": recompute the MoE MLP activation function. - "layernorm": recompute the input_layernorm and pre_mlp_layernorm. - "mla_up_proj": recompute the MLA up projection and RoPE applying parts. - "mlp": recompute the dense MLP submodule. - "moe": recompute the MoE layer. - "shared_experts": recompute the shared experts in the MoE layer. - "moe_act", "layernorm", and "mla_up_proj" use output-discarding checkpointing, - "core_attn", "mlp", "moe", and "shared_experts" use normal checkpointing. - """ - - #################### - # fp8 related - #################### - fp8: Optional[str] = None - """If set, enables the use of FP8 precision through Transformer Engine. There are 2 predefined - choices (1) 'e4m3' uniformly uses e4m3 for all FP8 tensors, (2) 'hybrid' uses e4m3 for all FP8 - activation and weight tensors and e5m2 for all FP8 output activation gradient tensors.""" - - fp8_recipe: Optional[str] = "delayed" - """If set, enables the use of FP8 precision through Transformer Engine. There are 5 predefined - choices (1) 'tensorwise' uses per tensor current scaling recipe, (2) 'delayed' - uses delayed scaling recipe, 3) 'mxfp8' for Blackwell architecture only, - 4) 'blockwise' for blockwise scaling recipe, 5) 'custom' for custom quantization recipe.""" - - fp8_param: bool = False - """If set, keep the parameters in fp8 precision to save memory. This option must be used - together with fp8 mode (i.e., TransformerConfig.fp8 is not None). Note that not all parameters - will be converted to fp8; for example, biases will remain unchanged. The parameters affected are - primarily the weights of GEMMs. The specific parameters that will be converted to fp8 are - determined by TE.""" - - fp8_quantizer_factory: Optional[str] = None - """Python import path to a callable quantizer factory, e.g., package.module.quantizer_factory. - Required when fp8_recipe is custom.""" - - fp8_margin: int = 0 - """Margin for the scaling factor computation.""" - - fp8_interval: int = 1 - """DEPRECATED from TransformerEngine v1.8.0. This flag is ignored. - Controls how often the scaling factor is recomputed. - """ - - fp8_amax_history_len: int = 1 - """The length of the amax history window used for scaling factor computation.""" - - fp8_amax_compute_algo: str = "most_recent" - """Algorithm used for choosing the `amax` value for the scaling factor computation. There are 2 - predefined choices: `max` chooses the largest `amax` in the history window, while `most_recent` - always chooses the most recently seen value. - - """ - - fp8_wgrad: bool = True - """When set to False, override FP8 config options and do the wgrad computation - in higher precision.""" - - fp8_dot_product_attention: bool = False - """When set to True, use the FP8 implementation of Dot Product Attention.""" - - fp8_multi_head_attention: bool = False - """When set to True, use the FP8 implementation of Multi Head Attention.""" - - tp_only_amax_red: bool = False - """When set to True, reduce the FP8 AMAX only in the TP or TP-CP domain""" - - first_last_layers_bf16: bool = False - """If True, retains first and last N TransformerBlocks in BF16 as opposed to FP8.""" - - num_layers_at_start_in_bf16: int = 1 - """Number of layers at the start of the model to keep in BF16 precision when - first_last_layers_bf16 is True.""" - - num_layers_at_end_in_bf16: int = 1 - """Number of layers at the end of the model to keep in BF16 precision when - first_last_layers_bf16 is True.""" - - use_kitchen: bool = False - """Use the kitchen extension for transformer quantization.""" - - #################### - # fp4 related - #################### - fp4: Optional[str] = None - """If set, enables the use of FP4 precision through Transformer Engine. Currently only - supports 'nvfp4' which uses NVFP4BlockScaling recipe (requires TE >= 2.7.0.dev0).""" - - fp4_recipe: Optional[str] = "nvfp4" - """If set, enables the use of FP4 precision through Transformer Engine. Currently only - 'nvfp4' is supported which uses NVFP4BlockScaling recipe for Blackwell+ architecture.""" - - fp4_param: bool = False - """If set, keep the parameters in fp4 precision to save memory. This option must be used - together with fp4 mode (i.e., TransformerConfig.fp4 is not None). Note that not all parameters - will be converted to fp4; for example, biases will remain unchanged.""" - - fp4_quantizer_factory: Optional[str] = None - """Python import path to a callable quantizer factory, e.g., package.module.quantizer_factory. - Required when fp4_recipe is custom.""" - - #################### - # MoE related - #################### - moe_shared_expert_intermediate_size: Optional[int] = None - """Shared expert total ffn hidden size. - It should be equal to 'num_shared_experts * ffn_size_of_each_shared_expert' if - there are multiple shared experts. - None means no shared expert. - By default, the shared experts execute before the router. However, when - moe_shared_expert_overlap or overlap_moe_expert_parallel_comm is set, - the shared experts execute after the router, before the routed experts. - This makes the gradients from the router and the shared experts added in - different orders to the hidden_states, causing minor numerical differences - in the hidden_states gradient.""" - - moe_shared_expert_gate: bool = False - """Enable gate for shared expert.""" - - moe_shared_expert_overlap: bool = False - """Enable overlapping between shared expert computations and dispatcher communications. - Without this, the shared experts execute before the router.""" - - moe_layer_freq: Union[int, List[int]] = 1 - """Frequency between MoE layers and Dense layers. Accepts either: - - An integer N: Represents a 1:N ratio, meaning one expert layer for every N-1 dense layers. - - A list that defines a custom pattern, e.g.: [1,1,1,0,1,1,1,0,1,1,1,0]""" - - moe_ffn_hidden_size: Optional[int] = None - """MoE Feed-Forward Network hidden size""" - - moe_router_load_balancing_type: Union[str, List[str]] = "aux_loss" - """The load balancing strategy for the router. - Options: - - "aux_loss": Load balancing loss used in GShard and SwitchTransformer, calculated at - micro-batch level. - - "seq_aux_loss": Load balancing loss used in DeepSeekV2 and DeepSeekV3, computes loss - for each individual sample. - - "global_aux_loss": Load balancing loss calculated at global batch level. - - "sinkhorn": Balancing algorithm used in S-BASE. - - "none": No load balancing. - A list of strings can be provided to combine multiple aux-loss load balancing types. - The default is "aux_loss". - """ - - moe_router_topk: int = 2 - """Number of experts to route to for each token.""" - - moe_router_topk_limited_devices: Optional[int] = None - """Number of EP ranks to consider for each token in group-limited routing, - DEPRECATED and replaced by moe_router_num_groups and moe_router_group_topk. - """ - - moe_router_padding_for_quantization: Optional[bool] = False - """Whether to pad the routing_map to make sure the number of tokens each expert receives - is a multiple of 16/32 for quantized precision (e.g., FP8, FP4). This can remove the explicit - padding in the GroupedMLP layer.""" - - moe_router_padding_for_fp8: Optional[bool] = False - """[Compatibility alias for moe_router_padding_for_quantization] - Enabling this will also enable moe_router_padding_for_quantization.""" - - moe_router_num_groups: Optional[int] = None - """Number of groups to divide experts into for group-limited routing. - When using group-limited routing: - 1. Experts are divided into 'moe_router_num_groups' equal-sized groups - 2. For each token, 'moe_router_group_topk' groups are selected based on sum of - top-('moe_router_topk'/'moe_router_group_topk') routing scores within each group - 3. From these selected groups, 'moe_router_topk' individual experts are chosen - Two common use cases: - - Device-limited routing: Set 'moe_router_num_groups' equal to expert parallel size (EP) - to limit each token to experts on a subset of devices - (See DeepSeek-V2: https://arxiv.org/pdf/2405.04434) - - Node-limited routing: Set 'moe_router_num_groups' equal to number of nodes in EP group - to limit each token to experts on a subset of nodes - (See DeepSeek-V3: https://arxiv.org/pdf/2412.19437) - """ - - moe_router_group_topk: Optional[int] = None - """Number of selected groups for group-limited routing.""" - - moe_router_pre_softmax: bool = False - """Enable pre-softmax(pre-sigmoid) routing for MoE, which means softmax is before the - top-k selection. - By default, softmax is done after top-k.""" - - moe_router_topk_scaling_factor: Optional[float] = None - """Scaling factor for routing score in top-k selection, only works when moe_router_pre_softmax - enabled. Defaults to None, which means no scaling.""" - - moe_router_score_function: str = "softmax" - """Score function for MoE routing. Can be "softmax" or "sigmoid".""" - - moe_router_dtype: Optional[str] = None - """Data type for routing and expert output weighted averaging. Using fp32 or fp64 can - improve stability especially when the number of experts is large (e.g. finegrained-moe). - None means no changes for dtype.""" - - moe_router_enable_expert_bias: bool = False - """TopK routing with dynamic per-expert bias in the aux-loss-free load balancing strategy. - The routing decision is based on the sum of the routing scores and the expert bias. - See https://arxiv.org/abs/2408.15664 for details.""" - - moe_router_bias_update_rate: float = 1e-3 - """The expert bias is updated based on the number of assigned tokens to each expert - in a global batch, where the bias is increased for the experts with less assigned tokens - and decreased for the experts with more assigned tokens. - The default value 1e-3 is same as that used in DeepSeekV3.""" - - moe_router_force_load_balancing: bool = False - """[Experimental] Force load balancing with random logits for MoE router, supports naive topk - and group-limited topk. This is an experimental feature and only for benchmark.""" - - moe_grouped_gemm: bool = False - """When there are multiple experts per rank, compress multiple local (potentially small) gemms - in a single kernel launch to improve the utilization and performance by leveraging the Grouped - GEMM feature introduced since CUTLASS 2.8 (https://github.com/fanshiqing/grouped_gemm). - """ - - moe_use_legacy_grouped_gemm: bool = False - """Use legacy GroupedMLP rather than TEGroupedMLP. - Note: The legacy one will be deprecated soon.""" - - moe_aux_loss_coeff: Union[float, List[float]] = 0.0 - """Scaling coefficient for the aux loss. A starting value of 1e-2 is recommended. - If a list of load balancing types is provided for `moe_router_load_balancing_type`, - a corresponding list of coefficients should be provided here.""" - - moe_z_loss_coeff: Optional[float] = None # 1e-3 would be a good start value for z-loss - """Scaling coefficient for the z-loss. A starting value of 1e-3 is recommended.""" - - moe_input_jitter_eps: Optional[float] = None - """Add noise to the input tensor by applying jitter with a specified epsilon value.""" - - moe_token_dropping: bool = False - """This feature involves selectively dropping and padding tokens for each expert to achieve a - specified capacity, similar to GShard, Switch-Transformer, and DeepSpeed-MoE. Note that this is - currently unsupported so should remain False.""" - - moe_token_dispatcher_type: str = "allgather" - """The type of token dispatcher to use. The default is 'allgather'. - Options are 'allgather','alltoall' and 'flex'.""" - - moe_enable_deepep: bool = False - """[Experimental] Enable DeepEP for efficient token dispatching and combine in MoE models.""" - - moe_flex_dispatcher_backend: str = "deepep" - """[Experimental] The backend to use for flex token dispatcher. The default is "deepep". - Options are "deepep" and "hybridep". Currently only "hybridep" backend supports - the MNNVL case.""" - - moe_per_layer_logging: bool = False - """Enable per-layer logging for MoE, currently supports auxiliary loss and z loss.""" - - moe_expert_capacity_factor: Optional[float] = None - """moe_expert_capacity_factor (float): The capacity factor for each expert, None means no token - will be dropped. The default is None.""" - - moe_pad_expert_input_to_capacity: bool = False - """moe_pad_expert_input_to_capacity (bool): If True, pads the input for each expert to match - the expert capacity length, effective only after the moe_expert_capacity_factor is set. The - default setting is False.""" - - moe_token_drop_policy: str = "probs" - """The policy to drop tokens. Can be either "probs" or "position". If "probs", the tokens with - the lowest probabilities will be dropped. If "position", tokens at the end of each batch will - be dropped. - """ - - moe_layer_recompute: bool = False - """Memory optimization: checkpointing moe_layer to save actiavtion memory.""" - - moe_permute_fusion: bool = False - """Fuse token rearrangement ops during token dispatching.""" - - moe_router_fusion: bool = False - """Fuse ops in routing and aux loss calculation.""" - - moe_apply_probs_on_input: bool = False - """Apply probs on input of experts instead of applying after activation and glu.""" - - ################## - # Context Parallel - ################## - cp_comm_type: Optional[Union[str, List[str]]] = None - """Inter-gpu communication type for context parallelism. - str: all layers share same communication type. - List[str]: each layer has its separate communication type. - cp_comm_type of each layer can be "p2p" or "all_gather" or "a2a" or "a2a+p2p". - "p2p": Exchange KV chunks with P2P communications in ring topology. P2P is async and can be - overlapped with attention compute. - "all_gather": All-gather to get full sequence of KV before attention. The all-gather is not - async, and cannot be overlapped. - "a2a": Like DeepSpeed Ulysses, scatter attention heads across the CP group, and gather to get - full sequence of QKV. - "a2a+p2p": A hierarchical implementation of context parallelism to attention. - It uses A2A communications in low-level CP groups (e.g., via NVLink), - and P2P communications in high-level CP groups (e.g., via IBLink). - """ - - ################## - # Cuda Graphs - ################## - enable_cuda_graph: bool = False - """DEPRECATED and replaced by cuda_graph_impl. - When set to true, either partial CUDA graph (1/many CUDA graph per layer) or full iteration - CUDA graph (1 CUDA graph for whole iteration excluding optimizer) is enabled. --cuda-graph-scope - determines the scope of graph capture.""" - - cuda_graph_use_single_mempool: bool = False - """When set to true, cudagraphs will be captured inside a single mempool, in which all - cudagraphs may only be used once per step. If false, cudagraphs may be reused across - microbatches. Enabling may reduce cudagraph memory overheads due to memory fragmentation, - however may greatly increase the number of cudagraphs created when the number of microbatches - is high.""" - - cuda_graph_retain_backward_graph: bool = False - """When set to true, cudagraph backward passes will be graph captured with 'retain_grad=True' - This may enable cudagraphs for certain modules that are not completely cudagraph safe. For - more details, see: https://pytorch.org/docs/stable/generated/torch.Tensor.backward.html.""" - - cuda_graph_warmup_steps: int = 3 - """Number of warmup steps for CUDA graphs""" - - external_cuda_graph: bool = False - """DEPRECATED and replaced by cuda_graph_impl. - When set to true, TransformerLayer layers are swapped with user provided CUDA graphs.""" - - cuda_graph_impl: str = "none" - """Determines the CUDA graph capture implementation. - "none": no CUDA graph. - "local": capture the CUDA graph using MCore local implementation. Either partial CUDA graph - (1/many CUDA graph per layer) or full iteration CUDA graph (1 CUDA graph for whole iteration - excluding optimizer) is enabled. - "transformer_engine": capture the CUDA graph using TE make_graphed_callables().""" - - cuda_graph_scope: Optional[List[CudaGraphScope]] = None - """Determines the CUDA graphs capturing scope. - When cuda_graph_impl is set to "transformer_engine", valid values are "attn", "mlp", "moe", - "moe_router", "moe_preprocess", "mamba". None means the full layer. - When cuda_graph_impl is set to "local", "full_iteration" can be specified as cuda_graph_scope - to enable whole iteration CUDA graph. All other values enable layerwise CUDA graph.""" - - #################### - # miscellaneous - #################### - clone_scatter_output_in_embedding: bool = True - """When set to True, clone the output of scatter_to_sequence_parallel_region in embedding layer - to facilitate garbage collection of input.""" - - disable_parameter_transpose_cache: bool = False - """When set to true, the parameter transposes are not cached for subsequent iterations.""" - - config_logger_dir: str = "" - """When non-empty, dumps entry-point configs to config_logger_dir""" - - flash_decode: bool = False - """ Use the optimized flash decoding kernel during inference. """ - - use_te_activation_func: bool = False - """Whether to use ffn activation functions implemented by TransformerEngine""" - - use_te_rng_tracker: bool = False - """ Whether to use the TE or MCore version of the RNG tracker. """ - - inference_rng_tracker: bool = False - """ Whether we should instantiate a separate RNG tracker for inference. """ - - inference_sampling_seed: int = 42 - """ Random seed to use for sampling during inference. """ - - symmetric_ar_type: Optional[str] = None - """Type of symmetric all reduce to use""" - - mrope_section: Optional[List[int]] = None - """ Multimodal rope section is for channel dimension of temporal, height and width - in rope calculation. """ - - is_hybrid_model: bool = False - """ Indicates whether this is a hybrid model. """ - - mamba_state_dim: int = 128 - """The dimensionality of the state representation in Mamba layers.""" - - mamba_head_dim: int = 64 - """The dimensionality of the heads in the Mamba layers.""" - - mamba_num_groups: int = 8 - """The number of groups used in Mamba layers.""" - - mamba_num_heads: Optional[int] = None - """The number of heads used in Mamba layers. - If None, the number of heads will be hidden_size * expand // mamba_head_dim.""" - - use_mamba_mem_eff_path: bool = True - """If True, use the memory efficient path for Mamba layers.""" - - mlp_chunks_for_prefill: int = 1 - """The number of chunks along the sequence dimension to use for MLP computation - during prefill.""" - - heterogeneous_block_specs: bool = False - """Whether to use heterogeneous block specs (nemotron-nas architecture).""" - - hetereogenous_dist_checkpoint: bool = False - """Whether to use heterogenous layers in distributed checkpoint.""" - - #################### - # Quantization - #################### - quant_recipe: Optional[RecipeConfig] = None - """Configuration of any quantization to be applied to the model""" - - transformer_impl: str = "transformer_engine" - """Transformer implementation to use. - Options are 'transformer_engine' for Transformer Engine and 'local' for MCore.""" - - fallback_to_eager_attn: bool = False - """Whether to fallback to eager attention in TE implementation. - Suggested for when desired features are not available in TE implementation.""" - - ##################################### - # Fine-grained Activation Offloading - ##################################### - fine_grained_activation_offloading: bool = False - """If True, offload the input of the specified modules to the CPU. - Fine-grained activation offloading is a module-level offloading method - instead of a layer-level offloading method like cpu_offloading.""" - - offload_modules: Optional[list[str]] = None - """The submodules to offload its input. - choices: "attn_norm", "qkv_linear", "core_attn", "attn_proj", - "mlp_norm", "expert_fc1", "moe_act". - "attn_norm": offload the input of the normalization in the attention part. - "qkv_linear": offload the input of the qkv linear part. - "core_attn": offload the input of the core attention part. - "attn_proj": offload the input of the attn linear projection part. - "mlp_norm": offload the input of the normalization in the mlp part. - "expert_fc1": offload the input of the expert fc1 part. - "moe_act": offload the input of the moe act part. - """ - min_offloaded_tensor_size: int = 1024 * 1024 - """The minimum size of the tensor to be offloaded.""" - - def __post_init__(self): - """Python dataclass method that is used to modify attributes after initialization. - See https://docs.python.org/3/library/dataclasses.html#post-init-processing for more - details. - """ - super().__post_init__() - if self.fp16 and self.bf16: - raise ValueError( - f"Only one of self.fp16: {self.fp16} and self.bf16 {self.bf16} should be True." - ) - - # Apply BF16 matmul precision setting if needed - if self.bf16 and self.disable_bf16_reduced_precision_matmul: - torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = False - - if self.num_attention_heads % self.tensor_model_parallel_size != 0: - raise ValueError( - f"num_attention_heads ({self.num_attention_heads}) must be a multiple of " - f"tensor_model_parallel_size ({self.tensor_model_parallel_size})." - ) - - if self.ffn_hidden_size is None: - self.ffn_hidden_size = 4 * self.hidden_size - - if self.kv_channels is None: - self.kv_channels = self.hidden_size // self.num_attention_heads - - if self.num_query_groups is None: - self.num_query_groups = self.num_attention_heads - - if self.num_query_groups % self.tensor_model_parallel_size != 0: - raise ValueError( - f"num_query_groups ({self.num_query_groups}) must be a multiple of " - f"tensor_model_parallel_size ({self.tensor_model_parallel_size})." - ) - - if self.experimental_attention_variant in ["gated_delta_net"]: - assert ( - self.linear_attention_freq is not None - ), f"linear_attention_freq must be set for linear attention." - - if self.experimental_attention_variant == "gated_delta_net": - # Check required parameters - assert ( - self.linear_conv_kernel_dim is not None - ), "linear_conv_kernel_dim must be set for gated delta net." - assert ( - self.linear_key_head_dim is not None - ), "linear_key_head_dim must be set for gated delta net." - assert ( - self.linear_value_head_dim is not None - ), "linear_value_head_dim must be set for gated delta net." - assert ( - self.linear_num_key_heads is not None - ), "linear_num_key_heads must be set for gated delta net." - assert ( - self.linear_num_value_heads is not None - ), "linear_num_value_heads must be set for gated delta net." - assert self.linear_num_value_heads % self.linear_num_key_heads == 0, ( - f"linear_num_value_heads ({self.linear_num_value_heads}) must be a multiple of " - f"linear_num_key_heads ({self.linear_num_key_heads})." - ) - - # Check tensor parallelism compatibility - assert ( - self.linear_num_key_heads % self.tensor_model_parallel_size == 0 - ), "linear_num_key_heads must be a multiple of tensor_model_parallel_size." - assert ( - self.linear_num_value_heads % self.tensor_model_parallel_size == 0 - ), "linear_num_value_heads must be a multiple of tensor_model_parallel_size." - - # Do not support yet, but coming soon. - assert self.context_parallel_size == 1, ( - f"Gated delta net does not support context parallel for now," - f" but got {self.context_parallel_size=}." - ) - elif self.experimental_attention_variant == "dsa": - # assert ( - # self.context_parallel_size == 1 - # ), "Currently context parallelism is not supported by DSAttention!" - assert not self.apply_rope_fusion, "RoPE fusion is not supported for DSAttention" - - if self.fp8: - # cannot support first last layer bf16 with delayed scaling - if self.first_last_layers_bf16 and self.fp8_recipe == Fp8Recipe.delayed: - raise ValueError("Delayed scaling does not support first / last layer in BF16.") - - # max bf16 layers per pipeline stage - max_bf16_layers_per_pipeline_stage = ( - self.num_layers // self.pipeline_model_parallel_size - ) - - # check start/end bf16 layer counts are valid - if self.first_last_layers_bf16: - if ( - self.num_layers_at_start_in_bf16 < 0 - or self.num_layers_at_start_in_bf16 > max_bf16_layers_per_pipeline_stage - ): - raise ValueError( - f"num_layers_at_start_in_bf16 ({self.num_layers_at_start_in_bf16}) must be " - f"between 0 and number of layers per pipeline stage " - f"({max_bf16_layers_per_pipeline_stage})." - ) - if ( - self.num_layers_at_end_in_bf16 < 0 - or self.num_layers_at_end_in_bf16 > max_bf16_layers_per_pipeline_stage - ): - raise ValueError( - f"num_layers_at_end_in_bf16 ({self.num_layers_at_end_in_bf16}) must be " - f"between 0 and number of layers per pipeline stage " - f"({max_bf16_layers_per_pipeline_stage})." - ) - - if self.fp8_recipe == Fp8Recipe.custom: - if not self.fp8_quantizer_factory: - raise ValueError( - "fp8_quantizer_factory must be provided when fp8_recipe is 'custom'. " - "Specify a Python import path (e.g., package.module.quantizer_factory) " - "via --fp8-quantizer-factory." - ) - - if self.fp8_param and not self.fp8: - raise ValueError("fp8_param must be used together with fp8 mode.") - - # FP4 validation - if self.fp4_param and not self.fp4: - raise ValueError("fp4_param must be used together with fp4 mode.") - - if self.fp4 and self.fp8: - raise ValueError("fp4 and fp8 cannot be used simultaneously. Please choose one.") - - if self.fp4 and self.fp4_recipe == Fp4Recipe.custom: - if not self.fp4_quantizer_factory: - raise ValueError( - "fp4_quantizer_factory must be provided when fp4_recipe is 'custom'. " - "Specify a Python import path (e.g., package.module.quantizer_factory) " - "via --fp4-quantizer-factory." - ) - - if self.apply_query_key_layer_scaling: - self.attention_softmax_in_fp32 = True - - if self.expert_model_parallel_size > 1 and self.num_moe_experts is None: - raise ValueError("num_moe_experts must be non None to use expert-parallel.") - - if self.num_moe_experts is not None and self.num_moe_experts <= 0: - raise ValueError("num_moe_experts must be non-negative.") - - if self.num_moe_experts is not None and self.moe_ffn_hidden_size is None: - self.moe_ffn_hidden_size = self.ffn_hidden_size - warnings.warn("moe_ffn_hidden_size is not set, using ffn_hidden_size instead.") - - if self.num_moe_experts is None: - assert ( - self.moe_ffn_hidden_size is None - ), "moe_ffn_hidden_size must be None when num_experts is not set." - - if self.moe_enable_deepep: - if self.moe_token_dispatcher_type != "flex": - raise ValueError("DeepEP backend is only supported with flex token dispatcher.") - if self.moe_flex_dispatcher_backend == "hybridep": - raise ValueError("Only one backend is supported for flex token dispatcher.") - self.moe_flex_dispatcher_backend = "deepep" - warnings.warn( - "moe_enable_deepep is deprecated." - "Please use --moe-flex-dispatcher-backend=deepep instead." - ) - - if self.moe_token_dispatcher_type == "flex": - if self.moe_pad_expert_input_to_capacity and ( - self.moe_enable_deepep or self.moe_flex_dispatcher_backend == "deepep" - ): - raise ValueError( - "Flex token dispatcher with deepep backend does not support " - "moe_pad_expert_input_to_capacity" - ) - - if self.moe_shared_expert_intermediate_size is not None: - if self.moe_shared_expert_intermediate_size <= 0: - raise ValueError( - f"moe_shared_expert_intermediate_size must be " - f"num_shared_experts * ffn_size_of_each_shared_expert, " - f"but got {self.moe_shared_expert_intermediate_size}" - ) - if self.moe_shared_expert_overlap and self.moe_token_dispatcher_type not in [ - "alltoall" - ]: - raise ValueError( - f"moe_shared_expert_overlap only works with alltoall token dispatcher." - ) - - if isinstance(self.moe_router_load_balancing_type, list): - assert isinstance(self.moe_aux_loss_coeff, list) and len( - self.moe_aux_loss_coeff - ) == len(self.moe_router_load_balancing_type), ( - "moe_aux_loss_coeff must be a list of the same length as " - "moe_router_load_balancing_type" - ) - - if self.moe_expert_capacity_factor is not None: - if self.moe_expert_capacity_factor < 0: - self.moe_expert_capacity_factor = None - if isinstance(self.moe_router_load_balancing_type, list): - for load_balancing_type in self.moe_router_load_balancing_type: - if load_balancing_type not in [ - "aux_loss", - "seq_aux_loss", - "global_aux_loss", - "none", - ]: - raise ValueError( - "moe_expert_capacity_factor only works with aux_loss, " - "seq_aux_loss, global_aux_loss or none load balancing" - ) - elif self.moe_router_load_balancing_type not in [ - "aux_loss", - "seq_aux_loss", - "global_aux_loss", - "none", - ]: - raise ValueError( - "moe_expert_capacity_factor only works with aux_loss, " - "seq_aux_loss, global_aux_loss or none load balancing" - ) - - if self.moe_pad_expert_input_to_capacity: - if self.moe_expert_capacity_factor is None: - raise ValueError( - "moe_expert_capacity_factor must be set to use moe_pad_expert_input_to_capacity" - ) - - if self.cpu_offloading and ( - self.cpu_offloading_num_layers < 0 or self.cpu_offloading_num_layers >= self.num_layers - ): - raise ValueError( - f"CPU offloading can be done only for layers less than {self.num_layers}" - ) - - if self.cpu_offloading and self.pipeline_model_parallel_size > 1: - raise ValueError( - "Currently there is no support for Pipeline parallelism with CPU offloading" - ) - - if self.cpu_offloading and self.recompute_granularity is not None: - raise ValueError( - "CPU offloading does not work when activation recomputation is enabled" - ) - - if self.recompute_granularity is not None: - if self.recompute_granularity not in ["full", "selective"]: - raise ValueError( - f'When using recompute_granuarlity: {self.recompute_granularity} must be "full"' - 'or "selective".' - ) - - if self.recompute_method is not None: - if self.recompute_method not in ["block", "uniform"]: - raise ValueError( - f'recompute_method: {self.recompute_method} must be "block" or "uniform".' - ) - elif self.recompute_granularity != "selective": - raise ValueError( - f"Using recompute_granularity: {self.recompute_granularity} so " - 'recompute_method must be "block" or "uniform"' - ) - - if self.recompute_granularity != "selective" and self.recompute_num_layers is None: - raise ValueError( - f"When using recompute_granularity: {self.recompute_granularity} " - "recompute_num_layers must be between " - "1 and num_layers_per_pipeline_rank: " - f"{self.num_layers // self.pipeline_model_parallel_size}" - ) - elif ( - self.recompute_granularity == "selective" and self.recompute_num_layers is not None - ): - raise ValueError( - f"When using recompute_granularity: {self.recompute_granularity} " - "recompute_num_layers must be None." - ) - - if self.distribute_saved_activations and self.sequence_parallel: - raise ValueError( - f"distribute_saved_activations: {self.distribute_saved_activations} must be " - f"false when sequence parallel is enabled: {self.sequence_parallel}" - ) - - if self.recompute_modules is None: - self.recompute_modules = ["core_attn"] - - if self.recompute_granularity == "selective": - if len(self.recompute_modules) > 0: - allowed_modules = { - "core_attn", - "moe_act", - "layernorm", - "mla_up_proj", - "mlp", - "moe", - "shared_experts", - } - invalid_modules = set(self.recompute_modules) - allowed_modules - assert not invalid_modules, ( - f"Invalid choices for recompute_modules: {invalid_modules}. " - f"Allowed modules are: {allowed_modules}" - ) - - if "moe_act" in self.recompute_modules and not self.moe_grouped_gemm: - raise ValueError( - "moe_act in recompute_modules is only supported with moe_grouped_gemm." - ) - - if "mla_up_proj" in self.recompute_modules and not self.multi_latent_attention: - raise ValueError( - "mla_up_proj in recompute_modules is only supported with " - "multi_latent_attention." - ) - - if "core_attn" in self.recompute_modules: - warnings.warn( - "If you are using transformer_engine as the transformer implementation, " - "the core_attn is from transformer_engine and may be the fused version. " - "For fused attention, you have no need to set 'core_attn' to recompute. " - "Please check that the core_attn recompute is really needed." - ) - - if "shared_experts" in self.recompute_modules: - if ( - self.moe_shared_expert_intermediate_size is not None - and self.moe_shared_expert_overlap - ): - raise ValueError( - "shared_experts recompute cannot work with --moe-shared-expert-overlap." - ) - - if self.fp8: - if "moe_act" in self.recompute_modules or "layernorm" in self.recompute_modules: - if self.fp8_recipe == 'delayed': - raise ValueError( - "Delayed scaling does not support moe_act and layernorm recompute " - "for fp8." - ) - if not is_te_min_version("2.6.0dev0"): - raise ValueError( - "moe_act and layernorm recompute for fp8 needs " - "transformer-engine>=2.6.0dev0, " - f"but your version is {get_te_version()}." - ) - - if self.moe_layer_recompute: - warnings.warn( - "--moe-layer-recompute is deprecated. " - "Use --recompute-granularity selective --recompute-modules moe_layer instead." - ) - if self.recompute_granularity == "full": - raise ValueError( - "Do not set --moe-layer-recompute with full recompute granularity. " - ) - self.recompute_granularity = "selective" - if "moe" not in self.recompute_modules: - self.recompute_modules.append("moe") - - if self.fine_grained_activation_offloading: - assert ( - not self.cpu_offloading - ), "fine_grained_activation_offloading cannot be enabled with cpu_offloading." - assert self.offload_modules is not None and len(self.offload_modules) > 0 - allowed_modules = { - "core_attn", - "attn_proj", - "expert_fc1", - "moe_act", - "attn_norm", - "mlp_norm", - "qkv_linear", - } - invalid_modules = set(self.offload_modules) - allowed_modules - assert not invalid_modules, ( - f'Invalid choices for offload_modules: {invalid_modules}. ' - f'Allowed modules are: {allowed_modules}' - ) - if "attn_proj" in self.offload_modules and "core_attn" not in self.offload_modules: - raise ValueError( - "attn_proj cannot be set to offload_modules alone without core_attn " - "because the input of attn_proj is the output of core_attn, " - "which is needed in core_attn.backward()." - ) - - if ( - self.num_layers_in_first_pipeline_stage is not None - or self.num_layers_in_last_pipeline_stage is not None - ) and ( - self.account_for_embedding_in_pipeline_split or self.account_for_loss_in_pipeline_split - ): - raise ValueError( - "num_layers_in_first_pipeline_stage and num_layers_in_last_pipeline_stage cannot be" - "set at the same time with account_for_embedding_in_pipeline_split" - "and account_for_loss_in_pipeline_split" - ) - - # PP layout - if self.pipeline_model_parallel_layout is not None: - # If pipeline layout is set, we will check the conflicts - # with other pipeline layout arguments. - any_conflict = ( - self.num_layers_in_first_pipeline_stage is not None - or self.num_layers_in_last_pipeline_stage is not None - or self.account_for_embedding_in_pipeline_split - or self.account_for_loss_in_pipeline_split - ) - if any_conflict: - raise ValueError( - "pipeline_model_parallel_layout cannot be set" - " with other pipeline layout arguments." - f" {self.num_layers_in_first_pipeline_stage=}," - f" {self.num_layers_in_last_pipeline_stage=}," - f" {self.account_for_embedding_in_pipeline_split=}," - f" {self.account_for_loss_in_pipeline_split=}." - ) - - # Transfer pipeline_model_parallel_layout from str or list to - # PipelineParallelLayerLayout - if isinstance(self.pipeline_model_parallel_layout, str): - self.pipeline_model_parallel_layout = PipelineParallelLayerLayout.from_str( - layout=self.pipeline_model_parallel_layout, - pipeline_model_parallel_size=self.pipeline_model_parallel_size, - ) - elif isinstance(self.pipeline_model_parallel_layout, list): - # Since list is not hashable, the initialization will not be cached. - self.pipeline_model_parallel_layout = PipelineParallelLayerLayout( - layout=self.pipeline_model_parallel_layout, - pipeline_model_parallel_size=self.pipeline_model_parallel_size, - ) - - # Check whether the input VPP size conflicts with the PP layout - detected_vpp_size = ( - self.pipeline_model_parallel_layout.virtual_pipeline_model_parallel_size - ) - if self.virtual_pipeline_model_parallel_size is not None: - assert self.virtual_pipeline_model_parallel_size == detected_vpp_size, ( - f"virtual_pipeline_model_parallel_size conflicts with" - f" pipeline_model_parallel_layout," - f" ({self.virtual_pipeline_model_parallel_size=}, " - f" {detected_vpp_size=})" - ) - elif detected_vpp_size > 1: - self.virtual_pipeline_model_parallel_size = detected_vpp_size - - # Check whether the layout is valid. - self.mtp_standalone = self.pipeline_model_parallel_layout.validate_layer_layout( - num_layers=self.num_layers, mtp_num_layers=self.mtp_num_layers - ) - - # Uneven PP - elif ( - self.num_layers_in_first_pipeline_stage is not None - or self.num_layers_in_last_pipeline_stage is not None - ): - pipeline_parallel_size = self.pipeline_model_parallel_size - num_layers = self.num_layers - - if self.num_layers_in_first_pipeline_stage is not None: - if self.num_layers_in_first_pipeline_stage <= 0: - raise ValueError("num_layers_in_first_pipeline_stage must be larger than 0") - - if self.virtual_pipeline_model_parallel_size is not None: - if ( - self.num_layers_in_first_pipeline_stage - % self.virtual_pipeline_model_parallel_size - != 0 - ): - raise ValueError( - f"number of layers at first stage: " - f"{self.num_layers_in_first_pipeline_stage}" - f"must be divisible by virtual pipeline" - f"parallel degree {self.virtual_pipeline_model_parallel_size}" - ) - num_layers -= self.num_layers_in_first_pipeline_stage - pipeline_parallel_size -= 1 - - if self.num_layers_in_last_pipeline_stage is not None: - if self.num_layers_in_last_pipeline_stage <= 0: - raise ValueError("num_layers_in_last_pipeline_stage must be larger than 0") - - if self.virtual_pipeline_model_parallel_size is not None: - if ( - self.num_layers_in_last_pipeline_stage - % self.virtual_pipeline_model_parallel_size - != 0 - ): - raise ValueError( - f"number of layers at last stage: " - f"{self.num_layers_in_last_pipeline_stage}" - f"must be divisible by virtual pipeline" - f"parallel degree {self.virtual_pipeline_model_parallel_size}" - ) - num_layers -= self.num_layers_in_last_pipeline_stage - pipeline_parallel_size -= 1 - - # Here pipeline_parallel_size is the number of middle PP stages. If there are middle - # PP stages, check number of layers at middle stage is divisible by middle PP size. - if pipeline_parallel_size and not num_layers % pipeline_parallel_size == 0: - raise ValueError( - f"number of layers at middle stage: {num_layers} must be divisible by" - f"the middle pipeline model parallel size {pipeline_parallel_size}" - ) - - # If there are middle PP stages, check number of layers - # on each middle PP rank is divisible by VPP size. - if pipeline_parallel_size and self.virtual_pipeline_model_parallel_size is not None: - num_layers_per_middle_pipeline_rank = num_layers // pipeline_parallel_size - if ( - not num_layers_per_middle_pipeline_rank - % self.virtual_pipeline_model_parallel_size - == 0 - ): - raise ValueError( - f"number of layers on each middle pipeline rank:" - f"{num_layers_per_middle_pipeline_rank} must be divisible by virtual" - f"pipeline parallel degree {self.virtual_pipeline_model_parallel_size}" - ) - - elif ( - self.account_for_embedding_in_pipeline_split or self.account_for_loss_in_pipeline_split - ): - if self.virtual_pipeline_model_parallel_size is None: - num_layers = self.num_layers - - if self.account_for_embedding_in_pipeline_split: - num_layers += 1 - - if self.account_for_loss_in_pipeline_split: - num_layers += 1 - - if not num_layers % self.pipeline_model_parallel_size == 0: - raise ValueError( - f"number of middle layers: {num_layers} must be divisible by " - f"middle pipeline_model_parallel_size {self.pipeline_model_parallel_size}" - ) - else: - num_layers = self.num_layers - if self.account_for_embedding_in_pipeline_split: - num_layers += 1 - - if self.account_for_loss_in_pipeline_split: - num_layers += 1 - - if not num_layers % self.pipeline_model_parallel_size == 0: - raise ValueError( - f"num_layers: {num_layers} after enable" - f"account_for_embedding_in_pipeline_split or " - f"account_for_loss_in_pipeline_split must be divisible" - f"by pipeline_model_parallel_size " - f"{self.pipeline_model_parallel_size}" - ) - - num_layers_per_pipeline_rank = num_layers // self.pipeline_model_parallel_size - if ( - not num_layers_per_pipeline_rank % self.virtual_pipeline_model_parallel_size - == 0 - ): - raise ValueError( - f"number of layers on each pipeline rank: {num_layers_per_pipeline_rank}" - f"(after enable account_for_embedding_in_pipeline_split or " - f"account_for_loss_in_pipeline_split) must be divisible by" - f"virtual_pipeline_model_parallel_size" - f"{self.virtual_pipeline_model_parallel_size}" - ) - - if self.apply_query_key_layer_scaling: - self.attention_softmax_in_fp32 = True - - if self.bias_activation_fusion: - if self.activation_func not in [F.gelu, F.silu, quick_gelu]: - raise ValueError( - "When bias_activation_fusion is True, activation function should be either " - "gelu, swiglu, or quick_geglu" - ) - if ( - self.activation_func == F.gelu - and not self.gated_linear_unit - and not self.add_bias_linear - ): - raise ValueError( - "When bias_activation_fusion is True, gated_linear_unit is False " - "and activation function is gelu, add_bias_linear must also be True." - ) - if self.activation_func == quick_gelu and not self.gated_linear_unit: - raise ValueError( - "When bias_activation_fusion is True and activation function is quick_gelu, " - "gated_linear_unit must be True." - ) - if self.glu_linear_offset != 0.0 and self.activation_func != quick_gelu: - raise ValueError( - "When bias_activation_fusion is True and glu_linear_offset is non-zero, " - "activation function must be quick_gelu." - ) - - if self.use_te_activation_func: - raise ValueError( - "bias_activation_fusion and use_te_activation_func cannot be both true. " - "If you use bias in MLP FC1, we recommend setting bias_activation_fusion " - "to True and use_te_activation_func to False." - ) - - if self.use_te_activation_func: - if self.activation_func not in (F.gelu, F.silu, F.relu): - raise ValueError( - "TransformerEngine only support gelu, geglu, silu, swiglu, relu, reglu. " - "If you don't want to use TransformerEngine activation function, set " - "use_te_activation_func to False" - ) - - if self.activation_func_fp8_input_store: - if self.activation_func != F.silu or not self.gated_linear_unit: - raise ValueError("Storing activation input in FP8 is supported only for SwiGLU.") - - if self.apply_rope_fusion: - if self.multi_latent_attention: - warnings.warn( - "apply_rope_fusion for multi-latent attention only supports training. " - "It is experimental and may change in future versions." - ) - else: - if self.rotary_interleaved: - if not is_te_min_version("2.3.0"): - raise ValueError( - "rotary_interleaved does not work with apply_rope_fusion for " - "TE < 2.3.0. Please install TE >= 2.3.0" - ) - - from megatron.core.models.common.embeddings.rope_utils import ( - fused_apply_rotary_pos_emb, - fused_apply_rotary_pos_emb_thd, - ) - - if fused_apply_rotary_pos_emb is None and fused_apply_rotary_pos_emb_thd is None: - raise ValueError( - "apply_rope_fusion is not available. Please install TE >= 1.4." - ) - - if self.fused_single_qkv_rope: - if self.attention_output_gate: - raise ValueError("fused_single_qkv_rope does not support gated attention for now.") - - if self.multi_latent_attention and self.rotary_interleaved: - raise ValueError("rotary_interleaved does not work with multi_latent_attention.") - - # Set the embedding init method - if self.embedding_init_method_std is None: - # By default, use the same init std as you use for every other non-output layer. - self.embedding_init_method_std = self.init_method_std - - if self.embedding_init_method is None: - if self.init_method is None or (self.embedding_init_method_std != self.init_method_std): - # In this case, we set both the init method and the embedding init method to - # whatever std value requested (or defaulted) for the embedding_init_layer - self.embedding_init_method = init_method_normal(self.embedding_init_method_std) - else: - # Replicate the current behavior where if you are not changing the std of the - # embedding init differently and the init method is set, we fallback to the - # init method for this layer. Since we are here after an OR we know that - # init_method is not None - self.embedding_init_method = self.init_method - - if self.init_method is None: - self.init_method = init_method_normal(self.init_method_std) - - if self.output_layer_init_method is None: - self.output_layer_init_method = scaled_init_method_normal( - self.init_method_std, - self.num_layers, - multiplier=2.0 if not self.is_hybrid_model else 1.0, - ) - - if self.num_moe_experts is not None and self.add_bias_linear: - assert ( - self.expert_tensor_parallel_size == 1 - ), "Bias in Moe is only supported when ETP==1" - - if self.moe_router_enable_expert_bias and self.moe_router_score_function != "sigmoid": - raise ValueError( - "Expert bias for aux-loss-free routing only supports sigmoid score function." - "Please set --moe-router-score-function sigmoid for sigmoid score function." - ) - - if self.num_moe_experts and self.fp8: - # TE version below 1.7.0 will raise Error when handle zeros tokens for expert - if not is_te_min_version("1.7.0.dev0"): - raise ValueError( - "Only transformer-engine>=1.7.0 supports MoE FP8 training, " - f"but your version is {get_te_version()}." - ) - - if self.moe_grouped_gemm and not is_te_min_version("1.11.0"): - raise ValueError( - "Only transformer-engine>=1.11.0 supports FP8 grouped gemm, " - f"but your version is {get_te_version()}." - ) - - if self.moe_router_padding_for_fp8: - # enable moe_router_padding_for_quantization - warnings.warn( - "--moe-router-padding-for-fp8 is going to be deprecated. " - "Use --moe-router-padding-for-quantization instead." - ) - self.moe_router_padding_for_quantization = True - - if self.moe_router_padding_for_quantization: - if self.fp8 is None and self.fp4 is None: - raise ValueError( - "fp8/fp4 must be specified when moe_router_padding_for_quantization is True." - ) - - if self.moe_token_dispatcher_type in ["allgather", "alltoall_seq"]: - raise ValueError( - "allgather and alltoall_seq dispatcher does not support " - "moe_router_padding_for_quantization." - ) - - if ( - self.moe_router_topk == 1 - and self.moe_router_score_function == "softmax" - and not self.moe_router_pre_softmax - and self.moe_router_load_balancing_type != "sinkhorn" - ): - # Requires applying softmax before selecting the top-k when k is 1, - # since softmax on a [num_tokens, 1] would yield a zero gradient. - raise ValueError("Please use --moe-router-pre-softmax when topk is 1.") - - if self.moe_router_group_topk: - if self.moe_router_topk_limited_devices: - raise ValueError( - "moe_router_topk_limited_devices is deprecated and replaced by " - "moe_router_group_topk and moe_router_num_groups." - ) - if not self.moe_router_num_groups: - raise ValueError( - "When using group limited routing, moe_router_num_groups must be specified." - ) - else: - assert self.num_moe_experts % self.moe_router_num_groups == 0, ( - f"num_moe_experts ({self.num_moe_experts}) should be divisible by " - f"moe_router_num_groups ({self.moe_router_num_groups})." - ) - assert self.moe_router_group_topk <= self.moe_router_num_groups, ( - f"moe_router_group_topk ({self.moe_router_group_topk}) should be smaller than " - f"moe_router_num_groups ({self.moe_router_num_groups})." - ) - elif self.moe_router_topk_limited_devices: - warnings.warn( - "moe_router_topk_limited_devices is deprecated. Use moe_router_group_topk and " - "moe_router_num_groups instead." - ) - self.moe_router_group_topk = self.moe_router_topk_limited_devices - self.moe_router_num_groups = self.expert_model_parallel_size - - if self.enable_cuda_graph or self.external_cuda_graph: - assert ( - self.cuda_graph_impl == "none" - ), "Do not use enable_cuda_graph or external_cuda_graph with cuda_graph_impl." - assert ( - not self.enable_cuda_graph or not self.external_cuda_graph - ), "enable_cuda_graph and external_cuda_graph cannot be enabled at the same time." - - if self.enable_cuda_graph: - warnings.warn('enable_cuda_graph is deprecated, use cuda_graph_impl=local instead.') - self.cuda_graph_impl = "local" - if self.external_cuda_graph: - warnings.warn( - 'external_cuda_graph is deprecated, ' - 'use cuda_graph_impl=transformer_engine instead.' - ) - self.cuda_graph_impl = "transformer_engine" - - if self.cuda_graph_scope is None: - self.cuda_graph_scope = [] - elif not isinstance(self.cuda_graph_scope, list): - if isinstance(self.cuda_graph_scope, CudaGraphScope): - self.cuda_graph_scope = [self.cuda_graph_scope] - else: - assert isinstance(self.cuda_graph_scope, str), ( - "cuda_graph_scope must be a string that can be converted to a list of " - f"CudaGraphScope, got {self.cuda_graph_scope}." - ) - self.cuda_graph_scope = self.cuda_graph_scope.split(',') - if all(isinstance(scope, str) for scope in self.cuda_graph_scope): - # Backward compatibility for "full" scope. Now we use an empty list instead. - if "full" in self.cuda_graph_scope: - assert self.cuda_graph_scope == [ - "full" - ], "full scope cannot be used with other scopes." - warnings.warn( - "full scope is deprecated. " - "Use empty cuda_graph_scope to capture the whole layer." - ) - self.cuda_graph_scope = [] - else: - self.cuda_graph_scope = [CudaGraphScope[scope] for scope in self.cuda_graph_scope] - assert all( - isinstance(scope, CudaGraphScope) for scope in self.cuda_graph_scope - ), f"cuda_graph_scope must be a list of CudaGraphScope, got {self.cuda_graph_scope}." - - if self.cuda_graph_impl != "none": - assert self.cuda_graph_impl in [ - "transformer_engine", - "local", - ], f"Invalid cuda graph implementation: {self.cuda_graph_impl}" - - if self.cpu_offloading: - raise ValueError("CUDA graphs not supported with CPU offloading.") - - if self.cuda_graph_impl == "local": - assert not self.cuda_graph_scope or self.cuda_graph_scope == [ - CudaGraphScope.full_iteration - ], ( - "For local cuda graph implementation, the only valid value for " - "cuda_graph_scope is full_iteration, or an empty list to denote layerwise " - "graphs. To use other scopes, use cuda_graph_impl=transformer_engine." - ) - - if self.cuda_graph_impl == "transformer_engine": - assert CudaGraphScope.full_iteration not in self.cuda_graph_scope, ( - "To use full iteration cuda graph, please use " - "cuda_graph_impl=local instead of cuda_graph_impl=transformer_engine." - ) - assert ( - CudaGraphScope.moe not in self.cuda_graph_scope - or CudaGraphScope.moe_router not in self.cuda_graph_scope - ), 'cuda_graph_scope must not contain both moe and moe_router.' - if CudaGraphScope.moe_preprocess in self.cuda_graph_scope: - assert ( - CudaGraphScope.moe_router in self.cuda_graph_scope - ), 'moe_preprocess cuda graph is only supported with moe_router cuda graph.' - if self.num_moe_experts is None or self.num_moe_experts <= 1: - assert ( - CudaGraphScope.moe not in self.cuda_graph_scope - and CudaGraphScope.moe_router not in self.cuda_graph_scope - ), 'moe cuda graph is only supported for MoE.' - else: - if self.moe_layer_freq == 1 or ( - isinstance(self.moe_layer_freq, list) and 0 not in self.moe_layer_freq - ): - assert CudaGraphScope.mlp not in self.cuda_graph_scope, ( - 'mlp cuda graph is only supported for dense layers, ' - 'but not found in the model.' - ) - if ( - self.moe_expert_capacity_factor is None - or not self.moe_pad_expert_input_to_capacity - ): - assert ( - CudaGraphScope.moe not in self.cuda_graph_scope - ), 'moe cuda graph is only supported with drop-padding MoE.' - if self.moe_token_dispatcher_type == 'alltoall' and ( - self.moe_expert_capacity_factor is not None - or self.moe_router_padding_for_quantization - ): - assert CudaGraphScope.moe_preprocess not in self.cuda_graph_scope, ( - 'moe_preprocess cuda graph is not supported when there are ' - 'DtoH copies and synchronizations in the preprocess step.' - ) - - if self.recompute_granularity: - if self.recompute_granularity != "selective" or not self.cuda_graph_scope: - raise ValueError( - "Full-layer CUDA graphs not supported with activation recomputation." - ) - elif self.cuda_graph_scope != [CudaGraphScope.full_iteration]: - # For scoped CUDA graphs, only the non-graphed parts of the layer can be - # recomputed. So check if there are overlaps between the recomputed parts - # and the graphed parts. - if CudaGraphScope.attn in self.cuda_graph_scope: - for module in self.recompute_modules: - if module in ['core_attn', 'mla_up_proj']: - raise ValueError( - f'attn cuda graph is not supported with {module} recompute.' - ) - if ( - CudaGraphScope.mlp in self.cuda_graph_scope - and "mlp" in self.recompute_modules - ): - raise ValueError(f'mlp cuda graph is not supported with mlp recompute.') - if CudaGraphScope.moe in self.cuda_graph_scope: - for module in self.recompute_modules: - if module in ['moe_act', 'moe', 'shared_experts']: - raise ValueError( - f'moe cuda graph is not supported with {module} recompute.' - ) - if CudaGraphScope.moe_router in self.cuda_graph_scope: - for module in self.recompute_modules: - if module in ['moe', 'shared_experts']: - raise ValueError( - f'moe_router cuda graph is not supported with {module} ' - 'recompute.' - ) - if "layernorm" in self.recompute_modules: - if ( - CudaGraphScope.attn in self.cuda_graph_scope - and CudaGraphScope.mlp in self.cuda_graph_scope - and ( - CudaGraphScope.moe in self.cuda_graph_scope - or CudaGraphScope.moe_router in self.cuda_graph_scope - ) - ): - raise ValueError( - 'cuda graph is not supported with layernorm recompute.' - ) - if CudaGraphScope.attn in self.cuda_graph_scope: - warnings.warn( - "input_layernorm recompute is not supported with attention " - "cudagraph. Will only recompute the pre_mlp_layernorm." - ) - if ( - CudaGraphScope.mlp in self.cuda_graph_scope - or CudaGraphScope.moe in self.cuda_graph_scope - or CudaGraphScope.moe_router in self.cuda_graph_scope - ): - warnings.warn( - "pre_mlp_layernorm recompute is not supported with mlp/moe " - "cudagraph. Will only recompute the input_layernorm." - ) - - if self.moe_token_dispatcher_type in ["allgather"]: - if self.variable_seq_lengths is True: - raise ValueError( - f"Token dispatcher type: {self.moe_token_dispatcher_type} does not support " - f"variable sequence length, please use alltoall dispatcher instead." - ) - - if self.moe_permute_fusion: - from megatron.core.transformer.moe.moe_utils import ( - fused_permute, - fused_permute_with_probs, - fused_sort_chunks_by_index, - fused_sort_chunks_by_index_with_probs, - fused_unpermute, - ) - - if ( - fused_permute is None - or fused_permute_with_probs is None - or fused_sort_chunks_by_index is None - or fused_sort_chunks_by_index_with_probs is None - or fused_unpermute is None - ): - raise ValueError("fused permutation is not available. Please install TE >= 2.1.0.") - - if self.overlap_moe_expert_parallel_comm: - # TODO: remove this after we fix the hang issue with torch version < 2.6.0 - assert is_torch_min_version( - "2.6.0" - ), "A2A Overlap encounters hang issue with torch version < 2.6.0" - if self.pipeline_model_parallel_size > 1: - assert self.virtual_pipeline_model_parallel_size is not None, ( - "If enabling EP A2A overlap, virtual_pipeline_model_parallel_size " - "must be specified when pipeline_model_parallel_size > 1" - ) - # Expert model parallelism requirements - assert ( - self.expert_model_parallel_size > 1 - ), 'overlap_moe_expert_parallel_comm is only supported with expert model parallelism' - assert self.moe_token_dispatcher_type in [ - 'alltoall', - 'flex', - ], 'overlap_moe_expert_parallel_comm is supported with alltoall/flex token dispatcher' - - assert ( - self.recompute_granularity != 'full' - ), 'disable full recomputation when enabling overlap_moe_expert_parallel_comm' - assert ( - self.recompute_method is None - ), 'disable recomputation method when enabling overlap_moe_expert_parallel_comm' - assert ( - self.recompute_num_layers is None - ), 'recompute_num_layers must be None when enabling overlap_moe_expert_parallel_comm' - - # Check if bf16 or fp16 is used - assert ( - self.bf16 or self.fp16 - ), 'overlap_moe_expert_parallel_comm is only supported with bf16 or fp16 model' - - assert ( - not self.moe_shared_expert_overlap - ), 'disable moe_shared_expert_overlap when enabling overlap_moe_expert_parallel_comm' - assert ( - self.mtp_num_layers is None or self.mtp_num_layers == 1 - ), 'MTP layernum only supports 1 when enabling overlap_moe_expert_parallel_comm.' - - # Check delay_wgrad_compute compatibility - if self.delay_wgrad_compute: - assert ( - self.overlap_moe_expert_parallel_comm - ), 'overlap_moe_expert_parallel_comm must be enabled when enabling delay_wgrad_compute' - assert ( - not self.moe_use_legacy_grouped_gemm - ), 'delay_wgrad_compute is not supported with legacy groupedgemm implementation' - - if self.context_parallel_size > 1 and self.cp_comm_type is not None: - if isinstance(self.cp_comm_type, list): - assert len(self.cp_comm_type) == self.num_layers, ( - f"Length of cp_comm_type ({len(self.cp_comm_type)}) should equal to " - f"the total number of transformer layers ({self.num_layers})!" - ) - else: - assert isinstance( - self.cp_comm_type, str - ), "Unsupported communication type for context parallelism!" - - assert ( - self.pipeline_model_parallel_size > 0 - ), f"Pipeline model parallel size must be larger than 0 \ - when enable --standalone-embedding-stage and --standalone-loss-stage" - - if ( - self.num_moe_experts is not None - and self.num_moe_experts >= 32 - and not self.moe_router_dtype - ): - warnings.warn( - "Using a large number of experts (e.g. >=32) without fp32 routing. " - "Consider enabling moe_router_dtype for better numerical stability." - ) - if self.symmetric_ar_type is not None: - if not HAVE_PACKAGING: - raise ImportError( - "packaging is not installed. Please install it with `pip install packaging`." - ) - assert is_torch_min_version("2.7.0a0"), "Must have at least torch version 2.7 or higher" - assert is_te_min_version("2.3.0") or get_te_version() == PkgVersion( - "2.3.0.dev0+39c0e70" - ), "Must have at least TE version 2.3 or higher to use symmetric memory all reduce" - - if self.no_rope_freq: - assert not self.flash_decode, "flash_decode cannot be used with no_rope." - if isinstance(self.no_rope_freq, int): - assert self.num_layers % self.no_rope_freq == 0, ( - f"no_rope_freq={self.no_rope_freq} should be " - f"divisible by num_layers={self.num_layers}." - ) - # Convert integer pattern to list pattern - # e.g. no_rope=4 with num_layers=8 becomes [0,0,0,1,0,0,0,1] - pattern = [0] * (self.no_rope_freq - 1) + [1] - self.no_rope_freq = pattern * (self.num_layers // self.no_rope_freq) - else: - assert len(self.no_rope_freq) == self.num_layers, ( - f"Length of no_rope list ({len(self.no_rope_freq)}) must match " - f"the number of layers ({self.num_layers})" - ) - - if self.fallback_to_eager_attn: - assert self.transformer_impl == "transformer_engine", ( - f"fallback_to_eager_attn is only available with transformer_engine implementation," - f" but got {self.transformer_impl=}." - ) - - if self.fallback_to_eager_attn or self.transformer_impl == "local": - if self.context_parallel_size > 1 and self.cp_comm_type is not None: - all_cp_comm_types_are_all_gather = ( - all(item == "all_gather" for item in self.cp_comm_type) - if isinstance(self.cp_comm_type, list) - else self.cp_comm_type == "all_gather" - ) - if not all_cp_comm_types_are_all_gather: - raise ValueError( - f"fallback_to_eager_attn only supports all_gather communication type " - f"for context parallelism, but got {self.cp_comm_type=} instead." - ) - - -@dataclass -class MLATransformerConfig(TransformerConfig): - """Configuration object for megatron-core Multi-Latent Attention (MLA) transformers. - - The initialization function has an argument for each parameter, including those in - ModelParallelConfig. Included YaRN RoPE parameters that is fused in MLA. - """ - - multi_latent_attention: bool = True - """Whether to use Multi-Latent Attention.""" - - q_lora_rank: int = 512 - """Rank of Query tensor's low rank representation.""" - - kv_lora_rank: int = 512 - """Rank of Key and Value tensors' low rank representation.""" - - qk_head_dim: int = 128 - """Dimension of the head in the QK projection. q_head_dim = qk_head_dim + qk_pos_emb_head_dim""" - - qk_pos_emb_head_dim: int = 64 - """Dimension of the position embedding in the QK projection.""" - - v_head_dim: int = 128 - """Dimension of the head in the V projection.""" - - normalization: str = "RMSNorm" - """Default normalization layer for MLA models is RMSNorm.""" - - rope_type: str = "yarn" - """Type of RoPE to use. Default to yarn, options are rope and yarn.""" - - rotary_base: float = 10000 - """Rotary base for the rotary embeddings, used by rope and yarn.""" - - rotary_percent: float = 1.0 - """Rotary percent for the rotary embeddings, used by rope.""" - - rotary_scaling_factor: float = 40 - """Rotary scaling factor for the rotary embeddings, used by yarn.""" - - original_max_position_embeddings: int = 4096 - """Original maximum position embeddings for the original model, used by yarn.""" - - beta_fast: float = 32 - """Beta fast for YaRN RoPE, used by yarn.""" - - beta_slow: float = 1 - """Beta slow for YaRN RoPE, used by yarn.""" - - mscale: float = 1.0 - """Mscale for YaRN RoPE in Multi-Latent Attention, used by yarn.""" - - mscale_all_dim: float = 0.0 - """Mscale all dimensions for YaRN RoPE in Multi-Latent Attention, used by yarn.""" - - cache_mla_latents: bool = False - """Cache the low dimensional tensors for MLA rather than full KV cache. - This is only for the dynamic inference backend and requires that - Flash MLA is installed.""" - - def __post_init__(self): - super().__post_init__() - if self.multi_latent_attention and self.apply_rope_fusion and self.rope_type != "yarn": - raise ValueError("apply_rope_fusion for MLA only works with YARN RoPE.") - - if self.attention_output_gate: - raise NotImplementedError("Output gate is not supported for MLA yet.") - - if self.cache_mla_latents: - assert ( - self.apply_rope_fusion is False - ), "Rope Fusion is not compatible with caching latents" \ No newline at end of file From 1def8420246d4604cfe0abab73deeb6f2bde9ea0 Mon Sep 17 00:00:00 2001 From: zhihaow6 Date: Sat, 24 Jan 2026 15:08:26 -0800 Subject: [PATCH 57/57] update --- scripts/run_deepseek_v32.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/run_deepseek_v32.py b/scripts/run_deepseek_v32.py index 6d76ade86..1065ebbd8 100644 --- a/scripts/run_deepseek_v32.py +++ b/scripts/run_deepseek_v32.py @@ -25,9 +25,9 @@ class ScriptArgs(U.ExecuteTrainConfig): task: Literal["dapo_aime", "gsm8k"] = "dapo_aime" enable_deepep: bool = True data_dir: str = "/root" - model_dir: str = "/root/.cache/dsv32" - model_local_dir: str = "/root/.cache/dsv32" - save_dir: str = "/root/.cache/dsv32" + model_dir: str = "/root/models" + model_local_dir: str = "/root/models" + save_dir: str = "/root/models" megatron_path: str = "/root/Megatron-LM" @@ -115,11 +115,11 @@ def train(args: ScriptArgs): "--rollout-shuffle " "--rm-type math " "--num-rollout 3000 " - "--rollout-batch-size 8 " - "--n-samples-per-prompt 8 " + "--rollout-batch-size 1 " + "--n-samples-per-prompt 1 " "--rollout-temperature 0.8 " # ------------ - "--num-steps-per-rollout 4 " + "--num-steps-per-rollout 1 " "--balance-data " ) @@ -212,6 +212,8 @@ def train(args: ScriptArgs): "--entropy-coef 0.00 " "--eps-clip 0.2 " "--eps-clip-high 0.28 " + "--use-miles-router " + "--use-rollout-routing-replay " ) optimizer_args = ( @@ -246,6 +248,7 @@ def train(args: ScriptArgs): f"--sglang-max-running-requests {sglang_world_size * sglang_decode_max_bs // sglang_attn_tp_size} " f"--sglang-chunked-prefill-size {sglang_world_size * sglang_decode_max_bs} " f"--sglang-cuda-graph-max-bs {sglang_decode_max_bs} " + "--sglang-disable-cuda-graph " # For quick experiments # """--sglang-json-model-override-args '{"num_hidden_layers": 5}' """ ) @@ -310,6 +313,5 @@ def train(args: ScriptArgs): megatron_path=args.megatron_path, ) - if __name__ == "__main__": - app() + app() \ No newline at end of file