From 1dd7dec7e3c25e2726bd767c3ea2f6d833e3debf Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Thu, 30 Apr 2026 17:57:03 -0700 Subject: [PATCH 0001/1130] Feat: Qwen3.6 RL support (#1022) --- .../megatron_utils/megatron_to_hf/__init__.py | 2 +- miles_plugins/mbridge/qwen3_5.py | 55 ++++- miles_plugins/models/qwen3_5.py | 4 + scripts/models/qwen3.6-27B.sh | 27 +++ scripts/models/qwen3.6-35B-A3B.sh | 58 +++++ scripts/run-qwen3.6-27B.sh | 153 +++++++++++++ scripts/run_qwen3_6_35b_a3b_mtp.py | 215 ++++++++++++++++++ 7 files changed, 506 insertions(+), 8 deletions(-) create mode 100644 scripts/models/qwen3.6-27B.sh create mode 100644 scripts/models/qwen3.6-35B-A3B.sh create mode 100755 scripts/run-qwen3.6-27B.sh create mode 100644 scripts/run_qwen3_6_35b_a3b_mtp.py diff --git a/miles/backends/megatron_utils/megatron_to_hf/__init__.py b/miles/backends/megatron_utils/megatron_to_hf/__init__.py index 888ade1f9a5..90a8f977a9b 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/__init__.py +++ b/miles/backends/megatron_utils/megatron_to_hf/__init__.py @@ -42,7 +42,7 @@ def _convert_to_hf_core(args, model_name, name, param): converted_named_tensors = convert_qwen3moe_to_hf(args, name, param) elif "qwen3next" in model_name: converted_named_tensors = convert_qwen3_next_to_hf(args, name, param) - elif "qwen3_5" in model_name: + elif "qwen3_5" in model_name or "qwen3_6" in model_name: converted_named_tensors = convert_qwen3_5_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) diff --git a/miles_plugins/mbridge/qwen3_5.py b/miles_plugins/mbridge/qwen3_5.py index 8da5b7204b4..fe1556ef7d9 100644 --- a/miles_plugins/mbridge/qwen3_5.py +++ b/miles_plugins/mbridge/qwen3_5.py @@ -7,12 +7,18 @@ from mbridge.models import Qwen2MoEBridge -@register_model(["qwen3_5", "qwen3_5_moe"]) +@register_model(["qwen3_5", "qwen3_5_moe", "qwen3_6", "qwen3_6_moe"]) class Qwen3_5Bridge(Qwen2MoEBridge): """ - Bridge for Qwen3.5 models (both dense and MoE variants). - Qwen3.5 is a VLM model with weights under model.language_model.layers prefix, - separate in_proj_qkv + in_proj_z for linear attention, and nested text_config. + Bridge for Qwen3.5 / Qwen3.6 models (both dense and MoE variants). + These share the ``qwen3_5_moe`` HF config schema: VLM layout under + ``model.language_model.layers``, separate ``in_proj_qkv`` + ``in_proj_z`` + for linear attention, and nested ``text_config``. + + Qwen3.6-35B-A3B's only structural difference is MTP-expert packing — + fused 3-D ``gate_up_proj`` / ``down_proj`` tensors instead of the + per-expert ``.weight`` files used by Qwen3.5 — which + ``_mtp_experts_fused()`` autodetects from the safetensor index. """ _DIRECT_MAPPING = { @@ -89,14 +95,21 @@ class Qwen3_5Bridge(Qwen2MoEBridge): "mlp.experts.linear_fc2": ["model.language_model.layers.{layer_number}.mlp.experts.down_proj"], } - # MTP layer uses individual expert format (not fused) - _MTP_MLP_MAPPING = { + # MTP MLP expert mapping — the format depends on the HF weights. + # Qwen3.5-35B-A3B ships MTP experts as individual per-expert tensors; + # Qwen3.6-35B-A3B packs them as fused 3-D tensors (like regular layers). + # ``_mtp_experts_fused_cached`` is resolved lazily from safetensor_io. + _MTP_MLP_MAPPING_UNFUSED = { "mlp.experts.linear_fc1": [ "mtp.layers.{layer_number}.mlp.experts.{expert_id}.gate_proj.weight", "mtp.layers.{layer_number}.mlp.experts.{expert_id}.up_proj.weight", ], "mlp.experts.linear_fc2": ["mtp.layers.{layer_number}.mlp.experts.{expert_id}.down_proj.weight"], } + _MTP_MLP_MAPPING_FUSED = { + "mlp.experts.linear_fc1": ["mtp.layers.{layer_number}.mlp.experts.gate_up_proj"], + "mlp.experts.linear_fc2": ["mtp.layers.{layer_number}.mlp.experts.down_proj"], + } # Override to make ffn_hidden_size optional (Qwen3.5 MoE has no intermediate_size) _CONFIG_MAPPING = { @@ -185,8 +198,36 @@ def _weight_name_mapping_mlp(self, name: str) -> list[str]: raise NotImplementedError(f"Unsupported parameter name: {name}") return convert_names + def _mtp_experts_fused(self) -> bool: + """Detect whether MTP expert weights are stored in fused 3-D tensors. + + Qwen3.5 MoE-A3B: unfused per-expert tensors (keys end in ``.weight``). + Qwen3.6 MoE-A3B: fused ``gate_up_proj`` / ``down_proj`` tensors. + Resolved from ``safetensor_io.index`` on first call; result is only + cached once ``safetensor_io`` is available, so early pre-init access + (e.g. from tests that instantiate via ``__new__``) does not lock in + a wrong answer. + """ + cached = getattr(self, "_mtp_experts_fused_cached", None) + if cached is not None: + return cached + io = getattr(self, "safetensor_io", None) + index = getattr(io, "index", None) if io is not None else None + if not index: + return False + fused = any( + "mtp.layers." in k and "mlp.experts." in k and (k.endswith("gate_up_proj") or k.endswith("down_proj")) + for k in index + ) + self._mtp_experts_fused_cached = fused + return fused + + @property + def _MTP_MLP_MAPPING(self): + return self._MTP_MLP_MAPPING_FUSED if self._mtp_experts_fused() else self._MTP_MLP_MAPPING_UNFUSED + def _weight_name_mapping_mtp_mlp(self, name: str) -> list[str]: - """Handle MTP MLP mappings, keeping per-expert tensors unfused for MoE layers.""" + """Handle MTP MLP mappings; per-expert format is detected from HF weights.""" layer_number = name.split(".")[2] mapping = self._MTP_MLP_MAPPING if "mlp.experts.linear_fc" in name else self._MLP_MAPPING convert_names = [] diff --git a/miles_plugins/models/qwen3_5.py b/miles_plugins/models/qwen3_5.py index 5c43d732dcd..5fef6e0b03d 100644 --- a/miles_plugins/models/qwen3_5.py +++ b/miles_plugins/models/qwen3_5.py @@ -72,6 +72,10 @@ def __init__(self, config, layer_idx: int): self.dt_bias = nn.Parameter(torch.ones(self.num_v_heads)) A = torch.empty(self.num_v_heads).uniform_(0, 16) + # Qwen3.5 ships A_log in fp32; Qwen3.6 reverted the HF weight to bf16. + # We still hold it in fp32 here for engineering simplicity (no special + # path for 3.6) — this matches the SGLang implementation, which also + # keeps A_log in fp32 regardless of the HF dtype. self.A_log = nn.Parameter(torch.log(A).to(torch.float32)) mark_param_dtype(self.A_log, torch.float32) diff --git a/scripts/models/qwen3.6-27B.sh b/scripts/models/qwen3.6-27B.sh new file mode 100644 index 00000000000..c30e566d0a3 --- /dev/null +++ b/scripts/models/qwen3.6-27B.sh @@ -0,0 +1,27 @@ +MODEL_ARGS=( + --spec "miles_plugins.models.qwen3_5" "get_qwen3_5_spec" + + --disable-bias-linear + --qk-layernorm + --group-query-attention + --num-attention-heads 24 + --num-query-groups 4 + --kv-channels 256 + --num-layers 64 + --hidden-size 5120 + --ffn-hidden-size 17408 + + --normalization RMSNorm + --apply-layernorm-1p + --position-embedding-type rope + --norm-epsilon 1e-6 + --rotary-percent 0.25 + --swiglu + --untie-embeddings-and-output-weights + --vocab-size 248320 + + --rotary-base 10000000 + + # qwen3.5-family specific + --attention-output-gate +) diff --git a/scripts/models/qwen3.6-35B-A3B.sh b/scripts/models/qwen3.6-35B-A3B.sh new file mode 100644 index 00000000000..0bb9dcc7c73 --- /dev/null +++ b/scripts/models/qwen3.6-35B-A3B.sh @@ -0,0 +1,58 @@ +NLAYERS=40 +FIRST_K_DENSE_REPLACE=0 + +arr=() +for ((i=0; i/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.6-27B.sh" + +MODEL_DIR="${MODEL_DIR:-/cluster_public/miles_data/models}" +DATA_DIR="${DATA_DIR:-/cluster_public/miles_data/datasets}" +OUTPUT_DIR="${OUTPUT_DIR:?set OUTPUT_DIR to a writable checkpoint directory}" + +CKPT_ARGS=( + --hf-checkpoint ${MODEL_DIR}/Qwen3.6-27B + --ref-load ${MODEL_DIR}/Qwen3.6-27B_torch_dist + --load ${OUTPUT_DIR}/checkpoints +) + +if [ "${NO_SAVE:-0}" != "1" ]; then + CKPT_ARGS+=( + --save ${OUTPUT_DIR}/checkpoints + --save-interval 20 + ) +fi + +ROLLOUT_ARGS=( + --prompt-data ${DATA_DIR}/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 1 + + --global-batch-size 256 + --balance-data +) + +if [ "${NO_EVAL:-0}" = "1" ]; then + EVAL_ARGS=() +else + EVAL_ARGS=( + --eval-interval 20 + --eval-prompt-data aime ${DATA_DIR}/aime-2024/aime-2024.jsonl + --n-samples-per-eval-prompt 16 + --eval-max-response-len 16384 + --eval-top-p 1 + ) +fi + +PERF_ARGS=( + --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 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 +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.5 +) + +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"} +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 + +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 8 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} \ + "$@" diff --git a/scripts/run_qwen3_6_35b_a3b_mtp.py b/scripts/run_qwen3_6_35b_a3b_mtp.py new file mode 100644 index 00000000000..ba3096bec5a --- /dev/null +++ b/scripts/run_qwen3_6_35b_a3b_mtp.py @@ -0,0 +1,215 @@ +"""Parametrized Qwen3.6-35B-A3B MTP RL training launcher (8xH200). + +Supports arbitrary (TP, EP, CP, PP, ETP) combinations so multiple +parallelism configs can be exercised in short 10-step runs. +""" + +from dataclasses import dataclass +from typing import Literal + +import typer + +import miles.utils.external_utils.command_utils as U + + +@dataclass +class ScriptArgs(U.ExecuteTrainConfig): + mode: Literal["normal", "debug_minimal"] = "debug_minimal" + run_id: str = U.create_run_id() + model_name: str = "Qwen3.6-35B-A3B" + megatron_model_type: str = "qwen3.6-35B-A3B" + num_gpus_per_node: int = 8 + hardware: Literal["H200"] = "H200" + enable_eval: bool = False + extra_args: str = "" + data_dir: str = "/root/datasets" + model_dir: str = "/root/models" + megatron_path: str = "/root/Megatron-LM" + + # parallelism knobs + tp: int = 1 + ep: int = 8 + cp: int = 1 + pp: int = 1 + etp: int = 1 + + # training knobs + num_rollout: int = 10 + max_tokens_per_gpu: int = 8192 + rollout_batch_size: int = 8 + n_samples_per_prompt: int = 2 + global_batch_size: int = 16 + rollout_max_response_len: int = 1024 + + # extra perf knobs + sglang_ep_size: int | None = None # defaults to num_gpus_per_node + recompute: bool = True + skip_prepare: bool = False + + +def prepare(args: ScriptArgs): + U.exec_command(f"mkdir -p {args.model_dir} {args.data_dir}") + # model path is a symlink to /cluster_public; skip download if already present + U.exec_command( + f"test -e {args.model_dir}/{args.model_name} || " + f"hf download Qwen/{args.model_name} --local-dir {args.model_dir}/{args.model_name}" + ) + # datasets are symlinked; skip if present + U.exec_command( + f"test -e {args.data_dir}/dapo-math-17k || " + f"hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir {args.data_dir}/dapo-math-17k" + ) + U.exec_command( + f"test -e {args.data_dir}/aime-2024 || " + f"hf download --repo-type dataset zhuzilin/aime-2024 --local-dir {args.data_dir}/aime-2024" + ) + + U.convert_checkpoint( + model_name=args.model_name, + megatron_model_type=args.megatron_model_type, + num_gpus_per_node=args.num_gpus_per_node, + dir_dst=args.model_dir, + hf_checkpoint=f"{args.model_dir}/{args.model_name}", + megatron_path=args.megatron_path, + ) + + +def execute(args: ScriptArgs): + ref_load_path = f"{args.model_dir}/{args.model_name}_torch_dist" + + # Smoke runs: no checkpoint save (Megatron's final save is forced on the + # last rollout whenever --save-interval is set — miles/utils/misc.py:192 — + # so we omit both --save and --save-interval to keep ~464G per-config off + # disk). The ref-load is still required to initialise model weights. + ckpt_args = f"--hf-checkpoint {args.model_dir}/{args.model_name} " f"--ref-load {ref_load_path} " + + rollout_args = ( + f"--prompt-data {args.data_dir}/dapo-math-17k/dapo-math-17k.jsonl " + "--input-key prompt " + "--label-key label " + "--apply-chat-template " + "--rollout-shuffle " + "--rm-type deepscaler " + f"--num-rollout {args.num_rollout} " + f"--rollout-batch-size {args.rollout_batch_size} " + f"--n-samples-per-prompt {args.n_samples_per_prompt} " + f"--rollout-max-response-len {args.rollout_max_response_len} " + "--rollout-temperature 1 " + f"--global-batch-size {args.global_batch_size} " + "--balance-data " + ) + + eval_args = "" + if args.enable_eval: + eval_args += ( + "--eval-interval 1000 " + f"--eval-prompt-data aime {args.data_dir}/aime-2024/aime-2024.jsonl " + "--n-samples-per-eval-prompt 1 " + "--eval-max-response-len 4096 " + "--eval-top-p 1 " + ) + + sglang_ep = args.sglang_ep_size if args.sglang_ep_size is not None else args.num_gpus_per_node + recompute = ( + ("--recompute-granularity full " "--recompute-method uniform " "--recompute-num-layers 1 ") + if args.recompute + else "" + ) + + perf_args = ( + f"--tensor-model-parallel-size {args.tp} " + "--sequence-parallel " + f"--pipeline-model-parallel-size {args.pp} " + f"--context-parallel-size {args.cp} " + f"--expert-model-parallel-size {args.ep} " + f"--expert-tensor-parallel-size {args.etp} " + f"{recompute}" + "--use-dynamic-batch-size " + f"--max-tokens-per-gpu {args.max_tokens_per_gpu} " + ) + + 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 " + ) + + sglang_args = ( + f"--rollout-num-gpus-per-engine {args.num_gpus_per_node} " + "--sglang-mem-fraction-static 0.7 " + f"--sglang-ep-size {sglang_ep} " + "--sglang-cuda-graph-bs 1 2 4 8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 " + # mtp speculative decoding + "--sglang-speculative-algorithm EAGLE " + "--sglang-speculative-num-steps 2 " + "--sglang-speculative-eagle-topk 1 " + "--sglang-speculative-num-draft-tokens 3 " + "--sglang-max-running-requests 256 " + "--sglang-mamba-scheduler-strategy extra_buffer " + ) + + mtp_args = "--enable-mtp-training " "--mtp-num-layers 1 " "--mtp-loss-scaling-factor 0.2 " + + misc_args = ( + "--attention-dropout 0.0 " + "--hidden-dropout 0.0 " + "--accumulate-allreduce-grads-in-fp32 " + "--attention-softmax-in-fp32 " + "--attention-backend flash " + "--moe-token-dispatcher-type flex " + 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 " + ) + + train_args = ( + f"{ckpt_args} " + f"{rollout_args} " + f"{optimizer_args} " + f"{grpo_args} " + f"{perf_args} " + f"{eval_args} " + f"{sglang_args} " + f"{mtp_args} " + f"{misc_args} " + f"{args.extra_args} " + ) + + U.execute_train( + train_args=train_args, + config=args, + num_gpus_per_node=args.num_gpus_per_node, + megatron_model_type=args.megatron_model_type, + extra_env_vars={ + "SGLANG_ENABLE_SPEC_V2": "1", + }, + megatron_path=args.megatron_path, + ) + + +@U.dataclass_cli +def main(args: ScriptArgs): + if not args.skip_prepare: + prepare(args) + execute(args) + + +if __name__ == "__main__": + typer.run(main) From 372846f1b8993d38e038ba5eadd37045327d251b Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Thu, 30 Apr 2026 17:57:24 -0700 Subject: [PATCH 0002/1130] Feat: NVIDIA Nemotron-3 RL support (#1033) --- docker/Dockerfile | 3 + miles/backends/megatron_utils/__init__.py | 5 + miles/backends/megatron_utils/checkpoint.py | 2 - .../hf_weight_iterator_bridge.py | 2 - miles_plugins/megatron_bridge/__init__.py | 69 +++++++ miles_plugins/megatron_bridge/nemotron_h.py | 189 ++++++++++++++++++ scripts/models/nemotron-3-nano-30b-a3b.sh | 47 +++++ scripts/models/nemotron-3-nano-4b.sh | 24 +++ scripts/run-nemotron-3-nano-30b-a3b.sh | 133 ++++++++++++ scripts/run-nemotron-3-nano-4b.sh | 132 ++++++++++++ 10 files changed, 602 insertions(+), 4 deletions(-) create mode 100644 miles_plugins/megatron_bridge/nemotron_h.py create mode 100644 scripts/models/nemotron-3-nano-30b-a3b.sh create mode 100644 scripts/models/nemotron-3-nano-4b.sh create mode 100755 scripts/run-nemotron-3-nano-30b-a3b.sh create mode 100644 scripts/run-nemotron-3-nano-4b.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 88d556b5c97..61be29d4036 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -66,6 +66,9 @@ RUN pip install git+https://github.com/ISEEKYAN/mbridge.git@89eb10887887bc74853f RUN pip install flash-linear-attention==0.4.2 RUN pip install tilelang -f https://tile-ai.github.io/whl/nightly/cu128/ +# Mamba kernels for nemotron_h hybrid (mamba+attention) models. +RUN pip install causal-conv1d==1.6.1 mamba-ssm==2.3.1 --no-build-isolation + RUN if [ "${ENABLE_CUDA_13}" = "1" ]; then \ pip install nvidia-mathdx==25.6.0 && \ pip -v install --no-build-isolation "transformer_engine[core_cu13,pytorch]==2.10.0"; \ diff --git a/miles/backends/megatron_utils/__init__.py b/miles/backends/megatron_utils/__init__.py index a4666fbeb94..879b689c855 100644 --- a/miles/backends/megatron_utils/__init__.py +++ b/miles/backends/megatron_utils/__init__.py @@ -39,4 +39,9 @@ def _patched_forward(self, *args, packed_seq_params=None, **kwargs): except ImportError: pass +try: + import miles_plugins.megatron_bridge # noqa: F401 +except Exception as _e: # best-effort; not every environment uses megatron.bridge + logging.warning("miles megatron.bridge plugins failed to load: %s", _e) + logging.getLogger("megatron").setLevel(logging.WARNING) diff --git a/miles/backends/megatron_utils/checkpoint.py b/miles/backends/megatron_utils/checkpoint.py index 03a9dd7bb4b..d7f39dc8bed 100644 --- a/miles/backends/megatron_utils/checkpoint.py +++ b/miles/backends/megatron_utils/checkpoint.py @@ -175,8 +175,6 @@ 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})") with megatron_bridge_utils.patch_megatron_model(ddp_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 10c3c8fb6ba..8c972717217 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 @@ -15,8 +15,6 @@ def __init__(self, *args, **kwargs): from megatron.bridge import AutoBridge - import miles_plugins.megatron_bridge # noqa: F401 - self._bridge = AutoBridge.from_hf_pretrained(self.args.hf_checkpoint, trust_remote_code=True) def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base"): diff --git a/miles_plugins/megatron_bridge/__init__.py b/miles_plugins/megatron_bridge/__init__.py index e69de29bb2d..66c6240999b 100644 --- a/miles_plugins/megatron_bridge/__init__.py +++ b/miles_plugins/megatron_bridge/__init__.py @@ -0,0 +1,69 @@ +"""Miles plugin package for ``megatron.bridge`` integration. + +Importing this package is enough to: + +* register miles' bridge subclasses (e.g. + :class:`~miles_plugins.megatron_bridge.nemotron_h.MilesNemotronHBridge`) via + ``@MegatronModelBridge.register_bridge`` so ``AutoBridge`` picks them up + instead of the upstream defaults; +* install general-purpose shims that make ``megatron.bridge`` cooperate with + miles infrastructure (e.g. ``ReloadableProcessGroup``). + +Every shim / registration is wrapped in try/except so an import-time failure of +one model does not prevent other models from working. +""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +def _install_bridge_pp_group_unwrap() -> None: + """Let ``MegatronParamMapping.broadcast_obj_from_pp_rank`` work with + miles' :class:`~miles.utils.reloadable_process_group.ReloadableProcessGroup`. + + ``broadcast_obj_from_pp_rank`` calls ``broadcast_object_list`` on + ``self.pp_group``, which goes through ``_world.pg_group_ranks``. Miles wraps + every ``ProcessGroup`` in ``ReloadableProcessGroup`` for reload-safety; that + wrapper is not in ``pg_group_ranks`` so ``get_group_rank`` raises + ``"Group ... is not registered"``. Temporarily swap in the inner group for + the duration of the broadcast. + """ + from megatron.bridge.models.conversion.param_mapping import MegatronParamMapping + + from miles.utils.reloadable_process_group import ReloadableProcessGroup + + if getattr(MegatronParamMapping, "_miles_pp_group_unwrap_installed", False): + return + + _orig = MegatronParamMapping.broadcast_obj_from_pp_rank + + def broadcast_obj_from_pp_rank(self, obj, name=None): + if not isinstance(self.pp_group, ReloadableProcessGroup): + return _orig(self, obj, name) + saved = self.pp_group + self.pp_group = saved.group + try: + return _orig(self, obj, name) + finally: + self.pp_group = saved + + MegatronParamMapping.broadcast_obj_from_pp_rank = broadcast_obj_from_pp_rank + MegatronParamMapping._miles_pp_group_unwrap_installed = True + + +try: + _install_bridge_pp_group_unwrap() +except Exception as _e: # best-effort + logger.warning("miles bridge shim _install_bridge_pp_group_unwrap not applied: %s", _e) + + +# Model-specific bridge subclasses. Each submodule self-installs on import. +# Keep imports here so merely importing ``miles_plugins.megatron_bridge`` is +# enough to pick up every miles bridge (mirrors ``miles_plugins.mbridge``). +try: + from . import nemotron_h # noqa: F401 +except Exception as _e: # pragma: no cover - defensive + logger.warning("miles nemotron_h plugin failed to load: %s", _e) diff --git a/miles_plugins/megatron_bridge/nemotron_h.py b/miles_plugins/megatron_bridge/nemotron_h.py new file mode 100644 index 00000000000..357c4bc43fc --- /dev/null +++ b/miles_plugins/megatron_bridge/nemotron_h.py @@ -0,0 +1,189 @@ +"""Miles integration for ``megatron.bridge`` Nemotron-H (Mamba+Attention hybrid). + +This plugin is a non-invasive drop-in that: + +1. Registers :class:`MilesNemotronHBridge` for ``NemotronHForCausalLM`` (overriding + the upstream :class:`NemotronHBridge`) so MoE variants (Nano-30B-A3B, + Super-120B-A12B) round-trip correctly through the HF↔Megatron + ``mapping_registry`` and get the right ``moe_router_*`` fields on the + provider. +2. Patches :class:`~megatron.core.transformer.transformer_layer.TransformerLayer` + so hybrid layers whose ``self_attention`` / ``mlp`` is an ``IdentityOp`` + don't blow up on the 2-tuple unpack in ``_forward_attention`` / + ``_forward_mlp``. +3. Patches :class:`~megatron.core.models.mamba.MambaModel` ``forward`` to + transparently swallow the ``loss_mask`` kwarg so miles' generic training + loop (which was designed around ``GPTModel``) can keep passing it + unconditionally. + +Importing this module is idempotent — safe to import multiple times. +""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + + +_NEMOTRONH_MOE_MAPPINGS: dict[str, str] = { + "decoder.layers.*.mlp.router.weight": "backbone.layers.*.mixer.gate.weight", + "decoder.layers.*.mlp.router.expert_bias": "backbone.layers.*.mixer.gate.e_score_correction_bias", + # Routed experts: up-only FFN (nemotron_h uses squared_relu, no gate). + "decoder.layers.*.mlp.experts.linear_fc1.weight*": "backbone.layers.*.mixer.experts.*.up_proj.weight", + "decoder.layers.*.mlp.experts.linear_fc2.weight*": "backbone.layers.*.mixer.experts.*.down_proj.weight", + "decoder.layers.*.mlp.experts.local_experts.*.linear_fc1.weight": "backbone.layers.*.mixer.experts.*.up_proj.weight", + "decoder.layers.*.mlp.experts.local_experts.*.linear_fc2.weight": "backbone.layers.*.mixer.experts.*.down_proj.weight", + "decoder.layers.*.mlp.shared_experts.linear_fc1.weight": "backbone.layers.*.mixer.shared_experts.up_proj.weight", + "decoder.layers.*.mlp.shared_experts.linear_fc2.weight": "backbone.layers.*.mixer.shared_experts.down_proj.weight", +} + +# HF name → provider name → cast. Miles' generic ``model_provider.py`` does not +# forward ``moe_router_topk_scaling_factor`` by default, which silently runs +# Megatron with scaling=1.0 instead of the HF config's (e.g.) 2.5 — producing +# ~0.28 train-vs-rollout logprob drift for Nano-30B-A3B. +_NEMOTRONH_MOE_ROUTING_FIELDS: tuple[tuple[str, str, type], ...] = ( + ("routed_scaling_factor", "moe_router_topk_scaling_factor", float), + ("n_group", "moe_router_num_groups", int), + ("topk_group", "moe_router_group_topk", int), +) + + +def _build_bridge_subclass(): + """Build the ``MilesNemotronHBridge`` class lazily so importing this module + does not force megatron.bridge.models.nemotronh to load until first use. + """ + from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry + from megatron.bridge.models.conversion.model_bridge import MegatronModelBridge + from megatron.bridge.models.conversion.param_mapping import AutoMapping + from megatron.bridge.models.nemotronh.nemotron_h_bridge import NemotronHBridge + from megatron.core.models.mamba import MambaModel + + @MegatronModelBridge.register_bridge(source="NemotronHForCausalLM", target=MambaModel) + class MilesNemotronHBridge(NemotronHBridge): + """Nemotron-H bridge with MoE support on top of upstream dense bridge. + + Upstream :class:`NemotronHBridge` only handles the dense Mamba+Attention + variants. The Nano-30B-A3B / Super-120B-A12B MoE variants add router, + routed-expert, and shared-expert tensors under ``backbone.layers.*.mixer.*`` + and need ``num_moe_experts`` / ``moe_router_*`` on the provider. + """ + + def provider_bridge(self, hf_pretrained): + provider = super().provider_bridge(hf_pretrained) + hf = hf_pretrained.config + + n_exp = int(getattr(hf, "num_experts", None) or getattr(hf, "n_routed_experts", None) or 0) + if n_exp == 0: + return provider + + provider.num_moe_experts = n_exp + provider.moe_router_topk = int(getattr(hf, "num_experts_per_tok", 1)) + provider.moe_router_score_function = "sigmoid" + provider.moe_router_enable_expert_bias = True + provider.moe_router_dtype = "fp32" + provider.moe_grouped_gemm = True + provider.moe_ffn_hidden_size = int(getattr(hf, "moe_intermediate_size", None) or provider.ffn_hidden_size) + + # hybrid_override_pattern ('MEMEM*...') marks which layers are MoE. + # Mirror to moe_layer_freq so miles' replay_utils registers the + # rollout routing replay once per real MoE layer, not per transformer layer. + pattern = getattr(provider, "hybrid_override_pattern", None) or getattr( + hf, "hybrid_override_pattern", None + ) + if pattern: + provider.moe_layer_freq = [1 if ch == "E" else 0 for ch in pattern][: int(provider.num_layers)] + + shared_size = getattr(hf, "moe_shared_expert_intermediate_size", None) + if shared_size is not None: + provider.moe_shared_expert_intermediate_size = int(shared_size) + elif getattr(hf, "n_shared_experts", 0): + provider.moe_shared_expert_intermediate_size = provider.moe_ffn_hidden_size * int(hf.n_shared_experts) + + for hf_name, prov_name, cast in _NEMOTRONH_MOE_ROUTING_FIELDS: + val = getattr(hf, hf_name, None) + if val is not None: + setattr(provider, prov_name, cast(val)) + return provider + + def mapping_registry(self): + # Append MoE mappings unconditionally. Dense variants do not carry + # the extra megatron params so these mappings are simply unreferenced. + registry = super().mapping_registry() + base = list(registry.mappings if hasattr(registry, "mappings") else registry._mappings) + extras = [AutoMapping(megatron_param=m, hf_param=h) for m, h in _NEMOTRONH_MOE_MAPPINGS.items()] + return MegatronMappingRegistry(*base, *extras) + + return MilesNemotronHBridge + + +def _install_nemotronh_hybrid_layer_shims() -> None: + """Make ``TransformerLayer`` tolerate ``IdentityOp`` self_attention / mlp. + + nemotron_h is a Mamba+Attention hybrid: non-attention positions use + ``IdentityOp`` for ``self_attention``; attention-only positions use + ``IdentityOp`` for ``self.mlp``. Both ``TransformerLayer._forward_attention`` + and ``_forward_mlp`` do a 2-tuple unpack that raises ``ValueError`` on the + raw tensor ``IdentityOp`` returns. Short-circuit both paths. + """ + from megatron.core.transformer.transformer_layer import TransformerLayer + + if getattr(TransformerLayer, "_miles_nemotron_hybrid_shim_installed", False): + return + + _orig_fwd_attn = TransformerLayer._forward_attention + _orig_fwd_mlp = TransformerLayer._forward_mlp + + def _forward_attention(self, *args, **kwargs): + if type(self.self_attention).__name__ == "IdentityOp": + hidden_states = args[0] if args else kwargs.get("hidden_states") + return hidden_states, None + return _orig_fwd_attn(self, *args, **kwargs) + + def _forward_mlp(self, hidden_states, *args, **kwargs): + if type(self.mlp).__name__ == "IdentityOp": + return hidden_states + return _orig_fwd_mlp(self, hidden_states, *args, **kwargs) + + TransformerLayer._forward_attention = _forward_attention + TransformerLayer._forward_mlp = _forward_mlp + TransformerLayer._miles_nemotron_hybrid_shim_installed = True + + +def _install_mamba_model_loss_mask_shim() -> None: + """Make ``MambaModel.forward`` silently accept (and drop) ``loss_mask``. + + Miles' generic training loop was written against ``GPTModel.forward``, + which has ``loss_mask: Optional[Tensor] = None`` as a keyword-only arg. + ``MambaModel.forward`` does not accept ``loss_mask``, so passing it raises + ``TypeError``. The loss itself is still computed downstream from the batch + ``loss_masks`` field — it is only the forward call that must drop it. + """ + from megatron.core.models.mamba import MambaModel + + if getattr(MambaModel, "_miles_loss_mask_shim_installed", False): + return + + _orig_forward = MambaModel.forward + + def forward(self, *args, loss_mask=None, **kwargs): + return _orig_forward(self, *args, **kwargs) + + MambaModel.forward = forward + MambaModel._miles_loss_mask_shim_installed = True + + +def install() -> None: + """Apply all Nemotron-H shims and register the miles bridge subclass.""" + for fn in ( + _install_nemotronh_hybrid_layer_shims, + _install_mamba_model_loss_mask_shim, + _build_bridge_subclass, + ): + try: + fn() + except Exception as e: # best-effort; avoid breaking unrelated models + logger.warning("miles nemotron_h shim %s not applied: %s", fn.__name__, e) + + +install() diff --git a/scripts/models/nemotron-3-nano-30b-a3b.sh b/scripts/models/nemotron-3-nano-30b-a3b.sh new file mode 100644 index 00000000000..bf10a50b7cc --- /dev/null +++ b/scripts/models/nemotron-3-nano-30b-a3b.sh @@ -0,0 +1,47 @@ +# NVIDIA Nemotron-3-Nano-30B-A3B (BF16, MoE nemotron_h = hybrid Mamba + Attention + MoE). +# HF config (verified 2026-04-21): +# num_hidden_layers=52 hidden_size=2688 num_attention_heads=32 num_key_value_heads=2 +# head_dim=128 intermediate_size=1856 moe_intermediate_size=1856 +# n_routed_experts=128 num_experts_per_tok=6 n_shared_experts=1 +# moe_shared_expert_intermediate_size=3712 sigmoid routing + aux-free expert bias +# The AutoBridge path (--megatron-to-hf-mode bridge) + miles NemotronHBridge MoE shim +# (see miles/backends/megatron_utils/__init__.py) construct the provider and +# HF↔Megatron mapping_registry at load time. Attention-side structural args go +# in MODEL_ARGS for miles' arg parser. + +MODEL_ARGS=( + --disable-bias-linear + --group-query-attention + --num-attention-heads 32 + --num-query-groups 2 + --kv-channels 128 + --num-layers 52 + --hidden-size 2688 + --ffn-hidden-size 1856 + --normalization RMSNorm + --position-embedding-type none + --vocab-size 131072 + --make-vocab-size-divisible-by 128 + --untie-embeddings-and-output-weights + + # MoE specifics + --num-experts 128 + --moe-router-topk 6 + --moe-ffn-hidden-size 1856 + --moe-shared-expert-intermediate-size 3712 + --moe-router-score-function sigmoid + --moe-router-enable-expert-bias + --moe-grouped-gemm + --moe-router-dtype fp32 + # Routing: config has n_group=1 (MoE groups), topk_group=1, + # routed_scaling_factor=2.5. `n_groups=8` is Mamba groups — unrelated to MoE. + # With n_group=1, group-limited routing is a no-op (single group of 128). + --moe-router-num-groups 1 + --moe-router-group-topk 1 + --moe-router-topk-scaling-factor 2.5 + --moe-router-pre-softmax + # Match glm4.7-flash (known-working MoE RL) settings more closely. + --moe-router-load-balancing-type seq_aux_loss + --moe-router-bias-update-rate 0 + --moe-aux-loss-coeff 0 +) diff --git a/scripts/models/nemotron-3-nano-4b.sh b/scripts/models/nemotron-3-nano-4b.sh new file mode 100644 index 00000000000..9e1a76b5e1d --- /dev/null +++ b/scripts/models/nemotron-3-nano-4b.sh @@ -0,0 +1,24 @@ +# NVIDIA Nemotron-3-Nano-4B (BF16, dense `nemotron_h` = hybrid Mamba + Attention). +# HF config (verified 2026-04-21): +# num_hidden_layers=42 hidden_size=3136 num_attention_heads=40 num_key_value_heads=8 +# vocab_size=131072 max_position_embeddings=262144 no RoPE squared-relu FFN +# The AutoBridge path (--megatron-to-hf-mode bridge) constructs the full Megatron +# provider from the HF config.json at load time, including all Mamba-specific +# fields (mamba_num_heads, mamba_state_dim, hybrid_override_pattern, etc.), so we +# only keep the attention-side structural args here for miles' arg parser. + +MODEL_ARGS=( + --disable-bias-linear + --group-query-attention + --num-attention-heads 40 + --num-query-groups 8 + --kv-channels 128 + --num-layers 42 + --hidden-size 3136 + --ffn-hidden-size 12544 + --normalization RMSNorm + --position-embedding-type none + --vocab-size 131072 + --make-vocab-size-divisible-by 128 + --untie-embeddings-and-output-weights +) diff --git a/scripts/run-nemotron-3-nano-30b-a3b.sh b/scripts/run-nemotron-3-nano-30b-a3b.sh new file mode 100755 index 00000000000..db2e1ec7ef0 --- /dev/null +++ b/scripts/run-nemotron-3-nano-30b-a3b.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Single-node (8x H200) 10-step RL smoke test for Nemotron-3-Nano-30B-A3B MoE +# via AutoBridge + miles NemotronHBridge MoE shim. +# Starting cell is EP=4 (32 experts per rank of 128); subsequent cells flip +# the PERF_ARGS block + OUT_DIR suffix. + +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 + +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" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/models/nemotron-3-nano-30b-a3b.sh" + +CKPT_ARGS=( + --hf-checkpoint $BASE_DIR/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + --ref-load $BASE_DIR/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + --save $BASE_DIR/nemotron-3-nano-30b-a3b_miles + --save-interval 20 + --megatron-to-hf-mode bridge +) + +ROLLOUT_ARGS=( + --prompt-data $BASE_DIR/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 10 + --rollout-batch-size 32 + --n-samples-per-prompt 4 + --rollout-max-response-len 1024 + --rollout-temperature 1 + --global-batch-size 128 + --balance-data +) + +EVAL_ARGS=( +) + +PERF_ARGS=( + --tensor-model-parallel-size 2 + --sequence-parallel + --pipeline-model-parallel-size 2 + --context-parallel-size 1 + --expert-model-parallel-size 2 + --expert-tensor-parallel-size 1 + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + --use-dynamic-batch-size + --max-tokens-per-gpu 1024 + --log-probs-chunk-size 128 +) + +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=() + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 + # Replay the exact rollout routing during training forward so + # train logprobs match rollout logprobs (needed for MoE). + --use-miles-router + --use-rollout-routing-replay +) + +MISC_ARGS=( + --attention-dropout 0.0 + --hidden-dropout 0.0 + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + --attention-backend auto +) + +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 + +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 \ + --colocate \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 8 \ + --rollout-num-gpus 8 \ + ${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-nemotron-3-nano-4b.sh b/scripts/run-nemotron-3-nano-4b.sh new file mode 100644 index 00000000000..34a61f2cb59 --- /dev/null +++ b/scripts/run-nemotron-3-nano-4b.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Single-node (8x H200) 10-step RL smoke test for Nemotron-3-Nano-4B via AutoBridge. +# Parallelism cell is TP=2 (matches scripts/run-qwen3.5-4B.sh starting point). +# Dense architecture so EP is N/A. Subsequent cells (PP=2, CP=2, TP=4, TP=2xPP=2) +# swap the PERF_ARGS block. + +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 + +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/nemotron-3-nano-4b.sh" + +CKPT_ARGS=( + --hf-checkpoint $BASE_DIR/NVIDIA-Nemotron-3-Nano-4B-BF16 + --ref-load $BASE_DIR/NVIDIA-Nemotron-3-Nano-4B-BF16 + --save $BASE_DIR/nemotron-3-nano-4b_miles + --save-interval 20 + --megatron-to-hf-mode bridge +) + +ROLLOUT_ARGS=( + --prompt-data $BASE_DIR/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 10 + --rollout-batch-size 32 + --n-samples-per-prompt 8 + --rollout-max-response-len 4096 + --rollout-temperature 1 + --global-batch-size 256 + --balance-data +) + +EVAL_ARGS=( + # no eval for 10-step smoke test +) + +PERF_ARGS=( + --tensor-model-parallel-size 2 + --sequence-parallel + --pipeline-model-parallel-size 2 + --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 +) + +WANDB_ARGS=( +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 +) + +MISC_ARGS=( + --attention-dropout 0.0 + --hidden-dropout 0.0 + --accumulate-allreduce-grads-in-fp32 + --attention-softmax-in-fp32 + --attention-backend auto +) + +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 + +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 \ + --colocate \ + --actor-num-nodes 1 \ + --actor-num-gpus-per-node 8 \ + --rollout-num-gpus 8 \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} From a333e4bf53dad25cab220390d445928c53963631 Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Thu, 30 Apr 2026 17:58:03 -0700 Subject: [PATCH 0003/1130] centralize miles_plugins.megatron_bridge import refactor (#1038) From ce10d214db00248ac4cd882aced29240144c21df Mon Sep 17 00:00:00 2001 From: Ziang Li Date: Fri, 1 May 2026 00:19:10 -0700 Subject: [PATCH 0004/1130] Use TE NVFP4 reference quantizer and add bitwise exact tests (#1054) --- .../processors/quantizer_nvfp4.py | 167 ++++++------ tests/fast/utils/test_mxfp8_quantizer.py | 161 +++++++++++ tests/fast/utils/test_nvfp4_quantizer.py | 163 ++++++++++++ tools/convert_hf_to_nvfp4.py | 250 ++++++++---------- 4 files changed, 514 insertions(+), 227 deletions(-) create mode 100644 tests/fast/utils/test_mxfp8_quantizer.py create mode 100644 tests/fast/utils/test_nvfp4_quantizer.py diff --git a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_nvfp4.py b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_nvfp4.py index 7d8b92afed2..6432d5ebed4 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_nvfp4.py +++ b/miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_nvfp4.py @@ -38,7 +38,12 @@ def _is_ignored(name: str, ignore_rules: list[str]) -> bool: def quantize_params_nvfp4(args, megatron_name, converted_named_params, quantization_config): assert quantization_config is not None assert quantization_config.get("quant_algo") == "NVFP4" or quantization_config.get("quant_method") == "nvfp4" - group_size = _resolve_group_size(quantization_config) + + if getattr(args, "extra_high_precision_layers_megatron", False): + for layer_name in getattr(args, "extra_high_precision_layers_megatron", ()): + if layer_name in megatron_name: + return converted_named_params + ignore_rules = _get_ignore_rules(quantization_config) decoder_layers_pattern = r"decoder\.layers\.(\d+)\.(.+)" @@ -50,10 +55,20 @@ def quantize_params_nvfp4(args, megatron_name, converted_named_params, quantizat match = re.search(mtp_layer_pattern, megatron_name) if not match: return converted_named_params - _, rest = match.groups() + layer_idx, rest = match.groups() rest = rest.replace("transformer_layer.", "") else: - _, rest = match.groups() + layer_idx, rest = match.groups() + + # Skip quantization for BF16 tail of main decoder layers. + if getattr(args, "first_last_layers_bf16", False): + num_layers = int(args.num_layers) + num_layers_at_start_in_bf16 = int(getattr(args, "num_layers_at_start_in_bf16", 0)) + num_layers_at_end_in_bf16 = int(getattr(args, "num_layers_at_end_in_bf16", 0)) + head_end_idx = num_layers_at_start_in_bf16 + tail_start_idx = num_layers - num_layers_at_end_in_bf16 + if int(layer_idx) < head_end_idx or int(layer_idx) >= tail_start_idx: + return converted_named_params # experts expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)" @@ -64,7 +79,7 @@ def quantize_params_nvfp4(args, megatron_name, converted_named_params, quantizat "linear_fc1", "linear_fc2", ]: - return _quantize_moe_params(converted_named_params, group_size, ignore_rules) + return _quantize_moe_params(converted_named_params, ignore_rules) # shared expert shared_expert_pattern = r"mlp.shared_experts\.(.+)" @@ -75,43 +90,47 @@ def quantize_params_nvfp4(args, megatron_name, converted_named_params, quantizat "linear_fc1.weight", "linear_fc2.weight", ]: - return _quantize_moe_params(converted_named_params, group_size, ignore_rules) + return _quantize_moe_params(converted_named_params, ignore_rules) # for other parameters, we just return the original converted_named_params return converted_named_params -def _resolve_group_size(quantization_config): - group_size = quantization_config.get("group_size", NVFP4_GROUP_SIZE) - if group_size != NVFP4_GROUP_SIZE: - raise ValueError(f"NVFP4 group_size must be {NVFP4_GROUP_SIZE}, got {group_size}.") - return group_size - - -def _quantize_moe_params(converted_named_params, group_size, ignore_rules): +def _quantize_moe_params(converted_named_params, ignore_rules): shared_global_amax = {} gated_candidates = {} for converted_name, param in converted_named_params: base, role = _split_gated_pair_name(converted_name) if base is None or role is None: continue - if _should_quantize_param(converted_name, param, group_size, ignore_rules): - gated_candidates.setdefault(base, {})[role] = param + if _should_quantize_param(converted_name, param, ignore_rules): + roles = gated_candidates.setdefault(base, {}) + if role in roles: + raise ValueError( + f"NVFP4 requires a single complete gate/up pair per conversion batch; " + f"found duplicate {role} tensor for {base}." + ) + roles[role] = param for base, roles in gated_candidates.items(): - if "gate" in roles and "up" in roles: - gate_amax = roles["gate"].abs().max().to(torch.float32) - up_amax = roles["up"].abs().max().to(torch.float32) - shared_global_amax[base] = torch.max(gate_amax, up_amax) + if set(roles) != {"gate", "up"}: + present = ", ".join(sorted(roles)) + raise ValueError( + f"NVFP4 requires gate/up tensors to be quantized together so they can share " + f"one global amax; found only {{{present}}} for {base}." + ) + gate_amax = roles["gate"].abs().max().to(torch.float32) + up_amax = roles["up"].abs().max().to(torch.float32) + shared_global_amax[base] = torch.max(gate_amax, up_amax) quantize_named_params = [] for converted_name, param in converted_named_params: - if not _should_quantize_param(converted_name, param, group_size, ignore_rules): + if not _should_quantize_param(converted_name, param, ignore_rules): quantize_named_params.append((converted_name, param)) continue base, _role = _split_gated_pair_name(converted_name) global_amax = shared_global_amax.get(base) if base else None - qweight, block_scale, weight_scale_2 = quantize_nvfp4(param, global_amax=global_amax, group_size=group_size) + qweight, block_scale, weight_scale_2 = quantize_nvfp4(param, global_amax=global_amax) quantize_named_params.append((converted_name, qweight)) quantize_named_params.append((converted_name.replace(".weight", ".weight_scale"), block_scale)) quantize_named_params.append((converted_name.replace(".weight", ".weight_scale_2"), weight_scale_2)) @@ -122,7 +141,7 @@ def _quantize_moe_params(converted_named_params, group_size, ignore_rules): return quantize_named_params -def _should_quantize_param(name, weight, group_size, ignore_rules): +def _should_quantize_param(name, weight, ignore_rules): if ignore_rules and _is_ignored(name, ignore_rules): return False if not name.endswith(".weight"): @@ -131,8 +150,8 @@ def _should_quantize_param(name, weight, group_size, ignore_rules): return False if weight.dim() < 2: return False - if weight.shape[-1] % group_size != 0: - raise ValueError(f"Last dim {weight.shape[-1]} must be divisible by {group_size} for NVFP4 ({name}).") + if weight.shape[-1] % NVFP4_GROUP_SIZE != 0: + raise ValueError(f"Last dim {weight.shape[-1]} must be divisible by {NVFP4_GROUP_SIZE} for NVFP4 ({name}).") return True @@ -143,34 +162,33 @@ def _split_gated_pair_name(name: str): return None, None -def cast_to_fp4x2(x: torch.Tensor) -> torch.Tensor: - """Quantize a tensor to FP4 E2M1 and pack two values per byte.""" - result = torch.zeros_like(x, dtype=torch.uint8) - result[(x >= 0.0) & (x <= 0.25)] = 0 - result[(x > 0.25) & (x < 0.75)] = 1 - result[(x >= 0.75) & (x <= 1.25)] = 2 - result[(x > 1.25) & (x < 1.75)] = 3 - result[(x >= 1.75) & (x <= 2.5)] = 4 - result[(x > 2.5) & (x < 3.5)] = 5 - result[(x >= 3.5) & (x <= 5.0)] = 6 - result[x > 5.0] = 7 - - result[(x >= -0.25) & (x < -0.0)] = 8 - result[(x < -0.25) & (x > -0.75)] = 9 - result[(x <= -0.75) & (x >= -1.25)] = 10 - result[(x < -1.25) & (x > -1.75)] = 11 - result[(x <= -1.75) & (x >= -2.5)] = 12 - result[(x < -2.5) & (x > -3.5)] = 13 - result[(x <= -3.5) & (x >= -5.0)] = 14 - result[x < -5.0] = 15 - - return result[:, ::2] + result[:, 1::2] * 16 +def _nvfp4_global_decode_scale_te(global_amax: torch.Tensor) -> torch.Tensor: + fp4_max = torch.tensor(FP4_E2M1_MAX, device=global_amax.device, dtype=torch.float32) + fp8_max = torch.tensor(FP8_E4M3_MAX, device=global_amax.device, dtype=torch.float32) + global_encode_scale = torch.div(fp8_max * fp4_max, global_amax.to(torch.float32)) + global_encode_scale = torch.min( + global_encode_scale, + torch.tensor( + torch.finfo(torch.float32).max, + device=global_encode_scale.device, + dtype=torch.float32, + ), + ) + if global_encode_scale.numel() == 1: + if global_encode_scale == torch.tensor(0.0, device=global_amax.device, dtype=torch.float32): + global_encode_scale = torch.tensor(1.0, device=global_amax.device, dtype=torch.float32) + else: + global_encode_scale = torch.where( + global_encode_scale == 0.0, + torch.ones_like(global_encode_scale), + global_encode_scale, + ) + return torch.div(1.0, global_encode_scale) def _quantize_nvfp4_1d( weight: torch.Tensor, global_amax: torch.Tensor | None = None, - group_size: int = NVFP4_GROUP_SIZE, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """ NVFP4 1D quantization (tile shape = 1x16), adapted from @@ -178,63 +196,38 @@ def _quantize_nvfp4_1d( Returns: qweight: uint8 packed fp4, shape (M, K // 2) - block_scale: float8_e4m3fn, shape (M, K // group_size) + block_scale: float8_e4m3fn, shape (M, K // 16) global_scale: float32 scalar tensor """ weight = weight.contiguous() m, n = weight.shape - if n % group_size != 0: - raise ValueError(f"NVFP4 requires K divisible by {group_size}, got {n}.") + if n % NVFP4_GROUP_SIZE != 0: + raise ValueError(f"NVFP4 requires K divisible by {NVFP4_GROUP_SIZE}, got {n}.") - weight_f = weight.to(torch.float32) if global_amax is None: - global_amax = torch.max(torch.abs(weight_f)) + global_amax = torch.max(torch.abs(weight.to(torch.float32))) else: global_amax = global_amax.to(device=weight.device, dtype=torch.float32) - if global_amax.item() == 0.0: - qweight = torch.zeros((m, n // 2), dtype=torch.uint8, device=weight.device) - block_scale = torch.zeros( - (m, n // group_size), - dtype=torch.float8_e4m3fn, - device=weight.device, - ) - global_scale = torch.tensor(1.0, device=weight.device, dtype=torch.float32) - return qweight, block_scale, global_scale - fp4_max = torch.tensor(FP4_E2M1_MAX, device=weight.device, dtype=torch.float32) - fp8_max = torch.tensor(FP8_E4M3_MAX, device=weight.device, dtype=torch.float32) + from transformer_engine.pytorch.custom_recipes.quantization_nvfp4 import NVFP4QuantizerRef - global_encode_scale = torch.div(fp8_max * fp4_max, global_amax) - # global_encode_scale = torch.tensor(1.0, device=weight.device, dtype=torch.float32) - global_encode_scale = torch.min( - global_encode_scale, - torch.tensor(torch.finfo(torch.float32).max, device=weight.device, dtype=torch.float32), + qweight, block_scale = NVFP4QuantizerRef._quantize_blockwise_reference( + weight, + global_amax, + NVFP4_GROUP_SIZE, + 1, + pow_2_scales=False, + eps=0.0, ) - if global_encode_scale.item() == 0.0: - global_encode_scale = torch.tensor(1.0, device=weight.device, dtype=torch.float32) - global_decode_scale = torch.div(1.0, global_encode_scale) - - weight_blocks = weight_f.view(m, n // group_size, group_size) - vec_max = torch.amax(torch.abs(weight_blocks), dim=-1, keepdim=True) - decode_scale = torch.div(vec_max, fp4_max) * global_encode_scale - decode_scale = torch.clamp(decode_scale, min=-fp8_max, max=fp8_max).to(torch.float8_e4m3fn) - - encode_scale = torch.div(1.0, decode_scale.to(torch.float32) * global_decode_scale) - scaled = weight_blocks * encode_scale - clipped = torch.clamp(scaled, -fp4_max, fp4_max).reshape(m, n) - - qweight = cast_to_fp4x2(clipped) - block_scale = decode_scale.squeeze(-1) - return qweight, block_scale, global_decode_scale + return qweight, block_scale, _nvfp4_global_decode_scale_te(global_amax) def quantize_nvfp4( weight: torch.Tensor, global_amax: torch.Tensor | None = None, - group_size: int = NVFP4_GROUP_SIZE, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: if weight.dim() == 2: - return _quantize_nvfp4_1d(weight, global_amax=global_amax, group_size=group_size) + return _quantize_nvfp4_1d(weight, global_amax=global_amax) if weight.dim() == 3: if global_amax is not None: raise ValueError("global_amax override is only supported for 2D weights.") @@ -242,7 +235,7 @@ def quantize_nvfp4( block_scales = [] global_scales = [] for idx in range(weight.shape[0]): - qweight, block_scale, global_scale = _quantize_nvfp4_1d(weight[idx], group_size=group_size) + qweight, block_scale, global_scale = _quantize_nvfp4_1d(weight[idx]) qweights.append(qweight) block_scales.append(block_scale) global_scales.append(global_scale) diff --git a/tests/fast/utils/test_mxfp8_quantizer.py b/tests/fast/utils/test_mxfp8_quantizer.py new file mode 100644 index 00000000000..fb4c2d3295d --- /dev/null +++ b/tests/fast/utils/test_mxfp8_quantizer.py @@ -0,0 +1,161 @@ +from tests.ci.ci_register import register_cuda_ci + +register_cuda_ci(est_time=60, suite="stage-b-fast-1-gpu", num_gpus=1) + + +import pytest +import torch +from tools.convert_hf_to_mxfp8 import quantize_mxfp8 as tool_quantize_mxfp8 +from tools.convert_hf_to_mxfp8 import should_quantize as tool_should_quantize_mxfp8 +from transformer_engine.pytorch import MXFP8Quantizer +from transformer_engine.pytorch.constants import TE_DType + +from miles.backends.megatron_utils.megatron_to_hf.processors.quantizer_mxfp8 import ( + _quantize_param as processor_quantize_mxfp8_param, +) +from miles.backends.megatron_utils.megatron_to_hf.processors.quantizer_mxfp8 import quantize_params_mxfp8 + +MXFP8_GROUP_SIZE = 32 +MXFP8_SHAPES = [ + (1, 64), + (1, 1024), + (3, 128), + (16, 64), + (64, 128), + (128, 64), + (256, 128), + (512, 256), + (128, 1024), + (1024, 2048), + (7168, 2048), + (2048, 7168), + (128, 16384), +] + + +def _make_weight(init_data: str, dtype: torch.dtype, shape: tuple[int, int], device: str) -> torch.Tensor: + m, n = shape + if init_data == "random": + return 16 * torch.randn((m, n), dtype=dtype, device=device) + if init_data == "boundary": + base = torch.linspace(-512.0, 512.0, steps=n // 2, dtype=torch.float32, device=device) + eps = torch.full_like(base, 1e-3) + eps = torch.maximum(eps, 1e-4 * torch.ones_like(base)) + row = torch.empty(n, dtype=torch.float32, device=device) + row[0::2] = base - eps + row[1::2] = base + eps + return row.unsqueeze(0).repeat(m, 1).to(dtype=dtype) + if init_data == "zeros": + return torch.zeros((m, n), dtype=dtype, device=device) + if init_data == "maxes": + return torch.full((m, n), torch.finfo(dtype).max, dtype=dtype, device=device) + raise ValueError(f"Unknown init_data: {init_data}") + + +def _processor_quantize_mxfp8(weight: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + quantized = dict(processor_quantize_mxfp8_param("model.layers.0.mlp.experts.0.down_proj.weight", weight)) + return ( + quantized["model.layers.0.mlp.experts.0.down_proj.weight"], + quantized["model.layers.0.mlp.experts.0.down_proj.weight_scale_inv"], + ) + + +def _te_mxfp8_reference(weight: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + weight = weight.contiguous() + m, k = weight.shape + if m % MXFP8_GROUP_SIZE != 0: + padded_m = ((m + MXFP8_GROUP_SIZE - 1) // MXFP8_GROUP_SIZE) * MXFP8_GROUP_SIZE + padded_weight = torch.zeros((padded_m, k), dtype=weight.dtype, device=weight.device) + padded_weight[:m].copy_(weight) + else: + padded_weight = weight + + quantizer = MXFP8Quantizer(fp8_dtype=TE_DType[torch.float8_e4m3fn], rowwise=True, columnwise=False) + quantized = quantizer(padded_weight) + return ( + quantized._rowwise_data[:m].contiguous(), + quantized._rowwise_scale_inv[:m, : k // MXFP8_GROUP_SIZE].contiguous(), + ) + + +def test_mxfp8_quantize_params_respects_extra_high_precision_layers_megatron(): + weight = torch.randn((4, MXFP8_GROUP_SIZE), dtype=torch.bfloat16) + converted_named_params = [ + ("model.layers.0.mlp.experts.0.down_proj.weight", weight), + ] + args = type("Args", (), {"extra_high_precision_layers_megatron": ("linear_fc2",)})() + + out = quantize_params_mxfp8( + args=args, + megatron_name="decoder.layers.0.mlp.experts.linear_fc2.weight0", + converted_named_params=converted_named_params, + quantization_config={"quant_method": "mxfp8"}, + ) + + assert out is converted_named_params + + +@pytest.mark.parametrize("layer_idx", [0, 3]) +def test_mxfp8_quantize_params_respects_first_last_layers_bf16(layer_idx): + weight = torch.randn((4, MXFP8_GROUP_SIZE), dtype=torch.bfloat16) + converted_named_params = [ + ("model.layers.0.mlp.experts.0.down_proj.weight", weight), + ] + args = type( + "Args", + (), + { + "first_last_layers_bf16": True, + "num_layers": 4, + "num_layers_at_start_in_bf16": 1, + "num_layers_at_end_in_bf16": 1, + }, + )() + + out = quantize_params_mxfp8( + args=args, + megatron_name=f"decoder.layers.{layer_idx}.mlp.experts.linear_fc2.weight0", + converted_named_params=converted_named_params, + quantization_config={"quant_method": "mxfp8"}, + ) + + assert out is converted_named_params + + +def test_mxfp8_hf_should_quantize_respects_extra_high_precision_layers_hf(): + weight = torch.randn((4, MXFP8_GROUP_SIZE), dtype=torch.bfloat16) + + assert not tool_should_quantize_mxfp8( + "model.layers.0.mlp.experts.0.down_proj.weight", + weight, + skip_weight_substrings=("mlp.experts.0",), + ) + assert tool_should_quantize_mxfp8( + "model.layers.0.mlp.experts.0.down_proj.weight", + weight, + skip_weight_substrings=("mlp.experts.1",), + ) + + +@pytest.mark.parametrize( + "quantize_fn", + [_processor_quantize_mxfp8, tool_quantize_mxfp8], + ids=["processor", "convert_tool"], +) +@pytest.mark.parametrize("shape", MXFP8_SHAPES) +@pytest.mark.parametrize("dtype", [torch.bfloat16], ids=str) +@pytest.mark.parametrize("init_data", ["random", "boundary", "zeros", "maxes"]) +def test_mxfp8_quantize_matches_reference(quantize_fn, shape, dtype, init_data): + device = "cuda" + torch.manual_seed(42) + + weight = _make_weight(init_data, dtype, shape, device) + qweight, scale = quantize_fn(weight) + qweight_ref, scale_ref = _te_mxfp8_reference(weight) + + assert qweight.shape == weight.shape + assert qweight.dtype == torch.float8_e4m3fn + assert scale.shape == (*weight.shape[:-1], weight.shape[-1] // MXFP8_GROUP_SIZE) + assert scale.dtype == torch.uint8 + torch.testing.assert_close(qweight.view(dtype=torch.uint8), qweight_ref.view(dtype=torch.uint8), rtol=0, atol=0) + torch.testing.assert_close(scale, scale_ref, rtol=0, atol=0) diff --git a/tests/fast/utils/test_nvfp4_quantizer.py b/tests/fast/utils/test_nvfp4_quantizer.py new file mode 100644 index 00000000000..3721f665600 --- /dev/null +++ b/tests/fast/utils/test_nvfp4_quantizer.py @@ -0,0 +1,163 @@ +from tests.ci.ci_register import register_cuda_ci + +register_cuda_ci(est_time=60, suite="stage-b-fast-1-gpu", num_gpus=1) + + +import pytest +import torch +from tools.convert_hf_to_nvfp4 import quantize_nvfp4 as tool_quantize_nvfp4 +from tools.convert_hf_to_nvfp4 import should_quantize as tool_should_quantize_nvfp4 +from transformer_engine.pytorch.custom_recipes.quantization_nvfp4 import NVFP4QuantizerRef + +from miles.backends.megatron_utils.megatron_to_hf.processors.quantizer_nvfp4 import ( + NVFP4_GROUP_SIZE, + _nvfp4_global_decode_scale_te, +) +from miles.backends.megatron_utils.megatron_to_hf.processors.quantizer_nvfp4 import ( + quantize_nvfp4 as processor_quantize_nvfp4, +) +from miles.backends.megatron_utils.megatron_to_hf.processors.quantizer_nvfp4 import quantize_params_nvfp4 + +NVFP4_SHAPES = [ + (1, 64), + (1, 1024), + (3, 128), + (16, 64), + (64, 128), + (128, 64), + (256, 128), + (512, 256), + (128, 1024), + (1024, 2048), + (7168, 2048), + (2048, 7168), + (128, 16384), +] + + +def _make_weight(init_data: str, dtype: torch.dtype, shape: tuple[int, int], device: str) -> torch.Tensor: + m, n = shape + if init_data == "random": + return torch.randn((m, n), dtype=dtype, device=device) + if init_data == "boundary": + base = torch.linspace(-12.0, 12.0, steps=n // 2, dtype=torch.float32, device=device) + eps = torch.full_like(base, 1e-3) + eps = torch.maximum(eps, 1e-4 * torch.ones_like(base)) + row = torch.empty(n, dtype=torch.float32, device=device) + row[0::2] = base - eps + row[1::2] = base + eps + return row.unsqueeze(0).repeat(m, 1).to(dtype=dtype) + if init_data == "zeros": + return torch.zeros((m, n), dtype=dtype, device=device) + if init_data == "maxes": + return torch.full((m, n), torch.finfo(dtype).max, dtype=dtype, device=device) + raise ValueError(f"Unknown init_data: {init_data}") + + +def _te_nvfp4_reference(weight: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + weight = weight.contiguous() + global_amax = torch.max(torch.abs(weight.to(torch.float32))) + qweight, block_scale = NVFP4QuantizerRef._quantize_blockwise_reference( + weight, + global_amax, + NVFP4_GROUP_SIZE, + 1, + pow_2_scales=False, + eps=0.0, + ) + return qweight, block_scale, _nvfp4_global_decode_scale_te(global_amax) + + +def test_nvfp4_quantize_params_requires_complete_gated_pair(): + weight = torch.randn((4, NVFP4_GROUP_SIZE), dtype=torch.float32) + with pytest.raises(ValueError, match="requires gate/up tensors to be quantized together"): + quantize_params_nvfp4( + args=None, + megatron_name="decoder.layers.0.mlp.experts.linear_fc1.weight0", + converted_named_params=[ + ("model.layers.0.mlp.experts.0.gate_proj.weight", weight), + ], + quantization_config={"quant_method": "nvfp4"}, + ) + + +def test_nvfp4_quantize_params_respects_extra_high_precision_layers_megatron(): + weight = torch.randn((4, NVFP4_GROUP_SIZE), dtype=torch.bfloat16) + converted_named_params = [ + ("model.layers.0.mlp.experts.0.gate_proj.weight", weight), + ("model.layers.0.mlp.experts.0.up_proj.weight", weight), + ] + args = type("Args", (), {"extra_high_precision_layers_megatron": ("linear_fc1",)})() + + out = quantize_params_nvfp4( + args=args, + megatron_name="decoder.layers.0.mlp.experts.linear_fc1.weight0", + converted_named_params=converted_named_params, + quantization_config={"quant_method": "nvfp4"}, + ) + + assert out is converted_named_params + + +@pytest.mark.parametrize("layer_idx", [0, 3]) +def test_nvfp4_quantize_params_respects_first_last_layers_bf16(layer_idx): + weight = torch.randn((4, NVFP4_GROUP_SIZE), dtype=torch.bfloat16) + converted_named_params = [ + ("model.layers.0.mlp.experts.0.gate_proj.weight", weight), + ("model.layers.0.mlp.experts.0.up_proj.weight", weight), + ] + args = type( + "Args", + (), + { + "first_last_layers_bf16": True, + "num_layers": 4, + "num_layers_at_start_in_bf16": 1, + "num_layers_at_end_in_bf16": 1, + }, + )() + + out = quantize_params_nvfp4( + args=args, + megatron_name=f"decoder.layers.{layer_idx}.mlp.experts.linear_fc1.weight0", + converted_named_params=converted_named_params, + quantization_config={"quant_method": "nvfp4"}, + ) + + assert out is converted_named_params + + +def test_nvfp4_hf_should_quantize_respects_extra_high_precision_layers_hf(): + weight = torch.randn((4, NVFP4_GROUP_SIZE), dtype=torch.bfloat16) + + assert not tool_should_quantize_nvfp4( + "model.layers.0.mlp.experts.0.gate_proj.weight", + weight, + skip_weight_substrings=("mlp.experts.0",), + ) + assert tool_should_quantize_nvfp4( + "model.layers.0.mlp.experts.0.gate_proj.weight", + weight, + skip_weight_substrings=("mlp.experts.1",), + ) + + +@pytest.mark.parametrize( + "quantize_fn", + [processor_quantize_nvfp4, tool_quantize_nvfp4], + ids=["processor", "convert_tool"], +) +@pytest.mark.parametrize("shape", NVFP4_SHAPES) +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16], ids=str) +@pytest.mark.parametrize("init_data", ["random", "boundary", "zeros", "maxes"]) +def test_nvfp4_quantize_matches_te_reference_bitwise(quantize_fn, shape, dtype, init_data): + device = "cuda" + torch.manual_seed(42) + + weight = _make_weight(init_data, dtype, shape, device) + qweight, block_scale, global_scale = quantize_fn(weight) + qweight_ref, block_scale_ref, global_scale_ref = _te_nvfp4_reference(weight) + + torch.testing.assert_close(qweight, qweight_ref, rtol=0, atol=0) + torch.testing.assert_close(block_scale.view(torch.uint8), block_scale_ref.view(torch.uint8), rtol=0, atol=0) + torch.testing.assert_close(global_scale, global_scale_ref, rtol=0, atol=0) diff --git a/tools/convert_hf_to_nvfp4.py b/tools/convert_hf_to_nvfp4.py index 2de2183a412..f8d1548ef60 100644 --- a/tools/convert_hf_to_nvfp4.py +++ b/tools/convert_hf_to_nvfp4.py @@ -1,9 +1,14 @@ """ python tools/convert_hf_to_nvfp4.py [-h] [--model-dir MODEL_DIR] [--save-dir SAVE_DIR] - [--device DEVICE] [--keep-last-n KEEP_LAST_N] [--keep-first-n KEEP_FIRST_N] + [--device DEVICE] + [--num-layers-at-start-in-bf16 NUM_LAYERS_AT_START_IN_BF16] + [--num-layers-at-end-in-bf16 NUM_LAYERS_AT_END_IN_BF16] + [--extra-high-precision-layers-hf ...] Convert a BF16/FP16/FP32 HF safetensors checkpoint to NVFP4 (E2M1) for MoE expert GEMMs only. Dense linear layers are left unmodified. +Use --extra-high-precision-layers-hf to keep additional HF weight-name +substrings unquantized. This follows the NVFP4 reference quantization in Transformer Engine and uses 1D block scaling (NVTE_NVFP4_1D_SCALING, group size = 16). @@ -60,20 +65,12 @@ def _is_moe_expert_weight_name(name: str) -> bool: return any(name.endswith(suffix) for suffix in EXPERT_WEIGHT_SUFFIXES) -def _extract_layer_id(name: str) -> int | None: - parts = name.split(".") - for idx, part in enumerate(parts): - if part == "layers" and idx + 1 < len(parts): - layer_id = parts[idx + 1] - if layer_id.isdigit(): - return int(layer_id) - return None - - def _get_num_hidden_layers(model_dir: str) -> int: config_path = os.path.join(model_dir, "config.json") if not os.path.exists(config_path): - raise ValueError("config.json is required to use --keep-first-n or --keep-last-n.") + raise ValueError( + "config.json is required to use --num-layers-at-start-in-bf16 or --num-layers-at-end-in-bf16." + ) cfg = json.load(open(config_path)) num_layers = cfg.get("num_hidden_layers") if num_layers is None and isinstance(cfg.get("text_config"), dict): @@ -83,65 +80,13 @@ def _get_num_hidden_layers(model_dir: str) -> int: return int(num_layers) -def _get_last_n_layer_ids(num_layers: int, keep_last_n: int) -> set[int]: - if keep_last_n <= 0: - return set() - start = max(0, num_layers - keep_last_n) - return set(range(start, num_layers)) - - -def _get_first_n_layer_ids(num_layers: int, keep_first_n: int) -> set[int]: - if keep_first_n <= 0: - return set() - end = min(num_layers, keep_first_n) - return set(range(0, end)) - - -def _build_keep_last_n_ignore_list(num_layers: int, keep_last_n: int) -> list[str]: - if keep_last_n <= 0: - return [] - start = max(0, num_layers - keep_last_n) - ignore_list = [] - for layer_id in range(start, num_layers): - prefix = f"model.layers.{layer_id}" - ignore_list.extend( - [ - f"{prefix}.self_attn.qkv_proj", - f"{prefix}.self_attn.o_proj", - f"{prefix}.mlp", - f"{prefix}.mlp.experts", - ] - ) - return ignore_list - - -def _build_keep_first_n_ignore_list(num_layers: int, keep_first_n: int) -> list[str]: - if keep_first_n <= 0: - return [] - end = min(num_layers, keep_first_n) - ignore_list = [] - for layer_id in range(0, end): - prefix = f"model.layers.{layer_id}" - ignore_list.extend( - [ - f"{prefix}.self_attn.qkv_proj", - f"{prefix}.self_attn.o_proj", - f"{prefix}.mlp", - f"{prefix}.mlp.experts", - ] - ) - return ignore_list - - def should_quantize( name: str, weight: torch.Tensor, - skip_layers: set[int] | None = None, + skip_weight_substrings: tuple[str, ...] = (), ) -> bool: - if skip_layers: - layer_id = _extract_layer_id(name) - if layer_id is not None and layer_id in skip_layers: - return False + if any(substr in name for substr in skip_weight_substrings): + return False if not _is_moe_expert_weight_name(name): return False if weight.dtype not in (torch.float16, torch.bfloat16, torch.float32): @@ -155,28 +100,28 @@ def should_quantize( return True -def cast_to_fp4x2(x: torch.Tensor) -> torch.Tensor: - """Quantize a tensor to FP4 E2M1 and pack two values per byte.""" - result = torch.zeros_like(x, dtype=torch.uint8) - result[(x >= 0.0) & (x <= 0.25)] = 0 - result[(x > 0.25) & (x < 0.75)] = 1 - result[(x >= 0.75) & (x <= 1.25)] = 2 - result[(x > 1.25) & (x < 1.75)] = 3 - result[(x >= 1.75) & (x <= 2.5)] = 4 - result[(x > 2.5) & (x < 3.5)] = 5 - result[(x >= 3.5) & (x <= 5.0)] = 6 - result[x > 5.0] = 7 - - result[(x >= -0.25) & (x < -0.0)] = 8 - result[(x < -0.25) & (x > -0.75)] = 9 - result[(x <= -0.75) & (x >= -1.25)] = 10 - result[(x < -1.25) & (x > -1.75)] = 11 - result[(x <= -1.75) & (x >= -2.5)] = 12 - result[(x < -2.5) & (x > -3.5)] = 13 - result[(x <= -3.5) & (x >= -5.0)] = 14 - result[x < -5.0] = 15 - - return result[:, ::2] + result[:, 1::2] * 16 +def _nvfp4_global_decode_scale_te(global_amax: torch.Tensor) -> torch.Tensor: + fp4_max = torch.tensor(FP4_E2M1_MAX, device=global_amax.device, dtype=torch.float32) + fp8_max = torch.tensor(FP8_E4M3_MAX, device=global_amax.device, dtype=torch.float32) + global_encode_scale = torch.div(fp8_max * fp4_max, global_amax.to(torch.float32)) + global_encode_scale = torch.min( + global_encode_scale, + torch.tensor( + torch.finfo(torch.float32).max, + device=global_encode_scale.device, + dtype=torch.float32, + ), + ) + if global_encode_scale.numel() == 1: + if global_encode_scale == torch.tensor(0.0, device=global_amax.device, dtype=torch.float32): + global_encode_scale = torch.tensor(1.0, device=global_amax.device, dtype=torch.float32) + else: + global_encode_scale = torch.where( + global_encode_scale == 0.0, + torch.ones_like(global_encode_scale), + global_encode_scale, + ) + return torch.div(1.0, global_encode_scale) def _quantize_nvfp4_1d( @@ -197,45 +142,22 @@ def _quantize_nvfp4_1d( if n % NVFP4_GROUP_SIZE != 0: raise ValueError(f"NVFP4 requires K divisible by {NVFP4_GROUP_SIZE}, got {n}.") - weight_f = weight.to(torch.float32) if global_amax is None: - global_amax = torch.max(torch.abs(weight_f)) + global_amax = torch.max(torch.abs(weight.to(torch.float32))) else: global_amax = global_amax.to(device=weight.device, dtype=torch.float32) - if global_amax.item() == 0.0: - qweight = torch.zeros((m, n // 2), dtype=torch.uint8, device=weight.device) - block_scale = torch.zeros( - (m, n // NVFP4_GROUP_SIZE), - dtype=torch.float8_e4m3fn, - device=weight.device, - ) - global_scale = torch.tensor(1.0, device=weight.device, dtype=torch.float32) - return qweight, block_scale, global_scale - fp4_max = torch.tensor(FP4_E2M1_MAX, device=weight.device, dtype=torch.float32) - fp8_max = torch.tensor(FP8_E4M3_MAX, device=weight.device, dtype=torch.float32) + from transformer_engine.pytorch.custom_recipes.quantization_nvfp4 import NVFP4QuantizerRef - global_encode_scale = torch.div(fp8_max * fp4_max, global_amax) - global_encode_scale = torch.min( - global_encode_scale, - torch.tensor(torch.finfo(torch.float32).max, device=weight.device, dtype=torch.float32), + qweight, block_scale = NVFP4QuantizerRef._quantize_blockwise_reference( + weight, + global_amax, + NVFP4_GROUP_SIZE, + 1, + pow_2_scales=False, + eps=0.0, ) - if global_encode_scale.item() == 0.0: - global_encode_scale = torch.tensor(1.0, device=weight.device, dtype=torch.float32) - global_decode_scale = torch.div(1.0, global_encode_scale) - - weight_blocks = weight_f.view(m, n // NVFP4_GROUP_SIZE, NVFP4_GROUP_SIZE) - vec_max = torch.amax(torch.abs(weight_blocks), dim=-1, keepdim=True) - decode_scale = torch.div(vec_max, fp4_max) * global_encode_scale - decode_scale = torch.clamp(decode_scale, min=-fp8_max, max=fp8_max).to(torch.float8_e4m3fn) - - encode_scale = torch.div(1.0, decode_scale.to(torch.float32) * global_decode_scale) - scaled = weight_blocks * encode_scale - clipped = torch.clamp(scaled, -fp4_max, fp4_max).reshape(m, n) - - qweight = cast_to_fp4x2(clipped) - block_scale = decode_scale.squeeze(-1) - return qweight, block_scale, global_decode_scale + return qweight, block_scale, _nvfp4_global_decode_scale_te(global_amax) def quantize_nvfp4( @@ -352,7 +274,7 @@ def _collect_shared_global_amax( input_path: str, safetensors_files: list[str], device: str, - skip_layers: set[int], + skip_weight_substrings: tuple[str, ...], ) -> dict[str, torch.Tensor]: """Collect shared gate/up amax across all shards to keep w1/w3 scales equal.""" gate_amax: dict[str, torch.Tensor] = {} @@ -361,7 +283,7 @@ def _collect_shared_global_amax( with safetensors.safe_open(os.path.join(input_path, filename), framework="pt", device=device) as f: for key in f.keys(): tensor = f.get_tensor(key) - if not should_quantize(key, tensor, skip_layers): + if not should_quantize(key, tensor, skip_weight_substrings=skip_weight_substrings): continue base, role = _split_gated_pair_name(key) if base is None or role is None: @@ -388,7 +310,10 @@ def process_file( filename: str, result_collector: ConversionResult, device: str, - skip_layers: set[int], + num_hidden_layers: int, + num_layers_at_start_in_bf16: int, + num_layers_at_end_in_bf16: int, + extra_high_precision_layers_hf: tuple[str, ...], shared_global_amax: dict[str, torch.Tensor], ) -> None: if not filename.endswith(".safetensors"): @@ -396,11 +321,24 @@ def process_file( modules_to_not_convert: list[str] = [] q_weights: dict[str, torch.Tensor] = {} + head_end_idx = num_layers_at_start_in_bf16 + tail_start_idx = num_hidden_layers - num_layers_at_end_in_bf16 + dynamic_skip_layer_prefixes: set[str] = set() + dynamic_skip_layer_prefixes.update({f"model.layers.{i}." for i in range(0, head_end_idx)}) + dynamic_skip_layer_prefixes.update({f"model.layers.{i}." for i in range(tail_start_idx, num_hidden_layers)}) + + if num_layers_at_end_in_bf16 > 0 or num_layers_at_start_in_bf16 > 0: + modules_to_not_convert.extend(sorted(dynamic_skip_layer_prefixes)) + + dynamic_skip_substrings = ( + *extra_high_precision_layers_hf, + *sorted(dynamic_skip_layer_prefixes), + ) with safetensors.safe_open(os.path.join(input_path, filename), framework="pt", device=device) as f: for key in f.keys(): tensor = f.get_tensor(key) - if should_quantize(key, tensor, skip_layers): + if should_quantize(key, tensor, skip_weight_substrings=dynamic_skip_substrings): base, _role = _split_gated_pair_name(key) global_amax = shared_global_amax.get(base) if base else None qweight, block_scale, weight_scale_2 = quantize_nvfp4(tensor, global_amax=global_amax) @@ -419,7 +357,14 @@ def process_file( result_collector.add_result(filename, q_weights, modules_to_not_convert) -def convert_nvfp4(model_dir: str, save_dir: str, device: str, keep_last_n: int, keep_first_n: int) -> None: +def convert_nvfp4( + model_dir: str, + save_dir: str, + device: str, + num_layers_at_start_in_bf16: int = 0, + num_layers_at_end_in_bf16: int = 0, + extra_high_precision_layers_hf: tuple[str, ...] = (), +) -> None: input_path = os.path.abspath(model_dir) output_path = os.path.abspath(save_dir) os.makedirs(output_path, exist_ok=True) @@ -430,16 +375,22 @@ def convert_nvfp4(model_dir: str, save_dir: str, device: str, keep_last_n: int, safetensors_files = [f for f in os.listdir(input_path) if f.endswith(".safetensors")] - num_layers = _get_num_hidden_layers(input_path) if (keep_last_n > 0 or keep_first_n > 0) else 0 - skip_layers = _get_last_n_layer_ids(num_layers, keep_last_n) | _get_first_n_layer_ids(num_layers, keep_first_n) - keep_last_ignore = _build_keep_last_n_ignore_list(num_layers, keep_last_n) - keep_first_ignore = _build_keep_first_n_ignore_list(num_layers, keep_first_n) + num_hidden_layers = _get_num_hidden_layers(input_path) + head_end_idx = num_layers_at_start_in_bf16 + tail_start_idx = num_hidden_layers - num_layers_at_end_in_bf16 + dynamic_skip_layer_prefixes: set[str] = set() + dynamic_skip_layer_prefixes.update({f"model.layers.{i}." for i in range(0, head_end_idx)}) + dynamic_skip_layer_prefixes.update({f"model.layers.{i}." for i in range(tail_start_idx, num_hidden_layers)}) + dynamic_skip_substrings = ( + *extra_high_precision_layers_hf, + *sorted(dynamic_skip_layer_prefixes), + ) shared_global_amax = _collect_shared_global_amax( input_path=input_path, safetensors_files=safetensors_files, device=device, - skip_layers=skip_layers, + skip_weight_substrings=dynamic_skip_substrings, ) result_collector = ConversionResult() for filename in tqdm(safetensors_files, desc="Processing files"): @@ -449,14 +400,17 @@ def convert_nvfp4(model_dir: str, save_dir: str, device: str, keep_last_n: int, filename, result_collector, device, - skip_layers, + num_hidden_layers, + num_layers_at_start_in_bf16, + num_layers_at_end_in_bf16, + extra_high_precision_layers_hf, shared_global_amax, ) gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() - ignore_list = _augment_ignore_list(result_collector.modules_to_not_convert + keep_last_ignore + keep_first_ignore) + ignore_list = _augment_ignore_list(result_collector.modules_to_not_convert) config_path = os.path.join(input_path, "config.json") if os.path.exists(config_path): @@ -488,18 +442,25 @@ def main() -> None: help="Torch device to run quantization on (default: cuda).", ) parser.add_argument( - "--keep-last-n", + "--num-layers-at-start-in-bf16", type=int, default=0, - help="Keep the last N transformer layers unquantized (BF16/FP16).", + help="Keep first N decoder layers in BF16 and do not quantize them.", ) parser.add_argument( - "--keep-first-n", + "--num-layers-at-end-in-bf16", type=int, default=0, - help="Keep the first N transformer layers unquantized (BF16/FP16).", + help="Keep last N decoder layers in BF16 and do not quantize them.", ) - args = parser.parse_args() + parser.add_argument( + "--extra-high-precision-layers-hf", + type=str, + nargs="*", + default=[], + help="Additional HF weight-name substrings to keep unquantized.", + ) + args, _ = parser.parse_known_args() if isinstance(args.device, str) and args.device.isdigit(): device = torch.device(f"cuda:{args.device}") @@ -519,7 +480,16 @@ def main() -> None: elif not os.path.isdir(args.save_dir): raise ValueError("The save_dir should be a directory.") - convert_nvfp4(args.model_dir, args.save_dir, str(device), args.keep_last_n, args.keep_first_n) + convert_nvfp4( + args.model_dir, + args.save_dir, + str(device), + num_layers_at_start_in_bf16=args.num_layers_at_start_in_bf16, + num_layers_at_end_in_bf16=args.num_layers_at_end_in_bf16, + extra_high_precision_layers_hf=tuple( + s.strip() for s in args.extra_high_precision_layers_hf if isinstance(s, str) and s.strip() + ), + ) if __name__ == "__main__": From 1d53697c9281b6c300cf003b5aef8a9fcbebdf58 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Fri, 1 May 2026 16:05:02 -0700 Subject: [PATCH 0005/1130] Add --debug-skip-weight-update and --debug-disable-optimizer (#1053) --- miles/backends/megatron_utils/actor.py | 7 +++ miles/backends/megatron_utils/model.py | 56 +++++++++++++------ miles/utils/arguments.py | 23 ++++++++ .../test_lora_model_branches.py | 1 + 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index 1b9c64eb98b..54ca4b81a5d 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -522,6 +522,13 @@ def update_weights(self) -> None: if dist.get_rank() == 0: ray.get(self.rollout_manager.clear_updatable_num_new_engines.remote()) + if self.args.debug_skip_weight_update: + if dist.get_rank() == 0: + logger.warning("Skipping actor-to-rollout weight update because " "--debug-skip-weight-update is set.") + if self.args.offload_train: + destroy_process_groups() + return + with torch_memory_saver.disable() if self.args.offload_train else nullcontext(): print_memory("before update_weights") self.weight_updater.update_weights() diff --git a/miles/backends/megatron_utils/model.py b/miles/backends/megatron_utils/model.py index a9be3696d7a..fdcd4038a84 100644 --- a/miles/backends/megatron_utils/model.py +++ b/miles/backends/megatron_utils/model.py @@ -104,7 +104,7 @@ def get_optimizer_param_scheduler(args: Namespace, optimizer: MegatronOptimizer) def setup_model_and_optimizer( args: Namespace, role: str = "actor", -) -> tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler]: +) -> tuple[list[DDP], MegatronOptimizer | None, OptimizerParamScheduler | None]: """Build model(s), wrap with DDP, and construct optimizer and scheduler. Args: @@ -114,8 +114,10 @@ def setup_model_and_optimizer( Returns: tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler]: - List of model chunks wrapped by ``DDP``. - - The constructed ``MegatronOptimizer`` instance. - - The learning-rate/weight-decay scheduler tied to the optimizer. + - The constructed ``MegatronOptimizer`` instance, or ``None`` when + ``--debug-disable-optimizer`` is set. + - The learning-rate/weight-decay scheduler tied to the optimizer, or + ``None`` when ``--debug-disable-optimizer`` is set. """ assert not args.moe_use_upcycling assert args.load is not None or args.pretrained_checkpoint is not None @@ -125,6 +127,14 @@ def setup_model_and_optimizer( else: model = get_model(get_model_provider_func(args, role), ModelType.encoder_or_decoder) + if args.debug_disable_optimizer: + if is_megatron_main_rank(): + logger.warning( + "Skipping Megatron optimizer and LR scheduler initialization " + "because --debug-disable-optimizer is set." + ) + return model, None, None + # Optimizer kwargs = {} for f in dataclasses.fields(OptimizerConfig): @@ -321,8 +331,8 @@ def train_one_step( step_id: int, data_iterator: Sequence[DataIterator], model: Sequence[DDP], - optimizer: MegatronOptimizer, - opt_param_scheduler: OptimizerParamScheduler, + optimizer: MegatronOptimizer | None, + opt_param_scheduler: OptimizerParamScheduler | None, num_microbatches: int, ) -> tuple[dict[str, float], float]: """Execute a single pipeline-parallel training step. @@ -345,11 +355,13 @@ def train_one_step( """ args = get_args() dumper_phase_util = DumperMegatronUtil(args, model, DumperPhase.FWD_BWD) + disable_optimizer = args.debug_disable_optimizer or optimizer is None # Set grad to zero. for model_chunk in model: model_chunk.zero_grad_buffer() - optimizer.zero_grad() + if not disable_optimizer: + optimizer.zero_grad() if args.custom_megatron_before_train_step_hook_path: from miles.utils.misc import load_function @@ -450,7 +462,8 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p ) valid_step = True - if not getattr(args, "check_for_nan_in_loss_and_grad", True): + grad_norm = 0.0 + if (not disable_optimizer) and (not getattr(args, "check_for_nan_in_loss_and_grad", True)): found_inf_flag = optimizer.prepare_grads() if found_inf_flag: valid_step = False @@ -469,7 +482,7 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p check_mtp_only_grad(model, step_id) - if valid_step: + if not disable_optimizer and valid_step: # Update parameters. update_successful, grad_norm, num_zeros_in_grad = optimizer.step() @@ -480,7 +493,8 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p # release grad for model_chunk in model: model_chunk.zero_grad_buffer() - optimizer.zero_grad() + if not disable_optimizer: + optimizer.zero_grad() dumper_phase_util.finalize(model) @@ -502,8 +516,8 @@ def finalize_model_grads_with_empty_cache(*args, **kwargs): def train( rollout_id: int, model: Sequence[DDP], - optimizer: MegatronOptimizer, - opt_param_scheduler: OptimizerParamScheduler, + optimizer: MegatronOptimizer | None, + opt_param_scheduler: OptimizerParamScheduler | None, data_iterator: Sequence[DataIterator], num_microbatches: Sequence[int], ) -> None: @@ -522,6 +536,7 @@ def train( """ parallel_state = get_parallel_state() args = get_args() + disable_optimizer = args.debug_disable_optimizer or optimizer is None for iterator in data_iterator: iterator.reset() @@ -532,7 +547,7 @@ def train( # Setup some training config params. config = get_model_config(model[0]) - config.grad_scale_func = optimizer.scale_loss + config.grad_scale_func = None if disable_optimizer else optimizer.scale_loss config.timers = None if isinstance(model[0], DDP) and args.overlap_grad_reduce: assert config.no_sync_func is None, ( @@ -554,7 +569,7 @@ def train( pre_hook_enabled = False - if args.reset_optimizer_states: + if args.reset_optimizer_states and not disable_optimizer: if is_megatron_main_rank(): print("Reset optimizer states") for chained_optimizer in optimizer.chained_optimizers: @@ -642,8 +657,9 @@ def train( if args.enable_mtp_training: extra_metrics["mtp_loss"] = mtp_losses - for param_group_id, param_group in enumerate(optimizer.param_groups): - extra_metrics[f"lr-pg_{param_group_id}"] = opt_param_scheduler.get_lr(param_group) + if not disable_optimizer: + for param_group_id, param_group in enumerate(optimizer.param_groups): + extra_metrics[f"lr-pg_{param_group_id}"] = opt_param_scheduler.get_lr(param_group) log_dict = log_train_step( args=args, @@ -678,7 +694,10 @@ def train( def save( - iteration: int, model: Sequence[DDP], optimizer: MegatronOptimizer, opt_param_scheduler: OptimizerParamScheduler + iteration: int, + model: Sequence[DDP], + optimizer: MegatronOptimizer | None, + opt_param_scheduler: OptimizerParamScheduler | None, ) -> None: """Persist a training checkpoint safely with forward hooks disabled. @@ -774,7 +793,7 @@ def save_hf_model(args, rollout_id: int, model: Sequence[DDP]) -> None: def initialize_model_and_optimizer( args: Namespace, role: str = "actor" -) -> tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler, int]: +) -> tuple[list[DDP], MegatronOptimizer | None, OptimizerParamScheduler | None, int]: """Initialize model(s), optimizer, scheduler, and load from checkpoint. Args: @@ -808,6 +827,7 @@ def initialize_model_and_optimizer( check_model_hashes(args, model, iteration) - opt_param_scheduler.step(increment=iteration * args.global_batch_size) + if opt_param_scheduler is not None: + opt_param_scheduler.step(increment=iteration * args.global_batch_size) return model, optimizer, opt_param_scheduler, iteration diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 02353a5e278..347f503d7c2 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -157,6 +157,25 @@ def add_train_arguments(parser): default=1024**3, help="Add margin for train memory allocation. By default we will reserve 1GB as margin.", ) + parser.add_argument( + "--debug-skip-weight-update", + action="store_true", + default=False, + help=( + "Debug-only: preserve the train/rollout offload-onload schedule, " + "but skip the actual actor-to-rollout weight update." + ), + ) + parser.add_argument( + "--debug-disable-optimizer", + action="store_true", + default=False, + help=( + "Debug-only: do not initialize the Megatron optimizer or LR scheduler. " + "Training still runs rollout, log-prob forward, and actor forward/backward, " + "but skips optimizer state allocation and optimizer updates." + ), + ) parser.add_argument( "--disable-weights-backuper", action="store_false", @@ -1838,6 +1857,10 @@ def miles_validate_args(args): "the canonical template output. Use at your own risk." ) + if args.debug_disable_optimizer: + args.no_load_optim = True + args.no_save_optim = True + if args.chat_template_path == "autofix": from miles.utils.chat_template_utils import try_get_fixed_chat_template diff --git a/tests/fast/backends/megatron_utils/test_lora_model_branches.py b/tests/fast/backends/megatron_utils/test_lora_model_branches.py index ae1720c5525..9d806e48de1 100644 --- a/tests/fast/backends/megatron_utils/test_lora_model_branches.py +++ b/tests/fast/backends/megatron_utils/test_lora_model_branches.py @@ -76,6 +76,7 @@ def _make_args(self, lora_rank=32, role="actor", mode="bridge"): lora_adapter_path=None, megatron_to_hf_mode=mode, moe_use_upcycling=False, + debug_disable_optimizer=False, load="/some/path", pretrained_checkpoint=None, # optimizer fields From cafbd11ac676124ceac452eb8153c48e0f423fdf Mon Sep 17 00:00:00 2001 From: Shi-Dong Date: Fri, 1 May 2026 23:24:55 -0700 Subject: [PATCH 0006/1130] docs: fix --rollout-sample-filter-path arg help signature (#1056) Co-authored-by: Shi Dong Co-authored-by: Zhichenzzz --- miles/utils/arguments.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 347f503d7c2..065f5983a43 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1506,8 +1506,9 @@ def add_rollout_buffer_arguments(parser): help=( "Path to the rollout sample filter function. " "This function determines whether a sample will participate in loss calculation. " - "The function should take args and samples (list[Sample]) as input, and return None. " - "Please directly modify the remove_sample attribute of Sample. " + "The function is called as `fn(args, data)` where `data` is `list[list[Sample]]` " + "(grouped by n_samples_per_prompt), and should return None. " + "To exclude a sample from the loss, set `sample.remove_sample = True`. " "Note: This attribute does not determine whether the sample participates in advantage normalization." ), ) From b216549e363526bcb122ac32e3a0c81ba18efb7b Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Sat, 2 May 2026 22:57:03 -0700 Subject: [PATCH 0007/1130] [TITO] refactor (1/N): Use (tito_model x allow_append_roles) to infer fixed chat template and kwargs, refactor chat template cpu tests, and add doc (#1048) Co-authored-by: Yuzhen Zhou --- docs/en/agentic/chat_template_verification.md | 143 ----- docs/en/agentic/oai_endpoint.md | 191 +++++++ docs/en/get_started/gen_endpoint.md | 2 +- docs/en/get_started/oai_endpoint.md | 140 ----- docs/en/index.rst | 2 +- .../run-glm47-flash-agentic-async.py | 1 - examples/experimental/swe-agent-v2/run.py | 1 - miles/rollout/session/linear_trajectory.py | 1 + miles/rollout/session/sessions.py | 1 + miles/utils/arguments.py | 61 +- miles/utils/chat_template_utils/__init__.py | 13 +- miles/utils/chat_template_utils/autofix.py | 40 -- .../templates/qwen3.5_fixed.jinja | 2 +- .../templates/qwen3_fixed.jinja | 2 +- .../qwen3_thinking_2507_and_next_fixed.jinja | 2 +- .../chat_template_utils/tito_tokenizer.py | 239 ++++++++ .../utils/test_utils/chat_template_verify.py | 520 +++++++++++++++--- miles/utils/test_utils/mock_trajectories.py | 61 +- scripts/tools/verify_chat_template.py | 148 ++++- tests/fast/fixtures/generation_fixtures.py | 58 +- tests/fast/fixtures/rollout_fixtures.py | 5 +- .../router/session_pretokenized_test_utils.py | 202 +++++++ .../router/test_session_pretokenized_e2e.py | 488 ++++------------ .../utils/chat_template_utils/test_autofix.py | 248 --------- .../test_fixed_templates.py | 158 ++++++ .../test_pretokenized_chat.py | 312 +++++------ .../test_pretokenized_via_tito.py | 161 ++++++ .../chat_template_utils/test_template.py | 201 ++++--- .../test_tito_tokenizer.py | 123 ++++- .../test_tito_tokenizer_model_matrix.py | 10 +- 30 files changed, 2119 insertions(+), 1417 deletions(-) delete mode 100644 docs/en/agentic/chat_template_verification.md create mode 100644 docs/en/agentic/oai_endpoint.md delete mode 100644 docs/en/get_started/oai_endpoint.md delete mode 100644 miles/utils/chat_template_utils/autofix.py create mode 100644 tests/fast/router/session_pretokenized_test_utils.py delete mode 100644 tests/fast/utils/chat_template_utils/test_autofix.py create mode 100644 tests/fast/utils/chat_template_utils/test_fixed_templates.py create mode 100644 tests/fast/utils/chat_template_utils/test_pretokenized_via_tito.py diff --git a/docs/en/agentic/chat_template_verification.md b/docs/en/agentic/chat_template_verification.md deleted file mode 100644 index 39854e9d529..00000000000 --- a/docs/en/agentic/chat_template_verification.md +++ /dev/null @@ -1,143 +0,0 @@ -# Chat Template Verification - -## Background - -In agentic workflows (multi-turn tool-calling), miles uses sglang's **pretokenized prefix** mechanism to avoid re-tokenizing the entire conversation history on every turn. This requires the chat template to satisfy an **append-only invariant**: rendering the first N messages must produce a string that is an exact prefix of rendering all messages. Some model families (e.g. certain Qwen3 variants) ship templates that use `loop.last` or similar context-dependent Jinja logic, which breaks this property. - -miles ships a one-click verification tool and an autofix mechanism to handle this. - -## Quick Start - -### Verify a HuggingFace model's template - -```shell -python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B -``` - -Example output for a template that **fails**: - -``` -Template source: HuggingFace: Qwen/Qwen3-0.6B -Thinking cases: disabled - - [FAIL] single_tool-N3 -- Prefix mismatch! - [PASS] single_tool-N3-no_tools - [FAIL] multi_turn-N4 -- Prefix mismatch! - [FAIL] multi_tool_single_turn-N3 -- Prefix mismatch! - [FAIL] parallel_tools-N3 -- Prefix mismatch! - [FAIL] long_chain-N4 -- Prefix mismatch! - [FAIL] long_chain-N6 -- Prefix mismatch! - [FAIL] multi_user_tool_chain-N8 -- Prefix mismatch! - [PASS] simple_no_tool-N3-no_tools - [FAIL] retry_system-N3 -- Prefix mismatch! - [FAIL] retry_system-N5 -- Prefix mismatch! - [FAIL] intermediate_system-N5 -- Prefix mismatch! - [FAIL] intermediate_system-N8 -- Prefix mismatch! - -Results: 2/13 passed, 11 failed - -Verdict: FAIL - template is NOT append-only after last user message -``` - -### Verify with autofix - -If miles has a built-in fix for the model, use `--autofix` to test the fixed version: - -```shell -python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B --autofix -``` - -``` -Template source: fixed template: .../miles/utils/chat_template_utils/templates/qwen3_fixed.jinja -Thinking cases: disabled - - [PASS] single_tool-N3 - [PASS] single_tool-N3-no_tools - [PASS] multi_turn-N4 - ... - [PASS] intermediate_system-N8 - -Results: 13/13 passed, 0 failed - -Verdict: PASS - template IS append-only after last user message -``` - -### Verify a local template file - -If you have a custom `.jinja` template, verify it directly: - -```shell -python scripts/tools/verify_chat_template.py --template path/to/my_template.jinja -``` - -### Include thinking-specific cases - -For models that support `enable_thinking` (e.g. Qwen3.5, GLM-5), add `--thinking` to also run thinking-specific test cases: - -```shell -python scripts/tools/verify_chat_template.py --model Qwen/Qwen3.5-0.8B --autofix --thinking -``` - -This runs 29 cases in total (13 standard + 16 thinking with `enable_thinking=True/False`). - -## CLI Reference - -``` -usage: verify_chat_template.py (--template PATH | --model MODEL_ID) - [--autofix] [--thinking] -``` - -| Argument | Description | -| :--- | :--- | -| `--template PATH` | Path to a local `.jinja` chat template file | -| `--model MODEL_ID` | HuggingFace model ID (e.g. `Qwen/Qwen3-0.6B`) | -| `--autofix` | When using `--model`, apply miles' fixed template if one exists | -| `--thinking` | Also run thinking-specific cases (`enable_thinking=True/False`) | - -The script exits with code **0** if all cases pass, or **1** if any case fails. - -## How It Works - -The verifier simulates the pretokenized incremental tokenization path at the text level: - -1. **Prefix render**: Render the first N messages with `add_generation_prompt=False` → `prefix_text` -2. **Full render**: Render all messages with `add_generation_prompt=True` → `full_text` -3. **Prefix check**: Verify that `full_text` starts with `prefix_text` -4. **Equivalence check**: Verify that the full render from the pretokenized path equals the standard full render - -This is tested across 13 diverse cases covering 9 trajectory patterns (single-turn, multi-turn, parallel tool calls, long chains, multi-user tool chain, no-tool, retry with intermediate system, and multi-step intermediate system scenarios). - -When `--thinking` is enabled, an additional 16 cases are added: 6 thinking-specific trajectory patterns × 2 (`enable_thinking=True/False`) + 2 intermediate system thinking patterns × 2. - -## Autofix: Built-in Template Fixes - -miles includes fixed templates for model families known to break the append-only invariant. When you pass `--chat-template-path autofix` in your training command, miles automatically selects the right fix: - -| Model Pattern | Fixed Template | -| :--- | :--- | -| `Qwen3.5-*` (e.g. Qwen3.5-0.8B, Qwen3.5-32B) | `qwen3.5_fixed.jinja` | -| `Qwen3-Next-*-Thinking` | `qwen3_thinking_2507_and_next_fixed.jinja` | -| `Qwen3-*B-Thinking-2507` | `qwen3_thinking_2507_and_next_fixed.jinja` | -| `Qwen3-*` (base, e.g. Qwen3-0.6B, Qwen3-4B) | `qwen3_fixed.jinja` | - -Rules are matched in order (first match wins), so more specific patterns take priority over the general `Qwen3-*` rule. - -Models that are already append-only (e.g. GLM-5, GLM-4, GLM-4.7-Flash, Qwen3-Instruct-2507, Qwen3-Next-Instruct, Qwen3-Coder-Next) do not need a fix. - -### Using autofix in training - -```shell -python run.py \ - --hf-checkpoint Qwen/Qwen3-4B \ - --chat-template-path autofix \ - ... -``` - -## Running Tests - -The verification logic is covered by comprehensive unit tests: - -```shell -# Run all chat template tests (autofix mapping + append-only verification) -python -m pytest tests/fast/utils/chat_template_utils/ -v -``` diff --git a/docs/en/agentic/oai_endpoint.md b/docs/en/agentic/oai_endpoint.md new file mode 100644 index 00000000000..4f40a4f2ea6 --- /dev/null +++ b/docs/en/agentic/oai_endpoint.md @@ -0,0 +1,191 @@ +# OAI Endpoint Usage + +This document explains how to use the OpenAI-format chat endpoint through Miles +Router sessions. For the `/generate` endpoint, see +`docs/en/get_started/gen_endpoint.md`. + +## 1. Minimal `run_agent` loop + +Your `run_agent` receives a session-scoped `base_url`. Send OpenAI-format chat +requests to `base_url/v1/chat/completions` and pass the `messages` list as the +prompt. + +Minimal custom agent example: + +```python +from miles.utils.http_utils import post + +async def run_agent(base_url: str, prompt, request_kwargs: dict | None = None) -> None: + payload = {"model": "default", "messages": prompt, **(request_kwargs or {})} + await post(f"{base_url}/v1/chat/completions", payload) +``` + +Notes for `run_agent`: + +- `base_url` already includes the session path (e.g. `/sessions/`), so you + should not manually add the session id. Just append the OpenAI route. +- `request_kwargs` already contains the default sampling settings from + `agentic_tool_call.build_chat_request_kwargs`, so you can directly expand it + into the chat request payload. +- If you pass rollout sampling params, `max_new_tokens` will be mapped to the + OpenAI `max_tokens` field before the request is sent. +- If you need structured parsing payloads, use SGLang's + `ChatCompletionRequest`-compatible format. It is compatible with native OpenAI + fields, plus extra SGLang parameters. + +## 2. OpenAI chat messages and the basic request + +The OpenAI-format chat API uses a list of `messages`, each with a `role` and +`content`. + +Minimal request shape: + +```json +{ + "model": "default", + "messages": [ + {"role": "system", "content": "You are a concise assistant."}, + {"role": "user", "content": "Answer with one word: 2+2?"} + ], + "logprobs": true, + "return_prompt_token_ids": true +} +``` + +You can pass any OpenAI-compatible parameters in the payload, or any +SGLang-compatible `ChatCompletionRequest` parameters. Note: +`logprobs=True` and `return_prompt_token_ids=True` are set in +`request_kwargs` to extract token ids and logprobs for TITO (see below). +Do **not** set `logprob_start_len=0` — it would disable SGLang's prefix +cache. + +## 3. Quickstart index + +If you just want something runnable, start here. + +Generator entry point: + +- `miles/rollout/generate_hub/agentic_tool_call.py` — OpenAI-format agent + loop via router sessions. + +OpenAI-format examples that use `agentic_tool_call.generate`: + +- Single-turn (DAPO math): + - `examples/openai_format/dapo_math.py` — custom agent. + - `examples/openai_format/run-qwen3-4B.sh` — launcher. +- Multi-turn (SWE agent): + - `examples/experimental/swe-agent-v2/run.py` — drives a multi-turn + tool-calling loop with `--tito-model glm47 --tito-allowed-append-roles tool user`. + +Key flags for an OpenAI-format agentic run: + +| Flag | Description | +| :--- | :--- | +| `--custom-generate-function-path` | Set to `miles.rollout.generate_hub.agentic_tool_call.generate`. The OpenAI wrapper that creates the session and collects records. | +| `--custom-agent-function-path` | Path to your `run_agent` function. It receives `base_url` and `request_kwargs` and sends chat requests. | +| `--use-session-server` | Enables the session-server middleware that forces the SGLang flags TITO needs and tracks token prefixes across turns. Required for TITO. | +| `--tito-model` | TITO tokenizer family (`qwen3`, `qwen35`, `qwennext`, `glm47`, ...). Use `default` to keep the model's HF-native chat template untouched. | +| `--tito-allowed-append-roles` | Roles the session may append after assistant turns. Default `tool`; add `user` / `system` if your conversation pattern needs them. | + +For the list of supported `--tito-model` families and the role surfaces +pre-wired for each, see +[`miles/utils/chat_template_utils/tito_tokenizer.py`](../../../miles/utils/chat_template_utils/tito_tokenizer.py) +or [issue #712](https://github.com/radixark/miles/issues/712). + +Customize like: + +``` +CUSTOM_ARGS=( + --custom-generate-function-path miles.rollout.generate_hub.agentic_tool_call.generate + --custom-agent-function-path examples.openai_format.dapo_math.run_agent +) +``` + +For OpenAI format, do not add `--apply-chat-template`; the +prompt must remain a `messages` list. + +## 4. Further customization (OpenAI wrapper generate function) + +For OpenAI-format rollout, the key generate function is +`miles/rollout/generate_hub/agentic_tool_call.generate`. It is a thin wrapper +around your custom agent: + +1. Create a session on Miles Router and build a session-scoped `base_url`. +2. Call the custom agent (from `--custom-agent-function-path`) to send one or + more chat requests to `base_url/v1/chat/completions`, typically using + `prompt` and `request_kwargs`. +3. Collect session records via `OpenAIEndpointTracer`. +4. Convert records into `Sample` objects with + `compute_samples_from_openai_records`. + +If you want general generate-function customization beyond the OpenAI wrapper, +see `docs/en/get_started/gen_endpoint.md`. + +## 5. TITO (token-in token-out) + +TITO is the layer that gives miles per-turn token ids and logprobs across a +multi-turn session, without re-tokenizing the full conversation on every +request. It requires `--use-session-server`; with that on, miles handles +three things on your behalf: + +- Hardcodes the SGLang flags TITO needs on every chat request + (`logprobs=True`, `return_meta_info=True`, `return_prompt_token_ids=True`, + `no_stop_trim=False`); these are set by the middleware in + `miles/rollout/session/sessions.py` and override any agent-passed values. +- Reuses the token prefix from previous turns by injecting `input_ids` on + follow-up requests. +- Accumulates per-turn records into the `Sample` you receive at the end of + the session, with `tokens` and `rollout_log_probs` already populated. + +You do not extract token ids from the response yourself. + +### Configuring TITO for your model and conversation + +`--use-session-server` turns TITO on, but you also need to set two more +flags so TITO renders with the right chat template and accepts your +conversation's roles. + +- `--tito-model` (default `default`) selects the TITO tokenizer family. Set + it to your model's family — `qwen3`, `qwen35`, `qwennext`, `glm47`, ... — + so miles auto-resolves a bundled fixed chat template verified against + TITO's append-only invariants. The `default` value skips auto-resolution + and uses the model's HF-native chat template; use it only if that native + template already satisfies append-only. +- `--tito-allowed-append-roles` (default `tool`) restricts which roles your + `run_agent` may append after the first assistant message. Extend it to + match your conversation pattern; see "Session server message constraints" + below for valid values and typical role sets. Adding `user` or `system` + typically selects a different bundled template (or different kwargs) + within the same family, since broader role surfaces need stricter + rendering rules to keep append-only valid. + +### Common pitfalls + +- Do **not** set `logprob_start_len=0` — it forces SGLang to compute + logprobs for every prompt token, which destroys the prefix cache and + hurts performance. + +## 6. Session server message constraints + +The session server enforces two invariants on every request's `messages`. +Violations raise `MessageValidationError`. + +- **Append-only.** Each new request's `messages` must contain the previous + turn's full `messages` as an exact prefix (same roles, same + template-relevant content). You can extend at the tail; you cannot edit, + reorder, or drop earlier messages. This is how TITO can reuse tokens + across turns: the stored token prefix is only valid if the client + confirms the same message prefix on every request. + +- **Allowed append roles.** After the first assistant message, every newly + appended message must have a role in `--tito-allowed-append-roles` + (default `{tool}`; valid values: `tool`, `user`, `system`). Typical usage: + + - Pure tool-calling agent — `tool` (default). + - Multi-turn chat with mid-conversation user follow-ups — add `user`. + - Mid-conversation system reminders — also add `system`. + +Both invariants depend on the chat template's rendering behavior under the +chosen role surface. Adding miles support for a new model family therefore +involves both updating the chat template and adapting the TITO tokenizer +subclass for that family. diff --git a/docs/en/get_started/gen_endpoint.md b/docs/en/get_started/gen_endpoint.md index a8e9d2ae1ed..8a45063a9f7 100644 --- a/docs/en/get_started/gen_endpoint.md +++ b/docs/en/get_started/gen_endpoint.md @@ -1,7 +1,7 @@ # Gen Endpoint Usage This document covers generate_hub usage for the `/generate` endpoint. For OpenAI -format usage, see `docs/en/get_started/oai_endpoint.md`. +format usage, see `docs/en/agentic/oai_endpoint.md`. ## 1. What generate_hub is diff --git a/docs/en/get_started/oai_endpoint.md b/docs/en/get_started/oai_endpoint.md deleted file mode 100644 index 25e145b67bc..00000000000 --- a/docs/en/get_started/oai_endpoint.md +++ /dev/null @@ -1,140 +0,0 @@ -# OAI Endpoint Usage - -This document explains how to use the OpenAI-format chat endpoint through Miles -Router sessions. For the `/generate` endpoint, see -`docs/en/get_started/gen_endpoint.md`. - -## 1. Minimal `run_agent` loop - -Your `run_agent` receives a session-scoped `base_url`. Send OpenAI-format chat -requests to `base_url/v1/chat/completions` and pass the `messages` list as the -prompt. - -Minimal custom agent example: - -```python -from miles.utils.http_utils import post - -async def run_agent(base_url: str, prompt, request_kwargs: dict | None = None) -> None: - payload = {"model": "default", "messages": prompt, **(request_kwargs or {})} - await post(f"{base_url}/v1/chat/completions", payload) -``` - -Notes for `run_agent`: - -- `base_url` already includes the session path (e.g. `/sessions/`), so you - should not manually add the session id. Just append the OpenAI route. -- `request_kwargs` already contains the default sampling settings from - `agentic_tool_call.build_chat_request_kwargs`, so you can directly expand it - into the chat request payload. -- If you pass rollout sampling params, `max_new_tokens` will be mapped to the - OpenAI `max_tokens` field before the request is sent. -- If you need structured parsing payloads, use SGLang's - `ChatCompletionRequest`-compatible format. It is compatible with native OpenAI - fields, plus extra SGLang parameters. - -## 2. OpenAI chat messages and the basic request - -The OpenAI-format chat API uses a list of `messages`, each with a `role` and -`content`. - -Minimal request shape: - -```json -{ - "model": "default", - "messages": [ - {"role": "system", "content": "You are a concise assistant."}, - {"role": "user", "content": "Answer with one word: 2+2?"} - ], - "logprobs": true, - "return_prompt_token_ids": true -} -``` - -You can pass any OpenAI-compatible parameters in the payload, or any -SGLang-compatible `ChatCompletionRequest` parameters. Note: -`logprobs=True` and `return_prompt_token_ids=True` are set in -`request_kwargs` to extract token ids and logprobs for TITO (see below). -Do **not** set `logprob_start_len=0` — it would disable SGLang's prefix -cache. - -## 3. Quickstart index - -If you just want something runnable, start here: - -Generator entry point: - -- `miles/rollout/generate_hub/agentic_tool_call.py` - - OpenAI-format agent loop via router sessions. - -OpenAI-format examples that use `agentic_tool_call.generate`: - -- `examples/openai_format/dapo_math.py` - - Single-turn OpenAI format agent (DAPO math). -- Launcher scripts: - - `examples/openai_format/run-qwen3-4B.sh` - - -You can customize generate function like: -``` -CUSTOM_ARGS=( - --custom-generate-function-path miles.rollout.generate_hub.agentic_tool_call.generate - --custom-agent-function-path examples.openai_format.dapo_math.run_agent -) -``` - -For OpenAI format, do not add `--apply-chat-template`; the -prompt must remain a `messages` list. - -More agentic multi-turn examples will come in the future. - -## 4. Further customization (OpenAI wrapper generate function) - -For OpenAI-format rollout, the key generate function is -`miles/rollout/generate_hub/agentic_tool_call.generate`. It is a thin wrapper -around your custom agent: - -1. Create a session on Miles Router and build a session-scoped `base_url`. -2. Call the custom agent (from `--custom-agent-function-path`) to send one or - more chat requests to `base_url/v1/chat/completions`, typically using - `prompt` and `request_kwargs`. -3. Collect session records via `OpenAIEndpointTracer`. -4. Convert records into `Sample` objects with - `compute_samples_from_openai_records`. - -If you want general generate-function customization beyond the OpenAI wrapper, -see `docs/en/get_started/gen_endpoint.md`. - -## 5. TITO (token-in token-out) - -TITO needs two things from the SGLang response: - -1. **Prompt token ids** — extracted from `response.choices[0].prompt_token_ids`. - This field is returned by SGLang when the request sets - `return_prompt_token_ids=True`. -2. **Output token ids and logprobs** — extracted from - `response.choices[0].logprobs.content[*].token_id` and - `response.choices[0].logprobs.content[*].logprob`. - These fields are returned when the request sets `logprobs=True`. - -By default, `build_chat_request_kwargs` in `agentic_tool_call.py` sets both -`return_prompt_token_ids=True` and `logprobs=True`. The session middleware -forwards raw `messages` to SGLang, which tokenizes the prompt and returns the -response. Then `_compute_sample_from_openai_record` in -`openai_endpoint_utils.py` extracts prompt token ids and output token ids from -the response and concatenates them into `sample.tokens`. You do not need to -provide `input_ids` yourself. - -We can save multi-turn samples within a single session, but currently we -still do not inherit or reuse tokens across turns. Each request is tokenized -independently. - -### Common pitfalls - -- Ensure `logprobs=True` and `return_prompt_token_ids=True` in OpenAI chat - requests (both are already set in `request_kwargs`). -- Do **not** set `logprob_start_len=0` — it forces SGLang to compute - logprobs for every prompt token, which destroys the prefix cache and hurts - performance. Use `return_prompt_token_ids=True` instead, which returns - prompt token ids at zero cost without affecting caching. diff --git a/docs/en/index.rst b/docs/en/index.rst index 7c48585b650..5ee81219814 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -25,7 +25,7 @@ miles is the RL-framework behind GLM-4.7, GLM-4.6 and GLM-4.5. Apart from models :maxdepth: 1 :caption: Agentic - agentic/chat_template_verification.md + agentic/oai_endpoint.md .. toctree:: :maxdepth: 1 diff --git a/examples/experimental/swe-agent-v2/run-glm47-flash-agentic-async.py b/examples/experimental/swe-agent-v2/run-glm47-flash-agentic-async.py index a21dc67d171..55f98ea57d8 100644 --- a/examples/experimental/swe-agent-v2/run-glm47-flash-agentic-async.py +++ b/examples/experimental/swe-agent-v2/run-glm47-flash-agentic-async.py @@ -280,7 +280,6 @@ def execute(args: ScriptArgs): "--custom-agent-function-path swe_agent_function.run " "--custom-rm-path generate.reward_func " "--tito-model glm47 " - "--chat-template-path autofix " "--use-session-server " "--session-server-port 30000 " "--tito-allowed-append-roles user tool " diff --git a/examples/experimental/swe-agent-v2/run.py b/examples/experimental/swe-agent-v2/run.py index 16e97f1a1d3..053547f1926 100644 --- a/examples/experimental/swe-agent-v2/run.py +++ b/examples/experimental/swe-agent-v2/run.py @@ -171,7 +171,6 @@ def execute(args: ScriptArgs): "--rollout-function-path generate.RolloutFn " "--dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_no_aborted " "--tito-model glm47 " - "--chat-template-path autofix " "--use-session-server " "--session-server-port 30000 " # This is required by terminus-2 harness diff --git a/miles/rollout/session/linear_trajectory.py b/miles/rollout/session/linear_trajectory.py index 79d092349b2..f061a678f94 100644 --- a/miles/rollout/session/linear_trajectory.py +++ b/miles/rollout/session/linear_trajectory.py @@ -287,6 +287,7 @@ def compute_session_mismatch(self, session: LinearTrajectory) -> list[dict] | No tools=tools, add_generation_prompt=False, tokenize=True, + **self.tito_tokenizer.chat_template_kwargs, ) mismatches = self.comparator.compare_sequences(expected_ids, session.token_ids) return [m.to_dict() for m in mismatches] diff --git a/miles/rollout/session/sessions.py b/miles/rollout/session/sessions.py index 172e9060741..21a862cda12 100644 --- a/miles/rollout/session/sessions.py +++ b/miles/rollout/session/sessions.py @@ -35,6 +35,7 @@ def setup_session_routes(app, backend, args): tito_tokenizer = get_tito_tokenizer( tokenizer, tokenizer_type=getattr(args, "tito_model", "default"), + chat_template_kwargs=getattr(args, "apply_chat_template_kwargs", None), allowed_append_roles=getattr(args, "tito_allowed_append_roles", None), ) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 065f5983a43..141a5abc45d 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -645,12 +645,13 @@ def add_data_arguments(parser): "--chat-template-path", type=str, default=None, - help="Path to a custom Jinja chat template file (.jinja), or 'autofix'. " + help="Path to an explicit custom Jinja chat template file (.jinja). " "Sets tokenizer.chat_template when loading via load_tokenizer, " "and also sets --sglang-chat-template so the sglang server uses the same template. " - "If set to 'autofix', Miles will automatically select a fixed chat template " - "maintained internally that resolves train-inference token mismatch issues " - "in agentic workflows (e.g. tool-call trajectories). " + "For Miles-maintained fixed templates, leave this unset and pass " + "--tito-model plus --tito-allowed-append-roles so Miles can auto-resolve " + "the registered template. The literal value 'autofix' is kept only as a " + "deprecated compatibility alias for that auto-resolve path. " "The path must be accessible on all Ray worker nodes " "(e.g. a path inside the miles repo, or a shared filesystem like NFS).", ) @@ -1862,15 +1863,51 @@ def miles_validate_args(args): args.no_load_optim = True args.no_save_optim = True + # Normalize the deprecated ``--chat-template-path=autofix`` alias to None + # up-front so the rest of this block treats it as "no path given". if args.chat_template_path == "autofix": - from miles.utils.chat_template_utils import try_get_fixed_chat_template - - resolved = try_get_fixed_chat_template(args.hf_checkpoint) - if resolved is None: - logger.warning( - "--chat-template-path=autofix but no fix rule found for %s, using HF default", args.hf_checkpoint - ) - args.chat_template_path = resolved + logger.warning( + "--chat-template-path=autofix is deprecated; remove the flag and rely " + "on --tito-model + --tito-allowed-append-roles to auto-resolve. The " + "alias will be removed in a future release." + ) + args.chat_template_path = None + + # Auto-resolve a bundled fixed chat-template only when: + # 1. the caller did NOT pass --chat-template-path (an explicit path always + # wins and is never overridden) + # 2. the caller chose a non-default --tito-model family (DEFAULT means + # "use the model's native HF chat template", which is loaded by + # AutoTokenizer.from_pretrained — no override needed here) + should_auto_resolve = args.chat_template_path is None and args.tito_model != TITOTokenizerType.DEFAULT.value + + if should_auto_resolve: + tito_model = TITOTokenizerType(args.tito_model) + from miles.utils.chat_template_utils import resolve_fixed_chat_template + + resolved_path, resolved_kwargs = resolve_fixed_chat_template( + tito_model, + allowed_append_roles=args.tito_allowed_append_roles, + ) + if resolved_path is not None: + args.chat_template_path = resolved_path + # Merge inferred kwargs. User-explicit values win on conflict; only + # keys the user did not set are auto-filled. + if resolved_kwargs: + user_kwargs = args.apply_chat_template_kwargs or {} + for key, value in resolved_kwargs.items(): + if key in user_kwargs: + continue + user_kwargs[key] = value + logger.warning( + "Auto-set --apply-chat-template-kwargs %s=%r for tito_model=%s " + "(allowed_append_roles=%s); pass an explicit value to override.", + key, + value, + tito_model.value, + sorted(args.tito_allowed_append_roles), + ) + args.apply_chat_template_kwargs = user_kwargs if args.chat_template_path is not None: if not os.path.isfile(args.chat_template_path): diff --git a/miles/utils/chat_template_utils/__init__.py b/miles/utils/chat_template_utils/__init__.py index 7bceb7d54b0..9bd1bdb0ab4 100644 --- a/miles/utils/chat_template_utils/__init__.py +++ b/miles/utils/chat_template_utils/__init__.py @@ -1,6 +1,5 @@ """Chat template utilities for agentic-workflow token consistency.""" -from miles.utils.chat_template_utils.autofix import TEMPLATE_DIR, try_get_fixed_chat_template from miles.utils.chat_template_utils.template import ( apply_chat_template, apply_chat_template_from_str, @@ -9,7 +8,14 @@ load_hf_chat_template, message_matches, ) -from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer, TITOTokenizerType, get_tito_tokenizer +from miles.utils.chat_template_utils.tito_tokenizer import ( + TEMPLATE_DIR, + TITOTokenizer, + TITOTokenizerType, + get_tito_tokenizer, + resolve_fixed_chat_template, + resolve_reasoning_and_tool_call_parser, +) from miles.utils.chat_template_utils.token_seq_comparator import Mismatch, MismatchType, TokenSeqComparator __all__ = [ @@ -17,7 +23,8 @@ "TITOTokenizerType", "get_tito_tokenizer", "TEMPLATE_DIR", - "try_get_fixed_chat_template", + "resolve_fixed_chat_template", + "resolve_reasoning_and_tool_call_parser", "load_hf_chat_template", "apply_chat_template", "apply_chat_template_from_str", diff --git a/miles/utils/chat_template_utils/autofix.py b/miles/utils/chat_template_utils/autofix.py deleted file mode 100644 index 0f99ff1ed47..00000000000 --- a/miles/utils/chat_template_utils/autofix.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Auto-fix chat templates for agentic-workflow token consistency. - -Some model families ship chat templates that use ``loop.last`` or similar -context-dependent Jinja logic, which breaks the append-only invariant -required by sglang's pretokenized prefix mechanism. This module maps -HuggingFace checkpoint names to fixed templates bundled in the same directory. -""" - -import fnmatch -import logging -from pathlib import Path - -logger = logging.getLogger(__name__) - -TEMPLATE_DIR = Path(__file__).parent / "templates" - -# (glob pattern, fixed template filename) — checked in order, first match wins. -# Patterns are matched case-insensitively against the hf_checkpoint string. -_FIX_RULES: list[tuple[str, str]] = [ - ("*qwen3.5*", "qwen3.5_fixed.jinja"), - ("*qwen3-next-*-thinking*", "qwen3_thinking_2507_and_next_fixed.jinja"), - ("*qwen3-*b-thinking-2507*", "qwen3_thinking_2507_and_next_fixed.jinja"), - ("*qwen3-[0-9]*", "qwen3_fixed.jinja"), -] - - -def try_get_fixed_chat_template(hf_checkpoint: str) -> str | None: - """Return the path to a fixed chat template for *hf_checkpoint*, or ``None``. - - Matches the checkpoint name against known glob patterns (case-insensitive). - Returns the absolute path to the bundled ``.jinja`` file if a rule matches, - otherwise ``None``. - """ - name = hf_checkpoint.lower() - for pattern, filename in _FIX_RULES: - if fnmatch.fnmatch(name, pattern): - path = str(TEMPLATE_DIR / filename) - logger.info("Checkpoint %r matched %r -> using fixed chat template %s", hf_checkpoint, pattern, path) - return path - return None diff --git a/miles/utils/chat_template_utils/templates/qwen3.5_fixed.jinja b/miles/utils/chat_template_utils/templates/qwen3.5_fixed.jinja index 07d0cdadbf9..b003bcb770b 100644 --- a/miles/utils/chat_template_utils/templates/qwen3.5_fixed.jinja +++ b/miles/utils/chat_template_utils/templates/qwen3.5_fixed.jinja @@ -94,7 +94,7 @@ {%- endif %} {%- endif %} {%- set reasoning_content = reasoning_content|trim %} - {%- if loop.index0 > ns.last_query_index %} + {%- if (not (clear_thinking | default(true))) or loop.index0 > ns.last_query_index %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content + '\n\n\n' + content }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} diff --git a/miles/utils/chat_template_utils/templates/qwen3_fixed.jinja b/miles/utils/chat_template_utils/templates/qwen3_fixed.jinja index 55824a91aec..88ca5354792 100644 --- a/miles/utils/chat_template_utils/templates/qwen3_fixed.jinja +++ b/miles/utils/chat_template_utils/templates/qwen3_fixed.jinja @@ -40,7 +40,7 @@ {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} - {%- if loop.index0 > ns.last_query_index %} + {%- if (not (clear_thinking | default(true))) or loop.index0 > ns.last_query_index %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} diff --git a/miles/utils/chat_template_utils/templates/qwen3_thinking_2507_and_next_fixed.jinja b/miles/utils/chat_template_utils/templates/qwen3_thinking_2507_and_next_fixed.jinja index dbb6d3b1c9e..1588dfbea76 100644 --- a/miles/utils/chat_template_utils/templates/qwen3_thinking_2507_and_next_fixed.jinja +++ b/miles/utils/chat_template_utils/templates/qwen3_thinking_2507_and_next_fixed.jinja @@ -40,7 +40,7 @@ {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} - {%- if loop.index0 > ns.last_query_index %} + {%- if (not (clear_thinking | default(true))) or loop.index0 > ns.last_query_index %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} diff --git a/miles/utils/chat_template_utils/tito_tokenizer.py b/miles/utils/chat_template_utils/tito_tokenizer.py index 48a564314ad..fa094ceabdf 100644 --- a/miles/utils/chat_template_utils/tito_tokenizer.py +++ b/miles/utils/chat_template_utils/tito_tokenizer.py @@ -19,15 +19,43 @@ from __future__ import annotations +import logging +from collections.abc import Iterable +from dataclasses import dataclass, field from enum import Enum +from pathlib import Path from typing import Any from miles.utils.chat_template_utils.template import apply_chat_template, assert_messages_append_only_with_allowed_role from miles.utils.chat_template_utils.token_seq_comparator import TokenSeqComparator +logger = logging.getLogger(__name__) + +# Bundled fixed-template files live under this directory; ``FixedTemplateRow.template`` +# values are filenames relative to it. +TEMPLATE_DIR = Path(__file__).parent / "templates" + +# Roles the TITO merge logic understands; passing anything else is a typo. +_VALID_ROLES = frozenset({"tool", "user", "system"}) + _DUMMY_SYSTEM: dict[str, Any] = {"role": "system", "content": "dummy system"} +@dataclass(frozen=True) +class FixedTemplateRow: + """A ``(roles, template, extra_kwargs)`` row owned by a TITO tokenizer family. + + Each row says: when the session is configured for ``allowed_roles``, this + family expects the given chat template plus the given extra kwargs. + ``template`` is a path relative to ``TEMPLATE_DIR`` for a bundled fixed + template, or ``None`` to keep the HF-native template (kwargs-only fix). + """ + + allowed_roles: frozenset[str] + template: str | None = None + extra_kwargs: dict[str, Any] = field(default_factory=dict) + + def _build_dummy_assistant(tool_responses: list[dict[str, Any]]) -> dict[str, Any]: """Build a dummy assistant message with tool_calls matching *tool_responses*, so the template correctly renders the subsequent tool-response turn boundaries.""" @@ -52,6 +80,7 @@ def _build_dummy_assistant(tool_responses: list[dict[str, Any]]) -> dict[str, An # --------------------------------------------------------------------------- # Base / default tokenizer # --------------------------------------------------------------------------- +# TODO: split different model's TITO tokenizer into different files class TITOTokenizer: @@ -60,6 +89,16 @@ class TITOTokenizer: max_trim_tokens: int = 0 trailing_token_ids: frozenset[int] = frozenset() + # ``(roles, template, extra_kwargs)`` rows this family supports. Resolved + # by ``resolve_fixed_chat_template`` via smallest-superset match against + # the caller's ``allowed_append_roles``. + SUPPORTED_TEMPLATES: tuple[FixedTemplateRow, ...] = () + + # sglang ``--reasoning-parser`` and ``--tool-call-parser`` values bound to + # this family. + reasoning_parser: str | None = None + tool_call_parser: str | None = None + def __init__( self, tokenizer: Any, @@ -252,6 +291,21 @@ class Qwen3TITOTokenizer(TITOTokenizer): prefix matches the canonical template output. """ + reasoning_parser = "qwen3" + tool_call_parser = "qwen25" + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template="qwen3_fixed.jinja", + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="qwen3_fixed.jinja", + extra_kwargs={"clear_thinking": False}, + ), + ) + _default_assistant_start_str: str = "<|im_start|>assistant" def __init__( @@ -287,6 +341,48 @@ def merge_tokens( return prefix + incremental +# Qwen3.5 and Qwen3-Next-Thinking share the ``<|im_end|>`` boundary handling +# with Qwen3, so they reuse Qwen3TITOTokenizer's token-level logic via plain +# inheritance. They are still split into named subclasses because each owns +# its own ``SUPPORTED_TEMPLATES`` row pointing to a distinct fixed jinja, even +# though their boundary behavior is identical. + + +class Qwen35TITOTokenizer(Qwen3TITOTokenizer): + """Qwen3.5 — same boundary behavior as Qwen3, distinct fixed template.""" + + tool_call_parser = "qwen3_coder" + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template="qwen3.5_fixed.jinja", + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="qwen3.5_fixed.jinja", + extra_kwargs={"clear_thinking": False}, + ), + ) + + +class QwenNextTITOTokenizer(Qwen3TITOTokenizer): + """Qwen3-Thinking-2507 / Qwen3-Next-Thinking — same boundary behavior as + Qwen3, distinct (shared) fixed template.""" + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template="qwen3_thinking_2507_and_next_fixed.jinja", + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="qwen3_thinking_2507_and_next_fixed.jinja", + extra_kwargs={"clear_thinking": False}, + ), + ) + + # --------------------------------------------------------------------------- # GLM 4.7 implementation # --------------------------------------------------------------------------- @@ -304,6 +400,28 @@ class GLM47TITOTokenizer(TITOTokenizer): is injected instead). """ + reasoning_parser = "glm45" + tool_call_parser = "glm47" + + # GLM's HF-native chat template already exposes a ``clear_thinking`` kwarg, + # so no fixed-jinja patch is needed for either append surface. + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template=None, + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template=None, + extra_kwargs={"clear_thinking": False}, + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user", "system"}), + template=None, + extra_kwargs={"clear_thinking": False}, + ), + ) + max_trim_tokens: int = 1 _default_assistant_start_str: str = "<|assistant|>" @@ -347,12 +465,16 @@ def merge_tokens( class TITOTokenizerType(str, Enum): DEFAULT = "default" QWEN3 = "qwen3" + QWEN35 = "qwen35" + QWENNEXT = "qwennext" GLM47 = "glm47" _TOKENIZER_REGISTRY: dict[TITOTokenizerType, type[TITOTokenizer]] = { TITOTokenizerType.DEFAULT: TITOTokenizer, TITOTokenizerType.QWEN3: Qwen3TITOTokenizer, + TITOTokenizerType.QWEN35: Qwen35TITOTokenizer, + TITOTokenizerType.QWENNEXT: QwenNextTITOTokenizer, TITOTokenizerType.GLM47: GLM47TITOTokenizer, } @@ -389,3 +511,120 @@ def get_tito_tokenizer( if allowed_append_roles is not None: kwargs["allowed_append_roles"] = allowed_append_roles return cls(tokenizer, **kwargs) + + +# --------------------------------------------------------------------------- +# Fixed-template resolution (smallest-superset over SUPPORTED_TEMPLATES) +# --------------------------------------------------------------------------- + + +def resolve_fixed_chat_template( + tito_model: TITOTokenizerType | str, + allowed_append_roles: Iterable[str], +) -> tuple[str | None, dict[str, Any]]: + """Smallest-superset lookup over the requested family's ``SUPPORTED_TEMPLATES``. + + Returns ``(template_path, extra_kwargs)``: + + - ``template_path``: absolute path to a bundled ``.jinja`` file, or ``None`` + when the matched row registers HF-native (kwargs-only fix) or when no + row matches at all. + - ``extra_kwargs``: kwargs the caller should merge into + ``apply_chat_template`` (caller's explicit user kwargs win on conflict). + Empty when no row matches or the matched row needs none. + + Raises ``ValueError`` on equally-minimal supersets — register a stricter + row to disambiguate. + """ + if isinstance(tito_model, str): + tito_model = TITOTokenizerType(tito_model) + + requested = frozenset(allowed_append_roles) + invalid = requested - _VALID_ROLES + if invalid: + raise ValueError( + f"Unknown roles in allowed_append_roles: {sorted(invalid)}. " f"Supported: {sorted(_VALID_ROLES)}." + ) + + cls = _TOKENIZER_REGISTRY[tito_model] + candidates = [row for row in cls.SUPPORTED_TEMPLATES if requested.issubset(row.allowed_roles)] + if not candidates: + raise ValueError( + f"No SUPPORTED_TEMPLATES row registered for tito_model={tito_model.value} " + f"with allowed_append_roles={sorted(requested)}. Register a row in " + f"{cls.__name__}.SUPPORTED_TEMPLATES (template=None for HF-native models)." + ) + + # Pick the most specific superset. Ties surface registration mistakes + # immediately rather than depending on iteration order. + min_size = min(len(row.allowed_roles) for row in candidates) + minimal = [row for row in candidates if len(row.allowed_roles) == min_size] + if len(minimal) > 1: + raise ValueError( + f"Ambiguous fixed-template registration for tito_model={tito_model.value}, " + f"requested_roles={sorted(requested)}: multiple equally-minimal supersets " + f"{[sorted(row.allowed_roles) for row in minimal]}. Register a stricter row to disambiguate." + ) + row = minimal[0] + + path = str(TEMPLATE_DIR / row.template) if row.template else None + logger.info( + "tito_model=%s requested_roles=%s -> matched registered_roles=%s -> template=%s kwargs=%s", + tito_model.value, + sorted(requested), + sorted(row.allowed_roles), + path, + row.extra_kwargs, + ) + return path, dict(row.extra_kwargs) + + +# --------------------------------------------------------------------------- +# sglang parser resolution (per-family binding + assert-equal on user input) +# --------------------------------------------------------------------------- + + +def resolve_reasoning_and_tool_call_parser( + tito_model: TITOTokenizerType | str, + user_reasoning_parser: str | None = None, + user_tool_call_parser: str | None = None, +) -> tuple[str | None, str | None]: + """Resolve sglang ``--reasoning-parser`` and ``--tool-call-parser`` for the + given TITO family. + + Both parsers are bound on the TITO subclass as class attributes because + the model's reasoning / tool-call emission shapes are per-family facts. + For each parser independently: + + * If the user didn't pass a value, return the family's bound value + (which may itself be ``None`` for ``DEFAULT`` or unbound subclasses + — the caller is then responsible for supplying one downstream). + * If the user passed a value and the family is bound, assert equality; + a mismatch is a configuration bug and raises ``ValueError`` rather + than silently overriding. + * If the user passed a value and the family is unbound, accept it. + + Returns ``(reasoning_parser, tool_call_parser)``. + """ + if isinstance(tito_model, str): + tito_model = TITOTokenizerType(tito_model) + cls = _TOKENIZER_REGISTRY[tito_model] + + def _resolve_one(field: str, bound: str | None, user: str | None) -> str | None: + if user is None: + return bound + if bound is None: + return user + if user != bound: + raise ValueError( + f"--{field.replace('_', '-')}={user!r} disagrees with the parser " + f"registered for tito_model={tito_model.value!r}: {bound!r}. The " + f"parser is bound on the TITO subclass; either pass {bound!r} or " + f"omit the flag to auto-resolve." + ) + return user + + return ( + _resolve_one("reasoning_parser", cls.reasoning_parser, user_reasoning_parser), + _resolve_one("tool_call_parser", cls.tool_call_parser, user_tool_call_parser), + ) diff --git a/miles/utils/test_utils/chat_template_verify.py b/miles/utils/test_utils/chat_template_verify.py index a0e9a7ca966..9a07a1f913b 100644 --- a/miles/utils/test_utils/chat_template_verify.py +++ b/miles/utils/test_utils/chat_template_verify.py @@ -12,10 +12,15 @@ from __future__ import annotations +from collections.abc import Iterable from copy import deepcopy from dataclasses import dataclass +from typing import TYPE_CHECKING, Any -from miles.utils.chat_template_utils.template import apply_chat_template_from_str +from miles.utils.chat_template_utils.template import apply_chat_template, apply_chat_template_from_str + +if TYPE_CHECKING: + from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer, TITOTokenizerType def simulate_pretokenized_path( @@ -128,6 +133,17 @@ def verify_append_only( # --------------------------------------------------------------------------- # Built-in test cases (shared between CLI and test suite) # --------------------------------------------------------------------------- +# +# Trajectories expose two class attributes used for verify-layer filtering: +# +# * ``APPEND_ROLES: frozenset[str]`` — non-assistant roles that appear after +# the first assistant message. Drives ``--tito-allowed-append-roles``. +# * ``IS_THINKING: bool`` — any assistant carries ``reasoning_content``. +# Drives ``--thinking`` and whether ``enable_thinking`` kwarg is passed. +# +# Both are declared on the trajectory class (mock_trajectories.py), alongside +# ``TOOLS`` / ``PRETOKENIZE_POSITIONS`` / ``MESSAGES``. This file only lists +# which trajectories to exercise and expands them into concrete cases. import re # noqa: E402 @@ -137,6 +153,8 @@ def verify_append_only( LongChainThinkingTrajectory, LongChainTrajectory, MultiToolSingleTurnTrajectory, + MultiTurnNoToolThinkingTrajectory, + MultiTurnNoToolTrajectory, MultiTurnThinkingTrajectory, MultiTurnTrajectory, MultiUserToolChainTrajectory, @@ -154,115 +172,435 @@ def _short_name(cls: type) -> str: return re.sub(r"(? list[tuple[str, type, int, list[dict] | None]]: - """Generate (case_name, traj_cls, N, tools) from trajectory specs. +# Trajectories exercised by ``run_all_checks`` / the CLI. Must be a subset of +# the classes defined in mock_trajectories.py. Callers (CLI, tests) pick the +# applicable subset via :func:`select_cases` based on each template's supported +# append roles and thinking mode; there is no global "exclude" list here. +_TRAJECTORIES: list[type] = [ + SingleToolTrajectory, + MultiTurnTrajectory, + MultiToolSingleTurnTrajectory, + ParallelToolsTrajectory, + LongChainTrajectory, + MultiUserToolChainTrajectory, + RetrySystemTrajectory, + IntermediateSystemTrajectory, + SimpleNoToolTrajectory, + MultiTurnNoToolTrajectory, + SingleToolThinkingTrajectory, + MultiTurnThinkingTrajectory, + LongChainThinkingTrajectory, + MultiUserTurnThinkingTrajectory, + IntermediateSystemThinkingTrajectory, + MultiTurnNoToolThinkingTrajectory, +] + + +@dataclass(frozen=True) +class CaseSpec: + """One verify case with classification metadata copied from its trajectory.""" + + case_name: str + traj_cls: type + pretokenize_n: int + tools: list[dict] | None + append_roles: frozenset[str] + is_thinking: bool + + +def _expand(traj_cls: type) -> list[CaseSpec]: + """Expand one trajectory into one CaseSpec per PRETOKENIZE_POSITIONS value.""" + short = _short_name(traj_cls) + return [ + CaseSpec( + case_name=f"{short}-N{n}", + traj_cls=traj_cls, + pretokenize_n=n, + tools=traj_cls.TOOLS, + append_roles=traj_cls.APPEND_ROLES, + is_thinking=traj_cls.IS_THINKING, + ) + for n in traj_cls.PRETOKENIZE_POSITIONS + ] + + +ALL_CASES: list[CaseSpec] = [c for t in _TRAJECTORIES for c in _expand(t)] + +THINKING_MODES: tuple[str, ...] = ("off", "on", "both") + + +def select_cases( + *, + allowed_append_roles: Iterable[str], + is_thinking: bool | None = None, +) -> list[CaseSpec]: + """Select trajectory cases by append-role surface and (optionally) thinking flag. - Each trajectory class's ``PRETOKENIZE_POSITIONS`` determines the N values. + A case is included iff ``case.append_roles`` is a subset of + *allowed_append_roles*, and (when *is_thinking* is not ``None``) + ``case.is_thinking`` matches. + + The caller is responsible for including ``"tool"`` in *allowed_append_roles* + when the session is tool-capable; this function does not silently union it. """ - cases: list[tuple[str, type, int, list[dict] | None]] = [] - for traj_cls, tools in specs: - short = _short_name(traj_cls) - tools_label = traj_cls.TOOLS if tools is ... else tools - suffix = "-no_tools" if tools is None else "" - for n in traj_cls.PRETOKENIZE_POSITIONS: - cases.append((f"{short}-N{n}{suffix}", traj_cls, n, tools_label)) - return cases - - -# (trajectory_cls, tools) — ... means "use traj_cls.TOOLS" -_STANDARD_SPECS: list[tuple[type, list[dict] | None]] = [ - (SingleToolTrajectory, ...), - (SingleToolTrajectory, None), - (MultiTurnTrajectory, ...), - (MultiToolSingleTurnTrajectory, ...), - (ParallelToolsTrajectory, ...), - (LongChainTrajectory, ...), - (MultiUserToolChainTrajectory, ...), - (SimpleNoToolTrajectory, None), -] + allowed = frozenset(allowed_append_roles) + out: list[CaseSpec] = [] + for c in ALL_CASES: + if not c.append_roles.issubset(allowed): + continue + if is_thinking is not None and c.is_thinking != is_thinking: + continue + out.append(c) + return out + + +def enable_thinking_variants(thinking: str) -> list[dict]: + """Return the list of ``enable_thinking`` kwarg variants to apply per case. + + * ``"off"`` → ``[{}]`` (no ``enable_thinking`` kwarg). + * ``"on"`` → ``[{"enable_thinking": True}]``. + * ``"both"`` → ``[{"enable_thinking": True}, {"enable_thinking": False}]``. + + Both CLI (:func:`run_all_checks`) and pytest parametrize callers use this + to avoid drifting in how the ``enable_thinking`` knob is exercised. + """ + if thinking == "off": + return [{}] + if thinking == "on": + return [{"enable_thinking": True}] + if thinking == "both": + return [{"enable_thinking": True}, {"enable_thinking": False}] + raise ValueError(f"thinking must be one of {THINKING_MODES}; got {thinking!r}") -_THINKING_SPECS: list[tuple[type, list[dict] | None]] = [ - (SingleToolTrajectory, ...), - (SingleToolThinkingTrajectory, ...), - (MultiTurnThinkingTrajectory, ...), - (LongChainThinkingTrajectory, ...), - (MultiUserTurnThinkingTrajectory, ...), -] -_INTERMEDIATE_SYSTEM_SPECS: list[tuple[type, list[dict] | None]] = [ - (RetrySystemTrajectory, ...), - (IntermediateSystemTrajectory, ...), -] +def format_case_id(case: CaseSpec, kwargs: dict) -> str: + """Human-readable label for a ``(case, template_kwargs)`` tuple. -_INTERMEDIATE_SYSTEM_THINKING_SPECS: list[tuple[type, list[dict] | None]] = [ - (IntermediateSystemThinkingTrajectory, ...), -] + Used for both CLI ``VerifyResult.case_name`` and pytest test ids so the + same tuple is identified the same way in both surfaces. Format: -STANDARD_CASES: list[tuple[str, type, int, list[dict] | None]] = _build_cases(_STANDARD_SPECS) -THINKING_CASES: list[tuple[str, type, int, list[dict] | None]] = _build_cases(_THINKING_SPECS) -INTERMEDIATE_SYSTEM_CASES: list[tuple[str, type, int, list[dict] | None]] = _build_cases(_INTERMEDIATE_SYSTEM_SPECS) -INTERMEDIATE_SYSTEM_THINKING_CASES: list[tuple[str, type, int, list[dict] | None]] = _build_cases( - _INTERMEDIATE_SYSTEM_THINKING_SPECS -) + * empty kwargs → ``case.case_name``. + * otherwise → ``-_on/off-=val`` (keys sorted; + bool values emit ``key_on`` / ``key_off``; other values ``key=val``). + """ + if not kwargs: + return case.case_name + parts: list[str] = [] + for k, v in sorted(kwargs.items()): + if isinstance(v, bool): + parts.append(f"{k}_{'on' if v else 'off'}") + else: + parts.append(f"{k}={v}") + return f"{case.case_name}-{'-'.join(parts)}" + + +@dataclass +class CoverageReport: + """Coverage of cases across ``(is_thinking, append_roles \\ {tool})``. + + ``covered`` maps each combination to the case names that fall in it; + ``missing`` lists combinations with no case. ``tool`` is excluded from + the role axis because it is implicitly always allowed. + """ + + covered: dict[tuple[bool, tuple[str, ...]], list[str]] + missing: list[tuple[bool, tuple[str, ...]]] + + +def check_coverage( + cases: list[CaseSpec] | None = None, + *, + role_universe: set[str] | None = None, +) -> CoverageReport: + """Enumerate ``thinking × append-role-subset`` combinations and report gaps. + + Used as a sanity check that every meaningful combination of + ``--tito-allowed-append-roles`` and ``--thinking`` is backed by at least + one trajectory — otherwise certain CLI settings would be no-ops. + """ + if cases is None: + cases = ALL_CASES + if role_universe is None: + role_universe = {"user", "system"} + + from itertools import chain, combinations + + ordered_universe = sorted(role_universe) + all_subsets: list[tuple[str, ...]] = [ + tuple(sub) + for sub in chain.from_iterable(combinations(ordered_universe, r) for r in range(len(ordered_universe) + 1)) + ] + + covered: dict[tuple[bool, tuple[str, ...]], list[str]] = { + (is_thinking, sub): [] for is_thinking in (False, True) for sub in all_subsets + } + for c in cases: + roles_key = tuple(sorted(c.append_roles - {"tool"})) + key = (c.is_thinking, roles_key) + if key in covered: + covered[key].append(c.case_name) + + missing = [k for k, v in covered.items() if not v] + return CoverageReport(covered=covered, missing=missing) def run_all_checks( chat_template: str, *, - include_thinking: bool = False, - include_intermediate_system: bool = True, + allowed_append_roles: set[str] | frozenset[str] | None = None, + thinking: str = "off", + extra_template_kwargs: dict | None = None, ) -> list[VerifyResult]: - """Run all built-in verification cases against *chat_template*. + """Run verification cases filtered by *allowed_append_roles* and *thinking*. + + ``allowed_append_roles`` is the role surface the session may append after + an assistant turn; defaults to ``{"tool"}`` for the agentic baseline. + Trajectories whose ``append_roles`` are not a subset are skipped. Caller + must include ``"tool"`` explicitly when relevant — there is no implicit + union. + + ``thinking`` selects which ``enable_thinking`` variants are exercised — + see :func:`enable_thinking_variants`. When ``"both"``, **every** selected + trajectory (thinking or not) is rerun with ``enable_thinking=True`` and + ``enable_thinking=False``, so templates that branch on the flag are + validated against non-reasoning input too. + + ``extra_template_kwargs`` is merged into every invocation — use it to + thread template-specific kwargs (e.g. GLM's ``clear_thinking=False``) + through the CLI. + """ + if allowed_append_roles is None: + allowed_append_roles = {"tool"} + if thinking not in THINKING_MODES: + raise ValueError(f"thinking must be one of {THINKING_MODES}; got {thinking!r}") + extra = extra_template_kwargs or {} - When *include_thinking* is True, also runs thinking-specific cases with - ``enable_thinking=True`` and ``enable_thinking=False``. + is_thinking_filter = {"off": False, "on": True, "both": None}[thinking] + selected = select_cases(allowed_append_roles=allowed_append_roles, is_thinking=is_thinking_filter) + variants = enable_thinking_variants(thinking) - When *include_intermediate_system* is True (default), also runs cases - that contain intermediate system messages. Set to False for templates - that forbid non-initial system messages (e.g. Qwen3.5). - """ results: list[VerifyResult] = [] + for case in selected: + for variant in variants: + kwargs = {**variant, **extra} + results.append( + verify_append_only( + chat_template, + deepcopy(case.traj_cls.MESSAGES), + case.pretokenize_n, + tools=case.tools, + case_name=format_case_id(case, kwargs), + **kwargs, + ) + ) - for case_name, traj_cls, n, tools in STANDARD_CASES: - results.append( - verify_append_only(chat_template, deepcopy(traj_cls.MESSAGES), n, tools=tools, case_name=case_name) - ) + return results - if include_intermediate_system: - for case_name, traj_cls, n, tools in INTERMEDIATE_SYSTEM_CASES: - results.append( - verify_append_only(chat_template, deepcopy(traj_cls.MESSAGES), n, tools=tools, case_name=case_name) + +# --------------------------------------------------------------------------- +# TITO-instance verification: decode-roundtrip equality +# --------------------------------------------------------------------------- +# +# The string-based primitive above asserts text-prefix at the chat-template +# layer. This is necessary but not sufficient for production correctness — +# production runs ``get_tito_tokenizer(...)`` and exercises ``merge_tokens`` +# (model-specific token-level boundary patches) plus +# ``tokenize_additional_non_assistant`` (renders appended segments under a +# synthetic ``[_DUMMY_SYSTEM, ...]`` context, not the real history). +# +# The primitive below mirrors the production path: it instantiates the actual +# TITO subclass + HF tokenizer, runs ``merge_tokens`` against the encoded +# prefix, decodes, and asserts text equality with the canonical full render. + + +def verify_append_only_via_tito_instance( + tito: TITOTokenizer, + tokenizer: Any, + messages: list[dict], + pretokenized_num_message: int, + tools: list[dict] | None = None, + case_name: str = "", + **template_kwargs, +) -> VerifyResult: + """Decode-roundtrip verify with a pre-built TITO instance. + + Asserts ``decode(tito.merge_tokens(prefix_msgs, full_msgs, encode(prefix_text))) + == full_text`` where ``prefix_text`` and ``full_text`` come from running the + chat template through ``tokenizer`` with the same kwargs ``tito`` was built + with. The test-only path (e.g. ``BuggyQwen3TITOTokenizer``) uses this + instance form directly; production-shape callers go through + :func:`verify_append_only_via_tito`. + """ + try: + # TITO's incremental path requires the appendix to be all non-assistant. + # From the pretokenized boundary N, greedily extend M forward over the + # maximal non-assistant run — that's the chunk production would call + # merge_tokens for (between two assistant generations, or up to end). + n = pretokenized_num_message + m = n + while m < len(messages) and messages[m].get("role") != "assistant": + m += 1 + if m == n: + return VerifyResult( + case_name=case_name, + passed=False, + error=( + f"Empty appendix at N={n}: messages[{n}] is assistant. " + "PRETOKENIZE_POSITIONS must land at a post-assistant boundary " + "where messages[N:] starts with a non-assistant turn." + ), ) - if include_thinking: - for enable in (True, False): - suffix = "thinking_on" if enable else "thinking_off" - for case_name, traj_cls, n, tools in THINKING_CASES: - full_name = f"{case_name}[{suffix}]" - results.append( - verify_append_only( - chat_template, - deepcopy(traj_cls.MESSAGES), - n, - tools=tools, - case_name=full_name, - enable_thinking=enable, - ) + prefix_msgs = deepcopy(messages[:n]) + full_msgs = deepcopy(messages[:m]) + + prefix_text = apply_chat_template( + prefix_msgs, + tokenizer=tokenizer, + tools=tools, + add_generation_prompt=False, + tokenize=False, + **template_kwargs, + ) + full_text = apply_chat_template( + full_msgs, + tokenizer=tokenizer, + tools=tools, + add_generation_prompt=True, + tokenize=False, + **template_kwargs, + ) + + prefix_ids = tokenizer.encode(prefix_text, add_special_tokens=False) + # Simulate production's model-stop: in production, ``pretokenized_token_ids`` + # ends where the model actually stopped — typically before the trailing + # tokens the chat template would otherwise emit (Qwen's ``\n`` after + # ``<|im_end|>``, GLM's ambiguous ``<|user|>``/``<|observation|>`` boundary). + # The TITO subclass declares those as ``trailing_token_ids``. Trim them + # here so ``merge_tokens``'s boundary patches see the prefix in its + # production shape so the verifier sees the same prefix the + # subclass merge_tokens / trailing trim path operates on. + trailing = tito.trailing_token_ids + while prefix_ids and prefix_ids[-1] in trailing: + prefix_ids = prefix_ids[:-1] + merged_ids = tito.merge_tokens(prefix_msgs, full_msgs, prefix_ids, tools=tools) + merged_text = tokenizer.decode(merged_ids) + + if merged_text == full_text: + return VerifyResult(case_name=case_name, passed=True) + + # Find first divergence and quote ~60 chars of context on each side. + common_len = min(len(merged_text), len(full_text)) + diff_idx = next( + (i for i in range(common_len) if merged_text[i] != full_text[i]), + common_len, + ) + ctx_start = max(0, diff_idx - 60) + ctx_end = diff_idx + 60 + return VerifyResult( + case_name=case_name, + passed=False, + error=( + f"Decode-roundtrip mismatch (N={pretokenized_num_message}) at char {diff_idx}\n" + f" expected: ...{full_text[ctx_start:ctx_end]!r}...\n" + f" actual: ...{merged_text[ctx_start:ctx_end]!r}..." + ), + ) + except Exception as e: + return VerifyResult(case_name=case_name, passed=False, error=f"{type(e).__name__}: {e}") + + +def verify_append_only_via_tito( + tokenizer: Any, + tito_model: TITOTokenizerType | str, + allowed_append_roles: list[str], + messages: list[dict], + pretokenized_num_message: int, + tools: list[dict] | None = None, + case_name: str = "", + **template_kwargs, +) -> VerifyResult: + """Decode-roundtrip verify, building TITO from the registered family. + + Matches the production wiring at ``miles/rollout/session/sessions.py:35`` — + the same ``get_tito_tokenizer`` factory call, with ``chat_template_kwargs`` + threaded through so ``merge_tokens`` and the dummy-context segment renders + use the same kwargs as the reference full render. + """ + from miles.utils.chat_template_utils import get_tito_tokenizer + + tito = get_tito_tokenizer( + tokenizer, + tokenizer_type=tito_model, + chat_template_kwargs=dict(template_kwargs), + allowed_append_roles=list(allowed_append_roles), + ) + return verify_append_only_via_tito_instance( + tito, + tokenizer, + messages, + pretokenized_num_message, + tools=tools, + case_name=case_name, + **template_kwargs, + ) + + +def run_all_checks_via_tito( + tokenizer: Any, + tito_model: TITOTokenizerType | str, + *, + allowed_append_roles: set[str] | frozenset[str] | None = None, + thinking: str = "off", + extra_template_kwargs: dict | None = None, +) -> list[VerifyResult]: + """Same shape as :func:`run_all_checks` but routes through TITO + tokenizer. + + Per-case TITO rebuild: each (case, ``enable_thinking`` variant) gets a fresh + TITO instance constructed with the merged kwargs, so the dummy-context + segment renders inside ``tokenize_additional_non_assistant`` see the same + ``enable_thinking`` value as the reference render. Construction is + millisecond-level and runs ~50 times per CLI invocation; cheap. + + The caller is responsible for setting ``tokenizer.chat_template`` (e.g. via + ``resolve_fixed_chat_template`` lookup or ``--template`` override) before + calling this — this function does not consult ``SUPPORTED_TEMPLATES``. + """ + if allowed_append_roles is None: + allowed_append_roles = {"tool"} + if thinking not in THINKING_MODES: + raise ValueError(f"thinking must be one of {THINKING_MODES}; got {thinking!r}") + extra = extra_template_kwargs or {} + + is_thinking_filter = {"off": False, "on": True, "both": None}[thinking] + selected = select_cases(allowed_append_roles=allowed_append_roles, is_thinking=is_thinking_filter) + variants = enable_thinking_variants(thinking) + roles_list = sorted(allowed_append_roles) + + results: list[VerifyResult] = [] + for case in selected: + # TITO incremental requires a non-empty non-assistant appendix at the + # boundary. Trajectories that end at the assistant turn (e.g. plain + # ``[sys, user, assistant]``) have no appendix to verify and are + # silently skipped here — the string-based primitive still covers + # them at the text-prefix layer. + msgs = case.traj_cls.MESSAGES + n = case.pretokenize_n + if n >= len(msgs) or msgs[n].get("role") == "assistant": + continue + for variant in variants: + kwargs = {**variant, **extra} + results.append( + verify_append_only_via_tito( + tokenizer, + tito_model, + roles_list, + deepcopy(case.traj_cls.MESSAGES), + case.pretokenize_n, + tools=case.tools, + case_name=format_case_id(case, kwargs), + **kwargs, ) - if include_intermediate_system: - for case_name, traj_cls, n, tools in INTERMEDIATE_SYSTEM_THINKING_CASES: - full_name = f"{case_name}[{suffix}]" - results.append( - verify_append_only( - chat_template, - deepcopy(traj_cls.MESSAGES), - n, - tools=tools, - case_name=full_name, - enable_thinking=enable, - ) - ) + ) return results diff --git a/miles/utils/test_utils/mock_trajectories.py b/miles/utils/test_utils/mock_trajectories.py index c304f514287..d00a654a6e4 100644 --- a/miles/utils/test_utils/mock_trajectories.py +++ b/miles/utils/test_utils/mock_trajectories.py @@ -4,6 +4,19 @@ Used by: - tests/fast/rollout/generate_hub/test_pretokenized_chat.py (chat template verification) - tests/fast/router/test_session_pretokenized_e2e.py (session proxy e2e) + +Class attributes consumed by chat_template_verify: + +- ``APPEND_ROLES: frozenset[str]`` — non-assistant roles that appear *after* + the first assistant message (``tool`` / ``user`` / ``system``). These are + the roles the session must allow to be appended on top of an assistant- + stopped prefix; drives ``--tito-allowed-append-roles`` filtering. +- ``IS_THINKING: bool`` — ``True`` iff at least one assistant message carries + ``reasoning_content``. Drives ``--thinking`` filtering and whether the + ``enable_thinking`` chat-template kwarg is passed. + +Both are declared explicitly on each class so readers can see a trajectory's +verify-layer classification without having to execute the module. """ from __future__ import annotations @@ -107,6 +120,8 @@ class SingleToolTrajectory: TOOLS = WEATHER_TOOLS PRETOKENIZE_POSITIONS = [3] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing?"}, @@ -136,7 +151,9 @@ class MultiTurnTrajectory: """sys, user, ass(tool), tool, ass(tool), tool — 2 turns""" TOOLS = WEATHER_TOOLS - PRETOKENIZE_POSITIONS = [4] + PRETOKENIZE_POSITIONS = [3, 5] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing and Shanghai?"}, @@ -186,6 +203,8 @@ class MultiToolSingleTurnTrajectory: TOOLS = ALL_TOOLS PRETOKENIZE_POSITIONS = [3] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing and what date is it?"}, @@ -229,6 +248,8 @@ class ParallelToolsTrajectory: TOOLS = WEATHER_TOOLS PRETOKENIZE_POSITIONS = [3] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Compare weather in Beijing, Shanghai, and Guangzhou"}, @@ -284,7 +305,9 @@ class LongChainTrajectory: """sys, user, ass(tool), tool, ass(tool:date), tool, ass(tool), tool — 3 turns""" TOOLS = ALL_TOOLS - PRETOKENIZE_POSITIONS = [4, 6] + PRETOKENIZE_POSITIONS = [3, 5, 7] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Do a multi-step task"}, @@ -356,7 +379,9 @@ class RetrySystemTrajectory: """ TOOLS = WEATHER_TOOLS - PRETOKENIZE_POSITIONS = [3, 5] + PRETOKENIZE_POSITIONS = [3, 6] + APPEND_ROLES = frozenset({"tool", "system"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing and Shanghai?"}, @@ -412,7 +437,9 @@ class MultiUserToolChainTrajectory: """ TOOLS = ALL_TOOLS - PRETOKENIZE_POSITIONS = [8] + PRETOKENIZE_POSITIONS = [3, 5, 7, 9] + APPEND_ROLES = frozenset({"tool", "user"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing?"}, @@ -486,6 +513,8 @@ class SimpleNoToolTrajectory: TOOLS = None PRETOKENIZE_POSITIONS = [3] + APPEND_ROLES = frozenset() + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}, @@ -498,6 +527,8 @@ class MultiTurnNoToolTrajectory: TOOLS = None PRETOKENIZE_POSITIONS = [3, 5] + APPEND_ROLES = frozenset({"user"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the capital of France?"}, @@ -511,6 +542,8 @@ class MultiTurnNoToolThinkingTrajectory: TOOLS = None PRETOKENIZE_POSITIONS = [3, 5] + APPEND_ROLES = frozenset({"user"}) + IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is the capital of France?"}, @@ -533,6 +566,8 @@ class SingleToolThinkingTrajectory: TOOLS = WEATHER_TOOLS PRETOKENIZE_POSITIONS = [3] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing?"}, @@ -563,7 +598,9 @@ class MultiTurnThinkingTrajectory: """sys, user, ass(thinking+tool), tool, ass(thinking+tool), tool — 2 turns""" TOOLS = WEATHER_TOOLS - PRETOKENIZE_POSITIONS = [4] + PRETOKENIZE_POSITIONS = [3, 5] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What's the weather in Beijing and Shanghai?"}, @@ -614,7 +651,9 @@ class LongChainThinkingTrajectory: """sys, user, ass(thinking+tool), tool, ass(thinking+tool), tool, ass(thinking+tool), tool — 3 turns""" TOOLS = ALL_TOOLS - PRETOKENIZE_POSITIONS = [4, 6] + PRETOKENIZE_POSITIONS = [3, 5, 7] + APPEND_ROLES = frozenset({"tool"}) + IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Do a multi-step task"}, @@ -691,6 +730,8 @@ class MultiUserTurnThinkingTrajectory: TOOLS = WEATHER_TOOLS PRETOKENIZE_POSITIONS = [7] + APPEND_ROLES = frozenset({"tool", "user"}) + IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, # --- user turn 1 --- @@ -754,7 +795,9 @@ class IntermediateSystemTrajectory: """sys, user, ass(tool), tool, system, ass(tool:date), tool, system, ass(tool), tool — 3 turns with system""" TOOLS = ALL_TOOLS - PRETOKENIZE_POSITIONS = [5, 8] + PRETOKENIZE_POSITIONS = [3, 6, 9] + APPEND_ROLES = frozenset({"tool", "system"}) + IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Do a multi-step task"}, @@ -824,7 +867,9 @@ class IntermediateSystemThinkingTrajectory: """sys, user, ass(t+tool), tool, system, ass(t+tool:date), tool, system, ass(t+tool), tool — 3 turns with system+thinking""" TOOLS = ALL_TOOLS - PRETOKENIZE_POSITIONS = [5, 8] + PRETOKENIZE_POSITIONS = [3, 6, 9] + APPEND_ROLES = frozenset({"tool", "system"}) + IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Do a multi-step task"}, diff --git a/scripts/tools/verify_chat_template.py b/scripts/tools/verify_chat_template.py index 6b2cdde8c84..f02f91aa7cb 100644 --- a/scripts/tools/verify_chat_template.py +++ b/scripts/tools/verify_chat_template.py @@ -9,36 +9,60 @@ # Verify a HuggingFace model's chat template python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B - # Verify with autofix (use our fixed template if available) - python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B --autofix + # Resolve a bundled fixed template for a TITO tokenizer family + python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B \\ + --tito-model qwen3 --tito-allowed-append-roles tool - # Also run thinking-specific cases - python scripts/tools/verify_chat_template.py --model Qwen/Qwen3.5-0.8B --thinking + # Restrict which append roles the session is allowed to use (tool is implicit) + python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B \\ + --tito-allowed-append-roles user + + # Run thinking cases: off (default) / on / both + python scripts/tools/verify_chat_template.py --model Qwen/Qwen3.5-0.8B --thinking both """ from __future__ import annotations import argparse +import json import sys +from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizerType + def _load_template_from_file(path: str) -> str: with open(path) as f: return f.read() -def _load_template_from_model(model_id: str, *, autofix: bool) -> tuple[str, str]: - """Load chat template for a HF model. Returns (template_str, source_description).""" - if autofix: - from miles.utils.chat_template_utils.autofix import try_get_fixed_chat_template +def _load_template_from_model( + model_id: str, + *, + tito_model: str | None, + allowed_roles: list[str], +) -> tuple[str, str, dict]: + """Load chat template for a HF model. - fixed_path = try_get_fixed_chat_template(model_id) + Returns ``(template_str, source_description, resolved_kwargs)``. When the + matched fixed-template row registers extra kwargs, the caller is expected + to merge ``resolved_kwargs`` into its own template kwargs (with explicit + user values winning on conflict). + """ + if tito_model: + from miles.utils.chat_template_utils import resolve_fixed_chat_template + + fixed_path, resolved_kwargs = resolve_fixed_chat_template(tito_model, allowed_roles) if fixed_path: - return _load_template_from_file(fixed_path), f"fixed template: {fixed_path}" + return _load_template_from_file(fixed_path), f"fixed template: {fixed_path}", resolved_kwargs + # No template path matched, but the row may still carry kwargs (e.g. an + # HF-native + clear_thinking=False fix). Fall through to HF-native + # loading and bubble the kwargs up. + else: + resolved_kwargs = {} from miles.utils.chat_template_utils.template import load_hf_chat_template - return load_hf_chat_template(model_id), f"HuggingFace: {model_id}" + return load_hf_chat_template(model_id), f"HuggingFace: {model_id}", resolved_kwargs def main() -> int: @@ -61,33 +85,111 @@ def main() -> int: ) parser.add_argument( - "--autofix", - action="store_true", - help="When using --model, apply our fixed template if one exists.", + "--tito-model", + choices=[t.value for t in TITOTokenizerType], + default=None, + help=( + "When using --model, look up a bundled fixed template registered for this " + "TITO tokenizer family under the given --tito-allowed-append-roles surface. " + "Falls back to the HF default chat template when no entry is registered." + ), + ) + parser.add_argument( + "--tito-allowed-append-roles", + nargs="+", + default=["tool"], + choices=["tool", "user", "system"], + metavar="ROLE", + help=( + "Roles the session may append after an assistant turn. 'tool' is " + "implicitly always allowed (listing it is fine). Trajectories that " + "require roles outside this set are skipped. Default: tool." + ), ) parser.add_argument( "--thinking", - action="store_true", - help="Also run thinking-specific cases (enable_thinking=True/False).", + choices=["off", "on", "both"], + default="on", + help=( + "Thinking-mode filter. off: non-thinking trajectories only. " + "on: thinking trajectories with enable_thinking=True. " + "both: non-thinking + thinking with enable_thinking={True,False}. " + "Default: on." + ), + ) + parser.add_argument( + "--chat-template-kwargs", + type=json.loads, + default={}, + metavar="JSON", + help=( + "Extra kwargs threaded into the chat template on every render, as a " + "JSON object (e.g. '{\"clear_thinking\": false}' for GLM). Same " + "convention as --apply-chat-template-kwargs in the training entrypoint." + ), ) args = parser.parse_args() + extra_template_kwargs = args.chat_template_kwargs + + # ``--tito-allowed-append-roles`` lists the *optional* extra roles; ``tool`` + # is implicit for tool-capable agentic workflows and unioned in here so both + # the fixed-template lookup and the trajectory filter see the same surface. + allowed_roles = set(args.tito_allowed_append_roles) | {"tool"} + # ── Load template ────────────────────────────────────────────────── if args.template: chat_template = _load_template_from_file(args.template) source_desc = f"file: {args.template}" else: - chat_template, source_desc = _load_template_from_model(args.model, autofix=args.autofix) - - print(f"Template source: {source_desc}") - print(f"Thinking cases: {'enabled' if args.thinking else 'disabled'}") + chat_template, source_desc, resolved_kwargs = _load_template_from_model( + args.model, + tito_model=args.tito_model, + allowed_roles=sorted(allowed_roles), + ) + # Merge auto-resolved kwargs into the user's --chat-template-kwargs. + # Explicit user values win on conflict; only keys the user did not set + # are auto-filled. + for key, value in resolved_kwargs.items(): + if key in extra_template_kwargs: + continue + extra_template_kwargs[key] = value + print(f"Auto-set --chat-template-kwargs {key}={value!r} (from --tito-model={args.tito_model})") + + from miles.utils.test_utils.chat_template_verify import ALL_CASES, check_coverage, run_all_checks, select_cases + + is_thinking_filter = {"off": False, "on": True, "both": None}[args.thinking] + selected = select_cases(allowed_append_roles=allowed_roles, is_thinking=is_thinking_filter) + + print(f"Template source: {source_desc}") + print(f"Allowed append roles: {sorted(allowed_roles)}") + print(f"Thinking mode: {args.thinking}") + if extra_template_kwargs: + print(f"Template kwargs: {extra_template_kwargs}") + print(f"Selected trajectories: {len(selected)} of {len(ALL_CASES)} (after filtering)") print() - # ── Run verification ─────────────────────────────────────────────── - from miles.utils.test_utils.chat_template_verify import run_all_checks + # Global coverage sanity check — reports gaps in the mock-trajectory pool, + # not gaps caused by the current CLI flags. A gap here means some CLI + # setting exercises no trajectory; fixing it requires adding a trajectory + # in mock_trajectories.py. + coverage = check_coverage() + if coverage.missing: + print("Trajectory coverage gaps ((thinking, append_roles \\ {tool}) with no trajectory):") + for is_thinking, roles in coverage.missing: + label = "thinking " if is_thinking else "non-thinking" + roles_str = "{" + ", ".join(roles) + "}" if roles else "{}" + print(f" - {label} x {roles_str}") + print() - results = run_all_checks(chat_template, include_thinking=args.thinking) + # ── Run verification ─────────────────────────────────────────────── + results = run_all_checks( + chat_template, + allowed_append_roles=allowed_roles, + thinking=args.thinking, + extra_template_kwargs=extra_template_kwargs, + ) # ── Print results ────────────────────────────────────────────────── passed = sum(1 for r in results if r.passed) diff --git a/tests/fast/fixtures/generation_fixtures.py b/tests/fast/fixtures/generation_fixtures.py index 7e70dcd187a..8d823a9d715 100644 --- a/tests/fast/fixtures/generation_fixtures.py +++ b/tests/fast/fixtures/generation_fixtures.py @@ -16,7 +16,6 @@ from miles.rollout.inference_rollout.inference_rollout_common import GenerateState from miles.rollout.session.session_server import SessionServer from miles.utils.async_utils import run -from miles.utils.chat_template_utils import try_get_fixed_chat_template from miles.utils.http_utils import find_available_port, init_http_client from miles.utils.misc import SingletonMeta from miles.utils.test_utils import mock_tools @@ -26,6 +25,8 @@ 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 = { @@ -67,6 +68,7 @@ def extra_argv_for_variant( argv.append("--generate-multi-samples") elif variant in ("agentic_tool_call_single_sample", "agentic_tool_call_multi_samples"): argv += ["--custom-agent-function-path", custom_agent_function_path] + argv += ["--tito-model", "qwen3", "--tito-allowed-append-roles", "tool"] if variant == "agentic_tool_call_multi_samples": argv.append("--generate-multi-samples") @@ -224,24 +226,21 @@ def _noop_port(port: int): @contextmanager def with_session_server( backend_url: str, - model_name: str, + args: Namespace, *, - use_rollout_routing_replay: bool = False, - chat_template_path: str | None = None, + port: int, ): - if chat_template_path is None: - chat_template_path = try_get_fixed_chat_template(model_name) args = SimpleNamespace( miles_router_timeout=30, - hf_checkpoint=model_name, - chat_template_path=chat_template_path, - tito_model="default", - use_rollout_routing_replay=use_rollout_routing_replay, + hf_checkpoint=args.hf_checkpoint, + chat_template_path=args.chat_template_path, + tito_model=args.tito_model, + tito_allowed_append_roles=args.tito_allowed_append_roles, + use_rollout_routing_replay=args.use_rollout_routing_replay, session_server_instance_id=uuid.uuid4().hex, ) session_server = SessionServer(args, backend_url=backend_url) - port = find_available_port(31000) server = UvicornThreadServer(session_server.app, host="127.0.0.1", port=port) server.start() @@ -274,34 +273,25 @@ def process_fn(_): ), ) - fixed_template = try_get_fixed_chat_template(model_name) - is_agentic = variant.startswith("agentic_tool_call") with with_mock_server(model_name=model_name, process_fn=process_fn) as mock_server: + server_port = find_available_port(31000) if is_agentic else mock_server.port + _FIXTURE_ONLY_KEYS = {"model_name", "agentic_return_metadata"} + other_args_kwargs = {k: v for k, v in args_kwargs.items() if k not in _FIXTURE_ONLY_KEYS} + args = make_args( + variant=variant, + router_port=server_port, + model_name=model_name, + custom_generate_function_path=custom_generate_function_path, + **other_args_kwargs, + ) + # Agentic variants need a SessionServer for TITO session tracking; # non-agentic variants talk directly to the mock sglang server. - if is_agentic: - cm = with_session_server( - mock_server.url, - model_name, - use_rollout_routing_replay=args_kwargs.get("use_rollout_routing_replay", False), - chat_template_path=fixed_template, - ) - else: - cm = _noop_port(mock_server.port) - - with cm as server_port: - _FIXTURE_ONLY_KEYS = {"model_name", "agentic_return_metadata"} - other_args_kwargs = {k: v for k, v in args_kwargs.items() if k not in _FIXTURE_ONLY_KEYS} - args = make_args( - variant=variant, - router_port=server_port, - model_name=model_name, - custom_generate_function_path=custom_generate_function_path, - chat_template_path=fixed_template, - **other_args_kwargs, - ) + cm = with_session_server(mock_server.url, args, port=server_port) if is_agentic else _noop_port(server_port) + + with cm: if is_agentic: # Point session server address to the SessionServer we just started args.session_server_ip = "127.0.0.1" diff --git a/tests/fast/fixtures/rollout_fixtures.py b/tests/fast/fixtures/rollout_fixtures.py index 90bfdd197de..650b2f110c3 100644 --- a/tests/fast/fixtures/rollout_fixtures.py +++ b/tests/fast/fixtures/rollout_fixtures.py @@ -17,7 +17,6 @@ from miles.rollout.session.session_server import SessionServer from miles.router.router import MilesRouter from miles.utils.arguments import parse_args -from miles.utils.chat_template_utils import try_get_fixed_chat_template 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 @@ -108,9 +107,9 @@ def _with_session_server(args: Namespace, backend_url: str) -> Iterator[UvicornT session_args = SimpleNamespace( miles_router_timeout=30, hf_checkpoint=args.hf_checkpoint, - chat_template_path=getattr(args, "chat_template_path", None) - or try_get_fixed_chat_template(args.hf_checkpoint), + chat_template_path=getattr(args, "chat_template_path", None), tito_model=getattr(args, "tito_model", "default"), + tito_allowed_append_roles=getattr(args, "tito_allowed_append_roles", ["tool"]), use_rollout_routing_replay=getattr(args, "use_rollout_routing_replay", False), ) session_server = SessionServer(session_args, backend_url=backend_url) diff --git a/tests/fast/router/session_pretokenized_test_utils.py b/tests/fast/router/session_pretokenized_test_utils.py new file mode 100644 index 00000000000..ca8f7bb3950 --- /dev/null +++ b/tests/fast/router/session_pretokenized_test_utils.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from dataclasses import dataclass +from types import SimpleNamespace +from typing import Any + +import requests +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from miles.rollout.session.session_server import SessionServer +from miles.utils.chat_template_utils import MismatchType, apply_chat_template, get_tito_tokenizer +from miles.utils.http_utils import find_available_port +from miles.utils.processing_utils import load_tokenizer +from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer + +FORBIDDEN_MISMATCH_TYPES: frozenset[str] = frozenset( + { + MismatchType.SPECIAL_TOKEN_COUNT.value, + MismatchType.SPECIAL_TOKEN_TYPE.value, + MismatchType.NON_ASSISTANT_TEXT.value, + } +) + + +@dataclass(frozen=True) +class ScriptedBackendTurn: + response_message: dict[str, Any] + render_message: dict[str, Any] + + +def load_test_tokenizer(hf_checkpoint: str, chat_template_path: str | None): + return load_tokenizer( + hf_checkpoint, + chat_template_path=chat_template_path, + trust_remote_code=True, + ) + + +def make_router_env( + backend, + *, + hf_checkpoint: str, + chat_template_path: str | None, + tito_model: str, + allowed_append_roles: list[str], +): + args = SimpleNamespace( + miles_router_timeout=30, + hf_checkpoint=hf_checkpoint, + chat_template_path=chat_template_path, + tito_model=tito_model, + tito_allowed_append_roles=allowed_append_roles, + use_rollout_routing_replay=False, + ) + session_server = SessionServer(args, backend_url=backend.url) + + port = find_available_port(31000) + server = UvicornThreadServer(session_server.app, host="127.0.0.1", port=port) + server.start() + + return SimpleNamespace( + url=f"http://127.0.0.1:{port}", + backend=backend, + server=server, + ) + + +def teardown_router_env(env) -> None: + env.server.stop() + env.backend.stop() + + +def fetch_session_payload(base_url: str, session_id: str) -> dict[str, Any]: + response = requests.get(f"{base_url}/sessions/{session_id}", timeout=5.0) + response.raise_for_status() + return response.json() + + +def compute_local_session_mismatch( + tokenizer, + *, + tito_model: str, + allowed_append_roles: list[str], + messages: list[dict[str, Any]], + accumulated_token_ids: list[int], + tools: list[dict[str, Any]] | None, +) -> list[dict[str, Any]]: + comparator = get_tito_tokenizer( + tokenizer, + tokenizer_type=tito_model, + allowed_append_roles=allowed_append_roles, + ).create_comparator() + expected_ids = apply_chat_template( + messages, + tokenizer=tokenizer, + tools=tools, + add_generation_prompt=False, + tokenize=True, + ) + return [m.to_dict() for m in comparator.compare_sequences(expected_ids, accumulated_token_ids)] + + +def forbidden_mismatches(mismatch: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [m for m in mismatch if m.get("type") in FORBIDDEN_MISMATCH_TYPES] + + +class ScriptedChatBackend: + def __init__(self, tokenizer, scripted_turns: list[ScriptedBackendTurn]): + self.tokenizer = tokenizer + self._scripted_turns = scripted_turns + self._call_count = 0 + self.request_log: list[dict[str, Any]] = [] + self.host = "127.0.0.1" + self.port = find_available_port(32000) + self.app = FastAPI() + self._server = UvicornThreadServer(self.app, host=self.host, port=self.port) + self._setup_routes() + + @property + def url(self) -> str: + return f"http://{self.host}:{self.port}" + + def start(self): + self._server.start() + + def stop(self): + self._server.stop() + + def reset_stats(self): + self.request_log.clear() + self._call_count = 0 + + def _setup_routes(self): + @self.app.get("/health") + async def health(): + return JSONResponse(content={"status": "ok"}) + + @self.app.post("/v1/chat/completions") + async def chat_completions(request: Request): + payload = await request.json() + self.request_log.append(payload) + + idx = self._call_count + assert idx < len(self._scripted_turns), f"Unexpected extra request #{idx + 1}" + self._call_count += 1 + + turn = self._scripted_turns[idx] + messages = payload["messages"] + tools = payload.get("tools") + + prompt_text = apply_chat_template( + messages, + tokenizer=self.tokenizer, + tools=tools, + add_generation_prompt=True, + tokenize=False, + ) + with_assistant_text = apply_chat_template( + messages + [turn.render_message], + tokenizer=self.tokenizer, + tools=tools, + add_generation_prompt=False, + tokenize=False, + ) + assert with_assistant_text.startswith(prompt_text), "Scripted assistant must extend prompt text" + + response_text = with_assistant_text[len(prompt_text) :] + output_ids = self.tokenizer.encode(response_text, add_special_tokens=False) + input_ids = payload.get("input_ids") + prompt_ids = ( + list(input_ids) + if input_ids is not None + else apply_chat_template( + messages, + tokenizer=self.tokenizer, + tools=tools, + add_generation_prompt=True, + tokenize=True, + ) + ) + + return JSONResponse( + content={ + "id": f"scripted-{idx}", + "object": "chat.completion", + "created": 0, + "model": "scripted-model", + "choices": [ + { + "index": 0, + "message": turn.response_message, + "prompt_token_ids": prompt_ids, + "finish_reason": "tool_calls" if turn.response_message.get("tool_calls") else "stop", + "meta_info": { + "completion_tokens": len(output_ids), + "output_token_logprobs": [[-i / 128, tid] for i, tid in enumerate(output_ids)], + }, + } + ], + } + ) diff --git a/tests/fast/router/test_session_pretokenized_e2e.py b/tests/fast/router/test_session_pretokenized_e2e.py index da48c4200e2..91bd3b7e94f 100644 --- a/tests/fast/router/test_session_pretokenized_e2e.py +++ b/tests/fast/router/test_session_pretokenized_e2e.py @@ -1,176 +1,77 @@ -"""End-to-end tests for pretokenized token injection through the session proxy. +"""Representative session-layer smoke tests for miles-maintained fixed templates. -Verifies that LinearTrajectory correctly accumulates token state -across multi-turn tool-call conversations and injects pretokenized_token_ids -into downstream requests. +These tests intentionally stay narrow: -Validation strategy: -- The mock server verifies the chat template prefix invariant (prefix_str is a - prefix of full_str) and uses pretokenized_token_ids for prompt construction. -- We check mock server request_log to verify pretokenized fields are injected - on turn 2+. -- We verify pretokenized_token_ids is a valid prefix of the returned - prompt_token_ids. +- only bundled fixed templates maintained by miles +- only tool-only multi-turn session flow +- only session/TITO plumbing + mismatch taxonomy checks -Tests are parametrized across multiple models and chat template combinations. -Models whose native templates satisfy the prefix invariant (Qwen3.5, -Qwen3-Next-Instruct) are tested with native templates. Models that ship -broken templates (GLM-5, Qwen3, Qwen3-Thinking-2507, Qwen3-Next-Thinking) are -tested with fixed templates, and a separate negative test verifies that -their native templates indeed break the invariant. - -NOTE: Qwen3-Coder-Next is NOT covered here because its template uses an -XML-style tool call format () and arguments|items (requiring -dict arguments), which are incompatible with the mock server's qwen25 tool -call parser and the OpenAI-format string arguments in mock trajectories. -Its prefix invariant is verified in the unit tests (test_pretokenized_chat.py). +Detailed template correctness remains covered by the lower-level chat-template +tests in ``tests/fast/utils/chat_template_utils/``. """ +from __future__ import annotations + from tests.ci.ci_register import register_cpu_ci register_cpu_ci(est_time=60, suite="stage-a-fast") - from dataclasses import dataclass -from types import SimpleNamespace import pytest import requests - -from miles.rollout.session.session_server import SessionServer -from miles.utils.chat_template_utils import try_get_fixed_chat_template -from miles.utils.http_utils import find_available_port -from miles.utils.processing_utils import load_tokenizer -from miles.utils.test_utils.mock_sglang_server import MockSGLangServer -from miles.utils.test_utils.mock_trajectories import ( - LongChainTrajectory, - MultiTurnTrajectory, - SequentialProcessFn, - build_trajectory, +from tests.fast.router.session_pretokenized_test_utils import ( + ScriptedBackendTurn, + ScriptedChatBackend, + compute_local_session_mismatch, + fetch_session_payload, + forbidden_mismatches, + load_test_tokenizer, + make_router_env, + teardown_router_env, ) -from miles.utils.test_utils.uvicorn_thread_server import UvicornThreadServer -# --------------------------------------------------------------------------- -# Model + template configurations -# --------------------------------------------------------------------------- +from miles.utils.chat_template_utils import TITOTokenizerType, resolve_fixed_chat_template +from miles.utils.test_utils.mock_trajectories import LongChainTrajectory, build_trajectory @dataclass(frozen=True) -class ModelTemplateConfig: +class FixedTemplateSmokeConfig: + name: str hf_checkpoint: str - chat_template_path: str | None # None = model's native template + chat_template_path: str + tito_model: str -WORKING_CONFIGS: dict[str, ModelTemplateConfig] = { - "qwen3-fixed": ModelTemplateConfig( - "Qwen/Qwen3-0.6B", - try_get_fixed_chat_template("Qwen/Qwen3-0.6B"), +FIXED_TEMPLATE_SMOKE_CONFIGS: tuple[FixedTemplateSmokeConfig, ...] = ( + FixedTemplateSmokeConfig( + name="qwen3-fixed", + hf_checkpoint="Qwen/Qwen3-0.6B", + chat_template_path=resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool"])[0], + tito_model=TITOTokenizerType.QWEN3.value, ), - "qwen3-thinking2507-fixed": ModelTemplateConfig( - "Qwen/Qwen3-4B-Thinking-2507", - try_get_fixed_chat_template("Qwen/Qwen3-4B-Thinking-2507"), + FixedTemplateSmokeConfig( + name="qwen3.5-fixed", + hf_checkpoint="Qwen/Qwen3.5-0.8B", + chat_template_path=resolve_fixed_chat_template(TITOTokenizerType.QWEN35, ["tool"])[0], + tito_model=TITOTokenizerType.QWEN35.value, ), - "qwen3.5-fixed": ModelTemplateConfig( - "Qwen/Qwen3.5-0.8B", - try_get_fixed_chat_template("Qwen/Qwen3.5-0.8B"), + FixedTemplateSmokeConfig( + name="qwen3-thinking2507-fixed", + hf_checkpoint="Qwen/Qwen3-4B-Thinking-2507", + chat_template_path=resolve_fixed_chat_template(TITOTokenizerType.QWENNEXT, ["tool"])[0], + tito_model=TITOTokenizerType.QWENNEXT.value, ), - "qwen3-next-instruct-native": ModelTemplateConfig("Qwen/Qwen3-Next-80B-A3B-Instruct", None), - "qwen3-next-thinking-fixed": ModelTemplateConfig( - "Qwen/Qwen3-Next-80B-A3B-Thinking", - try_get_fixed_chat_template("Qwen/Qwen3-Next-80B-A3B-Thinking"), + FixedTemplateSmokeConfig( + name="qwen3-next-thinking-fixed", + hf_checkpoint="Qwen/Qwen3-Next-80B-A3B-Thinking", + chat_template_path=resolve_fixed_chat_template(TITOTokenizerType.QWENNEXT, ["tool"])[0], + tito_model=TITOTokenizerType.QWENNEXT.value, ), -} - -BROKEN_CONFIGS: dict[str, ModelTemplateConfig] = { - # TODO: GLM-5 is here because the mock server lacks a reasoning parser, so - # mock trajectories have no thinking content, causing a /<\/think> - # mismatch with add_generation_prompt. Once the mock server supports - # ReasoningParser, GLM-5 should move back to WORKING_CONFIGS. - "glm5-native": ModelTemplateConfig("zai-org/GLM-5", None), - "qwen3-native": ModelTemplateConfig("Qwen/Qwen3-0.6B", None), - "qwen3-thinking2507-native": ModelTemplateConfig("Qwen/Qwen3-4B-Thinking-2507", None), - "qwen3-next-thinking-native": ModelTemplateConfig("Qwen/Qwen3-Next-80B-A3B-Thinking", None), -} - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - - -@pytest.fixture( - params=WORKING_CONFIGS.values(), - ids=WORKING_CONFIGS.keys(), - scope="class", ) -def model_config(request) -> ModelTemplateConfig: - return request.param - - -@pytest.fixture(scope="class") -def tokenizer(model_config): - try: - return load_tokenizer( - model_config.hf_checkpoint, - chat_template_path=model_config.chat_template_path, - trust_remote_code=True, - ) - except (ValueError, OSError) as exc: - pytest.skip(f"Cannot load tokenizer for {model_config.hf_checkpoint}: {exc}") - - -# --------------------------------------------------------------------------- -# Env helpers -# --------------------------------------------------------------------------- - - -def _make_router_env(tokenizer, model_config, trajectory_cls): - """Create a SessionServer + mock backend wired for a given trajectory.""" - trajectory = build_trajectory(tokenizer, trajectory_cls) - process_fn = SequentialProcessFn(trajectory) - - ctx = {} - - backend = MockSGLangServer( - model_name=model_config.hf_checkpoint, - process_fn=process_fn, - host="127.0.0.1", - port=find_available_port(30000), - chat_template_path=model_config.chat_template_path, - ) - backend.start() - ctx["backend"] = backend - - args = SimpleNamespace( - miles_router_timeout=30, - hf_checkpoint=model_config.hf_checkpoint, - chat_template_path=model_config.chat_template_path, - tito_model="default", - use_rollout_routing_replay=False, - ) - session_server = SessionServer(args, backend_url=backend.url) - - port = find_available_port(31000) - server = UvicornThreadServer(session_server.app, host="127.0.0.1", port=port) - server.start() - ctx["server"] = server - - url = f"http://127.0.0.1:{port}" - - return SimpleNamespace(url=url, backend=backend, trajectory=trajectory, process_fn=process_fn, _ctx=ctx) - - -def _teardown_router_env(env): - env._ctx["server"].stop() - env._ctx["backend"].stop() - - -# --------------------------------------------------------------------------- -# Trajectory runner and verifiers -# --------------------------------------------------------------------------- def _get_followup_messages_after_assistant(full_messages: list[dict], assistant_idx: int) -> list[dict]: - """Extract all non-assistant messages following an assistant message until the next assistant.""" followup = [] i = assistant_idx + 1 while i < len(full_messages) and full_messages[i]["role"] != "assistant": @@ -180,7 +81,6 @@ def _get_followup_messages_after_assistant(full_messages: list[dict], assistant_ def _remap_followup_messages(followup_msgs: list[dict], response_tool_calls: list[dict]) -> list[dict]: - """Remap tool_call_ids in tool messages from actual response; pass through system messages as-is.""" remapped = [] tool_idx = 0 for msg in followup_msgs: @@ -193,231 +93,87 @@ def _remap_followup_messages(followup_msgs: list[dict], response_tool_calls: lis return remapped -def _run_trajectory_e2e(env): - """Run all turns of a trajectory through the session proxy. - - Dynamically constructs messages from actual responses: turn 1 uses - pre-defined messages, subsequent turns use the actual assistant message - from the previous response + remapped tool messages from the trajectory. - """ - trajectory = env.trajectory - - env.process_fn.reset() - env.backend.reset_stats() - - session_id = requests.post(f"{env.url}/sessions", timeout=5.0).json()["session_id"] +@pytest.mark.parametrize("config", FIXED_TEMPLATE_SMOKE_CONFIGS, ids=[c.name for c in FIXED_TEMPLATE_SMOKE_CONFIGS]) +def test_bundled_fixed_template_session_smoke(config: FixedTemplateSmokeConfig): + assert config.chat_template_path is not None, f"{config.name} should resolve to a bundled fixed template" - assistant_indices = [i for i, m in enumerate(trajectory.full_messages) if m["role"] == "assistant"] - - responses = [] - accumulated_messages: list[dict] = [] - - for turn_idx, turn in enumerate(trajectory.turns): - if turn_idx == 0: - messages = turn.request_messages - accumulated_messages = list(messages) - else: - messages = list(accumulated_messages) - - payload = {"messages": messages} - if trajectory.tools: - payload["tools"] = trajectory.tools + try: + tokenizer = load_test_tokenizer(config.hf_checkpoint, config.chat_template_path) + except (ValueError, OSError) as exc: + pytest.skip(f"Cannot load tokenizer for {config.hf_checkpoint}: {exc}") - resp = requests.post( - f"{env.url}/sessions/{session_id}/v1/chat/completions", - json=payload, - timeout=10.0, + trajectory = build_trajectory(tokenizer, LongChainTrajectory) + scripted_turns = [ + ScriptedBackendTurn( + response_message={**turn.assistant_message, "content": turn.assistant_message.get("content") or ""}, + render_message=turn.assistant_message, ) - assert resp.status_code == 200, f"Turn {turn_idx} failed: {resp.text}" - resp_json = resp.json() - responses.append(resp_json) - - assistant_msg = resp_json["choices"][0]["message"] - accumulated_messages.append(assistant_msg) - - ass_idx = assistant_indices[turn_idx] - followup_msgs = _get_followup_messages_after_assistant(trajectory.full_messages, ass_idx) - response_tool_calls = assistant_msg.get("tool_calls") or [] - remapped = _remap_followup_messages(followup_msgs, response_tool_calls) - accumulated_messages.extend(remapped) - - return session_id, responses - - -def _verify_pretokenized_injection(env, responses): - """Verify input_ids in request_log and prompt consistency.""" - backend = env.backend - - for turn_idx in range(len(env.trajectory.turns)): - req = backend.request_log[turn_idx] - resp_prompt_ids = responses[turn_idx]["choices"][0]["prompt_token_ids"] - - if turn_idx == 0: - assert "input_ids" not in req, "Turn 0 should not have input_ids" - else: - assert "input_ids" in req, f"Turn {turn_idx} missing input_ids" - - input_ids = req["input_ids"] - assert len(input_ids) > 0, f"Turn {turn_idx} input_ids is empty" - - assert resp_prompt_ids == input_ids, ( - f"Turn {turn_idx}: input_ids does not match prompt_token_ids.\n" - f"input_ids len={len(input_ids)}, prompt len={len(resp_prompt_ids)}\n" - f"First diff at index {next((i for i, (a, b) in enumerate(zip(input_ids, resp_prompt_ids, strict=False)) if a != b), 'length')}" - ) - - -def _verify_pretokenized_NOT_prefix(env, responses): - """Verify that input_ids does NOT match prompt on turn 2+. - - This is the inverse of _verify_pretokenized_injection: it proves that a - broken native template causes token mismatch in the e2e flow. - """ - backend = env.backend - found_mismatch = False - - for turn_idx in range(1, len(env.trajectory.turns)): - req = backend.request_log[turn_idx] - if "input_ids" not in req: - continue - input_ids = req["input_ids"] - resp_prompt_ids = responses[turn_idx]["choices"][0]["prompt_token_ids"] - - if resp_prompt_ids != input_ids: - found_mismatch = True - break - - assert found_mismatch, ( - "Expected prefix invariant to be violated with native template, " - "but input_ids matched prompt_token_ids on all turns" + for turn in trajectory.turns + ] + backend = ScriptedChatBackend(tokenizer, scripted_turns) + backend.start() + env = make_router_env( + backend, + hf_checkpoint=config.hf_checkpoint, + chat_template_path=config.chat_template_path, + tito_model=config.tito_model, + allowed_append_roles=["tool"], ) - -# =========================================================================== -# Positive E2E tests (auto-parametrized across WORKING_CONFIGS) -# =========================================================================== - - -class TestMultiTurnE2E: - """2-turn trajectory: sys, user -> ass(tool) -> tool -> ass(tool) -> tool""" - - @pytest.fixture(autouse=True, scope="class") - def setup(self, tokenizer, model_config): - env = _make_router_env(tokenizer, model_config, MultiTurnTrajectory) - type(self)._env = env - yield - _teardown_router_env(env) - - def test_all_turns_succeed(self): - session_id, responses = _run_trajectory_e2e(self._env) - assert len(responses) == 2 - - def test_pretokenized_injection(self): - session_id, responses = _run_trajectory_e2e(self._env) - _verify_pretokenized_injection(self._env, responses) - - def test_session_records(self): - session_id, responses = _run_trajectory_e2e(self._env) - get_resp = requests.get(f"{self._env.url}/sessions/{session_id}", timeout=5.0) - records = get_resp.json()["records"] - assert len(records) == 2 + try: + backend.reset_stats() + session_id = requests.post(f"{env.url}/sessions", timeout=5.0).json()["session_id"] + assistant_indices = [i for i, m in enumerate(trajectory.full_messages) if m["role"] == "assistant"] + + accumulated_messages: list[dict] = [] + for turn_idx, turn in enumerate(trajectory.turns): + messages = list(accumulated_messages) if turn_idx > 0 else list(turn.request_messages) + if turn_idx == 0: + accumulated_messages = list(messages) + + payload = {"messages": messages, "tools": trajectory.tools} + response = requests.post( + f"{env.url}/sessions/{session_id}/v1/chat/completions", + json=payload, + timeout=10.0, + ) + assert response.status_code == 200, f"{config.name} turn {turn_idx} failed: {response.text}" + + body = response.json() + assert len(body["choices"]) == 1 + assert "prompt_token_ids" in body["choices"][0] + if turn_idx > 0: + assert "input_ids" in backend.request_log[turn_idx], f"{config.name} turn {turn_idx} missing input_ids" + + assistant_msg = body["choices"][0]["message"] + session_messages = list(messages) + [assistant_msg] + + session_payload = fetch_session_payload(env.url, session_id) + metadata = session_payload["metadata"] + remote_mismatch = metadata.get("tito_session_mismatch", []) + local_mismatch = compute_local_session_mismatch( + tokenizer, + tito_model=config.tito_model, + allowed_append_roles=["tool"], + messages=session_messages, + accumulated_token_ids=metadata["accumulated_token_ids"], + tools=trajectory.tools, + ) + assert remote_mismatch == local_mismatch + assert ( + forbidden_mismatches(remote_mismatch) == [] + ), f"{config.name} turn {turn_idx} has forbidden mismatch types: {remote_mismatch}" + + accumulated_messages.append(assistant_msg) + ass_idx = assistant_indices[turn_idx] + followup_msgs = _get_followup_messages_after_assistant(trajectory.full_messages, ass_idx) + response_tool_calls = assistant_msg.get("tool_calls") or [] + accumulated_messages.extend(_remap_followup_messages(followup_msgs, response_tool_calls)) + + final_session_payload = fetch_session_payload(env.url, session_id) + records = final_session_payload["records"] + assert len(records) == len(trajectory.turns) assert all(r["status_code"] == 200 for r in records) assert all(r["path"] == "/v1/chat/completions" for r in records) - - -class TestLongChainE2E: - """3-turn trajectory: sys, user -> ass(tool) -> tool -> ass(tool) -> tool -> ass(tool) -> tool""" - - @pytest.fixture(autouse=True, scope="class") - def setup(self, tokenizer, model_config): - env = _make_router_env(tokenizer, model_config, LongChainTrajectory) - type(self)._env = env - yield - _teardown_router_env(env) - - def test_all_turns_succeed(self): - session_id, responses = _run_trajectory_e2e(self._env) - assert len(responses) == 3 - - def test_pretokenized_injection(self): - self._env.backend.reset_stats() - session_id, responses = _run_trajectory_e2e(self._env) - _verify_pretokenized_injection(self._env, responses) - - def test_pretokenized_grows_across_turns(self): - self._env.backend.reset_stats() - session_id, responses = _run_trajectory_e2e(self._env) - - req1 = self._env.backend.request_log[1] - req2 = self._env.backend.request_log[2] - ids1 = req1["input_ids"] - ids2 = req2["input_ids"] - assert len(ids2) > len(ids1), f"Turn 2 input_ids ({len(ids2)}) should be longer than turn 1 ({len(ids1)})" - - assert ids2[: len(ids1)] == ids1, "Turn 2 input_ids should have turn 1's as prefix" - - def test_session_records(self): - session_id, responses = _run_trajectory_e2e(self._env) - get_resp = requests.get(f"{self._env.url}/sessions/{session_id}", timeout=5.0) - records = get_resp.json()["records"] - assert len(records) == 3 - - -# =========================================================================== -# Negative E2E tests (native templates that break the prefix invariant) -# =========================================================================== - - -@pytest.mark.parametrize("config", BROKEN_CONFIGS.values(), ids=BROKEN_CONFIGS.keys()) -class TestNativeTemplatePrefixBreaks: - """Verify that native (unfixed) templates break the prefix invariant in e2e flow. - - The mock server uses tito_tokenizer to build prompt tokens incrementally - (matching real sglang behaviour), so all turns succeed with 200. We detect - the broken template by comparing the pretokenized_token_ids injected on - turn 1+ against a fresh full re-tokenization of the same messages — the two - must diverge if the native template is truly broken. - """ - - def test_prefix_invariant_violated(self, config): - try: - tok = load_tokenizer(config.hf_checkpoint, trust_remote_code=True) - except (ValueError, OSError) as exc: - pytest.skip(f"Cannot load tokenizer for {config.hf_checkpoint}: {exc}") - - try: - env = _make_router_env(tok, config, MultiTurnTrajectory) - except AssertionError: - # Some broken templates fail the text-level prefix invariant at - # trajectory build time (e.g. GLM-5's /<\/think> mismatch). - # That confirms the invariant is violated. - return - - try: - _session_id, _responses = _run_trajectory_e2e(env) - - found_mismatch = False - for turn_idx in range(1, len(env.trajectory.turns)): - req = env.backend.request_log[turn_idx] - input_ids = req.get("input_ids") - if input_ids is None: - continue - messages = req["messages"] - tools = req.get("tools") - full_prompt_str = tok.apply_chat_template( - messages, - tokenize=False, - add_generation_prompt=True, - tools=tools, - ) - full_prompt_ids = tok.encode(full_prompt_str, add_special_tokens=False) - if full_prompt_ids != list(input_ids): - found_mismatch = True - break - - assert found_mismatch, ( - "Expected prefix invariant to be violated with native template, " - "but input_ids matched full re-tokenization on all turns" - ) - finally: - _teardown_router_env(env) + finally: + teardown_router_env(env) diff --git a/tests/fast/utils/chat_template_utils/test_autofix.py b/tests/fast/utils/chat_template_utils/test_autofix.py deleted file mode 100644 index 2f3fa99edb6..00000000000 --- a/tests/fast/utils/chat_template_utils/test_autofix.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Unit tests for try_get_fixed_chat_template mapping logic. - -hf_checkpoint is often a local path with user-customized naming rather than a -clean HuggingFace repo ID, so we test a wide variety of realistic (and weird) -path formats to make sure glob matching is robust. -""" - -from tests.ci.ci_register import register_cpu_ci - -register_cpu_ci(est_time=60, suite="stage-a-fast") - - -import os - -import pytest - -from miles.utils.chat_template_utils.autofix import TEMPLATE_DIR, try_get_fixed_chat_template - -_QWEN3_FIXED = str(TEMPLATE_DIR / "qwen3_fixed.jinja") -_QWEN35_FIXED = str(TEMPLATE_DIR / "qwen3.5_fixed.jinja") -_THINKING_FIXED = str(TEMPLATE_DIR / "qwen3_thinking_2507_and_next_fixed.jinja") - - -# --------------------------------------------------------------------------- -# Qwen3 base models → qwen3_fixed.jinja -# --------------------------------------------------------------------------- - - -class TestQwen3Mapping: - """Qwen3 LLM models (non-Thinking) should map to qwen3_fixed.jinja.""" - - @pytest.mark.parametrize( - "checkpoint", - [ - # Standard HF repo IDs - "Qwen/Qwen3-0.6B", - "Qwen/Qwen3-1.7B", - "Qwen/Qwen3-4B", - "Qwen/Qwen3-8B", - "Qwen/Qwen3-14B", - "Qwen/Qwen3-32B", - "Qwen/Qwen3-235B-A22B", - # Local absolute paths - "/data/models/Qwen3-4B", - "/home/user/checkpoints/Qwen3-8B-epoch3", - "/mnt/nfs/shared_models/Qwen3-32B", - # Local relative paths - "models/Qwen3-14B", - "./downloaded/Qwen3-0.6B-finetuned", - # User renamed / suffixed - "Qwen3-4B-sft-v2", - "Qwen3-8B_merged_lora", - "/data/Qwen3-235B-A22B-quantized", - # Lowercase / odd casing - "qwen/qwen3-4b", - "QWEN/QWEN3-32B", - "Qwen/qWeN3-14b", - ], - ) - def test_resolves_to_qwen3_fixed(self, checkpoint): - path = try_get_fixed_chat_template(checkpoint) - assert path is not None, f"{checkpoint} should match a fix rule" - assert path == _QWEN3_FIXED - assert os.path.isfile(path), f"Fixed template must exist: {path}" - - -# --------------------------------------------------------------------------- -# Qwen3-Thinking-2507 → qwen3_thinking_2507_and_next_fixed.jinja -# --------------------------------------------------------------------------- - - -class TestQwen3Thinking2507Mapping: - """Qwen3-Thinking-2507 models should map to the shared thinking fix.""" - - @pytest.mark.parametrize( - "checkpoint", - [ - # Standard HF repo IDs - "Qwen/Qwen3-4B-Thinking-2507", - "Qwen/Qwen3-8B-Thinking-2507", - "Qwen/Qwen3-32B-Thinking-2507", - # Local paths - "/data/models/Qwen3-4B-Thinking-2507", - "/home/user/Qwen3-32B-Thinking-2507-sft", - "models/Qwen3-8B-Thinking-2507-merged", - # Weird casing - "qwen/qwen3-4b-thinking-2507", - "QWEN/QWEN3-8B-THINKING-2507", - ], - ) - def test_resolves_to_thinking_fixed(self, checkpoint): - path = try_get_fixed_chat_template(checkpoint) - assert path is not None, f"{checkpoint} should match a fix rule" - assert path == _THINKING_FIXED - assert os.path.isfile(path), f"Fixed template must exist: {path}" - - -# --------------------------------------------------------------------------- -# Qwen3-Next → Thinking gets fix, Instruct does NOT -# --------------------------------------------------------------------------- - - -class TestQwen3NextMapping: - """Qwen3-Next Thinking models reuse the 2507 fix; Instruct needs no fix.""" - - @pytest.mark.parametrize( - "checkpoint", - [ - "Qwen/Qwen3-Next-80B-A3B-Thinking", - "Qwen/Qwen3-Next-80B-A3B-Thinking-FP8", - # Local path variants - "/data/Qwen3-Next-80B-A3B-Thinking", - "models/qwen3-next-80b-a3b-thinking-sft", - "/mnt/nfs/Qwen3-Next-80B-A3B-Thinking-epoch5", - ], - ) - def test_thinking_resolves_to_fix(self, checkpoint): - path = try_get_fixed_chat_template(checkpoint) - assert path is not None, f"{checkpoint} should match a fix rule" - assert path == _THINKING_FIXED - - @pytest.mark.parametrize( - "checkpoint", - [ - "Qwen/Qwen3-Next-80B-A3B-Instruct", - "Qwen/Qwen3-Next-80B-A3B-Instruct-FP8", - "/data/models/Qwen3-Next-80B-A3B-Instruct", - "models/qwen3-next-80b-a3b-instruct-lora", - ], - ) - def test_instruct_no_fix(self, checkpoint): - path = try_get_fixed_chat_template(checkpoint) - assert path is None, f"{checkpoint} should NOT match any fix rule" - - -# --------------------------------------------------------------------------- -# Qwen3.5 → qwen3.5_fixed.jinja -# --------------------------------------------------------------------------- - - -class TestQwen35Mapping: - """Qwen3.5 models should map to qwen3.5_fixed.jinja.""" - - @pytest.mark.parametrize( - "checkpoint", - [ - "Qwen/Qwen3.5-0.8B", - "Qwen/Qwen3.5-32B", - "/data/models/Qwen3.5-32B-sft", - "models/qwen3.5-0.8b-finetuned", - "QWEN/QWEN3.5-0.8B", - ], - ) - def test_resolves_to_qwen35_fixed(self, checkpoint): - path = try_get_fixed_chat_template(checkpoint) - assert path is not None, f"{checkpoint} should match a fix rule" - assert path == _QWEN35_FIXED - assert os.path.isfile(path), f"Fixed template must exist: {path}" - - -# --------------------------------------------------------------------------- -# Models that should never match -# --------------------------------------------------------------------------- - - -class TestNoMatch: - """Models outside fix coverage should return None.""" - - @pytest.mark.parametrize( - "checkpoint", - [ - # Other Qwen family - "Qwen/Qwen2.5-72B", - # Non-Qwen - "zai-org/GLM-5", - "meta-llama/Llama-3.1-8B", - "deepseek-ai/DeepSeek-V3", - "/home/user/models/llama-3-70b", - # Qwen3-Coder-Next (no prefix invariant issue, no fix needed) - "Qwen/Qwen3-Coder-Next", - "Qwen/Qwen3-Coder-Next-FP8", - "Qwen/Qwen3-Coder-Next-Base", - "/data/models/Qwen3-Coder-Next", - # Edge cases that look similar but shouldn't match - "Qwen/Qwen3-Next-80B-A3B-Instruct", - "some-org/not-qwen3-at-all-4B", - ], - ) - def test_returns_none(self, checkpoint): - assert try_get_fixed_chat_template(checkpoint) is None - - -# --------------------------------------------------------------------------- -# Case insensitivity -# --------------------------------------------------------------------------- - - -class TestCaseInsensitive: - """Pattern matching should be case-insensitive.""" - - @pytest.mark.parametrize( - "checkpoint", - [ - "QWEN/QWEN3-4B", - "Qwen/qWeN3-4b", - "qwen/QWEN3-14B", - "/DATA/MODELS/QWEN3-8B", - "QWEN/QWEN3-4B-THINKING-2507", - "qwen/qwen3-next-80b-a3b-thinking", - "QWEN/QWEN3.5-0.8B", - "qwen/qwen3.5-32b", - ], - ) - def test_always_matches(self, checkpoint): - assert try_get_fixed_chat_template(checkpoint) is not None - - -# --------------------------------------------------------------------------- -# Rule priority: specific patterns must match before general ones -# --------------------------------------------------------------------------- - - -class TestRulePriority: - """More specific rules must match before general ones.""" - - def test_thinking_2507_before_generic_qwen3(self): - path = try_get_fixed_chat_template("Qwen/Qwen3-4B-Thinking-2507") - assert path == _THINKING_FIXED - - def test_next_thinking_before_generic_qwen3(self): - path = try_get_fixed_chat_template("Qwen/Qwen3-Next-80B-A3B-Thinking") - assert path == _THINKING_FIXED - - def test_generic_qwen3_gets_qwen3_fix(self): - path = try_get_fixed_chat_template("Qwen/Qwen3-4B") - assert path == _QWEN3_FIXED - - def test_local_path_thinking_still_specific(self): - path = try_get_fixed_chat_template("/data/train/Qwen3-32B-Thinking-2507-sft-v3") - assert path == _THINKING_FIXED - - def test_local_path_base_still_generic(self): - path = try_get_fixed_chat_template("/data/train/Qwen3-32B-sft-v3") - assert path == _QWEN3_FIXED - - def test_qwen35_before_generic_qwen3(self): - path = try_get_fixed_chat_template("Qwen/Qwen3.5-0.8B") - assert path == _QWEN35_FIXED diff --git a/tests/fast/utils/chat_template_utils/test_fixed_templates.py b/tests/fast/utils/chat_template_utils/test_fixed_templates.py new file mode 100644 index 00000000000..532de906372 --- /dev/null +++ b/tests/fast/utils/chat_template_utils/test_fixed_templates.py @@ -0,0 +1,158 @@ +"""Unit tests for ``resolve_fixed_chat_template`` smallest-superset lookup. + +Rows live as ``SUPPORTED_TEMPLATES`` class attributes on each +``TITOTokenizer`` subclass; the resolver picks the smallest superset of the +caller's ``allowed_append_roles`` and returns ``(template_path, extra_kwargs)``. +""" + +import os + +import pytest + +from miles.utils.chat_template_utils import TEMPLATE_DIR, TITOTokenizerType, resolve_fixed_chat_template +from miles.utils.chat_template_utils.tito_tokenizer import FixedTemplateRow, Qwen3TITOTokenizer, Qwen35TITOTokenizer + +_QWEN3_FIXED = str(TEMPLATE_DIR / "qwen3_fixed.jinja") +_QWEN35_FIXED = str(TEMPLATE_DIR / "qwen3.5_fixed.jinja") +_THINKING_FIXED = str(TEMPLATE_DIR / "qwen3_thinking_2507_and_next_fixed.jinja") + + +@pytest.mark.parametrize( + "tito_model, expected_path", + [ + (TITOTokenizerType.QWEN3, _QWEN3_FIXED), + (TITOTokenizerType.QWEN35, _QWEN35_FIXED), + (TITOTokenizerType.QWENNEXT, _THINKING_FIXED), + ], +) +def test_tool_only_resolution_matches_registered_template(tito_model, expected_path): + path, kwargs = resolve_fixed_chat_template(tito_model, ["tool"]) + assert path == expected_path + assert kwargs == {} + assert os.path.isfile(path) + + +@pytest.mark.parametrize( + "tito_model", + [TITOTokenizerType.QWEN3, TITOTokenizerType.QWEN35, TITOTokenizerType.QWENNEXT], +) +def test_no_superset_raises(tito_model): + # ``{"tool", "system"}`` has no superset registered for the Qwen fixed + # families (which register ``{"tool"}`` and ``{"tool", "user"}``). The + # resolver must surface this as a hard error so the caller cannot silently + # fall through to a non-append-only HF render. + with pytest.raises(ValueError) as excinfo: + resolve_fixed_chat_template(tito_model, ["tool", "system"]) + msg = str(excinfo.value) + assert f"tito_model={tito_model.value}" in msg + assert "allowed_append_roles=['system', 'tool']" in msg + assert "SUPPORTED_TEMPLATES" in msg + + +# --------------------------------------------------------------------------- +# Smallest-superset semantics — covered with monkeypatched class attributes +# so the behavior is pinned independently of which rows happen to be +# registered today. +# --------------------------------------------------------------------------- + + +def test_subset_caller_falls_through_to_multi_role_row(monkeypatch): + # Only a multi-role row is registered; a strict-subset caller resolves to it. + monkeypatch.setattr( + Qwen3TITOTokenizer, + "SUPPORTED_TEMPLATES", + ( + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="fake.jinja", + ), + ), + ) + path, kwargs = resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool"]) + assert path == str(TEMPLATE_DIR / "fake.jinja") + assert kwargs == {} + + +def test_exact_match_wins_over_strict_superset(monkeypatch): + # Both an exact ``{tool}`` row and a multi-role ``{tool, user}`` row exist. + # Smallest-cardinality wins, so caller ``["tool"]`` gets the exact row. + monkeypatch.setattr( + Qwen3TITOTokenizer, + "SUPPORTED_TEMPLATES", + ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template="exact.jinja", + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="multi.jinja", + ), + ), + ) + path, kwargs = resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool"]) + assert path == str(TEMPLATE_DIR / "exact.jinja") + assert kwargs == {} + + +def test_ambiguous_minimal_supersets_raise(monkeypatch): + # Two rows of equal cardinality both ⊇ caller's roles → ambiguous. + monkeypatch.setattr( + Qwen3TITOTokenizer, + "SUPPORTED_TEMPLATES", + ( + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="a.jinja", + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "system"}), + template="b.jinja", + ), + ), + ) + with pytest.raises(ValueError, match="Ambiguous fixed-template registration"): + resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool"]) + + +def test_other_tito_model_rows_ignored(monkeypatch): + # Rows for a different tito_model never participate in the match — when + # the requested family has nothing registered, the resolver must throw. + monkeypatch.setattr(Qwen3TITOTokenizer, "SUPPORTED_TEMPLATES", ()) + monkeypatch.setattr( + Qwen35TITOTokenizer, + "SUPPORTED_TEMPLATES", + ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template="qwen35.jinja", + ), + ), + ) + with pytest.raises(ValueError, match="No SUPPORTED_TEMPLATES row registered"): + resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool"]) + + +def test_kwargs_only_row_returns_none_path_and_kwargs(monkeypatch): + # A row with template=None registers a kwargs-only fix (HF native + kwargs + # override). Resolver returns (None, kwargs) so caller can keep the HF + # template and merge the kwargs. + monkeypatch.setattr( + Qwen3TITOTokenizer, + "SUPPORTED_TEMPLATES", + ( + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template=None, + extra_kwargs={"clear_thinking": False}, + ), + ), + ) + path, kwargs = resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool", "user"]) + assert path is None + assert kwargs == {"clear_thinking": False} + + +def test_invalid_role_raises(): + with pytest.raises(ValueError, match="Unknown roles"): + resolve_fixed_chat_template(TITOTokenizerType.QWEN3, ["tool", "users"]) diff --git a/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py b/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py index 3cefca9b173..91df34aeb81 100644 --- a/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py +++ b/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py @@ -16,12 +16,15 @@ import pytest -from miles.utils.chat_template_utils.autofix import try_get_fixed_chat_template +from miles.utils.chat_template_utils import TITOTokenizerType, resolve_fixed_chat_template from miles.utils.chat_template_utils.template import load_hf_chat_template from miles.utils.test_utils.chat_template_verify import ( + CaseSpec, assert_pretokenized_equals_standard, + enable_thinking_variants, + format_case_id, + select_cases, simulate_pretokenized_path, - verify_append_only, ) from miles.utils.test_utils.mock_trajectories import ( MultiTurnTrajectory, @@ -30,34 +33,92 @@ last_user_index, ) -# --------------------------------------------------------------------------- -# Load chat templates -# --------------------------------------------------------------------------- - -def _load_fixed(hf_id: str) -> str: - path = try_get_fixed_chat_template(hf_id) - assert path is not None, f"try_get_fixed_chat_template should resolve {hf_id}" +def _load_fixed(tito_model: TITOTokenizerType) -> str: + path, _kwargs = resolve_fixed_chat_template(tito_model, ["tool"]) + assert path is not None, f"resolve_fixed_chat_template should resolve {tito_model.value}" with open(path) as f: return f.read() -TEMPLATES_WITH_THINKING = { - "qwen3_fixed": _load_fixed("Qwen/Qwen3-0.6B"), - "qwen3.5_fixed": _load_fixed("Qwen/Qwen3.5-0.8B"), - "glm5": load_hf_chat_template("zai-org/GLM-5"), - "glm47_flash": load_hf_chat_template("zai-org/GLM-4.7-Flash"), - "qwen3_thinking_2507_fixed": _load_fixed("Qwen/Qwen3-4B-Thinking-2507"), - "qwen3_next_thinking_fixed": _load_fixed("Qwen/Qwen3-Next-80B-A3B-Thinking"), -} +# --------------------------------------------------------------------------- +# Per-template capability declarations +# --------------------------------------------------------------------------- +# +# Each entry: (name, content, supports_thinking, allowed_append_roles, extra_template_kwargs) +# +# Design: +# * fixed templates default to {tool} only. When a fixed template carries a +# clear_thinking kwarg gate (Qwen3 today), a *_clear_thinking_off entry with +# {tool, user} + clear_thinking=False covers the multi-user-append surface +# the same way GLM does on its HF-native template. +# * GLM thinking templates are covered twice: a default entry (tool only, no +# kwargs) matching production behavior, and a *_clear_thinking_off entry that +# adds {user} to allowed roles with clear_thinking=False to preserve +# reasoning across user turns (GLM's canonical pattern for session reset). +# * Other non-thinking HF native templates only test {tool} — scope aligned +# with fixed. +# * system role is never tested here; those trajectories remain in ALL_CASES +# for CLI use but are filtered out of this test by the above role sets. + +_TEMPLATES: list[tuple[str, str, bool, frozenset[str], dict]] = [ + # fixed templates: tool only + ("qwen3_fixed", _load_fixed(TITOTokenizerType.QWEN3), True, frozenset({"tool"}), {}), + ( + "qwen3_fixed_clear_thinking_off", + _load_fixed(TITOTokenizerType.QWEN3), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + ("qwen3.5_fixed", _load_fixed(TITOTokenizerType.QWEN35), True, frozenset({"tool"}), {}), + ( + "qwen3.5_fixed_clear_thinking_off", + _load_fixed(TITOTokenizerType.QWEN35), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + ("qwen3_thinking_2507_fixed", _load_fixed(TITOTokenizerType.QWENNEXT), True, frozenset({"tool"}), {}), + ("qwen3_next_thinking_fixed", _load_fixed(TITOTokenizerType.QWENNEXT), True, frozenset({"tool"}), {}), + ( + "qwen3_next_thinking_fixed_clear_thinking_off", + _load_fixed(TITOTokenizerType.QWENNEXT), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + # GLM thinking: default (tool only) + user-append variant with clear_thinking=False + ("glm5", load_hf_chat_template("zai-org/GLM-5"), True, frozenset({"tool"}), {}), + ( + "glm5_clear_thinking_off", + load_hf_chat_template("zai-org/GLM-5"), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + ("glm47_flash", load_hf_chat_template("zai-org/GLM-4.7-Flash"), True, frozenset({"tool"}), {}), + ( + "glm47_flash_clear_thinking_off", + load_hf_chat_template("zai-org/GLM-4.7-Flash"), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + ( + "glm47_flash_clear_thinking_off_with_system", + load_hf_chat_template("zai-org/GLM-4.7-Flash"), + True, + frozenset({"tool", "user", "system"}), + {"clear_thinking": False}, + ), + # other HF native non-thinking: tool only + ("qwen3_instruct_2507", load_hf_chat_template("Qwen/Qwen3-4B-Instruct-2507"), False, frozenset({"tool"}), {}), + ("qwen3_next_instruct", load_hf_chat_template("Qwen/Qwen3-Next-80B-A3B-Instruct"), False, frozenset({"tool"}), {}), + ("qwen3_coder_next", load_hf_chat_template("Qwen/Qwen3-Coder-Next"), False, frozenset({"tool"}), {}), + ("glm4", load_hf_chat_template("THUDM/glm-4-9b-chat"), False, frozenset({"tool"}), {}), +] -ALL_TEMPLATES = { - **TEMPLATES_WITH_THINKING, - "qwen3_instruct_2507": load_hf_chat_template("Qwen/Qwen3-4B-Instruct-2507"), - "qwen3_next_instruct": load_hf_chat_template("Qwen/Qwen3-Next-80B-A3B-Instruct"), - "qwen3_coder_next": load_hf_chat_template("Qwen/Qwen3-Coder-Next"), - "glm4": load_hf_chat_template("THUDM/glm-4-9b-chat"), -} # Original (unfixed) HF templates referenced by negative tests _ORIGINAL_TEMPLATES = { @@ -67,29 +128,46 @@ def _load_fixed(hf_id: str) -> str: } +def _build_pretokenized_params(): + # Thinking templates: every selected trajectory × {enable_thinking=True, False}. + # Non-thinking templates: only non-thinking trajectories, no enable_thinking kwarg. + params = [] + for name, content, supports_thinking, allowed_roles, extra_kwargs in _TEMPLATES: + cases = select_cases( + allowed_append_roles=allowed_roles, + is_thinking=None if supports_thinking else False, + ) + variants = enable_thinking_variants("both" if supports_thinking else "off") + for case in cases: + for variant in variants: + kwargs = {**variant, **extra_kwargs} + ident = f"{name}-{format_case_id(case, kwargs)}" + params.append(pytest.param(content, case, kwargs, id=ident)) + return params + + # =========================================================================== -# Auto-generate test cases from PRETOKENIZE_POSITIONS +# Core tests: every (template, case, kwargs) tuple satisfies append-only # =========================================================================== -from miles.utils.test_utils.chat_template_verify import ( # noqa: E402 - INTERMEDIATE_SYSTEM_CASES, - INTERMEDIATE_SYSTEM_THINKING_CASES, - STANDARD_CASES, - THINKING_CASES, -) +@pytest.mark.parametrize("chat_template, case, kwargs", _build_pretokenized_params()) +def test_pretokenized(chat_template: str, case: CaseSpec, kwargs: dict): + assert_pretokenized_equals_standard( + chat_template=chat_template, + messages=deepcopy(case.traj_cls.MESSAGES), + pretokenized_num_message=case.pretokenize_n, + tools=case.tools, + **kwargs, + ) -def _to_pytest_params(cases): - """Convert (name, cls, n, tools) tuples to pytest.param list.""" - return [pytest.param(cls, n, tools, id=name) for name, cls, n, tools in cases] +# =========================================================================== +# Negative tests: original (unfixed) templates fail prefix invariant +# =========================================================================== -_STANDARD_PARAMS = _to_pytest_params(STANDARD_CASES) -_THINKING_PARAMS = _to_pytest_params(THINKING_CASES) -_INTERMEDIATE_SYSTEM_PARAMS = _to_pytest_params(INTERMEDIATE_SYSTEM_CASES) -_INTERMEDIATE_SYSTEM_THINKING_PARAMS = _to_pytest_params(INTERMEDIATE_SYSTEM_THINKING_CASES) -# (chat_template, trajectory_cls, pretokenize_n) — original templates that break prefix invariant +# (chat_template, trajectory_cls, pretokenize_n) _MISMATCH_CASES = [ pytest.param(_ORIGINAL_TEMPLATES["qwen3_original"], SingleToolTrajectory, 3, id="qwen3_original-single_tool"), pytest.param(_ORIGINAL_TEMPLATES["qwen3_original"], MultiTurnTrajectory, 3, id="qwen3_original-multi_turn"), @@ -105,147 +183,6 @@ def _to_pytest_params(cases): ] -def _template_params(templates: dict[str, str]) -> list: - """Convert a {name: template_str} dict to a list of pytest.param(template_str, id=name).""" - return [pytest.param(v, id=k) for k, v in templates.items()] - - -# Intermediate-system compatibility: only qwen3.5_fixed is known to reject them. -# test_intermediate_system_probe_matrix locks this set against drift. -_INTERMEDIATE_SYSTEM_FORBIDDEN = {"qwen3.5_fixed"} -_INTERMEDIATE_SYSTEM_TEMPLATES = {k: v for k, v in ALL_TEMPLATES.items() if k not in _INTERMEDIATE_SYSTEM_FORBIDDEN} -_INTERMEDIATE_SYSTEM_THINKING_TEMPLATES = { - k: v for k, v in TEMPLATES_WITH_THINKING.items() if k not in _INTERMEDIATE_SYSTEM_FORBIDDEN -} - - -def _collect_intermediate_system_failures(template_id: str, chat_template: str) -> list[str]: - failures: list[str] = [] - for case_name, traj_cls, n, tools in INTERMEDIATE_SYSTEM_CASES: - result = verify_append_only(chat_template, deepcopy(traj_cls.MESSAGES), n, tools=tools, case_name=case_name) - if not result.passed: - failures.append(f"{case_name}: {result.error}") - - if template_id in TEMPLATES_WITH_THINKING: - for enable in (True, False): - suffix = "thinking_on" if enable else "thinking_off" - for case_name, traj_cls, n, tools in INTERMEDIATE_SYSTEM_THINKING_CASES: - full_case_name = f"{case_name}[{suffix}]" - result = verify_append_only( - chat_template, - deepcopy(traj_cls.MESSAGES), - n, - tools=tools, - case_name=full_case_name, - enable_thinking=enable, - ) - if not result.passed: - failures.append(f"{full_case_name}: {result.error}") - - return failures - - -def _format_failure_map(failure_map: dict[str, list[str]]) -> str: - lines: list[str] = [] - for template_id in sorted(failure_map): - lines.append(f"{template_id}:") - lines.extend(f" - {item}" for item in failure_map[template_id]) - return "\n".join(lines) - - -# =========================================================================== -# Core tests: all templates × all trajectory/position combinations -# =========================================================================== - - -@pytest.mark.parametrize("chat_template", _template_params(ALL_TEMPLATES)) -@pytest.mark.parametrize("trajectory_cls,pretokenize_n,tools", _STANDARD_PARAMS) -def test_pretokenized_equals_standard(chat_template, trajectory_cls, pretokenize_n, tools): - """Pretokenized incremental path produces same text as standard full render.""" - assert_pretokenized_equals_standard( - chat_template=chat_template, - messages=deepcopy(trajectory_cls.MESSAGES), - pretokenized_num_message=pretokenize_n, - tools=tools, - ) - - -# =========================================================================== -# Thinking tests: thinking-capable templates × trajectories × enable_thinking -# =========================================================================== - - -@pytest.mark.parametrize("chat_template", _template_params(TEMPLATES_WITH_THINKING)) -@pytest.mark.parametrize("trajectory_cls,pretokenize_n,tools", _THINKING_PARAMS) -@pytest.mark.parametrize("enable_thinking", [True, False], ids=["thinking_on", "thinking_off"]) -def test_pretokenized_thinking(chat_template, trajectory_cls, pretokenize_n, tools, enable_thinking): - """Thinking-capable templates work with pretokenized path and enable_thinking flag.""" - assert_pretokenized_equals_standard( - chat_template=chat_template, - messages=deepcopy(trajectory_cls.MESSAGES), - pretokenized_num_message=pretokenize_n, - tools=tools, - enable_thinking=enable_thinking, - ) - - -# =========================================================================== -# Intermediate system message tests: templates that support them -# =========================================================================== - - -def test_intermediate_system_probe_matrix(): - """Probe ALL_TEMPLATES and lock the allow/forbid intermediate-system matrix.""" - failure_map: dict[str, list[str]] = {} - for template_id, chat_template in ALL_TEMPLATES.items(): - failures = _collect_intermediate_system_failures(template_id, chat_template) - if failures: - failure_map[template_id] = failures - - detected_forbidden = set(failure_map.keys()) - assert detected_forbidden == _INTERMEDIATE_SYSTEM_FORBIDDEN, ( - f"Intermediate-system forbidden set changed.\n" - f"expected={sorted(_INTERMEDIATE_SYSTEM_FORBIDDEN)}\n" - f"detected={sorted(detected_forbidden)}\n" - f"{_format_failure_map(failure_map)}" - ) - qwen35_failures = failure_map.get("qwen3.5_fixed", []) - assert any("System message must be at the beginning." in failure for failure in qwen35_failures), qwen35_failures - - -@pytest.mark.parametrize("chat_template", _template_params(_INTERMEDIATE_SYSTEM_TEMPLATES)) -@pytest.mark.parametrize("trajectory_cls,pretokenize_n,tools", _INTERMEDIATE_SYSTEM_PARAMS) -def test_pretokenized_intermediate_system(chat_template, trajectory_cls, pretokenize_n, tools): - """Templates in the allowlist support intermediate system messages.""" - assert_pretokenized_equals_standard( - chat_template=chat_template, - messages=deepcopy(trajectory_cls.MESSAGES), - pretokenized_num_message=pretokenize_n, - tools=tools, - ) - - -@pytest.mark.parametrize("chat_template", _template_params(_INTERMEDIATE_SYSTEM_THINKING_TEMPLATES)) -@pytest.mark.parametrize("trajectory_cls,pretokenize_n,tools", _INTERMEDIATE_SYSTEM_THINKING_PARAMS) -@pytest.mark.parametrize("enable_thinking", [True, False], ids=["thinking_on", "thinking_off"]) -def test_pretokenized_intermediate_system_thinking( - chat_template, trajectory_cls, pretokenize_n, tools, enable_thinking -): - """Thinking templates in the allowlist support intermediate system messages.""" - assert_pretokenized_equals_standard( - chat_template=chat_template, - messages=deepcopy(trajectory_cls.MESSAGES), - pretokenized_num_message=pretokenize_n, - tools=tools, - enable_thinking=enable_thinking, - ) - - -# =========================================================================== -# Negative tests: original (unfixed) templates fail prefix invariant -# =========================================================================== - - @pytest.mark.parametrize("chat_template,trajectory_cls,pretokenize_n", _MISMATCH_CASES) def test_original_template_prefix_mismatch(chat_template, trajectory_cls, pretokenize_n): """Original templates with loop.last cause prefix mismatch (our fix resolves this).""" @@ -268,7 +205,20 @@ def test_original_template_prefix_mismatch(chat_template, trajectory_cls, pretok _CROSS_USER_THINKING_N = last_user_index(MultiUserTurnThinkingTrajectory.MESSAGES) -@pytest.mark.parametrize("chat_template", _template_params(TEMPLATES_WITH_THINKING)) +def _unique_thinking_templates(): + seen: set[str] = set() + out = [] + for name, content, supports_thinking, _, _ in _TEMPLATES: + if not supports_thinking: + continue + if content in seen: + continue + seen.add(content) + out.append(pytest.param(content, id=name)) + return out + + +@pytest.mark.parametrize("chat_template", _unique_thinking_templates()) @pytest.mark.parametrize("enable_thinking", [True, False], ids=["thinking_on", "thinking_off"]) def test_cross_user_turn_thinking_prefix_mismatch(chat_template, enable_thinking): """Thinking templates compress reasoning_content from earlier user turns, breaking prefix invariant.""" diff --git a/tests/fast/utils/chat_template_utils/test_pretokenized_via_tito.py b/tests/fast/utils/chat_template_utils/test_pretokenized_via_tito.py new file mode 100644 index 00000000000..9430ebfb4a2 --- /dev/null +++ b/tests/fast/utils/chat_template_utils/test_pretokenized_via_tito.py @@ -0,0 +1,161 @@ +"""Unit tests for ``verify_append_only_via_tito_instance`` / +``run_all_checks_via_tito``: PASS on registered TITO families, FAIL on the +unfixed Qwen3 chat template, FAIL on a test-local ``_BuggyQwen3TITOTokenizer`` +that omits the ``\\n`` insertion at the ``<|im_end|>`` boundary. +""" + +from copy import deepcopy + +import pytest +from tests.ci.ci_register import register_cpu_ci +from transformers import AutoTokenizer + +register_cpu_ci(est_time=120, suite="stage-a-fast") + + +from miles.utils.chat_template_utils import TITOTokenizerType, resolve_fixed_chat_template +from miles.utils.chat_template_utils.tito_tokenizer import Qwen3TITOTokenizer +from miles.utils.test_utils.chat_template_verify import run_all_checks_via_tito, verify_append_only_via_tito_instance +from miles.utils.test_utils.mock_trajectories import SingleToolTrajectory + +# --------------------------------------------------------------------------- +# Test helpers +# --------------------------------------------------------------------------- + + +def _setup_tokenizer_with_registered_template( + model_id: str, + family: TITOTokenizerType, + roles: list[str], +): + """Mirror what production wiring does at startup. + + Loads tokenizer, looks up the registered ``SUPPORTED_TEMPLATES`` row for + ``(family, roles)``, and applies the resolved fixed template (if any) onto + ``tokenizer.chat_template``. Returns ``(tokenizer, extra_kwargs)``. + + A fresh tokenizer instance per call avoids state-mutation hazards from + overwriting ``chat_template``. + """ + tokenizer = AutoTokenizer.from_pretrained(model_id) + fixed_path, extra_kwargs = resolve_fixed_chat_template(family, roles) + if fixed_path is not None: + with open(fixed_path) as f: + tokenizer.chat_template = f.read() + return tokenizer, dict(extra_kwargs) + + +# --------------------------------------------------------------------------- +# (1) PASS on registered families × role surfaces +# --------------------------------------------------------------------------- + + +_PASS_PARAMS = [ + pytest.param(TITOTokenizerType.QWEN3, "Qwen/Qwen3-0.6B", frozenset({"tool"}), id="qwen3-tool"), + pytest.param(TITOTokenizerType.QWEN3, "Qwen/Qwen3-0.6B", frozenset({"tool", "user"}), id="qwen3-tool_user"), + pytest.param(TITOTokenizerType.QWEN35, "Qwen/Qwen3.5-0.8B", frozenset({"tool"}), id="qwen35-tool"), + pytest.param(TITOTokenizerType.QWEN35, "Qwen/Qwen3.5-0.8B", frozenset({"tool", "user"}), id="qwen35-tool_user"), + pytest.param(TITOTokenizerType.QWENNEXT, "Qwen/Qwen3-4B-Thinking-2507", frozenset({"tool"}), id="qwennext-tool"), + pytest.param( + TITOTokenizerType.QWENNEXT, + "Qwen/Qwen3-4B-Thinking-2507", + frozenset({"tool", "user"}), + id="qwennext-tool_user", + ), + pytest.param(TITOTokenizerType.GLM47, "zai-org/GLM-4.7-Flash", frozenset({"tool"}), id="glm47-tool"), + pytest.param(TITOTokenizerType.GLM47, "zai-org/GLM-4.7-Flash", frozenset({"tool", "user"}), id="glm47-tool_user"), + pytest.param( + TITOTokenizerType.GLM47, + "zai-org/GLM-4.7-Flash", + frozenset({"tool", "user", "system"}), + id="glm47-tool_user_system", + ), +] + + +@pytest.mark.parametrize("family,model_id,roles", _PASS_PARAMS) +def test_via_tito_pass_on_registered_families(family, model_id, roles): + """All 4 registered TITO families round-trip cleanly via decode-roundtrip.""" + tokenizer, extra_kwargs = _setup_tokenizer_with_registered_template(model_id, family, sorted(roles)) + results = run_all_checks_via_tito( + tokenizer, + family, + allowed_append_roles=set(roles), + thinking="both", + extra_template_kwargs=extra_kwargs, + ) + failures = [r for r in results if not r.passed] + assert not failures, ( + f"Expected all PASS for {family.value} × {sorted(roles)} via TITO primitive; " + f"got {len(failures)} FAIL(s) out of {len(results)}:\n" + + "\n".join(f" [{r.case_name}] {r.error}" for r in failures[:5]) + ) + + +# --------------------------------------------------------------------------- +# (2) FAIL on the original unfixed Qwen3 chat template +# --------------------------------------------------------------------------- + + +def test_via_tito_fail_on_original_qwen3_template(): + """The original Qwen3 chat template uses ``loop.last`` and breaks append-only. + + Bypass ``resolve_fixed_chat_template`` entirely — keep the HF default + ``tokenizer.chat_template`` and assert the primitive surfaces a FAIL. + """ + tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") + # Do NOT overwrite tokenizer.chat_template — keep the broken HF default. + + # Cast wide: thinking=both + multi-user-turn surface so trajectories that + # actually advance ``last_query_index`` between prefix and full are exercised. + # Those are the ones where ``loop.index0 > ns.last_query_index`` truncation + # in the original Qwen3 template renders the same assistant turn differently + # depending on the boundary position. + results = run_all_checks_via_tito( + tokenizer, + TITOTokenizerType.QWEN3, + allowed_append_roles={"tool", "user"}, + thinking="both", + ) + failures = [r for r in results if not r.passed] + assert failures, "Expected ≥1 FAIL on the original (unfixed) Qwen3 chat template; got all PASS." + + +# --------------------------------------------------------------------------- +# (3) FAIL on a test-local buggy subclass +# --------------------------------------------------------------------------- + + +class _BuggyQwen3TITOTokenizer(Qwen3TITOTokenizer): + """Test-only Qwen3 variant with the ``\\n`` boundary insertion deleted. + + Real ``Qwen3TITOTokenizer.merge_tokens`` appends ``self._newline_id`` after + a trailing ``<|im_end|>`` because the model stops without emitting the + newline the chat template would otherwise produce. This variant skips + that fixup; the decode-roundtrip primitive is expected to surface it as a + single-character diff at the prefix-suffix junction. + """ + + def merge_tokens(self, old_messages, new_messages, pretokenized_token_ids, tools=None): + incremental = self.tokenize_additional_non_assistant(old_messages, new_messages, tools) + # Intentionally omit the `+\n` insertion — that's the bug we're catching. + return list(pretokenized_token_ids) + incremental + + +def test_via_tito_fail_on_buggy_qwen3_subclass(): + """A buggy ``merge_tokens`` produces a junction-level diff that the verifier surfaces.""" + tokenizer, _ = _setup_tokenizer_with_registered_template("Qwen/Qwen3-0.6B", TITOTokenizerType.QWEN3, ["tool"]) + buggy = _BuggyQwen3TITOTokenizer(tokenizer, allowed_append_roles=["tool"]) + + result = verify_append_only_via_tito_instance( + buggy, + tokenizer, + deepcopy(SingleToolTrajectory.MESSAGES), + pretokenized_num_message=3, + tools=SingleToolTrajectory.TOOLS, + case_name="buggy_qwen3-single_tool-N3", + ) + assert not result.passed, "Expected FAIL on _BuggyQwen3TITOTokenizer (omits the `+\\n` boundary patch); got PASS." + assert "Decode-roundtrip mismatch" in ( + result.error or "" + ), f"Expected decode-roundtrip diff in error message; got: {result.error}" diff --git a/tests/fast/utils/chat_template_utils/test_template.py b/tests/fast/utils/chat_template_utils/test_template.py index 89c0aed40f2..ec0a5f83e19 100644 --- a/tests/fast/utils/chat_template_utils/test_template.py +++ b/tests/fast/utils/chat_template_utils/test_template.py @@ -21,7 +21,6 @@ register_cpu_ci(est_time=60, suite="stage-a-fast") import copy -from pathlib import Path from unittest.mock import MagicMock import pytest @@ -29,25 +28,16 @@ from sglang.srt.entrypoints.openai.serving_chat import OpenAIServingChat from transformers import AutoTokenizer +from miles.utils.chat_template_utils import TITOTokenizerType, resolve_fixed_chat_template from miles.utils.chat_template_utils.template import apply_chat_template from miles.utils.processing_utils import load_tokenizer -from miles.utils.test_utils.mock_trajectories import ( - IntermediateSystemThinkingTrajectory, - IntermediateSystemTrajectory, - LongChainThinkingTrajectory, - LongChainTrajectory, - MultiToolSingleTurnTrajectory, - MultiTurnNoToolThinkingTrajectory, - MultiTurnNoToolTrajectory, - MultiTurnThinkingTrajectory, - MultiTurnTrajectory, - MultiUserToolChainTrajectory, - MultiUserTurnThinkingTrajectory, - ParallelToolsTrajectory, - RetrySystemTrajectory, - SingleToolThinkingTrajectory, - SingleToolTrajectory, +from miles.utils.test_utils.chat_template_verify import ( + CaseSpec, + enable_thinking_variants, + format_case_id, + select_cases, ) +from miles.utils.test_utils.mock_trajectories import SimpleNoToolTrajectory, SingleToolTrajectory # --------------------------------------------------------------------------- # SGLang reference: calls OpenAIServingChat._process_messages directly @@ -89,7 +79,7 @@ def sglang_prompt_ids( # --------------------------------------------------------------------------- -# Tokenizer cache & fixtures +# Tokenizer cache & fixed-template loader # --------------------------------------------------------------------------- _TOK_CACHE: dict[str, AutoTokenizer] = {} @@ -101,111 +91,108 @@ def _get_tokenizer(model_id: str) -> AutoTokenizer: return _TOK_CACHE[model_id] -_MODEL_IDS = [ - "Qwen/Qwen3-4B", - "zai-org/GLM-4.7-Flash", - "Qwen/Qwen3.5-4B", - "Qwen/Qwen3-Coder-Next", -] - -# Fixed chat templates — keyed by model ID, loaded from bundled .jinja files. -_TEMPLATES_DIR = Path(__file__).resolve().parents[4] / "miles" / "utils" / "chat_template_utils" / "templates" -_FIXED_CHAT_TEMPLATES: dict[str, str] = { - "Qwen/Qwen3.5-4B": (_TEMPLATES_DIR / "qwen3.5_fixed.jinja").read_text(), -} - - -@pytest.fixture(params=_MODEL_IDS, ids=[m.split("/")[-1] for m in _MODEL_IDS]) -def tokenizer(request) -> AutoTokenizer: - return _get_tokenizer(request.param) +def _load_fixed_or_none(tito_model: TITOTokenizerType | None) -> str | None: + """Return the bundled fixed chat-template content for *tito_model*, or ``None``.""" + if tito_model is None: + return None + path, _kwargs = resolve_fixed_chat_template(tito_model, ["tool"]) + if path is None: + return None + with open(path) as f: + return f.read() # --------------------------------------------------------------------------- -# Trajectory / kwargs definitions +# Per-model declarations # --------------------------------------------------------------------------- - -_NO_INTERMEDIATE_SYSTEM_MODELS = { - "Qwen/Qwen3.5-4B", -} - -_STANDARD_CASES = [ - pytest.param(SingleToolTrajectory, {}, id="single_tool"), - pytest.param(MultiTurnTrajectory, {}, id="multi_turn"), - pytest.param(MultiToolSingleTurnTrajectory, {}, id="multi_tool_single_turn"), - pytest.param(ParallelToolsTrajectory, {}, id="parallel_tools"), - pytest.param(LongChainTrajectory, {}, id="long_chain"), - pytest.param(MultiUserToolChainTrajectory, {}, id="multi_user_tool_chain"), - pytest.param(MultiTurnNoToolTrajectory, {}, id="multi_turn_no_tool"), -] - -# Trajectories with intermediate system messages. -_INTERMEDIATE_SYSTEM_CASES = [ - pytest.param(RetrySystemTrajectory, {}, id="retry_system"), - pytest.param(IntermediateSystemTrajectory, {}, id="intermediate_system"), +# +# (model_id, supports_thinking, fixed_template_tito_model, allowed_append_roles) +# +# ``fixed_template_tito_model`` is ``None`` when the model uses its native HF +# template; set to a ``TITOTokenizerType`` when the test should swap in the +# bundled fixed template registered for that family. ``allowed_append_roles`` +# reflects the set of append-role combinations the model's template can render +# without raising — test asserts that the sglang path and our path produce +# identical tokens on all such cases. Qwen3.5-4B uses the bundled fixed +# template which raises on intermediate system post-revert, so the role set +# is narrowed to {tool} only. + +_MODELS: list[tuple[str, bool, TITOTokenizerType | None, frozenset[str]]] = [ + ("Qwen/Qwen3-4B", True, None, frozenset({"tool", "user", "system"})), + ("zai-org/GLM-4.7-Flash", True, None, frozenset({"tool", "user", "system"})), + ("Qwen/Qwen3.5-4B", True, TITOTokenizerType.QWEN35, frozenset({"tool"})), + ("Qwen/Qwen3-Coder-Next", False, None, frozenset({"tool", "user", "system"})), ] -_THINKING_CASES = [ - pytest.param(SingleToolThinkingTrajectory, {"enable_thinking": True}, id="single_tool_thinking_on"), - pytest.param(SingleToolThinkingTrajectory, {"enable_thinking": False}, id="single_tool_thinking_off"), - pytest.param(MultiTurnThinkingTrajectory, {"enable_thinking": True}, id="multi_turn_thinking_on"), - pytest.param(LongChainThinkingTrajectory, {"enable_thinking": True}, id="long_chain_thinking_on"), - pytest.param(MultiUserTurnThinkingTrajectory, {"enable_thinking": True}, id="multi_user_turn_thinking_on"), - pytest.param(MultiTurnNoToolThinkingTrajectory, {"enable_thinking": True}, id="multi_turn_no_tool_thinking_on"), - pytest.param(MultiTurnNoToolThinkingTrajectory, {"enable_thinking": False}, id="multi_turn_no_tool_thinking_off"), -] -_INTERMEDIATE_SYSTEM_THINKING_CASES = [ - pytest.param( - IntermediateSystemThinkingTrajectory, {"enable_thinking": True}, id="intermediate_system_thinking_on" - ), -] +def _build_align_params(): + # Thinking templates: every selected trajectory × {enable_thinking=True, False}. + # Non-thinking templates: only non-thinking trajectories, no enable_thinking kwarg. + params = [] + for model_id, supports_thinking, fixed_tito, allowed_roles in _MODELS: + short = model_id.split("/")[-1] + cases = select_cases( + allowed_append_roles=allowed_roles, + is_thinking=None if supports_thinking else False, + ) + variants = enable_thinking_variants("both" if supports_thinking else "off") + for case in cases: + # SimpleNoTool ends with a plain assistant message (no tool_calls). + # sglang's _process_messages treats that as continue_final_message + # and drops the trailing <|im_start|>assistant header, which + # diverges from apply_chat_template with add_generation_prompt=True. + # SGLang-alignment-specific, not a property of the trajectory itself, + # so this skip stays inline rather than living on mock_trajectories. + if case.traj_cls is SimpleNoToolTrajectory: + continue + for variant in variants: + ident = f"{short}-{format_case_id(case, variant)}" + params.append(pytest.param(model_id, fixed_tito, case, variant, id=ident)) + return params + + +def _per_model_params(): + return [pytest.param(model_id, fixed_tito, id=model_id.split("/")[-1]) for model_id, _, fixed_tito, _ in _MODELS] # --------------------------------------------------------------------------- -# Helper +# Tests # --------------------------------------------------------------------------- -def _assert_aligned(tokenizer, traj_cls, kwargs): - fixed_template = _FIXED_CHAT_TEMPLATES.get(tokenizer.name_or_path) - extra = {"chat_template": fixed_template} if fixed_template else {} - expected = sglang_prompt_ids(tokenizer, traj_cls.MESSAGES, traj_cls.TOOLS, **kwargs, **extra) +def _assert_aligned(tokenizer, case: CaseSpec, kwargs: dict, chat_template: str | None): + # ``kwargs`` are template kwargs (e.g. ``enable_thinking``, ``clear_thinking``) + # — both ``sglang_prompt_ids`` (via ``chat_template_kwargs``) and + # ``apply_chat_template`` (via ``**template_kwargs``) route them into jinja. + # Don't put non-template kwargs in here. + extra = {"chat_template": chat_template} if chat_template else {} + expected = sglang_prompt_ids(tokenizer, case.traj_cls.MESSAGES, case.traj_cls.TOOLS, **kwargs, **extra) actual = apply_chat_template( - traj_cls.MESSAGES, tokenizer=tokenizer, tools=traj_cls.TOOLS, tokenize=True, **kwargs, **extra + case.traj_cls.MESSAGES, + tokenizer=tokenizer, + tools=case.traj_cls.TOOLS, + tokenize=True, + **kwargs, + **extra, ) assert actual == expected -# --------------------------------------------------------------------------- -# Tests — parametrized over models × trajectories -# --------------------------------------------------------------------------- - - class TestAlignWithSGLang: """apply_chat_template must produce identical prompt_ids to SGLang's pipeline.""" - @pytest.mark.parametrize("traj_cls, kwargs", _STANDARD_CASES) - def test_standard(self, tokenizer, traj_cls, kwargs): - _assert_aligned(tokenizer, traj_cls, kwargs) - - @pytest.mark.parametrize("traj_cls, kwargs", _INTERMEDIATE_SYSTEM_CASES) - def test_intermediate_system(self, tokenizer, traj_cls, kwargs): - if tokenizer.name_or_path in _NO_INTERMEDIATE_SYSTEM_MODELS: - pytest.skip(f"{tokenizer.name_or_path} intentionally forbids intermediate system messages") - _assert_aligned(tokenizer, traj_cls, kwargs) - - @pytest.mark.parametrize("traj_cls, kwargs", _THINKING_CASES) - def test_thinking(self, tokenizer, traj_cls, kwargs): - _assert_aligned(tokenizer, traj_cls, kwargs) - - @pytest.mark.parametrize("traj_cls, kwargs", _INTERMEDIATE_SYSTEM_THINKING_CASES) - def test_intermediate_system_thinking(self, tokenizer, traj_cls, kwargs): - if tokenizer.name_or_path in _NO_INTERMEDIATE_SYSTEM_MODELS: - pytest.skip(f"{tokenizer.name_or_path} intentionally forbids intermediate system messages") - _assert_aligned(tokenizer, traj_cls, kwargs) + @pytest.mark.parametrize("model_id, fixed_tito, case, kwargs", _build_align_params()) + def test_align(self, model_id, fixed_tito, case, kwargs): + tokenizer = _get_tokenizer(model_id) + chat_template = _load_fixed_or_none(fixed_tito) + _assert_aligned(tokenizer, case, kwargs, chat_template) - def test_json_string_arguments(self, tokenizer): + @pytest.mark.parametrize("model_id, fixed_tito", _per_model_params()) + def test_json_string_arguments(self, model_id, fixed_tito): """JSON-string tool_call arguments should produce same IDs as dict arguments.""" + tokenizer = _get_tokenizer(model_id) + chat_template = _load_fixed_or_none(fixed_tito) + extra = {"chat_template": chat_template} if chat_template else {} messages = [ {"role": "user", "content": "weather?"}, { @@ -230,25 +217,27 @@ def test_json_string_arguments(self, tokenizer): }, } ] - fixed_template = _FIXED_CHAT_TEMPLATES.get(tokenizer.name_or_path) - extra = {"chat_template": fixed_template} if fixed_template else {} expected = sglang_prompt_ids(tokenizer, messages, tools, **extra) actual = apply_chat_template(messages, tokenizer=tokenizer, tools=tools, tokenize=True, **extra) assert actual == expected - def test_no_tools(self, tokenizer): + @pytest.mark.parametrize("model_id, fixed_tito", _per_model_params()) + def test_no_tools(self, model_id, fixed_tito): """Plain conversation without tools.""" + tokenizer = _get_tokenizer(model_id) + chat_template = _load_fixed_or_none(fixed_tito) + extra = {"chat_template": chat_template} if chat_template else {} messages = [ {"role": "system", "content": "You are helpful."}, {"role": "user", "content": "Hello"}, ] - fixed_template = _FIXED_CHAT_TEMPLATES.get(tokenizer.name_or_path) - extra = {"chat_template": fixed_template} if fixed_template else {} expected = sglang_prompt_ids(tokenizer, messages, **extra) actual = apply_chat_template(messages, tokenizer=tokenizer, tokenize=True, **extra) assert actual == expected - def test_does_not_mutate_input(self, tokenizer): + @pytest.mark.parametrize("model_id, fixed_tito", _per_model_params()) + def test_does_not_mutate_input(self, model_id, fixed_tito): + tokenizer = _get_tokenizer(model_id) messages = copy.deepcopy(SingleToolTrajectory.MESSAGES) tools = copy.deepcopy(SingleToolTrajectory.TOOLS) saved_msgs = copy.deepcopy(messages) diff --git a/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py b/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py index 07ff85598ef..15714692c27 100644 --- a/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py +++ b/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py @@ -54,10 +54,12 @@ import pytest from transformers import AutoTokenizer -from miles.utils.chat_template_utils import MismatchType, apply_chat_template, try_get_fixed_chat_template +from miles.utils.chat_template_utils import MismatchType, apply_chat_template, resolve_fixed_chat_template from miles.utils.chat_template_utils.tito_tokenizer import ( GLM47TITOTokenizer, Qwen3TITOTokenizer, + Qwen35TITOTokenizer, + QwenNextTITOTokenizer, TITOTokenizer, TITOTokenizerType, _build_dummy_assistant, @@ -82,8 +84,8 @@ _TOK_CACHE: dict[tuple[str, str | None], AutoTokenizer] = {} -def _get_tokenizer(model_id: str) -> AutoTokenizer: - chat_template_path = try_get_fixed_chat_template(model_id) +def _get_tokenizer(model_id: str, tito_type: TITOTokenizerType | None = None) -> AutoTokenizer: + chat_template_path = resolve_fixed_chat_template(tito_type, ["tool"])[0] if tito_type is not None else None cache_key = (model_id, chat_template_path) if cache_key not in _TOK_CACHE: _TOK_CACHE[cache_key] = load_tokenizer( @@ -102,9 +104,9 @@ def _get_tokenizer(model_id: str) -> AutoTokenizer: # for tests specific to one subclass's boundary logic. # --------------------------------------------------------------------------- -_TITO_MODELS: dict[str, tuple[str, type[TITOTokenizer]]] = { - "qwen3": ("Qwen/Qwen3-4B", Qwen3TITOTokenizer), - "glm47": ("zai-org/GLM-4.7-Flash", GLM47TITOTokenizer), +_TITO_MODELS: dict[str, tuple[str, type[TITOTokenizer], TITOTokenizerType]] = { + "qwen3": ("Qwen/Qwen3-4B", Qwen3TITOTokenizer, TITOTokenizerType.QWEN3), + "glm47": ("zai-org/GLM-4.7-Flash", GLM47TITOTokenizer, TITOTokenizerType.GLM47), } @@ -113,18 +115,24 @@ def _get_tokenizer(model_id: str) -> AutoTokenizer: @pytest.fixture(params=list(_TITO_MODELS.keys())) def tito(request) -> TITOTokenizer: - model_id, cls = _TITO_MODELS[request.param] - return cls(_get_tokenizer(model_id), allowed_append_roles=_ALLOWED_APPEND_ROLES) + model_id, cls, tito_type = _TITO_MODELS[request.param] + return cls(_get_tokenizer(model_id, tito_type), allowed_append_roles=_ALLOWED_APPEND_ROLES) @pytest.fixture def qwen3_tito() -> Qwen3TITOTokenizer: - return Qwen3TITOTokenizer(_get_tokenizer("Qwen/Qwen3-4B"), allowed_append_roles=_ALLOWED_APPEND_ROLES) + return Qwen3TITOTokenizer( + _get_tokenizer("Qwen/Qwen3-4B", TITOTokenizerType.QWEN3), + allowed_append_roles=_ALLOWED_APPEND_ROLES, + ) @pytest.fixture def glm47_tito() -> GLM47TITOTokenizer: - return GLM47TITOTokenizer(_get_tokenizer("zai-org/GLM-4.7-Flash"), allowed_append_roles=_ALLOWED_APPEND_ROLES) + return GLM47TITOTokenizer( + _get_tokenizer("zai-org/GLM-4.7-Flash", TITOTokenizerType.GLM47), + allowed_append_roles=_ALLOWED_APPEND_ROLES, + ) @pytest.fixture @@ -457,6 +465,8 @@ class TestFactory: "type_str, model_id, cls", [ ("qwen3", "Qwen/Qwen3-4B", Qwen3TITOTokenizer), + ("qwen35", "Qwen/Qwen3-4B", Qwen35TITOTokenizer), + ("qwennext", "Qwen/Qwen3-4B", QwenNextTITOTokenizer), ("glm47", "zai-org/GLM-4.7-Flash", GLM47TITOTokenizer), ("default", "Qwen/Qwen3-4B", TITOTokenizer), ], @@ -470,6 +480,18 @@ def test_enum_input(self): tito = get_tito_tokenizer(_get_tokenizer("Qwen/Qwen3-4B"), tokenizer_type=TITOTokenizerType.QWEN3) assert isinstance(tito, Qwen3TITOTokenizer) + @pytest.mark.parametrize( + "type_str, cls", + [("qwen35", Qwen35TITOTokenizer), ("qwennext", QwenNextTITOTokenizer)], + ) + def test_qwen_variant_inherits_qwen3_boundary_logic(self, type_str, cls): + """Qwen3.5 / Qwen3-Next reuse Qwen3's boundary handling via inheritance. + The named subclass exists so fixed_templates can key on (tito_model, + surface) — but token-level merge behavior is identical to Qwen3.""" + tito = get_tito_tokenizer(_get_tokenizer("Qwen/Qwen3-4B"), tokenizer_type=type_str) + assert isinstance(tito, cls) + assert isinstance(tito, Qwen3TITOTokenizer) + def test_invalid_type_raises(self): with pytest.raises(ValueError): get_tito_tokenizer(_get_tokenizer("Qwen/Qwen3-4B"), tokenizer_type="nonexistent") @@ -477,3 +499,84 @@ def test_invalid_type_raises(self): def test_none_tokenizer_raises(self): with pytest.raises(ValueError, match="must not be None"): get_tito_tokenizer(None) + + +class TestParserBinding: + """Each TITO subclass binds sglang ``--reasoning-parser`` and + ``--tool-call-parser`` values; ``resolve_reasoning_and_tool_call_parser`` + enforces user-supplied values agree with the bindings (or returns the + bound values when the user didn't pass one). The two parsers are + resolved independently — a missing binding on one doesn't suppress the + assert on the other.""" + + @pytest.mark.parametrize( + "tito_model, expected_reasoning, expected_tool_call", + [ + (TITOTokenizerType.QWEN3, "qwen3", "qwen25"), + (TITOTokenizerType.QWEN35, "qwen3", "qwen3_coder"), + (TITOTokenizerType.QWENNEXT, "qwen3", "qwen25"), + (TITOTokenizerType.GLM47, "glm45", "glm47"), + (TITOTokenizerType.DEFAULT, None, None), + ], + ) + def test_subclass_binding(self, tito_model, expected_reasoning, expected_tool_call): + from miles.utils.chat_template_utils.tito_tokenizer import _TOKENIZER_REGISTRY + + cls = _TOKENIZER_REGISTRY[tito_model] + assert cls.reasoning_parser == expected_reasoning + assert cls.tool_call_parser == expected_tool_call + + def test_resolve_returns_binding_when_user_omits(self): + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.QWEN3) == ("qwen3", "qwen25") + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.QWEN35) == ("qwen3", "qwen3_coder") + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.GLM47) == ("glm45", "glm47") + # DEFAULT family has no binding for either parser; both come back None. + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.DEFAULT) == (None, None) + + def test_resolve_accepts_matching_user_value(self): + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + assert resolve_reasoning_and_tool_call_parser("qwen3", "qwen3", "qwen25") == ("qwen3", "qwen25") + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.QWEN35, "qwen3", "qwen3_coder") == ( + "qwen3", + "qwen3_coder", + ) + + def test_resolve_raises_on_reasoning_mismatch(self): + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + with pytest.raises(ValueError, match="--reasoning-parser='glm45' disagrees"): + resolve_reasoning_and_tool_call_parser(TITOTokenizerType.QWEN3, user_reasoning_parser="glm45") + + def test_resolve_raises_on_tool_call_mismatch(self): + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + with pytest.raises(ValueError, match="--tool-call-parser='glm47' disagrees"): + resolve_reasoning_and_tool_call_parser(TITOTokenizerType.QWEN3, user_tool_call_parser="glm47") + + def test_resolve_accepts_user_value_when_family_unbound(self): + # DEFAULT family has no binding for either parser; user-provided wins + # (for families that haven't been wired up to a sglang parser yet). + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + assert resolve_reasoning_and_tool_call_parser( + TITOTokenizerType.DEFAULT, "custom_reasoning", "custom_tool_call" + ) == ("custom_reasoning", "custom_tool_call") + + def test_resolve_partial_user_input(self): + # User can pass only one of the two; the other auto-resolves from + # the family binding independently. + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + # User passes reasoning only — tool_call comes from binding. + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.QWEN3, user_reasoning_parser="qwen3") == ( + "qwen3", + "qwen25", + ) + # User passes tool_call only — reasoning comes from binding. + assert resolve_reasoning_and_tool_call_parser(TITOTokenizerType.GLM47, user_tool_call_parser="glm47") == ( + "glm45", + "glm47", + ) diff --git a/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py b/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py index 69cbc4eca1a..82fa298ba24 100644 --- a/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py +++ b/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py @@ -10,7 +10,7 @@ import pytest from transformers import AutoTokenizer -from miles.utils.chat_template_utils import MismatchType, apply_chat_template, try_get_fixed_chat_template +from miles.utils.chat_template_utils import MismatchType, apply_chat_template, resolve_fixed_chat_template from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer, TITOTokenizerType, get_tito_tokenizer from miles.utils.processing_utils import load_tokenizer from miles.utils.test_utils.mock_trajectories import ( @@ -157,7 +157,13 @@ def _resolve_tito_type(model_name: str) -> TITOTokenizerType: def _get_tokenizer(model_name: str) -> AutoTokenizer: - chat_template_path = try_get_fixed_chat_template(model_name) + # Mirror arguments.py: DEFAULT means "use HF native, no override", so we + # bypass the resolver. Only fixed-template families go through resolve. + tito_type = _resolve_tito_type(model_name) + if tito_type == TITOTokenizerType.DEFAULT: + chat_template_path = None + else: + chat_template_path, _kwargs = resolve_fixed_chat_template(tito_type, ["tool"]) cache_key = (model_name, chat_template_path) if cache_key not in _TOK_CACHE: _TOK_CACHE[cache_key] = load_tokenizer( From 389ada4c61c8ad3f153eadda25a76d599e8dbc57 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Sun, 3 May 2026 15:36:35 -0700 Subject: [PATCH 0008/1130] [TITO] feat (2/N): TITO tokenizer e2e verification. (#1049) Co-authored-by: Yuzhen Zhou --- miles/utils/arguments.py | 12 + .../utils/test_utils/chat_template_verify.py | 2 + miles/utils/test_utils/mock_trajectories.py | 117 +++++- .../utils/test_utils/session_verify_agent.py | 358 ++++++++++++++++++ .../utils/test_utils/session_verify_runner.py | 272 +++++++++++++ .../tools/verify_session_tito_tokenizer.py | 190 ++++++++++ .../sglang/test_session_server_multi_role.py | 94 +++++ .../sglang/test_session_server_tool_call.py | 158 -------- tests/fast/fixtures/generation_fixtures.py | 2 +- .../rollout/generate_hub/test_multi_turn.py | 22 +- .../chat_template_utils/test_template.py | 24 +- .../test_tito_tokenizer_model_matrix.py | 238 ------------ 12 files changed, 1068 insertions(+), 421 deletions(-) create mode 100644 miles/utils/test_utils/session_verify_agent.py create mode 100644 miles/utils/test_utils/session_verify_runner.py create mode 100755 scripts/tools/verify_session_tito_tokenizer.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role.py delete mode 100644 tests/e2e/sglang/test_session_server_tool_call.py delete mode 100644 tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 141a5abc45d..695d466ffd1 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1849,6 +1849,18 @@ def miles_validate_args(args): raw_roles = getattr(args, "tito_allowed_append_roles", ["tool"]) args.tito_allowed_append_roles = sorted(set(r.lower() for r in raw_roles)) + if not args.use_session_server: + misconfigured = [] + if args.tito_model != TITOTokenizerType.DEFAULT.value: + misconfigured.append(f"--tito-model={args.tito_model}") + if args.tito_allowed_append_roles != ["tool"]: + misconfigured.append(f"--tito-allowed-append-roles={args.tito_allowed_append_roles}") + if misconfigured: + raise ValueError( + f"{', '.join(misconfigured)} require --use-session-server; " + "these flags only configure the session-server TITO middleware." + ) + if "user" in args.tito_allowed_append_roles: logger.warning( "--tito-allowed-append-roles includes 'user'. " diff --git a/miles/utils/test_utils/chat_template_verify.py b/miles/utils/test_utils/chat_template_verify.py index 9a07a1f913b..f0630fb0a81 100644 --- a/miles/utils/test_utils/chat_template_verify.py +++ b/miles/utils/test_utils/chat_template_verify.py @@ -152,6 +152,7 @@ def verify_append_only( IntermediateSystemTrajectory, LongChainThinkingTrajectory, LongChainTrajectory, + MultiRoleSequenceTrajectory, MultiToolSingleTurnTrajectory, MultiTurnNoToolThinkingTrajectory, MultiTurnNoToolTrajectory, @@ -193,6 +194,7 @@ def _short_name(cls: type) -> str: MultiUserTurnThinkingTrajectory, IntermediateSystemThinkingTrajectory, MultiTurnNoToolThinkingTrajectory, + MultiRoleSequenceTrajectory, ] diff --git a/miles/utils/test_utils/mock_trajectories.py b/miles/utils/test_utils/mock_trajectories.py index d00a654a6e4..c81202ea28a 100644 --- a/miles/utils/test_utils/mock_trajectories.py +++ b/miles/utils/test_utils/mock_trajectories.py @@ -509,16 +509,24 @@ class MultiUserToolChainTrajectory: class SimpleNoToolTrajectory: - """sys, user, assistant (no tools) — 1 turn, no tool calls""" + """sys, user, asst, system_reminder, user2, asst2 — no tools, with system/user append boundaries. + + Codifies the synthetic 'single_system' case the old CI used to manually + append: at N=3 prefix ends at the first asst, append starts with the + system_reminder, exercising the system-append boundary on a no-tool model. + """ TOOLS = None - PRETOKENIZE_POSITIONS = [3] - APPEND_ROLES = frozenset() + PRETOKENIZE_POSITIONS = [3, 4, 5, 6] + APPEND_ROLES = frozenset({"user", "system"}) IS_THINKING = False MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}, {"role": "assistant", "content": "Hi there! How can I help?"}, + {"role": "system", "content": "Please answer in one short sentence."}, + {"role": "user", "content": "What's 2+2?"}, + {"role": "assistant", "content": "Four."}, ] @@ -562,11 +570,16 @@ class MultiTurnNoToolThinkingTrajectory: class SingleToolThinkingTrajectory: - """sys, user, assistant(reasoning_content + tool_calls), tool — 1 turn""" + """sys, user, ass(think+tool), tool, user2, ass(think+tool), tool, user3, ass — multi-role alternating with thinking. + + Codifies the synthetic 'alternating_user_tool' case the old CI used to + manually append: prefix cuts at N=4/N=7 exercise the user-after-tool + boundary on a thinking model. + """ TOOLS = WEATHER_TOOLS - PRETOKENIZE_POSITIONS = [3] - APPEND_ROLES = frozenset({"tool"}) + PRETOKENIZE_POSITIONS = [3, 4, 5, 6, 7, 8] + APPEND_ROLES = frozenset({"tool", "user"}) IS_THINKING = True MESSAGES = [ {"role": "system", "content": "You are a helpful assistant."}, @@ -591,6 +604,33 @@ class SingleToolThinkingTrajectory: "content": '{"temperature": 25, "condition": "sunny"}', "tool_call_id": "call_1", }, + {"role": "user", "content": "Now check Shanghai too."}, + { + "role": "assistant", + "reasoning_content": "Now the user wants Shanghai's weather. Calling get_weather again.", + "content": None, + "tool_calls": [ + { + "id": "call_2", + "type": "function", + "function": { + "name": "get_weather", + "arguments": {"city": "Shanghai", "unit": "celsius"}, + }, + } + ], + }, + { + "role": "tool", + "content": '{"temperature": 30, "condition": "cloudy"}', + "tool_call_id": "call_2", + }, + {"role": "user", "content": "And tell me the date as well."}, + { + "role": "assistant", + "reasoning_content": "The user is asking for the date. I'll answer based on what I know.", + "content": "Beijing is 25°C and sunny; Shanghai is 30°C and cloudy. I don't have access to the current date.", + }, ] @@ -938,6 +978,71 @@ class IntermediateSystemThinkingTrajectory: ] +class MultiRoleSequenceTrajectory: + """sys, user, asst+tool, tool, user2, asst+tool, system_reminder, tool, asst-final. + + Fills the {thinking=False, append_roles={tool, user, system}} matrix cell + that GLM47's tool+user+system SUPPORTED_TEMPLATES row otherwise has no + fixture for. Cuts exercise three boundaries: tool-append (N=3), user-after-tool + (N=4), system-after-asst (N=6). Cuts at N=5/N=7/N=8 are intentionally not + listed — they only exercise generation-prompt-only or repeat tool-append + which other trajectories already cover. + """ + + TOOLS = ALL_TOOLS + PRETOKENIZE_POSITIONS = [3, 4, 6] + APPEND_ROLES = frozenset({"tool", "user", "system"}) + IS_THINKING = False + MESSAGES = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What's the weather in Beijing today?"}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_w1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": {"city": "Beijing", "unit": "celsius"}, + }, + } + ], + }, + { + "role": "tool", + "content": '{"temperature": 22, "condition": "sunny"}', + "tool_call_id": "call_w1", + }, + {"role": "user", "content": "Also tell me the date in Beijing."}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_d1", + "type": "function", + "function": { + "name": "get_date", + "arguments": {"timezone": "Asia/Shanghai"}, + }, + } + ], + }, + {"role": "system", "content": "Please answer in one short sentence."}, + { + "role": "tool", + "content": '{"date": "2026-04-28"}', + "tool_call_id": "call_d1", + }, + { + "role": "assistant", + "content": "Beijing is 22°C and sunny on 2026-04-28.", + }, + ] + + # --------------------------------------------------------------------------- # Helpers: build Trajectory and process_fn from a trajectory class # --------------------------------------------------------------------------- diff --git a/miles/utils/test_utils/session_verify_agent.py b/miles/utils/test_utils/session_verify_agent.py new file mode 100644 index 00000000000..f01696f731c --- /dev/null +++ b/miles/utils/test_utils/session_verify_agent.py @@ -0,0 +1,358 @@ +"""Custom-generate / custom-agent driver for TITO session-server verification. + +Wired through ``--custom-generate-function-path`` / +``--custom-agent-function-path``; consumed by +``tests/e2e/sglang/test_session_server_multi_role.py`` and +``scripts/tools/verify_session_tito_tokenizer.py``. +""" + +from __future__ import annotations + +import json +import logging +import os +from enum import Enum + +import httpx + +from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput +from miles.rollout.generate_hub.agentic_tool_call import generate as _base_generate + +logger = logging.getLogger(__name__) + + +class DriverAction(Enum): + TOOL_RESULT = "tool_result" + USER_FOLLOWUP = "user_followup" + SYSTEM_REMINDER = "system_reminder" + ROLLBACK = "rollback" + FORCE_FINAL = "force_final" + + +_T = DriverAction.TOOL_RESULT +_U = DriverAction.USER_FOLLOWUP +_S = DriverAction.SYSTEM_REMINDER +_R = DriverAction.ROLLBACK +_F = DriverAction.FORCE_FINAL + +# Override per call: ``--session-verify-cycles N`` (CLI) or ``cycles=N`` +# (pytest via ``run_session_verify``). Smaller-context models with a 4K +# response budget should drop to 2 to avoid context overflow. +DEFAULT_CYCLES = 3 + +_SUPPORTED_ROLE_SURFACES: tuple[frozenset[str], ...] = ( + frozenset({"tool"}), + frozenset({"tool", "user"}), + frozenset({"tool", "user", "system"}), +) + + +def _build_cycle(role_surface: frozenset[str]) -> list[DriverAction]: + cycle: list[DriverAction] = [_T] + if "user" in role_surface: + cycle.append(_U) + cycle.append(_T) + if "system" in role_surface: + cycle.append(_S) + cycle.append(_R) + return cycle + + +# English-only on purpose: matches the production agentic flows tokenization +# and tool-call parsing are tuned against. +USER_FOLLOWUP_TEXT = "Now check the weather in Shanghai." +SYSTEM_REMINDER_TEXT = "Note: from now on, answer in a single sentence; skip all pleasantries." +FORCE_FINAL_TEXT = "Please summarize all results inside ... tags." + +TOOLS = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather for a given city.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city name, e.g. Beijing", + }, + }, + "required": ["location"], + }, + }, + }, +] + +MOCK_TOOL_RESULTS = [ + '{"temperature_celsius": 22, "condition": "sunny"}', + '{"temperature_celsius": 15, "condition": "cloudy"}', + '{"temperature_celsius": 30, "condition": "rainy"}', + '{"temperature_celsius": 8, "condition": "snowy"}', +] + + +INITIAL_SYSTEM_PROMPT = ( + "You are a weather assistant. Use the get_weather tool when the user asks " + "about a city's weather. Answer one question at a time and wait for the " + "next user message; do not summarize until the user explicitly asks you " + "to. When asked to summarize, wrap the final summary in " + "... tags." +) +INITIAL_USER_PROMPT = "What's the weather in Beijing?" + + +def select_schedule(allowed_roles, *, cycles: int = DEFAULT_CYCLES) -> list[DriverAction]: + """Pick the schedule for ``frozenset(allowed_roles)``; raises on unregistered.""" + key = frozenset(allowed_roles) + if key not in _SUPPORTED_ROLE_SURFACES: + registered = sorted(sorted(k) for k in _SUPPORTED_ROLE_SURFACES) + raise ValueError(f"No schedule registered for allowed_roles={sorted(key)}. Registered: {registered}") + if cycles < 1: + raise ValueError(f"cycles must be >= 1, got {cycles}") + cycle = _build_cycle(key) + # Extra R after the first cycle exercises consecutive-rollback adjacency, + # which cycle-repeat alone never produces. + schedule = list(cycle) + [_R] + cycle * (cycles - 1) + if "user" in key: + schedule.append(_F) + return schedule + + +def build_initial_messages() -> list[dict]: + """The fixed (system, user) prompt all schedules start from.""" + return [ + {"role": "system", "content": INITIAL_SYSTEM_PROMPT}, + {"role": "user", "content": INITIAL_USER_PROMPT}, + ] + + +async def _chat(client, base_url, messages, request_kwargs, *, label): + payload = {"messages": messages, "tools": TOOLS, **request_kwargs} + resp = await client.post(f"{base_url}/v1/chat/completions", json=payload) + assert resp.status_code == 200, f"{label} failed ({resp.status_code}): {resp.text}" + return resp.json() + + +async def run_agent(base_url, prompt, request_kwargs, metadata, **kwargs): + """Custom-agent entry point. Returns ``{"driver_events": [...], **counters}``. + + ``allowed_append_roles`` must be present in ``metadata`` (the ``generate`` + wrapper below injects it from ``args.tito_allowed_append_roles``). + ``prompt`` is ignored — the driver synthesizes its own initial conversation + from ``build_initial_messages`` so runs are reproducible. + """ + allowed_roles = metadata.get("allowed_append_roles") + if allowed_roles is None: + raise ValueError( + "session_verify_agent.run_agent requires allowed_append_roles in metadata; " + "the generate wrapper should inject it from args.tito_allowed_append_roles." + ) + cycles = metadata.get("session_verify_cycles", DEFAULT_CYCLES) + schedule = select_schedule(allowed_roles, cycles=cycles) + + rk = {k: v for k, v in request_kwargs.items() if k not in ("tools",)} + messages = build_initial_messages() + events: list[str] = [] + counters = { + "rollback_count": 0, + "user_count": 0, + "system_count": 0, + "tool_result_count": 0, + "tool_call_count": 0, + } + + async with httpx.AsyncClient(timeout=180) as client: + # Initial completion — no driver action yet. + resp = await _chat(client, base_url, messages, rk, label="Initial") + assistant = resp["choices"][0]["message"] + messages.append(assistant) + events.append("initial") + counters["tool_call_count"] += len(assistant.get("tool_calls") or []) + + for step_idx, action in enumerate(schedule): + label = f"Step {step_idx + 1} {action.value}" + + if action is DriverAction.TOOL_RESULT: + tool_calls = assistant.get("tool_calls") or [] + if tool_calls: + for i, tc in enumerate(tool_calls): + result_idx = (counters["tool_result_count"] + i) % len(MOCK_TOOL_RESULTS) + messages.append( + { + "role": "tool", + "content": MOCK_TOOL_RESULTS[result_idx], + "tool_call_id": tc["id"], + } + ) + counters["tool_result_count"] += len(tool_calls) + events.append("append_tool") + else: + # Model didn't tool-call — append a sentinel tool message + # so a ``{tool}``-only session is not rejected by the + # server's append-role check. Coverage still flags this + # via the missing ``append_tool`` event. + messages.append( + { + "role": "tool", + "tool_call_id": "none", + "content": ( + "Driver note: the previous assistant turn did not " + "emit a tool_call, so no tool was actually invoked." + ), + } + ) + events.append("tool_result_skipped_no_tool_call") + + elif action is DriverAction.USER_FOLLOWUP: + messages.append({"role": "user", "content": USER_FOLLOWUP_TEXT}) + counters["user_count"] += 1 + events.append("append_user") + + elif action is DriverAction.SYSTEM_REMINDER: + messages.append({"role": "system", "content": SYSTEM_REMINDER_TEXT}) + counters["system_count"] += 1 + events.append("append_system") + + elif action is DriverAction.ROLLBACK: + # Drop the last assistant; the next request's messages list + # is shorter than the session accumulator, so the server walks + # _detect_and_rollback before re-inferencing the assistant. + if not messages or messages[-1]["role"] != "assistant": + raise AssertionError( + f"Cannot rollback at step {step_idx}: tail role is " + f"{messages[-1]['role'] if messages else 'EMPTY'}, expected assistant" + ) + messages.pop() + counters["rollback_count"] += 1 + events.append("rollback") + + elif action is DriverAction.FORCE_FINAL: + messages.append({"role": "user", "content": FORCE_FINAL_TEXT}) + events.append("force_final") + + else: + raise AssertionError(f"Unknown DriverAction {action!r}") + + resp = await _chat(client, base_url, messages, rk, label=label) + assistant = resp["choices"][0]["message"] + messages.append(assistant) + counters["tool_call_count"] += len(assistant.get("tool_calls") or []) + + logger.info("Agent done: events=%s counters=%s", events, counters) + + return {"driver_events": events, **counters} + + +async def generate(input: GenerateFnInput) -> GenerateFnOutput: + """Custom-generate wrapper that asserts driver-action coverage. + + - Per-sample: every sample must contain ``rollback``, plus ``append_user`` + / ``append_system`` when those roles are allowed. + - Cross-sample: at least one sample must contain ``append_tool`` + (model-dependent on emitting a tool_call). + """ + allowed_roles = list(input.args.tito_allowed_append_roles) + cycles = getattr(input.args, "session_verify_cycles", DEFAULT_CYCLES) + # Sample.metadata is mutable even when the outer dataclass is frozen. + input.sample.metadata["allowed_append_roles"] = allowed_roles + input.sample.metadata["session_verify_cycles"] = cycles + + output = await _base_generate(input) + + samples = output.samples if isinstance(output.samples, list) else [output.samples] + events_per_sample = [s.metadata.get("driver_events", []) for s in samples] + + required_per_sample = ["rollback"] + if "user" in allowed_roles: + required_per_sample.append("append_user") + if "system" in allowed_roles: + required_per_sample.append("append_system") + + for i, events in enumerate(events_per_sample): + missing = [req for req in required_per_sample if req not in events] + if missing: + raise AssertionError( + f"Session multi-role e2e: sample {i} missing required driver events " + f"{missing}. allowed_roles={allowed_roles}, events={events}" + ) + + if not any("append_tool" in events for events in events_per_sample): + raise AssertionError( + "Session multi-role e2e: no sample produced an append_tool action — " + f"the model may not be tool-calling. events_per_sample={events_per_sample}" + ) + + # Token-seq comparator metrics (server-computed in sessions.py:83). Any of + # special_token_count / special_token_type / non_assistant_text mismatches + # is a hard TITO bug and must be 0 per-sample; assistant_text mismatches + # are softer (assistant tokens inherited from the pretokenized prefix may + # not match the chat template's canonical tokenization) and are aggregated + # across samples via a metrics file. + forbidden_types = {"special_token_count", "special_token_type", "non_assistant_text"} + metrics_path = os.environ.get("MILES_SESSION_VERIFY_METRICS_PATH") + for i, sample in enumerate(samples): + mismatches = sample.metadata.get("tito_session_mismatch") + if mismatches is None: + raise AssertionError( + f"Session multi-role e2e: sample {i} has no tito_session_mismatch " + f"in metadata. The session-server's compute_session_mismatch raised " + f"TokenizationError (sessions.py:83 swallows it) — this always " + f"indicates a TITO subclass / setup bug, not a real PASS." + ) + forbidden = [m for m in mismatches if m.get("type") in forbidden_types] + if forbidden: + raise AssertionError( + f"Session multi-role e2e: sample {i} has forbidden mismatches " + f"{forbidden}. allowed_roles={allowed_roles}. These types must be 0 " + f"for any TITO-correct setup." + ) + if metrics_path: + assistant_mismatches = [m for m in mismatches if m.get("type") == "assistant_text"] + had_assistant_mismatch = bool(assistant_mismatches) + example = None + if assistant_mismatches: + first = assistant_mismatches[0] + example = { + "segment_index": first.get("segment_index"), + "expected_text": (first.get("expected_text") or "")[:300], + "actual_text": (first.get("actual_text") or "")[:300], + } + with open(metrics_path, "a") as f: + f.write( + json.dumps( + { + "sample_index": i, + "had_assistant_mismatch": had_assistant_mismatch, + "total_mismatches": len(mismatches), + "assistant_mismatch_count": len(assistant_mismatches), + "assistant_mismatch_example": example, + } + ) + + "\n" + ) + + logger.info( + "Multi-role coverage verified: per_sample=%s, samples=%d, events=%s", + required_per_sample, + len(samples), + events_per_sample, + ) + return output + + +def _add_arguments(parser): + _base_generate.add_arguments(parser) + parser.add_argument( + "--session-verify-cycles", + type=int, + default=DEFAULT_CYCLES, + help="Number of driver schedule cycles per sample for session-server " + "TITO verification. Each cycle exercises every action in the role " + "surface plus a rollback; more cycles stress the TITO accumulator " + "longer but expand context length. Drop to 2 on tighter-context " + "models (e.g. Qwen3 32K with 4K response budget).", + ) + + +generate.add_arguments = _add_arguments diff --git a/miles/utils/test_utils/session_verify_runner.py b/miles/utils/test_utils/session_verify_runner.py new file mode 100644 index 00000000000..2e7fdad01dc --- /dev/null +++ b/miles/utils/test_utils/session_verify_runner.py @@ -0,0 +1,272 @@ +"""Boot a real ``miles`` rollout pipeline + run the multi-role TITO driver. + +Used by both consumers: + +- pytest e2e: ``tests/e2e/sglang/test_session_server_multi_role.py`` +- CLI: ``scripts/tools/verify_session_tito_tokenizer.py`` + +Both forms run the same ``execute_train(--debug-rollout-only)`` path: full miles +pipeline (sglang + miles-router with session support) is launched, ``train`` is +skipped, and the rollout drives ``session_verify_agent.run_agent`` against the +session server. + +# Backend choice + +``execute_train`` asserts ``("--train-backend fsdp" in train_args) == (megatron_model_type is None)``, +so the ``fsdp`` + ``None`` pair is the only consistent way to skip megatron init +in ``--debug-rollout-only`` mode. We use that. +""" + +from __future__ import annotations + +import json +import logging +import os +import tempfile + +logger = logging.getLogger(__name__) + +# Soft cap on how many samples may report any assistant_text mismatch. Hard +# mismatch types (special_token_count / special_token_type / non_assistant_text) +# are asserted per-sample inside the agent wrapper — those must be 0. +ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD = 0.05 + +PROMPT_DATA_PATH = "/root/datasets/session_multi_role_verify.jsonl" +LOCAL_MODELS_ROOT = "/root/models" + +# The driver agent synthesizes its own initial conversation, but the rollout +# pipeline still needs a non-empty prompt-data file as input. This placeholder +# matches the agent's own initial prompt so the prompt is well-formed even if +# something downstream inspects it. +_PLACEHOLDER_PROMPT_RECORD = { + "messages": [ + {"role": "system", "content": "You are a weather assistant."}, + {"role": "user", "content": "What's the weather in Beijing?"}, + ], +} + + +def _ensure_prompt_data() -> str: + os.makedirs(os.path.dirname(PROMPT_DATA_PATH), exist_ok=True) + with open(PROMPT_DATA_PATH, "w") as f: + f.write(json.dumps(_PLACEHOLDER_PROMPT_RECORD) + "\n") + return PROMPT_DATA_PATH + + +def _ensure_model_downloaded(hf_checkpoint: str) -> str: + """Download the model to ``/root/models/`` if not already there.""" + import miles.utils.external_utils.command_utils as U + + short = hf_checkpoint.split("/")[-1] + local_dir = os.path.join(LOCAL_MODELS_ROOT, short) + os.makedirs(LOCAL_MODELS_ROOT, exist_ok=True) + U.exec_command(f"hf download {hf_checkpoint} --local-dir {local_dir}") + return local_dir + + +def _clear_proxy_env() -> None: + for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): + os.environ.pop(proxy_var, None) + + +def build_train_args( + *, + local_model_dir: str, + tito_model: str, + allowed_append_roles, + tp_size: int, + num_gpus: int = 8, + reasoning_parser: str, + tool_call_parser: str | None, + n_samples_per_prompt: int = 4, + cycles: int = 3, +) -> str: + """Compose the ``train_args`` string for ``execute_train``. + + Caller-supplied ``allowed_append_roles`` is forwarded verbatim to + ``--tito-allowed-append-roles``; the agent's ``run_agent`` will read it back + from ``args.tito_allowed_append_roles`` to pick a schedule. + + Rollout batching: ``--rollout-batch-size 16`` × ``--n-samples-per-prompt`` × + ``--num-rollout 1`` = ``--global-batch-size 64``. The single placeholder + prompt is cycled inside the batch — that's the path miles' rollout layer + actually parallelizes on, so leave it at 16 even though the prompt-data + file is single-record. Setting batch-size=1 with a multi-sample n triggers + unexpected agent re-invocation in the rollout loop. + """ + allowed_roles_arg = " ".join(allowed_append_roles) + + ckpt_args = f"--hf-checkpoint {local_model_dir} " + + rollout_args = ( + f"--prompt-data {PROMPT_DATA_PATH} " + "--input-key messages " + "--num-rollout 1 " + "--rollout-batch-size 16 " + f"--n-samples-per-prompt {n_samples_per_prompt} " + "--rollout-max-response-len 4096 " + "--rollout-temperature 0.7 " + "--global-batch-size 64 " + ) + + generate_args = ( + "--custom-generate-function-path " + "miles.utils.test_utils.session_verify_agent.generate " + "--custom-agent-function-path " + "miles.utils.test_utils.session_verify_agent.run_agent " + f"--session-verify-cycles {cycles} " + ) + + router_args = ( + "--use-miles-router " + "--use-session-server " + f"--tito-model {tito_model} " + f"--tito-allowed-append-roles {allowed_roles_arg} " + ) + + sglang_args = f"--rollout-num-gpus-per-engine {tp_size} " f"--sglang-reasoning-parser {reasoning_parser} " + if tool_call_parser: + sglang_args += f"--sglang-tool-call-parser {tool_call_parser} " + sglang_args += "--rm-type random " + + infra_args = ( + "--debug-rollout-only " + "--ci-test " + "--actor-num-nodes 1 " + # ``num_gpus`` controls the actor's full-node allocation (default 8 = + # whole node). The sglang engine's tensor-parallel slice is the + # separate ``tp_size`` parameter — a small model can run TP=1/2/4 + # while the test still occupies the whole node, keeping the CI lane + # allocation stable regardless of the engine TP. + f"--actor-num-gpus-per-node {num_gpus} " + "--colocate " + "--train-backend fsdp " + ) + + return ckpt_args + rollout_args + generate_args + router_args + sglang_args + infra_args + + +def run_session_verify( + *, + hf_checkpoint: str, + tito_model: str, + allowed_append_roles, + reasoning_parser: str | None = None, + tool_call_parser: str | None = None, + tp_size: int = 1, + num_gpus: int = 8, + n_samples_per_prompt: int = 4, + cycles: int = 3, +) -> None: + """Boot ``miles`` rollout pipeline and run the multi-role driver. + + Returns nothing on success; raises ``AssertionError`` on TITO mismatch + (HTTP 500 from server-side prefix check) or coverage shortfall (raised by + ``session_verify_agent.generate``). + + Both parsers are resolved against the TITO subclass's bound values via + ``resolve_reasoning_and_tool_call_parser`` — caller-passed values that + disagree with the bound values raise ``ValueError`` here, before any GPU + work starts. Pass ``None`` for either to auto-resolve from the TITO + subclass. + """ + import miles.utils.external_utils.command_utils as U + from miles.utils.chat_template_utils import resolve_reasoning_and_tool_call_parser + + reasoning_parser, tool_call_parser = resolve_reasoning_and_tool_call_parser( + tito_model, reasoning_parser, tool_call_parser + ) + + _ensure_prompt_data() + _clear_proxy_env() + local_model_dir = _ensure_model_downloaded(hf_checkpoint) + + train_args = build_train_args( + local_model_dir=local_model_dir, + tito_model=tito_model, + allowed_append_roles=allowed_append_roles, + tp_size=tp_size, + num_gpus=num_gpus, + reasoning_parser=reasoning_parser, + tool_call_parser=tool_call_parser, + n_samples_per_prompt=n_samples_per_prompt, + cycles=cycles, + ) + + # Per-sample token-seq metrics file: rollout workers append one JSONL line + # per sample inside session_verify_agent.generate; we aggregate after + # execute_train returns to apply the assistant_text soft threshold. + metrics_fd, metrics_path = tempfile.mkstemp(prefix="session_verify_metrics_", suffix=".jsonl") + os.close(metrics_fd) + + preserved_metrics_path = None + try: + U.execute_train( + train_args=train_args, + num_gpus_per_node=num_gpus, + megatron_model_type=None, + extra_env_vars={ + "MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1", + "SGLANG_E2E_MODEL_PATH": local_model_dir, + "MILES_TITO_MODEL": tito_model, + "MILES_SESSION_VERIFY_METRICS_PATH": metrics_path, + }, + ) + try: + _assert_assistant_text_mismatch_ratio(metrics_path) + except AssertionError: + import shutil + + preserved_metrics_path = metrics_path + ".failed" + shutil.copy(metrics_path, preserved_metrics_path) + logger.error("Preserved per-sample mismatch payloads at %s for post-mortem", preserved_metrics_path) + raise + finally: + try: + os.unlink(metrics_path) + except OSError: + pass + + +def _assert_assistant_text_mismatch_ratio(metrics_path: str) -> None: + """Read the per-sample JSONL metrics and assert the soft threshold. + + Forbidden mismatch types (special_*, non_assistant_text) are caught + per-sample in the agent wrapper and would have already raised by now. + Here we only cross-check the soft assistant_text rate. + """ + samples_with_mismatch = 0 + total_samples = 0 + with open(metrics_path) as f: + for line in f: + line = line.strip() + if not line: + continue + entry = json.loads(line) + total_samples += 1 + if entry.get("had_assistant_mismatch"): + samples_with_mismatch += 1 + + if total_samples == 0: + raise AssertionError( + f"Session multi-role e2e: no per-sample metrics found at {metrics_path}. " + "Either the rollout produced 0 samples, or the agent wrapper failed to " + "run before any sample completed. Check rollout logs." + ) + + ratio = samples_with_mismatch / total_samples + logger.info( + "Token-seq metric summary: samples=%d, with_assistant_text_mismatch=%d, ratio=%.3f", + total_samples, + samples_with_mismatch, + ratio, + ) + if ratio > ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD: + raise AssertionError( + f"Session multi-role e2e: assistant_text mismatch ratio " + f"{samples_with_mismatch}/{total_samples}={ratio:.3f} exceeds " + f"threshold {ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD}. TITO " + "tokenization for assistant content has drifted from the chat " + "template's canonical render — investigate via " + "verify_session_tito_tokenizer.py + sample-level mismatch logs." + ) diff --git a/scripts/tools/verify_session_tito_tokenizer.py b/scripts/tools/verify_session_tito_tokenizer.py new file mode 100755 index 00000000000..b8a329443a7 --- /dev/null +++ b/scripts/tools/verify_session_tito_tokenizer.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""CLI: run the multi-role TITO session-server verifier against a real model. + +Boots the miles rollout pipeline (sglang + miles-router) under +``--debug-rollout-only`` and drives the schedule registered for the requested +``--tito-allowed-append-roles`` surface (see +``miles.utils.test_utils.session_verify_agent``). PASS iff every sample +completes without HTTP error from the server-side prefix check and the +custom-generate coverage assertion is satisfied. + +Usage examples:: + + # GLM-4.7-Flash with tool + user + system surface + python scripts/tools/verify_session_tito_tokenizer.py \\ + --hf-checkpoint zai-org/GLM-4.7-Flash \\ + --tito-model glm47 \\ + --tito-allowed-append-roles tool user system \\ + --reasoning-parser glm45 \\ + --tool-call-parser glm47 + + # Qwen3-4B with tool + user surface + python scripts/tools/verify_session_tito_tokenizer.py \\ + --hf-checkpoint Qwen/Qwen3-4B \\ + --tito-model qwen3 \\ + --tito-allowed-append-roles tool user \\ + --reasoning-parser qwen3 \\ + --tool-call-parser qwen25 +""" + +from __future__ import annotations + +import argparse +import logging +import sys + +from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizerType +from miles.utils.test_utils.session_verify_agent import select_schedule +from miles.utils.test_utils.session_verify_runner import run_session_verify + + +def _print_action_table(allowed_roles: list[str]) -> None: + schedule = select_schedule(allowed_roles) + print("Driver schedule (after initial completion):") + for i, action in enumerate(schedule, 1): + print(f" {i}. {action.value}") + print() + print("Required per-sample driver events (asserted in generate wrapper):") + print(" - rollback (deterministic; always required)") + if "user" in allowed_roles: + print(" - append_user (deterministic; required because 'user' in roles)") + if "system" in allowed_roles: + print(" - append_system (deterministic; required because 'system' in roles)") + print() + print("Required cross-sample driver events (asserted in generate wrapper):") + print(" - append_tool (model-dependent; >=1 sample must emit a tool_call)") + print() + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Verify a model's TITO tokenizer under multi-role session-server " + "driver against real model inference.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--hf-checkpoint", + required=True, + help="HuggingFace model ID, e.g. zai-org/GLM-4.7-Flash.", + ) + parser.add_argument( + "--tito-model", + required=True, + choices=[t.value for t in TITOTokenizerType], + help="TITO tokenizer family (e.g. qwen3, glm47).", + ) + parser.add_argument( + "--tito-allowed-append-roles", + nargs="+", + required=True, + choices=["tool", "user", "system"], + help=( + "Role surface to verify. Must match a registered schedule in " + "session_verify_agent._SUPPORTED_ROLE_SURFACES. 'tool' is implicitly " + "added if omitted." + ), + ) + parser.add_argument( + "--reasoning-parser", + required=True, + help="--sglang-reasoning-parser value (e.g. qwen3, glm45).", + ) + parser.add_argument( + "--tool-call-parser", + default=None, + help="--sglang-tool-call-parser value (e.g. qwen25, glm47). Optional.", + ) + parser.add_argument( + "--tp-size", + type=int, + default=1, + help="sglang engine tensor-parallel size " + "(``--rollout-num-gpus-per-engine``). Pick the smallest TP that fits " + "the model — small models can run TP=1 even on a full 8-GPU node.", + ) + parser.add_argument( + "--num-gpus", + type=int, + default=8, + help="Actor / ray-cluster allocation per node " + "(``--actor-num-gpus-per-node``). Defaults to 8 (full node) — leave " + "alone unless you know what you're doing; the engine TP is a separate " + "knob (``--tp-size``).", + ) + parser.add_argument( + "--n-samples", + type=int, + default=4, + help="--n-samples-per-prompt; total samples generated equals " + "rollout-batch-size (16, fixed) * n-samples-per-prompt. Coverage " + "assertion is per-sample for deterministic actions and cross-sample " + "for tool_call. Group-of-N is needed because miles' rollout loop " + "drops the whole group on any TRUNCATED sample, then refills — " + "small N with one TRUNCATED-prone sample cycles forever.", + ) + parser.add_argument( + "--cycles", + type=int, + default=3, + help="Driver schedule cycles per sample (default 3). Drop to 2 for " + "tighter-context models (e.g. Qwen3 32K with 4K response budget).", + ) + + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + + # Normalize role surface: lowercase, dedup, ensure 'tool' is in. Same convention + # as miles/utils/arguments.py:1828 so the CLI matches train pipeline behavior. + allowed_roles = sorted(set(r.lower() for r in args.tito_allowed_append_roles) | {"tool"}) + + print(f"Model: {args.hf_checkpoint}") + print(f"TITO model family: {args.tito_model}") + print(f"Allowed append roles: {allowed_roles}") + print(f"Reasoning parser: {args.reasoning_parser}") + print(f"Tool call parser: {args.tool_call_parser or '(none)'}") + print(f"Engine TP size: {args.tp_size}") + print(f"Actor GPUs per node: {args.num_gpus}") + print(f"Samples per prompt: {args.n_samples}") + print(f"Cycles per sample: {args.cycles}") + print() + + try: + select_schedule(allowed_roles) + except ValueError as e: + print(f"Verdict: FAIL -- {e}", file=sys.stderr) + return 1 + + _print_action_table(allowed_roles) + + try: + run_session_verify( + hf_checkpoint=args.hf_checkpoint, + tito_model=args.tito_model, + allowed_append_roles=allowed_roles, + reasoning_parser=args.reasoning_parser, + tool_call_parser=args.tool_call_parser, + tp_size=args.tp_size, + num_gpus=args.num_gpus, + n_samples_per_prompt=args.n_samples, + cycles=args.cycles, + ) + except Exception as e: + print() + print(f"Verdict: FAIL -- {type(e).__name__}: {e}", file=sys.stderr) + return 1 + + print() + print( + "Verdict: PASS -- TITO incremental tokenization matched standard re-tokenize " + "across all required driver actions." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/e2e/sglang/test_session_server_multi_role.py b/tests/e2e/sglang/test_session_server_multi_role.py new file mode 100644 index 00000000000..f7090d7cf19 --- /dev/null +++ b/tests/e2e/sglang/test_session_server_multi_role.py @@ -0,0 +1,94 @@ +"""E2E test: multi-role session-server TITO verification under real model inference. + +Thin wrapper around +``miles.utils.test_utils.session_verify_runner.run_session_verify`` (driver +and coverage assertions live in ``session_verify_agent``). Requires 8 GPUs. +""" + +from tests.ci.ci_register import register_cuda_ci + +register_cuda_ci(est_time=600, suite="stage-b-short-8-gpu", num_gpus=8) + + +import os +from dataclasses import dataclass + +from miles.utils.test_utils.session_verify_runner import run_session_verify + + +@dataclass(frozen=True) +class ModelConfig: + model_name: str + reasoning_parser: str + tool_call_parser: str | None + tito_model: str + allowed_append_roles: tuple[str, ...] + tp_size: int = 1 + cycles: int = 3 + + +MODEL_REGISTRY: dict[str, ModelConfig] = { + "glm47-multi-role": ModelConfig( + model_name="zai-org/GLM-4.7-Flash", + reasoning_parser="glm45", + tool_call_parser="glm47", + tito_model="glm47", + allowed_append_roles=("tool", "user", "system"), + tp_size=4, + ), + "qwen3-tool-user": ModelConfig( + model_name="Qwen/Qwen3-30B-A3B", + reasoning_parser="qwen3", + tool_call_parser="qwen25", + tito_model="qwen3", + allowed_append_roles=("tool", "user"), + tp_size=1, + cycles=2, + ), + "qwen35-tool-user": ModelConfig( + model_name="Qwen/Qwen3.5-35B-A3B", + reasoning_parser="qwen3", + tool_call_parser="qwen3_coder", + tito_model="qwen35", + allowed_append_roles=("tool", "user"), + tp_size=1, + cycles=2, + ), + "qwennext-tool-user": ModelConfig( + model_name="Qwen/Qwen3-Next-80B-A3B-Thinking", + reasoning_parser="qwen3", + tool_call_parser="qwen25", + tito_model="qwennext", + allowed_append_roles=("tool", "user"), + tp_size=2, + cycles=2, + ), +} + +DEFAULT_MODEL_FAMILY = "glm47-multi-role" + + +def _get_config() -> ModelConfig: + family = os.environ.get("SESSION_TEST_MODEL_FAMILY", DEFAULT_MODEL_FAMILY) + if family not in MODEL_REGISTRY: + raise ValueError( + f"Unknown SESSION_TEST_MODEL_FAMILY={family!r}. " f"Choose from: {list(MODEL_REGISTRY.keys())}" + ) + return MODEL_REGISTRY[family] + + +def test_session_server_multi_role(): + cfg = _get_config() + run_session_verify( + hf_checkpoint=cfg.model_name, + tito_model=cfg.tito_model, + allowed_append_roles=list(cfg.allowed_append_roles), + reasoning_parser=cfg.reasoning_parser, + tool_call_parser=cfg.tool_call_parser, + tp_size=cfg.tp_size, + cycles=cfg.cycles, + ) + + +if __name__ == "__main__": + test_session_server_multi_role() diff --git a/tests/e2e/sglang/test_session_server_tool_call.py b/tests/e2e/sglang/test_session_server_tool_call.py deleted file mode 100644 index 967bdddb428..00000000000 --- a/tests/e2e/sglang/test_session_server_tool_call.py +++ /dev/null @@ -1,158 +0,0 @@ -"""E2E test: session-server pretokenized TITO with real model inference. - -Starts the full miles pipeline (sglang + miles-router with session support) -via ``execute_train --debug-rollout-only``, then runs the agentic_tool_call -generate function with a custom agent that performs multi-turn tool calls and -asserts the pretokenized prefix invariant on every turn. - -Requires 1 GPU. -""" - -from tests.ci.ci_register import register_cuda_ci - -register_cuda_ci(est_time=600, suite="stage-b-sglang-1-gpu", num_gpus=1) - - -import json -import os -from dataclasses import dataclass -import miles.utils.external_utils.command_utils as U - -# --------------------------------------------------------------------------- -# Model family registry -# --------------------------------------------------------------------------- - -MODEL_FAMILY = os.environ.get("SESSION_TEST_MODEL_FAMILY", "qwen3") - - -@dataclass(frozen=True) -class ModelConfig: - model_name: str - reasoning_parser: str - tool_call_parser: str | None = None - tito_model: str = "default" - num_gpus: int = 1 - - -MODEL_REGISTRY: dict[str, ModelConfig] = { - "qwen3": ModelConfig( - model_name="Qwen/Qwen3-4B", - reasoning_parser="qwen3", - tool_call_parser="qwen25", - tito_model="qwen3", - ), - "glm47": ModelConfig( - model_name="zai-org/GLM-4.7-Flash", - reasoning_parser="glm45", - tool_call_parser="glm47", - tito_model="glm47", - num_gpus=1, - ), -} - -PROMPT_DATA_PATH = "/root/datasets/session_tool_call.jsonl" - - -def _get_config() -> ModelConfig: - if MODEL_FAMILY not in MODEL_REGISTRY: - raise ValueError(f"Unknown model family {MODEL_FAMILY!r}. " f"Choose from: {list(MODEL_REGISTRY.keys())}") - return MODEL_REGISTRY[MODEL_FAMILY] - - -def prepare(): - cfg = _get_config() - U.exec_command("mkdir -p /root/models /root/datasets") - U.exec_command(f"hf download {cfg.model_name} --local-dir /root/models/{cfg.model_name.split('/')[-1]}") - - prompts = [ - { - "messages": [ - { - "role": "system", - "content": ( - "You are a helpful assistant with access to weather tools. " - "Use the get_weather tool to look up weather information. " - "When you have gathered all the information, " - "wrap your final summary in ... tags." - ), - }, - { - "role": "user", - "content": "What's the weather like in Beijing, Shanghai, Tokyo, and New York?", - }, - ], - }, - ] - with open(PROMPT_DATA_PATH, "w") as f: - for p in prompts: - f.write(json.dumps(p) + "\n") - - -def execute(): - cfg = _get_config() - local_model_dir = f"/root/models/{cfg.model_name.split('/')[-1]}" - - ckpt_args = f"--hf-checkpoint {local_model_dir} " - - rollout_args = ( - f"--prompt-data {PROMPT_DATA_PATH} " - "--input-key messages " - "--num-rollout 1 " - "--rollout-batch-size 16 " - "--n-samples-per-prompt 4 " - "--rollout-max-response-len 1024 " - "--rollout-temperature 0.7 " - "--global-batch-size 64 " - ) - - generate_args = ( - "--custom-generate-function-path " - "miles.rollout.generate_hub.agentic_tool_call.generate " - "--custom-agent-function-path " - "tests.e2e.sglang.utils.session_tool_agent.run_agent " - ) - - router_args = ( - "--use-miles-router " "--use-session-server " "--chat-template-path autofix " f"--tito-model {cfg.tito_model} " - ) - - sglang_args = f"--rollout-num-gpus-per-engine {cfg.num_gpus} " f"--sglang-reasoning-parser {cfg.reasoning_parser} " - if cfg.tool_call_parser: - sglang_args += f"--sglang-tool-call-parser {cfg.tool_call_parser} " - sglang_args += "--rm-type random " - - infra_args = ( - "--debug-rollout-only " - "--ci-test " - "--actor-num-nodes 1 " - f"--actor-num-gpus-per-node {cfg.num_gpus} " - "--colocate " - "--train-backend fsdp " - ) - - train_args = f"{ckpt_args}" f"{rollout_args}" f"{generate_args}" f"{router_args}" f"{sglang_args}" f"{infra_args}" - - U.execute_train( - train_args=train_args, - num_gpus_per_node=cfg.num_gpus, - megatron_model_type=None, - extra_env_vars={ - "MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1", - "SGLANG_E2E_MODEL_PATH": local_model_dir, - "MILES_TITO_MODEL": cfg.tito_model, - }, - ) - - -def test_session_server_tool_call(): - prepare() - for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): - os.environ.pop(proxy_var, None) - execute() - - -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/fast/fixtures/generation_fixtures.py b/tests/fast/fixtures/generation_fixtures.py index 8d823a9d715..dca0ba7d1aa 100644 --- a/tests/fast/fixtures/generation_fixtures.py +++ b/tests/fast/fixtures/generation_fixtures.py @@ -68,7 +68,7 @@ def extra_argv_for_variant( argv.append("--generate-multi-samples") elif variant in ("agentic_tool_call_single_sample", "agentic_tool_call_multi_samples"): argv += ["--custom-agent-function-path", custom_agent_function_path] - argv += ["--tito-model", "qwen3", "--tito-allowed-append-roles", "tool"] + argv += ["--use-session-server", "--tito-model", "qwen3", "--tito-allowed-append-roles", "tool"] if variant == "agentic_tool_call_multi_samples": argv.append("--generate-multi-samples") diff --git a/tests/fast/rollout/generate_hub/test_multi_turn.py b/tests/fast/rollout/generate_hub/test_multi_turn.py index 1558c62cad8..9f37bc9fb2f 100644 --- a/tests/fast/rollout/generate_hub/test_multi_turn.py +++ b/tests/fast/rollout/generate_hub/test_multi_turn.py @@ -709,6 +709,7 @@ def test_no_records_returns_aborted(self, agentic_variant): make_args, with_session_server, ) + from miles.utils.http_utils import find_available_port from miles.utils.misc import SingletonMeta from miles.utils.test_utils.mock_sglang_server import with_mock_server @@ -718,16 +719,17 @@ def test_no_records_returns_aborted(self, agentic_variant): model_name=MODEL_NAME, process_fn=lambda _: ProcessResult(text="unused", finish_reason="stop"), ) as mock_server: - with with_session_server(mock_server.url, MODEL_NAME) as session_port: - noop_argv = extra_argv_for_variant( - agentic_variant, - custom_agent_function_path="miles.utils.test_utils.mock_tools.run_agentic_noop", - ) - args = make_args( - variant=agentic_variant, - router_port=session_port, - extra_argv=noop_argv, - ) + session_port = find_available_port(31000) + noop_argv = extra_argv_for_variant( + agentic_variant, + custom_agent_function_path="miles.utils.test_utils.mock_tools.run_agentic_noop", + ) + args = make_args( + variant=agentic_variant, + router_port=session_port, + extra_argv=noop_argv, + ) + with with_session_server(mock_server.url, args, port=session_port): args.session_server_ip = "127.0.0.1" args.session_server_port = session_port env = GenerateEnv(args=args, mock_server=mock_server) diff --git a/tests/fast/utils/chat_template_utils/test_template.py b/tests/fast/utils/chat_template_utils/test_template.py index ec0a5f83e19..c5ebf6794dd 100644 --- a/tests/fast/utils/chat_template_utils/test_template.py +++ b/tests/fast/utils/chat_template_utils/test_template.py @@ -37,7 +37,12 @@ format_case_id, select_cases, ) -from miles.utils.test_utils.mock_trajectories import SimpleNoToolTrajectory, SingleToolTrajectory +from miles.utils.test_utils.mock_trajectories import ( + MultiRoleSequenceTrajectory, + SimpleNoToolTrajectory, + SingleToolThinkingTrajectory, + SingleToolTrajectory, +) # --------------------------------------------------------------------------- # SGLang reference: calls OpenAIServingChat._process_messages directly @@ -137,13 +142,16 @@ def _build_align_params(): ) variants = enable_thinking_variants("both" if supports_thinking else "off") for case in cases: - # SimpleNoTool ends with a plain assistant message (no tool_calls). - # sglang's _process_messages treats that as continue_final_message - # and drops the trailing <|im_start|>assistant header, which - # diverges from apply_chat_template with add_generation_prompt=True. - # SGLang-alignment-specific, not a property of the trajectory itself, - # so this skip stays inline rather than living on mock_trajectories. - if case.traj_cls is SimpleNoToolTrajectory: + # Trajectories ending with a plain assistant message (no tool_calls): + # sglang's _process_messages treats that as continue_final_message and + # drops the trailing <|im_start|>assistant header, which diverges from + # apply_chat_template(add_generation_prompt=True). SGLang-alignment- + # specific quirk — kept inline rather than living on mock_trajectories. + if case.traj_cls in ( + SimpleNoToolTrajectory, + SingleToolThinkingTrajectory, + MultiRoleSequenceTrajectory, + ): continue for variant in variants: ident = f"{short}-{format_case_id(case, variant)}" diff --git a/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py b/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py deleted file mode 100644 index 82fa298ba24..00000000000 --- a/tests/fast/utils/chat_template_utils/test_tito_tokenizer_model_matrix.py +++ /dev/null @@ -1,238 +0,0 @@ -from __future__ import annotations - -from tests.ci.ci_register import register_cpu_ci - -register_cpu_ci(est_time=60, suite="stage-a-fast") - -from copy import deepcopy -from dataclasses import dataclass - -import pytest -from transformers import AutoTokenizer - -from miles.utils.chat_template_utils import MismatchType, apply_chat_template, resolve_fixed_chat_template -from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer, TITOTokenizerType, get_tito_tokenizer -from miles.utils.processing_utils import load_tokenizer -from miles.utils.test_utils.mock_trajectories import ( - MultiUserTurnThinkingTrajectory, - SimpleNoToolTrajectory, - SingleToolThinkingTrajectory, - SingleToolTrajectory, -) - -TOOL_CALL_TEST_MODELS = [ - "Qwen/Qwen2.5-0.5B-Instruct", - "Qwen/Qwen3-0.6B", - "Qwen/Qwen3.5-0.8B", - "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 - "zai-org/GLM-4.7-Flash", - "mistralai/Mistral-7B-Instruct-v0.3", - "deepseek-ai/DeepSeek-V3", - "stepfun-ai/step3", - "MiniMaxAI/MiniMax-M2", - "MiniMaxAI/MiniMax-M2.5", - "internlm/internlm3-8b-instruct", - "THUDM/glm-4-9b-chat", - "moonshotai/Kimi-K2-Instruct", - "moonshotai/Kimi-K2.5", - "XiaomiMiMo/MiMo-7B-RL", - "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16", -] - -# Models excluded from TITO testing due to known template incompatibilities. -# Filtered out of parametrized test cases below. -_TITO_EXCLUDED_MODELS: dict[str, str] = { - "Qwen/Qwen3.5-0.8B": ( - "The qwen3.5 fixed template rejects non-first system messages with " - "'System message must be at the beginning'. TITO's synthetic bases " - "place system first, so this exclusion may be removable — needs testing." - ), - "deepseek-ai/DeepSeek-V3": ( - "TITO tokenizes each tool segment independently via _tokenize_tool_segment, " - "which causes DeepSeek-V3's template to emit extra " - "<|tool_outputs_begin|>/<|tool_outputs_end|> wrappers that differ from " - "full-conversation rendering." - ), -} -_TITO_TEST_MODELS = [m for m in TOOL_CALL_TEST_MODELS if m not in _TITO_EXCLUDED_MODELS] - -_ALLOWED_APPEND_ROLES = ["tool", "user", "system"] -_TOK_CACHE: dict[tuple[str, str | None], AutoTokenizer] = {} -_ASSISTANT_START_BY_MODEL: dict[str, str] = { - "Qwen/Qwen2.5-0.5B-Instruct": "<|im_start|>assistant\n", - "mistralai/Mistral-7B-Instruct-v0.3": "[/INST]", - "deepseek-ai/DeepSeek-V3": "<|Assistant|>", - "stepfun-ai/step3": "<|BOT|>assistant\n", - "MiniMaxAI/MiniMax-M2": "]~b]ai\n", - "MiniMaxAI/MiniMax-M2.5": "]~b]ai\n", - "internlm/internlm3-8b-instruct": "<|im_start|>assistant\n", - "THUDM/glm-4-9b-chat": "<|assistant|>", - "moonshotai/Kimi-K2-Instruct": "<|im_assistant|>assistant<|im_middle|>", - "moonshotai/Kimi-K2.5": "<|im_assistant|>assistant<|im_middle|>", - "XiaomiMiMo/MiMo-7B-RL": "<|im_start|>assistant\n", - "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": "<|im_start|>assistant\n", -} -_NO_SYSTEM_APPEND_MODELS = { - "deepseek-ai/DeepSeek-V3", - "stepfun-ai/step3", - "MiniMaxAI/MiniMax-M2", - "MiniMaxAI/MiniMax-M2.5", -} -_CONTENT_WHITESPACE_AGNOSTIC_MODELS = { - "stepfun-ai/step3", -} - - -@dataclass(frozen=True) -class AppendCase: - name: str - old_messages: list[dict] - appended_messages: list[dict] - tools: list[dict] | None - required_contents: tuple[str, ...] = () - - -_APPEND_CASES = [ - AppendCase( - name="single_tool", - old_messages=deepcopy(SingleToolTrajectory.MESSAGES[:3]), - appended_messages=deepcopy([SingleToolTrajectory.MESSAGES[3]]), - tools=deepcopy(SingleToolTrajectory.TOOLS), - required_contents=(SingleToolTrajectory.MESSAGES[3]["content"],), - ), - AppendCase( - name="single_user", - old_messages=deepcopy(MultiUserTurnThinkingTrajectory.MESSAGES[:5]), - appended_messages=deepcopy([MultiUserTurnThinkingTrajectory.MESSAGES[5]]), - tools=deepcopy(MultiUserTurnThinkingTrajectory.TOOLS), - required_contents=(MultiUserTurnThinkingTrajectory.MESSAGES[5]["content"],), - ), - AppendCase( - name="single_system", - old_messages=deepcopy(SimpleNoToolTrajectory.MESSAGES), - appended_messages=[{"role": "system", "content": "Please answer in one short sentence."}], - tools=None, - required_contents=("Please answer in one short sentence.",), - ), - AppendCase( - name="alternating_user_tool", - old_messages=deepcopy(SingleToolThinkingTrajectory.MESSAGES[:3]), - appended_messages=[ - deepcopy(SingleToolThinkingTrajectory.MESSAGES[3]), - {"role": "user", "content": "Now check Shanghai too."}, - { - "role": "tool", - "tool_call_id": "call_followup_1", - "content": '{"temperature": 30, "condition": "cloudy"}', - }, - {"role": "user", "content": "And tell me the date as well."}, - ], - tools=deepcopy(SingleToolThinkingTrajectory.TOOLS), - required_contents=( - SingleToolThinkingTrajectory.MESSAGES[3]["content"], - "Now check Shanghai too.", - '{"temperature": 30, "condition": "cloudy"}', - "And tell me the date as well.", - ), - ), -] - -_ALL_PARAMS = [ - pytest.param(model_name, case, id=f"{case.name}-{model_name}") - for model_name in _TITO_TEST_MODELS - for case in _APPEND_CASES - if not (case.name == "single_system" and model_name in _NO_SYSTEM_APPEND_MODELS) -] - - -def _resolve_tito_type(model_name: str) -> TITOTokenizerType: - lowered = model_name.lower() - if "qwen3" in lowered: - return TITOTokenizerType.QWEN3 - if "glm-4.7" in lowered: - return TITOTokenizerType.GLM47 - return TITOTokenizerType.DEFAULT - - -def _get_tokenizer(model_name: str) -> AutoTokenizer: - # Mirror arguments.py: DEFAULT means "use HF native, no override", so we - # bypass the resolver. Only fixed-template families go through resolve. - tito_type = _resolve_tito_type(model_name) - if tito_type == TITOTokenizerType.DEFAULT: - chat_template_path = None - else: - chat_template_path, _kwargs = resolve_fixed_chat_template(tito_type, ["tool"]) - cache_key = (model_name, chat_template_path) - if cache_key not in _TOK_CACHE: - _TOK_CACHE[cache_key] = load_tokenizer( - model_name, - chat_template_path=chat_template_path, - trust_remote_code=True, - ) - return _TOK_CACHE[cache_key] - - -def _get_tito(model_name: str, tokenizer: AutoTokenizer) -> TITOTokenizer: - tokenizer_type = _resolve_tito_type(model_name) - kwargs = { - "tokenizer_type": tokenizer_type, - "allowed_append_roles": _ALLOWED_APPEND_ROLES, - } - if tokenizer_type == TITOTokenizerType.DEFAULT: - kwargs["assistant_start_str"] = _ASSISTANT_START_BY_MODEL[model_name] - return get_tito_tokenizer(tokenizer, **kwargs) - - -def _render_ids( - tokenizer: AutoTokenizer, messages: list[dict], tools: list[dict] | None, *, add_generation_prompt: bool -) -> list[int]: - return apply_chat_template( - messages, - tokenizer=tokenizer, - tokenize=True, - add_generation_prompt=add_generation_prompt, - tools=tools, - ) - - -def _assert_only_assistant_mismatches(tito: TITOTokenizer, expected: list[int], merged: list[int]) -> None: - mismatches = tito.create_comparator().compare_sequences(expected, merged) - bad = [m for m in mismatches if m.type != MismatchType.ASSISTANT_TEXT] - assert not bad, [m.to_dict() for m in bad] - - -def _assert_contents_in_order( - incremental_text: str, required_contents: tuple[str, ...], *, model_name: str, case_name: str -) -> None: - if model_name in _CONTENT_WHITESPACE_AGNOSTIC_MODELS: - incremental_text = "".join(incremental_text.split()) - required_contents = tuple("".join(content.split()) for content in required_contents) - cursor = 0 - for content in required_contents: - found = incremental_text.find(content, cursor) - assert found >= 0, f"{model_name=} {case_name=} missing ordered content {content!r}" - cursor = found + len(content) - - -def _run_case(model_name: str, case: AppendCase) -> tuple[TITOTokenizer, list[int], list[int], str]: - tokenizer = _get_tokenizer(model_name) - tito = _get_tito(model_name, tokenizer) - old_messages = deepcopy(case.old_messages) - new_messages = old_messages + deepcopy(case.appended_messages) - try: - expected = _render_ids(tokenizer, new_messages, case.tools, add_generation_prompt=True) - pretokenized = _render_ids(tokenizer, old_messages, case.tools, add_generation_prompt=False) - except Exception as exc: - pytest.skip(f"{model_name} cannot render case {case.name}: {type(exc).__name__}: {exc}") - merged = tito.merge_tokens(old_messages, new_messages, pretokenized, case.tools) - incremental_text = tokenizer.decode(tito.tokenize_additional_non_assistant(old_messages, new_messages, case.tools)) - return tito, merged, expected, incremental_text - - -@pytest.mark.parametrize(("model_name", "case"), _ALL_PARAMS) -def test_appended_non_assistant_content_preserved(model_name: str, case: AppendCase): - tito, merged, expected, incremental_text = _run_case(model_name, case) - _assert_only_assistant_mismatches(tito, expected, merged) - _assert_contents_in_order(incremental_text, case.required_contents, model_name=model_name, case_name=case.name) From 50b2224698a8956b0c3a75cf535847ad74eeb6e1 Mon Sep 17 00:00:00 2001 From: Yueming Yuan Date: Mon, 4 May 2026 11:19:14 -0700 Subject: [PATCH 0009/1130] [feat] Add train_rollout_kl metric for outlier-robust train/rollout divergence (#1067) --- miles/backends/training_utils/loss.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index 7f616a8b70b..565e6b694fa 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -751,9 +751,14 @@ def policy_loss_function( loss += 0 * logits.sum() train_rollout_logprob_abs_diff = None + train_rollout_kl = None if "rollout_log_probs" in batch and batch["rollout_log_probs"]: rollout_log_probs = torch.cat(batch["rollout_log_probs"], dim=0) train_rollout_logprob_abs_diff = sum_of_sample_mean((old_log_probs - rollout_log_probs).abs()) + # KL(rollout || train) at sampled tokens via Schulman k3 with per-token clamp [-10, 10] + train_rollout_kl = sum_of_sample_mean( + compute_approx_kl(rollout_log_probs, old_log_probs, kl_loss_type="low_var_kl") + ) reported_loss = { "loss": loss.clone().detach(), @@ -766,6 +771,8 @@ def policy_loss_function( if train_rollout_logprob_abs_diff is not None: reported_loss["train_rollout_logprob_abs_diff"] = train_rollout_logprob_abs_diff.clone().detach() + if train_rollout_kl is not None: + reported_loss["train_rollout_kl"] = train_rollout_kl.clone().detach() if args.use_kl_loss: reported_loss["kl_loss"] = kl_loss.clone().detach() From 569d88faaca06aa9aefc593a262054213ff6c755 Mon Sep 17 00:00:00 2001 From: Yuzhen Zhou <82826991+zyzshishui@users.noreply.github.com> Date: Mon, 4 May 2026 14:18:42 -0700 Subject: [PATCH 0010/1130] [TITO] feat (3/N): verify chat templates through TITO tokenizer (#1063) --- .../utils/test_utils/chat_template_verify.py | 12 +- scripts/tools/verify_chat_template.py | 142 ++++++++++-------- 2 files changed, 86 insertions(+), 68 deletions(-) diff --git a/miles/utils/test_utils/chat_template_verify.py b/miles/utils/test_utils/chat_template_verify.py index f0630fb0a81..2f153a44fca 100644 --- a/miles/utils/test_utils/chat_template_verify.py +++ b/miles/utils/test_utils/chat_template_verify.py @@ -17,7 +17,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from miles.utils.chat_template_utils.template import apply_chat_template, apply_chat_template_from_str +from miles.utils.chat_template_utils.template import apply_chat_template_from_str if TYPE_CHECKING: from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer, TITOTokenizerType @@ -456,21 +456,15 @@ def verify_append_only_via_tito_instance( prefix_msgs = deepcopy(messages[:n]) full_msgs = deepcopy(messages[:m]) - prefix_text = apply_chat_template( + prefix_text = tito._render_messages( prefix_msgs, - tokenizer=tokenizer, tools=tools, add_generation_prompt=False, - tokenize=False, - **template_kwargs, ) - full_text = apply_chat_template( + full_text = tito._render_messages( full_msgs, - tokenizer=tokenizer, tools=tools, add_generation_prompt=True, - tokenize=False, - **template_kwargs, ) prefix_ids = tokenizer.encode(prefix_text, add_special_tokens=False) diff --git a/scripts/tools/verify_chat_template.py b/scripts/tools/verify_chat_template.py index f02f91aa7cb..a776d993c3b 100644 --- a/scripts/tools/verify_chat_template.py +++ b/scripts/tools/verify_chat_template.py @@ -3,16 +3,20 @@ Usage examples:: - # Verify a local .jinja template file + # Verify a local .jinja template file at raw template-string level python scripts/tools/verify_chat_template.py --template path/to/template.jinja - # Verify a HuggingFace model's chat template + # Verify a HuggingFace model's chat template at raw template-string level python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B - # Resolve a bundled fixed template for a TITO tokenizer family + # Verify through the registered TITO tokenizer family python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B \\ --tito-model qwen3 --tito-allowed-append-roles tool + # Verify through TITO while testing a local template override + python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B \\ + --tito-model qwen3 --template miles/utils/chat_template_utils/templates/qwen3_fixed.jinja + # Restrict which append roles the session is allowed to use (tool is implicit) python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B \\ --tito-allowed-append-roles user @@ -37,32 +41,11 @@ def _load_template_from_file(path: str) -> str: def _load_template_from_model( model_id: str, - *, - tito_model: str | None, - allowed_roles: list[str], -) -> tuple[str, str, dict]: - """Load chat template for a HF model. - - Returns ``(template_str, source_description, resolved_kwargs)``. When the - matched fixed-template row registers extra kwargs, the caller is expected - to merge ``resolved_kwargs`` into its own template kwargs (with explicit - user values winning on conflict). - """ - if tito_model: - from miles.utils.chat_template_utils import resolve_fixed_chat_template - - fixed_path, resolved_kwargs = resolve_fixed_chat_template(tito_model, allowed_roles) - if fixed_path: - return _load_template_from_file(fixed_path), f"fixed template: {fixed_path}", resolved_kwargs - # No template path matched, but the row may still carry kwargs (e.g. an - # HF-native + clear_thinking=False fix). Fall through to HF-native - # loading and bubble the kwargs up. - else: - resolved_kwargs = {} - +) -> tuple[str, str]: + """Load the raw HuggingFace chat template for a model.""" from miles.utils.chat_template_utils.template import load_hf_chat_template - return load_hf_chat_template(model_id), f"HuggingFace: {model_id}", resolved_kwargs + return load_hf_chat_template(model_id), f"HuggingFace: {model_id}" def main() -> int: @@ -72,16 +55,19 @@ def main() -> int: epilog=__doc__, ) - source = parser.add_mutually_exclusive_group(required=True) - source.add_argument( + parser.add_argument( "--template", metavar="PATH", - help="Path to a local .jinja chat template file.", + help=( + "Path to a local .jinja chat template file. Without --tito-model this " + "runs the legacy raw template-string verifier; with --tito-model this " + "overrides the registered TITO template while still using --model for tokenizer loading." + ), ) - source.add_argument( + parser.add_argument( "--model", metavar="MODEL_ID", - help="HuggingFace model ID (e.g. Qwen/Qwen3-0.6B).", + help="HuggingFace model ID or local checkpoint path (e.g. Qwen/Qwen3-0.6B).", ) parser.add_argument( @@ -89,9 +75,10 @@ def main() -> int: choices=[t.value for t in TITOTokenizerType], default=None, help=( - "When using --model, look up a bundled fixed template registered for this " - "TITO tokenizer family under the given --tito-allowed-append-roles surface. " - "Falls back to the HF default chat template when no entry is registered." + "Verify through the registered TITO tokenizer family under the given " + "--tito-allowed-append-roles surface. Requires --model and exercises " + "the production-shape TITO merge/tokenize path instead of the raw " + "template-string verifier." ), ) parser.add_argument( @@ -120,7 +107,7 @@ def main() -> int: parser.add_argument( "--chat-template-kwargs", type=json.loads, - default={}, + default=None, metavar="JSON", help=( "Extra kwargs threaded into the chat template on every render, as a " @@ -131,33 +118,55 @@ def main() -> int: args = parser.parse_args() - extra_template_kwargs = args.chat_template_kwargs + if args.model is None and args.template is None: + parser.error("one of --model or --template is required") + if args.tito_model is not None and args.model is None: + parser.error("--tito-model requires --model so the TITO verifier can load the tokenizer") + + extra_template_kwargs = dict(args.chat_template_kwargs or {}) # ``--tito-allowed-append-roles`` lists the *optional* extra roles; ``tool`` # is implicit for tool-capable agentic workflows and unioned in here so both # the fixed-template lookup and the trajectory filter see the same surface. allowed_roles = set(args.tito_allowed_append_roles) | {"tool"} - # ── Load template ────────────────────────────────────────────────── - if args.template: - chat_template = _load_template_from_file(args.template) - source_desc = f"file: {args.template}" - else: - chat_template, source_desc, resolved_kwargs = _load_template_from_model( - args.model, - tito_model=args.tito_model, - allowed_roles=sorted(allowed_roles), - ) - # Merge auto-resolved kwargs into the user's --chat-template-kwargs. - # Explicit user values win on conflict; only keys the user did not set - # are auto-filled. + use_tito_instance = args.tito_model is not None + + # ── Load template/tokenizer ──────────────────────────────────────── + if use_tito_instance: + from miles.utils.chat_template_utils import resolve_fixed_chat_template + from miles.utils.processing_utils import load_tokenizer + + fixed_path, resolved_kwargs = resolve_fixed_chat_template(args.tito_model, sorted(allowed_roles)) for key, value in resolved_kwargs.items(): if key in extra_template_kwargs: continue extra_template_kwargs[key] = value print(f"Auto-set --chat-template-kwargs {key}={value!r} (from --tito-model={args.tito_model})") - from miles.utils.test_utils.chat_template_verify import ALL_CASES, check_coverage, run_all_checks, select_cases + template_path = args.template or fixed_path + tokenizer = load_tokenizer(args.model, chat_template_path=template_path, trust_remote_code=True) + if args.template: + source_desc = f"template override via TITO: {args.template}" + elif fixed_path: + source_desc = f"fixed template via TITO: {fixed_path}" + elif getattr(tokenizer, "chat_template", None) is not None: + source_desc = f"HuggingFace via TITO: {args.model}" + else: + source_desc = f"TITO encoder: {args.tito_model}" + elif args.template: + chat_template = _load_template_from_file(args.template) + source_desc = f"file: {args.template}" + else: + chat_template, source_desc = _load_template_from_model(args.model) + + from miles.utils.test_utils.chat_template_verify import ( + ALL_CASES, + check_coverage, + run_all_checks, + run_all_checks_via_tito, + select_cases, + ) is_thinking_filter = {"off": False, "on": True, "both": None}[args.thinking] selected = select_cases(allowed_append_roles=allowed_roles, is_thinking=is_thinking_filter) @@ -184,12 +193,21 @@ def main() -> int: print() # ── Run verification ─────────────────────────────────────────────── - results = run_all_checks( - chat_template, - allowed_append_roles=allowed_roles, - thinking=args.thinking, - extra_template_kwargs=extra_template_kwargs, - ) + if use_tito_instance: + results = run_all_checks_via_tito( + tokenizer, + args.tito_model, + allowed_append_roles=allowed_roles, + thinking=args.thinking, + extra_template_kwargs=extra_template_kwargs, + ) + else: + results = run_all_checks( + chat_template, + allowed_append_roles=allowed_roles, + thinking=args.thinking, + extra_template_kwargs=extra_template_kwargs, + ) # ── Print results ────────────────────────────────────────────────── passed = sum(1 for r in results if r.passed) @@ -211,10 +229,16 @@ def main() -> int: print(f"Results: {passed}/{len(results)} passed, {failed} failed") if failed: - print("\nVerdict: FAIL - template is NOT append-only after last user message") + if use_tito_instance: + print("\nVerdict: FAIL - TITO incremental tokenization did NOT match standard render") + else: + print("\nVerdict: FAIL - template is NOT append-only after last user message") return 1 else: - print("\nVerdict: PASS - template IS append-only after last user message") + if use_tito_instance: + print("\nVerdict: PASS - TITO incremental tokenization matched standard render") + else: + print("\nVerdict: PASS - template IS append-only after last user message") return 0 From 35d5198e87c12fffd4f04540774cce18959efcfd Mon Sep 17 00:00:00 2001 From: Yuzhen Zhou <82826991+zyzshishui@users.noreply.github.com> Date: Tue, 5 May 2026 00:44:19 -0700 Subject: [PATCH 0011/1130] [TITO] feat (4/N): Use prompt ids from miles session server (#1064) Co-authored-by: Jiajun Li --- docs/en/agentic/oai_endpoint.md | 18 +++---- .../generate_utils/openai_endpoint_utils.py | 2 +- miles/rollout/session/linear_trajectory.py | 23 +-------- miles/rollout/session/session_server.py | 8 ++- miles/rollout/session/sessions.py | 37 ++++++++------ .../chat_template_utils/tito_tokenizer.py | 11 +++-- .../utils/test_utils/chat_template_verify.py | 4 +- miles/utils/test_utils/mock_sglang_server.py | 49 ++++++++++--------- .../rollout/generate_hub/test_multi_turn.py | 36 +++++++++++--- tests/fast/router/test_linear_trajectory.py | 34 +++++++------ tests/fast/router/test_sessions.py | 7 ++- .../test_tito_tokenizer.py | 2 +- .../test_utils/test_mock_sglang_server.py | 4 ++ 13 files changed, 133 insertions(+), 102 deletions(-) diff --git a/docs/en/agentic/oai_endpoint.md b/docs/en/agentic/oai_endpoint.md index 4f40a4f2ea6..3fe95f28b32 100644 --- a/docs/en/agentic/oai_endpoint.md +++ b/docs/en/agentic/oai_endpoint.md @@ -46,16 +46,13 @@ Minimal request shape: "messages": [ {"role": "system", "content": "You are a concise assistant."}, {"role": "user", "content": "Answer with one word: 2+2?"} - ], - "logprobs": true, - "return_prompt_token_ids": true + ] } ``` You can pass any OpenAI-compatible parameters in the payload, or any -SGLang-compatible `ChatCompletionRequest` parameters. Note: -`logprobs=True` and `return_prompt_token_ids=True` are set in -`request_kwargs` to extract token ids and logprobs for TITO (see below). +SGLang-compatible `ChatCompletionRequest` parameters. Note: with +`--use-session-server`, the session middleware sets the token-tracking fields TITO needs. Do **not** set `logprob_start_len=0` — it would disable SGLang's prefix cache. @@ -129,11 +126,10 @@ request. It requires `--use-session-server`; with that on, miles handles three things on your behalf: - Hardcodes the SGLang flags TITO needs on every chat request - (`logprobs=True`, `return_meta_info=True`, `return_prompt_token_ids=True`, - `no_stop_trim=False`); these are set by the middleware in - `miles/rollout/session/sessions.py` and override any agent-passed values. -- Reuses the token prefix from previous turns by injecting `input_ids` on - follow-up requests. + (`logprobs=True`, `return_meta_info=True`, `no_stop_trim=False`); these are + set by the middleware in `miles/rollout/session/sessions.py` and override any + agent-passed values. +- Reuses the token prefix from previous turns by injecting Miles-owned `input_ids` on every proxied chat request. - Accumulates per-turn records into the `Sample` you receive at the end of the session, with `tokens` and `rollout_log_probs` already populated. diff --git a/miles/rollout/generate_utils/openai_endpoint_utils.py b/miles/rollout/generate_utils/openai_endpoint_utils.py index 2b3016fd4fa..7db40ae585c 100644 --- a/miles/rollout/generate_utils/openai_endpoint_utils.py +++ b/miles/rollout/generate_utils/openai_endpoint_utils.py @@ -168,7 +168,7 @@ def _compute_sample_from_openai_record( if "prompt_token_ids" in choice: prompt_token_ids = choice["prompt_token_ids"] else: - raise ValueError("prompt_token_ids not found in response choice — ensure return_prompt_token_ids=True is set") + raise ValueError("prompt_token_ids not found in response choice — the session server should populate it") output_token_ids = [item[1] for item in choice["meta_info"]["output_token_logprobs"]] output_log_probs = [item[0] for item in choice["meta_info"]["output_token_logprobs"]] diff --git a/miles/rollout/session/linear_trajectory.py b/miles/rollout/session/linear_trajectory.py index f061a678f94..ea1e5ccbbaf 100644 --- a/miles/rollout/session/linear_trajectory.py +++ b/miles/rollout/session/linear_trajectory.py @@ -6,11 +6,7 @@ from miles.rollout.session.session_errors import MessageValidationError, SessionNotFoundError, TokenizationError from miles.rollout.session.session_types import SessionRecord -from miles.utils.chat_template_utils import ( - apply_chat_template, - assert_messages_append_only_with_allowed_role, - message_matches, -) +from miles.utils.chat_template_utils import assert_messages_append_only_with_allowed_role, message_matches from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer logger = logging.getLogger(__name__) @@ -21,19 +17,6 @@ MAX_ASSISTANT_ROLLBACK_STEPS = 1 -def _assert_no_user_after_assistant(messages: list[dict[str, Any]]) -> None: - """Assert no user message appears after the first assistant message.""" - seen_assistant = False - for i, msg in enumerate(messages): - role = msg.get("role") - if role == "assistant": - seen_assistant = True - elif role == "user" and seen_assistant: - raise MessageValidationError( - f"invalid message structure: user message at index {i} " f"appears after the first assistant message" - ) - - @dataclass class LinearTrajectory: """State for a linear trajectory. @@ -281,13 +264,11 @@ def compute_session_mismatch(self, session: LinearTrajectory) -> list[dict] | No return None try: tools = session.records[-1].request.get("tools") if session.records else None - expected_ids = apply_chat_template( + expected_ids = self.tito_tokenizer.render_messages( session.messages, - tokenizer=self.tokenizer, tools=tools, add_generation_prompt=False, tokenize=True, - **self.tito_tokenizer.chat_template_kwargs, ) mismatches = self.comparator.compare_sequences(expected_ids, session.token_ids) return [m.to_dict() for m in mismatches] diff --git a/miles/rollout/session/session_server.py b/miles/rollout/session/session_server.py index bc2633350ea..ea64ace826c 100644 --- a/miles/rollout/session/session_server.py +++ b/miles/rollout/session/session_server.py @@ -81,7 +81,13 @@ async def do_proxy( def build_proxy_response(self, result: dict) -> Response: content = result["response_body"] status_code = result["status_code"] - headers = result["headers"] + # Drop wire-level framing headers from upstream so Starlette rebuilds them + # from the body we actually send: transfer-encoding is hop-by-hop + headers = { + k: v + for k, v in result["headers"].items() + if k.lower() not in ("content-length", "transfer-encoding", "content-encoding") + } content_type = headers.get("content-type", "") try: data = json.loads(content) diff --git a/miles/rollout/session/sessions.py b/miles/rollout/session/sessions.py index 21a862cda12..858e0424ab6 100644 --- a/miles/rollout/session/sessions.py +++ b/miles/rollout/session/sessions.py @@ -137,20 +137,18 @@ async def chat_completions(request: Request, session_id: str): body = await request.body() request_body = json.loads(body) if body else {} - # TITO token tracking requires three SGLang flags working together: - # logprobs=True → populates meta_info.output_token_logprobs - # return_prompt_token_ids → adds choice.prompt_token_ids - # return_meta_info → wraps the above in choice.meta_info - # All three are hardcoded (not setdefault) to prevent agent-side + # TITO token tracking requires Miles-owned input_ids plus SGLang + # output-token metadata: + # logprobs=True → populates meta_info.output_token_logprobs + # return_meta_info → wraps the above in choice.meta_info + # Both flags are hardcoded (not set default) to prevent agent-side # overrides from breaking the token accumulation invariants. request_body["logprobs"] = True - request_body["return_prompt_token_ids"] = True request_body["return_meta_info"] = True if getattr(args, "use_rollout_routing_replay", False): request_body["return_routed_experts"] = True - # Must be False so stop tokens are trimmed from output: otherwise the - # agent sees stop-token text in content, and the accumulated checkpoint - # would duplicate structural delimiters that the chat template also emits. + # Must be False so stop-token text is trimmed from assistant + # message content; token IDs are still taken from logprobs below. request_body["no_stop_trim"] = False request_messages = request_body.get("messages", []) @@ -159,12 +157,21 @@ async def chat_completions(request: Request, session_id: str): tools=request_body.get("tools"), tito_tokenizer=registry.tito_tokenizer, ) + prompt_token_ids = None if pretokenized is not None: - request_body["input_ids"] = pretokenized["input_ids"] - logger.debug( - "Using pretokenized input_ids: %d tokens", - len(pretokenized["input_ids"]), + prompt_token_ids = pretokenized["input_ids"] + else: + prompt_token_ids = registry.tito_tokenizer.render_messages( + request_messages, + tools=request_body.get("tools"), + add_generation_prompt=True, + tokenize=True, ) + request_body["input_ids"] = prompt_token_ids + logger.debug( + "Using TITO input_ids: %d tokens", + len(prompt_token_ids), + ) body = json.dumps(request_body).encode() expected_num_assistant = session.num_assistant @@ -195,7 +202,9 @@ async def chat_completions(request: Request, session_id: str): "an empty content rather than None. Please check your modified SGLang version." ) - prompt_token_ids = choice.get("prompt_token_ids") + choice["prompt_token_ids"] = prompt_token_ids + # This must be re-encoded to return the prompt token ids + result["response_body"] = json.dumps(response).encode() output_token_logprobs = meta_info["output_token_logprobs"] completion_tokens = meta_info["completion_tokens"] diff --git a/miles/utils/chat_template_utils/tito_tokenizer.py b/miles/utils/chat_template_utils/tito_tokenizer.py index fa094ceabdf..a568ab316df 100644 --- a/miles/utils/chat_template_utils/tito_tokenizer.py +++ b/miles/utils/chat_template_utils/tito_tokenizer.py @@ -120,17 +120,18 @@ def create_comparator(self) -> TokenSeqComparator: trim_trailing_ids=self.trailing_token_ids or None, ) - def _render_messages( + def render_messages( self, messages: list[dict[str, Any]], *, add_generation_prompt: bool, tools: list[dict[str, Any]] | None = None, - ) -> str: + tokenize: bool = False, + ) -> str | list[int]: return apply_chat_template( messages, tokenizer=self.tokenizer, - tokenize=False, + tokenize=tokenize, add_generation_prompt=add_generation_prompt, tools=tools, **self.chat_template_kwargs, @@ -175,8 +176,8 @@ def _tokenize_rendered_suffix( When *add_generation_prompt* is True and *appended_messages* is empty, this computes the generation-prompt suffix (the assistant opener tokens). """ - text_without = self._render_messages(base_messages, add_generation_prompt=False, tools=tools) - text_with = self._render_messages( + text_without = self.render_messages(base_messages, add_generation_prompt=False, tools=tools) + text_with = self.render_messages( base_messages + appended_messages, add_generation_prompt=add_generation_prompt, tools=tools, diff --git a/miles/utils/test_utils/chat_template_verify.py b/miles/utils/test_utils/chat_template_verify.py index 2f153a44fca..b51fc339b4b 100644 --- a/miles/utils/test_utils/chat_template_verify.py +++ b/miles/utils/test_utils/chat_template_verify.py @@ -456,12 +456,12 @@ def verify_append_only_via_tito_instance( prefix_msgs = deepcopy(messages[:n]) full_msgs = deepcopy(messages[:m]) - prefix_text = tito._render_messages( + prefix_text = tito.render_messages( prefix_msgs, tools=tools, add_generation_prompt=False, ) - full_text = tito._render_messages( + full_text = tito.render_messages( full_msgs, tools=tools, add_generation_prompt=True, diff --git a/miles/utils/test_utils/mock_sglang_server.py b/miles/utils/test_utils/mock_sglang_server.py index dbe4d497c79..294aa413af2 100644 --- a/miles/utils/test_utils/mock_sglang_server.py +++ b/miles/utils/test_utils/mock_sglang_server.py @@ -146,11 +146,13 @@ def _compute_chat_completions_response(self, payload: dict) -> dict: messages, tokenize=False, add_generation_prompt=True, tools=tools ) - input_ids = payload.get("input_ids") - if input_ids is not None: - prompt_ids = list(input_ids) - else: - prompt_ids = self.tokenizer.encode(prompt_str, add_special_tokens=False) + prompt_ids = None + if payload.get("return_prompt_token_ids"): + input_ids = payload.get("input_ids") + if input_ids is not None: + prompt_ids = list(input_ids) + else: + prompt_ids = self.tokenizer.encode(prompt_str, add_special_tokens=False) process_result = self.process_fn(prompt_str) output_ids = self.tokenizer.encode(process_result.text, add_special_tokens=False) @@ -187,29 +189,30 @@ def _compute_chat_completions_response(self, payload: dict) -> dict: output_token_logprobs = [(-1 / 128 * i, tid) for i, tid in enumerate(output_ids)] + choice = { + "index": 0, + "message": { + "role": "assistant", + "content": message_content, + "tool_calls": tool_calls, + }, + "logprobs": {"content": logprobs_content}, + "finish_reason": finish_reason, + "meta_info": { + "output_token_logprobs": output_token_logprobs, + "completion_tokens": len(output_ids), + **process_result.meta_info.to_dict(), + }, + } + if prompt_ids is not None: + choice["prompt_token_ids"] = prompt_ids + 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}, - "prompt_token_ids": prompt_ids, - "finish_reason": finish_reason, - "meta_info": { - "output_token_logprobs": output_token_logprobs, - "completion_tokens": len(output_ids), - **process_result.meta_info.to_dict(), - }, - } - ], + "choices": [choice], } diff --git a/tests/fast/rollout/generate_hub/test_multi_turn.py b/tests/fast/rollout/generate_hub/test_multi_turn.py index 9f37bc9fb2f..4a5851438b1 100644 --- a/tests/fast/rollout/generate_hub/test_multi_turn.py +++ b/tests/fast/rollout/generate_hub/test_multi_turn.py @@ -10,6 +10,7 @@ from tests.fast.fixtures.generation_fixtures import GenerateEnv, generation_env, listify, make_sample, run_generate +from miles.utils.chat_template_utils import TITOTokenizerType, get_tito_tokenizer from miles.utils.processing_utils import load_tokenizer from miles.utils.test_utils.mock_sglang_server import ProcessResult, ProcessResultMetaInfo from miles.utils.test_utils.mock_tools import SAMPLE_TOOLS, ThreeTurnStub, TwoTurnStub @@ -160,7 +161,6 @@ def expected_openai_request(messages: list[dict], **extra) -> dict: "tools": SAMPLE_TOOLS, # Injected by the session route for TITO token tracking "logprobs": True, - "return_prompt_token_ids": True, "return_meta_info": True, "no_stop_trim": False, **extra, @@ -196,7 +196,7 @@ def test_single_turn_no_tool_call(self, variant, generation_env): 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)] + assert _strip_pretokenized(result.requests) == [expected_openai_request(SINGLE_TURN_PROMPT)] else: assert result.requests == [expected_request(SINGLE_TURN_PROMPT_TOKEN_IDS)] verify_samples( @@ -318,7 +318,7 @@ def test_finish_reason_length_exits_and_preserves_content(self, variant, generat 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)] + assert _strip_pretokenized(result.requests) == [expected_openai_request(S.OPENAI_MESSAGES_FIRST_TURN)] else: assert result.requests == [expected_request(S.FIRST_PROMPT_TOKEN_IDS)] verify_samples( @@ -344,7 +344,7 @@ def test_max_turns_reached(self, variant, generation_env): 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)] + assert _strip_pretokenized(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": @@ -568,6 +568,28 @@ def test_two_turns_routed_experts(self, variant, generation_env): num_layers, moe_router_topk = 2, 4 generation_env.args.num_layers = num_layers generation_env.args.moe_router_topk = moe_router_topk + if is_agentic_variant(variant): + tito = get_tito_tokenizer( + TOKENIZER, + # Note: tokenizer_type needs to match MODEL_NAME + tokenizer_type=TITOTokenizerType.QWEN3, + allowed_append_roles=["tool"], + ) + first_prompt_token_ids = tito.render_messages( + S.OPENAI_MESSAGES_FIRST_TURN, + tools=SAMPLE_TOOLS, + add_generation_prompt=True, + tokenize=True, + ) + second_prompt_token_ids = tito.render_messages( + S.OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT, + tools=SAMPLE_TOOLS, + add_generation_prompt=True, + tokenize=True, + ) + else: + first_prompt_token_ids = S.FIRST_PROMPT_TOKEN_IDS + second_prompt_token_ids = S.SECOND_PROMPT_TOKEN_IDS def make_routed_experts(prompt_token_ids, response_text): total_tokens = len(prompt_token_ids) + token_len(response_text) @@ -576,8 +598,8 @@ def make_routed_experts(prompt_token_ids, response_text): 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) + first_routed_experts = make_routed_experts(first_prompt_token_ids, S.FIRST_RESPONSE) + second_routed_experts = make_routed_experts(second_prompt_token_ids, S.SECOND_RESPONSE) def process_fn(prompt: str) -> ProcessResult: if prompt == S.FIRST_PROMPT: @@ -603,10 +625,10 @@ def process_fn(prompt: str) -> ProcessResult: assert result.requests[1]["messages"] == S.OPENAI_MESSAGES_SECOND_TURN_FROM_CLIENT for req in result.requests: assert req["logprobs"] is True - assert req["return_prompt_token_ids"] is True assert req["return_meta_info"] is True assert req["no_stop_trim"] is False assert req["return_routed_experts"] is True + assert "input_ids" in req else: assert result.requests == [ expected_request(S.FIRST_PROMPT_TOKEN_IDS, return_routed_experts=True), diff --git a/tests/fast/router/test_linear_trajectory.py b/tests/fast/router/test_linear_trajectory.py index e3245a6549f..b56c643f806 100644 --- a/tests/fast/router/test_linear_trajectory.py +++ b/tests/fast/router/test_linear_trajectory.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from types import SimpleNamespace from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest @@ -662,14 +662,13 @@ def test_returns_none_for_empty_token_ids(self, registry: SessionRegistry): session = registry.get_session(sid) assert registry.compute_session_mismatch(session) is None - @patch("miles.rollout.session.linear_trajectory.apply_chat_template") - def test_returns_empty_list_when_no_mismatch(self, mock_template, registry: SessionRegistry): + def test_returns_empty_list_when_no_mismatch(self, registry: SessionRegistry): sid = registry.create_session() session = registry.get_session(sid) session.update_pretokenized_state([SYS_MSG, USER_MSG], ASSISTANT_MSG_1, [1, 2, 3], [10, 11], max_trim_tokens=0) # Simulate: template returns same IDs as stored - mock_template.return_value = [1, 2, 3, 10, 11] + registry.tito_tokenizer.render_messages = MagicMock(return_value=[1, 2, 3, 10, 11]) # Need a real comparator; replace the None one mock_comparator = MagicMock() @@ -679,14 +678,19 @@ def test_returns_empty_list_when_no_mismatch(self, mock_template, registry: Sess result = registry.compute_session_mismatch(session) assert result == [] mock_comparator.compare_sequences.assert_called_once_with([1, 2, 3, 10, 11], [1, 2, 3, 10, 11]) + registry.tito_tokenizer.render_messages.assert_called_once_with( + session.messages, + tools=None, + add_generation_prompt=False, + tokenize=True, + ) - @patch("miles.rollout.session.linear_trajectory.apply_chat_template") - def test_returns_mismatch_dicts(self, mock_template, registry: SessionRegistry): + def test_returns_mismatch_dicts(self, registry: SessionRegistry): sid = registry.create_session() session = registry.get_session(sid) session.update_pretokenized_state([SYS_MSG, USER_MSG], ASSISTANT_MSG_1, [1, 2, 3], [10, 11], max_trim_tokens=0) - mock_template.return_value = [1, 2, 99, 10, 11] + registry.tito_tokenizer.render_messages = MagicMock(return_value=[1, 2, 99, 10, 11]) @dataclass class FakeMismatch: @@ -702,19 +706,17 @@ def to_dict(self): result = registry.compute_session_mismatch(session) assert result == [{"position": 2, "detail": "mismatch"}] - @patch("miles.rollout.session.linear_trajectory.apply_chat_template") - def test_raises_tokenization_error_on_exception(self, mock_template, registry: SessionRegistry): + def test_raises_tokenization_error_on_exception(self, registry: SessionRegistry): sid = registry.create_session() session = registry.get_session(sid) session.update_pretokenized_state([SYS_MSG, USER_MSG], ASSISTANT_MSG_1, [1, 2, 3], [10, 11], max_trim_tokens=0) - mock_template.side_effect = RuntimeError("tokenizer failed") + registry.tito_tokenizer.render_messages = MagicMock(side_effect=RuntimeError("tokenizer failed")) with pytest.raises(TokenizationError, match="tokenizer failed"): registry.compute_session_mismatch(session) - @patch("miles.rollout.session.linear_trajectory.apply_chat_template") - def test_uses_tools_from_last_record(self, mock_template, registry: SessionRegistry): + def test_uses_tools_from_last_record(self, registry: SessionRegistry): sid = registry.create_session() session = registry.get_session(sid) session.update_pretokenized_state([SYS_MSG, USER_MSG], ASSISTANT_MSG_1, [1, 2], [10], max_trim_tokens=0) @@ -730,13 +732,15 @@ def test_uses_tools_from_last_record(self, mock_template, registry: SessionRegis ) session.append_record(record) - mock_template.return_value = [1, 2, 10] + mock_tokenize = MagicMock(return_value=[1, 2, 10]) + registry.tito_tokenizer.render_messages = mock_tokenize mock_comparator = MagicMock() mock_comparator.compare_sequences.return_value = [] registry.comparator = mock_comparator registry.compute_session_mismatch(session) - # Verify tools were passed to apply_chat_template - _, kwargs = mock_template.call_args + # Verify tools were passed to the TITO renderer. + _, kwargs = mock_tokenize.call_args assert kwargs["tools"] == tools + assert kwargs["add_generation_prompt"] is False diff --git a/tests/fast/router/test_sessions.py b/tests/fast/router/test_sessions.py index 1486ccb5460..7fb33d775b4 100644 --- a/tests/fast/router/test_sessions.py +++ b/tests/fast/router/test_sessions.py @@ -45,6 +45,9 @@ def patched_chat_response(self, payload: dict) -> dict: miles_router_timeout=30, hf_checkpoint="Qwen/Qwen3-0.6B", chat_template_path=None, + apply_chat_template_kwargs={"enable_thinking": False}, + tito_model="default", + tito_allowed_append_roles=["tool"], trajectory_manager="linear_trajectory", session_server_instance_id=uuid.uuid4().hex, ) @@ -57,7 +60,7 @@ def patched_chat_response(self, payload: dict) -> dict: url = f"http://127.0.0.1:{port}" try: - yield SimpleNamespace(url=url) + yield SimpleNamespace(url=url, backend=backend) finally: server.stop() @@ -129,6 +132,8 @@ def test_proxy_chat_appends_record(self, router_env): body = resp.json() assert "choices" in body assert body["choices"] + assert isinstance(body["choices"][0]["prompt_token_ids"], list) + assert body["choices"][0]["prompt_token_ids"] get_resp = requests.get(f"{router_env.url}/sessions/{session_id}", timeout=5.0) records = get_resp.json()["records"] diff --git a/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py b/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py index 15714692c27..67b12c1ad62 100644 --- a/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py +++ b/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py @@ -389,7 +389,7 @@ def test_qwen3_tool_dummy_assistant_preserves_reasoning_shape(self): ) tool_messages = [SingleToolThinkingTrajectory.MESSAGES[3]] dummy_assistant = _build_dummy_assistant(tool_messages) - rendered = thinking_tito._render_messages( + rendered = thinking_tito.render_messages( [{"role": "system", "content": "dummy system"}, dummy_assistant], add_generation_prompt=False, tools=SingleToolThinkingTrajectory.TOOLS, diff --git a/tests/fast/utils/test_utils/test_mock_sglang_server.py b/tests/fast/utils/test_utils/test_mock_sglang_server.py index 24d3439fd95..a86fd9c2f25 100644 --- a/tests/fast/utils/test_utils/test_mock_sglang_server.py +++ b/tests/fast/utils/test_utils/test_mock_sglang_server.py @@ -248,6 +248,7 @@ def test_basic(self, mock_server): json={ "model": "test-model", "messages": messages, + "return_prompt_token_ids": True, }, timeout=5.0, ) @@ -288,6 +289,7 @@ def process_fn(_: str) -> ProcessResult: "model": "test", "messages": [{"role": "user", "content": "What year is it?"}], "tools": SAMPLE_TOOLS, + "return_prompt_token_ids": True, }, timeout=5.0, ) @@ -326,6 +328,7 @@ def process_fn(_: str) -> ProcessResult: "model": "test", "messages": [{"role": "user", "content": "What's the weather?"}], "tools": SAMPLE_TOOLS, + "return_prompt_token_ids": True, }, timeout=5.0, ) @@ -362,6 +365,7 @@ def process_fn(_: str) -> ProcessResult: "model": "test", "messages": [{"role": "user", "content": "What year and temperature?"}], "tools": SAMPLE_TOOLS, + "return_prompt_token_ids": True, }, timeout=5.0, ) From 34eab93b1d14f3502b0a2e0879c74aeea36139c7 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 5 May 2026 21:22:28 +0800 Subject: [PATCH 0012/1130] Add torchft dependency (#880) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 87e7d149cb6..14a539c18a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,6 @@ ray[default] ring_flash_attn sglang-router>=0.2.3 tensorboard +torchft-nightly==2026.4.3 transformers wandb From b4d30fc7e42f2fd58a06801d81ba3db260700740 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 5 May 2026 21:22:41 +0800 Subject: [PATCH 0013/1130] Add mechanical refactor verify skills (#897) --- .../mechanical-refactor-verify/SKILL.md | 134 ++++++++++++++++++ .../mechanical_refactor_verify_utils.py | 80 +++++++++++ .gitignore | 1 - 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/mechanical-refactor-verify/SKILL.md create mode 100644 .claude/skills/mechanical-refactor-verify/mechanical_refactor_verify_utils.py diff --git a/.claude/skills/mechanical-refactor-verify/SKILL.md b/.claude/skills/mechanical-refactor-verify/SKILL.md new file mode 100644 index 00000000000..98f9720d8ec --- /dev/null +++ b/.claude/skills/mechanical-refactor-verify/SKILL.md @@ -0,0 +1,134 @@ +--- +name: mechanical-refactor-verify +description: Verify mechanical refactoring commits by requiring a reproducible transform script (gist) in the PR description. Use when doing or reviewing file splits, function moves, or module extractions. +user_invocable: true +argument: "[verify ] — verify an existing PR, or omit to see the workflow guide" +--- + +# Mechanical Refactor — Reproducible Verification + +## Core Principle + +The deliverable of a mechanical move (file split, function move, module extraction) is NOT the diff — it is **the script that produces the diff**. +A script is auditable; a diff is not. + +## Workflow + +Regardless of who did the move (human or agent) and when (before or after committing), the workflow is the same: + +### Step 1: Write the transform script to /tmp/ + +Write the script to `/tmp/transform_.py`. **Never write it inside the repo.** + +The scaffold (worktree creation, diff check, ruff format, result reporting) lives in `mechanical_refactor_verify_utils.py` next to this skill. + +**MANDATORY**: The transform script MUST use `verify_mechanical_refactor()` from the utils module. Do NOT reimplement the verification scaffold — no hand-written worktree management, no hand-written diff checking. The script only defines `transform()` and calls `verify_mechanical_refactor`. + +Script template (follow this structure exactly): + +```python +#!/usr/bin/env python3 +"""Reproducible transform for: + +Run from the repo root: python3 /tmp/transform_.py +""" +import sys +from pathlib import Path + +sys.path.append(".claude/skills/mechanical-refactor-verify") +from mechanical_refactor_verify_utils import verify_mechanical_refactor, exec_command, git_add_and_commit, dedent + +BASE_COMMIT = "" +TARGET_COMMIT = "" + + +def transform(dir_root: Path) -> None: + """Perform the mechanical transformation and commit each step. + + Args: + dir_root: Path to the worktree (checked out at BASE_COMMIT). + """ + # --- Step 1: Split source file --- + source = dir_root / "path/to/source.py" + content = source.read_text() + lines = content.splitlines(keepends=True) + + splits = [ + ("path/to/pkg/target_a.py", 1, 50), + ("path/to/pkg/target_b.py", 51, 120), + ] + for target_path, start, end in splits: + target = dir_root / target_path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("".join(lines[start - 1 : end])) + + source.unlink() + (dir_root / "path/to/pkg/__init__.py").touch() + + git_add_and_commit("mechanical: split source.py", cwd=str(dir_root)) + + # --- Step 2: Fix imports --- + # + # git_add_and_commit("fix imports", cwd=str(dir_root)) + + # Note: pre-commit run --all-files is run automatically after transform() returns + + +if __name__ == "__main__": + verify_mechanical_refactor( + base_commit=BASE_COMMIT, + target_commit=TARGET_COMMIT, + transform=transform, + ) +``` + +### Step 2: Run the script from the repo root + +```bash +cd +python3 /tmp/transform_.py +# Expected: "PASS: transform reproduces the commit exactly." +``` + +If FAIL, fix the script and re-run until PASS. + +### Step 3: Upload gist, delete local file, update PR description + +One gist per PR. Do all three: + +```bash +# 1. Create gist (or update existing) +gh gist create --public -d "Mechanical refactor transform: " /tmp/transform_.py +# Or update: gh gist edit -a /tmp/transform_.py + +# 2. Delete local file +rm /tmp/transform_.py + +# 3. Update PR description (paste the block below) +``` + +PR description must include: + +````markdown +## Mechanical Move + +Transform script: + +### One-click verification + +```bash +python3 <(curl -sL ) +``` +```` + +### Step 4: PR scope + +A mechanical refactor PR must contain **only** mechanical changes (moves, splits, renames, import fixes, formatting). All of these must be reproducible by the transform script. + +Semantic changes (new logic, API restructuring, behavior changes) belong in a **separate PR**. + +## Verifying an existing PR (`/mechanical-refactor-verify verify`) + +1. Find the gist URL and one-click command in the PR description +2. Run the one-click command from the repo root +3. Report: PASS or show the diff diff --git a/.claude/skills/mechanical-refactor-verify/mechanical_refactor_verify_utils.py b/.claude/skills/mechanical-refactor-verify/mechanical_refactor_verify_utils.py new file mode 100644 index 00000000000..5896bcb8700 --- /dev/null +++ b/.claude/skills/mechanical-refactor-verify/mechanical_refactor_verify_utils.py @@ -0,0 +1,80 @@ +"""Utilities for mechanical refactor verification scripts. + +See SKILL.md for usage and transform script template. +""" + +import shlex +import subprocess +import sys +import tempfile +from collections.abc import Callable +from pathlib import Path + + +def exec_command(cmd: str, cwd: str | None = None, check: bool = True) -> str: + print(f" $ {cmd}", flush=True) + result = subprocess.run( + cmd, + shell=True, + cwd=cwd, + capture_output=True, + text=True, + ) + if check and result.returncode != 0: + print(f"FAILED: {result.stderr}", file=sys.stderr) + sys.exit(1) + return result.stdout.strip() + + +def git_add_and_commit(message: str, cwd: str) -> None: + exec_command(f"git add -A && git commit -m {shlex.quote(message)}", cwd=cwd) + + +def dedent(text: str, n: int) -> str: + """Remove exactly n leading spaces from each line.""" + lines = text.splitlines(keepends=True) + return "".join(line[n:] if line[:n] == " " * n else line for line in lines) + + +def verify_mechanical_refactor( + base_commit: str, + target_commit: str, + transform: "Callable[[Path], None]", +) -> None: + repo_root = exec_command("git rev-parse --show-toplevel") + worktree_dir = tempfile.mkdtemp(prefix="verify-mechanical-") + branch_name = f"verify-mechanical-{base_commit[:8]}" + + try: + print(f"[1/4] Creating worktree at {base_commit[:8]}...") + exec_command( + f"git worktree add -b {branch_name} {worktree_dir} {base_commit}", + cwd=repo_root, + ) + + print("[2/4] Running transformation...") + transform(Path(worktree_dir)) + + print("[3/4] Running pre-commit...") + exec_command("pre-commit run --all-files", cwd=worktree_dir, check=False) + if exec_command("git status --porcelain", cwd=worktree_dir): + git_add_and_commit("pre-commit fixes", cwd=worktree_dir) + + print(f"[4/4] Diffing against {target_commit[:8]}...") + diff = exec_command( + f"git diff {target_commit} -- .", + cwd=worktree_dir, + check=False, + ) + + if diff: + print(f"\nFAIL: diff is non-empty:\n{diff}") + sys.exit(1) + else: + print("\nPASS: transform reproduces the commit exactly.") + + finally: + print(f"\nWorktree left at: {worktree_dir}") + print(f"Branch: {branch_name}") + print("To clean up manually:") + print(f" git worktree remove {worktree_dir} && git branch -D {branch_name}") diff --git a/.gitignore b/.gitignore index da2e15c936c..d937b99965c 100644 --- a/.gitignore +++ b/.gitignore @@ -192,4 +192,3 @@ glm/ _examples_synced/ .env .DS_Store -.claude/ From 571972c31d9277ca55243ba983f32f69b9a6d548 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 5 May 2026 21:30:50 +0800 Subject: [PATCH 0014/1130] Forbid all mpu usage and use ParallelState instead (#874) Co-authored-by: Ethan (Yusheng) Su --- .pre-commit-config.yaml | 8 +++--- .../experimental/fsdp_utils/parallel.py | 3 +++ miles/backends/megatron_utils/ci_utils.py | 7 +++-- miles/backends/megatron_utils/initialize.py | 6 ++--- miles/backends/megatron_utils/lora_utils.py | 9 +++---- miles/backends/megatron_utils/model.py | 2 +- miles/backends/megatron_utils/parallel.py | 15 +++++++++++ .../megatron_utils/update_weight/common.py | 22 ++++++++-------- .../hf_weight_iterator_direct.py | 26 +++++++++---------- .../broadcast.py | 5 ++-- .../update_weight_from_distributed/mixin.py | 17 +++++------- .../p2p_transfer_utils.py | 8 +++--- .../update_weight_from_tensor.py | 5 ++-- miles/backends/training_utils/parallel.py | 3 +++ miles/utils/ppo_utils.py | 12 ++++----- 15 files changed, 80 insertions(+), 68 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4deb9158e30..157ad0a476a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,9 +47,9 @@ repos: - repo: local hooks: - - id: ban-mpu-get-data-parallel - name: Ban direct mpu.get_data_parallel_* calls + - id: ban-mpu-get + name: Ban direct mpu.get_* calls (use ParallelState) language: pygrep - entry: 'mpu\.get_data_parallel' + entry: 'mpu\.get_' types: [python] - exclude: '(backends/megatron_utils/parallel\.py|tools/convert_to_hf\.py)' + exclude: '(backends/megatron_utils/parallel\.py|tools/convert_to_hf\.py|miles_plugins/|examples/)' diff --git a/miles/backends/experimental/fsdp_utils/parallel.py b/miles/backends/experimental/fsdp_utils/parallel.py index fa444975bd4..acf97a1a49f 100644 --- a/miles/backends/experimental/fsdp_utils/parallel.py +++ b/miles/backends/experimental/fsdp_utils/parallel.py @@ -58,6 +58,9 @@ def create_fsdp_parallel_state(args: Namespace) -> ParallelState: size=1, group=dist.new_group([rank]), ), + pp=GroupInfo(rank=0, size=1, group=None), + ep=GroupInfo(rank=0, size=1, group=None), + etp=GroupInfo(rank=0, size=1, group=None), ) parallel_state.dp_mesh = mesh["dp"] diff --git a/miles/backends/megatron_utils/ci_utils.py b/miles/backends/megatron_utils/ci_utils.py index c0f3c2d3f79..3ecf15eab0f 100644 --- a/miles/backends/megatron_utils/ci_utils.py +++ b/miles/backends/megatron_utils/ci_utils.py @@ -8,7 +8,6 @@ from pathlib import Path import torch -from megatron.core import parallel_state as mpu from megatron.core.distributed import DistributedDataParallel as DDP from miles.backends.training_utils.parallel import get_parallel_state @@ -61,10 +60,10 @@ def compute_model_hashes_by_layer(model: Sequence[DDP]) -> dict[str, str]: def _hash_file_path(base_dir: str | Path, iteration: int) -> Path: - tp_rank = mpu.get_tensor_model_parallel_rank() - pp_rank = mpu.get_pipeline_model_parallel_rank() + tp_rank = get_parallel_state().tp.rank + pp_rank = get_parallel_state().pp.rank dp_rank = get_parallel_state().intra_dp_cp.rank - cp_rank = mpu.get_context_parallel_rank() + cp_rank = get_parallel_state().cp.rank base = Path(base_dir) iter_dir = base if base.name.startswith("iter_") else base / f"iter_{int(iteration):07d}" return iter_dir / f"model_hash_tp{tp_rank}_pp{pp_rank}_dp{dp_rank}_cp{cp_rank}.json" diff --git a/miles/backends/megatron_utils/initialize.py b/miles/backends/megatron_utils/initialize.py index 4b7abf696c8..dd3415c0ae5 100644 --- a/miles/backends/megatron_utils/initialize.py +++ b/miles/backends/megatron_utils/initialize.py @@ -24,7 +24,7 @@ def _set_random_seed( ): """Set random seed for reproducability.""" # Ensure that different pipeline MP stages get different seeds. - seed = seed_ + (100 * mpu.get_pipeline_model_parallel_rank()) + seed = seed_ + (100 * get_parallel_state().pp.rank) # Ensure different data parallel ranks get different seeds if data_parallel_random_init: seed = seed + (10 * get_parallel_state().intra_dp.rank) @@ -114,6 +114,6 @@ def init(args): def is_megatron_main_rank(): return ( get_parallel_state().intra_dp_cp.rank == 0 - and mpu.get_tensor_model_parallel_rank() == 0 - and mpu.get_pipeline_model_parallel_rank() == mpu.get_pipeline_model_parallel_world_size() - 1 + and get_parallel_state().tp.rank == 0 + and get_parallel_state().pp.rank == get_parallel_state().pp.size - 1 ) diff --git a/miles/backends/megatron_utils/lora_utils.py b/miles/backends/megatron_utils/lora_utils.py index 2c48c08fd05..1a78cad63a2 100644 --- a/miles/backends/megatron_utils/lora_utils.py +++ b/miles/backends/megatron_utils/lora_utils.py @@ -9,7 +9,6 @@ import torch import torch.distributed as dist -from megatron.core import mpu from miles.backends.training_utils.parallel import get_parallel_state @@ -309,8 +308,8 @@ def save_lora_checkpoint( save_path = Path(save_dir) is_dp_rank_0 = get_parallel_state().intra_dp.rank == 0 - tp_rank = mpu.get_tensor_model_parallel_rank() - pp_rank = mpu.get_pipeline_model_parallel_rank() + tp_rank = get_parallel_state().tp.rank + pp_rank = get_parallel_state().pp.rank # Create directory on dp_rank=0, then synchronize if is_dp_rank_0: @@ -420,8 +419,8 @@ def load_lora_adapter( logger.warning(f"LoRA adapter path does not exist: {adapter_dir}") return False, None - tp_rank = mpu.get_tensor_model_parallel_rank() - pp_rank = mpu.get_pipeline_model_parallel_rank() + tp_rank = get_parallel_state().tp.rank + pp_rank = get_parallel_state().pp.rank # ---- Try Megatron-native format first (fast, no conversion needed) ---- native_path = adapter_dir / f"adapter_megatron_tp{tp_rank}_pp{pp_rank}.pt" diff --git a/miles/backends/megatron_utils/model.py b/miles/backends/megatron_utils/model.py index fdcd4038a84..5d2503dab17 100644 --- a/miles/backends/megatron_utils/model.py +++ b/miles/backends/megatron_utils/model.py @@ -750,7 +750,7 @@ def save_hf_model(args, rollout_id: int, model: Sequence[DDP]) -> None: model (Sequence[DDP]): Sequence of DDP-wrapped model chunks. rollout_id (int): Rollout ID for path formatting. """ - should_log = get_parallel_state().intra_dp_cp.rank == 0 and mpu.get_tensor_model_parallel_rank() == 0 + should_log = get_parallel_state().intra_dp_cp.rank == 0 and get_parallel_state().tp.rank == 0 try: from megatron.bridge import AutoBridge diff --git a/miles/backends/megatron_utils/parallel.py b/miles/backends/megatron_utils/parallel.py index fc8333f42c3..f4c9c6cad6e 100644 --- a/miles/backends/megatron_utils/parallel.py +++ b/miles/backends/megatron_utils/parallel.py @@ -37,6 +37,21 @@ def _create_intra_dp(with_context_parallel: bool): size=mpu.get_tensor_model_parallel_world_size(), group=mpu.get_tensor_model_parallel_group(), ), + pp=GroupInfo( + rank=mpu.get_pipeline_model_parallel_rank(), + size=mpu.get_pipeline_model_parallel_world_size(), + group=mpu.get_pipeline_model_parallel_group(), + ), + ep=GroupInfo( + rank=mpu.get_expert_model_parallel_rank(), + size=mpu.get_expert_model_parallel_world_size(), + group=mpu.get_expert_model_parallel_group(), + ), + etp=GroupInfo( + rank=mpu.get_expert_tensor_parallel_rank(), + size=mpu.get_expert_tensor_parallel_world_size(), + group=mpu.get_expert_tensor_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, diff --git a/miles/backends/megatron_utils/update_weight/common.py b/miles/backends/megatron_utils/update_weight/common.py index c61f2648766..677f93c6273 100644 --- a/miles/backends/megatron_utils/update_weight/common.py +++ b/miles/backends/megatron_utils/update_weight/common.py @@ -7,11 +7,11 @@ import ray import torch import torch.distributed as dist -from megatron.core import mpu from megatron.core.transformer.transformer_layer import get_transformer_layer_offset from ray.actor import ActorHandle from miles.backends.megatron_utils.misc_utils import strip_param_name_prefix +from miles.backends.training_utils.parallel import get_parallel_state from miles.utils.types import ParamInfo logger = logging.getLogger(__name__) @@ -60,11 +60,11 @@ def all_gather_param(args: Namespace, name: str, param: torch.nn.Parameter) -> t return param.data if ".experts." in name: - tp_size = mpu.get_expert_tensor_parallel_world_size() - tp_group = mpu.get_expert_tensor_parallel_group() + tp_size = get_parallel_state().etp.size + tp_group = get_parallel_state().etp.group else: - tp_size = mpu.get_tensor_model_parallel_world_size() - tp_group = mpu.get_tensor_model_parallel_group() + tp_size = get_parallel_state().tp.size + tp_group = get_parallel_state().tp.group param_partitions = [torch.empty_like(param.data) for _ in range(tp_size)] dist.all_gather(param_partitions, param.data, group=tp_group) @@ -100,11 +100,11 @@ def all_gather_params_async( else: # Start async all_gather if ".experts." in info.name: - tp_size = mpu.get_expert_tensor_parallel_world_size() - tp_group = mpu.get_expert_tensor_parallel_group() + tp_size = get_parallel_state().etp.size + tp_group = get_parallel_state().etp.group else: - tp_size = mpu.get_tensor_model_parallel_world_size() - tp_group = mpu.get_tensor_model_parallel_group() + tp_size = get_parallel_state().tp.size + tp_group = get_parallel_state().tp.group param_partitions = [torch.empty_like(param.data) for _ in range(tp_size)] handle = dist.all_gather(param_partitions, param.data, group=tp_group, async_op=True) @@ -183,8 +183,8 @@ def _named_params_and_buffers_global( Yield (global_name, param/buffer) with consistent names across PP/EP. Adjusts indices for virtual PP + EP offsets. Handles decoder.layers, mtp.layers (Multi-Token Prediction), expert_bias. """ - ep_size = mpu.get_expert_model_parallel_world_size() - ep_rank = mpu.get_expert_model_parallel_rank() + ep_size = get_parallel_state().ep.size + ep_rank = get_parallel_state().ep.rank if args.num_experts: expert_offset = ep_rank * args.num_experts // ep_size diff --git a/miles/backends/megatron_utils/update_weight/hf_weight_iterator_direct.py b/miles/backends/megatron_utils/update_weight/hf_weight_iterator_direct.py index 4e97d6c3003..cf281367b66 100644 --- a/miles/backends/megatron_utils/update_weight/hf_weight_iterator_direct.py +++ b/miles/backends/megatron_utils/update_weight/hf_weight_iterator_direct.py @@ -4,9 +4,9 @@ import torch import torch.distributed as dist -from megatron.core import mpu from tqdm import tqdm +from miles.backends.training_utils.parallel import get_parallel_state from miles.utils.distributed_utils import get_gloo_group from miles.utils.types import ParamInfo @@ -49,8 +49,8 @@ def _get_megatron_full_params( megatron_local_weights, ) -> Sequence[torch.Tensor]: monkey_patch_torch_reductions() - pp_size = mpu.get_pipeline_model_parallel_world_size() - ep_size = mpu.get_expert_model_parallel_world_size() + pp_size = get_parallel_state().pp.size + ep_size = get_parallel_state().ep.size rank = dist.get_rank() # init params: params = [] @@ -70,10 +70,10 @@ def _get_megatron_full_params( if pp_size > 1: handles = [] for info, param in zip(megatron_local_param_infos, params, strict=False): - if info.src_rank in dist.get_process_group_ranks(mpu.get_pipeline_model_parallel_group()): + if info.src_rank in dist.get_process_group_ranks(get_parallel_state().pp.group): handles.append( torch.distributed.broadcast( - param, src=info.src_rank, group=mpu.get_pipeline_model_parallel_group(), async_op=True + param, src=info.src_rank, group=get_parallel_state().pp.group, async_op=True ) ) for handle in handles: @@ -86,12 +86,12 @@ def _get_megatron_full_params( if ".experts." in info.name: src_rank = ( info.src_rank - if info.src_rank in dist.get_process_group_ranks(mpu.get_expert_model_parallel_group()) + if info.src_rank in dist.get_process_group_ranks(get_parallel_state().ep.group) else rank ) handles.append( torch.distributed.broadcast( - param, src=src_rank, group=mpu.get_expert_model_parallel_group(), async_op=True + param, src=src_rank, group=get_parallel_state().ep.group, async_op=True ) ) for handle in handles: @@ -119,9 +119,9 @@ def _get_megatron_local_param_info_buckets(args: Namespace, model: Sequence[torc for info in param_infos: # Expert params use expert-TP size, others use regular-TP size if ".experts." in info.name: - tp_size = mpu.get_expert_tensor_parallel_world_size() + tp_size = get_parallel_state().etp.size else: - tp_size = mpu.get_tensor_model_parallel_world_size() + tp_size = get_parallel_state().tp.size # Full param size = shard size × TP replicas (all-gather will reconstruct full param) param_size = info.size * tp_size @@ -143,8 +143,8 @@ def _get_megatron_local_param_infos(args: Namespace, model: Sequence[torch.nn.Mo Build global param metadata: collect → exchange PP/EP → resolve duplicates (MTP virtual PP) by min src_rank → validate. Returns sorted ParamInfo identical across all ranks. """ - pp_size = mpu.get_pipeline_model_parallel_world_size() - ep_size = mpu.get_expert_model_parallel_world_size() + pp_size = get_parallel_state().pp.size + ep_size = get_parallel_state().ep.size param_infos = {} rank = dist.get_rank() @@ -166,7 +166,7 @@ def _get_megatron_local_param_infos(args: Namespace, model: Sequence[torch.nn.Mo if pp_size > 1: param_infos_list = [None] * pp_size dist.all_gather_object( - obj=(rank, param_infos), object_list=param_infos_list, group=mpu.get_pipeline_model_parallel_group() + obj=(rank, param_infos), object_list=param_infos_list, group=get_parallel_state().pp.group ) for src_rank, infos in param_infos_list: if src_rank == rank: @@ -183,7 +183,7 @@ def _get_megatron_local_param_infos(args: Namespace, model: Sequence[torch.nn.Mo if ep_size > 1: param_infos_list = [None] * ep_size dist.all_gather_object( - obj=(rank, param_infos), object_list=param_infos_list, group=mpu.get_expert_model_parallel_group() + obj=(rank, param_infos), object_list=param_infos_list, group=get_parallel_state().ep.group ) for src_rank, infos in param_infos_list: for name, info in infos.items(): diff --git a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/broadcast.py b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/broadcast.py index 21706ce6983..51afedac4e1 100644 --- a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/broadcast.py +++ b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/broadcast.py @@ -6,7 +6,6 @@ import ray import torch import torch.distributed as dist -from megatron.core import mpu from ray import ObjectRef from ray.actor import ActorHandle from tqdm import tqdm @@ -60,7 +59,7 @@ def connect_rollout_engines( # For TP: # 1. AllGather parameters to rank 0 # 2. Broadcast parameters from rank 0 to all sglang engines - pp_rank = mpu.get_pipeline_model_parallel_rank() + pp_rank = get_parallel_state().pp.rank if self._is_source: self._group_name = f"miles-pp_{pp_rank}" @@ -76,7 +75,7 @@ def connect_rollout_engines( @property def _is_source(self): """If it's the source gpu that broadcasting weights to rollout side""" - return get_parallel_state().intra_dp_cp.rank == 0 and mpu.get_tensor_model_parallel_rank() == 0 + return get_parallel_state().intra_dp_cp.rank == 0 and get_parallel_state().tp.rank == 0 def _update_weight_implementation( self, converted_named_tensors: list[tuple[str, torch.Tensor]], pbar: tqdm | None = None diff --git a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/mixin.py b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/mixin.py index 62d410ac653..4ebf995b726 100644 --- a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/mixin.py +++ b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/mixin.py @@ -3,10 +3,9 @@ import ray import torch import torch.distributed as dist -from megatron.core import mpu - from tqdm import tqdm +from miles.backends.training_utils.parallel import get_parallel_state from miles.utils.distributed_utils import get_gloo_group from miles.utils.timer import timer @@ -80,7 +79,7 @@ def _gather_and_update_expert_weights( param_size = param.numel() * param.element_size() if ( buffer_size + param_size - ) * mpu.get_expert_model_parallel_world_size() > self.args.update_weight_buffer_size and named_tensors: + ) * get_parallel_state().ep.size > self.args.update_weight_buffer_size and named_tensors: self._update_expert_bucket_weights(named_tensors, update_bucket_weight_func, pbar) named_tensors = [] buffer_size = 0 @@ -101,24 +100,22 @@ def _update_expert_bucket_weights( Gather EP → HF → update weights. Clears buffer. """ names = [name for name, _ in named_tensors] - all_names: list[list[str] | None] = [None] * mpu.get_expert_model_parallel_world_size() - dist.all_gather_object(all_names, names, group=mpu.get_expert_model_parallel_group()) + all_names: list[list[str] | None] = [None] * get_parallel_state().ep.size + dist.all_gather_object(all_names, names, group=get_parallel_state().ep.group) for ep_names in all_names: assert len(named_tensors) == len( ep_names ), f"mismatch names length: {len(named_tensors)} != {len(ep_names)}" - all_gathered_params: list[list[tuple[str, torch.Tensor]]] = [ - [] for _ in range(mpu.get_expert_model_parallel_world_size()) - ] + all_gathered_params: list[list[tuple[str, torch.Tensor]]] = [[] for _ in range(get_parallel_state().ep.size)] handles = [] for i, (_name, param) in enumerate(named_tensors): params = [ torch.empty_like(param.data, device=torch.cuda.current_device()) - for _ in range(mpu.get_expert_model_parallel_world_size()) + for _ in range(get_parallel_state().ep.size) ] - handle = dist.all_gather(params, param.data, group=mpu.get_expert_model_parallel_group(), async_op=True) + handle = dist.all_gather(params, param.data, group=get_parallel_state().ep.group, async_op=True) handles.append(handle) for ep_rank, ep_names in enumerate(all_names): all_gathered_params[ep_rank].append((ep_names[i], params[ep_rank])) diff --git a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p_transfer_utils.py b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p_transfer_utils.py index 804d73ce043..5adbfaa293d 100644 --- a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p_transfer_utils.py +++ b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p_transfer_utils.py @@ -8,10 +8,10 @@ import ray import torch import torch.distributed as dist -from megatron.core import mpu from mooncake.engine import TransferEngine from ray.actor import ActorHandle from sglang.srt.server_args import ServerArgs +from miles.backends.training_utils.parallel import get_parallel_state logger = logging.getLogger(__name__) @@ -45,13 +45,13 @@ def _get_parallelism(self, args: Namespace) -> None: after bucketed all-gather across TP/EP/ETP dims. The size of this group is gathered_dp_size, and each rank has a unique gathered_dp_rank in [0, gathered_dp_size). """ - self._pp_rank = mpu.get_pipeline_model_parallel_rank() - self._pp_size = mpu.get_pipeline_model_parallel_world_size() + self._pp_rank = get_parallel_state().pp.rank + self._pp_size = get_parallel_state().pp.size world_size = dist.get_world_size() self._gathered_dp_size = world_size // self._pp_size - my_pp_group = dist.get_process_group_ranks(mpu.get_pipeline_model_parallel_group()) + my_pp_group = dist.get_process_group_ranks(get_parallel_state().pp.group) my_column_id = min(my_pp_group) all_column_ids = [None] * world_size dist.all_gather_object(all_column_ids, my_column_id) 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 e045b5f278e..82a35332916 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 @@ -6,7 +6,6 @@ import ray import torch import torch.distributed as dist -from megatron.core import mpu from ray import ObjectRef from ray.actor import ActorHandle @@ -116,8 +115,8 @@ def connect_rollout_engines( distributed_gpu_counts = engine_gpu_counts[colocate_engine_nums:] self._is_distributed_src_rank = ( get_parallel_state().intra_dp_cp.rank == 0 - and mpu.get_tensor_model_parallel_rank() == 0 - and mpu.get_pipeline_model_parallel_rank() == 0 + and get_parallel_state().tp.rank == 0 + and get_parallel_state().pp.rank == 0 ) self._group_name = "miles" if self._is_distributed_src_rank: diff --git a/miles/backends/training_utils/parallel.py b/miles/backends/training_utils/parallel.py index a989b759923..75032684b91 100644 --- a/miles/backends/training_utils/parallel.py +++ b/miles/backends/training_utils/parallel.py @@ -53,6 +53,9 @@ class ParallelState: intra_dp_cp: GroupInfo cp: GroupInfo tp: GroupInfo + pp: GroupInfo + ep: GroupInfo + etp: GroupInfo 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/ppo_utils.py b/miles/utils/ppo_utils.py index 18fb2681a23..4c09dc50816 100644 --- a/miles/utils/ppo_utils.py +++ b/miles/utils/ppo_utils.py @@ -7,6 +7,8 @@ import torch.distributed as dist import torch.nn.functional as F +from miles.backends.training_utils.parallel import get_parallel_state + @torch.compile(dynamic=True) def compute_approx_kl( @@ -233,9 +235,8 @@ def get_reinforce_plus_plus_returns( List[torch.Tensor]: A list of return (G_t) tensors for the local sequence chunks owned by the current GPU rank. """ - from megatron.core import mpu - cp_size = mpu.get_context_parallel_world_size() + cp_size = get_parallel_state().cp.size final_returns_chunks = [] for i in range(len(rewards)): @@ -335,9 +336,8 @@ def get_advantages_and_returns( - advantages: Tensor of shape (response_size,) - returns: Tensor of shape (response_size,) """ - from megatron.core import mpu - cp_size = mpu.get_context_parallel_world_size() + cp_size = get_parallel_state().cp.size if cp_size > 1: from miles.backends.training_utils.cp_utils import all_gather_with_cp @@ -391,14 +391,12 @@ def get_advantages_and_returns_batch( returns_list: list[Tensor], same shape """ - from megatron.core import mpu - with torch.no_grad(): B = len(response_lengths) assert B == len(values_list) assert B == len(rewards_list) - cp_size = mpu.get_context_parallel_world_size() + cp_size = get_parallel_state().cp.size device = values_list[0].device dtype = values_list[0].dtype From c8cf9c3ce19c1ff50194a37984ca76a5ad595865 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 5 May 2026 23:11:20 +0800 Subject: [PATCH 0015/1130] fix: skip linux-only deps on macOS in requirements.txt (#1074) --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 14a539c18a8..1d7f099e22a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,9 +12,9 @@ pytest-asyncio pyyaml qwen_vl_utils # for VLM ray[default] -ring_flash_attn +ring_flash_attn; platform_system == "Linux" sglang-router>=0.2.3 tensorboard -torchft-nightly==2026.4.3 +torchft-nightly==2026.4.3; platform_system == "Linux" transformers wandb From 4f4ee5a86076c589cc683166dd27558abc69d80d Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 5 May 2026 23:11:40 +0800 Subject: [PATCH 0016/1130] refactor: mark create_cpu_replica as private (#1075) --- .../update_weight/update_weight_from_distributed/p2p.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p.py b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p.py index b74f72e7218..e5c33cf3145 100644 --- a/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p.py +++ b/miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p.py @@ -224,7 +224,7 @@ def connect_rollout_engines( ) server_args = self.session_id_to_server_args[session_id] - model_replica = self.create_cpu_replica( + model_replica = self._create_cpu_replica( parallelism_config, self.args.hf_checkpoint, server_args, @@ -247,7 +247,7 @@ def connect_rollout_engines( self._transfer_engine_meta_list.append((model_replica, remote_infos)) - def create_cpu_replica( + def _create_cpu_replica( self, parallelism_config: RankParallelismConfig, model_path: str, From bad1691e2b70b7ba9cb8481fd844100f1b62bd44 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Tue, 5 May 2026 23:11:48 +0800 Subject: [PATCH 0017/1130] ci: trigger PR Test on any base branch, not just main (#1076) --- .github/workflows/pr-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 75f5b7bc227..15aa7bda441 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -2,7 +2,6 @@ name: PR Test on: pull_request: - branches: [main] types: [synchronize, labeled] workflow_dispatch: inputs: From 507fb3183936421f0756170ad66d39328591b2c3 Mon Sep 17 00:00:00 2001 From: mouad-hpc Date: Tue, 5 May 2026 10:18:33 -0700 Subject: [PATCH 0018/1130] [Feature] Add modular tracking interface with MLflow backend (#613) Co-authored-by: Zhichenzzz --- miles/utils/arguments.py | 25 +++ miles/utils/tracking_utils.py | 37 ---- miles/utils/tracking_utils/__init__.py | 19 +++ miles/utils/tracking_utils/base.py | 158 ++++++++++++++++++ miles/utils/tracking_utils/mlflow_utils.py | 136 +++++++++++++++ .../{ => tracking_utils}/prometheus_utils.py | 0 .../{ => tracking_utils}/tensorboard_utils.py | 0 .../utils/{ => tracking_utils}/wandb_utils.py | 0 setup.py | 5 +- train.py | 7 +- train_async.py | 7 +- 11 files changed, 352 insertions(+), 42 deletions(-) delete mode 100644 miles/utils/tracking_utils.py create mode 100644 miles/utils/tracking_utils/__init__.py create mode 100644 miles/utils/tracking_utils/base.py create mode 100644 miles/utils/tracking_utils/mlflow_utils.py rename miles/utils/{ => tracking_utils}/prometheus_utils.py (100%) rename miles/utils/{ => tracking_utils}/tensorboard_utils.py (100%) rename miles/utils/{ => tracking_utils}/wandb_utils.py (100%) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 695d466ffd1..abfa137ac9a 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1222,6 +1222,30 @@ def add_wandb_arguments(parser): parser.add_argument("--wandb-run-id", type=str, default=None) return parser + # mlflow + def add_mlflow_arguments(parser): + parser.add_argument("--use-mlflow", action="store_true", default=False) + parser.add_argument( + "--mlflow-tracking-uri", + type=str, + default=None, + help="MLflow tracking server URI. Defaults to MLFLOW_TRACKING_URI env var, or local mlruns/ directory.", + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="miles", + help="MLflow experiment name.", + ) + parser.add_argument( + "--mlflow-run-name", + type=str, + default=None, + help="MLflow run name. Defaults to --wandb-group if not set.", + ) + parser.add_argument("--mlflow-run-id", type=str, default=None) + return parser + # tensorboard def add_tensorboard_arguments(parser): # tb_project_name, tb_experiment_name @@ -1697,6 +1721,7 @@ def add_sglang_tp_size(): parser = add_algo_arguments(parser) parser = add_lora_arguments(parser) parser = add_wandb_arguments(parser) + parser = add_mlflow_arguments(parser) parser = add_tensorboard_arguments(parser) parser = add_prometheus_arguments(parser) parser = add_router_arguments(parser) diff --git a/miles/utils/tracking_utils.py b/miles/utils/tracking_utils.py deleted file mode 100644 index 88e5b8024f3..00000000000 --- a/miles/utils/tracking_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging - -import wandb -from miles.utils.tensorboard_utils import _TensorboardAdapter - -from . import wandb_utils -from .prometheus_utils import get_prometheus, init_prometheus - -logger = logging.getLogger(__name__) - - -def init_tracking(args, primary: bool = True, **kwargs): - if primary: - wandb_utils.init_wandb_primary(args, **kwargs) - else: - wandb_utils.init_wandb_secondary(args, **kwargs) - - if args.use_prometheus: - init_prometheus(args, start_server=primary) - - -# TODO further refactor, e.g. put TensorBoard init to the "init" part -def log(args, metrics, step_key: str): - if args.use_wandb: - wandb.log(metrics) - - if args.use_tensorboard: - metrics_except_step = {k: v for k, v in metrics.items() if k != step_key} - _TensorboardAdapter(args).log(data=metrics_except_step, step=metrics[step_key]) - - if args.use_prometheus: - prom = get_prometheus() - assert prom is not None, ( - "Prometheus collector is not initialized; ensure init_tracking(..., primary=...) ran on the " - "driver and workers can resolve the miles_prometheus_collector Ray actor." - ) - prom.update.remote(metrics) diff --git a/miles/utils/tracking_utils/__init__.py b/miles/utils/tracking_utils/__init__.py new file mode 100644 index 00000000000..7453ad2f5f8 --- /dev/null +++ b/miles/utils/tracking_utils/__init__.py @@ -0,0 +1,19 @@ +import logging + +from .base import TrackingManager + +logger = logging.getLogger(__name__) +_manager = TrackingManager() + + +def init_tracking(args, primary: bool = True, **kwargs): + _manager.init(args, primary=primary, **kwargs) + + +def log(args, metrics, step_key: str): + step = metrics.get(step_key) + _manager.log(metrics, step=step) + + +def finish_tracking(): + _manager.finish() diff --git a/miles/utils/tracking_utils/base.py b/miles/utils/tracking_utils/base.py new file mode 100644 index 00000000000..2faa9959f9b --- /dev/null +++ b/miles/utils/tracking_utils/base.py @@ -0,0 +1,158 @@ +""" +Shared tracking interface for experiment logging backends. + +Each backend implements ``init / log / finish``, and :class:`TrackingManager` fans out +calls to every active backend. + +To add a new backend: +-------------------- +1. Subclass :class:`TrackingBackend`. +2. Register it in :data:`BACKEND_REGISTRY`. +3. Add a corresponding ``--use-`` CLI flag in ``arguments.py``. +""" + +from __future__ import annotations + +import logging +from abc import ABC, abstractmethod +from typing import Any + +logger = logging.getLogger(__name__) + + +class TrackingBackend(ABC): + # Interface every logging backend must satisfy. + + @abstractmethod + def init(self, args, *, primary: bool = True, **kwargs) -> None: ... + + @abstractmethod + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: ... + + @abstractmethod + def finish(self) -> None: ... + + +# Thin adapters for backwards compatibility to keep wandb_utils and tensorboard_utils untouched. +class WandbBackend(TrackingBackend): + # Delegates to the existing ``wandb_utils`` helpers. + + def init(self, args, *, primary: bool = True, **kwargs) -> None: + from . import wandb_utils + + if primary: + wandb_utils.init_wandb_primary(args, **kwargs) + else: + wandb_utils.init_wandb_secondary(args, **kwargs) + + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: + import wandb + + wandb.log(metrics) + + def finish(self) -> None: + import wandb + + wandb.finish() + + +class TensorboardBackend(TrackingBackend): + def __init__(self) -> None: + self._adapter = None + + def init(self, args, *, primary: bool = True, **kwargs) -> None: + from .tensorboard_utils import _TensorboardAdapter + + self._adapter = _TensorboardAdapter(args) + + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: + if self._adapter is not None: + # Strip step-key entries (e.g. "train/step", "rollout/step") — + # tensorboard receives step as an explicit argument instead. + data = {k: v for k, v in metrics.items() if not k.endswith("/step")} + self._adapter.log(data=data, step=step) + + def finish(self) -> None: + if self._adapter is not None: + self._adapter.finish() + + +class MlflowBackend(TrackingBackend): + + def init(self, args, *, primary: bool = True, **kwargs) -> None: + from . import mlflow_utils + + mlflow_utils.init_mlflow(args, primary=primary, **kwargs) + + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: + from . import mlflow_utils + + mlflow_utils.log_metrics(metrics, step=step) + + def finish(self) -> None: + from . import mlflow_utils + + mlflow_utils.finish() + + +class PrometheusBackend(TrackingBackend): + # Wraps the existing Ray-actor based prometheus collector. The actor lifetime is + # tied to the Ray job, so finish() is intentionally a no-op. + + def init(self, args, *, primary: bool = True, **kwargs) -> None: + from .prometheus_utils import init_prometheus + + init_prometheus(args, start_server=primary) + + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: + from .prometheus_utils import get_prometheus + + prom = get_prometheus() + assert prom is not None, ( + "Prometheus collector is not initialized; ensure init_tracking(..., primary=...) ran on the " + "driver and workers can resolve the miles_prometheus_collector Ray actor." + ) + prom.update.remote(metrics) + + def finish(self) -> None: + return + + +# Registry that maps backend name → (class, args-flag attribute) + +BACKEND_REGISTRY: dict[str, tuple[type[TrackingBackend], str]] = { + "wandb": (WandbBackend, "use_wandb"), + "tensorboard": (TensorboardBackend, "use_tensorboard"), + "mlflow": (MlflowBackend, "use_mlflow"), + "prometheus": (PrometheusBackend, "use_prometheus"), +} + + +class TrackingManager: + # Initializes and logs to every enabled backend; used internally by ``tracking_utils``. + + def __init__(self) -> None: + self._backends: list[TrackingBackend] = [] + + def init(self, args, *, primary: bool = True, **kwargs) -> None: + for name, (cls, flag) in BACKEND_REGISTRY.items(): + if getattr(args, flag, False): + logger.info("Initialising tracking backend: %s", name) + backend = cls() + backend.init(args, primary=primary, **kwargs) + self._backends.append(backend) + + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: + for backend in self._backends: + backend.log(metrics, step=step) + + def finish(self) -> None: + for backend in self._backends: + try: + backend.finish() + except Exception: + logger.exception( + "Error finishing tracking backend %s", + type(backend).__name__, + ) + self._backends.clear() diff --git a/miles/utils/tracking_utils/mlflow_utils.py b/miles/utils/tracking_utils/mlflow_utils.py new file mode 100644 index 00000000000..a2a372542b7 --- /dev/null +++ b/miles/utils/tracking_utils/mlflow_utils.py @@ -0,0 +1,136 @@ +""" +MLflow tracking backend for miles. + +MLflow docs for future reference: + - Tracking overview : https://mlflow.org/docs/latest/ml/tracking/ + - Python API : https://mlflow.org/docs/latest/python_api/mlflow.html + - Remote tracking : https://mlflow.org/docs/latest/tracking/server.html +""" + +from __future__ import annotations + +import logging +import os +import re +from copy import deepcopy +from typing import Any + +logger = logging.getLogger(__name__) + + +# Helpers/utils +def _sanitize_key(key: str) -> str: + return re.sub(r"[^a-zA-Z0-9_\-./\s]", "_", key) + + +def _compute_config_for_logging(args) -> dict[str, str]: + # Build a flat param dict from *args*, mirroring ``wandb_utils._compute_config_for_logging``. + raw = deepcopy(args.__dict__) + + whitelist_env_vars = ["SLURM_JOB_ID"] + raw["env_vars"] = {k: v for k, v in os.environ.items() if k in whitelist_env_vars} + + return _flatten_dict(raw) + + +def _flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> dict[str, str]: + # Recursively flatten nested dicts into ``dotted.key`` → ``str(value)`` pairs. + items: list[tuple[str, str]] = [] + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.extend(_flatten_dict(v, new_key, sep).items()) + else: + items.append((new_key, str(v))) + return dict(items) + + +def init_mlflow(args, *, primary: bool = True, **kwargs) -> None: + if not args.use_mlflow: + args.mlflow_run_id = None + return + + import mlflow + + tracking_uri = args.mlflow_tracking_uri or os.environ.get("MLFLOW_TRACKING_URI") + if tracking_uri: + mlflow.set_tracking_uri(tracking_uri) + logger.info("MLflow tracking URI: %s", tracking_uri) + + experiment_name = args.mlflow_experiment_name + mlflow.set_experiment(experiment_name) + + if primary: + _init_mlflow_primary(args, experiment_name) + else: + _init_mlflow_secondary(args) + + +def _init_mlflow_primary(args, experiment_name: str) -> None: + import mlflow + + run_name = args.mlflow_run_name or args.wandb_group + + tags = {} + slurm_job_id = os.environ.get("SLURM_JOB_ID") + if slurm_job_id: + tags["slurm_job_id"] = slurm_job_id + tags["rank"] = str(args.rank) + + run = mlflow.start_run(run_name=run_name, tags=tags) + mlflow.log_params(_compute_config_for_logging(args)) + + args.mlflow_run_id = run.info.run_id + logger.info("MLflow run started: %s (experiment=%s, name=%s)", run.info.run_id, experiment_name, run_name) + + +def _init_mlflow_secondary(args) -> None: + """Attach to an existing MLflow run created by the primary rank.""" + import mlflow + + run_id = args.mlflow_run_id or os.environ.get("MLFLOW_RUN_ID") + if run_id is None: + return + + mlflow.start_run(run_id=run_id) + logger.info("MLflow secondary attached to run: %s", run_id) + + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + + +def log_metrics(metrics: dict[str, Any], step: int | None = None) -> None: + import mlflow + + if mlflow.active_run() is None: + return + + sanitized: dict[str, float] = {} + for k, v in metrics.items(): + if k.endswith("/step"): + continue + try: + sanitized[_sanitize_key(k)] = float(v) + except (TypeError, ValueError): + continue + + if sanitized: + mlflow.log_metrics(sanitized, step=int(step) if step is not None else None) + + +# --------------------------------------------------------------------------- +# Cleanup +# --------------------------------------------------------------------------- + + +def finish() -> None: + import mlflow + + if mlflow.active_run() is None: + return + + run_id = mlflow.active_run().info.run_id + mlflow.end_run() + logger.info("MLflow run ended: %s", run_id) diff --git a/miles/utils/prometheus_utils.py b/miles/utils/tracking_utils/prometheus_utils.py similarity index 100% rename from miles/utils/prometheus_utils.py rename to miles/utils/tracking_utils/prometheus_utils.py diff --git a/miles/utils/tensorboard_utils.py b/miles/utils/tracking_utils/tensorboard_utils.py similarity index 100% rename from miles/utils/tensorboard_utils.py rename to miles/utils/tracking_utils/tensorboard_utils.py diff --git a/miles/utils/wandb_utils.py b/miles/utils/tracking_utils/wandb_utils.py similarity index 100% rename from miles/utils/wandb_utils.py rename to miles/utils/tracking_utils/wandb_utils.py diff --git a/setup.py b/setup.py index 8c079432086..a16d0a18642 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,10 @@ def get_tag(self): extras_require={ "fsdp": [ "torch>=2.0", - ] + ], + "mlflow": [ + "mlflow>=2.0", + ], }, python_requires=">=3.10", classifiers=[ diff --git a/train.py b/train.py index 9de6c30e0dd..9a4a2955096 100644 --- a/train.py +++ b/train.py @@ -7,7 +7,7 @@ from miles.utils.async_utils import eager_create_task from miles.utils.logging_utils import configure_logger from miles.utils.misc import should_run_periodic_action -from miles.utils.tracking_utils import init_tracking +from miles.utils.tracking_utils import finish_tracking, init_tracking async def train(args): @@ -106,4 +106,7 @@ async def save(rollout_id): if __name__ == "__main__": args = parse_args() - asyncio.run(train(args)) + try: + asyncio.run(train(args)) + finally: + finish_tracking() diff --git a/train_async.py b/train_async.py index e9e05a40629..a234bcb6e53 100644 --- a/train_async.py +++ b/train_async.py @@ -5,7 +5,7 @@ from miles.utils.async_utils import eager_create_task from miles.utils.logging_utils import configure_logger from miles.utils.misc import should_run_periodic_action -from miles.utils.tracking_utils import init_tracking +from miles.utils.tracking_utils import finish_tracking, init_tracking # The framework supports other asynchronous approaches such as fully async (which is shown in examples/full_async). @@ -75,4 +75,7 @@ async def train(args): if __name__ == "__main__": args = parse_args() - asyncio.run(train(args)) + try: + asyncio.run(train(args)) + finally: + finish_tracking() From db9837bacc3a65efb8eda0198eb9b46c9ea73461 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 08:45:46 +0800 Subject: [PATCH 0019/1130] fix(test): pass real ckpt_step to load in test_qwen3_4B_ckpt (#1073) --- tests/e2e/ckpt/test_qwen3_4B_ckpt.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/e2e/ckpt/test_qwen3_4B_ckpt.py b/tests/e2e/ckpt/test_qwen3_4B_ckpt.py index 55255c16a16..742cdd377fa 100644 --- a/tests/e2e/ckpt/test_qwen3_4B_ckpt.py +++ b/tests/e2e/ckpt/test_qwen3_4B_ckpt.py @@ -14,6 +14,15 @@ NUM_GPUS = 8 +def _get_latest_checkpointed_iteration() -> int: + latest_path = f"/root/models/{MODEL_NAME}_miles/latest_checkpointed_iteration.txt" + with open(latest_path, encoding="utf-8") as f: + latest_text = f.read().strip() + if not latest_text.isdigit(): + raise ValueError(f"Invalid latest checkpoint value: {latest_text}") + return int(latest_text) + + 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}") @@ -134,6 +143,6 @@ def execute(mode: str = "", ckpt_step: int | None = None): for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): os.environ.pop(proxy_var, None) execute("save") - execute("load") + execute("load", ckpt_step=_get_latest_checkpointed_iteration()) execute("async_save") - execute("load") + execute("load", ckpt_step=_get_latest_checkpointed_iteration()) From 8d93d9599d18f820b88c9715c7264d18b27df696 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 12:02:09 +0800 Subject: [PATCH 0020/1130] ci: use est_time as per-file timeout, fix long test budget (#1077) --- tests/ci/ci_utils.py | 8 +++++--- tests/e2e/long/test_qwen2.5_0.5B_gsm8k.py | 3 ++- tests/e2e/long/test_qwen2.5_0.5B_gsm8k_async.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/ci/ci_utils.py b/tests/ci/ci_utils.py index bf45e53b021..b79a4e8d771 100644 --- a/tests/ci/ci_utils.py +++ b/tests/ci/ci_utils.py @@ -159,6 +159,8 @@ def run_unittest_files( else: filename, estimated_time = file.name, file.estimated_time + effective_timeout = max(timeout_per_file, int(estimated_time * 1.25)) + process = None output_lines = [] @@ -213,7 +215,7 @@ def run_one_file(filename, capture_output=False, _i=i, _estimated_time=estimated run_one_file, args=(filename,), kwargs={"capture_output": enable_retry}, - timeout=timeout_per_file, + timeout=effective_timeout, ) if ret_code == 0: @@ -248,10 +250,10 @@ def run_one_file(filename, capture_output=False, _i=i, _estimated_time=estimated except TimeoutError: _kill_process_tree(process.pid) time.sleep(5) - logger.info(f"\nTIMEOUT: {filename} after {timeout_per_file} seconds\n") + logger.info(f"\nTIMEOUT: {filename} after {effective_timeout} seconds\n") if was_retried: retried_tests.append((filename, attempt, "timeout")) - failed_tests.append((filename, f"timeout after {timeout_per_file}s")) + failed_tests.append((filename, f"timeout after {effective_timeout}s")) break if not file_passed: diff --git a/tests/e2e/long/test_qwen2.5_0.5B_gsm8k.py b/tests/e2e/long/test_qwen2.5_0.5B_gsm8k.py index 4eef9fc7a4d..dea841a23db 100644 --- a/tests/e2e/long/test_qwen2.5_0.5B_gsm8k.py +++ b/tests/e2e/long/test_qwen2.5_0.5B_gsm8k.py @@ -4,7 +4,8 @@ import miles.utils.external_utils.command_utils as U -register_cuda_ci(est_time=1800, suite="stage-c-long-8-gpu", num_gpus=8) +# est_time calibrated against historical CI runtime: ~5071–5923s on 8×H100. +register_cuda_ci(est_time=6000, suite="stage-c-long-8-gpu", num_gpus=8) FEW_GPU = U.get_bool_env_var("MILES_TEST_FEW_GPU", "1") TIGHT_DEVICE_MEMORY = U.get_bool_env_var("MILES_TEST_TIGHT_DEVICE_MEMORY", "1") diff --git a/tests/e2e/long/test_qwen2.5_0.5B_gsm8k_async.py b/tests/e2e/long/test_qwen2.5_0.5B_gsm8k_async.py index 026c2deccb2..a9107d5cc24 100644 --- a/tests/e2e/long/test_qwen2.5_0.5B_gsm8k_async.py +++ b/tests/e2e/long/test_qwen2.5_0.5B_gsm8k_async.py @@ -4,7 +4,8 @@ import miles.utils.external_utils.command_utils as U -register_cuda_ci(est_time=1800, suite="stage-c-long-8-gpu", num_gpus=8) +# est_time calibrated against historical CI runtime: ~4124–4182s on 8×H100. +register_cuda_ci(est_time=5000, suite="stage-c-long-8-gpu", num_gpus=8) FEW_GPU = U.get_bool_env_var("MILES_TEST_FEW_GPU", "1") TIGHT_DEVICE_MEMORY = U.get_bool_env_var("MILES_TEST_TIGHT_DEVICE_MEMORY", "1") From d44d60f1e2b08dc309d548e48ea7e6680d538717 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:24:20 +0800 Subject: [PATCH 0021/1130] Add argparse_utils and enhance typer_utils for megatron runner (#769) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- miles/backends/megatron_utils/arguments.py | 2 +- miles/utils/argparse_utils.py | 131 ++++ miles/utils/typer_utils.py | 127 ++-- tests/fast/utils/test_argparse_utils.py | 264 ++++++++ tests/fast/utils/test_parallel_utils.py | 193 ++++++ tests/fast/utils/test_typer_utils.py | 738 +++++++++++++++++++++ 6 files changed, 1402 insertions(+), 53 deletions(-) create mode 100644 miles/utils/argparse_utils.py create mode 100644 tests/fast/utils/test_argparse_utils.py create mode 100644 tests/fast/utils/test_parallel_utils.py create mode 100644 tests/fast/utils/test_typer_utils.py diff --git a/miles/backends/megatron_utils/arguments.py b/miles/backends/megatron_utils/arguments.py index f5ece6d882a..f445ca979ee 100644 --- a/miles/backends/megatron_utils/arguments.py +++ b/miles/backends/megatron_utils/arguments.py @@ -1,8 +1,8 @@ import logging import os +from megatron.core.tokenizers.utils.build_tokenizer import vocab_size_with_padding as _vocab_size_with_padding from megatron.training.arguments import parse_args, validate_args -from megatron.training.tokenizer.tokenizer import _vocab_size_with_padding __all__ = ["validate_args", "parse_args", "set_default_megatron_args"] diff --git a/miles/utils/argparse_utils.py b/miles/utils/argparse_utils.py new file mode 100644 index 00000000000..13b8f2b5cb8 --- /dev/null +++ b/miles/utils/argparse_utils.py @@ -0,0 +1,131 @@ +"""Dataclass ↔ argparse bridge. + +Supported field types: str, int, float, Path, bool, and their ``X | None`` +variants (except ``bool | None`` which is not supported). +""" + +from __future__ import annotations + +import argparse +import dataclasses +import types +from pathlib import Path +from typing import Generic, TypeVar, get_type_hints + +T = TypeVar("T") + +_SCALAR_TYPES: dict[type, type] = {str: str, int: int, float: float, Path: Path} + + +def _is_bool(tp: type) -> bool: + return tp is bool + + +def _is_optional(tp: type) -> tuple[bool, type | None]: + if not isinstance(tp, types.UnionType): + return False, None + + args: tuple[type, ...] = tp.__args__ + non_none: list[type] = [a for a in args if a is not type(None)] + if type(None) not in args or len(non_none) != 1: + return False, None + + return True, non_none[0] + + +def _resolve_default(field: dataclasses.Field[object]) -> object: + """Return the effective default for a dataclass field, or ``MISSING``.""" + if field.default is not dataclasses.MISSING: + return field.default + if field.default_factory is not dataclasses.MISSING: # type: ignore[misc] + return field.default_factory() # type: ignore[misc] + return dataclasses.MISSING + + +class DataclassArgparseBridge(Generic[T]): + """Bi-directional converter: dataclass ↔ argparse. + + *prefix* controls the CLI flag prefix: ``"script"`` → ``--script-field-name``, + ``""`` → ``--field-name``. + """ + + def __init__( + self, + dataclass_type: type[T], + *, + prefix: str, + group_title: str | None = None, + ) -> None: + if not dataclasses.is_dataclass(dataclass_type): + raise TypeError(f"{dataclass_type!r} is not a dataclass") + + self._cls: type[T] = dataclass_type + self._prefix: str = prefix + self._group_title: str = group_title or f"{dataclass_type.__name__} args" + self._hints: dict[str, type] = get_type_hints(dataclass_type) + + def _flag(self, field_name: str) -> str: + stem: str = field_name.replace("_", "-") + if self._prefix: + return f"--{self._prefix}-{stem}" + return f"--{stem}" + + def _dest(self, field_name: str) -> str: + if self._prefix: + return f"{self._prefix}_{field_name}" + return field_name + + def register_on_parser(self, parser: argparse.ArgumentParser) -> argparse.ArgumentParser: + group: argparse._ArgumentGroup = parser.add_argument_group(self._group_title) + + for field in dataclasses.fields(self._cls): + flag: str = self._flag(field.name) + dest: str = self._dest(field.name) + tp: type = self._hints[field.name] + + if _is_bool(tp): + group.add_argument(flag, dest=dest, action="store_true", default=False) + continue + + is_opt, inner = _is_optional(tp) + if is_opt: + if inner not in _SCALAR_TYPES: + raise TypeError(f"Unsupported optional inner type {inner!r} for field {field.name}") + group.add_argument(flag, dest=dest, type=_SCALAR_TYPES[inner], default=None) + continue + + if tp in _SCALAR_TYPES: + default: object = _resolve_default(field) + kwargs: dict[str, object] = {"dest": dest, "type": _SCALAR_TYPES[tp]} + if default is not dataclasses.MISSING: + kwargs["default"] = default + else: + kwargs["required"] = True + group.add_argument(flag, **kwargs) # type: ignore[arg-type] + continue + + raise TypeError(f"Unsupported field type {tp!r} for field {field.name}") + + return parser + + def from_namespace(self, namespace: argparse.Namespace) -> T: + kwargs: dict[str, object] = {} + for field in dataclasses.fields(self._cls): + kwargs[field.name] = getattr(namespace, self._dest(field.name)) + return self._cls(**kwargs) # type: ignore[call-arg] + + def to_cli_args(self, instance: T) -> str: + parts: list[str] = [] + + for field in dataclasses.fields(self._cls): # type: ignore[arg-type] + value: object = getattr(instance, field.name) + flag: str = self._flag(field.name) + tp: type = self._hints[field.name] + + if _is_bool(tp): + if value: + parts.append(flag) + elif value is not None: + parts.append(f"{flag} {value}") + + return " ".join(parts) diff --git a/miles/utils/typer_utils.py b/miles/utils/typer_utils.py index 4cd7cafd848..1e7c09d10c9 100644 --- a/miles/utils/typer_utils.py +++ b/miles/utils/typer_utils.py @@ -1,34 +1,88 @@ import dataclasses +import functools import inspect -from typing import Annotated +import typing +from collections.abc import Callable +from typing import Annotated, TypeVar, overload import typer +_F = TypeVar("_F", bound=Callable[..., object]) -def dataclass_cli(func, env_var_prefix: str = "MILES_SCRIPT_"): - """Modified from https://github.com/fastapi/typer/issues/154#issuecomment-1544876144""" - # The dataclass type is the first argument of the function. - sig = inspect.signature(func) - param = list(sig.parameters.values())[0] - dataclass_cls = param.annotation +@overload +def dataclass_cli(func: _F) -> _F: ... + + +@overload +def dataclass_cli( + func: None = None, + *, + env_var_prefix: str = "MILES_SCRIPT_", +) -> Callable[[_F], _F]: ... + + +def dataclass_cli( + func: _F | None = None, + *, + env_var_prefix: str = "MILES_SCRIPT_", +) -> _F | Callable[[_F], _F]: + """Turn a function whose first param is a dataclass into a typer-compatible CLI. + + Modified from https://github.com/fastapi/typer/issues/154#issuecomment-1544876144 + + Supports field ``metadata`` keys: + - ``"help"``: passed as ``help=`` to ``typer.Option`` + + Usage:: + + @app.command() + @dataclass_cli # bare — uses MILES_SCRIPT_ env prefix + def cmd(args: MyArgs): ... + + @app.command() + @dataclass_cli(env_var_prefix="") # no env-var binding + def cmd(args: MyArgs): ... + """ + if func is None: + return functools.partial(dataclass_cli, env_var_prefix=env_var_prefix) # type: ignore[return-value] + + return _wrap(func, env_var_prefix=env_var_prefix) + + +def _wrap(func: _F, *, env_var_prefix: str) -> _F: + hints: dict[str, type] = typing.get_type_hints(func) + first_param_name: str = next(iter(inspect.signature(func).parameters)) + dataclass_cls: type = hints[first_param_name] assert dataclasses.is_dataclass(dataclass_cls) - # To construct the signature, we remove the first argument (self) - # from the dataclass __init__ signature. - signature = inspect.signature(dataclass_cls.__init__) - old_parameters = list(signature.parameters.values()) - if len(old_parameters) > 0 and old_parameters[0].name == "self": + init_sig: inspect.Signature = inspect.signature(dataclass_cls.__init__) + old_parameters: list[inspect.Parameter] = list(init_sig.parameters.values()) + if old_parameters and old_parameters[0].name == "self": del old_parameters[0] - new_parameters = [] + resolved_hints: dict[str, type] = typing.get_type_hints(dataclass_cls) + fields_by_name: dict[str, dataclasses.Field] = { # type: ignore[type-arg] + f.name: f for f in dataclasses.fields(dataclass_cls) + } + + new_parameters: list[inspect.Parameter] = [] for param in old_parameters: - env_var_name = f"{env_var_prefix}{param.name.upper()}" - new_annotation = Annotated[param.annotation, typer.Option(envvar=env_var_name)] + field: dataclasses.Field = fields_by_name[param.name] # type: ignore[type-arg] + + typer_kwargs: dict[str, object] = {} + if env_var_prefix: + typer_kwargs["envvar"] = f"{env_var_prefix}{param.name.upper()}" + if "help" in field.metadata: + typer_kwargs["help"] = field.metadata["help"] + + resolved_type: type = resolved_hints.get(param.name, param.annotation) + new_annotation = Annotated[resolved_type, typer.Option(**typer_kwargs)] + new_parameters.append(param.replace(annotation=new_annotation)) - def wrapped(**kwargs): - data = dataclass_cls(**kwargs) + def wrapped(**kwargs: object) -> object: + data: object = dataclass_cls(**kwargs) fields = dataclasses.fields(data) max_key_len = max(len(f.name) for f in fields) sep = "+" + "-" * (max_key_len + 2) + "+" + "-" * 52 + "+" @@ -43,40 +97,9 @@ def wrapped(**kwargs): print(sep) return func(data) - wrapped.__signature__ = signature.replace(parameters=new_parameters) + wrapped.__signature__ = init_sig.replace(parameters=new_parameters) # type: ignore[attr-defined] wrapped.__doc__ = func.__doc__ - wrapped.__name__ = func.__name__ - wrapped.__qualname__ = func.__qualname__ - - return wrapped - - -# unit test -if __name__ == "__main__": - from typer.testing import CliRunner - - @dataclasses.dataclass - class DemoArgs: - name: str - count: int = 1 - - app = typer.Typer() - - @app.command() - @dataclass_cli - def main(args: DemoArgs): - print(f"{args.name}|{args.count}") - - runner = CliRunner() - - res1 = runner.invoke(app, [], env={"MILES_SCRIPT_NAME": "EnvName", "MILES_SCRIPT_COUNT": "10"}) - print(f"{res1.stdout=}") - assert res1.exit_code == 0 - assert "EnvName|10" in res1.stdout.strip() - - res2 = runner.invoke(app, ["--count", "999"], env={"MILES_SCRIPT_NAME": "EnvName"}) - print(f"{res2.stdout=}") - assert res2.exit_code == 0 - assert "EnvName|999" in res2.stdout.strip() + wrapped.__name__ = func.__name__ # type: ignore[attr-defined] + wrapped.__qualname__ = func.__qualname__ # type: ignore[attr-defined] - print("✅ All Tests Passed!") + return wrapped # type: ignore[return-value] diff --git a/tests/fast/utils/test_argparse_utils.py b/tests/fast/utils/test_argparse_utils.py new file mode 100644 index 00000000000..4221e24c6c9 --- /dev/null +++ b/tests/fast/utils/test_argparse_utils.py @@ -0,0 +1,264 @@ +"""Tests for DataclassArgparseBridge.""" + +from __future__ import annotations + +import argparse +import dataclasses + +import pytest + +from miles.utils.argparse_utils import DataclassArgparseBridge + + +@dataclasses.dataclass(frozen=True) +class _SampleArgs: + mandatory_str: str + mandatory_int: int + mandatory_float: float + defaulted_str: str = "default" + defaulted_int: int = 10 + defaulted_float: float = 0.5 + flag: bool = False + optional_str: str | None = None + optional_int: int | None = None + optional_float: float | None = None + + +_BRIDGE: DataclassArgparseBridge[_SampleArgs] = DataclassArgparseBridge( + _SampleArgs, + prefix="sample", + group_title="sample args", +) + + +@dataclasses.dataclass(frozen=True) +class _NoPrefixArgs: + name: str + debug: bool = False + + +_NO_PREFIX_BRIDGE: DataclassArgparseBridge[_NoPrefixArgs] = DataclassArgparseBridge( + _NoPrefixArgs, + prefix="", +) + + +class TestRegisterOnParser: + def test_required_fields_are_required(self) -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _BRIDGE.register_on_parser(parser) + + with pytest.raises(SystemExit): + parser.parse_args([]) + + def test_parses_all_field_types(self) -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _BRIDGE.register_on_parser(parser) + + namespace: argparse.Namespace = parser.parse_args( + [ + "--sample-mandatory-str", + "hello", + "--sample-mandatory-int", + "42", + "--sample-mandatory-float", + "3.14", + "--sample-defaulted-str", + "custom", + "--sample-defaulted-int", + "99", + "--sample-defaulted-float", + "1.5", + "--sample-flag", + "--sample-optional-str", + "mytag", + "--sample-optional-int", + "100", + "--sample-optional-float", + "0.9", + ] + ) + + assert namespace.sample_mandatory_str == "hello" + assert namespace.sample_mandatory_int == 42 + assert namespace.sample_mandatory_float == 3.14 + assert namespace.sample_defaulted_str == "custom" + assert namespace.sample_defaulted_int == 99 + assert namespace.sample_defaulted_float == 1.5 + assert namespace.sample_flag is True + assert namespace.sample_optional_str == "mytag" + assert namespace.sample_optional_int == 100 + assert namespace.sample_optional_float == 0.9 + + def test_defaults_are_applied(self) -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _BRIDGE.register_on_parser(parser) + + namespace: argparse.Namespace = parser.parse_args( + [ + "--sample-mandatory-str", + "test", + "--sample-mandatory-int", + "1", + "--sample-mandatory-float", + "2.0", + ] + ) + + assert namespace.sample_defaulted_str == "default" + assert namespace.sample_defaulted_int == 10 + assert namespace.sample_defaulted_float == 0.5 + assert namespace.sample_flag is False + assert namespace.sample_optional_str is None + assert namespace.sample_optional_int is None + assert namespace.sample_optional_float is None + + def test_no_prefix(self) -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _NO_PREFIX_BRIDGE.register_on_parser(parser) + + namespace: argparse.Namespace = parser.parse_args(["--name", "foo", "--debug"]) + assert namespace.name == "foo" + assert namespace.debug is True + + +class TestFromNamespace: + def test_constructs_dataclass_from_namespace(self) -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _BRIDGE.register_on_parser(parser) + + namespace: argparse.Namespace = parser.parse_args( + [ + "--sample-mandatory-str", + "world", + "--sample-mandatory-int", + "7", + "--sample-mandatory-float", + "1.0", + "--sample-flag", + "--sample-optional-str", + "x", + ] + ) + + instance: _SampleArgs = _BRIDGE.from_namespace(namespace) + + assert instance.mandatory_str == "world" + assert instance.mandatory_int == 7 + assert instance.mandatory_float == 1.0 + assert instance.defaulted_str == "default" + assert instance.flag is True + assert instance.optional_str == "x" + assert instance.optional_int is None + + def test_no_prefix_from_namespace(self) -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _NO_PREFIX_BRIDGE.register_on_parser(parser) + + namespace: argparse.Namespace = parser.parse_args(["--name", "bar"]) + instance: _NoPrefixArgs = _NO_PREFIX_BRIDGE.from_namespace(namespace) + + assert instance.name == "bar" + assert instance.debug is False + + +class TestToCliArgs: + def test_serializes_all_types(self) -> None: + instance: _SampleArgs = _SampleArgs( + mandatory_str="hello", + mandatory_int=42, + mandatory_float=3.14, + flag=True, + optional_str="mytag", + optional_int=100, + optional_float=0.9, + ) + + cli: str = _BRIDGE.to_cli_args(instance) + + assert "--sample-mandatory-str hello" in cli + assert "--sample-mandatory-int 42" in cli + assert "--sample-mandatory-float 3.14" in cli + assert "--sample-flag" in cli + assert "--sample-optional-str mytag" in cli + assert "--sample-optional-int 100" in cli + assert "--sample-optional-float 0.9" in cli + + def test_skips_none_and_false(self) -> None: + instance: _SampleArgs = _SampleArgs( + mandatory_str="test", + mandatory_int=1, + mandatory_float=2.0, + ) + + cli: str = _BRIDGE.to_cli_args(instance) + + assert "--sample-flag" not in cli + assert "--sample-optional-str" not in cli + assert "--sample-optional-int" not in cli + assert "--sample-optional-float" not in cli + + def test_no_prefix_serialization(self) -> None: + instance: _NoPrefixArgs = _NoPrefixArgs(name="foo", debug=True) + cli: str = _NO_PREFIX_BRIDGE.to_cli_args(instance) + + assert "--name foo" in cli + assert "--debug" in cli + + +class TestRoundTrip: + def test_to_cli_args_then_parse_back(self) -> None: + original: _SampleArgs = _SampleArgs( + mandatory_str="roundtrip", + mandatory_int=99, + mandatory_float=2.5, + defaulted_str="custom", + defaulted_int=77, + defaulted_float=1.1, + flag=True, + optional_str="t", + optional_int=50, + optional_float=0.1, + ) + + cli: str = _BRIDGE.to_cli_args(original) + + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _BRIDGE.register_on_parser(parser) + namespace: argparse.Namespace = parser.parse_args(cli.split()) + + restored: _SampleArgs = _BRIDGE.from_namespace(namespace) + assert restored == original + + def test_round_trip_with_defaults(self) -> None: + original: _SampleArgs = _SampleArgs( + mandatory_str="minimal", + mandatory_int=1, + mandatory_float=2.0, + ) + + cli: str = _BRIDGE.to_cli_args(original) + + parser: argparse.ArgumentParser = argparse.ArgumentParser() + _BRIDGE.register_on_parser(parser) + namespace: argparse.Namespace = parser.parse_args(cli.split()) + + restored: _SampleArgs = _BRIDGE.from_namespace(namespace) + assert restored == original + + +class TestValidation: + def test_rejects_non_dataclass(self) -> None: + with pytest.raises(TypeError, match="not a dataclass"): + DataclassArgparseBridge(str, prefix="x") + + def test_rejects_unsupported_type(self) -> None: + @dataclasses.dataclass(frozen=True) + class _Bad: + data: list[str] = dataclasses.field(default_factory=list) + + bridge: DataclassArgparseBridge[_Bad] = DataclassArgparseBridge(_Bad, prefix="bad") + parser: argparse.ArgumentParser = argparse.ArgumentParser() + + with pytest.raises(TypeError, match="Unsupported field type"): + bridge.register_on_parser(parser) diff --git a/tests/fast/utils/test_parallel_utils.py b/tests/fast/utils/test_parallel_utils.py new file mode 100644 index 00000000000..230672275e3 --- /dev/null +++ b/tests/fast/utils/test_parallel_utils.py @@ -0,0 +1,193 @@ +import dataclasses + +import pytest + +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig, parse_parallel_args + +# --------------------------------------------------------------------------- +# Construction & defaults +# --------------------------------------------------------------------------- + + +class TestConstruction: + def test_defaults(self) -> None: + config = ParallelConfig() + assert config.tp == 1 + assert config.pp == 1 + assert config.cp == 1 + assert config.ep is None + assert config.etp == 1 + + def test_explicit_values(self) -> None: + config = ParallelConfig(tp=2, pp=4, cp=2, ep=8, etp=2) + assert config.tp == 2 + assert config.pp == 4 + assert config.cp == 2 + assert config.ep == 8 + assert config.etp == 2 + + def test_frozen(self) -> None: + config = ParallelConfig() + with pytest.raises(dataclasses.FrozenInstanceError): + config.tp = 2 # type: ignore[misc] + + +# --------------------------------------------------------------------------- +# Properties +# --------------------------------------------------------------------------- + + +class TestProperties: + def test_nproc(self) -> None: + assert ParallelConfig(tp=2, pp=4, cp=2).nproc == 16 + + def test_nproc_defaults(self) -> None: + assert ParallelConfig().nproc == 1 + + def test_effective_ep_defaults_to_tp(self) -> None: + config = ParallelConfig(tp=4) + assert config.effective_ep == 4 + + def test_effective_ep_explicit(self) -> None: + config = ParallelConfig(tp=4, ep=2) + assert config.effective_ep == 2 + + +# --------------------------------------------------------------------------- +# __post_init__ validation +# --------------------------------------------------------------------------- + + +class TestValidation: + def test_valid_config_no_error(self) -> None: + ParallelConfig(tp=2, pp=2, cp=2, ep=4) + + def test_nproc_not_divisible_by_ep_raises(self) -> None: + with pytest.raises(ValueError, match="not divisible by effective EP"): + ParallelConfig(tp=2, pp=1, cp=1, ep=3) + + def test_default_ep_equals_tp_always_valid(self) -> None: + # When ep=None, effective_ep=tp, and nproc=tp*pp*cp is always + # divisible by tp, so validation never fires. + ParallelConfig(tp=3, pp=2, cp=1) # no error + + +# --------------------------------------------------------------------------- +# from_parsed_args +# --------------------------------------------------------------------------- + + +class TestFromParsedArgs: + def test_all_provided(self) -> None: + config = ParallelConfig.from_parsed_args({"tp": 2, "pp": 4, "cp": 2, "ep": 8, "etp": 2}) + assert config == ParallelConfig(tp=2, pp=4, cp=2, ep=8, etp=2) + + def test_partial_uses_defaults(self) -> None: + config = ParallelConfig.from_parsed_args({"tp": 4}) + assert config == ParallelConfig(tp=4, pp=1, cp=1, ep=None, etp=1) + + def test_empty_dict_gives_defaults(self) -> None: + config = ParallelConfig.from_parsed_args({}) + assert config == ParallelConfig() + + +# --------------------------------------------------------------------------- +# from_run_args +# --------------------------------------------------------------------------- + + +class TestFromRunArgs: + def test_from_run_args(self) -> None: + from miles.utils.debug_utils.run_megatron.cli.commands.args import RunArgs + + args = RunArgs( + model_type="test", + hf_checkpoint="/tmp/hf", + tp=2, + pp=4, + cp=1, + ep=2, + etp=1, + ) + config = ParallelConfig.from_run_args(args) + assert config == ParallelConfig(tp=2, pp=4, cp=1, ep=2, etp=1) + + def test_from_run_args_defaults(self) -> None: + from miles.utils.debug_utils.run_megatron.cli.commands.args import RunArgs + + args = RunArgs( + model_type="test", + hf_checkpoint="/tmp/hf", + ) + config = ParallelConfig.from_run_args(args) + assert config == ParallelConfig() + + +# --------------------------------------------------------------------------- +# __str__ +# --------------------------------------------------------------------------- + + +class TestStr: + def test_str_repr(self) -> None: + config = ParallelConfig(tp=2, pp=4, cp=2, ep=8, etp=2) + text = str(config) + assert "tp=2" in text + assert "pp=4" in text + assert "cp=2" in text + assert "ep=8" in text + assert "etp=2" in text + assert "nproc=16" in text + + def test_str_defaults(self) -> None: + text = str(ParallelConfig()) + assert "nproc=1" in text + + +# --------------------------------------------------------------------------- +# dir_name +# --------------------------------------------------------------------------- + + +class TestDirName: + def test_tp_only(self) -> None: + assert ParallelConfig(tp=2).dir_name() == "tp2" + + def test_all_non_default(self) -> None: + assert ParallelConfig(tp=2, pp=2, cp=2, ep=4, etp=2).dir_name() == "tp2_pp2_cp2_ep4_etp2" + + def test_ep_equal_tp_omitted(self) -> None: + assert ParallelConfig(tp=4, ep=4).dir_name() == "tp4" + + def test_ep_different_from_tp_included(self) -> None: + assert ParallelConfig(tp=4, ep=2).dir_name() == "tp4_ep2" + + def test_pp1_cp1_omitted(self) -> None: + assert ParallelConfig(tp=2, pp=1, cp=1).dir_name() == "tp2" + + def test_defaults_dir_name(self) -> None: + assert ParallelConfig().dir_name() == "tp1" + + +# --------------------------------------------------------------------------- +# parse_parallel_args +# --------------------------------------------------------------------------- + + +class TestParseParallelArgs: + def test_full_string(self) -> None: + result = parse_parallel_args("--tp 2 --pp 4 --cp 2 --ep 8 --etp 2") + assert result == {"tp": 2, "pp": 4, "cp": 2, "ep": 8, "etp": 2} + + def test_partial_string(self) -> None: + result = parse_parallel_args("--tp 4 --cp 2") + assert result == {"tp": 4, "cp": 2} + + def test_empty_string(self) -> None: + result = parse_parallel_args("") + assert result == {} + + def test_roundtrip_with_from_parsed_args(self) -> None: + parsed = parse_parallel_args("--tp 2 --pp 4 --cp 2") + config = ParallelConfig.from_parsed_args(parsed) + assert config == ParallelConfig(tp=2, pp=4, cp=2) diff --git a/tests/fast/utils/test_typer_utils.py b/tests/fast/utils/test_typer_utils.py new file mode 100644 index 00000000000..4dd3b1a56b4 --- /dev/null +++ b/tests/fast/utils/test_typer_utils.py @@ -0,0 +1,738 @@ +import dataclasses +import enum + +import pytest +import typer +from typer.testing import CliRunner + +from miles.utils.typer_utils import dataclass_cli + +runner = CliRunner() + + +# --------------------------------------------------------------------------- +# Test dataclasses +# --------------------------------------------------------------------------- + + +@dataclasses.dataclass +class _SimpleArgs: + name: str + count: int = 1 + + +@dataclasses.dataclass +class _BoolArgs: + verbose: bool = False + + +@dataclasses.dataclass +class _MetaHelpArgs: + level: int = dataclasses.field( + default=0, + metadata={"help": "Verbosity level"}, + ) + + +@dataclasses.dataclass +class _MetaHelpAndFlagArgs: + verbose: bool = dataclasses.field( + default=False, + metadata={"help": "Enable verbose output"}, + ) + + +@dataclasses.dataclass +class _MultiFieldArgs: + host: str = "localhost" + port: int = 8080 + debug: bool = False + + +@dataclasses.dataclass +class _AllRequiredArgs: + first: str + second: int + + +@dataclasses.dataclass +class _SingleFieldArgs: + value: str = "default" + + +@dataclasses.dataclass +class _FloatArgs: + rate: float = 0.01 + + +@dataclasses.dataclass +class _OptionalStrArgs: + tag: str | None = None + + +class _Color(str, enum.Enum): + RED = "red" + GREEN = "green" + BLUE = "blue" + + +@dataclasses.dataclass +class _EnumArgs: + color: _Color = _Color.RED + + +@dataclasses.dataclass +class _FieldWithDefaultNoMeta: + count: int = dataclasses.field(default=5) + + +@dataclasses.dataclass +class _DefaultFactoryArgs: + items: str = dataclasses.field(default_factory=lambda: "a,b,c") + + +@dataclasses.dataclass +class _MixedMetaArgs: + plain: str = "hello" + with_help: int = dataclasses.field( + default=0, + metadata={"help": "A number"}, + ) + with_flag: str = dataclasses.field( + default="x", + ) + with_both: bool = dataclasses.field( + default=False, + metadata={"help": "Toggle it"}, + ) + + +@dataclasses.dataclass +class _EmptyMetaArgs: + value: str = dataclasses.field(default="ok", metadata={}) + + +@dataclasses.dataclass +class _MultipleRequiredArgs: + alpha: str + beta: str + gamma: str + + +# --------------------------------------------------------------------------- +# Bare decorator: @dataclass_cli +# --------------------------------------------------------------------------- + + +class TestBareDecorator: + def test_env_vars(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, [], env={"MILES_SCRIPT_NAME": "EnvName", "MILES_SCRIPT_COUNT": "10"}) + assert result.exit_code == 0 + assert "EnvName|10" in result.stdout + + def test_cli_flag_overrides_env_var(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, ["--count", "999"], env={"MILES_SCRIPT_NAME": "EnvName"}) + assert result.exit_code == 0 + assert "EnvName|999" in result.stdout + + def test_default_value_used_when_omitted(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, ["--name", "Alice"]) + assert result.exit_code == 0 + assert "Alice|1" in result.stdout + + def test_all_flags_explicit(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, ["--name", "Bob", "--count", "42"]) + assert result.exit_code == 0 + assert "Bob|42" in result.stdout + + def test_missing_required_field_fails(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _SimpleArgs) -> None: + print(args.name) + + result = runner.invoke(app, []) + assert result.exit_code != 0 + + +# --------------------------------------------------------------------------- +# Parameterized decorator: @dataclass_cli(env_var_prefix=...) +# --------------------------------------------------------------------------- + + +class TestParameterizedDecorator: + def test_empty_prefix_disables_env_vars(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke( + app, + [], + env={"MILES_SCRIPT_NAME": "ShouldBeIgnored"}, + ) + assert result.exit_code != 0 + + def test_custom_prefix(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="MY_APP_") + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, [], env={"MY_APP_NAME": "Custom", "MY_APP_COUNT": "77"}) + assert result.exit_code == 0 + assert "Custom|77" in result.stdout + + def test_custom_prefix_ignores_default_prefix(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="MY_APP_") + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, [], env={"MILES_SCRIPT_NAME": "Wrong"}) + assert result.exit_code != 0 + + def test_default_prefix_when_called_with_parens(self) -> None: + """@dataclass_cli() with no args should behave same as bare @dataclass_cli.""" + app = typer.Typer() + + @app.command() + @dataclass_cli() + def cmd(args: _SimpleArgs) -> None: + print(f"{args.name}|{args.count}") + + result = runner.invoke(app, [], env={"MILES_SCRIPT_NAME": "ViaParens", "MILES_SCRIPT_COUNT": "3"}) + assert result.exit_code == 0 + assert "ViaParens|3" in result.stdout + + +# --------------------------------------------------------------------------- +# Field metadata: help +# --------------------------------------------------------------------------- + + +class TestFieldMetadata: + def test_help_appears_in_help_text(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _MetaHelpArgs) -> None: + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Verbosity level" in result.stdout + + def test_help_and_flag_together(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _MetaHelpAndFlagArgs) -> None: + print(f"verbose={args.verbose}") + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Enable verbose output" in result.stdout + + result = runner.invoke(app, ["--verbose"]) + assert result.exit_code == 0 + assert "verbose=True" in result.stdout + + def test_empty_metadata_treated_as_no_metadata(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _EmptyMetaArgs) -> None: + print(f"value={args.value}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "value=ok" in result.stdout + + def test_mixed_metadata_fields(self) -> None: + """Dataclass with a mix of plain fields and help-annotated fields.""" + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _MixedMetaArgs) -> None: + print(f"{args.plain}|{args.with_help}|{args.with_flag}|{args.with_both}") + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "A number" in result.stdout + assert "Toggle it" in result.stdout + + result = runner.invoke(app, ["--with-help", "7", "--with-flag", "y", "--with-both"]) + assert result.exit_code == 0 + assert "hello|7|y|True" in result.stdout + + +# --------------------------------------------------------------------------- +# Bool fields +# --------------------------------------------------------------------------- + + +class TestBoolFields: + def test_bool_default_false(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _BoolArgs) -> None: + print(f"verbose={args.verbose}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "verbose=False" in result.stdout + + def test_bool_flag_enables(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _BoolArgs) -> None: + print(f"verbose={args.verbose}") + + result = runner.invoke(app, ["--verbose"]) + assert result.exit_code == 0 + assert "verbose=True" in result.stdout + + def test_bool_no_flag_disables(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _BoolArgs) -> None: + print(f"verbose={args.verbose}") + + result = runner.invoke(app, ["--no-verbose"]) + assert result.exit_code == 0 + assert "verbose=False" in result.stdout + + +# --------------------------------------------------------------------------- +# Various field types +# --------------------------------------------------------------------------- + + +class TestFieldTypes: + def test_float_field(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _FloatArgs) -> None: + print(f"rate={args.rate}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "rate=0.01" in result.stdout + + result = runner.invoke(app, ["--rate", "0.5"]) + assert result.exit_code == 0 + assert "rate=0.5" in result.stdout + + def test_optional_str_default_none(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _OptionalStrArgs) -> None: + print(f"tag={args.tag}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "tag=None" in result.stdout + + def test_optional_str_provided(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _OptionalStrArgs) -> None: + print(f"tag={args.tag}") + + result = runner.invoke(app, ["--tag", "v1"]) + assert result.exit_code == 0 + assert "tag=v1" in result.stdout + + def test_enum_field_default(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _EnumArgs) -> None: + print(f"color={args.color.value}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "color=red" in result.stdout + + def test_enum_field_override(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _EnumArgs) -> None: + print(f"color={args.color.value}") + + result = runner.invoke(app, ["--color", "blue"]) + assert result.exit_code == 0 + assert "color=blue" in result.stdout + + def test_enum_invalid_value_fails(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _EnumArgs) -> None: + pass + + result = runner.invoke(app, ["--color", "yellow"]) + assert result.exit_code != 0 + + +# --------------------------------------------------------------------------- +# dataclasses.field without metadata / with default_factory +# --------------------------------------------------------------------------- + + +class TestDataclassField: + def test_field_with_default_no_metadata(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _FieldWithDefaultNoMeta) -> None: + print(f"count={args.count}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "count=5" in result.stdout + + def test_field_with_default_factory_override(self) -> None: + """default_factory defaults show as in the signature, + so only the explicit-override path is reliable.""" + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _DefaultFactoryArgs) -> None: + print(f"items={args.items}") + + result = runner.invoke(app, ["--items", "x,y"]) + assert result.exit_code == 0 + assert "items=x,y" in result.stdout + + +# --------------------------------------------------------------------------- +# Single field / many required fields +# --------------------------------------------------------------------------- + + +class TestFieldCounts: + def test_single_field_dataclass(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _SingleFieldArgs) -> None: + print(f"value={args.value}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "value=default" in result.stdout + + def test_multiple_required_all_provided(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _MultipleRequiredArgs) -> None: + print(f"{args.alpha}|{args.beta}|{args.gamma}") + + result = runner.invoke(app, ["--alpha", "a", "--beta", "b", "--gamma", "c"]) + assert result.exit_code == 0 + assert "a|b|c" in result.stdout + + def test_multiple_required_missing_any_fails(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _MultipleRequiredArgs) -> None: + pass + + result = runner.invoke(app, ["--alpha", "a", "--beta", "b"]) + assert result.exit_code != 0 + + +# --------------------------------------------------------------------------- +# Multiple fields +# --------------------------------------------------------------------------- + + +class TestMultipleFields: + def test_all_defaults(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _MultiFieldArgs) -> None: + print(f"{args.host}:{args.port}:{args.debug}") + + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "localhost:8080:False" in result.stdout + + def test_override_some(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _MultiFieldArgs) -> None: + print(f"{args.host}:{args.port}:{args.debug}") + + result = runner.invoke(app, ["--port", "9090", "--debug"]) + assert result.exit_code == 0 + assert "localhost:9090:True" in result.stdout + + def test_all_required_both_provided(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _AllRequiredArgs) -> None: + print(f"{args.first}|{args.second}") + + result = runner.invoke(app, ["--first", "a", "--second", "2"]) + assert result.exit_code == 0 + assert "a|2" in result.stdout + + def test_all_required_missing_one_fails(self) -> None: + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _AllRequiredArgs) -> None: + pass + + result = runner.invoke(app, ["--first", "a"]) + assert result.exit_code != 0 + + +# --------------------------------------------------------------------------- +# Return value passthrough +# --------------------------------------------------------------------------- + + +class TestReturnValue: + def test_return_value_is_forwarded(self) -> None: + @dataclass_cli(env_var_prefix="") + def compute(args: _SimpleArgs) -> str: + return f"result:{args.name}" + + value = compute(name="test", count=1) + assert value == "result:test" + + def test_return_none(self) -> None: + @dataclass_cli(env_var_prefix="") + def noop(args: _SimpleArgs) -> None: + pass + + value = noop(name="x", count=0) + assert value is None + + +# --------------------------------------------------------------------------- +# Dataclass instance correctness +# --------------------------------------------------------------------------- + + +class TestDataclassInstance: + def test_receives_correct_dataclass_type(self) -> None: + received: list = [] + + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _SimpleArgs) -> None: + received.append(args) + + runner.invoke(app, ["--name", "check", "--count", "7"]) + assert len(received) == 1 + assert isinstance(received[0], _SimpleArgs) + assert received[0].name == "check" + assert received[0].count == 7 + + def test_bool_field_receives_python_bool(self) -> None: + received: list = [] + + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _BoolArgs) -> None: + received.append(args) + + runner.invoke(app, ["--verbose"]) + assert received[0].verbose is True + + def test_enum_field_receives_enum_instance(self) -> None: + received: list = [] + + app = typer.Typer() + + @app.command() + @dataclass_cli(env_var_prefix="") + def cmd(args: _EnumArgs) -> None: + received.append(args) + + runner.invoke(app, ["--color", "green"]) + assert received[0].color is _Color.GREEN + + +# --------------------------------------------------------------------------- +# Wrapped function attributes +# --------------------------------------------------------------------------- + + +class TestWrappedAttributes: + def test_preserves_name(self) -> None: + @dataclass_cli + def my_func(args: _SimpleArgs) -> None: + """My docstring.""" + + assert my_func.__name__ == "my_func" + assert my_func.__qualname__.endswith("my_func") + + def test_preserves_docstring(self) -> None: + @dataclass_cli + def my_func(args: _SimpleArgs) -> None: + """My docstring.""" + + assert my_func.__doc__ == "My docstring." + + def test_preserves_name_parameterized(self) -> None: + @dataclass_cli(env_var_prefix="") + def another_func(args: _SimpleArgs) -> None: + """Another doc.""" + + assert another_func.__name__ == "another_func" + assert another_func.__doc__ == "Another doc." + + def test_no_docstring(self) -> None: + @dataclass_cli + def no_doc(args: _SimpleArgs) -> None: + pass + + assert no_doc.__doc__ is None + + def test_has_signature(self) -> None: + @dataclass_cli(env_var_prefix="") + def cmd(args: _SimpleArgs) -> None: + pass + + sig = inspect.signature(cmd) + param_names = list(sig.parameters.keys()) + assert "name" in param_names + assert "count" in param_names + assert "args" not in param_names + + +# --------------------------------------------------------------------------- +# Error: non-dataclass annotation +# --------------------------------------------------------------------------- + + +class TestErrorCases: + def test_non_dataclass_raises(self) -> None: + class NotADataclass: + pass + + with pytest.raises(AssertionError): + + @dataclass_cli + def bad(args: NotADataclass) -> None: + pass + + def test_non_dataclass_raises_parameterized(self) -> None: + class NotADataclass: + pass + + with pytest.raises(AssertionError): + + @dataclass_cli(env_var_prefix="") + def bad(args: NotADataclass) -> None: + pass + + +# --------------------------------------------------------------------------- +# Env var naming: uppercase conversion +# --------------------------------------------------------------------------- + + +class TestEnvVarNaming: + def test_underscore_field_name_uppercased(self) -> None: + @dataclasses.dataclass + class _SnakeArgs: + my_long_name: str = "default" + + app = typer.Typer() + + @app.command() + @dataclass_cli + def cmd(args: _SnakeArgs) -> None: + print(f"val={args.my_long_name}") + + result = runner.invoke(app, [], env={"MILES_SCRIPT_MY_LONG_NAME": "from_env"}) + assert result.exit_code == 0 + assert "val=from_env" in result.stdout + + +# --------------------------------------------------------------------------- +# import needed for signature inspection +# --------------------------------------------------------------------------- +import inspect From edf785ff9ce2a5ae4d5ef48bd0e2745893c0ca09 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:24:35 +0800 Subject: [PATCH 0022/1130] Add worker batch preparation for megatron runner (#770) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../debug_utils/run_megatron/__init__.py | 1 + .../run_megatron/cli/commands/__init__.py | 0 .../run_megatron/cli/commands/args.py | 43 +++++++ .../run_megatron/worker/__init__.py | 0 .../debug_utils/run_megatron/worker/batch.py | 60 ++++++++++ .../run_megatron/worker/script_args.py | 30 +++++ tests/fast/utils/debug_utils/__init__.py | 0 .../debug_utils/run_megatron/__init__.py | 0 .../debug_utils/run_megatron/cli/__init__.py | 0 .../run_megatron/cli/commands/__init__.py | 0 .../run_megatron/worker/__init__.py | 0 .../run_megatron/worker/test_batch.py | 113 ++++++++++++++++++ .../run_megatron/worker/test_script_args.py | 53 ++++++++ 13 files changed, 300 insertions(+) create mode 100644 miles/utils/debug_utils/run_megatron/__init__.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/commands/__init__.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/commands/args.py create mode 100644 miles/utils/debug_utils/run_megatron/worker/__init__.py create mode 100644 miles/utils/debug_utils/run_megatron/worker/batch.py create mode 100644 miles/utils/debug_utils/run_megatron/worker/script_args.py create mode 100644 tests/fast/utils/debug_utils/__init__.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/__init__.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/__init__.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/commands/__init__.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/__init__.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py diff --git a/miles/utils/debug_utils/run_megatron/__init__.py b/miles/utils/debug_utils/run_megatron/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/__init__.py @@ -0,0 +1 @@ + diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py new file mode 100644 index 00000000000..e59a932e029 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import dataclasses +from pathlib import Path + + +def _field( + default: object = dataclasses.MISSING, + *, + help: str, +) -> dataclasses.Field: # type: ignore[type-arg] + """Shorthand to create a dataclass field with typer metadata.""" + return dataclasses.field(default=default, metadata={"help": help}) # type: ignore[arg-type] + + +@dataclasses.dataclass +class CommonRunArgs: + model_type: str = _field(help="Model type matching scripts/models/{model_type}.sh") + hf_checkpoint: Path = _field(help="HuggingFace checkpoint path") + ref_load: Path | None = _field(default=None, help="Megatron checkpoint path") + sp: bool = _field(default=False, help="Enable sequence parallelism") + run_backward: bool = _field(default=False, help="Run backward pass") + prompt_mode: str = _field(default="math", help="Prompt mode: math / file / text") + prompt_text: str | None = _field(default=None, help="Prompt text (for text mode)") + prompt_file: Path | None = _field(default=None, help="Prompt file (for file mode)") + # odd + somewhat large; also the fine-structure constant + seq_length: int = _field(default=137, help="Sequence length") + batch_size: int = _field(default=1, help="Micro batch size") + apply_chat_template: bool = _field(default=False, help="Apply chat template") + role: str = _field(default="actor", help="Model role: actor / critic") + source_patcher_config: Path | None = _field(default=None, help="Source patcher YAML config path") + dumper_filter: str = _field(default="", help="Dumper filter expression") + megatron_path: Path | None = _field(default=None, help="Path to Megatron-LM") + extra_args: str = _field(default="", help="Extra args passed to worker") + + +@dataclasses.dataclass +class RunArgs(CommonRunArgs): + output_dir: Path = _field(default=Path("/tmp/run_megatron_dump"), help="Dump output directory") + tp: int = _field(default=1, help="Tensor parallel size") + pp: int = _field(default=1, help="Pipeline parallel size") + ep: int | None = _field(default=None, help="Expert parallel size (default=tp)") + etp: int = _field(default=1, help="Expert tensor parallel size") diff --git a/miles/utils/debug_utils/run_megatron/worker/__init__.py b/miles/utils/debug_utils/run_megatron/worker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/miles/utils/debug_utils/run_megatron/worker/batch.py b/miles/utils/debug_utils/run_megatron/worker/batch.py new file mode 100644 index 00000000000..aeb09c70819 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/worker/batch.py @@ -0,0 +1,60 @@ +"""Input batch preparation and loss function for standalone Megatron forward/backward.""" + +from typing import Any + +import torch + + +def prepare_batch( + *, + token_ids: list[int], + batch_size: int, + device: str | torch.device = "cuda", +) -> dict[str, torch.Tensor]: + """Build the batch dict for Megatron forward from pre-tokenized token IDs. + + Returns a dict containing: + - input_ids: [batch_size, seq_len] + - position_ids: [batch_size, seq_len] + - attention_mask: None (let flash attention handle causal masking) + - labels: [batch_size, seq_len] (next-token labels) + """ + seq_length: int = len(token_ids) + + token_tensor: torch.Tensor = torch.tensor(token_ids, dtype=torch.long, device=device) + position_tensor: torch.Tensor = torch.arange(seq_length, dtype=torch.long, device=device) + + input_ids: torch.Tensor = token_tensor.unsqueeze(0).expand(batch_size, -1) + position_ids: torch.Tensor = position_tensor.unsqueeze(0).expand(batch_size, -1) + + labels: torch.Tensor = torch.cat( + [input_ids[:, 1:], torch.full((batch_size, 1), -100, device=input_ids.device, dtype=input_ids.dtype)], + dim=1, + ) + + return { + "input_ids": input_ids, + "position_ids": position_ids, + "attention_mask": None, + "labels": labels, + } + + +def loss_func( + labels: torch.Tensor, + output_tensor: torch.Tensor, +) -> tuple[torch.Tensor, dict[str, Any]]: + """Cross-entropy loss for forward-backward pipeline schedule. + + Uses ignore_index=-100 to handle label masking. + """ + logits: torch.Tensor = output_tensor.float() + vocab_size: int = logits.size(-1) + + loss: torch.Tensor = torch.nn.functional.cross_entropy( + logits.view(-1, vocab_size), + labels.view(-1), + ignore_index=-100, + reduction="mean", + ) + return loss, {"loss": loss.detach()} diff --git a/miles/utils/debug_utils/run_megatron/worker/script_args.py b/miles/utils/debug_utils/run_megatron/worker/script_args.py new file mode 100644 index 00000000000..b026d8f060b --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/worker/script_args.py @@ -0,0 +1,30 @@ +"""WorkerScriptArgs: our custom arguments passed from CLI to torchrun worker. + +All flags use the ``--script-`` prefix to avoid collision with Megatron's own +argparse namespace. Adding a new field here is the *only* change needed — +serialization (CLI -> worker) and deserialization (worker argparse) are automatic. +""" + +from __future__ import annotations + +import dataclasses +from pathlib import Path + +from miles.utils.argparse_utils import DataclassArgparseBridge + + +@dataclasses.dataclass(frozen=True) +class WorkerScriptArgs: + hf_checkpoint: Path + token_ids_file: Path + role: str = "actor" + ref_load: Path | None = None + run_backward: bool = False + source_patcher_config: Path | None = None + + +WORKER_SCRIPT_ARGS_BRIDGE: DataclassArgparseBridge[WorkerScriptArgs] = DataclassArgparseBridge( + WorkerScriptArgs, + prefix="script", + group_title="run_megatron script args", +) diff --git a/tests/fast/utils/debug_utils/__init__.py b/tests/fast/utils/debug_utils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fast/utils/debug_utils/run_megatron/__init__.py b/tests/fast/utils/debug_utils/run_megatron/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/__init__.py b/tests/fast/utils/debug_utils/run_megatron/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/commands/__init__.py b/tests/fast/utils/debug_utils/run_megatron/cli/commands/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/__init__.py b/tests/fast/utils/debug_utils/run_megatron/worker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py new file mode 100644 index 00000000000..b4ddef01501 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import torch + +from miles.utils.debug_utils.run_megatron.worker.batch import loss_func, prepare_batch + + +class TestLossFunc: + def test_return_type(self) -> None: + logits = torch.randn(1, 4, 10) + labels = torch.tensor([[1, 2, 3, -100]], dtype=torch.long) + result = loss_func(labels=labels, output_tensor=logits) + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], torch.Tensor) + assert isinstance(result[1], dict) + assert "loss" in result[1] + + def test_loss_scalar(self) -> None: + logits = torch.randn(1, 4, 10) + labels = torch.tensor([[1, 2, 3, 4]], dtype=torch.long) + loss, metrics = loss_func(labels=labels, output_tensor=logits) + assert loss.dim() == 0 + assert metrics["loss"].dim() == 0 + + def test_loss_finite(self) -> None: + logits = torch.randn(1, 4, 10) + labels = torch.tensor([[1, 2, 3, 4]], dtype=torch.long) + loss, _ = loss_func(labels=labels, output_tensor=logits) + assert torch.isfinite(loss) + + def test_loss_detached_in_metrics(self) -> None: + logits = torch.randn(1, 4, 10, requires_grad=True) + labels = torch.tensor([[1, 2, 3, 4]], dtype=torch.long) + _, metrics = loss_func(labels=labels, output_tensor=logits) + assert not metrics["loss"].requires_grad + + def test_ignores_neg100(self) -> None: + logits = torch.randn(1, 4, 10) + all_masked = torch.full((1, 4), -100, dtype=torch.long) + loss, _ = loss_func(labels=all_masked, output_tensor=logits) + assert torch.isnan(loss) or loss.item() == 0.0 + + def test_perfect_prediction(self) -> None: + vocab_size = 5 + logits = torch.full((1, 3, vocab_size), -100.0) + labels = torch.tensor([[0, 1, 2]], dtype=torch.long) + for pos in range(3): + logits[0, pos, labels[0, pos]] = 100.0 + loss, _ = loss_func(labels=labels, output_tensor=logits) + assert loss.item() < 0.01 + + def test_batch_size_2(self) -> None: + vocab_size = 10 + logits = torch.randn(2, 4, vocab_size) + labels = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=torch.long) + loss, _ = loss_func(labels=labels, output_tensor=logits) + assert torch.isfinite(loss) + + +class TestPrepareBatchCP1: + def test_all_keys_present(self) -> None: + batch = prepare_batch(token_ids=list(range(8)), batch_size=1, device="cpu") + expected_keys = {"input_ids", "position_ids", "attention_mask", "labels"} + assert set(batch.keys()) == expected_keys + + def test_all_tensors_long_except_mask(self) -> None: + batch = prepare_batch(token_ids=list(range(8)), batch_size=1, device="cpu") + assert batch["input_ids"].dtype == torch.long + assert batch["position_ids"].dtype == torch.long + assert batch["labels"].dtype == torch.long + assert batch["attention_mask"] is None + + def test_shapes(self) -> None: + token_ids = list(range(8)) + batch = prepare_batch(token_ids=token_ids, batch_size=2, device="cpu") + assert batch["input_ids"].shape == (2, 8) + assert batch["position_ids"].shape == (2, 8) + assert batch["attention_mask"] is None + assert batch["labels"].shape == (2, 8) + + def test_input_ids_values(self) -> None: + token_ids = [10, 20, 30] + batch = prepare_batch(token_ids=token_ids, batch_size=1, device="cpu") + assert batch["input_ids"][0].tolist() == [10, 20, 30] + + def test_position_ids_sequential(self) -> None: + token_ids = list(range(5)) + batch = prepare_batch(token_ids=token_ids, batch_size=1, device="cpu") + assert batch["position_ids"][0].tolist() == [0, 1, 2, 3, 4] + + def test_attention_mask_is_none(self) -> None: + token_ids = list(range(4)) + batch = prepare_batch(token_ids=token_ids, batch_size=1, device="cpu") + assert batch["attention_mask"] is None + + def test_labels_next_token(self) -> None: + token_ids = [10, 20, 30, 40] + batch = prepare_batch(token_ids=token_ids, batch_size=1, device="cpu") + assert batch["labels"][0].tolist() == [20, 30, 40, -100] + + def test_batch_dim_broadcast(self) -> None: + token_ids = [10, 20, 30] + batch = prepare_batch(token_ids=token_ids, batch_size=3, device="cpu") + for i in range(3): + assert batch["input_ids"][i].tolist() == [10, 20, 30] + assert batch["position_ids"][i].tolist() == [0, 1, 2] + + def test_single_token(self) -> None: + batch = prepare_batch(token_ids=[42], batch_size=1, device="cpu") + assert batch["input_ids"][0].tolist() == [42] + assert batch["labels"][0].tolist() == [-100] + assert batch["attention_mask"] is None diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py new file mode 100644 index 00000000000..37a3210c728 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py @@ -0,0 +1,53 @@ +import dataclasses +from pathlib import Path + +import pytest + +from miles.utils.debug_utils.run_megatron.worker.script_args import WORKER_SCRIPT_ARGS_BRIDGE, WorkerScriptArgs + + +class TestWorkerScriptArgs: + def test_frozen(self) -> None: + args = WorkerScriptArgs(hf_checkpoint=Path("/hf"), token_ids_file=Path("/tokens.json")) + with pytest.raises(dataclasses.FrozenInstanceError): + args.role = "critic" # type: ignore[misc] + + def test_defaults(self) -> None: + args = WorkerScriptArgs(hf_checkpoint=Path("/hf"), token_ids_file=Path("/tokens.json")) + assert args.role == "actor" + assert args.run_backward is False + assert args.ref_load is None + assert args.source_patcher_config is None + + def test_to_cli_args_roundtrip(self) -> None: + original = WorkerScriptArgs( + hf_checkpoint=Path("/hf"), + token_ids_file=Path("/tokens.json"), + role="critic", + run_backward=True, + ) + cli_str = WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(original) + + import argparse + + parser = argparse.ArgumentParser() + WORKER_SCRIPT_ARGS_BRIDGE.register_on_parser(parser) + namespace = parser.parse_args(cli_str.split()) + reconstructed = WORKER_SCRIPT_ARGS_BRIDGE.from_namespace(namespace) + + assert reconstructed == original + + def test_none_fields_omitted(self) -> None: + args = WorkerScriptArgs(hf_checkpoint=Path("/hf"), token_ids_file=Path("/tokens.json")) + cli_str = WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(args) + assert "--script-ref-load" not in cli_str + assert "--script-source-patcher-config" not in cli_str + + def test_bool_serialization(self) -> None: + args = WorkerScriptArgs( + hf_checkpoint=Path("/hf"), + token_ids_file=Path("/tokens.json"), + run_backward=True, + ) + cli_str = WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(args) + assert "--script-run-backward" in cli_str From 32c7f3e0f53b5d3c02ad2a57698ab433fb67fa79 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:28:49 +0800 Subject: [PATCH 0023/1130] Add CP zigzag support for megatron runner (#771) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/cli/commands/args.py | 1 + .../debug_utils/run_megatron/worker/batch.py | 67 ++- .../run_megatron/worker/test_batch.py | 519 +++++++++++++++++- 3 files changed, 577 insertions(+), 10 deletions(-) diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index e59a932e029..ef19d6e7ef8 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -39,5 +39,6 @@ class RunArgs(CommonRunArgs): output_dir: Path = _field(default=Path("/tmp/run_megatron_dump"), help="Dump output directory") tp: int = _field(default=1, help="Tensor parallel size") pp: int = _field(default=1, help="Pipeline parallel size") + cp: int = _field(default=1, help="Context parallel size") ep: int | None = _field(default=None, help="Expert parallel size (default=tp)") etp: int = _field(default=1, help="Expert tensor parallel size") diff --git a/miles/utils/debug_utils/run_megatron/worker/batch.py b/miles/utils/debug_utils/run_megatron/worker/batch.py index aeb09c70819..5fc74a67cce 100644 --- a/miles/utils/debug_utils/run_megatron/worker/batch.py +++ b/miles/utils/debug_utils/run_megatron/worker/batch.py @@ -1,5 +1,6 @@ """Input batch preparation and loss function for standalone Megatron forward/backward.""" +from types import SimpleNamespace from typing import Any import torch @@ -9,27 +10,48 @@ def prepare_batch( *, token_ids: list[int], batch_size: int, + cp_rank: int = 0, + cp_size: int = 1, device: str | torch.device = "cuda", ) -> dict[str, torch.Tensor]: """Build the batch dict for Megatron forward from pre-tokenized token IDs. Returns a dict containing: - - input_ids: [batch_size, seq_len] - - position_ids: [batch_size, seq_len] - - attention_mask: None (let flash attention handle causal masking) - - labels: [batch_size, seq_len] (next-token labels) + - input_ids: [batch_size, local_seq_len] + - position_ids: [batch_size, local_seq_len] + - attention_mask: [batch_size, 1, local_seq_len, local_seq_len] causal mask + - labels: [batch_size, local_seq_len] (CP-aware next-token labels) + - global_input_ids: [batch_size, full_seq_len] (unsliced, for reference) """ seq_length: int = len(token_ids) token_tensor: torch.Tensor = torch.tensor(token_ids, dtype=torch.long, device=device) position_tensor: torch.Tensor = torch.arange(seq_length, dtype=torch.long, device=device) + # Keep global copy before CP slicing (needed for CP-aware labels) + global_tokens: torch.Tensor = token_tensor.clone() + + if cp_size > 1: + from miles.backends.training_utils.cp_utils import slice_with_cp + + cp_kwargs: dict[str, object] = dict( + pad_value=0, + parallel_state=SimpleNamespace(cp_rank=cp_rank, cp_size=cp_size), + qkv_format="bshd", + max_seq_len=seq_length, + ) + token_tensor = slice_with_cp(token_tensor, **cp_kwargs) + position_tensor = slice_with_cp(position_tensor, **cp_kwargs) + input_ids: torch.Tensor = token_tensor.unsqueeze(0).expand(batch_size, -1) position_ids: torch.Tensor = position_tensor.unsqueeze(0).expand(batch_size, -1) + global_input_ids: torch.Tensor = global_tokens.unsqueeze(0).expand(batch_size, -1) - labels: torch.Tensor = torch.cat( - [input_ids[:, 1:], torch.full((batch_size, 1), -100, device=input_ids.device, dtype=input_ids.dtype)], - dim=1, + labels: torch.Tensor = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=cp_size, ) return { @@ -37,16 +59,45 @@ def prepare_batch( "position_ids": position_ids, "attention_mask": None, "labels": labels, + "global_input_ids": global_input_ids, } +def _build_labels( + *, + input_ids: torch.Tensor, + position_ids: torch.Tensor, + global_input_ids: torch.Tensor, + cp_size: int, +) -> torch.Tensor: + """Build next-token prediction labels, handling CP zigzag slicing. + + With CP>1, each rank sees non-contiguous positions (zigzag pattern). + We use position_ids to gather the correct next-token from global_input_ids. + Positions at the end of the global sequence get label -100 (ignored by loss). + """ + if cp_size > 1: + global_seq_len: int = global_input_ids.shape[1] + valid_mask: torch.Tensor = position_ids < global_seq_len - 1 + next_pos: torch.Tensor = torch.where(valid_mask, position_ids + 1, torch.zeros_like(position_ids)) + labels: torch.Tensor = global_input_ids.gather(1, next_pos) + labels = labels.masked_fill(~valid_mask, -100) + return labels + else: + batch_size: int = input_ids.shape[0] + return torch.cat( + [input_ids[:, 1:], torch.full((batch_size, 1), -100, device=input_ids.device, dtype=input_ids.dtype)], + dim=1, + ) + + def loss_func( labels: torch.Tensor, output_tensor: torch.Tensor, ) -> tuple[torch.Tensor, dict[str, Any]]: """Cross-entropy loss for forward-backward pipeline schedule. - Uses ignore_index=-100 to handle label masking. + Uses ignore_index=-100 to handle CP-aware label masking. """ logits: torch.Tensor = output_tensor.float() vocab_size: int = logits.size(-1) diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py index b4ddef01501..79bc98b8d85 100644 --- a/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py @@ -1,8 +1,159 @@ from __future__ import annotations +from types import SimpleNamespace + +import pytest import torch -from miles.utils.debug_utils.run_megatron.worker.batch import loss_func, prepare_batch +from miles.backends.training_utils.cp_utils import slice_with_cp +from miles.utils.debug_utils.run_megatron.worker.batch import _build_labels, loss_func, prepare_batch + + +def _zigzag_slice(tokens: torch.Tensor, *, cp_rank: int, cp_size: int) -> torch.Tensor: + return slice_with_cp( + tokens, + pad_value=0, + parallel_state=SimpleNamespace(cp_rank=cp_rank, cp_size=cp_size), + qkv_format="bshd", + max_seq_len=len(tokens), + ) + + +# --------------------------------------------------------------------------- +# TestBuildLabels +# --------------------------------------------------------------------------- + + +class TestBuildLabels: + def test_cp1_next_token_shift(self) -> None: + input_ids = torch.tensor([[10, 20, 30]], dtype=torch.long) + position_ids = torch.arange(3).unsqueeze(0) + global_input_ids = input_ids.clone() + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=1, + ) + assert labels.shape == (1, 3) + assert labels.dtype == torch.long + assert labels[0].tolist() == [20, 30, -100] + + def test_cp1_batch_size_2(self) -> None: + input_ids = torch.tensor([[10, 20, 30], [40, 50, 60]], dtype=torch.long) + position_ids = torch.arange(3).unsqueeze(0).expand(2, -1) + global_input_ids = input_ids.clone() + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=1, + ) + assert labels.shape == (2, 3) + assert labels[0].tolist() == [20, 30, -100] + assert labels[1].tolist() == [50, 60, -100] + + def test_cp1_single_token(self) -> None: + input_ids = torch.tensor([[99]], dtype=torch.long) + position_ids = torch.tensor([[0]]) + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=input_ids.clone(), + cp_size=1, + ) + assert labels[0].tolist() == [-100] + + def test_cp_gt1_uses_position_ids(self) -> None: + global_input_ids = torch.tensor([[10, 20, 30, 40]], dtype=torch.long) + position_ids = torch.tensor([[0, 3]], dtype=torch.long) + input_ids = torch.tensor([[10, 40]], dtype=torch.long) + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=2, + ) + assert labels.shape == (1, 2) + assert labels[0, 0].item() == 20 # next token after position 0 + assert labels[0, 1].item() == -100 # position 3 is last -> ignored + + def test_cp_gt1_middle_positions(self) -> None: + global_input_ids = torch.tensor([[10, 20, 30, 40, 50, 60]], dtype=torch.long) + position_ids = torch.tensor([[1, 4]], dtype=torch.long) + input_ids = torch.tensor([[20, 50]], dtype=torch.long) + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=2, + ) + assert labels[0, 0].item() == 30 # next after pos 1 + assert labels[0, 1].item() == 60 # next after pos 4 + + def test_cp_gt1_second_to_last_position_is_valid(self) -> None: + global_input_ids = torch.tensor([[10, 20, 30, 40]], dtype=torch.long) + position_ids = torch.tensor([[2, 3]], dtype=torch.long) + input_ids = torch.tensor([[30, 40]], dtype=torch.long) + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=2, + ) + assert labels[0, 0].item() == 40 # pos 2 -> next is pos 3 = token 40 + assert labels[0, 1].item() == -100 # pos 3 is last + + def test_cp_gt1_batch_size_2(self) -> None: + global_input_ids = torch.tensor([[10, 20, 30, 40], [50, 60, 70, 80]], dtype=torch.long) + position_ids = torch.tensor([[0, 3], [0, 3]], dtype=torch.long) + input_ids = torch.tensor([[10, 40], [50, 80]], dtype=torch.long) + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=2, + ) + assert labels.shape == (2, 2) + assert labels[0].tolist() == [20, -100] + assert labels[1].tolist() == [60, -100] + + def test_cp_gt1_all_positions_valid(self) -> None: + global_input_ids = torch.tensor([[10, 20, 30, 40, 50, 60]], dtype=torch.long) + position_ids = torch.tensor([[0, 2, 3]], dtype=torch.long) + input_ids = torch.tensor([[10, 30, 40]], dtype=torch.long) + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=2, + ) + assert labels[0].tolist() == [20, 40, 50] # all valid, none is last pos + + def test_cp_gt1_all_positions_are_last(self) -> None: + global_input_ids = torch.tensor([[10, 20, 30, 40]], dtype=torch.long) + position_ids = torch.tensor([[3, 3]], dtype=torch.long) + input_ids = torch.tensor([[40, 40]], dtype=torch.long) + + labels = _build_labels( + input_ids=input_ids, + position_ids=position_ids, + global_input_ids=global_input_ids, + cp_size=2, + ) + assert labels[0].tolist() == [-100, -100] + + +# --------------------------------------------------------------------------- +# TestLossFunc +# --------------------------------------------------------------------------- class TestLossFunc: @@ -58,10 +209,15 @@ def test_batch_size_2(self) -> None: assert torch.isfinite(loss) +# --------------------------------------------------------------------------- +# TestPrepareBatch — cp_size=1 +# --------------------------------------------------------------------------- + + class TestPrepareBatchCP1: def test_all_keys_present(self) -> None: batch = prepare_batch(token_ids=list(range(8)), batch_size=1, device="cpu") - expected_keys = {"input_ids", "position_ids", "attention_mask", "labels"} + expected_keys = {"input_ids", "position_ids", "attention_mask", "labels", "global_input_ids"} assert set(batch.keys()) == expected_keys def test_all_tensors_long_except_mask(self) -> None: @@ -69,6 +225,7 @@ def test_all_tensors_long_except_mask(self) -> None: assert batch["input_ids"].dtype == torch.long assert batch["position_ids"].dtype == torch.long assert batch["labels"].dtype == torch.long + assert batch["global_input_ids"].dtype == torch.long assert batch["attention_mask"] is None def test_shapes(self) -> None: @@ -78,6 +235,7 @@ def test_shapes(self) -> None: assert batch["position_ids"].shape == (2, 8) assert batch["attention_mask"] is None assert batch["labels"].shape == (2, 8) + assert batch["global_input_ids"].shape == (2, 8) def test_input_ids_values(self) -> None: token_ids = [10, 20, 30] @@ -99,6 +257,11 @@ def test_labels_next_token(self) -> None: batch = prepare_batch(token_ids=token_ids, batch_size=1, device="cpu") assert batch["labels"][0].tolist() == [20, 30, 40, -100] + def test_global_input_ids_equals_input_ids(self) -> None: + token_ids = [10, 20, 30] + batch = prepare_batch(token_ids=token_ids, batch_size=2, device="cpu") + assert torch.equal(batch["global_input_ids"], batch["input_ids"]) + def test_batch_dim_broadcast(self) -> None: token_ids = [10, 20, 30] batch = prepare_batch(token_ids=token_ids, batch_size=3, device="cpu") @@ -111,3 +274,355 @@ def test_single_token(self) -> None: assert batch["input_ids"][0].tolist() == [42] assert batch["labels"][0].tolist() == [-100] assert batch["attention_mask"] is None + + +# --------------------------------------------------------------------------- +# TestPrepareBatch — cp_size > 1 (zigzag) +# --------------------------------------------------------------------------- + + +class TestPrepareBatchZigzag: + """Tests that exercise the real slice_with_cp zigzag CP path.""" + + @pytest.fixture() + def seq8_cp2(self) -> dict[str, dict[str, torch.Tensor]]: + """Prepare batches for all ranks with seq_len=8, cp_size=2. + + Zigzag with cp_size=2, seq_len=8: + chunk_size = ceil(8 / 4) = 2 + rank 0: tokens[0:2] + tokens[6:8] = positions [0,1,6,7] + rank 1: tokens[2:4] + tokens[4:6] = positions [2,3,4,5] + """ + token_ids = [100, 200, 300, 400, 500, 600, 700, 800] + batches: dict[str, dict[str, torch.Tensor]] = {} + for rank in range(2): + batches[f"rank{rank}"] = prepare_batch( + token_ids=token_ids, + batch_size=1, + cp_rank=rank, + cp_size=2, + device="cpu", + ) + return batches + + @pytest.fixture() + def seq16_cp4(self) -> dict[str, dict[str, torch.Tensor]]: + """Prepare batches for all ranks with seq_len=16, cp_size=4. + + chunk_size = ceil(16 / 8) = 2 + rank 0: tokens[0:2] + tokens[14:16] + rank 1: tokens[2:4] + tokens[12:14] + rank 2: tokens[4:6] + tokens[10:12] + rank 3: tokens[6:8] + tokens[8:10] + """ + token_ids = list(range(1000, 1016)) + batches: dict[str, dict[str, torch.Tensor]] = {} + for rank in range(4): + batches[f"rank{rank}"] = prepare_batch( + token_ids=token_ids, + batch_size=1, + cp_rank=rank, + cp_size=4, + device="cpu", + ) + return batches + + def test_zigzag_token_and_position_assignment_cp2(self, seq8_cp2: dict) -> None: + """Verify exact zigzag token/position assignment for all ranks with cp_size=2. + + seq_len=8, chunk_size=2: + rank 0: tokens[0:2] + tokens[6:8] -> ids [100,200,700,800], pos [0,1,6,7] + rank 1: tokens[2:4] + tokens[4:6] -> ids [300,400,500,600], pos [2,3,4,5] + """ + expected_ids = { + "rank0": [100, 200, 700, 800], + "rank1": [300, 400, 500, 600], + } + expected_pos = { + "rank0": [0, 1, 6, 7], + "rank1": [2, 3, 4, 5], + } + for rank_key in seq8_cp2: + batch = seq8_cp2[rank_key] + assert batch["input_ids"][0].tolist() == expected_ids[rank_key] + assert batch["position_ids"][0].tolist() == expected_pos[rank_key] + + assert batch["input_ids"].shape == (1, 4) + assert batch["labels"].shape == (1, 4) + assert batch["attention_mask"] is None + assert batch["global_input_ids"].shape == (1, 8) + assert batch["global_input_ids"][0].tolist() == [100, 200, 300, 400, 500, 600, 700, 800] + + def test_zigzag_token_and_position_assignment_cp4(self, seq16_cp4: dict) -> None: + """Verify exact zigzag token/position assignment for all ranks with cp_size=4. + + seq_len=16, chunk_size=2: + rank 0: tokens[0:2] + tokens[14:16] -> [1000,1001,1014,1015], pos [0,1,14,15] + rank 1: tokens[2:4] + tokens[12:14] -> [1002,1003,1012,1013], pos [2,3,12,13] + rank 2: tokens[4:6] + tokens[10:12] -> [1004,1005,1010,1011], pos [4,5,10,11] + rank 3: tokens[6:8] + tokens[8:10] -> [1006,1007,1008,1009], pos [6,7,8,9] + """ + expected_ids = { + "rank0": [1000, 1001, 1014, 1015], + "rank1": [1002, 1003, 1012, 1013], + "rank2": [1004, 1005, 1010, 1011], + "rank3": [1006, 1007, 1008, 1009], + } + expected_pos = { + "rank0": [0, 1, 14, 15], + "rank1": [2, 3, 12, 13], + "rank2": [4, 5, 10, 11], + "rank3": [6, 7, 8, 9], + } + for rank_key in seq16_cp4: + batch = seq16_cp4[rank_key] + assert batch["input_ids"][0].tolist() == expected_ids[rank_key] + assert batch["position_ids"][0].tolist() == expected_pos[rank_key] + + assert batch["input_ids"].shape == (1, 4) + assert batch["labels"].shape == (1, 4) + assert batch["global_input_ids"].shape == (1, 16) + + def test_all_ranks_partition_full_sequence(self, seq8_cp2: dict, seq16_cp4: dict) -> None: + """All ranks together cover every position exactly once (no overlap, no gap).""" + for batches, seq_len in [(seq8_cp2, 8), (seq16_cp4, 16)]: + all_positions: list[int] = [] + all_tokens: list[int] = [] + for rank_key in batches: + all_positions.extend(batches[rank_key]["position_ids"][0].tolist()) + all_tokens.extend(batches[rank_key]["input_ids"][0].tolist()) + + assert sorted(all_positions) == list(range(seq_len)) + assert len(all_positions) == len(set(all_positions)) + assert len(all_tokens) == seq_len + + def test_labels_per_rank_cp2(self, seq8_cp2: dict) -> None: + """Verify labels for all ranks with cp_size=2. + + rank 0 positions [0,1,6,7]: labels = [tok@1=200, tok@2=300, tok@7=800, -100] + rank 1 positions [2,3,4,5]: labels = [tok@3=400, tok@4=500, tok@5=600, tok@6=700] + """ + expected_labels = { + "rank0": [200, 300, 800, -100], + "rank1": [400, 500, 600, 700], + } + for rank_key in seq8_cp2: + assert seq8_cp2[rank_key]["labels"][0].tolist() == expected_labels[rank_key] + + neg100_count = sum((seq8_cp2[rk]["labels"] == -100).sum().item() for rk in seq8_cp2) + assert neg100_count == 1 + + def test_labels_per_rank_cp4(self, seq16_cp4: dict) -> None: + """Verify labels for all ranks with cp_size=4. + + rank 0 positions [0,1,14,15]: labels = [1001, 1002, 1015, -100] + rank 1 positions [2,3,12,13]: labels = [1003, 1004, 1013, 1014] + rank 2 positions [4,5,10,11]: labels = [1005, 1006, 1011, 1012] + rank 3 positions [6,7,8,9]: labels = [1007, 1008, 1009, 1010] + """ + expected_labels = { + "rank0": [1001, 1002, 1015, -100], + "rank1": [1003, 1004, 1013, 1014], + "rank2": [1005, 1006, 1011, 1012], + "rank3": [1007, 1008, 1009, 1010], + } + for rank_key in seq16_cp4: + assert seq16_cp4[rank_key]["labels"][0].tolist() == expected_labels[rank_key] + + neg100_count = sum((seq16_cp4[rk]["labels"] == -100).sum().item() for rk in seq16_cp4) + assert neg100_count == 1 + + def test_input_ids_match_global_at_positions(self, seq8_cp2: dict, seq16_cp4: dict) -> None: + """For every rank, input_ids[i] == global_input_ids[position_ids[i]].""" + for batches in [seq8_cp2, seq16_cp4]: + for rank_key in batches: + batch = batches[rank_key] + gathered = batch["global_input_ids"][0][batch["position_ids"][0]] + assert torch.equal(batch["input_ids"][0], gathered) + + def test_attention_mask_is_none_zigzag(self, seq8_cp2: dict) -> None: + for rank_key in seq8_cp2: + assert seq8_cp2[rank_key]["attention_mask"] is None + + def test_positions_monotonic_within_each_chunk(self, seq8_cp2: dict, seq16_cp4: dict) -> None: + """Each half (chunk) of local positions should be monotonically increasing.""" + for batches in [seq8_cp2, seq16_cp4]: + for rank_key in batches: + pos = batches[rank_key]["position_ids"][0] + half = len(pos) // 2 + chunk1, chunk2 = pos[:half], pos[half:] + assert all(chunk1[i] < chunk1[i + 1] for i in range(len(chunk1) - 1)) + assert all(chunk2[i] < chunk2[i + 1] for i in range(len(chunk2) - 1)) + + def test_batch_dim_broadcast_cp2(self) -> None: + token_ids = list(range(100, 108)) + batch = prepare_batch(token_ids=token_ids, batch_size=3, cp_rank=0, cp_size=2, device="cpu") + assert batch["input_ids"].shape[0] == 3 + for i in range(3): + assert batch["input_ids"][i].tolist() == batch["input_ids"][0].tolist() + assert batch["labels"][i].tolist() == batch["labels"][0].tolist() + + +# --------------------------------------------------------------------------- +# TestPrepareBatch — zigzag with padding (seq_len not divisible by 2*cp_size) +# --------------------------------------------------------------------------- + + +class TestPrepareBatchZigzagPadding: + """When seq_len is not evenly divisible by 2*cp_size, slice_with_cp pads.""" + + def test_seq7_cp2_shapes(self) -> None: + """seq_len=7, cp_size=2 -> chunk_size=ceil(7/4)=2, padded_len=8, local=4.""" + token_ids = list(range(10, 17)) # 7 tokens + batch = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=0, cp_size=2, device="cpu") + assert batch["input_ids"].shape == (1, 4) + assert batch["global_input_ids"].shape == (1, 7) + + def test_seq7_cp2_padded_token_is_zero(self) -> None: + """The padding token (pad_value=0) should appear in the local slice.""" + token_ids = list(range(10, 17)) # 7 tokens + batch_r0 = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=0, cp_size=2, device="cpu") + # rank 0: tokens[0:2] + tokens[6:8] where tokens[7]=0 (padded) + local_ids = batch_r0["input_ids"][0].tolist() + assert local_ids[0] == 10 + assert local_ids[1] == 11 + assert local_ids[2] == 16 # original token at index 6 + assert local_ids[3] == 0 # padded + + def test_seq7_cp2_all_ranks_cover_original_tokens(self) -> None: + token_ids = list(range(10, 17)) + all_token_pos: list[tuple[int, int]] = [] + for rank in range(2): + batch = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=rank, cp_size=2, device="cpu") + pos = batch["position_ids"][0].tolist() + ids = batch["input_ids"][0].tolist() + all_token_pos.extend(zip(pos, ids, strict=False)) + + original_positions = {p for p, _ in all_token_pos if p < 7} + assert original_positions == set(range(7)) + + def test_seq5_cp2_shapes(self) -> None: + """seq_len=5, cp_size=2 -> chunk_size=ceil(5/4)=2, padded=8, local=4.""" + token_ids = list(range(5)) + batch = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=0, cp_size=2, device="cpu") + assert batch["input_ids"].shape == (1, 4) + + @pytest.mark.parametrize("seq_len", [3, 5, 7, 9, 11, 13]) + def test_odd_seq_len_cp2_no_crash(self, seq_len: int) -> None: + token_ids = list(range(seq_len)) + for rank in range(2): + batch = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=rank, cp_size=2, device="cpu") + assert batch["input_ids"].shape[0] == 1 + assert batch["input_ids"].shape[1] == batch["position_ids"].shape[1] + assert batch["labels"].shape == batch["input_ids"].shape + + @pytest.mark.parametrize("seq_len", [5, 7, 10, 13, 15]) + def test_various_seq_len_cp4_no_crash(self, seq_len: int) -> None: + token_ids = list(range(seq_len)) + for rank in range(4): + batch = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=rank, cp_size=4, device="cpu") + assert batch["input_ids"].shape[0] == 1 + assert batch["labels"].shape == batch["input_ids"].shape + + +# --------------------------------------------------------------------------- +# TestPrepareBatch — label cross-validation across all ranks +# --------------------------------------------------------------------------- + + +class TestLabelsCrossRankConsistency: + """Verify that labels across all CP ranks, when reassembled, form correct next-token targets.""" + + @pytest.mark.parametrize("cp_size", [2, 4]) + def test_reassembled_labels_match_naive(self, cp_size: int) -> None: + """Gather (position, label) from all ranks and verify against naive next-token.""" + seq_len = cp_size * 4 # evenly divisible + token_ids = list(range(1000, 1000 + seq_len)) + + position_to_label: dict[int, int] = {} + for rank in range(cp_size): + batch = prepare_batch( + token_ids=token_ids, + batch_size=1, + cp_rank=rank, + cp_size=cp_size, + device="cpu", + ) + positions = batch["position_ids"][0].tolist() + labels = batch["labels"][0].tolist() + for pos, label in zip(positions, labels, strict=True): + position_to_label[pos] = label + + for pos in range(seq_len - 1): + assert ( + position_to_label[pos] == token_ids[pos + 1] + ), f"At pos={pos}, expected label={token_ids[pos + 1]}, got {position_to_label[pos]}" + assert position_to_label[seq_len - 1] == -100 + + @pytest.mark.parametrize("cp_size,seq_len", [(2, 7), (2, 11), (4, 9), (4, 15)]) + def test_reassembled_labels_match_naive_with_padding(self, cp_size: int, seq_len: int) -> None: + """Same cross-rank check but with non-divisible seq_len (padding involved).""" + token_ids = list(range(1000, 1000 + seq_len)) + + position_to_label: dict[int, int] = {} + for rank in range(cp_size): + batch = prepare_batch( + token_ids=token_ids, + batch_size=1, + cp_rank=rank, + cp_size=cp_size, + device="cpu", + ) + positions = batch["position_ids"][0].tolist() + labels = batch["labels"][0].tolist() + for pos, label in zip(positions, labels, strict=True): + if pos < seq_len: + position_to_label[pos] = label + + for pos in range(seq_len - 1): + assert ( + position_to_label.get(pos) == token_ids[pos + 1] + ), f"At pos={pos}, expected label={token_ids[pos + 1]}, got {position_to_label.get(pos)}" + + +# --------------------------------------------------------------------------- +# TestPrepareBatch — slice_with_cp consistency +# --------------------------------------------------------------------------- + + +class TestSliceWithCPConsistency: + """Verify that prepare_batch's CP slicing matches direct slice_with_cp calls.""" + + @pytest.mark.parametrize("cp_size", [2, 4]) + def test_input_ids_match_direct_slice(self, cp_size: int) -> None: + seq_len = cp_size * 4 + token_ids = list(range(1000, 1000 + seq_len)) + tokens_tensor = torch.tensor(token_ids, dtype=torch.long) + + for rank in range(cp_size): + batch = prepare_batch( + token_ids=token_ids, + batch_size=1, + cp_rank=rank, + cp_size=cp_size, + device="cpu", + ) + expected = _zigzag_slice(tokens_tensor, cp_rank=rank, cp_size=cp_size) + assert torch.equal(batch["input_ids"][0], expected) + + @pytest.mark.parametrize("cp_size", [2, 4]) + def test_position_ids_match_direct_slice(self, cp_size: int) -> None: + seq_len = cp_size * 4 + token_ids = list(range(seq_len)) + positions_tensor = torch.arange(seq_len, dtype=torch.long) + + for rank in range(cp_size): + batch = prepare_batch( + token_ids=token_ids, + batch_size=1, + cp_rank=rank, + cp_size=cp_size, + device="cpu", + ) + expected = _zigzag_slice(positions_tensor, cp_rank=rank, cp_size=cp_size) + assert torch.equal(batch["position_ids"][0], expected) From a3d2c8e5d6bfbfb395dd6ae1efb4de0ebd288ea6 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:28:58 +0800 Subject: [PATCH 0024/1130] Add CLI utilities for megatron runner (#772) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/cli/parallel_utils.py | 68 +++++++ .../run_megatron/cli/path_utils.py | 33 ++++ .../run_megatron/cli/prompt_utils.py | 92 +++++++++ .../run_megatron/cli/test_parallel_utils.py | 187 ++++++++++++++++++ .../run_megatron/cli/test_path_utils.py | 51 +++++ .../run_megatron/cli/test_prompt_utils.py | 163 +++++++++++++++ 6 files changed, 594 insertions(+) create mode 100644 miles/utils/debug_utils/run_megatron/cli/parallel_utils.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/path_utils.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/prompt_utils.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/test_parallel_utils.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/test_path_utils.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/test_prompt_utils.py diff --git a/miles/utils/debug_utils/run_megatron/cli/parallel_utils.py b/miles/utils/debug_utils/run_megatron/cli/parallel_utils.py new file mode 100644 index 00000000000..f527fe81de8 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/parallel_utils.py @@ -0,0 +1,68 @@ +"""Parallel configuration utilities for run_megatron CLI.""" + +from __future__ import annotations + +import argparse +import dataclasses +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from miles.utils.debug_utils.run_megatron.cli.commands.args import RunArgs + +_FIELD_NAMES: tuple[str, ...] = ("tp", "pp", "cp", "ep", "etp") + + +@dataclasses.dataclass(frozen=True) +class ParallelConfig: + tp: int = 1 + pp: int = 1 + cp: int = 1 + ep: int | None = None + etp: int = 1 + + def __post_init__(self) -> None: + if self.nproc % self.effective_ep != 0: + raise ValueError( + f"nproc ({self.nproc} = tp*pp*cp = {self.tp}*{self.pp}*{self.cp}) " + f"is not divisible by effective EP ({self.effective_ep})" + ) + + @property + def effective_ep(self) -> int: + return self.ep if self.ep is not None else self.tp + + @property + def nproc(self) -> int: + return self.tp * self.pp * self.cp + + @classmethod + def from_parsed_args(cls, parsed: dict[str, int]) -> ParallelConfig: + defaults: dict[str, object] = {f.name: f.default for f in dataclasses.fields(cls)} + return cls(**{name: parsed.get(name, defaults[name]) for name in _FIELD_NAMES}) # type: ignore[arg-type] + + @classmethod + def from_run_args(cls, args: RunArgs) -> ParallelConfig: + return cls(**{name: getattr(args, name) for name in _FIELD_NAMES}) # type: ignore[arg-type] + + def __str__(self) -> str: + field_str: str = ", ".join(f"{name}={getattr(self, name)}" for name in _FIELD_NAMES) + return f"{field_str}, nproc={self.nproc}" + + def dir_name(self) -> str: + """Build directory name from parallel config, e.g. 'tp2_cp2_ep2'.""" + _SKIP: dict[str, object] = {"tp": None, "pp": 1, "cp": 1, "ep": self.tp, "etp": 1} + parts: list[str] = [ + f"{name}{getattr(self, name)}" + for name in _FIELD_NAMES + if getattr(self, name) is not None and getattr(self, name) != _SKIP[name] + ] + return "_".join(parts) + + +def parse_parallel_args(args_str: str) -> dict[str, int]: + """Parse a parallel config string like '--tp 2 --cp 2' into a dict.""" + parser: argparse.ArgumentParser = argparse.ArgumentParser() + for flag in _FIELD_NAMES: + parser.add_argument(f"--{flag}", type=int) + namespace: argparse.Namespace = parser.parse_args(args_str.split()) + return {k: v for k, v in vars(namespace).items() if v is not None} diff --git a/miles/utils/debug_utils/run_megatron/cli/path_utils.py b/miles/utils/debug_utils/run_megatron/cli/path_utils.py new file mode 100644 index 00000000000..b34f91d1459 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/path_utils.py @@ -0,0 +1,33 @@ +"""Path resolution utilities for run_megatron CLI.""" + +import os +from pathlib import Path + +import typer + +_DEFAULT_MEGATRON_PATH: Path = Path("/root/Megatron-LM") + + +def resolve_megatron_path(megatron_path: Path | None) -> Path: + if megatron_path is not None: + return megatron_path + env_path: str | None = os.environ.get("MEGATRON_PATH") + if env_path: + return Path(env_path) + return _DEFAULT_MEGATRON_PATH + + +def resolve_model_script(model_type: str) -> Path: + repo_base: Path = _resolve_repo_base() + script: Path = repo_base / "scripts" / "models" / f"{model_type}.sh" + if not script.exists(): + raise typer.BadParameter(f"Model script not found: {script}") + return script + + +def _resolve_repo_base() -> Path: + current: Path = Path(__file__).resolve() + for parent in current.parents: + if (parent / "pyproject.toml").exists(): + return parent + raise RuntimeError(f"Cannot find repo root (no pyproject.toml found above {current})") diff --git a/miles/utils/debug_utils/run_megatron/cli/prompt_utils.py b/miles/utils/debug_utils/run_megatron/cli/prompt_utils.py new file mode 100644 index 00000000000..26a22fcdb7e --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/prompt_utils.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import dataclasses +import json +import tempfile +from pathlib import Path +from typing import Literal + + +@dataclasses.dataclass(frozen=True) +class PromptConfig: + mode: Literal["math", "file", "text"] = "math" + text: str | None = None + file: Path | None = None + seq_length: int = 137 + apply_chat_template: bool = False + + +def generate_token_ids( + *, + prompt: PromptConfig, + tokenizer_path: Path, +) -> list[int]: + """Generate token IDs for Megatron standalone forward/backward. + + Three modes: + - math: deterministic arithmetic sequence "1+1=2, 1+2=3, ..." tokenized and padded/truncated to seq_length + - file: read long text from file, tokenize and truncate to seq_length + - text: use user-provided text directly, tokenize and truncate to seq_length + """ + from transformers import AutoTokenizer, PreTrainedTokenizerBase + + raw_text: str = _resolve_raw_text(prompt) + + tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(str(tokenizer_path), trust_remote_code=True) + + if prompt.apply_chat_template: + messages: list[dict[str, str]] = [{"role": "user", "content": raw_text}] + raw_text = tokenizer.apply_chat_template( + messages, + tokenize=False, + ) + + token_ids: list[int] = tokenizer.encode(raw_text) + if len(token_ids) < prompt.seq_length: + raise ValueError( + f"Token count ({len(token_ids)}) is less than seq_length ({prompt.seq_length}). " + f"Provide longer input text or reduce --seq-length." + ) + token_ids = token_ids[: prompt.seq_length] + + return token_ids + + +def write_token_ids_to_tmpfile(token_ids: list[int]) -> Path: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, prefix="run_megatron_token_ids_") as tmp: + json.dump(token_ids, tmp) + return Path(tmp.name) + + +def _resolve_raw_text(prompt: PromptConfig) -> str: + if prompt.mode == "math": + return _build_math_sequence(target_char_length=prompt.seq_length * 16) + elif prompt.mode == "file": + if prompt.file is None: + raise ValueError("--prompt-file is required for file mode") + return prompt.file.read_text() + elif prompt.mode == "text": + if prompt.text is None: + raise ValueError("--prompt-text is required for text mode") + return prompt.text + else: + raise ValueError(f"Unknown prompt mode: {prompt.mode!r}") + + +def _build_math_sequence(target_char_length: int) -> str: + """Build "1+1=2, 1+2=3, ..." until reaching target_char_length characters.""" + parts: list[str] = [] + total_len: int = 0 + a: int = 1 + b: int = 1 + + while total_len < target_char_length: + segment: str = f"{a}+{b}={a + b}, " + parts.append(segment) + total_len += len(segment) + b += 1 + if b > 100: + a += 1 + b = 1 + + return "".join(parts) diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/test_parallel_utils.py b/tests/fast/utils/debug_utils/run_megatron/cli/test_parallel_utils.py new file mode 100644 index 00000000000..fb4de761512 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/test_parallel_utils.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import dataclasses + +import pytest + +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig, parse_parallel_args + + +class TestParallelConfigDefaults: + def test_frozen(self) -> None: + config = ParallelConfig() + with pytest.raises(dataclasses.FrozenInstanceError): + config.tp = 2 # type: ignore[misc] + + def test_defaults(self) -> None: + config = ParallelConfig() + assert config.tp == 1 + assert config.pp == 1 + assert config.cp == 1 + assert config.ep is None + assert config.etp == 1 + + +class TestEffectiveEp: + def test_ep_none_falls_back_to_tp(self) -> None: + config = ParallelConfig(tp=4) + assert config.effective_ep == 4 + + def test_ep_explicit(self) -> None: + config = ParallelConfig(tp=4, ep=2) + assert config.effective_ep == 2 + + def test_ep_1_is_not_none(self) -> None: + config = ParallelConfig(tp=4, ep=1) + assert config.effective_ep == 1 + + +class TestNproc: + def test_all_ones(self) -> None: + assert ParallelConfig().nproc == 1 + + def test_tp_pp_cp(self) -> None: + config = ParallelConfig(tp=2, pp=3, cp=4) + assert config.nproc == 24 + + def test_ep_etp_dont_affect_nproc(self) -> None: + config = ParallelConfig(tp=2, ep=2, etp=2) + assert config.nproc == 2 + + +class TestPostInit: + def test_valid_nproc_divisible_by_ep(self) -> None: + ParallelConfig(tp=4, pp=1, cp=1, ep=2) + + def test_invalid_nproc_not_divisible_by_ep(self) -> None: + with pytest.raises(ValueError, match="not divisible by effective EP"): + ParallelConfig(tp=3, pp=1, cp=1, ep=2) + + def test_ep_none_uses_tp_for_validation(self) -> None: + # nproc=tp*pp*cp=2, effective_ep=tp=2, 2%2=0 → valid + ParallelConfig(tp=2) + + def test_ep_none_invalid(self) -> None: + # nproc=tp*pp*cp=2*3*1=6, effective_ep=tp=2, 6%2=0 → valid + ParallelConfig(tp=2, pp=3) + # nproc=3*1*1=3, effective_ep=3, 3%3=0 → valid + ParallelConfig(tp=3) + + +class TestDirName: + def test_tp_only(self) -> None: + # tp is never skipped (skip value is None), pp=1 skipped, cp=1 skipped, + # ep=None skipped, etp=1 skipped + name = ParallelConfig(tp=2).dir_name() + assert name == "tp2" + + def test_tp_cp(self) -> None: + name = ParallelConfig(tp=2, cp=4).dir_name() + assert name == "tp2_cp4" + + def test_ep_equals_tp_is_skipped(self) -> None: + # ep == tp → skipped + name = ParallelConfig(tp=4, ep=4).dir_name() + assert "ep" not in name + + def test_ep_differs_from_tp(self) -> None: + name = ParallelConfig(tp=4, ep=2).dir_name() + assert "ep2" in name + + def test_pp_1_skipped(self) -> None: + name = ParallelConfig(tp=2, pp=1).dir_name() + assert "pp" not in name + + def test_pp_gt1_included(self) -> None: + name = ParallelConfig(tp=2, pp=2).dir_name() + assert "pp2" in name + + def test_etp_gt1_included(self) -> None: + name = ParallelConfig(tp=2, etp=2).dir_name() + assert "etp2" in name + + def test_all_fields(self) -> None: + name = ParallelConfig(tp=2, pp=3, cp=4, ep=8, etp=2).dir_name() + assert name == "tp2_pp3_cp4_ep8_etp2" + + def test_default_config(self) -> None: + # tp=1, pp=1, cp=1, ep=None, etp=1 + # tp=1 is not None so included; pp=1 skipped; cp=1 skipped; ep=None skipped; etp=1 skipped + name = ParallelConfig().dir_name() + assert name == "tp1" + + +class TestFromParsedArgs: + def test_full_args(self) -> None: + parsed = {"tp": 2, "pp": 3, "cp": 4, "ep": 8, "etp": 2} + config = ParallelConfig.from_parsed_args(parsed) + assert config.tp == 2 + assert config.pp == 3 + assert config.cp == 4 + assert config.ep == 8 + assert config.etp == 2 + + def test_partial_args_use_defaults(self) -> None: + parsed = {"tp": 4} + config = ParallelConfig.from_parsed_args(parsed) + assert config.tp == 4 + assert config.pp == 1 + assert config.cp == 1 + assert config.ep is None + assert config.etp == 1 + + def test_empty_args(self) -> None: + config = ParallelConfig.from_parsed_args({}) + assert config == ParallelConfig() + + +class TestFromRunArgs: + def test_extracts_fields(self) -> None: + from miles.utils.debug_utils.run_megatron.cli.commands.args import RunArgs + + args = RunArgs( + model_type="deepseek_v3", + hf_checkpoint="/fake", # type: ignore[arg-type] + tp=4, + pp=2, + cp=2, + ep=8, + etp=2, + ) + config = ParallelConfig.from_run_args(args) + assert config.tp == 4 + assert config.pp == 2 + assert config.cp == 2 + assert config.ep == 8 + assert config.etp == 2 + + +class TestStr: + def test_contains_all_fields(self) -> None: + config = ParallelConfig(tp=2, pp=3, cp=4, ep=8, etp=2) + s = str(config) + assert "tp=2" in s + assert "pp=3" in s + assert "cp=4" in s + assert "ep=8" in s + assert "etp=2" in s + assert "nproc=24" in s + + +class TestParseParallelArgs: + def test_basic(self) -> None: + result = parse_parallel_args("--tp 2 --cp 4") + assert result == {"tp": 2, "cp": 4} + + def test_all_flags(self) -> None: + result = parse_parallel_args("--tp 2 --pp 3 --cp 4 --ep 8 --etp 2") + assert result == {"tp": 2, "pp": 3, "cp": 4, "ep": 8, "etp": 2} + + def test_empty_string(self) -> None: + result = parse_parallel_args("") + assert result == {} + + def test_omitted_flags_excluded(self) -> None: + result = parse_parallel_args("--tp 2") + assert "pp" not in result + assert "cp" not in result diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/test_path_utils.py b/tests/fast/utils/debug_utils/run_megatron/cli/test_path_utils.py new file mode 100644 index 00000000000..3858d58d24a --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/test_path_utils.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import pytest +import typer + +from miles.utils.debug_utils.run_megatron.cli.path_utils import resolve_megatron_path, resolve_model_script + + +class TestResolveMegatronPath: + def test_explicit_path(self) -> None: + explicit = Path("/custom/megatron") + assert resolve_megatron_path(explicit) is explicit + + def test_env_var_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MEGATRON_PATH", "/env/megatron") + assert resolve_megatron_path(None) == Path("/env/megatron") + + def test_default(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("MEGATRON_PATH", raising=False) + assert resolve_megatron_path(None) == Path("/root/Megatron-LM") + + def test_explicit_overrides_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MEGATRON_PATH", "/env/megatron") + explicit = Path("/custom/megatron") + assert resolve_megatron_path(explicit) is explicit + + +class TestResolveModelScript: + def test_returns_path_when_exists(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + scripts_dir = tmp_path / "scripts" / "models" + scripts_dir.mkdir(parents=True) + script_file = scripts_dir / "deepseek_v3.sh" + script_file.touch() + + monkeypatch.setattr( + "miles.utils.debug_utils.run_megatron.cli.path_utils._resolve_repo_base", + lambda: tmp_path, + ) + result = resolve_model_script("deepseek_v3") + assert result == script_file + + def test_raises_when_missing(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + scripts_dir = tmp_path / "scripts" / "models" + scripts_dir.mkdir(parents=True) + + monkeypatch.setattr( + "miles.utils.debug_utils.run_megatron.cli.path_utils._resolve_repo_base", + lambda: tmp_path, + ) + with pytest.raises(typer.BadParameter, match="Model script not found"): + resolve_model_script("nonexistent") diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/test_prompt_utils.py b/tests/fast/utils/debug_utils/run_megatron/cli/test_prompt_utils.py new file mode 100644 index 00000000000..2804a5f5c1b --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/test_prompt_utils.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import dataclasses +import json +from pathlib import Path + +import pytest +from transformers import AutoTokenizer + +from miles.utils.debug_utils.run_megatron.cli.prompt_utils import ( + PromptConfig, + _build_math_sequence, + _resolve_raw_text, + generate_token_ids, + write_token_ids_to_tmpfile, +) + +QWEN3_MODEL_ID = "Qwen/Qwen3-0.6B" + + +class TestPromptConfig: + def test_frozen(self) -> None: + config = PromptConfig() + with pytest.raises(dataclasses.FrozenInstanceError): + config.mode = "text" # type: ignore[misc] + + def test_defaults(self) -> None: + config = PromptConfig() + assert config.mode == "math" + assert config.text is None + assert config.file is None + assert config.seq_length == 137 + assert config.apply_chat_template is False + + +class TestResolveRawText: + def test_math_mode(self) -> None: + text = _resolve_raw_text(PromptConfig(mode="math", seq_length=10)) + assert "1+1=2" in text + + def test_text_mode(self) -> None: + text = _resolve_raw_text(PromptConfig(mode="text", text="hello world")) + assert text == "hello world" + + def test_text_mode_missing_raises(self) -> None: + with pytest.raises(ValueError, match="--prompt-text is required"): + _resolve_raw_text(PromptConfig(mode="text")) + + def test_file_mode(self, tmp_path: Path) -> None: + f = tmp_path / "prompt.txt" + f.write_text("content from file") + text = _resolve_raw_text(PromptConfig(mode="file", file=f)) + assert text == "content from file" + + def test_file_mode_missing_raises(self) -> None: + with pytest.raises(ValueError, match="--prompt-file is required"): + _resolve_raw_text(PromptConfig(mode="file")) + + def test_unknown_mode_raises(self) -> None: + with pytest.raises(ValueError, match="Unknown prompt mode"): + _resolve_raw_text(PromptConfig(mode="unknown")) # type: ignore[arg-type] + + +class TestBuildMathSequence: + def test_starts_with_1_plus_1(self) -> None: + seq = _build_math_sequence(target_char_length=100) + assert seq.startswith("1+1=2") + + def test_reaches_target_length(self) -> None: + target = 500 + seq = _build_math_sequence(target_char_length=target) + assert len(seq) >= target + + def test_small_target(self) -> None: + seq = _build_math_sequence(target_char_length=1) + assert len(seq) > 0 + assert "1+1=2" in seq + + def test_b_wraps_after_100(self) -> None: + seq = _build_math_sequence(target_char_length=10000) + assert "2+1=" in seq + + +class TestWriteTokenIdsToTmpfile: + def test_roundtrip_json(self) -> None: + token_ids = [10, 20, 30, 40] + path = write_token_ids_to_tmpfile(token_ids) + loaded = json.loads(path.read_text()) + assert loaded == token_ids + + def test_prefix_and_suffix(self) -> None: + path = write_token_ids_to_tmpfile([1, 2, 3]) + assert path.name.startswith("run_megatron_token_ids_") + assert path.name.endswith(".json") + + +class TestGenerateTokenIds: + """Uses real Qwen3-0.6B tokenizer.""" + + @pytest.fixture(scope="class") + def tokenizer(self) -> AutoTokenizer: + return AutoTokenizer.from_pretrained(QWEN3_MODEL_ID, trust_remote_code=True) + + def test_correct_length(self, tokenizer: AutoTokenizer) -> None: + result = generate_token_ids( + prompt=PromptConfig(mode="math", seq_length=50), + tokenizer_path=Path(QWEN3_MODEL_ID), + ) + assert len(result) == 50 + assert all(isinstance(t, int) for t in result) + + def test_chat_template_changes_tokens(self, tokenizer: AutoTokenizer) -> None: + prompt_text = "The quick brown fox jumps over the lazy dog. " * 5 + seq_length = 10 + + without_template = generate_token_ids( + prompt=PromptConfig( + mode="text", + text=prompt_text, + seq_length=seq_length, + apply_chat_template=False, + ), + tokenizer_path=Path(QWEN3_MODEL_ID), + ) + with_template = generate_token_ids( + prompt=PromptConfig( + mode="text", + text=prompt_text, + seq_length=seq_length, + apply_chat_template=True, + ), + tokenizer_path=Path(QWEN3_MODEL_ID), + ) + assert without_template != with_template + + def test_math_mode_deterministic(self) -> None: + prompt = PromptConfig(mode="math", seq_length=50) + r1 = generate_token_ids(prompt=prompt, tokenizer_path=Path(QWEN3_MODEL_ID)) + r2 = generate_token_ids(prompt=prompt, tokenizer_path=Path(QWEN3_MODEL_ID)) + assert r1 == r2 + + def test_too_short_raises(self) -> None: + with pytest.raises(ValueError, match="less than seq_length"): + generate_token_ids( + prompt=PromptConfig(mode="text", text="hi", seq_length=9999), + tokenizer_path=Path(QWEN3_MODEL_ID), + ) + + def test_text_mode_encodes_real_text(self) -> None: + result = generate_token_ids( + prompt=PromptConfig(mode="text", text="The quick brown fox jumps over the lazy dog " * 10, seq_length=30), + tokenizer_path=Path(QWEN3_MODEL_ID), + ) + assert len(result) == 30 + + def test_file_mode(self, tmp_path: Path) -> None: + f = tmp_path / "prompt.txt" + f.write_text("Hello world, this is a test prompt. " * 20) + result = generate_token_ids( + prompt=PromptConfig(mode="file", file=f, seq_length=40), + tokenizer_path=Path(QWEN3_MODEL_ID), + ) + assert len(result) == 40 From c00eb5b6f80a21baebf70bb6db976cce93a4c8d7 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:29:08 +0800 Subject: [PATCH 0025/1130] Add worker main and run command for megatron runner (#773) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../debug_utils/run_megatron/__main__.py | 4 + .../debug_utils/run_megatron/cli/__init__.py | 3 + .../run_megatron/cli/commands/__init__.py | 14 ++ .../run_megatron/cli/commands/run.py | 98 ++++++++ .../run_megatron/cli/worker_executor.py | 115 +++++++++ .../debug_utils/run_megatron/worker/main.py | 187 ++++++++++++++ .../run_megatron/cli/commands/test_run.py | 76 ++++++ .../run_megatron/cli/test_worker_executor.py | 182 ++++++++++++++ .../debug_utils/run_megatron/conftest.py | 14 ++ .../run_megatron/worker/test_main.py | 232 ++++++++++++++++++ 10 files changed, 925 insertions(+) create mode 100644 miles/utils/debug_utils/run_megatron/__main__.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/__init__.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/commands/run.py create mode 100644 miles/utils/debug_utils/run_megatron/cli/worker_executor.py create mode 100644 miles/utils/debug_utils/run_megatron/worker/main.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/conftest.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/test_main.py diff --git a/miles/utils/debug_utils/run_megatron/__main__.py b/miles/utils/debug_utils/run_megatron/__main__.py new file mode 100644 index 00000000000..9b08afaa8b9 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/__main__.py @@ -0,0 +1,4 @@ +from miles.utils.debug_utils.run_megatron.cli import app + +if __name__ == "__main__": + app() diff --git a/miles/utils/debug_utils/run_megatron/cli/__init__.py b/miles/utils/debug_utils/run_megatron/cli/__init__.py new file mode 100644 index 00000000000..4c11e373e75 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/__init__.py @@ -0,0 +1,3 @@ +from miles.utils.debug_utils.run_megatron.cli.commands import app + +__all__ = ["app"] diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py index e69de29bb2d..d107309ba60 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py @@ -0,0 +1,14 @@ +"""CLI commands for run_megatron. + +Usage: + python -m miles.utils.debug_utils.run_megatron run ... + python -m miles.utils.debug_utils.run_megatron show-model-args ... +""" + +import typer + +from miles.utils.debug_utils.run_megatron.cli.commands import run + +app: typer.Typer = typer.Typer(pretty_exceptions_enable=False) + +run.register(app) diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/run.py b/miles/utils/debug_utils/run_megatron/cli/commands/run.py new file mode 100644 index 00000000000..1694b3a2ddc --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/commands/run.py @@ -0,0 +1,98 @@ +"""``run`` and ``show-model-args`` CLI commands.""" + +from pathlib import Path +from typing import Annotated + +import typer + +from miles.utils.debug_utils.run_megatron.cli.commands.args import RunArgs +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig +from miles.utils.debug_utils.run_megatron.cli.path_utils import resolve_megatron_path, resolve_model_script +from miles.utils.debug_utils.run_megatron.cli.prompt_utils import ( + PromptConfig, + generate_token_ids, + write_token_ids_to_tmpfile, +) +from miles.utils.debug_utils.run_megatron.cli.worker_executor import ( + build_dumper_env, + build_torchrun_cmd, + build_worker_args, +) +from miles.utils.debug_utils.run_megatron.worker.script_args import WorkerScriptArgs +from miles.utils.misc import exec_command +from miles.utils.typer_utils import dataclass_cli + + +def register(app: typer.Typer) -> None: + """Register ``run`` and ``show-model-args`` commands on *app*.""" + app.command()(run) + app.command(name="show-model-args")(show_model_args) + + +def run_impl(args: RunArgs) -> None: + """Core run logic, called by both ``run`` command and ``run_and_compare``.""" + parallel: ParallelConfig = ParallelConfig.from_run_args(args) + + resolved_megatron: Path = resolve_megatron_path(args.megatron_path) + + prompt: PromptConfig = PromptConfig( + mode=args.prompt_mode, # type: ignore[arg-type] + text=args.prompt_text, + file=args.prompt_file, + seq_length=args.seq_length, + apply_chat_template=args.apply_chat_template, + ) + token_ids: list[int] = generate_token_ids(prompt=prompt, tokenizer_path=args.hf_checkpoint) + token_ids_file: Path = write_token_ids_to_tmpfile(token_ids) + print(f"[cli] Token IDs written to {token_ids_file} ({len(token_ids)} tokens)", flush=True) + + script_args: WorkerScriptArgs = WorkerScriptArgs( + hf_checkpoint=args.hf_checkpoint, + token_ids_file=token_ids_file, + role=args.role, + ref_load=args.ref_load, + run_backward=args.run_backward, + source_patcher_config=args.source_patcher_config, + ) + worker_args_str: str = build_worker_args( + parallel=parallel, + sp=args.sp, + seq_length=args.seq_length, + batch_size=args.batch_size, + script_args=script_args, + extra_args=args.extra_args, + ) + + dumper_env: dict[str, str] = build_dumper_env( + output_dir=args.output_dir, + run_backward=args.run_backward, + dumper_filter=args.dumper_filter, + ) + env_exports: str = " && ".join(f"export {k}='{v}'" for k, v in dumper_env.items()) + + cmd: str = build_torchrun_cmd( + model_type=args.model_type, + megatron_path=resolved_megatron, + nproc=parallel.nproc, + worker_args=worker_args_str, + ) + exec_command(f"{env_exports} && {cmd}") + print(f"[cli] Run completed. Output: {args.output_dir}", flush=True) + + +@dataclass_cli(env_var_prefix="") +def run(args: RunArgs) -> None: + """Launch torchrun to run Megatron standalone forward (or forward+backward).""" + run_impl(args) + + +def show_model_args( + model_type: Annotated[str, typer.Option(help="Model type matching scripts/models/{model_type}.sh")], +) -> None: + """Show the MODEL_ARGS for a given model type (debug helper).""" + output: str | None = exec_command( + f'source "{resolve_model_script(model_type)}" && echo "${{MODEL_ARGS[@]}}"', + capture_output=True, + ) + if output: + print(output.strip()) diff --git a/miles/utils/debug_utils/run_megatron/cli/worker_executor.py b/miles/utils/debug_utils/run_megatron/cli/worker_executor.py new file mode 100644 index 00000000000..427fab9333a --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/worker_executor.py @@ -0,0 +1,115 @@ +"""Build torchrun command and worker arguments.""" + +from pathlib import Path + +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig +from miles.utils.debug_utils.run_megatron.cli.path_utils import resolve_model_script +from miles.utils.debug_utils.run_megatron.worker.script_args import WORKER_SCRIPT_ARGS_BRIDGE, WorkerScriptArgs + + +def build_torchrun_cmd( + *, + model_type: str, + megatron_path: Path, + nproc: int, + worker_args: str, +) -> str: + """Build the full shell command to launch the worker via torchrun.""" + model_script: Path = resolve_model_script(model_type) + worker_module: str = "miles.utils.debug_utils.run_megatron.worker.main" + + cmd: str = ( + f'source "{model_script}" && ' + f"PYTHONPATH={megatron_path}:$PYTHONPATH " + f"CUDA_DEVICE_MAX_CONNECTIONS=1 " + f"torchrun --nproc-per-node {nproc} " + f"-m {worker_module} " + f"${{MODEL_ARGS[@]}} " + f"--hidden-dropout 0 --attention-dropout 0 " + f"{worker_args}" + ) + return cmd + + +def build_worker_args( + *, + parallel: ParallelConfig, + sp: bool, + seq_length: int, + batch_size: int, + script_args: WorkerScriptArgs, + extra_args: str, +) -> str: + """Build the worker argument string. + + Megatron-native flags come from declarative tables in + ``_build_megatron_flags``; ``--script-*`` flags come from the bridge. + """ + parts: list[str] = [ + _build_megatron_flags( + parallel=parallel, + sp=sp, + seq_length=seq_length, + batch_size=batch_size, + ), + WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(script_args), + ] + if extra_args: + parts.append(extra_args) + + return " ".join(parts) + + +def build_dumper_env( + *, + output_dir: Path, + run_backward: bool, + dumper_filter: str, +) -> dict[str, str]: + """Build DUMPER_* environment variables for the worker.""" + env: dict[str, str] = { + "DUMPER_ENABLE": "1", + "DUMPER_DIR": str(output_dir), + "DUMPER_EXP_NAME": "standalone", + } + if dumper_filter: + env["DUMPER_FILTER"] = dumper_filter + if run_backward: + env["DUMPER_ENABLE_MODEL_GRAD"] = "1" + return env + + +def _build_megatron_flags( + *, + parallel: ParallelConfig, + sp: bool, + seq_length: int, + batch_size: int, +) -> str: + """Build Megatron-native CLI flags from declarative tables.""" + key_value_args: list[tuple[str, object | None]] = [ + ("--tensor-model-parallel-size", parallel.tp), + ("--pipeline-model-parallel-size", parallel.pp), + ("--context-parallel-size", parallel.cp), + ("--expert-model-parallel-size", parallel.effective_ep), + ("--expert-tensor-parallel-size", parallel.etp), + ("--seq-length", seq_length), + ("--micro-batch-size", batch_size), + ("--global-batch-size", batch_size), + ] + + bool_flags: list[tuple[str, bool]] = [ + ("--sequence-parallel", sp), + ("--bf16", True), + ("--no-gradient-accumulation-fusion", True), + ] + + parts: list[str] = [] + for flag, value in key_value_args: + if value is not None: + parts.append(f"{flag} {value}") + for flag, condition in bool_flags: + if condition: + parts.append(flag) + + return " ".join(parts) diff --git a/miles/utils/debug_utils/run_megatron/worker/main.py b/miles/utils/debug_utils/run_megatron/worker/main.py new file mode 100644 index 00000000000..892814a3cf8 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/worker/main.py @@ -0,0 +1,187 @@ +"""Torchrun worker script for standalone Megatron forward/backward. + +This script is launched by ``cli.py run`` via ``torchrun`` and runs inside +each GPU process. It uses Megatron's argparse (to consume ``MODEL_ARGS`` +from the shell script) plus a handful of custom arguments. + +Not intended to be run directly — use ``python -m miles.utils.debug_utils.run_megatron run`` instead. +""" + +import argparse +import json +import os +from collections.abc import Callable +from functools import partial +from pathlib import Path +from typing import Any + +import torch +import torch.distributed as dist +from megatron.core.enums import ModelType +from megatron.core.pipeline_parallel import get_forward_backward_func +from megatron.training.arguments import parse_args, validate_args +from megatron.training.training import get_model +from sglang.srt.debug_utils.dumper import dumper +from sglang.srt.debug_utils.source_patcher import apply_patches_from_config + +from miles.backends.megatron_utils.arguments import set_default_megatron_args +from miles.backends.megatron_utils.checkpoint import load_checkpoint +from miles.backends.megatron_utils.initialize import init +from miles.backends.megatron_utils.model_provider import get_model_provider_func +from miles.utils.debug_utils.run_megatron.worker.batch import loss_func, prepare_batch +from miles.utils.debug_utils.run_megatron.worker.script_args import WORKER_SCRIPT_ARGS_BRIDGE, WorkerScriptArgs + + +def main() -> None: + args, script = _parse_args() + _initialize_megatron(args) + + rank: int = dist.get_rank() + if rank == 0: + _print_config(args, script) + + if script.source_patcher_config: + _apply_source_patches(script.source_patcher_config) + + model: list[Any] = _build_and_load_model(args, script) + + for m in model: + dumper.register_non_intrusive_dumper(m) + + token_ids: list[int] = json.loads(script.token_ids_file.read_text()) + batch: dict[str, torch.Tensor] = prepare_batch( + token_ids=token_ids, + batch_size=args.micro_batch_size, + ) + + if rank == 0: + print(f"[worker] input_ids shape={batch['input_ids'].shape}", flush=True) + + _run_forward_backward( + args=args, + script=script, + model=model, + batch=batch, + ) + + _finalize_dumper() + + if rank == 0: + print("[worker] Done.", flush=True) + + dist.barrier() + dist.destroy_process_group() + + +def _parse_args() -> tuple[argparse.Namespace, WorkerScriptArgs]: + args: argparse.Namespace = parse_args(extra_args_provider=WORKER_SCRIPT_ARGS_BRIDGE.register_on_parser) + script_args: WorkerScriptArgs = WORKER_SCRIPT_ARGS_BRIDGE.from_namespace(args) + + if script_args.ref_load is not None: + args.load = str(script_args.ref_load) + + return args, script_args + + +def _initialize_megatron(args: argparse.Namespace) -> None: + torch.distributed.init_process_group(backend="nccl") + local_rank: int = int(os.environ.get("LOCAL_RANK", 0)) + torch.cuda.set_device(local_rank) + + args.hf_checkpoint = str(args.script_hf_checkpoint) + args.__dict__.setdefault("megatron_to_hf_mode", "raw") + args.__dict__.setdefault("decrease_batch_size_if_needed", False) + set_default_megatron_args(args) + validate_args(args) + + init(args) + + +def _build_and_load_model(args: argparse.Namespace, script: WorkerScriptArgs) -> list[Any]: + model_provider: Callable[..., Any] = get_model_provider_func(args, role=script.role) + model: list[Any] = get_model(model_provider, ModelType.encoder_or_decoder) + + if args.load is not None: + load_checkpoint( + model, + optimizer=None, + opt_param_scheduler=None, + checkpointing_context=None, + skip_load_to_model_and_opt=False, + ) + + for m in model: + m.train() + return model + + +def _apply_source_patches(config_path: Path) -> None: + yaml_content: str = config_path.read_text() + apply_patches_from_config( + yaml_content, + extra_imports=["from sglang.srt.debug_utils.dumper import dumper"], + ) + print(f"[worker] Applied source patches from {config_path}", flush=True) + + +def _run_forward_backward( + args: argparse.Namespace, + script: WorkerScriptArgs, + model: list[Any], + batch: dict[str, torch.Tensor], +) -> torch.Tensor | None: + """Run forward (and optionally backward) pass, returning captured logits.""" + forward_backward_func: Callable[..., Any] = get_forward_backward_func() + captured: list[torch.Tensor] = [] + + def forward_step_func( + data_iterator: Any, + model_chunk: Any, + ) -> tuple[torch.Tensor, partial[tuple[torch.Tensor, dict[str, Any]]]]: + data: dict[str, torch.Tensor] = next(data_iterator) + output: torch.Tensor = model_chunk( + input_ids=data["input_ids"], + position_ids=data["position_ids"], + attention_mask=data.get("attention_mask"), + runtime_gather_output=True, + ) + captured.append(output.detach()) + return output, partial(loss_func, data["labels"]) + + losses: Any = forward_backward_func( + forward_step_func=forward_step_func, + data_iterator=iter([batch]), + model=model, + num_microbatches=1, + seq_length=args.seq_length, + micro_batch_size=args.micro_batch_size, + forward_only=not script.run_backward, + ) + + rank: int = dist.get_rank() + if rank == 0 and losses: + print(f"[worker rank={rank}] losses={losses}", flush=True) + + return captured[0] if captured else None + + +def _print_config(args: argparse.Namespace, script: WorkerScriptArgs) -> None: + print(f"[worker] seq_length={args.seq_length}, micro_batch_size={args.micro_batch_size}", flush=True) + print( + f"[worker] tp={args.tensor_model_parallel_size}, pp={args.pipeline_model_parallel_size}, " + f"cp={args.context_parallel_size}, ep={args.expert_model_parallel_size}, " + f"etp={args.expert_tensor_parallel_size}", + flush=True, + ) + print(f"[worker] run_backward={script.run_backward}, role={script.role}", flush=True) + + +def _finalize_dumper() -> None: + """Step + disable dumper after forward/backward.""" + if os.environ.get("DUMPER_ENABLE", "0") == "1": + dumper.step() + dumper.configure(enable=False) + + +if __name__ == "__main__": + main() diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py new file mode 100644 index 00000000000..d7523d0f3b5 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from collections.abc import Generator +from pathlib import Path +from unittest.mock import patch + +import pytest + +from miles.utils.debug_utils.run_megatron.cli.commands.args import RunArgs +from miles.utils.debug_utils.run_megatron.cli.commands.run import run_impl + + +def _make_run_args(**overrides: object) -> RunArgs: + defaults = dict( + model_type="deepseek_v3", + hf_checkpoint=Path("/fake/hf"), + output_dir=Path("/tmp/dump"), + ) + defaults.update(overrides) + return RunArgs(**defaults) # type: ignore[arg-type] + + +class TestRunImplExecCommand: + """Only mock exec_command, generate_token_ids, write_token_ids_to_tmpfile, + and resolve_model_script — let the rest (build_worker_args, build_dumper_env, + build_torchrun_cmd, ParallelConfig, WorkerScriptArgs) run for real.""" + + @pytest.fixture(autouse=True) + def _patch_externals(self) -> Generator[None, None, None]: + with ( + patch("miles.utils.debug_utils.run_megatron.cli.commands.run.exec_command") as mock_exec, + patch( + "miles.utils.debug_utils.run_megatron.cli.commands.run.generate_token_ids", + return_value=list(range(200)), + ), + patch( + "miles.utils.debug_utils.run_megatron.cli.commands.run.write_token_ids_to_tmpfile", + return_value=Path("/tmp/tokens.json"), + ), + patch( + "miles.utils.debug_utils.run_megatron.cli.worker_executor.resolve_model_script", + return_value=Path("/repo/scripts/models/deepseek_v3.sh"), + ), + ): + self.mock_exec = mock_exec + yield + + def test_cmd_contains_torchrun(self) -> None: + run_impl(_make_run_args()) + self.mock_exec.assert_called_once() + cmd = self.mock_exec.call_args[0][0] + assert "torchrun" in cmd + + def test_cmd_contains_parallel_config(self) -> None: + run_impl(_make_run_args(tp=4, pp=2)) + cmd = self.mock_exec.call_args[0][0] + assert "--tensor-model-parallel-size 4" in cmd + assert "--pipeline-model-parallel-size 2" in cmd + assert "--nproc-per-node 8" in cmd + + def test_cmd_contains_dumper_env(self) -> None: + run_impl(_make_run_args(output_dir=Path("/out/test"), dumper_filter="logits")) + cmd = self.mock_exec.call_args[0][0] + assert "DUMPER_ENABLE=1" in cmd + assert "DUMPER_DIR=/out/test" in cmd + assert "DUMPER_FILTER=logits" in cmd + + def test_cmd_contains_script_args(self) -> None: + run_impl(_make_run_args(role="critic")) + cmd = self.mock_exec.call_args[0][0] + assert "--script-role critic" in cmd + + def test_backward_enables_grad_env(self) -> None: + run_impl(_make_run_args(run_backward=True)) + cmd = self.mock_exec.call_args[0][0] + assert "DUMPER_ENABLE_MODEL_GRAD=1" in cmd diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py b/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py new file mode 100644 index 00000000000..d64518a3296 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py @@ -0,0 +1,182 @@ +from pathlib import Path +from unittest.mock import patch + +from tests.fast.utils.debug_utils.run_megatron.conftest import make_script_args + +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig +from miles.utils.debug_utils.run_megatron.cli.worker_executor import ( + _build_megatron_flags, + build_dumper_env, + build_torchrun_cmd, + build_worker_args, +) + + +class TestBuildDumperEnv: + def test_basic_env_vars(self, tmp_path: Path) -> None: + env = build_dumper_env(output_dir=tmp_path, run_backward=False, dumper_filter="") + assert env["DUMPER_ENABLE"] == "1" + assert env["DUMPER_DIR"] == str(tmp_path) + assert env["DUMPER_EXP_NAME"] == "standalone" + + def test_filter_when_non_empty(self, tmp_path: Path) -> None: + env = build_dumper_env(output_dir=tmp_path, run_backward=False, dumper_filter="hidden_states") + assert env["DUMPER_FILTER"] == "hidden_states" + + def test_filter_omitted_when_empty(self, tmp_path: Path) -> None: + env = build_dumper_env(output_dir=tmp_path, run_backward=False, dumper_filter="") + assert "DUMPER_FILTER" not in env + + def test_backward_enables_grad(self, tmp_path: Path) -> None: + env = build_dumper_env(output_dir=tmp_path, run_backward=True, dumper_filter="") + assert env["DUMPER_ENABLE_MODEL_GRAD"] == "1" + + def test_no_grad_forward_only(self, tmp_path: Path) -> None: + env = build_dumper_env(output_dir=tmp_path, run_backward=False, dumper_filter="") + assert "DUMPER_ENABLE_MODEL_GRAD" not in env + + +class TestBuildMegatronFlags: + def test_parallel_sizes(self) -> None: + flags = _build_megatron_flags( + parallel=ParallelConfig(tp=2, pp=4, cp=2, ep=8, etp=2), + sp=False, + seq_length=128, + batch_size=1, + ) + assert "--tensor-model-parallel-size 2" in flags + assert "--pipeline-model-parallel-size 4" in flags + assert "--context-parallel-size 2" in flags + assert "--expert-model-parallel-size 8" in flags + assert "--expert-tensor-parallel-size 2" in flags + + def test_seq_length_batch_size(self) -> None: + flags = _build_megatron_flags( + parallel=ParallelConfig(), + sp=False, + seq_length=256, + batch_size=4, + ) + assert "--seq-length 256" in flags + assert "--micro-batch-size 4" in flags + assert "--global-batch-size 4" in flags + + def test_bf16_always(self) -> None: + flags = _build_megatron_flags( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + ) + assert "--bf16" in flags + + def test_sp_on_off(self) -> None: + flags_on = _build_megatron_flags( + parallel=ParallelConfig(), + sp=True, + seq_length=128, + batch_size=1, + ) + assert "--sequence-parallel" in flags_on + + flags_off = _build_megatron_flags( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + ) + assert "--sequence-parallel" not in flags_off + + +class TestBuildWorkerArgs: + def test_includes_parallel_flags(self) -> None: + result = build_worker_args( + parallel=ParallelConfig(tp=2), + sp=False, + seq_length=128, + batch_size=1, + script_args=make_script_args(), + extra_args="", + ) + assert "--tensor-model-parallel-size 2" in result + + def test_includes_script_args(self) -> None: + result = build_worker_args( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + script_args=make_script_args(hf_checkpoint=Path("/my/hf")), + extra_args="", + ) + assert "--script-hf-checkpoint /my/hf" in result + + def test_extra_args_appended(self) -> None: + result = build_worker_args( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + script_args=make_script_args(), + extra_args="--custom-flag value", + ) + assert result.endswith("--custom-flag value") + + def test_empty_extra_args(self) -> None: + result = build_worker_args( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + script_args=make_script_args(), + extra_args="", + ) + assert not result.endswith(" ") + + +class TestBuildTorchrunCmd: + @patch("miles.utils.debug_utils.run_megatron.cli.worker_executor.resolve_model_script") + def test_basic_structure(self, mock_resolve: object) -> None: + mock_resolve.return_value = Path("/repo/scripts/models/deepseek_v3.sh") # type: ignore[union-attr] + cmd = build_torchrun_cmd( + model_type="deepseek_v3", + megatron_path=Path("/megatron"), + nproc=4, + worker_args="--foo bar", + ) + assert "torchrun" in cmd + assert "source" in cmd + assert "PYTHONPATH" in cmd + + @patch("miles.utils.debug_utils.run_megatron.cli.worker_executor.resolve_model_script") + def test_nproc(self, mock_resolve: object) -> None: + mock_resolve.return_value = Path("/repo/scripts/models/test.sh") # type: ignore[union-attr] + cmd = build_torchrun_cmd( + model_type="test", + megatron_path=Path("/megatron"), + nproc=8, + worker_args="", + ) + assert "--nproc-per-node 8" in cmd + + @patch("miles.utils.debug_utils.run_megatron.cli.worker_executor.resolve_model_script") + def test_worker_args_in_cmd(self, mock_resolve: object) -> None: + mock_resolve.return_value = Path("/repo/scripts/models/test.sh") # type: ignore[union-attr] + cmd = build_torchrun_cmd( + model_type="test", + megatron_path=Path("/megatron"), + nproc=1, + worker_args="--my-flag 42", + ) + assert "--my-flag 42" in cmd + + @patch("miles.utils.debug_utils.run_megatron.cli.worker_executor.resolve_model_script") + def test_megatron_in_pythonpath(self, mock_resolve: object) -> None: + mock_resolve.return_value = Path("/repo/scripts/models/test.sh") # type: ignore[union-attr] + cmd = build_torchrun_cmd( + model_type="test", + megatron_path=Path("/my/megatron"), + nproc=1, + worker_args="", + ) + assert "PYTHONPATH=/my/megatron" in cmd diff --git a/tests/fast/utils/debug_utils/run_megatron/conftest.py b/tests/fast/utils/debug_utils/run_megatron/conftest.py new file mode 100644 index 00000000000..2ba7e6f6eb9 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/conftest.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pathlib import Path + +from miles.utils.debug_utils.run_megatron.worker.script_args import WorkerScriptArgs + + +def make_script_args(**overrides: object) -> WorkerScriptArgs: + defaults = dict( + hf_checkpoint=Path("/fake/hf"), + token_ids_file=Path("/tmp/tokens.json"), + ) + defaults.update(overrides) + return WorkerScriptArgs(**defaults) # type: ignore[arg-type] diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_main.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_main.py new file mode 100644 index 00000000000..d3a261dba15 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_main.py @@ -0,0 +1,232 @@ +"""Unit tests for worker/main.py functions. + +main.py has heavy top-level imports (megatron.training.arguments, etc.) +that aren't fully available in the lightweight test container. We intercept +the import by patching sys.modules for missing leaves *before* importing +the module under test. +""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path +from types import ModuleType +from typing import Any +from unittest.mock import MagicMock, patch + +import torch + + +def _ensure_module(dotted: str) -> ModuleType: + """Ensure *dotted* exists in sys.modules, creating stubs for any missing segments.""" + parts = dotted.split(".") + for i in range(len(parts)): + partial = ".".join(parts[: i + 1]) + if partial not in sys.modules: + sys.modules[partial] = ModuleType(partial) + return sys.modules[dotted] + + +# Stub modules whose top-level imports in main.py would fail. +_STUBS: dict[str, dict[str, Any]] = { + "megatron.training.arguments": { + "parse_args": MagicMock(), + "validate_args": MagicMock(), + }, + "megatron.training.training": { + "get_model": MagicMock(), + }, + "megatron.core.enums": { + "ModelType": MagicMock(), + }, + "megatron.core.pipeline_parallel": { + "get_forward_backward_func": MagicMock(), + }, + "megatron.core.mpu": MagicMock(), +} + +for _mod_path, _attrs in _STUBS.items(): + mod = _ensure_module(_mod_path) + if isinstance(_attrs, dict): + for attr_name, attr_val in _attrs.items(): + if not hasattr(mod, attr_name): + setattr(mod, attr_name, attr_val) + else: + # Replace the whole module with a MagicMock + sys.modules[_mod_path] = _attrs + +# Also ensure miles.backends.megatron_utils sub-modules have their needed symbols +for _sub in [ + "miles.backends.megatron_utils.arguments", + "miles.backends.megatron_utils.checkpoint", + "miles.backends.megatron_utils.initialize", + "miles.backends.megatron_utils.model_provider", +]: + mod = _ensure_module(_sub) + # Provide any names imported by main.py from these modules + for name in [ + "set_default_megatron_args", + "load_checkpoint", + "init", + "get_model_provider_func", + ]: + if not hasattr(mod, name): + setattr(mod, name, MagicMock()) + +from miles.utils.debug_utils.run_megatron.worker.main import ( # noqa: E402 + _apply_source_patches, + _finalize_dumper, + _parse_args, + _run_forward_backward, +) + +_MODULE = "miles.utils.debug_utils.run_megatron.worker.main" + + +class TestParseArgs: + @patch(f"{_MODULE}.WORKER_SCRIPT_ARGS_BRIDGE") + @patch(f"{_MODULE}.parse_args") + def test_ref_load_overrides_args_load( + self, + mock_parse_args: MagicMock, + mock_bridge: MagicMock, + ) -> None: + """When script_args.ref_load is set, args.load is overridden.""" + args = argparse.Namespace(load=None) + mock_parse_args.return_value = args + + script_args = MagicMock() + script_args.ref_load = Path("/some/path") + mock_bridge.from_namespace.return_value = script_args + + returned_args, returned_script = _parse_args() + + assert returned_args.load == "/some/path" + assert returned_script is script_args + + @patch(f"{_MODULE}.WORKER_SCRIPT_ARGS_BRIDGE") + @patch(f"{_MODULE}.parse_args") + def test_ref_load_none_preserves_original_load( + self, + mock_parse_args: MagicMock, + mock_bridge: MagicMock, + ) -> None: + """When script_args.ref_load is None, args.load stays as-is.""" + args = argparse.Namespace(load="/orig") + mock_parse_args.return_value = args + + script_args = MagicMock() + script_args.ref_load = None + mock_bridge.from_namespace.return_value = script_args + + returned_args, _ = _parse_args() + + assert returned_args.load == "/orig" + + +class TestApplySourcePatches: + @patch(f"{_MODULE}.apply_patches_from_config") + def test_reads_yaml_and_calls_patcher( + self, + mock_apply: MagicMock, + tmp_path: Path, + ) -> None: + config_file = tmp_path / "patches.yaml" + config_file.write_text("patches:\n - target: foo") + + _apply_source_patches(config_file) + + mock_apply.assert_called_once() + call_args = mock_apply.call_args + assert call_args[0][0] == "patches:\n - target: foo" + assert "extra_imports" in call_args[1] or len(call_args[0]) > 1 + + +class TestRunForwardBackward: + @patch(f"{_MODULE}.dist") + @patch(f"{_MODULE}.get_forward_backward_func") + def test_forward_only_when_run_backward_false( + self, + mock_get_fb: MagicMock, + mock_dist: MagicMock, + ) -> None: + """run_backward=False → forward_only=True passed to the func.""" + mock_fb_func = MagicMock(return_value=[]) + mock_get_fb.return_value = mock_fb_func + mock_dist.get_rank.return_value = 1 + + args = argparse.Namespace(seq_length=4, micro_batch_size=1) + script = MagicMock() + script.run_backward = False + + model = [MagicMock()] + batch = { + "input_ids": torch.tensor([[1, 2, 3, 4]]), + "position_ids": torch.arange(4).unsqueeze(0), + "labels": torch.tensor([[2, 3, 4, -100]]), + } + + _run_forward_backward(args=args, script=script, model=model, batch=batch) + + call_kwargs = mock_fb_func.call_args[1] + assert call_kwargs["forward_only"] is True + + @patch(f"{_MODULE}.dist") + @patch(f"{_MODULE}.get_forward_backward_func") + def test_no_logits_captured_returns_none( + self, + mock_get_fb: MagicMock, + mock_dist: MagicMock, + ) -> None: + """If no logits captured (non-last PP stage), returns None.""" + mock_fb_func = MagicMock(return_value=[]) + mock_get_fb.return_value = mock_fb_func + mock_dist.get_rank.return_value = 1 + + args = argparse.Namespace(seq_length=4, micro_batch_size=1) + script = MagicMock() + script.run_backward = False + + result = _run_forward_backward( + args=args, + script=script, + model=[MagicMock()], + batch={ + "input_ids": torch.tensor([[1, 2]]), + "position_ids": torch.arange(2).unsqueeze(0), + "labels": torch.tensor([[2, -100]]), + }, + ) + + assert result is None + + +class TestFinalizeDumper: + @patch(f"{_MODULE}.dumper") + def test_dumper_enable_env_triggers_step_and_disable( + self, + mock_dumper: MagicMock, + ) -> None: + with patch.dict(os.environ, {"DUMPER_ENABLE": "1"}): + _finalize_dumper() + + mock_dumper.step.assert_called_once() + mock_dumper.configure.assert_called_once_with(enable=False) + + @patch(f"{_MODULE}.dumper") + def test_no_dumper_enable_env_does_nothing( + self, + mock_dumper: MagicMock, + ) -> None: + with patch.dict(os.environ, {}, clear=True): + env_backup = os.environ.pop("DUMPER_ENABLE", None) + try: + _finalize_dumper() + finally: + if env_backup is not None: + os.environ["DUMPER_ENABLE"] = env_backup + + mock_dumper.step.assert_not_called() + mock_dumper.configure.assert_not_called() From 91f0373462cbf9e7953df3167c6b5d12e289feff Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:30:27 +0800 Subject: [PATCH 0026/1130] Add routing replay for megatron runner (#774) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- miles/backends/training_utils/cp_utils.py | 26 ++ .../run_megatron/cli/commands/args.py | 2 + .../run_megatron/cli/commands/run.py | 5 + .../run_megatron/cli/worker_executor.py | 6 + .../debug_utils/run_megatron/worker/main.py | 12 + .../debug_utils/run_megatron/worker/replay.py | 160 +++++++++ .../run_megatron/worker/script_args.py | 2 + .../run_megatron/cli/commands/test_run.py | 30 ++ .../run_megatron/cli/test_worker_executor.py | 47 +++ .../run_megatron/worker/test_replay.py | 303 ++++++++++++++++++ .../run_megatron/worker/test_script_args.py | 4 + 11 files changed, 597 insertions(+) create mode 100644 miles/utils/debug_utils/run_megatron/worker/replay.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/test_replay.py diff --git a/miles/backends/training_utils/cp_utils.py b/miles/backends/training_utils/cp_utils.py index fd1b1cb6d28..5f077c9e748 100644 --- a/miles/backends/training_utils/cp_utils.py +++ b/miles/backends/training_utils/cp_utils.py @@ -242,6 +242,32 @@ def pad_tokens(tokens, pad): return torch.cat([tokens[start_1:end_1], tokens[start_2:end_2]]) +def natural_to_zigzag_slice( + tensor: torch.Tensor, dim: int, cp_size: int, cp_rank: int +) -> torch.Tensor: + """Slice a full-length tensor into the zigzag ring-attention CP layout. + + Rank ``cp_rank`` owns chunks ``[cp_rank, 2*cp_size - 1 - cp_rank]`` from the + ``2*cp_size`` equal-sized partitions along ``dim``. This is the inverse of + an all-gather over the zigzag CP layout (hence "natural → zigzag"). + + Unlike :func:`slice_with_cp`, this helper does not pad — it expects the + input to already be divisible by ``2 * cp_size`` along ``dim``. If not, it + prints a warning and returns the tensor unchanged. + """ + total = tensor.shape[dim] + num_chunks = 2 * cp_size + if total % num_chunks != 0: + print(f"Warning: dim {dim} size {total} not divisible by 2*cp_size={num_chunks}") + return tensor + + chunk_size = total // num_chunks + chunk_indices = [cp_rank, 2 * cp_size - 1 - cp_rank] + + slices = [tensor.narrow(dim, idx * chunk_size, chunk_size) for idx in chunk_indices] + return torch.cat(slices, dim=dim) + + def _allgather_cp_redistribute( res: dict[str, list[torch.Tensor]], *, diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index ef19d6e7ef8..de24e182c95 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -42,3 +42,5 @@ class RunArgs(CommonRunArgs): cp: int = _field(default=1, help="Context parallel size") ep: int | None = _field(default=None, help="Expert parallel size (default=tp)") etp: int = _field(default=1, help="Expert tensor parallel size") + routing_replay_dump_path: Path | None = _field(default=None, help="Routing replay dump path") + routing_replay_load_path: Path | None = _field(default=None, help="Routing replay load path") diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/run.py b/miles/utils/debug_utils/run_megatron/cli/commands/run.py index 1694b3a2ddc..bf75ed01f31 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/run.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/run.py @@ -33,6 +33,9 @@ def run_impl(args: RunArgs) -> None: """Core run logic, called by both ``run`` command and ``run_and_compare``.""" parallel: ParallelConfig = ParallelConfig.from_run_args(args) + if args.routing_replay_dump_path is not None and parallel.nproc != 1: + raise ValueError(f"Routing replay dump requires single-rank run (nproc=1), got {parallel}") + resolved_megatron: Path = resolve_megatron_path(args.megatron_path) prompt: PromptConfig = PromptConfig( @@ -53,6 +56,8 @@ def run_impl(args: RunArgs) -> None: ref_load=args.ref_load, run_backward=args.run_backward, source_patcher_config=args.source_patcher_config, + routing_replay_dump_path=args.routing_replay_dump_path, + routing_replay_load_path=args.routing_replay_load_path, ) worker_args_str: str = build_worker_args( parallel=parallel, diff --git a/miles/utils/debug_utils/run_megatron/cli/worker_executor.py b/miles/utils/debug_utils/run_megatron/cli/worker_executor.py index 427fab9333a..211ae089067 100644 --- a/miles/utils/debug_utils/run_megatron/cli/worker_executor.py +++ b/miles/utils/debug_utils/run_megatron/cli/worker_executor.py @@ -45,12 +45,16 @@ def build_worker_args( Megatron-native flags come from declarative tables in ``_build_megatron_flags``; ``--script-*`` flags come from the bridge. """ + use_routing_replay: bool = ( + script_args.routing_replay_dump_path is not None or script_args.routing_replay_load_path is not None + ) parts: list[str] = [ _build_megatron_flags( parallel=parallel, sp=sp, seq_length=seq_length, batch_size=batch_size, + use_routing_replay=use_routing_replay, ), WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(script_args), ] @@ -85,6 +89,7 @@ def _build_megatron_flags( sp: bool, seq_length: int, batch_size: int, + use_routing_replay: bool, ) -> str: """Build Megatron-native CLI flags from declarative tables.""" key_value_args: list[tuple[str, object | None]] = [ @@ -102,6 +107,7 @@ def _build_megatron_flags( ("--sequence-parallel", sp), ("--bf16", True), ("--no-gradient-accumulation-fusion", True), + ("--use-routing-replay", use_routing_replay), ] parts: list[str] = [] diff --git a/miles/utils/debug_utils/run_megatron/worker/main.py b/miles/utils/debug_utils/run_megatron/worker/main.py index 892814a3cf8..508c2003b9e 100644 --- a/miles/utils/debug_utils/run_megatron/worker/main.py +++ b/miles/utils/debug_utils/run_megatron/worker/main.py @@ -17,6 +17,7 @@ import torch import torch.distributed as dist +from megatron.core import mpu from megatron.core.enums import ModelType from megatron.core.pipeline_parallel import get_forward_backward_func from megatron.training.arguments import parse_args, validate_args @@ -29,6 +30,11 @@ from miles.backends.megatron_utils.initialize import init from miles.backends.megatron_utils.model_provider import get_model_provider_func from miles.utils.debug_utils.run_megatron.worker.batch import loss_func, prepare_batch +from miles.utils.debug_utils.run_megatron.worker.replay import ( + load_replay_data, + save_replay_data, + setup_replay_before_model, +) from miles.utils.debug_utils.run_megatron.worker.script_args import WORKER_SCRIPT_ARGS_BRIDGE, WorkerScriptArgs @@ -43,15 +49,20 @@ def main() -> None: if script.source_patcher_config: _apply_source_patches(script.source_patcher_config) + setup_replay_before_model(script) model: list[Any] = _build_and_load_model(args, script) for m in model: dumper.register_non_intrusive_dumper(m) + load_replay_data(script, rank=rank, sequence_parallel=getattr(args, "sequence_parallel", False)) + token_ids: list[int] = json.loads(script.token_ids_file.read_text()) batch: dict[str, torch.Tensor] = prepare_batch( token_ids=token_ids, batch_size=args.micro_batch_size, + cp_rank=mpu.get_context_parallel_rank(), + cp_size=mpu.get_context_parallel_world_size(), ) if rank == 0: @@ -64,6 +75,7 @@ def main() -> None: batch=batch, ) + save_replay_data(script, rank=rank) _finalize_dumper() if rank == 0: diff --git a/miles/utils/debug_utils/run_megatron/worker/replay.py b/miles/utils/debug_utils/run_megatron/worker/replay.py new file mode 100644 index 00000000000..0049f874483 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/worker/replay.py @@ -0,0 +1,160 @@ +"""Routing replay stage management for standalone Megatron worker. + +Only single-rank (nproc=1) baselines are supported: save always writes +a rank-0 file, and load always reads that file with CP/SP slicing. +""" + +from pathlib import Path +from typing import NamedTuple + +import torch + +from miles.utils.debug_utils.run_megatron.worker.script_args import WorkerScriptArgs +from miles.utils.replay_base import routing_replay_manager + + +class _ParallelRanks(NamedTuple): + cp_size: int + cp_rank: int + tp_size: int + tp_rank: int + + +def setup_replay_before_model(script: WorkerScriptArgs) -> None: + """Enable replay manager and set stage BEFORE model construction. + + Must be called before ``get_model()`` so that ``register_to_module`` + (called during model construction) sees ``enabled=True`` and creates + Replay objects on MoE modules. + """ + if script.routing_replay_dump_path: + routing_replay_manager.enabled = True + routing_replay_manager.stage = "record" + print(f"[worker] Routing replay enabled, stage=record (dump → {script.routing_replay_dump_path})", flush=True) + elif script.routing_replay_load_path: + routing_replay_manager.enabled = True + routing_replay_manager.stage = "replay_forward" + print( + f"[worker] Routing replay enabled, stage=replay_forward (load ← {script.routing_replay_load_path})", + flush=True, + ) + + +def load_replay_data( + script: WorkerScriptArgs, + *, + rank: int, + sequence_parallel: bool = False, +) -> None: + """Load routing replay data from rank 0's file with CP/SP slicing.""" + if not script.routing_replay_load_path: + return + + load_dir: Path = script.routing_replay_load_path + replay_files: list[Path] = sorted(load_dir.glob(f"*_{routing_replay_manager.filename}")) + if len(replay_files) != 1: + raise ValueError( + f"Expected exactly 1 replay file in {load_dir}, " + f"found {len(replay_files)}: {[f.name for f in replay_files]}" + ) + + replay_file: Path = replay_files[0] + _load_replay(replay_file, rank=rank, sequence_parallel=sequence_parallel) + + +def save_replay_data(script: WorkerScriptArgs, *, rank: int) -> None: + """Save recorded routing replay data to disk (rank 0 only).""" + if not script.routing_replay_dump_path: + return + assert rank == 0 + + script.routing_replay_dump_path.mkdir(parents=True, exist_ok=True) + + replays_data: list[list[torch.Tensor]] = [replay.top_indices_list for replay in routing_replay_manager.replays] + total_entries: int = sum(len(d) for d in replays_data) + assert total_entries > 0 + + save_path: Path = _replay_file_path(base_dir=script.routing_replay_dump_path) + torch.save(replays_data, save_path) + print( + f"[worker] Saved routing replay ({total_entries} entries, {len(replays_data)} replays) → {save_path}", + flush=True, + ) + + +def _replay_file_path(*, base_dir: Path) -> Path: + return base_dir / f"rank0_{routing_replay_manager.filename}" + + +def _get_parallel_ranks() -> _ParallelRanks: + """Return parallel ranks, defaulting to 1/0 if mpu is not initialized.""" + from megatron.core import mpu + + initialized: bool = mpu.is_initialized() + return _ParallelRanks( + cp_size=mpu.get_context_parallel_world_size() if initialized else 1, + cp_rank=mpu.get_context_parallel_rank() if initialized else 0, + tp_size=mpu.get_tensor_model_parallel_world_size() if initialized else 1, + tp_rank=mpu.get_tensor_model_parallel_rank() if initialized else 0, + ) + + +def _load_replay( + replay_file: Path, + *, + rank: int, + sequence_parallel: bool, +) -> None: + """Load replay from rank 0's file with CP zigzag slicing and SP slicing.""" + saved_replays: list[list[torch.Tensor]] = torch.load(replay_file, weights_only=False) + + expected: int = len(routing_replay_manager.replays) + if len(saved_replays) != expected: + raise ValueError(f"Replay file has {len(saved_replays)} replays but model expects {expected}") + + ranks: _ParallelRanks = _get_parallel_ranks() + do_sp_slice: bool = sequence_parallel and routing_replay_manager.if_sp_region and ranks.tp_size > 1 + + total_entries: int = 0 + for replay_idx, (replay, indices_list) in enumerate( + zip(routing_replay_manager.replays, saved_replays, strict=True) + ): + sliced: list[torch.Tensor] = indices_list + + if ranks.cp_size > 1: + from miles.backends.training_utils.cp_utils import natural_to_zigzag_slice + + sliced = [natural_to_zigzag_slice(t, dim=0, cp_size=ranks.cp_size, cp_rank=ranks.cp_rank) for t in sliced] + + if do_sp_slice: + sliced = [_sp_slice(t, tp_size=ranks.tp_size, tp_rank=ranks.tp_rank) for t in sliced] + + replay.top_indices_list = sliced + replay.forward_index = 0 + replay.backward_index = 0 + total_entries += len(sliced) + + if rank == 0: + shapes_before: list[torch.Size] = [t.shape for t in indices_list] + shapes_after: list[torch.Size] = [t.shape for t in sliced] + print( + f"[worker] replay[{replay_idx}]: cp={ranks.cp_size}/{ranks.cp_rank}, tp={ranks.tp_size}/{ranks.tp_rank}, " + f"sp={sequence_parallel}, shapes {shapes_before} → {shapes_after}", + flush=True, + ) + + if rank == 0: + print( + f"[worker] Loaded routing replay ({total_entries} entries, {expected} replays) ← {replay_file}", + flush=True, + ) + + +def _sp_slice(tensor: torch.Tensor, *, tp_size: int, tp_rank: int) -> torch.Tensor: + """Slice tensor along dim=0 for sequence parallelism.""" + seqlen: int = tensor.size(0) + assert seqlen % tp_size == 0, f"seqlen {seqlen} not divisible by tp_size {tp_size}" + chunk_size: int = seqlen // tp_size + start: int = chunk_size * tp_rank + end: int = chunk_size * (tp_rank + 1) + return tensor[start:end] diff --git a/miles/utils/debug_utils/run_megatron/worker/script_args.py b/miles/utils/debug_utils/run_megatron/worker/script_args.py index b026d8f060b..a62542ea2d3 100644 --- a/miles/utils/debug_utils/run_megatron/worker/script_args.py +++ b/miles/utils/debug_utils/run_megatron/worker/script_args.py @@ -21,6 +21,8 @@ class WorkerScriptArgs: ref_load: Path | None = None run_backward: bool = False source_patcher_config: Path | None = None + routing_replay_dump_path: Path | None = None + routing_replay_load_path: Path | None = None WORKER_SCRIPT_ARGS_BRIDGE: DataclassArgparseBridge[WorkerScriptArgs] = DataclassArgparseBridge( diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py index d7523d0f3b5..0e50cc81bdc 100644 --- a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py +++ b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py @@ -20,6 +20,26 @@ def _make_run_args(**overrides: object) -> RunArgs: return RunArgs(**defaults) # type: ignore[arg-type] +class TestRunImplValidation: + def test_routing_replay_requires_nproc1(self) -> None: + with pytest.raises(ValueError, match="single-rank"): + run_impl( + _make_run_args( + tp=2, + routing_replay_dump_path=Path("/dump"), + ) + ) + + def test_routing_replay_pp_gt1_also_fails(self) -> None: + with pytest.raises(ValueError, match="single-rank"): + run_impl( + _make_run_args( + pp=2, + routing_replay_dump_path=Path("/dump"), + ) + ) + + class TestRunImplExecCommand: """Only mock exec_command, generate_token_ids, write_token_ids_to_tmpfile, and resolve_model_script — let the rest (build_worker_args, build_dumper_env, @@ -70,6 +90,16 @@ def test_cmd_contains_script_args(self) -> None: cmd = self.mock_exec.call_args[0][0] assert "--script-role critic" in cmd + def test_routing_replay_nproc1_ok(self) -> None: + run_impl( + _make_run_args( + tp=1, + routing_replay_dump_path=Path("/dump"), + ) + ) + cmd = self.mock_exec.call_args[0][0] + assert "--use-routing-replay" in cmd + def test_backward_enables_grad_env(self) -> None: run_impl(_make_run_args(run_backward=True)) cmd = self.mock_exec.call_args[0][0] diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py b/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py index d64518a3296..ea0aa24d8d6 100644 --- a/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py +++ b/tests/fast/utils/debug_utils/run_megatron/cli/test_worker_executor.py @@ -43,6 +43,7 @@ def test_parallel_sizes(self) -> None: sp=False, seq_length=128, batch_size=1, + use_routing_replay=False, ) assert "--tensor-model-parallel-size 2" in flags assert "--pipeline-model-parallel-size 4" in flags @@ -56,6 +57,7 @@ def test_seq_length_batch_size(self) -> None: sp=False, seq_length=256, batch_size=4, + use_routing_replay=False, ) assert "--seq-length 256" in flags assert "--micro-batch-size 4" in flags @@ -67,15 +69,37 @@ def test_bf16_always(self) -> None: sp=False, seq_length=128, batch_size=1, + use_routing_replay=False, ) assert "--bf16" in flags + def test_routing_replay_on(self) -> None: + flags = _build_megatron_flags( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + use_routing_replay=True, + ) + assert "--use-routing-replay" in flags + + def test_routing_replay_off(self) -> None: + flags = _build_megatron_flags( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + use_routing_replay=False, + ) + assert "--use-routing-replay" not in flags + def test_sp_on_off(self) -> None: flags_on = _build_megatron_flags( parallel=ParallelConfig(), sp=True, seq_length=128, batch_size=1, + use_routing_replay=False, ) assert "--sequence-parallel" in flags_on @@ -84,6 +108,7 @@ def test_sp_on_off(self) -> None: sp=False, seq_length=128, batch_size=1, + use_routing_replay=False, ) assert "--sequence-parallel" not in flags_off @@ -133,6 +158,28 @@ def test_empty_extra_args(self) -> None: ) assert not result.endswith(" ") + def test_routing_replay_flag(self) -> None: + result = build_worker_args( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + script_args=make_script_args(routing_replay_dump_path=Path("/dump")), + extra_args="", + ) + assert "--use-routing-replay" in result + + def test_no_routing_replay(self) -> None: + result = build_worker_args( + parallel=ParallelConfig(), + sp=False, + seq_length=128, + batch_size=1, + script_args=make_script_args(), + extra_args="", + ) + assert "--use-routing-replay" not in result + class TestBuildTorchrunCmd: @patch("miles.utils.debug_utils.run_megatron.cli.worker_executor.resolve_model_script") diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_replay.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_replay.py new file mode 100644 index 00000000000..590866cc5dc --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_replay.py @@ -0,0 +1,303 @@ +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest +import torch +from tests.fast.utils.debug_utils.run_megatron.conftest import make_script_args + +from miles.utils.debug_utils.run_megatron.worker.replay import ( + _load_replay, + _ParallelRanks, + _sp_slice, + load_replay_data, + save_replay_data, + setup_replay_before_model, +) + + +class TestSpSlice: + def test_slices_evenly(self) -> None: + tensor = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]) + result = _sp_slice(tensor, tp_size=2, tp_rank=0) + assert result.tolist() == [1, 2, 3, 4] + + def test_second_rank(self) -> None: + tensor = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]) + result = _sp_slice(tensor, tp_size=2, tp_rank=1) + assert result.tolist() == [5, 6, 7, 8] + + def test_not_divisible_raises(self) -> None: + tensor = torch.tensor([1, 2, 3, 4, 5]) + with pytest.raises(AssertionError, match="not divisible"): + _sp_slice(tensor, tp_size=2, tp_rank=0) + + def test_single_rank_full(self) -> None: + tensor = torch.tensor([1, 2, 3, 4]) + result = _sp_slice(tensor, tp_size=1, tp_rank=0) + assert result.tolist() == [1, 2, 3, 4] + + +class TestSetupReplayBeforeModel: + def test_dump_enables_record(self) -> None: + script = make_script_args(routing_replay_dump_path=Path("/dump")) + with patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") as mock_mgr: + mock_mgr.enabled = False + mock_mgr.stage = "fallthrough" + setup_replay_before_model(script) + assert mock_mgr.enabled is True + assert mock_mgr.stage == "record" + + def test_load_enables_replay_forward(self) -> None: + script = make_script_args(routing_replay_load_path=Path("/load")) + with patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") as mock_mgr: + mock_mgr.enabled = False + mock_mgr.stage = "fallthrough" + setup_replay_before_model(script) + assert mock_mgr.enabled is True + assert mock_mgr.stage == "replay_forward" + + def test_neither_noop(self) -> None: + script = make_script_args() + with patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") as mock_mgr: + mock_mgr.enabled = False + mock_mgr.stage = "fallthrough" + setup_replay_before_model(script) + assert mock_mgr.enabled is False + assert mock_mgr.stage == "fallthrough" + + +class TestSaveReplayData: + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_saves_file(self, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_mgr.filename = "routing_replay.pt" + replay = SimpleNamespace(top_indices_list=[torch.tensor([1, 2])]) + mock_mgr.replays = [replay] + + script = make_script_args(routing_replay_dump_path=tmp_path) + save_replay_data(script, rank=0) + + saved_files = list(tmp_path.glob("*.pt")) + assert len(saved_files) == 1 + assert "rank0" in saved_files[0].name + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_saved_content_roundtrips(self, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_mgr.filename = "routing_replay.pt" + original = [torch.tensor([10, 20]), torch.tensor([30, 40])] + replay = SimpleNamespace(top_indices_list=original) + mock_mgr.replays = [replay] + + script = make_script_args(routing_replay_dump_path=tmp_path) + save_replay_data(script, rank=0) + + saved_file = list(tmp_path.glob("*.pt"))[0] + loaded = torch.load(saved_file, weights_only=False) + assert len(loaded) == 1 + assert len(loaded[0]) == 2 + assert loaded[0][0].tolist() == [10, 20] + assert loaded[0][1].tolist() == [30, 40] + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_noop_when_no_path(self, mock_mgr: MagicMock) -> None: + script = make_script_args() + save_replay_data(script, rank=0) + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_asserts_rank_zero(self, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_mgr.filename = "routing_replay.pt" + replay = SimpleNamespace(top_indices_list=[torch.tensor([1, 2])]) + mock_mgr.replays = [replay] + + script = make_script_args(routing_replay_dump_path=tmp_path) + with pytest.raises(AssertionError): + save_replay_data(script, rank=1) + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_empty_entries_asserts(self, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_mgr.filename = "routing_replay.pt" + replay = SimpleNamespace(top_indices_list=[]) + mock_mgr.replays = [replay] + + script = make_script_args(routing_replay_dump_path=tmp_path) + with pytest.raises(AssertionError): + save_replay_data(script, rank=0) + + +class TestLoadReplayData: + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_noop_when_no_path(self, mock_mgr: MagicMock) -> None: + script = make_script_args() + load_replay_data(script, rank=0) + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_no_file_raises(self, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_mgr.filename = "routing_replay.pt" + script = make_script_args(routing_replay_load_path=tmp_path) + with pytest.raises(ValueError, match="Expected exactly 1 replay file"): + load_replay_data(script, rank=0) + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_multiple_files_raises(self, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_mgr.filename = "routing_replay.pt" + (tmp_path / "a_routing_replay.pt").touch() + (tmp_path / "b_routing_replay.pt").touch() + + script = make_script_args(routing_replay_load_path=tmp_path) + with pytest.raises(ValueError, match="Expected exactly 1 replay file"): + load_replay_data(script, rank=0) + + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_loads_data( + self, + mock_mgr: MagicMock, + mock_ranks: MagicMock, + tmp_path: Path, + ) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=1, cp_rank=0, tp_size=1, tp_rank=0) + mock_mgr.filename = "routing_replay.pt" + mock_mgr.if_sp_region = False + + replay_obj = SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0) + mock_mgr.replays = [replay_obj] + + saved_data = [[torch.tensor([10, 20]), torch.tensor([30, 40])]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + script = make_script_args(routing_replay_load_path=tmp_path) + load_replay_data(script, rank=0) + + assert len(replay_obj.top_indices_list) == 2 + assert replay_obj.forward_index == 0 + assert replay_obj.backward_index == 0 + + +class TestLoadReplaySpSlicing: + """Test _load_replay with SP (sequence parallel) slicing.""" + + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_sp_slices_tensors(self, mock_mgr: MagicMock, mock_ranks: MagicMock, tmp_path: Path) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=1, cp_rank=0, tp_size=2, tp_rank=0) + mock_mgr.if_sp_region = True + + replay_obj = SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0) + mock_mgr.replays = [replay_obj] + + # 8 elements, tp_size=2, tp_rank=0 → first 4 + saved_data = [[torch.arange(8)]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + _load_replay(save_path, rank=0, sequence_parallel=True) + + assert len(replay_obj.top_indices_list) == 1 + assert replay_obj.top_indices_list[0].tolist() == [0, 1, 2, 3] + + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_sp_second_rank(self, mock_mgr: MagicMock, mock_ranks: MagicMock, tmp_path: Path) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=1, cp_rank=0, tp_size=2, tp_rank=1) + mock_mgr.if_sp_region = True + + replay_obj = SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0) + mock_mgr.replays = [replay_obj] + + saved_data = [[torch.arange(8)]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + _load_replay(save_path, rank=1, sequence_parallel=True) + + assert replay_obj.top_indices_list[0].tolist() == [4, 5, 6, 7] + + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_sp_disabled_no_slice(self, mock_mgr: MagicMock, mock_ranks: MagicMock, tmp_path: Path) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=1, cp_rank=0, tp_size=2, tp_rank=0) + mock_mgr.if_sp_region = True + + replay_obj = SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0) + mock_mgr.replays = [replay_obj] + + saved_data = [[torch.arange(8)]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + # sequence_parallel=False → no sp slice even if if_sp_region=True + _load_replay(save_path, rank=0, sequence_parallel=False) + + assert replay_obj.top_indices_list[0].tolist() == list(range(8)) + + +class TestLoadReplayCpSlicing: + """Test _load_replay with CP (context parallel) zigzag slicing.""" + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + def test_cp_calls_zigzag_slice(self, mock_ranks: MagicMock, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=2, cp_rank=0, tp_size=1, tp_rank=0) + mock_mgr.if_sp_region = False + + replay_obj = SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0) + mock_mgr.replays = [replay_obj] + + original_tensor = torch.arange(8) + saved_data = [[original_tensor]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + mock_zigzag = MagicMock(return_value=torch.tensor([0, 7, 1, 6])) + + with patch( + "miles.backends.training_utils.cp_utils.natural_to_zigzag_slice", + mock_zigzag, + ): + _load_replay(save_path, rank=0, sequence_parallel=False) + + mock_zigzag.assert_called_once() + call_kwargs = mock_zigzag.call_args[1] + assert call_kwargs["dim"] == 0 + assert call_kwargs["cp_size"] == 2 + assert call_kwargs["cp_rank"] == 0 + assert replay_obj.top_indices_list[0].tolist() == [0, 7, 1, 6] + + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + def test_cp1_no_zigzag(self, mock_ranks: MagicMock, mock_mgr: MagicMock, tmp_path: Path) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=1, cp_rank=0, tp_size=1, tp_rank=0) + mock_mgr.if_sp_region = False + + replay_obj = SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0) + mock_mgr.replays = [replay_obj] + + saved_data = [[torch.arange(4)]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + _load_replay(save_path, rank=0, sequence_parallel=False) + assert replay_obj.top_indices_list[0].tolist() == [0, 1, 2, 3] + + +class TestLoadReplayMismatch: + @patch("miles.utils.debug_utils.run_megatron.worker.replay._get_parallel_ranks") + @patch("miles.utils.debug_utils.run_megatron.worker.replay.routing_replay_manager") + def test_replay_count_mismatch_raises(self, mock_mgr: MagicMock, mock_ranks: MagicMock, tmp_path: Path) -> None: + mock_ranks.return_value = _ParallelRanks(cp_size=1, cp_rank=0, tp_size=1, tp_rank=0) + mock_mgr.if_sp_region = False + mock_mgr.replays = [ + SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0), + SimpleNamespace(top_indices_list=[], forward_index=0, backward_index=0), + ] + + # Save only 1 replay but model expects 2 + saved_data = [[torch.tensor([1, 2])]] + save_path = tmp_path / "rank0_routing_replay.pt" + torch.save(saved_data, save_path) + + with pytest.raises(ValueError, match="has 1 replays but model expects 2"): + _load_replay(save_path, rank=0, sequence_parallel=False) diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py index 37a3210c728..2e8088ecdd5 100644 --- a/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py @@ -18,6 +18,8 @@ def test_defaults(self) -> None: assert args.run_backward is False assert args.ref_load is None assert args.source_patcher_config is None + assert args.routing_replay_dump_path is None + assert args.routing_replay_load_path is None def test_to_cli_args_roundtrip(self) -> None: original = WorkerScriptArgs( @@ -42,6 +44,8 @@ def test_none_fields_omitted(self) -> None: cli_str = WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(args) assert "--script-ref-load" not in cli_str assert "--script-source-patcher-config" not in cli_str + assert "--script-routing-replay-dump-path" not in cli_str + assert "--script-routing-replay-load-path" not in cli_str def test_bool_serialization(self) -> None: args = WorkerScriptArgs( From d22d51b9b874d5f51da13eacc87b8f0b639a7c76 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:34:04 +0800 Subject: [PATCH 0027/1130] Add top-k prediction printing for megatron runner (#775) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/cli/commands/args.py | 1 + .../run_megatron/cli/commands/run.py | 1 + .../debug_utils/run_megatron/worker/main.py | 13 +- .../run_megatron/worker/script_args.py | 1 + .../run_megatron/worker/top_k_print.py | 114 ++++++++++ .../run_megatron/cli/commands/test_run.py | 3 +- .../run_megatron/worker/test_script_args.py | 2 + .../run_megatron/worker/test_top_k_print.py | 208 ++++++++++++++++++ 8 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 miles/utils/debug_utils/run_megatron/worker/top_k_print.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/test_top_k_print.py diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index de24e182c95..97b64185877 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -29,6 +29,7 @@ class CommonRunArgs: apply_chat_template: bool = _field(default=False, help="Apply chat template") role: str = _field(default="actor", help="Model role: actor / critic") source_patcher_config: Path | None = _field(default=None, help="Source patcher YAML config path") + top_k: int = _field(default=0, help="Print top-k predictions per position (0=disabled)") dumper_filter: str = _field(default="", help="Dumper filter expression") megatron_path: Path | None = _field(default=None, help="Path to Megatron-LM") extra_args: str = _field(default="", help="Extra args passed to worker") diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/run.py b/miles/utils/debug_utils/run_megatron/cli/commands/run.py index bf75ed01f31..29e2bb04040 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/run.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/run.py @@ -58,6 +58,7 @@ def run_impl(args: RunArgs) -> None: source_patcher_config=args.source_patcher_config, routing_replay_dump_path=args.routing_replay_dump_path, routing_replay_load_path=args.routing_replay_load_path, + top_k=args.top_k, ) worker_args_str: str = build_worker_args( parallel=parallel, diff --git a/miles/utils/debug_utils/run_megatron/worker/main.py b/miles/utils/debug_utils/run_megatron/worker/main.py index 508c2003b9e..966f7ea84dc 100644 --- a/miles/utils/debug_utils/run_megatron/worker/main.py +++ b/miles/utils/debug_utils/run_megatron/worker/main.py @@ -36,6 +36,7 @@ setup_replay_before_model, ) from miles.utils.debug_utils.run_megatron.worker.script_args import WORKER_SCRIPT_ARGS_BRIDGE, WorkerScriptArgs +from miles.utils.debug_utils.run_megatron.worker.top_k_print import print_top_k def main() -> None: @@ -68,13 +69,23 @@ def main() -> None: if rank == 0: print(f"[worker] input_ids shape={batch['input_ids'].shape}", flush=True) - _run_forward_backward( + captured_logits: torch.Tensor | None = _run_forward_backward( args=args, script=script, model=model, batch=batch, ) + is_last_pp_stage: bool = mpu.is_pipeline_last_stage() + + if script.top_k > 0 and captured_logits is not None and is_last_pp_stage: + print_top_k( + logits=captured_logits, + input_ids=batch["input_ids"], + top_k=script.top_k, + tokenizer_path=script.hf_checkpoint, + ) + save_replay_data(script, rank=rank) _finalize_dumper() diff --git a/miles/utils/debug_utils/run_megatron/worker/script_args.py b/miles/utils/debug_utils/run_megatron/worker/script_args.py index a62542ea2d3..747d7e362b9 100644 --- a/miles/utils/debug_utils/run_megatron/worker/script_args.py +++ b/miles/utils/debug_utils/run_megatron/worker/script_args.py @@ -23,6 +23,7 @@ class WorkerScriptArgs: source_patcher_config: Path | None = None routing_replay_dump_path: Path | None = None routing_replay_load_path: Path | None = None + top_k: int = 0 WORKER_SCRIPT_ARGS_BRIDGE: DataclassArgparseBridge[WorkerScriptArgs] = DataclassArgparseBridge( diff --git a/miles/utils/debug_utils/run_megatron/worker/top_k_print.py b/miles/utils/debug_utils/run_megatron/worker/top_k_print.py new file mode 100644 index 00000000000..79af05170e8 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/worker/top_k_print.py @@ -0,0 +1,114 @@ +"""Top-K prediction printing for debugging forward pass outputs.""" + +import sys +from pathlib import Path +from typing import Any + +import torch +import torch.distributed as dist + + +def print_top_k( + *, + logits: torch.Tensor, + input_ids: torch.Tensor, + top_k: int, + tokenizer_path: Path, +) -> None: + """Load tokenizer and print top-k predictions across all ranks.""" + from transformers import AutoTokenizer + + tokenizer: Any = AutoTokenizer.from_pretrained(str(tokenizer_path), trust_remote_code=True) + pad_token_id: int | None = tokenizer.pad_token_id or tokenizer.eos_token_id + + _print_top_predictions_all_ranks( + logits=logits, + input_ids=input_ids, + top_k=top_k, + tokenizer=tokenizer, + pad_token_id=pad_token_id, + ) + + +def _print_top_predictions_all_ranks( + *, + logits: torch.Tensor, + input_ids: torch.Tensor, + top_k: int, + tokenizer: object, + pad_token_id: int | None = None, +) -> None: + """Print top-k predictions from all ranks sequentially (rank 0 first, then rank 1, etc.).""" + rank, world_size = _get_dist_info() + + if rank == 0: + print(f"\n{'=' * 80}") + print(f"Top-{top_k} Predictions (all ranks)") + print(f"World size: {world_size}") + print(f"{'=' * 80}") + + for r in range(world_size): + _maybe_barrier() + if rank == r: + _print_top_predictions_for_rank( + logits=logits, + input_ids=input_ids, + top_k=top_k, + tokenizer=tokenizer, + rank=rank, + pad_token_id=pad_token_id, + ) + sys.stdout.flush() + + _maybe_barrier() + + +def _print_top_predictions_for_rank( + *, + logits: torch.Tensor, + input_ids: torch.Tensor, + top_k: int, + tokenizer: object, + rank: int, + pad_token_id: int | None = None, +) -> None: + """Print top-k predictions for this rank, one line per position.""" + batch_size: int = logits.shape[0] + seq_length: int = logits.shape[1] + + print(f"\n--- Rank {rank} (seq_len={seq_length}) ---") + for b in range(batch_size): + if batch_size > 1: + print(f" Batch {b}:") + for pos in range(seq_length): + if pad_token_id is not None and input_ids[b, pos].item() == pad_token_id: + continue + + input_token: int = input_ids[b, pos].item() + probs: torch.Tensor = torch.softmax(logits[b, pos], dim=-1) + top_probs: torch.Tensor + top_indices: torch.Tensor + top_probs, top_indices = torch.topk(probs, top_k) + + input_str: str = _decode_token(tokenizer, token_id=input_token) + preds: str = ", ".join( + f"{_decode_token(tokenizer, token_id=idx.item())}({prob.item():.3f})" + for prob, idx in zip(top_probs, top_indices, strict=True) + ) + print(f"pos[{pos:3d}] {input_str!r:12s} -> {preds}") + + +def _decode_token(tokenizer: object, *, token_id: int) -> str: + return tokenizer.decode([token_id]) if tokenizer else f"t{token_id}" + + +def _get_dist_info() -> tuple[int, int]: + """Return (rank, world_size), defaulting to (0, 1) if not initialized.""" + if dist.is_initialized(): + return dist.get_rank(), dist.get_world_size() + return 0, 1 + + +def _maybe_barrier() -> None: + if dist.is_initialized(): + dist.barrier() diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py index 0e50cc81bdc..4943ade6c8f 100644 --- a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py +++ b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run.py @@ -86,9 +86,10 @@ def test_cmd_contains_dumper_env(self) -> None: assert "DUMPER_FILTER=logits" in cmd def test_cmd_contains_script_args(self) -> None: - run_impl(_make_run_args(role="critic")) + run_impl(_make_run_args(role="critic", top_k=5)) cmd = self.mock_exec.call_args[0][0] assert "--script-role critic" in cmd + assert "--script-top-k 5" in cmd def test_routing_replay_nproc1_ok(self) -> None: run_impl( diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py index 2e8088ecdd5..3a4e8159428 100644 --- a/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_script_args.py @@ -15,6 +15,7 @@ def test_frozen(self) -> None: def test_defaults(self) -> None: args = WorkerScriptArgs(hf_checkpoint=Path("/hf"), token_ids_file=Path("/tokens.json")) assert args.role == "actor" + assert args.top_k == 0 assert args.run_backward is False assert args.ref_load is None assert args.source_patcher_config is None @@ -27,6 +28,7 @@ def test_to_cli_args_roundtrip(self) -> None: token_ids_file=Path("/tokens.json"), role="critic", run_backward=True, + top_k=5, ) cli_str = WORKER_SCRIPT_ARGS_BRIDGE.to_cli_args(original) diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_top_k_print.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_top_k_print.py new file mode 100644 index 00000000000..3125f8f9210 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_top_k_print.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import re +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +import torch + +from miles.utils.debug_utils.run_megatron.worker.top_k_print import ( + _decode_token, + _get_dist_info, + _print_top_predictions_all_ranks, + _print_top_predictions_for_rank, + print_top_k, +) + + +class TestDecodeToken: + def test_with_tokenizer(self) -> None: + mock_tok = MagicMock() + mock_tok.decode.return_value = "hello" + result = _decode_token(mock_tok, token_id=42) + assert result == "hello" + mock_tok.decode.assert_called_once_with([42]) + + def test_without_tokenizer(self) -> None: + result = _decode_token(None, token_id=42) + assert result == "t42" + + +class TestGetDistInfo: + @patch("miles.utils.debug_utils.run_megatron.worker.top_k_print.dist") + def test_not_initialized(self, mock_dist: MagicMock) -> None: + mock_dist.is_initialized.return_value = False + rank, world_size = _get_dist_info() + assert rank == 0 + assert world_size == 1 + + @patch("miles.utils.debug_utils.run_megatron.worker.top_k_print.dist") + def test_initialized(self, mock_dist: MagicMock) -> None: + mock_dist.is_initialized.return_value = True + mock_dist.get_rank.return_value = 2 + mock_dist.get_world_size.return_value = 8 + rank, world_size = _get_dist_info() + assert rank == 2 + assert world_size == 8 + + +class TestPrintTopPredictionsForRank: + def test_output_format(self, capsys: pytest.CaptureFixture[str]) -> None: + logits = torch.randn(1, 3, 10) # batch=1, seq=3, vocab=10 + input_ids = torch.tensor([[1, 2, 3]]) + mock_tok = MagicMock() + mock_tok.decode.return_value = "x" + + _print_top_predictions_for_rank( + logits=logits, + input_ids=input_ids, + top_k=3, + tokenizer=mock_tok, + rank=0, + ) + + output = capsys.readouterr().out + assert "Rank 0" in output + assert output.count("pos[") == 3 + for line in output.strip().splitlines(): + if "pos[" in line: + assert "->" in line + assert re.search(r"\(\d+\.\d{3}\)", line) + + def test_pad_token_skipped(self, capsys: pytest.CaptureFixture[str]) -> None: + logits = torch.randn(1, 4, 10) + input_ids = torch.tensor([[1, 99, 2, 99]]) # 99 is pad + mock_tok = MagicMock() + mock_tok.decode.return_value = "x" + + _print_top_predictions_for_rank( + logits=logits, + input_ids=input_ids, + top_k=2, + tokenizer=mock_tok, + rank=0, + pad_token_id=99, + ) + + output = capsys.readouterr().out + assert output.count("pos[") == 2 + # decode called for non-pad positions only: + # 2 non-pad positions × (1 input token + 2 top-k tokens) = 6 calls + assert mock_tok.decode.call_count == 6 + + def test_batch_size_gt1(self, capsys: pytest.CaptureFixture[str]) -> None: + logits = torch.randn(2, 2, 10) + input_ids = torch.tensor([[1, 2], [3, 4]]) + mock_tok = MagicMock() + mock_tok.decode.return_value = "x" + + _print_top_predictions_for_rank( + logits=logits, + input_ids=input_ids, + top_k=2, + tokenizer=mock_tok, + rank=0, + ) + + output = capsys.readouterr().out + assert output.count("Batch") == 2 + assert output.count("pos[") == 4 + # 2 batches × 2 positions × (1 input + 2 topk) = 12 decode calls + assert mock_tok.decode.call_count == 12 + + +_TOP_K_MODULE = "miles.utils.debug_utils.run_megatron.worker.top_k_print" + + +class TestPrintTopK: + @patch(f"{_TOP_K_MODULE}._print_top_predictions_all_ranks") + @patch("transformers.AutoTokenizer") + def test_loads_tokenizer_and_calls_print_all_ranks( + self, + mock_auto_tok: MagicMock, + mock_print_all: MagicMock, + ) -> None: + mock_tokenizer = MagicMock() + mock_tokenizer.pad_token_id = 5 + mock_auto_tok.from_pretrained.return_value = mock_tokenizer + + logits = torch.randn(1, 2, 10) + input_ids = torch.tensor([[1, 2]]) + + print_top_k( + logits=logits, + input_ids=input_ids, + top_k=3, + tokenizer_path=Path("/fake/model"), + ) + + mock_auto_tok.from_pretrained.assert_called_once() + mock_print_all.assert_called_once() + call_kwargs = mock_print_all.call_args[1] + assert call_kwargs["pad_token_id"] == 5 + + @patch(f"{_TOP_K_MODULE}._print_top_predictions_all_ranks") + @patch("transformers.AutoTokenizer") + def test_pad_token_id_fallback_to_eos( + self, + mock_auto_tok: MagicMock, + mock_print_all: MagicMock, + ) -> None: + mock_tokenizer = MagicMock() + mock_tokenizer.pad_token_id = None + mock_tokenizer.eos_token_id = 2 + mock_auto_tok.from_pretrained.return_value = mock_tokenizer + + print_top_k( + logits=torch.randn(1, 2, 10), + input_ids=torch.tensor([[1, 2]]), + top_k=3, + tokenizer_path=Path("/fake/model"), + ) + + call_kwargs = mock_print_all.call_args[1] + assert call_kwargs["pad_token_id"] == 2 + + +class TestPrintTopPredictionsAllRanks: + @patch(f"{_TOP_K_MODULE}._maybe_barrier") + @patch(f"{_TOP_K_MODULE}._get_dist_info", return_value=(0, 2)) + @patch(f"{_TOP_K_MODULE}._print_top_predictions_for_rank") + def test_sequential_rank_printing( + self, + mock_print_rank: MagicMock, + mock_dist_info: MagicMock, + mock_barrier: MagicMock, + ) -> None: + """Current rank=0, world_size=2 → prints only for rank 0.""" + _print_top_predictions_all_ranks( + logits=torch.randn(1, 2, 10), + input_ids=torch.tensor([[1, 2]]), + top_k=3, + tokenizer=MagicMock(), + ) + + assert mock_print_rank.call_count == 1 + call_kwargs = mock_print_rank.call_args[1] + assert call_kwargs["rank"] == 0 + + @patch(f"{_TOP_K_MODULE}._maybe_barrier") + @patch(f"{_TOP_K_MODULE}._get_dist_info", return_value=(0, 1)) + @patch(f"{_TOP_K_MODULE}._print_top_predictions_for_rank") + def test_single_rank_barrier_count( + self, + mock_print_rank: MagicMock, + mock_dist_info: MagicMock, + mock_barrier: MagicMock, + ) -> None: + """World_size=1 → barrier called world_size+1 times (loop + final).""" + _print_top_predictions_all_ranks( + logits=torch.randn(1, 2, 10), + input_ids=torch.tensor([[1, 2]]), + top_k=3, + tokenizer=MagicMock(), + ) + + # loop: 1 iteration × 1 barrier + 1 final barrier = 2 + assert mock_barrier.call_count == 2 From cb8e787afcaac2665b089995c573d5b2364b9389 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:34:58 +0800 Subject: [PATCH 0028/1130] Add logprob output for megatron runner (#776) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/cli/commands/args.py | 1 + .../run_megatron/cli/commands/run.py | 1 + .../debug_utils/run_megatron/worker/main.py | 9 + .../debug_utils/run_megatron/worker/output.py | 117 ++++++++ .../run_megatron/worker/script_args.py | 1 + .../run_megatron/worker/test_output.py | 266 ++++++++++++++++++ 6 files changed, 395 insertions(+) create mode 100644 miles/utils/debug_utils/run_megatron/worker/output.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/worker/test_output.py diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index 97b64185877..0046ecc7d57 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -45,3 +45,4 @@ class RunArgs(CommonRunArgs): etp: int = _field(default=1, help="Expert tensor parallel size") routing_replay_dump_path: Path | None = _field(default=None, help="Routing replay dump path") routing_replay_load_path: Path | None = _field(default=None, help="Routing replay load path") + logprob_output: Path | None = _field(default=None, help="Directory to save per-token logprob JSON files") diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/run.py b/miles/utils/debug_utils/run_megatron/cli/commands/run.py index 29e2bb04040..f6ff7719159 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/run.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/run.py @@ -59,6 +59,7 @@ def run_impl(args: RunArgs) -> None: routing_replay_dump_path=args.routing_replay_dump_path, routing_replay_load_path=args.routing_replay_load_path, top_k=args.top_k, + logprob_output=args.logprob_output, ) worker_args_str: str = build_worker_args( parallel=parallel, diff --git a/miles/utils/debug_utils/run_megatron/worker/main.py b/miles/utils/debug_utils/run_megatron/worker/main.py index 966f7ea84dc..2218a439eb4 100644 --- a/miles/utils/debug_utils/run_megatron/worker/main.py +++ b/miles/utils/debug_utils/run_megatron/worker/main.py @@ -30,6 +30,7 @@ from miles.backends.megatron_utils.initialize import init from miles.backends.megatron_utils.model_provider import get_model_provider_func from miles.utils.debug_utils.run_megatron.worker.batch import loss_func, prepare_batch +from miles.utils.debug_utils.run_megatron.worker.output import compute_and_save_output_info from miles.utils.debug_utils.run_megatron.worker.replay import ( load_replay_data, save_replay_data, @@ -78,6 +79,14 @@ def main() -> None: is_last_pp_stage: bool = mpu.is_pipeline_last_stage() + if script.logprob_output is not None and captured_logits is not None and is_last_pp_stage: + compute_and_save_output_info( + logits=captured_logits, + labels=batch["labels"], + position_ids=batch["position_ids"], + output_dir=script.logprob_output, + ) + if script.top_k > 0 and captured_logits is not None and is_last_pp_stage: print_top_k( logits=captured_logits, diff --git a/miles/utils/debug_utils/run_megatron/worker/output.py b/miles/utils/debug_utils/run_megatron/worker/output.py new file mode 100644 index 00000000000..0552cfb4d36 --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/worker/output.py @@ -0,0 +1,117 @@ +"""Compute and save output info (logprobs, etc.) from forward-pass results.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import torch +import torch.distributed as dist +from megatron.core import mpu + + +def compute_and_save_output_info( + *, + logits: torch.Tensor, + labels: torch.Tensor, + position_ids: torch.Tensor, + output_dir: Path, +) -> None: + """Compute output info and save one JSON file per rank.""" + rank = _get_rank() + payload = _compute_output_info( + logits=logits, + labels=labels, + position_ids=position_ids, + rank=rank, + ) + + if payload is None: + print( + f"[output] rank={rank}: skipping — logits shape {logits.shape} " + f"does not look like vocab logits (critic model?)", + flush=True, + ) + return + + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / f"rank_{rank}.json" + output_path.write_text(json.dumps(payload, indent=2)) + + print(f"[output] rank={rank}: saved to {output_path}", flush=True) + + +def _compute_output_info( + *, + logits: torch.Tensor, + labels: torch.Tensor, + position_ids: torch.Tensor, + rank: int, +) -> dict[str, Any] | None: + """Assemble the full output info payload for one rank. + + Returns None if there is nothing to compute (e.g. critic model logits). + """ + logprob_entries = _compute_logprob_entries( + logits=logits, + labels=labels, + position_ids=position_ids, + ) + if logprob_entries is None: + return None + + return { + "rank": rank, + "tp_size": mpu.get_tensor_model_parallel_world_size() if dist.is_initialized() else 1, + "cp_size": mpu.get_context_parallel_world_size() if dist.is_initialized() else 1, + "pp_size": mpu.get_pipeline_model_parallel_world_size() if dist.is_initialized() else 1, + "logprob_entries": logprob_entries, + } + + +def _compute_logprob_entries( + *, + logits: torch.Tensor, + labels: torch.Tensor, + position_ids: torch.Tensor, +) -> list[list[dict[str, Any]]] | None: + """Compute per-token logprob entries. + + Returns ``entries[batch][seq]`` dicts, or None if logits don't look like + vocab logits (e.g. critic model). + + Args: + logits: [batch_size, local_seq_len, vocab_size] — must already be gathered across TP. + labels: [batch_size, local_seq_len], -100 = ignore. + position_ids: [batch_size, local_seq_len], global positions. + """ + if logits.ndim < 3 or logits.size(-1) == 1: + return None + + batch_size, local_seq_len, _ = logits.shape + log_probs = torch.log_softmax(logits.float(), dim=-1) + + all_entries: list[list[dict[str, Any]]] = [] + for b in range(batch_size): + batch_entries: list[dict[str, Any]] = [] + for s in range(local_seq_len): + label_id: int = labels[b, s].item() + is_valid = label_id != -100 + lp: float = log_probs[b, s, label_id].item() if is_valid else 0.0 + + batch_entries.append( + { + "global_position": position_ids[b, s].item(), + "token_id": label_id if is_valid else -1, + "logprob": lp, + "is_valid": is_valid, + } + ) + all_entries.append(batch_entries) + + return all_entries + + +def _get_rank() -> int: + return dist.get_rank() if dist.is_initialized() else 0 diff --git a/miles/utils/debug_utils/run_megatron/worker/script_args.py b/miles/utils/debug_utils/run_megatron/worker/script_args.py index 747d7e362b9..e8fb8bbc6c1 100644 --- a/miles/utils/debug_utils/run_megatron/worker/script_args.py +++ b/miles/utils/debug_utils/run_megatron/worker/script_args.py @@ -24,6 +24,7 @@ class WorkerScriptArgs: routing_replay_dump_path: Path | None = None routing_replay_load_path: Path | None = None top_k: int = 0 + logprob_output: Path | None = None WORKER_SCRIPT_ARGS_BRIDGE: DataclassArgparseBridge[WorkerScriptArgs] = DataclassArgparseBridge( diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_output.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_output.py new file mode 100644 index 00000000000..1394272b869 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_output.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any +from unittest.mock import patch + +import torch + +from miles.utils.debug_utils.run_megatron.worker.output import ( + _compute_logprob_entries, + _compute_output_info, + compute_and_save_output_info, +) + + +# --------------------------------------------------------------------------- +# TestComputeLogprobEntries +# --------------------------------------------------------------------------- + + +class TestComputeLogprobEntries: + def test_returns_none_for_2d_logits(self) -> None: + logits = torch.randn(2, 4) + labels = torch.tensor([[1, 2, 3, -100]]) + position_ids = torch.arange(4).unsqueeze(0) + assert _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) is None + + def test_returns_none_for_scalar_vocab(self) -> None: + logits = torch.randn(1, 4, 1) + labels = torch.tensor([[0, 0, 0, 0]]) + position_ids = torch.arange(4).unsqueeze(0) + assert _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) is None + + def test_basic_shape(self) -> None: + batch_size, seq_len, vocab_size = 2, 3, 10 + logits = torch.randn(batch_size, seq_len, vocab_size) + labels = torch.tensor([[1, 2, -100], [3, -100, 5]]) + position_ids = torch.arange(seq_len).unsqueeze(0).expand(batch_size, -1) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + assert len(result) == batch_size + for batch_entries in result: + assert len(batch_entries) == seq_len + + def test_entry_keys(self) -> None: + logits = torch.randn(1, 2, 5) + labels = torch.tensor([[1, -100]]) + position_ids = torch.tensor([[0, 1]]) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + expected_keys = {"global_position", "token_id", "logprob", "is_valid"} + for entry in result[0]: + assert set(entry.keys()) == expected_keys + + def test_valid_entry_values(self) -> None: + vocab_size = 5 + logits = torch.zeros(1, 1, vocab_size) + logits[0, 0, 2] = 10.0 + labels = torch.tensor([[2]]) + position_ids = torch.tensor([[7]]) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + entry = result[0][0] + assert entry["global_position"] == 7 + assert entry["token_id"] == 2 + assert entry["is_valid"] is True + assert entry["logprob"] < 0 # log_softmax is always negative + + def test_ignored_entry_values(self) -> None: + logits = torch.randn(1, 1, 5) + labels = torch.tensor([[-100]]) + position_ids = torch.tensor([[3]]) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + entry = result[0][0] + assert entry["global_position"] == 3 + assert entry["token_id"] == -1 + assert entry["is_valid"] is False + assert entry["logprob"] == 0.0 + + def test_logprob_sums_to_one(self) -> None: + """exp(logprob) for each valid token should be a valid probability.""" + logits = torch.randn(1, 4, 10) + labels = torch.tensor([[0, 3, 7, 9]]) + position_ids = torch.arange(4).unsqueeze(0) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + for entry in result[0]: + prob = torch.exp(torch.tensor(entry["logprob"])) + assert 0.0 < prob.item() <= 1.0 + + def test_perfect_prediction_high_logprob(self) -> None: + vocab_size = 5 + logits = torch.full((1, 1, vocab_size), -100.0) + logits[0, 0, 3] = 100.0 + labels = torch.tensor([[3]]) + position_ids = torch.tensor([[0]]) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + assert result[0][0]["logprob"] > -0.01 + + def test_position_ids_preserved(self) -> None: + logits = torch.randn(1, 3, 5) + labels = torch.tensor([[1, 2, -100]]) + position_ids = torch.tensor([[10, 20, 30]]) + + result = _compute_logprob_entries(logits=logits, labels=labels, position_ids=position_ids) + assert result is not None + assert [e["global_position"] for e in result[0]] == [10, 20, 30] + + +# --------------------------------------------------------------------------- +# TestComputeOutputInfo +# --------------------------------------------------------------------------- + + +_MOCK_MPU = "miles.utils.debug_utils.run_megatron.worker.output.mpu" +_MOCK_DIST = "miles.utils.debug_utils.run_megatron.worker.output.dist" + + +def _patch_distributed( + tp_size: int = 1, + cp_size: int = 1, + pp_size: int = 1, +) -> Any: + """Context manager that mocks dist.is_initialized and mpu parallel sizes.""" + + def _decorator(func: Any) -> Any: + @patch(f"{_MOCK_DIST}.is_initialized", return_value=True) + @patch(f"{_MOCK_MPU}.get_tensor_model_parallel_world_size", return_value=tp_size) + @patch(f"{_MOCK_MPU}.get_context_parallel_world_size", return_value=cp_size) + @patch(f"{_MOCK_MPU}.get_pipeline_model_parallel_world_size", return_value=pp_size) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return func(*args, **kwargs) + + return wrapper + + return _decorator + + +class TestComputeOutputInfo: + def test_returns_none_for_invalid_logits(self) -> None: + logits = torch.randn(1, 4, 1) + labels = torch.tensor([[0, 0, 0, 0]]) + position_ids = torch.arange(4).unsqueeze(0) + + result = _compute_output_info(logits=logits, labels=labels, position_ids=position_ids, rank=0) + assert result is None + + @_patch_distributed(tp_size=2, cp_size=4, pp_size=3) + def test_payload_structure(self, *_mocks: Any) -> None: + logits = torch.randn(1, 3, 10) + labels = torch.tensor([[1, 2, -100]]) + position_ids = torch.arange(3).unsqueeze(0) + + result = _compute_output_info(logits=logits, labels=labels, position_ids=position_ids, rank=5) + assert result is not None + assert result["rank"] == 5 + assert result["tp_size"] == 2 + assert result["cp_size"] == 4 + assert result["pp_size"] == 3 + assert "logprob_entries" in result + assert len(result["logprob_entries"]) == 1 + assert len(result["logprob_entries"][0]) == 3 + + @_patch_distributed() + def test_defaults_to_size_1(self, *_mocks: Any) -> None: + logits = torch.randn(1, 2, 5) + labels = torch.tensor([[1, 2]]) + position_ids = torch.arange(2).unsqueeze(0) + + result = _compute_output_info(logits=logits, labels=labels, position_ids=position_ids, rank=0) + assert result is not None + assert result["tp_size"] == 1 + assert result["cp_size"] == 1 + assert result["pp_size"] == 1 + + +# --------------------------------------------------------------------------- +# TestComputeAndSaveOutputInfo +# --------------------------------------------------------------------------- + + +class TestComputeAndSaveOutputInfo: + @patch(f"{_MOCK_DIST}.is_initialized", return_value=False) + def test_writes_json_file(self, _mock: Any, tmp_path: Path) -> None: + logits = torch.randn(1, 3, 10) + labels = torch.tensor([[1, 2, -100]]) + position_ids = torch.arange(3).unsqueeze(0) + + compute_and_save_output_info( + logits=logits, + labels=labels, + position_ids=position_ids, + output_dir=tmp_path, + ) + + output_file = tmp_path / "rank_0.json" + assert output_file.exists() + + payload = json.loads(output_file.read_text()) + assert payload["rank"] == 0 + assert "logprob_entries" in payload + assert len(payload["logprob_entries"]) == 1 + assert len(payload["logprob_entries"][0]) == 3 + + @patch(f"{_MOCK_DIST}.is_initialized", return_value=False) + def test_skips_invalid_logits(self, _mock: Any, tmp_path: Path) -> None: + logits = torch.randn(1, 4, 1) + labels = torch.tensor([[0, 0, 0, 0]]) + position_ids = torch.arange(4).unsqueeze(0) + + compute_and_save_output_info( + logits=logits, + labels=labels, + position_ids=position_ids, + output_dir=tmp_path, + ) + + assert not (tmp_path / "rank_0.json").exists() + + @patch(f"{_MOCK_DIST}.is_initialized", return_value=False) + def test_creates_output_dir(self, _mock: Any, tmp_path: Path) -> None: + output_dir = tmp_path / "nested" / "dir" + logits = torch.randn(1, 2, 5) + labels = torch.tensor([[1, 2]]) + position_ids = torch.arange(2).unsqueeze(0) + + compute_and_save_output_info( + logits=logits, + labels=labels, + position_ids=position_ids, + output_dir=output_dir, + ) + + assert output_dir.exists() + assert (output_dir / "rank_0.json").exists() + + @patch(f"{_MOCK_DIST}.is_initialized", return_value=False) + def test_json_is_valid(self, _mock: Any, tmp_path: Path) -> None: + logits = torch.randn(2, 4, 8) + labels = torch.tensor([[1, 2, -100, 3], [4, -100, 5, 6]]) + position_ids = torch.arange(4).unsqueeze(0).expand(2, -1) + + compute_and_save_output_info( + logits=logits, + labels=labels, + position_ids=position_ids, + output_dir=tmp_path, + ) + + payload = json.loads((tmp_path / "rank_0.json").read_text()) + assert len(payload["logprob_entries"]) == 2 + assert len(payload["logprob_entries"][0]) == 4 + assert len(payload["logprob_entries"][1]) == 4 + + for batch_entries in payload["logprob_entries"]: + for entry in batch_entries: + assert set(entry.keys()) == {"global_position", "token_id", "logprob", "is_valid"} From ec130a076eb748e81093f264ac2c3fa103c3c6d3 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:35:07 +0800 Subject: [PATCH 0029/1130] Add logprob comparator for megatron runner (#777) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/logprob_comparator.py | 196 +++++++++ .../run_megatron/test_logprob_comparator.py | 384 ++++++++++++++++++ 2 files changed, 580 insertions(+) create mode 100644 miles/utils/debug_utils/run_megatron/logprob_comparator.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/test_logprob_comparator.py diff --git a/miles/utils/debug_utils/run_megatron/logprob_comparator.py b/miles/utils/debug_utils/run_megatron/logprob_comparator.py new file mode 100644 index 00000000000..bbd7523835c --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/logprob_comparator.py @@ -0,0 +1,196 @@ +"""Compare per-token logprobs between baseline and target runs.""" + +from __future__ import annotations + +import json +import statistics +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + + +@dataclass +class _PositionLogprob: + global_position: int + token_id: int + logprob: float + + +@dataclass +class _CompareResult: + passed: bool + num_positions: int + max_abs_diff: float + max_diff_position: int + max_diff_baseline_logprob: float + max_diff_target_logprob: float + max_diff_token_id: int + mean_abs_diff: float + median_abs_diff: float + p95_abs_diff: float + p99_abs_diff: float + baseline_mean_logprob: float + target_mean_logprob: float + threshold: float + per_position_diffs: list[float] = field(repr=False) + + +def compare_logprobs( + *, + baseline_dir: Path, + target_dir: Path, + threshold: float = 1e-3, +) -> bool: + """Compare logprob JSON files between baseline and target. + + Returns True if the comparison passes (mean abs diff <= threshold). + """ + baseline_entries = _load_and_merge(baseline_dir) + target_entries = _load_and_merge(target_dir) + + if not baseline_entries: + print("[logprob-compare] WARNING: no valid baseline logprob entries found", flush=True) + return True + if not target_entries: + print("[logprob-compare] WARNING: no valid target logprob entries found", flush=True) + return True + + result = _compute_comparison( + baseline_entries=baseline_entries, + target_entries=target_entries, + threshold=threshold, + ) + + _print_report(result) + return result.passed + + +def _load_and_merge(directory: Path) -> dict[tuple[int, int], _PositionLogprob]: + """Load all rank JSON files and merge by (batch_index, global_position). + + TP ranks produce identical logprobs (runtime_gather_output=True), so we deduplicate. + PP intermediate stages have no logits and produce no files. + """ + merged: dict[tuple[int, int], _PositionLogprob] = {} + + json_files = sorted(directory.glob("rank_*.json")) + if not json_files: + print(f"[logprob-compare] WARNING: no rank_*.json files found in {directory}", flush=True) + return merged + + for json_file in json_files: + data: dict[str, Any] = json.loads(json_file.read_text()) + entries_by_batch: list[list[dict[str, Any]]] = data["logprob_entries"] + + for batch_idx, batch_entries in enumerate(entries_by_batch): + for entry in batch_entries: + if not entry["is_valid"]: + continue + + key = (batch_idx, entry["global_position"]) + if key not in merged: + merged[key] = _PositionLogprob( + global_position=entry["global_position"], + token_id=entry["token_id"], + logprob=entry["logprob"], + ) + + return merged + + +def _compute_comparison( + *, + baseline_entries: dict[tuple[int, int], _PositionLogprob], + target_entries: dict[tuple[int, int], _PositionLogprob], + threshold: float, +) -> _CompareResult: + common_keys = sorted(set(baseline_entries.keys()) & set(target_entries.keys())) + + if not common_keys: + return _CompareResult( + passed=True, + num_positions=0, + max_abs_diff=0.0, + max_diff_position=-1, + max_diff_baseline_logprob=0.0, + max_diff_target_logprob=0.0, + max_diff_token_id=-1, + mean_abs_diff=0.0, + median_abs_diff=0.0, + p95_abs_diff=0.0, + p99_abs_diff=0.0, + baseline_mean_logprob=0.0, + target_mean_logprob=0.0, + threshold=threshold, + per_position_diffs=[], + ) + + diffs: list[float] = [] + max_abs_diff = 0.0 + max_diff_key: tuple[int, int] = common_keys[0] + + baseline_logprobs: list[float] = [] + target_logprobs: list[float] = [] + + for key in common_keys: + baseline = baseline_entries[key] + target = target_entries[key] + abs_diff = abs(baseline.logprob - target.logprob) + diffs.append(abs_diff) + baseline_logprobs.append(baseline.logprob) + target_logprobs.append(target.logprob) + + if abs_diff > max_abs_diff: + max_abs_diff = abs_diff + max_diff_key = key + + sorted_diffs = sorted(diffs) + num = len(sorted_diffs) + + baseline_worst = baseline_entries[max_diff_key] + target_worst = target_entries[max_diff_key] + + return _CompareResult( + passed=statistics.mean(diffs) <= threshold, + num_positions=num, + max_abs_diff=max_abs_diff, + max_diff_position=max_diff_key[1], + max_diff_baseline_logprob=baseline_worst.logprob, + max_diff_target_logprob=target_worst.logprob, + max_diff_token_id=baseline_worst.token_id, + mean_abs_diff=statistics.mean(diffs), + median_abs_diff=statistics.median(diffs), + p95_abs_diff=sorted_diffs[int(num * 0.95)] if num > 0 else 0.0, + p99_abs_diff=sorted_diffs[int(num * 0.99)] if num > 0 else 0.0, + baseline_mean_logprob=statistics.mean(baseline_logprobs), + target_mean_logprob=statistics.mean(target_logprobs), + threshold=threshold, + per_position_diffs=diffs, + ) + + +def _print_report(result: _CompareResult) -> None: + status = "PASSED" if result.passed else "FAILED" + print(f"\n{'=' * 70}", flush=True) + print(f"Logprob Comparison: {status}", flush=True) + print(f"{'=' * 70}", flush=True) + print(f" Positions compared : {result.num_positions}", flush=True) + print(f" Threshold (mean) : {result.threshold}", flush=True) + print(f" Max abs diff : {result.max_abs_diff:.6e}", flush=True) + print( + f" Mean abs diff : {result.mean_abs_diff:.6e} {'<= threshold' if result.passed else '> threshold'}", + flush=True, + ) + print(f" Median abs diff : {result.median_abs_diff:.6e}", flush=True) + print(f" P95 abs diff : {result.p95_abs_diff:.6e}", flush=True) + print(f" P99 abs diff : {result.p99_abs_diff:.6e}", flush=True) + + if result.num_positions > 0: + print(f"\n Worst position : {result.max_diff_position}", flush=True) + print(f" token_id : {result.max_diff_token_id}", flush=True) + print(f" baseline logprob : {result.max_diff_baseline_logprob:.6f}", flush=True) + print(f" target logprob : {result.max_diff_target_logprob:.6f}", flush=True) + + print(f"\n Baseline mean logprob : {result.baseline_mean_logprob:.6f}", flush=True) + print(f" Target mean logprob : {result.target_mean_logprob:.6f}", flush=True) + print(f"{'=' * 70}\n", flush=True) diff --git a/tests/fast/utils/debug_utils/run_megatron/test_logprob_comparator.py b/tests/fast/utils/debug_utils/run_megatron/test_logprob_comparator.py new file mode 100644 index 00000000000..53274aa6fc8 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/test_logprob_comparator.py @@ -0,0 +1,384 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from miles.utils.debug_utils.run_megatron.logprob_comparator import ( + _compute_comparison, + _load_and_merge, + _PositionLogprob, + compare_logprobs, +) + + +def _make_rank_json( + entries_by_batch: list[list[dict]], +) -> str: + return json.dumps({"logprob_entries": entries_by_batch}) + + +def _write_rank_file( + directory: Path, + rank: int, + entries_by_batch: list[list[dict]], +) -> Path: + path = directory / f"rank_{rank}.json" + path.write_text(_make_rank_json(entries_by_batch)) + return path + + +def _entry( + global_position: int, + token_id: int, + logprob: float, + is_valid: bool = True, +) -> dict: + return { + "global_position": global_position, + "token_id": token_id, + "logprob": logprob, + "is_valid": is_valid, + } + + +class TestLoadAndMerge: + def test_single_rank_single_batch(self, tmp_path: Path) -> None: + _write_rank_file( + tmp_path, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.0), _entry(1, 101, -2.0)], + ], + ) + result = _load_and_merge(tmp_path) + assert len(result) == 2 + assert (0, 0) in result + assert (0, 1) in result + assert result[(0, 0)].logprob == -1.0 + + def test_multi_batch(self, tmp_path: Path) -> None: + _write_rank_file( + tmp_path, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.0)], + [_entry(0, 200, -3.0)], + ], + ) + result = _load_and_merge(tmp_path) + assert (0, 0) in result + assert (1, 0) in result + assert result[(0, 0)].token_id == 100 + assert result[(1, 0)].token_id == 200 + + def test_is_valid_false_skipped(self, tmp_path: Path) -> None: + _write_rank_file( + tmp_path, + rank=0, + entries_by_batch=[ + [ + _entry(0, 100, -1.0, is_valid=True), + _entry(1, 101, -2.0, is_valid=False), + ], + ], + ) + result = _load_and_merge(tmp_path) + assert len(result) == 1 + assert (0, 0) in result + assert (0, 1) not in result + + def test_tp_deduplication(self, tmp_path: Path) -> None: + entries = [_entry(0, 100, -1.0)] + _write_rank_file(tmp_path, rank=0, entries_by_batch=[entries]) + _write_rank_file(tmp_path, rank=1, entries_by_batch=[entries]) + result = _load_and_merge(tmp_path) + assert len(result) == 1 + + def test_empty_directory(self, tmp_path: Path) -> None: + result = _load_and_merge(tmp_path) + assert result == {} + + def test_no_matching_files(self, tmp_path: Path) -> None: + (tmp_path / "other.json").write_text("{}") + result = _load_and_merge(tmp_path) + assert result == {} + + def test_multi_rank_different_positions_merge_cp_scenario(self, tmp_path: Path) -> None: + """CP zigzag: rank 0 has positions [0,1,6,7], rank 1 has [2,3,4,5] → merged has all 8.""" + _write_rank_file( + tmp_path, + rank=0, + entries_by_batch=[ + [ + _entry(0, 100, -1.0), + _entry(1, 101, -1.1), + _entry(6, 106, -1.6), + _entry(7, 107, -1.7), + ], + ], + ) + _write_rank_file( + tmp_path, + rank=1, + entries_by_batch=[ + [ + _entry(2, 102, -1.2), + _entry(3, 103, -1.3), + _entry(4, 104, -1.4), + _entry(5, 105, -1.5), + ], + ], + ) + result = _load_and_merge(tmp_path) + + assert len(result) == 8 + for pos in range(8): + assert (0, pos) in result + assert result[(0, pos)].token_id == 100 + pos + assert result[(0, pos)].logprob == pytest.approx(-1.0 - pos * 0.1) + + +class TestComputeComparison: + def _make_entries(self, data: list[tuple[int, int, float]]) -> dict[tuple[int, int], _PositionLogprob]: + return {(0, pos): _PositionLogprob(global_position=pos, token_id=tid, logprob=lp) for pos, tid, lp in data} + + def test_identical_passes(self) -> None: + entries = self._make_entries([(0, 100, -1.0), (1, 101, -2.0)]) + result = _compute_comparison( + baseline_entries=entries, + target_entries=entries, + threshold=1e-3, + ) + assert result.passed is True + assert result.max_abs_diff == 0.0 + assert result.num_positions == 2 + + def test_within_threshold_passes(self) -> None: + baseline = self._make_entries([(0, 100, -1.0)]) + target = self._make_entries([(0, 100, -1.0005)]) + result = _compute_comparison( + baseline_entries=baseline, + target_entries=target, + threshold=1e-3, + ) + assert result.passed is True + assert result.max_abs_diff == pytest.approx(0.0005) + + def test_exceeds_threshold_fails(self) -> None: + baseline = self._make_entries([(0, 100, -1.0)]) + target = self._make_entries([(0, 100, -1.1)]) + result = _compute_comparison( + baseline_entries=baseline, + target_entries=target, + threshold=0.01, + ) + assert result.passed is False + assert result.max_abs_diff == pytest.approx(0.1) + + def test_no_common_keys_passes(self) -> None: + baseline = self._make_entries([(0, 100, -1.0)]) + target: dict[tuple[int, int], _PositionLogprob] = { + (1, 0): _PositionLogprob(global_position=0, token_id=200, logprob=-2.0) + } + result = _compute_comparison( + baseline_entries=baseline, + target_entries=target, + threshold=1e-3, + ) + assert result.passed is True + assert result.num_positions == 0 + + def test_statistics(self) -> None: + baseline = self._make_entries( + [ + (0, 100, -1.0), + (1, 101, -2.0), + (2, 102, -3.0), + ] + ) + target = self._make_entries( + [ + (0, 100, -1.01), + (1, 101, -2.02), + (2, 102, -3.03), + ] + ) + result = _compute_comparison( + baseline_entries=baseline, + target_entries=target, + threshold=1.0, + ) + assert result.passed is True + assert result.num_positions == 3 + assert result.max_abs_diff == pytest.approx(0.03) + assert result.mean_abs_diff == pytest.approx(0.02) + assert result.median_abs_diff == pytest.approx(0.02) + + def test_worst_position_tracked(self) -> None: + baseline = self._make_entries( + [ + (0, 100, -1.0), + (1, 101, -2.0), + ] + ) + target = self._make_entries( + [ + (0, 100, -1.0), + (1, 101, -2.5), + ] + ) + result = _compute_comparison( + baseline_entries=baseline, + target_entries=target, + threshold=1.0, + ) + assert result.max_diff_position == 1 + assert result.max_diff_token_id == 101 + assert result.max_diff_baseline_logprob == pytest.approx(-2.0) + assert result.max_diff_target_logprob == pytest.approx(-2.5) + + def test_threshold_boundary_equal_passes(self) -> None: + baseline = self._make_entries([(0, 100, -1.0)]) + target = self._make_entries([(0, 100, -1.001)]) + result = _compute_comparison( + baseline_entries=baseline, + target_entries=target, + threshold=0.001, + ) + assert result.passed is True + + +class TestCompareLogprobs: + def test_both_have_data_passes(self, tmp_path: Path) -> None: + baseline_dir = tmp_path / "baseline" + target_dir = tmp_path / "target" + baseline_dir.mkdir() + target_dir.mkdir() + + entries = [_entry(0, 100, -1.0), _entry(1, 101, -2.0)] + _write_rank_file(baseline_dir, rank=0, entries_by_batch=[entries]) + _write_rank_file(target_dir, rank=0, entries_by_batch=[entries]) + + assert ( + compare_logprobs( + baseline_dir=baseline_dir, + target_dir=target_dir, + threshold=1e-3, + ) + is True + ) + + def test_large_diff_fails(self, tmp_path: Path) -> None: + baseline_dir = tmp_path / "baseline" + target_dir = tmp_path / "target" + baseline_dir.mkdir() + target_dir.mkdir() + + _write_rank_file( + baseline_dir, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.0)], + ], + ) + _write_rank_file( + target_dir, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -2.0)], + ], + ) + + assert ( + compare_logprobs( + baseline_dir=baseline_dir, + target_dir=target_dir, + threshold=0.01, + ) + is False + ) + + def test_empty_baseline_returns_true(self, tmp_path: Path) -> None: + baseline_dir = tmp_path / "baseline" + target_dir = tmp_path / "target" + baseline_dir.mkdir() + target_dir.mkdir() + _write_rank_file( + target_dir, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.0)], + ], + ) + + assert ( + compare_logprobs( + baseline_dir=baseline_dir, + target_dir=target_dir, + ) + is True + ) + + def test_empty_target_returns_true(self, tmp_path: Path) -> None: + baseline_dir = tmp_path / "baseline" + target_dir = tmp_path / "target" + baseline_dir.mkdir() + target_dir.mkdir() + _write_rank_file( + baseline_dir, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.0)], + ], + ) + + assert ( + compare_logprobs( + baseline_dir=baseline_dir, + target_dir=target_dir, + ) + is True + ) + + def test_custom_threshold(self, tmp_path: Path) -> None: + baseline_dir = tmp_path / "baseline" + target_dir = tmp_path / "target" + baseline_dir.mkdir() + target_dir.mkdir() + + _write_rank_file( + baseline_dir, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.0)], + ], + ) + _write_rank_file( + target_dir, + rank=0, + entries_by_batch=[ + [_entry(0, 100, -1.05)], + ], + ) + + # 0.05 > 0.01 → fail + assert ( + compare_logprobs( + baseline_dir=baseline_dir, + target_dir=target_dir, + threshold=0.01, + ) + is False + ) + + # 0.05 <= 0.1 → pass + assert ( + compare_logprobs( + baseline_dir=baseline_dir, + target_dir=target_dir, + threshold=0.1, + ) + is True + ) From 4e4b6e3f56e3e53363bff1045a2653293b341808 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:36:11 +0800 Subject: [PATCH 0030/1130] Add compare command for megatron runner (#778) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/cli/commands/__init__.py | 4 +- .../run_megatron/cli/commands/args.py | 14 ++ .../run_megatron/cli/commands/compare.py | 79 ++++++++++ .../run_megatron/cli/commands/test_compare.py | 149 ++++++++++++++++++ 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 miles/utils/debug_utils/run_megatron/cli/commands/compare.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/commands/test_compare.py diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py index d107309ba60..2bc30bfde65 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py @@ -2,13 +2,15 @@ Usage: python -m miles.utils.debug_utils.run_megatron run ... + python -m miles.utils.debug_utils.run_megatron compare ... python -m miles.utils.debug_utils.run_megatron show-model-args ... """ import typer -from miles.utils.debug_utils.run_megatron.cli.commands import run +from miles.utils.debug_utils.run_megatron.cli.commands import compare, run app: typer.Typer = typer.Typer(pretty_exceptions_enable=False) run.register(app) +compare.register(app) diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index 0046ecc7d57..acc814675a9 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -46,3 +46,17 @@ class RunArgs(CommonRunArgs): routing_replay_dump_path: Path | None = _field(default=None, help="Routing replay dump path") routing_replay_load_path: Path | None = _field(default=None, help="Routing replay load path") logprob_output: Path | None = _field(default=None, help="Directory to save per-token logprob JSON files") + + +@dataclasses.dataclass +class CompareArgs: + baseline_dir: Path = _field(help="Baseline dump directory") + target_dir: Path = _field(help="Target dump directory") + output_format: str = _field(default="text", help="Output format: text / json") + override_baseline_dims: str | None = _field(default=None, help="Override baseline dims") + override_target_dims: str | None = _field(default=None, help="Override target dims") + patch_config: Path | None = _field(default=None, help="Patch config YAML path") + diff_threshold: float | None = _field(default=None, help="Pass/fail threshold") + baseline_logprob_dir: Path | None = _field(default=None, help="Baseline logprob JSON directory") + target_logprob_dir: Path | None = _field(default=None, help="Target logprob JSON directory") + logprob_threshold: float | None = _field(default=None, help="Logprob max abs diff threshold") diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/compare.py b/miles/utils/debug_utils/run_megatron/cli/commands/compare.py new file mode 100644 index 00000000000..375c9c9fe7c --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/commands/compare.py @@ -0,0 +1,79 @@ +"""``compare`` CLI command.""" + +import subprocess +import sys + +import typer + +from miles.utils.debug_utils.run_megatron.cli.commands.args import CompareArgs +from miles.utils.debug_utils.run_megatron.logprob_comparator import compare_logprobs +from miles.utils.misc import exec_command +from miles.utils.typer_utils import dataclass_cli + + +def register(app: typer.Typer) -> None: + """Register the ``compare`` command on *app*.""" + app.command()(compare) + + +def compare_impl(args: CompareArgs) -> None: + """Core compare logic, called by both ``compare`` command and ``run_and_compare``.""" + activation_passed = _run_activation_comparison(args) + + logprob_passed = True + if args.baseline_logprob_dir is not None and args.target_logprob_dir is not None: + logprob_passed = compare_logprobs( + baseline_dir=args.baseline_logprob_dir, + target_dir=args.target_logprob_dir, + threshold=args.logprob_threshold if args.logprob_threshold is not None else 1e-3, + ) + + if not activation_passed or not logprob_passed: + failures: list[str] = [] + if not activation_passed: + failures.append("activation comparison") + if not logprob_passed: + failures.append("logprob comparison") + print(f"[cli] FAILED: {', '.join(failures)}", flush=True) + sys.exit(1) + + print("[cli] Compare completed.", flush=True) + + +def _run_activation_comparison(args: CompareArgs) -> bool: + cmd_parts: list[str] = [ + sys.executable, + "-m", + "sglang.srt.debug_utils.comparator", + "--baseline-path", + str(args.baseline_dir), + "--target-path", + str(args.target_dir), + "--output-format", + args.output_format, + "--preset", + "sglang_megatron", + ] + + optional_args: dict[str, object | None] = { + "--override-baseline-dims": args.override_baseline_dims, + "--override-target-dims": args.override_target_dims, + "--patch-config": args.patch_config, + "--diff-threshold": args.diff_threshold, + } + for flag, value in optional_args.items(): + if value is not None: + cmd_parts.extend([flag, str(value)]) + + try: + exec_command(" ".join(cmd_parts)) + return True + except subprocess.CalledProcessError: + print("[cli] Activation comparison failed", flush=True) + return False + + +@dataclass_cli(env_var_prefix="") +def compare(args: CompareArgs) -> None: + """Run comparator on existing dump directories.""" + compare_impl(args) diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_compare.py b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_compare.py new file mode 100644 index 00000000000..21ea7998b49 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_compare.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from miles.utils.debug_utils.run_megatron.cli.commands.args import CompareArgs +from miles.utils.debug_utils.run_megatron.cli.commands.compare import compare_impl + + +def _make_compare_args(**overrides: object) -> CompareArgs: + defaults = dict( + baseline_dir=Path("/baseline"), + target_dir=Path("/target"), + ) + defaults.update(overrides) + return CompareArgs(**defaults) # type: ignore[arg-type] + + +class TestActivationComparison: + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_calls_comparator(self, mock_exec: MagicMock) -> None: + compare_impl(_make_compare_args()) + mock_exec.assert_called_once() + cmd = mock_exec.call_args[0][0] + assert "sglang.srt.debug_utils.comparator" in cmd + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_required_args_in_command(self, mock_exec: MagicMock) -> None: + compare_impl(_make_compare_args()) + cmd = mock_exec.call_args[0][0] + assert "--baseline-path" in cmd + assert "--target-path" in cmd + assert "/baseline" in cmd + assert "/target" in cmd + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_optional_args_included(self, mock_exec: MagicMock) -> None: + compare_impl( + _make_compare_args( + override_baseline_dims="b s h", + override_target_dims="b s v", + patch_config=Path("/patch.yaml"), + diff_threshold=0.01, + ) + ) + cmd = mock_exec.call_args[0][0] + assert "--override-baseline-dims" in cmd + assert "--override-target-dims" in cmd + assert "--patch-config" in cmd + assert "--diff-threshold" in cmd + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_optional_args_excluded(self, mock_exec: MagicMock) -> None: + compare_impl(_make_compare_args()) + cmd = mock_exec.call_args[0][0] + assert "--override-baseline-dims" not in cmd + assert "--override-target-dims" not in cmd + assert "--patch-config" not in cmd + assert "--diff-threshold" not in cmd + + +class TestActivationFailure: + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_activation_failure_exits(self, mock_exec: MagicMock) -> None: + mock_exec.side_effect = subprocess.CalledProcessError(returncode=1, cmd="test") + with pytest.raises(SystemExit) as exc_info: + compare_impl(_make_compare_args()) + assert exc_info.value.code == 1 + + +class TestLogprobBranch: + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.compare_logprobs") + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_logprob_called_when_dirs_provided(self, mock_exec: MagicMock, mock_logprob: MagicMock) -> None: + mock_logprob.return_value = True + compare_impl( + _make_compare_args( + baseline_logprob_dir=Path("/bl_logprob"), + target_logprob_dir=Path("/tg_logprob"), + logprob_threshold=0.005, + ) + ) + mock_logprob.assert_called_once_with( + baseline_dir=Path("/bl_logprob"), + target_dir=Path("/tg_logprob"), + threshold=0.005, + ) + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.compare_logprobs") + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_logprob_not_called_when_dirs_missing(self, mock_exec: MagicMock, mock_logprob: MagicMock) -> None: + compare_impl(_make_compare_args()) + mock_logprob.assert_not_called() + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.compare_logprobs") + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_logprob_default_threshold(self, mock_exec: MagicMock, mock_logprob: MagicMock) -> None: + mock_logprob.return_value = True + compare_impl( + _make_compare_args( + baseline_logprob_dir=Path("/bl"), + target_logprob_dir=Path("/tg"), + logprob_threshold=None, + ) + ) + call_kwargs = mock_logprob.call_args[1] + assert call_kwargs["threshold"] == 1e-3 + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.compare_logprobs") + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_logprob_failure_exits(self, mock_exec: MagicMock, mock_logprob: MagicMock) -> None: + mock_logprob.return_value = False + with pytest.raises(SystemExit) as exc_info: + compare_impl( + _make_compare_args( + baseline_logprob_dir=Path("/bl"), + target_logprob_dir=Path("/tg"), + ) + ) + assert exc_info.value.code == 1 + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.compare_logprobs") + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_activation_pass_logprob_fail(self, mock_exec: MagicMock, mock_logprob: MagicMock) -> None: + mock_logprob.return_value = False + with pytest.raises(SystemExit): + compare_impl( + _make_compare_args( + baseline_logprob_dir=Path("/bl"), + target_logprob_dir=Path("/tg"), + ) + ) + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.compare_logprobs") + @patch("miles.utils.debug_utils.run_megatron.cli.commands.compare.exec_command") + def test_activation_fail_logprob_pass(self, mock_exec: MagicMock, mock_logprob: MagicMock) -> None: + mock_exec.side_effect = subprocess.CalledProcessError(returncode=1, cmd="test") + mock_logprob.return_value = True + with pytest.raises(SystemExit): + compare_impl( + _make_compare_args( + baseline_logprob_dir=Path("/bl"), + target_logprob_dir=Path("/tg"), + ) + ) + mock_logprob.assert_called_once() From 1ecb8cd2df7f99833f86cf495c0024711614aca9 Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 13:37:41 +0800 Subject: [PATCH 0031/1130] Add run-and-compare command for megatron runner (#779) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- .../run_megatron/cli/commands/__init__.py | 4 +- .../run_megatron/cli/commands/args.py | 19 ++ .../cli/commands/run_and_compare.py | 121 +++++++++++++ .../cli/commands/test_run_and_compare.py | 166 ++++++++++++++++++ 4 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 miles/utils/debug_utils/run_megatron/cli/commands/run_and_compare.py create mode 100644 tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run_and_compare.py diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py index 2bc30bfde65..88ba79cc17b 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/__init__.py @@ -3,14 +3,16 @@ Usage: python -m miles.utils.debug_utils.run_megatron run ... python -m miles.utils.debug_utils.run_megatron compare ... + python -m miles.utils.debug_utils.run_megatron run-and-compare ... python -m miles.utils.debug_utils.run_megatron show-model-args ... """ import typer -from miles.utils.debug_utils.run_megatron.cli.commands import compare, run +from miles.utils.debug_utils.run_megatron.cli.commands import compare, run, run_and_compare app: typer.Typer = typer.Typer(pretty_exceptions_enable=False) run.register(app) compare.register(app) +run_and_compare.register(app) diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index acc814675a9..b33de612dc6 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -60,3 +60,22 @@ class CompareArgs: baseline_logprob_dir: Path | None = _field(default=None, help="Baseline logprob JSON directory") target_logprob_dir: Path | None = _field(default=None, help="Target logprob JSON directory") logprob_threshold: float | None = _field(default=None, help="Logprob max abs diff threshold") + + +@dataclasses.dataclass(kw_only=True) +class RunAndCompareArgs(CommonRunArgs): + output_base_dir: Path = _field(help="Base output directory for dumps") + baseline: str = _field(help='Baseline parallel config, e.g. "--tp 1 --cp 1"') + target: str = _field(help='Target parallel config, e.g. "--tp 2 --cp 2"') + routing_replay: bool = _field( + default=False, + help="Enable routing replay (record on baseline, replay on target)", + ) + compare_logprobs: bool = _field( + default=True, + help="Compute and compare per-token logprobs between baseline and target", + ) + baseline_extra_args: str = _field(default="", help="Extra megatron args for baseline run only") + target_extra_args: str = _field(default="", help="Extra megatron args for target run only") + diff_threshold: float | None = _field(default=None, help="Activation diff pass/fail threshold") + logprob_threshold: float | None = _field(default=None, help="Logprob max abs diff threshold") diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/run_and_compare.py b/miles/utils/debug_utils/run_megatron/cli/commands/run_and_compare.py new file mode 100644 index 00000000000..9af9b2652ef --- /dev/null +++ b/miles/utils/debug_utils/run_megatron/cli/commands/run_and_compare.py @@ -0,0 +1,121 @@ +"""``run-and-compare`` CLI command.""" + +from __future__ import annotations + +import dataclasses +from pathlib import Path + +import typer + +from miles.utils.debug_utils.run_megatron.cli.commands.args import ( + CommonRunArgs, + CompareArgs, + RunAndCompareArgs, + RunArgs, +) +from miles.utils.debug_utils.run_megatron.cli.commands.compare import compare_impl +from miles.utils.debug_utils.run_megatron.cli.commands.run import run_impl +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig, parse_parallel_args +from miles.utils.typer_utils import dataclass_cli + + +def register(app: typer.Typer) -> None: + """Register the ``run-and-compare`` command on *app*.""" + app.command(name="run-and-compare")(run_and_compare) + + +@dataclass_cli(env_var_prefix="") +def run_and_compare(args: RunAndCompareArgs) -> None: + """Run baseline + target configs, then compare dumps.""" + baseline_config: ParallelConfig = ParallelConfig.from_parsed_args(parse_parallel_args(args.baseline)) + target_config: ParallelConfig = ParallelConfig.from_parsed_args(parse_parallel_args(args.target)) + + baseline_output: Path = args.output_base_dir / baseline_config.dir_name() + target_output: Path = args.output_base_dir / target_config.dir_name() + + common_fields: dict[str, object] = {f.name: getattr(args, f.name) for f in dataclasses.fields(CommonRunArgs)} + + replay_dir: Path | None = args.output_base_dir / "routing_replay" if args.routing_replay else None + + baseline_logprob_dir: Path | None = None + target_logprob_dir: Path | None = None + if args.compare_logprobs: + baseline_logprob_dir = baseline_output / "logprobs" + target_logprob_dir = target_output / "logprobs" + + _run_baseline_and_target( + baseline_config=baseline_config, + target_config=target_config, + baseline_output=baseline_output, + target_output=target_output, + replay_dir=replay_dir, + common_fields=common_fields, + baseline_extra_args=args.baseline_extra_args, + target_extra_args=args.target_extra_args, + baseline_logprob_dir=baseline_logprob_dir, + target_logprob_dir=target_logprob_dir, + ) + + print("[cli] Comparing baseline vs target", flush=True) + compare_impl( + CompareArgs( + baseline_dir=baseline_output / "standalone", + target_dir=target_output / "standalone", + output_format="json", + diff_threshold=args.diff_threshold, + baseline_logprob_dir=baseline_logprob_dir, + target_logprob_dir=target_logprob_dir, + logprob_threshold=args.logprob_threshold, + ) + ) + + +def _append_extra_args(common_fields: dict[str, object], extra: str) -> dict[str, object]: + if not extra: + return common_fields + result: dict[str, object] = dict(common_fields) + result["extra_args"] = f"{common_fields['extra_args']} {extra}".strip() + return result + + +def _run_baseline_and_target( + *, + baseline_config: ParallelConfig, + target_config: ParallelConfig, + baseline_output: Path, + target_output: Path, + replay_dir: Path | None, + common_fields: dict[str, object], + baseline_extra_args: str, + target_extra_args: str, + baseline_logprob_dir: Path | None, + target_logprob_dir: Path | None, +) -> None: + if replay_dir is not None: + if baseline_config.nproc != 1: + raise ValueError(f"Routing replay requires single-rank baseline (nproc=1), got {baseline_config}") + print("[cli] Routing replay enabled", flush=True) + + print("[cli] Step 1/2: Baseline run", flush=True) + run_impl( + RunArgs( + **_append_extra_args(common_fields, baseline_extra_args), + **dataclasses.asdict(baseline_config), + output_dir=baseline_output, + routing_replay_dump_path=replay_dir, + routing_replay_load_path=None, + logprob_output=baseline_logprob_dir, + ) + ) + + print("[cli] Step 2/2: Target run", flush=True) + run_impl( + RunArgs( + **_append_extra_args(common_fields, target_extra_args), + **dataclasses.asdict(target_config), + output_dir=target_output, + routing_replay_dump_path=None, + routing_replay_load_path=replay_dir, + logprob_output=target_logprob_dir, + ) + ) diff --git a/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run_and_compare.py b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run_and_compare.py new file mode 100644 index 00000000000..25dc325d571 --- /dev/null +++ b/tests/fast/utils/debug_utils/run_megatron/cli/commands/test_run_and_compare.py @@ -0,0 +1,166 @@ +import dataclasses +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from miles.utils.debug_utils.run_megatron.cli.commands.args import CommonRunArgs +from miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare import ( + _append_extra_args, + _run_baseline_and_target, +) +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig + + +def _make_common_fields(**overrides: object) -> dict[str, object]: + base = CommonRunArgs( + model_type="deepseek_v3", + hf_checkpoint=Path("/fake/hf"), + ) + result = dataclasses.asdict(base) + result.update(overrides) + return result + + +class TestRunAndCompare: + @patch("miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare.run_impl") + def test_calls_run_twice(self, mock_run: MagicMock) -> None: + _run_baseline_and_target( + baseline_config=ParallelConfig(tp=1), + target_config=ParallelConfig(tp=2), + baseline_output=Path("/tmp/baseline"), + target_output=Path("/tmp/target"), + replay_dir=None, + common_fields=_make_common_fields(), + baseline_extra_args="", + target_extra_args="", + baseline_logprob_dir=None, + target_logprob_dir=None, + ) + assert mock_run.call_count == 2 + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare.run_impl") + def test_output_dir_uses_dir_name(self, mock_run: MagicMock) -> None: + baseline_config = ParallelConfig(tp=1) + target_config = ParallelConfig(tp=2) + + _run_baseline_and_target( + baseline_config=baseline_config, + target_config=target_config, + baseline_output=Path("/tmp/out") / baseline_config.dir_name(), + target_output=Path("/tmp/out") / target_config.dir_name(), + replay_dir=None, + common_fields=_make_common_fields(), + baseline_extra_args="", + target_extra_args="", + baseline_logprob_dir=None, + target_logprob_dir=None, + ) + + baseline_args = mock_run.call_args_list[0][0][0] + target_args = mock_run.call_args_list[1][0][0] + + assert baseline_config.dir_name() in str(baseline_args.output_dir) + assert target_config.dir_name() in str(target_args.output_dir) + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare.run_impl") + def test_replay_baseline_nproc1_required(self, mock_run: MagicMock) -> None: + with pytest.raises(ValueError, match="single-rank baseline"): + _run_baseline_and_target( + baseline_config=ParallelConfig(tp=2), + target_config=ParallelConfig(tp=2), + baseline_output=Path("/tmp/baseline"), + target_output=Path("/tmp/target"), + replay_dir=Path("/tmp/replay"), + common_fields=_make_common_fields(), + baseline_extra_args="", + target_extra_args="", + baseline_logprob_dir=None, + target_logprob_dir=None, + ) + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare.run_impl") + def test_replay_paths_passed_correctly(self, mock_run: MagicMock) -> None: + _run_baseline_and_target( + baseline_config=ParallelConfig(tp=1), + target_config=ParallelConfig(tp=2), + baseline_output=Path("/tmp/baseline"), + target_output=Path("/tmp/target"), + replay_dir=Path("/tmp/replay"), + common_fields=_make_common_fields(), + baseline_extra_args="", + target_extra_args="", + baseline_logprob_dir=None, + target_logprob_dir=None, + ) + + baseline_args = mock_run.call_args_list[0][0][0] + target_args = mock_run.call_args_list[1][0][0] + + assert baseline_args.routing_replay_dump_path is not None + assert baseline_args.routing_replay_load_path is None + assert target_args.routing_replay_dump_path is None + assert target_args.routing_replay_load_path is not None + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare.run_impl") + def test_logprob_dirs_wired_when_provided(self, mock_run: MagicMock) -> None: + _run_baseline_and_target( + baseline_config=ParallelConfig(tp=1), + target_config=ParallelConfig(tp=2), + baseline_output=Path("/tmp/baseline"), + target_output=Path("/tmp/target"), + replay_dir=None, + common_fields=_make_common_fields(), + baseline_extra_args="", + target_extra_args="", + baseline_logprob_dir=Path("/tmp/baseline/logprobs"), + target_logprob_dir=Path("/tmp/target/logprobs"), + ) + + baseline_args = mock_run.call_args_list[0][0][0] + target_args = mock_run.call_args_list[1][0][0] + + assert baseline_args.logprob_output == Path("/tmp/baseline/logprobs") + assert target_args.logprob_output == Path("/tmp/target/logprobs") + + @patch("miles.utils.debug_utils.run_megatron.cli.commands.run_and_compare.run_impl") + def test_extra_args_appended_to_baseline_and_target(self, mock_run: MagicMock) -> None: + _run_baseline_and_target( + baseline_config=ParallelConfig(tp=1), + target_config=ParallelConfig(tp=2), + baseline_output=Path("/tmp/baseline"), + target_output=Path("/tmp/target"), + replay_dir=None, + common_fields=_make_common_fields(), + baseline_extra_args="--baseline-flag", + target_extra_args="--target-flag", + baseline_logprob_dir=None, + target_logprob_dir=None, + ) + + baseline_args = mock_run.call_args_list[0][0][0] + target_args = mock_run.call_args_list[1][0][0] + + assert "--baseline-flag" in baseline_args.extra_args + assert "--target-flag" in target_args.extra_args + + +class TestAppendExtraArgs: + def test_appends_extra_to_existing_extra_args(self) -> None: + result = _append_extra_args( + _make_common_fields(extra_args="--foo"), + "--bar", + ) + assert result["extra_args"] == "--foo --bar" + + def test_empty_extra_preserves_original(self) -> None: + result = _append_extra_args( + _make_common_fields(extra_args="--foo"), + "", + ) + assert result is _make_common_fields(extra_args="--foo") or result == _make_common_fields(extra_args="--foo") + + def test_empty_both_returns_original(self) -> None: + common = _make_common_fields(extra_args="") + result = _append_extra_args(common, "") + assert result is common From 7f039258fb1ba1309aa41b72059b1111e228fdda Mon Sep 17 00:00:00 2001 From: fzyzcjy <5236035+fzyzcjy@users.noreply.github.com> Date: Wed, 6 May 2026 14:05:24 +0800 Subject: [PATCH 0032/1130] Add standalone E2E tests for megatron runner (#780) Co-authored-by: Yueming Yuan <112649537+yueming-yuan@users.noreply.github.com> --- miles/backends/megatron_utils/arguments.py | 2 +- miles/backends/training_utils/cp_utils.py | 4 +- .../run_megatron/cli/commands/args.py | 28 ++-- .../debug_utils/run_megatron/worker/main.py | 5 +- .../debug_utils/run_megatron/worker/output.py | 9 +- .../debug_utils/run_megatron/worker/replay.py | 15 +- .../run_megatron/worker/script_args.py | 2 +- tests/e2e/short/test_run_megatron.py | 151 ++++++++++++++++++ .../run_megatron/worker/test_batch.py | 2 +- tools/convert_hf_to_torch_dist.py | 4 +- 10 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 tests/e2e/short/test_run_megatron.py diff --git a/miles/backends/megatron_utils/arguments.py b/miles/backends/megatron_utils/arguments.py index f445ca979ee..f5ece6d882a 100644 --- a/miles/backends/megatron_utils/arguments.py +++ b/miles/backends/megatron_utils/arguments.py @@ -1,8 +1,8 @@ import logging import os -from megatron.core.tokenizers.utils.build_tokenizer import vocab_size_with_padding as _vocab_size_with_padding from megatron.training.arguments import parse_args, validate_args +from megatron.training.tokenizer.tokenizer import _vocab_size_with_padding __all__ = ["validate_args", "parse_args", "set_default_megatron_args"] diff --git a/miles/backends/training_utils/cp_utils.py b/miles/backends/training_utils/cp_utils.py index 5f077c9e748..cb3188e01eb 100644 --- a/miles/backends/training_utils/cp_utils.py +++ b/miles/backends/training_utils/cp_utils.py @@ -242,9 +242,7 @@ def pad_tokens(tokens, pad): return torch.cat([tokens[start_1:end_1], tokens[start_2:end_2]]) -def natural_to_zigzag_slice( - tensor: torch.Tensor, dim: int, cp_size: int, cp_rank: int -) -> torch.Tensor: +def natural_to_zigzag_slice(tensor: torch.Tensor, dim: int, cp_size: int, cp_rank: int) -> torch.Tensor: """Slice a full-length tensor into the zigzag ring-attention CP layout. Rank ``cp_rank`` owns chunks ``[cp_rank, 2*cp_size - 1 - cp_rank]`` from the diff --git a/miles/utils/debug_utils/run_megatron/cli/commands/args.py b/miles/utils/debug_utils/run_megatron/cli/commands/args.py index b33de612dc6..a851b0e400c 100644 --- a/miles/utils/debug_utils/run_megatron/cli/commands/args.py +++ b/miles/utils/debug_utils/run_megatron/cli/commands/args.py @@ -48,20 +48,6 @@ class RunArgs(CommonRunArgs): logprob_output: Path | None = _field(default=None, help="Directory to save per-token logprob JSON files") -@dataclasses.dataclass -class CompareArgs: - baseline_dir: Path = _field(help="Baseline dump directory") - target_dir: Path = _field(help="Target dump directory") - output_format: str = _field(default="text", help="Output format: text / json") - override_baseline_dims: str | None = _field(default=None, help="Override baseline dims") - override_target_dims: str | None = _field(default=None, help="Override target dims") - patch_config: Path | None = _field(default=None, help="Patch config YAML path") - diff_threshold: float | None = _field(default=None, help="Pass/fail threshold") - baseline_logprob_dir: Path | None = _field(default=None, help="Baseline logprob JSON directory") - target_logprob_dir: Path | None = _field(default=None, help="Target logprob JSON directory") - logprob_threshold: float | None = _field(default=None, help="Logprob max abs diff threshold") - - @dataclasses.dataclass(kw_only=True) class RunAndCompareArgs(CommonRunArgs): output_base_dir: Path = _field(help="Base output directory for dumps") @@ -79,3 +65,17 @@ class RunAndCompareArgs(CommonRunArgs): target_extra_args: str = _field(default="", help="Extra megatron args for target run only") diff_threshold: float | None = _field(default=None, help="Activation diff pass/fail threshold") logprob_threshold: float | None = _field(default=None, help="Logprob max abs diff threshold") + + +@dataclasses.dataclass +class CompareArgs: + baseline_dir: Path = _field(help="Baseline dump directory") + target_dir: Path = _field(help="Target dump directory") + output_format: str = _field(default="text", help="Output format: text / json") + override_baseline_dims: str | None = _field(default=None, help="Override baseline dims") + override_target_dims: str | None = _field(default=None, help="Override target dims") + patch_config: Path | None = _field(default=None, help="Patch config YAML path") + diff_threshold: float | None = _field(default=None, help="Pass/fail threshold") + baseline_logprob_dir: Path | None = _field(default=None, help="Baseline logprob JSON directory") + target_logprob_dir: Path | None = _field(default=None, help="Target logprob JSON directory") + logprob_threshold: float | None = _field(default=None, help="Logprob max abs diff threshold") diff --git a/miles/utils/debug_utils/run_megatron/worker/main.py b/miles/utils/debug_utils/run_megatron/worker/main.py index 2218a439eb4..359a51c1d60 100644 --- a/miles/utils/debug_utils/run_megatron/worker/main.py +++ b/miles/utils/debug_utils/run_megatron/worker/main.py @@ -29,6 +29,7 @@ from miles.backends.megatron_utils.checkpoint import load_checkpoint from miles.backends.megatron_utils.initialize import init from miles.backends.megatron_utils.model_provider import get_model_provider_func +from miles.backends.training_utils.parallel import get_parallel_state from miles.utils.debug_utils.run_megatron.worker.batch import loss_func, prepare_batch from miles.utils.debug_utils.run_megatron.worker.output import compute_and_save_output_info from miles.utils.debug_utils.run_megatron.worker.replay import ( @@ -63,8 +64,8 @@ def main() -> None: batch: dict[str, torch.Tensor] = prepare_batch( token_ids=token_ids, batch_size=args.micro_batch_size, - cp_rank=mpu.get_context_parallel_rank(), - cp_size=mpu.get_context_parallel_world_size(), + cp_rank=get_parallel_state().cp.rank, + cp_size=get_parallel_state().cp.size, ) if rank == 0: diff --git a/miles/utils/debug_utils/run_megatron/worker/output.py b/miles/utils/debug_utils/run_megatron/worker/output.py index 0552cfb4d36..ad0406ea44d 100644 --- a/miles/utils/debug_utils/run_megatron/worker/output.py +++ b/miles/utils/debug_utils/run_megatron/worker/output.py @@ -8,7 +8,8 @@ import torch import torch.distributed as dist -from megatron.core import mpu + +from miles.backends.training_utils.parallel import get_parallel_state def compute_and_save_output_info( @@ -63,9 +64,9 @@ def _compute_output_info( return { "rank": rank, - "tp_size": mpu.get_tensor_model_parallel_world_size() if dist.is_initialized() else 1, - "cp_size": mpu.get_context_parallel_world_size() if dist.is_initialized() else 1, - "pp_size": mpu.get_pipeline_model_parallel_world_size() if dist.is_initialized() else 1, + "tp_size": get_parallel_state().tp.size if dist.is_initialized() else 1, + "cp_size": get_parallel_state().cp.size if dist.is_initialized() else 1, + "pp_size": get_parallel_state().pp.size if dist.is_initialized() else 1, "logprob_entries": logprob_entries, } diff --git a/miles/utils/debug_utils/run_megatron/worker/replay.py b/miles/utils/debug_utils/run_megatron/worker/replay.py index 0049f874483..9b84b47ea5e 100644 --- a/miles/utils/debug_utils/run_megatron/worker/replay.py +++ b/miles/utils/debug_utils/run_megatron/worker/replay.py @@ -90,12 +90,17 @@ def _get_parallel_ranks() -> _ParallelRanks: """Return parallel ranks, defaulting to 1/0 if mpu is not initialized.""" from megatron.core import mpu - initialized: bool = mpu.is_initialized() + from miles.backends.training_utils.parallel import get_parallel_state + + if not mpu.is_initialized(): + return _ParallelRanks(cp_size=1, cp_rank=0, tp_size=1, tp_rank=0) + + state = get_parallel_state() return _ParallelRanks( - cp_size=mpu.get_context_parallel_world_size() if initialized else 1, - cp_rank=mpu.get_context_parallel_rank() if initialized else 0, - tp_size=mpu.get_tensor_model_parallel_world_size() if initialized else 1, - tp_rank=mpu.get_tensor_model_parallel_rank() if initialized else 0, + cp_size=state.cp.size, + cp_rank=state.cp.rank, + tp_size=state.tp.size, + tp_rank=state.tp.rank, ) diff --git a/miles/utils/debug_utils/run_megatron/worker/script_args.py b/miles/utils/debug_utils/run_megatron/worker/script_args.py index e8fb8bbc6c1..de77d0cb652 100644 --- a/miles/utils/debug_utils/run_megatron/worker/script_args.py +++ b/miles/utils/debug_utils/run_megatron/worker/script_args.py @@ -2,7 +2,7 @@ All flags use the ``--script-`` prefix to avoid collision with Megatron's own argparse namespace. Adding a new field here is the *only* change needed — -serialization (CLI -> worker) and deserialization (worker argparse) are automatic. +serialization (CLI → worker) and deserialization (worker argparse) are automatic. """ from __future__ import annotations diff --git a/tests/e2e/short/test_run_megatron.py b/tests/e2e/short/test_run_megatron.py new file mode 100644 index 00000000000..05eda14dad4 --- /dev/null +++ b/tests/e2e/short/test_run_megatron.py @@ -0,0 +1,151 @@ +# WARNING: Do NOT relax any assert logic in this file. All assertions must remain strict. +# The comparator must report all-passed with zero failures — no exceptions. + +# Usage: This is a typer CLI with 2 commands: +# python test_run_megatron.py run --mode Full: prepare + run baseline + run target + compare +# python test_run_megatron.py compare --mode --dump-dir +# Re-run comparator on existing dumps + +import dataclasses +import sys +import tempfile +from pathlib import Path +from typing import Annotated + +_MILES_ROOT: Path = Path(__file__).resolve().parents[3] +if str(_MILES_ROOT) not in sys.path: + sys.path.insert(0, str(_MILES_ROOT)) + +import typer +from tests.e2e.conftest_dumper import MEGATRON_PATCHER_YAMLS, clear_proxy_env + +import miles.utils.external_utils.command_utils as U +from miles.utils.debug_utils.run_megatron.cli.parallel_utils import ParallelConfig, parse_parallel_args +from miles.utils.misc import exec_command + +app: typer.Typer = typer.Typer() + +HF_REPO: str = "fzyzcjy/Qwen3-30B-A3B-5layer" +MODEL_NAME: str = "Qwen3-30B-A3B-5layer" +MODEL_TYPE: str = "qwen3-30B-A3B-5layer" +NUM_GPUS: int = 8 +NUM_LAYERS: int = 5 + +_RUN_DIR: Path = Path(tempfile.mkdtemp(prefix="test_run_megatron_")) + + +@dataclasses.dataclass(frozen=True) +class _ModeConfig: + baseline_args: str + target_args: str + format: str = "bshd" + target_extra_megatron_args: str = "" + + +CONFIGS: dict[str, _ModeConfig] = { + "tp1_vs_tp2pp2cp2": _ModeConfig( + baseline_args="--tp 1", + target_args="--tp 2 --pp 2 --cp 2 --ep 1", + format="bshd", + target_extra_megatron_args="--decoder-first-pipeline-num-layers 3 --decoder-last-pipeline-num-layers 2", + ), + # TODO: THD mode not yet supported — standalone worker (batch.py) hardcodes + # qkv_format="bshd" and does not construct PackedSeqParams. Enabling THD + # requires reworking CP slicing, qkv_format passthrough, and PackedSeqParams + # construction in the worker. + # "tp1_vs_tp2pp2cp2_thd": _ModeConfig( + # baseline_args="--tp 1", + # target_args="--tp 2 --pp 2 --cp 2 --ep 1", + # format="thd", + # target_extra_megatron_args="--decoder-first-pipeline-num-layers 3 --decoder-last-pipeline-num-layers 2", + # ), +} + + +def _resolve_mode(mode: str) -> tuple[str, _ModeConfig]: + if mode not in CONFIGS: + raise typer.BadParameter(f"Unknown mode {mode!r}, valid: {list(CONFIGS.keys())}") + return mode, CONFIGS[mode] + + +def _prepare(dump_dir: Path, config: _ModeConfig) -> Path: + """Download model, convert checkpoint, write source patcher config.""" + exec_command("mkdir -p /root/models") + exec_command(f"hf download {HF_REPO} --local-dir /root/models/{MODEL_NAME}") + U.convert_checkpoint( + model_name=MODEL_NAME, + megatron_model_type=MODEL_TYPE, + num_gpus_per_node=min(NUM_GPUS, NUM_LAYERS), + ) + exec_command(f"rm -rf {dump_dir}") + + source_patcher_path: Path = _RUN_DIR / "megatron_source_patcher.yaml" + yaml_content: str = ( + MEGATRON_PATCHER_YAMLS[config.format].replace(" ep:replicated", "").replace(" etp:replicated", "") + ) + source_patcher_path.write_text(yaml_content) + return source_patcher_path + + +@app.command() +def run( + mode: Annotated[str, typer.Option(help="Config mode: " + ", ".join(CONFIGS.keys()))], +) -> None: + """Full pipeline: prepare + run baseline + run target + compare.""" + _config_name, config = _resolve_mode(mode) + dump_dir: Path = _RUN_DIR / "dumps" + print(f"Run directory: {_RUN_DIR}") + + source_patcher_config: Path = _prepare(dump_dir=dump_dir, config=config) + clear_proxy_env() + + extra_args: str = "--attention-backend flash" + + target_extra_args_part: str = "" + if config.target_extra_megatron_args: + target_extra_args_part = f"--target-extra-args '{config.target_extra_megatron_args}' " + + cmd: str = ( + f"python -m miles.utils.debug_utils.run_megatron run-and-compare " + f"--model-type {MODEL_TYPE} " + f"--hf-checkpoint /root/models/{MODEL_NAME} " + f"--ref-load /root/{MODEL_NAME}_torch_dist " + f"--output-base-dir {dump_dir} " + f"--baseline '{config.baseline_args}' " + f"--target '{config.target_args}' " + f"--seq-length 128 " + f"--batch-size 1 " + f"--prompt-mode math " + f"--sp " + f"--source-patcher-config {source_patcher_config} " + f"--dumper-filter 'layer_id is None or layer_id < 3' " + f"--diff-threshold 0.002 " + f"--logprob-threshold 0.06 " + f"{target_extra_args_part}" + f"--extra-args '{extra_args}'" + ) + exec_command(cmd) + + +@app.command() +def compare( + mode: Annotated[str, typer.Option(help="Config mode: " + ", ".join(CONFIGS.keys()))], + dump_dir: Annotated[str, typer.Option(help="Path to existing dump base directory")], +) -> None: + """Re-run comparator on existing dumps (no training).""" + _config_name, config = _resolve_mode(mode) + base: Path = Path(dump_dir) + + baseline_dir_name: str = ParallelConfig.from_parsed_args(parse_parallel_args(config.baseline_args)).dir_name() + target_dir_name: str = ParallelConfig.from_parsed_args(parse_parallel_args(config.target_args)).dir_name() + + cmd: str = ( + f"python -m miles.utils.debug_utils.run_megatron compare " + f"--baseline-dir {base / baseline_dir_name / 'standalone'} " + f"--target-dir {base / target_dir_name / 'standalone'}" + ) + exec_command(cmd) + + +if __name__ == "__main__": + app() diff --git a/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py b/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py index 79bc98b8d85..d59556f7efa 100644 --- a/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py +++ b/tests/fast/utils/debug_utils/run_megatron/worker/test_batch.py @@ -496,7 +496,7 @@ def test_seq7_cp2_all_ranks_cover_original_tokens(self) -> None: batch = prepare_batch(token_ids=token_ids, batch_size=1, cp_rank=rank, cp_size=2, device="cpu") pos = batch["position_ids"][0].tolist() ids = batch["input_ids"][0].tolist() - all_token_pos.extend(zip(pos, ids, strict=False)) + all_token_pos.extend(zip(pos, ids, strict=True)) original_positions = {p for p, _ in all_token_pos if p < 7} assert original_positions == set(range(7)) diff --git a/tools/convert_hf_to_torch_dist.py b/tools/convert_hf_to_torch_dist.py index 1ea11cf1561..250af0b683e 100644 --- a/tools/convert_hf_to_torch_dist.py +++ b/tools/convert_hf_to_torch_dist.py @@ -46,8 +46,8 @@ def get_args(): args.global_batch_size = int(os.environ.get("WORLD_SIZE", "1")) assert world_size <= args.num_layers, ( - f"World size {world_size} must be less than or equal to number of layers {args.num_layers}. " - "You are using too many GPUs for this conversion." + f"World size {world_size} must be <= number of layers {args.num_layers}. " + "Use fewer GPUs (--nproc-per-node) for this conversion." ) def ceildiv(a, b): From b6ccabd6be9cdfe4f87636a6716de6bc05d159d2 Mon Sep 17 00:00:00 2001 From: Yisheng Gong Date: Wed, 6 May 2026 23:58:45 -0700 Subject: [PATCH 0033/1130] [lora] avoid merge adapter weights for base weight sync (#1087) --- .../megatron_utils/update_weight/hf_weight_iterator_bridge.py | 1 + 1 file changed, 1 insertion(+) 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 8c972717217..41ed0a95227 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 @@ -34,6 +34,7 @@ def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base" self.model, cpu=False, conversion_tasks=conversion_tasks, + merge_adapter_weights=False, ) # TODO: verify if postprocess_hf_param is needed for LoRA weights From f86d44963f94539213984fe31be3046592e5ae8b Mon Sep 17 00:00:00 2001 From: Mathew Han <49226490+mathewjhan@users.noreply.github.com> Date: Thu, 7 May 2026 15:53:42 -0700 Subject: [PATCH 0034/1130] [fix] eval sample logging when sample is a list (#985) Co-authored-by: Zhichen Zeng --- miles/rollout/sglang_rollout.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 551021f53c5..461ebdf3148 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -565,10 +565,11 @@ async def eval_rollout_single_dataset( for coro in asyncio.as_completed(tasks): sample = await coro if do_print: + logged_sample = sample[0] if isinstance(sample, list) else sample logger.info( "eval_rollout_single_dataset example data: " - f"{[str(sample.prompt) + sample.response]} " - f"reward={sample.reward}" + f"{[str(logged_sample.prompt) + logged_sample.response]} " + f"reward={logged_sample.reward}" ) do_print = False if isinstance(sample, list): From 44803cd0d5db32b0d2b4f1cd6cb2a2818a35eaab Mon Sep 17 00:00:00 2001 From: "Ethan (Yusheng) Su" Date: Fri, 8 May 2026 07:51:28 +0200 Subject: [PATCH 0035/1130] Revert "[lora] avoid merge adapter weights for base weight sync" (#1091) --- .../megatron_utils/update_weight/hf_weight_iterator_bridge.py | 1 - 1 file changed, 1 deletion(-) 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 41ed0a95227..8c972717217 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 @@ -34,7 +34,6 @@ def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base" self.model, cpu=False, conversion_tasks=conversion_tasks, - merge_adapter_weights=False, ) # TODO: verify if postprocess_hf_param is needed for LoRA weights From 712201d5e05d1c5dd825d79e8467a5cda44cce58 Mon Sep 17 00:00:00 2001 From: Yisheng Gong Date: Fri, 8 May 2026 00:51:15 -0700 Subject: [PATCH 0036/1130] chore: [lora] avoid merge adapter weights (#1092) --- .../update_weight/hf_weight_iterator_bridge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 8c972717217..33f7523822d 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 @@ -34,16 +34,17 @@ def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base" self.model, cpu=False, conversion_tasks=conversion_tasks, + merge_adapter_weights=False, ) # TODO: verify if postprocess_hf_param is needed for LoRA weights named_weights = ( ( - hf_param_name, + hf_param_name.replace(".base_layer.", "."), postprocess_hf_param( args=self.args, megatron_param_name=megatron_param_name, - hf_param_name=hf_param_name, + hf_param_name=hf_param_name.replace(".base_layer.", "."), param=weight, ), ) From 323ccbdd1dddea3fc9a0dec95c918fe0f3d96afd Mon Sep 17 00:00:00 2001 From: Lawrence Wu Date: Fri, 8 May 2026 12:09:46 -0700 Subject: [PATCH 0037/1130] fix: handle Qwen3.5 in quantize_params_fp8 (#1093) Co-authored-by: Zhichen Zeng --- .../megatron_to_hf/processors/quantizer_fp8.py | 7 ++++++- 1 file changed, 6 insertions(+), 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 2d4197e1e14..afef9ed6789 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 @@ -9,7 +9,8 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantization_config): assert quantization_config["quant_method"] == "fp8" - assert quantization_config["fmt"] == "e4m3" + fmt = quantization_config.get("fmt", "e4m3") + assert fmt == "e4m3", f"Unsupported FP8 format: {fmt}" assert quantization_config["activation_scheme"] == "dynamic" weight_block_size = quantization_config.get("weight_block_size", None) @@ -75,6 +76,10 @@ def quantize_params_fp8(args, megatron_name, converted_named_params, quantizatio # indexer "self_attention.wq_b.weight", "self_attention.wk.weight", + # linear attention + "self_attention.linear_attn.in_proj_qkv.weight", + "self_attention.linear_attn.in_proj_z.weight", + "self_attention.linear_attn.out_proj.weight", ]: quantize_named_params = [] for converted_name, param in converted_named_params: From 95e10bcbe3450a4fafd3324527cbd5289e48b7f2 Mon Sep 17 00:00:00 2001 From: Lawrence Wu Date: Sun, 10 May 2026 15:17:47 -0700 Subject: [PATCH 0038/1130] fix: quote $MOE_LAYER_FREQ (#1094) --- scripts/models/deepseek-v3.sh | 2 +- scripts/models/glm4.5-106B-A12B.sh | 2 +- scripts/models/glm4.5-355B-A32B.sh | 2 +- scripts/models/kimi-k2-thinking.sh | 2 +- scripts/models/kimi-k2.sh | 2 +- scripts/models/moonlight.sh | 2 +- scripts/models/qwen3-235B-A22B.sh | 2 +- scripts/models/qwen3-30B-A3B.sh | 2 +- scripts/models/qwen3-next-80B-A3B.sh | 2 +- scripts/models/qwen3.5-35B-A3B.sh | 2 +- scripts/models/qwen3.6-35B-A3B.sh | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/models/deepseek-v3.sh b/scripts/models/deepseek-v3.sh index 7efe0e99df4..8c50d2c9405 100644 --- a/scripts/models/deepseek-v3.sh +++ b/scripts/models/deepseek-v3.sh @@ -43,7 +43,7 @@ MODEL_ARGS=( # moe --num-experts 256 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --moe-ffn-hidden-size 2048 --moe-router-topk 8 --moe-shared-expert-intermediate-size 2048 diff --git a/scripts/models/glm4.5-106B-A12B.sh b/scripts/models/glm4.5-106B-A12B.sh index 6bab15b77f7..8c244164336 100644 --- a/scripts/models/glm4.5-106B-A12B.sh +++ b/scripts/models/glm4.5-106B-A12B.sh @@ -31,7 +31,7 @@ MODEL_ARGS=( --moe-router-load-balancing-type seq_aux_loss --moe-token-dispatcher-type alltoall --moe-router-topk 8 - --moe-layer-freq [0]*$N_DENSE_LAYERS+[1]*$N_MOE_LAYERS + --moe-layer-freq "[0]*$N_DENSE_LAYERS+[1]*$N_MOE_LAYERS" --num-experts 128 --moe-grouped-gemm --moe-router-dtype fp32 diff --git a/scripts/models/glm4.5-355B-A32B.sh b/scripts/models/glm4.5-355B-A32B.sh index 8aca21fd475..68b590ed4a7 100644 --- a/scripts/models/glm4.5-355B-A32B.sh +++ b/scripts/models/glm4.5-355B-A32B.sh @@ -34,7 +34,7 @@ MODEL_ARGS=( --moe-token-dispatcher-type alltoall --moe-router-topk 8 --moe-router-topk-scaling-factor 2.5 - --moe-layer-freq [0]*$N_DENSE_LAYERS+[1]*$N_MOE_LAYERS + --moe-layer-freq "[0]*$N_DENSE_LAYERS+[1]*$N_MOE_LAYERS" --num-experts 160 --moe-grouped-gemm --moe-router-dtype fp32 diff --git a/scripts/models/kimi-k2-thinking.sh b/scripts/models/kimi-k2-thinking.sh index 51fe4fe37a1..b7fceda5913 100644 --- a/scripts/models/kimi-k2-thinking.sh +++ b/scripts/models/kimi-k2-thinking.sh @@ -43,7 +43,7 @@ MODEL_ARGS=( # moe --num-experts 384 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --moe-ffn-hidden-size 2048 --moe-router-topk 8 --moe-shared-expert-intermediate-size 2048 diff --git a/scripts/models/kimi-k2.sh b/scripts/models/kimi-k2.sh index d9fb1564e32..eafb7acadd1 100644 --- a/scripts/models/kimi-k2.sh +++ b/scripts/models/kimi-k2.sh @@ -43,7 +43,7 @@ MODEL_ARGS=( # moe --num-experts 384 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --moe-ffn-hidden-size 2048 --moe-router-topk 8 --moe-shared-expert-intermediate-size 2048 diff --git a/scripts/models/moonlight.sh b/scripts/models/moonlight.sh index e1e4596f648..bcce99892a7 100644 --- a/scripts/models/moonlight.sh +++ b/scripts/models/moonlight.sh @@ -48,7 +48,7 @@ MODEL_ARGS=( # moe --num-experts 64 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --moe-ffn-hidden-size $MOE_FFN_HIDDEN --moe-router-topk 6 --moe-shared-expert-intermediate-size $MOE_SHARED_EXPERT_INTERMEDIATE_SIZE diff --git a/scripts/models/qwen3-235B-A22B.sh b/scripts/models/qwen3-235B-A22B.sh index 32a98679f76..1f663552653 100644 --- a/scripts/models/qwen3-235B-A22B.sh +++ b/scripts/models/qwen3-235B-A22B.sh @@ -39,7 +39,7 @@ MODEL_ARGS=( --moe-router-score-function softmax --moe-token-dispatcher-type alltoall --moe-router-topk 8 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --num-experts 128 --moe-grouped-gemm --moe-token-drop-policy probs diff --git a/scripts/models/qwen3-30B-A3B.sh b/scripts/models/qwen3-30B-A3B.sh index 275c286a1b6..0221af9bba7 100644 --- a/scripts/models/qwen3-30B-A3B.sh +++ b/scripts/models/qwen3-30B-A3B.sh @@ -39,7 +39,7 @@ MODEL_ARGS=( --moe-router-score-function softmax --moe-token-dispatcher-type alltoall --moe-router-topk 8 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --num-experts 128 --moe-grouped-gemm --moe-token-drop-policy probs diff --git a/scripts/models/qwen3-next-80B-A3B.sh b/scripts/models/qwen3-next-80B-A3B.sh index 47c40cab652..e5390854126 100644 --- a/scripts/models/qwen3-next-80B-A3B.sh +++ b/scripts/models/qwen3-next-80B-A3B.sh @@ -43,7 +43,7 @@ MODEL_ARGS=( --moe-router-score-function softmax --moe-token-dispatcher-type alltoall --moe-router-topk 10 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --num-experts 512 --moe-grouped-gemm --moe-token-drop-policy probs diff --git a/scripts/models/qwen3.5-35B-A3B.sh b/scripts/models/qwen3.5-35B-A3B.sh index 4a098c54c6b..e6912b17dd8 100644 --- a/scripts/models/qwen3.5-35B-A3B.sh +++ b/scripts/models/qwen3.5-35B-A3B.sh @@ -43,7 +43,7 @@ MODEL_ARGS=( --moe-router-score-function softmax --moe-token-dispatcher-type alltoall --moe-router-topk 8 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --num-experts 256 --moe-grouped-gemm --moe-token-drop-policy probs diff --git a/scripts/models/qwen3.6-35B-A3B.sh b/scripts/models/qwen3.6-35B-A3B.sh index 0bb9dcc7c73..48d323213a6 100644 --- a/scripts/models/qwen3.6-35B-A3B.sh +++ b/scripts/models/qwen3.6-35B-A3B.sh @@ -43,7 +43,7 @@ MODEL_ARGS=( --moe-router-score-function softmax --moe-token-dispatcher-type alltoall --moe-router-topk 8 - --moe-layer-freq $MOE_LAYER_FREQ + --moe-layer-freq "$MOE_LAYER_FREQ" --num-experts 256 --moe-grouped-gemm --moe-token-drop-policy probs From f672f9d3c15c0ad7277bdb87639bed19e0c10628 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Sun, 10 May 2026 17:48:34 -0700 Subject: [PATCH 0039/1130] [tiny-refac] TITO: inline first-turn fallback in prepare_pretokenized (#1095) --- miles/rollout/session/linear_trajectory.py | 21 +++++-- miles/rollout/session/sessions.py | 12 +--- tests/fast/router/test_linear_trajectory.py | 70 ++++++++++++--------- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/miles/rollout/session/linear_trajectory.py b/miles/rollout/session/linear_trajectory.py index ea1e5ccbbaf..97145187fff 100644 --- a/miles/rollout/session/linear_trajectory.py +++ b/miles/rollout/session/linear_trajectory.py @@ -50,14 +50,24 @@ def prepare_pretokenized( tools: list[dict[str, Any]] | None = None, *, tito_tokenizer: TITOTokenizer, - ) -> dict[str, Any] | None: - """Validate messages, rollback if needed, and compute merged input_ids. + ) -> list[int]: + """Build the full prompt input_ids for *request_messages*. + + On the first turn (no stored token_ids), renders *request_messages* + from scratch via the chat template. On subsequent turns, validates + that *request_messages* extends the stored history (rolling back at + most one assistant step on agent retries) and reuses the stored + token_ids as the pretokenized prefix. - Returns ``None`` on the first turn (no stored token_ids yet). Must be called under ``self.lock``. """ if not self.token_ids: - return None + return tito_tokenizer.render_messages( + request_messages, + tools=tools, + add_generation_prompt=True, + tokenize=True, + ) # 1. Detect agent retries and roll back (at most one assistant step). self._try_detect_and_rollback_to_assistant_checkpoint(request_messages) @@ -70,13 +80,12 @@ def prepare_pretokenized( except ValueError as e: raise MessageValidationError(f"{e}; to allow more roles use --tito-allowed-append-roles") from e - merged = tito_tokenizer.merge_tokens( + return tito_tokenizer.merge_tokens( old_messages=self.messages, new_messages=request_messages, pretokenized_token_ids=self.token_ids, tools=tools, ) - return {"input_ids": merged} def update_pretokenized_state( self, diff --git a/miles/rollout/session/sessions.py b/miles/rollout/session/sessions.py index 858e0424ab6..d9016073746 100644 --- a/miles/rollout/session/sessions.py +++ b/miles/rollout/session/sessions.py @@ -152,21 +152,11 @@ async def chat_completions(request: Request, session_id: str): request_body["no_stop_trim"] = False request_messages = request_body.get("messages", []) - pretokenized = session.prepare_pretokenized( + prompt_token_ids = session.prepare_pretokenized( request_messages, tools=request_body.get("tools"), tito_tokenizer=registry.tito_tokenizer, ) - prompt_token_ids = None - if pretokenized is not None: - prompt_token_ids = pretokenized["input_ids"] - else: - prompt_token_ids = registry.tito_tokenizer.render_messages( - request_messages, - tools=request_body.get("tools"), - add_generation_prompt=True, - tokenize=True, - ) request_body["input_ids"] = prompt_token_ids logger.debug( "Using TITO input_ids: %d tokens", diff --git a/tests/fast/router/test_linear_trajectory.py b/tests/fast/router/test_linear_trajectory.py index b56c643f806..c7f45ef83c2 100644 --- a/tests/fast/router/test_linear_trajectory.py +++ b/tests/fast/router/test_linear_trajectory.py @@ -22,14 +22,28 @@ from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizer +_MOCK_FIRST_TURN_TOKENS = [0] + + class _MockTITOTokenizer(TITOTokenizer): """Stub for unit tests: returns pretokenized_token_ids unchanged (no - incremental tokens) and skips real tokenizer operations. + incremental tokens), renders first-turn prompts as a fixed sentinel, and + skips real tokenizer operations. """ def create_comparator(self): return None + def render_messages( + self, + messages: list[dict[str, Any]], + *, + add_generation_prompt: bool, + tools: list[dict[str, Any]] | None = None, + tokenize: bool = False, + ) -> list[int]: + return list(_MOCK_FIRST_TURN_TOKENS) + def tokenize_additional_non_assistant( self, old_messages: list[dict[str, Any]], @@ -148,13 +162,13 @@ def test_append_record_missing_session(self, registry: SessionRegistry): class TestSingleUserTurnPretokenized: """Test prepare_pretokenized and update_pretokenized_state across turns.""" - def test_first_turn_returns_none(self, registry: SessionRegistry): - """First turn has no prior token_ids, so prepare returns None.""" + def test_first_turn_renders_from_scratch(self, registry: SessionRegistry): + """First turn has no prior token_ids, so prepare renders from scratch.""" sid = registry.create_session() session = registry.get_session(sid) messages = [SYS_MSG, USER_MSG] result = session.prepare_pretokenized(messages, tito_tokenizer=registry.tito_tokenizer) - assert result is None + assert result == _MOCK_FIRST_TURN_TOKENS def test_two_turn_trajectory(self, registry: SessionRegistry): """Full 2-turn: user -> assistant(tool_call) -> tool -> final answer.""" @@ -163,7 +177,10 @@ def test_two_turn_trajectory(self, registry: SessionRegistry): # --- Turn 1: [sys, user] -> assistant with tool_call --- turn1_messages = [SYS_MSG, USER_MSG] - assert session.prepare_pretokenized(turn1_messages, tito_tokenizer=registry.tito_tokenizer) is None + assert ( + session.prepare_pretokenized(turn1_messages, tito_tokenizer=registry.tito_tokenizer) + == _MOCK_FIRST_TURN_TOKENS + ) turn1_prompt_ids = [1, 2, 3, 4, 5] turn1_completion_ids = [10, 11, 12] @@ -177,8 +194,7 @@ def test_two_turn_trajectory(self, registry: SessionRegistry): # --- Turn 2: [sys, user, assistant, tool] -> final answer --- turn2_messages = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(turn2_messages, tito_tokenizer=registry.tito_tokenizer) - assert result is not None - assert result["input_ids"] == [1, 2, 3, 4, 5, 10, 11, 12] + assert result == [1, 2, 3, 4, 5, 10, 11, 12] turn2_prompt_ids = [1, 2, 3, 4, 5, 10, 11, 12, 20, 21] turn2_completion_ids = [30, 31, 32] @@ -201,7 +217,7 @@ def test_three_turn_trajectory(self, registry: SessionRegistry): # Turn 2 t2_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(t2_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result == {"input_ids": [1, 2, 3, 10, 11]} + assert result == [1, 2, 3, 10, 11] session.update_pretokenized_state( t2_msgs, ASSISTANT_MSG_2, [1, 2, 3, 10, 11, 20, 21], [30, 31], max_trim_tokens=0 @@ -210,7 +226,7 @@ def test_three_turn_trajectory(self, registry: SessionRegistry): # Turn 3 t3_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, ASSISTANT_MSG_2, TOOL_MSG_2] result = session.prepare_pretokenized(t3_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result == {"input_ids": [1, 2, 3, 10, 11, 20, 21, 30, 31]} + assert result == [1, 2, 3, 10, 11, 20, 21, 30, 31] session.update_pretokenized_state( t3_msgs, ASSISTANT_MSG_FINAL, [1, 2, 3, 10, 11, 20, 21, 30, 31, 40], [50, 51], max_trim_tokens=0 @@ -257,7 +273,7 @@ def test_no_system_message(self, registry: SessionRegistry): t2_msgs = [USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(t2_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result == {"input_ids": [1, 2, 10]} + assert result == [1, 2, 10] def test_multiple_system_messages_at_start(self, registry: SessionRegistry): """Multiple system messages before the user message are allowed (part of stored prefix).""" @@ -266,15 +282,14 @@ def test_multiple_system_messages_at_start(self, registry: SessionRegistry): extra_sys = {"role": "system", "content": "Extra instructions."} msgs = [SYS_MSG, extra_sys, USER_MSG] result = session.prepare_pretokenized(msgs, tito_tokenizer=registry.tito_tokenizer) - assert result is None # first turn, no prior tokens + assert result == _MOCK_FIRST_TURN_TOKENS # first turn, no prior tokens session.update_pretokenized_state(msgs, ASSISTANT_MSG_1, [1, 2, 3, 4], [10, 11], max_trim_tokens=0) assert session.messages == [SYS_MSG, extra_sys, USER_MSG, ASSISTANT_MSG_1] t2_msgs = [SYS_MSG, extra_sys, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(t2_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result is not None - assert result["input_ids"] == [1, 2, 3, 4, 10, 11] + assert result == [1, 2, 3, 4, 10, 11] # --------------------------------------------------------------------------- @@ -296,7 +311,7 @@ def test_tool_append_allowed(self, registry: SessionRegistry): messages = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(messages, tito_tokenizer=registry.tito_tokenizer) - assert result is not None + assert isinstance(result, list) def test_system_append_rejected(self, registry: SessionRegistry): sid = registry.create_session() @@ -327,7 +342,7 @@ def test_tool_append_allowed(self, registry_with_system: SessionRegistry): messages = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(messages, tito_tokenizer=registry_with_system.tito_tokenizer) - assert result is not None + assert isinstance(result, list) def test_system_append_allowed(self, registry_with_system: SessionRegistry): sid = registry_with_system.create_session() @@ -336,8 +351,7 @@ def test_system_append_allowed(self, registry_with_system: SessionRegistry): messages = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, RETRY_SYS_MSG] result = session.prepare_pretokenized(messages, tito_tokenizer=registry_with_system.tito_tokenizer) - assert result is not None - assert result["input_ids"] == [1, 2, 3, 10, 11] + assert result == [1, 2, 3, 10, 11] def test_system_then_assistant_trajectory(self, registry_with_system: SessionRegistry): """Full trajectory with a retry system message between tool-call turns.""" @@ -349,7 +363,7 @@ def test_system_then_assistant_trajectory(self, registry_with_system: SessionReg t2_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, RETRY_SYS_MSG] result = session.prepare_pretokenized(t2_msgs, tito_tokenizer=registry_with_system.tito_tokenizer) - assert result is not None + assert isinstance(result, list) session.update_pretokenized_state( t2_msgs, ASSISTANT_MSG_2, [1, 2, 3, 10, 11, 20, 21, 22], [30, 31], max_trim_tokens=0 @@ -358,7 +372,7 @@ def test_system_then_assistant_trajectory(self, registry_with_system: SessionReg t3_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, RETRY_SYS_MSG, ASSISTANT_MSG_2, TOOL_MSG_2] result = session.prepare_pretokenized(t3_msgs, tito_tokenizer=registry_with_system.tito_tokenizer) - assert result is not None + assert isinstance(result, list) def test_user_append_rejected(self, registry_with_system: SessionRegistry): sid = registry_with_system.create_session() @@ -380,7 +394,7 @@ def test_tool_append_allowed(self, registry_with_user: SessionRegistry): messages = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(messages, tito_tokenizer=registry_with_user.tito_tokenizer) - assert result is not None + assert isinstance(result, list) def test_user_append_allowed(self, registry_with_user: SessionRegistry): sid = registry_with_user.create_session() @@ -389,7 +403,7 @@ def test_user_append_allowed(self, registry_with_user: SessionRegistry): messages = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, {"role": "user", "content": "follow-up"}] result = session.prepare_pretokenized(messages, tito_tokenizer=registry_with_user.tito_tokenizer) - assert result is not None + assert isinstance(result, list) def test_user_then_assistant_trajectory(self, registry_with_user: SessionRegistry): """Full trajectory: tool → user follow-up → assistant → tool → final.""" @@ -404,7 +418,7 @@ def test_user_then_assistant_trajectory(self, registry_with_user: SessionRegistr follow_up = {"role": "user", "content": "Also check Shanghai."} t2_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, follow_up] result = session.prepare_pretokenized(t2_msgs, tito_tokenizer=registry_with_user.tito_tokenizer) - assert result is not None + assert isinstance(result, list) session.update_pretokenized_state( t2_msgs, ASSISTANT_MSG_2, [1, 2, 3, 10, 11, 20, 21, 22], [30, 31], max_trim_tokens=0 @@ -414,7 +428,7 @@ def test_user_then_assistant_trajectory(self, registry_with_user: SessionRegistr # Turn 3: append tool after the second assistant t3_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, follow_up, ASSISTANT_MSG_2, TOOL_MSG_2] result = session.prepare_pretokenized(t3_msgs, tito_tokenizer=registry_with_user.tito_tokenizer) - assert result is not None + assert isinstance(result, list) def test_system_append_rejected(self, registry_with_user: SessionRegistry): sid = registry_with_user.create_session() @@ -452,7 +466,7 @@ def test_rollback_to_first_assistant(self, registry: SessionRegistry): new_tool = {"role": "tool", "content": '{"temperature": 99}', "tool_call_id": "call_1"} rollback_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, new_tool] result = session.prepare_pretokenized(rollback_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result is not None + assert isinstance(result, list) # State should be rolled back to checkpoint 0 assert session.num_assistant == 1 @@ -518,7 +532,7 @@ def test_rollback_then_continue_full_trajectory(self, registry: SessionRegistry) new_tool = {"role": "tool", "content": '{"retry": true}', "tool_call_id": "call_1"} rollback_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, new_tool] result = session.prepare_pretokenized(rollback_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result is not None + assert isinstance(result, list) # Continue: complete a new turn from the rolled-back state session.update_pretokenized_state( @@ -547,7 +561,7 @@ def test_rollback_fewer_messages_than_stored(self, registry_with_system: Session # Agent retries with only [sys, user, asst1, sys_retry] (4 messages) retry_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, RETRY_SYS_MSG] result = session.prepare_pretokenized(retry_msgs, tito_tokenizer=registry_with_system.tito_tokenizer) - assert result is not None + assert isinstance(result, list) assert session.num_assistant == 1 assert session.messages == [SYS_MSG, USER_MSG, ASSISTANT_MSG_1] @@ -574,7 +588,7 @@ def test_rollback_to_second_assistant(self, registry: SessionRegistry): new_tool = {"role": "tool", "content": '{"alt": 1}', "tool_call_id": "call_2"} rollback_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1, ASSISTANT_MSG_2, new_tool] result = session.prepare_pretokenized(rollback_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result is not None + assert isinstance(result, list) assert session.num_assistant == 2 assert len(session.trajectory_token_ids) == 2 @@ -591,7 +605,7 @@ def test_no_rollback_when_append_only(self, registry: SessionRegistry): # Append tool - not a rollback t2_msgs = [SYS_MSG, USER_MSG, ASSISTANT_MSG_1, TOOL_MSG_1] result = session.prepare_pretokenized(t2_msgs, tito_tokenizer=registry.tito_tokenizer) - assert result is not None + assert isinstance(result, list) # State should NOT have been rolled back assert session.num_assistant == 1 From bce1d21a5c0adbd2cc94dc697c9c9c01ec51af7f Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Mon, 11 May 2026 17:44:54 -0700 Subject: [PATCH 0040/1130] doc [1/n]: cleanup old docs/ and add top-level index.md, faq.md, style.css (#1097) Co-authored-by: Zhichenzzz Co-authored-by: Shi-Dong --- docs/README.md | 31 - docs/_static/css/custom_log.css | 67 -- docs/_static/css/readthedocs.css | 9 - .../image/blogs/release_v0.1.0/cuda_vmm.png | Bin 186743 -> 0 bytes .../image/blogs/release_v0.1.0/overrall.png | Bin 610601 -> 0 bytes docs/_static/image/logo.ico | Bin 270398 -> 0 bytes docs/_static/image/logo.jpg | Bin 50447 -> 0 bytes docs/_static/js/lang-toggle.js | 127 ---- docs/build.sh | 13 - docs/build_all.sh | 44 -- docs/conf.py | 262 -------- .../advanced/arch-support-beyond-megatron.md | 63 -- docs/en/advanced/fault-tolerance.md | 13 - docs/en/advanced/miles-router.md | 93 --- docs/en/advanced/miles_server_args.md | 492 --------------- docs/en/advanced/p2p-weight-transfer.md | 139 ----- docs/en/advanced/pd-disaggregation.md | 5 - docs/en/advanced/speculative-decoding.md | 39 -- docs/en/agentic/oai_endpoint.md | 187 ------ docs/en/developer_guide/debug.md | 51 -- docs/en/developer_guide/migration.md | 50 -- docs/en/examples/deepseek-r1.md | 209 ------- docs/en/examples/glm4-9B.md | 280 --------- docs/en/examples/glm4.5-355B-A32B.md | 190 ------ docs/en/examples/qwen3-30B-A3B.md | 120 ---- docs/en/examples/qwen3-4B.md | 299 --------- docs/en/examples/qwen3-4b-base-openhermes.md | 86 --- docs/en/get_started/customization.md | 442 ------------- docs/en/get_started/gen_endpoint.md | 104 ---- docs/en/get_started/qa.md | 72 --- docs/en/get_started/quick_start.md | 589 ------------------ docs/en/get_started/usage.md | 384 ------------ docs/en/index.rst | 85 --- docs/en/platform_support/amd_tutorial.md | 291 --------- docs/faq.md | 141 +++++ docs/index.md | 100 +++ docs/requirements.txt | 20 - docs/serve.sh | 29 - docs/style.css | 9 + 39 files changed, 250 insertions(+), 4885 deletions(-) delete mode 100644 docs/README.md delete mode 100644 docs/_static/css/custom_log.css delete mode 100644 docs/_static/css/readthedocs.css delete mode 100644 docs/_static/image/blogs/release_v0.1.0/cuda_vmm.png delete mode 100644 docs/_static/image/blogs/release_v0.1.0/overrall.png delete mode 100644 docs/_static/image/logo.ico delete mode 100644 docs/_static/image/logo.jpg delete mode 100644 docs/_static/js/lang-toggle.js delete mode 100755 docs/build.sh delete mode 100755 docs/build_all.sh delete mode 100644 docs/conf.py delete mode 100644 docs/en/advanced/arch-support-beyond-megatron.md delete mode 100644 docs/en/advanced/fault-tolerance.md delete mode 100644 docs/en/advanced/miles-router.md delete mode 100644 docs/en/advanced/miles_server_args.md delete mode 100644 docs/en/advanced/p2p-weight-transfer.md delete mode 100644 docs/en/advanced/pd-disaggregation.md delete mode 100644 docs/en/advanced/speculative-decoding.md delete mode 100644 docs/en/agentic/oai_endpoint.md delete mode 100644 docs/en/developer_guide/debug.md delete mode 100644 docs/en/developer_guide/migration.md delete mode 100644 docs/en/examples/deepseek-r1.md delete mode 100644 docs/en/examples/glm4-9B.md delete mode 100644 docs/en/examples/glm4.5-355B-A32B.md delete mode 100644 docs/en/examples/qwen3-30B-A3B.md delete mode 100644 docs/en/examples/qwen3-4B.md delete mode 100644 docs/en/examples/qwen3-4b-base-openhermes.md delete mode 100644 docs/en/get_started/customization.md delete mode 100644 docs/en/get_started/gen_endpoint.md delete mode 100644 docs/en/get_started/qa.md delete mode 100644 docs/en/get_started/quick_start.md delete mode 100644 docs/en/get_started/usage.md delete mode 100644 docs/en/index.rst delete mode 100644 docs/en/platform_support/amd_tutorial.md create mode 100644 docs/faq.md create mode 100644 docs/index.md delete mode 100644 docs/requirements.txt delete mode 100755 docs/serve.sh create mode 100644 docs/style.css diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 5251aa5ab38..00000000000 --- a/docs/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Miles Documentation - -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 - -### Install Dependency - -```bash -apt-get update && apt-get install -y pandoc parallel retry -pip install -r requirements.txt -``` - -### Update Documentation - -You can update the documentation in the en and zh folders by adding Markdown or Jupyter Notebook files to the appropriate subdirectories. If you create new files, make sure to update index.rst (or any other relevant .rst files) accordingly. - -## Build and Render - -```bash -# build english version -bash ./build.sh en -bash ./serve.sh en - -# build chinese version -bash ./build.sh zh -bash ./serve.sh zh -``` - -You can then visit `http://localhost:8000` to view the documentation. \ No newline at end of file diff --git a/docs/_static/css/custom_log.css b/docs/_static/css/custom_log.css deleted file mode 100644 index 3edb005dfb7..00000000000 --- a/docs/_static/css/custom_log.css +++ /dev/null @@ -1,67 +0,0 @@ -.output_area { - color: #615656; -} - -table.autosummary td { - width: 50% - } - - img.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -.output_area.stderr { - color: #d3d3d3 !important; -} - -.output_area.stdout { - color: #d3d3d3 !important; -} - -div.output_area.stderr { - color: #d3d3d3 !important; -} - -div.output_area.stdout { - color: #d3d3d3 !important; -} - -/* Language toggle button styling */ -.lang-toggle-btn { - --lt-border: var(--pst-color-border, #d0d7de); - --lt-bg: var(--pst-color-surface, #f6f8fa); - --lt-bg-hover: var(--pst-color-on-surface, #e6ebf1); - --lt-active: var(--pst-color-primary, #0969da); - display: inline-flex; - align-items: center; - padding: 2px 10px; - line-height: 1.1; - font-size: 0.72rem; - font-weight: 600; - border: 1px solid var(--lt-border); - border-radius: 6px; - background: var(--lt-bg); - cursor: pointer; - gap: 2px; - letter-spacing: .5px; -} -.lang-toggle-btn:hover { - background: var(--lt-bg-hover); -} -.lang-toggle-btn .lang-seg { - opacity: .55; - transition: opacity .15s; -} -.lang-toggle-btn[data-current="en"] .lang-seg[data-lang="en"], -.lang-toggle-btn[data-current="zh"] .lang-seg[data-lang="zh"] { - opacity: 1; - color: var(--lt-active); -} -.lang-toggle-btn .lang-sep { opacity: .35; } - -@media (prefers-color-scheme: dark) { - .lang-toggle-btn { --lt-border: #30363d; --lt-bg:#161b22; --lt-bg-hover:#1c2128; } - .lang-toggle-btn .lang-seg { color: #adbac7; } -} diff --git a/docs/_static/css/readthedocs.css b/docs/_static/css/readthedocs.css deleted file mode 100644 index aca6649b436..00000000000 --- a/docs/_static/css/readthedocs.css +++ /dev/null @@ -1,9 +0,0 @@ -table.autosummary td { - width: 50% -} - -img.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} diff --git a/docs/_static/image/blogs/release_v0.1.0/cuda_vmm.png b/docs/_static/image/blogs/release_v0.1.0/cuda_vmm.png deleted file mode 100644 index 5bc852b9ed901dbadd6c4156ebad3f6c10d3164e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 186743 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∾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 Kqx0?q;Nz3K}F&6T5 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

4;ILLM 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

ep{pMdNh+*#7wuI^ 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 z�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

kJqbk0|Oz%~o9@i{^x@e?{eDnqg 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%0Aqd=^01-ayhQZA4%5UGRiXnqMSB#*Ge!BA* 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

G$BvWIL3I-Xs5dOVN%{{6vIPeKyR$&NKeS#1 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

^djtgUiLOMKYaiu%}U}6&X-vVHsD2RUu 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Ü+WS__8ioI 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 diff --git a/docs/_static/image/logo.jpg b/docs/_static/image/logo.jpg deleted file mode 100644 index c1257d72b355bbee4172b77c07713bd968deba4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50447 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

cpSL!9V%_chRyaq*#6n{{FDn z|BnvCCBn%|g_ys(_G{Dt+<=9E#Q*xZ32xhIKz<>h_5A?_kN!Fw_%*@*jbMv}`|bQM zuj}9Bi0{hq|HYVJDj58x|8o3DA)Nn2l0}E~_s-uX+t&}t@8PMXLRMd0`yN1<|BY||0@ihD(tLB|KM9ksXa38a@I5a1M?U+W%DNOi{~Oo-3MKzh s3NA7gNDBvg!}n6%7XJ3PmigaV(pO^eN5$ph_m+A_{=E357d_wle insert /miles/zh/ - * / (no lang) -> insert /zh/ - */ - function analyzePath(){ - const rawParts = window.location.pathname.split('/').filter(Boolean); - const parts = rawParts.slice(); - let repoRoot = null; - if(parts.length > 0 && (window.location.host.endsWith('github.io') || parts[0] === 'miles')){ - repoRoot = parts[0]; - } - let zhIndex = -1; - if(parts[0] === 'zh') zhIndex = 0; else if(parts[1] === 'zh') zhIndex = 1; - return { parts, repoRoot, zhIndex }; - } - - function buildTargetUrl(target){ - const url = new URL(window.location.href); - const trailingSlash = url.pathname.endsWith('/') || url.pathname === '/'; - const { parts, repoRoot, zhIndex } = analyzePath(); - if(target === 'zh'){ - if(zhIndex === -1){ - if(repoRoot){ - if(parts.length === 1) parts.push('zh'); else parts.splice(1,0,'zh'); - } else { - parts.unshift('zh'); - } - } - } else { // target en => remove zh if present - if(zhIndex !== -1) parts.splice(zhIndex,1); - } - let newPath = '/' + parts.join('/'); - if(newPath === '/') { - // stay root - } else if(trailingSlash && !/\.[a-zA-Z0-9]+$/.test(parts[parts.length-1] || '')) newPath += '/'; - url.pathname = newPath; - return url.toString(); - } - function createButton(){ - const current = detectCurrent(); - const tgt = otherLang(current); - const btn = document.createElement('button'); - btn.className = 'btn btn-sm lang-toggle-btn'; - btn.type = 'button'; - btn.setAttribute('data-current', current); - btn.title = current === 'en' ? '切换到中文 (当前 EN)' : 'Switch to English (当前 中文)'; - // Show two labels with active highlighted via CSS - btn.innerHTML = ` - EN - / - 中 - `; - btn.addEventListener('click', ()=>{ - const targetUrl = buildTargetUrl(tgt); - try{ localStorage.setItem(STORAGE_KEY, tgt);}catch(e){} - window.location.href = targetUrl; - }); - return btn; - } - function findContainer(){ - // Priority: sidebar end area or header button groups - return document.querySelector( - '.article-header-buttons, .header-article-items__end, .sidebar-header-items, .sidebar-primary-items__end, .bd-header, .bd-sidebar' - ); - } - function idealContainer(){ - return document.querySelector('.article-header-buttons'); - } - function insert(attempt=0){ - // Prefer final header buttons group - let c = idealContainer(); - if(!c) c = findContainer(); - if(!c){ - if(attempt < 40) return setTimeout(()=>insert(attempt+1), 125); - return; - } - // If button exists elsewhere but not inside ideal container, move it - const existing = document.querySelector('.lang-toggle-btn'); - if(existing && c !== existing.parentElement){ - c.appendChild(existing); - return; - } - if(existing) return; // already good - const btn = createButton(); - // Insert near theme switch button if present - const themeBtn = c.querySelector('.theme-switch-button'); - // Insert just before theme switch if found, else at end - if(themeBtn){ - const parent = themeBtn.parentElement; - if(parent === c) c.insertBefore(btn, themeBtn); - else c.appendChild(btn); - } else c.appendChild(btn); - // If current is zh ensure active highlighting reflects zh - btn.setAttribute('data-current', detectCurrent()); - } - document.addEventListener('DOMContentLoaded', ()=>{ - insert(); - // Observe for dynamic header injection - const obs = new MutationObserver(()=>{ insert(); }); - obs.observe(document.body, {childList:true, subtree:true}); - // Stop observing after 5s - setTimeout(()=>obs.disconnect(), 5000); - }); -})(); - -// Minimal styling; can be overridden in custom css -// Light structural styling now moved to CSS file; keep minimal runtime if CSS missing. -(function(){ - if(document.querySelector('style[data-lang-toggle-style]')) return; - const style = document.createElement('style'); - style.setAttribute('data-lang-toggle-style',''); - style.textContent = `.lang-toggle-btn{display:inline-flex;align-items:center;gap:2px}`; - document.head.appendChild(style); -})(); diff --git a/docs/build.sh b/docs/build.sh deleted file mode 100755 index 5bbcbb937af..00000000000 --- a/docs/build.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -LANG=$1 - -# make sure language is only en or zh -if [ "$LANG" != "en" ] && [ "$LANG" != "zh" ]; then - echo "Language must be en or zh" - exit 1 -fi - -cd $SCRIPT_DIR -MILES_DOC_LANG=$LANG sphinx-build -b html -D language=$LANG --conf-dir ./ ./$LANG ./build/$LANG \ No newline at end of file diff --git a/docs/build_all.sh b/docs/build_all.sh deleted file mode 100755 index 32f02013c89..00000000000 --- a/docs/build_all.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd "$SCRIPT_DIR" - -echo "[miles-docs] Building EN..." -./build.sh en -echo "[miles-docs] Building ZH..." -./build.sh zh - -# Create a lightweight root index with auto redirect based on localStorage (done client side) -ROOT_INDEX=build/index.html -cat > "$ROOT_INDEX" <<'EOF' - - - - - miles docs - - - - - -

Miles Documentation

-

Select language:

-
-

Auto-redirect uses your last choice if stored; else pick above.

- - -EOF - -echo "[miles-docs] Done. Root landing page at build/index.html" \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 467d33cc1a6..00000000000 --- a/docs/conf.py +++ /dev/null @@ -1,262 +0,0 @@ -import os -import shutil -import sys -from datetime import datetime -from pathlib import Path - -sys.path.insert(0, os.path.abspath("../..")) - -__version__ = "0.0.1" - -project = "miles" -copyright = f"2025-{datetime.now().year}, miles" -author = "miles Team" - -version = __version__ -release = __version__ - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.napoleon", - "sphinx.ext.viewcode", - "sphinx.ext.autosectionlabel", - "sphinx.ext.intersphinx", - "sphinx_tabs.tabs", - "myst_parser", - "sphinx_copybutton", - "sphinxcontrib.mermaid", - "nbsphinx", - "sphinx.ext.mathjax", -] - -nbsphinx_allow_errors = True -nbsphinx_execute = "never" - -autosectionlabel_prefix_document = True -nbsphinx_allow_directives = True - - -myst_enable_extensions = [ - "dollarmath", - "amsmath", - "deflist", - "colon_fence", - "html_image", - "linkify", - "substitution", -] - -myst_heading_anchors = 3 - -nbsphinx_kernel_name = "python3" -nbsphinx_execute_arguments = [ - "--InlineBackend.figure_formats={'svg', 'pdf'}", - "--InlineBackend.rc={'figure.dpi': 96}", -] - - -nb_render_priority = { - "html": ( - "application/vnd.jupyter.widget-view+json", - "application/javascript", - "text/html", - "image/svg+xml", - "image/png", - "image/jpeg", - "text/markdown", - "text/latex", - "text/plain", - ) -} - -myst_enable_extensions = [ - "dollarmath", - "amsmath", - "deflist", - "colon_fence", - "html_image", - "linkify", - "substitution", -] - -myst_heading_anchors = 3 -myst_ref_domains = ["std", "py"] - -templates_path = ["_templates"] - -source_suffix = { - ".rst": "restructuredtext", - ".md": "markdown", -} - -master_doc = "index" - -language = os.environ.get("MILES_DOC_LANG", "en") - -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -pygments_style = "sphinx" - -html_theme = "sphinx_book_theme" -html_logo = "_static/image/logo.jpg" -html_favicon = "_static/image/logo.ico" -html_title = project -html_copy_source = True -html_last_updated_fmt = "" - -html_theme_options = { - "repository_url": "https://github.com/radixark/miles", - "repository_branch": "main", - "show_navbar_depth": 3, - "max_navbar_depth": 4, - "collapse_navbar": True, - "use_edit_page_button": True, - "use_source_button": True, - "use_issues_button": True, - "use_repository_button": True, - "use_download_button": True, - "use_sidenotes": True, - "show_toc_level": 2, -} - -html_context = { - "display_github": True, - "github_user": "sgl-project", - "github_repo": "sgl-project.github.io", - "github_version": "main", - "conf_py_path": "/docs/", -} - -html_static_path = ["_static"] -html_css_files = ["css/custom_log.css"] -# Add custom javascript for language toggle (en <-> zh) -html_js_files = [ - "js/lang-toggle.js", -] - - -def _sync_examples(app): - """Sync top-level examples into language-specific doc trees. - - Policy: - - README.md -> English docs/en/_examples_synced//README.md - - README_zh.md -> Chinese docs/zh/_examples_synced//README_zh.md - - If a language-specific README missing, that example is simply skipped for that language. - """ - docs_root = Path(__file__).resolve().parent - src_dir = docs_root.parent / "examples" - if not src_dir.exists(): - return - - lang_cfgs = { - "en": { - "dir": docs_root / "en", - "readme_name": "README.md", - }, - "zh": { - "dir": docs_root / "zh", - # primary preferred name; will fallback to README.md - "readme_name": "README_zh.md", - }, - } - - for lang, cfg in lang_cfgs.items(): - lang_dir = cfg["dir"] - if not lang_dir.exists(): - continue - out_dir = lang_dir / "_examples_synced" - if out_dir.exists(): - shutil.rmtree(out_dir) - out_dir.mkdir(parents=True, exist_ok=True) - - entries = [] # (example_name, readme_rel_path) - for d in sorted(src_dir.iterdir()): - if not d.is_dir(): - continue - # language-specific selection with fallback for zh - if lang == "zh": - primary = d / cfg["readme_name"] # README_zh.md - fallback = d / "README.md" - candidate = primary if primary.exists() else fallback - else: - candidate = d / cfg["readme_name"] - if not candidate.exists(): - continue # skip entirely if nothing suitable - target_dir = out_dir / d.name - target_dir.mkdir(parents=True, exist_ok=True) - shutil.copy2(candidate, target_dir / "README.md") - entries.append((d.name, f"_examples_synced/{d.name}/README.md")) - - -def setup(app): - # ensure examples are synced before reading source files - app.connect("builder-inited", _sync_examples) - - -myst_enable_extensions = [ - "dollarmath", - "amsmath", - "deflist", - "colon_fence", -] -myst_heading_anchors = 5 - -htmlhelp_basename = "milesdoc" - -latex_elements = {} - -latex_documents = [ - (master_doc, "miles.tex", "Miles Documentation", "miles Team", "manual"), -] - -man_pages = [(master_doc, "miles", "Miles Documentation", [author], 1)] - -texinfo_documents = [ - ( - master_doc, - "miles", - "Miles Documentation", - author, - "miles", - "One line description of project.", - "Miscellaneous", - ), -] - -epub_title = project - -epub_exclude_files = ["search.html"] - -copybutton_prompt_text = r">>> |\.\.\. " -copybutton_prompt_is_regexp = True - -autodoc_preserve_defaults = True -navigation_with_keys = False - -autodoc_mock_imports = [ - "torch", - "transformers", - "triton", -] - -intersphinx_mapping = { - "python": ("https://docs.python.org/3.12", None), - "typing_extensions": ("https://typing-extensions.readthedocs.io/en/latest", None), - "pillow": ("https://pillow.readthedocs.io/en/stable", None), - "numpy": ("https://numpy.org/doc/stable", None), - "torch": ("https://pytorch.org/docs/stable", None), -} - -html_theme = "sphinx_book_theme" - - -nbsphinx_prolog = """ -.. raw:: html - - -""" diff --git a/docs/en/advanced/arch-support-beyond-megatron.md b/docs/en/advanced/arch-support-beyond-megatron.md deleted file mode 100644 index 4b3e2a02ca6..00000000000 --- a/docs/en/advanced/arch-support-beyond-megatron.md +++ /dev/null @@ -1,63 +0,0 @@ -# Supporting Model Architectures Beyond Megatron-LM - -While the Megatron-LM framework is highly efficient for parallel training, it can lack the flexibility to support rapidly evolving model architectures like Qwen3Next. Natively supporting the unique structures of these models, such as Gated-Delta-Net, often requires invasive and time-consuming modifications to Megatron's core codebase. - -To accelerate the adoption of these cutting-edge models, miles introduces a more agile approach: **instead of deeply re-engineering Megatron, we directly import and wrap the model's official HuggingFace implementation**, embedding it as a "black-box" module into Megatron's parallel training pipeline. - -This document uses Qwen3Next 80B-A3B as an example to illustrate this concept. - -## Principle and Core Components - -Megatron's model instantiation is a two-step process: first, it generates a "layer specification" (`ModuleSpec`) based on the configuration, and then it instantiates the actual PyTorch modules according to that spec. - -miles leverages this mechanism by **hijacking the spec generation stage to replace Megatron's native modules** with an external implementation (in this case, from HuggingFace). This process involves the coordination of three core components: - -1. **Replacing the Megatron Module Spec** - This is the entry point for our solution. We use a custom function (e.g., `get_qwen3_next_spec`) to modify the standard `ModuleSpec`, swapping out Megatron's native Attention layer with our custom wrapper. - * **Implementation**: It retrieves the standard Decoder Block Spec, points its `self_attention` field to our custom module, and enables model-specific configurations like `qk_layernorm` as needed. - * **Corresponding File**: `miles_plugins/models/qwen3_next.py` - -2. **Wrapping the HuggingFace Implementation** - The spec modified in the previous step now points to a wrapper layer, such as `HuggingfaceAttention`. This layer inherits from Megatron's `MegatronModule`. Its core responsibility is to act as a bridge, handling the data alignment required by parallelism strategies (like sequence parallelism), and then internally calling the native `Qwen3NextAttention` module loaded from HuggingFace. - * **Corresponding File**: `miles_plugins/models/hf_attention.py` - -3. **Aligning Model Weights** - Once the model architecture is integrated, we must ensure that the weights can be loaded correctly. We use the [mbridge](https://github.com/ISEEKYAN/mbridge) library, through our `Qwen3NextBridge`, to establish a naming map between the HuggingFace checkpoint and Megatron's parameters, enabling seamless, bidirectional conversion. - * **Corresponding File**: `miles_plugins/mbridge/qwen3_next.py` - -Through the coordination of these three components, we can successfully run a complex model architecture not natively supported by Megatron—using its HuggingFace implementation as the vehicle—on top of Megatron's parallel framework. This is achieved while fully retaining all key capabilities like model parallelism, MoE acceleration, and pipeline scheduling. - -## Mixed-Precision: Preserving fp32 Parameters in bf16 Models - -Some model architectures require specific parameters to remain in fp32 even when the rest of the model runs in bf16. For example, Qwen3.5's `A_log` parameter must stay fp32 — if rounded to bf16, Megatron-side activations diverge from sglang's fp32 `A_log` on the rollout side, causing precision drift. - -Megatron's training stack has **three implicit cast points** that silently round fp32 parameters to bf16: `Float16Module` construction, `Bridge._weight_to_mcore_format`, and `Bridge.load_weights`. Both steps below are required — doing only one leaves a silent precision trap where the final dtype *looks* correct (fp32) but values were already rounded to bf16 precision. - -### Step 1: Mark the parameter in your model definition - -```python -from miles.backends.megatron_utils.fp32_param_utils import mark_param_dtype - -# In your model's __init__: -self.A_log = nn.Parameter(torch.log(A).to(torch.float32)) -mark_param_dtype(self.A_log, torch.float32) -``` - -`enforce_marked_param_dtypes(model)` — already wired into training and checkpoint conversion entry points — restores tagged params to fp32 after `Float16Module` casts the entire model to bf16. - -### Step 2: Override the Bridge to bypass bf16 pre-cast during weight loading - -```python -class Qwen3_5Bridge(Qwen2MoEBridge): - def _weight_to_mcore_format(self, mcore_weights_name, hf_weights): - if mcore_weights_name.endswith("self_attention.linear_attn.A_log"): - assert len(hf_weights) == 1 - return hf_weights[0].to(dtype=torch.float32).contiguous() - return super()._weight_to_mcore_format(mcore_weights_name, hf_weights) -``` - -## Current Limitations - -* This approach does not currently support Tensor Parallelism (TP) within the replaced module itself (e.g., the Attention layer in this case). -* **Impact**: In most large-scale MoE models, the parameter count of the Attention layer is relatively small, so this limitation typically has a minimal effect on memory footprint and training throughput. -* **Alternative**: If TP for the module is critical, the only alternative is to revert to the more invasive approach of modifying Megatron's native implementation. diff --git a/docs/en/advanced/fault-tolerance.md b/docs/en/advanced/fault-tolerance.md deleted file mode 100644 index 31ac2aa7090..00000000000 --- a/docs/en/advanced/fault-tolerance.md +++ /dev/null @@ -1,13 +0,0 @@ -# Fault Tolerance - -To ensure long-term, stable RL training, miles enables a certain level of fault tolerance by default. This section introduces the design philosophy behind fault tolerance in miles. - -To enable the fault tolerance function in miles, please set `--use-fault-tolerance`. - -## Rollout Fault Tolerance - -During the rollout process, miles periodically sends heartbeat requests (`/health_generate`) to all SGLang servers. If a heartbeat times out, that SGLang server will be stopped. After the current rollout round is complete, the server will be restarted and its parameters will be correctly updated. - -- `--rollout-health-check-first-wait`: Since some large MoE models require compilation on their first run, miles will wait for `rollout_health_check_first_wait` seconds before the first rollout to start sending heartbeats. Defaults to 300s. -- `--rollout-health-check-interval`: The interval between heartbeat checks. Defaults to 10s. -- `--rollout-health-check-timeout`: The timeout limit for a heartbeat request. Defaults to 5s. diff --git a/docs/en/advanced/miles-router.md b/docs/en/advanced/miles-router.md deleted file mode 100644 index 1eeb42b659b..00000000000 --- a/docs/en/advanced/miles-router.md +++ /dev/null @@ -1,93 +0,0 @@ -# Miles Router - -miles includes an optional Miles Router used during rollout / data generation. It is a lightweight HTTP router/proxy that sits in front of one or more SGLang worker servers and adds training-oriented capabilities that are not the main goal of serving-focused routers. - ---- - -## 1. What is Miles Router? - -Miles Router is a small FastAPI service that: - -- Registers workers (SGLang HTTP servers) into a local pool -- Routes requests to a selected worker (simple least-inflight load balancing) -- Proxies arbitrary paths to the selected worker (e.g. `/generate`) -- Runs periodic health checks and quarantines unhealthy workers -- Supports middleware plugins (via `--miles-router-middleware-paths`) to implement rollout-specific processing (e.g. caching, request/response transforms) - -In miles's architecture, the router is part of the rollout system ("SGLang + router") that generates samples and pushes them into the data buffer. - -### How it is launched - -In distributed training, miles will start a router automatically when `--sglang-router-ip` is not provided: - -- If `--use-miles-router` is set, miles starts Miles Router -- Otherwise, miles starts SGLang Model Gateway - ---- - -## 2. Why we need Miles Router - -Unlike production inference, RL rollout needs to capture additional metadata for training: token-level logprobs, loss masks, and (for MoE models) expert routing decisions. Miles Router provides these capabilities through its middleware system and passthrough proxy design. - -### 2.1 Radix-tree cache (transparent token management) - -> Use this when your rollout pipeline is text-in/text-out and you cannot reliably persist token IDs; if you already control token-in/token-out (e.g. search r1, multiturn VLM examples), you likely don't need the radix-tree cache. - -Text-in text-out interfaces can cause token retokenization mismatches - re-tokenizing text at training time may produce different token sequences than rollout, breaking per-token alignment needed for PPO/GRPO losses. - -The radix-tree cache solves this transparently: it intercepts text-based requests, tokenizes them, and stores trajectories (text, token IDs, logprobs, loss masks) keyed by the text prefix. After rollout finishes, calling `/retrieve_from_text` returns the exact token sequence with aligned metadata, without requiring any changes to your rollout code. - -Implementation-wise, the radix-tree cache: - -- Accepts text plus tokens/metadata and stores them in a radix tree -- Uses longest-prefix matching to reuse cached token sequences (enabling token-in/token-out downstream) -- Allows insertion of new text continuations as rollout proceeds (multiple trajectories per prompt, e.g. GRPO) -- Periodically cleans up stale nodes to control memory usage - -Use the radix cache when you have text-based rollout code and want token-level precision without rewriting, or when running GRPO with multiple trajectories sharing the same prompt prefix. - -### 2.2 Rollout routing replay (R3) for MoE - -For MoE models, miles supports rollout routing replay (R3): record expert routing decisions during rollout and replay them during training to improve stability. - -#### SGLang side - -SGLang provides expert routing capture via: - -- `--enable-return-routed-experts`: server argument to enable routing capture -- `RoutedExpertsCapturer`: captures `topk_ids` (selected expert IDs) at each MoE layer during forward pass -- `return_routed_experts`: request parameter to retrieve routing data -- Returns `routed_experts` in response `meta_info` - a `[seq_len - 1, num_layers, top_k]` tensor of expert IDs - -#### miles side - -miles consumes the routing data and replays it during training: - -- `--use-miles-router --use-rollout-routing-replay`: both flags required to enable R3 -- Rollout sends `return_routed_experts=True` and stores results in `sample.rollout_routed_experts` -- Training calls `fill_routing_replay()` to load routing data into `RoutingReplay` objects -- During forward pass, recorded routing decisions are replayed instead of recomputed - -#### Why Miles Router is needed - -We need Miles Router because the SGLang worker returns routed experts in the response (`meta_info.routed_experts`) when the request sets `return_routed_experts=true`, and Miles Router preserves this field end-to-end. SGLang Model Gateway may drop this extra metadata when it reconstructs responses with a fixed schema (see section 3). - ---- - -## 3. Differences vs SGLang Model Gateway - -Miles Router and SGLang Model Gateway can both route requests to workers, but they are optimized for different goals. - -### Key differences - -Miles Router is a lightweight Python/FastAPI proxy that acts as a passthrough to SGLang workers. This passthrough design enables RL-specific features like radix-tree trajectory caching and R3 (which require preserving raw response metadata like `routed_experts`). - -SGLang Model Gateway is a high-performance Rust-based router optimized for large-scale inference: async non-blocking routing, advanced fault tolerance (retries, circuit breakers), multiple load balancing policies (including cache-aware routing), and PD disaggregation support. However, it reconstructs responses with a fixed schema, so it does not preserve the metadata needed for miles's R3 flow. - -For more details on SGLang Model Gateway, see the [official documentation](https://docs.sglang.io/advanced_features/sgl_model_gateway.html). - -### When to use which - -- Use Miles Router when you need R3 or radix-tree caching -- Use SGLang Model Gateway for everything else (recommended default) - diff --git a/docs/en/advanced/miles_server_args.md b/docs/en/advanced/miles_server_args.md deleted file mode 100644 index e8d70db59fc..00000000000 --- a/docs/en/advanced/miles_server_args.md +++ /dev/null @@ -1,492 +0,0 @@ -# Miles Server Arguments - -This document provides a detailed list of command-line arguments used to configure Miles for RL training and inference. These arguments enable precise control over cluster resources, training backends (Megatron/FSDP), inference optimization via SGLang, and RL algorithmic hyperparameters. - -You can find all arguments by running: -```bash -python3 train.py --help -``` - -Note that this document is based on commit `a93d484` and was last updated on 02/09/2026. We try our best to ensure the quality and accuracy of these documents. Even so, it's hard to accurately describe all the hundreds of parameters' effect on such complex RL scenarios. This doc is for reference and may contain some tiny errors. - -## Argument Sources - -Miles acts as an orchestrator that integrates multiple frameworks. To help identify where an argument is directed, we follow these prefix conventions: - -* **`--sglang-*`**: Arguments passed directly to the **SGLang** rollout. -* **`--router-*`**: Arguments directed to the **SGLang Model Gateway/Router**. -* **No Prefix**: Default arguments corresponding to **Megatron-LM** (when using the Megatron backend) or **Miles native** configuration. -* **`--fsdp-*`**: Specific arguments for the experimental **FSDP** backend. - -**Note** that Arguments labeled as **Megatron-LM (Reset by Miles)** are native Megatron-LM parameters where Miles has modified the default value or behavior to better suit RL training workflows. - -## Table of Contents - -1. [Cluster and Resource Management](#cluster-and-resource-management) -2. [Training Backend](#training-backend) -3. [Rollout Management](#rollout-management) -4. [Sampling and Filtering](#sampling-and-filtering) -5. [Data Arguments](#data-arguments) -6. [Evaluation Arguments](#evaluation-arguments) -7. [Checkpointing and Resuming](#checkpointing-and-resuming) -8. [Algorithm and RL Arguments](#algorithm-and-rl-arguments) -9. [Logging and Monitoring](#logging-and-monitoring) -10. [Fault Tolerance](#fault-tolerance) -11. [Miles Router](#miles-router) -12. [Reward Model Arguments](#reward-model-arguments) -13. [Rollout Buffer Management](#rollout-buffer-management) -14. [Multi-Token Prediction (MTP) Arguments](#multi-token-prediction-mtp-arguments) -15. [SGLang Backend Arguments](#sglang-backend-arguments) -16. [Megatron Specific Arguments](#megatron-specific-arguments) -17. [FSDP Specific Arguments](#fsdp-specific-arguments) -18. [Debug and Profiling](#debug-and-profiling) -19. [Environment Variables](#environment-variables) -20. [Multi-Turn and Agentic Arguments](#multi-turn-and-agentic-arguments) -21. [Advanced Developer Hooks and CI](#advanced-developer-hooks-and-ci) -22. [Miscellaneous and System](#miscellaneous-and-system) - -## Cluster and Resource Management - -Arguments for configuring Ray cluster resources and GPU allocation. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--actor-num-nodes` | Number of nodes for training the Actor. | `1` | Type: int | Miles Native | -| `--actor-num-gpus-per-node` | Number of GPUs per node for training the Actor. | `8` | Type: int | Miles Native | -| `--critic-num-nodes` | Number of nodes for the Critic. Defaults to `--actor-num-nodes`. | `None` | Type: int | Miles Native | -| `--critic-num-gpus-per-node` | Number of GPUs per node for the Critic. Defaults to `--actor-num-gpus-per-node`. | `None` | Type: int | Miles Native | -| `--rollout-num-gpus` | Total number of GPUs required for rollout (inference). In `--colocate` mode, this is ignored and set to `actor-num-gpus-per-node * actor-num-nodes` (plus critic GPUs if enabled). | `None` | Type: int | Miles Native | -| `--rollout-num-gpus-per-engine` | Number of GPUs per inference engine, same as `tp_size` in SGLang. For multi-node serving, this should be the total GPU count / `tp_size` for each SGLang instance. | `1` | Type: int | Miles Native | -| `--num-gpus-per-node` | Total GPUs per node on the physical machine. This informs the Ray scheduler of the hardware capacity. In **Colocate mode**, it is required that the machine has fewer than 8 GPUs to calculate correct VRAM offsets. In **Disaggregated mode**, it ensures SGLang engines are distributed correctly across nodes without exceeding per-node GPU limits. | `8` | Type: int | Miles Native | -| `--colocate` | Deploy training and rollout on the same GPUs. This mode automatically enables `--offload-train` and `--offload-rollout` to facilitate weight-swapping between the training actor and inference engine. **Note:** The offload parameters are currently only used for AMD GPUs and will be removed soon. **Memory Tip:** When colocating, it is highly recommended to set `--sglang-mem-fraction-static` to **0.8** (especially on **NVIDIA Blackwell B200/B300** GPUs). This leaves sufficient VRAM (~20%) for Megatron to initialize its structures before the first weight offload to CPU occurs. On GB200/GB300, values up to 0.75 are safer for long-running jobs to prevent potential OOMs. #TODO: Verify optimal fraction for Blackwell in production | `False` | bool flag (set to enable) | Miles Native | -| `--prefill-num-servers` | Number of dedicated prefill servers for PD disaggregation. | `None` | Type: int | Miles Native | -| `--distributed-backend` | Backend for distributed communication. | `nccl` | `nccl`, `gloo` | Megatron-LM (Reset by Miles) | -| `--distributed-timeout-minutes` | Timeout for distributed operations in minutes. | `10` | Type: int | Megatron-LM (Reset by Miles) | - -Note that most use cases do not need to consider offload parameters, including `--offload-rollout, --no-offload-rollout, --offload-train, --no-offload-train`. - -## Training Backend - -Arguments for configuring the training engine (Megatron or FSDP). - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--train-backend` | The backend for training. Highly suggest Megatron for numerical stability and efficiency. | `"megatron"` | `megatron`, `fsdp` | Miles Native | -| `--qkv-format` | Whether to pack variable-length sequences into the token dimension for training. `thd` (T-H-D, a.k.a. varlen / packed sequence) concatenates sequences and uses `cu_seqlens` to avoid padding; it is the default and is usually faster by reducing padding overhead. `bshd` (B-S-H-D) uses fixed-shape padded batches; use it for newer models with novel attention architectures (e.g., sparse attention, attention sink) where the training backend does not support `thd`. | `"thd"` | `thd`, `bshd` | Miles Native | -| `--optimizer` | Optimizer type. | `adam` | `adam`, `sgd` | Megatron-LM & FSDP | -| `--lr` | Learning rate for the Actor. | `1e-6` | Type: float | Megatron-LM (Reset by Miles) & FSDP | -| `--lr-warmup-init` | Initial learning rate for warmup. | `0.0` | Type: float | Megatron-LM & FSDP | -| `--min-lr` | Minimum learning rate after decay. | `0.0` | Type: float | Megatron-LM & FSDP | -| `--lr-decay-style` | Learning rate decay style. | `constant`(FSDP), `linear`(Megatron) | Type: str | Megatron-LM & FSDP | -| `--lr-warmup-iters` | Number of iterations for warmup. | `0` | Type: int | Megatron-LM & FSDP | -| `--lr-decay-iters` | Number of iterations for learning rate decay. | `None` | Type: int | Megatron-LM & FSDP | -| `--lr-warmup-fraction` | Fraction of total steps to warmup. | `None` | Type: float | Megatron-LM & FSDP | -| `--adam-beta1` | Beta1 for Adam optimizer. | `0.9` | Type: float | Megatron-LM & FSDP | -| `--adam-beta2` | Beta2 for Adam optimizer. | `0.95` | Type: float | Megatron-LM & FSDP | -| `--adam-eps` | Epsilon for Adam optimizer. | `1e-8` | Type: float | Megatron-LM & FSDP | -| `--true-on-policy-mode` | Strictly align SGLang's log probs and training engine's log probs to bit-wise equal. This parameter is only used for FSDP right now. [Ref](https://github.com/zhaochenyang20/Awesome-ML-SYS-Tutorial/blob/main/rlhf/slime/mismatch/blog-en.md#truly-on-policy-training) | `False` | bool flag (set to enable) | Miles Native | -| `--train-env-vars` | Extra environment variables for training process, e.g., PyTorch memory management ones. | `{}` | Type: JSON / Dict | Miles Native | -| `--train-memory-margin-bytes` | Reserved memory margin for training in bytes. Defaults to 1GB. | `1073741824` | Type: int | Miles Native | -| `--disable-weights-backuper` | Applies to `megatron` training backend only. Disables the system that backs up model weights (Actor, Ref, Old Actor) to CPU RAM. Disabling saves significant host memory but prevents features that rely on weight-swapping, such as computing the KL-divergence against a reference model. **Note**: do not set `--ref-load` and `--keep-old-actor` if disable weights backuper. | `False` | bool flag (set to disable) | Miles Native | -| `--custom-model-provider-path` | Path to a custom function that replaces the default model provider. [Ref](../get_started/customization.md#20-model-provider---custom-model-provider-path) | `None` | Type: str | Miles Native | -| `--recompute-loss-function` | Enable recomputing the loss function to save memory during training. | `False` | bool flag (set to enable) | Miles Native | -| `--log-probs-chunk-size` | Specifies the chunk size for logprobs computation to reduce peak memory usage. Processing logits in smaller batches, it prevents CUDA OOM errors during long-context prefilling or re-computation. Set to `-1` to disable chunking. [Ref](https://github.com/sgl-project/sglang/pull/6318) | `-1` | Type: int | Miles Native | -| `--keep-old-actor` | Maintains a "Model Queue" (Actor, Rollout Actor, Old Actor) to ensure importance sampling ratios are calculated against the exact policy version that generated the data. Essential for asynchronous RL where training and inference are decoupled, preventing mathematical incorrectness due to model staleness. It consumes additional Host Memory (extra ~1x model size for `update_weights_interval > 1` or 2x for `update_weights_interval == 1`) depending on update interval. | `False` | bool flag (set to enable) | Miles Native | -| `--update-weight-buffer-size` | Buffer size for updating weights, in bytes. [Ref](https://hebiao064.github.io/rl-weight-sync#42-optimizing-sglang-server-calls-with-tensor-bucketing-from-50s-to-30s) | `536870912` | Type: int | Miles Native | -| `--update-weights-interval` | Interval (in rollout rounds) for syncing weights to inference engines. Set to `>1` for async RL. | `1` | Type: int | Miles Native | -| `--fp16` | Enable FP16 mixed precision. | `False` | bool flag (set to enable) | Megatron-LM & FSDP | -| `--context-parallel-size` | Size of context parallelism. | `1` | Type: int | Megatron-LM & FSDP | -| `--deterministic-mode` | Enable deterministic mode for reproducibility. [Ref](https://lmsys.org/blog/2025-09-22-sglang-deterministic/) | `False` | bool flag (set to enable) | Megatron-LM & FSDP | - -## Rollout Management - -Arguments for configuring the rollout (inference) process and custom rollout logic. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--hf-checkpoint` | Path to the HuggingFace checkpoint used to initialize SGLang and provide the tokenizer. | `None` | Type: str | Miles Native | -| `--model-name` | The name of the model that is used to convert the Megatron weights into HuggingFace format. If not set, we will use `type(AutoConfig.from_pretrained(args.hf_checkpoint)).__name__.lower()` as `model_name`. Providing this argument can also help in cases where transformers cannot find certain models. | `None` | Type: str | Miles Native | -| `--rollout-function-path` | Path to the rollout generation function. Use this to inject custom logic (e.g., for multi-turn or tool use). [Ref](../get_started/customization.md#1-rollout-function---rollout-function-path) | `miles.rollout.sglang_rollout.generate_rollout` (or `miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn` when `MILES_EXPERIMENTAL_ROLLOUT_REFACTOR=1`) | Type: str | Miles Native | -| `--rollout-temperature` | Sampling temperature for the inference engine during rollout. | `1.0` | Type: float | Miles Native | -| `--rollout-top-p` | Top-p (nucleus) sampling threshold during rollout. | `1.0` | Type: float | Miles Native | -| `--rollout-top-k` | Top-k sampling threshold during rollout. `-1` means disabled. | `-1` | Type: int | Miles Native | -| `--rollout-max-context-len` | The maximum context size for the inference engine during rollout. It should not exceed the `max_position_embeddings` in the HuggingFace model's `config.json`. **Note:** This acts as a hard cap for the total tokens (Prompt + Response). | `None` | Type: int | Miles Native | -| `--rollout-max-prompt-len` | Maximum length of the prompt. Longer prompts are filtered during dataset initialization. This is not recommended if the dataset is large. **Note:** Defaults to `rollout-max-context-len - 1` if not set, ensuring at least one token can be generated. | `None` | Type: int | Miles Native | -| `--rollout-max-response-len` | Maximum length of the response (`max_tokens` in SGLang). **Note:** Generation will stop when either this limit is reached or the total session length hits `rollout-max-context-len`. | `None` | Type: int | Miles Native | -| `--rollout-skip-special-tokens` | Skip special tokens (e.g., `<\|im_end\|>`, `<\|endoftext\|>`) in the decoded response string. **Critical for Multi-Turn RL:** Ensures that when a response is appended to the conversation history for the next turn, it doesn't include terminal special tokens that would interfere with chat template formatting or cause early termination in subsequent turns. | `False` | bool flag (set to enable) | Miles Native | -| `--rollout-stop` | A list of strings that trigger termination of generation if they appear in the output (e.g., `"\nUser:"`). | `None` | Type: List[str] | Miles Native | -| `--rollout-stop-token-ids` | A list of numerical token IDs that trigger termination. This is the token-level equivalent of `--rollout-stop` and is preferred for special control tokens that are difficult to input as strings. | `None` | Type: List[int] | Miles Native | -| `--rollout-shuffle` | Shuffle the prompts during rollout. | `False` | bool flag (set to enable) | Miles Native | -| `--rollout-seed` | Seed for the random number generator during rollout (used for shuffling and sampling). | `42` | Type: int | Miles Native | -| `--rollout-external` | Use external SGLang instances instead of launching them inside the framework. | `False` | bool flag (set to enable) | Miles Native | -| `--rollout-external-engine-addrs` | Addresses and ports of the external engines. | `None` | Type: List[str] | Miles Native | -| `--custom-generate-function-path` | Path to override only the `generate` step within the default rollout function. If your custom `generate` returns `list[Sample]` (multi-sample), make sure your rollout pipeline can handle it; the default rollout expects a flat `list[Sample]` of length `--n-samples-per-prompt` for each prompt group. [Ref](../get_started/customization.md#2-custom-generate-function---custom-generate-function-path) | `None` | Type: str | Miles Native | -| `--custom-rollout-log-function-path` | Path to a custom function for logging training rollout data. [Ref](../get_started/customization.md#14-logging-functions) | `None` | Type: str | Miles Native | -| `--custom-eval-rollout-log-function-path` | Path to a custom function for logging evaluation rollout data. [Ref](../get_started/customization.md#14-logging-functions) | `None` | Type: str | Miles Native | -| `--rollout-data-postprocess-path` | Path to a function called after all rollout data (including log probs) is ready. [Ref](../get_started/customization.md#8-rollout-data-postprocess---rollout-data-postprocess-path) | `None` | Type: str | Miles Native | - -## Sampling and Filtering - -Arguments for sampling strategies and data filtering during rollout and buffer management. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--over-sampling-batch-size` | Number of prompts requested in each **oversampling** round when **dynamic sampling** is enabled. Miles samples `over_sampling_batch_size` prompts, generates `--n-samples-per-prompt` responses per prompt asynchronously, and then keeps/discards each prompt group via `--dynamic-sampling-filter-path`. If filtering is strict and the remaining accepted batch size drops below the target `--rollout-batch-size`, Miles automatically triggers another oversampling round of the same size. If unset, defaults to `--rollout-batch-size`. See [Dynamic Sampling](../get_started/quick_start.md#dynamic-sampling). | `None` | Type: int | Miles Native | -| `--dynamic-sampling-filter-path` | Path to the filter function for dynamic sampling. [Ref](../get_started/customization.md#4-dynamic-sampling-filter---dynamic-sampling-filter-path) | `None` | Type: str | Miles Native | -| `--partial-rollout` | Enable partial rollout for **dynamic sampling**: cache partially generated (aborted/unfinished) samples and resume generation in later rollout steps, reducing wasted compute for long responses. Cached samples are stored in the rollout buffer and can be prioritized/selected via `--buffer-filter-path` (default FIFO behavior). See [Partial Rollout](../get_started/quick_start.md#partial-rollout). | `False` | bool flag (set to enable) | Miles Native | -| `--mask-offpolicy-in-partial-rollout` | When using partial rollout, mask the previously generated (cached) response tokens so they do not contribute to the loss; only tokens generated after resuming are used for training. This helps avoid training on a cached prefix produced by an older policy version. See [Partial Rollout](../get_started/quick_start.md#partial-rollout). | `False` | bool flag (set to enable) | Miles Native | -| `--buffer-filter-path` | Path to the function to filter or sort samples in the rollout buffer before training. [Ref](../get_started/customization.md#5-buffer-filter---buffer-filter-path) | `None` | Type: str | Miles Native | -| `--rollout-sample-filter-path` | Path to the function that marks individual samples to be excluded from loss calculation. [Ref](../get_started/customization.md#6-rollout-sample-filter---rollout-sample-filter-path) | `None` | Type: str | Miles Native | -| `--rollout-all-samples-process-path` | Path to the function to process all samples (including filtered ones) after rollout. [Ref](../get_started/customization.md#7-rollout-all-samples-process---rollout-all-samples-process-path) | `None` | Type: str | Miles Native | - -## Data Arguments - -Arguments for dataset configuration, prompt mapping, and training batch sizes. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--prompt-data` | Path to the prompt dataset (JSONL format), and each line should contain `--input-key` and `--label-key`, which will be used as the prompt and the label, respectively. | `None` | Type: str | Miles Native | -| `--disable-rollout-global-dataset` | Disable the global dataset for rollout. By default, Miles loads `--prompt-data` into a global dataset and samples from it for rollout. Setting this flag turns off this behavior. Use this flag only when providing a custom `--rollout-function-path` (and usually a custom `--data-source-path`) that handles data loading independently. | `False` | bool flag (set to disable) | Miles Native | -| `--data-source-path` | Path to a custom Python class for the rollout data source. [Ref](../get_started/customization.md#15-data-source---data-source-path) | `miles.rollout.data_source.RolloutDataSourceWithBuffer` | Type: str | Miles Native | -| `--input-key` | Key in the JSONL data representing the user input/prompt. | `"input"` | Type: str | Miles Native | -| `--label-key` | Key in the JSONL data representing the label/ground truth. | `None` | Type: str | Miles Native | -| `--metadata-key` | When adding tools during `apply_chat_template`, provide the key for the tools to the prompt dataset. | `"metadata"` | Type: str | Miles Native | -| `--multimodal-keys` | JSON string for multimodal data mapping media types to data keys. Example: `'{"image": "image_file"}'` | `None` | Type: str | Miles Native | -| `--tool-key` | JSON key for tool definitions in the prompt dataset (used when applying chat templates). | `"tools"` | Type: str | Miles Native | -| `--apply-chat-template` | Whether to apply the chat template to the input prompt. The input should be the same structure as an OpenAI message, e.g., `[{'role': 'user', 'content': 'blabla'}]`. | `False` | bool flag (set to enable) | Miles Native | -| `--apply-chat-template-kwargs` | Extra arguments for the chat template processing (JSON string). | `"{}"` | Type: str | Miles Native | -| `--num-rollout` | Number of rollout steps. If not set, Miles will calculate the number of rollout steps from the dataset size. **Note:** This value will be overwritten if `--num-epoch` is also set. | `None` | Type: int | Miles Native | -| `--num-epoch` | Number of epochs for the training. If set, `num_rollout` is calculated as `(num_epoch * dataset_size) // rollout_batch_size`. **Note:** This argument takes precedence and will overwrite `--num-rollout` if both are specified. | `None` | Type: int | Miles Native | -| `--rollout-batch-size` | Number of prompts per rollout batch. The total data returned should be `rollout_batch_size` * `n_samples_per_prompt`. | Required | Type: int | Miles Native | -| `--n-samples-per-prompt` | Number of responses to generate for each prompt, e.g., the group size of GRPO. The default rollout pipeline expects each prompt group to contain exactly `n_samples_per_prompt` samples. | `1` | Type: int | Miles Native | -| `--global-batch-size` | Total samples per optimizer step. Automatically calculated or **overridden** if `num_steps_per_rollout` is set. | `None` | Type: int | Megatron-LM (Reset by Miles) | -| `--num-steps-per-rollout` | The number of training steps to perform using the data collected in a single rollout round. Setting this to `n` means the policy model will be updated `n` times using the same batch of rollout data. Miles ensures that `(rollout-batch-size * n-samples-per-prompt) = (global-batch-size * num-steps-per-rollout)`. If this value is not provided, you have to set `--global-batch-size` explicitly. If both are provided, `--num-steps-per-rollout` will **override** the global batch size with `num_steps_per_rollout = (rollout_batch_size * n_samples_per_prompt) // num_steps_per_rollout`. | `None` | Type: int | Miles Native | -| `--use-dynamic-batch-size` | Dynamically packs variable-length samples into micro-batches to maximize GPU utilization, ensuring the total token count per batch does not exceed `--max-tokens-per-gpu`. For example, with a 300-token limit, samples of lengths 100, 200, and 300 would be packed into two batches: `[100, 200]` and `[300]`. **Note:** Miles ensures that enabling this optimization does not affect the mathematical correctness of per-sample or per-token loss calculation. It is **strongly recommended** to enable this for maximum efficiency. **Compatibility:** only supported when `--qkv-format` is `thd` (does not work for `bshd`). | `False` | bool flag (set to enable) | Miles Native | -| `--max-tokens-per-gpu` | The maximum number of tokens (Prompt + Response combined) per GPU for dynamic batch size. This parameter defines the total sequence length budget for packing samples into micro-batches during training. Note that when enabling context parallel (CP), the effective capacity is shared, so the value should be approximately `(Total_Sequence_Length) // cp_size`. | `None` | Type: int | Miles Native | -| `--log-probs-max-tokens-per-gpu` | The maximum number of tokens per GPU for calculating log probs. This is used to calculate the log probs of the responses during rollout, and should be set to a larger value than `max_tokens_per_gpu` if you want better performance. | `None` | Type: int | Miles Native | -| `--balance-data` | Repartition each rollout batch so each data-parallel rank gets a similar total token count via the Karmarkar-Karp method. It may be beneficial for training speed, but changes per-rank sample grouping and adds a small CPU scheduling overhead. | `False` | bool flag (set to enable) | Miles Native | -| `--data-pad-size-multiplier` | Multiplier used to calculate the sequence padding boundary. Miles rounds sequence lengths up to a multiple of `tensor_parallel_size * data_pad_size_multiplier`. This optimization ensures that matrix dimensions are aligned with NVIDIA Tensor Core requirements, maximizing throughput and reducing VRAM fragmentation. **Note:** better not change this; values `<128` may trigger accuracy loss under `--qkv-format thd` when `TP>=4`. | `128` | Type: int | Miles Native | -| `--micro-batch-size` | Micro batch size per GPU. Ignored when `--use-dynamic-batch-size` is enabled. Works for both `--qkv-format thd` and `--qkv-format bshd` (and is required for `bshd` because dynamic batch size is unsupported). | `1` | Type: int | Megatron-LM (Reset by Miles) | - -## Evaluation Arguments - -Arguments for configuring the evaluation process during training. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--eval-interval` | Interval (in rollout steps) between evaluations. | `None` | Type: int | Megatron-LM (Reset by Miles) | -| `--eval-prompt-data` | List of name and path pairs for evaluation datasets (e.g., `aime /path/to/aime.jsonl`). | `None` | Type: List[str] | Miles Native | -| `--eval-config` | Path to an OmegaConf YAML/JSON file describing evaluation datasets (overrides `--eval-prompt-data`). | `None` | Type: str | Miles Native | -| `--skip-eval-before-train` | Skip the evaluation step before training starts. | `False` | bool flag (set to enable) | Miles Native | -| `--n-samples-per-eval-prompt` | Number of responses for each prompt in generation. | `1` | Type: int | Miles Native | -| `--eval-temperature` | Temperature for evaluation (defaults to rollout temperature if not set). | `None` | Type: float | Miles Native | -| `--eval-top-p` | Top-p sampling threshold for evaluation (defaults to rollout top-p if not set). | `None` | Type: float | Miles Native | -| `--eval-top-k` | Top-k sampling threshold for evaluation (defaults to rollout top-k if not set). | `None` | Type: int | Miles Native | -| `--eval-max-response-len` | Maximum response length for evaluation (defaults to rollout max response length if not set). | `None` | Type: int | Miles Native | -| `--eval-max-prompt-len` | Maximum prompt length for evaluation. | `None` | Type: int | Miles Native | -| `--eval-min-new-tokens` | Minimum tokens to generate for evaluation responses (Not used). | `None` | Type: int | Miles Native | -| `--eval-max-context-len` | Maximum context length for evaluation (defaults to rollout max context length if not set). | `None` | Type: int | Miles Native | -| `--eval-function-path` | Path to a custom evaluation function. [Ref](../get_started/customization.md#16-evaluation-function---eval-function-path) | `None` | Type: str | Miles Native | -| `--eval-input-key` | JSON key for input text in evaluation datasets. | `None` | Type: str | Miles Native | -| `--eval-label-key` | JSON key for ground truth labels in evaluation datasets. | `None` | Type: str | Miles Native | -| `--eval-tool-key` | JSON key for tool definitions in evaluation datasets. | `None` | Type: str | Miles Native | - -## Checkpointing and Resuming - -Arguments for saving and loading model states. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--load` | Path to the training model checkpoint to load. | `None` | Type: str | Megatron-LM (Reset by Miles) | -| `--save` | Path to save checkpoints. | `None` | Type: str | Megatron-LM (Reset by Miles) | -| `--save-interval` | Interval (in rollout steps) to save checkpoints. Requires `--save` to be set. | `None` | Type: int | Megatron-LM (Reset by Miles) | -| `--async-save` | Enable asynchronous checkpoint saving (Megatron backend only). | `False` | bool flag (set to enable) | Megatron-LM (Reset by Miles) | -| `--save-hf` | Path to save the model in HuggingFace format when using Megatron backend. The model will be saved to `save_hf.format(rollout_id)`. | `None` | Type: str | Miles Native | -| `--no-save-optim` | If set, optimizer state is not saved with checkpoints to reduce size, but prevents resumption of training. | `False` | bool flag (set to enable) | Megatron-LM (Reset by Miles) | -| `--ref-load` | Path to the reference model checkpoint. Used as an initial checkpoint if `--load` is not set. | `None` | Type: str | Miles Native | -| `--ref-ckpt-step` | The checkpoint step for the reference model. | `None` | Type: int | Miles Native | -| `--critic-load` | Checkpoint to load for the critic model. | value of `--load` | Type: str | Miles Native | -| `--critic-save` | Path to save the critic model. | `None` | Type: str | Miles Native | -| `--start-rollout-id` | The starting rollout step. If not set, it is inferred from the --load checkpoint when resuming training. Otherwise, if training is not continuous, Miles will start training from scratch | `None` | Type: int | Miles Native | - ---- - -## Algorithm and RL Arguments - -Arguments for reinforcement learning algorithms and loss calculation. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--advantage-estimator` | Advantage estimator to use. | `"grpo"` | `grpo`, `gspo`, `ppo`, `reinforce_plus_plus`, `reinforce_plus_plus_baseline`, `on_policy_distillation` | Miles Native | -| `--loss-type` | Type of loss function to use. | `"policy_loss"` | `policy_loss`, `sft_loss`, `custom_loss` | Miles Native | -| `--custom-loss-function-path` | Path to a custom loss calculation function (requires `--loss-type custom_loss`). [Ref](../get_started/customization.md#9-custom-loss-function---custom-loss-function-path) | `None` | Type: str | Miles Native | -| `--critic-lr` | Learning rate for the Critic. Defaults to `--lr`. | `None` | Type: float | Miles Native | -| `--critic-lr-warmup-iters` | Number of iterations for Critic learning rate linear warmup. | `0` | Type: int | Miles Native | -| `--num-critic-only-steps` | Number of initial steps dedicated to training only the Critic. | `0` | Type: int | Miles Native | -| `--eps-clip` | PPO clip range. | `0.2` | Type: float | Miles Native | -| `--eps-clip-high` | PPO clip upper range (defaults to `--eps-clip` if not set). | `None` | Type: float | Miles Native | -| `--eps-clip-c` | Lower bound for [Dual-clip PPO](https://arxiv.org/pdf/1912.09729). | `None` | Type: float | Miles Native | -| `--value-clip` | Clip range for value loss. | `0.2` | Type: float | Miles Native | -| `--kl-coef` | KL penalty coefficient for reward shaping. This is applied to the reward signal before advantage calculation for PPO and REINFORCE-style estimator. | `0.00` | Type: float | Miles Native | -| `--use-kl-loss` | Enable KL loss term in the final objective (as in GRPO). | `False` | bool flag (set to enable) | Miles Native | -| `--kl-loss-coef` | Weight of the KL loss term in the final objective. | `0.0` | Type: float | Miles Native | -| `--kl-loss-type` | Selection of the KL loss implementation. See [Approximating KL Divergence](http://joschu.net/blog/kl-approx.html) for more details. | `k1` | `k1`, `k2`, `k3`, `low_var_kl` | Miles Native | -| `--use-unbiased-kl` | Apply Importance Sampling (IS) correction to the KL estimator. Reduces bias from distribution shift. | `False` | bool flag (set to enable) | Miles Native | -| `--entropy-coef` | Coefficient for entropy regularization term. Penalizes low entropy to encourage exploration and prevent premature convergence. | `0.0` | Type: float | Miles Native | -| `--gamma` | Discount factor for future rewards. Used in PPO (GAE) and REINFORCE++. | `1.0` | Type: float | Miles Native | -| `--lambd` | PPO GAE lambda. | `1.0` | Type: float | Miles Native | -| `--normalize-advantages` | Performs distributed masked whitening of advantages. Normalization statistics are computed globally across the Data-Parallel group, ignoring padding tokens. | `False` | bool flag (set to enable) | Miles Native | -| `--disable-compute-advantages-and-returns` | Disables the calculation of advantages and returns. This is typically used for SFT or custom loss functions where value estimation is not required. | `False` | bool flag (set to enable) | Miles Native | -| `--use-tis` | Enable Token-level Importance Sampling (TIS) from this [blog](https://fengyao.notion.site/off-policy-rl#279721e3f6c48092bbe2fcfe0e9c6b33). | `False` | bool (set to enable) | Miles Native | -| `--tis-clip` | Clipping threshold C for importance sampling ratios to control variance. | `2.0` | Type: float | Miles Native | -| `--tis-clip-low` | Lower bound clipping threshold C for importance sampling ratios to control variance. | `0.0` | Type: float | Miles Native | -| `--custom-tis-function-path` | Path to a custom TIS or MIS function. [Ref](../get_started/customization.md#10-custom-tisrs-function---custom-tis-function-path) | `None` | Type: str | Miles Native | -| `--custom-pg-loss-reducer-function-path` | Custom reducer function for policy gradient loss. [Ref](../get_started/customization.md#11-custom-pg-loss-reducer---custom-pg-loss-reducer-function-path) | `None` | Type: str | Miles Native | -| `--use-routing-replay` | Enable R2 (Routing Replay) for MoE: record expert routing decisions during forward and replay them during backward. [Paper](https://arxiv.org/abs/2507.18071) **Note:** automatically set to `True` when `--use-rollout-routing-replay` is enabled. | `False` | bool flag (set to enable) | Miles Native | -| `--use-rollout-routing-replay` | Enable R3 (Rollout Routing Replay) for MoE: record expert routing decisions during rollout and replay them during training. **Requires `--use-miles-router`**. [Paper](https://arxiv.org/abs/2510.11370) [Ref](miles-router.md#22-rollout-routing-replay-r3-for-moe) | `False` | bool flag (set to enable) | Miles Native | -| `--use-opsm` | Enable Off-Policy Sequence Masking (OPSM). Filters sequences that have **BOTH** negative advantages (bad results) AND high KL divergence (stale data). This stabilizes training by preventing updates from unreliable, highly off-policy samples. | `False` | bool flag (set to enable) | Miles Native | -| `--opsm-delta` | The threshold for Off-Policy Sequence Masking (OPSM). | `1e-4` | Type: float | Miles Native | -| `--get-mismatch-metrics` | Calculate mismatch metrics. If it is set, you need to provide a custom TIS function via `--custom-tis-function-path`. | `False` | bool flag (set to enable) | Miles Native | -| `--ref-update-interval` | Interval (in rollout steps) to update ref model from actor. If `None`, ref model is not updated. | `None` | Type: int | Miles Native | -| `--reset-optimizer-states` | Resets the optimizer state after each rollout round. This clears the optimization history, which can improve stability or satisfy specific experimental requirements. | `False` | bool flag (set to enable) | Miles Native | -| `--disable-grpo-std-normalization` | Disable standard deviation normalization for GRPO. From [Dr.GRPO](https://arxiv.org/pdf/2503.20783) | `False` | bool flag (set to enable) | Miles Native | -| `--disable-rewards-normalization` | Disable the default group-wise reward normalization for GRPO, GSPO, and REINFORCE++. This effectively skips the baseline subtraction step. | `False` | bool flag (set to enable) | Miles Native | -| `--use-rollout-entropy` | Enable entropy calculation when calculating the logprobs from actor and reference model. This is useful for implementing custom entropy-based loss masking. | `False` | bool flag (set to enable) | Miles Native | -| `--use-rollout-logprobs` | Use rollout logprobs as the old-policy logprobs when computing importance sampling ratios / PPO-style KL in GRPO/GSPO/PPO. If not set, Miles recomputes old-policy logprobs with the training actor (e.g., `old_actor` or `actor`, depending on configuration). If `--get-mismatch-metrics` is set, the log probs will still be recomputed by the training engine (one more forward pass will be applied). | `False` | bool flag (set to enable) | Miles Native | -| `--calculate-per-token-loss` | Calculate loss on a per-token basis. | `False` | bool flag (set to enable) | Megatron-LM (Reset by Miles) | -| `--seed` | Random seed for the training process. **Also passed to SGLang servers as `random_seed`** (Miles uses `seed + engine_rank` so each engine has a distinct but reproducible seed). | `1234` | Type: int | Megatron-LM (Reset by Miles) | -| `--clip-grad` | Maximum gradient norm for gradient clipping. | `1.0` | Type: float | Megatron-LM (Reset by Miles) | - ---- - -## Logging and Monitoring - -Arguments for WandB, Tensorboard, and general logging. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--use-wandb` | Enable WandB logging. | `False` | bool flag (set to enable) | Miles Native | -| `--wandb-mode` | WandB operating mode. Overrides `WANDB_MODE`. | `None` | `online`, `offline`, `disabled` | Miles Native | -| `--wandb-project` | WandB project name. | `None` | Type: str | Megatron-LM (Reset by Miles) | -| `--wandb-group` | WandB group name. | `None` | Type: str | Miles Native | -| `--wandb-team` | WandB team name. | `None` | Type: str | Miles Native | -| `--wandb-host` | WandB host address. | `None` | Type: str | Miles Native | -| `--wandb-key` | WandB API key. | `None` | Type: str | Miles Native | -| `--wandb-run-id` | Specific WandB run ID to resume. | `None` | Type: str | Miles Native | -| `--wandb-dir` | Directory to store WandB logs. Default is `./wandb` in current directory. | `None` | Type: str | Miles Native | -| `--disable-wandb-random-suffix` | Disable adding a random suffix to the WandB run name. By default, we will add a random 6 length string with characters to the run name. | `False` | bool flag (set to enable) | Miles Native | -| `--wandb-always-use-train-step` | Use training steps instead of rollout steps for the x-axis. | `False` | bool flag (set to enable) | Miles Native | -| `--use-tensorboard` | Enable Tensorboard logging. | `False` | bool flag (set to enable) | Miles Native | -| `--tb-project-name` | Tensorboard project directory. | `None` | Type: str | Miles Native | -| `--tb-experiment-name` | Tensorboard experiment name. | `None` | Type: str | Miles Native | -| `--tensorboard-dir` | Directory to store Tensorboard logs. | `None` | Type: str | Miles Native | -| `--log-multi-turn` | Log detailed information for multi-turn conversations. | `False` | bool flag (set to enable) | Miles Native | -| `--log-passrate` | Enable logging of `pass@n` metrics. | `False` | bool flag (set to enable) | Miles Native | -| `--log-correct-samples` | Explicitly log metrics for correct samples. | `False` | bool flag (set to enable) | Miles Native | -| `--log-reward-category` | Log reward-category statistics (e.g., why the reward function marked a failure). Use this argument to specify the key in the reward dict. | `None` | Type: str | Miles Native | - ---- - -## Fault Tolerance - -Arguments for handling server failures during rollout. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--use-fault-tolerance` | Enable fault tolerance for rollout engines. Periodically sends `/health_generate` heartbeats. | `False` | bool flag (set to enable) | Miles Native | -| `--rollout-health-check-interval` | Interval in seconds between rollout engine `/health_generate` checks during generate/eval. | `30.0` | Type: float | Miles Native | -| `--rollout-health-check-timeout` | Timeout in seconds to wait for a rollout engine `/health_generate` response before killing it. | `30.0` | Type: float | Miles Native | -| `--rollout-health-check-first-wait` | Initial grace period (in seconds) before starting health checks. This allows time for model compilation and initialization. Increase this value significantly when using deepgemm. | `0.0` | Type: float | Miles Native | - ---- - -## Miles Router - -Arguments for the specialized Miles text-based router. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--use-miles-router` | Use Miles Router (FastAPI passthrough proxy) instead of SGLang Model Gateway for rollout routing. Required for features that depend on preserving extra rollout metadata (e.g., R3). [Ref](miles-router.md) | `False` | bool flag (set to enable) | Miles Native | -| `--miles-router-middleware-paths` | Paths to custom MilesRouter middleware functions. [Ref](../get_started/customization.md#18-miles-router-middleware---miles-router-middleware-paths) | `""` | Type: List[str] | Miles Native | -| `--miles-router-timeout` | Timeout for router HTTP requests in seconds. | `None` | Type: float | Miles Native | -| `--miles-router-max-connections` | Max connections for MilesRouter HTTP client. | `None` | Type: int | Miles Native | -| `--miles-router-health-check-failure-threshold` | Number of consecutive failures before marking a worker as unhealthy. | `3` | Type: int | Miles Native | - ---- - -## Reward Model Arguments - -Arguments for configuring reward signals and post-processing. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--rm-type` | Built-in reward model selection. | `None` | `remote_rm`, `deepscaler`, `dapo`, `math`, `f1`, `gpqa`, `ifbench`, `random` | Miles Native | -| `--rm-url` | URL for the reward model service (used with `--rm-type remote_rm`). | `None` | Type: str | Miles Native | -| `--reward-key` | JSON key to extract the numerical reward from a returned dictionary if reward model returns a dict instead of a value. | `None` | Type: str | Miles Native | -| `--eval-reward-key` | Evaluation variant for `--reward-key`. | `None` | Type: str | Miles Native | -| `--custom-rm-path` | Path to a custom Python reward function. [Ref](../get_started/customization.md#3-reward-model---custom-rm-path) | `None` | Type: str | Miles Native | -| `--group-rm` | Defer reward computation to process the entire group of samples (per-prompt) at once. Essential for comparative/ranking reward models and improves throughput. **Not supported in eval**. | `False` | bool flag (set to enable) | Miles Native | -| `--custom-reward-post-process-path` | Path to a custom reward post-processor. [Ref](../get_started/customization.md#12-reward-post-processing---custom-reward-post-process-path) | `None` | Type: str | Miles Native | -| `--custom-convert-samples-to-train-data-path` | Path to a custom data format converter. [Ref](../get_started/customization.md#13-samples-to-train-data-conversion---custom-convert-samples-to-train-data-path) | `None` | Type: str | Miles Native | - ---- - -## Rollout Buffer Management - -Arguments for managing the rollout data buffer. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--rollout-buffer-url` | URL for the rollout buffer service. | `None` | Type: str | Miles Native | -| `--fetch-trajectory-retry-times` | Number of times to retry fetching trajectory, -1 means unlimited retry. | `-1` | Type: int | Miles Native | -| `--min-batch-collection-ratio` | Minimum batch collection ratio before proceeding. | `1.0` | Type: float | Miles Native | -| `--disable-rollout-trim-samples` | Disable trim samples in rollout buffer when converting samples to train data. | `False` | bool flag (set to enable) | Miles Native | -| `--use-dynamic-global-batch-size` | Enable dynamic global batch size, disable trim samples in rollout buffer when converting samples to train data. | `False` | bool flag (set to enable) | Miles Native | -| `--rollout-task-type` | Type of task being performed. | `math` | Type: str | Miles Native | -| `--loss-mask-type` | Selection of the token masking logic. | `qwen` | `qwen`, `qwen3`, `distill_qwen` | Miles Native | - ---- - -## Multi-Token Prediction (MTP) Arguments - -Arguments for MTP-based training. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--enable-mtp-training` | Enable MTP layer parameter updates during training. | `False` | bool flag (set to enable) | Miles Native | -| `--mtp-num-layers` | Number of MTP layers to include. | `None` | Type: int | Megatron-LM (Reset by Miles) | -| `--mtp-loss-scaling-factor` | Scaling factor applied to the MTP loss. | `0.2` | Type: float | Megatron-LM (Reset by Miles) | - ---- - -## SGLang Backend Arguments - -Most SGLang server arguments can be passed through by adding the `--sglang-` prefix (some are intentionally skipped, e.g. `model_path`, `tp_size`, `port`, `nnodes`, `node_rank`). For a full list, refer to the [SGLang Server Arguments documentation](https://docs.sglang.io/advanced_features/server_arguments.html). - -Commonly used arguments: - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--sglang-mem-fraction-static` | Fraction of GPU memory to reserve for SGLang KV cache. | `0.9` | Type: float | SGLang | -| `--sglang-server-concurrency` | Maximum number of concurrent requests. | `512` | Type: int | SGLang | -| `--sglang-router-ip` | IP address of the SGLang router and Miles Router. | `None` | Type: str | SGLang Gateway & Miles Router | -| `--sglang-router-port` | Port of the SGLang router and Miles Router. | `None` | Type: int | SGLang Gateway & Miles Router | -| `--sglang-router-request-timeout-secs` | Timeout for requests to the SGLang router. | `14400` | Type: int | SGLang Gateway | - ---- - -## Megatron Specific Arguments - -Arguments applicable when using `--train-backend megatron`. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--megatron-to-hf-mode` | Method to convert Megatron weights to HuggingFace format for SGLang integration. | `raw` | `raw`, `bridge` | Miles Native | -| `--seq-length` | Megatron’s “maximum sequence length” parameter. **In miles training, this parameter has no effect in most setups**: miles uses varlen/packed samples (no truncation based on `seq_length`), forces variable sequence lengths for PP communication buffers, and uses all-to-all token dispatch for MoE. This parameter mainly matters in Megatron’s dataset pipeline. | `None` | Type: int | Megatron-LM | - ---- - -## FSDP Specific Arguments - -Arguments applicable when using `--train-backend fsdp`. **Note: The FSDP backend is still under development and experimental.** - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--warmup-ratio` | Ratio of total steps for warmup. | `0.03` | Type: float | Miles Native | -| `--weight-decay` | Weight decay for the optimizer. | `0.0` | Type: float | Miles Native | -| `--gradient-checkpointing` | Enable gradient checkpointing. | `False` | bool flag (set to enable) | Miles Native | -| `--fsdp-cpu-offload` | Offload parameters and gradients to CPU. | `False` | bool flag (set to enable) | Miles Native | -| `--fsdp-state-dict-cpu-offload` | Offload full state dict to CPU during collection. | `False` | bool flag (set to enable) | Miles Native | -| `--fsdp-cpu-backend` | CPU backend for FSDP CPU offload. | `gloo` | `gloo`, `None` | Miles Native | -| `--attn-implementation` | Selection of the attention implementation. | `flash_attention_2` | `flash_attention_2`, `sdpa`, `eager` | Miles Native | -| `--use-pytorch-profiler` | Enable PyTorch-native profiling. | `False` | bool flag (set to enable) | Miles Native | -| `--profile-step-start` | Starting step for profiling. | `10` | Type: int | Miles Native | -| `--profile-step-end` | Ending step for profiling. | `12` | Type: int | Miles Native | -| `--lr-wsd-decay-iters` | Number of iterations for WSD decay. | `None` | Type: int | Miles Native | -| `--lr-wsd-decay-style` | Decay style for WSD. | `None` | Type: str | Miles Native | -| `--use-checkpoint-lr-scheduler` | Use the checkpoint's LR scheduler state. | `False` | bool flag (set to enable) | Miles Native | -| `--override-lr-scheduler` | Override the loaded LR scheduler state. | `False` | bool flag (set to enable) | Miles Native | -| `--wandb-run-name` | Specific run name for WandB (FSDP backend). | `None` | Type: str | Miles Native | - ---- - -## Debug and Profiling - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--check-weight-update-equal` | Use SGLang's weight checker to check and ensure that the loaded weight from HF checkpoint and received from Megatron are bit-wise equal. | `False` | bool flag (set to enable) | Miles Native | -| `--save-debug-rollout-data` | Path to save rollout data for offline analysis. [Ref](../developer_guide/debug.md) | `None` | Type: str | Miles Native | -| `--load-debug-rollout-data` | Path to load debug rollout data (bypasses SGLang). [Ref](../developer_guide/debug.md) | `None` | Type: str | Miles Native | -| `--load-debug-rollout-data-subsample` | Percentage of debug data to load (0.0 to 1.0). [Ref](../developer_guide/debug.md) | `None` | Type: float | Miles Native | -| `--debug-rollout-only` | Run the rollout phase only without training. [Ref](../developer_guide/debug.md) | `False` | bool flag (set to enable) | Miles Native | -| `--debug-train-only` | Run the training phase only without launching SGLang servers. [Ref](../developer_guide/debug.md) | `False` | bool flag (set to enable) | Miles Native | -| `--save-debug-train-data` | Path to save training batches for offline math debugging. | `None` | Type: str | Miles Native | -| `--dump-details` | Dump exhaustive training details for post-hoc visualization. | `None` | Type: str | Miles Native | -| `--memory-snapshot-path` | Path to save memory snapshots. | `snapshot.pickle` | Type: str | Miles Native | -| `--record-memory-history` | Record memory history for snapshots. | `False` | bool flag (set to enable) | Miles Native | -| `--memory-snapshot-dir` | Directory for PyTorch memory snapshots. | `.` | Type: str | Miles Native | -| `--memory-snapshot-num-steps` | Number of steps to record before saving snapshot. | `None` | Type: int | Miles Native | -| `--memory-recorder` | Selection of the memory recording backend. | `torch` | `torch`, `memray` | Miles Native | -| `--profile-target` | Training components to profile (accepts multiple). | `train_overall` | `train_overall`, `train_actor`, `train_log_probs` | Miles Native | - ---- - -## Environment Variables - -Miles recognizes several environment variables for advanced configuration. - -| Variable | Description | Source | -| :--- | :--- | :--- | -| `MILES_EXPERIMENTAL_ROLLOUT_REFACTOR` | Set to `1` to enable the experimental rollout implementation refactor. | Miles Native | -| `ENABLE_ROUTING_REPLAY` | Internal variable used to enable MoE routing consistency checks during training. | Miles Native | -| `TENSORBOARD_DIR` | Base directory for Tensorboard logs. | Miles Native | -| `MILES_HOST_IP` | Overrides the host IP used for distributed communication. | Miles Native | -| `PYTHONPATH` | Must include the path to your `Megatron-LM` installation when using the Megatron backend. | System | -| `NCCL_SOCKET_IFNAME` | Specifies the network interface for NCCL communication (e.g., `eth0`, `bond0`). | System | -| `GLOO_SOCKET_IFNAME` | Specifies the network interface for GLOO communication. | System | -| `NVSHMEM_BOOTSTRAP_UID_SOCK_IFNAME` | Network interface for NVSHMEM bootstrap. | System | - ---- - -## Multi-Turn and Agentic Arguments - -Arguments for managing interactions and tools. Only available when `MILES_EXPERIMENTAL_ROLLOUT_REFACTOR=1` and the rollout/generate function exposes `add_arguments`. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--generate-max-turns` | Maximum number of turns in a conversation. | `16` | Type: int | Miles Native | -| `--generate-tool-specs-path` | Path to the tool specifications (JSON). | `None` | Type: str | Miles Native | -| `--generate-tool-call-parser` | The parser used to extract tool calls from text. | `None` | Type: str | Miles Native | -| `--generate-execute-tool-function-path` | Path to the function that executes the tool. | `None` | Type: str | Miles Native | -| `--generate-multi-samples` | Whether to generate multiple samples within one turn. | `False` | bool flag (set to enable) | Miles Native | - ---- - -## Advanced Developer Hooks and CI - -Hooks for custom logic and Continuous Integration testing flags. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--custom-megatron-init-path` | Path to custom Megatron initialization logic. [Ref](../get_started/customization.md#17-megatron-hooks) | `None` | Type: str | Miles Native | -| `--custom-megatron-before-log-prob-hook-path` | Hook called before calculating log probabilities. [Ref](../get_started/customization.md#17-megatron-hooks) | `None` | Type: str | Miles Native | -| `--custom-megatron-before-train-step-hook-path` | Hook called before each training step. [Ref](../get_started/customization.md#17-megatron-hooks) | `None` | Type: str | Miles Native | -| `--ci-test` | Enable Continuous Integration testing mode. | `False` | bool flag (set to enable) | Miles Native | -| `--ci-disable-kl-checker` | Disable KL divergence sanity checks in CI. | `False` | bool flag (set to enable) | Miles Native | -| `--ci-metric-checker-key` | Metric key to monitor for pass/fail in CI. | `None` | Type: str | Miles Native | -| `--ci-metric-checker-threshold` | Pass/fail threshold (minimum value) for the monitored metric. | `None` | Type: float | Miles Native | -| `--ci-save-grad-norm` | Path to save gradient norms for CI comparison. | `None` | Type: str | Miles Native | -| `--ci-load-grad-norm` | Path to load gradient norms for CI verification. | `None` | Type: str | Miles Native | - ---- - -## Miscellaneous and System - -General arguments for infrastructure and configuration overrides. - -| Argument | Description | Default | Options | Source | -| :--- | :--- | :--- | :--- | :--- | -| `--http-proxy` | HTTP proxy server for remote reward model calls. | `None` | Type: str | Miles Native | -| `--use-distributed-post` | Use distributed POST requests for remote reward models. | `False` | bool flag (set to enable) | Miles Native | -| `--custom-config-path` | Path to the YAML config for custom function arguments. | `None` | Type: str | Miles Native | -| `--padded-vocab-size` | Manually specify the vocab size for padding. | `None` | Type: int | Miles Native | diff --git a/docs/en/advanced/p2p-weight-transfer.md b/docs/en/advanced/p2p-weight-transfer.md deleted file mode 100644 index 4baaed20638..00000000000 --- a/docs/en/advanced/p2p-weight-transfer.md +++ /dev/null @@ -1,139 +0,0 @@ -# P2P Weight Transfer - -miles supports P2P (point-to-point) weight transfer between training and rollout engines. By using `--update-weight-transfer-mode p2p`, miles enables more efficient weight transfer from training ranks to rollout engine ranks. More details on the design and implementation can be found in [this issue](https://github.com/radixark/miles/issues/755). - -## Usage - -To enable P2P weight transfer, add the following flag to your training command: - -``` ---update-weight-transfer-mode p2p -``` - -## How It Works - -The default weight transfer mode in miles is `broadcast`: after training, updated weights are broadcast via NCCL to all rollout engine ranks. This works but does not fully utilize the available bandwidth, as redundant copies of the same weights are transferred to multiple ranks. - -P2P mode addresses this by having each training rank transfer only the specific weight shards required by its target rollout engine rank(s), writing them directly to remote memory without redundant copies. The key steps are: - -1. **Initialization**: Training ranks establish point-to-point connections (via RDMA) to their target rollout engine ranks. Including: - - Create a transfer plan that maps each training rank to its target rollout rank(s) based on GPU counts and parallelism configuration. - - Query remote rollout engines for their weight memory registration info (addresses and sizes for RDMA writes). - - Query remote parallelism config and construct a local CPU model replica that mirrors the target's sharding layout, enabling correct weight format conversion before transfer. - -2. **Weight gather**: Megatron TP/EP shards are all-gathered and converted to HF format, same as the broadcast path. - -3. **P2P transfer**: Instead of a collective broadcast, each source rank writes bucketed weight tensors directly to the destination rollout rank's memory, in a write-only fashion. - -4. **Synchronization**: Once all RDMA writes are confirmed complete, rollout engines increment their weight version and resume generation for the next training step. - -## Architecture - -Both broadcast and P2P modes share the same bucketed weight-update pipeline in `miles/backends/megatron_utils/update_weight/`. The diagram below shows which components are shared and which are P2P-specific. - -### Shared components (broadcast & P2P) - -| Component | Description | -|---|---| -| **TP/EP all-gather** | Megatron TP shards are all-gathered within each PP stage; EP shards are gathered per-bucket when the accumulated expert data exceeds `buffer_size * ep_size`. Both modes perform this identically via `common.py`. | -| **Bucketed update** | Weights are not transferred one parameter at a time. Instead, converted tensors are accumulated into a fixed-size buffer (`--update-weight-buffer-size`, default 1 GB). When the buffer is full, the entire bucket is flushed — via NCCL broadcast or RDMA write depending on the mode. This amortizes per-transfer overhead. Non-expert and expert weights use separate buckets. | -| **PP independence** | Each pipeline-parallel stage updates its own weights independently. In broadcast mode, each PP rank has its own NCCL group (`miles-pp_{pp_rank}`). In P2P mode, each PP rank has its own transfer plan. No cross-PP synchronization is needed during weight transfer, which is key to scaling. | -| **HF format conversion** | After all-gather, Megatron-format tensors (with custom naming and sharding) are converted to HuggingFace-format names expected by the sglang rollout engine. | - -### P2P-specific components - -| Component | File | Description | -|---|---|---| -| **Transfer plan** | `p2p_transfer_utils.py` | Maps each training rank to its target rollout engine rank(s). Uses round-robin assignment with load balancing: the first `min(sources, targets)` ranks get 1:1 mapping, remaining targets are distributed evenly. This minimizes the number of RDMA sessions per source. | -| **CPU model replica** | `p2p.py` | A full sglang model is instantiated on CPU (not GPU) to mirror the target engine's parallelism layout. This replica provides the correct `weight_loader` functions to re-shard all-gathered HF weights into the exact format expected by each target rank. Only the first engine's replica pins memory; subsequent engines reuse the mapping via `ParameterMapper`. | -| **Shared pinned buffer** | `p2p.py` | A single CPU pinned memory buffer is registered with the mooncake TransferEngine for RDMA. This buffer is reused across all target engines (O(1) memory, not O(num\_engines)). The buffer is overwritten per-engine, per-bucket. | -| **Pipelined transfer** | `p2p.py` | RDMA writes to multiple target engines are pipelined: for non-last engines, the transfer manager waits for the previous write to complete before reusing the buffer; for the last engine, writes are fire-and-forget to a background thread pool, overlapping with the next bucket's load phase. | - -## Supported Model Architectures - -P2P weight transfer relies on a unified weight name mapping interface between Megatron and sglang (see [sglang#17326](https://github.com/sgl-project/sglang/pull/17326)). The following sglang model classes are supported: - -| sglang Model Class | Model Family | Example Models | -|---|---|---| -| `Qwen2ForCausalLM` | Qwen2 (dense) | Qwen2.5-0.5B, Qwen2.5-7B | -| `Qwen3ForCausalLM` | Qwen3 (dense) | Qwen3-4B, Qwen3-8B | -| `Qwen3MoeForCausalLM` | Qwen3-MoE | Qwen3-30B-A3B, Qwen3-235B-A22B | -| `Glm4ForCausalLM` | GLM4 (dense) | GLM-Z1-9B-0414 | -| `Glm4MoeForCausalLM` | GLM4-MoE | GLM-4.5-Air | -| `Glm4MoeLiteForCausalLM` | GLM4-MoE | GLM-4.7-9B-Flash | -| `DeepseekV2ForCausalLM` | DeepSeek V2 | Moonlight-16B-A3B | -| `DeepseekV3ForCausalLM` | DeepSeek V3p2 | GLM-5 (744B-A40B) | -| `DeepseekV3ForCausalLM` | DeepSeek V3 | Kimi-K2 (1T) \* | - -> **Note:** All the above models are tested on H100-80GB clusters. -> -> For Kimi-K2, we set `training nodes = inference nodes = 32` to ensure sufficient memory. -> The original Kimi-K2 checkpoint uses a block-quant size of `[128, 128]`, which will trigger errors if `sglang-tp-size = 32`. -> To work around this, we re-quantize to `[64, 64]` and update all affected scale tensors accordingly. - -## Validated Models - -All models below have been validated with `--check-weight-update-equal` in P2P mode. - -| Model | sglang Model Class | Nodes | -|---|---|---| -| Qwen3-4B | `Qwen3ForCausalLM` | 1 | -| GLM-Z1-9B-0414 | `Glm4ForCausalLM` | 1 | -| Moonlight-16B-A3B | `DeepseekV2ForCausalLM` | 2 | -| GLM-4.7-9B-Flash | `Glm4MoeLiteForCausalLM` | 2 | -| GLM-5_4layer | `DeepseekV3ForCausalLM` | 2 | -| Qwen3-30B-A3B | `Qwen3MoeForCausalLM` | 4 | -| GLM-4.5-Air | `Glm4MoeForCausalLM` | 8 | - -> Enabling `--check-weight-update-equal` for Kimi-K2 is non-trivial due to several issues: -> -> - The user must first dequantize the Kimi-K2 model to BF16 for training, then re-quantize the parameters for weight updating. Meanwhile, the rollout side snapshots the original Kimi-K2 tensors as the reference when enabling `--check-weight-update-equal`. This means the rollout SGLang engine should load the checkpoints processed by the `dequant-requant` pipeline. -> - We use modified checkpoints with block-quant size `[64, 64]`, so any `quant/dequant` code that hard-codes the block-quant size as `[128, 128]` will break. -> - Certain tensors that are only initialized on the rollout side (e.g., `k_scale` / `v_scale`) must be skipped during the weight-check process. -> -> We verified P2P correctness for Kimi-K2 by enabling `--check-weight-update-equal` with hard-coded workarounds for the issues above. The transferred weights were confirmed correct. These hard-coded workarounds are verification-only and will not be merged into the main branch to keep the codebase maintainable. - -## Profiling Results - -For M source ranks and N target ranks, with source pp size `src_pp` and target ep size `sgl_ep`, the benefit of P2P transfer is approximately: -1. utilizes `M // src_pp` times more source transmission bandwidth. -2. each target rank receives `sgl_ep` times less data. - -Thus we expect our solution to scale better, especially on the MoE models. - -All profiling is run on H100-80GB clusters with 1GB transfer bucket. -Timing measures after `pause_generation` call returns and before `update_weight` call exits to exclude request queue abortion time. -Table includes steady-state steps 3–12 average. For Kimi-K2, the RDMA (ms) column includes GPU-side post-processing time (`post_load_weights` ~884ms) since this model requires GPU-side weight requantization after RDMA transfer. - -Models marked with ★ are MoE architectures, where P2P benefits are most pronounced due to expert-parallel sharding reducing per-target transfer volume. - -| Model Family | Model Name | Total Param | sglang Model Class | Train Config | Inference Config | NCCL (ms) | RDMA (ms) | Delta | -|---|---|---|---|---|---|---|---|---| -| GLM4 | GLM-Z1-9B-0414 | 9B | `Glm4ForCausalLM` | TP=2, PP=1, CP=2, EP=1, ETP=1, 1 node | TP=4, EP=1, 1 node | 694.6 | 707.1 | +1.8% | -| DeepSeek-V2 ★ | Moonlight-16B-A3B | 16B(3B) | `DeepseekV2ForCausalLM` | TP=2, PP=1, CP=1, EP=8, ETP=1, 1 node | TP=8, EP=8, 1 node | 1,482.0 | 1,073.3 | **−27.6%** | -| GLM4-MoE ★ | GLM-4.7-9B-Flash | 30B(3B) | `Glm4MoeLiteForCausalLM` | TP=4, PP=1, CP=1, EP=8, ETP=1, 1 node | TP=4, EP=4, 1 node | 2,508.6 | 4,229.0 | +68.6% | -| DeepSeek-V3 ★ | GLM-5_4layer | 4-layer | `DeepseekV3ForCausalLM` | TP=4, PP=1, CP=1, EP=8, ETP=1, 1 node | TP=8, EP=8, 1 node | 732.2 | 1,260.8 | +72.2% | -| Qwen3-MoE ★ | Qwen3-30B-A3B | 30B(3B) | `Qwen3MoeForCausalLM` | TP=4, PP=1, CP=1, EP=8, ETP=1, 2 nodes | TP=8, EP=8, 2 nodes | 2,670.0 | 2,160.2 | **−19.1%** | -| GLM4-MoE ★ | GLM-4.5-Air | 106B(12B) | `Glm4MoeForCausalLM` | TP=1, PP=4, CP=1, EP=8, ETP=1, 4 nodes | TP=8, EP=8, 4 nodes | 5,001.1 | 2,637.2 | **−47.3%** | -| Qwen3-MoE ★ | Qwen3-235B-A22B | 235B(22B) | `Qwen3MoeForCausalLM` | TP=4, PP=4, CP=2, EP=16, ETP=1, 8 nodes | TP=32, EP=32, 8 nodes | 10,753.6 | 3,162.0 | **−70.6%** | -| DeepSeek-V3p2 ★ | GLM-5 | 744B(40B) | `DeepseekV3ForCausalLM` | TP=4, PP=8, CP=2, EP=16, ETP=1, 16 nodes | TP=64, EP=64, 16 nodes | 58,301.5 | 8,479.7 | **−85.5%** | -| DeepSeek-V3 ★ | Kimi-K2 | 1T(64B) | `DeepseekV3ForCausalLM` | TP=8, PP=8, CP=4, EP=32, ETP=1, 32 nodes | TP=32, EP=32, 32 nodes | 53,279.1 | 7,227.3 | **−86.4%** | - - -![P2P vs NCCL Broadcast Scaling](../../../imgs/p2p_vs_nccl_scaling.png) - -\* Kimi-K2 RDMA time includes ~884 ms GPU-side `post_load_weights` requantization on rollout engines. - -## Examples - -### CI Test (single-node, Qwen3-4B) - -The P2P weight transfer E2E test validates correctness on a single node using `Qwen3-4B`: - -```python -# tests/e2e/megatron/test_qwen3_4B_p2p.py -# -# Train: 4 GPUs (TP=2, CP=2) -# Rollout: 4 GPUs (sglang, 2 engines × 2 GPUs each) -# Flags: --update-weight-transfer-mode p2p --check-weight-update-equal -``` diff --git a/docs/en/advanced/pd-disaggregation.md b/docs/en/advanced/pd-disaggregation.md deleted file mode 100644 index f509d72d83f..00000000000 --- a/docs/en/advanced/pd-disaggregation.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index 2c37b6aa449..00000000000 --- a/docs/en/advanced/speculative-decoding.md +++ /dev/null @@ -1,39 +0,0 @@ -# Speculative Decoding - -Speculative decoding is a key optimization for speeding up rollouts. Instead of having the expensive target model decode token by token during inference, a lightweight draft model first decodes ahead to produce several tokens, and then the target model verifies them in a batch. - -## Accelerating Inference with Speculative Decoding - -For models with MTP layers (e.g., GLM-4.7, DeepSeek-V3/R1), simply add: - -```bash ---sglang-speculative-algorithm EAGLE ---sglang-speculative-num-steps 3 ---sglang-speculative-eagle-topk 1 ---sglang-speculative-num-draft-tokens 4 ---sglang-enable-draft-weights-cpu-backup -``` - -If you want to use a separately trained draft model (e.g., one trained with [SpecForge](https://docs.sglang.ai/SpecForge/)), also set: - -```bash ---sglang-speculative-draft-model-path /your/draft/model/path -``` - -For detailed parameter meanings and configuration, see SGLang’s speculative decoding [documentation](https://docs.sglang.ai/advanced_features/speculative_decoding.html). - -## Online SFT for the Draft Model - -As RL progresses, the sampling distributions of the draft and target models can drift apart. Fewer draft tokens pass verification, and speculative decoding can even yield negative returns. - -miles currently supports online training of the MTP layers during RL, updating the draft model in sync with training to consistently improve sampling speed. See the related rationale in this [blog](https://www.notion.so/jiajunli-guapisolo/Power-Up-Speculative-Decoding-In-Reinforcement-Learning-2a92d24a293b802d9c73dbae429e581e). Use it as follows: - -```bash ---mtp-num-layers 1 ---enable-mtp-training ---mtp-loss-scaling-factor 0.2 -``` - -And note that this requires a torch dist checkpoint with the MTP weight, you need to add `--mtp-num-layers 1` during the checkpoint conversion from huggingface to torch dist. - -Training external draft models is still a WIP. diff --git a/docs/en/agentic/oai_endpoint.md b/docs/en/agentic/oai_endpoint.md deleted file mode 100644 index 3fe95f28b32..00000000000 --- a/docs/en/agentic/oai_endpoint.md +++ /dev/null @@ -1,187 +0,0 @@ -# OAI Endpoint Usage - -This document explains how to use the OpenAI-format chat endpoint through Miles -Router sessions. For the `/generate` endpoint, see -`docs/en/get_started/gen_endpoint.md`. - -## 1. Minimal `run_agent` loop - -Your `run_agent` receives a session-scoped `base_url`. Send OpenAI-format chat -requests to `base_url/v1/chat/completions` and pass the `messages` list as the -prompt. - -Minimal custom agent example: - -```python -from miles.utils.http_utils import post - -async def run_agent(base_url: str, prompt, request_kwargs: dict | None = None) -> None: - payload = {"model": "default", "messages": prompt, **(request_kwargs or {})} - await post(f"{base_url}/v1/chat/completions", payload) -``` - -Notes for `run_agent`: - -- `base_url` already includes the session path (e.g. `/sessions/`), so you - should not manually add the session id. Just append the OpenAI route. -- `request_kwargs` already contains the default sampling settings from - `agentic_tool_call.build_chat_request_kwargs`, so you can directly expand it - into the chat request payload. -- If you pass rollout sampling params, `max_new_tokens` will be mapped to the - OpenAI `max_tokens` field before the request is sent. -- If you need structured parsing payloads, use SGLang's - `ChatCompletionRequest`-compatible format. It is compatible with native OpenAI - fields, plus extra SGLang parameters. - -## 2. OpenAI chat messages and the basic request - -The OpenAI-format chat API uses a list of `messages`, each with a `role` and -`content`. - -Minimal request shape: - -```json -{ - "model": "default", - "messages": [ - {"role": "system", "content": "You are a concise assistant."}, - {"role": "user", "content": "Answer with one word: 2+2?"} - ] -} -``` - -You can pass any OpenAI-compatible parameters in the payload, or any -SGLang-compatible `ChatCompletionRequest` parameters. Note: with -`--use-session-server`, the session middleware sets the token-tracking fields TITO needs. -Do **not** set `logprob_start_len=0` — it would disable SGLang's prefix -cache. - -## 3. Quickstart index - -If you just want something runnable, start here. - -Generator entry point: - -- `miles/rollout/generate_hub/agentic_tool_call.py` — OpenAI-format agent - loop via router sessions. - -OpenAI-format examples that use `agentic_tool_call.generate`: - -- Single-turn (DAPO math): - - `examples/openai_format/dapo_math.py` — custom agent. - - `examples/openai_format/run-qwen3-4B.sh` — launcher. -- Multi-turn (SWE agent): - - `examples/experimental/swe-agent-v2/run.py` — drives a multi-turn - tool-calling loop with `--tito-model glm47 --tito-allowed-append-roles tool user`. - -Key flags for an OpenAI-format agentic run: - -| Flag | Description | -| :--- | :--- | -| `--custom-generate-function-path` | Set to `miles.rollout.generate_hub.agentic_tool_call.generate`. The OpenAI wrapper that creates the session and collects records. | -| `--custom-agent-function-path` | Path to your `run_agent` function. It receives `base_url` and `request_kwargs` and sends chat requests. | -| `--use-session-server` | Enables the session-server middleware that forces the SGLang flags TITO needs and tracks token prefixes across turns. Required for TITO. | -| `--tito-model` | TITO tokenizer family (`qwen3`, `qwen35`, `qwennext`, `glm47`, ...). Use `default` to keep the model's HF-native chat template untouched. | -| `--tito-allowed-append-roles` | Roles the session may append after assistant turns. Default `tool`; add `user` / `system` if your conversation pattern needs them. | - -For the list of supported `--tito-model` families and the role surfaces -pre-wired for each, see -[`miles/utils/chat_template_utils/tito_tokenizer.py`](../../../miles/utils/chat_template_utils/tito_tokenizer.py) -or [issue #712](https://github.com/radixark/miles/issues/712). - -Customize like: - -``` -CUSTOM_ARGS=( - --custom-generate-function-path miles.rollout.generate_hub.agentic_tool_call.generate - --custom-agent-function-path examples.openai_format.dapo_math.run_agent -) -``` - -For OpenAI format, do not add `--apply-chat-template`; the -prompt must remain a `messages` list. - -## 4. Further customization (OpenAI wrapper generate function) - -For OpenAI-format rollout, the key generate function is -`miles/rollout/generate_hub/agentic_tool_call.generate`. It is a thin wrapper -around your custom agent: - -1. Create a session on Miles Router and build a session-scoped `base_url`. -2. Call the custom agent (from `--custom-agent-function-path`) to send one or - more chat requests to `base_url/v1/chat/completions`, typically using - `prompt` and `request_kwargs`. -3. Collect session records via `OpenAIEndpointTracer`. -4. Convert records into `Sample` objects with - `compute_samples_from_openai_records`. - -If you want general generate-function customization beyond the OpenAI wrapper, -see `docs/en/get_started/gen_endpoint.md`. - -## 5. TITO (token-in token-out) - -TITO is the layer that gives miles per-turn token ids and logprobs across a -multi-turn session, without re-tokenizing the full conversation on every -request. It requires `--use-session-server`; with that on, miles handles -three things on your behalf: - -- Hardcodes the SGLang flags TITO needs on every chat request - (`logprobs=True`, `return_meta_info=True`, `no_stop_trim=False`); these are - set by the middleware in `miles/rollout/session/sessions.py` and override any - agent-passed values. -- Reuses the token prefix from previous turns by injecting Miles-owned `input_ids` on every proxied chat request. -- Accumulates per-turn records into the `Sample` you receive at the end of - the session, with `tokens` and `rollout_log_probs` already populated. - -You do not extract token ids from the response yourself. - -### Configuring TITO for your model and conversation - -`--use-session-server` turns TITO on, but you also need to set two more -flags so TITO renders with the right chat template and accepts your -conversation's roles. - -- `--tito-model` (default `default`) selects the TITO tokenizer family. Set - it to your model's family — `qwen3`, `qwen35`, `qwennext`, `glm47`, ... — - so miles auto-resolves a bundled fixed chat template verified against - TITO's append-only invariants. The `default` value skips auto-resolution - and uses the model's HF-native chat template; use it only if that native - template already satisfies append-only. -- `--tito-allowed-append-roles` (default `tool`) restricts which roles your - `run_agent` may append after the first assistant message. Extend it to - match your conversation pattern; see "Session server message constraints" - below for valid values and typical role sets. Adding `user` or `system` - typically selects a different bundled template (or different kwargs) - within the same family, since broader role surfaces need stricter - rendering rules to keep append-only valid. - -### Common pitfalls - -- Do **not** set `logprob_start_len=0` — it forces SGLang to compute - logprobs for every prompt token, which destroys the prefix cache and - hurts performance. - -## 6. Session server message constraints - -The session server enforces two invariants on every request's `messages`. -Violations raise `MessageValidationError`. - -- **Append-only.** Each new request's `messages` must contain the previous - turn's full `messages` as an exact prefix (same roles, same - template-relevant content). You can extend at the tail; you cannot edit, - reorder, or drop earlier messages. This is how TITO can reuse tokens - across turns: the stored token prefix is only valid if the client - confirms the same message prefix on every request. - -- **Allowed append roles.** After the first assistant message, every newly - appended message must have a role in `--tito-allowed-append-roles` - (default `{tool}`; valid values: `tool`, `user`, `system`). Typical usage: - - - Pure tool-calling agent — `tool` (default). - - Multi-turn chat with mid-conversation user follow-ups — add `user`. - - Mid-conversation system reminders — also add `system`. - -Both invariants depend on the chat template's rendering behavior under the -chosen role surface. Adding miles support for a new model family therefore -involves both updating the chat template and adapting the TITO tokenizer -subclass for that family. diff --git a/docs/en/developer_guide/debug.md b/docs/en/developer_guide/debug.md deleted file mode 100644 index 6317819b6b6..00000000000 --- a/docs/en/developer_guide/debug.md +++ /dev/null @@ -1,51 +0,0 @@ -# Debugging - -## Aligning Precision - -During the development of miles, it is often necessary to check if the model's precision is correct. This can be verified in the following ways: - -1. **First Training Step** - 1. Check if the generated `rollout` is coherent. If not, there are two possible reasons: - * Parameters were not loaded correctly. You need to check the logs for a confirmation that Megatron successfully loaded the checkpoint (ckpt). - * There was an error in updating the parameters. You can check if all parameters were converted and mapped correctly, or if the parameter names were converted according to the parallelization strategy (e.g., when `pp_size > 1`, check if the layer IDs for the parameters provided by the second stage are correct). A thorough method is to save all parameters in the `load_weights` implementation of the corresponding model in SGLang and verify that they are consistent with the loaded checkpoint. - * If all parameters are updated correctly and the problem persists, it's possible that some special buffers in SGLang were released during the release process. - * If you are testing with a pretrained model, you can switch to an instruct version of a model with the same architecture to see if this garbled output is specific to the pretrained model. - - 2. Check the printed rollout stats to see if `log_probs` and `ref_log_probs` are exactly equal (meaning KL divergence is 0 in the first step) and their values are small. - * If they are not exactly equal, it is usually caused by certain non-deterministic kernels in the Transformer Engine, for example: - * In some versions of Transformer Engine (TE), Megatron requires `--attention-backend flash` to enforce the use of Flash Attention, thereby avoiding numerical instability from the fused attention under Context Parallelism (CP). - * If the values are large (e.g., > 1), there are generally two possibilities: - * If the value is extremely large, there is likely a problem with the training configuration. - * If the value is only slightly larger than the SFT loss, for example, if the log probability of an instruct model reaches 0.8, it might be because the data does not conform to the trained chat template or does not match the cold-start distribution. - - 3. When running one inference step per training step (`num_steps_per_rollout == 1`), check if the KL divergence is 0 and if the `grad_norm` is small. - * This is basically due to some Megatron / TE related bugs, for example: - * Mixture of Experts (MoE) requires enabling `--moe-permute-fusion`. - -2. **Second Training Step** - 1. For integrated training and inference, check if the second step can be loaded correctly and whether it results in an Out of Memory (OOM) error. - -## Separate Debugging for Training and Inference - -miles supports debugging the training and inference parts separately, which allows for the following: - -* When tuning/debugging the inference part, you can start the task with only a few GPUs. -* When tuning/debugging the training part, you can ensure the model input is fixed, removing the randomness of rollouts. - -Specifically, miles currently provides the following parameters for separate debugging: - -1. `--debug-rollout-only` - - When enabled, miles will not load Megatron and will only initialize SGLang. You can use this method to debug the inference part. - -2. `--debug-train-only` - - When enabled, miles will not load SGLang and will only initialize Megatron. You can use this method to debug the training part. - -3. `--save-debug-rollout-data /your/saved/debug/data_{rollout_id}.pt` - - When enabled, the results of each rollout will be saved. This can be used in conjunction with `--debug-rollout-only`. Note that the data is saved using the format: `args.save_debug_rollout_data.format(rollout_id=rollout_id)`. - -4. `--load-debug-rollout-data /your/saved/debug/data_{rollout_id}.pt` - - When enabled, data will be loaded from `args.load_debug_rollout_data.format(rollout_id=rollout_id)`, and SGLang will not be initialized (automatically setting `debug_train_only=True`). This method allows you to fix the input for the training part to tune it, for example, by switching between different parallelization strategies. \ No newline at end of file diff --git a/docs/en/developer_guide/migration.md b/docs/en/developer_guide/migration.md deleted file mode 100644 index 6b52832c461..00000000000 --- a/docs/en/developer_guide/migration.md +++ /dev/null @@ -1,50 +0,0 @@ -# Migration Guide - -## Train Loop: Sync → Async - -### What is changed - -The train loop (`train.py`, `train_async.py`) and `RayTrainGroup` now use Python async/await instead of sync `ray.get()`. - -### Why it is changed - -Python async is more expressive than sync code with `ray.get`. As a concrete example, in fault tolerance, we need to capture ray actor results and do retries when calling `actor_model.train`, while still allowing it to be overlapped freely with `critic_model.train`. This is hard to achieve without Python async. - -### How to mechanically migrate - -**1. Make the train function async:** - -```python -# Before # After -def train(args): async def train(args): - ... ... - -if __name__ == "__main__": if __name__ == "__main__": - train(parse_args()) asyncio.run(train(parse_args())) -``` - -**2. `ray.get(x)` → `await x`, drop the `async_` prefix, and add `await` on group methods that previously had none:** - -```python -ray.get(group.async_init(...)) → await group.init(...) -ray.get(group.async_train(...)) → await group.train(...) -group.save_model(...) → await group.save_model(...) -group.update_weights() → await group.update_weights() -ray.get(rollout_manager.generate.remote(id)) → await rollout_manager.generate.remote(id) -# Same pattern for offload, onload, clear_memory, connect, set_rollout_manager -``` - -**3. Dispatch handles:** replace `handle = group.async_fn(...)` with `task = await eager_create_task(group.fn(...))`. - -```python -# Before # After -handle = critic.async_train(...) task = await eager_create_task(critic.train(...)) -ray.get(actor.async_train(...)) await actor.train(...) -ray.get(handle) await task -``` - -**4. `create_training_models` is now async:** - -```python -actor, critic = await create_training_models(args, pgs, rollout_manager) -``` diff --git a/docs/en/examples/deepseek-r1.md b/docs/en/examples/deepseek-r1.md deleted file mode 100644 index 19b418e97ce..00000000000 --- a/docs/en/examples/deepseek-r1.md +++ /dev/null @@ -1,209 +0,0 @@ -# DeepSeek R1 with 128xH100 - - -This is an example of doing DeepSeek R1 RL training using 128xH100 GPUs. - -We will use bf16 for training, and an fp8 format with 128x128 blockwise quantization for inference. The maximum response length is 32k, and dynamic sampling will be used to filter data during training. - -Regarding parallelism, for sglang we will enable EP64, activate dp attention, and deepep. For the Megatron part, we will use TP8, PP4, EP32, and CP4. - -⚠️ To save GPU memory, we will use CPU Adam. Each node (8xH100) will occupy 1.4\~1.5TB of host memory. If a single machine's host memory is insufficient, this can be resolved by adding more GPUs to expand the parallelism. - -## Environment Setup - -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`): - -```bash -hf download deepseek-ai/DeepSeek-R1 --local-dir $BASE_DIR/DeepSeek-R1 -``` - -The Hugging Face checkpoint for DeepSeek-R1 is in a block-quantized fp8 format. To convert it into a torch_dist format that Megatron can load, you first need to convert it to a bf16 Hugging Face checkpoint: - -```bash -cd miles/ -python tools/fp8_cast_bf16.py --input-fp8-hf-path $BASE_DIR/DeepSeek-R1 --output-bf16-hf-path $BASE_DIR/DeepSeek-R1-bf16/ -``` - -Next, we need to convert the bf16 version of DeepSeek-R1 into the torch_dist format. Specifically, execute the following on 4 separate nodes: - -```bash -cd miles/ -source scripts/models/deepseek-v3.sh -PYTHONPATH=/root/Megatron-LM/ torchrun \ - --nproc-per-node 8 \ - --master-addr ${MASTER_ADDR} --master-port 12345 \ - --nnodes=4 --node-rank ${NODE_RANK} \ - tools/convert_hf_to_torch_dist.py \ - ${MODEL_ARGS[@]} \ - --tensor-model-parallel-size 1 \ - --pipeline-model-parallel-size 8 \ - --expert-tensor-parallel-size 1 \ - --expert-model-parallel-size 4 \ - --decoder-first-pipeline-num-layers 7 \ - --decoder-last-pipeline-num-layers 6 \ - --hf-checkpoint $BASE_DIR/DeepSeek-R1-bf16/ \ - --save $BASE_DIR/DeepSeek-R1_torch_dist/ -``` - -Here, `MASTER_ADDR` is the IP of node0, and `NODE_RANK` indicates the node's index, both configured similarly to a multi-node `torchrun` setup. - -## Executing the Training - -On node0, run: - -```bash -cd miles/ -bash scripts/run-deepseek-r1.sh -``` - -On other nodes, you need to join the Ray cluster with the following command: - -```bash -ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 --node-ip-address ${WORKER_IP} --disable-usage-stats" -``` - -Alternatively, if you have a list of all node IPs, for example, an MPI hostfile (where each line is `ip slot=8`), you can add the following commands after the `ray start --head` command in `scripts/run-deepseek-r1.sh`. This allows you to execute the training entirely from node0: - -```bash -for WORKER_IP in $(awk '{print $1}' $BASE_DIR/mpi_hostfile); do - if [[ "$WORKER_IP" == "$MASTER_ADDR" ]]; 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" & -done -wait -``` - -### Parameter Introduction - -```bash -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](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 - -```bash -CKPT_ARGS=( - # HF ckpt required by sglang, we also read the tokenizer from here - --hf-checkpoint $BASE_DIR/DeepSeek-R1/ - #--hf-checkpoint $BASE_DIR/DeepSeek-R1-bf16/ - --ref-load $BASE_DIR/DeepSeek-R1_torch_dist/ - # Actor's load directory, if empty, it will read from `ref_load` - --load $BASE_DIR/DeepSeek-R1_miles/ - --save $BASE_DIR/DeepSeek-R1_miles/ - --save-interval 20 -) -``` - -miles will perform online quantization during training based on the quantization configuration in `hf_checkpoint`. For instance, in the current example, we are using the fp8 checkpoint of DeepSeek R1. This means that when updating parameters, we will first perform blockwise quantization on the parameters before passing them to sglang. - -#### PERF\_ARGS - -A set of Megatron parallelism parameters. Only `--use-dynamic-batch-size` and `--max-tokens-per-gpu` are added by miles. - -For the Megatron part, we have configured TP8, PP4, CP4, and EP32. Since DeepSeek-R1 has 61 layers, which is not divisible by 4, we have specifically configured the last pipeline stage to have 13 layers. - -`max_tokens_per_gpu` refers to the maximum number of tokens each GPU can process. When `use_dynamic_batch_size` is enabled, it will pack data of varying lengths within a batch as close to `max_tokens_per_gpu`. If a single data item exceeds `max_tokens_per_gpu`, it will form its own batch without truncation. When context parallelism (CP) is enabled, it allows CP GPUs to share a total length of `CP * max_tokens_per_gpu` tokens. - -When `dynamic_batch_size` is enabled, the traditional `micro_batch_size` is ignored. - -⚠️ miles always trains the model using data packing and strictly guarantees per-sample or per-token loss. This means enabling dynamic batch size will not affect the loss calculation. It is recommended to enable it. - -```bash -PERF_ARGS=( - --tensor-model-parallel-size 8 - --sequence-parallel - --pipeline-model-parallel-size 4 - --context-parallel-size 4 - --expert-model-parallel-size 32 - --expert-tensor-parallel-size 1 - --decoder-last-pipeline-num-layers 13 - - --recompute-granularity full - --recompute-method uniform - --recompute-num-layers 1 - - --use-dynamic-batch-size - --max-tokens-per-gpu 16384 -) -``` - -#### GRPO\_ARGS - -Currently, these are some GRPO-related parameters in miles: - -```bash -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 -) -``` - -If you wish to train without loading the reference model, you need to remove `--use-kl-loss` and set `--kl-coef 0.00` (the default value is 0). - -#### OPTIMIZER\_ARGS - -We have configured CPU Adam with the following parameters to save GPU memory. - -```bash -OPTIMIZER_ARGS=( - ... - - --optimizer-cpu-offload - --overlap-cpu-optimizer-d2h-h2d - --use-precision-aware-optimizer -) -``` - -#### SGLANG\_ARGS - -These are the parameters required by sglang. Here, `--rollout-num-gpus-per-engine` basically corresponds to sglang's `tp_size`. Other sglang parameters are passed to miles by adding a `--sglang-` prefix. To fully leverage sglang's large EP inference capabilities, we have added configurations like ep64, dp\_attention dp8, and deepep mode auto. - -The final `--sglang-server-concurrency` is a parameter specific to miles. It is used to prevent the sglang server's concurrent requests from becoming too large and crashing the HTTP server. The default is 512. However, since we now have one server for 8 nodes, we have adjusted it to 1024 to ensure that each dp rank can have a concurrency of 128. - -```bash -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 64 - --sglang-mem-fraction-static 0.7 - --sglang-enable-ep-moe - - # dp attention - --sglang-enable-dp-attention - --sglang-dp-size 8 - --sglang-moe-dense-tp-size 1 - --sglang-enable-dp-lm-head - - # enable deepep for sglang - --sglang-enable-deepep-moe - --sglang-deepep-mode auto - - # make every dp rank have 128 concurrency - --sglang-server-concurrency 1024 -) -``` - -#### MISC\_ARGS - -Some additional Megatron configurations. Note that Megatron's deepep is configured here. - -```bash -MISC_ARGS=( - ... - - # use deepep for megatron - --moe-enable-deepep - --moe-token-dispatcher-type flex -) -``` diff --git a/docs/en/examples/glm4-9B.md b/docs/en/examples/glm4-9B.md deleted file mode 100644 index 36629568f43..00000000000 --- a/docs/en/examples/glm4-9B.md +++ /dev/null @@ -1,280 +0,0 @@ -# GLM4-9B with 8xH100 - -## Environment Setup - -After pulling the `radixark/miles:latest` image, initialize the image environment as follows: - -```bash -cd /root/ -git clone https://github.com/radixark/miles.git -cd miles/ -pip install -e . --no-deps -``` - -Download the model and data: - -```bash -# hf checkpoint -hf download zai-org/GLM-Z1-9B-0414 --local-dir /root/GLM-Z1-9B-0414 - -# 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 -``` - -Convert the Hugging Face checkpoint to a Megatron-loadable Hugging Face checkpoint: - -```bash -# mcore checkpoint -cd /root/miles -source scripts/models/glm4-9B.sh -PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ - ${MODEL_ARGS[@]} \ - --hf-checkpoint /root/GLM-Z1-9B-0414 \ - --save /root/GLM-Z1-9B-0414_torch_dist -``` - -## Run Training - -Execute the training: - -```bash -cd /root/miles -bash scripts/run-glm4-9B.sh -``` - -### Parameter Introduction - -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 - -```bash -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](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: - -```bash -source "${SCRIPT_DIR}/models/glm4-9B.sh" - -MODEL_ARGS += ( --rotary-base 10000 ) -``` - -#### CKPT\_ARGS - -```bash -CKPT_ARGS=( - # HF checkpoint required by sglang; we also read the tokenizer from here - --hf-checkpoint /root/GLM-Z1-9B-0414 - # Checkpoint for the reference model - --ref-load /root/GLM-Z1-9B-0414_torch_dist - # Load directory for the actor; if empty, it will be loaded from `ref_load` - --load /root/GLM-Z1-9B-0414_miles/ - --save /root/GLM-Z1-9B-0414_miles/ - --save-interval 20 -) -``` - -#### ROLLOUT\_ARGS - -```bash -ROLLOUT_ARGS=( - # Prompt dataset, each line is a JSON object - --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl - --input-key prompt - --label-key label - # If the `input_key` in the prompt contains an OpenAI message, - # tokenizer.apply_chat_template(...) will be executed - --apply-chat-template - # Whether to shuffle the data - --rollout-shuffle - - # Reward model type. - # miles provides many types and --custom-rm-path for custom models - --rm-type deepscaler - - # Total number of rollouts to train - --num-rollout 3000 - # Number of prompts in one rollout - --rollout-batch-size 32 - # Number of responses to sample per prompt - # A rollout will have rollout_batch_size * n_samples_per_prompt items - --n-samples-per-prompt 8 - # Rollout sampling parameters - --rollout-max-response-len 8192 - --rollout-temperature 1 - - # Number of training steps corresponding to one rollout - --num-steps-per-rollout 1 - # Whether to balance data during training, which might improve speed - --balance-data -) -``` - -#### EVAL\_ARGS - -During evaluation, most rollout parameters are inherited, but we provide some parameters that can override the rollout configuration, allowing for different sampling strategies for training and evaluation. - -```bash -EVAL_ARGS=( - --eval-interval 5 - --eval-prompt-data /root/aime-2024/aime-2024.jsonl - --n-samples-per-eval-prompt 16 - --eval-max-response-len 16384 - --eval-top-p 1 -) -``` - -#### PERF\_ARGS - -A set of Megatron's parallelism parameters. Only `--use-dynamic-batch-size` and `--max-tokens-per-gpu` are added by miles. - -`max_tokens_per_gpu` specifies the maximum number of tokens each GPU can process. When `use_dynamic_batch_size` is enabled, it will try to pack data of varying lengths within a batch up to `max_tokens_per_gpu`, thus forming a dynamic micro-batch size. If a single data item's length exceeds `max_tokens_per_gpu`, it will form its own batch without being truncated. When context parallelism (CP) is enabled, it allows the CP GPUs to share data with a total length of `CP * max_tokens_per_gpu` tokens. - -When `dynamic_batch_size` is enabled, the traditional `micro_batch_size` is ignored. - -⚠️ miles always trains the model using data packing and strictly guarantees per-sample or per-token loss. This means enabling dynamic batch size will not affect the loss calculation. It is recommended to enable it. - -```bash -PERF_ARGS=( - --tensor-model-parallel-size 2 - --sequence-parallel - --pipeline-model-parallel-size 1 - --context-parallel-size 2 - --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 4608 -) -``` - -#### GRPO\_ARGS - -Here are some GRPO-related parameters: - -```bash -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 - -```bash -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 - -Parameters required by sglang. Here, `--rollout-num-gpus-per-engine` basically corresponds to sglang's `tp_size`. Other sglang parameters are passed to miles by adding the `--sglang-` prefix. - -```bash -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 2 -) -``` - -⚠️ miles uses `sgl-router` to schedule multiple sglang servers. `dp_size` is not supported when DP attention is disabled. - -### Co-located Training and Inference - -In the original script, the resource configuration is as follows: - -```bash -ray job submit ... \ - -- python3 train.py \ - --actor-num-nodes 1 \ - --actor-num-gpus-per-node 4 \ - --rollout-num-gpus 4 \ - ... -``` - -This enables decoupled training and inference, where the training part will use 1 machine with 4 GPUs, and the inference will use another 4 GPUs. - -If you want to use the co-located feature, you need to add `--colocate` and remove `--rollout-num-gpus`: - -```bash -ray job submit ... \ - -- python3 train.py \ - --actor-num-nodes 1 \ - --actor-num-gpus-per-node 8 \ - --colocate \ - ... -``` - -In this case, both training and inference will share these 8 GPUs. - -⚠️ When using co-located training and inference, Megatron will always occupy some GPU memory. Therefore, you need to adjust `--sglang-mem-fraction-static` to reduce the proportion of memory occupied by sglang. - -### Dynamic Sampling - -miles supports more complex sampling schemes, such as the dynamic sampling in [DAPO](https://dapo-sia.github.io/). To enable dynamic sampling, you need to configure: - -```bash - --over-sampling-batch-size ${OVER_SAMPLING_BS} \ - --dynamic-sampling-filter-path \ - miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std \ -``` - -Here, `over_sampling_batch_size` needs to be greater than `rollout_batch_size`. For example: - -```bash - --rollout-batch-size 32 \ - --n-samples-per-prompt 8 \ - --over-sampling-batch-size 64 \ -``` - -The sampling will then directly sample 64 prompts, with 8 samples per prompt. Since miles performs asynchronous sampling internally, we will receive the 8 responses for each prompt sequentially. Upon receiving responses, they will be filtered using the function specified by `dynamic_sampling_filter_path`. If they pass, these 8 data points are kept; otherwise, they are discarded. The function in the example checks if the answers are all correct or all incorrect: - -```python -def check_reward_nonzero_std(args, samples: list[Sample], **kwargs): - rewards = [sample.reward for sample in samples] - return torch.tensor(rewards, dtype=torch.float).std() > 0.0 -``` - -When we have received 32 \* 8 data points, we will immediately stop sampling and will not wait for the remaining data to be sampled. If more than 32 prompts' worth of data is discarded (leaving fewer than 32 prompts' worth), we will then sample another 64 prompts. - -### Partial Rollout - -During the process of dynamic sampling, a large number of requests are aborted prematurely. We can configure the `--partial-rollout` parameter to save these partially generated requests to a data buffer. In the next rollout, these requests can be retrieved to continue data generation, thereby further optimizing performance. - -You can customize how data is retrieved from the buffer by configuring the `--buffer-filter-path`. The default function is: - -```python -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 -``` - -This means that each time, the data corresponding to the first `num_samples` prompts is retrieved, totaling `num_samples * n_samples_per_prompt` items. - -⚠️ The `sample.metadata` of each partial rollout sample stores the rollout ID from its initial generation, which can be used for data filtering. diff --git a/docs/en/examples/glm4.5-355B-A32B.md b/docs/en/examples/glm4.5-355B-A32B.md deleted file mode 100644 index b5336ee177b..00000000000 --- a/docs/en/examples/glm4.5-355B-A32B.md +++ /dev/null @@ -1,190 +0,0 @@ -# GLM-4.5 with 64xH100 - - -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). - -First, you will need to download GLM-4.5 to a directory accessible by all machines (hereinafter referred to as `$BASE_DIR`): - -```bash -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: - -```bash -cd miles/ -source scripts/models/glm4.5-355B-A32B.sh -PYTHONPATH=/root/Megatron-LM/ torchrun \ - --nproc-per-node 8 \ - --master-addr ${MASTER_ADDR} --master-port 12345 \ - --nnodes=2 --node-rank ${NODE_RANK} \ - tools/convert_hf_to_torch_dist.py \ - ${MODEL_ARGS[@]} \ - --hf-checkpoint $BASE_DIR/GLM-4.5-355B-A32B/ \ - --save $BASE_DIR/GLM-4.5-355B-A32B_torch_dist/ -``` - -Here, `MASTER_ADDR` is the IP of node0, and `NODE_RANK` indicates the node's index, both configured similarly to a multi-node `torchrun` setup. - -## Executing the Training - -On node0, run: - -```bash -cd miles/ -bash scripts/run-glm4.5-355B-A32B.sh -``` - -On other nodes, you need to join the Ray cluster with the following command: - -```bash -ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 --node-ip-address ${WORKER_IP} --disable-usage-stats" -``` - -Alternatively, if you have a list of all node IPs, for example, an MPI hostfile (where each line is `ip slot=8`), you can add the following commands after the `ray start --head` command in `scripts/run-glm4.5-355B-A32B.sh.sh`. This allows you to execute the training entirely from node0: - -```bash -for WORKER_IP in $(awk '{print $1}' $BASE_DIR/mpi_hostfile); do - if [[ "$WORKER_IP" == "$MASTER_ADDR" ]]; 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" & -done -wait -``` - -### Parameter Introduction - -```bash -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](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 - -A set of Megatron parallelism parameters. Only `--use-dynamic-batch-size` and `--max-tokens-per-gpu` are added by miles. - -For the Megatron part, we have configured TP8, PP4, CP2, and EP16. - -`max_tokens_per_gpu` refers to the maximum number of tokens each GPU can process. When `use_dynamic_batch_size` is enabled, it will pack data of varying lengths within a batch as close to `max_tokens_per_gpu`. If a single data item exceeds `max_tokens_per_gpu`, it will form its own batch without truncation. When context parallelism (CP) is enabled, it allows CP GPUs to share a total length of `CP * max_tokens_per_gpu` tokens. - -When `dynamic_batch_size` is enabled, the traditional `micro_batch_size` is ignored. - -⚠️ miles always trains the model using data packing and strictly guarantees per-sample or per-token loss. This means enabling dynamic batch size will not affect the loss calculation. It is recommended to enable it. - -```bash -PERF_ARGS=( - --tensor-model-parallel-size 8 - --sequence-parallel - --pipeline-model-parallel-size 4 - --context-parallel-size 2 - --expert-model-parallel-size 16 - --expert-tensor-parallel-size 1 - - --recompute-granularity full - --recompute-method uniform - --recompute-num-layers 1 - - --use-dynamic-batch-size - --max-tokens-per-gpu 16384 -) -``` - -#### GRPO\_ARGS - -Currently, these are some GRPO-related parameters in miles: - -```bash -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 -) -``` - -If you wish to train without loading the reference model, you need to remove `--use-kl-loss` and set `--kl-coef 0.00` (the default value is 0). - -#### OPTIMIZER\_ARGS - -We have configured CPU Adam with the following parameters to save GPU memory. - -```bash -OPTIMIZER_ARGS=( - ... - - --optimizer-cpu-offload - --overlap-cpu-optimizer-d2h-h2d - --use-precision-aware-optimizer -) -``` - -#### SGLANG\_ARGS - -These are the parameters required by sglang. Here, `--rollout-num-gpus-per-engine` basically corresponds to sglang's `tp_size`. Other sglang parameters are passed to miles by adding a `--sglang-` prefix. - -```bash -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 32 - --sglang-mem-fraction-static 0.7 - --sglang-enable-dp-attention - --sglang-dp-size 4 -) -``` - -#### MISC\_ARGS - -Some additional Megatron configurations. Note that Megatron's deepep is configured here. - -```bash -MISC_ARGS=( - ... - - # use deepep for megatron - --moe-enable-deepep - --moe-token-dispatcher-type flex -) -``` - -## FP8 Rollout - -The open-source FP8 checkpoint of GLM-4.5 is of per-channel quantization, which could not enable deepep in SGLang. We can use the tool scripts within miles to convert an FP8 checkpoint of 128x128 per-block quant: - -```bash -python tools/convert_hf_to_fp8.py \ - --model-dir $BASE_DIR/GLM-4.5-355B-A32B/ \ - --save-dir $BASE_DIR/GLM-4.5-355B-A32B-FP8/ \ - --strategy block --block-size 128 128 \ - --max-workers 4 -``` - -Then, simply change `--hf-checkpoint` to `$BASE_DIR/GLM-4.5-355B-A32B-FP8/` to enable FP8 rollout. - -And exemplar `SGLANG_ARGS` for FP8 is: - -```bash -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 32 - --sglang-mem-fraction-static 0.7 - --sglang-enable-dp-attention - --sglang-dp-size 32 - --sglang-ep-size 32 - --sglang-moe-dense-tp-size 1 - --sglang-enable-dp-lm-head - --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 128) - - --sglang-moe-a2a-backend deepep - --sglang-deepep-mode auto -) -``` diff --git a/docs/en/examples/qwen3-30B-A3B.md b/docs/en/examples/qwen3-30B-A3B.md deleted file mode 100644 index 2f731d461f6..00000000000 --- a/docs/en/examples/qwen3-30B-A3B.md +++ /dev/null @@ -1,120 +0,0 @@ -# Qwen3-30B-A3B with 8xH100 - - -## 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. - -To convert huggingface checkpoint to torch_dist, please try: - -```bash -cd miles/ -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 \ - ${MODEL_ARGS[@]} \ - --hf-checkpoint /root/Qwen3-30B-A3B/ \ - --save /root/Qwen3-30B-A3B_torch_dist/ -``` - -## Run Training - -Execute the training script: - -```bash -cd /root/miles -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](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: - - ```bash - OPTIMIZER_ARGS=( - ... - --optimizer-cpu-offload - --overlap-cpu-optimizer-d2h-h2d - --use-precision-aware-optimizer - ) - ``` - -2. Enable MoE optimization supported by Megatron. The current configuration is tp4, ep8: - - ```bash - 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 - ... - ) - ``` - -3. Enable MoE optimization supported by SGLang. The current configuration is ep8: - - ```bash - SGLANG_ARGS=( - --rollout-num-gpus-per-engine 8 - --sglang-mem-fraction-static 0.7 - --sglang-enable-ep-moe - --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) - ) - ``` - - Similarly, you can also add DP attention, for example, by configuring: - - ```bash - --sglang-enable-dp-attention - --sglang-dp-size 8 - ``` - -### BF16 Training with FP8 Inference - -miles also supports BF16 training with FP8 inference. For the Qwen3-30B-A3B model, you just need to download the following model: - -```bash -hf download Qwen/Qwen3-30B-A3B-FP8 --local-dir /root/Qwen3-30B-A3B-FP8 -``` - -And replace `--hf-checkpoint` with: - -```bash -#--hf-checkpoint /root/Qwen3-30B-A3B ---hf-checkpoint /root/Qwen3-30B-A3B-FP8 -``` - -This will trigger FP8 inference. Currently, we directly cast the BF16 weights to FP8. In the future, we will gradually add more sophisticated quantization schemes that have less impact on precision. - -⚠️ The Megatron checkpoint for training still needs to be the one that was originally converted from the BF16 Hugging Face model. - -### Multi-Node Support - -For a multi-node environment, the following modifications are necessary: - - - Place the training model and data on a path accessible by all nodes. - - Set the `MASTER_ADDR` to an address that is accessible by all nodes. - - Remove configurations related to CPU Adam. This is because a distributed optimizer is used, which significantly reduces the optimizer's video memory (VRAM) usage in a multi-node setup. - -In addition, you can make the following changes: - - - When the total number of GPUs is not a multiple or divisor of the total number of experts, you can use `--sglang-ep-num-redundant-experts` to add redundant experts. For example, in a 24-GPU scenario, you can configure it as follows: - - ```bash - SGLANG_ARGS=( - --rollout-num-gpus-per-engine 24 - --sglang-mem-fraction-static 0.7 - --sglang-enable-ep-moe - --sglang-enable-dp-attention - --sglang-dp-size 3 - - --sglang-moe-dense-tp-size 1 - --sglang-enable-dp-lm-head - --sglang-ep-num-redundant-experts 16 - ) - ``` diff --git a/docs/en/examples/qwen3-4B.md b/docs/en/examples/qwen3-4B.md deleted file mode 100644 index 8374de4beec..00000000000 --- a/docs/en/examples/qwen3-4B.md +++ /dev/null @@ -1,299 +0,0 @@ -# Qwen3-4B with 8xH100 - -## Environment Setup - -After pulling the `radixark/miles:latest` image, initialize the image environment as follows: - -```bash -cd /root/ -git clone https://github.com/radixark/miles.git -cd miles/ -pip install -e . --no-deps -``` - -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 -``` - -Convert the Hugging Face checkpoint into a format that Megatron can load: - -```bash -# 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/Qwen3-4B \ - --save /root/Qwen3-4B_torch_dist -``` - -## Run Training - -Execute the training script: - -```bash -cd /root/miles -bash scripts/run-qwen3-4B.sh -``` - -### Parameter Introduction - -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 - -```bash -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](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: - -```bash -source "${SCRIPT_DIR}/models/qwen3-4B.sh" - -MODEL_ARGS += ( --rotary-base 10000 ) -``` - -#### CKPT\_ARGS - -```bash -CKPT_ARGS=( - # HF checkpoint required by sglang; we also read the tokenizer from here - --hf-checkpoint /root/Qwen3-4B - # Checkpoint for the reference model - --ref-load /root/Qwen3-4B_torch_dist - # Load directory for the actor; if empty, it will be loaded from `ref_load` - --load /root/Qwen3-4B_miles/ - --save /root/Qwen3-4B_miles/ - --save-interval 20 -) -``` - -#### ROLLOUT\_ARGS - -```bash -ROLLOUT_ARGS=( - # Prompt dataset, each line is a JSON object - --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl - --input-key prompt - --label-key label - # If the `input_key` in the prompt contains an OpenAI message, - # tokenizer.apply_chat_template(...) will be executed - --apply-chat-template - # Whether to shuffle the data - --rollout-shuffle - - # Reward model type. - # miles provides many types and --custom-rm-path for custom models - --rm-type deepscaler - - # Total number of rollouts to train - --num-rollout 3000 - # Number of prompts in one rollout - --rollout-batch-size 32 - # Number of responses to sample per prompt - # A rollout will have rollout_batch_size * n_samples_per_prompt samples - --n-samples-per-prompt 8 - # Rollout sampling parameters - --rollout-max-response-len 8192 - --rollout-temperature 1 - - # Number of training steps corresponding to one rollout - --num-steps-per-rollout 1 - # Whether to balance data during training, which might improve speed - --balance-data -) -``` - -#### EVAL\_ARGS - -During evaluation, most rollout parameters are inherited, but we provide some parameters that can override the rollout configuration to allow for different sampling strategies for training and evaluation. - -```bash -EVAL_ARGS=( - --eval-interval 5 - --eval-prompt-data /root/aime-2024/aime-2024.jsonl - --n-samples-per-eval-prompt 16 - --eval-max-response-len 16384 - --eval-top-p 1 -) -``` - -#### PERF\_ARGS - -This is a set of Megatron's parallelism parameters. Only `--use-dynamic-batch-size` and `--max-tokens-per-gpu` are added by miles. - -`max_tokens_per_gpu` specifies the maximum number of tokens each GPU can process. When `use_dynamic_batch_size` is enabled, it attempts to pack data of varying lengths within a batch as close to `max_tokens_per_gpu` as possible, thus forming a dynamic micro-batch size. If a single data item exceeds `max_tokens_per_gpu`, it forms its own batch without being truncated. When context parallelism (CP) is enabled, it allows the CP GPUs to share a total of `CP * max_tokens_per_gpu` tokens. - -When `dynamic_batch_size` is enabled, the traditional `micro_batch_size` is ignored. - -⚠️ miles always trains the model using data packing and strictly guarantees per-sample or per-token loss. This means enabling dynamic batch size will not affect the loss calculation. It is recommended to enable it. - -```bash -PERF_ARGS=( - --tensor-model-parallel-size 2 - --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 - - # --micro-batch-size 1 - --use-dynamic-batch-size - --max-tokens-per-gpu 9216 -) -``` - -#### GRPO\_ARGS - -Here are some GRPO-related parameters: - -```bash -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 - -```bash -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 - -These are the parameters required by sglang. Here, `--rollout-num-gpus-per-engine` basically corresponds to sglang's `tp_size`. Other sglang parameters are passed to miles by adding the `--sglang-` prefix. - -```bash -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 2 - --sglang-mem-fraction-static 0.7 -) -``` - -⚠️ miles uses `sgl-router` to schedule multiple sglang servers. `dp_size` is not supported when DP attention is disabled. - -### Dynamic Sampling - -miles supports more complex sampling schemes, such as the dynamic sampling in [DAPO](https://dapo-sia.github.io/). To enable dynamic sampling, you need to configure: - -```bash - --over-sampling-batch-size ${OVER_SAMPLING_BS} \ - --dynamic-sampling-filter-path \ - miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std \ -``` - -Here, `over_sampling_batch_size` needs to be greater than `rollout_batch_size`. For example, you can configure it as: - -```bash - --rollout-batch-size 32 \ - --n-samples-per-prompt 8 \ - --over-sampling-batch-size 64 \ -``` - -In this case, the sampling process will directly sample 64 prompts, with 8 samples per prompt. Since miles performs asynchronous sampling internally, we will receive the 8 responses for each prompt sequentially. Upon receiving the responses, the function specified by `dynamic_sampling_filter_path` is used for filtering. If the samples pass the filter, these 8 data points are kept; otherwise, they are discarded. The function in the example checks if the rewards for the samples are not all identical (i.e., not all correct or all incorrect): - -```python -def check_reward_nonzero_std(args, samples: list[Sample], **kwargs): - rewards = [sample.reward for sample in samples] - return torch.tensor(rewards, dtype=torch.float).std() > 0.0 -``` - -When we have received 32 \* 8 data points, we will immediately stop the current sampling round and will not wait for the remaining data to be sampled. If more than 32 prompts' worth of data is discarded (leaving fewer than 32 prompts' worth), we will then sample another 64 prompts. - -### Partial Rollout - -During the process of dynamic sampling, a large number of requests are aborted prematurely. We can configure the `--partial-rollout` parameter to save these partially generated requests to a data buffer. In the next rollout, these requests can be retrieved to continue data generation, thereby further optimizing performance. - -You can customize how data is retrieved from the buffer by configuring the `--buffer-filter-path`. The default function is: - -```python -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 -``` - -This means that each time, the data corresponding to the first `num_samples` prompts is retrieved, totaling `num_samples * n_samples_per_prompt` items. - -⚠️ The `sample.metadata` of each partial rollout sample stores the rollout ID from its initial generation, which can be used for data filtering. - -### Decoupled Training and Inference - -In the original script, the resource configuration is as follows: - -```bash -ray job submit ... \ - -- python3 train.py \ - --actor-num-nodes 1 \ - --actor-num-gpus-per-node 8 \ - --colocate \ - ... -``` - -This enables co-located training and inference, where the training part uses 1 machine with 8 GPUs, and inference shares these 8 GPUs with training. - -If you want to use the decoupled training and inference feature, you need to remove `--colocate` and configure `--rollout-num-gpus`. For example: - -```bash -ray job submit ... \ - -- python3 train.py \ - --actor-num-nodes 1 \ - --actor-num-gpus-per-node 2 \ - --rollout-num-gpus 6 \ - ... -``` - -In this case, 2 GPUs will be allocated for training, and 6 GPUs will be allocated for inference. - -⚠️ If the concurrency on each sglang server is too high, it may exceed sglang's default CUDA graph concurrency limit (the default maximum is 160), which will affect inference speed. You can adjust this in the following two ways: - -1. Use `--sglang-server-concurrency` to limit the maximum number of concurrent requests sent to a single sglang server. For example: - - ```bash - --sglang-server-concurrency 160 - ``` - -2. Use `--sglang-cuda-graph-bs` (which corresponds to sglang's native `--cuda-graph-bs` argument) to increase the number of CUDA graphs initialized by sglang. For example: - - ```bash - --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) - ``` - -### Asynchronous Training - -When you separate training and inference, you may notice that the training and inference GPUs are always waiting for each other. To prevent these resources from being idle, we can enable asynchronous training. This can be done by changing `train.py` to `train_async.py` in the startup script. By doing this, miles will generate data for the next rollout while training on the current one. - -The only difference between `train.py` and `train_async.py` lies in the synchronization logic of the training loop. We achieve this by using Ray's asynchronous features (`.remote`, `ray.get`). diff --git a/docs/en/examples/qwen3-4b-base-openhermes.md b/docs/en/examples/qwen3-4b-base-openhermes.md deleted file mode 100644 index a4b6237b68f..00000000000 --- a/docs/en/examples/qwen3-4b-base-openhermes.md +++ /dev/null @@ -1,86 +0,0 @@ -# SFT Qwen3-4B-Base - - -## Environment Preparation - -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`. - -```python -from datasets import load_dataset - -ds = load_dataset("teknium/OpenHermes-2.5")["train"] - -def convert(sample): - conversations = sample["conversations"] - - def convert_role(role): - if role == "human": - return "user" - elif role == "gpt": - return "assistant" - elif role == "system": - return "system" - else: - raise ValueError(f"Unknown role: {role}") - - messages = [ - { - "role": convert_role(turn["from"]), - "content": turn["value"], - } - for turn in conversations - ] - - return {"messages": messages} - -ds = ds.map(convert) -ds.to_parquet("/root/openhermes2_5.parquet") -``` - -## Execute Training - -Execute the training: - -```bash -cd /root/miles -bash script/run-qwen3-4B-base-sft.sh -``` - -### Parameter Introduction - -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. - -2. Renamed `ROLLOUT_ARGS` to `SFT_ARGS` and configured it as follows: - - ```bash - SFT_ARGS=( - --rollout-function-path miles.rollout.sft_rollout.generate_rollout - --prompt-data /root/openhermes2_5.parquet - --input-key messages - --rollout-shuffle - --num-epoch 3 - --rollout-batch-size 128 - --global-batch-size 128 - - --loss-type sft_loss - --calculate-per-token-loss - --disable-compute-advantages-and-returns - --debug-train-only - ) - ``` - - SFT actually reuses the custom rollout functionality of miles. By using `--rollout-function-path`, the data generation part is switched from the RL rollout that uses `sglang` to the SFT version that reads data from a file, which is `miles.rollout.sft_rollout.generate_rollout`. - - For SFT, it is recommended to set `rollout_batch_size` and `global_batch_size` to the same value and not to configure `n_samples_per_prompt`. This is equivalent to training one batch right after reading one batch. - - `miles` also supports different loss types, and we configure the SFT loss using `--loss-type sft_loss`. - - As for `--calculate-per-token-loss`, this is because `miles` defaults to calculating the per-sample mean for GRPO. In general SFT training, the average is taken over all unmasked tokens in a batch, so it is recommended to configure this. - - Finally, `--disable-compute-advantages-and-returns` indicates that there is no need to pre-calculate log probabilities during the SFT process, and `--debug-train-only` means that `sglang` does not need to be initialized. - -3. Used `train_async.py` instead of `train.py`. This is to leverage the asynchronous training process to implement data prefetching. diff --git a/docs/en/get_started/customization.md b/docs/en/get_started/customization.md deleted file mode 100644 index 8aa63c23fbd..00000000000 --- a/docs/en/get_started/customization.md +++ /dev/null @@ -1,442 +0,0 @@ -# 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. | -| [`--custom-model-provider-path`](#20-model-provider---custom-model-provider-path) | Path to a custom function that replaces the default model provider. | - -## Detailed Interface Reference - -### 1. Rollout Function (`--rollout-function-path`) - -**Default**: -```python -if enable_experimental_rollout_refactor(): - miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn -else: - 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 - ---- - -### 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)) | - -For detailed explanation of R3 and MilesRouter, see [Miles Router](../advanced/miles-router.md). - ---- - -### 20. Model Provider (`--custom-model-provider-path`) - -**Default**: `None` - -**Purpose**: Path to a custom function that replaces the default model provider (e.g., `'my_module.my_provider'`). The function must return a GPTModel. - -**Signature**: -```python -def custom_model_provider(pre_process: bool, post_process: bool, vp_stage: int | None = None) -> GPTModel -``` - - diff --git a/docs/en/get_started/gen_endpoint.md b/docs/en/get_started/gen_endpoint.md deleted file mode 100644 index 8a45063a9f7..00000000000 --- a/docs/en/get_started/gen_endpoint.md +++ /dev/null @@ -1,104 +0,0 @@ -# Gen Endpoint Usage - -This document covers generate_hub usage for the `/generate` endpoint. For OpenAI -format usage, see `docs/en/agentic/oai_endpoint.md`. - -## 1. What generate_hub is - -`miles/rollout/generate_hub/` contains reusable generate functions that plug into -rollout through `--custom-generate-function-path`. They use the refactor -interface (`GenerateFnInput` / `GenerateFnOutput`) and are meant to be composed -with custom agents, tool use, or multi-turn logic. - -Key types and entry points: - -- `miles/rollout/base_types.py` defines `GenerateFnInput` and `GenerateFnOutput`. -- `miles/rollout/inference_rollout/inference_rollout_common.py` builds a - `GenerateState` and calls the generate function. -- `MILES_EXPERIMENTAL_ROLLOUT_REFACTOR=1` enables the new path (see - `examples/openai_format/*.sh`). - -## 2. Generate function basics - -The intended abstraction is: - -1. The rollout engine provides a `GenerateFnInput` with: - - `state` (tokenizer, processor, args, sampling defaults) - - `sample` (prompt, current tokens, response, status) - - `sampling_params` (max_new_tokens, temperature, top_p, etc.) -2. The generate function focuses only on: - - turning the sample into a model request - - executing the request (SGLang `/generate` or OpenAI format) - - updating the `Sample` with tokens, logprobs, loss mask, and status - -Minimal skeleton: - -```python -from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput -from miles.utils.types import Sample - -async def generate(input: GenerateFnInput) -> GenerateFnOutput: - args = input.args - sample = input.sample - sampling_params = input.sampling_params - - # 1) build request from prompt and sampling params - # 2) call backend - # 3) update sample.tokens, sample.response, sample.rollout_log_probs, sample.loss_mask, sample.status - - return GenerateFnOutput(samples=sample) - -def _add_arguments(parser): - parser.add_argument("--your-arg", type=str) - -generate.add_arguments = _add_arguments -``` - -Notes: - -- `generate.add_arguments = _add_arguments` is the hook for custom CLI flags. - Add any arguments you want; they are parsed into `input.args` and can be used - freely by your generator without touching rollout core code. -- Use `compute_prompt_ids_from_sample` and `compute_request_payload` from - `miles/rollout/generate_utils/generate_endpoint_utils.py` to build requests - for the `/generate` endpoint. -- If you want to return multiple samples, set `--generate-multi-samples` and - return a list. - -## 3. /generate endpoint examples - -Examples (library side): - -- `miles/rollout/generate_hub/single_turn.py` - - Single-turn generation using `/generate`. - - Works with text or multimodal prompts. -- `miles/rollout/generate_hub/multi_turn.py` - - Multi-turn tool calling using `/generate`. - - CLI flags: `--generate-max-turns`, `--generate-tool-specs-path`, - `--generate-tool-call-parser`, `--generate-execute-tool-function-path`, - `--generate-multi-samples`. -- `miles/rollout/generate_hub/benchmarkers.py` - - Benchmark helper that forces random output sequence length (OSL). - -## 4. Radix tree middleware helper (full TITO for `/generate`) - -Full TITO caching for the `/generate` endpoint is provided by the radix tree -middleware. This is unrelated to session middleware and works only on the -`/generate` and `/retrieve_from_text` routes. - -What it does: - -- Caches token ids and logprobs by prompt text in a radix tree. -- Lets `/generate` requests include `input_tokens` and avoids re-tokenization. -- Enables `update_sample_from_response` to fetch tokens via - `/retrieve_from_text` for training. - -How to enable: - -``` ---use-miles-router \ ---miles-router-middleware-paths miles.router.middleware_hub.radix_tree_middleware.RadixTreeMiddleware -``` - -Make sure `--sglang-router-ip` and `--sglang-router-port` point to the Miles -Router so `/retrieve_from_text` can be reached during rollout. diff --git a/docs/en/get_started/qa.md b/docs/en/get_started/qa.md deleted file mode 100644 index f6d5beea118..00000000000 --- a/docs/en/get_started/qa.md +++ /dev/null @@ -1,72 +0,0 @@ -# FAQ - -1. **Why do I see garbled text during training?** - - This situation generally occurs because Megatron is not loaded correctly. Please check if there is a corresponding checkpoint in the directory specified by `--load` or `--ref-load`. Note that Megatron can only load a directory that contains a `latest_checkpointed_iteration.txt` file. - - If you need to specify a particular iteration, you can refer to the current Megatron usage instructions. Generally, you can specify the step number using `--ckpt-step`. - -2. **Why is my task stuck on the Ray submission page?** - - Please check whether your task is set up for co-located training and inference or decoupled training and inference. - - If it's **co-located** (training and inference share the same GPUs), please check: - - * Whether the `--colocate` parameter is set to enable co-located mode. - * Whether the total number of GPUs for the current task is greater than or equal to `actor_num_nodes * actor_num_gpus_per_node`. - - If it's **decoupled**, please check: - - * Whether the total number of GPUs for the current task is greater than or equal to `actor_num_nodes * actor_num_gpus_per_node + rollout_num_gpus`. - -3. **Why did I encounter an Out-of-Memory (OOM) error during training? What is `max_tokens_per_gpu` for?** - - OOM errors often happen because `max_tokens_per_gpu` is set too high. This parameter defines the maximum number of tokens that can be processed on each GPU during training. If you are concerned about OOM, you can initially set this value to `rollout_max_response_len / cp_size` and then increase it later to improve training efficiency. Note that `--max-tokens-per-gpu` is only active when `--use-dynamic-batch-size` is enabled. - - If you still experience OOM with a small `max_tokens_per_gpu`, check if the data generated in a single pass is too long. You may need to enable context parallelism (CP) with `--context-parallel-size`. If you are using custom data generation, check if the total length of multi-turn generations is much longer than expected. - -4. **During multi-node training, what should I do if the `transformers` library reports it cannot find a model?** - - This usually happens when multiple processes try to read local files simultaneously using methods like `AutoConfig.from_pretrained` or `AutoModelForCausalLM.from_pretrained`, causing file system write conflicts. You can mitigate this issue by setting the `--model-name` argument. - -5. **How do I resume training?** - - Simply set the `--load` directory to your `--save` directory. - -6. **How is the batch size calculated?** - - A single rollout uses `rollout_batch_size` prompts. For each prompt, `n_samples_per_prompt` samples are generated. Therefore, one rollout contains a total of `rollout_batch_size * n_samples_per_prompt` data entries. - - You can use `--num-steps-per-rollout` to determine how many steps to run per rollout. This is equivalent to setting the `global_batch_size` to `rollout_batch_size * n_samples_per_prompt // num_steps_per_rollout`. - -7. **Does miles perform data packing / variable-length (varlen) processing?** - - Yes. Data packing refers to the process of concatenating samples of varying lengths during training to improve GPU utilization. miles performs this operation by default. - -8. **What should I do if the sglang component shows a `Max retries exceeded with url: /get_model_info (Caused by NewConnectionError)` error?** - - This issue primarily stems from port conflicts caused by multiple sglang servers running on a single machine. We are currently working with the sglang team to resolve this. A temporary workaround is to minimize the number of sglang servers on a single machine, for example, by setting `tp=8`. - -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](../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?** - - Please verify that the model corresponding to `--hf-checkpoint` has its stop tokens configured correctly. If not, you can set them using the `--rollout-stop` or `--rollout-stop-token-ids` arguments. - -11. **Sglang shows an `an illegal memory access was encountered` error.** - - 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.** - - This is generally an issue with the torch compiler's cache read/write operations. You can try adding `"TORCHINDUCTOR_FORCE_DISABLE_CACHES": "1"` to the `env_vars` in your Ray configuration. - -13. **Gradient becomes NaN or Inf during training.** - - You can try setting the `--no-check-for-nan-in-loss-and-grad` flag to skip the corresponding training steps. - -14. **NCCL error: `Failed to bind NVLink SHARP (NVLS) Multicast memory ... CUDA error 2 'out of memory'`.** - - This issue has been observed on H100 in colocate mode with piece-wise CUDA graph enabled. Piece-wise CUDA graph is now disabled by default in colocate mode. If you encounter this after explicitly enabling it via `--sglang-enforce-piecewise-cuda-graph`, remove that flag. diff --git a/docs/en/get_started/quick_start.md b/docs/en/get_started/quick_start.md deleted file mode 100644 index e3bf424dda1..00000000000 --- a/docs/en/get_started/quick_start.md +++ /dev/null @@ -1,589 +0,0 @@ -# Quick Start - - -This document will guide you through setting up the environment and getting started with miles within one hour, covering environment configuration, data preparation, training startup, and key code analysis and modifications. - -## Basic Environment Setup - -Since miles may contain temporary patches for sglang/megatron, to avoid potential environment configuration issues, we strongly recommend **users to use our latest Docker image**, which comes pre-configured with all dependencies. - -### Hardware Support - -**miles** supports multiple NVIDIA GPU hardware platforms: - -- **B200 Series**: Fully supported with identical setup steps as H-series GPUs -- **H-Series (H100/H200)**: Official support with comprehensive CI testing and stable performance - -**Important Notes**: -- Latest Docker images are compatible with both B-series and H-series GPUs without additional configuration -- Megatron backend on H-series GPUs has CI protection, thoroughly validated, recommended for production environments -- B-series basic functionality is stable and suitable for development/testing, but currently lacks CI protection -- Both hardware platforms use identical installation and startup procedures - -- For AMD support, please refer to [AMD Usage Tutorial](../platform_support/amd_tutorial.md). - -### Pull and Start Docker Container - -Please execute the following commands to pull the latest image and start an interactive container: - -```shell -# Pull the latest image -docker pull radixark/miles:latest - -# Start the container -docker run --rm --gpus all --ipc=host --shm-size=16g \ - --ulimit memlock=-1 --ulimit stack=67108864 \ - -it radixark/miles:latest /bin/bash -``` - -### Install miles - -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 -cd /root/miles -git pull -pip install -e . --no-deps -``` - -## Model and Dataset Download - -You can download required models and datasets from platforms like Hugging Face, ModelScope, etc. Here are the commands to download example resources using `huggingface_hub`: - -```bash -# Download model weights (GLM-Z1-9B) -hf download zai-org/GLM-Z1-9B-0414 --local-dir /root/GLM-Z1-9B-0414 - -# 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 -``` - -## Model Weight Conversion - -### Convert from Hugging Face Format to Megatron Format - -When using Megatron as the training backend, you need to first convert Hugging Face format model weights to Megatron `torch_dist` format. - -First, load the configuration file of the target model. The `miles/scripts/models` directory contains configuration files for supported models. You need to `source` the corresponding model script to load the configuration parameters into the current environment. Here we use GLM4-9B model as an example, and it's similar for Qwen3-4B, Qwen3-30B-A3B, etc. - -```bash -cd /root/miles -source scripts/models/glm4-9B.sh -``` - -Next, run the conversion script. Please note the following parameters: -- `--hf-checkpoint`: Specify the path of the downloaded Hugging Face model weights. -- `--save`: Specify the save path for the converted `torch_dist` format weights. - -```bash -PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ - ${MODEL_ARGS[@]} \ - --hf-checkpoint /root/GLM-Z1-9B-0414 \ - --save /root/GLM-Z1-9B-0414_torch_dist -``` - -For larger models, you can use `torchrun` to start the conversion script to convert with multi-gpus or even multi-nodes. -Note: When converting the kimi-k2 model weights, you need to open config.json in the model path and change "model_type": "kimi_k2" to "model_type": "deepseek_v3". - -### Convert from Megatron Format to Hugging Face Format - -You can use the following script to convert the saved Megatron checkpoints back to Hugging Face format: - -```bash -PYTHONPATH=/root/Megatron-LM python tools/convert_torch_dist_to_hf.py \ - --input-dir /path/to/torch_dist_ckpt/iter_xxx/ \ - --output-dir /root/GLM-Z1-9B-0414-iter_xxx \ - --origin-hf-dir /root/GLM-Z1-9B-0414 -``` - -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 - -After completing the above preparation work, you can run the training script. - -```bash -cd /root/miles -bash scripts/run-glm4-9B.sh -``` - -We still use the run-glm4-9B.sh script as an example to briefly analyze the main parameters. - -### MODEL_ARGS: Model Configuration Parameters - -```bash -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" -source "${SCRIPT_DIR}/models/glm4-9B.sh" -``` - -This part loads model configuration from the `scripts/models/glm4-9B.sh` file through the `source` command. These configurations are all hyperparameters required by Megatron. Since Megatron cannot directly read model configuration from checkpoints, it needs to be manually specified. We provide configuration examples for some commonly used models in the `scripts/models/` directory. - -> ⚠️ **Note**: -> Please make sure to check whether the parameters in the model configuration file (such as `--rotary-base`) completely match the model you are currently using. Different versions of the same model structure may use different configuration values. If you need to modify, you can directly override after `source`, for example: -> ```bash -> source "${SCRIPT_DIR}/models/glm4-9B.sh" -> MODEL_ARGS+=(--rotary-base 10000) -> ``` - -### CKPT_ARGS: Checkpoint and Path Parameters - -```bash -CKPT_ARGS=( - # To load tokenizer and other information, won't actually use model weight parameters from hf path - --hf-checkpoint /root/GLM-Z1-9B-0414 - # Reference Model's Megatron format checkpoint - --ref-load /root/GLM-Z1-9B-0414_torch_dist - # Actor model loading path. Should typically match --save for checkpoint resumption - # If empty or doesn't contain a valid checkpoint, loads from --ref-load instead - --load /root/GLM-Z1-9B-0414_miles/ - # Model save path during training - --save /root/GLM-Z1-9B-0414_miles/ - # Model save interval (steps) - --save-interval 20 -) -``` - -### ROLLOUT_ARGS: Data Generation (Rollout) Parameters - -The entire training process can be viewed as a closed loop of **"Data Sampling → Weight Update"**. - -**Phase One: Data Sampling (Rollout)** -- `--rollout-batch-size`: Defines the **number of Prompts** for each round of sampling -- `--n-samples-per-prompt`: Defines the **number of responses** generated for each Prompt (used for GRPO-like algorithms) - -> The product of the two determines the **total number of samples generated in a single round of sampling**. - -**Phase Two: Model Training (Training)** -- `--global-batch-size`: Defines the **sample size required to execute one parameter update (optimizer.step)** -- `--num-steps-per-rollout`: Defines **how many parameter updates to execute** using the current sampled data (we default to 1, using on-policy training) - -> The product of the two determines the **total number of samples consumed in a single round of training**. - -> ⚠️ The **parameter update** here refers to the optimizer.step() in the training phase, which is different from the weight synchronization (Weight Sync) initiated by the training engine to the inference engine. - -In this process, the "output" and "consumption" of each round must be equal, following this constraint: -**`(rollout-batch-size × n-samples-per-prompt) = (global-batch-size × num-steps-per-rollout)`** - -- In miles, if `--num-steps-per-rollout` is set, `--global-batch-size` will be automatically set if not set, and if set, it will be validated using the above formula. - -**Training Process Count Control** -- `--num-rollout`: Controls the **total number of execution rounds** of the entire **"sampling→training"** loop. - -```bash -ROLLOUT_ARGS=( - # Prompt dataset, JSONL format - --prompt-data /root/dapo-math-17k/dapo-math-17k.jsonl - --input-key prompt - --label-key label - # If the `input_key` of Prompt is in OpenAI message format, apply Chat Template - --apply-chat-template - # Whether to shuffle data in Rollout phase - --rollout-shuffle - - # Reward Model type. miles has built-in multiple types, also supports custom through --custom-rm-path - --rm-type deepscaler - - # These five parameters control the relationship between rollout and train - --num-rollout 3000 - --rollout-batch-size 16 - --n-samples-per-prompt 8 - --num-steps-per-rollout 1 - --global-batch-size 128 - - # Rollout sampling parameters - --rollout-max-response-len 8192 - --rollout-temperature 1 - - # Load balancing for data collected in rollout phase. It ensures that the computational workload allocated to each training process (DP rank) is roughly equal, which may be beneficial for training speed - --balance-data -) -``` - -### EVAL_ARGS: Evaluation Parameters - -The evaluation process inherits most of the Rollout parameters, but you can override them with the following parameters to implement evaluation strategies different from training. - -```bash -EVAL_ARGS=( - # Evaluation interval (number of Rollouts) - --eval-interval 5 - # Prompt dataset for evaluation - --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl - # Number of samples per evaluation Prompt - --n-samples-per-eval-prompt 16 - # Maximum response length during evaluation - --eval-max-response-len 16384 - # Sampling parameters during evaluation - --eval-top-p 1 -) -``` - -### PERF_ARGS: Performance and Parallelism Parameters - -This part mainly contains Megatron's parallel configuration. `--use-dynamic-batch-size` and `--max-tokens-per-gpu` are miles-specific optimizations. - -- `--max-tokens-per-gpu`: Maximum number of tokens processed per GPU. After enabling dynamic batching (`use_dynamic_batch_size`), the system will intelligently pack samples of varying lengths so that the total token count of each micro-batch approaches this limit, thereby improving training efficiency. If a single sample length exceeds this value, it will form an independent batch. In context parallel (CP) mode, `N` CP cards share the total length of `N * max_tokens_per_gpu`. -- `--use-dynamic-batch-size`: Enable dynamic batching. At this time, `--micro-batch-size` will be ignored. - -> 💡 **Tip**: -> miles always trains models through data packing methods and strictly ensures that per sample loss or per token loss is correct. Therefore, enabling dynamic batch size will not affect loss calculation, and it is strongly recommended to enable it. - -```bash -PERF_ARGS=( - --tensor-model-parallel-size 2 - --sequence-parallel - --pipeline-model-parallel-size 1 - --context-parallel-size 2 - --expert-model-parallel-size 1 - --expert-tensor-parallel-size 1 - - --recompute-granularity full - --recompute-method uniform - --recompute-num-layers 1 - - # --micro-batch-size 1 # This item is ignored when dynamic batching is enabled - --use-dynamic-batch-size - --max-tokens-per-gpu 4608 -) -``` - -### GRPO_ARGS: GRPO Algorithm Parameters - -- `--use-kl-loss`: Enabling this option will load a reference model and calculate the KL divergence between the current model and the reference model as a monitoring metric. Whether KL divergence is included in the final training loss depends on the `--kl-loss-coef` parameter. If this parameter is set to 0, KL divergence will only be displayed as an observation metric and will not participate in loss calculation. - -```bash -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 -) -``` - -- `--advantage-estimator`: In addition to [GRPO](https://arxiv.org/abs/2402.03300), miles also supports several other training algorithms, such as [GSPO](https://arxiv.org/abs/2507.18071), [Reinforce++](https://arxiv.org/abs/2501.03262) and [Reinforce++ Baseline](https://arxiv.org/abs/2501.03262), and [PPO](https://arxiv.org/abs/1707.06347). -- `--calculate-per-token-loss`: By default, miles calculates the loss on a per-sample basis, i.e., `mean(sum(sample_i) / len(sample_i))`. To calculate the loss on a per-token basis, i.e., `sum(sum(sample_i)) / sum(len(sample_i))`, you can enable this flag. -- `--use-tis`: Enable this setting to use TIS (Truncated Importance Sampling), which is introduced by this [blog](https://fengyao.notion.site/off-policy-rl). - -### OPTIMIZER_ARGS: Optimizer Parameters - -```bash -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: SGLang Service Parameters - -This part of parameters is used to configure SGLang inference service. -- `--rollout-num-gpus-per-engine`: Basically equivalent to SGLang's `tp_size`. -- Other SGLang parameters can be passed to miles by adding the `--sglang-` prefix, and miles will automatically forward them to SGLang. For example, to set SGLang's `--log-level INFO` parameter, just use `--sglang-log-level INFO`. - -> ⚠️ **Note**: -> miles uses `sgl-router` to schedule multiple SGLang Servers. Without enabling DP Attention, `dp_size` will be calculated through `rollout-num-gpus/rollout-num-gpus-per-engine`. - -```bash -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 2 -) -``` - -## Feature Introduction - -### Colocated Actor and Rollout - -Under the default configuration, training (Actor) and inference (Rollout) resources are specified separately. Ray allocates `actor_num_nodes * actor_num_gpus_per_node` GPUs to the training part and `rollout_num_gpus` GPUs to inference, that is, training and inference are separated. - -**Standard (Disaggregated) Configuration**: -```bash -ray job submit ... \ - -- python3 train.py \ - --actor-num-nodes 1 \ - --actor-num-gpus-per-node 4 \ - --rollout-num-gpus 4 \ - ... -``` -In the above configuration, Actor uses 4 cards, and Rollout also uses 4 cards, running in parallel. - -**Training-Inference Integration (Colocated) Configuration**: -To deploy training and inference on the same group of GPUs, please add the `--colocate` parameter. After enabling, `--rollout-num-gpus` will be ignored to make the number of cards for training and inference equal. - -```bash -ray job submit ... \ - -- python3 train.py \ - --actor-num-nodes 1 \ - --actor-num-gpus-per-node 8 \ - --colocate \ - ... -``` -At this time, training and inference will share all 8 GPUs. - -> ⚠️ **Note**: -> In training-inference integration mode, Megatron will occupy a certain amount of GPU memory before it can be offloaded after initialization. You need to adjust the `--sglang-mem-fraction-static` parameter to reduce SGLang's GPU memory usage ratio to avoid insufficient GPU memory. We usually recommend 0.8. - -### Dynamic Sampling - -miles supports more complex sampling strategies, such as dynamic sampling used in [DAPO](https://dapo-sia.github.io/). To enable this feature, you need to configure the following parameters: - -```bash - --over-sampling-batch-size 64 \ - --dynamic-sampling-filter-path \ - miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std -``` - -Here `over_sampling_batch_size` needs to be greater than `rollout_batch_size`, for example, configured as: - -```bash - --rollout-batch-size 32 \ - --n-samples-per-prompt 8 \ - --over-sampling-batch-size 64 \ -``` - -Then each sampling will directly sample 64 prompts, and each prompt will be sampled 8 times. Because miles performs asynchronous sampling internally, we will successively obtain 8 responses for each prompt. When receiving responses, the function corresponding to `dynamic_sampling_filter_path` will be used for filtering. If it passes, these 8 pieces of data will be kept; otherwise, they will be discarded. - -The filtering function `check_reward_nonzero_std` in the example will check whether the standard deviation of rewards for a group of samples is greater than zero, ensuring that the reward scores of each group of samples left have differences, thereby avoiding overly homogeneous data and improving data diversity. - -```python -def check_reward_nonzero_std(args, samples: list[Sample], **kwargs): - rewards = [sample.reward for sample in samples] - return torch.tensor(rewards, dtype=torch.float).std() > 0.0 -``` - -If the filtering function is very strict, causing a large number of prompt groups to be discarded, the system will monitor the number of pending tasks in `remaining_batch_size`. Once the number of pending tasks drops below the target number (32) due to too many being discarded, the system will automatically trigger a new round of oversampling, requesting `over_sampling_batch_size` (64) new prompts again to repeat the above process. - -### Partial Rollout - -During dynamic sampling, a large number of requests may be aborted early, causing waste of computational resources. By enabling the `--partial-rollout` parameter, these half-generated samples can be cached and continued to be generated in the next Rollout phase, thereby improving performance. - -You can also customize the strategy for extracting data from the cache through `--buffer-filter-path`. The default strategy is `pop_first`, which extracts the required number of samples in first-in-first-out order. - -```python -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 -``` - -That is, take out the first `num_samples` prompts corresponding to `num_samples * n_samples_per_prompt` pieces of data each time. - -> 💡 **Tip**: -> The `sample.metadata` of each partial rollout sample stores the rollout id of the first generation, which can be used for data filtering. - -### bf16 Training fp8 Inference - -miles directly supports bf16 training and fp8 inference. For Qwen3-4B model, you only need to download the following model: - -```bash -hf download Qwen/Qwen3-4B-FP8 --local-dir /root/Qwen3-4B-FP8 -``` - -And replace `--hf-checkpoint` with: - -```bash - # Used to load tokenizer and other information, actually won't use model weight parameters from hf path - --hf-checkpoint /root/Qwen3-4B-FP8 - - # The megatron checkpoint still needs to be the dist weights converted from bf16 huggingface at the beginning, not modified because of FP8 rollout. - --ref-load /root/Qwen3-4B_torch_dist -``` - -This will trigger fp8 inference. Currently, we will directly cast bf16 weights to fp8, and we will gradually add quantization schemes with less impact on accuracy in the future. - -⚠️ The training megatron checkpoint still needs to be the one converted from bf16 huggingface at the beginning. - -## Multiturn Adaptation - -The miles framework is highly extensible and supports complex Agent scenarios (such as multi-turn interaction and tool calling). Its core mechanism is to rewrite the default data generation (Rollout) and reward calculation (Reward) logic through custom functions. - -This section uses an implementation based on [Search-R1](https://github.com/PeterGriffinJin/Search-R1) as an example to illustrate how to adapt miles to support multi-turn interaction. - -### Adaptation Strategy Summary - -Adapting miles to support multi-turn interaction mainly includes three steps: - -1. **Data Preparation**: Adapt the multi-turn interaction dataset to miles's `Sample` objects. Map conversation history, real labels, etc. to `prompt` and `label` fields, and store additional information such as tool definitions and intermediate states in the `metadata` field for subsequent function calls. - -2. **Implement Custom Generation Function**: Write functions to simulate the interaction loop of "model generates action → executes tool → concatenates observation results", and correctly handle Loss Masking. - -3. **Implement Custom Reward Function**: Write functions to evaluate complete interaction trajectories and return final reward scores. - -### Data Preparation and Mapping - -To pass complex contextual information to custom functions, you need to aggregate all relevant additional fields during the **data preprocessing stage**. - -**Core Idea**: Merge all additional information in the dataset except `prompt` and `label` (such as `session_id`, `user_profile`, `tool_code`, etc.) to construct a **single, structured field** (for example, a column named `metadata` with JSON string content). - -### Step One: Construct `metadata` Field in Dataset - -Before training starts, you need to process the original dataset. For example, your original data might be as follows: - -| question | final_answer | session_id | tool_code | -| :--- | :--- | :--- | :--- | -| "..." | "..." | "sess_123" | "code_A" | - -You need to convert it to: - -| question | final_answer | metadata | -| :--- | :--- | :--- | -| "..." | "..." | `{"session_id": "sess_123", "tool_code": "code_A"}` | - -### Step Two: Specify Mapping in Training Script - -After completing data preparation, in the training script, map this preprocessed `metadata` column to miles's `Sample.metadata` field through `ROLLOUT_ARGS`. - -```bash -ROLLOUT_ARGS=( - # 1. Specify the preprocessed dataset file - --prompt-data /root/nq_search/train_processed.json - - # 2. Map "question" column to input prompt - --input-key question - - # 3. Map "final_answer" column to evaluation label - --label-key final_answer - - # 4. Load the pre-constructed "metadata" column into Sample.metadata - # miles will automatically parse it as a Python dictionary - --metadata-key metadata -) -``` - -Through this approach, you can easily access all pre-prepared structured information through methods like `sample.metadata['session_id']` in custom `generate` or `reward` functions. - -### Writing Custom Generation Function - -First, specify a custom asynchronous Python function through the `--custom-generate-function-path` parameter. - -**Function Signature**: `async def generate(args, sample: Sample, sampling_params) -> Sample:` - -**Core Implementation Points**: - -1. **Build Interaction Loop**: Create a loop to control maximum interaction rounds (such as `for _ in range(max_turns):`). -2. **Call Model to Generate Action**: In each round of the loop, call SGLang service to let the model generate the next action (such as `query`) based on the current conversation history. -3. **Parse and Execute Action**: Parse model output, identify actions and parameters, and call external tools or APIs (such as Google search). -4. **Build Observation Results**: Format the results returned by tools and append them to the conversation history as input for the next round. -5. **Handle Loss Masking**: This is the key to Agent training. - - Note: `loss_mask` should be the same length as `response`, where tokens that need to calculate loss are 1, and masked ones are 0 - - **Model-generated** tokens (such as thinking, action instructions) → set `loss_mask` to `1`, participate in loss calculation. - - **Tool or environment returned** tokens (such as API results) → set `loss_mask` to `0`, do not participate in loss calculation. -6. **Termination Conditions**: End the loop when the model generates termination tags (such as `...`) or reaches maximum rounds. -7. **Encapsulate Return**: Fill the complete interaction history, token IDs, and `loss_masks` into the `Sample` object and return. - -**Code Example (Pseudocode)**: -```python -async def generate(args, sample: Sample, sampling_params) -> Sample: - # ... initialization ... - prompt, full_response, loss_masks = sample.prompt, "", [] - - for _ in range(max_turns): - # 1. Model generates action - model_output = await call_sglang(prompt + full_response, ...) - # ... tokenization and appending ... - loss_masks += [1] * len(model_tokens) # loss_mask = 1 - full_response += model_output - - # 2. Parse and execute action - action, content = parse_action(model_output) - if action == "search": - # 3 & 4. Get and append observation results - tool_output = await google_search(content) - # ... tokenization and appending ... - loss_masks += [0] * len(tool_tokens) # loss_mask = 0 - full_response += tool_output - - elif action == "answer": - break # end loop - - # 7. Fill and return Sample object - sample.response = full_response - sample.tokens = ... - sample.loss_mask = loss_masks - return sample -``` - -### Writing Custom Reward Function - -Similarly, specify a custom reward function through `--custom-rm-path`. - -**Function Signature**: `async def reward_func(args, sample: Sample, **kwargs) -> float:` - -This function receives a complete `Sample` object and calculates scores based on the final interaction results. You can implement custom scoring logic here or call external Reward Model services. - -### Configure in Training Script - -Finally, in the training script, enable the above custom functions through the following parameters: - -```bash -CUSTOM_ARGS=( - # Specify the path of custom generation function (format: path.to.your.file:function_name) - --custom-generate-function-path your_module.multiturn_logic.generate - - # Specify the path of custom reward function - --custom-rm-path your_module.multiturn_logic.reward_func -) -``` - -## Multi-Node Training for Large-Scale MOE Models - -To start a multi-node task, you need to first start a Ray cluster. On node 0, run: - -```bash -# Node0 (HEAD) -ray start --head --node-ip-address ${MASTER_ADDR} \ - --num-gpus 8 --disable-usage-stats - -# Other Nodes -ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 -``` - -After the Ray cluster has started, you can submit a job from node 0, for example: - -```bash -ray job submit --address="http://127.0.0.1:8265" \ - --runtime-env-json='{ - "env_vars": { - "PYTHONPATH": "/root/Megatron-LM/", - ... # e.g., no_proxy, API variables, etc. - } - }' \ - -- python3 train.py \ - --... # Other Megatron/SGLang/miles arguments -``` - -Optionally, the following environment variables may be needed based on your environment. For example, when there are multiple IPs and the wrong one is chosen in a Docker or SLURM envionment. We provide an example used in a SLURM + enroot multi-node system as follows: - -``` -export MILES_HOST_IP=$(hostname -I | awk '{print $1}') -export GLOO_SOCKET_IFNAME=$(ip -o -4 addr show | awk '$4 ~ /^10\\./ {print $2}') -export NCCL_SOCKET_IFNAME=$(ip -o -4 addr show | awk '$4 ~ /^10\\./ {print $2}') -export NVSHMEM_BOOTSTRAP_UID_SOCK_IFNAME=$(ip -o -4 addr show | awk '$4 ~ /^10\./ {print $2}') -``` - -miles has been deeply optimized for distributed training of large-scale Mixture of Experts (MoE) models. We provide some end-to-end training cases for reference: - -- [Example: 64xH100 Training GLM-4.5](../examples/glm4.5-355B-A32B.md) -- [Example: 128xH100 Training DeepSeek-R1](../examples/deepseek-r1.md) -- The scripts such as `scripts/run_qwen3_30b_a3b.py`, `scripts/run_glm45_355b_a32b.py` also support multi-node training, though there are little documentations about it currently. diff --git a/docs/en/get_started/usage.md b/docs/en/get_started/usage.md deleted file mode 100644 index 97f6449fe47..00000000000 --- a/docs/en/get_started/usage.md +++ /dev/null @@ -1,384 +0,0 @@ -# Usage Guide - - -## Introduction to miles Parameters - -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 or FSDP for the training portion. -3. To load SGLang for the inference portion. -4. To configure the hyperparameters required for RL training. - -Following this order, we need to configure these parameters: - -### Cluster Resource Allocation - -There are four main parameters for cluster resource allocation: - - - `--actor-num-nodes`: The number of nodes required for RL actor training. - - `--actor-num-gpus-per-node`: The number of GPUs per node for RL actor training. - - `--rollout-num-gpus`: The total number of GPUs required for rollout (inference). - - `--rollout-num-gpus-per-engine`: The number of GPUs per inference engine. This parameter is similar to SGLang's `tp_size`. When performing multi-node serving, this value should be the total number of GPUs. For example, if serving one model with 2 nodes and 16 GPUs, this value should be 16. - The reason for not using a parameter like `--sglang-tp-size` is that we might consider supporting SGLang's `dp_size` parameter in the future, which means an engine could contain multiple SGLang servers (currently, only `--sglang-dp-size` under the `--sglang-enable-dp-attention` condition is supported). - -With the default configuration, we use these parameters to allocate `actor_num_nodes * actor_num_gpus_per_node` GPUs for training and `rollout_num_gpus` GPUs for inference via Ray, thus achieving a separation of training and inference resources. - -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. - -Generally, we need to perform three preparatory steps: - - - Configure model parameters. - - Configure parallelism and other optimizations. - - Configure the checkpoint to be loaded. - -For details on some of Megatron's customizations and the principles behind how miles incorporates Megatron, please see the "How to Use Megatron" section. - -#### Configuring Model Parameters - -Taking qwen3 4B as an example, we need these parameters: - -```bash -MODEL_ARGS=( - --num-layers 36 - --hidden-size 2560 - --ffn-hidden-size 9728 - --swiglu - --vocab-size 151936 - --disable-bias-linear - # attn head - --num-attention-heads 32 - --group-query-attention - --num-query-groups 8 - --kv-channels 128 - --qk-layernorm - # norm - --normalization "RMSNorm" - --norm-epsilon 1e-6 - # rope - --use-rotary-position-embeddings - --rotary-base 1000000 -) -``` - -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: - - - miles will load all parameters of Megatron found in the `PYTHONPATH`, so you can find parameters and their descriptions within the Megatron in your environment. - - miles uses data packing (also known as varlen or thd) for training. There is no need to configure `--seq-length` or `--max-positional-embedding`, as these parameters do not affect the maximum context length of the trained model. - -#### Setting Up Parallelism and Recomputation - -Megatron is currently the most comprehensively optimized training framework. A major reason for using Megatron is to pursue its excellent performance. Here is a brief introduction to configuring Megatron's parallelism and recomputation. - - - Here we list Megatron's parallelism strategies. For a more detailed discussion on the trade-offs between these strategies, please refer to more specialized discussions: - - `--tensor-model-parallel-size`: TP - - `--sequence-parallel`: Megatron's SP is an optimization for TP. It is recommended to always enable SP when using TP. - - `--pipeline-model-parallel-size`: PP - - `--context-parallel-size`: Megatron's CP, also known as sequence parallelism, generally corresponds to ring attention. - - `--expert-model-parallel-size`: EP for MoE, where each GPU has `num_experts / ep_size` experts. - - `--expert-tensor-parallel-size`: Megatron supports using a different `tp_size` for the MoE experts than for other parts of the model, which we generally call ETP. - - For recomputation, the following flags are commonly configured in Megatron: - - `--recompute-granularity`: This can be set to `full` or `selective`. `full` means complete recomputation, while `selective` recomputes less. If not configured, no recomputation is done. - - `--recompute-method`: `uniform` is generally sufficient. - - `--recompute-num-layers`: The number of layers per group for recomputation. A value of 1 is usually fine. - -#### Loading Megatron Checkpoints - -Megatron supports several of its custom checkpoint formats. Here are two of the more common ones: - - - The once mainstream `torch` format (corresponding to `--ckpt-format torch`). - - The currently recommended `torch_dist` format (corresponding to `--ckpt-format torch_dist`). - -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). - -In terms of storage structure, a Megatron checkpoint typically looks like this, assuming the storage path is `/ckpt/`: - -```bash ---/ckpt/ - |-- latest_checkpointed_iteration.txt - |-- iter_0000100/ - |-- _0_0.distcp - |-- _0_1.distcp - |-- ... - |-- iter_0000200/ - |-- iter_0000300/ - |-- ... -``` - -The `latest_checkpointed_iteration.txt` file records the latest training step. When loading a model, you should not directly pass `/ckpt/iter_xxxxxxx`, but rather pass `/ckpt/` and use `--ckpt-step` to select the corresponding training step (if `--ckpt-step` is not used, the step will be read from `latest_checkpointed_iteration.txt`). - -When using miles, there are three parameters for loading and saving checkpoints: - - - `--ref-load`: The Megatron checkpoint for the reference model. - - `--load`: The Megatron checkpoint for the actor. If `--load` is not set, or if the specified directory does not exist or does not contain `latest_checkpointed_iteration.txt`, the actor will be initialized from the `--ref-load` checkpoint. - - `--save`: The path where the actor's checkpoints are saved. - -Note: - - - Regardless of the checkpoint storage method (i.e., however `--ckpt-format` is set), Megatron can load both `torch` and `torch_dist` formats. - -### Loading SGLang - -Loading SGLang is very simple. You only need: - - - `--hf-checkpoint`: The Hugging Face checkpoint used to initialize SGLang. - -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. - -### Data Format - -Currently, miles only supports loading files in `.jsonl` format, where each line of the file is a JSON object. An example of a single data entry (expanded) is as follows: - -```json -{ - "prompt": [ - { - "content": "Solve the following math problem step by step. The last line of your response should be of the form Answer: \\boxed{$Answer} where $Answer is the answer to the problem.\n\nIn triangle $ABC$, $\\sin \\angle A = \\frac{4}{5}$ and $\\angle A < 90^\\circ$. Let $D$ be a point outside triangle $ABC$ such that $\\angle BAD = \\angle DAC$ and $\\angle BDC = 90^\\circ$. Suppose that $AD = 1$ and that $\\frac{BD}{CD} = \\frac{3}{2}$. If $AB + AC$ can be expressed in the form $\\frac{a\\sqrt{b}}{c}$ where $a, b, c$ are pairwise relatively prime integers, find $a + b + c$.\n\nRemember to put your answer on its own line after \"Answer:\".", - "role": "user", - "step_loss_mask": 1, - } - ], - "label": "34" -} -``` - -This corresponds to the following configuration: - -```bash - --input-key prompt - --label-key label - --apply-chat-template -``` - -Please note that the `step_loss_mask` (default=1) here is for SFT phase. If it is set to 0, the turn will not contibute to the final loss; if it is set to 1, miles will use the normal `loss_mask`. -Additionally, we provide a `metadata_key`, which defaults to `"metadata"`. When read, miles will load the metadata from the data, which can be helpful for custom data generation or creating custom reward models. - -### Hyperparameters for RL Training - -- `--advantage-estimator`: Specifies the RL algorithm for the training process. Currently supported algorithms include: - - `grpo` ([https://arxiv.org/abs/2402.03300](https://arxiv.org/abs/2402.03300)) - - `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_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: - - ```python - def generate_rollout(args, rollout_id, data_buffer, evaluation=False) -> list[list[Sample]]: - """ - 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 samples generated by the rollout - """ - ... - return samples - ``` - - Where: - - - `args`: The complete arguments used for the miles run. - - - `rollout_id`: The ID of the current data generation round, used to ensure data order when resuming training. - - - `data_buffer`: A globally unique data buffer in miles, which can be used to get initial prompts, data IDs, and store partially generated samples for later use. - - - `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](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. - - `reward`: The reward for this data sample. - - `truncated`: Whether this data sample was truncated, similar to `finish_reason == length` in SGLang. - - And if there are scenarios like tool calls or multi-turn usage, ensure the `loss_mask` is correct: - - - `loss_mask` should be the same length as `response_length`, with `1` for tokens that should be included in the loss calculation and `0` for those that should be masked out. - - - In some cases, you may only need to replace the data generation logic. You can do this using `--custom-generate-function-path`. A simplified implementation of this function is as follows: - - ```python - async def generate(args, sample: Sample, sampling_params) -> Sample: - global TOKENIZER - if TOKENIZER is None: - TOKENIZER = AutoTokenizer.from_pretrained(args.hf_checkpoint, trust_remote_code=True) - - # send request to router - output = await post( - f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate", - { - "text": sample.prompt, - "sampling_params": sampling_params, - } - ) - - prompt_tokens_ids = TOKENIZER(sample.prompt, add_special_tokens=False)["input_ids"] - response_token_ids = TOKENIZER(output["text"], add_special_tokens=False)["input_ids"] - - # set sample - sample.tokens = prompt_tokens_ids + response_token_ids - sample.response_length = len(response_token_ids) - sample.truncated = output["meta_info"]["finish_reason"]["type"] == "length" - sample.response = output["text"] - sample.aborted = output["meta_info"]["finish_reason"]["type"] == "abort" - - return sample - ``` - - 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`. - -## How to Use SGLang - -miles implements a server-based engine using SGLang via the `HttpServerEngineAdapter` as an intermediary. - -### Parameter Configuration - -miles incorporates almost all SGLang parameters by using SGLang's `ServerArgs.add_cli_args`. When setting an SGLang parameter, you need to add the `--sglang-` prefix. For example: - - - In co-located training and inference, you often need to limit `--mem-fraction-static`. This parameter should be changed to `--sglang-mem-fraction-static`. - - During training, if you want SGLang to infer beyond the maximum context length specified in the Hugging Face checkpoint's `config.json`, you need to use `--context-length`, which becomes `--sglang-context-length` in miles. - - For multi-node large EP inference, you might need `--enable-ep-moe`, `--enable-dp-attention`, `--dp-size`, `--enable-deepep-moe`, etc. These can be passed as `--sglang-enable-ep-moe`, `--sglang-enable-dp-attention`, `--sglang-dp-size`, and `--sglang-enable-deepep-moe` respectively. - -Some parameters related to miles's resource scheduling are configured by miles itself, for example: - - - `--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](https://github.com/radixark/miles/blob/main/miles/backends/sglang_utils/arguments.py). - -### How to Use the Router - -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. - -When you configure an external router using `--sglang-router-ip` and `--sglang-router-port`, miles will not start an internal router. Instead, it will register all its servers with this external router. You can then use this external router's address to implement more complex data generation workflows. Note that the router supports OpenAI-compatible APIs. - -## How to Use Megatron - -miles supports different and lightly modified versions of Megatron by reusing common functions from the `megatron.training` directory, such as `parse_args`, `save_checkpoint`, and `load_checkpoint`. Therefore, when using it, you must ensure that Megatron is accessible in the `PYTHONPATH`, for example, by adding `export PYTHONPATH=/root/Megatron-LM` at runtime. - -### 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](https://github.com/radixark/miles/blob/main/train.py), for example: - -```python -if __name__ == "__main__": - try: - from pretrain_gpt import extra_args_provider - except: - extra_args_provider = None - args = parse_args(extra_args_provider) - train(args) -``` - -### Custom Parameters - -In some customized Megatron implementations, special operations need to be performed during initialization or before/after a training step. We have added the following plugins for this purpose: - - - `--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. - -## 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 . --no-deps - - -# 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 deleted file mode 100644 index 5ee81219814..00000000000 --- a/docs/en/index.rst +++ /dev/null @@ -1,85 +0,0 @@ -Miles Documentation -==================== - -miles is an LLM post-training framework for RL scaling, providing two core capabilities: - -- 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.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); -- Llama 3. - -.. toctree:: - :maxdepth: 1 - :caption: Get Started - - get_started/quick_start.md - get_started/usage.md - get_started/customization.md - get_started/qa.md - -.. toctree:: - :maxdepth: 1 - :caption: Agentic - - agentic/oai_endpoint.md - -.. toctree:: - :maxdepth: 1 - :caption: Dense - - examples/qwen3-4B.md - examples/glm4-9B.md - -.. toctree:: - :maxdepth: 1 - :caption: MoE - - examples/qwen3-30B-A3B.md - examples/glm4.5-355B-A32B.md - examples/deepseek-r1.md - -.. toctree:: - :maxdepth: 1 - :caption: Advanced Features - - _examples_synced/reproducibility/README.md - advanced/miles-router.md - advanced/speculative-decoding.md - advanced/fault-tolerance.md - advanced/arch-support-beyond-megatron.md - advanced/pd-disaggregation.md - advanced/p2p-weight-transfer.md - -.. toctree:: - :maxdepth: 1 - :caption: Other Usage - - examples/qwen3-4b-base-openhermes.md - _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/on_policy_distillation/README.md - -.. toctree:: - :maxdepth: 1 - :caption: Developer Guide - - developer_guide/debug.md - -.. toctree:: - :maxdepth: 1 - :caption: Hardware Platforms - - platform_support/amd_tutorial.md - -.. toctree:: - :maxdepth: 1 - :caption: Blogs - - blogs/release_v0.1.0.md - blogs/introducing_miles.md diff --git a/docs/en/platform_support/amd_tutorial.md b/docs/en/platform_support/amd_tutorial.md deleted file mode 100644 index ad5cc497e5e..00000000000 --- a/docs/en/platform_support/amd_tutorial.md +++ /dev/null @@ -1,291 +0,0 @@ -# AMD - -⚠️ If you encounter problems on AMD instinct, feel free to reach out [Yusheng Su](https://yushengsu-thu.github.io/). - - -## Introduction - -If you are running miles on AMD's Instinct, please refer to the following materials. This tutorial will explain how to set up the development environment (Docker), use the modified ROCm dependencies, and provide an example for running the experiments. The current rocm docker only support AMD's MI300 and MI325 GPUs. - - - - -## Docker - -You can download the prebuilt image from DockerHub: [rlsys/miles](https://hub.docker.com/r/rlsys/miles/tags). -```bash -# MI350/MI355X -docker pull rlsys/miles:MI350-355-latest -# MI300 -docker pull rlsys/miles:MI300-latest -``` -Or you can use the [Dockerfile.rocm](https://github.com/radixark/miles/blob/main/docker/Dockerfile.rocm_MI350-5) to build it on your side. -```bash -cd docker -docker build -f Dockerfile.rocm_MI350-5 -t rlsys/miles:latest . -``` - -Acknowledgement: Thanks to [Yang Wang](https://www.microsoft.com/en-us/research/people/yangwang5/) for working on the patch for this [ROCm base Docker image](https://hub.docker.com/r/rlsys/rocm-6.3.4-patch/tags) to support virtual memory management on MI300X. - - -## Quick Start - -### Environment Setup - -Based on the [rlsys/miles](https://hub.docker.com/r/rlsys/miles/tags) image (pre-installed with SGLang and Megatron-LM): -```bash -docker run --rm -it \ - --device /dev/dri \ - --device /dev/kfd \ - -p 8265:8265 \ - --group-add video \ - --cap-add SYS_PTRACE \ - --security-opt seccomp=unconfined \ - --privileged \ - -v $HOME/.ssh:/root/.ssh \ - -v $HOME:$HOME \ - --shm-size 128G \ - --name miles_dev \ - --ulimit memlock=-1 \ - --ulimit stack=67108864 \ - -w $PWD \ - rlsys/miles:latest \ - /bin/bash -``` - -Then, download and install miles: -```bash -git clone https://github.com/radixark/miles.git -cd miles -pip install -e . --no-deps -``` - -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 - -Since miles uses Megatron, and Megatron does not support loading Hugging Face checkpoints directly, we need to convert the model to the `torch_dist` format that Megatron supports. - -#### HF → Megatron torch\_dist ckpt - -Use [mbridge](https://github.com/ISEEKYAN/mbridge.git) or [Megatron-LM-amd_version-amd](https://github.com/yushengsu-thu/Megatron-LM-amd_version.git) for conversion: - -```bash -cd miles/ -source scripts/models/qwen3-4B.sh -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[@]} \ - --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. - -⚠️ If you encounter an issue where miles cannot be found, please run `pip install -e . --no-deps` in the miles directory. - - -### Example: Qwen3-4B - -We provide examples to use [Qwen3-4B](https://huggingface.co/Qwen/Qwen3-4B), please refer to: -- [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 -``` - -⚠️ Note: The main difference between ROCm's training script and NVIDIA's script is that you need to set `RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES` and `HIP_VISIBLE_DEVICES` for ray to function properly on AMD GPUs. - -- We show the training script below: - -```bash -#!/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 -euxo pipefail - - -### AMD Support ### -MILES_DIR="${MILES_DIR:-/home/yushensu/projects/miles}" # 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 -export MODEL_DIR - -DATA_DIR="${DATA_DIR:-/home/yushensu/projects/data}" # Default path if not set in environment -export DATA_DIR - -# For AMD GPU -export RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES=${RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES:-"1"} # Must set to 1 -export HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES:-"0,1,2,3,4,5,6,7"} #You can choose which gpus to use -#################### - - -# will prevent ray from buffering stdout/stderr -export PYTHONBUFFERED=16 - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" -source "${SCRIPT_DIR}/models/qwen3-4B.sh" - -CKPT_ARGS=( - --hf-checkpoint ${MODEL_DIR}/Qwen3-4B - --ref-load ${MODEL_DIR}/Qwen3-4B_torch_dist - --load ${MODEL_DIR}/Qwen3-4B_miles/ - --save ${MODEL_DIR}/Qwen3-4B_miles/ - --save-interval 20 -) - -ROLLOUT_ARGS=( - --prompt-data ${DATA_DIR}/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 1 - - --global-batch-size 256 - --balance-data -) - -EVAL_ARGS=( - --eval-interval 20 - --eval-prompt-data aime ${DATA_DIR}/aime-2024/aime-2024.jsonl - --n-samples-per-eval-prompt 16 - --eval-max-response-len 16384 - --eval-top-p 1 -) - -PERF_ARGS=( - --tensor-model-parallel-size 2 - --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 - - # --micro-batch-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 -) - -WANDB_ARGS=( - # --use-wandb - # --wandb-project miles-dev - # --wandb-group qwen3-4B-test - # --wandb-key ${WANDB_KEY} -) - -SGLANG_ARGS=( - --rollout-num-gpus-per-engine 2 - --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 - ################### -) - -# launch the master node of ray in container -export MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"} - -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}') - -ray job submit --address="http://127.0.0.1:8265" \ - --runtime-env-json='{ - "env_vars": { - "PYTHONPATH": "/workspace/Megatron-LM/", - "CUDA_DEVICE_MAX_CONNECTIONS": "1" - } - }' \ - -- 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[@]} - - -####clear after training -pkill -9 sglang -sleep 3 -ray stop --force -pkill -9 ray -pkill -9 python -sleep 3 -pkill -9 ray -pkill -9 python -``` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000000..a3ba0be132c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,141 @@ +--- +title: FAQ +description: The questions every new Miles user asks in their first week. +--- + +# FAQ + + + + + +Almost always a checkpoint-loading issue. Megatron requires a directory that contains +`latest_checkpointed_iteration.txt`. Verify: + +* `--load` (and/or `--ref-load`) point to a directory with that file. +* If you want a specific iteration, use `--ckpt-step `. + + + + + +Check whether you're running **colocated** or **disaggregated**: + +* **Colocated** (`--colocate`): total GPUs ≥ `actor_num_nodes × actor_num_gpus_per_node`. +* **Disaggregated**: total GPUs ≥ `actor_num_nodes × actor_num_gpus_per_node + rollout_num_gpus`. + +A common mistake is forgetting `--colocate` when sharing GPUs. + + + + + +`max-tokens-per-gpu` caps how many tokens a single GPU sees per micro-batch (only when +`--use-dynamic-batch-size` is on, which it should be). + +Safe starting point: +``` +max_tokens_per_gpu = rollout_max_response_len / cp_size +``` +Then bump it up until you OOM, then back off ~10%. + +Still OOM with a small value? Your individual samples are too long — enable context +parallel (`--context-parallel-size N`) to spread one sample across N ranks. + + + + + +Multiple workers calling `AutoConfig.from_pretrained` on a shared filesystem race each +other. Set `--model-name ` so workers don't re-resolve the path. + + + + + +Set `--load` to whatever directory `--save` was writing to. That's it. + + + + + +For one rollout: + +* `rollout_batch_size` prompts are sampled. +* Each prompt produces `n_samples_per_prompt` responses. + +So one rollout produces `rollout_batch_size × n_samples_per_prompt` samples. + +`--num-steps-per-rollout` decides how many optimizer steps consume that data. The +invariant is: + +> `rollout-batch-size × n-samples-per-prompt = global-batch-size × num-steps-per-rollout` + + + + + +Yes, always. Variable-length samples are packed into the same micro-batch and the loss +is corrected per-sample (or per-token if you set `--calculate-per-token-loss`). You +never need to pad manually. + + + + + +Multiple SGLang servers are colliding on the same node. Reduce the number of SGLang +instances per node — e.g. set `--rollout-num-gpus-per-engine 8` so there's exactly +one server per host. + + + + +Check the chat template first — most "exploding gradient" reports come from feeding +already-templated data into a model that re-applies its template. Then read +[Debugging](developer/debug.md). + + + + + +Stop tokens are misconfigured. The model is babbling without ever emitting EOS. Set +them explicitly with `--rollout-stop` or `--rollout-stop-token-ids`. + + + + + +Per the [SGLang FAQ](https://docs.sglang.io/references/faq.html), this is usually OOM +masquerading. Lower `--sglang-mem-fraction-static`. + + + + + +Torch's compiler cache is corrupt. Add `TORCHINDUCTOR_FORCE_DISABLE_CACHES=1` to your +Ray env vars and re-run. + + + + + +Add `--no-check-for-nan-in-loss-and-grad` to skip the offending steps temporarily — +then go investigate the data + model alignment that caused it. + + + + + +| What | Path | +|---|---| +| Trainer stdout | wherever you redirected `ray job submit` | +| SGLang server | stdout/stderr captured by Ray under `~/.ray/session_latest/logs/`; pass `--sglang-log-dir ` to write to a chosen directory instead | +| Ray workers | `~/.ray/session_latest/logs/` | +| wandb | `wandb/` in your run dir, plus the cloud UI | + + + + + +Still stuck? Drop a thread in the Miles channel of the [SGLang Slack](https://slack.sglang.ai) +or open an issue on [GitHub](https://github.com/radixark/miles/issues). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000000..146e72ba594 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,100 @@ +--- +title: Miles Documentation +--- + +# Miles + +Miles is a high-performance, enterprise-ready reinforcement learning (RL) framework specifically optimized for **Large-Scale model Post-Training**. It +couples [SGLang](https://github.com/sgl-project/sglang) for high-throughput rollout with +[Megatron-LM](https://github.com/NVIDIA/Megatron-LM) for scalable training, and ships the precision, stability, and observability features +needed to run RL at trillion-parameter scale. + + +*"A journey of a thousand miles begins with a single rollout."* — Miles focuses on the low-level system optimizations that make large-scale RL stable, efficient, and reproducible. + +## Core features + +- **Fast and stable support for the latest models.** Day-0 enablement of frontier + releases such as DeepSeek-V4, with rapid follow-on support for new architectures + including GLM-5, Qwen 3.6, and Nemotron-3-Super. +- **Unified low-precision training.** Customisable precision across the rollout and + training engines, with unified **BF16**, **FP8**, **MXFP8**, and **INT4 QAT** recipes + available now and an **NVFP4** training recipe in progress. +- **Efficient Rollout Routing Replay (R3).** For MoE models, expert routing captured + during inference is replayed during the trainer's forward pass, eliminating the + mismatch that destabilizes large-scale MoE RL. Optimized with a routing-result cache + and overlapped device-to-host (D2H) copy to reduce overhead in both single-turn and + multi-turn RL. +- **Speculative rollout with online MTP-SFT.** Miles keeps the draft model's acceptance + rate high through training by fine-tuning MTP layers on-policy. +- **LoRA training and serving.** Both SFT and RL recipes support LoRA adapters, + and the same adapters load directly into SGLang for rollout — no separate + merge or conversion step. +- **Native agentic rollout.** Tool use, multi-turn dialogue, search, code + execution, and multi-agent co-evolution are all supported through clean Python + extension points. +- **Minimal core, maximal extension.** Twenty-plus plug-points let you replace the + rollout, reward, loss, or filter without forking the trainer. +- **Broad hardware support.** First-class on NVIDIA Hopper (H100, H200) and + Blackwell (B100, B200, GB200, GB300), with AMD MI300X / MI325 / MI350 / + MI355X also supported via ROCm. + +## Supported models + + + + + + - **Qwen**: [Qwen3.6](models/qwen/qwen3-6.md), [Qwen3.5](models/qwen/qwen3-5.md), [Qwen3](models/qwen/qwen3.md) + - **GLM**: [GLM4](models/glm/glm4.md) + - **Nemotron**: [Nemotron-3-Nano](models/nemotron/nemotron-3-nano.md) + - **MiMo**: [MiMo](models/mimo/mimo.md) + - **GPT-OSS**: [GPT-OSS](models/gpt-oss/gpt-oss.md) + + + + + + - **DeepSeek**: [DeepSeek-V4 Pro](models/deepseek/deepseek-v4-pro.md), [DeepSeek-V4 Flash](models/deepseek/deepseek-v4-flash.md), [DeepSeek-V3 / R1](models/deepseek/deepseek.md) + - **Qwen**: [Qwen3.6 MoE](models/qwen/qwen3-6-moe.md), [Qwen3.5 MoE](models/qwen/qwen3-5-moe.md), [Qwen3-Next](models/qwen/qwen3-next.md), [Qwen3 MoE](models/qwen/qwen3-moe.md) + - **GLM**: [GLM5 / GLM5.1](models/glm/glm5.md), [GLM4.7](models/glm/glm4-7-flash.md), [GLM4.5](models/glm/glm4-5.md) + - **Kimi**: [Kimi K2.5 / K2.6](models/kimi/kimi-k2.5.md), [Kimi K2](models/kimi/kimi-k2.md), [Moonlight](models/kimi/moonlight.md) + - **Nemotron**: [Nemotron-3-Nano MoE](models/nemotron/nemotron-3-nano-moe.md), [Nemotron-3-Super](models/nemotron/nemotron-3-super.md) + + + + + +See [Models](models/index.md) for exact conversion commands, launch scripts, and +parallelism settings. + +## Supported hardware + +- **NVIDIA**: GB300, GB200, B200, B100, H200, H100, A100. +- **AMD**: MI300X, MI325, MI350, MI355X (via ROCm). + +See [Platforms](platforms/index.md). + +## Latest updates + +- **[2026/02]** Complete argument reference. [CLI Reference](user-guide/cli-reference.md) +- **[2026/01]** INT4 W4A16 QAT. [INT4 Quantization-Aware Training](advanced/int4-qat.md) +- **[2026/01]** Unified VLM/LLM multi-turn rollout. [Multi-Agent Co-Evolution](examples/multi-agent.md) +- **[2025/12]** Rollout Routing Replay (R3) for MoE. [Rollout Routing Replay (R3)](advanced/miles-router.md) +- **[2025/11]** Unified FP8 pipeline generally available. [FP8 and Low Precision](advanced/fp8-low-precision.md) +- **[2025/11]** Speculative decoding with online MTP-SFT. [Speculative Decoding](advanced/speculative-decoding.md) + +## Start here + +1. **[Installation](getting-started/installation.md)** — Docker, bare metal, AMD. +2. **[Quick Start](getting-started/quick-start.md)** — a working training run in under an hour. +3. **[Core concepts](user-guide/concepts.md)** — the four objects in every Miles job. +4. **[Training backend](user-guide/usage.md)** — Megatron-LM, parallelism, checkpoints, and hooks. +5. **[Training script walkthrough](user-guide/training-script-walkthrough.md)** — every + argument group in a launch script, annotated. + +## Contribute + +- GitHub: [github.com/radixark/miles](https://github.com/radixark/miles) +- Slack: [slack.sglang.ai](https://slack.sglang.ai), channel `#miles` +- Contributing: [developer guide](developer/contributing.md) diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 146749f664c..00000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -gguf>=0.10.0 -ipykernel -ipywidgets -jupyter_client -markdown>=3.4.0 -matplotlib -myst-parser -nbconvert -nbsphinx -nbstripout -pandoc -pillow -pydantic -sphinx -sphinx-autobuild -sphinx-book-theme -sphinx-copybutton -sphinx-tabs -sphinxcontrib-mermaid -urllib3<2.0.0 diff --git a/docs/serve.sh b/docs/serve.sh deleted file mode 100755 index 6a723201047..00000000000 --- a/docs/serve.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -LANG="${1:-all}" -PORT="${PORT:-8000}" - -cd "$SCRIPT_DIR" - -if [ "$LANG" = "all" ]; then - # Expect both builds present - if [ ! -d build/en ] || [ ! -d build/zh ]; then - echo "[serve] Missing build/en or build/zh. Run ./build_all.sh first." >&2 - fi - echo "[serve] Serving multi-language docs root on http://localhost:$PORT (en/, zh/)" - python -m http.server -d ./build "$PORT" - exit $? -fi - -if [ "$LANG" != "en" ] && [ "$LANG" != "zh" ]; then - echo "Usage: $0 [en|zh|all]" >&2 - exit 1 -fi - -if [ ! -d "build/$LANG" ]; then - echo "[serve] build/$LANG not found. Run ./build.sh $LANG first." >&2 - exit 1 -fi -echo "[serve] Serving $LANG docs on http://localhost:$PORT" -python -m http.server -d ./build/$LANG "$PORT" \ No newline at end of file diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 00000000000..bcbc904178e --- /dev/null +++ b/docs/style.css @@ -0,0 +1,9 @@ +/* Regular-weight model links inside the homepage CardGroup. + The Card body applies a bold weight to its children, so the family + name (e.g. **Qwen**) and the comma-separated model links would both + render bold without this override. */ +.model-list, +.model-list a { + font-weight: 400 !important; +} + From 441681084cdbe8a215612b066b839b98c3e56cd9 Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Mon, 11 May 2026 17:45:15 -0700 Subject: [PATCH 0041/1130] doc [2/n]: add docs/assets (#1098) Co-authored-by: Zhichenzzz Co-authored-by: Shi-Dong --- docs/assets/images/arch.png | Bin 0 -> 273013 bytes docs/assets/images/miles_logo.png | Bin 0 -> 669310 bytes docs/assets/images/miles_square.png | Bin 0 -> 365100 bytes docs/assets/images/p2p_vs_nccl_scaling.png | Bin 0 -> 133006 bytes docs/assets/javascripts/extra.js | 32 + docs/assets/stylesheets/extra.css | 1156 ++++++++++++++++++++ docs/assets/stylesheets/landing.css | 486 ++++++++ 7 files changed, 1674 insertions(+) create mode 100644 docs/assets/images/arch.png create mode 100644 docs/assets/images/miles_logo.png create mode 100644 docs/assets/images/miles_square.png create mode 100644 docs/assets/images/p2p_vs_nccl_scaling.png create mode 100644 docs/assets/javascripts/extra.js create mode 100644 docs/assets/stylesheets/extra.css create mode 100644 docs/assets/stylesheets/landing.css diff --git a/docs/assets/images/arch.png b/docs/assets/images/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..b809bd3ffee17e4c361c73a9cca454a849c3a8e8 GIT binary patch literal 273013 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�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 literal 0 HcmV?d00001 diff --git a/docs/assets/images/miles_logo.png b/docs/assets/images/miles_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8c7bc52a3394071d9171cbcf4561939d892856 GIT binary patch literal 669310 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

fEO}%n{J`7`5i?$Xd)nc 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

OOgks4Dz4u;&wHGri&-wTL41fU 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

IwvnrM8gwAyqAf5>tBj%;%bs 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 Koy;8vPyat-3rmjx literal 0 HcmV?d00001 diff --git a/docs/assets/images/p2p_vs_nccl_scaling.png b/docs/assets/images/p2p_vs_nccl_scaling.png new file mode 100644 index 0000000000000000000000000000000000000000..c1453899ade80ee571a2455a99a9796c95609caa GIT binary patch literal 133006 zcmcG$byQaE_BD*6k`@>UDkdNbsB|kLsI-7|NJ=Q35`rQIh>A!Hh;)k}9R?wABONNB zbV=Mag74h^-t&HEeB=G&`{x_uIp;YCZuY(Rb**cyx#pZ}KU7wf-Ac<$OF==g_3|a@ zs}vNQSt%$sZKmFWp9rm%hu~i#ju$l@Rc*{1U9Q`kQYc(^w6(Tzw6-uhgyr^cK7Hf3=+W}qHF6-X6w0*R54K=zPCe) z`hWk|uG4p4b5s1+PX@k8SIYjMU+<8*6c^if^uK=pMp=3K+5hwRJFaqYEM5A~)$sEZ zyLNdT`|mHHrIld#uj|on-poq#U)S^V^$q&3k3W?<|Nq0+*SK7YQ?6)e*m~p{HU8Pk zB5-Q#h$op@z&g3R_tvdj${f2c>K&6RJCT{0nd%^ajNy8Fx)y#i8y+p`{mOyhfEfJk;>Bua6={ZAI(J(GLk;>$kez`tf?#z2qw4 zyZ1DUl6czQ1M{r(V>}lwTo4ixioO2vei*;i9{m#6!(l;EaTT8*@16N(Qy!}G?X|SPH$#r zX6x*HKQwf&`}~++$VrVOA|l%30?Aa&Zw>zbAqQ2NG8uEICI|8NjvP5{ z)qaX}lAAkIxnmy_Q;p~lw|)snSUwgr%8}vWCK?(VUyI4fNns0i#VF0N$eGWg-s@t+ z4tixCZm(vW2EC-djo;oDyN+iX`B5yW7$Xt5IMsXf=+XG($1m7jet)kVX^L0L(B{|F z(mKx>@}aw%87rHtnzU`pR_1%VPhRV5PLR8(rzTaicz>r>qilDsIc=ySe4VQo)Y1X zwT1Py+iW3MHiu5)ZZhl4$g{Ioxr{9=YK@GIowR>jy%{AJy3eN?y3afEnv+_?7U#!1 zwAxbDog`k}BNC?(t_QMzduCL8Yxa8aP>c^Tc zX(zKfY+lpabv(|POH8apxg$I(s=jz=ZQ+|uiQ|=o=i!l&ckYg2j4a9Xz}~(7xYAIem_gbJ z)x-g- z8XlaO&^25RwrOdI6m(m3Ow-7!3T9U-i|D`6_109g#MRE{`(4l4BIn63qSZ8q4XFMl z+1MhV<*D+&wnO#%-rZ>O4V6)y&ekoo4->RG= z6*-Q+=4@PBUCgm)VcT``T0k%xCnxPmjm%cN@pX1yUdHkE4As)mT9c9PyfjjsdFVJP zT2wQ~Xmdb7fDjo8&2E@_i^CFp?YeHuo#itpc}f`?}Mt8cU<1}d&{vBw?i zZ%I3)}DB8)@5N~Q7EuT!Y&Bbn4E1}bsBFU-T!I*nr?yZhUy^J#+J9TK?gr) zU;pTO-VkeM-I=|CIEnq$L3Yctrl$VsrMYf!*4*4& zUztrzOn2I$#x3R=LIh-Mm;KgrN}Z-o48;BkmwFflQ)#35?OYzW7%h|qgGX1i$k*{>xjb7-bqO>iB3 zCE=NRh1a6_nDf%~^-8BV2?;sO{Fc)%C7maj*rSzsPsEuinUkV^CXg2v+HZdgVwEJ- z#^3yw&)hQhHpc7EjI4gdk75^cGYx<7HhTJjgxkxr$I~=(j_^$4sJU9SMQRRNTOJFo z({gm=V_)mcG2Yx_R&HvJ9;1+1c3c<46SYW+X@Gr3wBmwjZSBX_)@`(anv9pHZDN#JVAZKOG6O{=)~U zY2BLodJ_~Lvr_kYqu;-O*SEI5s5Ln|G%`|yd#ZZLrBn5*zn}ls$MV1HbzaMZd)r@M zq`Q`Htz~9o)A;7<30?sKv*TA@7-nea8KvP~^CCARSq2802D2&ryWZEYPlbhqvVZ?5 z&P572Q#FTmTl}pjaO=C!(BR-_YisK;HBQs~{QPv(L~CsL1j{1abiso@ljJ#$;lk~W(~{>AGb?pi#UyC zUro4jhK`Po?b)R;KfldHjIx|Iq!4u&3aFa3uypq7uiDxkFT0)CJ(IKh9Gsp1J;JkR z&zQ*~4ny>0GTGSl%Zux{xcrf4r&(B7h@EUt#E~R0f=gzm-iM{7CFfRf*WqBS-iWH8?{i zetmviIJ7*|FPm@Ob+$cK{p2;|W46$`@P`j+PU19JwIpuUDf19sof~B|4@c_DMJ0dh zJlUN?yPNYg*4gaFjhX>_;gi?WnXH=M(C8G{9-dm4|5jFeLUMQrv82^s6&M~8qMkWy zrnqn4z6sXz(gtf2R?c2G?A7`tUhI78=NKw~H0W#OP$cDx;8-Hh2>&im@ z9w~Ws@$sISm%Vvbd)WgsC21KL8Qn(3#Kq^kZ&ej+5TdYddVTRDOxb&j1WL{YvZDCi8G2zn5Zym4xQDE0E zWKmjD(uoA)=JvhVMalSJ*B)-&8ZEQ$1$Nq@wI{^Iqj5fLXEk%p0u8$XP8^yWqD3|M zZ3e%Dh%9cMUzpS>{Zhm9m;9`5FltfMd7|%)hCz~h-;Iy=x0djgm6a84ald!$*sH7F5AWUVDbY&mGd40h!oqjsY5QBXl&gNZQC}&k6?hB` z40`4tovB)w>TN8}w{SOI4AgF|L{XBQUHPC>;(CT%fi&KPbR^sn5*vF+T_kQwbbkGE ze}BK$`&&QW-LhJq9o*cgrFS<-HA%58%%#wNu+w)looh>5N{o`1msjCI9pDEu>}u*@ z`Tf5Bdo+xjSl^g=d$S7Jo*TZ2?BL&b_f%Y8{|7%h{)Nhsfq`R8ccO&sUMmis=HqKE zxTTO4wIQLtUS7cEw{C{#W*u$qw|DMPRIa7JS^zROMl&OFpje9ApsZf!TIlLd1r)-R z7N=E|o~Xf8wUpN_>+f=Mnq0RhPj)uSP3M?5zDk!13gL8iF7{jRg6M(Q;8MCS81Uljb zd`aTCC?kX3VdKxAKdI^IKcVQkS#;;$OyiFczrE0h|I0k=y)?Z+A%d?GDec|RZbrtt zc*aprvEjL2pXnS&zWTW6zDZ1c?YX+ZEIUi8si}B+_`)=ft}eN;rsf>lZ2+o7u~3!0 z!;LZ3HP4O;w=_35pLQHkM*ZGBRzw#8$TB4onmalNShr*M?gaPVLI?8~Cp8r3CCq~j z;?|Ei2#hN#DaoP;=D1AhXAIKP(Op1Zn8A_xA#lte<@hHGM2FDdpCA3hBs`0HmVft@ zigp-}e=l?p@s#zrjGktDpeM(;)#C3TVdA&*+Z}lf%03O$2J<^#o*imn(eFiPS*iKf zp3>stCRuCiQ~0Z6ovaSmJ4<#J>C64t$|o>iYI=e1B{W))JsYxaIqgA8yf$R>4tG0k{)-rqg!> zl}dTIZ(hB6)pn{!ob(R;)p&@kwDczIq%DdraAB07;T3c&bZ>YL69<`3Z{>OYzI$%- zW4pH<5ZP44wEl;z)HcVa5E1LKJS!49h~xI)$&+iPp@_1sTy~0z#>Svv=jta1w7s7a zNr5cE$G7Xmi4!8BR1{mbAFOv{-L>lzUH!!O!eDY4@@s125bh$1A}B5GioN*YE|6bgAekRg4z- z9F7wFfM5=e9ovM=Se0X3J&hthy^^~doy^q)TJ-7n(fs^Eo^<`QbVdq&0>CSJ;U~fC zG2ZKy;~iP*Q&YX=jr(4FK}U_eUxANLFU=Tl+PwMtWOv>)nS3`dFOO4ip^tWKn0LCo zOp>m+Tw;DI0?8`npzZ)YcTqt>tn@>MDDv6&ea5xVkEvP?jG&sA&oy)i75-GyDB5I1$ZsTar7!`-}d)5y;VeN ztK`nn48Ud>_LR5@c`TjI)GNM^;&TX#b$jvm;Cykh(P!F>4X#y8pN!5B%*g8ER6h`` zt-bxPH(l>-SciG9O6I3(WIe$R|H3x7PWNrVvTw!>zq|E=*}hOW{37(Owb}7%v-SJ3DZhwETR{Q2N``T6#x9gK{W#1~k!Y}vML8;8p4P406eKFBhMPn@`XItJIgjvhCNY~0Wym2G> z3d`QTNgd+nyiQlmi3s|n`>&5}XN-!9$_9)jK8l5q#ZHnPGY$cG%?CsrFWHt`S+=*wU7%7d61dr& zJcLf+Q3s#ADsNg;Yoyg+yI?RI+S9>-XSp<`S5r(B6&0hFv5o2ty-0HAuay7ZAqpqm zo5t$(wbk5!0D1y+w(()5)q9=CV*K5MwTz98Ngh;GRHb#Fn2Z6=2QtQ&X8PT}q@jfw zUKndl39c%uxD6k*f*j%6u*<9&S6~NKxP<3E zvzB@0q=r}Gw_V-*?%zLKRZyK26T@m8kmJ3+EX&^jWO*XwN)?m$-^I}u|J;KvE-oYw zgoM6U>#F3Q$DgAbuYB`jShln0SQ2nXz;)VCF(PV_vWvj1dbg8%_ad$INZq&;Lps+=yl8Dy^IVl zFo`TYL2l%sndVH*94T8*kPD{}vRP-J92D0ccT)c5bdskblWc!W@s(J3Xf@!iumz4w zmE47fOq!;E|Dlu+)spK=N3)!LcO+bWh!jqidHqV%g(F3MPR@DoK|TER4(&aZ zv+dS84>V6z0nO%snIc^xEmv-epd9Zm5ik!PLwYrCesei>kw9YnvJHC<9B8n58IfC? zo14q);o%`fcCxq@nrp^LQ5qp_3jDca*prmBK)?jx8?`AcVN;ps?Y32<5{HFhz}m0q zN#(=OZjKbV`2k7yX;c(F#p$zWIkQzHJ|lrNL?*fa{ry7}<*(%2ESuK0{X%!HgmC^u z;?Qmp5fU;)1@I+RWHdUoj$!$bVz zsxaoz{!2kgse1C;dtq|1^CT^ZS`y!Ucg2`~xmUyJeF_S%rJq!RM-xCl&nAAZt~Lhj zz0sDs`}_CrhL)DJl$4a9%r6~d(C7Ue^-Tp_tK!WsIC8Nla$#ubQKI~jHj-b4UP*}R z)^hv&cEwqv^q%F{ajE3{cFuuSQT!~qRvv@$=yQXRN2D)aoLTTDF&kwWloO1a5ZQZW zl)dfD^EJ!vcPH2bucox7=-jaeLolqMDw(MuFaH&=B$`}*t;o@Wpu%`Fth?SuEfS|h zwD+|d_*^79whq3jp3;vcZsf0TE;GL1P}#`F#+FL{K!mlv>LQJB;6d>Svc9L}#_yh5 z@iJRUC8)Kr0GfbXBKbGJ-S6u&R1cKn<>k#7?>J`OpnE9I^xn!DOH0cywKa_ z97keZ0> z-2eYD&Z!Fuq-z{^ZdFU2uc<3c@ye*VW${!MC6fH3ZO7qWMuPZzumwCjamh^GLf_+Z zFK?8F%U!u5<-hHKtHLwyziTe?kpf%>rq8Y<5XBil#1CIXBOu4kc_o2?kxjmDO|vE_ zs$q9-S6A2QJ8LY8u%s)<>8urRBImQc>YnJ#x!dxl3d|nkC(0#-EXs`=g&I!M$psx0 zH^fnNXJue`J(i?ay8ILwh5huz&!3ev%}^SCwWg?QqP+!pqPMZJ(bU#{t(9w5xZv_) zZ?ve33=*2!)Y{sbQ@6objJLIQdQU@}-|Pm7-tDoz37l}Fws<~8*}&WT}rTh4dKo+zb+z>wlu~_ zYADogW81@{AJuY80nqmrgh~bc9H(Nu(~)aR%3JVjZA4Fy8&*gz9}kh0rndYTC-pd~&FCXH;s&dc zTAGrSSY2IRZ^%mmP?B!XkDEXaIK1bvleUY!{aNjN>xT$%Be1{JTen`+)z!_^Yq}x2 zhxUp-Nnzf~FVRX$KXdmzkF0;%U*Ezcd1c<}7#rI!RAT~`sw62M$fzs7y|_(6LPE%W zF4;0djq@{7)l3~v`H1>UZoQ)9FOal+2(3j}c;Df}hr@&&5)BnL>LpX@&0eLvd*(h= zE8QYT8b~Yy^*VW$yPwFo2c&8UKZ~JjhCfScqhG^42R_ zWo#3B3+yNX#|ydBdV;jCGYQ^`>Y=|AcQZB#XU4t;_Hk|6d@~5Wm!UOBB zju~G=W8;sJiB^3>av|UNkFc?A@m!f}>hz9f>n4qs{I14P--{w56E71eLWFYi|G5x-cv6D<(BUU-+R_?agD zVV^-9pP7xHyC6tD0Puynagc~~JYJ6d;1$Kd(^X7>+L1}K7R|{jM->^9(I{Bx$s`y4 zd42Km-1-bkHlu!UJCqP|#67Q@tyYN}z7{DIt}QXFoJsFFwWo&mP4(x`c8Lf_`g7L~ z>5JRyfvJF~#flKt5I3pKIJpUoV^85JK2B@my!=Zr9kC;**weFva-?@)VERF>p&!bz z6m@hvJup80xb*houaExQL1*nI#YhH$CZCQVBLJ29Z zCvO*tKj@*R#51>?u2=k0aQe~LH1EIbA~yaj$rZGIQrXuczUHipf0<;qn;TI9Z)Tgg zhR%$dmi7Z^$ejldl2|xWPeUg)thlp5$mREbAgmd{`2CVz``6dkqsEQ3wVC&DX}?Hv zN@h)>q}tXx%A{B7?v%N_$N6b@Q`1f`aKrRpLb=SRH9c3Qq@*0nwxw);q>k3hy0`2A zPC@G0X^*98u@_}Gk_}FxG2hhEl;^wShYH~tm?b*1>4k+V=rn3AS6-fah-S{y?&t_J z3roC|A8nLHx24QrRc$Q|!7`Y47rRWkdajK0BdLsZ<{Y)*t5=Da8EsQieaLY9YmTuj zw6w_I-$$BV;@QBx{o-3BT^5hk!nRbhnHJq3Bpul{z%jR@g{f2=NaHHPl#QT{h%OsE z*BP^VCMZij!4k#7F6Sd8y~{Y4VtXWCK9%rXnU0VJ`rC&7i^NX>`aoTgf{+e6E}jEX z1UqUqv3QR~jr`cD@SKv89eei7AHX$lEFd-c3BEu_{Y zhV~2ViVKDFda9}InktqNF9rOxnfs15`!&dbDF3^{1^^rdPUA22d$$q-r-Eu$PRxrJ z$ApSpudDpECaVUM9j;395Dl*Lq;_kT0V{-WJi=!Lt%RJ`uV24}Ruzssi$(E-CV3pB z$|pL3p?Oh-(z-|#pi1QC{xf4B3c%h(7YEj+jf~s?7etAWuIG<-=TgtntWVjUy#90% z0nbdCgcK3w<6pLy;4G>Ty3o5(ZsVdhayO?=Au_%$QW6pp^z)`@BZ7jqvvW8@Jm?8J=LvRj3lZt( zh@4A=Kt`S{Iyrx$?z6M^Lk}Uc6ZD&!w96{!LTXSs(?f*JFB{Fv@eI(sKR(&5&H zVxiwM3bk{9xguR7>ma*po@w2lQ~D)gp^9rmqUCEY+nD(tw7iovS71b4s`O!eCF%VH zNh;2??&Xc|1!sU_D9|JVBTLK3%z7o$@Yj*DEZb7!0L?n{tU?t>2knKgW$6ckxw5r1 zT3THtC}D!c0W+`-mz0*3Jt`!`gkF@8b5Em;V<#@=)dSetf@~3Uo0aCD>?ysS3>`%# z=wKjVALzafmoHy-og3K+K7l|M%x_|#Z6B?DIYh(DPuiz<(RfI?+1PKzC{j5d$@#6q?k?Lt1>l^7KKU?SY-kQ)r zH0s0l!wGZBk9XZj??=;l#oVkbz#e|~AY}v z%hp4obzolTpjmIiofmj4pQZTeC52|yJb>xUMj&QT3%@3*oIudQ(ISJtYIHb`6IX(NFW?K`KKl(CJYS?@1R+mIk=t_hp(LW+&Dc(Nuj~Z^n8B~F0R!~^@bD2Cmy|~yof4(I&(WbYI=m5;M@pRcH zwG4z)0&T+wC?AiK9q)N0NRUUm@9nrc0IKu(Y^Xlmed;7dI@&uzx$+%%g1v<7#n?@#1=kuRZTl!} z)=@ki8XUgHo{yS)^5n@Wh*F`M3XuX)j_?G1E&MJ8qVD4-qny6=@o(?5`p$wcZy9%} zsXLQWH%#vJ{(B}xEw!ca`Azg85Eci7P2U9ZINw!=*%W3K|$NfgLD?pLL#m zZp}eSPR zpBrnFvn@sc>f2F-EglQek5kURzm;XkN|Giz7&=E3ADEJsh%BHin+Op-QWDOqRfBBk zxSiLCLrk34k9NlcW&t@hHJ6}HCU+EaIqpyGWlc6P=f8P@)GvPM(7CLB8P8(J<*2tb zKgc7i%e&_amkIf*-`|u>KUU? z)SIZN$PvFhw~gi1?BBm1eNKaJsLIrP07Dn(Kby?E!MqD8?AFuMYXaBk5t0Xw3aU~< zpO`0A=T2!|qhGIl5$~4m4Q<7)ukW+S2 zW5(rRwy5=`SuoVv>c#RbIyyS&wZb%lT_9ejN*wZ&kCD(*SG~#zx7dC#iT$zkIQftF za53!96Jk$MdxTm$#>Y^wvLblvJ2hg+ZXLXn;mw;F8f6g$1qFuS+M_H0NZhyUU;E0} z_B>1HeP*BVu~s$F_j54nZP>7ZkON7kUzR-+PfALb>n@t2Njr0U!CJqIRt_!;5Jjs} zb>-Q+c=Y$faM&WW!#^*Ne;cdBpK!SZJ%~kT+-3*FcqF~oJ%|)MJhk;^M}jX@g!$Bl z6QOlE5Z^1Ura;PM-~1*J78yCNejjIOxckE!(75j2y?gV3UWdB!^VFtgNTmL)C4DO*I!i$t05Tfr?nEew~TeEWl6#hKSAryQ4(G zMj|6DMjTpikHLa?_}L{xQ&T^*&&EsBKapFPj;y9JJpP=WoqaChO2}#0759s~?}8i0 z2ONXXfWd2N8ls0qGL~+9B~CaVQMloFBy6A%+%$_EPvEe!F>PwxHVe9v8A|ojD+$?o zc^B~j1cb+Z$tKwo$(j&+W~x~U?X6lufj*Jrw94cZ*Vn|8fu6%>E! zr1-l#j}9SU0ojxAu!8iFf+zLUr%eEm#IF5WTiXd;2KPT&ewc!&GU~avVEn)y#-YN5 zfp;YcT9y)ya=qg>K=&Q2eNsyrGp9U5+L_HUPXwEZjPs#V$PXeFP7Cy@V8T z0Dtij-NG5;Dtdrj11E*2b%O+~0M~LFb`W^e&KsA%-CSE$wFP{zkki;M!g2;CGalsZ zX+c4M%~SW`)kJ#wntlBO@x_qFhQ8CK!k2OZD&@;lTE1wlVv(5;c=vL0&VVfa3U>;K z+xu<455)~XPe>XbY`tR=TPbIp}@zI?P}HoP6Y%8ZcS8-PIdPNzK((lO)WR)Ng!x*>0bzNl|Hn6kIo@o zXCv2?xX$?Uir+R2jPjACDCu6i_tXZ^dy#16h(;Q#nmP(&1cF)`SV71~7R;(6m`;vI zS86o=c|ZU8xMNZ4Q&rqxw0i&!dDqRjGCLT$rInR;+(|DZ>{8O6lsly^?ZoP-wXF*Z z2;2uIVqjnpivMGV0?)I)zB<*YCyh_g(a@aFvusnFf~Im9U=#2Y^88G*O=ou8xs`6V z0iBEgF{9VG95tU-VFa-qz#_N}Nxs?57JlFN7sQv6AHuD4EnU+GP{sTH)t$5Rum9ti zA1JmPsM$%dN-$!5#2MOg;K0@@Cc@l}M89=lmXc)(x1LZTq z(*6MI@$%m5478C_eGg6?(9jUlG$m8g70Q!6&L&t(} zCwOt2Wo))#Uxg1dIkavNCF@RPBn!mHyJ%_R(Zz{`@?`0kc|3%x323VyWG&&NMMw%; z*o!8T$8>(gNDJ!ZTmRJUfNk>;a?TztH^&^0WqXp%8MrN$R#xzXvR&ZAh?v;1e}4c# z*%1MOJvi9#Vcx?jM8;DtcD_0H`v(g|)rQ3txG(L%oReHkU>{*`z?V9Bo{LrHu!Vj} zxPf5ZrFbpFKNVAg`$z z)_0bo6XjFVN|?VC>K1LneuGG0Uh;y%b>+VvD~nSG9OEZwVTT{Ej(q%B?p5=^z^`Ak zTBJx|`JQ2r9hoi?G2=o;6XV|mi!NQhO!x{5^>a5;Qqr0Jc+;1{8sDw2o~l;sauA&` zp_%yQD+o9Ge-IgFGEPU_xJSE;S$KMQxLUyRpZ8l*uRIt%Q^SD0Tc%qZNK+^q7WGFt zIUluU>WOxU!6Bjp+@xswr2!qC7Pi+(x|vF{9hM z)wHLl$NfMrTn34}Z9qwHJrq+Z%8=dY*VRHJ)YX?UlJ& znAHFo!%>0?*2Irs!e{d&^hj%1mEj0k$b62RS(lC(q1iRMl5p0*l+DjSLNA!78RW6; zbl>?(H}5|))x%u$hcDjvANzquDLPfP){83>gb}@q3K4fv?~O+qrXy@3;X0)=)q_p! z9DH`>-E*TYGw^q`6^*0A=NM<}y6wC8SErfSG`pXM0PR=V?a}nvco8C#%ESF7o=1>B4og6bd#LKIi z)d7oWrZ}#U^03 zxi9_bqKJl_kohg}Fu)i3nvpZ=P>wGE>YVmmaU{eZkQ?q?f6;pBzo$n2t3t+v%_VVc zRZwSv&j|tj35=ln6lPA$8eav1ej(})(9^Fstb(`pW>qIRM;k7}dA>{Q(C`$}AS_)Y#PMJff z1w?Kj4cJ0nc$v1dx#w+ys1$lPPAC%0`I>PV5IILQ_VD|8N|#VXLtza;yzkH zW9X{O?y<@sABb;6K)EhWAAYi5*cYKP+svg`v}liiVfBT>Tl8lKjA&xDKJ*@@l+zsbJ0aYNGRx|V6sF;|X43}p!mXVQ32lJbb1tg5< z)&Q1-c}DxaFgvn0sR-OsL-=3N9d<*4maPv${XrPBH^7RgPPVtYnjuF{o6sP!ZIx0` zIm^r21>CEJD})335GHxT)D1$T#(}(9LE&+He0&!%#{(skmygc`Z3xr~=LsBY-{ld9 z{WxiN+p-K|FfPOkwdfk8^?a0(m#kk_XrB7c$al4NGQQ-23KYsth~enpu;jE zOd6|8Gx6DA1li7zm?)^P57@`dOw5E_EBEpg01XSDBX6ZC*@3IYZ)Lr$y+KN! zjBb$<`_%siGZEV58neHwZaGJXhu20XmU-v#t3<(5KczoX{&&6q%_yeO*aJ~3hb0c3 zIrD7M4IvX0Tn*OEZBfx-sJ=GOIs&p`$a`<#SR2I%XeqPJSlY+IiBLzPmT^xYxb*lX zVf!g8Dza}*790n8G?=iV0;i#>R;~t-+65}vt;H`hW=w&yrKN>1uCps(u*j*)(=E}f zgeHWyydksPZ!KlC3|`C9bX+<0b~~qfYBu36u+~n^s1LAM&)hSMEJPTQ+o7oq_4QT2 zmd`6F`0qSm!%fjd4o(>BMd-!AC5&};XeQLc&(P7DLEHod9uR%0o}nG0rKJUm?F(^W zfC+Wb6c4*USBT*jp^YC8d-LX4KKyd+V0MQF20j57g`>+lsb3<5l*l{|8}KT&ny`ke z*NtfFpIg$O1!^K9JRKg-dN>v?CtM|t3N-Cn0qf4#8OampFR|09Ch;02=u#>hFrz2E}(Ub{)Rxd-?L9P?gj4Ve?DgfeM z%Q3nr0Q$fgJ!{WfI3vxkro2$yqF^ckk#0)VVC~PtfB+NZb#rWh32-(P*c_zNWTT@i zzd!O|ruOo)tL4JUTDfewZ{J=m{fmh~HH(ld#~oZ;G#ad+&rE${Txoj#{P_p40jcwJ zmq1nZoTdp;ZP|Oxi&`%Ld}bq3(dIfG93En%1Px6kJw5%V7VzKl#Gcb3l6=x}-qK=kGSNLawa`W=(7ftN|JA z^!F>1F+%v~Bsr?prXUa_WP;q_MfyX20)nJ2#~fW+YEhER!TGh&70B{jrT*~%%N+?< z3QeXgTW}8axY^mJ<4c%DsYLZpvtuFh8#%IWQPmgRo|R-o&5Cnx2JUS1KwMEWtkQ&=~l{CF=5Q|SBT{Bd`hfGL#6EDl`X79 z4!mqYYpO zjA&YVffeIn62Wj!PZ8Rj#>+<;9%@4Qvs@>hW89)M`+C{RTJv#D0NqNumu;A#YaBn7 zY(FGOJ;ybnG{5=*0@>h-PQ|xxI@84$b;9?xh%Xfo_Q~JH4icAI_ju6L(*BwxCWuI- zXci2y0PRIRaKVxfZrp1+J%i>X9Ycd68MpCOse_JOJQby(0T>X4g`%ZM9<@ril4LEl z=mb957!mBd4En9-Bf1~LnHgMbB0?<5yF-}Ko#vE&0nholj%T5L@d9X?e+`~^XOkyv z+joalt%zghy4@HKyZXy5pc^XZlNas2aCBqoO4*k7##y@Z{jJpD6#tRg`9&$Q1YYRB@Fh-@%Epch(kUzy^<55TW5%~S1#r=%pi^ER{M$epqs-&^_>C)l%NbhHMNzBo8S;B@z4!Y+T~;=g%J@3%HHp3Tw6~Zgm=M_6ybk zf!qbIM-!0xHWa}u({pG#gg2xkm6E@la%_AH7_DCf*Ix3F%S7ih!M}uQwL8&TszV8A zZV$`^Xl(vn0oEf|csmc%sb1g2N|X&RgXBpc=BK;mm6KkvWmU?~cQ+K4I&PKFN8!@= zRtUFdiPbyzjM4A<@Q6)-Q%p5|e0C{Wu7Ynhq_+^NUV3w5yO|C+ZUc|3+ur++bAHoAQ{Wk=XeC z_N}V7+LS|4HrCelY0;_59Iif}p(4uiONxj@U_5D{NI$Os5ne+ua*kt(3D32>;XQ<^ zDkftjY*mGmdPRH~a2$ws9h20)_ljOES=r}+nCnw`Xy0P}W-YH@KE|+s{zkVhW%9V% zQ5hD6qh@enKJ~kIFB@!#gQeGyheG&SzaQn^F~kI+5>d_EsaWBXay?4ZOUxPc zV*c>`_C2RGVo|KohmEQ+h~8uXy$cH|xX{hI_l{t5V- z-8ES3#@9oo3v|XG1dS*(k=MGA(7V8sY;Mc|K(U`~+T#)M@Zrw`E#TJwAx|9J323Ok zd1H<=5?&@~31ID|W8#b_E2YF6<4_j&!2r zb|O1e>(uvP&Q}_yJoPPcNX`7 z_lyiqM0;4$sH+p~;h7R@1@2%1DS86ltbcogFSY*l>0vtd+8RJP%9BVf#F&Ma7pT3G zv*xNcDaUb3AalB~>}rcTV-3$R7FBm5EAe&@3VseQpM#mlY$Ou$(0j|aDRsTeI#G%{5g}Tjh{3UN5rd*gpv$xpu-Y#WCc>#+=^~DiK4Z9hrZnwbTpCpsCIPl` zL7#zGAIc&AHx6hHsQ2urPj}=)pcy*go~Sv0&-vwwj0vX z(z45R7|VD^k&Q!|gDrXsIyV;vVrmJ)B=Ex`J7^`&R(XzTL(p01tE417Bw%h8)8E8% zYqD|xEyCo6h6Xpu7k(>TE6c+%4qQq38s|5&reO?t^rSH$O9>w)TLuQ=d&FwtSmrfI zz8~h^ka6$cCNmfaFRP|q<7)e3T>WHbRypf4rl{)C2fB^gqoHCke-#}aZMZc6ioHW~ zieO#y9*_Ec%4Y-xhdor187-aT1KsyhU{Os2Oy?(q#ktJ0^HDofP1`8H5Hr~W8FG?- zMQ>^^uleWT-5h|g{ck|_Z)I1|_uVEXC_v0h>~8^x9ZpyVHWyyLK_`)^Um783ATERh zm?qZd@h56kCbX{~L)bzVkb~STS}C^`r9VDXMc6_VBM?9{w|aTB4$GLzuNy2lVI{^bA3Wogy35c zOH25q@>1JYzjq|q%=;$x46L9vVQ!)QQA{^4nelL?v;b2-Jb^1tmrm_*_)U55cOF4E z)XA<@z5;tk^bP^0{O=otv!LTK*F~X@WceQ`+{_vn7}$mR7utk`3+Jg=(UsKI@tB7m z+k67^8SlC0Giiwdc+wwq_6|dfBTYv;9IlqO8xxZ~K5H<#?i!K!6LsUA)$B!PsH9_m z(J=rz9yAQtK!GNh&^|NVI6|&K?|jIcfy0vh_U$iuf(j(mBf=m3S?pzr;qvmboy;~z z2Z#QI(T*(E;ZoRYhS$J32~Y7r3~-zJ{rd?3y=`K6M8s<()+h^Df55hHq0lY&iq?37 z<8If(g3;tz!=A_m!#osoQ@yN61r&FXzK-H`7dAa5l@J`&Ebyis!i>7OLQaEb%%PFF z-&`9FvhY+}P<#n~vuijkKR5SoV4y--dxDqPmqGg_JXxZ!sAxQv-TmbP`8NwVN(HwL ziTSq!Nn&He{gAp2?RhLb{95`*C5ocZ=)343bX6Ciy*g^MQ?#b4KX%zlh@b2jk#^Z6 zJ43R7iy~^VdqEw8dN%8r>*RmZV`*WLVkb&Fu6<1n1Qndr|Co3ul`^`nMbYlW-HsCA zHy%KuS$g#NasQj`khsFnnoh%%J*@o%8T|wNgm6o8PM@V~GXFH_I}7_Kp)Dah{hb)+ zw(k9~4&IX!CNom_HCR6}D-psA5zcD!+p?(EavkFRKB%O0iTwQhe$q#3`O9IGuq!J@ zF7d~0OL2zSH`AUJ#wES~WEEIu(@DoWw%=61d2>0vm7ssUJL{wzDRm6Uuh zHov{L!auCPpTY|>9X($Endtb0o3(GyS$n^eg8rab82Qzi`zW#$6kkGlwxjO1mHc1k zNo4F^UP*v%TV#`b5VY|b%T|uobS*~0{^CXua9&C`iUm({VCn%Gy&BA#a0@`iV;Db% zg?kZg*Wm;r%xPC%aBP4jiiwE{u~kXTgGih``<(0r^(1N$rCJ7R1qJAto$zQxEei+v z@8!`y1S+4`yW-A<5~~j&q33$Dc{Xikt=*e~_<;)ldTb4ymHJ;KDOR8oZSsS_z>9F( znn&P$4u;(*es(>0gNBUNI_{R)K*~VCEl!^sjSgn>@0gHyxDJi~iV9xIPszH_G6xLO zwpnHE!;Kp^sL=&vn5u^IjwA&^BN_|wf;S>+jo8+7{ZeL3cbtQZ|L4R6y^@mBDcyn{ zunAAiCR4HI+AP@jlq@eTp;Kg`*n!vFpuO*D(8I}5+wi6FG6zSz*M3tI6OWv6_r>1_ zhCSiBJrrE)I?5OQw{RKxn+5MgVPa+;(ti2pMN&4Y8cwj?OCk>XLH989`WlW__<0vX zR`QzQl$|E;oh{5XmHHpLTLpwD`31BFDP$Ld^oPmY5Qea2?(t9#j7;>Gt?q|pakv~vGe?%repTFBpsFe1U6tF*!V;NuTRYaUEOOSWDqvrf zp^}LSVI6${!C@aT<{fwn!4MNO56K(t-JW$!*_>PTVi5U#FVU3M1#%McVjlAzu*ZZi zj-Ew;9&W$v{CpYAAi|^fN-UIx0xuk^WV&Ex#`Zcso)9p4raIsznLapV03g#b^$^s; zV47~xrT-cC0oKqa&4I%u+zfRNWe88aQmi7YeEktdL=NNSTeY>dg%T91+w+k*m_eJW zl_D%OJg?!RYU%?U-X453P$IjhXq|e^xwdX9oM4t}_ntJLthPS#?2@g!3Jkz)OU=iH zQXfYNc2F1^R!Ew*wRmiEOYu>8de7^@Kox=sY(0De$IuXOY7z5X*#-9eJW|daw-UH? zA%08X$QjfZ^#)lGMHr?DBAf0a=Bg!9sm@65osaOR*kof;{?ci;30Giaq0PJH=U<)& z=aW@NDy6SyEz14S7@I47NrN{76ne4Jh`+mbNrmzyp7hBr_(%CYaVZd^WkZG=nI4O{JlCXXLHP~KcTi|&vf4Nvu3(>kG+ z;OEbYm;DTg{(UXIs>(Z0wLQh4;zDp-B@d~#9sf+LU_B=kpovfipKWD>oD zX13u5X2Dy3x(;p{R%2SVz=41jVwZIWmC5E9;w4phft4YenPq(g=fE;Je69l}mQV~J zPh$N2heuXYY{~z9;$}d&H!O?^9V!*vzwTV@j%%vD{^$lND2PdRUcJ&i=}%uLyXlA7 zXLTrC+Br)mf5dQxkOCdv{gOFDcPSb!4^X>F3-RuGE7rnkzbUnp7|PWlL_uhY8!OFR z{Zvsw0aF!4>`J6UoN^x)qa;G6+ww^=)p2B5f8eOWvJEvlJ1ReZqy%sK0XPJ$3l$L^ z=uSU_q##~E)fOrXP6vJcV^GZ_QSSjR!S@i_W7|8t|SK^wTC(r)!7Jjf{N3BRgojieV@wlzVmcSQ5}G?RpF^ z>-;tPv1Jfj`biXdMSwtdJ>NPzr+ve~W#F`(v$f5()Fw^N&CdE>I`-?2Ym4bOXmPL# zs7)cv>Xz3x-vR2317h}D|Cj;Oc8LYnfUDJMPri+vw`CEldQ7I5BxbRMH&jREy?iSU zJ59&`;_J<$vE1H2;HQK_QK=}Asi+K*M5QuQNkpbXhC)S(%u^ClhD2m6WFAVHr<6I$ zSeeQgl01?S-p}rw-}^o5{o_6BtaZ*hr)RqFeeZo;pXstYASLw$1c7ErN8-F^7m5E{ zTAa@sU5J;v{jSX2{Zi-4yHnfbzTS9;+iYhU&3}4Vfxv!g>GSYh5cw_iFT}4+L;~mz z-{ATO#l)z&c+XyEeLiI$mm zx5(ffh{4L{(PL|M7_(iHpDjs33w!4{pd%~X8QOIGw)yAbq5dJhzZ!-*Rp zYOpnsh7#3I`>}MWSN!x;uuBfKM~){~RNdQ}Hbq530qMSvcS6&Zbr?YdncRE#hKdVa zSpCBKM5DR-5$&K!^p8;%gra-8_am6{_&HAvXc5kpqW7m za48-jVYfj__23-j!n66B*6085AIw>dY7MN0)c4%;K&Wg%w)S1yIF|Orji6#1;C|TL zRE-3zrkZUz>dD&yQ!WrGWAU`6HU=O?0*;>8T=vE;HgG?5q6+wMPKDr&M5;_cl& zy?F*O&zBXuC_f@UfK(%5m(&@kJqD(olS&&llZr9r@HYCGUa!E`b2GvMz?I+cn9h_M;~E zvOsC|qW$w0adRf(29NlKP0$E}_9zRWS5xn{)HsQL|NJ>_vZxl>5J;*TYRli{KYx7M z`35*NA|^#YME2Tu(2X{_OgAZZh(XXe#o5ep(18W~NW%+-3N&B2za$WXrI^to&l5cQ zC0HY6pzS9}LLW%32@^I(GmRq9>%|)hKkVYuhXwe=#qVoA5!G*`KmGCT3yG5J?BU!* zp+tdkRqb;QxKk*GHA-m$DP^-IAn1siA60K9wlvfbUd+pGvBAfL{UCKFU=QgAN;~03 zt2xkwgV14F4TL?{3iT)+ICQ_Bz9+I40y@K7ASbC;u0#8QdO5L!N}O*8xmA^wk3d-| z3SCrwQBgmxDDg~RVE2>vB6?e^9@&#Tw)kt^w;P$st2y?wU|%@!bSVZjy)me85>5#> z!5+v3dcs?9!L^noFMndi_8cWrBXw=49DDWUPq{>11}*V7gLzY$XE8mCKU+?w$_TTW zAhmBbQb9b>g_Za8aN>c%9TY_WPV~%hbS+2F=jQoT){q6hT!3}({nLRvHMX?$$7A{h z28*ajwjPlztEAx@>1pRIOSblGP{jR31=4mrcKM`69UU(yVHjS+} zdu=n4$Pd7&7iBUBmh6a&%RzvAfx!9tpT@d9J`%X`V~5YTEkA+RKx=d!jy(jUaN?m{ z%f*#7s__{IiUSH&Y+F-ZXDoet8GSEB(vwKeM_s5rdSyW&e?SxNjROz6KVt;-gC02lOQ zpZmQq)9c9W(iZ+`|5cs~NA8hU9AESIV7Os9TP2t_$D0O(tDHI~98NH7BD#GV_6b#x z4bl%up?U7yIU+8FgD>V0OV#Vw$uiXdj))x{-YmTxK2d&fU^^S%(rW+>?v7iho4f10587Z*P8zKEB%bxVMXeKJUx;Wq|%6FLL$?Ef-D=C7~ zZ-=N0aSC7V+z6{Z{HO4SYfTGEp}}ml8^i~alv}JIT2#}3>`5I))(r$bfAc0Y^f#R? zyk2X!1wHKnY}tC*RG2iL+Dqk?w3=Gb~* zzbeoF2EN*n7*Z>Jz1)ixKmzwW!mN38%oY)~R=T*1gs)3GI6AMeFg1eC?SdP&k?W0} zkYgZ@V(d@B;K1k4+a3->;-zH)9hA^Rb7&pC9Esrje&d@rx59_O3FzhR+OcUlq2`Ntn&WnUX&Jj-QQtJKltxu*&wOh!k6Kq#t0 z(WgW}I(#oF+$pBeWqYB4iJYwLLl||`%yeE}V!!awD?e;R)^%x=ItTGvrsKdMo3OG} z;Hk^i$J~-*H4CB{112Z-?CpN?b>4C1;B(Z#(I_S|IY89)4F-pX63@ibYcjyx8$0ir z1|U~48+*Xmi*A5cy8$?ahrb(sjNr$Gm=?6d5vO;y%}=qQ!7ROT-SL6IDKCClfT_)c zpPuZRwmNHk_Co2vLJH&bylnjZsp&K5$sA@$8r7IBPq6RQxvzg>_L%rIc1BG(MR(@h z2fs}6E9`n_AaBib75?e^gOC!10dxDTibOhY$&~d1fpSKm>4jMcK8FgAH;M_eGPFbe zUh?by+&;!`D`-B3BX1){>Cd_2%E~oo*mG;^9rq)R@z7x@;rNBe}wcM58YeV}fhx1&IFj#gZM`ONX9ha2T$0Vp1z5IYhX(9Tv(? zI2LIcHx5{Zrf0oDn%oaU@U|K}!!jnm@~n^bFfGu=ZoUA7_BA|WFNR{f^VA#Hj@z1; zn`>XZ`3<%MV-)6&oTAFfACsV28n(!GTL19U?8rn8>P-cbWr96T`mjpgix(R>6!f@I4CLEbDy z45bh}OqH;r;<%|oHBdH^lroB2bqg2mJhn%~&@piyb_;-p4)?s`<45+Mo*tseL|^`N zParBsf*I+Qx=4B5ycrx8MhAckn#T>`z^brW*zG)qWkO)fKRADh1`@|7@mC~jtb*zc zr5Yr7n+bdfaY4$BY!mFZEOV*}dX!P$A(vE+z$?>UH3Uc>42gXqfC-j|u7*})eflF%PuK~f?;AHW|( zL&sE(bHSlPC7!E-&nmPOWg}Ub`(5YuavpT>08#0+hYsl_p+FuGG$0RC24ZjSFCTHv zfq?}kpeiio9P3^gkZv^K|e%ncM3!$X8fk~gIN1TN53;=BnZ(@}g^D8ujt z0wFgd&L{-Pwft6>;|VrGt48iI!R1KgP~-@Sa6l3M8ry>1Tnwa^+pqMN-o^j>3}z-D z^br{&M#K^muv zQ~ckXieceG8B9;~c_g|TaG3J3V{+t{xRxkY5K_>CnCME>i*tC6fvZqhGH7^z$>`8Av`~d8~BsqC71_1cx0$R)S>}i zvjQjWA7nnjKBQA%%RhYM9a2GV<1LSFzh8lR9>k#>@WpqC`fW5)`v{oTX0UDp&S-kz z+Er-Lq-JtPTLWi{IVN!a%H!J+*b%**zTHGjWF!OLJ`};8a3rT=Bkh-;qtHWQ!2ZsWX6mQ65Nk4PnYFk?D zQS_I@c1O$+*!hrbcN1+lnzKq=Ir97QSiC2Sqh;!)W@x#NfFQ|679J^ofd!Fr9qrgH z)IwMQx>^1`M8la$ z^*fPg@wv03sDqw>dmwRqPff-^86Z<~J^bE%D>#uGMhpjI@5+l0%Mr;;o~Gq|ToP`UlM6j7V2U^6Mlef)u`6<3>Yj5H6XtvtBA z^;;y1Y>lMH9v>Je+LA+ohKcUkbCh%r+xK^ZPn~Mr8-M{J@3nwtW6nZ`QyIQp){qhp zw_~vQhaQCZX7I|cm(2$VpA9M;&judfazFr}YhWQM&IR5W^**QU`riQ~)VC^Vld-0( z3nAbnN_Q07n~tVn6p718G?IyP6$oFn*^Ax5_n5r$m%24F(tywaHSF$dW}({E118Mv zs!IVt_yKuqrB%Crcm4HL4WwN#H}?UR)rUqZN-Y=H3P}d)5NKjaK87(CF|!V*HihHI z-iX7iCkI9M%}!YV21_)}Yxfw0hN+=X3g|f+|rTEdn{W zS@hZ`99|_i^wh!E)JHTs*Xwl~4b^G2@xZZ#kae;Q^#7YT6Xc>QWlyQ4zu{>n!2R;Y zZR5M2L`R0`b6yBhj%YNmrqgb(`SW|(Zf?$12lk~}%9zu0u1>$qon|06VfXy!UQ_u} zdCPamcGP{Lm%1c=uB7~SiizjX+$Ev=AxvhfVxiWCHw87w8WP<5pBXq*L2((i=`!kN zzK}q9p6O;O9T5XsKh0IXOskD0OxE_D5k8{$+g%yDL#eUtbz_n~AIpP9MfbHkM*Hh` zOkQp0`!4UtYHMxykU928*>%aR``Yh>EXvqW2edU;*I(u0-(cixLzK75kv`^G?Xz!{-_YL9|beUI1cKbTxJI`s;`RSDFO&@M};2YF>J-_z# z!?BBWyK{!lWOLJ6be zNx$~eT`DToWs?l@yTlnCL|9}?Hvru6#&LmTGyLh`>f9aNy^rwi z5}uB=*X|MIz&@gyctw)K+a>Mm!EL<Xh{+Ci7nmi4$Y!R6aj8B#%lTSDn ztY~abpMGN|#^k;=@GV!!+Nh{|$=CT!dE#>8S9A;A&F-pYs(QSG!(WMqbu(Ouh*LLZlp<4 zKVo{ZW>;X}cOjQQcEzJ5(;+W{9K+~Mn0VGo7Cwx4=n?qbO?Iw7z1LOy`Nqm@#mlZY zmT||@x^l5*{HgLwZu);`L-8?(EdRwV^m8A)jkj|zDfqr#Cm`$fe9q;MyN^T(cSzl~ zt3rv9VH=&c;EUc&*N`xkKPXo>+mhOLAMMvQq@AgS3 z>D)iU&uL~n&}lFx&2FOXKF2b0l8a#n%N=8x^E^xgic4ijwl{O#=3cn^(8!iX-?=8$ z!Q!k4xA(vs$+lq=AWpzYy#W)JW6}{C^5kvh^BQ2`A;$&i zUe#lE!|= zXy5fq+CnPpHOr4gJ?MGDbbNlzt+#&;|2+meaGTjAtZ4j>3HmlPJf{Y4IjZq$gJOns zx%sRjo5Wmi@<)v(s_6ID_07*V7#o?bR!;CZO?P$AaQe^1G-IPn1{df3Sg5Y^=5sE^ zc4HfX*m#2TC%_+SZIXjSVwk~s5Z1IA&nO2}2_C3dKvElDyyy$ak(-~N7VbiN1Qlg; z%s~zmh*y%!3MP?w{(v1bRx|BNJ3c9~ z>3e_tXOs6&7eJ-`i1+Jyxh5v#8pr}qvh11QSfRaDDbqxfV2lNbp?P%w26bmju^Q zaZ($o4if(+k+QEz(kFdROza&nGuUobM7FhT8$ zm7@g9Y^dts1>K1}VW`q5KX7+>gjdaKV|WeXG+s3}t~sBRpL|E|-)&%E%XAw(krPuW zzkdB9VR=NF9f4_5r!RHouSLJNoTByleO(;`E~%FzU}j!C7o;hJ6~*?fySUVG zuVN#wMz@7ZA-Y}V`u&o_DL$J8cVie9ffOK{g3HUu=*OUzcT=g_I%>(|YaT4p%qy4S zlv@!u6_+gdpw2~0Z+7%1@CvsT6ykRU+ySUV7C;oDj{^`5xE?5iBAllrZ3^w)$$xOrP2lPZluBYG|y(V{B|>L;<)2tVnMA zeMDjeWSy{o7wlD-AZluE&V#C&d{ZcaL75}m36mT)2?ss5!n&Okf0-J*(E9GUZ4`!Me<~DVJGOAVepO8;bYrygZQ~RM)0wkFqqc z>1gN>!~&sS*a)}}9a^}~f~1^%)ezpPUKNXVB)-Q$;C zc=q&cz71jvrKEICVj$?CP|%Y{cVwM(ld4a*kct<(s2?sf*eFu<&{C9R*RC{KLoV)w z&t`_2FnlNb9sV~ZB1m=;OXjSB0mW280aC{Pw>RwUXeq_+OPmyf`A~GMY;i z@eY3%r?T;Ha?yCTG2sb3|BoRuSfhN&@;BCTs+R*Ht`psne~GT|b6?+My96r=rVldV z+N|BW^)AT2sUZNcTPUY*R+*+Dz4(Hg8cHYP1_qdb=hIGad!F4|Szne>&Cq|xQYstI z<0Ap*$wSLl&Za4m(J1JwXCJhVLbeMEL4`$Jf8_#b8p(CT1Hbo57aJuTn(pYS`V1xr z>yP5mhpU7EjGq2>xY{;dyzB2T3z(5a+KrEAXny?91WUo=hOR%>Qrr+rM=1xO_5(_v zwx8g`Q4_DAAPv)|?I#-Dqq{c}Dh>%ek(P6KuJ&L7VIM@A$h|yH0F8KHkoyrMz5tAU z1qI%NO~7olPLw%K?*!4zi1UG{AfGotT4;3T$~}n8zd+}ub;5mVvBc6DX>~QaNzhx6 zYy?8=I#NyBe!`=OHM$Phi_FVILcXx`o&SyQ3~K66;~L*gfx7Q5j$nd1kb@3j=xyx3 zK%8mu)Z%j#@Z_BysSgz-^8x-q1x5B&hzRH+j{E$3bYmO$25@eD=p(OH8vP8eoj-oE zF&svSHQtFo>jOCfS5>r}-kMCT}|Bjg1$1dw7xIEM2x=x&?ID)-{ zeDqpYRx(!O6=I`s_8=jONyHWV7@mtSBO`u*)PD?-l7@Uyqzn-SBEgZMmw$uZ62^X| z3Nb(=w2&&v1IV<&NDYny_GCOK2^LAMN-89zE{PsPJP8r`fo23#X@ux1Qq$l$nC`r4 zxIgJM&&cO1um8Mbyn1cB+|}vjVy~8NQpc@Ky_e|Uv@!ZV5@+9Y_@``hMYqus*EV5Q z&?ER87uxsEo%P3`!vKv1hESqoCNd<72CL}v$jB;e95sWsK=2-e_udb~%1*XSVL`!F zI5iajW`D+DQ>4!zV7nCzCZ@fWqf(U`L8NKoi`|9$-4{UsUn?cg;lYDM#@SO?w*8|- z?Em|4JT9_+Jj5Hc;Rn0)bphf#oe~3+-BMs(4YUy!)Ge%MW?daqHA~&^c;9vB5&FitdxzTXuZ$~SVdSO>DyoO# z7w(;_CnHZ_BKd&S6p!@CLfnR1$G6L2q9UAPfly#S#=0ce^}(j(OiiJrt;2xVV>TMO(EjPdCbJaC7Qrtb%EjPR)hFa}9?Mqq3)2C7ps zWENp0#hKiHcfXnr{tNY40X3UEcsA_Wp43Xc!$Q@XGxrL-B_t}!La<08>?;)LrNs@t zQp+Kbd9Kw;iri5BhMfdKY!PLv8x$Z;rM^l*XzOi=M{LeK9%{2{5JO+5@8S~}m_K1p>Gavrrfz8br#gnv1 z9`;?>*GVKB)Q$tFEXxr948FSp0pzD!*i616vPnZza}QWUGTvmpLxZp;L8T!OgP?rS zwVzs!SQxYtG!z3A{!I`5?r=fYFy^r2*=%38Z?Tr~U~2u9*$sI{bPr`GBUCZ2*ikNc zFZE5W)2HyV&&IapGNOskJXAv^p;Yw*+e9WOy{@Vn%{^`(XA5Pojg3u>L`Gg--sloU z=bym`BN$AZc6KkMZAY-l+3h@e^jTgWeUOB-vl?oqH16KpG<<%&4(JGut z1h6d^E90u3uCoUzZX`1awR|Q1m*d#^0Au{DuNXswQ_cAN`Q>;x;R#@LUziTBS`^dN z&=5r_j&x1Z3xeb#y&%RfWOn{SbO#X>fOix{uo$Ari%Uw#Y?sDgi?9Gb#H?Y&lrn;| zz(?+f!T=a1P;@_|9#Xe}Ga@?y$|oIteUehl27!uYC!TRya5)s+v+@Jqu4(pWZu@6F z56_f^8Gb5?suN5b?66i??7Yn)Yl}%5D)B~tp``%}T?OUr_8qRD1fKNp48F2p(@u$> zc}5r3PTOo+gjGy>29SbuI7@Ilw{`r)Gz8cS>0r1(M;G5w2st7HH8+$;%1$RQGWu_a%ERg!GfmS0KSpX0y5?L{nkgMs}aFjz@g~U%9 zt#dfV@ZkGE!wTy_AdWX)0P-YQ2+}1Rq&%E!`GM1kfTu_ffonw;vJ6BJeMU8hcyW(C z+jYK^jRQHRDDH_91bei*tNg)(;I@NEHb!$zC!! zv(4<`K}Rv7QX@4p7Or=6^pFem2^0#dehu2R>kmqZA#zHm`4t$GC!=GhFmI|ZRM2aL zqyXY{?1L^hY8)(tEY>V|J1_D^I2SbD0?A-^!)#HKFNj{2x*z;>781M zf%oo_pO=G}Es|FS!f0S%1I|*kPhX%>&CbbL7Nn*B9yb*b{g(%`umcip3UY7kzkTpT zIh{5H0VIx7aJ4B(cHcg%#TC|U(`X>$fXO@Jsj0?TxzjBqDH~O`5twMA(cWKQjBpzi zJ*h6}8~o8pGyW=2`MZ2e4QSh__4$`KZT9v71pU~&!<&_do}Qkd>!_q5lr_$@c>c(I zcw^k|05ir&r{{*CD)GKFXz)m^9hsMVl^7$b%d?=JJBoy15&=v)XXM$c!}t%M!uh4e zrS5E)g0I6iz20HPvtLc15|&XgkE5?)ek`kuLcu_ZQ)H`yb`Z7Trri^eGX_OQ`g6!w?a|wlm|qktyqMf8aR~G%Wrfmq5hPP$QW|Z&!aqNd z)4#KWF=6n`r|2!Tr+F}rT?bm(AlZGW2f=jGVfj^&z{nRO)mhO_&wJ759d!B4f~!Yz zBnj%kNcR>P!1nztYN&T9ak#l&V=zosLQu&9k^V9CS6(<{zM!}vwk#7SDB~&5EQQR; zTv;%utB_*`2G}?69zYOds5*;Bkmk0GLYVICOR@-@U(3Scf$|uSz)>ydNys9N0=^)2 zG<9>jgr%w77814yVgBCBpH6EP-T%9pyKK==sz~NQ+34RXxlEzy8^mC zEW3B82@MWEjT+pHx{ai1Blj&1g)H1hB!z>J-k>-u;Oc)my^cJ*K_cPQZmV&>>p_d( z_K5ZO8yOkvZkq*=eiWJpZx=eUmB27YLRpJlcXcH^hNzC>0l;d?%it4fb16`*kN;tP zuD%+ZJp_E1oEKr~5fw$%dxAl6lSw4!h)kr%!wuc=RnU^$LPDzp_Oz{hA4wM^xEpQq zS4;>*qlH*%CX7DAWUp0Vxs3wi(SjD+`n@?1pY#?o#td}uDa;aq<&GFhZo;F7$ud;k zJkP(kgY{roq{g;c(^B`_Gj8K7;#XMK?OURKGd^aNl0q9I34D|u;?`TQ%9}Ua>o2}- zdFCHvP84iAROl zgGh)c;;t&8@52c-J^mMNp-QZ;y_>tES*~j8X^q1otIE>%6?_^sbz0u({+mp-td5MH zqT`tipVjwdp^iU}3`|h>eSO~d@;B;F=pokRxaPQI7=BY{Qj~d1&0Tzj6moclz^B@F zuE;Q(!%URxnJ+BSTH=Pt@vS2acj^dYh$%tA!SR|he=(4O`fc{RGY_3QXO_S_-m{hO zieVu0eV=~UiI>0k&urq``Y|xzjEIc0)i$I)`gsKo+~Hk!$GA&v%D1La^h(EzcV51Z zW+o;ip;i{tx6yTZS>Ox&jX#IT5emLnNThPKzzx3*5q@P;(@BXFIFqpM@mP^UhFDZF zpXX|rNx&BDw!4)O$ z{}St&VuWTq>L=d05^c0le$2OJW@w^)u@fdIVd-!nr)I5zIrR7VWSug%L-rFrZzHPN zDsfDa#51!wlvYx%GF5Nh7(*eM>UrU>vwBDQ!*Q$@JW?dm+5*cQVh}RoGE_Cu$nkGu zR1;pEtUK29QaCJX#hdA4$~r|Hdw3UE4rMd;g}s%lD_E~yRps*17Ic=#>E1|};DAm$ z1-%c41Tv{s@fIdJP4rdcg&Sp4sMc6FUMNTm22 z19Xfn7F&?4>J6<~at~&3_dzR!36eAvqUMrjez%+gV3CMkpai)MEei46{D9sLya(xc zVF)~r;x@R5x# z#|c4#nsGAVbPstRfP09rlmN&jh+q}XAMqIzX;)X7I~SU&S0E8UF091-c#`0c5AnfE zh>3-QJ@qyw$3fiaLA?4@_`(iD*g%h*T%ziM>WEBxgwC3vbJ*%ro9j%)axZ;chyI@o z&>{mE&=#XtW#ZVcKD>Y!b37d~B9)^rxX~32I)5qsV#gD_oc+)3kDpAd6nUq+RwcyL z&u!f%_=Q1NOQa^X)g%497Td$GJ?H&cWcS7+Tad@^^?4n&sC)VR?{-lvaF@dltKF{o zT%X=*g#=Ro%U0X#BLNp2E`6*5t{Cyk>ZU=2b3mWU=GL`RBQ&EeG==M zjM5~Df)_6e0GC|}wJ&^Kbj14#0AR(V#ZtEgJ(FuTt~e_Q{F6Gf&Tj<)kh==R$;b%E z*+!x-;Km?N`vi&B!#$M4dja_VGCX`6kt-W8aQjOF@^ZPMbtmd!5~5BdIxukkH)Zp+ZSX}vv) zG^mv~Wf^|$NnBT}^ow`)N2x*V3jeMC75Qg7(udj&FMdA7_3^Fw8$+FztJKT02E$j+ zy?Jdav5@mG_q+Ei$2M@b)CKfbeH$AauGNdw*IsidyBRw1ByQzuMZFrKgzl1C4f)KQ zZQtFkQD0+mYo}=XC@t2QvVp4W`m&Om4$o3%|1jaJkGs_0Dp3{I`@InzdAGIda?!T& z#?^eW(hO#;Te3q&(WZ!q!MtAa` zuY97bI?)OACLsI^xYS0cLo1gZQ+aQ7_IR*=Zf8fZ{k)TQ#9JXFoy4l8z=UyHKeM_1`M@KanG{QB=w}gadoDS&@BD`VUVC1Z zAIg#+Nq2Tq`HMgrfvSa<7Zvi*OnhBIF2wySGW+dW#JFv*{1 zP-3B$#66GxKaf^XwsyywsU&tCy~^w`$#wmGGR}^#wcACh77Vy>8^^UgyVf!30{gd;h&Yr>Yd?u1@W= zEkM(hEFaXW^rV+V3zg#xwnLBs^SL?kyoD{ z%Q_0%hYx17JZVnkS$N&P_wgR?-jK&-AxIPdXXW&!go;;t9A7jNQGDCE;iyF})?QGU zFr&rDvm=s+MQ<&)2>i}1#K-0zq+jFki_PeG)Le4?f$mVCSLx{5%>lVV`pQLH0vhrI=gc0a$j?2QS9ZV7rBak9YbtO%7pI1H%Cdi+ z@1v$h>MQkS8IDnd7mQT=qAVhE-mAYRx0Ipb|Kwc83<}yFdMAeFZz#Jitw}i}QcfM# z4RV&;nf>g3G0vTT$siVu6(J`r*)As8-{cWE@r9-HKzY8!7Bek1j zuJ~zzCA*ciglgh{I-$GJCs)6NLZRCj7NnIjxx{PXe6&wJU3A~guQkaQR%{ZND<28l z+iEX}q#0~|_H4s8;UT8roK>M&hCEk?H=0(tQYd^|KO}1YSW9;vmU?>Kwn&x(8*eI` z`(n`r=Ua*Es4@ga_$A0W3;g~~`ya6wHA2wDBre}ALb5yAB2DCcT*NCfw$#qh(`^-_a~e?c&GKwg=Lh!^KZD*`W|SBKeM%C zd#KM^mBg}|=^MwkGE5an9hYESCH8Zp z7*EVt^i$yc3B|5=L)YJ?1g{LyNL+Gfy)1%`!%JCM#9N5_qsG0hN2nENjo4LvFOM7R zJlS`=aMpsOA}aAIH!qc6<7Q)OQf+EmT}xpE9^)TR)mJQ8|NI^0DEPhSRkhUNYjpDG z@mz1`mz|yCm#@Mzwwn*nnPF&X!`{}_vx!Hjg?e@}0dIV0LpJ@pURYVfBK!t4mlcmd zP!q(u>)Z}&o`C%1N8K*PhCIkOrzv6w?gtDf&r z{Q94{{n_f+03>@76wW>!_5vUsNSt~@nZIpLf%{n)LvZC7{3M?THB z+sbv}M4#~PVA=2Ej8k9M|J1ucoxJ(`XUDq6y}?~86;J3@q)!AbWlW7KfalIXae=M< zG6h;E_N}*ai}H+&E~R%k`F&45qbjbeozA?NZq{p6PeMJ-PF^Qgoy|I}+{qQG!ew$s$um?W+n zL2ROY^5mulYv2i9^+;iTjG~NygO49S-W#@Kn2OJAd8ihz4t1;EgI@rL3X)ztdj`9> zEPA4A-CQVgZUct~oDyLr{m<_4GkBw|rbArB#_>CGdkuwi9;^;NBFhmZXQwrRjigzh1k4(QxW&{q@o zpgC+A|GTB+g2MV^fY&J<@l2c~!;%Pr(NS9{~7~(2ZK{!=Y_w z%Y=({Gjy-8mQSe(?Daey(dIceQVca0|DA`@RKiNi#`!5)idzPlE!c_V1P z2wy^$3qAAEF5x z0zSVL%|B6`!5@CDyAfY@8JU>i14QdQ;;w*(KQsCS0S=k!po8Q;n9q5Bv;R|TJ!T$+ znDB~0-w8>}Pn&sCa#yPUv|IQ8SFt>7$%_>hGwJ!KyF0Lq5)^t}Q{sJ?RyxB?4vAr3 zduZjBW)xH8&1ILr-ltokMANclGEv%}wtHeJuxuhK{l3CO;|XqYj~aibk7j-h_s*wX z)W4je@b1&%C%qf<89e^YsaoZpdMjwz>v~xBc?&+1l-*{`w__}7enzPw*dg{vRdiH9 z$JAs^<-oP?V}&yDfpT>_I_z9|O72?7O51B2RVJxAZJ*D#8(J)>leRPPv>P3^+Nv~| zYI`~}^3a*5R~URVAJ^YE4CYJB(B~HK7ZLN4W7Wy>OjV2A+0jf}_aRUJd&`c20*9j! z^D_e1mN*lw@11E|HDSYA#%A+vvj7=&)*<3H$*9tdg!$r0@EN0<%6{L<`|+?}vTXJ) z9TVHOyv__59EPk`SBm~@5N}EbDQR0^pa;0Fv?_Z4RavZ~4j|i8XVXIyus9s<_yeV_ zo7t~oD;@2G$MXeHdR=D(F^MeJh3Yr>0xBcdh3Sq_%xtr4Pj~E= z06CNl2Zo;7Q_YEznV1BwJHd_xNXamZOzLqQj&^aoK%r2M^cd8aPJVn}z#|bUxJFUg zSN8k?L8_;c*7)?y>Hhn({)5y6aa}B)t7@(XP z-KgSrgm8pLM5G*;DAt7kKpThE3rwXUo<4ly52@|K!fM#eJFp>4*X_#E@qbM&t6KzT zpuruNy^($;x}LXo2UX;G)CkuJXde7Uzz-HzDQ%}twvSjW!s*8M~+Iq2>!D2^xQom z)##c2?U6zHeWojD$3`!-gqBL3Mrs*Hjg;@&Cdz{*dJb9DmhosaLl$a zV}_C_3ilK&(td5lbp)v)Kzj{!`kSSH9^tUYYp`SsLi?usq1&|N|l_MI69q-PnENW0(F6 z&s85@jK^&q)^*v-M9f^xkjtdwmh@(;tj4_v3J=(-Kd`tM!2dnr{qdY@t;s3bi*l^( z?!L!cPPgY&e!NxVEmZd*dYkxLt+?5SpL@+Nb;kvOa5d}w$j@xKc4(erPndl#91~vnexs}fi?rkoNzbNNP^%s@^5l|X#z7|_*r{1E|}|m zU%XzvQ(;%G6*FdnU&Y;s^EtuVn(g;WsSDwOni0L@noh4antl85xM9Khd6DoYk712x z&DtWGpGM6>_>?lF53}-EYuj|*WAIJAev`c}(>Hi#SGM`(MC;z&GX>A72C89eSp}5F zofyVH$r)EpXdlse$R{{3BuwRt(Hy!lKT!HmK%8Ge(I%Ut&4jgeySDMo33YWDMvumG z3(syOaDEUuNmYmt4i9*zE@j!SZBulQVT)7vauZ9(SmuI)I|ny4?K0`T;wT&yK3r#3 zyEeJ}p&mm);-gne3y)uYKPPmBmCN(|kbRLI#>X!7b1kXk3c=#K*Sz83*uC1ix@I}% zLkSps+kLI+P~q&*BkO!MBsD`Ox!3-i&e>8IUF2xKyPVf;UwIXBq6eMh+Stsg3x^yJ za&DEfm51p29oTof`SHB580YV`Grwa~wa2T4ZNa?y`}yS@To0lb*s%oy)=))Dy`m@Uva-zvaavnv1v&4BW*6U zFX|^E4VEOOP9B`}M&0xj(zCI{OS30qnqUp68S;b3pLabx z^z8md)>~e8*?#;;DxJt1)3ID(& zdmKM|(({=~dieSKYv2RzhNExvWY^5VH$R4dJB1K?(6j?WAm`Vw?>{8V!N!)1rz;gt z{ATIv$zIgiPBKSErA|0tXvVqjquDGBmZK#zJ(`eNm7L9DgyZe~{Ot6i3);2>ymIvZ zNtsi~9!E4Q=OJizdZGgy{Q?6!V3cf2*A=%L37MBP!MPY4<1{Mx7Lybq6k%Xy&I<_i z_YVo}*4={NFCfs;A8eHqs3<-zy)#?Q^{}b8m(A(Sofjq+ZGU|iY1xY-S(g34OLNKW zj|E!hGScG1ArX`P-8Z?)cJLmQt-!1H|1!Vz+CJooS_#kP?-bQ7FW9X^{{ymq|`P-gu`_9Xj zP_D$=!k1XYmx56+9YT?Gw6r7NizeD(xy&g0Gbj=gn#J28b=MqvW;-13b5WA~{0fF{ zvx#OG5`LO^?vs|D^RZfnA9(md&b3Ef7f$N;HF{S530m#{N|cGVJteDLdV0gYfi)3V z@}8}64RlcnF)hb{5cq6*B3abI*i5iR{fT?#`R;>77w0ooiS)6q)kp2Vcy6tB;blLi36ttU6`E zc`WQ*NjLKx8P&D_9`$U?ZjG` zGtx6t+=aipxE_cNyVo7GRZprmnV+GsZaBcT?C=)sFb2ZmB-0lWSQpWg_Iz)W3TNtd z8q1h#%W~7YB*nxoCJz;FJ8qK%q-e_n-M<$M|7<&BXn~W+X(-~tX)<;=s|>rYUV&|5 z!nd@6W#U4aPrb5wDLap27G@A1C+iz`K6*aYX;5r*nsXOCs3Y&WH+vl(le&W`T zp}PB7b)p6GzBJaYTQfjacgFDIx7TGI-10Ve{K%6ChPqQvl3vX*pVC?47CXdgw)H@i z>lss0T&M*_4DEd9Vjg*nU0+hbWQ zrVeuU_qd@{m(2Zl0!eLo&Sg@8R64hI%2Yw)c<;dXcj1O^DM)b9vpm_127|eV`l1VCJM-WeK&KP+D{M zz70vB<5cAw%ltSt$+tuB^0Y=`#<`s;sMym!|N3|~_hzTv=_3j5P8@OP#g9Mtkc@rY zWz}8yg3khB5{@$clMcBMkf>-t$H&TI)U;~x^o_#8@!#$*8#eSV#+nT^R>#^d+3c?T zViWoy5*7As(hUJr(y7f^x4_TT{Z*CWE+91xeE2WCwux4c=yoe55rww9SSn=Ve^CRm|t6%2r=s*FjsAU!Y)=MWlhh!H^e)rvpJa8iuV&6D0 zq`wbDYka|@H}~~P;B}b5!i!#!h2kH6KuQpY)=lbJ2l+P}SvF?AYrM)Z9@QPx>bmC? zDsjgH7S8LMHe8ZL#b2>N4sJn17am&LUHN8PX!9l#f2O4_`! z-?%i729wV=b>5~3F!%>jipdOFL@dl5xU12BSF-2b7VF|K6MOMV>QxUp12r~bUtjUU zBCEV`Z{*SUD7?w(rSr7(l}{IU%eZ!X{KPZH-~%rOOeTZ{cRfFHEmH6dIle|FKOV5B zNo`YC{`U5xQm%=0*c02fzTevF%hX*~;LR!JitI%%>s^Nu_!~2oST|LEQCFV|W3cSe z*gVY`F){)OWb8KYC;d>nuLn{*gbp9{8y?X6br%3aFdyx{i|L;~d{g^Waamem+wS!J zr5i%!Sh>iNOUVwldT(&7!8btT;!F%y`i9`O>mCRFie_ytz0bq%C~T-vm3dk9ReD;-w7nh7V@0$C}a&IZmBdr66=xJjbpSqt8p?2T0@v#D~)!Js?d$`39WgMA` zunQ9mwEL%^1;r;#qHABP1ubr;d8_oDGof=wn4XHjGCv~V+ zMODA#v_)FDY6!=R(Vh7VZ$C!ok{!h~wCSq3Q(=gq^C{FnSMq)4yV%c0i0QIB-w>Ru zy2U7&=K9umzKm2u1s7~4~q5N85Q&i|!gGxc^tg09j?LLc1 zlO5w^qwub|Ui9G4C&r#DowHcbsSC6C z2--DMHo98@Jar#~t73V-nMWoav_|TS-q$DZkIN^>_B(~U zh{TZ;b?M8rfKNT3zS*Inp6#Xo z;(?b}=wxNixEJnQV(YA-z@J}5j1ru0?)338WRT+kr3)?&D*Xo!AAX$(t-Bjl^c#0G zUn*KnE9+c$uLYJ=sAJ5rT2+1PLpxQUu!lgRB(Rs& z9xu?#hF2vtIQvD7V`Af@#O(MRIrBNJWGwndEg%qv`;{PykJ z$ib1083FjfvuoV;(@-d^5mvLZg1cJQ4cAE2iC2oU>g1I!A8SAJJLrH};qy?(uLsP& z{f=iVxDnnoqj1&6-CdErQ&KQNGjE+P^DK^3h4XAaxQHdy`%bhZ+{|2UYagtgrv0`l zBJNKc z%aN;adU`|BDCK5}>^_=_F!d-&**4LV&N?7sj%kv3$Ti~mk73wud17FUT`ShFc~TOGDKrDr2TJS zk4)*Ud9<}eQ`V=}Byf@Cyb70#s`y&mAa-*vAMoJwx5P*1fXw$_`q>Hqg206=KnfpX zbfvPFp2?SZwm3(Qc{ztb^xkbKEXME6Robyj@8c00lb5B5y4)352h0l`?^U0BWz`@S zqN_1;yWjgc_p}Qqu3hw8eFt9a(Rb;Z0#Td#Wm z_(Yk?J7IAzA+Hdku+Lqa%B0;?HB59 z-gZFf`StKOUmvyBu}yAb=I#{Oh9lMnx&L$HPJxucfiE#C{y)C$bZ0f+;II1rOU)#w z%bwl2f+KU>z;SN6BCRwmhd^I3;lO0hB?|=P_Fr_IyW}Y@Ke;~UHedwi@~1U?IQPm@ zpGW%i47Wo!vY*^LSj|}+`UpFXPaXlG&-f_+oPj_fR!F4}`E2lbkZ9(}*O_+F0gFib z^iQn7-8)Yyi82pt>tU3vqc!Ylj@R7bfJ6K9PrYc`(?wlo*Tfef>Hkhc0Pn*>r~8>!!N1EN)a_~myixddw)Q`vt&(S)6S~n z5a5*}Hg7km+8K$ClkgJWJGErEkRT}5$wu{rKlAn_z09V)%Hnvc$;E>Kjeh8HSks*h zT1gMQVwieD7=2{K@d4QvaLKn;rzXLg;1_b{Pjzzp+_7Hk-9AnTHUuzr;=di<-N|$&yE$ zr2)yzLJHShkt!U5cPp?b29h!@XS=ZBq!g7zHm`sJ5u7H6MD2#?v^pUPg-Y=)aGXYg zacK>YHv0YbR-qOMMpJBtW1*ZdihQswI67e-k5mM`y=`b0 zEuK}Phw@YdhxJ5C;xecIb53LiYo=YQTK=mZ~cbZXO`rujk!XjhMzy5j(UO5OZ#6?y@^i47ofsyKK zwn@IlA@!&KQWpPhqr*06k{N8*AFdq=#2eMTI|-(N*ioreT>E{(oTj{E+BG}&QQSV| z3R62BF2E@b4GlJ2Jcc6L2QT*kZ5ZZ0QUN|x^AIX;+gI~fiUnZh3&Izt-Iqm^ta{Bk z9BO-{WMp%#S)Xkk*k_R)^<~CqynA*_SXS-9Shpc>%`8^e4143$YNMSi9J#fHJ8f*f zI4-7wk;WH+Ohwny%~J{Pw@!eMs`4HX{khRU_ijW;<@IM3MpS~GMWT0%)_4)6chyC zsh49Da{Kl?Ug?U{D8F2SUuw*d)h)%cn9oX^uVf@_JCb3Saxv!@)3A@97uaQXxeiXt z8qLynifubAxL%wmIvBT)9RPRZK6cWyw`l5SE9HxMk6?RM(eFW9Hf{M^kKYa7`CZZ7 zj|4O6NRa)!O6ckDRICd2>}UYhhX5cUjLUk1(Oz6F93B`gDUn*Q8jV%wp< z%Xratb7m*A2I~yCy&A02Lv#`iYoj2_4hQtFNAq2cP~Wgvr$xQwwRV;- zoPVKe|CIw8Llo7e^K5!9!!qgxvvZY!dDIYW5~jH;&G~`;;uF-638zQj;gLSU{#LBG zsRh(}jRjbo8rwlFSvk2jOkGHf&|Pw4PWC&z(KyGr$r?xy;`1)UF-)l`g`)ZcCa_jx z3S06k>Qp)J=AN#!zPp&4{P_;g&Py_zGRI=kh9-SwiZPhUm8gAO!T)?4X^Xb52fA+6 z9+S%}57Opzjr}pY29|Tgwb2KrLk@i0JI+0ylij^%FH6bJZTQAEintWNakhjZBskr@ zTLxQ5JlujXVuiv9X$217=njU2`O=672nRwDqgrb-Gtmq>9`kZMgF>M<7eMSUU*eKm zKQ4jELFM{{aAi!4=o2hUgbeA42(cz*kBOH@pO|+z#8+_H*xK@TlKI)gXdd@r0D@7z?Fs zB$MNs@;asM{V%41@oM(S&RC6#h`$wqii^h)`&9ubB|x-q3;p@Wbn7I{Y>O^GVSNCI z<56I|8|!N<>gQqs9DvEZ6b3a;G_{Ah;XDkQIGL|E4l}_LK1`M1gWj6aGk^N&9K`ch z9SP+sm`U*#+%j5j7HgST2jT-lPzp&I^N1Qe2KpWjf5&;SpICaHfb0S=u%iVaeg4@{^@ObVwNVC%qpGR-II~3HD4c@syd9Nsxka8Tn#dm50f^(nyME8Ix8U)6 zS}%!UAoFJ5c+&>0iK3qr(^~bvs{(bgd(AI88O1i}d508q*=!WR>HN-N+KfC~Y-apN zaG|zb)Qsh#M^45)0+GRTlh&Om%w=yyBMtjtdI9U%;U>bmzIkJ{U0R~1KF#3(loa%r zkbBCDK?MH35QQe|*@DU3QO2=e{($PoRz4(7MHw8wxG<|zYAVT7*?C^yJUJ z{*ceVISvHTzu%eq_kX&dD?dooiy^Ka;biuY?^t`WKUM;X=O87R6F+{_5@8;?2Rn`` z&0hAFC3AVICwH@x`Vk&P}`NeX(*X03jrQSCznVEq3=w2vwT55(W zXtbdIcah@w`|oeGC$~(!!_693oo^DTURTH@=XHPncvJEsqQ94m9942ILzTo5oHzAT zjfDETr|H@?hrch_Oh0PNRHegTt|Mylo+Jtum259zKtP37T!RYpSb+{yDIYWMvi^l4=Ea#o1NbY~r!A>Rf*Ocfy- zxr1t`G9MNC6zM-Dj?J7;*UE3_DK}3L+9k!RU6*#!t+`&IY*T<73ZbqayFefN3#nLP zUnO|aP~iCtuLF8w!{%qlTG4fo#d6>fMxlh0aVaar&q-xC6aH-*Y31((w;aAEWhNScpVQx zQhQ=9HPLd&KDJ#i0`L!QZwME{(Z)JVInW776MW=+TIg3wGyt-_1n4U@<>E*wB|YA9 zh}a6JZ*Q5OwrW_)s3W9Y=8wp)dsr;K(0#d42TD``dQ+UMhLQ*wnOcT$2Py7C@?hFs5?b<-9ojmI^_^874wAW}zX1>@IWP{HY3L_UCjaug+FN;U-Db+tKg%U{+EhI|bq&=7FI8Xc zcnm@oIv)FS^!BMGTMrz8aHZ7#`>3|K2a_YHC;_n4%wMct_vCUq*KewC<}aPb;(g@o zf@5XC4kL(VH~34NUvzpTlrE zumhl)Rb->qq+FX6b;<)hZ8&`^lO^cAepeOxFE@NV9Gn5!)~4apEY^v{MLZbaxk{ce z$ic0(c&takazY|<6bq;{)7~D%t~{psvAy$|YDv(qKu69k**qAVow;s9e1asm3$Fm? zYA6qMzJk)>AiET~NSE=h181hsxY%LNyfMT<)4A`Qi1BiImbDimCsZTVRq4m)Hx`3$ zD#*(DUuSFcZVi7U9r-OfJkkDzE&=Tk#o-xn^$po~5s4_lsGk$eR?{$b0mgd+F;PI9`@V$<6%vO{E8wf$E%!sV%=`E-oLDSdO&`bqJr(+xU=bX z%k=|O=gV)$*WX34y6qgHGk$I{2}9_&a5{Zrq^%};SdrPwCzDEZfW`bI81K?mgVpdI zFB$?@CqEMN)xy7ek|N=}?&&IAzLn?BnUn#ywO(||A1l;t#vqPPD6KBb%>tg-8MYPJ@?abvp3TW6avFnKNgeM_P>_9&2nxFPE9w ztnQ}f?|GL_!AonWnH?;H!nj?2#&phQ%k-K{=`}br+8Qo{@HDt3K5O2C1t;624X3eo z#Pl0BV?4zP6ia!pU%vyFYWT0$o4VqZDa^R0h;v4UZhZ>U+YxLb6XYiv>y+a&S*n(ZnL=UZWE7hR$tx(g8 z88y5%^$nf`5}&)q4}Gu{@mku68lE+*tKV}OwIPXa@f4HV5}0FcSN7)9UT31t(lxPO zJLl)R>dU?T$YCHy_3v9m%nWee$r%&p-}mgj%)H1>u&opqUUdc+KF6O{yb)HsdB?}b z>bDSGYhhlM5+(?|?#qy4UT^R+!6IY(*GsP}yUtRu_mjb=ig_PIf%*E&rE|cG+p^PmlU8t>^fD zE>Fwya>0HEx~wz4;X=QXX&<}XQ9lG&%0IX*tappVcB-Bs*L*-W;s<|mla}4tFrxAJx_nI z^timF)wg%hg+)Ov9s>6^$FIM}Ncf^d0kA4e0XRT06q}sSC3kI!2=Lg$%FJo+cKP_( zLmPB*&maxRQd&CiCg3@IIDQ*IiX+Ig@T@*{kk~UG#R~TF3_RrnNGWB}b%9_aVPs4= z`vo^_$QRE|kj+Ihw75rO^0RfLa>}$^z&G4*v)RgGX>g8I^h0v-t;)DaVCz<8Xnwae zXQI&tl-$=*Eu-(R80PYP#r5Z5z()ULULdqzE6@6VfN;%uBz^K$@Ne6o7{y0-zEfCE zm4fbZ)vt9r78cJZpss2IfakUO)Kz3+jY9ki>{Cl>wCnJ~qjofE0rJ#%^WZPlCAR7J&&`YoYk#yrG>Br6w6s`^WAf_|ri(JE6=-5dR^ZeJDeHk4!W=R0ZIq#Xr( z@&STeWPgQ)=b5|Ic&x0lB(LHiEiV@i+sT84CH{O-QPR&drZ==7qCd1vt-G;T1$k`! zmg$NT4ep&rDv_OZ9Q`1Gx*vUe+tw%_yTj}9ycJ-#@RzBIqx(^*Bkq_=_>+5gwP^8Y zLDI^6yPa$Q_)^^qwWn`Uj+i%pvJ_!uUJ~-zpE=kj#}8}A3WWRTprh$;a}^>N7H%R_)UwjgRg7%mxAre)&ib{V4k+ZUG$T|Nsv z(~2v^2PtVfMGid&;Da|Fq-_mdWB?>Y3hbamzELhxBl}3t*r6|s0YR91_{46!OSSb*Lk#3bhc@+sou@RII7>KO^FtWM)AsS8J;g(bv_E3aQs1(!( zVL>Yah#@m50nXPCWUWcukgm1+W-03S>)yw*w`_mzs1`U*+G1@6k z1rb{WjwX6U=&4l<^y`8~d81K_s~q=Xw7vNTjzVj&@?Ycu2|;Ttz*c-iycID7!s@t|>qNEKyiPLFW*5t`+^DdQgR(i^Lph#z$p9+~=a8^pE5h*a1Q(+584 zFWYwJHZ*;gtIuNodNefbgcJZ|Oh~K2mvucJcD;9Rl{{pNcZ!|{GgL&n_#fgXUr#Ot zBPWSs{s464CFnjXq~BO@ytImfieJDaBocO%i3dlJ=0r<|RiWox`e%2YWEW8>iyy``Hr4wVGt9pTaosu+MhnqFgx-=a9cLGYNqmByLy$VLkv8CASCg% zenx+p2EltUUk2e|tLYxbCQ)ExOnR@)Itd9)jJla3K%5yS;^Xc!cE@F8 z)q{sohWV%O!}b+9^>ArU9GuoIUNbA?D+A@{tsPYJ)qvjiMzNTwn%MV=?pq9!UklR8 zd@WkvrS$du<2W0__}BZ;PE)w38NjX4c+#`GJkn8_;~j=LLOr-`x?ik$OY$NXzf{mC zA>+4O6|2sDI=uHoD-$9XUZ@^^7^kxt@HR*!=CC(LOE^jnH_w5pI&@zfK>?IZW4+z) z+JXf<$x!%$-*F9C-f2}(^j`^z`{gl%|M&Dno^$+xHn+znFBV3Nv@vH0qxdHzV}Dl58IrHjxF!ycvL|Jr}MyT z63yF!xi>Y(3Kw)Cek!s_=xbbv=DK|A*N1~>8WyapK9}*>0>fT6>af}Dw3*pgZ*O?5 zPPap8+4Ds}ERO_w!HUG3a(p>u-Yx`1gOK5shZ@=@%AgE=3=bnGO#rKBcmd3$?1^m% z4G15-!)OlaHJE_bhVshrVQ?;7_!O#tw39m$1_mT*3o76H0d3sNGmk=o%!@_|)yPKT zP0|LoYnzzFqZfKKxn^*Xr(hi!-`PdU%kRH`!14yC)u_H<_3A&!Rq*-6HKAlq5r!7w z|FyHPc7tu(&x((!?wY8?5`YT)eo2cPs)-h%F<|za2ty!}>bNH>HQ@qc4 zFDYq|kZa^Y^xhJs0fO@Ek`@(!-MsRM*zpCs5WeM$pPkC?eXs|yvJ@S$z$*)(e`IYR zP!itqHrbVNwxeb%;kH8m!5+Kl$y*p8e0-IqMDn#8Po7QNGkzca>|UZ4#EiAOgA+NA z=at_$)zE4tB#*v6R`3a^qJ5vZM^O^G?I^Mdcte2CTJ8czL26e>$p`hSI>VNrSm>Q1XlV<7){-Z(SNCM05EPJss^)&>+l(o9? zQepRvTpj39br>7ewo~B(h#1fT_30k(d9c)iahSAX^Xb)Qr0>1GnBr>c<1vy=$Xl!z`zb$5;T9JT3UjA%icZ7aN(ihh$IQ8JQ_xG zL9BV`C*G6S1_fbRY(BJfEY{eYgmXT8i@i-)rG~)PPGqxtv&Yf z(D4UCY4agGo;X@6=Gu`Pl^+f@#yRZx#*A);;xN11`2O50d8E-Ho1Gdf`q58mJ7N+n zoBN>ORz2}(!`y64eam-RJM+`qM`tA`!Q}3@P#C6IWbkoUJ&A+Ri?kyS(%2-ZOiJOs z35Qy~s{43}NV4y5bgg-z-!2kpFnI!x<&@RclX(er+o#d(IkNPUkcT!qyh1e{DFnPA z=%wNQAzt0ZD-Vv19~v+iGd_$6%*fJ3PJI zR$o^`!~qbXB6$w;F3!Hnb9)m?60 zUV6>n=(`=1g_$~MRf^N@l&V%LIwsBlB6#?42~msWgeDIrWt^bikc}NyQo7e@nZ21Z zJnYH(sq|BUyOv`-UH%_nNF){8n5R z(pqI%1<_U3W`g_q=U$LB8;4y{pAMdAV=-+870F8i8y!zch{|ce$0(O$2aQWaIvz9N zosttQFKddDeqZEorG?dmg$qv~K8HgM?zS^7cMlMfe zSO|RNK0$L_!u>H`yW{aBD9Vw%X`P^Ly%Z*nz1#MT6C5Ezpk1BwCPCb0)WlW;m1lqaP>x9p@piA4fUJ+E^ziU%Q|mg4 zz9>AtVi%T#M`whAdksTh(l8{%K7&#d<6ZVi+n5wW=2rk4$2*PBGJq-bb@B9dc&cwU z&tVcoQ#5#h;ix}FXR_3kseF)fi8il*`f4pwk335rvWR>fRnCS6wQ+lih_NJdkNGvK z7XSVr<9sAvXFGvkGCPpkOACoC8dypfDbvyfVR!=}ysHSxKEIWD7uS@y?6cWQE;y8# z@V$4cB%SH*`b@R6EAq~n$JgkKhYmYT=?mQ?L!Fz{|Jtj<=V;dPh1Oai*A!!U<@tSK ze%tJ2iV_jECzt;5f=QCpxkf5XnZuG;CsZTGIlcLt=YR)}cs7X+j%6jdVLLjE37GW2 zocv(xoIL;$-H+Kl4D#$kM;!bo@LEgH>x7+nPNuD+2m&(aU&ZJ%mzDuEZ{1*J36|mK z$z|B#RNC=TX$*MhCJhb$8`AK1uGq=c1$i7PTFhGwUMJw$y2BfFlfWc5uBlsJ-gxj} z9!i>3OJA&J?jWpQj2}Q#?PITDK*UDG1QvEEt6VKWoW~fg;?Ci0*m@u?O9zqryxb({ z_t#0UAZ6PX$@BD9hG$vLWw3&cF@Zx5k;_Asw%FqFsU>;6J)am@Pp0KwjDBj--fgFt zhy8quhLek9We*d%`NuVx=Y863cj{m-_+E$DSMGKGr zBXIYgbo=BF9%@iee23e11%M8#!9^%17eHexOwVTfCzI-#uzZI>QwelxnY+zymcv$o ztlD4cGg%70x2we)q&fi7rgBeX&6Jr$;!pld8lF1HV3fLxrWT(t)Y_J!UcbV6z*_Yk zI8Q(&6PVpLMRCmzjM%UEcx^e$;i{l=wkHrt8MX=-Sq(Px_=h~sdnst`ELu2U@58|> z{mbc?kvV=epspVINP8ahw3Ikb!#pq;TJOG-evjh|?@2^Ix>OeM+Fpz6usVZb-I~Ki_tg!LwMLQ@_)J_YEzjrR(LQ@F`RLHOY|4EZ}~Tv(zt!*@&~IRzqG@ z>XFX-x&IL}(xzB-rPhCv-HDE1$#R`*K8@bG-|T9rrFfXio5B|6x^hf= zazuQ>hK-lcc1@!tRq;>RjcveZ2Dp1S_Fb9@b5;8$d$?#;$N%&K(T2$^y3hjT%)8)k0TYb9{6(G|aK zMfsvteY^KC$5yv*fo-ar>Db>y%1mzS4G6iMTy)?4zA0**zPM2Dy5k0lYpms!>NXNa zFGn<{_F7%CmF#(a4|rZ{W^n%2N(u*u-aK$RsI=N2qf0RAt2vq*7RfIKTzIKr8S7*m z%YHp`3;1_ot3=2}UGqvJK}MEYPVuF^>_mg=FfhCNIH1Ka_5u}q!l~?^NgIUL8>l%` z`U=IJ^)2ZW9OlEk#t%mixWao{Si+-~Cu|B(p!&N=g~9&)ZuzzYM}SAU@gDHpDoeN( zFUppO{vYYhN>?>!H>>}3W83~GlFVu3i&PKYi6ZHZpZZ6FtD~XEI%s$StuW0t&8N4# zzSi8zm0Wgg`P=IQ+{p%GkdlC$GEdDQ}k``4^{th#hKd; zn&<&?UBLr{e#uhW7Z5yQb&ASS)h@^yz?@;Ni&+mu{OjGx_R+C-NAyItF>IS45hl

1k3b|nWy zf4t{r`~yWBQLitrx3l~nVokL}eKyz!L`zETY#Z>I5OfZrazb>+$t>0st2}m{Lfg;- zKJJQm$E#k6BqDNf0)bes<082(xI{o;bXFDcmo(qtpddZ82CL~T$BG$wYiOr4#nAe` z9y7oV;f{rBXf6qU9GqTZu|jHvYa zO{#J0&$C$VVDb*L?O^*Stta$^PxhC@+t5&Ml#B0?((4ZvGdEV>LODwCEr=$cajxG; z!64H!d*faCn{$^3p#K(l#YE^+3@PQE)}N(>7p4WKa2lpza% zfC_AM;f)YD610yk1&WBucqrHXIRPDGc=hsP=9K09)6Cy98A|Espme66Me@46t*lpIQzj?cu zZBcS-OGDqCU_S9NJ`prIWR8xr7n7#Ya4gt6l3DP4`S-o*Qs3{7w&VC^_8{}D!rtXZ zQ8fX0BwyqJ)R2|SMGqW7`&`=$ue>EUo&YKSCx|YZ)xG-ShlOyqO`de@qhGyUv{FoZ zxMOx4*j9eI?QwKp->I8a^x?6QTbNEj$LrTau$dTL5!QC&|F7Nve(Bx|A|=l~r&Bi1 ze+qe-6twRgzr5x)Sh6>&zHFDac31X6a^!z|D_R?Z?L+O9Xrcu~Hn!k`sm}+Mk@*kT z`&jJ8!SNnuWvaMI)K60kV7c86{l)ait{w|4GJfKbP|uX2!Eh;h5fL#!B4U7$Hg~Pz z?G(X4EOG$AGf$0(upQKPqst+3(8D*6FikXIyGXk34 z76{MaMTmd}lSU8SwyENb6k?wHQI)>W|=XJtM`@z)OP+Zvvs>THKt`mkfXdBKBAp8|gvphv> z{<9sSH{Gt#1^~4a1QKaIshuf`ME)Qg^g}J%xNI9bwvojjlrO~mk3=!D-FO^Os498^ zU>2&IegX9GRAS}4dPRnb8jC;wylx)c@E@K8w@p3}SVgMq-MTr@u85%Hpv@UN^i}b^ z_`q^bPF{JAdGp+P3ER`o06hZbS$^NH7-0O3`V(vA?w-RaIEdMoZ9Tm&qd7mhJZ9A{ zO~d`TwL~O$)TC?RsC=u|Nn16~BmRS;Epdc|7-UH!`Zu+oldv6$_byrVk&1N`?L-gN zth|dsE97Y>mJ{vh)n8$5$6g4I--MoBp8GZpwz({QB#wP+VvzthT_K!3U>&s$iR*RFi~o>eH+@I$@eZA zx8dUE698<5F{IGf7XyXjT)Gs^&d5D526Ke_7W~sui|1Yb^8wnE7V=-Ng~&DtR}zYb zu)j)ysq(E`*QC!~(e-~p@;UM(toC(Y8x^`-6lrS;N)t4{I4Smdh8}(D7%2O|k&V*~ zJ6Pn@KQL?%T;^gwwnuqj{02$Aqt;f^a_VdUs(B3AY%wsVG;LAnq4+eX!12O896NWdLv`Iubo$PJbFv^f;p(0Rk`rk+#Kg@d+jP zsTd_Z4*c+oZElMRd0Sdkx!^cp8}|NLQr($wAFw`?DEamLLwaF_hUz-E#I5^}1ddQe z*ZvBBwEetEwz8kU(xw?`;qr9fW)W>qQA3~Nk`gFo8iy%0D{M^1DM?atYDq^yvmfp5 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

S?-{VyX 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 literal 0 HcmV?d00001 diff --git a/docs/assets/javascripts/extra.js b/docs/assets/javascripts/extra.js new file mode 100644 index 00000000000..37f2d6a91cf --- /dev/null +++ b/docs/assets/javascripts/extra.js @@ -0,0 +1,32 @@ +/* Tiny JS helpers for the Miles docs site */ + +document.addEventListener("DOMContentLoaded", () => { + // Scroll-reveal: add .is-visible to landing cards / strips when they enter the viewport. + if ("IntersectionObserver" in window) { + const io = new IntersectionObserver( + (entries, obs) => { + for (const e of entries) { + if (e.isIntersecting) { + e.target.classList.add("is-visible"); + obs.unobserve(e.target); + } + } + }, + { rootMargin: "0px 0px -10% 0px", threshold: 0.05 } + ); + document + .querySelectorAll(".miles-card, .miles-update, .miles-quote, .miles-strip") + .forEach((el) => io.observe(el)); + } else { + // Fallback: just show everything. + document + .querySelectorAll(".miles-card, .miles-update, .miles-quote, .miles-strip") + .forEach((el) => el.classList.add("is-visible")); + } + + // Hover micro-interaction on update timeline rows. + document.querySelectorAll(".miles-update").forEach((el) => { + el.addEventListener("mouseenter", () => el.classList.add("is-hover")); + el.addEventListener("mouseleave", () => el.classList.remove("is-hover")); + }); +}); diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 00000000000..b522304ecc6 --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,1156 @@ +/* ========================================================================== + Miles — Frontier Lab + Restrained, confident, technical. Bricolage display, Geist body, Geist Mono. + Single accent gestures, generous whitespace, rigorous grid. + ========================================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&display=swap'); + +/* -------------------------------------------------------------------------- + Tokens — restrained palette, more neutral, single accent + -------------------------------------------------------------------------- */ +:root { + --miles-ink: #0a0e1f; + --miles-ink-soft: #11172e; + + --miles-accent: #1f7be0; /* refined blue, the single accent */ + --miles-accent-deep:#1565c0; + --miles-cyan: #21d4fd; /* used sparingly, only in dark gradients */ + --miles-violet: #8b5cf6; + --miles-green: #10b981; + --miles-amber: #f59e0b; + --miles-rose: #f43f5e; + + --miles-fg: #0f172a; + --miles-fg-soft: #334155; + --miles-fg-mute: #64748b; + --miles-fg-faint: #94a3b8; + + --miles-bg: #ffffff; + --miles-bg-soft: #f8fafc; + --miles-bg-mute: #f1f5f9; + --miles-line: rgba(15, 23, 42, 0.08); + --miles-line-soft: rgba(15, 23, 42, 0.04); + + /* Material overrides */ + --md-text-font: "Geist", system-ui, sans-serif; + --md-code-font: "Geist Mono", "JetBrains Mono", ui-monospace, monospace; + + --md-primary-fg-color: #0a0e1f; + --md-primary-fg-color--light: #11172e; + --md-primary-fg-color--dark: #04050f; + --md-accent-fg-color: #1f7be0; + --md-accent-fg-color--transparent: rgba(31, 123, 224, 0.08); + --md-typeset-a-color: #1f7be0; +} + +[data-md-color-scheme="slate"] { + --md-default-bg-color: #08090f; + --md-default-bg-color--light: #0d0f1a; + --md-default-bg-color--lighter: rgba(255, 255, 255, 0.04); + --md-default-bg-color--lightest: rgba(255, 255, 255, 0.06); + + --md-default-fg-color: #f1f5f9; + --md-default-fg-color--light: rgba(241, 245, 249, 0.78); + --md-default-fg-color--lighter: rgba(241, 245, 249, 0.50); + --md-default-fg-color--lightest: rgba(241, 245, 249, 0.14); + + --md-primary-fg-color: #08090f; + --md-primary-fg-color--light: #11172e; + --md-primary-fg-color--dark: #04050f; + --md-accent-fg-color: #5cb6ff; + --md-accent-fg-color--transparent: rgba(92, 182, 255, 0.10); + --md-typeset-a-color: #5cb6ff; + + --md-code-bg-color: #0d0f1a; + --md-code-fg-color: #e2e8f0; + --md-typeset-mark-color: rgba(245, 158, 11, 0.4); + + --miles-line: rgba(255, 255, 255, 0.07); + --miles-line-soft: rgba(255, 255, 255, 0.04); +} + +/* -------------------------------------------------------------------------- + Global typography + -------------------------------------------------------------------------- */ +body, .md-typeset { + font-family: "Geist", system-ui, sans-serif; + font-feature-settings: "ss01", "cv01", "cv02"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.md-typeset { + font-size: 0.84rem; + line-height: 1.7; + color: var(--md-default-fg-color--light); +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4, +.md-typeset h5 { + font-family: "Bricolage Grotesque", "Geist", sans-serif; + font-weight: 600; + letter-spacing: -0.02em; + color: var(--md-default-fg-color); +} + +.md-typeset h1 { + font-size: 2.2rem; + line-height: 1.1; + font-weight: 700; + font-variation-settings: "opsz" 96; + letter-spacing: -0.035em; + margin: 0.4rem 0 1.5rem; +} + +.md-typeset h2 { + font-size: 1.45rem; + line-height: 1.25; + font-weight: 600; + margin-top: 2.8rem; + margin-bottom: 0.9rem; + padding-bottom: 0.4rem; + border-bottom: 1px solid var(--miles-line); + font-variation-settings: "opsz" 32; +} + +.md-typeset h3 { + font-size: 1.1rem; + line-height: 1.35; + font-weight: 600; + margin-top: 2rem; + font-variation-settings: "opsz" 20; +} + +.md-typeset h4 { + font-size: 0.94rem; + font-weight: 600; + margin-top: 1.4rem; + letter-spacing: -0.01em; + color: var(--md-default-fg-color); +} + +.md-typeset h5, +.md-typeset h6 { + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--md-default-fg-color--lighter); + margin-top: 1.2rem; +} + +.md-typeset p, +.md-typeset li { + font-feature-settings: "ss01", "cv01", "cv02"; +} + +.md-typeset a { + text-decoration: none; + color: var(--md-accent-fg-color); + transition: opacity 0.15s ease; +} + +.md-typeset a:hover { + opacity: 0.7; +} + +.md-typeset h1 a, +.md-typeset h2 a, +.md-typeset h3 a, +.md-typeset h4 a { + color: inherit; +} + +/* Permalink anchors */ +.md-typeset .headerlink { + color: var(--md-default-fg-color--lightest); + margin-left: 0.4rem; + opacity: 0; + transition: opacity 0.15s ease; +} + +.md-typeset h1:hover .headerlink, +.md-typeset h2:hover .headerlink, +.md-typeset h3:hover .headerlink { + opacity: 1; +} + +/* -------------------------------------------------------------------------- + Header — blends with page, no border, logo lives in the sidebar + -------------------------------------------------------------------------- */ +.md-header { + background: var(--md-default-bg-color); + backdrop-filter: none; + border-bottom: none; + box-shadow: none; + color: var(--md-default-fg-color); + height: 3rem; + transition: background-color 0.15s ease; +} + +[data-md-color-scheme="default"] .md-header { + background: #ffffff; + color: var(--miles-fg); +} + +[data-md-color-scheme="slate"] .md-header { + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); +} + +.md-header * { color: inherit; } + +/* Hide the logo + site title from the header — they move to the sidebar */ +.md-header__button.md-logo, +.md-header__title { + display: none !important; +} + +/* Expose the hamburger only on narrow screens (Material already handles this) */ + +.md-search__form { + background: var(--miles-bg-mute); + border-radius: 8px; + border: 1px solid var(--miles-line); + transition: background 0.15s ease, border-color 0.15s ease; +} + +[data-md-color-scheme="slate"] .md-search__form { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.06); +} + +.md-search__form:hover { + border-color: var(--md-accent-fg-color); +} + +.md-search__input { + font-family: "Geist", sans-serif; + font-size: 0.82rem; + color: var(--md-default-fg-color); +} + +.md-search__input::placeholder { + color: var(--md-default-fg-color--lighter); +} + +/* Right-side header buttons (GitHub / theme toggle) stay subtle */ +.md-header__option .md-icon svg, +.md-header__source .md-source__icon svg { + fill: var(--md-default-fg-color--light); +} + +.md-header__source { + color: var(--md-default-fg-color--light); + font-family: "Geist Mono", monospace; + font-size: 0.78rem; +} + +.md-header__source:hover { + color: var(--md-accent-fg-color); +} + +/* -------------------------------------------------------------------------- + Brand in sidebar (logo replaces the header logo) + -------------------------------------------------------------------------- */ +.md-sidebar.md-sidebar--primary { + padding-top: 0; +} + +.md-sidebar--primary .md-sidebar__inner { + position: relative; +} + +.md-sidebar--primary .md-sidebar__inner::before { + content: "Miles"; + display: flex; + align-items: center; + padding: 0 1.1rem 0.55rem 2.55rem; + margin-bottom: 0.4rem; + height: 1.6rem; + + font-family: "Bricolage Grotesque", sans-serif; + font-weight: 700; + font-size: 1.2rem; + letter-spacing: -0.035em; + color: var(--md-default-fg-color); + + border-bottom: 1px solid var(--miles-line); + background-image: url("../images/miles_square.png"); + background-repeat: no-repeat; + background-position: 1.05rem top; + background-size: 22px 22px; +} + +[data-md-color-scheme="slate"] .md-sidebar--primary .md-sidebar__inner::before { + filter: none; +} + +/* -------------------------------------------------------------------------- + Announcement bar — quiet + -------------------------------------------------------------------------- */ +.md-banner { + background: #04050f; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + color: #cbd5e1; + font-size: 0.76rem; +} + +.md-banner a { + color: #5cb6ff; + font-weight: 500; + text-decoration: none; + border-bottom: 1px solid rgba(92, 182, 255, 0.3); +} + +.md-banner a:hover { + border-bottom-color: currentColor; +} + +/* -------------------------------------------------------------------------- + Vertical sidebar (SGLang/verl style) + -------------------------------------------------------------------------- */ +.md-sidebar { + padding-top: 1rem; +} + +.md-sidebar--primary { + background: var(--md-default-bg-color); +} + +[data-md-color-scheme="default"] .md-sidebar--primary { + background: var(--md-default-bg-color); + border-right: 1px solid var(--miles-line); +} + +[data-md-color-scheme="slate"] .md-sidebar--primary { + background: var(--md-default-bg-color); + border-right: 1px solid rgba(255, 255, 255, 0.05); +} + +.md-sidebar__scrollwrap { + scrollbar-width: thin; +} + +.md-nav { font-size: 0.78rem; } + +.md-nav__title { + font-family: "Bricolage Grotesque", sans-serif; + font-weight: 600; + font-size: 0.66rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--md-default-fg-color--lighter); + padding: 0.5rem 0.8rem 0.4rem; +} + +/* The brand sits in the header; suppress the duplicate logo + "Miles" + that mkdocs-material renders inside the primary nav title. */ +.md-nav--primary > .md-nav__title { + display: none; +} + +.md-nav__item { padding: 0; } + +.md-nav__link { + padding: 0.32rem 0.7rem; + border-radius: 6px; + margin: 1px 0.4rem; + position: relative; + color: var(--md-default-fg-color--light); + transition: background-color 0.12s ease, color 0.12s ease; +} + +.md-nav__link:hover { + background: var(--md-accent-fg-color--transparent); + color: var(--md-default-fg-color); +} + +.md-nav__link--active { + color: var(--md-accent-fg-color); + background: var(--md-accent-fg-color--transparent); + font-weight: 600; +} + +.md-nav__link--active::before { + content: ""; + position: absolute; + left: -4px; + top: 50%; + transform: translateY(-50%); + width: 2px; + height: 60%; + border-radius: 2px; + background: var(--md-accent-fg-color); +} + +/* Section labels in sidebar */ +.md-nav__item--section > .md-nav__link { + font-family: "Bricolage Grotesque", sans-serif; + font-weight: 600; + font-size: 0.7rem; + letter-spacing: 0.10em; + text-transform: uppercase; + color: var(--md-default-fg-color); + padding: 1rem 0.7rem 0.4rem; + margin: 0.5rem 0 0; + border-top: 1px solid var(--miles-line); + border-radius: 0; + background: transparent !important; +} + +.md-nav__item--section:first-child > .md-nav__link { + border-top: none; + margin-top: 0; +} + +.md-nav__item--section > .md-nav__link:hover { + color: var(--md-default-fg-color); +} + +.md-nav__item--section > .md-nav__link::before { + display: none; +} + +/* Right rail (TOC) — quieter */ +.md-sidebar--secondary .md-nav__title { + font-family: "Bricolage Grotesque", sans-serif; + letter-spacing: 0.14em; + font-weight: 600; + font-size: 0.64rem; + text-transform: uppercase; + color: var(--md-default-fg-color--lighter); +} + +.md-sidebar--secondary .md-nav__link { + font-size: 0.74rem; + color: var(--md-default-fg-color--light); + padding: 0.22rem 0.7rem; +} + +/* -------------------------------------------------------------------------- + Code — frontier-lab minimal (OpenAI / Anthropic / Thinking Machines) + Light mode: near-white off-surface on white page. + Dark mode: one shade lighter than page on dark body. + No gradients, no borders screaming for attention. + -------------------------------------------------------------------------- */ + +/* Inline code */ +.md-typeset code { + font-family: var(--md-code-font); + font-size: 0.84em; + font-weight: 500; + font-feature-settings: "liga" 0, "calt" 0; + border-radius: 4px; + padding: 0.12em 0.38em; + line-height: 1.5; + letter-spacing: -0.005em; +} + +[data-md-color-scheme="default"] .md-typeset code { + background: #f5f6f8; + color: #24292f; + border: 1px solid rgba(0, 0, 0, 0.04); +} + +[data-md-color-scheme="slate"] .md-typeset code { + background: rgba(255, 255, 255, 0.05); + color: #e6eaf3; + border: 1px solid rgba(255, 255, 255, 0.04); +} + +/* Code inside headings: no background, just mono */ +.md-typeset h1 code, +.md-typeset h2 code, +.md-typeset h3 code, +.md-typeset h4 code, +.md-typeset h5 code { + background: transparent; + border: none; + padding: 0; + font-size: 0.92em; + color: inherit; +} + +/* Code inside links: inherit link color, no border */ +.md-typeset a > code { + color: inherit; + border: none; +} + +/* Code block wrapper */ +.md-typeset .highlight, +.md-typeset .highlighttable { + margin: 1.4rem 0; + font-feature-settings: "liga" 0, "calt" 0; +} + +/* The

 */
+.md-typeset .highlight > pre,
+.md-typeset .highlighttable pre {
+  border-radius: 8px;
+  margin: 0;
+  padding: 1rem 1.1rem;
+  font-size: 0.78em;
+  line-height: 1.62;
+  overflow-x: auto;
+  box-shadow: none;
+}
+
+.md-typeset .highlight > pre > code,
+.md-typeset .highlighttable pre > code {
+  background: transparent !important;
+  border: none !important;
+  padding: 0 !important;
+  font-size: inherit !important;
+  color: inherit !important;
+}
+
+/* Light-mode code surface: soft off-white, barely-there border */
+[data-md-color-scheme="default"] .md-typeset .highlight > pre,
+[data-md-color-scheme="default"] .md-typeset .highlighttable pre {
+  background: #f6f8fa;
+  color: #1f2328;
+  border: 1px solid rgba(15, 23, 42, 0.07);
+}
+
+/* Dark-mode: one shade above page bg */
+[data-md-color-scheme="slate"] .md-typeset .highlight > pre,
+[data-md-color-scheme="slate"] .md-typeset .highlighttable pre {
+  background: #0e1020;
+  color: #e6eaf3;
+  border: 1px solid rgba(255, 255, 255, 0.055);
+}
+
+/* Filename chip — restrained, same family as the pre, not a huge stripe */
+.md-typeset .highlight .filename,
+.md-typeset .highlighttable .filename {
+  font-family: var(--md-code-font);
+  font-weight: 500;
+  font-size: 0.72rem;
+  letter-spacing: -0.005em;
+  padding: 0.5rem 1rem 0.4rem;
+  border-radius: 8px 8px 0 0;
+  border: 1px solid;
+  border-bottom: none;
+  margin-bottom: 0;
+}
+
+[data-md-color-scheme="default"] .md-typeset .highlight .filename {
+  background: #f1f3f5;
+  color: #57606a;
+  border-color: rgba(15, 23, 42, 0.07);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .highlight .filename {
+  background: #0a0c18;
+  color: #9ca3af;
+  border-color: rgba(255, 255, 255, 0.05);
+}
+
+.md-typeset .highlight .filename + pre {
+  border-radius: 0 0 8px 8px !important;
+}
+
+/* Hide the "Code" default filename caption that Material sometimes renders */
+.md-typeset .highlight .filename[data-md-default] {
+  display: none;
+}
+
+/* Line numbers — quieter */
+.md-typeset .highlighttable .linenos {
+  background: transparent;
+  border-right: 1px solid;
+  padding-right: 0.55rem;
+  user-select: none;
+  color: var(--md-default-fg-color--lightest);
+}
+
+[data-md-color-scheme="default"] .md-typeset .highlighttable .linenos {
+  border-right-color: rgba(15, 23, 42, 0.06);
+  color: rgba(15, 23, 42, 0.32);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .highlighttable .linenos {
+  border-right-color: rgba(255, 255, 255, 0.05);
+  color: rgba(255, 255, 255, 0.30);
+}
+
+.md-typeset .highlighttable {
+  border-radius: 8px;
+  overflow: hidden;
+  background: transparent;
+  border: 1px solid;
+}
+
+[data-md-color-scheme="default"] .md-typeset .highlighttable {
+  border-color: rgba(15, 23, 42, 0.07);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .highlighttable {
+  border-color: rgba(255, 255, 255, 0.055);
+}
+
+.md-typeset .highlighttable pre {
+  border: none !important;
+  border-radius: 0 !important;
+  background: transparent !important;
+}
+
+/* Copy button — only visible on hover, then subtle */
+.md-clipboard {
+  color: transparent;
+  transition: color 0.15s ease, background-color 0.15s ease;
+  border-radius: 4px;
+  top: 0.5rem;
+  right: 0.5rem;
+}
+
+.md-typeset .highlight:hover .md-clipboard,
+.md-typeset .highlighttable:hover .md-clipboard {
+  color: var(--md-default-fg-color--lighter);
+}
+
+.md-clipboard:hover {
+  color: var(--md-accent-fg-color) !important;
+  background: transparent;
+}
+
+/* Pygments token tuning — light mode (GitHub-ish, but quieter) */
+[data-md-color-scheme="default"] .md-typeset .highlight .k,
+[data-md-color-scheme="default"] .md-typeset .highlight .kd,
+[data-md-color-scheme="default"] .md-typeset .highlight .kn,
+[data-md-color-scheme="default"] .md-typeset .highlight .kr { color: #cf222e; font-weight: 500; }
+[data-md-color-scheme="default"] .md-typeset .highlight .s,
+[data-md-color-scheme="default"] .md-typeset .highlight .sb,
+[data-md-color-scheme="default"] .md-typeset .highlight .sd,
+[data-md-color-scheme="default"] .md-typeset .highlight .s1,
+[data-md-color-scheme="default"] .md-typeset .highlight .s2 { color: #0a3069; }
+[data-md-color-scheme="default"] .md-typeset .highlight .c,
+[data-md-color-scheme="default"] .md-typeset .highlight .c1,
+[data-md-color-scheme="default"] .md-typeset .highlight .cm { color: #6e7781; font-style: italic; }
+[data-md-color-scheme="default"] .md-typeset .highlight .n,
+[data-md-color-scheme="default"] .md-typeset .highlight .nx { color: #1f2328; }
+[data-md-color-scheme="default"] .md-typeset .highlight .nf,
+[data-md-color-scheme="default"] .md-typeset .highlight .nc { color: #8250df; }
+[data-md-color-scheme="default"] .md-typeset .highlight .nb { color: #116329; }
+[data-md-color-scheme="default"] .md-typeset .highlight .mi,
+[data-md-color-scheme="default"] .md-typeset .highlight .mf { color: #0550ae; }
+[data-md-color-scheme="default"] .md-typeset .highlight .o { color: #cf222e; }
+[data-md-color-scheme="default"] .md-typeset .highlight .p { color: #1f2328; }
+
+/* Pygments token tuning — dark mode (Dracula-ish, but desaturated) */
+[data-md-color-scheme="slate"] .md-typeset .highlight .k,
+[data-md-color-scheme="slate"] .md-typeset .highlight .kd,
+[data-md-color-scheme="slate"] .md-typeset .highlight .kn,
+[data-md-color-scheme="slate"] .md-typeset .highlight .kr { color: #ff7b72; font-weight: 500; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .s,
+[data-md-color-scheme="slate"] .md-typeset .highlight .sb,
+[data-md-color-scheme="slate"] .md-typeset .highlight .sd,
+[data-md-color-scheme="slate"] .md-typeset .highlight .s1,
+[data-md-color-scheme="slate"] .md-typeset .highlight .s2 { color: #a5d6ff; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .c,
+[data-md-color-scheme="slate"] .md-typeset .highlight .c1,
+[data-md-color-scheme="slate"] .md-typeset .highlight .cm { color: #8b949e; font-style: italic; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .n,
+[data-md-color-scheme="slate"] .md-typeset .highlight .nx { color: #e6eaf3; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .nf,
+[data-md-color-scheme="slate"] .md-typeset .highlight .nc { color: #d2a8ff; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .nb { color: #7ee787; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .mi,
+[data-md-color-scheme="slate"] .md-typeset .highlight .mf { color: #79c0ff; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .o { color: #ff7b72; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .p { color: #c9d1d9; }
+
+/* Diff highlights */
+[data-md-color-scheme="default"] .md-typeset .highlight .gd { background: #ffebe9; color: #82071e; }
+[data-md-color-scheme="default"] .md-typeset .highlight .gi { background: #dafbe1; color: #116329; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .gd  { background: rgba(248, 81, 73, 0.15); color: #ffa198; }
+[data-md-color-scheme="slate"] .md-typeset .highlight .gi  { background: rgba(46, 160, 67, 0.15); color: #7ee787; }
+
+/* --------------------------------------------------------------------------
+   Admonitions — quiet, single colored stripe
+   -------------------------------------------------------------------------- */
+.md-typeset .admonition,
+.md-typeset details {
+  border: 1px solid var(--miles-line);
+  border-left-width: 3px;
+  border-radius: 8px;
+  font-size: 0.82rem;
+  background: var(--md-default-bg-color);
+  box-shadow: none;
+}
+
+[data-md-color-scheme="default"] .md-typeset .admonition,
+[data-md-color-scheme="default"] .md-typeset details {
+  background: var(--miles-bg-soft);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .admonition,
+[data-md-color-scheme="slate"] .md-typeset details {
+  background: rgba(255, 255, 255, 0.018);
+}
+
+.md-typeset .admonition-title,
+.md-typeset summary {
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 600;
+  font-size: 0.82rem;
+  letter-spacing: -0.005em;
+  padding-top: 0.6rem;
+  padding-bottom: 0.5rem;
+  background: transparent !important;
+}
+
+.md-typeset .admonition.tip,
+.md-typeset details.tip { border-left-color: var(--miles-green); }
+.md-typeset .admonition.tip > .admonition-title::before,
+.md-typeset details.tip > summary::before { background-color: var(--miles-green); }
+
+.md-typeset .admonition.note,
+.md-typeset details.note { border-left-color: var(--miles-accent); }
+.md-typeset .admonition.note > .admonition-title::before,
+.md-typeset details.note > summary::before { background-color: var(--miles-accent); }
+
+.md-typeset .admonition.warning,
+.md-typeset details.warning { border-left-color: var(--miles-amber); }
+.md-typeset .admonition.warning > .admonition-title::before,
+.md-typeset details.warning > summary::before { background-color: var(--miles-amber); }
+
+.md-typeset .admonition.danger,
+.md-typeset details.danger { border-left-color: var(--miles-rose); }
+
+.md-typeset .admonition.question,
+.md-typeset details.question { border-left-color: var(--miles-violet); }
+.md-typeset .admonition.question > .admonition-title::before,
+.md-typeset details.question > summary::before { background-color: var(--miles-violet); }
+
+/* --------------------------------------------------------------------------
+   Tables — quiet, no gradient
+   -------------------------------------------------------------------------- */
+.md-typeset table:not([class]) {
+  font-size: 0.78rem;
+  border: 1px solid var(--miles-line);
+  border-radius: 8px;
+  overflow: hidden;
+  border-collapse: separate;
+  border-spacing: 0;
+  box-shadow: none;
+  margin: 1.2rem 0;
+}
+
+.md-typeset table:not([class]) th {
+  background: var(--miles-bg-mute);
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 600;
+  font-size: 0.7rem;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  color: var(--md-default-fg-color);
+  padding: 0.55rem 0.8rem;
+  border-bottom: 1px solid var(--miles-line);
+}
+
+[data-md-color-scheme="slate"] .md-typeset table:not([class]) th {
+  background: rgba(255, 255, 255, 0.04);
+  color: var(--md-default-fg-color);
+}
+
+.md-typeset table:not([class]) td {
+  padding: 0.55rem 0.8rem;
+  border-bottom: 1px solid var(--miles-line-soft);
+  vertical-align: top;
+}
+
+.md-typeset table:not([class]) tr:last-child td { border-bottom: none; }
+.md-typeset table:not([class]) tr:hover td {
+  background: var(--md-accent-fg-color--transparent);
+}
+
+/* --------------------------------------------------------------------------
+   Buttons
+   -------------------------------------------------------------------------- */
+.md-typeset .md-button {
+  font-family: "Bricolage Grotesque", sans-serif;
+  border-radius: 8px;
+  font-weight: 500;
+  font-size: 0.84rem;
+  letter-spacing: -0.005em;
+  padding: 0.55em 1.25em;
+  border: 1px solid var(--miles-line);
+  color: var(--md-default-fg-color);
+  background: transparent;
+  transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.md-typeset .md-button:hover {
+  border-color: var(--md-accent-fg-color);
+  background: var(--md-accent-fg-color--transparent);
+}
+
+.md-typeset .md-button--primary {
+  background: var(--miles-fg);
+  color: #ffffff !important;
+  border: 1px solid var(--miles-fg);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .md-button--primary {
+  background: #ffffff;
+  color: var(--miles-ink) !important;
+  border-color: #ffffff;
+}
+
+.md-typeset .md-button--primary:hover {
+  background: var(--miles-accent);
+  border-color: var(--miles-accent);
+  color: #ffffff !important;
+}
+
+/* --------------------------------------------------------------------------
+   Material grid cards
+   -------------------------------------------------------------------------- */
+.md-typeset .grid {
+  margin: 1.4rem 0;
+}
+
+.md-typeset .grid.cards {
+  grid-gap: 0.9rem;
+}
+
+.md-typeset .grid.cards > ol > li,
+.md-typeset .grid.cards > ul > li,
+.md-typeset .grid > .card {
+  border: 1px solid var(--miles-line);
+  border-radius: 12px;
+  padding: 1.2rem 1.3rem 1.3rem;
+  background: var(--md-default-bg-color);
+  transition: border-color 0.15s ease, transform 0.18s ease;
+  position: relative;
+  overflow: hidden;
+}
+
+[data-md-color-scheme="default"] .md-typeset .grid.cards > ol > li,
+[data-md-color-scheme="default"] .md-typeset .grid.cards > ul > li {
+  background: #ffffff;
+}
+
+[data-md-color-scheme="slate"] .md-typeset .grid.cards > ol > li,
+[data-md-color-scheme="slate"] .md-typeset .grid.cards > ul > li {
+  background: rgba(255, 255, 255, 0.02);
+}
+
+.md-typeset .grid.cards > ol > li:hover,
+.md-typeset .grid.cards > ul > li:hover {
+  border-color: rgba(31, 123, 224, 0.4);
+  transform: translateY(-1px);
+}
+
+.md-typeset .grid.cards li > p:first-child {
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 600;
+  font-size: 0.95rem;
+  margin-top: 0;
+  margin-bottom: 0.4rem;
+  color: var(--md-default-fg-color);
+  letter-spacing: -0.01em;
+}
+
+.md-typeset .grid.cards li > p:last-child {
+  font-size: 0.8rem;
+  color: var(--md-default-fg-color--light);
+  margin-bottom: 0;
+}
+
+.md-typeset .grid.cards .twemoji,
+.md-typeset .grid.cards svg {
+  color: var(--md-accent-fg-color);
+  fill: currentColor;
+  height: 1.2rem;
+}
+
+.md-typeset .grid.cards li > hr {
+  margin: 0.4rem 0 0.6rem;
+  border: 0;
+  border-top: 1px solid var(--miles-line);
+}
+
+.md-typeset .grid.cards a {
+  border: none;
+  color: inherit;
+}
+
+/* --------------------------------------------------------------------------
+   Tabs
+   -------------------------------------------------------------------------- */
+.md-typeset .tabbed-set {
+  border: 1px solid var(--miles-line);
+  border-radius: 10px;
+  overflow: hidden;
+  margin: 1.2rem 0;
+}
+
+.md-typeset .tabbed-labels {
+  background: var(--miles-bg-soft);
+  border-bottom: 1px solid var(--miles-line);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .tabbed-labels {
+  background: rgba(255, 255, 255, 0.025);
+}
+
+.md-typeset .tabbed-labels > label {
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 500;
+  font-size: 0.78rem;
+  padding: 0.7rem 1.05rem;
+  border-bottom: 2px solid transparent;
+  color: var(--md-default-fg-color--light);
+  transition: color 0.12s ease, border-bottom-color 0.12s ease;
+}
+
+.md-typeset .tabbed-labels > label:hover {
+  color: var(--md-default-fg-color);
+}
+
+.md-typeset .tabbed-set > input:checked + label {
+  color: var(--md-accent-fg-color);
+  border-bottom-color: var(--md-accent-fg-color);
+}
+
+.md-typeset .tabbed-content {
+  padding: 1.1rem 1.2rem;
+}
+
+/* --------------------------------------------------------------------------
+   Quote / blockquote
+   -------------------------------------------------------------------------- */
+.md-typeset blockquote {
+  border-left: 3px solid var(--md-accent-fg-color);
+  background: transparent;
+  padding: 0 0 0 1rem;
+  border-radius: 0;
+  font-style: normal;
+  color: var(--md-default-fg-color--light);
+  margin-left: 0;
+}
+
+/* --------------------------------------------------------------------------
+   Inline tags
+   -------------------------------------------------------------------------- */
+.md-tag {
+  border-radius: 4px;
+  background: var(--md-accent-fg-color--transparent);
+  color: var(--md-accent-fg-color);
+  font-family: var(--md-code-font);
+  font-weight: 500;
+  font-size: 0.7rem;
+  padding: 0.16em 0.5em;
+  letter-spacing: 0.02em;
+}
+
+/* --------------------------------------------------------------------------
+   Footer — blends with page
+   -------------------------------------------------------------------------- */
+.md-footer {
+  background: var(--md-default-bg-color);
+  border-top: 1px solid var(--miles-line);
+  color: var(--md-default-fg-color--light);
+}
+
+[data-md-color-scheme="default"] .md-footer {
+  background: #ffffff;
+  color: var(--md-default-fg-color--light);
+}
+
+.md-footer a,
+.md-footer__link,
+.md-footer__title {
+  color: var(--md-default-fg-color);
+}
+
+.md-footer__direction {
+  color: var(--md-default-fg-color--lighter);
+  font-family: "Geist Mono", monospace;
+  font-size: 0.7rem;
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+}
+
+.md-footer__title {
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 500;
+  font-size: 0.86rem;
+  color: var(--md-default-fg-color);
+}
+
+.md-footer-meta {
+  background: var(--md-default-bg-color);
+  border-top: 1px solid var(--miles-line);
+  font-size: 0.74rem;
+  color: var(--md-default-fg-color--lighter);
+}
+
+[data-md-color-scheme="default"] .md-footer-meta {
+  background: #ffffff;
+}
+
+.md-footer-meta .md-footer-copyright,
+.md-footer-meta .md-footer-copyright a {
+  color: var(--md-default-fg-color--lighter);
+}
+
+.md-footer-meta .md-footer-copyright a:hover {
+  color: var(--md-accent-fg-color);
+}
+
+.md-footer-meta .md-social__link svg {
+  fill: var(--md-default-fg-color--light);
+  transition: fill 0.15s ease;
+}
+
+.md-footer-meta .md-social__link:hover svg {
+  fill: var(--md-accent-fg-color);
+}
+
+.md-footer__link {
+  border-radius: 8px;
+  transition: background-color 0.15s ease;
+}
+
+.md-footer__link:hover {
+  background: var(--md-accent-fg-color--transparent);
+}
+
+/* --------------------------------------------------------------------------
+   Selection / scrollbars / mermaid
+   -------------------------------------------------------------------------- */
+::selection {
+  background: rgba(31, 123, 224, 0.25);
+  color: inherit;
+}
+
+::-webkit-scrollbar { width: 10px; height: 10px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb {
+  background: rgba(100, 116, 139, 0.3);
+  border-radius: 10px;
+}
+::-webkit-scrollbar-thumb:hover {
+  background: rgba(100, 116, 139, 0.5);
+}
+
+.md-typeset .mermaid {
+  text-align: center;
+  margin: 1.5rem 0;
+}
+
+/* --------------------------------------------------------------------------
+   Edit / view buttons
+   -------------------------------------------------------------------------- */
+.md-content__button {
+  color: var(--md-default-fg-color--lighter);
+  transition: color 0.15s ease;
+}
+.md-content__button:hover {
+  color: var(--md-accent-fg-color);
+}
+
+/* Search results */
+.md-search-result__meta {
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 500;
+  font-size: 0.7rem;
+  letter-spacing: 0.06em;
+}
+
+/* Source / last updated */
+.md-source-file__fact {
+  font-family: var(--md-code-font);
+  font-size: 0.7rem;
+  color: var(--md-default-fg-color--lighter);
+}
+
+/* --------------------------------------------------------------------------
+   Content sizing — narrower column for readability
+   -------------------------------------------------------------------------- */
+.md-grid {
+  max-width: 80rem;
+}
+
+.md-content {
+  max-width: 56rem;
+}
+
+.md-content__inner {
+  margin: 0 1rem 1rem;
+  padding-top: 1.2rem;
+}
+
+/* --------------------------------------------------------------------------
+   Homepage model matrix
+   -------------------------------------------------------------------------- */
+.md-typeset .miles-model-matrix {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 0.75rem;
+  margin: 1.1rem 0 0.7rem;
+}
+
+.md-typeset .miles-model-panel {
+  border: 1px solid var(--miles-line);
+  border-radius: 8px;
+  padding: 0.85rem 0.95rem 0.9rem;
+  background:
+    linear-gradient(180deg, rgba(31, 123, 224, 0.055), transparent 46%),
+    var(--md-default-bg-color);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .miles-model-panel {
+  background:
+    linear-gradient(180deg, rgba(92, 182, 255, 0.07), transparent 48%),
+    rgba(255, 255, 255, 0.018);
+}
+
+.md-typeset .miles-model-panel--moe {
+  background:
+    linear-gradient(180deg, rgba(16, 185, 129, 0.06), transparent 46%),
+    var(--md-default-bg-color);
+}
+
+[data-md-color-scheme="slate"] .md-typeset .miles-model-panel--moe {
+  background:
+    linear-gradient(180deg, rgba(16, 185, 129, 0.075), transparent 48%),
+    rgba(255, 255, 255, 0.018);
+}
+
+.md-typeset .miles-model-kicker {
+  display: block;
+  margin-bottom: 0.45rem;
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-size: 0.68rem;
+  font-weight: 700;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: var(--md-default-fg-color);
+}
+
+.md-typeset .miles-model-panel p {
+  margin: 0;
+  color: var(--md-default-fg-color--light);
+}
+
+.md-typeset .miles-model-panel a {
+  font-weight: 500;
+}
+
+@media screen and (max-width: 44rem) {
+  .md-typeset .miles-model-matrix {
+    grid-template-columns: 1fr;
+  }
+}
diff --git a/docs/assets/stylesheets/landing.css b/docs/assets/stylesheets/landing.css
new file mode 100644
index 00000000000..ac09795e787
--- /dev/null
+++ b/docs/assets/stylesheets/landing.css
@@ -0,0 +1,486 @@
+/* ==========================================================================
+   Landing — restrained, frontier-lab confidence
+   One hero gesture, generous space, no candy gradients elsewhere.
+   ========================================================================== */
+
+/* --------------------------------------------------------------------------
+   Hero
+   -------------------------------------------------------------------------- */
+.miles-hero {
+  position: relative;
+  margin: -1.6rem -1.2rem 3rem;
+  padding: 6rem 1.6rem 5rem;
+  text-align: center;
+  overflow: hidden;
+  border-radius: 0;
+  color: #f1f5f9;
+  background: #050610;
+}
+
+/* subtle radial glow */
+.miles-hero::before {
+  content: "";
+  position: absolute;
+  inset: 0;
+  background:
+    radial-gradient(ellipse 80% 60% at 50% 0%, rgba(31, 123, 224, 0.20), transparent 60%),
+    radial-gradient(ellipse 60% 40% at 50% 100%, rgba(139, 92, 246, 0.15), transparent 60%);
+  pointer-events: none;
+}
+
+/* fine grid lines */
+.miles-hero::after {
+  content: "";
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
+  background-size: 60px 60px;
+  mask-image: radial-gradient(ellipse 70% 60% at center, black 30%, transparent 80%);
+  -webkit-mask-image: radial-gradient(ellipse 70% 60% at center, black 30%, transparent 80%);
+  pointer-events: none;
+}
+
+.miles-hero__inner {
+  position: relative;
+  z-index: 1;
+  max-width: 880px;
+  margin: 0 auto;
+}
+
+.miles-hero__eyebrow {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.5rem;
+  padding: 0.32rem 0.85rem;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.06);
+  border: 1px solid rgba(255, 255, 255, 0.10);
+  color: #cbd5e1;
+  font-family: "Geist Mono", monospace;
+  font-size: 0.66rem;
+  font-weight: 500;
+  letter-spacing: 0.18em;
+  text-transform: uppercase;
+}
+
+.miles-hero__eyebrow .dot {
+  width: 6px; height: 6px;
+  border-radius: 50%;
+  background: #10b981;
+  box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
+}
+
+.miles-hero__title {
+  font-family: "Bricolage Grotesque", "Geist", sans-serif !important;
+  font-variation-settings: "opsz" 96, "wght" 700;
+  font-size: clamp(2.6rem, 6.5vw, 4.6rem);
+  font-weight: 700;
+  letter-spacing: -0.045em;
+  line-height: 1.0;
+  margin: 1.6rem auto 1.2rem;
+  color: #f1f5f9;
+}
+
+.miles-hero__title em {
+  font-style: normal;
+  background: linear-gradient(120deg, #5cb6ff 20%, #b78cff 80%);
+  -webkit-background-clip: text;
+  background-clip: text;
+  color: transparent;
+  -webkit-text-fill-color: transparent;
+}
+
+.miles-hero__subtitle {
+  margin: 0 auto 2.2rem;
+  max-width: 640px;
+  font-size: 1.04rem;
+  line-height: 1.55;
+  color: rgba(241, 245, 249, 0.72);
+  font-family: "Geist", sans-serif;
+}
+
+.miles-hero__cta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.7rem;
+  justify-content: center;
+  margin-bottom: 2.6rem;
+}
+
+.miles-hero__cta a {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.45rem;
+  padding: 0.65rem 1.25rem;
+  border-radius: 8px;
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 500;
+  font-size: 0.86rem;
+  letter-spacing: -0.005em;
+  text-decoration: none !important;
+  border: 1px solid transparent;
+  transition: background 0.15s ease, border-color 0.15s ease, transform 0.18s ease;
+}
+
+.miles-hero__cta .primary {
+  background: #ffffff;
+  color: #06070d !important;
+  border-color: #ffffff;
+}
+
+.miles-hero__cta .primary:hover {
+  background: #5cb6ff;
+  border-color: #5cb6ff;
+  transform: translateY(-1px);
+}
+
+.miles-hero__cta .ghost {
+  background: transparent;
+  color: #f1f5f9 !important;
+  border-color: rgba(255, 255, 255, 0.18);
+}
+
+.miles-hero__cta .ghost:hover {
+  background: rgba(255, 255, 255, 0.06);
+  border-color: rgba(255, 255, 255, 0.32);
+  transform: translateY(-1px);
+}
+
+/* Stat strip ------------------------------------------------------------- */
+.miles-hero__stats {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+  gap: 1.4rem;
+  max-width: 760px;
+  margin: 0 auto;
+  padding-top: 2.2rem;
+  border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.miles-hero__stat { text-align: center; }
+
+.miles-hero__stat .num {
+  display: block;
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-size: 1.7rem;
+  font-weight: 600;
+  letter-spacing: -0.03em;
+  color: #f1f5f9;
+}
+
+.miles-hero__stat .lbl {
+  display: block;
+  margin-top: 0.35rem;
+  font-family: "Geist Mono", monospace;
+  font-size: 0.66rem;
+  color: rgba(241, 245, 249, 0.5);
+  letter-spacing: 0.16em;
+  text-transform: uppercase;
+}
+
+/* --------------------------------------------------------------------------
+   Sections — restrained, no underline accents, just space
+   -------------------------------------------------------------------------- */
+.miles-section {
+  margin: 4rem auto;
+  max-width: 1080px;
+}
+
+.miles-section__head {
+  text-align: center;
+  margin-bottom: 2.4rem;
+}
+
+.miles-section__eyebrow {
+  display: inline-block;
+  font-family: "Geist Mono", monospace;
+  font-size: 0.7rem;
+  letter-spacing: 0.2em;
+  text-transform: uppercase;
+  color: var(--md-accent-fg-color);
+  font-weight: 500;
+  margin-bottom: 0.6rem;
+}
+
+.miles-section__title {
+  font-family: "Bricolage Grotesque", sans-serif !important;
+  font-variation-settings: "opsz" 48, "wght" 600;
+  margin: 0 0 0.8rem !important;
+  font-size: clamp(1.7rem, 3vw, 2.2rem) !important;
+  font-weight: 600 !important;
+  letter-spacing: -0.03em !important;
+  color: var(--md-default-fg-color) !important;
+  border: none !important;
+  padding-bottom: 0 !important;
+  background: none !important;
+  -webkit-text-fill-color: var(--md-default-fg-color) !important;
+}
+
+.miles-section__lede {
+  margin: 0 auto;
+  max-width: 660px;
+  color: var(--md-default-fg-color--light);
+  font-size: 0.96rem;
+  line-height: 1.6;
+}
+
+/* --------------------------------------------------------------------------
+   Feature cards
+   -------------------------------------------------------------------------- */
+.miles-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+  gap: 1rem;
+}
+
+.miles-card {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  padding: 1.4rem 1.4rem 1.5rem;
+  border-radius: 12px;
+  border: 1px solid var(--miles-line);
+  background: var(--md-default-bg-color);
+  text-decoration: none !important;
+  color: inherit;
+  transition: border-color 0.15s ease, transform 0.18s ease;
+  overflow: hidden;
+}
+
+[data-md-color-scheme="default"] .miles-card {
+  background: #ffffff;
+}
+
+[data-md-color-scheme="slate"] .miles-card {
+  background: rgba(255, 255, 255, 0.018);
+}
+
+.miles-card:hover {
+  transform: translateY(-1px);
+  border-color: var(--md-accent-fg-color);
+}
+
+.miles-card__icon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 8px;
+  background: var(--md-accent-fg-color--transparent);
+  color: var(--md-accent-fg-color);
+  font-size: 1.05rem;
+  margin-bottom: 0.95rem;
+}
+
+.miles-card__title {
+  font-family: "Bricolage Grotesque", sans-serif;
+  margin: 0 0 0.4rem;
+  font-size: 1.0rem;
+  font-weight: 600;
+  letter-spacing: -0.015em;
+  color: var(--md-default-fg-color);
+}
+
+.miles-card__desc {
+  margin: 0 0 0.95rem;
+  color: var(--md-default-fg-color--light);
+  font-size: 0.84rem;
+  line-height: 1.55;
+  flex-grow: 1;
+}
+
+.miles-card__chip {
+  display: inline-block;
+  padding: 0.16em 0.55em;
+  border-radius: 4px;
+  background: var(--miles-bg-mute);
+  color: var(--md-default-fg-color--lighter);
+  font-family: "Geist Mono", monospace;
+  font-size: 0.66rem;
+  font-weight: 500;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+}
+
+[data-md-color-scheme="slate"] .miles-card__chip {
+  background: rgba(255, 255, 255, 0.05);
+  color: rgba(241, 245, 249, 0.6);
+}
+
+/* --------------------------------------------------------------------------
+   Architecture strip
+   -------------------------------------------------------------------------- */
+.miles-strip {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+  gap: 0;
+  border: 1px solid var(--miles-line);
+  border-radius: 12px;
+  background: var(--md-default-bg-color);
+  overflow: hidden;
+}
+
+[data-md-color-scheme="default"] .miles-strip { background: #ffffff; }
+
+.miles-strip__cell {
+  padding: 1.3rem 1.3rem;
+  border-right: 1px solid var(--miles-line);
+  border-bottom: 1px solid var(--miles-line);
+}
+
+.miles-strip__cell:last-child { border-right: none; }
+
+@media (max-width: 720px) {
+  .miles-strip__cell { border-right: none; }
+}
+
+.miles-strip__cell .label {
+  display: block;
+  font-family: "Geist Mono", monospace;
+  font-size: 0.66rem;
+  letter-spacing: 0.16em;
+  text-transform: uppercase;
+  color: var(--md-default-fg-color--lighter);
+  margin-bottom: 0.45rem;
+}
+
+.miles-strip__cell .value {
+  display: block;
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-size: 0.96rem;
+  font-weight: 600;
+  color: var(--md-default-fg-color);
+  letter-spacing: -0.005em;
+}
+
+/* --------------------------------------------------------------------------
+   Update timeline
+   -------------------------------------------------------------------------- */
+.miles-updates {
+  display: flex;
+  flex-direction: column;
+  gap: 0;
+  border: 1px solid var(--miles-line);
+  border-radius: 12px;
+  overflow: hidden;
+  background: var(--md-default-bg-color);
+}
+
+[data-md-color-scheme="default"] .miles-updates { background: #ffffff; }
+
+.miles-update {
+  display: grid;
+  grid-template-columns: 130px 1fr;
+  gap: 1.4rem;
+  padding: 1.1rem 1.4rem;
+  border-bottom: 1px solid var(--miles-line);
+  transition: background-color 0.15s ease;
+}
+
+.miles-update:last-child { border-bottom: none; }
+
+.miles-update:hover {
+  background: var(--miles-bg-soft);
+}
+
+[data-md-color-scheme="slate"] .miles-update:hover {
+  background: rgba(255, 255, 255, 0.025);
+}
+
+.miles-update__date {
+  font-family: "Geist Mono", monospace;
+  font-weight: 500;
+  font-size: 0.74rem;
+  color: var(--md-default-fg-color--lighter);
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+  padding-top: 0.1rem;
+}
+
+.miles-update__body {
+  font-size: 0.86rem;
+  line-height: 1.6;
+  color: var(--md-default-fg-color--light);
+}
+
+.miles-update__body strong {
+  font-family: "Bricolage Grotesque", sans-serif;
+  font-weight: 600;
+  color: var(--md-default-fg-color);
+  margin-right: 0.4rem;
+}
+
+.miles-update__body a {
+  color: var(--md-accent-fg-color);
+  text-decoration: none;
+  border-bottom: 1px solid currentColor;
+  border-bottom-color: transparent;
+  transition: border-color 0.15s ease;
+}
+
+.miles-update__body a:hover {
+  border-bottom-color: currentColor;
+}
+
+/* --------------------------------------------------------------------------
+   Quote
+   -------------------------------------------------------------------------- */
+.miles-quote {
+  margin: 3.5rem auto;
+  max-width: 720px;
+  padding: 0;
+  text-align: center;
+  font-family: "Bricolage Grotesque", "Geist", sans-serif;
+  font-variation-settings: "opsz" 36, "wght" 500;
+  font-size: 1.32rem;
+  line-height: 1.5;
+  letter-spacing: -0.015em;
+  font-weight: 500;
+  color: var(--md-default-fg-color);
+  background: none;
+  border: none;
+}
+
+.miles-quote::before {
+  content: """;
+  display: block;
+  font-family: "Bricolage Grotesque", serif;
+  font-size: 3rem;
+  line-height: 0.5;
+  margin-bottom: 1rem;
+  color: var(--md-accent-fg-color);
+}
+
+.miles-quote cite {
+  display: block;
+  margin-top: 1.2rem;
+  font-family: "Geist Mono", monospace;
+  font-style: normal;
+  font-size: 0.74rem;
+  letter-spacing: 0.05em;
+  color: var(--md-default-fg-color--lighter);
+  text-transform: uppercase;
+}
+
+/* --------------------------------------------------------------------------
+   Mobile
+   -------------------------------------------------------------------------- */
+@media (max-width: 600px) {
+  .miles-hero {
+    margin: -1rem -0.6rem 1.5rem;
+    padding: 4rem 1rem 3rem;
+  }
+  .miles-hero__title {
+    font-size: clamp(2.2rem, 9vw, 3rem);
+  }
+  .miles-update {
+    grid-template-columns: 1fr;
+    gap: 0.4rem;
+  }
+  .miles-quote {
+    font-size: 1.08rem;
+  }
+}

From 667bdb25850e98011f060a5ef44fe678625b5b31 Mon Sep 17 00:00:00 2001
From: Zhichen Zeng 
Date: Mon, 11 May 2026 17:45:29 -0700
Subject: [PATCH 0042/1130] doc [3/n]: add docs/getting-started (#1099)

Co-authored-by: Zhichenzzz 
Co-authored-by: Shi-Dong 
---
 docs/getting-started/index.md        |  13 +++
 docs/getting-started/installation.md | 109 ++++++++++++++++++++++++
 docs/getting-started/quick-start.md  | 123 +++++++++++++++++++++++++++
 3 files changed, 245 insertions(+)
 create mode 100644 docs/getting-started/index.md
 create mode 100644 docs/getting-started/installation.md
 create mode 100644 docs/getting-started/quick-start.md

diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
new file mode 100644
index 00000000000..77783edaa80
--- /dev/null
+++ b/docs/getting-started/index.md
@@ -0,0 +1,13 @@
+---
+title: Getting Started
+description: Install Miles and run your first RL training job — the two pages you need to go from zero to a working loop.
+---
+
+# Getting Started
+
+Two pages take you from a fresh machine to a running RL training job:
+
+- **[Installation](installation.md)** — Docker (recommended), pip, or building from source on NVIDIA / AMD.
+- **[Quick Start](quick-start.md)** — `docker pull` to a GRPO run on Qwen3-4B in under an hour, on a single 8-GPU node.
+
+After the loop is running, [Models](../models/index.md) covers the family-specific recipes and the [User Guide](../user-guide/index.md) walks through concepts, data, monitoring, and customization.
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
new file mode 100644
index 00000000000..e3d78d25dbb
--- /dev/null
+++ b/docs/getting-started/installation.md
@@ -0,0 +1,109 @@
+---
+title: Installation
+description: Install Miles on NVIDIA or AMD GPUs. Docker is the recommended path.
+---
+
+# Installation
+
+There are three ways to install Miles. Docker is recommended because Miles pins patched
+versions of SGLang, Megatron-LM, and a few CUDA kernels.
+
+## Method 1: Docker (recommended)
+
+
+
+  
+
+    ```bash
+    docker pull radixark/miles:latest
+
+    docker run --rm \
+      --gpus all --ipc=host --shm-size=32g \
+      --ulimit memlock=-1 --ulimit stack=67108864 \
+      --network=host \
+      -it radixark/miles:latest /bin/bash
+    ```
+
+  
+  
+
+    ```bash
+    docker pull rlsys/miles:MI350-355-latest    # or MI300-latest
+
+    docker run --rm \
+      --device /dev/dri --device /dev/kfd \
+      --group-add video --ipc=host --shm-size=32g \
+      --cap-add SYS_PTRACE --security-opt seccomp=unconfined \
+      --privileged \
+      -it rlsys/miles:MI350-355-latest /bin/bash
+    ```
+
+  
+
+
+
+The image ships with:
+
+- PyTorch (matching the container's CUDA / ROCm version)
+- Megatron-LM, SGLang, FlashAttention-3, DeepGEMM, Apex
+- Ray, uv, and Miles installed editable at `/root/miles`
+
+See [Platforms](../platforms/index.md) for platform-specific notes.
+
+## Method 2: From source
+
+Clone and install in an existing environment.
+
+```bash
+git clone https://github.com/radixark/miles.git
+cd miles
+pip install -r requirements.txt
+pip install -e . --no-deps
+```
+
+
+
+**Patched dependencies.** Miles pins patched versions of SGLang and Megatron-LM. Installing them yourself at
+the wrong commit is the most common source of bug reports — use Docker if you can.
+
+
+
+## Method 3: Update an existing container
+
+If you already run a Miles image and want the latest code:
+
+```bash
+cd /root/miles
+git pull --rebase
+pip install -e . --no-deps
+ray stop && ray start --head --port=6379
+```
+
+## Verify
+
+Confirm Miles imports and the GPUs are visible:
+
+```bash
+python -c "import miles; print('Miles import OK')"
+nvidia-smi
+```
+
+If either command fails, see [Debugging](../developer/debug.md) or the [FAQ](../faq.md).
+
+## Hardware requirements
+
+| Hardware | Status |
+|---|---|
+| NVIDIA H100 / H200 | Production (CI guarded) |
+| NVIDIA B100 / B200 | Production |
+| NVIDIA A100 | Supported — FP8 features disabled |
+| AMD MI300X, MI325, MI350X, MI355X | Supported via ROCm |
+
+For multi-node training you also need a high-bandwidth interconnect — InfiniBand, RoCEv2,
+or Slingshot — and 200+ GB/s per node. Single-node jobs run fine over NVLink only.
+
+## Next steps
+
+- [Quick Start](quick-start.md) — run your first training job.
+- [Core concepts](../user-guide/concepts.md) — the mental model behind Miles.
+- [Training backends](../user-guide/usage.md) — Megatron vs FSDP.
diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md
new file mode 100644
index 00000000000..c8fa98923a8
--- /dev/null
+++ b/docs/getting-started/quick-start.md
@@ -0,0 +1,123 @@
+---
+title: Quick Start
+description: A working RL training job on Qwen3-4B in under an hour.
+---
+
+# Quick Start
+
+This page takes you from `docker pull` to a running GRPO training job on Qwen3-4B. It
+assumes an 8-GPU node (H100 / H200 / B-series) and roughly 200 GB of disk.
+
+For other models, see [Models](../models/index.md).
+
+## 1. Start the container
+
+On the **host**:
+
+```bash
+docker pull radixark/miles:latest
+docker run --rm \
+  --gpus all --ipc=host --shm-size=32g \
+  --ulimit memlock=-1 --ulimit stack=67108864 \
+  --network=host \
+  -it radixark/miles:latest /bin/bash
+```
+
+That drops you into a shell inside the container. Refresh the editable install:
+
+```bash
+cd /root/miles && git pull && pip install -e . --no-deps
+```
+
+Steps 2–4 below all run inside the container.
+
+## 2. Download model and data
+
+```bash
+hf download Qwen/Qwen3-4B                          --local-dir /root/Qwen3-4B
+hf download --repo-type dataset BytedTsinghua-SIA/DAPO-Math-17K --local-dir /root/dapo-math-17k
+hf download --repo-type dataset zhuzilin/aime-2024     --local-dir /root/aime-2024
+```
+
+## 3. Convert to Megatron format
+
+Megatron consumes a `torch_dist` checkpoint, not the raw HuggingFace directory.
+
+```bash
+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/Qwen3-4B \
+   --save          /root/Qwen3-4B_torch_dist
+```
+
+For larger models, run the converter under `torchrun --nproc-per-node 8` (optionally
+multi-node). See the [Models](../models/index.md) section for per-family conversion
+commands.
+
+## 4. Launch training
+
+```bash
+bash scripts/run-qwen3-4B.sh
+```
+
+The script starts Ray, launches SGLang engines, loads the Megatron actor, and runs the
+rollout / train loop. After a minute or two you should see iteration logs:
+
+```text
+[ray]      starting cluster on 1 node, 8 gpus
+[sglang]   launching 4 engines (tp=2 each)
+[megatron] loading dist checkpoint from /root/Qwen3-4B_torch_dist
+[trainer]  iter 1/3000 | loss=0.412 reward=0.61 rollout=18.4s train=22.1s
+```
+
+## 5. What's happening
+
+Each iteration runs the same four steps:
+
+```mermaid
+flowchart LR
+    P[Prompt dataset] --> R[SGLang rollout]
+    R --> RM[Reward fn]
+    RM --> A[Megatron actor]
+    A == P2P weight sync ==> R
+    A -. KL .-> Ref[(Reference model)]
+```
+
+1. Sample `rollout-batch-size` prompts and generate `n-samples-per-prompt` responses.
+2. Score responses with the reward model (`--rm-type deepscaler` in this recipe).
+3. Compute the GRPO objective and step the optimizer.
+4. Push updated weights back to the SGLang engines via P2P.
+
+The four batch-sizing knobs satisfy:
+
+```
+rollout_batch_size × n_samples_per_prompt
+  = global_batch_size × num_steps_per_rollout
+```
+
+Miles fills in whichever side you leave unset.
+
+## Inspecting a run
+
+| Question | Where to look |
+|---|---|
+| Is the policy learning? | `loss` and `reward` columns in stdout, or wandb |
+| Rollout or train bottleneck? | `rollout=` vs. `train=` timings per iteration |
+| Are GPUs saturated? | `nvidia-smi dmon -s u` |
+| SGLang internals? | `tail -f /tmp/sglang/*.log` |
+| Ranks crashing? | `~/.ray/session_latest/logs/worker-*.err` |
+
+## Next steps
+
+- [Core concepts](../user-guide/concepts.md) — the model behind rollout / actor / reference.
+- [Training script walkthrough](../user-guide/training-script-walkthrough.md) —
+  an annotated tour through every argument group in a launch script, plus colocation,
+  dynamic sampling, partial rollout, and BF16+FP8 inference.
+- [Training backends](../user-guide/usage.md) — Megatron vs FSDP.
+- [Customization](../user-guide/customization.md) — plug in custom rollout / reward.
+- [Models](../models/index.md) — recipes for Qwen3.5, GLM4.5, DeepSeek R1, Kimi K2, and more.
+
+If you hit issues, the [FAQ](../faq.md) covers the common ones.

From 5ef83f3b62df757291d67fa5970db611ce22a22a Mon Sep 17 00:00:00 2001
From: Zhichen Zeng 
Date: Mon, 11 May 2026 17:45:51 -0700
Subject: [PATCH 0043/1130] doc [4/n]: add docs/user-guide (#1100)

Co-authored-by: Zhichenzzz 
Co-authored-by: Shi-Dong 
---
 docs/user-guide/agentic-chat-template.md      | 129 +++++
 docs/user-guide/argument-groups.md            | 169 +++++++
 docs/user-guide/cli-reference.md              | 353 +++++++++++++
 docs/user-guide/concepts.md                   |  94 ++++
 docs/user-guide/customization.md              | 294 +++++++++++
 docs/user-guide/fully-async.md                | 104 ++++
 docs/user-guide/index.md                      |  26 +
 docs/user-guide/monitoring.md                 | 120 +++++
 docs/user-guide/rollout-endpoints.md          | 258 ++++++++++
 .../user-guide/training-script-walkthrough.md | 472 ++++++++++++++++++
 docs/user-guide/usage.md                      | 185 +++++++
 11 files changed, 2204 insertions(+)
 create mode 100644 docs/user-guide/agentic-chat-template.md
 create mode 100644 docs/user-guide/argument-groups.md
 create mode 100644 docs/user-guide/cli-reference.md
 create mode 100644 docs/user-guide/concepts.md
 create mode 100644 docs/user-guide/customization.md
 create mode 100644 docs/user-guide/fully-async.md
 create mode 100644 docs/user-guide/index.md
 create mode 100644 docs/user-guide/monitoring.md
 create mode 100644 docs/user-guide/rollout-endpoints.md
 create mode 100644 docs/user-guide/training-script-walkthrough.md
 create mode 100644 docs/user-guide/usage.md

diff --git a/docs/user-guide/agentic-chat-template.md b/docs/user-guide/agentic-chat-template.md
new file mode 100644
index 00000000000..dca075a5a66
--- /dev/null
+++ b/docs/user-guide/agentic-chat-template.md
@@ -0,0 +1,129 @@
+---
+title: Agentic Chat Templates
+description: How to verify and override the chat template applied during multi-turn rollout.
+---
+
+# Agentic Chat Templates
+
+In agentic / multi-turn workflows, Miles uses SGLang's pretokenized prefix mechanism
+so the conversation history is not re-tokenized every turn. That requires the chat
+template to satisfy an **append-only invariant**: rendering messages `[1..N]` must
+produce a string that is an exact prefix of rendering `[1..N+1]`.
+
+Some community templates violate this. They use `loop.last` or other
+context-dependent Jinja logic that flips bits across turns, and the result is silent
+tokenization drift, divergent log-probabilities, and gradient blow-up after a few
+iterations of multi-turn RL.
+
+Miles ships a verifier and an autofix.
+
+## Quick start
+
+### Verify a HuggingFace template
+
+```bash
+python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B
+```
+
+Failing output (illustrative):
+
+```text
+Template source: HuggingFace: Qwen/Qwen3-0.6B
+Thinking cases:  disabled
+
+  [FAIL] single_tool-N3                Prefix mismatch
+  [PASS] single_tool-N3-no_tools
+  [FAIL] multi_turn-N4                 Prefix mismatch
+  ...
+Verdict: FAIL - template is NOT append-only after last user message
+```
+
+### Apply Miles's autofix
+
+If a fixed template ships for that model, `--autofix` swaps it in and re-runs the
+suite:
+
+```bash
+python scripts/tools/verify_chat_template.py --model Qwen/Qwen3-0.6B --autofix
+```
+
+```text
+Template source: fixed template: .../templates/qwen3_fixed.jinja
+Verdict: PASS - template IS append-only after last user message
+```
+
+### Verify a local Jinja file
+
+```bash
+python scripts/tools/verify_chat_template.py --template path/to/my_template.jinja
+```
+
+### Include thinking-specific cases
+
+For Qwen3.5, GLM-5, and other models that toggle `enable_thinking`, add `--thinking`
+to also run thinking-specific trajectories.
+
+```bash
+python scripts/tools/verify_chat_template.py --model Qwen/Qwen3.5-0.8B --autofix --thinking
+```
+
+## CLI
+
+```text
+usage: verify_chat_template.py (--template PATH | --model MODEL_ID)
+                               [--autofix] [--thinking]
+```
+
+| Flag | What |
+|---|---|
+| `--template PATH` | Local `.jinja` template. |
+| `--model MODEL_ID` | HF model ID. |
+| `--autofix` | Apply Miles's fixed template if available. |
+| `--thinking` | Also run thinking-specific cases. |
+
+Exit code is `0` on pass, `1` on fail.
+
+## How it works
+
+For each test case (a list of messages), the verifier renders progressive prefixes
+and checks the invariant character by character:
+
+```python
+for n in range(1, len(messages)):
+    full   = render(messages[: n + 1])
+    prefix = render(messages[: n])
+    assert full.startswith(prefix), f"break between turn {n} and {n+1}"
+```
+
+The trajectory specs and cases live in
+`miles/utils/test_utils/chat_template_verify.py`. Standard cases cover
+single-tool, multi-turn, parallel-tool, and long-chain trajectories; the thinking
+suite adds variants that toggle `enable_thinking`.
+
+A break almost always comes from `loop.last`, conditional whitespace, or a closing
+token that's only emitted on the final turn.
+
+## Using the fixed template at training time
+
+Once you have the right template, point Miles at it:
+
+```bash
+ROLLOUT_ARGS+=(
+   --chat-template-path /opt/miles/utils/chat_template_utils/templates/qwen3_fixed.jinja
+)
+```
+
+Built-in fixed templates that ship with Miles live under
+`miles/utils/chat_template_utils/templates/` (e.g. `qwen3_fixed.jinja`,
+`qwen3.5_fixed.jinja`, `qwen3_thinking_2507_and_next_fixed.jinja`).
+
+## What "append-only" buys you
+
+| Without it | With it |
+|---|---|
+| Re-tokenize everything each turn | Tokenize only the new turn |
+| O(N²) tokenization cost | O(N) tokenization cost |
+| Subtle drift between turns | Bit-stable tokens |
+| Multi-turn RL collapses after ~50 steps | Stable across thousands of steps |
+
+Running the verifier as part of every model's pre-flight is recommended.
diff --git a/docs/user-guide/argument-groups.md b/docs/user-guide/argument-groups.md
new file mode 100644
index 00000000000..92f226cccd8
--- /dev/null
+++ b/docs/user-guide/argument-groups.md
@@ -0,0 +1,169 @@
+---
+title: Argument Groups
+description: The launch-script argument groups used by Miles recipes, with links to the flags that belong in each group.
+---
+
+# Argument Groups
+
+Miles launch scripts are bash arrays. The grouping is deliberately boring: each array
+owns one operational concern, then the script expands all arrays into `train.py` or
+`train_async.py`.
+
+Use this page to decide where a flag belongs. Use the [CLI Reference](cli-reference.md)
+when you need the full default and type for an individual flag.
+
+| Group | Owns | Typical source |
+|---|---|---|
+| [`MODEL_ARGS`](#model-args) | Architecture constants and plugin specs | `scripts/models/.sh` |
+| [`CKPT_ARGS`](#ckpt-args) | Actor, reference, HF tokenizer/config, save paths | Launch script |
+| [`ROLLOUT_ARGS`](#rollout-args) | Prompt data, sampling, reward, train/eval batch flow | Launch script |
+| [`EVAL_ARGS`](#eval-args) | Evaluation datasets and eval-only sampling overrides | Launch script |
+| [`PERF_ARGS`](#perf-args) | Parallelism, recomputation, dynamic batching | Recipe defaults |
+| [`GRPO_ARGS`](#grpo-args) | RL objective, KL, clipping, entropy, advantage estimator | Recipe defaults |
+| [`OPTIMIZER_ARGS`](#optimizer-args) | Learning rate, schedule, weight decay, Adam betas | Recipe defaults |
+| [`SGLANG_ARGS`](#sglang-args) | Rollout engine topology and `--sglang-*` passthrough | Deployment shape |
+
+
+## MODEL_ARGS - architecture constants
+
+`MODEL_ARGS` tells Megatron what model it is instantiating. Megatron cannot infer all
+architecture details from a HuggingFace checkpoint, so each recipe sources a matching
+file from `scripts/models/`.
+
+Common entries:
+
+| Flag family | Example |
+|---|---|
+| Transformer shape | `--num-layers`, `--hidden-size`, `--num-attention-heads` |
+| Tokenizer/model dimensions | `--seq-length`, `--max-position-embeddings`, `--vocab-size` |
+| Rotary and attention variants | `--rotary-base`, `--rotary-percent`, `--kv-channels` |
+| MoE architecture | `--num-experts`, `--moe-router-topk`, `--moe-grouped-gemm` |
+| Plugin specs | `--spec miles_plugins.models.qwen3_5 get_qwen3_5_spec` |
+
+Keep these values aligned with the checkpoint's `config.json`. If one checkpoint in a
+family changes rotary base, vocab padding, or normalization epsilon, override the
+sourced defaults in the launch script.
+
+
+## CKPT_ARGS - checkpoint paths
+
+`CKPT_ARGS` wires the three model roles in a run:
+
+| Role | Flag |
+|---|---|
+| HuggingFace directory for tokenizer, config, and SGLang boot | `--hf-checkpoint` |
+| Frozen reference model for KL anchoring | `--ref-load` |
+| Actor resume point | `--load` |
+| Actor output directory | `--save` |
+
+`--load` and `--save` usually point to the same directory. If `--load` has no
+`latest_checkpointed_iteration.txt`, Miles warm-starts the actor from `--ref-load`.
+
+
+## ROLLOUT_ARGS - sampling and reward
+
+`ROLLOUT_ARGS` controls data entering the loop and how many samples each rollout
+produces.
+
+| Concern | Flags |
+|---|---|
+| Prompt data | `--prompt-data`, `--input-key`, `--label-key`, `--apply-chat-template` |
+| Rollout volume | `--rollout-batch-size`, `--n-samples-per-prompt`, `--num-rollout` |
+| Training consumption | `--global-batch-size`, `--num-steps-per-rollout` |
+| Sampling | `--rollout-temperature`, `--rollout-top-p`, `--rollout-max-response-len` |
+| Reward | `--rm-type`, `--custom-rm-path` |
+| Filtering | `--over-sampling-batch-size`, `--dynamic-sampling-filter-path` |
+
+The rollout volume and training consumption must satisfy the
+[four-knob invariant](concepts.md#the-four-knob-invariant).
+
+
+## EVAL_ARGS - evaluation overrides
+
+Evaluation reuses the rollout stack but usually runs with a different dataset and more
+deterministic sampling.
+
+Common entries:
+
+| Concern | Flags |
+|---|---|
+| Cadence | `--eval-interval` |
+| Dataset | `--eval-prompt-data` |
+| Eval group size | `--n-samples-per-eval-prompt` |
+| Eval-only generation | `--eval-max-response-len`, `--eval-top-p`, `--eval-temperature` |
+
+Flags not set in `EVAL_ARGS` inherit from `ROLLOUT_ARGS`.
+
+
+## PERF_ARGS - parallelism and memory
+
+`PERF_ARGS` controls how training is sharded and how activation memory is managed.
+
+| Concern | Flags |
+|---|---|
+| Tensor parallelism | `--tensor-model-parallel-size`, `--sequence-parallel` |
+| Pipeline parallelism | `--pipeline-model-parallel-size` |
+| Context parallelism | `--context-parallel-size` |
+| Expert parallelism | `--expert-model-parallel-size`, `--expert-tensor-parallel-size` |
+| Recomputation | `--recompute-granularity`, `--recompute-method`, `--recompute-num-layers` |
+| Dynamic batching | `--use-dynamic-batch-size`, `--max-tokens-per-gpu` |
+
+Megatron exposes TP, PP, CP, EP, and ETP, but not every product of those dimensions is
+valid or worth using for every model. Start from the recipe's tested combination and
+see [parallelism compatibility](usage.md#parallelism-compatibility) before changing
+more than one dimension.
+
+
+## GRPO_ARGS - RL objective
+
+`GRPO_ARGS` controls the policy-gradient objective and the stability terms around it.
+
+| Concern | Flags |
+|---|---|
+| Algorithm | `--advantage-estimator` |
+| KL | `--use-kl-loss`, `--kl-loss-coef`, `--kl-loss-type` |
+| Clipping | `--eps-clip`, `--eps-clip-high` |
+| Entropy | `--entropy-coef` |
+| Loss reduction | `--calculate-per-token-loss` |
+| Precision/off-policy safety | `--use-tis` |
+
+Zero-weight KL is recipe-specific. `--use-kl-loss --kl-loss-coef 0.00` still loads the
+reference and logs KL; it does not remove the reference model.
+
+
+## OPTIMIZER_ARGS - optimizer schedule
+
+`OPTIMIZER_ARGS` carries the optimizer choice and scalar schedule.
+
+Common entries:
+
+| Concern | Flags |
+|---|---|
+| Optimizer | `--optimizer` |
+| Learning rate | `--lr`, `--min-lr`, `--lr-decay-style` |
+| Adam | `--adam-beta1`, `--adam-beta2`, `--adam-eps` |
+| Regularization | `--weight-decay`, `--clip-grad` |
+
+Post-training is sensitive to large updates. Most recipes start near `1e-6` and use a
+constant schedule unless the model page says otherwise.
+
+
+## SGLANG_ARGS - rollout engine passthrough
+
+`SGLANG_ARGS` configures the inference side. Miles owns
+`--rollout-num-gpus-per-engine`; everything prefixed with `--sglang-` is forwarded to
+`python -m sglang.launch_server` after removing the prefix.
+
+Common entries:
+
+| Concern | Flags |
+|---|---|
+| Engine tensor parallelism | `--rollout-num-gpus-per-engine` |
+| Engine memory | `--sglang-mem-fraction-static` |
+| Context length | `--sglang-context-length` |
+| MoE serving | `--sglang-enable-ep-moe`, `--sglang-enable-dp-attention` |
+| Debugging | `--sglang-log-level` |
+
+SGLang parallelism is separate from trainer parallelism. For example,
+`--rollout-num-gpus-per-engine` maps to the SGLang server's TP size, not Megatron's
+`--tensor-model-parallel-size`.
diff --git a/docs/user-guide/cli-reference.md b/docs/user-guide/cli-reference.md
new file mode 100644
index 00000000000..4ce581236a3
--- /dev/null
+++ b/docs/user-guide/cli-reference.md
@@ -0,0 +1,353 @@
+---
+title: CLI Reference
+description: Every command-line flag Miles accepts, grouped by subsystem.
+---
+
+# CLI Reference
+
+Miles is configured through command-line flags passed to `train.py` or
+`train_async.py`. The Megatron flags (such as `--num-layers`, `--rotary-base`,
+`--recompute-granularity`) are inherited via Megatron's argument parser; Miles adds
+its own flags through an `extra_args_provider`. Run `python3 train.py --help` against
+your installed Megatron source for the canonical list.
+
+This page has two passes.
+
+1. **Essentials** lists the flags most runs actually touch.
+2. **Complete reference** lists every Miles flag with type and default.
+
+---
+
+## Essentials
+
+### Cluster topology
+
+| Flag | Default | What |
+|---|---|---|
+| `--actor-num-nodes` | `1` | Total nodes for the actor. |
+| `--actor-num-gpus-per-node` | `8` | GPUs per actor node. |
+| `--rollout-num-gpus` | derived | GPUs for SGLang rollout (ignored when `--colocate`). |
+| `--rollout-num-gpus-per-engine` | `1` | TP size of each SGLang engine. |
+| `--colocate` | off | Share GPUs between actor and rollout. |
+
+See [Training Script Walkthrough: Colocation](training-script-walkthrough.md#colocation-share-gpus-or-dont)
+for what `--colocate` flips on under the hood.
+
+### Batch sizing
+
+The four-knob invariant:
+
+```
+rollout_batch_size × n_samples_per_prompt
+  = global_batch_size × num_steps_per_rollout
+```
+
+| Flag | Typical | What |
+|---|---|---|
+| `--rollout-batch-size` | `16 – 256` | Prompts per rollout. |
+| `--n-samples-per-prompt` | `4 – 16` | Responses per prompt (GRPO group size). |
+| `--global-batch-size` | derived | Samples per optimizer step. |
+| `--num-steps-per-rollout` | `1` | Optimizer steps per rollout. |
+| `--num-rollout` | `1000 – 10000` | Total rollout iterations. |
+
+### Memory and throughput
+
+| Flag | Default | What |
+|---|---|---|
+| `--use-dynamic-batch-size` | off | Pack varlen samples into micro-batches. |
+| `--max-tokens-per-gpu` | `–` | Token budget per micro-batch per GPU. Required when dynamic batching is on. |
+| `--context-parallel-size` | `1` | Spread a single sample across N CP ranks. |
+| `--recompute-granularity` | Megatron default | `full` or `selective`. |
+| `--recompute-method` | Megatron default | `uniform` or `block`. |
+| `--recompute-num-layers` | Megatron default | Layers per recompute chunk. |
+
+Rule of thumb: start with `max_tokens_per_gpu = rollout_max_response_len / cp_size`,
+then push up until you OOM.
+
+### RL algorithm
+
+| Flag | Default | What |
+|---|---|---|
+| `--advantage-estimator` | `grpo` | `grpo`, `gspo`, `ppo`, `reinforce_plus_plus`, `reinforce_plus_plus_baseline`, `on_policy_distillation` |
+| `--use-kl-loss` | off | Compute KL against the reference model. |
+| `--kl-loss-coef` | `0.0` | Weight of KL in the loss (0 means monitor only). |
+| `--kl-loss-type` | `k1` | `k1`, `k2`, `k3`, `low_var_kl`. |
+| `--entropy-coef` | `0.0` | Entropy bonus weight. |
+| `--eps-clip` | `0.2` | PPO/GRPO low clip. |
+| `--eps-clip-high` | `–` | Asymmetric high clip (DAPO-style). |
+| `--use-tis` | off | Truncated Importance Sampling for train/inference precision mismatch. |
+
+### Sampling
+
+| Flag | Default | What |
+|---|---|---|
+| `--rollout-temperature` | `1.0` | Sampling temperature. |
+| `--rollout-top-p` | `1.0` | Top-p truncation. |
+| `--rollout-max-response-len` | `–` | Max tokens per response. |
+| `--rollout-stop-token-ids` | model default | Stop token IDs. Override when generations don't stop. |
+| `--apply-chat-template` | off | Apply the tokenizer's chat template. |
+| `--rollout-shuffle` | off | Shuffle prompts each rollout. |
+
+### Optimizer
+
+| Flag | Default | What |
+|---|---|---|
+| `--optimizer` | `adam` | `adam`, `sgd`. |
+| `--lr` | `1e-6` | Learning rate. Post-training is sensitive to large updates; recipes typically stay near `1e-6`. |
+| `--lr-decay-style` | `constant` | `constant`, `linear`, `cosine`. |
+| `--weight-decay` | `0.1` | L2 weight decay. |
+| `--adam-beta1`, `--adam-beta2` | `0.9, 0.98` | Adam moments. |
+
+### Logging
+
+| Flag | Default | What |
+|---|---|---|
+| `--use-wandb` | off | Log to Weights and Biases. |
+| `--wandb-project` | – | wandb project name. |
+| `--log-interval` | `1` | Stdout log cadence (rollouts). |
+| `--save-interval` | – | Checkpoint cadence (rollouts). Recipes typically set 20 to 100. |
+
+### SGLang passthrough
+
+Any flag accepted by `python -m sglang.launch_server` is accepted by Miles with the
+`--sglang-` prefix:
+
+```bash
+--sglang-log-level INFO
+--sglang-mem-fraction-static 0.8
+--sglang-enable-overlap-schedule
+--sglang-enable-ep-moe
+--sglang-enable-dp-attention
+```
+
+See [SGLang docs](https://docs.sglang.io) for the full list.
+
+### Environment variables
+
+Set these in Ray's `env_vars` for multi-node runs:
+
+| Variable | Effect |
+|---|---|
+| `TORCHINDUCTOR_FORCE_DISABLE_CACHES=1` | Workaround for torch-compile JSONDecodeError. |
+| `RAY_DEDUP_LOGS=0` | Don't deduplicate worker logs. |
+| `NCCL_DEBUG=INFO` | NCCL diagnostics. |
+| `PYTHONPATH=/root/Megatron-LM` | Required when using the Megatron backend. |
+
+---
+
+## Complete reference
+
+Sections mirror the launch-script argument groups.
+
+### Cluster
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--actor-num-nodes` | int | `1` | Total nodes for actor training. |
+| `--actor-num-gpus-per-node` | int | `8` | GPUs per actor node. |
+| `--rollout-num-gpus` | int | derived | Ignored under `--colocate`. |
+| `--rollout-num-gpus-per-engine` | int | `1` | TP size of each SGLang engine. |
+| `--colocate` | flag | off | Share GPUs between actor and rollout. Implicitly enables `--offload-train`, `--offload-rollout`, and `--sglang-disable-piecewise-cuda-graph`. |
+
+### Model and checkpoints
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--train-backend` | enum | `megatron` | `megatron` or `fsdp`. |
+| `--hf-checkpoint` | path | – | HF model dir. Provides tokenizer, config, and the weights FSDP loads. |
+| `--ref-load` | path | – | Reference model in `torch_dist` format (Megatron). |
+| `--load` | path | – | Actor checkpoint to resume from. |
+| `--save` | path | – | Actor checkpoint write directory. |
+| `--save-interval` | int | – | Rollouts between saves. |
+| `--model-name` | str | – | Set in multi-node to avoid `transformers` file-system race. |
+| `--spec` | ` ` | – | Plugin spec for custom architectures (e.g. `miles_plugins.models.qwen3_5 get_qwen3_5_spec`). |
+
+### Rollout: data and batching
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--prompt-data` | str | – | Path to a single JSONL file. |
+| `--input-key` | str | `prompt` | JSONL key to `Sample.prompt`. |
+| `--label-key` | str | `label` | JSONL key to `Sample.label`. |
+| `--metadata-key` | str | `metadata` | JSONL key to `Sample.metadata`. |
+| `--apply-chat-template` | flag | off | Apply tokenizer chat template. |
+| `--rollout-shuffle` | flag | off | Shuffle prompts each rollout. |
+| `--num-rollout` | int | – | Total rollout iterations. If unset, derived from dataset size. |
+| `--rollout-batch-size` | int | – | Prompts per rollout. |
+| `--n-samples-per-prompt` | int | `1` | Responses per prompt. |
+| `--global-batch-size` | int | derived | Samples per optimizer step. |
+| `--num-steps-per-rollout` | int | `1` | Optimizer steps per rollout. |
+| `--over-sampling-batch-size` | int | – | Oversample size for dynamic sampling (DAPO). |
+| `--balance-data` | flag | off | Balance per-rank token count. |
+
+### Rollout: sampling
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--rollout-max-response-len` | int | – | Max tokens per response. |
+| `--rollout-temperature` | float | `1.0` | Sampling temperature. |
+| `--rollout-top-p` | float | `1.0` | Top-p truncation. |
+| `--rollout-top-k` | int | `-1` | Top-k truncation (-1 disables). |
+| `--rollout-stop` | str+ | – | Stop strings. |
+| `--rollout-stop-token-ids` | int+ | – | Stop token IDs. |
+
+### Eval
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--eval-prompt-data` | str+ | – | One or more `name path` pairs. |
+| `--eval-interval` | int | – | Rollouts between eval runs. |
+| `--n-samples-per-eval-prompt` | int | `1` | Responses per eval prompt. |
+| `--eval-max-response-len` | int | – | Max eval response length. Inherits from rollout if unset. |
+| `--eval-temperature` | float | – | Eval temperature. Inherits from rollout if unset. |
+| `--eval-top-p` | float | – | Eval top-p. Inherits from rollout if unset. |
+
+### Performance
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--tensor-model-parallel-size` | int | `1` | TP. |
+| `--pipeline-model-parallel-size` | int | `1` | PP. |
+| `--context-parallel-size` | int | `1` | CP. |
+| `--expert-model-parallel-size` | int | `1` | EP (MoE). |
+| `--expert-tensor-parallel-size` | int | `1` | TP within experts. |
+| `--sequence-parallel` | flag | off | Enable Megatron sequence parallel. |
+| `--use-dynamic-batch-size` | flag | off | Pack varlen samples. Recommended for varlen workloads. |
+| `--max-tokens-per-gpu` | int | – | Token budget per micro-batch per GPU. Required when dynamic batching is on. |
+| `--micro-batch-size` | int | `1` | Ignored when dynamic batching is on. |
+| `--recompute-granularity` | enum | Megatron default | `full` or `selective`. |
+| `--recompute-method` | enum | Megatron default | `uniform` or `block`. |
+| `--recompute-num-layers` | int | Megatron default | Recompute chunk size. |
+| `--gradient-checkpointing` | flag | off | FSDP equivalent of recompute flags. |
+| `--fsdp-cpu-offload` | flag | off | FSDP: offload params, grads, optimizer state to CPU. |
+| `--fsdp-cpu-backend` | str | `gloo` | FSDP: CPU backend for hybrid offload. |
+| `--attn-implementation` | enum | `flash_attention_2` | FSDP only: `flash_attention_2`, `sdpa`, `eager`. |
+
+### RL algorithm
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--advantage-estimator` | enum | `grpo` | `grpo`, `gspo`, `ppo`, `reinforce_plus_plus`, `reinforce_plus_plus_baseline`, `on_policy_distillation` |
+| `--use-kl-loss` | flag | off | Compute KL vs. reference. |
+| `--kl-loss-coef` | float | `0.0` | KL weight in loss (0 means monitor). |
+| `--kl-loss-type` | enum | `k1` | `k1`, `k2`, `k3`, `low_var_kl`. |
+| `--entropy-coef` | float | `0.0` | Entropy bonus weight. |
+| `--eps-clip` | float | `0.2` | PPO/GRPO low clip. |
+| `--eps-clip-high` | float | – | Asymmetric high clip. |
+| `--use-tis` | flag | off | Truncated Importance Sampling. |
+| `--use-routing-replay` | flag | off | Forward/backward routing consistency. |
+| `--use-rollout-routing-replay` | flag | off | R3 — capture inference-side expert routing and replay it during training. |
+| `--calculate-per-token-loss` | flag | off | Per-token loss reduction. |
+| `--no-check-for-nan-in-loss-and-grad` | flag | off | Skip NaN/Inf guard (Megatron flag, debug only). |
+| `--true-on-policy-mode` | flag | off | Strict on-policy: reject samples from a prior policy. |
+
+### Optimizer
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--optimizer` | enum | `adam` | `adam`, `sgd`. |
+| `--lr` | float | `1e-6` | Learning rate. |
+| `--lr-decay-style` | enum | `constant` | `constant`, `linear`, `cosine`. |
+| `--lr-warmup-iters` | int | `0` | Warmup steps (Megatron flag). |
+| `--min-lr` | float | `0` | Lower LR bound for decay schedules (Megatron flag). |
+| `--weight-decay` | float | `0.1` | L2 weight decay. |
+| `--adam-beta1` | float | `0.9` | |
+| `--adam-beta2` | float | `0.98` | |
+| `--clip-grad` | float | `1.0` | Grad clipping (Megatron flag). |
+| `--optimizer-cpu-offload` | flag | off | Megatron CPU Adam (Megatron flag). |
+| `--overlap-cpu-optimizer-d2h-h2d` | flag | off | Overlap D2H/H2D with compute (Megatron flag). |
+| `--use-precision-aware-optimizer` | flag | off | Precision-aware optimizer path (Megatron flag). |
+
+### Reward and filters
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--rm-type` | enum | – | Built-in reward: `math`, `dapo`, `deepscaler`, `f1`, `gpqa`, `ifbench`, `remote_rm`, `random`. |
+| `--rm-url` | str | – | Endpoint when `--rm-type remote_rm`. |
+| `--group-rm` | flag | off | Batched reward computation. |
+| `--custom-rm-path` | str | – | Custom reward function (see [Customization](customization.md)). |
+| `--dynamic-sampling-filter-path` | str | – | Group filter (DAPO-style). |
+| `--buffer-filter-path` | str | – | Buffer dequeue filter. |
+| `--rollout-sample-filter-path` | str | – | Per-sample filter. |
+
+### SGLang and router
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--sglang-router-ip` | str | – | External router IP. Miles starts its own router if unset. |
+| `--sglang-router-port` | int | – | External router port. |
+| `--sglang-*` | passthrough | | Any flag accepted by `python -m sglang.launch_server` works with this prefix. |
+| `--router-*` | passthrough | | Any flag accepted by the active router works with this prefix. |
+
+Common `--sglang-*` flags:
+
+```bash
+--sglang-mem-fraction-static 0.8
+--sglang-context-length 32768
+--sglang-log-level INFO
+--sglang-enable-ep-moe
+--sglang-enable-dp-attention
+--sglang-enable-deepep
+--sglang-enable-overlap-schedule
+--sglang-enforce-piecewise-cuda-graph     # off by default in colocate mode
+```
+
+### MTP / speculative decoding
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--mtp-num-layers` | int | `0` | Number of MTP layers in the checkpoint. |
+| `--enable-mtp-training` | flag | off | Train MTP alongside the policy. |
+| `--mtp-loss-scaling-factor` | float | `0.2` | Weight of MTP loss. |
+
+### Fault tolerance
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--use-fault-tolerance` | flag | off | Enable rank-level recovery and heartbeats. |
+| `--rollout-health-check-first-wait` | int | `0` | Grace period before heartbeats start. |
+| `--rollout-health-check-interval` | int | `30` | Seconds between heartbeats. |
+| `--rollout-health-check-timeout` | int | `30` | Heartbeat timeout. |
+
+### Async / partial rollout
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--partial-rollout` | flag | off | Resume aborted rollouts in the next iteration. |
+
+### Logging
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--use-wandb` | flag | off | Enable wandb. |
+| `--wandb-project` | str | – | Project name. |
+| `--wandb-group` | str | – | Group name. |
+| `--log-interval` | int | `1` | Stdout log cadence (rollouts). |
+| `--custom-rollout-log-function-path` | str | – | Custom train logger. |
+| `--custom-eval-rollout-log-function-path` | str | – | Custom eval logger. |
+
+### Profiling
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--profile-target` | enum+ | `[train_overall]` | Which sub-loop to profile: `train_overall`, `train_actor`, `train_log_probs`. |
+| `--use-pytorch-profiler` | flag | off | FSDP only: enable PyTorch profiler. |
+| `--profile-step-start` | int | `10` | FSDP only: first step to profile. |
+| `--profile-step-end` | int | `12` | FSDP only: last step to profile. |
+| `--memory-snapshot-path` | str | `snapshot.pickle` | FSDP only: memory snapshot output. |
+| `--tensorboard-dir` | str | – | FSDP only: TensorBoard output dir. |
+
+### Debugging
+
+| Flag | Type | Default | Notes |
+|---|---|---|---|
+| `--debug-rollout-only` | flag | off | Skip Megatron, only spin up SGLang. |
+| `--debug-train-only` | flag | off | Skip SGLang, only spin up Megatron. |
+| `--save-debug-rollout-data` | path | – | Pickle every rollout to disk. |
+| `--load-debug-rollout-data` | path | – | Replay rollouts from disk (implies `--debug-train-only`). |
+| `--deterministic-mode` | flag | off | Megatron deterministic mode. |
+
+### Customization
+
+See [Customization](customization.md) for the full catalogue of `--*-path` flags
+that replace or extend Miles's behaviour.
diff --git a/docs/user-guide/concepts.md b/docs/user-guide/concepts.md
new file mode 100644
index 00000000000..a4d79ac0138
--- /dev/null
+++ b/docs/user-guide/concepts.md
@@ -0,0 +1,94 @@
+---
+title: Core Concepts
+description: The four objects that make up every Miles job, and how data flows between them.
+---
+
+# Core Concepts
+
+A Miles training job is a loop over four objects. Once you understand what each one
+*is* and how data flows between them, every flag in the system has an obvious home.
+
+## The four objects
+
+```mermaid
+flowchart LR
+    subgraph Rollout
+      P[Prompt dataset] --> R[SGLang engines]
+    end
+    subgraph Scoring
+      R --> RM[Reward model]
+    end
+    subgraph Training
+      RM --> A[Actor — Megatron]
+      A -. KL .-> RF[(Frozen reference)]
+    end
+    A -- weights --> R
+```
+
+| Object | Role | Lives in |
+|---|---|---|
+| **Prompt dataset** | Source of input examples | JSONL on disk (or `--data-source-path`) |
+| **Rollout (SGLang engines)** | Generates responses given prompts | One or more SGLang servers behind a router |
+| **Reward model** | Maps `(prompt, response, label) → score` | Built-in (`--rm-type`) or custom (`--custom-rm-path`) |
+| **Actor (Megatron / FSDP)** | The model being trained | Megatron `torch_dist` checkpoint, or HF directory under FSDP |
+| **Reference** | Frozen copy of the actor for KL anchoring | Loaded from `--ref-load`, never updated |
+
+## The training loop
+
+```python
+for it in range(num_rollout):
+    # 1. Sample
+    prompts   = dataset.sample(rollout_batch_size)
+    responses = sglang.generate(prompts, n=n_samples_per_prompt)
+
+    # 2. Score
+    rewards   = reward_fn(prompts, responses, labels)
+
+    # 3. Optimize
+    for step in range(num_steps_per_rollout):
+        batch = pack(prompts, responses, rewards, size=global_batch_size)
+        loss  = grpo_loss(actor, ref_model, batch)
+        loss.backward(); optimizer.step()
+
+    # 4. Sync
+    p2p_weight_transfer(actor → sglang_engines)
+```
+
+That's the whole thing. Every flag in Miles configures one of these four steps.
+
+## The four-knob invariant
+
+Two knobs govern the sampling half of the loop, two govern the training half, and they
+are locked into a single equation:
+
+```
+rollout_batch_size × n_samples_per_prompt
+  = global_batch_size × num_steps_per_rollout
+```
+
+**Every sample produced by rollout is consumed by training, and every sample consumed
+by training was produced by rollout.** Set any three sides; Miles fills in the fourth.
+Set all four inconsistently and Miles aborts with a validation error.
+
+## Where every flag goes
+
+Use this map when reading any launch script:
+
+| Argument group | Concerns |
+|---|---|
+| [`MODEL_ARGS`](argument-groups.md#model-args) | Architecture constants (layers, hidden size, rotary base, ...) |
+| [`CKPT_ARGS`](argument-groups.md#ckpt-args) | Filesystem paths for the actor / reference / save directory |
+| [`ROLLOUT_ARGS`](argument-groups.md#rollout-args) | Prompt dataset, batch knobs, sampling parameters, reward type |
+| [`EVAL_ARGS`](argument-groups.md#eval-args) | Eval dataset, cadence, sampling overrides for evaluation |
+| [`PERF_ARGS`](argument-groups.md#perf-args) | Parallelism (TP/PP/CP/EP/ETP), recomputation, dynamic batching |
+| [`GRPO_ARGS`](argument-groups.md#grpo-args) | RL algorithm, KL, clipping, entropy bonus, advantage estimator |
+| [`OPTIMIZER_ARGS`](argument-groups.md#optimizer-args) | Learning rate, schedule, weight decay, Adam betas |
+| [`SGLANG_ARGS`](argument-groups.md#sglang-args) | Engine TP, memory fraction, log level, `--sglang-*` passthrough |
+
+## Next
+
+- [Training Backend](usage.md) — Megatron-LM, parallelism, checkpoints, and hooks.
+- [Argument Groups](argument-groups.md) — where each launch-script array belongs.
+- [Training Script Walkthrough](training-script-walkthrough.md) — the launch script
+  group by group, plus execution modes (colocation, sync/async, dynamic sampling, …).
+- [CLI Reference](cli-reference.md) — every flag, grouped and fully catalogued.
diff --git a/docs/user-guide/customization.md b/docs/user-guide/customization.md
new file mode 100644
index 00000000000..715b144cd72
--- /dev/null
+++ b/docs/user-guide/customization.md
@@ -0,0 +1,294 @@
+---
+title: Customization
+description: The 22 plug-points where you can drop in your own Python without forking Miles.
+---
+
+# Customization
+
+Most of Miles's behaviour can be replaced with user-supplied Python by passing a
+`--*-path` flag. This page lists every such hook, the function signature it expects,
+and the default it replaces.
+
+## At a glance
+
+| Stage | Flag | Replaces |
+|---|---|---|
+| **Rollout** | `--rollout-function-path` | The whole rollout loop |
+| | `--custom-generate-function-path` | A single sample's generation |
+| | `--data-source-path` | How prompts are loaded |
+| | `--eval-function-path` | The eval rollout |
+| **Reward** | `--custom-rm-path` | Reward computation |
+| | `--custom-reward-post-process-path` | Reward normalization |
+| **Filtering** | `--dynamic-sampling-filter-path` | Per-group filter (DAPO) |
+| | `--buffer-filter-path` | Buffer dequeue filter |
+| | `--rollout-sample-filter-path` | Per-sample loss filter |
+| | `--rollout-all-samples-process-path` | Inspect all samples post-rollout |
+| | `--rollout-data-postprocess-path` | Mutate samples post-logprob |
+| **Training** | `--custom-loss-function-path` | The loss formula |
+| | `--custom-tis-function-path` | Importance sampling correction |
+| | `--custom-pg-loss-reducer-function-path` | Loss reduction (Dr.GRPO) |
+| | `--custom-convert-samples-to-train-data-path` | Sample to tensor batch |
+| **Megatron hooks** | `--custom-megatron-init-path` | After Megatron init |
+| | `--custom-megatron-before-log-prob-hook-path` | Before logprob compute |
+| | `--custom-megatron-before-train-step-hook-path` | Before each train step |
+| **Logging** | `--custom-rollout-log-function-path` | Train-rollout logging |
+| | `--custom-eval-rollout-log-function-path` | Eval-rollout logging |
+| **Routing** | `--miles-router-middleware-paths` | Router middleware |
+| **Model** | `--custom-model-provider-path` | Megatron model factory |
+
+---
+
+## Rollout
+
+### `--rollout-function-path`
+
+Replace the entire rollout function. Use this only for fundamentally different flows
+such as multi-agent co-evolution.
+
+```python
+def generate_rollout(args, rollout_id, data_source, evaluation=False) \
+        -> RolloutFnTrainOutput | RolloutFnEvalOutput:
+    ...
+```
+
+**Default:** `miles.rollout.sglang_rollout.generate_rollout`, or
+`miles.rollout.inference_rollout.inference_rollout_common.InferenceRolloutFn` when
+`enable_experimental_rollout_refactor()` is on.
+
+**Reference:** [`examples/multi_agent/rollout_with_multi_agents.py`](https://github.com/radixark/miles/blob/main/examples/multi_agent/rollout_with_multi_agents.py).
+
+### `--custom-generate-function-path`
+
+Replace just the generation step inside the default rollout. Most tool-use, RAG, and
+multi-turn workflows live here.
+
+```python
+async def custom_generate(args, sample: Sample, sampling_params: dict) -> Sample:
+    ...
+```
+
+**Reference:** [`examples/search-r1/generate_with_search.py`](https://github.com/radixark/miles/blob/main/examples/search-r1/generate_with_search.py).
+
+### `--data-source-path`
+
+```python
+class CustomDataSource(DataSource):
+    def get_samples(self, num_samples) -> list[list[Sample]]: ...
+    def add_samples(self, samples) -> None: ...
+    def save(self, rollout_id) -> None: ...
+    def load(self, rollout_id=None) -> None: ...
+```
+
+**Default:** `miles.rollout.data_source.RolloutDataSourceWithBuffer`.
+
+### `--eval-function-path`
+
+Same signature as `--rollout-function-path`. Defaults to whatever rollout function is
+configured.
+
+---
+
+## Reward
+
+### `--custom-rm-path`
+
+```python
+# Single-sample mode
+async def custom_rm(args, sample: Sample) -> float:
+    ...
+
+# Batched mode (set --group-rm)
+async def batched_custom_rm(args, samples: list[Sample]) -> list[float]:
+    ...
+```
+
+**Built-in `--rm-type` options:** `math`, `dapo`, `deepscaler`, `f1`, `gpqa`,
+`ifbench`, `remote_rm` (with `--rm-url`), `random`.
+
+### `--custom-reward-post-process-path`
+
+Hook to normalize rewards differently from the default GRPO normalization.
+
+---
+
+## Filtering
+
+### `--dynamic-sampling-filter-path`
+
+Per-group filter; runs after scoring, before queueing for training.
+
+```python
+def filter_function(args, samples: list[Sample], **kwargs) -> DynamicFilterOutput:
+    return DynamicFilterOutput(keep=True, reason=None)
+```
+
+**Stock implementation:** `miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std`.
+
+### `--buffer-filter-path`
+
+Pops samples from the rollout buffer at dequeue time. The default is
+`pop_first` in `miles/rollout/data_source.py`.
+
+```python
+def buffer_filter(
+    args,
+    rollout_id: int | None,
+    buffer: list[list[Sample]],
+    num_samples: int,
+) -> list[list[Sample]]:
+    ...
+```
+
+### `--rollout-sample-filter-path`
+
+Per-sample, in-place. Set `s.remove_sample = True` to exclude a sample from the loss
+(advantage normalization still uses it).
+
+The framework passes `data: list[list[Sample]]` — a list of
+`n_samples_per_prompt`-size groups — so iterate the outer list once to reach `Sample`
+objects:
+
+```python
+def filter_function(args, data: list[list[Sample]]) -> None:
+    for group in data:
+        for s in group:
+            if not_good(s):
+                s.remove_sample = True
+```
+
+### `--rollout-all-samples-process-path`
+
+Runs after rollout completes and can see all samples, including filtered ones.
+Useful for logging or analysis.
+
+### `--rollout-data-postprocess-path`
+
+Runs after log probabilities have been computed but before training. Useful for
+updating loss masks based on per-token logprobs.
+
+---
+
+## Training
+
+### `--custom-loss-function-path`
+
+Replace the GRPO/PPO loss. Requires `--loss-type custom_loss`. Useful for novel
+objectives or multi-objective work.
+
+### `--custom-tis-function-path`
+
+Importance sampling correction for off-policy training when train and inference
+diverge.
+
+**Reference:** [`examples/train_infer_mismatch_helper/mis.py`](https://github.com/radixark/miles/blob/main/examples/train_infer_mismatch_helper/mis.py).
+
+### `--custom-pg-loss-reducer-function-path`
+
+```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 case: Dr.GRPO divides by a constant instead of effective token count.
+**Reference:** [`examples/DrGRPO/custom_reducer.py`](https://github.com/radixark/miles/blob/main/examples/DrGRPO/custom_reducer.py).
+
+### `--custom-convert-samples-to-train-data-path`
+
+```python
+def convert_samples_to_train_data(args, samples) -> dict:
+    return {
+        "tokens":           [...],
+        "response_lengths": [...],
+        "rewards":          [...],
+        "raw_reward":       [...],
+        "truncated":        [...],
+        "sample_indices":   [...],
+        "loss_masks":       [...],
+        # optional
+        "round_number":            [...],
+        "rollout_log_probs":       [...],
+        "rollout_routed_experts":  [...],
+        "metadata":                [...],
+        "multimodal_train_inputs": [...],
+        "teacher_log_probs":       [...],
+    }
+```
+
+---
+
+## Megatron hooks
+
+| Flag | Signature |
+|---|---|
+| `--custom-megatron-init-path` | `def custom_init(args) -> None` |
+| `--custom-megatron-before-log-prob-hook-path` | `def custom_hook(args, model, store_prefix) -> None` |
+| `--custom-megatron-before-train-step-hook-path` | `def custom_hook(args, rollout_id, step_id, model, optimizer, opt_param_scheduler) -> None` |
+
+These give per-step access to the live Megatron model and optimizer, useful for
+custom probes, weight clipping, or surgical interventions.
+
+---
+
+## Logging
+
+```python
+# Training rollouts
+def log_rollout_data(rollout_id, args, samples, rollout_extra_metrics, rollout_time) -> bool:
+    ...
+
+# Eval rollouts
+def log_eval_rollout_data(rollout_id, args, data, extra_metrics) -> bool:
+    ...
+```
+
+Return `True` to suppress Miles's default logging, `False` to layer on top.
+
+---
+
+## Router
+
+### `--miles-router-middleware-paths`
+
+Inject middleware into the router for request and response transformation,
+caching, or custom routing.
+
+---
+
+## Model
+
+### `--custom-model-provider-path`
+
+Replace Megatron's default model factory.
+
+```python
+def custom_model_provider(
+    pre_process: bool,
+    post_process: bool,
+    vp_stage: int | None = None,
+) -> GPTModel:
+    ...
+```
+
+---
+
+## Worked example
+
+A custom rollout plus a custom reward in one launch script:
+
+```bash
+ROLLOUT_ARGS+=(
+   --custom-generate-function-path my_pkg.search_rollout.generate
+   --custom-rm-path                my_pkg.rewards.f1_with_grounding
+   --metadata-key metadata
+   --rollout-max-response-len 4096
+)
+```
+
+That is the entire delta from the stock GRPO recipe, with no source changes to Miles.
+
+→ Next: [Server arguments reference](cli-reference.md)
diff --git a/docs/user-guide/fully-async.md b/docs/user-guide/fully-async.md
new file mode 100644
index 00000000000..8c615b3c207
--- /dev/null
+++ b/docs/user-guide/fully-async.md
@@ -0,0 +1,104 @@
+---
+title: Fully Async Rollout
+description: How fully async rollout decouples generation from training, when to use it, and which flags enable it.
+---
+
+# Fully Async Rollout
+
+Fully async rollout splits Miles into two concurrent loops:
+
+1. A background rollout worker keeps SGLang generation in flight and pushes completed
+   samples into a queue.
+2. The trainer drains the queue, runs optimizer steps, and syncs updated weights back
+   to rollout engines.
+
+When rollout and training take similar time, per-iteration wall time moves from
+`rollout_time + train_time` toward `max(rollout_time, train_time)`.
+
+## When to use it
+
+| Use fully async when | Stay synchronous when |
+|---|---|
+| Rollout is a large part of wall time | Debugging a new recipe |
+| The run is long enough to amortize queue warm-up | You need the strictest possible on-policy cadence |
+| SGLang engines can keep many requests in flight | Queue depth stays at zero even after tuning concurrency |
+| You can tolerate slightly older samples in exchange for throughput | You are validating loss math or reward plumbing |
+
+The mode is especially useful for long-context math, tool-use, and agentic workloads
+where generation dominates the iteration.
+
+## Enable it
+
+Switch the entrypoint from `train.py` to `train_async.py` and provide a rollout
+function that owns the background worker:
+
+```diff
+- python3 train.py ...
++ python3 train_async.py ...
++   --rollout-function-path fully_async_rollout.generate_rollout_fully_async
+```
+
+Everything else belongs in the same [argument groups](argument-groups.md) as a
+synchronous run.
+
+## Queue model
+
+```mermaid
+sequenceDiagram
+    participant T as Trainer
+    participant Q as Rollout queue
+    participant W as Async worker
+    participant S as SGLang engines
+
+    par Producer
+        loop continuously
+            W->>S: generate(prompt)
+            S-->>W: response
+            W->>Q: enqueue sample
+        end
+    and Consumer
+        loop each trainer iteration
+            T->>Q: drain batch
+            T->>T: optimizer step
+            T->>S: sync weights
+        end
+    end
+```
+
+The queue is the contract. If it stays populated, the trainer does not wait for
+generation. If it is empty, rollout is still the bottleneck and async cannot hide it.
+
+## Tuning knobs
+
+| Knob | What it changes |
+|---|---|
+| `--rollout-batch-size` | Target amount of work the async producer keeps in flight |
+| `--sglang-server-concurrency` | Per-engine request concurrency |
+| `--global-batch-size` | Number of samples the trainer drains per step |
+| `--num-steps-per-rollout` | Number of optimizer steps per queue drain cycle |
+| `--max-weight-staleness` | When the rollout engine's weight version lags the trainer's by more than this, the worker recycles the stale group instead of feeding it to the loss |
+
+The reference worker caps its output queue at 1000 groups, so if training is slower
+than rollout the producer eventually blocks rather than growing the queue without
+bound. If the queue stays at zero, rollout is the bottleneck — scale rollout capacity
+or lower per-sample generation cost.
+
+## What to monitor
+
+The reference worker logs progress to stdout, not wandb. Useful lines to grep for:
+
+```text
+Global worker queue size: 
+Staleness stats: recycled=, avg_staleness=, max_staleness=
+Warning: No progress for s. Queue size: , Collected: /
+```
+
+Treat large staleness windows as a training-quality signal, not just a performance
+signal. Fast [P2P weight transfer](../advanced/p2p-weight-transfer.md) keeps the
+rollout engines closer to the latest actor weights so fewer groups get recycled by
+`--max-weight-staleness`.
+
+## Example implementation
+
+For a complete Qwen3 launch script and worker implementation, see the
+[Fully Async Rollout example](../examples/fully-async.md).
diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md
new file mode 100644
index 00000000000..d537ea714d4
--- /dev/null
+++ b/docs/user-guide/index.md
@@ -0,0 +1,26 @@
+---
+title: User Guide
+description: Concepts, launch script walkthrough, customization hooks, and a complete CLI reference.
+---
+
+# User Guide
+
+| Page | What it covers |
+|---|---|
+| [Core Concepts](concepts.md) | The four objects in the training loop and the four-knob invariant. |
+| [Argument Groups](argument-groups.md) | Where `MODEL_ARGS`, `PERF_ARGS`, `GRPO_ARGS`, and the other launch-script arrays belong. |
+| [Training Backend](usage.md) | Megatron-LM as the training backend — parallelism, checkpoints, and hooks. |
+| [Training Script Walkthrough](training-script-walkthrough.md) | The eight `XXX_ARGS` arrays in a launch script, plus the execution modes (sync/async, colocation, dynamic sampling, partial rollout, BF16+FP8). |
+| [Monitoring & Logging](monitoring.md) | wandb, structured logs, per-source breakdowns, profiling, router metrics. |
+| [Customization](customization.md) | The 22 `--*-path` plug-points for custom Python — rollout, reward, filters, loss, hooks. |
+| [Rollout Endpoints](rollout-endpoints.md) | The `/generate` endpoint and the OpenAI chat endpoint for agentic sessions. |
+| [Fully Async Rollout](fully-async.md) | Queue-backed rollout production, tuning knobs, and when to use `train_async.py`. |
+| [Agentic Chat Templates](agentic-chat-template.md) | Verifying and fixing the chat template so multi-turn rollout stays append-only. |
+| [CLI Reference](cli-reference.md) | Every flag Miles accepts, grouped by subsystem. |
+
+## Which pages do I actually need?
+
+- **Training my first job** — read [Core Concepts](concepts.md), then [Training Script Walkthrough](training-script-walkthrough.md).
+- **Tuning a running job** — [Training Script Walkthrough](training-script-walkthrough.md) in depth + [CLI Reference](cli-reference.md).
+- **Plugging in a custom reward / rollout / filter** — skim [Core Concepts](concepts.md) for vocabulary, then go to [Customization](customization.md).
+- **Contributor onboarding** — read top to bottom.
diff --git a/docs/user-guide/monitoring.md b/docs/user-guide/monitoring.md
new file mode 100644
index 00000000000..4c6aebb9038
--- /dev/null
+++ b/docs/user-guide/monitoring.md
@@ -0,0 +1,120 @@
+---
+title: Monitoring & Logging
+description: wandb, structured logs, profiling, and what to look at when something looks off.
+---
+
+# Monitoring & Logging
+
+Miles emits per-rollout metrics to stdout and (optionally) Weights & Biases. SGLang and
+Ray write their own logs to their default directories.
+
+## What gets logged by default
+
+Each rollout iteration emits a structured row to stdout (illustrative shape — exact
+fields depend on backend and config):
+
+```text
+[trainer] iter 12/3000 | loss=0.412 reward=0.61 kl=0.018
+                      | rollout=18.4s train=22.1s p2p=2.1s  (total 42.6s)
+                      | grad_norm=0.93 lr=1.0e-06
+```
+
+When `--use-wandb` is set, metrics also go to wandb under the `train/`, `rollout/`,
+and `perf/` namespaces (see `miles/utils/wandb_utils.py`).
+
+## Enabling wandb
+
+```bash
+ray job submit --address=auto -- \
+  python3 train.py ... \
+    --use-wandb \
+    --wandb-project miles \
+    --wandb-group qwen3-30b-grpo
+```
+
+Available flags: `--use-wandb`, `--wandb-project`, `--wandb-group`. `WANDB_API_KEY`
+should be supplied via Ray's `env_vars` rather than baked into the launch script.
+
+## What to watch
+
+| Signal | Healthy pattern | Red flag |
+|---|---|---|
+| `loss` | Slow decay over hundreds of iterations | Spike → crash within an iteration |
+| `raw_reward` | Trending up, with healthy variance | Saturates near a single value (collapse) |
+| `kl_loss` | Bounded, drifts up over time | Sudden jump (policy diverged from ref) — only logged when `--use-kl-loss` |
+| `train_rollout_logprob_abs_diff` | Stable and small (≪ 1.0) | Climbing without bound → train/inference precision drift |
+| `entropy_loss` | Slowly decreasing | Falls to ~0 too fast (mode collapse) |
+| `grad_norm` | < `clip_grad` (1.0 by default) | Repeatedly hitting clip threshold |
+| `rollout_time` / `train_time` | Roughly balanced | One ≫ other → resource imbalance |
+| `train/pg_clipfrac` | < 0.2 | > 0.5 means policy is moving fast → drop LR |
+
+Panel names follow what `loss.py` and the rollout logger emit; Miles's wandb metrics
+live under `train/`, `rollout/`, `perf/`, `multi_turn/`, `passrate/` namespaces.
+
+## Custom loggers
+
+Replace the default rollout logger with your own to push to internal systems:
+
+```python
+def my_log(rollout_id, args, samples, extra, rollout_time) -> bool:
+    statsd.gauge("miles.reward", mean([s.reward for s in samples]))
+    return False   # also keep default logging
+```
+
+```bash
+--custom-rollout-log-function-path my_pkg.logging.my_log
+```
+
+## Profiling
+
+| Tool | When |
+|---|---|
+| `nvidia-smi dmon -s u` | Quick sanity check on GPU utilization |
+| `nsys profile` | Deep CUDA-level profiling |
+| `py-spy dump --pid ` | Find Python-side stalls |
+| `ray timeline` | Inspect Ray task scheduling |
+
+### Built-in PyTorch profiler
+
+The PyTorch profiler is wired into Miles via `miles/utils/profile_utils.py`. Flags
+differ by backend:
+
+**Megatron** — choose which sub-loop to profile:
+
+```bash
+--profile-target train_overall    # or train_actor, train_log_probs (multi-arg)
+```
+
+**FSDP** — additionally exposes the standard FSDPArgs window:
+
+```bash
+--use-pytorch-profiler
+--profile-step-start 10
+--profile-step-end 12
+--memory-snapshot-path snapshot.pickle
+--tensorboard-dir /data/tb-run-42
+```
+
+Open the trace in `chrome://tracing` or [Perfetto](https://ui.perfetto.dev/).
+
+## Where the log files live
+
+| Source | Path |
+|---|---|
+| Trainer stdout | wherever you redirected `ray job submit` (or Ray dashboard) |
+| Ray workers | `~/.ray/session_latest/logs/` |
+| wandb local cache | `wandb/run-/files/` |
+| FSDP profiler / memory snapshot | `--tensorboard-dir`, `--memory-snapshot-path` |
+
+## Router endpoints
+
+The router exposes a small FastAPI surface used internally by Miles:
+
+| Endpoint | Method | What |
+|---|---|---|
+| `/add_worker` | POST | Register an SGLang engine |
+| `/list_workers` | GET | List registered workers |
+| any other path | GET / POST / PUT / DELETE | Proxied to a selected SGLang worker — e.g. `/generate`, `/v1/chat/completions`, `/health`. |
+
+Middleware plugins (e.g. radix-tree caching) can mount additional routes — see
+`miles/router/middleware_hub/`.
diff --git a/docs/user-guide/rollout-endpoints.md b/docs/user-guide/rollout-endpoints.md
new file mode 100644
index 00000000000..ac5fb14a185
--- /dev/null
+++ b/docs/user-guide/rollout-endpoints.md
@@ -0,0 +1,258 @@
+---
+title: Rollout Endpoints
+description: How Miles talks to SGLang. The /generate endpoint and the OpenAI-format /v1/chat/completions endpoint.
+---
+
+# Rollout Endpoints
+
+Miles supports two ways for a custom rollout function to talk to SGLang. The
+`/generate` endpoint is the most direct interface; you control tokenization. The
+OpenAI-format `/v1/chat/completions` endpoint is router-session aware and fits
+agent loops with multi-turn dialogue.
+
+| | `/generate` | OpenAI `/v1/chat/completions` |
+|---|---|---|
+| Input | Text or tokens | `messages` list |
+| Tokenization | Your code | SGLang |
+| Session state | Stateless | Router sessions (base_url includes `/sessions/`) |
+| Best for | Tool use with custom token handling, benchmarking | Agentic loops, multi-turn dialogue |
+| Reference generator | `generate_hub/single_turn.py`, `generate_hub/multi_turn.py` | `generate_hub/agentic_tool_call.py` |
+
+Both entry points are wired up through `--custom-generate-function-path`.
+
+---
+
+## The `/generate` endpoint
+
+### What `generate_hub` is
+
+`miles/rollout/generate_hub/` ships reusable generate functions that conform to the
+refactored rollout interface (`GenerateFnInput` / `GenerateFnOutput`). They compose
+with custom agents, tool use, or multi-turn logic.
+
+Key modules:
+
+| Path | Purpose |
+|---|---|
+| `miles/rollout/base_types.py` | `GenerateFnInput` / `GenerateFnOutput` |
+| `miles/rollout/inference_rollout/inference_rollout_common.py` | Builds a `GenerateState` and calls the generate function |
+| `MILES_EXPERIMENTAL_ROLLOUT_REFACTOR=1` | Enables the new path (see `examples/openai_format/*.sh`) |
+
+### Generate function basics
+
+The runtime contract:
+
+1. The rollout engine passes a `GenerateFnInput` containing:
+    - `state`: tokenizer, processor, args, sampling defaults.
+    - `sample`: the prompt, current tokens, response, status.
+    - `sampling_params`: `max_new_tokens`, `temperature`, `top_p`, etc.
+2. Your function:
+    - Builds a request from the prompt.
+    - Executes it against SGLang.
+    - Updates the `Sample` with tokens, logprobs, loss mask, status.
+
+Minimal skeleton:
+
+```python
+from miles.rollout.base_types import GenerateFnInput, GenerateFnOutput
+from miles.utils.types import Sample
+
+
+async def generate(input: GenerateFnInput) -> GenerateFnOutput:
+    args = input.args
+    sample = input.sample
+    sampling_params = input.sampling_params
+
+    # 1) build request from prompt and sampling params
+    # 2) call backend
+    # 3) update sample.tokens, sample.response, sample.rollout_log_probs,
+    #    sample.loss_mask, sample.status
+
+    return GenerateFnOutput(samples=sample)
+
+
+def _add_arguments(parser):
+    parser.add_argument("--your-arg", type=str)
+
+
+generate.add_arguments = _add_arguments
+```
+
+
+
+**Custom CLI flags.** `generate.add_arguments = _add_arguments` registers extra CLI flags. They are
+parsed into `input.args` and available everywhere in your generator.
+
+
+
+Helpers:
+
+- `compute_prompt_ids_from_sample` and `compute_request_payload` from
+  `miles/rollout/generate_utils/generate_endpoint_utils.py` build `/generate` requests.
+- For multi-sample outputs, set `--generate-multi-samples` and return a list.
+
+### Reference generators
+
+- **`single_turn.py`**: single-turn generation via `/generate`. Text or multimodal prompts.
+- **`multi_turn.py`**: multi-turn tool calling via `/generate`. Adds CLI flags
+  `--generate-max-turns`, `--generate-tool-specs-path`, `--generate-tool-call-parser`,
+  `--generate-execute-tool-function-path`, `--generate-multi-samples`.
+- **`benchmarkers.py`**: forces random output sequence length for benchmarking.
+
+### Radix-tree middleware (full TITO for `/generate`)
+
+For token-in / token-out caching on `/generate`, enable the radix-tree middleware.
+It is independent of the OpenAI session middleware and only affects the `/generate`
+and `/retrieve_from_text` routes.
+
+What it does:
+
+- Caches token ids and logprobs by prompt text in a radix tree.
+- Lets `/generate` requests include `input_tokens`, skipping re-tokenization.
+- Enables `update_sample_from_response` to fetch tokens via `/retrieve_from_text`
+  during training.
+
+Enable it:
+
+```bash
+--miles-router-middleware-paths miles.router.middleware_hub.radix_tree_middleware.RadixTreeMiddleware
+```
+
+Make sure `--sglang-router-ip` and `--sglang-router-port` point at the router so
+`/retrieve_from_text` is reachable during rollout.
+
+---
+
+## The OpenAI chat endpoint
+
+### Minimal `run_agent`
+
+A `run_agent` receives a session-scoped `base_url`. Send OpenAI-format chat requests
+to `base_url/v1/chat/completions` and pass the `messages` list as the prompt.
+
+```python
+from miles.utils.http_utils import post
+
+
+async def run_agent(base_url: str, prompt, request_kwargs: dict | None = None) -> None:
+    payload = {"model": "default", "messages": prompt, **(request_kwargs or {})}
+    await post(f"{base_url}/v1/chat/completions", payload)
+```
+
+
+
+**What's already handled.**
+- `base_url` already includes `/sessions/`. Don't append it manually.
+- `request_kwargs` already contains sampling defaults from
+  `agentic_tool_call.build_chat_request_kwargs`.
+- `max_new_tokens` from Miles's rollout params is mapped to OpenAI's `max_tokens`
+  before the request is sent.
+- For structured parsing, use SGLang's `ChatCompletionRequest`-compatible
+  format, a superset of OpenAI plus SGLang extras.
+
+
+
+### OpenAI chat messages
+
+Standard OpenAI format:
+
+```json
+{
+  "model": "default",
+  "messages": [
+    {"role": "system", "content": "You are a concise assistant."},
+    {"role": "user",   "content": "Answer with one word: 2+2?"}
+  ],
+  "logprobs": true,
+  "return_prompt_token_ids": true
+}
+```
+
+
+
+**Leave `logprob_start_len` alone.** `logprobs=True` and `return_prompt_token_ids=True` are set by default; they
+enable TITO. Do **not** set `logprob_start_len=0`. That forces SGLang to compute
+logprobs for every prompt token, destroys the prefix cache, and hurts
+performance. `return_prompt_token_ids=True` returns prompt token ids at zero
+cost with full caching.
+
+
+
+### Quickstart
+
+Generator entry point:
+
+- `miles/rollout/generate_hub/agentic_tool_call.py`: OpenAI-format agent loop via
+  router sessions.
+
+Examples:
+
+- [`examples/openai_format/dapo_math.py`](https://github.com/radixark/miles/blob/main/examples/openai_format/dapo_math.py):
+  single-turn OpenAI-format agent (DAPO math).
+- [`examples/openai_format/run-qwen3-4B.sh`](https://github.com/radixark/miles/blob/main/examples/openai_format/run-qwen3-4B.sh): launcher.
+
+Wire-up:
+
+```bash
+CUSTOM_ARGS=(
+   --custom-generate-function-path miles.rollout.generate_hub.agentic_tool_call.generate
+   --custom-agent-function-path    examples.openai_format.dapo_math.run_agent
+)
+```
+
+
+
+**Don't apply chat template.** For OpenAI format, do **not** pass `--apply-chat-template`. The prompt must
+remain a `messages` list. SGLang handles templating server-side.
+
+
+
+### Customizing the wrapper
+
+[`agentic_tool_call.generate`](https://github.com/radixark/miles/blob/main/miles/rollout/generate_hub/agentic_tool_call.py)
+is a thin wrapper around the custom agent. It:
+
+1. Creates a session on MilesRouter and builds a session-scoped `base_url`.
+2. Calls the custom agent (from `--custom-agent-function-path`) to send one or more
+   chat requests.
+3. Collects session records via `OpenAIEndpointTracer`.
+4. Converts records into `Sample` objects via `compute_samples_from_openai_records`.
+
+For broader customization beyond the OpenAI wrapper, see the `/generate` path above.
+
+### TITO (token-in / token-out)
+
+TITO needs two things from every SGLang response:
+
+1. **Prompt token ids**: extracted from `response.choices[0].prompt_token_ids`.
+   Returned when the request sets `return_prompt_token_ids=True`.
+2. **Output token ids and logprobs**: from `response.choices[0].logprobs.content[*]`
+   (`token_id`, `logprob`). Returned when `logprobs=True`.
+
+By default, `build_chat_request_kwargs` sets both flags. The session middleware
+forwards raw `messages` to SGLang, which tokenizes the prompt and returns the
+response. `_compute_sample_from_openai_record` in
+[`openai_endpoint_utils.py`](https://github.com/radixark/miles/blob/main/miles/rollout/generate_utils/openai_endpoint_utils.py)
+extracts prompt and output ids from the response and concatenates them into
+`sample.tokens`. You don't need to provide `input_ids` yourself.
+
+Multi-turn samples can be saved within a single session, but tokens are **not**
+inherited across turns. Each request is tokenized independently.
+
+### Common pitfalls
+
+| Pitfall | Fix |
+|---|---|
+| Missing logprobs / prompt token ids | Ensure `logprobs=True` and `return_prompt_token_ids=True`. |
+| Prefix cache hit rate drops to 0 | Remove `logprob_start_len=0`. |
+| Tokenization drift across turns | Expected. Tokens aren't inherited. |
+| Custom agent hitting the wrong URL | `base_url` already has `/sessions/`. Don't append it. |
+
+---
+
+## Next
+
+- [Customization](customization.md): the full catalogue of `--*-path` hooks.
+- [Agentic Chat Templates](agentic-chat-template.md): verifying that a template is
+  append-only across turns.
+- [Multi-agent example](../examples/multi-agent.md): full agentic walkthrough.
diff --git a/docs/user-guide/training-script-walkthrough.md b/docs/user-guide/training-script-walkthrough.md
new file mode 100644
index 00000000000..87782a0cd55
--- /dev/null
+++ b/docs/user-guide/training-script-walkthrough.md
@@ -0,0 +1,472 @@
+---
+title: Training Script Walkthrough
+description: An annotated tour through every argument group in a Miles launch script, plus the feature modes you turn on when a recipe isn't enough.
+---
+
+# Training Script Walkthrough
+
+A Miles launch script is plain bash — a sequence of `XXX_ARGS=( ... )` arrays handed
+to `train.py` or `train_async.py`. This page walks through each group and then covers
+the execution modes you turn on beyond the default recipe.
+
+`scripts/run-glm4-9B.sh` is the reference script; other recipes follow the same shape.
+
+## The eight argument groups
+
+Every launch script assembles eight bash arrays, passes them as CLI flags, and hands
+off to `train.py`:
+
+| Array | Governs |
+|---|---|
+| [`MODEL_ARGS`](argument-groups.md#model-args) | Architecture constants (layers, hidden size, rotary base, ...) |
+| [`CKPT_ARGS`](argument-groups.md#ckpt-args) | Filesystem paths for the actor / reference / save directory |
+| [`ROLLOUT_ARGS`](argument-groups.md#rollout-args) | Prompt dataset, batch knobs, sampling parameters, reward type |
+| [`EVAL_ARGS`](argument-groups.md#eval-args) | Eval dataset, cadence, sampling overrides for evaluation |
+| [`PERF_ARGS`](argument-groups.md#perf-args) | Parallelism (TP/PP/CP/EP/ETP), recomputation, dynamic batching |
+| [`GRPO_ARGS`](argument-groups.md#grpo-args) | RL algorithm, KL, clipping, entropy bonus, advantage estimator |
+| [`OPTIMIZER_ARGS`](argument-groups.md#optimizer-args) | Learning rate, schedule, weight decay, Adam betas |
+| [`SGLANG_ARGS`](argument-groups.md#sglang-args) | Engine TP, memory fraction, log level, `--sglang-*` passthrough |
+
+---
+
+## MODEL_ARGS — architecture constants
+
+Megatron needs the model architecture hardcoded at launch because it cannot introspect
+a HuggingFace checkpoint. Miles therefore sources a matching bash file from
+`scripts/models/.sh`:
+
+```bash
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+source "${SCRIPT_DIR}/models/glm4-9B.sh"
+```
+
+The sourced file sets `MODEL_ARGS=(--num-layers ... --hidden-size ... --rotary-base ...)`.
+
+
+
+**Architecture parameters are not self-validating.** Two checkpoints from the same family can ship different `--rotary-base`, vocab
+padding, or normalization epsilon. Diff the `config.json` against the file in
+`scripts/models/` before you run, and override anything that drifts:
+
+```bash
+source "${SCRIPT_DIR}/models/glm4-9B.sh"
+MODEL_ARGS+=(--rotary-base 10000)
+```
+
+
+
+## CKPT_ARGS — paths
+
+The three roles — actor, frozen reference, HuggingFace directory — are defined in [Core Concepts](concepts.md#the-four-objects). Here they map to four flags:
+
+```bash
+CKPT_ARGS=(
+   --hf-checkpoint /root/GLM-Z1-9B-0414          # tokenizer, config, SGLang init
+   --ref-load      /root/GLM-Z1-9B-0414_torch_dist   # frozen ref (KL anchor)
+   --load          /root/GLM-Z1-9B-0414_miles/   # actor resume point
+   --save          /root/GLM-Z1-9B-0414_miles/   # where checkpoints are written
+   --save-interval 20                            # rollouts between writes
+)
+```
+
+`--load` and `--save` usually point at the same directory so the run is
+restart-idempotent: the trainer reads whatever was last written there. When `--load` is
+empty or missing `latest_checkpointed_iteration.txt`, the actor is warm-started from
+`--ref-load` instead.
+
+## ROLLOUT_ARGS — where data comes from and how much flows
+
+Every Miles iteration alternates between **sampling** (rollout) and **consumption**
+(training). Four knobs govern the balance.
+
+**Sampling side**
+
+- `--rollout-batch-size` — prompts drawn per rollout.
+- `--n-samples-per-prompt` — responses generated per prompt (used as the GRPO group).
+
+Their product is the total sample count produced each rollout.
+
+**Consumption side**
+
+- `--global-batch-size` — samples used per optimizer step.
+- `--num-steps-per-rollout` — optimizer steps per rollout. Leave at `1` for strict
+  on-policy behaviour; raise it for off-policy reuse of rollout batches.
+
+Their product is the total sample count consumed each rollout.
+
+These two products must be equal — that's the [four-knob invariant](concepts.md#the-four-knob-invariant). Set three sides; Miles fills in the fourth. Set all four and Miles validates the equation — inconsistent values abort early.
+
+**Outer loop**
+
+- `--num-rollout` — total sample/train iterations.
+
+A compact example:
+
+```bash
+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
+
+   # the four-knob relationship
+   --num-rollout 3000
+   --rollout-batch-size 16
+   --n-samples-per-prompt 8
+   --num-steps-per-rollout 1
+   --global-batch-size 128
+
+   --rollout-max-response-len 8192
+   --rollout-temperature 1
+
+   --balance-data                  # equalize tokens across DP ranks
+)
+```
+
+
+
+**Optimizer step vs. weight sync.** `--num-steps-per-rollout` counts calls to `optimizer.step()`, not the weight
+handshake between trainer and SGLang. The latter happens exactly once per rollout,
+regardless of how many optimizer steps fired in between.
+
+
+
+## EVAL_ARGS — a strict subset of rollout
+
+Evaluation reuses the rollout machinery but lets you override sampling behaviour so
+that eval is deterministic and comparable across runs.
+
+```bash
+EVAL_ARGS=(
+   --eval-interval 5                                # rollouts between eval runs
+   --eval-prompt-data aime /root/aime-2024/aime-2024.jsonl
+   --n-samples-per-eval-prompt 16                   # eval group size
+   --eval-max-response-len 16384                    # longer than train rollout
+   --eval-top-p 1                                   # disable nucleus
+)
+```
+
+Flags not overridden here inherit their values from `ROLLOUT_ARGS`.
+
+## PERF_ARGS — parallelism and memory
+
+This group controls how the model is sharded across GPUs and how much activation
+memory is recomputed vs. stored. Miles forwards Megatron's parallelism flags
+untouched and adds two of its own.
+
+```bash
+PERF_ARGS=(
+   --tensor-model-parallel-size 2
+   --sequence-parallel
+   --pipeline-model-parallel-size 1
+   --context-parallel-size 2
+   --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 4608
+)
+```
+
+A few notes worth committing to memory:
+
+- **Always pair `--tensor-model-parallel-size > 1` with `--sequence-parallel`.** The
+  sequence-parallel pass reclaims the activation memory TP leaves behind.
+- **`--use-dynamic-batch-size` overrides `--micro-batch-size`.** When dynamic batching
+  is active, Miles packs variable-length samples into the closest fit under
+  `--max-tokens-per-gpu`. A sample whose length already exceeds the cap takes a whole
+  micro-batch by itself — that batch still exceeds the cap and may OOM, so keep
+  `--rollout-max-response-len` ≤ `--max-tokens-per-gpu`.
+- **Under context parallel, the budget is shared.** A CP group of size `N` jointly
+  processes up to `N × max_tokens_per_gpu` tokens per micro-batch. Size CP before
+  tuning `max-tokens-per-gpu`.
+- **Loss correctness is preserved.** Miles packs with proper attention masks and
+  per-sample / per-token loss reductions — dynamic batching never changes the gradient
+  value, only the throughput.
+
+## GRPO_ARGS — the RL objective
+
+`GRPO_ARGS` is the only group that carries RL semantics. The defaults encode vanilla
+GRPO with a DAPO-style asymmetric clip.
+
+```bash
+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
+)
+```
+
+A few design choices become visible here:
+
+- **KL as a monitor vs. KL in the loss.** `--use-kl-loss` always loads the reference
+  model and computes the divergence — its weight in the loss is controlled separately
+  by `--kl-loss-coef`. Setting the coefficient to `0.0` turns KL into a pure
+  observability signal, which is often what you want for early experiments.
+- **`--advantage-estimator` covers more than GRPO.** `gspo`, `reinforce_plus_plus`,
+  `reinforce_plus_plus_baseline`, `ppo`, and `on_policy_distillation` are all
+  drop-in replacements.
+- **Per-sample vs. per-token loss.** The default reduction is per-sample mean:
+  `mean(sum(sample_i) / len(sample_i))`. Add `--calculate-per-token-loss` to switch to
+  `sum(sum(sample_i)) / sum(len(sample_i))` — the correct choice for SFT-style loss
+  or when you want length-proportional weighting.
+- **`--use-tis` is the numerical safety belt.** Switch it on when rollout and trainer
+  operate at different precisions or when you explicitly want off-policy reuse. See
+  the R3 deep dive in [Rollout Routing Replay (R3)](../advanced/miles-router.md).
+
+## OPTIMIZER_ARGS — nothing surprising
+
+Post-training is unusually sensitive to optimizer settings: the model is already in a
+good basin and large updates destabilize it.
+
+```bash
+OPTIMIZER_ARGS=(
+   --optimizer adam
+   --lr 1e-6
+   --lr-decay-style constant
+   --weight-decay 0.1
+   --adam-beta1 0.9
+   --adam-beta2 0.98
+)
+```
+
+A rule of thumb: start at `lr = 1e-6` with a constant schedule. If the loss plateaus
+early, investigate the reward signal before raising the learning rate — in most cases
+the reward pipeline collapsed (same score for every sample) rather than the optimizer
+stalling.
+
+## SGLANG_ARGS — passthrough to the rollout engine
+
+The only Miles-owned flag here is `--rollout-num-gpus-per-engine`, which corresponds
+loosely to SGLang's `tp_size`. Everything else prefixed with `--sglang-` is forwarded
+verbatim.
+
+```bash
+SGLANG_ARGS=(
+   --rollout-num-gpus-per-engine 2
+)
+```
+
+Common additions that don't ship in the canonical recipe:
+
+| Flag | When to add it |
+|---|---|
+| `--sglang-mem-fraction-static 0.7` | Colocated mode; Megatron needs headroom after init. |
+| `--sglang-context-length 32768` | Rollout max length exceeds the model's `config.json`. |
+| `--sglang-enable-ep-moe` | MoE models. |
+| `--sglang-enable-dp-attention` | Long prompts on MoE. |
+| `--sglang-log-level INFO` | Debugging. |
+
+Miles multiplexes across multiple SGLang engines through a router. When
+DP-attention is off, the effective `dp_size` is derived from
+`rollout-num-gpus / rollout-num-gpus-per-engine`.
+
+---
+
+# Execution modes
+
+The eight argument groups describe **what** you're training. The next set of sections
+describe **how** the training runs — the execution modes that flip Miles from its
+default one-rollout-then-one-train cadence into something more interesting.
+
+## Synchronous vs. asynchronous rollout
+
+In the default cadence the trainer blocks on rollout: `generate()` returns, then
+`train_step()` fires, then the next rollout kicks off. Every iteration's wall-clock
+time is the sum of the two.
+
+Async rollout turns the cadence into two concurrent loops. A background worker keeps
+`--rollout-batch-size` generations in flight at all times and pushes completed samples
+into a queue; the trainer drains the queue, steps, and syncs weights. Per-iteration
+wall time drops to roughly `max(rollout_time, train_time)`.
+
+Enable it with two changes to the launch script:
+
+```diff
+- python3 train.py ...
++ python3 train_async.py ...
++   --rollout-function-path fully_async_rollout.generate_rollout_fully_async
+```
+
+| Mode | Per-iteration latency | Throughput | When to use |
+|---|---|---|---|
+| Sync *(default)* | Lower | Lower overall | Strict on-policy, debugging |
+| Async | Higher | Up to 2× | Rollout-bound jobs, long runs |
+
+See the [Fully Async Rollout example](../examples/fully-async.md) for the full
+walkthrough including the worker implementation.
+
+## Colocation: share GPUs or don't
+
+In the default disaggregated layout, training and inference claim separate GPUs
+through Ray. The simplest form is:
+
+```bash
+ray job submit ... -- python3 train.py \
+   --actor-num-nodes 1 \
+   --actor-num-gpus-per-node 4 \
+   --rollout-num-gpus 4 \
+   ...
+```
+
+Eight GPUs, half training, half inference, running concurrently.
+
+When GPUs are scarce, you can **colocate** actor and rollout onto the same set of
+devices. Miles time-slices between them:
+
+```bash
+ray job submit ... -- python3 train.py \
+   --actor-num-nodes 1 \
+   --actor-num-gpus-per-node 8 \
+   --colocate \
+   ...
+```
+
+`--rollout-num-gpus` is ignored under `--colocate`; the two phases always share the
+entire allocation.
+
+
+
+**Leave SGLang some headroom.** Megatron reserves VRAM during initialization and only releases it once the first
+offload completes. If SGLang is sized to the full device capacity, the init
+collision produces an OOM before training ever starts. Drop
+`--sglang-mem-fraction-static` to **0.8** (further if memory is still tight).
+
+
+
+
+
+**What `--colocate` flips on.** Setting `--colocate` also enables (unless you've set them explicitly):
+
+- `--offload-train` — train state offloads to CPU between phases
+- `--offload-rollout` — rollout state offloads to CPU between phases
+- `--sglang-disable-piecewise-cuda-graph` — avoids NVLS OOM in colocate mode
+
+
+
+## Dynamic sampling (DAPO-style filtering)
+
+A common failure mode of GRPO is *reward homogeneity*: every trajectory in a group gets
+the same score, the advantage is zero, and the gradient goes flat. DAPO addresses this
+by **oversampling** and throwing away groups that lack reward variance.
+
+Miles exposes the same capability through two flags:
+
+```bash
+ROLLOUT_ARGS+=(
+   --over-sampling-batch-size 64
+   --dynamic-sampling-filter-path \
+     miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std
+)
+```
+
+When `--over-sampling-batch-size` exceeds `--rollout-batch-size`, Miles draws the
+larger batch, runs generation and reward scoring asynchronously, and applies the
+filter function as results arrive. Groups that survive the filter enter the training
+queue; groups that fail are discarded.
+
+The shipped filter checks that reward standard deviation is strictly positive:
+
+```python
+def check_reward_nonzero_std(args, samples: list[Sample], **kwargs):
+    rewards = [sample.reward for sample in samples]
+    return torch.tensor(rewards, dtype=torch.float).std() > 0.0
+```
+
+The system watches `remaining_batch_size` — the count of usable groups still needed to
+fill the rollout. Whenever the filter discards enough samples that the count drops
+below `--rollout-batch-size`, Miles automatically kicks off another oversampling wave.
+The mechanism is self-healing: a strict filter just means more oversampling rounds,
+not a stuck trainer.
+
+## Partial rollout: reclaim aborted work
+
+Dynamic sampling implies that *some* in-flight generations will be abandoned. Without
+care, the compute invested in those half-finished trajectories is lost.
+
+`--partial-rollout` flips on a buffer that retains partial samples and resumes their
+generation during the next rollout. The buffer dequeue policy is itself pluggable via
+`--buffer-filter-path`; the default is a first-in-first-out pop:
+
+```python
+def pop_first(args, rollout_id, buffer, num_samples):
+    num_to_pop = min(len(buffer), num_samples)
+    samples = buffer[:num_to_pop]
+    del buffer[:num_to_pop]
+    return samples
+```
+
+Each entry in the buffer carries its original `sample.metadata` (including the
+rollout ID that first launched it), which is usually enough to reason about staleness
+if you want a stricter eviction policy.
+
+
+
+**Partial rollout and off-policy correction.** Samples that re-enter the trainer from the buffer were generated under an older
+policy than the one currently being trained. If you enable `--partial-rollout`
+together with aggressive reuse (`--num-steps-per-rollout > 1`), pair it with
+`--use-tis` to keep the gradient well-behaved.
+
+
+
+## BF16 training with FP8 inference
+
+The simplest way to exploit FP8 on Hopper-class hardware is to leave the trainer in
+BF16 and serve FP8 weights to SGLang. Miles supports this path without any code
+changes — only checkpoint pointers differ.
+
+Download the FP8 weights alongside the BF16 originals:
+
+```bash
+hf download Qwen/Qwen3-4B-FP8 --local-dir /root/Qwen3-4B-FP8
+```
+
+Then swap the `--hf-checkpoint` pointer to the FP8 directory while leaving the
+Megatron side unchanged:
+
+```bash
+CKPT_ARGS=(
+   # SGLang picks up the FP8 weights from here
+   --hf-checkpoint /root/Qwen3-4B-FP8
+
+   # Megatron still trains from the BF16-derived torch_dist checkpoint
+   --ref-load      /root/Qwen3-4B_torch_dist
+)
+```
+
+The trainer continues to operate in BF16; SGLang will cast the BF16 weights it
+receives via P2P sync into FP8 before running them. This is the lowest-friction way
+to take the inference-side speedup, but it does introduce the precision mismatch
+between rollout and trainer that R3 and TIS were designed to absorb.
+
+
+
+**Do not point `--ref-load` at the FP8 directory.** The reference model must remain BF16. Replacing it with FP8 weights changes the
+KL anchor silently and makes the loss curve incomparable to earlier runs.
+
+
+
+For end-to-end FP8 (trainer and inference at bit-identical precision), see
+[Low Precision RL](../advanced/fp8-low-precision.md). For INT4 quant-aware
+training, see [INT4 QAT](../advanced/int4-qat.md).
+
+---
+
+## Next
+
+- [Configuration](cli-reference.md) — the same material organized as a flag-by-flag
+  reference.
+- [Server Arguments](cli-reference.md) — the complete CLI surface.
+- [Customization](customization.md) — the twenty-plus Python extension points.
+- [Training Backends](usage.md) — Megatron vs FSDP and each one's plumbing.
diff --git a/docs/user-guide/usage.md b/docs/user-guide/usage.md
new file mode 100644
index 00000000000..cbbd7c848ad
--- /dev/null
+++ b/docs/user-guide/usage.md
@@ -0,0 +1,185 @@
+---
+title: Training Backend
+description: Megatron-LM as the training backend — parameters, parallelism, checkpoints, and hooks.
+---
+
+# Training Backend
+
+Miles decouples the **training backend** (how the model is sharded, checkpointed, and
+stepped) from the **inference backend** (SGLang). The production training backend is
+**Megatron-LM**.
+
+## Megatron-LM
+
+### Parameter discovery
+
+Miles imports Megatron's entire argument surface at launch through Megatron's parser:
+
+```python
+from megatron.training.arguments import parse_args
+```
+
+That means every Megatron flag in your installed checkpoint works without Miles having
+to re-declare it — `--kv-channels`, `--rotary-base`, `--moe-grouped-gemm`, and so on.
+
+Export the Megatron source directory before you launch:
+
+```bash
+export PYTHONPATH=/root/Megatron-LM
+```
+
+Miles adds its own arguments by threading an `extra_args_provider` into Megatron's
+`parse_args` (see `get_miles_extra_args_provider` in `miles/utils/arguments.py`),
+so Miles flags and Megatron flags share a single CLI surface.
+
+### Architecture specs
+
+Most models work with stock `--num-layers / --hidden-size / ...` flags. For models that
+need a custom module (Qwen3-Next's Gated-Delta-Net, Qwen3.5's attention-output gate,
+GLM5's expert routing), Miles ships a plugin spec:
+
+```bash
+MODEL_ARGS=(
+   --spec "miles_plugins.models.qwen3_5" "get_qwen3_5_spec"
+   ...
+)
+```
+
+The spec function replaces specific Megatron submodules with the HF implementation
+without patching Megatron itself. Details:
+[Backends Beyond Megatron](../advanced/architecture-support.md).
+
+### Parallelism compatibility
+
+Megatron exposes five useful parallel dimensions, but you can't combine them in
+arbitrary ways — only a subset of TP × PP × CP × EP × ETP combinations is actually
+supported, and some legal combinations are slower than the recipe baseline. Start from
+the model recipe's tested combination, then change one dimension at a time.
+
+| Dimension | Use it for | Compatibility notes |
+|---|---|---|
+| TP | Shard dense matrix multiplications inside each layer | When `--tensor-model-parallel-size` is set above 1, also pass `--sequence-parallel` unless the recipe says otherwise. |
+| PP | Split layers across pipeline stages | Combines with TP and CP, but changes micro-batch scheduling and checkpoint layout. |
+| CP | Split long sequences across ranks | Useful for long context; size token budgets as `CP x max_tokens_per_gpu`. |
+| EP | Distribute MoE experts across ranks | MoE-only. Keep trainer EP and SGLang EP as separate choices. |
+| ETP | Tensor-parallelize expert MLPs | MoE-only. Use it only when the recipe enables it or when EP alone cannot fit the experts. |
+
+Do not assume TP, CP, EP, and ETP can all be raised independently for a new model — the
+exact set of supported combinations depends on the Megatron Core kernels and model spec
+being used. The [Argument Groups](argument-groups.md#perf-args) page lists the flags
+that belong in `PERF_ARGS`.
+
+### Checkpoint format
+
+Miles uses Megatron's `torch_dist` format — `.distcp` files that are
+parallelism-agnostic, so you can change TP / PP / EP without re-converting.
+
+A checkpoint directory looks like:
+
+```text
+/ckpt/
+├── latest_checkpointed_iteration.txt
+├── iter_0000100/
+│   ├── _0_0.distcp
+│   └── ...
+├── iter_0000200/
+└── ...
+```
+
+Always pass the **parent** directory to `--load`, not a specific iteration. The
+loader reads `latest_checkpointed_iteration.txt` to pick the step.
+
+### HuggingFace → torch_dist
+
+```bash
+source scripts/models/.sh
+PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \
+   ${MODEL_ARGS[@]} \
+   --hf-checkpoint /root/ \
+   --save          /root/_torch_dist
+```
+
+For models larger than a single node, drive the converter with
+`torchrun --nnodes= --nproc-per-node=8 ...`. Each recipe page lists the exact
+command.
+
+### Hooks
+
+Three extension points override Megatron behaviour without forking:
+
+| Flag | Runs |
+|---|---|
+| `--custom-megatron-init-path` | After Megatron initialization |
+| `--custom-megatron-before-log-prob-hook-path` | Before every log-probability computation |
+| `--custom-megatron-before-train-step-hook-path` | Before every training step |
+
+Typical use cases: mixing in an auxiliary loss, instrumenting per-step metrics, or
+clipping weights surgically. See [Customization](customization.md#megatron-hooks).
+
+---
+
+## SGLang as the inference engine
+
+SGLang is the fixed inference engine regardless of training backend. Three pieces of
+configuration matter:
+
+**HuggingFace pointer.** SGLang boots from `--hf-checkpoint`. Before the first training
+step, Miles syncs the actor's weights from the trainer — so the checkpoint at that path
+does **not** need to be current. The tokenizer and the `config.json`-derived context
+length are the only things SGLang cares about at init time.
+
+**Context length override.** SGLang reads max context from the model's `config.json`.
+To serve beyond that during training, set `--sglang-context-length`.
+
+**Colocation memory.** Under `--colocate`, Megatron reserves VRAM during init before
+handing off to SGLang. Drop `--sglang-mem-fraction-static` to **0.8** (or lower) so
+both can coexist.
+
+### Passthrough convention
+
+Any flag accepted by `python -m sglang.launch_server` is accepted by Miles prefixed
+with `--sglang-`:
+
+```bash
+--sglang-enable-ep-moe
+--sglang-enable-dp-attention
+--sglang-dp-size 8
+--sglang-mem-fraction-static 0.7
+--sglang-log-level INFO
+```
+
+Conversely, two flags are **set by Miles** rather than the user:
+
+- `--tp-size` ← `--rollout-num-gpus-per-engine`
+- `--model-path` ← `--hf-checkpoint`
+
+The integration lives at
+[`miles/backends/sglang_utils/arguments.py`](https://github.com/radixark/miles/blob/main/miles/backends/sglang_utils/arguments.py).
+
+### Router
+
+A router sits in front of the SGLang workers. Pass router-side flags with the
+`--router-` prefix:
+
+```bash
+--router-balance-abs-threshold 0   # force uniform distribution (lowers prefix-cache hit rate)
+```
+
+If `--sglang-router-ip` and `--sglang-router-port` are set, Miles treats them as an
+**external** router and skips starting its own — engines register via `/add_worker`
+at startup.
+
+---
+
+## Further reading
+
+- [Core concepts](concepts.md) — the four objects that make up any Miles job.
+- [Training script walkthrough](training-script-walkthrough.md) — the launch script,
+  argument group by argument group.
+- [Fully Async Rollout](fully-async.md) — decouple generation from trainer steps with
+  a queue-backed rollout worker.
+- [Configuration](cli-reference.md) — the flag taxonomy and defaults.
+- [Backends beyond Megatron](../advanced/architecture-support.md) — wrapping new
+  architectures without patching Megatron core.
+- [Experimental Features → FSDP backend](../developer/experimental-features.md#fsdp-backend)
+  — experimental PyTorch FSDP2 backend for fast iteration on small dense models.

From 4a48ebf56e6563ec7564601f705dc69502a68e2b Mon Sep 17 00:00:00 2001
From: Zhichen Zeng 
Date: Mon, 11 May 2026 17:46:18 -0700
Subject: [PATCH 0044/1130] doc [5/n]: add docs/developer (#1101)

Co-authored-by: Zhichenzzz 
Co-authored-by: Shi-Dong 
---
 docs/developer/architecture.md          | 140 ++++++++++++++++++++++++
 docs/developer/contributing.md          | 111 +++++++++++++++++++
 docs/developer/debug.md                 | 135 +++++++++++++++++++++++
 docs/developer/experimental-features.md |  81 ++++++++++++++
 docs/developer/index.md                 |  51 +++++++++
 docs/developer/migration.md             | 113 +++++++++++++++++++
 6 files changed, 631 insertions(+)
 create mode 100644 docs/developer/architecture.md
 create mode 100644 docs/developer/contributing.md
 create mode 100644 docs/developer/debug.md
 create mode 100644 docs/developer/experimental-features.md
 create mode 100644 docs/developer/index.md
 create mode 100644 docs/developer/migration.md

diff --git a/docs/developer/architecture.md b/docs/developer/architecture.md
new file mode 100644
index 00000000000..c470b4b6529
--- /dev/null
+++ b/docs/developer/architecture.md
@@ -0,0 +1,140 @@
+---
+title: Architecture Overview
+description: The 30-minute tour of how Miles is organized internally.
+---
+
+# Architecture Overview
+
+A reading guide before you start patching.
+
+## The processes
+
+A Miles run is three kinds of processes wrapped in a Ray cluster:
+
+```mermaid
+flowchart TB
+    subgraph Ray cluster
+        subgraph "Trainer (1+ Megatron group)"
+            T1[Actor rank 0]
+            T2[Actor rank 1]
+            T3[Actor rank ...]
+        end
+        subgraph "Rollout (N SGLang servers + Miles Router)"
+            R1[SGLang server 1]
+            R2[SGLang server 2]
+            MR[Miles Router]
+            R1 -. health, route .- MR
+            R2 -. health, route .- MR
+        end
+        D[Data Source
RolloutDataSourceWithBuffer] + T1 <-- weight sync --> R1 + T1 <-- weight sync --> R2 + D --> MR + MR --> D + end +``` + +* **Trainer ranks** — Megatron processes that load `torch_dist` checkpoints and run the + RL loop. +* **SGLang servers** — independent HTTP services that produce rollouts. +* **Miles Router** — FastAPI proxy that distributes rollout requests, preserves + metadata (R3), and enforces health checks. +* **Data Source** — Python object owned by the trainer; reads prompt JSONL and acts as + a buffer between rollout and training. + +## The package layout + +```text +miles/ +├── backends/ +│ ├── megatron_utils/ # fp32 markers, optimizer offload helpers, weight sync +│ ├── sglang_utils/ # SGLang glue +│ ├── training_utils/ # loss / GRPO / PPO / GSPO / REINFORCE++ plumbing +│ └── experimental/ +│ └── fsdp_utils/ # FSDP-flavoured trainer (in progress) +├── ray/ # Ray actors + rollout driver +├── rollout/ +│ ├── sglang_rollout.py # default rollout function +│ ├── data_source.py # buffer + JSONL loader +│ ├── filter_hub/ # built-in filters +│ └── inference_rollout/# experimental refactor +├── router/ # FastAPI proxy + middleware engine (router.py) +└── utils/ # async, types, IO, distributed helpers, arguments.py +``` + +`train.py` and `train_async.py` are the two entry points. They're thin: ~200 lines +each. Most logic lives in the modules above. + +## A request's life + +For a single GRPO iteration: + +```mermaid +sequenceDiagram + participant T as Trainer + participant DS as DataSource + participant MR as MilesRouter + participant SG as SGLang + participant RM as RewardFn + + T->>DS: get_samples(N) + DS-->>T: prompts + T->>MR: generate(prompts) + MR->>SG: dispatch + SG-->>MR: responses + meta_info + MR-->>T: samples + T->>RM: score(samples) + RM-->>T: rewards + T->>T: GRPO loss / step + T->>SG: weight_sync(p2p) +``` + +This is the sync path. Async (`train_async.py` + `--rollout-function-path +fully_async_rollout.generate_rollout_fully_async`) breaks the request from the trainer +loop and uses a continuously-running worker. + +## Where common changes go + +| You want to … | Edit | +|---|---| +| Add a new RL algorithm | `miles/backends/training_utils/loss.py` + enum in `miles/utils/arguments.py` | +| Add a new built-in reward type | `miles/rollout/sglang_rollout.py` (rm dispatch) | +| Add a new built-in filter | `miles/rollout/filter_hub/` | +| Wrap a new model architecture | `miles_plugins/models/.py` + `mbridge` | +| Add a new flag | `miles/utils/arguments.py` | +| Change weight sync | `miles/backends/megatron_utils/update_weight/` and `miles/utils/distributed_utils.py` | +| Change rollout buffer | `miles/rollout/data_source.py` | + +## Extension points (the right way) + +The trainer is plug-in-friendly. Most extensions don't need a code change inside Miles — +just pass a `--something-path my_pkg.thing`. See [Customization](../user-guide/customization.md) +for the full list. + +If you find yourself patching the trainer to make something work, that's a sign we're +missing a hook. Open an issue. + +## Tests + +```text +tests/ +├── fast/ # fast, no GPU +├── ci/ # CI-gated suite +├── e2e/ # end-to-end (spins up Ray + SGLang) +└── utils/ # shared test helpers +``` + +Run `pytest tests/fast` for a quick check; run `tests/e2e` before landing anything that +touches the train loop. + +## Where to look first when reading the code + +If you have 30 minutes and want to understand Miles end-to-end: + +1. `train.py` — the loop, top-to-bottom. +2. `miles/rollout/sglang_rollout.py:generate_rollout` — how prompts become samples. +3. `miles/backends/training_utils/loss.py` — the loss and advantage computation. +4. `miles/router/router.py` — the FastAPI proxy. +5. `miles/utils/distributed_utils.py` — weight sync. + +That's the spine. Everything else hangs off it. diff --git a/docs/developer/contributing.md b/docs/developer/contributing.md new file mode 100644 index 00000000000..864652d6b1e --- /dev/null +++ b/docs/developer/contributing.md @@ -0,0 +1,111 @@ +--- +title: Contributing +description: PR conventions, code layout, and how reviews work. +--- + +# Contributing + +Miles is open source under the LICENSE file in the repo. We accept community +contributions of every size — bug reports, doc fixes, new model recipes, full features. + +## Repository layout + +```text +miles/ +├── train.py # synchronous entry point +├── train_async.py # fully-async entry point +├── miles/ # the package itself +│ ├── backends/ # Megatron, SGLang, training (loss / GRPO / PPO / ...), experimental FSDP +│ ├── ray/ # Ray actors + rollout driver +│ ├── rollout/ # rollout / data source / filters +│ ├── router/ # Miles Router (FastAPI proxy) +│ └── utils/ # async, types, IO, distributed helpers, arguments.py +├── miles_plugins/ # opt-in plugins +│ ├── mbridge/ # mbridge integration +│ ├── megatron_bridge/ # megatron-bridge integration +│ └── models/ # model wrappers (GLM4, GLM5, Qwen3.5, Qwen3-Next, ...) +├── examples/ # the recipes documented in this site +├── scripts/ +│ ├── models/ # per-model MODEL_ARGS bash files +│ ├── amd/ # AMD-specific launchers +│ ├── tools/ # script-side utilities (e.g. verify_chat_template.py) +│ ├── run-*.sh # bash launch scripts +│ └── run_*.py # Python launch scripts +├── tools/ # ckpt converters, calibrators, debug tools +├── tests/ # pytest suite (fast / ci / e2e / utils) +├── docker/ # Dockerfiles +└── docs/ # the source of this site +``` + +## Local dev loop + +```bash +# Inside the radixark/miles container +cd /root/miles +git remote add me git@github.com:/miles.git +git checkout -b feat/awesome + +# Edit code +pip install -e . --no-deps # editable install picks up changes +pytest tests/test_my_thing.py -xvs # run the relevant tests + +git add -p && git commit -m "feat: short imperative description" +git push me feat/awesome +gh pr create --title "..." --body "..." +``` + +## Style + +* **Python:** [`ruff`](https://docs.astral.sh/ruff/) for linting, `black` for + formatting (line length 100). `pre-commit` is wired up. +* **Type hints:** use them. We're strict about new code. +* **Docstrings:** Google style for public functions; one-liners for internal. +* **Imports:** `isort` ordering, no relative imports across packages. + +## PR checklist + +Before you click "Ready for review": + +- [ ] `pre-commit run --all-files` passes. +- [ ] You added or updated tests for new behaviour. +- [ ] You ran `pytest -x` and it's green. +- [ ] If you touched the launch flags, `python3 train.py --help` still parses. +- [ ] If you added a public flag, it appears in [Server Arguments](../user-guide/cli-reference.md). +- [ ] If you added a new example, you wrote a real walkthrough (use + [examples/index](../examples/index.md) as the structural template). + +## Commit messages + +We follow the conventional-commits style: + +``` +feat(rollout): add partial-rollout buffer +fix(megatron): correct fp32 marker on Qwen3.5 A_log +docs: clarify FP8 rationale for MoE +test: cover R3 routing replay +``` + +The first line is < 70 chars. Body explains *why*, not *what* — the diff already +shows what. + +## Issue triage + +We label issues with: + +| Label | Meaning | +|---|---| +| `good first issue` | Self-contained, no system knowledge needed | +| `help wanted` | We'd love community PRs | +| `bug` | Reproducible breakage | +| `enhancement` | Feature request | +| `discussion` | Design conversation, not yet a task | +| `needs-repro` | We can't reproduce — please provide a minimal example | + +If you're new, sort by `good first issue`. Comment to claim before you start so we +don't double-up. + +## Where to ask questions + +* **Quick questions:** Miles channel of the [SGLang Slack](https://slack.sglang.ai). +* **Design discussions:** open a GitHub Discussion or an Issue with `discussion` label. +* **Security:** email security@radixark.ai (do not open a public issue). diff --git a/docs/developer/debug.md b/docs/developer/debug.md new file mode 100644 index 00000000000..dd9b1a41b83 --- /dev/null +++ b/docs/developer/debug.md @@ -0,0 +1,135 @@ +--- +title: Debugging +description: Aligning precision, separate train/rollout debugging, common kernel pitfalls. +--- + +# Debugging + +When something is wrong with a Miles run, the question is almost always: **rollout or +training?** Once you've isolated which side is misbehaving, the rest is a normal +debugging session. + +## Aligning precision + +The single most common class of bug. Walk through these checks before anything else. + +### First training step + +#### Is the rollout coherent? + +If the very first rollout is gibberish: + +* **Parameters didn't load.** Check Megatron's logs — there should be a clear + "loaded checkpoint from ..." line. If absent, fix `--load` / `--ref-load`. +* **Parameter mapping is wrong.** When `pp_size > 1`, the second-stage layer IDs are a + common offset bug. Save all parameters in `load_weights` of the corresponding model + in SGLang and verify they match the loaded checkpoint exactly. +* **SGLang dropped buffers.** Some special buffers can be released during the parameter + release process. Check if they're re-loaded correctly after weight sync. +* **Pretrained vs. instruct.** Try the instruct version of the same architecture. If + that works, your pretrained model + chat template combination is wrong. + +#### Are `log_probs` and `ref_log_probs` exactly equal in step 1? + +They should be: at step 1 the actor and reference are the same weights. If KL > 0: + +* **Non-deterministic kernels.** Some Transformer Engine versions need + `--attention-backend flash` to force deterministic Flash Attention under context + parallel. +* **Slightly off values (KL < 1e-4).** Acceptable — kernel-level numerical jitter. +* **Large values (KL > 1).** Configuration error. Re-check parallelism and precision. +* **Slightly elevated logp on instruct (~0.8 per token).** Almost always a chat-template + mismatch — your prompts don't match the format the model was trained on. Run the + [chat template verifier](../user-guide/agentic-chat-template.md). + +#### Is `grad_norm` reasonable? + +Step 1 with `num_steps_per_rollout=1` should produce a tiny gradient. If it doesn't: + +* **Megatron / TE bug.** MoE in particular requires `--moe-permute-fusion`. Check + release notes for known fixes. +* **Reward signal is broken.** Confirm rewards are computed from the right key (often a + `--label-key` typo). + +### Second training step + +If step 2 OOMs in colocate mode, Megatron's offload→reload cycle is hitting the SGLang +side. Lower `--sglang-mem-fraction-static` to 0.7 (or 0.6). + +## Separate train/rollout debugging + +Miles ships flags to isolate one side: + +| Flag | What | +|---|---| +| `--debug-rollout-only` | Don't init Megatron — only spin up SGLang. | +| `--debug-train-only` | Don't init SGLang — only spin up Megatron. | +| `--save-debug-rollout-data /path/data_{rollout_id}.pt` | Pickle every rollout to disk. | +| `--load-debug-rollout-data /path/data_{rollout_id}.pt` | Replay rollouts from disk; auto-sets `--debug-train-only`. | + +A typical workflow: + +1. Run with `--debug-rollout-only --save-debug-rollout-data` to capture a few + well-formed rollouts. +2. Switch to `--debug-train-only --load-debug-rollout-data` to iterate on training + changes (parallelism, optimizer, custom loss) with **fixed inputs**. Removes + rollout randomness from your bisect. + +This is the single most useful pattern in the Miles workflow. Use it. + +## Determinism for bisecting + +When you need to A/B test a code change, bit-wise reproducibility is your friend. See +the [Reproducibility recipe](../examples/reproducibility.md) for the exact flag set +and env vars. The 25% throughput cost is worth it during development. + +## Common kernel pitfalls + +| Symptom | Likely culprit | +|---|---| +| Garbled rollout, parameters loaded fine | Chat template mismatch, or buffer drop in SGLang | +| KL ≠ 0 in step 1 | Non-det fused attention; force `--attention-backend flash` | +| MoE training collapses after ~50 steps | R3 not on, or routing not preserved | +| Gradient NaN/Inf | Bad chat template, or activation overflow in FP8 | +| `illegal memory access` in SGLang | OOM in disguise — lower `--sglang-mem-fraction-static` | +| `JSONDecodeError` from inductor | Cache corrupt; set `TORCHINDUCTOR_FORCE_DISABLE_CACHES=1` | +| NCCL hang during weight sync | `NCCL_TIMEOUT=900`; check `NCCL_DEBUG=INFO` | + +## Reading logs + +Where to look: + +| Component | Path | +|---|---| +| Trainer stdout | wherever you redirected `ray job submit` | +| SGLang | `/tmp/sglang/*.log` (or `--sglang-log-dir`) | +| Ray workers | `~/.ray/session_latest/logs/worker-*.{out,err}` | +| NCCL | `NCCL_DEBUG=INFO NCCL_DEBUG_FILE=/tmp/nccl_%h_%p.log` | + +`grep` for these signal phrases: + +* `Loaded checkpoint from` — Megatron load OK. +* `weight_sync` — P2P / broadcast events. +* `Continuous async rollout worker started` — async worker alive. +* `WorkerCrashed` / `RaySystemError` — ranks died. +* `forward_logp_max_diff` — FP8 train/inference alignment. + +## When all else fails + +* Drop to a tiny model (Qwen2.5-0.5B) on a known-good recipe (Reproducibility) to + isolate framework vs. model. +* `git bisect` between a known-working commit and HEAD. Determinism makes this + trustworthy. +* Open a GitHub issue with: launch script, `pip freeze`, the first 200 lines of + trainer stdout, and a short description. + +## Useful debugging knobs + +```bash +--debug-determinism # log per-step hashes +--debug-weight-sync 1 # verbose P2P transfer logs +--debug-rollout-print-every 1 # dump every Nth rollout to stdout +NCCL_DEBUG=INFO +NCCL_DEBUG_SUBSYS=COLL,P2P +RAY_DEDUP_LOGS=0 +``` diff --git a/docs/developer/experimental-features.md b/docs/developer/experimental-features.md new file mode 100644 index 00000000000..7fa9dc1c218 --- /dev/null +++ b/docs/developer/experimental-features.md @@ -0,0 +1,81 @@ +--- +title: Experimental Features +description: Backends and features that exist in tree but are not production-ready — opt-in at your own risk. +--- + +# Experimental Features + +These features live in the Miles tree but are **not** production-ready. They typically +have rough edges, missing parallelism, or known bugs against current dependency +versions. Use them when you want to iterate quickly or co-develop a feature, not for +the long-running training jobs you'd publish results from. + +## FSDP backend + +A PyTorch FSDP2 training backend lives at `miles/backends/fsdp_utils/`. +It trades maximum throughput for **zero conversion overhead**: there is no +`torch_dist` step, Miles reads architecture information from the HuggingFace +`config.json`, and weights load directly via `AutoModelForCausalLM.from_pretrained()`. +The distributed optimizer is built into FSDP, and mixed precision falls out of standard +PyTorch. + + + +**Status.** Experimental. Known bug after SGLang v0.5.10. No TP / PP / CP / EP — runs as +plain FSDP data parallel only. Suitable for fast iteration on small-to-mid dense +models, not for production runs. + + + +### When to reach for it + +- Iterating on a new model architecture and you don't want to write a Megatron spec yet. +- Small-to-mid dense workloads where the parallelism story doesn't matter. +- You want a HuggingFace-native checkpoint at every step with no conversion. + +For large MoE models, multi-rack jobs, or anything where TP / PP / CP / EP matters, +use the production [Megatron-LM backend](../user-guide/usage.md#megatron-lm) instead. + +### Enabling it + +```bash +--train-backend fsdp +``` + +### Flag mapping vs. Megatron + +Most RL-level flags carry over unchanged. Backend-specific differences: + +| Concern | Megatron | FSDP | +|---|---|---| +| Model load | `--load` + architecture args | `--hf-checkpoint` *(single flag, required)* | +| Tensor parallel | `--tensor-model-parallel-size` | Not supported yet | +| Pipeline parallel | `--pipeline-model-parallel-size` | Not supported yet | +| Expert parallel | `--expert-model-parallel-size` | Not supported yet | +| Context parallel | `--context-parallel-size` | Not supported yet | +| Optimizer | `--use-distributed-optimizer` *(forced on by Miles)* | Built-in | +| Gradient checkpoint | `--recompute-granularity / method / num-layers` | `--gradient-checkpointing` *(boolean)* | +| CPU offload | Distributed optimizer | `--fsdp-cpu-offload` | +| CPU backend | *(in distributed optimizer)* | `--fsdp-cpu-backend` | +| Attention backend | Decided by Megatron Core | `--attn-implementation flash_attention_2 / sdpa / eager` | +| Mixed precision | `--fp16` / `--bf16` | `--fp16` *(bf16 inferred)* | +| Extra backend config | — | `--config ` | + +### Quick start + +```bash +# Optional: wandb +export WANDB_API_KEY= + +# Model + data +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B +hf download --repo-type dataset BytedTsinghua-SIA/DAPO-Math-17K --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 + +# Code +git clone https://github.com/radixark/miles.git && cd miles +pip install -e . --no-deps + +# Launch — no conversion step +bash scripts/run-qwen3-4B-fsdp.sh +``` diff --git a/docs/developer/index.md b/docs/developer/index.md new file mode 100644 index 00000000000..8ab006543bb --- /dev/null +++ b/docs/developer/index.md @@ -0,0 +1,51 @@ +--- +title: Developer Guide +description: Architecture, contribution conventions, debugging, and migration notes. +--- + +# Developer Guide + +You're here because you want to change Miles, not just use it. This section is the +short tour for new contributors. + + + + + + PR conventions, code layout, how reviews work. + + + + + + Aligning precision, separate train/rollout debugging, common kernel pitfalls. + + + + + + Sync → async loop, breaking flag changes between releases. + + + + + + The 30-minute tour of how Miles is organized internally. + + + + + + Opt-in backends and features (FSDP, …) that aren't production-ready yet. + + + + + +## TL;DR for first-time contributors + +1. Pick something small from `good first issue` on [GitHub](https://github.com/radixark/miles/issues). +2. Run the [Reproducibility recipe](../examples/reproducibility.md) so you can be sure + "I changed X and it broke" actually means that. +3. Use `--debug-train-only` or `--debug-rollout-only` to scope your changes. +4. Open a PR. We'll review within ~48h. diff --git a/docs/developer/migration.md b/docs/developer/migration.md new file mode 100644 index 00000000000..2cd43ceb6fc --- /dev/null +++ b/docs/developer/migration.md @@ -0,0 +1,113 @@ +--- +title: Migration Guide +description: Sync → async loop, breaking flag changes between releases. +--- + +# Migration Guide + +This page tracks breaking changes between Miles releases and how to update your code or +launch scripts. + +## Train loop: sync → async + +### What changed + +`train.py`, `train_async.py`, and `RayTrainGroup` now use Python `async`/`await` +instead of `ray.get()`-style sync calls. + +### Why + +Async code is more expressive when you need to overlap work. A concrete example: in +fault tolerance we want to capture Ray actor errors and retry `actor_model.train` +while still letting `critic_model.train` proceed. That's hard to write with synchronous +`ray.get`. + +### How to migrate mechanically + +#### 1. Make `train` async + +```diff +- def train(args): ++ async def train(args): + ... + + if __name__ == "__main__": +- train(parse_args()) ++ asyncio.run(train(parse_args())) +``` + +#### 2. Drop `async_` prefixes; `ray.get(x)` → `await x` + +```diff +- ray.get(group.async_init(...)) ++ await group.init(...) + +- ray.get(group.async_train(...)) ++ await group.train(...) + +- group.save_model(...) ++ await group.save_model(...) + +- group.update_weights() ++ await group.update_weights() + +- ray.get(rollout_manager.generate.remote(rollout_id)) ++ await rollout_manager.generate.remote(rollout_id) +``` + +Same pattern applies to `offload`, `onload`, `clear_memory`, `connect`, +`set_rollout_manager`. + +#### 3. Dispatch handles → eager tasks + +```diff +- handle = critic.async_train(...) +- ray.get(actor.async_train(...)) +- ray.get(handle) + ++ task = await eager_create_task(critic.train(...)) ++ await actor.train(...) ++ await task +``` + +#### 4. `create_training_models` is now async + +```diff +- actor, critic = create_training_models(args, pgs, rollout_manager) ++ actor, critic = await create_training_models(args, pgs, rollout_manager) +``` + +## Other recent breakages + +### v0.0.8 → v0.0.9 + +* `--rollout-router-ip` renamed to `--sglang-router-ip`. +* `--rm-path` renamed to `--custom-rm-path` (legacy alias warns then exits in v0.1.0). +* `--num-rollout` is now required (previously defaulted to 1000). + +### v0.0.7 → v0.0.8 + +* Default `--save-interval` changed from `10` → `100`. Set explicitly if you relied on + the old cadence. +* `--apply-chat-template` is no longer implicit when `prompt` is a list; pass it + explicitly. + +## Versioning policy + +* **Patch (0.1.x):** bug fixes only, no flag changes. +* **Minor (0.x.0):** new features; old flags marked deprecated, alias kept for one minor. +* **Major (x.0.0):** breaking changes; deprecated aliases removed. + +We log a clear deprecation warning at startup whenever you use a soon-to-be-removed +flag — pay attention to those, they're the cheapest way to migrate ahead of time. + +## Compatibility matrix + +| Miles | SGLang | Megatron-LM | Notes | +|---|---|---|---| +| 0.1.0 | 0.4.4+ | mcore-r0.10 | Current stable | +| 0.0.9 | 0.4.3 | mcore-r0.9 | | +| 0.0.8 | 0.4.2 | mcore-r0.8 | Last sync-loop release | + +If you're pinned to a specific SGLang or Megatron version, use the matching Miles +release rather than mixing. From b774a4ecee07a6bc35f7413bf0d22f09d65e1b82 Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Mon, 11 May 2026 17:46:34 -0700 Subject: [PATCH 0045/1130] doc [6/n]: add docs/models (#1102) Co-authored-by: Zhichenzzz Co-authored-by: Shi-Dong --- docs/models/deepseek/deepseek-v4-flash.md | 189 +++++++++++++++ docs/models/deepseek/deepseek-v4-pro.md | 131 +++++++++++ docs/models/deepseek/deepseek.md | 246 ++++++++++++++++++++ docs/models/deepseek/index.md | 45 ++++ docs/models/gpt-oss/gpt-oss.md | 122 ++++++++++ docs/models/index.md | 41 ++++ docs/models/kimi/index.md | 34 +++ docs/models/kimi/kimi-k2.5.md | 7 + docs/models/kimi/kimi-k2.md | 174 ++++++++++++++ docs/models/kimi/moonlight.md | 114 +++++++++ docs/models/mimo/mimo.md | 123 ++++++++++ docs/models/nemotron/index.md | 38 +++ docs/models/nemotron/nemotron-3-nano-moe.md | 150 ++++++++++++ docs/models/nemotron/nemotron-3-nano.md | 131 +++++++++++ docs/models/nemotron/nemotron-3-super.md | 158 +++++++++++++ docs/models/qwen/index.md | 39 ++++ docs/models/qwen/qwen3-5-moe.md | 110 +++++++++ docs/models/qwen/qwen3-5.md | 112 +++++++++ docs/models/qwen/qwen3-6-moe.md | 157 +++++++++++++ docs/models/qwen/qwen3-6.md | 130 +++++++++++ docs/models/qwen/qwen3-moe.md | 144 ++++++++++++ docs/models/qwen/qwen3-next.md | 125 ++++++++++ docs/models/qwen/qwen3.md | 129 ++++++++++ 23 files changed, 2649 insertions(+) create mode 100644 docs/models/deepseek/deepseek-v4-flash.md create mode 100644 docs/models/deepseek/deepseek-v4-pro.md create mode 100644 docs/models/deepseek/deepseek.md create mode 100644 docs/models/deepseek/index.md create mode 100644 docs/models/gpt-oss/gpt-oss.md create mode 100644 docs/models/index.md create mode 100644 docs/models/kimi/index.md create mode 100644 docs/models/kimi/kimi-k2.5.md create mode 100644 docs/models/kimi/kimi-k2.md create mode 100644 docs/models/kimi/moonlight.md create mode 100644 docs/models/mimo/mimo.md create mode 100644 docs/models/nemotron/index.md create mode 100644 docs/models/nemotron/nemotron-3-nano-moe.md create mode 100644 docs/models/nemotron/nemotron-3-nano.md create mode 100644 docs/models/nemotron/nemotron-3-super.md create mode 100644 docs/models/qwen/index.md create mode 100644 docs/models/qwen/qwen3-5-moe.md create mode 100644 docs/models/qwen/qwen3-5.md create mode 100644 docs/models/qwen/qwen3-6-moe.md create mode 100644 docs/models/qwen/qwen3-6.md create mode 100644 docs/models/qwen/qwen3-moe.md create mode 100644 docs/models/qwen/qwen3-next.md create mode 100644 docs/models/qwen/qwen3.md diff --git a/docs/models/deepseek/deepseek-v4-flash.md b/docs/models/deepseek/deepseek-v4-flash.md new file mode 100644 index 00000000000..5e6c77de033 --- /dev/null +++ b/docs/models/deepseek/deepseek-v4-flash.md @@ -0,0 +1,189 @@ +--- +title: DeepSeek-V4 Flash +description: Launch recipe for DeepSeek-V4-Flash (284 B) — FP8 rollout / BF16 train, 8-node H200 (64 GPUs). +--- + +# DeepSeek-V4 Flash + +DeepSeek V4 training tracking issue: [`radixark/miles#1046`](https://github.com/radixark/miles/issues/1046). + +## 1. Model Introduction + +[DeepSeek-V4-Flash](https://huggingface.co/sgl-project/DeepSeek-V4-Flash-FP8) is a 13 B-active / 284 B-total MoE model with a substantially different attention stack from V3/R1. The miles + Megatron-Core (`mcore`) integration is shipped together in the [`radixark/miles#1045`](https://github.com/radixark/miles/pull/1045) and [`radixark/Megatron-LM#28`](https://github.com/radixark/Megatron-LM/pull/28) pull requests, plus the published images `radixark/miles:deepseek-v4` (H200 / B200, cu129 x86) and `radixark/miles:gb300-dev-dskv4` (GB300, cu130 arm64). + +**Key highlights:** + +- **Hybrid Attention (CSA + HCA)**: combines **Compressed Sparse Attention** (light compression) and **Heavily Compressed Attention** (heavy compression) layers — DeepSeek's official V4 name (see [HF model card §Introduction](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash#introduction)). Implementation uses low-rank Q (`q_lora_rank=1024`), single-head latent KV (`head_dim=512`), grouped output projection (8 groups, LoRA rank 1024). A learned topk **indexer** (`index_topk=512`, 64 heads × 128 dim) picks 512 KV per query at runtime, inheriting V3.2's DSA-style design. +- **KV compressors**: 44-element compression schedule `compress_ratios = [0, 0, 4, 128, 4, 128, …, 4, 0]` — first / last few layers are uncompressed (ratio 0), middle layers alternate 4× (CSA) and 128× (HCA). The compressor RoPE has its own base (`compress_rope_theta=160000`), separate from the main attention RoPE. +- **Hyper-connection (HC) routing**: each layer expands hidden state into `hc_mult=4` parallel streams and recombines via sinkhorn-normalized mixing. Pipeline-parallel buffers are 4-D `[s, b, hc_mult, d]` instead of 3-D. +- **YaRN RoPE on main attention**: `rope_theta=10000`, YaRN `factor=16`, `original_max_position_embeddings=65536` → effective context length **1,048,576 tokens (1 M)**. Per-head learnable attention sinks (one scalar per head, added to softmax denominator). +- **FP8 weights with simulated FP8 QAT** on indexer and compressor activations. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| DeepSeek-V4-Flash | 13 B / 284 B | [sgl-project/DeepSeek-V4-Flash-FP8](https://huggingface.co/sgl-project/DeepSeek-V4-Flash-FP8) | + +## 3. Quick start + +### 3.1 One-line launch + +One command runs the full pipeline — dataset download, FP8 → BF16 cast, distributed `torch_dist` conversion, and the training loop: + +```bash +# Pull the image matching your cluster: +# H200 / B200 (cu129 x86) -> radixark/miles:deepseek-v4 +# GB300 (cu130 arm64) -> radixark/miles:gb300-dev-dskv4 +docker pull radixark/miles:deepseek-v4 + +# 8-node Flash run, inside the container +cd /root/miles +python scripts/run_deepseek_v4.py full-train \ + --model-name DeepSeek-V4-Flash-FP8 \ + --num-nodes 8 --num-gpus-per-node 8 +``` + +The `full-train` subcommand chains `prepare-download → prepare-single → prepare-spmd → prepare-cp → train`. Each stage has a sentinel-based skip so you can re-run safely after the first invocation. + +### 3.2 Launcher path defaults + +The Python launcher (`scripts/run_deepseek_v4.py`) takes its path arguments from CLI flags. The defaults are: + +| Flag | Default | Use | +|---|---|---| +| `--data-dir` | `/root/datasets` | HF datasets (e.g. dapo-math-17k, …) | +| `--model-dir` | `/root/models` | parent directory holding the HF checkpoint and Megatron `_torch_dist` artifacts as separate sibling sub-directories | +| `--model-local-dir` | `/root/local_data` | local NVMe path on each node; `prepare-cp` rsyncs the HF checkpoint and `_torch_dist` here so the trainer reads from local disk instead of shared storage | +| `--save-dir` | `/root/models` | training checkpoints under `{save-dir}/{run-id}/checkpoints/` | + +You can override these on the launcher when your cluster mounts a different layout. There are no `MILES_SCRIPT_*` env vars that preconfigure these paths; the only env vars the launcher reads are `MILES_SCRIPT_EXTERNAL_RAY` and `MILES_SCRIPT_ENABLE_RAY_SUBMIT` (both governing Ray bootstrapping, see [§4.3](#43-multi-node-fan-out)). + +## 4. Script breakdown + +In this section, we explain what `full-train` does under the hood, and how to drive each stage manually if you need to debug or run outside the one-line launcher. + +### 4.1 Download model + datasets + +```bash +# inside the radixark/miles:deepseek-v4 (H200 / B200) or +# radixark/miles:gb300-dev-dskv4 (GB300) container +hf download sgl-project/DeepSeek-V4-Flash-FP8 --local-dir /root/models/DeepSeek-V4-Flash-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 +``` + +The Python launcher's `prepare-download` subcommand does the dataset fetch automatically; pass `--hf-checkpoint ` to skip the model download when the FP8 weights are already on a shared filesystem. + +### 4.2 HF → Megatron `torch_dist` conversion + +The conversion happens in two stages — a single-rank FP8 → BF16 cast, followed by a distributed `torch_dist` shard: + +```bash +cd /root/miles +python tools/fp8_cast_bf16.py \ + --input-fp8-hf-path /root/models/DeepSeek-V4-Flash-FP8 \ + --output-bf16-hf-path /root/models/DeepSeek-V4-Flash-FP8-bf16/ + +source scripts/models/deepseek-v4-flash.sh +PYTHONPATH=/root/Megatron-LM torchrun \ + --nproc-per-node 4 --nnodes 8 \ + --master-addr ${MASTER_ADDR} --master-port 12345 \ + --node-rank ${NODE_RANK} \ + tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --tensor-model-parallel-size 1 \ + --pipeline-model-parallel-size 8 \ + --expert-model-parallel-size 4 \ + --decoder-first-pipeline-num-layers 7 \ + --decoder-last-pipeline-num-layers 6 \ + --hf-checkpoint /root/models/DeepSeek-V4-Flash-FP8-bf16/ \ + --save /root/models/DeepSeek-V4-Flash-FP8_torch_dist/ +``` + +The Python launcher's `prepare-spmd` subcommand drives the same conversion. + +### 4.3 Multi-node fan-out + +The Python launcher manages Ray internally — start each pod with the appropriate image for the cluster (`radixark/miles:deepseek-v4` on H200 / B200, `radixark/miles:gb300-dev-dskv4` on GB300) and a working shared filesystem mounted at the same path on every node, then on the head node: + +```bash +ray start --head --num-gpus 8 --disable-usage-stats +# … then on each worker: +ray start --address=${HEAD_IP}:6379 --num-gpus 8 --disable-usage-stats +``` + +Alternatively, you can set `MILES_SCRIPT_EXTERNAL_RAY=1` and `RAY_ADDRESS=…` to point the launcher at an existing Ray cluster (for example, one that an orchestration layer has already brought up). When `RAY_ADDRESS` is unset, the launcher boots a local Ray head. + +### 4.4 Notable quirks + +- **Custom `transformers` patch.** miles ships `with_transformers_patch()` (`miles/utils/transformers_patch.py`) so HF's `AutoConfig.from_pretrained` recognizes `model_type=deepseek_v4` / `deepseek_ref` until support lands upstream. + +## 5. Example Recipe Configuration + +### 5.1 Megatron Parallelism + +These are the validated layouts shipped with the launcher; All parallelisms are supported, you can supply any other TP / EP / PP / CP combination that fits your compute. + +| Hardware | Nodes × GPUs | TP | PP | CP | EP | expert-TP | Pipeline layout | +|---|---|---|---|---|---|---|---| +| H200 | 8 × 8 = 64 | 8 | 8 | 1 | 8 | 1 | first 4 / last 3 layers | +| GB300 | 8 × 4 = 32 | 8 | 4 | 1 | 8 | 1 | first 11 / last 10 layers | +| GB300 | 8 × 4 = 32 | 2 | 8 | 2 | 4 | 1 | first 4 / last 3 layers | + +### 5.2 Algorithm + +Using GRPO as an example, you can configure the algorithm with the following flags: + +```bash +--advantage-estimator grpo +--eps-clip 0.2 +--eps-clip-high 0.28 +--kl-loss-coef 0.00 +--kl-loss-type low_var_kl +--entropy-coef 0.00 +``` + +The flags `--moe-router-freeze-gate` and `--freeze-e-score-correction-bias` are required and asserted on the `mcore` side — bias-update during RL is forbidden. + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-tp-size 8 + --sglang-dp-size 8 + --sglang-ep-size 8 + --sglang-enable-dp-attention + --sglang-attention-backend compressed # V4 sparse-MLA backend + --sglang-page-size 256 + --sglang-max-running-requests 64 + --sglang-chunked-prefill-size 8192 + --sglang-mem-fraction-static 0.5 # leave headroom for Megatron during wake_up + --use-rollout-routing-replay # MoE routing replay (R3) + --use-miles-router # miles router fronts /generate +) +``` + +The launcher sets the required env vars for you: `SGLANG_SKIP_CHECKPOINT_LOAD_CHECK=1`, `SGLANG_DSV4_FP4_EXPERTS=0`, `MILES_HACK_TRAIN_TORCH_DETERMINISTIC=1`, and `NCCL_ALGO=Ring`. + +On the Megatron side, V4 needs `--qkv-format bshd` with CP-aware data slicing. The DSA indexer additionally supports replay via `--use-rollout-indexer-replay` (off by default). + +### 5.4 Optimizer + +```bash +--optimizer adam +--lr 1e-6 --lr-decay-style constant +--weight-decay 0.1 +--adam-beta1 0.9 --adam-beta2 0.98 +--accumulate-allreduce-grads-in-fp32 +--attention-softmax-in-fp32 +--clip-grad 1.0 # Megatron default; not overridden by the launcher +``` + +The `--low-memory-resume` flag (off by default) puts optimizer states on CPU during ckpt resume to avoid OOM on the very first iteration. + +## 6. Pairs Well With + +- [FP8 & Low Precision](../../advanced/fp8-low-precision.md) +- [Architecture Support](../../advanced/architecture-support.md) — the V4 plugin lives under `miles_plugins/models/deepseek_v4/`. diff --git a/docs/models/deepseek/deepseek-v4-pro.md b/docs/models/deepseek/deepseek-v4-pro.md new file mode 100644 index 00000000000..6ce127b8348 --- /dev/null +++ b/docs/models/deepseek/deepseek-v4-pro.md @@ -0,0 +1,131 @@ +--- +title: DeepSeek-V4 Pro +description: Launch recipe for DeepSeek-V4-Pro (1.6 T) — V4-family architecture at Pro scale. +--- + +# DeepSeek-V4 Pro + +DeepSeek V4 training tracking issue: [`radixark/miles#1046`](https://github.com/radixark/miles/issues/1046). + +## 1. Model Introduction + +[DeepSeek-V4-Pro](https://huggingface.co/sgl-project/DeepSeek-V4-Pro-FP8) is a 49 B-active / 1.6 T-total MoE that scales up the same sparse-MLA + DSA-indexer + KV-compressor + hyper-connection stack as [V4-Flash](deepseek-v4-flash.md). The architecture family is identical; the deltas are size and a handful of tuned knobs (indexer top-k, output-projection groups, compression schedule). The miles + Megatron-Core integration ships in the same image as Flash and is selected with `--model-name DeepSeek-V4-Pro-FP8`. + +**Key highlights** (deltas vs [V4-Flash](deepseek-v4-flash.md#1-model-introduction)): + +- **Scaled-up V4 architecture**: 61 layers (vs 43), hidden-size 7168 (vs 4096), 128 attention heads (vs 64), `ffn_hidden_size=3072` and `moe_ffn_hidden_size=3072` (vs 2048). All layers are MoE (same `--moe-layer-freq` pattern). `q_lora_rank=1536` (vs 1024); latent KV (`kv_lora_rank=512`, `qk_head_dim=512`, `v_head_dim=512`) is unchanged across V4. +- **Hybrid Attention with wider indexer and output projection**: `index_topk=1024` (vs Flash's 512) — Pro keeps 64 indexer heads × 128 dim but picks twice as many KV per query. Grouped output projection uses `o_groups=16` (vs 8), keeping `o_lora_rank=1024`. +- **KV compressors start heavily compressed**: 60-element schedule `[128, 128, 4, 128, 4, 128, …, 4, 0]` — Pro skips Flash's two leading uncompressed layers and starts at ratio-128 (HCA) from layer 0. Middle layers still alternate 4× (CSA) and 128× (HCA); only the final layer is uncompressed. Compressor RoPE base (`compress_rope_theta=160000`) is shared with Flash. +- **MoE topology**: 384 routed experts + 1 shared (vs Flash's 256 + 1), top-6. `--moe-router-topk-scaling-factor 2.5` (vs Flash 1.5) compensates for the larger expert pool. The first 3 layers (`num_hash_layers=3`) remain dense-routed via hash buckets. +- **Identical YaRN RoPE and context**: `rope_theta=10000`, YaRN `factor=16`, `original_max_position_embeddings=65536` → effective context length **1,048,576 tokens (1 M)**, same as Flash. +- **Hyper-connection (HC) routing**: `hc_mult=4` parallel streams with sinkhorn-normalised mixing, same as Flash (PP buffers stay 4-D). +- **FP8 weights with simulated FP8 QAT** on indexer and compressor activations; default training is BF16 on the cast checkpoint and default rollout is FP8 in SGLang with `--sglang-attention-backend compressed`. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| DeepSeek-V4-Pro-FP8 | 49 B / 1.6 T | [sgl-project/DeepSeek-V4-Pro-FP8](https://huggingface.co/sgl-project/DeepSeek-V4-Pro-FP8) | + +## 3. Quick start + +### 3.1 One-line launch + +```bash +# Pull the image matching your cluster: +# H200 / B200 (cu129 x86) -> radixark/miles:deepseek-v4 +# GB300 (cu130 arm64) -> radixark/miles:gb300-dev-dskv4 +docker pull radixark/miles:deepseek-v4 + +# Production Pro run, inside the container +cd /root/miles +python scripts/run_deepseek_v4.py full-train \ + --model-name DeepSeek-V4-Pro-FP8 \ + --num-nodes 32 --num-gpus-per-node 8 +``` + +The `full-train` subcommand chains `prepare-download → prepare-single → prepare-spmd → prepare-cp → train`. Each stage has a sentinel-based skip so you can re-run safely after the first invocation. + +### 3.2 Launcher path defaults + +| Flag | Default | Use | +|---|---|---| +| `--data-dir` | `/root/datasets` | HF datasets (e.g. dapo-math-17k, …) | +| `--model-dir` | `/root/models` | parent directory holding the HF checkpoint and Megatron `_torch_dist` artifacts | +| `--model-local-dir` | `/root/local_data` | local NVMe path on each node; `prepare-cp` rsyncs the HF checkpoint and `_torch_dist` here so the trainer reads from local disk | +| `--save-dir` | `/root/models` | training checkpoints under `{save-dir}/{run-id}/checkpoints/` | + +TBD — Pro-specific overrides or env-var notes. + +## 4. Script breakdown + +The under-the-hood stages are essentially identical to V4-Flash — see the [V4-Flash Script breakdown](deepseek-v4-flash.md#4-script-breakdown) and substitute the Pro model name and path defaults shown above. + +## 5. Example Recipe Configuration + +### 5.1 Megatron Parallelism + +These are the validated layouts shipped with the launcher; All parallelisms are supported, you can supply any other TP / EP / PP / CP combination that fits your compute. + +| Hardware | Nodes × GPUs | TP | PP | CP | EP | expert-TP | Pipeline layout | +|---|---|---|---|---|---|---|---| +| H200 | 32 × 8 = 256 | 8 | 8 | 1 | 32 | 1 | first 7 / last 6 layers | + +### 5.2 Algorithm + +Same as Flash — see [V4-Flash §5.2 Algorithm](deepseek-v4-flash.md#52-algorithm). + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 32 + --sglang-tp-size 32 + --sglang-dp-size 32 + --sglang-ep-size 32 + --sglang-enable-dp-attention + --sglang-attention-backend compressed # V4 sparse-MLA backend + --sglang-page-size 256 + --sglang-max-running-requests 64 + --sglang-chunked-prefill-size 8192 + --sglang-server-concurrency 1024 + --sglang-moe-a2a-backend deepep # DeepEP normal-mode dispatch + --sglang-cuda-graph-max-bs 8 # see hang caveat below + --sglang-mem-fraction-static 0.7 # Pro needs a larger dynamic buffer + --use-miles-router +) +``` + +Required env vars (the launcher sets these for you): `SGLANG_SKIP_CHECKPOINT_LOAD_CHECK=1`, `SGLANG_DSV4_FP4_EXPERTS=0`, and the Pro-only pair `SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK=256`, `SGLANG_JIT_DEEPGEMM_PRECOMPILE=0`. + +Megatron side: `--qkv-format bshd` (V4 needs `bshd` with CP-aware data slicing). The DSA indexer additionally supports replay via `--use-rollout-indexer-replay` (off by default). + +!!! warning "Pro-specific rollout caveats" + 1. **Engine size ≥ 32 GPUs.** Pro needs a single SGLang engine spanning at least 32 GPUs — the launcher hard-codes `--rollout-num-gpus-per-engine 32`. Smaller engines do not leave enough memory after weights, KV cache, indexer state, and DeepEP buffers, and rollout will OOM under load. + 2. **EP is mandatory; pure TP will not shard the model.** 384 routed experts × `moe_ffn_hidden_size=3072` cannot be partitioned by tensor parallelism alone — the model must use expert parallelism (`--sglang-ep-size 32`) to spread the expert MLPs across ranks. `--sglang-tp-size 32` only covers the attention / embedding paths. + 3. **DeepEP normal-mode + CUDA graphs can hang at large batch sizes.** When `--sglang-moe-a2a-backend deepep` is on, an overly large `--sglang-cuda-graph-max-bs` makes SGLang hang during graph capture or replay. The launcher pins it to `8` for Pro — raise it only after verifying the engine doesn't deadlock at your target batch. + +### 5.4 Optimizer + +```bash +--optimizer adam +--lr 1e-6 --lr-decay-style constant +--weight-decay 0.1 +--adam-beta1 0.9 --adam-beta2 0.98 +--accumulate-allreduce-grads-in-fp32 +--attention-softmax-in-fp32 +--clip-grad 1.0 # Megatron default; not overridden by the launcher + +# Pro-only — forced on by the launcher (optimizer_offload=True) +--optimizer-cpu-offload +--use-precision-aware-optimizer +--overlap-cpu-optimizer-d2h-h2d +``` + +Pro selects `--model-name DeepSeek-V4-Pro-FP8`, which flips `optimizer_offload=True` in the launcher (`scripts/run_deepseek_v4.py`) and appends the three CPU-offload flags above. Adam states live on host RAM and are D2H/H2D-overlapped with the backward pass, freeing GPU memory for the 1.6 T weight + KV footprint. The `--low-memory-resume` flag (off by default) additionally puts optimizer states on CPU during ckpt resume to avoid OOM on the very first iteration. + +## 6. Pairs Well With + +- [FP8 & Low Precision](../../advanced/fp8-low-precision.md) +- [Architecture Support](../../advanced/architecture-support.md) +- [DeepSeek V4 Flash](deepseek-v4-flash.md) — sibling recipe; shares the V4-family architecture. diff --git a/docs/models/deepseek/deepseek.md b/docs/models/deepseek/deepseek.md new file mode 100644 index 00000000000..decfbe2deba --- /dev/null +++ b/docs/models/deepseek/deepseek.md @@ -0,0 +1,246 @@ +--- +title: DeepSeek R1 / V3 +description: Launch recipe for DeepSeek-R1 / DeepSeek-V3 (671 B total / 37 B active) on 16 nodes × 8 H100. +--- + +# DeepSeek R1 / V3 + +## 1. Model Introduction + +[DeepSeek-V3](https://huggingface.co/deepseek-ai/DeepSeek-V3) is a large-scale Mixture-of-Experts language model from DeepSeek, and [DeepSeek-R1](https://huggingface.co/deepseek-ai/DeepSeek-R1) is the reasoning-tuned variant built on the same architecture. Both expose the same Megatron-side definition in miles and share the launch recipe on this page. + +**Key highlights:** + +- **Fine-grained MoE architecture**: 671 B total / 37 B active per token, 256 routed experts with top-8 plus 1 shared expert, sigmoid router with bias. +- **MLA attention**: Multi-head Latent Attention with q-LoRA rank 1536, keeping the KV cache compact under long contexts. +- **Long-context capability**: trained at 32 K response length in this recipe; supports extended reasoning and agent-style workflows. +- **Strong reasoning and coding**: R1 in particular targets mathematical reasoning and step-by-step inference; V3 is the general-purpose base. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| DeepSeek-V3 | 37 B / 671 B | [deepseek-ai/DeepSeek-V3](https://huggingface.co/deepseek-ai/DeepSeek-V3) | +| DeepSeek-R1 | 37 B / 671 B | [deepseek-ai/DeepSeek-R1](https://huggingface.co/deepseek-ai/DeepSeek-R1) | + +## 3. Environment Setup + +### 3.1 Required env vars + +```bash +export BASE_DIR= +export MASTER_ADDR= +``` + +### 3.2 Download model + datasets + +```bash +hf download deepseek-ai/DeepSeek-R1 --local-dir $BASE_DIR/DeepSeek-R1 +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir $BASE_DIR/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir $BASE_DIR/rl_data/aime-2024 +``` + +### 3.3 HF → Megatron `torch_dist` conversion + +The HF checkpoint ships in block-quantized FP8 — first cast it to BF16: + +```bash +cd miles/ +python tools/fp8_cast_bf16.py \ + --input-fp8-hf-path $BASE_DIR/DeepSeek-R1 \ + --output-bf16-hf-path $BASE_DIR/DeepSeek-R1-bf16/ +``` + +Then convert BF16 HF → Megatron `torch_dist`. Run on **4 separate nodes** (`NODE_RANK=0..3`); `MASTER_ADDR` is the IP of node 0: + +```bash +cd miles/ +source scripts/models/deepseek-v3.sh +PYTHONPATH=/root/Megatron-LM/ torchrun \ + --nproc-per-node 8 \ + --master-addr ${MASTER_ADDR} --master-port 12345 \ + --nnodes=4 --node-rank ${NODE_RANK} \ + tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --tensor-model-parallel-size 1 \ + --pipeline-model-parallel-size 8 \ + --expert-tensor-parallel-size 1 \ + --expert-model-parallel-size 4 \ + --decoder-first-pipeline-num-layers 7 \ + --decoder-last-pipeline-num-layers 6 \ + --hf-checkpoint $BASE_DIR/DeepSeek-R1-bf16/ \ + --save $BASE_DIR/DeepSeek-R1_torch_dist/ +``` + +## 4. Launch + +### 4.1 Quick start + +On node 0: + +```bash +cd miles/ +bash scripts/run-deepseek-r1.sh +``` + +### 4.2 Multi-node fan-out + +On every other node, join the Ray cluster: + +```bash +ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 \ + --node-ip-address ${WORKER_IP} --disable-usage-stats +``` + +Alternatively, with an MPI-style hostfile (each line `ip slot=8`), append a loop after `ray start --head` in `scripts/run-deepseek-r1.sh` to ssh out from node 0: + +```bash +for WORKER_IP in $(awk '{print $1}' $BASE_DIR/mpi_hostfile); do + if [[ "$WORKER_IP" == "$MASTER_ADDR" ]]; then + continue + fi + 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" & +done +wait +``` + +`scripts/run_deepseek.py` is an alternative Python entry point (in preview) that wraps download + FP8→BF16 cast + `torch_dist` conversion + `train.py` submission behind a Typer CLI. + +## 5. Recipe Configuration + +All values below come straight from `scripts/run-deepseek-r1.sh`. + +### 5.1 Parallelism + +| TP | PP | CP | EP | expert-TP | `decoder-last-pipeline-num-layers` | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---|---| +| 8 | 4 | 4 | 32 | 1 | 13 | 16384 | 128 (16 × 8) | + +```bash +PERF_ARGS=( + --tensor-model-parallel-size 8 + --sequence-parallel + --pipeline-model-parallel-size 4 + --context-parallel-size 4 + --expert-model-parallel-size 32 + --expert-tensor-parallel-size 1 + --decoder-last-pipeline-num-layers 13 + + --recompute-granularity full + --recompute-method uniform + --recompute-num-layers 1 + + --use-dynamic-batch-size + --max-tokens-per-gpu 16384 +) +``` + +DeepSeek-R1 has 61 layers, which doesn't divide evenly into PP=4 — `--decoder-last-pipeline-num-layers 13` puts the extra layers on the last stage. With `--use-dynamic-batch-size`, miles packs samples up to `--max-tokens-per-gpu`; under CP=4, a CP group shares a `CP × max-tokens-per-gpu` budget. miles always trains with data packing and per-token loss, so dynamic batch size doesn't change the loss. + +### 5.2 Algorithm + +GRPO with DAPO-style dynamic sampling: + +```bash +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 +) + +ROLLOUT_ARGS=( + --prompt-data $BASE_DIR/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 128 + --n-samples-per-prompt 8 + --rollout-max-response-len 32768 + --rollout-temperature 1 + + --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 +) +``` + +`--use-kl-loss` is enabled but the coefficient is 0 — to drop the reference model entirely, remove `--use-kl-loss`. `--over-sampling-batch-size 256` paired with `check_reward_nonzero_std` is the DAPO-style setup: oversample, then drop prompts whose reward distribution has zero variance. + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 64 + --sglang-mem-fraction-static 0.7 + --sglang-enable-ep-moe + + # dp attention + --sglang-enable-dp-attention + --sglang-dp-size 8 + --sglang-moe-dense-tp-size 1 + --sglang-enable-dp-lm-head + + # enable deepep for sglang + --sglang-enable-deepep-moe + --sglang-deepep-mode auto + + # make every dp rank has 128 concurrency + --sglang-server-concurrency 1024 +) +``` + +`--rollout-num-gpus-per-engine 64` corresponds to SGLang's `tp_size`. To exploit large-EP inference, the recipe sets EP64, DP-attention with DP8, and DeepEP `auto`. `--sglang-server-concurrency` is a miles-specific knob to keep the SGLang HTTP server from being swamped — default 512, raised to 1024 here so each of the 8 DP ranks gets 128 concurrent requests. + +Megatron side enables DeepEP as well: + +```bash +MISC_ARGS=( + --moe-enable-deepep + --moe-token-dispatcher-type flex + ... +) +``` + +### 5.4 Optimizer + +```bash +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 +) +``` + +`--optimizer-cpu-offload` puts the Adam state on host RAM (~1.4–1.5 TB / 8-GPU node). If a node runs out of host memory, add more nodes to widen parallelism rather than swapping. + +### 5.5 Notable quirks + +- **Online FP8 quantization against the HF config**: `--hf-checkpoint` points at the FP8 HF directory (also where the tokenizer is read from). miles applies the quantization config from the HF checkpoint, so weights are block-wise quantized before being passed to SGLang. The BF16 HF directory is available as a commented alternative in `CKPT_ARGS`. +- **`--decoder-last-pipeline-num-layers 13`** is mandatory under PP=4 (61 layers don't divide evenly). +- **CKPT_ARGS** point at `$BASE_DIR/DeepSeek-R1_miles/` for both `--load` and `--save`; `--load` defaults to `--ref-load` when empty, so first run reads from `torch_dist`. +- **`--colocate`** runs actor and rollout on the same GPUs (16 nodes × 8 GPU = 128 GPUs total via `--actor-num-nodes 16 --actor-num-gpus-per-node 8`). + +## 6. Pairs Well With + +- [PD Disaggregation](../../advanced/pd-disaggregation.md) +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) +- [Fault Tolerance](../../advanced/fault-tolerance.md) diff --git a/docs/models/deepseek/index.md b/docs/models/deepseek/index.md new file mode 100644 index 00000000000..b1e95ea46c5 --- /dev/null +++ b/docs/models/deepseek/index.md @@ -0,0 +1,45 @@ +--- +title: DeepSeek +description: Miles recipes for the DeepSeek family — DeepSeek-V4 Flash (sparse-MLA + DSA indexer), V3, and R1. +--- + +# DeepSeek family + +Miles ships recipes for the DeepSeek family across two generations: **DeepSeek-V4 Flash** introduces sparse multi-head latent attention with a learned indexer and KV compressors (8-node H200), while **V3 / R1** remain the canonical 16-node 671 B-parameter recipes (BF16 train + 128×128 block-wise FP8 rollout, DeepEP, DAPO-style dynamic sampling). + +## Variants + +| Model | Active / Total | HF ID | Recipe | +|---|---|---|---| +| DeepSeek-V4-Pro | 49 B / 1.6 T | TBA | [deepseek-v4-pro](deepseek-v4-pro.md) | +| DeepSeek-V4-Flash | 13 B / 284 B | `sgl-project/DeepSeek-V4-Flash-FP8` | [deepseek-v4-flash](deepseek-v4-flash.md) | +| DeepSeek-V3 | 37 B / 671 B | `deepseek-ai/DeepSeek-V3` | [deepseek](deepseek.md) | +| DeepSeek-R1 | 37 B / 671 B | `deepseek-ai/DeepSeek-R1` | [deepseek](deepseek.md) | + +A validated DeepSeek-V4-Pro recipe is not yet available — see [`radixark/miles#1046`](https://github.com/radixark/miles/issues/1046) for tracking. + +## Fastest path to train + +DeepSeek-V4-Flash needs 8 nodes of 8× H200 and the `radixark/miles:deepseek-v4` image: + +```bash +cd /root/miles +python scripts/run_deepseek_v4.py full-train \ + --model-name DeepSeek-V4-Flash-FP8 \ + --num-nodes 8 --num-gpus-per-node 8 +``` + +DeepSeek-R1 needs 16 nodes of 8× H100: + +```bash +cd /root/miles +bash scripts/run-deepseek-r1.sh # full 16-node run +``` + +See the [DeepSeek-V4 Flash](deepseek-v4-flash.md) page for the V4 architecture summary, parallelism layouts, and known workarounds; see the [DeepSeek R1 / V3](deepseek.md) page for the V3 flow — FP8 → BF16 conversion, Megatron parallelism layout (TP8 / PP4 / EP32 / CP4), per-arg walkthrough, and the alternate Python launcher (`scripts/run_deepseek.py`). + +## Pairs well with + +- [PD Disaggregation](../../advanced/pd-disaggregation.md) — 671 B is where PD really earns its keep. +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) — amortize weight sync across ranks. +- [Fault Tolerance](../../advanced/fault-tolerance.md) — node failures are inevitable at 16-node scale. diff --git a/docs/models/gpt-oss/gpt-oss.md b/docs/models/gpt-oss/gpt-oss.md new file mode 100644 index 00000000000..a07a8ed7e34 --- /dev/null +++ b/docs/models/gpt-oss/gpt-oss.md @@ -0,0 +1,122 @@ +--- +title: GPT-OSS 20B +description: Two launchers — Megatron BF16 (8 GPU, mbridge) and FSDP (4 GPU, dequantizes MXFP4 → BF16 first). +--- + +# GPT-OSS 20B + +## 1. Model Introduction + +[GPT-OSS](https://huggingface.co/openai/gpt-oss-20b) is OpenAI's open-weight language model, designed for reasoning, agentic tasks, and developer use cases. miles supports the 20 B variant. + +**Key highlights:** + +- **Configurable reasoning effort**: low / medium / high reasoning effort selectable per request. +- **Full chain-of-thought**: the reasoning trace is exposed and trainable. +- **MXFP4 native weights**: the HF checkpoint ships in MXFP4 (post-trained) — the FSDP launcher dequantizes to BF16 first; the BF16 launcher uses mbridge to load HF directly. +- **Sink attention**: requires `--qkv-format bshd` on the Megatron path, which precludes dynamic batch sizing. + +## 2. Supported Variants + +| Model | HF ID | +|---|---| +| gpt-oss-20b | [openai/gpt-oss-20b](https://huggingface.co/openai/gpt-oss-20b) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +# Megatron BF16 path — stage everything under /root/shared (script's hardcoded BASE_DIR) +hf download openai/gpt-oss-20b --local-dir /root/shared/gpt-oss-20b +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/shared/dapo-math-17k + +# FSDP path — script downloads + dequantizes automatically; only the dataset is needed +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +Neither launcher needs a `convert_hf_to_torch_dist.py` step. The Megatron BF16 launcher loads the HF checkpoint directly via `--megatron-to-hf-mode bridge` (mbridge); the FSDP launcher loads HF directly. + +The FSDP launcher additionally runs an inline `convert_model.py` snippet that downloads `openai/gpt-oss-20b` and dequantizes its MXFP4 weights to BF16, saving to `/root/models/gpt-oss-20b-bf16`. + +## 4. Launch + +### 4.1 Quick start + +```bash +# Megatron BF16 (1 node × 8 GPU) +cd /root/miles +bash scripts/run-gpt-oss-20b-bf16.sh + +# FSDP (1 node × 4 GPU; restricts to GPUs 4-7 via CUDA_VISIBLE_DEVICES) +bash scripts/run-gptoss-20b-fsdp.sh +``` + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| Launcher | TP | PP | CP | EP | expert-TP | `micro-batch-size` | GPUs | +|---|---|---|---|---|---|---|---| +| `run-gpt-oss-20b-bf16.sh` (Megatron) | 8 | 1 | 1 | 8 | 1 | 1 | 8 (1 × 8) | +| `run-gptoss-20b-fsdp.sh` (FSDP) | 1 | – | – | – | – | – | 4 (1 × 4) | + +`--use-dynamic-batch-size` is **not** used on the Megatron BF16 path — the script's comment explains: `--qkv-format bshd` (required for sink attention with TE) is incompatible with dynamic batch size. Only `--micro-batch-size 1` is set. `--sequence-parallel` is on (required for TP + EP). + +The FSDP variant passes `--train-backend fsdp --bf16 --attn-implementation eager` to `train.py`. + +### 5.2 Algorithm + +Both scripts use GRPO with `--eps-clip 0.2 --eps-clip-high 0.28 --entropy-coef 0.00`. **`--use-kl-loss` is commented out in both scripts** (the BF16 script's comment notes "need gpt oss ckpt conversion" before KL can be enabled). + +Note that `--rm-type` differs: `math` for the Megatron BF16 path, `deepscaler` for FSDP. + +### 5.3 Rollout & SGLang + +`run-gpt-oss-20b-bf16.sh`: + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 4 + --sglang-dtype bfloat16 + --sglang-decode-log-interval 1000 + --sglang-mem-fraction-static 0.70 +) +``` + +`run-gptoss-20b-fsdp.sh`: + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 4 + --sglang-tensor-parallel-size 1 + --sglang-dtype bfloat16 + --sglang-decode-log-interval 1000 +) +``` + +### 5.4 Optimizer + +The Megatron BF16 launcher enables CPU Adam (`--optimizer-cpu-offload --overlap-cpu-optimizer-d2h-h2d --use-precision-aware-optimizer`); the FSDP launcher does not. + +### 5.5 Notable quirks + +`MISC_ARGS` in the BF16 script: + +```bash +--attention-dropout 0.0 +--hidden-dropout 0.0 +--qkv-format bshd # required for TE sink attention (SWA + learnable softmax offset) +--attention-backend fused +``` + +`--qkv-format bshd` is mandated by the sink-attention pattern; in turn it precludes `--use-dynamic-batch-size`. Don't toggle either flag without the other. + +Neither launcher writes `--save`/`--load`/`--save-interval`. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) — the FSDP variant. +- [Low Precision RL](../../advanced/fp8-low-precision.md) diff --git a/docs/models/index.md b/docs/models/index.md new file mode 100644 index 00000000000..7529335043c --- /dev/null +++ b/docs/models/index.md @@ -0,0 +1,41 @@ +--- +title: Supported Models +description: Per-family recipes covering weight conversion, launch flags, and parallelism choices. +--- + +# Supported Models + +Miles ships ready-to-run recipes for every model family listed below. Each page covers +weight conversion, parallelism, and the launch script in the order you'd actually run +them. + +## By family + +Each model name links to its recipe page. + +| Family | Models | +|---|---| +| **DeepSeek** | [DeepSeek-V4 Pro](deepseek/deepseek-v4-pro.md)
[DeepSeek-V4 Flash](deepseek/deepseek-v4-flash.md)
[DeepSeek-R1](deepseek/deepseek.md)
[DeepSeek-V3](deepseek/deepseek.md) | +| **Qwen** | [Qwen3.6 MoE](qwen/qwen3-6-moe.md)
[Qwen3.6](qwen/qwen3-6.md)
[Qwen3.5-35B-A3B](qwen/qwen3-5-moe.md)
[Qwen3.5-4B / 9B / 27B](qwen/qwen3-5.md)
[Qwen3-Next-80B-A3B-Thinking](qwen/qwen3-next.md)
[Qwen3-30B-A3B / 235B-A22B](qwen/qwen3-moe.md)
[Qwen3-0.6B / 1.7B / 4B / 8B / 14B / 32B](qwen/qwen3.md) | +| **GLM** | [GLM-5.1](glm/glm5.md)
[GLM-5](glm/glm5.md)
[GLM-4.7-Flash](glm/glm4-7-flash.md)
[GLM-4.5](glm/glm4-5.md)
[GLM-Z1-9B-0414](glm/glm4.md) | +| **Kimi** | [Kimi-K2.6](kimi/kimi-k2.5.md)
[Kimi-K2.5](kimi/kimi-k2.5.md)
[Kimi-K2-Instruct / Thinking](kimi/kimi-k2.md)
[Moonlight-16B-A3B](kimi/moonlight.md) | +| **Nemotron** | [Nemotron-3-Super-120B-A12B-FP8](nemotron/nemotron-3-super.md)
[Nemotron-3-Nano MoE](nemotron/nemotron-3-nano-moe.md)
[Nemotron-3-Nano](nemotron/nemotron-3-nano.md) | +| **MiMo** | [MiMo-7B-RL](mimo/mimo.md) | +| **GPT-OSS** | [gpt-oss-20b](gpt-oss/gpt-oss.md) | + +## How a recipe is structured + +Every recipe page follows the same six sections: + +1. **Model Introduction** — what the model is and why miles supports it. +2. **Supported Variants** — model sizes + HF links. +3. **Environment Setup** — env vars, downloads, and HF → Megatron conversion. +4. **Launch** — the `scripts/run-.sh` (or `run_.py`) invocation. +5. **Recipe Configuration** — parallelism, algorithm, rollout/SGLang, optimizer. +6. **Pairs Well With** — links to the advanced features that complement this recipe. + +## Adding a new model + +Miles's plugin architecture lets you wrap a HuggingFace implementation as a Megatron +module without patching Megatron core. See +[Backends Beyond Megatron](../advanced/architecture-support.md) for the workflow. diff --git a/docs/models/kimi/index.md b/docs/models/kimi/index.md new file mode 100644 index 00000000000..6708717a8be --- /dev/null +++ b/docs/models/kimi/index.md @@ -0,0 +1,34 @@ +--- +title: Kimi +description: Miles recipes for the Moonshot family — Kimi K2 / K2-Thinking (1 T / 32 B-A) and Moonlight 16B-A3B. +--- + +# Kimi family + +Miles supports both ends of Moonshot's MoE line: the 1 T-parameter Kimi K2 (Instruct and Thinking variants) at 32 B active per token, and the compact Moonlight 16B-A3B that fits on a single 8× H100 node — handy as a single-node test target before scaling K2 across 16 nodes. K2-Thinking is also the canonical target for INT4 QAT. + +## Variants + +| Model | Active / Total | HF ID | Recipe | +|---|---|---|---| +| Kimi-K2-Instruct | 32 B / 1 T | `moonshotai/Kimi-K2-Instruct` | [kimi-k2](kimi-k2.md) | +| Kimi-K2-Thinking | 32 B / 1 T | `moonshotai/Kimi-K2-Thinking` | [kimi-k2](kimi-k2.md) | +| Moonlight-16B-A3B | 3 B / 16 B | `moonshotai/Moonlight-16B-A3B` | [moonlight](moonlight.md) | + +## Fastest path to train + +Moonlight on a single 8× H100 node — the smallest Moonshot recipe and a good MoE smoke test: + +```bash +cd /root/miles +hf download moonshotai/Moonlight-16B-A3B --local-dir /root/Moonlight-16B-A3B +bash scripts/run-moonlight-16B-A3B.sh +``` + +See the [Moonlight](moonlight.md) page for the full walkthrough, or [Kimi K2](kimi-k2.md) for the 16-node K2-Thinking recipe (including the one-line `model_type` patch that lets Miles treat K2 as a DeepSeek-V3-shaped architecture). + +## Which variant do I pick? + +- **Single-node MoE smoke test** → Moonlight-16B-A3B ([moonlight](moonlight.md)). +- **Frontier-scale instruction-tuned MoE** → Kimi-K2-Instruct ([kimi-k2](kimi-k2.md)). +- **Reasoning-style training, INT4 QAT target** → Kimi-K2-Thinking ([kimi-k2](kimi-k2.md)). diff --git a/docs/models/kimi/kimi-k2.5.md b/docs/models/kimi/kimi-k2.5.md new file mode 100644 index 00000000000..32141f85824 --- /dev/null +++ b/docs/models/kimi/kimi-k2.5.md @@ -0,0 +1,7 @@ +--- +title: Kimi K2.5 +description: Launch recipe for Kimi K2.5. +--- +# Kimi K2.5 + +Placeholder for the tutorial, refer to [PR](https://github.com/radixark/miles/pull/792) \ No newline at end of file diff --git a/docs/models/kimi/kimi-k2.md b/docs/models/kimi/kimi-k2.md new file mode 100644 index 00000000000..7b7c8dfd340 --- /dev/null +++ b/docs/models/kimi/kimi-k2.md @@ -0,0 +1,174 @@ +--- +title: Kimi K2 +description: Launch recipes for Kimi-K2-Instruct and Kimi-K2-Thinking — 32 nodes × 8 GPU. +--- + +# Kimi K2 + +## 1. Model Introduction + +[Kimi-K2](https://moonshotai.github.io/Kimi-K2/) is a state-of-the-art MoE language model from Moonshot AI with 32 B activated parameters and 1 T total parameters. + +**Key highlights:** + +- **Trillion-parameter MoE**: 1 T total / 32 B active per token, 61 layers (1 dense + the rest MoE), MLA attention shaped like DeepSeek-V3. +- **Instruct and Thinking variants**: Instruct is the general-purpose chat / agentic post-train; Thinking adds step-by-step reasoning with a 256 K context and ships in native INT4. +- **DeepSeek-V3-shaped architecture**: miles loads it through the DeepSeek-V3 path (one `sed` away), reusing the conversion + parallelism plumbing. +- **INT4 QAT target**: Kimi-K2-Thinking is the canonical reference recipe for INT4 QAT in miles. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Kimi-K2-Instruct | 32 B / 1 T | [moonshotai/Kimi-K2-Instruct](https://huggingface.co/moonshotai/Kimi-K2-Instruct) | +| Kimi-K2-Thinking | 32 B / 1 T | [moonshotai/Kimi-K2-Thinking](https://huggingface.co/moonshotai/Kimi-K2-Thinking) | + +## 3. Environment Setup + +### 3.1 Required env vars + +```bash +export BASE_DIR= +export MASTER_ADDR= +``` + +Both are referenced but never set inside the scripts — export them yourself before launch. + +### 3.2 Download model + datasets + +```bash +hf download moonshotai/Kimi-K2-Instruct --local-dir $BASE_DIR/Kimi-K2-Instruct +# or, for Thinking (FP8 by default): +hf download moonshotai/Kimi-K2-Thinking --local-dir $BASE_DIR/Kimi-K2-Thinking-fp8 + +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir $BASE_DIR/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir $BASE_DIR/rl_data/aime-2024 +``` + +### 3.3 HF → Megatron `torch_dist` conversion + +Convert across 4 nodes (mirror the DeepSeek-V3 procedure): + +```bash +cd /root/miles +source scripts/models/kimi-k2.sh # or kimi-k2-thinking.sh +PYTHONPATH=/root/Megatron-LM/ torchrun \ + --nproc-per-node 8 \ + --master-addr ${MASTER_ADDR} --master-port 12345 \ + --nnodes=4 --node-rank ${NODE_RANK} \ + tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint $BASE_DIR/Kimi-K2-Instruct/ \ + --save $BASE_DIR/Kimi-K2_torch_dist/ +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +export BASE_DIR=...; export MASTER_ADDR=... + +bash scripts/run-kimi-k2-Thinking.sh # or run-kimi-k2-Instruct.sh +``` + +Both launchers submit to an **already-running Ray cluster** (`ray job submit ...`); neither runs `ray start --head` itself. + +### 4.2 Multi-node fan-out + +Bring up Ray on every node before launching: + +```bash +# head +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats +# each worker +ray start --address=${MASTER_ADDR}:6379 --num-gpus 8 --node-ip-address ${WORKER_IP} +``` + +## 5. Recipe Configuration + +### 5.1 Parallelism + +Identical for both Instruct and Thinking: + +| TP | PP | CP | EP | expert-TP | `decoder-last-pipeline-num-layers` | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---|---| +| 8 | 8 | 4 | 32 | 1 | 5 | 16384 | 256 (32 × 8) | + +Both scripts pass `--actor-num-nodes 32 --actor-num-gpus-per-node 8 --colocate --update-weight-buffer-size $((4*512*1024*1024))` to `train.py`. + +### 5.2 Algorithm + +| Script | Advantage | TIS | +|---|---|---| +| Instruct | GRPO (`--eps-clip 0.2 --eps-clip-high 0.28`) | – | +| Thinking | GRPO (`--eps-clip 0.2 --eps-clip-high 0.28`) | `--use-tis` | + +Both use `--use-kl-loss --kl-loss-coef 0.00 --kl-loss-type low_var_kl --entropy-coef 0.00`. + +Rollout shape (both): + +```bash +--rm-type math +--num-rollout 100 +--rollout-batch-size 128 +--n-samples-per-prompt 8 +--rollout-max-response-len 32768 # Instruct +--rollout-max-response-len 16384 # Thinking +--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 +``` + +DAPO-style dynamic sampling is on by default in both scripts. + +### 5.3 Rollout & SGLang + +Identical for both: + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 16 + --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-ep-size 16 + + # deepep — commented out in both scripts + # --sglang-enable-deepep-moe + # --sglang-deepep-mode auto + + --sglang-server-concurrency 1024 +) +``` + +Megatron-side `--moe-enable-deepep` and `--moe-token-dispatcher-type flex` are **on** in the Instruct script but **commented out** in the Thinking script. + +### 5.4 Optimizer + +CPU Adam is enabled in both: + +```bash +--optimizer-cpu-offload +--overlap-cpu-optimizer-d2h-h2d +--use-precision-aware-optimizer +``` + +### 5.5 Notable quirks + +- Instruct loads the BF16 HF checkpoint by default (FP8 commented out) and reads eval data from `$BASE_DIR/rl_data/`; Thinking loads the FP8 HF checkpoint by default and reads eval data from `$BASE_DIR/`. +- `--global-batch-size 1024` is commented out in both scripts. + +## 6. Pairs Well With + +- [PD Disaggregation](../../advanced/pd-disaggregation.md) +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) +- [Fault Tolerance](../../advanced/fault-tolerance.md) +- [INT4 QAT](../../advanced/int4-qat.md) diff --git a/docs/models/kimi/moonlight.md b/docs/models/kimi/moonlight.md new file mode 100644 index 00000000000..e4968b7f97d --- /dev/null +++ b/docs/models/kimi/moonlight.md @@ -0,0 +1,114 @@ +--- +title: Moonlight +description: Single-node MoE recipe (8 GPU) — DAPO-style dynamic sampling and CPU Adam on by default. +--- + +# Moonlight + +## 1. Model Introduction + +[Moonlight](https://huggingface.co/moonshotai/Moonlight-16B-A3B) is Moonshot AI's compact MoE — 16 B total / 3 B active, trained with the Muon optimizer — and a useful single-node test target for MoE RL code changes before scaling to Kimi K2. + +**Key highlights:** + +- **Compact MoE**: 16 B total / 3 B active, 27 layers (1 dense + 26 MoE), 64 routed experts top-6 + 2 shared. +- **MLA attention**: Multi-head Latent Attention with `kv-LoRA rank 512`. +- **Single-node footprint**: full RL recipe fits on 1 × 8 H100. +- **Muon-trained base**: pretrained with the Muon optimizer; weight decay matters at scale. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Moonlight-16B-A3B | 3 B / 16 B | [moonshotai/Moonlight-16B-A3B](https://huggingface.co/moonshotai/Moonlight-16B-A3B) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +hf download moonshotai/Moonlight-16B-A3B --local-dir /root/Moonlight-16B-A3B +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/moonlight.sh +PYTHONPATH=/root/Megatron-LM torchrun --nproc-per-node 8 \ + tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Moonlight-16B-A3B \ + --save /root/Moonlight-16B-A3B_torch_dist +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +bash scripts/run-moonlight-16B-A3B.sh +``` + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| TP | PP | CP | EP | expert-TP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| 4 | 1 | 1 | 8 | 1 | 8192 | 8 (1 × 8) | + +### 5.2 Algorithm + +GRPO with `--eps-clip 0.2 --eps-clip-high 0.28 --use-kl-loss --kl-loss-coef 0.00`. R3 is **not** enabled. + +```bash +ROLLOUT_ARGS=( + --rm-type math + --num-rollout 3000 + --rollout-batch-size 128 + --n-samples-per-prompt 8 + --rollout-max-response-len 4096 + --rollout-temperature 1 + + --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 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-mem-fraction-static 0.7 + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) +) +``` + +Megatron-side DeepEP is on: `--moe-enable-deepep --moe-token-dispatcher-type flex`. + +### 5.4 Optimizer + +CPU Adam on: + +```bash +--optimizer-cpu-offload +--overlap-cpu-optimizer-d2h-h2d +--use-precision-aware-optimizer +``` + +### 5.5 Notable quirks + +- `--attention-backend flash` is **commented out** in this script (script comment: "need to comment this when using model with MLA"). + +## 6. Pairs Well With + +- [Rollout Routing Replay (R3)](../../advanced/miles-router.md) +- [Low Precision RL](../../advanced/fp8-low-precision.md) diff --git a/docs/models/mimo/mimo.md b/docs/models/mimo/mimo.md new file mode 100644 index 00000000000..ee72bf1cea7 --- /dev/null +++ b/docs/models/mimo/mimo.md @@ -0,0 +1,123 @@ +--- +title: MiMo +description: Single-node GRPO + EAGLE speculative recipe with online MTP training. +--- + +# MiMo 7B + +## 1. Model Introduction + +[MiMo-7B-RL](https://huggingface.co/XiaomiMiMo/MiMo-7B-RL) is Xiaomi's dense reasoning RL model with a built-in MTP (Multi-Token Prediction) layer. + +**Key highlights:** + +- **Dense 7 B with built-in MTP head**: a convenient target for EAGLE-style speculative rollout with online MTP-SFT. +- **Strong reasoning**: matches o1-mini on math and code reasoning at 7 B scale. +- **Single-node recipe**: full RL flow fits on 1 × 8 GPU. +- **Online MTP training**: the recipe trains the MTP head jointly with the policy via `--enable-mtp-training`. + +## 2. Supported Variants + +| Model | HF ID | +|---|---| +| MiMo-7B-RL | [XiaomiMiMo/MiMo-7B-RL](https://huggingface.co/XiaomiMiMo/MiMo-7B-RL) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +hf download XiaomiMiMo/MiMo-7B-RL --local-dir /root/MiMo-7B-RL +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/mimo-7B-rl.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/MiMo-7B-RL \ + --save /root/MiMo-7B-RL_torch_dist +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +bash scripts/run-mimo-7B-rl-eagle.sh +``` + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| TP | PP | CP | EP | expert-TP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| 2 | 1 | 1 | 1 | 1 | 9216 | 8 (1 × 8) | + +`--use-dynamic-batch-size` + `--sequence-parallel` on; `--micro-batch-size` is commented out. + +### 5.2 Algorithm + +GRPO baseline: + +```bash +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 +) +``` + +Plus online MTP training: + +```bash +SPEC_ARGS=( + --enable-mtp-training + --mtp-loss-scaling-factor 0.2 +) +``` + +`--mtp-num-layers 1` lives in `MODEL_ARGS`, so you get it for free when you source the model config. + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 + + # for speculative decoding + --sglang-speculative-algorithm EAGLE + --sglang-speculative-num-steps 3 + --sglang-speculative-eagle-topk 1 + --sglang-speculative-num-draft-tokens 4 + + # sometimes flashinfer has IMA bugs. Use fa3 instead + --sglang-attention-backend fa3 +) +``` + +`--rollout-num-gpus-per-engine 1` is intentional — one SGLang engine per GPU. The `fa3` attention backend is chosen because flashinfer was hitting IMAs (in-source comment). + +### 5.4 Optimizer + +CPU Adam is **not** enabled. + +### 5.5 Notable quirks + +- `--save-interval` is **2000** here (much larger than the 20 used in most other launchers); the actor save dir has a `-mtp` suffix (`/root/MiMo-7B-RL-mtp_miles/`). +- R3 is **not** enabled. + +## 6. Pairs Well With + +- [Speculative Decoding](../../advanced/speculative-decoding.md) diff --git a/docs/models/nemotron/index.md b/docs/models/nemotron/index.md new file mode 100644 index 00000000000..23370848b17 --- /dev/null +++ b/docs/models/nemotron/index.md @@ -0,0 +1,38 @@ +--- +title: Nemotron +description: Miles recipes for NVIDIA's Nemotron-3 family — Mamba+Attention(+MoE) hybrids loaded via Megatron AutoBridge. +--- + +# Nemotron family + +Miles supports NVIDIA's Nemotron-3 line: a Mamba + Attention hybrid that, in the Super tier, adds MoE and ships natively in FP8. All three variants load via the Megatron AutoBridge path, so there is no offline HF → `torch_dist` conversion step. + +## Variants + +| Model | Active / Total | HF ID | Recipe | +|---|---|---|---| +| Nemotron-3-Nano | 4 B / 4 B (dense) | `nvidia/Nemotron-3-Nano-4B` | [nemotron-3-nano](nemotron-3-nano.md) | +| Nemotron-3-Nano MoE | 3 B / 30 B | `nvidia/Nemotron-3-Nano-30B-A3B` | [nemotron-3-nano-moe](nemotron-3-nano-moe.md) | +| Nemotron-3-Super | 12 B / 120 B (FP8) | `nvidia/Nemotron-3-Super-120B-A12B-FP8` | [nemotron-3-super](nemotron-3-super.md) | + +## Fastest path to train + +Nemotron-3-Nano (dense, 4 B) is the smallest and runs on a single 8-GPU node: + +```bash +cd /root/miles +bash scripts/run-nemotron-3-nano.sh +``` + +See the [Nemotron-3-Nano](nemotron-3-nano.md) page for the dense walkthrough, [Nemotron-3-Nano MoE](nemotron-3-nano-moe.md) for the 30 B MoE variant, and [Nemotron-3-Super](nemotron-3-super.md) for the FP8-native 120 B-A12B recipe. + +## Which variant do I pick? + +- **Smallest, single-node smoke test** → Nemotron-3-Nano ([nemotron-3-nano](nemotron-3-nano.md)). +- **Mid-scale hybrid MoE** → Nemotron-3-Nano MoE ([nemotron-3-nano-moe](nemotron-3-nano-moe.md)). +- **Frontier-scale FP8-native MoE** → Nemotron-3-Super ([nemotron-3-super](nemotron-3-super.md)). + +## Pairs well with + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) — the AutoBridge path Nemotron rides on. +- [Low Precision RL](../../advanced/fp8-low-precision.md) — Super ships natively in FP8. diff --git a/docs/models/nemotron/nemotron-3-nano-moe.md b/docs/models/nemotron/nemotron-3-nano-moe.md new file mode 100644 index 00000000000..489d8213d79 --- /dev/null +++ b/docs/models/nemotron/nemotron-3-nano-moe.md @@ -0,0 +1,150 @@ +--- +title: Nemotron-3-Nano MoE +description: Launch recipe for NVIDIA Nemotron-3-Nano-30B-A3B (Mamba+Attention+MoE hybrid) via Megatron AutoBridge. +--- + +# Nemotron-3-Nano MoE + +## 1. Model Introduction + +[NVIDIA Nemotron-3-Nano-30B-A3B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16) +is a hybrid Mamba + attention + MoE model. It pairs the `nemotron_h` block +pattern from the dense 4B with a 128-expert sparse layer (top-6 routing, +1 shared expert, DSv3-style sigmoid routing with `routed_scaling_factor=2.5`). + +miles loads it through the `megatron.bridge` AutoBridge with a custom +**NemotronH MoE bridge shim** (`miles_plugins/megatron_bridge/nemotron_h.py`) that +wires `routed_scaling_factor`, `n_group`, and `topk_group` onto the Megatron +provider. Without the shim the routed output is silently scaled 1.0× → ~0.28 +logprob drift between train and rollout. + +**Key highlights:** + +- **Hybrid + MoE**: Mamba + attention + sparse MoE in the `nemotron_h` family. +- **128 experts, top-6 routing**, 1 shared expert (3712-dim), aux-free expert-bias load balancing. +- **Sigmoid routing** with `--moe-router-topk-scaling-factor 2.5`. +- **Bridge-mode load**: `--megatron-to-hf-mode bridge` — no `torch_dist` conversion step. +- **No RoPE**: `--position-embedding-type none`. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Nemotron-3-Nano-30B-A3B | 3 B / 30 B | [nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +export BASE_DIR=/root/miles_data +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir $BASE_DIR/dapo-math-17k +hf download nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 --local-dir $BASE_DIR/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 +``` + +### 3.2 No `torch_dist` conversion + +AutoBridge + the NemotronH MoE shim load the HF checkpoint directly. Both +`--hf-checkpoint` and `--ref-load` point at the HF directory: + +```bash +CKPT_ARGS=( + --hf-checkpoint $BASE_DIR/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + --ref-load $BASE_DIR/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + --save $BASE_DIR/nemotron-3-nano-30b-a3b_miles + --save-interval 20 + --megatron-to-hf-mode bridge +) +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +export BASE_DIR=/root/miles_data +bash scripts/run-nemotron-3-nano-30b-a3b.sh +``` + +The script targets 1 node × 8 GPU (H200). Default cell is `TP=2 PP=2 EP=2`. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +Default cell is `TP=2 PP=2 EP=2`. Other verified cells from the upstream PR +(10-step RL smoke, max logprob diff ≈ 0.014): + +| Cell | TP | PP | CP | EP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| **default (run script)** | 2 | 2 | 1 | 2 | 1024 | 8 (1 × 8) | +| EP=4 | 1 | 1 | 1 | 4 | 1024 | 8 | +| TP=2×EP=4+SP | 2 | 1 | 1 | 4 | 1024 | 8 | +| PP=2×EP=4 | 1 | 2 | 1 | 4 | 1024 | 8 | +| CP=2×EP=4 | 1 | 1 | 2 | 4 | 1024 | 8 | +| TP=2×PP=2×EP=2+SP | 2 | 2 | 1 | 2 | 1024 | 8 | + +`--sequence-parallel` is enabled in the run script. Activation checkpointing is on +(`--recompute-granularity full --recompute-method uniform --recompute-num-layers 1`). +`--log-probs-chunk-size 128` is required for the smoke memory budget. + +### 5.2 Algorithm + +GRPO with low-variance KL: + +```bash +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 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 + # Replay the exact rollout routing during training forward so + # train logprobs match rollout logprobs (needed for MoE). + --use-miles-router + --use-rollout-routing-replay +) +``` + +`--use-miles-router --use-rollout-routing-replay` is what keeps train and rollout +logprobs aligned for the sigmoid-routed MoE — drop them and you'll see the same +~0.28 drift the bridge shim was added to fix. + +### 5.4 Optimizer + +GPU Adam in the smoke recipe (no `--optimizer-cpu-offload`). Switch on CPU Adam if +memory pressure rises. + +### 5.5 Notable quirks + +From `scripts/models/nemotron-3-nano-30b-a3b.sh` and `scripts/run-nemotron-3-nano-30b-a3b.sh`: + +- **No `--spec`**: AutoBridge + the NemotronH shim synthesize the Megatron MoE spec from HF config. +- 128 experts, `--moe-router-topk 6`, shared expert (3712-dim). +- Routing: `--moe-router-score-function sigmoid --moe-router-pre-softmax --moe-router-topk-scaling-factor 2.5`. +- Group routing: `--moe-router-num-groups 1 --moe-router-group-topk 1` (no-op for `n_group=1`, kept for parity with HF config). +- Aux-free balancing: `--moe-router-enable-expert-bias --moe-router-load-balancing-type seq_aux_loss --moe-router-bias-update-rate 0 --moe-aux-loss-coeff 0`. +- `--moe-grouped-gemm`, `--moe-router-dtype fp32`. +- `--position-embedding-type none`, `--vocab-size 131072 --make-vocab-size-divisible-by 128`. +- `--attention-backend auto` (Mamba layers select their own kernel). + +See [Backends Beyond Megatron](../../advanced/architecture-support.md) for how the bridge +shim layers `routed_scaling_factor` / `n_group` / `topk_group` onto the Megatron provider. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) +- [FP8 & Low Precision](../../advanced/fp8-low-precision.md) diff --git a/docs/models/nemotron/nemotron-3-nano.md b/docs/models/nemotron/nemotron-3-nano.md new file mode 100644 index 00000000000..c8337e9d428 --- /dev/null +++ b/docs/models/nemotron/nemotron-3-nano.md @@ -0,0 +1,131 @@ +--- +title: Nemotron-3-Nano +description: Launch recipe for the dense NVIDIA Nemotron-3-Nano-4B (Mamba+Attention hybrid) via Megatron AutoBridge. +--- + +# Nemotron-3-Nano + +## 1. Model Introduction + +[NVIDIA Nemotron-3-Nano-4B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16) +is a dense `nemotron_h` hybrid model — interleaved Mamba and attention blocks +with squared-relu FFNs, no RoPE, and a 262 144 max position. miles wires it via +the `megatron.bridge` AutoBridge path, so there is **no `torch_dist` conversion +step**: the AutoBridge constructs the full Megatron provider from the HF +`config.json` at load time, including all Mamba-specific fields +(`mamba_num_heads`, `mamba_state_dim`, `hybrid_override_pattern`, etc.). + +**Key highlights:** + +- **Hybrid architecture**: Mamba + attention layers (`nemotron_h` family). +- **Bridge-mode load**: `--megatron-to-hf-mode bridge` — no separate Megatron checkpoint. +- **No RoPE**: `--position-embedding-type none`. +- **Vocab**: 131 072 tokens, padded to a multiple of 128. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Nemotron-3-Nano-4B | 4 B / 4 B | [nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +export BASE_DIR=/root/miles_data +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir $BASE_DIR/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir $BASE_DIR/aime-2024 +hf download nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16 --local-dir $BASE_DIR/NVIDIA-Nemotron-3-Nano-4B-BF16 +``` + +### 3.2 No `torch_dist` conversion + +AutoBridge loads the HF checkpoint directly. Both `--hf-checkpoint` and +`--ref-load` point at the HF directory, and `--megatron-to-hf-mode bridge` +turns on the bridge code path: + +```bash +CKPT_ARGS=( + --hf-checkpoint $BASE_DIR/NVIDIA-Nemotron-3-Nano-4B-BF16 + --ref-load $BASE_DIR/NVIDIA-Nemotron-3-Nano-4B-BF16 + --megatron-to-hf-mode bridge +) +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +export BASE_DIR=/root/miles_data +bash scripts/run-nemotron-3-nano-4b.sh +``` + +The script targets 1 node × 8 GPU (H100/H200). Default cell is `TP=2 PP=2`. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +The script ships a starting cell of `TP=2 PP=2`. Other verified cells (10-step RL +smoke tests, max train/rollout logprob diff): TP=2, TP=4, PP=2, CP=2, TP=2×PP=2. +Swap the `PERF_ARGS` block to switch. + +| Cell | TP | PP | CP | EP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| **default (run script)** | 2 | 2 | 1 | 1 | 9216 | 8 (1 × 8) | +| TP=2 | 2 | 1 | 1 | 1 | 9216 | 8 | +| TP=4 | 4 | 1 | 1 | 1 | 9216 | 8 | +| CP=2 | 1 | 1 | 2 | 1 | 9216 | 8 | + +`--sequence-parallel` is not enabled in the dense smoke recipe; activation +checkpointing is also off. Dense Nemotron-3-Nano has no expert parallelism. + +### 5.2 Algorithm + +GRPO with low-variance KL: + +```bash +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 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.7 +) +``` + +### 5.4 Optimizer + +GPU Adam in the smoke recipe (no `--optimizer-cpu-offload`). Switch on CPU Adam if +memory pressure rises. + +### 5.5 Notable quirks + +From `scripts/models/nemotron-3-nano-4b.sh` and `scripts/run-nemotron-3-nano-4b.sh`: + +- **No `--spec`**: the AutoBridge synthesizes the Megatron spec from HF config. +- `--position-embedding-type none` (no RoPE). +- `--vocab-size 131072 --make-vocab-size-divisible-by 128`. +- `--attention-backend auto` (the Mamba layers select their own kernel; flash-only is not safe here). +- Bridge load is required for hybrid `nemotron_h`: the AutoBridge wires `mamba_num_heads`, `mamba_state_dim`, `hybrid_override_pattern`. PP additionally needs miles' PP-unwrap shim (already on the `feat/nemotron-gemma4-rl` branch). + +See [Backends Beyond Megatron](../../advanced/architecture-support.md) for the AutoBridge wiring. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) diff --git a/docs/models/nemotron/nemotron-3-super.md b/docs/models/nemotron/nemotron-3-super.md new file mode 100644 index 00000000000..bf76d880f2e --- /dev/null +++ b/docs/models/nemotron/nemotron-3-super.md @@ -0,0 +1,158 @@ +--- +title: Nemotron-3-Super +description: Launch recipe for NVIDIA Nemotron-3-Super-120B-A12B-FP8 (Mamba+Attention+MoE hybrid, FP8 native) via Megatron AutoBridge. +--- + +# Nemotron-3-Super + +## 1. Model Introduction + +[NVIDIA Nemotron-3-Super-120B-A12B-FP8](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8) +is the Super-tier sibling of Nemotron-3-Nano: the same `nemotron_h` block +pattern (interleaved Mamba and attention blocks, no RoPE, squared-relu FFNs) +scaled to **120 B total / 12 B active** with a sparse MoE FFN, and shipped as +an **FP8-native** checkpoint. + +miles loads it through the `megatron.bridge` AutoBridge with the shared +**NemotronH MoE bridge shim** (`miles_plugins/megatron_bridge/nemotron_h.py`) +that wires `routed_scaling_factor`, `n_group`, and `topk_group` onto the +Megatron provider — without the shim the routed output is silently scaled 1.0×, +the same drift class that affects the Nano-MoE recipe. + +**Key highlights:** + +- **Hybrid + MoE**: Mamba + attention + sparse MoE in the `nemotron_h` family. +- **FP8 native**: weights ship in FP8; load + train without an offline upcast. +- **Sigmoid routing** with per-token group selection, aux-free expert-bias load balancing. +- **Bridge-mode load**: `--megatron-to-hf-mode bridge` — no `torch_dist` conversion step. +- **No RoPE**: `--position-embedding-type none`. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Nemotron-3-Super-120B-A12B-FP8 | 12 B / 120 B | [nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +export BASE_DIR=/root/miles_data +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir $BASE_DIR/dapo-math-17k +hf download nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8 \ + --local-dir $BASE_DIR/NVIDIA-Nemotron-3-Super-120B-A12B-FP8 +``` + +### 3.2 No `torch_dist` conversion + +AutoBridge + the NemotronH MoE shim load the FP8 HF checkpoint directly. Both +`--hf-checkpoint` and `--ref-load` point at the HF directory: + +```bash +CKPT_ARGS=( + --hf-checkpoint $BASE_DIR/NVIDIA-Nemotron-3-Super-120B-A12B-FP8 + --ref-load $BASE_DIR/NVIDIA-Nemotron-3-Super-120B-A12B-FP8 + --save $BASE_DIR/nemotron-3-super-120b-a12b_miles + --save-interval 20 + --megatron-to-hf-mode bridge +) +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +export BASE_DIR=/root/miles_data +bash scripts/run-nemotron-3-super-120b-a12b.sh +``` + +The script targets **2 nodes × 8 GPU (H200, FP8)**. Default cell is +`TP=4 PP=2 EP=8`. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +Default cell is `TP=4 PP=2 EP=8` on 16 GPUs. The 120B-A12B footprint requires +either a wider EP fan-out or PP=2 to fit the activation memory of the hybrid +Mamba+Attention stack at the FP8 weight resolution. + +| Cell | TP | PP | CP | EP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| **default (run script)** | 4 | 2 | 1 | 8 | 1024 | 16 (2 × 8) | +| TP=4×EP=8 (1 node) | 4 | 1 | 1 | 8 | 1024 | 8 (1 × 8) | +| TP=2×PP=2×EP=8 + SP | 2 | 2 | 1 | 8 | 1024 | 16 (2 × 8) | + +`--sequence-parallel` is enabled in the run script. Activation checkpointing is +on (`--recompute-granularity full --recompute-method uniform +--recompute-num-layers 1`). `--log-probs-chunk-size 128` keeps the smoke +memory budget intact for FP8. + +### 5.2 Algorithm + +GRPO with low-variance KL — same defaults as Nano-MoE: + +```bash +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 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 2 + --sglang-mem-fraction-static 0.7 + # Replay the exact rollout routing during training forward so + # train logprobs match rollout logprobs (needed for sigmoid-routed MoE). + --use-miles-router + --use-rollout-routing-replay +) +``` + +`--use-miles-router --use-rollout-routing-replay` keeps train and rollout +logprobs aligned for the sigmoid-routed MoE — the same routing-replay rule that +Nano-MoE uses. + +### 5.4 Optimizer + +CPU Adam (`--optimizer-cpu-offload`) is the default for the 120B-A12B smoke +recipe; the FP8 weights save GPU memory but the Adam states still dominate at +this scale. + +### 5.5 Notable quirks + +- **FP8 native load**: the HF checkpoint is FP8; the bridge passes the + per-block scales through to Megatron — no offline upcast step. +- **No `--spec`**: AutoBridge + the NemotronH shim synthesize the Megatron MoE + spec from HF config. +- Routing follows the family default: `--moe-router-score-function sigmoid + --moe-router-pre-softmax --moe-router-topk-scaling-factor 2.5`. +- Aux-free balancing: `--moe-router-enable-expert-bias + --moe-router-load-balancing-type seq_aux_loss + --moe-router-bias-update-rate 0 --moe-aux-loss-coeff 0`. +- `--moe-grouped-gemm`, `--moe-router-dtype fp32`. +- `--position-embedding-type none`, `--vocab-size 131072 + --make-vocab-size-divisible-by 128`. +- `--attention-backend auto` (Mamba layers select their own kernel). + +See [Backends Beyond Megatron](../../advanced/architecture-support.md) for how +the bridge shim layers `routed_scaling_factor` / `n_group` / `topk_group` onto +the Megatron provider, and [FP8 & Low Precision](../../advanced/fp8-low-precision.md) +for the FP8 weight format. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [FP8 & Low Precision](../../advanced/fp8-low-precision.md) +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) diff --git a/docs/models/qwen/index.md b/docs/models/qwen/index.md new file mode 100644 index 00000000000..00e1e98524b --- /dev/null +++ b/docs/models/qwen/index.md @@ -0,0 +1,39 @@ +--- +title: Qwen +description: Miles recipes for the full Qwen3, Qwen3.5, and Qwen3-Next line — dense and MoE. +--- + +# Qwen family + +Miles ships ready-to-run RL recipes for every generation of the Qwen line: the dense Qwen3 series (0.6 B → 32 B), the Qwen3.5 family with its gated-attention architecture, the Qwen3 and Qwen3.5 MoE variants, and the Gated-Delta-Net Qwen3-Next-80B-A3B. + +## Variants + +| Family | Class | Sizes | Recipe | +|---|---|---|---| +| Qwen3 | Dense | 0.6 B · 1.7 B · 4 B · 8 B · 14 B · 32 B | [qwen3](qwen3.md) | +| Qwen3 | MoE | 3 B / 30 B · 22 B / 235 B | [qwen3-moe](qwen3-moe.md) | +| Qwen3.5 | Dense | 4 B · 9 B · 27 B | [qwen3-5](qwen3-5.md) | +| Qwen3.5 | MoE | 3 B / 35 B | [qwen3-5-moe](qwen3-5-moe.md) | +| Qwen3-Next | MoE (GDN) | 3 B / 80 B | [qwen3-next](qwen3-next.md) | + +## Fastest path to train + +Qwen3-4B on a single 8× H100 node — the canonical starter recipe: + +```bash +cd /root/miles +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B +bash scripts/run-qwen3-4B.sh +``` + +Dataset is [DAPO-Math-17k](https://huggingface.co/datasets/BytedTsinghua-SIA/DAPO-Math-17K) at `/root/dapo-math-17k/dapo-math-17k.jsonl`. See the [Qwen3 Dense](qwen3.md) page for the full walkthrough, weight conversion, and variants. + +## Which variant do I pick? + +- **Learning Miles for the first time** → Qwen3-4B ([qwen3](qwen3.md)). Fits one H100 node, fast loop. +- **Need MoE on a single node** → Qwen3-30B-A3B ([qwen3-moe](qwen3-moe.md)). +- **Scaling to multi-node** → Qwen3-235B-A22B ([qwen3-moe](qwen3-moe.md)). +- **Latest dense architecture (gated attention, A\_log FP32)** → Qwen3.5-4B ([qwen3-5](qwen3-5.md)). +- **Hybrid MTP / speculative decoding experiments** → Qwen3.5-35B-A3B ([qwen3-5-moe](qwen3-5-moe.md)). +- **Gated-Delta-Net (fla backend, real-CP)** → Qwen3-Next-80B-A3B ([qwen3-next](qwen3-next.md)). diff --git a/docs/models/qwen/qwen3-5-moe.md b/docs/models/qwen/qwen3-5-moe.md new file mode 100644 index 00000000000..a62c07fbd2b --- /dev/null +++ b/docs/models/qwen/qwen3-5-moe.md @@ -0,0 +1,110 @@ +--- +title: Qwen3.5 MoE +description: Launch recipe for Qwen3.5-35B-A3B with MTP training and EAGLE speculative rollout. +--- + +# Qwen3.5 MoE + +## 1. Model Introduction + +[Qwen3.5-35B-A3B](https://github.com/QwenLM/Qwen3) is the MoE branch of the Qwen3.5 line — 3 B active / 35 B total — combining the gated-attention architecture with a built-in MTP head. + +**Key highlights:** + +- **Sparse MoE**: 3 B active out of 35 B total parameters. +- **Attention-output gate**: shared with the Qwen3.5 dense series, with FP32-preserved `A_log`. +- **Multi-Token Prediction (MTP)**: `--mtp-num-layers 1` baked into the model config; the recipe trains the MTP head and uses EAGLE speculative decoding at rollout. +- **Single-node footprint**: full recipe fits on 1 × 8 GPU. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Qwen3.5-35B-A3B | 3 B / 35 B | [Qwen/Qwen3.5-35B-A3B](https://huggingface.co/Qwen/Qwen3.5-35B-A3B) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +# Place the model checkpoint at /root/Qwen3.5-35B-A3B +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/qwen3.5-35B-A3B.sh +PYTHONPATH=/root/Megatron-LM torchrun --nproc-per-node 8 \ + tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen3.5-35B-A3B \ + --save /root/Qwen3.5-35B-A3B_torch_dist \ + --mtp-num-layers 1 +``` + +`--mtp-num-layers 1` during conversion preserves the MTP layer so it survives into Megatron format. + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +bash scripts/run-qwen3.5-35B-A3B-mtp.sh +``` + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| TP | PP | CP | EP | expert-TP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| 1 | 1 | 1 | 8 | 1 | 8192 | 8 (1 × 8) | + +### 5.2 Algorithm + +GRPO with `--eps-clip 0.2 --eps-clip-high 0.28 --use-kl-loss --kl-loss-coef 0.00`. Plus MTP training: + +```bash +MTP_ARGS=( + --enable-mtp-training + --mtp-num-layers 1 + --mtp-loss-scaling-factor 0.2 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-mem-fraction-static 0.7 + --sglang-ep-size 8 + --sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) + + # mtp speculative decoding + --sglang-speculative-algorithm EAGLE + --sglang-speculative-num-steps 2 + --sglang-speculative-eagle-topk 1 + --sglang-speculative-num-draft-tokens 3 + + --sglang-max-running-requests 512 +) +``` + +### 5.4 Optimizer + +CPU Adam is enabled (`--optimizer-cpu-offload --overlap-cpu-optimizer-d2h-h2d --use-precision-aware-optimizer`). + +### 5.5 Notable quirks + +- The Megatron side uses `--moe-token-dispatcher-type flex`; DeepEP isn't enabled here, unlike Qwen3-Next. +- The model config (`scripts/models/qwen3.5-35B-A3B.sh`) reuses the Qwen3.5 spec: `--attention-output-gate`, `--rotary-base 10000000`, `--rotary-percent 0.25`, `A_log` kept in FP32 via the bridge. See [Backends Beyond Megatron](../../advanced/architecture-support.md). + +## 6. Pairs Well With + +- [Speculative Decoding](../../advanced/speculative-decoding.md) +- [Backends Beyond Megatron](../../advanced/architecture-support.md) diff --git a/docs/models/qwen/qwen3-5.md b/docs/models/qwen/qwen3-5.md new file mode 100644 index 00000000000..793cd6995df --- /dev/null +++ b/docs/models/qwen/qwen3-5.md @@ -0,0 +1,112 @@ +--- +title: Qwen3.5 +description: Launch recipes for Qwen3.5-4B / 9B / 27B with attention-output-gate. +--- + +# Qwen3.5 + +## 1. Model Introduction + +[Qwen3.5](https://github.com/QwenLM/Qwen3) is the next iteration of the Qwen3 dense series, introducing the gated-attention architecture and an FP32-preserved `A_log` parameter. + +**Key highlights:** + +- **Attention-output gate**: a learned gate on the attention output, trained alongside attention weights for stronger long-context behaviour. +- **Extended rotary base**: `--rotary-base 10000000`, `--rotary-percent 0.25` — wider effective context than the original Qwen3. +- **Larger vocabulary**: 248320 tokens. +- **FP32 `A_log` preservation**: a parameter that must stay in FP32 through Megatron's mixed-precision pipeline; miles handles this via the bridge. + +## 2. Supported Variants + +| Model | HF ID | +|---|---| +| Qwen3.5-4B | [Qwen/Qwen3.5-4B](https://huggingface.co/Qwen/Qwen3.5-4B) | +| Qwen3.5-9B | [Qwen/Qwen3.5-9B](https://huggingface.co/Qwen/Qwen3.5-9B) | +| Qwen3.5-27B | [Qwen/Qwen3.5-27B](https://huggingface.co/Qwen/Qwen3.5-27B) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +# Place the model checkpoint at /root/Qwen3.5-{4B,9B,27B} +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/qwen3.5-4B.sh # or qwen3.5-9B.sh / qwen3.5-27B.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen3.5-4B \ + --save /root/Qwen3.5-4B_torch_dist +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +bash scripts/run-qwen3.5-4B.sh # or run-qwen3.5-9B.sh / run-qwen3.5-27B.sh +``` + +All three scripts are 1 node × 8 GPU. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| Script | TP | PP | CP | `max_tokens_per_gpu` | SGLang `mem-fraction-static` | CPU Adam | GPUs | +|---|---|---|---|---|---|---|---| +| `scripts/run-qwen3.5-4B.sh` | 2 | 1 | 1 | 9216 | 0.7 | – | 8 (1 × 8) | +| `scripts/run-qwen3.5-9B.sh` | 2 | 1 | 1 | 9216 | 0.6 | – | 8 (1 × 8) | +| `scripts/run-qwen3.5-27B.sh` | 4 | 1 | 1 | 8192 | 0.5 | ✓ | 8 (1 × 8) | + +### 5.2 Algorithm + +GRPO across all three sizes: + +```bash +GRPO_ARGS=( + --advantage-estimator grpo + --use-kl-loss + --kl-loss-coef 0.00 + --eps-clip 0.2 + --eps-clip-high 0.28 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 +) +``` + +All three scripts pin `--rollout-num-gpus-per-engine 1` because SGLang TP > 1 produced garbage output for Qwen3.5 on 0.5.9 (in-source comment, [sglang#21039](https://github.com/sgl-project/sglang/issues/21039)). If you bump the SGLang version past the fix, you can raise this back up. + +### 5.4 Optimizer + +Only the 27 B script enables CPU Adam (`--optimizer-cpu-offload --overlap-cpu-optimizer-d2h-h2d --use-precision-aware-optimizer`). The 4 B and 9 B recipes leave Adam on GPU. + +### 5.5 Notable quirks + +From `scripts/models/qwen3.5-4B.sh` (and analogous configs for 9 B / 27 B): + +- `--spec miles_plugins.models.qwen3_5 get_qwen3_5_spec` — attention-output gate, `A_log` parameter handling. +- `--rotary-base 10000000`, `--rotary-percent 0.25`. +- `--vocab-size 248320`. +- `--apply-layernorm-1p`, `--qk-layernorm`, `--group-query-attention`. +- `--attention-output-gate`. + +See [Backends Beyond Megatron](../../advanced/architecture-support.md) for how miles preserves FP32 parameters like `A_log` through Megatron's mixed-precision pipeline. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [Low Precision RL](../../advanced/fp8-low-precision.md) diff --git a/docs/models/qwen/qwen3-6-moe.md b/docs/models/qwen/qwen3-6-moe.md new file mode 100644 index 00000000000..43d34303438 --- /dev/null +++ b/docs/models/qwen/qwen3-6-moe.md @@ -0,0 +1,157 @@ +--- +title: Qwen3.6 MoE +description: Launch recipe for Qwen3.6-35B-A3B with MTP training and EAGLE speculative rollout. +--- + +# Qwen3.6 MoE + +## 1. Model Introduction + +[Qwen3.6-35B-A3B](https://github.com/QwenLM/Qwen3) is the sparse MoE branch of +Alibaba's Qwen3.6 line — 35 B total / 3 B active parameters on a Gated Delta +Networks backbone. Like the dense Qwen3.6-27B, it's tuned for agentic-coding +workflows and long-session reasoning, with native hybrid thinking mode, +built-in tool calling, and multimodal text / image / video input. Context +reaches 262 K and extends past 1 M; weights are Apache 2.0 in BF16 and FP8. +Qwen3.6 also ships native Multi-Token Prediction for speculative decoding, +which this recipe trains and serves via EAGLE. + +In miles, Qwen3.6-35B-A3B reuses the Qwen3.5 spec +(`miles_plugins.models.qwen3_5.get_qwen3_5_spec`) and bakes in MTP training +plus a shared-expert gate. + +**Key highlights:** + +- **Sparse MoE on a GDN backbone**: 256 experts, top-8 routing, 3 B active / 35 B total. +- **Attention-output gate**: shared with the Qwen3.5 / 3.6 dense series. +- **Shared expert + gate**: `--moe-shared-expert-intermediate-size 512 --moe-shared-expert-gate`. +- **Multi-Token Prediction (MTP)**: `--mtp-num-layers 1`; trained alongside the policy and served via EAGLE at rollout. +- **Dispatcher**: `--moe-token-dispatcher-type alltoall` for HF→Megatron conversion; runtime uses `flex` (set in the launcher). +- **Long context**: 262 K tokens, extensible past 1 M. +- **Single-node footprint**: full recipe fits on 1 × 8 GPU (H200). + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Qwen3.6-35B-A3B | 3 B / 35 B | [Qwen/Qwen3.6-35B-A3B](https://huggingface.co/Qwen/Qwen3.6-35B-A3B) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +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 +hf download Qwen/Qwen3.6-35B-A3B --local-dir /root/models/Qwen3.6-35B-A3B +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/qwen3.6-35B-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.6-35B-A3B \ + --save /root/models/Qwen3.6-35B-A3B_torch_dist \ + --mtp-num-layers 1 +``` + +`--mtp-num-layers 1` during conversion preserves the MTP layer so it survives into Megatron format. + +## 4. Launch + +### 4.1 Quick start + +The launcher is a parametrized Typer script (8 × H200) that exercises arbitrary +(TP, EP, CP, PP, ETP) cells: + +```bash +cd /root/miles +python scripts/run_qwen3_6_35b_a3b_mtp.py \ + --tp 1 --ep 8 --cp 1 --pp 1 --etp 1 \ + --num-rollout 10 +``` + +Default knobs in the launcher: `--mode debug_minimal`, 8 GPUs, `max_tokens_per_gpu=8192`, +`rollout_batch_size=8`, `n_samples_per_prompt=2`, `global_batch_size=16`, +`rollout_max_response_len=1024`. Override via flags for longer runs. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +The default cell is `TP=1 EP=8 CP=1 PP=1 ETP=1`. Sequence parallelism is on; activation +checkpointing defaults on (`--recompute-granularity full --recompute-method uniform --recompute-num-layers 1`) +and can be turned off with `--no-recompute`. + +| TP | PP | CP | EP | expert-TP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| 1 | 1 | 1 | 8 | 1 | 8192 | 8 (1 × 8) | + +### 5.2 Algorithm + +GRPO with low-variance KL plus MTP training: + +```bash +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 +) + +MTP_ARGS=( + --enable-mtp-training + --mtp-num-layers 1 + --mtp-loss-scaling-factor 0.2 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-mem-fraction-static 0.7 + --sglang-ep-size 8 + --sglang-cuda-graph-bs 1 2 4 8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 + + # MTP speculative decoding + --sglang-speculative-algorithm EAGLE + --sglang-speculative-num-steps 2 + --sglang-speculative-eagle-topk 1 + --sglang-speculative-num-draft-tokens 3 + + --sglang-max-running-requests 256 + --sglang-mamba-scheduler-strategy extra_buffer +) +``` + +### 5.4 Optimizer + +CPU Adam is enabled (`--optimizer-cpu-offload --overlap-cpu-optimizer-d2h-h2d --use-precision-aware-optimizer`). + +### 5.5 Notable quirks + +From `scripts/models/qwen3.6-35B-A3B.sh` and `scripts/run_qwen3_6_35b_a3b_mtp.py`: + +- `--spec miles_plugins.models.qwen3_5 get_qwen3_5_spec` — Qwen3.6 reuses the Qwen3.5 spec. +- 256 experts, `--moe-router-topk 8`, `--moe-router-score-function softmax`. +- `--moe-shared-expert-gate` and `--moe-shared-expert-intermediate-size 512`. +- Megatron-side dispatcher overridden to `--moe-token-dispatcher-type flex` at runtime; conversion uses `alltoall`. +- `--moe-grouped-gemm`, `--moe-token-drop-policy probs`, `--moe-router-dtype fp32`, `--moe-permute-fusion`, `--moe-aux-loss-coeff 0`. +- `--attention-output-gate`, `--rotary-base 10000000`, `--rotary-percent 0.25`, `--vocab-size 248320`. + +See [Backends Beyond Megatron](../../advanced/architecture-support.md) for FP32 parameter handling and how miles wires the spec. + +## 6. Pairs Well With + +- [Speculative Decoding](../../advanced/speculative-decoding.md) +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [P2P Weight Transfer](../../advanced/p2p-weight-transfer.md) diff --git a/docs/models/qwen/qwen3-6.md b/docs/models/qwen/qwen3-6.md new file mode 100644 index 00000000000..0a460966ec1 --- /dev/null +++ b/docs/models/qwen/qwen3-6.md @@ -0,0 +1,130 @@ +--- +title: Qwen3.6 +description: Launch recipe for the dense Qwen3.6-27B with attention-output-gate. +--- + +# Qwen3.6 + +## 1. Model Introduction + +[Qwen3.6](https://github.com/QwenLM/Qwen3) is the next iteration of Alibaba's +Qwen3 line, focused on agentic-coding workflows and on preserving reasoning +context across long sessions. The family ships two variants — a sparse MoE +(Qwen3.6-35B-A3B) and a dense GDN-backbone model (Qwen3.6-27B) — both with +native hybrid reasoning (thinking by default), built-in tool calling, and +multimodal text / image / video input. Context windows reach 262 K and +extend past 1 M. Weights are Apache 2.0, available in BF16 and FP8. + +The dense **Qwen3.6-27B** is the single-GPU-friendly variant. In miles it +reuses the Qwen3.5 Megatron spec +(`miles_plugins.models.qwen3_5.get_qwen3_5_spec`); architecturally it's a +wider, deeper Qwen3.5 with the gated-attention design preserved. + +**Key highlights:** + +- **Dense GDN backbone**: 27 B parameters, single-GPU friendly footprint. +- **Attention-output gate**: shared with Qwen3.5, trained alongside attention weights. +- **Extended rotary base**: `--rotary-base 10000000`, `--rotary-percent 0.25`. +- **Larger vocabulary**: 248320 tokens. +- **Shape**: `hidden-size 5120`, `ffn-hidden-size 17408`, 64 layers. +- **Long context**: 262 K tokens, extensible past 1 M. + +## 2. Supported Variants + +| Model | HF ID | +|---|---| +| Qwen3.6-27B | [Qwen/Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +# Place the model checkpoint at /root/Qwen3.6-27B +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/qwen3.6-27B.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen3.6-27B \ + --save /root/Qwen3.6-27B_torch_dist +``` + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +bash scripts/run-qwen3.6-27B.sh +``` + +The script targets 1 node × 8 GPU. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| TP | PP | CP | EP | `max_tokens_per_gpu` | SGLang `mem-fraction-static` | CPU Adam | GPUs | +|---|---|---|---|---|---|---|---| +| 4 | 1 | 1 | 1 | 8192 | 0.5 | ✓ | 8 (1 × 8) | + +`--sequence-parallel` is enabled. Activation checkpointing is on +(`--recompute-granularity full --recompute-method uniform --recompute-num-layers 1`). + +### 5.2 Algorithm + +GRPO with low-variance KL: + +```bash +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 +) +``` + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 1 + --sglang-mem-fraction-static 0.5 +) +``` + +`--rollout-num-gpus-per-engine 1` follows the Qwen3.5 line; SGLang TP > 1 has been +problematic on this family. If your SGLang version carries the fix for +[sglang#21039](https://github.com/sgl-project/sglang/issues/21039), you can raise it. + +### 5.4 Optimizer + +CPU Adam is enabled (`--optimizer-cpu-offload --overlap-cpu-optimizer-d2h-h2d --use-precision-aware-optimizer`). + +### 5.5 Notable quirks + +From `scripts/models/qwen3.6-27B.sh`: + +- `--spec miles_plugins.models.qwen3_5 get_qwen3_5_spec` — Qwen3.6 reuses the Qwen3.5 spec (gated attention, FP32 `A_log`). +- `--rotary-base 10000000`, `--rotary-percent 0.25`. +- `--vocab-size 248320`. +- `--apply-layernorm-1p`, `--qk-layernorm`, `--group-query-attention`. +- `--attention-output-gate`. + +See [Backends Beyond Megatron](../../advanced/architecture-support.md) for how miles +preserves FP32 parameters like `A_log` through Megatron's mixed-precision pipeline. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [FP8 & Low Precision](../../advanced/fp8-low-precision.md) diff --git a/docs/models/qwen/qwen3-moe.md b/docs/models/qwen/qwen3-moe.md new file mode 100644 index 00000000000..1af4c86c473 --- /dev/null +++ b/docs/models/qwen/qwen3-moe.md @@ -0,0 +1,144 @@ +--- +title: Qwen3 MoE +description: Launch recipes for Qwen3-30B-A3B (single node) and Qwen3-235B-A22B (multi-node). +--- + +# Qwen3 MoE + +## 1. Model Introduction + +[Qwen3 MoE](https://github.com/QwenLM/Qwen3) is the Mixture-of-Experts branch of the Qwen3 series, available in two sizes: 30 B-A3B (single-node) and 235 B-A22B (multi-node). + +**Key highlights:** + +- **Sparse MoE architecture**: 30 B / 3 B-active and 235 B / 22 B-active variants, scaling capacity without proportional compute cost. +- **Strong reasoning and coding**: shares the Qwen3 generation's improvements in instruction following, math, and tool usage. +- **Long-context capability**: 256 K-token context inherited from the Qwen3 series. +- **Flexible scaling**: 30 B fits a single 8-GPU node; 235 B is the canonical multi-node target with FP8 rollout. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Qwen3-30B-A3B | 3 B / 30 B | [Qwen/Qwen3-30B-A3B](https://huggingface.co/Qwen/Qwen3-30B-A3B) | +| Qwen3-235B-A22B | 22 B / 235 B | [Qwen/Qwen3-235B-A22B](https://huggingface.co/Qwen/Qwen3-235B-A22B) | + +## 3. Environment Setup + +### 3.1 Required env vars + +The 235 B bash launcher requires: + +```bash +export BASE_FOLDER= +export MASTER_ADDR= +``` + +The 30 B Python launcher reads no env vars — pass options via the Typer CLI. + +### 3.2 Download model + datasets + +```bash +# 30 B (single node) +hf download Qwen/Qwen3-30B-A3B --local-dir /root/models/Qwen3-30B-A3B +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 + +# 235 B (multi-node, FP8 by default) +hf download Qwen/Qwen3-235B-A22B-FP8 --local-dir $BASE_FOLDER/Qwen3-235B-A22B-FP8 +``` + +### 3.3 HF → Megatron `torch_dist` conversion + +```bash +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/models/Qwen3-30B-A3B_torch_dist +``` + +Drive the conversion across more GPUs / nodes for the 235 B variant; the launcher reads `$BASE_FOLDER/Qwen3-235B-A22B_torch_dist/` as `--ref-load`. + +## 4. Launch + +### 4.1 Quick start + +```bash +# 30 B (1 node × 8 GPU) — Python launcher handles download + conversion + submit +cd /root/miles +python scripts/run_qwen3_30b_a3b.py + +# 235 B (8 nodes × 8 GPU) +export BASE_FOLDER=...; export MASTER_ADDR=... +bash scripts/run-qwen3-235B-A22B.sh +``` + +### 4.2 Multi-node fan-out + +`run-qwen3-235B-A22B.sh` ssh-fans-out to workers via `/root/mpi_rack_hostfile` itself; you only need the env vars set on the head node. The 30 B launcher is single-node. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| Script | Backend | TP | PP | CP | EP | expert-TP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---|---|---| +| `run_qwen3_30b_a3b.py` (H100, 1 node) | Megatron | 4 | 1 | 1 | 8 | 1 | 32768 | 8 (1 × 8) | +| `run-qwen3-235B-A22B.sh` | Megatron | 4 | 4 | 2 | 16 | 1 | 16384 | 64 (8 × 8) | +| `run-qwen3-235B-A22B-sft.sh` | Megatron | 4 | 1 | 1 | 32 | 1 | 9216 | 32 (4 × 8) | + +`run-qwen3-235B-A22B.sh` sets `--decoder-last-pipeline-num-layers 22` to balance the layer count across PP=4. + +### 5.2 Algorithm + +- **30 B Python launcher**: GRPO with `--eps-clip 0.2 --eps-clip-high 0.28`. +- **235 B bash launcher**: GSPO (`--advantage-estimator gspo`, `--eps-clip 4e-4`); `--use-kl-loss` is commented out. + +### 5.3 Rollout & SGLang + +`run_qwen3_30b_a3b.py` (H100, 1 node, BF16 rollout): + +```bash +--rollout-num-gpus-per-engine 8 +--sglang-mem-fraction-static 0.7 +--sglang-cuda-graph-max-bs 512 +``` + +`run-qwen3-235B-A22B.sh`: + +```bash +--rollout-num-gpus-per-engine 32 +--sglang-mem-fraction-static 0.7 +--sglang-enable-dp-attention +--sglang-dp-size 4 +--sglang-ep-size 32 +--sglang-enable-dp-lm-head +--sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256) +--sglang-moe-a2a-backend deepep +--sglang-deepep-mode auto +``` + +### 5.4 Optimizer + +Both `run_qwen3_30b_a3b.py` (H100, 1 node) and `run-qwen3-235B-A22B.sh` enable CPU Adam: + +```bash +--optimizer-cpu-offload +--overlap-cpu-optimizer-d2h-h2d +--use-precision-aware-optimizer +``` + +`run_qwen3_30b_a3b.py` removes them when running on Blackwell (`B200/B300/GB200/GB300`) per the hardware match in the launcher. + +### 5.5 Notable quirks + +- **30 B Python launcher** supports FP8 / MXFP8 / INT4 rollout, Blackwell hardware, Megatron-bridge mode, and MIS via Typer flags. +- **235 B defaults to FP8 HF checkpoint** — the BF16 directory is available as a commented alternative in `CKPT_ARGS`. +- **R3 not on by default**; opt-in via `run_qwen3_30b_a3b.py --enable-mis` (TIS / RS) for routing-stability experiments. + +## 6. Pairs Well With + +- [Low Precision RL](../../advanced/fp8-low-precision.md) +- [Rollout Routing Replay (R3)](../../advanced/miles-router.md) diff --git a/docs/models/qwen/qwen3-next.md b/docs/models/qwen/qwen3-next.md new file mode 100644 index 00000000000..dc23260875d --- /dev/null +++ b/docs/models/qwen/qwen3-next.md @@ -0,0 +1,125 @@ +--- +title: Qwen3-Next 80B-A3B +description: Launch recipes for Qwen3-Next-80B-A3B-Thinking on Megatron and FSDP backends. +--- + +# Qwen3-Next 80B-A3B + +## 1. Model Introduction + +[Qwen3-Next](https://huggingface.co/collections/Qwen/qwen3-next) is Alibaba's next-generation Qwen architecture, swapping classical attention for a hybrid Gated DeltaNet + Full Attention design. + +**Key highlights:** + +- **Hybrid Attention**: combines Gated DeltaNet (linear attention) with Full Attention to handle context lengths up to 262 K tokens efficiently. +- **Highly Sparse MoE**: 80 B total / 3 B active per token — drastically reduces FLOPs per token without sacrificing model capacity. +- **Multi-Token Prediction (MTP)**: built-in MTP layer enables EAGLE-style speculative rollout out of the box. +- **HuggingFace-wrapped Megatron backend**: miles loads the `Qwen/Qwen3-Next-80B-A3B` HF module as a Megatron stage without re-implementing GDN from scratch. + +## 2. Supported Variants + +| Model | Active / Total | HF ID | +|---|---|---| +| Qwen3-Next-80B-A3B-Thinking | 3 B / 80 B | [Qwen/Qwen3-Next-80B-A3B-Thinking](https://huggingface.co/Qwen/Qwen3-Next-80B-A3B-Thinking) | + +## 3. Environment Setup + +### 3.1 Required env vars + +```bash +export BASE_FOLDER= +export MASTER_ADDR= +``` + +All three launchers (`run-qwen3-next-80B-A3B.sh`, `run-qwen3-next-80B-A3B-8gpus.sh`, `run-qwen3-next-80B-A3B-fsdp.sh`) hard-fail if these aren't set. + +### 3.2 Download model + datasets + +```bash +hf download Qwen/Qwen3-Next-80B-A3B-Thinking --local-dir $BASE_FOLDER/Qwen3-Next-80B-A3B-Thinking +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir $BASE_FOLDER/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir $BASE_FOLDER/aime-2024 +``` + +### 3.3 HF → Megatron `torch_dist` conversion + +```bash +cd /root/miles +source scripts/models/qwen3-next-80B-A3B.sh +PYTHONPATH=/root/Megatron-LM torchrun --nproc-per-node 8 \ + tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint $BASE_FOLDER/Qwen3-Next-80B-A3B-Thinking \ + --save $BASE_FOLDER/Qwen3-Next-80B-A3B-Thinking_torch_dist +``` + +The FSDP variant skips this step and loads HF directly. + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +export BASE_FOLDER=...; export MASTER_ADDR=... +bash scripts/run-qwen3-next-80B-A3B.sh +``` + +### 4.2 Multi-node fan-out + +`run-qwen3-next-80B-A3B.sh` performs ssh fan-out internally — set `BASE_FOLDER` / `MASTER_ADDR` on the head node and the launcher reaches out to the workers. The 8-GPU and FSDP variants are single-node. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| Script | Backend | TP | PP | CP | EP | expert-TP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---|---|---| +| `scripts/run-qwen3-next-80B-A3B.sh` | Megatron | 2 | 4 | 2 | 8 | 1 | 8192 | 32 (4 × 8) | + +### 5.2 Algorithm + +All three scripts use GSPO (`--advantage-estimator gspo --eps-clip 4e-4`); `--use-kl-loss` is commented out. + +### 5.3 Rollout & SGLang + +The canonical script enables EAGLE speculative rollout: + +```bash +--rollout-num-gpus-per-engine 8 +--sglang-mem-fraction-static 0.8 +--sglang-ep-size 8 +--sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 128) + +# mtp / EAGLE +--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 +``` + +The 6-GPU and FSDP variants ship the EAGLE block commented out and use `--rollout-num-gpus-per-engine 2 --rollout-num-gpus 2 --sglang-mem-fraction-static 0.8 --sglang-ep-size 1`. + +### 5.4 Optimizer + +The Megatron variants enable CPU Adam: + +```bash +--optimizer-cpu-offload +--overlap-cpu-optimizer-d2h-h2d +--use-precision-aware-optimizer +``` + +The FSDP variant leaves Adam on GPU. + +### 5.5 Notable quirks + +- Gated DeltaNet (GDN) is loaded via the HuggingFace bridge; miles doesn't re-implement GDN in Megatron native code. + +## 6. Pairs Well With + +- [Backends Beyond Megatron](../../advanced/architecture-support.md) +- [Rollout Routing Replay (R3)](../../advanced/miles-router.md) +- [Speculative Decoding](../../advanced/speculative-decoding.md) diff --git a/docs/models/qwen/qwen3.md b/docs/models/qwen/qwen3.md new file mode 100644 index 00000000000..14382b71bbb --- /dev/null +++ b/docs/models/qwen/qwen3.md @@ -0,0 +1,129 @@ +--- +title: Qwen3 +description: Launch recipes for dense Qwen3 models (0.6 B – 32 B). +--- + +# Qwen3 + +## 1. Model Introduction + +[Qwen3](https://github.com/QwenLM/Qwen3) is the latest generation of Alibaba's Qwen language model series, available in dense and MoE variants with both Instruct and reasoning-enhanced Thinking editions. + +**Key highlights:** + +- **Stronger general intelligence**: significant improvements in instruction following, logical reasoning, mathematics, science, coding, and tool usage over Qwen2.5. +- **Extended context length**: trained for 256 K-token contexts, useful for long-document reasoning and agentic workflows. +- **Flexible deployment options**: dense sizes from 0.6 B up to 32 B; this page covers the dense recipes (MoE recipes live in [qwen3-moe](qwen3-moe.md)). +- **Stronger agent interaction**: improved tool-use and search-based agent performance. + +## 2. Supported Variants + +| Model | HF ID | +|---|---| +| Qwen3-0.6B | [Qwen/Qwen3-0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) | +| Qwen3-1.7B | [Qwen/Qwen3-1.7B](https://huggingface.co/Qwen/Qwen3-1.7B) | +| Qwen3-4B | [Qwen/Qwen3-4B](https://huggingface.co/Qwen/Qwen3-4B) | +| Qwen3-4B-Instruct-2507 | [Qwen/Qwen3-4B-Instruct-2507](https://huggingface.co/Qwen/Qwen3-4B-Instruct-2507) | +| Qwen3-8B | [Qwen/Qwen3-8B](https://huggingface.co/Qwen/Qwen3-8B) | +| Qwen3-14B | [Qwen/Qwen3-14B](https://huggingface.co/Qwen/Qwen3-14B) | +| Qwen3-32B | [Qwen/Qwen3-32B](https://huggingface.co/Qwen/Qwen3-32B) | + +## 3. Environment Setup + +### 3.1 Download model + datasets + +```bash +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B +hf download --repo-type dataset zhuzilin/dapo-math-17k --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +``` + +### 3.2 HF → Megatron `torch_dist` conversion + +```bash +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/Qwen3-4B \ + --save /root/Qwen3-4B_torch_dist +``` + +The converter auto-derives PP from `WORLD_SIZE`; for larger sizes drive it with `torchrun --nproc-per-node 8`. The FSDP launcher loads the HF checkpoint directly and skips this step. + +## 4. Launch + +### 4.1 Quick start + +```bash +cd /root/miles +bash scripts/run-qwen3-4B.sh +``` + +Other variants follow the same pattern — replace the script name (`run-qwen3-32B.sh`, `run-qwen3-4B-fsdp.sh`, etc.) and the `qwen3-XB.sh` model config. + +The Qwen3-4B-Instruct-2507 config (`scripts/models/qwen3-4B-Instruct-2507.sh`) just sets `MODEL_ARGS_ROTARY_BASE=5000000` and re-sources `qwen3-4B.sh` — source it when converting / launching the Instruct-2507 checkpoint. + +## 5. Recipe Configuration + +### 5.1 Parallelism + +| Script | TP | PP | CP | EP | `max_tokens_per_gpu` | GPUs | +|---|---|---|---|---|---|---| +| `run-qwen3-4B.sh` | 2 | 1 | 1 | 1 | 9216 | 8 (1 × 8) | +| `run-qwen3-4B_4xgpu.sh` | 2 | 1 | 1 | 1 | 9216 | 4 (1 × 4) | +| `run-qwen3-32B.sh` | 8 | 1 | 1 | 1 | 20480 | 8 (1 × 8) | + +`--sequence-parallel` is on whenever TP > 1. + +### 5.2 Algorithm + +GRPO baseline across all dense recipes: + +```bash +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 +) +``` + +Rollout uses `--rm-type deepscaler` against `dapo-math-17k`. The SFT recipe (`run-qwen3-4B-base-sft.sh`) trains on `/root/openhermes2_5.parquet`. + +### 5.3 Rollout & SGLang + +```bash +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 2 + --sglang-mem-fraction-static 0.7 +) +``` + +`run-qwen3-32B.sh` additionally pins `--sglang-cuda-graph-bs 1 2 4 8 $(seq 16 8 256)`. The FSDP variant uses `--attn-implementation flash_attention_3`, SGLang attention backend `fa3`, and adds `--update-weight-buffer-size 536870912 --gradient-checkpointing`. + +### 5.4 Optimizer + +`run-qwen3-32B.sh` enables CPU Adam: + +```bash +--optimizer-cpu-offload +--overlap-cpu-optimizer-d2h-h2d +--use-precision-aware-optimizer +``` + +The 4 B / 8 B / 14 B recipes leave Adam on GPU. + +### 5.5 Notable quirks + +- **BF16 train + FP8 inference**: `run-qwen3-4B.sh:30-31` ships a commented `--hf-checkpoint /root/Qwen3-4B-FP8` alternative — uncomment it (and download `Qwen/Qwen3-4B-FP8`) to swap rollout to FP8 while keeping BF16 training. See [Low Precision RL](../../advanced/fp8-low-precision.md). +- **FSDP backend**: `run-qwen3-4B-fsdp.sh` runs the same recipe with `--train-backend fsdp`; no Megatron `torch_dist` conversion needed. +- **AMD ROCm**: `scripts/amd/run-qwen3-4B-amd.sh` mirrors the recipe with `${NUM_GPUS}` resolved from the AMD environment. + +## 6. Pairs Well With + +- [Low Precision RL](../../advanced/fp8-low-precision.md) +- [Backends Beyond Megatron](../../advanced/architecture-support.md) — for the FSDP variant. From 70859d743340dee6d39ff23d03916cdb85f6598f Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Mon, 11 May 2026 17:46:45 -0700 Subject: [PATCH 0046/1130] doc [7/n]: add docs/advanced and docs/platforms (#1103) Co-authored-by: Zhichenzzz Co-authored-by: Shi-Dong --- docs/advanced/architecture-support.md | 175 ++++++++++++++++++++++ docs/advanced/fault-tolerance.md | 85 +++++++++++ docs/advanced/fp8-low-precision.md | 204 ++++++++++++++++++++++++++ docs/advanced/index.md | 48 ++++++ docs/advanced/int4-qat.md | 97 ++++++++++++ docs/advanced/lora.md | 99 +++++++++++++ docs/advanced/miles-router.md | 68 +++++++++ docs/advanced/p2p-weight-transfer.md | 144 ++++++++++++++++++ docs/advanced/pd-disaggregation.md | 93 ++++++++++++ docs/advanced/speculative-decoding.md | 99 +++++++++++++ docs/platforms/amd.md | 96 ++++++++++++ docs/platforms/index.md | 47 ++++++ docs/platforms/nvidia.md | 92 ++++++++++++ 13 files changed, 1347 insertions(+) create mode 100644 docs/advanced/architecture-support.md create mode 100644 docs/advanced/fault-tolerance.md create mode 100644 docs/advanced/fp8-low-precision.md create mode 100644 docs/advanced/index.md create mode 100644 docs/advanced/int4-qat.md create mode 100644 docs/advanced/lora.md create mode 100644 docs/advanced/miles-router.md create mode 100644 docs/advanced/p2p-weight-transfer.md create mode 100644 docs/advanced/pd-disaggregation.md create mode 100644 docs/advanced/speculative-decoding.md create mode 100644 docs/platforms/amd.md create mode 100644 docs/platforms/index.md create mode 100644 docs/platforms/nvidia.md diff --git a/docs/advanced/architecture-support.md b/docs/advanced/architecture-support.md new file mode 100644 index 00000000000..1d680c209e2 --- /dev/null +++ b/docs/advanced/architecture-support.md @@ -0,0 +1,175 @@ +--- +title: Backends Beyond Megatron +description: Embed HuggingFace implementations as black-box modules inside Megatron's parallel pipeline. +--- + +# Backends Beyond Megatron + +Adding a new architecture (such as Qwen3-Next's Gated-Delta-Net) directly to +Megatron-LM's native code path is invasive. Miles takes a different approach: +wrap the model's official HuggingFace implementation as a black-box module and +embed it inside Megatron's parallel scheduling. This trades some throughput +ceiling (no TP inside the wrapped module) for a much shorter time-to-train +when the architecture is new. + +This page uses Qwen3-Next 80B-A3B as the running example. + +## How it works + +Megatron instantiates a model in two steps: + +1. Generate a layer specification (`ModuleSpec` / decoder block spec). +2. Instantiate concrete PyTorch modules from that spec. + +Miles intercepts step 1 and rewrites specific submodules to point at a +HuggingFace-backed implementation. Three components do the work. + +### 1. Custom decoder spec + +`miles_plugins/models/qwen3_next.py` defines `get_qwen3_next_spec`. It +starts from `get_gpt_decoder_block_spec`, then for the layers whose HF +`layer_types[i] == "linear_attention"` it overrides the layer's +`submodules.self_attention` with a `ModuleSpec(module=Attention, +params={"args": args})` (referenced from `miles_plugins/models/`): + +```python +# miles_plugins/models/qwen3_next.py (simplified excerpt) +transformer_layer_spec = get_gpt_decoder_block_spec(config, **kwargs) +... +for layer_id in range(num_layers_to_build): + if hf_config.layer_types[layer_id + offset] == "linear_attention": + layer_specs = copy.deepcopy(transformer_layer_spec.layer_specs[layer_id]) + layer_specs.submodules.self_attention = ModuleSpec( + module=Attention, + params={"args": args}, + ) + transformer_layer_spec.layer_specs[layer_id] = layer_specs +return transformer_layer_spec +``` + +The spec function is wired up via `--spec`: + +```bash +MODEL_ARGS+=( + --spec miles_plugins.models.qwen3_next get_qwen3_next_spec +) +``` + +### 2. Abstract Megatron-side wrapper + +`miles_plugins/models/hf_attention.py` defines an abstract +`HuggingfaceAttention(MegatronModule, ABC)` whose `__init__` takes +`(args, config, layer_number, cp_comm_type, pg_collection)`. It loads the +HuggingFace config from `args.hf_checkpoint` and prepares the layout +adapters Megatron's parallelism contract requires (sequence parallel, CP +zigzag/packed-shard conversions). Concrete Attention classes subclass it +and embed the actual HF attention module. + +### 3. Align weights with mbridge + +The HF parameter layout differs from Megatron's. `miles_plugins/mbridge/` +ships per-architecture bridges that reconcile the two. For Qwen3-Next: + +```python +# miles_plugins/mbridge/qwen3_next.py +@register_model("qwen3_next") +class Qwen3NextBridge(Qwen2MoEBridge): + _ATTENTION_MAPPING = ( + Qwen2MoEBridge._ATTENTION_MAPPING + | { + f"self_attention.{w}": ["model.layers.{layer_number}." + w] + for w in [ + "input_layernorm.weight", + "linear_attn.A_log", + "linear_attn.conv1d.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", + ], + } + ) +``` + +The class-level `_ATTENTION_MAPPING` (and `_MLP_MAPPING`, etc.) extends the +parent bridge with the layer-name substitutions specific to this +architecture. Bridges that need to reshape weights at conversion time +override `_weight_to_mcore_format`. See +[mbridge](https://github.com/ISEEKYAN/mbridge) for the parent bridges. + +## Capabilities and limits + +| | Patch Megatron core | Miles wrapper approach | +|---|---|---| +| Pipeline parallel | Supported | Supported | +| Sequence parallel | Supported | Supported | +| MoE acceleration | Supported | Supported | +| TP inside the wrapped module | Supported | Not supported | + +For Attention-only swaps, missing TP inside the module is usually acceptable +because Attention parameters are a small fraction of total params in MoE +models. For cases where TP inside the new module is required, the native +Megatron path is the right choice. + +## Mixed precision: keeping fp32 parameters fp32 + +Some architectures need certain parameters to remain fp32 even when the rest +of the model is bf16. Qwen3.5's `A_log` is the canonical example. Rounding it +to bf16 makes Megatron-side activations diverge from SGLang-side rollout, +causing precision drift. + +The canonical cast point is Megatron's `Float16Module`, which (per the +docstring on `enforce_marked_param_dtypes` in +`miles/backends/megatron_utils/fp32_param_utils.py`) "unconditionally casts +every floating-point parameter to bf16/fp16 at wrap time". The mbridge +weight-conversion path (`_weight_to_mcore_format` and friends) is the +other place fp32 weights can be silently downcast. Two steps are required +to keep tagged params in fp32. + +### Mark the parameter + +```python +from miles.backends.megatron_utils.fp32_param_utils import mark_param_dtype + +class MyModel(nn.Module): + def __init__(self, ...): + super().__init__(...) + self.A_log = nn.Parameter(torch.log(A).to(torch.float32)) + mark_param_dtype(self.A_log, torch.float32) +``` + +`enforce_marked_param_dtypes(model)` (already wired into the training and +checkpoint conversion entry points) restores tagged params to fp32 after +`Float16Module` casts the rest of the model to bf16. + +### Override the bridge + +```python +class Qwen3_5Bridge(Qwen2MoEBridge): + def _weight_to_mcore_format(self, mcore_weights_name, hf_weights): + if mcore_weights_name.endswith("self_attention.linear_attn.A_log"): + assert len(hf_weights) == 1 + return hf_weights[0].to(dtype=torch.float32).contiguous() + return super()._weight_to_mcore_format(mcore_weights_name, hf_weights) +``` + +If only one of these is done, the final dtype looks correct but the values +have already been rounded. + +## When this path fits + +* New architectures not yet integrated into Megatron core. +* Research models with non-standard layers (Mamba-style state space, + Gated-Delta-Net, etc.). +* Cases where the cost of patching Megatron exceeds the value of squeezing + the last few percent of throughput. + +## When native Megatron is preferable + +* Stable, frozen architectures (Qwen3 standard, GLM4) where Megatron's native + path is mature. +* Cases where TP inside the new module is critical. diff --git a/docs/advanced/fault-tolerance.md b/docs/advanced/fault-tolerance.md new file mode 100644 index 00000000000..d6bbc1d8dae --- /dev/null +++ b/docs/advanced/fault-tolerance.md @@ -0,0 +1,85 @@ +--- +title: Fault Tolerance +description: Rollout-side health checks and engine recovery, gated by --use-fault-tolerance. +--- + +# Fault Tolerance + +`--use-fault-tolerance` enables Miles's rollout-side fault-tolerance machinery. +It gates two code paths: + +1. A `RolloutHealthMonitor` thread per server group, started in + `miles/ray/rollout.py:379`, which periodically heart-beats each SGLang + engine. +2. A recovery hook in the trainer's weight-update step + (`miles/backends/megatron_utils/actor.py:500`), which restarts engines + that the health monitor has killed. + +```bash +--use-fault-tolerance +``` + +The flag is `action="store_true"`, default `False` +(`miles/utils/arguments.py:528`). + +## Health monitor + +`RolloutHealthMonitor` (`miles/utils/health_monitor.py`) runs in a daemon +thread. Lifecycle: `start` (called once during init), `pause` and `resume` +(called when engines offload / onload), `stop` (called during dispose). +`pause` / `resume` are wired up in `miles/ray/rollout.py:497, 501` and called +around offload / onload events. + +Each loop iteration does: + +1. After a `resume`, wait `--rollout-health-check-first-wait` seconds before + the first check (intended to cover model compilation and initialization). +2. For every active engine in the group, call `engine.health_generate.remote(timeout=self._check_timeout)`. +3. If the call raises, run `_kill_engine`: `engine.shutdown.remote()`, + `ray.kill(engine)`, and the engine slot is set to `None` + (`miles/utils/health_monitor.py:160-180`). +4. Sleep `--rollout-health-check-interval` seconds, then repeat. + +### Flags + +| Flag | Default | Source help text | +|---|---|---| +| `--rollout-health-check-interval` | `30.0` | "Interval in seconds between rollout engine `/health_generate` checks during generate/eval." | +| `--rollout-health-check-timeout` | `30.0` | "Timeout in seconds to wait for a rollout engine `/health_generate` response before killing it." | +| `--rollout-health-check-first-wait` | `0` | "Initial grace period (in seconds) before starting health checks. This allows time for model compilation and initialization. Increase this value significantly when using deepgemm." | + +## Engine recovery + +When `--use-fault-tolerance` is on, `MegatronActor.update_weights` calls +`rollout_manager.recover_updatable_engines` on rank 0 before each weight +update (`miles/backends/megatron_utils/actor.py:500`). + +`recover_updatable_engines` (`miles/ray/rollout.py:513`): + +1. Pauses health monitoring. +2. Calls `srv.recover()` on the updatable server. + +`srv.recover()` (`miles/ray/rollout.py:263`): + +1. Finds engine slots set to `None` (killed by the health monitor). +2. Calls `start_engines` for each affected group. +3. Releases memory occupation on the new engines. + +After `recover_updatable_engines` returns, the weight updater connects to +the new engines and the next weight transfer proceeds normally. + +## P2P weight transfer timeouts + +When `--update-weight-transfer-mode p2p` is on, every P2P transfer is +bounded by `--p2p-transfer-timeout` (default `30.0`s, defined in +`miles/utils/arguments.py:519`; consumed at +`miles/backends/megatron_utils/update_weight/update_weight_from_distributed/p2p.py:73`). +On timeout the failed transfer is logged (`[P2P] Transfer future failed: ...`) +in `p2p_transfer_utils.py`. There is no automatic retry or automatic +broadcast-mode fallback in the source today. + +## Dumper-mode interaction + +In dumper mode (`miles/utils/arguments.py:2102`), Miles forces +`use_fault_tolerance = False` and `rollout_health_check_interval = 1e18` +to keep heartbeats from firing. diff --git a/docs/advanced/fp8-low-precision.md b/docs/advanced/fp8-low-precision.md new file mode 100644 index 00000000000..fba328a789e --- /dev/null +++ b/docs/advanced/fp8-low-precision.md @@ -0,0 +1,204 @@ +--- +title: Low Precision RL +description: Unified low-precision pipelines for RL — block-wise FP8, MXFP8, and NVFP4 across rollout and training. +--- + +# Low Precision RL + +A common failure mode in MoE RL is precision drift between training and +inference. Pipelines that train in BF16 and serve in FP8 accumulate per-layer +numerical disagreement, which compounds into divergent log-probabilities and +gradients pointing in unintended directions. + +Miles supports a unified low-precision path where rollout and training share +the same quantization logic on the forward pass. Backward passes and master +weights stay in BF16. The same path is wired up for three formats today — +**block-wise FP8**, **MXFP8**, and (experimental) **NVFP4** — plus the +lower-friction "BF16 train + FP8 inference" mode that's useful when standing +up a new model architecture. + +## Choose a precision + +| Format | Block layout | Hardware | Models tested | Maturity | +|---|---|---|---|---| +| **BF16** | — | All NVIDIA + AMD MI300X / MI325 / MI350 / MI355X | All | Baseline | +| **FP8 block-wise** (DeepSeek-style) | 128×128, FP32 scales | Hopper (H100 / H200), Blackwell (B200+) | Qwen3-4B, Qwen3-30B-A3B, DeepSeek-V3 / R1 | Generally available | +| **MXFP8** | 1×32, UE8M0 scales | Blackwell only (B200, B300, GB200, GB300) | Qwen3-30B-A3B | Beta | +| **NVFP4** (E2M1) | 1×16, two-level (FP8 + FP32) scales, MoE experts only | Blackwell, following the TransformerEngine NVFP4 reference | — | Experimental | + +## Rollout × training compatibility + +Each row is a rollout (inference) precision; each column is the trainer's +forward precision. ✅ = supported; ✗ = not supported. + +| Rollout \ Train | BF16 | FP8 block-wise | MXFP8 | NVFP4 | +|---|---|---|---|---| +| **BF16** | ✅ baseline | ✗ | ✗ | ✗ | +| **FP8 block-wise** | ✅ | ✅ Hopper + Blackwell | ✗ | ✗ | +| **MXFP8** | ✅ | ✗ | ✅ Blackwell | ✗ | +| **NVFP4** | ✗ | ✗ | ✗ | 🚧 coming soon | + +Two rules enforced in the reference script +(`scripts/run_qwen3_30b_a3b.py`): + +* `--rollout-mxfp8` and `--rollout-fp8` are mutually exclusive. +* `--train-mxfp8` requires `--rollout-mxfp8` (no MXFP8-train + FP8-rollout + combo). + +## Unified training recipe + +| Stage | Typical pipeline | Miles unified low-precision | +|---|---|---| +| Rollout (forward) | FP8 / MXFP8 GEMM | FP8 / MXFP8 GEMM | +| Trainer (forward) | BF16 GEMM | FP8 / MXFP8 GEMM with matching quant config | +| Trainer (backward) | BF16 grads | BF16 backward (master weights in BF16) | +| Optimizer | BF16 master | BF16 master | + +The forward pass in training matches rollout. The backward pass and master +weights remain BF16, which keeps the gradient signal stable. Weights are +re-quantized on each weight-update sync to sglang. + +## Modes + +### 1. BF16 train + FP8 inference + +The lowest-friction path. SGLang loads FP8 weights while the trainer keeps a +BF16 `torch_dist` checkpoint. There is precision drift between the two paths; +on MoE workloads, pair this with R3 (and optionally TIS). + +```bash +hf download Qwen/Qwen3-30B-A3B-FP8 --local-dir /root/Qwen3-30B-A3B-FP8 + +CKPT_ARGS=( + --hf-checkpoint /root/Qwen3-30B-A3B-FP8 # FP8 weights for SGLang + --ref-load /root/Qwen3-30B-A3B_torch_dist # BF16 torch_dist for trainer +) +``` + +### 2. Unified block-wise FP8 (DeepSeek-style) + +Rollout and training share the same block-wise FP8 quantization. This is the +recipe to use on Hopper, and the recipe DeepSeek-V3 / DeepSeek-R1 ship in. +Block layout is 128×128 with FP32 scales. + +```bash +# Megatron / TransformerEngine +--transformer-impl transformer_engine +--bf16 +--fp8-format e4m3 +--fp8-recipe blockwise + +# Optional, for MoE numerical stability +--use-tis +``` + +| Flag | Effect | +|---|---| +| `--transformer-impl transformer_engine` | Routes Megatron's forward through TransformerEngine so FP8 GEMM is engaged. | +| `--fp8-format e4m3` | Forward FP8 format used by TransformerEngine. | +| `--fp8-recipe blockwise` | 128×128 block-wise quantization; sglang must serve weights in the matching layout. | +| `--use-tis` | Truncated Importance Sampling for residual precision drift. | + +Set `NVTE_FP8_BLOCK_SCALING_FP32_SCALES=1` in the Ray runtime env to use FP32 +scales (`miles/ray/actor_group.py` already sets this in the actor env). + +For models that already ship 128×128 block-wise FP8 weights (DeepSeek-V3, +DeepSeek-R1, `Qwen/Qwen3-30B-A3B-FP8`), point `--hf-checkpoint` at the +block-wise FP8 directory and let SGLang autodetect. Otherwise convert with +`tools/convert_hf_to_fp8.py`. + +For MoE workloads, also consider `--use-rollout-routing-replay` (R3). The +canonical recipe leaves it commented out by default but the flag is available. + +Reference recipes: + +* [`examples/low_precision/run-qwen3-4b-fp8.sh`](https://github.com/radixark/miles/blob/main/examples/low_precision/run-qwen3-4b-fp8.sh) — single-node Qwen3-4B. +* [`examples/low_precision/run-qwen3-30b-a3b-fp8-two-nodes.sh`](https://github.com/radixark/miles/blob/main/examples/low_precision/run-qwen3-30b-a3b-fp8-two-nodes.sh) — two-node Qwen3-30B-A3B. + +### 3. Unified MXFP8 (Blackwell) + +MXFP8 uses a finer block layout (1×32) with UE8M0 (power-of-two) scales packed +as `uint8`. Weights are stored as `float8_e4m3fn`. This is the format wired +into the Blackwell path of the Qwen3-30B-A3B reference script. + +**Hardware**: Blackwell only — B200, B300, GB200, GB300. The reference script +asserts the GPU class on enable (`scripts/run_qwen3_30b_a3b.py`). + +**Train flags** — same Megatron knobs as FP8, with `mxfp8` recipe: + +```bash +--transformer-impl transformer_engine +--bf16 +--fp8-format e4m3 +--fp8-recipe mxfp8 +``` + +**Rollout flags** — sglang side: + +```bash +--sglang-fp8-gemm-backend triton +--sglang-moe-runner-backend cutlass +# DeepEP / DeepGEMM are not yet supported with MXFP8 in sglang; +# do not set --sglang-moe-a2a-backend deepep here. +``` + +**Conversion**. There is no HF-shipped MXFP8 checkpoint for Qwen3 today, so +convert from BF16 first: + +```bash +python tools/convert_hf_to_mxfp8.py \ + --model-dir /root/models/Qwen3-30B-A3B \ + --save-dir /root/models/Qwen3-30B-A3B-MXFP8 +``` + +The tool quantizes every 2D `*.weight` tensor whose last dim is divisible by +32, except `layernorm`, `embed`, `router`, `mlp.gate.`, `norm`, `lm_head`, +`eh_proj`, `weights_proj` (`tools/convert_hf_to_mxfp8.py`). The HF +config is rewritten with: + +```json +{ + "quant_method": "mxfp8", + "fmt": "e4m3", + "weight_block_size": [1, 32], + "scale_fmt": "ue8m0", + "activation_scheme": "dynamic" +} +``` + +**Caveats**: + +* No DeepEP / DeepGEMM yet — MoE all-to-all uses the cutlass MoE runner, which + does not currently support EP. Plan EP/PP accordingly. +* `--train-mxfp8` requires `--rollout-mxfp8` (the script enforces this). + +**Reference recipe**: [`scripts/run_qwen3_30b_a3b.py`](https://github.com/radixark/miles/blob/main/scripts/run_qwen3_30b_a3b.py) +with `--rollout-mxfp8 --train-mxfp8 --hardware B200`. There is no dedicated +shell script under `examples/low_precision/` yet. + +### 4. NVFP4 (experimental) + +NVFP4 is FP4 E2M1 with 1D block scaling (group size 16) and a two-level scale +(per-block FP8 + per-tensor FP32), following the TransformerEngine NVFP4 +reference. Today only **MoE expert GEMMs** are quantized; dense layers stay in +their original precision. + +The full unified NVFP4 recipe is in development. + +## Hardware support + +| GPU | BF16 | FP8 block-wise | MXFP8 | NVFP4 | +|---|---|---|---|---| +| NVIDIA H100 / H200 | ✅ | ✅ | ✗ | ✗ | +| NVIDIA B200 / B300 / GB200 / GB300 | ✅ | ✅ | ✅ | 🚧 in development | +| NVIDIA A100 | ✅ | ✗ | ✗ | ✗ | +| AMD MI300X / MI325 / MI350 / MI355X | ✅ | ✗ | ✗ | ✗ | + +## When BF16 is enough + +* Dense models below ~30 B. +* A100 hardware (no FP8 GEMM). +* AMD hardware today. +* Bring-up of a new model architecture, where clean BF16 numerics simplify + debugging. + diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000000..d281ef0fea7 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,48 @@ +--- +title: Advanced Features +description: Systems-level features for large-scale and long-running RL. +--- + +# Advanced Features + +This section covers the Miles features that the Core-features section of the +homepage points at: low-precision training (FP8 / MXFP8 / INT4 QAT), Rollout +Routing Replay for MoE, speculative decoding, and LoRA training and serving. + + + + + + The unified FP8 path: matched quantization between training and inference, + BF16 backward and master weights. + + + + + + W4A16 quantization-aware training for fitting large models on a single + 8-GPU node. + + + + + + Capture expert routing during inference and replay during training. The + mechanism that keeps MoE RL stable. + + + + + + Draft + target speculative rollout, with online MTP-SFT for the draft. + + + + + + Train LoRA adapters with SFT or RL and serve them through SGLang from the + same checkpoint. + + + + diff --git a/docs/advanced/int4-qat.md b/docs/advanced/int4-qat.md new file mode 100644 index 00000000000..2e3aa043d3e --- /dev/null +++ b/docs/advanced/int4-qat.md @@ -0,0 +1,97 @@ +--- +title: INT4 Quantization-Aware Training +description: Fit large models on a single 8-GPU node by training with W4A16 quantization in the loop. +--- + +# INT4 W4A16 Quantization-Aware Training + +When the model is large enough that even FP8 will not fit on one node, the +options are spreading across more nodes (and paying cross-node bandwidth) or +quantizing further. Miles ships an INT4 W4A16 quant-aware-training pipeline. +On an 8 × 141 GB H200 node, this is the path used to fit very large models in +a single box. + +The recipe is inspired by the +[Kimi K2-Thinking](https://www.kimi.com/k2-thinking) team's report. + +## What W4A16 means + +| Term | Bits | Notes | +|---|---|---| +| W4 | 4-bit weights | Group-quantized (typical group size 32–128) | +| A16 | 16-bit activations | BF16 activation pathway | + +The combination keeps the weights small (memory-bound) while activations stay +in BF16 (math-bound). With QAT the model trains *with* the quantization in the +loop, so the weights round well during inference. + +## Calibration + +Convert a BF16 HuggingFace checkpoint to INT4 with `tools/convert_hf_to_int4.py` +(GPTQ via `llmcompressor`): + +```bash +python tools/convert_hf_to_int4.py \ + --input-dir /root/MyModel \ + --output-dir /root/MyModel-INT4 \ + --data-dir /root/calibration_dataset \ + --quant-type W4A16 \ + --num-calibration-samples 256 \ + --quant-group-size 128 +``` + +| Flag | Default | Notes | +|---|---|---| +| `--quant-type` | `W4A16` | Also accepts `W8A16`. | +| `--num-calibration-samples` | `256` | Calibration set size. | +| `--quant-group-size` | `32` | GPTQ group size; `128` is also common. | +| `--max-sequence-length` | `2048` | Calibration sequence length. | +| `--dampening-frac` | `0.01` | GPTQ damping. | +| `--trust-remote-code` | off | Pass when the HF config requires custom code. | + +The output is a HuggingFace directory with per-group INT4 weights and scales. +Point `--hf-checkpoint` at it; SGLang autodetects the quantization at load time. + +## Enabling QAT + +QAT is currently driven by environment variables passed through Ray's runtime +env rather than CLI flags. The canonical recipe is +[`examples/low_precision/run-qwen3-30B-A3B-int4.sh`](https://github.com/radixark/miles/blob/main/examples/low_precision/run-qwen3-30B-A3B-int4.sh): + +```bash +RUNTIME_ENV_JSON='{ + "env_vars": { + "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 ... +``` + +Pair the INT4 `--hf-checkpoint` with a BF16 `--ref-load` torch_dist directory +so the KL anchor stays full-precision. + +## Tuning + +| Symptom | Try | +|---|---| +| Eval reward drops noticeably vs BF16 | Lower `OPEN_TRAINING_INT4_GROUP_SIZE` (e.g. 64), or recalibrate with more samples. | +| Slower than BF16 | Confirm `--sglang-cuda-graph-bs` covers your batch sizes. | + +## Pairs with + +* [R3](miles-router.md). Keeps MoE routing stable across the quantized forward. +* [P2P weight transfer](p2p-weight-transfer.md). INT4 weights are 4× smaller, + so weight sync transfers less data. +* [Speculative decoding](speculative-decoding.md). Compounds for end-to-end + rollout speedup. + +## When QAT is not appropriate + +* The model fits comfortably without it. +* The model architecture is still in development; introduce QAT after a BF16 + baseline. +* Tasks that are highly precision-sensitive (some math and safety eval suites). diff --git a/docs/advanced/lora.md b/docs/advanced/lora.md new file mode 100644 index 00000000000..0a49d43f680 --- /dev/null +++ b/docs/advanced/lora.md @@ -0,0 +1,99 @@ +--- +title: LoRA Training and Serving +description: Train LoRA adapters with miles SFT or RL recipes and serve them through SGLang from the same checkpoint. +--- + +# LoRA Training and Serving + +Miles supports LoRA adapters for both SFT and RL recipes. Adapters trained by +miles load directly into SGLang for rollout, so there is no separate merge or +conversion step in the training-serving loop. + +This page is a stub; the full LoRA tutorial is being written. In the meantime, +the pieces below are enough to get a recipe running. + +## Example launchers + +The canonical LoRA recipes live under +[`examples/lora/`](https://github.com/radixark/miles/tree/main/examples/lora) in +the miles repo: + +- `examples/lora/run-qwen2.5-0.5B-megatron-lora.sh` — small dense, single GPU. +- `examples/lora/run-qwen3-4B-megatron-lora.sh` — Qwen3-4B, RL with LoRA. +- `examples/lora/run-gpt-oss-20B-megatron-moe-lora.sh` — MoE example. + +## Key flags + +| Flag | Purpose | +|---|---| +| `--lora-rank` | LoRA rank. Typical values: 8, 16, 32, 64. | +| `--lora-alpha` | LoRA alpha. Usually 2 x rank. | +| `--lora-dropout` | Dropout on the LoRA path. Set to `0.0` for RL training. | +| `--lora-type` | LoRA variant: `lora` (merged QKV / gated-MLP) or `canonical_lora` (split Q / K / V). Default `lora`. | +| `--target-modules` | Which linear layers receive adapters. Required when `--lora-rank > 0`. Accepts `all-linear` or a comma-separated list (HF names like `q_proj,k_proj,v_proj,o_proj` or Megatron names like `linear_qkv,linear_proj`). | +| `--exclude-modules` | Comma-separated names to subtract from `--target-modules`. | +| `--lora-adapter-path` | Path to a pre-trained adapter to resume from. | +| `--lora-sync-from-tensor` | Sync adapter weights to SGLang via in-memory tensors instead of a file round-trip. | + +Two existing arguments also have LoRA-specific requirements that are easy to +miss: the launcher has to pass `--megatron-to-hf-mode bridge` (the LoRA path +goes through Megatron-Bridge's PEFT integration; the default `raw` converter +does not understand LoRA layers), and the Ray job has to run with +`--colocate`. Distributed (PD-disaggregated) rollout with LoRA is not +supported today. + +## MoE + +For MoE models, attach LoRA to the FFN expert projections and switch the +SGLang LoRA backend to triton: + +```bash +LORA_ARGS=( + --lora-rank 32 + --lora-alpha 32 + --lora-dropout 0.0 + --target-modules "gate_proj,up_proj,down_proj" + --sglang-lora-backend triton # required for MoE LoRA + --megatron-to-hf-mode bridge +) +``` + +The default SGLang LoRA backend skips MoE layers and logs +`Current LoRA backend does not support LoRA on MoE layers; skipping MoE layer`, +which means the expert adapters get silently dropped at inference time. The +GPT-OSS-20B example launcher sets `--sglang-lora-backend triton` for this +reason. + +## Compatibility and limitations + +* **Training backend**: Megatron only. The FSDP backend does not have a LoRA + path yet. +* **Rollout topology**: colocate only. Distributed / PD-disaggregated rollout + raises `NotImplementedError` at weight-sync time when LoRA is enabled. +* **Algorithms**: orthogonal to the advantage estimator; the GRPO recipes in + `examples/lora/` carry straight over to PPO and any other algorithm that + drives `train.py`. +* **Low-precision training**: the LoRA branch follows the surrounding + precision, so block-wise FP8, MXFP8, and INT4 QAT recipes are compatible. + See [Low Precision RL](fp8-low-precision.md) and [INT4 QAT](int4-qat.md). +* **`--target-modules` is mandatory** when `--lora-rank > 0`. There is no + auto-detection; the launcher asserts at startup. +* **Single adapter per run**: multi-LoRA training in a single job is not + implemented today. + +## Internals + +The bridge between Megatron's LoRA path and SGLang adapter loading is in: + +- `miles/backends/megatron_utils/lora_utils.py` — argument parsing helpers, + LoRA detection (`is_lora_enabled`, `is_lora_model`), and HF ↔ Megatron + module-name conversion for both the `lora` and `canonical_lora` variants. +- `miles/backends/megatron_utils/bridge_lora_helpers.py` — the Megatron-Bridge + PEFT hook that wraps the model with LoRA layers before training. +- `miles/backends/megatron_utils/checkpoint.py` — adapter-aware save and load. +- `miles/backends/megatron_utils/update_weight/update_weight_from_tensor.py` + — colocate-mode weight sync from the trainer's LoRA tensors into the SGLang + rollout engine. We will merge this [PR](https://github.com/radixark/miles/pull/988) soon to support disaggregate mode. + +A worked tutorial covering checkpoint conversion, SGLang adapter loading, and +LoRA-specific evaluation will land here in a future doc pass. diff --git a/docs/advanced/miles-router.md b/docs/advanced/miles-router.md new file mode 100644 index 00000000000..35c8c73ed7b --- /dev/null +++ b/docs/advanced/miles-router.md @@ -0,0 +1,68 @@ +--- +title: Rollout Routing Replay (R3) +description: Capture expert routing during inference and replay it during training so MoE RL is stable. +--- + +# Rollout Routing Replay (R3) + +Rollout Routing Replay (R3) records the expert routing decisions made during +inference and replays them during training, producing bit-identical expert +allocation between rollout and training. + +## Why MoE RL was previously unstable + +For each token, an MoE router picks `top-k` experts. The choice depends on the +input through a soft router and a top-k op. In production the router is a +learned `nn.Linear` with non-deterministic kernels and FP8 quantization, so tiny +numerical differences flip routes at the per-layer, per-token level. + +Without R3: + +* Rollout selects experts `{2, 7}` for token 314. +* Training (with the same weights but slightly different precision and kernels) + selects experts `{2, 8}` for token 314. +* The gradient is computed against the wrong expert. Multiplied by hundreds of + layers, tens of thousands of tokens, and thousands of training steps, the + policy diverges. + +With R3 the inference router's choice is what training also uses. Numerical +noise no longer flips routes. + +## How R3 wires up + +**SGLang side.** Miles enables `enable_return_routed_experts` automatically when +`--use-rollout-routing-replay` is on. SGLang then includes `routed_experts` +in `meta_info` of each response, with shape +`(seq_len - 1, num_layers, top_k)` and dtype `int32`. + +**Miles side.** Enable R3 with: + +```bash +--use-rollout-routing-replay +``` + +Rollout sends `return_routed_experts=true` in each request and stores the +results in `sample.rollout_routed_experts` (`miles/utils/types.py`). The +trainer pushes the arrays through `RoutingReplayManager` +(`miles/utils/replay_base.py`), and `replay_utils.py` plugs them into the +forward pass so recorded routes are used instead of recomputed ones. + +## Memory cost + +`(num_tokens - 1) × num_layers × top_k × 4 bytes` (int32 per element, see +`miles/utils/types.py:29`). For a 32K-token sequence, 60 layers, and +`top_k = 8`, that is roughly 60 MB per sample of routing metadata. + +## When R3 is not required + +* The model is dense. +* The `--advantage-estimator` is `reinforce_plus_plus` and `--use-tis` already + masks the off-policy term. + +For MoE training with `--advantage-estimator grpo`, current recipes turn R3 +on (for example `scripts/run-glm4.7-flash.sh`). + +## References + +* R3 paper: [arXiv 2510.11370](https://arxiv.org/pdf/2510.11370). +* Routing replay: [arXiv 2507.18071](https://arxiv.org/abs/2507.18071). diff --git a/docs/advanced/p2p-weight-transfer.md b/docs/advanced/p2p-weight-transfer.md new file mode 100644 index 00000000000..31212c2fbcf --- /dev/null +++ b/docs/advanced/p2p-weight-transfer.md @@ -0,0 +1,144 @@ +--- +title: P2P Weight Transfer +description: Direct rank-to-rank weight sync from actor to rollout via RDMA writes. +--- + +# P2P Weight Transfer + +miles supports P2P (point-to-point) weight transfer between training and rollout engines. By using `--update-weight-transfer-mode p2p`, miles enables more efficient weight transfer from training ranks to rollout engine ranks. More details on the design and implementation can be found in [this issue](https://github.com/radixark/miles/issues/755). + +## Usage + +To enable P2P weight transfer, add the following flag to your training command: + +``` +--update-weight-transfer-mode p2p +``` + +## How It Works + +The default weight transfer mode in miles is `broadcast`: after training, updated weights are broadcast via NCCL to all rollout engine ranks. This works but does not fully utilize the available bandwidth, as redundant copies of the same weights are transferred to multiple ranks. + +P2P mode addresses this by having each training rank transfer only the specific weight shards required by its target rollout engine rank(s), writing them directly to remote memory without redundant copies. The key steps are: + +1. **Initialization**: Training ranks establish point-to-point connections (via RDMA) to their target rollout engine ranks. Including: + - Create a transfer plan that maps each training rank to its target rollout rank(s) based on GPU counts and parallelism configuration. + - Query remote rollout engines for their weight memory registration info (addresses and sizes for RDMA writes). + - Query remote parallelism config and construct a local CPU model replica that mirrors the target's sharding layout, enabling correct weight format conversion before transfer. + +2. **Weight gather**: Megatron TP/EP shards are all-gathered and converted to HF format, same as the broadcast path. + +3. **P2P transfer**: Instead of a collective broadcast, each source rank writes bucketed weight tensors directly to the destination rollout rank's memory, in a write-only fashion. + +4. **Synchronization**: Once all RDMA writes are confirmed complete, rollout engines increment their weight version and resume generation for the next training step. + +## Architecture + +Both broadcast and P2P modes share the same bucketed weight-update pipeline in `miles/backends/megatron_utils/update_weight/`. The diagram below shows which components are shared and which are P2P-specific. + +### Shared components (broadcast & P2P) + +| Component | Description | +|---|---| +| **TP/EP all-gather** | Megatron TP shards are all-gathered within each PP stage; EP shards are gathered per-bucket when the accumulated expert data exceeds `buffer_size * ep_size`. Both modes perform this identically via `common.py`. | +| **Bucketed update** | Weights are not transferred one parameter at a time. Instead, converted tensors are accumulated into a fixed-size buffer (`--update-weight-buffer-size`, default 1 GB). When the buffer is full, the entire bucket is flushed — via NCCL broadcast or RDMA write depending on the mode. This amortizes per-transfer overhead. Non-expert and expert weights use separate buckets. | +| **PP independence** | Each pipeline-parallel stage updates its own weights independently. In broadcast mode, each PP rank has its own NCCL group (`miles-pp_{pp_rank}`). In P2P mode, each PP rank has its own transfer plan. No cross-PP synchronization is needed during weight transfer, which is key to scaling. | +| **HF format conversion** | After all-gather, Megatron-format tensors (with custom naming and sharding) are converted to HuggingFace-format names expected by the sglang rollout engine. | + +### P2P-specific components + +| Component | File | Description | +|---|---|---| +| **Transfer plan** | `p2p_transfer_utils.py` | Maps each training rank to its target rollout engine rank(s). Uses round-robin assignment with load balancing: the first `min(sources, targets)` ranks get 1:1 mapping, remaining targets are distributed evenly. This minimizes the number of RDMA sessions per source. | +| **CPU model replica** | `p2p.py` | A full sglang model is instantiated on CPU (not GPU) to mirror the target engine's parallelism layout. This replica provides the correct `weight_loader` functions to re-shard all-gathered HF weights into the exact format expected by each target rank. Only the first engine's replica pins memory; subsequent engines reuse the mapping via `ParameterMapper`. | +| **Shared pinned buffer** | `p2p.py` | A single CPU pinned memory buffer is registered with the mooncake TransferEngine for RDMA. This buffer is reused across all target engines (O(1) memory, not O(num\_engines)). The buffer is overwritten per-engine, per-bucket. | +| **Pipelined transfer** | `p2p.py` | RDMA writes to multiple target engines are pipelined: for non-last engines, the transfer manager waits for the previous write to complete before reusing the buffer; for the last engine, writes are fire-and-forget to a background thread pool, overlapping with the next bucket's load phase. | + +## Supported Model Architectures + +P2P weight transfer relies on a unified weight name mapping interface between Megatron and sglang (see [sglang#17326](https://github.com/sgl-project/sglang/pull/17326)). The following sglang model classes are supported: + +| sglang Model Class | Model Family | Example Models | +|---|---|---| +| `Qwen2ForCausalLM` | Qwen2 (dense) | Qwen2.5-0.5B, Qwen2.5-7B | +| `Qwen3ForCausalLM` | Qwen3 (dense) | Qwen3-4B, Qwen3-8B | +| `Qwen3MoeForCausalLM` | Qwen3-MoE | Qwen3-30B-A3B, Qwen3-235B-A22B | +| `Glm4ForCausalLM` | GLM4 (dense) | GLM-Z1-9B-0414 | +| `Glm4MoeForCausalLM` | GLM4-MoE | GLM-4.5-Air | +| `Glm4MoeLiteForCausalLM` | GLM4-MoE | GLM-4.7-9B-Flash | +| `DeepseekV2ForCausalLM` | DeepSeek V2 | Moonlight-16B-A3B | +| `DeepseekV3ForCausalLM` | DeepSeek V3p2 | GLM-5 (744B-A40B) | +| `DeepseekV3ForCausalLM` | DeepSeek V3 | Kimi-K2 (1T) \* | + +> **Note:** All the above models are tested on H100-80GB clusters. +> +> For Kimi-K2, we set `training nodes = inference nodes = 32` to ensure sufficient memory. +> The original Kimi-K2 checkpoint uses a block-quant size of `[128, 128]`, which will trigger errors if `sglang-tp-size = 32`. +> To work around this, we re-quantize to `[64, 64]` and update all affected scale tensors accordingly. + +## Validated Models + +All models below have been validated with `--check-weight-update-equal` in P2P mode. + +| Model | sglang Model Class | Nodes | +|---|---|---| +| Qwen3-4B | `Qwen3ForCausalLM` | 1 | +| GLM-Z1-9B-0414 | `Glm4ForCausalLM` | 1 | +| Moonlight-16B-A3B | `DeepseekV2ForCausalLM` | 2 | +| GLM-4.7-9B-Flash | `Glm4MoeLiteForCausalLM` | 2 | +| GLM-5_4layer | `DeepseekV3ForCausalLM` | 2 | +| Qwen3-30B-A3B | `Qwen3MoeForCausalLM` | 4 | +| GLM-4.5-Air | `Glm4MoeForCausalLM` | 8 | + +> Enabling `--check-weight-update-equal` for Kimi-K2 is non-trivial due to several issues: +> +> - The user must first dequantize the Kimi-K2 model to BF16 for training, then re-quantize the parameters for weight updating. Meanwhile, the rollout side snapshots the original Kimi-K2 tensors as the reference when enabling `--check-weight-update-equal`. This means the rollout SGLang engine should load the checkpoints processed by the `dequant-requant` pipeline. +> - We use modified checkpoints with block-quant size `[64, 64]`, so any `quant/dequant` code that hard-codes the block-quant size as `[128, 128]` will break. +> - Certain tensors that are only initialized on the rollout side (e.g., `k_scale` / `v_scale`) must be skipped during the weight-check process. +> +> We verified P2P correctness for Kimi-K2 by enabling `--check-weight-update-equal` with hard-coded workarounds for the issues above. The transferred weights were confirmed correct. These hard-coded workarounds are verification-only and will not be merged into the main branch to keep the codebase maintainable. + +## Profiling Results + +For M source ranks and N target ranks, with source pp size `src_pp` and target ep size `sgl_ep`, the benefit of P2P transfer is approximately: +1. utilizes `M // src_pp` times more source transmission bandwidth. +2. each target rank receives `sgl_ep` times less data. + +Thus we expect our solution to scale better, especially on the MoE models. + +All profiling is run on H100-80GB clusters with 1GB transfer bucket. +Timing measures after `pause_generation` call returns and before `update_weight` call exits to exclude request queue abortion time. +Table includes steady-state steps 3–12 average. For Kimi-K2, the RDMA (ms) column includes GPU-side post-processing time (`post_load_weights` ~884ms) since this model requires GPU-side weight requantization after RDMA transfer. + +Models marked with ★ are MoE architectures, where P2P benefits are most pronounced due to expert-parallel sharding reducing per-target transfer volume. + +| Model Family | Model Name | Total Param | sglang Model Class | Train Config | Inference Config | NCCL (ms) | RDMA (ms) | Delta | +|---|---|---|---|---|---|---|---|---| +| GLM4 | GLM-Z1-9B-0414 | 9B | `Glm4ForCausalLM` | TP=2, PP=1, CP=2, EP=1, ETP=1, 1 node | TP=4, EP=1, 1 node | 694.6 | 707.1 | +1.8% | +| DeepSeek-V2 ★ | Moonlight-16B-A3B | 16B(3B) | `DeepseekV2ForCausalLM` | TP=2, PP=1, CP=1, EP=8, ETP=1, 1 node | TP=8, EP=8, 1 node | 1,482.0 | 1,073.3 | **−27.6%** | +| GLM4-MoE ★ | GLM-4.7-9B-Flash | 30B(3B) | `Glm4MoeLiteForCausalLM` | TP=4, PP=1, CP=1, EP=8, ETP=1, 1 node | TP=4, EP=4, 1 node | 2,508.6 | 4,229.0 | +68.6% | +| DeepSeek-V3 ★ | GLM-5_4layer | 4-layer | `DeepseekV3ForCausalLM` | TP=4, PP=1, CP=1, EP=8, ETP=1, 1 node | TP=8, EP=8, 1 node | 732.2 | 1,260.8 | +72.2% | +| Qwen3-MoE ★ | Qwen3-30B-A3B | 30B(3B) | `Qwen3MoeForCausalLM` | TP=4, PP=1, CP=1, EP=8, ETP=1, 2 nodes | TP=8, EP=8, 2 nodes | 2,670.0 | 2,160.2 | **−19.1%** | +| GLM4-MoE ★ | GLM-4.5-Air | 106B(12B) | `Glm4MoeForCausalLM` | TP=1, PP=4, CP=1, EP=8, ETP=1, 4 nodes | TP=8, EP=8, 4 nodes | 5,001.1 | 2,637.2 | **−47.3%** | +| Qwen3-MoE ★ | Qwen3-235B-A22B | 235B(22B) | `Qwen3MoeForCausalLM` | TP=4, PP=4, CP=2, EP=16, ETP=1, 8 nodes | TP=32, EP=32, 8 nodes | 10,753.6 | 3,162.0 | **−70.6%** | +| DeepSeek-V3p2 ★ | GLM-5 | 744B(40B) | `DeepseekV3ForCausalLM` | TP=4, PP=8, CP=2, EP=16, ETP=1, 16 nodes | TP=64, EP=64, 16 nodes | 58,301.5 | 8,479.7 | **−85.5%** | +| DeepSeek-V3 ★ | Kimi-K2 | 1T(64B) | `DeepseekV3ForCausalLM` | TP=8, PP=8, CP=4, EP=32, ETP=1, 32 nodes | TP=32, EP=32, 32 nodes | 53,279.1 | 7,227.3 | **−86.4%** | + + +![P2P vs NCCL Broadcast Scaling](../assets/images/p2p_vs_nccl_scaling.png) + +\* Kimi-K2 RDMA time includes ~884 ms GPU-side `post_load_weights` requantization on rollout engines. + +## Examples + +### CI Test (single-node, Qwen3-4B) + +The P2P weight transfer E2E test validates correctness on a single node using `Qwen3-4B`: + +```python +# tests/e2e/megatron/test_qwen3_4B_p2p.py +# +# Train: 4 GPUs (TP=2, CP=2) +# Rollout: 4 GPUs (sglang, 2 engines × 2 GPUs each) +# Flags: --update-weight-transfer-mode p2p --check-weight-update-equal +``` diff --git a/docs/advanced/pd-disaggregation.md b/docs/advanced/pd-disaggregation.md new file mode 100644 index 00000000000..197315cb9bb --- /dev/null +++ b/docs/advanced/pd-disaggregation.md @@ -0,0 +1,93 @@ +--- +title: PD Disaggregation +description: Separate prefill and decode pools so each is sized for its workload. +--- + +# PD Disaggregation + +In a typical SGLang deployment, every engine handles both **prefill** (the +one-shot forward over the prompt) and **decode** (the per-token autoregressive +loop). The two phases have different compute profiles: + +| Phase | Compute pattern | Bottleneck | +|---|---|---| +| Prefill | Long sequence × full batch | FLOPs | +| Decode | One token × batch | Memory bandwidth | + +Mixing them in one engine means each engine is sized for the worse case. +PD disaggregation splits them into two pools, each sized for its own workload. + +## Enable + +```bash +--prefill-num-servers 2 +``` + +`--prefill-num-servers` is a Miles-native flag added by +`add_prefill_decode_disaggregation_arguments` in `miles/utils/arguments.py`. +When set, `miles/ray/rollout.py:1090` calls +`SglangConfig.from_prefill_num_servers(args)` to dedicate that many SGLang +servers to prefill, with the rest used for decode. + +`--prefill-num-servers` is mutually exclusive with the `sglang_config` +attribute (the YAML `server_groups` config), and also cannot be combined +with `--rollout-external` (`arguments.py:2082-2087`). + +## When PD is worth it + +* Long prompts (≥ 4K). Prefill dominates total latency. +* High decode batch sizes. Decode is memory-bandwidth bound. +* MoE models. Decode benefits disproportionately from EP scaling that prefill + does not need. + +For typical post-training with 1–2K prompts, the routing and KV-transfer +overhead can outweigh the speedup. Measure first. + +## How requests flow + +```mermaid +flowchart LR + P[Prompt] --> Router + Router -->|prefill request| Prefill[Prefill pool] + Prefill -->|KV cache + first token| Router + Router -->|decode request + KV| Decode[Decode pool] + Decode -->|stream tokens| Router + Router --> Out[Response] +``` + +The KV cache produced by the prefill pool is migrated to the decode pool. +SGLang handles the cache transfer when the +[SGLang Model Gateway](https://docs.sglang.io/advanced_features/sgl_model_gateway.html) +fronts the engines (PD support is a feature of the SGLang router). + +## Sizing the pools + +A starting heuristic: + +``` +prefill_servers = ceil(rollout_qps × avg_prompt_tokens / single_engine_prefill_tps) +decode_servers = N - prefill_servers +``` + +Without measurements, start at `prefill_num_servers = N / 4` and adjust based +on observed queueing: + +| Symptom | Action | +|---|---| +| Prefill queue backing up | Increase `prefill_num_servers` | +| Decode latency creeping up | Decrease `prefill_num_servers` | +| Both queues growing | Scale up both pools, then revisit the ratio | + +## Pairs with + +* [DeepSeek R1 recipe](../models/deepseek/deepseek.md). PD is a clear win at + 671B scale. +* [Speculative decoding](speculative-decoding.md). Both are SGLang-side + features; pool sizing should account for the verify-batch size when + speculative is on. + +## When PD is not useful + +* Short prompts (under ~1K tokens). Prefill is already cheap. +* Single-node setups. Pool boundaries do not help. +* Highly variable workloads. Fixed pool sizes are wasteful. diff --git a/docs/advanced/speculative-decoding.md b/docs/advanced/speculative-decoding.md new file mode 100644 index 00000000000..420946de412 --- /dev/null +++ b/docs/advanced/speculative-decoding.md @@ -0,0 +1,99 @@ +--- +title: Speculative Decoding +description: Draft + target speculative rollout, with online SFT for MTP-style drafts. +--- + +# Speculative Decoding + +Speculative decoding accelerates rollout by letting a lightweight draft model +generate ahead a few tokens and then verifying them with a single batched +forward of the target model. When the draft is correct the target produces N +tokens for the cost of one forward pass. + +## Enabling speculative decoding + +For models with built-in MTP (Multi-Token Prediction) layers (GLM-4.7, +DeepSeek-V3, DeepSeek-R1): + +```bash +SGLANG_ARGS+=( + --sglang-speculative-algorithm EAGLE + --sglang-speculative-num-steps 3 + --sglang-speculative-eagle-topk 1 + --sglang-speculative-num-draft-tokens 4 +) +``` + +These are passthrough flags forwarded to SGLang. Miles auto-enables +`enable_draft_weights_cpu_backup` so SGLang can run training without +MTP weights resident on GPU +(`miles/backends/sglang_utils/sglang_engine.py`). + +For an externally trained draft model (for example, trained with +[SpecForge](https://docs.sglang.ai/SpecForge/)): + +```bash +SGLANG_ARGS+=( + --sglang-speculative-draft-model-path /data/draft_model/ +) +``` + +Full reference: [SGLang speculative decoding docs](https://docs.sglang.ai/advanced_features/speculative_decoding.html). + +## Drift over a long RL run + +As RL training progresses, the target model's distribution shifts away from the +draft. Fewer draft tokens pass verification, and over many steps speculative +decoding can become a net negative because the wasted draft compute outweighs +the verified speedup. + +Miles supports training the draft alongside the target through online MTP-SFT. + +## Online SFT for MTP-style draft models + +```bash +PERF_ARGS+=( + --mtp-num-layers 1 + --enable-mtp-training + --mtp-loss-scaling-factor 0.2 +) +``` + +| Flag | Notes | +|---|---| +| `--mtp-num-layers` | Number of MTP layers in the checkpoint (1 matches GLM/DeepSeek release defaults). | +| `--enable-mtp-training` | Backprop through MTP loss alongside the policy loss. | +| `--mtp-loss-scaling-factor` | Weight of the MTP loss in the combined gradient (default `0.2`). | + + + +**Checkpoint must contain MTP weights.** Pass `--mtp-num-layers 1` when running `convert_hf_to_torch_dist.py`. +Without it the resulting `torch_dist` checkpoint will not contain the MTP +layer to train. + + + +## External draft model SFT + +Training an external (non-MTP) draft model online is not yet supported in +Miles. The current path is to retrain the external draft offline every N +rollouts and reload it. + +## Pairs with + +* [Unified FP8](fp8-low-precision.md). Draft and target both quantized the + same way. +* [INT4 QAT](int4-qat.md). A quantized draft is cheaper to verify. +* [R3](miles-router.md). R3 captures routing for the verified tokens emitted + by the target. + +## When to skip + +* Rollout-bound on dense models below ~13 B. The verification overhead can + outweigh the benefit. +* Already at high draft acceptance and the bottleneck is verification compute, + not generation. + +## Reading + +* SpecForge: [SGLang docs](https://docs.sglang.ai/SpecForge/). diff --git a/docs/platforms/amd.md b/docs/platforms/amd.md new file mode 100644 index 00000000000..e8244409932 --- /dev/null +++ b/docs/platforms/amd.md @@ -0,0 +1,96 @@ +--- +title: AMD MI300X +description: ROCm 6.3+ with patches for virtual memory management. Same launch scripts. +--- + +# AMD MI300X + +Miles runs on AMD Instinct GPUs (MI300, MI325, MI350, MI355X) with ROCm. The launch +scripts are the same as on NVIDIA — only the container and a few env vars differ. + +## Container images + +```bash +# MI350 / MI355X +docker pull rlsys/miles:MI350-355-latest + +# MI300 / MI325 +docker pull rlsys/miles:MI300-latest +``` + +Or build from the repo: + +```bash +cd docker +docker build -f Dockerfile.rocm_MI350-5 -t rlsys/miles:latest . +``` + +The base ROCm image bundles the patches needed for virtual memory management on MI300X +— thanks to [Yang Wang](https://www.microsoft.com/en-us/research/people/yangwang5/) for +that work. + +## Launch the container + +```bash +docker run --rm -it \ + --device /dev/dri \ + --device /dev/kfd \ + -p 8265:8265 \ + --group-add video \ + --cap-add SYS_PTRACE \ + --security-opt seccomp=unconfined \ + --privileged \ + -v $HOME/.ssh:/root/.ssh \ + -v $HOME:$HOME \ + --shm-size 128G \ + --name miles_dev \ + --ulimit memlock=-1 --ulimit stack=67108864 \ + -w $PWD \ + rlsys/miles:latest \ + /bin/bash +``` + +Inside, install Miles editable: + +```bash +git clone https://github.com/radixark/miles.git +cd miles && pip install -e . --no-deps +``` + +## Download model + data + +```bash +hf download Qwen/Qwen3-4B --local-dir /root/Qwen3-4B +hf download --repo-type dataset BytedTsinghua-SIA/DAPO-Math-17K --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 +``` + +## Convert weights (CPU + Gloo) + +We force CPU-only conversion on AMD to bypass some ROCm-specific issues. A GPU-based +ROCm converter is in development. + +```bash +cd /root/miles +source scripts/models/qwen3-4B.sh +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[@]} \ + --hf-checkpoint /root/Qwen3-4B \ + --save /root/Qwen3-4B_torch_dist +``` + +If you see `miles cannot be found`, re-run `pip install -e . --no-deps` in the repo. + +## Launch + +The standard `scripts/run-qwen3-4B.sh` works as-is. The image already sets the +ROCm-specific env vars you'd otherwise need: + +```bash +HSA_OVERRIDE_GFX_VERSION=11.0.0 # or 9.4.0 for MI300 +NCCL_NET=Socket # for non-RDMA setups +NCCL_IB_HCA=... # if your fabric supports it +PYTORCH_NO_HIP_MEMORY_CACHING=0 +``` diff --git a/docs/platforms/index.md b/docs/platforms/index.md new file mode 100644 index 00000000000..5cd2043b37f --- /dev/null +++ b/docs/platforms/index.md @@ -0,0 +1,47 @@ +--- +title: Platforms +description: Hardware-specific tutorials. Most users want NVIDIA H/B; AMD MI300X is supported via ROCm. +--- + +# Platforms + +Miles runs on NVIDIA H/B-series and AMD MI300X with the same launch scripts. Each platform page covers driver versions, build flags, and the FP8 / ROCm quirks you need to know before kicking off a job. + + + + + + The default GB300 / GB200 / B200 / B100 / H200 / H100 with FP8, NVLink, and InfiniBand. + + + + + + ROCm 6.3+ with patches for virtual memory management. Same launch scripts. + + + + + +## Supported features by GPU + +| Feature | H / B-series | A100 | MI300X | +|---|---|---|---| +| BF16 training | ✅ | ✅ | ✅ | +| FP8 GEMM | ✅ | - | ✅ | +| INT4 W4A16 QAT | ✅ | ✅ | ⚠️ | +| Speculative decoding | ✅ | ✅ | ✅ | +| Miles Router (R3) | ✅ | ✅ | ✅ | +| P2P weight transfer (RDMA) | ✅ IB / RoCEv2 | ✅ | ✅ Infinity Fabric | +| Megatron CP | ✅ | ✅ | ✅ | +| Deterministic inference | ✅ | ✅ | ⚠️ | + +## Storage and network + +Independent of GPU vendor, you'll want: + +* **Shared filesystem** for multi-node — NFS, GPFS, Lustre. Reads dominate writes during + training; provision read bandwidth. +* **High-bandwidth interconnect** — IB (NVIDIA), RoCEv2 (NVIDIA), Slingshot, or + Infinity Fabric (AMD). 200+ GB/s per node is typical for trillion-param training. +* **NVMe local scratch** for SGLang radix cache and Ray spill — at least 1 TB. diff --git a/docs/platforms/nvidia.md b/docs/platforms/nvidia.md new file mode 100644 index 00000000000..7bf0a1aa3a1 --- /dev/null +++ b/docs/platforms/nvidia.md @@ -0,0 +1,92 @@ +--- +title: NVIDIA H / B Series +description: H100, H200, B100, B200 — Miles's primary target. +--- + +# NVIDIA GPUs + +NVIDIA Blackwell (GB300 / GB200 / B200 / B100) and Hopper (H200 / H100) are Miles's first-class targets. + +## Recommended setup + +```bash +docker pull radixark/miles:latest + +docker run --rm \ + --gpus all --ipc=host --shm-size=32g \ + --ulimit memlock=-1 --ulimit stack=67108864 \ + --network=host \ + -it radixark/miles:latest /bin/bash +``` + +The image bundles: + +| Component | Why pinned | +|---|---| +| CUDA 12.4+ | Required for FP8 GEMM via cuBLAS | +| FlashAttention-3 (default), Flashinfer| Best-in-class attention kernels | +| DeepGEMM | Kernel for grouped GEMM (MoE) | +| NCCL 2.20+ | NVLink SHARP, IB-aware collectives | +| TransformerEngine | FP8 forward/backward | + +## Per-GPU notes + +### H100 / H200 + +* The default target the recipes on this site are tuned against. +* H200 ships with 141 GB HBM (vs. 80 GB on H100), so you can often reduce TP for the + same model — e.g. TP 8 → TP 4 on a single 8-GPU node. + +### B100 / B200 + +* Same launch flags as H-series; FP8 GEMM uses the same code path. +* First-run kernel compilation can take longer than on H-series. If the rollout engine + is flagged unhealthy during warm-up, raise `--rollout-health-check-first-wait` + (e.g. 600s). + +### A100 + +* No FP8 GEMM — the BF16 path is used automatically. +* Supported, but not part of CI; expect rougher edges than on H/B-series. + +## Multi-node networking + +* **InfiniBand HDR/NDR**: ~200/400 Gbps per port. Default in most H100 deployments. +* **RoCEv2**: works, configure `NCCL_IB_HCA` to your physical NICs. +* **Slingshot 11**: requires `NCCL_NET_PLUGIN=cassini`. + +Confirm bandwidth with `ib_send_bw` between two ranks before launching a multi-day run. + +## Common environment variables + +```bash +NCCL_DEBUG=INFO +NCCL_DEBUG_SUBSYS=COLL,P2P +NCCL_IB_HCA=mlx5_0,mlx5_1,... +NCCL_TIMEOUT=900 +NVTE_FUSED_ATTN=1 # default, but verify +TORCHINDUCTOR_CACHE_DIR=/data/.inductor +``` + +## NVLink + IB topology + +For 8× GPUs per node: + +* All-to-all NVLink connectivity (`nvidia-smi topo -m` should show `NV4` between every + pair). +* 4–8 IB NICs per node, one per GPU pair, configured via `NCCL_IB_HCA`. + +If `nvidia-smi topo -m` shows `PIX` or `PHB` instead of `NV*`, you've lost a link — +fix before training. + +## Quick health probe + +Before submitting a job, sanity-check the node: + +```bash +nvidia-smi # GPUs visible, driver / CUDA versions +nvidia-smi topo -m # NVLink mesh (NV* between every pair) +ibstat # IB ports up (multi-node only) +``` + +If anything's wrong, fix it here — chasing it inside Ray is harder. From 217b94e4b10271f2f8e4bba3ad028525af85aeea Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Mon, 11 May 2026 17:46:57 -0700 Subject: [PATCH 0047/1130] doc [8/n]: add docs/examples and docs/blog (#1104) Co-authored-by: Zhichenzzz Co-authored-by: Shi-Dong --- docs/blog/index.md | 23 +++ docs/blog/introducing-miles.md | 70 ++++++++ docs/examples/fully-async.md | 208 +++++++++++++++++++++++ docs/examples/index.md | 76 +++++++++ docs/examples/multi-agent.md | 200 ++++++++++++++++++++++ docs/examples/openhermes-sft.md | 164 ++++++++++++++++++ docs/examples/reproducibility.md | 146 ++++++++++++++++ docs/examples/retool.md | 223 +++++++++++++++++++++++++ docs/examples/search-r1.md | 278 +++++++++++++++++++++++++++++++ 9 files changed, 1388 insertions(+) create mode 100644 docs/blog/index.md create mode 100644 docs/blog/introducing-miles.md create mode 100644 docs/examples/fully-async.md create mode 100644 docs/examples/index.md create mode 100644 docs/examples/multi-agent.md create mode 100644 docs/examples/openhermes-sft.md create mode 100644 docs/examples/reproducibility.md create mode 100644 docs/examples/retool.md create mode 100644 docs/examples/search-r1.md diff --git a/docs/blog/index.md b/docs/blog/index.md new file mode 100644 index 00000000000..cd20f64425b --- /dev/null +++ b/docs/blog/index.md @@ -0,0 +1,23 @@ +--- +title: Blog +description: Engineering posts and release notes from the Miles team. +--- + +# Blog + + + + + + Why we built Miles, what it means for production-scale RL post-training, and what + we shipped on day one. + + + + + +## Subscribe + +* GitHub releases: [radixark/miles/releases](https://github.com/radixark/miles/releases) +* RSS: coming soon +* Twitter/X: [@radixark_miles](https://twitter.com) diff --git a/docs/blog/introducing-miles.md b/docs/blog/introducing-miles.md new file mode 100644 index 00000000000..5b71acdbb29 --- /dev/null +++ b/docs/blog/introducing-miles.md @@ -0,0 +1,70 @@ +--- +title: Introducing Miles +description: Why RadixArk built Miles, and what it means for production-scale RL post-training. +date: 2025-11-19 +--- + +# Introducing Miles + +*November 19, 2025 — RadixArk team* + +Today RadixArk is open-sourcing **Miles**, a reinforcement learning framework purpose- +built for post-training large language models. Miles pairs high-throughput rollout +(SGLang) with scalable training (Megatron-LM and FSDP), and adds the precision, +stability, and observability features that production RL actually needs. + +## Why another RL framework? + +Reinforcement learning at the trillion-parameter scale runs into problems that a +research codebase never has to solve: the training and inference engines disagree on +expert routing; the numerical precision drifts silently between rollout and backprop; +long runs get killed by flaky NICs; adding a new model architecture means patching +Megatron. + +Miles attacks each of these problems at the system layer so the RL researcher can focus +on the RL part. + +## What ships on day one + +- **Unified low-precision pipeline.** End-to-end FP8 sampling and training share a + single quantization path. Rollout and trainer see bit-identical policies. +- **Rollout Routing Replay (R3).** For MoE models, expert routing captured at + inference time is replayed during training, eliminating the mismatch that destabilizes + large-scale MoE RL. +- **Speculative rollout with online MTP-SFT.** The draft model's acceptance rate stays + high through training because its MTP layers are fine-tuned on-policy. +- **Algorithmic safety nets.** Truncated Importance Sampling (TIS) and Masked + Importance Sampling (MIS) for off-policy correction when train ≠ inference. +- **Fault tolerance.** Rank-level recovery, step-level replay, RDMA P2P weight sync — + weeks-long runs survive routine hardware faults. +- **First-class agentic rollout.** Tool use, search, code execution, and asynchronous + multi-agent co-evolution all supported through clean Python extension points. +- **Customize without forking.** Twenty-plus plug-points let you replace the rollout, + reward, loss, filter, or Megatron hook through CLI flags. + +## Design principles + +**Small core, many edges.** The trainer is a short Python program; almost every +behaviour is swappable through a `--*-path` flag rather than a code patch. + +**Match the hardware.** Miles is designed around NVLink, InfiniBand, and RDMA — at +trillion-parameter scale, the interconnect is the rate limiter, so we optimize for it +first. + +**Systems-first, algorithms-second.** We chase the instability that kills production +runs (routing mismatch, precision drift, NCCL hangs) before chasing the next algorithm. + +## Try it + +Head to the [Quick Start](../getting-started/quick-start.md) for a quick GRPO +run on Qwen3-4B with a single 8-GPU node. + +## What's next + +- FSDP-only trainer path for teams without Megatron expertise. +- Built-in Chrome-trace profiling and Grafana dashboards. +- Adaptive per-source data weighting driven by rolling reward signals. +- Deeper VLM multi-turn support. + +The repo is on GitHub: [github.com/radixark/miles](https://github.com/radixark/miles). +Come say hi in the `#miles` channel of the [SGLang Slack](https://slack.sglang.ai). diff --git a/docs/examples/fully-async.md b/docs/examples/fully-async.md new file mode 100644 index 00000000000..d5c4fbebb87 --- /dev/null +++ b/docs/examples/fully-async.md @@ -0,0 +1,208 @@ +--- +title: Fully Async Rollout +description: Keep generation running continuously in the background so the trainer never waits. +--- + +# Fully Async Rollout + +**What you'll learn:** how to make rollout production and trainer consumption fully +parallel, with a queue in between, by using a custom rollout function. + +In the default training loop, every iteration looks like: + +```text +for it in range(N): + prompts = sample() # cheap + responses = generate() # 10-30s + rewards = score() # 1-3s + loss = train_step() # 5-20s + sync_weights() # 1-10s +``` + +`generate()` blocks `train_step()`. With Async Rollout the loop is split: a background +thread runs `generate` continuously, and the trainer drains a queue. The two run in +parallel and the wall-clock time per iteration drops to roughly `max(generate, train)` +instead of the sum. + +## Prerequisites + +* You completed the [Qwen3-4B](../models/qwen/qwen3.md) recipe (or have an + equivalent model + dataset). +* Comfortable with [Customization](../user-guide/customization.md) — async rollout uses + a custom rollout function. + +## Files + +```text +examples/fully_async/ +├── fully_async_rollout.py # AsyncRolloutWorker + entry function +├── run-qwen3-4b-fully_async.sh # launch script (Qwen3-4B) +└── run_qwen3_30b_a3b_fully_async.py # MoE variant +``` + +## Quick start + +```bash +cd /root/miles +bash examples/fully_async/run-qwen3-4b-fully_async.sh +``` + +You should see: + +```text +Creating new global async worker... +Continuous async rollout worker started +[trainer] iter 1/3000 | drained 32 samples (queued: 18) +``` + +## What changes vs. the default recipe + +Just two flags: + +```diff +- python3 train.py ... ++ python3 train_async.py ... ++ --rollout-function-path fully_async_rollout.generate_rollout_fully_async +``` + +Everything else — model args, optimizer, GRPO config — stays the same. + +## Walkthrough + +The interesting code is small. Here's the global worker manager: + +```python fully_async_rollout.py +_global_worker = None +_worker_lock = threading.Lock() + +def get_global_worker(args, data_buffer): + global _global_worker + with _worker_lock: + if _global_worker is None or not _global_worker.worker_thread.is_alive(): + print("Creating new global async worker...") + _global_worker = AsyncRolloutWorker(args, data_buffer, + concurrency=args.sglang_server_concurrency) + _global_worker.start() + return _global_worker +``` + +Key points: + +* **Singleton.** One worker per process — multiple `train.py` calls share it. +* **Thread + asyncio loop.** Cheaper than a subprocess; SGLang HTTP calls are I/O-bound, + so an asyncio loop in a single thread saturates them. +* **`atexit` hook.** Worker is torn down when the process exits — no orphaned + generation tasks. + +The worker itself keeps `--rollout-batch-size` tasks in flight using +`generate_and_rm_group`: + +```python +async def _producer(self): + while not self._stop: + if len(self._inflight) < self.target_inflight: + self._inflight.add(asyncio.create_task(self._launch_one())) + done, self._inflight = await asyncio.wait( + self._inflight, return_when=asyncio.FIRST_COMPLETED + ) + for task in done: + self._output_queue.put(task.result()) +``` + +And the trainer-side entry simply drains: + +```python +async def generate_rollout_fully_async(args, rollout_id, *, evaluation=False): + worker = get_global_worker(args, data_buffer) + samples = [] + while len(samples) < args.global_batch_size: + samples.append(worker._output_queue.get(timeout=600)) + return RolloutFnTrainOutput(samples=samples) +``` + +## What's happening underneath + +```mermaid +sequenceDiagram + participant T as train_async.py + participant W as AsyncRolloutWorker + participant S as SGLang engines + + par Background + loop forever + W->>S: generate(prompt) + S-->>W: response + W->>W: enqueue + end + and Foreground + loop each iteration + T->>W: drain N samples + W-->>T: samples + T->>T: train_step + T->>S: weight_sync + end + end +``` + +The producer loop is decoupled from the consumer loop. As long as the queue stays +populated, the trainer never blocks on generation. + +## Tuning knobs + +| Knob | Effect | +|---|---| +| `--rollout-batch-size` | Worker target in-flight count | +| `--sglang-server-concurrency` | Per-engine concurrency cap | +| `--num-steps-per-rollout` | Increase to consume more per drain (off-policy) | + +If queue depth grows unbounded, training is slower than rollout — bump +`--num-steps-per-rollout` (you'll be slightly off-policy) or scale up trainer +parallelism. + +If queue depth stays at 0, rollout is the bottleneck — that's where async helps least +because there's nothing waiting to be consumed. + +## What to watch + +```text +async/queue_depth stable (50-200 typical) +async/producer_throughput_qps consistent +async/consumer_drain_seconds < producer cycle time +``` + +If `consumer_drain_seconds > producer_cycle_time`, your trainer is starving the queue — +check GPU utilization. + +## Limitations + +* **No evaluation mode in this example.** Eval still runs through the synchronous path + in `train_async.py`. Adding async eval is straightforward — copy the worker pattern + and use `evaluation=True`. +* **Best-effort ordering.** Samples are sorted by index at drain time, but exact-order + guarantees aren't provided. +* **Minimal error handling.** If a generate task throws, it's logged but the worker + keeps going. Production users wire in [fault tolerance](../advanced/fault-tolerance.md). + +## Variations + +### Async on a 30 B MoE + +`run_qwen3_30b_a3b_fully_async.py` shows the same pattern with `tp=4 ep=8` and +`--sglang-enable-ep-moe`. The only practical difference is increasing +`--rollout-batch-size` to 64+ to keep the larger engine pool fed. + +### Async + R3 + +Async rollout and R3 stack cleanly. Add: + +```bash +GRPO_ARGS+=( --use-rollout-routing-replay ) +``` + +The custom rollout function automatically passes `return_routed_experts=true` because +it uses `generate_and_rm_group` under the hood. + +### Async + partial rollout + +If you also use `--partial-rollout`, half-finished trajectories are saved to disk and +resumed — useful when the worker is killed mid-flight. diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 00000000000..ef6e96ef2fb --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,76 @@ +--- +title: Examples +description: Annotated end-to-end walkthroughs for the workflows people actually want to build. +--- + +# Examples + +The model recipes show you how to train a model. The examples below show you how to +*build something useful* with Miles — tools, search, multi-agent, distillation, and +async rollout. + +Each example follows the same template: + +1. **What you'll learn** — the takeaway in one sentence. +2. **Prerequisites** — what you need installed/downloaded first. +3. **Files** — what's in the example directory. +4. **Quick start** — single command to run. +5. **Walkthrough** — annotated tour of the key code. +6. **What's happening underneath** — the moving parts you can't see. +7. **Tuning knobs** — the levers that matter. +8. **Troubleshooting** — the failure modes we've actually hit. +9. **Variations** — common adaptations. + +## The catalogue + + + + + + Continuous background generation with a queue between rollout and training. + Up to 2× end-to-end speedup. + + + + + + Multi-turn rollout where the model can issue `...` actions, get + observations from a retrieval server, and produce a final answer. + + + + + + SFT + RL pipeline for tool-augmented reasoning. Sandboxed Python code execution + interleaved with thinking. + + + + + + Two specialized agents (e.g. doctor + patient) train together and improve + each other. + + + + + + Bit-stable training across reruns. Determinism flags, seeds, and what to + watch. + + + + + + Plain SFT (no RL) — sometimes you just need a quick fine-tune. + + + + + +## Where to start + +* **Never used Miles for anything beyond GRPO?** → [Fully Async Rollout](fully-async.md). +* **Want tool use / RAG?** → [Search-R1](search-r1.md), then [ReTool](retool.md). +* **VLM / multi-agent?** → [Multi-Agent Co-Evolution](multi-agent.md). +* **Replay an old result?** → [Reproducibility Recipe](reproducibility.md). diff --git a/docs/examples/multi-agent.md b/docs/examples/multi-agent.md new file mode 100644 index 00000000000..4c5b559280f --- /dev/null +++ b/docs/examples/multi-agent.md @@ -0,0 +1,200 @@ +--- +title: Multi-Agent Co-Evolution +description: Two specialized agents train together and improve each other. +--- + +# Multi-Agent Co-Evolution + +**What you'll learn:** how to wire up an asynchronous multi-agent system in Miles, where +two (or more) specialized agents take alternating turns and the joint outcome drives a +single shared reward. + +This example uses a dual-agent setup that interleaves a "thinker" and a "verifier", but +the same pattern scales to: + +* Doctor / patient simulations. +* Multi-step DeepResearch pipelines. +* Adversarial games (proposer / solver). + +The supporting framework for the production version of this is +[MrlX](https://github.com/AQ-MedAI/MrlX) — Miles ships the kernel of the same idea so +you can hack on it without pulling in MrlX's full dependency tree. + +## Prerequisites + +* You've completed the [Qwen3-30B-A3B](../models/qwen/qwen3-moe.md) recipe (the + example uses that model). +* Familiar with [Customization](../user-guide/customization.md). + +## Files + +```text +examples/multi_agent/ +├── agent_system.py # the agent state machine +├── prompts.py # role / system prompts +├── rollout_with_multi_agents.py # custom rollout (calls agent_system) +└── run-qwen3-30B-A3B-multi-agent.sh # launch script +``` + +## Quick start + +```bash +cd /root/miles +bash examples/multi_agent/run-qwen3-30B-A3B-multi-agent.sh +``` + +## Configuration + +```python +MULTI_AGENT_CONFIGS = { + "custom_multi_agent_function_path": + "examples.multi_agent.agent_system.run_agent_system", + "num_parallel": 5, # parallel agent runs per prompt + "incorrect_reward_weight": 0.8, # weight on agent A's reward when wrong + "correct_reward_weight": 1.2, # weight on agent A's reward when right +} +``` + +Asymmetric reward weighting (0.8 / 1.2) gives a small bias toward upweighting "correct" +trajectories, which empirically stabilizes early training when most attempts fail. + +## Launch script highlights + +```bash +ROLLOUT_ARGS=( + --custom-generate-function-path \ + examples.multi_agent.rollout_with_multi_agents.generate_with_multi_agents + --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-context-len 16384 # entire conversation budget + --rollout-max-response-len 8192 # per-turn cap + + --global-batch-size 256 + --balance-data +) +``` + +Two flags matter most: + +* `--rollout-max-context-len` — total context budget across all turns. Larger than + `--rollout-max-response-len` because we accumulate. +* `--global-batch-size 256 = 32 × 8` — matches the rollout invariant. + +## Walkthrough — the agent loop + +The shipped pipeline is **solver → rewriter → selector**: `num_parallel` solver +attempts run in parallel, each rewriter rewrites the previous solutions, and a +`SelectorAgent` picks one. Sampling params are set on `args` upstream by the rollout +helper, so `run_agent_system` only takes `(args, sample)`. + +```python agent_system.py +async def run_agent_system(args, sample): + args = deepcopy(args) + args.sample = sample + args.results_dict = {"solver": [], "rewriter": [], "selector": []} + + problem_statement = sample.prompt + + # 1. Solver: num_parallel attempts in parallel. + tasks = [solver_worker(args, problem_statement, i) + for i in range(args.num_parallel)] + solver_solutions = await asyncio.gather(*tasks, return_exceptions=True) + rewards = await batched_async_rm(args, args.results_dict["solver"]) + for s, r in zip(args.results_dict["solver"], rewards): + s.reward = r + + previous = [r for r in solver_solutions if isinstance(r, str)] + + # 2. Rewriter: each worker rewrites the previous solutions. + tasks = [rewrite_worker(args, previous, problem_statement, i) + for i in range(args.num_parallel)] + rewritten = [r for r in await asyncio.gather(*tasks, return_exceptions=True) + if isinstance(r, str)] + + # 3. Selector: pick one of the rewritten solutions. + selector = SelectorAgent() + response = await selector.select(args, problem_statement, rewritten) + + # ... apply asymmetric reward weighting using + # args.incorrect_reward_weight / args.correct_reward_weight on the solver + # and rewriter samples, then return them all together. + return args.results_dict["solver"] + args.results_dict["rewriter"] + ... +``` + +Both roles share the same SGLang process — `solver_worker`, `rewrite_worker`, and +`SelectorAgent.select` all post to the same engine, just with different prompts. So +**both agents are the same model** updating in lockstep. For *architecturally distinct* +agents (separate models), see the MrlX repo. + +## Walkthrough — rollout integration + +`rollout_with_multi_agents.py` exposes `generate_with_multi_agents(args, sample, +sampling_params, evaluation=False)`. Internally it: + +1. Sets `args.sampling_params = sampling_params` and `args.tokenizer`, then loads the + custom multi-agent function from `args.custom_multi_agent_function_path`. +2. Calls `await custom_multi_agent_func(args, sample)` to get the list of samples + produced by the solver / rewriter / selector pipeline. +3. Returns the shuffled list of `Sample`s for the trainer to pack. + +The per-sample tokenization and reward already happen inside `solver_worker` / +`rewrite_worker` / `SelectorAgent.select` (which call `batched_async_rm`), so the +rollout integration itself is a thin wrapper. + +## Tuning knobs + +| Knob | Effect | +|---|---| +| `MAX_TURNS` | Conversation depth — longer = more context = slower | +| `incorrect_reward_weight` / `correct_reward_weight` | Asymmetric shaping | +| `num_parallel` | Rollouts per prompt running concurrently | +| `--rollout-max-context-len` | Stops the conversation when budget is hit | + +## What to watch + +```text +multi_agent/avg_turns 2.5 – 4.0 +multi_agent/early_termination_rate 0.4 – 0.6 (reaches ) +multi_agent/conversation_token_count 4096 – 12288 +loss_mask/role_split balanced (~50/50) +reward/avg trending up +``` + +If `loss_mask/role_split` is heavily skewed, one role is dominating — typically the +verifier becomes verbose. Tighten its system prompt or reduce its `max_tokens`. + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| OOM mid-rollout | Reduce `MAX_TURNS` or `--rollout-max-context-len` | +| Both agents repeat each other | Verifier prompt is too permissive — make it adversarial | +| Reward never moves | Check that `` extraction matches the verifier output | +| Rollout much slower than baseline | Per-turn SGLang RTT × MAX_TURNS — consider async rollout | + +## Variations + +### VLM multi-turn + +Replace `call_role` with a VLM-aware caller that includes images in messages. Miles +supports VLM multi-turn natively — same pattern, just `multimodal_train_inputs` in the +sample dict (see [Customization #13](../user-guide/customization.md#training)). + +### True asymmetric agents + +Run two SGLang services — one per agent — and have your rollout function call the +appropriate URL per turn. The trainer can either train both jointly (one optimizer per +model) or train one and freeze the other (PvE). + +### Adversarial pairing + +Instead of a verifier, the second agent is an adversary that tries to find weaknesses +in the thinker's answer. Reward both: thinker for surviving, adversary for breaking. +This is the seed of self-play RLHF. diff --git a/docs/examples/openhermes-sft.md b/docs/examples/openhermes-sft.md new file mode 100644 index 00000000000..183bfeffa35 --- /dev/null +++ b/docs/examples/openhermes-sft.md @@ -0,0 +1,164 @@ +--- +title: SFT on OpenHermes +description: Plain supervised fine-tuning of Qwen3-4B-Base on the OpenHermes-2.5 dataset. +--- + +# SFT on OpenHermes + +**What you'll learn:** how to use Miles for plain supervised fine-tuning. No RL, no +rollout, no reward — just data → loss → optimizer. + +Why use Miles for SFT? Two reasons: + +1. **Same launch convention as your RL run** — one config, one Ray cluster. +2. **Async data prefetching** — the SFT loop reuses the rollout machinery to overlap + data loading with training. + +## Prerequisites + +* You completed the [Qwen3-4B](../models/qwen/qwen3.md) recipe (we reuse the + conversion). +* ~50 GB free disk for OpenHermes-2.5. + +## Quick start + +### 1. Convert Qwen3-4B-Base + +If you don't already have it: + +```bash +hf download Qwen/Qwen3-4B-Base --local-dir /root/Qwen3-4B-Base + +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/Qwen3-4B-Base \ + --save /root/Qwen3-4B-Base_torch_dist +``` + +### 2. Prepare the dataset + +OpenHermes ships in a custom shape. Convert to OpenAI messages format: + +```python +from datasets import load_dataset + +ds = load_dataset("teknium/OpenHermes-2.5")["train"] + +def convert(sample): + role_map = {"human": "user", "gpt": "assistant", "system": "system"} + return { + "messages": [ + {"role": role_map[turn["from"]], "content": turn["value"]} + for turn in sample["conversations"] + ] + } + +ds = ds.map(convert) +ds.to_parquet("/root/openhermes2_5.parquet") +``` + +### 3. Run + +```bash +bash scripts/run-qwen3-4B-base-sft.sh +``` + +## What changes vs. the GRPO recipe + +Compare to [run-qwen3-4B.sh](../models/qwen/qwen3.md). The deltas: + +```diff +- python3 train.py ++ python3 train_async.py # async for data prefetch + +- ROLLOUT_ARGS=( ... GRPO knobs, n-samples-per-prompt, ... ) ++ SFT_ARGS=( ++ --rollout-function-path miles.rollout.sft_rollout.generate_rollout ++ --prompt-data /root/openhermes2_5.parquet ++ --input-key messages ++ --rollout-shuffle ++ --num-epoch 3 ++ --rollout-batch-size 128 ++ --global-batch-size 128 ++ ++ --loss-type sft_loss ++ --calculate-per-token-loss ++ --disable-compute-advantages-and-returns ++ --debug-train-only ++ ) + +- GRPO_ARGS=( ... ) # removed entirely +- SGLANG_ARGS=( ... ) # removed — no inference needed +``` + +## Why each flag + +| Flag | Why | +|---|---| +| `--rollout-function-path miles.rollout.sft_rollout.generate_rollout` | Read from disk instead of generating | +| `--rollout-batch-size = --global-batch-size` | One batch read = one optimizer step | +| **No** `--n-samples-per-prompt` | SFT has one target per input | +| `--loss-type sft_loss` | Cross-entropy instead of policy-gradient | +| `--calculate-per-token-loss` | Standard SFT averages over unmasked tokens | +| `--disable-compute-advantages-and-returns` | No advantage / return needed | +| `--debug-train-only` | Skip SGLang init (we don't need rollout) | +| `train_async.py` | Async data prefetch overlaps load with train | + +## What to watch + +```text +sft/loss decreasing +sft/per_token_loss decreasing (mirrors loss when using per-token) +sft/tokens_seen steadily increasing +sft/epoch 0 → num_epoch +data/prefetch_queue_depth > 0 (else loader is the bottleneck) +``` + +If `data/prefetch_queue_depth` stays at 0, your data loader is too slow — increase +worker count or use parquet (we already do). + +## Tuning knobs + +| Knob | Effect | +|---|---| +| `--num-epoch` | Total passes over dataset | +| `--rollout-batch-size` | Bigger = better GPU utilization, more memory | +| `--max-tokens-per-gpu` | As always — push it up until OOM | +| `--lr` | SFT typically `1e-5` to `5e-5` (10× higher than RL) | +| `--lr-decay-style cosine --lr-warmup-iters 100` | Standard SFT schedule | + +## Variations + +### Mix datasets + +Pass multiple `--prompt-data` entries: + +```bash +SFT_ARGS+=( + --prompt-data \ + hermes /root/openhermes2_5.parquet \ + slimorca /data/slimorca.parquet +) +``` + +Per-source loss is logged separately. + +### Continue with RL + +After SFT, point the RL run at the SFT checkpoint: + +```bash +CKPT_ARGS=( + --hf-checkpoint /root/Qwen3-4B-Base + --ref-load /root/Qwen3-4B-Base_torch_dist # original (anchor) + --load /root/Qwen3-4B-Base_sft/ # SFT output (start point) + --save /root/Qwen3-4B-Base_rl/ +) +``` + +### LoRA SFT + +Use the LoRA hooks (`--lora-rank 16`) to keep VRAM low when fine-tuning a +larger model. See `examples/lora/` in the repo. diff --git a/docs/examples/reproducibility.md b/docs/examples/reproducibility.md new file mode 100644 index 00000000000..c12457a9da6 --- /dev/null +++ b/docs/examples/reproducibility.md @@ -0,0 +1,146 @@ +--- +title: Reproducibility Recipe +description: Bit-stable training across reruns. Determinism flags, seeds, and what to watch. +--- + +# Reproducibility Recipe + +**What you'll learn:** how to configure Miles + SGLang + Megatron for **bit-wise +reproducible** RL training. Same inputs → identical outputs across reruns, machines, +and time. + +Reproducibility matters more than people admit: it's the only way to A/B test changes +in a system this complex. If you can't trust that "I changed flag X and reward went up", +you're flying blind. + +## How it works + +Bit-wise reproducibility requires **three** independent stacks to be deterministic: + +1. **Inference (SGLang)** — every kernel must be deterministic. +2. **Training (Megatron-LM)** — same. +3. **Communication (NCCL)** — algorithm choice and CUBLAS workspace can be + non-deterministic by default. + +Miles ships verified configurations that switch all three into deterministic mode. + +## Quick start + +We use the smallest Miles model (Qwen2.5-0.5B) on GSM8K so the loop fits in 5 minutes +and you can reproduce the bit-stability check yourself. + +### 1. Disable FA3 + +Flash-Attention 3 currently has non-deterministic backward kernels. Drop it: + +```bash +pip uninstall flash_attn_3 -y +``` + +### 2. Set the deterministic flags + +```bash +SGLANG_ARGS+=( + --sglang-enable-deterministic-inference + --sglang-attention-backend flashinfer +) + +PERF_ARGS+=( + --deterministic-mode +) +``` + +### 3. Set the env vars (Ray `env_vars`) + +```python +"env_vars": { + "NCCL_ALGO": "Ring", + "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", + "CUBLAS_WORKSPACE_CONFIG": ":4096:8" +} +``` + +| Variable | Why | +|---|---| +| `NCCL_ALGO=Ring` | Forces a deterministic collective algorithm | +| `NVTE_ALLOW_NONDETERMINISTIC_ALGO=0` | Disables non-deterministic Transformer-Engine kernels | +| `CUBLAS_WORKSPACE_CONFIG=:4096:8` | cuBLAS's deterministic workspace allocation | + +### 4. Download + convert + run + +```bash +hf download --repo-type dataset openai/gsm8k --local-dir /root/gsm8k +hf download Qwen/Qwen2.5-0.5B-Instruct --local-dir /root/Qwen2.5-0.5B-Instruct + +cd /root/miles +source scripts/models/qwen2.5-0.5B.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen2.5-0.5B-Instruct \ + --save /root/Qwen2.5-0.5B-Instruct_torch_dist/ + +bash examples/reproducibility/run-qwen2.5-0.5B-gsm8k.sh +``` + +### 5. Verify + +Run twice, then: + +```bash +md5sum /root/Qwen2.5-0.5B-Instruct_torch_dist_run1/iter_0000020/*.pt +md5sum /root/Qwen2.5-0.5B-Instruct_torch_dist_run2/iter_0000020/*.pt +``` + +The hashes should match exactly. If they don't, something escaped the deterministic +configuration — see troubleshooting below. + +## What's deterministic and what isn't + +| Component | Default | Deterministic mode | +|---|---|---| +| Megatron forward | non-det | ✅ via `--deterministic-mode` | +| Megatron backward | non-det | ✅ | +| SGLang kernels | non-det | ✅ via `--sglang-enable-deterministic-inference` | +| Flash-Attn 3 | non-det | ❌ — uninstall | +| NCCL collectives | non-det | ✅ via `NCCL_ALGO=Ring` | +| cuBLAS GEMM | non-det | ✅ via `CUBLAS_WORKSPACE_CONFIG` | +| TE fused kernels | non-det | ✅ via `NVTE_ALLOW_NONDETERMINISTIC_ALGO=0` | +| Python dataloader shuffle | seeded | ✅ already | + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| Hashes diverge after iter 1 | Flash-Attn 3 still installed | +| Hashes match for trainer but not SGLang | `--sglang-attention-backend flashinfer` not set | +| Hashes diverge across nodes | `NCCL_ALGO=Ring` not propagated to all workers | +| Hashes match locally but not on a different machine | cuDNN version mismatch | + +## Cost of determinism + +Roughly: + +| Component | Throughput cost | +|---|---| +| Megatron deterministic mode | -3% to -8% | +| SGLang deterministic | -10% to -15% | +| NCCL Ring | -2% (vs. Tree) | +| Drop FA3 | -10% to -25% | + +Total: ~25% slower for full bit-wise determinism. Worth it for debugging, science, and +A/B tests; usually disabled for production runs. + +## When to disable determinism + +* Production training runs where the cost is too high. +* When you've already nailed the result and want maximum throughput. +* On hardware that doesn't support deterministic kernels. + +For most other cases — **keep it on while developing**. The hours you save when "I +swear that worked yesterday" stops being a phrase you say will pay back the throughput +many times over. + +## References + +* [SGLang deterministic inference blog](https://lmsys.org/blog/2025-09-22-sglang-deterministic/) +* Megatron-LM deterministic-mode docs diff --git a/docs/examples/retool.md b/docs/examples/retool.md new file mode 100644 index 00000000000..8ea7478a939 --- /dev/null +++ b/docs/examples/retool.md @@ -0,0 +1,223 @@ +--- +title: ReTool — Code Execution Tool Use +description: SFT + RL pipeline that teaches a model to interleave thinking with sandboxed Python execution. +--- + +# ReTool — Code Execution Tool Use + +**What you'll learn:** the full SFT → RL pipeline for tool-augmented reasoning, with +sandboxed Python execution and a reward function that checks both the final answer and +the tool-use trace. + +This is the canonical example for teaching a model to **think with code** — write a +Python snippet, get the result, continue reasoning, repeat. + +## Prerequisites + +* Familiar with [Search-R1](search-r1.md) — same general pattern. +* `radixark/miles:latest` container. +* ~150 GB free disk for the SFT data + base model. + +## Files + +```text +examples/retool/ +├── generate_with_retool.py # multi-turn rollout with code-interpreter +├── tool_sandbox.py # sandboxed Python execution +├── sft_data_processing.py # SFT data prep +├── rl_data_preprocess.py # RL data prep +├── retool_qwen3_4b_sft.sh # SFT launch +└── retool_qwen3_4b_rl.sh # RL launch +``` + +## The pipeline + +```mermaid +flowchart LR + A[ReTool-SFT dataset] --> B[SFT on Qwen3-4B] + B --> C[SFT model] + C --> D[RL on dapo-math-17k] + D --> E[Final tool-using model] + style B fill:#21d4fd,stroke:#0b1020,color:#0b1020 + style D fill:#b721ff,stroke:#0b1020,color:#0b1020 +``` + +You can skip the SFT phase by using the pre-trained checkpoint we publish: +`font-info/qwen3-4b-sft-SGLang-RL`. + +## Quick start (RL only) + +```bash +# 1. Download model + datasets +hf download font-info/qwen3-4b-sft-SGLang-RL --local-dir /root/font-info/qwen3-4b-sft +hf download --repo-type dataset BytedTsinghua-SIA/DAPO-Math-17K --local-dir /root/dapo-math-17k +hf download --repo-type dataset zhuzilin/aime-2024 --local-dir /root/aime-2024 + +# 2. Convert +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/font-info/qwen3-4b-sft \ + --rotary-base 5000000 \ + --save /root/font-info/qwen3-4b-sft_torch_dist + +# 3. Run +bash examples/retool/retool_qwen3_4b_rl.sh +``` + +## Full pipeline (SFT then RL) + +```bash +# 0. Download SFT data + base model +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 + +# 1. Convert (note the non-default rotary base) +source scripts/models/qwen3-4B.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen/Qwen3-4B-Instruct-2507 \ + --rotary-base 5000000 \ + --save /root/Qwen/Qwen3-4B-Instruct-2507_torch_dist + +# 2. SFT data prep + run +python examples/retool/sft_data_processing.py +bash examples/retool/retool_qwen3_4b_sft.sh + +# 3. RL (uses output of SFT) +bash examples/retool/retool_qwen3_4b_rl.sh +``` + + + +**rotary-base 5000000.** Qwen3-4B-Instruct-2507 ships with `rotary_base=5_000_000`. If you forget the +`--rotary-base 5000000` override during conversion, you'll see garbled generations +that look like the model has been lobotomized. Don't skip it. + + + +## Walkthrough — tool format + +The model is taught a strict tool-call grammar: + +```text +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": { + "name": "code_interpreter", + "description": "A tool for executing code.", + "parameters": {"type": "object", "properties": { + "code": {"type": "string", "description": "The code to execute."} + }, "required": ["code"]} +}} + + +For each function call, return a json object with function name and arguments +within XML tags: + +{"name": "code_interpreter", "arguments": {"code": "print(2+2)"}} + +``` + +Output from the interpreter is fed back as `...` and +masked out of the loss. + +## Walkthrough — sandbox + +`tool_sandbox.py` keeps execution **boring** — Python, no shell, restricted modules, +hard memory + time caps. Defaults live in a module-level `TOOL_CONFIGS` dict, and the +executor is `PythonSandbox`: + +```python +TOOL_CONFIGS = { + "python_timeout": 120, # seconds + "python_memory_limit": "4GB", + "tool_concurrency": 32, + # ... +} + +class PythonSandbox: + def __init__(self, timeout: int = 10, memory_limit: str = "100MB"): + self.timeout = timeout + self.memory_limit = memory_limit + self.allowed_modules = {"math", "random", "datetime", "collections", + "itertools", "functools", "operator", + "statistics", "decimal", "fractions"} + + async def execute_code(self, code: str) -> str: + ... +``` + +Every execution runs in its own subprocess. If you need a richer sandbox (network, +filesystem), swap in a container-based runner — the interface is just +`PythonSandbox.execute_code(code) -> str`. + +## Walkthrough — reward + +```python +async def reward_func(args, sample, **kwargs): + parsed = parse_trace(sample.response) + correct = check_answer(parsed.final_answer, sample.label) + + # 1. Final answer (most weight) + r_ans = 1.0 if correct else 0.0 + + # 2. Format bonus + r_fmt = 0.1 if parsed.has_valid_format else 0.0 + + # 3. Tool-use bonus (small, just to encourage tool calls) + r_use = 0.05 if parsed.num_tool_calls > 0 else 0.0 + + return r_ans + r_fmt + r_use +``` + +The shaping rewards are deliberately small. The dominant signal is correctness — the +shape and tool use just keep gradient alive in the early epochs. + +## Tuning knobs + +| Knob | Effect | +|---|---| +| `TOOL_CONFIGS["python_memory_limit"]` | Sandbox per-call memory | +| `TOOL_CONFIGS["python_timeout"]` | Sandbox per-call wallclock | +| `PythonSandbox.allowed_modules` | What modules the model can import | +| `--rollout-max-response-len` | Total response cap (incl. tool I/O) | +| `format / tool-use bonus weights` | How aggressively to shape early | + +## What to watch + +```text +retool/tool_calls_per_sample 1.5 – 3.0 +retool/tool_success_rate > 0.85 +retool/avg_code_length_chars 50 – 200 +reward/exact_match trending up +reward/format_bonus ~0.10 +sandbox/timeout_rate < 0.02 +``` + +If `sandbox/timeout_rate` climbs, the model is generating expensive code. Either bump +`TOOL_CONFIGS["python_timeout"]` or restrict `PythonSandbox.allowed_modules` (numpy is +the usual suspect). + +## Troubleshooting + +| Problem | Fix | +|---|---| +| `ModuleNotFoundError` from sandbox | Add the module to `PythonSandbox.allowed_modules` | +| Tool response not masked | Check `` boundary regex matches your output | +| EM = 0 forever | Verify your answer extractor — check a few samples manually | +| Sandbox returns `MemoryError` | Bump `TOOL_CONFIGS["python_memory_limit"]`, or restrict `numpy` calls | + +## Variations + +* **Use a different tool.** Replace the sandbox with shell, search, calculator, or any + other function. The grammar in `` is the only contract. +* **Multiple tools.** Add more entries to the `` block — the model picks per + call. +* **Skip SFT.** The pre-SFT'd model is published; for many tasks the RL phase alone is + enough. +* **GRPO with multiple trajectories.** `--n-samples-per-prompt 8` works as expected; + each trajectory has its own tool-execution trace. diff --git a/docs/examples/search-r1.md b/docs/examples/search-r1.md new file mode 100644 index 00000000000..9c41f320bc2 --- /dev/null +++ b/docs/examples/search-r1.md @@ -0,0 +1,278 @@ +--- +title: Search-R1 (Tool Use) +description: Train a model to issue search queries, integrate observations, and answer multi-turn QA. +--- + +# Search-R1 — Tool-augmented multi-turn RL + +**What you'll learn:** how to wire up a tool (web search) into a Miles training loop — +custom multi-turn rollout, observation interleaving, reward function, and TIS to keep +training stable when train ≠ inference. + +This is a Miles-friendly reproduction of the original +[Search-R1](https://github.com/PeterGriffinJin/Search-R1). + +## Prerequisites + +* `radixark/miles:latest` container. +* Either a serper.dev API key (Google search backend) or ~135 GB free disk for the + local Wikipedia retriever (see [appendix](#appendix-local-wikipedia-retriever)). +* You completed [Customization](../user-guide/customization.md) — this example uses a + custom rollout function and reward. + +## Files + +```text +examples/search-r1/ +├── generate_with_search.py # custom rollout (multi-turn loop) +├── google_search_server.py # serper.dev wrapper +├── local_search_server.py # FastAPI server in front of FAISS index +├── local_dense_retriever/ # E5-base index/corpus downloader +├── qa_em_format.py # exact-match reward +└── run_qwen2.5_3B.sh # launch script +``` + +## Quick start + +### 1. Set up environment + +```bash +cd /root && git clone https://github.com/radixark/miles.git +cd miles && pip install -e . --no-deps && pip install chardet +``` + +### 2. Prepare data + +```bash +git clone https://github.com/PeterGriffinJin/Search-R1.git +cd Search-R1 && pip install -e . --no-deps && pip install tensordict + +WORK_DIR=/root/Search-R1 +LOCAL_DIR=$WORK_DIR/data/nq_hotpotqa_train +python $WORK_DIR/scripts/data_process/qa_search_train_merge.py \ + --local_dir $LOCAL_DIR \ + --data_sources nq,hotpotqa +``` + +### 3. Convert the model + +```bash +hf download Qwen/Qwen2.5-3B --local-dir /root/Qwen2.5-3B +cd /root/miles +source scripts/models/qwen2.5-3B.sh +PYTHONPATH=/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ + ${MODEL_ARGS[@]} \ + --hf-checkpoint /root/Qwen2.5-3B \ + --save /root/Qwen2.5-3B_torch_dist +``` + +### 4. Run + +```bash +bash examples/search-r1/run_qwen2.5_3B.sh +``` + +## Configuration + +Open `generate_with_search.py` and edit `SEARCH_R1_CONFIGS`: + +```python +SEARCH_R1_CONFIGS = { + "max_turns": 2, + "topk": 3, + "search_concurrency": 256, + "search_backend": "local", # or "google" + + "local": { + "search_url": "http://127.0.0.1:8000/retrieve", + "proxy": None, + }, + + "google": { + "api_key": "your_serper_key", + "snippet_only": True, + "proxy": None, + }, + + "return_logprob": True, # required for TIS + "format_score": 0.2, +} +``` + +## Walkthrough — multi-turn rollout + +The custom rollout lives in `generate_with_search.py:generate`. The loop is +straightforward but every step matters: + +```python +async def generate(args, sample: Sample, sampling_params) -> Sample: + prompt = build_prompt(sample) + full_response, loss_masks, tokens = "", [], [] + + for turn in range(SEARCH_R1_CONFIGS["max_turns"]): + # 1. Model generates an action + out = await call_sglang(prompt + full_response, sampling_params) + toks = tokenize(out.text) + full_response += out.text + tokens += toks + loss_masks += [1] * len(toks) # model tokens count toward loss + + # 2. Parse action + action, content = parse_action(out.text) + + # 3. Run the tool + if action == "search": + result = await search_backend(content, topk=SEARCH_R1_CONFIGS["topk"]) + obs_text = render_observation(result) + obs_toks = tokenize(obs_text) + full_response += obs_text + tokens += obs_toks + loss_masks += [0] * len(obs_toks) # observation tokens MASKED OUT + elif action == "answer": + break + + sample.response = full_response + sample.tokens = tokens + sample.loss_mask = loss_masks + sample.metadata["turns_used"] = turn + 1 + return sample +``` + +### The two crucial details + +1. **Loss masking.** Tool/observation tokens get `loss_mask=0`. Without this, the model + learns to *predict the search results*, which is both wrong and wildly unhelpful. +2. **Tokenization alignment.** The model must see and the trainer must score the + *exact same tokens*. Pre-tokenizing vs. re-tokenizing at training time can drift — + that's where the [chat template verifier](../user-guide/agentic-chat-template.md) + matters. + +## Walkthrough — reward + +```python +async def reward_func(args, sample: Sample, **kwargs) -> float: + answer = extract_final_answer(sample.response) + label = sample.label + em = exact_match(answer, label) + fmt = SEARCH_R1_CONFIGS["format_score"] if has_valid_format(sample.response) else 0 + return em + fmt +``` + +`format_score=0.2` gives partial credit for the correct `...` shape even if the +content is wrong — keeps gradient flowing during early exploration. + +## Enabling TIS + +The trajectory mixes model tokens (we want gradients) with tool tokens (we don't). +Without correction, the implicit policy ratio in the GRPO objective is *off-policy* — +the search results came from a stochastic environment, not the model. + +**Truncated Importance Sampling (TIS)** corrects for this. To enable: + +1. Set `"return_logprob": True` in `SEARCH_R1_CONFIGS`. +2. Uncomment the TIS flags in `run_qwen2.5_3B.sh`: + +```bash +GRPO_ARGS+=( --use-tis ) +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_with_cp +) +``` + +When `return_logprob=True`, response post-processing is automatically disabled to keep +token / logp alignment. + +## What to watch + +```text +search_r1/turns_per_sample ~1.5 (depends on max_turns) +search_r1/search_calls_per_sample ~1.0 +reward/exact_match trending up +reward/format ~0.18 (steady — most outputs are well-formed) +loss_mask/observation_fraction 0.4 – 0.7 (lots of obs tokens, all masked) +tis/effective_sample_size > 0.7 × batch_size +``` + +If `tis/effective_sample_size` collapses below 0.5, your inference distribution has +drifted too far. Lower `--lr` or shorten `max_turns`. + +## Tuning knobs + +| Knob | Effect | +|---|---| +| `max_turns` | More turns = more retrieval, more drift | +| `topk` | More retrieved snippets = longer context | +| `search_concurrency` | Cap on simultaneous tool calls (mind your QPS limit) | +| `format_score` | Partial credit for correct shape — higher = faster early shaping | + +## Troubleshooting + +| Problem | Fix | +|---|---| +| "Ray process stuck" | `rm -rf /root/.cache`, then `rm -rf /root/.*` if still stuck | +| Retriever 502 errors | `lsof -i :8000` — make sure your local server is alive | +| Conda activation collisions | Deactivate the `retriever` env before launching training | +| EM stays at 0 | Check the answer extractor — most often a regex mismatch | +| Loss masks shifted by one token | Tokenizer added a leading space; align with `add_special_tokens=False` | + +## Variations + +* **Use Google instead of local.** Set `"search_backend": "google"` and add an API key. +* **Different tool.** Replace `search_backend` with anything else — calculator, code + exec, internal API. The pattern is identical. +* **Group RM.** With multiple trajectories per prompt (GRPO), enable `--group-rm` so + rewards are computed in a batch. +* **Longer chains.** Bump `max_turns` to 8+ for deep-reasoning tasks. Watch + `loss_mask/observation_fraction` — if it dominates, the model is barely training. + +## Appendix — local Wikipedia retriever + +Heavy but completely offline. ~135 GB total disk and a separate conda env to avoid +conflicting with Miles. + +### One-time setup + +```bash +# 1. Conda +wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh +bash ~/miniconda.sh -b -p $HOME/miniconda3 +source ~/miniconda3/etc/profile.d/conda.sh + +conda create -n retriever python=3.10 -y && conda activate retriever +conda install pytorch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 \ + pytorch-cuda=12.1 -c pytorch -c nvidia -y +pip install transformers datasets pyserini huggingface_hub uvicorn fastapi +conda install faiss-gpu=1.8.0 -c pytorch -c nvidia -y + +# 2. Index + corpus (~135 GB) +save_path=/root/Index +python /root/miles/examples/search-r1/local_dense_retriever/download.py \ + --save_path $save_path +cat $save_path/part_* > $save_path/e5_Flat.index +gzip -d $save_path/wiki-18.jsonl.gz +``` + +### Run the server + +```bash +conda activate retriever +python /root/miles/examples/search-r1/local_dense_retriever/retrieval_server.py \ + --index_path /root/Index/e5_Flat.index \ + --corpus_path /root/Index/wiki-18.jsonl \ + --topk 3 \ + --retriever_name e5 \ + --retriever_model intfloat/e5-base-v2 \ + --faiss_gpu +``` + +5–7 GB of GPU memory per GPU. First startup is slow (model + index load); subsequent +restarts are 1–2 minutes. + +### Then launch training + +```bash +conda deactivate # don't train inside the retriever env! +cd /root/miles +bash examples/search-r1/run_qwen2.5_3B.sh +``` From 514d66150dc8f34f4015f306800e3c8af7866cbb Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Tue, 12 May 2026 10:49:25 -0700 Subject: [PATCH 0048/1130] ci: fix ci-sglang-pr and ci-megatron-pr input (#1106) --- .github/workflows/_run-ci.yml | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_run-ci.yml b/.github/workflows/_run-ci.yml index 7325a54b1ce..35a6a3c420b 100644 --- a/.github/workflows/_run-ci.yml +++ b/.github/workflows/_run-ci.yml @@ -85,8 +85,8 @@ jobs: MEGATRON_PR="${INPUT_MEGATRON_PR}" SGLANG_PR="${INPUT_SGLANG_PR}" if [ -n "$PR_BODY" ]; then - PR_MEGATRON_PR=$(echo "$PR_BODY" | grep -oP '(?<=ci-megatron-pr:\s)\S+' || true) - PR_SGLANG_PR=$(echo "$PR_BODY" | grep -oP '(?<=ci-sglang-pr:\s)\S+' || true) + PR_MEGATRON_PR=$(echo "$PR_BODY" | grep -m1 -oP '^ci-megatron-pr:\s+\K\S+' || true) + PR_SGLANG_PR=$(echo "$PR_BODY" | grep -m1 -oP '^ci-sglang-pr:\s+\K\S+' || true) [ -z "$MEGATRON_PR" ] && [ -n "$PR_MEGATRON_PR" ] && MEGATRON_PR="$PR_MEGATRON_PR" [ -z "$SGLANG_PR" ] && [ -n "$PR_SGLANG_PR" ] && SGLANG_PR="$PR_SGLANG_PR" fi @@ -95,7 +95,7 @@ jobs: resolve_fetch_ref() { local ref="$1" if [[ "$ref" =~ ^#([0-9]+)$ ]]; then - echo "pull/${BASH_REMATCH[1]}/head" + echo "refs/pull/${BASH_REMATCH[1]}/head" else echo "$ref" fi @@ -160,18 +160,50 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 + - name: Resolve dependency refs + id: resolve-refs + shell: bash + env: + PR_BODY: ${{ github.event.pull_request.body || '' }} + INPUT_MEGATRON_PR: ${{ github.event.inputs.ci_megatron_pr || '' }} + INPUT_SGLANG_PR: ${{ github.event.inputs.ci_sglang_pr || '' }} + run: | + MEGATRON_PR="${INPUT_MEGATRON_PR}" + SGLANG_PR="${INPUT_SGLANG_PR}" + if [ -n "$PR_BODY" ]; then + PR_MEGATRON_PR=$(echo "$PR_BODY" | grep -m1 -oP '^ci-megatron-pr:\s+\K\S+' || true) + PR_SGLANG_PR=$(echo "$PR_BODY" | grep -m1 -oP '^ci-sglang-pr:\s+\K\S+' || true) + [ -z "$MEGATRON_PR" ] && [ -n "$PR_MEGATRON_PR" ] && MEGATRON_PR="$PR_MEGATRON_PR" + [ -z "$SGLANG_PR" ] && [ -n "$PR_SGLANG_PR" ] && SGLANG_PR="$PR_SGLANG_PR" + fi + [ -z "$MEGATRON_PR" ] && MEGATRON_PR="miles-main" + [ -z "$SGLANG_PR" ] && SGLANG_PR="sglang-miles" + resolve_fetch_ref() { + local ref="$1" + if [[ "$ref" =~ ^#([0-9]+)$ ]]; then + echo "refs/pull/${BASH_REMATCH[1]}/head" + else + echo "$ref" + fi + } + MEGATRON_FETCH=$(resolve_fetch_ref "$MEGATRON_PR") + SGLANG_FETCH=$(resolve_fetch_ref "$SGLANG_PR") + echo "ci_megatron_pr=$MEGATRON_FETCH" >> $GITHUB_OUTPUT + echo "ci_sglang_pr=$SGLANG_FETCH" >> $GITHUB_OUTPUT + echo "Resolved: megatron=$MEGATRON_PR -> fetch=$MEGATRON_FETCH, sglang=$SGLANG_PR -> fetch=$SGLANG_FETCH" + - name: Checkout sglang uses: actions/checkout@v4 with: repository: sgl-project/sglang - ref: sglang-miles + ref: ${{ steps.resolve-refs.outputs.ci_sglang_pr }} path: sglang - name: Checkout Megatron-LM uses: actions/checkout@v4 with: repository: radixark/Megatron-LM - ref: miles-main + ref: ${{ steps.resolve-refs.outputs.ci_megatron_pr }} path: Megatron-LM - name: Install dependencies From 9d810ffaa360ba19e1f1e06a35aab90b6cee59a3 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Tue, 12 May 2026 10:49:40 -0700 Subject: [PATCH 0049/1130] ci: fix sglang-ci gpu count and fix some tests with params (#1107) --- .github/workflows/pr-test.yml | 6 +- tests/ci/run_suite.py | 2 +- tests/e2e/ckpt/test_glm47_flash_ckpt.py | 17 ++-- tests/e2e/megatron/test_qwen3_30B_A3B.py | 72 ++++++++++++--- .../sglang/test_chat_input_ids_equivalence.py | 2 +- .../e2e/sglang/test_r3_router_equivalence.py | 87 ++++++++++++------- .../sglang/test_session_server_multi_role.py | 43 ++++++--- .../sglang/test_tito_logprob_equivalence.py | 61 +++++++++---- 8 files changed, 199 insertions(+), 91 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 15aa7bda441..3bdf9983453 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -63,7 +63,7 @@ jobs: execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-b-fast-1-gpu" secrets: inherit - # Stage B: SGLang patch tests (1 GPU) + # Stage B: SGLang patch tests (8 GPU) stage-b-sglang: needs: [stage-a-fast] if: | @@ -71,8 +71,8 @@ jobs: ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-sglang'))) uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 1 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-b-sglang-1-gpu" + gpu_count: 8 + execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-b-sglang-8-gpu" secrets: inherit # Stage B: Short 8-GPU tests diff --git a/tests/ci/run_suite.py b/tests/ci/run_suite.py index 0c1ba9685a7..d3ce6044aa7 100644 --- a/tests/ci/run_suite.py +++ b/tests/ci/run_suite.py @@ -17,7 +17,7 @@ "stage-a-fast", ], HWBackend.CUDA: [ - "stage-b-sglang-1-gpu", + "stage-b-sglang-8-gpu", "stage-b-fast-1-gpu", "stage-b-short-8-gpu", "stage-c-fsdp-8-gpu", diff --git a/tests/e2e/ckpt/test_glm47_flash_ckpt.py b/tests/e2e/ckpt/test_glm47_flash_ckpt.py index ac858bbbe53..3bbbd588a5d 100644 --- a/tests/e2e/ckpt/test_glm47_flash_ckpt.py +++ b/tests/e2e/ckpt/test_glm47_flash_ckpt.py @@ -1,11 +1,12 @@ import os -from argparse import ArgumentParser from tests.ci.ci_register import register_cuda_ci import miles.utils.external_utils.command_utils as U -register_cuda_ci(est_time=1200, suite="stage-c-ckpt-8-gpu", num_gpus=8) +# Covers two ckpt modes back-to-back in one job (save + async_save, each +# followed by a load roundtrip), so est_time is roughly 2x of a single mode. +register_cuda_ci(est_time=2400, suite="stage-c-ckpt-8-gpu", num_gpus=8) ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) @@ -17,10 +18,6 @@ NUM_GPUS = 8 -parser = ArgumentParser() -parser.add_argument("--async-save", action="store_true", help="Whether to test async save/load.") - - def _get_latest_checkpointed_iteration() -> int: latest_path = f"/root/models/{MODEL_NAME}_miles/latest_checkpointed_iteration.txt" with open(latest_path, encoding="utf-8") as f: @@ -184,10 +181,10 @@ def execute(mode: str = "", ckpt_step: int | None = None): if __name__ == "__main__": - args = parser.parse_args() prepare() 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") - latest_step = _get_latest_checkpointed_iteration() - execute("load", ckpt_step=latest_step) + execute("save") + execute("load", ckpt_step=_get_latest_checkpointed_iteration()) + execute("async_save") + execute("load", ckpt_step=_get_latest_checkpointed_iteration()) diff --git a/tests/e2e/megatron/test_qwen3_30B_A3B.py b/tests/e2e/megatron/test_qwen3_30B_A3B.py index 9f06d8635a2..ffd9712260a 100644 --- a/tests/e2e/megatron/test_qwen3_30B_A3B.py +++ b/tests/e2e/megatron/test_qwen3_30B_A3B.py @@ -4,25 +4,58 @@ import miles.utils.external_utils.command_utils as U -register_cuda_ci(est_time=900, suite="stage-c-megatron-8-gpu", num_gpus=8) +# Sweeps enabled rollout / dispatcher variants in one job so the previously +# matrix-parameterized CI is back. est_time stays conservative while the +# bridge + DeepEP variant below is disabled pending a fix. +register_cuda_ci(est_time=3600, suite="stage-c-megatron-8-gpu", num_gpus=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 = True -USE_FP8_ROLLOUT = True -USE_INT4_ROLLOUT = False MODEL_NAME = "Qwen3-30B-A3B" MODEL_TYPE = "qwen3-30B-A3B" NUM_GPUS = 8 +# Each entry is one matrix variant from the legacy parameterized job. +CONFIGS: list[dict] = [ + { + "USE_DEEPEP": True, + "USE_FP8_ROLLOUT": True, + "USE_INT4_ROLLOUT": False, + "USE_BRIDGE": False, + }, + # TODO: This deepep test need fix. + # { + # "USE_DEEPEP": True, + # "USE_FP8_ROLLOUT": True, + # "USE_INT4_ROLLOUT": False, + # "USE_BRIDGE": True, + # }, + { + "USE_DEEPEP": False, + "USE_FP8_ROLLOUT": False, + "USE_INT4_ROLLOUT": False, + "USE_BRIDGE": False, + }, + { + "USE_DEEPEP": False, + "USE_FP8_ROLLOUT": False, + "USE_INT4_ROLLOUT": True, + "USE_BRIDGE": False, + }, +] + + +def _any_config(key: str) -> bool: + return any(c[key] for c in CONFIGS) + 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") - if USE_FP8_ROLLOUT: + if _any_config("USE_FP8_ROLLOUT"): U.exec_command("hf download Qwen/Qwen3-30B-A3B-FP8 --local-dir /root/models/Qwen3-30B-A3B-FP8") - if USE_INT4_ROLLOUT: + if _any_config("USE_INT4_ROLLOUT"): U.exec_command( f"python tools/convert_hf_to_int4_direct.py " f"--model-dir /root/models/{MODEL_NAME} " @@ -31,16 +64,25 @@ def prepare(): 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) + # Bridge mode reads the HF checkpoint directly without the torch_dist + # conversion, but every non-bridge variant needs it, so do the conversion + # if any variant requires it. + if not all(c["USE_BRIDGE"] for c in CONFIGS): + U.convert_checkpoint( + model_name=MODEL_NAME, + megatron_model_type=MODEL_TYPE, + num_gpus_per_node=NUM_GPUS, + ) -def execute(): +def execute(USE_DEEPEP: bool, USE_FP8_ROLLOUT: bool, USE_INT4_ROLLOUT: bool, USE_BRIDGE: bool): + ref_load = f"/root/models/{MODEL_NAME}" if USE_BRIDGE else f"/root/{MODEL_NAME}_torch_dist" if USE_INT4_ROLLOUT: - ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-INT4/ " f"--ref-load /root/{MODEL_NAME}_torch_dist " + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-INT4/ " f"--ref-load {ref_load} " elif USE_FP8_ROLLOUT: - ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-FP8 " f"--ref-load /root/{MODEL_NAME}_torch_dist " + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME}-FP8 " f"--ref-load {ref_load} " else: - ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " f"--ref-load /root/{MODEL_NAME}_torch_dist " + ckpt_args = f"--hf-checkpoint /root/models/{MODEL_NAME} " f"--ref-load {ref_load} " rollout_args = ( "--prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl " @@ -137,6 +179,9 @@ def execute(): "--colocate " ) + if USE_BRIDGE: + misc_args += "--megatron-to-hf-mode bridge " + if USE_DEEPEP: misc_args += "--moe-token-dispatcher-type flex --moe-enable-deepep " else: @@ -171,8 +216,9 @@ def execute(): 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() + for config in CONFIGS: + print(f"\n{'=' * 60}\nRunning config: {config}\n{'=' * 60}\n", flush=True) + execute(**config) diff --git a/tests/e2e/sglang/test_chat_input_ids_equivalence.py b/tests/e2e/sglang/test_chat_input_ids_equivalence.py index bcdcc740df7..507b8657890 100644 --- a/tests/e2e/sglang/test_chat_input_ids_equivalence.py +++ b/tests/e2e/sglang/test_chat_input_ids_equivalence.py @@ -7,7 +7,7 @@ from tests.e2e.sglang.utils.sglang_server import start_sglang_server from transformers import AutoTokenizer -register_cuda_ci(est_time=300, suite="stage-b-sglang-1-gpu", num_gpus=1) +register_cuda_ci(est_time=300, suite="stage-b-sglang-8-gpu", num_gpus=8) MODEL_PATH = os.environ.get("SGLANG_E2E_MODEL_PATH", "Qwen/Qwen3-0.6B") SEED = 1234 diff --git a/tests/e2e/sglang/test_r3_router_equivalence.py b/tests/e2e/sglang/test_r3_router_equivalence.py index 15c6b2a38ea..16d7d846f21 100644 --- a/tests/e2e/sglang/test_r3_router_equivalence.py +++ b/tests/e2e/sglang/test_r3_router_equivalence.py @@ -1,13 +1,15 @@ from tests.ci.ci_register import register_cuda_ci -register_cuda_ci(est_time=600, suite="stage-b-sglang-1-gpu", num_gpus=1) +# Two model families run sequentially in one job, so est_time is roughly 2x +# of a single family. +register_cuda_ci(est_time=1200, suite="stage-b-sglang-8-gpu", num_gpus=8) """E2E test: verify sglang router and miles router produce identical rollout routing replay results across MoE models. Design ~~~~~~ -For each model in ``MODEL_REGISTRY``, run the same rollout workload twice +For each model in :data:`CONFIGS`, run the same rollout workload twice under ``--debug-rollout-only --sglang-enable-deterministic-inference --use-rollout-routing-replay``: @@ -37,10 +39,13 @@ Controls ~~~~~~~~ -- ``ROUTER_EQ_MODEL_FAMILY``: ``qwen3_30b_a3b`` (default) | ``glm47_flash``. -- Single H200, bf16, rollout batch 10, num_rollout 1. Single engine - (``--rollout-num-gpus-per-engine 1``) so both variants hit the same - underlying sglang process topology. +- :data:`CONFIGS` (``qwen3_30b_a3b``, ``glm47_flash``): default CI sweep. + ``ROUTER_EQ_MODEL_FAMILY`` overrides this to run a single family, primarily + for local debugging. +- The CI suite reserves 8 GPUs to avoid sharing the runner with another GPU + job while this SGLang/Ray process tree is alive. The test workload itself + still uses a single engine (``--rollout-num-gpus-per-engine 1``) so both + variants hit the same underlying sglang process topology. """ import base64 @@ -52,7 +57,6 @@ import miles.utils.external_utils.command_utils as U -MODEL_FAMILY = os.environ.get("ROUTER_EQ_MODEL_FAMILY", "qwen3_30b_a3b") DUMP_ROOT = Path(os.environ.get("ROUTER_EQ_DUMP_ROOT", "/tmp/router-eq")) PROMPT_DATA_PATH = "/root/datasets/dapo-math-17k/dapo-math-17k.jsonl" NUM_PROMPTS = int(os.environ.get("ROUTER_EQ_NUM_PROMPTS", "10")) @@ -93,15 +97,26 @@ class ModelConfig: ), } +# Default CI sweep. ``ROUTER_EQ_MODEL_FAMILY`` (single family) overrides this +# list, primarily for local debugging. +CONFIGS: list[str] = ["qwen3_30b_a3b", "glm47_flash"] -def _get_config() -> ModelConfig: - if MODEL_FAMILY not in MODEL_REGISTRY: - raise ValueError(f"Unknown ROUTER_EQ_MODEL_FAMILY={MODEL_FAMILY!r}; choose from {list(MODEL_REGISTRY)}") - return MODEL_REGISTRY[MODEL_FAMILY] +def _resolve_configs() -> list[str]: + override = os.environ.get("ROUTER_EQ_MODEL_FAMILY") + if override: + return [override] + return list(CONFIGS) -def prepare() -> None: - cfg = _get_config() + +def _get_config(model_family: str) -> ModelConfig: + if model_family not in MODEL_REGISTRY: + raise ValueError(f"Unknown ROUTER_EQ_MODEL_FAMILY={model_family!r}; choose from {list(MODEL_REGISTRY)}") + return MODEL_REGISTRY[model_family] + + +def prepare(model_family: str) -> None: + cfg = _get_config(model_family) U.exec_command("mkdir -p /root/models /root/datasets") if not Path(cfg.local_dir).exists(): U.exec_command(f"hf download {cfg.hf_repo} --local-dir {cfg.local_dir}") @@ -109,12 +124,12 @@ def prepare() -> None: U.hf_download_dataset("zhuzilin/dapo-math-17k") -def _variant_dir(variant: str) -> Path: - return DUMP_ROOT / MODEL_FAMILY / variant +def _variant_dir(model_family: str, variant: str) -> Path: + return DUMP_ROOT / model_family / variant -def _variant_dump_path(variant: str) -> Path: - return _variant_dir(variant) / "dump.jsonl" +def _variant_dump_path(model_family: str, variant: str) -> Path: + return _variant_dir(model_family, variant) / "dump.jsonl" def _build_train_args(cfg: ModelConfig, variant: str) -> str: @@ -171,12 +186,12 @@ def _build_train_args(cfg: ModelConfig, variant: str) -> str: return ckpt_args + rollout_args + generate_args + router_args + perf_args + sglang_args + infra_args -def _run_variant(cfg: ModelConfig, variant: str) -> None: - dump_dir = _variant_dir(variant) +def _run_variant(model_family: str, cfg: ModelConfig, variant: str) -> None: + dump_dir = _variant_dir(model_family, variant) if dump_dir.exists(): shutil.rmtree(dump_dir) dump_dir.mkdir(parents=True, exist_ok=True) - dump_path = _variant_dump_path(variant) + dump_path = _variant_dump_path(model_family, variant) train_args = _build_train_args(cfg, variant) U.execute_train( @@ -233,31 +248,37 @@ def _assert_records_equal(left: list[dict], right: list[dict]) -> None: assert ba == bb, f"index={a['index']} routed_experts bytes differ" -def execute() -> None: - cfg = _get_config() +def execute(model_family: str) -> None: + cfg = _get_config(model_family) for variant in ("miles", "sgl"): - _run_variant(cfg, variant) + _run_variant(model_family, cfg, variant) - miles_records = _load_dump(_variant_dump_path("miles")) - sgl_records = _load_dump(_variant_dump_path("sgl")) + miles_records = _load_dump(_variant_dump_path(model_family, "miles")) + sgl_records = _load_dump(_variant_dump_path(model_family, "sgl")) assert miles_records, "miles-router run produced no dump records" assert sgl_records, "sglang-router run produced no dump records" _assert_records_equal(miles_records, sgl_records) - print(f"[router-eq] model_family={MODEL_FAMILY} variants miles/sgl " f"match across {len(miles_records)} samples") + print(f"[router-eq] model_family={model_family} variants miles/sgl " f"match across {len(miles_records)} samples") -def test_r3_router_equivalence(): - prepare() +def _run_all() -> None: for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): os.environ.pop(proxy_var, None) - execute() + for model_family in _resolve_configs(): + print( + f"\n{'=' * 60}\nRunning model_family: {model_family}\n{'=' * 60}\n", + flush=True, + ) + prepare(model_family) + execute(model_family) + + +def test_r3_router_equivalence(): + _run_all() if __name__ == "__main__": - prepare() - for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): - os.environ.pop(proxy_var, None) - execute() + _run_all() diff --git a/tests/e2e/sglang/test_session_server_multi_role.py b/tests/e2e/sglang/test_session_server_multi_role.py index f7090d7cf19..75d030b415d 100644 --- a/tests/e2e/sglang/test_session_server_multi_role.py +++ b/tests/e2e/sglang/test_session_server_multi_role.py @@ -7,7 +7,9 @@ from tests.ci.ci_register import register_cuda_ci -register_cuda_ci(est_time=600, suite="stage-b-short-8-gpu", num_gpus=8) +# Four model families run sequentially in one job, so est_time is roughly 4x +# of a single family. +register_cuda_ci(est_time=2400, suite="stage-b-sglang-8-gpu", num_gpus=8) import os @@ -42,7 +44,7 @@ class ModelConfig: tool_call_parser="qwen25", tito_model="qwen3", allowed_append_roles=("tool", "user"), - tp_size=1, + tp_size=2, cycles=2, ), "qwen35-tool-user": ModelConfig( @@ -51,7 +53,7 @@ class ModelConfig: tool_call_parser="qwen3_coder", tito_model="qwen35", allowed_append_roles=("tool", "user"), - tp_size=1, + tp_size=2, cycles=2, ), "qwennext-tool-user": ModelConfig( @@ -60,25 +62,33 @@ class ModelConfig: tool_call_parser="qwen25", tito_model="qwennext", allowed_append_roles=("tool", "user"), - tp_size=2, + tp_size=4, cycles=2, ), } -DEFAULT_MODEL_FAMILY = "glm47-multi-role" +# Default CI sweep. ``SESSION_TEST_MODEL_FAMILY`` (single family) overrides +# this list, primarily for local debugging. +CONFIGS: list[str] = list(MODEL_REGISTRY) + + +def _resolve_configs() -> list[str]: + override = os.environ.get("SESSION_TEST_MODEL_FAMILY") + if override: + return [override] + return list(CONFIGS) -def _get_config() -> ModelConfig: - family = os.environ.get("SESSION_TEST_MODEL_FAMILY", DEFAULT_MODEL_FAMILY) - if family not in MODEL_REGISTRY: +def _get_config(model_family: str) -> ModelConfig: + if model_family not in MODEL_REGISTRY: raise ValueError( - f"Unknown SESSION_TEST_MODEL_FAMILY={family!r}. " f"Choose from: {list(MODEL_REGISTRY.keys())}" + f"Unknown SESSION_TEST_MODEL_FAMILY={model_family!r}. " f"Choose from: {list(MODEL_REGISTRY.keys())}" ) - return MODEL_REGISTRY[family] + return MODEL_REGISTRY[model_family] -def test_session_server_multi_role(): - cfg = _get_config() +def _run_one(model_family: str): + cfg = _get_config(model_family) run_session_verify( hf_checkpoint=cfg.model_name, tito_model=cfg.tito_model, @@ -90,5 +100,14 @@ def test_session_server_multi_role(): ) +def test_session_server_multi_role(): + for model_family in _resolve_configs(): + print( + f"\n{'=' * 60}\nRunning model_family: {model_family}\n{'=' * 60}\n", + flush=True, + ) + _run_one(model_family) + + if __name__ == "__main__": test_session_server_multi_role() diff --git a/tests/e2e/sglang/test_tito_logprob_equivalence.py b/tests/e2e/sglang/test_tito_logprob_equivalence.py index 1ef651193ef..734b8ea08a8 100644 --- a/tests/e2e/sglang/test_tito_logprob_equivalence.py +++ b/tests/e2e/sglang/test_tito_logprob_equivalence.py @@ -15,12 +15,21 @@ ``execute_train --debug-rollout-only`` with a custom generate function that wraps the normal agentic flow with re-prefill verification. -Requires 1 GPU. +In CI, the file runs every model in :data:`CONFIGS` back-to-back so both +qwen3 and glm47 paths are covered by a single job. The ``CONFIGS`` default +can be overridden by setting ``SESSION_TEST_MODEL_FAMILY`` to a single +family name, which is handy when debugging locally. + +The CI suite reserves 8 GPUs to avoid sharing the runner with another GPU job +while this SGLang/Ray process tree is alive. The test workload itself still +uses one GPU by default. """ from tests.ci.ci_register import register_cuda_ci -register_cuda_ci(est_time=600, suite="stage-b-sglang-1-gpu", num_gpus=1) +# Two model families run sequentially in one job, so est_time is roughly 2x +# of a single family. +register_cuda_ci(est_time=1200, suite="stage-b-sglang-8-gpu", num_gpus=8) import json @@ -32,7 +41,6 @@ # Model family registry # --------------------------------------------------------------------------- -MODEL_FAMILY = os.environ.get("SESSION_TEST_MODEL_FAMILY", "qwen3") ENABLE_R3 = os.environ.get("ENABLE_R3", "0") == "1" @@ -61,17 +69,28 @@ class ModelConfig: ), } +# Default CI sweep. ``SESSION_TEST_MODEL_FAMILY`` (single family) overrides +# this list, primarily for local debugging. +CONFIGS: list[str] = ["qwen3", "glm47"] + PROMPT_DATA_PATH = "/root/datasets/session_logprob_verify.jsonl" -def _get_config() -> ModelConfig: - if MODEL_FAMILY not in MODEL_REGISTRY: - raise ValueError(f"Unknown model family {MODEL_FAMILY!r}. " f"Choose from: {list(MODEL_REGISTRY.keys())}") - return MODEL_REGISTRY[MODEL_FAMILY] +def _resolve_configs() -> list[str]: + override = os.environ.get("SESSION_TEST_MODEL_FAMILY") + if override: + return [override] + return list(CONFIGS) + + +def _get_config(model_family: str) -> ModelConfig: + if model_family not in MODEL_REGISTRY: + raise ValueError(f"Unknown model family {model_family!r}. " f"Choose from: {list(MODEL_REGISTRY.keys())}") + return MODEL_REGISTRY[model_family] -def prepare(): - cfg = _get_config() +def prepare(model_family: str): + cfg = _get_config(model_family) U.exec_command("mkdir -p /root/models /root/datasets") U.exec_command(f"hf download {cfg.model_name} --local-dir /root/models/{cfg.model_name.split('/')[-1]}") @@ -99,8 +118,8 @@ def prepare(): f.write(json.dumps(p) + "\n") -def execute(): - cfg = _get_config() +def execute(model_family: str): + cfg = _get_config(model_family) local_model_dir = f"/root/models/{cfg.model_name.split('/')[-1]}" ckpt_args = f"--hf-checkpoint {local_model_dir} " @@ -157,15 +176,21 @@ def execute(): ) -def test_tito_logprob_equivalence(): - prepare() +def _run_all(): for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): os.environ.pop(proxy_var, None) - execute() + for model_family in _resolve_configs(): + print( + f"\n{'=' * 60}\nRunning model_family: {model_family}\n{'=' * 60}\n", + flush=True, + ) + prepare(model_family) + execute(model_family) + + +def test_tito_logprob_equivalence(): + _run_all() if __name__ == "__main__": - prepare() - for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): - os.environ.pop(proxy_var, None) - execute() + _run_all() From d94ee8cb5538ea67cccd07da8ef4a89fd36fdd42 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Tue, 12 May 2026 18:56:16 -0700 Subject: [PATCH 0050/1130] [tiny-refac] TITO: drop prompt token ids response dup (#1096) --- .../generate_utils/openai_endpoint_utils.py | 15 ++++----------- miles/rollout/session/sessions.py | 3 --- tests/e2e/sglang/utils/logprob_verify_generate.py | 6 +++--- .../generate_utils/test_openai_endpoint_utils.py | 3 +-- .../fast/router/test_session_pretokenized_e2e.py | 1 - tests/fast/router/test_sessions.py | 2 -- 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/miles/rollout/generate_utils/openai_endpoint_utils.py b/miles/rollout/generate_utils/openai_endpoint_utils.py index 7db40ae585c..820dad477c4 100644 --- a/miles/rollout/generate_utils/openai_endpoint_utils.py +++ b/miles/rollout/generate_utils/openai_endpoint_utils.py @@ -115,7 +115,7 @@ def compute_samples_from_openai_records( for i, record in enumerate(records): is_last = i == len(records) - 1 - prompt_ids = record.response["choices"][0]["prompt_token_ids"] + prompt_ids = record.request["input_ids"] output_ids = [t[1] for t in record.response["choices"][0]["meta_info"]["output_token_logprobs"]] trim_count = 0 @@ -165,21 +165,14 @@ def _compute_sample_from_openai_record( ) -> Sample: choice = record.response["choices"][0] - if "prompt_token_ids" in choice: - prompt_token_ids = choice["prompt_token_ids"] - else: - raise ValueError("prompt_token_ids not found in response choice — the session server should populate it") + prompt_token_ids = record.request.get("input_ids") + if prompt_token_ids is None: + raise ValueError("input_ids not found in request — the session server should populate it") output_token_ids = [item[1] for item in choice["meta_info"]["output_token_logprobs"]] output_log_probs = [item[0] for item in choice["meta_info"]["output_token_logprobs"]] sample = deepcopy(input_sample) - request_input_ids = record.request.get("input_ids") - if request_input_ids is not None: - assert ( - request_input_ids == prompt_token_ids - ), "for prompt part, input_ids return by sglang should match with the request input_ids" - sample.tokens = prompt_token_ids + output_token_ids sample.rollout_log_probs = output_log_probs sample.response = tokenizer.decode(output_token_ids) diff --git a/miles/rollout/session/sessions.py b/miles/rollout/session/sessions.py index d9016073746..e7ceb5e3581 100644 --- a/miles/rollout/session/sessions.py +++ b/miles/rollout/session/sessions.py @@ -192,9 +192,6 @@ async def chat_completions(request: Request, session_id: str): "an empty content rather than None. Please check your modified SGLang version." ) - choice["prompt_token_ids"] = prompt_token_ids - # This must be re-encoded to return the prompt token ids - result["response_body"] = json.dumps(response).encode() output_token_logprobs = meta_info["output_token_logprobs"] completion_tokens = meta_info["completion_tokens"] diff --git a/tests/e2e/sglang/utils/logprob_verify_generate.py b/tests/e2e/sglang/utils/logprob_verify_generate.py index 2827818a579..b92aade7bbc 100644 --- a/tests/e2e/sglang/utils/logprob_verify_generate.py +++ b/tests/e2e/sglang/utils/logprob_verify_generate.py @@ -241,7 +241,7 @@ async def _verify_logprobs_via_reprefill( ``input_token_logprobs`` (single prefill pass) against per-turn ``output_token_logprobs`` (incremental decode) from session records. """ - first_prompt_len = len(records[0].response["choices"][0]["prompt_token_ids"]) + first_prompt_len = len(records[0].request["input_ids"]) # ── A: send re-prefill request ── payload = { @@ -272,7 +272,7 @@ async def _verify_logprobs_via_reprefill( for i, record in enumerate(records): is_last = i == len(records) - 1 choice = record.response["choices"][0] - prompt_ids = choice["prompt_token_ids"] + prompt_ids = record.request["input_ids"] session_output_logprobs = choice["meta_info"]["output_token_logprobs"] output_ids = [t[1] for t in session_output_logprobs] @@ -370,7 +370,7 @@ def _verify_routed_experts( for i, record in enumerate(records): choice = record.response["choices"][0] - prompt_ids = choice["prompt_token_ids"] + prompt_ids = record.request["input_ids"] output_logprobs = choice["meta_info"]["output_token_logprobs"] output_ids = [t[1] for t in output_logprobs] diff --git a/tests/fast/rollout/generate_utils/test_openai_endpoint_utils.py b/tests/fast/rollout/generate_utils/test_openai_endpoint_utils.py index 2ed89d2a602..4ac8d7553ee 100644 --- a/tests/fast/rollout/generate_utils/test_openai_endpoint_utils.py +++ b/tests/fast/rollout/generate_utils/test_openai_endpoint_utils.py @@ -82,11 +82,10 @@ def _make_record( method="POST", path="/v1/chat/completions", status_code=200, - request={"messages": [{"role": "user", "content": "hello"}]}, + request={"messages": [{"role": "user", "content": "hello"}], "input_ids": prompt_token_ids}, response={ "choices": [ { - "prompt_token_ids": prompt_token_ids, "message": {"role": "assistant", "content": "response"}, "finish_reason": finish_reason, "logprobs": {"content": logprobs_content}, diff --git a/tests/fast/router/test_session_pretokenized_e2e.py b/tests/fast/router/test_session_pretokenized_e2e.py index 91bd3b7e94f..e419f58a848 100644 --- a/tests/fast/router/test_session_pretokenized_e2e.py +++ b/tests/fast/router/test_session_pretokenized_e2e.py @@ -141,7 +141,6 @@ def test_bundled_fixed_template_session_smoke(config: FixedTemplateSmokeConfig): body = response.json() assert len(body["choices"]) == 1 - assert "prompt_token_ids" in body["choices"][0] if turn_idx > 0: assert "input_ids" in backend.request_log[turn_idx], f"{config.name} turn {turn_idx} missing input_ids" diff --git a/tests/fast/router/test_sessions.py b/tests/fast/router/test_sessions.py index 7fb33d775b4..2309c6bc439 100644 --- a/tests/fast/router/test_sessions.py +++ b/tests/fast/router/test_sessions.py @@ -132,8 +132,6 @@ def test_proxy_chat_appends_record(self, router_env): body = resp.json() assert "choices" in body assert body["choices"] - assert isinstance(body["choices"][0]["prompt_token_ids"], list) - assert body["choices"][0]["prompt_token_ids"] get_resp = requests.get(f"{router_env.url}/sessions/{session_id}", timeout=5.0) records = get_resp.json()["records"] From a8b51c5e02c73f976723c5490c24d91fb10abb1a Mon Sep 17 00:00:00 2001 From: Shi-Dong Date: Fri, 15 May 2026 05:42:20 +0800 Subject: [PATCH 0051/1130] Point README Documentation link at the Mintlify docs site (#1128) Co-authored-by: Shi Dong --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30ce5a01126..c0f7e448471 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![License](https://img.shields.io/github/license/radixark/miles)](LICENSE) [![Slack](https://img.shields.io/badge/slack-join-brightgreen.svg)](https://slack.sglang.ai) -[**Latest Updates**](#latest-updates) | [**Quick Start**](#quick-start) | [**Key Features**](#key-features) | [**Documentation**](docs/en/get_started/quick_start.md) +[**Latest Updates**](#latest-updates) | [**Quick Start**](#quick-start) | [**Key Features**](#key-features) | [**Documentation**](https://www.radixark.com/miles/docs) From a0b75e7f19eab7c270993b61a7f7fddab692ef31 Mon Sep 17 00:00:00 2001 From: Kaixi Hou Date: Fri, 15 May 2026 18:58:45 -0700 Subject: [PATCH 0052/1130] [NVIDIA] B300 (x86_64, sm_103a) Container Support (#977) Co-authored-by: Jiajun Li --- docker/Dockerfile | 44 +++++++++++++++---- .../patch/cu13/te_fa2_sm103_whitelist.patch | 11 +++++ 2 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 docker/patch/cu13/te_fa2_sm103_whitelist.patch diff --git a/docker/Dockerfile b/docker/Dockerfile index 61be29d4036..433ba318b60 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,15 @@ -# 1. radixark/miles:dev +# 1. radixark/miles:dev (x86 CUDA 12) # use default settings # -# 2. radixark/miles:dev-cu13-arm64 +# 2. radixark/miles:dev-cu13-arm64 (arm64 CUDA 13 / GB300) # build-arg:ENABLE_CUDA_13=1 \ # build-arg:SGLANG_IMAGE_TAG=v0.5.10-cu130 \ # build-arg:WHEELS_TAG=cu130-aarch64 \ +# +# 3. radixark/miles:dev-cu13-x86 (x86 CUDA 13 / B300, sm_103a) +# build-arg:ENABLE_CUDA_13=1 \ +# build-arg:SGLANG_IMAGE_TAG=v0.5.10-cu130 \ +# build-arg:WHEELS_TAG=cu130-x86_64 \ ARG SGLANG_IMAGE_TAG=v0.5.10 FROM lmsysorg/sglang:${SGLANG_IMAGE_TAG} AS sglang @@ -40,13 +45,17 @@ RUN git clone https://github.com/NVIDIA/nccl-tests.git /tmp/nccl-tests && \ cp /tmp/nccl-tests/build/*_perf /usr/local/bin/ && \ rm -rf /tmp/nccl-tests -# ====================================== Download pre-built wheels ============================================ +# ====================================== Collect pre-built wheels ============================================ +# Optional: drop pre-built wheels into /wheels/ to skip the release download below. +# `wheel[s]/` is a glob — silently no-ops when wheels/ is absent (CI's path). +COPY wheel[s]/ /tmp/wheels/ RUN mkdir -p /tmp/wheels && \ curl -sL "https://api.github.com/repos/${WHEELS_REPO}/releases/tags/${WHEELS_TAG}" \ - | python3 -c "import sys, json, subprocess; \ -[subprocess.run(['curl', '-fSL', '-o', '/tmp/wheels/' + a['name'], a['browser_download_url']], check=True) \ - for a in json.load(sys.stdin)['assets'] if a['name'].endswith(('.whl', '.tar.gz'))]" && \ + | python3 -c "import sys,json,subprocess,os; w='/tmp/wheels'; \ +[subprocess.run(['curl','-fSL','-o',os.path.join(w,a['name']),a['browser_download_url']],check=True) \ + for a in json.load(sys.stdin).get('assets',[]) \ + if a['name'].endswith(('.whl', '.tar.gz')) and not os.path.exists(os.path.join(w,a['name']))]" && \ ls -lh /tmp/wheels/ # ====================================== Python dependencies ============================================ @@ -54,7 +63,7 @@ RUN mkdir -p /tmp/wheels && \ # flash-attn RUN pip install /tmp/wheels/flash_attn-*.whl -# flash-attn hopper +# flash-attn hopper (FA3): Hopper-only (sm_90a). Coexists with FA2. RUN pip install /tmp/wheels/flash_attn_3-*.whl && \ python_path=$(python -c "import site; print(site.getsitepackages()[0])") && \ mkdir -p $python_path/flash_attn_3 && \ @@ -69,13 +78,30 @@ RUN pip install tilelang -f https://tile-ai.github.io/whl/nightly/cu128/ # Mamba kernels for nemotron_h hybrid (mamba+attention) models. RUN pip install causal-conv1d==1.6.1 mamba-ssm==2.3.1 --no-build-isolation +# transformer_engine RUN if [ "${ENABLE_CUDA_13}" = "1" ]; then \ - pip install nvidia-mathdx==25.6.0 && \ - pip -v install --no-build-isolation "transformer_engine[core_cu13,pytorch]==2.10.0"; \ + pip install --no-deps transformer_engine==2.12.0 && \ + pip install transformer_engine_cu13==2.12.0 && \ + if ls /tmp/wheels/transformer_engine_torch-*.whl 2>/dev/null | grep -q .; then \ + pip install /tmp/wheels/transformer_engine_torch-*.whl; \ + else \ + pip install nvidia-mathdx==25.6.0 && \ + pip -v install --no-build-isolation transformer_engine_torch==2.12.0; \ + fi; \ else \ pip -v install --no-build-isolation "transformer_engine[pytorch]==2.10.0"; \ fi +# TE patches (cu13): B300/GB300 sm103 FA2 whitelist fix +COPY docker/patch/ /tmp/patches/ +RUN if [ "${ENABLE_CUDA_13}" = "1" ] && [ -d /tmp/patches/cu13 ]; then \ + TE_DIR=$(python -c 'import transformer_engine; print(transformer_engine.__path__[0])') && \ + for p in /tmp/patches/cu13/*.patch; do \ + echo "Applying $(basename $p) to $TE_DIR" && \ + patch -d "$TE_DIR" -p1 < "$p"; \ + done; \ + fi && rm -rf /tmp/patches + # apex RUN pip install /tmp/wheels/apex-*.whl diff --git a/docker/patch/cu13/te_fa2_sm103_whitelist.patch b/docker/patch/cu13/te_fa2_sm103_whitelist.patch new file mode 100644 index 00000000000..203fe27e65a --- /dev/null +++ b/docker/patch/cu13/te_fa2_sm103_whitelist.patch @@ -0,0 +1,11 @@ +--- a/pytorch/attention/dot_product_attention/utils.py ++++ b/pytorch/attention/dot_product_attention/utils.py +@@ -629,7 +629,7 @@ + or head_dim_qk % 8 != 0 + or ( + head_dim_qk > 192 +- and device_compute_capability not in ((8, 0), (9, 0), (10, 0), (12, 0)) ++ and device_compute_capability not in ((8, 0), (9, 0), (10, 0), (10, 3), (12, 0)) + ) + ): + if FlashAttentionUtils.is_installed: From d1cd80116409a52103e4a33ef56bfb1c4ec92d86 Mon Sep 17 00:00:00 2001 From: Kaixi Hou Date: Fri, 15 May 2026 19:51:09 -0700 Subject: [PATCH 0053/1130] Fix aarch64 editable wheel platform tag (#1114) --- setup.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index a16d0a18642..de7281b7425 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,18 @@ from wheel.bdist_wheel import bdist_wheel as _bdist_wheel +def _get_platform_tag(): + if platform.system() != "Linux": + return platform.system().lower() + + machine = platform.machine().lower() + if machine in ("x86_64", "amd64"): + return "manylinux1_x86_64" + if machine in ("aarch64", "arm64"): + return "manylinux2014_aarch64" + return f"linux_{machine}" + + def _fetch_requirements(path): with open(path) as fd: return [r.strip() for r in fd.readlines() if r.strip() and not r.startswith("#")] @@ -19,11 +31,7 @@ def finalize_options(self): def get_tag(self): python_version = f"cp{sys.version_info.major}{sys.version_info.minor}" abi_tag = f"{python_version}" - - if platform.system() == "Linux": - platform_tag = "manylinux1_x86_64" - else: - platform_tag = platform.system().lower() + platform_tag = _get_platform_tag() return python_version, abi_tag, platform_tag From 551d15914c89b1229b76fe806ca5f5aa5a826309 Mon Sep 17 00:00:00 2001 From: Douglas Yang Date: Sun, 17 May 2026 09:24:35 -0700 Subject: [PATCH 0054/1130] fix: restoring CI tests (#1137) Co-authored-by: Jiajun Li Co-authored-by: Jiajun Li Co-authored-by: Claude Opus 4.7 (1M context) --- miles/backends/megatron_utils/checkpoint.py | 2 +- .../hf_weight_iterator_bridge.py | 37 +++-- miles/backends/training_utils/log_utils.py | 5 +- tests/e2e/megatron/test_glm47_flash_r3_mtp.py | 1 - tests/e2e/megatron/test_moonlight_16B_A3B.py | 131 ------------------ .../e2e/megatron/test_moonlight_16B_A3B_r3.py | 131 ------------------ tests/e2e/megatron/test_qwen3_30B_A3B.py | 17 +-- tests/e2e/megatron/test_qwen3_30B_A3B_r3.py | 3 +- .../generate_hub/test_tool_call_utils.py | 3 +- 9 files changed, 36 insertions(+), 294 deletions(-) delete mode 100644 tests/e2e/megatron/test_moonlight_16B_A3B.py delete mode 100644 tests/e2e/megatron/test_moonlight_16B_A3B_r3.py diff --git a/miles/backends/megatron_utils/checkpoint.py b/miles/backends/megatron_utils/checkpoint.py index d7f39dc8bed..7d6ba7681ec 100644 --- a/miles/backends/megatron_utils/checkpoint.py +++ b/miles/backends/megatron_utils/checkpoint.py @@ -178,7 +178,7 @@ def _load_checkpoint_hf(ddp_model, optimizer, args, load_path: str): logger.info(f"Load checkpoint from HuggingFace model into Megatron (path={load_path})") with megatron_bridge_utils.patch_megatron_model(ddp_model): - bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True) + bridge = AutoBridge.from_hf_pretrained(load_path, 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/update_weight/hf_weight_iterator_bridge.py b/miles/backends/megatron_utils/update_weight/hf_weight_iterator_bridge.py index 33f7523822d..9890e23b4df 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 @@ -5,6 +5,7 @@ from miles.utils.iter_utils import chunk_named_params_by_size from ..megatron_to_hf import postprocess_hf_param +from ..megatron_to_hf.processors import quantize_params from ..misc_utils import strip_param_name_prefix from .hf_weight_iterator_base import HfWeightIteratorBase @@ -18,7 +19,6 @@ def __init__(self, *args, **kwargs): self._bridge = AutoBridge.from_hf_pretrained(self.args.hf_checkpoint, trust_remote_code=True) def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base"): - # TODO: support quantization (e.g. modify megatron-bridge to provide megatron param name) renamed_megatron_local_weights = {strip_param_name_prefix(k): v for k, v in megatron_local_weights.items()} with megatron_bridge_utils.patch_megatron_model(self.model): if weight_type == "lora": @@ -37,19 +37,11 @@ def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base" merge_adapter_weights=False, ) - # TODO: verify if postprocess_hf_param is needed for LoRA weights - named_weights = ( - ( - hf_param_name.replace(".base_layer.", "."), - postprocess_hf_param( - args=self.args, - megatron_param_name=megatron_param_name, - hf_param_name=hf_param_name.replace(".base_layer.", "."), - param=weight, - ), - ) - for hf_param_name, weight, megatron_param_name in named_weights - ) + # Apply postprocess + quantization (when targeting a quantized rollout, + # e.g. FP8 sglang). Base weights are quantized to match the rollout's + # storage format so update_weights_from_tensor lands real weight + scale + # pairs; LoRA adapters are passed through unchanged. + named_weights = self._postprocess_and_quantize(named_weights, weight_type) if weight_type == "base": named_weights = ((n, t) for n, t in named_weights if not is_lora_weight_name(n)) @@ -58,6 +50,23 @@ def get_hf_weight_chunks(self, megatron_local_weights, weight_type: str = "base" yield from chunk_named_params_by_size(named_weights, chunk_size=self.args.update_weight_buffer_size) + def _postprocess_and_quantize(self, named_weights, weight_type: str): + for hf_param_name, weight, megatron_param_name in named_weights: + hf_name = hf_param_name.replace(".base_layer.", ".") + weight = postprocess_hf_param( + args=self.args, + megatron_param_name=megatron_param_name, + hf_param_name=hf_name, + param=weight, + ) + if weight_type == "base" and self.quantization_config is not None: + # quantize_params expects the megatron name with the `module.module.` + # prefix that the direct iterator uses; the bridge yields it without. + qmegatron_name = f"module.module.{megatron_param_name}" + yield from quantize_params(self.args, qmegatron_name, [(hf_name, weight)], self.quantization_config) + else: + yield hf_name, weight + def _process_conversion_tasks(vanilla_conversion_tasks, new_weight_dict): def _handle_one(task): diff --git a/miles/backends/training_utils/log_utils.py b/miles/backends/training_utils/log_utils.py index 54aa44a6d8f..07754b5a63d 100644 --- a/miles/backends/training_utils/log_utils.py +++ b/miles/backends/training_utils/log_utils.py @@ -178,12 +178,15 @@ def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatc # so log_probs and ref_log_probs may diverge; use a relaxed tolerance. # When --sglang-config deploys multiple models, the heavier offload/onload # cycle can amplify flash-attention non-determinism; use 1e-8. + # The default branch also covers larger TP/CP/EP variants (e.g. stage-c-long + # test_qwen2.5_0.5B_gsm8k.py on 8 GPUs hit ~3.7e-9 diff in CI), so use 1e-8 + # rather than the previous 3e-9 to absorb BF16 reduction noise across configs. if args.use_rollout_routing_replay: abs_tol = 1e-5 elif getattr(args, "sglang_config", None) is not None: abs_tol = 1e-8 else: - abs_tol = 1e-9 + abs_tol = 1e-8 assert isclose( reduced_log_dict["rollout/log_probs"], reduced_log_dict["rollout/ref_log_probs"], abs_tol=abs_tol ), f"CI check failed: log_probs ({reduced_log_dict['rollout/log_probs']}) != ref_log_probs ({reduced_log_dict['rollout/ref_log_probs']})" diff --git a/tests/e2e/megatron/test_glm47_flash_r3_mtp.py b/tests/e2e/megatron/test_glm47_flash_r3_mtp.py index bdc8863c45b..c5d450fcfc6 100644 --- a/tests/e2e/megatron/test_glm47_flash_r3_mtp.py +++ b/tests/e2e/megatron/test_glm47_flash_r3_mtp.py @@ -8,7 +8,6 @@ est_time=900, suite="stage-c-megatron-8-gpu", num_gpus=8, - disabled="CalledProcessError exit code 1 on CI (pre-existing, not caused by CI refactor)", ) ENABLE_EVAL = False diff --git a/tests/e2e/megatron/test_moonlight_16B_A3B.py b/tests/e2e/megatron/test_moonlight_16B_A3B.py deleted file mode 100644 index 890287f4185..00000000000 --- a/tests/e2e/megatron/test_moonlight_16B_A3B.py +++ /dev/null @@ -1,131 +0,0 @@ -import os - -from tests.ci.ci_register import register_cuda_ci - -import miles.utils.external_utils.command_utils as U - -register_cuda_ci(est_time=900, suite="stage-c-megatron-8-gpu", num_gpus=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"))) - -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 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 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 " - f"--sglang-mem-fraction-static {0.7 if TIGHT_HOST_MEMORY else 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, - extra_env_vars={"MILES_EXPERIMENTAL_ROLLOUT_REFACTOR": "1"}, - ) - - -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/e2e/megatron/test_moonlight_16B_A3B_r3.py b/tests/e2e/megatron/test_moonlight_16B_A3B_r3.py deleted file mode 100644 index 64af6d0ea98..00000000000 --- a/tests/e2e/megatron/test_moonlight_16B_A3B_r3.py +++ /dev/null @@ -1,131 +0,0 @@ -import os - -from tests.ci.ci_register import register_cuda_ci - -import miles.utils.external_utils.command_utils as U - -register_cuda_ci(est_time=900, suite="stage-c-megatron-8-gpu", num_gpus=8) - -ENABLE_EVAL = False -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 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 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 " - ) - - 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 " - f"--sglang-mem-fraction-static {0.7 if TIGHT_HOST_MEMORY else 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/e2e/megatron/test_qwen3_30B_A3B.py b/tests/e2e/megatron/test_qwen3_30B_A3B.py index ffd9712260a..5e1ab275913 100644 --- a/tests/e2e/megatron/test_qwen3_30B_A3B.py +++ b/tests/e2e/megatron/test_qwen3_30B_A3B.py @@ -4,9 +4,6 @@ import miles.utils.external_utils.command_utils as U -# Sweeps enabled rollout / dispatcher variants in one job so the previously -# matrix-parameterized CI is back. est_time stays conservative while the -# bridge + DeepEP variant below is disabled pending a fix. register_cuda_ci(est_time=3600, suite="stage-c-megatron-8-gpu", num_gpus=8) ENABLE_EVAL = bool(int(os.environ.get("MILES_TEST_ENABLE_EVAL", "1"))) @@ -16,7 +13,6 @@ MODEL_TYPE = "qwen3-30B-A3B" NUM_GPUS = 8 -# Each entry is one matrix variant from the legacy parameterized job. CONFIGS: list[dict] = [ { "USE_DEEPEP": True, @@ -24,13 +20,6 @@ "USE_INT4_ROLLOUT": False, "USE_BRIDGE": False, }, - # TODO: This deepep test need fix. - # { - # "USE_DEEPEP": True, - # "USE_FP8_ROLLOUT": True, - # "USE_INT4_ROLLOUT": False, - # "USE_BRIDGE": True, - # }, { "USE_DEEPEP": False, "USE_FP8_ROLLOUT": False, @@ -43,6 +32,12 @@ "USE_INT4_ROLLOUT": True, "USE_BRIDGE": False, }, + { + "USE_DEEPEP": True, + "USE_FP8_ROLLOUT": True, + "USE_INT4_ROLLOUT": False, + "USE_BRIDGE": True, + }, ] diff --git a/tests/e2e/megatron/test_qwen3_30B_A3B_r3.py b/tests/e2e/megatron/test_qwen3_30B_A3B_r3.py index 104ba4d6694..06e42bac284 100644 --- a/tests/e2e/megatron/test_qwen3_30B_A3B_r3.py +++ b/tests/e2e/megatron/test_qwen3_30B_A3B_r3.py @@ -5,10 +5,9 @@ import miles.utils.external_utils.command_utils as U register_cuda_ci( - est_time=1800, + est_time=3600, suite="stage-c-megatron-8-gpu", num_gpus=8, - disabled="Timeout after 1800s on CI (pre-existing, not caused by CI refactor)", ) TIGHT_HOST_MEMORY = bool(int(os.environ.get("MILES_TEST_TIGHT_HOST_MEMORY", "1"))) diff --git a/tests/fast/rollout/generate_hub/test_tool_call_utils.py b/tests/fast/rollout/generate_hub/test_tool_call_utils.py index bc32e1fb3be..8c3ce6d12d9 100644 --- a/tests/fast/rollout/generate_hub/test_tool_call_utils.py +++ b/tests/fast/rollout/generate_hub/test_tool_call_utils.py @@ -3,7 +3,6 @@ register_cpu_ci( est_time=60, suite="stage-a-fast", - disabled="stepfun-ai/Step-3.5-Flash config num_hidden_layers/layer_types mismatch breaks huggingface_hub strict validation", ) import pytest @@ -24,7 +23,7 @@ "MiniMaxAI/MiniMax-M2.5", "internlm/internlm3-8b-instruct", "zai-org/GLM-4.7-Flash", - "stepfun-ai/Step-3.5-Flash", + # "stepfun-ai/Step-3.5-Flash", # Disabled: config num_hidden_layers/layer_types mismatch breaks huggingface_hub strict validation "moonshotai/Kimi-K2-Instruct", "moonshotai/Kimi-K2.5", "XiaomiMiMo/MiMo-7B-RL", From cbe19400118e972301e05b233dfdff1646191732 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Sun, 17 May 2026 21:49:00 -0700 Subject: [PATCH 0055/1130] [TITO] model support for Kimi-k2/2.5/2.6, nemotron-3-super, mimimax-m2.5/2.7 (#1117) --- .../templates/kimi_k25_fixed.jinja | 111 +++++++ .../templates/minimax_m25_fixed.jinja | 159 +++++++++ .../templates/minimax_m27_fixed.jinja | 159 +++++++++ .../chat_template_utils/tito_tokenizer.py | 304 +++++++++++++++++- .../token_seq_comparator.py | 14 +- miles/utils/processing_utils.py | 43 +++ .../utils/test_utils/session_verify_agent.py | 155 +++++++-- .../utils/test_utils/session_verify_runner.py | 50 ++- .../tools/verify_session_tito_tokenizer.py | 31 +- .../e2e/sglang/test_r3_router_equivalence.py | 18 +- .../sglang/test_session_server_multi_role.py | 89 ++++- .../generate_hub/test_tool_call_utils.py | 2 +- .../test_pretokenized_chat.py | 78 +++++ .../test_tito_tokenizer.py | 9 +- .../test_utils/test_session_verify_runner.py | 59 ++++ 15 files changed, 1205 insertions(+), 76 deletions(-) create mode 100644 miles/utils/chat_template_utils/templates/kimi_k25_fixed.jinja create mode 100644 miles/utils/chat_template_utils/templates/minimax_m25_fixed.jinja create mode 100644 miles/utils/chat_template_utils/templates/minimax_m27_fixed.jinja create mode 100644 tests/fast/utils/test_utils/test_session_verify_runner.py diff --git a/miles/utils/chat_template_utils/templates/kimi_k25_fixed.jinja b/miles/utils/chat_template_utils/templates/kimi_k25_fixed.jinja new file mode 100644 index 00000000000..0f05753bfed --- /dev/null +++ b/miles/utils/chat_template_utils/templates/kimi_k25_fixed.jinja @@ -0,0 +1,111 @@ +{%- macro render_content(msg) -%} + {%- set c = msg.get('content') -%} + {%- if c is string -%} + {{ c }} + {%- elif c is not none -%} + {% for content in c -%} + {% if content['type'] == 'image' or content['type'] == 'image_url' -%} + <|media_begin|>image<|media_content|><|media_pad|><|media_end|> + {% elif content['type'] == 'video' or content['type']== 'video_url'-%} + <|kimi_k25_video_placeholder|> + {% else -%} + {{ content['text'] }} + {%- endif -%} + {%- endfor -%} + {%- endif -%} +{%- endmacro -%} + +{% macro set_roles(message) -%} + {%- set role_name = message.get('name') or message['role'] -%} + {%- if message['role'] == 'user' -%} + <|im_user|>{{role_name}}<|im_middle|> + {%- elif message['role'] == 'assistant' -%} + <|im_assistant|>{{role_name}}<|im_middle|> + {%- else -%} + <|im_system|>{{role_name}}<|im_middle|> + {%- endif -%} +{%- endmacro -%} + + +{%- macro render_toolcalls(message) -%} + <|tool_calls_section_begin|> + {%- for tool_call in message['tool_calls'] -%} + {%- set formatted_id = tool_call['id'] -%} + <|tool_call_begin|>{{ formatted_id }}<|tool_call_argument_begin|>{% if tool_call['function']['arguments'] is string %}{{ tool_call['function']['arguments'] }}{% else %}{{ tool_call['function']['arguments'] | tojson }}{% endif %}<|tool_call_end|> + {%- endfor -%} + <|tool_calls_section_end|> +{%- endmacro -%} + + +{%- set preserve_thinking = preserve_thinking | default(false) -%} +{# Find last non-tool-call assistant message. If preserve_thinking, keep -1 so hist is empty and all msgs use suffix (retain reasoning). #} +{%- set ns = namespace(last_non_tool_call_assistant_msg=-1) -%} +{%- if not preserve_thinking -%} +{%- for idx in range(messages|length-1, -1, -1) -%} + {%- if messages[idx]['role'] == 'assistant' and not messages[idx].get('tool_calls') -%} + {%- set ns.last_non_tool_call_assistant_msg = idx -%} + {%- break -%} + {%- endif -%} +{%- endfor -%} +{%- endif -%} + +{# split all messages into history & suffix, reasoning_content in suffix should be reserved.#} +{%- set hist_msgs = messages[:ns.last_non_tool_call_assistant_msg+1] -%} +{%- set suffix_msgs = messages[ns.last_non_tool_call_assistant_msg+1:] -%} + +{%- if tools -%} + {%- if tools_ts_str -%} + <|im_system|>tool_declare<|im_middle|>{{ tools_ts_str }}<|im_end|> + {%- else -%} + <|im_system|>tool_declare<|im_middle|>{{ tools | tojson(separators=(',', ':')) }}<|im_end|> + {%- endif -%} +{%- endif -%} + +{%- for message in hist_msgs -%} + {{set_roles(message)}} + {%- if message['role'] == 'assistant' -%} + {{render_content(message)}} + {%- if message.get('tool_calls') -%} + {{render_toolcalls(message)}} + {%- endif -%} + {%- elif message['role'] == 'tool' -%} + {%- set tool_call_id = message.tool_call_id -%} + ## Return of {{ tool_call_id }} +{{render_content(message)}} + {%- elif message['content'] is not none -%} + {{render_content(message)}} + {%- endif -%} + <|im_end|> +{%- endfor -%} + +{%- for message in suffix_msgs -%} + {{set_roles(message)}} + {%- if message['role'] == 'assistant' -%} + {%- if thinking is defined and thinking is false -%} + {{render_content(message)}} + {%- else -%} + {%- set rc = message.get('reasoning_content', '') -%} + {{rc}}{{render_content(message)}} + {%- endif -%} + {%- if message.get('tool_calls') -%} + {{render_toolcalls(message)}} + {%- endif -%} + {%- elif message['role'] == 'tool' -%} + {%- set tool_call_id = message.tool_call_id -%} + ## Return of {{ tool_call_id }} +{{render_content(message)}} + {%- elif message['content'] is not none -%} + {{render_content(message)}} + {%- endif -%} + <|im_end|> +{%- endfor -%} + + +{%- if add_generation_prompt -%} + <|im_assistant|>assistant<|im_middle|> + {%- if thinking is defined and thinking is false -%} + + {%- else -%} + + {%- endif -%} +{%- endif -%} diff --git a/miles/utils/chat_template_utils/templates/minimax_m25_fixed.jinja b/miles/utils/chat_template_utils/templates/minimax_m25_fixed.jinja new file mode 100644 index 00000000000..c121f2d2e62 --- /dev/null +++ b/miles/utils/chat_template_utils/templates/minimax_m25_fixed.jinja @@ -0,0 +1,159 @@ +{# ----------‑‑‑ special token variables ‑‑‑---------- #} +{%- set toolcall_begin_token = '' -%} +{%- set toolcall_end_token = '' -%} +{#- Tool Rendering Functions ============================================== -#} +{%- macro render_tool_namespace(namespace_name, tool_list) -%} +{%- for tool in tool_list -%} +{{ tool.function | tojson(ensure_ascii=False) }} +{% endfor -%} +{%- endmacro -%} +{%- macro visible_text(content) -%} + {%- if content is string -%} + {{ content }} + {%- elif content is iterable and content is not mapping -%} + {%- for item in content -%} + {%- if item is mapping and item.type == 'text' -%} + {{- item.text }} + {%- elif item is string -%} + {{- item }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {{- content }} + {%- endif -%} +{%- endmacro -%} +{#- System Message Construction ============================================ -#} +{%- macro build_system_message(system_message) -%} + {%- if system_message and system_message.content -%} + {{- visible_text(system_message.content) }} + {%- else -%} + {%- if model_identity is not defined -%} + {%- set model_identity = "You are a helpful assistant. Your name is MiniMax-M2.5 and is built by MiniMax." -%} + {%- endif -%} + {{- model_identity }} + {%- endif -%} + + {#- Handle current_date -#} + {%- if system_message and system_message.current_date -%} + {{- '\n' ~ 'Current date: ' + system_message.current_date }} + {%- endif -%} + {#- Handle current_location -#} + {%- if system_message and system_message.current_location -%} + {{- '\n' ~ 'Current location: ' + system_message.current_location }} + {%- endif -%} +{%- endmacro -%} +{#- Main Template Logic ================================================= -#} +{#- Extract system message (only first message if it's system) -#} +{%- set system_message = none -%} +{%- set conversation_messages = messages -%} +{%- if messages and messages[0].role == "system" -%} + {%- set system_message = messages[0] -%} + {%- set conversation_messages = messages[1:] -%} +{%- endif -%} +{#- Get the last user message turn, for interleved thinking -#} +{%- set ns = namespace(last_user_index=-1) %} +{% for m in conversation_messages %} + {%- if m.role == 'user' %} + {% set ns.last_user_index = loop.index0 -%} + {%- endif %} +{%- endfor %} +{#- Render system message -#} +{{- ']~!b[' ~ ']~b]system' ~ '\n' }} +{{- build_system_message(system_message) }} +{#- Render tools if available -#} +{%- if tools -%} + {{- '\n\n' ~ '# Tools' ~ '\n' ~ 'You may call one or more tools to assist with the user query.\nHere are the tools available in JSONSchema format:' ~ '\n' }} + {{- '\n' ~ '' ~ '\n' }} + {{- render_tool_namespace("functions", tools) }} + {{- '' ~ '\n\n' }} +{{- 'When making tool calls, use XML format to invoke tools and pass parameters:' ~ '\n' }} +{{- '\n' ~ toolcall_begin_token }} + +param-value-1 +param-value-2 +... + +{{- '\n' ~ toolcall_end_token }} +{%- endif -%} +{{- '[e~[\n' }} + +{#- Render messages -#} +{%- set last_tool_call = namespace(name=none) -%} +{%- for message in conversation_messages -%} + {%- if message.role == 'assistant' -%} + {#- Only render reasoning_content if no user message follows (or clear_thinking disabled) -#} + {{- ']~b]ai' ~ '\n' }} + + {%- set reasoning_content = '' %} + {%- set content = visible_text(message.content) %} + {%- if message.reasoning_content is string %} + {%- set reasoning_content = message.reasoning_content %} + {%- else %} + {%- if '' in content %} + {%- set reasoning_content = content.split('')[0].strip('\n').split('')[-1].strip('\n') %} + {%- set content = content.split('')[-1].strip('\n') %} + {%- endif %} + {%- endif %} + {%- if reasoning_content and (not (clear_thinking | default(true)) or loop.index0 > ns.last_user_index) -%} + {{- '' ~ '\n' ~ reasoning_content ~ '\n' ~ '' ~ '\n\n' }} + {%- endif -%} + {%- if content -%} + {{- content }} + {%- endif -%} + {%- if message.tool_calls -%} + {{- '\n' ~ toolcall_begin_token ~ '\n' }} + + {%- for tool_call in message.tool_calls -%} + {%- if tool_call.function %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {{- '' }} + {% set _args = tool_call.arguments %} + {%- for k, v in _args.items() %} + {{- '' }} + {{- v | tojson(ensure_ascii=False) if v is not string else v }} + {{- '' }} + {% endfor %} + {{- '' ~ '\n' }} + {%- endfor -%} + + {{- toolcall_end_token}} + {%- set last_tool_call.name = message.tool_calls[-1].name -%} + {%- else -%} + {%- set last_tool_call.name = none -%} + {%- endif -%} + {{- '[e~[' ~ '\n' }} + + {%- elif message.role == 'tool' -%} + {%- if last_tool_call.name is none -%} + {{- raise_exception("Message has tool role, but there was no previous assistant message with a tool call!") }} + {%- endif -%} + {%- if loop.first or (conversation_messages[loop.index0 - 1].role != 'tool') -%} + {{- ']~b]tool' }} + {%- endif -%} + {%- if message.content is string -%} + {{- '\n' }} + {{- message.content }} + {{- '' }} + {%- else -%} + {%- for tr in message.content -%} + {{- '\n' }} + {{- tr.output if tr.output is defined else (tr.text if tr.type == 'text' and tr.text is defined else tr) }} + {{- '\n' }} + {%- endfor -%} + {%- endif -%} + {%- if loop.last or (conversation_messages[loop.index0 + 1].role != 'tool') -%} + {{- '[e~[\n' -}} + {%- endif -%} + + {%- elif message.role == 'user' -%} + {{- ']~b]user' ~ '\n' }} + {{- visible_text(message.content) }} + {{- '[e~[' ~ '\n' }} + {%- endif -%} +{%- endfor -%} + +{#- Generation prompt -#} +{%- if add_generation_prompt -%} +{{- ']~b]ai' ~ '\n' ~ '' ~ '\n' }} +{%- endif -%} diff --git a/miles/utils/chat_template_utils/templates/minimax_m27_fixed.jinja b/miles/utils/chat_template_utils/templates/minimax_m27_fixed.jinja new file mode 100644 index 00000000000..b49b81532ba --- /dev/null +++ b/miles/utils/chat_template_utils/templates/minimax_m27_fixed.jinja @@ -0,0 +1,159 @@ +{# ----------‑‑‑ special token variables ‑‑‑---------- #} +{%- set toolcall_begin_token = '' -%} +{%- set toolcall_end_token = '' -%} +{#- Tool Rendering Functions ============================================== -#} +{%- macro render_tool_namespace(namespace_name, tool_list) -%} +{%- for tool in tool_list -%} +{{ tool.function | tojson(ensure_ascii=False) }} +{% endfor -%} +{%- endmacro -%} +{%- macro visible_text(content) -%} + {%- if content is string -%} + {{ content }} + {%- elif content is iterable and content is not mapping -%} + {%- for item in content -%} + {%- if item is mapping and item.type == 'text' -%} + {{- item.text }} + {%- elif item is string -%} + {{- item }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {{- content }} + {%- endif -%} +{%- endmacro -%} +{#- System Message Construction ============================================ -#} +{%- macro build_system_message(system_message) -%} + {%- if system_message and system_message.content -%} + {{- visible_text(system_message.content) }} + {%- else -%} + {%- if model_identity is not defined -%} + {%- set model_identity = "You are a helpful assistant. Your name is MiniMax-M2.7 and is built by MiniMax." -%} + {%- endif -%} + {{- model_identity }} + {%- endif -%} + + {#- Handle current_date -#} + {%- if system_message and system_message.current_date -%} + {{- '\n' ~ 'Current date: ' + system_message.current_date }} + {%- endif -%} + {#- Handle current_location -#} + {%- if system_message and system_message.current_location -%} + {{- '\n' ~ 'Current location: ' + system_message.current_location }} + {%- endif -%} +{%- endmacro -%} +{#- Main Template Logic ================================================= -#} +{#- Extract system message (only first message if it's system) -#} +{%- set system_message = none -%} +{%- set conversation_messages = messages -%} +{%- if messages and messages[0].role == "system" -%} + {%- set system_message = messages[0] -%} + {%- set conversation_messages = messages[1:] -%} +{%- endif -%} +{#- Get the last user message turn, for interleved thinking -#} +{%- set ns = namespace(last_user_index=-1) %} +{% for m in conversation_messages %} + {%- if m.role == 'user' %} + {% set ns.last_user_index = loop.index0 -%} + {%- endif %} +{%- endfor %} +{#- Render system message -#} +{{- ']~!b[' ~ ']~b]system' ~ '\n' }} +{{- build_system_message(system_message) }} +{#- Render tools if available -#} +{%- if tools -%} + {{- '\n\n' ~ '# Tools' ~ '\n' ~ 'You may call one or more tools to assist with the user query.\nHere are the tools available in JSONSchema format:' ~ '\n' }} + {{- '\n' ~ '' ~ '\n' }} + {{- render_tool_namespace("functions", tools) }} + {{- '' ~ '\n\n' }} +{{- 'When making tool calls, use XML format to invoke tools and pass parameters:' ~ '\n' }} +{{- '\n' ~ toolcall_begin_token }} + +param-value-1 +param-value-2 +... + +{{- '\n' ~ toolcall_end_token }} +{%- endif -%} +{{- '[e~[\n' }} + +{#- Render messages -#} +{%- set last_tool_call = namespace(name=none) -%} +{%- for message in conversation_messages -%} + {%- if message.role == 'assistant' -%} + {#- Only render reasoning_content if no user message follows (or clear_thinking disabled) -#} + {{- ']~b]ai' ~ '\n' }} + + {%- set reasoning_content = '' %} + {%- set content = visible_text(message.content) %} + {%- if message.reasoning_content is string %} + {%- set reasoning_content = message.reasoning_content %} + {%- else %} + {%- if '' in content %} + {%- set reasoning_content = content.split('')[0].strip('\n').split('')[-1].strip('\n') %} + {%- set content = content.split('')[-1].strip('\n') %} + {%- endif %} + {%- endif %} + {%- if reasoning_content and (not (clear_thinking | default(true)) or loop.index0 > ns.last_user_index) -%} + {{- '' ~ '\n' ~ reasoning_content ~ '\n' ~ '' ~ '\n\n' }} + {%- endif -%} + {%- if content -%} + {{- content }} + {%- endif -%} + {%- if message.tool_calls -%} + {{- '\n' ~ toolcall_begin_token ~ '\n' }} + + {%- for tool_call in message.tool_calls -%} + {%- if tool_call.function %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {{- '' }} + {% set _args = tool_call.arguments %} + {%- for k, v in _args.items() %} + {{- '' }} + {{- v | tojson(ensure_ascii=False) if v is not string else v }} + {{- '' }} + {% endfor %} + {{- '' ~ '\n' }} + {%- endfor -%} + + {{- toolcall_end_token}} + {%- set last_tool_call.name = message.tool_calls[-1].name -%} + {%- else -%} + {%- set last_tool_call.name = none -%} + {%- endif -%} + {{- '[e~[' ~ '\n' }} + + {%- elif message.role == 'tool' -%} + {%- if last_tool_call.name is none -%} + {{- raise_exception("Message has tool role, but there was no previous assistant message with a tool call!") }} + {%- endif -%} + {%- if loop.first or (conversation_messages[loop.index0 - 1].role != 'tool') -%} + {{- ']~b]tool' }} + {%- endif -%} + {%- if message.content is string -%} + {{- '\n' }} + {{- message.content }} + {{- '' }} + {%- else -%} + {%- for tr in message.content -%} + {{- '\n' }} + {{- tr.output if tr.output is defined else (tr.text if tr.type == 'text' and tr.text is defined else tr) }} + {{- '\n' }} + {%- endfor -%} + {%- endif -%} + {%- if loop.last or (conversation_messages[loop.index0 + 1].role != 'tool') -%} + {{- '[e~[\n' -}} + {%- endif -%} + + {%- elif message.role == 'user' -%} + {{- ']~b]user' ~ '\n' }} + {{- visible_text(message.content) }} + {{- '[e~[' ~ '\n' }} + {%- endif -%} +{%- endfor -%} + +{#- Generation prompt -#} +{%- if add_generation_prompt -%} +{{- ']~b]ai' ~ '\n' ~ '' ~ '\n' }} +{%- endif -%} diff --git a/miles/utils/chat_template_utils/tito_tokenizer.py b/miles/utils/chat_template_utils/tito_tokenizer.py index a568ab316df..0eb8744f88c 100644 --- a/miles/utils/chat_template_utils/tito_tokenizer.py +++ b/miles/utils/chat_template_utils/tito_tokenizer.py @@ -22,7 +22,7 @@ import logging from collections.abc import Iterable from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from pathlib import Path from typing import Any @@ -104,12 +104,14 @@ def __init__( tokenizer: Any, chat_template_kwargs: dict[str, Any] | None = None, assistant_start_str: str | None = None, + special_token_ids: set[int] | None = None, allowed_append_roles: list[str] | None = None, ): self.tokenizer = tokenizer self.chat_template_kwargs = chat_template_kwargs or {} self._assistant_start_str = assistant_start_str self.allowed_append_roles: list[str] = allowed_append_roles if allowed_append_roles is not None else ["tool"] + self.special_token_ids: set[int] = special_token_ids def create_comparator(self) -> TokenSeqComparator: """Create a :class:`TokenSeqComparator` configured with this @@ -117,6 +119,7 @@ def create_comparator(self) -> TokenSeqComparator: return TokenSeqComparator( self.tokenizer, assistant_start_str=self._assistant_start_str, + special_token_ids=self.special_token_ids, trim_trailing_ids=self.trailing_token_ids or None, ) @@ -459,25 +462,296 @@ def merge_tokens( # --------------------------------------------------------------------------- -# Enum + Registry + Factory +# Nemotron 3 implementation # --------------------------------------------------------------------------- -class TITOTokenizerType(str, Enum): +class Nemotron3TITOTokenizer(Qwen3TITOTokenizer): + """NVIDIA Nemotron 3 family: ``<|im_end|>\\n`` message boundaries. + + Inherits Qwen3's boundary handling — Nemotron 3 emits the same + ``<|im_end|>\\n`` after every message and the model stops at + ``<|im_end|>`` without the trailing newline. + + No fixed jinja is shipped — HF native template is append-only when + ``truncate_history_thinking=False``. Multi-user-turn surfaces + auto-merge that kwarg via ``extra_kwargs`` below; ``{tool}``-only does + not need it (no user-turn boundary to truncate across). + + The plain-text assistant turn does not roundtrip cleanly under + sglang's upstream ``nemotron_3`` reasoning parser (it keeps a trailing + ``\\n`` in ``reasoning_content``), so step-4 ``assistant_text`` soft + assertion is expected to fail until the parser is patched upstream — + out of scope for this family registration. + """ + + reasoning_parser = "nemotron_3" + tool_call_parser = "qwen3_coder" + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template=None, + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template=None, + extra_kwargs={"truncate_history_thinking": False}, + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user", "system"}), + template=None, + extra_kwargs={"truncate_history_thinking": False}, + ), + ) + + _default_assistant_start_str: str = "<|im_start|>assistant\n" + + def __init__( + self, + tokenizer: Any, + chat_template_kwargs: dict[str, Any] | None = None, + assistant_start_str: str | None = None, + allowed_append_roles: list[str] | None = None, + ): + super().__init__( + tokenizer, + chat_template_kwargs, + assistant_start_str or self._default_assistant_start_str, + allowed_append_roles=allowed_append_roles, + ) + + +# --------------------------------------------------------------------------- +# Kimi K2 implementation +# --------------------------------------------------------------------------- + + +def _kimi_segment_special_token_ids(tokenizer: Any) -> set[int]: + """Kimi specials minus ``<|im_middle|>`` (intra-turn role-name/body + separator, not a role boundary; must not be a segment boundary).""" + return TokenSeqComparator.collect_special_ids(tokenizer) - {tokenizer.convert_tokens_to_ids("<|im_middle|>")} + + +class Kimi25TITOTokenizer(TITOTokenizer): + """Moonshot Kimi K2.5: ``<|im_end|>`` boundary (no trailing newline). + + K2.5 has no kwarg escape hatch for the "drop reasoning of prior assistants + once a new non-tool-call assistant arrives" behavior. Ships a + bundled fixed jinja that wraps the ``last_non_tool_call_assistant_msg`` + loop in ``{%- if not preserve_thinking -%}`` so multi-user-turn rollout + can pass ``preserve_thinking=True`` to keep history append-only. Only the + ``{tool, user}`` surface is registered (per current onboarding scope). + """ + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="kimi_k25_fixed.jinja", + extra_kwargs={"preserve_thinking": True}, + ), + ) + + _default_assistant_start_str: str = "<|im_assistant|>" + + def __init__( + self, + tokenizer: Any, + chat_template_kwargs: dict[str, Any] | None = None, + assistant_start_str: str | None = None, + allowed_append_roles: list[str] | None = None, + ): + super().__init__( + tokenizer, + chat_template_kwargs, + assistant_start_str or self._default_assistant_start_str, + special_token_ids=_kimi_segment_special_token_ids(tokenizer), + allowed_append_roles=allowed_append_roles, + ) + + +class Kimi26TITOTokenizer(TITOTokenizer): + """Moonshot Kimi K2.6: same boundary as K2.5 + native ``preserve_thinking`` kwarg. + + K2.6's HF-native template already carries the ``preserve_thinking`` gate + that K2.5 needs patched in. No bundled fixed + template required; ``{tool, user}`` row registers ``template=None`` and + auto-merges ``preserve_thinking=True`` for multi-user-turn rollout. + + Tool-call parser is bound to ``kimi_k2_raw_id`` rather than ``kimi_k2``: + RL trajectories need the model-emitted ``tool_call_id`` to round-trip + verbatim across turns (no ``history_tool_calls_cnt`` renumbering), and + miles is the primary consumer of this TITO family. + """ + + reasoning_parser = "kimi_k2" + tool_call_parser = "kimi_k2_raw_id" + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template=None, + extra_kwargs={"preserve_thinking": True}, + ), + ) + + _default_assistant_start_str: str = "<|im_assistant|>" + + def __init__( + self, + tokenizer: Any, + chat_template_kwargs: dict[str, Any] | None = None, + assistant_start_str: str | None = None, + allowed_append_roles: list[str] | None = None, + ): + super().__init__( + tokenizer, + chat_template_kwargs, + assistant_start_str or self._default_assistant_start_str, + special_token_ids=_kimi_segment_special_token_ids(tokenizer), + allowed_append_roles=allowed_append_roles, + ) + + +# --------------------------------------------------------------------------- +# MiniMax M2 family implementation (M2.5 and M2.7 share tokenizer/arch and +# stop-token semantics; only their default system identity strings differ). +# --------------------------------------------------------------------------- + + +class MinimaxM25TITOTokenizer(TITOTokenizer): + """MiniMax-M2.5 family: bespoke ``]~!b[`` / ``[e~[`` / ``]~b]`` tag set. + + Shares tokenizer.json (sha256) and architecture (MiniMaxM2ForCausalLM) + with M2.7 — only the chat template's default system identity string + differs (``MiniMax-M2.5`` vs ``MiniMax-M2.7``). Stop-token handling + (``[e~[`` / trailing newline) is identical to M2.7. + + Reasoning is gated by a per-message ``last_user_index`` check: + ``reasoning_content`` is only rendered for assistant turns *after* the + last ``user`` — appending a new ``user`` therefore strips prior assistant + ```` blocks and breaks append-only. Only ``{tool}`` surface is + registered on HF-native template for that reason; multi-user-turn + requires the fixed jinja with ``clear_thinking=False`` to always + preserve history reasoning. + """ + + reasoning_parser = "minimax-append-think" + tool_call_parser = "minimax-m2" + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template=None, + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="minimax_m25_fixed.jinja", + extra_kwargs={"clear_thinking": False}, + ), + ) + + _default_assistant_start_str: str = "]~b]ai" + + def __init__( + self, + tokenizer: Any, + chat_template_kwargs: dict[str, Any] | None = None, + assistant_start_str: str | None = None, + allowed_append_roles: list[str] | None = None, + ): + super().__init__( + tokenizer, + chat_template_kwargs, + assistant_start_str or self._default_assistant_start_str, + allowed_append_roles=allowed_append_roles, + ) + nl_ids = tokenizer.encode("\n", add_special_tokens=False) + assert len(nl_ids) == 1, f"Expected single newline token, got {nl_ids}" + self._newline_id: int = nl_ids[0] + self._eos_id: int = tokenizer.convert_tokens_to_ids("[e~[") + self.trailing_token_ids = frozenset({self._newline_id}) + + def merge_tokens( + self, + old_messages: list[dict[str, Any]], + new_messages: list[dict[str, Any]], + pretokenized_token_ids: list[int], + tools: list[dict[str, Any]] | None = None, + ) -> list[int]: + incremental = self.tokenize_additional_non_assistant(old_messages, new_messages, tools) + prefix = list(pretokenized_token_ids) + if prefix and prefix[-1] == self._eos_id: + prefix.append(self._newline_id) + return prefix + incremental + + +class MinimaxM27TITOTokenizer(MinimaxM25TITOTokenizer): + """MiniMax-M2.7 family: tokenizer / arch / stop-token semantics identical + to M2.5; the chat template only differs by default system identity string. + + Inherits parsers, ``__init__``, ``merge_tokens``, and + ``_default_assistant_start_str`` from M2.5; only ``SUPPORTED_TEMPLATES`` + is rebound to ``minimax_m27_fixed.jinja`` so the fixed-template lookup + points at the M2.7-derived jinja. + """ + + SUPPORTED_TEMPLATES = ( + FixedTemplateRow( + allowed_roles=frozenset({"tool"}), + template=None, + ), + FixedTemplateRow( + allowed_roles=frozenset({"tool", "user"}), + template="minimax_m27_fixed.jinja", + extra_kwargs={"clear_thinking": False}, + ), + ) + + +# --------------------------------------------------------------------------- +# Enum + Factory +# --------------------------------------------------------------------------- + + +class TITOTokenizerType(StrEnum): DEFAULT = "default" QWEN3 = "qwen3" QWEN35 = "qwen35" QWENNEXT = "qwennext" GLM47 = "glm47" - - -_TOKENIZER_REGISTRY: dict[TITOTokenizerType, type[TITOTokenizer]] = { - TITOTokenizerType.DEFAULT: TITOTokenizer, - TITOTokenizerType.QWEN3: Qwen3TITOTokenizer, - TITOTokenizerType.QWEN35: Qwen35TITOTokenizer, - TITOTokenizerType.QWENNEXT: QwenNextTITOTokenizer, - TITOTokenizerType.GLM47: GLM47TITOTokenizer, -} + NEMOTRON3 = "nemotron3" + KIMI25 = "kimi25" + KIMI26 = "kimi26" + MINIMAX_M25 = "minimax_m25" + MINIMAX_M27 = "minimax_m27" + + @classmethod + def get_tokenizer_class(cls, t: TITOTokenizerType) -> type[TITOTokenizer]: + """Resolve the concrete ``TITOTokenizer`` subclass for *t*.""" + match t: + case cls.DEFAULT: + return TITOTokenizer + case cls.QWEN3: + return Qwen3TITOTokenizer + case cls.QWEN35: + return Qwen35TITOTokenizer + case cls.QWENNEXT: + return QwenNextTITOTokenizer + case cls.GLM47: + return GLM47TITOTokenizer + case cls.NEMOTRON3: + return Nemotron3TITOTokenizer + case cls.KIMI25: + return Kimi25TITOTokenizer + case cls.KIMI26: + return Kimi26TITOTokenizer + case cls.MINIMAX_M25: + return MinimaxM25TITOTokenizer + case cls.MINIMAX_M27: + return MinimaxM27TITOTokenizer + case _: + raise ValueError(f"Unknown TITOTokenizerType: {t!r}") def get_tito_tokenizer( @@ -505,7 +779,7 @@ def get_tito_tokenizer( raise ValueError("tokenizer must not be None") if isinstance(tokenizer_type, str): tokenizer_type = TITOTokenizerType(tokenizer_type) - cls = _TOKENIZER_REGISTRY[tokenizer_type] + cls = TITOTokenizerType.get_tokenizer_class(tokenizer_type) kwargs: dict[str, Any] = {"chat_template_kwargs": chat_template_kwargs} if assistant_start_str is not None: kwargs["assistant_start_str"] = assistant_start_str @@ -547,7 +821,7 @@ def resolve_fixed_chat_template( f"Unknown roles in allowed_append_roles: {sorted(invalid)}. " f"Supported: {sorted(_VALID_ROLES)}." ) - cls = _TOKENIZER_REGISTRY[tito_model] + cls = TITOTokenizerType.get_tokenizer_class(tito_model) candidates = [row for row in cls.SUPPORTED_TEMPLATES if requested.issubset(row.allowed_roles)] if not candidates: raise ValueError( @@ -609,7 +883,7 @@ def resolve_reasoning_and_tool_call_parser( """ if isinstance(tito_model, str): tito_model = TITOTokenizerType(tito_model) - cls = _TOKENIZER_REGISTRY[tito_model] + cls = TITOTokenizerType.get_tokenizer_class(tito_model) def _resolve_one(field: str, bound: str | None, user: str | None) -> str | None: if user is None: diff --git a/miles/utils/chat_template_utils/token_seq_comparator.py b/miles/utils/chat_template_utils/token_seq_comparator.py index c2889d22a13..2f4b93d5fb2 100644 --- a/miles/utils/chat_template_utils/token_seq_comparator.py +++ b/miles/utils/chat_template_utils/token_seq_comparator.py @@ -61,7 +61,12 @@ class TokenSeqComparator: ---------- tokenizer : PreTrainedTokenizerBase special_token_ids : set[int] | None - Extra IDs treated as segment boundaries (merged with auto-detected ones). + Token IDs that mark segment boundaries. Default None auto-detects + them (see :meth:`collect_special_ids`). Pass an explicit set to + override — needed when the tokenizer flags a token ``special=True`` + even though it lives inside a role's turn (e.g. Kimi's + ``<|im_middle|>`` between role name and body) and so must not + split the segment. assistant_start_str : str Decoded text prefix identifying assistant content segments, e.g. ``"<|im_start|>assistant"`` (Qwen3) or ``"<|assistant|>"`` (GLM). @@ -81,14 +86,15 @@ def __init__( trim_trailing_ids: set[int] | None = None, ): self.tokenizer = tokenizer - self._special_ids = self._collect_special_ids(tokenizer) if special_token_ids is not None: - self._special_ids |= set(special_token_ids) + self._special_ids = set(special_token_ids) + else: + self._special_ids = self.collect_special_ids(tokenizer) self._assistant_start_str = assistant_start_str self._trim_trailing_ids: set[int] | None = set(trim_trailing_ids) if trim_trailing_ids else None @staticmethod - def _collect_special_ids(tokenizer) -> set[int]: + def collect_special_ids(tokenizer) -> set[int]: """Collect token IDs with ``special=True`` from the tokenizer. Special tokens are structural markers added by the chat template to diff --git a/miles/utils/processing_utils.py b/miles/utils/processing_utils.py index 9d12eed8b31..8d32915aad0 100644 --- a/miles/utils/processing_utils.py +++ b/miles/utils/processing_utils.py @@ -2,11 +2,53 @@ import io import logging import os +from pathlib import Path +from huggingface_hub import hf_hub_download +from tokenizers import Tokenizer as RawTokenizer from transformers import AutoProcessor, AutoTokenizer, PreTrainedTokenizerBase, ProcessorMixin logger = logging.getLogger(__name__) + +def _fix_v5_tokenizer_components(tokenizer: PreTrainedTokenizerBase, model_name_or_path: str) -> None: + # transformers v5's LlamaTokenizerFast rebuilds pre_tokenizer/decoder in + # __init__, discarding the originals from tokenizer.json. DeepSeek-V3.2 + # declares LlamaTokenizerFast but actually uses ByteLevel, so without this + # fix the loaded tokenizer decodes Metaspace ▁ instead of ByteLevel Ġ/Ċ + # and diverges from the sglang-served tokenizer. Mirrors sglang's + # _fix_v5_tokenizer_components (hf_transformers_utils.py). + backend = getattr(tokenizer, "_tokenizer", None) + if backend is None: + return + + try: + local_path = Path(model_name_or_path) / "tokenizer.json" + if local_path.is_file(): + tok_file = str(local_path) + else: + tok_file = hf_hub_download(model_name_or_path, "tokenizer.json", local_files_only=True) + raw = RawTokenizer.from_file(tok_file) + except Exception as e: + logger.warning("Could not load tokenizer.json for %s: %s", model_name_or_path, e) + return + + raw_pre = type(raw.pre_tokenizer).__name__ if raw.pre_tokenizer else None + loaded_pre = type(backend.pre_tokenizer).__name__ if backend.pre_tokenizer else None + + if raw_pre and loaded_pre and raw_pre != loaded_pre: + logger.info( + "Fixing v5 tokenizer component mismatch for %s: pre_tokenizer %s -> %s, decoder %s -> %s", + model_name_or_path, + loaded_pre, + raw_pre, + type(backend.decoder).__name__ if backend.decoder else None, + type(raw.decoder).__name__ if raw.decoder else None, + ) + backend.pre_tokenizer = raw.pre_tokenizer + backend.decoder = raw.decoder + + # 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 @@ -34,6 +76,7 @@ def load_tokenizer(name_or_path: str, chat_template_path: str | None = None, **k return _TOKENIZER_CACHE[cache_key] tokenizer = AutoTokenizer.from_pretrained(name_or_path, **kwargs) + _fix_v5_tokenizer_components(tokenizer, name_or_path) if chat_template_path: assert os.path.isfile(chat_template_path), ( f"chat_template_path not found: {chat_template_path}. " diff --git a/miles/utils/test_utils/session_verify_agent.py b/miles/utils/test_utils/session_verify_agent.py index f01696f731c..8ef8df61a18 100644 --- a/miles/utils/test_utils/session_verify_agent.py +++ b/miles/utils/test_utils/session_verify_agent.py @@ -11,7 +11,7 @@ import json import logging import os -from enum import Enum +from enum import Enum, StrEnum import httpx @@ -35,6 +35,50 @@ class DriverAction(Enum): _R = DriverAction.ROLLBACK _F = DriverAction.FORCE_FINAL + +class ToolCallFailureMode(StrEnum): + """Recovery strategy when a TOOL_RESULT step finds the assistant emitted no tool_calls. + + APPEND_TOOL : Splice a sentinel ``tool`` message and continue. Works on + lenient templates; strict templates that hard-assert any + ``tool`` role must follow an assistant with ``tool_calls`` + (e.g. MiniMax-M2.7) will reject the next request at server-side. + APPEND_USER : Splice a ``user`` message carrying the same failure text as + APPEND_TOOL. Requires "user" in ``allowed_append_roles`` — + raises ValueError at agent start otherwise, so misconfig is + immediately visible instead of silently downgrading. + ROLLBACK : Pop the offending assistant and let the loop's chat call at + the bottom re-inference. Universal — no role-surface + dependency — and the default. + """ + + APPEND_TOOL = "append_tool" + APPEND_USER = "append_user" + ROLLBACK = "rollback" + + +DEFAULT_TOOL_CALL_FAILURE_MODE = ToolCallFailureMode.ROLLBACK + +# Cap consecutive ROLLBACK retries — same context every time, so a model that +# never tool-calls would loop forever. +MAX_CONSECUTIVE_TOOL_CALL_FAILURE_ROLLBACKS = 3 + +# Same body for both APPEND_TOOL and APPEND_USER fallbacks; only the role of +# the spliced message differs between the two modes. +TOOL_CALL_PARSE_FAILURE_TEXT = ( + "Tool call parsing failed: the previous assistant turn did not emit a " + "parseable tool_call. Please retry with a valid tool invocation." +) + +# Mismatch tiers reported by the session-server's per-sample comparator +# (sessions.py:83). Any occurrence of these "hard" types in a sample's +# tito_session_mismatch indicates a TITO bug and fails the sample. The +# soft `assistant_text` tier is excluded — it is aggregated across samples +# and gated by a ratio threshold instead. +_FORBIDDEN_MISMATCH_TYPES: frozenset[str] = frozenset( + {"special_token_count", "special_token_type", "non_assistant_text"} +) + # Override per call: ``--session-verify-cycles N`` (CLI) or ``cycles=N`` # (pytest via ``run_session_verify``). Smaller-context models with a 4K # response budget should drop to 2 to avoid context overflow. @@ -151,6 +195,16 @@ async def run_agent(base_url, prompt, request_kwargs, metadata, **kwargs): cycles = metadata.get("session_verify_cycles", DEFAULT_CYCLES) schedule = select_schedule(allowed_roles, cycles=cycles) + failure_mode = ToolCallFailureMode(metadata.get("tool_call_failure_mode", DEFAULT_TOOL_CALL_FAILURE_MODE)) + # APPEND_USER injects a user message — only valid if 'user' is in + # allowed_append_roles. Refuse up front instead of silently downgrading. + if failure_mode is ToolCallFailureMode.APPEND_USER and "user" not in allowed_roles: + raise ValueError( + f"tool_call_failure_mode=APPEND_USER requires 'user' in allowed_append_roles, " + f"got {sorted(allowed_roles)}. Pick ROLLBACK (universal) or APPEND_TOOL " + "(lenient-template) for tool-only surfaces." + ) + rk = {k: v for k, v in request_kwargs.items() if k not in ("tools",)} messages = build_initial_messages() events: list[str] = [] @@ -161,6 +215,11 @@ async def run_agent(base_url, prompt, request_kwargs, metadata, **kwargs): "tool_result_count": 0, "tool_call_count": 0, } + # Streak of TOOL_RESULT steps that fell into the ROLLBACK fallback without + # the model recovering to a real tool_call. Reset on any successful + # tool_call; gated by MAX_CONSECUTIVE_TOOL_CALL_FAILURE_ROLLBACKS to keep + # silently-stuck samples from burning wall-time. + consecutive_failure_rollbacks = 0 async with httpx.AsyncClient(timeout=180) as client: # Initial completion — no driver action yet. @@ -176,6 +235,7 @@ async def run_agent(base_url, prompt, request_kwargs, metadata, **kwargs): if action is DriverAction.TOOL_RESULT: tool_calls = assistant.get("tool_calls") or [] if tool_calls: + consecutive_failure_rollbacks = 0 for i, tc in enumerate(tool_calls): result_idx = (counters["tool_result_count"] + i) % len(MOCK_TOOL_RESULTS) messages.append( @@ -188,21 +248,49 @@ async def run_agent(base_url, prompt, request_kwargs, metadata, **kwargs): counters["tool_result_count"] += len(tool_calls) events.append("append_tool") else: - # Model didn't tool-call — append a sentinel tool message - # so a ``{tool}``-only session is not rejected by the - # server's append-role check. Coverage still flags this - # via the missing ``append_tool`` event. - messages.append( - { - "role": "tool", - "tool_call_id": "none", - "content": ( - "Driver note: the previous assistant turn did not " - "emit a tool_call, so no tool was actually invoked." - ), - } - ) - events.append("tool_result_skipped_no_tool_call") + # Model emitted no tool_calls — apply the configured fallback. + # Templates differ on what role may follow a "no tool_calls" + # assistant: + # - GLM / Nemotron (lenient): a tool message is fine -> APPEND_TOOL. + # - Kimi: a tool message must carry the id from a valid + # tool_call, which we don't have -> APPEND_TOOL not OK. + # - MiniMax: a tool message must follow an assistant with + # non-empty tool_calls -> APPEND_TOOL not OK. + # If APPEND_TOOL not ok, use APPEND_USER as instead. + match failure_mode: + case ToolCallFailureMode.APPEND_TOOL: + messages.append( + { + "role": "tool", + "tool_call_id": "none", + "content": TOOL_CALL_PARSE_FAILURE_TEXT, + } + ) + events.append("tool_call_failure_append_tool") + case ToolCallFailureMode.APPEND_USER: + messages.append({"role": "user", "content": TOOL_CALL_PARSE_FAILURE_TEXT}) + counters["user_count"] += 1 + events.append("tool_call_failure_append_user") + case ToolCallFailureMode.ROLLBACK: + # Same as the schedule's ROLLBACK + assert messages and messages[-1]["role"] == "assistant", ( + f"tool_call_failure_mode=ROLLBACK: tail role is " + f"{messages[-1]['role'] if messages else 'EMPTY'}, expected assistant" + ) + consecutive_failure_rollbacks += 1 + if consecutive_failure_rollbacks > MAX_CONSECUTIVE_TOOL_CALL_FAILURE_ROLLBACKS: + raise AssertionError( + f"ROLLBACK fallback hit {consecutive_failure_rollbacks} consecutive " + f"tool_call failures (limit={MAX_CONSECUTIVE_TOOL_CALL_FAILURE_ROLLBACKS}). " + "Model is not tool-calling on this prompt — check sampling temperature, " + "the tool spec, or switch tool_call_failure_mode to APPEND_TOOL/APPEND_USER " + "if sentinel-driven retry is preferred." + ) + messages.pop() + counters["rollback_count"] += 1 + events.append("tool_call_failure_rollback") + case _: + raise AssertionError(f"Unknown ToolCallFailureMode {failure_mode!r}") elif action is DriverAction.USER_FOLLOWUP: messages.append({"role": "user", "content": USER_FOLLOWUP_TEXT}) @@ -215,9 +303,11 @@ async def run_agent(base_url, prompt, request_kwargs, metadata, **kwargs): events.append("append_system") elif action is DriverAction.ROLLBACK: - # Drop the last assistant; the next request's messages list - # is shorter than the session accumulator, so the server walks - # _detect_and_rollback before re-inferencing the assistant. + # Pop the last assistant from our local copy. The next + # request therefore has one fewer message than what the + # server has stored, which is the trigger for its + # ``_detect_and_rollback`` path — the server rewinds its + # state, then re-inferences. if not messages or messages[-1]["role"] != "assistant": raise AssertionError( f"Cannot rollback at step {step_idx}: tail role is " @@ -254,14 +344,17 @@ async def generate(input: GenerateFnInput) -> GenerateFnOutput: """ allowed_roles = list(input.args.tito_allowed_append_roles) cycles = getattr(input.args, "session_verify_cycles", DEFAULT_CYCLES) + failure_mode = getattr(input.args, "tool_call_failure_mode", DEFAULT_TOOL_CALL_FAILURE_MODE) # Sample.metadata is mutable even when the outer dataclass is frozen. input.sample.metadata["allowed_append_roles"] = allowed_roles input.sample.metadata["session_verify_cycles"] = cycles + input.sample.metadata["tool_call_failure_mode"] = failure_mode output = await _base_generate(input) samples = output.samples if isinstance(output.samples, list) else [output.samples] events_per_sample = [s.metadata.get("driver_events", []) for s in samples] + metrics_path = os.environ.get("MILES_SESSION_VERIFY_METRICS_PATH") required_per_sample = ["rollback"] if "user" in allowed_roles: @@ -277,20 +370,12 @@ async def generate(input: GenerateFnInput) -> GenerateFnOutput: f"{missing}. allowed_roles={allowed_roles}, events={events}" ) - if not any("append_tool" in events for events in events_per_sample): + if not metrics_path and not any("append_tool" in events for events in events_per_sample): raise AssertionError( "Session multi-role e2e: no sample produced an append_tool action — " f"the model may not be tool-calling. events_per_sample={events_per_sample}" ) - # Token-seq comparator metrics (server-computed in sessions.py:83). Any of - # special_token_count / special_token_type / non_assistant_text mismatches - # is a hard TITO bug and must be 0 per-sample; assistant_text mismatches - # are softer (assistant tokens inherited from the pretokenized prefix may - # not match the chat template's canonical tokenization) and are aggregated - # across samples via a metrics file. - forbidden_types = {"special_token_count", "special_token_type", "non_assistant_text"} - metrics_path = os.environ.get("MILES_SESSION_VERIFY_METRICS_PATH") for i, sample in enumerate(samples): mismatches = sample.metadata.get("tito_session_mismatch") if mismatches is None: @@ -300,7 +385,7 @@ async def generate(input: GenerateFnInput) -> GenerateFnOutput: f"TokenizationError (sessions.py:83 swallows it) — this always " f"indicates a TITO subclass / setup bug, not a real PASS." ) - forbidden = [m for m in mismatches if m.get("type") in forbidden_types] + forbidden = [m for m in mismatches if m.get("type") in _FORBIDDEN_MISMATCH_TYPES] if forbidden: raise AssertionError( f"Session multi-role e2e: sample {i} has forbidden mismatches " @@ -323,6 +408,7 @@ async def generate(input: GenerateFnInput) -> GenerateFnOutput: json.dumps( { "sample_index": i, + "driver_events": events_per_sample[i], "had_assistant_mismatch": had_assistant_mismatch, "total_mismatches": len(mismatches), "assistant_mismatch_count": len(assistant_mismatches), @@ -353,6 +439,17 @@ def _add_arguments(parser): "longer but expand context length. Drop to 2 on tighter-context " "models (e.g. Qwen3 32K with 4K response budget).", ) + parser.add_argument( + "--tool-call-failure-mode", + type=str, + default=DEFAULT_TOOL_CALL_FAILURE_MODE.value, + choices=[m.value for m in ToolCallFailureMode], + help="Recovery mode when a TOOL_RESULT step sees no tool_calls on the " + "assistant. 'rollback' (default, universal) pops the assistant and " + "re-inferences. 'append_tool' splices a sentinel tool message (only " + "works on lenient templates). 'append_user' splices a user message " + "with the same failure text — requires 'user' in allowed_append_roles.", + ) generate.add_arguments = _add_arguments diff --git a/miles/utils/test_utils/session_verify_runner.py b/miles/utils/test_utils/session_verify_runner.py index 2e7fdad01dc..f692381da5f 100644 --- a/miles/utils/test_utils/session_verify_runner.py +++ b/miles/utils/test_utils/session_verify_runner.py @@ -29,7 +29,7 @@ # Soft cap on how many samples may report any assistant_text mismatch. Hard # mismatch types (special_token_count / special_token_type / non_assistant_text) # are asserted per-sample inside the agent wrapper — those must be 0. -ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD = 0.05 +ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD = 0.2 PROMPT_DATA_PATH = "/root/datasets/session_multi_role_verify.jsonl" LOCAL_MODELS_ROOT = "/root/models" @@ -54,9 +54,17 @@ def _ensure_prompt_data() -> str: def _ensure_model_downloaded(hf_checkpoint: str) -> str: - """Download the model to ``/root/models/`` if not already there.""" + """Return a local model path, downloading HF repos when needed. + + Lets callers pass either a HuggingFace repo id (downloaded under + ``/root/models/``) or an existing local checkpoint path + (returned as-is, no download). + """ import miles.utils.external_utils.command_utils as U + if os.path.exists(hf_checkpoint): + return hf_checkpoint + short = hf_checkpoint.split("/")[-1] local_dir = os.path.join(LOCAL_MODELS_ROOT, short) os.makedirs(LOCAL_MODELS_ROOT, exist_ok=True) @@ -80,6 +88,8 @@ def build_train_args( tool_call_parser: str | None, n_samples_per_prompt: int = 4, cycles: int = 3, + tool_call_failure_mode: str = "rollback", + rollout_max_response_len: int = 8192, ) -> str: """Compose the ``train_args`` string for ``execute_train``. @@ -104,7 +114,7 @@ def build_train_args( "--num-rollout 1 " "--rollout-batch-size 16 " f"--n-samples-per-prompt {n_samples_per_prompt} " - "--rollout-max-response-len 4096 " + f"--rollout-max-response-len {rollout_max_response_len} " "--rollout-temperature 0.7 " "--global-batch-size 64 " ) @@ -115,6 +125,7 @@ def build_train_args( "--custom-agent-function-path " "miles.utils.test_utils.session_verify_agent.run_agent " f"--session-verify-cycles {cycles} " + f"--tool-call-failure-mode {tool_call_failure_mode} " ) router_args = ( @@ -157,6 +168,9 @@ def run_session_verify( num_gpus: int = 8, n_samples_per_prompt: int = 4, cycles: int = 3, + assistant_text_threshold: float = ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD, + tool_call_failure_mode: str = "rollback", + rollout_max_response_len: int = 8192, ) -> None: """Boot ``miles`` rollout pipeline and run the multi-role driver. @@ -191,6 +205,8 @@ def run_session_verify( tool_call_parser=tool_call_parser, n_samples_per_prompt=n_samples_per_prompt, cycles=cycles, + tool_call_failure_mode=tool_call_failure_mode, + rollout_max_response_len=rollout_max_response_len, ) # Per-sample token-seq metrics file: rollout workers append one JSONL line @@ -213,7 +229,7 @@ def run_session_verify( }, ) try: - _assert_assistant_text_mismatch_ratio(metrics_path) + _assert_session_verify_metrics(metrics_path, assistant_text_threshold=assistant_text_threshold) except AssertionError: import shutil @@ -228,15 +244,20 @@ def run_session_verify( pass -def _assert_assistant_text_mismatch_ratio(metrics_path: str) -> None: - """Read the per-sample JSONL metrics and assert the soft threshold. +def _assert_session_verify_metrics(metrics_path: str, *, assistant_text_threshold: float) -> None: + """Read per-sample JSONL metrics and assert cross-sample verifier gates. Forbidden mismatch types (special_*, non_assistant_text) are caught per-sample in the agent wrapper and would have already raised by now. - Here we only cross-check the soft assistant_text rate. + Here we only cross-check the soft assistant_text rate against the + caller-provided threshold (per-model: some upstream sglang reasoning + parsers — notably ``nemotron_3`` — leave a trailing ``\\n`` in + ``reasoning_content`` that breaks the canonical roundtrip until the + parser is patched, so those families ride at threshold=1.0). """ samples_with_mismatch = 0 total_samples = 0 + has_append_tool = False with open(metrics_path) as f: for line in f: line = line.strip() @@ -244,6 +265,7 @@ def _assert_assistant_text_mismatch_ratio(metrics_path: str) -> None: continue entry = json.loads(line) total_samples += 1 + has_append_tool = has_append_tool or "append_tool" in entry.get("driver_events", []) if entry.get("had_assistant_mismatch"): samples_with_mismatch += 1 @@ -254,18 +276,26 @@ def _assert_assistant_text_mismatch_ratio(metrics_path: str) -> None: "run before any sample completed. Check rollout logs." ) + if not has_append_tool: + raise AssertionError( + "Session multi-role e2e: no sample produced an append_tool action — " + "the model may not be tool-calling. Check sampling temperature, " + "the tool spec, or parser configuration." + ) + ratio = samples_with_mismatch / total_samples logger.info( - "Token-seq metric summary: samples=%d, with_assistant_text_mismatch=%d, ratio=%.3f", + "Token-seq metric summary: samples=%d, with_assistant_text_mismatch=%d, ratio=%.3f, threshold=%.3f", total_samples, samples_with_mismatch, ratio, + assistant_text_threshold, ) - if ratio > ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD: + if ratio > assistant_text_threshold: raise AssertionError( f"Session multi-role e2e: assistant_text mismatch ratio " f"{samples_with_mismatch}/{total_samples}={ratio:.3f} exceeds " - f"threshold {ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD}. TITO " + f"threshold {assistant_text_threshold}. TITO " "tokenization for assistant content has drifted from the chat " "template's canonical render — investigate via " "verify_session_tito_tokenizer.py + sample-level mismatch logs." diff --git a/scripts/tools/verify_session_tito_tokenizer.py b/scripts/tools/verify_session_tito_tokenizer.py index b8a329443a7..b8b6f31a315 100755 --- a/scripts/tools/verify_session_tito_tokenizer.py +++ b/scripts/tools/verify_session_tito_tokenizer.py @@ -34,7 +34,11 @@ import sys from miles.utils.chat_template_utils.tito_tokenizer import TITOTokenizerType -from miles.utils.test_utils.session_verify_agent import select_schedule +from miles.utils.test_utils.session_verify_agent import ( + DEFAULT_TOOL_CALL_FAILURE_MODE, + ToolCallFailureMode, + select_schedule, +) from miles.utils.test_utils.session_verify_runner import run_session_verify @@ -66,7 +70,7 @@ def main() -> int: parser.add_argument( "--hf-checkpoint", required=True, - help="HuggingFace model ID, e.g. zai-org/GLM-4.7-Flash.", + help="HuggingFace model ID or local checkpoint path, e.g. zai-org/GLM-4.7-Flash.", ) parser.add_argument( "--tito-model", @@ -130,6 +134,26 @@ def main() -> int: help="Driver schedule cycles per sample (default 3). Drop to 2 for " "tighter-context models (e.g. Qwen3 32K with 4K response budget).", ) + parser.add_argument( + "--assistant-text-threshold", + type=float, + default=0.1, + help="Soft threshold for assistant_text mismatch ratio. Default 0.1. " + "Raise to 1.0 for families whose upstream sglang reasoning parser " + "is known to roundtrip imperfectly (e.g. nemotron_3 keeps a trailing " + "newline in reasoning_content) — hard mismatches still gate.", + ) + parser.add_argument( + "--tool-call-failure-mode", + type=str, + default=DEFAULT_TOOL_CALL_FAILURE_MODE.value, + choices=[m.value for m in ToolCallFailureMode], + help="Recovery mode when a TOOL_RESULT step sees no tool_calls on the " + "assistant. 'rollback' (default, universal) pops the assistant and " + "re-inferences. 'append_tool' splices a sentinel tool message (only " + "lenient templates accept this). 'append_user' splices a user message " + "with the same failure text — requires 'user' in --tito-allowed-append-roles.", + ) args = parser.parse_args() @@ -151,6 +175,7 @@ def main() -> int: print(f"Actor GPUs per node: {args.num_gpus}") print(f"Samples per prompt: {args.n_samples}") print(f"Cycles per sample: {args.cycles}") + print(f"Tool-call failure mode:{args.tool_call_failure_mode}") print() try: @@ -172,6 +197,8 @@ def main() -> int: num_gpus=args.num_gpus, n_samples_per_prompt=args.n_samples, cycles=args.cycles, + assistant_text_threshold=args.assistant_text_threshold, + tool_call_failure_mode=args.tool_call_failure_mode, ) except Exception as e: print() diff --git a/tests/e2e/sglang/test_r3_router_equivalence.py b/tests/e2e/sglang/test_r3_router_equivalence.py index 16d7d846f21..15b9457010b 100644 --- a/tests/e2e/sglang/test_r3_router_equivalence.py +++ b/tests/e2e/sglang/test_r3_router_equivalence.py @@ -43,9 +43,9 @@ ``ROUTER_EQ_MODEL_FAMILY`` overrides this to run a single family, primarily for local debugging. - The CI suite reserves 8 GPUs to avoid sharing the runner with another GPU - job while this SGLang/Ray process tree is alive. The test workload itself - still uses a single engine (``--rollout-num-gpus-per-engine 1``) so both - variants hit the same underlying sglang process topology. + job while this SGLang/Ray process tree is alive. Each model config uses the + smallest tensor-parallel size that fits its serving weights on the CI GPU + fleet; both variants use the same topology for a given model. """ import base64 @@ -59,7 +59,10 @@ DUMP_ROOT = Path(os.environ.get("ROUTER_EQ_DUMP_ROOT", "/tmp/router-eq")) PROMPT_DATA_PATH = "/root/datasets/dapo-math-17k/dapo-math-17k.jsonl" -NUM_PROMPTS = int(os.environ.get("ROUTER_EQ_NUM_PROMPTS", "10")) +# Keep the default divisible by the 4-GPU CI topology: Megatron validates +# global_batch_size % (micro_batch_size * data_parallel_size) == 0 even in +# debug-rollout-only mode. +NUM_PROMPTS = int(os.environ.get("ROUTER_EQ_NUM_PROMPTS", "8")) MAX_RESPONSE_LEN = int(os.environ.get("ROUTER_EQ_MAX_RESPONSE_LEN", "256")) # Repo root (tests/e2e/sglang/test_*.py → parents[3]). Used to prepend the @@ -75,7 +78,8 @@ class ModelConfig: local_dir: str megatron_model_type: str reasoning_parser: str | None = None - num_gpus: int = 1 + num_gpus: int = 4 + mem_fraction_static: float = 0.7 MODEL_REGISTRY: dict[str, ModelConfig] = { @@ -85,7 +89,6 @@ class ModelConfig: local_dir="/root/models/Qwen3-30B-A3B", megatron_model_type="qwen3-30B-A3B", reasoning_parser=None, - num_gpus=1, ), "glm47_flash": ModelConfig( model_name="GLM-4.7-Flash", @@ -93,7 +96,6 @@ class ModelConfig: local_dir="/root/models/GLM-4.7-Flash", megatron_model_type="glm4.7-flash", reasoning_parser="glm45", - num_gpus=1, ), } @@ -170,7 +172,7 @@ def _build_train_args(cfg: ModelConfig, variant: str) -> str: sglang_args = ( f"--rollout-num-gpus-per-engine {cfg.num_gpus} " "--sglang-enable-deterministic-inference " - "--sglang-mem-fraction-static 0.85 " + f"--sglang-mem-fraction-static {cfg.mem_fraction_static} " ) if cfg.reasoning_parser: sglang_args += f"--sglang-reasoning-parser {cfg.reasoning_parser} " diff --git a/tests/e2e/sglang/test_session_server_multi_role.py b/tests/e2e/sglang/test_session_server_multi_role.py index 75d030b415d..023111b8609 100644 --- a/tests/e2e/sglang/test_session_server_multi_role.py +++ b/tests/e2e/sglang/test_session_server_multi_role.py @@ -7,15 +7,17 @@ from tests.ci.ci_register import register_cuda_ci -# Four model families run sequentially in one job, so est_time is roughly 4x -# of a single family. -register_cuda_ci(est_time=2400, suite="stage-b-sglang-8-gpu", num_gpus=8) +# Six model families run sequentially in one file, including large TP4 models. +# Keep est_time in sync with the full sweep so the CI runner does not kill an +# otherwise progressing session-server verification before it reaches the later +# families. +register_cuda_ci(est_time=9600, suite="stage-b-sglang-8-gpu", num_gpus=8) import os from dataclasses import dataclass -from miles.utils.test_utils.session_verify_runner import run_session_verify +from miles.utils.test_utils.session_verify_runner import ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD, run_session_verify @dataclass(frozen=True) @@ -27,6 +29,16 @@ class ModelConfig: allowed_append_roles: tuple[str, ...] tp_size: int = 1 cycles: int = 3 + # Soft-threshold override for assistant_text mismatch ratio. Default + # mirrors session_verify_runner; raise per-family when an upstream sglang + # reasoning parser is known to roundtrip imperfectly (e.g. nemotron_3 + # keeps trailing newline in reasoning_content) so the gate does not + # block on a documented out-of-scope issue. + assistant_text_threshold: float = ASSISTANT_TEXT_MISMATCH_RATIO_THRESHOLD + # Recovery mode when a TOOL_RESULT step finds the assistant emitted no + # tool_calls. Default "rollback" is universal (pop assistant + retry); + # see ToolCallFailureMode for "append_tool" / "append_user" variants. + tool_call_failure_mode: str = "rollback" MODEL_REGISTRY: dict[str, ModelConfig] = { @@ -37,6 +49,10 @@ class ModelConfig: tito_model="glm47", allowed_append_roles=("tool", "user", "system"), tp_size=4, + # Lenient template: tool message is rendered without validating that + # the preceding assistant carries a matching tool_call.id, so the + # APPEND_TOOL sentinel ("tool_call_id": "none") roundtrips cleanly. + tool_call_failure_mode="append_tool", ), "qwen3-tool-user": ModelConfig( model_name="Qwen/Qwen3-30B-A3B", @@ -46,6 +62,7 @@ class ModelConfig: allowed_append_roles=("tool", "user"), tp_size=2, cycles=2, + tool_call_failure_mode="append_tool", ), "qwen35-tool-user": ModelConfig( model_name="Qwen/Qwen3.5-35B-A3B", @@ -55,6 +72,7 @@ class ModelConfig: allowed_append_roles=("tool", "user"), tp_size=2, cycles=2, + tool_call_failure_mode="append_tool", ), "qwennext-tool-user": ModelConfig( model_name="Qwen/Qwen3-Next-80B-A3B-Thinking", @@ -64,6 +82,67 @@ class ModelConfig: allowed_append_roles=("tool", "user"), tp_size=4, cycles=2, + tool_call_failure_mode="append_tool", + ), + # MiniMax-M2.5 e2e lane is intentionally omitted: M2.5 and M2.7 share + # tokenizer.json (sha256-identical), arch (MiniMaxM2ForCausalLM), and the + # same sglang reasoning_parser / tool_call_parser bindings, so the M2.7 + # session-server lane exercises the same TITO code paths. Stage-2 CPU + # coverage for M2.5 stays in tests/fast/utils/chat_template_utils/. + "minimax-m27-tool-user": ModelConfig( + # MiniMax-M2.7 (MiniMaxM2ForCausalLM arch, 62 layers, 8 KV heads, + # ~215GB fp8). CI runs this lane on 80GB GPUs; tp=2 OOMs while SGLang + # allocates fp8 MoE weights, so use tp=4. cycles=2 to keep wall-time + # bounded given the 192K context budget. + # + # Surface is {tool, user}: M2.7's chat template gates reasoning_content + # on last_user_index, so a scheduled USER_FOLLOWUP step strips prior + # blocks — that's a documented template behavior; the fixed + # jinja (clear_thinking=False) preserves history reasoning across user + # turns to keep append-only. + # + # tool_call_failure_mode="append_user": M2.7's strict template hard- + # asserts that any ``tool`` role MUST follow an assistant with + # non-empty ``tool_calls``, so APPEND_TOOL would be server-rejected. + # Splicing a user-role parse-failure message gives the model a clean + # retry hint without breaking the tool-call invariant. + # + # ``reasoning_parser="minimax-append-think"`` matches the binding on + # ``MinimaxM27TITOTokenizer``; ``resolve_reasoning_and_tool_call_parser`` + # hard-asserts equality with the class-bound value. + model_name="MiniMaxAI/MiniMax-M2.7", + reasoning_parser="minimax-append-think", + tool_call_parser="minimax-m2", + tito_model="minimax_m27", + allowed_append_roles=("tool", "user"), + tp_size=4, + cycles=2, + assistant_text_threshold=0.1, + tool_call_failure_mode="append_user", + ), + "nemotron3-tool-user": ModelConfig( + # Nemotron-3-Super-120B-A12B-FP8 (~120GB fp8, A12B activated). + # num_attention_heads=32, num_key_value_heads=2. SGLang replicates + # KV heads when tp_size > num_key_value_heads, requiring tp_size to be + # divisible by num_key_value_heads; tp=4 satisfies that and avoids + # 80GB-runner OOM while creating MoE expert weights. FP8 also loads + # ~2x faster than the BF16 variant, cutting Stage 3 wall-time. + # Tool calls use the same XML + # wrapping as Qwen3.5, so qwen3_coder is the right tool_call_parser. The + # nemotron_3 reasoning parser is documented (in Nemotron3TITOTokenizer) + # to leave a trailing newline in reasoning_content — assistant_text + # roundtrip mismatches on every plain-text turn until upstream sglang + # is patched, so the soft threshold is relaxed to 1.0 for this row; + # hard mismatches (special tokens / non-assistant text) still gate. + model_name="nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8", + reasoning_parser="nemotron_3", + tool_call_parser="qwen3_coder", + tito_model="nemotron3", + allowed_append_roles=("tool", "user"), + tp_size=4, + cycles=2, + assistant_text_threshold=1.0, + tool_call_failure_mode="append_tool", ), } @@ -97,6 +176,8 @@ def _run_one(model_family: str): tool_call_parser=cfg.tool_call_parser, tp_size=cfg.tp_size, cycles=cfg.cycles, + assistant_text_threshold=cfg.assistant_text_threshold, + tool_call_failure_mode=cfg.tool_call_failure_mode, ) diff --git a/tests/fast/rollout/generate_hub/test_tool_call_utils.py b/tests/fast/rollout/generate_hub/test_tool_call_utils.py index 8c3ce6d12d9..94f0b40478f 100644 --- a/tests/fast/rollout/generate_hub/test_tool_call_utils.py +++ b/tests/fast/rollout/generate_hub/test_tool_call_utils.py @@ -19,8 +19,8 @@ "Qwen/Qwen3-Coder-Next", # "meta-llama/Llama-3.2-1B-Instruct", # Skipped: gated repo, requires HF_TOKEN in CI "mistralai/Mistral-7B-Instruct-v0.3", - "MiniMaxAI/MiniMax-M2", "MiniMaxAI/MiniMax-M2.5", + "MiniMaxAI/MiniMax-M2.7", "internlm/internlm3-8b-instruct", "zai-org/GLM-4.7-Flash", # "stepfun-ai/Step-3.5-Flash", # Disabled: config num_hidden_layers/layer_types mismatch breaks huggingface_hub strict validation diff --git a/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py b/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py index 91df34aeb81..41ea1bee1c2 100644 --- a/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py +++ b/tests/fast/utils/chat_template_utils/test_pretokenized_chat.py @@ -41,6 +41,13 @@ def _load_fixed(tito_model: TITOTokenizerType) -> str: return f.read() +def _load_fixed_for_roles(tito_model: TITOTokenizerType, roles: list[str]) -> str: + path, _kwargs = resolve_fixed_chat_template(tito_model, roles) + assert path is not None, f"resolve_fixed_chat_template should resolve {tito_model.value} for roles={roles}" + with open(path) as f: + return f.read() + + # --------------------------------------------------------------------------- # Per-template capability declarations # --------------------------------------------------------------------------- @@ -112,6 +119,70 @@ def _load_fixed(tito_model: TITOTokenizerType) -> str: frozenset({"tool", "user", "system"}), {"clear_thinking": False}, ), + # Kimi K2: K2.5 needs patched jinja gating the "drop reasoning of prior + # assistants once a non-tool-call assistant arrives" loop on + # preserve_thinking=True; K2.6 already exposes that gate natively. + # Only {tool, user} surface registered per current onboarding. + ( + "kimi_k25_fixed_preserve_thinking", + _load_fixed(TITOTokenizerType.KIMI25), + True, + frozenset({"tool", "user"}), + {"preserve_thinking": True}, + ), + ( + "kimi_k26_preserve_thinking", + load_hf_chat_template("moonshotai/Kimi-K2.6"), + True, + frozenset({"tool", "user"}), + {"preserve_thinking": True}, + ), + # MiniMax-M2 family (M2.5, M2.7): thinking via reasoning_content; reasoning + # is rendered only for assistant turns after the last user (last_user_index + # gate), so appending a new user would strip prior blocks. {tool} + # uses HF-native template; {tool, user} requires the fixed jinja with + # clear_thinking=False to preserve history reasoning across user turns. + ("minimax_m25", load_hf_chat_template("MiniMaxAI/MiniMax-M2.5"), True, frozenset({"tool"}), {}), + ( + "minimax_m25_fixed_clear_thinking_off", + _load_fixed_for_roles(TITOTokenizerType.MINIMAX_M25, ["tool", "user"]), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + ("minimax_m27", load_hf_chat_template("MiniMaxAI/MiniMax-M2.7"), True, frozenset({"tool"}), {}), + ( + "minimax_m27_fixed_clear_thinking_off", + _load_fixed_for_roles(TITOTokenizerType.MINIMAX_M27, ["tool", "user"]), + True, + frozenset({"tool", "user"}), + {"clear_thinking": False}, + ), + # Nemotron 3 Super: HF-native template is append-only. No fixed jinja is + # shipped (SUPPORTED_TEMPLATES.template=None on all surfaces); multi-user + # surfaces require truncate_history_thinking=False to preserve reasoning + # across user turns, while {tool}-only does not. + ( + "nemotron3", + load_hf_chat_template("nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16"), + True, + frozenset({"tool"}), + {}, + ), + ( + "nemotron3_truncate_history_thinking_off", + load_hf_chat_template("nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16"), + True, + frozenset({"tool", "user"}), + {"truncate_history_thinking": False}, + ), + ( + "nemotron3_truncate_history_thinking_off_with_system", + load_hf_chat_template("nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16"), + True, + frozenset({"tool", "user", "system"}), + {"truncate_history_thinking": False}, + ), # other HF native non-thinking: tool only ("qwen3_instruct_2507", load_hf_chat_template("Qwen/Qwen3-4B-Instruct-2507"), False, frozenset({"tool"}), {}), ("qwen3_next_instruct", load_hf_chat_template("Qwen/Qwen3-Next-80B-A3B-Instruct"), False, frozenset({"tool"}), {}), @@ -211,6 +282,13 @@ def _unique_thinking_templates(): for name, content, supports_thinking, _, _ in _TEMPLATES: if not supports_thinking: continue + # Kimi K2.5/K2.6 compress reasoning at the "first non-tool-call assistant" + # boundary (single_tool_thinking trajectories), not at the "last user + # message" boundary like qwen3/glm — so MultiUserTurnThinking's + # cross-user pretokenize does not trip their compression. This negative + # test only applies to templates whose compression keys off user index. + if name.startswith("kimi_"): + continue if content in seen: continue seen.add(content) diff --git a/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py b/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py index 67b12c1ad62..745c2d5e5f1 100644 --- a/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py +++ b/tests/fast/utils/chat_template_utils/test_tito_tokenizer.py @@ -516,13 +516,16 @@ class TestParserBinding: (TITOTokenizerType.QWEN35, "qwen3", "qwen3_coder"), (TITOTokenizerType.QWENNEXT, "qwen3", "qwen25"), (TITOTokenizerType.GLM47, "glm45", "glm47"), + (TITOTokenizerType.NEMOTRON3, "nemotron_3", "qwen3_coder"), + (TITOTokenizerType.KIMI25, None, None), + (TITOTokenizerType.KIMI26, "kimi_k2", "kimi_k2_raw_id"), + (TITOTokenizerType.MINIMAX_M25, "minimax-append-think", "minimax-m2"), + (TITOTokenizerType.MINIMAX_M27, "minimax-append-think", "minimax-m2"), (TITOTokenizerType.DEFAULT, None, None), ], ) def test_subclass_binding(self, tito_model, expected_reasoning, expected_tool_call): - from miles.utils.chat_template_utils.tito_tokenizer import _TOKENIZER_REGISTRY - - cls = _TOKENIZER_REGISTRY[tito_model] + cls = TITOTokenizerType.get_tokenizer_class(tito_model) assert cls.reasoning_parser == expected_reasoning assert cls.tool_call_parser == expected_tool_call diff --git a/tests/fast/utils/test_utils/test_session_verify_runner.py b/tests/fast/utils/test_utils/test_session_verify_runner.py new file mode 100644 index 00000000000..774bf7dd23d --- /dev/null +++ b/tests/fast/utils/test_utils/test_session_verify_runner.py @@ -0,0 +1,59 @@ +import json + +from tests.ci.ci_register import register_cpu_ci + +register_cpu_ci(est_time=60, suite="stage-a-fast") + +import pytest + +from miles.utils.test_utils.session_verify_runner import _assert_session_verify_metrics, build_train_args + + +def _build_args(**overrides) -> str: + kwargs = { + "local_model_dir": "/root/models/test-model", + "tito_model": "qwen3", + "allowed_append_roles": ["tool", "user"], + "tp_size": 2, + "reasoning_parser": "qwen3", + "tool_call_parser": "qwen25", + } + kwargs.update(overrides) + return build_train_args(**kwargs) + + +def test_build_train_args_uses_default_rollout_max_response_len(): + train_args = _build_args() + + assert "--rollout-max-response-len 8192" in train_args + + +def test_build_train_args_allows_model_specific_rollout_max_response_len(): + train_args = _build_args(rollout_max_response_len=16384) + + assert "--rollout-max-response-len 16384" in train_args + + +def _write_metrics(path, entries: list[dict]) -> None: + path.write_text("\n".join(json.dumps(entry) for entry in entries) + "\n") + + +def test_session_verify_metrics_accepts_cross_sample_append_tool(tmp_path): + metrics_path = tmp_path / "metrics.jsonl" + _write_metrics( + metrics_path, + [ + {"driver_events": ["initial", "append_user"], "had_assistant_mismatch": False}, + {"driver_events": ["initial", "append_tool"], "had_assistant_mismatch": False}, + ], + ) + + _assert_session_verify_metrics(str(metrics_path), assistant_text_threshold=0.1) + + +def test_session_verify_metrics_requires_at_least_one_append_tool(tmp_path): + metrics_path = tmp_path / "metrics.jsonl" + _write_metrics(metrics_path, [{"driver_events": ["initial", "append_user"], "had_assistant_mismatch": False}]) + + with pytest.raises(AssertionError, match="no sample produced an append_tool action"): + _assert_session_verify_metrics(str(metrics_path), assistant_text_threshold=0.1) From b8649e6cd2a5772ec7461baa22d6cdb42b9f5564 Mon Sep 17 00:00:00 2001 From: maocheng23 <35615230+maocheng23@users.noreply.github.com> Date: Sun, 17 May 2026 23:21:45 -0700 Subject: [PATCH 0056/1130] [feat] Init true on policy with qwen_dense Squash merge of the dense true-on-policy Miles branch. Co-authored-by: zju-stu-lizheng Co-authored-by: zyxiyy02 <282300612+zyxiyy02@users.noreply.github.com> Co-authored-by: Yi Zhang <1109276519@qq.com> Co-authored-by: Claude Opus 4.7 (1M context) --- .../megatron_utils/megatron_to_hf/qwen2.py | 4 + miles/backends/megatron_utils/model.py | 6 +- .../backends/megatron_utils/model_provider.py | 5 + miles/backends/megatron_utils/parallel.py | 2 + miles/backends/sglang_utils/arguments.py | 9 + miles/backends/training_utils/cp_utils.py | 135 ++++++--- miles/backends/training_utils/data.py | 12 +- miles/backends/training_utils/debug_dump.py | 72 +++++ miles/backends/training_utils/loss.py | 101 ++++++- miles/backends/training_utils/parallel.py | 8 + .../generate_utils/prefill_logprobs.py | 176 +++++++++++ .../inference_rollout_train.py | 8 + miles/rollout/sglang_rollout.py | 8 + miles/true_on_policy/__init__.py | 27 ++ miles/true_on_policy/config.py | 279 ++++++++++++++++++ miles/true_on_policy/contracts.py | 85 ++++++ miles/true_on_policy/model_profiles.py | 97 ++++++ miles/true_on_policy/schema.py | 34 +++ miles/utils/arguments.py | 28 +- miles/utils/dumper_utils.py | 51 +++- miles/utils/ppo_utils.py | 194 +++++++++++- scripts/run_qwen3_4b.py | 139 +++++---- .../test_model_provider_true_on_policy.py | 82 +++++ .../test_qwen2_true_on_policy_conversion.py | 30 ++ .../test_true_on_policy_data_log_dtype.py | 62 ++++ .../test_true_on_policy_loss_metrics.py | 231 +++++++++++++++ .../training_utils/test_ulysses_cp_utils.py | 70 +++++ .../generate_utils/test_prefill_logprobs.py | 169 +++++++++++ tests/fast/true_on_policy/__init__.py | 1 + tests/fast/true_on_policy/test_config.py | 243 +++++++++++++++ .../fast/true_on_policy/test_run_qwen3_4b.py | 131 ++++++++ tests/fast/utils/test_arguments.py | 18 ++ tests/fast/utils/test_dumper_utils.py | 27 ++ .../utils/test_true_on_policy_logprobs.py | 86 ++++++ 34 files changed, 2489 insertions(+), 141 deletions(-) create mode 100644 miles/backends/training_utils/debug_dump.py create mode 100644 miles/rollout/generate_utils/prefill_logprobs.py create mode 100644 miles/true_on_policy/__init__.py create mode 100644 miles/true_on_policy/config.py create mode 100644 miles/true_on_policy/contracts.py create mode 100644 miles/true_on_policy/model_profiles.py create mode 100644 miles/true_on_policy/schema.py create mode 100644 tests/fast/backends/megatron_utils/test_model_provider_true_on_policy.py create mode 100644 tests/fast/backends/megatron_utils/test_qwen2_true_on_policy_conversion.py create mode 100644 tests/fast/backends/training_utils/test_true_on_policy_data_log_dtype.py create mode 100644 tests/fast/backends/training_utils/test_true_on_policy_loss_metrics.py create mode 100644 tests/fast/backends/training_utils/test_ulysses_cp_utils.py create mode 100644 tests/fast/rollout/generate_utils/test_prefill_logprobs.py create mode 100644 tests/fast/true_on_policy/__init__.py create mode 100644 tests/fast/true_on_policy/test_config.py create mode 100644 tests/fast/true_on_policy/test_run_qwen3_4b.py create mode 100644 tests/fast/utils/test_true_on_policy_logprobs.py diff --git a/miles/backends/megatron_utils/megatron_to_hf/qwen2.py b/miles/backends/megatron_utils/megatron_to_hf/qwen2.py index f7b72935cf5..29e879d1916 100644 --- a/miles/backends/megatron_utils/megatron_to_hf/qwen2.py +++ b/miles/backends/megatron_utils/megatron_to_hf/qwen2.py @@ -59,8 +59,12 @@ def convert_qwen2_to_hf(args, name, param): return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)] elif rest == "self_attention.linear_qkv.layer_norm_weight": return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)] + elif 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 == "pre_mlp_layernorm.weight": + return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)] # qk norm elif rest == "self_attention.q_layernorm.weight": diff --git a/miles/backends/megatron_utils/model.py b/miles/backends/megatron_utils/model.py index 5d2503dab17..f3a1da27c4f 100644 --- a/miles/backends/megatron_utils/model.py +++ b/miles/backends/megatron_utils/model.py @@ -482,6 +482,10 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p check_mtp_only_grad(model, step_id) + # Dump backward tensors while gradients are still attached. The optimizer + # step and subsequent zero_grad release them. + dumper_phase_util.finalize(model) + if not disable_optimizer and valid_step: # Update parameters. update_successful, grad_norm, num_zeros_in_grad = optimizer.step() @@ -496,8 +500,6 @@ def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_p if not disable_optimizer: optimizer.zero_grad() - dumper_phase_util.finalize(model) - if mpu.is_pipeline_last_stage(ignore_virtual=True): loss_reduced = aggregate_train_losses(losses_reduced) return loss_reduced, grad_norm diff --git a/miles/backends/megatron_utils/model_provider.py b/miles/backends/megatron_utils/model_provider.py index 45adc86792f..e7c24a31913 100644 --- a/miles/backends/megatron_utils/model_provider.py +++ b/miles/backends/megatron_utils/model_provider.py @@ -180,6 +180,11 @@ def model_provider( qk_layernorm=args.qk_layernorm, multi_latent_attention=args.multi_latent_attention, moe_use_legacy_grouped_gemm=args.moe_use_legacy_grouped_gemm, + normalization=args.normalization, + use_kitchen=config.use_kitchen, + use_true_on_policy_backend=config.true_on_policy_contract is not None, + use_kitchen_attention=config.use_kitchen_attention, + kitchen_attention_backend=config.kitchen_attention_backend, ) build_model_context = nullcontext diff --git a/miles/backends/megatron_utils/parallel.py b/miles/backends/megatron_utils/parallel.py index f4c9c6cad6e..38b52ed5a26 100644 --- a/miles/backends/megatron_utils/parallel.py +++ b/miles/backends/megatron_utils/parallel.py @@ -15,6 +15,7 @@ def create_megatron_parallel_state() -> ParallelState: vpp_size, microbatch_group_size_per_vp_stage = _compute_vpp_fields() + args = get_args() def _create_intra_dp(with_context_parallel: bool): return GroupInfo( @@ -32,6 +33,7 @@ def _create_intra_dp(with_context_parallel: bool): size=mpu.get_context_parallel_world_size(), group=mpu.get_context_parallel_group(), ), + cp_comm_type=getattr(args, "cp_comm_type", None), tp=GroupInfo( rank=mpu.get_tensor_model_parallel_rank(), size=mpu.get_tensor_model_parallel_world_size(), diff --git a/miles/backends/sglang_utils/arguments.py b/miles/backends/sglang_utils/arguments.py index f4b9978a403..40853c8d39b 100644 --- a/miles/backends/sglang_utils/arguments.py +++ b/miles/backends/sglang_utils/arguments.py @@ -137,6 +137,15 @@ def validate_args(args): args.sglang_dp_size = args.sglang_data_parallel_size args.sglang_pp_size = args.sglang_pipeline_parallel_size args.sglang_ep_size = args.sglang_expert_parallel_size + if hasattr(args, "sglang_attention_context_parallel_size"): + args.sglang_attn_cp_size = args.sglang_attention_context_parallel_size + + if args.true_on_policy_mode: + args.sglang_enable_deterministic_inference = True + + if getattr(args, "recompute_logprobs_via_prefill", False): + args.sglang_enable_prefill_only_deterministic_inference = True + args.sglang_enable_deterministic_inference = True if args.sglang_dp_size > 1: assert args.sglang_enable_dp_attention diff --git a/miles/backends/training_utils/cp_utils.py b/miles/backends/training_utils/cp_utils.py index cb3188e01eb..4877a1bb565 100644 --- a/miles/backends/training_utils/cp_utils.py +++ b/miles/backends/training_utils/cp_utils.py @@ -61,6 +61,23 @@ def get_logits_and_tokens_offset_with_cp( return chunk_size, (chunk_0, chunk_1), (logits_0, logits_1), (token_0, token_1) +def _slice_loss_mask_for_local_cp( + total_length: int, + response_length: int, + loss_mask: torch.Tensor, + qkv_format: str, + max_seq_len: int | None, +) -> torch.Tensor: + """Slice a per-sample response loss mask into this CP rank's local zigzag layout.""" + prompt_length = total_length - response_length + _, _, _, tokens_offset = get_logits_and_tokens_offset_with_cp( + total_length, response_length, qkv_format, max_seq_len + ) + mask_0 = loss_mask[tokens_offset[0][0] - prompt_length : tokens_offset[0][1] - prompt_length] + mask_1 = loss_mask[tokens_offset[1][0] - prompt_length : tokens_offset[1][1] - prompt_length] + return torch.cat([mask_0, mask_1], dim=0) + + def slice_loss_masks_for_local_cp( loss_masks: list[torch.Tensor], total_lengths: list[int], @@ -68,29 +85,8 @@ def slice_loss_masks_for_local_cp( qkv_format: str = "thd", max_seq_lens: list[int] | None = None, ) -> list[torch.Tensor]: - """Slice global loss masks to the local CP rank's token chunks. - - When CP == 1 returns the original masks unchanged. When CP > 1, each - mask is sliced according to the zigzag token offsets for this rank. - """ - parallel_state = get_parallel_state() - if parallel_state.cp.size == 1: - return loss_masks - - local_masks = [] - for i, (loss_mask, total_length, response_length) in enumerate( - zip(loss_masks, total_lengths, response_lengths, strict=False) - ): - max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None - prompt_length = total_length - response_length - _, _, _, token_offsets = get_logits_and_tokens_offset_with_cp( - total_length, response_length, qkv_format, max_seq_len - ) - mask_0 = loss_mask[token_offsets[0][0] - prompt_length : token_offsets[0][1] - prompt_length] - mask_1 = loss_mask[token_offsets[1][0] - prompt_length : token_offsets[1][1] - prompt_length] - local_masks.append(torch.cat([mask_0, mask_1], dim=0)) - - return local_masks + """Backward-compatible wrapper for local CP response mask slicing.""" + return get_local_response_loss_masks(total_lengths, response_lengths, loss_masks, qkv_format, max_seq_lens) def get_sum_of_sample_mean( @@ -107,35 +103,82 @@ def get_sum_of_sample_mean( parallel_state = get_parallel_state() cp_size = parallel_state.cp.size if cp_size == 1: - chunk_lengths = response_lengths - chunk_masks = loss_masks + + def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor: + 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=True) + ] + ) + + 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=True) + ] + ) + else: - chunk_masks = slice_loss_masks_for_local_cp( - loss_masks, total_lengths, response_lengths, qkv_format, max_seq_lens - ) - chunk_lengths = [m.size(0) for m in chunk_masks] - - def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor: - return sum( - [ - (x_i * chunked_loss_mask).sum() / torch.clamp_min(loss_mask.sum(), 1) - for x_i, chunked_loss_mask, loss_mask in zip( - x.split(chunk_lengths, dim=0), chunk_masks, loss_masks, strict=False - ) - ] - ) + cp_chunk_lengths = [] + chunked_loss_masks = [] + for i, (total_length, response_length, loss_mask) in enumerate( + zip(total_lengths, response_lengths, loss_masks, strict=True) + ): + max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None + chunked_loss_masks.append( + _slice_loss_mask_for_local_cp(total_length, response_length, loss_mask, qkv_format, max_seq_len) + ) + cp_chunk_lengths.append(chunked_loss_masks[i].size(0)) + + def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor: + return sum( + [ + (x_i * chunked_loss_mask).sum() / torch.clamp_min(loss_mask.sum(), 1) + for x_i, chunked_loss_mask, loss_mask in zip( + x.split(cp_chunk_lengths, dim=0), chunked_loss_masks, loss_masks, strict=True + ) + ] + ) - def sum_of_token(x: torch.Tensor) -> torch.Tensor: - return sum( - [ - (x_i * chunked_loss_mask).sum() - for x_i, chunked_loss_mask in zip(x.split(chunk_lengths, dim=0), chunk_masks, strict=False) - ] - ) + def sum_of_token(x: torch.Tensor) -> torch.Tensor: + return sum( + [ + (x_i * chunked_loss_mask).sum() + for x_i, chunked_loss_mask in zip( + x.split(cp_chunk_lengths, dim=0), chunked_loss_masks, strict=True + ) + ] + ) return sum_of_sample_mean if not calculate_per_token_loss else sum_of_token +def get_local_response_loss_masks( + total_lengths: list[int], + response_lengths: list[int], + loss_masks: list[torch.Tensor], + qkv_format: str = "thd", + max_seq_lens: list[int] | None = None, +) -> list[torch.Tensor]: + """Return response loss masks aligned with this rank's local log-probs.""" + parallel_state = get_parallel_state() + if parallel_state.cp.size == 1: + return loss_masks + + local_masks = [] + for i, (total_length, response_length, loss_mask) in enumerate( + zip(total_lengths, response_lengths, loss_masks, strict=True) + ): + max_seq_len = max_seq_lens[i] if max_seq_lens is not None else None + local_masks.append( + _slice_loss_mask_for_local_cp(total_length, response_length, loss_mask, qkv_format, max_seq_len) + ) + + return local_masks + + def all_gather_with_cp( tensor: torch.Tensor, total_length: int, diff --git a/miles/backends/training_utils/data.py b/miles/backends/training_utils/data.py index 3bda887b26a..55a17e27c4f 100644 --- a/miles/backends/training_utils/data.py +++ b/miles/backends/training_utils/data.py @@ -18,6 +18,15 @@ logger = logging.getLogger(__name__) +def _rollout_logprob_dtype(args: Namespace) -> torch.dtype: + if getattr(args, "true_on_policy_mode", False): + if getattr(args, "bf16", False): + return torch.bfloat16 + if getattr(args, "fp16", False): + return torch.float16 + return torch.float32 + + def get_rollout_data(args: Namespace, rollout_data_ref: Box) -> RolloutBatch: parallel_state = get_parallel_state() # Fetch data through ray on CPU, not sure if this will be performance bottleneck. @@ -57,6 +66,7 @@ def get_rollout_data(args: Namespace, rollout_data_ref: Box) -> RolloutBatch: rollout_data["max_seq_lens"] = [max_seq_len] * len(rollout_data["tokens"]) if "rollout_log_probs" in rollout_data: + rollout_logprob_dtype = _rollout_logprob_dtype(args) rollout_data["rollout_log_probs"] = [ torch.tensor( slice_log_prob_with_cp( @@ -67,7 +77,7 @@ def get_rollout_data(args: Namespace, rollout_data_ref: Box) -> RolloutBatch: rollout_data["max_seq_lens"][i] if args.qkv_format == "bshd" else None, ), device=torch.cuda.current_device(), - dtype=torch.float32, + dtype=rollout_logprob_dtype, ) for i, (log_prob, total_length, response_length) in enumerate( zip( diff --git a/miles/backends/training_utils/debug_dump.py b/miles/backends/training_utils/debug_dump.py new file mode 100644 index 00000000000..15e6e835799 --- /dev/null +++ b/miles/backends/training_utils/debug_dump.py @@ -0,0 +1,72 @@ +from argparse import Namespace +from pathlib import Path + +import torch +import torch.distributed as dist + +from miles.utils.types import RolloutBatch + +_POLICY_LOSS_DUMP_COUNTER = 0 + + +def maybe_dump_policy_loss_debug( + *, + args: Namespace, + batch: RolloutBatch, + train_log_probs: list[torch.Tensor], + old_log_probs: list[torch.Tensor], + rollout_log_probs: list[torch.Tensor] | None, + advantages: list[torch.Tensor], + local_loss_masks: list[torch.Tensor], + ppo_kl: torch.Tensor, + pg_loss: torch.Tensor, +) -> None: + dump_dir = getattr(args, "dump_details", None) + if dump_dir is None: + return + + global _POLICY_LOSS_DUMP_COUNTER + counter = _POLICY_LOSS_DUMP_COUNTER + _POLICY_LOSS_DUMP_COUNTER += 1 + + rank = dist.get_rank() if dist.is_available() and dist.is_initialized() else 0 + path = Path(dump_dir) / "policy_loss_debug" / f"rank_{rank}_call_{counter}.pt" + path.parent.mkdir(parents=True, exist_ok=True) + + def to_cpu(tensor: torch.Tensor) -> torch.Tensor: + return tensor.detach().float().cpu() + + samples = [] + for index, train_lp in enumerate(train_log_probs): + sample = { + "index": index, + "total_length": batch["total_lengths"][index], + "response_length": batch["response_lengths"][index], + "train_log_probs": to_cpu(train_lp), + "old_log_probs": to_cpu(old_log_probs[index]), + "advantages": to_cpu(advantages[index]), + "local_loss_mask": to_cpu(local_loss_masks[index]), + } + if rollout_log_probs is not None: + sample["rollout_log_probs"] = to_cpu(rollout_log_probs[index]) + if train_lp.shape == rollout_log_probs[index].shape: + sample["train_rollout_abs_diff"] = to_cpu((train_lp - rollout_log_probs[index]).abs()) + samples.append(sample) + + torch.save( + { + "rank": rank, + "call": counter, + "samples": samples, + "ppo_kl": to_cpu(ppo_kl), + "pg_loss": to_cpu(pg_loss), + "finite": { + "ppo_kl": torch.isfinite(ppo_kl).all().item(), + "pg_loss": torch.isfinite(pg_loss).all().item(), + "train_log_probs": all(torch.isfinite(t).all().item() for t in train_log_probs), + "old_log_probs": all(torch.isfinite(t).all().item() for t in old_log_probs), + "advantages": all(torch.isfinite(t).all().item() for t in advantages), + }, + }, + path, + ) diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index 565e6b694fa..cbd36060ec5 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -24,6 +24,7 @@ from .cp_utils import ( _allgather_cp_redistribute, all_gather_with_cp, + get_local_response_loss_masks, get_logits_and_tokens_offset_with_cp, get_sum_of_sample_mean, slice_loss_masks_for_local_cp, @@ -50,7 +51,8 @@ def get_responses( Args: logits: Model outputs with shape `[1, T, V]` (policy) or `[1, T, 1]` - (value). Must be float32. + (value). Standard training uses float32. True-on-policy scoring + preserves the rollout engine's logits dtype. args: Configuration containing `rollout_temperature` for scaling. unconcat_tokens: List of token tensors (prompt+response) per sample. total_lengths: Total sequence lengths (prompt+response) per sample. @@ -64,7 +66,8 @@ def get_responses( parallel_state = get_parallel_state() qkv_format = args.qkv_format - assert logits.dtype == torch.float32, f"{logits.dtype}" + if not args.true_on_policy_mode: + assert logits.dtype == torch.float32, f"{logits.dtype}" assert len(logits.shape) == 3, f"{logits.shape}" if qkv_format == "thd": @@ -75,6 +78,11 @@ def get_responses( logits = logits.view(-1, logits.size(-1)) logits = logits.div(args.rollout_temperature) + if args.true_on_policy_mode: + if getattr(args, "bf16", False): + logits = logits.to(torch.bfloat16) + elif getattr(args, "fp16", False): + logits = logits.to(torch.float16) cp_size = parallel_state.cp.size end = 0 @@ -88,10 +96,11 @@ def get_responses( if qkv_format == "bshd": end = max_seq_len * i + total_length start = end - response_length + logits_chunk = logits[start - 1 : end - 1] else: end += total_length start = end - response_length - logits_chunk = logits[start - 1 : end - 1] + logits_chunk = logits[start - 1 : end - 1] tokens_chunk = tokens[-response_length:] elif args.allgather_cp: # DSA: global concat then contiguous CP split. Each rank owns logits for @@ -194,6 +203,7 @@ def get_log_probs_and_entropy( with_entropy=with_entropy, chunk_size=args.log_probs_chunk_size, true_on_policy=args.true_on_policy_mode, + vocab_size=getattr(args, "vocab_size", None), ) log_probs_list.append(log_prob.squeeze(-1)) @@ -596,11 +606,13 @@ def policy_loss_function( unconcat_tokens=batch["unconcat_tokens"], total_lengths=total_lengths, response_lengths=response_lengths, - with_entropy=True, + with_entropy=args.entropy_coef != 0, max_seq_lens=max_seq_lens, ) log_probs = log_probs_and_entropy["log_probs"] + train_log_probs_list = log_probs + old_log_probs_list = old_log_probs # Pre-gather log probs if needed by OPSM or GSPO to avoid duplicate gathering need_full_log_probs = args.use_opsm or args.advantage_estimator == "gspo" @@ -646,8 +658,43 @@ def policy_loss_function( log_probs = torch.cat(log_probs, dim=0) ppo_kl = old_log_probs - log_probs + local_loss_mask_list = get_local_response_loss_masks( + total_lengths, + response_lengths, + batch["loss_masks"], + args.qkv_format, + max_seq_lens, + ) + local_loss_masks = torch.cat(local_loss_mask_list, dim=0).to(device=ppo_kl.device) + active_tokens = local_loss_masks.bool() + ppo_kl = torch.where( + active_tokens, + torch.nan_to_num(ppo_kl, nan=0.0, posinf=0.0, neginf=0.0), + ppo_kl.new_zeros(()), + ) + advantages = torch.where( + active_tokens, + torch.nan_to_num(advantages, nan=0.0, posinf=0.0, neginf=0.0), + advantages.new_zeros(()), + ) + pg_loss, pg_clipfrac = compute_policy_loss(ppo_kl, advantages, args.eps_clip, args.eps_clip_high) + if getattr(args, "dump_details", None) is not None: + from .debug_dump import maybe_dump_policy_loss_debug + + maybe_dump_policy_loss_debug( + args=args, + batch=batch, + train_log_probs=train_log_probs_list, + old_log_probs=old_log_probs_list, + rollout_log_probs=batch.get("rollout_log_probs"), + advantages=batch["advantages"], + local_loss_masks=local_loss_mask_list, + ppo_kl=ppo_kl, + pg_loss=pg_loss, + ) + if args.use_opsm: pg_loss = pg_loss * opsm_mask @@ -724,11 +771,14 @@ def policy_loss_function( ppo_kl = sum_of_sample_mean(ppo_kl) # entropy loss - entropy = log_probs_and_entropy["entropy"] - entropy = torch.cat(entropy, dim=0) - entropy_loss = sum_of_sample_mean(entropy) - - loss = pg_loss - args.entropy_coef * entropy_loss + if args.entropy_coef != 0: + entropy = log_probs_and_entropy["entropy"] + entropy = torch.cat(entropy, dim=0) + entropy_loss = sum_of_sample_mean(entropy) + loss = pg_loss - args.entropy_coef * entropy_loss + else: + entropy_loss = pg_loss.new_zeros(()) + loss = pg_loss if args.use_kl_loss: ref_log_probs = batch["ref_log_probs"] @@ -742,23 +792,41 @@ def policy_loss_function( kl_loss_type=args.kl_loss_type, importance_ratio=importance_ratio, ) + kl = torch.where( + active_tokens, + torch.nan_to_num(kl, nan=0.0, posinf=0.0, neginf=0.0), + kl.new_zeros(()), + ) kl_loss = sum_of_sample_mean(kl) - loss = loss + args.kl_loss_coef * kl_loss + if args.kl_loss_coef != 0: + loss = loss + args.kl_loss_coef * kl_loss # make sure the gradient could backprop correctly. if log_probs.numel() == 0: loss += 0 * logits.sum() + train_scored_log_probs = log_probs train_rollout_logprob_abs_diff = None train_rollout_kl = None if "rollout_log_probs" in batch and batch["rollout_log_probs"]: rollout_log_probs = torch.cat(batch["rollout_log_probs"], dim=0) - train_rollout_logprob_abs_diff = sum_of_sample_mean((old_log_probs - rollout_log_probs).abs()) + abs_diff = (train_scored_log_probs - rollout_log_probs).abs() + abs_diff = torch.where( + active_tokens, + torch.nan_to_num(abs_diff, nan=0.0, posinf=0.0, neginf=0.0), + abs_diff.new_zeros(()), + ) + train_rollout_logprob_abs_diff = sum_of_sample_mean(abs_diff) + # KL(rollout || train) at sampled tokens via Schulman k3 with per-token clamp [-10, 10] - train_rollout_kl = sum_of_sample_mean( - compute_approx_kl(rollout_log_probs, old_log_probs, kl_loss_type="low_var_kl") + rollout_train_kl = compute_approx_kl(rollout_log_probs, train_scored_log_probs, kl_loss_type="low_var_kl") + rollout_train_kl = torch.where( + active_tokens, + torch.nan_to_num(rollout_train_kl, nan=0.0, posinf=0.0, neginf=0.0), + rollout_train_kl.new_zeros(()), ) + train_rollout_kl = sum_of_sample_mean(rollout_train_kl) reported_loss = { "loss": loss.clone().detach(), @@ -980,7 +1048,12 @@ def loss_function( global_batch_size = batch.get("dynamic_global_batch_size", args.global_batch_size) if not args.calculate_per_token_loss: if apply_megatron_loss_scaling: - loss = loss * num_microbatches / global_batch_size * parallel_state.intra_dp_cp.size + loss_parallel_size = ( + parallel_state.intra_dp.size + if args.true_on_policy_mode and parallel_state.is_ulysses_cp + else parallel_state.intra_dp_cp.size + ) + loss = loss * num_microbatches / global_batch_size * loss_parallel_size else: loss = loss / global_batch_size * parallel_state.intra_dp.size else: diff --git a/miles/backends/training_utils/parallel.py b/miles/backends/training_utils/parallel.py index 75032684b91..2d8d6e082c1 100644 --- a/miles/backends/training_utils/parallel.py +++ b/miles/backends/training_utils/parallel.py @@ -56,6 +56,14 @@ class ParallelState: pp: GroupInfo ep: GroupInfo etp: GroupInfo + cp_comm_type: str | list[str] | tuple[str, ...] | None = None is_pp_last_stage: bool = True vpp_size: int | None = 1 microbatch_group_size_per_vp_stage: int | None = None + + @property + def is_ulysses_cp(self) -> bool: + cp_comm_type = self.cp_comm_type + if isinstance(cp_comm_type, (list, tuple)): + cp_comm_type = cp_comm_type[0] if cp_comm_type else None + return self.cp.size > 1 and cp_comm_type == "a2a" diff --git a/miles/rollout/generate_utils/prefill_logprobs.py b/miles/rollout/generate_utils/prefill_logprobs.py new file mode 100644 index 00000000000..680cf353782 --- /dev/null +++ b/miles/rollout/generate_utils/prefill_logprobs.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Mapping +from typing import Any + +from miles.backends.megatron_utils.lora_utils import LORA_ADAPTER_NAME, is_lora_enabled +from miles.utils.http_utils import post +from miles.utils.processing_utils import encode_image_for_rollout_engine +from miles.utils.types import Sample + + +def _build_prefill_scoring_payload( + args: Any, + sample: Sample, + sampling_params: Mapping[str, Any], +) -> dict[str, Any]: + prompt_len = len(sample.tokens) - sample.response_length + if prompt_len <= 0: + raise ValueError( + "Cannot recompute rollout logprobs via prefill without at least one prompt token: " + f"tokens={len(sample.tokens)}, response_length={sample.response_length}" + ) + + payload = { + "input_ids": sample.tokens, + "sampling_params": { + **dict(sampling_params), + "max_new_tokens": 0, + "temperature": 0, + "skip_special_tokens": False, + }, + "return_logprob": True, + # SGLang returns input_token_logprobs aligned to tokens from logprob_start_len, + # with the first value None. Start one token before the response so the + # returned tail contains every response-token logprob. + "logprob_start_len": prompt_len - 1, + } + + if is_lora_enabled(args): + payload["lora_path"] = LORA_ADAPTER_NAME + + if sample.multimodal_inputs and sample.multimodal_inputs.get("images"): + image_data = sample.multimodal_inputs["images"] + payload["image_data"] = [encode_image_for_rollout_engine(image) for image in image_data] + + return payload + + +def _can_batch_prefill_score(args: Any, samples: list[Sample]) -> bool: + if getattr(args, "sglang_router_policy", None) == "consistent_hashing": + return False + return not any(sample.multimodal_inputs and sample.multimodal_inputs.get("images") for sample in samples) + + +def _build_batch_prefill_scoring_payload( + args: Any, + samples: list[Sample], + sampling_params: Mapping[str, Any], +) -> dict[str, Any]: + payloads = [_build_prefill_scoring_payload(args, sample, sampling_params) for sample in samples] + logprob_start_len = payloads[0]["logprob_start_len"] + if any(payload["logprob_start_len"] != logprob_start_len for payload in payloads): + raise ValueError("Batched SGLang prefill scoring requires a shared logprob_start_len") + + batch_payload: dict[str, Any] = { + "input_ids": [payload["input_ids"] for payload in payloads], + "sampling_params": payloads[0]["sampling_params"], + "return_logprob": True, + "logprob_start_len": logprob_start_len, + } + if "lora_path" in payloads[0]: + batch_payload["lora_path"] = payloads[0]["lora_path"] + return batch_payload + + +def _extract_response_logprobs(sample: Sample, meta_info: Mapping[str, Any]) -> list[float]: + input_token_logprobs = meta_info.get("input_token_logprobs") + if not input_token_logprobs: + raise ValueError("SGLang prefill scoring response did not include input_token_logprobs") + + response_items = input_token_logprobs[-sample.response_length :] + response_tokens = sample.tokens[-sample.response_length :] + scored_tokens = [item[1] for item in response_items] + if scored_tokens != response_tokens: + raise ValueError( + "SGLang prefill scoring token alignment mismatch: " + f"expected response tail {response_tokens[:8]}... len={len(response_tokens)}, " + f"got {scored_tokens[:8]}... len={len(scored_tokens)}" + ) + + response_logprobs = [item[0] for item in response_items] + if any(logprob is None for logprob in response_logprobs): + raise ValueError("SGLang prefill scoring returned None for a response-token logprob") + + return response_logprobs + + +async def recompute_rollout_logprobs_via_prefill( + args: Any, + sample: Sample, + *, + url: str, + sampling_params: Mapping[str, Any], + headers: Mapping[str, str] | None = None, +) -> None: + if not getattr(args, "recompute_logprobs_via_prefill", False): + return + if sample.response_length == 0: + sample.rollout_log_probs = [] + return + if sample.status == Sample.Status.ABORTED: + return + + payload = _build_prefill_scoring_payload(args, sample, sampling_params) + output = await post(url, payload, headers=headers) + sample.rollout_log_probs = _extract_response_logprobs(sample, output["meta_info"]) + sample.metadata["rollout_log_probs_source"] = "sglang_prefill_recompute" + + +async def recompute_samples_rollout_logprobs_via_prefill( + args: Any, + samples: list[Sample], + *, + url: str, + sampling_params: Mapping[str, Any], +) -> None: + if not getattr(args, "recompute_logprobs_via_prefill", False): + return + + samples_to_score = [ + sample for sample in samples if sample.response_length != 0 and sample.status != Sample.Status.ABORTED + ] + if not samples_to_score: + return + + flush_url = url.rsplit("/", 1)[0] + "/flush_cache" + + if _can_batch_prefill_score(args, samples_to_score): + samples_by_logprob_start_len: dict[int, list[Sample]] = defaultdict(list) + for sample in samples_to_score: + prompt_len = len(sample.tokens) - sample.response_length + samples_by_logprob_start_len[prompt_len - 1].append(sample) + + for batch_samples in samples_by_logprob_start_len.values(): + # SGLang can serve scoring requests from radix/KV cache. Flush before + # each scoring group so every group uses the same clean-prefill path. + await post(flush_url, {}) + payload = _build_batch_prefill_scoring_payload(args, batch_samples, sampling_params) + outputs = await post(url, payload) + if not isinstance(outputs, list): + raise ValueError(f"SGLang batch prefill scoring returned {type(outputs).__name__}, expected list") + if len(outputs) != len(batch_samples): + raise ValueError( + "SGLang batch prefill scoring output count mismatch: " + f"expected {len(batch_samples)}, got {len(outputs)}" + ) + for sample, output in zip(batch_samples, outputs, strict=True): + sample.rollout_log_probs = _extract_response_logprobs(sample, output["meta_info"]) + sample.metadata["rollout_log_probs_source"] = "sglang_prefill_recompute" + return + + for sample in samples_to_score: + headers = None + uses_consistent_hashing = getattr(args, "sglang_router_policy", None) == "consistent_hashing" + if uses_consistent_hashing and sample.session_id: + headers = {"X-SMG-Routing-Key": sample.session_id} + + await post(flush_url, {}, headers=headers) + await recompute_rollout_logprobs_via_prefill( + args, + sample, + url=url, + sampling_params=sampling_params, + headers=headers, + ) diff --git a/miles/rollout/inference_rollout/inference_rollout_train.py b/miles/rollout/inference_rollout/inference_rollout_train.py index fbf909b3742..eed15557bdf 100644 --- a/miles/rollout/inference_rollout/inference_rollout_train.py +++ b/miles/rollout/inference_rollout/inference_rollout_train.py @@ -9,6 +9,7 @@ from miles.rollout.base_types import RolloutFnTrainOutput from miles.rollout.filter_hub.base_types import MetricGatherer, call_dynamic_filter +from miles.rollout.generate_utils.prefill_logprobs import recompute_samples_rollout_logprobs_via_prefill from miles.rollout.inference_rollout.inference_rollout_common import GenerateState, generate_and_rm_group from miles.utils import dumper_utils from miles.utils.http_utils import get, post @@ -152,4 +153,11 @@ async def generate_rollout_async( if f := load_function(args.rollout_all_samples_process_path): f(args, all_samples, data_source) + await recompute_samples_rollout_logprobs_via_prefill( + args, + [sample for group in data for sample in group], + url=f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate", + sampling_params=state.sampling_params, + ) + return RolloutFnTrainOutput(samples=data, metrics=metric_gatherer.collect()), aborted_samples diff --git a/miles/rollout/sglang_rollout.py b/miles/rollout/sglang_rollout.py index 461ebdf3148..c0016650879 100644 --- a/miles/rollout/sglang_rollout.py +++ b/miles/rollout/sglang_rollout.py @@ -31,6 +31,7 @@ ) from miles.utils.types import Sample +from .generate_utils.prefill_logprobs import recompute_samples_rollout_logprobs_via_prefill from .rm_hub import async_rm, batched_async_rm __all__ = ["generate_rollout", "get_model_url"] @@ -467,6 +468,13 @@ async def generate_rollout_async( process_func = load_function(args.rollout_all_samples_process_path) process_func(args, all_samples, data_source) + await recompute_samples_rollout_logprobs_via_prefill( + args, + [sample for group in data for sample in group], + url=get_model_url(args, "default"), + sampling_params=state.sampling_params, + ) + return RolloutFnTrainOutput(samples=data, metrics=metric_gatherer.collect()), aborted_samples diff --git a/miles/true_on_policy/__init__.py b/miles/true_on_policy/__init__.py new file mode 100644 index 00000000000..1167583afc6 --- /dev/null +++ b/miles/true_on_policy/__init__.py @@ -0,0 +1,27 @@ +"""True-on-policy launch contract helpers.""" + +from .config import ( + TrueOnPolicyArgList, + TrueOnPolicyKernelPolicy, + TrueOnPolicyLaunchPlan, + TrueOnPolicyParallelLayout, + apply_true_on_policy_script_defaults, + build_true_on_policy_launch_plan, +) +from .contracts import QWEN3_DENSE_TRUE_ON_POLICY_V1, TrueOnPolicyContract, get_true_on_policy_contract +from .model_profiles import TrueOnPolicyModelProfile, get_megatron_model_type, get_true_on_policy_model_profile + +__all__ = [ + "TrueOnPolicyLaunchPlan", + "TrueOnPolicyArgList", + "TrueOnPolicyKernelPolicy", + "TrueOnPolicyContract", + "TrueOnPolicyModelProfile", + "TrueOnPolicyParallelLayout", + "QWEN3_DENSE_TRUE_ON_POLICY_V1", + "apply_true_on_policy_script_defaults", + "build_true_on_policy_launch_plan", + "get_true_on_policy_contract", + "get_megatron_model_type", + "get_true_on_policy_model_profile", +] diff --git a/miles/true_on_policy/config.py b/miles/true_on_policy/config.py new file mode 100644 index 00000000000..dfc843106e3 --- /dev/null +++ b/miles/true_on_policy/config.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import os +import shlex +from dataclasses import dataclass, field +from typing import Any, Literal + +from .contracts import TrueOnPolicyContract, get_true_on_policy_contract +from .model_profiles import TrueOnPolicyModelProfile, get_true_on_policy_model_profile + + +OnPolicyTarget = Literal["fsdp", "fsdp_tp"] +TrainBackend = Literal["fsdp", "megatron"] + + +@dataclass(frozen=True) +class TrueOnPolicyArgList: + """Structured command-line args that stringify only at launch boundaries.""" + + values: tuple[str, ...] = () + + def as_cli_string(self) -> str: + if not self.values: + return "" + return " ".join(shlex.quote(value) for value in self.values) + " " + + def contains(self, flag: str) -> bool: + return flag in self.values + + +@dataclass(frozen=True) +class TrueOnPolicyParallelLayout: + """Training and rollout topology relevant to true-on-policy parity.""" + + train_tensor_parallel_size: int + train_context_parallel_size: int + train_pipeline_parallel_size: int + rollout_num_gpus_per_engine: int + + @property + def uses_train_tp(self) -> bool: + return self.train_tensor_parallel_size > 1 + + @property + def uses_ulysses_cp(self) -> bool: + return self.train_context_parallel_size > 1 + + @property + def uses_train_pp(self) -> bool: + return self.train_pipeline_parallel_size > 1 + + @property + def uses_rollout_tp(self) -> bool: + return self.rollout_num_gpus_per_engine > 1 + + +@dataclass(frozen=True) +class TrueOnPolicyKernelPolicy: + """Kernel/runtime switches required to keep SGLang and Megatron aligned.""" + + contract: TrueOnPolicyContract + deterministic_inference: bool + deterministic_training: bool + sglang_attention_backend: str + megatron_uses_sglang_backend: bool + disable_rope_fusion: bool + disable_bias_swiglu_fusion: bool + batch_invariant_mode: bool + tp_invariant_row_linear: bool + deterministic_tp_allreduce: bool + + def build_sglang_args(self) -> TrueOnPolicyArgList: + values = [ + "--sglang-true-on-policy-contract", + self.contract.name, + "--sglang-attention-backend", + self.sglang_attention_backend, + ] + if self.deterministic_inference: + values.insert(0, "--sglang-enable-deterministic-inference") + return TrueOnPolicyArgList(tuple(values)) + + def build_megatron_args(self) -> TrueOnPolicyArgList: + values: list[str] = [] + if self.megatron_uses_sglang_backend: + values.extend( + [ + "--true-on-policy-contract", + self.contract.name, + "--transformer-impl", + "local", + "--use-cpu-initialization", + ] + ) + if self.batch_invariant_mode: + values.append("--batch-invariant-mode") + if self.disable_bias_swiglu_fusion: + values.append("--no-bias-swiglu-fusion") + if self.disable_rope_fusion: + values.append("--no-rope-fusion") + return TrueOnPolicyArgList(tuple(values)) + + def build_env_vars(self) -> dict[str, str]: + return { + "NCCL_ALGO": os.environ.get("NCCL_ALGO", "Ring"), + "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", + "CUBLAS_WORKSPACE_CONFIG": ":4096:8", + } + + +@dataclass(frozen=True) +class TrueOnPolicyLaunchPlan: + """Derived cross-repo launch contract for one true-on-policy run.""" + + enabled: bool + model_profile: TrueOnPolicyModelProfile | None = None + contract: TrueOnPolicyContract | None = None + train_backend: TrainBackend | None = None + sglang_target: OnPolicyTarget | None = None + parallel_layout: TrueOnPolicyParallelLayout | None = None + kernel_policy: TrueOnPolicyKernelPolicy | None = None + sglang_args: TrueOnPolicyArgList = field(default_factory=TrueOnPolicyArgList) + megatron_args: TrueOnPolicyArgList = field(default_factory=TrueOnPolicyArgList) + fsdp_args: TrueOnPolicyArgList = field(default_factory=TrueOnPolicyArgList) + miles_args: TrueOnPolicyArgList = field(default_factory=TrueOnPolicyArgList) + env_vars: dict[str, str] = field(default_factory=dict) + + @property + def train_args(self) -> str: + return ( + self.sglang_args.as_cli_string() + + self.megatron_args.as_cli_string() + + self.fsdp_args.as_cli_string() + + self.miles_args.as_cli_string() + ) + + +@dataclass(frozen=True) +class TrueOnPolicyConfig: + """Typed contract derived from the single public true-on-policy switch.""" + + enabled: bool + model_profile: TrueOnPolicyModelProfile + train_backend: TrainBackend + tensor_model_parallel_size: int + context_parallel_size: int + pipeline_model_parallel_size: int + rollout_num_gpus_per_engine: int + contract_override: str | None = None + + @property + def parallel_layout(self) -> TrueOnPolicyParallelLayout: + return TrueOnPolicyParallelLayout( + train_tensor_parallel_size=self.tensor_model_parallel_size, + train_context_parallel_size=self.context_parallel_size, + train_pipeline_parallel_size=self.pipeline_model_parallel_size, + rollout_num_gpus_per_engine=self.rollout_num_gpus_per_engine, + ) + + @property + def requires_tp_invariant_rollout(self) -> bool: + layout = self.parallel_layout + return layout.uses_train_tp or layout.uses_rollout_tp + + @property + def sglang_target(self) -> OnPolicyTarget: + return "fsdp_tp" if self.requires_tp_invariant_rollout else "fsdp" + + @property + def contract(self) -> TrueOnPolicyContract: + if self.contract_override is not None: + return get_true_on_policy_contract(self.contract_override) + return self.model_profile.contract + + def validate(self) -> None: + if not self.enabled: + return + layout = self.parallel_layout + if self.contract.model_family != self.model_profile.family: + raise ValueError( + f"Contract {self.contract.name!r} is for {self.contract.model_family}, " + f"but model profile is {self.model_profile.family}" + ) + if self.train_backend == "megatron" and not self.model_profile.supports_megatron: + raise ValueError(f"{self.model_profile.family} does not support Megatron true-on-policy") + if self.train_backend == "fsdp" and not self.model_profile.supports_fsdp: + raise ValueError(f"{self.model_profile.family} does not support FSDP true-on-policy") + if layout.uses_ulysses_cp and not self.model_profile.supports_ulysses_cp: + raise ValueError(f"{self.model_profile.family} does not support Ulysses CP true-on-policy") + if layout.uses_train_pp and "pp" not in self.model_profile.supported_train_layouts: + raise ValueError(f"{self.model_profile.family} does not support PP true-on-policy") + if self.sglang_target == "fsdp_tp" and not self.model_profile.supports_tp_invariant: + raise ValueError(f"{self.model_profile.family} does not support TP-invariant true-on-policy") + + def build_kernel_policy(self) -> TrueOnPolicyKernelPolicy: + return TrueOnPolicyKernelPolicy( + contract=self.contract, + **self.contract.kernel_policy_kwargs_for( + train_backend=self.train_backend, + sglang_target=self.sglang_target, + ), + ) + + def build_launch_plan(self) -> TrueOnPolicyLaunchPlan: + self.validate() + kernel_policy = self.build_kernel_policy() + miles_args = TrueOnPolicyArgList( + ( + "--deterministic-mode", + "--true-on-policy-mode", + "--recompute-logprobs-via-prefill", + ) + ) + + if self.train_backend == "megatron": + megatron_args = kernel_policy.build_megatron_args() + fsdp_args = TrueOnPolicyArgList() + elif self.train_backend == "fsdp": + megatron_args = TrueOnPolicyArgList() + fsdp_args = TrueOnPolicyArgList(("--attn-implementation", self.contract.fsdp_attention_implementation)) + else: + raise NotImplementedError(f"Unsupported true-on-policy train backend: {self.train_backend}") + + return TrueOnPolicyLaunchPlan( + enabled=True, + model_profile=self.model_profile, + contract=self.contract, + train_backend=self.train_backend, + sglang_target=self.sglang_target, + parallel_layout=self.parallel_layout, + kernel_policy=kernel_policy, + sglang_args=kernel_policy.build_sglang_args(), + megatron_args=megatron_args, + fsdp_args=fsdp_args, + miles_args=miles_args, + env_vars=kernel_policy.build_env_vars(), + ) + + +def _get_required_int(args: Any, name: str) -> int: + value = getattr(args, name) + if value is None: + raise ValueError(f"{name} must be initialized before deriving true-on-policy config") + return int(value) + + +def build_true_on_policy_config(args: Any) -> TrueOnPolicyConfig | None: + if not getattr(args, "true_on_policy", False): + return None + + profile = get_true_on_policy_model_profile(args.model_name) + return TrueOnPolicyConfig( + enabled=True, + model_profile=profile, + train_backend=args.train_backend, + tensor_model_parallel_size=_get_required_int(args, "tensor_model_parallel_size"), + context_parallel_size=_get_required_int(args, "context_parallel_size"), + pipeline_model_parallel_size=_get_required_int(args, "pipeline_model_parallel_size"), + rollout_num_gpus_per_engine=_get_required_int(args, "rollout_num_gpus_per_engine"), + contract_override=getattr(args, "true_on_policy_contract", None), + ) + + +def build_true_on_policy_launch_plan(args: Any) -> TrueOnPolicyLaunchPlan: + config = build_true_on_policy_config(args) + if config is None: + return TrueOnPolicyLaunchPlan(enabled=False) + return config.build_launch_plan() + + +def apply_true_on_policy_script_defaults(args: Any) -> None: + """Apply derived defaults that must be visible before command assembly.""" + config = build_true_on_policy_config(args) + if config is None: + return + + config.validate() + if args.train_backend == "megatron" and config.model_profile.disable_megatron_sequence_parallel: + args.use_sequence_parallel = False diff --git a/miles/true_on_policy/contracts.py b/miles/true_on_policy/contracts.py new file mode 100644 index 00000000000..7e5ca85f6dd --- /dev/null +++ b/miles/true_on_policy/contracts.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .schema import ( + QWEN3_DENSE_TRUE_ON_POLICY_V1_SCHEMA, + KernelContract, + LogprobContract, + ModelFamily, + TrueOnPolicyContractName, + TrueOnPolicyContractSchema, +) + + +@dataclass(frozen=True) +class TrueOnPolicyContract: + """Internal parity contract selected by Miles and implemented by each backend.""" + + schema: TrueOnPolicyContractSchema + + @property + def name(self) -> TrueOnPolicyContractName: + return self.schema.name + + @property + def model_family(self) -> ModelFamily: + return self.schema.model_family + + @property + def required_kernel_contracts(self) -> tuple[KernelContract, ...]: + return self.schema.required_kernel_contracts + + @property + def logprob_contract(self) -> LogprobContract: + return self.schema.logprob_contract + + @property + def sglang_attention_backend(self) -> str: + return self.schema.sglang_attention_backend + + @property + def fsdp_attention_implementation(self) -> str: + return self.schema.fsdp_attention_implementation + + @property + def disable_megatron_sequence_parallel(self) -> bool: + return self.schema.disable_megatron_sequence_parallel + + def kernel_policy_kwargs_for( + self, + *, + train_backend: str, + sglang_target: str, + ) -> dict[str, object]: + uses_megatron = train_backend == "megatron" + uses_tp_invariant_rollout = sglang_target == "fsdp_tp" + return { + "deterministic_inference": True, + "deterministic_training": True, + "sglang_attention_backend": self.sglang_attention_backend, + "megatron_uses_sglang_backend": uses_megatron, + "disable_rope_fusion": uses_megatron, + "disable_bias_swiglu_fusion": uses_megatron, + "batch_invariant_mode": uses_megatron, + "tp_invariant_row_linear": uses_tp_invariant_rollout, + "deterministic_tp_allreduce": uses_tp_invariant_rollout, + } + + +QWEN3_DENSE_TRUE_ON_POLICY_V1 = TrueOnPolicyContract( + schema=QWEN3_DENSE_TRUE_ON_POLICY_V1_SCHEMA, +) + + +_CONTRACT_BY_NAME = { + QWEN3_DENSE_TRUE_ON_POLICY_V1.name: QWEN3_DENSE_TRUE_ON_POLICY_V1, +} + + +def get_true_on_policy_contract(name: str) -> TrueOnPolicyContract: + try: + return _CONTRACT_BY_NAME[name] + except KeyError as exc: + supported = ", ".join(sorted(_CONTRACT_BY_NAME)) + raise ValueError(f"Unsupported true-on-policy contract {name!r}. Supported contracts: {supported}") from exc diff --git a/miles/true_on_policy/model_profiles.py b/miles/true_on_policy/model_profiles.py new file mode 100644 index 00000000000..69db4b0b71a --- /dev/null +++ b/miles/true_on_policy/model_profiles.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .contracts import QWEN3_DENSE_TRUE_ON_POLICY_V1, LogprobContract, ModelFamily, TrueOnPolicyContract + +ParallelLayout = str + + +@dataclass(frozen=True) +class TrueOnPolicyModelProfile: + """Model-specific true-on-policy capabilities and launch defaults.""" + + family: ModelFamily + model_names: tuple[str, ...] + megatron_model_types: dict[str, str] + supported_train_layouts: tuple[ParallelLayout, ...] + supported_rollout_layouts: tuple[ParallelLayout, ...] + contract: TrueOnPolicyContract + supports_megatron: bool = True + supports_fsdp: bool = True + + @property + def required_kernel_contracts(self): + return self.contract.required_kernel_contracts + + @property + def logprob_contract(self) -> LogprobContract: + return self.contract.logprob_contract + + @property + def sglang_attention_backend(self) -> str: + return self.contract.sglang_attention_backend + + @property + def fsdp_attention_implementation(self) -> str: + return self.contract.fsdp_attention_implementation + + @property + def disable_megatron_sequence_parallel(self) -> bool: + return self.contract.disable_megatron_sequence_parallel + + @property + def supports_ulysses_cp(self) -> bool: + return "ulysses_cp" in self.supported_train_layouts + + @property + def supports_tp_invariant(self) -> bool: + return "tp" in self.supported_train_layouts or "tp" in self.supported_rollout_layouts + + def megatron_model_type_for(self, model_name: str) -> str: + try: + return self.megatron_model_types[model_name] + except KeyError as exc: + supported = ", ".join(sorted(self.megatron_model_types)) + raise ValueError( + f"{model_name!r} does not have a Megatron model type in profile " + f"{self.family!r}; supported names: {supported}" + ) from exc + + +QWEN3_DENSE_PROFILE = TrueOnPolicyModelProfile( + family="qwen3_dense", + model_names=( + "Qwen3-0.6B", + "Qwen3-4B", + "Qwen3-4B-Base", + "Qwen3-4B-Instruct-2507", + ), + megatron_model_types={ + "Qwen3-0.6B": "qwen3-0.6B", + "Qwen3-4B": "qwen3-4B", + "Qwen3-4B-Base": "qwen3-4B", + "Qwen3-4B-Instruct-2507": "qwen3-4B-Instruct-2507", + }, + supported_train_layouts=("dp", "tp", "pp", "ulysses_cp"), + supported_rollout_layouts=("dp", "tp"), + contract=QWEN3_DENSE_TRUE_ON_POLICY_V1, +) + + +_MODEL_PROFILES = (QWEN3_DENSE_PROFILE,) +_PROFILE_BY_MODEL_NAME = {model_name: profile for profile in _MODEL_PROFILES for model_name in profile.model_names} + + +def get_true_on_policy_model_profile(model_name: str) -> TrueOnPolicyModelProfile: + try: + return _PROFILE_BY_MODEL_NAME[model_name] + except KeyError as exc: + supported = ", ".join(sorted(_PROFILE_BY_MODEL_NAME)) + raise ValueError( + f"true-on-policy does not have a model profile for {model_name!r}. " f"Supported models: {supported}" + ) from exc + + +def get_megatron_model_type(model_name: str) -> str: + return get_true_on_policy_model_profile(model_name).megatron_model_type_for(model_name) diff --git a/miles/true_on_policy/schema.py b/miles/true_on_policy/schema.py new file mode 100644 index 00000000000..ccfb91285e7 --- /dev/null +++ b/miles/true_on_policy/schema.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + + +TrueOnPolicyContractName = Literal["qwen3_dense_true_on_policy_v1"] +ModelFamily = Literal["qwen3_dense", "qwen3_moe", "qwen3_next"] +KernelContract = Literal["qwen3_dense_sglang_math"] +LogprobContract = Literal["sglang_prefill"] + + +@dataclass(frozen=True) +class TrueOnPolicyContractSchema: + """Declarative cross-repo identity for a true-on-policy parity contract.""" + + name: TrueOnPolicyContractName + model_family: ModelFamily + required_kernel_contracts: tuple[KernelContract, ...] + logprob_contract: LogprobContract + sglang_attention_backend: str + fsdp_attention_implementation: str + disable_megatron_sequence_parallel: bool + + +QWEN3_DENSE_TRUE_ON_POLICY_V1_SCHEMA = TrueOnPolicyContractSchema( + name="qwen3_dense_true_on_policy_v1", + model_family="qwen3_dense", + required_kernel_contracts=("qwen3_dense_sglang_math",), + logprob_contract="sglang_prefill", + sglang_attention_backend="fa3", + fsdp_attention_implementation="flash_attention_3", + disable_megatron_sequence_parallel=True, +) diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index abfa137ac9a..df8317ef5b9 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -145,6 +145,15 @@ def add_train_arguments(parser): default=False, help="Whether to enable true-on-policy mode.", ) + parser.add_argument( + "--recompute-logprobs-via-prefill", + action="store_true", + default=False, + help=( + "Recompute rollout logprobs via SGLang prefill instead of decode kernels. " + "Only needed for models whose prefill and decode paths are not numerically identical." + ), + ) parser.add_argument( "--train-env-vars", type=json.loads, @@ -1688,7 +1697,10 @@ def add_session_arguments(parser): return parser def add_user_provided_function_arguments(parser): - args_partial, _ = parser.parse_known_args() + try: + args_partial, _ = parser.parse_known_args() + except SystemExit: + return parser for path in [ args_partial.rollout_function_path, args_partial.custom_generate_function_path, @@ -1870,6 +1882,9 @@ def _resolve_eval_datasets(args) -> list[EvalDatasetConfig]: def miles_validate_args(args): args.eval_datasets = _resolve_eval_datasets(args) + if args.recompute_logprobs_via_prefill: + assert args.true_on_policy_mode, "--recompute-logprobs-via-prefill requires --true-on-policy-mode" + # Normalize --tito-allowed-append-roles: lowercase + deduplicate. raw_roles = getattr(args, "tito_allowed_append_roles", ["tool"]) args.tito_allowed_append_roles = sorted(set(r.lower() for r in raw_roles)) @@ -2224,10 +2239,19 @@ def _maybe_apply_dumper_overrides(args) -> None: args.router_disable_health_check = True args.rollout_health_check_interval = 1e18 - logger.info("Dumper mode: forced num_rollout=%d, disabled eval and save", args.num_rollout) + if args.start_rollout_id is None: + args.start_rollout_id = 0 + args.num_rollout = (args.start_rollout_id or 0) + 1 + logger.info( + "Dumper mode: forced rollout range [%d, %d), disabled eval and save", + args.start_rollout_id, + args.num_rollout, + ) args.eval_interval = None + args.save = None args.save_interval = None + args.save_retain_interval = None def hf_validate_args(args, hf_config): diff --git a/miles/utils/dumper_utils.py b/miles/utils/dumper_utils.py index 2d14899752d..aea925aa6e3 100644 --- a/miles/utils/dumper_utils.py +++ b/miles/utils/dumper_utils.py @@ -31,9 +31,18 @@ def get_sglang_env(args: Namespace) -> dict[str, str]: return {} env: dict[str, str] = {"DUMPER_SERVER_PORT": "reuse"} + overrides = _get_phase_override_configs(args, DumperPhase.INFERENCE) + + # SGLang registers non-intrusive hooks while loading the model. Configs that + # affect hook registration must be present in the actor environment; the + # later HTTP configure call only controls active dumping/output location. + if non_intrusive_mode := overrides.get("non_intrusive_mode"): + env["DUMPER_NON_INTRUSIVE_MODE"] = str(non_intrusive_mode) if source_patcher_config := args.dumper_source_patcher_config_inference: env["DUMPER_SOURCE_PATCHER_CONFIG"] = source_patcher_config + elif source_patcher_config := overrides.get("source_patcher_config"): + env["DUMPER_SOURCE_PATCHER_CONFIG"] = str(source_patcher_config) return env @@ -70,7 +79,9 @@ async def configure_sglang(args: Namespace) -> None: class DumperMegatronUtil: def __init__(self, args: Namespace, model: Sequence[torch.nn.Module], phase: DumperPhase) -> None: - self.enabled = self._configure(args, phase) + self.phase = phase + self.overrides = _get_phase_override_configs(args, phase) + self.enabled = self._configure(args, phase, self.overrides) if self.enabled: dumper.register_non_intrusive_dumper(self._extract_model(model)) @@ -84,7 +95,11 @@ def finalize(self, model: Sequence[torch.nn.Module]) -> None: if not self.enabled: return - dumper.dump_model(self._extract_model(model)) + extracted_model = self._extract_model(model) + if self.phase is DumperPhase.FWD_BWD and self.overrides.get("enable_model_grad"): + _log_model_grad_coverage(extracted_model) + + dumper.dump_model(extracted_model) dumper.step() dumper.configure(enable=False) @@ -96,8 +111,9 @@ def _extract_model(model: Sequence[torch.nn.Module]) -> torch.nn.Module: return model[0] @staticmethod - def _configure(args: Namespace, phase: DumperPhase) -> bool: - overrides = _get_phase_override_configs(args, phase) + def _configure(args: Namespace, phase: DumperPhase, overrides: dict[str, Any] | None = None) -> bool: + if overrides is None: + overrides = _get_phase_override_configs(args, phase) if not overrides.get("enable"): return False @@ -114,6 +130,33 @@ def _configure(args: Namespace, phase: DumperPhase) -> bool: return True +def _log_model_grad_coverage(model: torch.nn.Module) -> None: + missing: list[str] = [] + with_grad = 0 + total = 0 + + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + + total += 1 + grad = param.grad if param.grad is not None else getattr(param, "main_grad", None) + if grad is None: + missing.append(name) + else: + with_grad += 1 + + rank = dist.get_rank() if dist.is_available() and dist.is_initialized() else _get_rank() + logger.info( + "Dumper fwd_bwd model grad coverage rank=%s with_grad=%d total=%d missing=%d missing_names=%s", + rank, + with_grad, + total, + len(missing), + missing[:20], + ) + + def _wrap_forward_step_with_stepping(forward_step_func: Callable) -> Callable: is_first_call = True diff --git a/miles/utils/ppo_utils.py b/miles/utils/ppo_utils.py index 4c09dc50816..26a28c2e8b3 100644 --- a/miles/utils/ppo_utils.py +++ b/miles/utils/ppo_utils.py @@ -2,6 +2,7 @@ # and https://github.com/OpenRLHF/OpenRLHF/blob/10c733694ed9fbb78a0a2ff6a05efc7401584d46/openrlhf/trainer/ppo_utils/experience_maker.py from argparse import Namespace +from pathlib import Path import torch import torch.distributed as dist @@ -10,6 +11,56 @@ from miles.backends.training_utils.parallel import get_parallel_state +_TOP_LOGPROB_BWD_DUMP_COUNTER = 0 + + +def _maybe_dump_top_logprob_backward(name: str, tensor: torch.Tensor) -> None: + dump_dir = None + try: + import os + + dump_dir = os.environ.get("MILES_LOGPROB_BACKWARD_DEBUG_DIR") + except Exception: + dump_dir = None + if not dump_dir or not tensor.requires_grad: + return + + def hook(grad: torch.Tensor) -> torch.Tensor: + global _TOP_LOGPROB_BWD_DUMP_COUNTER + rank = dist.get_rank() if dist.is_available() and dist.is_initialized() else 0 + stats = { + "rank": rank, + "name": name, + "shape": tuple(grad.shape), + "dtype": str(grad.dtype), + "numel": grad.numel(), + "finite": torch.isfinite(grad).sum().item(), + "nan": torch.isnan(grad).sum().item(), + "inf": torch.isinf(grad).sum().item(), + } + finite = grad[torch.isfinite(grad)] + if finite.numel() > 0: + finite_f = finite.float() + stats.update( + { + "max_abs_finite": finite_f.abs().max().item(), + "min_finite": finite_f.min().item(), + "max_finite": finite_f.max().item(), + } + ) + else: + stats.update({"max_abs_finite": None, "min_finite": None, "max_finite": None}) + counter = _TOP_LOGPROB_BWD_DUMP_COUNTER + _TOP_LOGPROB_BWD_DUMP_COUNTER += 1 + path = Path(dump_dir) / f"rank_{rank}_{counter:05d}_{name}.pt" + path.parent.mkdir(parents=True, exist_ok=True) + torch.save({"stats": stats, "grad": grad.detach().cpu()}, path) + print(f"[MILES_LOGPROB_BACKWARD_DEBUG] {stats} wrote {path}", flush=True) + return grad + + tensor.register_hook(hook) + + @torch.compile(dynamic=True) def compute_approx_kl( log_probs: torch.Tensor, @@ -150,7 +201,19 @@ def compute_policy_loss( return pg_losses, clipfrac -def compute_log_probs(logits: torch.Tensor, tokens: torch.Tensor, process_group: dist.ProcessGroup | None): +def compute_log_probs( + logits: torch.Tensor, + tokens: torch.Tensor, + process_group: dist.ProcessGroup | None, + *, + true_on_policy_mode: bool = False, + vocab_size: int | None = None, +): + if true_on_policy_mode: + full_logits = _gather_true_on_policy_full_logits(logits, process_group, vocab_size=vocab_size) + log_probs = torch.log_softmax(full_logits, dim=-1) + return log_probs.gather(dim=-1, index=tokens.unsqueeze(-1)).squeeze(-1) + # TODO: when megatron is not installed, fall back to naive implementation from megatron.core.fusions.fused_cross_entropy import fused_vocab_parallel_cross_entropy @@ -160,6 +223,100 @@ def compute_log_probs(logits: torch.Tensor, tokens: torch.Tensor, process_group: return -fused_vocab_parallel_cross_entropy(logits, tokens, process_group) +def _prepare_true_on_policy_full_logits( + logits_or_shards: torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor, ...], + *, + vocab_size: int | None = None, +) -> torch.Tensor: + if isinstance(logits_or_shards, (list, tuple)): + full_logits = torch.cat([shard.contiguous() for shard in logits_or_shards], dim=-1) + else: + full_logits = logits_or_shards.contiguous() + + if vocab_size is not None and full_logits.size(-1) > vocab_size: + full_logits = full_logits[..., :vocab_size] + + return full_logits + + +def _gather_true_on_policy_full_logits( + logits: torch.Tensor, + process_group: dist.ProcessGroup | None, + *, + vocab_size: int | None = None, +) -> torch.Tensor: + if process_group is None: + return _prepare_true_on_policy_full_logits(logits, vocab_size=vocab_size) + + tp_size = dist.get_world_size(process_group) + if tp_size <= 1: + return _prepare_true_on_policy_full_logits(logits, vocab_size=vocab_size) + + full_logits = _ReplicatedLossAllGatherLastDim.apply(logits.contiguous(), process_group) + return _prepare_true_on_policy_full_logits(full_logits, vocab_size=vocab_size) + + +def _split_replicated_loss_gather_grad( + grad_output: torch.Tensor, + *, + rank: int, + world_size: int, + local_last_dim: int, +) -> torch.Tensor: + if world_size <= 1: + return grad_output.contiguous() + + expected_last_dim = local_last_dim * world_size + if grad_output.size(-1) != expected_last_dim: + raise RuntimeError( + "True-on-policy replicated-loss gather backward expected the full padded " + f"vocab dimension to be {expected_last_dim}, got {grad_output.size(-1)}." + ) + + return torch.narrow(grad_output, dim=-1, start=rank * local_last_dim, length=local_last_dim).contiguous() + + +class _ReplicatedLossAllGatherLastDim(torch.autograd.Function): + """All-gather vocab shards for a loss replicated on every TP rank. + + Megatron's standard all-gather autograd uses reduce-scatter in backward, + which is correct when each rank contributes a distinct output gradient. In + the true-on-policy logprob path every TP rank computes the same scalar loss + from the gathered full vocabulary, so reduce-scatter would sum identical + gradients and scale the local logits gradient by TP size. + """ + + @staticmethod + def forward(ctx, input_: torch.Tensor, group: dist.ProcessGroup) -> torch.Tensor: + world_size = group.size() + ctx.group = group + ctx.local_last_dim = input_.shape[-1] + ctx.world_size = world_size + + if world_size == 1: + return input_.contiguous() + + from megatron.core.tensor_parallel.mappings import dist_all_gather_func + + gather_shape = list(input_.shape) + gather_shape[0] *= world_size + gathered = torch.empty(gather_shape, dtype=input_.dtype, device=input_.device) + dist_all_gather_func(gathered, input_.contiguous(), group=group) + return torch.cat(gathered.chunk(world_size, dim=0), dim=-1).contiguous() + + @staticmethod + def backward(ctx, grad_output: torch.Tensor): + return ( + _split_replicated_loss_gather_grad( + grad_output, + rank=ctx.group.rank(), + world_size=ctx.world_size, + local_last_dim=ctx.local_last_dim, + ), + None, + ) + + # from https://github.com/volcengine/verl/blob/0bdf7f469854815177e73dcfe9e420836c952e6e/verl/utils/megatron/tensor_parallel.py#L99 class _VocabParallelEntropy(torch.autograd.Function): @@ -644,10 +801,22 @@ def chunked_gae( def calculate_log_probs_and_entropy( - logits, tokens, tp_group, with_entropy: bool = False, chunk_size: int = -1, true_on_policy: bool = False + logits, + tokens, + tp_group, + with_entropy: bool = False, + chunk_size: int = -1, + true_on_policy: bool = False, + vocab_size: int | None = None, ): if true_on_policy: - return _calculate_log_probs_and_entropy_true_on_policy(logits, tokens, with_entropy=with_entropy) + return _calculate_log_probs_and_entropy_true_on_policy( + logits, + tokens, + tp_group, + with_entropy=with_entropy, + vocab_size=vocab_size, + ) logits = logits.contiguous() # TODO: not sure why we need to clone the logits here. @@ -685,15 +854,20 @@ def calculate_log_probs_and_entropy( def _calculate_log_probs_and_entropy_true_on_policy( logits: torch.Tensor, tokens: torch.Tensor, + tp_group: dist.ProcessGroup | None, with_entropy: bool = False, + vocab_size: int | None = None, ) -> tuple[torch.Tensor, torch.Tensor | None]: - """Simple log-prob and entropy computation matching SGLang's inference path. + """True-on-policy log-prob and entropy computation matching SGLang's scoring contract. Args: - logits: Aligned logits of shape ``[R, V]`` (already response-sliced - and temperature-scaled by ``get_responses``). + logits: Aligned local logits of shape ``[R, V_local]`` (already + response-sliced and temperature-scaled by ``get_responses``). tokens: Target tokens of shape ``[R]``. + tp_group: Tensor-parallel process group for vocab gather. with_entropy: If True, also compute entropy. + vocab_size: Real tokenizer vocab size. If provided, padded logits are + truncated after the full-vocab gather and before ``log_softmax``. Returns: Tuple of ``(log_probs, entropy)`` where *log_probs* has shape ``[R]`` @@ -704,12 +878,16 @@ def _calculate_log_probs_and_entropy_true_on_policy( entropy = logits.new_zeros((0,)) if with_entropy else None return log_prob, entropy - log_probs_full = torch.log_softmax(logits, dim=-1) + full_logits = _gather_true_on_policy_full_logits(logits, tp_group, vocab_size=vocab_size) + _maybe_dump_top_logprob_backward("full_logits", full_logits) + log_probs_full = torch.log_softmax(full_logits, dim=-1) + _maybe_dump_top_logprob_backward("log_probs_full", log_probs_full) log_prob = torch.gather(log_probs_full, dim=-1, index=tokens.unsqueeze(-1)).squeeze(-1) + _maybe_dump_top_logprob_backward("log_prob", log_prob) entropy = None if with_entropy: - probs = torch.softmax(logits, dim=-1) + probs = log_probs_full.exp() entropy = -(probs * log_probs_full).sum(dim=-1) return log_prob, entropy diff --git a/scripts/run_qwen3_4b.py b/scripts/run_qwen3_4b.py index f16582cf320..7544e2c9b7b 100644 --- a/scripts/run_qwen3_4b.py +++ b/scripts/run_qwen3_4b.py @@ -4,11 +4,16 @@ import typer import miles.utils.external_utils.command_utils as U +from miles.true_on_policy import ( + apply_true_on_policy_script_defaults, + build_true_on_policy_launch_plan, + get_megatron_model_type, +) @dataclass class ScriptArgs(U.ExecuteTrainConfig): - mode: Literal["normal", "debug_minimal"] = "normal" + mode: Literal["normal", "debug_minimal", "debug_one_sample"] = "normal" run_id: str = U.create_run_id() model_name: str = "Qwen3-4B" megatron_model_type: str | None = None @@ -27,20 +32,32 @@ class ScriptArgs(U.ExecuteTrainConfig): train_fp8: bool = False enable_megatron_bridge: bool = False enable_mis: bool = False - # TODO improve, should be able to override more easily + use_kl_loss: bool = True tis_use_rs: bool = True def __post_init__(self): if self.train_backend == "megatron": - self.megatron_model_type = { - "Qwen3-0.6B": "qwen3-0.6B", - "Qwen3-4B-Instruct-2507": "qwen3-4B-Instruct-2507", - "Qwen3-4B-Base": "qwen3-4B", - "Qwen3-4B": "qwen3-4B", - }[self.model_name] + self.megatron_model_type = get_megatron_model_type(self.model_name) self.num_gpus_per_node = self.num_gpus_per_node or U.NUM_GPUS_OF_HARDWARE[self.hardware] + # Derived parallelism defaults for Qwen3 dense models + self.tensor_model_parallel_size = 1 if self.model_name == "Qwen3-0.6B" else 2 + self.pipeline_model_parallel_size = 1 + self.context_parallel_size = 1 if self.model_name == "Qwen3-0.6B" else 4 + self.cp_comm_type = "a2a" if self.context_parallel_size > 1 else None + self.use_sequence_parallel = self.tensor_model_parallel_size > 1 + self.max_tokens_per_gpu = 32768 if self.train_backend == "fsdp" else 9216 + self.rollout_num_gpus_per_engine = 1 + self.train_memory_margin_bytes = 3221225472 + + apply_true_on_policy_script_defaults(self) + if self.train_backend == "megatron" and self.enable_megatron_bridge and self.use_kl_loss: + raise ValueError( + "Megatron bridge mode does not provide a Megatron-format ref checkpoint for KL. " + "Disable KL loss or disable bridge mode." + ) + def prepare(args: ScriptArgs): U.exec_command(f"mkdir -p {args.model_dir} {args.data_dir}") @@ -67,22 +84,34 @@ def prepare(args: ScriptArgs): def execute(args: ScriptArgs): + is_debug_mode = args.mode != "normal" + is_debug_one_sample = args.mode == "debug_one_sample" + model_parallel_size = ( + args.tensor_model_parallel_size * args.pipeline_model_parallel_size * args.context_parallel_size + ) + actor_num_gpus_per_node = model_parallel_size + train_world_size = args.num_nodes * actor_num_gpus_per_node + data_parallel_size = max(1, train_world_size // model_parallel_size) + debug_num_rollout = max(2, data_parallel_size) + debug_global_batch_size = data_parallel_size load_save_path = f"{args.output_dir}/{args.run_id}/checkpoints" - - ref_load_path = f"{args.model_dir}/{args.model_name}" - if args.train_backend == "megatron" and not args.enable_megatron_bridge: - ref_load_path = f"{args.model_dir}/{args.model_name}_torch_dist" + megatron_load_path = f"{args.model_dir}/{args.model_name}_torch_dist" ckpt_args = ( f"--hf-checkpoint {args.model_dir}/{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-interval {2 if is_debug_mode else 20} " ) + if not args.enable_megatron_bridge: + ckpt_args += f"--load {megatron_load_path} " + if args.use_kl_loss: + ref_load_path = f"{args.model_dir}/{args.model_name}" + if args.train_backend == "megatron": + ref_load_path = f"{args.model_dir}/{args.model_name}_torch_dist" + ckpt_args += f"--ref-load {ref_load_path} " if args.train_backend == "megatron": - ckpt_args += f"--save-retain-interval {2 if args.mode == 'debug_minimal' else 20} " + ckpt_args += f"--save-retain-interval {2 if is_debug_mode else 20} " rollout_args = ( f"--prompt-data {args.data_dir}/dapo-math-17k/dapo-math-17k.jsonl " @@ -93,16 +122,16 @@ def execute(args: ScriptArgs): # """--apply-chat-template-kwargs '{"enable_thinking":false}' """ "--rollout-shuffle " "--rm-type math " - "--num-rollout 3000 " - "--rollout-batch-size 32 " - "--n-samples-per-prompt 8 " - f"--rollout-max-response-len {100 if args.mode == 'debug_minimal' else 8192} " + f"--num-rollout {debug_num_rollout if is_debug_one_sample else 3000} " + f"--rollout-batch-size {1 if is_debug_one_sample else 32} " + f"--n-samples-per-prompt {1 if is_debug_one_sample else 8} " + f"--rollout-max-response-len {2 if is_debug_one_sample else (100 if args.mode == 'debug_minimal' else 8192)} " "--rollout-temperature 1 " - "--global-batch-size 256 " + f"--global-batch-size {debug_global_batch_size if is_debug_one_sample else 256} " "--balance-data " ) - if args.dynamic_sampling and (args.true_on_policy != "debug_minimal"): + if args.dynamic_sampling and not is_debug_mode: rollout_args += ( "--over-sampling-batch-size 64 " "--dynamic-sampling-filter-path miles.rollout.filter_hub.dynamic_sampling_filters.check_reward_nonzero_std " @@ -110,7 +139,7 @@ def execute(args: ScriptArgs): # sometimes disable eval to speed up debugging eval_args = "" - if (args.mode != "debug_minimal") and args.enable_eval: + if (not is_debug_mode) and args.enable_eval: eval_max_response_len = 16384 eval_args += "--eval-interval 20 " if args.multi_eval: @@ -142,15 +171,9 @@ def execute(args: ScriptArgs): "--eval-top-p 1 " ) - 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 " - ) + grpo_args = "--advantage-estimator grpo " "--entropy-coef 0.00 " "--eps-clip 0.2 " "--eps-clip-high 0.28 " + if args.use_kl_loss: + grpo_args += "--use-kl-loss --kl-loss-coef 0.00 --kl-loss-type low_var_kl " optimizer_args = ( "--optimizer adam " @@ -162,7 +185,12 @@ def execute(args: ScriptArgs): "--adam-beta2 0.98 " ) - sglang_args = "--rollout-num-gpus-per-engine 1 " "--sglang-chunked-prefill-size 4096 " + sglang_args = ( + f"--rollout-num-gpus-per-engine {args.rollout_num_gpus_per_engine} " + "--sglang-chunked-prefill-size 4096 " + f"{'--sglang-disable-cuda-graph ' if is_debug_one_sample else ''}" + ) + ci_args = "--ci-test --ci-disable-kl-checker " if is_debug_one_sample else "" match args.train_backend: case "fsdp": @@ -174,20 +202,15 @@ def execute(args: ScriptArgs): """--train-env-vars '{"PYTORCH_CUDA_ALLOC_CONF":"expandable_segments:True"}' """ ) sglang_args += "--sglang-mem-fraction-static 0.75 " - perf_args = "--use-dynamic-batch-size " "--max-tokens-per-gpu 32768 " + perf_args = f"--use-dynamic-batch-size --max-tokens-per-gpu {args.max_tokens_per_gpu} " case "megatron": - if args.model_name in ("Qwen3-0.6B",): - tp_size = 1 - cp_size = 1 - else: - tp_size = 2 if args.num_gpus_per_node == 8 else 1 - cp_size = 4 if args.num_gpus_per_node == 8 else 1 train_backend_args = ( - f"--tensor-model-parallel-size {tp_size} " - "--sequence-parallel " - "--pipeline-model-parallel-size 1 " - f"--context-parallel-size {cp_size} " + f"--tensor-model-parallel-size {args.tensor_model_parallel_size} " + f"{'--sequence-parallel ' if args.use_sequence_parallel and args.tensor_model_parallel_size > 1 else ''}" + f"--pipeline-model-parallel-size {args.pipeline_model_parallel_size} " + f"--context-parallel-size {args.context_parallel_size} " + f"{f'--cp-comm-type {args.cp_comm_type} ' if args.cp_comm_type is not None else ''}" "--expert-model-parallel-size 1 " "--expert-tensor-parallel-size 1 " "--recompute-granularity full " @@ -201,21 +224,21 @@ def execute(args: ScriptArgs): "--attention-softmax-in-fp32 " # need to comment this when using model with MLA "--attention-backend flash " - "--train-memory-margin-bytes 3221225472 " + f"--train-memory-margin-bytes {args.train_memory_margin_bytes} " ) # TODO improve sglang_args += "--sglang-mem-fraction-static 0.7 " - perf_args = "--use-dynamic-batch-size " "--max-tokens-per-gpu 9216 " + perf_args = f"--use-dynamic-batch-size --max-tokens-per-gpu {args.max_tokens_per_gpu} " case _: raise NotImplementedError misc_args = ( f"--actor-num-nodes {args.num_nodes} " - f"--actor-num-gpus-per-node {args.num_gpus_per_node} " + f"--actor-num-gpus-per-node {actor_num_gpus_per_node} " f"--num-gpus-per-node {args.num_gpus_per_node} " "--colocate " - f"{'--use-fault-tolerance ' if args.mode != 'debug_minimal' else ''}" + f"{'--use-fault-tolerance ' if not is_debug_mode else ''}" f"--dump-details {args.output_dir}/{args.run_id}/dump_details " ) misc_env_vars = {} @@ -260,22 +283,9 @@ def execute(args: ScriptArgs): "--custom-tis-function-path examples.train_infer_mismatch_helper.mis.compute_mis_weights_with_cp " ) - true_on_policy_args = "" - true_on_policy_envs = {} - if args.true_on_policy: - true_on_policy_args = ( - "--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 " - ) - true_on_policy_envs = { - "NCCL_ALGO": "allreduce:tree", - "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", - "CUBLAS_WORKSPACE_CONFIG": ":4096:8", - } + true_on_policy_plan = build_true_on_policy_launch_plan(args) + true_on_policy_args = true_on_policy_plan.train_args + true_on_policy_envs = true_on_policy_plan.env_vars train_args = ( f"{ckpt_args} " @@ -285,6 +295,7 @@ def execute(args: ScriptArgs): f"{U.get_default_wandb_args(__file__, run_id=args.run_id)} " f"{perf_args} " f"{eval_args} " + f"{ci_args} " f"{sglang_args} " f"{train_backend_args} " f"{misc_args} " diff --git a/tests/fast/backends/megatron_utils/test_model_provider_true_on_policy.py b/tests/fast/backends/megatron_utils/test_model_provider_true_on_policy.py new file mode 100644 index 00000000000..15a0b3e418d --- /dev/null +++ b/tests/fast/backends/megatron_utils/test_model_provider_true_on_policy.py @@ -0,0 +1,82 @@ +import sys +from argparse import Namespace +from pathlib import Path +from types import SimpleNamespace + +from tests.ci.ci_register import register_cpu_ci + +register_cpu_ci(est_time=10, suite="stage-a-fast") + + +def _make_args(): + return Namespace( + custom_model_provider_path=None, + megatron_to_hf_mode="raw", + transformer_impl="local", + spec=None, + num_experts=None, + moe_grouped_gemm=False, + qk_layernorm=True, + multi_latent_attention=False, + moe_use_legacy_grouped_gemm=False, + normalization="RMSNorm", + fp8_param_gather=False, + padded_vocab_size=151936, + max_position_embeddings=4096, + fp16_lm_cross_entropy=False, + untie_embeddings_and_output_weights=False, + position_embedding_type="rope", + rotary_percent=1.0, + rotary_base=1000000, + use_rope_scaling=False, + mtp_num_layers=0, + ) + + +def test_local_model_provider_passes_true_on_policy_spec_flags(monkeypatch): + megatron_path = Path.cwd().parent / "Megatron-LM" + if megatron_path.exists(): + sys.path.insert(0, str(megatron_path)) + + from miles.backends.megatron_utils import model_provider as provider_module + + captured = {} + + def fake_core_transformer_config_from_args(_args): + return SimpleNamespace( + hidden_size=2560, + use_kitchen=False, + true_on_policy_contract="qwen3_dense_true_on_policy_v1", + use_kitchen_attention=False, + kitchen_attention_backend="sdpa", + ) + + def fake_get_gpt_layer_local_spec(**kwargs): + captured.update(kwargs) + return "layer-spec" + + class FakeGPTModel: + def __init__(self, **kwargs): + self.kwargs = kwargs + + monkeypatch.setattr( + provider_module, + "core_transformer_config_from_args", + fake_core_transformer_config_from_args, + ) + monkeypatch.setattr( + provider_module, + "get_gpt_layer_local_spec", + fake_get_gpt_layer_local_spec, + ) + monkeypatch.setattr(provider_module, "GPTModel", FakeGPTModel) + + provider = provider_module.get_model_provider_func(_make_args()) + model = provider() + + assert model.kwargs["transformer_layer_spec"] == "layer-spec" + assert captured["normalization"] == "RMSNorm" + assert captured["use_true_on_policy_backend"] is True + assert captured["use_kitchen"] is False + assert captured["use_kitchen_attention"] is False + assert captured["kitchen_attention_backend"] == "sdpa" diff --git a/tests/fast/backends/megatron_utils/test_qwen2_true_on_policy_conversion.py b/tests/fast/backends/megatron_utils/test_qwen2_true_on_policy_conversion.py new file mode 100644 index 00000000000..f6207c79269 --- /dev/null +++ b/tests/fast/backends/megatron_utils/test_qwen2_true_on_policy_conversion.py @@ -0,0 +1,30 @@ +from argparse import Namespace + +import torch + +from tests.ci.ci_register import register_cpu_ci + +register_cpu_ci(est_time=10, suite="stage-a-fast") + + +def test_qwen2_converter_accepts_explicit_true_on_policy_layernorm_names(): + from miles.backends.megatron_utils.megatron_to_hf.qwen2 import convert_qwen2_to_hf + + args = Namespace( + hidden_size=4, + kv_channels=2, + num_attention_heads=2, + num_query_groups=1, + ) + param = torch.ones(4) + + assert convert_qwen2_to_hf( + args, + "module.module.decoder.layers.0.input_layernorm.weight", + param, + ) == [("model.layers.0.input_layernorm.weight", param)] + assert convert_qwen2_to_hf( + args, + "module.module.decoder.layers.0.pre_mlp_layernorm.weight", + param, + ) == [("model.layers.0.post_attention_layernorm.weight", param)] diff --git a/tests/fast/backends/training_utils/test_true_on_policy_data_log_dtype.py b/tests/fast/backends/training_utils/test_true_on_policy_data_log_dtype.py new file mode 100644 index 00000000000..4c401025b56 --- /dev/null +++ b/tests/fast/backends/training_utils/test_true_on_policy_data_log_dtype.py @@ -0,0 +1,62 @@ +from argparse import Namespace +from types import SimpleNamespace + +import torch + +from miles.backends.training_utils import cp_utils +from miles.backends.training_utils import data as data_utils +from miles.backends.training_utils import log_utils + + +def test_true_on_policy_rollout_logprob_dtype_follows_training_precision(): + assert ( + data_utils._rollout_logprob_dtype(Namespace(true_on_policy_mode=True, bf16=True, fp16=False)) is torch.bfloat16 + ) + assert ( + data_utils._rollout_logprob_dtype(Namespace(true_on_policy_mode=True, bf16=False, fp16=True)) is torch.float16 + ) + assert ( + data_utils._rollout_logprob_dtype(Namespace(true_on_policy_mode=False, bf16=True, fp16=False)) is torch.float32 + ) + + +def test_true_on_policy_log_checker_passes_when_values_and_dtype_match(monkeypatch): + captured = {} + parallel_state = SimpleNamespace( + tp=SimpleNamespace(rank=0), + cp=SimpleNamespace(size=1), + is_pp_last_stage=True, + ) + + monkeypatch.setattr(log_utils, "get_parallel_state", lambda: parallel_state) + monkeypatch.setattr(cp_utils, "get_parallel_state", lambda: parallel_state) + monkeypatch.setattr( + log_utils, + "gather_log_data", + lambda metric_name, args, rollout_id, log_dict: captured.setdefault("log_dict", log_dict), + ) + + rollout_data = { + "tokens": [torch.tensor([1, 2, 3])], + "total_lengths": [3], + "response_lengths": [2], + "loss_masks": [torch.tensor([1, 1], dtype=torch.int32)], + "log_probs": [torch.tensor([-13.25, -13.5], dtype=torch.bfloat16)], + "rollout_log_probs": [torch.tensor([-13.25, -13.5], dtype=torch.bfloat16)], + } + + log_utils.log_rollout_data( + 1, + Namespace( + ci_test=True, + ci_disable_logprobs_checker=False, + true_on_policy_mode=True, + qkv_format="thd", + log_multi_turn=False, + log_passrate=False, + log_correct_samples=False, + ), + rollout_data, + ) + + assert captured["log_dict"]["log_probs"] == captured["log_dict"]["rollout_log_probs"] diff --git a/tests/fast/backends/training_utils/test_true_on_policy_loss_metrics.py b/tests/fast/backends/training_utils/test_true_on_policy_loss_metrics.py new file mode 100644 index 00000000000..c7952330947 --- /dev/null +++ b/tests/fast/backends/training_utils/test_true_on_policy_loss_metrics.py @@ -0,0 +1,231 @@ +from argparse import Namespace +from types import SimpleNamespace + +import pytest +import torch + +from miles.backends.training_utils import loss as loss_utils + + +def _make_args(*, use_rollout_logprobs: bool) -> Namespace: + return Namespace( + use_rollout_logprobs=use_rollout_logprobs, + use_opsm=False, + advantage_estimator="ppo", + get_mismatch_metrics=False, + use_tis=False, + eps_clip=0.2, + eps_clip_high=0.2, + custom_tis_function_path=None, + custom_pg_loss_reducer_function_path=None, + calculate_per_token_loss=False, + qkv_format="thd", + entropy_coef=0.0, + use_kl_loss=False, + use_unbiased_kl=False, + kl_loss_type="k1", + kl_loss_coef=0.0, + rollout_temperature=1.0, + log_probs_chunk_size=-1, + true_on_policy_mode=False, + allgather_cp=False, + ) + + +def _make_batch(*, old_log_probs: torch.Tensor, rollout_log_probs: torch.Tensor) -> dict: + return { + "advantages": [torch.tensor([1.0, -0.5], dtype=torch.float32)], + "log_probs": [old_log_probs], + "rollout_log_probs": [rollout_log_probs], + "unconcat_tokens": [torch.tensor([7, 8, 9], dtype=torch.long)], + "response_lengths": [2], + "total_lengths": [3], + "loss_masks": [torch.tensor([1.0, 1.0], dtype=torch.float32)], + } + + +def _patch_single_rank_masks(monkeypatch): + monkeypatch.setattr( + loss_utils, + "get_local_response_loss_masks", + lambda total_lengths, response_lengths, loss_masks, qkv_format="thd", max_seq_lens=None: loss_masks, + ) + + +@pytest.mark.parametrize( + ("use_rollout_logprobs", "train_log_probs", "old_log_probs", "rollout_log_probs", "expected_abs_diff"), + [ + ( + False, + torch.tensor([0.40, 0.80], dtype=torch.float32), + torch.tensor([0.10, 0.20], dtype=torch.float32), + torch.tensor([0.40, 0.80], dtype=torch.float32), + 0.0, + ), + ( + True, + torch.tensor([0.50, 1.00], dtype=torch.float32), + torch.tensor([0.10, 0.20], dtype=torch.float32), + torch.tensor([0.40, 0.80], dtype=torch.float32), + 0.15, + ), + ], +) +def test_train_rollout_logprob_abs_diff_uses_recomputed_train_logprobs( + monkeypatch, + use_rollout_logprobs: bool, + train_log_probs: torch.Tensor, + old_log_probs: torch.Tensor, + rollout_log_probs: torch.Tensor, + expected_abs_diff: float, +): + args = _make_args(use_rollout_logprobs=use_rollout_logprobs) + batch = _make_batch(old_log_probs=old_log_probs, rollout_log_probs=rollout_log_probs) + + monkeypatch.setattr( + loss_utils, + "get_parallel_state", + lambda: SimpleNamespace(tp=SimpleNamespace(group=None)), + ) + _patch_single_rank_masks(monkeypatch) + monkeypatch.setattr( + loss_utils, + "get_log_probs_and_entropy", + lambda *args, **kwargs: { + "log_probs": [train_log_probs.clone()], + "entropy": [torch.zeros_like(train_log_probs)], + }, + ) + monkeypatch.setattr( + loss_utils, + "compute_policy_loss", + lambda ppo_kl, advantages, eps_clip, eps_clip_high: ( + torch.zeros_like(ppo_kl), + torch.zeros_like(ppo_kl), + ), + ) + + loss, metrics = loss_utils.policy_loss_function( + args, + batch, + logits=torch.zeros((1, 3, 8), dtype=torch.float32), + sum_of_sample_mean=lambda tensor: tensor.float().mean(), + ) + + assert torch.isfinite(loss) + torch.testing.assert_close(metrics["train_rollout_logprob_abs_diff"], torch.tensor(expected_abs_diff)) + + +def test_zero_weighted_entropy_nan_does_not_poison_policy_loss(monkeypatch): + args = _make_args(use_rollout_logprobs=False) + batch = _make_batch( + old_log_probs=torch.tensor([0.10, 0.20], dtype=torch.float32), + rollout_log_probs=torch.tensor([0.10, 0.20], dtype=torch.float32), + ) + + monkeypatch.setattr( + loss_utils, + "get_parallel_state", + lambda: SimpleNamespace(tp=SimpleNamespace(group=None)), + ) + _patch_single_rank_masks(monkeypatch) + + def fake_get_log_probs_and_entropy(*args, **kwargs): + assert kwargs["with_entropy"] is False + return {"log_probs": [torch.tensor([0.10, 0.20], dtype=torch.float32)]} + + monkeypatch.setattr(loss_utils, "get_log_probs_and_entropy", fake_get_log_probs_and_entropy) + monkeypatch.setattr( + loss_utils, + "compute_policy_loss", + lambda ppo_kl, advantages, eps_clip, eps_clip_high: ( + torch.zeros_like(ppo_kl), + torch.zeros_like(ppo_kl), + ), + ) + + loss, metrics = loss_utils.policy_loss_function( + args, + batch, + logits=torch.zeros((1, 3, 8), dtype=torch.float32), + sum_of_sample_mean=lambda tensor: tensor.float().mean(), + ) + + assert torch.isfinite(loss) + torch.testing.assert_close(metrics["entropy_loss"], torch.tensor(0.0)) + + +def test_zero_weighted_kl_nan_does_not_poison_policy_loss(monkeypatch): + args = _make_args(use_rollout_logprobs=False) + args.use_kl_loss = True + args.kl_loss_coef = 0.0 + batch = _make_batch( + old_log_probs=torch.tensor([0.10, 0.20], dtype=torch.float32), + rollout_log_probs=torch.tensor([0.10, 0.20], dtype=torch.float32), + ) + batch["ref_log_probs"] = [torch.tensor([float("nan"), float("nan")], dtype=torch.float32)] + + monkeypatch.setattr( + loss_utils, + "get_parallel_state", + lambda: SimpleNamespace(tp=SimpleNamespace(group=None)), + ) + _patch_single_rank_masks(monkeypatch) + monkeypatch.setattr( + loss_utils, + "get_log_probs_and_entropy", + lambda *args, **kwargs: { + "log_probs": [torch.tensor([0.10, 0.20], dtype=torch.float32)], + }, + ) + monkeypatch.setattr( + loss_utils, + "compute_policy_loss", + lambda ppo_kl, advantages, eps_clip, eps_clip_high: ( + torch.zeros_like(ppo_kl), + torch.zeros_like(ppo_kl), + ), + ) + + loss, metrics = loss_utils.policy_loss_function( + args, + batch, + logits=torch.zeros((1, 3, 8), dtype=torch.float32), + sum_of_sample_mean=lambda tensor: tensor.float().mean(), + ) + + assert torch.isfinite(loss) + assert torch.isfinite(metrics["kl_loss"]) + + +def test_masked_nonfinite_ppo_terms_do_not_poison_policy_loss(monkeypatch): + args = _make_args(use_rollout_logprobs=False) + batch = _make_batch( + old_log_probs=torch.tensor([0.10, float("nan")], dtype=torch.float32), + rollout_log_probs=torch.tensor([0.10, float("nan")], dtype=torch.float32), + ) + batch["loss_masks"] = [torch.tensor([1.0, 0.0], dtype=torch.float32)] + + monkeypatch.setattr( + loss_utils, + "get_parallel_state", + lambda: SimpleNamespace(tp=SimpleNamespace(group=None)), + ) + _patch_single_rank_masks(monkeypatch) + monkeypatch.setattr( + loss_utils, + "get_log_probs_and_entropy", + lambda *args, **kwargs: { + "log_probs": [torch.tensor([0.10, float("nan")], dtype=torch.float32)], + }, + ) + + loss, metrics = loss_utils.policy_loss_function( + args, + batch, + logits=torch.zeros((1, 3, 8), dtype=torch.float32), + sum_of_sample_mean=lambda tensor: (tensor * batch["loss_masks"][0]).sum(), + ) + + assert torch.isfinite(loss) + assert torch.isfinite(metrics["ppo_kl"]) diff --git a/tests/fast/backends/training_utils/test_ulysses_cp_utils.py b/tests/fast/backends/training_utils/test_ulysses_cp_utils.py new file mode 100644 index 00000000000..de5380f57da --- /dev/null +++ b/tests/fast/backends/training_utils/test_ulysses_cp_utils.py @@ -0,0 +1,70 @@ +import torch + +from miles.backends.training_utils import cp_utils +from miles.backends.training_utils.parallel import GroupInfo, ParallelState + + +def _parallel_state(*, cp_size: int, cp_rank: int = 0, cp_comm_type: str | None = None) -> ParallelState: + return ParallelState( + intra_dp=GroupInfo(rank=0, size=1, group=None), + intra_dp_cp=GroupInfo(rank=0, size=cp_size, group=None), + cp=GroupInfo(rank=cp_rank, size=cp_size, group=None), + tp=GroupInfo(rank=0, size=1, group=None), + cp_comm_type=cp_comm_type, + ) + + +def test_parallel_state_detects_ulysses_cp(): + assert _parallel_state(cp_size=4, cp_comm_type="a2a").is_ulysses_cp + assert _parallel_state(cp_size=4, cp_comm_type=["a2a"]).is_ulysses_cp + assert not _parallel_state(cp_size=1, cp_comm_type="a2a").is_ulysses_cp + assert not _parallel_state(cp_size=4, cp_comm_type="p2p").is_ulysses_cp + + +def test_slice_with_cp_uses_sequence_shards_for_sglang_ulysses(monkeypatch): + monkeypatch.setattr( + cp_utils, "get_parallel_state", lambda: _parallel_state(cp_size=4, cp_rank=2, cp_comm_type="a2a") + ) + + tokens = torch.arange(11) + + sliced = cp_utils.slice_with_cp(tokens, 0) + + torch.testing.assert_close(sliced, torch.tensor([4, 5, 10, 0])) + + +def test_slice_log_prob_with_cp_uses_sequence_shards_for_sglang_ulysses(monkeypatch): + monkeypatch.setattr( + cp_utils, "get_parallel_state", lambda: _parallel_state(cp_size=4, cp_rank=2, cp_comm_type="a2a") + ) + + log_probs = torch.arange(7, dtype=torch.float32) + + torch.testing.assert_close( + cp_utils.slice_log_prob_with_cp(log_probs, total_length=11, response_length=7), + torch.tensor([1.0, 2.0]), + ) + + +def test_regular_cp_still_uses_zigzag_response_slice(monkeypatch): + monkeypatch.setattr( + cp_utils, "get_parallel_state", lambda: _parallel_state(cp_size=2, cp_rank=1, cp_comm_type="p2p") + ) + + log_probs = torch.arange(7, dtype=torch.float32) + + sliced = cp_utils.slice_log_prob_with_cp(log_probs, total_length=11, response_length=7) + + torch.testing.assert_close(sliced, torch.tensor([0.0, 1.0, 2.0, 3.0, 4.0, 5.0])) + + +def test_local_response_masks_use_sequence_shards_for_sglang_ulysses(monkeypatch): + monkeypatch.setattr( + cp_utils, "get_parallel_state", lambda: _parallel_state(cp_size=4, cp_rank=2, cp_comm_type="a2a") + ) + + masks = [torch.ones(7)] + + local_masks = cp_utils.get_local_response_loss_masks([11], [7], masks) + + torch.testing.assert_close(local_masks[0], torch.ones(2)) diff --git a/tests/fast/rollout/generate_utils/test_prefill_logprobs.py b/tests/fast/rollout/generate_utils/test_prefill_logprobs.py new file mode 100644 index 00000000000..aa736a8194c --- /dev/null +++ b/tests/fast/rollout/generate_utils/test_prefill_logprobs.py @@ -0,0 +1,169 @@ +from types import SimpleNamespace + +import pytest + +from miles.rollout.generate_utils import prefill_logprobs +from miles.utils.types import Sample + + +@pytest.mark.asyncio +async def test_recompute_rollout_logprobs_via_prefill_uses_response_tail(monkeypatch): + sample = Sample( + tokens=[10, 11, 12, 20, 21, 22], + response_length=3, + rollout_log_probs=[-9.0, -9.0, -9.0], + status=Sample.Status.COMPLETED, + ) + args = SimpleNamespace(recompute_logprobs_via_prefill=True, sglang_enable_lora=False) + seen = {} + + async def fake_post(url, payload, headers=None): + seen["url"] = url + seen["payload"] = payload + seen["headers"] = headers + return { + "meta_info": { + "input_token_logprobs": [ + (None, 12), + (-0.1, 20), + (-0.2, 21), + (-0.3, 22), + ] + } + } + + monkeypatch.setattr(prefill_logprobs, "post", fake_post) + + await prefill_logprobs.recompute_rollout_logprobs_via_prefill( + args, + sample, + url="http://localhost/generate", + sampling_params={"temperature": 1, "max_new_tokens": 128}, + headers={"X-Test": "1"}, + ) + + assert sample.rollout_log_probs == [-0.1, -0.2, -0.3] + assert sample.metadata["rollout_log_probs_source"] == "sglang_prefill_recompute" + assert seen["url"] == "http://localhost/generate" + assert seen["headers"] == {"X-Test": "1"} + assert seen["payload"]["input_ids"] == sample.tokens + assert seen["payload"]["return_logprob"] is True + assert seen["payload"]["logprob_start_len"] == 2 + assert seen["payload"]["sampling_params"]["max_new_tokens"] == 0 + assert seen["payload"]["sampling_params"]["temperature"] == 0 + + +@pytest.mark.asyncio +async def test_recompute_rollout_logprobs_via_prefill_checks_token_alignment(monkeypatch): + sample = Sample(tokens=[10, 11, 20], response_length=1, status=Sample.Status.COMPLETED) + args = SimpleNamespace(recompute_logprobs_via_prefill=True, sglang_enable_lora=False) + + async def fake_post(url, payload, headers=None): + return {"meta_info": {"input_token_logprobs": [(None, 11), (-0.1, 999)]}} + + monkeypatch.setattr(prefill_logprobs, "post", fake_post) + + with pytest.raises(ValueError, match="token alignment mismatch"): + await prefill_logprobs.recompute_rollout_logprobs_via_prefill( + args, + sample, + url="http://localhost/generate", + sampling_params={}, + ) + + +@pytest.mark.asyncio +async def test_recompute_samples_flushes_each_batch_and_batches_prefill_score(monkeypatch): + samples = [ + Sample(tokens=[10, 11, 20], response_length=1, status=Sample.Status.COMPLETED), + Sample(tokens=[10, 11, 21], response_length=1, status=Sample.Status.COMPLETED), + ] + args = SimpleNamespace( + recompute_logprobs_via_prefill=True, + sglang_enable_lora=False, + sglang_router_policy="round_robin", + ) + calls = [] + + async def fake_post(url, payload, action="post", headers=None): + calls.append((url, payload, action, headers)) + if url.endswith("/flush_cache"): + return {} + return [ + {"meta_info": {"input_token_logprobs": [(None, 11), (-float(tokens[-1]), tokens[-1])]}} + for tokens in payload["input_ids"] + ] + + monkeypatch.setattr(prefill_logprobs, "post", fake_post) + + await prefill_logprobs.recompute_samples_rollout_logprobs_via_prefill( + args, + samples, + url="http://localhost/generate", + sampling_params={"max_new_tokens": 32}, + ) + + assert [sample.rollout_log_probs for sample in samples] == [[-20.0], [-21.0]] + assert [call[0] for call in calls] == [ + "http://localhost/flush_cache", + "http://localhost/generate", + ] + assert [call[2] for call in calls] == ["post", "post"] + assert calls[1][1]["input_ids"] == [[10, 11, 20], [10, 11, 21]] + assert calls[1][1]["logprob_start_len"] == 1 + + +@pytest.mark.asyncio +async def test_recompute_samples_batches_by_logprob_start_len(monkeypatch): + samples = [ + Sample(tokens=[10, 11, 20], response_length=1, status=Sample.Status.COMPLETED), + Sample(tokens=[10, 11, 12, 21], response_length=1, status=Sample.Status.COMPLETED), + Sample(tokens=[10, 11, 22], response_length=1, status=Sample.Status.COMPLETED), + ] + args = SimpleNamespace( + recompute_logprobs_via_prefill=True, + sglang_enable_lora=False, + sglang_router_policy="round_robin", + ) + calls = [] + + async def fake_post(url, payload, action="post", headers=None): + calls.append((url, payload, action, headers)) + if url.endswith("/flush_cache"): + return {} + return [ + { + "meta_info": { + "input_token_logprobs": [ + (None, tokens[-2]), + (-float(tokens[-1]), tokens[-1]), + ] + } + } + for tokens in payload["input_ids"] + ] + + monkeypatch.setattr(prefill_logprobs, "post", fake_post) + + await prefill_logprobs.recompute_samples_rollout_logprobs_via_prefill( + args, + samples, + url="http://localhost/generate", + sampling_params={"max_new_tokens": 32}, + ) + + assert [sample.rollout_log_probs for sample in samples] == [ + [-20.0], + [-21.0], + [-22.0], + ] + assert [call[0] for call in calls] == [ + "http://localhost/flush_cache", + "http://localhost/generate", + "http://localhost/flush_cache", + "http://localhost/generate", + ] + assert calls[1][1]["logprob_start_len"] == 1 + assert calls[1][1]["input_ids"] == [[10, 11, 20], [10, 11, 22]] + assert calls[3][1]["logprob_start_len"] == 2 + assert calls[3][1]["input_ids"] == [[10, 11, 12, 21]] diff --git a/tests/fast/true_on_policy/__init__.py b/tests/fast/true_on_policy/__init__.py new file mode 100644 index 00000000000..bd5c4fc756c --- /dev/null +++ b/tests/fast/true_on_policy/__init__.py @@ -0,0 +1 @@ +"""Tests for true-on-policy launch contracts.""" diff --git a/tests/fast/true_on_policy/test_config.py b/tests/fast/true_on_policy/test_config.py new file mode 100644 index 00000000000..5125bb16fc3 --- /dev/null +++ b/tests/fast/true_on_policy/test_config.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from miles.true_on_policy import ( + QWEN3_DENSE_TRUE_ON_POLICY_V1, + apply_true_on_policy_script_defaults, + build_true_on_policy_launch_plan, + get_megatron_model_type, + get_true_on_policy_contract, + get_true_on_policy_model_profile, +) + + +def _args(**overrides): + values = { + "true_on_policy": True, + "model_name": "Qwen3-4B", + "train_backend": "megatron", + "tensor_model_parallel_size": 2, + "context_parallel_size": 4, + "pipeline_model_parallel_size": 1, + "rollout_num_gpus_per_engine": 1, + "sglang_rl_on_policy_target": None, + "true_on_policy_contract": None, + "use_sequence_parallel": True, + } + values.update(overrides) + return SimpleNamespace(**values) + + +def test_qwen3_dense_profile_resolves_model_names(): + profile = get_true_on_policy_model_profile("Qwen3-4B") + contract = get_true_on_policy_contract("qwen3_dense_true_on_policy_v1") + + assert profile.family == "qwen3_dense" + assert profile.contract is QWEN3_DENSE_TRUE_ON_POLICY_V1 + assert profile.contract is contract + assert contract.schema.name == "qwen3_dense_true_on_policy_v1" + assert contract.schema.model_family == "qwen3_dense" + assert profile.supported_train_layouts == ("dp", "tp", "pp", "ulysses_cp") + assert profile.supported_rollout_layouts == ("dp", "tp") + assert profile.required_kernel_contracts == ("qwen3_dense_sglang_math",) + assert profile.logprob_contract == "sglang_prefill" + assert profile.supports_ulysses_cp + assert profile.supports_tp_invariant + assert get_megatron_model_type("Qwen3-4B") == "qwen3-4B" + assert get_megatron_model_type("Qwen3-4B-Instruct-2507") == "qwen3-4B-Instruct-2507" + + +def test_unknown_true_on_policy_model_fails_early(): + with pytest.raises(ValueError, match="does not have a model profile"): + get_true_on_policy_model_profile("unknown-model") + + +@pytest.mark.parametrize( + ("tp_size", "rollout_tp_size", "expected_target"), + [ + (1, 1, "fsdp"), + (2, 1, "fsdp_tp"), + (1, 2, "fsdp_tp"), + ], +) +def test_true_on_policy_target_is_derived_from_train_and_rollout_tp( + tp_size: int, + rollout_tp_size: int, + expected_target: str, +): + args = _args( + tensor_model_parallel_size=tp_size, + context_parallel_size=1, + rollout_num_gpus_per_engine=rollout_tp_size, + ) + + apply_true_on_policy_script_defaults(args) + plan = build_true_on_policy_launch_plan(args) + + assert args.sglang_rl_on_policy_target is None + assert plan.sglang_target == expected_target + assert plan.contract is QWEN3_DENSE_TRUE_ON_POLICY_V1 + assert plan.sglang_args.values == ( + "--sglang-enable-deterministic-inference", + "--sglang-true-on-policy-contract", + "qwen3_dense_true_on_policy_v1", + "--sglang-attention-backend", + "fa3", + ) + assert "--sglang-rl-on-policy-target" not in plan.train_args + + +def test_legacy_sglang_target_override_does_not_change_contract_policy(): + args = _args( + tensor_model_parallel_size=2, + context_parallel_size=1, + rollout_num_gpus_per_engine=1, + sglang_rl_on_policy_target="fsdp", + ) + + apply_true_on_policy_script_defaults(args) + plan = build_true_on_policy_launch_plan(args) + + assert args.sglang_rl_on_policy_target == "fsdp" + assert plan.sglang_target == "fsdp_tp" + assert plan.kernel_policy is not None + assert plan.kernel_policy.tp_invariant_row_linear + assert "--sglang-rl-on-policy-target" not in plan.train_args + assert "ROW_LINEAR_ENABLE_INV" not in plan.env_vars + + +def test_contract_object_owns_miles_kernel_policy_values(): + args = _args( + train_backend="megatron", + tensor_model_parallel_size=2, + context_parallel_size=1, + rollout_num_gpus_per_engine=1, + ) + + plan = build_true_on_policy_launch_plan(args) + + assert plan.kernel_policy is not None + assert plan.kernel_policy.contract is QWEN3_DENSE_TRUE_ON_POLICY_V1 + assert plan.kernel_policy.sglang_attention_backend == "fa3" + assert plan.kernel_policy.megatron_uses_sglang_backend + assert plan.kernel_policy.disable_rope_fusion + assert plan.kernel_policy.disable_bias_swiglu_fusion + assert plan.kernel_policy.batch_invariant_mode + assert plan.kernel_policy.tp_invariant_row_linear + assert plan.kernel_policy.deterministic_tp_allreduce + + +def test_megatron_true_on_policy_disables_sequence_parallel_and_enables_backend_flags(): + args = _args(train_backend="megatron", use_sequence_parallel=True) + + apply_true_on_policy_script_defaults(args) + plan = build_true_on_policy_launch_plan(args) + + assert args.use_sequence_parallel is False + assert "--use-sglang" not in plan.train_args + assert "--true-on-policy-contract qwen3_dense_true_on_policy_v1" in plan.train_args + assert "--sglang-true-on-policy-contract qwen3_dense_true_on_policy_v1" in plan.train_args + assert "--recompute-logprobs-via-prefill" in plan.train_args + assert "--batch-invariant-mode" in plan.train_args + assert "--no-rope-fusion" in plan.train_args + assert "ROW_LINEAR_ENABLE_INV" not in plan.env_vars + assert "MEGATRON_USE_DETERMINISTIC_ALLREDUCE" not in plan.env_vars + + +def test_megatron_tp2_cp4_normal_topology_has_complete_true_on_policy_contract(monkeypatch): + monkeypatch.delenv("NCCL_ALGO", raising=False) + + args = _args( + train_backend="megatron", + tensor_model_parallel_size=2, + context_parallel_size=4, + pipeline_model_parallel_size=1, + rollout_num_gpus_per_engine=8, + use_sequence_parallel=True, + ) + + apply_true_on_policy_script_defaults(args) + plan = build_true_on_policy_launch_plan(args) + + assert args.use_sequence_parallel is False + assert args.sglang_rl_on_policy_target is None + assert plan.parallel_layout is not None + assert plan.parallel_layout.train_tensor_parallel_size == 2 + assert plan.parallel_layout.train_context_parallel_size == 4 + assert plan.parallel_layout.rollout_num_gpus_per_engine == 8 + assert plan.parallel_layout.uses_train_tp + assert plan.parallel_layout.uses_ulysses_cp + assert plan.parallel_layout.uses_rollout_tp + assert plan.kernel_policy is not None + assert plan.kernel_policy.tp_invariant_row_linear + assert plan.kernel_policy.deterministic_tp_allreduce + assert plan.sglang_args.values == ( + "--sglang-enable-deterministic-inference", + "--sglang-true-on-policy-contract", + "qwen3_dense_true_on_policy_v1", + "--sglang-attention-backend", + "fa3", + ) + assert plan.megatron_args.values == ( + "--true-on-policy-contract", + "qwen3_dense_true_on_policy_v1", + "--transformer-impl", + "local", + "--use-cpu-initialization", + "--batch-invariant-mode", + "--no-bias-swiglu-fusion", + "--no-rope-fusion", + ) + assert plan.miles_args.values == ( + "--deterministic-mode", + "--true-on-policy-mode", + "--recompute-logprobs-via-prefill", + ) + assert plan.env_vars == { + "NCCL_ALGO": "Ring", + "NVTE_ALLOW_NONDETERMINISTIC_ALGO": "0", + "CUBLAS_WORKSPACE_CONFIG": ":4096:8", + } + + +def test_true_on_policy_contract_override_is_validated(): + args = _args(true_on_policy_contract="unknown_contract") + + with pytest.raises(ValueError, match="Unsupported true-on-policy contract"): + build_true_on_policy_launch_plan(args) + + +def test_fsdp_true_on_policy_uses_fsdp_attention_without_megatron_backend_flags(): + args = _args( + train_backend="fsdp", + tensor_model_parallel_size=1, + context_parallel_size=1, + rollout_num_gpus_per_engine=1, + ) + + apply_true_on_policy_script_defaults(args) + plan = build_true_on_policy_launch_plan(args) + + assert args.use_sequence_parallel is True + assert plan.sglang_target == "fsdp" + assert plan.fsdp_args.values == ("--attn-implementation", "flash_attention_3") + assert plan.megatron_args.values == () + assert "--attn-implementation flash_attention_3" in plan.train_args + assert "--use-sglang" not in plan.train_args + assert "ROW_LINEAR_ENABLE_INV" not in plan.env_vars + + +def test_off_policy_builds_empty_launch_plan_and_does_not_mutate_args(): + args = _args(true_on_policy=False, use_sequence_parallel=True) + + apply_true_on_policy_script_defaults(args) + plan = build_true_on_policy_launch_plan(args) + + assert args.use_sequence_parallel is True + assert args.sglang_rl_on_policy_target is None + assert not plan.enabled + assert plan.train_args == "" + assert plan.env_vars == {} diff --git a/tests/fast/true_on_policy/test_run_qwen3_4b.py b/tests/fast/true_on_policy/test_run_qwen3_4b.py new file mode 100644 index 00000000000..1abfa3af89e --- /dev/null +++ b/tests/fast/true_on_policy/test_run_qwen3_4b.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from scripts import run_qwen3_4b + + +def test_qwen3_script_true_on_policy_single_knob_expands_to_megatron_contract(monkeypatch): + captured = {} + + def fake_execute_train(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(run_qwen3_4b.U, "execute_train", fake_execute_train) + monkeypatch.setattr(run_qwen3_4b.U, "get_default_wandb_args", lambda *args, **kwargs: "") + + args = run_qwen3_4b.ScriptArgs( + run_id="unit-test", + model_name="Qwen3-4B", + true_on_policy=True, + enable_eval=False, + use_kl_loss=False, + ) + + assert args.sglang_rl_on_policy_target is None + assert args.use_sequence_parallel is False + + run_qwen3_4b.execute(args) + + train_args = captured["train_args"] + env_vars = captured["extra_env_vars"] + + assert "--true-on-policy-mode" in train_args + assert "--sglang-enable-deterministic-inference" in train_args + assert "--sglang-true-on-policy-contract qwen3_dense_true_on_policy_v1" in train_args + assert "--sglang-rl-on-policy-target" not in train_args + assert "--sglang-attention-backend fa3" in train_args + assert "--true-on-policy-contract qwen3_dense_true_on_policy_v1" in train_args + assert "--recompute-logprobs-via-prefill" in train_args + assert "--load /root/models/Qwen3-4B_torch_dist" in train_args + assert "--save /root/shared_data/unit-test/checkpoints" in train_args + assert "--use-sglang" not in train_args + assert "--batch-invariant-mode" in train_args + assert "--no-rope-fusion" in train_args + assert "--sequence-parallel" not in train_args + assert "ROW_LINEAR_ENABLE_INV" not in env_vars + assert "MEGATRON_USE_DETERMINISTIC_ALLREDUCE" not in env_vars + + +def test_qwen3_script_true_on_policy_tp2_cp4_normal_topology_contract(monkeypatch): + captured = {} + + def fake_execute_train(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(run_qwen3_4b.U, "execute_train", fake_execute_train) + monkeypatch.setattr(run_qwen3_4b.U, "get_default_wandb_args", lambda *args, **kwargs: "") + + args = run_qwen3_4b.ScriptArgs( + run_id="unit-test-tp2-cp4", + model_name="Qwen3-4B", + mode="normal", + true_on_policy=True, + enable_eval=False, + use_kl_loss=False, + tensor_model_parallel_size=2, + context_parallel_size=4, + pipeline_model_parallel_size=1, + cp_comm_type="a2a", + rollout_num_gpus=8, + rollout_num_gpus_per_engine=8, + ) + + assert args.sglang_rl_on_policy_target is None + assert args.use_sequence_parallel is False + + run_qwen3_4b.execute(args) + + train_args = captured["train_args"] + env_vars = captured["extra_env_vars"] + + assert "--tensor-model-parallel-size 2" in train_args + assert "--context-parallel-size 4" in train_args + assert "--cp-comm-type a2a" in train_args + assert "--rollout-num-gpus 8" in train_args + assert "--rollout-num-gpus-per-engine 8" in train_args + assert "--load /root/models/Qwen3-4B_torch_dist" in train_args + assert "--save /root/shared_data/unit-test-tp2-cp4/checkpoints" in train_args + assert "--sglang-true-on-policy-contract qwen3_dense_true_on_policy_v1" in train_args + assert "--sglang-rl-on-policy-target" not in train_args + assert "--sglang-attention-backend fa3" in train_args + assert "--recompute-logprobs-via-prefill" in train_args + assert "--use-sglang" not in train_args + assert "--batch-invariant-mode" in train_args + assert "--no-bias-swiglu-fusion" in train_args + assert "--no-rope-fusion" in train_args + assert "--sequence-parallel" not in train_args + assert env_vars["NCCL_ALGO"] == "Ring" + assert env_vars["NVTE_ALLOW_NONDETERMINISTIC_ALGO"] == "0" + assert env_vars["CUBLAS_WORKSPACE_CONFIG"] == ":4096:8" + assert "ROW_LINEAR_ENABLE_INV" not in env_vars + assert "MEGATRON_USE_DETERMINISTIC_ALLREDUCE" not in env_vars + + +def test_qwen3_script_off_policy_does_not_emit_true_on_policy_contract(monkeypatch): + captured = {} + + def fake_execute_train(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(run_qwen3_4b.U, "execute_train", fake_execute_train) + monkeypatch.setattr(run_qwen3_4b.U, "get_default_wandb_args", lambda *args, **kwargs: "") + + args = run_qwen3_4b.ScriptArgs( + run_id="unit-test", + model_name="Qwen3-4B", + true_on_policy=False, + enable_eval=False, + use_kl_loss=False, + ) + + run_qwen3_4b.execute(args) + + train_args = captured["train_args"] + env_vars = captured["extra_env_vars"] + + assert "--true-on-policy-mode" not in train_args + assert "--sglang-true-on-policy-contract" not in train_args + assert "--sglang-rl-on-policy-target" not in train_args + assert "--true-on-policy-contract" not in train_args + assert "--recompute-logprobs-via-prefill" not in train_args + assert "--use-sglang" not in train_args + assert "ROW_LINEAR_ENABLE_INV" not in env_vars diff --git a/tests/fast/utils/test_arguments.py b/tests/fast/utils/test_arguments.py index ef4faf6ea25..1f8c5c3b8a5 100644 --- a/tests/fast/utils/test_arguments.py +++ b/tests/fast/utils/test_arguments.py @@ -74,7 +74,9 @@ def _make_args( start_rollout_id: int | None = None, num_rollout: int = 10, eval_interval: int | None = 5, + save: str | None = "/tmp/checkpoint", save_interval: int | None = 5, + save_retain_interval: int | None = 10, ) -> SimpleNamespace: return SimpleNamespace( dumper_enable=dumper_enable, @@ -84,7 +86,9 @@ def _make_args( start_rollout_id=start_rollout_id, num_rollout=num_rollout, eval_interval=eval_interval, + save=save, save_interval=save_interval, + save_retain_interval=save_retain_interval, ) def test_noop_when_dumper_disabled(self) -> None: @@ -100,7 +104,9 @@ def test_noop_when_dumper_disabled(self) -> None: assert args.rollout_health_check_interval == 30.0 assert args.num_rollout == 10 assert args.eval_interval == 5 + assert args.save == "/tmp/checkpoint" assert args.save_interval == 5 + assert args.save_retain_interval == 10 def test_disables_all_heartbeats(self) -> None: args = self._make_args( @@ -118,12 +124,24 @@ def test_forces_single_rollout(self) -> None: args = self._make_args(dumper_enable=True, num_rollout=100) _maybe_apply_dumper_overrides(args) + assert args.start_rollout_id == 0 assert args.num_rollout == 1 assert args.eval_interval is None + assert args.save is None assert args.save_interval is None + assert args.save_retain_interval is None def test_respects_start_rollout_id(self) -> None: args = self._make_args(dumper_enable=True, start_rollout_id=5, num_rollout=100) _maybe_apply_dumper_overrides(args) assert args.num_rollout == 6 + + +def test_recompute_logprobs_via_prefill_flag_is_parsed(): + parser = argparse.ArgumentParser() + get_miles_extra_args_provider()(parser) + + args = parser.parse_args(["--recompute-logprobs-via-prefill"] + REQUIRED_ARGS) + + assert args.recompute_logprobs_via_prefill is True diff --git a/tests/fast/utils/test_dumper_utils.py b/tests/fast/utils/test_dumper_utils.py index 45c930720f0..b4d00c807a7 100644 --- a/tests/fast/utils/test_dumper_utils.py +++ b/tests/fast/utils/test_dumper_utils.py @@ -2,6 +2,7 @@ register_cpu_ci(est_time=60, suite="stage-a-fast") +from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest @@ -32,3 +33,29 @@ def test_passes_args_and_returns_result(self, setup) -> None: result = wrapped("my_iter", "my_model", extra=True) inner.assert_called_once_with("my_iter", "my_model", extra=True) assert result == ("output", "loss_fn") + + +def test_sglang_env_includes_startup_dumper_settings() -> None: + args = SimpleNamespace( + dumper_enable=False, + dumper_inference=["enable=true", "non_intrusive_mode=all"], + dumper_source_patcher_config_inference="/tmp/patcher.yaml", + ) + + env = dumper_utils.get_sglang_env(args) + + assert env == { + "DUMPER_SERVER_PORT": "reuse", + "DUMPER_NON_INTRUSIVE_MODE": "all", + "DUMPER_SOURCE_PATCHER_CONFIG": "/tmp/patcher.yaml", + } + + +def test_sglang_env_disabled_when_inference_phase_disabled() -> None: + args = SimpleNamespace( + dumper_enable=False, + dumper_inference=["non_intrusive_mode=all"], + dumper_source_patcher_config_inference=None, + ) + + assert dumper_utils.get_sglang_env(args) == {} diff --git a/tests/fast/utils/test_true_on_policy_logprobs.py b/tests/fast/utils/test_true_on_policy_logprobs.py new file mode 100644 index 00000000000..20b485dc8d2 --- /dev/null +++ b/tests/fast/utils/test_true_on_policy_logprobs.py @@ -0,0 +1,86 @@ +import torch + +from miles.utils.ppo_utils import ( + _calculate_log_probs_and_entropy_true_on_policy, + _prepare_true_on_policy_full_logits, + _split_replicated_loss_gather_grad, +) + + +def test_true_on_policy_logprobs_tp1_truncate_after_real_vocab(): + logits = torch.tensor( + [ + [1.0, 0.0, -1.0, 3.0, 40.0, 50.0], + [2.0, 1.0, 0.5, -0.5, 60.0, 70.0], + ], + dtype=torch.float16, + ) + tokens = torch.tensor([3, 0], dtype=torch.long) + + log_probs, entropy = _calculate_log_probs_and_entropy_true_on_policy( + logits, + tokens, + None, + with_entropy=True, + vocab_size=4, + ) + + expected_log_probs_full = torch.log_softmax(logits[:, :4], dim=-1) + expected_log_probs = expected_log_probs_full.gather(dim=-1, index=tokens.unsqueeze(-1)).squeeze(-1) + expected_entropy = -(expected_log_probs_full.exp() * expected_log_probs_full).sum(dim=-1) + + torch.testing.assert_close(log_probs, expected_log_probs) + torch.testing.assert_close(entropy, expected_entropy) + + +def test_true_on_policy_fake_tp_vocab_gather_truncates_before_log_softmax(): + shard_0 = torch.tensor( + [ + [5.0, 1.0, -2.0, 0.0], + [0.0, 3.0, -4.0, 1.0], + ], + dtype=torch.float16, + ) + shard_1 = torch.tensor( + [ + [2.0, 4.0, 30.0, 40.0], + [-1.0, 2.0, 50.0, 60.0], + ], + dtype=torch.float16, + ) + tokens = torch.tensor([5, 4], dtype=torch.long) + + gathered_logits = _prepare_true_on_policy_full_logits((shard_0, shard_1), vocab_size=6) + log_probs, _ = _calculate_log_probs_and_entropy_true_on_policy( + gathered_logits, + tokens, + None, + vocab_size=6, + ) + + expected_full_logits = torch.cat([shard_0, shard_1], dim=-1)[:, :6] + expected_log_probs = torch.log_softmax(expected_full_logits, dim=-1) + expected_selected = expected_log_probs.gather(dim=-1, index=tokens.unsqueeze(-1)).squeeze(-1) + + torch.testing.assert_close(gathered_logits, expected_full_logits) + torch.testing.assert_close(log_probs, expected_selected) + + +def test_true_on_policy_replicated_loss_gather_backward_splits_without_tp_sum(): + grad_output = torch.arange(12, dtype=torch.float32).reshape(2, 6) + + rank0_grad = _split_replicated_loss_gather_grad( + grad_output, + rank=0, + world_size=2, + local_last_dim=3, + ) + rank1_grad = _split_replicated_loss_gather_grad( + grad_output, + rank=1, + world_size=2, + local_last_dim=3, + ) + + torch.testing.assert_close(rank0_grad, grad_output[:, :3]) + torch.testing.assert_close(rank1_grad, grad_output[:, 3:]) From f7960bb21f529e42724437915252cbac1f4f4502 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Mon, 18 May 2026 03:04:18 -0700 Subject: [PATCH 0057/1130] [CI] Replace sglang ci fp8 (#1143) --- tests/e2e/sglang/test_session_server_multi_role.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/sglang/test_session_server_multi_role.py b/tests/e2e/sglang/test_session_server_multi_role.py index 023111b8609..6545f25e600 100644 --- a/tests/e2e/sglang/test_session_server_multi_role.py +++ b/tests/e2e/sglang/test_session_server_multi_role.py @@ -55,7 +55,7 @@ class ModelConfig: tool_call_failure_mode="append_tool", ), "qwen3-tool-user": ModelConfig( - model_name="Qwen/Qwen3-30B-A3B", + model_name="Qwen/Qwen3-30B-A3B-FP8", reasoning_parser="qwen3", tool_call_parser="qwen25", tito_model="qwen3", @@ -65,7 +65,7 @@ class ModelConfig: tool_call_failure_mode="append_tool", ), "qwen35-tool-user": ModelConfig( - model_name="Qwen/Qwen3.5-35B-A3B", + model_name="Qwen/Qwen3.5-35B-A3B-FP8", reasoning_parser="qwen3", tool_call_parser="qwen3_coder", tito_model="qwen35", @@ -75,7 +75,7 @@ class ModelConfig: tool_call_failure_mode="append_tool", ), "qwennext-tool-user": ModelConfig( - model_name="Qwen/Qwen3-Next-80B-A3B-Thinking", + model_name="Qwen/Qwen3-Next-80B-A3B-Thinking-FP8", reasoning_parser="qwen3", tool_call_parser="qwen25", tito_model="qwennext", From 9399a60f1b37bcd9626e22f53a6f9b6de6c7b62f Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Mon, 18 May 2026 12:01:04 -0700 Subject: [PATCH 0058/1130] [CI] tiny workaround fix ci worker tag. (#1148) --- .github/workflows/_run-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_run-ci.yml b/.github/workflows/_run-ci.yml index 35a6a3c420b..866f28c55ff 100644 --- a/.github/workflows/_run-ci.yml +++ b/.github/workflows/_run-ci.yml @@ -12,7 +12,7 @@ on: runs_on: type: string required: false - default: 'self-hosted' + default: '8gpu' container_image: type: string required: false From 7a6cf48e6f8fc7dbf3c30be6ccf42af3879a4ed6 Mon Sep 17 00:00:00 2001 From: Shi-Dong Date: Tue, 19 May 2026 08:58:40 +0800 Subject: [PATCH 0059/1130] add Code of Conduct (#1145) --- CODE_OF_CONDUCT.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..4b089f4a870 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,92 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, maintainers, and leaders of the Miles community pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, caste, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, respectful, and healthy technical community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for the Miles community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, backgrounds, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility, apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best for the overall community and the long-term health of the project +* Collaborating in good faith across research, engineering, operations, documentation, and user-support discussions +* Helping maintain a professional environment in issues, pull requests, discussions, chat channels, meetings, documentation, examples, and community events + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, personal attacks, or political attacks unrelated to the project +* Public or private harassment +* Publishing others' private information, such as a physical address, private email address, phone number, credentials, private logs, or internal deployment details, without explicit permission +* Deliberately disrupting project work, reviews, discussions, meetings, issue triage, release processes, or community spaces +* Repeatedly ignoring maintainer guidance, review boundaries, or moderation decisions +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior. They will take appropriate and fair corrective action in response to behavior that they deem inappropriate, threatening, offensive, harmful, or disruptive to the Miles community. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, documentation, wiki edits, issues, pull requests, discussions, and other contributions that are not aligned to this Code of Conduct. When appropriate, they will communicate reasons for moderation decisions. + +## Scope + +This Code of Conduct applies within all Miles community spaces, including but not limited to: + +* GitHub repositories, issues, pull requests, discussions, and review comments +* Documentation, examples, tutorials, and project websites +* Community chat channels, mailing lists, forums, and meetings +* Conferences, meetups, workshops, demos, and other online or offline events connected to the project + +This Code of Conduct also applies when an individual is officially representing the Miles project or community in public spaces. Examples of representing the project include using an official project email address, posting through an official social media account, publishing official documentation or release notes, speaking on behalf of the project at an event, or acting as an appointed project representative. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at: + +miles@radixark.ai + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. Reports should be handled with appropriate confidentiality, and information should be shared only with those who need it to review, investigate, or resolve the incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for actions they deem to be in violation of this Code of Conduct. + +### 1. Correction + +**Community Impact:** Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence:** A private, written correction from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested where appropriate. + +### 2. Warning + +**Community Impact:** A violation through a single incident or a series of actions. + +**Consequence:** A warning with consequences for continued behavior. The person may be asked to avoid interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels such as social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact:** A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence:** A temporary ban from any sort of interaction or public communication with the Miles community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact:** Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, aggression toward others, or disparagement of classes of individuals. + +**Consequence:** A permanent ban from any sort of public interaction within the Miles community. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at: + +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html + +Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder. From 2b5b7fc1f43c8c79740ebd9ff9bc962ed89ab2c3 Mon Sep 17 00:00:00 2001 From: maocheng23 <35615230+maocheng23@users.noreply.github.com> Date: Tue, 19 May 2026 12:03:15 -0700 Subject: [PATCH 0060/1130] Fix assignment of train_scored_log_probs variable (#1151) --- miles/backends/training_utils/loss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miles/backends/training_utils/loss.py b/miles/backends/training_utils/loss.py index cbd36060ec5..bea14cbc301 100644 --- a/miles/backends/training_utils/loss.py +++ b/miles/backends/training_utils/loss.py @@ -806,7 +806,7 @@ def policy_loss_function( if log_probs.numel() == 0: loss += 0 * logits.sum() - train_scored_log_probs = log_probs + train_scored_log_probs = old_log_probs train_rollout_logprob_abs_diff = None train_rollout_kl = None if "rollout_log_probs" in batch and batch["rollout_log_probs"]: From 14fafadaa4dcf030534f06f57bb2dd96ed7c5fa2 Mon Sep 17 00:00:00 2001 From: Zhichen Zeng Date: Tue, 19 May 2026 23:18:19 -0700 Subject: [PATCH 0061/1130] fix: expert_overlap and pp issues in nemotron-3-super (#1150) --- .../backends/megatron_utils/model_provider.py | 5 + miles_plugins/megatron_bridge/nemotron_h.py | 25 +++ scripts/models/nemotron-3-super-120b-a12b.sh | 54 +++++ scripts/run-nemotron-3-super-120b-a12b.sh | 188 ++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 scripts/models/nemotron-3-super-120b-a12b.sh create mode 100755 scripts/run-nemotron-3-super-120b-a12b.sh diff --git a/miles/backends/megatron_utils/model_provider.py b/miles/backends/megatron_utils/model_provider.py index e7c24a31913..116675e6118 100644 --- a/miles/backends/megatron_utils/model_provider.py +++ b/miles/backends/megatron_utils/model_provider.py @@ -120,6 +120,11 @@ def wrapped_bridge_provider( pg_collection=None, ) -> GPTModel: assert config is None, "miles builds the config from args, so it expects config to be None" + # PP>1 paths in megatron.bridge providers (e.g. mamba_provider) read + # self._pg_collection.pp during provide(); without forwarding the + # caller's pg_collection here, those code paths hit AttributeError. + if pg_collection is not None: + provider._pg_collection = pg_collection return provider.provide(pre_process=pre_process, post_process=post_process, vp_stage=vp_stage) return wrapped_bridge_provider diff --git a/miles_plugins/megatron_bridge/nemotron_h.py b/miles_plugins/megatron_bridge/nemotron_h.py index 357c4bc43fc..8b587eb9aef 100644 --- a/miles_plugins/megatron_bridge/nemotron_h.py +++ b/miles_plugins/megatron_bridge/nemotron_h.py @@ -36,6 +36,11 @@ "decoder.layers.*.mlp.experts.local_experts.*.linear_fc2.weight": "backbone.layers.*.mixer.experts.*.down_proj.weight", "decoder.layers.*.mlp.shared_experts.linear_fc1.weight": "backbone.layers.*.mixer.shared_experts.up_proj.weight", "decoder.layers.*.mlp.shared_experts.linear_fc2.weight": "backbone.layers.*.mixer.shared_experts.down_proj.weight", + # MoE latent projections (Super-120B-A12B uses moe_latent_size=1024 to bottleneck + # expert input/output; Nano variants leave moe_latent_size=None and these tensors + # do not exist, so the mappings stay unreferenced). + "decoder.layers.*.mlp.fc1_latent_proj.weight": "backbone.layers.*.mixer.fc1_latent_proj.weight", + "decoder.layers.*.mlp.fc2_latent_proj.weight": "backbone.layers.*.mixer.fc2_latent_proj.weight", } # HF name → provider name → cast. Miles' generic ``model_provider.py`` does not @@ -104,6 +109,26 @@ def provider_bridge(self, hf_pretrained): val = getattr(hf, hf_name, None) if val is not None: setattr(provider, prov_name, cast(val)) + + # MoE latent bottleneck (Super-120B-A12B). Upstream NemotronHBridge does + # not surface this; without it, fc1/fc2 latent projections are not built + # and routed experts get the wrong input dim (hidden_size vs moe_latent_size). + # Megatron's moe_layer.preprocess() asserts the two are mutually exclusive, + # so disable shared-expert overlap whenever a latent bottleneck is in use. + latent_size = getattr(hf, "moe_latent_size", None) + if latent_size is not None: + provider.moe_latent_size = int(latent_size) + provider.moe_shared_expert_overlap = False + + # MTP head: Super-120B's HF config has num_nextn_predict_layers=1, which + # the base CONFIG_MAPPING translates to provider.mtp_num_layers=1. Miles' + # generic training loop only feeds MTP labels when --enable-mtp-training is + # set, so building the head adds dead weights. Disable unless explicitly + # requested via the MILES_NEMOTRONH_KEEP_MTP env var. + import os + + if not os.environ.get("MILES_NEMOTRONH_KEEP_MTP"): + provider.mtp_num_layers = None return provider def mapping_registry(self): diff --git a/scripts/models/nemotron-3-super-120b-a12b.sh b/scripts/models/nemotron-3-super-120b-a12b.sh new file mode 100644 index 00000000000..13e3eceb8bb --- /dev/null +++ b/scripts/models/nemotron-3-super-120b-a12b.sh @@ -0,0 +1,54 @@ +# NVIDIA Nemotron-3-Super-120B-A12B (BF16, MoE nemotron_h = hybrid Mamba + Attention + MoE). +# HF config (verified 2026-05-01): +# num_hidden_layers=88 hidden_size=4096 num_attention_heads=32 num_key_value_heads=2 +# head_dim=128 intermediate_size=2688 moe_intermediate_size=2688 +# n_routed_experts=512 num_experts_per_tok=22 n_shared_experts=1 +# moe_shared_expert_intermediate_size=5376 routed_scaling_factor=5.0 +# n_group=1 topk_group=1 sigmoid routing + aux-free expert bias +# The AutoBridge path (--megatron-to-hf-mode bridge) + miles NemotronHBridge MoE shim +# (see miles_plugins/megatron_bridge/nemotron_h.py) construct the provider and +# HF<->Megatron mapping_registry at load time. Attention-side structural args go +# in MODEL_ARGS for miles' arg parser. + +MODEL_ARGS=( + --disable-bias-linear + --group-query-attention + --num-attention-heads 32 + --num-query-groups 2 + --kv-channels 128 + --num-layers 88 + --hidden-size 4096 + --ffn-hidden-size 2688 + --normalization RMSNorm + --position-embedding-type none + --vocab-size 131072 + --make-vocab-size-divisible-by 128 + --untie-embeddings-and-output-weights + + # MoE specifics + --num-experts 512 + --moe-router-topk 22 + --moe-ffn-hidden-size 2688 + --moe-shared-expert-intermediate-size 5376 + # Super-120B bottlenecks expert input/output through a 1024-dim latent. + # Routed experts run on moe_latent_size, NOT hidden_size, with two extra + # fc1/fc2 latent projections per MoE layer. The miles NemotronH bridge + # surfaces this from HF config; the CLI arg keeps Megatron's parser happy. + --moe-latent-size 1024 + --moe-router-score-function sigmoid + --moe-router-enable-expert-bias + --moe-grouped-gemm + --moe-router-dtype fp32 + # Routing: HF config has n_group=1 (MoE groups), topk_group=1, + # routed_scaling_factor=5.0. With n_group=1, group-limited routing is a + # no-op (single group of 512). `n_groups=8` in HF is Mamba groups — + # unrelated to MoE. + --moe-router-num-groups 1 + --moe-router-group-topk 1 + --moe-router-topk-scaling-factor 5.0 + --moe-router-pre-softmax + # Match nano-30b-a3b (known-working MoE RL on nemotron_h) settings. + --moe-router-load-balancing-type seq_aux_loss + --moe-router-bias-update-rate 0 + --moe-aux-loss-coeff 0 +) diff --git a/scripts/run-nemotron-3-super-120b-a12b.sh b/scripts/run-nemotron-3-super-120b-a12b.sh new file mode 100755 index 00000000000..c56cb1ae3c2 --- /dev/null +++ b/scripts/run-nemotron-3-super-120b-a12b.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +# Two-node (2x 8x H200, 16 GPUs) launcher for Nemotron-3-Super-120B-A12B. +# Usage on each pod: +# head: bash run-nemotron-3-super-120b-a12b.sh head +# worker: bash run-nemotron-3-super-120b-a12b.sh worker + +ROLE=${1:?Usage: $0 } +HEAD_IP=${2:?Usage: $0 } + +cd "$(dirname -- "${BASH_SOURCE[0]}")/.." + +# 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 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)" + +# Worker just joins the head's ray cluster and blocks. +if [[ "$ROLE" == "worker" ]]; then + for i in $(seq 1 60); do + if nc -z "$HEAD_IP" 6379 2>/dev/null; then break; fi + echo "waiting for head $HEAD_IP:6379 ..." + sleep 5 + done + ray start --address="${HEAD_IP}:6379" --num-gpus=8 --disable-usage-stats --block + exit 0 +fi + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${SCRIPT_DIR}/models/nemotron-3-super-120b-a12b.sh" + +MODELS_DIR=${MODELS_DIR:-/cluster_public/miles_data/models} +DATASETS_DIR=${DATASETS_DIR:-/cluster_public/miles_data/datasets} + +CKPT_ARGS=( + --hf-checkpoint $MODELS_DIR/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 + --ref-load $MODELS_DIR/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 + --save $MODELS_DIR/nemotron-3-super-120b-a12b_miles + --save-interval 20 + --megatron-to-hf-mode bridge +) + +ROLLOUT_ARGS=( + --prompt-data $DATASETS_DIR/dapo-math-17k/dapo-math-17k.jsonl + --input-key prompt + --label-key label + --apply-chat-template + --rollout-shuffle + --rm-type deepscaler + --num-rollout 10 + --rollout-batch-size 32 + --n-samples-per-prompt 4 + --rollout-max-response-len 1024 + --rollout-temperature 1 + + --global-batch-size 128 + --balance-data +) + +EVAL_ARGS=( +) + +PERF_ARGS=( + --tensor-model-parallel-size 4 + --sequence-parallel + --pipeline-model-parallel-size 2 + --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 1024 + --log-probs-chunk-size 128 +) + +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 nemotron-3-super-120b-a12b + # --wandb-key ${WANDB_KEY} +) + +SGLANG_ARGS=( + --rollout-num-gpus-per-engine 8 + --sglang-mem-fraction-static 0.7 + # Replay the exact rollout routing during training forward so + # train logprobs match rollout logprobs (needed for MoE). + --use-miles-router + --use-rollout-routing-replay +) + +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 + --attention-backend auto +) + +# launch the master node of ray in container +export MASTER_ADDR=${HEAD_IP} +ray start --head --node-ip-address ${MASTER_ADDR} --num-gpus 8 --disable-usage-stats --dashboard-host=0.0.0.0 --dashboard-port=8265 + +# wait for the worker to join so the cluster has 16 GPUs before submitting +echo "Waiting for ray cluster to have 16 GPUs..." +for i in $(seq 1 120); do + if ray status 2>/dev/null | grep -q '16.0 GPU'; then + echo "[ray] cluster ready: 16 GPUs" + break + fi + sleep 5 +done +ray status + +# 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}\" + } +}" + +ray job submit --address="http://127.0.0.1:8265" \ + --runtime-env-json="${RUNTIME_ENV_JSON}" \ + -- python3 train.py \ + --actor-num-nodes 2 \ + --actor-num-gpus-per-node 8 \ + --rollout-num-gpus 16 \ + --colocate \ + ${MODEL_ARGS[@]} \ + ${CKPT_ARGS[@]} \ + ${ROLLOUT_ARGS[@]} \ + ${OPTIMIZER_ARGS[@]} \ + ${GRPO_ARGS[@]} \ + ${WANDB_ARGS[@]} \ + ${PERF_ARGS[@]} \ + ${EVAL_ARGS[@]} \ + ${SGLANG_ARGS[@]} \ + ${MISC_ARGS[@]} From 595fac55dafa6761a6d8992175a71db36583bde0 Mon Sep 17 00:00:00 2001 From: Yuzhen Zhou <82826991+zyzshishui@users.noreply.github.com> Date: Wed, 20 May 2026 19:22:59 -0700 Subject: [PATCH 0062/1130] fix: skip actor CPU backup for disaggregated Megatron training (#1161) --- miles/backends/megatron_utils/actor.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/miles/backends/megatron_utils/actor.py b/miles/backends/megatron_utils/actor.py index 54ca4b81a5d..858c1248239 100644 --- a/miles/backends/megatron_utils/actor.py +++ b/miles/backends/megatron_utils/actor.py @@ -143,7 +143,8 @@ def init( single_tag=None if args.enable_weights_backuper else "actor", ) self._active_model_tag: str | None = "actor" - self.weights_backuper.backup("actor") + if self._enable_weight_backup: + self.weights_backuper.backup("actor") if with_ref: self.load_other_checkpoint("ref", args.ref_load) @@ -221,7 +222,14 @@ def wake_up(self) -> None: reload_process_groups() print_memory("after wake_up model") + @property + def _enable_weight_backup(self) -> bool: + """Weight backup is only needed for CPU-side model switching or colocated tensor weight sync.""" + return self.with_ref or self.args.keep_old_actor or self.args.colocate + def _switch_model(self, target_tag: str) -> None: + if not self._enable_weight_backup: + return if target_tag not in self.weights_backuper.backup_tags: raise ValueError(f"Cannot switch to unknown model tag: {target_tag}") self.weights_backuper.restore(target_tag) @@ -452,7 +460,10 @@ def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None: m.clear_all() # update the cpu actor weight to the latest model - self.weights_backuper.backup("actor") + if self._enable_weight_backup: + self.weights_backuper.backup("actor") + else: + torch.cuda.synchronize() # Update ref model if needed if ( From ba8d54d5eb5653455e9febdd883d6e7e3ad93405 Mon Sep 17 00:00:00 2001 From: Ying Sheng Date: Thu, 21 May 2026 00:34:59 -0700 Subject: [PATCH 0063/1130] Add @guapisolo as code owner for workflows (#1167) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f55b2ca702d..e1b8a4d0545 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ -.github/CODEOWNERS @fzyzcjy @Ying1123 -.github/workflows/ @yushengsu-thu +/.github/CODEOWNERS @fzyzcjy @Ying1123 +/.github/workflows/ @yushengsu-thu @guapisolo /miles/ @fzyzcjy @yueming-yuan /miles/backends/ @fzyzcjy @yueming-yuan @maocheng23 @Zhichenzzz /miles/backends/megatron_utils/ @fzyzcjy @yueming-yuan @maocheng23 @yushengsu-thu @Zhichenzzz From cdb6f00325080eb57e061bfe2f66a4316826f292 Mon Sep 17 00:00:00 2001 From: Jiajun Li <48857426+guapisolo@users.noreply.github.com> Date: Thu, 21 May 2026 12:20:41 -0700 Subject: [PATCH 0064/1130] [CI] Refactor CI workflow into stage classified by hardware and gpu num, fix buggy tests (#1149) --- .claude/skills/manage-gh-runners/SKILL.md | 89 +++++ .../manage-gh-runners/manage-runners.sh | 149 ++++++++ .claude/skills/setup-ci-host/SKILL.md | 103 ++++++ .claude/skills/setup-ci-host/setup-host.sh | 162 +++++++++ .github/workflows/_run-ci.yml | 47 ++- .github/workflows/pr-test.yml | 193 ++++------- .gitignore | 3 + miles/backends/training_utils/ci_utils.py | 2 +- miles/backends/training_utils/log_utils.py | 3 +- .../utils/test_utils/session_verify_agent.py | 4 +- .../utils/test_utils/session_verify_runner.py | 3 +- miles/utils/test_wandb_utils.py | 56 +++ miles/utils/tracking_utils/wandb_utils.py | 10 +- tests/ci/README.md | 117 ++++++- tests/ci/ci_register.py | 241 ++++++++----- tests/ci/github_runner/.gitignore | 3 +- tests/ci/github_runner/docker-compose.yml | 26 +- tests/ci/gpu_lock_exec.py | 198 ----------- tests/ci/labels.py | 28 ++ tests/ci/run_suite.py | 143 ++++++-- tests/ci/test_ci_register.py | 239 +++++++++++++ tests/ci/test_labels.py | 33 ++ tests/ci/test_run_suite.py | 324 ++++++++++++++++++ tests/e2e/ckpt/test_glm47_flash_ckpt.py | 39 ++- tests/e2e/ckpt/test_qwen3_4B_ckpt.py | 16 +- .../fsdp/test_qwen3_0.6B_fsdp_distributed.py | 14 +- .../test_qwen3_0.6B_megatron_fsdp_align.py | 8 +- .../fsdp/test_qwen3_4B_fsdp_true_on_policy.py | 7 +- tests/e2e/fsdp/test_qwen3_vl_4B_fsdp.py | 6 +- tests/e2e/long/test_qwen2.5_0.5B_gsm8k.py | 16 +- .../e2e/long/test_qwen2.5_0.5B_gsm8k_async.py | 18 +- tests/e2e/lora/test_lora_qwen2.5_0.5B.py | 6 +- .../_common.py} | 106 +++--- .../megatron/test_glm47_flash/test_r3_mtp.py | 15 + .../test_glm47_flash/test_r3_mtp_deepep.py | 21 ++ .../megatron/test_glm5_744b_a40b_4layer.py | 6 +- .../megatron/test_mimo_7B_mtp_only_grad.py | 11 +- .../e2e/megatron/test_quick_start_glm4_9B.py | 15 +- .../_common.py} | 165 +++++---- .../test_qwen3_30B_A3B/test_baseline.py | 24 ++ .../test_qwen3_30B_A3B/test_deepep_fp8.py | 24 ++ .../test_deepep_fp8_bridge.py | 26 ++ .../test_qwen3_30B_A3B/test_int4_rollout.py | 24 ++ .../test_qwen3_30B_A3B/test_r3_baseline.py | 24 ++ .../test_qwen3_30B_A3B/test_r3_deepep_fp8.py | 30 ++ tests/e2e/megatron/test_qwen3_30B_A3B_r3.py | 159 --------- tests/e2e/megatron/test_qwen3_4B_p2p.py | 24 +- tests/e2e/megatron/test_qwen3_4B_ppo.py | 27 +- tests/e2e/megatron/test_qwen3_5_35B_A3B_cp.py | 17 +- .../test_qwen3_0.6B_parallel_check.py | 14 +- .../sglang/test_chat_input_ids_equivalence.py | 2 +- .../e2e/sglang/test_r3_router_equivalence.py | 6 +- .../sglang/test_session_server_multi_role.py | 194 ----------- .../test_session_server_multi_role/_common.py | 50 +++ .../test_glm47.py | 26 ++ .../test_minimax_m27.py | 54 +++ .../test_nemotron3.py | 39 +++ .../test_qwen3.py | 24 ++ .../test_qwen35.py | 24 ++ .../test_qwennext.py | 24 ++ .../sglang/test_tito_logprob_equivalence.py | 196 ----------- tests/e2e/sglang_config/test_sglang_config.py | 8 +- .../test_sglang_config_mixed_offload.py | 8 +- .../test_sglang_config_mixed_offload_ft.py | 8 +- tests/e2e/short/test_dumper.py | 6 + .../test_qwen2.5_0.5B_gsm8k_async_short.py | 12 +- .../short/test_qwen2.5_0.5B_gsm8k_short.py | 12 +- .../test_qwen3_0.6B_fsdp_colocated_2xGPU.py | 16 +- tests/e2e/short/test_run_megatron.py | 7 +- .../megatron_utils/test_fp32_param_utils.py | 2 +- .../test_lora_checkpoint_helpers.py | 2 +- .../test_lora_hf_weight_iterator.py | 2 +- .../test_lora_model_branches.py | 2 +- .../megatron_utils/test_lora_update_weight.py | 2 +- .../megatron_utils/test_lora_utils.py | 2 +- .../test_lora_weight_sync_validation.py | 2 +- .../test_model_provider_true_on_policy.py | 2 +- .../test_qwen2_true_on_policy_conversion.py | 2 +- .../rollout/generate_hub/test_multi_turn.py | 2 +- .../rollout/generate_hub/test_single_turn.py | 2 +- .../generate_hub/test_tool_call_utils.py | 5 +- .../test_openai_endpoint_utils.py | 2 +- .../generate_utils/test_sample_utils.py | 2 +- .../integration/test_agent_metadata.py | 2 +- .../integration/test_basic.py | 2 +- .../integration/test_deterministic.py | 2 +- .../integration/test_dynamic_filter.py | 2 +- .../integration/test_group_rm.py | 2 +- .../integration/test_multi_sample.py | 2 +- .../integration/test_multi_turn.py | 2 +- .../integration/test_over_sampling.py | 2 +- .../integration/test_sample_filter.py | 2 +- .../integration/test_semaphore.py | 2 +- .../inference_rollout/test_compatibility.py | 2 +- tests/fast/rollout/rm_hub/test_deepscaler.py | 2 +- tests/fast/rollout/rm_hub/test_f1.py | 2 +- tests/fast/rollout/rm_hub/test_gpqa.py | 2 +- .../rollout/rm_hub/test_math_dapo_utils.py | 2 +- tests/fast/rollout/rm_hub/test_math_utils.py | 2 +- tests/fast/rollout/rm_hub/test_rm_hub.py | 2 +- tests/fast/router/test_linear_trajectory.py | 2 +- tests/fast/router/test_router.py | 2 +- .../router/test_session_pretokenized_e2e.py | 2 +- .../router/test_session_race_conditions.py | 2 +- tests/fast/router/test_sessions.py | 2 +- tests/fast/test_megatron_cli_flags.py | 2 +- .../test_pretokenized_chat.py | 2 +- .../test_pretokenized_via_tito.py | 2 +- .../chat_template_utils/test_template.py | 2 +- .../test_tito_tokenizer.py | 2 +- .../test_token_seq_comparator.py | 2 +- tests/fast/utils/test_arguments.py | 2 +- tests/fast/utils/test_async_utils.py | 2 +- tests/fast/utils/test_dumper_utils.py | 2 +- tests/fast/utils/test_env_report.py | 2 +- tests/fast/utils/test_http_utils.py | 2 +- tests/fast/utils/test_logging_utils.py | 2 +- tests/fast/utils/test_lora_arguments.py | 2 +- tests/fast/utils/test_mask_utils.py | 2 +- tests/fast/utils/test_misc.py | 2 +- tests/fast/utils/test_mxfp8_quantizer.py | 2 +- tests/fast/utils/test_nvfp4_quantizer.py | 2 +- tests/fast/utils/test_quantizer_ci.py | 2 +- tests/fast/utils/test_types.py | 2 +- .../test_utils/test_mock_sglang_server.py | 2 +- .../fast/utils/test_utils/test_mock_tools.py | 2 +- .../test_utils/test_session_verify_runner.py | 2 +- tests/utils/test_sglang_config.py | 2 +- 128 files changed, 2500 insertions(+), 1375 deletions(-) create mode 100644 .claude/skills/manage-gh-runners/SKILL.md create mode 100755 .claude/skills/manage-gh-runners/manage-runners.sh create mode 100644 .claude/skills/setup-ci-host/SKILL.md create mode 100755 .claude/skills/setup-ci-host/setup-host.sh create mode 100644 miles/utils/test_wandb_utils.py delete mode 100644 tests/ci/gpu_lock_exec.py create mode 100644 tests/ci/labels.py create mode 100644 tests/ci/test_ci_register.py create mode 100644 tests/ci/test_labels.py create mode 100644 tests/ci/test_run_suite.py rename tests/e2e/megatron/{test_glm47_flash_r3_mtp.py => test_glm47_flash/_common.py} (60%) create mode 100644 tests/e2e/megatron/test_glm47_flash/test_r3_mtp.py create mode 100644 tests/e2e/megatron/test_glm47_flash/test_r3_mtp_deepep.py rename tests/e2e/megatron/{test_qwen3_30B_A3B.py => test_qwen3_30B_A3B/_common.py} (56%) create mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B/test_baseline.py create mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B/test_deepep_fp8.py create mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B/test_deepep_fp8_bridge.py create mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B/test_int4_rollout.py create mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B/test_r3_baseline.py create mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B/test_r3_deepep_fp8.py delete mode 100644 tests/e2e/megatron/test_qwen3_30B_A3B_r3.py delete mode 100644 tests/e2e/sglang/test_session_server_multi_role.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/_common.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/test_glm47.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/test_minimax_m27.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/test_nemotron3.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/test_qwen3.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/test_qwen35.py create mode 100644 tests/e2e/sglang/test_session_server_multi_role/test_qwennext.py delete mode 100644 tests/e2e/sglang/test_tito_logprob_equivalence.py diff --git a/.claude/skills/manage-gh-runners/SKILL.md b/.claude/skills/manage-gh-runners/SKILL.md new file mode 100644 index 00000000000..6ffcf341161 --- /dev/null +++ b/.claude/skills/manage-gh-runners/SKILL.md @@ -0,0 +1,89 @@ +--- +name: manage-gh-runners +description: Add, remove, list, or swap GitHub Actions self-hosted runners on a CI host that uses the env-var-driven `gh-runner` docker image (raw `docker run`, no compose). Triggers, "add gh runner", "remove gh runner", "list ci runners", or similar. Run `setup-ci-host` first if /data/miles_ci is not yet provisioned. Boundary vs the `actions-runner` + docker-compose flow documented in `tests/ci/README.md`, only use this skill when `docker ps` shows the `gh-runner` image. Does NOT prepare the host filesystem layout — that is `setup-ci-host`'s job. +user_invocable: true +--- + +# Manage gh-runner Containers + +Add, remove, or swap GitHub Actions self-hosted runners on a CI host that +uses the `gh-runner` image (raw `docker run`, env-var driven). This is +distinct from the `actions-runner` + `docker-compose` flow documented in +[`tests/ci/README.md`](../../../tests/ci/README.md). + +If you don't already know which flow your host uses, check: + +```shell +docker ps --format '{{.Names}}\t{{.Image}}' +``` + +* `gh-runner` image → use this skill +* image starting with `ghcr.io/actions/actions-runner` → use the README's compose flow + +Run [Step 0 (`setup-ci-host`)](../setup-ci-host/SKILL.md) before any of the +operations below if `/data/miles_ci` is not yet provisioned on the host. + +## Usage + +```shell +cd /root/miles/.claude/skills/manage-gh-runners +export REPO_URL=https://github.com// + +./manage-runners.sh list +./manage-runners.sh add --name -4gpu-0 --cvd 0,1,2,3 --hwtype h200 --ngpu 4 --token +./manage-runners.sh remove --name -4gpu-0 --token +``` + +Get `` from the repo's settings UI: +`https://github.com///settings/actions/runners/new?arch=x64&os=linux`. +The same token is accepted by both `config.sh --token` (register) and +`config.sh remove --token` (deregister); no separate removal token is +needed. Tokens expire ~1 hour after issuance. + +## Label and CVD convention + +Every runner gets: + +``` +self-hosted, Linux, X64, gpu-runner, , , gpu +``` + +* `` — typically `-gpu-`. +* `` — `h200`, `h100`, ... (matches workflow `runs_on` predicates). +* `gpu` — `2gpu`, `4gpu`, `8gpu` (meta-label required by jobs that + pin GPU count, e.g. `pr-test.yml`'s `stage-c-4-gpu-h200`). + +`--ngpu` must equal the number of devices in `--cvd`; the script enforces +this so a 4-GPU runner can't accidentally claim the `2gpu` label. + +CVD partitioning rule: contiguous, non-overlapping slices summing to the +host's physical GPU count. Example 4+2+2 split on an 8-GPU H200 host: + +| Runner | CVD | +|---|---| +| `-4gpu-0` | `0,1,2,3` | +| `-2gpu-0` | `4,5` | +| `-2gpu-1` | `6,7` | + +## Why the script does what it does + +A few invariants the script encodes that are easy to get wrong by hand: + +* **Identity bind** `/data/runner-setup/data/:/data/runner-setup/data/` + — the runner spawns sibling job containers through the host docker socket + and forwards its in-container `--work` path verbatim to the daemon; if + host and container paths differ, the daemon resolves a directory the + runner never wrote to. +* **Cache mount** `/data/runner-cache//.cache → /root/.cache` keeps + pip / HF / torch caches on the big disk instead of the root partition. +* **No `/data/miles_ci` mount on the runner container itself** — only the + per-job containers it spawns need that path, and that bind is declared + in [`_run-ci.yml`](../../../../.github/workflows/_run-ci.yml) (resolved + against the HOST filesystem by the daemon). +* **Deregister BEFORE `docker rm -f`**: `config.sh remove --token ` + inside the container clears the runner from GitHub's UI. Skipping it + leaves an offline-zombie entry that confuses future operators. +* **Per-runner host dirs (`/data/runner-setup/data/`, + `/data/runner-cache/`) are not reusable across runner names** — + `.runner` registration state is name-bound; the script deletes these on + `remove` for that reason. diff --git a/.claude/skills/manage-gh-runners/manage-runners.sh b/.claude/skills/manage-gh-runners/manage-runners.sh new file mode 100755 index 00000000000..5c112c2f301 --- /dev/null +++ b/.claude/skills/manage-gh-runners/manage-runners.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# +# manage-runners.sh — add/remove/list GitHub Actions runner containers on +# a CI host that uses the `gh-runner` image flow (raw docker run, env-var +# driven), NOT the `actions-runner` + docker-compose flow. +# +# See SKILL.md (next to this script) for the operator-facing docs. +# +# What this script encodes: +# * launch convention — host paths, mount layout, runtime flags, env vars +# observed on the existing externally-managed hosts; copy these so a new +# container is bind-compatible with the host's docker-out-of-docker setup. +# * label convention — name, hw-type, gpu-count meta-labels matching the +# scheme `_run-ci.yml` and `pr-test.yml` route on. +# * clean-deregister convention — run `config.sh remove` inside the +# container BEFORE `docker rm -f`, so GitHub's runner list doesn't +# accumulate offline zombies. + +set -euo pipefail + +REPO_URL=${REPO_URL:-} +IMAGE=${IMAGE:-gh-runner:latest} +SETUP_ROOT=/data/runner-setup/data +CACHE_ROOT=/data/runner-cache + +die() { echo "ERROR: $*" >&2; exit 1; } + +usage() { + cat >&2 <-4gpu-0 + --cvd CVD CUDA_VISIBLE_DEVICES, e.g. 0,1,2,3 + --hwtype TYPE hardware label, e.g. h200 / h100 + --ngpu N gpu-count for the \${N}gpu meta-label (must match #devices in --cvd) + --token TOKEN registration token from + https://github.com///settings/actions/runners/new + (the same token is accepted for config.sh remove) + +env vars: + REPO_URL required for 'add', e.g. export REPO_URL=https://github.com// + IMAGE (default: $IMAGE) +EOF + exit 1 +} + +parse_flags() { + while [ $# -gt 0 ]; do + case "$1" in + --name) NAME=$2; shift 2 ;; + --cvd) CVD=$2; shift 2 ;; + --hwtype) HWTYPE=$2; shift 2 ;; + --ngpu) NGPU=$2; shift 2 ;; + --token) TOKEN=$2; shift 2 ;; + *) die "unknown flag: $1" ;; + esac + done +} + +cmd_list() { + local ids + ids=$(docker ps -q --filter "ancestor=$IMAGE") + [ -n "$ids" ] || { echo "no $IMAGE containers running"; return 0; } + printf "%-24s %-12s %-12s %s\n" NAME STATUS CVD LABELS + for c in $ids; do + local name status cvd labels + name=$(docker inspect -f '{{.Name}}' "$c" | tr -d /) + status=$(docker inspect -f '{{.State.Status}}' "$c") + cvd=$(docker exec "$c" printenv CUDA_VISIBLE_DEVICES 2>/dev/null || echo -) + labels=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$c" \ + | awk -F= '/^RUNNER_LABELS=/{ $1=""; sub(/^=/,""); print }') + printf "%-24s %-12s %-12s %s\n" "$name" "$status" "$cvd" "$labels" + done +} + +cmd_add() { + NAME=${NAME:-}; CVD=${CVD:-}; HWTYPE=${HWTYPE:-}; NGPU=${NGPU:-}; TOKEN=${TOKEN:-} + [ -n "$NAME" ] && [ -n "$CVD" ] && [ -n "$HWTYPE" ] && [ -n "$NGPU" ] && [ -n "$TOKEN" ] \ + || die "add requires --name --cvd --hwtype --ngpu --token (see --help)" + [ -n "$REPO_URL" ] || die "REPO_URL not set; export REPO_URL=https://github.com//" + + local cvd_count + cvd_count=$(awk -F, '{print NF}' <<<"$CVD") + [ "$cvd_count" = "$NGPU" ] || die "--cvd has $cvd_count devices but --ngpu=$NGPU; refusing mismatched label" + + if docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then + die "container $NAME already exists; remove it first or pick a different --name" + fi + + local labels="self-hosted,Linux,X64,gpu-runner,$NAME,$HWTYPE,${NGPU}gpu" + mkdir -p "$SETUP_ROOT/$NAME" "$CACHE_ROOT/$NAME/.cache" + + docker run -d --name "$NAME" \ + --restart unless-stopped \ + --runtime=nvidia --gpus all \ + -e RUNNER_URL="$REPO_URL" \ + -e RUNNER_TOKEN="$TOKEN" \ + -e RUNNER_NAME="$NAME" \ + -e RUNNER_LABELS="$labels" \ + -e RUNNER_DIR="$SETUP_ROOT/$NAME" \ + -e CUDA_VISIBLE_DEVICES="$CVD" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$SETUP_ROOT/$NAME:$SETUP_ROOT/$NAME" \ + -v "$CACHE_ROOT/$NAME/.cache:/root/.cache" \ + "$IMAGE" >/dev/null + + echo "started $NAME (cvd=$CVD, labels=$labels)" + echo "tail logs with: docker logs -f $NAME" +} + +cmd_remove() { + NAME=${NAME:-}; TOKEN=${TOKEN:-} + [ -n "$NAME" ] || die "remove requires --name (see --help)" + + local containers + containers=$(docker ps -a --format '{{.Names}}') || die "failed to query docker containers" + + if grep -Fxq "$NAME" <<<"$containers"; then + if [ -n "$TOKEN" ]; then + if [ "$(docker inspect -f '{{.State.Running}}' "$NAME")" != "true" ]; then + echo "$NAME is stopped; starting it to deregister from GitHub..." + docker start "$NAME" >/dev/null + fi + echo "deregistering $NAME from GitHub..." + docker exec "$NAME" bash -c 'cd "$1" && ./config.sh remove --token "$2"' _ "$SETUP_ROOT/$NAME" "$TOKEN" \ + || die "config.sh remove failed; refusing to delete local runner state so deregistration can be retried" + else + echo "WARN: no --token given; $NAME will linger as 'offline' in GitHub UI until manually removed" + fi + else + echo "$NAME container not found (skipping in-container deregister)" + fi + + docker rm -f "$NAME" 2>/dev/null || true + rm -rf "$SETUP_ROOT/$NAME" "$CACHE_ROOT/$NAME" + echo "removed container + host dirs for $NAME" +} + +case "${1:-}" in + list) shift; parse_flags "$@"; cmd_list ;; + add) shift; parse_flags "$@"; cmd_add ;; + remove) shift; parse_flags "$@"; cmd_remove ;; + -h|--help|"") usage ;; + *) die "unknown subcommand: $1 (try $0 --help)" ;; +esac diff --git a/.claude/skills/setup-ci-host/SKILL.md b/.claude/skills/setup-ci-host/SKILL.md new file mode 100644 index 00000000000..e684b2c819e --- /dev/null +++ b/.claude/skills/setup-ci-host/SKILL.md @@ -0,0 +1,103 @@ +--- +name: setup-ci-host +description: Prepare a new physical CI host so /data/miles_ci is canonical — a real directory on the biggest disk, or a symlink to it — before installing miles GitHub Actions runners. Triggers, "setup ci host", "/data/miles_ci not ready", "miles ci host bootstrap", or similar. Idempotent; safe to re-run. Run once per host before either the README's docker-compose runner flow or `manage-gh-runners`. Boundary vs `manage-gh-runners`, this skill only prepares the host filesystem layout (big-disk detection, mkdir, symlink); it does NOT add, remove, or talk to GitHub runners. +user_invocable: true +--- + +# Setup CI Host + +Prepares a new physical host to run miles GitHub Actions CI runners by making +`/data/miles_ci` canonical on the host. + +The miles CI workflow (`.github/workflows/_run-ci.yml`) bind-mounts +`/data/miles_ci` and its subdirectories (`models`, `datasets`, `hf_cache`) +from the host into every CI job container, using literal paths everywhere. +For that to work, every CI host must provide `/data/miles_ci` either as a +real directory on its biggest disk OR as a symlink to wherever the real big +disk is. This script automates that. + +Run this once on each new CI host, before the runner-setup steps in +[`tests/ci/README.md`](../../../tests/ci/README.md) or the externally-managed +flow in [`manage-gh-runners`](../manage-gh-runners/). + +## Usage + +```shell +cd /root/miles/.claude/skills/setup-ci-host +./setup-host.sh +``` + +Re-runs are safe: the script is idempotent and a no-op on hosts that already +have `/data/miles_ci` correctly set up. + +## What it does + +1. Probes mounted filesystems with `df` and picks the mount point with the + most total bytes, excluding `tmpfs`/`devtmpfs`/`overlay`/`squashfs` and + system mounts (`/`, `/boot`, `/dev`, `/proc`, `/run`, `/sys`). +2. **If the biggest mount is `/data`** (the canonical case, e.g. scitix-72): + ensures `/data/miles_ci` exists as a real directory. Done. +3. **If the biggest mount is something else** (e.g. `/mnt/nvme0n1` on novita): + ensures `/miles_ci` exists as a real directory, then atomically + replaces `/data/miles_ci` with a symlink pointing to `/miles_ci` + (creates `/data` first if missing). +4. Creates `/data/miles_ci/models`, `/data/miles_ci/datasets`, + `/data/miles_ci/hf_cache` if they do not exist. +5. Prints a summary: chosen mount, whether `/data/miles_ci` is a real dir or + a symlink, free space on the backing disk. + +## Verification + +After running, the operator can confirm the result with: + +```shell +ls -la /data/miles_ci +df -h /data/miles_ci +readlink /data/miles_ci || echo "(real directory, not a symlink)" +``` + +The first command should list the four subdirs (`models`, `datasets`, +`hf_cache`, plus any existing `runner_` work dirs). `df -h` should +report on the host's big disk regardless of whether `/data/miles_ci` is real +or a symlink. `readlink` succeeds only when `/data/miles_ci` is a symlink. + +## When to re-run + +- After adding a larger disk to the host (the script will re-detect; if + `/data/miles_ci` already exists pointing at a smaller disk, the script + prompts before moving anything — see [Safety](#safety)). +- After noticing CI mount errors that suggest `/data/miles_ci` is wrong. + +## Safety + +The script never silently overwrites a `/data/miles_ci` that already exists +and points somewhere unexpected. When run from a terminal, it prompts the +operator interactively; in non-interactive contexts (cron, CI, scripts) it +refuses and exits non-zero so a human always makes the destructive call. + +Two prompts can appear: + +1. **Existing real directory on the wrong disk** — `/data/miles_ci` has data + but the biggest disk is elsewhere. Options: + - `[m] migrate`: `rsync` the data to the big disk, then `rm -rf` the + original and symlink. Preserves data. + - `[w] wipe`: `rm -rf` the original (data lost), then symlink. Requires + typing `yes` to confirm. + - `[a] abort` (default): leave everything as-is, exit non-zero. +2. **Existing symlink pointing at the wrong target** — `/data/miles_ci` + already symlinks somewhere, but not to the current biggest disk. The + prompt asks `[y/N]` to re-point. Re-pointing does NOT migrate data from + the old target; only the symlink changes. + +The script requires root (or write access to `/data`). It will detect a +non-root invocation and suggest `sudo ./setup-host.sh`. + +## Inside the container + +Once the host side is prepared, the runner container's compose mount +`/data/miles_ci:/data/miles_ci` works as an identity bind (per the docker-out- +of-docker requirement explained in `docker-compose.yml`). The inner job +container sees `/data/miles_ci` as a real bind from the host's canonical +path, whether or not the host side is itself a symlink — Docker resolves the +host symlink at bind time, so the container always sees the underlying +directory. This is the desired behavior. diff --git a/.claude/skills/setup-ci-host/setup-host.sh b/.claude/skills/setup-ci-host/setup-host.sh new file mode 100755 index 00000000000..66dc05013b7 --- /dev/null +++ b/.claude/skills/setup-ci-host/setup-host.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# +# setup-host.sh — prepare a new miles CI host so /data/miles_ci is canonical. +# See SKILL.md (next to this script) for the full operator-facing docs. +# +# Behavior summary: +# 1. df probes mounted filesystems; picks the mount point with most total bytes +# among non-system fs types. +# 2. If the chosen mount is /data: ensure /data/miles_ci exists as a real dir. +# 3. Else: ensure /miles_ci exists, atomically symlink /data/miles_ci. +# 4. mkdir -p /data/miles_ci/{models,datasets,hf_cache}. +# 5. Print summary. +# +# Idempotent. Re-running on a properly-prepared host is a no-op except for the +# summary line. When /data/miles_ci already exists pointing somewhere +# unexpected, prompts the operator interactively (migrate / wipe / abort); +# non-interactive runs (cron, CI) exit non-zero instead of choosing silently. + +set -euo pipefail + +# --- Preconditions ----------------------------------------------------------- + +if [[ "$(id -u)" -ne 0 ]] && ! [ -w / ]; then + echo "ERROR: setup-host.sh needs to write under /; re-run with sudo:" >&2 + echo " sudo ./setup-host.sh" >&2 + exit 1 +fi + +# --- Disk probe -------------------------------------------------------------- + +# Pick the mount point with the most total size, excluding ephemeral / pseudo +# filesystems and well-known system mounts. The leading `df` columns under +# --output=target,size are: target, 1K-blocks (with -B1: bytes). +biggest_mount=$( + df --output=target,size -B1 -x tmpfs -x devtmpfs -x overlay -x squashfs 2>/dev/null \ + | awk 'NR>1 && $1 !~ /^\/(boot|dev|proc|run|sys)(\/|$)/ {print $2, $1}' \ + | sort -rn \ + | head -1 \ + | awk '{print $2}' +) + +if [[ -z "${biggest_mount}" ]]; then + echo "ERROR: could not detect any non-system mount from df output" >&2 + exit 1 +fi + +# --- Decide canonical target ------------------------------------------------- + +# canonical: /data/miles_ci on every host. real_target is the path on the +# biggest disk where the data actually lives. +canonical=/data/miles_ci + +if [[ "${biggest_mount}" == "/data" ]]; then + real_target=/data/miles_ci +else + real_target="${biggest_mount}/miles_ci" +fi + +# --- Establish /data/miles_ci ------------------------------------------------ + +# Make sure the real target directory exists on the big disk. +mkdir -p "${real_target}" + +# Helper: place the symlink ${canonical} -> ${real_target}, atomically. +place_symlink() { + mkdir -p /data + local tmp_link + tmp_link="$(mktemp -u "${canonical}.XXXXXX.tmp")" + ln -s "${real_target}" "${tmp_link}" + mv -T "${tmp_link}" "${canonical}" +} + +# Helper: prompt the operator only when running interactively. In +# non-interactive contexts (cron, scripts, CI), fall through to the caller's +# bail path — never make destructive choices without a human. +interactive() { [ -t 0 ] && [ -t 1 ]; } + +if [[ "${real_target}" == "${canonical}" ]]; then + # /data IS the big disk; /data/miles_ci is the real path; nothing to symlink. + : +elif [[ -L "${canonical}" ]]; then + current_target=$(readlink "${canonical}") + if [[ "${current_target}" == "${real_target}" ]]; then + : # already correct symlink + else + cat >&2 <&2 + echo "Re-run from a terminal to confirm, or fix manually." >&2 + exit 1 + fi + read -r -p "Re-point ${canonical} -> ${real_target}? [y/N]: " choice &2 + exit 1 + fi + fi +elif [[ -d "${canonical}" ]] && [[ -n "$(ls -A "${canonical}" 2>/dev/null || true)" ]]; then + cat >&2 < ${real_target}/, then rm -rf ${canonical}, then symlink + [w] wipe: rm -rf ${canonical} (DATA LOST), then symlink to ${real_target} + [a] abort: leave as-is and exit non-zero (default) +EOF + if ! interactive; then + echo "Refusing to choose silently in non-interactive mode." >&2 + echo "Re-run from a terminal, or migrate/wipe manually." >&2 + exit 1 + fi + read -r -p "Choice [m/w/a]: " choice /dev/null || { echo "ERROR: rsync not installed" >&2; exit 1; } + rsync -aHX --info=progress2 "${canonical}/" "${real_target}/" + rm -rf "${canonical}" + ;; + w|W) + read -r -p "Type 'yes' to confirm rm -rf ${canonical}: " confirm &2; exit 1; } + rm -rf "${canonical}" + ;; + *) + echo "Aborted by operator." >&2 + exit 1 + ;; + esac + place_symlink +else + # /data/miles_ci does not exist (or is an empty dir we can remove). + [[ -d "${canonical}" ]] && rmdir "${canonical}" 2>/dev/null || true + place_symlink +fi + +# --- Standard subdirectories ------------------------------------------------- + +mkdir -p \ + "${canonical}/models" \ + "${canonical}/datasets" \ + "${canonical}/hf_cache" + +# --- Summary ----------------------------------------------------------------- + +echo "biggest mount : ${biggest_mount}" +echo "real target : ${real_target}" +if [[ -L "${canonical}" ]]; then + echo "/data/miles_ci : symlink -> $(readlink "${canonical}")" +else + echo "/data/miles_ci : real directory" +fi +df -h --output=target,size,used,avail "${biggest_mount}" | tail -1 \ + | awk '{printf "disk usage : %s used / %s total (%s available)\n", $3, $2, $4}' diff --git a/.github/workflows/_run-ci.yml b/.github/workflows/_run-ci.yml index 866f28c55ff..3a2bc20de6c 100644 --- a/.github/workflows/_run-ci.yml +++ b/.github/workflows/_run-ci.yml @@ -3,16 +3,14 @@ name: CI Job on: workflow_call: inputs: - gpu_count: - type: number - required: true execute_command: type: string required: true runs_on: type: string required: false - default: '8gpu' + default: '["8gpu"]' + description: 'JSON-encoded list of runner labels (all required). e.g. ''["h100", "8gpu"]'' picks runners that carry BOTH labels. Default ''["8gpu"]'' preserves the previous bare ''8gpu'' single-label routing.' container_image: type: string required: false @@ -30,24 +28,28 @@ on: jobs: run: if: ${{ !inputs.cpu_runner }} - runs-on: ${{ inputs.runs_on }} + # fromJSON parses the input list so `runs-on` receives an actual array. + # Without this, GitHub treats the string verbatim as one label literal + # (e.g. the label name '["h100", "8gpu"]' with brackets), which no + # runner carries, and the job queues forever. + runs-on: ${{ fromJSON(inputs.runs_on) }} container: image: ${{ inputs.container_image }} options: > --gpus all + --env CUDA_VISIBLE_DEVICES --ipc=host --shm-size=32g --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 - -v /mnt/nvme0n1/miles_ci/hf_cache:/root/.cache/huggingface + -v /data/miles_ci:/data/miles_ci + -v /data/miles_ci/models:/root/models + -v /data/miles_ci/datasets:/root/datasets + -v /data/miles_ci/hf_cache:/root/.cache/huggingface --privileged --ulimit nofile=65535:65535 - -v /tmp:/tmp defaults: run: working-directory: ${{ github.workspace }} @@ -56,7 +58,6 @@ 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_FEW_GPU: '0' steps: - name: Checkout repository uses: actions/checkout@v4 @@ -70,9 +71,29 @@ jobs: 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: Wait for GPU ready + shell: bash + run: | + # When a self-hosted runner accepts a new job seconds after the + # previous job's container exits, the NVIDIA driver subsystem may + # still be tearing down its prior CUDA context. The next process + # to call cudaGetDeviceCount() then hits Error 802 ("system not + # yet initialized"). Block here until nvidia-smi succeeds, with + # a bounded retry budget so a genuinely-broken GPU fails fast. + for i in $(seq 1 60); do + if nvidia-smi -L >/dev/null 2>&1; then + echo "GPU ready after $((i-1)) retry(s)" + nvidia-smi --query-gpu=index,name,memory.used --format=csv,noheader + exit 0 + fi + sleep 2 + done + echo "ERROR: nvidia-smi did not become ready after 120s" >&2 + nvidia-smi 2>&1 | tail -20 || true + exit 1 + - name: Resolve dependency refs if: ${{ !inputs.skip_dependency_install }} id: resolve-refs @@ -125,7 +146,7 @@ jobs: - name: Execute shell: bash - run: python tests/ci/gpu_lock_exec.py --count ${{ inputs.gpu_count }} -- ${{ inputs.execute_command }} + run: ${{ inputs.execute_command }} # CPU-only path: GitHub-hosted ubuntu-latest, bare pip install. # Used for fast CPU tests so they don't occupy GPU-fleet runner slots. diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 3bdf9983453..811ebb67b2c 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -21,181 +21,120 @@ on: type: string default: 'sglang-miles' +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +# PR labels are passed through to run_suite.py as raw `run-ci-` strings via +# `--labels`. run_suite.py strips the `run-ci-` prefix internally and ignores +# any label that does not start with `run-ci-` (see tests/ci/run_suite.py +# `strip_run_ci_prefix`). For `workflow_dispatch`, no PR labels exist, so the +# `--labels` list collapses to empty; `--match-all-labels` is then added +# unconditionally to bypass the labels predicate inside run_suite.py and run +# every enabled test in the suite. + jobs: # Stage A: CPU-only fast tests (always runs on PR) # Runs on GitHub-hosted ubuntu-latest (cpu_runner: true) to avoid # occupying GPU-fleet runner slots. - stage-a-fast: + stage-a-cpu: if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request) uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 0 cpu_runner: true - execute_command: "python tests/ci/run_suite.py --hw cpu --suite stage-a-fast" + execute_command: >- + python tests/ci/run_suite.py --hw cpu --suite stage-a-cpu + --labels ${{ join(github.event.pull_request.labels.*.name, ' ') }} + ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-ci-image') || contains(github.event.pull_request.labels.*.name, 'run-ci-all')) && '--match-all-labels' || '' }} secrets: inherit - # Stage A: Unit test (single FSDP test, 8 GPU) - stage-a-unit-test: - if: | - (github.event_name == 'workflow_dispatch') || - (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-unit-test')) + # Stage B: CPU-only bucket (currently empty; reserved for future CPU tests + # that don't fit stage-a-cpu's fast budget). Always runs on PR. Empty result + # exits 0 in run_suite.py (see tests/ci/run_suite.py L149-L151). + stage-b-cpu: + if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request) uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 8 - execute_command: "pytest tests/e2e/fsdp/test_qwen3_4B_fsdp_true_on_policy.py -x -v" + cpu_runner: true + execute_command: >- + python tests/ci/run_suite.py --hw cpu --suite stage-b-cpu + --labels ${{ join(github.event.pull_request.labels.*.name, ' ') }} + ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-ci-image') || contains(github.event.pull_request.labels.*.name, 'run-ci-all')) && '--match-all-labels' || '' }} secrets: inherit - # Stage B: Fast GPU tests (1 GPU, runs every PR) - # For fast tests that require GPU (e.g. deterministic inference paths that - # pull in flash_attn). Parallel with stage-b-sglang; one self-hosted daemon slot. - stage-b-fast-gpu: - needs: [stage-a-fast] + + stage-b-2-gpu-h200: + needs: [stage-a-cpu] if: | always() && !failure() && !cancelled() && ((github.event_name == 'workflow_dispatch') || (github.event.pull_request)) uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 1 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-b-fast-1-gpu" + runs_on: '["h200", "2gpu"]' + execute_command: >- + python tests/ci/run_suite.py --hw cuda --suite stage-b-2-gpu-h200 + --labels ${{ join(github.event.pull_request.labels.*.name, ' ') }} + ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-ci-image') || contains(github.event.pull_request.labels.*.name, 'run-ci-all')) && '--match-all-labels' || '' }} secrets: inherit - # Stage B: SGLang patch tests (8 GPU) - stage-b-sglang: - needs: [stage-a-fast] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-sglang'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-b-sglang-8-gpu" - secrets: inherit - # Stage B: Short 8-GPU tests - stage-b-short: - needs: [stage-a-fast] + stage-c-8-gpu-h100: + needs: [stage-a-cpu] if: | always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-short'))) + ((github.event_name == 'workflow_dispatch') || (github.event.pull_request)) + strategy: + fail-fast: false + matrix: + partition_id: [0, 1] uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-b-short-8-gpu" + runs_on: '["h100", "8gpu"]' + execute_command: >- + python tests/ci/run_suite.py --hw cuda --suite stage-c-8-gpu-h100 + --auto-partition-id ${{ matrix.partition_id }} --auto-partition-size 2 + --labels ${{ join(github.event.pull_request.labels.*.name, ' ') }} + ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-ci-image') || contains(github.event.pull_request.labels.*.name, 'run-ci-all')) && '--match-all-labels' || '' }} secrets: inherit - # Stage C: FSDP 8-GPU tests - stage-c-fsdp: - needs: [stage-b-sglang, stage-b-short] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-fsdp'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-c-fsdp-8-gpu" - secrets: inherit - # Stage C: Megatron 8-GPU tests (3 partitions) - stage-c-megatron: - needs: [stage-b-sglang, stage-b-short] + stage-c-4-gpu-h200: + needs: [stage-a-cpu] if: | always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-megatron'))) + ((github.event_name == 'workflow_dispatch') || (github.event.pull_request)) strategy: fail-fast: false matrix: partition_id: [0, 1, 2] uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-c-megatron-8-gpu --auto-partition-id ${{ matrix.partition_id }} --auto-partition-size 3" - secrets: inherit - - # Stage C: Precision 8-GPU tests - stage-c-precision: - needs: [stage-b-sglang, stage-b-short] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-precision'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-c-precision-8-gpu" + runs_on: '["h200", "4gpu"]' + execute_command: >- + python tests/ci/run_suite.py --hw cuda --suite stage-c-4-gpu-h200 + --auto-partition-id ${{ matrix.partition_id }} --auto-partition-size 3 + --labels ${{ join(github.event.pull_request.labels.*.name, ' ') }} + ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-ci-image') || contains(github.event.pull_request.labels.*.name, 'run-ci-all')) && '--match-all-labels' || '' }} secrets: inherit - # Stage C: Checkpoint 8-GPU tests - stage-c-ckpt: - needs: [stage-b-sglang, stage-b-short] + stage-c-2-gpu-h200: + needs: [stage-a-cpu] if: | always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-ckpt'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-c-ckpt-8-gpu" - secrets: inherit - - # Stage C: Long 8-GPU tests - stage-c-long: - needs: [stage-b-sglang, stage-b-short] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-long'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-c-long-8-gpu" - secrets: inherit - - # Stage C: LoRA 8-GPU tests - stage-c-lora: - needs: [stage-b-sglang, stage-b-short] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-lora'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite stage-c-lora-8-gpu" - secrets: inherit - - # Stage C: GLM5 8-GPU tests (custom Docker image, no sglang/megatron deps) - stage-c-glm5: - needs: [stage-b-sglang, stage-b-short] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-glm5'))) - uses: ./.github/workflows/_run-ci.yml - with: - gpu_count: 8 - container_image: "radixark/miles:glm5" - skip_dependency_install: true - execute_command: "python tests/ci/gpu_lock_exec.py --count 8 -- python tests/e2e/megatron/test_glm5_744b_a40b_4layer.py" - secrets: inherit - - # Stage C: All suites (triggered by run-ci-image label) - stage-c-all: - needs: [stage-b-sglang, stage-b-short] - if: | - always() && !failure() && !cancelled() && - ((github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, 'run-ci-image'))) + ((github.event_name == 'workflow_dispatch') || (github.event.pull_request)) strategy: fail-fast: false matrix: - suite: - - stage-c-fsdp-8-gpu - - stage-c-megatron-8-gpu - - stage-c-precision-8-gpu - - stage-c-ckpt-8-gpu - - stage-c-long-8-gpu - - stage-c-lora-8-gpu - - stage-b-short-8-gpu + partition_id: [0, 1] uses: ./.github/workflows/_run-ci.yml with: - gpu_count: 8 - execute_command: "python tests/ci/run_suite.py --hw cuda --suite ${{ matrix.suite }}" + runs_on: '["h200", "2gpu"]' + execute_command: >- + python tests/ci/run_suite.py --hw cuda --suite stage-c-2-gpu-h200 + --auto-partition-id ${{ matrix.partition_id }} --auto-partition-size 2 + --labels ${{ join(github.event.pull_request.labels.*.name, ' ') }} + ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-ci-image') || contains(github.event.pull_request.labels.*.name, 'run-ci-all')) && '--match-all-labels' || '' }} secrets: inherit diff --git a/.gitignore b/.gitignore index d937b99965c..657152a9b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -192,3 +192,6 @@ glm/ _examples_synced/ .env .DS_Store + +.humanize/ + diff --git a/miles/backends/training_utils/ci_utils.py b/miles/backends/training_utils/ci_utils.py index 9124889beda..33016468124 100644 --- a/miles/backends/training_utils/ci_utils.py +++ b/miles/backends/training_utils/ci_utils.py @@ -19,7 +19,7 @@ def check_kl(args: Namespace, log_dict: dict[str, float], step_id: int, accumula # small floating-point differences, so use a relaxed threshold. assert abs(log_dict["train/ppo_kl"]) < 1e-8 and abs(log_dict["train/pg_clipfrac"]) < 1e-10, f"{log_dict=}" else: - assert abs(log_dict["train/ppo_kl"]) < 1e-10 and abs(log_dict["train/pg_clipfrac"]) < 1e-10, f"{log_dict=}" + assert abs(log_dict["train/ppo_kl"]) < 1e-9 and abs(log_dict["train/pg_clipfrac"]) < 1e-10, f"{log_dict=}" if accumulated_step_id == 0 and "train/kl_loss" in log_dict and not args.use_rollout_routing_replay: assert abs(log_dict["train/kl_loss"]) < 1e-9, f"{log_dict=}" diff --git a/miles/backends/training_utils/log_utils.py b/miles/backends/training_utils/log_utils.py index 07754b5a63d..7bea5b68279 100644 --- a/miles/backends/training_utils/log_utils.py +++ b/miles/backends/training_utils/log_utils.py @@ -182,7 +182,8 @@ def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatc # test_qwen2.5_0.5B_gsm8k.py on 8 GPUs hit ~3.7e-9 diff in CI), so use 1e-8 # rather than the previous 3e-9 to absorb BF16 reduction noise across configs. if args.use_rollout_routing_replay: - abs_tol = 1e-5 + # lop diff w/ w/o r3 is very big + abs_tol = 5e-3 elif getattr(args, "sglang_config", None) is not None: abs_tol = 1e-8 else: diff --git a/miles/utils/test_utils/session_verify_agent.py b/miles/utils/test_utils/session_verify_agent.py index 8ef8df61a18..4776f9aa6cc 100644 --- a/miles/utils/test_utils/session_verify_agent.py +++ b/miles/utils/test_utils/session_verify_agent.py @@ -2,8 +2,8 @@ Wired through ``--custom-generate-function-path`` / ``--custom-agent-function-path``; consumed by -``tests/e2e/sglang/test_session_server_multi_role.py`` and -``scripts/tools/verify_session_tito_tokenizer.py``. +``tests/e2e/sglang/test_session_server_multi_role/`` (one test file per +model family) and ``scripts/tools/verify_session_tito_tokenizer.py``. """ from __future__ import annotations diff --git a/miles/utils/test_utils/session_verify_runner.py b/miles/utils/test_utils/session_verify_runner.py index f692381da5f..a2e5c7e0588 100644 --- a/miles/utils/test_utils/session_verify_runner.py +++ b/miles/utils/test_utils/session_verify_runner.py @@ -2,7 +2,8 @@ Used by both consumers: -- pytest e2e: ``tests/e2e/sglang/test_session_server_multi_role.py`` +- pytest e2e: ``tests/e2e/sglang/test_session_server_multi_role/`` (one test + file per model family) - CLI: ``scripts/tools/verify_session_tito_tokenizer.py`` Both forms run the same ``execute_train(--debug-rollout-only)`` path: full miles diff --git a/miles/utils/test_wandb_utils.py b/miles/utils/test_wandb_utils.py new file mode 100644 index 00000000000..c5e5c5ad7c4 --- /dev/null +++ b/miles/utils/test_wandb_utils.py @@ -0,0 +1,56 @@ +from types import SimpleNamespace + +from miles.utils.tracking_utils import wandb_utils + + +def _args(**overrides): + values = { + "env_report": None, + "rank": 0, + "sglang_enable_metrics": False, + "use_wandb": True, + "wandb_dir": None, + "wandb_group": "group", + "wandb_host": None, + "wandb_key": None, + "wandb_mode": None, + "wandb_project": "project", + "wandb_random_suffix": False, + "wandb_run_id": None, + "wandb_team": None, + } + values.update(overrides) + return SimpleNamespace(**values) + + +def test_primary_wandb_init_uses_extended_init_timeout(monkeypatch): + init_calls = [] + + monkeypatch.setattr(wandb_utils.wandb, "init", lambda **kwargs: init_calls.append(kwargs)) + monkeypatch.setattr(wandb_utils.wandb, "define_metric", lambda *args, **kwargs: None) + monkeypatch.setattr(wandb_utils.wandb, "run", SimpleNamespace(id="run-id"), raising=False) + + args = _args() + wandb_utils.init_wandb_primary(args) + + settings = init_calls[0]["settings"] + assert settings.mode == "shared" + assert settings.x_primary is True + assert settings.init_timeout == 300.0 + assert args.wandb_run_id == "run-id" + + +def test_secondary_wandb_init_uses_extended_init_timeout(monkeypatch): + init_calls = [] + + monkeypatch.setattr(wandb_utils.wandb, "init", lambda **kwargs: init_calls.append(kwargs)) + monkeypatch.setattr(wandb_utils.wandb, "define_metric", lambda *args, **kwargs: None) + + args = _args(wandb_run_id="run-id") + wandb_utils.init_wandb_secondary(args) + + settings = init_calls[0]["settings"] + assert settings.mode == "shared" + assert settings.x_primary is False + assert settings.x_update_finish_state is False + assert settings.init_timeout == 300.0 diff --git a/miles/utils/tracking_utils/wandb_utils.py b/miles/utils/tracking_utils/wandb_utils.py index 4cfe7d59d25..7b5f1f6615d 100644 --- a/miles/utils/tracking_utils/wandb_utils.py +++ b/miles/utils/tracking_utils/wandb_utils.py @@ -21,6 +21,10 @@ def _is_offline_mode(args) -> bool: return os.environ.get("WANDB_MODE") == "offline" +def _wandb_settings(**kwargs): + return wandb.Settings(init_timeout=300.0, **kwargs) + + def init_wandb_primary(args): if not args.use_wandb: args.wandb_run_id = None @@ -62,9 +66,9 @@ def init_wandb_primary(args): # Configure settings based on offline/online mode if offline: - init_kwargs["settings"] = wandb.Settings(mode="offline") + init_kwargs["settings"] = _wandb_settings(mode="offline") else: - init_kwargs["settings"] = wandb.Settings(mode="shared", x_primary=True) + init_kwargs["settings"] = _wandb_settings(mode="shared", x_primary=True) # Add custom directory if specified if args.wandb_dir: @@ -140,7 +144,7 @@ def init_wandb_secondary(args, router_addr=None): "config": args.__dict__, "resume": "allow", "reinit": True, - "settings": wandb.Settings(**settings_kwargs), + "settings": _wandb_settings(**settings_kwargs), } # Add custom directory if specified diff --git a/tests/ci/README.md b/tests/ci/README.md index 4a2a5d02244..86334a5f167 100644 --- a/tests/ci/README.md +++ b/tests/ci/README.md @@ -8,6 +8,23 @@ https://github.com/radixark/miles/settings/secrets/actions ## Setup new GitHub runners +### Step 0: Prepare `/data/miles_ci` + +The miles CI workflow bind-mounts `/data/miles_ci` (and its `models`, +`datasets`, `hf_cache` subdirectories) from the host into every job container. +Every CI host must provide `/data/miles_ci` either as a real directory on its +biggest disk or as a symlink to wherever the big disk is. +[`tests/ci/skills/setup-ci-host`](skills/setup-ci-host/) automates this: + +```shell +cd /root/miles/tests/ci/skills/setup-ci-host +./setup-host.sh +``` + +The script probes mounts with `df`, picks the biggest non-system mount, and +ensures `/data/miles_ci` resolves there (real dir if `/data` is biggest; +symlink otherwise). Idempotent. + ### Step 1: Env Write `.env` mimicking `.env.example`. @@ -15,11 +32,14 @@ The token can be found at https://github.com/radixark/miles/settings/actions/run WARN: The `GITHUB_RUNNER_TOKEN` changes after a while. +> Steps 1-3 cover the `docker-compose` flow. For hosts that use the +> `gh-runner` image flow instead, see [`manage-gh-runners`](skills/manage-gh-runners/). + ### Step 2: Prepare `/home/runner/externals` ```shell docker run --rm -it --privileged --pid=host -v /:/host_root ubuntu /bin/bash -c 'rm -rf /host_root/home/runner/externals && mkdir -p /host_root/home/runner/externals && chmod -R 777 /host_root/home/runner/externals' -docker run -d --name temp-runner ghcr.io/actions/actions-runner:2.328.0 tail -f /dev/null +docker run -d --name temp-runner ghcr.io/actions/actions-runner:2.334.0 tail -f /dev/null docker cp temp-runner:/home/runner/externals/. /home/runner/externals docker rm -f temp-runner ls -alh /home/runner/externals @@ -28,11 +48,87 @@ ls -alh /home/runner/externals ### Step 3: Run ```shell -cd /mnt/data/tom/primary_synced/miles/tests/ci/github_runner +cd /root/miles/tests/ci/github_runner docker compose up -d ``` -### Debugging +The base `docker-compose.yml` defines a template `runner` service; the per-host +concrete runner services (e.g. `scitix-72-0`, `scitix-72-1`) live in +`docker-compose.override.yml`. The base service is set to `replicas: 1` so a +stray `docker compose up` without the override never silently spins up many +runners. + +## GPU partitioning convention + +GPU exposure to inner CI job containers happens in two layers: + +* **Driver layer**: the workflow `.github/workflows/_run-ci.yml` keeps + `--gpus all` in `container.options`, so every job's inner container sees + every physical GPU on the host via `nvidia-smi -L`. Removing `--gpus all` + would make `nvidia-container-runtime` default to `NVIDIA_VISIBLE_DEVICES=void` + and produce a zero-GPU container, so this flag is intentional. + +* **CUDA layer**: each runner container sets `CUDA_VISIBLE_DEVICES` in its + compose `environment:` block. `_run-ci.yml`'s `container.options` includes + a bare `--env CUDA_VISIBLE_DEVICES` flag that propagates that env into the + inner job container. miles tests respect CVD (e.g. `miles/ray/train_actor.py`, + `miles/backends/sglang_utils/sglang_engine.py`), so CUDA-level partitioning + is sufficient. + +Host conventions: + +* **scitix-72** (H200, 2 runners): `scitix-72-0` pins `CUDA_VISIBLE_DEVICES=0,1,2,3`; + `scitix-72-1` pins `4,5,6,7`. Per-runner CVD is defined in + `docker-compose.override.yml`. +* **scitix-73** (H200, 3 runners, externally managed via `gh-runner` image): + `scitix-73-4gpu-0` pins `0,1,2,3` (label `4gpu`); `scitix-73-2gpu-0` pins + `4,5` and `scitix-73-2gpu-1` pins `6,7` (label `2gpu`). CVD is injected + via the container's `RUNNER_LABELS` + `CUDA_VISIBLE_DEVICES` env at + `docker run` time — see [`manage-gh-runners`](skills/manage-gh-runners/). +* **novita-host2 / novita-host4** (H100, 1 runner each, externally managed): + CVD intentionally left unset so jobs see all 8 GPUs. The bare + `--env CUDA_VISIBLE_DEVICES` forwards the "unset" state, and CUDA defaults + to seeing every visible device. + +## /data/miles_ci path identity rule + +`docker-compose.yml` mounts `/data/miles_ci:/data/miles_ci` — the SAME path on +both sides of the colon. This is mandatory: the runner process spawns sibling +job containers through the mounted `/var/run/docker.sock`, and the runner's +in-container `--work` path is forwarded verbatim to the host daemon. If the +host source and container target differ, the daemon resolves a host path the +runner never wrote to, and the sibling container mounts an unrelated (or +empty) tree. + +Step 0's `setup-host.sh` guarantees `/data/miles_ci` exists on the host +(either as a real directory if `/data` is the big disk, or as a symlink to +wherever the big disk is). Either way, the bind mount works uniformly across +hosts. + +The workflow `.github/workflows/_run-ci.yml`'s four data mounts use the same +literal path (no parameterization). Tests hardcode the container-side targets +(`/root/models`, `/root/datasets`, `/root/.cache/huggingface`); the host side +is always `/data/miles_ci/...`. + +## Restarting runners after compose changes + +* **env-only changes** (e.g. adjusting `CUDA_VISIBLE_DEVICES`): + recreate the container so it picks up the new compose env. + + ```shell + docker compose down && docker compose up -d + ``` + + A bare `docker restart ` does NOT pick up compose env changes — + it restarts the existing container with its original env. + +* **label changes** (e.g. adding a new runner label): the entrypoint guards + `config.sh` with `if [ ! -f /home/runner/.runner ]`, so a recreated + container does NOT re-register. To force re-registration, delete + `/home/runner/.runner` inside the container before recreate, OR run + `config.sh --replace` manually. + +## Debugging Logs @@ -41,17 +137,24 @@ Logs docker compose logs -f # One container -docker logs -f github_runner-runner-1 +docker logs -f scitix-72-0 ``` Exec ```shell -docker exec -it github_runner-runner-1 /bin/bash +docker exec -it scitix-72-0 /bin/bash +``` + +Verify a runner's CUDA partition + +```shell +docker exec scitix-72-0 printenv CUDA_VISIBLE_DEVICES # -> 0,1,2,3 +docker exec scitix-72-1 printenv CUDA_VISIBLE_DEVICES # -> 4,5,6,7 ``` -An example of quickly iterate +Quickly iterate ```shell -docker compose down -v && docker compose up -d && docker logs -f github_runner-runner-1 +docker compose down -v && docker compose up -d && docker logs -f scitix-72-0 ``` diff --git a/tests/ci/ci_register.py b/tests/ci/ci_register.py index fb47cc97884..d2cd7f73908 100644 --- a/tests/ci/ci_register.py +++ b/tests/ci/ci_register.py @@ -1,8 +1,10 @@ import ast import warnings -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum, auto +from tests.ci.labels import KNOWN_LABELS + __all__ = [ "HWBackend", "CIRegistry", @@ -12,8 +14,15 @@ "ut_parse_one_file", ] -_CPU_PARAM_ORDER = ("est_time", "suite", "nightly", "disabled") -_CUDA_PARAM_ORDER = ("est_time", "suite", "num_gpus", "nightly", "disabled") +# Only these two parameters may be passed positionally; everything else +# (labels, always_on, nightly, disabled) is keyword-only. +_POSITIONAL_PARAMS = ("est_time", "suite") + +# All accepted keyword arguments (in addition to the positional pair above). +_VALID_KWARGS = frozenset({"est_time", "suite", "labels", "nightly", "disabled"}) + +_REGISTER_NAMES = frozenset({"register_cpu_ci", "register_cuda_ci"}) + _UNSET = object() @@ -28,128 +37,181 @@ class CIRegistry: filename: str est_time: float suite: str - num_gpus: int = 0 + labels: list[str] = field(default_factory=list) nightly: bool = False - disabled: str | None = None # None = enabled, string = disabled with reason + disabled: str | None = None # None = enabled, string = disabled reason + +def register_cpu_ci( + est_time: float, + suite: str, + *, + labels: list[str] | None = None, + nightly: bool = False, + disabled: str | None = None, +): + """Marker for CPU CI registration (parsed via AST; runtime no-op). -def register_cpu_ci(est_time: float, suite: str, nightly: bool = False, disabled: str | None = None): - """Marker for CPU CI registration (parsed via AST; runtime no-op).""" + `labels=None` and `labels=[]` are equivalent: the test runs on every PR + regardless of `run-ci-*` labels. A non-empty `labels` list gates the test + on PR labels — the test runs when the PR carries `run-ci-` for any + `` in `labels`. + """ return None def register_cuda_ci( est_time: float, suite: str, - num_gpus: int = 8, + *, + labels: list[str] | None = None, nightly: bool = False, disabled: str | None = None, ): - """Marker for CUDA CI registration (parsed via AST; runtime no-op).""" + """Marker for CUDA CI registration (parsed via AST; runtime no-op). + + See `register_cpu_ci` for label semantics. + """ return None -REGISTER_MAPPING = { - "register_cpu_ci": (HWBackend.CPU, _CPU_PARAM_ORDER), - "register_cuda_ci": (HWBackend.CUDA, _CUDA_PARAM_ORDER), +_REGISTER_BACKEND_MAP = { + "register_cpu_ci": HWBackend.CPU, + "register_cuda_ci": HWBackend.CUDA, } +def _extract_constant(node: ast.AST) -> object: + """Return the literal value of an ast.Constant; otherwise return _UNSET. + + Sentinel return (instead of raising) lets callers compose richer error + messages with parameter names and file paths. + """ + if isinstance(node, ast.Constant): + return node.value + return _UNSET + + +def _extract_list_constant(node: ast.AST, *, context: str = "value") -> list: + """Return a list of literal string constants from `ast.List`. + + Accepts `None` (as `ast.Constant(None)`) and treats it as an empty list, + so callers may write `labels=None` interchangeably with `labels=[]`. + + Raises ValueError when the node is neither a list literal of string + constants nor a literal `None`. + """ + if isinstance(node, ast.Constant) and node.value is None: + return [] + if not isinstance(node, ast.List): + raise ValueError(f"{context} must be a list of string literals or None (got {type(node).__name__})") + out: list = [] + for elt in node.elts: + v = _extract_constant(elt) + if v is _UNSET: + raise ValueError(f"{context} must be a list of string literals (non-literal element)") + if not isinstance(v, str): + raise ValueError(f"{context} must be a list of string literals (got {type(v).__name__} element)") + out.append(v) + return out + + class RegistryVisitor(ast.NodeVisitor): def __init__(self, filename: str): self.filename = filename self.registries: list[CIRegistry] = [] - def _constant_value(self, node: ast.AST) -> object: - if isinstance(node, ast.Constant): - return node.value - return _UNSET + def _parse_call_args(self, func_call: ast.Call, func_name: str) -> CIRegistry: + if any(isinstance(arg, ast.Starred) for arg in func_call.args): + raise ValueError(f"{self.filename}: starred arguments are not supported in {func_name}()") - def _parse_call_args(self, func_call: ast.Call, param_order: tuple) -> tuple[float, str, int, bool, str | None]: - args = {name: _UNSET for name in param_order} - seen = set() + if len(func_call.args) > len(_POSITIONAL_PARAMS): + raise ValueError( + f"{self.filename}: too many positional arguments in {func_name}(); " + f"only {list(_POSITIONAL_PARAMS)} may be positional " + f"(labels and later are keyword-only)" + ) - if any(isinstance(arg, ast.Starred) for arg in func_call.args): - raise ValueError(f"{self.filename}: starred arguments are not supported in {func_call.func.id}()") - if len(func_call.args) > len(param_order): - raise ValueError(f"{self.filename}: too many positional arguments in {func_call.func.id}()") + parsed: dict[str, object] = {} - for name, arg in zip(param_order, func_call.args, strict=False): - seen.add(name) - args[name] = self._constant_value(arg) + for name, arg in zip(_POSITIONAL_PARAMS, func_call.args, strict=False): + v = _extract_constant(arg) + if v is _UNSET: + raise ValueError(f"{self.filename}: {name} in {func_name}() must be a literal constant") + parsed[name] = v for kw in func_call.keywords: if kw.arg is None: - raise ValueError(f"{self.filename}: **kwargs are not supported in {func_call.func.id}()") - if kw.arg not in args: - raise ValueError(f"{self.filename}: unknown argument '{kw.arg}' in {func_call.func.id}()") - if kw.arg in seen: - raise ValueError(f"{self.filename}: duplicated argument '{kw.arg}' in {func_call.func.id}()") - seen.add(kw.arg) - args[kw.arg] = self._constant_value(kw.value) - - if args["est_time"] is _UNSET or args["suite"] is _UNSET: - raise ValueError(f"{self.filename}: est_time and suite are required constants in {func_call.func.id}()") - - est_time, suite = args["est_time"], args["suite"] - - if not isinstance(est_time, (int, float)): - raise ValueError(f"{self.filename}: est_time must be a number in {func_call.func.id}()") - if not isinstance(suite, str): - raise ValueError(f"{self.filename}: suite must be a string in {func_call.func.id}()") - - # num_gpus (CUDA only) - num_gpus_value = args.get("num_gpus", _UNSET) - if num_gpus_value is _UNSET: - num_gpus = 8 if "num_gpus" in param_order else 0 - elif isinstance(num_gpus_value, int): - num_gpus = num_gpus_value - else: - raise ValueError(f"{self.filename}: num_gpus must be an integer in {func_call.func.id}()") - - # nightly - nightly_value = args.get("nightly", _UNSET) - if nightly_value is _UNSET: - nightly = False - elif isinstance(nightly_value, bool): - nightly = nightly_value - else: - raise ValueError(f"{self.filename}: nightly must be a boolean in {func_call.func.id}()") - - # disabled - disabled = args.get("disabled", _UNSET) - if disabled is _UNSET: - disabled = None - elif disabled is not None and not isinstance(disabled, str): - raise ValueError(f"{self.filename}: disabled must be a string in {func_call.func.id}()") - - return float(est_time), suite, num_gpus, nightly, disabled - - def _collect_ci_registry(self, func_call: ast.Call): - if not isinstance(func_call.func, ast.Name): - return None - - mapping = REGISTER_MAPPING.get(func_call.func.id) - if mapping is None: - return None + raise ValueError(f"{self.filename}: **kwargs are not supported in {func_name}()") + if kw.arg in parsed: + raise ValueError(f"{self.filename}: duplicated argument '{kw.arg}' in {func_name}()") + if kw.arg not in _VALID_KWARGS: + raise ValueError(f"{self.filename}: unknown argument '{kw.arg}' in {func_name}()") + if kw.arg == "labels": + parsed["labels"] = _extract_list_constant( + kw.value, context=f"{self.filename}: labels in {func_name}()" + ) + else: + v = _extract_constant(kw.value) + if v is _UNSET: + raise ValueError(f"{self.filename}: {kw.arg} in {func_name}() must be a literal constant") + parsed[kw.arg] = v + + if "est_time" not in parsed: + raise ValueError(f"{self.filename}: est_time is required in {func_name}()") + if "suite" not in parsed: + raise ValueError(f"{self.filename}: suite is required in {func_name}()") + + if not isinstance(parsed["est_time"], (int, float)): + raise ValueError(f"{self.filename}: est_time must be a number in {func_name}()") + if not isinstance(parsed["suite"], str): + raise ValueError(f"{self.filename}: suite must be a string in {func_name}()") + + # `labels` is optional. Missing / None / [] all mean "always run on + # every PR"; only a non-empty list gates the test on PR labels. + labels = parsed.get("labels", []) + if not isinstance(labels, list): + raise ValueError(f"{self.filename}: labels must be a list or None in {func_name}()") + + nightly = parsed.get("nightly", False) + if not isinstance(nightly, bool): + raise ValueError(f"{self.filename}: nightly must be a boolean in {func_name}()") + + disabled = parsed.get("disabled", None) + if disabled is not None and not isinstance(disabled, str): + raise ValueError(f"{self.filename}: disabled must be a string or None in {func_name}()") + + unknown = [label for label in labels if label not in KNOWN_LABELS] + if unknown: + valid_list = ", ".join(sorted(KNOWN_LABELS)) + raise ValueError( + f"{self.filename}: unknown labels {unknown} in {func_name}(); " + f"valid labels: [{valid_list}]. " + f"To add a new label: edit tests/ci/labels.py + create matching " + f"`run-ci-