Skip to content
Merged
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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to confirm these changes are stable, can you run a basic gsm8k example before and after this PR, and link it here? Just want to check there is no significant speed regression from chunking entropy calculation, and that the entropy metric is exactly matching before

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a unit test on this part: tests/backends/skyrl_train/distributed/test_vocab_entropy_chunking.py, this verifies that the chunked CE is numerically identical. For the overhead, the following table shows:

chunk_memory_mb tokens/chunk #chunks exact op time vs unchunked
off (unchunked) 6.3 ms 1.0×
4096 4096 2 6.4 ms 1.0×
2048 2048 4 7.5 ms 1.2×
1024 1024 8 14.9 ms 2.4×
512 (current default) 512 16 29.7 ms 4.7×
256 256 32 59.6 ms 9.4×
128 128 64 118.6 ms 18.8×

The slowdown is expected, but since the overall step time here is at most hundreds of ms, the impact of chunking is almost negligible.

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 @@ -325,6 +343,9 @@ def collection_func(logits, data):

def forward_step(batch_iter, model):
batch = next(batch_iter)
# Microbatches are held on CPU and transferred just before their forward
# step to cap resident input memory (no-op if already on device).
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 +814,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 +918,9 @@ 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)
# Microbatches are held on CPU and transferred just before their forward
# step to cap resident input memory (no-op if already on device).
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
Original file line number Diff line number Diff line change
Expand Up @@ -643,15 +643,13 @@ 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)
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 @@ -1019,8 +1017,6 @@ 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())

# Build micro-batch dicts expected by forward_backward_mini_batch
micro_buffer = []
Expand Down Expand Up @@ -1129,8 +1125,6 @@ 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())

use_token_batching = self.cfg.max_tokens_per_microbatch > 0

Expand Down
17 changes: 15 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,7 @@ def __init__(
self.colocate_all = colocate_all
self.sequence_parallel_size = sequence_parallel_size
self.record_memory = record_memory
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 +680,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 +697,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