Files
modelscope--ms-swift/swift/megatron/trainers/vocab_parallel_utils.py
T
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

237 lines
9.6 KiB
Python

# Copyright (c) ModelScope Contributors. All rights reserved.
"""Vocabulary-parallel utilities for Tensor Parallelism.
This module provides utilities for computing log_softmax, entropy, KL divergence,
and other operations across vocab-parallel sharded tensors in Tensor Parallelism (TP).
When using TP, the vocabulary dimension is sharded across TP ranks. These utilities
correctly handle the distributed computation by:
1. Finding global max via all_reduce (for numerical stability)
2. Computing sum of exp via all_reduce (for normalization)
3. All-reducing partial sums for final results
"""
import torch
from megatron.core import mpu
from typing import Optional, Tuple
def vocab_parallel_log_softmax(logits: torch.Tensor) -> torch.Tensor:
"""Compute log_softmax across vocab-parallel sharded logits.
When using Tensor Parallelism, vocab is sharded across TP ranks.
This function correctly computes log_softmax by:
1. Finding global max via all_reduce
2. Computing sum of exp via all_reduce
3. Computing log_softmax using the global statistics
Args:
logits: Logits tensor [..., partition_vocab_size]
Returns:
log_softmax tensor [..., partition_vocab_size]
"""
tp_size = mpu.get_tensor_model_parallel_world_size()
if tp_size == 1:
return torch.nn.functional.log_softmax(logits, dim=-1)
tp_group = mpu.get_tensor_model_parallel_group()
# Step 1: Find global max for numerical stability
logits_max = logits.max(dim=-1, keepdim=True)[0]
torch.distributed.all_reduce(logits_max, op=torch.distributed.ReduceOp.MAX, group=tp_group)
# Step 2: Compute exp(logits - max) and sum across all TP ranks
exp_logits = torch.exp(logits - logits_max)
sum_exp = exp_logits.sum(dim=-1, keepdim=True)
torch.distributed.all_reduce(sum_exp, op=torch.distributed.ReduceOp.SUM, group=tp_group)
# Step 3: Compute log_softmax
log_softmax = logits - logits_max - torch.log(sum_exp)
return log_softmax
def vocab_parallel_entropy(log_probs: torch.Tensor, chunk_size: int = 512) -> torch.Tensor:
"""Compute entropy from pre-computed vocab-parallel sharded log probabilities.
When using Tensor Parallelism, vocab is sharded across TP ranks.
This function correctly computes entropy by:
1. Computing partial entropy = -sum(exp(log_p) * log_p) on each rank's partition
2. All-reducing the partial entropies to get the global sum.
Entropy is computed in chunks to reduce memory usage.
Args:
log_probs: Pre-computed log probabilities tensor [..., partition_vocab_size]
chunk_size: Number of tokens to process per chunk (default: 512)
Returns:
Entropy tensor [...] (scalar per position)
"""
tp_group = mpu.get_tensor_model_parallel_group()
tp_size = mpu.get_tensor_model_parallel_world_size()
# Flatten all but the last dimension for chunked processing
original_shape = log_probs.shape[:-1]
vocab_size = log_probs.shape[-1]
log_probs_flat = log_probs.view(-1, vocab_size) # [total_tokens, partition_vocab_size]
total_tokens = log_probs_flat.shape[0]
entropies_list = []
for start_idx in range(0, total_tokens, chunk_size):
end_idx = min(start_idx + chunk_size, total_tokens)
log_probs_chunk = log_probs_flat[start_idx:end_idx] # [chunk_size, partition_vocab_size]
# Compute partial entropy on this rank's vocab partition
# entropy = -sum(p * log_p) = -sum(exp(log_p) * log_p)
probs = torch.exp(log_probs_chunk)
partial_entropy = -(probs * log_probs_chunk).sum(dim=-1) # [chunk_size]
# All-reduce to get global entropy if using TP
if tp_size > 1:
torch.distributed.all_reduce(partial_entropy, op=torch.distributed.ReduceOp.SUM, group=tp_group)
entropies_list.append(partial_entropy)
# Concatenate all chunks and reshape back
entropies = torch.cat(entropies_list, dim=0)
entropies = entropies.view(original_shape)
return entropies
def vocab_parallel_kl_div(input_log_probs: torch.Tensor, target_log_probs: torch.Tensor) -> torch.Tensor:
"""Compute KL divergence for vocab-parallel sharded log probabilities.
KL(target || input) = sum(target_prob * (target_log_prob - input_log_prob))
= sum(exp(target_log_prob) * (target_log_prob - input_log_prob))
Since both log_probs are sharded across TP, we compute the partial sum
on each rank and then all_reduce to get the global sum.
Args:
input_log_probs: Input log probabilities [..., partition_vocab_size]
target_log_probs: Target log probabilities [..., partition_vocab_size]
Returns:
KL divergence per position [...], already reduced across TP
"""
tp_group = mpu.get_tensor_model_parallel_group()
# Compute partial KL on this rank's vocab partition
target_probs = torch.exp(target_log_probs)
partial_kl = (target_probs * (target_log_probs - input_log_probs)).sum(dim=-1)
if mpu.get_tensor_model_parallel_world_size() > 1:
tp_group = mpu.get_tensor_model_parallel_group()
torch.distributed.all_reduce(partial_kl, op=torch.distributed.ReduceOp.SUM, group=tp_group)
return partial_kl
def vocab_parallel_gather_logps(
logits: torch.Tensor,
labels: torch.Tensor,
log_probs: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Gather log probabilities for target labels from vocab-parallel logits.
When using TP, each rank only has a partition of the vocabulary. This function:
1. Computes log_softmax with vocab-parallel support (if log_probs not provided)
2. Gathers log probs for target tokens
3. All-reduces to combine results from all TP ranks
Args:
logits: Logits tensor [batch, seq, partition_vocab_size]
labels: Token labels [batch, seq], -100 for masked positions
log_probs: Pre-computed log_softmax (optional, to avoid recomputation)
Returns:
per_token_logps: [batch, seq] log probabilities for target tokens
"""
tp_size = mpu.get_tensor_model_parallel_world_size()
tp_rank = mpu.get_tensor_model_parallel_rank()
# Compute log_probs if not provided
if log_probs is None:
log_probs = vocab_parallel_log_softmax(logits)
# Get the local vocab range for this TP rank
partition_vocab_size = log_probs.shape[-1]
if tp_size > 1:
vocab_start = tp_rank * partition_vocab_size
vocab_end = vocab_start + partition_vocab_size
# Check which labels fall within this TP rank's vocab partition
local_labels = labels - vocab_start
# Mask for labels within local vocab range
in_range_mask = (labels >= vocab_start) & (labels < vocab_end)
# Clamp local_labels to valid range for gather
local_labels = local_labels.clamp(min=0, max=partition_vocab_size - 1)
else:
local_labels = labels
in_range_mask = torch.ones_like(labels, dtype=torch.bool)
local_labels = local_labels.clamp(min=0)
# Gather log probs for target tokens
gathered_logps = torch.gather(log_probs, dim=-1, index=local_labels.unsqueeze(-1)).squeeze(-1)
# For TP: only the rank that owns the target token has the correct log prob
# Other ranks have incorrect values, so we need to zero them out
if tp_size > 1:
gathered_logps = gathered_logps * in_range_mask.float()
# All-reduce to sum contributions from all ranks
# (only one rank has non-zero value for each token)
torch.distributed.all_reduce(
gathered_logps, op=torch.distributed.ReduceOp.SUM, group=mpu.get_tensor_model_parallel_group())
# Apply loss mask (labels == -100 are masked)
loss_mask = labels != -100
per_token_logps = gathered_logps * loss_mask.float()
return per_token_logps
def compute_logps_and_entropy_from_logits(
logits: torch.Tensor,
labels: torch.Tensor,
compute_entropy: bool = False,
entropy_chunk_size: int = 512,
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
"""Compute per-token log probabilities and optionally entropy from logits.
This is a unified method that efficiently computes both logps and entropy
in a single pass when both are needed, sharing the log_softmax computation.
Note: In Megatron, labels are already shifted (via torch.roll in get_batch_on_this_tp_rank),
so logits and labels are already aligned. No additional shift is needed here.
Temperature scaling should be applied by the caller before invoking this function,
so that this function remains a pure computation without side effects on the input.
Args:
logits: Logits tensor [batch, seq, partition_vocab_size] or [1, total_tokens, partition_vocab_size].
Should be pre-scaled by temperature if needed.
labels: Token labels [batch, seq] or [1, total_tokens], -100 for masked positions
compute_entropy: Whether to compute entropy (default: False)
entropy_chunk_size: Chunk size for entropy computation (default: 512)
Returns:
Tuple of:
- per_token_logps: [batch, seq] or [1, total_tokens] log probabilities for target tokens
- per_token_entropy: Same shape as per_token_logps, or None if compute_entropy=False
"""
# Compute log_softmax (shared for both logps and entropy)
log_probs = vocab_parallel_log_softmax(logits)
# Gather logps for target tokens
per_token_logps = vocab_parallel_gather_logps(logits, labels, log_probs=log_probs)
# Compute entropy if requested (reuse log_probs to avoid redundant computation)
per_token_entropy = None
if compute_entropy:
per_token_entropy = vocab_parallel_entropy(log_probs, chunk_size=entropy_chunk_size)
return per_token_logps, per_token_entropy