diff --git a/pyproject.toml b/pyproject.toml
index abbacc60..d8efeb34 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -68,6 +68,8 @@ extend-exclude = [
"vllm_gguf_plugin/csrc/gguf/dequantize.cuh",
"vllm_gguf_plugin/csrc/gguf/vecdotq.cuh",
"vllm_gguf_plugin/csrc/gguf/mmq.cuh",
+ "vllm_gguf_plugin/csrc/gguf/mma_v2.cuh",
+ "vllm_gguf_plugin/csrc/gguf/mmq_v2.cuh",
"vllm_gguf_plugin/csrc/gguf/mmvq.cuh",
]
ignore-hidden = false
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 70fd12d6..21f2cb41 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -1,10 +1,21 @@
# SPDX-License-Identifier: Apache-2.0
+import json
+import subprocess
+import sys
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
import torch
+import vllm.config.model as model_config_module
import vllm.engine.arg_utils as arg_utils_module
+import vllm.model_executor.layers.quantization as quantization_module
import vllm.model_executor.layers.vocab_parallel_embedding as vocab_embedding_module
import vllm.model_executor.parameter as parameter_module
import vllm.transformers_utils.config as config_module
+from gguf import GGMLQuantizationType as WeightType
+from gguf.constants import Keys, VisionProjectorType
from transformers import PretrainedConfig
from vllm.config.load import LoadConfig
from vllm.engine.arg_utils import EngineArgs
@@ -12,27 +23,48 @@
WEIGHT_LOADER_V2_SUPPORTED,
MergedColumnParallelLinear,
QKVParallelLinear,
+ RowParallelLinear,
)
-from vllm.model_executor.layers.quantization import get_quantization_config
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.model_executor.model_loader import get_model_loader
from vllm.transformers_utils.config import get_config_parser
import vllm_gguf_plugin.config_parser as gguf_config_parser_module
+import vllm_gguf_plugin.gguf_tokenizer_builder as gguf_tokenizer_builder_module
+import vllm_gguf_plugin.gguf_utils as gguf_utils_module
+import vllm_gguf_plugin.plugin as gguf_plugin_module
import vllm_gguf_plugin.quantization as gguf_quantization
+import vllm_gguf_plugin.quantization.params as gguf_params_module
+import vllm_gguf_plugin.weights_adapter.default as default_adapter_module
+import vllm_gguf_plugin.weights_adapter.qwen3_5 as qwen3_5_adapter_module
from vllm_gguf_plugin import OOTGGUFConfig, OOTGGUFModelLoader, register
from vllm_gguf_plugin.config_parser import GGUFConfigParser
+from vllm_gguf_plugin.gguf_tokenizer_builder import build_tokenizer_from_gguf
+from vllm_gguf_plugin.gguf_utils import (
+ _gguf_sequence_edge,
+ extract_vision_config_from_gguf,
+ maybe_patch_hf_config_from_gguf,
+ resolve_gguf_config_source,
+)
from vllm_gguf_plugin.quantization import (
GGUFUninitializedParameter,
GGUFWeightParameter,
GGUFWeightTypeParameter,
)
+from vllm_gguf_plugin.weights_adapter.default import (
+ GGUFWeightsAdapter,
+ _add_gemma4_gguf_mappings,
+ _add_gemma4_mtp_gguf_mappings,
+ _add_qwen3_5_mtp_gguf_mappings,
+)
+from vllm_gguf_plugin.weights_adapter.gemma4 import Gemma4GGUFAdapter
+from vllm_gguf_plugin.weights_adapter.qwen3_5 import Qwen3_5GGUFAdapter
def test_register_overrides_gguf_config():
register()
- quant_config = get_quantization_config("gguf")
+ quant_config = quantization_module.get_quantization_config("gguf")
assert quant_config is OOTGGUFConfig
@@ -49,13 +81,47 @@ def test_register_is_idempotent():
register()
register()
- assert get_quantization_config("gguf") is OOTGGUFConfig
+ assert quantization_module.get_quantization_config("gguf") is OOTGGUFConfig
assert isinstance(
get_model_loader(LoadConfig(load_format="gguf")), OOTGGUFModelLoader
)
assert isinstance(get_config_parser("gguf"), GGUFConfigParser)
+@pytest.mark.parametrize(
+ "script",
+ [
+ """
+import torch
+import vllm_gguf_plugin
+import vllm.model_executor.layers.quantization.gguf
+assert hasattr(torch.ops.vllm_gguf_plugin, "_fused_mul_mat_gguf")
+assert hasattr(torch.ops.vllm_gguf_plugin, "_fused_moe_gguf")
+assert hasattr(torch.ops.vllm_gguf_plugin, "_apply_gguf_embedding")
+""",
+ """
+import torch
+import vllm.model_executor.layers.quantization.gguf
+import vllm_gguf_plugin
+assert hasattr(torch.ops.vllm_gguf_plugin, "_fused_mul_mat_gguf")
+assert hasattr(torch.ops.vllm_gguf_plugin, "_fused_moe_gguf")
+assert hasattr(torch.ops.vllm_gguf_plugin, "_apply_gguf_embedding")
+""",
+ ],
+)
+def test_plugin_custom_ops_do_not_conflict_with_core_gguf_import(script):
+ subprocess.run([sys.executable, "-c", script], check=True)
+
+
+def test_register_patches_model_config_gguf_helper():
+ register()
+
+ assert (
+ model_config_module.maybe_patch_hf_config_from_gguf
+ is gguf_utils_module.maybe_patch_hf_config_from_gguf
+ )
+
+
def test_oot_config_reuses_in_tree_behavior():
quant_config = OOTGGUFConfig.from_config({})
@@ -64,6 +130,27 @@ def test_oot_config_reuses_in_tree_behavior():
assert repr(quant_config) == "GGUFConfig()"
+def test_gguf_override_quantization_method_accepts_hf_config_keyword():
+ register()
+
+ # hf_config keyword matches core QuantizationConfig.override_quantization_method
+ # signature.
+ # This was added to fix a TypeError when ModelConfig._verify_quantization() calls
+ # override_quantization_method(hf_quant_cfg, user_quant, hf_config=hf_config).
+ result_explicit = OOTGGUFConfig.override_quantization_method(
+ {}, "gguf", hf_config=object()
+ )
+ assert result_explicit == "gguf"
+
+ result_non_gguf = OOTGGUFConfig.override_quantization_method(
+ {}, "awq", hf_config=object()
+ )
+ assert result_non_gguf is None
+
+ result_none = OOTGGUFConfig.override_quantization_method({}, None)
+ assert result_none is None
+
+
def test_gguf_linear_uses_weight_loader_v2(monkeypatch):
register()
monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
@@ -101,6 +188,40 @@ def test_gguf_linear_uses_weight_loader_v2(monkeypatch):
assert layer.qweight_type.shard_weight_type == {0: 3, 1: 4}
+def test_gguf_linear_keeps_multi_shards_separate(monkeypatch):
+ register()
+ monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
+ monkeypatch.setattr(
+ parameter_module, "get_tensor_model_parallel_world_size", lambda: 1
+ )
+
+ layer = MergedColumnParallelLinear(
+ input_size=4,
+ output_sizes=[4, 4],
+ bias=False,
+ quant_config=OOTGGUFConfig.from_config({}),
+ disable_tp=True,
+ )
+ layer.weight_loader_v2(layer.qweight, torch.ones((4, 4), dtype=torch.uint8), 0)
+ layer.weight_loader_v2(layer.qweight, 2 * torch.ones((4, 4), dtype=torch.uint8), 1)
+ layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), 0)
+ layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), 1)
+
+ layer.quant_method.process_weights_after_loading(layer)
+
+ assert layer.qweight.numel() == 0
+ assert layer.qweight.shard_id == [0, 1]
+ assert layer.qweight.shard_id_map == {0: 0, 1: 1}
+ assert layer.qweight.shard_offset_map == {0: (0, 4, 4), 1: (4, 8, 4)}
+ assert len(layer.qweight.data_container) == 2
+ assert torch.equal(
+ layer.qweight.data_container[0], torch.ones((4, 4), dtype=torch.uint8)
+ )
+ assert torch.equal(
+ layer.qweight.data_container[1], 2 * torch.ones((4, 4), dtype=torch.uint8)
+ )
+
+
def test_gguf_embedding_uses_plugin_weight_loader(monkeypatch):
monkeypatch.setattr(
vocab_embedding_module, "get_tensor_model_parallel_rank", lambda: 0
@@ -172,145 +293,2184 @@ def fake_fused_mul_mat_gguf(x, qweight, qweight_type):
)
out = layer.quant_method.apply(layer, torch.ones((2, 4), dtype=torch.float32))
- assert calls == [((8, 4), 3)]
+ assert calls == [((4, 4), 3), ((4, 4), 3)]
assert out.shape == (2, 8)
-def test_gguf_config_parser_uses_parent_dir_for_local_file(tmp_path, monkeypatch):
- gguf_path = tmp_path / "model.gguf"
- gguf_path.write_bytes(b"GGUF")
- calls = {}
+def test_gguf_tuple_shard_loader_splits_fused_qweight(monkeypatch):
+ register()
+ monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
+ monkeypatch.setattr(
+ parameter_module, "get_tensor_model_parallel_world_size", lambda: 1
+ )
+
+ layer = MergedColumnParallelLinear(
+ input_size=4,
+ output_sizes=[4, 2, 6, 8],
+ bias=False,
+ quant_config=OOTGGUFConfig.from_config({}),
+ disable_tp=True,
+ )
+
+ fused_qkv = torch.cat(
+ [
+ torch.full((4, 4), 1, dtype=torch.uint8),
+ torch.full((2, 4), 2, dtype=torch.uint8),
+ torch.full((6, 4), 3, dtype=torch.uint8),
+ ],
+ dim=0,
+ )
+ layer.qweight.weight_loader(layer.qweight, fused_qkv, (0, 1, 2))
+ layer.qweight.weight_loader(
+ layer.qweight, torch.full((8, 4), 4, dtype=torch.uint8), 3
+ )
+ layer.qweight_type.weight_loader(
+ layer.qweight_type,
+ torch.tensor(WeightType.Q4_0, dtype=torch.uint8),
+ (0, 1, 2),
+ )
+ layer.qweight_type.weight_loader(
+ layer.qweight_type, torch.tensor(WeightType.Q4_1, dtype=torch.uint8), 3
+ )
+
+ layer.quant_method.process_weights_after_loading(layer)
+
+ assert layer.qweight.shard_id == [0, 1, 2, 3]
+ assert layer.qweight.shard_offset_map == {
+ 0: (0, 4, 4),
+ 1: (4, 6, 4),
+ 2: (6, 12, 4),
+ 3: (12, 20, 4),
+ }
+ assert layer.qweight.numel() == 0
+ assert torch.equal(
+ layer.qweight.data_container[0], torch.full((4, 4), 1, dtype=torch.uint8)
+ )
+ assert torch.equal(
+ layer.qweight.data_container[1], torch.full((2, 4), 2, dtype=torch.uint8)
+ )
+ assert torch.equal(
+ layer.qweight.data_container[2], torch.full((6, 4), 3, dtype=torch.uint8)
+ )
+ assert torch.equal(
+ layer.qweight.data_container[3], torch.full((8, 4), 4, dtype=torch.uint8)
+ )
+ assert layer.qweight_type.shard_weight_type == {
+ 0: WeightType.Q4_0,
+ 1: WeightType.Q4_0,
+ 2: WeightType.Q4_0,
+ 3: WeightType.Q4_1,
+ }
- def fake_parse(
- self, model, trust_remote_code, revision=None, code_revision=None, **kwargs
- ):
- calls["model"] = model
- calls["trust_remote_code"] = trust_remote_code
- return {}, PretrainedConfig(model_type="qwen3_moe")
+def test_gguf_row_parallel_weight_loader_v2_omits_empty_shard_id(monkeypatch):
+ register()
+ monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
monkeypatch.setattr(
- gguf_config_parser_module.HFConfigParser,
- "parse",
- fake_parse,
+ parameter_module, "get_tensor_model_parallel_world_size", lambda: 1
)
+ monkeypatch.setattr(gguf_params_module, "get_tensor_model_parallel_rank", lambda: 0)
monkeypatch.setattr(
- gguf_config_parser_module,
- "maybe_patch_hf_config_from_gguf",
- lambda model, config: config,
+ gguf_params_module, "get_tensor_model_parallel_world_size", lambda: 1
)
- config_dict, config = GGUFConfigParser().parse(gguf_path, trust_remote_code=False)
+ layer = RowParallelLinear(
+ input_size=4,
+ output_size=8,
+ bias=False,
+ quant_config=OOTGGUFConfig.from_config({}),
+ disable_tp=True,
+ )
- assert calls["model"] == gguf_path.parent
- assert calls["trust_remote_code"] is False
- assert config_dict["norm_topk_prob"] is True
- assert config.architectures == ["Qwen3MoeForCausalLM"]
+ layer.qweight.weight_loader(layer.qweight, torch.ones((8, 4), dtype=torch.uint8))
+ assert torch.equal(layer.qweight.data, torch.ones((8, 4), dtype=torch.uint8))
-def test_register_sets_engine_args_for_gguf_model(monkeypatch):
- register()
- captured = {}
- def fake_model_config(**kwargs):
- captured.update(kwargs)
- return kwargs
+def test_gemma4_adapter_transforms_quantized_moe_names():
+ adapter = Gemma4GGUFAdapter(PretrainedConfig(model_type="gemma4"))
+ weight = torch.empty((2, 3), dtype=torch.uint8)
- monkeypatch.setattr(arg_utils_module, "ModelConfig", fake_model_config)
- engine_args = EngineArgs(model="/tmp/model.gguf", tokenizer="/tmp/tokenizer")
+ transformed = dict(
+ adapter.map_weights(
+ [
+ (
+ "model.language_model.layers.0.experts.gate_up_proj.qweight",
+ weight,
+ ),
+ (
+ "model.language_model.layers.0.experts.gate_up_proj.qweight_type",
+ torch.tensor(1, dtype=torch.uint8),
+ ),
+ (
+ "model.language_model.layers.0.experts.down_proj.qweight",
+ weight,
+ ),
+ (
+ "model.language_model.layers.0.experts.down_proj.qweight_type",
+ torch.tensor(1, dtype=torch.uint8),
+ ),
+ ]
+ )
+ )
- engine_args.create_model_config()
+ assert (
+ "model.language_model.layers.0.moe.experts.routed_experts.w13_qweight"
+ in transformed
+ )
+ assert (
+ "model.language_model.layers.0.moe.experts.routed_experts.w13_qweight_type"
+ in transformed
+ )
+ assert (
+ "model.language_model.layers.0.moe.experts.routed_experts.w2_qweight"
+ in transformed
+ )
+ assert (
+ "model.language_model.layers.0.moe.experts.routed_experts.w2_qweight_type"
+ in transformed
+ )
- assert captured["config_format"] == "gguf"
- assert captured["model"] == "/tmp/tokenizer"
- assert captured["model_weights"] == "/tmp/model.gguf"
- assert captured["quantization"] == "gguf"
- assert engine_args.load_format == "gguf"
+def test_gemma4_gguf_mappings_match_current_hf_names():
+ config = PretrainedConfig(model_type="gemma4", num_hidden_layers=2)
+ config.vision_config = PretrainedConfig(num_hidden_layers=2)
+ mapping: dict[str, str] = {}
+ sideload_params = []
-def test_register_skips_speculator_probe_for_gguf():
- register()
+ _add_gemma4_gguf_mappings(config, mapping, sideload_params)
- model, tokenizer, speculative_config = (
- config_module.maybe_override_with_speculators(
- model="/tmp/model.gguf",
- tokenizer="/tmp/tokenizer",
- trust_remote_code=False,
- revision=None,
- vllm_speculative_config={"foo": "bar"},
- hf_token=None,
- )
+ assert mapping["blk.1.ffn_gate_inp.scale"] == (
+ "model.language_model.layers.1.router.scale"
+ )
+ assert mapping["blk.1.ffn_gate_inp.weight"] == (
+ "model.language_model.layers.1.router.proj.weight"
+ )
+ assert mapping["blk.1.ffn_down_exps.scale"] == (
+ "model.language_model.layers.1.router.per_expert_scale"
+ )
+ assert mapping["blk.1.ffn_gate_up_exps.weight"] == (
+ "model.language_model.layers.1.experts.gate_up_proj.weight"
+ )
+ assert mapping["blk.1.ffn_down_exps.weight"] == (
+ "model.language_model.layers.1.experts.down_proj.weight"
+ )
+ assert mapping["v.blk.1.ln1.weight"] == (
+ "model.vision_tower.encoder.layers.1.input_layernorm.weight"
+ )
+ assert mapping["v.blk.1.ln2.weight"] == (
+ "model.vision_tower.encoder.layers.1.pre_feedforward_layernorm.weight"
)
- assert model == "/tmp/model.gguf"
- assert tokenizer == "/tmp/tokenizer"
- assert speculative_config == {"foo": "bar"}
+def test_gemma4_text_only_does_not_add_vision_projector_mappings():
+ config = PretrainedConfig(
+ architectures=["Gemma4ForCausalLM"],
+ model_type="gemma4",
+ num_hidden_layers=2,
+ )
+ mapping: dict[str, str] = {}
+ sideload_params = []
-def test_gguf_qkv_shards_are_padded_in_qkv_order(monkeypatch):
- register()
- monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
+ _add_gemma4_gguf_mappings(config, mapping, sideload_params)
+
+ assert mapping["blk.1.ffn_gate_inp.scale"] == (
+ "model.layers.1.router.scale"
+ )
+ assert mapping["blk.1.ffn_gate_up_exps.weight"] == (
+ "model.layers.1.experts.gate_up_proj.weight"
+ )
+ assert "v.std_bias" not in mapping
+ assert "v.patch_embd.weight" not in mapping
+ assert "mm.input_projection.weight" not in mapping
+ assert "v.blk.1.ln1.weight" not in mapping
+
+
+def test_gemma4_causal_lm_with_vision_config_uses_text_layout():
+ config = PretrainedConfig(
+ architectures=["Gemma4ForCausalLM"],
+ model_type="gemma4",
+ num_hidden_layers=2,
+ )
+ config.vision_config = PretrainedConfig(num_hidden_layers=2)
+ mapping: dict[str, str] = {}
+ sideload_params = []
+
+ _add_gemma4_gguf_mappings(config, mapping, sideload_params)
+
+ assert mapping["blk.1.ffn_gate_inp.scale"] == (
+ "model.layers.1.router.scale"
+ )
+ assert "v.std_bias" not in mapping
+ assert "v.patch_embd.weight" not in mapping
+
+
+def test_gemma4_adapter_flattens_patch_embed_weight():
+ adapter = Gemma4GGUFAdapter(PretrainedConfig(model_type="gemma4"))
+ weight = torch.arange(2 * 3 * 4 * 5).reshape(2, 3, 4, 5)
+
+ transformed = adapter.transform_weight(
+ "model.vision_tower.patch_embedder.input_proj.weight",
+ weight,
+ )
+
+ assert transformed.shape == (2, 60)
+ assert torch.equal(transformed, weight.flatten(1))
+
+
+def test_gemma4_mtp_gguf_mappings():
+ config = PretrainedConfig(model_type="gemma4_assistant", num_hidden_layers=2)
+ mapping: dict[str, str] = {}
+
+ _add_gemma4_mtp_gguf_mappings(config, mapping)
+
+ assert mapping["token_embd.weight"] == "model.embed_tokens.weight"
+ assert mapping["nextn.pre_projection.weight"] == "model.pre_projection.weight"
+ assert mapping["nextn.post_projection.weight"] == "model.post_projection.weight"
+ assert mapping["blk.1.attn_q.weight"] == ("model.layers.1.self_attn.q_proj.weight")
+ assert "blk.1.attn_k.weight" not in mapping
+ assert "blk.1.attn_v.weight" not in mapping
+ assert mapping["blk.1.ffn_gate.weight"] == ("model.layers.1.mlp.gate_proj.weight")
+ assert mapping["blk.1.layer_output_scale.weight"] == "model.layers.1.layer_scalar"
+
+
+def test_gguf_sequence_edge_accepts_scalar_and_sequence_values():
+ assert _gguf_sequence_edge(None, first=True) is None
+ assert _gguf_sequence_edge(8, first=True) == 8
+ assert _gguf_sequence_edge(8, first=False) == 8
+ assert _gguf_sequence_edge([8, 8, 8, 2], first=True) == 8
+ assert _gguf_sequence_edge([8, 8, 8, 2], first=False) == 2
+
+
+class _FakeGGUFField:
+ def __init__(self, value):
+ self.value = value
+ self.parts = [value]
+
+ def contents(self):
+ return self.value
+
+
+class _FakeGGUFReader:
+ def __init__(self, fields):
+ self.fields = {key: _FakeGGUFField(value) for key, value in fields.items()}
+ self.tensors = []
+
+ def get_field(self, key):
+ return self.fields.get(key)
+
+
+def test_build_tokenizer_from_gguf_metadata_uses_arch_alias_and_cache(
+ tmp_path,
+ monkeypatch,
+):
+ gguf_path = tmp_path / "model.gguf"
+ mmproj_path = tmp_path / "mmproj.gguf"
+ gguf_path.write_bytes(b"GGUF")
+ mmproj_path.write_bytes(b"GGUF")
+ qwen_mm_tokens = [
+ "<|vision_start|>",
+ "<|vision_end|>",
+ "<|vision_pad|>",
+ "<|image_pad|>",
+ "<|video_pad|>",
+ ]
+ qwen_control_tokens = [
+ "",
+ "",
+ "",
+ "",
+ ]
+ main_reader = _FakeGGUFReader(
+ {
+ "general.architecture": "qwen35moe",
+ "tokenizer.ggml.tokens": [
+ "",
+ "",
+ "",
+ "hello",
+ *qwen_mm_tokens,
+ *qwen_control_tokens,
+ "[PAD000]",
+ ],
+ "tokenizer.ggml.token_type": [
+ 3,
+ 3,
+ 3,
+ 1,
+ *([3] * len(qwen_mm_tokens)),
+ *([4] * len(qwen_control_tokens)),
+ 5,
+ ],
+ "tokenizer.ggml.model": "gpt2",
+ "tokenizer.ggml.merges": ["h ello"],
+ "tokenizer.ggml.bos_token_id": 1,
+ "tokenizer.ggml.eos_token_id": 2,
+ "tokenizer.ggml.padding_token_id": 0,
+ "tokenizer.chat_template": "{{ messages }}",
+ }
+ )
+ mmproj_reader = _FakeGGUFReader(
+ {
+ "general.architecture": "clip",
+ "general.type": "mmproj",
+ "clip.vision.patch_size": 16,
+ "clip.vision.spatial_merge_size": 2,
+ "clip.vision.temporal_patch_size": 2,
+ }
+ )
+ calls = []
+
+ class FakeTokenizer:
+ def __init__(self, *args, **kwargs):
+ calls.append(("fast", kwargs))
+ self.chat_template = None
+
+ def save_pretrained(self, path):
+ (path / "tokenizer.json").write_text("{}", encoding="utf-8")
+ (path / "tokenizer_config.json").write_text("{}", encoding="utf-8")
+
+ def fake_convert(architecture, tokenizer_dict):
+ calls.append(("convert", architecture, tokenizer_dict))
+ return object(), {}
+
+ def fake_gguf_reader(path):
+ return mmproj_reader if Path(path) == mmproj_path else main_reader
+
+ monkeypatch.setenv(
+ "VLLM_GGUF_TOKENIZER_CACHE",
+ str(tmp_path / "tokenizer-cache"),
+ )
monkeypatch.setattr(
- parameter_module, "get_tensor_model_parallel_world_size", lambda: 1
+ gguf_tokenizer_builder_module.gguf,
+ "GGUFReader",
+ fake_gguf_reader,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "convert_gguf_tokenizer",
+ fake_convert,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "PreTrainedTokenizerFast",
+ FakeTokenizer,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "detect_gguf_multimodal",
+ lambda model: mmproj_path,
)
- layer = QKVParallelLinear(
- hidden_size=4,
- head_size=2,
- total_num_heads=2,
- total_num_kv_heads=1,
- bias=False,
- quant_config=OOTGGUFConfig.from_config({}),
- disable_tp=True,
+ tokenizer_path = build_tokenizer_from_gguf(gguf_path)
+
+ assert tokenizer_path is not None
+ tokenizer_cache = Path(tokenizer_path)
+ assert (tmp_path / "tokenizer-cache").is_dir()
+ processor_config = json.loads(
+ (tokenizer_cache / "processor_config.json").read_text(encoding="utf-8")
+ )
+ preprocessor_config = json.loads(
+ (tokenizer_cache / "preprocessor_config.json").read_text(encoding="utf-8")
+ )
+ video_config = json.loads(
+ (tokenizer_cache / "video_preprocessor_config.json").read_text(encoding="utf-8")
)
+ assert processor_config["processor_class"] == "Qwen3VLProcessor"
+ assert processor_config["image_processor"]["patch_size"] == 16
+ assert preprocessor_config["image_processor_type"] == "Qwen2VLImageProcessor"
+ assert preprocessor_config["merge_size"] == 2
+ assert video_config["video_processor_type"] == "Qwen3VLVideoProcessor"
+ tokenizer_config = json.loads(
+ (tokenizer_cache / "tokenizer_config.json").read_text(encoding="utf-8")
+ )
+ assert tokenizer_config["additional_special_tokens"] == [
+ *qwen_mm_tokens,
+ *qwen_control_tokens,
+ ]
+ assert tokenizer_config["image_token"] == "<|image_pad|>"
+ assert tokenizer_config["video_token"] == "<|video_pad|>"
+ assert calls[0][0] == "convert"
+ assert calls[0][1] == "qwen3_moe"
+ assert calls[0][2]["tokens"] == [
+ "",
+ "",
+ "",
+ "hello",
+ *qwen_mm_tokens,
+ *qwen_control_tokens,
+ "[PAD000]",
+ ]
+ assert calls[0][2]["token_type"] == [
+ 3,
+ 3,
+ 3,
+ 1,
+ *([3] * len(qwen_mm_tokens)),
+ *([4] * len(qwen_control_tokens)),
+ 5,
+ ]
+ assert calls[1][1]["bos_token"] == ""
+ assert calls[1][1]["eos_token"] == ""
+ assert calls[1][1]["pad_token"] == ""
- q = torch.full((4, 4), 1, dtype=torch.uint8)
- k = torch.full((2, 4), 2, dtype=torch.uint8)
- v = torch.full((2, 4), 3, dtype=torch.uint8)
- # Load out of canonical order to match GGUF tensor iteration order.
- layer.weight_loader_v2(layer.qweight, k, "k")
- layer.weight_loader_v2(layer.qweight, q, "q")
- layer.weight_loader_v2(layer.qweight, v, "v")
- layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), "k")
- layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), "q")
- layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), "v")
+ def fail_convert(*args, **kwargs):
+ raise AssertionError("cached tokenizer should not call converter again")
- layer.quant_method.process_weights_after_loading(layer)
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "convert_gguf_tokenizer",
+ fail_convert,
+ )
+ (tokenizer_cache / "tokenizer_config.json").write_text("{}", encoding="utf-8")
+ (tokenizer_cache / "preprocessor_config.json").unlink()
+ assert build_tokenizer_from_gguf(gguf_path) == tokenizer_path
+ assert (tokenizer_cache / "preprocessor_config.json").is_file()
+ cached_tokenizer_config = json.loads(
+ (tokenizer_cache / "tokenizer_config.json").read_text(encoding="utf-8")
+ )
+ assert cached_tokenizer_config["additional_special_tokens"] == [
+ *qwen_mm_tokens,
+ *qwen_control_tokens,
+ ]
- assert layer.qweight.shard_id == ["q", "k", "v"]
- assert layer.qweight.shard_offset_map == {
- "q": (0, 4, 4),
- "k": (4, 6, 4),
- "v": (6, 8, 4),
- }
- assert torch.equal(layer.qweight[:4], q)
- assert torch.equal(layer.qweight[4:6], k)
- assert torch.equal(layer.qweight[6:8], v)
+def test_build_tokenizer_from_qwen35_gguf_uses_dense_arch_alias(
+ tmp_path,
+ monkeypatch,
+):
+ gguf_path = tmp_path / "model.gguf"
+ gguf_path.write_bytes(b"GGUF")
+ main_reader = _FakeGGUFReader(
+ {
+ "general.architecture": "qwen35",
+ "tokenizer.ggml.tokens": ["", "", "", "hello"],
+ "tokenizer.ggml.model": "gpt2",
+ "tokenizer.ggml.merges": ["h ello"],
+ "tokenizer.ggml.bos_token_id": 1,
+ "tokenizer.ggml.eos_token_id": 2,
+ "tokenizer.ggml.padding_token_id": 0,
+ }
+ )
+ calls = []
-def test_gguf_linear_preserves_cuda_weight_device(monkeypatch):
- if not torch.cuda.is_available():
- return
+ class FakeTokenizer:
+ def __init__(self, *args, **kwargs):
+ calls.append(("fast", kwargs))
+ self.chat_template = None
- register()
- monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
+ def save_pretrained(self, path):
+ (path / "tokenizer.json").write_text("{}", encoding="utf-8")
+ (path / "tokenizer_config.json").write_text("{}", encoding="utf-8")
+
+ def fake_convert(architecture, tokenizer_dict):
+ calls.append(("convert", architecture, tokenizer_dict))
+ return object(), {}
+
+ monkeypatch.setenv(
+ "VLLM_GGUF_TOKENIZER_CACHE",
+ str(tmp_path / "tokenizer-cache"),
+ )
monkeypatch.setattr(
- parameter_module, "get_tensor_model_parallel_world_size", lambda: 1
+ gguf_tokenizer_builder_module.gguf,
+ "GGUFReader",
+ lambda path: main_reader,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "convert_gguf_tokenizer",
+ fake_convert,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "PreTrainedTokenizerFast",
+ FakeTokenizer,
)
- with torch.device("cuda"):
- layer = MergedColumnParallelLinear(
- input_size=4,
- output_sizes=[4, 4],
- bias=False,
- quant_config=OOTGGUFConfig.from_config({}),
- disable_tp=True,
- )
+ tokenizer_path = build_tokenizer_from_gguf(gguf_path)
- layer.weight_loader_v2(layer.qweight, torch.ones((4, 4), dtype=torch.uint8), 0)
- layer.weight_loader_v2(layer.qweight, 2 * torch.ones((4, 4), dtype=torch.uint8), 1)
- layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), 0)
- layer.weight_loader_v2(layer.qweight_type, torch.tensor(3, dtype=torch.uint8), 1)
- layer.quant_method.process_weights_after_loading(layer)
+ assert tokenizer_path is not None
+ assert calls[0][0] == "convert"
+ assert calls[0][1] == "qwen3"
- assert layer.qweight.device.type == "cuda"
- assert layer.qweight_type.device.type == "cuda"
+
+def test_build_tokenizer_from_gguf_returns_none_when_cache_key_stat_fails(
+ tmp_path,
+ monkeypatch,
+):
+ gguf_path = tmp_path / "missing.gguf"
+
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "check_gguf_file",
+ lambda model: True,
+ )
+
+ def fail_reader(*args, **kwargs):
+ raise AssertionError("stat failure must return before reading GGUF")
+
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module.gguf,
+ "GGUFReader",
+ fail_reader,
+ )
+
+ assert build_tokenizer_from_gguf(gguf_path) is None
+
+
+def test_build_tokenizer_from_gguf_copies_local_sidecars_first(
+ tmp_path,
+ monkeypatch,
+):
+ gguf_path = tmp_path / "model.gguf"
+ gguf_path.write_bytes(b"GGUF")
+ local_preprocessor = {"processor_class": "LocalProcessor"}
+ (tmp_path / "preprocessor_config.json").write_text(
+ json.dumps(local_preprocessor),
+ encoding="utf-8",
+ )
+ fake_reader = _FakeGGUFReader(
+ {
+ "general.architecture": "gemma4",
+ "tokenizer.ggml.tokens": ["", "", "", "hello"],
+ "tokenizer.ggml.model": "gpt2",
+ "tokenizer.ggml.merges": ["h ello"],
+ }
+ )
+
+ class FakeTokenizer:
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def save_pretrained(self, path):
+ (path / "tokenizer.json").write_text("{}", encoding="utf-8")
+
+ monkeypatch.setenv(
+ "VLLM_GGUF_TOKENIZER_CACHE",
+ str(tmp_path / "tokenizer-cache"),
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module.gguf,
+ "GGUFReader",
+ lambda path: fake_reader,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "convert_gguf_tokenizer",
+ lambda architecture, tokenizer_dict: (object(), {}),
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "PreTrainedTokenizerFast",
+ FakeTokenizer,
+ )
+ monkeypatch.setattr(
+ gguf_tokenizer_builder_module,
+ "detect_gguf_multimodal",
+ lambda model: None,
+ )
+
+ tokenizer_path = build_tokenizer_from_gguf(gguf_path)
+
+ assert tokenizer_path is not None
+ copied = json.loads(
+ (Path(tokenizer_path) / "preprocessor_config.json").read_text(encoding="utf-8")
+ )
+ assert copied == local_preprocessor
+
+
+def test_build_tokenizer_from_gguf_patches_gemma4_special_tokens(
+ tmp_path,
+ monkeypatch,
+):
+ gguf_path = tmp_path / "model.gguf"
+ gguf_path.write_bytes(b"GGUF")
+ fake_reader = _FakeGGUFReader(
+ {
+ "general.architecture": "gemma4",
+ "tokenizer.ggml.tokens": [
+ "",
+ "",
+ "",
+ "<|image>",
+ "<|image|>",
+ "",
+ "<|audio>",
+ "<|audio|>",
+ "