Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/nncf/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ class CompressWeightsMode(StrEnum):
Weights are quantized to a primary precision symmetrically without zero point.
:param INT2_SYM: Stands for a mixed-precision weights quantization with 2-bit integer as a primary precision.
Weights are quantized to a primary precision symmetrically without zero point.
:param INT2_ASYM: The same as INT2_SYM mode, but weights are quantized to a primary precision
asymmetrically with a typical non-fixed zero point. At 2 bits the symmetric grid
(-2, -1, 0, 1) spends a level on an unused sign for one-sided groups, so the asymmetric
variant is worth having despite the extra per-group zero point.
:param NF4: The the same as INT4_SYM mode, but primary precision is NF4 data type without zero point.
:param MXFP4: MX-compliant FP4 format with E2M1 values sharing group-level E8M0 scale. The size of group is 32.
:param MXFP8_E4M3: MX-compliant FP8 format with E4M3 values sharing group-level E8M0 scale. The size of group is 32.
Expand All @@ -110,6 +114,7 @@ class CompressWeightsMode(StrEnum):
INT4_ASYM = "int4_asym"
INT3_SYM = "int3_sym"
INT2_SYM = "int2_sym"
INT2_ASYM = "int2_asym"
NF4 = "nf4"
CB4 = "cb4"
MXFP4 = "mxfp4"
Expand Down
14 changes: 13 additions & 1 deletion src/nncf/quantization/algorithms/weight_compression/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def num_bits(self) -> int:
CompressWeightsMode.MXFP8_E4M3: 8,
CompressWeightsMode.INT3_SYM: 3,
CompressWeightsMode.INT2_SYM: 2,
CompressWeightsMode.INT2_ASYM: 2,
}

try:
Expand All @@ -84,7 +85,11 @@ def num_bits(self) -> int:

@property
def is_asym_mode(self) -> bool:
return self.mode in [CompressWeightsMode.INT4_ASYM, CompressWeightsMode.INT8_ASYM]
return self.mode in [
CompressWeightsMode.INT2_ASYM,
CompressWeightsMode.INT4_ASYM,
CompressWeightsMode.INT8_ASYM,
]
Comment on lines 87 to +92

@property
def is_integer(self) -> bool:
Expand Down Expand Up @@ -130,7 +135,14 @@ def compression_dtype(self) -> TensorDataType:
return TensorDataType.uint8
return TensorDataType.uint16
dtype_per_mode = {
# OpenVINO has no i2/i3 element type, so TensorDataType.int2 is physically stored as
# ov.Type.u2 (see DTYPE_MAP in tensor/functions/openvino_numeric.py). That is why the
# symmetric path shifts codes by +2**(num_bits-1) and subtracts a scalar zero point,
# and it is also why INT2_ASYM maps to the same int2 dtype rather than to a new uint2:
# asymmetric codes already land in [0, 2**num_bits - 1], so only the zero point
# differs -- per-group and u2-packed instead of a folded scalar.
CompressWeightsMode.INT2_SYM: TensorDataType.int2,
CompressWeightsMode.INT2_ASYM: TensorDataType.int2,
CompressWeightsMode.INT3_SYM: TensorDataType.int3,
CompressWeightsMode.INT4_SYM: TensorDataType.int4,
CompressWeightsMode.INT4_ASYM: TensorDataType.uint4,
Expand Down
8 changes: 4 additions & 4 deletions src/nncf/quantization/algorithms/weight_compression/gptq.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,10 @@ def _quantize_weights(
scales = fns.stack(scales, axis=1)
if wc_params.compression_config.group_size == -1:
scales = fns.squeeze(scales, axis=-1)
if wc_params.compression_config.mode in [
CompressWeightsMode.INT8_ASYM,
CompressWeightsMode.INT4_ASYM,
]:
# is_asym_mode rather than an explicit mode list: a zero point exists for exactly the
# asymmetric modes, and the list this replaces had already gone stale once (it predates
# INT2_ASYM, which would have silently fallen through to zero_points = None).
if wc_params.compression_config.is_asym_mode:
zero_points = fns.stack(zero_points, axis=1)
if wc_params.compression_config.group_size == -1:
zero_points = fns.squeeze(zero_points, axis=-1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def calculate_low_rank_matrices(
CompressWeightsMode.INT4_ASYM,
CompressWeightsMode.INT3_SYM,
CompressWeightsMode.INT2_SYM,
CompressWeightsMode.INT2_ASYM,
):
fq_weights = do_integer_dequantization(
compressed_weight,
Expand All @@ -183,7 +184,8 @@ def calculate_low_rank_matrices(
fq_weights = do_float_dequantization(compressed_weight, reduction_axis)
else:
msg = (
f"{mode.value} mode is invalid for Lora Correction algorithm. Supported modes: INT4_SYM, INT4_ASYM, NF4"
f"{mode.value} mode is invalid for Lora Correction algorithm. "
"Supported modes: INT4_SYM, INT4_ASYM, INT3_SYM, INT2_SYM, INT2_ASYM, NF4"
)
raise nncf.InternalError(msg)
# fq_w + residual = w => residual = w - fq_w
Expand Down
62 changes: 62 additions & 0 deletions tests/openvino/native/quantization/test_weights_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,64 @@ def test_int_compressed_weighs_range(mode, data):
assert np.allclose(np.abs(compressed_weight.tensor.data), np.abs(w.data))


@pytest.mark.parametrize(
("mode", "num_bits"),
(
(CompressWeightsMode.INT2_ASYM, 2),
(CompressWeightsMode.INT4_ASYM, 4),
(CompressWeightsMode.INT8_ASYM, 8),
),
)
@pytest.mark.parametrize("sign", (1.0, -1.0), ids=("positive", "negative"))
def test_int_asym_compressed_weights_range(mode, num_bits, sign):
# Asymmetric codes are unsigned and span [0, 2**num_bits - 1]. The quantization range is
# clamped to always include zero (calculate_integer_quantization_params), so one-sided data
# is what actually exercises the zero point: all-positive gives zp == 0, all-negative gives
# zp == level_high. Both must still use the full code range.
# The data starts at 0 so that the lowest code is 0 at every bit width: with a range clamped
# to [0, 4] a smallest value of, say, 0.5 still rounds to 0 on the coarse 2-bit grid but not
# on the 8-bit one.
level_high = 2**num_bits - 1
data = (sign * np.linspace(0.0, 4.0, 16)).astype(np.float32)
w = Tensor(data)

config = WeightCompressionConfig(mode=mode)
compressed_weight = do_integer_quantization(w, config, -1)

codes = compressed_weight.tensor.data
assert codes.min() == 0
assert codes.max() == level_high

zero_point = compressed_weight.zero_point
assert zero_point is not None
assert np.all(zero_point.data == (0 if sign > 0 else level_high))


def test_int2_asym_group_wise_zero_point_shape():
# A per-group asymmetric zero point must have one entry per (channel, group), matching the
# scale -- INT2_SYM folds its zero point away, so this is specific to the asymmetric mode.
group_size = 4
w = Tensor(np.linspace(-1.0, 1.0, 2 * 16).astype(np.float32).reshape(2, 16))

config = WeightCompressionConfig(mode=CompressWeightsMode.INT2_ASYM, group_size=group_size)
compressed_weight = do_integer_quantization(w, config, reduction_axes=(1,))

assert compressed_weight.zero_point is not None
assert compressed_weight.zero_point.shape == compressed_weight.scale.shape
assert compressed_weight.zero_point.shape == (2, 16 // group_size, 1)


def test_int2_asym_config():
config = WeightCompressionConfig(mode=CompressWeightsMode.INT2_ASYM)

assert config.num_bits == 2
assert config.is_asym_mode
# OpenVINO has no i2 type, so int2 is the physically-unsigned u2 storage that INT2_SYM also
# maps to; only the zero point differs between the two modes.
assert config.compression_dtype == TensorDataType.int2
assert not config.is_symmetric_represented_by_unsigned


FP4_REF = {
"neg": [
-8.0,
Expand Down Expand Up @@ -1667,6 +1725,10 @@ def test_codebook_weights_range(data):
(WeightCompressionConfig(CompressWeightsMode.INT3_SYM), False, False, False),
(WeightCompressionConfig(CompressWeightsMode.INT2_SYM), True, False, False),
(WeightCompressionConfig(CompressWeightsMode.INT2_SYM), False, False, False),
(WeightCompressionConfig(CompressWeightsMode.INT2_ASYM), False, False, False),
(WeightCompressionConfig(CompressWeightsMode.INT2_ASYM), True, True, False),
(WeightCompressionConfig(CompressWeightsMode.INT2_ASYM), True, False, True),
(WeightCompressionConfig(CompressWeightsMode.INT2_ASYM), False, True, True),
],
)
def test_int_quantization_with_precomputed_parameters(config, precompute_scale, precompute_zero_point, raises):
Expand Down