Files
unslothai--unsloth/unsloth/kernels/moe/tests/moe_utils.py
T
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

478 lines
18 KiB
Python

# SPDX-License-Identifier: GNU Affero General Public License v3.0
# Copyright 2023-present the Unsloth team. All rights reserved.
from dataclasses import dataclass, fields
import torch
import torch.nn as nn
from huggingface_hub import HfApi
from huggingface_hub.utils import _safetensors
from transformers.models.qwen3_moe.configuration_qwen3_moe import Qwen3MoeConfig
from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeSparseMoeBlock
from grouped_gemm.interface import grouped_gemm
from grouped_gemm.kernels.tuning import (
KernelConfigBackward_dW,
KernelConfigBackward_dX,
KernelConfigForward,
)
from grouped_gemm.reference.layers.qwen3_moe import (
GroupedGEMMResult,
Qwen3MoeGroupedGEMMBlock,
)
from grouped_gemm.reference.moe_ops import permute, unpermute
def rebind_experts_to_shared_buffer(moe_block: Qwen3MoeSparseMoeBlock, config: Qwen3MoeConfig):
num_experts = config.num_experts
hidden_size = config.hidden_size
interm_size = config.moe_intermediate_size
device = moe_block.experts[0].down_proj.weight.device
dtype = moe_block.experts[0].down_proj.weight.dtype
buffer_up = torch.empty(num_experts, interm_size, hidden_size, device = device, dtype = dtype)
buffer_gate = torch.empty(num_experts, interm_size, hidden_size, device = device, dtype = dtype)
buffer_down = torch.empty(num_experts, hidden_size, interm_size, device = device, dtype = dtype)
# Copy existing expert weights into buffers
for i, expert in enumerate(moe_block.experts):
buffer_up[i].copy_(expert.up_proj.weight.data)
buffer_gate[i].copy_(expert.gate_proj.weight.data)
buffer_down[i].copy_(expert.down_proj.weight.data)
# Rebind expert weights to views in the shared buffer
for i, expert in enumerate(moe_block.experts):
expert.up_proj.weight = torch.nn.Parameter(buffer_up[i])
expert.gate_proj.weight = torch.nn.Parameter(buffer_gate[i])
expert.down_proj.weight = torch.nn.Parameter(buffer_down[i])
return buffer_up, buffer_gate, buffer_down
def get_expert_metadata(model_id: str):
api = HfApi()
metadata: _safetensors.SafetensorsRepoMetadata = api.get_safetensors_metadata(model_id)
return metadata.files_metadata
def clone_experts(
moe_block: Qwen3MoeSparseMoeBlock,
config: Qwen3MoeConfig,
copy: bool = True,
):
down_projs = torch.empty(config.num_experts, config.hidden_size, config.moe_intermediate_size)
up_projs = torch.empty(config.num_experts, config.moe_intermediate_size, config.hidden_size)
gate_projs = torch.empty(config.num_experts, config.moe_intermediate_size, config.hidden_size)
for expert_idx, expert in enumerate(moe_block.experts):
down_projs[expert_idx].copy_(expert.down_proj.weight.data)
up_projs[expert_idx].copy_(expert.up_proj.weight.data)
gate_projs[expert_idx].copy_(expert.gate_proj.weight.data)
return gate_projs, up_projs, down_projs
@dataclass
class ForwardResult:
output: torch.Tensor
router_logits: torch.Tensor
X: torch.Tensor
# When using grouped gemm MoE implementation to additional debugging / checking of intermediate results
grouped_gemm_result: GroupedGEMMResult = None
@dataclass
class BackwardResult:
X_grad: torch.Tensor
gate_grad: torch.Tensor
gate_proj_grad: torch.Tensor
up_proj_grad: torch.Tensor
down_proj_grad: torch.Tensor
def check_down_proj_grad(
moe_block: Qwen3MoeSparseMoeBlock,
grouped_gemm_block: Qwen3MoeGroupedGEMMBlock,
atol: float,
rtol: float,
):
for i, expert in enumerate(moe_block.experts):
ref_grad = expert.down_proj.weight.grad
assert ref_grad is not None
test_grad = grouped_gemm_block.down_proj.grad[i]
assert test_grad is not None
diff = (ref_grad - test_grad).abs().max()
if not torch.allclose(ref_grad, test_grad, atol = atol, rtol = rtol):
print(f"expert {i} down_proj_grad_diff: {diff.detach().cpu().item():.6f}")
def check_gate_up_proj_grad(
moe_block: Qwen3MoeSparseMoeBlock,
grouped_gemm_block: Qwen3MoeGroupedGEMMBlock,
atol: float,
rtol: float,
):
moe_intermediate_size = grouped_gemm_block.moe_intermediate_size
for i, expert in enumerate(moe_block.experts):
ref_gate_proj_grad = expert.gate_proj.weight.grad
ref_up_proj_grad = expert.up_proj.weight.grad
assert ref_gate_proj_grad is not None
assert ref_up_proj_grad is not None
# Extract gradients
test_gate_proj_grad = grouped_gemm_block.gate_up_proj.grad[i, :moe_intermediate_size]
test_up_proj_grad = grouped_gemm_block.gate_up_proj.grad[i, moe_intermediate_size:]
assert test_gate_proj_grad is not None
assert test_up_proj_grad is not None
# Sanity check shapes
assert (
ref_gate_proj_grad.shape == test_gate_proj_grad.shape
), f"{ref_gate_proj_grad.shape} != {test_gate_proj_grad.shape}"
assert (
ref_up_proj_grad.shape == test_up_proj_grad.shape
), f"{ref_up_proj_grad.shape} != {test_up_proj_grad.shape}"
# Check gradients
diff = (ref_gate_proj_grad - test_gate_proj_grad).abs().max()
if not torch.allclose(ref_gate_proj_grad, test_gate_proj_grad, atol = atol, rtol = rtol):
print(f"expert {i} gate_proj_grad_diff: {diff.detach().cpu().item():.6f}")
diff = (ref_up_proj_grad - test_up_proj_grad).abs().max()
if not torch.allclose(ref_up_proj_grad, test_up_proj_grad, atol = atol, rtol = rtol):
print(f"expert {i} up_proj_grad_diff: {diff.detach().cpu().item():.6f}")
def check_gate_grad(
moe_block: Qwen3MoeSparseMoeBlock,
grouped_gemm_block: Qwen3MoeGroupedGEMMBlock,
atol: float,
rtol: float,
):
ref_grad = moe_block.gate.weight.grad
assert ref_grad is not None
test_grad = grouped_gemm_block.gate.grad
assert test_grad is not None
diff = (ref_grad - test_grad).abs().max()
if not torch.allclose(ref_grad, test_grad, atol = atol, rtol = rtol):
print(f"gate_grad_diff: {diff.detach().cpu().item():.6f}")
def check_wgrad(
moe_block: Qwen3MoeSparseMoeBlock,
grouped_gemm_block: Qwen3MoeGroupedGEMMBlock,
atol: float,
rtol: float,
):
check_down_proj_grad(moe_block, grouped_gemm_block, atol, rtol)
check_gate_up_proj_grad(moe_block, grouped_gemm_block, atol, rtol)
check_gate_grad(moe_block, grouped_gemm_block, atol, rtol)
def check_tensor_allclose(
X_ref: torch.Tensor,
X_test: torch.Tensor,
atol: float,
rtol: float,
name: str,
verbose: bool = False,
):
diff = (X_ref - X_test).abs().max()
if verbose:
print(f"{name} diff: {diff.detach().cpu().item():.6f}")
assert torch.allclose(
X_ref, X_test, atol = atol, rtol = rtol
), f"{name} diff: {diff.detach().cpu().item():.6f}"
def check_expert_grads(
ref_result: BackwardResult,
test_result: BackwardResult,
atol: float,
rtol: float,
verbose: bool = False,
):
fields_to_check = [f.name for f in fields(BackwardResult) if "proj" in f.name]
assert len(fields_to_check) == 3
for field in fields_to_check:
ref_grads = getattr(ref_result, field)
test_grads = getattr(test_result, field)
assert (
ref_grads.shape == test_grads.shape
), f"{field}: {ref_grads.shape} != {test_grads.shape}"
# Test each expert
for i in range(ref_grads.shape[0]):
ref_grad = ref_grads[i]
test_grad = test_grads[i]
diff = (ref_grad - test_grad).abs().max()
assert torch.allclose(
ref_grad, test_grad, atol = atol, rtol = rtol
), f"{field}[{i}] diff: {diff.detach().cpu().item():.6f}"
# Test all experts
diff = (ref_grads - test_grads).abs().max()
if verbose:
print(f"{field} diff: {diff.detach().cpu().item():.6f}")
assert torch.allclose(
ref_grads, test_grads, atol = atol, rtol = rtol
), f"{field} diff: {diff.detach().cpu().item():.6f}"
def check_grads(
ref_result: BackwardResult,
test_result: BackwardResult,
atol: float,
rtol: float,
verbose: bool = False,
):
check_tensor_allclose(ref_result.X_grad, test_result.X_grad, atol, rtol, "X.grad", verbose)
check_tensor_allclose(
ref_result.gate_grad, test_result.gate_grad, atol, rtol, "gate.grad", verbose
)
check_expert_grads(ref_result, test_result, atol, rtol, verbose)
def check_fwd(
ref_result: ForwardResult,
test_result: ForwardResult,
atol: float,
rtol: float,
verbose: bool = False,
):
# First check hidden states (output)
ref_output = ref_result.output
test_output = test_result.output
diff = (ref_output - test_output).abs().max()
if verbose:
print(f"output diff: {diff.detach().cpu().item():.6f}")
assert torch.allclose(
ref_output, test_output, atol = atol, rtol = rtol
), f"output diff: {diff.detach().cpu().item():.6f}"
# Check router logits
ref_router_logits = ref_result.router_logits
test_router_logits = test_result.router_logits
diff = (ref_router_logits - test_router_logits).abs().max()
if verbose:
print(f"router_logits diff: {diff.detach().cpu().item():.6f}")
assert torch.allclose(
ref_router_logits, test_router_logits, atol = atol, rtol = rtol
), f"router_logits diff: {diff.detach().cpu().item():.6f}"
def check_grouped_gemm_results(
grouped_result: GroupedGEMMResult,
fused_result: GroupedGEMMResult,
permute_y: bool,
atol: float,
rtol: float,
verbose: bool = False,
):
for field in fields(GroupedGEMMResult):
ref_value = getattr(grouped_result, field.name)
test_value = getattr(fused_result, field.name)
diff = (ref_value - test_value).abs().max()
# second_gemm in torch grouped gemm is not yet unpermuted so comparing the fused unpermuted second_gemm will result in error
# instead the hidden_states_unpermute should match since hidden_states_unpermute for the fused result is the same as second_gemm
if field.name == "second_gemm" and permute_y:
continue
if verbose:
print(f"{field.name} diff: {diff.detach().cpu().item():.6f}")
assert torch.allclose(
ref_value, test_value, atol = atol, rtol = rtol
), f"{field.name} diff: {diff.detach().cpu().item():.6f}"
def run_forward(
model: nn.Module,
X: torch.Tensor,
is_grouped_gemm: bool = False,
):
X = X.detach().clone().requires_grad_(True)
output, router_logits = model(X)
if is_grouped_gemm:
result = ForwardResult(
output = output.hidden_states,
router_logits = router_logits,
X = X,
grouped_gemm_result = output,
)
else:
result = ForwardResult(output = output, router_logits = router_logits, X = X)
return result
def run_backward(
model: nn.Module, grad_output: torch.Tensor, output: torch.Tensor, X: torch.Tensor
):
output.backward(grad_output)
assert X.grad is not None
for name, param in model.named_parameters():
assert param.grad is not None, f"{name} grad is None"
if isinstance(model, Qwen3MoeSparseMoeBlock):
gate_grad = model.gate.weight.grad
gate_proj_grad = torch.stack([expert.gate_proj.weight.grad for expert in model.experts])
up_proj_grad = torch.stack([expert.up_proj.weight.grad for expert in model.experts])
down_proj_grad = torch.stack([expert.down_proj.weight.grad for expert in model.experts])
elif isinstance(model, Qwen3MoeGroupedGEMMBlock):
gate_grad = model.gate.grad
gate_proj_grad, up_proj_grad = model.gate_up_proj.grad.chunk(2, dim = 1)
down_proj_grad = model.down_proj.grad
else:
raise ValueError(f"Unsupported model type: {type(model)}")
return BackwardResult(
X_grad = X.grad,
gate_grad = gate_grad,
gate_proj_grad = gate_proj_grad,
up_proj_grad = up_proj_grad,
down_proj_grad = down_proj_grad,
)
class Qwen3MoeFusedGroupedGEMMBlock(Qwen3MoeGroupedGEMMBlock):
"""Reference MoE block using triton grouped gemm.
Like Qwen3MoeGroupedGEMMBlock but with triton (not torch-native) grouped gemm.
NOT for production: it saves intermediate results and runs extra checks for
debugging. See grouped_gemm/reference/moe_block.py for a cleaner version.
"""
def __init__(
self,
config: Qwen3MoeConfig,
gate: torch.Tensor,
gate_up_proj: torch.Tensor,
down_proj: torch.Tensor,
permute_x: bool = False,
permute_y: bool = False,
autotune: bool = True,
kernel_config_fwd: KernelConfigForward = None,
kernel_config_bwd_dW: KernelConfigBackward_dW = None,
kernel_config_bwd_dX: KernelConfigBackward_dX = None,
):
super().__init__(config, gate, gate_up_proj, down_proj)
self.permute_x = permute_x
self.permute_y = permute_y
self.autotune = autotune
if not autotune:
assert (
kernel_config_fwd is not None
and kernel_config_bwd_dW is not None
and kernel_config_bwd_dX is not None
), "Kernel configs must be provided if autotune is False"
self.kernel_config_fwd = kernel_config_fwd
self.kernel_config_bwd_dW = kernel_config_bwd_dW
self.kernel_config_bwd_dX = kernel_config_bwd_dX
@classmethod
def from_hf(
cls,
moe_block: Qwen3MoeSparseMoeBlock,
permute_x: bool = False,
permute_y: bool = False,
autotune: bool = True,
kernel_config_fwd: KernelConfigForward = None,
kernel_config_bwd_dW: KernelConfigBackward_dW = None,
kernel_config_bwd_dX: KernelConfigBackward_dX = None,
):
config: Qwen3MoeConfig = moe_block.experts[0].config
gate, gate_up_proj, down_proj = Qwen3MoeGroupedGEMMBlock.extract_hf_weights(moe_block)
return cls(
config,
gate,
gate_up_proj,
down_proj,
permute_x = permute_x,
permute_y = permute_y,
autotune = autotune,
kernel_config_fwd = kernel_config_fwd,
kernel_config_bwd_dW = kernel_config_bwd_dW,
kernel_config_bwd_dX = kernel_config_bwd_dX,
)
def forward(
self,
hidden_states: torch.Tensor,
debug: bool = False,
) -> torch.Tensor:
batch_size, sequence_length, hidden_dim = hidden_states.shape
num_tokens = batch_size * sequence_length
total_tokens = num_tokens * self.top_k
hidden_states = hidden_states.view(-1, hidden_dim)
router_logits, routing_weights, selected_experts = self.run_router(hidden_states)
# Pre-processing: token counts per expert + token-order -> expert-order
# gather indices (auxiliary, not recorded in the autograd graph).
token_counts_by_expert, gather_indices = self.get_token_counts_and_gather_indices(
selected_experts
)
# permute_x fuses the permutation into the first grouped gemm's prologue
if not self.permute_x:
hidden_states = permute(hidden_states, gather_indices, self.top_k)
assert hidden_states.shape == (total_tokens, hidden_dim)
# Start expert computation
first_gemm = grouped_gemm(
X = hidden_states,
W = self.gate_up_proj,
m_sizes = token_counts_by_expert,
gather_indices = gather_indices,
topk = self.top_k,
permute_x = self.permute_x,
permute_y = False, # output of first grouped gemm should never be permuted
autotune = self.autotune,
kernel_config_fwd = self.kernel_config_fwd,
kernel_config_bwd_dW = self.kernel_config_bwd_dW,
kernel_config_bwd_dX = self.kernel_config_bwd_dX,
is_first_gemm = True,
)
assert first_gemm.shape == (total_tokens, 2 * self.moe_intermediate_size)
intermediate = self.act_and_mul(first_gemm)
assert intermediate.shape == (total_tokens, self.moe_intermediate_size)
second_gemm = grouped_gemm(
X = intermediate,
W = self.down_proj,
m_sizes = token_counts_by_expert,
gather_indices = gather_indices,
topk = self.top_k,
permute_x = False,
permute_y = self.permute_y,
autotune = self.autotune,
kernel_config_fwd = self.kernel_config_fwd,
kernel_config_bwd_dW = self.kernel_config_bwd_dW,
kernel_config_bwd_dX = self.kernel_config_bwd_dX,
is_first_gemm = False,
)
assert second_gemm.shape == (total_tokens, hidden_dim)
# Post-processing: unpermute expert order -> token order
if not self.permute_y:
hidden_states_unpermute = unpermute(second_gemm, gather_indices)
assert hidden_states_unpermute.shape == (total_tokens, hidden_dim)
else:
hidden_states_unpermute = second_gemm
# Merge topk weights
hidden_states = (
hidden_states_unpermute.view(num_tokens, self.top_k, hidden_dim)
* routing_weights[..., None]
)
hidden_states = hidden_states.sum(dim = 1)
assert hidden_states.shape == (num_tokens, hidden_dim)
hidden_states = hidden_states.view(batch_size, sequence_length, hidden_dim)
return GroupedGEMMResult(
token_counts_by_expert = token_counts_by_expert,
gather_indices = gather_indices,
topk_weights = routing_weights,
first_gemm = first_gemm,
intermediate = intermediate,
second_gemm = second_gemm,
hidden_states_unpermute = hidden_states_unpermute,
hidden_states = hidden_states,
), router_logits