* WIP OCP FP8 E4M3 support * Fix python type-checker * Materialize input_scales also for FP8 when converting * Fix conversion of https://huggingface.co/RedHatAI/Qwen3.8-27B-NVFP4 1. Per row FP8 wrongly fell to NVFP4 conversion, as NVFP4 checked on dims alone. Also check on dtype for FP8 2. Need to reshape FP8 QKV projections scales in the same way that weights are reshaped * Allow remaining FP8 tensors to be dequantized during conversion * Allow tensor-strategy for FP8-only compressed-tensor ckpts * Fix and test gguf-py behavior for FP8_E4M3 * Disallow QKV-fusion for FP8 temporarily * Fix conversion of fused qkvz & FP8 for Qwen3-Next * Avoid fusing FP8 for --fuse-gate-up-exps as we may have different ws * Emit warnings when not fusing FP8 during conversion * More FP8 conversion fixes * Add supported for strided copies to/from FP8 in CPU * remove unneeded scratch buffer from FP8 cpu cpy/dup path * add explanatory comments and TODOs * Add dedicated encoder and decoder tests for FP8 * Whitelist ggml_op_get_rows in metal instead of blacklisting required to fix failing CI, and white-listing should be more future-proof than adding FP8 to the black-list https://github.com/ggml-org/llama.cpp/actions/runs/38433916498/job/115422880633?pr=27512 * Fix more conversion bugs * Defer weight materialization during conversion * Blacklist fp8 in metal backend instead of white-list * Adjust indents * Fix lint + execute tests * Add fast CPU kernel for F8_E4M3 * Conversion fixes * Fix remaining gaps * Enable tied weights with shared scales * Make mandatory-F32 rules apply to FP8 during conversion * Fix python type checks https://github.com/ggml-org/llama.cpp/actions/runs/39765670052/job/119780379595?pr=27512 * Fix kaleidai unit-test failures https://github.com/ggml-org/llama.cpp/actions/runs/39765670125/job/119780383150 * Relax numeric precision constraints for pre-check * Leave fast-FP8 CPU on RISC-V for follow-up We already exclude arm from the fast-path * Allow output scale tensors to be located in any gguf shard * Move test_conversion_fp8.py test execution to behavior-check.yaml * Apply batched suggestions from code review Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * Move mixed compressed-tensor logic into dequant_fp8 + add unit-tests for it * Install all requirements also on Windows for behavior-check Fixes https://github.com/ggml-org/llama.cpp/actions/runs/40535852590/job/122300758223?pr=27512 * Unmap lazy tensors before clean-up in Windows Should fix https://github.com/ggml-org/llama.cpp/actions/runs/40539797824/job/122314782459?pr=27512 * LF line endings https://github.com/ggml-org/llama.cpp/actions/runs/40539798168/job/122314791118?pr=27512 * Extract FP8 tests from llama-arch to its own test-fixture See https://github.com/ggml-org/llama.cpp/pull/27512#discussion_r4400628221 * Update gguf-py/gguf/scripts/gguf_convert_endian.py Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> --------- Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
371 lines
21 KiB
Python
371 lines
21 KiB
Python
"""Test FP8 conversion with small synthetic checkpoints; no GPU or model downloads required.
|
|
|
|
Install dependencies and run from the repository root:
|
|
python -m pip install -r requirements/requirements-convert_hf_to_gguf.txt
|
|
python -m unittest discover -s tests -p 'test_conversion_fp8.py' -v
|
|
|
|
Use unittest discovery so the repository root is available for imports.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
|
|
import numpy as np
|
|
import torch
|
|
from safetensors.torch import save_file
|
|
|
|
from conversion.base import LazyTorchTensor, gguf
|
|
from conversion.deepseek import DeepseekV2Model
|
|
from conversion.llama import LlamaModel
|
|
from conversion.nemotron import NemotronHModel
|
|
from conversion.phi import PhiMoeModel
|
|
from conversion.qwen import Qwen2MoeModel, Qwen3NextModel
|
|
|
|
|
|
class ForcedF16Model(Qwen2MoeModel):
|
|
model_arch = gguf.MODEL_ARCH.QWEN2MOE
|
|
|
|
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
|
if name.endswith("q_proj.weight"):
|
|
return gguf.GGMLQuantizationType.F16
|
|
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
|
|
|
|
|
class OffsetNormModel(Qwen2MoeModel):
|
|
model_arch = gguf.MODEL_ARCH.QWEN2MOE
|
|
|
|
def modify_tensors(self, data_torch, name, bid):
|
|
if name.endswith("input_layernorm.weight"):
|
|
data_torch = data_torch + 1.0
|
|
yield from super().modify_tensors(data_torch, name, bid)
|
|
|
|
|
|
class TestFP8Conversion(unittest.TestCase):
|
|
def convert(self, model_class, tensors, eager=False, check_lazy=False, scan_nvfp4=False, ftype=gguf.LlamaFileType.GUESSED, hparams_override=None, **kwargs):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
return self._convert(Path(directory), model_class, tensors, eager, check_lazy, scan_nvfp4, ftype, hparams_override, **kwargs)
|
|
|
|
def _convert(self, path, model_class, tensors, eager, check_lazy, scan_nvfp4, ftype, hparams_override, **kwargs):
|
|
hparams = {
|
|
"model_type": "qwen2_moe",
|
|
"architectures": ["Qwen2MoeForCausalLM"],
|
|
"num_hidden_layers": 1,
|
|
"hidden_size": 4,
|
|
"num_attention_heads": 1,
|
|
"num_key_value_heads": 1,
|
|
"num_experts": 2,
|
|
"qk_nope_head_dim": 2,
|
|
"v_head_dim": 2,
|
|
"quantization_config": {"quant_method": "modelopt"},
|
|
}
|
|
hparams.update(hparams_override or {})
|
|
(path / "config.json").write_text(json.dumps(hparams))
|
|
save_file(tensors, str(path / "model.safetensors"))
|
|
model = model_class(path, ftype, path / "out.gguf", hparams=hparams, eager=eager, **kwargs)
|
|
reads: list[str] = []
|
|
if check_lazy:
|
|
self.assertFalse(eager)
|
|
|
|
def track(name, gen):
|
|
tensor = gen()
|
|
|
|
def load(source):
|
|
reads.append(name)
|
|
return LazyTorchTensor.to_eager(source())
|
|
|
|
return LazyTorchTensor(meta=torch.empty(tuple(tensor.shape), dtype=tensor.dtype, device="meta"), args=(gen,), func=load)
|
|
|
|
model.model_tensors = {
|
|
name: lambda name=name, gen=gen: track(name, gen)
|
|
for name, gen in model.model_tensors.items()
|
|
}
|
|
if scan_nvfp4:
|
|
model._generate_nvfp4_tensors()
|
|
self.assertEqual(reads, [])
|
|
model.prepare_tensors()
|
|
if check_lazy:
|
|
deferred = {name for name, tensor in tensors.items() if tensor.numel() != 1 or name.endswith((".weight", ".bias"))}
|
|
self.assertFalse(deferred.intersection(reads), f"Materialized during preparation: {reads}")
|
|
for name, info in model.gguf_writer.tensors[0].items():
|
|
if name.endswith((".weight", ".bias")):
|
|
self.assertIsInstance(info.tensor, gguf.LazyNumpyTensor)
|
|
result = {
|
|
name: (info.dtype, gguf.LazyNumpyTensor.to_eager(info.tensor).copy())
|
|
for name, info in model.gguf_writer.tensors[0].items()
|
|
}
|
|
if check_lazy:
|
|
self.assertTrue(deferred.intersection(reads))
|
|
return result
|
|
|
|
def test_lazy_fp8_export(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
weight = torch.tensor([[1., -1.], [2., -2.]]).to(torch.float8_e4m3fn)
|
|
for channel_scale in (False, True):
|
|
for scan_nvfp4 in (False, True):
|
|
with self.subTest(channel_scale=channel_scale, scan_nvfp4=scan_nvfp4):
|
|
tensors = {
|
|
name: weight,
|
|
name + "_scale": torch.full((2, 1) if channel_scale else (1,), 0.25),
|
|
}
|
|
result = self.convert(Qwen2MoeModel, tensors, check_lazy=True, scan_nvfp4=scan_nvfp4)
|
|
dtype, data = result["blk.0.attn_q.weight"]
|
|
if channel_scale:
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
np.testing.assert_array_equal(data, weight.float().numpy() * 0.25)
|
|
else:
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F8_E4M3)
|
|
np.testing.assert_array_equal(gguf.dequantize(data, dtype), weight.float().numpy())
|
|
np.testing.assert_array_equal(result["blk.0.attn_q.scale"][1], [0.25])
|
|
|
|
def test_mixed_compressed_fp8_block_scale(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
scale = torch.tensor([[0.25, 0.5], [1.0, 2.0]])
|
|
tensors = {
|
|
name: torch.ones((3, 32)).to(torch.float8_e4m3fn),
|
|
name + "_scale": scale,
|
|
}
|
|
config = {
|
|
"quant_method": "compressed-tensors",
|
|
"format": "mixed-precision",
|
|
"config_groups": {
|
|
"nvfp4": {"format": "nvfp4-pack-quantized"},
|
|
"fp8": {"format": "float-quantized", "weights": {
|
|
"type": "float", "num_bits": 8, "strategy": "block", "block_structure": [2, 16],
|
|
}},
|
|
},
|
|
}
|
|
result = self.convert(Qwen2MoeModel, tensors, check_lazy=True, hparams_override={"quantization_config": config})
|
|
self.assertEqual(set(result), {"blk.0.attn_q.weight"})
|
|
dtype, data = result["blk.0.attn_q.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
expected = scale.repeat_interleave(2, 0).repeat_interleave(16, 1)[:3, :32]
|
|
np.testing.assert_array_equal(data, expected.numpy())
|
|
|
|
def test_mixed_compressed_fp8_block_layout_errors(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
tensors = {
|
|
name: torch.ones((3, 32)).to(torch.float8_e4m3fn),
|
|
name + "_scale": torch.ones((2, 2)),
|
|
}
|
|
for configs, message in (
|
|
([], "requires a block layout"),
|
|
([{"strategy": "block"}], "requires a block layout"),
|
|
([{"strategy": "block", "block_structure": [2, 16], "group_size": 16}], "requires a block layout"),
|
|
([{"strategy": "block", "block_structure": [2, 16]}, {"strategy": "block", "block_structure": [2, 32]}], "ambiguous block layouts"),
|
|
):
|
|
with self.subTest(configs=configs):
|
|
groups: dict[str, dict[str, object]] = {"nvfp4": {"format": "nvfp4-pack-quantized"}}
|
|
for i, config in enumerate(configs):
|
|
groups[str(i)] = {"format": "float-quantized", "weights": {"type": "float", "num_bits": 8, **config}}
|
|
config = {"quant_method": "compressed-tensors", "format": "mixed-precision", "config_groups": groups}
|
|
with self.assertRaisesRegex(ValueError, message):
|
|
self.convert(Qwen2MoeModel, tensors, hparams_override={"quantization_config": config})
|
|
|
|
def test_mla_scale_fallback(self):
|
|
weight = torch.arange(16, dtype=torch.float32).reshape(4, 4).to(torch.float8_e4m3fn)
|
|
prefix = "model.layers.0.self_attn."
|
|
tensors = {}
|
|
for proj in ("q_a_proj", "q_b_proj", "kv_a_proj_with_mqa", "kv_b_proj"):
|
|
tensors[prefix + proj + ".weight"] = weight.clone()
|
|
tensors[prefix + proj + ".weight_scale"] = torch.tensor([0.25])
|
|
for eager in (False, True):
|
|
with self.subTest(eager=eager):
|
|
result = self.convert(DeepseekV2Model, tensors, eager=eager)
|
|
self.assertFalse(any(name.endswith(".scale") for name in result))
|
|
np.testing.assert_array_equal(result["blk.0.attn_k_b.weight"][1], (weight.float()[:2].T * 0.25).numpy()[None])
|
|
np.testing.assert_array_equal(result["blk.0.attn_v_b.weight"][1], (weight.float()[2:] * 0.25).numpy()[None])
|
|
|
|
def test_output_type_override(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
weight = torch.tensor([[1., -1.], [2., -2.]]).to(torch.float8_e4m3fn)
|
|
for eager in (False, True):
|
|
for scaled in (False, True):
|
|
with self.subTest(eager=eager, scaled=scaled):
|
|
tensors = {name: weight}
|
|
if scaled:
|
|
tensors[name + "_scale"] = torch.tensor([0.25])
|
|
result = self.convert(ForcedF16Model, tensors, eager=eager, check_lazy=not eager)
|
|
dtype, data = result["blk.0.attn_q.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
self.assertNotIn("blk.0.attn_q.scale", result)
|
|
np.testing.assert_array_equal(data, weight.float().numpy() * (0.25 if scaled else 1.))
|
|
|
|
def test_fp8_bias_is_numeric_f32(self):
|
|
bias = torch.tensor([1., -1.]).to(torch.float8_e4m3fn)
|
|
result = self.convert(Qwen2MoeModel, {"model.layers.0.self_attn.q_proj.bias": bias}, check_lazy=True)
|
|
dtype, data = result["blk.0.attn_q.bias"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F32)
|
|
np.testing.assert_array_equal(data, [1., -1.])
|
|
|
|
def test_fp8_mandatory_f32(self):
|
|
for name, target, shape in (
|
|
("model.layers.0.linear_attn.conv1d.weight", "blk.0.ssm_conv1d.weight", (4, 1, 4)),
|
|
("model.layers.0.mlp.gate.weight", "blk.0.ffn_gate_inp.weight", (2, 4)),
|
|
):
|
|
for eager in (False, True):
|
|
for scaled in (False, True):
|
|
with self.subTest(name=name, eager=eager, scaled=scaled):
|
|
weight = torch.arange(int(np.prod(shape)), dtype=torch.float32).reshape(shape).to(torch.float8_e4m3fn)
|
|
tensors = {name: weight}
|
|
if scaled:
|
|
tensors[name + "_scale"] = torch.tensor([0.25])
|
|
result = self.convert(Qwen3NextModel, tensors, eager=eager, check_lazy=not eager, hparams_override={"mtp_num_hidden_layers": 1})
|
|
dtype, data = result[target]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F32)
|
|
np.testing.assert_array_equal(data, weight.float().squeeze().numpy() * (0.25 if scaled else 1.))
|
|
self.assertNotIn(target.removesuffix("weight") + "scale", result)
|
|
|
|
def test_fp8_norm_transform(self):
|
|
for shape in ((4,), (2, 2)):
|
|
with self.subTest(shape=shape):
|
|
weight = torch.ones(shape, dtype=torch.float32).to(torch.float8_e4m3fn)
|
|
result = self.convert(OffsetNormModel, {"model.layers.0.input_layernorm.weight": weight}, check_lazy=True)
|
|
dtype, data = result["blk.0.attn_norm.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F32)
|
|
np.testing.assert_array_equal(data, np.full(shape, 2.))
|
|
|
|
def test_explicit_outtypes(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
weight = torch.ones((4, 256), dtype=torch.float32).to(torch.float8_e4m3fn)
|
|
for ftype, qtype in (
|
|
(gguf.LlamaFileType.ALL_F32, gguf.GGMLQuantizationType.F32),
|
|
(gguf.LlamaFileType.MOSTLY_F16, gguf.GGMLQuantizationType.F16),
|
|
(gguf.LlamaFileType.MOSTLY_BF16, gguf.GGMLQuantizationType.BF16),
|
|
(gguf.LlamaFileType.MOSTLY_Q8_0, gguf.GGMLQuantizationType.Q8_0),
|
|
(gguf.LlamaFileType.MOSTLY_TQ1_0, gguf.GGMLQuantizationType.TQ1_0),
|
|
(gguf.LlamaFileType.MOSTLY_TQ2_0, gguf.GGMLQuantizationType.TQ2_0),
|
|
):
|
|
for eager in (False, True):
|
|
for scaled in (False, True):
|
|
with self.subTest(ftype=ftype, eager=eager, scaled=scaled):
|
|
tensors = {name: weight}
|
|
if scaled:
|
|
tensors[name + "_scale"] = torch.tensor([0.25])
|
|
tensors[name.removesuffix("weight") + "input_scale"] = torch.tensor([0.5])
|
|
result = self.convert(Qwen2MoeModel, tensors, ftype=ftype, eager=eager, check_lazy=not eager)
|
|
self.assertEqual(set(result), {"blk.0.attn_q.weight"})
|
|
dtype, data = result["blk.0.attn_q.weight"]
|
|
self.assertEqual(dtype, qtype)
|
|
np.testing.assert_allclose(gguf.dequantize(data, dtype), np.full((4, 256), 0.25 if scaled else 1.), atol=1e-4)
|
|
|
|
def test_embedding_scale_fallback(self):
|
|
name = "model.embed_tokens.weight"
|
|
weight = torch.ones((4, 4), dtype=torch.float32).to(torch.float8_e4m3fn)
|
|
for eager in (False, True):
|
|
with self.subTest(eager=eager):
|
|
result = self.convert(Qwen2MoeModel, {name: weight, name + "_scale": torch.tensor([0.25])}, eager=eager, check_lazy=not eager)
|
|
self.assertEqual(set(result), {"token_embd.weight"})
|
|
dtype, data = result["token_embd.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
np.testing.assert_array_equal(data, np.full((4, 4), 0.25))
|
|
|
|
def test_fp8_scale_formats(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
weight = torch.ones((4, 4), dtype=torch.float32).to(torch.float8_e4m3fn)
|
|
for suffix, config in (
|
|
("_scale_inv", {"quant_method": "fp8"}),
|
|
("_scale", {"quant_method": "compressed-tensors", "format": "float-quantized", "config_groups": {"group_0": {"weights": {"type": "float", "num_bits": 8, "strategy": "tensor"}}}}),
|
|
):
|
|
for ftype in (gguf.LlamaFileType.GUESSED, gguf.LlamaFileType.MOSTLY_F16):
|
|
with self.subTest(suffix=suffix, ftype=ftype):
|
|
result = self.convert(Qwen2MoeModel, {name: weight, name + suffix: torch.tensor([0.25])}, ftype=ftype, hparams_override={"quantization_config": config}, check_lazy=True)
|
|
dtype, data = result["blk.0.attn_q.weight"]
|
|
if ftype == gguf.LlamaFileType.GUESSED:
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F8_E4M3)
|
|
np.testing.assert_array_equal(result["blk.0.attn_q.scale"][1], [0.25])
|
|
else:
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
self.assertEqual(set(result), {"blk.0.attn_q.weight"})
|
|
np.testing.assert_array_equal(data, np.full((4, 4), 0.25))
|
|
|
|
def test_expert_naming(self):
|
|
for model_class, prefix, projections, hparams in (
|
|
(LlamaModel, "model.layers.0.block_sparse_moe", (("w1", "gate"), ("w3", "up"), ("w2", "down")), {"num_local_experts": 2}),
|
|
(PhiMoeModel, "model.layers.0.block_sparse_moe", (("w1", "gate"), ("w3", "up"), ("w2", "down")), {"max_position_embeddings": 16, "rope_scaling": {"original_max_position_embeddings": 16}}),
|
|
(NemotronHModel, "backbone.layers.0.mixer", (("up_proj", "up"), ("down_proj", "down")), {"num_experts_per_tok": 1, "n_routed_experts": 2, "head_dim": 4, "num_heads": 1, "n_groups": 1, "hybrid_override_pattern": "E"}),
|
|
):
|
|
for eager in (False, True):
|
|
for mixed in (False, True):
|
|
with self.subTest(model=model_class.__name__, eager=eager, mixed=mixed):
|
|
tensors = {}
|
|
for expert in (1, 0):
|
|
for proj, _ in projections:
|
|
name = f"{prefix}.experts.{expert}.{proj}.weight"
|
|
dtype = torch.float16 if mixed and expert == 1 else torch.float8_e4m3fn
|
|
tensors[name] = torch.ones((4, 4), dtype=torch.float32).to(dtype)
|
|
tensors[name + "_scale"] = torch.tensor([0.25 * (expert + 1)])
|
|
result = self.convert(model_class, tensors, hparams_override=hparams, eager=eager, check_lazy=not eager)
|
|
for _, proj in projections:
|
|
dtype, data = result[f"blk.0.ffn_{proj}_exps.weight"]
|
|
if mixed:
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
np.testing.assert_array_equal(data, np.array([np.full((4, 4), 0.25), np.full((4, 4), 0.5)]))
|
|
self.assertNotIn(f"blk.0.ffn_{proj}_exps.scale", result)
|
|
else:
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F8_E4M3)
|
|
np.testing.assert_array_equal(gguf.dequantize(data, dtype), np.ones((2, 4, 4)))
|
|
np.testing.assert_array_equal(result[f"blk.0.ffn_{proj}_exps.scale"][1], [0.25, 0.5])
|
|
|
|
def test_mixed_experts(self):
|
|
for eager in (False, True):
|
|
for other in ("f16", "channel", "unscaled"):
|
|
with self.subTest(eager=eager, other=other):
|
|
tensors = {}
|
|
for expert in range(2):
|
|
for proj in ("gate", "up", "down"):
|
|
name = f"model.layers.0.mlp.experts.{expert}.{proj}_proj.weight"
|
|
dtype = torch.float16 if expert == 1 and other == "f16" else torch.float8_e4m3fn
|
|
tensors[name] = torch.ones((4, 4), dtype=torch.float32).to(dtype)
|
|
if expert == 0:
|
|
tensors[name + "_scale"] = torch.tensor([0.25])
|
|
elif other == "channel":
|
|
tensors[name + "_scale"] = torch.full((4, 1), 0.5)
|
|
result = self.convert(Qwen2MoeModel, tensors, eager=eager, check_lazy=not eager)
|
|
self.assertEqual(len(result), 3)
|
|
for proj in ("gate", "up", "down"):
|
|
dtype, data = result[f"blk.0.ffn_{proj}_exps.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F16)
|
|
np.testing.assert_array_equal(data[0], np.full((4, 4), 0.25))
|
|
np.testing.assert_array_equal(data[1], np.full((4, 4), 0.5 if other == "channel" else 1.))
|
|
|
|
def test_uniform_experts_stay_fp8(self):
|
|
tensors = {}
|
|
for expert in range(2):
|
|
for proj in ("gate", "up", "down"):
|
|
name = f"model.layers.0.mlp.experts.{expert}.{proj}_proj.weight"
|
|
tensors[name] = torch.ones((4, 4), dtype=torch.float32).to(torch.float8_e4m3fn)
|
|
tensors[name + "_scale"] = torch.tensor([0.25 * (expert + 1)])
|
|
result = self.convert(Qwen2MoeModel, tensors, fuse_gate_up_exps=True, check_lazy=True)
|
|
for proj in ("gate", "up", "down"):
|
|
dtype, data = result[f"blk.0.ffn_{proj}_exps.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F8_E4M3)
|
|
np.testing.assert_array_equal(gguf.dequantize(data, dtype), np.ones((2, 4, 4)))
|
|
np.testing.assert_array_equal(result[f"blk.0.ffn_{proj}_exps.scale"][1], [0.25, 0.5])
|
|
|
|
unscaled = {name: tensor for name, tensor in tensors.items() if name.endswith(".weight")}
|
|
result = self.convert(Qwen2MoeModel, unscaled, check_lazy=True)
|
|
self.assertEqual(len(result), 3)
|
|
for dtype, data in result.values():
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.F8_E4M3)
|
|
np.testing.assert_array_equal(gguf.dequantize(data, dtype), np.ones((2, 4, 4)))
|
|
|
|
def test_fp8_as_q8(self):
|
|
name = "model.layers.0.self_attn.q_proj.weight"
|
|
tensors = {
|
|
name: torch.ones((4, 32), dtype=torch.float32).to(torch.float8_e4m3fn),
|
|
name + "_scale": torch.tensor([0.25]),
|
|
}
|
|
result = self.convert(Qwen2MoeModel, tensors, fp8_as_q8=True)
|
|
self.assertNotIn("blk.0.attn_q.scale", result)
|
|
dtype, data = result["blk.0.attn_q.weight"]
|
|
self.assertEqual(dtype, gguf.GGMLQuantizationType.Q8_0)
|
|
np.testing.assert_allclose(gguf.dequantize(data, dtype), np.full((4, 32), 0.25), atol=1e-4)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|