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
64 changes: 59 additions & 5 deletions megatron/core/distributed/finalize_model_grads.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
from megatron.core.process_groups_config import ProcessGroupCollection

from .. import parallel_state
from ..transformer.moe.moe_utils import get_updated_expert_bias
from ..transformer.moe.moe_utils import (
get_updated_expert_bias,
get_updated_expert_bias_with_quantile,
)
from ..transformer.transformer_config import TransformerConfig
from ..utils import (
get_attr_wrapped_model,
Expand Down Expand Up @@ -325,6 +328,8 @@ def reset_model_temporary_tensors(config: TransformerConfig, model: List[torch.n
and module.expert_bias is not None
):
module.local_tokens_per_expert.zero_()
if getattr(module, 'qb_histogram', None) is not None:
module.qb_histogram.zero_()
if (
config.moe_router_load_balancing_type == "global_aux_loss"
or "global_aux_loss" in config.moe_router_load_balancing_type
Expand Down Expand Up @@ -373,6 +378,44 @@ def _update_router_expert_bias(
expert_bias.copy_(updated_expert_bias)


def _update_router_expert_bias_with_quantile(
model: List[torch.nn.Module],
config: TransformerConfig,
tp_dp_cp_group: Optional[torch.distributed.ProcessGroup] = None,
):
"""Pool K3 histograms and update every local router once per global batch."""
expert_bias_list = []
qb_histogram_list = []
qb_bin_bounds_list = []
for model_chunk in model:
for module in get_attr_wrapped_model(model_chunk, 'modules')():
if (
getattr(module, 'qb_histogram', None) is not None
and module.training
and not getattr(module, 'frozen_expert_bias', False)
):
expert_bias_list.append(module.expert_bias)
qb_histogram_list.append(module.qb_histogram)
qb_bin_bounds_list.append(module.qb_bin_bounds)

if not expert_bias_list:
return

stacked_bias = torch.stack(expert_bias_list, dim=0)
stacked_histogram = torch.stack(qb_histogram_list, dim=0)
stacked_bin_bounds = torch.stack(qb_bin_bounds_list, dim=0)
if get_pg_size(tp_dp_cp_group) > 1:
torch.distributed.all_reduce(stacked_histogram, group=tp_dp_cp_group)
updated_bias, updated_bin_bounds = get_updated_expert_bias_with_quantile(
stacked_histogram, stacked_bin_bounds, stacked_bias, config.moe_router_topk
)
for bias, bounds, next_bias, next_bounds in zip(
expert_bias_list, qb_bin_bounds_list, updated_bias, updated_bin_bounds
):
bias.copy_(next_bias)
bounds.copy_(next_bounds)


def _allreduce_non_tensor_model_parallel_grads(
model: List[torch.nn.Module],
config: TransformerConfig,
Expand Down Expand Up @@ -483,10 +526,13 @@ def finalize_model_grads(
"If you don't need pos_embd_group, you need to explicitly set it to None."
)
assert hasattr(pg_collection, 'dp_cp')
if config.moe_router_enable_expert_bias:
assert hasattr(pg_collection, 'tp_dp_cp') and pg_collection.tp_dp_cp is not None, (
"pg_collection must have tp_dp_cp when " "moe_router_enable_expert_bias is enabled."
)
if (
config.moe_router_enable_expert_bias
or config.moe_router_load_balancing_type == "quantile_balancing"
):
assert (
hasattr(pg_collection, 'tp_dp_cp') and pg_collection.tp_dp_cp is not None
), "pg_collection must have tp_dp_cp when router bias updates are enabled."
tp_dp_cp_group = pg_collection.tp_dp_cp
tp_group = pg_collection.tp
pp_group = pg_collection.pp
Expand Down Expand Up @@ -547,6 +593,14 @@ def finalize_model_grads(
)
_update_router_expert_bias(model, config, tp_dp_cp_group=tp_dp_cp_group)

if config.moe_router_load_balancing_type == "quantile_balancing":
if pg_collection is None:
# Legacy compatibility; modern callers provide pg_collection.tp_dp_cp above.
tp_dp_cp_group = parallel_state.get_tensor_and_data_parallel_group(
Comment thread
FDecaYed marked this conversation as resolved.
with_context_parallel=True
)
_update_router_expert_bias_with_quantile(model, config, tp_dp_cp_group=tp_dp_cp_group)

reset_model_temporary_tensors(config, model)

# normalize gradients for per-token loss normalization.
Expand Down
12 changes: 12 additions & 0 deletions megatron/core/extensions/transformer_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3420,10 +3420,22 @@ def te_general_gemm(
fused_topk_with_score_function,
)

try:
_fused_topk_sig = inspect.signature(fused_topk_with_score_function)
fused_topk_with_score_function_supports_qb = {
"qb_histogram",
"qb_bin_bounds",
"qb_histogram_mode",
}.issubset(_fused_topk_sig.parameters)
del _fused_topk_sig
except (TypeError, ValueError):
fused_topk_with_score_function_supports_qb = False

else:
fused_topk_with_score_function = None
fused_compute_score_for_moe_aux_loss = None
fused_moe_aux_loss = None
fused_topk_with_score_function_supports_qb = False


def set_save_original_input(module):
Expand Down
5 changes: 4 additions & 1 deletion megatron/core/transformer/moe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ Routers determine which expert(s) handle each token. A lightweight MLP scores ev
| **seq_aux_loss** | Sequence-level auxiliary loss for balancing expert usage on each sequence| `--moe-router-load-balancing-type seq_aux_loss` |
| **global_aux_loss** | Global auxiliary loss for balancing expert usage on a global batch across all ranks | `--moe-router-load-balancing-type global_aux_loss` |
| **sinkhorn** | Optimal transport formulation for balancing expert usage | `--moe-router-load-balancing-type sinkhorn` |
| **quantile_balancing** | Kimi K3 aux-loss-free global-batch histogram quantile bias updates | `--moe-router-load-balancing-type quantile_balancing --moe-router-score-function sigmoid --moe-aux-loss-coeff 0` |
| **aux loss free** | Dynamic bias-based load balancing strategy without auxiliary loss | `--moe-router-enable-expert-bias --moe-router-bias-update-rate 1e-3`|
| **none** | No load balancing | `--moe-router-load-balancing-type none` |

Expand Down Expand Up @@ -528,7 +529,9 @@ For MoE models, certain configurations may prevent CUDA Graph capture of MoE lay
### Router Arguments
| Argument | Description | Default |
|----------|-------------|---------|
| --moe-router-load-balancing-type | Load balancing: aux_loss, sinkhorn, seq_aux_loss, none | aux_loss |
| --moe-router-load-balancing-type | Load balancing: aux_loss, seq_aux_loss, global_aux_loss, sinkhorn, quantile_balancing, none | aux_loss |
| --moe-router-quantile-balancing-estimation-scope | Quantile population; dev supports global_batch | global_batch |
| --moe-router-qb-num-bins | Uniform histogram bins per expert for global-batch quantile balancing | 1000 |
| --moe-router-topk | Number of experts per token | 2 |
| --moe-router-score-function | Score function: softmax, sigmoid | softmax |
| --moe-router-pre-softmax | Softmax before top-k | False |
Expand Down
143 changes: 132 additions & 11 deletions megatron/core/transformer/moe/moe_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
fused_sort_chunks_by_index,
fused_sort_chunks_by_index_with_probs,
fused_topk_with_score_function,
fused_topk_with_score_function_supports_qb,
fused_unpermute,
te_general_gemm,
)
Expand All @@ -53,6 +54,7 @@
fused_unpermute,
te_general_gemm,
) = (None, None, None, None, None, None, None, None, None, None)
fused_topk_with_score_function_supports_qb = False


def switch_load_balancing_loss_func(
Expand Down Expand Up @@ -692,6 +694,8 @@ def topk_routing_with_score_function(
fused: bool = False,
router_replay: Optional['RouterReplay'] = None,
dense_output: bool = False,
qb_histogram: Optional[torch.Tensor] = None,
qb_bin_bounds: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute the routing probabilities and map for top-k selection with score function.

Expand All @@ -716,6 +720,10 @@ def topk_routing_with_score_function(
Defaults to None.
dense_output (bool, optional): If True, return dense tensors [num_tokens, topk] instead of
sparse tensors [num_tokens, num_experts]. Defaults to False.
qb_histogram (torch.Tensor, optional): Caller-owned int32 K3 Quantile Balancing histogram
with shape [num_experts, num_bins].
qb_bin_bounds (torch.Tensor, optional): FP32 CUDA tensor containing the lower and upper
K3 Quantile Balancing histogram bounds.

Returns:
Tuple[torch.Tensor, torch.Tensor]:
Expand All @@ -734,6 +742,31 @@ def topk_routing_with_score_function(
"""
assert logits.dim() == 2, f"Expected 2D logits [num_tokens, num_experts], got {logits.dim()}."
num_tokens, num_experts = logits.shape
use_quantile_balancing = qb_histogram is not None or qb_bin_bounds is not None
if use_quantile_balancing and (qb_histogram is None or qb_bin_bounds is None):
raise ValueError("qb_histogram and qb_bin_bounds must be provided together.")
if use_quantile_balancing:
if expert_bias is None:
raise ValueError("Quantile Balancing requires an expert bias.")
if score_function != "sigmoid":
raise ValueError("Quantile Balancing currently requires score_function='sigmoid'.")
if use_pre_softmax:
raise ValueError("Quantile Balancing does not use pre-softmax routing.")
if topk >= num_experts:
raise ValueError("Quantile Balancing requires topk < num_experts.")
if num_groups is not None or group_topk is not None:
raise ValueError("Quantile Balancing does not support group-limited routing.")
if router_replay is not None:
raise ValueError("Quantile Balancing does not support router replay.")
if qb_histogram.dim() != 2 or qb_histogram.shape[0] != num_experts:
raise ValueError(
"qb_histogram must have shape [num_experts, num_bins], got "
f"{tuple(qb_histogram.shape)}."
)
if qb_histogram.dtype != torch.int32:
raise ValueError("qb_histogram must have dtype torch.int32.")
if qb_bin_bounds.shape != (2,) or qb_bin_bounds.dtype != torch.float32:
raise ValueError("qb_bin_bounds must be an FP32 tensor with shape [2].")
if fused:
if not HAVE_TE or fused_topk_with_score_function is None:
raise ValueError(
Expand All @@ -744,16 +777,29 @@ def topk_routing_with_score_function(
"Fused sqrtsoftplus score function requires TE >= 2.13.0. "
"Please upgrade Transformer Engine or disable moe_router_fusion."
)
return fused_topk_with_score_function(
logits=logits,
topk=topk,
use_pre_softmax=use_pre_softmax,
num_groups=num_groups,
group_topk=group_topk,
scaling_factor=scaling_factor,
score_function=score_function,
expert_bias=expert_bias,
)
kwargs = {
"logits": logits,
"topk": topk,
"use_pre_softmax": use_pre_softmax,
"num_groups": num_groups,
"group_topk": group_topk,
"scaling_factor": scaling_factor,
"score_function": score_function,
"expert_bias": expert_bias,
}
if use_quantile_balancing:
if not fused_topk_with_score_function_supports_qb:
raise ValueError(
"The installed Transformer Engine fused router does not expose Quantile "
"Balancing histogram outputs. Upgrade Transformer Engine or disable "
"moe_router_fusion."
)
kwargs.update(
qb_histogram=qb_histogram,
qb_bin_bounds=qb_bin_bounds,
qb_histogram_mode="fused_atomic",
)
return fused_topk_with_score_function(**kwargs)

def _compute_topk(
scores: torch.Tensor,
Expand Down Expand Up @@ -815,7 +861,27 @@ def compute_topk(scores, topk, num_groups=None, group_topk=None):
scores = torch.nn.functional.softplus(logits.float()).sqrt()
if expert_bias is not None:
scores_for_routing = scores + expert_bias.float()
_, top_indices = compute_topk(scores_for_routing, topk, num_groups, group_topk)
if use_quantile_balancing:
topk_result = torch.topk(scores_for_routing, topk + 1, dim=1, sorted=True)
cutoff = topk_result.values[:, -1:]
top_indices = topk_result.indices[:, :-1]
with torch.no_grad():
num_bins = qb_histogram.shape[1]
lower, upper = qb_bin_bounds.unbind()
bin_indices = torch.floor(
(cutoff - scores.detach() - lower) * (num_bins / (upper - lower))
).to(torch.int64)
bin_indices.clamp_(0, num_bins - 1)
expert_offsets = (
torch.arange(num_experts, device=logits.device, dtype=torch.int64)
* num_bins
)
flat_indices = (bin_indices + expert_offsets).reshape(-1)
qb_histogram.view(-1).scatter_add_(
0, flat_indices, torch.ones_like(flat_indices, dtype=torch.int32)
)
else:
_, top_indices = compute_topk(scores_for_routing, topk, num_groups, group_topk)
scores = torch.gather(scores, dim=1, index=top_indices)
else:
scores, top_indices = compute_topk(scores, topk, num_groups, group_topk)
Expand Down Expand Up @@ -1196,6 +1262,61 @@ def get_updated_expert_bias(
return updated_expert_bias


def get_updated_expert_bias_with_quantile(
histogram: torch.Tensor, bin_bounds: torch.Tensor, expert_bias: torch.Tensor, topk: int
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Recover Kimi K3 Quantile Balancing biases from a pooled global-batch histogram."""
if histogram.dim() < 2:
raise ValueError("QB histogram must have shape [..., num_experts, num_bins].")
num_experts, num_bins = histogram.shape[-2:]
if num_experts <= 0 or num_bins <= 0:
raise ValueError("QB histogram dimensions must be positive.")
if expert_bias.shape != histogram.shape[:-1]:
raise ValueError(
f"QB expert_bias shape {expert_bias.shape} does not match expected "
f"{histogram.shape[:-1]} for histogram shape {histogram.shape}."
)
if bin_bounds.shape != histogram.shape[:-2] + (2,):
raise ValueError(
f"QB bin_bounds shape {bin_bounds.shape} does not match expected "
f"{histogram.shape[:-2] + (2,)} for histogram shape {histogram.shape}."
)
Comment thread
harryzhou2000 marked this conversation as resolved.
if not 0 < topk < num_experts:
raise ValueError(f"QB topk must be in [1, {num_experts}), got {topk}.")

with torch.no_grad():
cumulative_counts = torch.cumsum(histogram, dim=-1, dtype=torch.int64)
tokens_per_expert = cumulative_counts[..., -1]
# Every token contributes one margin sample to every expert, so all expert totals match.
total_tokens = tokens_per_expert[..., 0]
target_quantile = total_tokens.to(torch.float32) * (topk / num_experts)
target_rank = torch.ceil(target_quantile).to(torch.int64).clamp_min_(1)
selected_bins = (
(cumulative_counts >= target_rank[..., None, None]).to(torch.int64).argmax(dim=-1)
)
selected_counts = torch.gather(histogram, -1, selected_bins.unsqueeze(-1)).squeeze(-1)
previous_bins = (selected_bins - 1).clamp_min(0)
counts_before = torch.gather(cumulative_counts, -1, previous_bins.unsqueeze(-1)).squeeze(-1)
counts_before = torch.where(
selected_bins == 0, torch.zeros_like(counts_before), counts_before
)
interpolation = (
(target_quantile[..., None] - counts_before.to(torch.float32))
/ selected_counts.clamp_min(1).to(torch.float32)
).clamp_(0.0, 1.0)
lower = bin_bounds[..., 0, None]
upper = bin_bounds[..., 1, None]
bin_width = (upper - lower) / num_bins
updated_expert_bias = lower + (selected_bins.to(torch.float32) + interpolation) * bin_width
updated_expert_bias -= updated_expert_bias.mean(dim=-1, keepdim=True)
has_tokens = total_tokens > 0
updated_expert_bias = torch.where(has_tokens[..., None], updated_expert_bias, expert_bias)
bias_min, bias_max = torch.aminmax(updated_expert_bias, dim=-1)
updated_bin_bounds = torch.stack((bias_min - 1.0, bias_max + 1.0), dim=-1)
updated_bin_bounds = torch.where(has_tokens[..., None], updated_bin_bounds, bin_bounds)
return updated_expert_bias, updated_bin_bounds


def maybe_move_tensor_to_cpu(
tensor: torch.Tensor, as_numpy: bool = False, record_stream: bool = False
) -> torch.Tensor:
Expand Down
13 changes: 13 additions & 0 deletions megatron/core/transformer/moe/paged_stash.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,14 @@ def _set_moe_paged_stash_all(self, value: bool) -> None:
for c in self._configs_to_sync_moe_paged_stash:
c.moe_paged_stash = value

def _reset_qb_histograms(self) -> None:
"""Discard histogram observations from a failed full-iteration attempt."""
for mlp in self.moe_layers:
router = getattr(mlp, 'router', None)
qb_histogram = getattr(router, 'qb_histogram', None)
if qb_histogram is not None:
qb_histogram.zero_()

def data_read(self, data_iterator, model, training, num_microbatches):
"""Read all microbatch inputs from Dataloader and copy to static buffers."""
data_iterator_saved = []
Expand Down Expand Up @@ -1105,6 +1113,11 @@ def prepare_for_rerun(self, is_training=True):
self.stash_manager.host_spill.zero_()
self._set_moe_paged_stash_all(False)

# The dropless retry replays every microbatch. Preserve CUDA addresses while
# discarding the failed attempt so each token contributes exactly once.
if is_training:
self._reset_qb_histograms()

# Set grad to zero.
for model_chunk in self.model:
model_chunk.zero_grad_buffer()
Expand Down
Loading
Loading