Add W6A8 checkpoint loading, conversion, inference, LoRA requantization, compile compatibility, and export, plus architecture-aware w4a8_mixed selection. Add MOSS-Image detection and quantization presets, update documentation, tests, and version to 2.4.0.
1004 lines
37 KiB
Python
1004 lines
37 KiB
Python
import importlib
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
import torch
|
|
from torch import nn
|
|
|
|
|
|
COMFY_ROOT = Path(__file__).resolve().parents[3]
|
|
CUSTOM_NODES_ROOT = COMFY_ROOT / "custom_nodes"
|
|
PACKAGE_NAME = Path(__file__).resolve().parents[1].name
|
|
sys.path.insert(0, str(COMFY_ROOT))
|
|
sys.path.insert(0, str(CUSTOM_NODES_ROOT))
|
|
|
|
quant = importlib.import_module(f"{PACKAGE_NAME}.int8_quant")
|
|
loader = importlib.import_module(f"{PACKAGE_NAME}.int8_unet_loader")
|
|
model_adapter = importlib.import_module(f"{PACKAGE_NAME}.int8_model_adapter")
|
|
quantization_policy = importlib.import_module(f"{PACKAGE_NAME}.quantization_policy")
|
|
|
|
|
|
class QuantizationModeTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.quantization_state = {
|
|
"dynamic_quantize": quant.Int8TensorwiseOps.dynamic_quantize,
|
|
"_is_prequantized": quant.Int8TensorwiseOps._is_prequantized,
|
|
"quantization_mode": quant.Int8TensorwiseOps.quantization_mode,
|
|
"keep_float_names": list(quant.Int8TensorwiseOps.keep_float_names),
|
|
"int4_sensitive_names": list(quant.Int8TensorwiseOps.int4_sensitive_names),
|
|
"int4_mixed_selected_names": set(quant.Int8TensorwiseOps.int4_mixed_selected_names),
|
|
"int4_mixed_ratio": quant.Int8TensorwiseOps.int4_mixed_ratio,
|
|
}
|
|
quant.Int8TensorwiseOps.reset_otf_progress()
|
|
|
|
def tearDown(self):
|
|
quant.Int8TensorwiseOps.reset_otf_progress()
|
|
for name, value in self.quantization_state.items():
|
|
setattr(quant.Int8TensorwiseOps, name, value)
|
|
|
|
def test_int_mm_dispatch_preserves_aligned_and_unaligned_results(self):
|
|
for rows, inner, columns in ((17, 8, 8), (3, 7, 5)):
|
|
left = torch.randint(-8, 9, (rows, inner), dtype=torch.int8)
|
|
right = torch.randint(-8, 9, (inner, columns), dtype=torch.int8)
|
|
|
|
actual = quant._torch_int_mm_dispatch(left, right, output_columns=columns)
|
|
expected = left.to(torch.int32) @ right.to(torch.int32)
|
|
|
|
self.assertTrue(torch.equal(actual, expected))
|
|
|
|
def test_int8_convrot_uses_native_runtime_and_preserves_dynamic_lora(self):
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = torch.nn.Parameter(torch.randint(-8, 9, (16, 256), dtype=torch.int8), requires_grad=False)
|
|
module.weight_scale = torch.ones((16, 1), dtype=torch.float32)
|
|
module._is_quantized = True
|
|
module._quant_format = "int8_tensorwise"
|
|
module._is_per_row = True
|
|
module._outlier_method = quant.OUTLIER_METHOD_CONVROT
|
|
module._convrot_groupsize = 256
|
|
module.quarot_hadamard = quant._build_outlier_hadamard(
|
|
quant.OUTLIER_METHOD_CONVROT,
|
|
256,
|
|
device="cpu",
|
|
dtype=torch.float32,
|
|
)
|
|
|
|
x = torch.randn(3, 256, dtype=torch.float32)
|
|
lora_a = torch.randn(4, 256, dtype=torch.float32)
|
|
lora_b = torch.randn(16, 4, dtype=torch.float32)
|
|
rotated_lora_a = quant._transform_weight_like_for_outlier_method(
|
|
lora_a,
|
|
quant.OUTLIER_METHOD_CONVROT,
|
|
torch.device("cpu"),
|
|
)
|
|
module.dynamic_lora_entries = [{"A": rotated_lora_a, "B": lora_b, "offset": None}]
|
|
base_output = torch.randn(3, 16, dtype=torch.bfloat16)
|
|
expected_base = base_output.clone()
|
|
|
|
with mock.patch.object(quant, "_native_int8_convrot_linear", return_value=base_output) as native_linear:
|
|
actual = module(x)
|
|
|
|
native_linear.assert_called_once()
|
|
expected = expected_base + torch.nn.functional.linear(
|
|
torch.nn.functional.linear(x, lora_a),
|
|
lora_b,
|
|
).to(base_output.dtype)
|
|
torch.testing.assert_close(actual, expected, rtol=0.02, atol=0.02)
|
|
|
|
def test_linear_cast_cleanup_runs_when_execution_raises(self):
|
|
for quant_format, kernel_owner, kernel_name in (
|
|
(None, quant.F, "linear"),
|
|
("convrot_w4a4", quant, "native_int4_linear"),
|
|
(quant.W4A8_FORMAT, quant, "native_w4a8_linear"),
|
|
):
|
|
with self.subTest(quant_format=quant_format or "float"):
|
|
module = quant.Int8TensorwiseOps.Linear(4, 3, bias=False, device="cpu", dtype=torch.float32)
|
|
module._is_quantized = quant_format is not None
|
|
module._quant_format = quant_format
|
|
offload_stream = object()
|
|
|
|
with mock.patch(
|
|
"comfy.ops.cast_bias_weight",
|
|
return_value=(module.weight, module.bias, offload_stream),
|
|
), mock.patch("comfy.ops.uncast_bias_weight") as uncast_bias_weight, mock.patch.object(
|
|
kernel_owner,
|
|
kernel_name,
|
|
side_effect=RuntimeError("execution failed"),
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "execution failed"):
|
|
module(torch.randn(2, 4, dtype=torch.float32))
|
|
|
|
uncast_bias_weight.assert_called_once_with(
|
|
module,
|
|
module.weight,
|
|
module.bias,
|
|
offload_stream,
|
|
)
|
|
|
|
def test_native_int8_convrot_metadata_round_trip_preserves_layer_group_size(self):
|
|
state_dict = {
|
|
"weight": torch.randint(-8, 9, (16, 2688), dtype=torch.int8),
|
|
"weight_scale": torch.ones((16, 1), dtype=torch.float32),
|
|
"comfy_quant": quant._encode_comfy_quant_config({
|
|
"format": "int8_tensorwise",
|
|
"convrot": True,
|
|
"convrot_groupsize": 64,
|
|
}),
|
|
}
|
|
module = quant.Int8TensorwiseOps.Linear(2688, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
|
|
module.load_state_dict(state_dict, strict=False)
|
|
|
|
self.assertEqual(module._convrot_groupsize, 64)
|
|
self.assertEqual(tuple(module.quarot_hadamard.shape), (64, 64))
|
|
self.assertEqual(module._outlier_method, quant.OUTLIER_METHOD_CONVROT)
|
|
with mock.patch.object(
|
|
quant,
|
|
"_native_int8_convrot_linear",
|
|
return_value=torch.zeros((2, 16), dtype=torch.bfloat16),
|
|
) as native_linear:
|
|
module(torch.randn(2, 2688, dtype=torch.float32))
|
|
native_linear.assert_called_once()
|
|
self.assertEqual(native_linear.call_args.args[-1], 64)
|
|
|
|
saved_state_dict = module.state_dict()
|
|
saved_config = quant._decode_comfy_quant_config(saved_state_dict["comfy_quant"])
|
|
self.assertEqual(saved_config["convrot_groupsize"], 64)
|
|
self.assertNotIn("quarot_hadamard", saved_state_dict)
|
|
|
|
reloaded = quant.Int8TensorwiseOps.Linear(2688, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
reloaded.load_state_dict(saved_state_dict, strict=False)
|
|
self.assertEqual(reloaded._convrot_groupsize, 64)
|
|
self.assertEqual(tuple(reloaded.quarot_hadamard.shape), (64, 64))
|
|
|
|
def test_native_int8_convrot_load_rejects_invalid_group_metadata(self):
|
|
for group_size, message in (
|
|
(0, "must be positive"),
|
|
(256, "2688 is not divisible"),
|
|
(32, "must be a power of 4"),
|
|
("invalid", "invalid ConvRot group size"),
|
|
):
|
|
with self.subTest(group_size=group_size):
|
|
module = quant.Int8TensorwiseOps.Linear(2688, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
state_dict = {
|
|
"weight": torch.randint(-8, 9, (16, 2688), dtype=torch.int8),
|
|
"weight_scale": torch.ones((16, 1), dtype=torch.float32),
|
|
"comfy_quant": quant._encode_comfy_quant_config({
|
|
"format": "int8_tensorwise",
|
|
"convrot": True,
|
|
"convrot_groupsize": group_size,
|
|
}),
|
|
}
|
|
|
|
with self.assertRaisesRegex(ValueError, message):
|
|
module.load_state_dict(state_dict, strict=False)
|
|
|
|
def test_native_int8_convrot_save_rejects_inconsistent_hadamard_shape(self):
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = nn.Parameter(torch.randint(-8, 9, (16, 256), dtype=torch.int8), requires_grad=False)
|
|
module.weight_scale = torch.ones((16, 1), dtype=torch.float32)
|
|
module._is_quantized = True
|
|
module._quant_format = "int8_tensorwise"
|
|
module._outlier_method = quant.OUTLIER_METHOD_CONVROT
|
|
module._convrot_groupsize = 256
|
|
module.quarot_hadamard = quant._build_outlier_hadamard(
|
|
quant.OUTLIER_METHOD_CONVROT,
|
|
64,
|
|
device="cpu",
|
|
dtype=torch.float32,
|
|
)
|
|
|
|
with self.assertRaisesRegex(ValueError, "Hadamard shape must be"):
|
|
module.state_dict()
|
|
|
|
def test_native_int8_convrot_save_rejects_nondivisible_group_size(self):
|
|
module = quant.Int8TensorwiseOps.Linear(2688, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = nn.Parameter(torch.randint(-8, 9, (16, 2688), dtype=torch.int8), requires_grad=False)
|
|
module.weight_scale = torch.ones((16, 1), dtype=torch.float32)
|
|
module._is_quantized = True
|
|
module._quant_format = "int8_tensorwise"
|
|
module._outlier_method = quant.OUTLIER_METHOD_CONVROT
|
|
module._convrot_groupsize = 256
|
|
module.quarot_hadamard = quant._build_outlier_hadamard(
|
|
quant.OUTLIER_METHOD_CONVROT,
|
|
256,
|
|
device="cpu",
|
|
dtype=torch.float32,
|
|
)
|
|
|
|
with self.assertRaisesRegex(ValueError, "2688 is not divisible"):
|
|
module.state_dict()
|
|
|
|
def test_dynamic_lora_hook_supports_baked_native_int8_linear(self):
|
|
class DiffusionModel(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.layer = nn.Linear(4, 3, bias=False)
|
|
self.layer.quant_format = "int8_tensorwise"
|
|
|
|
diffusion_model = DiffusionModel()
|
|
input_tensor = torch.randn(2, 4)
|
|
base_output = diffusion_model.layer(input_tensor).detach().clone()
|
|
lora_up = torch.randn(3, 2)
|
|
lora_down = torch.randn(2, 4)
|
|
adapter = quant.LoRAAdapter(
|
|
[],
|
|
(lora_up, lora_down, None, None, None, None),
|
|
)
|
|
dynamic_loras = [{
|
|
"name": "native.safetensors",
|
|
"strength": 0.75,
|
|
"patches": {"diffusion_model.layer.weight": adapter},
|
|
"patch_uuid": "native-test",
|
|
}]
|
|
|
|
quant.DynamicLoRAHook().apply_composition(diffusion_model, dynamic_loras)
|
|
actual = diffusion_model.layer(input_tensor)
|
|
expected_delta = torch.nn.functional.linear(
|
|
torch.nn.functional.linear(input_tensor, lora_down * 0.75),
|
|
lora_up,
|
|
)
|
|
|
|
self.assertTrue(hasattr(diffusion_model.layer, quant._NATIVE_DYNAMIC_LORA_HOOK_ATTRIBUTE))
|
|
torch.testing.assert_close(actual, base_output + expected_delta)
|
|
|
|
def test_native_dynamic_lora_combines_compatible_factors_without_changing_delta(self):
|
|
class DiffusionModel(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.layer = nn.Linear(4, 3, bias=False)
|
|
self.layer.quant_format = "int8_tensorwise"
|
|
|
|
diffusion_model = DiffusionModel()
|
|
input_tensor = torch.randn(2, 4)
|
|
base_output = diffusion_model.layer(input_tensor).detach().clone()
|
|
adapters = []
|
|
expected_delta = torch.zeros_like(base_output)
|
|
for index, (rank, strength, alpha) in enumerate(((2, 0.75, 1.0), (3, -0.4, 6.0))):
|
|
lora_up = torch.randn(3, rank)
|
|
lora_down = torch.randn(rank, 4)
|
|
adapters.append({
|
|
"name": f"native-{index}.safetensors",
|
|
"strength": strength,
|
|
"patches": {
|
|
"diffusion_model.layer.weight": quant.LoRAAdapter(
|
|
[],
|
|
(lora_up, lora_down, alpha, None, None, None),
|
|
),
|
|
},
|
|
"patch_uuid": f"native-combined-{index}",
|
|
})
|
|
expected_delta += torch.nn.functional.linear(
|
|
torch.nn.functional.linear(input_tensor, lora_down * ((alpha / rank) * strength)),
|
|
lora_up,
|
|
)
|
|
|
|
quant.DynamicLoRAHook().apply_composition(diffusion_model, adapters)
|
|
actual = diffusion_model.layer(input_tensor)
|
|
|
|
self.assertEqual(len(diffusion_model.layer.dynamic_lora_entries), 1)
|
|
torch.testing.assert_close(actual, base_output + expected_delta, rtol=1e-5, atol=1e-5)
|
|
|
|
def test_dynamic_lora_hook_matches_linear_inside_compiled_module_wrapper(self):
|
|
class CompiledModule(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._orig_mod = nn.Module()
|
|
self._orig_mod.layer = nn.Linear(4, 3, bias=False)
|
|
self._orig_mod.layer.quant_format = "int8_tensorwise"
|
|
|
|
class DiffusionModel(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.blocks = nn.ModuleList([CompiledModule()])
|
|
|
|
diffusion_model = DiffusionModel()
|
|
lora_up = torch.randn(3, 2)
|
|
lora_down = torch.randn(2, 4)
|
|
adapter = quant.LoRAAdapter(
|
|
[],
|
|
(lora_up, lora_down, None, None, None, None),
|
|
)
|
|
dynamic_loras = [{
|
|
"name": "compiled.safetensors",
|
|
"strength": 1.0,
|
|
"patches": {"diffusion_model.blocks.0.layer.weight": adapter},
|
|
"patch_uuid": "compiled-test",
|
|
}]
|
|
|
|
quant.DynamicLoRAHook().apply_composition(diffusion_model, dynamic_loras)
|
|
|
|
self.assertEqual(
|
|
len(diffusion_model.blocks[0]._orig_mod.layer.dynamic_lora_entries),
|
|
1,
|
|
)
|
|
|
|
def test_dynamic_lora_hook_unwraps_hidden_lazy_compile_source(self):
|
|
class SourceBlock(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.layer = nn.Linear(4, 3, bias=False)
|
|
self.layer.quant_format = "int8_tensorwise"
|
|
|
|
class HiddenCompiledModule(nn.Module):
|
|
def __init__(self, source_module):
|
|
super().__init__()
|
|
object.__setattr__(self, "_source_module", source_module)
|
|
|
|
class DiffusionModel(nn.Module):
|
|
def __init__(self, source_block):
|
|
super().__init__()
|
|
self.blocks = nn.ModuleList([HiddenCompiledModule(source_block)])
|
|
|
|
source_block = SourceBlock()
|
|
diffusion_model = DiffusionModel(source_block)
|
|
lora_up = torch.randn(3, 2)
|
|
lora_down = torch.randn(2, 4)
|
|
adapter = quant.LoRAAdapter(
|
|
[],
|
|
(lora_up, lora_down, None, None, None, None),
|
|
)
|
|
dynamic_loras = [{
|
|
"name": "hidden-compiled.safetensors",
|
|
"strength": 1.0,
|
|
"patches": {"diffusion_model.blocks.0.layer.weight": adapter},
|
|
"patch_uuid": "hidden-compiled-test",
|
|
}]
|
|
|
|
quant.DynamicLoRAHook().apply_composition(diffusion_model, dynamic_loras)
|
|
|
|
self.assertEqual(len(source_block.layer.dynamic_lora_entries), 1)
|
|
|
|
def test_legacy_modes_are_not_exposed_or_normalized(self):
|
|
for mode in ("none", "convrot", "quarot", "hadanorm"):
|
|
self.assertNotIn(mode, quant.QUANTIZATION_MODE_CHOICES)
|
|
self.assertEqual(quant.normalize_quantization_mode(mode), "int8")
|
|
|
|
def test_grouped_int8_modes_are_exposed_as_distinct_low_bit_modes(self):
|
|
for quantization_mode in (
|
|
quant.QUANTIZATION_MODE_W4A8,
|
|
quant.QUANTIZATION_MODE_W4A8_MIXED,
|
|
quant.QUANTIZATION_MODE_W6A8,
|
|
):
|
|
with self.subTest(quantization_mode=quantization_mode):
|
|
self.assertIn(quantization_mode, quant.QUANTIZATION_MODE_CHOICES)
|
|
self.assertTrue(quant.quantization_mode_is_int4(quantization_mode))
|
|
self.assertEqual(
|
|
quant.quantization_mode_outlier_method(quantization_mode),
|
|
quant.OUTLIER_METHOD_NONE,
|
|
)
|
|
|
|
self.assertEqual(quant.quantization_mode_grouped_bits(quant.QUANTIZATION_MODE_W4A8), 4)
|
|
self.assertEqual(quant.quantization_mode_grouped_bits(quant.QUANTIZATION_MODE_W4A8_MIXED), 4)
|
|
self.assertEqual(quant.quantization_mode_grouped_bits(quant.QUANTIZATION_MODE_W6A8), 6)
|
|
self.assertEqual(quant.quantization_mode_grouped_format(quant.QUANTIZATION_MODE_W6A8), quant.W6A8_FORMAT)
|
|
self.assertTrue(quant.quantization_mode_uses_mixed_int8(quant.QUANTIZATION_MODE_W4A8_MIXED))
|
|
|
|
def test_model_adapter_rejects_w4a8_without_native_layout(self):
|
|
with mock.patch.object(model_adapter, "native_w4a8_available", return_value=False):
|
|
with self.assertRaisesRegex(RuntimeError, "ComfyUI 0.32.0 or newer"):
|
|
model_adapter.INT8ModelAdapter().apply_quantization(
|
|
object(),
|
|
enable_quantization=model_adapter.QUANTIZATION_CONTROL_ALWAYS,
|
|
model_type=model_adapter.AUTO_MODEL_TYPE,
|
|
quantization_mode=quant.QUANTIZATION_MODE_W4A8,
|
|
log_progress=False,
|
|
)
|
|
|
|
@unittest.skipUnless(quant._NATIVE_W4A8_AVAILABLE, "Native ComfyUI W4A8 layout is unavailable")
|
|
def test_native_w4a8_state_dict_round_trip_and_requantization(self):
|
|
q_weight = quant.quantize_native_w4a8(torch.randn(16, 256, dtype=torch.float32))
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = torch.nn.Parameter(q_weight, requires_grad=False)
|
|
module.weight_scale = None
|
|
module._is_quantized = True
|
|
module._quant_format = quant.W4A8_FORMAT
|
|
module._convrot_groupsize = q_weight._params.convrot_groupsize
|
|
module._w4a8_group_size = q_weight._params.group_size
|
|
|
|
state_dict = module.state_dict()
|
|
self.assertIn("weight_s_rel", state_dict)
|
|
self.assertIn("weight_s_channel", state_dict)
|
|
self.assertEqual(
|
|
quant._decode_comfy_quant_config(state_dict["comfy_quant"])["format"],
|
|
quant.W4A8_FORMAT,
|
|
)
|
|
|
|
loaded = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
loaded.load_state_dict(state_dict, strict=False)
|
|
self.assertEqual(loaded._quant_format, quant.W4A8_FORMAT)
|
|
self.assertEqual(loaded.weight._layout_cls, "AsymW4A8Int8Layout")
|
|
|
|
requantized = loaded.set_weight(
|
|
loaded.convert_weight(loaded.weight) + 0.01,
|
|
seed=17,
|
|
return_weight=True,
|
|
)
|
|
self.assertEqual(requantized._layout_cls, "AsymW4A8Int8Layout")
|
|
|
|
@unittest.skipUnless(quant._NATIVE_W6A8_AVAILABLE, "Native ComfyUI W6A8 support is unavailable")
|
|
def test_native_w6a8_state_dict_round_trip_and_requantization(self):
|
|
q_weight = quant.quantize_native_w6a8(torch.randn(8, 256, dtype=torch.float32))
|
|
module = quant.Int8TensorwiseOps.Linear(256, 8, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = torch.nn.Parameter(q_weight, requires_grad=False)
|
|
module.weight_scale = None
|
|
module._is_quantized = True
|
|
module._quant_format = quant.W6A8_FORMAT
|
|
module._convrot_groupsize = q_weight._params.convrot_groupsize
|
|
module._w4a8_group_size = q_weight._params.group_size
|
|
module._grouped_int8_bits = 6
|
|
|
|
state_dict = module.state_dict()
|
|
self.assertEqual(tuple(state_dict["weight"].shape), (8, 192))
|
|
self.assertEqual(state_dict["weight_s_rel"].shape, (8, 8))
|
|
self.assertNotIn("weight_codebook", state_dict)
|
|
quant_config = quant._decode_comfy_quant_config(state_dict["comfy_quant"])
|
|
self.assertEqual(quant_config["format"], quant.W6A8_FORMAT)
|
|
self.assertEqual(quant_config["group_size"], 32)
|
|
|
|
loaded = quant.Int8TensorwiseOps.Linear(256, 8, bias=False, device="cpu", dtype=torch.float32)
|
|
loaded.load_state_dict(state_dict, strict=False)
|
|
self.assertEqual(loaded._quant_format, quant.W6A8_FORMAT)
|
|
self.assertEqual(loaded._grouped_int8_bits, 6)
|
|
self.assertEqual(loaded.weight._layout_cls, "AsymW4A8Int8Layout")
|
|
|
|
requantized = loaded.set_weight(
|
|
loaded.convert_weight(loaded.weight) + 0.01,
|
|
seed=17,
|
|
return_weight=True,
|
|
)
|
|
self.assertEqual(requantized._layout_cls, "AsymW4A8Int8Layout")
|
|
self.assertEqual(tuple(requantized._qdata.shape), (8, 192))
|
|
|
|
@unittest.skipUnless(quant._NATIVE_INT4_AVAILABLE, "Native ComfyUI INT4 layout is unavailable")
|
|
def test_native_int4_state_dict_round_trip(self):
|
|
weight = torch.randn(16, 256, dtype=torch.float32)
|
|
q_weight = quant.quantize_native_int4(weight)
|
|
|
|
module = quant.Int8TensorwiseOps.Linear(
|
|
256,
|
|
16,
|
|
bias=True,
|
|
device="cpu",
|
|
dtype=torch.float32,
|
|
)
|
|
module.weight = torch.nn.Parameter(q_weight, requires_grad=False)
|
|
module.weight_scale = q_weight._params.scale
|
|
module._is_quantized = True
|
|
module._quant_format = "convrot_w4a4"
|
|
module._convrot_groupsize = 256
|
|
module._quant_group_size = 64
|
|
module._linear_dtype = "int4"
|
|
|
|
state_dict = module.state_dict()
|
|
self.assertEqual(tuple(state_dict["weight"].shape), (16, 128))
|
|
|
|
loaded = quant.Int8TensorwiseOps.Linear(
|
|
256,
|
|
16,
|
|
bias=True,
|
|
device="cpu",
|
|
dtype=torch.float32,
|
|
)
|
|
loaded.load_state_dict(state_dict, strict=False)
|
|
|
|
self.assertEqual(loaded._quant_format, "convrot_w4a4")
|
|
self.assertEqual(loaded.weight._layout_cls, "TensorCoreConvRotW4A4Layout")
|
|
output = loaded(torch.randn(3, 256, dtype=torch.float32))
|
|
self.assertEqual(tuple(output.shape), (3, 16))
|
|
self.assertTrue(torch.isfinite(output).all())
|
|
|
|
@unittest.skipUnless(quant._NATIVE_INT4_AVAILABLE, "Native ComfyUI INT4 layout is unavailable")
|
|
def test_native_int4_int8_compute_metadata_round_trip(self):
|
|
q_weight = quant.quantize_native_int4(
|
|
torch.randn(16, 256, dtype=torch.float32),
|
|
linear_dtype="int8",
|
|
)
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = torch.nn.Parameter(q_weight, requires_grad=False)
|
|
module.weight_scale = q_weight._params.scale
|
|
module._is_quantized = True
|
|
module._quant_format = "convrot_w4a4"
|
|
|
|
loaded = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
loaded.load_state_dict(module.state_dict(), strict=False)
|
|
self.assertEqual(loaded._linear_dtype, "int8")
|
|
self.assertEqual(loaded.weight._params.linear_dtype, "int8")
|
|
|
|
@unittest.skipUnless(quant._NATIVE_INT4_AVAILABLE, "Native ComfyUI INT4 layout is unavailable")
|
|
def test_native_int4_dynamic_lora_preserves_runtime_delta(self):
|
|
q_weight = quant.quantize_native_int4(torch.randn(16, 256, dtype=torch.float32))
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module.weight = torch.nn.Parameter(q_weight, requires_grad=False)
|
|
module.weight_scale = q_weight._params.scale
|
|
module._is_quantized = True
|
|
module._quant_format = "convrot_w4a4"
|
|
|
|
x = torch.randn(3, 256, dtype=torch.float32)
|
|
base_output = module(x)
|
|
lora_a = torch.randn(4, 256, dtype=torch.float32)
|
|
lora_b = torch.randn(16, 4, dtype=torch.float32)
|
|
module.dynamic_lora_entries = [{"A": lora_a, "B": lora_b, "offset": None}]
|
|
|
|
actual = module(x)
|
|
expected = base_output + torch.nn.functional.linear(torch.nn.functional.linear(x, lora_a), lora_b)
|
|
torch.testing.assert_close(actual, expected)
|
|
|
|
@unittest.skipUnless(quant._NATIVE_INT4_AVAILABLE, "Native ComfyUI INT4 layout is unavailable")
|
|
def test_int4_mixed_keeps_sensitive_layer_in_int8(self):
|
|
quant.Int8TensorwiseOps.dynamic_quantize = True
|
|
quant.Int8TensorwiseOps._is_prequantized = False
|
|
quant.Int8TensorwiseOps.quantization_mode = quant.QUANTIZATION_MODE_INT4_MIXED
|
|
quant.Int8TensorwiseOps.keep_float_names = []
|
|
quant.Int8TensorwiseOps.int4_sensitive_names = ["sensitive"]
|
|
quant.Int8TensorwiseOps._init_otf_progress({
|
|
"bulk.weight": torch.randn(16, 256),
|
|
"sensitive.weight": torch.randn(16, 256),
|
|
})
|
|
|
|
bulk = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
bulk._load_from_state_dict(
|
|
{"bulk.weight": torch.randn(16, 256)},
|
|
"bulk.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
sensitive = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
sensitive._load_from_state_dict(
|
|
{"sensitive.weight": torch.randn(16, 256)},
|
|
"sensitive.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
self.assertEqual(bulk._quant_format, "convrot_w4a4")
|
|
self.assertEqual(sensitive._quant_format, "int8_tensorwise")
|
|
|
|
@unittest.skipUnless(quant._NATIVE_W4A8_AVAILABLE, "Native ComfyUI W4A8 layout is unavailable")
|
|
def test_w4a8_mixed_keeps_sensitive_layer_in_int8(self):
|
|
quant.Int8TensorwiseOps.dynamic_quantize = True
|
|
quant.Int8TensorwiseOps._is_prequantized = False
|
|
quant.Int8TensorwiseOps.quantization_mode = quant.QUANTIZATION_MODE_W4A8_MIXED
|
|
quant.Int8TensorwiseOps.keep_float_names = []
|
|
quant.Int8TensorwiseOps.int4_sensitive_names = ["sensitive"]
|
|
quant.Int8TensorwiseOps.int4_mixed_ratio = 0.5
|
|
state_dict = {
|
|
"bulk.weight": torch.randn(16, 256),
|
|
"sensitive.weight": torch.randn(16, 256),
|
|
}
|
|
quant.Int8TensorwiseOps._init_otf_progress(state_dict)
|
|
|
|
bulk = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
bulk._load_from_state_dict(
|
|
{"bulk.weight": state_dict["bulk.weight"]},
|
|
"bulk.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
sensitive = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
sensitive._load_from_state_dict(
|
|
{"sensitive.weight": state_dict["sensitive.weight"]},
|
|
"sensitive.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
self.assertEqual(bulk._quant_format, quant.W4A8_FORMAT)
|
|
self.assertEqual(sensitive._quant_format, "int8_tensorwise")
|
|
|
|
@unittest.skipUnless(quant._NATIVE_W6A8_AVAILABLE, "Native ComfyUI W6A8 support is unavailable")
|
|
def test_w6a8_on_the_fly_uses_native_layout(self):
|
|
quant.Int8TensorwiseOps.dynamic_quantize = True
|
|
quant.Int8TensorwiseOps._is_prequantized = False
|
|
quant.Int8TensorwiseOps.quantization_mode = quant.QUANTIZATION_MODE_W6A8
|
|
quant.Int8TensorwiseOps.keep_float_names = []
|
|
|
|
module = quant.Int8TensorwiseOps.Linear(256, 8, bias=False, device="cpu", dtype=torch.float32)
|
|
module._load_from_state_dict(
|
|
{"bulk.weight": torch.randn(8, 256)},
|
|
"bulk.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
self.assertEqual(module._quant_format, quant.W6A8_FORMAT)
|
|
self.assertEqual(module._grouped_int8_bits, 6)
|
|
self.assertEqual(module.weight._layout_cls, "AsymW4A8Int8Layout")
|
|
|
|
def test_model_adapter_quantizes_w6a8_and_w4a8_mixed(self):
|
|
def quantize(mode, int4_sensitive=False):
|
|
module = nn.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
with mock.patch.object(model_adapter, "_get_int8_compute_device", return_value=torch.device("cpu")):
|
|
return model_adapter._quantize_linear_module(
|
|
"blocks.0.proj",
|
|
module,
|
|
module.weight.detach(),
|
|
mode,
|
|
int4_sensitive=int4_sensitive,
|
|
)[0]
|
|
|
|
if quant._NATIVE_W6A8_AVAILABLE:
|
|
w6_module = quantize(quant.QUANTIZATION_MODE_W6A8)
|
|
self.assertEqual(w6_module._quant_format, quant.W6A8_FORMAT)
|
|
self.assertEqual(w6_module._grouped_int8_bits, 6)
|
|
self.assertEqual(w6_module._w4a8_group_size, 32)
|
|
|
|
mixed_w4a8_module = quantize(quant.QUANTIZATION_MODE_W4A8_MIXED)
|
|
self.assertEqual(mixed_w4a8_module._quant_format, quant.W4A8_FORMAT)
|
|
|
|
mixed_int8_module = quantize(quant.QUANTIZATION_MODE_W4A8_MIXED, int4_sensitive=True)
|
|
self.assertEqual(mixed_int8_module._quant_format, "int8_tensorwise")
|
|
|
|
@unittest.skipUnless(quant._NATIVE_INT4_AVAILABLE, "Native ComfyUI INT4 layout is unavailable")
|
|
def test_int4_full_promotes_sensitive_layer_to_int4(self):
|
|
quant.Int8TensorwiseOps.dynamic_quantize = True
|
|
quant.Int8TensorwiseOps._is_prequantized = False
|
|
quant.Int8TensorwiseOps.quantization_mode = quant.QUANTIZATION_MODE_INT4_FULL
|
|
quant.Int8TensorwiseOps.keep_float_names = []
|
|
quant.Int8TensorwiseOps.int4_sensitive_names = ["sensitive"]
|
|
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module._load_from_state_dict(
|
|
{"sensitive.weight": torch.randn(16, 256)},
|
|
"sensitive.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
self.assertEqual(module._quant_format, "convrot_w4a4")
|
|
|
|
@unittest.skipUnless(quant._NATIVE_INT4_AVAILABLE, "Native ComfyUI INT4 layout is unavailable")
|
|
def test_int4_modes_preserve_keep_float_layers(self):
|
|
quant.Int8TensorwiseOps.dynamic_quantize = True
|
|
quant.Int8TensorwiseOps.keep_float_names = ["protected"]
|
|
quant.Int8TensorwiseOps.int4_sensitive_names = []
|
|
|
|
for quantization_mode in (
|
|
quant.QUANTIZATION_MODE_INT4_MIXED,
|
|
quant.QUANTIZATION_MODE_INT4_FULL,
|
|
):
|
|
with self.subTest(quantization_mode=quantization_mode):
|
|
quant.Int8TensorwiseOps._is_prequantized = False
|
|
quant.Int8TensorwiseOps.quantization_mode = quantization_mode
|
|
weight = torch.randn(16, 256)
|
|
module = quant.Int8TensorwiseOps.Linear(256, 16, bias=False, device="cpu", dtype=torch.float32)
|
|
module._load_from_state_dict(
|
|
{"protected.weight": weight.clone()},
|
|
"protected.",
|
|
{},
|
|
False,
|
|
[],
|
|
[],
|
|
[],
|
|
)
|
|
|
|
self.assertFalse(module._is_quantized)
|
|
self.assertIsNone(module._quant_format)
|
|
torch.testing.assert_close(module.weight, weight)
|
|
|
|
def test_architecture_presets_protect_residual_writeback_paths(self):
|
|
krea_preset = loader.get_model_type_quantization_preset(loader.MODEL_TYPE_KREA2)
|
|
anima_preset = loader.get_model_type_quantization_preset("anima")
|
|
minimax_preset = loader.get_model_type_quantization_preset(loader.MODEL_TYPE_MINIMAX_H3)
|
|
moss_preset = loader.get_model_type_quantization_preset(loader.MODEL_TYPE_MOSS)
|
|
|
|
self.assertIn("first", krea_preset["keep_float"])
|
|
self.assertEqual(krea_preset["int4_sensitive"], ("attn.wo", "mlp.down"))
|
|
self.assertIn("final_layer", anima_preset["keep_float"])
|
|
self.assertEqual(
|
|
anima_preset["int4_sensitive"],
|
|
("self_attn.output_proj", "cross_attn.output_proj", "mlp.layer2"),
|
|
)
|
|
self.assertEqual(
|
|
minimax_preset["keep_float"],
|
|
(
|
|
"video_patch_proj", "audio_patch_proj", "condition_proj", "time_embedder",
|
|
"token_refiner", "adaln_proj", "final_layer",
|
|
),
|
|
)
|
|
self.assertEqual(minimax_preset["int4_sensitive"], ("attn.out_proj", "mlp.fc2"))
|
|
self.assertEqual(
|
|
moss_preset["keep_float"],
|
|
(
|
|
"video_embedder", "text_embedder", "time_embedding", "shared_block_modulation",
|
|
"video_head_modulation", "final_linear_video", "final_norm_video",
|
|
),
|
|
)
|
|
self.assertEqual(moss_preset["int4_sensitive"], ("proj.o", "proj.ffn_down"))
|
|
|
|
def test_model_adapter_auto_detects_minimax_h3(self):
|
|
class MiniMaxH3(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.video_patch_proj = nn.Linear(4, 4)
|
|
self.audio_patch_proj = nn.Linear(4, 4)
|
|
self.condition_proj = nn.Linear(4, 4)
|
|
self.token_refiner = nn.Sequential(nn.Linear(4, 4))
|
|
self.blocks = nn.ModuleList([nn.Module()])
|
|
self.blocks[0].adaln_proj = nn.Sequential(nn.Linear(4, 4))
|
|
|
|
self.assertEqual(
|
|
model_adapter._infer_model_type_from_modules(MiniMaxH3()),
|
|
loader.MODEL_TYPE_MINIMAX_H3,
|
|
)
|
|
|
|
def test_model_adapter_auto_detects_moss_image(self):
|
|
class MossImage(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.video_embedder = nn.Linear(4, 4)
|
|
self.text_embedder = nn.Module()
|
|
self.text_embedder.blocks = nn.ModuleList([nn.Module()])
|
|
self.time_embedding = nn.Sequential(nn.Linear(4, 4))
|
|
self.shared_block_modulation = nn.Linear(4, 4)
|
|
self.video_head_modulation = nn.Linear(4, 4)
|
|
self.final_linear_video = nn.Linear(4, 4)
|
|
self.layers = nn.ModuleList([nn.Module()])
|
|
self.layers[0].proj = nn.ModuleDict({"ffn_up_gate": nn.Linear(4, 4)})
|
|
|
|
self.assertEqual(
|
|
model_adapter._infer_model_type_from_modules(MossImage()),
|
|
loader.MODEL_TYPE_MOSS,
|
|
)
|
|
|
|
def test_moss_preset_matches_the_official_quantized_layer_boundary(self):
|
|
class MossImage(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.video_embedder = nn.Linear(256, 16, bias=False)
|
|
self.text_embedder = nn.Sequential(nn.Linear(256, 16, bias=False))
|
|
self.time_embedding = nn.Sequential(nn.Linear(256, 16, bias=False))
|
|
self.shared_block_modulation = nn.Linear(256, 16, bias=False)
|
|
self.video_head_modulation = nn.Linear(256, 16, bias=False)
|
|
self.final_linear_video = nn.Linear(256, 16, bias=False)
|
|
self.layers = nn.ModuleList([nn.Module()])
|
|
self.layers[0].proj = nn.ModuleDict({
|
|
"qkv": nn.Linear(256, 16, bias=False),
|
|
"o": nn.Linear(256, 16, bias=False),
|
|
"ffn_up_gate": nn.Linear(256, 16, bias=False),
|
|
"ffn_down": nn.Linear(256, 16, bias=False),
|
|
})
|
|
|
|
preset = loader.get_model_type_quantization_preset(loader.MODEL_TYPE_MOSS)
|
|
candidate_names = {
|
|
name
|
|
for name, _module in model_adapter._collect_int8_candidates(
|
|
MossImage(),
|
|
preset["keep_float"],
|
|
)
|
|
}
|
|
|
|
self.assertEqual(
|
|
candidate_names,
|
|
{
|
|
"layers.0.proj.qkv",
|
|
"layers.0.proj.o",
|
|
"layers.0.proj.ffn_up_gate",
|
|
"layers.0.proj.ffn_down",
|
|
},
|
|
)
|
|
|
|
def test_architecture_ratios_cover_residual_writeback_paths(self):
|
|
krea_names = [
|
|
f"blocks.{block}.{path}"
|
|
for block in range(28)
|
|
for path in (
|
|
"attn.wq", "attn.wk", "attn.wv", "attn.gate", "attn.wo",
|
|
"mlp.gate", "mlp.up", "mlp.down",
|
|
)
|
|
]
|
|
anima_names = [
|
|
f"blocks.{block}.{path}"
|
|
for block in range(28)
|
|
for path in (
|
|
"self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj", "self_attn.output_proj",
|
|
"cross_attn.q_proj", "cross_attn.k_proj", "cross_attn.v_proj", "cross_attn.output_proj",
|
|
"mlp.layer1", "mlp.layer2",
|
|
)
|
|
]
|
|
krea_preset = loader.get_model_type_quantization_preset(loader.MODEL_TYPE_KREA2)
|
|
anima_preset = loader.get_model_type_quantization_preset("anima")
|
|
|
|
krea_selected = quantization_policy.select_int4_mixed_layers(
|
|
krea_names,
|
|
0.25,
|
|
krea_preset["int4_sensitive"],
|
|
)
|
|
anima_selected = quantization_policy.select_int4_mixed_layers(
|
|
anima_names,
|
|
0.3,
|
|
anima_preset["int4_sensitive"],
|
|
)
|
|
|
|
self.assertEqual(len(krea_selected), 56)
|
|
self.assertTrue(all(name.endswith(("attn.wo", "mlp.down")) for name in krea_selected))
|
|
self.assertEqual(len(anima_selected), 84)
|
|
self.assertTrue(all(name.endswith(("output_proj", "mlp.layer2")) for name in anima_selected))
|
|
|
|
def test_mixed_ratio_selects_a_stable_model_agnostic_budget(self):
|
|
layer_names = [f"blocks.{index}.linear" for index in range(10)]
|
|
|
|
first = quantization_policy.select_int4_mixed_layers(
|
|
layer_names,
|
|
0.2,
|
|
priority_patterns=("blocks.9.",),
|
|
)
|
|
second = quantization_policy.select_int4_mixed_layers(
|
|
layer_names,
|
|
0.2,
|
|
priority_patterns=("blocks.9.",),
|
|
)
|
|
|
|
self.assertEqual(first, second)
|
|
self.assertEqual(len(first), 2)
|
|
self.assertIn("blocks.9.linear", first)
|
|
|
|
def test_mixed_ratio_boundaries_select_none_or_all(self):
|
|
layer_names = [f"layer.{index}" for index in range(5)]
|
|
|
|
self.assertEqual(
|
|
quantization_policy.select_int4_mixed_layers(layer_names, 0.0),
|
|
(),
|
|
)
|
|
self.assertEqual(
|
|
quantization_policy.select_int4_mixed_layers(layer_names, 1.0),
|
|
tuple(layer_names),
|
|
)
|
|
|
|
def test_mixed_ratio_profiles_are_nested_and_preserve_the_default_selection(self):
|
|
layer_names = [f"layer.{index}" for index in range(100)]
|
|
lower_profile = set(quantization_policy.select_int4_mixed_layers(layer_names, 0.15))
|
|
default_profile = set(quantization_policy.select_int4_mixed_layers(layer_names, 0.2))
|
|
higher_profile = set(quantization_policy.select_int4_mixed_layers(layer_names, 0.3))
|
|
|
|
self.assertLess(lower_profile, default_profile)
|
|
self.assertLess(default_profile, higher_profile)
|
|
self.assertEqual(
|
|
set(quantization_policy.select_int4_mixed_layers(layer_names[:10], 0.2)),
|
|
{"layer.2", "layer.7"},
|
|
)
|
|
|
|
def test_otf_mixed_profile_selects_the_requested_fraction(self):
|
|
quant.Int8TensorwiseOps.quantization_mode = quant.QUANTIZATION_MODE_INT4_MIXED
|
|
quant.Int8TensorwiseOps.keep_float_names = []
|
|
quant.Int8TensorwiseOps.int4_sensitive_names = ["layer.4"]
|
|
quant.Int8TensorwiseOps.int4_mixed_ratio = 0.4
|
|
state_dict = {
|
|
f"layer.{index}.weight": torch.randn(16, 256)
|
|
for index in range(5)
|
|
}
|
|
|
|
quant.Int8TensorwiseOps._init_otf_progress(state_dict)
|
|
|
|
self.assertEqual(len(quant.Int8TensorwiseOps.int4_mixed_selected_names), 2)
|
|
self.assertIn("layer.4", quant.Int8TensorwiseOps.int4_mixed_selected_names)
|
|
|
|
def test_loader_preselects_mixed_profile_from_safetensors_shapes(self):
|
|
from safetensors.torch import save_file
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
path = Path(temp_dir) / "mixed.safetensors"
|
|
save_file(
|
|
{
|
|
"model.diffusion_model.layers.0.proj.qkv.weight": torch.empty(16, 256),
|
|
"model.diffusion_model.layers.0.proj.o.weight": torch.empty(16, 256),
|
|
"model.diffusion_model.text_embedder.blocks.0.qkv.weight": torch.empty(16, 256),
|
|
"model.diffusion_model.layers.0.norm.weight": torch.empty(16),
|
|
},
|
|
str(path),
|
|
)
|
|
|
|
selected = loader._select_int4_mixed_layers_from_safetensors(
|
|
str(path),
|
|
keep_float_names=("text_embedder",),
|
|
priority_patterns=("proj.qkv",),
|
|
ratio=0.5,
|
|
)
|
|
|
|
self.assertEqual(selected, {"layers.0.proj.qkv"})
|
|
|
|
def test_public_quantization_inputs_do_not_use_legacy_int8_names(self):
|
|
adapter_inputs = model_adapter.INT8ModelAdapter.INPUT_TYPES()["required"]
|
|
loader_inputs = loader.UNetLoaderINTW8A8.INPUT_TYPES()["required"]
|
|
|
|
self.assertIn("enable_quantization", adapter_inputs)
|
|
self.assertNotIn("enable_int8", adapter_inputs)
|
|
self.assertIn("prepack_weights", adapter_inputs)
|
|
self.assertNotIn("prepack_int8_weights", adapter_inputs)
|
|
self.assertIn("prepack_weights", loader_inputs)
|
|
self.assertNotIn("prepack_int8_weights", loader_inputs)
|
|
self.assertEqual(
|
|
adapter_inputs["enable_quantization"],
|
|
(
|
|
model_adapter.QUANTIZATION_CONTROL_CHOICES,
|
|
{
|
|
"default": model_adapter.QUANTIZATION_CONTROL_AS_NEEDED,
|
|
"tooltip": "as_needed converts FP8 and floating-point inputs, but leaves MODEL inputs containing Toolkit-supported INT8, W4A4, W4A8, or W6A8 layers unchanged. always converts remaining eligible layers; bypass returns the MODEL unchanged.",
|
|
},
|
|
),
|
|
)
|
|
self.assertEqual(adapter_inputs["int4_mixed_ratio"][1]["default"], 0.2)
|
|
self.assertEqual(loader_inputs["int4_mixed_ratio"][1]["default"], 0.2)
|
|
self.assertEqual(model_adapter.INT8ModelAdapter.FUNCTION, "apply_quantization")
|
|
|
|
def test_as_needed_returns_a_supported_model_unchanged(self):
|
|
shared_model = nn.Module()
|
|
shared_model.diffusion_model = nn.Sequential(nn.Linear(4, 4))
|
|
shared_model.diffusion_model[0]._is_quantized = True
|
|
model = SimpleNamespace(model=shared_model, object_patches={}, object_patches_backup={})
|
|
|
|
output = model_adapter.INT8ModelAdapter().apply_quantization(
|
|
model,
|
|
enable_quantization=model_adapter.QUANTIZATION_CONTROL_AS_NEEDED,
|
|
model_type=model_adapter.AUTO_MODEL_TYPE,
|
|
quantization_mode=quant.QUANTIZATION_MODE_INT8,
|
|
log_progress=False,
|
|
)[0]
|
|
|
|
self.assertIs(output, model)
|
|
|
|
def test_as_needed_detects_supported_object_patches(self):
|
|
quantized_module = nn.Linear(4, 4)
|
|
quantized_module._is_quantized = True
|
|
model = SimpleNamespace(
|
|
model=SimpleNamespace(diffusion_model=nn.Module()),
|
|
object_patches={"diffusion_model.block": quantized_module},
|
|
object_patches_backup={},
|
|
)
|
|
|
|
self.assertTrue(model_adapter._model_has_quantized_modules(model))
|
|
|
|
def test_as_needed_recognizes_native_int8_but_not_fp8(self):
|
|
native_int8_module = nn.Linear(4, 4)
|
|
native_int8_module.quant_format = "int8_tensorwise"
|
|
native_w4a8_module = nn.Linear(4, 4)
|
|
native_w4a8_module.quant_format = "asym_w4a8_int8"
|
|
native_w6a8_module = nn.Linear(4, 4)
|
|
native_w6a8_module.quant_format = "w6a8_int8"
|
|
native_fp8_module = nn.Linear(4, 4)
|
|
native_fp8_module.quant_format = "float8_e4m3fn"
|
|
|
|
self.assertTrue(model_adapter._module_has_target_quantization(native_int8_module))
|
|
self.assertTrue(model_adapter._module_has_target_quantization(native_w4a8_module))
|
|
self.assertTrue(model_adapter._module_has_target_quantization(native_w6a8_module))
|
|
self.assertFalse(model_adapter._module_has_target_quantization(native_fp8_module))
|
|
|
|
def test_bypass_returns_model_without_inspection(self):
|
|
model = object()
|
|
|
|
output = model_adapter.INT8ModelAdapter().apply_quantization(
|
|
model,
|
|
enable_quantization=model_adapter.QUANTIZATION_CONTROL_BYPASS,
|
|
model_type=model_adapter.AUTO_MODEL_TYPE,
|
|
quantization_mode=quant.QUANTIZATION_MODE_INT8,
|
|
)[0]
|
|
|
|
self.assertIs(output, model)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|