convert : add compressed-tensors NVFP4 support (#21095)
* Refactored Compressed Tensors NVFP4 support for new base.py * Support compressed-tensors NVFP4 conversion * Moved Qwen MTP remap into filter_tensors * simplify * pathlib no longer used --------- Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@scala.com>
This commit is contained in:
co-authored by
Sigbjørn Skjæret
parent
d161ea7071
commit
a4d2d4ae41
+53
-6
@@ -467,7 +467,14 @@ class ModelBase:
|
|||||||
elif quant_method == "compressed-tensors":
|
elif quant_method == "compressed-tensors":
|
||||||
quant_format = quant_config["format"]
|
quant_format = quant_config["format"]
|
||||||
groups = quant_config["config_groups"]
|
groups = quant_config["config_groups"]
|
||||||
if len(groups) > 1:
|
nvfp4_compressed_tensors = (
|
||||||
|
quant_format == "nvfp4-pack-quantized"
|
||||||
|
or quant_format == "mixed-precision"
|
||||||
|
and bool(groups)
|
||||||
|
and all(g.get("format") == "nvfp4-pack-quantized" for g in groups.values() if isinstance(g, dict))
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(groups) > 1 and not nvfp4_compressed_tensors:
|
||||||
raise NotImplementedError("Can't handle multiple config groups for compressed-tensors yet")
|
raise NotImplementedError("Can't handle multiple config groups for compressed-tensors yet")
|
||||||
weight_config = tuple(groups.values())[0]["weights"]
|
weight_config = tuple(groups.values())[0]["weights"]
|
||||||
|
|
||||||
@@ -505,6 +512,9 @@ class ModelBase:
|
|||||||
tensors_to_remove += [base_name + n for n in ("_packed", "_shape", "_scale")]
|
tensors_to_remove += [base_name + n for n in ("_packed", "_shape", "_scale")]
|
||||||
if (base_name + "_zero_point") in self.model_tensors:
|
if (base_name + "_zero_point") in self.model_tensors:
|
||||||
tensors_to_remove.append(base_name + "_zero_point")
|
tensors_to_remove.append(base_name + "_zero_point")
|
||||||
|
elif nvfp4_compressed_tensors:
|
||||||
|
# Don't error from compressed-tensors, we'll handle them in _generate_nvfp4_tensors
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Quant format {quant_format!r} for method {quant_method!r} is not yet supported")
|
raise NotImplementedError(f"Quant format {quant_format!r} for method {quant_method!r} is not yet supported")
|
||||||
elif quant_method == "modelopt":
|
elif quant_method == "modelopt":
|
||||||
@@ -746,10 +756,13 @@ class ModelBase:
|
|||||||
del experts, merged
|
del experts, merged
|
||||||
|
|
||||||
def prepare_tensors(self):
|
def prepare_tensors(self):
|
||||||
# detect NVFP4 quantization (ModelOpt format)
|
# detect NVFP4 quantization (ModelOpt and Compressed-tensors formats)
|
||||||
quant_algo = (self.hparams.get("quantization_config") or {}).get("quant_algo")
|
quantization_config = self.hparams.get("quantization_config") or {}
|
||||||
quant_method = (self.hparams.get("quantization_config") or {}).get("quant_method")
|
quant_algo = quantization_config.get("quant_algo")
|
||||||
quant_layers = (self.hparams.get("quantization_config") or {}).get("quantized_layers") or {}
|
quant_method = quantization_config.get("quant_method")
|
||||||
|
quant_format = quantization_config.get("format")
|
||||||
|
quant_groups = quantization_config.get("config_groups") or {}
|
||||||
|
quant_layers = quantization_config.get("quantized_layers") or {}
|
||||||
quant_config_file = self.dir_model / "hf_quant_config.json"
|
quant_config_file = self.dir_model / "hf_quant_config.json"
|
||||||
|
|
||||||
if (not quant_algo or not quant_layers) and quant_config_file.is_file():
|
if (not quant_algo or not quant_layers) and quant_config_file.is_file():
|
||||||
@@ -760,13 +773,25 @@ class ModelBase:
|
|||||||
producer_name = (producer.get("name") or "").lower()
|
producer_name = (producer.get("name") or "").lower()
|
||||||
if quant_method is None:
|
if quant_method is None:
|
||||||
self.hparams.setdefault("quantization_config", {})["quant_method"] = producer_name
|
self.hparams.setdefault("quantization_config", {})["quant_method"] = producer_name
|
||||||
|
quant_method = producer_name
|
||||||
quant_algo = quant_config.get("quant_algo", quant_algo)
|
quant_algo = quant_config.get("quant_algo", quant_algo)
|
||||||
|
quant_method = quant_config.get("quant_method", quant_method)
|
||||||
|
quant_format = quant_config.get("format", quant_format)
|
||||||
|
quant_groups = quant_config.get("config_groups", quant_groups) or {}
|
||||||
quant_layers = quant_config.get("quantized_layers", quant_layers) or {}
|
quant_layers = quant_config.get("quantized_layers", quant_layers) or {}
|
||||||
|
|
||||||
# Some models use per-tensor quant_algo (e.g. "MIXED_PRECISION" with
|
# Some models use per-tensor quant_algo (e.g. "MIXED_PRECISION" with
|
||||||
# per-layer NVFP4/FP8) instead of a single global "NVFP4" value.
|
# per-layer NVFP4/FP8) instead of a single global "NVFP4" value.
|
||||||
|
nvfp4_compressed_tensors = quant_method == "compressed-tensors" and (
|
||||||
|
quant_format == "nvfp4-pack-quantized"
|
||||||
|
or quant_format == "mixed-precision"
|
||||||
|
and bool(quant_groups)
|
||||||
|
and all(g.get("format") == "nvfp4-pack-quantized" for g in quant_groups.values() if isinstance(g, dict))
|
||||||
|
)
|
||||||
if quant_algo != "NVFP4":
|
if quant_algo != "NVFP4":
|
||||||
if any(v.get("quant_algo") == "NVFP4" for v in quant_layers.values() if isinstance(v, dict)):
|
if nvfp4_compressed_tensors:
|
||||||
|
quant_algo = "NVFP4"
|
||||||
|
elif any(v.get("quant_algo") == "NVFP4" for v in quant_layers.values() if isinstance(v, dict)):
|
||||||
quant_algo = "NVFP4"
|
quant_algo = "NVFP4"
|
||||||
|
|
||||||
self._is_nvfp4 = quant_algo == "NVFP4"
|
self._is_nvfp4 = quant_algo == "NVFP4"
|
||||||
@@ -776,6 +801,28 @@ class ModelBase:
|
|||||||
# This must run before dequant_model so NVFP4 tensors are removed
|
# This must run before dequant_model so NVFP4 tensors are removed
|
||||||
# from model_tensors, leaving only non-NVFP4 (e.g. FP8) for dequant.
|
# from model_tensors, leaving only non-NVFP4 (e.g. FP8) for dequant.
|
||||||
if self._is_nvfp4:
|
if self._is_nvfp4:
|
||||||
|
if nvfp4_compressed_tensors:
|
||||||
|
# Convert compressed-tensors 'global' scales into the reciprocal
|
||||||
|
def inverse_scale(gen):
|
||||||
|
def load():
|
||||||
|
scale = LazyTorchTensor.to_eager(gen()).float()
|
||||||
|
return 1.0 / scale
|
||||||
|
return load
|
||||||
|
|
||||||
|
# Change the compressed-tensors names to the ModelOpt names for handling consistently later
|
||||||
|
for name in list(self.model_tensors.keys()):
|
||||||
|
if name.endswith(".weight_packed"):
|
||||||
|
weight_name = name.removesuffix("_packed")
|
||||||
|
if weight_name not in self.model_tensors:
|
||||||
|
self.model_tensors[weight_name] = self.model_tensors.pop(name)
|
||||||
|
elif name.endswith(".weight_global_scale"):
|
||||||
|
scale2_name = name.replace(".weight_global_scale", ".weight_scale_2")
|
||||||
|
if scale2_name not in self.model_tensors:
|
||||||
|
self.model_tensors[scale2_name] = inverse_scale(self.model_tensors.pop(name))
|
||||||
|
elif name.endswith(".input_global_scale"):
|
||||||
|
input_scale_name = name.replace(".input_global_scale", ".input_scale")
|
||||||
|
if input_scale_name not in self.model_tensors:
|
||||||
|
self.model_tensors[input_scale_name] = inverse_scale(self.model_tensors.pop(name))
|
||||||
self._generate_nvfp4_tensors()
|
self._generate_nvfp4_tensors()
|
||||||
|
|
||||||
self.dequant_model()
|
self.dequant_model()
|
||||||
|
|||||||
+29
-30
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
@@ -549,6 +548,7 @@ class _Qwen35MtpMixin:
|
|||||||
tensor_map: gguf.TensorNameMap
|
tensor_map: gguf.TensorNameMap
|
||||||
no_mtp: bool
|
no_mtp: bool
|
||||||
mtp_only: bool
|
mtp_only: bool
|
||||||
|
_original_block_count: int | None = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -557,22 +557,44 @@ class _Qwen35MtpMixin:
|
|||||||
self.block_count += self.hparams.get("mtp_num_hidden_layers", 0)
|
self.block_count += self.hparams.get("mtp_num_hidden_layers", 0)
|
||||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
||||||
|
|
||||||
|
def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
|
||||||
|
hparams = {**self.hparams, **self.hparams.get("text_config", {})}
|
||||||
|
key = next((k for k in ["n_layers", "num_hidden_layers", "n_layer", "num_layers"] if k in hparams), None)
|
||||||
|
type(self)._original_block_count = hparams.get(key)
|
||||||
|
return super().index_tensors(remote_hf_model_id=remote_hf_model_id) # ty: ignore[unresolved-attribute]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_tensors(cls, item):
|
def filter_tensors(cls, item):
|
||||||
name, _ = item
|
assert cls._original_block_count is not None
|
||||||
|
# TODO: change TextModel to super()
|
||||||
|
if (titem := TextModel.filter_tensors(item)) is None:
|
||||||
|
return None
|
||||||
|
name, gen = titem
|
||||||
|
if name.startswith("model.mtp."):
|
||||||
|
name = name.replace("model.", "", 1)
|
||||||
if name.startswith("mtp."):
|
if name.startswith("mtp."):
|
||||||
if cls.no_mtp:
|
if cls.no_mtp:
|
||||||
return None
|
return None
|
||||||
return item
|
remapper = {
|
||||||
if cls.mtp_only:
|
"fc": "eh_proj",
|
||||||
canonical = name.replace("language_model.", "")
|
"pre_fc_norm_embedding": "enorm",
|
||||||
keep = canonical in (
|
"pre_fc_norm_hidden": "hnorm",
|
||||||
|
"norm": "shared_head.norm",
|
||||||
|
}
|
||||||
|
parts = name.split(".", 3)
|
||||||
|
if len(parts) == 4 and parts[1] == "layers" and parts[2].isdecimal():
|
||||||
|
mtp_idx = int(parts[2])
|
||||||
|
name = f"model.layers.{cls._original_block_count + mtp_idx}.{parts[3]}"
|
||||||
|
elif len(parts) == 3 and parts[1] in remapper:
|
||||||
|
name = f"model.layers.{cls._original_block_count}.{remapper[parts[1]]}.{parts[2]}"
|
||||||
|
elif cls.mtp_only:
|
||||||
|
keep = name in (
|
||||||
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
|
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
|
||||||
"embed_tokens.weight", "norm.weight",
|
"embed_tokens.weight", "norm.weight",
|
||||||
)
|
)
|
||||||
if not keep:
|
if not keep:
|
||||||
return None
|
return None
|
||||||
return super().filter_tensors(item) # ty: ignore[unresolved-attribute]
|
return name, gen
|
||||||
|
|
||||||
def set_gguf_parameters(self):
|
def set_gguf_parameters(self):
|
||||||
super().set_gguf_parameters() # ty: ignore[unresolved-attribute]
|
super().set_gguf_parameters() # ty: ignore[unresolved-attribute]
|
||||||
@@ -594,29 +616,6 @@ class _Qwen35MtpMixin:
|
|||||||
self.metadata.version, size_label=None, output_type=output_type, model_type=None) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
self.metadata.version, size_label=None, output_type=output_type, model_type=None) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
||||||
self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
|
self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
|
||||||
|
|
||||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
|
||||||
if name.startswith("mtp."):
|
|
||||||
n_layer = self.hparams["num_hidden_layers"]
|
|
||||||
if name.find("layers.") != -1:
|
|
||||||
assert bid is not None
|
|
||||||
name = name.replace(f"mtp.layers.{bid}", f"model.layers.{bid + n_layer}")
|
|
||||||
bid = bid + n_layer
|
|
||||||
else:
|
|
||||||
remapper = {
|
|
||||||
"mtp.fc": "model.layers.{bid}.eh_proj",
|
|
||||||
"mtp.pre_fc_norm_embedding": "model.layers.{bid}.enorm",
|
|
||||||
"mtp.pre_fc_norm_hidden": "model.layers.{bid}.hnorm",
|
|
||||||
"mtp.norm": "model.layers.{bid}.shared_head.norm",
|
|
||||||
}
|
|
||||||
stem = Path(name).stem
|
|
||||||
suffix = Path(name).suffix
|
|
||||||
tmpl = remapper[stem] + suffix
|
|
||||||
for b in range(n_layer, self.block_count):
|
|
||||||
yield from super().modify_tensors(data_torch, tmpl.format(bid=b), b) # ty: ignore[unresolved-attribute]
|
|
||||||
return
|
|
||||||
|
|
||||||
yield from super().modify_tensors(data_torch, name, bid) # ty: ignore[unresolved-attribute]
|
|
||||||
|
|
||||||
|
|
||||||
@ModelBase.register("Qwen3_5ForConditionalGeneration", "Qwen3_5ForCausalLM")
|
@ModelBase.register("Qwen3_5ForConditionalGeneration", "Qwen3_5ForCausalLM")
|
||||||
class Qwen3_5TextModel(_Qwen35MtpMixin, _Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
class Qwen3_5TextModel(_Qwen35MtpMixin, _Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
||||||
|
|||||||
Reference in New Issue
Block a user