diff --git a/conversion/qwen.py b/conversion/qwen.py index b4ae528bf2d..a2f4e995c2b 100644 --- a/conversion/qwen.py +++ b/conversion/qwen.py @@ -289,12 +289,20 @@ class _QwenMtpMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.block_count = self.hparams["num_hidden_layers"] + # text_config may nest the text hyperparameters; merge them like index_tensors does + hparams = {**self.hparams, **self.hparams.get("text_config", {})} + self.block_count = hparams["num_hidden_layers"] if not self.no_mtp: - n_mtp = self.hparams.get("mtp_num_hidden_layers", 0) - # Qwen-3-Next doesn't include `mtp_num_hidden_layers` in config. + n_mtp = hparams.get("mtp_num_hidden_layers", 0) + # Qwen-3-Next doesn't include mtp_num_hidden_layers in config. if n_mtp == 0: - assert self.opt_num_mtp_layers != 0 + # The count is recovered from tensor names later; fail clearly instead of asserting. + if self.opt_num_mtp_layers == 0: + raise ValueError( + "MTP layer count not found in config (checked top level and " + "text_config) and not yet recovered from tensor names. " + "Re-export with --no-mtp, or add mtp_num_hidden_layers to config." + ) n_mtp = self.opt_num_mtp_layers self.block_count += n_mtp self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) diff --git a/conversion/tests/test_qwen_mtp.py b/conversion/tests/test_qwen_mtp.py new file mode 100644 index 00000000000..1f48707905d --- /dev/null +++ b/conversion/tests/test_qwen_mtp.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import pytest + +import gguf +from conversion.qwen import _QwenMtpMixin + + +class _DummyParent: + def __init__(self, *, hparams: dict, no_mtp: bool = False, model_arch: gguf.MODEL_ARCH, **kwargs): + self.hparams = hparams + self.no_mtp = no_mtp + self.model_arch = model_arch + + +class _MtpModel(_QwenMtpMixin, _DummyParent): + pass + + +@pytest.fixture(autouse=True) +def _reset_mtp_class_state(): + """The mixin uses class attributes that persist across conversions; isolate tests.""" + _MtpModel._original_block_count = None + _MtpModel.opt_num_mtp_layers = 0 + yield + _MtpModel._original_block_count = None + _MtpModel.opt_num_mtp_layers = 0 + + +def test_mtp_count_read_from_text_config(): + model = _MtpModel( + hparams={ + "num_hidden_layers": 24, + "text_config": {"num_hidden_layers": 24, "mtp_num_hidden_layers": 1}, + }, + model_arch=gguf.MODEL_ARCH.QWEN3NEXT, + ) + assert model.block_count == 25 + + +def test_mtp_count_read_from_top_level_config(): + model = _MtpModel( + hparams={"num_hidden_layers": 24, "mtp_num_hidden_layers": 2}, + model_arch=gguf.MODEL_ARCH.QWEN3NEXT, + ) + assert model.block_count == 26 + + +def test_mtp_count_falls_back_to_recovered_class_attribute(): + _MtpModel.opt_num_mtp_layers = 1 + model = _MtpModel( + hparams={"num_hidden_layers": 24}, + model_arch=gguf.MODEL_ARCH.QWEN3NEXT, + ) + assert model.block_count == 25 + + +def test_missing_mtp_count_raises_actionable_error(): + with pytest.raises(ValueError, match="MTP layer count not found"): + _MtpModel( + hparams={"num_hidden_layers": 24}, + model_arch=gguf.MODEL_ARCH.QWEN3NEXT, + ) + + +def test_no_mtp_skips_mtp_count_lookup(): + model = _MtpModel( + hparams={"num_hidden_layers": 24}, + no_mtp=True, + model_arch=gguf.MODEL_ARCH.QWEN3NEXT, + ) + assert model.block_count == 24