[AMD][GLM-5.2] Keep GlmMoeDsa MoE e_score_correction_bias in fp32 (#37133)

Co-authored-by: JohnQinAMD <yanyuan.qin@amd.com>
Co-authored-by: Thomas Wang <thomawan@amd.com>
Co-authored-by: Zhang, Jiejing <jiejing.zhang@amd.com>
This commit is contained in:
xiaobochen-amd
2026-09-07 18:17:54 -07:00
committed by GitHub
co-authored by JohnQinAMD Thomas Wang Zhang, Jiejing
parent f4bbf12423
commit 5aa913e156
4 changed files with 166 additions and 5 deletions
+8 -2
View File
@@ -151,6 +151,14 @@ def is_deepseek_dsa(config) -> bool:
)
def is_glm_moe_dsa(config) -> bool:
"""True for GLM-5.2, both the main arch and the NextN draft head."""
return _hf_arch(config) in (
"GlmMoeDsaForCausalLM",
"GlmMoeDsaForCausalLMNextN",
)
def is_kimi_k3(config) -> bool:
return _hf_arch(config) in (
"KimiK3ForConditionalGeneration",
@@ -682,7 +690,6 @@ class ModelConfig:
context_length: Optional[int] = None,
**kwargs,
):
cfg = resolving_view(server_args)
quantization = (
cfg.speculative_draft_model_quantization
@@ -2239,7 +2246,6 @@ def is_hybrid_swa_model(
model_architectures: List[str],
hf_text_config: Optional[PretrainedConfig] = None,
):
hybrid_swa_archs = {
"Llama4ForConditionalGeneration",
"DeepseekV4ForCausalLM",
+13 -2
View File
@@ -1757,9 +1757,20 @@ def biased_grouped_topk_gpu(
topk_weights = torch.empty((token, topk), dtype=torch.float32, device=device)
topk_ids = torch.empty((token, topk), dtype=torch.int32, device=device)
# Don't re-downcast an fp32 correction bias at the aiter boundary: an
# offset bias loses too many levels in bf16 and reorders top-k. Cast the
# gating logits up instead. Gated on the bias dtype rather than the
# architecture, so a bias that arrives as bf16 is byte-identical to
# before, as is the radix4 path above.
if correction_bias.dtype == torch.float32:
aiter_gating_output = gating_output.to(torch.float32)
aiter_bias = correction_bias
else:
aiter_gating_output = gating_output
aiter_bias = bias
aiter_biased_grouped_topk(
gating_output,
bias,
aiter_gating_output,
aiter_bias,
topk_weights,
topk_ids,
num_expert_group,
+4 -1
View File
@@ -49,6 +49,7 @@ from sglang.srt.configs.model_config import (
get_dsa_index_n_heads,
get_dsa_index_topk,
is_deepseek_dsa,
is_glm_moe_dsa,
)
from sglang.srt.distributed import (
divide,
@@ -473,7 +474,9 @@ class MoEGate(nn.Module):
)
if config.topk_method == "noaux_tc" and not is_hash_moe:
correction_bias_dtype = torch.float32
if quant_config is not None:
# GLM-5.2's bias sits at an offset where its spread is only a few bf16 ULPs
# wide, so bf16 collapses it and reorders top-k routing. HF stores it fp32.
if quant_config is not None and not is_glm_moe_dsa(config):
if _use_aiter and quant_config.get_name() in (
"fp8",
"compressed_tensors",
@@ -0,0 +1,141 @@
"""Unit tests for the GLM-5.2 (GlmMoeDsa) fp32 MoE correction-bias fix.
GlmMoeDsa's MoE ``e_score_correction_bias`` values are ~34. bf16 has ULP 0.25 at
that magnitude, so downcasting collapses the ~174 distinct biases to ~3 levels,
which scrambles top-k expert routing (noaux_tc picks experts by sigmoid-score +
bias, and the ~34 bias dominates the [0, 1] sigmoid term). The fix keeps the bias
in fp32 for GlmMoeDsa at both the parameter-construction site (MoEGate) and the
aiter routing boundary (layers/moe/topk.py). These tests are pure dtype / CPU
logic -- no server, no weight loading, no GPU required.
"""
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
from sglang.srt.configs.model_config import is_glm_moe_dsa
from sglang.srt.models.deepseek_v2 import MoEGate
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
GLM_MAIN_ARCH = "GlmMoeDsaForCausalLM"
GLM_NEXTN_ARCH = "GlmMoeDsaForCausalLMNextN" # draft head, rewritten in model_config.py
NON_GLM_ARCH = "DeepseekV3ForCausalLM"
# Biases spanning a ~0.5-wide window around 34: distinct in fp32 (fp32 ULP ~4e-6
# there), but within ~2-3 bf16 bins (bf16 ULP 0.25 at magnitude 34).
NUM_EXPERTS = 174
BIAS_BASE = 34.0
def _glm_bias_values() -> torch.Tensor:
step = 0.5 / NUM_EXPERTS
return torch.tensor(
[BIAS_BASE + i * step for i in range(NUM_EXPERTS)], dtype=torch.float32
)
class TestMoEGateCorrectionBiasDtype(CustomTestCase):
"""Behavioral guard on the production dtype block in MoEGate.__init__.
Fails on pre-fix code (GlmMoeDsa was downcast to bf16 like every other aiter
fp8 model); passes once the arch-gate skips the downcast for GlmMoeDsa.
"""
def _build_gate(self, arch: str) -> MoEGate:
config = SimpleNamespace(
n_routed_experts=NUM_EXPERTS,
hidden_size=16,
topk_method="noaux_tc",
architectures=[arch],
)
quant_config = SimpleNamespace(get_name=lambda: "fp8")
# Force the aiter fp8 path (the branch that downcasts to bf16 for non-GLM);
# _is_cpu=False avoids the AMX PackWeightMethod branch so the test is
# deterministic across CPU and GPU CI runners.
import sglang.srt.models.deepseek_v2 as dv2
with patch.object(dv2, "_use_aiter", True), patch.object(dv2, "_is_cpu", False):
return MoEGate(config=config, quant_config=quant_config)
def test_glm_main_keeps_fp32(self):
gate = self._build_gate(GLM_MAIN_ARCH)
self.assertEqual(gate.e_score_correction_bias.dtype, torch.float32)
def test_glm_nextn_keeps_fp32(self):
# Guards the NextN draft head: the "GlmMoeDsa" substring must cover it too.
gate = self._build_gate(GLM_NEXTN_ARCH)
self.assertEqual(gate.e_score_correction_bias.dtype, torch.float32)
def test_non_glm_still_downcasts_bf16(self):
# Blast-radius guard: the fix must not widen dtype for other aiter models.
# Fails if the gate is accidentally made too broad (e.g. always-skip).
gate = self._build_gate(NON_GLM_ARCH)
self.assertEqual(gate.e_score_correction_bias.dtype, torch.bfloat16)
class TestIsGlmMoeDsaHelper(CustomTestCase):
"""The arch-gate predicate used at both fix sites."""
def test_matches_main_and_nextn(self):
self.assertTrue(is_glm_moe_dsa(SimpleNamespace(architectures=[GLM_MAIN_ARCH])))
self.assertTrue(is_glm_moe_dsa(SimpleNamespace(architectures=[GLM_NEXTN_ARCH])))
def test_reads_the_first_architecture_like_its_neighbours(self):
# is_deepseek_dsa and is_kimi_k3 next to it both decide on
# architectures[0]; a HF config carries the model's own arch there.
self.assertFalse(
is_glm_moe_dsa(SimpleNamespace(architectures=["Foo", GLM_MAIN_ARCH]))
)
def test_rejects_non_glm(self):
self.assertFalse(is_glm_moe_dsa(SimpleNamespace(architectures=[NON_GLM_ARCH])))
def test_returns_false_for_none_or_empty_architectures(self):
# A config with architectures=None or [] must return False, never
# mis-gating a non-GLM model. _hf_arch() returns None for both.
self.assertFalse(is_glm_moe_dsa(SimpleNamespace(architectures=None)))
self.assertFalse(is_glm_moe_dsa(SimpleNamespace(architectures=[])))
class TestCorrectionBiasBf16Collapse(CustomTestCase):
"""Pins the numeric mechanism the fp32 fix protects against."""
def test_bf16_collapses_distinct_biases(self):
biases = _glm_bias_values()
fp32_distinct = torch.unique(biases).numel()
bf16_distinct = torch.unique(biases.to(torch.bfloat16)).numel()
# fp32 keeps every distinct bias; bf16 collapses the 174 values to a
# handful (the documented ~3), i.e. an order-of-magnitude information loss.
self.assertEqual(fp32_distinct, NUM_EXPERTS)
self.assertLessEqual(bf16_distinct, 4)
self.assertLess(bf16_distinct * 20, fp32_distinct)
def test_bf16_bias_scrambles_topk_routing(self):
# noaux_tc selects top-k experts by (sigmoid(logits) + correction_bias).
# With the bias collapsed to ~3 levels, selection within a level is decided
# by the tiny sigmoid term instead of the intended bias order -> the chosen
# expert set diverges from the fp32 (correct) selection for most tokens.
torch.manual_seed(0)
topk = 8
num_tokens = 64
biases_fp32 = _glm_bias_values()[torch.randperm(NUM_EXPERTS)]
biases_bf16 = biases_fp32.to(torch.bfloat16).to(torch.float32)
scores = torch.randn(num_tokens, NUM_EXPERTS).sigmoid()
top_fp32 = (scores + biases_fp32).topk(topk, dim=-1).indices
top_bf16 = (scores + biases_bf16).topk(topk, dim=-1).indices
differ = sum(
set(top_fp32[t].tolist()) != set(top_bf16[t].tolist())
for t in range(num_tokens)
)
self.assertGreater(differ / num_tokens, 0.5)
if __name__ == "__main__":
unittest.main()