Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions skyrl/backends/skyrl_train/distributed/megatron/megatron_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import gc
from typing import Any, Dict, List, Optional, Union

import torch
Expand Down Expand Up @@ -210,8 +209,6 @@ def offload_megatron_grads_to_cpu(models):
for _, param in model_chunk.named_parameters():
if param.grad is not None:
param.grad = param.grad.to("cpu", non_blocking=True)
gc.collect()
torch.cuda.empty_cache()


@torch.no_grad()
Expand All @@ -223,8 +220,6 @@ def load_megatron_grads_to_gpu(models):
for _, param in model_chunk.named_parameters():
if param.grad is not None:
param.grad = param.grad.to(torch.cuda.current_device(), non_blocking=True)
gc.collect()
torch.cuda.empty_cache()


@torch.no_grad()
Expand Down Expand Up @@ -259,8 +254,6 @@ def offload_megatron_model_to_cpu(models):
else:
for _, param in model_chunk.named_parameters():
param.data = param.data.to("cpu", non_blocking=True)
gc.collect()
torch.cuda.empty_cache()


@torch.no_grad()
Expand All @@ -280,8 +273,6 @@ def load_megatron_model_to_gpu(models):
device_id = torch.cuda.current_device()
for _, param in model_chunk.named_parameters():
param.data = param.data.to(device_id, non_blocking=True)
gc.collect()
torch.cuda.empty_cache()


@torch.no_grad()
Expand Down Expand Up @@ -381,8 +372,6 @@ def _iter_opts(opt):
v["exp_avg"] = v["exp_avg"].to("cpu", non_blocking=True)
if "exp_avg_sq" in v:
v["exp_avg_sq"] = v["exp_avg_sq"].to("cpu", non_blocking=True)
gc.collect()
torch.cuda.empty_cache()


@torch.no_grad()
Expand All @@ -404,8 +393,6 @@ def _iter_opts(opt):
v["exp_avg"] = v["exp_avg"].to(torch.cuda.current_device(), non_blocking=True)
if "exp_avg_sq" in v:
v["exp_avg_sq"] = v["exp_avg_sq"].to(torch.cuda.current_device(), non_blocking=True)
gc.collect()
torch.cuda.empty_cache()


def preprocess_packed_seqs(
Expand Down
99 changes: 92 additions & 7 deletions skyrl/backends/skyrl_train/distributed/megatron/model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# limitations under the License.

import warnings
from math import prod
from typing import Any, Optional

import megatron.core.parallel_state as mpu
Expand Down Expand Up @@ -1001,6 +1002,8 @@ def vocab_parallel_entropy_packed_sequences(
loss_mask: Optional[torch.Tensor],
cp_group: Optional[torch.distributed.ProcessGroup],
sub_seq_lengths: Optional[list[list[int]]] = None,
chunk_size: Optional[int] = None,
chunk_memory_mb: int = 512,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Compute action-token entropy directly on TP+CP sharded packed logits.

Expand All @@ -1009,9 +1012,8 @@ def vocab_parallel_entropy_packed_sequences(
local term is normalized by the global action-token count. Megatron's
schedule already applies the CP loss scale for two-output loss funcs.
"""
entropy_tokens = vocab_parallel_entropy(vocab_parallel_logits).squeeze(0)
device = entropy_tokens.device
dtype = entropy_tokens.dtype
device = vocab_parallel_logits.device
dtype = vocab_parallel_logits.dtype

attention_mask = attention_mask.to(device=device, dtype=torch.bool)
cu_seqlens_padded = cu_seqlens_padded.to(device=device, dtype=torch.long)
Expand Down Expand Up @@ -1055,13 +1057,18 @@ def vocab_parallel_entropy_packed_sequences(
cp_rank_for_token, local_indices = _packed_cp_rank_and_local_indices(
cu_seqlens_padded, seq_indices, seq_offsets, seq_lens_padded, cp_size
)
local_weights = torch.zeros_like(entropy_tokens)
local_weights = torch.zeros((int(vocab_parallel_logits.shape[-2]),), dtype=dtype, device=device)
current_rank_mask = cp_rank_for_token == cp_rank
local_weights[local_indices[current_rank_mask]] = packed_weights[current_rank_mask]
else:
local_weights = packed_weights

local_entropy_sum = (entropy_tokens * local_weights).sum()
local_entropy_sum = vocab_parallel_entropy_weighted_sum(
vocab_parallel_logits,
local_weights,
chunk_size=chunk_size,
chunk_memory_mb=chunk_memory_mb,
)
local_count = local_weights.sum()
global_count = local_count.detach().clone()
global_entropy_sum = local_entropy_sum.detach().clone()
Expand Down Expand Up @@ -1333,7 +1340,51 @@ def backward(ctx, grad_output: torch.Tensor) -> torch.Tensor:
return softmax_logits


def vocab_parallel_entropy(vocab_parallel_logits: torch.Tensor) -> torch.Tensor:
def _floor_power_of_two(value: int) -> int:
return 1 << (value.bit_length() - 1)


def _resolve_vocab_entropy_chunk_size(
vocab_parallel_logits: torch.Tensor,
chunk_size: Optional[int],
chunk_memory_mb: int,
peak_factor: int = 4,
) -> Optional[int]:
"""Resolve the sequence chunk size for vocab entropy.

``None`` disables chunking. ``0`` uses the runtime vocab shard and dtype to
choose a size. Positive values specify the number of tokens per chunk.
"""
if chunk_size is None:
return None
if chunk_size < 0:
raise ValueError(f"chunk_size must be non-negative or None, got {chunk_size}")
if chunk_memory_mb <= 0:
raise ValueError(f"chunk_memory_mb must be positive, got {chunk_memory_mb}")
seq_len = int(vocab_parallel_logits.shape[-2])
if seq_len <= 0:
return None
if chunk_size > 0:
return chunk_size if chunk_size < seq_len else None

budget_bytes = int(chunk_memory_mb) * 1024 * 1024
leading_elements = prod(vocab_parallel_logits.shape[:-2])
bytes_per_token = (
leading_elements * int(vocab_parallel_logits.shape[-1]) * vocab_parallel_logits.element_size() * peak_factor
)
if bytes_per_token <= 0:
return None

auto_chunk = max(1, min(seq_len, budget_bytes // bytes_per_token))
auto_chunk = _floor_power_of_two(auto_chunk)
return auto_chunk if auto_chunk < seq_len else None


def vocab_parallel_entropy(
vocab_parallel_logits: torch.Tensor,
chunk_size: Optional[int] = None,
chunk_memory_mb: int = 512,
) -> torch.Tensor:
"""Compute entropy when the logits are sharded in tp ranks

Args:
Expand All @@ -1342,4 +1393,38 @@ def vocab_parallel_entropy(vocab_parallel_logits: torch.Tensor) -> torch.Tensor:
Returns: (total_nnz,)

"""
return _VocabParallelEntropy.apply(vocab_parallel_logits)
resolved_chunk_size = _resolve_vocab_entropy_chunk_size(vocab_parallel_logits, chunk_size, chunk_memory_mb)
if resolved_chunk_size is None:
return _VocabParallelEntropy.apply(vocab_parallel_logits)

entropy_chunks = []
seq_len = int(vocab_parallel_logits.shape[-2])
for start in range(0, seq_len, resolved_chunk_size):
end = min(start + resolved_chunk_size, seq_len)
entropy_chunks.append(_VocabParallelEntropy.apply(vocab_parallel_logits[..., start:end, :]))
return torch.cat(entropy_chunks, dim=-1)


def vocab_parallel_entropy_weighted_sum(
vocab_parallel_logits: torch.Tensor,
weights: torch.Tensor,
chunk_size: Optional[int] = None,
chunk_memory_mb: int = 512,
) -> torch.Tensor:
"""Compute ``sum(entropy * weights)`` with bounded temporary memory."""
resolved_chunk_size = _resolve_vocab_entropy_chunk_size(vocab_parallel_logits, chunk_size, chunk_memory_mb)
if resolved_chunk_size is None:
entropy_tokens = _VocabParallelEntropy.apply(vocab_parallel_logits)
return (entropy_tokens * weights).sum()

# Keep an autograd edge even when every chunk is masked out.
local_entropy_sum = vocab_parallel_logits[..., :0, :].sum()
seq_len = int(vocab_parallel_logits.shape[-2])
for start in range(0, seq_len, resolved_chunk_size):
end = min(start + resolved_chunk_size, seq_len)
weight_chunk = weights[start:end]
if torch.count_nonzero(weight_chunk).item() == 0:
continue
entropy_chunk = _VocabParallelEntropy.apply(vocab_parallel_logits[..., start:end, :])
local_entropy_sum = local_entropy_sum + (entropy_chunk * weight_chunk).sum()
return local_entropy_sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
shift_mask_for_mtp,
unpadded_vocab_shard_width,
)
from skyrl.backends.skyrl_train.training_batch import TensorList
from skyrl.backends.skyrl_train.utils.ppo_utils import (
PolicyLossRegistry,
compute_approx_kl,
Expand Down Expand Up @@ -125,6 +126,23 @@ def _build_packed_valid_mask(
return mask.unsqueeze(0)


def _copy_tensor_tree_to_device(value: Any, device: int) -> Any:
"""Move all tensors in a nested microbatch to a CUDA device."""
if torch.is_tensor(value) or isinstance(value, TensorList):
return value.to(device=device, non_blocking=True)
if isinstance(value, dict):
return {key: _copy_tensor_tree_to_device(item, device) for key, item in value.items()}
if isinstance(value, list):
return [_copy_tensor_tree_to_device(item, device) for item in value]
if isinstance(value, tuple):
return tuple(_copy_tensor_tree_to_device(item, device) for item in value)
return value


def _copy_tensor_dict_to_device(batch: Dict[str, Any], device: int) -> Dict[str, Any]:
return {key: _copy_tensor_tree_to_device(value, device) for key, value in batch.items()}


def _fused_lm_head_output_processor(**kwargs):
"""GPTModel ``output_processor`` hook for the fused LM-head log-prob path.

Expand Down Expand Up @@ -162,13 +180,15 @@ def __init__(
actor_optimizer: Optional[torch.optim.Optimizer] = None,
policy_loss_fn: Optional[Callable] = None,
is_vlm: bool = False,
cpu_resident_microbatches: bool = False,
):
self.cfg = config
self.actor_module = actor_module
self.actor_optimizer = actor_optimizer
self.policy_loss_fn = policy_loss_fn
self.remove_microbatch_padding = self.cfg.remove_microbatch_padding
self.is_vlm = is_vlm
self.cpu_resident_microbatches = cpu_resident_microbatches
# Fuse the LM-head projection into the chunked log-prob/entropy via the
# GPTModel output_processor hook (avoids materializing the full
# [B, S, vocab//TP] logits + its fp32 grad). See model_utils.
Expand Down Expand Up @@ -325,6 +345,8 @@ def collection_func(logits, data):

def forward_step(batch_iter, model):
batch = next(batch_iter)
if self.cpu_resident_microbatches:
batch = _copy_tensor_dict_to_device(batch, torch.cuda.current_device())

model_config = get_model_config(model)
fp8_enabled = is_fp8_enabled(getattr(model_config, "fp8", None))
Expand Down Expand Up @@ -793,10 +815,16 @@ def loss_func(logits, data):
loss_mask,
mpu.get_context_parallel_group(),
sub_seq_lengths=data.get("sub_seq_lengths_list"),
chunk_size=self.cfg.vocab_entropy_chunk_size,
chunk_memory_mb=self.cfg.vocab_entropy_chunk_memory_mb,
)
else:
action_logits = logits[:, -num_actions - 1 : -1, :]
entropy_BS = vocab_parallel_entropy(action_logits)
entropy_BS = vocab_parallel_entropy(
action_logits,
chunk_size=self.cfg.vocab_entropy_chunk_size,
chunk_memory_mb=self.cfg.vocab_entropy_chunk_memory_mb,
)
entropy = masked_mean(entropy_BS, loss_mask)
entropy_for_loss = entropy

Expand Down Expand Up @@ -891,6 +919,8 @@ def forward_step(batch_iter, model):
# for recover_left_padding and setup_per_microbatch_replay_forward. Especially relevant
# after this PR https://github.com/NovaSky-AI/SkyRL/pull/1285.
batch = next(batch_iter)
if self.cpu_resident_microbatches:
batch = _copy_tensor_dict_to_device(batch, torch.cuda.current_device())

model_config = get_model_config(model)
fp8_enabled = is_fp8_enabled(getattr(model_config, "fp8", None))
Expand Down
13 changes: 7 additions & 6 deletions skyrl/backends/skyrl_train/workers/megatron/megatron_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,15 +643,15 @@ def _forward_logprobs(self, data: TrainingInputBatch) -> torch.Tensor:

# Build micro-batch dicts expected by policy.forward_mini_batch
micro_dicts = []
device = torch.cuda.current_device()

if microbatch_iterator is not None:
micro_batches = microbatch_iterator
else:
micro_batches = data.chunk(self.cfg.micro_forward_batch_size_per_gpu)

for micro in micro_batches:
micro.to(device)
if not self.model.cpu_resident_microbatches:
micro.to(torch.cuda.current_device())
attention_mask = micro["attention_mask"]
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 0)
Expand Down Expand Up @@ -978,6 +978,7 @@ def init_model(self, model_path, num_training_steps: int = 1e9):
actor_optimizer=self.optimizer,
policy_loss_fn=self.policy_loss_fn,
is_vlm=self.is_vlm,
cpu_resident_microbatches=self.cfg.policy.megatron_config.cpu_resident_microbatches,
)

self.empty_cuda_cache = self.cfg.policy.megatron_config.empty_cuda_cache
Expand Down Expand Up @@ -1019,8 +1020,8 @@ def forward(
all_loss_fn_outputs: List[Dict[str, Any]] = []

self._drop_pixel_values_on_non_first_pp_stage(data)
# Move data to GPU
data.to(torch.cuda.current_device())
if not self.cfg.policy.megatron_config.cpu_resident_microbatches:
data.to(torch.cuda.current_device())

# Build micro-batch dicts expected by forward_backward_mini_batch
micro_buffer = []
Expand Down Expand Up @@ -1129,8 +1130,8 @@ def forward_backward(
all_metrics = defaultdict(list)

self._drop_pixel_values_on_non_first_pp_stage(data)
# Move data to GPU
data.to(torch.cuda.current_device())
if not self.cfg.policy.megatron_config.cpu_resident_microbatches:
data.to(torch.cuda.current_device())

use_token_batching = self.cfg.max_tokens_per_microbatch > 0

Expand Down
19 changes: 17 additions & 2 deletions skyrl/backends/skyrl_train/workers/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,9 @@ def init_model(self, *args, **kwargs):
raise NotImplementedError()

def empty_cache(self) -> None:
"""Empty GPU memory cache on Worker's CUDA device"""
torch.cuda.empty_cache()
"""Empty this worker's CUDA allocator cache."""
if torch.cuda.is_available():
torch.cuda.empty_cache()

def _set_expandable_segments(self, enabled: bool) -> None:
"""Toggle PyTorch's CUDA ``expandable_segments`` allocator at runtime.
Expand Down Expand Up @@ -567,6 +568,9 @@ def __init__(
self.colocate_all = colocate_all
self.sequence_parallel_size = sequence_parallel_size
self.record_memory = record_memory
self._pg = pg
self._num_gpus_per_actor = num_gpus_per_actor
self._last_dp_size: Optional[int] = None
self._initiate_actors(pg, num_gpus_per_actor)

def _initiate_actors(self, pg: Optional[ResolvedPlacementGroup], num_gpus_per_actor: float):
Expand Down Expand Up @@ -678,6 +682,8 @@ def _scheduling_strategy_for_rank(rank):
ray.get([actor.init_worker_process_group.remote() for actor in self._actor_handlers])
logger.info("Initialized process group for RayActorGroup")
self.actor_infos = [ActorInfo(actor, ray.get(actor.get_mesh_rank.remote())) for actor in self._actor_handlers]
if self.actor_infos:
self._last_dp_size = self.actor_infos[0].rank.dp_size
logger.info(f"Mesh Ranks: {[actor_info.rank for actor_info in self.actor_infos]}")

def async_init_model(
Expand All @@ -693,6 +699,15 @@ def async_init_model(
"""
return [actor.init_model.remote(*args, **kwargs) for actor in self._actor_handlers]

def get_dp_size(self) -> int:
"""Return the current or last-known data-parallel size for this actor group."""
if self.actor_infos:
self._last_dp_size = self.actor_infos[0].rank.dp_size
return self._last_dp_size
if self._last_dp_size is None:
raise RuntimeError("Cannot determine data-parallel size before actor group initialization.")
return self._last_dp_size

def offload_to_cpu(self, nonblocking=False, offload_optimizer=True, offload_model=True):
"""Offload all worker state to CPU.

Expand Down
Loading
Loading