[GraniteMoE] Load split per-expert quantized MoE weights (#37679)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com> Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Xinyuan Tong
Xinyuan Tong
parent
7b89b95168
commit
ae1acf822d
@@ -54,6 +54,52 @@ def granitemoe_split_expert_weights(
|
|||||||
yield name, weight
|
yield name, weight
|
||||||
|
|
||||||
|
|
||||||
|
def _match_split_expert(
|
||||||
|
name: str,
|
||||||
|
expert_params_mapping: list[tuple[str, str, int, str]],
|
||||||
|
) -> Optional[tuple[str, int, str]]:
|
||||||
|
"""Resolve a split-expert tensor name to (param_name, expert_id, shard_id)."""
|
||||||
|
for param_name, weight_name, expert_id, shard_id in expert_params_mapping:
|
||||||
|
if weight_name in name:
|
||||||
|
# Keeping the checkpoint's trailing suffix (`.weight` vs
|
||||||
|
# `.weight_scale`) routes scales to FusedMoE's scale branch.
|
||||||
|
return name.replace(weight_name, param_name), expert_id, shard_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_packed_expert(name: str) -> bool:
|
||||||
|
"""Whether an expert tensor is the packed layout, already split to w1/w2/w3."""
|
||||||
|
return any(f".{shard}." in name for shard in ("w1", "w2", "w3"))
|
||||||
|
|
||||||
|
|
||||||
|
def granitemoe_load_split_experts(
|
||||||
|
weights: Iterable[tuple[str, torch.Tensor]],
|
||||||
|
expert_params_mapping: list[tuple[str, str, int, str]],
|
||||||
|
params_dict: dict[str, torch.nn.Parameter],
|
||||||
|
) -> Iterator[tuple[str, torch.Tensor]]:
|
||||||
|
"""Load split quantized experts, yielding the tensors it does not claim."""
|
||||||
|
for name, loaded_weight in weights:
|
||||||
|
match = _match_split_expert(name, expert_params_mapping)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
# Packed-layout experts load downstream; any other expert tensor
|
||||||
|
# would be dropped silently and generate garbage, so fail loudly.
|
||||||
|
if ".block_sparse_moe.experts." in name and not _is_packed_expert(name):
|
||||||
|
raise ValueError(f"unmatched MoE expert tensor: {name}")
|
||||||
|
yield name, loaded_weight
|
||||||
|
continue
|
||||||
|
|
||||||
|
mapped_name, expert_id, shard_id = match
|
||||||
|
param = params_dict[mapped_name]
|
||||||
|
param.weight_loader(
|
||||||
|
param,
|
||||||
|
loaded_weight,
|
||||||
|
mapped_name,
|
||||||
|
shard_id=shard_id,
|
||||||
|
expert_id=expert_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GraniteMoeMoE(nn.Module):
|
class GraniteMoeMoE(nn.Module):
|
||||||
"""A tensor-parallel MoE implementation for GraniteMoe that shards each
|
"""A tensor-parallel MoE implementation for GraniteMoe that shards each
|
||||||
expert across all ranks.
|
expert across all ranks.
|
||||||
@@ -443,10 +489,26 @@ class GraniteMoeForCausalLM(nn.Module):
|
|||||||
else:
|
else:
|
||||||
return self.pooler(hidden_states, forward_batch)
|
return self.pooler(hidden_states, forward_batch)
|
||||||
|
|
||||||
|
def _split_expert_params_mapping(self) -> list[tuple[str, str, int, str]]:
|
||||||
|
# llmcompressor emits one tensor per expert per projection
|
||||||
|
# (`experts.{e}.gate_proj.weight`, `.weight_scale`, ...), not the packed
|
||||||
|
# input_linear/output_linear that granitemoe_split_expert_weights maps.
|
||||||
|
return FusedMoE.make_expert_params_mapping(
|
||||||
|
ckpt_gate_proj_name="gate_proj",
|
||||||
|
ckpt_down_proj_name="down_proj",
|
||||||
|
ckpt_up_proj_name="up_proj",
|
||||||
|
num_experts=self.config.num_local_experts,
|
||||||
|
)
|
||||||
|
|
||||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||||
weights = granitemoe_split_expert_weights(
|
weights = granitemoe_split_expert_weights(
|
||||||
self.hf_to_sglang_mapper.apply(weights)
|
self.hf_to_sglang_mapper.apply(weights)
|
||||||
)
|
)
|
||||||
|
weights = granitemoe_load_split_experts(
|
||||||
|
weights,
|
||||||
|
expert_params_mapping=self._split_expert_params_mapping(),
|
||||||
|
params_dict=dict(self.named_parameters()),
|
||||||
|
)
|
||||||
mixtral.MixtralForCausalLM.load_weights(self, weights)
|
mixtral.MixtralForCausalLM.load_weights(self, weights)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"""Unit tests for granitemoe_load_split_experts.
|
||||||
|
|
||||||
|
Compressed-tensors checkpoints (llmcompressor) store MoE experts one tensor per
|
||||||
|
expert per projection -- `experts.<id>.{gate,up,down}_proj.{weight,weight_scale}`
|
||||||
|
-- while the unquantized HF checkpoint packs them into `input_linear` /
|
||||||
|
`output_linear`. Only the packed layout was recognised, so every split expert
|
||||||
|
tensor fell through to a `logger.warning(...not found in params_dict)` and was
|
||||||
|
silently dropped: the server started normally and then emitted garbage
|
||||||
|
("capital capital capital..." instead of " Paris.").
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=6, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
|
||||||
|
from sglang.srt.models.granitemoe import (
|
||||||
|
_is_packed_expert,
|
||||||
|
granitemoe_load_split_experts,
|
||||||
|
)
|
||||||
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
|
NUM_EXPERTS = 4
|
||||||
|
PREFIX = "model.layers.0.block_sparse_moe"
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingParam:
|
||||||
|
"""Deliberately rejects `return_success`: no loader in the tree accepts
|
||||||
|
it, so a caller passing it raises TypeError here."""
|
||||||
|
|
||||||
|
def __init__(self, calls):
|
||||||
|
self._calls = calls
|
||||||
|
|
||||||
|
def weight_loader(self, param, loaded_weight, name, shard_id, expert_id):
|
||||||
|
self._calls.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"shard_id": shard_id,
|
||||||
|
"expert_id": expert_id,
|
||||||
|
"value": loaded_weight,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mapping(num_experts=NUM_EXPERTS):
|
||||||
|
return FusedMoE.make_expert_params_mapping(
|
||||||
|
ckpt_gate_proj_name="gate_proj",
|
||||||
|
ckpt_down_proj_name="down_proj",
|
||||||
|
ckpt_up_proj_name="up_proj",
|
||||||
|
num_experts=num_experts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(weights, params_dict=None, calls=None):
|
||||||
|
"""Drive the loader, returning (passed_through, calls)."""
|
||||||
|
calls = [] if calls is None else calls
|
||||||
|
if params_dict is None:
|
||||||
|
params_dict = _AutoParams(calls)
|
||||||
|
passed_through = list(
|
||||||
|
granitemoe_load_split_experts(
|
||||||
|
weights,
|
||||||
|
expert_params_mapping=_mapping(),
|
||||||
|
params_dict=params_dict,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return passed_through, calls
|
||||||
|
|
||||||
|
|
||||||
|
class _AutoParams(dict):
|
||||||
|
"""params_dict that materialises a recording param for any requested name."""
|
||||||
|
|
||||||
|
def __init__(self, calls):
|
||||||
|
super().__init__()
|
||||||
|
self._calls = calls
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key not in self:
|
||||||
|
super().__setitem__(key, _RecordingParam(self._calls))
|
||||||
|
return super().__getitem__(key)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGraniteMoeLoadSplitExperts(CustomTestCase):
|
||||||
|
def test_split_experts_are_loaded_not_passed_through(self):
|
||||||
|
"""The bug: these tensors reached the generic path and were dropped."""
|
||||||
|
weights = [
|
||||||
|
(f"{PREFIX}.experts.3.gate_proj.weight", torch.zeros(2)),
|
||||||
|
(f"{PREFIX}.experts.3.up_proj.weight", torch.zeros(2)),
|
||||||
|
(f"{PREFIX}.experts.1.down_proj.weight", torch.zeros(2)),
|
||||||
|
]
|
||||||
|
passed_through, calls = _run(weights)
|
||||||
|
|
||||||
|
self.assertEqual(passed_through, [], "split experts must not fall through")
|
||||||
|
self.assertEqual(len(calls), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
[(c["name"], c["shard_id"], c["expert_id"]) for c in calls],
|
||||||
|
[
|
||||||
|
(f"{PREFIX}.experts.w13_weight", "w1", 3),
|
||||||
|
(f"{PREFIX}.experts.w13_weight", "w3", 3),
|
||||||
|
(f"{PREFIX}.experts.w2_weight", "w2", 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_scale_keeps_its_suffix(self):
|
||||||
|
"""FusedMoE's loader dispatches on substrings of the name it is handed,
|
||||||
|
so a scale must arrive as `*_weight_scale`. Handing it `*_weight` would
|
||||||
|
load the scale as if it were the weight."""
|
||||||
|
weights = [
|
||||||
|
(f"{PREFIX}.experts.0.gate_proj.weight_scale", torch.zeros(1)),
|
||||||
|
(f"{PREFIX}.experts.0.down_proj.weight_scale", torch.zeros(1)),
|
||||||
|
]
|
||||||
|
_, calls = _run(weights)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[c["name"] for c in calls],
|
||||||
|
[
|
||||||
|
f"{PREFIX}.experts.w13_weight_scale",
|
||||||
|
f"{PREFIX}.experts.w2_weight_scale",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_every_expert_and_projection_is_loaded(self):
|
||||||
|
"""A missing (expert, projection) pair is the silent-drop bug: nothing
|
||||||
|
matches and those weights never reach the layer."""
|
||||||
|
weights = [
|
||||||
|
(f"{PREFIX}.experts.{e}.{proj}.{suffix}", torch.zeros(1))
|
||||||
|
for e in range(NUM_EXPERTS)
|
||||||
|
for proj in ("gate_proj", "up_proj", "down_proj")
|
||||||
|
for suffix in ("weight", "weight_scale")
|
||||||
|
]
|
||||||
|
passed_through, calls = _run(weights)
|
||||||
|
|
||||||
|
self.assertEqual(passed_through, [])
|
||||||
|
self.assertEqual(len(calls), len(weights))
|
||||||
|
self.assertEqual(
|
||||||
|
{(c["expert_id"], c["shard_id"]) for c in calls},
|
||||||
|
{(e, s) for e in range(NUM_EXPERTS) for s in ("w1", "w2", "w3")},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_non_expert_tensors_pass_through_untouched(self):
|
||||||
|
weights = [
|
||||||
|
("model.layers.0.self_attn.q_proj.weight", torch.zeros(1)),
|
||||||
|
("model.layers.0.self_attn.q_proj.weight_scale", torch.zeros(1)),
|
||||||
|
(f"{PREFIX}.router.layer.weight", torch.zeros(1)),
|
||||||
|
("model.layers.0.input_layernorm.weight", torch.zeros(1)),
|
||||||
|
("lm_head.weight", torch.zeros(1)),
|
||||||
|
]
|
||||||
|
passed_through, calls = _run(weights)
|
||||||
|
|
||||||
|
self.assertEqual([n for n, _ in passed_through], [n for n, _ in weights])
|
||||||
|
self.assertEqual(calls, [])
|
||||||
|
|
||||||
|
def test_packed_layout_experts_pass_through(self):
|
||||||
|
"""The packed (unquantized) path pre-splits into w1/w2/w3, which load
|
||||||
|
downstream. Claiming or rejecting them here breaks the bf16 model."""
|
||||||
|
weights = [
|
||||||
|
(f"{PREFIX}.experts.0.{shard}.weight", torch.zeros(1))
|
||||||
|
for shard in ("w1", "w2", "w3")
|
||||||
|
]
|
||||||
|
passed_through, calls = _run(weights)
|
||||||
|
|
||||||
|
self.assertEqual([n for n, _ in passed_through], [n for n, _ in weights])
|
||||||
|
self.assertEqual(calls, [])
|
||||||
|
|
||||||
|
def test_unrecognised_expert_tensor_raises(self):
|
||||||
|
"""The original failure was silent. An expert tensor that matches no
|
||||||
|
mapping must now fail the load instead of producing a garbage model."""
|
||||||
|
weights = [(f"{PREFIX}.experts.0.mystery_proj.weight", torch.zeros(1))]
|
||||||
|
with self.assertRaises(ValueError) as ctx:
|
||||||
|
_run(weights)
|
||||||
|
self.assertIn("unmatched MoE expert tensor", str(ctx.exception))
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsPackedExpert(CustomTestCase):
|
||||||
|
"""A false positive here silently reintroduces the dropped-expert bug:
|
||||||
|
an unmatched expert tensor passes through instead of raising."""
|
||||||
|
|
||||||
|
def test_shard_substring_requires_dot_delimiters(self):
|
||||||
|
"""`w1` appearing inside a longer segment is not a packed shard. Without
|
||||||
|
the dots this predicate would swallow such a tensor as packed and the
|
||||||
|
loader would drop it silently instead of raising."""
|
||||||
|
for name in (
|
||||||
|
f"{PREFIX}.experts.0.w1_proj.weight",
|
||||||
|
f"{PREFIX}.experts.0.gate_w2.weight",
|
||||||
|
f"{PREFIX}.experts.0.w3x.weight",
|
||||||
|
):
|
||||||
|
with self.subTest(name=name):
|
||||||
|
self.assertFalse(_is_packed_expert(name))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user