初始化项目,由ModelHub XC社区提供模型
Model: ayh015/myLightningOPD Source: Original Platform
This commit is contained in:
4
slime/backends/__init__.py
Normal file
4
slime/backends/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
||||
BIN
slime/backends/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
slime/backends/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
36
slime/backends/fsdp_utils/__init__.py
Normal file
36
slime/backends/fsdp_utils/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
try:
|
||||
_TORCH_MEMORY_SAVER_AVAILABLE = True
|
||||
except ImportError:
|
||||
logging.warning("torch_memory_saver is not installed, refer to : https://github.com/fzyzcjy/torch_memory_saver")
|
||||
_TORCH_MEMORY_SAVER_AVAILABLE = False
|
||||
|
||||
try:
|
||||
_FSDP_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
logging.warning(f"FSDP backend dependencies not available: {e}")
|
||||
_FSDP_AVAILABLE = False
|
||||
|
||||
if _FSDP_AVAILABLE:
|
||||
from .actor import FSDPTrainRayActor
|
||||
from .arguments import load_fsdp_args
|
||||
else:
|
||||
|
||||
def _raise_import_error(*args, **kwargs):
|
||||
raise ImportError(
|
||||
"FSDP backend is not available. "
|
||||
"Please ensure PyTorch with FSDP2 support is installed. "
|
||||
"For installation instructions, refer to: https://pytorch.org/docs/stable/distributed.fsdp.fully_shard.html"
|
||||
)
|
||||
|
||||
FSDPTrainRayActor = _raise_import_error
|
||||
load_fsdp_args = _raise_import_error
|
||||
|
||||
__all__ = ["load_fsdp_args", "FSDPTrainRayActor"]
|
||||
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
1145
slime/backends/fsdp_utils/actor.py
Normal file
1145
slime/backends/fsdp_utils/actor.py
Normal file
File diff suppressed because it is too large
Load Diff
101
slime/backends/fsdp_utils/arguments.py
Normal file
101
slime/backends/fsdp_utils/arguments.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSDPArgs:
|
||||
# Optim
|
||||
optimizer: str = "adam" # Optimizer type: "adam" (AdamW)
|
||||
lr: float = 2e-5
|
||||
lr_warmup_init: float = 0.0
|
||||
min_lr: float = 0.0
|
||||
lr_decay_style: str = "constant"
|
||||
lr_decay_iters: int | None = None
|
||||
lr_warmup_iters: int = 0
|
||||
lr_warmup_fraction: float | None = None
|
||||
lr_wsd_decay_iters: int | None = None
|
||||
lr_wsd_decay_style: str | None = None
|
||||
use_checkpoint_lr_scheduler: bool = True
|
||||
override_lr_scheduler: bool = False
|
||||
weight_decay: float = 0.0
|
||||
adam_beta1: float = 0.9
|
||||
adam_beta2: float = 0.95
|
||||
adam_eps: float = 1e-8
|
||||
warmup_ratio: float = 0.03
|
||||
|
||||
attn_implementation: str = "flash_attention_2"
|
||||
|
||||
# Logging
|
||||
wandb_project: str = "slime-fsdp"
|
||||
wandb_run_name: str | None = None
|
||||
|
||||
# Precision
|
||||
gradient_checkpointing: bool = False
|
||||
fp16: bool = False
|
||||
|
||||
# FSDP configuration
|
||||
fsdp_state_dict_cpu_offload: bool = True # If True, offload full state dict to CPU during collection.
|
||||
fsdp_cpu_offload: bool = (
|
||||
False # If True, offload parameters, gradients, and optimizer states to CPU (optimizer runs on CPU)
|
||||
)
|
||||
fsdp_cpu_backend: str | None = (
|
||||
"gloo" # CPU backend for FSDP CPU offload (e.g., "gloo"). Set to None to disable hybrid backend.
|
||||
)
|
||||
|
||||
deterministic_mode: bool = False # This name must be the same as Megatron's
|
||||
|
||||
# Context Parallelism
|
||||
context_parallel_size: int = 1 # Context Parallelism size
|
||||
# Profile
|
||||
record_memory_history: bool = False
|
||||
memory_snapshot_path: str = "snapshot.pickle"
|
||||
use_pytorch_profiler: bool = False
|
||||
profile_step_start: int = 10
|
||||
profile_step_end: int = 12
|
||||
tensorboard_dir: str | None = None
|
||||
|
||||
# YAML bookkeeping
|
||||
config: str | None = None
|
||||
|
||||
|
||||
def parse_fsdp_cli(extra_args_provider=None):
|
||||
parser = argparse.ArgumentParser("FSDP Training (slime)")
|
||||
parser.add_argument("--config", type=str, default=None, help="YAML config path")
|
||||
for f in dataclasses.fields(FSDPArgs):
|
||||
if f.name == "config":
|
||||
continue
|
||||
|
||||
# Handle union types like int | None, str | None, etc.
|
||||
if hasattr(f.type, "__args__"): # Check if it's a Union type
|
||||
# For T | None, use T as the type
|
||||
non_none_types = [t for t in f.type.__args__ if t is not type(None)]
|
||||
arg_type = non_none_types[0] if non_none_types else str
|
||||
else:
|
||||
arg_type = f.type
|
||||
|
||||
if arg_type is bool:
|
||||
parser.add_argument(f"--{f.name.replace('_', '-')}", action="store_true")
|
||||
else:
|
||||
parser.add_argument(f"--{f.name.replace('_', '-')}", type=arg_type, default=f.default)
|
||||
|
||||
if extra_args_provider is not None:
|
||||
parser = extra_args_provider(parser)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def load_fsdp_args(extra_args_provider=None):
|
||||
args = parse_fsdp_cli(extra_args_provider)
|
||||
if args.config:
|
||||
with open(args.config) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
for k, v in data.items():
|
||||
if not hasattr(args, k):
|
||||
setattr(args, k, v)
|
||||
return args
|
||||
252
slime/backends/fsdp_utils/checkpoint.py
Normal file
252
slime/backends/fsdp_utils/checkpoint.py
Normal file
@@ -0,0 +1,252 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed.checkpoint as dcp
|
||||
from torch.distributed.checkpoint.state_dict import get_state_dict, set_state_dict
|
||||
from torch.distributed.checkpoint.stateful import Stateful
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelState(Stateful):
|
||||
"""Wrapper for model state only."""
|
||||
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
|
||||
def state_dict(self):
|
||||
model_state_dict, _ = get_state_dict(self.model, optimizers=[])
|
||||
return {"model": model_state_dict}
|
||||
|
||||
def load_state_dict(self, state_dict):
|
||||
set_state_dict(self.model, optimizers=[], model_state_dict=state_dict["model"], optim_state_dict=None)
|
||||
|
||||
|
||||
class OptimizerState(Stateful):
|
||||
"""Wrapper for optimizer state only."""
|
||||
|
||||
def __init__(self, model, optimizer):
|
||||
self.model = model
|
||||
self.optimizer = optimizer
|
||||
|
||||
def state_dict(self):
|
||||
_, optimizer_state_dict = get_state_dict(self.model, optimizers=self.optimizer)
|
||||
return {"optim": optimizer_state_dict}
|
||||
|
||||
def load_state_dict(self, state_dict):
|
||||
set_state_dict(
|
||||
self.model, optimizers=self.optimizer, model_state_dict=None, optim_state_dict=state_dict["optim"]
|
||||
)
|
||||
|
||||
|
||||
class LRSchedulerState(Stateful):
|
||||
"""Wrapper for LR scheduler state only."""
|
||||
|
||||
def __init__(self, lr_scheduler):
|
||||
self.lr_scheduler = lr_scheduler
|
||||
|
||||
def state_dict(self):
|
||||
return {"lr_scheduler": self.lr_scheduler.state_dict()}
|
||||
|
||||
def load_state_dict(self, state_dict):
|
||||
self.lr_scheduler.load_state_dict(state_dict["lr_scheduler"])
|
||||
|
||||
|
||||
def _read_checkpoint_metadata(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse checkpoint metadata at {path}")
|
||||
return {}
|
||||
|
||||
|
||||
def _write_checkpoint_metadata(path: Path, metadata: dict[str, Any]) -> None:
|
||||
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp_path.write_text(json.dumps(metadata, indent=2, sort_keys=True))
|
||||
tmp_path.replace(path)
|
||||
|
||||
|
||||
def load(actor: Any) -> dict[str, Any] | None:
|
||||
"""Load checkpoint from disk.
|
||||
|
||||
Loads model weights and optionally optimizer state from separate directories.
|
||||
This allows loading weights without optimizer or deleting optimizer before loading.
|
||||
"""
|
||||
load_root = getattr(actor.args, "load", None)
|
||||
if load_root is None:
|
||||
return None
|
||||
|
||||
root_path = Path(load_root).expanduser()
|
||||
if not root_path.exists():
|
||||
logger.info(f"[FSDP] Checkpoint directory {root_path} not found; skipping load.")
|
||||
return None
|
||||
|
||||
target_step = getattr(actor.args, "ckpt_step", None)
|
||||
if target_step is None:
|
||||
tracker_file = root_path / "latest_checkpointed_iteration.txt"
|
||||
if not tracker_file.exists():
|
||||
logger.info(f"[FSDP] No tracker file at {tracker_file}; skipping load.")
|
||||
return None
|
||||
tracker_text = tracker_file.read_text().strip()
|
||||
target_step = int(tracker_text)
|
||||
|
||||
checkpoint_dir = root_path / f"iter_{target_step:07d}"
|
||||
model_dir = checkpoint_dir / "model"
|
||||
optimizer_dir = checkpoint_dir / "optimizer"
|
||||
lr_scheduler_dir = checkpoint_dir / "lr_scheduler"
|
||||
|
||||
if not model_dir.exists():
|
||||
logger.info(f"[FSDP] Model checkpoint {model_dir} not found; skipping load.")
|
||||
return None
|
||||
|
||||
# Load model weights (always)
|
||||
model_state = ModelState(actor.model)
|
||||
state_dict = {"model_state": model_state}
|
||||
|
||||
try:
|
||||
dcp.load(state_dict=state_dict, checkpoint_id=str(model_dir))
|
||||
logger.info(f"[FSDP] Loaded model from {model_dir}")
|
||||
except Exception as e:
|
||||
logger.error(f"[FSDP] Failed to load model from {model_dir}: {e}")
|
||||
return None
|
||||
|
||||
# Load optimizer state (optional)
|
||||
load_optimizer = not getattr(actor.args, "no_load_optim", False) and hasattr(actor, "optimizer")
|
||||
if load_optimizer and optimizer_dir.exists():
|
||||
optimizer_state = OptimizerState(actor.model, actor.optimizer)
|
||||
optim_state_dict = {"optim_state": optimizer_state}
|
||||
try:
|
||||
dcp.load(state_dict=optim_state_dict, checkpoint_id=str(optimizer_dir))
|
||||
logger.info(f"[FSDP] Loaded optimizer from {optimizer_dir}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[FSDP] Failed to load optimizer from {optimizer_dir}: {e}")
|
||||
elif load_optimizer:
|
||||
logger.info(f"[FSDP] Optimizer checkpoint not found at {optimizer_dir}, skipping optimizer load.")
|
||||
|
||||
# Load LR scheduler state (optional)
|
||||
load_lr_scheduler = hasattr(actor, "lr_scheduler") and lr_scheduler_dir.exists()
|
||||
if load_lr_scheduler:
|
||||
lr_scheduler_state = LRSchedulerState(actor.lr_scheduler)
|
||||
lr_scheduler_state_dict = {"lr_scheduler_state": lr_scheduler_state}
|
||||
try:
|
||||
dcp.load(state_dict=lr_scheduler_state_dict, checkpoint_id=str(lr_scheduler_dir))
|
||||
logger.info(f"[FSDP] Loaded LR scheduler from {lr_scheduler_dir}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[FSDP] Failed to load LR scheduler from {lr_scheduler_dir}: {e}")
|
||||
elif hasattr(actor, "lr_scheduler"):
|
||||
logger.info(f"[FSDP] LR scheduler checkpoint not found at {lr_scheduler_dir}, skipping LR scheduler load.")
|
||||
|
||||
rng_state = None
|
||||
rng_path = checkpoint_dir / "rng.pt"
|
||||
if rng_path.exists():
|
||||
rng_state = torch.load(rng_path, map_location="cpu")
|
||||
|
||||
metadata = _read_checkpoint_metadata(checkpoint_dir / "meta.json")
|
||||
|
||||
return {
|
||||
"rng": rng_state,
|
||||
"metadata": metadata,
|
||||
"iteration": target_step,
|
||||
}
|
||||
|
||||
|
||||
def finalize_load(actor: Any, checkpoint_payload: dict[str, Any] | None) -> None:
|
||||
if checkpoint_payload is None:
|
||||
dist.barrier()
|
||||
return
|
||||
|
||||
if checkpoint_payload.get("rng") is not None and not getattr(actor.args, "no_load_rng", False):
|
||||
rng_state = checkpoint_payload["rng"]
|
||||
if "torch" in rng_state:
|
||||
torch.set_rng_state(rng_state["torch"])
|
||||
if torch.cuda.is_available() and "cuda" in rng_state:
|
||||
torch.cuda.set_rng_state_all(rng_state["cuda"])
|
||||
|
||||
metadata = checkpoint_payload.get("metadata") or {}
|
||||
iteration = checkpoint_payload.get("iteration")
|
||||
if metadata:
|
||||
actor.global_step = int(metadata.get("global_step", actor.global_step))
|
||||
actor.micro_step = int(metadata.get("micro_step", actor.micro_step))
|
||||
next_rollout = metadata.get("next_rollout_id")
|
||||
if next_rollout is not None:
|
||||
actor.args.start_rollout_id = next_rollout
|
||||
elif iteration is not None:
|
||||
if getattr(actor.args, "start_rollout_id", None) is None:
|
||||
actor.args.start_rollout_id = iteration
|
||||
|
||||
torch.cuda.synchronize()
|
||||
dist.barrier()
|
||||
|
||||
|
||||
def save(actor: Any, iteration: int) -> None:
|
||||
"""Save checkpoint to disk.
|
||||
|
||||
Saves model weights and optimizer state to separate directories.
|
||||
This allows loading weights without optimizer or deleting optimizer before loading.
|
||||
"""
|
||||
torch.cuda.synchronize()
|
||||
|
||||
base_dir = Path(actor.args.save).expanduser()
|
||||
step_id = iteration + 1
|
||||
checkpoint_dir = base_dir / f"iter_{step_id:07d}"
|
||||
model_dir = checkpoint_dir / "model"
|
||||
optimizer_dir = checkpoint_dir / "optimizer"
|
||||
lr_scheduler_dir = checkpoint_dir / "lr_scheduler"
|
||||
|
||||
if dist.get_rank() == 0:
|
||||
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
||||
model_dir.mkdir(parents=True, exist_ok=True)
|
||||
optimizer_dir.mkdir(parents=True, exist_ok=True)
|
||||
lr_scheduler_dir.mkdir(parents=True, exist_ok=True)
|
||||
dist.barrier()
|
||||
|
||||
# Save model weights
|
||||
model_state = ModelState(actor.model)
|
||||
state_dict = {"model_state": model_state}
|
||||
dcp.save(state_dict, checkpoint_id=str(model_dir))
|
||||
|
||||
# Save optimizer state
|
||||
if hasattr(actor, "optimizer") and actor.optimizer is not None:
|
||||
optimizer_state = OptimizerState(actor.model, actor.optimizer)
|
||||
optim_state_dict = {"optim_state": optimizer_state}
|
||||
dcp.save(optim_state_dict, checkpoint_id=str(optimizer_dir))
|
||||
|
||||
# Save LR scheduler state
|
||||
if hasattr(actor, "lr_scheduler") and actor.lr_scheduler is not None:
|
||||
lr_scheduler_state = LRSchedulerState(actor.lr_scheduler)
|
||||
lr_scheduler_state_dict = {"lr_scheduler_state": lr_scheduler_state}
|
||||
dcp.save(lr_scheduler_state_dict, checkpoint_id=str(lr_scheduler_dir))
|
||||
|
||||
if dist.get_rank() == 0:
|
||||
rng_state = {"torch": torch.get_rng_state()}
|
||||
rng_state["cuda"] = torch.cuda.get_rng_state_all()
|
||||
torch.save(rng_state, checkpoint_dir / "rng.pt")
|
||||
|
||||
metadata = {
|
||||
"iteration": step_id,
|
||||
"rollout_id": iteration,
|
||||
"next_rollout_id": iteration + 1,
|
||||
"global_step": actor.global_step,
|
||||
"micro_step": actor.micro_step,
|
||||
"world_size": dist.get_world_size(),
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
_write_checkpoint_metadata(checkpoint_dir / "meta.json", metadata)
|
||||
|
||||
tracker_file = base_dir / "latest_checkpointed_iteration.txt"
|
||||
tracker_file.write_text(str(step_id))
|
||||
logger.info(f"[FSDP] Saved checkpoint to {checkpoint_dir}")
|
||||
|
||||
dist.barrier()
|
||||
221
slime/backends/fsdp_utils/data_packing.py
Normal file
221
slime/backends/fsdp_utils/data_packing.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""Data packing utilities for FSDP backend to reduce padding overhead."""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from slime.utils.seqlen_balancing import get_seqlen_balanced_partitions
|
||||
|
||||
|
||||
def pack_sequences(
|
||||
tokens: list[list[int]],
|
||||
loss_masks: list[list[int]],
|
||||
rewards: list[float],
|
||||
raw_rewards: list,
|
||||
response_lengths: list[int],
|
||||
advantages: list[float],
|
||||
returns: list[float],
|
||||
rollout_log_probs: list[list[float]] | None = None,
|
||||
multimodal_train_inputs: list[dict] | None = None,
|
||||
max_tokens_per_gpu: int | None = None,
|
||||
num_packs: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Pack sequences into dense batches with cumulative sequence lengths.
|
||||
|
||||
Args:
|
||||
tokens: List of token sequences
|
||||
loss_masks: List of loss masks
|
||||
rewards: List of rewards per sequence
|
||||
raw_rewards: List of raw rewards per sequence
|
||||
response_lengths: List of response lengths per sequence
|
||||
advantages: List of advantages per sequence
|
||||
returns: List of returns per sequence
|
||||
rollout_log_probs: List of rollout log probabilities per sequence
|
||||
multimodal_train_inputs: List of dict of multimodal tensors for training per sequence
|
||||
max_tokens_per_gpu: Maximum tokens per GPU pack
|
||||
num_packs: Explicit number of packs to create
|
||||
|
||||
Returns:
|
||||
List of packed batches with tokens, masks, cu_seqlens, rewards, raw_rewards, response_lengths, advantages, returns
|
||||
"""
|
||||
if not tokens:
|
||||
return []
|
||||
|
||||
seq_lengths = [len(t) for t in tokens]
|
||||
|
||||
# Determine number of packs and use balanced partitioning
|
||||
if num_packs:
|
||||
k_partitions = num_packs
|
||||
elif max_tokens_per_gpu:
|
||||
total_tokens = sum(seq_lengths)
|
||||
k_partitions = max(1, math.ceil(total_tokens / max_tokens_per_gpu))
|
||||
else:
|
||||
k_partitions = 1
|
||||
|
||||
# Use balanced partitioning for optimal load distribution
|
||||
partitions = get_seqlen_balanced_partitions(
|
||||
seq_lengths, k_partitions=k_partitions, equal_size=False # Allow variable sizes for better balance
|
||||
)
|
||||
|
||||
# Pack each partition
|
||||
result = []
|
||||
for indices in partitions:
|
||||
# Build cumulative sequence lengths
|
||||
cu_seqlens = [0]
|
||||
flat_tokens = []
|
||||
flat_masks = []
|
||||
flat_positionids = []
|
||||
flat_advantages = []
|
||||
flat_returns = []
|
||||
flat_rollout_log_probs = []
|
||||
|
||||
for i in indices:
|
||||
seq_tokens = tokens[i]
|
||||
seq_mask = loss_masks[i]
|
||||
seq_positionids = list(range(len(seq_tokens)))
|
||||
|
||||
flat_tokens.extend(seq_tokens)
|
||||
flat_positionids.extend(seq_positionids)
|
||||
flat_masks.extend(seq_mask)
|
||||
flat_advantages.extend(advantages[i])
|
||||
flat_returns.extend(returns[i])
|
||||
if rollout_log_probs:
|
||||
flat_rollout_log_probs.extend(rollout_log_probs[i])
|
||||
cu_seqlens.append(cu_seqlens[-1] + len(seq_tokens))
|
||||
|
||||
packed_batch = {
|
||||
"tokens": torch.tensor(flat_tokens, dtype=torch.long),
|
||||
"loss_masks": torch.tensor(flat_masks, dtype=torch.int),
|
||||
"position_ids": torch.tensor(flat_positionids, dtype=torch.int),
|
||||
"cu_seqlens": torch.tensor(cu_seqlens, dtype=torch.int32),
|
||||
"rewards": torch.tensor([rewards[i] for i in indices], dtype=torch.float32),
|
||||
"raw_reward": [raw_rewards[i] for i in indices],
|
||||
"response_lengths": [response_lengths[i] for i in indices],
|
||||
"advantages": torch.tensor(flat_advantages, dtype=torch.float32),
|
||||
"returns": torch.tensor(flat_returns, dtype=torch.float32),
|
||||
"rollout_log_probs": torch.tensor(
|
||||
flat_rollout_log_probs, dtype=torch.float32, device=torch.cuda.current_device()
|
||||
),
|
||||
}
|
||||
|
||||
# Collect and add multimodal training tensors for this partition
|
||||
if multimodal_train_inputs:
|
||||
multimodal_data = {} # key -> concatenated tensor
|
||||
multimodal_num_items = {} # key -> list of item counts per sequence
|
||||
for i in indices:
|
||||
for key, mm_tensor in multimodal_train_inputs[i].items():
|
||||
if key not in multimodal_data:
|
||||
multimodal_data[key] = mm_tensor
|
||||
multimodal_num_items[key] = [mm_tensor.size(0)]
|
||||
else:
|
||||
multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0)
|
||||
multimodal_num_items[key].append(mm_tensor.size(0))
|
||||
packed_batch["multimodal_train_inputs"] = multimodal_data
|
||||
packed_batch["multimodal_num_items"] = multimodal_num_items
|
||||
|
||||
result.append(packed_batch)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def unpack_sequences(packed_batch: dict) -> list[dict]:
|
||||
"""
|
||||
Unpack sequences from a packed batch.
|
||||
|
||||
Args:
|
||||
packed_batch: Packed batch
|
||||
|
||||
Returns:
|
||||
List of unpacked batches
|
||||
"""
|
||||
|
||||
cu_seqlens = packed_batch["cu_seqlens"]
|
||||
num_sequences = len(cu_seqlens) - 1
|
||||
response_lengths = packed_batch["response_lengths"]
|
||||
multimodal_num_items = packed_batch.get("multimodal_num_items", {})
|
||||
|
||||
instances = []
|
||||
|
||||
# Calculate pad_length by counting trailing zeros
|
||||
tokens = packed_batch["tokens"]
|
||||
nonzero_indices = (tokens != 0).nonzero(as_tuple=True)[0]
|
||||
if len(nonzero_indices) > 0:
|
||||
# Last non-zero index, pad_length is everything after it
|
||||
pad_length = len(tokens) - nonzero_indices[-1].item() - 1
|
||||
else:
|
||||
pad_length = 0 # No padding if no non-zero tokens (or all zeros)
|
||||
for i in range(num_sequences):
|
||||
start_idx = cu_seqlens[i].item()
|
||||
end_idx = cu_seqlens[i + 1].item()
|
||||
instance = {}
|
||||
|
||||
# Copy any additional attributes that might exist in the packed batch
|
||||
for key, value in packed_batch.items():
|
||||
if key not in instance:
|
||||
# Skip multimodal_num_items - it's metadata
|
||||
if key == "multimodal_num_items":
|
||||
continue
|
||||
# Handle multimodal_train_inputs dict: split each tensor using multimodal_num_items
|
||||
elif key == "multimodal_train_inputs" and isinstance(value, dict):
|
||||
instance[key] = {}
|
||||
for mm_key, mm_tensor in value.items():
|
||||
if mm_key in multimodal_num_items:
|
||||
num_items_list = multimodal_num_items[mm_key]
|
||||
start_mm_idx = sum(num_items_list[:i])
|
||||
end_mm_idx = start_mm_idx + num_items_list[i]
|
||||
if num_items_list[i] > 0:
|
||||
instance[key][mm_key] = mm_tensor[start_mm_idx:end_mm_idx]
|
||||
# For tensor attributes, we need to slice them appropriately
|
||||
elif isinstance(value, torch.Tensor):
|
||||
if key in ["log_probs", "ref_log_probs", "cur_log_probs", "entropy"]:
|
||||
# These are computed from logits[:-1] so they have length seq_len-1
|
||||
instance[key] = value[
|
||||
end_idx - 1 - response_lengths[i] - pad_length : end_idx - 1 - pad_length
|
||||
]
|
||||
elif key == "rollout_log_probs":
|
||||
# rollout_log_probs is packed based on response_lengths, so slice differently
|
||||
instance[key] = value[sum(response_lengths[:i]) : sum(response_lengths[: i + 1])]
|
||||
elif key in ["tokens", "position_ids"]:
|
||||
# For other tensor attributes, try to slice them
|
||||
if len(value) > start_idx:
|
||||
instance[key] = value[start_idx:end_idx]
|
||||
else:
|
||||
raise ValueError(f"Attribute {key} is not found in the packed batch")
|
||||
elif key in ["loss_masks", "advantages", "returns"]:
|
||||
instance[key] = value[sum(response_lengths[:i]) : sum(response_lengths[: i + 1])]
|
||||
elif isinstance(value, list):
|
||||
instance[key] = value[i]
|
||||
else:
|
||||
raise ValueError(f"Attribute {key} is not found in the packed batch")
|
||||
|
||||
instances.append(instance)
|
||||
|
||||
return instances
|
||||
|
||||
|
||||
def pad_packed_sequence_with_cp(packed_sequence: dict, cp_size: int) -> dict:
|
||||
"""Pad packed sequence to make total length divisible by cp_size.
|
||||
|
||||
Args:
|
||||
packed_sequence: Packed sequence dict containing tokens, position_ids, cu_seqlens, etc.
|
||||
cp_size: Context parallelism world size
|
||||
|
||||
Returns:
|
||||
Padded packed sequence
|
||||
"""
|
||||
seq_length = len(packed_sequence["tokens"])
|
||||
# Calculate padding needed: (cp_size - seq_length % cp_size) % cp_size
|
||||
remainder = seq_length % cp_size
|
||||
pad_length = (cp_size - remainder) % cp_size
|
||||
|
||||
if pad_length > 0:
|
||||
packed_sequence["tokens"] = F.pad(packed_sequence["tokens"], (0, pad_length), value=0)
|
||||
packed_sequence["position_ids"] = F.pad(packed_sequence["position_ids"], (0, pad_length), value=0)
|
||||
packed_sequence["loss_masks"] = F.pad(packed_sequence["loss_masks"], (0, pad_length), value=0)
|
||||
packed_sequence["cu_seqlens"][-1] += pad_length
|
||||
return packed_sequence
|
||||
3
slime/backends/fsdp_utils/kernels/__init__.py
Normal file
3
slime/backends/fsdp_utils/kernels/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
384
slime/backends/fsdp_utils/kernels/fused_experts.py
Normal file
384
slime/backends/fsdp_utils/kernels/fused_experts.py
Normal file
@@ -0,0 +1,384 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import triton.language as tl
|
||||
from sglang.srt.layers.moe.fused_moe_triton.fused_moe import (
|
||||
invoke_fused_moe_kernel,
|
||||
moe_align_block_size,
|
||||
moe_sum_reduce,
|
||||
silu_and_mul,
|
||||
)
|
||||
|
||||
from .fused_moe_triton_backward_kernels import invoke_fused_moe_backward_kernel
|
||||
|
||||
|
||||
class GateUpProjFunction(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
hidden_states: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
):
|
||||
num_tokens, _ = hidden_states.shape
|
||||
E, N, _ = w1.shape
|
||||
# We execute the fused_moe kernel in chunks to circumvent this issue:
|
||||
# https://github.com/vllm-project/vllm/issues/5938
|
||||
CHUNK_SIZE = 64 * 1024
|
||||
|
||||
# default deterministic config
|
||||
config = {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 32,
|
||||
"GROUP_SIZE_M": 8,
|
||||
}
|
||||
|
||||
topk = topk_ids.shape[1]
|
||||
|
||||
intermediate_cache1 = torch.empty(
|
||||
(num_tokens * topk, N),
|
||||
device=hidden_states.device,
|
||||
dtype=hidden_states.dtype,
|
||||
)
|
||||
|
||||
for chunk in range((num_tokens // CHUNK_SIZE) + 1):
|
||||
begin_chunk_idx, end_chunk_idx = (
|
||||
chunk * CHUNK_SIZE,
|
||||
min((chunk + 1) * CHUNK_SIZE, num_tokens),
|
||||
)
|
||||
curr_hidden_states = hidden_states[begin_chunk_idx:end_chunk_idx]
|
||||
cur_intermediate_cache1 = intermediate_cache1[begin_chunk_idx * topk : end_chunk_idx * topk]
|
||||
|
||||
curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
|
||||
curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
|
||||
|
||||
sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
|
||||
curr_topk_ids, config["BLOCK_SIZE_M"], E
|
||||
)
|
||||
|
||||
invoke_fused_moe_kernel(
|
||||
curr_hidden_states,
|
||||
w1,
|
||||
None,
|
||||
cur_intermediate_cache1,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
curr_topk_weights,
|
||||
curr_topk_ids,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
False,
|
||||
topk_ids.shape[1],
|
||||
config,
|
||||
compute_type=tl.bfloat16,
|
||||
use_fp8_w8a8=False,
|
||||
use_int8_w8a8=False,
|
||||
use_int8_w8a16=False,
|
||||
use_int4_w4a16=False,
|
||||
per_channel_quant=False,
|
||||
block_shape=None,
|
||||
c_sorted=False,
|
||||
filter_expert=True,
|
||||
)
|
||||
|
||||
ctx.save_for_backward(hidden_states, w1, topk_weights, topk_ids)
|
||||
ctx.config = config
|
||||
ctx.num_tokens = num_tokens
|
||||
ctx.topk = topk
|
||||
|
||||
return intermediate_cache1
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
"""
|
||||
Backward pass for GateUpProjFunction using Triton kernels.
|
||||
|
||||
Args:
|
||||
grad_output: shape (num_tokens * topk, N)
|
||||
|
||||
Returns:
|
||||
(grad_hidden_states, grad_w1, grad_topk_weights, None)
|
||||
"""
|
||||
|
||||
hidden_states, w1, topk_weights, topk_ids = ctx.saved_tensors
|
||||
config = ctx.config
|
||||
num_tokens = ctx.num_tokens
|
||||
topk = ctx.topk
|
||||
|
||||
E, N, D_in = w1.shape
|
||||
CHUNK_SIZE = 64 * 1024
|
||||
|
||||
# Initialize gradient tensors
|
||||
grad_hidden_states = torch.zeros_like(hidden_states)
|
||||
grad_w1 = torch.zeros_like(w1)
|
||||
# GateUpProj stage doesn't need topk_weights gradient
|
||||
grad_topk_weights = torch.zeros_like(topk_weights)
|
||||
|
||||
# Process in chunks to match forward pass
|
||||
for chunk in range((num_tokens // CHUNK_SIZE) + 1):
|
||||
begin_chunk_idx, end_chunk_idx = (
|
||||
chunk * CHUNK_SIZE,
|
||||
min((chunk + 1) * CHUNK_SIZE, num_tokens),
|
||||
)
|
||||
|
||||
curr_num_tokens = end_chunk_idx - begin_chunk_idx
|
||||
if curr_num_tokens == 0:
|
||||
continue
|
||||
|
||||
curr_hidden_states = hidden_states[begin_chunk_idx:end_chunk_idx]
|
||||
curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
|
||||
curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
|
||||
curr_grad_output = grad_output[begin_chunk_idx * topk : end_chunk_idx * topk]
|
||||
|
||||
# Get aligned metadata
|
||||
sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
|
||||
curr_topk_ids, config["BLOCK_SIZE_M"], E
|
||||
)
|
||||
|
||||
# Prepare gradient buffer for this chunk
|
||||
curr_grad_hidden_states = torch.zeros_like(curr_hidden_states)
|
||||
curr_grad_w1 = torch.zeros_like(w1)
|
||||
|
||||
# Call Triton backward kernel with MUL_ROUTED_WEIGHT=False
|
||||
# Use chunk of hidden_states to match sorted_token_ids indices
|
||||
invoke_fused_moe_backward_kernel(
|
||||
grad_output=curr_grad_output,
|
||||
input=curr_hidden_states, # Use chunk of hidden_states to match sorted_token_ids
|
||||
weight=w1,
|
||||
grad_input=curr_grad_hidden_states,
|
||||
grad_weight=curr_grad_w1,
|
||||
grad_topk_weights=None, # Not needed for GateUpProj
|
||||
topk_weights=curr_topk_weights,
|
||||
topk_ids=curr_topk_ids,
|
||||
sorted_token_ids=sorted_token_ids,
|
||||
expert_ids=expert_ids,
|
||||
num_tokens_post_padded=num_tokens_post_padded,
|
||||
mul_routed_weight=False,
|
||||
top_k=topk,
|
||||
config=config,
|
||||
compute_type=tl.bfloat16,
|
||||
)
|
||||
|
||||
# Accumulate gradients
|
||||
grad_hidden_states[begin_chunk_idx:end_chunk_idx] += curr_grad_hidden_states
|
||||
grad_w1 += curr_grad_w1
|
||||
|
||||
return grad_hidden_states, grad_w1, grad_topk_weights, None
|
||||
|
||||
|
||||
class SiluAndMulFunction(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, intermediate_cache1: torch.Tensor):
|
||||
num_tokens, N = intermediate_cache1.shape
|
||||
intermediate_cache2 = torch.empty(
|
||||
(num_tokens, N // 2),
|
||||
device=intermediate_cache1.device,
|
||||
dtype=intermediate_cache1.dtype,
|
||||
)
|
||||
silu_and_mul(intermediate_cache1.view(-1, N), intermediate_cache2)
|
||||
|
||||
ctx.save_for_backward(intermediate_cache1)
|
||||
return intermediate_cache2
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
(intermediate_cache1,) = ctx.saved_tensors
|
||||
N = intermediate_cache1.shape[-1]
|
||||
x1, x2 = intermediate_cache1.view(-1, N).chunk(2, dim=-1)
|
||||
silu_x1 = torch.nn.functional.silu(x1)
|
||||
|
||||
sig = torch.sigmoid(x1)
|
||||
dsilu_dx1 = sig + x1 * sig * (1 - sig)
|
||||
grad_x1 = grad_output * x2 * dsilu_dx1
|
||||
grad_x2 = grad_output * silu_x1
|
||||
grad_input = torch.cat([grad_x1, grad_x2], dim=-1)
|
||||
|
||||
return grad_input.view_as(intermediate_cache1)
|
||||
|
||||
|
||||
class DownProjFunction(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
intermediate_cache2: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
):
|
||||
num_tokens, _ = intermediate_cache2.shape
|
||||
topk = topk_ids.shape[1]
|
||||
num_tokens //= topk
|
||||
E, _, _ = w2.shape
|
||||
# We execute the fused_moe kernel in chunks to circumvent this issue:
|
||||
# https://github.com/vllm-project/vllm/issues/5938
|
||||
CHUNK_SIZE = 64 * 1024
|
||||
|
||||
# default deterministic config
|
||||
config = {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 32,
|
||||
"GROUP_SIZE_M": 8,
|
||||
}
|
||||
|
||||
intermediate_cache3 = torch.empty(
|
||||
(num_tokens, topk, w2.shape[1]),
|
||||
device=intermediate_cache2.device,
|
||||
dtype=intermediate_cache2.dtype,
|
||||
)
|
||||
|
||||
for chunk in range((num_tokens // CHUNK_SIZE) + 1):
|
||||
begin_chunk_idx, end_chunk_idx = (
|
||||
chunk * CHUNK_SIZE,
|
||||
min((chunk + 1) * CHUNK_SIZE, num_tokens),
|
||||
)
|
||||
cur_intermediate_cache2 = intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk]
|
||||
cur_intermediate_cache3 = intermediate_cache3[begin_chunk_idx:end_chunk_idx]
|
||||
|
||||
curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
|
||||
curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
|
||||
|
||||
sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
|
||||
curr_topk_ids, config["BLOCK_SIZE_M"], E
|
||||
)
|
||||
invoke_fused_moe_kernel(
|
||||
cur_intermediate_cache2,
|
||||
w2,
|
||||
None,
|
||||
cur_intermediate_cache3,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
curr_topk_weights,
|
||||
curr_topk_ids,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
True,
|
||||
1,
|
||||
config,
|
||||
compute_type=tl.bfloat16,
|
||||
use_fp8_w8a8=False,
|
||||
use_int8_w8a8=False,
|
||||
use_int8_w8a16=False,
|
||||
use_int4_w4a16=False,
|
||||
per_channel_quant=False,
|
||||
block_shape=None,
|
||||
a_use_tma=False,
|
||||
b_use_tma=False,
|
||||
)
|
||||
|
||||
ctx.save_for_backward(intermediate_cache2, w2, topk_weights, topk_ids)
|
||||
ctx.config = config
|
||||
ctx.num_tokens = num_tokens
|
||||
ctx.topk = topk
|
||||
|
||||
return intermediate_cache3
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
"""
|
||||
Backward pass for DownProjFunction using Triton kernels.
|
||||
|
||||
Args:
|
||||
grad_output: shape (num_tokens, topk, hidden_size)
|
||||
|
||||
Returns:
|
||||
(grad_intermediate_cache2, grad_w2, grad_topk_weights, None)
|
||||
"""
|
||||
intermediate_cache2, w2, topk_weights, topk_ids = ctx.saved_tensors
|
||||
config = ctx.config
|
||||
num_tokens = ctx.num_tokens
|
||||
topk = ctx.topk
|
||||
|
||||
E, hidden_size, intermediate_size = w2.shape
|
||||
CHUNK_SIZE = 64 * 1024
|
||||
|
||||
# Initialize gradient tensors
|
||||
grad_intermediate_cache2 = torch.zeros_like(intermediate_cache2)
|
||||
grad_w2 = torch.zeros_like(w2)
|
||||
grad_topk_weights = torch.zeros_like(topk_weights)
|
||||
|
||||
# Process in chunks to match forward pass
|
||||
for chunk in range((num_tokens // CHUNK_SIZE) + 1):
|
||||
begin_chunk_idx, end_chunk_idx = (
|
||||
chunk * CHUNK_SIZE,
|
||||
min((chunk + 1) * CHUNK_SIZE, num_tokens),
|
||||
)
|
||||
|
||||
curr_num_tokens = end_chunk_idx - begin_chunk_idx
|
||||
if curr_num_tokens == 0:
|
||||
continue
|
||||
|
||||
curr_intermediate_cache2 = intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk]
|
||||
curr_topk_ids = topk_ids[begin_chunk_idx:end_chunk_idx]
|
||||
curr_topk_weights = topk_weights[begin_chunk_idx:end_chunk_idx]
|
||||
curr_grad_output = grad_output[begin_chunk_idx:end_chunk_idx]
|
||||
|
||||
# Get aligned metadata
|
||||
sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
|
||||
curr_topk_ids, config["BLOCK_SIZE_M"], E
|
||||
)
|
||||
|
||||
# Prepare gradient buffers for this chunk
|
||||
curr_grad_intermediate_cache2 = torch.zeros_like(curr_intermediate_cache2)
|
||||
curr_grad_w2 = torch.zeros_like(w2)
|
||||
curr_grad_topk_weights = torch.zeros_like(curr_topk_weights)
|
||||
|
||||
# Call Triton backward kernel with MUL_ROUTED_WEIGHT=True
|
||||
# Note: Use top_k=1 to match forward pass indexing
|
||||
invoke_fused_moe_backward_kernel(
|
||||
grad_output=curr_grad_output,
|
||||
input=curr_intermediate_cache2,
|
||||
weight=w2,
|
||||
grad_input=curr_grad_intermediate_cache2,
|
||||
grad_weight=curr_grad_w2,
|
||||
grad_topk_weights=curr_grad_topk_weights,
|
||||
topk_weights=curr_topk_weights,
|
||||
topk_ids=curr_topk_ids,
|
||||
sorted_token_ids=sorted_token_ids,
|
||||
expert_ids=expert_ids,
|
||||
num_tokens_post_padded=num_tokens_post_padded,
|
||||
mul_routed_weight=True,
|
||||
top_k=1,
|
||||
config=config,
|
||||
compute_type=tl.bfloat16,
|
||||
)
|
||||
|
||||
# Accumulate gradients
|
||||
grad_intermediate_cache2[begin_chunk_idx * topk : end_chunk_idx * topk] = curr_grad_intermediate_cache2
|
||||
grad_w2 += curr_grad_w2
|
||||
grad_topk_weights[begin_chunk_idx:end_chunk_idx] = curr_grad_topk_weights
|
||||
|
||||
return grad_intermediate_cache2, grad_w2, grad_topk_weights, None
|
||||
|
||||
|
||||
class MoeSumReduceFunction(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
intermediate_cache3: torch.Tensor,
|
||||
hidden_states_shape,
|
||||
):
|
||||
out_hidden_states = torch.empty(
|
||||
hidden_states_shape, device=intermediate_cache3.device, dtype=intermediate_cache3.dtype
|
||||
)
|
||||
moe_sum_reduce(
|
||||
intermediate_cache3,
|
||||
out_hidden_states,
|
||||
1.0,
|
||||
)
|
||||
ctx.save_for_backward(intermediate_cache3)
|
||||
return out_hidden_states
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
(intermediate_cache3,) = ctx.saved_tensors
|
||||
return grad_output.unsqueeze(1).expand_as(intermediate_cache3), None
|
||||
@@ -0,0 +1,543 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def fused_moe_backward_input_kernel(
|
||||
# Pointers to matrices
|
||||
grad_output_ptr,
|
||||
weight_ptr,
|
||||
grad_input_ptr,
|
||||
grad_topk_weights_ptr,
|
||||
topk_weights_ptr,
|
||||
sorted_token_ids_ptr,
|
||||
expert_ids_ptr,
|
||||
num_tokens_post_padded_ptr,
|
||||
# Matrix dimensions
|
||||
N,
|
||||
K,
|
||||
EM,
|
||||
num_valid_tokens,
|
||||
# Strides
|
||||
stride_gom,
|
||||
stride_gon,
|
||||
stride_we,
|
||||
stride_wn,
|
||||
stride_wk,
|
||||
stride_gim,
|
||||
stride_gik,
|
||||
# Meta-parameters
|
||||
BLOCK_SIZE_M: tl.constexpr,
|
||||
BLOCK_SIZE_N: tl.constexpr,
|
||||
BLOCK_SIZE_K: tl.constexpr,
|
||||
GROUP_SIZE_M: tl.constexpr,
|
||||
MUL_ROUTED_WEIGHT: tl.constexpr,
|
||||
top_k: tl.constexpr,
|
||||
compute_type: tl.constexpr,
|
||||
):
|
||||
"""
|
||||
Backward kernel for computing grad_input.
|
||||
|
||||
Forward: output = input @ weight.T (optionally multiplied by topk_weights)
|
||||
Backward: grad_input = grad_output @ weight (optionally multiplied by topk_weights)
|
||||
|
||||
This kernel computes: grad_input[token] = sum_over_N(grad_output[token, n] * weight[expert, n, :])
|
||||
If MUL_ROUTED_WEIGHT: grad_input[token] *= topk_weights[token]
|
||||
|
||||
Parallelization: Similar to forward, parallel over M and N dimensions, loop over K.
|
||||
"""
|
||||
# Map program ids to blocks (parallel over M and N, similar to forward)
|
||||
pid = tl.program_id(axis=0)
|
||||
num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
|
||||
num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
|
||||
num_pid_in_group = GROUP_SIZE_M * num_pid_n
|
||||
group_id = pid // num_pid_in_group
|
||||
first_pid_m = group_id * GROUP_SIZE_M
|
||||
group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
|
||||
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
|
||||
pid_n = (pid % num_pid_in_group) // group_size_m
|
||||
|
||||
# Check bounds
|
||||
num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
|
||||
|
||||
# Only process if this block is valid
|
||||
if pid_m * BLOCK_SIZE_M < num_tokens_post_padded:
|
||||
# Load token information
|
||||
offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
|
||||
offs_token = tl.load(sorted_token_ids_ptr + offs_token_id)
|
||||
offs_token = offs_token.to(tl.int64)
|
||||
token_mask = offs_token < num_valid_tokens
|
||||
|
||||
# Get expert ID for this block
|
||||
off_experts = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
|
||||
|
||||
# Only process if expert is valid
|
||||
if off_experts != -1:
|
||||
# Initialize offsets for N dimension (current block)
|
||||
offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
|
||||
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
||||
|
||||
# Load grad_output block: shape (BLOCK_SIZE_M, BLOCK_SIZE_N)
|
||||
grad_output_ptrs = grad_output_ptr + (offs_token[:, None] * stride_gom + offs_n[None, :] * stride_gon)
|
||||
grad_out = tl.load(
|
||||
grad_output_ptrs,
|
||||
mask=token_mask[:, None] & (offs_n[None, :] < N),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Apply topk_weights to grad_output if needed
|
||||
if MUL_ROUTED_WEIGHT:
|
||||
moe_weight = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0)
|
||||
grad_out = grad_out * moe_weight[:, None]
|
||||
|
||||
# Iterate over K dimension
|
||||
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
||||
# Current K offsets
|
||||
curr_offs_k = k * BLOCK_SIZE_K + offs_k
|
||||
|
||||
# Load weight block: shape (BLOCK_SIZE_N, BLOCK_SIZE_K)
|
||||
# weight: shape (E, N, K)
|
||||
weight_ptrs = (
|
||||
weight_ptr
|
||||
+ off_experts * stride_we
|
||||
+ offs_n[:, None] * stride_wn
|
||||
+ curr_offs_k[None, :] * stride_wk
|
||||
)
|
||||
w = tl.load(
|
||||
weight_ptrs,
|
||||
mask=(offs_n[:, None] < N) & (curr_offs_k[None, :] < K),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Compute contribution: grad_out @ weight
|
||||
# grad_out: (BLOCK_SIZE_M, BLOCK_SIZE_N)
|
||||
# w: (BLOCK_SIZE_N, BLOCK_SIZE_K)
|
||||
# result: (BLOCK_SIZE_M, BLOCK_SIZE_K)
|
||||
contribution = tl.dot(grad_out, w)
|
||||
|
||||
# Atomic add to grad_input because different N blocks contribute to same K
|
||||
grad_input_ptrs = grad_input_ptr + (
|
||||
(offs_token[:, None] // top_k) * stride_gim + curr_offs_k[None, :] * stride_gik
|
||||
)
|
||||
grad_input_mask = token_mask[:, None] & (curr_offs_k[None, :] < K)
|
||||
tl.atomic_add(grad_input_ptrs, contribution.to(compute_type), mask=grad_input_mask)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def fused_moe_backward_weight_kernel(
|
||||
# Pointers to matrices
|
||||
grad_output_ptr,
|
||||
input_ptr,
|
||||
grad_weight_ptr,
|
||||
topk_weights_ptr,
|
||||
sorted_token_ids_ptr,
|
||||
expert_ids_ptr,
|
||||
num_tokens_post_padded_ptr,
|
||||
# Matrix dimensions
|
||||
N,
|
||||
K,
|
||||
EM,
|
||||
num_valid_tokens,
|
||||
# Strides
|
||||
stride_gom,
|
||||
stride_gon,
|
||||
stride_im,
|
||||
stride_ik,
|
||||
stride_gwe,
|
||||
stride_gwn,
|
||||
stride_gwk,
|
||||
# Meta-parameters
|
||||
BLOCK_SIZE_M: tl.constexpr,
|
||||
BLOCK_SIZE_N: tl.constexpr,
|
||||
BLOCK_SIZE_K: tl.constexpr,
|
||||
GROUP_SIZE_M: tl.constexpr,
|
||||
MUL_ROUTED_WEIGHT: tl.constexpr,
|
||||
top_k: tl.constexpr,
|
||||
compute_type: tl.constexpr,
|
||||
):
|
||||
"""
|
||||
Backward kernel for computing grad_weight.
|
||||
|
||||
Forward: output = input @ weight.T (optionally multiplied by topk_weights)
|
||||
Backward: grad_weight = input.T @ grad_output (optionally multiplied by topk_weights)
|
||||
|
||||
This kernel computes: grad_weight[expert, n, k] = sum_over_tokens(input[token, k] * grad_output[token, n])
|
||||
If MUL_ROUTED_WEIGHT: the accumulation is weighted by topk_weights[token]
|
||||
|
||||
Parallelization: Parallel over M and N dimensions with grouping, loop over K.
|
||||
"""
|
||||
# Map program ids to blocks (parallel over M and N with grouping, similar to forward and backward_input)
|
||||
pid = tl.program_id(axis=0)
|
||||
num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
|
||||
num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
|
||||
num_pid_in_group = GROUP_SIZE_M * num_pid_n
|
||||
group_id = pid // num_pid_in_group
|
||||
first_pid_m = group_id * GROUP_SIZE_M
|
||||
group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
|
||||
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
|
||||
pid_n = (pid % num_pid_in_group) // group_size_m
|
||||
|
||||
# Check bounds
|
||||
num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
|
||||
|
||||
# Only process if this block is valid
|
||||
if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
|
||||
return
|
||||
|
||||
# Get expert ID for this M block
|
||||
expert_id = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
|
||||
|
||||
# Only process if expert is valid
|
||||
if expert_id == -1:
|
||||
return
|
||||
|
||||
# Load token information for this M block
|
||||
offs_m = tl.arange(0, BLOCK_SIZE_M)
|
||||
offs_token_id = pid_m * BLOCK_SIZE_M + offs_m.to(tl.int64)
|
||||
offs_token = tl.load(
|
||||
sorted_token_ids_ptr + offs_token_id, mask=offs_token_id < num_tokens_post_padded, other=num_valid_tokens
|
||||
)
|
||||
offs_token = offs_token.to(tl.int64)
|
||||
token_mask = (offs_token_id < num_tokens_post_padded) & (offs_token < num_valid_tokens)
|
||||
|
||||
# Clamp offs_token to valid range
|
||||
offs_token_clamped = tl.where(token_mask, offs_token, 0)
|
||||
|
||||
# Determine input token indices based on MUL_ROUTED_WEIGHT
|
||||
if MUL_ROUTED_WEIGHT:
|
||||
input_token_idx = offs_token_clamped
|
||||
input_mask = token_mask
|
||||
else:
|
||||
input_token_idx = offs_token_clamped // top_k
|
||||
num_input_tokens = num_valid_tokens // top_k
|
||||
input_mask = token_mask & (input_token_idx < num_input_tokens)
|
||||
|
||||
# Load topk_weights if needed
|
||||
if MUL_ROUTED_WEIGHT:
|
||||
moe_weight = tl.load(topk_weights_ptr + offs_token_clamped, mask=token_mask, other=0.0)
|
||||
|
||||
# Current N offset for this program
|
||||
offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
|
||||
|
||||
# Load grad_output for this N block: shape (M, BLOCK_SIZE_N)
|
||||
# grad_output is always indexed by sorted_token_ids (offs_token_clamped)
|
||||
# because it has shape (num_tokens * topk, N)
|
||||
grad_output_ptrs = grad_output_ptr + (offs_token_clamped[:, None] * stride_gom + offs_n[None, :] * stride_gon)
|
||||
grad_out = tl.load(
|
||||
grad_output_ptrs,
|
||||
mask=token_mask[:, None] & (offs_n[None, :] < N),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Apply topk_weights if needed
|
||||
if MUL_ROUTED_WEIGHT:
|
||||
grad_out = grad_out * moe_weight[:, None]
|
||||
|
||||
# Zero out padding tokens
|
||||
token_mask_col = token_mask[:, None]
|
||||
grad_out = grad_out * token_mask_col
|
||||
|
||||
# Iterate over K blocks and accumulate
|
||||
for k_block in range(tl.cdiv(K, BLOCK_SIZE_K)):
|
||||
offs_k = k_block * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K).to(tl.int64)
|
||||
|
||||
# Load input for this K block
|
||||
input_ptrs = input_ptr + (input_token_idx[:, None] * stride_im + offs_k[None, :] * stride_ik)
|
||||
inp = tl.load(
|
||||
input_ptrs,
|
||||
mask=input_mask[:, None] & (offs_k[None, :] < K),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Zero out padding tokens - use input_mask for input, token_mask for grad_output
|
||||
input_mask_col = input_mask[:, None]
|
||||
inp = inp * input_mask_col
|
||||
|
||||
# Compute grad_weight contribution: grad_out.T @ inp
|
||||
grad_w_contribution = tl.dot(grad_out.T, inp)
|
||||
|
||||
# Write back using atomic add
|
||||
grad_weight_ptrs = (
|
||||
grad_weight_ptr + expert_id * stride_gwe + offs_n[:, None] * stride_gwn + offs_k[None, :] * stride_gwk
|
||||
)
|
||||
grad_weight_mask = (offs_n[:, None] < N) & (offs_k[None, :] < K)
|
||||
tl.atomic_add(grad_weight_ptrs, grad_w_contribution.to(compute_type), mask=grad_weight_mask)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def fused_moe_backward_topk_weights_kernel(
|
||||
# Pointers to matrices
|
||||
grad_output_ptr,
|
||||
input_ptr,
|
||||
weight_ptr,
|
||||
grad_topk_weights_ptr,
|
||||
sorted_token_ids_ptr,
|
||||
expert_ids_ptr,
|
||||
num_tokens_post_padded_ptr,
|
||||
# Matrix dimensions
|
||||
N,
|
||||
K,
|
||||
EM,
|
||||
num_valid_tokens,
|
||||
# Strides
|
||||
stride_gom,
|
||||
stride_gon,
|
||||
stride_im,
|
||||
stride_ik,
|
||||
stride_we,
|
||||
stride_wn,
|
||||
stride_wk,
|
||||
# Meta-parameters
|
||||
BLOCK_SIZE_M: tl.constexpr,
|
||||
BLOCK_SIZE_N: tl.constexpr,
|
||||
BLOCK_SIZE_K: tl.constexpr,
|
||||
top_k: tl.constexpr,
|
||||
compute_type: tl.constexpr,
|
||||
):
|
||||
"""
|
||||
Backward kernel for computing grad_topk_weights.
|
||||
|
||||
Forward: output = topk_weights * (input @ weight.T)
|
||||
Backward: grad_topk_weights = sum(grad_output * (input @ weight.T))
|
||||
|
||||
This kernel computes the gradient of topk_weights by computing the dot product
|
||||
of grad_output with the forward output before weight multiplication.
|
||||
"""
|
||||
# Map program id to token block
|
||||
pid = tl.program_id(axis=0)
|
||||
|
||||
# Check bounds
|
||||
num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
|
||||
|
||||
# Only process if this block is valid
|
||||
if pid * BLOCK_SIZE_M < num_tokens_post_padded:
|
||||
# Load token information
|
||||
offs_token_id = pid * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
|
||||
offs_token = tl.load(
|
||||
sorted_token_ids_ptr + offs_token_id, mask=offs_token_id < num_tokens_post_padded, other=num_valid_tokens
|
||||
)
|
||||
offs_token = offs_token.to(tl.int64)
|
||||
token_mask = (offs_token_id < num_tokens_post_padded) & (offs_token < num_valid_tokens)
|
||||
|
||||
# Clamp offs_token to valid range for safe pointer arithmetic
|
||||
offs_token_clamped = tl.where(token_mask, offs_token, 0)
|
||||
|
||||
# Get expert ID for this block
|
||||
off_experts = tl.load(expert_ids_ptr + pid).to(tl.int64)
|
||||
|
||||
# Only process if expert is valid
|
||||
if off_experts != -1:
|
||||
# Initialize offsets
|
||||
offs_n = tl.arange(0, BLOCK_SIZE_N)
|
||||
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
||||
|
||||
# Accumulator for grad_topk_weights
|
||||
accumulator = tl.zeros((BLOCK_SIZE_M,), dtype=tl.float32)
|
||||
|
||||
# Iterate over N and K dimensions to compute forward output and gradient
|
||||
for n in range(0, tl.cdiv(N, BLOCK_SIZE_N)):
|
||||
# Current N offset
|
||||
curr_offs_n = n * BLOCK_SIZE_N + offs_n
|
||||
|
||||
# Load grad_output block: (M, N)
|
||||
grad_output_ptrs = grad_output_ptr + (
|
||||
offs_token_clamped[:, None] * stride_gom + curr_offs_n[None, :] * stride_gon
|
||||
)
|
||||
grad_out = tl.load(
|
||||
grad_output_ptrs,
|
||||
mask=token_mask[:, None] & (curr_offs_n[None, :] < N),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Compute forward output for this N block: input @ weight[:, n, :].T
|
||||
forward_output_n = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
|
||||
|
||||
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
||||
# Current K offset
|
||||
curr_offs_k = k * BLOCK_SIZE_K + offs_k
|
||||
|
||||
# Load input block: (M, K)
|
||||
input_ptrs = input_ptr + (
|
||||
(offs_token_clamped[:, None] // top_k) * stride_im + curr_offs_k[None, :] * stride_ik
|
||||
)
|
||||
inp = tl.load(
|
||||
input_ptrs,
|
||||
mask=token_mask[:, None] & (curr_offs_k[None, :] < K),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Load weight block: (N, K)
|
||||
weight_ptrs = (
|
||||
weight_ptr
|
||||
+ off_experts * stride_we
|
||||
+ curr_offs_n[:, None] * stride_wn
|
||||
+ curr_offs_k[None, :] * stride_wk
|
||||
)
|
||||
w = tl.load(
|
||||
weight_ptrs,
|
||||
mask=(curr_offs_n[:, None] < N) & (curr_offs_k[None, :] < K),
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Accumulate forward output: input @ weight.T
|
||||
# inp: (M, K), w.T: (K, N) -> (M, N)
|
||||
forward_output_n += tl.dot(inp, w.T)
|
||||
|
||||
# Compute contribution to grad_topk_weights: sum(grad_out * forward_output)
|
||||
# Sum over N dimension
|
||||
accumulator += tl.sum(grad_out * forward_output_n, axis=1)
|
||||
|
||||
# Write back grad_topk_weights using atomic add with clamped token indices
|
||||
tl.atomic_add(grad_topk_weights_ptr + offs_token_clamped, accumulator.to(compute_type), mask=token_mask)
|
||||
|
||||
|
||||
def invoke_fused_moe_backward_kernel(
|
||||
grad_output: torch.Tensor,
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
grad_input: torch.Tensor,
|
||||
grad_weight: torch.Tensor,
|
||||
grad_topk_weights: torch.Tensor | None,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
sorted_token_ids: torch.Tensor,
|
||||
expert_ids: torch.Tensor,
|
||||
num_tokens_post_padded: torch.Tensor,
|
||||
mul_routed_weight: bool,
|
||||
top_k: int,
|
||||
config: dict[str, Any],
|
||||
compute_type: tl.dtype,
|
||||
) -> None:
|
||||
"""
|
||||
Invoke the fused MOE backward kernels to compute gradients.
|
||||
|
||||
Args:
|
||||
grad_output: Gradient of output, shape (num_tokens * topk, N) or (num_tokens, topk, N)
|
||||
input: Input tensor, shape (num_tokens, K)
|
||||
weight: Weight tensor, shape (E, N, K)
|
||||
grad_input: Output gradient for input, shape (num_tokens, K)
|
||||
grad_weight: Output gradient for weight, shape (E, N, K)
|
||||
grad_topk_weights: Output gradient for topk_weights, shape (num_tokens, topk) or None
|
||||
topk_weights: Top-K routing weights, shape (num_tokens, topk)
|
||||
topk_ids: Top-K expert IDs, shape (num_tokens, topk)
|
||||
sorted_token_ids: Sorted token IDs
|
||||
expert_ids: Expert IDs for each block
|
||||
num_tokens_post_padded: Number of tokens after padding
|
||||
mul_routed_weight: Whether to multiply by routing weights
|
||||
top_k: Number of experts per token
|
||||
config: Kernel configuration
|
||||
compute_type: Computation data type
|
||||
"""
|
||||
assert topk_weights.stride(1) == 1
|
||||
assert sorted_token_ids.stride(0) == 1
|
||||
|
||||
# Flatten grad_output if needed
|
||||
# Before: (num_tokens, topk, hidden_size)
|
||||
# After: (num_tokens * topk, hidden_size)
|
||||
if grad_output.ndim == 3:
|
||||
grad_output = grad_output.reshape(-1, grad_output.shape[-1])
|
||||
|
||||
E, N, K = weight.shape
|
||||
|
||||
# ===================== Compute grad_input =====================
|
||||
def grid_input(META):
|
||||
return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]),)
|
||||
|
||||
fused_moe_backward_input_kernel[grid_input](
|
||||
grad_output,
|
||||
weight,
|
||||
grad_input,
|
||||
grad_topk_weights if grad_topk_weights is not None else grad_input, # dummy pointer
|
||||
topk_weights,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
N,
|
||||
K,
|
||||
sorted_token_ids.shape[0],
|
||||
grad_output.shape[0],
|
||||
grad_output.stride(0),
|
||||
grad_output.stride(1),
|
||||
weight.stride(0),
|
||||
weight.stride(1),
|
||||
weight.stride(2),
|
||||
grad_input.stride(0),
|
||||
grad_input.stride(1),
|
||||
MUL_ROUTED_WEIGHT=mul_routed_weight,
|
||||
top_k=top_k,
|
||||
compute_type=compute_type,
|
||||
**config,
|
||||
)
|
||||
|
||||
# ===================== Compute grad_weight =====================
|
||||
# Initialize grad_weight to zero
|
||||
grad_weight.zero_()
|
||||
|
||||
# Use same grid configuration as forward kernel: encode both M and N dimensions
|
||||
def grid_weight(META):
|
||||
return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]) * triton.cdiv(N, META["BLOCK_SIZE_N"]),)
|
||||
|
||||
fused_moe_backward_weight_kernel[grid_weight](
|
||||
grad_output,
|
||||
input,
|
||||
grad_weight,
|
||||
topk_weights,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
N,
|
||||
K,
|
||||
sorted_token_ids.shape[0],
|
||||
grad_output.shape[0],
|
||||
grad_output.stride(0),
|
||||
grad_output.stride(1),
|
||||
input.stride(0),
|
||||
input.stride(1),
|
||||
grad_weight.stride(0),
|
||||
grad_weight.stride(1),
|
||||
grad_weight.stride(2),
|
||||
MUL_ROUTED_WEIGHT=mul_routed_weight,
|
||||
top_k=top_k,
|
||||
compute_type=compute_type,
|
||||
**config,
|
||||
)
|
||||
|
||||
# ===================== Compute grad_topk_weights (if needed) =====================
|
||||
if mul_routed_weight and grad_topk_weights is not None:
|
||||
|
||||
def grid_topk(META):
|
||||
return (triton.cdiv(sorted_token_ids.shape[0], META["BLOCK_SIZE_M"]),)
|
||||
|
||||
fused_moe_backward_topk_weights_kernel[grid_topk](
|
||||
grad_output,
|
||||
input,
|
||||
weight,
|
||||
grad_topk_weights.view(-1),
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
N,
|
||||
K,
|
||||
sorted_token_ids.shape[0],
|
||||
grad_output.shape[0],
|
||||
grad_output.stride(0),
|
||||
grad_output.stride(1),
|
||||
input.stride(0),
|
||||
input.stride(1),
|
||||
weight.stride(0),
|
||||
weight.stride(1),
|
||||
weight.stride(2),
|
||||
top_k=top_k,
|
||||
compute_type=compute_type,
|
||||
BLOCK_SIZE_M=config["BLOCK_SIZE_M"],
|
||||
BLOCK_SIZE_N=config["BLOCK_SIZE_N"],
|
||||
BLOCK_SIZE_K=config["BLOCK_SIZE_K"],
|
||||
)
|
||||
197
slime/backends/fsdp_utils/lr_scheduler.py
Normal file
197
slime/backends/fsdp_utils/lr_scheduler.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
||||
"""Learning rate scheduler for FSDP training."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch.optim.lr_scheduler import LRScheduler
|
||||
from typing_extensions import override
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FSDPLRScheduler(LRScheduler):
|
||||
"""Learning rate scheduler for FSDP training.
|
||||
|
||||
Args:
|
||||
optimizer (torch.optim.Optimizer): The optimizer to be used.
|
||||
init_lr (float): Initial learning rate.
|
||||
max_lr (float): Maximum learning rate.
|
||||
min_lr (float): Minimum learning rate.
|
||||
lr_warmup_steps (int): Number of warmup steps.
|
||||
lr_decay_steps (int): Number of decay steps.
|
||||
lr_decay_style (str): Decay style for learning rate.
|
||||
use_checkpoint_lr_scheduler (bool, optional): Whether to use the checkpoint values
|
||||
for the lr scheduler.
|
||||
override_lr_scheduler (bool, optional): Whether to override the lr scheduler values
|
||||
with the class values.
|
||||
wsd_decay_steps (int, optional): Number of weight decay decay steps.
|
||||
lr_wsd_decay_style (str, optional): Decay style for learning rate during weight decay decay
|
||||
steps.
|
||||
last_epoch (int, optional): The index of last epoch. Default: -1.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
init_lr: float,
|
||||
max_lr: float,
|
||||
min_lr: float,
|
||||
lr_warmup_steps: int,
|
||||
lr_decay_steps: int,
|
||||
lr_decay_style: str,
|
||||
use_checkpoint_lr_scheduler: bool | None = True,
|
||||
override_lr_scheduler: bool | None = False,
|
||||
wsd_decay_steps: int | None = None,
|
||||
lr_wsd_decay_style: str | None = None,
|
||||
last_epoch: int = -1,
|
||||
) -> None:
|
||||
# Store our custom parameters
|
||||
self.init_lr = init_lr
|
||||
self.max_lr = float(max_lr)
|
||||
self.min_lr = min_lr
|
||||
assert self.min_lr >= 0.0
|
||||
assert self.max_lr >= self.min_lr
|
||||
assert self.init_lr <= self.max_lr
|
||||
|
||||
self.lr_warmup_steps = lr_warmup_steps
|
||||
self.lr_decay_steps = lr_decay_steps
|
||||
self.wsd_decay_steps = wsd_decay_steps
|
||||
self.lr_wsd_decay_style = lr_wsd_decay_style
|
||||
|
||||
assert self.lr_decay_steps > 0
|
||||
assert self.lr_warmup_steps < self.lr_decay_steps
|
||||
|
||||
self.lr_decay_style = lr_decay_style
|
||||
if self.lr_decay_style == "WSD":
|
||||
assert self.wsd_decay_steps is not None
|
||||
|
||||
self.override_lr_scheduler = override_lr_scheduler
|
||||
self.use_checkpoint_lr_scheduler = use_checkpoint_lr_scheduler
|
||||
|
||||
if self.override_lr_scheduler:
|
||||
assert not self.use_checkpoint_lr_scheduler, "both override and use-checkpoint are set."
|
||||
|
||||
# Initialize parent class
|
||||
super().__init__(optimizer, last_epoch)
|
||||
|
||||
logger.info(f"> learning rate decay style: {self.lr_decay_style}")
|
||||
|
||||
def _get_lr_for_group(self, param_group: dict) -> float:
|
||||
"""Compute learning rate for a specific parameter group.
|
||||
|
||||
Args:
|
||||
param_group (dict): parameter group from the optimizer.
|
||||
|
||||
Returns:
|
||||
float: learning rate for this parameter group.
|
||||
"""
|
||||
max_lr = param_group.get("max_lr", self.max_lr)
|
||||
min_lr = param_group.get("min_lr", self.min_lr)
|
||||
|
||||
# Use linear warmup for the initial part.
|
||||
if self.lr_warmup_steps > 0 and self.last_epoch <= self.lr_warmup_steps:
|
||||
return self.init_lr + ((max_lr - self.init_lr) * float(self.last_epoch) / float(self.lr_warmup_steps))
|
||||
|
||||
# If the learning rate is constant, just return the initial value.
|
||||
if self.lr_decay_style == "constant":
|
||||
return max_lr
|
||||
|
||||
# For any steps larger than `self.lr_decay_steps`, use `min_lr`.
|
||||
if self.last_epoch > self.lr_decay_steps:
|
||||
return min_lr
|
||||
|
||||
# If we are done with the warmup period, use the decay style.
|
||||
if self.lr_decay_style == "inverse-square-root":
|
||||
warmup_steps = max(self.lr_warmup_steps, 1)
|
||||
num_steps = max(self.last_epoch, 1)
|
||||
lr = max_lr * warmup_steps**0.5 / (num_steps**0.5)
|
||||
return max(min_lr, lr)
|
||||
|
||||
num_steps_ = self.last_epoch - self.lr_warmup_steps
|
||||
decay_steps_ = self.lr_decay_steps - self.lr_warmup_steps
|
||||
decay_ratio = float(num_steps_) / float(decay_steps_)
|
||||
assert decay_ratio >= 0.0
|
||||
assert decay_ratio <= 1.0
|
||||
|
||||
delta_lr = max_lr - min_lr
|
||||
coeff = None
|
||||
|
||||
if self.lr_decay_style == "linear":
|
||||
coeff = 1.0 - decay_ratio
|
||||
elif self.lr_decay_style == "cosine":
|
||||
coeff = 0.5 * (math.cos(math.pi * decay_ratio) + 1.0)
|
||||
elif self.lr_decay_style == "WSD":
|
||||
wsd_anneal_start_ = self.lr_decay_steps - self.wsd_decay_steps
|
||||
if self.last_epoch <= wsd_anneal_start_:
|
||||
coeff = 1.0
|
||||
else:
|
||||
wsd_steps = self.last_epoch - wsd_anneal_start_
|
||||
wsd_decay_ratio = float(wsd_steps) / float(self.wsd_decay_steps)
|
||||
if self.lr_wsd_decay_style == "linear":
|
||||
coeff = 1.0 - wsd_decay_ratio
|
||||
elif self.lr_wsd_decay_style == "cosine":
|
||||
coeff = 0.5 * (math.cos(math.pi * wsd_decay_ratio) + 1.0)
|
||||
elif self.lr_wsd_decay_style == "exponential":
|
||||
coeff = (2.0 * math.pow(0.5, wsd_decay_ratio)) - 1.0
|
||||
elif self.lr_wsd_decay_style == "minus_sqrt":
|
||||
coeff = 1.0 - math.sqrt(wsd_decay_ratio)
|
||||
else:
|
||||
raise Exception(f"{self.lr_decay_style} decay style is not supported.")
|
||||
|
||||
assert coeff is not None
|
||||
return min_lr + coeff * delta_lr
|
||||
|
||||
@override
|
||||
def get_lr(self) -> list[float]:
|
||||
"""Compute the learning rates for each parameter group.
|
||||
|
||||
Returns:
|
||||
list[float]: A list of learning rates, one for each parameter group.
|
||||
"""
|
||||
return [self._get_lr_for_group(group) for group in self.optimizer.param_groups]
|
||||
|
||||
|
||||
def get_lr_scheduler(args, optimizer: torch.optim.Optimizer) -> FSDPLRScheduler:
|
||||
"""Create and configure the learning-rate scheduler.
|
||||
|
||||
This configures iteration-based schedules derived from the global batch size
|
||||
and run-time arguments.
|
||||
|
||||
Args:
|
||||
args: Training/runtime arguments (namespace).
|
||||
optimizer (torch.optim.Optimizer): Optimizer bound to the model.
|
||||
|
||||
Returns:
|
||||
FSDPLRScheduler: Initialized scheduler bound to ``optimizer``.
|
||||
"""
|
||||
args.train_iters = args.num_rollout * args.rollout_batch_size * args.n_samples_per_prompt // args.global_batch_size
|
||||
if args.lr_decay_iters is None:
|
||||
args.lr_decay_iters = args.train_iters
|
||||
lr_decay_steps = args.lr_decay_iters
|
||||
wsd_decay_steps = None
|
||||
if args.lr_wsd_decay_iters is not None:
|
||||
wsd_decay_steps = args.lr_wsd_decay_iters
|
||||
if args.lr_warmup_fraction is not None:
|
||||
lr_warmup_steps = args.lr_warmup_fraction * lr_decay_steps
|
||||
else:
|
||||
lr_warmup_steps = args.lr_warmup_iters
|
||||
lr_scheduler = FSDPLRScheduler(
|
||||
optimizer,
|
||||
init_lr=args.lr_warmup_init,
|
||||
max_lr=args.lr,
|
||||
min_lr=args.min_lr,
|
||||
lr_warmup_steps=lr_warmup_steps,
|
||||
lr_decay_steps=lr_decay_steps,
|
||||
lr_decay_style=args.lr_decay_style,
|
||||
use_checkpoint_lr_scheduler=args.use_checkpoint_lr_scheduler,
|
||||
override_lr_scheduler=args.override_lr_scheduler,
|
||||
wsd_decay_steps=wsd_decay_steps,
|
||||
lr_wsd_decay_style=args.lr_wsd_decay_style,
|
||||
)
|
||||
|
||||
return lr_scheduler
|
||||
3
slime/backends/fsdp_utils/models/__init__.py
Normal file
3
slime/backends/fsdp_utils/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
128
slime/backends/fsdp_utils/models/qwen3_moe.py
Normal file
128
slime/backends/fsdp_utils/models/qwen3_moe.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeMLP
|
||||
|
||||
from slime.backends.fsdp_utils.kernels.fused_experts import (
|
||||
DownProjFunction,
|
||||
GateUpProjFunction,
|
||||
MoeSumReduceFunction,
|
||||
SiluAndMulFunction,
|
||||
)
|
||||
|
||||
|
||||
def fused_experts_impl(
|
||||
hidden_states: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
):
|
||||
assert hidden_states.shape[1] == w1.shape[2], "Hidden size mismatch"
|
||||
assert topk_weights.shape == topk_ids.shape, "topk shape mismatch"
|
||||
assert hidden_states.is_contiguous(), "Hidden_states must be contiguous"
|
||||
assert w1.is_contiguous(), "Expert weights1 must be contiguous"
|
||||
assert w2.is_contiguous(), "Expert weights2 must be contiguous"
|
||||
assert hidden_states.dtype in [torch.bfloat16]
|
||||
|
||||
intermediate_cache1 = GateUpProjFunction.apply(
|
||||
hidden_states,
|
||||
w1,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
)
|
||||
intermediate_cache2 = SiluAndMulFunction.apply(intermediate_cache1)
|
||||
intermediate_cache3 = DownProjFunction.apply(
|
||||
intermediate_cache2,
|
||||
w2,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
)
|
||||
output_hidden_states = MoeSumReduceFunction.apply(
|
||||
intermediate_cache3,
|
||||
hidden_states.shape,
|
||||
)
|
||||
return output_hidden_states
|
||||
|
||||
|
||||
class StandardDispatcher:
|
||||
def __init__(self, num_experts: int, num_local_experts: int):
|
||||
self.moe_ep_size = 1
|
||||
self.num_experts = num_experts
|
||||
self.num_local_experts = num_local_experts
|
||||
self.moe_ep_rank = 0
|
||||
self.local_expert_mapping = None
|
||||
|
||||
if self.moe_ep_size > 1:
|
||||
self.local_expert_mapping = torch.full((self.num_experts,), -1, dtype=torch.int32, device="cuda")
|
||||
self.local_expert_mapping[
|
||||
self.moe_ep_rank * self.num_local_experts : (self.moe_ep_rank + 1) * self.num_local_experts
|
||||
] = torch.arange(0, self.num_local_experts, dtype=torch.int32, device="cuda")
|
||||
|
||||
def dispatch(self, topk_ids) -> torch.Tensor:
|
||||
if self.local_expert_mapping is not None:
|
||||
return self.local_expert_mapping[topk_ids]
|
||||
return topk_ids
|
||||
|
||||
|
||||
class Qwen3MoeSparseMoeBlock(nn.Module):
|
||||
dispatcher = None
|
||||
runner = None
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
self.num_experts = config.num_experts
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.norm_topk_prob = config.norm_topk_prob
|
||||
|
||||
# gating
|
||||
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
|
||||
|
||||
self.experts = nn.ModuleList(
|
||||
[Qwen3MoeMLP(config, intermediate_size=config.moe_intermediate_size) for _ in range(self.num_experts)]
|
||||
)
|
||||
|
||||
if Qwen3MoeSparseMoeBlock.dispatcher is None:
|
||||
Qwen3MoeSparseMoeBlock.dispatcher = StandardDispatcher(
|
||||
num_experts=config.num_experts, num_local_experts=config.num_experts
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
batch_size, sequence_length, hidden_dim = hidden_states.shape
|
||||
hidden_states = hidden_states.view(-1, hidden_dim)
|
||||
# router_logits: (batch * sequence_length, n_experts)
|
||||
router_logits = self.gate(hidden_states)
|
||||
|
||||
routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float)
|
||||
routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
|
||||
|
||||
if self.norm_topk_prob: # only diff with mixtral sparse moe block!
|
||||
routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
|
||||
# we cast back to the input dtype
|
||||
routing_weights = routing_weights.to(hidden_states.dtype)
|
||||
|
||||
selected_experts = Qwen3MoeSparseMoeBlock.dispatcher.dispatch(selected_experts)
|
||||
|
||||
w13_weight = torch.stack(
|
||||
[torch.cat([layer.gate_proj.weight, layer.up_proj.weight], dim=0) for layer in self.experts]
|
||||
)
|
||||
w2_weight = torch.stack([layer.down_proj.weight for layer in self.experts], dim=0)
|
||||
|
||||
final_hidden_states = fused_experts_impl(
|
||||
hidden_states.to(torch.bfloat16),
|
||||
w13_weight,
|
||||
w2_weight,
|
||||
routing_weights,
|
||||
selected_experts,
|
||||
)
|
||||
|
||||
return final_hidden_states, router_logits
|
||||
|
||||
|
||||
def apply_true_on_policy_patch_for_qwen3_moe():
|
||||
from transformers.models.qwen3_moe import modeling_qwen3_moe
|
||||
|
||||
modeling_qwen3_moe.Qwen3MoeSparseMoeBlock = Qwen3MoeSparseMoeBlock
|
||||
46
slime/backends/fsdp_utils/models/qwen3_moe_hf.py
Normal file
46
slime/backends/fsdp_utils/models/qwen3_moe_hf.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def apply_fsdp_moe_patch():
|
||||
|
||||
from transformers.models.qwen3_moe import modeling_qwen3_moe
|
||||
|
||||
def _forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
batch_size, sequence_length, hidden_dim = hidden_states.shape
|
||||
hidden_states = hidden_states.view(-1, hidden_dim)
|
||||
router_logits = self.gate(hidden_states)
|
||||
|
||||
routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float)
|
||||
routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
|
||||
if self.norm_topk_prob:
|
||||
routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
|
||||
routing_weights = routing_weights.to(hidden_states.dtype)
|
||||
|
||||
final_hidden_states = torch.zeros(
|
||||
(batch_size * sequence_length, hidden_dim), dtype=hidden_states.dtype, device=hidden_states.device
|
||||
)
|
||||
|
||||
expert_mask = torch.nn.functional.one_hot(selected_experts, num_classes=self.num_experts).permute(2, 1, 0)
|
||||
|
||||
# Loop over all experts
|
||||
for expert_idx in range(self.num_experts):
|
||||
expert_layer = self.experts[expert_idx]
|
||||
idx, top_x = torch.where(expert_mask[expert_idx])
|
||||
|
||||
if top_x.numel() > 0:
|
||||
current_state = hidden_states[None, top_x].reshape(-1, hidden_dim)
|
||||
current_hidden_states = expert_layer(current_state) * routing_weights[top_x, idx, None]
|
||||
final_hidden_states.index_add_(0, top_x, current_hidden_states.to(hidden_states.dtype))
|
||||
else:
|
||||
# force experts to participate in computation graph
|
||||
dummy_output = expert_layer(hidden_states[:1]) * 0.0
|
||||
final_hidden_states[:1] = final_hidden_states[:1] + dummy_output
|
||||
|
||||
final_hidden_states = final_hidden_states.reshape(batch_size, sequence_length, hidden_dim)
|
||||
return final_hidden_states, router_logits
|
||||
|
||||
modeling_qwen3_moe.Qwen3MoeSparseMoeBlock.forward = _forward
|
||||
261
slime/backends/fsdp_utils/update_weight_utils.py
Normal file
261
slime/backends/fsdp_utils/update_weight_utils.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import socket
|
||||
from argparse import Namespace
|
||||
from collections.abc import Sequence
|
||||
|
||||
import ray
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from ray.actor import ActorHandle
|
||||
from torch.distributed.tensor import DTensor, Replicate
|
||||
|
||||
try:
|
||||
from sglang.srt.utils.patch_torch import monkey_patch_torch_reductions # type: ignore[import]
|
||||
except ImportError:
|
||||
from sglang.srt.patch_torch import monkey_patch_torch_reductions # type: ignore[import]
|
||||
|
||||
from sglang.srt.utils import MultiprocessingSerializer
|
||||
|
||||
from slime.utils.distributed_utils import init_process_group
|
||||
|
||||
|
||||
try:
|
||||
from sglang.srt.weight_sync.tensor_bucket import FlattenedTensorBucket # type: ignore[import]
|
||||
except ImportError:
|
||||
from sglang.srt.model_executor.model_runner import FlattenedTensorBucket # type: ignore[import]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateWeight(abc.ABC):
|
||||
def __init__(self, args: Namespace, model: torch.nn.Module) -> None:
|
||||
self.args = args
|
||||
self.model = model
|
||||
self.weight_version = 0
|
||||
|
||||
@abc.abstractmethod
|
||||
def connect_rollout_engines(
|
||||
self,
|
||||
rollout_engines: Sequence[ActorHandle],
|
||||
rollout_engine_lock: ActorHandle | None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def update_weights(self) -> None:
|
||||
self.weight_version += 1
|
||||
bucket = []
|
||||
bucket_size = 0
|
||||
for name, param in self.model.state_dict().items():
|
||||
param_size = param.numel() * param.element_size()
|
||||
if bucket and bucket_size + param_size >= self.args.update_weight_buffer_size:
|
||||
self.wait_and_update_bucket_weights(bucket)
|
||||
del bucket
|
||||
bucket = []
|
||||
bucket_size = 0
|
||||
|
||||
param = param.cuda()
|
||||
if isinstance(param, DTensor):
|
||||
# async version of param.full_tensor
|
||||
param = param.redistribute(
|
||||
placements=[Replicate()] * param.device_mesh.ndim,
|
||||
async_op=True,
|
||||
).to_local()
|
||||
bucket.append((name, param))
|
||||
bucket_size += param_size
|
||||
|
||||
if bucket:
|
||||
self.wait_and_update_bucket_weights(bucket)
|
||||
del bucket
|
||||
bucket = []
|
||||
bucket_size = 0
|
||||
|
||||
def wait_and_update_bucket_weights(self, bucket):
|
||||
bucket = [(name, param.wait()) if hasattr(param, "wait") else (name, param) for name, param in bucket]
|
||||
self.update_bucket_weights(bucket, weight_version=self.weight_version)
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_bucket_weights(self, named_tensors, weight_version=None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class UpdateWeightFromTensor(UpdateWeight):
|
||||
"""Push model weights to rollout engines using tensors.
|
||||
|
||||
Streams parameters in size-bounded buckets; optionally groups tensors by dtype
|
||||
and flattens per dtype, gathers per-rank blobs to the source, and issues one
|
||||
RPC per dtype per bucket (or one per bucket if not flattened).
|
||||
"""
|
||||
|
||||
def connect_rollout_engines(
|
||||
self,
|
||||
rollout_engines: Sequence[ActorHandle],
|
||||
rollout_engine_lock: ActorHandle | None,
|
||||
) -> None:
|
||||
"""Attach rollout engines and create per-engine IPC (Gloo) groups.
|
||||
|
||||
Sets the gather source rank, engine handle, and `tp_rank` within the
|
||||
engine's local group.
|
||||
"""
|
||||
self.rollout_engines = rollout_engines
|
||||
|
||||
# Here we assume the gpu id of rollout engines and train actors are the same.
|
||||
for i, engine in enumerate(self.rollout_engines):
|
||||
start_rank = i * self.args.rollout_num_gpus_per_engine
|
||||
end_rank = (i + 1) * self.args.rollout_num_gpus_per_engine
|
||||
group_ranks = list(range(start_rank, end_rank))
|
||||
new_group = dist.new_group(
|
||||
ranks=group_ranks,
|
||||
backend="gloo",
|
||||
)
|
||||
if dist.get_rank() in group_ranks:
|
||||
self._ipc_gather_src = start_rank
|
||||
self._ipc_gather_group = new_group
|
||||
self._ipc_engine = engine
|
||||
# Calculate TP rank within this SGLang engine group
|
||||
self.tp_rank = dist.get_rank() - start_rank
|
||||
|
||||
def update_bucket_weights(self, named_tensors, weight_version=None) -> None:
|
||||
monkey_patch_torch_reductions()
|
||||
# Use flattened bucket approach similar to Megatron
|
||||
logger.info("Using flattened tensor bucket")
|
||||
# Group tensors by dtype (same as Megatron)
|
||||
named_tensors_by_dtypes = {}
|
||||
for name, tensor in named_tensors:
|
||||
dtype = tensor.dtype
|
||||
if dtype not in named_tensors_by_dtypes:
|
||||
named_tensors_by_dtypes[dtype] = []
|
||||
named_tensors_by_dtypes[dtype].append((name, tensor))
|
||||
|
||||
# Create flattened bucket for each dtype group
|
||||
serialized_tensors = []
|
||||
for _dtype, named_tensors in named_tensors_by_dtypes.items():
|
||||
flattened_tensor_bucket = FlattenedTensorBucket(named_tensors=named_tensors)
|
||||
metadata = flattened_tensor_bucket.get_metadata()
|
||||
flattened_tensor_data = {
|
||||
"flattened_tensor": flattened_tensor_bucket.get_flattened_tensor(),
|
||||
"metadata": metadata,
|
||||
}
|
||||
serialized_tensors.append(MultiprocessingSerializer.serialize(flattened_tensor_data, output_str=True))
|
||||
|
||||
if self._ipc_gather_src == dist.get_rank():
|
||||
# On rank 0, prepare a list to hold the gathered batches from all ranks.
|
||||
gathered_serialized_batches = [None for _ in range(dist.get_world_size(self._ipc_gather_group))]
|
||||
else:
|
||||
gathered_serialized_batches = None
|
||||
|
||||
# Gather the serialized batches from all ranks to rank 0.
|
||||
dist.gather_object(
|
||||
obj=serialized_tensors,
|
||||
object_gather_list=gathered_serialized_batches,
|
||||
dst=self._ipc_gather_src,
|
||||
group=self._ipc_gather_group,
|
||||
)
|
||||
|
||||
if dist.get_rank() == self._ipc_gather_src:
|
||||
# Handle flattened bucket format (same as Megatron approach)
|
||||
# Each rank may have multiple dtype buckets
|
||||
# TODO: here we assume all ranks have the same number of dtypes
|
||||
num_dtypes = len(gathered_serialized_batches[0])
|
||||
assert num_dtypes > 0
|
||||
for i in range(num_dtypes):
|
||||
kwargs = {
|
||||
"serialized_named_tensors": [tensors[i] for tensors in gathered_serialized_batches],
|
||||
"load_format": "flattened_bucket",
|
||||
"flush_cache": False,
|
||||
"weight_version": str(weight_version),
|
||||
}
|
||||
ref = self._ipc_engine.update_weights_from_tensor.remote(**kwargs)
|
||||
ray.get(ref)
|
||||
|
||||
if dist.get_rank() == self._ipc_gather_src:
|
||||
ref = self._ipc_engine.flush_cache.remote()
|
||||
ray.get(ref)
|
||||
|
||||
|
||||
class UpdateWeightFromDistributed(UpdateWeight):
|
||||
"""Broadcast weights via a temporary NCCL group to rollout engines."""
|
||||
|
||||
def connect_rollout_engines(
|
||||
self,
|
||||
rollout_engines: Sequence[ActorHandle],
|
||||
rollout_engine_lock: ActorHandle | None,
|
||||
) -> None:
|
||||
"""On rank 0, initialize a temporary NCCL group for parameter broadcast."""
|
||||
self.rollout_engines = rollout_engines
|
||||
self.rollout_engine_lock = rollout_engine_lock
|
||||
|
||||
# For TP:
|
||||
# 1. AllGather parameters to rank 0
|
||||
# 2. Broadcast parameters from rank 0 to all sglang engines
|
||||
self._is_src_rank = dist.get_rank() == 0
|
||||
if self._is_src_rank:
|
||||
self._group_name = "slime"
|
||||
master_address = ray._private.services.get_node_ip_address()
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("", 0))
|
||||
master_port = sock.getsockname()[1]
|
||||
## TODO: why +1?
|
||||
world_size = self.args.rollout_num_gpus + 1
|
||||
|
||||
refs = [
|
||||
engine.init_weights_update_group.remote(
|
||||
master_address,
|
||||
master_port,
|
||||
i * self.args.rollout_num_gpus_per_engine + 1,
|
||||
world_size,
|
||||
self._group_name,
|
||||
backend="nccl",
|
||||
)
|
||||
for i, engine in enumerate(self.rollout_engines)
|
||||
]
|
||||
self._model_update_groups = init_process_group(
|
||||
backend="nccl",
|
||||
init_method=f"tcp://{master_address}:{master_port}",
|
||||
world_size=world_size,
|
||||
rank=0,
|
||||
group_name=self._group_name,
|
||||
)
|
||||
ray.get(refs)
|
||||
|
||||
def update_bucket_weights(self, named_tensors, weight_version=None) -> None:
|
||||
"""Send names/dtypes/shapes metadata to engines, then broadcast tensors.
|
||||
|
||||
Ensures tensors are contiguous; when `world_size == 1`, converts DTensors
|
||||
to full tensors prior to `dist.broadcast`.
|
||||
"""
|
||||
if not self._is_src_rank or not named_tensors:
|
||||
return
|
||||
|
||||
refs = [
|
||||
engine.update_weights_from_distributed.remote(
|
||||
names=[name for name, _ in named_tensors],
|
||||
dtypes=[param.dtype for _, param in named_tensors],
|
||||
shapes=[param.shape for _, param in named_tensors],
|
||||
group_name=self._group_name,
|
||||
weight_version=str(weight_version),
|
||||
)
|
||||
for engine in self.rollout_engines
|
||||
]
|
||||
|
||||
handles = []
|
||||
# Broadcast parameters one by one with memory management
|
||||
for _name, param in named_tensors:
|
||||
torch.cuda.empty_cache()
|
||||
# Ensure tensor is contiguous and on the right device
|
||||
param_data = param.data.contiguous()
|
||||
|
||||
# avoid `DTensor._op_dispatcher.dispatch` has `assert compute_mesh is not None` error
|
||||
if dist.get_world_size() == 1 and isinstance(param_data, DTensor):
|
||||
param_data = param_data.full_tensor()
|
||||
|
||||
# Synchronous broadcast to avoid memory buildup
|
||||
handles.append(dist.broadcast(param_data, 0, group=self._model_update_groups, async_op=True))
|
||||
|
||||
for handle in handles:
|
||||
handle.wait()
|
||||
ray.get(refs)
|
||||
45
slime/backends/megatron_utils/__init__.py
Normal file
45
slime/backends/megatron_utils/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
try:
|
||||
import deep_ep
|
||||
from torch_memory_saver import torch_memory_saver
|
||||
|
||||
old_init = deep_ep.Buffer.__init__
|
||||
|
||||
def new_init(self, *args, **kwargs):
|
||||
if torch_memory_saver._impl is not None:
|
||||
torch_memory_saver._impl._binary_wrapper.cdll.tms_set_interesting_region(False)
|
||||
old_init(self, *args, **kwargs)
|
||||
torch.cuda.synchronize()
|
||||
if torch_memory_saver._impl is not None:
|
||||
torch_memory_saver._impl._binary_wrapper.cdll.tms_set_interesting_region(True)
|
||||
|
||||
deep_ep.Buffer.__init__ = new_init
|
||||
except ImportError:
|
||||
logging.warning("deep_ep is not installed, some functionalities may be limited.")
|
||||
|
||||
try:
|
||||
from megatron.bridge.models.qwen_vl.modelling_qwen3_vl.text_model import (
|
||||
Qwen3VLMoETextRotaryEmbedding,
|
||||
Qwen3VLTextRotaryEmbedding,
|
||||
)
|
||||
|
||||
def patch_rotary_embedding(cls):
|
||||
_original_forward = cls.forward
|
||||
|
||||
def _patched_forward(self, *args, packed_seq_params=None, **kwargs):
|
||||
return _original_forward(self, *args, **kwargs)
|
||||
|
||||
cls.forward = _patched_forward
|
||||
|
||||
patch_rotary_embedding(Qwen3VLTextRotaryEmbedding)
|
||||
patch_rotary_embedding(Qwen3VLMoETextRotaryEmbedding)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
Binary file not shown.
BIN
slime/backends/megatron_utils/__pycache__/actor.cpython-312.pyc
Normal file
BIN
slime/backends/megatron_utils/__pycache__/actor.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
slime/backends/megatron_utils/__pycache__/data.cpython-312.pyc
Normal file
BIN
slime/backends/megatron_utils/__pycache__/data.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
slime/backends/megatron_utils/__pycache__/loss.cpython-312.pyc
Normal file
BIN
slime/backends/megatron_utils/__pycache__/loss.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
slime/backends/megatron_utils/__pycache__/model.cpython-312.pyc
Normal file
BIN
slime/backends/megatron_utils/__pycache__/model.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
slime/backends/megatron_utils/__pycache__/sglang.cpython-312.pyc
Normal file
BIN
slime/backends/megatron_utils/__pycache__/sglang.cpython-312.pyc
Normal file
Binary file not shown.
575
slime/backends/megatron_utils/actor.py
Normal file
575
slime/backends/megatron_utils/actor.py
Normal file
@@ -0,0 +1,575 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
from argparse import Namespace
|
||||
from contextlib import nullcontext
|
||||
|
||||
import ray
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from megatron.core import mpu
|
||||
from ray.actor import ActorHandle
|
||||
from torch_memory_saver import torch_memory_saver
|
||||
from transformers import AutoConfig, AutoTokenizer
|
||||
|
||||
from slime.ray.train_actor import TrainRayActor
|
||||
from slime.utils import train_dump_utils
|
||||
from slime.utils.context_utils import with_defer
|
||||
from slime.utils.data import process_rollout_data
|
||||
from slime.utils.distributed_utils import get_gloo_group, init_process_group
|
||||
from slime.utils.memory_utils import clear_memory, print_memory
|
||||
from slime.utils.ray_utils import Box
|
||||
from slime.utils.reloadable_process_group import destroy_process_groups, monkey_patch_torch_dist, reload_process_groups
|
||||
from slime.utils.routing_replay import RoutingReplay
|
||||
from slime.utils.timer import Timer, inverse_timer, timer
|
||||
from slime.utils.tracking_utils import init_tracking
|
||||
from slime.utils.types import RolloutBatch
|
||||
|
||||
from ...utils.profile_utils import TrainProfiler
|
||||
from ...utils.tensor_backper import TensorBackuper
|
||||
from .checkpoint import load_checkpoint
|
||||
from .cp_utils import slice_log_prob_with_cp, slice_with_cp
|
||||
from .data import DataIterator, get_data_iterator, log_perf_data, log_rollout_data, sync_actor_critic_data
|
||||
from .initialize import init, is_megatron_main_rank
|
||||
from .loss import compute_advantages_and_returns, get_log_probs_and_entropy, get_values
|
||||
from .model import forward_only, initialize_model_and_optimizer, save, train
|
||||
from .update_weight.common import named_params_and_buffers
|
||||
from .update_weight.update_weight_from_distributed import UpdateWeightFromDistributed
|
||||
from .update_weight.update_weight_from_tensor import UpdateWeightFromTensor
|
||||
|
||||
logging.getLogger("megatron").setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MegatronTrainRayActor(TrainRayActor):
|
||||
@with_defer(lambda: Timer().start("train_wait"))
|
||||
def init(
|
||||
self,
|
||||
args: Namespace,
|
||||
role: str,
|
||||
with_ref: bool = False,
|
||||
) -> int | None:
|
||||
monkey_patch_torch_dist()
|
||||
|
||||
super().init(args, role, with_ref)
|
||||
|
||||
init(args)
|
||||
|
||||
if is_megatron_main_rank():
|
||||
init_tracking(args, primary=False)
|
||||
|
||||
self.prof = TrainProfiler(args)
|
||||
|
||||
# read config and tokenizer serialized to prevent concurrent writing bug.
|
||||
for i in range(dist.get_world_size()):
|
||||
if i == dist.get_rank():
|
||||
self.hf_config = AutoConfig.from_pretrained(args.hf_checkpoint, trust_remote_code=True)
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(self.args.hf_checkpoint, trust_remote_code=True)
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
self.train_parallel_config = {
|
||||
"dp_size": mpu.get_data_parallel_world_size(with_context_parallel=False),
|
||||
}
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
if args.offload_train:
|
||||
if (x := args.train_memory_margin_bytes) > 0:
|
||||
logger.info(f"Set torch_memory_saver.memory_margin_bytes to {x}")
|
||||
torch_memory_saver.memory_margin_bytes = x
|
||||
|
||||
if self.args.debug_rollout_only:
|
||||
return 0
|
||||
|
||||
if role == "critic":
|
||||
self.args.load = self.args.critic_load
|
||||
self.args.save = self.args.critic_save
|
||||
self.args.lr = self.args.critic_lr
|
||||
self.args.lr_warmup_iters = self.args.critic_lr_warmup_iters
|
||||
|
||||
(self.model, self.optimizer, self.opt_param_scheduler, loaded_rollout_id) = initialize_model_and_optimizer(
|
||||
args, role
|
||||
)
|
||||
|
||||
if role == "critic":
|
||||
if self.args.offload_train:
|
||||
self.sleep()
|
||||
return
|
||||
|
||||
start_rollout_id = loaded_rollout_id + 1
|
||||
|
||||
self.weights_backuper = TensorBackuper.create(
|
||||
source_getter=lambda: named_params_and_buffers(
|
||||
self.args,
|
||||
self.model,
|
||||
convert_to_global_name=args.megatron_to_hf_mode == "raw",
|
||||
translate_gpu_to_cpu=not self.args.enable_weights_backuper,
|
||||
),
|
||||
single_tag=None if args.enable_weights_backuper else "actor",
|
||||
)
|
||||
self._active_model_tag: str | None = "actor"
|
||||
self.weights_backuper.backup("actor")
|
||||
|
||||
if with_ref:
|
||||
self.load_other_checkpoint("ref", args.ref_load)
|
||||
|
||||
if self.args.keep_old_actor:
|
||||
# Load old_actor checkpoint
|
||||
self.load_other_checkpoint("old_actor", args.load)
|
||||
# Create rollout_actor as a copy of current actor
|
||||
if args.update_weights_interval == 1:
|
||||
self.weights_backuper.backup("rollout_actor")
|
||||
|
||||
if self.args.vocab_size is None:
|
||||
self.args.vocab_size = self.tokenizer.vocab_size
|
||||
|
||||
update_weight_cls = UpdateWeightFromTensor if self.args.colocate else UpdateWeightFromDistributed
|
||||
self.weight_updater = update_weight_cls(
|
||||
self.args,
|
||||
self.model,
|
||||
weights_getter=lambda: self.weights_backuper.get("actor"),
|
||||
model_name=type(self.hf_config).__name__.lower() if self.args.model_name is None else self.args.model_name,
|
||||
quantization_config=getattr(self.hf_config, "quantization_config", None),
|
||||
)
|
||||
|
||||
# empty cache after initialization
|
||||
clear_memory()
|
||||
|
||||
if self.args.offload_train:
|
||||
# recover to actor in the end.
|
||||
self._switch_model("actor")
|
||||
self.sleep()
|
||||
|
||||
self.rollout_engines = None
|
||||
|
||||
self.rollout_data_postprocess = None
|
||||
if self.args.rollout_data_postprocess_path is not None:
|
||||
from slime.utils.misc import load_function
|
||||
|
||||
self.rollout_data_postprocess = load_function(self.args.rollout_data_postprocess_path)
|
||||
|
||||
self.prof.on_init_end()
|
||||
|
||||
return start_rollout_id
|
||||
|
||||
@timer
|
||||
def sleep(self) -> None:
|
||||
assert self.args.offload_train
|
||||
|
||||
clear_memory(clear_host_memory=True)
|
||||
print_memory("before offload model")
|
||||
destroy_process_groups()
|
||||
|
||||
torch_memory_saver.pause()
|
||||
|
||||
print_memory("after offload model")
|
||||
|
||||
@timer
|
||||
def wake_up(self) -> None:
|
||||
assert self.args.offload_train
|
||||
print_memory("before wake_up model")
|
||||
|
||||
torch_memory_saver.resume()
|
||||
|
||||
clear_memory()
|
||||
reload_process_groups()
|
||||
print_memory("after wake_up model")
|
||||
|
||||
def _get_rollout_data(self, rollout_data_ref: Box) -> RolloutBatch:
|
||||
# Fetch data through ray on CPU, not sure if this will be performance bottleneck.
|
||||
# Both first pp stage and the last pp stage will receive the data.
|
||||
rollout_data = process_rollout_data(
|
||||
self.args,
|
||||
rollout_data_ref,
|
||||
mpu.get_data_parallel_rank(with_context_parallel=False),
|
||||
mpu.get_data_parallel_world_size(with_context_parallel=False),
|
||||
)
|
||||
# TODO: this is ugly, move to somewhere else?
|
||||
# move tokens to GPU in advance
|
||||
rollout_data["tokens"] = [
|
||||
torch.tensor(t, dtype=torch.long, device=torch.cuda.current_device()) for t in rollout_data["tokens"]
|
||||
]
|
||||
rollout_data["loss_masks"] = [
|
||||
torch.tensor(t, dtype=torch.int, device=torch.cuda.current_device()) for t in rollout_data["loss_masks"]
|
||||
]
|
||||
if "multimodal_train_inputs" in rollout_data:
|
||||
# Move multimodal training tensors to GPU in advance
|
||||
rollout_data["multimodal_train_inputs"] = [
|
||||
(
|
||||
{key: tensor.to(device=torch.cuda.current_device()) for key, tensor in mm_dict.items()}
|
||||
if mm_dict is not None
|
||||
else None
|
||||
)
|
||||
for mm_dict in rollout_data["multimodal_train_inputs"]
|
||||
]
|
||||
if "rollout_log_probs" in rollout_data:
|
||||
rollout_data["rollout_log_probs"] = [
|
||||
torch.tensor(
|
||||
slice_log_prob_with_cp(log_prob, total_length, response_length),
|
||||
device=torch.cuda.current_device(),
|
||||
dtype=torch.float32,
|
||||
)
|
||||
for log_prob, total_length, response_length in zip(
|
||||
rollout_data["rollout_log_probs"],
|
||||
rollout_data["total_lengths"],
|
||||
rollout_data["response_lengths"],
|
||||
strict=False,
|
||||
)
|
||||
]
|
||||
if "rollout_routed_experts" in rollout_data:
|
||||
rollout_data["rollout_routed_experts"] = [
|
||||
torch.from_numpy(r) for r in rollout_data["rollout_routed_experts"]
|
||||
]
|
||||
return rollout_data
|
||||
|
||||
def _switch_model(self, target_tag: str) -> None:
|
||||
if target_tag not in self.weights_backuper.backup_tags:
|
||||
raise ValueError(f"Cannot switch to unknown model tag: {target_tag}")
|
||||
self.weights_backuper.restore(target_tag)
|
||||
self._active_model_tag = target_tag
|
||||
|
||||
def fill_routing_replay(self, data_iterator, num_microbatches, rollout_data):
|
||||
if "rollout_routed_experts" not in rollout_data:
|
||||
raise ValueError(
|
||||
"rollout_routed_experts is required in rollout_data when use_rollout_routing_replay is set."
|
||||
)
|
||||
|
||||
from megatron.core.transformer.transformer_block import get_num_layers_to_build
|
||||
from megatron.core.transformer.transformer_layer import get_transformer_layer_offset
|
||||
|
||||
from slime.utils.routing_replay import RoutingReplay
|
||||
|
||||
for iterator in data_iterator:
|
||||
iterator.reset()
|
||||
|
||||
tp_rank = mpu.get_tensor_model_parallel_rank()
|
||||
tp_size = mpu.get_tensor_model_parallel_world_size()
|
||||
|
||||
def pad_func(experts, pad):
|
||||
_, num_layers, topk = experts.shape
|
||||
pad = (
|
||||
torch.arange(
|
||||
pad * num_layers * topk,
|
||||
device=experts.device,
|
||||
dtype=experts.dtype,
|
||||
).reshape((pad, num_layers, topk))
|
||||
% self.args.num_experts
|
||||
)
|
||||
return torch.cat([experts, pad], dim=0)
|
||||
|
||||
for _ in range(sum(num_microbatches)):
|
||||
batch = data_iterator[0].get_next(["rollout_routed_experts", "tokens"])
|
||||
rollout_routed_experts = batch["rollout_routed_experts"]
|
||||
tokens = batch["tokens"]
|
||||
assert len(rollout_routed_experts) == len(tokens)
|
||||
for a, b in zip(rollout_routed_experts, tokens, strict=False):
|
||||
assert a.shape[0] == b.shape[0] - 1, f"{a.shape}, {b.shape}"
|
||||
|
||||
# We need to pad the experts to the last token. We won't calculate loss on this token so this should be fine.
|
||||
# TODO: fuse this padding with the following slice_with_cp to reduce memory copy.
|
||||
rollout_routed_experts = [pad_func(r, 1) for r in rollout_routed_experts]
|
||||
# TODO: maybe extract a common process function for here and get_batch?
|
||||
rollout_routed_experts = [slice_with_cp(r, pad_func) for r in rollout_routed_experts]
|
||||
rollout_routed_experts = torch.cat(rollout_routed_experts, dim=0)
|
||||
pad_size = mpu.get_tensor_model_parallel_world_size() * self.args.data_pad_size_multiplier
|
||||
pad = (pad_size - rollout_routed_experts.size(0) % pad_size) % pad_size
|
||||
if pad != 0:
|
||||
rollout_routed_experts = pad_func(rollout_routed_experts, pad)
|
||||
|
||||
if self.args.sequence_parallel:
|
||||
seqlen = rollout_routed_experts.size(0)
|
||||
assert seqlen % tp_size == 0
|
||||
start, end = seqlen // tp_size * tp_rank, seqlen // tp_size * (tp_rank + 1)
|
||||
rollout_routed_experts = rollout_routed_experts[start:end]
|
||||
|
||||
routing_replay_offset = 0
|
||||
for vp_stage, model in enumerate(self.model):
|
||||
config = model.module.config
|
||||
num_layers_to_build = get_num_layers_to_build(config, vp_stage=vp_stage)
|
||||
offset = get_transformer_layer_offset(config, vp_stage=vp_stage)
|
||||
for layer_id in range(offset, offset + num_layers_to_build):
|
||||
# skip dense layer
|
||||
if isinstance(config.moe_layer_freq, int):
|
||||
if layer_id % config.moe_layer_freq != 0:
|
||||
continue
|
||||
elif isinstance(config.moe_layer_freq, list):
|
||||
assert len(config.moe_layer_freq) == config.num_layers
|
||||
if config.moe_layer_freq[layer_id] == 0:
|
||||
continue
|
||||
layer_routed_experts = rollout_routed_experts[:, layer_id]
|
||||
RoutingReplay.all_routing_replays[routing_replay_offset].record(layer_routed_experts)
|
||||
routing_replay_offset += 1
|
||||
assert routing_replay_offset == len(RoutingReplay.all_routing_replays)
|
||||
|
||||
del rollout_data["rollout_routed_experts"]
|
||||
|
||||
for iterator in data_iterator:
|
||||
iterator.reset()
|
||||
|
||||
def compute_log_prob(
|
||||
self,
|
||||
data_iterator: list[DataIterator],
|
||||
num_microbatches: list[int],
|
||||
store_prefix: str = "",
|
||||
) -> dict[str, list[torch.Tensor]]:
|
||||
|
||||
with timer(f"{store_prefix}log_probs"):
|
||||
return forward_only(
|
||||
get_log_probs_and_entropy,
|
||||
self.args,
|
||||
self.model,
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
store_prefix=store_prefix,
|
||||
)
|
||||
|
||||
def train(self, rollout_id: int, rollout_data_ref: Box) -> None:
|
||||
if self.args.offload_train:
|
||||
self.wake_up()
|
||||
|
||||
with timer("data_preprocess"):
|
||||
rollout_data = self._get_rollout_data(rollout_data_ref)
|
||||
if self.args.debug_rollout_only:
|
||||
log_rollout_data(rollout_id, self.args, rollout_data)
|
||||
return
|
||||
|
||||
if self.role == "critic":
|
||||
return self.train_critic(rollout_id, rollout_data)
|
||||
else:
|
||||
return self.train_actor(rollout_id, rollout_data)
|
||||
|
||||
def train_critic(self, rollout_id: int, rollout_data: RolloutBatch) -> None:
|
||||
# Create data iterator for log_probs and train.
|
||||
data_iterator, num_microbatches = get_data_iterator(self.args, self.model, rollout_data)
|
||||
rollout_data.update(
|
||||
forward_only(
|
||||
get_values,
|
||||
self.args,
|
||||
self.model,
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
)
|
||||
)
|
||||
|
||||
if rollout_id >= self.args.num_critic_only_steps:
|
||||
sync_actor_critic_data(self.args, rollout_data, self._actor_critic_groups)
|
||||
|
||||
compute_advantages_and_returns(self.args, rollout_data)
|
||||
|
||||
self.args.loss_type = "value_loss"
|
||||
train(
|
||||
rollout_id,
|
||||
self.model,
|
||||
self.optimizer,
|
||||
self.opt_param_scheduler,
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
)
|
||||
|
||||
def train_actor(self, rollout_id: int, rollout_data: RolloutBatch) -> None:
|
||||
# Create data iterator for log_probs and train.
|
||||
data_iterator, num_microbatches = get_data_iterator(self.args, self.model, rollout_data)
|
||||
|
||||
if self.args.use_rollout_routing_replay:
|
||||
self.fill_routing_replay(data_iterator, num_microbatches, rollout_data)
|
||||
|
||||
with inverse_timer("train_wait"), timer("train"):
|
||||
if self.args.compute_advantages_and_returns:
|
||||
if "ref" in self.weights_backuper.backup_tags:
|
||||
if self.args.use_routing_replay:
|
||||
os.environ["ROUTING_REPLAY_STAGE"] = "fallthrough"
|
||||
self._switch_model("ref")
|
||||
rollout_data.update(
|
||||
self.compute_log_prob(
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
store_prefix="ref_",
|
||||
)
|
||||
)
|
||||
self._switch_model("old_actor" if self.args.keep_old_actor else "actor")
|
||||
if not self.args.use_rollout_logprobs or self.args.get_mismatch_metrics:
|
||||
if self.args.use_routing_replay:
|
||||
if self.args.use_rollout_routing_replay:
|
||||
os.environ["ROUTING_REPLAY_STAGE"] = "replay_forward"
|
||||
else:
|
||||
os.environ["ROUTING_REPLAY_STAGE"] = "record"
|
||||
rollout_data.update(
|
||||
self.compute_log_prob(
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
store_prefix="",
|
||||
)
|
||||
)
|
||||
if self.args.use_rollout_routing_replay:
|
||||
RoutingReplay.clear_all_forward()
|
||||
|
||||
if self.args.use_critic:
|
||||
sync_actor_critic_data(
|
||||
self.args,
|
||||
rollout_data,
|
||||
self._actor_critic_groups,
|
||||
)
|
||||
if self._active_model_tag != "actor":
|
||||
self._switch_model("actor")
|
||||
|
||||
# Calculate adv and returns. Need to performed before training (instead of on the fly),
|
||||
# because we may need normalize the whole rollout.
|
||||
compute_advantages_and_returns(self.args, rollout_data)
|
||||
|
||||
if self.rollout_data_postprocess is not None:
|
||||
self.rollout_data_postprocess(self.args)
|
||||
|
||||
log_rollout_data(rollout_id, self.args, rollout_data)
|
||||
|
||||
# Train
|
||||
if self.args.use_routing_replay:
|
||||
os.environ["ROUTING_REPLAY_STAGE"] = "replay_backward"
|
||||
with timer("actor_train"):
|
||||
train(
|
||||
rollout_id,
|
||||
self.model,
|
||||
self.optimizer,
|
||||
self.opt_param_scheduler,
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
)
|
||||
|
||||
self.prof.step(rollout_id=rollout_id)
|
||||
|
||||
train_dump_utils.save_debug_train_data(self.args, rollout_id=rollout_id, rollout_data=rollout_data)
|
||||
|
||||
if self.args.use_routing_replay:
|
||||
RoutingReplay.clear_all()
|
||||
|
||||
# update the cpu actor weight to the latest model
|
||||
self.weights_backuper.backup("actor")
|
||||
|
||||
# Update ref model if needed
|
||||
if (
|
||||
self.args.ref_update_interval is not None
|
||||
and (rollout_id + 1) % self.args.ref_update_interval == 0
|
||||
and "ref" in self.weights_backuper.backup_tags
|
||||
):
|
||||
with timer("ref_model_update"):
|
||||
if is_megatron_main_rank():
|
||||
logger.info(f"Updating ref model at rollout_id {rollout_id}")
|
||||
self.weights_backuper.backup("ref")
|
||||
|
||||
log_perf_data(rollout_id, self.args)
|
||||
|
||||
@timer
|
||||
def save_model(self, rollout_id: int, force_sync: bool = False) -> None:
|
||||
if self.args.debug_rollout_only:
|
||||
return
|
||||
|
||||
# torch dist may trigger nccl communication during saving.
|
||||
if self.args.offload_train:
|
||||
reload_process_groups()
|
||||
|
||||
if self.args.async_save:
|
||||
from megatron.training.async_utils import maybe_finalize_async_save
|
||||
|
||||
maybe_finalize_async_save(blocking=True)
|
||||
|
||||
save(rollout_id, self.model, self.optimizer, self.opt_param_scheduler)
|
||||
|
||||
if force_sync and self.args.async_save:
|
||||
maybe_finalize_async_save(blocking=True)
|
||||
|
||||
if self.args.offload_train:
|
||||
destroy_process_groups()
|
||||
|
||||
@timer
|
||||
def update_weights(self) -> None:
|
||||
if self.args.debug_train_only or self.args.debug_rollout_only:
|
||||
return
|
||||
|
||||
if self.args.offload_train:
|
||||
reload_process_groups()
|
||||
|
||||
rollout_engines, rollout_engine_lock, num_new_engines = ray.get(
|
||||
self.rollout_manager.get_rollout_engines_and_lock.remote()
|
||||
)
|
||||
if num_new_engines > 0:
|
||||
self.weight_updater.connect_rollout_engines(rollout_engines, rollout_engine_lock)
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
with torch_memory_saver.disable() if self.args.offload_train else nullcontext():
|
||||
print_memory("before update_weights")
|
||||
self.weight_updater.update_weights()
|
||||
print_memory("after update_weights")
|
||||
|
||||
if self.args.ci_test and len(rollout_engines) > 0:
|
||||
engine = random.choice(rollout_engines)
|
||||
engine_version = ray.get(engine.get_weight_version.remote())
|
||||
if str(engine_version) != str(self.weight_updater.weight_version):
|
||||
raise RuntimeError(
|
||||
f"Weight version mismatch! Engine: {engine_version}, Updater: {self.weight_updater.weight_version}"
|
||||
)
|
||||
|
||||
if getattr(self.args, "keep_old_actor", False):
|
||||
if self.args.update_weights_interval == 1:
|
||||
logger.info("updating model queue: rollout_actor -> old_actor, actor -> rollout_actor")
|
||||
# Queue-style update: rollout_actor params -> old_actor, actor params -> rollout_actor
|
||||
# First copy rollout_actor to old_actor
|
||||
self.weights_backuper.copy(src_tag="rollout_actor", dst_tag="old_actor")
|
||||
# Then copy current actor to rollout_actor
|
||||
self.weights_backuper.backup("rollout_actor")
|
||||
else:
|
||||
self.weights_backuper.backup("old_actor")
|
||||
|
||||
if self.args.offload_train:
|
||||
destroy_process_groups()
|
||||
|
||||
def load_other_checkpoint(self, model_tag: str, path: str) -> None:
|
||||
old_args = self.args.load, self.args.no_load_optim, self.args.no_load_rng, self.args.finetune
|
||||
self.args.load = path
|
||||
self.args.no_load_optim = True
|
||||
self.args.no_load_rng = True
|
||||
self.args.finetune = True
|
||||
|
||||
if model_tag == "ref" and self.args.ref_ckpt_step is not None:
|
||||
old_ckpt_step = self.args.ckpt_step
|
||||
self.args.ckpt_step = self.args.ref_ckpt_step
|
||||
|
||||
_, _ = load_checkpoint(
|
||||
self.model,
|
||||
None,
|
||||
None,
|
||||
checkpointing_context={},
|
||||
skip_load_to_model_and_opt=False,
|
||||
)
|
||||
self.args.load, self.args.no_load_optim, self.args.no_load_rng, self.args.finetune = old_args
|
||||
|
||||
if model_tag == "ref" and self.args.ref_ckpt_step is not None:
|
||||
self.args.ckpt_step = old_ckpt_step
|
||||
|
||||
self.weights_backuper.backup(model_tag)
|
||||
self._active_model_tag = model_tag
|
||||
|
||||
def connect_actor_critic(
|
||||
self,
|
||||
actor_handle: ActorHandle | None = None,
|
||||
master_address: str | None = None,
|
||||
master_port: int | None = None,
|
||||
) -> None:
|
||||
if self.role == "actor":
|
||||
master_address = ray.util.get_node_ip_address()
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("", 0))
|
||||
master_port = sock.getsockname()[1]
|
||||
actor_handle.connect_actor_critic.remote(master_address=master_address, master_port=master_port)
|
||||
|
||||
group_name = "actor_critic"
|
||||
world_size = 2
|
||||
self._actor_critic_groups = init_process_group(
|
||||
backend="nccl",
|
||||
init_method=f"tcp://{master_address}:{master_port}",
|
||||
world_size=world_size,
|
||||
rank=0 if self.role == "actor" else 1,
|
||||
group_name=group_name,
|
||||
)
|
||||
33
slime/backends/megatron_utils/arguments.py
Normal file
33
slime/backends/megatron_utils/arguments.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
|
||||
from megatron.training.arguments import parse_args, validate_args
|
||||
from megatron.training.tokenizer.tokenizer import _vocab_size_with_padding
|
||||
|
||||
__all__ = ["validate_args", "parse_args", "set_default_megatron_args"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_default_megatron_args(args):
|
||||
# always use zero optimizer
|
||||
args.use_distributed_optimizer = True
|
||||
# TODO: maybe change this after megatron has good fp8 support
|
||||
args.bf16 = not args.fp16
|
||||
# placeholders
|
||||
args.seq_length = 4096
|
||||
args.max_position_embeddings = args.seq_length
|
||||
# compatible for megatron
|
||||
if hasattr(args, "rope_type") and args.rope_type is None:
|
||||
args.rope_type = "yarn" if args.multi_latent_attention else "rope"
|
||||
|
||||
if args.vocab_size and not args.padded_vocab_size:
|
||||
args.padded_vocab_size = _vocab_size_with_padding(args.vocab_size, args)
|
||||
|
||||
if not args.tokenizer_model and not args.tokenizer_type:
|
||||
logger.info("--tokenizer-model not set, use --hf-checkpoint as tokenizer model.")
|
||||
args.tokenizer_model = args.hf_checkpoint
|
||||
args.tokenizer_type = "HuggingFaceTokenizer"
|
||||
return args
|
||||
79
slime/backends/megatron_utils/checkpoint.py
Normal file
79
slime/backends/megatron_utils/checkpoint.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# TODO: may need to copy those 2 functions and do refactoring.
|
||||
from megatron.training.checkpointing import load_checkpoint as _load_checkpoint_megatron
|
||||
from megatron.training.checkpointing import save_checkpoint
|
||||
from megatron.training.global_vars import get_args
|
||||
|
||||
from slime.utils import megatron_bridge_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ["save_checkpoint"]
|
||||
|
||||
|
||||
def load_checkpoint(ddp_model, optimizer, opt_param_scheduler, checkpointing_context, skip_load_to_model_and_opt):
|
||||
# ref: how megatron `load_checkpoint` gets directory
|
||||
args = get_args()
|
||||
load_path = args.load
|
||||
|
||||
assert Path(load_path).exists() and _is_dir_nonempty(
|
||||
load_path
|
||||
), f"{args.load=} does not exist or is an empty directory. Did you specify the wrong folder?"
|
||||
|
||||
if _is_megatron_checkpoint(load_path):
|
||||
return _load_checkpoint_megatron(
|
||||
ddp_model=ddp_model,
|
||||
optimizer=optimizer,
|
||||
opt_param_scheduler=opt_param_scheduler,
|
||||
checkpointing_context=checkpointing_context,
|
||||
skip_load_to_model_and_opt=skip_load_to_model_and_opt,
|
||||
)
|
||||
else:
|
||||
return _load_checkpoint_hf(
|
||||
ddp_model=ddp_model,
|
||||
optimizer=optimizer,
|
||||
args=args,
|
||||
load_path=load_path,
|
||||
)
|
||||
|
||||
|
||||
def _is_megatron_checkpoint(path: str | Path) -> bool:
|
||||
return (Path(path) / "latest_checkpointed_iteration.txt").is_file() or bool(
|
||||
re.fullmatch(r"iter_\d{7}", Path(path).name)
|
||||
)
|
||||
|
||||
|
||||
def _load_checkpoint_hf(ddp_model, optimizer, args, load_path: str):
|
||||
assert args.megatron_to_hf_mode == "bridge", "Only bridge mode is supported for loading HF checkpoint"
|
||||
from megatron.bridge import AutoBridge
|
||||
|
||||
import slime_plugins.megatron_bridge # noqa: F401
|
||||
|
||||
logger.info(f"Load checkpoint from HuggingFace model into Megatron (path={load_path})")
|
||||
|
||||
with megatron_bridge_utils.patch_megatron_model(ddp_model):
|
||||
bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True)
|
||||
bridge.load_hf_weights(ddp_model)
|
||||
|
||||
# Copied from Megatron-core :: load_checkpoint (with simplifications)
|
||||
if (args.fp16 or args.bf16) and optimizer is not None:
|
||||
assert not args.load_main_params_from_ckpt
|
||||
optimizer.reload_model_params()
|
||||
|
||||
# We can see `successfully loaded checkpoint from ... [ t 1/2, p 1/1 ] at iteration 0`
|
||||
# when loading Megatron, thus it is 0
|
||||
iteration = 0
|
||||
num_floating_point_operations_so_far = 0
|
||||
return iteration, num_floating_point_operations_so_far
|
||||
|
||||
|
||||
def _is_dir_nonempty(path):
|
||||
with os.scandir(path) as it:
|
||||
return any(it)
|
||||
87
slime/backends/megatron_utils/ci_utils.py
Normal file
87
slime/backends/megatron_utils/ci_utils.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""CI utilities for Megatron backend testing."""
|
||||
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
|
||||
from megatron.core.distributed import DistributedDataParallel as DDP
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_mtp_only_grad(model: Sequence[DDP], step_id: int) -> None:
|
||||
"""Check that only MTP parameters have non-zero gradients.
|
||||
|
||||
This is used for CI testing to verify that when all outputs are truncated,
|
||||
only the MTP layers receive gradients (since only mtp_loss contributes).
|
||||
|
||||
Args:
|
||||
model: Sequence of DDP-wrapped model chunks.
|
||||
step_id: Current step index for logging.
|
||||
|
||||
Raises:
|
||||
AssertionError: If any non-MTP parameter has a non-zero gradient.
|
||||
"""
|
||||
non_mtp_nonzero_grads = []
|
||||
mtp_nonzero_grads = []
|
||||
|
||||
for model_chunk in model:
|
||||
for name, param in model_chunk.named_parameters():
|
||||
# Get the main_grad from the distributed optimizer if available
|
||||
grad = getattr(param, "main_grad", None)
|
||||
if grad is None:
|
||||
grad = param.grad
|
||||
if grad is None:
|
||||
continue
|
||||
|
||||
grad_norm = grad.abs().max().item()
|
||||
is_mtp = ".mtp." in name
|
||||
|
||||
if is_mtp:
|
||||
if grad_norm > 0:
|
||||
mtp_nonzero_grads.append((name, grad_norm))
|
||||
else:
|
||||
if grad_norm > 0:
|
||||
non_mtp_nonzero_grads.append((name, grad_norm))
|
||||
|
||||
# Log the results
|
||||
logger.info(
|
||||
f"[CI MTP Grad Check] Step {step_id}: "
|
||||
f"MTP params with non-zero grad: {len(mtp_nonzero_grads)}, "
|
||||
f"non-MTP params with non-zero grad: {len(non_mtp_nonzero_grads)}"
|
||||
)
|
||||
|
||||
if non_mtp_nonzero_grads:
|
||||
# Log the first few non-MTP params with non-zero gradients for debugging
|
||||
for name, grad_norm in non_mtp_nonzero_grads[:5]:
|
||||
logger.error(f"[CI MTP Grad Check] Non-MTP param with non-zero grad: {name}, max_grad={grad_norm}")
|
||||
|
||||
assert len(non_mtp_nonzero_grads) == 0, (
|
||||
f"Expected all non-MTP parameters to have zero gradients, "
|
||||
f"but found {len(non_mtp_nonzero_grads)} with non-zero gradients. "
|
||||
f"First few: {non_mtp_nonzero_grads[:5]}"
|
||||
)
|
||||
|
||||
# Also verify that MTP params do have gradients (otherwise the test is not valid)
|
||||
assert len(mtp_nonzero_grads) > 0, (
|
||||
"Expected MTP parameters to have non-zero gradients, but all were zero. "
|
||||
"This may indicate the MTP loss is not being computed."
|
||||
)
|
||||
|
||||
|
||||
def check_mtp_loss(mtp_loss: float, max_mtp_loss: float = 1.0) -> None:
|
||||
"""Check that MTP loss is within expected bounds.
|
||||
|
||||
Args:
|
||||
mtp_loss: The computed MTP loss value.
|
||||
max_mtp_loss: Maximum allowed MTP loss (default: 1.0).
|
||||
|
||||
Raises:
|
||||
AssertionError: If MTP loss exceeds the maximum allowed value.
|
||||
"""
|
||||
assert mtp_loss < max_mtp_loss, (
|
||||
f"MTP loss {mtp_loss} exceeds maximum allowed value {max_mtp_loss}. "
|
||||
"This may indicate an issue with MTP training."
|
||||
)
|
||||
210
slime/backends/megatron_utils/cp_utils.py
Normal file
210
slime/backends/megatron_utils/cp_utils.py
Normal file
@@ -0,0 +1,210 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn.functional as F
|
||||
from megatron.core import mpu
|
||||
|
||||
|
||||
def get_logits_and_tokens_offset_with_cp(
|
||||
total_length: int,
|
||||
response_length: int,
|
||||
):
|
||||
"""
|
||||
All offsets start from the begining of the prompt.
|
||||
"""
|
||||
cp_rank = mpu.get_context_parallel_rank()
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
assert cp_size > 1
|
||||
|
||||
prompt_length = total_length - response_length
|
||||
chunk_size = (total_length + 2 * cp_size - 1) // (2 * cp_size)
|
||||
|
||||
# the offset of 2 chunks
|
||||
chunk_0 = (cp_rank * chunk_size, (cp_rank + 1) * chunk_size)
|
||||
chunk_1 = ((2 * cp_size - cp_rank - 1) * chunk_size, (2 * cp_size - cp_rank) * chunk_size)
|
||||
|
||||
# the offset of 2 logits, note that the logits need a "-1".
|
||||
logits_0 = (max(chunk_0[0], prompt_length - 1), min(chunk_0[1], total_length - 1))
|
||||
logits_1 = (max(chunk_1[0], prompt_length - 1), min(chunk_1[1], total_length - 1))
|
||||
|
||||
# when the sequence is empty, make an empty slice to continue the gradient flow.
|
||||
if logits_0[0] < logits_0[1]:
|
||||
token_0 = (logits_0[0] + 1, logits_0[1] + 1)
|
||||
else:
|
||||
logits_0 = (0, 0)
|
||||
token_0 = (0, 0)
|
||||
|
||||
if logits_1[0] < logits_1[1]:
|
||||
token_1 = (logits_1[0] + 1, logits_1[1] + 1)
|
||||
else:
|
||||
logits_1 = (0, 0)
|
||||
token_1 = (0, 0)
|
||||
|
||||
return chunk_size, (chunk_0, chunk_1), (logits_0, logits_1), (token_0, token_1)
|
||||
|
||||
|
||||
def get_sum_of_sample_mean(
|
||||
total_lengths: list[int],
|
||||
response_lengths: list[int],
|
||||
loss_masks: list[torch.Tensor],
|
||||
calculate_per_token_loss: bool = False,
|
||||
) -> Callable[[torch.Tensor], torch.Tensor]:
|
||||
"""
|
||||
Calculate correct sample mean for CP
|
||||
"""
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
if cp_size == 1:
|
||||
|
||||
def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor:
|
||||
return sum(
|
||||
[
|
||||
(x_i * loss_mask_i).sum() / torch.clamp_min(loss_mask_i.sum(), 1)
|
||||
for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False)
|
||||
]
|
||||
)
|
||||
|
||||
def sum_of_token(x: torch.Tensor) -> torch.Tensor:
|
||||
return sum(
|
||||
[
|
||||
(x_i * loss_mask_i).sum()
|
||||
for x_i, loss_mask_i in zip(x.split(response_lengths, dim=0), loss_masks, strict=False)
|
||||
]
|
||||
)
|
||||
|
||||
else:
|
||||
cp_chunk_lengths = []
|
||||
chunked_loss_masks = []
|
||||
for i, (total_length, response_length, loss_mask) in enumerate(
|
||||
zip(total_lengths, response_lengths, loss_masks, strict=False)
|
||||
):
|
||||
prompt_length = total_length - response_length
|
||||
_, _, _, tokens_offset = get_logits_and_tokens_offset_with_cp(total_length, response_length)
|
||||
loss_mask_0 = loss_mask[tokens_offset[0][0] - prompt_length : tokens_offset[0][1] - prompt_length]
|
||||
loss_mask_1 = loss_mask[tokens_offset[1][0] - prompt_length : tokens_offset[1][1] - prompt_length]
|
||||
chunked_loss_masks.append(torch.cat([loss_mask_0, loss_mask_1], dim=0))
|
||||
cp_chunk_lengths.append(chunked_loss_masks[i].size(0))
|
||||
|
||||
def sum_of_sample_mean(x: torch.Tensor) -> torch.Tensor:
|
||||
return sum(
|
||||
[
|
||||
(x_i * chunked_loss_mask).sum() / torch.clamp_min(loss_mask.sum(), 1)
|
||||
for x_i, chunked_loss_mask, loss_mask in zip(
|
||||
x.split(cp_chunk_lengths, dim=0), chunked_loss_masks, loss_masks, strict=False
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def sum_of_token(x: torch.Tensor) -> torch.Tensor:
|
||||
return sum(
|
||||
[
|
||||
(x_i * chunked_loss_mask).sum()
|
||||
for x_i, chunked_loss_mask in zip(
|
||||
x.split(cp_chunk_lengths, dim=0), chunked_loss_masks, strict=False
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return sum_of_sample_mean if not calculate_per_token_loss else sum_of_token
|
||||
|
||||
|
||||
def all_gather_with_cp(tensor: torch.Tensor, total_length: int, response_length: int) -> torch.Tensor:
|
||||
"""
|
||||
Gather tensors across all ranks in the context parallel group.
|
||||
The first dimension of the output tensor will be the `response_length`.
|
||||
"""
|
||||
cp_group = mpu.get_context_parallel_group()
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
|
||||
if cp_size == 1:
|
||||
return tensor
|
||||
|
||||
_, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp(total_length, response_length)
|
||||
|
||||
prompt_length = total_length - response_length
|
||||
|
||||
chunk_0 = tensor[: logits_offset[0][1] - logits_offset[0][0]]
|
||||
chunk_1 = tensor[logits_offset[0][1] - logits_offset[0][0] :]
|
||||
assert chunk_1.shape[0] == logits_offset[1][1] - logits_offset[1][0]
|
||||
|
||||
def zero(len: int) -> torch.Tensor:
|
||||
return torch.zeros(
|
||||
[len] + list(tensor.shape[1:]),
|
||||
dtype=tensor.dtype,
|
||||
device=tensor.device,
|
||||
requires_grad=True,
|
||||
)
|
||||
|
||||
# logprob should be within the range of [prompt_length - 1, total_length - 1]
|
||||
if chunk_0.shape[0] == 0 and chunk_1.shape[0] == 0:
|
||||
# all empty
|
||||
full_tensor = zero(response_length)
|
||||
elif chunk_0.shape[0] != 0 and chunk_1.shape[0] == 0:
|
||||
# only first chunk
|
||||
left = zero(logits_offset[0][0] - (prompt_length - 1))
|
||||
right = zero(total_length - 1 - logits_offset[0][1])
|
||||
full_tensor = torch.cat([left, chunk_0, right], dim=0)
|
||||
elif chunk_0.shape[0] == 0 and chunk_1.shape[0] != 0:
|
||||
# only second chunk
|
||||
left = zero(logits_offset[1][0] - (prompt_length - 1))
|
||||
right = zero(total_length - 1 - logits_offset[1][1])
|
||||
full_tensor = torch.cat([left, chunk_1, right], dim=0)
|
||||
else:
|
||||
left = zero(logits_offset[0][0] - (prompt_length - 1))
|
||||
mid = zero(logits_offset[1][0] - logits_offset[0][1])
|
||||
right = zero(total_length - 1 - logits_offset[1][1])
|
||||
full_tensor = torch.cat([left, chunk_0, mid, chunk_1, right], dim=0)
|
||||
|
||||
assert full_tensor.shape[0] == response_length, f"Expected {response_length}, got {full_tensor.shape}"
|
||||
full_tensor = dist.nn.all_reduce(full_tensor, group=cp_group)
|
||||
return full_tensor
|
||||
|
||||
|
||||
def slice_with_cp(tokens: torch.Tensor, pad_value: tuple[int, float, Callable]) -> torch.Tensor:
|
||||
cp_rank = mpu.get_context_parallel_rank()
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
|
||||
if cp_size == 1:
|
||||
return tokens
|
||||
|
||||
# pad
|
||||
chunk_size = (len(tokens) + 2 * cp_size - 1) // (2 * cp_size)
|
||||
pad = 2 * cp_size * chunk_size - len(tokens)
|
||||
if isinstance(pad_value, Callable):
|
||||
pad_func = pad_value
|
||||
tokens = pad_func(tokens, pad)
|
||||
else:
|
||||
# pad on the first dimension
|
||||
pad_tuple = (0, 0) * (tokens.dim() - 1) + (0, pad)
|
||||
tokens = F.pad(tokens, pad_tuple, value=pad_value)
|
||||
# get 2 chunk for thd cp
|
||||
start_1, end_1 = chunk_size * cp_rank, chunk_size * (cp_rank + 1)
|
||||
start_2, end_2 = chunk_size * (2 * cp_size - cp_rank - 1), chunk_size * (2 * cp_size - cp_rank)
|
||||
return torch.cat([tokens[start_1:end_1], tokens[start_2:end_2]])
|
||||
|
||||
|
||||
def slice_log_prob_with_cp(
|
||||
log_prob: list[float] | torch.Tensor,
|
||||
total_length: int,
|
||||
response_length: int,
|
||||
) -> list[float] | torch.Tensor:
|
||||
assert len(log_prob) == response_length
|
||||
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
|
||||
if cp_size == 1:
|
||||
return log_prob
|
||||
|
||||
prompt_length = total_length - response_length
|
||||
_, _, logits_offset, _ = get_logits_and_tokens_offset_with_cp(total_length, response_length)
|
||||
|
||||
chunk_1 = log_prob[logits_offset[0][0] - (prompt_length - 1) : logits_offset[0][1] - (prompt_length - 1)]
|
||||
chunk_2 = log_prob[logits_offset[1][0] - (prompt_length - 1) : logits_offset[1][1] - (prompt_length - 1)]
|
||||
|
||||
if isinstance(log_prob, list):
|
||||
return chunk_1 + chunk_2
|
||||
else:
|
||||
return torch.cat([chunk_1, chunk_2], dim=0)
|
||||
599
slime/backends/megatron_utils/data.py
Normal file
599
slime/backends/megatron_utils/data.py
Normal file
@@ -0,0 +1,599 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from collections.abc import Sequence
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn.functional as F
|
||||
from megatron.core import mpu
|
||||
from megatron.core.packed_seq_params import PackedSeqParams
|
||||
|
||||
from slime.utils import train_metric_utils
|
||||
from slime.utils.data import get_minimum_num_micro_batch_size
|
||||
from slime.utils.flops_utils import calculate_fwd_flops
|
||||
from slime.utils.metric_utils import compute_pass_rate, compute_rollout_step
|
||||
from slime.utils.seqlen_balancing import get_seqlen_balanced_partitions
|
||||
from slime.utils.types import RolloutBatch
|
||||
|
||||
from ...utils import tracking_utils
|
||||
from .cp_utils import get_sum_of_sample_mean, slice_with_cp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_batch(
|
||||
data_iterator: "DataIterator",
|
||||
keys: Sequence[str],
|
||||
pad_multiplier: int = 128,
|
||||
) -> dict[str, torch.Tensor | PackedSeqParams | list[torch.Tensor] | None]:
|
||||
"""
|
||||
Generate a CP-ready micro-batch with packed sequence parameters.
|
||||
|
||||
Steps:
|
||||
- Fetch raw fields via iterator.
|
||||
- Save original token tensors under "unconcat_tokens".
|
||||
- Slice tokens into two chunks for Context Parallelism (CP), concatenate, and pad to a configurable multiple.
|
||||
- Build cu_seqlens and `PackedSeqParams` with T-H-D layout (T: sequence length, H: attention heads, D: head dimension).
|
||||
|
||||
Args:
|
||||
data_iterator: Iterator providing micro-batch data.
|
||||
keys: List of keys to fetch from the iterator.
|
||||
pad_multiplier: Multiplier for padding size calculation (default: 128).
|
||||
|
||||
Returns a dict including:
|
||||
- "tokens": torch.LongTensor of shape [1, T_padded] on the current CUDA device
|
||||
- "unconcat_tokens": list[torch.LongTensor] for the micro-batch before CP slicing/concat
|
||||
- "packed_seq_params": PackedSeqParams with T-H-D settings (cu_seqlens on CUDA, dtype=int)
|
||||
Plus any other requested keys forwarded from the iterator.
|
||||
"""
|
||||
|
||||
assert "tokens" in keys
|
||||
batch = data_iterator.get_next(keys)
|
||||
|
||||
tokens = batch["tokens"]
|
||||
# use 0 as the pad token id should be fine?
|
||||
pad_token_id = 0
|
||||
|
||||
# for cp, we need all tokens to calculate logprob
|
||||
batch["unconcat_tokens"] = tokens
|
||||
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
tokens = [slice_with_cp(t, pad_token_id) for t in tokens]
|
||||
|
||||
cu_seqlens = [0]
|
||||
for t in tokens:
|
||||
cu_seqlens.append(cu_seqlens[-1] + t.size(0))
|
||||
|
||||
tokens = torch.cat(tokens)
|
||||
|
||||
# Always pad to reduce memory fragmentation and maybe make the computation faster
|
||||
pad_size = mpu.get_tensor_model_parallel_world_size() * pad_multiplier
|
||||
pad = (pad_size - tokens.size(0) % pad_size) % pad_size
|
||||
if pad != 0:
|
||||
tokens = F.pad(tokens, (0, pad), value=pad_token_id)
|
||||
cu_seqlens.append(cu_seqlens[-1] + pad)
|
||||
|
||||
# thd requires the cu_seqlens to be of the origin length
|
||||
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int).cuda() * cp_size
|
||||
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
|
||||
|
||||
packed_seq_params = PackedSeqParams(
|
||||
cu_seqlens_q=cu_seqlens,
|
||||
cu_seqlens_kv=cu_seqlens,
|
||||
max_seqlen_q=max_seqlen,
|
||||
max_seqlen_kv=max_seqlen,
|
||||
qkv_format="thd",
|
||||
)
|
||||
|
||||
tokens = tokens.unsqueeze(0)
|
||||
batch["tokens"] = tokens
|
||||
batch["packed_seq_params"] = packed_seq_params
|
||||
|
||||
# loss masks
|
||||
loss_masks = []
|
||||
for loss_mask, total_length, response_length in zip(
|
||||
batch["loss_masks"],
|
||||
batch["total_lengths"],
|
||||
batch["response_lengths"],
|
||||
strict=True,
|
||||
):
|
||||
prompt_length = total_length - response_length
|
||||
loss_mask = F.pad(loss_mask, (prompt_length - 1, 1), value=0)
|
||||
loss_mask = slice_with_cp(loss_mask, 0)
|
||||
loss_masks.append(loss_mask)
|
||||
loss_masks = torch.cat(loss_masks)
|
||||
loss_masks = F.pad(loss_masks, (0, pad), value=0).unsqueeze(0)
|
||||
assert loss_masks.shape == tokens.shape, f"loss_masks.shape: {loss_masks.shape}, tokens.shape: {tokens.shape}"
|
||||
batch["full_loss_masks"] = loss_masks
|
||||
|
||||
# Process multimodal training tensors if present
|
||||
multimodal_train_inputs = batch.get("multimodal_train_inputs", None)
|
||||
if multimodal_train_inputs is not None:
|
||||
multimodal_data = {} # key -> concatenated tensor
|
||||
multimodal_num_items = {} # key -> list of item counts per sequence
|
||||
for mm_input_dict in multimodal_train_inputs:
|
||||
if mm_input_dict is not None:
|
||||
for key, mm_tensor in mm_input_dict.items():
|
||||
if key not in multimodal_data:
|
||||
multimodal_data[key] = mm_tensor
|
||||
multimodal_num_items[key] = [mm_tensor.size(0)]
|
||||
else:
|
||||
multimodal_data[key] = torch.cat([multimodal_data[key], mm_tensor], dim=0)
|
||||
multimodal_num_items[key].append(mm_tensor.size(0))
|
||||
batch["multimodal_train_inputs"] = multimodal_data
|
||||
batch["multimodal_num_items"] = multimodal_num_items
|
||||
|
||||
return batch
|
||||
|
||||
|
||||
def gather_log_data(
|
||||
metric_name: str,
|
||||
args: Namespace,
|
||||
rollout_id: int,
|
||||
log_dict: dict[str, float],
|
||||
) -> dict[str, float] | None:
|
||||
"""
|
||||
Gather per-rank metrics, reduce by mean on the DP source rank, and log.
|
||||
|
||||
Expects `log_dict` to contain plain scalars. The DP source rank prints and
|
||||
optionally logs to WandB/TensorBoard with a step derived from `rollout_id` and
|
||||
batch sizes. Returns the reduced dict on the DP source rank; returns None on others.
|
||||
"""
|
||||
|
||||
if mpu.get_data_parallel_rank(with_context_parallel=True) == 0:
|
||||
dp_size = mpu.get_data_parallel_world_size(with_context_parallel=True)
|
||||
|
||||
gathered_log_dict = [None] * dp_size
|
||||
# Not sure if this will be a performance bottleneck.
|
||||
dist.gather_object(
|
||||
log_dict,
|
||||
gathered_log_dict,
|
||||
dst=mpu.get_data_parallel_src_rank(with_context_parallel=True),
|
||||
group=mpu.get_data_parallel_group_gloo(with_context_parallel=True),
|
||||
)
|
||||
|
||||
reduced_log_dict = {
|
||||
f"{metric_name}/{key}": sum([d[key] for d in gathered_log_dict]) / dp_size for key in log_dict
|
||||
}
|
||||
logger.info(f"{metric_name} {rollout_id}: {reduced_log_dict}")
|
||||
|
||||
# Calculate step once to avoid duplication
|
||||
step = compute_rollout_step(args, rollout_id)
|
||||
reduced_log_dict["rollout/step"] = step
|
||||
tracking_utils.log(args, reduced_log_dict, step_key="rollout/step")
|
||||
|
||||
return reduced_log_dict
|
||||
else:
|
||||
dist.gather_object(
|
||||
log_dict,
|
||||
None,
|
||||
dst=mpu.get_data_parallel_src_rank(with_context_parallel=True),
|
||||
group=mpu.get_data_parallel_group_gloo(with_context_parallel=True),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class DataIterator:
|
||||
"""Micro-batch iterator over rollout dicts.
|
||||
|
||||
Supports either fixed contiguous micro-batches or an explicit per-step
|
||||
index schedule (for dynamic batch sizing / sequence-length balancing).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rollout_data: RolloutBatch,
|
||||
micro_batch_size: int | None = None,
|
||||
micro_batch_indices: list[list[int]] | None = None,
|
||||
) -> None:
|
||||
"""Initialize an iterator over `rollout_data`.
|
||||
|
||||
Args:
|
||||
rollout_data: Dict of per-sample fields for the local step.
|
||||
micro_batch_size: Fixed contiguous slice size when not using dynamic scheduling.
|
||||
micro_batch_indices: Explicit indices per micro-batch when using dynamic balancing.
|
||||
Must be mutually exclusive with `micro_batch_size`.
|
||||
"""
|
||||
self.rollout_data = rollout_data
|
||||
self.micro_batch_size = micro_batch_size
|
||||
self.micro_batch_indices = micro_batch_indices
|
||||
assert micro_batch_size is None or micro_batch_indices is None
|
||||
self.offset = 0
|
||||
|
||||
# Keys that are batch-level (not per-sample) and should be passed through as-is
|
||||
BATCH_LEVEL_KEYS = set()
|
||||
|
||||
def get_next(self, keys: Sequence[str]) -> dict[str, list[object] | None]:
|
||||
"""Return the next micro-batch for the requested keys.
|
||||
|
||||
- If `micro_batch_indices` is provided, selects rows according to the current
|
||||
index list for each requested key.
|
||||
- Otherwise, slices a contiguous window of size `micro_batch_size` starting
|
||||
at the current offset.
|
||||
|
||||
Returns a dict mapping each key to a list subset (or None if absent).
|
||||
"""
|
||||
batch = {}
|
||||
for key in keys:
|
||||
vals = self.rollout_data.get(key, None)
|
||||
if vals is None:
|
||||
batch[key] = None
|
||||
elif key in self.BATCH_LEVEL_KEYS:
|
||||
# Batch-level keys are not per-sample, pass through as-is
|
||||
batch[key] = vals
|
||||
else:
|
||||
if self.micro_batch_indices is not None:
|
||||
indices = self.micro_batch_indices[self.offset]
|
||||
batch[key] = [vals[i] for i in indices]
|
||||
else:
|
||||
assert self.offset + self.micro_batch_size <= len(
|
||||
vals
|
||||
), f"offset: {self.offset}, micro_batch_size: {self.micro_batch_size}, len(vals): {len(vals)}"
|
||||
batch[key] = vals[self.offset : self.offset + self.micro_batch_size]
|
||||
|
||||
if self.micro_batch_indices is not None:
|
||||
self.offset += 1
|
||||
else:
|
||||
self.offset += self.micro_batch_size
|
||||
return batch
|
||||
|
||||
def reset(self) -> "DataIterator":
|
||||
"""Reset internal offset to the start and return self."""
|
||||
self.offset = 0
|
||||
return self
|
||||
|
||||
|
||||
def get_data_iterator(
|
||||
args: Namespace,
|
||||
model: torch.nn.Module | Sequence[torch.nn.Module],
|
||||
rollout_data: RolloutBatch,
|
||||
) -> tuple[list[DataIterator], list[int]]:
|
||||
"""
|
||||
Create iterators and a micro-batch schedule for a rollout step.
|
||||
|
||||
- If `use_dynamic_batch_size` is False, splits into fixed-size contiguous
|
||||
micro-batches of `micro_batch_size`.
|
||||
- If True, computes the number of micro-batches per local step based on
|
||||
`max_tokens_per_gpu` and per-sample lengths, all-reduces to a DP-wide
|
||||
maximum, optionally enforces divisibility for Virtual Pipeline Parallelism (VPP), and builds a balanced
|
||||
index schedule to equalize token counts across micro-batches.
|
||||
|
||||
Returns `(data_iterators, num_microbatches)` where:
|
||||
- `data_iterators`: list of `DataIterator`, one per VPP stage (size 1 if VPP disabled)
|
||||
- `num_microbatches`: list[int], one per local step in the rollout (length = steps)
|
||||
"""
|
||||
dp_size = mpu.get_data_parallel_world_size(with_context_parallel=False)
|
||||
dp_group = mpu.get_data_parallel_group()
|
||||
vpp_size = mpu.get_virtual_pipeline_model_parallel_world_size()
|
||||
if vpp_size is None:
|
||||
vpp_size = 1
|
||||
if vpp_size > 1:
|
||||
from megatron.core.utils import get_model_config
|
||||
|
||||
config = get_model_config(model[0])
|
||||
microbatch_group_size_per_vp_stage = config.microbatch_group_size_per_vp_stage
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
|
||||
num_local_samples = len(rollout_data["total_lengths"])
|
||||
num_local_gbs = args.global_batch_size // dp_size
|
||||
num_steps_per_rollout = num_local_samples // num_local_gbs
|
||||
|
||||
def _generate_data_iterator(rollout_data, micro_batch_size, micro_batch_indices=None):
|
||||
data_iterator = []
|
||||
for _ in range(vpp_size):
|
||||
data_iterator.append(DataIterator(rollout_data, micro_batch_size, micro_batch_indices))
|
||||
return data_iterator
|
||||
|
||||
if not args.use_dynamic_batch_size:
|
||||
num_microbatches = [num_local_gbs // args.micro_batch_size for _ in range(num_steps_per_rollout)]
|
||||
data_iterator = _generate_data_iterator(rollout_data, args.micro_batch_size)
|
||||
else:
|
||||
assert args.max_tokens_per_gpu is not None
|
||||
# calculate the number of mirobatches for each step
|
||||
samples = rollout_data["total_lengths"]
|
||||
assert len(samples) == num_local_samples
|
||||
num_microbatches = []
|
||||
for i in range(num_steps_per_rollout):
|
||||
start, end = i * num_local_gbs, (i + 1) * num_local_gbs
|
||||
num_microbatches.append(
|
||||
get_minimum_num_micro_batch_size(samples[start:end], args.max_tokens_per_gpu * cp_size)
|
||||
)
|
||||
|
||||
num_microbatches = torch.tensor(num_microbatches, dtype=torch.int, device=torch.cuda.current_device())
|
||||
dist.all_reduce(num_microbatches, op=dist.ReduceOp.MAX, group=dp_group)
|
||||
|
||||
if vpp_size > 1:
|
||||
# vpp requies the number of microbatches to be divisible by vpp_size
|
||||
num_microbatches = torch.clamp(
|
||||
num_microbatches // microbatch_group_size_per_vp_stage * microbatch_group_size_per_vp_stage,
|
||||
min=1,
|
||||
)
|
||||
|
||||
num_microbatches = num_microbatches.tolist()
|
||||
|
||||
# balance the each micro batch
|
||||
samples = rollout_data["total_lengths"]
|
||||
# balance the number of mirobatches across steps
|
||||
micro_batch_indices = []
|
||||
for i, num_mbs in enumerate(num_microbatches):
|
||||
start, end = i * num_local_gbs, (i + 1) * num_local_gbs
|
||||
samples = rollout_data["total_lengths"][start:end]
|
||||
partitions = get_seqlen_balanced_partitions(samples, num_mbs, equal_size=False)
|
||||
for j in range(num_mbs):
|
||||
for k in range(len(partitions[j])):
|
||||
partitions[j][k] += start
|
||||
micro_batch_indices.extend(partitions)
|
||||
|
||||
assert len(set(sum(micro_batch_indices, []))) == num_local_samples
|
||||
|
||||
data_iterator = _generate_data_iterator(rollout_data, None, micro_batch_indices)
|
||||
|
||||
return (
|
||||
data_iterator,
|
||||
num_microbatches,
|
||||
)
|
||||
|
||||
|
||||
def log_rollout_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None:
|
||||
"""
|
||||
Summarize rollout fields and log reduced metrics on PP last stage, TP rank 0.
|
||||
|
||||
- Tensor-valued lists are concatenated and averaged. For token-level metrics
|
||||
like log-probs/returns/advantages/values, computes a CP-correct sample mean
|
||||
using `loss_masks` and total/response lengths.
|
||||
- Non-tensor lists are averaged elementwise.
|
||||
- Scalars are converted to Python numbers.
|
||||
"""
|
||||
if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
log_dict = {}
|
||||
response_lengths = rollout_data["response_lengths"]
|
||||
loss_masks = rollout_data["loss_masks"]
|
||||
total_lengths = rollout_data["total_lengths"]
|
||||
|
||||
for key, val in rollout_data.items():
|
||||
if key in [
|
||||
"tokens",
|
||||
"multimodal_train_inputs",
|
||||
"loss_masks",
|
||||
"sample_indices",
|
||||
"rollout_routed_experts",
|
||||
]:
|
||||
continue
|
||||
# Skip None values
|
||||
if val is None:
|
||||
continue
|
||||
# Upload per sample mean for each rollout value
|
||||
# There are the following assumptions:
|
||||
# - Each dp rank has the same number of samples
|
||||
if isinstance(val, (list, tuple)):
|
||||
# Filter out None entries before processing.
|
||||
val = [v for v in val if v is not None]
|
||||
if not val:
|
||||
continue
|
||||
if all(isinstance(v, torch.Tensor) for v in val):
|
||||
# NOTE: Here we have to do the clone().detach(), otherwise the tensor will be
|
||||
# modified in place and will cause problem for the next rollout.
|
||||
val = torch.cat(val).clone().detach()
|
||||
if key in ["log_probs", "ref_log_probs", "rollout_log_probs", "returns", "advantages", "values"]:
|
||||
sum_of_sample_mean = get_sum_of_sample_mean(total_lengths, response_lengths, loss_masks)
|
||||
val = cp_size * sum_of_sample_mean(val) / len(loss_masks)
|
||||
else:
|
||||
val = val.mean() * cp_size
|
||||
else:
|
||||
# Mixed Tensor/scalar list.
|
||||
# Convert everything to float scalar for logging.
|
||||
val = sum(float(v.mean()) if isinstance(v, torch.Tensor) else float(v) for v in val) / len(val)
|
||||
elif isinstance(val, torch.Tensor):
|
||||
val = val.float().mean()
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(val)} for key: {key}")
|
||||
log_dict[key] = val.item() if isinstance(val, torch.Tensor) else val
|
||||
|
||||
reduced_log_dict = gather_log_data("rollout", args, rollout_id, log_dict)
|
||||
if args.ci_test and reduced_log_dict is not None:
|
||||
if (
|
||||
rollout_id == 0
|
||||
and "rollout/log_probs" in reduced_log_dict
|
||||
and "rollout/ref_log_probs" in reduced_log_dict
|
||||
):
|
||||
assert reduced_log_dict["rollout/log_probs"] == reduced_log_dict["rollout/ref_log_probs"]
|
||||
if "rollout/log_probs" in reduced_log_dict:
|
||||
assert -0.5 < reduced_log_dict["rollout/log_probs"] < 0
|
||||
if "rollout/entropy" in reduced_log_dict:
|
||||
assert 0 < reduced_log_dict["rollout/entropy"] < 0.5
|
||||
|
||||
if args.log_multi_turn:
|
||||
log_multi_turn_data(rollout_id, args, rollout_data)
|
||||
if args.log_passrate:
|
||||
log_passrate(rollout_id, args, rollout_data)
|
||||
|
||||
if args.log_correct_samples:
|
||||
if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
log_dict = {}
|
||||
response_lengths = rollout_data["response_lengths"]
|
||||
loss_masks = rollout_data["loss_masks"]
|
||||
total_lengths = rollout_data["total_lengths"]
|
||||
|
||||
def quantile(total_value, n_quantiles, data) -> dict:
|
||||
import math
|
||||
|
||||
assert n_quantiles > 1, f"n_quantiles({n_quantiles}) must be greater than 1."
|
||||
|
||||
quantiles = [((i + 1) / n_quantiles) for i in range(n_quantiles)]
|
||||
cut_points = [total_value * q for q in quantiles]
|
||||
cut_points[-1] = total_value
|
||||
|
||||
count = [0] * n_quantiles
|
||||
for d in data:
|
||||
for i, point in enumerate(cut_points):
|
||||
if d <= point:
|
||||
count[i] += 1
|
||||
break
|
||||
|
||||
total = sum(count) + 1e-9
|
||||
percentile = [c / total for c in count]
|
||||
|
||||
percentile = {f"p{min(math.ceil(q*100),100)}": p for q, p in zip(quantiles, percentile, strict=True)}
|
||||
return percentile
|
||||
|
||||
raw_rewards = rollout_data["raw_reward"]
|
||||
# Additional metrics for correct cases are calculated separately below.
|
||||
correct_response_lengths = []
|
||||
correct_total_lengths = []
|
||||
correct_loss_masks = []
|
||||
correct_entropy = []
|
||||
for i, raw_reward in enumerate(raw_rewards):
|
||||
if raw_reward == 1:
|
||||
correct_response_lengths.append(response_lengths[i])
|
||||
correct_total_lengths.append(total_lengths[i])
|
||||
correct_loss_masks.append(loss_masks[i])
|
||||
correct_entropy.append(-rollout_data["log_probs"][i])
|
||||
num_correct_responses = len(correct_total_lengths)
|
||||
rollout_data["correct_response_lengths"] = correct_response_lengths
|
||||
correct_response_length_percentile = quantile(
|
||||
args.rollout_max_response_len, 4, rollout_data["correct_response_lengths"]
|
||||
)
|
||||
for p, val in correct_response_length_percentile.items():
|
||||
rollout_data[f"correct_length/{p}"] = [val] * num_correct_responses
|
||||
if len(correct_entropy) > 0:
|
||||
sum_of_sample_mean = get_sum_of_sample_mean(
|
||||
correct_total_lengths, correct_response_lengths, correct_loss_masks
|
||||
)
|
||||
correct_entropy = sum_of_sample_mean(torch.cat(correct_entropy, dim=0))
|
||||
rollout_data["correct_entropy"] = [correct_entropy.item()] * num_correct_responses
|
||||
else:
|
||||
rollout_data["correct_entropy"] = [0] * num_correct_responses
|
||||
|
||||
|
||||
def log_multi_turn_data(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None:
|
||||
"""
|
||||
Log multi-turn auxiliary metrics such as raw/observed response lengths and rounds.
|
||||
|
||||
Operates only on PP last stage and TP rank 0. Uses GPU tensors when available
|
||||
to compute statistics without host transfers.
|
||||
"""
|
||||
if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
|
||||
log_dict = {}
|
||||
for key, val in rollout_data.items():
|
||||
if key == "loss_masks":
|
||||
if val: # Check if val is not empty
|
||||
device = val[0].device # Get device from first tensor
|
||||
|
||||
# Vectorized length calculation using torch
|
||||
raw_response_lengths = torch.tensor([v.shape[0] for v in val], dtype=torch.float32, device=device)
|
||||
log_dict["raw_response_length/response_length_mean"] = raw_response_lengths.mean().item()
|
||||
log_dict["raw_response_length/response_length_max"] = raw_response_lengths.max().item()
|
||||
log_dict["raw_response_length/response_length_min"] = raw_response_lengths.min().item()
|
||||
log_dict["raw_response_length/response_length_clip_ratio"] = (
|
||||
(raw_response_lengths >= args.rollout_max_response_len).float().mean().item()
|
||||
)
|
||||
|
||||
# Vectorized sum calculation using torch - stay on GPU
|
||||
wo_obs_response_lengths = torch.tensor(
|
||||
[v.sum().item() for v in val], dtype=torch.float32, device=device
|
||||
)
|
||||
log_dict["wo_obs_response_length/response_length_mean"] = wo_obs_response_lengths.mean().item()
|
||||
log_dict["wo_obs_response_length/response_length_max"] = wo_obs_response_lengths.max().item()
|
||||
log_dict["wo_obs_response_length/response_length_min"] = wo_obs_response_lengths.min().item()
|
||||
if key == "round_number":
|
||||
# Use numpy for vectorized round number statistics
|
||||
round_number_array = np.array(val)
|
||||
log_dict["multi_turn_metric/round_number_mean"] = np.mean(round_number_array)
|
||||
log_dict["multi_turn_metric/round_number_max"] = np.max(round_number_array)
|
||||
log_dict["multi_turn_metric/round_number_min"] = np.min(round_number_array)
|
||||
gather_log_data("multi_turn", args, rollout_id, log_dict)
|
||||
|
||||
|
||||
def log_passrate(rollout_id: int, args: Namespace, rollout_data: RolloutBatch) -> None:
|
||||
"""
|
||||
Compute pass@k metrics from `raw_reward` groups and log the results.
|
||||
|
||||
`raw_reward` is reshaped to `[group_number, group_size]`, then pass@k is
|
||||
estimated per problem and averaged.
|
||||
"""
|
||||
if mpu.get_tensor_model_parallel_rank() == 0 and mpu.is_pipeline_last_stage():
|
||||
log_dict = {}
|
||||
for key, val in rollout_data.items():
|
||||
if key != "raw_reward":
|
||||
continue
|
||||
|
||||
log_dict |= compute_pass_rate(
|
||||
flat_rewards=val,
|
||||
group_size=args.n_samples_per_prompt,
|
||||
num_groups=args.rollout_batch_size,
|
||||
)
|
||||
|
||||
gather_log_data("passrate", args, rollout_id, log_dict)
|
||||
|
||||
|
||||
def log_perf_data(rollout_id: int, args: Namespace) -> None:
|
||||
train_metric_utils.log_perf_data_raw(
|
||||
rollout_id=rollout_id,
|
||||
args=args,
|
||||
is_primary_rank=(
|
||||
mpu.get_tensor_model_parallel_rank() == 0
|
||||
and mpu.is_pipeline_last_stage()
|
||||
and mpu.get_data_parallel_rank(with_context_parallel=True) == 0
|
||||
),
|
||||
compute_total_fwd_flops=lambda seq_lens: calculate_fwd_flops(seqlens=seq_lens, args=args)
|
||||
/ dist.get_world_size()
|
||||
/ 1e12,
|
||||
)
|
||||
|
||||
|
||||
def sync_actor_critic_data(
|
||||
args: Namespace,
|
||||
rollout_data: RolloutBatch | None = None,
|
||||
group: dist.ProcessGroup | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Broadcast `values` (from critic) and optionally `log_probs`/`ref_log_probs`
|
||||
(from actor) across PP ranks to align data dependencies.
|
||||
|
||||
- Values are broadcast from src=1.
|
||||
- Log-probs and ref-log-probs are broadcast from src=0 when KL is used.
|
||||
Updates `rollout_data` in place with the synchronized tensors.
|
||||
"""
|
||||
log_probs_key = "log_probs" if not args.use_rollout_logprobs else "rollout_log_probs"
|
||||
values, log_probs, ref_log_probs = map(rollout_data.get, ("values", log_probs_key, "ref_log_probs"))
|
||||
|
||||
# return when not the pp last stage
|
||||
if not values and not log_probs:
|
||||
return
|
||||
|
||||
handles = []
|
||||
|
||||
if not values:
|
||||
values = [torch.empty_like(log_prob) for log_prob in log_probs]
|
||||
for value in values:
|
||||
handles.append(dist.broadcast(value, src=1, group=group, async_op=True))
|
||||
|
||||
if args.kl_coef != 0 or args.use_kl_loss:
|
||||
if not log_probs:
|
||||
log_probs = [torch.empty_like(value) for value in values]
|
||||
if not ref_log_probs:
|
||||
ref_log_probs = [torch.empty_like(value) for value in values]
|
||||
for ref_log_prob, log_prob in zip(ref_log_probs, log_probs, strict=False):
|
||||
handles.append(dist.broadcast(log_prob, src=0, group=group, async_op=True))
|
||||
handles.append(dist.broadcast(ref_log_prob, src=0, group=group, async_op=True))
|
||||
|
||||
for handle in handles:
|
||||
handle.wait()
|
||||
|
||||
rollout_data.update(
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
"values": values,
|
||||
log_probs_key: log_probs,
|
||||
"ref_log_probs": ref_log_probs,
|
||||
}.items()
|
||||
if v is not None
|
||||
}
|
||||
)
|
||||
116
slime/backends/megatron_utils/initialize.py
Normal file
116
slime/backends/megatron_utils/initialize.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from megatron.core import mpu, tensor_parallel
|
||||
from megatron.core.config import set_experimental_flag
|
||||
from megatron.core.num_microbatches_calculator import init_num_microbatches_calculator
|
||||
from megatron.training.global_vars import _build_tokenizer, set_args
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _set_random_seed(
|
||||
seed_: int,
|
||||
data_parallel_random_init: bool = False,
|
||||
te_rng_tracker: bool = False,
|
||||
inference_rng_tracker: bool = False,
|
||||
use_cudagraphable_rng: bool = False,
|
||||
):
|
||||
"""Set random seed for reproducability."""
|
||||
# Ensure that different pipeline MP stages get different seeds.
|
||||
seed = seed_ + (100 * mpu.get_pipeline_model_parallel_rank())
|
||||
# Ensure different data parallel ranks get different seeds
|
||||
if data_parallel_random_init:
|
||||
seed = seed + (10 * mpu.get_data_parallel_rank(with_context_parallel=False))
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
tensor_parallel.model_parallel_cuda_manual_seed(seed, te_rng_tracker, inference_rng_tracker, use_cudagraphable_rng)
|
||||
|
||||
|
||||
def _initialize_distributed(args, get_embedding_ranks=None, get_position_embedding_ranks=None):
|
||||
"""Initialize torch.distributed and core model parallel."""
|
||||
# Set the tensor model-parallel, pipeline model-parallel, and
|
||||
# data-parallel communicators.
|
||||
mpu.initialize_model_parallel(
|
||||
args.tensor_model_parallel_size,
|
||||
args.pipeline_model_parallel_size,
|
||||
args.virtual_pipeline_model_parallel_size,
|
||||
pipeline_model_parallel_comm_backend=args.pipeline_model_parallel_comm_backend,
|
||||
context_parallel_size=args.context_parallel_size,
|
||||
hierarchical_context_parallel_sizes=args.hierarchical_context_parallel_sizes,
|
||||
expert_model_parallel_size=args.expert_model_parallel_size,
|
||||
num_distributed_optimizer_instances=args.num_distributed_optimizer_instances,
|
||||
expert_tensor_parallel_size=args.expert_tensor_parallel_size,
|
||||
distributed_timeout_minutes=args.distributed_timeout_minutes,
|
||||
nccl_communicator_config_path=args.nccl_communicator_config_path,
|
||||
order="tp-cp-ep-dp-pp" if not args.use_tp_pp_dp_mapping else "tp-cp-ep-pp-dp",
|
||||
get_embedding_ranks=get_embedding_ranks,
|
||||
get_position_embedding_ranks=get_position_embedding_ranks,
|
||||
create_gloo_process_groups=args.enable_gloo_process_groups,
|
||||
)
|
||||
|
||||
|
||||
def init(args):
|
||||
set_args(args)
|
||||
if args.enable_experimental:
|
||||
logger.info("Enable megatron experimental")
|
||||
set_experimental_flag(True)
|
||||
|
||||
# Pytorch distributed.
|
||||
_initialize_distributed(args)
|
||||
|
||||
# https://github.com/NVIDIA/Megatron-LM/issues/1563
|
||||
assert np.__version__.startswith("1."), "Megatron does not support numpy 2.x"
|
||||
|
||||
# Random seeds for reproducibility.
|
||||
if args.rank == 0:
|
||||
logger.info(f"> setting random seeds to {args.seed} ...")
|
||||
_set_random_seed(
|
||||
args.seed,
|
||||
args.data_parallel_random_init,
|
||||
args.te_rng_tracker,
|
||||
args.inference_rng_tracker,
|
||||
)
|
||||
_build_tokenizer(args)
|
||||
# We won't use this. initialize to pass some validation in megatron.
|
||||
init_num_microbatches_calculator(
|
||||
args.rank,
|
||||
args.rampup_batch_size,
|
||||
args.global_batch_size,
|
||||
args.micro_batch_size,
|
||||
args.data_parallel_size,
|
||||
args.decrease_batch_size_if_needed,
|
||||
)
|
||||
|
||||
if args.deterministic_mode:
|
||||
if args.rank == 0:
|
||||
logger.info("> running in deterministic mode")
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
torch.use_deterministic_algorithms(True, warn_only=False)
|
||||
|
||||
if args.tp_comm_overlap:
|
||||
from megatron.training.initialize import _initialize_tp_communicators
|
||||
|
||||
_initialize_tp_communicators()
|
||||
|
||||
if getattr(args, "custom_megatron_init_path", None):
|
||||
from slime.utils.misc import load_function
|
||||
|
||||
custom_init = load_function(args.custom_megatron_init_path)
|
||||
custom_init(args)
|
||||
|
||||
|
||||
# TODO shall we use a simpler method to determine which rank to init wandb?
|
||||
def is_megatron_main_rank():
|
||||
return (
|
||||
mpu.get_data_parallel_rank(with_context_parallel=True) == 0
|
||||
and mpu.get_tensor_model_parallel_rank() == 0
|
||||
and mpu.get_pipeline_model_parallel_rank() == mpu.get_pipeline_model_parallel_world_size() - 1
|
||||
)
|
||||
@@ -0,0 +1,368 @@
|
||||
#include <torch/extension.h>
|
||||
#include <ATen/cuda/CUDAContext.h>
|
||||
|
||||
#define FINAL_MASK 0xFFFFFFFF
|
||||
|
||||
__device__ __host__ __forceinline__
|
||||
int ceil_div(int a, int b) {
|
||||
return (a + b - 1) / b;
|
||||
}
|
||||
|
||||
__device__ __forceinline__
|
||||
float warpReduceMax(float val) {
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1)
|
||||
val = fmaxf(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32));
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
__device__ __forceinline__
|
||||
float warpReduceMin(float val) {
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1)
|
||||
val = fminf(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32));
|
||||
return val;
|
||||
}
|
||||
|
||||
// almost all int4 use blocksize = [1, 32]
|
||||
template<typename scalar_t>
|
||||
__global__
|
||||
void int4_quant_1x32_kernel(
|
||||
const scalar_t* __restrict__ x,
|
||||
scalar_t* __restrict__ out,
|
||||
scalar_t* out_scale,
|
||||
scalar_t* out_zero,
|
||||
const int M, const int N,
|
||||
const int stride_xm, const int stride_xn,
|
||||
const int stride_om, const int stride_on,
|
||||
const int stride_osm, const int stride_osn,
|
||||
const int stride_ozm, const int stride_ozn,
|
||||
bool sym
|
||||
) {
|
||||
constexpr int WARPS_PER_BLOCK = 8;
|
||||
const int needed_warps = ceil_div(N, 32);
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
const int warp_id = tid >> 5;
|
||||
const int lane_id = tid & 0x1F;
|
||||
constexpr float SYM_CONS = 1.0f / 7.0f;
|
||||
constexpr float ASYM_CONS = 1.0f / 15.0f;
|
||||
|
||||
const int row = blockIdx.x;
|
||||
|
||||
for (int item = warp_id; item < needed_warps; item += WARPS_PER_BLOCK) {
|
||||
const int col = item * 32 + lane_id;
|
||||
float val = 0.0f;
|
||||
|
||||
if (col < N) {
|
||||
val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
|
||||
}
|
||||
|
||||
float scale = 0.0f;
|
||||
float zero = 0.0f;
|
||||
|
||||
if (sym) {
|
||||
float abs_val = fabsf(val);
|
||||
|
||||
float block_max = warpReduceMax(abs_val);
|
||||
|
||||
scale = fmaxf(block_max * SYM_CONS, 1e-5f);
|
||||
|
||||
val = rintf(val / scale);
|
||||
} else {
|
||||
float block_min = warpReduceMin(val);
|
||||
float block_max = warpReduceMax(val);
|
||||
|
||||
scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f);
|
||||
zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f);
|
||||
|
||||
val = rintf(val / scale) + zero;
|
||||
}
|
||||
|
||||
if (col < N) {
|
||||
out[row * stride_om + col * stride_on] = static_cast<scalar_t>(val);
|
||||
out_scale[row * stride_osm + item * stride_osn] = static_cast<scalar_t>(scale);
|
||||
if(!sym) {
|
||||
out_zero[row * stride_ozm + item * stride_ozn] = static_cast<scalar_t>(zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for some transpose case, blocksize = [32, 1]
|
||||
template<typename scalar_t>
|
||||
__global__
|
||||
void int4_quant_32x1_kernel(
|
||||
const scalar_t* __restrict__ x,
|
||||
scalar_t* __restrict__ out,
|
||||
scalar_t* out_scale,
|
||||
scalar_t* out_zero,
|
||||
const int M, const int N,
|
||||
const int stride_xm, const int stride_xn,
|
||||
const int stride_om, const int stride_on,
|
||||
const int stride_osm, const int stride_osn,
|
||||
const int stride_ozm, const int stride_ozn,
|
||||
bool sym
|
||||
) {
|
||||
constexpr int WARPS_PER_BLOCK = 8;
|
||||
const int start_row = blockIdx.x * 32;
|
||||
const int end_row = min((blockIdx.x + 1) * 32, M);
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
const int warp_id = tid >> 5;
|
||||
const int lane_id = tid & 0x1F;
|
||||
constexpr float SYM_CONS = 1.0f / 7.0f;
|
||||
constexpr float ASYM_CONS = 1.0f / 15.0f;
|
||||
|
||||
for (int item = warp_id; item < N; item += WARPS_PER_BLOCK) {
|
||||
const int col = item;
|
||||
const int row = start_row + lane_id;
|
||||
|
||||
float val = 0.0f;
|
||||
|
||||
if (row < end_row) {
|
||||
val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
|
||||
}
|
||||
|
||||
float scale = 0.0f;
|
||||
float zero = 0.0f;
|
||||
|
||||
if (sym) {
|
||||
float abs_val = fabsf(val);
|
||||
|
||||
float block_max = warpReduceMax(abs_val);
|
||||
|
||||
scale = fmaxf(block_max * SYM_CONS, 1e-5f);
|
||||
|
||||
val = rintf(val / scale);
|
||||
} else {
|
||||
float block_min = warpReduceMin(val);
|
||||
float block_max = warpReduceMax(val);
|
||||
|
||||
scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f);
|
||||
zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f);
|
||||
|
||||
val = rintf(val / scale) + zero;
|
||||
}
|
||||
|
||||
if (row < end_row) {
|
||||
out[row * stride_om + col * stride_on] = static_cast<scalar_t>(val);
|
||||
out_scale[blockIdx.x * stride_osm + item * stride_osn] = static_cast<scalar_t>(scale);
|
||||
if (!sym) {
|
||||
out_zero[blockIdx.x * stride_ozm + item * stride_ozn] = static_cast<scalar_t>(zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename scalar_t>
|
||||
__global__ void int4_quant_common_kernel(
|
||||
const scalar_t* __restrict__ x,
|
||||
scalar_t* __restrict__ out,
|
||||
scalar_t* out_scale,
|
||||
scalar_t* out_zero,
|
||||
const int M, const int N,
|
||||
const int stride_xm, const int stride_xn,
|
||||
const int stride_om, const int stride_on,
|
||||
const int stride_osm, const int stride_osn,
|
||||
const int stride_ozm, const int stride_ozn,
|
||||
const int BLOCK_M, const int BLOCK_N,
|
||||
bool sym
|
||||
) {
|
||||
const int start_row = blockIdx.x * BLOCK_M;
|
||||
const int WARPS_PER_BLOCK = blockDim.x >> 5;
|
||||
|
||||
const int warp_id = threadIdx.x >> 5;
|
||||
const int lane_id = threadIdx.x & 0x1F;
|
||||
constexpr float SYM_CONS = 1.0f / 7.0f;
|
||||
constexpr float ASYM_CONS = 1.0f / 15.0f;
|
||||
constexpr int WARP_SIZE = 32;
|
||||
|
||||
const int needed_warps = ceil_div(N, BLOCK_N);
|
||||
const int iters = ceil_div(BLOCK_M * BLOCK_N, 32);
|
||||
int warp_rows = 1;
|
||||
|
||||
if (BLOCK_N <= WARP_SIZE) {
|
||||
warp_rows = WARP_SIZE / BLOCK_N;
|
||||
}
|
||||
|
||||
for (int item = warp_id; item < needed_warps; item += WARPS_PER_BLOCK) {
|
||||
float local_max = -INFINITY;
|
||||
float local_min = INFINITY;
|
||||
|
||||
float val = 0.0f;
|
||||
float scale, zero = 0.0f;
|
||||
|
||||
const int row_off = lane_id / BLOCK_N;
|
||||
const int col_off = lane_id % BLOCK_N;
|
||||
int row, col = 0;
|
||||
|
||||
for (int i = 0; i < iters; ++i) {
|
||||
if (BLOCK_N <= WARP_SIZE) {
|
||||
row = start_row + i * warp_rows + row_off;
|
||||
col = item * BLOCK_N + col_off;
|
||||
} else {
|
||||
row = start_row;
|
||||
col = item * BLOCK_N + i * WARP_SIZE + col_off;
|
||||
}
|
||||
|
||||
if (row < M && col < N) {
|
||||
val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
|
||||
} else {
|
||||
val = 0.0f;
|
||||
}
|
||||
|
||||
if (sym) {
|
||||
local_max = fmaxf(local_max, fabsf(val));
|
||||
} else {
|
||||
local_max = fmaxf(local_max, val);
|
||||
local_min = fminf(local_min, val);
|
||||
}
|
||||
}
|
||||
|
||||
if (sym) {
|
||||
float block_max = warpReduceMax(local_max);
|
||||
scale = fmaxf(block_max * SYM_CONS, 1e-5f);
|
||||
} else {
|
||||
float block_max = warpReduceMax(local_max);
|
||||
float block_min = warpReduceMin(local_min);
|
||||
scale = fmaxf((block_max - block_min) * ASYM_CONS, 1e-5f);
|
||||
zero = fminf(fmaxf(-rintf(block_min / scale), 0.0f), 15.0f);
|
||||
}
|
||||
|
||||
for (int i = 0; i < iters; ++i) {
|
||||
if (BLOCK_N <= WARP_SIZE) {
|
||||
row = start_row + i * warp_rows + row_off;
|
||||
col = item * BLOCK_N + col_off;
|
||||
} else {
|
||||
row = start_row;
|
||||
col = item * BLOCK_N + i * WARP_SIZE + col_off;
|
||||
}
|
||||
|
||||
if (row < M && col < N) {
|
||||
float val = static_cast<float>(x[row * stride_xm + col * stride_xn]);
|
||||
if (sym) {
|
||||
val = rintf(val / scale);
|
||||
} else {
|
||||
val = rintf(val / scale) + zero;
|
||||
}
|
||||
|
||||
out[row * stride_om + col * stride_on] = static_cast<scalar_t>(val);
|
||||
out_scale[blockIdx.x * stride_osm + item * stride_osn] = static_cast<scalar_t>(scale);
|
||||
if (!sym) {
|
||||
out_zero[blockIdx.x * stride_ozm + item * stride_ozn] = static_cast<scalar_t>(zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch
|
||||
template<typename scalar_t>
|
||||
void launch_int4_quant_kernel(
|
||||
const scalar_t* x,
|
||||
scalar_t* out,
|
||||
scalar_t* out_scale,
|
||||
scalar_t* out_zero,
|
||||
int M, int N,
|
||||
const int stride_xm, const int stride_xn,
|
||||
const int stride_om, const int stride_on,
|
||||
const int stride_osm, const int stride_osn,
|
||||
const int stride_ozm, const int stride_ozn,
|
||||
int block_m, int block_n,
|
||||
bool sym,
|
||||
cudaStream_t stream
|
||||
) {
|
||||
constexpr int WARPS_PER_BLOCK = 8;
|
||||
constexpr int THREADS_PER_BLOCK = WARPS_PER_BLOCK * 32; // 256
|
||||
|
||||
if (block_m == 1 && block_n == 32) {
|
||||
dim3 grid(M);
|
||||
dim3 block(THREADS_PER_BLOCK);
|
||||
|
||||
int4_quant_1x32_kernel<scalar_t><<<grid, block, 0, stream>>>(
|
||||
x, out, out_scale, out_zero, M, N,
|
||||
stride_xm, stride_xn,
|
||||
stride_om, stride_on,
|
||||
stride_osm, stride_osn,
|
||||
stride_ozm, stride_ozn,
|
||||
sym
|
||||
);
|
||||
} else if (block_m == 32 && block_n == 1) {
|
||||
dim3 grid(ceil_div(M, block_m));
|
||||
dim3 block(THREADS_PER_BLOCK);
|
||||
|
||||
int4_quant_32x1_kernel<scalar_t><<<grid, block, 0, stream>>>(
|
||||
x, out, out_scale, out_zero, M, N,
|
||||
stride_xm, stride_xn,
|
||||
stride_om, stride_on,
|
||||
stride_osm, stride_osn,
|
||||
stride_ozm, stride_ozn,
|
||||
sym
|
||||
);
|
||||
} else {
|
||||
dim3 grid(ceil_div(M, block_m));
|
||||
dim3 block(THREADS_PER_BLOCK);
|
||||
int4_quant_common_kernel<scalar_t><<<grid, block, 0, stream>>>(
|
||||
x, out, out_scale, out_zero, M, N,
|
||||
stride_xm, stride_xn,
|
||||
stride_om, stride_on,
|
||||
stride_osm, stride_osn,
|
||||
stride_ozm, stride_ozn,
|
||||
block_m, block_n,
|
||||
sym
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>
|
||||
fake_int4_quant_cuda(
|
||||
torch::Tensor& x,
|
||||
std::vector<int64_t>& block_size,
|
||||
bool sym
|
||||
) {
|
||||
TORCH_CHECK(x.dim() == 2, "Input must be 2D");
|
||||
TORCH_CHECK(x.is_cuda(), "Input must be on CUDA");
|
||||
|
||||
int M = x.size(0);
|
||||
int N = x.size(1);
|
||||
int block_m = block_size[0];
|
||||
int block_n = block_size[1];
|
||||
|
||||
TORCH_CHECK(block_m > 0 && block_n > 0, "Block sizes must be positive, got block_m=", block_m, ", block_n=", block_n);
|
||||
TORCH_CHECK((block_m * block_n) % 32 == 0,
|
||||
"block_m * block_n (", block_m * block_n, ") must be divisible by 32. "
|
||||
"But got a ", block_m, "x", block_n, " block.");
|
||||
|
||||
auto out = torch::empty_like(x);
|
||||
auto out_scale = torch::empty({ceil_div(M, block_m), ceil_div(N, block_n)}, x.options());
|
||||
auto out_zero = torch::empty_like(out_scale);
|
||||
|
||||
const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
||||
|
||||
AT_DISPATCH_FLOATING_TYPES_AND(
|
||||
at::ScalarType::BFloat16,
|
||||
x.scalar_type(), "int4_quant_cuda", [&] {
|
||||
launch_int4_quant_kernel<scalar_t>(
|
||||
x.const_data_ptr<scalar_t>(),
|
||||
out.data_ptr<scalar_t>(),
|
||||
out_scale.data_ptr<scalar_t>(),
|
||||
out_zero.data_ptr<scalar_t>(),
|
||||
M, N,
|
||||
x.stride(0), x.stride(1),
|
||||
out.stride(0), out.stride(1),
|
||||
out_scale.stride(0), out_scale.stride(1),
|
||||
out_zero.stride(0), out_zero.stride(1),
|
||||
block_m, block_n,
|
||||
sym,
|
||||
stream
|
||||
);
|
||||
});
|
||||
|
||||
return std::make_tuple(out, out_scale, out_zero);
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("fake_int4_quant_cuda", &fake_int4_quant_cuda, "fake INT4 quantization cuda");
|
||||
}
|
||||
42
slime/backends/megatron_utils/kernels/int4_qat/setup.py
Normal file
42
slime/backends/megatron_utils/kernels/int4_qat/setup.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from setuptools import setup
|
||||
from torch.utils.cpp_extension import BuildExtension, CUDAExtension
|
||||
import torch
|
||||
|
||||
# Get CUDA arch list
|
||||
arch_list = []
|
||||
if torch.cuda.is_available():
|
||||
for i in range(torch.cuda.device_count()):
|
||||
major, minor = torch.cuda.get_device_capability(i)
|
||||
arch_list.append(f"{major}.{minor}")
|
||||
arch_list = sorted(set(arch_list))
|
||||
|
||||
setup(
|
||||
name="fake_int4_quant_cuda",
|
||||
ext_modules=[
|
||||
CUDAExtension(
|
||||
name="fake_int4_quant_cuda",
|
||||
sources=["fake_int4_quant_cuda.cu"],
|
||||
extra_compile_args={
|
||||
"cxx": [
|
||||
"-O3",
|
||||
"-std=c++17",
|
||||
],
|
||||
"nvcc": [
|
||||
"-O3",
|
||||
"-std=c++17",
|
||||
"--expt-relaxed-constexpr",
|
||||
"-Xcompiler",
|
||||
"-fPIC",
|
||||
]
|
||||
+ [
|
||||
f'-gencode=arch=compute_{arch.replace(".", "")},code=sm_{arch.replace(".", "")}'
|
||||
for arch in arch_list
|
||||
],
|
||||
},
|
||||
)
|
||||
],
|
||||
cmdclass={"build_ext": BuildExtension},
|
||||
)
|
||||
768
slime/backends/megatron_utils/loss.py
Normal file
768
slime/backends/megatron_utils/loss.py
Normal file
@@ -0,0 +1,768 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from collections.abc import Callable, Iterator
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from megatron.core import mpu
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
|
||||
from slime.utils.distributed_utils import distributed_masked_whiten
|
||||
from slime.utils.misc import load_function
|
||||
from slime.utils.ppo_utils import (
|
||||
calculate_log_probs_and_entropy,
|
||||
compute_approx_kl,
|
||||
compute_gspo_kl,
|
||||
compute_opsm_mask,
|
||||
compute_policy_loss,
|
||||
get_advantages_and_returns_batch,
|
||||
get_grpo_returns,
|
||||
get_reinforce_plus_plus_baseline_advantages,
|
||||
get_reinforce_plus_plus_returns,
|
||||
)
|
||||
from slime.utils.types import RolloutBatch
|
||||
|
||||
from .cp_utils import all_gather_with_cp, get_logits_and_tokens_offset_with_cp, get_sum_of_sample_mean
|
||||
|
||||
|
||||
def get_responses(
|
||||
logits: torch.Tensor,
|
||||
*,
|
||||
args: Namespace,
|
||||
unconcat_tokens: list[torch.Tensor],
|
||||
total_lengths: list[int],
|
||||
response_lengths: list[int],
|
||||
) -> Iterator[tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Yield response-aligned `(logits_chunk, tokens_chunk)` pairs per sample.
|
||||
|
||||
After squeezing batch dimension and applying temperature scaling, this
|
||||
function extracts the logits and tokens corresponding to response segments
|
||||
for each sample. When context parallelism is disabled, it slices directly
|
||||
from the concatenated sequence. With context parallelism enabled, it
|
||||
handles split sequences across ranks.
|
||||
|
||||
Args:
|
||||
logits: Model outputs with shape `[1, T, V]` (policy) or `[1, T, 1]`
|
||||
(value). Must be float32.
|
||||
args: Configuration containing `rollout_temperature` for scaling.
|
||||
unconcat_tokens: List of token tensors (prompt+response) per sample.
|
||||
total_lengths: Total sequence lengths (prompt+response) per sample.
|
||||
response_lengths: Response segment lengths per sample.
|
||||
|
||||
Yields:
|
||||
Tuple of `(logits_chunk, tokens_chunk)` where `logits_chunk` is shape
|
||||
`[R, V]` (policy) or `[R, 1]` (value) and `tokens_chunk` is shape `[R]`
|
||||
(1D int64), both aligned to response tokens for one sample.
|
||||
"""
|
||||
assert logits.size(0) == 1, f"{logits.shape}"
|
||||
assert logits.dtype == torch.float32, f"{logits.dtype}"
|
||||
|
||||
logits = logits.squeeze(0)
|
||||
logits = logits.div(args.rollout_temperature)
|
||||
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
end = 0
|
||||
for tokens, total_length, response_length in zip(unconcat_tokens, total_lengths, response_lengths, strict=False):
|
||||
if cp_size == 1:
|
||||
end += total_length
|
||||
start = end - response_length
|
||||
logits_chunk = logits[start - 1 : end - 1]
|
||||
tokens_chunk = tokens[-response_length:]
|
||||
else:
|
||||
# TODO: this is super ugly... do better abstraction.
|
||||
chunk_size, chunks_offset, logits_offset, tokens_offset = get_logits_and_tokens_offset_with_cp(
|
||||
total_length, response_length
|
||||
)
|
||||
|
||||
logits_0, logits_1 = logits[end : end + chunk_size], logits[end + chunk_size : end + 2 * chunk_size]
|
||||
end += 2 * chunk_size
|
||||
|
||||
logits_0 = logits_0[logits_offset[0][0] - chunks_offset[0][0] : logits_offset[0][1] - chunks_offset[0][0]]
|
||||
tokens_0 = tokens[tokens_offset[0][0] : tokens_offset[0][1]]
|
||||
|
||||
logits_1 = logits_1[logits_offset[1][0] - chunks_offset[1][0] : logits_offset[1][1] - chunks_offset[1][0]]
|
||||
tokens_1 = tokens[tokens_offset[1][0] : tokens_offset[1][1]]
|
||||
|
||||
assert logits_0.size(0) == tokens_0.size(0), f"{logits_0.size(0)} vs {tokens_0.size(0)}"
|
||||
assert logits_1.size(0) == tokens_1.size(0), f"{logits_1.size(0)} vs {tokens_1.size(0)}"
|
||||
|
||||
logits_chunk = torch.cat([logits_0, logits_1], dim=0)
|
||||
tokens_chunk = torch.cat([tokens_0, tokens_1], dim=0)
|
||||
|
||||
yield logits_chunk, tokens_chunk
|
||||
|
||||
|
||||
def get_log_probs_and_entropy(
|
||||
logits: torch.Tensor,
|
||||
*,
|
||||
args: Namespace,
|
||||
unconcat_tokens: list[torch.Tensor],
|
||||
total_lengths: list[int],
|
||||
response_lengths: list[int],
|
||||
with_entropy: bool = False,
|
||||
non_loss_data: bool = True,
|
||||
) -> dict[str, list[torch.Tensor]]:
|
||||
"""Compute per-token log-probabilities (and optionally entropy) on responses.
|
||||
|
||||
For each sample, extracts response-aligned logits and tokens, then computes
|
||||
log-probabilities via softmax across the tensor-parallel group. Log-probs
|
||||
are squeezed from `[R, 1]` to `[R]`. Entropy values are always appended
|
||||
(even when `with_entropy=False`), but only included in the result dict
|
||||
when requested.
|
||||
|
||||
Args:
|
||||
logits: Policy logits with shape `[1, T, V]`.
|
||||
args: Configuration (temperature applied in `get_responses`).
|
||||
unconcat_tokens: List of token tensors per sample.
|
||||
total_lengths: Total sequence lengths per sample.
|
||||
response_lengths: Response segment lengths per sample.
|
||||
with_entropy: If True, include "entropy" key in result.
|
||||
non_loss_data: Unused; kept for API compatibility.
|
||||
|
||||
Returns:
|
||||
Dict with key "log_probs" mapping to a list of `[R]` tensors per
|
||||
sample. If `with_entropy` is True, also includes "entropy" key with
|
||||
a list of `[R]` tensors.
|
||||
"""
|
||||
assert non_loss_data
|
||||
log_probs_list = []
|
||||
entropy_list = []
|
||||
for logits_chunk, tokens_chunk in get_responses(
|
||||
logits,
|
||||
args=args,
|
||||
unconcat_tokens=unconcat_tokens,
|
||||
total_lengths=total_lengths,
|
||||
response_lengths=response_lengths,
|
||||
):
|
||||
log_prob, entropy = calculate_log_probs_and_entropy(
|
||||
logits_chunk,
|
||||
tokens_chunk,
|
||||
mpu.get_tensor_model_parallel_group(),
|
||||
with_entropy=with_entropy,
|
||||
chunk_size=args.log_probs_chunk_size,
|
||||
)
|
||||
|
||||
log_probs_list.append(log_prob.squeeze(-1))
|
||||
entropy_list.append(entropy)
|
||||
|
||||
res = {
|
||||
"log_probs": log_probs_list,
|
||||
}
|
||||
if with_entropy:
|
||||
res["entropy"] = entropy_list
|
||||
return res
|
||||
|
||||
|
||||
def get_values(
|
||||
logits: torch.Tensor,
|
||||
*,
|
||||
args: Namespace,
|
||||
unconcat_tokens: list[torch.Tensor],
|
||||
total_lengths: list[int],
|
||||
response_lengths: list[int],
|
||||
with_entropy: bool = False,
|
||||
non_loss_data: bool = True,
|
||||
) -> dict[str, list[torch.Tensor]]:
|
||||
"""Extract per-token value predictions over response tokens.
|
||||
|
||||
For each sample, extracts response-aligned chunks from the value head
|
||||
output and squeezes the final dimension from `[R, 1]` to `[R]`.
|
||||
|
||||
Args:
|
||||
logits: Value head output with shape `[1, T, 1]`.
|
||||
args: Configuration (passed to `get_responses` which uses
|
||||
`rollout_temperature` even though values don't need temperature).
|
||||
unconcat_tokens: List of token tensors per sample.
|
||||
total_lengths: Total sequence lengths per sample.
|
||||
response_lengths: Response segment lengths per sample.
|
||||
with_entropy: Unused; kept for signature compatibility.
|
||||
non_loss_data: Unused; kept for signature compatibility.
|
||||
|
||||
Returns:
|
||||
Dict with key "values" mapping to a list of `[R]` value tensors
|
||||
per sample.
|
||||
"""
|
||||
value_list = []
|
||||
for logits_chunk, _ in get_responses(
|
||||
logits,
|
||||
args=args,
|
||||
unconcat_tokens=unconcat_tokens,
|
||||
total_lengths=total_lengths,
|
||||
response_lengths=response_lengths,
|
||||
):
|
||||
assert logits_chunk.size(-1) == 1, f"{logits_chunk.shape}"
|
||||
value_list.append(logits_chunk.squeeze(-1))
|
||||
|
||||
return {
|
||||
"values": value_list,
|
||||
}
|
||||
|
||||
|
||||
def compute_advantages_and_returns(args: Namespace, rollout_data: RolloutBatch) -> None:
|
||||
"""Compute advantages and returns in-place based on `args.advantage_estimator`.
|
||||
|
||||
This function extracts rewards, log-probs, values, and masks from
|
||||
`rollout_data`, computes KL divergences, then applies the chosen advantage
|
||||
estimator. Supported methods: "grpo", "gspo", "ppo", "reinforce_plus_plus",
|
||||
and "reinforce_plus_plus_baseline". When `args.normalize_advantages` is
|
||||
True, advantages are whitened across the data-parallel group using masked
|
||||
statistics.
|
||||
|
||||
Early returns if both `log_probs` and `values` are None (intermediate
|
||||
pipeline stages).
|
||||
|
||||
Args:
|
||||
args: Configuration specifying estimator type, KL coefficient,
|
||||
normalization settings, and other hyperparameters.
|
||||
rollout_data: Dict containing input lists ("log_probs", "ref_log_probs",
|
||||
"rewards", "values", "response_lengths", "loss_masks",
|
||||
"total_lengths"). Modified in-place to add "advantages" and
|
||||
"returns" keys, each mapping to lists of tensors per sample.
|
||||
"""
|
||||
log_probs: list[torch.Tensor] = rollout_data.get("rollout_log_probs" if args.use_rollout_logprobs else "log_probs")
|
||||
ref_log_probs: list[torch.Tensor] = rollout_data.get("ref_log_probs")
|
||||
rewards: list[float] = rollout_data.get("rewards")
|
||||
values: None | list[torch.Tensor] = rollout_data.get("values")
|
||||
response_lengths: list[int] = rollout_data.get("response_lengths")
|
||||
loss_masks: list[torch.Tensor] = rollout_data.get("loss_masks")
|
||||
total_lengths: list[int] = rollout_data.get("total_lengths")
|
||||
|
||||
# return when not the last pp stage.
|
||||
if log_probs is None and values is None:
|
||||
return
|
||||
|
||||
if args.kl_coef == 0 or not log_probs:
|
||||
# when kl_coef is 0, we won't compute ref_log_prob
|
||||
xs = log_probs if log_probs is not None else values
|
||||
kl = [torch.zeros_like(x, dtype=torch.float32, device=x.device) for x in xs]
|
||||
else:
|
||||
kl = [
|
||||
compute_approx_kl(
|
||||
log_probs[i],
|
||||
ref_log_probs[i],
|
||||
kl_loss_type=args.kl_loss_type,
|
||||
)
|
||||
for i in range(len(log_probs))
|
||||
]
|
||||
|
||||
if args.advantage_estimator in ["grpo", "gspo"]:
|
||||
rewards = torch.tensor(rewards, dtype=torch.float32, device=kl[0].device)
|
||||
returns = get_grpo_returns(rewards, kl)
|
||||
# TODO: is the copy necessary?
|
||||
advantages = [r for r in returns]
|
||||
|
||||
elif args.advantage_estimator == "ppo":
|
||||
old_rewards = rewards
|
||||
rewards = []
|
||||
kl_coef = -args.kl_coef
|
||||
cp_rank = mpu.get_context_parallel_rank()
|
||||
for reward, k in zip(old_rewards, kl, strict=False):
|
||||
k *= kl_coef
|
||||
if cp_rank == 0:
|
||||
k[-1] += reward
|
||||
rewards.append(k)
|
||||
advantages, returns = get_advantages_and_returns_batch(
|
||||
total_lengths, response_lengths, values, rewards, args.gamma, args.lambd
|
||||
)
|
||||
|
||||
elif args.advantage_estimator == "reinforce_plus_plus":
|
||||
rewards = torch.tensor(rewards, dtype=torch.float32, device=kl[0].device)
|
||||
returns = get_reinforce_plus_plus_returns(
|
||||
rewards=rewards,
|
||||
kl=kl,
|
||||
loss_masks=loss_masks,
|
||||
response_lengths=response_lengths,
|
||||
total_lengths=total_lengths,
|
||||
kl_coef=args.kl_coef,
|
||||
gamma=args.gamma,
|
||||
)
|
||||
advantages = [r for r in returns]
|
||||
|
||||
elif args.advantage_estimator == "reinforce_plus_plus_baseline":
|
||||
rewards = torch.tensor(rewards, dtype=torch.float32, device=kl[0].device)
|
||||
advantages = get_reinforce_plus_plus_baseline_advantages(
|
||||
rewards=rewards,
|
||||
kl=kl,
|
||||
loss_masks=loss_masks,
|
||||
kl_coef=args.kl_coef,
|
||||
)
|
||||
returns = advantages
|
||||
|
||||
elif args.advantage_estimator == "on_policy_distillation":
|
||||
student_log_probs = log_probs
|
||||
teacher_log_probs = rollout_data.get("teacher_log_probs")
|
||||
response_lengths = rollout_data.get("response_lengths")
|
||||
|
||||
device = student_log_probs[0].device
|
||||
teacher_log_probs = [t_log_prob.to(device=device) for t_log_prob in teacher_log_probs]
|
||||
teacher_log_probs = [
|
||||
t_log_prob[-response_length:]
|
||||
for t_log_prob, response_length in zip(teacher_log_probs, response_lengths, strict=False)
|
||||
]
|
||||
|
||||
advantages = [
|
||||
teacher_log_prob - student_log_prob
|
||||
for teacher_log_prob, student_log_prob in zip(teacher_log_probs, student_log_probs, strict=False)
|
||||
]
|
||||
|
||||
returns = advantages
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"advantage_estimator {args.advantage_estimator} is not supported. ")
|
||||
|
||||
# TODO: OpenRLHF always does advantages normalization but veRL doesn't seem to do it.
|
||||
if args.normalize_advantages:
|
||||
all_advs = torch.cat(advantages)
|
||||
cp_size = mpu.get_context_parallel_world_size()
|
||||
if cp_size == 1:
|
||||
all_masks = torch.cat(loss_masks)
|
||||
else:
|
||||
mask_chunks = []
|
||||
for i in range(len(advantages)):
|
||||
total_len = total_lengths[i]
|
||||
response_len = response_lengths[i]
|
||||
prompt_len = total_len - response_len
|
||||
|
||||
_, _, _, token_offsets = get_logits_and_tokens_offset_with_cp(total_len, response_len)
|
||||
|
||||
# Convert global offsets to response-space offsets
|
||||
s0, e0 = token_offsets[0]
|
||||
s1, e1 = token_offsets[1]
|
||||
res_s0, res_e0 = max(0, s0 - prompt_len), max(0, e0 - prompt_len)
|
||||
res_s1, res_e1 = max(0, s1 - prompt_len), max(0, e1 - prompt_len)
|
||||
|
||||
local_mask_parts = []
|
||||
full_mask = loss_masks[i]
|
||||
if res_e0 > res_s0:
|
||||
local_mask_parts.append(full_mask[res_s0:res_e0])
|
||||
if res_e1 > res_s1:
|
||||
local_mask_parts.append(full_mask[res_s1:res_e1])
|
||||
|
||||
# Concatenate the parts to form the final mask chunk for this rank and this sequence
|
||||
local_mask_chunk = (
|
||||
torch.cat(local_mask_parts)
|
||||
if local_mask_parts
|
||||
else torch.tensor([], device=all_advs.device, dtype=full_mask.dtype)
|
||||
)
|
||||
mask_chunks.append(local_mask_chunk)
|
||||
|
||||
all_masks = torch.cat(mask_chunks)
|
||||
|
||||
if all_masks.numel() > 0:
|
||||
assert (
|
||||
all_advs.size() == all_masks.size()
|
||||
), f"Shape mismatch before whitening: advantages {all_advs.size()}, masks {all_masks.size()}"
|
||||
dp_group = mpu.get_data_parallel_group()
|
||||
|
||||
whitened_advs_flat = distributed_masked_whiten(
|
||||
all_advs,
|
||||
all_masks,
|
||||
process_group=dp_group,
|
||||
shift_mean=True,
|
||||
)
|
||||
chunk_lengths = [chunk.size(0) for chunk in advantages]
|
||||
advantages = list(torch.split(whitened_advs_flat, chunk_lengths))
|
||||
|
||||
rollout_data["advantages"] = advantages
|
||||
rollout_data["returns"] = returns
|
||||
|
||||
|
||||
def vanilla_tis_function(
|
||||
args,
|
||||
*,
|
||||
pg_loss: torch.Tensor,
|
||||
train_log_probs: list[torch.Tensor],
|
||||
rollout_log_probs: list[torch.Tensor],
|
||||
loss_masks: list[torch.Tensor],
|
||||
**kwargs: Any,
|
||||
) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]:
|
||||
rollout_log_probs = torch.cat(rollout_log_probs, dim=0)
|
||||
old_log_probs = torch.cat(train_log_probs, dim=0)
|
||||
tis = torch.exp(old_log_probs - rollout_log_probs)
|
||||
tis_abs = (torch.exp(old_log_probs - rollout_log_probs) - 1).abs()
|
||||
tis_weights = torch.clamp(tis, min=args.tis_clip_low, max=args.tis_clip)
|
||||
tis_clipfrac = (tis_weights != tis).float()
|
||||
metrics = {
|
||||
"tis": tis.clone().detach(),
|
||||
"tis_clipfrac": tis_clipfrac.clone().detach(),
|
||||
"tis_abs": tis_abs.clone().detach(),
|
||||
}
|
||||
pg_loss = pg_loss * tis_weights
|
||||
return pg_loss, loss_masks, metrics
|
||||
|
||||
|
||||
def icepop_function(
|
||||
args,
|
||||
*,
|
||||
pg_loss: torch.Tensor,
|
||||
train_log_probs: list[torch.Tensor],
|
||||
rollout_log_probs: list[torch.Tensor],
|
||||
loss_masks: list[torch.Tensor],
|
||||
**kwargs: Any,
|
||||
) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]:
|
||||
rollout_log_probs = torch.cat(rollout_log_probs, dim=0)
|
||||
old_log_probs = torch.cat(train_log_probs, dim=0)
|
||||
ice_ratio = torch.exp(old_log_probs - rollout_log_probs)
|
||||
ice_abs = (torch.exp(old_log_probs - rollout_log_probs) - 1).abs()
|
||||
ice_weight = torch.where(
|
||||
(ice_ratio >= args.tis_clip_low) & (ice_ratio <= args.tis_clip), ice_ratio, torch.zeros_like(ice_ratio)
|
||||
)
|
||||
ice_clipfrac = (ice_weight != ice_ratio).float()
|
||||
metrics = {
|
||||
"tis": ice_ratio.clone().detach(),
|
||||
"tis_clipfrac": ice_clipfrac.clone().detach(),
|
||||
"tis_abs": ice_abs.clone().detach(),
|
||||
}
|
||||
pg_loss = pg_loss * ice_weight
|
||||
return pg_loss, loss_masks, metrics
|
||||
|
||||
|
||||
def policy_loss_function(
|
||||
args: Namespace,
|
||||
batch: RolloutBatch,
|
||||
logits: torch.Tensor,
|
||||
sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor],
|
||||
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
||||
"""Compute policy loss (PPO/GSPO) and metrics.
|
||||
|
||||
Computes current log-probabilities and entropy from model logits, then
|
||||
calculates PPO-style clipped policy gradient loss. For GSPO, gathers
|
||||
full sequences via context-parallel all-gather before computing per-sample
|
||||
KL. Optionally applies TIS (Truncated Importance Sampling) correction and
|
||||
adds KL loss term if configured.
|
||||
|
||||
Args:
|
||||
args: Configuration controlling advantage estimator, clipping thresholds,
|
||||
entropy/KL coefficients, and TIS settings.
|
||||
batch: Mini-batch containing "advantages", "log_probs" (old policy),
|
||||
"unconcat_tokens", "response_lengths", "total_lengths", "loss_masks",
|
||||
and optionally "ref_log_probs" and "rollout_log_probs".
|
||||
logits: Policy logits with shape `[1, T, V]`.
|
||||
sum_of_sample_mean: Reduction function that averages per-sample values.
|
||||
|
||||
Returns:
|
||||
Tuple of `(loss, metrics)` where `loss` is a scalar tensor and `metrics`
|
||||
is a dict containing detached scalars: "loss", "pg_loss",
|
||||
"entropy_loss", "pg_clipfrac", "ppo_kl". Additional keys "kl_loss",
|
||||
"tis", "ois", "tis_clipfrac" are included when the respective features
|
||||
are enabled.
|
||||
"""
|
||||
advantages = torch.cat(batch["advantages"], dim=0)
|
||||
old_log_probs = batch["rollout_log_probs"] if args.use_rollout_logprobs else batch["log_probs"]
|
||||
|
||||
response_lengths = batch["response_lengths"]
|
||||
total_lengths = batch["total_lengths"]
|
||||
|
||||
log_probs_and_entropy = get_log_probs_and_entropy(
|
||||
logits,
|
||||
args=args,
|
||||
unconcat_tokens=batch["unconcat_tokens"],
|
||||
total_lengths=total_lengths,
|
||||
response_lengths=response_lengths,
|
||||
with_entropy=True,
|
||||
)
|
||||
|
||||
log_probs = log_probs_and_entropy["log_probs"]
|
||||
|
||||
# Pre-gather log probs if needed by OPSM or GSPO to avoid duplicate gathering
|
||||
need_full_log_probs = args.use_opsm or args.advantage_estimator == "gspo"
|
||||
|
||||
full_log_probs = None
|
||||
full_old_log_probs = None
|
||||
if need_full_log_probs:
|
||||
full_log_probs = [
|
||||
all_gather_with_cp(log_prob, total_length, response_length)
|
||||
for log_prob, total_length, response_length in zip(
|
||||
log_probs, total_lengths, response_lengths, strict=False
|
||||
)
|
||||
]
|
||||
full_old_log_probs = [
|
||||
all_gather_with_cp(old_log_prob, total_length, response_length)
|
||||
for old_log_prob, total_length, response_length in zip(
|
||||
old_log_probs, total_lengths, response_lengths, strict=False
|
||||
)
|
||||
]
|
||||
|
||||
# Compute OPSM mask if enabled
|
||||
if args.use_opsm:
|
||||
opsm_mask, opsm_clipfrac = compute_opsm_mask(
|
||||
args=args,
|
||||
full_log_probs=full_log_probs,
|
||||
full_old_log_probs=full_old_log_probs,
|
||||
advantages=batch["advantages"],
|
||||
loss_masks=batch["loss_masks"],
|
||||
)
|
||||
|
||||
# Compute KL divergence (GSPO uses sequence-level KL, others use per-token KL)
|
||||
if args.advantage_estimator == "gspo":
|
||||
ppo_kl = compute_gspo_kl(
|
||||
full_log_probs=full_log_probs,
|
||||
full_old_log_probs=full_old_log_probs,
|
||||
local_log_probs=log_probs,
|
||||
loss_masks=batch["loss_masks"],
|
||||
)
|
||||
old_log_probs = torch.cat(old_log_probs, dim=0)
|
||||
log_probs = torch.cat(log_probs, dim=0)
|
||||
else:
|
||||
old_log_probs = torch.cat(old_log_probs, dim=0)
|
||||
log_probs = torch.cat(log_probs, dim=0)
|
||||
ppo_kl = old_log_probs - log_probs
|
||||
|
||||
pg_loss, pg_clipfrac = compute_policy_loss(ppo_kl, advantages, args.eps_clip, args.eps_clip_high)
|
||||
|
||||
if args.use_opsm:
|
||||
pg_loss = pg_loss * opsm_mask
|
||||
|
||||
# Apply off-policy correction using importance sampling if enabled
|
||||
if args.get_mismatch_metrics or args.use_tis:
|
||||
# NOTE:
|
||||
# `tis_func` may apply rejection-sampling style masking (RS) and return `modified_response_masks`.
|
||||
# We rebuild `sum_of_sample_mean` with those masks to correct denominators for loss/backprop.
|
||||
#
|
||||
# However, mismatch/TIS/RS metrics (e.g., "truncate_fraction") are often defined over the
|
||||
# *pre-RS* valid tokens. If we aggregate metrics with `modified_response_masks`, the rejected
|
||||
# tokens are excluded from the denominator and the metric can be artificially driven to 0.
|
||||
# Keep a copy of the original reducer (based on `batch["loss_masks"]`) for metric aggregation.
|
||||
sum_of_sample_mean_for_mismatch_metrics = sum_of_sample_mean
|
||||
|
||||
assert "rollout_log_probs" in batch, "rollout_log_probs must be provided for TIS"
|
||||
|
||||
ois = (-ppo_kl).exp()
|
||||
tis_kwargs = {
|
||||
"args": args,
|
||||
"pg_loss": pg_loss,
|
||||
"train_log_probs": batch["log_probs"],
|
||||
"rollout_log_probs": batch["rollout_log_probs"],
|
||||
"loss_masks": batch["loss_masks"],
|
||||
"total_lengths": total_lengths,
|
||||
"response_lengths": response_lengths,
|
||||
}
|
||||
|
||||
if args.custom_tis_function_path is not None:
|
||||
tis_func = load_function(args.custom_tis_function_path)
|
||||
else:
|
||||
tis_func = vanilla_tis_function
|
||||
pg_loss, modified_response_masks, tis_metrics = tis_func(**tis_kwargs)
|
||||
|
||||
# [decouple IS and rejection] Rebuild sum_of_sample_mean with modified_response_masks for denominator correction
|
||||
# modified_response_masks will be sliced with cp in get_sum_of_sample_mean
|
||||
sum_of_sample_mean = get_sum_of_sample_mean(
|
||||
total_lengths, response_lengths, modified_response_masks, args.calculate_per_token_loss
|
||||
)
|
||||
|
||||
pg_loss = sum_of_sample_mean(pg_loss)
|
||||
pg_clipfrac = sum_of_sample_mean(pg_clipfrac)
|
||||
ppo_kl = sum_of_sample_mean(ppo_kl)
|
||||
|
||||
# entropy loss
|
||||
entropy = log_probs_and_entropy["entropy"]
|
||||
entropy = torch.cat(entropy, dim=0)
|
||||
entropy_loss = sum_of_sample_mean(entropy)
|
||||
|
||||
loss = pg_loss - args.entropy_coef * entropy_loss
|
||||
|
||||
if args.use_kl_loss:
|
||||
ref_log_probs = batch["ref_log_probs"]
|
||||
ref_log_probs = torch.cat(ref_log_probs, dim=0)
|
||||
importance_ratio = None
|
||||
if args.use_unbiased_kl:
|
||||
importance_ratio = torch.exp(log_probs - old_log_probs)
|
||||
kl = compute_approx_kl(
|
||||
log_probs,
|
||||
ref_log_probs,
|
||||
kl_loss_type=args.kl_loss_type,
|
||||
importance_ratio=importance_ratio,
|
||||
)
|
||||
kl_loss = sum_of_sample_mean(kl)
|
||||
|
||||
loss = loss + args.kl_loss_coef * kl_loss
|
||||
|
||||
# make sure the gradient could backprop correctly.
|
||||
if log_probs.numel() == 0:
|
||||
loss += 0 * logits.sum()
|
||||
|
||||
train_rollout_logprob_abs_diff = None
|
||||
importance_weight_mean = None
|
||||
importance_weight_std = None
|
||||
if "rollout_log_probs" in batch and batch["rollout_log_probs"]:
|
||||
rollout_log_probs = torch.cat(batch["rollout_log_probs"], dim=0)
|
||||
train_rollout_logprob_abs_diff = sum_of_sample_mean((old_log_probs - rollout_log_probs).abs())
|
||||
iw = torch.exp(log_probs.detach() - rollout_log_probs)
|
||||
importance_weight_mean = sum_of_sample_mean(iw)
|
||||
importance_weight_std = sum_of_sample_mean((iw - 1).pow(2)).sqrt()
|
||||
|
||||
reported_loss = {
|
||||
"loss": loss.clone().detach(),
|
||||
"pg_loss": pg_loss.clone().detach(),
|
||||
"entropy_loss": entropy_loss.clone().detach(),
|
||||
"pg_clipfrac": pg_clipfrac.clone().detach(),
|
||||
"ppo_kl": ppo_kl.clone().detach(),
|
||||
}
|
||||
|
||||
if train_rollout_logprob_abs_diff is not None:
|
||||
reported_loss["train_rollout_logprob_abs_diff"] = train_rollout_logprob_abs_diff.clone().detach()
|
||||
if importance_weight_mean is not None:
|
||||
reported_loss["importance_weight_mean"] = importance_weight_mean.clone().detach()
|
||||
reported_loss["importance_weight_std"] = importance_weight_std.clone().detach()
|
||||
|
||||
if args.use_kl_loss:
|
||||
reported_loss["kl_loss"] = kl_loss.clone().detach()
|
||||
|
||||
if args.get_mismatch_metrics or args.use_tis:
|
||||
# Aggregate mismatch/TIS/RS related metrics with the *pre-RS* masks.
|
||||
# See comment above where `sum_of_sample_mean_for_mismatch_metrics` is defined.
|
||||
reported_loss["ois"] = sum_of_sample_mean_for_mismatch_metrics(ois).clone().detach()
|
||||
# Assume all metrics are already cloned and detached
|
||||
for metric_key, metric_value in tis_metrics.items():
|
||||
key_name = f"{metric_key}"
|
||||
reported_loss[key_name] = sum_of_sample_mean_for_mismatch_metrics(metric_value)
|
||||
|
||||
if args.use_opsm:
|
||||
reported_loss["opsm_clipfrac"] = opsm_clipfrac
|
||||
|
||||
return loss, reported_loss
|
||||
|
||||
|
||||
def value_loss_function(
|
||||
args: Namespace,
|
||||
batch: RolloutBatch,
|
||||
logits: torch.Tensor,
|
||||
sum_of_sample_mean: Callable[[torch.Tensor], torch.Tensor],
|
||||
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
||||
"""Compute clipped value loss and metrics.
|
||||
|
||||
Extracts current value predictions from `logits`, compares them against
|
||||
stored old values with clipping, and computes the maximum of clipped and
|
||||
unclipped squared errors (PPO-style value clipping).
|
||||
|
||||
Args:
|
||||
args: Configuration containing `value_clip` threshold.
|
||||
batch: Mini-batch with "values" (old predictions), "returns",
|
||||
"unconcat_tokens", "total_lengths", and "response_lengths".
|
||||
logits: Value head output with shape `[1, T, 1]`.
|
||||
sum_of_sample_mean: Reduction function that averages per-sample values.
|
||||
|
||||
Returns:
|
||||
Tuple of `(loss, metrics)` where `loss` is a scalar tensor and
|
||||
`metrics` contains detached scalars "value_loss" and "value_clipfrac".
|
||||
"""
|
||||
old_values = torch.cat(batch["values"], dim=0)
|
||||
|
||||
values = get_values(
|
||||
logits,
|
||||
args=args,
|
||||
unconcat_tokens=batch["unconcat_tokens"],
|
||||
total_lengths=batch["total_lengths"],
|
||||
response_lengths=batch["response_lengths"],
|
||||
)
|
||||
values = torch.cat([value.flatten() for value in values["values"]], dim=0)
|
||||
|
||||
returns = torch.cat(batch["returns"], dim=0)
|
||||
|
||||
values_clipfrac = torch.abs(values - old_values) > args.value_clip
|
||||
values_clipped = old_values + (values - old_values).clamp(-args.value_clip, args.value_clip)
|
||||
surr1 = (values_clipped - returns) ** 2
|
||||
surr2 = (values - returns) ** 2
|
||||
loss = torch.max(surr1, surr2)
|
||||
|
||||
loss = sum_of_sample_mean(loss)
|
||||
values_clipfrac = sum_of_sample_mean(values_clipfrac.float())
|
||||
|
||||
# make sure the gradient could backprop correctly.
|
||||
if values.numel() == 0:
|
||||
loss += 0 * values.sum()
|
||||
|
||||
reported_loss = {
|
||||
"value_loss": loss.clone().detach(),
|
||||
"value_clipfrac": values_clipfrac.clone().detach(),
|
||||
}
|
||||
|
||||
return loss, reported_loss
|
||||
|
||||
|
||||
def loss_function(
|
||||
args: Namespace,
|
||||
batch: RolloutBatch,
|
||||
num_microbatches: int,
|
||||
logits: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, int | torch.Tensor, dict[str, list[str] | torch.Tensor]]:
|
||||
"""Dispatch to the configured loss and rescale for Megatron integration.
|
||||
|
||||
Selects one of "policy_loss", "value_loss", or a custom loss
|
||||
function based on `args.loss_type`, computes the loss and metrics, then
|
||||
rescales the loss by micro-batch and parallelism factors to integrate with
|
||||
Megatron's gradient accumulation.
|
||||
|
||||
Args:
|
||||
args: Configuration specifying `loss_type`, `calculate_per_token_loss`,
|
||||
`global_batch_size`, and optionally `custom_loss_function_path`.
|
||||
batch: Mini-batch with "loss_masks", "response_lengths", and other
|
||||
keys required by the selected loss function.
|
||||
num_microbatches: Number of gradient accumulation steps.
|
||||
logits: Model outputs (policy or value head).
|
||||
|
||||
Returns:
|
||||
Tuple of `(scaled_loss, normalizer, logging_dict)` where:
|
||||
- `scaled_loss` is the loss tensor (scalar) rescaled for Megatron.
|
||||
- `normalizer` is `num_tokens` (scalar tensor) if
|
||||
`args.calculate_per_token_loss` is True, else `1` (int).
|
||||
- `logging_dict` has keys "keys" (list of str metric names) and
|
||||
"values" (1D tensor: [count, metric1, metric2, ...]).
|
||||
"""
|
||||
num_tokens = sum([torch.clamp_min(loss_mask.sum(), 1) for loss_mask in batch["loss_masks"]])
|
||||
num_samples = len(batch["response_lengths"])
|
||||
|
||||
sum_of_sample_mean = get_sum_of_sample_mean(
|
||||
batch["total_lengths"],
|
||||
batch["response_lengths"],
|
||||
batch["loss_masks"],
|
||||
args.calculate_per_token_loss,
|
||||
)
|
||||
|
||||
loss_type = args.loss_type
|
||||
|
||||
match loss_type:
|
||||
case "policy_loss":
|
||||
func = policy_loss_function
|
||||
case "value_loss":
|
||||
func = value_loss_function
|
||||
case "custom_loss":
|
||||
func = load_function(args.custom_loss_function_path)
|
||||
case _:
|
||||
raise ValueError(f"Unknown loss type: {loss_type}")
|
||||
|
||||
if args.recompute_loss_function:
|
||||
loss, log = checkpoint(func, args, batch, logits, sum_of_sample_mean)
|
||||
else:
|
||||
loss, log = func(args, batch, logits, sum_of_sample_mean)
|
||||
|
||||
# Here we need to divide by cp_size because to cancel the multiply in Megatron.
|
||||
if not args.calculate_per_token_loss:
|
||||
loss = (
|
||||
loss
|
||||
* num_microbatches
|
||||
/ args.global_batch_size
|
||||
* mpu.get_data_parallel_world_size(with_context_parallel=True)
|
||||
)
|
||||
else:
|
||||
loss = loss * mpu.get_context_parallel_world_size()
|
||||
|
||||
return (
|
||||
loss,
|
||||
torch.tensor(num_tokens if args.calculate_per_token_loss else 1, device=logits.device),
|
||||
{
|
||||
"keys": list(log.keys()),
|
||||
"values": torch.tensor(
|
||||
[
|
||||
num_samples if not args.calculate_per_token_loss else num_tokens,
|
||||
]
|
||||
+ list(log.values()),
|
||||
device=logits.device,
|
||||
),
|
||||
},
|
||||
)
|
||||
88
slime/backends/megatron_utils/megatron_to_hf/__init__.py
Normal file
88
slime/backends/megatron_utils/megatron_to_hf/__init__.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from .deepseekv3 import convert_deepseekv3_to_hf
|
||||
from .glm4 import convert_glm4_to_hf
|
||||
from .glm4moe import convert_glm4moe_to_hf
|
||||
from .llama import convert_llama_to_hf
|
||||
from .mimo import convert_mimo_to_hf
|
||||
from .processors.padding_remover import remove_padding
|
||||
from .processors.quantizer import quantize_params
|
||||
from .qwen2 import convert_qwen2_to_hf
|
||||
from .qwen3_next import convert_qwen3_next_to_hf
|
||||
from .qwen3moe import convert_qwen3moe_to_hf
|
||||
|
||||
|
||||
# TODO unify w/ `convert_to_hf`
|
||||
def postprocess_hf_param(args, megatron_param_name, hf_param_name, param):
|
||||
param = remove_padding(megatron_param_name, param, args.vocab_size)
|
||||
# TODO support quant
|
||||
return param
|
||||
|
||||
|
||||
# TODO optimize code details
|
||||
def convert_to_hf(args, model_name, name, param, quantization_config=None):
|
||||
param = remove_padding(name, param, args.vocab_size)
|
||||
|
||||
converted_named_tensors = _convert_to_hf_core(args, model_name, name, param)
|
||||
|
||||
if not quantization_config:
|
||||
return converted_named_tensors
|
||||
|
||||
return quantize_params(args, name, converted_named_tensors, quantization_config)
|
||||
|
||||
|
||||
# TODO optimize
|
||||
_cached_tensors = {}
|
||||
|
||||
|
||||
# TODO optimize code details
|
||||
def _convert_to_hf_core(args, model_name, name, param):
|
||||
if "glm4moe" in model_name:
|
||||
converted_named_tensors = convert_glm4moe_to_hf(args, name, param)
|
||||
elif "glm4" in model_name:
|
||||
converted_named_tensors = convert_glm4_to_hf(args, name, param)
|
||||
elif "qwen3moe" in model_name:
|
||||
converted_named_tensors = convert_qwen3moe_to_hf(args, name, param)
|
||||
elif "qwen3next" in model_name:
|
||||
converted_named_tensors = convert_qwen3_next_to_hf(args, name, param)
|
||||
elif "qwen2" in model_name or "qwen3" in model_name:
|
||||
converted_named_tensors = convert_qwen2_to_hf(args, name, param)
|
||||
elif "deepseekv3" in model_name:
|
||||
converted_named_tensors = convert_deepseekv3_to_hf(args, name, param)
|
||||
|
||||
elif "llama" in model_name:
|
||||
converted_named_tensors = convert_llama_to_hf(args, name, param)
|
||||
elif "mimo" in model_name:
|
||||
converted_named_tensors = convert_mimo_to_hf(args, name, param)
|
||||
else:
|
||||
raise ValueError(f"Unsupported model: {model_name}")
|
||||
|
||||
# to compatible with sglang implementation
|
||||
if args.q_lora_rank is not None:
|
||||
old_converted_named_tensors = converted_named_tensors
|
||||
converted_named_tensors = []
|
||||
for converted_name, converted_param in old_converted_named_tensors:
|
||||
if "q_a_proj" in converted_name:
|
||||
pair_name = converted_name.replace("q_a_proj", "kv_a_proj_with_mqa")
|
||||
if pair_name in _cached_tensors:
|
||||
converted_named_tensors += [
|
||||
(converted_name, converted_param),
|
||||
(pair_name, _cached_tensors[pair_name]),
|
||||
]
|
||||
del _cached_tensors[pair_name]
|
||||
else:
|
||||
_cached_tensors[converted_name] = converted_param
|
||||
elif "kv_a_proj_with_mqa" in converted_name:
|
||||
pair_name = converted_name.replace("kv_a_proj_with_mqa", "q_a_proj")
|
||||
if pair_name in _cached_tensors:
|
||||
converted_named_tensors += [
|
||||
(converted_name, converted_param),
|
||||
(pair_name, _cached_tensors[pair_name]),
|
||||
]
|
||||
del _cached_tensors[pair_name]
|
||||
else:
|
||||
_cached_tensors[converted_name] = converted_param
|
||||
else:
|
||||
converted_named_tensors.append((converted_name, converted_param))
|
||||
return converted_named_tensors
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
132
slime/backends/megatron_utils/megatron_to_hf/deepseekv3.py
Normal file
132
slime/backends/megatron_utils/megatron_to_hf/deepseekv3.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def convert_deepseekv3_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
|
||||
# experts
|
||||
expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if match:
|
||||
rest, expert_idx = match.groups()
|
||||
if rest == "linear_fc1":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.up_proj.weight", up_weight),
|
||||
]
|
||||
return outputs
|
||||
elif rest == "linear_fc2":
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.weight", param),
|
||||
]
|
||||
return outputs
|
||||
else:
|
||||
raise ValueError(f"Unknown expert parameter name: {name}")
|
||||
|
||||
# shared expert
|
||||
shared_expert_pattern = r"mlp.shared_experts\.(.+)"
|
||||
match = re.match(shared_expert_pattern, rest)
|
||||
if match:
|
||||
rest = match.groups()[0]
|
||||
if rest == "linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.shared_experts.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.shared_experts.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.shared_experts.down_proj.weight", param)]
|
||||
else:
|
||||
raise ValueError(f"Unknown shared expert parameter name: {name}")
|
||||
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_q_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_q_down_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_a_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_q_up_proj.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_a_layernorm.weight", param)]
|
||||
elif rest == "self_attention.linear_q_up_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_b_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.bias":
|
||||
param = param.view(args.num_query_groups, -1)
|
||||
q_bias, k_bias, v_bias = torch.split(
|
||||
param,
|
||||
split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim],
|
||||
dim=1,
|
||||
)
|
||||
q_bias = q_bias.contiguous().flatten()
|
||||
k_bias = k_bias.contiguous().flatten()
|
||||
v_bias = v_bias.contiguous().flatten()
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight" or rest == "input_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "self_attention.linear_kv_down_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.kv_a_proj_with_mqa.weight", param)]
|
||||
elif rest == "self_attention.linear_kv_up_proj.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.kv_a_layernorm.weight", param)]
|
||||
elif rest == "self_attention.linear_kv_up_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.kv_b_proj.weight", param)]
|
||||
elif rest == "pre_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "mlp.router.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)]
|
||||
elif rest == "mlp.router.expert_bias":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.e_score_correction_bias", param)]
|
||||
|
||||
mtp_layer_pattern = r"module\.module\.mtp\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(mtp_layer_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
layer_idx = int(layer_idx) + args.num_layers
|
||||
if rest == "eh_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.eh_proj.weight", param)]
|
||||
elif rest == "enorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.enorm.weight", param)]
|
||||
elif rest == "hnorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.hnorm.weight", param)]
|
||||
elif rest == "final_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.shared_head.norm.weight", param)]
|
||||
else:
|
||||
name = f"module.module.decoder.layers.{layer_idx}.{rest}"
|
||||
name = name.replace("transformer_layer.", "")
|
||||
return convert_deepseekv3_to_hf(args, name, param)
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
78
slime/backends/megatron_utils/megatron_to_hf/glm4.py
Normal file
78
slime/backends/megatron_utils/megatron_to_hf/glm4.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
import torch
|
||||
|
||||
|
||||
def convert_glm4_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.weight":
|
||||
|
||||
param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
|
||||
q_param, k_param, v_param = torch.split(param, split_size_or_sections=[value_num_per_group, 1, 1], dim=1)
|
||||
q_param = q_param.reshape(-1, args.hidden_size)
|
||||
k_param = k_param.reshape(-1, args.hidden_size)
|
||||
v_param = v_param.reshape(-1, args.hidden_size)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
|
||||
]
|
||||
elif rest == "self_attention.linear_qkv.bias":
|
||||
param = param.view(args.num_query_groups, -1)
|
||||
q_bias, k_bias, v_bias = torch.split(
|
||||
param,
|
||||
split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim],
|
||||
dim=1,
|
||||
)
|
||||
q_bias = q_bias.contiguous().flatten()
|
||||
k_bias = k_bias.contiguous().flatten()
|
||||
v_bias = v_bias.contiguous().flatten()
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_up_proj.weight", param),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
|
||||
# qk norm
|
||||
elif rest == "self_attention.q_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_norm.weight", param)]
|
||||
elif rest == "self_attention.k_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
|
||||
|
||||
# sandwitch norm
|
||||
elif rest == "post_self_attn_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_self_attn_layernorm.weight", param)]
|
||||
elif rest == "post_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_mlp_layernorm.weight", param)]
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
142
slime/backends/megatron_utils/megatron_to_hf/glm4moe.py
Normal file
142
slime/backends/megatron_utils/megatron_to_hf/glm4moe.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def convert_glm4moe_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
|
||||
# experts
|
||||
expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if match:
|
||||
rest, expert_idx = match.groups()
|
||||
if rest == "linear_fc1":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.up_proj.weight", up_weight),
|
||||
]
|
||||
return outputs
|
||||
elif rest == "linear_fc2":
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.weight", param),
|
||||
]
|
||||
return outputs
|
||||
else:
|
||||
raise ValueError(f"Unknown expert parameter name: {name}")
|
||||
|
||||
# shared expert
|
||||
shared_expert_pattern = r"mlp.shared_experts\.(.+)"
|
||||
match = re.match(shared_expert_pattern, rest)
|
||||
if match:
|
||||
rest = match.groups()[0]
|
||||
if rest == "linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.shared_experts.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.shared_experts.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "linear_fc2.weight":
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.shared_experts.down_proj.weight", param),
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unknown shared expert parameter name: {name}")
|
||||
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.weight":
|
||||
|
||||
param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
|
||||
q_param, k_param, v_param = torch.split(param, split_size_or_sections=[value_num_per_group, 1, 1], dim=1)
|
||||
q_param = q_param.reshape(-1, args.hidden_size)
|
||||
k_param = k_param.reshape(-1, args.hidden_size)
|
||||
v_param = v_param.reshape(-1, args.hidden_size)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
|
||||
]
|
||||
elif rest == "self_attention.linear_qkv.bias":
|
||||
param = param.view(args.num_query_groups, -1)
|
||||
q_bias, k_bias, v_bias = torch.split(
|
||||
param,
|
||||
split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim],
|
||||
dim=1,
|
||||
)
|
||||
q_bias = q_bias.contiguous().flatten()
|
||||
k_bias = k_bias.contiguous().flatten()
|
||||
v_bias = v_bias.contiguous().flatten()
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "post_self_attn_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_self_attn_layernorm.weight", param)]
|
||||
elif rest == "post_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_mlp_layernorm.weight", param)]
|
||||
elif rest == "pre_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "mlp.router.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)]
|
||||
elif rest == "mlp.router.expert_bias":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.e_score_correction_bias", param)]
|
||||
|
||||
# qk norm
|
||||
elif rest == "self_attention.q_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_norm.weight", param)]
|
||||
elif rest == "self_attention.k_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
|
||||
|
||||
mtp_layer_pattern = r"module\.module\.mtp\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(mtp_layer_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
layer_idx = int(layer_idx) + args.num_layers
|
||||
if rest == "eh_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.eh_proj.weight", param)]
|
||||
elif rest == "enorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.enorm.weight", param)]
|
||||
elif rest == "hnorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.hnorm.weight", param)]
|
||||
elif rest == "final_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.shared_head.norm.weight", param)]
|
||||
else:
|
||||
name = f"module.module.decoder.layers.{layer_idx}.{rest}"
|
||||
name = name.replace("transformer_layer.", "")
|
||||
return convert_glm4moe_to_hf(args, name, param)
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
56
slime/backends/megatron_utils/megatron_to_hf/llama.py
Normal file
56
slime/backends/megatron_utils/megatron_to_hf/llama.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
import torch
|
||||
|
||||
|
||||
def convert_llama_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.weight":
|
||||
# Split QKV weight for Llama
|
||||
param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
|
||||
q_param, k_param, v_param = torch.split(param, split_size_or_sections=[value_num_per_group, 1, 1], dim=1)
|
||||
q_param = q_param.reshape(-1, args.hidden_size)
|
||||
k_param = k_param.reshape(-1, args.hidden_size)
|
||||
v_param = v_param.reshape(-1, args.hidden_size)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
# Split gate and up projections for SwiGLU
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "pre_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
78
slime/backends/megatron_utils/megatron_to_hf/mimo.py
Normal file
78
slime/backends/megatron_utils/megatron_to_hf/mimo.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
import torch
|
||||
from .qwen2 import convert_qwen2_to_hf
|
||||
|
||||
|
||||
def convert_mimo_to_hf(args, name, param):
|
||||
"""
|
||||
Convert MiMo model parameters from Megatron to HuggingFace format.
|
||||
|
||||
MiMo extends Qwen2 with MTP (Multi-Token Prediction) layers.
|
||||
"""
|
||||
|
||||
if "mtp" in name:
|
||||
return convert_mimo_mtp_param(args, name, param)
|
||||
|
||||
return convert_qwen2_to_hf(args, name, param)
|
||||
|
||||
|
||||
def convert_mimo_mtp_param(args, name, param):
|
||||
"""
|
||||
Convert MTP layer parameters from Megatron to HuggingFace format.
|
||||
|
||||
MTP layers in MiMo contain:
|
||||
- LayerNorms (token_layernorm, hidden_layernorm, final_layernorm)
|
||||
- Input projection (input_proj)
|
||||
- Self attention (reuses Qwen2 attention structure)
|
||||
- MLP (reuses Qwen2 MLP structure)
|
||||
|
||||
Based on MimoBridge._convert_mtp_param logic (reverse mapping)
|
||||
"""
|
||||
mtp_pattern = r"module\.module\.mtp\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(mtp_pattern, name)
|
||||
|
||||
if not match:
|
||||
raise ValueError(f"Invalid MTP parameter name: {name}")
|
||||
|
||||
layer_idx, component = match.groups()
|
||||
|
||||
# Direct mappings for MTP-specific components (Megatron -> HF)
|
||||
# Based on MimoBridge direct_name_mapping (reversed)
|
||||
direct_mappings = {
|
||||
"enorm.weight": f"model.mtp_layers.{layer_idx}.token_layernorm.weight",
|
||||
"hnorm.weight": f"model.mtp_layers.{layer_idx}.hidden_layernorm.weight",
|
||||
"eh_proj.weight": f"model.mtp_layers.{layer_idx}.input_proj.weight",
|
||||
"final_layernorm.weight": f"model.mtp_layers.{layer_idx}.final_layernorm.weight",
|
||||
}
|
||||
if component == "eh_proj.weight":
|
||||
first_half, second_half = param.chunk(2, dim=1)
|
||||
param = torch.cat([second_half, first_half], dim=1)
|
||||
|
||||
# Check direct mappings first
|
||||
if component in direct_mappings:
|
||||
return [(direct_mappings[component], param)]
|
||||
|
||||
# Handle transformer_layer components
|
||||
if component.startswith("transformer_layer."):
|
||||
# Remove "transformer_layer." prefix
|
||||
transformer_component = component[len("transformer_layer.") :]
|
||||
|
||||
# Create proxy name for reusing existing Qwen2 conversion functions
|
||||
proxy_name = f"module.module.decoder.layers.{layer_idx}.{transformer_component}"
|
||||
|
||||
# Use existing convert_qwen2_to_hf function for transformer components
|
||||
results = convert_qwen2_to_hf(args, proxy_name, param)
|
||||
|
||||
# Replace model.layers with mtp_layers in results
|
||||
converted_results = []
|
||||
for hf_name, hf_param in results:
|
||||
# Replace model.layers.{idx} with mtp_layers.{idx}
|
||||
hf_name = hf_name.replace(f"model.layers.{layer_idx}", f"model.mtp_layers.{layer_idx}")
|
||||
converted_results.append((hf_name, hf_param))
|
||||
|
||||
return converted_results
|
||||
|
||||
raise ValueError(f"Unknown MTP component: {component} in {name}")
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import torch
|
||||
|
||||
from slime.backends.megatron_utils.misc_utils import strip_param_name_prefix
|
||||
|
||||
|
||||
def remove_padding(name: str, param: torch.Tensor, vocab_size: int) -> torch.Tensor:
|
||||
"""
|
||||
Remove vocab padding: param[:vocab_size] for embedding/output layers, else unchanged.
|
||||
"""
|
||||
if strip_param_name_prefix(name) in {"embedding.word_embeddings.weight", "output_layer.weight"}:
|
||||
return param[:vocab_size]
|
||||
return param
|
||||
@@ -0,0 +1,110 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
from slime.utils.fp8_kernel import blockwise_cast_to_fp8_triton
|
||||
|
||||
from ...sglang import quant_weight_ue8m0, should_deepgemm_weight_requant_ue8m0, transform_scale_ue8m0
|
||||
|
||||
|
||||
def quantize_params(args, megatron_name, converted_named_params, quantization_config):
|
||||
if quantization_config is None:
|
||||
return converted_named_params
|
||||
assert quantization_config["quant_method"] == "fp8"
|
||||
assert quantization_config["fmt"] == "e4m3"
|
||||
assert quantization_config["activation_scheme"] == "dynamic"
|
||||
weight_block_size = quantization_config.get("weight_block_size", None)
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, megatron_name)
|
||||
|
||||
if not match:
|
||||
# check mtp layers
|
||||
mtp_layer_pattern = r"module\.module\.mtp\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(mtp_layer_pattern, megatron_name)
|
||||
if not match:
|
||||
return converted_named_params
|
||||
layer_idx, rest = match.groups()
|
||||
rest = rest.replace("transformer_layer.", "")
|
||||
else:
|
||||
layer_idx, rest = match.groups()
|
||||
|
||||
# experts
|
||||
expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if match:
|
||||
rest, expert_idx = match.groups()
|
||||
if rest in [
|
||||
"linear_fc1",
|
||||
"linear_fc2",
|
||||
]:
|
||||
quantize_named_params = []
|
||||
for converted_name, param in converted_named_params:
|
||||
# skip bf16 weight_scale and input_scale
|
||||
# TODO: find a clearer way.
|
||||
if converted_name.endswith("_scale"):
|
||||
continue
|
||||
quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size))
|
||||
|
||||
return quantize_named_params
|
||||
|
||||
# shared expert
|
||||
shared_expert_pattern = r"mlp.shared_experts\.(.+)"
|
||||
match = re.match(shared_expert_pattern, rest)
|
||||
if match:
|
||||
rest = match.groups()[0]
|
||||
if rest in [
|
||||
"linear_fc1.weight",
|
||||
"linear_fc2.weight",
|
||||
]:
|
||||
quantize_named_params = []
|
||||
for converted_name, param in converted_named_params:
|
||||
quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size))
|
||||
|
||||
return quantize_named_params
|
||||
|
||||
if rest in [
|
||||
"self_attention.linear_proj.weight",
|
||||
"self_attention.linear_qkv.weight",
|
||||
"mlp.linear_fc1.weight",
|
||||
"mlp.linear_fc2.weight",
|
||||
# mla
|
||||
"self_attention.linear_q_proj.weight",
|
||||
"self_attention.linear_q_down_proj.weight",
|
||||
"self_attention.linear_q_up_proj.weight",
|
||||
"self_attention.linear_kv_down_proj.weight",
|
||||
"self_attention.linear_kv_up_proj.weight",
|
||||
]:
|
||||
quantize_named_params = []
|
||||
for converted_name, param in converted_named_params:
|
||||
quantize_named_params.extend(_quantize_param(converted_name, param, weight_block_size))
|
||||
|
||||
return quantize_named_params
|
||||
|
||||
# for other parameters, we just return the original converted_named_params
|
||||
return converted_named_params
|
||||
|
||||
|
||||
def _quantize_param(name, weight, weight_block_size):
|
||||
assert name.endswith(".weight"), f"Expected weight parameter, got {name}"
|
||||
FP8_MIN = torch.finfo(torch.float8_e4m3fn).min
|
||||
FP8_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
if weight_block_size is not None:
|
||||
if should_deepgemm_weight_requant_ue8m0 and should_deepgemm_weight_requant_ue8m0(
|
||||
weight_block_size=weight_block_size
|
||||
):
|
||||
qweight, scale = quant_weight_ue8m0(weight, weight_block_size=weight_block_size)
|
||||
scale = transform_scale_ue8m0(scale, mn=qweight.shape[-2])
|
||||
else:
|
||||
qweight, scale = blockwise_cast_to_fp8_triton(weight, weight_block_size)
|
||||
scale_name = name.replace(".weight", ".weight_scale_inv")
|
||||
else:
|
||||
# per tensor quant
|
||||
scale = weight.abs().max().clamp(min=1e-12).to(torch.float32) / FP8_MAX
|
||||
qweight = (weight / scale).clamp(min=FP8_MIN, max=FP8_MAX).to(torch.float8_e4m3fn)
|
||||
scale = scale.view(1)
|
||||
scale_name = name.replace(".weight", ".weight_scale")
|
||||
return [(name, qweight), (scale_name, scale)]
|
||||
74
slime/backends/megatron_utils/megatron_to_hf/qwen2.py
Normal file
74
slime/backends/megatron_utils/megatron_to_hf/qwen2.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
import torch
|
||||
|
||||
|
||||
def convert_qwen2_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.weight":
|
||||
|
||||
param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
|
||||
q_param, k_param, v_param = torch.split(param, split_size_or_sections=[value_num_per_group, 1, 1], dim=1)
|
||||
q_param = q_param.reshape(-1, args.hidden_size)
|
||||
k_param = k_param.reshape(-1, args.hidden_size)
|
||||
v_param = v_param.reshape(-1, args.hidden_size)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
|
||||
]
|
||||
elif rest == "self_attention.linear_qkv.bias":
|
||||
param = param.view(args.num_query_groups, -1)
|
||||
q_bias, k_bias, v_bias = torch.split(
|
||||
param,
|
||||
split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim],
|
||||
dim=1,
|
||||
)
|
||||
q_bias = q_bias.contiguous().flatten()
|
||||
k_bias = k_bias.contiguous().flatten()
|
||||
v_bias = v_bias.contiguous().flatten()
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
|
||||
# qk norm
|
||||
elif rest == "self_attention.q_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_norm.weight", param)]
|
||||
elif rest == "self_attention.k_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
145
slime/backends/megatron_utils/megatron_to_hf/qwen3_next.py
Normal file
145
slime/backends/megatron_utils/megatron_to_hf/qwen3_next.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def convert_qwen3_next_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
|
||||
# experts
|
||||
expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if match:
|
||||
rest, expert_idx = match.groups()
|
||||
if rest == "linear_fc1":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.up_proj.weight", up_weight),
|
||||
]
|
||||
return outputs
|
||||
elif rest == "linear_fc2":
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.weight", param),
|
||||
]
|
||||
return outputs
|
||||
else:
|
||||
raise ValueError(f"Unknown expert parameter name: {name}")
|
||||
|
||||
# shared expert
|
||||
shared_expert_pattern = r"mlp.shared_experts\.(.+)"
|
||||
match = re.match(shared_expert_pattern, rest)
|
||||
if match:
|
||||
rest = match.groups()[0]
|
||||
if rest == "linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.shared_expert.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.shared_expert.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.shared_expert.down_proj.weight", param)]
|
||||
elif rest == "gate_weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.shared_expert_gate.weight", param)]
|
||||
else:
|
||||
raise ValueError(f"Unknown shared expert parameter name: {name}")
|
||||
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.weight":
|
||||
param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
|
||||
q_param, k_param, v_param = torch.split(
|
||||
param, split_size_or_sections=[2 * value_num_per_group, 1, 1], dim=1
|
||||
)
|
||||
q_param = (
|
||||
q_param.reshape(args.num_query_groups, 2, value_num_per_group, head_dim, args.hidden_size)
|
||||
.transpose(1, 2)
|
||||
.reshape(-1, args.hidden_size)
|
||||
)
|
||||
k_param = k_param.reshape(-1, args.hidden_size)
|
||||
v_param = v_param.reshape(-1, args.hidden_size)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
|
||||
]
|
||||
elif rest == "self_attention.linear_qkv.bias":
|
||||
param = param.view(args.num_query_groups, -1)
|
||||
q_bias, k_bias, v_bias = torch.split(
|
||||
param,
|
||||
split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim],
|
||||
dim=1,
|
||||
)
|
||||
q_bias = q_bias.contiguous().flatten()
|
||||
k_bias = k_bias.contiguous().flatten()
|
||||
v_bias = v_bias.contiguous().flatten()
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "pre_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "mlp.router.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)]
|
||||
elif rest == "mlp.router.expert_bias":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.e_score_correction_bias", param)]
|
||||
|
||||
# qk norm
|
||||
elif rest == "self_attention.q_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_norm.weight", param)]
|
||||
elif rest == "self_attention.k_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
|
||||
elif rest.startswith("self_attention.") and rest[len("self_attention.") :] in [
|
||||
"input_layernorm.weight",
|
||||
# linear attn
|
||||
"linear_attn.A_log",
|
||||
"linear_attn.conv1d.weight",
|
||||
"linear_attn.dt_bias",
|
||||
"linear_attn.in_proj_ba.weight",
|
||||
"linear_attn.in_proj_qkvz.weight",
|
||||
"linear_attn.norm.weight",
|
||||
"linear_attn.out_proj.weight",
|
||||
# gated attn
|
||||
"self_attn.k_norm.weight",
|
||||
"self_attn.k_proj.weight",
|
||||
"self_attn.o_proj.weight",
|
||||
"self_attn.q_norm.weight",
|
||||
"self_attn.q_proj.weight",
|
||||
"self_attn.v_proj.weight",
|
||||
]:
|
||||
rest = rest[len("self_attention.") :]
|
||||
return [(f"model.layers.{layer_idx}.{rest}", param)]
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
120
slime/backends/megatron_utils/megatron_to_hf/qwen3moe.py
Normal file
120
slime/backends/megatron_utils/megatron_to_hf/qwen3moe.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def convert_qwen3moe_to_hf(args, name, param):
|
||||
if name == "module.module.embedding.word_embeddings.weight":
|
||||
return [("model.embed_tokens.weight", param)]
|
||||
if name == "module.module.output_layer.weight":
|
||||
return [("lm_head.weight", param)]
|
||||
if name == "module.module.decoder.final_layernorm.weight":
|
||||
return [("model.norm.weight", param)]
|
||||
|
||||
try:
|
||||
head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
|
||||
except AttributeError:
|
||||
head_dim = args.hidden_size // args.num_attention_heads
|
||||
value_num_per_group = args.num_attention_heads // args.num_query_groups
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if match:
|
||||
layer_idx, rest = match.groups()
|
||||
|
||||
# experts
|
||||
expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if match:
|
||||
rest, expert_idx = match.groups()
|
||||
if rest == "linear_fc1":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.up_proj.weight", up_weight),
|
||||
]
|
||||
return outputs
|
||||
elif rest == "linear_fc2":
|
||||
outputs = [
|
||||
(f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.down_proj.weight", param),
|
||||
]
|
||||
return outputs
|
||||
else:
|
||||
raise ValueError(f"Unknown expert parameter name: {name}")
|
||||
|
||||
# shared expert
|
||||
shared_expert_pattern = r"mlp.shared_experts\.(.+)"
|
||||
match = re.match(shared_expert_pattern, rest)
|
||||
if match:
|
||||
rest = match.groups()[0]
|
||||
if rest == "linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.shared_expert.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.shared_expert.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.shared_expert.down_proj.weight", param)]
|
||||
elif rest == "gate_weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.shared_expert_gate.weight", param)]
|
||||
else:
|
||||
raise ValueError(f"Unknown shared expert parameter name: {name}")
|
||||
|
||||
if rest == "self_attention.linear_proj.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.weight":
|
||||
|
||||
param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
|
||||
q_param, k_param, v_param = torch.split(param, split_size_or_sections=[value_num_per_group, 1, 1], dim=1)
|
||||
q_param = q_param.reshape(-1, args.hidden_size)
|
||||
k_param = k_param.reshape(-1, args.hidden_size)
|
||||
v_param = v_param.reshape(-1, args.hidden_size)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
|
||||
]
|
||||
elif rest == "self_attention.linear_qkv.bias":
|
||||
param = param.view(args.num_query_groups, -1)
|
||||
q_bias, k_bias, v_bias = torch.split(
|
||||
param,
|
||||
split_size_or_sections=[value_num_per_group * head_dim, head_dim, head_dim],
|
||||
dim=1,
|
||||
)
|
||||
q_bias = q_bias.contiguous().flatten()
|
||||
k_bias = k_bias.contiguous().flatten()
|
||||
v_bias = v_bias.contiguous().flatten()
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.self_attn.q_proj.bias", q_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.k_proj.bias", k_bias),
|
||||
(f"model.layers.{layer_idx}.self_attn.v_proj.bias", v_bias),
|
||||
]
|
||||
elif rest == "mlp.linear_fc1.weight":
|
||||
gate_weight, up_weight = param.chunk(2, dim=0)
|
||||
return [
|
||||
(f"model.layers.{layer_idx}.mlp.gate_proj.weight", gate_weight),
|
||||
(f"model.layers.{layer_idx}.mlp.up_proj.weight", up_weight),
|
||||
]
|
||||
elif rest == "mlp.linear_fc2.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.down_proj.weight", param)]
|
||||
elif rest == "self_attention.linear_qkv.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
|
||||
elif rest == "mlp.linear_fc1.layer_norm_weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "pre_mlp_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
|
||||
elif rest == "mlp.router.weight":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)]
|
||||
elif rest == "mlp.router.expert_bias":
|
||||
return [(f"model.layers.{layer_idx}.mlp.gate.e_score_correction_bias", param)]
|
||||
|
||||
# qk norm
|
||||
elif rest == "self_attention.q_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.q_norm.weight", param)]
|
||||
elif rest == "self_attention.k_layernorm.weight":
|
||||
return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
|
||||
|
||||
raise ValueError(f"Unknown parameter name: {name}")
|
||||
8
slime/backends/megatron_utils/misc_utils.py
Normal file
8
slime/backends/megatron_utils/misc_utils.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
def strip_param_name_prefix(name: str):
|
||||
prefix = "module."
|
||||
while name.startswith(prefix):
|
||||
name = name.removeprefix(prefix)
|
||||
return name
|
||||
730
slime/backends/megatron_utils/model.py
Normal file
730
slime/backends/megatron_utils/model.py
Normal file
@@ -0,0 +1,730 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import dataclasses
|
||||
import gc
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from collections.abc import Callable, Sequence
|
||||
from functools import partial
|
||||
|
||||
import torch
|
||||
from megatron.core import mpu
|
||||
from megatron.core.distributed import DistributedDataParallel as DDP
|
||||
from megatron.core.distributed import finalize_model_grads
|
||||
from megatron.core.enums import ModelType
|
||||
from megatron.core.models.gpt import GPTModel
|
||||
from megatron.core.optimizer import OptimizerConfig, get_megatron_optimizer
|
||||
from megatron.core.optimizer.optimizer import MegatronOptimizer
|
||||
from megatron.core.optimizer_param_scheduler import OptimizerParamScheduler
|
||||
from megatron.core.pipeline_parallel import get_forward_backward_func
|
||||
from megatron.core.utils import get_model_config
|
||||
from megatron.training.global_vars import get_args
|
||||
from megatron.training.training import get_model
|
||||
|
||||
from slime.utils import tracking_utils
|
||||
from slime.utils.memory_utils import clear_memory
|
||||
|
||||
from .checkpoint import load_checkpoint, save_checkpoint
|
||||
from .data import DataIterator, get_batch
|
||||
from .loss import loss_function
|
||||
from .model_provider import get_model_provider_func
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_optimizer_param_scheduler(args: Namespace, optimizer: MegatronOptimizer) -> OptimizerParamScheduler:
|
||||
"""Create and configure the optimizer learning-rate/weight-decay scheduler.
|
||||
|
||||
This configures iteration-based schedules derived from the global batch size
|
||||
and run-time arguments.
|
||||
|
||||
Args:
|
||||
args (Namespace): Training/runtime arguments (argparse namespace).
|
||||
optimizer (MegatronOptimizer): Megatron optimizer bound to the model.
|
||||
|
||||
Returns:
|
||||
OptimizerParamScheduler: Initialized scheduler bound to ``optimizer``.
|
||||
"""
|
||||
# Iteration-based training.
|
||||
args.train_iters = args.num_rollout * args.rollout_batch_size * args.n_samples_per_prompt // args.global_batch_size
|
||||
if args.lr_decay_iters is None:
|
||||
args.lr_decay_iters = args.train_iters
|
||||
lr_decay_steps = args.lr_decay_iters * args.global_batch_size
|
||||
wd_incr_steps = args.train_iters * args.global_batch_size
|
||||
wsd_decay_steps = None
|
||||
if args.lr_wsd_decay_iters is not None:
|
||||
wsd_decay_steps = args.lr_wsd_decay_iters * args.global_batch_size
|
||||
if args.lr_warmup_fraction is not None:
|
||||
lr_warmup_steps = args.lr_warmup_fraction * lr_decay_steps
|
||||
else:
|
||||
lr_warmup_steps = args.lr_warmup_iters * args.global_batch_size
|
||||
|
||||
opt_param_scheduler = OptimizerParamScheduler(
|
||||
optimizer,
|
||||
init_lr=args.lr_warmup_init,
|
||||
max_lr=args.lr,
|
||||
min_lr=args.min_lr,
|
||||
lr_warmup_steps=lr_warmup_steps,
|
||||
lr_decay_steps=lr_decay_steps,
|
||||
lr_decay_style=args.lr_decay_style,
|
||||
start_wd=args.start_weight_decay,
|
||||
end_wd=args.end_weight_decay,
|
||||
wd_incr_steps=wd_incr_steps,
|
||||
wd_incr_style=args.weight_decay_incr_style,
|
||||
use_checkpoint_opt_param_scheduler=args.use_checkpoint_opt_param_scheduler,
|
||||
override_opt_param_scheduler=args.override_opt_param_scheduler,
|
||||
wsd_decay_steps=wsd_decay_steps,
|
||||
lr_wsd_decay_style=args.lr_wsd_decay_style,
|
||||
)
|
||||
|
||||
return opt_param_scheduler
|
||||
|
||||
|
||||
def setup_model_and_optimizer(
|
||||
args: Namespace,
|
||||
role: str = "actor",
|
||||
) -> tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler]:
|
||||
"""Build model(s), wrap with DDP, and construct optimizer and scheduler.
|
||||
|
||||
Args:
|
||||
args (Namespace): Training/runtime arguments (argparse namespace).
|
||||
role (str): Logical role of the model (e.g., "actor", "critic").
|
||||
no_wd_decay_cond (Callable[..., bool] | None): Predicate to exclude
|
||||
parameters from weight decay.
|
||||
scale_lr_cond (Callable[..., bool] | None): Predicate to scale LR for
|
||||
selected parameter groups.
|
||||
lr_mult (float): Global learning-rate multiplier for the optimizer.
|
||||
|
||||
Returns:
|
||||
tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler]:
|
||||
- List of model chunks wrapped by ``DDP``.
|
||||
- The constructed ``MegatronOptimizer`` instance.
|
||||
- The learning-rate/weight-decay scheduler tied to the optimizer.
|
||||
"""
|
||||
assert not args.moe_use_upcycling
|
||||
assert args.load is not None or args.pretrained_checkpoint is not None
|
||||
|
||||
model = get_model(get_model_provider_func(args, role), ModelType.encoder_or_decoder)
|
||||
|
||||
# Optimizer
|
||||
kwargs = {}
|
||||
for f in dataclasses.fields(OptimizerConfig):
|
||||
if hasattr(args, f.name):
|
||||
kwargs[f.name] = getattr(args, f.name)
|
||||
config = OptimizerConfig(**kwargs)
|
||||
config.timers = None
|
||||
|
||||
optimizer = get_megatron_optimizer(
|
||||
config=config,
|
||||
model_chunks=model,
|
||||
use_gloo_process_groups=args.enable_gloo_process_groups,
|
||||
)
|
||||
opt_param_scheduler = get_optimizer_param_scheduler(args, optimizer)
|
||||
return model, optimizer, opt_param_scheduler
|
||||
|
||||
|
||||
def enable_forward_pre_hook(model_chunks: Sequence[DDP]) -> None:
|
||||
"""Enable forward pre-hooks for provided DDP-wrapped model chunks.
|
||||
|
||||
Args:
|
||||
model_chunks (Sequence[DDP]): Sequence of DDP modules to enable hooks on.
|
||||
"""
|
||||
for model_chunk in model_chunks:
|
||||
assert isinstance(model_chunk, DDP)
|
||||
model_chunk.enable_forward_pre_hook()
|
||||
|
||||
|
||||
def disable_forward_pre_hook(model_chunks: Sequence[DDP], param_sync: bool = True) -> None:
|
||||
"""Disable forward pre-hooks for provided DDP-wrapped model chunks.
|
||||
|
||||
Args:
|
||||
model_chunks (Sequence[DDP]): Sequence of DDP modules to disable hooks on.
|
||||
param_sync (bool): Whether to synchronize parameters when disabling.
|
||||
"""
|
||||
for model_chunk in model_chunks:
|
||||
assert isinstance(model_chunk, DDP)
|
||||
model_chunk.disable_forward_pre_hook(param_sync=param_sync)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def forward_only(
|
||||
f: Callable[..., dict[str, list[torch.Tensor]]],
|
||||
args: Namespace,
|
||||
model: Sequence[DDP],
|
||||
data_iterator: Sequence[DataIterator],
|
||||
num_microbatches: Sequence[int],
|
||||
store_prefix: str = "",
|
||||
) -> dict[str, list[torch.Tensor]]:
|
||||
"""Run forward passes only and collect non-loss outputs (e.g., logprobs).
|
||||
|
||||
The model is put into evaluation mode, a forward-only pipeline pass is
|
||||
executed, and relevant outputs are aggregated and returned.
|
||||
|
||||
Args:
|
||||
f (Callable[..., dict[str, list[torch.Tensor]]]): Post-forward callback used to
|
||||
compute and package outputs to collect. This should accept a logits
|
||||
tensor as its first positional argument and additional keyword-only
|
||||
arguments; see ``get_log_probs_and_entropy``/``get_values`` in
|
||||
``megatron_utils.loss`` for examples. It will be partially applied
|
||||
so that the callable returned from the internal forward step only
|
||||
requires the logits tensor.
|
||||
args (Namespace): Runtime arguments.
|
||||
model (Sequence[DDP]): Sequence of DDP-wrapped model chunks.
|
||||
data_iterator (Sequence[DataIterator]): Iterable(s) yielding batches for inference.
|
||||
num_microbatches (Sequence[int]): Number of microbatches per rollout step.
|
||||
store_prefix (str): Prefix to prepend to stored output keys.
|
||||
|
||||
Returns:
|
||||
dict[str, list[torch.Tensor]]: Aggregated outputs keyed by ``store_prefix + key``.
|
||||
"""
|
||||
|
||||
# reset data iterator
|
||||
for iterator in data_iterator:
|
||||
iterator.reset()
|
||||
|
||||
config = get_model_config(model[0])
|
||||
|
||||
def forward_step(
|
||||
data_iterator: DataIterator, model: GPTModel, return_schedule_plan: bool = False
|
||||
) -> tuple[torch.Tensor, Callable[[torch.Tensor], dict[str, list[torch.Tensor]]]]:
|
||||
"""Forward step used by Megatron's pipeline engine.
|
||||
|
||||
Args:
|
||||
data_iterator (DataIterator): Input data iterator.
|
||||
model (GPTModel): The GPT model chunk to execute.
|
||||
|
||||
Returns:
|
||||
tuple[torch.Tensor, Callable[[torch.Tensor], dict[str, list[torch.Tensor]]]]:
|
||||
Output tensor(s) and a callable that computes and packages results
|
||||
to be collected by the engine.
|
||||
"""
|
||||
|
||||
assert not return_schedule_plan, "forward_only step should never return schedule plan"
|
||||
|
||||
# Get the batch.
|
||||
batch = get_batch(
|
||||
data_iterator,
|
||||
[
|
||||
"tokens",
|
||||
"loss_masks",
|
||||
"multimodal_train_inputs",
|
||||
"total_lengths",
|
||||
"response_lengths",
|
||||
],
|
||||
args.data_pad_size_multiplier,
|
||||
)
|
||||
unconcat_tokens = batch["unconcat_tokens"]
|
||||
tokens = batch["tokens"]
|
||||
packed_seq_params = batch["packed_seq_params"]
|
||||
total_lengths = batch["total_lengths"]
|
||||
response_lengths = batch["response_lengths"]
|
||||
output_tensor = model(
|
||||
input_ids=tokens,
|
||||
position_ids=None,
|
||||
attention_mask=None,
|
||||
labels=None,
|
||||
packed_seq_params=packed_seq_params,
|
||||
loss_mask=batch["full_loss_masks"],
|
||||
**(batch["multimodal_train_inputs"] if batch["multimodal_train_inputs"] is not None else {}),
|
||||
)
|
||||
|
||||
return output_tensor, partial(
|
||||
f,
|
||||
args=args,
|
||||
unconcat_tokens=unconcat_tokens,
|
||||
total_lengths=total_lengths,
|
||||
response_lengths=response_lengths,
|
||||
with_entropy=args.use_rollout_entropy,
|
||||
)
|
||||
|
||||
# Turn on evaluation mode which disables dropout.
|
||||
for model_module in model:
|
||||
model_module.eval()
|
||||
|
||||
if args.custom_megatron_before_log_prob_hook_path:
|
||||
from slime.utils.misc import load_function
|
||||
|
||||
custom_before_log_prob_hook = load_function(args.custom_megatron_before_log_prob_hook_path)
|
||||
custom_before_log_prob_hook(args, model, store_prefix)
|
||||
|
||||
forward_backward_func = get_forward_backward_func()
|
||||
# Don't care about timing during evaluation
|
||||
config.timers = None
|
||||
forward_data_store = []
|
||||
num_steps_per_rollout = len(num_microbatches)
|
||||
for step_id in range(num_steps_per_rollout):
|
||||
# collect_non_loss_data
|
||||
forward_data_store += forward_backward_func(
|
||||
forward_step_func=forward_step,
|
||||
data_iterator=data_iterator,
|
||||
model=model,
|
||||
num_microbatches=num_microbatches[step_id],
|
||||
seq_length=args.seq_length,
|
||||
micro_batch_size=args.micro_batch_size,
|
||||
forward_only=True,
|
||||
collect_non_loss_data=True,
|
||||
)
|
||||
|
||||
# Move model back to the train mode.
|
||||
for model_module in model:
|
||||
model_module.train()
|
||||
|
||||
rollout_data = {}
|
||||
# Store the results on the last stage
|
||||
if mpu.is_pipeline_last_stage():
|
||||
keys = forward_data_store[0].keys()
|
||||
for key in keys:
|
||||
values = []
|
||||
for value in forward_data_store:
|
||||
assert isinstance(value[key], list)
|
||||
values += value[key]
|
||||
|
||||
if args.use_dynamic_batch_size:
|
||||
# TODO: This is ugly... Find a better way to make the data have the same order.
|
||||
# TODO: move this out of the loop.
|
||||
origin_values = [None] * len(values)
|
||||
origin_indices = sum(data_iterator[0].micro_batch_indices, [])
|
||||
for value, origin_index in zip(values, origin_indices, strict=False):
|
||||
origin_values[origin_index] = value
|
||||
values = origin_values
|
||||
rollout_data[f"{store_prefix}{key}"] = values
|
||||
return rollout_data
|
||||
|
||||
|
||||
def train_one_step(
|
||||
args: Namespace,
|
||||
rollout_id: int,
|
||||
step_id: int,
|
||||
data_iterator: Sequence[DataIterator],
|
||||
model: Sequence[DDP],
|
||||
optimizer: MegatronOptimizer,
|
||||
opt_param_scheduler: OptimizerParamScheduler,
|
||||
num_microbatches: int,
|
||||
) -> tuple[dict[str, float], float]:
|
||||
"""Execute a single pipeline-parallel training step.
|
||||
|
||||
Runs forward/backward over ``num_microbatches``, applies optimizer step and
|
||||
one scheduler step when gradients are valid.
|
||||
|
||||
Args:
|
||||
args (Namespace): Runtime arguments.
|
||||
rollout_id (int): Rollout identifier.
|
||||
step_id (int): Step index within the current rollout.
|
||||
data_iterator (Sequence[DataIterator]): Iterable(s) yielding training batches.
|
||||
model (Sequence[DDP]): Sequence of DDP-wrapped model chunks.
|
||||
optimizer (MegatronOptimizer): Optimizer instance.
|
||||
opt_param_scheduler (OptimizerParamScheduler): LR/WD scheduler.
|
||||
num_microbatches (int): Number of microbatches to process.
|
||||
|
||||
Returns:
|
||||
tuple[dict[str, float], float]: Reduced loss dictionary (last stage only)
|
||||
and gradient norm for logging.
|
||||
"""
|
||||
args = get_args()
|
||||
|
||||
# Set grad to zero.
|
||||
for model_chunk in model:
|
||||
model_chunk.zero_grad_buffer()
|
||||
optimizer.zero_grad()
|
||||
|
||||
if args.custom_megatron_before_train_step_hook_path:
|
||||
from slime.utils.misc import load_function
|
||||
|
||||
custom_before_train_step_hook = load_function(args.custom_megatron_before_train_step_hook_path)
|
||||
custom_before_train_step_hook(args, rollout_id, step_id, model, optimizer, opt_param_scheduler)
|
||||
|
||||
def forward_step(data_iterator: DataIterator, model: GPTModel, return_schedule_plan: bool = False) -> tuple[
|
||||
torch.Tensor,
|
||||
Callable[[torch.Tensor], tuple[torch.Tensor, int, dict[str, torch.Tensor | list[str]]]],
|
||||
]:
|
||||
"""Forward step used by Megatron's pipeline engine during training.
|
||||
|
||||
Args:
|
||||
data_iterator (DataIterator): Input data iterator.
|
||||
model (GPTModel): The GPT model chunk to execute.
|
||||
|
||||
Returns:
|
||||
tuple[torch.Tensor, Callable[[torch.Tensor], tuple[torch.Tensor, int, dict[str, torch.Tensor | list[str]]]]]:
|
||||
Output tensor(s) and the loss function, which returns
|
||||
(loss, num_elems, {"keys": list[str], "values": torch.Tensor}).
|
||||
"""
|
||||
|
||||
# Get the batch.
|
||||
batch = get_batch(
|
||||
data_iterator,
|
||||
[
|
||||
"tokens",
|
||||
"multimodal_train_inputs",
|
||||
"packed_seq_params",
|
||||
"total_lengths",
|
||||
"response_lengths",
|
||||
"loss_masks",
|
||||
"log_probs",
|
||||
"ref_log_probs",
|
||||
"values",
|
||||
"advantages",
|
||||
"returns",
|
||||
"rollout_log_probs",
|
||||
"teacher_log_probs", # For OPD distillation loss
|
||||
],
|
||||
args.data_pad_size_multiplier,
|
||||
)
|
||||
|
||||
if os.environ.get("ENABLE_ROUTING_REPLAY", "0") == "1":
|
||||
old_stage = os.environ["ROUTING_REPLAY_STAGE"]
|
||||
os.environ["ROUTING_REPLAY_STAGE"] = "replay_forward"
|
||||
|
||||
if return_schedule_plan:
|
||||
assert not args.enable_mtp_training, "MTP training should not be enabled when using combined 1f1b"
|
||||
output_tensor = model.build_schedule_plan(
|
||||
input_ids=batch["tokens"],
|
||||
position_ids=None,
|
||||
attention_mask=None,
|
||||
labels=None,
|
||||
packed_seq_params=batch["packed_seq_params"],
|
||||
loss_mask=batch["full_loss_masks"],
|
||||
)
|
||||
else:
|
||||
output_tensor = model(
|
||||
input_ids=batch["tokens"],
|
||||
position_ids=None,
|
||||
attention_mask=None,
|
||||
labels=None,
|
||||
packed_seq_params=batch["packed_seq_params"],
|
||||
loss_mask=batch["full_loss_masks"],
|
||||
mtp_kwargs={"mtp_labels": batch["tokens"]} if args.enable_mtp_training else {},
|
||||
**(batch["multimodal_train_inputs"] if batch["multimodal_train_inputs"] is not None else {}),
|
||||
)
|
||||
|
||||
if os.environ.get("ENABLE_ROUTING_REPLAY", "0") == "1":
|
||||
os.environ["ROUTING_REPLAY_STAGE"] = old_stage
|
||||
|
||||
return output_tensor, partial(loss_function, args, batch, num_microbatches)
|
||||
|
||||
# Forward pass.
|
||||
forward_backward_func = get_forward_backward_func()
|
||||
losses_reduced = forward_backward_func(
|
||||
forward_step_func=forward_step,
|
||||
data_iterator=data_iterator,
|
||||
model=model,
|
||||
num_microbatches=num_microbatches,
|
||||
seq_length=args.seq_length,
|
||||
micro_batch_size=args.micro_batch_size,
|
||||
decoder_seq_length=args.decoder_seq_length,
|
||||
forward_only=False,
|
||||
)
|
||||
|
||||
valid_step = True
|
||||
if not getattr(args, "check_for_nan_in_loss_and_grad", True):
|
||||
found_inf_flag = optimizer.prepare_grads()
|
||||
if found_inf_flag:
|
||||
valid_step = False
|
||||
else:
|
||||
grad_norm = optimizer.get_grad_norm()
|
||||
if isinstance(grad_norm, torch.Tensor):
|
||||
valid_step = not (torch.isnan(grad_norm) or torch.isinf(grad_norm))
|
||||
else:
|
||||
valid_step = not (math.isnan(grad_norm) or math.isinf(grad_norm))
|
||||
|
||||
# CI check: verify only MTP parameters have non-zero gradients when truncation happens
|
||||
# This check must happen before optimizer.step() as gradients may be modified during step
|
||||
if args.ci_test and args.enable_mtp_training:
|
||||
from slime.backends.megatron_utils.ci_utils import check_mtp_only_grad
|
||||
|
||||
check_mtp_only_grad(model, step_id)
|
||||
|
||||
if valid_step:
|
||||
# Update parameters.
|
||||
update_successful, grad_norm, num_zeros_in_grad = optimizer.step()
|
||||
|
||||
# Update learning rate.
|
||||
assert update_successful
|
||||
opt_param_scheduler.step(increment=args.global_batch_size)
|
||||
|
||||
# release grad
|
||||
for model_chunk in model:
|
||||
model_chunk.zero_grad_buffer()
|
||||
optimizer.zero_grad()
|
||||
|
||||
if mpu.is_pipeline_last_stage(ignore_virtual=True):
|
||||
# Average loss across microbatches.
|
||||
keys = losses_reduced[0]["keys"]
|
||||
values = None
|
||||
for x in losses_reduced:
|
||||
if values is None:
|
||||
values = x["values"]
|
||||
else:
|
||||
values += x["values"]
|
||||
assert len(keys) + 1 == values.numel()
|
||||
torch.distributed.all_reduce(values, group=mpu.get_data_parallel_group(with_context_parallel=True))
|
||||
|
||||
loss_reduced = {}
|
||||
values = values.tolist()
|
||||
num_samples_or_tokens = values[0]
|
||||
for key, value in zip(keys, values[1:], strict=False):
|
||||
loss_reduced[key] = value * mpu.get_context_parallel_world_size() / num_samples_or_tokens
|
||||
return loss_reduced, grad_norm
|
||||
return {}, grad_norm
|
||||
|
||||
|
||||
def should_disable_forward_pre_hook(args: Namespace) -> bool:
|
||||
"""Block forward pre-hook for certain configurations."""
|
||||
return args.use_distributed_optimizer and args.overlap_param_gather
|
||||
|
||||
|
||||
def finalize_model_grads_with_empty_cache(*args, **kwargs):
|
||||
# trigger empty cache when there are less than 10% free memory before the final reduce scatter.
|
||||
# TODO: this is an ad-hoc method and we should figure out why the oom happens in the first place.
|
||||
device = torch.cuda.current_device()
|
||||
free, total = torch.cuda.mem_get_info(device)
|
||||
if free / total < 0.1:
|
||||
clear_memory()
|
||||
return finalize_model_grads(*args, **kwargs)
|
||||
|
||||
|
||||
def train(
|
||||
rollout_id: int,
|
||||
model: Sequence[DDP],
|
||||
optimizer: MegatronOptimizer,
|
||||
opt_param_scheduler: OptimizerParamScheduler,
|
||||
data_iterator: Sequence[DataIterator],
|
||||
num_microbatches: Sequence[int],
|
||||
) -> None:
|
||||
"""Run training over a rollout consisting of multiple steps.
|
||||
|
||||
The model is switched to train mode, training hooks are configured, and
|
||||
``train_one_step`` is invoked for each step in the rollout.
|
||||
|
||||
Args:
|
||||
rollout_id (int): Rollout identifier.
|
||||
model (Sequence[DDP]): Sequence of DDP-wrapped model chunks.
|
||||
optimizer (MegatronOptimizer): Optimizer instance.
|
||||
opt_param_scheduler (OptimizerParamScheduler): LR/WD scheduler.
|
||||
data_iterator (Sequence[DataIterator]): Iterable(s) yielding training batches.
|
||||
num_microbatches (Sequence[int]): Microbatches per step in the rollout.
|
||||
"""
|
||||
args = get_args()
|
||||
|
||||
for iterator in data_iterator:
|
||||
iterator.reset()
|
||||
|
||||
# Turn on training mode which enables dropout.
|
||||
for model_module in model:
|
||||
model_module.train()
|
||||
|
||||
# Setup some training config params.
|
||||
config = get_model_config(model[0])
|
||||
config.grad_scale_func = optimizer.scale_loss
|
||||
config.timers = None
|
||||
if isinstance(model[0], DDP) and args.overlap_grad_reduce:
|
||||
assert config.no_sync_func is None, (
|
||||
"When overlap_grad_reduce is True, config.no_sync_func must be None; "
|
||||
"a custom no_sync_func is not supported when overlapping grad-reduce"
|
||||
)
|
||||
config.no_sync_func = [model_chunk.no_sync for model_chunk in model]
|
||||
if len(model) == 1:
|
||||
config.no_sync_func = config.no_sync_func[0]
|
||||
if args.align_grad_reduce:
|
||||
config.grad_sync_func = [model_chunk.start_grad_sync for model_chunk in model]
|
||||
if len(model) == 1:
|
||||
config.grad_sync_func = config.grad_sync_func[0]
|
||||
if args.overlap_param_gather and args.align_param_gather:
|
||||
config.param_sync_func = [model_chunk.start_param_sync for model_chunk in model]
|
||||
if len(model) == 1:
|
||||
config.param_sync_func = config.param_sync_func[0]
|
||||
config.finalize_model_grads_func = finalize_model_grads_with_empty_cache
|
||||
|
||||
pre_hook_enabled = False
|
||||
|
||||
if args.manual_gc:
|
||||
# Disable the default garbage collector and perform the collection manually.
|
||||
# This is to align the timing of garbage collection across ranks.
|
||||
assert args.manual_gc_interval >= 0, "Manual garbage collection interval should be larger than or equal to 0"
|
||||
gc.disable()
|
||||
gc.collect()
|
||||
|
||||
# Disable forward pre-hook to start training to ensure that errors in checkpoint loading
|
||||
# or random initialization don't propagate to all ranks in first all-gather (which is a
|
||||
# no-op if things work correctly).
|
||||
if should_disable_forward_pre_hook(args):
|
||||
disable_forward_pre_hook(model, param_sync=False)
|
||||
# Also remove param_sync_func temporarily so that sync calls made in
|
||||
# `forward_backward_func` are no-ops.
|
||||
param_sync_func = config.param_sync_func
|
||||
config.param_sync_func = None
|
||||
pre_hook_enabled = False
|
||||
|
||||
num_steps_per_rollout = len(num_microbatches)
|
||||
|
||||
# Run training iterations till done.
|
||||
for step_id in range(num_steps_per_rollout):
|
||||
|
||||
# Run training step.
|
||||
loss_dict, grad_norm = train_one_step(
|
||||
args,
|
||||
rollout_id,
|
||||
step_id,
|
||||
data_iterator,
|
||||
model,
|
||||
optimizer,
|
||||
opt_param_scheduler,
|
||||
num_microbatches[step_id],
|
||||
)
|
||||
|
||||
if step_id == 0:
|
||||
# Enable forward pre-hook after training step has successfully run. All subsequent
|
||||
# forward passes will use the forward pre-hook / `param_sync_func` in
|
||||
# `forward_backward_func`.
|
||||
if should_disable_forward_pre_hook(args):
|
||||
enable_forward_pre_hook(model)
|
||||
config.param_sync_func = param_sync_func
|
||||
pre_hook_enabled = True
|
||||
|
||||
if args.enable_mtp_training:
|
||||
from megatron.core.transformer.multi_token_prediction import MTPLossLoggingHelper
|
||||
|
||||
mtp_loss_scale = 1 / num_microbatches[step_id]
|
||||
tracker = MTPLossLoggingHelper.tracker
|
||||
if "values" in tracker:
|
||||
values = tracker["values"]
|
||||
if tracker.get("reduce_group") is not None:
|
||||
torch.distributed.all_reduce(values, group=tracker.get("reduce_group"))
|
||||
if tracker.get("avg_group") is not None:
|
||||
torch.distributed.all_reduce(values, group=tracker["avg_group"], op=torch.distributed.ReduceOp.AVG)
|
||||
# here we assume only one mtp layer
|
||||
mtp_losses = (tracker["values"] * mtp_loss_scale).item()
|
||||
MTPLossLoggingHelper.clean_loss_in_tracker()
|
||||
|
||||
# CI check: verify MTP loss is within expected bounds
|
||||
if args.ci_test:
|
||||
from slime.backends.megatron_utils.ci_utils import check_mtp_loss
|
||||
|
||||
check_mtp_loss(mtp_losses)
|
||||
|
||||
# per train step log.
|
||||
if (
|
||||
mpu.get_data_parallel_rank(with_context_parallel=True) == 0
|
||||
and mpu.get_tensor_model_parallel_rank() == 0
|
||||
and mpu.get_pipeline_model_parallel_rank() == mpu.get_pipeline_model_parallel_world_size() - 1
|
||||
):
|
||||
accumulated_step_id = rollout_id * num_steps_per_rollout + step_id
|
||||
role = getattr(model[0], "role", "actor")
|
||||
role_tag = "" if role == "actor" else f"{role}-"
|
||||
log_dict = {
|
||||
f"train/{role_tag}{key}": val.mean().item() if isinstance(val, torch.Tensor) else val
|
||||
for key, val in loss_dict.items()
|
||||
}
|
||||
log_dict[f"train/{role_tag}grad_norm"] = grad_norm
|
||||
if args.enable_mtp_training:
|
||||
log_dict[f"train/{role_tag}mtp_loss"] = mtp_losses
|
||||
|
||||
for param_group_id, param_group in enumerate(optimizer.param_groups):
|
||||
log_dict[f"train/{role_tag}lr-pg_{param_group_id}"] = opt_param_scheduler.get_lr(param_group)
|
||||
|
||||
log_dict["train/step"] = accumulated_step_id
|
||||
tracking_utils.log(args, log_dict, step_key="train/step")
|
||||
|
||||
if args.ci_test and not args.ci_disable_kl_checker:
|
||||
if step_id == 0 and "train/ppo_kl" in log_dict and "train/pg_clipfrac" in log_dict:
|
||||
if args.multi_latent_attention:
|
||||
# TODO: mla currently have non-zero kl, need further investigation
|
||||
assert log_dict["train/ppo_kl"] < 1e-8, f"{log_dict=}"
|
||||
else:
|
||||
assert log_dict["train/ppo_kl"] == 0.0 and log_dict["train/pg_clipfrac"] == 0.0, f"{log_dict=}"
|
||||
if accumulated_step_id == 0 and "train/kl_loss" in log_dict:
|
||||
assert log_dict["train/kl_loss"] == 0.0, f"{log_dict=}"
|
||||
|
||||
logger.info(f"{role_tag}step {accumulated_step_id}: {log_dict}")
|
||||
|
||||
if args.ci_save_grad_norm is not None:
|
||||
ci_save_grad_norm_path = args.ci_save_grad_norm.format(
|
||||
role=role,
|
||||
rollout_id=rollout_id,
|
||||
step_id=step_id,
|
||||
)
|
||||
torch.save(grad_norm, ci_save_grad_norm_path)
|
||||
elif args.ci_load_grad_norm is not None:
|
||||
ci_load_grad_norm_path = args.ci_load_grad_norm.format(
|
||||
role=role,
|
||||
rollout_id=rollout_id,
|
||||
step_id=step_id,
|
||||
)
|
||||
expected_grad_norm = torch.load(ci_load_grad_norm_path)
|
||||
assert math.isclose(
|
||||
grad_norm,
|
||||
expected_grad_norm,
|
||||
rel_tol=0.01,
|
||||
abs_tol=0.01,
|
||||
), f"grad norm mismatch: {grad_norm} != {expected_grad_norm}"
|
||||
# Close out pre-hooks if using distributed optimizer and overlapped param gather.
|
||||
if pre_hook_enabled:
|
||||
disable_forward_pre_hook(model)
|
||||
|
||||
|
||||
def save(
|
||||
iteration: int, model: Sequence[DDP], optimizer: MegatronOptimizer, opt_param_scheduler: OptimizerParamScheduler
|
||||
) -> None:
|
||||
"""Persist a training checkpoint safely with forward hooks disabled.
|
||||
|
||||
Args:
|
||||
iteration (int): Current global iteration number.
|
||||
model (Sequence[DDP]): Sequence of DDP-wrapped model chunks.
|
||||
optimizer (MegatronOptimizer): Optimizer instance.
|
||||
opt_param_scheduler (OptimizerParamScheduler): LR/WD scheduler.
|
||||
"""
|
||||
args = get_args()
|
||||
if should_disable_forward_pre_hook(args):
|
||||
disable_forward_pre_hook(model)
|
||||
save_checkpoint(
|
||||
iteration,
|
||||
model,
|
||||
optimizer,
|
||||
opt_param_scheduler,
|
||||
num_floating_point_operations_so_far=0,
|
||||
checkpointing_context=None,
|
||||
train_data_iterator=None,
|
||||
preprocess_common_state_dict_fn=None,
|
||||
)
|
||||
if should_disable_forward_pre_hook(args):
|
||||
enable_forward_pre_hook(model)
|
||||
|
||||
|
||||
def initialize_model_and_optimizer(
|
||||
args: Namespace, role: str = "actor"
|
||||
) -> tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler, int]:
|
||||
"""Initialize model(s), optimizer, scheduler, and load from checkpoint.
|
||||
|
||||
Args:
|
||||
args (Namespace): Runtime arguments.
|
||||
role (str): Logical role of the model (e.g., "actor", "critic").
|
||||
|
||||
Returns:
|
||||
tuple[list[DDP], MegatronOptimizer, OptimizerParamScheduler, int]:
|
||||
DDP-wrapped model chunks, optimizer, scheduler, and iteration index.
|
||||
"""
|
||||
|
||||
if torch.version.hip:
|
||||
import megatron.core.dist_checkpointing.strategies.filesystem_async as filesystem_async_module
|
||||
from slime.utils.rocm_checkpoint_writer import ROCmFileSystemWriterAsync
|
||||
|
||||
filesystem_async_module.FileSystemWriterAsync = ROCmFileSystemWriterAsync
|
||||
print("[ROCm] Applied FileSystemWriterAsync patch for HIP compatibility")
|
||||
|
||||
model, optimizer, opt_param_scheduler = setup_model_and_optimizer(args, role)
|
||||
model[0].role = role
|
||||
clear_memory()
|
||||
iteration, _ = load_checkpoint(
|
||||
model,
|
||||
optimizer,
|
||||
opt_param_scheduler,
|
||||
checkpointing_context={},
|
||||
skip_load_to_model_and_opt=False,
|
||||
)
|
||||
clear_memory()
|
||||
|
||||
opt_param_scheduler.step(increment=iteration * args.global_batch_size)
|
||||
|
||||
return model, optimizer, opt_param_scheduler, iteration
|
||||
180
slime/backends/megatron_utils/model_provider.py
Normal file
180
slime/backends/megatron_utils/model_provider.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Adapt from https://github.com/NVIDIA/Megatron-LM/blob/b1efb3c7126ef7615e8c333432d76e08038e17ff/pretrain_gpt.py
|
||||
import argparse
|
||||
import inspect
|
||||
from contextlib import nullcontext
|
||||
from typing import Literal
|
||||
|
||||
import torch
|
||||
from megatron.core import tensor_parallel
|
||||
from megatron.core.models.gpt import GPTModel
|
||||
from megatron.core.models.gpt.gpt_layer_specs import (
|
||||
get_gpt_decoder_block_spec,
|
||||
get_gpt_layer_local_spec,
|
||||
get_gpt_layer_with_transformer_engine_spec,
|
||||
)
|
||||
from megatron.core.transformer.spec_utils import import_module
|
||||
from megatron.core.transformer.transformer_config import TransformerConfig
|
||||
from megatron.training.arguments import core_transformer_config_from_args
|
||||
|
||||
|
||||
# Adapt from https://github.com/volcengine/verl/blob/c3b20575d2bc815fcccd84bddb4c0401fc4b632b/verl/models/llama/megatron/layers/parallel_linear.py#L82
|
||||
class LinearForLastLayer(torch.nn.Linear):
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int,
|
||||
*,
|
||||
config: TransformerConfig,
|
||||
bias: bool = True,
|
||||
) -> None:
|
||||
super().__init__(in_features=input_size, out_features=output_size, bias=bias)
|
||||
self.sequence_parallel = config.sequence_parallel
|
||||
if self.sequence_parallel:
|
||||
self.weight.sequence_parallel = True
|
||||
|
||||
self.weight.data.normal_(mean=0.0, std=0.02)
|
||||
if bias:
|
||||
self.bias.data.zero_()
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_: torch.Tensor,
|
||||
weight: torch.Tensor | None = None,
|
||||
runtime_gather_output: bool | None = None,
|
||||
) -> tuple[torch.Tensor, None]:
|
||||
logits = super().forward(input_)
|
||||
logits = logits.float()
|
||||
if self.sequence_parallel:
|
||||
logits = tensor_parallel.gather_from_sequence_parallel_region(logits, tensor_parallel_output_grad=False)
|
||||
return logits, None
|
||||
|
||||
|
||||
def get_model_provider_func(
|
||||
args: argparse.Namespace,
|
||||
role: Literal["actor", "critic"] = "actor",
|
||||
):
|
||||
if args.megatron_to_hf_mode == "bridge":
|
||||
from megatron.bridge import AutoBridge
|
||||
|
||||
bridge = AutoBridge.from_hf_pretrained(args.hf_checkpoint, trust_remote_code=True)
|
||||
provider = bridge.to_megatron_provider(load_weights=False)
|
||||
# TODO: we should not manually set this...
|
||||
provider.tensor_model_parallel_size = args.tensor_model_parallel_size
|
||||
provider.pipeline_model_parallel_size = args.pipeline_model_parallel_size
|
||||
provider.expert_model_parallel_size = args.expert_model_parallel_size
|
||||
provider.expert_tensor_parallel_size = args.expert_tensor_parallel_size
|
||||
provider.sequence_parallel = args.sequence_parallel
|
||||
provider.finalize()
|
||||
return provider.provide
|
||||
|
||||
def model_provider(pre_process: bool = True, post_process: bool = True, vp_stage: int | None = None) -> GPTModel:
|
||||
"""Builds the model.
|
||||
|
||||
If you set the use_legacy_models to True, it will return the legacy GPT model and if not the mcore GPT model.
|
||||
|
||||
Args:
|
||||
pre_process (bool, optional): Set to true if you need to compute embedings. Defaults to True.
|
||||
post_process (bool, optional): Set to true if you need to want to compute output logits/loss. Defaults to True.
|
||||
|
||||
|
||||
Returns:
|
||||
Union[GPTModel, megatron.legacy.model.GPTModel]: The returned model
|
||||
"""
|
||||
use_te = args.transformer_impl == "transformer_engine"
|
||||
|
||||
# Experimental loading arguments from yaml
|
||||
config: TransformerConfig = core_transformer_config_from_args(args)
|
||||
|
||||
if args.spec is not None:
|
||||
transformer_layer_spec = import_module(args.spec)
|
||||
# Allow the spec to be a function so that user can use customized Megatron easier.
|
||||
if callable(transformer_layer_spec):
|
||||
transformer_layer_spec = transformer_layer_spec(args, config, vp_stage)
|
||||
else:
|
||||
if args.num_experts:
|
||||
# Define the decoder block spec
|
||||
kwargs = {
|
||||
"use_transformer_engine": use_te,
|
||||
}
|
||||
if vp_stage is not None:
|
||||
kwargs["vp_stage"] = vp_stage
|
||||
transformer_layer_spec = get_gpt_decoder_block_spec(config, **kwargs)
|
||||
else:
|
||||
# Define the decoder layer spec
|
||||
if use_te:
|
||||
transformer_layer_spec = get_gpt_layer_with_transformer_engine_spec(
|
||||
num_experts=args.num_experts,
|
||||
moe_grouped_gemm=args.moe_grouped_gemm,
|
||||
qk_layernorm=args.qk_layernorm,
|
||||
multi_latent_attention=args.multi_latent_attention,
|
||||
moe_use_legacy_grouped_gemm=args.moe_use_legacy_grouped_gemm,
|
||||
)
|
||||
else:
|
||||
transformer_layer_spec = get_gpt_layer_local_spec(
|
||||
num_experts=args.num_experts,
|
||||
moe_grouped_gemm=args.moe_grouped_gemm,
|
||||
qk_layernorm=args.qk_layernorm,
|
||||
multi_latent_attention=args.multi_latent_attention,
|
||||
moe_use_legacy_grouped_gemm=args.moe_use_legacy_grouped_gemm,
|
||||
)
|
||||
|
||||
build_model_context = nullcontext
|
||||
build_model_context_args = {}
|
||||
if args.fp8_param_gather:
|
||||
try:
|
||||
from transformer_engine.pytorch import fp8_model_init
|
||||
|
||||
build_model_context = fp8_model_init
|
||||
build_model_context_args["enabled"] = True
|
||||
|
||||
# Check if fp8_model_init supports preserve_high_precision_init_val
|
||||
if "preserve_high_precision_init_val" in inspect.signature(fp8_model_init).parameters:
|
||||
build_model_context_args["preserve_high_precision_init_val"] = True
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"--fp8-param-gather requires `fp8_model_init` from TransformerEngine, but not found."
|
||||
) from e
|
||||
|
||||
kwargs = {
|
||||
"config": config,
|
||||
"transformer_layer_spec": transformer_layer_spec,
|
||||
"vocab_size": args.padded_vocab_size,
|
||||
"max_sequence_length": args.max_position_embeddings,
|
||||
"pre_process": pre_process,
|
||||
"post_process": post_process,
|
||||
"fp16_lm_cross_entropy": args.fp16_lm_cross_entropy,
|
||||
"parallel_output": True,
|
||||
"share_embeddings_and_output_weights": not args.untie_embeddings_and_output_weights,
|
||||
"position_embedding_type": args.position_embedding_type,
|
||||
"rotary_percent": args.rotary_percent,
|
||||
"rotary_base": args.rotary_base,
|
||||
"rope_scaling": args.use_rope_scaling,
|
||||
}
|
||||
|
||||
if vp_stage is not None:
|
||||
kwargs["vp_stage"] = vp_stage
|
||||
|
||||
if args.mtp_num_layers:
|
||||
from megatron.core.models.gpt.gpt_layer_specs import get_gpt_mtp_block_spec
|
||||
|
||||
mtp_kwargs = {
|
||||
"use_transformer_engine": use_te,
|
||||
}
|
||||
if vp_stage is not None:
|
||||
mtp_kwargs["vp_stage"] = vp_stage
|
||||
|
||||
mtp_block_spec = get_gpt_mtp_block_spec(config, transformer_layer_spec, **mtp_kwargs)
|
||||
kwargs["mtp_block_spec"] = mtp_block_spec
|
||||
|
||||
with build_model_context(**build_model_context_args):
|
||||
model = GPTModel(**kwargs)
|
||||
|
||||
if post_process and role == "critic":
|
||||
model.output_layer = LinearForLastLayer(input_size=config.hidden_size, output_size=1, config=config)
|
||||
|
||||
return model
|
||||
|
||||
return model_provider
|
||||
34
slime/backends/megatron_utils/sglang.py
Normal file
34
slime/backends/megatron_utils/sglang.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# the file to manage all sglang deps in the megatron actor
|
||||
try:
|
||||
from sglang.srt.layers.quantization.fp8_utils import quant_weight_ue8m0, transform_scale_ue8m0
|
||||
from sglang.srt.model_loader.utils import should_deepgemm_weight_requant_ue8m0
|
||||
except ImportError:
|
||||
quant_weight_ue8m0 = None
|
||||
transform_scale_ue8m0 = None
|
||||
should_deepgemm_weight_requant_ue8m0 = None
|
||||
|
||||
try:
|
||||
from sglang.srt.utils.patch_torch import monkey_patch_torch_reductions
|
||||
except ImportError:
|
||||
from sglang.srt.patch_torch import monkey_patch_torch_reductions
|
||||
|
||||
|
||||
from sglang.srt.utils import MultiprocessingSerializer
|
||||
|
||||
|
||||
try:
|
||||
from sglang.srt.weight_sync.tensor_bucket import FlattenedTensorBucket # type: ignore[import]
|
||||
except ImportError:
|
||||
from sglang.srt.model_executor.model_runner import FlattenedTensorBucket # type: ignore[import]
|
||||
|
||||
__all__ = [
|
||||
"quant_weight_ue8m0",
|
||||
"transform_scale_ue8m0",
|
||||
"should_deepgemm_weight_requant_ue8m0",
|
||||
"monkey_patch_torch_reductions",
|
||||
"MultiprocessingSerializer",
|
||||
"FlattenedTensorBucket",
|
||||
]
|
||||
3
slime/backends/megatron_utils/update_weight/__init__.py
Normal file
3
slime/backends/megatron_utils/update_weight/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
238
slime/backends/megatron_utils/update_weight/common.py
Normal file
238
slime/backends/megatron_utils/update_weight/common.py
Normal file
@@ -0,0 +1,238 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from argparse import Namespace
|
||||
from collections.abc import Iterator, Sequence
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from megatron.core import mpu
|
||||
from megatron.core.transformer.transformer_layer import get_transformer_layer_offset
|
||||
|
||||
from slime.backends.megatron_utils.misc_utils import strip_param_name_prefix
|
||||
from slime.utils.types import ParamInfo
|
||||
|
||||
|
||||
def all_gather_param(name: str, param: torch.nn.Parameter) -> torch.Tensor:
|
||||
"""
|
||||
All-gather TP-sharded param to full tensor. expert_bias→param, non-TP/duplicated→param.data.
|
||||
Uses expert-TP for ".experts.", else regular-TP. linear_fc1 rechunked (GLU), linear_fc2 dim fix.
|
||||
"""
|
||||
if "expert_bias" in name:
|
||||
return param
|
||||
|
||||
assert hasattr(param, "tensor_model_parallel"), f"{name} does not have tensor_model_parallel attribute"
|
||||
if not param.tensor_model_parallel or getattr(param, "parallel_mode", None) == "duplicated":
|
||||
return param.data
|
||||
|
||||
if ".experts." in name:
|
||||
tp_size = mpu.get_expert_tensor_parallel_world_size()
|
||||
tp_group = mpu.get_expert_tensor_parallel_group()
|
||||
else:
|
||||
tp_size = mpu.get_tensor_model_parallel_world_size()
|
||||
tp_group = mpu.get_tensor_model_parallel_group()
|
||||
|
||||
param_partitions = [torch.empty_like(param.data) for _ in range(tp_size)]
|
||||
dist.all_gather(param_partitions, param.data, group=tp_group)
|
||||
partition_dim = param.partition_dim
|
||||
assert param.partition_stride == 1, "partition_stride != 1 is not supported"
|
||||
# TODO: here we did an extra copy during concat, maybe merge this with convert_to_hf is better?
|
||||
# TODO: check only GLU is used.
|
||||
if "linear_fc1.weight" in name:
|
||||
param_partitions = [p.chunk(2, dim=0) for p in param_partitions]
|
||||
param_partitions = [p[0] for p in param_partitions] + [p[1] for p in param_partitions]
|
||||
# this is bug in megatron's grouped moe.
|
||||
if "linear_fc2.weight" in name:
|
||||
if partition_dim == 0:
|
||||
partition_dim = 1
|
||||
param = torch.cat(param_partitions, dim=partition_dim)
|
||||
return param
|
||||
|
||||
|
||||
def all_gather_params_async(
|
||||
param_infos_and_params: list[tuple[ParamInfo, torch.Tensor]],
|
||||
) -> list[torch.Tensor]:
|
||||
"""
|
||||
Parallel TP all-gather for multiple params. Loop 1: for each TP param, allocate buffers +
|
||||
dist.all_gather(async_op=True) on expert-TP/regular-TP group (skip expert_bias/non-TP/duplicated).
|
||||
Loop 2: wait all NCCL handles (enables overlap). Loop 3: concat partitions + apply GLU rechunk/MoE dim fix.
|
||||
"""
|
||||
# Phase 1: Start all async all_gather operations
|
||||
gather_tasks = []
|
||||
handles = []
|
||||
|
||||
for info, param in param_infos_and_params:
|
||||
# Prepare async all_gather
|
||||
if "expert_bias" in info.name:
|
||||
gather_tasks.append((info, param, None, None, None))
|
||||
handles.append(None)
|
||||
elif not param.tensor_model_parallel or getattr(param, "parallel_mode", None) == "duplicated":
|
||||
gather_tasks.append((info, param.data, None, None, None))
|
||||
handles.append(None)
|
||||
else:
|
||||
# Start async all_gather
|
||||
if ".experts." in info.name:
|
||||
tp_size = mpu.get_expert_tensor_parallel_world_size()
|
||||
tp_group = mpu.get_expert_tensor_parallel_group()
|
||||
else:
|
||||
tp_size = mpu.get_tensor_model_parallel_world_size()
|
||||
tp_group = mpu.get_tensor_model_parallel_group()
|
||||
|
||||
param_partitions = [torch.empty_like(param.data) for _ in range(tp_size)]
|
||||
handle = dist.all_gather(param_partitions, param.data, group=tp_group, async_op=True)
|
||||
gather_tasks.append((info, None, handle, param_partitions, param.partition_dim))
|
||||
handles.append(handle)
|
||||
|
||||
# Phase 2: Wait for ALL async operations to complete at once
|
||||
# This ensures maximum parallelism by not blocking on individual operations
|
||||
for handle in handles:
|
||||
if handle is not None:
|
||||
handle.wait()
|
||||
|
||||
# Phase 3: Process all results after all communications are done
|
||||
gathered_params = []
|
||||
for info, direct_param, handle, param_partitions, partition_dim in gather_tasks:
|
||||
if handle is None:
|
||||
# No all_gather needed
|
||||
param = direct_param
|
||||
else:
|
||||
# Process the gathered partitions (same logic as original all_gather_param)
|
||||
assert partition_dim is not None, "partition_stride != 1 is not supported"
|
||||
# TODO: here we did an extra copy during concat, maybe merge this with convert_to_hf is better?
|
||||
# TODO: check only GLU is used.
|
||||
if "linear_fc1.weight" in info.name:
|
||||
param_partitions = [p.chunk(2, dim=0) for p in param_partitions]
|
||||
param_partitions = [p[0] for p in param_partitions] + [p[1] for p in param_partitions]
|
||||
# this is bug in megatron's grouped moe.
|
||||
if "linear_fc2.weight" in info.name:
|
||||
if partition_dim == 0:
|
||||
partition_dim = 1
|
||||
param = torch.cat(param_partitions, dim=partition_dim)
|
||||
|
||||
gathered_params.append(param)
|
||||
|
||||
return gathered_params
|
||||
|
||||
|
||||
def named_params_and_buffers(
|
||||
args: Namespace,
|
||||
model: Sequence[torch.nn.Module],
|
||||
convert_to_global_name: bool = True,
|
||||
translate_gpu_to_cpu: bool = False,
|
||||
) -> Iterator[tuple[str, torch.Tensor]]:
|
||||
if convert_to_global_name:
|
||||
ans = _named_params_and_buffers_global(args, model)
|
||||
else:
|
||||
ans = _named_params_and_buffers_vanilla(model)
|
||||
|
||||
if translate_gpu_to_cpu:
|
||||
ans = ((name, _maybe_get_cpu_backup(tensor)) for name, tensor in ans)
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
def _maybe_get_cpu_backup(x: torch.Tensor):
|
||||
from torch_memory_saver import torch_memory_saver
|
||||
|
||||
if (cpu_tensor := torch_memory_saver.get_cpu_backup(x)) is not None:
|
||||
return cpu_tensor
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def _named_params_and_buffers_vanilla(model: Sequence[torch.nn.Module]) -> Iterator[tuple[str, torch.Tensor]]:
|
||||
for vp_stage, model_module in enumerate(model):
|
||||
|
||||
def _compute_fqn(name, vp_stage=vp_stage):
|
||||
return f"vp_stages.{vp_stage}.{strip_param_name_prefix(name)}"
|
||||
|
||||
for name, param in model_module.named_parameters():
|
||||
yield _compute_fqn(name), param
|
||||
|
||||
for name, buffer in model_module.named_buffers():
|
||||
# TODO shall we handle (almost) all buffers like Megatron Bridge
|
||||
if "expert_bias" not in name:
|
||||
continue
|
||||
yield _compute_fqn(name), buffer
|
||||
|
||||
|
||||
def _named_params_and_buffers_global(
|
||||
args: Namespace, model: Sequence[torch.nn.Module]
|
||||
) -> Iterator[tuple[str, torch.Tensor]]:
|
||||
"""
|
||||
Yield (global_name, param/buffer) with consistent names across PP/EP. Adjusts indices for
|
||||
virtual PP + EP offsets. Handles decoder.layers, mtp.layers (Multi-Token Prediction), expert_bias.
|
||||
"""
|
||||
ep_size = mpu.get_expert_model_parallel_world_size()
|
||||
ep_rank = mpu.get_expert_model_parallel_rank()
|
||||
if args.num_experts:
|
||||
expert_offset = ep_rank * args.num_experts // ep_size
|
||||
|
||||
sig = inspect.signature(get_transformer_layer_offset)
|
||||
need_vp_stage = "vp_stage" in sig.parameters
|
||||
|
||||
for vp_stage, model_module in enumerate(model):
|
||||
if need_vp_stage:
|
||||
layer_offset = get_transformer_layer_offset(model_module.config, vp_stage)
|
||||
else:
|
||||
layer_offset = get_transformer_layer_offset(model_module.config)
|
||||
for name, param in model_module.named_parameters():
|
||||
# for model without ddp wrap
|
||||
if not name.startswith("module.module."):
|
||||
name = "module." + name
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if not match:
|
||||
# MTP (Multi-Token Prediction) layers for speculative decoding
|
||||
mtp_layers_pattern = r"module\.module\.mtp\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(mtp_layers_pattern, name)
|
||||
if not match:
|
||||
yield name, param
|
||||
continue
|
||||
|
||||
# MTP layer indices start from 0
|
||||
layer_idx, rest = match.groups()
|
||||
expert_pattern = r"transformer_layer.mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if not match:
|
||||
yield name, param
|
||||
continue
|
||||
|
||||
rest, expert_idx = match.groups()
|
||||
expert_idx = int(expert_idx) + expert_offset
|
||||
yield f"module.module.mtp.layers.{layer_idx}.transformer_layer.mlp.experts.{rest}.weight{expert_idx}", param
|
||||
continue
|
||||
|
||||
layer_idx, rest = match.groups()
|
||||
layer_idx = int(layer_idx) + layer_offset
|
||||
|
||||
# this is hardcoded for te grouped matmul
|
||||
expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
|
||||
match = re.match(expert_pattern, rest)
|
||||
if match:
|
||||
rest, expert_idx = match.groups()
|
||||
expert_idx = int(expert_idx) + expert_offset
|
||||
yield f"module.module.decoder.layers.{layer_idx}.mlp.experts.{rest}.weight{expert_idx}", param
|
||||
else:
|
||||
yield f"module.module.decoder.layers.{layer_idx}.{rest}", param
|
||||
|
||||
# treat expert bias as normal parameters
|
||||
for name, buffer in model_module.named_buffers():
|
||||
# TODO shall we handle (almost) all buffers like Megatron Bridge
|
||||
if "expert_bias" not in name:
|
||||
continue
|
||||
# for model without ddp wrap
|
||||
if not name.startswith("module.module."):
|
||||
name = "module." + name
|
||||
|
||||
decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
|
||||
match = re.match(decoder_layers_pattern, name)
|
||||
if not match:
|
||||
yield name, buffer
|
||||
else:
|
||||
layer_idx, rest = match.groups()
|
||||
layer_idx = int(layer_idx) + layer_offset
|
||||
yield f"module.module.decoder.layers.{layer_idx}.{rest}", buffer
|
||||
@@ -0,0 +1,32 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class HfWeightIteratorBase(ABC):
|
||||
@staticmethod
|
||||
def create(args, model, **kwargs):
|
||||
from .hf_weight_iterator_bridge import HfWeightIteratorBridge
|
||||
from .hf_weight_iterator_direct import HfWeightIteratorDirect
|
||||
|
||||
c = {
|
||||
"raw": HfWeightIteratorDirect,
|
||||
"bridge": HfWeightIteratorBridge,
|
||||
}[args.megatron_to_hf_mode]
|
||||
|
||||
return c(args, model, **kwargs)
|
||||
|
||||
def __init__(self, args, model, model_name, quantization_config):
|
||||
self.args = args
|
||||
self.model = model
|
||||
self.model_name = model_name
|
||||
self.quantization_config = quantization_config
|
||||
|
||||
@abstractmethod
|
||||
def get_hf_weight_chunks(self, megatron_local_weights):
|
||||
"""
|
||||
Mental model of the API:
|
||||
megatron_model.to_hf_magically().named_parameters()
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,75 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import dataclasses
|
||||
|
||||
from slime.utils import megatron_bridge_utils
|
||||
from slime.utils.iter_utils import chunk_named_params_by_size
|
||||
|
||||
from ..megatron_to_hf import postprocess_hf_param
|
||||
from ..misc_utils import strip_param_name_prefix
|
||||
from .hf_weight_iterator_base import HfWeightIteratorBase
|
||||
|
||||
|
||||
class HfWeightIteratorBridge(HfWeightIteratorBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
from megatron.bridge import AutoBridge
|
||||
import slime_plugins.megatron_bridge # noqa: F401
|
||||
|
||||
self._bridge = AutoBridge.from_hf_pretrained(self.args.hf_checkpoint)
|
||||
|
||||
def get_hf_weight_chunks(self, megatron_local_weights):
|
||||
# TODO support quantization (e.g. modify megatron-bridge to provide megatron param name)
|
||||
renamed_megatron_local_weights = {strip_param_name_prefix(k): v for k, v in megatron_local_weights.items()}
|
||||
with megatron_bridge_utils.patch_megatron_model(self.model):
|
||||
conversion_tasks = self._bridge.get_conversion_tasks(self.model)
|
||||
conversion_tasks = _process_conversion_tasks(conversion_tasks, renamed_megatron_local_weights)
|
||||
|
||||
named_weights = self._bridge.export_hf_weights(self.model, cpu=False, conversion_tasks=conversion_tasks)
|
||||
|
||||
named_weights = (
|
||||
(
|
||||
hf_param_name,
|
||||
postprocess_hf_param(
|
||||
args=self.args,
|
||||
megatron_param_name=megatron_param_name,
|
||||
hf_param_name=hf_param_name,
|
||||
param=weight,
|
||||
),
|
||||
)
|
||||
for hf_param_name, weight, megatron_param_name in named_weights
|
||||
)
|
||||
|
||||
yield from chunk_named_params_by_size(named_weights, chunk_size=self.args.update_weight_buffer_size)
|
||||
|
||||
|
||||
def _process_conversion_tasks(vanilla_conversion_tasks, new_weight_dict):
|
||||
def _handle_one(task):
|
||||
if task.param_weight is None:
|
||||
return task
|
||||
|
||||
weight_dict_key = f"vp_stages.{task.vp_stage}.{task.param_name}"
|
||||
assert (
|
||||
weight_dict_key in new_weight_dict
|
||||
), f"{weight_dict_key=} not in new_weight_dict ({task.vp_stage=}, {task.param_name=}, {list(new_weight_dict)=})"
|
||||
|
||||
new_param_weight = new_weight_dict[weight_dict_key]
|
||||
new_param_weight = new_param_weight.cuda()
|
||||
return dataclasses.replace(task, param_weight=new_param_weight)
|
||||
|
||||
return _MapWithLen(_handle_one, vanilla_conversion_tasks)
|
||||
|
||||
|
||||
class _MapWithLen:
|
||||
def __init__(self, fn, xs):
|
||||
self.fn = fn
|
||||
self.xs = xs
|
||||
|
||||
def __len__(self):
|
||||
return len(self.xs)
|
||||
|
||||
def __iter__(self):
|
||||
for x in self.xs:
|
||||
yield self.fn(x)
|
||||
@@ -0,0 +1,215 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import dataclasses
|
||||
from argparse import Namespace
|
||||
from collections.abc import Sequence
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from megatron.core import mpu
|
||||
from tqdm import tqdm
|
||||
|
||||
from slime.utils.distributed_utils import get_gloo_group
|
||||
from slime.utils.types import ParamInfo
|
||||
|
||||
from ..megatron_to_hf import convert_to_hf
|
||||
from ..sglang import monkey_patch_torch_reductions
|
||||
from .common import all_gather_params_async, named_params_and_buffers
|
||||
from .hf_weight_iterator_base import HfWeightIteratorBase
|
||||
|
||||
|
||||
class HfWeightIteratorDirect(HfWeightIteratorBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.megatron_local_param_info_buckets = _get_megatron_local_param_info_buckets(self.args, self.model)
|
||||
|
||||
def get_hf_weight_chunks(self, megatron_local_weights):
|
||||
rank = dist.get_rank()
|
||||
|
||||
for megatron_local_param_infos in tqdm(
|
||||
self.megatron_local_param_info_buckets, disable=rank != 0, desc="Update weights"
|
||||
):
|
||||
megatron_full_params = _get_megatron_full_params(megatron_local_param_infos, megatron_local_weights)
|
||||
hf_named_tensors = self._convert_to_hf_named_tensors(megatron_full_params, megatron_local_param_infos)
|
||||
yield hf_named_tensors
|
||||
del megatron_full_params
|
||||
|
||||
def _convert_to_hf_named_tensors(self, megatron_full_params: Sequence[torch.Tensor], param_infos: list[ParamInfo]):
|
||||
hf_named_tensors = []
|
||||
for info, param in zip(param_infos, megatron_full_params, strict=False):
|
||||
hf_named_tensors.extend(
|
||||
convert_to_hf(self.args, self.model_name, info.name, param, self.quantization_config)
|
||||
)
|
||||
return hf_named_tensors
|
||||
|
||||
|
||||
def _get_megatron_full_params(
|
||||
megatron_local_param_infos: Sequence[ParamInfo],
|
||||
megatron_local_weights,
|
||||
) -> Sequence[torch.Tensor]:
|
||||
monkey_patch_torch_reductions()
|
||||
pp_size = mpu.get_pipeline_model_parallel_world_size()
|
||||
ep_size = mpu.get_expert_model_parallel_world_size()
|
||||
rank = dist.get_rank()
|
||||
# init params:
|
||||
params = []
|
||||
for info in megatron_local_param_infos:
|
||||
if dist.get_rank() == info.src_rank:
|
||||
params.append(
|
||||
torch.nn.Parameter(
|
||||
megatron_local_weights[info.name].to(device=torch.cuda.current_device(), non_blocking=True),
|
||||
requires_grad=False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
params.append(torch.empty(info.shape, dtype=info.dtype, device=torch.cuda.current_device()))
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# broadcast params across pp ranks
|
||||
if pp_size > 1:
|
||||
handles = []
|
||||
for info, param in zip(megatron_local_param_infos, params, strict=False):
|
||||
if info.src_rank in dist.get_process_group_ranks(mpu.get_pipeline_model_parallel_group()):
|
||||
handles.append(
|
||||
torch.distributed.broadcast(
|
||||
param, src=info.src_rank, group=mpu.get_pipeline_model_parallel_group(), async_op=True
|
||||
)
|
||||
)
|
||||
for handle in handles:
|
||||
handle.wait()
|
||||
|
||||
# broadcast params across ep ranks
|
||||
if ep_size > 1:
|
||||
handles = []
|
||||
for info, param in zip(megatron_local_param_infos, params, strict=False):
|
||||
if ".experts." in info.name:
|
||||
src_rank = (
|
||||
info.src_rank
|
||||
if info.src_rank in dist.get_process_group_ranks(mpu.get_expert_model_parallel_group())
|
||||
else rank
|
||||
)
|
||||
handles.append(
|
||||
torch.distributed.broadcast(
|
||||
param, src=src_rank, group=mpu.get_expert_model_parallel_group(), async_op=True
|
||||
)
|
||||
)
|
||||
for handle in handles:
|
||||
handle.wait()
|
||||
|
||||
# Set tp attrs for all params
|
||||
for info, param in zip(megatron_local_param_infos, params, strict=False):
|
||||
for key, value in info.attrs.items():
|
||||
setattr(param, key, value)
|
||||
|
||||
# Batch async all_gather for all parameters
|
||||
gathered_params = all_gather_params_async(list(zip(megatron_local_param_infos, params, strict=False)))
|
||||
|
||||
return gathered_params
|
||||
|
||||
|
||||
def _get_megatron_local_param_info_buckets(args: Namespace, model: Sequence[torch.nn.Module]) -> list[list[ParamInfo]]:
|
||||
"""
|
||||
Partition params into buckets ≤ update_weight_buffer_size (with TP replication).
|
||||
"""
|
||||
param_infos = _get_megatron_local_param_infos(args, model)
|
||||
param_info_buckets = [[]] # Start with one empty bucket
|
||||
buffer_size = 0 # Track current bucket size in bytes
|
||||
|
||||
for info in param_infos:
|
||||
# Expert params use expert-TP size, others use regular-TP size
|
||||
if ".experts." in info.name:
|
||||
tp_size = mpu.get_expert_tensor_parallel_world_size()
|
||||
else:
|
||||
tp_size = mpu.get_tensor_model_parallel_world_size()
|
||||
|
||||
# Full param size = shard size × TP replicas (all-gather will reconstruct full param)
|
||||
param_size = info.size * tp_size
|
||||
|
||||
# If adding this param exceeds limit AND current bucket has params: start new bucket
|
||||
if buffer_size + param_size > args.update_weight_buffer_size and len(param_info_buckets[-1]) > 0:
|
||||
param_info_buckets.append([])
|
||||
buffer_size = 0
|
||||
|
||||
# Add param to current bucket and update size
|
||||
param_info_buckets[-1].append(info)
|
||||
buffer_size += param_size
|
||||
|
||||
return param_info_buckets
|
||||
|
||||
|
||||
def _get_megatron_local_param_infos(args: Namespace, model: Sequence[torch.nn.Module]) -> list[ParamInfo]:
|
||||
"""
|
||||
Build global param metadata: collect → exchange PP/EP → resolve duplicates (MTP virtual PP)
|
||||
by min src_rank → validate. Returns sorted ParamInfo identical across all ranks.
|
||||
"""
|
||||
pp_size = mpu.get_pipeline_model_parallel_world_size()
|
||||
ep_size = mpu.get_expert_model_parallel_world_size()
|
||||
|
||||
param_infos = {}
|
||||
rank = dist.get_rank()
|
||||
for name, param in named_params_and_buffers(args, model):
|
||||
param_infos[name] = ParamInfo(
|
||||
name=name,
|
||||
dtype=param.dtype,
|
||||
shape=param.shape,
|
||||
attrs={
|
||||
"tensor_model_parallel": getattr(param, "tensor_model_parallel", False),
|
||||
"partition_dim": getattr(param, "partition_dim", -1),
|
||||
"partition_stride": getattr(param, "partition_stride", 1),
|
||||
"parallel_mode": getattr(param, "parallel_mode", None),
|
||||
},
|
||||
size=param.numel() * param.element_size(),
|
||||
src_rank=rank,
|
||||
)
|
||||
|
||||
if pp_size > 1:
|
||||
param_infos_list = [None] * pp_size
|
||||
dist.all_gather_object(
|
||||
obj=(rank, param_infos), object_list=param_infos_list, group=mpu.get_pipeline_model_parallel_group()
|
||||
)
|
||||
for src_rank, infos in param_infos_list:
|
||||
if src_rank == rank:
|
||||
continue
|
||||
for name, info in infos.items():
|
||||
if name in param_infos:
|
||||
assert args.mtp_num_layers is not None
|
||||
old_info = param_infos[name]
|
||||
if old_info.src_rank > src_rank:
|
||||
param_infos[name] = info
|
||||
else:
|
||||
param_infos[name] = info
|
||||
|
||||
if ep_size > 1:
|
||||
param_infos_list = [None] * ep_size
|
||||
dist.all_gather_object(
|
||||
obj=(rank, param_infos), object_list=param_infos_list, group=mpu.get_expert_model_parallel_group()
|
||||
)
|
||||
for src_rank, infos in param_infos_list:
|
||||
for name, info in infos.items():
|
||||
if name not in param_infos:
|
||||
# here we need to set the src_rank to the rank within the expert model parallel group
|
||||
info = dataclasses.replace(info, src_rank=src_rank)
|
||||
param_infos[name] = info
|
||||
|
||||
param_infos = list(param_infos.values())
|
||||
param_infos = sorted(param_infos, key=lambda info: info.name)
|
||||
|
||||
# Check all ranks has the same parameter info
|
||||
all_param_info_list = [None] * dist.get_world_size()
|
||||
dist.all_gather_object(
|
||||
obj=param_infos,
|
||||
object_list=all_param_info_list,
|
||||
group=get_gloo_group(),
|
||||
)
|
||||
for i, param_info in enumerate(param_infos):
|
||||
for infos in all_param_info_list:
|
||||
assert infos[i].name == param_info.name, f"Parameter name mismatch: {infos[i].name} != {param_info.name}"
|
||||
assert (
|
||||
infos[i].shape == param_info.shape
|
||||
), f"Parameter shape mismatch: {infos[i].shape} != {param_info.shape}"
|
||||
assert (
|
||||
infos[i].dtype == param_info.dtype
|
||||
), f"Parameter dtype mismatch: {infos[i].dtype} != {param_info.dtype}"
|
||||
|
||||
return param_infos
|
||||
@@ -0,0 +1,306 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import socket
|
||||
import time
|
||||
from argparse import Namespace
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
|
||||
import ray
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from megatron.core import mpu
|
||||
from ray import ObjectRef
|
||||
from ray.actor import ActorHandle
|
||||
from tqdm import tqdm
|
||||
|
||||
from slime.utils.distributed_utils import get_gloo_group, init_process_group
|
||||
|
||||
from ..megatron_to_hf import convert_to_hf
|
||||
from .common import all_gather_param, named_params_and_buffers
|
||||
|
||||
|
||||
class UpdateWeightFromDistributed:
|
||||
"""
|
||||
Update distributed engines via NCCL. Each PP rank: group "slime-pp_{pp_rank}",
|
||||
only DP=TP=0 broadcasts. Non-expert (TP) and expert (EP) params separate.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: Namespace,
|
||||
model: Sequence[torch.nn.Module],
|
||||
weights_getter: Callable[[], Mapping[str, torch.Tensor]],
|
||||
*,
|
||||
model_name: str,
|
||||
quantization_config: dict[str, int | str | list[str]] | None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize. Groups created in connect_rollout_engines.
|
||||
"""
|
||||
self.args = args
|
||||
self.model = model
|
||||
self.model_name = model_name
|
||||
self.quantization_config = quantization_config
|
||||
self.weight_version = 0
|
||||
self._model_update_groups = None
|
||||
self.rollout_engines = []
|
||||
|
||||
def connect_rollout_engines(
|
||||
self, rollout_engines: Sequence[ActorHandle], rollout_engine_lock: ActorHandle
|
||||
) -> None:
|
||||
"""
|
||||
Create NCCL "slime-pp_{pp_rank}" if PP source (DP=TP=0). Lock prevents concurrent broadcasts.
|
||||
"""
|
||||
self.rollout_engines = rollout_engines
|
||||
self.rollout_engine_lock = rollout_engine_lock
|
||||
|
||||
# For TP:
|
||||
# 1. AllGather parameters to rank 0
|
||||
# 2. Broadcast parameters from rank 0 to all sglang engines
|
||||
self._is_pp_src_rank = (
|
||||
mpu.get_data_parallel_rank(with_context_parallel=True) == 0 and mpu.get_tensor_model_parallel_rank() == 0
|
||||
)
|
||||
pp_rank = mpu.get_pipeline_model_parallel_rank()
|
||||
if self._is_pp_src_rank:
|
||||
self._group_name = f"slime-pp_{pp_rank}"
|
||||
|
||||
if self._is_pp_src_rank:
|
||||
if self._model_update_groups is not None:
|
||||
disconnect_rollout_engines_from_distributed(
|
||||
self.args, self._group_name, self._model_update_groups, self.rollout_engines
|
||||
)
|
||||
self._model_update_groups = connect_rollout_engines_from_distributed(
|
||||
self.args, self._group_name, rollout_engines
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def update_weights(self) -> None:
|
||||
"""
|
||||
Pause → flush → non-expert (TP) → expert (EP) → continue. Progress on PP source.
|
||||
"""
|
||||
if not self.rollout_engines:
|
||||
return
|
||||
|
||||
self.weight_version += 1
|
||||
|
||||
if dist.get_rank() == 0:
|
||||
ray.get([engine.pause_generation.remote() for engine in self.rollout_engines])
|
||||
ray.get([engine.flush_cache.remote() for engine in self.rollout_engines])
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
buffer_size = 0
|
||||
converted_named_tensors = []
|
||||
# non expert params
|
||||
pbar = tqdm(desc=f"[{self._group_name}] Update weights", total=0) if self._is_pp_src_rank else None
|
||||
|
||||
for name, param in named_params_and_buffers(self.args, self.model):
|
||||
if ".experts." in name:
|
||||
continue
|
||||
buffer_size = self._update_weight_from_distributed(
|
||||
name, param, converted_named_tensors, buffer_size, pbar=pbar
|
||||
)
|
||||
|
||||
if converted_named_tensors:
|
||||
self._update_bucket_weights_from_distributed(converted_named_tensors, pbar=pbar)
|
||||
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
buffer_size = 0
|
||||
named_tensors = []
|
||||
for name, param in named_params_and_buffers(self.args, self.model):
|
||||
if ".experts." not in name:
|
||||
continue
|
||||
buffer_size = self._update_expert_weight_from_distributed(
|
||||
name, param, named_tensors, buffer_size, pbar=pbar
|
||||
)
|
||||
|
||||
if named_tensors:
|
||||
self._update_expert_bucket_weights_from_distributed(named_tensors, pbar=pbar)
|
||||
|
||||
dist.barrier(group=get_gloo_group())
|
||||
if dist.get_rank() == 0:
|
||||
ray.get([engine.continue_generation.remote() for engine in self.rollout_engines])
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
def _update_weight_from_distributed(
|
||||
self,
|
||||
name: str,
|
||||
param: torch.nn.Parameter,
|
||||
converted_named_tensors: list[tuple[str, torch.Tensor]],
|
||||
buffer_size: int,
|
||||
pbar: tqdm | None = None,
|
||||
) -> int | None:
|
||||
"""
|
||||
Non-expert: gather TP → rm pad → HF → buffer (flush if full). All gather, PP source buffers.
|
||||
Returns updated bytes on source, None on non-source.
|
||||
"""
|
||||
param = all_gather_param(name, param)
|
||||
if not self._is_pp_src_rank:
|
||||
return
|
||||
|
||||
param_size = param.numel() * param.element_size()
|
||||
if buffer_size + param_size > self.args.update_weight_buffer_size:
|
||||
self._update_bucket_weights_from_distributed(converted_named_tensors, pbar=pbar)
|
||||
buffer_size = 0
|
||||
converted_named_tensors += convert_to_hf(self.args, self.model_name, name, param, self.quantization_config)
|
||||
buffer_size += param_size
|
||||
return buffer_size
|
||||
|
||||
def _update_expert_weight_from_distributed(
|
||||
self,
|
||||
name: str,
|
||||
param: torch.nn.Parameter,
|
||||
named_tensors: list[tuple[str, torch.Tensor]],
|
||||
buffer_size: int,
|
||||
pbar: tqdm | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
Expert: gather TP → rm pad → buffer. EP gather + HF deferred. Threshold × EP size.
|
||||
"""
|
||||
param = all_gather_param(name, param)
|
||||
|
||||
param_size = param.numel() * param.element_size()
|
||||
if (
|
||||
buffer_size + param_size
|
||||
) * mpu.get_expert_model_parallel_world_size() > self.args.update_weight_buffer_size:
|
||||
self._update_expert_bucket_weights_from_distributed(named_tensors, pbar=pbar)
|
||||
buffer_size = 0
|
||||
|
||||
named_tensors.append((name, param))
|
||||
buffer_size += param_size
|
||||
return buffer_size
|
||||
|
||||
def _update_expert_bucket_weights_from_distributed(
|
||||
self, named_tensors: list[tuple[str, torch.Tensor]], pbar: tqdm | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Gather EP → HF → broadcast. Clears buffer.
|
||||
"""
|
||||
names = [name for name, _ in named_tensors]
|
||||
all_names = [None] * mpu.get_expert_model_parallel_world_size()
|
||||
dist.all_gather_object(all_names, names, group=mpu.get_expert_model_parallel_group())
|
||||
|
||||
for names in all_names:
|
||||
assert len(named_tensors) == len(names), f"mismatch names length: {len(named_tensors)} != {len(names)}"
|
||||
|
||||
all_gathered_params = [[] for _ in range(mpu.get_expert_model_parallel_world_size())]
|
||||
handles = []
|
||||
for i, (_name, param) in enumerate(named_tensors):
|
||||
params = [
|
||||
torch.empty_like(param.data, device=torch.cuda.current_device())
|
||||
for _ in range(mpu.get_expert_model_parallel_world_size())
|
||||
]
|
||||
handle = dist.all_gather(params, param.data, group=mpu.get_expert_model_parallel_group(), async_op=True)
|
||||
handles.append(handle)
|
||||
for ep_rank, names in enumerate(all_names):
|
||||
all_gathered_params[ep_rank].append((names[i], params[ep_rank]))
|
||||
for handle in handles:
|
||||
handle.wait()
|
||||
|
||||
named_tensors.clear()
|
||||
if not self._is_pp_src_rank:
|
||||
return
|
||||
|
||||
all_gathered_params = sum(all_gathered_params, [])
|
||||
converted_hf_tensors = []
|
||||
for name, param in all_gathered_params:
|
||||
converted_hf_tensors += convert_to_hf(self.args, self.model_name, name, param, self.quantization_config)
|
||||
|
||||
self._update_bucket_weights_from_distributed(converted_hf_tensors, pbar)
|
||||
|
||||
def _update_bucket_weights_from_distributed(
|
||||
self, converted_named_tensors: list[tuple[str, torch.Tensor]], pbar: tqdm | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Lock → broadcast → clear → unlock → pbar++. Lock prevents NCCL deadlock.
|
||||
"""
|
||||
# lock the rollout engines to prevent dead lock on broadcast.
|
||||
while not ray.get(self.rollout_engine_lock.acquire.remote()):
|
||||
time.sleep(0.1)
|
||||
|
||||
refs = update_weights_from_distributed(
|
||||
self._group_name,
|
||||
self._model_update_groups,
|
||||
self.weight_version,
|
||||
self.rollout_engines,
|
||||
converted_named_tensors,
|
||||
)
|
||||
|
||||
ray.get(refs)
|
||||
converted_named_tensors.clear()
|
||||
ray.get(self.rollout_engine_lock.release.remote())
|
||||
pbar.update(1)
|
||||
|
||||
|
||||
def connect_rollout_engines_from_distributed(
|
||||
args: Namespace, group_name: str, rollout_engines: Sequence[ActorHandle]
|
||||
) -> dist.ProcessGroup:
|
||||
"""
|
||||
Create NCCL group: training rank 0 + all engine GPUs. Blocks until joined.
|
||||
"""
|
||||
master_address = ray._private.services.get_node_ip_address()
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("", 0))
|
||||
master_port = sock.getsockname()[1]
|
||||
world_size = len(rollout_engines) * args.rollout_num_gpus_per_engine + 1
|
||||
|
||||
refs = [
|
||||
engine.init_weights_update_group.remote(
|
||||
master_address,
|
||||
master_port,
|
||||
i * args.rollout_num_gpus_per_engine + 1,
|
||||
world_size,
|
||||
group_name,
|
||||
backend="nccl",
|
||||
)
|
||||
for i, engine in enumerate(rollout_engines)
|
||||
]
|
||||
model_update_groups = init_process_group(
|
||||
backend="nccl",
|
||||
init_method=f"tcp://{master_address}:{master_port}",
|
||||
world_size=world_size,
|
||||
rank=0,
|
||||
group_name=group_name,
|
||||
)
|
||||
ray.get(refs)
|
||||
return model_update_groups
|
||||
|
||||
|
||||
def disconnect_rollout_engines_from_distributed(args, group_name, model_update_groups, rollout_engines):
|
||||
"""
|
||||
Destroy NCCL on training and engines.
|
||||
"""
|
||||
refs = [engine.destroy_weights_update_group.remote(group_name) for engine in rollout_engines]
|
||||
dist.destroy_process_group(model_update_groups)
|
||||
ray.get(refs)
|
||||
|
||||
|
||||
def update_weights_from_distributed(
|
||||
group_name: str,
|
||||
group: dist.ProcessGroup,
|
||||
weight_version: int,
|
||||
rollout_engines: Sequence[ActorHandle],
|
||||
converted_named_tensors: Sequence[tuple[str, torch.Tensor]],
|
||||
) -> list[ObjectRef]:
|
||||
"""
|
||||
Send metadata (Ray), broadcast tensors (NCCL rank 0 → engines).
|
||||
"""
|
||||
refs = [
|
||||
engine.update_weights_from_distributed.remote(
|
||||
names=[name for name, _ in converted_named_tensors],
|
||||
dtypes=[param.dtype for _, param in converted_named_tensors],
|
||||
shapes=[param.shape for _, param in converted_named_tensors],
|
||||
group_name=group_name,
|
||||
weight_version=str(weight_version),
|
||||
)
|
||||
for engine in rollout_engines
|
||||
]
|
||||
|
||||
handles = []
|
||||
for _, param in converted_named_tensors:
|
||||
handles.append(dist.broadcast(param.data, 0, group=group, async_op=True))
|
||||
for handle in handles:
|
||||
handle.wait()
|
||||
|
||||
return refs
|
||||
@@ -0,0 +1,209 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from argparse import Namespace
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
import ray
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from megatron.core import mpu
|
||||
from ray import ObjectRef
|
||||
from ray.actor import ActorHandle
|
||||
|
||||
from slime.utils.distributed_utils import get_gloo_group
|
||||
|
||||
from ..sglang import FlattenedTensorBucket, MultiprocessingSerializer
|
||||
from .hf_weight_iterator_base import HfWeightIteratorBase
|
||||
from .update_weight_from_distributed import (
|
||||
connect_rollout_engines_from_distributed,
|
||||
disconnect_rollout_engines_from_distributed,
|
||||
update_weights_from_distributed,
|
||||
)
|
||||
|
||||
|
||||
class UpdateWeightFromTensor:
|
||||
"""
|
||||
Update rollout engines from tensor dict:
|
||||
load(dict→GPU) → broadcast PP/EP(GPU NCCL) → gather TP(GPU NCCL) → convert HF(GPU) → send.
|
||||
Colocated: GPU→CPU serialize → gather_object(Gloo CPU, collects from rollout_num_gpus_per_engine ranks) → Ray IPC to engine.
|
||||
Distributed: GPU NCCL broadcast to remote engines.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: Namespace,
|
||||
model: Sequence[torch.nn.Module],
|
||||
weights_getter: Callable[[], Mapping[str, torch.Tensor]],
|
||||
*,
|
||||
model_name: str,
|
||||
quantization_config: dict[str, int | str | list[str]] | None,
|
||||
) -> None:
|
||||
"""
|
||||
Compute param buckets, create IPC Gloo groups (rollout_num_gpus_per_engine ranks/group).
|
||||
"""
|
||||
self.args = args
|
||||
self.model = model
|
||||
self.weights_getter = weights_getter
|
||||
self.model_name = model_name
|
||||
self.quantization_config = quantization_config
|
||||
self.weight_version = 0
|
||||
|
||||
self._hf_weight_iterator = HfWeightIteratorBase.create(
|
||||
args=args, model=model, model_name=model_name, quantization_config=quantization_config
|
||||
)
|
||||
|
||||
# create the group within megatron.
|
||||
for start_rank in range(0, dist.get_world_size(), self.args.rollout_num_gpus_per_engine):
|
||||
end_rank = start_rank + self.args.rollout_num_gpus_per_engine
|
||||
group_ranks = list(range(start_rank, end_rank))
|
||||
new_group = dist.new_group(ranks=group_ranks, backend="gloo")
|
||||
if dist.get_rank() in group_ranks:
|
||||
self._ipc_gather_group = new_group
|
||||
self._ipc_gather_src = start_rank
|
||||
|
||||
self._model_update_groups = None
|
||||
|
||||
def connect_rollout_engines(
|
||||
self, rollout_engines: Sequence[ActorHandle], rollout_engine_lock: ActorHandle
|
||||
) -> None:
|
||||
"""
|
||||
Split colocated/distributed engines. Global source rank (DP=TP=PP=0) creates NCCL
|
||||
for distributed. Map ranks to colocated IPC engines.
|
||||
"""
|
||||
self.rollout_engines = rollout_engines
|
||||
colocate_engine_nums = (
|
||||
self.args.actor_num_nodes * self.args.actor_num_gpus_per_node // self.args.rollout_num_gpus_per_engine
|
||||
)
|
||||
self.use_distribute = len(rollout_engines) > colocate_engine_nums
|
||||
|
||||
if self.use_distribute:
|
||||
self.rollout_engines = rollout_engines[:colocate_engine_nums]
|
||||
self.distributed_rollout_engines = rollout_engines[colocate_engine_nums:]
|
||||
self._is_distributed_src_rank = (
|
||||
mpu.get_data_parallel_rank(with_context_parallel=True) == 0
|
||||
and mpu.get_tensor_model_parallel_rank() == 0
|
||||
and mpu.get_pipeline_model_parallel_rank() == 0
|
||||
)
|
||||
self._group_name = "slime"
|
||||
if self._is_distributed_src_rank:
|
||||
if self._model_update_groups is not None:
|
||||
disconnect_rollout_engines_from_distributed(
|
||||
self.args, self._group_name, self._model_update_groups, self.distributed_rollout_engines
|
||||
)
|
||||
|
||||
self._model_update_groups = connect_rollout_engines_from_distributed(
|
||||
self.args, self._group_name, self.distributed_rollout_engines
|
||||
)
|
||||
|
||||
# Here we assume the gpu id of rollout engines and train actors are the same.
|
||||
for i, engine in enumerate(self.rollout_engines):
|
||||
start_rank = i * self.args.rollout_num_gpus_per_engine
|
||||
end_rank = (i + 1) * self.args.rollout_num_gpus_per_engine
|
||||
group_ranks = list(range(start_rank, end_rank))
|
||||
if dist.get_rank() in group_ranks:
|
||||
self._ipc_engine = engine
|
||||
|
||||
@torch.no_grad()
|
||||
def update_weights(self) -> None:
|
||||
"""
|
||||
version++, flush caches, process buckets. Progress on rank 0.
|
||||
"""
|
||||
self.weight_version += 1
|
||||
|
||||
rank = dist.get_rank()
|
||||
if rank == 0:
|
||||
ray.get([engine.flush_cache.remote() for engine in self.rollout_engines])
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
megatron_local_weights = self.weights_getter()
|
||||
|
||||
for hf_named_tensors in self._hf_weight_iterator.get_hf_weight_chunks(megatron_local_weights):
|
||||
refs, long_lived_tensors = self._send_hf_params(hf_named_tensors)
|
||||
ray.get(refs)
|
||||
del long_lived_tensors
|
||||
|
||||
dist.barrier(group=get_gloo_group())
|
||||
|
||||
def _send_hf_params(self, hf_named_tensors) -> tuple[list[ObjectRef], Any]:
|
||||
all_refs = []
|
||||
|
||||
refs_colocated, long_lived_tensors = _send_to_colocated_engine(
|
||||
hf_named_tensors,
|
||||
ipc_engine=self._ipc_engine,
|
||||
ipc_gather_src=self._ipc_gather_src,
|
||||
ipc_gather_group=self._ipc_gather_group,
|
||||
weight_version=self.weight_version,
|
||||
)
|
||||
all_refs.extend(refs_colocated)
|
||||
|
||||
if self.use_distribute and self._is_distributed_src_rank:
|
||||
refs_distributed = update_weights_from_distributed(
|
||||
self._group_name,
|
||||
self._model_update_groups,
|
||||
self.weight_version,
|
||||
self.distributed_rollout_engines,
|
||||
hf_named_tensors,
|
||||
)
|
||||
if refs_distributed:
|
||||
all_refs.extend(refs_distributed)
|
||||
|
||||
return all_refs, long_lived_tensors
|
||||
|
||||
|
||||
def _send_to_colocated_engine(
|
||||
hf_named_tensors: list[tuple[str, torch.Tensor]],
|
||||
*,
|
||||
ipc_engine,
|
||||
ipc_gather_src,
|
||||
ipc_gather_group,
|
||||
weight_version,
|
||||
) -> tuple[list[ObjectRef], Any]:
|
||||
# TODO improve
|
||||
long_live_tensors = []
|
||||
|
||||
if getattr(FlattenedTensorBucket, "supports_multi_dtypes", False):
|
||||
converted_named_tensors_by_dtypes = {"dtype": hf_named_tensors}
|
||||
else:
|
||||
converted_named_tensors_by_dtypes = {}
|
||||
for name, tensor in hf_named_tensors:
|
||||
dtype = tensor.dtype
|
||||
if dtype not in converted_named_tensors_by_dtypes:
|
||||
converted_named_tensors_by_dtypes[dtype] = []
|
||||
converted_named_tensors_by_dtypes[dtype].append((name, tensor))
|
||||
|
||||
serialized_tensors = []
|
||||
for _dtype, named_tensors in converted_named_tensors_by_dtypes.items():
|
||||
flattened_tensor_bucket = FlattenedTensorBucket(named_tensors=named_tensors)
|
||||
metadata = flattened_tensor_bucket.get_metadata()
|
||||
flattened_tensor_data = {
|
||||
"flattened_tensor": flattened_tensor_bucket.get_flattened_tensor(),
|
||||
"metadata": metadata,
|
||||
}
|
||||
long_live_tensors.append(flattened_tensor_data)
|
||||
serialized_tensors.append(MultiprocessingSerializer.serialize(flattened_tensor_data, output_str=True))
|
||||
|
||||
serialized_named_tensors = (
|
||||
[None] * dist.get_world_size(ipc_gather_group) if ipc_gather_src == dist.get_rank() else None
|
||||
)
|
||||
dist.gather_object(
|
||||
serialized_tensors,
|
||||
object_gather_list=serialized_named_tensors,
|
||||
dst=ipc_gather_src,
|
||||
group=ipc_gather_group,
|
||||
)
|
||||
|
||||
refs = []
|
||||
if dist.get_rank() == ipc_gather_src:
|
||||
# TODO: here we assume all ranks have the same number of dtypes, not sure if that is correct.
|
||||
num_dtypes = len(serialized_named_tensors[0])
|
||||
for i in range(num_dtypes):
|
||||
kwargs = {
|
||||
"serialized_named_tensors": [tensors[i] for tensors in serialized_named_tensors],
|
||||
"load_format": "flattened_bucket",
|
||||
"weight_version": str(weight_version),
|
||||
}
|
||||
refs.append(ipc_engine.update_weights_from_tensor.remote(**kwargs))
|
||||
|
||||
return refs, long_live_tensors
|
||||
3
slime/backends/sglang_utils/__init__.py
Normal file
3
slime/backends/sglang_utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
BIN
slime/backends/sglang_utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
slime/backends/sglang_utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
133
slime/backends/sglang_utils/arguments.py
Normal file
133
slime/backends/sglang_utils/arguments.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import sglang
|
||||
from packaging.version import parse
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from slime.utils.http_utils import _wrap_ipv6
|
||||
|
||||
|
||||
# TODO: use all sglang router arguments with `--sglang-router` prefix
|
||||
def add_sglang_router_arguments(parser):
|
||||
"""
|
||||
Add arguments to the parser for the SGLang router.
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--sglang-router-ip",
|
||||
type=str,
|
||||
default=None,
|
||||
help="IP address of the SGLang router",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sglang-router-port",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Port of the SGLang router",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sglang-router-request-timeout-secs",
|
||||
type=int,
|
||||
default=14400,
|
||||
help="Timeout for requests to the SGLang router in seconds",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def add_sglang_arguments(parser):
|
||||
"""
|
||||
Add arguments to the parser for the SGLang server.
|
||||
"""
|
||||
parser = add_sglang_router_arguments(parser)
|
||||
parser.add_argument("--sglang-server-concurrency", type=int, default=512)
|
||||
|
||||
old_add_argument = parser.add_argument
|
||||
|
||||
skipped_args = [
|
||||
"model_path",
|
||||
"dtype",
|
||||
"trust_remote_code",
|
||||
"random_seed",
|
||||
# memory
|
||||
"enable_memory_saver",
|
||||
# distributed
|
||||
"tp_size",
|
||||
"port",
|
||||
"nnodes",
|
||||
"node_rank",
|
||||
"dist_init_addr",
|
||||
"gpu_id_step",
|
||||
"base_gpu_id",
|
||||
"nccl_port",
|
||||
"skip_server_warmup",
|
||||
"enable_return_routed_experts",
|
||||
]
|
||||
|
||||
def new_add_argument_wrapper(*name_or_flags, **kwargs):
|
||||
"""
|
||||
Add arguments to the parser, ensuring that the server arguments are prefixed and skippable.
|
||||
"""
|
||||
# Determine the canonical name for skip check (e.g., "model_path")
|
||||
canonical_name_for_skip_check = None
|
||||
if "dest" in kwargs:
|
||||
canonical_name_for_skip_check = kwargs["dest"]
|
||||
else:
|
||||
for flag_name_candidate in name_or_flags:
|
||||
if isinstance(flag_name_candidate, str) and flag_name_candidate.startswith("--"):
|
||||
# Derive from first long flag: --foo-bar -> foo_bar
|
||||
stem = flag_name_candidate[2:]
|
||||
canonical_name_for_skip_check = stem.replace("-", "_")
|
||||
break
|
||||
# If no long flag and no dest, skip logic might not catch it unless short flags imply a dest.
|
||||
|
||||
if canonical_name_for_skip_check and canonical_name_for_skip_check in skipped_args:
|
||||
return # Skip this entire argument definition
|
||||
|
||||
# If not skipped, proceed to prefix flags and dest
|
||||
new_name_or_flags_list = []
|
||||
for item_flag in name_or_flags:
|
||||
if isinstance(item_flag, str) and item_flag.startswith("-"):
|
||||
original_flag_stem = item_flag.lstrip("-") # "foo-bar" from "--foo-bar", or "f" from "-f"
|
||||
prefixed_item = f"--sglang-{original_flag_stem}"
|
||||
new_name_or_flags_list.append(prefixed_item)
|
||||
else:
|
||||
# Positional arguments or non-string items
|
||||
new_name_or_flags_list.append(item_flag)
|
||||
|
||||
# Prepare kwargs for the actual add_argument call.
|
||||
# Make a copy to avoid modifying the original kwargs dict.
|
||||
final_kwargs = kwargs.copy()
|
||||
|
||||
# If 'dest' is explicitly provided and is a string, prefix it.
|
||||
# This ensures the attribute on the args namespace becomes, e.g., args.sglang_dest_name.
|
||||
if "dest" in final_kwargs and isinstance(final_kwargs["dest"], str):
|
||||
original_dest = final_kwargs["dest"]
|
||||
# Avoid double prefixing if dest somehow already starts with sglang_
|
||||
if not original_dest.startswith("sglang_"):
|
||||
final_kwargs["dest"] = f"sglang_{original_dest}"
|
||||
# If 'dest' is not explicitly provided (or is None/not a string),
|
||||
# argparse will derive 'dest' from the (now prefixed) flag names.
|
||||
# E.g., if the first flag is "--sglang-foo-bar", argparse sets dest to "sglang_foo_bar".
|
||||
|
||||
old_add_argument(*new_name_or_flags_list, **final_kwargs)
|
||||
|
||||
parser.add_argument = new_add_argument_wrapper
|
||||
ServerArgs.add_cli_args(parser)
|
||||
parser.add_argument = old_add_argument
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def validate_args(args):
|
||||
if parse(sglang.__version__) == parse("0.4.10") and getattr(args, "sglang_enable_ep_moe", False):
|
||||
args.sglang_expert_parallel_size = args.rollout_num_gpus_per_engine
|
||||
|
||||
args.sglang_tp_size = args.rollout_num_gpus_per_engine
|
||||
args.sglang_dp_size = args.sglang_data_parallel_size
|
||||
args.sglang_pp_size = args.sglang_pipeline_parallel_size
|
||||
args.sglang_ep_size = args.sglang_expert_parallel_size
|
||||
|
||||
if args.sglang_dp_size > 1:
|
||||
assert args.sglang_enable_dp_attention
|
||||
|
||||
if getattr(args, "sglang_router_ip", None):
|
||||
args.sglang_router_ip = _wrap_ipv6(args.sglang_router_ip)
|
||||
491
slime/backends/sglang_utils/sglang_engine.py
Normal file
491
slime/backends/sglang_utils/sglang_engine.py
Normal file
@@ -0,0 +1,491 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import multiprocessing
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
import sglang_router
|
||||
from packaging.version import parse
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from urllib3.exceptions import NewConnectionError
|
||||
|
||||
from slime.ray.ray_actor import RayActor
|
||||
from slime.utils.http_utils import get_host_info
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_base_gpu_id(args, rank):
|
||||
num_gpus = min(args.num_gpus_per_node, args.rollout_num_gpus_per_engine)
|
||||
if args.colocate:
|
||||
start_index = (rank * num_gpus) % args.num_gpus_per_node
|
||||
else:
|
||||
num_actor_gpus = 0 if args.debug_rollout_only else args.actor_num_gpus_per_node * args.actor_num_nodes
|
||||
start_index = (num_actor_gpus + rank * num_gpus) % args.num_gpus_per_node
|
||||
if args.use_critic:
|
||||
num_critic_gpus = args.critic_num_gpus_per_node * args.critic_num_nodes
|
||||
start_index = (num_actor_gpus + num_critic_gpus + rank * num_gpus) % args.num_gpus_per_node
|
||||
return start_index
|
||||
|
||||
|
||||
def launch_server_process(server_args: ServerArgs) -> multiprocessing.Process:
|
||||
from sglang.srt.entrypoints.http_server import launch_server
|
||||
|
||||
multiprocessing.set_start_method("spawn", force=True)
|
||||
server_args.host = server_args.host.strip("[]")
|
||||
p = multiprocessing.Process(target=launch_server, args=(server_args,))
|
||||
p.start()
|
||||
|
||||
if server_args.node_rank != 0:
|
||||
return
|
||||
|
||||
_wait_server_healthy(
|
||||
base_url=server_args.url(),
|
||||
api_key=server_args.api_key,
|
||||
is_process_alive=lambda: p.is_alive(),
|
||||
)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def _wait_server_healthy(base_url, api_key, is_process_alive):
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
|
||||
with requests.Session() as session:
|
||||
while True:
|
||||
try:
|
||||
response = session.get(f"{base_url}/health_generate", headers=headers)
|
||||
if response.status_code == 200:
|
||||
break
|
||||
except requests.RequestException:
|
||||
pass
|
||||
|
||||
if not is_process_alive():
|
||||
raise Exception("Server process terminated unexpectedly.")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# use flush_cache to make sure the working queue is empty, so that we can do offload
|
||||
while True:
|
||||
try:
|
||||
response = session.get(f"{base_url}/flush_cache", headers=headers)
|
||||
if response.status_code == 200:
|
||||
break
|
||||
|
||||
except requests.RequestException:
|
||||
pass
|
||||
|
||||
if not is_process_alive():
|
||||
raise Exception("Server process terminated unexpectedly.")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
class SGLangEngine(RayActor):
|
||||
def __init__(self, args, rank: int, worker_type: str = "regular"):
|
||||
self.args = args
|
||||
self.rank = rank
|
||||
self.worker_type = worker_type
|
||||
|
||||
def init(self, dist_init_addr, port, nccl_port, host=None, disaggregation_bootstrap_port=None):
|
||||
self.router_ip = self.args.sglang_router_ip
|
||||
self.router_port = self.args.sglang_router_port
|
||||
|
||||
host = host or get_host_info()[1]
|
||||
|
||||
# support ipv6 address
|
||||
if ":" in host and not host.startswith("["):
|
||||
host = f"[{host}]"
|
||||
|
||||
# dist_init_addr may be 2605:...:10163, should split port
|
||||
*addr_parts, port_str = dist_init_addr.split(":")
|
||||
ipv6_addr = ":".join(addr_parts)
|
||||
if ":" in ipv6_addr and not ipv6_addr.startswith("["):
|
||||
dist_init_addr = f"[{ipv6_addr}]:{port_str}"
|
||||
|
||||
server_args_dict, external_engine_need_check_fields = _compute_server_args(
|
||||
self.args,
|
||||
self.rank,
|
||||
dist_init_addr,
|
||||
nccl_port,
|
||||
host,
|
||||
port,
|
||||
self.worker_type,
|
||||
disaggregation_bootstrap_port,
|
||||
)
|
||||
|
||||
self.node_rank = server_args_dict["node_rank"]
|
||||
self.server_host = server_args_dict["host"]
|
||||
self.server_port = server_args_dict["port"]
|
||||
|
||||
if self.args.rollout_external:
|
||||
self._init_external(server_args_dict, external_engine_need_check_fields=external_engine_need_check_fields)
|
||||
else:
|
||||
self._init_normal(server_args_dict)
|
||||
|
||||
def _init_external(self, expect_server_args, external_engine_need_check_fields):
|
||||
logger.info(f"Use external SGLang engine (rank={self.rank}, expect_server_args={expect_server_args})")
|
||||
|
||||
def _get_actual_server_args():
|
||||
response = requests.get(f"http://{self.server_host}:{self.server_port}/get_server_info")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def _sanity_check_server_args(actual_server_args, expect_server_args):
|
||||
for name in external_engine_need_check_fields:
|
||||
expect_value = expect_server_args.get(name)
|
||||
actual_value = actual_server_args.get(name)
|
||||
assert (
|
||||
actual_value == expect_value
|
||||
), f"{name=} {expect_value=} {actual_value=} {expect_server_args=} {actual_server_args=}"
|
||||
|
||||
_wait_server_healthy(
|
||||
base_url=f"http://{self.server_host}:{self.server_port}",
|
||||
api_key=None,
|
||||
is_process_alive=lambda: True,
|
||||
)
|
||||
actual_server_args = _get_actual_server_args()
|
||||
_sanity_check_server_args(actual_server_args, expect_server_args)
|
||||
|
||||
def _init_normal(self, server_args_dict):
|
||||
logger.info(f"Launch HttpServerEngineAdapter at: {self.server_host}:{self.server_port}")
|
||||
self.process = launch_server_process(ServerArgs(**server_args_dict))
|
||||
|
||||
if self.node_rank == 0 and self.router_ip and self.router_port:
|
||||
if parse(sglang_router.__version__) <= parse("0.2.1") or self.args.use_slime_router:
|
||||
assert (
|
||||
self.worker_type == "regular"
|
||||
), "pd disaggregation is not supported in old router or slime router."
|
||||
response = requests.post(
|
||||
f"http://{self.router_ip}:{self.router_port}/add_worker?url=http://{self.server_host}:{self.server_port}"
|
||||
)
|
||||
else:
|
||||
payload = {
|
||||
"url": f"http://{self.server_host}:{self.server_port}",
|
||||
"worker_type": self.worker_type,
|
||||
}
|
||||
if self.worker_type == "prefill":
|
||||
payload["bootstrap_port"] = server_args_dict["disaggregation_bootstrap_port"]
|
||||
response = requests.post(
|
||||
f"http://{self.router_ip}:{self.router_port}/workers",
|
||||
json=payload,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def _make_request(self, endpoint: str, payload: dict | None = None):
|
||||
"""Make a POST request to the specified endpoint with the given payload.
|
||||
|
||||
Args:
|
||||
endpoint: The API endpoint to call
|
||||
payload: The JSON payload to send (default: empty dict)
|
||||
|
||||
Returns:
|
||||
The JSON response from the server
|
||||
"""
|
||||
if self.node_rank != 0:
|
||||
return
|
||||
|
||||
url = f"http://{self.server_host}:{self.server_port}/{endpoint}"
|
||||
response = requests.post(url, json=payload or {})
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
e.add_note(f"{response.text=}")
|
||||
raise
|
||||
return response.json()
|
||||
|
||||
def health_generate(self, timeout: float = 5.0) -> bool:
|
||||
"""Run /health_generate on the underlying SGLang HTTP server.
|
||||
|
||||
Args:
|
||||
timeout: Timeout for the health request in seconds.
|
||||
|
||||
Returns:
|
||||
True if the server responds with HTTP 200.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the request fails for any reason, including timeout.
|
||||
"""
|
||||
if self.node_rank != 0:
|
||||
return True
|
||||
|
||||
response = requests.get(
|
||||
f"http://{self.server_host}:{self.server_port}/health_generate",
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
|
||||
def update_weights_from_tensor(
|
||||
self,
|
||||
serialized_named_tensors: list[str],
|
||||
load_format: str | None = None,
|
||||
flush_cache: bool = False,
|
||||
weight_version: str | None = None,
|
||||
):
|
||||
"""
|
||||
Update model weights from tensor data. The HTTP server will only post meta data, and the real weights will be copied directly from GPUs.
|
||||
|
||||
Note: The model should be on GPUs rather than CPU for this functionality to work properly.
|
||||
If you encounter issues, ensure your model is loaded on GPU devices rather than CPU.
|
||||
"""
|
||||
payload = {
|
||||
"serialized_named_tensors": serialized_named_tensors,
|
||||
"load_format": load_format,
|
||||
"flush_cache": flush_cache,
|
||||
}
|
||||
if weight_version is not None:
|
||||
payload["weight_version"] = weight_version
|
||||
return self._make_request(
|
||||
"update_weights_from_tensor",
|
||||
payload,
|
||||
)
|
||||
|
||||
def flush_cache(self):
|
||||
"""Flush the cache of the server."""
|
||||
if self.node_rank != 0:
|
||||
return
|
||||
# flush cache will not return status_code 200 when there are pending requests
|
||||
for _ in range(60):
|
||||
try:
|
||||
response = requests.get(f"http://{self.server_host}:{self.server_port}/flush_cache")
|
||||
if response.status_code == 200:
|
||||
break
|
||||
except NewConnectionError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.info(f"Error flushing cache: {e}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
else:
|
||||
raise TimeoutError("Timeout while flushing cache.")
|
||||
|
||||
def shutdown(self):
|
||||
if self.args.rollout_external:
|
||||
return
|
||||
|
||||
logger.info(f"Shutdown engine {self.server_host}:{self.server_port}...")
|
||||
if self.node_rank == 0:
|
||||
worker_url = f"http://{self.server_host}:{self.server_port}"
|
||||
response = None
|
||||
if parse(sglang_router.__version__) <= parse("0.2.1") or self.args.use_slime_router:
|
||||
response = requests.post(
|
||||
f"http://{self.router_ip}:{self.router_port}/remove_worker?url=http://{self.server_host}:{self.server_port}"
|
||||
)
|
||||
elif parse(sglang_router.__version__) < parse("0.3.0"):
|
||||
worker_url = quote(worker_url, safe="")
|
||||
response = requests.delete(f"http://{self.router_ip}:{self.router_port}/workers/{worker_url}")
|
||||
else:
|
||||
try:
|
||||
all_workers = requests.get(f"http://{self.router_ip}:{self.router_port}/workers").json()["workers"]
|
||||
for worker in all_workers:
|
||||
if worker["url"] == worker_url:
|
||||
worker_id = worker["id"]
|
||||
response = requests.delete(
|
||||
f"http://{self.router_ip}:{self.router_port}/workers/{worker_id}"
|
||||
)
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Worker {worker_url} not found in router during shutdown.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch workers list or remove worker: {e}")
|
||||
|
||||
if response is not None:
|
||||
response.raise_for_status()
|
||||
kill_process_tree(self.process.pid)
|
||||
|
||||
def get_weight_version(self):
|
||||
if self.node_rank != 0:
|
||||
return
|
||||
url = f"http://{self.server_host}:{self.server_port}/get_weight_version"
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()["weight_version"]
|
||||
|
||||
def release_memory_occupation(self):
|
||||
self.flush_cache()
|
||||
return self._make_request("release_memory_occupation")
|
||||
|
||||
def resume_memory_occupation(self, tags: list[str] = None):
|
||||
"""
|
||||
Available tags for multi-stage resume: weights, kv_cache
|
||||
"""
|
||||
return self._make_request(
|
||||
"resume_memory_occupation",
|
||||
{"tags": tags},
|
||||
)
|
||||
|
||||
def check_weights(self, action: str):
|
||||
return self._make_request("weights_checker", {"action": action})
|
||||
|
||||
def init_weights_update_group(self, master_address, master_port, rank_offset, world_size, group_name, backend):
|
||||
return self._make_request(
|
||||
"init_weights_update_group",
|
||||
{
|
||||
"master_address": master_address,
|
||||
"master_port": master_port,
|
||||
"rank_offset": rank_offset,
|
||||
"world_size": world_size,
|
||||
"group_name": group_name,
|
||||
"backend": backend,
|
||||
},
|
||||
)
|
||||
|
||||
def destroy_weights_update_group(self, group_name):
|
||||
try:
|
||||
return self._make_request(
|
||||
"destroy_weights_update_group",
|
||||
{
|
||||
"group_name": group_name,
|
||||
},
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
# catch the case there the engine is just created and does not have the group.
|
||||
pass
|
||||
|
||||
def update_weights_from_distributed(
|
||||
self, names, dtypes, shapes, group_name, flush_cache=False, weight_version: str | None = None
|
||||
):
|
||||
payload = {
|
||||
"names": names,
|
||||
"dtypes": [str(dtype).replace("torch.", "") for dtype in dtypes],
|
||||
"shapes": shapes,
|
||||
"group_name": group_name,
|
||||
"flush_cache": flush_cache,
|
||||
}
|
||||
if weight_version is not None:
|
||||
payload["weight_version"] = weight_version
|
||||
return self._make_request(
|
||||
"update_weights_from_distributed",
|
||||
payload,
|
||||
)
|
||||
|
||||
def pause_generation(self):
|
||||
response = requests.post(f"http://{self.server_host}:{self.server_port}/pause_generation", json={})
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def continue_generation(self):
|
||||
response = requests.post(f"http://{self.server_host}:{self.server_port}/continue_generation", json={})
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def start_profile(
|
||||
self,
|
||||
# The output directory
|
||||
output_dir: str | None = None,
|
||||
# If set, it profile as many as this number of steps.
|
||||
# If it is set, profiling is automatically stopped after this step, and
|
||||
# the caller doesn't need to run stop_profile.
|
||||
start_step: int | None = None,
|
||||
num_steps: int | None = None,
|
||||
activities: list[str] | None = None,
|
||||
profile_by_stage: bool = False,
|
||||
with_stack: bool | None = None,
|
||||
record_shapes: bool | None = None,
|
||||
):
|
||||
response = requests.post(
|
||||
f"http://{self.server_host}:{self.server_port}/start_profile",
|
||||
json={
|
||||
"output_dir": output_dir,
|
||||
"start_step": start_step,
|
||||
"num_steps": num_steps,
|
||||
"activities": activities,
|
||||
"profile_by_stage": profile_by_stage,
|
||||
"with_stack": with_stack,
|
||||
"record_shapes": record_shapes,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def stop_profile(self):
|
||||
response = requests.post(f"http://{self.server_host}:{self.server_port}/stop_profile", json={})
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
|
||||
def _compute_server_args(
|
||||
args,
|
||||
rank,
|
||||
dist_init_addr,
|
||||
nccl_port,
|
||||
host,
|
||||
port,
|
||||
worker_type: str = "regular",
|
||||
disaggregation_bootstrap_port: int | None = None,
|
||||
):
|
||||
nnodes = max(1, args.rollout_num_gpus_per_engine // args.num_gpus_per_node)
|
||||
node_rank = rank % nnodes
|
||||
kwargs = {
|
||||
"model_path": args.hf_checkpoint,
|
||||
"trust_remote_code": True,
|
||||
"random_seed": args.seed + rank,
|
||||
# memory
|
||||
"enable_memory_saver": args.offload_rollout,
|
||||
# distributed
|
||||
"host": host,
|
||||
"port": port,
|
||||
"nccl_port": nccl_port,
|
||||
"nnodes": nnodes,
|
||||
"node_rank": node_rank,
|
||||
"dist_init_addr": dist_init_addr,
|
||||
"gpu_id_step": 1,
|
||||
"base_gpu_id": get_base_gpu_id(args, rank),
|
||||
# parallel
|
||||
"tp_size": args.rollout_num_gpus_per_engine,
|
||||
"dp_size": args.sglang_dp_size,
|
||||
"pp_size": args.sglang_pp_size,
|
||||
"ep_size": args.sglang_ep_size,
|
||||
# always skip warmup to prevent warmup timeout.
|
||||
"skip_server_warmup": True,
|
||||
}
|
||||
|
||||
if worker_type == "prefill":
|
||||
kwargs["disaggregation_mode"] = "prefill"
|
||||
kwargs["load_balance_method"] = "round_robin"
|
||||
assert (
|
||||
disaggregation_bootstrap_port is not None
|
||||
), "disaggregation_bootstrap_port must be set for prefill worker"
|
||||
kwargs["disaggregation_bootstrap_port"] = disaggregation_bootstrap_port
|
||||
elif worker_type == "decode":
|
||||
kwargs["disaggregation_mode"] = "decode"
|
||||
kwargs["prefill_round_robin_balance"] = True
|
||||
|
||||
if args.use_rollout_routing_replay:
|
||||
kwargs["enable_return_routed_experts"] = True
|
||||
if args.fp16:
|
||||
kwargs["dtype"] = "float16"
|
||||
external_engine_need_check_fields = [k for k in kwargs.keys() if k not in _EXTERNAL_ENGINE_SKIP_CHECK_FIELDS]
|
||||
|
||||
unused_keys = set(kwargs.keys())
|
||||
for attr in dataclasses.fields(ServerArgs):
|
||||
if hasattr(args, f"sglang_{attr.name}") and attr.name not in kwargs:
|
||||
kwargs[attr.name] = getattr(args, f"sglang_{attr.name}")
|
||||
unused_keys.discard(attr.name)
|
||||
|
||||
# for compatibility with old args
|
||||
if len(unused_keys) > 0:
|
||||
logger.info(f"Warning: The following arguments is not supported in the current sglang: {unused_keys}.")
|
||||
for key in unused_keys:
|
||||
kwargs.pop(key)
|
||||
|
||||
return kwargs, external_engine_need_check_fields
|
||||
|
||||
|
||||
_EXTERNAL_ENGINE_SKIP_CHECK_FIELDS = [
|
||||
"model_path",
|
||||
"trust_remote_code",
|
||||
"random_seed",
|
||||
"nccl_port",
|
||||
"dist_init_addr",
|
||||
"skip_server_warmup",
|
||||
]
|
||||
Reference in New Issue
Block a user