@@ -519,6 +519,7 @@ def to_mlx_qparams(
519519 zero_point : torch .Tensor ,
520520 bits : int ,
521521 compute_biases : bool = True ,
522+ prepacked : bool = False ,
522523) -> Tuple [torch .Tensor , Optional [torch .Tensor ]]:
523524 """
524525 Convert TorchAO quantization params to MLX format.
@@ -536,10 +537,31 @@ def to_mlx_qparams(
536537 Returns (Q, None) in this case. This is valid when
537538 zero_point is all zeros, as the C++ runtime will compute
538539 biases = -scales * 2^(bits-1).
540+ prepacked: If True, ``qdata`` is already nibble-packed uint8 holding the
541+ unsigned values ``q + offset`` (two 4-bit values per byte,
542+ even index -> low nibble) — the exact layout this function
543+ produces for ``bits == 4``. It is reinterpreted as uint32
544+ directly (``view``), skipping the int8 -> uint32 repack.
545+ Only supported for ``bits == 4``.
539546 """
540- assert qdata .dtype == torch .int8
541547 offset = 2 ** (bits - 1 )
542548
549+ if prepacked :
550+ assert bits == 4 , "prepacked to_mlx_qparams only supports 4-bit"
551+ assert (
552+ qdata .dtype == torch .uint8
553+ ), f"prepacked qdata must be uint8, got { qdata .dtype } "
554+ # (rows, cols//2) uint8 -> (rows, cols//8) uint32. cols//2 must be a
555+ # multiple of 4, i.e. in_features a multiple of 8 (holds: gs >= 32).
556+ assert qdata .shape [- 1 ] % 4 == 0 , "packed cols must be a multiple of 4"
557+ Q = qdata .contiguous ().view (torch .uint32 )
558+ if compute_biases :
559+ B = - scale * (zero_point .to (scale .dtype ) + offset )
560+ return Q , B
561+ return Q , None
562+
563+ assert qdata .dtype == torch .int8
564+
543565 # Pack data into a contiguous uint32 bitstream. cols*bits must be a
544566 # multiple of 32 (holds since in_features is a multiple of group_size>=32).
545567 rows , cols = qdata .shape
@@ -640,7 +662,9 @@ def parse_dequant_int4_node(
640662 """Parse a torchao.dequantize_int4_tensor node.
641663
642664 Returns (qdata, scale, zero_point, group_size, output_dtype) or None if not a
643- dequantize_int4_tensor node or the custom op is not registered.
665+ dequantize_int4_tensor node or the custom op is not registered. ``group_size``
666+ is the MLX-legal group_size (16/32/64/128); coarser int4 groups (e.g. 256)
667+ are regrouped to it via ``regroup_affine_scales`` at emit time.
644668 """
645669 target = get_aten_target (node .target )
646670 try :
@@ -652,6 +676,10 @@ def parse_dequant_int4_node(
652676 return None
653677
654678 qdata , scale , zero_point , group_size = node .args [0 :4 ]
679+ mlx_group_size = mlx_affine_group_size (int (group_size ))
680+ if mlx_group_size is None :
681+ return None
682+ group_size = mlx_group_size
655683
656684 output_dtype = None
657685 if len (node .args ) > 4 :
@@ -662,6 +690,60 @@ def parse_dequant_int4_node(
662690 return qdata , scale , zero_point , group_size , output_dtype
663691
664692
693+ _MLX_AFFINE_GROUP_SIZES = (128 , 64 , 32 , 16 )
694+
695+
696+ def mlx_affine_group_size (group_size : int ) -> Optional [int ]:
697+ """Largest MLX-supported group_size (128/64/32/16) that divides ``group_size``.
698+
699+ MLX's affine quantized kernels only support group_size in {16, 32, 64, 128}.
700+ torchao may quantize with a coarser or per-axis group_size (e.g. 256 or a
701+ full row of 5376). A coarse group is a stack of finer groups that share one
702+ scale, so any legal divisor is exact after repeating scale/zero_point (see
703+ ``regroup_affine_scales``). Returns ``group_size`` unchanged when already
704+ legal, the coarsest legal divisor otherwise, or ``None`` when none divides.
705+ """
706+ if group_size in _MLX_AFFINE_GROUP_SIZES :
707+ return group_size
708+ for candidate in _MLX_AFFINE_GROUP_SIZES :
709+ if group_size % candidate == 0 :
710+ return candidate
711+ return None
712+
713+
714+ def regroup_affine_scales (
715+ scale : torch .Tensor ,
716+ zero_point : torch .Tensor ,
717+ in_features : int ,
718+ target_group_size : int ,
719+ ) -> Tuple [torch .Tensor , torch .Tensor , bool ]:
720+ """Repeat per-group scale/zero_point so the effective group_size becomes
721+ ``target_group_size`` (an MLX-legal size from ``mlx_affine_group_size``).
722+
723+ ``scale``/``zero_point`` are shaped ``[..., in_features // old_group_size]``.
724+ A coarse group shares one scale across ``old_group_size // target_group_size``
725+ finer groups, so repeat-interleaving along the last axis is numerically exact.
726+
727+ Returns ``(scale, zero_point, changed)``; a no-op (``changed=False``) when the
728+ group_size is already ``target_group_size``.
729+ """
730+ old_groups = scale .shape [- 1 ]
731+ old_group_size = in_features // old_groups
732+ assert (
733+ old_group_size >= target_group_size and old_group_size % target_group_size == 0
734+ ), (
735+ f"cannot regroup: weight group_size={ old_group_size } is finer than, or "
736+ f"not a multiple of, target_group_size={ target_group_size } — "
737+ f"repeat-interleave can only refine groups"
738+ )
739+ repeat = old_group_size // target_group_size
740+ if repeat == 1 :
741+ return scale , zero_point , False
742+ scale = scale .repeat_interleave (repeat , dim = - 1 )
743+ zero_point = zero_point .repeat_interleave (repeat , dim = - 1 )
744+ return scale , zero_point , True
745+
746+
665747def parse_dequant_node (
666748 node : Node ,
667749) -> Optional [Tuple [Node , Node , Node , int , int , Optional [torch .dtype ], int ]]:
@@ -673,7 +755,9 @@ def parse_dequant_node(
673755 - Conv2d weights (4D): block_size=[1, 32, 1, 1] → quantized_dim=1
674756
675757 Returns (qdata, scale, zero_point, group_size, bits, out_dtype, quantized_dim)
676- or None if unsupported.
758+ or None if unsupported. ``group_size`` is the MLX-legal group_size (16/32/64/
759+ 128); when the weight uses a coarser group, callers must regroup scale/
760+ zero_point to it via ``regroup_affine_scales`` before packing.
677761 """
678762 qdata , block_size , scale , zero_point , dtype , qmin , qmax = node .args [0 :7 ]
679763 out_dtype = (
@@ -687,8 +771,13 @@ def parse_dequant_node(
687771 if len (non_one ) != 1 :
688772 return None
689773 quantized_dim , group_size = non_one [0 ]
690- if group_size not in [16 , 32 , 64 , 128 ]:
774+ # MLX kernels only support group_size in {16,32,64,128}. Coarser/per-axis
775+ # groups are lowered by repeating scale/zero_point to the coarsest legal
776+ # divisor at emit time (regroup_affine_scales); reject if none divides.
777+ mlx_group_size = mlx_affine_group_size (group_size )
778+ if mlx_group_size is None :
691779 return None
780+ group_size = mlx_group_size
692781
693782 # MLX supports 2,3,4,5,6,8-bit affine quantization. to_mlx_qparams packs
694783 # 2/4/8 via fast paths and other widths (e.g. 5, 6) via a general
0 commit comments