Skip to content

Commit ca31fb2

Browse files
author
Krishna Teja Chitty Venkata
committed
FourOverSix
1 parent 6469fdc commit ca31fb2

5 files changed

Lines changed: 182 additions & 1 deletion

File tree

src/compressed_tensors/compressors/nvfp4/base.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
QuantizationType,
1919
)
2020
from compressed_tensors.quantization.lifecycle.forward import dequantize, quantize
21+
from compressed_tensors.quantization.quant_args import round_to_quantized_type_args
2122
from compressed_tensors.utils import TensorStateDict, getattr_chain
2223

2324

@@ -60,6 +61,59 @@ def _compress_scale(
6061
def _decompress_scale(cls, scale: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
6162
return scale.to(dtype)
6263

64+
@classmethod
65+
def _adjust_scale_for_four_over_six(
66+
cls,
67+
weight: torch.Tensor,
68+
scale: torch.Tensor,
69+
global_scale: torch.Tensor | None,
70+
weights: QuantizationArgs,
71+
) -> torch.Tensor:
72+
"""
73+
Pre-adjust per-group scales for Four Over Six: for each group, compare
74+
MSE of standard quantization (scale-to-6) vs alternative (scale-to-4,
75+
i.e. scale * 1.5). Groups where scale-to-4 wins get their scale
76+
multiplied by 1.5 so that the stored scale reflects the actual
77+
quantization used.
78+
"""
79+
from compressed_tensors.quantization.utils import calculate_range
80+
81+
q_min, q_max = calculate_range(weights, weight.device)
82+
83+
group_size = weights.group_size
84+
rows, cols = weight.shape
85+
num_groups = cols // group_size
86+
w_grouped = weight.reshape(rows, num_groups, group_size)
87+
88+
if global_scale is not None:
89+
eff_scale = scale / global_scale
90+
else:
91+
eff_scale = scale.clone()
92+
93+
eff_scale_3d = eff_scale.unsqueeze(-1)
94+
95+
scaled_a = w_grouped / eff_scale_3d
96+
q_a = round_to_quantized_type_args(
97+
tensor=scaled_a, args=weights, min=q_min, max=q_max
98+
)
99+
dq_a = q_a.to(eff_scale.dtype) * eff_scale_3d
100+
101+
eff_scale_b = eff_scale * 1.5
102+
eff_scale_b_3d = eff_scale_b.unsqueeze(-1)
103+
scaled_b = w_grouped / eff_scale_b_3d
104+
q_b = round_to_quantized_type_args(
105+
tensor=scaled_b, args=weights, min=q_min, max=q_max
106+
)
107+
dq_b = q_b.to(eff_scale_b.dtype) * eff_scale_b_3d
108+
109+
mse_a = ((w_grouped - dq_a) ** 2).mean(dim=-1)
110+
mse_b = ((w_grouped - dq_b) ** 2).mean(dim=-1)
111+
112+
use_4 = mse_b < mse_a
113+
adjusted_scale = scale.clone()
114+
adjusted_scale[use_4] = adjusted_scale[use_4] * 1.5
115+
return adjusted_scale.to(scale.dtype)
116+
63117
@classmethod
64118
def compress(
65119
cls, state_dict: TensorStateDict, scheme: QuantizationScheme
@@ -82,6 +136,11 @@ def compress(
82136
zero_point = state_dict.get("weight_zero_point", None)
83137
weights = scheme.weights
84138

139+
if getattr(weights, "four_over_six", False):
140+
scale = cls._adjust_scale_for_four_over_six(
141+
weight, scale, global_scale, weights
142+
)
143+
85144
quantized_weight = quantize(
86145
x=weight,
87146
scale=scale,

src/compressed_tensors/quantization/lifecycle/forward_helpers.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import torch
77
from compressed_tensors.quantization.quant_args import (
88
QuantizationArgs,
9+
QuantizationType,
910
round_to_quantized_type_args,
1011
)
1112
from compressed_tensors.quantization.utils import maybe_pad_tensor_for_block_quant
@@ -187,6 +188,15 @@ def _quantize_dequantize(
187188
- Double scale/global_scale division
188189
- Intermediate quantized dtype allocation
189190
"""
191+
if (
192+
getattr(args, "four_over_six", False)
193+
and args.num_bits == 4
194+
and args.type == QuantizationType.FLOAT
195+
):
196+
return _four_over_six_quantize_dequantize(
197+
x, scale, zero_point, q_min, q_max, args, global_scale
198+
)
199+
190200
# compute effective scale once
191201
if global_scale is not None:
192202
scale = scale / global_scale
@@ -210,6 +220,56 @@ def _quantize_dequantize(
210220
return dequant * scale
211221

212222

223+
@torch.no_grad()
224+
def _four_over_six_quantize_dequantize(
225+
x: torch.Tensor,
226+
scale: torch.Tensor,
227+
zero_point: torch.Tensor | None,
228+
q_min: torch.Tensor,
229+
q_max: torch.Tensor,
230+
args: QuantizationArgs,
231+
global_scale: torch.Tensor | None = None,
232+
) -> torch.Tensor:
233+
"""
234+
Four Over Six adaptive block scaling: for each group, try quantizing
235+
with the standard scale (maps max to 6) and an alternative scale
236+
(maps max to 4, i.e. scale * 1.5). Pick whichever yields lower MSE.
237+
"""
238+
if global_scale is not None:
239+
eff_scale = scale / global_scale
240+
else:
241+
eff_scale = scale
242+
243+
# --- Path A: standard (scale to 6) ---
244+
scaled_a = x / eff_scale
245+
if zero_point is not None:
246+
scaled_a = scaled_a + zero_point.to(x.dtype)
247+
q_a = round_to_quantized_type_args(tensor=scaled_a, args=args, min=q_min, max=q_max)
248+
dq_a = q_a.to(eff_scale.dtype)
249+
if zero_point is not None:
250+
dq_a = dq_a - zero_point.to(eff_scale.dtype)
251+
dq_a = dq_a * eff_scale
252+
253+
# --- Path B: scale to 4 (scale * 1.5) ---
254+
eff_scale_b = eff_scale * 1.5
255+
scaled_b = x / eff_scale_b
256+
if zero_point is not None:
257+
scaled_b = scaled_b + zero_point.to(x.dtype)
258+
q_b = round_to_quantized_type_args(tensor=scaled_b, args=args, min=q_min, max=q_max)
259+
dq_b = q_b.to(eff_scale_b.dtype)
260+
if zero_point is not None:
261+
dq_b = dq_b - zero_point.to(eff_scale_b.dtype)
262+
dq_b = dq_b * eff_scale_b
263+
264+
# --- Per-group MSE comparison ---
265+
group_dims = tuple(range(scale.ndim, x.ndim))
266+
mse_a = ((x - dq_a) ** 2).mean(dim=group_dims, keepdim=True)
267+
mse_b = ((x - dq_b) ** 2).mean(dim=group_dims, keepdim=True)
268+
269+
use_b = mse_b < mse_a
270+
return torch.where(use_b, dq_b, dq_a)
271+
272+
213273
@torch.no_grad()
214274
def _quantize(
215275
x: torch.Tensor,

src/compressed_tensors/quantization/quant_args.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,17 @@ class QuantizationArgs(BaseModel, use_enum_values=True):
224224
"Observers constructor excluding quantization range or symmetry"
225225
),
226226
)
227+
four_over_six: bool = Field(
228+
default=False,
229+
exclude=True,
230+
description=(
231+
"Enable Four Over Six (4/6) adaptive block scaling for NVFP4 "
232+
"quantization. For each group of values, tries scaling to both 4 "
233+
"and 6, selecting the scale with lower MSE. Reduces quantization "
234+
"error for near-maximal values in FP4. Only applies to FP4 "
235+
"quantization (num_bits=4, type=float)."
236+
),
237+
)
227238

228239
@field_serializer("zp_dtype")
229240
def serialize_dtype(self, dtype: torch.dtype):

src/compressed_tensors/quantization/quant_scheme.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,47 @@ def is_preset_scheme(name: str) -> bool:
180180
),
181181
)
182182

183+
NVFP4A16_46 = dict(
184+
weights=QuantizationArgs(
185+
num_bits=4,
186+
type=QuantizationType.FLOAT,
187+
strategy=QuantizationStrategy.TENSOR_GROUP,
188+
symmetric=True,
189+
dynamic=False,
190+
group_size=16,
191+
scale_dtype=FP8_E4M3_DATA.dtype,
192+
zp_dtype=FP8_E4M3_DATA.dtype,
193+
four_over_six=True,
194+
)
195+
)
196+
197+
198+
NVFP4_46 = dict(
199+
weights=QuantizationArgs(
200+
num_bits=4,
201+
type=QuantizationType.FLOAT,
202+
strategy=QuantizationStrategy.TENSOR_GROUP,
203+
symmetric=True,
204+
dynamic=False,
205+
group_size=16,
206+
scale_dtype=FP8_E4M3_DATA.dtype,
207+
zp_dtype=FP8_E4M3_DATA.dtype,
208+
four_over_six=True,
209+
),
210+
input_activations=QuantizationArgs(
211+
num_bits=4,
212+
type=QuantizationType.FLOAT,
213+
strategy=QuantizationStrategy.TENSOR_GROUP,
214+
symmetric=True,
215+
dynamic=DynamicType.LOCAL,
216+
group_size=16,
217+
observer="static_minmax",
218+
scale_dtype=FP8_E4M3_DATA.dtype,
219+
zp_dtype=FP8_E4M3_DATA.dtype,
220+
four_over_six=True,
221+
),
222+
)
223+
183224
MXFP4A16 = dict(
184225
weights=QuantizationArgs(
185226
num_bits=4,
@@ -422,7 +463,9 @@ def is_preset_scheme(name: str) -> bool:
422463
"FP8_DYNAMIC": FP8_DYNAMIC,
423464
"FP8_BLOCK": FP8_BLOCK,
424465
"NVFP4A16": NVFP4A16,
466+
"NVFP4A16_46": NVFP4A16_46,
425467
"NVFP4": NVFP4,
468+
"NVFP4_46": NVFP4_46,
426469
"MXFP4A16": MXFP4A16,
427470
"MXFP4": MXFP4,
428471
"MXFP8A16": MXFP8A16,

src/compressed_tensors/quantization/utils/helpers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def generate_gparam(
312312
scale_data: FloatArgs | None = FP8_E4M3_DATA,
313313
quant_data: FloatArgs | None = FP4_E2M1_DATA,
314314
dtype: torch.dtype | None = torch.float32,
315+
four_over_six: bool = False,
315316
):
316317
"""
317318
Generate a global scale for an entire tensor (input_tensor).
@@ -321,12 +322,19 @@ def generate_gparam(
321322
E.g. for NVFP4, group (local) scales are in dtype FP8. The global_scale
322323
attempts to use the entire FP8 dtype range while mapping a per-group max
323324
to the FP4 max.
325+
326+
When four_over_six=True, uses 256 instead of 448 for the FP8 scale max.
327+
This allows blocks containing the tensor's largest values to have their
328+
FP8 scale multiplied by 1.5 (for scale-to-4) without overflowing
329+
(256 * 1.5 = 384 < 448).
324330
"""
325331
min_vals = torch.min(updated_min_val, torch.zeros_like(updated_min_val))
326332
max_vals = torch.max(updated_max_val, torch.zeros_like(updated_max_val))
327333
max_val_pos = torch.max(torch.abs(min_vals), torch.abs(max_vals))
328334
max_val_pos = torch.clamp(max_val_pos, min=torch.finfo(max_val_pos.dtype).tiny)
329-
global_scale = scale_data.max * quant_data.max / max_val_pos
335+
336+
scale_max = 256.0 if four_over_six else scale_data.max
337+
global_scale = scale_max * quant_data.max / max_val_pos
330338

331339
# Replace any NaN or Inf with 1.0. NaN arises when max_val_pos was NaN
332340
# (clamp does not propagate NaN, so it passes through to the division).

0 commit comments

Comments
 (0)