[Fix] Load HunyuanV3 NextN final_layernorm into the draft head's output norm (#30331)
Co-authored-by: Auroter <auroter@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Auroter
Claude Fable 5
parent
a74bee2261
commit
9fec359a60
@@ -186,6 +186,10 @@ class HYV3ForCausalLMNextN(nn.Module):
|
|||||||
subname = name[len(nextn_prefix) :]
|
subname = name[len(nextn_prefix) :]
|
||||||
if any(subname.startswith(s) for s in spec_weight_names):
|
if any(subname.startswith(s) for s in spec_weight_names):
|
||||||
name = f"model.{subname}"
|
name = f"model.{subname}"
|
||||||
|
elif subname.startswith("final_layernorm"):
|
||||||
|
# Released checkpoints store the draft head's output norm
|
||||||
|
# as model.layers.<N>.final_layernorm.weight.
|
||||||
|
name = "model.shared_head.norm.weight"
|
||||||
else:
|
else:
|
||||||
name = f"model.decoder.{subname}"
|
name = f"model.decoder.{subname}"
|
||||||
elif name == "model.shared_head.norm.weight":
|
elif name == "model.shared_head.norm.weight":
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for HYV3ForCausalLMNextN.load_weights.
|
||||||
|
|
||||||
|
Regression test for the released Hy3 MTP checkpoint key
|
||||||
|
``model.layers.<num_hidden_layers>.final_layernorm.weight``, which must load
|
||||||
|
into the draft head's output norm (``model.shared_head.norm``) instead of
|
||||||
|
being remapped to a nonexistent decoder parameter and silently dropped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=4, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.models.hunyuan_v3_nextn import HYV3ForCausalLMNextN
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeParam:
|
||||||
|
def __init__(self):
|
||||||
|
self.loaded = None
|
||||||
|
|
||||||
|
def weight_loader(self, param, loaded_weight, *args, **kwargs):
|
||||||
|
self.loaded = (param, loaded_weight, args, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHunyuanV3NextNWeightLoading(unittest.TestCase):
|
||||||
|
def _make_minimal_model(self, named_parameters=()):
|
||||||
|
model = object.__new__(HYV3ForCausalLMNextN)
|
||||||
|
model.config = SimpleNamespace(num_hidden_layers=80, num_experts=2)
|
||||||
|
model.named_parameters = lambda: iter(named_parameters)
|
||||||
|
return model
|
||||||
|
|
||||||
|
def test_final_layernorm_loads_into_shared_head_norm(self):
|
||||||
|
param = _FakeParam()
|
||||||
|
model = self._make_minimal_model([("model.shared_head.norm.weight", param)])
|
||||||
|
loaded_weight = torch.ones(1)
|
||||||
|
|
||||||
|
model.load_weights([("model.layers.80.final_layernorm.weight", loaded_weight)])
|
||||||
|
|
||||||
|
self.assertEqual(param.loaded, (param, loaded_weight, (), {}))
|
||||||
|
|
||||||
|
def test_spec_weights_map_to_model_prefix(self):
|
||||||
|
params = {
|
||||||
|
"model.enorm.weight": _FakeParam(),
|
||||||
|
"model.hnorm.weight": _FakeParam(),
|
||||||
|
"model.eh_proj.weight": _FakeParam(),
|
||||||
|
}
|
||||||
|
model = self._make_minimal_model(list(params.items()))
|
||||||
|
weights = [
|
||||||
|
("model.layers.80.enorm.weight", torch.ones(1)),
|
||||||
|
("model.layers.80.hnorm.weight", torch.ones(1)),
|
||||||
|
("model.layers.80.eh_proj.weight", torch.ones(1)),
|
||||||
|
]
|
||||||
|
|
||||||
|
model.load_weights(weights)
|
||||||
|
|
||||||
|
for name, param in params.items():
|
||||||
|
self.assertIsNotNone(param.loaded, f"{name} was not loaded")
|
||||||
|
|
||||||
|
def test_decoder_layer_weight_maps_to_decoder_prefix(self):
|
||||||
|
param = _FakeParam()
|
||||||
|
model = self._make_minimal_model(
|
||||||
|
[("model.decoder.input_layernorm.weight", param)]
|
||||||
|
)
|
||||||
|
loaded_weight = torch.ones(1)
|
||||||
|
|
||||||
|
model.load_weights([("model.layers.80.input_layernorm.weight", loaded_weight)])
|
||||||
|
|
||||||
|
self.assertEqual(param.loaded, (param, loaded_weight, (), {}))
|
||||||
|
|
||||||
|
def test_embed_tokens_and_lm_head_are_skipped(self):
|
||||||
|
params = {
|
||||||
|
"model.embed_tokens.weight": _FakeParam(),
|
||||||
|
"lm_head.weight": _FakeParam(),
|
||||||
|
}
|
||||||
|
model = self._make_minimal_model(list(params.items()))
|
||||||
|
weights = [
|
||||||
|
("model.embed_tokens.weight", torch.ones(1)),
|
||||||
|
("lm_head.weight", torch.ones(1)),
|
||||||
|
]
|
||||||
|
|
||||||
|
model.load_weights(weights)
|
||||||
|
|
||||||
|
for name, param in params.items():
|
||||||
|
self.assertIsNone(param.loaded, f"{name} should have been skipped")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user