Text Generation
Transformers
Safetensors
qwen3
llama-factory
full
Generated from Trainer
conversational
text-generation-inference
Instructions to use ayh015/myLightningOPD with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ayh015/myLightningOPD with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="ayh015/myLightningOPD") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("ayh015/myLightningOPD") model = AutoModelForCausalLM.from_pretrained("ayh015/myLightningOPD", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ayh015/myLightningOPD with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ayh015/myLightningOPD" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/ayh015/myLightningOPD
- SGLang
How to use ayh015/myLightningOPD with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "ayh015/myLightningOPD" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "ayh015/myLightningOPD" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use ayh015/myLightningOPD with Docker Model Runner:
docker model run hf.co/ayh015/myLightningOPD
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- slime/__init__.py +3 -0
- slime/__pycache__/__init__.cpython-312.pyc +0 -0
- slime/backends/__init__.py +4 -0
- slime/backends/__pycache__/__init__.cpython-312.pyc +0 -0
- slime/backends/fsdp_utils/__init__.py +36 -0
- slime/backends/fsdp_utils/actor.py +1145 -0
- slime/backends/fsdp_utils/arguments.py +101 -0
- slime/backends/fsdp_utils/checkpoint.py +252 -0
- slime/backends/fsdp_utils/data_packing.py +221 -0
- slime/backends/fsdp_utils/kernels/__init__.py +3 -0
- slime/backends/fsdp_utils/kernels/fused_experts.py +384 -0
- slime/backends/fsdp_utils/kernels/fused_moe_triton_backward_kernels.py +543 -0
- slime/backends/fsdp_utils/lr_scheduler.py +197 -0
- slime/backends/fsdp_utils/models/__init__.py +3 -0
- slime/backends/fsdp_utils/models/qwen3_moe.py +128 -0
- slime/backends/fsdp_utils/models/qwen3_moe_hf.py +46 -0
- slime/backends/fsdp_utils/update_weight_utils.py +261 -0
- slime/backends/megatron_utils/__init__.py +45 -0
- slime/backends/megatron_utils/__pycache__/__init__.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/actor.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/arguments.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/checkpoint.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/cp_utils.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/data.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/initialize.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/loss.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/misc_utils.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/model.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/model_provider.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/__pycache__/sglang.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/actor.py +575 -0
- slime/backends/megatron_utils/arguments.py +33 -0
- slime/backends/megatron_utils/checkpoint.py +79 -0
- slime/backends/megatron_utils/ci_utils.py +87 -0
- slime/backends/megatron_utils/cp_utils.py +210 -0
- slime/backends/megatron_utils/data.py +599 -0
- slime/backends/megatron_utils/initialize.py +116 -0
- slime/backends/megatron_utils/kernels/int4_qat/fake_int4_quant_cuda.cu +368 -0
- slime/backends/megatron_utils/kernels/int4_qat/setup.py +42 -0
- slime/backends/megatron_utils/loss.py +768 -0
- slime/backends/megatron_utils/megatron_to_hf/__init__.py +88 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/__init__.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/deepseekv3.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/glm4.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/glm4moe.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/llama.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/mimo.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen2.cpython-312.pyc +0 -0
- slime/backends/megatron_utils/megatron_to_hf/__pycache__/qwen3_next.cpython-312.pyc +0 -0
- 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
|
|
|