ayh015 commited on
Commit
6011e08
·
verified ·
1 Parent(s): 0e96581

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. slime/__init__.py +3 -0
  2. slime/__pycache__/__init__.cpython-312.pyc +0 -0
  3. slime/backends/__init__.py +4 -0
  4. slime/backends/__pycache__/__init__.cpython-312.pyc +0 -0
  5. slime/backends/fsdp_utils/__init__.py +36 -0
  6. slime/backends/fsdp_utils/actor.py +1145 -0
  7. slime/backends/fsdp_utils/arguments.py +101 -0
  8. slime/backends/fsdp_utils/checkpoint.py +252 -0
  9. slime/backends/fsdp_utils/data_packing.py +221 -0
  10. slime/backends/fsdp_utils/kernels/__init__.py +3 -0
  11. slime/backends/fsdp_utils/kernels/fused_experts.py +384 -0
  12. slime/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py +543 -0
  13. slime/backends/fsdp_utils/lr_scheduler.py +197 -0
  14. slime/backends/fsdp_utils/models/__init__.py +3 -0
  15. slime/backends/fsdp_utils/models/qwen3_moe.py +128 -0
  16. slime/backends/fsdp_utils/models/qwen3_moe_hf.py +46 -0
  17. slime/backends/fsdp_utils/update_weight_utils.py +261 -0
  18. slime/backends/megatron_utils/__init__.py +45 -0
  19. slime/backends/megatron_utils/__pycache__/__init__.cpython-312.pyc +0 -0
  20. slime/backends/megatron_utils/__pycache__/actor.cpython-312.pyc +0 -0
  21. slime/backends/megatron_utils/__pycache__/arguments.cpython-312.pyc +0 -0
  22. slime/backends/megatron_utils/__pycache__/checkpoint.cpython-312.pyc +0 -0
  23. slime/backends/megatron_utils/__pycache__/cp_utils.cpython-312.pyc +0 -0
  24. slime/backends/megatron_utils/__pycache__/data.cpython-312.pyc +0 -0
  25. slime/backends/megatron_utils/__pycache__/initialize.cpython-312.pyc +0 -0
  26. slime/backends/megatron_utils/__pycache__/loss.cpython-312.pyc +0 -0
  27. slime/backends/megatron_utils/__pycache__/misc_utils.cpython-312.pyc +0 -0
  28. slime/backends/megatron_utils/__pycache__/model.cpython-312.pyc +0 -0
  29. slime/backends/megatron_utils/__pycache__/model_provider.cpython-312.pyc +0 -0
  30. slime/backends/megatron_utils/__pycache__/sglang.cpython-312.pyc +0 -0
  31. slime/backends/megatron_utils/actor.py +575 -0
  32. slime/backends/megatron_utils/arguments.py +33 -0
  33. slime/backends/megatron_utils/checkpoint.py +79 -0
  34. slime/backends/megatron_utils/ci_utils.py +87 -0
  35. slime/backends/megatron_utils/cp_utils.py +210 -0
  36. slime/backends/megatron_utils/data.py +599 -0
  37. slime/backends/megatron_utils/initialize.py +116 -0
  38. slime/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu +368 -0
  39. slime/backends/megatron_utils/kernels/int4_qat/setup.py +42 -0
  40. slime/backends/megatron_utils/loss.py +768 -0
  41. slime/backends/megatron_utils/megatron_to_hf/__init__.py +88 -0
  42. slime/backends/megatron_utils/megatron_to_hf/__pycache__/__init__.cpython-312.pyc +0 -0
  43. slime/backends/megatron_utils/megatron_to_hf/__pycache__/deepseekv3.cpython-312.pyc +0 -0
  44. slime/backends/megatron_utils/megatron_to_hf/__pycache__/glm4.cpython-312.pyc +0 -0
  45. slime/backends/megatron_utils/megatron_to_hf/__pycache__/glm4moe.cpython-312.pyc +0 -0
  46. slime/backends/megatron_utils/megatron_to_hf/__pycache__/llama.cpython-312.pyc +0 -0
  47. slime/backends/megatron_utils/megatron_to_hf/__pycache__/mimo.cpython-312.pyc +0 -0
  48. slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen2.cpython-312.pyc +0 -0
  49. slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen3_next.cpython-312.pyc +0 -0
  50. slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen3moe.cpython-312.pyc +0 -0
slime/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
slime/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (139 Bytes). View file
 
slime/backends/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
slime/backends/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (148 Bytes). View file
 
slime/backends/fsdp_utils/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+
6
+
7
+ try:
8
+ _TORCH_MEMORY_SAVER_AVAILABLE = True
9
+ except ImportError:
10
+ logging.warning("torch_memory_saver is not installed, refer to : https://github.com/fzyzcjy/torch_memory_saver")
11
+ _TORCH_MEMORY_SAVER_AVAILABLE = False
12
+
13
+ try:
14
+ _FSDP_AVAILABLE = True
15
+ except ImportError as e:
16
+ logging.warning(f"FSDP backend dependencies not available: {e}")
17
+ _FSDP_AVAILABLE = False
18
+
19
+ if _FSDP_AVAILABLE:
20
+ from .actor import FSDPTrainRayActor
21
+ from .arguments import load_fsdp_args
22
+ else:
23
+
24
+ def _raise_import_error(*args, **kwargs):
25
+ raise ImportError(
26
+ "FSDP backend is not available. "
27
+ "Please ensure PyTorch with FSDP2 support is installed. "
28
+ "For installation instructions, refer to: https://pytorch.org/docs/stable/distributed.fsdp.fully_shard.html"
29
+ )
30
+
31
+ FSDPTrainRayActor = _raise_import_error
32
+ load_fsdp_args = _raise_import_error
33
+
34
+ __all__ = ["load_fsdp_args", "FSDPTrainRayActor"]
35
+
36
+ logging.getLogger().setLevel(logging.WARNING)
slime/backends/fsdp_utils/actor.py ADDED
@@ -0,0 +1,1145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ import os
6
+ import random
7
+ from argparse import Namespace
8
+ from itertools import accumulate
9
+
10
+ import ray
11
+ import torch
12
+ import torch.distributed as dist
13
+ import torch.nn.functional as F
14
+ from ring_flash_attn import substitute_hf_flash_attn, update_ring_flash_attn_params
15
+ from tqdm import tqdm
16
+ from transformers import AutoConfig
17
+
18
+ from slime.ray.train_actor import TrainRayActor
19
+ from slime.utils import train_dump_utils, train_metric_utils
20
+ from slime.utils.context_utils import with_defer
21
+ from slime.utils.data import get_minimum_num_micro_batch_size, process_rollout_data
22
+ from slime.utils.distributed_utils import get_gloo_group
23
+ from slime.utils.memory_utils import clear_memory, print_memory
24
+ from slime.utils.metric_utils import compute_rollout_step
25
+ from slime.utils.misc import load_function
26
+ from slime.utils.ppo_utils import (
27
+ compute_approx_kl,
28
+ compute_gspo_kl,
29
+ compute_opsm_mask,
30
+ compute_policy_loss,
31
+ vanilla_tis_function,
32
+ )
33
+ from slime.utils.processing_utils import load_processor, load_tokenizer
34
+ from slime.utils.ray_utils import Box
35
+ from slime.utils.timer import Timer, inverse_timer, timer
36
+ from slime.utils.tracking_utils import init_tracking
37
+
38
+ from ...utils import tracking_utils
39
+ from ...utils.profile_utils import TrainProfiler
40
+ from . import checkpoint
41
+ from .data_packing import pack_sequences, pad_packed_sequence_with_cp, unpack_sequences
42
+ from .lr_scheduler import get_lr_scheduler
43
+ from .update_weight_utils import UpdateWeightFromDistributed, UpdateWeightFromTensor
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ class FSDPTrainRayActor(TrainRayActor):
49
+ """Simplified TrainRayActor for pure HF+FSDP training.
50
+
51
+ Responsibilities:
52
+ * Initialize model/tokenizer on rank0 sequentially to avoid race on cache
53
+ * Wrap model with FSDP
54
+ * Provide minimal train / save / update_weights hooks compatible with existing RayTrainGroup
55
+
56
+ Weight update strategy:
57
+ * Rank0 gathers state_dict (full) and broadcasts tensor-by-tensor.
58
+ * For small models this is fine; for larger models consider sharded state_dict type.
59
+ """
60
+
61
+ @with_defer(lambda: Timer().start("train_wait"))
62
+ def init(self, args: Namespace, role: str, with_ref: bool = False) -> int: # type: ignore[override]
63
+ super().init(args, role, with_ref)
64
+
65
+ # Setup device mesh for parallelism (handles both CP and non-CP cases)
66
+ self._setup_device_mesh()
67
+ torch.manual_seed(args.seed)
68
+
69
+ self.train_parallel_config = {
70
+ "dp_size": self.dp_size,
71
+ }
72
+
73
+ if self.args.debug_rollout_only:
74
+ return 0
75
+
76
+ self.fsdp_cpu_offload = getattr(self.args, "fsdp_cpu_offload", False)
77
+ # Offload train and fsdp cpu offload cannot be used together, fsdp_cpu_offload is more aggressive
78
+ if self.args.offload_train and self.fsdp_cpu_offload:
79
+ self.args.offload_train = False
80
+
81
+ self._enable_true_on_policy_optimizations(args)
82
+ if dist.get_rank() == 0:
83
+ init_tracking(args, primary=False)
84
+
85
+ if getattr(self.args, "start_rollout_id", None) is None:
86
+ self.args.start_rollout_id = 0
87
+
88
+ self.prof = TrainProfiler(args)
89
+
90
+ for i in range(dist.get_world_size()):
91
+ if i == dist.get_rank():
92
+ self.hf_config = AutoConfig.from_pretrained(self.args.hf_checkpoint, trust_remote_code=True)
93
+ self.tokenizer = load_tokenizer(self.args.hf_checkpoint, trust_remote_code=True)
94
+ # Vision models have `vision_config` in the config
95
+ if hasattr(self.hf_config, "vision_config"):
96
+ self.processor = load_processor(self.args.hf_checkpoint, trust_remote_code=True)
97
+ dist.barrier(group=get_gloo_group())
98
+
99
+ init_context = self._get_init_weight_context_manager()
100
+
101
+ with init_context():
102
+ model = self.get_model_cls().from_pretrained(
103
+ self.args.hf_checkpoint,
104
+ trust_remote_code=True,
105
+ attn_implementation=self.args.attn_implementation,
106
+ )
107
+
108
+ model.train()
109
+
110
+ full_state = model.state_dict()
111
+
112
+ model = apply_fsdp2(model, mesh=self.dp_mesh, cpu_offload=self.fsdp_cpu_offload, args=self.args)
113
+
114
+ model = self._fsdp2_load_full_state_dict(
115
+ model, full_state, self.dp_mesh, cpu_offload=True if self.fsdp_cpu_offload else None
116
+ )
117
+
118
+ self.model = model
119
+
120
+ if args.gradient_checkpointing:
121
+ self.model.gradient_checkpointing_enable()
122
+
123
+ if args.optimizer == "adam":
124
+ self.optimizer = torch.optim.AdamW(
125
+ self.model.parameters(),
126
+ lr=args.lr,
127
+ betas=(args.adam_beta1, args.adam_beta2),
128
+ eps=args.adam_eps,
129
+ weight_decay=args.weight_decay,
130
+ )
131
+ else:
132
+ raise ValueError(f"Unsupported optimizer: {args.optimizer}. Supported options: 'adam'")
133
+
134
+ # Initialize LR scheduler
135
+ self.lr_scheduler = get_lr_scheduler(args, self.optimizer)
136
+
137
+ self.global_step = 0
138
+ self.micro_step = 0
139
+
140
+ checkpoint_payload = checkpoint.load(self)
141
+
142
+ # Create separate ref model if needed (kept in CPU until needed)
143
+ self.ref_model = None
144
+ if with_ref:
145
+ self.ref_model = self._create_ref_model(args.ref_load)
146
+
147
+ self.weight_updater = (
148
+ UpdateWeightFromTensor(self.args, self.model)
149
+ if self.args.colocate
150
+ else UpdateWeightFromDistributed(self.args, self.model)
151
+ )
152
+
153
+ checkpoint.finalize_load(self, checkpoint_payload)
154
+
155
+ # Initialize data packing parameters
156
+ self.max_tokens_per_gpu = args.max_tokens_per_gpu # From main arguments
157
+
158
+ if self.args.offload_train:
159
+ self.sleep()
160
+
161
+ self.prof.on_init_end()
162
+
163
+ return int(getattr(self.args, "start_rollout_id", 0))
164
+
165
+ def get_model_cls(self):
166
+ # Vision models have `vision_config` in the config
167
+ if hasattr(self.hf_config, "vision_config"):
168
+ from transformers import AutoModelForImageTextToText
169
+
170
+ return AutoModelForImageTextToText
171
+ else:
172
+ from transformers import AutoModelForCausalLM
173
+
174
+ return AutoModelForCausalLM
175
+
176
+ def _enable_true_on_policy_optimizations(self, args):
177
+ if args.true_on_policy_mode:
178
+ from sglang.srt.batch_invariant_ops import enable_batch_invariant_mode
179
+
180
+ from .models.qwen3_moe import apply_true_on_policy_patch_for_qwen3_moe
181
+
182
+ logger.info("FSDPTrainRayActor call enable_batch_invariant_mode for true-on-policy")
183
+ enable_batch_invariant_mode(
184
+ # In Qwen3, rope `inv_freq_expanded.float() @ position_ids_expanded.float()` uses bmm
185
+ # and disabling it will make it aligned
186
+ enable_bmm=False,
187
+ )
188
+
189
+ apply_true_on_policy_patch_for_qwen3_moe()
190
+ else:
191
+ from .models.qwen3_moe_hf import apply_fsdp_moe_patch
192
+
193
+ apply_fsdp_moe_patch()
194
+
195
+ def _setup_device_mesh(self) -> None:
196
+ """Setup device mesh for parallelism (always called, handles both CP and non-CP cases).
197
+
198
+ Creates 2D mesh (dp_size, cp_size) for all cases:
199
+ - When context_parallel_size > 1: hybrid CP + DP
200
+ - When context_parallel_size = 1: pure DP (equivalent to 1D mesh)
201
+
202
+ This ensures consistent group management across all parallelism modes.
203
+ """
204
+ from torch.distributed.device_mesh import init_device_mesh
205
+
206
+ world_size = dist.get_world_size()
207
+ rank = dist.get_rank()
208
+
209
+ # Use context_parallel_size directly (defaults to 1 for pure DP)
210
+ self.cp_size = self.args.context_parallel_size
211
+ self.dp_size = world_size // self.cp_size
212
+
213
+ # Create 2D device mesh: (dp_size, cp_size)
214
+ # Ranks laid out in row-major: mesh[dp_idx, cp_idx] = dp_idx * cp_size + cp_idx
215
+ # - CP groups: consecutive ranks along dim 1, e.g., [0,1], [2,3], [4,5], [6,7]
216
+ # - DP groups: striped ranks along dim 0, e.g., [0,2,4,6], [1,3,5,7]
217
+ # When cp_size=1, this degenerates to pure DP
218
+ self.mesh = init_device_mesh("cuda", mesh_shape=(self.dp_size, self.cp_size), mesh_dim_names=("dp", "cp"))
219
+
220
+ # Extract process groups from mesh
221
+ self.dp_group = self.mesh.get_group("dp") # For FSDP gradient sync, metric reduction
222
+ self.cp_group = self.mesh.get_group("cp") # For Ring Flash Attention, logit gathering
223
+ self.dp_mesh = self.mesh["dp"] # For FSDP
224
+
225
+ # Compute local ranks within each dimension
226
+ self.dp_rank = rank // self.cp_size
227
+ self.cp_rank = rank % self.cp_size
228
+
229
+ logger.info(
230
+ f"[Rank {rank}] Device mesh (2D): world_size={world_size}, "
231
+ f"cp_size={self.cp_size}, dp_size={self.dp_size}"
232
+ )
233
+ logger.info(f"[Rank {rank}] Mesh shape: {self.mesh.shape}, " f"dp_rank={self.dp_rank}, cp_rank={self.cp_rank}")
234
+
235
+ # Setup Ring Flash Attention with CP group from mesh (only when cp_size > 1)
236
+ if self.cp_size > 1:
237
+ substitute_hf_flash_attn(self.cp_group, heads_k_stride=1)
238
+ logger.info(f"[Rank {rank}] CP initialized via device mesh")
239
+ else:
240
+ logger.info(f"[Rank {rank}] Pure DP mode (cp_size=1)")
241
+
242
+ def _get_init_weight_context_manager(self):
243
+ """Get context manager for model initialization.
244
+
245
+ Returns a callable that creates a context manager.
246
+ Uses meta device (no memory allocation) for non-rank-0 processes,
247
+ UNLESS tie_word_embeddings=True (which causes hangs with meta tensors).
248
+
249
+ Ref: verl/utils/fsdp_utils.py::get_init_weight_context_manager
250
+ NOTE: tie_word_embedding causes meta_tensor init to hang
251
+ """
252
+ from accelerate import init_empty_weights
253
+
254
+ # Check if model uses tied word embeddings (which doesn't work with meta tensors)
255
+ use_meta_tensor = not self.hf_config.tie_word_embeddings
256
+
257
+ def cpu_init_weights():
258
+ return torch.device("cpu")
259
+
260
+ if use_meta_tensor:
261
+ # Rank 0: CPU, others: meta device (memory efficient for large models)
262
+ return init_empty_weights if dist.get_rank() != 0 else cpu_init_weights
263
+ else:
264
+ logger.info(f"[Rank {dist.get_rank()}] tie_word_embeddings=True, loading full model to CPU on all ranks")
265
+ return cpu_init_weights
266
+
267
+ def _fsdp2_load_full_state_dict(self, model, full_state, device_mesh, cpu_offload):
268
+ """Load full state dict into FSDP2 model with efficient broadcast from rank 0.
269
+
270
+ This function loads weights from rank 0 and broadcasts to all other ranks,
271
+ avoiding the need for each rank to load the full model from disk.
272
+
273
+ Args:
274
+ model: FSDP2-wrapped model
275
+ full_state: State dict (only rank 0 has real weights, others have empty dict)
276
+ device_mesh: Device mesh for FSDP
277
+ cpu_offload: If not None, enables StateDictOptions cpu_offload
278
+
279
+ Ref:verl/utils/fsdp_utils.py::fsdp2_load_full_state_dict
280
+ """
281
+ from torch.distributed.checkpoint.state_dict import StateDictOptions, set_model_state_dict
282
+
283
+ # Rank 0: move with weights, others: allocate empty tensors on device
284
+ if dist.get_rank() == 0:
285
+ model = model.to(device=torch.cuda.current_device(), non_blocking=True)
286
+ else:
287
+ # to_empty creates tensors on device without initializing memory
288
+ model = model.to_empty(device=torch.cuda.current_device())
289
+
290
+ is_cpu_offload = cpu_offload is not None
291
+ options = StateDictOptions(full_state_dict=True, cpu_offload=is_cpu_offload, broadcast_from_rank0=True)
292
+
293
+ set_model_state_dict(model, full_state, options=options)
294
+
295
+ # set_model_state_dict will not broadcast buffers, so we need to broadcast them manually.
296
+ for _name, buf in model.named_buffers():
297
+ dist.broadcast(buf, src=0)
298
+
299
+ if is_cpu_offload:
300
+ model.to("cpu", non_blocking=True)
301
+ for buf in model.buffers():
302
+ buf.data = buf.data.to(torch.cuda.current_device())
303
+
304
+ return model
305
+
306
+ @timer
307
+ def sleep(self) -> None:
308
+ """Pause CUDA memory for all tracked tensors."""
309
+ if not self.args.offload_train:
310
+ return
311
+
312
+ print_memory("before offload model")
313
+
314
+ self.model.cpu()
315
+ move_torch_optimizer(self.optimizer, "cpu")
316
+ clear_memory()
317
+ dist.barrier(group=get_gloo_group())
318
+ print_memory("after offload model")
319
+
320
+ @timer
321
+ def wake_up(self) -> None:
322
+ """Resume CUDA memory for all tracked tensors."""
323
+ if not self.args.offload_train:
324
+ return
325
+
326
+ self.model.cuda()
327
+ move_torch_optimizer(self.optimizer, "cuda")
328
+ dist.barrier(group=get_gloo_group())
329
+ print_memory("after wake_up model")
330
+
331
+ def save_model(self, rollout_id: int, force_sync: bool = False) -> None:
332
+ """Delegate checkpoint saving to the shared checkpoint utilities."""
333
+ if self.args.debug_rollout_only or self.args.save is None:
334
+ return
335
+
336
+ assert not self.args.async_save, "FSDPTrainRayActor does not support async_save yet."
337
+ checkpoint.save(self, rollout_id)
338
+
339
+ def _compute_log_prob(
340
+ self,
341
+ model_tag: str,
342
+ packed_batches: list[dict[str, torch.Tensor]],
343
+ store_prefix: str = "",
344
+ ) -> dict[str, list[torch.Tensor]]:
345
+ """Compute token log-probabilities for a list of packed batches.
346
+
347
+ Parameters:
348
+ model_tag: Which parameters to use, e.g. "actor" or "ref".
349
+ packed_batches: A list of packed batch dictionaries produced by
350
+ `pack_sequences`, each containing at least `tokens` and
351
+ `position_ids`; may also include multimodal keys like `pixel_values`.
352
+ store_prefix: Prefix to use for keys in outputs (e.g., "ref_").
353
+
354
+ Returns:
355
+ A lightweight dictionary keyed by f"{store_prefix}log_probs". The
356
+ actual per-sequence results are written in-place into each element of
357
+ `packed_batches` under the same key and can be read back by callers.
358
+
359
+ Note:
360
+ Uses separate ref model when model_tag == "ref". The ref model is
361
+ loaded from CPU to GPU on-demand and offloaded back after use.
362
+ """
363
+ # Select which model to use
364
+ if model_tag == "ref" and self.ref_model is not None:
365
+ if not self.fsdp_cpu_offload:
366
+ self.model.cpu()
367
+ torch.cuda.empty_cache()
368
+ dist.barrier(group=get_gloo_group())
369
+
370
+ active_model = self.ref_model
371
+ active_model.eval()
372
+ else:
373
+ active_model = self.model
374
+
375
+ try:
376
+ rollout_data = {f"{store_prefix}log_probs": []}
377
+ with timer(f"{store_prefix}log_probs"), torch.no_grad():
378
+ for batch in self.prof.iterate_train_log_probs(
379
+ tqdm(packed_batches, desc=f"{store_prefix}log_probs", disable=dist.get_rank() != 0)
380
+ ):
381
+ model_args = self._get_model_inputs_args(batch)
382
+ logits = active_model(**model_args).logits.squeeze(0).float()
383
+ log_probs_result, entropy_result = get_logprob_and_entropy_with_cp(
384
+ logits=logits,
385
+ target_tokens=batch["tokens"],
386
+ cp_rank=self.cp_rank,
387
+ cp_size=self.cp_size,
388
+ cp_group=self.cp_group,
389
+ model_input_ids=model_args["input_ids"],
390
+ allow_compile=not self.args.true_on_policy_mode,
391
+ temperature=self.args.rollout_temperature,
392
+ )
393
+ batch[f"{store_prefix}log_probs"] = log_probs_result
394
+ if store_prefix == "":
395
+ batch["entropy"] = entropy_result
396
+ return rollout_data
397
+
398
+ finally:
399
+ # Restore actor model if it was offloaded
400
+ if model_tag == "ref" and self.ref_model is not None:
401
+ torch.cuda.empty_cache()
402
+ dist.barrier(group=get_gloo_group())
403
+
404
+ if not self.fsdp_cpu_offload:
405
+ self.model.cuda()
406
+ dist.barrier(group=get_gloo_group())
407
+
408
+ def _packed_data(
409
+ self, rollout_data: dict[str, list[torch.Tensor]]
410
+ ) -> tuple[list[dict[str, torch.Tensor]], list[int]]:
411
+ """Pack variable-length sequences for efficient processing.
412
+
413
+ Parameters:
414
+ rollout_data: Dictionary of lists containing sequence-level tensors
415
+ such as `tokens`, `loss_masks`, `rewards`, `response_lengths`,
416
+ `advantages`, `returns`, and optional `rollout_log_probs`.
417
+
418
+ Returns:
419
+ A pair `(packed_batches, grad_accum)` where `packed_batches` is a list
420
+ of packed batch dictionaries and `grad_accum` lists the micro-batch
421
+ indices at which to perform optimizer steps.
422
+ """
423
+ # Pack sequences efficiently
424
+ tokens = rollout_data["tokens"]
425
+
426
+ packed_batches = []
427
+ mbs_size_list = []
428
+ local_batch_size = self.args.global_batch_size // self.dp_size
429
+ assert (
430
+ self.args.global_batch_size % self.dp_size == 0
431
+ ), f"global_batch_size {self.args.global_batch_size} is not divisible by dp_world_size {self.dp_size}"
432
+ # Use global_batch_size for splitting when max_tokens_per_gpu is enabled
433
+ if self.args.use_dynamic_batch_size:
434
+ # In CP mode, CP group shares sequences, so total capacity is max_tokens_per_gpu * cp_size
435
+ max_tokens = self.args.max_tokens_per_gpu
436
+ if self.cp_size > 1:
437
+ max_tokens = max_tokens * self.cp_size
438
+
439
+ for i in range(0, len(tokens), local_batch_size):
440
+ mbs_size_list.append(
441
+ get_minimum_num_micro_batch_size(
442
+ [len(t) for t in rollout_data["tokens"][i : i + local_batch_size]],
443
+ max_tokens,
444
+ )
445
+ )
446
+ num_microbatches = torch.tensor(mbs_size_list, dtype=torch.int, device=torch.cuda.current_device())
447
+ dist.all_reduce(num_microbatches, op=dist.ReduceOp.MAX, group=self.dp_group)
448
+ num_microbatches = num_microbatches.tolist()
449
+ else:
450
+ num_microbatches = [self.args.global_batch_size // (self.args.micro_batch_size * self.dp_size)] * (
451
+ len(tokens) // local_batch_size
452
+ )
453
+
454
+ start = 0
455
+ for mbs_size in num_microbatches:
456
+ end = start + local_batch_size
457
+ packed_batches.extend(
458
+ pack_sequences(
459
+ rollout_data["tokens"][start:end],
460
+ rollout_data["loss_masks"][start:end],
461
+ rollout_data["rewards"][start:end],
462
+ rollout_data["raw_reward"][start:end],
463
+ rollout_data["response_lengths"][start:end],
464
+ rollout_data["advantages"][start:end],
465
+ rollout_data["returns"][start:end],
466
+ rollout_log_probs=(
467
+ rollout_data["rollout_log_probs"][start:end] if "rollout_log_probs" in rollout_data else None
468
+ ),
469
+ multimodal_train_inputs=(
470
+ rollout_data["multimodal_train_inputs"][start:end]
471
+ if "multimodal_train_inputs" in rollout_data
472
+ else None
473
+ ),
474
+ num_packs=mbs_size,
475
+ )
476
+ )
477
+ start = end
478
+ grad_accum = list(accumulate(num_microbatches))
479
+
480
+ return packed_batches, grad_accum
481
+
482
+ def train(self, rollout_id: int, rollout_data_ref: Box) -> None:
483
+ """Run one training update over a rollout batch.
484
+
485
+ Parameters:
486
+ rollout_id: Monotonic id for logging.
487
+ rollout_data_ref: A Box handle wrapping a Ray object reference to a
488
+ dictionary with rollout tensors and metadata (e.g., `tokens`,
489
+ `loss_masks`, `rewards`, `response_lengths`, optional
490
+ `rollout_log_probs`, etc.). It will be fetched and partitioned
491
+ by `process_rollout_data` based on data-parallel rank/size.
492
+ """
493
+ if self.args.offload_train:
494
+ self.wake_up()
495
+
496
+ with inverse_timer("train_wait"), timer("train"):
497
+ rollout_data = process_rollout_data(self.args, rollout_data_ref, self.dp_rank, self.dp_size)
498
+ if self.args.debug_rollout_only:
499
+ return
500
+ self._train_core(rollout_id=rollout_id, rollout_data=rollout_data)
501
+
502
+ train_metric_utils.log_perf_data_raw(
503
+ rollout_id=rollout_id,
504
+ args=self.args,
505
+ is_primary_rank=dist.get_rank() == 0,
506
+ compute_total_fwd_flops=None,
507
+ )
508
+
509
+ def _log_rollout_data(self, rollout_id: int, rollout_data, packed_batches):
510
+ log_dict = {}
511
+ if "raw_reward" in rollout_data and dist.get_rank() == 0:
512
+ raw_reward_list = rollout_data["raw_reward"]
513
+ if raw_reward_list:
514
+ log_dict["rollout/raw_reward"] = sum(raw_reward_list) / len(raw_reward_list)
515
+
516
+ for metric_key in ["log_probs", "rollout_log_probs", "ref_log_probs", "advantages", "returns"]:
517
+ if metric_key not in packed_batches[0]:
518
+ continue
519
+ val = torch.tensor([0.0], device=torch.cuda.current_device())
520
+ for _mbs_id, batches in enumerate(packed_batches):
521
+ unpacked_batches = unpack_sequences(batches)
522
+ for unpacked_batch in unpacked_batches:
523
+ if isinstance(unpacked_batch[metric_key], torch.Tensor):
524
+ loss_masks_tensor = unpacked_batch["loss_masks"].to(device=torch.cuda.current_device())
525
+ metric_tensor = unpacked_batch[metric_key].to(device=torch.cuda.current_device())
526
+ val += (metric_tensor * loss_masks_tensor).sum() / loss_masks_tensor.sum().clamp_min(1)
527
+ else:
528
+ val += unpacked_batch[metric_key]
529
+ dist.all_reduce(val, op=dist.ReduceOp.SUM, group=self.dp_group)
530
+ log_dict[f"rollout/{metric_key}"] = (
531
+ val / (self.args.n_samples_per_prompt * self.args.rollout_batch_size)
532
+ ).item()
533
+ if dist.get_rank() == 0:
534
+ logger.info(f"rollout {rollout_id}: {log_dict}")
535
+ log_dict["rollout/step"] = compute_rollout_step(self.args, rollout_id)
536
+ tracking_utils.log(self.args, log_dict, step_key="rollout/step")
537
+
538
+ if self.args.ci_test and self.args.true_on_policy_mode:
539
+ assert log_dict["rollout/log_probs"] == log_dict["rollout/rollout_log_probs"], (
540
+ f"CI check failed: true_on_policy_mode is enabled, but log_probs "
541
+ f"({log_dict['rollout/log_probs']}) != rollout_log_probs "
542
+ f"({log_dict['rollout/rollout_log_probs']})"
543
+ )
544
+
545
+ def _train_core(self, rollout_id: int, rollout_data) -> None:
546
+ if self.args.advantage_estimator in ["grpo", "gspo"]:
547
+ rollout_data["advantages"] = rollout_data["returns"] = [
548
+ torch.tensor([rollout_data["rewards"][i]] * rollout_data["response_lengths"][i])
549
+ for i in range(len(rollout_data["rewards"]))
550
+ ]
551
+ else:
552
+ raise NotImplementedError(f"Unsupported advantage_estimator {self.args.advantage_estimator}")
553
+
554
+ packed_batches, grad_accum = self._packed_data(rollout_data)
555
+
556
+ assert (
557
+ len(grad_accum) > 0
558
+ ), f"Invalid grad_accum {grad_accum} for micro_batch_size {self.args.micro_batch_size} and global_batch_size {self.args.global_batch_size}"
559
+
560
+ if self.ref_model is not None:
561
+ self._compute_log_prob("ref", packed_batches, store_prefix="ref_")
562
+
563
+ self._compute_log_prob("actor", packed_batches)
564
+ self._log_rollout_data(rollout_id, rollout_data, packed_batches)
565
+
566
+ with timer("actor_train"):
567
+ reported_accum: dict[str, list[torch.Tensor]] = {}
568
+ self.optimizer.zero_grad(set_to_none=True)
569
+ for mbs_id, packed_batch in self.prof.iterate_train_actor(
570
+ enumerate(tqdm(packed_batches, desc="actor_train", disable=dist.get_rank() != 0))
571
+ ):
572
+ self._train_step(
573
+ packed_batch=packed_batch,
574
+ reported_accum=reported_accum,
575
+ mbs_id=mbs_id,
576
+ grad_accum=grad_accum,
577
+ )
578
+
579
+ self.prof.step(rollout_id=rollout_id)
580
+
581
+ train_dump_utils.save_debug_train_data(self.args, rollout_id=rollout_id, rollout_data=rollout_data)
582
+
583
+ # Update ref model if needed (copy actor weights to ref)
584
+ if (
585
+ self.args.ref_update_interval is not None
586
+ and (rollout_id + 1) % self.args.ref_update_interval == 0
587
+ and self.ref_model is not None
588
+ ):
589
+ if dist.get_rank() == 0:
590
+ logger.info(f"Updating ref model at rollout_id {rollout_id}")
591
+ # Copy actor model state to ref model
592
+ actor_state = self.model.state_dict()
593
+ self.ref_model.load_state_dict(actor_state)
594
+ self.ref_model.cpu()
595
+
596
+ def _train_step(self, packed_batch, reported_accum, mbs_id, grad_accum):
597
+ # Prepare model inputs
598
+ model_args = self._get_model_inputs_args(packed_batch)
599
+ logits = self.model(**model_args).logits.squeeze(0).float()
600
+
601
+ # Compute log probs and entropy (unified for both CP and non-CP modes)
602
+ log_probs, entropy_result = get_logprob_and_entropy_with_cp(
603
+ logits=logits,
604
+ target_tokens=packed_batch["tokens"],
605
+ cp_rank=self.cp_rank,
606
+ cp_size=self.cp_size,
607
+ cp_group=self.cp_group,
608
+ model_input_ids=model_args["input_ids"],
609
+ allow_compile=not self.args.true_on_policy_mode,
610
+ temperature=self.args.rollout_temperature,
611
+ )
612
+ packed_batch["cur_log_probs"] = log_probs
613
+ packed_batch["entropy"] = entropy_result
614
+
615
+ unpacked_batches = unpack_sequences(packed_batch)
616
+
617
+ old_log_prob_key = "rollout_log_probs" if self.args.use_rollout_logprobs else "log_probs"
618
+ missing_old_log_probs = [
619
+ idx
620
+ for idx, batch in enumerate(unpacked_batches)
621
+ if old_log_prob_key not in batch or not isinstance(batch[old_log_prob_key], torch.Tensor)
622
+ ]
623
+ if missing_old_log_probs:
624
+ raise KeyError(
625
+ f"{old_log_prob_key} must be provided as torch.Tensor for all microbatches when "
626
+ f"use_rollout_logprobs is set to {self.args.use_rollout_logprobs}. Missing in batches: {missing_old_log_probs}"
627
+ )
628
+ old_log_probs = torch.cat([batch[old_log_prob_key] for batch in unpacked_batches], dim=0)
629
+ log_probs = torch.cat([batch["cur_log_probs"] for batch in unpacked_batches], dim=0)
630
+ advantages = torch.cat([batch["advantages"] for batch in unpacked_batches], dim=0)
631
+ loss_masks = [batch["loss_masks"].to(device=log_probs.device) for batch in unpacked_batches]
632
+ response_lengths = [batch["response_lengths"] for batch in unpacked_batches]
633
+
634
+ advantages = advantages.to(device=log_probs.device)
635
+ old_log_probs = old_log_probs.to(device=log_probs.device)
636
+ ppo_kl = old_log_probs - log_probs
637
+
638
+ if self.args.use_opsm:
639
+ opsm_mask, opsm_clipfrac = compute_opsm_mask(
640
+ args=self.args,
641
+ full_log_probs=[batch["cur_log_probs"] for batch in unpacked_batches],
642
+ full_old_log_probs=[batch[old_log_prob_key] for batch in unpacked_batches],
643
+ advantages=[batch["advantages"] for batch in unpacked_batches],
644
+ loss_masks=loss_masks,
645
+ )
646
+
647
+ if self.args.advantage_estimator == "gspo":
648
+ ppo_kl = compute_gspo_kl(
649
+ full_log_probs=[batch["cur_log_probs"] for batch in unpacked_batches],
650
+ full_old_log_probs=[batch[old_log_prob_key] for batch in unpacked_batches],
651
+ local_log_probs=[batch["cur_log_probs"] for batch in unpacked_batches],
652
+ loss_masks=loss_masks,
653
+ )
654
+
655
+ pg_loss, pg_clipfrac = compute_policy_loss(ppo_kl, advantages, self.args.eps_clip, self.args.eps_clip_high)
656
+
657
+ if self.args.use_opsm:
658
+ pg_loss = pg_loss * opsm_mask
659
+
660
+ def _has_rollout_log_probs(batch) -> bool:
661
+ rollout_tensor = batch.get("rollout_log_probs")
662
+ return isinstance(rollout_tensor, torch.Tensor) and rollout_tensor.numel() > 0
663
+
664
+ has_rollout_log_probs = all(_has_rollout_log_probs(batch) for batch in unpacked_batches)
665
+ rollout_log_probs = (
666
+ torch.cat([batch["rollout_log_probs"] for batch in unpacked_batches], dim=0)
667
+ if has_rollout_log_probs
668
+ else None
669
+ )
670
+
671
+ # Apply off-policy correction using importance sampling if enabled
672
+ if self.args.use_tis:
673
+ assert (
674
+ has_rollout_log_probs and rollout_log_probs is not None
675
+ ), "rollout_log_probs must be provided as non-empty torch.Tensor for TIS/MIS"
676
+
677
+ train_log_probs_list = list(log_probs.split(response_lengths, dim=0))
678
+ rollout_log_probs_list = list(rollout_log_probs.split(response_lengths, dim=0))
679
+ ois = (-ppo_kl).exp()
680
+ tis_kwargs = {
681
+ "args": self.args,
682
+ "pg_loss": pg_loss,
683
+ "train_log_probs": train_log_probs_list,
684
+ "rollout_log_probs": rollout_log_probs_list,
685
+ "loss_masks": loss_masks,
686
+ "response_lengths": response_lengths,
687
+ "cp_rank": self.cp_rank,
688
+ "cp_size": self.cp_size,
689
+ "cp_group": self.cp_group,
690
+ }
691
+
692
+ if self.args.custom_tis_function_path is not None:
693
+ tis_func = load_function(self.args.custom_tis_function_path)
694
+ else:
695
+ tis_func = vanilla_tis_function
696
+ pg_loss, loss_masks, tis_metrics = tis_func(**tis_kwargs)
697
+
698
+ if self.args.calculate_per_token_loss:
699
+ pg_loss = sum_of_token(pg_loss, response_lengths, loss_masks)
700
+ pg_clipfrac = sum_of_token(pg_clipfrac, response_lengths, loss_masks)
701
+ ppo_kl = sum_of_token(ppo_kl.abs(), response_lengths, loss_masks)
702
+ else:
703
+ pg_loss = sum_of_sample_mean(pg_loss, response_lengths, loss_masks)
704
+ pg_clipfrac = sum_of_sample_mean(pg_clipfrac, response_lengths, loss_masks)
705
+ ppo_kl = sum_of_sample_mean(ppo_kl.abs(), response_lengths, loss_masks)
706
+
707
+ # Only compare rollout vs. train log probs when they originate from different stages.
708
+ train_rollout_logprob_abs_diff = None
709
+ if not self.args.use_rollout_logprobs and rollout_log_probs is not None:
710
+ train_rollout_logprob_abs_diff = (old_log_probs - rollout_log_probs).abs()
711
+ train_rollout_logprob_abs_diff = sum_of_sample_mean(
712
+ train_rollout_logprob_abs_diff, response_lengths, loss_masks
713
+ ).detach()
714
+
715
+ entropy = torch.cat([batch["entropy"] for batch in unpacked_batches], dim=0)
716
+ entropy_loss = sum_of_sample_mean(entropy, response_lengths, loss_masks)
717
+
718
+ loss = pg_loss - self.args.entropy_coef * entropy_loss
719
+
720
+ if self.args.use_kl_loss:
721
+ ref_log_probs = torch.cat([batch["ref_log_probs"] for batch in unpacked_batches], dim=0)
722
+ importance_ratio = None
723
+ if self.args.use_unbiased_kl:
724
+ importance_ratio = torch.exp(log_probs - old_log_probs)
725
+ kl = compute_approx_kl(
726
+ log_probs,
727
+ ref_log_probs,
728
+ kl_loss_type=self.args.kl_loss_type,
729
+ importance_ratio=importance_ratio,
730
+ )
731
+ kl_loss = sum_of_sample_mean(kl, response_lengths, loss_masks)
732
+
733
+ loss = loss + self.args.kl_loss_coef * kl_loss
734
+
735
+ reported = {
736
+ "loss": loss.detach(),
737
+ "pg_loss": pg_loss.detach(),
738
+ "pg_clipfrac": pg_clipfrac.detach(),
739
+ "ppo_kl": ppo_kl.detach(),
740
+ "entropy_loss": entropy_loss.detach(),
741
+ }
742
+
743
+ if train_rollout_logprob_abs_diff is not None:
744
+ reported["train_rollout_logprob_abs_diff"] = train_rollout_logprob_abs_diff
745
+
746
+ if self.args.use_kl_loss:
747
+ reported["kl_loss"] = kl_loss.detach()
748
+
749
+ if self.args.use_opsm:
750
+ reported["opsm_clipfrac"] = opsm_clipfrac
751
+
752
+ if self.args.use_tis and tis_metrics:
753
+ reported["ois"] = sum_of_sample_mean(ois, response_lengths, loss_masks).detach()
754
+ for k, v in tis_metrics.items():
755
+ if self.args.calculate_per_token_loss:
756
+ reported[k] = sum_of_token(v, response_lengths, loss_masks).detach()
757
+ else:
758
+ reported[k] = sum_of_sample_mean(v, response_lengths, loss_masks).detach()
759
+
760
+ # Scale loss for gradient accumulation
761
+ loss = loss * self.dp_size / self.args.global_batch_size
762
+ loss.backward()
763
+
764
+ # Accumulate reported metrics (store tensors for later mean)
765
+ for k, v in reported.items():
766
+ reported_accum.setdefault(k, []).append(v)
767
+
768
+ if (mbs_id + 1) in grad_accum:
769
+ # TODO: check if the grad norm is global grad norm.
770
+ grad_norm = torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.clip_grad)
771
+ # the grad norm used to be of DTensor
772
+ grad_norm = float(grad_norm)
773
+
774
+ self.optimizer.step()
775
+ # Update learning rate
776
+ self.lr_scheduler.step()
777
+ self.optimizer.zero_grad(set_to_none=True)
778
+ # Aggregate logs
779
+ aggregated = {k: torch.stack(v).sum().item() for k, v in reported_accum.items()}
780
+ # TODO: change this, this is slow.
781
+ reduced_aggregated = [None] * self.dp_size
782
+ dist.all_gather_object(reduced_aggregated, aggregated, group=self.dp_group)
783
+ aggregated = {}
784
+ for k in reported_accum.keys():
785
+ aggregated[k] = sum([r[k] for r in reduced_aggregated]) / (self.args.global_batch_size)
786
+ reported_accum.clear()
787
+ if dist.get_rank() == 0:
788
+ log_dict = {
789
+ f"train/{k}": (val.item() if torch.is_tensor(val) else val) for k, val in aggregated.items()
790
+ }
791
+ log_dict["train/grad_norm"] = grad_norm
792
+
793
+ # Log learning rate per parameter group; use scheduler's last computed LRs
794
+ lr_values = self.lr_scheduler.get_last_lr()
795
+ for gid, _group in enumerate(self.optimizer.param_groups):
796
+ log_dict[f"train/lr-pg_{gid}"] = lr_values[gid]
797
+
798
+ kl_info = ""
799
+ if self.args.use_kl_loss and "kl_loss" in aggregated:
800
+ kl_info = f", kl_loss: {aggregated['kl_loss']:.4f}, kl_penalty: {aggregated['kl_loss'] * self.args.kl_loss_coef:.4f}"
801
+ logger.info(kl_info)
802
+ logger.info(f"step {self.global_step}: {log_dict}")
803
+
804
+ log_dict["train/step"] = self.global_step
805
+ tracking_utils.log(self.args, log_dict, step_key="train/step")
806
+ self.global_step += 1
807
+
808
+ @timer
809
+ def update_weights(self) -> None: # type: ignore[override]
810
+ """Synchronize actor weights to rollout engines.
811
+
812
+ Handles both colocated and distributed update modes. In offload mode,
813
+ wakes up parameters as needed to perform the update.
814
+ """
815
+ if self.args.debug_train_only or self.args.debug_rollout_only:
816
+ return
817
+
818
+ rollout_engines, rollout_engine_lock, num_new_engines = ray.get(
819
+ self.rollout_manager.get_rollout_engines_and_lock.remote()
820
+ )
821
+ if num_new_engines > 0:
822
+ self.weight_updater.connect_rollout_engines(rollout_engines, rollout_engine_lock)
823
+ dist.barrier(group=get_gloo_group())
824
+
825
+ self.weight_updater.update_weights()
826
+
827
+ if self.args.ci_test and len(rollout_engines) > 0:
828
+ engine = random.choice(rollout_engines)
829
+ engine_version = ray.get(engine.get_weight_version.remote())
830
+ if str(engine_version) != str(self.weight_updater.weight_version):
831
+ raise RuntimeError(
832
+ f"Weight version mismatch! Engine: {engine_version}, Updater: {self.weight_updater.weight_version}"
833
+ )
834
+
835
+ clear_memory()
836
+
837
+ def _create_ref_model(self, ref_load_path: str | None):
838
+ """Create and initialize a separate reference model with FSDP2 CPUOffloadPolicy.
839
+
840
+ Parameters:
841
+ ref_load_path: Path to a directory containing a HF checkpoint. If
842
+ None, a ValueError is raised.
843
+
844
+ Returns:
845
+ FSDP2-wrapped ref model with CPU offload enabled
846
+
847
+ Note:
848
+ Creates a separate FSDP2 model instance for the reference model.
849
+ ALWAYS uses CPUOffloadPolicy for the reference model to save memory,
850
+ regardless of the actor model's CPU offload setting.
851
+ """
852
+ if ref_load_path is None:
853
+ raise ValueError("ref_load_path must be provided when loading reference model")
854
+
855
+ if os.path.isdir(ref_load_path):
856
+ logger.info(f"[Rank {dist.get_rank()}] Creating separate ref model from {ref_load_path}")
857
+
858
+ init_context = self._get_init_weight_context_manager()
859
+
860
+ with init_context():
861
+ ref_model = self.get_model_cls().from_pretrained(
862
+ ref_load_path,
863
+ trust_remote_code=True,
864
+ attn_implementation=self.args.attn_implementation,
865
+ )
866
+
867
+ full_state = ref_model.state_dict()
868
+
869
+ # Always use CPUOffloadPolicy for reference, let FSDP2 handle the offload. It is faster than model.cpu().
870
+ ref_model = apply_fsdp2(ref_model, mesh=self.dp_mesh, cpu_offload=True, args=self.args)
871
+ ref_model = self._fsdp2_load_full_state_dict(ref_model, full_state, self.dp_mesh, cpu_offload=True)
872
+
873
+ logger.info(f"[Rank {dist.get_rank()}] Reference model created with FSDP2 CPUOffloadPolicy")
874
+ return ref_model
875
+ else:
876
+ raise NotImplementedError(f"Loading from checkpoint file {ref_load_path} not yet implemented")
877
+
878
+ def _get_model_inputs_args(self, packed_sequence: dict) -> dict:
879
+ input_ids = packed_sequence["tokens"].unsqueeze(0)
880
+ position_ids = packed_sequence["position_ids"].unsqueeze(0)
881
+ if self.cp_size > 1:
882
+
883
+ packed_sequence = pad_packed_sequence_with_cp(packed_sequence, self.cp_size)
884
+
885
+ if not packed_sequence["cu_seqlens"].is_cuda:
886
+ packed_sequence["cu_seqlens"] = packed_sequence["cu_seqlens"].cuda()
887
+ cu_seqlens = packed_sequence["cu_seqlens"]
888
+ update_ring_flash_attn_params(cu_seqlens, self.cp_group)
889
+
890
+ input_ids = torch.chunk(packed_sequence["tokens"].unsqueeze(0), self.cp_size, dim=1)[self.cp_rank]
891
+ position_ids = torch.chunk(packed_sequence["position_ids"].unsqueeze(0), self.cp_size, dim=1)[self.cp_rank]
892
+
893
+ model_args = {
894
+ "input_ids": input_ids,
895
+ "position_ids": position_ids,
896
+ "attention_mask": None,
897
+ }
898
+ if packed_sequence.get("multimodal_train_inputs"):
899
+ model_args.update(packed_sequence["multimodal_train_inputs"])
900
+ return model_args
901
+
902
+
903
+ def selective_log_softmax_raw(logits: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor:
904
+ """Fused version of the common `log_softmax -> gather` operation.
905
+
906
+ The fused version of this operation avoids the (potentially large) memory overhead
907
+ of allocating a new tensor to store the full logprobs.
908
+
909
+ Parameters:
910
+ logits: Tensor of shape [..., V] containing model logits.
911
+ input_ids: Tensor of shape [...] of token indices whose log-probabilities are gathered.
912
+
913
+ Returns:
914
+ Tensor of shape [...] containing the log-probabilities corresponding to `input_ids`.
915
+ """
916
+ logprobs = logits.log_softmax(dim=-1)
917
+ return torch.gather(logprobs, dim=-1, index=input_ids.unsqueeze(-1)).squeeze(-1)
918
+
919
+
920
+ selective_log_softmax_compiled = torch.compile(dynamic=True)(selective_log_softmax_raw)
921
+
922
+
923
+ def gather_log_probs_packed(
924
+ shifted_logits: torch.Tensor,
925
+ input_ids: torch.Tensor,
926
+ allow_compile: bool,
927
+ cu_seqlens: torch.Tensor | float | None = None,
928
+ temperature: torch.Tensor | None = None,
929
+ ) -> torch.Tensor:
930
+ """Gather next-token log probabilities for packed sequences.
931
+
932
+ Parameters:
933
+ logits: Model logits of shape [B, T, V] or [T, V].
934
+ input_ids: Token ids of shape [B, T] or [T].
935
+ cu_seqlens: Optional cumulative sequence lengths (unused here). Present
936
+ for API compatibility with callers.
937
+
938
+ Returns:
939
+ A tensor of shape [T-1] (or [B, T-1]) with log-probabilities of targets.
940
+ """
941
+ # Handle batch dimension - logits should be [batch_size, seq_len, vocab_size]
942
+ if shifted_logits.dim() == 3:
943
+ # Remove batch dimension for packed sequences
944
+ shifted_logits = shifted_logits.squeeze(0)
945
+ input_ids = input_ids.squeeze(0)
946
+
947
+ if temperature is not None:
948
+ shifted_logits = shifted_logits.div(temperature)
949
+
950
+ targets = input_ids[1:].to(device=shifted_logits.device)
951
+
952
+ # Gather log probs for targets
953
+ selective_log_softmax = selective_log_softmax_compiled if allow_compile else selective_log_softmax_raw
954
+ return selective_log_softmax(shifted_logits, targets)
955
+
956
+
957
+ def get_logprob_and_entropy_with_cp(
958
+ logits: torch.Tensor,
959
+ target_tokens: torch.Tensor,
960
+ cp_rank: int,
961
+ cp_size: int,
962
+ cp_group,
963
+ model_input_ids: torch.Tensor,
964
+ allow_compile: bool,
965
+ temperature: float | None = None,
966
+ ) -> tuple[torch.Tensor, torch.Tensor]:
967
+ """Compute log probabilities and entropy in Context Parallel mode.
968
+
969
+ Parameters:
970
+ logits: Model output logits with shape [chunk_size, vocab_size]
971
+ target_tokens: Target tokens with shape [total_seq_len]
972
+ cp_rank: Current CP rank
973
+ cp_size: CP world size
974
+ cp_group: CP communication group
975
+ model_input_ids: Model input_ids (used for the last rank)
976
+ allow_compile: Whether to allow compilation
977
+ temperature: Temperature parameter (optional)
978
+
979
+ Returns:
980
+ log_probs: Aggregated log probabilities with shape [total_seq_len - 1]
981
+ entropy: Aggregated entropy with shape [total_seq_len - 1]
982
+ """
983
+ # Fast path for non-CP mode (cp_size=1): avoid unnecessary communication
984
+ if cp_size == 1:
985
+ shifted_logits = logits[:-1, :]
986
+ local_log_probs = gather_log_probs_packed(
987
+ shifted_logits, target_tokens, allow_compile=allow_compile, temperature=temperature
988
+ )
989
+ log_probs_full = torch.log_softmax(shifted_logits, dim=-1)
990
+ probs = torch.softmax(shifted_logits, dim=-1)
991
+ entropy = -(probs * log_probs_full).sum(dim=-1)
992
+ return local_log_probs, entropy
993
+
994
+ chunk_size = logits.shape[0]
995
+ tokens_start_index = chunk_size * cp_rank
996
+ tokens_end_index = (
997
+ tokens_start_index + chunk_size + 1 if cp_rank < cp_size - 1 else tokens_start_index + chunk_size
998
+ )
999
+
1000
+ # For the last rank, remove the last logit
1001
+ logits = logits if cp_rank < cp_size - 1 else logits[:-1, :]
1002
+
1003
+ # Get local tokens for current rank
1004
+ local_tokens = (
1005
+ target_tokens[tokens_start_index:tokens_end_index] if cp_rank < cp_size - 1 else model_input_ids.squeeze(0)
1006
+ )
1007
+
1008
+ # Compute local log probs
1009
+ local_log_probs = gather_log_probs_packed(
1010
+ logits, local_tokens, allow_compile=allow_compile, temperature=temperature
1011
+ )
1012
+
1013
+ # Pad for the last rank
1014
+ if cp_rank == cp_size - 1:
1015
+ local_log_probs = F.pad(local_log_probs, (0, chunk_size - local_log_probs.shape[0]), value=0)
1016
+
1017
+ # Compute entropy
1018
+ shifted_logits = logits[:-1, :] if cp_rank == cp_size - 1 else logits
1019
+ log_probs_full = torch.log_softmax(shifted_logits, dim=-1)
1020
+ probs = torch.softmax(shifted_logits, dim=-1)
1021
+ entropy = -(probs * log_probs_full).sum(dim=-1)
1022
+
1023
+ # Pad entropy for the last rank
1024
+ if cp_rank == cp_size - 1:
1025
+ entropy = F.pad(entropy, (0, chunk_size - entropy.shape[0]), value=0)
1026
+
1027
+ # Merge with a single all_gather: stack as [2, chunk_size]
1028
+ stacked_local = torch.stack([local_log_probs, entropy], dim=0)
1029
+ gathered_stacked = torch.distributed.nn.functional.all_gather(stacked_local, group=cp_group)
1030
+
1031
+ # Concatenate by effective length (non-last rank=chunk_size, last rank=chunk_size-1)
1032
+ lp_parts, ent_parts = [], []
1033
+ for r in range(cp_size):
1034
+ eff_len = chunk_size if r < cp_size - 1 else max(0, chunk_size - 1)
1035
+ if eff_len > 0:
1036
+ lp_parts.append(gathered_stacked[r][0][:eff_len])
1037
+ ent_parts.append(gathered_stacked[r][1][:eff_len])
1038
+
1039
+ log_probs = torch.cat(lp_parts, dim=0) if lp_parts else local_log_probs.new_zeros((0,))
1040
+ entropy_result = torch.cat(ent_parts, dim=0) if ent_parts else entropy.new_zeros((0,))
1041
+
1042
+ # Truncate to global effective length T-1 (packed tokens length is T)
1043
+ log_probs = log_probs[: len(target_tokens) - 1]
1044
+ entropy_result = entropy_result[: len(target_tokens) - 1]
1045
+
1046
+ return log_probs, entropy_result
1047
+
1048
+
1049
+ def sum_of_sample_mean(x: torch.Tensor, response_lengths: list[int], loss_masks: list[torch.Tensor]) -> torch.Tensor:
1050
+ """Compute sum of per-sample means across variable-length responses.
1051
+
1052
+ Parameters:
1053
+ x: Flat tensor containing concatenated per-token values across samples.
1054
+ response_lengths: Lengths of each sample's response segment in `x`.
1055
+ loss_masks: Per-sample masks aligned with `response_lengths`.
1056
+
1057
+ Returns:
1058
+ A scalar tensor equal to the sum over samples of the mean value within
1059
+ each sample's response segment.
1060
+ """
1061
+ return sum(
1062
+ [
1063
+ (x_i * loss_mask_i).sum() / torch.clamp_min(loss_mask_i.sum(), 1)
1064
+ for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False)
1065
+ ]
1066
+ )
1067
+
1068
+
1069
+ @torch.no_grad()
1070
+ def move_torch_optimizer(optimizer, device):
1071
+ """ref: https://github.com/volcengine/verl/blob/main/verl/utils/fsdp_utils.py"""
1072
+ if not optimizer.state:
1073
+ return
1074
+
1075
+ for param_group in optimizer.param_groups:
1076
+ for param in param_group["params"]:
1077
+ state = optimizer.state[param]
1078
+ for key, value in state.items():
1079
+ if isinstance(value, torch.Tensor):
1080
+ state[key] = value.to(device, non_blocking=True)
1081
+
1082
+ torch.cuda.synchronize()
1083
+
1084
+
1085
+ def apply_fsdp2(model, mesh=None, cpu_offload=False, args=None):
1086
+ """Apply FSDP v2 to the model.
1087
+
1088
+ Args:
1089
+ model: The model to wrap with FSDP
1090
+ mesh: Optional DeviceMesh for FSDP. If None, uses all ranks.
1091
+ cpu_offload: If True, offload parameters, gradients, and optimizer states
1092
+ to CPU. The optimizer step will run on CPU. (Default: False)
1093
+ args: Arguments containing precision settings (fp16/bf16)
1094
+
1095
+ Ref: https://github.com/volcengine/verl/blob/main/verl/utils/fsdp_utils.py
1096
+ """
1097
+ from torch.distributed.fsdp import CPUOffloadPolicy, MixedPrecisionPolicy, fully_shard
1098
+
1099
+ offload_policy = CPUOffloadPolicy() if cpu_offload else None
1100
+
1101
+ layer_cls_to_wrap = model._no_split_modules
1102
+ assert len(layer_cls_to_wrap) > 0 and layer_cls_to_wrap[0] is not None
1103
+
1104
+ modules = [
1105
+ module
1106
+ for name, module in model.named_modules()
1107
+ if module.__class__.__name__ in layer_cls_to_wrap
1108
+ or (isinstance(module, torch.nn.Embedding) and not model.config.tie_word_embeddings)
1109
+ ]
1110
+
1111
+ # Determine precision policy based on args
1112
+ param_dtype = torch.bfloat16 # Default to bf16 as before
1113
+ reduce_dtype = torch.float32
1114
+
1115
+ if args.fp16:
1116
+ param_dtype = torch.float16
1117
+
1118
+ logger.info(f"FSDP MixedPrecision Policy: param_dtype={param_dtype}, reduce_dtype={reduce_dtype}")
1119
+
1120
+ fsdp_kwargs = {
1121
+ "mp_policy": MixedPrecisionPolicy(
1122
+ param_dtype=param_dtype,
1123
+ reduce_dtype=reduce_dtype,
1124
+ ),
1125
+ "offload_policy": offload_policy,
1126
+ "mesh": mesh,
1127
+ }
1128
+
1129
+ # Apply FSDP to each module (offload_policy=None is equivalent to not passing it)
1130
+ for module in modules:
1131
+ fully_shard(module, **fsdp_kwargs)
1132
+
1133
+ # Apply FSDP to the top-level model
1134
+ fully_shard(model, **fsdp_kwargs)
1135
+
1136
+ return model
1137
+
1138
+
1139
+ def sum_of_token(x: torch.Tensor, response_lengths: list[int], loss_masks: list[torch.Tensor]) -> torch.Tensor:
1140
+ return sum(
1141
+ [
1142
+ (x_i * loss_mask_i).sum()
1143
+ for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False)
1144
+ ]
1145
+ )
slime/backends/fsdp_utils/arguments.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import argparse
5
+ import dataclasses
6
+ from dataclasses import dataclass
7
+
8
+ import yaml
9
+
10
+
11
+ @dataclass
12
+ class FSDPArgs:
13
+ # Optim
14
+ optimizer: str = "adam" # Optimizer type: "adam" (AdamW)
15
+ lr: float = 2e-5
16
+ lr_warmup_init: float = 0.0
17
+ min_lr: float = 0.0
18
+ lr_decay_style: str = "constant"
19
+ lr_decay_iters: int | None = None
20
+ lr_warmup_iters: int = 0
21
+ lr_warmup_fraction: float | None = None
22
+ lr_wsd_decay_iters: int | None = None
23
+ lr_wsd_decay_style: str | None = None
24
+ use_checkpoint_lr_scheduler: bool = True
25
+ override_lr_scheduler: bool = False
26
+ weight_decay: float = 0.0
27
+ adam_beta1: float = 0.9
28
+ adam_beta2: float = 0.95
29
+ adam_eps: float = 1e-8
30
+ warmup_ratio: float = 0.03
31
+
32
+ attn_implementation: str = "flash_attention_2"
33
+
34
+ # Logging
35
+ wandb_project: str = "slime-fsdp"
36
+ wandb_run_name: str | None = None
37
+
38
+ # Precision
39
+ gradient_checkpointing: bool = False
40
+ fp16: bool = False
41
+
42
+ # FSDP configuration
43
+ fsdp_state_dict_cpu_offload: bool = True # If True, offload full state dict to CPU during collection.
44
+ fsdp_cpu_offload: bool = (
45
+ False # If True, offload parameters, gradients, and optimizer states to CPU (optimizer runs on CPU)
46
+ )
47
+ fsdp_cpu_backend: str | None = (
48
+ "gloo" # CPU backend for FSDP CPU offload (e.g., "gloo"). Set to None to disable hybrid backend.
49
+ )
50
+
51
+ deterministic_mode: bool = False # This name must be the same as Megatron's
52
+
53
+ # Context Parallelism
54
+ context_parallel_size: int = 1 # Context Parallelism size
55
+ # Profile
56
+ record_memory_history: bool = False
57
+ memory_snapshot_path: str = "snapshot.pickle"
58
+ use_pytorch_profiler: bool = False
59
+ profile_step_start: int = 10
60
+ profile_step_end: int = 12
61
+ tensorboard_dir: str | None = None
62
+
63
+ # YAML bookkeeping
64
+ config: str | None = None
65
+
66
+
67
+ def parse_fsdp_cli(extra_args_provider=None):
68
+ parser = argparse.ArgumentParser("FSDP Training (slime)")
69
+ parser.add_argument("--config", type=str, default=None, help="YAML config path")
70
+ for f in dataclasses.fields(FSDPArgs):
71
+ if f.name == "config":
72
+ continue
73
+
74
+ # Handle union types like int | None, str | None, etc.
75
+ if hasattr(f.type, "__args__"): # Check if it's a Union type
76
+ # For T | None, use T as the type
77
+ non_none_types = [t for t in f.type.__args__ if t is not type(None)]
78
+ arg_type = non_none_types[0] if non_none_types else str
79
+ else:
80
+ arg_type = f.type
81
+
82
+ if arg_type is bool:
83
+ parser.add_argument(f"--{f.name.replace('_', '-')}", action="store_true")
84
+ else:
85
+ parser.add_argument(f"--{f.name.replace('_', '-')}", type=arg_type, default=f.default)
86
+
87
+ if extra_args_provider is not None:
88
+ parser = extra_args_provider(parser)
89
+ args = parser.parse_args()
90
+ return args
91
+
92
+
93
+ def load_fsdp_args(extra_args_provider=None):
94
+ args = parse_fsdp_cli(extra_args_provider)
95
+ if args.config:
96
+ with open(args.config) as f:
97
+ data = yaml.safe_load(f) or {}
98
+ for k, v in data.items():
99
+ if not hasattr(args, k):
100
+ setattr(args, k, v)
101
+ return args
slime/backends/fsdp_utils/checkpoint.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import logging
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import torch
13
+ import torch.distributed as dist
14
+ import torch.distributed.checkpoint as dcp
15
+ from torch.distributed.checkpoint.state_dict import get_state_dict, set_state_dict
16
+ from torch.distributed.checkpoint.stateful import Stateful
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ModelState(Stateful):
22
+ """Wrapper for model state only."""
23
+
24
+ def __init__(self, model):
25
+ self.model = model
26
+
27
+ def state_dict(self):
28
+ model_state_dict, _ = get_state_dict(self.model, optimizers=[])
29
+ return {"model": model_state_dict}
30
+
31
+ def load_state_dict(self, state_dict):
32
+ set_state_dict(self.model, optimizers=[], model_state_dict=state_dict["model"], optim_state_dict=None)
33
+
34
+
35
+ class OptimizerState(Stateful):
36
+ """Wrapper for optimizer state only."""
37
+
38
+ def __init__(self, model, optimizer):
39
+ self.model = model
40
+ self.optimizer = optimizer
41
+
42
+ def state_dict(self):
43
+ _, optimizer_state_dict = get_state_dict(self.model, optimizers=self.optimizer)
44
+ return {"optim": optimizer_state_dict}
45
+
46
+ def load_state_dict(self, state_dict):
47
+ set_state_dict(
48
+ self.model, optimizers=self.optimizer, model_state_dict=None, optim_state_dict=state_dict["optim"]
49
+ )
50
+
51
+
52
+ class LRSchedulerState(Stateful):
53
+ """Wrapper for LR scheduler state only."""
54
+
55
+ def __init__(self, lr_scheduler):
56
+ self.lr_scheduler = lr_scheduler
57
+
58
+ def state_dict(self):
59
+ return {"lr_scheduler": self.lr_scheduler.state_dict()}
60
+
61
+ def load_state_dict(self, state_dict):
62
+ self.lr_scheduler.load_state_dict(state_dict["lr_scheduler"])
63
+
64
+
65
+ def _read_checkpoint_metadata(path: Path) -> dict[str, Any]:
66
+ if not path.exists():
67
+ return {}
68
+ try:
69
+ return json.loads(path.read_text())
70
+ except json.JSONDecodeError:
71
+ logger.warning(f"Failed to parse checkpoint metadata at {path}")
72
+ return {}
73
+
74
+
75
+ def _write_checkpoint_metadata(path: Path, metadata: dict[str, Any]) -> None:
76
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
77
+ tmp_path.write_text(json.dumps(metadata, indent=2, sort_keys=True))
78
+ tmp_path.replace(path)
79
+
80
+
81
+ def load(actor: Any) -> dict[str, Any] | None:
82
+ """Load checkpoint from disk.
83
+
84
+ Loads model weights and optionally optimizer state from separate directories.
85
+ This allows loading weights without optimizer or deleting optimizer before loading.
86
+ """
87
+ load_root = getattr(actor.args, "load", None)
88
+ if load_root is None:
89
+ return None
90
+
91
+ root_path = Path(load_root).expanduser()
92
+ if not root_path.exists():
93
+ logger.info(f"[FSDP] Checkpoint directory {root_path} not found; skipping load.")
94
+ return None
95
+
96
+ target_step = getattr(actor.args, "ckpt_step", None)
97
+ if target_step is None:
98
+ tracker_file = root_path / "latest_checkpointed_iteration.txt"
99
+ if not tracker_file.exists():
100
+ logger.info(f"[FSDP] No tracker file at {tracker_file}; skipping load.")
101
+ return None
102
+ tracker_text = tracker_file.read_text().strip()
103
+ target_step = int(tracker_text)
104
+
105
+ checkpoint_dir = root_path / f"iter_{target_step:07d}"
106
+ model_dir = checkpoint_dir / "model"
107
+ optimizer_dir = checkpoint_dir / "optimizer"
108
+ lr_scheduler_dir = checkpoint_dir / "lr_scheduler"
109
+
110
+ if not model_dir.exists():
111
+ logger.info(f"[FSDP] Model checkpoint {model_dir} not found; skipping load.")
112
+ return None
113
+
114
+ # Load model weights (always)
115
+ model_state = ModelState(actor.model)
116
+ state_dict = {"model_state": model_state}
117
+
118
+ try:
119
+ dcp.load(state_dict=state_dict, checkpoint_id=str(model_dir))
120
+ logger.info(f"[FSDP] Loaded model from {model_dir}")
121
+ except Exception as e:
122
+ logger.error(f"[FSDP] Failed to load model from {model_dir}: {e}")
123
+ return None
124
+
125
+ # Load optimizer state (optional)
126
+ load_optimizer = not getattr(actor.args, "no_load_optim", False) and hasattr(actor, "optimizer")
127
+ if load_optimizer and optimizer_dir.exists():
128
+ optimizer_state = OptimizerState(actor.model, actor.optimizer)
129
+ optim_state_dict = {"optim_state": optimizer_state}
130
+ try:
131
+ dcp.load(state_dict=optim_state_dict, checkpoint_id=str(optimizer_dir))
132
+ logger.info(f"[FSDP] Loaded optimizer from {optimizer_dir}")
133
+ except Exception as e:
134
+ logger.warning(f"[FSDP] Failed to load optimizer from {optimizer_dir}: {e}")
135
+ elif load_optimizer:
136
+ logger.info(f"[FSDP] Optimizer checkpoint not found at {optimizer_dir}, skipping optimizer load.")
137
+
138
+ # Load LR scheduler state (optional)
139
+ load_lr_scheduler = hasattr(actor, "lr_scheduler") and lr_scheduler_dir.exists()
140
+ if load_lr_scheduler:
141
+ lr_scheduler_state = LRSchedulerState(actor.lr_scheduler)
142
+ lr_scheduler_state_dict = {"lr_scheduler_state": lr_scheduler_state}
143
+ try:
144
+ dcp.load(state_dict=lr_scheduler_state_dict, checkpoint_id=str(lr_scheduler_dir))
145
+ logger.info(f"[FSDP] Loaded LR scheduler from {lr_scheduler_dir}")
146
+ except Exception as e:
147
+ logger.warning(f"[FSDP] Failed to load LR scheduler from {lr_scheduler_dir}: {e}")
148
+ elif hasattr(actor, "lr_scheduler"):
149
+ logger.info(f"[FSDP] LR scheduler checkpoint not found at {lr_scheduler_dir}, skipping LR scheduler load.")
150
+
151
+ rng_state = None
152
+ rng_path = checkpoint_dir / "rng.pt"
153
+ if rng_path.exists():
154
+ rng_state = torch.load(rng_path, map_location="cpu")
155
+
156
+ metadata = _read_checkpoint_metadata(checkpoint_dir / "meta.json")
157
+
158
+ return {
159
+ "rng": rng_state,
160
+ "metadata": metadata,
161
+ "iteration": target_step,
162
+ }
163
+
164
+
165
+ def finalize_load(actor: Any, checkpoint_payload: dict[str, Any] | None) -> None:
166
+ if checkpoint_payload is None:
167
+ dist.barrier()
168
+ return
169
+
170
+ if checkpoint_payload.get("rng") is not None and not getattr(actor.args, "no_load_rng", False):
171
+ rng_state = checkpoint_payload["rng"]
172
+ if "torch" in rng_state:
173
+ torch.set_rng_state(rng_state["torch"])
174
+ if torch.cuda.is_available() and "cuda" in rng_state:
175
+ torch.cuda.set_rng_state_all(rng_state["cuda"])
176
+
177
+ metadata = checkpoint_payload.get("metadata") or {}
178
+ iteration = checkpoint_payload.get("iteration")
179
+ if metadata:
180
+ actor.global_step = int(metadata.get("global_step", actor.global_step))
181
+ actor.micro_step = int(metadata.get("micro_step", actor.micro_step))
182
+ next_rollout = metadata.get("next_rollout_id")
183
+ if next_rollout is not None:
184
+ actor.args.start_rollout_id = next_rollout
185
+ elif iteration is not None:
186
+ if getattr(actor.args, "start_rollout_id", None) is None:
187
+ actor.args.start_rollout_id = iteration
188
+
189
+ torch.cuda.synchronize()
190
+ dist.barrier()
191
+
192
+
193
+ def save(actor: Any, iteration: int) -> None:
194
+ """Save checkpoint to disk.
195
+
196
+ Saves model weights and optimizer state to separate directories.
197
+ This allows loading weights without optimizer or deleting optimizer before loading.
198
+ """
199
+ torch.cuda.synchronize()
200
+
201
+ base_dir = Path(actor.args.save).expanduser()
202
+ step_id = iteration + 1
203
+ checkpoint_dir = base_dir / f"iter_{step_id:07d}"
204
+ model_dir = checkpoint_dir / "model"
205
+ optimizer_dir = checkpoint_dir / "optimizer"
206
+ lr_scheduler_dir = checkpoint_dir / "lr_scheduler"
207
+
208
+ if dist.get_rank() == 0:
209
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
210
+ model_dir.mkdir(parents=True, exist_ok=True)
211
+ optimizer_dir.mkdir(parents=True, exist_ok=True)
212
+ lr_scheduler_dir.mkdir(parents=True, exist_ok=True)
213
+ dist.barrier()
214
+
215
+ # Save model weights
216
+ model_state = ModelState(actor.model)
217
+ state_dict = {"model_state": model_state}
218
+ dcp.save(state_dict, checkpoint_id=str(model_dir))
219
+
220
+ # Save optimizer state
221
+ if hasattr(actor, "optimizer") and actor.optimizer is not None:
222
+ optimizer_state = OptimizerState(actor.model, actor.optimizer)
223
+ optim_state_dict = {"optim_state": optimizer_state}
224
+ dcp.save(optim_state_dict, checkpoint_id=str(optimizer_dir))
225
+
226
+ # Save LR scheduler state
227
+ if hasattr(actor, "lr_scheduler") and actor.lr_scheduler is not None:
228
+ lr_scheduler_state = LRSchedulerState(actor.lr_scheduler)
229
+ lr_scheduler_state_dict = {"lr_scheduler_state": lr_scheduler_state}
230
+ dcp.save(lr_scheduler_state_dict, checkpoint_id=str(lr_scheduler_dir))
231
+
232
+ if dist.get_rank() == 0:
233
+ rng_state = {"torch": torch.get_rng_state()}
234
+ rng_state["cuda"] = torch.cuda.get_rng_state_all()
235
+ torch.save(rng_state, checkpoint_dir / "rng.pt")
236
+
237
+ metadata = {
238
+ "iteration": step_id,
239
+ "rollout_id": iteration,
240
+ "next_rollout_id": iteration + 1,
241
+ "global_step": actor.global_step,
242
+ "micro_step": actor.micro_step,
243
+ "world_size": dist.get_world_size(),
244
+ "timestamp": time.time(),
245
+ }
246
+ _write_checkpoint_metadata(checkpoint_dir / "meta.json", metadata)
247
+
248
+ tracker_file = base_dir / "latest_checkpointed_iteration.txt"
249
+ tracker_file.write_text(str(step_id))
250
+ logger.info(f"[FSDP] Saved checkpoint to {checkpoint_dir}")
251
+
252
+ dist.barrier()
slime/backends/fsdp_utils/data_packing.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Data packing utilities for FSDP backend to reduce padding overhead."""
5
+
6
+ import math
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+
11
+ from slime.utils.seqlen_balancing import get_seqlen_balanced_partitions
12
+
13
+
14
+ def pack_sequences(
15
+ tokens: list[list[int]],
16
+ loss_masks: list[list[int]],
17
+ rewards: list[float],
18
+ raw_rewards: list,
19
+ response_lengths: list[int],
20
+ advantages: list[float],
21
+ returns: list[float],
22
+ rollout_log_probs: list[list[float]] | None = None,
23
+ multimodal_train_inputs: list[dict] | None = None,
24
+ max_tokens_per_gpu: int | None = None,
25
+ num_packs: int | None = None,
26
+ ) -> list[dict]:
27
+ """
28
+ Pack sequences into dense batches with cumulative sequence lengths.
29
+
30
+ Args:
31
+ tokens: List of token sequences
32
+ loss_masks: List of loss masks
33
+ rewards: List of rewards per sequence
34
+ raw_rewards: List of raw rewards per sequence
35
+ response_lengths: List of response lengths per sequence
36
+ advantages: List of advantages per sequence
37
+ returns: List of returns per sequence
38
+ rollout_log_probs: List of rollout log probabilities per sequence
39
+ multimodal_train_inputs: List of dict of multimodal tensors for training per sequence
40
+ max_tokens_per_gpu: Maximum tokens per GPU pack
41
+ num_packs: Explicit number of packs to create
42
+
43
+ Returns:
44
+ List of packed batches with tokens, masks, cu_seqlens, rewards, raw_rewards, response_lengths, advantages, returns
45
+ """
46
+ if not tokens:
47
+ return []
48
+
49
+ seq_lengths = [len(t) for t in tokens]
50
+
51
+ # Determine number of packs and use balanced partitioning
52
+ if num_packs:
53
+ k_partitions = num_packs
54
+ elif max_tokens_per_gpu:
55
+ total_tokens = sum(seq_lengths)
56
+ k_partitions = max(1, math.ceil(total_tokens / max_tokens_per_gpu))
57
+ else:
58
+ k_partitions = 1
59
+
60
+ # Use balanced partitioning for optimal load distribution
61
+ partitions = get_seqlen_balanced_partitions(
62
+ seq_lengths, k_partitions=k_partitions, equal_size=False # Allow variable sizes for better balance
63
+ )
64
+
65
+ # Pack each partition
66
+ result = []
67
+ for indices in partitions:
68
+ # Build cumulative sequence lengths
69
+ cu_seqlens = [0]
70
+ flat_tokens = []
71
+ flat_masks = []
72
+ flat_positionids = []
73
+ flat_advantages = []
74
+ flat_returns = []
75
+ flat_rollout_log_probs = []
76
+
77
+ for i in indices:
78
+ seq_tokens = tokens[i]
79
+ seq_mask = loss_masks[i]
80
+ seq_positionids = list(range(len(seq_tokens)))
81
+
82
+ flat_tokens.extend(seq_tokens)
83
+ flat_positionids.extend(seq_positionids)
84
+ flat_masks.extend(seq_mask)
85
+ flat_advantages.extend(advantages[i])
86
+ flat_returns.extend(returns[i])
87
+ if rollout_log_probs:
88
+ flat_rollout_log_probs.extend(rollout_log_probs[i])
89
+ cu_seqlens.append(cu_seqlens[-1] + len(seq_tokens))
90
+
91
+ packed_batch = {
92
+ "tokens": torch.tensor(flat_tokens, dtype=torch.long),
93
+ "loss_masks": torch.tensor(flat_masks, dtype=torch.int),
94
+ "position_ids": torch.tensor(flat_positionids, dtype=torch.int),
95
+ "cu_seqlens": torch.tensor(cu_seqlens, dtype=torch.int32),
96
+ "rewards": torch.tensor([rewards[i] for i in indices], dtype=torch.float32),
97
+ "raw_reward": [raw_rewards[i] for i in indices],
98
+ "response_lengths": [response_lengths[i] for i in indices],
99
+ "advantages": torch.tensor(flat_advantages, dtype=torch.float32),
100
+ "returns": torch.tensor(flat_returns, dtype=torch.float32),
101
+ "rollout_log_probs": torch.tensor(
102
+ flat_rollout_log_probs, dtype=torch.float32, device=torch.cuda.current_device()
103
+ ),
104
+ }
105
+
106
+ # Collect and add multimodal training tensors for this partition
107
+ if multimodal_train_inputs:
108
+ multimodal_data = {} # key -> concatenated tensor
109
+ multimodal_num_items = {} # key -> list of item counts per sequence
110
+ for i in indices:
111
+ for key, mm_tensor in multimodal_train_inputs[i].items():
112
+ if key not in multimodal_data:
113
+ multimodal_data[key] = mm_tensor
114
+ multimodal_num_items[key] = [mm_tensor.size(0)]
115
+ else:
116
+ multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0)
117
+ multimodal_num_items[key].append(mm_tensor.size(0))
118
+ packed_batch["multimodal_train_inputs"] = multimodal_data
119
+ packed_batch["multimodal_num_items"] = multimodal_num_items
120
+
121
+ result.append(packed_batch)
122
+
123
+ return result
124
+
125
+
126
+ def unpack_sequences(packed_batch: dict) -> list[dict]:
127
+ """
128
+ Unpack sequences from a packed batch.
129
+
130
+ Args:
131
+ packed_batch: Packed batch
132
+
133
+ Returns:
134
+ List of unpacked batches
135
+ """
136
+
137
+ cu_seqlens = packed_batch["cu_seqlens"]
138
+ num_sequences = len(cu_seqlens) - 1
139
+ response_lengths = packed_batch["response_lengths"]
140
+ multimodal_num_items = packed_batch.get("multimodal_num_items", {})
141
+
142
+ instances = []
143
+
144
+ # Calculate pad_length by counting trailing zeros
145
+ tokens = packed_batch["tokens"]
146
+ nonzero_indices = (tokens != 0).nonzero(as_tuple=True)[0]
147
+ if len(nonzero_indices) > 0:
148
+ # Last non-zero index, pad_length is everything after it
149
+ pad_length = len(tokens) - nonzero_indices[-1].item() - 1
150
+ else:
151
+ pad_length = 0 # No padding if no non-zero tokens (or all zeros)
152
+ for i in range(num_sequences):
153
+ start_idx = cu_seqlens[i].item()
154
+ end_idx = cu_seqlens[i + 1].item()
155
+ instance = {}
156
+
157
+ # Copy any additional attributes that might exist in the packed batch
158
+ for key, value in packed_batch.items():
159
+ if key not in instance:
160
+ # Skip multimodal_num_items - it's metadata
161
+ if key == "multimodal_num_items":
162
+ continue
163
+ # Handle multimodal_train_inputs dict: split each tensor using multimodal_num_items
164
+ elif key == "multimodal_train_inputs" and isinstance(value, dict):
165
+ instance[key] = {}
166
+ for mm_key, mm_tensor in value.items():
167
+ if mm_key in multimodal_num_items:
168
+ num_items_list = multimodal_num_items[mm_key]
169
+ start_mm_idx = sum(num_items_list[:i])
170
+ end_mm_idx = start_mm_idx + num_items_list[i]
171
+ if num_items_list[i] > 0:
172
+ instance[key][mm_key] = mm_tensor[start_mm_idx:end_mm_idx]
173
+ # For tensor attributes, we need to slice them appropriately
174
+ elif isinstance(value, torch.Tensor):
175
+ if key in ["log_probs", "ref_log_probs", "cur_log_probs", "entropy"]:
176
+ # These are computed from logits[:-1] so they have length seq_len-1
177
+ instance[key] = value[
178
+ end_idx - 1 - response_lengths[i] - pad_length : end_idx - 1 - pad_length
179
+ ]
180
+ elif key == "rollout_log_probs":
181
+ # rollout_log_probs is packed based on response_lengths, so slice differently
182
+ instance[key] = value[sum(response_lengths[:i]) : sum(response_lengths[: i + 1])]
183
+ elif key in ["tokens", "position_ids"]:
184
+ # For other tensor attributes, try to slice them
185
+ if len(value) > start_idx:
186
+ instance[key] = value[start_idx:end_idx]
187
+ else:
188
+ raise ValueError(f"Attribute {key} is not found in the packed batch")
189
+ elif key in ["loss_masks", "advantages", "returns"]:
190
+ instance[key] = value[sum(response_lengths[:i]) : sum(response_lengths[: i + 1])]
191
+ elif isinstance(value, list):
192
+ instance[key] = value[i]
193
+ else:
194
+ raise ValueError(f"Attribute {key} is not found in the packed batch")
195
+
196
+ instances.append(instance)
197
+
198
+ return instances
199
+
200
+
201
+ def pad_packed_sequence_with_cp(packed_sequence: dict, cp_size: int) -> dict:
202
+ """Pad packed sequence to make total length divisible by cp_size.
203
+
204
+ Args:
205
+ packed_sequence: Packed sequence dict containing tokens, position_ids, cu_seqlens, etc.
206
+ cp_size: Context parallelism world size
207
+
208
+ Returns:
209
+ Padded packed sequence
210
+ """
211
+ seq_length = len(packed_sequence["tokens"])
212
+ # Calculate padding needed: (cp_size - seq_length % cp_size) % cp_size
213
+ remainder = seq_length % cp_size
214
+ pad_length = (cp_size - remainder) % cp_size
215
+
216
+ if pad_length > 0:
217
+ packed_sequence["tokens"] = F.pad(packed_sequence["tokens"], (0, pad_length), value=0)
218
+ packed_sequence["position_ids"] = F.pad(packed_sequence["position_ids"], (0, pad_length), value=0)
219
+ packed_sequence["loss_masks"] = F.pad(packed_sequence["loss_masks"], (0, pad_length), value=0)
220
+ packed_sequence["cu_seqlens"][-1] += pad_length
221
+ return packed_sequence
slime/backends/fsdp_utils/kernels/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
slime/backends/fsdp_utils/kernels/fused_experts.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import torch
7
+ import triton.language as tl
8
+ from sglang.srt.layers.moe.fused_moe_triton.fused_moe import (
9
+ invoke_fused_moe_kernel,
10
+ moe_align_block_size,
11
+ moe_sum_reduce,
12
+ silu_and_mul,
13
+ )
14
+
15
+ from .fused_moe_triton_backward_kernels import invoke_fused_moe_backward_kernel
16
+
17
+
18
+ class GateUpProjFunction(torch.autograd.Function):
19
+ @staticmethod
20
+ def forward(
21
+ ctx,
22
+ hidden_states: torch.Tensor,
23
+ w1: torch.Tensor,
24
+ topk_weights: torch.Tensor,
25
+ topk_ids: torch.Tensor,
26
+ ):
27
+ num_tokens, _ = hidden_states.shape
28
+ E, N, _ = w1.shape
29
+ # We execute the fused_moe kernel in chunks to circumvent this issue:
30
+ # https://github.com/vllm-project/vllm/issues/5938
31
+ CHUNK_SIZE = 64 * 1024
32
+
33
+ # default deterministic config
34
+ config = {
35
+ "BLOCK_SIZE_M": 64,
36
+ "BLOCK_SIZE_N": 64,
37
+ "BLOCK_SIZE_K": 32,
38
+ "GROUP_SIZE_M": 8,
39
+ }
40
+
41
+ topk = topk_ids.shape[1]
42
+
43
+ intermediate_cache1 = torch.empty(
44
+ (num_tokens * topk, N),
45
+ device=hidden_states.device,
46
+ dtype=hidden_states.dtype,
47
+ )
48
+
49
+ for chunk in range((num_tokens // CHUNK_SIZE) + 1):
50
+ begin_chunk_idx, end_chunk_idx = (
51
+ chunk * CHUNK_SIZE,
52
+ min((chunk + 1) * CHUNK_SIZE, num_tokens),
53
+ )
54
+ curr_hidden_states = hidden_states[begin_chunk_idx:end_chunk_idx]
55
+ cur_intermediate_cache1 = intermediate_cache1[begin_chunk_idx * topk : end_chunk_idx * topk]
56
+
57
+ curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
58
+ curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
59
+
60
+ sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
61
+ curr_topk_ids, config["BLOCK_SIZE_M"], E
62
+ )
63
+
64
+ invoke_fused_moe_kernel(
65
+ curr_hidden_states,
66
+ w1,
67
+ None,
68
+ cur_intermediate_cache1,
69
+ None,
70
+ None,
71
+ None,
72
+ curr_topk_weights,
73
+ curr_topk_ids,
74
+ sorted_token_ids,
75
+ expert_ids,
76
+ num_tokens_post_padded,
77
+ False,
78
+ topk_ids.shape[1],
79
+ config,
80
+ compute_type=tl.bfloat16,
81
+ use_fp8_w8a8=False,
82
+ use_int8_w8a8=False,
83
+ use_int8_w8a16=False,
84
+ use_int4_w4a16=False,
85
+ per_channel_quant=False,
86
+ block_shape=None,
87
+ c_sorted=False,
88
+ filter_expert=True,
89
+ )
90
+
91
+ ctx.save_for_backward(hidden_states, w1, topk_weights, topk_ids)
92
+ ctx.config = config
93
+ ctx.num_tokens = num_tokens
94
+ ctx.topk = topk
95
+
96
+ return intermediate_cache1
97
+
98
+ @staticmethod
99
+ def backward(ctx, grad_output):
100
+ """
101
+ Backward pass for GateUpProjFunction using Triton kernels.
102
+
103
+ Args:
104
+ grad_output: shape (num_tokens * topk, N)
105
+
106
+ Returns:
107
+ (grad_hidden_states, grad_w1, grad_topk_weights, None)
108
+ """
109
+
110
+ hidden_states, w1, topk_weights, topk_ids = ctx.saved_tensors
111
+ config = ctx.config
112
+ num_tokens = ctx.num_tokens
113
+ topk = ctx.topk
114
+
115
+ E, N, D_in = w1.shape
116
+ CHUNK_SIZE = 64 * 1024
117
+
118
+ # Initialize gradient tensors
119
+ grad_hidden_states = torch.zeros_like(hidden_states)
120
+ grad_w1 = torch.zeros_like(w1)
121
+ # GateUpProj stage doesn't need topk_weights gradient
122
+ grad_topk_weights = torch.zeros_like(topk_weights)
123
+
124
+ # Process in chunks to match forward pass
125
+ for chunk in range((num_tokens // CHUNK_SIZE) + 1):
126
+ begin_chunk_idx, end_chunk_idx = (
127
+ chunk * CHUNK_SIZE,
128
+ min((chunk + 1) * CHUNK_SIZE, num_tokens),
129
+ )
130
+
131
+ curr_num_tokens = end_chunk_idx - begin_chunk_idx
132
+ if curr_num_tokens == 0:
133
+ continue
134
+
135
+ curr_hidden_states = hidden_states[begin_chunk_idx:end_chunk_idx]
136
+ curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
137
+ curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
138
+ curr_grad_output = grad_output[begin_chunk_idx * topk : end_chunk_idx * topk]
139
+
140
+ # Get aligned metadata
141
+ sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
142
+ curr_topk_ids, config["BLOCK_SIZE_M"], E
143
+ )
144
+
145
+ # Prepare gradient buffer for this chunk
146
+ curr_grad_hidden_states = torch.zeros_like(curr_hidden_states)
147
+ curr_grad_w1 = torch.zeros_like(w1)
148
+
149
+ # Call Triton backward kernel with MUL_ROUTED_WEIGHT=False
150
+ # Use chunk of hidden_states to match sorted_token_ids indices
151
+ invoke_fused_moe_backward_kernel(
152
+ grad_output=curr_grad_output,
153
+ input=curr_hidden_states, # Use chunk of hidden_states to match sorted_token_ids
154
+ weight=w1,
155
+ grad_input=curr_grad_hidden_states,
156
+ grad_weight=curr_grad_w1,
157
+ grad_topk_weights=None, # Not needed for GateUpProj
158
+ topk_weights=curr_topk_weights,
159
+ topk_ids=curr_topk_ids,
160
+ sorted_token_ids=sorted_token_ids,
161
+ expert_ids=expert_ids,
162
+ num_tokens_post_padded=num_tokens_post_padded,
163
+ mul_routed_weight=False,
164
+ top_k=topk,
165
+ config=config,
166
+ compute_type=tl.bfloat16,
167
+ )
168
+
169
+ # Accumulate gradients
170
+ grad_hidden_states[begin_chunk_idx:end_chunk_idx] += curr_grad_hidden_states
171
+ grad_w1 += curr_grad_w1
172
+
173
+ return grad_hidden_states, grad_w1, grad_topk_weights, None
174
+
175
+
176
+ class SiluAndMulFunction(torch.autograd.Function):
177
+ @staticmethod
178
+ def forward(ctx, intermediate_cache1: torch.Tensor):
179
+ num_tokens, N = intermediate_cache1.shape
180
+ intermediate_cache2 = torch.empty(
181
+ (num_tokens, N // 2),
182
+ device=intermediate_cache1.device,
183
+ dtype=intermediate_cache1.dtype,
184
+ )
185
+ silu_and_mul(intermediate_cache1.view(-1, N), intermediate_cache2)
186
+
187
+ ctx.save_for_backward(intermediate_cache1)
188
+ return intermediate_cache2
189
+
190
+ @staticmethod
191
+ def backward(ctx, grad_output):
192
+ (intermediate_cache1,) = ctx.saved_tensors
193
+ N = intermediate_cache1.shape[-1]
194
+ x1, x2 = intermediate_cache1.view(-1, N).chunk(2, dim=-1)
195
+ silu_x1 = torch.nn.functional.silu(x1)
196
+
197
+ sig = torch.sigmoid(x1)
198
+ dsilu_dx1 = sig + x1 * sig * (1 - sig)
199
+ grad_x1 = grad_output * x2 * dsilu_dx1
200
+ grad_x2 = grad_output * silu_x1
201
+ grad_input = torch.cat([grad_x1, grad_x2], dim=-1)
202
+
203
+ return grad_input.view_as(intermediate_cache1)
204
+
205
+
206
+ class DownProjFunction(torch.autograd.Function):
207
+ @staticmethod
208
+ def forward(
209
+ ctx,
210
+ intermediate_cache2: torch.Tensor,
211
+ w2: torch.Tensor,
212
+ topk_weights: torch.Tensor,
213
+ topk_ids: torch.Tensor,
214
+ ):
215
+ num_tokens, _ = intermediate_cache2.shape
216
+ topk = topk_ids.shape[1]
217
+ num_tokens //= topk
218
+ E, _, _ = w2.shape
219
+ # We execute the fused_moe kernel in chunks to circumvent this issue:
220
+ # https://github.com/vllm-project/vllm/issues/5938
221
+ CHUNK_SIZE = 64 * 1024
222
+
223
+ # default deterministic config
224
+ config = {
225
+ "BLOCK_SIZE_M": 64,
226
+ "BLOCK_SIZE_N": 64,
227
+ "BLOCK_SIZE_K": 32,
228
+ "GROUP_SIZE_M": 8,
229
+ }
230
+
231
+ intermediate_cache3 = torch.empty(
232
+ (num_tokens, topk, w2.shape[1]),
233
+ device=intermediate_cache2.device,
234
+ dtype=intermediate_cache2.dtype,
235
+ )
236
+
237
+ for chunk in range((num_tokens // CHUNK_SIZE) + 1):
238
+ begin_chunk_idx, end_chunk_idx = (
239
+ chunk * CHUNK_SIZE,
240
+ min((chunk + 1) * CHUNK_SIZE, num_tokens),
241
+ )
242
+ cur_intermediate_cache2 = intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk]
243
+ cur_intermediate_cache3 = intermediate_cache3[begin_chunk_idx:end_chunk_idx]
244
+
245
+ curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
246
+ curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
247
+
248
+ sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
249
+ curr_topk_ids, config["BLOCK_SIZE_M"], E
250
+ )
251
+ invoke_fused_moe_kernel(
252
+ cur_intermediate_cache2,
253
+ w2,
254
+ None,
255
+ cur_intermediate_cache3,
256
+ None,
257
+ None,
258
+ None,
259
+ curr_topk_weights,
260
+ curr_topk_ids,
261
+ sorted_token_ids,
262
+ expert_ids,
263
+ num_tokens_post_padded,
264
+ True,
265
+ 1,
266
+ config,
267
+ compute_type=tl.bfloat16,
268
+ use_fp8_w8a8=False,
269
+ use_int8_w8a8=False,
270
+ use_int8_w8a16=False,
271
+ use_int4_w4a16=False,
272
+ per_channel_quant=False,
273
+ block_shape=None,
274
+ a_use_tma=False,
275
+ b_use_tma=False,
276
+ )
277
+
278
+ ctx.save_for_backward(intermediate_cache2, w2, topk_weights, topk_ids)
279
+ ctx.config = config
280
+ ctx.num_tokens = num_tokens
281
+ ctx.topk = topk
282
+
283
+ return intermediate_cache3
284
+
285
+ @staticmethod
286
+ def backward(ctx, grad_output):
287
+ """
288
+ Backward pass for DownProjFunction using Triton kernels.
289
+
290
+ Args:
291
+ grad_output: shape (num_tokens, topk, hidden_size)
292
+
293
+ Returns:
294
+ (grad_intermediate_cache2, grad_w2, grad_topk_weights, None)
295
+ """
296
+ intermediate_cache2, w2, topk_weights, topk_ids = ctx.saved_tensors
297
+ config = ctx.config
298
+ num_tokens = ctx.num_tokens
299
+ topk = ctx.topk
300
+
301
+ E, hidden_size, intermediate_size = w2.shape
302
+ CHUNK_SIZE = 64 * 1024
303
+
304
+ # Initialize gradient tensors
305
+ grad_intermediate_cache2 = torch.zeros_like(intermediate_cache2)
306
+ grad_w2 = torch.zeros_like(w2)
307
+ grad_topk_weights = torch.zeros_like(topk_weights)
308
+
309
+ # Process in chunks to match forward pass
310
+ for chunk in range((num_tokens // CHUNK_SIZE) + 1):
311
+ begin_chunk_idx, end_chunk_idx = (
312
+ chunk * CHUNK_SIZE,
313
+ min((chunk + 1) * CHUNK_SIZE, num_tokens),
314
+ )
315
+
316
+ curr_num_tokens = end_chunk_idx - begin_chunk_idx
317
+ if curr_num_tokens == 0:
318
+ continue
319
+
320
+ curr_intermediate_cache2 = intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk]
321
+ curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
322
+ curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
323
+ curr_grad_output = grad_output[begin_chunk_idx:end_chunk_idx]
324
+
325
+ # Get aligned metadata
326
+ sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
327
+ curr_topk_ids, config["BLOCK_SIZE_M"], E
328
+ )
329
+
330
+ # Prepare gradient buffers for this chunk
331
+ curr_grad_intermediate_cache2 = torch.zeros_like(curr_intermediate_cache2)
332
+ curr_grad_w2 = torch.zeros_like(w2)
333
+ curr_grad_topk_weights = torch.zeros_like(curr_topk_weights)
334
+
335
+ # Call Triton backward kernel with MUL_ROUTED_WEIGHT=True
336
+ # Note: Use top_k=1 to match forward pass indexing
337
+ invoke_fused_moe_backward_kernel(
338
+ grad_output=curr_grad_output,
339
+ input=curr_intermediate_cache2,
340
+ weight=w2,
341
+ grad_input=curr_grad_intermediate_cache2,
342
+ grad_weight=curr_grad_w2,
343
+ grad_topk_weights=curr_grad_topk_weights,
344
+ topk_weights=curr_topk_weights,
345
+ topk_ids=curr_topk_ids,
346
+ sorted_token_ids=sorted_token_ids,
347
+ expert_ids=expert_ids,
348
+ num_tokens_post_padded=num_tokens_post_padded,
349
+ mul_routed_weight=True,
350
+ top_k=1,
351
+ config=config,
352
+ compute_type=tl.bfloat16,
353
+ )
354
+
355
+ # Accumulate gradients
356
+ grad_intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk] = curr_grad_intermediate_cache2
357
+ grad_w2 += curr_grad_w2
358
+ grad_topk_weights[begin_chunk_idx:end_chunk_idx] = curr_grad_topk_weights
359
+
360
+ return grad_intermediate_cache2, grad_w2, grad_topk_weights, None
361
+
362
+
363
+ class MoeSumReduceFunction(torch.autograd.Function):
364
+ @staticmethod
365
+ def forward(
366
+ ctx,
367
+ intermediate_cache3: torch.Tensor,
368
+ hidden_states_shape,
369
+ ):
370
+ out_hidden_states = torch.empty(
371
+ hidden_states_shape, device=intermediate_cache3.device, dtype=intermediate_cache3.dtype
372
+ )
373
+ moe_sum_reduce(
374
+ intermediate_cache3,
375
+ out_hidden_states,
376
+ 1.0,
377
+ )
378
+ ctx.save_for_backward(intermediate_cache3)
379
+ return out_hidden_states
380
+
381
+ @staticmethod
382
+ def backward(ctx, grad_output):
383
+ (intermediate_cache3,) = ctx.saved_tensors
384
+ return grad_output.unsqueeze(1).expand_as(intermediate_cache3), None
slime/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ import torch
9
+ import triton
10
+ import triton.language as tl
11
+
12
+
13
+ @triton.jit
14
+ def fused_moe_backward_input_kernel(
15
+ # Pointers to matrices
16
+ grad_output_ptr,
17
+ weight_ptr,
18
+ grad_input_ptr,
19
+ grad_topk_weights_ptr,
20
+ topk_weights_ptr,
21
+ sorted_token_ids_ptr,
22
+ expert_ids_ptr,
23
+ num_tokens_post_padded_ptr,
24
+ # Matrix dimensions
25
+ N,
26
+ K,
27
+ EM,
28
+ num_valid_tokens,
29
+ # Strides
30
+ stride_gom,
31
+ stride_gon,
32
+ stride_we,
33
+ stride_wn,
34
+ stride_wk,
35
+ stride_gim,
36
+ stride_gik,
37
+ # Meta-parameters
38
+ BLOCK_SIZE_M: tl.constexpr,
39
+ BLOCK_SIZE_N: tl.constexpr,
40
+ BLOCK_SIZE_K: tl.constexpr,
41
+ GROUP_SIZE_M: tl.constexpr,
42
+ MUL_ROUTED_WEIGHT: tl.constexpr,
43
+ top_k: tl.constexpr,
44
+ compute_type: tl.constexpr,
45
+ ):
46
+ """
47
+ Backward kernel for computing grad_input.
48
+
49
+ Forward: output = input @ weight.T (optionally multiplied by topk_weights)
50
+ Backward: grad_input = grad_output @ weight (optionally multiplied by topk_weights)
51
+
52
+ This kernel computes: grad_input[token] = sum_over_N(grad_output[token, n] * weight[expert, n, :])
53
+ If MUL_ROUTED_WEIGHT: grad_input[token] *= topk_weights[token]
54
+
55
+ Parallelization: Similar to forward, parallel over M and N dimensions, loop over K.
56
+ """
57
+ # Map program ids to blocks (parallel over M and N, similar to forward)
58
+ pid = tl.program_id(axis=0)
59
+ num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
60
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
61
+ num_pid_in_group = GROUP_SIZE_M * num_pid_n
62
+ group_id = pid // num_pid_in_group
63
+ first_pid_m = group_id * GROUP_SIZE_M
64
+ group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
65
+ pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
66
+ pid_n = (pid % num_pid_in_group) // group_size_m
67
+
68
+ # Check bounds
69
+ num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
70
+
71
+ # Only process if this block is valid
72
+ if pid_m * BLOCK_SIZE_M < num_tokens_post_padded:
73
+ # Load token information
74
+ offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
75
+ offs_token = tl.load(sorted_token_ids_ptr + offs_token_id)
76
+ offs_token = offs_token.to(tl.int64)
77
+ token_mask = offs_token < num_valid_tokens
78
+
79
+ # Get expert ID for this block
80
+ off_experts = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
81
+
82
+ # Only process if expert is valid
83
+ if off_experts != -1:
84
+ # Initialize offsets for N dimension (current block)
85
+ offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
86
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
87
+
88
+ # Load grad_output block: shape (BLOCK_SIZE_M, BLOCK_SIZE_N)
89
+ grad_output_ptrs = grad_output_ptr + (offs_token[:, None] * stride_gom + offs_n[None, :] * stride_gon)
90
+ grad_out = tl.load(
91
+ grad_output_ptrs,
92
+ mask=token_mask[:, None] & (offs_n[None, :] < N),
93
+ other=0.0,
94
+ )
95
+
96
+ # Apply topk_weights to grad_output if needed
97
+ if MUL_ROUTED_WEIGHT:
98
+ moe_weight = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0)
99
+ grad_out = grad_out * moe_weight[:, None]
100
+
101
+ # Iterate over K dimension
102
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
103
+ # Current K offsets
104
+ curr_offs_k = k * BLOCK_SIZE_K + offs_k
105
+
106
+ # Load weight block: shape (BLOCK_SIZE_N, BLOCK_SIZE_K)
107
+ # weight: shape (E, N, K)
108
+ weight_ptrs = (
109
+ weight_ptr
110
+ + off_experts * stride_we
111
+ + offs_n[:, None] * stride_wn
112
+ + curr_offs_k[None, :] * stride_wk
113
+ )
114
+ w = tl.load(
115
+ weight_ptrs,
116
+ mask=(offs_n[:, None] < N) & (curr_offs_k[None, :] < K),
117
+ other=0.0,
118
+ )
119
+
120
+ # Compute contribution: grad_out @ weight
121
+ # grad_out: (BLOCK_SIZE_M, BLOCK_SIZE_N)
122
+ # w: (BLOCK_SIZE_N, BLOCK_SIZE_K)
123
+ # result: (BLOCK_SIZE_M, BLOCK_SIZE_K)
124
+ contribution = tl.dot(grad_out, w)
125
+
126
+ # Atomic add to grad_input because different N blocks contribute to same K
127
+ grad_input_ptrs = grad_input_ptr + (
128
+ (offs_token[:, None] // top_k) * stride_gim + curr_offs_k[None, :] * stride_gik
129
+ )
130
+ grad_input_mask = token_mask[:, None] & (curr_offs_k[None, :] < K)
131
+ tl.atomic_add(grad_input_ptrs, contribution.to(compute_type), mask=grad_input_mask)
132
+
133
+
134
+ @triton.jit
135
+ def fused_moe_backward_weight_kernel(
136
+ # Pointers to matrices
137
+ grad_output_ptr,
138
+ input_ptr,
139
+ grad_weight_ptr,
140
+ topk_weights_ptr,
141
+ sorted_token_ids_ptr,
142
+ expert_ids_ptr,
143
+ num_tokens_post_padded_ptr,
144
+ # Matrix dimensions
145
+ N,
146
+ K,
147
+ EM,
148
+ num_valid_tokens,
149
+ # Strides
150
+ stride_gom,
151
+ stride_gon,
152
+ stride_im,
153
+ stride_ik,
154
+ stride_gwe,
155
+ stride_gwn,
156
+ stride_gwk,
157
+ # Meta-parameters
158
+ BLOCK_SIZE_M: tl.constexpr,
159
+ BLOCK_SIZE_N: tl.constexpr,
160
+ BLOCK_SIZE_K: tl.constexpr,
161
+ GROUP_SIZE_M: tl.constexpr,
162
+ MUL_ROUTED_WEIGHT: tl.constexpr,
163
+ top_k: tl.constexpr,
164
+ compute_type: tl.constexpr,
165
+ ):
166
+ """
167
+ Backward kernel for computing grad_weight.
168
+
169
+ Forward: output = input @ weight.T (optionally multiplied by topk_weights)
170
+ Backward: grad_weight = input.T @ grad_output (optionally multiplied by topk_weights)
171
+
172
+ This kernel computes: grad_weight[expert, n, k] = sum_over_tokens(input[token, k] * grad_output[token, n])
173
+ If MUL_ROUTED_WEIGHT: the accumulation is weighted by topk_weights[token]
174
+
175
+ Parallelization: Parallel over M and N dimensions with grouping, loop over K.
176
+ """
177
+ # Map program ids to blocks (parallel over M and N with grouping, similar to forward and backward_input)
178
+ pid = tl.program_id(axis=0)
179
+ num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
180
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
181
+ num_pid_in_group = GROUP_SIZE_M * num_pid_n
182
+ group_id = pid // num_pid_in_group
183
+ first_pid_m = group_id * GROUP_SIZE_M
184
+ group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
185
+ pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
186
+ pid_n = (pid % num_pid_in_group) // group_size_m
187
+
188
+ # Check bounds
189
+ num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
190
+
191
+ # Only process if this block is valid
192
+ if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
193
+ return
194
+
195
+ # Get expert ID for this M block
196
+ expert_id = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
197
+
198
+ # Only process if expert is valid
199
+ if expert_id == -1:
200
+ return
201
+
202
+ # Load token information for this M block
203
+ offs_m = tl.arange(0, BLOCK_SIZE_M)
204
+ offs_token_id = pid_m * BLOCK_SIZE_M + offs_m.to(tl.int64)
205
+ offs_token = tl.load(
206
+ sorted_token_ids_ptr + offs_token_id, mask=offs_token_id < num_tokens_post_padded, other=num_valid_tokens
207
+ )
208
+ offs_token = offs_token.to(tl.int64)
209
+ token_mask = (offs_token_id < num_tokens_post_padded) & (offs_token < num_valid_tokens)
210
+
211
+ # Clamp offs_token to valid range
212
+ offs_token_clamped = tl.where(token_mask, offs_token, 0)
213
+
214
+ # Determine input token indices based on MUL_ROUTED_WEIGHT
215
+ if MUL_ROUTED_WEIGHT:
216
+ input_token_idx = offs_token_clamped
217
+ input_mask = token_mask
218
+ else:
219
+ input_token_idx = offs_token_clamped // top_k
220
+ num_input_tokens = num_valid_tokens // top_k
221
+ input_mask = token_mask & (input_token_idx < num_input_tokens)
222
+
223
+ # Load topk_weights if needed
224
+ if MUL_ROUTED_WEIGHT:
225
+ moe_weight = tl.load(topk_weights_ptr + offs_token_clamped, mask=token_mask, other=0.0)
226
+
227
+ # Current N offset for this program
228
+ offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
229
+
230
+ # Load grad_output for this N block: shape (M, BLOCK_SIZE_N)
231
+ # grad_output is always indexed by sorted_token_ids (offs_token_clamped)
232
+ # because it has shape (num_tokens * topk, N)
233
+ grad_output_ptrs = grad_output_ptr + (offs_token_clamped[:, None] * stride_gom + offs_n[None, :] * stride_gon)
234
+ grad_out = tl.load(
235
+ grad_output_ptrs,
236
+ mask=token_mask[:, None] & (offs_n[None, :] < N),
237
+ other=0.0,
238
+ )
239
+
240
+ # Apply topk_weights if needed
241
+ if MUL_ROUTED_WEIGHT:
242
+ grad_out = grad_out * moe_weight[:, None]
243
+
244
+ # Zero out padding tokens
245
+ token_mask_col = token_mask[:, None]
246
+ grad_out = grad_out * token_mask_col
247
+
248
+ # Iterate over K blocks and accumulate
249
+ for k_block in range(tl.cdiv(K, BLOCK_SIZE_K)):
250
+ offs_k = k_block * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K).to(tl.int64)
251
+
252
+ # Load input for this K block
253
+ input_ptrs = input_ptr + (input_token_idx[:, None] * stride_im + offs_k[None, :] * stride_ik)
254
+ inp = tl.load(
255
+ input_ptrs,
256
+ mask=input_mask[:, None] & (offs_k[None, :] < K),
257
+ other=0.0,
258
+ )
259
+
260
+ # Zero out padding tokens - use input_mask for input, token_mask for grad_output
261
+ input_mask_col = input_mask[:, None]
262
+ inp = inp * input_mask_col
263
+
264
+ # Compute grad_weight contribution: grad_out.T @ inp
265
+ grad_w_contribution = tl.dot(grad_out.T, inp)
266
+
267
+ # Write back using atomic add
268
+ grad_weight_ptrs = (
269
+ grad_weight_ptr + expert_id * stride_gwe + offs_n[:, None] * stride_gwn + offs_k[None, :] * stride_gwk
270
+ )
271
+ grad_weight_mask = (offs_n[:, None] < N) & (offs_k[None, :] < K)
272
+ tl.atomic_add(grad_weight_ptrs, grad_w_contribution.to(compute_type), mask=grad_weight_mask)
273
+
274
+
275
+ @triton.jit
276
+ def fused_moe_backward_topk_weights_kernel(
277
+ # Pointers to matrices
278
+ grad_output_ptr,
279
+ input_ptr,
280
+ weight_ptr,
281
+ grad_topk_weights_ptr,
282
+ sorted_token_ids_ptr,
283
+ expert_ids_ptr,
284
+ num_tokens_post_padded_ptr,
285
+ # Matrix dimensions
286
+ N,
287
+ K,
288
+ EM,
289
+ num_valid_tokens,
290
+ # Strides
291
+ stride_gom,
292
+ stride_gon,
293
+ stride_im,
294
+ stride_ik,
295
+ stride_we,
296
+ stride_wn,
297
+ stride_wk,
298
+ # Meta-parameters
299
+ BLOCK_SIZE_M: tl.constexpr,
300
+ BLOCK_SIZE_N: tl.constexpr,
301
+ BLOCK_SIZE_K: tl.constexpr,
302
+ top_k: tl.constexpr,
303
+ compute_type: tl.constexpr,
304
+ ):
305
+ """
306
+ Backward kernel for computing grad_topk_weights.
307
+
308
+ Forward: output = topk_weights * (input @ weight.T)
309
+ Backward: grad_topk_weights = sum(grad_output * (input @ weight.T))
310
+
311
+ This kernel computes the gradient of topk_weights by computing the dot product
312
+ of grad_output with the forward output before weight multiplication.
313
+ """
314
+ # Map program id to token block
315
+ pid = tl.program_id(axis=0)
316
+
317
+ # Check bounds
318
+ num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
319
+
320
+ # Only process if this block is valid
321
+ if pid * BLOCK_SIZE_M < num_tokens_post_padded:
322
+ # Load token information
323
+ offs_token_id = pid * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
324
+ offs_token = tl.load(
325
+ sorted_token_ids_ptr + offs_token_id, mask=offs_token_id < num_tokens_post_padded, other=num_valid_tokens
326
+ )
327
+ offs_token = offs_token.to(tl.int64)
328
+ token_mask = (offs_token_id < num_tokens_post_padded) & (offs_token < num_valid_tokens)
329
+
330
+ # Clamp offs_token to valid range for safe pointer arithmetic
331
+ offs_token_clamped = tl.where(token_mask, offs_token, 0)
332
+
333
+ # Get expert ID for this block
334
+ off_experts = tl.load(expert_ids_ptr + pid).to(tl.int64)
335
+
336
+ # Only process if expert is valid
337
+ if off_experts != -1:
338
+ # Initialize offsets
339
+ offs_n = tl.arange(0, BLOCK_SIZE_N)
340
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
341
+
342
+ # Accumulator for grad_topk_weights
343
+ accumulator = tl.zeros((BLOCK_SIZE_M,), dtype=tl.float32)
344
+
345
+ # Iterate over N and K dimensions to compute forward output and gradient
346
+ for n in range(0, tl.cdiv(N, BLOCK_SIZE_N)):
347
+ # Current N offset
348
+ curr_offs_n = n * BLOCK_SIZE_N + offs_n
349
+
350
+ # Load grad_output block: (M, N)
351
+ grad_output_ptrs = grad_output_ptr + (
352
+ offs_token_clamped[:, None] * stride_gom + curr_offs_n[None, :] * stride_gon
353
+ )
354
+ grad_out = tl.load(
355
+ grad_output_ptrs,
356
+ mask=token_mask[:, None] & (curr_offs_n[None, :] < N),
357
+ other=0.0,
358
+ )
359
+
360
+ # Compute forward output for this N block: input @ weight[:, n, :].T
361
+ forward_output_n = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
362
+
363
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
364
+ # Current K offset
365
+ curr_offs_k = k * BLOCK_SIZE_K + offs_k
366
+
367
+ # Load input block: (M, K)
368
+ input_ptrs = input_ptr + (
369
+ (offs_token_clamped[:, None] // top_k) * stride_im + curr_offs_k[None, :] * stride_ik
370
+ )
371
+ inp = tl.load(
372
+ input_ptrs,
373
+ mask=token_mask[:, None] & (curr_offs_k[None, :] < K),
374
+ other=0.0,
375
+ )
376
+
377
+ # Load weight block: (N, K)
378
+ weight_ptrs = (
379
+ weight_ptr
380
+ + off_experts * stride_we
381
+ + curr_offs_n[:, None] * stride_wn
382
+ + curr_offs_k[None, :] * stride_wk
383
+ )
384
+ w = tl.load(
385
+ weight_ptrs,
386
+ mask=(curr_offs_n[:, None] < N) & (curr_offs_k[None, :] < K),
387
+ other=0.0,
388
+ )
389
+
390
+ # Accumulate forward output: input @ weight.T
391
+ # inp: (M, K), w.T: (K, N) -> (M, N)
392
+ forward_output_n += tl.dot(inp, w.T)
393
+
394
+ # Compute contribution to grad_topk_weights: sum(grad_out * forward_output)
395
+ # Sum over N dimension
396
+ accumulator += tl.sum(grad_out * forward_output_n, axis=1)
397
+
398
+ # Write back grad_topk_weights using atomic add with clamped token indices
399
+ tl.atomic_add(grad_topk_weights_ptr + offs_token_clamped, accumulator.to(compute_type), mask=token_mask)
400
+
401
+
402
+ def invoke_fused_moe_backward_kernel(
403
+ grad_output: torch.Tensor,
404
+ input: torch.Tensor,
405
+ weight: torch.Tensor,
406
+ grad_input: torch.Tensor,
407
+ grad_weight: torch.Tensor,
408
+ grad_topk_weights: torch.Tensor | None,
409
+ topk_weights: torch.Tensor,
410
+ topk_ids: torch.Tensor,
411
+ sorted_token_ids: torch.Tensor,
412
+ expert_ids: torch.Tensor,
413
+ num_tokens_post_padded: torch.Tensor,
414
+ mul_routed_weight: bool,
415
+ top_k: int,
416
+ config: dict[str, Any],
417
+ compute_type: tl.dtype,
418
+ ) -> None:
419
+ """
420
+ Invoke the fused MOE backward kernels to compute gradients.
421
+
422
+ Args:
423
+ grad_output: Gradient of output, shape (num_tokens * topk, N) or (num_tokens, topk, N)
424
+ input: Input tensor, shape (num_tokens, K)
425
+ weight: Weight tensor, shape (E, N, K)
426
+ grad_input: Output gradient for input, shape (num_tokens, K)
427
+ grad_weight: Output gradient for weight, shape (E, N, K)
428
+ grad_topk_weights: Output gradient for topk_weights, shape (num_tokens, topk) or None
429
+ topk_weights: Top-K routing weights, shape (num_tokens, topk)
430
+ topk_ids: Top-K expert IDs, shape (num_tokens, topk)
431
+ sorted_token_ids: Sorted token IDs
432
+ expert_ids: Expert IDs for each block
433
+ num_tokens_post_padded: Number of tokens after padding
434
+ mul_routed_weight: Whether to multiply by routing weights
435
+ top_k: Number of experts per token
436
+ config: Kernel configuration
437
+ compute_type: Computation data type
438
+ """
439
+ assert topk_weights.stride(1) == 1
440
+ assert sorted_token_ids.stride(0) == 1
441
+
442
+ # Flatten grad_output if needed
443
+ # Before: (num_tokens, topk, hidden_size)
444
+ # After: (num_tokens * topk, hidden_size)
445
+ if grad_output.ndim == 3:
446
+ grad_output = grad_output.reshape(-1, grad_output.shape[-1])
447
+
448
+ E, N, K = weight.shape
449
+
450
+ # ===================== Compute grad_input =====================
451
+ def grid_input(META):
452
+ return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]),)
453
+
454
+ fused_moe_backward_input_kernel[grid_input](
455
+ grad_output,
456
+ weight,
457
+ grad_input,
458
+ grad_topk_weights if grad_topk_weights is not None else grad_input, # dummy pointer
459
+ topk_weights,
460
+ sorted_token_ids,
461
+ expert_ids,
462
+ num_tokens_post_padded,
463
+ N,
464
+ K,
465
+ sorted_token_ids.shape[0],
466
+ grad_output.shape[0],
467
+ grad_output.stride(0),
468
+ grad_output.stride(1),
469
+ weight.stride(0),
470
+ weight.stride(1),
471
+ weight.stride(2),
472
+ grad_input.stride(0),
473
+ grad_input.stride(1),
474
+ MUL_ROUTED_WEIGHT=mul_routed_weight,
475
+ top_k=top_k,
476
+ compute_type=compute_type,
477
+ **config,
478
+ )
479
+
480
+ # ===================== Compute grad_weight =====================
481
+ # Initialize grad_weight to zero
482
+ grad_weight.zero_()
483
+
484
+ # Use same grid configuration as forward kernel: encode both M and N dimensions
485
+ def grid_weight(META):
486
+ return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]),)
487
+
488
+ fused_moe_backward_weight_kernel[grid_weight](
489
+ grad_output,
490
+ input,
491
+ grad_weight,
492
+ topk_weights,
493
+ sorted_token_ids,
494
+ expert_ids,
495
+ num_tokens_post_padded,
496
+ N,
497
+ K,
498
+ sorted_token_ids.shape[0],
499
+ grad_output.shape[0],
500
+ grad_output.stride(0),
501
+ grad_output.stride(1),
502
+ input.stride(0),
503
+ input.stride(1),
504
+ grad_weight.stride(0),
505
+ grad_weight.stride(1),
506
+ grad_weight.stride(2),
507
+ MUL_ROUTED_WEIGHT=mul_routed_weight,
508
+ top_k=top_k,
509
+ compute_type=compute_type,
510
+ **config,
511
+ )
512
+
513
+ # ===================== Compute grad_topk_weights (if needed) =====================
514
+ if mul_routed_weight and grad_topk_weights is not None:
515
+
516
+ def grid_topk(META):
517
+ return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]),)
518
+
519
+ fused_moe_backward_topk_weights_kernel[grid_topk](
520
+ grad_output,
521
+ input,
522
+ weight,
523
+ grad_topk_weights.view(-1),
524
+ sorted_token_ids,
525
+ expert_ids,
526
+ num_tokens_post_padded,
527
+ N,
528
+ K,
529
+ sorted_token_ids.shape[0],
530
+ grad_output.shape[0],
531
+ grad_output.stride(0),
532
+ grad_output.stride(1),
533
+ input.stride(0),
534
+ input.stride(1),
535
+ weight.stride(0),
536
+ weight.stride(1),
537
+ weight.stride(2),
538
+ top_k=top_k,
539
+ compute_type=compute_type,
540
+ BLOCK_SIZE_M=config["BLOCK_SIZE_M"],
541
+ BLOCK_SIZE_N=config["BLOCK_SIZE_N"],
542
+ BLOCK_SIZE_K=config["BLOCK_SIZE_K"],
543
+ )
slime/backends/fsdp_utils/lr_scheduler.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ """Learning rate scheduler for FSDP training."""
6
+
7
+ import logging
8
+ import math
9
+
10
+ import torch
11
+ from torch.optim.lr_scheduler import LRScheduler
12
+ from typing_extensions import override
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class FSDPLRScheduler(LRScheduler):
18
+ """Learning rate scheduler for FSDP training.
19
+
20
+ Args:
21
+ optimizer (torch.optim.Optimizer): The optimizer to be used.
22
+ init_lr (float): Initial learning rate.
23
+ max_lr (float): Maximum learning rate.
24
+ min_lr (float): Minimum learning rate.
25
+ lr_warmup_steps (int): Number of warmup steps.
26
+ lr_decay_steps (int): Number of decay steps.
27
+ lr_decay_style (str): Decay style for learning rate.
28
+ use_checkpoint_lr_scheduler (bool, optional): Whether to use the checkpoint values
29
+ for the lr scheduler.
30
+ override_lr_scheduler (bool, optional): Whether to override the lr scheduler values
31
+ with the class values.
32
+ wsd_decay_steps (int, optional): Number of weight decay decay steps.
33
+ lr_wsd_decay_style (str, optional): Decay style for learning rate during weight decay decay
34
+ steps.
35
+ last_epoch (int, optional): The index of last epoch. Default: -1.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ optimizer: torch.optim.Optimizer,
41
+ init_lr: float,
42
+ max_lr: float,
43
+ min_lr: float,
44
+ lr_warmup_steps: int,
45
+ lr_decay_steps: int,
46
+ lr_decay_style: str,
47
+ use_checkpoint_lr_scheduler: bool | None = True,
48
+ override_lr_scheduler: bool | None = False,
49
+ wsd_decay_steps: int | None = None,
50
+ lr_wsd_decay_style: str | None = None,
51
+ last_epoch: int = -1,
52
+ ) -> None:
53
+ # Store our custom parameters
54
+ self.init_lr = init_lr
55
+ self.max_lr = float(max_lr)
56
+ self.min_lr = min_lr
57
+ assert self.min_lr >= 0.0
58
+ assert self.max_lr >= self.min_lr
59
+ assert self.init_lr <= self.max_lr
60
+
61
+ self.lr_warmup_steps = lr_warmup_steps
62
+ self.lr_decay_steps = lr_decay_steps
63
+ self.wsd_decay_steps = wsd_decay_steps
64
+ self.lr_wsd_decay_style = lr_wsd_decay_style
65
+
66
+ assert self.lr_decay_steps > 0
67
+ assert self.lr_warmup_steps < self.lr_decay_steps
68
+
69
+ self.lr_decay_style = lr_decay_style
70
+ if self.lr_decay_style == "WSD":
71
+ assert self.wsd_decay_steps is not None
72
+
73
+ self.override_lr_scheduler = override_lr_scheduler
74
+ self.use_checkpoint_lr_scheduler = use_checkpoint_lr_scheduler
75
+
76
+ if self.override_lr_scheduler:
77
+ assert not self.use_checkpoint_lr_scheduler, "both override and use-checkpoint are set."
78
+
79
+ # Initialize parent class
80
+ super().__init__(optimizer, last_epoch)
81
+
82
+ logger.info(f"> learning rate decay style: {self.lr_decay_style}")
83
+
84
+ def _get_lr_for_group(self, param_group: dict) -> float:
85
+ """Compute learning rate for a specific parameter group.
86
+
87
+ Args:
88
+ param_group (dict): parameter group from the optimizer.
89
+
90
+ Returns:
91
+ float: learning rate for this parameter group.
92
+ """
93
+ max_lr = param_group.get("max_lr", self.max_lr)
94
+ min_lr = param_group.get("min_lr", self.min_lr)
95
+
96
+ # Use linear warmup for the initial part.
97
+ if self.lr_warmup_steps > 0 and self.last_epoch <= self.lr_warmup_steps:
98
+ return self.init_lr + ((max_lr - self.init_lr) * float(self.last_epoch) / float(self.lr_warmup_steps))
99
+
100
+ # If the learning rate is constant, just return the initial value.
101
+ if self.lr_decay_style == "constant":
102
+ return max_lr
103
+
104
+ # For any steps larger than `self.lr_decay_steps`, use `min_lr`.
105
+ if self.last_epoch > self.lr_decay_steps:
106
+ return min_lr
107
+
108
+ # If we are done with the warmup period, use the decay style.
109
+ if self.lr_decay_style == "inverse-square-root":
110
+ warmup_steps = max(self.lr_warmup_steps, 1)
111
+ num_steps = max(self.last_epoch, 1)
112
+ lr = max_lr * warmup_steps**0.5 / (num_steps**0.5)
113
+ return max(min_lr, lr)
114
+
115
+ num_steps_ = self.last_epoch - self.lr_warmup_steps
116
+ decay_steps_ = self.lr_decay_steps - self.lr_warmup_steps
117
+ decay_ratio = float(num_steps_) / float(decay_steps_)
118
+ assert decay_ratio >= 0.0
119
+ assert decay_ratio <= 1.0
120
+
121
+ delta_lr = max_lr - min_lr
122
+ coeff = None
123
+
124
+ if self.lr_decay_style == "linear":
125
+ coeff = 1.0 - decay_ratio
126
+ elif self.lr_decay_style == "cosine":
127
+ coeff = 0.5 * (math.cos(math.pi * decay_ratio) + 1.0)
128
+ elif self.lr_decay_style == "WSD":
129
+ wsd_anneal_start_ = self.lr_decay_steps - self.wsd_decay_steps
130
+ if self.last_epoch <= wsd_anneal_start_:
131
+ coeff = 1.0
132
+ else:
133
+ wsd_steps = self.last_epoch - wsd_anneal_start_
134
+ wsd_decay_ratio = float(wsd_steps) / float(self.wsd_decay_steps)
135
+ if self.lr_wsd_decay_style == "linear":
136
+ coeff = 1.0 - wsd_decay_ratio
137
+ elif self.lr_wsd_decay_style == "cosine":
138
+ coeff = 0.5 * (math.cos(math.pi * wsd_decay_ratio) + 1.0)
139
+ elif self.lr_wsd_decay_style == "exponential":
140
+ coeff = (2.0 * math.pow(0.5, wsd_decay_ratio)) - 1.0
141
+ elif self.lr_wsd_decay_style == "minus_sqrt":
142
+ coeff = 1.0 - math.sqrt(wsd_decay_ratio)
143
+ else:
144
+ raise Exception(f"{self.lr_decay_style} decay style is not supported.")
145
+
146
+ assert coeff is not None
147
+ return min_lr + coeff * delta_lr
148
+
149
+ @override
150
+ def get_lr(self) -> list[float]:
151
+ """Compute the learning rates for each parameter group.
152
+
153
+ Returns:
154
+ list[float]: A list of learning rates, one for each parameter group.
155
+ """
156
+ return [self._get_lr_for_group(group) for group in self.optimizer.param_groups]
157
+
158
+
159
+ def get_lr_scheduler(args, optimizer: torch.optim.Optimizer) -> FSDPLRScheduler:
160
+ """Create and configure the learning-rate scheduler.
161
+
162
+ This configures iteration-based schedules derived from the global batch size
163
+ and run-time arguments.
164
+
165
+ Args:
166
+ args: Training/runtime arguments (namespace).
167
+ optimizer (torch.optim.Optimizer): Optimizer bound to the model.
168
+
169
+ Returns:
170
+ FSDPLRScheduler: Initialized scheduler bound to ``optimizer``.
171
+ """
172
+ args.train_iters = args.num_rollout * args.rollout_batch_size * args.n_samples_per_prompt // args.global_batch_size
173
+ if args.lr_decay_iters is None:
174
+ args.lr_decay_iters = args.train_iters
175
+ lr_decay_steps = args.lr_decay_iters
176
+ wsd_decay_steps = None
177
+ if args.lr_wsd_decay_iters is not None:
178
+ wsd_decay_steps = args.lr_wsd_decay_iters
179
+ if args.lr_warmup_fraction is not None:
180
+ lr_warmup_steps = args.lr_warmup_fraction * lr_decay_steps
181
+ else:
182
+ lr_warmup_steps = args.lr_warmup_iters
183
+ lr_scheduler = FSDPLRScheduler(
184
+ optimizer,
185
+ init_lr=args.lr_warmup_init,
186
+ max_lr=args.lr,
187
+ min_lr=args.min_lr,
188
+ lr_warmup_steps=lr_warmup_steps,
189
+ lr_decay_steps=lr_decay_steps,
190
+ lr_decay_style=args.lr_decay_style,
191
+ use_checkpoint_lr_scheduler=args.use_checkpoint_lr_scheduler,
192
+ override_lr_scheduler=args.override_lr_scheduler,
193
+ wsd_decay_steps=wsd_decay_steps,
194
+ lr_wsd_decay_style=args.lr_wsd_decay_style,
195
+ )
196
+
197
+ return lr_scheduler
slime/backends/fsdp_utils/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
slime/backends/fsdp_utils/models/qwen3_moe.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeMLP
8
+
9
+ from slime.backends.fsdp_utils.kernels.fused_experts import (
10
+ DownProjFunction,
11
+ GateUpProjFunction,
12
+ MoeSumReduceFunction,
13
+ SiluAndMulFunction,
14
+ )
15
+
16
+
17
+ def fused_experts_impl(
18
+ hidden_states: torch.Tensor,
19
+ w1: torch.Tensor,
20
+ w2: torch.Tensor,
21
+ topk_weights: torch.Tensor,
22
+ topk_ids: torch.Tensor,
23
+ ):
24
+ assert hidden_states.shape[1] == w1.shape[2], "Hidden size mismatch"
25
+ assert topk_weights.shape == topk_ids.shape, "topk shape mismatch"
26
+ assert hidden_states.is_contiguous(), "Hidden_states must be contiguous"
27
+ assert w1.is_contiguous(), "Expert weights1 must be contiguous"
28
+ assert w2.is_contiguous(), "Expert weights2 must be contiguous"
29
+ assert hidden_states.dtype in [torch.bfloat16]
30
+
31
+ intermediate_cache1 = GateUpProjFunction.apply(
32
+ hidden_states,
33
+ w1,
34
+ topk_weights,
35
+ topk_ids,
36
+ )
37
+ intermediate_cache2 = SiluAndMulFunction.apply(intermediate_cache1)
38
+ intermediate_cache3 = DownProjFunction.apply(
39
+ intermediate_cache2,
40
+ w2,
41
+ topk_weights,
42
+ topk_ids,
43
+ )
44
+ output_hidden_states = MoeSumReduceFunction.apply(
45
+ intermediate_cache3,
46
+ hidden_states.shape,
47
+ )
48
+ return output_hidden_states
49
+
50
+
51
+ class StandardDispatcher:
52
+ def __init__(self, num_experts: int, num_local_experts: int):
53
+ self.moe_ep_size = 1
54
+ self.num_experts = num_experts
55
+ self.num_local_experts = num_local_experts
56
+ self.moe_ep_rank = 0
57
+ self.local_expert_mapping = None
58
+
59
+ if self.moe_ep_size > 1:
60
+ self.local_expert_mapping = torch.full((self.num_experts,), -1, dtype=torch.int32, device="cuda")
61
+ self.local_expert_mapping[
62
+ self.moe_ep_rank * self.num_local_experts : (self.moe_ep_rank + 1) * self.num_local_experts
63
+ ] = torch.arange(0, self.num_local_experts, dtype=torch.int32, device="cuda")
64
+
65
+ def dispatch(self, topk_ids) -> torch.Tensor:
66
+ if self.local_expert_mapping is not None:
67
+ return self.local_expert_mapping[topk_ids]
68
+ return topk_ids
69
+
70
+
71
+ class Qwen3MoeSparseMoeBlock(nn.Module):
72
+ dispatcher = None
73
+ runner = None
74
+
75
+ def __init__(self, config):
76
+ super().__init__()
77
+ self.num_experts = config.num_experts
78
+ self.top_k = config.num_experts_per_tok
79
+ self.norm_topk_prob = config.norm_topk_prob
80
+
81
+ # gating
82
+ self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
83
+
84
+ self.experts = nn.ModuleList(
85
+ [Qwen3MoeMLP(config, intermediate_size=config.moe_intermediate_size) for _ in range(self.num_experts)]
86
+ )
87
+
88
+ if Qwen3MoeSparseMoeBlock.dispatcher is None:
89
+ Qwen3MoeSparseMoeBlock.dispatcher = StandardDispatcher(
90
+ num_experts=config.num_experts, num_local_experts=config.num_experts
91
+ )
92
+
93
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
94
+ batch_size, sequence_length, hidden_dim = hidden_states.shape
95
+ hidden_states = hidden_states.view(-1, hidden_dim)
96
+ # router_logits: (batch * sequence_length, n_experts)
97
+ router_logits = self.gate(hidden_states)
98
+
99
+ routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float)
100
+ routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
101
+
102
+ if self.norm_topk_prob: # only diff with mixtral sparse moe block!
103
+ routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
104
+ # we cast back to the input dtype
105
+ routing_weights = routing_weights.to(hidden_states.dtype)
106
+
107
+ selected_experts = Qwen3MoeSparseMoeBlock.dispatcher.dispatch(selected_experts)
108
+
109
+ w13_weight = torch.stack(
110
+ [torch.cat([layer.gate_proj.weight, layer.up_proj.weight], dim=0) for layer in self.experts]
111
+ )
112
+ w2_weight = torch.stack([layer.down_proj.weight for layer in self.experts], dim=0)
113
+
114
+ final_hidden_states = fused_experts_impl(
115
+ hidden_states.to(torch.bfloat16),
116
+ w13_weight,
117
+ w2_weight,
118
+ routing_weights,
119
+ selected_experts,
120
+ )
121
+
122
+ return final_hidden_states, router_logits
123
+
124
+
125
+ def apply_true_on_policy_patch_for_qwen3_moe():
126
+ from transformers.models.qwen3_moe import modeling_qwen3_moe
127
+
128
+ modeling_qwen3_moe.Qwen3MoeSparseMoeBlock = Qwen3MoeSparseMoeBlock
slime/backends/fsdp_utils/models/qwen3_moe_hf.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import torch
5
+ import torch.nn.functional as F
6
+
7
+
8
+ def apply_fsdp_moe_patch():
9
+
10
+ from transformers.models.qwen3_moe import modeling_qwen3_moe
11
+
12
+ def _forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
13
+ batch_size, sequence_length, hidden_dim = hidden_states.shape
14
+ hidden_states = hidden_states.view(-1, hidden_dim)
15
+ router_logits = self.gate(hidden_states)
16
+
17
+ routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float)
18
+ routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
19
+ if self.norm_topk_prob:
20
+ routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
21
+ routing_weights = routing_weights.to(hidden_states.dtype)
22
+
23
+ final_hidden_states = torch.zeros(
24
+ (batch_size * sequence_length, hidden_dim), dtype=hidden_states.dtype, device=hidden_states.device
25
+ )
26
+
27
+ expert_mask = torch.nn.functional.one_hot(selected_experts, num_classes=self.num_experts).permute(2, 1, 0)
28
+
29
+ # Loop over all experts
30
+ for expert_idx in range(self.num_experts):
31
+ expert_layer = self.experts[expert_idx]
32
+ idx, top_x = torch.where(expert_mask[expert_idx])
33
+
34
+ if top_x.numel() > 0:
35
+ current_state = hidden_states[None, top_x].reshape(-1, hidden_dim)
36
+ current_hidden_states = expert_layer(current_state) * routing_weights[top_x, idx, None]
37
+ final_hidden_states.index_add_(0, top_x, current_hidden_states.to(hidden_states.dtype))
38
+ else:
39
+ # force experts to participate in computation graph
40
+ dummy_output = expert_layer(hidden_states[:1]) * 0.0
41
+ final_hidden_states[:1] = final_hidden_states[:1] + dummy_output
42
+
43
+ final_hidden_states = final_hidden_states.reshape(batch_size, sequence_length, hidden_dim)
44
+ return final_hidden_states, router_logits
45
+
46
+ modeling_qwen3_moe.Qwen3MoeSparseMoeBlock.forward = _forward
slime/backends/fsdp_utils/update_weight_utils.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import abc
5
+ import logging
6
+ import socket
7
+ from argparse import Namespace
8
+ from collections.abc import Sequence
9
+
10
+ import ray
11
+ import torch
12
+ import torch.distributed as dist
13
+ from ray.actor import ActorHandle
14
+ from torch.distributed.tensor import DTensor, Replicate
15
+
16
+ try:
17
+ from sglang.srt.utils.patch_torch import monkey_patch_torch_reductions # type: ignore[import]
18
+ except ImportError:
19
+ from sglang.srt.patch_torch import monkey_patch_torch_reductions # type: ignore[import]
20
+
21
+ from sglang.srt.utils import MultiprocessingSerializer
22
+
23
+ from slime.utils.distributed_utils import init_process_group
24
+
25
+
26
+ try:
27
+ from sglang.srt.weight_sync.tensor_bucket import FlattenedTensorBucket # type: ignore[import]
28
+ except ImportError:
29
+ from sglang.srt.model_executor.model_runner import FlattenedTensorBucket # type: ignore[import]
30
+
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class UpdateWeight(abc.ABC):
36
+ def __init__(self, args: Namespace, model: torch.nn.Module) -> None:
37
+ self.args = args
38
+ self.model = model
39
+ self.weight_version = 0
40
+
41
+ @abc.abstractmethod
42
+ def connect_rollout_engines(
43
+ self,
44
+ rollout_engines: Sequence[ActorHandle],
45
+ rollout_engine_lock: ActorHandle | None,
46
+ ) -> None:
47
+ pass
48
+
49
+ def update_weights(self) -> None:
50
+ self.weight_version += 1
51
+ bucket = []
52
+ bucket_size = 0
53
+ for name, param in self.model.state_dict().items():
54
+ param_size = param.numel() * param.element_size()
55
+ if bucket and bucket_size + param_size >= self.args.update_weight_buffer_size:
56
+ self.wait_and_update_bucket_weights(bucket)
57
+ del bucket
58
+ bucket = []
59
+ bucket_size = 0
60
+
61
+ param = param.cuda()
62
+ if isinstance(param, DTensor):
63
+ # async version of param.full_tensor
64
+ param = param.redistribute(
65
+ placements=[Replicate()] * param.device_mesh.ndim,
66
+ async_op=True,
67
+ ).to_local()
68
+ bucket.append((name, param))
69
+ bucket_size += param_size
70
+
71
+ if bucket:
72
+ self.wait_and_update_bucket_weights(bucket)
73
+ del bucket
74
+ bucket = []
75
+ bucket_size = 0
76
+
77
+ def wait_and_update_bucket_weights(self, bucket):
78
+ bucket = [(name, param.wait()) if hasattr(param, "wait") else (name, param) for name, param in bucket]
79
+ self.update_bucket_weights(bucket, weight_version=self.weight_version)
80
+
81
+ @abc.abstractmethod
82
+ def update_bucket_weights(self, named_tensors, weight_version=None) -> None:
83
+ pass
84
+
85
+
86
+ class UpdateWeightFromTensor(UpdateWeight):
87
+ """Push model weights to rollout engines using tensors.
88
+
89
+ Streams parameters in size-bounded buckets; optionally groups tensors by dtype
90
+ and flattens per dtype, gathers per-rank blobs to the source, and issues one
91
+ RPC per dtype per bucket (or one per bucket if not flattened).
92
+ """
93
+
94
+ def connect_rollout_engines(
95
+ self,
96
+ rollout_engines: Sequence[ActorHandle],
97
+ rollout_engine_lock: ActorHandle | None,
98
+ ) -> None:
99
+ """Attach rollout engines and create per-engine IPC (Gloo) groups.
100
+
101
+ Sets the gather source rank, engine handle, and `tp_rank` within the
102
+ engine's local group.
103
+ """
104
+ self.rollout_engines = rollout_engines
105
+
106
+ # Here we assume the gpu id of rollout engines and train actors are the same.
107
+ for i, engine in enumerate(self.rollout_engines):
108
+ start_rank = i * self.args.rollout_num_gpus_per_engine
109
+ end_rank = (i + 1) * self.args.rollout_num_gpus_per_engine
110
+ group_ranks = list(range(start_rank, end_rank))
111
+ new_group = dist.new_group(
112
+ ranks=group_ranks,
113
+ backend="gloo",
114
+ )
115
+ if dist.get_rank() in group_ranks:
116
+ self._ipc_gather_src = start_rank
117
+ self._ipc_gather_group = new_group
118
+ self._ipc_engine = engine
119
+ # Calculate TP rank within this SGLang engine group
120
+ self.tp_rank = dist.get_rank() - start_rank
121
+
122
+ def update_bucket_weights(self, named_tensors, weight_version=None) -> None:
123
+ monkey_patch_torch_reductions()
124
+ # Use flattened bucket approach similar to Megatron
125
+ logger.info("Using flattened tensor bucket")
126
+ # Group tensors by dtype (same as Megatron)
127
+ named_tensors_by_dtypes = {}
128
+ for name, tensor in named_tensors:
129
+ dtype = tensor.dtype
130
+ if dtype not in named_tensors_by_dtypes:
131
+ named_tensors_by_dtypes[dtype] = []
132
+ named_tensors_by_dtypes[dtype].append((name, tensor))
133
+
134
+ # Create flattened bucket for each dtype group
135
+ serialized_tensors = []
136
+ for _dtype, named_tensors in named_tensors_by_dtypes.items():
137
+ flattened_tensor_bucket = FlattenedTensorBucket(named_tensors=named_tensors)
138
+ metadata = flattened_tensor_bucket.get_metadata()
139
+ flattened_tensor_data = {
140
+ "flattened_tensor": flattened_tensor_bucket.get_flattened_tensor(),
141
+ "metadata": metadata,
142
+ }
143
+ serialized_tensors.append(MultiprocessingSerializer.serialize(flattened_tensor_data, output_str=True))
144
+
145
+ if self._ipc_gather_src == dist.get_rank():
146
+ # On rank 0, prepare a list to hold the gathered batches from all ranks.
147
+ gathered_serialized_batches = [None for _ in range(dist.get_world_size(self._ipc_gather_group))]
148
+ else:
149
+ gathered_serialized_batches = None
150
+
151
+ # Gather the serialized batches from all ranks to rank 0.
152
+ dist.gather_object(
153
+ obj=serialized_tensors,
154
+ object_gather_list=gathered_serialized_batches,
155
+ dst=self._ipc_gather_src,
156
+ group=self._ipc_gather_group,
157
+ )
158
+
159
+ if dist.get_rank() == self._ipc_gather_src:
160
+ # Handle flattened bucket format (same as Megatron approach)
161
+ # Each rank may have multiple dtype buckets
162
+ # TODO: here we assume all ranks have the same number of dtypes
163
+ num_dtypes = len(gathered_serialized_batches[0])
164
+ assert num_dtypes > 0
165
+ for i in range(num_dtypes):
166
+ kwargs = {
167
+ "serialized_named_tensors": [tensors[i] for tensors in gathered_serialized_batches],
168
+ "load_format": "flattened_bucket",
169
+ "flush_cache": False,
170
+ "weight_version": str(weight_version),
171
+ }
172
+ ref = self._ipc_engine.update_weights_from_tensor.remote(**kwargs)
173
+ ray.get(ref)
174
+
175
+ if dist.get_rank() == self._ipc_gather_src:
176
+ ref = self._ipc_engine.flush_cache.remote()
177
+ ray.get(ref)
178
+
179
+
180
+ class UpdateWeightFromDistributed(UpdateWeight):
181
+ """Broadcast weights via a temporary NCCL group to rollout engines."""
182
+
183
+ def connect_rollout_engines(
184
+ self,
185
+ rollout_engines: Sequence[ActorHandle],
186
+ rollout_engine_lock: ActorHandle | None,
187
+ ) -> None:
188
+ """On rank 0, initialize a temporary NCCL group for parameter broadcast."""
189
+ self.rollout_engines = rollout_engines
190
+ self.rollout_engine_lock = rollout_engine_lock
191
+
192
+ # For TP:
193
+ # 1. AllGather parameters to rank 0
194
+ # 2. Broadcast parameters from rank 0 to all sglang engines
195
+ self._is_src_rank = dist.get_rank() == 0
196
+ if self._is_src_rank:
197
+ self._group_name = "slime"
198
+ master_address = ray._private.services.get_node_ip_address()
199
+ with socket.socket() as sock:
200
+ sock.bind(("", 0))
201
+ master_port = sock.getsockname()[1]
202
+ ## TODO: why +1?
203
+ world_size = self.args.rollout_num_gpus + 1
204
+
205
+ refs = [
206
+ engine.init_weights_update_group.remote(
207
+ master_address,
208
+ master_port,
209
+ i * self.args.rollout_num_gpus_per_engine + 1,
210
+ world_size,
211
+ self._group_name,
212
+ backend="nccl",
213
+ )
214
+ for i, engine in enumerate(self.rollout_engines)
215
+ ]
216
+ self._model_update_groups = init_process_group(
217
+ backend="nccl",
218
+ init_method=f"tcp://{master_address}:{master_port}",
219
+ world_size=world_size,
220
+ rank=0,
221
+ group_name=self._group_name,
222
+ )
223
+ ray.get(refs)
224
+
225
+ def update_bucket_weights(self, named_tensors, weight_version=None) -> None:
226
+ """Send names/dtypes/shapes metadata to engines, then broadcast tensors.
227
+
228
+ Ensures tensors are contiguous; when `world_size == 1`, converts DTensors
229
+ to full tensors prior to `dist.broadcast`.
230
+ """
231
+ if not self._is_src_rank or not named_tensors:
232
+ return
233
+
234
+ refs = [
235
+ engine.update_weights_from_distributed.remote(
236
+ names=[name for name, _ in named_tensors],
237
+ dtypes=[param.dtype for _, param in named_tensors],
238
+ shapes=[param.shape for _, param in named_tensors],
239
+ group_name=self._group_name,
240
+ weight_version=str(weight_version),
241
+ )
242
+ for engine in self.rollout_engines
243
+ ]
244
+
245
+ handles = []
246
+ # Broadcast parameters one by one with memory management
247
+ for _name, param in named_tensors:
248
+ torch.cuda.empty_cache()
249
+ # Ensure tensor is contiguous and on the right device
250
+ param_data = param.data.contiguous()
251
+
252
+ # avoid `DTensor._op_dispatcher.dispatch` has `assert compute_mesh is not None` error
253
+ if dist.get_world_size() == 1 and isinstance(param_data, DTensor):
254
+ param_data = param_data.full_tensor()
255
+
256
+ # Synchronous broadcast to avoid memory buildup
257
+ handles.append(dist.broadcast(param_data, 0, group=self._model_update_groups, async_op=True))
258
+
259
+ for handle in handles:
260
+ handle.wait()
261
+ ray.get(refs)
slime/backends/megatron_utils/__init__.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+
6
+ import torch
7
+
8
+ try:
9
+ import deep_ep
10
+ from torch_memory_saver import torch_memory_saver
11
+
12
+ old_init = deep_ep.Buffer.__init__
13
+
14
+ def new_init(self, *args, **kwargs):
15
+ if torch_memory_saver._impl is not None:
16
+ torch_memory_saver._impl._binary_wrapper.cdll.tms_set_interesting_region(False)
17
+ old_init(self, *args, **kwargs)
18
+ torch.cuda.synchronize()
19
+ if torch_memory_saver._impl is not None:
20
+ torch_memory_saver._impl._binary_wrapper.cdll.tms_set_interesting_region(True)
21
+
22
+ deep_ep.Buffer.__init__ = new_init
23
+ except ImportError:
24
+ logging.warning("deep_ep is not installed, some functionalities may be limited.")
25
+
26
+ try:
27
+ from megatron.bridge.models.qwen_vl.modelling_qwen3_vl.text_model import (
28
+ Qwen3VLMoETextRotaryEmbedding,
29
+ Qwen3VLTextRotaryEmbedding,
30
+ )
31
+
32
+ def patch_rotary_embedding(cls):
33
+ _original_forward = cls.forward
34
+
35
+ def _patched_forward(self, *args, packed_seq_params=None, **kwargs):
36
+ return _original_forward(self, *args, **kwargs)
37
+
38
+ cls.forward = _patched_forward
39
+
40
+ patch_rotary_embedding(Qwen3VLTextRotaryEmbedding)
41
+ patch_rotary_embedding(Qwen3VLMoETextRotaryEmbedding)
42
+ except ImportError:
43
+ pass
44
+
45
+ logging.getLogger().setLevel(logging.WARNING)
slime/backends/megatron_utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (2.19 kB). View file
 
slime/backends/megatron_utils/__pycache__/actor.cpython-312.pyc ADDED
Binary file (29.2 kB). View file
 
slime/backends/megatron_utils/__pycache__/arguments.cpython-312.pyc ADDED
Binary file (1.53 kB). View file
 
slime/backends/megatron_utils/__pycache__/checkpoint.cpython-312.pyc ADDED
Binary file (3.4 kB). View file
 
slime/backends/megatron_utils/__pycache__/cp_utils.cpython-312.pyc ADDED
Binary file (10.6 kB). View file
 
slime/backends/megatron_utils/__pycache__/data.cpython-312.pyc ADDED
Binary file (28.3 kB). View file
 
slime/backends/megatron_utils/__pycache__/initialize.cpython-312.pyc ADDED
Binary file (5.54 kB). View file
 
slime/backends/megatron_utils/__pycache__/loss.cpython-312.pyc ADDED
Binary file (32.1 kB). View file
 
slime/backends/megatron_utils/__pycache__/misc_utils.cpython-312.pyc ADDED
Binary file (507 Bytes). View file
 
slime/backends/megatron_utils/__pycache__/model.cpython-312.pyc ADDED
Binary file (29.5 kB). View file
 
slime/backends/megatron_utils/__pycache__/model_provider.cpython-312.pyc ADDED
Binary file (7.59 kB). View file
 
slime/backends/megatron_utils/__pycache__/sglang.cpython-312.pyc ADDED
Binary file (984 Bytes). View file
 
slime/backends/megatron_utils/actor.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ import os
6
+ import random
7
+ import socket
8
+ from argparse import Namespace
9
+ from contextlib import nullcontext
10
+
11
+ import ray
12
+ import torch
13
+ import torch.distributed as dist
14
+ from megatron.core import mpu
15
+ from ray.actor import ActorHandle
16
+ from torch_memory_saver import torch_memory_saver
17
+ from transformers import AutoConfig, AutoTokenizer
18
+
19
+ from slime.ray.train_actor import TrainRayActor
20
+ from slime.utils import train_dump_utils
21
+ from slime.utils.context_utils import with_defer
22
+ from slime.utils.data import process_rollout_data
23
+ from slime.utils.distributed_utils import get_gloo_group, init_process_group
24
+ from slime.utils.memory_utils import clear_memory, print_memory
25
+ from slime.utils.ray_utils import Box
26
+ from slime.utils.reloadable_process_group import destroy_process_groups, monkey_patch_torch_dist, reload_process_groups
27
+ from slime.utils.routing_replay import RoutingReplay
28
+ from slime.utils.timer import Timer, inverse_timer, timer
29
+ from slime.utils.tracking_utils import init_tracking
30
+ from slime.utils.types import RolloutBatch
31
+
32
+ from ...utils.profile_utils import TrainProfiler
33
+ from ...utils.tensor_backper import TensorBackuper
34
+ from .checkpoint import load_checkpoint
35
+ from .cp_utils import slice_log_prob_with_cp, slice_with_cp
36
+ from .data import DataIterator, get_data_iterator, log_perf_data, log_rollout_data, sync_actor_critic_data
37
+ from .initialize import init, is_megatron_main_rank
38
+ from .loss import compute_advantages_and_returns, get_log_probs_and_entropy, get_values
39
+ from .model import forward_only, initialize_model_and_optimizer, save, train
40
+ from .update_weight.common import named_params_and_buffers
41
+ from .update_weight.update_weight_from_distributed import UpdateWeightFromDistributed
42
+ from .update_weight.update_weight_from_tensor import UpdateWeightFromTensor
43
+
44
+ logging.getLogger("megatron").setLevel(logging.WARNING)
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ class MegatronTrainRayActor(TrainRayActor):
50
+ @with_defer(lambda: Timer().start("train_wait"))
51
+ def init(
52
+ self,
53
+ args: Namespace,
54
+ role: str,
55
+ with_ref: bool = False,
56
+ ) -> int | None:
57
+ monkey_patch_torch_dist()
58
+
59
+ super().init(args, role, with_ref)
60
+
61
+ init(args)
62
+
63
+ if is_megatron_main_rank():
64
+ init_tracking(args, primary=False)
65
+
66
+ self.prof = TrainProfiler(args)
67
+
68
+ # read config and tokenizer serialized to prevent concurrent writing bug.
69
+ for i in range(dist.get_world_size()):
70
+ if i == dist.get_rank():
71
+ self.hf_config = AutoConfig.from_pretrained(args.hf_checkpoint, trust_remote_code=True)
72
+ self.tokenizer = AutoTokenizer.from_pretrained(self.args.hf_checkpoint, trust_remote_code=True)
73
+ dist.barrier(group=get_gloo_group())
74
+
75
+ self.train_parallel_config = {
76
+ "dp_size": mpu.get_data_parallel_world_size(with_context_parallel=False),
77
+ }
78
+ dist.barrier(group=get_gloo_group())
79
+
80
+ if args.offload_train:
81
+ if (x := args.train_memory_margin_bytes) > 0:
82
+ logger.info(f"Set torch_memory_saver.memory_margin_bytes to {x}")
83
+ torch_memory_saver.memory_margin_bytes = x
84
+
85
+ if self.args.debug_rollout_only:
86
+ return 0
87
+
88
+ if role == "critic":
89
+ self.args.load = self.args.critic_load
90
+ self.args.save = self.args.critic_save
91
+ self.args.lr = self.args.critic_lr
92
+ self.args.lr_warmup_iters = self.args.critic_lr_warmup_iters
93
+
94
+ (self.model, self.optimizer, self.opt_param_scheduler, loaded_rollout_id) = initialize_model_and_optimizer(
95
+ args, role
96
+ )
97
+
98
+ if role == "critic":
99
+ if self.args.offload_train:
100
+ self.sleep()
101
+ return
102
+
103
+ start_rollout_id = loaded_rollout_id + 1
104
+
105
+ self.weights_backuper = TensorBackuper.create(
106
+ source_getter=lambda: named_params_and_buffers(
107
+ self.args,
108
+ self.model,
109
+ convert_to_global_name=args.megatron_to_hf_mode == "raw",
110
+ translate_gpu_to_cpu=not self.args.enable_weights_backuper,
111
+ ),
112
+ single_tag=None if args.enable_weights_backuper else "actor",
113
+ )
114
+ self._active_model_tag: str | None = "actor"
115
+ self.weights_backuper.backup("actor")
116
+
117
+ if with_ref:
118
+ self.load_other_checkpoint("ref", args.ref_load)
119
+
120
+ if self.args.keep_old_actor:
121
+ # Load old_actor checkpoint
122
+ self.load_other_checkpoint("old_actor", args.load)
123
+ # Create rollout_actor as a copy of current actor
124
+ if args.update_weights_interval == 1:
125
+ self.weights_backuper.backup("rollout_actor")
126
+
127
+ if self.args.vocab_size is None:
128
+ self.args.vocab_size = self.tokenizer.vocab_size
129
+
130
+ update_weight_cls = UpdateWeightFromTensor if self.args.colocate else UpdateWeightFromDistributed
131
+ self.weight_updater = update_weight_cls(
132
+ self.args,
133
+ self.model,
134
+ weights_getter=lambda: self.weights_backuper.get("actor"),
135
+ model_name=type(self.hf_config).__name__.lower() if self.args.model_name is None else self.args.model_name,
136
+ quantization_config=getattr(self.hf_config, "quantization_config", None),
137
+ )
138
+
139
+ # empty cache after initialization
140
+ clear_memory()
141
+
142
+ if self.args.offload_train:
143
+ # recover to actor in the end.
144
+ self._switch_model("actor")
145
+ self.sleep()
146
+
147
+ self.rollout_engines = None
148
+
149
+ self.rollout_data_postprocess = None
150
+ if self.args.rollout_data_postprocess_path is not None:
151
+ from slime.utils.misc import load_function
152
+
153
+ self.rollout_data_postprocess = load_function(self.args.rollout_data_postprocess_path)
154
+
155
+ self.prof.on_init_end()
156
+
157
+ return start_rollout_id
158
+
159
+ @timer
160
+ def sleep(self) -> None:
161
+ assert self.args.offload_train
162
+
163
+ clear_memory(clear_host_memory=True)
164
+ print_memory("before offload model")
165
+ destroy_process_groups()
166
+
167
+ torch_memory_saver.pause()
168
+
169
+ print_memory("after offload model")
170
+
171
+ @timer
172
+ def wake_up(self) -> None:
173
+ assert self.args.offload_train
174
+ print_memory("before wake_up model")
175
+
176
+ torch_memory_saver.resume()
177
+
178
+ clear_memory()
179
+ reload_process_groups()
180
+ print_memory("after wake_up model")
181
+
182
+ def _get_rollout_data(self, rollout_data_ref: Box) -> RolloutBatch:
183
+ # Fetch data through ray on CPU, not sure if this will be performance bottleneck.
184
+ # Both first pp stage and the last pp stage will receive the data.
185
+ rollout_data = process_rollout_data(
186
+ self.args,
187
+ rollout_data_ref,
188
+ mpu.get_data_parallel_rank(with_context_parallel=False),
189
+ mpu.get_data_parallel_world_size(with_context_parallel=False),
190
+ )
191
+ # TODO: this is ugly, move to somewhere else?
192
+ # move tokens to GPU in advance
193
+ rollout_data["tokens"] = [
194
+ torch.tensor(t, dtype=torch.long, device=torch.cuda.current_device()) for t in rollout_data["tokens"]
195
+ ]
196
+ rollout_data["loss_masks"] = [
197
+ torch.tensor(t, dtype=torch.int, device=torch.cuda.current_device()) for t in rollout_data["loss_masks"]
198
+ ]
199
+ if "multimodal_train_inputs" in rollout_data:
200
+ # Move multimodal training tensors to GPU in advance
201
+ rollout_data["multimodal_train_inputs"] = [
202
+ (
203
+ {key: tensor.to(device=torch.cuda.current_device()) for key, tensor in mm_dict.items()}
204
+ if mm_dict is not None
205
+ else None
206
+ )
207
+ for mm_dict in rollout_data["multimodal_train_inputs"]
208
+ ]
209
+ if "rollout_log_probs" in rollout_data:
210
+ rollout_data["rollout_log_probs"] = [
211
+ torch.tensor(
212
+ slice_log_prob_with_cp(log_prob, total_length, response_length),
213
+ device=torch.cuda.current_device(),
214
+ dtype=torch.float32,
215
+ )
216
+ for log_prob, total_length, response_length in zip(
217
+ rollout_data["rollout_log_probs"],
218
+ rollout_data["total_lengths"],
219
+ rollout_data["response_lengths"],
220
+ strict=False,
221
+ )
222
+ ]
223
+ if "rollout_routed_experts" in rollout_data:
224
+ rollout_data["rollout_routed_experts"] = [
225
+ torch.from_numpy(r) for r in rollout_data["rollout_routed_experts"]
226
+ ]
227
+ return rollout_data
228
+
229
+ def _switch_model(self, target_tag: str) -> None:
230
+ if target_tag not in self.weights_backuper.backup_tags:
231
+ raise ValueError(f"Cannot switch to unknown model tag: {target_tag}")
232
+ self.weights_backuper.restore(target_tag)
233
+ self._active_model_tag = target_tag
234
+
235
+ def fill_routing_replay(self, data_iterator, num_microbatches, rollout_data):
236
+ if "rollout_routed_experts" not in rollout_data:
237
+ raise ValueError(
238
+ "rollout_routed_experts is required in rollout_data when use_rollout_routing_replay is set."
239
+ )
240
+
241
+ from megatron.core.transformer.transformer_block import get_num_layers_to_build
242
+ from megatron.core.transformer.transformer_layer import get_transformer_layer_offset
243
+
244
+ from slime.utils.routing_replay import RoutingReplay
245
+
246
+ for iterator in data_iterator:
247
+ iterator.reset()
248
+
249
+ tp_rank = mpu.get_tensor_model_parallel_rank()
250
+ tp_size = mpu.get_tensor_model_parallel_world_size()
251
+
252
+ def pad_func(experts, pad):
253
+ _, num_layers, topk = experts.shape
254
+ pad = (
255
+ torch.arange(
256
+ pad * num_layers * topk,
257
+ device=experts.device,
258
+ dtype=experts.dtype,
259
+ ).reshape((pad, num_layers, topk))
260
+ % self.args.num_experts
261
+ )
262
+ return torch.cat([experts, pad], dim=0)
263
+
264
+ for _ in range(sum(num_microbatches)):
265
+ batch = data_iterator[0].get_next(["rollout_routed_experts", "tokens"])
266
+ rollout_routed_experts = batch["rollout_routed_experts"]
267
+ tokens = batch["tokens"]
268
+ assert len(rollout_routed_experts) == len(tokens)
269
+ for a, b in zip(rollout_routed_experts, tokens, strict=False):
270
+ assert a.shape[0] == b.shape[0] - 1, f"{a.shape}, {b.shape}"
271
+
272
+ # We need to pad the experts to the last token. We won't calculate loss on this token so this should be fine.
273
+ # TODO: fuse this padding with the following slice_with_cp to reduce memory copy.
274
+ rollout_routed_experts = [pad_func(r, 1) for r in rollout_routed_experts]
275
+ # TODO: maybe extract a common process function for here and get_batch?
276
+ rollout_routed_experts = [slice_with_cp(r, pad_func) for r in rollout_routed_experts]
277
+ rollout_routed_experts = torch.cat(rollout_routed_experts, dim=0)
278
+ pad_size = mpu.get_tensor_model_parallel_world_size() * self.args.data_pad_size_multiplier
279
+ pad = (pad_size - rollout_routed_experts.size(0) % pad_size) % pad_size
280
+ if pad != 0:
281
+ rollout_routed_experts = pad_func(rollout_routed_experts, pad)
282
+
283
+ if self.args.sequence_parallel:
284
+ seqlen = rollout_routed_experts.size(0)
285
+ assert seqlen % tp_size == 0
286
+ start, end = seqlen // tp_size * tp_rank, seqlen // tp_size * (tp_rank + 1)
287
+ rollout_routed_experts = rollout_routed_experts[start:end]
288
+
289
+ routing_replay_offset = 0
290
+ for vp_stage, model in enumerate(self.model):
291
+ config = model.module.config
292
+ num_layers_to_build = get_num_layers_to_build(config, vp_stage=vp_stage)
293
+ offset = get_transformer_layer_offset(config, vp_stage=vp_stage)
294
+ for layer_id in range(offset, offset + num_layers_to_build):
295
+ # skip dense layer
296
+ if isinstance(config.moe_layer_freq, int):
297
+ if layer_id % config.moe_layer_freq != 0:
298
+ continue
299
+ elif isinstance(config.moe_layer_freq, list):
300
+ assert len(config.moe_layer_freq) == config.num_layers
301
+ if config.moe_layer_freq[layer_id] == 0:
302
+ continue
303
+ layer_routed_experts = rollout_routed_experts[:, layer_id]
304
+ RoutingReplay.all_routing_replays[routing_replay_offset].record(layer_routed_experts)
305
+ routing_replay_offset += 1
306
+ assert routing_replay_offset == len(RoutingReplay.all_routing_replays)
307
+
308
+ del rollout_data["rollout_routed_experts"]
309
+
310
+ for iterator in data_iterator:
311
+ iterator.reset()
312
+
313
+ def compute_log_prob(
314
+ self,
315
+ data_iterator: list[DataIterator],
316
+ num_microbatches: list[int],
317
+ store_prefix: str = "",
318
+ ) -> dict[str, list[torch.Tensor]]:
319
+
320
+ with timer(f"{store_prefix}log_probs"):
321
+ return forward_only(
322
+ get_log_probs_and_entropy,
323
+ self.args,
324
+ self.model,
325
+ data_iterator,
326
+ num_microbatches,
327
+ store_prefix=store_prefix,
328
+ )
329
+
330
+ def train(self, rollout_id: int, rollout_data_ref: Box) -> None:
331
+ if self.args.offload_train:
332
+ self.wake_up()
333
+
334
+ with timer("data_preprocess"):
335
+ rollout_data = self._get_rollout_data(rollout_data_ref)
336
+ if self.args.debug_rollout_only:
337
+ log_rollout_data(rollout_id, self.args, rollout_data)
338
+ return
339
+
340
+ if self.role == "critic":
341
+ return self.train_critic(rollout_id, rollout_data)
342
+ else:
343
+ return self.train_actor(rollout_id, rollout_data)
344
+
345
+ def train_critic(self, rollout_id: int, rollout_data: RolloutBatch) -> None:
346
+ # Create data iterator for log_probs and train.
347
+ data_iterator, num_microbatches = get_data_iterator(self.args, self.model, rollout_data)
348
+ rollout_data.update(
349
+ forward_only(
350
+ get_values,
351
+ self.args,
352
+ self.model,
353
+ data_iterator,
354
+ num_microbatches,
355
+ )
356
+ )
357
+
358
+ if rollout_id >= self.args.num_critic_only_steps:
359
+ sync_actor_critic_data(self.args, rollout_data, self._actor_critic_groups)
360
+
361
+ compute_advantages_and_returns(self.args, rollout_data)
362
+
363
+ self.args.loss_type = "value_loss"
364
+ train(
365
+ rollout_id,
366
+ self.model,
367
+ self.optimizer,
368
+ self.opt_param_scheduler,
369
+ data_iterator,
370
+ num_microbatches,
371
+ )
372
+
373
+ def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None:
374
+ # Create data iterator for log_probs and train.
375
+ data_iterator, num_microbatches = get_data_iterator(self.args, self.model, rollout_data)
376
+
377
+ if self.args.use_rollout_routing_replay:
378
+ self.fill_routing_replay(data_iterator, num_microbatches, rollout_data)
379
+
380
+ with inverse_timer("train_wait"), timer("train"):
381
+ if self.args.compute_advantages_and_returns:
382
+ if "ref" in self.weights_backuper.backup_tags:
383
+ if self.args.use_routing_replay:
384
+ os.environ["ROUTING_REPLAY_STAGE"] = "fallthrough"
385
+ self._switch_model("ref")
386
+ rollout_data.update(
387
+ self.compute_log_prob(
388
+ data_iterator,
389
+ num_microbatches,
390
+ store_prefix="ref_",
391
+ )
392
+ )
393
+ self._switch_model("old_actor" if self.args.keep_old_actor else "actor")
394
+ if not self.args.use_rollout_logprobs or self.args.get_mismatch_metrics:
395
+ if self.args.use_routing_replay:
396
+ if self.args.use_rollout_routing_replay:
397
+ os.environ["ROUTING_REPLAY_STAGE"] = "replay_forward"
398
+ else:
399
+ os.environ["ROUTING_REPLAY_STAGE"] = "record"
400
+ rollout_data.update(
401
+ self.compute_log_prob(
402
+ data_iterator,
403
+ num_microbatches,
404
+ store_prefix="",
405
+ )
406
+ )
407
+ if self.args.use_rollout_routing_replay:
408
+ RoutingReplay.clear_all_forward()
409
+
410
+ if self.args.use_critic:
411
+ sync_actor_critic_data(
412
+ self.args,
413
+ rollout_data,
414
+ self._actor_critic_groups,
415
+ )
416
+ if self._active_model_tag != "actor":
417
+ self._switch_model("actor")
418
+
419
+ # Calculate adv and returns. Need to performed before training (instead of on the fly),
420
+ # because we may need normalize the whole rollout.
421
+ compute_advantages_and_returns(self.args, rollout_data)
422
+
423
+ if self.rollout_data_postprocess is not None:
424
+ self.rollout_data_postprocess(self.args)
425
+
426
+ log_rollout_data(rollout_id, self.args, rollout_data)
427
+
428
+ # Train
429
+ if self.args.use_routing_replay:
430
+ os.environ["ROUTING_REPLAY_STAGE"] = "replay_backward"
431
+ with timer("actor_train"):
432
+ train(
433
+ rollout_id,
434
+ self.model,
435
+ self.optimizer,
436
+ self.opt_param_scheduler,
437
+ data_iterator,
438
+ num_microbatches,
439
+ )
440
+
441
+ self.prof.step(rollout_id=rollout_id)
442
+
443
+ train_dump_utils.save_debug_train_data(self.args, rollout_id=rollout_id, rollout_data=rollout_data)
444
+
445
+ if self.args.use_routing_replay:
446
+ RoutingReplay.clear_all()
447
+
448
+ # update the cpu actor weight to the latest model
449
+ self.weights_backuper.backup("actor")
450
+
451
+ # Update ref model if needed
452
+ if (
453
+ self.args.ref_update_interval is not None
454
+ and (rollout_id + 1) % self.args.ref_update_interval == 0
455
+ and "ref" in self.weights_backuper.backup_tags
456
+ ):
457
+ with timer("ref_model_update"):
458
+ if is_megatron_main_rank():
459
+ logger.info(f"Updating ref model at rollout_id {rollout_id}")
460
+ self.weights_backuper.backup("ref")
461
+
462
+ log_perf_data(rollout_id, self.args)
463
+
464
+ @timer
465
+ def save_model(self, rollout_id: int, force_sync: bool = False) -> None:
466
+ if self.args.debug_rollout_only:
467
+ return
468
+
469
+ # torch dist may trigger nccl communication during saving.
470
+ if self.args.offload_train:
471
+ reload_process_groups()
472
+
473
+ if self.args.async_save:
474
+ from megatron.training.async_utils import maybe_finalize_async_save
475
+
476
+ maybe_finalize_async_save(blocking=True)
477
+
478
+ save(rollout_id, self.model, self.optimizer, self.opt_param_scheduler)
479
+
480
+ if force_sync and self.args.async_save:
481
+ maybe_finalize_async_save(blocking=True)
482
+
483
+ if self.args.offload_train:
484
+ destroy_process_groups()
485
+
486
+ @timer
487
+ def update_weights(self) -> None:
488
+ if self.args.debug_train_only or self.args.debug_rollout_only:
489
+ return
490
+
491
+ if self.args.offload_train:
492
+ reload_process_groups()
493
+
494
+ rollout_engines, rollout_engine_lock, num_new_engines = ray.get(
495
+ self.rollout_manager.get_rollout_engines_and_lock.remote()
496
+ )
497
+ if num_new_engines > 0:
498
+ self.weight_updater.connect_rollout_engines(rollout_engines, rollout_engine_lock)
499
+ dist.barrier(group=get_gloo_group())
500
+
501
+ with torch_memory_saver.disable() if self.args.offload_train else nullcontext():
502
+ print_memory("before update_weights")
503
+ self.weight_updater.update_weights()
504
+ print_memory("after update_weights")
505
+
506
+ if self.args.ci_test and len(rollout_engines) > 0:
507
+ engine = random.choice(rollout_engines)
508
+ engine_version = ray.get(engine.get_weight_version.remote())
509
+ if str(engine_version) != str(self.weight_updater.weight_version):
510
+ raise RuntimeError(
511
+ f"Weight version mismatch! Engine: {engine_version}, Updater: {self.weight_updater.weight_version}"
512
+ )
513
+
514
+ if getattr(self.args, "keep_old_actor", False):
515
+ if self.args.update_weights_interval == 1:
516
+ logger.info("updating model queue: rollout_actor -> old_actor, actor -> rollout_actor")
517
+ # Queue-style update: rollout_actor params -> old_actor, actor params -> rollout_actor
518
+ # First copy rollout_actor to old_actor
519
+ self.weights_backuper.copy(src_tag="rollout_actor", dst_tag="old_actor")
520
+ # Then copy current actor to rollout_actor
521
+ self.weights_backuper.backup("rollout_actor")
522
+ else:
523
+ self.weights_backuper.backup("old_actor")
524
+
525
+ if self.args.offload_train:
526
+ destroy_process_groups()
527
+
528
+ def load_other_checkpoint(self, model_tag: str, path: str) -> None:
529
+ old_args = self.args.load, self.args.no_load_optim, self.args.no_load_rng, self.args.finetune
530
+ self.args.load = path
531
+ self.args.no_load_optim = True
532
+ self.args.no_load_rng = True
533
+ self.args.finetune = True
534
+
535
+ if model_tag == "ref" and self.args.ref_ckpt_step is not None:
536
+ old_ckpt_step = self.args.ckpt_step
537
+ self.args.ckpt_step = self.args.ref_ckpt_step
538
+
539
+ _, _ = load_checkpoint(
540
+ self.model,
541
+ None,
542
+ None,
543
+ checkpointing_context={},
544
+ skip_load_to_model_and_opt=False,
545
+ )
546
+ self.args.load, self.args.no_load_optim, self.args.no_load_rng, self.args.finetune = old_args
547
+
548
+ if model_tag == "ref" and self.args.ref_ckpt_step is not None:
549
+ self.args.ckpt_step = old_ckpt_step
550
+
551
+ self.weights_backuper.backup(model_tag)
552
+ self._active_model_tag = model_tag
553
+
554
+ def connect_actor_critic(
555
+ self,
556
+ actor_handle: ActorHandle | None = None,
557
+ master_address: str | None = None,
558
+ master_port: int | None = None,
559
+ ) -> None:
560
+ if self.role == "actor":
561
+ master_address = ray.util.get_node_ip_address()
562
+ with socket.socket() as sock:
563
+ sock.bind(("", 0))
564
+ master_port = sock.getsockname()[1]
565
+ actor_handle.connect_actor_critic.remote(master_address=master_address, master_port=master_port)
566
+
567
+ group_name = "actor_critic"
568
+ world_size = 2
569
+ self._actor_critic_groups = init_process_group(
570
+ backend="nccl",
571
+ init_method=f"tcp://{master_address}:{master_port}",
572
+ world_size=world_size,
573
+ rank=0 if self.role == "actor" else 1,
574
+ group_name=group_name,
575
+ )
slime/backends/megatron_utils/arguments.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+
6
+ from megatron.training.arguments import parse_args, validate_args
7
+ from megatron.training.tokenizer.tokenizer import _vocab_size_with_padding
8
+
9
+ __all__ = ["validate_args", "parse_args", "set_default_megatron_args"]
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def set_default_megatron_args(args):
15
+ # always use zero optimizer
16
+ args.use_distributed_optimizer = True
17
+ # TODO: maybe change this after megatron has good fp8 support
18
+ args.bf16 = not args.fp16
19
+ # placeholders
20
+ args.seq_length = 4096
21
+ args.max_position_embeddings = args.seq_length
22
+ # compatible for megatron
23
+ if hasattr(args, "rope_type") and args.rope_type is None:
24
+ args.rope_type = "yarn" if args.multi_latent_attention else "rope"
25
+
26
+ if args.vocab_size and not args.padded_vocab_size:
27
+ args.padded_vocab_size = _vocab_size_with_padding(args.vocab_size, args)
28
+
29
+ if not args.tokenizer_model and not args.tokenizer_type:
30
+ logger.info("--tokenizer-model not set, use --hf-checkpoint as tokenizer model.")
31
+ args.tokenizer_model = args.hf_checkpoint
32
+ args.tokenizer_type = "HuggingFaceTokenizer"
33
+ return args
slime/backends/megatron_utils/checkpoint.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+
9
+ # TODO: may need to copy those 2 functions and do refactoring.
10
+ from megatron.training.checkpointing import load_checkpoint as _load_checkpoint_megatron
11
+ from megatron.training.checkpointing import save_checkpoint
12
+ from megatron.training.global_vars import get_args
13
+
14
+ from slime.utils import megatron_bridge_utils
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ __all__ = ["save_checkpoint"]
19
+
20
+
21
+ def load_checkpoint(ddp_model, optimizer, opt_param_scheduler, checkpointing_context, skip_load_to_model_and_opt):
22
+ # ref: how megatron `load_checkpoint` gets directory
23
+ args = get_args()
24
+ load_path = args.load
25
+
26
+ assert Path(load_path).exists() and _is_dir_nonempty(
27
+ load_path
28
+ ), f"{args.load=} does not exist or is an empty directory. Did you specify the wrong folder?"
29
+
30
+ if _is_megatron_checkpoint(load_path):
31
+ return _load_checkpoint_megatron(
32
+ ddp_model=ddp_model,
33
+ optimizer=optimizer,
34
+ opt_param_scheduler=opt_param_scheduler,
35
+ checkpointing_context=checkpointing_context,
36
+ skip_load_to_model_and_opt=skip_load_to_model_and_opt,
37
+ )
38
+ else:
39
+ return _load_checkpoint_hf(
40
+ ddp_model=ddp_model,
41
+ optimizer=optimizer,
42
+ args=args,
43
+ load_path=load_path,
44
+ )
45
+
46
+
47
+ def _is_megatron_checkpoint(path: str | Path) -> bool:
48
+ return (Path(path) / "latest_checkpointed_iteration.txt").is_file() or bool(
49
+ re.fullmatch(r"iter_\d{7}", Path(path).name)
50
+ )
51
+
52
+
53
+ def _load_checkpoint_hf(ddp_model, optimizer, args, load_path: str):
54
+ assert args.megatron_to_hf_mode == "bridge", "Only bridge mode is supported for loading HF checkpoint"
55
+ from megatron.bridge import AutoBridge
56
+
57
+ import slime_plugins.megatron_bridge # noqa: F401
58
+
59
+ logger.info(f"Load checkpoint from HuggingFace model into Megatron (path={load_path})")
60
+
61
+ with megatron_bridge_utils.patch_megatron_model(ddp_model):
62
+ bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True)
63
+ bridge.load_hf_weights(ddp_model)
64
+
65
+ # Copied from Megatron-core :: load_checkpoint (with simplifications)
66
+ if (args.fp16 or args.bf16) and optimizer is not None:
67
+ assert not args.load_main_params_from_ckpt
68
+ optimizer.reload_model_params()
69
+
70
+ # We can see `successfully loaded checkpoint from ... [ t 1/2, p 1/1 ] at iteration 0`
71
+ # when loading Megatron, thus it is 0
72
+ iteration = 0
73
+ num_floating_point_operations_so_far = 0
74
+ return iteration, num_floating_point_operations_so_far
75
+
76
+
77
+ def _is_dir_nonempty(path):
78
+ with os.scandir(path) as it:
79
+ return any(it)
slime/backends/megatron_utils/ci_utils.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """CI utilities for Megatron backend testing."""
5
+
6
+ import logging
7
+ from collections.abc import Sequence
8
+
9
+ from megatron.core.distributed import DistributedDataParallel as DDP
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def check_mtp_only_grad(model: Sequence[DDP], step_id: int) -> None:
15
+ """Check that only MTP parameters have non-zero gradients.
16
+
17
+ This is used for CI testing to verify that when all outputs are truncated,
18
+ only the MTP layers receive gradients (since only mtp_loss contributes).
19
+
20
+ Args:
21
+ model: Sequence of DDP-wrapped model chunks.
22
+ step_id: Current step index for logging.
23
+
24
+ Raises:
25
+ AssertionError: If any non-MTP parameter has a non-zero gradient.
26
+ """
27
+ non_mtp_nonzero_grads = []
28
+ mtp_nonzero_grads = []
29
+
30
+ for model_chunk in model:
31
+ for name, param in model_chunk.named_parameters():
32
+ # Get the main_grad from the distributed optimizer if available
33
+ grad = getattr(param, "main_grad", None)
34
+ if grad is None:
35
+ grad = param.grad
36
+ if grad is None:
37
+ continue
38
+
39
+ grad_norm = grad.abs().max().item()
40
+ is_mtp = ".mtp." in name
41
+
42
+ if is_mtp:
43
+ if grad_norm > 0:
44
+ mtp_nonzero_grads.append((name, grad_norm))
45
+ else:
46
+ if grad_norm > 0:
47
+ non_mtp_nonzero_grads.append((name, grad_norm))
48
+
49
+ # Log the results
50
+ logger.info(
51
+ f"[CI MTP Grad Check] Step {step_id}: "
52
+ f"MTP params with non-zero grad: {len(mtp_nonzero_grads)}, "
53
+ f"non-MTP params with non-zero grad: {len(non_mtp_nonzero_grads)}"
54
+ )
55
+
56
+ if non_mtp_nonzero_grads:
57
+ # Log the first few non-MTP params with non-zero gradients for debugging
58
+ for name, grad_norm in non_mtp_nonzero_grads[:5]:
59
+ logger.error(f"[CI MTP Grad Check] Non-MTP param with non-zero grad: {name}, max_grad={grad_norm}")
60
+
61
+ assert len(non_mtp_nonzero_grads) == 0, (
62
+ f"Expected all non-MTP parameters to have zero gradients, "
63
+ f"but found {len(non_mtp_nonzero_grads)} with non-zero gradients. "
64
+ f"First few: {non_mtp_nonzero_grads[:5]}"
65
+ )
66
+
67
+ # Also verify that MTP params do have gradients (otherwise the test is not valid)
68
+ assert len(mtp_nonzero_grads) > 0, (
69
+ "Expected MTP parameters to have non-zero gradients, but all were zero. "
70
+ "This may indicate the MTP loss is not being computed."
71
+ )
72
+
73
+
74
+ def check_mtp_loss(mtp_loss: float, max_mtp_loss: float = 1.0) -> None:
75
+ """Check that MTP loss is within expected bounds.
76
+
77
+ Args:
78
+ mtp_loss: The computed MTP loss value.
79
+ max_mtp_loss: Maximum allowed MTP loss (default: 1.0).
80
+
81
+ Raises:
82
+ AssertionError: If MTP loss exceeds the maximum allowed value.
83
+ """
84
+ assert mtp_loss < max_mtp_loss, (
85
+ f"MTP loss {mtp_loss} exceeds maximum allowed value {max_mtp_loss}. "
86
+ "This may indicate an issue with MTP training."
87
+ )
slime/backends/megatron_utils/cp_utils.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from collections.abc import Callable
5
+
6
+ import torch
7
+ import torch.distributed as dist
8
+ import torch.nn.functional as F
9
+ from megatron.core import mpu
10
+
11
+
12
+ def get_logits_and_tokens_offset_with_cp(
13
+ total_length: int,
14
+ response_length: int,
15
+ ):
16
+ """
17
+ All offsets start from the begining of the prompt.
18
+ """
19
+ cp_rank = mpu.get_context_parallel_rank()
20
+ cp_size = mpu.get_context_parallel_world_size()
21
+ assert cp_size > 1
22
+
23
+ prompt_length = total_length - response_length
24
+ chunk_size = (total_length + 2 * cp_size - 1) // (2 * cp_size)
25
+
26
+ # the offset of 2 chunks
27
+ chunk_0 = (cp_rank * chunk_size, (cp_rank + 1) * chunk_size)
28
+ chunk_1 = ((2 * cp_size - cp_rank - 1) * chunk_size, (2 * cp_size - cp_rank) * chunk_size)
29
+
30
+ # the offset of 2 logits, note that the logits need a "-1".
31
+ logits_0 = (max(chunk_0[0], prompt_length - 1), min(chunk_0[1], total_length - 1))
32
+ logits_1 = (max(chunk_1[0], prompt_length - 1), min(chunk_1[1], total_length - 1))
33
+
34
+ # when the sequence is empty, make an empty slice to continue the gradient flow.
35
+ if logits_0[0] < logits_0[1]:
36
+ token_0 = (logits_0[0] + 1, logits_0[1] + 1)
37
+ else:
38
+ logits_0 = (0, 0)
39
+ token_0 = (0, 0)
40
+
41
+ if logits_1[0] < logits_1[1]:
42
+ token_1 = (logits_1[0] + 1, logits_1[1] + 1)
43
+ else:
44
+ logits_1 = (0, 0)
45
+ token_1 = (0, 0)
46
+
47
+ return chunk_size, (chunk_0, chunk_1), (logits_0, logits_1), (token_0, token_1)
48
+
49
+
50
+ def get_sum_of_sample_mean(
51
+ total_lengths: list[int],
52
+ response_lengths: list[int],
53
+ loss_masks: list[torch.Tensor],
54
+ calculate_per_token_loss: bool = False,
55
+ ) -> Callable[[torch.Tensor], torch.Tensor]:
56
+ """
57
+ Calculate correct sample mean for CP
58
+ """
59
+ cp_size = mpu.get_context_parallel_world_size()
60
+ if cp_size == 1:
61
+
62
+ def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor:
63
+ return sum(
64
+ [
65
+ (x_i * loss_mask_i).sum() / torch.clamp_min(loss_mask_i.sum(), 1)
66
+ for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False)
67
+ ]
68
+ )
69
+
70
+ def sum_of_token(x: torch.Tensor) -> torch.Tensor:
71
+ return sum(
72
+ [
73
+ (x_i * loss_mask_i).sum()
74
+ for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False)
75
+ ]
76
+ )
77
+
78
+ else:
79
+ cp_chunk_lengths = []
80
+ chunked_loss_masks = []
81
+ for i, (total_length, response_length, loss_mask) in enumerate(
82
+ zip(total_lengths, response_lengths, loss_masks, strict=False)
83
+ ):
84
+ prompt_length = total_length - response_length
85
+ _, _, _, tokens_offset = get_logits_and_tokens_offset_with_cp(total_length, response_length)
86
+ loss_mask_0 = loss_mask[tokens_offset[0][0] - prompt_length : tokens_offset[0][1] - prompt_length]
87
+ loss_mask_1 = loss_mask[tokens_offset[1][0] - prompt_length : tokens_offset[1][1] - prompt_length]
88
+ chunked_loss_masks.append(torch.cat([loss_mask_0, loss_mask_1], dim=0))
89
+ cp_chunk_lengths.append(chunked_loss_masks[i].size(0))
90
+
91
+ def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor:
92
+ return sum(
93
+ [
94
+ (x_i * chunked_loss_mask).sum() / torch.clamp_min(loss_mask.sum(), 1)
95
+ for x_i, chunked_loss_mask, loss_mask in zip(
96
+ x.split(cp_chunk_lengths, dim=0), chunked_loss_masks, loss_masks, strict=False
97
+ )
98
+ ]
99
+ )
100
+
101
+ def sum_of_token(x: torch.Tensor) -> torch.Tensor:
102
+ return sum(
103
+ [
104
+ (x_i * chunked_loss_mask).sum()
105
+ for x_i, chunked_loss_mask in zip(
106
+ x.split(cp_chunk_lengths, dim=0), chunked_loss_masks, strict=False
107
+ )
108
+ ]
109
+ )
110
+
111
+ return sum_of_sample_mean if not calculate_per_token_loss else sum_of_token
112
+
113
+
114
+ def all_gather_with_cp(tensor: torch.Tensor, total_length: int, response_length: int) -> torch.Tensor:
115
+ """
116
+ Gather tensors across all ranks in the context parallel group.
117
+ The first dimension of the output tensor will be the `response_length`.
118
+ """
119
+ cp_group = mpu.get_context_parallel_group()
120
+ cp_size = mpu.get_context_parallel_world_size()
121
+
122
+ if cp_size == 1:
123
+ return tensor
124
+
125
+ _, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp(total_length, response_length)
126
+
127
+ prompt_length = total_length - response_length
128
+
129
+ chunk_0 = tensor[: logits_offset[0][1] - logits_offset[0][0]]
130
+ chunk_1 = tensor[logits_offset[0][1] - logits_offset[0][0] :]
131
+ assert chunk_1.shape[0] == logits_offset[1][1] - logits_offset[1][0]
132
+
133
+ def zero(len: int) -> torch.Tensor:
134
+ return torch.zeros(
135
+ [len] + list(tensor.shape[1:]),
136
+ dtype=tensor.dtype,
137
+ device=tensor.device,
138
+ requires_grad=True,
139
+ )
140
+
141
+ # logprob should be within the range of [prompt_length - 1, total_length - 1]
142
+ if chunk_0.shape[0] == 0 and chunk_1.shape[0] == 0:
143
+ # all empty
144
+ full_tensor = zero(response_length)
145
+ elif chunk_0.shape[0] != 0 and chunk_1.shape[0] == 0:
146
+ # only first chunk
147
+ left = zero(logits_offset[0][0] - (prompt_length - 1))
148
+ right = zero(total_length - 1 - logits_offset[0][1])
149
+ full_tensor = torch.cat([left, chunk_0, right], dim=0)
150
+ elif chunk_0.shape[0] == 0 and chunk_1.shape[0] != 0:
151
+ # only second chunk
152
+ left = zero(logits_offset[1][0] - (prompt_length - 1))
153
+ right = zero(total_length - 1 - logits_offset[1][1])
154
+ full_tensor = torch.cat([left, chunk_1, right], dim=0)
155
+ else:
156
+ left = zero(logits_offset[0][0] - (prompt_length - 1))
157
+ mid = zero(logits_offset[1][0] - logits_offset[0][1])
158
+ right = zero(total_length - 1 - logits_offset[1][1])
159
+ full_tensor = torch.cat([left, chunk_0, mid, chunk_1, right], dim=0)
160
+
161
+ assert full_tensor.shape[0] == response_length, f"Expected {response_length}, got {full_tensor.shape}"
162
+ full_tensor = dist.nn.all_reduce(full_tensor, group=cp_group)
163
+ return full_tensor
164
+
165
+
166
+ def slice_with_cp(tokens: torch.Tensor, pad_value: tuple[int, float, Callable]) -> torch.Tensor:
167
+ cp_rank = mpu.get_context_parallel_rank()
168
+ cp_size = mpu.get_context_parallel_world_size()
169
+
170
+ if cp_size == 1:
171
+ return tokens
172
+
173
+ # pad
174
+ chunk_size = (len(tokens) + 2 * cp_size - 1) // (2 * cp_size)
175
+ pad = 2 * cp_size * chunk_size - len(tokens)
176
+ if isinstance(pad_value, Callable):
177
+ pad_func = pad_value
178
+ tokens = pad_func(tokens, pad)
179
+ else:
180
+ # pad on the first dimension
181
+ pad_tuple = (0, 0) * (tokens.dim() - 1) + (0, pad)
182
+ tokens = F.pad(tokens, pad_tuple, value=pad_value)
183
+ # get 2 chunk for thd cp
184
+ start_1, end_1 = chunk_size * cp_rank, chunk_size * (cp_rank + 1)
185
+ start_2, end_2 = chunk_size * (2 * cp_size - cp_rank - 1), chunk_size * (2 * cp_size - cp_rank)
186
+ return torch.cat([tokens[start_1:end_1], tokens[start_2:end_2]])
187
+
188
+
189
+ def slice_log_prob_with_cp(
190
+ log_prob: list[float] | torch.Tensor,
191
+ total_length: int,
192
+ response_length: int,
193
+ ) -> list[float] | torch.Tensor:
194
+ assert len(log_prob) == response_length
195
+
196
+ cp_size = mpu.get_context_parallel_world_size()
197
+
198
+ if cp_size == 1:
199
+ return log_prob
200
+
201
+ prompt_length = total_length - response_length
202
+ _, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp(total_length, response_length)
203
+
204
+ chunk_1 = log_prob[logits_offset[0][0] - (prompt_length - 1) : logits_offset[0][1] - (prompt_length - 1)]
205
+ chunk_2 = log_prob[logits_offset[1][0] - (prompt_length - 1) : logits_offset[1][1] - (prompt_length - 1)]
206
+
207
+ if isinstance(log_prob, list):
208
+ return chunk_1 + chunk_2
209
+ else:
210
+ return torch.cat([chunk_1, chunk_2], dim=0)
slime/backends/megatron_utils/data.py ADDED
@@ -0,0 +1,599 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ from argparse import Namespace
6
+ from collections.abc import Sequence
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.distributed as dist
11
+ import torch.nn.functional as F
12
+ from megatron.core import mpu
13
+ from megatron.core.packed_seq_params import PackedSeqParams
14
+
15
+ from slime.utils import train_metric_utils
16
+ from slime.utils.data import get_minimum_num_micro_batch_size
17
+ from slime.utils.flops_utils import calculate_fwd_flops
18
+ from slime.utils.metric_utils import compute_pass_rate, compute_rollout_step
19
+ from slime.utils.seqlen_balancing import get_seqlen_balanced_partitions
20
+ from slime.utils.types import RolloutBatch
21
+
22
+ from ...utils import tracking_utils
23
+ from .cp_utils import get_sum_of_sample_mean, slice_with_cp
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def get_batch(
29
+ data_iterator: "DataIterator",
30
+ keys: Sequence[str],
31
+ pad_multiplier: int = 128,
32
+ ) -> dict[str, torch.Tensor | PackedSeqParams | list[torch.Tensor] | None]:
33
+ """
34
+ Generate a CP-ready micro-batch with packed sequence parameters.
35
+
36
+ Steps:
37
+ - Fetch raw fields via iterator.
38
+ - Save original token tensors under "unconcat_tokens".
39
+ - Slice tokens into two chunks for Context Parallelism (CP), concatenate, and pad to a configurable multiple.
40
+ - Build cu_seqlens and `PackedSeqParams` with T-H-D layout (T: sequence length, H: attention heads, D: head dimension).
41
+
42
+ Args:
43
+ data_iterator: Iterator providing micro-batch data.
44
+ keys: List of keys to fetch from the iterator.
45
+ pad_multiplier: Multiplier for padding size calculation (default: 128).
46
+
47
+ Returns a dict including:
48
+ - "tokens": torch.LongTensor of shape [1, T_padded] on the current CUDA device
49
+ - "unconcat_tokens": list[torch.LongTensor] for the micro-batch before CP slicing/concat
50
+ - "packed_seq_params": PackedSeqParams with T-H-D settings (cu_seqlens on CUDA, dtype=int)
51
+ Plus any other requested keys forwarded from the iterator.
52
+ """
53
+
54
+ assert "tokens" in keys
55
+ batch = data_iterator.get_next(keys)
56
+
57
+ tokens = batch["tokens"]
58
+ # use 0 as the pad token id should be fine?
59
+ pad_token_id = 0
60
+
61
+ # for cp, we need all tokens to calculate logprob
62
+ batch["unconcat_tokens"] = tokens
63
+
64
+ cp_size = mpu.get_context_parallel_world_size()
65
+ tokens = [slice_with_cp(t, pad_token_id) for t in tokens]
66
+
67
+ cu_seqlens = [0]
68
+ for t in tokens:
69
+ cu_seqlens.append(cu_seqlens[-1] + t.size(0))
70
+
71
+ tokens = torch.cat(tokens)
72
+
73
+ # Always pad to reduce memory fragmentation and maybe make the computation faster
74
+ pad_size = mpu.get_tensor_model_parallel_world_size() * pad_multiplier
75
+ pad = (pad_size - tokens.size(0) % pad_size) % pad_size
76
+ if pad != 0:
77
+ tokens = F.pad(tokens, (0, pad), value=pad_token_id)
78
+ cu_seqlens.append(cu_seqlens[-1] + pad)
79
+
80
+ # thd requires the cu_seqlens to be of the origin length
81
+ cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int).cuda() * cp_size
82
+ max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
83
+
84
+ packed_seq_params = PackedSeqParams(
85
+ cu_seqlens_q=cu_seqlens,
86
+ cu_seqlens_kv=cu_seqlens,
87
+ max_seqlen_q=max_seqlen,
88
+ max_seqlen_kv=max_seqlen,
89
+ qkv_format="thd",
90
+ )
91
+
92
+ tokens = tokens.unsqueeze(0)
93
+ batch["tokens"] = tokens
94
+ batch["packed_seq_params"] = packed_seq_params
95
+
96
+ # loss masks
97
+ loss_masks = []
98
+ for loss_mask, total_length, response_length in zip(
99
+ batch["loss_masks"],
100
+ batch["total_lengths"],
101
+ batch["response_lengths"],
102
+ strict=True,
103
+ ):
104
+ prompt_length = total_length - response_length
105
+ loss_mask = F.pad(loss_mask, (prompt_length - 1, 1), value=0)
106
+ loss_mask = slice_with_cp(loss_mask, 0)
107
+ loss_masks.append(loss_mask)
108
+ loss_masks = torch.cat(loss_masks)
109
+ loss_masks = F.pad(loss_masks, (0, pad), value=0).unsqueeze(0)
110
+ assert loss_masks.shape == tokens.shape, f"loss_masks.shape: {loss_masks.shape}, tokens.shape: {tokens.shape}"
111
+ batch["full_loss_masks"] = loss_masks
112
+
113
+ # Process multimodal training tensors if present
114
+ multimodal_train_inputs = batch.get("multimodal_train_inputs", None)
115
+ if multimodal_train_inputs is not None:
116
+ multimodal_data = {} # key -> concatenated tensor
117
+ multimodal_num_items = {} # key -> list of item counts per sequence
118
+ for mm_input_dict in multimodal_train_inputs:
119
+ if mm_input_dict is not None:
120
+ for key, mm_tensor in mm_input_dict.items():
121
+ if key not in multimodal_data:
122
+ multimodal_data[key] = mm_tensor
123
+ multimodal_num_items[key] = [mm_tensor.size(0)]
124
+ else:
125
+ multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0)
126
+ multimodal_num_items[key].append(mm_tensor.size(0))
127
+ batch["multimodal_train_inputs"] = multimodal_data
128
+ batch["multimodal_num_items"] = multimodal_num_items
129
+
130
+ return batch
131
+
132
+
133
+ def gather_log_data(
134
+ metric_name: str,
135
+ args: Namespace,
136
+ rollout_id: int,
137
+ log_dict: dict[str, float],
138
+ ) -> dict[str, float] | None:
139
+ """
140
+ Gather per-rank metrics, reduce by mean on the DP source rank, and log.
141
+
142
+ Expects `log_dict` to contain plain scalars. The DP source rank prints and
143
+ optionally logs to WandB/TensorBoard with a step derived from `rollout_id` and
144
+ batch sizes. Returns the reduced dict on the DP source rank; returns None on others.
145
+ """
146
+
147
+ if mpu.get_data_parallel_rank(with_context_parallel=True) == 0:
148
+ dp_size = mpu.get_data_parallel_world_size(with_context_parallel=True)
149
+
150
+ gathered_log_dict = [None] * dp_size
151
+ # Not sure if this will be a performance bottleneck.
152
+ dist.gather_object(
153
+ log_dict,
154
+ gathered_log_dict,
155
+ dst=mpu.get_data_parallel_src_rank(with_context_parallel=True),
156
+ group=mpu.get_data_parallel_group_gloo(with_context_parallel=True),
157
+ )
158
+
159
+ reduced_log_dict = {
160
+ f"{metric_name}/{key}": sum([d[key] for d in gathered_log_dict]) / dp_size for key in log_dict
161
+ }
162
+ logger.info(f"{metric_name} {rollout_id}: {reduced_log_dict}")
163
+
164
+ # Calculate step once to avoid duplication
165
+ step = compute_rollout_step(args, rollout_id)
166
+ reduced_log_dict["rollout/step"] = step
167
+ tracking_utils.log(args, reduced_log_dict, step_key="rollout/step")
168
+
169
+ return reduced_log_dict
170
+ else:
171
+ dist.gather_object(
172
+ log_dict,
173
+ None,
174
+ dst=mpu.get_data_parallel_src_rank(with_context_parallel=True),
175
+ group=mpu.get_data_parallel_group_gloo(with_context_parallel=True),
176
+ )
177
+ return None
178
+
179
+
180
+ class DataIterator:
181
+ """Micro-batch iterator over rollout dicts.
182
+
183
+ Supports either fixed contiguous micro-batches or an explicit per-step
184
+ index schedule (for dynamic batch sizing / sequence-length balancing).
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ rollout_data: RolloutBatch,
190
+ micro_batch_size: int | None = None,
191
+ micro_batch_indices: list[list[int]] | None = None,
192
+ ) -> None:
193
+ """Initialize an iterator over `rollout_data`.
194
+
195
+ Args:
196
+ rollout_data: Dict of per-sample fields for the local step.
197
+ micro_batch_size: Fixed contiguous slice size when not using dynamic scheduling.
198
+ micro_batch_indices: Explicit indices per micro-batch when using dynamic balancing.
199
+ Must be mutually exclusive with `micro_batch_size`.
200
+ """
201
+ self.rollout_data = rollout_data
202
+ self.micro_batch_size = micro_batch_size
203
+ self.micro_batch_indices = micro_batch_indices
204
+ assert micro_batch_size is None or micro_batch_indices is None
205
+ self.offset = 0
206
+
207
+ # Keys that are batch-level (not per-sample) and should be passed through as-is
208
+ BATCH_LEVEL_KEYS = set()
209
+
210
+ def get_next(self, keys: Sequence[str]) -> dict[str, list[object] | None]:
211
+ """Return the next micro-batch for the requested keys.
212
+
213
+ - If `micro_batch_indices` is provided, selects rows according to the current
214
+ index list for each requested key.
215
+ - Otherwise, slices a contiguous window of size `micro_batch_size` starting
216
+ at the current offset.
217
+
218
+ Returns a dict mapping each key to a list subset (or None if absent).
219
+ """
220
+ batch = {}
221
+ for key in keys:
222
+ vals = self.rollout_data.get(key, None)
223
+ if vals is None:
224
+ batch[key] = None
225
+ elif key in self.BATCH_LEVEL_KEYS:
226
+ # Batch-level keys are not per-sample, pass through as-is
227
+ batch[key] = vals
228
+ else:
229
+ if self.micro_batch_indices is not None:
230
+ indices = self.micro_batch_indices[self.offset]
231
+ batch[key] = [vals[i] for i in indices]
232
+ else:
233
+ assert self.offset + self.micro_batch_size <= len(
234
+ vals
235
+ ), f"offset: {self.offset}, micro_batch_size: {self.micro_batch_size}, len(vals): {len(vals)}"
236
+ batch[key] = vals[self.offset : self.offset + self.micro_batch_size]
237
+
238
+ if self.micro_batch_indices is not None:
239
+ self.offset += 1
240
+ else:
241
+ self.offset += self.micro_batch_size
242
+ return batch
243
+
244
+ def reset(self) -> "DataIterator":
245
+ """Reset internal offset to the start and return self."""
246
+ self.offset = 0
247
+ return self
248
+
249
+
250
+ def get_data_iterator(
251
+ args: Namespace,
252
+ model: torch.nn.Module | Sequence[torch.nn.Module],
253
+ rollout_data: RolloutBatch,
254
+ ) -> tuple[list[DataIterator], list[int]]:
255
+ """
256
+ Create iterators and a micro-batch schedule for a rollout step.
257
+
258
+ - If `use_dynamic_batch_size` is False, splits into fixed-size contiguous
259
+ micro-batches of `micro_batch_size`.
260
+ - If True, computes the number of micro-batches per local step based on
261
+ `max_tokens_per_gpu` and per-sample lengths, all-reduces to a DP-wide
262
+ maximum, optionally enforces divisibility for Virtual Pipeline Parallelism (VPP), and builds a balanced
263
+ index schedule to equalize token counts across micro-batches.
264
+
265
+ Returns `(data_iterators, num_microbatches)` where:
266
+ - `data_iterators`: list of `DataIterator`, one per VPP stage (size 1 if VPP disabled)
267
+ - `num_microbatches`: list[int], one per local step in the rollout (length = steps)
268
+ """
269
+ dp_size = mpu.get_data_parallel_world_size(with_context_parallel=False)
270
+ dp_group = mpu.get_data_parallel_group()
271
+ vpp_size = mpu.get_virtual_pipeline_model_parallel_world_size()
272
+ if vpp_size is None:
273
+ vpp_size = 1
274
+ if vpp_size > 1:
275
+ from megatron.core.utils import get_model_config
276
+
277
+ config = get_model_config(model[0])
278
+ microbatch_group_size_per_vp_stage = config.microbatch_group_size_per_vp_stage
279
+ cp_size = mpu.get_context_parallel_world_size()
280
+
281
+ num_local_samples = len(rollout_data["total_lengths"])
282
+ num_local_gbs = args.global_batch_size // dp_size
283
+ num_steps_per_rollout = num_local_samples // num_local_gbs
284
+
285
+ def _generate_data_iterator(rollout_data, micro_batch_size, micro_batch_indices=None):
286
+ data_iterator = []
287
+ for _ in range(vpp_size):
288
+ data_iterator.append(DataIterator(rollout_data, micro_batch_size, micro_batch_indices))
289
+ return data_iterator
290
+
291
+ if not args.use_dynamic_batch_size:
292
+ num_microbatches = [num_local_gbs // args.micro_batch_size for _ in range(num_steps_per_rollout)]
293
+ data_iterator = _generate_data_iterator(rollout_data, args.micro_batch_size)
294
+ else:
295
+ assert args.max_tokens_per_gpu is not None
296
+ # calculate the number of mirobatches for each step
297
+ samples = rollout_data["total_lengths"]
298
+ assert len(samples) == num_local_samples
299
+ num_microbatches = []
300
+ for i in range(num_steps_per_rollout):
301
+ start, end = i * num_local_gbs, (i + 1) * num_local_gbs
302
+ num_microbatches.append(
303
+ get_minimum_num_micro_batch_size(samples[start:end], args.max_tokens_per_gpu * cp_size)
304
+ )
305
+
306
+ num_microbatches = torch.tensor(num_microbatches, dtype=torch.int, device=torch.cuda.current_device())
307
+ dist.all_reduce(num_microbatches, op=dist.ReduceOp.MAX, group=dp_group)
308
+
309
+ if vpp_size > 1:
310
+ # vpp requies the number of microbatches to be divisible by vpp_size
311
+ num_microbatches = torch.clamp(
312
+ num_microbatches // microbatch_group_size_per_vp_stage * microbatch_group_size_per_vp_stage,
313
+ min=1,
314
+ )
315
+
316
+ num_microbatches = num_microbatches.tolist()
317
+
318
+ # balance the each micro batch
319
+ samples = rollout_data["total_lengths"]
320
+ # balance the number of mirobatches across steps
321
+ micro_batch_indices = []
322
+ for i, num_mbs in enumerate(num_microbatches):
323
+ start, end = i * num_local_gbs, (i + 1) * num_local_gbs
324
+ samples = rollout_data["total_lengths"][start:end]
325
+ partitions = get_seqlen_balanced_partitions(samples, num_mbs, equal_size=False)
326
+ for j in range(num_mbs):
327
+ for k in range(len(partitions[j])):
328
+ partitions[j][k] += start
329
+ micro_batch_indices.extend(partitions)
330
+
331
+ assert len(set(sum(micro_batch_indices, []))) == num_local_samples
332
+
333
+ data_iterator = _generate_data_iterator(rollout_data, None, micro_batch_indices)
334
+
335
+ return (
336
+ data_iterator,
337
+ num_microbatches,
338
+ )
339
+
340
+
341
+ def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None:
342
+ """
343
+ Summarize rollout fields and log reduced metrics on PP last stage, TP rank 0.
344
+
345
+ - Tensor-valued lists are concatenated and averaged. For token-level metrics
346
+ like log-probs/returns/advantages/values, computes a CP-correct sample mean
347
+ using `loss_masks` and total/response lengths.
348
+ - Non-tensor lists are averaged elementwise.
349
+ - Scalars are converted to Python numbers.
350
+ """
351
+ if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
352
+ cp_size = mpu.get_context_parallel_world_size()
353
+ log_dict = {}
354
+ response_lengths = rollout_data["response_lengths"]
355
+ loss_masks = rollout_data["loss_masks"]
356
+ total_lengths = rollout_data["total_lengths"]
357
+
358
+ for key, val in rollout_data.items():
359
+ if key in [
360
+ "tokens",
361
+ "multimodal_train_inputs",
362
+ "loss_masks",
363
+ "sample_indices",
364
+ "rollout_routed_experts",
365
+ ]:
366
+ continue
367
+ # Skip None values
368
+ if val is None:
369
+ continue
370
+ # Upload per sample mean for each rollout value
371
+ # There are the following assumptions:
372
+ # - Each dp rank has the same number of samples
373
+ if isinstance(val, (list, tuple)):
374
+ # Filter out None entries before processing.
375
+ val = [v for v in val if v is not None]
376
+ if not val:
377
+ continue
378
+ if all(isinstance(v, torch.Tensor) for v in val):
379
+ # NOTE: Here we have to do the clone().detach(), otherwise the tensor will be
380
+ # modified in place and will cause problem for the next rollout.
381
+ val = torch.cat(val).clone().detach()
382
+ if key in ["log_probs", "ref_log_probs", "rollout_log_probs", "returns", "advantages", "values"]:
383
+ sum_of_sample_mean = get_sum_of_sample_mean(total_lengths, response_lengths, loss_masks)
384
+ val = cp_size * sum_of_sample_mean(val) / len(loss_masks)
385
+ else:
386
+ val = val.mean() * cp_size
387
+ else:
388
+ # Mixed Tensor/scalar list.
389
+ # Convert everything to float scalar for logging.
390
+ val = sum(float(v.mean()) if isinstance(v, torch.Tensor) else float(v) for v in val) / len(val)
391
+ elif isinstance(val, torch.Tensor):
392
+ val = val.float().mean()
393
+ else:
394
+ raise ValueError(f"Unsupported type: {type(val)} for key: {key}")
395
+ log_dict[key] = val.item() if isinstance(val, torch.Tensor) else val
396
+
397
+ reduced_log_dict = gather_log_data("rollout", args, rollout_id, log_dict)
398
+ if args.ci_test and reduced_log_dict is not None:
399
+ if (
400
+ rollout_id == 0
401
+ and "rollout/log_probs" in reduced_log_dict
402
+ and "rollout/ref_log_probs" in reduced_log_dict
403
+ ):
404
+ assert reduced_log_dict["rollout/log_probs"] == reduced_log_dict["rollout/ref_log_probs"]
405
+ if "rollout/log_probs" in reduced_log_dict:
406
+ assert -0.5 < reduced_log_dict["rollout/log_probs"] < 0
407
+ if "rollout/entropy" in reduced_log_dict:
408
+ assert 0 < reduced_log_dict["rollout/entropy"] < 0.5
409
+
410
+ if args.log_multi_turn:
411
+ log_multi_turn_data(rollout_id, args, rollout_data)
412
+ if args.log_passrate:
413
+ log_passrate(rollout_id, args, rollout_data)
414
+
415
+ if args.log_correct_samples:
416
+ if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
417
+ cp_size = mpu.get_context_parallel_world_size()
418
+ log_dict = {}
419
+ response_lengths = rollout_data["response_lengths"]
420
+ loss_masks = rollout_data["loss_masks"]
421
+ total_lengths = rollout_data["total_lengths"]
422
+
423
+ def quantile(total_value, n_quantiles, data) -> dict:
424
+ import math
425
+
426
+ assert n_quantiles > 1, f"n_quantiles({n_quantiles}) must be greater than 1."
427
+
428
+ quantiles = [((i + 1) / n_quantiles) for i in range(n_quantiles)]
429
+ cut_points = [total_value * q for q in quantiles]
430
+ cut_points[-1] = total_value
431
+
432
+ count = [0] * n_quantiles
433
+ for d in data:
434
+ for i, point in enumerate(cut_points):
435
+ if d <= point:
436
+ count[i] += 1
437
+ break
438
+
439
+ total = sum(count) + 1e-9
440
+ percentile = [c / total for c in count]
441
+
442
+ percentile = {f"p{min(math.ceil(q*100),100)}": p for q, p in zip(quantiles, percentile, strict=True)}
443
+ return percentile
444
+
445
+ raw_rewards = rollout_data["raw_reward"]
446
+ # Additional metrics for correct cases are calculated separately below.
447
+ correct_response_lengths = []
448
+ correct_total_lengths = []
449
+ correct_loss_masks = []
450
+ correct_entropy = []
451
+ for i, raw_reward in enumerate(raw_rewards):
452
+ if raw_reward == 1:
453
+ correct_response_lengths.append(response_lengths[i])
454
+ correct_total_lengths.append(total_lengths[i])
455
+ correct_loss_masks.append(loss_masks[i])
456
+ correct_entropy.append(-rollout_data["log_probs"][i])
457
+ num_correct_responses = len(correct_total_lengths)
458
+ rollout_data["correct_response_lengths"] = correct_response_lengths
459
+ correct_response_length_percentile = quantile(
460
+ args.rollout_max_response_len, 4, rollout_data["correct_response_lengths"]
461
+ )
462
+ for p, val in correct_response_length_percentile.items():
463
+ rollout_data[f"correct_length/{p}"] = [val] * num_correct_responses
464
+ if len(correct_entropy) > 0:
465
+ sum_of_sample_mean = get_sum_of_sample_mean(
466
+ correct_total_lengths, correct_response_lengths, correct_loss_masks
467
+ )
468
+ correct_entropy = sum_of_sample_mean(torch.cat(correct_entropy, dim=0))
469
+ rollout_data["correct_entropy"] = [correct_entropy.item()] * num_correct_responses
470
+ else:
471
+ rollout_data["correct_entropy"] = [0] * num_correct_responses
472
+
473
+
474
+ def log_multi_turn_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None:
475
+ """
476
+ Log multi-turn auxiliary metrics such as raw/observed response lengths and rounds.
477
+
478
+ Operates only on PP last stage and TP rank 0. Uses GPU tensors when available
479
+ to compute statistics without host transfers.
480
+ """
481
+ if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
482
+ log_dict = {}
483
+ for key, val in rollout_data.items():
484
+ if key == "loss_masks":
485
+ if val: # Check if val is not empty
486
+ device = val[0].device # Get device from first tensor
487
+
488
+ # Vectorized length calculation using torch
489
+ raw_response_lengths = torch.tensor([v.shape[0] for v in val], dtype=torch.float32, device=device)
490
+ log_dict["raw_response_length/response_length_mean"] = raw_response_lengths.mean().item()
491
+ log_dict["raw_response_length/response_length_max"] = raw_response_lengths.max().item()
492
+ log_dict["raw_response_length/response_length_min"] = raw_response_lengths.min().item()
493
+ log_dict["raw_response_length/response_length_clip_ratio"] = (
494
+ (raw_response_lengths >= args.rollout_max_response_len).float().mean().item()
495
+ )
496
+
497
+ # Vectorized sum calculation using torch - stay on GPU
498
+ wo_obs_response_lengths = torch.tensor(
499
+ [v.sum().item() for v in val], dtype=torch.float32, device=device
500
+ )
501
+ log_dict["wo_obs_response_length/response_length_mean"] = wo_obs_response_lengths.mean().item()
502
+ log_dict["wo_obs_response_length/response_length_max"] = wo_obs_response_lengths.max().item()
503
+ log_dict["wo_obs_response_length/response_length_min"] = wo_obs_response_lengths.min().item()
504
+ if key == "round_number":
505
+ # Use numpy for vectorized round number statistics
506
+ round_number_array = np.array(val)
507
+ log_dict["multi_turn_metric/round_number_mean"] = np.mean(round_number_array)
508
+ log_dict["multi_turn_metric/round_number_max"] = np.max(round_number_array)
509
+ log_dict["multi_turn_metric/round_number_min"] = np.min(round_number_array)
510
+ gather_log_data("multi_turn", args, rollout_id, log_dict)
511
+
512
+
513
+ def log_passrate(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None:
514
+ """
515
+ Compute pass@k metrics from `raw_reward` groups and log the results.
516
+
517
+ `raw_reward` is reshaped to `[group_number, group_size]`, then pass@k is
518
+ estimated per problem and averaged.
519
+ """
520
+ if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
521
+ log_dict = {}
522
+ for key, val in rollout_data.items():
523
+ if key != "raw_reward":
524
+ continue
525
+
526
+ log_dict |= compute_pass_rate(
527
+ flat_rewards=val,
528
+ group_size=args.n_samples_per_prompt,
529
+ num_groups=args.rollout_batch_size,
530
+ )
531
+
532
+ gather_log_data("passrate", args, rollout_id, log_dict)
533
+
534
+
535
+ def log_perf_data(rollout_id: int, args: Namespace) -> None:
536
+ train_metric_utils.log_perf_data_raw(
537
+ rollout_id=rollout_id,
538
+ args=args,
539
+ is_primary_rank=(
540
+ mpu.get_tensor_model_parallel_rank() == 0
541
+ and mpu.is_pipeline_last_stage()
542
+ and mpu.get_data_parallel_rank(with_context_parallel=True) == 0
543
+ ),
544
+ compute_total_fwd_flops=lambda seq_lens: calculate_fwd_flops(seqlens=seq_lens, args=args)
545
+ / dist.get_world_size()
546
+ / 1e12,
547
+ )
548
+
549
+
550
+ def sync_actor_critic_data(
551
+ args: Namespace,
552
+ rollout_data: RolloutBatch | None = None,
553
+ group: dist.ProcessGroup | None = None,
554
+ ) -> None:
555
+ """
556
+ Broadcast `values` (from critic) and optionally `log_probs`/`ref_log_probs`
557
+ (from actor) across PP ranks to align data dependencies.
558
+
559
+ - Values are broadcast from src=1.
560
+ - Log-probs and ref-log-probs are broadcast from src=0 when KL is used.
561
+ Updates `rollout_data` in place with the synchronized tensors.
562
+ """
563
+ log_probs_key = "log_probs" if not args.use_rollout_logprobs else "rollout_log_probs"
564
+ values, log_probs, ref_log_probs = map(rollout_data.get, ("values", log_probs_key, "ref_log_probs"))
565
+
566
+ # return when not the pp last stage
567
+ if not values and not log_probs:
568
+ return
569
+
570
+ handles = []
571
+
572
+ if not values:
573
+ values = [torch.empty_like(log_prob) for log_prob in log_probs]
574
+ for value in values:
575
+ handles.append(dist.broadcast(value, src=1, group=group, async_op=True))
576
+
577
+ if args.kl_coef != 0 or args.use_kl_loss:
578
+ if not log_probs:
579
+ log_probs = [torch.empty_like(value) for value in values]
580
+ if not ref_log_probs:
581
+ ref_log_probs = [torch.empty_like(value) for value in values]
582
+ for ref_log_prob, log_prob in zip(ref_log_probs, log_probs, strict=False):
583
+ handles.append(dist.broadcast(log_prob, src=0, group=group, async_op=True))
584
+ handles.append(dist.broadcast(ref_log_prob, src=0, group=group, async_op=True))
585
+
586
+ for handle in handles:
587
+ handle.wait()
588
+
589
+ rollout_data.update(
590
+ {
591
+ k: v
592
+ for k, v in {
593
+ "values": values,
594
+ log_probs_key: log_probs,
595
+ "ref_log_probs": ref_log_probs,
596
+ }.items()
597
+ if v is not None
598
+ }
599
+ )
slime/backends/megatron_utils/initialize.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ import random
6
+
7
+ import numpy as np
8
+ import torch
9
+ from megatron.core import mpu, tensor_parallel
10
+ from megatron.core.config import set_experimental_flag
11
+ from megatron.core.num_microbatches_calculator import init_num_microbatches_calculator
12
+ from megatron.training.global_vars import _build_tokenizer, set_args
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _set_random_seed(
18
+ seed_: int,
19
+ data_parallel_random_init: bool = False,
20
+ te_rng_tracker: bool = False,
21
+ inference_rng_tracker: bool = False,
22
+ use_cudagraphable_rng: bool = False,
23
+ ):
24
+ """Set random seed for reproducability."""
25
+ # Ensure that different pipeline MP stages get different seeds.
26
+ seed = seed_ + (100 * mpu.get_pipeline_model_parallel_rank())
27
+ # Ensure different data parallel ranks get different seeds
28
+ if data_parallel_random_init:
29
+ seed = seed + (10 * mpu.get_data_parallel_rank(with_context_parallel=False))
30
+ random.seed(seed)
31
+ np.random.seed(seed)
32
+ torch.manual_seed(seed)
33
+ tensor_parallel.model_parallel_cuda_manual_seed(seed, te_rng_tracker, inference_rng_tracker, use_cudagraphable_rng)
34
+
35
+
36
+ def _initialize_distributed(args, get_embedding_ranks=None, get_position_embedding_ranks=None):
37
+ """Initialize torch.distributed and core model parallel."""
38
+ # Set the tensor model-parallel, pipeline model-parallel, and
39
+ # data-parallel communicators.
40
+ mpu.initialize_model_parallel(
41
+ args.tensor_model_parallel_size,
42
+ args.pipeline_model_parallel_size,
43
+ args.virtual_pipeline_model_parallel_size,
44
+ pipeline_model_parallel_comm_backend=args.pipeline_model_parallel_comm_backend,
45
+ context_parallel_size=args.context_parallel_size,
46
+ hierarchical_context_parallel_sizes=args.hierarchical_context_parallel_sizes,
47
+ expert_model_parallel_size=args.expert_model_parallel_size,
48
+ num_distributed_optimizer_instances=args.num_distributed_optimizer_instances,
49
+ expert_tensor_parallel_size=args.expert_tensor_parallel_size,
50
+ distributed_timeout_minutes=args.distributed_timeout_minutes,
51
+ nccl_communicator_config_path=args.nccl_communicator_config_path,
52
+ order="tp-cp-ep-dp-pp" if not args.use_tp_pp_dp_mapping else "tp-cp-ep-pp-dp",
53
+ get_embedding_ranks=get_embedding_ranks,
54
+ get_position_embedding_ranks=get_position_embedding_ranks,
55
+ create_gloo_process_groups=args.enable_gloo_process_groups,
56
+ )
57
+
58
+
59
+ def init(args):
60
+ set_args(args)
61
+ if args.enable_experimental:
62
+ logger.info("Enable megatron experimental")
63
+ set_experimental_flag(True)
64
+
65
+ # Pytorch distributed.
66
+ _initialize_distributed(args)
67
+
68
+ # https://github.com/NVIDIA/Megatron-LM/issues/1563
69
+ assert np.__version__.startswith("1."), "Megatron does not support numpy 2.x"
70
+
71
+ # Random seeds for reproducibility.
72
+ if args.rank == 0:
73
+ logger.info(f"> setting random seeds to {args.seed} ...")
74
+ _set_random_seed(
75
+ args.seed,
76
+ args.data_parallel_random_init,
77
+ args.te_rng_tracker,
78
+ args.inference_rng_tracker,
79
+ )
80
+ _build_tokenizer(args)
81
+ # We won't use this. initialize to pass some validation in megatron.
82
+ init_num_microbatches_calculator(
83
+ args.rank,
84
+ args.rampup_batch_size,
85
+ args.global_batch_size,
86
+ args.micro_batch_size,
87
+ args.data_parallel_size,
88
+ args.decrease_batch_size_if_needed,
89
+ )
90
+
91
+ if args.deterministic_mode:
92
+ if args.rank == 0:
93
+ logger.info("> running in deterministic mode")
94
+ torch.backends.cudnn.deterministic = True
95
+ torch.backends.cudnn.benchmark = False
96
+ torch.use_deterministic_algorithms(True, warn_only=False)
97
+
98
+ if args.tp_comm_overlap:
99
+ from megatron.training.initialize import _initialize_tp_communicators
100
+
101
+ _initialize_tp_communicators()
102
+
103
+ if getattr(args, "custom_megatron_init_path", None):
104
+ from slime.utils.misc import load_function
105
+
106
+ custom_init = load_function(args.custom_megatron_init_path)
107
+ custom_init(args)
108
+
109
+
110
+ # TODO shall we use a simpler method to determine which rank to init wandb?
111
+ def is_megatron_main_rank():
112
+ return (
113
+ mpu.get_data_parallel_rank(with_context_parallel=True) == 0
114
+ and mpu.get_tensor_model_parallel_rank() == 0
115
+ and mpu.get_pipeline_model_parallel_rank() == mpu.get_pipeline_model_parallel_world_size() - 1
116
+ )
slime/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <torch/extension.h>
2
+ #include <ATen/cuda/CUDAContext.h>
3
+
4
+ #define FINAL_MASK 0xFFFFFFFF
5
+
6
+ __device__ __host__ __forceinline__
7
+ int ceil_div(int a, int b) {
8
+ return (a + b - 1) / b;
9
+ }
10
+
11
+ __device__ __forceinline__
12
+ float warpReduceMax(float val) {
13
+ #pragma unroll
14
+ for (int mask = 16; mask > 0; mask >>= 1)
15
+ val = fmaxf(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32));
16
+ return val;
17
+ }
18
+
19
+
20
+ __device__ __forceinline__
21
+ float warpReduceMin(float val) {
22
+ #pragma unroll
23
+ for (int mask = 16; mask > 0; mask >>= 1)
24
+ val = fminf(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32));
25
+ return val;
26
+ }
27
+
28
+ // almost all int4 use blocksize = [1, 32]
29
+ template<typename scalar_t>
30
+ __global__
31
+ void int4_quant_1x32_kernel(
32
+ const scalar_t* __restrict__ x,
33
+ scalar_t* __restrict__ out,
34
+ scalar_t* out_scale,
35
+ scalar_t* out_zero,
36
+ const int M, const int N,
37
+ const int stride_xm, const int stride_xn,
38
+ const int stride_om, const int stride_on,
39
+ const int stride_osm, const int stride_osn,
40
+ const int stride_ozm, const int stride_ozn,
41
+ bool sym
42
+ ) {
43
+ constexpr int WARPS_PER_BLOCK = 8;
44
+ const int needed_warps = ceil_div(N, 32);
45
+
46
+ const int tid = threadIdx.x;
47
+ const int warp_id = tid >> 5;
48
+ const int lane_id = tid & 0x1F;
49
+ constexpr float SYM_CONS = 1.0f / 7.0f;
50
+ constexpr float ASYM_CONS = 1.0f / 15.0f;
51
+
52
+ const int row = blockIdx.x;
53
+
54
+ for (int item = warp_id; item < needed_warps; item += WARPS_PER_BLOCK) {
55
+ const int col = item * 32 + lane_id;
56
+ float val = 0.0f;
57
+
58
+ if (col < N) {
59
+ val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
60
+ }
61
+
62
+ float scale = 0.0f;
63
+ float zero = 0.0f;
64
+
65
+ if (sym) {
66
+ float abs_val = fabsf(val);
67
+
68
+ float block_max = warpReduceMax(abs_val);
69
+
70
+ scale = fmaxf(block_max * SYM_CONS, 1e-5f);
71
+
72
+ val = rintf(val / scale);
73
+ } else {
74
+ float block_min = warpReduceMin(val);
75
+ float block_max = warpReduceMax(val);
76
+
77
+ scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f);
78
+ zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f);
79
+
80
+ val = rintf(val / scale) + zero;
81
+ }
82
+
83
+ if (col < N) {
84
+ out[row * stride_om + col * stride_on] = static_cast<scalar_t>(val);
85
+ out_scale[row * stride_osm + item * stride_osn] = static_cast<scalar_t>(scale);
86
+ if(!sym) {
87
+ out_zero[row * stride_ozm + item * stride_ozn] = static_cast<scalar_t>(zero);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ // for some transpose case, blocksize = [32, 1]
94
+ template<typename scalar_t>
95
+ __global__
96
+ void int4_quant_32x1_kernel(
97
+ const scalar_t* __restrict__ x,
98
+ scalar_t* __restrict__ out,
99
+ scalar_t* out_scale,
100
+ scalar_t* out_zero,
101
+ const int M, const int N,
102
+ const int stride_xm, const int stride_xn,
103
+ const int stride_om, const int stride_on,
104
+ const int stride_osm, const int stride_osn,
105
+ const int stride_ozm, const int stride_ozn,
106
+ bool sym
107
+ ) {
108
+ constexpr int WARPS_PER_BLOCK = 8;
109
+ const int start_row = blockIdx.x * 32;
110
+ const int end_row = min((blockIdx.x + 1) * 32, M);
111
+
112
+ const int tid = threadIdx.x;
113
+ const int warp_id = tid >> 5;
114
+ const int lane_id = tid & 0x1F;
115
+ constexpr float SYM_CONS = 1.0f / 7.0f;
116
+ constexpr float ASYM_CONS = 1.0f / 15.0f;
117
+
118
+ for (int item = warp_id; item < N; item += WARPS_PER_BLOCK) {
119
+ const int col = item;
120
+ const int row = start_row + lane_id;
121
+
122
+ float val = 0.0f;
123
+
124
+ if (row < end_row) {
125
+ val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
126
+ }
127
+
128
+ float scale = 0.0f;
129
+ float zero = 0.0f;
130
+
131
+ if (sym) {
132
+ float abs_val = fabsf(val);
133
+
134
+ float block_max = warpReduceMax(abs_val);
135
+
136
+ scale = fmaxf(block_max * SYM_CONS, 1e-5f);
137
+
138
+ val = rintf(val / scale);
139
+ } else {
140
+ float block_min = warpReduceMin(val);
141
+ float block_max = warpReduceMax(val);
142
+
143
+ scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f);
144
+ zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f);
145
+
146
+ val = rintf(val / scale) + zero;
147
+ }
148
+
149
+ if (row < end_row) {
150
+ out[row * stride_om + col * stride_on] = static_cast<scalar_t>(val);
151
+ out_scale[blockIdx.x * stride_osm + item * stride_osn] = static_cast<scalar_t>(scale);
152
+ if (!sym) {
153
+ out_zero[blockIdx.x * stride_ozm + item * stride_ozn] = static_cast<scalar_t>(zero);
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ template<typename scalar_t>
160
+ __global__ void int4_quant_common_kernel(
161
+ const scalar_t* __restrict__ x,
162
+ scalar_t* __restrict__ out,
163
+ scalar_t* out_scale,
164
+ scalar_t* out_zero,
165
+ const int M, const int N,
166
+ const int stride_xm, const int stride_xn,
167
+ const int stride_om, const int stride_on,
168
+ const int stride_osm, const int stride_osn,
169
+ const int stride_ozm, const int stride_ozn,
170
+ const int BLOCK_M, const int BLOCK_N,
171
+ bool sym
172
+ ) {
173
+ const int start_row = blockIdx.x * BLOCK_M;
174
+ const int WARPS_PER_BLOCK = blockDim.x >> 5;
175
+
176
+ const int warp_id = threadIdx.x >> 5;
177
+ const int lane_id = threadIdx.x & 0x1F;
178
+ constexpr float SYM_CONS = 1.0f / 7.0f;
179
+ constexpr float ASYM_CONS = 1.0f / 15.0f;
180
+ constexpr int WARP_SIZE = 32;
181
+
182
+ const int needed_warps = ceil_div(N, BLOCK_N);
183
+ const int iters = ceil_div(BLOCK_M * BLOCK_N, 32);
184
+ int warp_rows = 1;
185
+
186
+ if (BLOCK_N <= WARP_SIZE) {
187
+ warp_rows = WARP_SIZE / BLOCK_N;
188
+ }
189
+
190
+ for (int item = warp_id; item < needed_warps; item += WARPS_PER_BLOCK) {
191
+ float local_max = -INFINITY;
192
+ float local_min = INFINITY;
193
+
194
+ float val = 0.0f;
195
+ float scale, zero = 0.0f;
196
+
197
+ const int row_off = lane_id / BLOCK_N;
198
+ const int col_off = lane_id % BLOCK_N;
199
+ int row, col = 0;
200
+
201
+ for (int i = 0; i < iters; ++i) {
202
+ if (BLOCK_N <= WARP_SIZE) {
203
+ row = start_row + i * warp_rows + row_off;
204
+ col = item * BLOCK_N + col_off;
205
+ } else {
206
+ row = start_row;
207
+ col = item * BLOCK_N + i * WARP_SIZE + col_off;
208
+ }
209
+
210
+ if (row < M && col < N) {
211
+ val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
212
+ } else {
213
+ val = 0.0f;
214
+ }
215
+
216
+ if (sym) {
217
+ local_max = fmaxf(local_max, fabsf(val));
218
+ } else {
219
+ local_max = fmaxf(local_max, val);
220
+ local_min = fminf(local_min, val);
221
+ }
222
+ }
223
+
224
+ if (sym) {
225
+ float block_max = warpReduceMax(local_max);
226
+ scale = fmaxf(block_max * SYM_CONS, 1e-5f);
227
+ } else {
228
+ float block_max = warpReduceMax(local_max);
229
+ float block_min = warpReduceMin(local_min);
230
+ scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f);
231
+ zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f);
232
+ }
233
+
234
+ for (int i = 0; i < iters; ++i) {
235
+ if (BLOCK_N <= WARP_SIZE) {
236
+ row = start_row + i * warp_rows + row_off;
237
+ col = item * BLOCK_N + col_off;
238
+ } else {
239
+ row = start_row;
240
+ col = item * BLOCK_N + i * WARP_SIZE + col_off;
241
+ }
242
+
243
+ if (row < M && col < N) {
244
+ float val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
245
+ if (sym) {
246
+ val = rintf(val / scale);
247
+ } else {
248
+ val = rintf(val / scale) + zero;
249
+ }
250
+
251
+ out[row * stride_om + col * stride_on] = static_cast<scalar_t>(val);
252
+ out_scale[blockIdx.x * stride_osm + item * stride_osn] = static_cast<scalar_t>(scale);
253
+ if (!sym) {
254
+ out_zero[blockIdx.x * stride_ozm + item * stride_ozn] = static_cast<scalar_t>(zero);
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // dispatch
262
+ template<typename scalar_t>
263
+ void launch_int4_quant_kernel(
264
+ const scalar_t* x,
265
+ scalar_t* out,
266
+ scalar_t* out_scale,
267
+ scalar_t* out_zero,
268
+ int M, int N,
269
+ const int stride_xm, const int stride_xn,
270
+ const int stride_om, const int stride_on,
271
+ const int stride_osm, const int stride_osn,
272
+ const int stride_ozm, const int stride_ozn,
273
+ int block_m, int block_n,
274
+ bool sym,
275
+ cudaStream_t stream
276
+ ) {
277
+ constexpr int WARPS_PER_BLOCK = 8;
278
+ constexpr int THREADS_PER_BLOCK = WARPS_PER_BLOCK * 32; // 256
279
+
280
+ if (block_m == 1 && block_n == 32) {
281
+ dim3 grid(M);
282
+ dim3 block(THREADS_PER_BLOCK);
283
+
284
+ int4_quant_1x32_kernel<scalar_t><<<grid, block, 0, stream>>>(
285
+ x, out, out_scale, out_zero, M, N,
286
+ stride_xm, stride_xn,
287
+ stride_om, stride_on,
288
+ stride_osm, stride_osn,
289
+ stride_ozm, stride_ozn,
290
+ sym
291
+ );
292
+ } else if (block_m == 32 && block_n == 1) {
293
+ dim3 grid(ceil_div(M, block_m));
294
+ dim3 block(THREADS_PER_BLOCK);
295
+
296
+ int4_quant_32x1_kernel<scalar_t><<<grid, block, 0, stream>>>(
297
+ x, out, out_scale, out_zero, M, N,
298
+ stride_xm, stride_xn,
299
+ stride_om, stride_on,
300
+ stride_osm, stride_osn,
301
+ stride_ozm, stride_ozn,
302
+ sym
303
+ );
304
+ } else {
305
+ dim3 grid(ceil_div(M, block_m));
306
+ dim3 block(THREADS_PER_BLOCK);
307
+ int4_quant_common_kernel<scalar_t><<<grid, block, 0, stream>>>(
308
+ x, out, out_scale, out_zero, M, N,
309
+ stride_xm, stride_xn,
310
+ stride_om, stride_on,
311
+ stride_osm, stride_osn,
312
+ stride_ozm, stride_ozn,
313
+ block_m, block_n,
314
+ sym
315
+ );
316
+ }
317
+ }
318
+
319
+ std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>
320
+ fake_int4_quant_cuda(
321
+ torch::Tensor& x,
322
+ std::vector<int64_t>& block_size,
323
+ bool sym
324
+ ) {
325
+ TORCH_CHECK(x.dim() == 2, "Input must be 2D");
326
+ TORCH_CHECK(x.is_cuda(), "Input must be on CUDA");
327
+
328
+ int M = x.size(0);
329
+ int N = x.size(1);
330
+ int block_m = block_size[0];
331
+ int block_n = block_size[1];
332
+
333
+ TORCH_CHECK(block_m > 0 && block_n > 0, "Block sizes must be positive, got block_m=", block_m, ", block_n=", block_n);
334
+ TORCH_CHECK((block_m * block_n) % 32 == 0,
335
+ "block_m * block_n (", block_m * block_n, ") must be divisible by 32. "
336
+ "But got a ", block_m, "x", block_n, " block.");
337
+
338
+ auto out = torch::empty_like(x);
339
+ auto out_scale = torch::empty({ceil_div(M, block_m), ceil_div(N, block_n)}, x.options());
340
+ auto out_zero = torch::empty_like(out_scale);
341
+
342
+ const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
343
+
344
+ AT_DISPATCH_FLOATING_TYPES_AND(
345
+ at::ScalarType::BFloat16,
346
+ x.scalar_type(), "int4_quant_cuda", [&] {
347
+ launch_int4_quant_kernel<scalar_t>(
348
+ x.const_data_ptr<scalar_t>(),
349
+ out.data_ptr<scalar_t>(),
350
+ out_scale.data_ptr<scalar_t>(),
351
+ out_zero.data_ptr<scalar_t>(),
352
+ M, N,
353
+ x.stride(0), x.stride(1),
354
+ out.stride(0), out.stride(1),
355
+ out_scale.stride(0), out_scale.stride(1),
356
+ out_zero.stride(0), out_zero.stride(1),
357
+ block_m, block_n,
358
+ sym,
359
+ stream
360
+ );
361
+ });
362
+
363
+ return std::make_tuple(out, out_scale, out_zero);
364
+ }
365
+
366
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
367
+ m.def("fake_int4_quant_cuda", &fake_int4_quant_cuda, "fake INT4 quantization cuda");
368
+ }
slime/backends/megatron_utils/kernels/int4_qat/setup.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from setuptools import setup
5
+ from torch.utils.cpp_extension import BuildExtension, CUDAExtension
6
+ import torch
7
+
8
+ # Get CUDA arch list
9
+ arch_list = []
10
+ if torch.cuda.is_available():
11
+ for i in range(torch.cuda.device_count()):
12
+ major, minor = torch.cuda.get_device_capability(i)
13
+ arch_list.append(f"{major}.{minor}")
14
+ arch_list = sorted(set(arch_list))
15
+
16
+ setup(
17
+ name="fake_int4_quant_cuda",
18
+ ext_modules=[
19
+ CUDAExtension(
20
+ name="fake_int4_quant_cuda",
21
+ sources=["fake_int4_quant_cuda.cu"],
22
+ extra_compile_args={
23
+ "cxx": [
24
+ "-O3",
25
+ "-std=c++17",
26
+ ],
27
+ "nvcc": [
28
+ "-O3",
29
+ "-std=c++17",
30
+ "--expt-relaxed-constexpr",
31
+ "-Xcompiler",
32
+ "-fPIC",
33
+ ]
34
+ + [
35
+ f'-gencode=arch=compute_{arch.replace(".", "")},code=sm_{arch.replace(".", "")}'
36
+ for arch in arch_list
37
+ ],
38
+ },
39
+ )
40
+ ],
41
+ cmdclass={"build_ext": BuildExtension},
42
+ )
slime/backends/megatron_utils/loss.py ADDED
@@ -0,0 +1,768 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ from argparse import Namespace
6
+ from collections.abc import Callable, Iterator
7
+ from typing import Any
8
+
9
+ import torch
10
+
11
+ logger = logging.getLogger(__name__)
12
+ from megatron.core import mpu
13
+ from torch.utils.checkpoint import checkpoint
14
+
15
+ from slime.utils.distributed_utils import distributed_masked_whiten
16
+ from slime.utils.misc import load_function
17
+ from slime.utils.ppo_utils import (
18
+ calculate_log_probs_and_entropy,
19
+ compute_approx_kl,
20
+ compute_gspo_kl,
21
+ compute_opsm_mask,
22
+ compute_policy_loss,
23
+ get_advantages_and_returns_batch,
24
+ get_grpo_returns,
25
+ get_reinforce_plus_plus_baseline_advantages,
26
+ get_reinforce_plus_plus_returns,
27
+ )
28
+ from slime.utils.types import RolloutBatch
29
+
30
+ from .cp_utils import all_gather_with_cp, get_logits_and_tokens_offset_with_cp, get_sum_of_sample_mean
31
+
32
+
33
+ def get_responses(
34
+ logits: torch.Tensor,
35
+ *,
36
+ args: Namespace,
37
+ unconcat_tokens: list[torch.Tensor],
38
+ total_lengths: list[int],
39
+ response_lengths: list[int],
40
+ ) -> Iterator[tuple[torch.Tensor, torch.Tensor]]:
41
+ """Yield response-aligned `(logits_chunk, tokens_chunk)` pairs per sample.
42
+
43
+ After squeezing batch dimension and applying temperature scaling, this
44
+ function extracts the logits and tokens corresponding to response segments
45
+ for each sample. When context parallelism is disabled, it slices directly
46
+ from the concatenated sequence. With context parallelism enabled, it
47
+ handles split sequences across ranks.
48
+
49
+ Args:
50
+ logits: Model outputs with shape `[1, T, V]` (policy) or `[1, T, 1]`
51
+ (value). Must be float32.
52
+ args: Configuration containing `rollout_temperature` for scaling.
53
+ unconcat_tokens: List of token tensors (prompt+response) per sample.
54
+ total_lengths: Total sequence lengths (prompt+response) per sample.
55
+ response_lengths: Response segment lengths per sample.
56
+
57
+ Yields:
58
+ Tuple of `(logits_chunk, tokens_chunk)` where `logits_chunk` is shape
59
+ `[R, V]` (policy) or `[R, 1]` (value) and `tokens_chunk` is shape `[R]`
60
+ (1D int64), both aligned to response tokens for one sample.
61
+ """
62
+ assert logits.size(0) == 1, f"{logits.shape}"
63
+ assert logits.dtype == torch.float32, f"{logits.dtype}"
64
+
65
+ logits = logits.squeeze(0)
66
+ logits = logits.div(args.rollout_temperature)
67
+
68
+ cp_size = mpu.get_context_parallel_world_size()
69
+ end = 0
70
+ for tokens, total_length, response_length in zip(unconcat_tokens, total_lengths, response_lengths, strict=False):
71
+ if cp_size == 1:
72
+ end += total_length
73
+ start = end - response_length
74
+ logits_chunk = logits[start - 1 : end - 1]
75
+ tokens_chunk = tokens[-response_length:]
76
+ else:
77
+ # TODO: this is super ugly... do better abstraction.
78
+ chunk_size, chunks_offset, logits_offset, tokens_offset = get_logits_and_tokens_offset_with_cp(
79
+ total_length, response_length
80
+ )
81
+
82
+ logits_0, logits_1 = logits[end : end + chunk_size], logits[end + chunk_size : end + 2 * chunk_size]
83
+ end += 2 * chunk_size
84
+
85
+ logits_0 = logits_0[logits_offset[0][0] - chunks_offset[0][0] : logits_offset[0][1] - chunks_offset[0][0]]
86
+ tokens_0 = tokens[tokens_offset[0][0] : tokens_offset[0][1]]
87
+
88
+ logits_1 = logits_1[logits_offset[1][0] - chunks_offset[1][0] : logits_offset[1][1] - chunks_offset[1][0]]
89
+ tokens_1 = tokens[tokens_offset[1][0] : tokens_offset[1][1]]
90
+
91
+ assert logits_0.size(0) == tokens_0.size(0), f"{logits_0.size(0)} vs {tokens_0.size(0)}"
92
+ assert logits_1.size(0) == tokens_1.size(0), f"{logits_1.size(0)} vs {tokens_1.size(0)}"
93
+
94
+ logits_chunk = torch.cat([logits_0, logits_1], dim=0)
95
+ tokens_chunk = torch.cat([tokens_0, tokens_1], dim=0)
96
+
97
+ yield logits_chunk, tokens_chunk
98
+
99
+
100
+ def get_log_probs_and_entropy(
101
+ logits: torch.Tensor,
102
+ *,
103
+ args: Namespace,
104
+ unconcat_tokens: list[torch.Tensor],
105
+ total_lengths: list[int],
106
+ response_lengths: list[int],
107
+ with_entropy: bool = False,
108
+ non_loss_data: bool = True,
109
+ ) -> dict[str, list[torch.Tensor]]:
110
+ """Compute per-token log-probabilities (and optionally entropy) on responses.
111
+
112
+ For each sample, extracts response-aligned logits and tokens, then computes
113
+ log-probabilities via softmax across the tensor-parallel group. Log-probs
114
+ are squeezed from `[R, 1]` to `[R]`. Entropy values are always appended
115
+ (even when `with_entropy=False`), but only included in the result dict
116
+ when requested.
117
+
118
+ Args:
119
+ logits: Policy logits with shape `[1, T, V]`.
120
+ args: Configuration (temperature applied in `get_responses`).
121
+ unconcat_tokens: List of token tensors per sample.
122
+ total_lengths: Total sequence lengths per sample.
123
+ response_lengths: Response segment lengths per sample.
124
+ with_entropy: If True, include "entropy" key in result.
125
+ non_loss_data: Unused; kept for API compatibility.
126
+
127
+ Returns:
128
+ Dict with key "log_probs" mapping to a list of `[R]` tensors per
129
+ sample. If `with_entropy` is True, also includes "entropy" key with
130
+ a list of `[R]` tensors.
131
+ """
132
+ assert non_loss_data
133
+ log_probs_list = []
134
+ entropy_list = []
135
+ for logits_chunk, tokens_chunk in get_responses(
136
+ logits,
137
+ args=args,
138
+ unconcat_tokens=unconcat_tokens,
139
+ total_lengths=total_lengths,
140
+ response_lengths=response_lengths,
141
+ ):
142
+ log_prob, entropy = calculate_log_probs_and_entropy(
143
+ logits_chunk,
144
+ tokens_chunk,
145
+ mpu.get_tensor_model_parallel_group(),
146
+ with_entropy=with_entropy,
147
+ chunk_size=args.log_probs_chunk_size,
148
+ )
149
+
150
+ log_probs_list.append(log_prob.squeeze(-1))
151
+ entropy_list.append(entropy)
152
+
153
+ res = {
154
+ "log_probs": log_probs_list,
155
+ }
156
+ if with_entropy:
157
+ res["entropy"] = entropy_list
158
+ return res
159
+
160
+
161
+ def get_values(
162
+ logits: torch.Tensor,
163
+ *,
164
+ args: Namespace,
165
+ unconcat_tokens: list[torch.Tensor],
166
+ total_lengths: list[int],
167
+ response_lengths: list[int],
168
+ with_entropy: bool = False,
169
+ non_loss_data: bool = True,
170
+ ) -> dict[str, list[torch.Tensor]]:
171
+ """Extract per-token value predictions over response tokens.
172
+
173
+ For each sample, extracts response-aligned chunks from the value head
174
+ output and squeezes the final dimension from `[R, 1]` to `[R]`.
175
+
176
+ Args:
177
+ logits: Value head output with shape `[1, T, 1]`.
178
+ args: Configuration (passed to `get_responses` which uses
179
+ `rollout_temperature` even though values don't need temperature).
180
+ unconcat_tokens: List of token tensors per sample.
181
+ total_lengths: Total sequence lengths per sample.
182
+ response_lengths: Response segment lengths per sample.
183
+ with_entropy: Unused; kept for signature compatibility.
184
+ non_loss_data: Unused; kept for signature compatibility.
185
+
186
+ Returns:
187
+ Dict with key "values" mapping to a list of `[R]` value tensors
188
+ per sample.
189
+ """
190
+ value_list = []
191
+ for logits_chunk, _ in get_responses(
192
+ logits,
193
+ args=args,
194
+ unconcat_tokens=unconcat_tokens,
195
+ total_lengths=total_lengths,
196
+ response_lengths=response_lengths,
197
+ ):
198
+ assert logits_chunk.size(-1) == 1, f"{logits_chunk.shape}"
199
+ value_list.append(logits_chunk.squeeze(-1))
200
+
201
+ return {
202
+ "values": value_list,
203
+ }
204
+
205
+
206
+ def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) -> None:
207
+ """Compute advantages and returns in-place based on `args.advantage_estimator`.
208
+
209
+ This function extracts rewards, log-probs, values, and masks from
210
+ `rollout_data`, computes KL divergences, then applies the chosen advantage
211
+ estimator. Supported methods: "grpo", "gspo", "ppo", "reinforce_plus_plus",
212
+ and "reinforce_plus_plus_baseline". When `args.normalize_advantages` is
213
+ True, advantages are whitened across the data-parallel group using masked
214
+ statistics.
215
+
216
+ Early returns if both `log_probs` and `values` are None (intermediate
217
+ pipeline stages).
218
+
219
+ Args:
220
+ args: Configuration specifying estimator type, KL coefficient,
221
+ normalization settings, and other hyperparameters.
222
+ rollout_data: Dict containing input lists ("log_probs", "ref_log_probs",
223
+ "rewards", "values", "response_lengths", "loss_masks",
224
+ "total_lengths"). Modified in-place to add "advantages" and
225
+ "returns" keys, each mapping to lists of tensors per sample.
226
+ """
227
+ log_probs: list[torch.Tensor] = rollout_data.get("rollout_log_probs" if args.use_rollout_logprobs else "log_probs")
228
+ ref_log_probs: list[torch.Tensor] = rollout_data.get("ref_log_probs")
229
+ rewards: list[float] = rollout_data.get("rewards")
230
+ values: None | list[torch.Tensor] = rollout_data.get("values")
231
+ response_lengths: list[int] = rollout_data.get("response_lengths")
232
+ loss_masks: list[torch.Tensor] = rollout_data.get("loss_masks")
233
+ total_lengths: list[int] = rollout_data.get("total_lengths")
234
+
235
+ # return when not the last pp stage.
236
+ if log_probs is None and values is None:
237
+ return
238
+
239
+ if args.kl_coef == 0 or not log_probs:
240
+ # when kl_coef is 0, we won't compute ref_log_prob
241
+ xs = log_probs if log_probs is not None else values
242
+ kl = [torch.zeros_like(x, dtype=torch.float32, device=x.device) for x in xs]
243
+ else:
244
+ kl = [
245
+ compute_approx_kl(
246
+ log_probs[i],
247
+ ref_log_probs[i],
248
+ kl_loss_type=args.kl_loss_type,
249
+ )
250
+ for i in range(len(log_probs))
251
+ ]
252
+
253
+ if args.advantage_estimator in ["grpo", "gspo"]:
254
+ rewards = torch.tensor(rewards, dtype=torch.float32, device=kl[0].device)
255
+ returns = get_grpo_returns(rewards, kl)
256
+ # TODO: is the copy necessary?
257
+ advantages = [r for r in returns]
258
+
259
+ elif args.advantage_estimator == "ppo":
260
+ old_rewards = rewards
261
+ rewards = []
262
+ kl_coef = -args.kl_coef
263
+ cp_rank = mpu.get_context_parallel_rank()
264
+ for reward, k in zip(old_rewards, kl, strict=False):
265
+ k *= kl_coef
266
+ if cp_rank == 0:
267
+ k[-1] += reward
268
+ rewards.append(k)
269
+ advantages, returns = get_advantages_and_returns_batch(
270
+ total_lengths, response_lengths, values, rewards, args.gamma, args.lambd
271
+ )
272
+
273
+ elif args.advantage_estimator == "reinforce_plus_plus":
274
+ rewards = torch.tensor(rewards, dtype=torch.float32, device=kl[0].device)
275
+ returns = get_reinforce_plus_plus_returns(
276
+ rewards=rewards,
277
+ kl=kl,
278
+ loss_masks=loss_masks,
279
+ response_lengths=response_lengths,
280
+ total_lengths=total_lengths,
281
+ kl_coef=args.kl_coef,
282
+ gamma=args.gamma,
283
+ )
284
+ advantages = [r for r in returns]
285
+
286
+ elif args.advantage_estimator == "reinforce_plus_plus_baseline":
287
+ rewards = torch.tensor(rewards, dtype=torch.float32, device=kl[0].device)
288
+ advantages = get_reinforce_plus_plus_baseline_advantages(
289
+ rewards=rewards,
290
+ kl=kl,
291
+ loss_masks=loss_masks,
292
+ kl_coef=args.kl_coef,
293
+ )
294
+ returns = advantages
295
+
296
+ elif args.advantage_estimator == "on_policy_distillation":
297
+ student_log_probs = log_probs
298
+ teacher_log_probs = rollout_data.get("teacher_log_probs")
299
+ response_lengths = rollout_data.get("response_lengths")
300
+
301
+ device = student_log_probs[0].device
302
+ teacher_log_probs = [t_log_prob.to(device=device) for t_log_prob in teacher_log_probs]
303
+ teacher_log_probs = [
304
+ t_log_prob[-response_length:]
305
+ for t_log_prob, response_length in zip(teacher_log_probs, response_lengths, strict=False)
306
+ ]
307
+
308
+ advantages = [
309
+ teacher_log_prob - student_log_prob
310
+ for teacher_log_prob, student_log_prob in zip(teacher_log_probs, student_log_probs, strict=False)
311
+ ]
312
+
313
+ returns = advantages
314
+
315
+ else:
316
+ raise NotImplementedError(f"advantage_estimator {args.advantage_estimator} is not supported. ")
317
+
318
+ # TODO: OpenRLHF always does advantages normalization but veRL doesn't seem to do it.
319
+ if args.normalize_advantages:
320
+ all_advs = torch.cat(advantages)
321
+ cp_size = mpu.get_context_parallel_world_size()
322
+ if cp_size == 1:
323
+ all_masks = torch.cat(loss_masks)
324
+ else:
325
+ mask_chunks = []
326
+ for i in range(len(advantages)):
327
+ total_len = total_lengths[i]
328
+ response_len = response_lengths[i]
329
+ prompt_len = total_len - response_len
330
+
331
+ _, _, _, token_offsets = get_logits_and_tokens_offset_with_cp(total_len, response_len)
332
+
333
+ # Convert global offsets to response-space offsets
334
+ s0, e0 = token_offsets[0]
335
+ s1, e1 = token_offsets[1]
336
+ res_s0, res_e0 = max(0, s0 - prompt_len), max(0, e0 - prompt_len)
337
+ res_s1, res_e1 = max(0, s1 - prompt_len), max(0, e1 - prompt_len)
338
+
339
+ local_mask_parts = []
340
+ full_mask = loss_masks[i]
341
+ if res_e0 > res_s0:
342
+ local_mask_parts.append(full_mask[res_s0:res_e0])
343
+ if res_e1 > res_s1:
344
+ local_mask_parts.append(full_mask[res_s1:res_e1])
345
+
346
+ # Concatenate the parts to form the final mask chunk for this rank and this sequence
347
+ local_mask_chunk = (
348
+ torch.cat(local_mask_parts)
349
+ if local_mask_parts
350
+ else torch.tensor([], device=all_advs.device, dtype=full_mask.dtype)
351
+ )
352
+ mask_chunks.append(local_mask_chunk)
353
+
354
+ all_masks = torch.cat(mask_chunks)
355
+
356
+ if all_masks.numel() > 0:
357
+ assert (
358
+ all_advs.size() == all_masks.size()
359
+ ), f"Shape mismatch before whitening: advantages {all_advs.size()}, masks {all_masks.size()}"
360
+ dp_group = mpu.get_data_parallel_group()
361
+
362
+ whitened_advs_flat = distributed_masked_whiten(
363
+ all_advs,
364
+ all_masks,
365
+ process_group=dp_group,
366
+ shift_mean=True,
367
+ )
368
+ chunk_lengths = [chunk.size(0) for chunk in advantages]
369
+ advantages = list(torch.split(whitened_advs_flat, chunk_lengths))
370
+
371
+ rollout_data["advantages"] = advantages
372
+ rollout_data["returns"] = returns
373
+
374
+
375
+ def vanilla_tis_function(
376
+ args,
377
+ *,
378
+ pg_loss: torch.Tensor,
379
+ train_log_probs: list[torch.Tensor],
380
+ rollout_log_probs: list[torch.Tensor],
381
+ loss_masks: list[torch.Tensor],
382
+ **kwargs: Any,
383
+ ) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]:
384
+ rollout_log_probs = torch.cat(rollout_log_probs, dim=0)
385
+ old_log_probs = torch.cat(train_log_probs, dim=0)
386
+ tis = torch.exp(old_log_probs - rollout_log_probs)
387
+ tis_abs = (torch.exp(old_log_probs - rollout_log_probs) - 1).abs()
388
+ tis_weights = torch.clamp(tis, min=args.tis_clip_low, max=args.tis_clip)
389
+ tis_clipfrac = (tis_weights != tis).float()
390
+ metrics = {
391
+ "tis": tis.clone().detach(),
392
+ "tis_clipfrac": tis_clipfrac.clone().detach(),
393
+ "tis_abs": tis_abs.clone().detach(),
394
+ }
395
+ pg_loss = pg_loss * tis_weights
396
+ return pg_loss, loss_masks, metrics
397
+
398
+
399
+ def icepop_function(
400
+ args,
401
+ *,
402
+ pg_loss: torch.Tensor,
403
+ train_log_probs: list[torch.Tensor],
404
+ rollout_log_probs: list[torch.Tensor],
405
+ loss_masks: list[torch.Tensor],
406
+ **kwargs: Any,
407
+ ) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]:
408
+ rollout_log_probs = torch.cat(rollout_log_probs, dim=0)
409
+ old_log_probs = torch.cat(train_log_probs, dim=0)
410
+ ice_ratio = torch.exp(old_log_probs - rollout_log_probs)
411
+ ice_abs = (torch.exp(old_log_probs - rollout_log_probs) - 1).abs()
412
+ ice_weight = torch.where(
413
+ (ice_ratio >= args.tis_clip_low) & (ice_ratio <= args.tis_clip), ice_ratio, torch.zeros_like(ice_ratio)
414
+ )
415
+ ice_clipfrac = (ice_weight != ice_ratio).float()
416
+ metrics = {
417
+ "tis": ice_ratio.clone().detach(),
418
+ "tis_clipfrac": ice_clipfrac.clone().detach(),
419
+ "tis_abs": ice_abs.clone().detach(),
420
+ }
421
+ pg_loss = pg_loss * ice_weight
422
+ return pg_loss, loss_masks, metrics
423
+
424
+
425
+ def policy_loss_function(
426
+ args: Namespace,
427
+ batch: RolloutBatch,
428
+ logits: torch.Tensor,
429
+ sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor],
430
+ ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
431
+ """Compute policy loss (PPO/GSPO) and metrics.
432
+
433
+ Computes current log-probabilities and entropy from model logits, then
434
+ calculates PPO-style clipped policy gradient loss. For GSPO, gathers
435
+ full sequences via context-parallel all-gather before computing per-sample
436
+ KL. Optionally applies TIS (Truncated Importance Sampling) correction and
437
+ adds KL loss term if configured.
438
+
439
+ Args:
440
+ args: Configuration controlling advantage estimator, clipping thresholds,
441
+ entropy/KL coefficients, and TIS settings.
442
+ batch: Mini-batch containing "advantages", "log_probs" (old policy),
443
+ "unconcat_tokens", "response_lengths", "total_lengths", "loss_masks",
444
+ and optionally "ref_log_probs" and "rollout_log_probs".
445
+ logits: Policy logits with shape `[1, T, V]`.
446
+ sum_of_sample_mean: Reduction function that averages per-sample values.
447
+
448
+ Returns:
449
+ Tuple of `(loss, metrics)` where `loss` is a scalar tensor and `metrics`
450
+ is a dict containing detached scalars: "loss", "pg_loss",
451
+ "entropy_loss", "pg_clipfrac", "ppo_kl". Additional keys "kl_loss",
452
+ "tis", "ois", "tis_clipfrac" are included when the respective features
453
+ are enabled.
454
+ """
455
+ advantages = torch.cat(batch["advantages"], dim=0)
456
+ old_log_probs = batch["rollout_log_probs"] if args.use_rollout_logprobs else batch["log_probs"]
457
+
458
+ response_lengths = batch["response_lengths"]
459
+ total_lengths = batch["total_lengths"]
460
+
461
+ log_probs_and_entropy = get_log_probs_and_entropy(
462
+ logits,
463
+ args=args,
464
+ unconcat_tokens=batch["unconcat_tokens"],
465
+ total_lengths=total_lengths,
466
+ response_lengths=response_lengths,
467
+ with_entropy=True,
468
+ )
469
+
470
+ log_probs = log_probs_and_entropy["log_probs"]
471
+
472
+ # Pre-gather log probs if needed by OPSM or GSPO to avoid duplicate gathering
473
+ need_full_log_probs = args.use_opsm or args.advantage_estimator == "gspo"
474
+
475
+ full_log_probs = None
476
+ full_old_log_probs = None
477
+ if need_full_log_probs:
478
+ full_log_probs = [
479
+ all_gather_with_cp(log_prob, total_length, response_length)
480
+ for log_prob, total_length, response_length in zip(
481
+ log_probs, total_lengths, response_lengths, strict=False
482
+ )
483
+ ]
484
+ full_old_log_probs = [
485
+ all_gather_with_cp(old_log_prob, total_length, response_length)
486
+ for old_log_prob, total_length, response_length in zip(
487
+ old_log_probs, total_lengths, response_lengths, strict=False
488
+ )
489
+ ]
490
+
491
+ # Compute OPSM mask if enabled
492
+ if args.use_opsm:
493
+ opsm_mask, opsm_clipfrac = compute_opsm_mask(
494
+ args=args,
495
+ full_log_probs=full_log_probs,
496
+ full_old_log_probs=full_old_log_probs,
497
+ advantages=batch["advantages"],
498
+ loss_masks=batch["loss_masks"],
499
+ )
500
+
501
+ # Compute KL divergence (GSPO uses sequence-level KL, others use per-token KL)
502
+ if args.advantage_estimator == "gspo":
503
+ ppo_kl = compute_gspo_kl(
504
+ full_log_probs=full_log_probs,
505
+ full_old_log_probs=full_old_log_probs,
506
+ local_log_probs=log_probs,
507
+ loss_masks=batch["loss_masks"],
508
+ )
509
+ old_log_probs = torch.cat(old_log_probs, dim=0)
510
+ log_probs = torch.cat(log_probs, dim=0)
511
+ else:
512
+ old_log_probs = torch.cat(old_log_probs, dim=0)
513
+ log_probs = torch.cat(log_probs, dim=0)
514
+ ppo_kl = old_log_probs - log_probs
515
+
516
+ pg_loss, pg_clipfrac = compute_policy_loss(ppo_kl, advantages, args.eps_clip, args.eps_clip_high)
517
+
518
+ if args.use_opsm:
519
+ pg_loss = pg_loss * opsm_mask
520
+
521
+ # Apply off-policy correction using importance sampling if enabled
522
+ if args.get_mismatch_metrics or args.use_tis:
523
+ # NOTE:
524
+ # `tis_func` may apply rejection-sampling style masking (RS) and return `modified_response_masks`.
525
+ # We rebuild `sum_of_sample_mean` with those masks to correct denominators for loss/backprop.
526
+ #
527
+ # However, mismatch/TIS/RS metrics (e.g., "truncate_fraction") are often defined over the
528
+ # *pre-RS* valid tokens. If we aggregate metrics with `modified_response_masks`, the rejected
529
+ # tokens are excluded from the denominator and the metric can be artificially driven to 0.
530
+ # Keep a copy of the original reducer (based on `batch["loss_masks"]`) for metric aggregation.
531
+ sum_of_sample_mean_for_mismatch_metrics = sum_of_sample_mean
532
+
533
+ assert "rollout_log_probs" in batch, "rollout_log_probs must be provided for TIS"
534
+
535
+ ois = (-ppo_kl).exp()
536
+ tis_kwargs = {
537
+ "args": args,
538
+ "pg_loss": pg_loss,
539
+ "train_log_probs": batch["log_probs"],
540
+ "rollout_log_probs": batch["rollout_log_probs"],
541
+ "loss_masks": batch["loss_masks"],
542
+ "total_lengths": total_lengths,
543
+ "response_lengths": response_lengths,
544
+ }
545
+
546
+ if args.custom_tis_function_path is not None:
547
+ tis_func = load_function(args.custom_tis_function_path)
548
+ else:
549
+ tis_func = vanilla_tis_function
550
+ pg_loss, modified_response_masks, tis_metrics = tis_func(**tis_kwargs)
551
+
552
+ # [decouple IS and rejection] Rebuild sum_of_sample_mean with modified_response_masks for denominator correction
553
+ # modified_response_masks will be sliced with cp in get_sum_of_sample_mean
554
+ sum_of_sample_mean = get_sum_of_sample_mean(
555
+ total_lengths, response_lengths, modified_response_masks, args.calculate_per_token_loss
556
+ )
557
+
558
+ pg_loss = sum_of_sample_mean(pg_loss)
559
+ pg_clipfrac = sum_of_sample_mean(pg_clipfrac)
560
+ ppo_kl = sum_of_sample_mean(ppo_kl)
561
+
562
+ # entropy loss
563
+ entropy = log_probs_and_entropy["entropy"]
564
+ entropy = torch.cat(entropy, dim=0)
565
+ entropy_loss = sum_of_sample_mean(entropy)
566
+
567
+ loss = pg_loss - args.entropy_coef * entropy_loss
568
+
569
+ if args.use_kl_loss:
570
+ ref_log_probs = batch["ref_log_probs"]
571
+ ref_log_probs = torch.cat(ref_log_probs, dim=0)
572
+ importance_ratio = None
573
+ if args.use_unbiased_kl:
574
+ importance_ratio = torch.exp(log_probs - old_log_probs)
575
+ kl = compute_approx_kl(
576
+ log_probs,
577
+ ref_log_probs,
578
+ kl_loss_type=args.kl_loss_type,
579
+ importance_ratio=importance_ratio,
580
+ )
581
+ kl_loss = sum_of_sample_mean(kl)
582
+
583
+ loss = loss + args.kl_loss_coef * kl_loss
584
+
585
+ # make sure the gradient could backprop correctly.
586
+ if log_probs.numel() == 0:
587
+ loss += 0 * logits.sum()
588
+
589
+ train_rollout_logprob_abs_diff = None
590
+ importance_weight_mean = None
591
+ importance_weight_std = None
592
+ if "rollout_log_probs" in batch and batch["rollout_log_probs"]:
593
+ rollout_log_probs = torch.cat(batch["rollout_log_probs"], dim=0)
594
+ train_rollout_logprob_abs_diff = sum_of_sample_mean((old_log_probs - rollout_log_probs).abs())
595
+ iw = torch.exp(log_probs.detach() - rollout_log_probs)
596
+ importance_weight_mean = sum_of_sample_mean(iw)
597
+ importance_weight_std = sum_of_sample_mean((iw - 1).pow(2)).sqrt()
598
+
599
+ reported_loss = {
600
+ "loss": loss.clone().detach(),
601
+ "pg_loss": pg_loss.clone().detach(),
602
+ "entropy_loss": entropy_loss.clone().detach(),
603
+ "pg_clipfrac": pg_clipfrac.clone().detach(),
604
+ "ppo_kl": ppo_kl.clone().detach(),
605
+ }
606
+
607
+ if train_rollout_logprob_abs_diff is not None:
608
+ reported_loss["train_rollout_logprob_abs_diff"] = train_rollout_logprob_abs_diff.clone().detach()
609
+ if importance_weight_mean is not None:
610
+ reported_loss["importance_weight_mean"] = importance_weight_mean.clone().detach()
611
+ reported_loss["importance_weight_std"] = importance_weight_std.clone().detach()
612
+
613
+ if args.use_kl_loss:
614
+ reported_loss["kl_loss"] = kl_loss.clone().detach()
615
+
616
+ if args.get_mismatch_metrics or args.use_tis:
617
+ # Aggregate mismatch/TIS/RS related metrics with the *pre-RS* masks.
618
+ # See comment above where `sum_of_sample_mean_for_mismatch_metrics` is defined.
619
+ reported_loss["ois"] = sum_of_sample_mean_for_mismatch_metrics(ois).clone().detach()
620
+ # Assume all metrics are already cloned and detached
621
+ for metric_key, metric_value in tis_metrics.items():
622
+ key_name = f"{metric_key}"
623
+ reported_loss[key_name] = sum_of_sample_mean_for_mismatch_metrics(metric_value)
624
+
625
+ if args.use_opsm:
626
+ reported_loss["opsm_clipfrac"] = opsm_clipfrac
627
+
628
+ return loss, reported_loss
629
+
630
+
631
+ def value_loss_function(
632
+ args: Namespace,
633
+ batch: RolloutBatch,
634
+ logits: torch.Tensor,
635
+ sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor],
636
+ ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
637
+ """Compute clipped value loss and metrics.
638
+
639
+ Extracts current value predictions from `logits`, compares them against
640
+ stored old values with clipping, and computes the maximum of clipped and
641
+ unclipped squared errors (PPO-style value clipping).
642
+
643
+ Args:
644
+ args: Configuration containing `value_clip` threshold.
645
+ batch: Mini-batch with "values" (old predictions), "returns",
646
+ "unconcat_tokens", "total_lengths", and "response_lengths".
647
+ logits: Value head output with shape `[1, T, 1]`.
648
+ sum_of_sample_mean: Reduction function that averages per-sample values.
649
+
650
+ Returns:
651
+ Tuple of `(loss, metrics)` where `loss` is a scalar tensor and
652
+ `metrics` contains detached scalars "value_loss" and "value_clipfrac".
653
+ """
654
+ old_values = torch.cat(batch["values"], dim=0)
655
+
656
+ values = get_values(
657
+ logits,
658
+ args=args,
659
+ unconcat_tokens=batch["unconcat_tokens"],
660
+ total_lengths=batch["total_lengths"],
661
+ response_lengths=batch["response_lengths"],
662
+ )
663
+ values = torch.cat([value.flatten() for value in values["values"]], dim=0)
664
+
665
+ returns = torch.cat(batch["returns"], dim=0)
666
+
667
+ values_clipfrac = torch.abs(values - old_values) > args.value_clip
668
+ values_clipped = old_values + (values - old_values).clamp(-args.value_clip, args.value_clip)
669
+ surr1 = (values_clipped - returns) ** 2
670
+ surr2 = (values - returns) ** 2
671
+ loss = torch.max(surr1, surr2)
672
+
673
+ loss = sum_of_sample_mean(loss)
674
+ values_clipfrac = sum_of_sample_mean(values_clipfrac.float())
675
+
676
+ # make sure the gradient could backprop correctly.
677
+ if values.numel() == 0:
678
+ loss += 0 * values.sum()
679
+
680
+ reported_loss = {
681
+ "value_loss": loss.clone().detach(),
682
+ "value_clipfrac": values_clipfrac.clone().detach(),
683
+ }
684
+
685
+ return loss, reported_loss
686
+
687
+
688
+ def loss_function(
689
+ args: Namespace,
690
+ batch: RolloutBatch,
691
+ num_microbatches: int,
692
+ logits: torch.Tensor,
693
+ ) -> tuple[torch.Tensor, int | torch.Tensor, dict[str, list[str] | torch.Tensor]]:
694
+ """Dispatch to the configured loss and rescale for Megatron integration.
695
+
696
+ Selects one of "policy_loss", "value_loss", or a custom loss
697
+ function based on `args.loss_type`, computes the loss and metrics, then
698
+ rescales the loss by micro-batch and parallelism factors to integrate with
699
+ Megatron's gradient accumulation.
700
+
701
+ Args:
702
+ args: Configuration specifying `loss_type`, `calculate_per_token_loss`,
703
+ `global_batch_size`, and optionally `custom_loss_function_path`.
704
+ batch: Mini-batch with "loss_masks", "response_lengths", and other
705
+ keys required by the selected loss function.
706
+ num_microbatches: Number of gradient accumulation steps.
707
+ logits: Model outputs (policy or value head).
708
+
709
+ Returns:
710
+ Tuple of `(scaled_loss, normalizer, logging_dict)` where:
711
+ - `scaled_loss` is the loss tensor (scalar) rescaled for Megatron.
712
+ - `normalizer` is `num_tokens` (scalar tensor) if
713
+ `args.calculate_per_token_loss` is True, else `1` (int).
714
+ - `logging_dict` has keys "keys" (list of str metric names) and
715
+ "values" (1D tensor: [count, metric1, metric2, ...]).
716
+ """
717
+ num_tokens = sum([torch.clamp_min(loss_mask.sum(), 1) for loss_mask in batch["loss_masks"]])
718
+ num_samples = len(batch["response_lengths"])
719
+
720
+ sum_of_sample_mean = get_sum_of_sample_mean(
721
+ batch["total_lengths"],
722
+ batch["response_lengths"],
723
+ batch["loss_masks"],
724
+ args.calculate_per_token_loss,
725
+ )
726
+
727
+ loss_type = args.loss_type
728
+
729
+ match loss_type:
730
+ case "policy_loss":
731
+ func = policy_loss_function
732
+ case "value_loss":
733
+ func = value_loss_function
734
+ case "custom_loss":
735
+ func = load_function(args.custom_loss_function_path)
736
+ case _:
737
+ raise ValueError(f"Unknown loss type: {loss_type}")
738
+
739
+ if args.recompute_loss_function:
740
+ loss, log = checkpoint(func, args, batch, logits, sum_of_sample_mean)
741
+ else:
742
+ loss, log = func(args, batch, logits, sum_of_sample_mean)
743
+
744
+ # Here we need to divide by cp_size because to cancel the multiply in Megatron.
745
+ if not args.calculate_per_token_loss:
746
+ loss = (
747
+ loss
748
+ * num_microbatches
749
+ / args.global_batch_size
750
+ * mpu.get_data_parallel_world_size(with_context_parallel=True)
751
+ )
752
+ else:
753
+ loss = loss * mpu.get_context_parallel_world_size()
754
+
755
+ return (
756
+ loss,
757
+ torch.tensor(num_tokens if args.calculate_per_token_loss else 1, device=logits.device),
758
+ {
759
+ "keys": list(log.keys()),
760
+ "values": torch.tensor(
761
+ [
762
+ num_samples if not args.calculate_per_token_loss else num_tokens,
763
+ ]
764
+ + list(log.values()),
765
+ device=logits.device,
766
+ ),
767
+ },
768
+ )
slime/backends/megatron_utils/megatron_to_hf/__init__.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .deepseekv3 import convert_deepseekv3_to_hf
5
+ from .glm4 import convert_glm4_to_hf
6
+ from .glm4moe import convert_glm4moe_to_hf
7
+ from .llama import convert_llama_to_hf
8
+ from .mimo import convert_mimo_to_hf
9
+ from .processors.padding_remover import remove_padding
10
+ from .processors.quantizer import quantize_params
11
+ from .qwen2 import convert_qwen2_to_hf
12
+ from .qwen3_next import convert_qwen3_next_to_hf
13
+ from .qwen3moe import convert_qwen3moe_to_hf
14
+
15
+
16
+ # TODO unify w/ `convert_to_hf`
17
+ def postprocess_hf_param(args, megatron_param_name, hf_param_name, param):
18
+ param = remove_padding(megatron_param_name, param, args.vocab_size)
19
+ # TODO support quant
20
+ return param
21
+
22
+
23
+ # TODO optimize code details
24
+ def convert_to_hf(args, model_name, name, param, quantization_config=None):
25
+ param = remove_padding(name, param, args.vocab_size)
26
+
27
+ converted_named_tensors = _convert_to_hf_core(args, model_name, name, param)
28
+
29
+ if not quantization_config:
30
+ return converted_named_tensors
31
+
32
+ return quantize_params(args, name, converted_named_tensors, quantization_config)
33
+
34
+
35
+ # TODO optimize
36
+ _cached_tensors = {}
37
+
38
+
39
+ # TODO optimize code details
40
+ def _convert_to_hf_core(args, model_name, name, param):
41
+ if "glm4moe" in model_name:
42
+ converted_named_tensors = convert_glm4moe_to_hf(args, name, param)
43
+ elif "glm4" in model_name:
44
+ converted_named_tensors = convert_glm4_to_hf(args, name, param)
45
+ elif "qwen3moe" in model_name:
46
+ converted_named_tensors = convert_qwen3moe_to_hf(args, name, param)
47
+ elif "qwen3next" in model_name:
48
+ converted_named_tensors = convert_qwen3_next_to_hf(args, name, param)
49
+ elif "qwen2" in model_name or "qwen3" in model_name:
50
+ converted_named_tensors = convert_qwen2_to_hf(args, name, param)
51
+ elif "deepseekv3" in model_name:
52
+ converted_named_tensors = convert_deepseekv3_to_hf(args, name, param)
53
+
54
+ elif "llama" in model_name:
55
+ converted_named_tensors = convert_llama_to_hf(args, name, param)
56
+ elif "mimo" in model_name:
57
+ converted_named_tensors = convert_mimo_to_hf(args, name, param)
58
+ else:
59
+ raise ValueError(f"Unsupported model: {model_name}")
60
+
61
+ # to compatible with sglang implementation
62
+ if args.q_lora_rank is not None:
63
+ old_converted_named_tensors = converted_named_tensors
64
+ converted_named_tensors = []
65
+ for converted_name, converted_param in old_converted_named_tensors:
66
+ if "q_a_proj" in converted_name:
67
+ pair_name = converted_name.replace("q_a_proj", "kv_a_proj_with_mqa")
68
+ if pair_name in _cached_tensors:
69
+ converted_named_tensors += [
70
+ (converted_name, converted_param),
71
+ (pair_name, _cached_tensors[pair_name]),
72
+ ]
73
+ del _cached_tensors[pair_name]
74
+ else:
75
+ _cached_tensors[converted_name] = converted_param
76
+ elif "kv_a_proj_with_mqa" in converted_name:
77
+ pair_name = converted_name.replace("kv_a_proj_with_mqa", "q_a_proj")
78
+ if pair_name in _cached_tensors:
79
+ converted_named_tensors += [
80
+ (converted_name, converted_param),
81
+ (pair_name, _cached_tensors[pair_name]),
82
+ ]
83
+ del _cached_tensors[pair_name]
84
+ else:
85
+ _cached_tensors[converted_name] = converted_param
86
+ else:
87
+ converted_named_tensors.append((converted_name, converted_param))
88
+ return converted_named_tensors
slime/backends/megatron_utils/megatron_to_hf/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (2.94 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/deepseekv3.cpython-312.pyc ADDED
Binary file (6.52 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/glm4.cpython-312.pyc ADDED
Binary file (4.09 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/glm4moe.cpython-312.pyc ADDED
Binary file (6.74 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/llama.cpython-312.pyc ADDED
Binary file (3.13 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/mimo.cpython-312.pyc ADDED
Binary file (2.73 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen2.cpython-312.pyc ADDED
Binary file (4.01 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen3_next.cpython-312.pyc ADDED
Binary file (6.52 kB). View file
 
slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen3moe.cpython-312.pyc ADDED
Binary file (5.73 kB). View file