@@ -157,7 +157,8 @@ def emit_lifted_constant(P: "MLXProgramBuilder", value, dtype: torch.dtype) -> S
157157
158158 if isinstance (value , (int , float , bool )):
159159 return P .make_or_get_constant (
160- f"_scalar_{ value } " , torch .tensor (value , dtype = dtype ) # 0-D
160+ f"_scalar_{ value } " ,
161+ torch .tensor (value , dtype = dtype ), # 0-D
161162 )
162163
163164 from executorch .backends .mlx .serialization .mlx_graph_schema import FullNode
@@ -533,11 +534,10 @@ def to_mlx_qparams(
533534 assert qdata .dtype == torch .int8
534535 offset = 2 ** (bits - 1 )
535536
536- # Pack data tightly into uint32
537- assert 32 % bits == 0
538- vals_per_uint32 = 32 // bits
539- assert qdata .shape [1 ] % vals_per_uint32 == 0
537+ # Pack data into a contiguous uint32 bitstream. cols*bits must be a
538+ # multiple of 32 (holds since in_features is a multiple of group_size>=32).
540539 rows , cols = qdata .shape
540+ assert (cols * bits ) % 32 == 0
541541
542542 if bits == 4 :
543543 # 4-bit: view(uint8) + wrapping add + pack 2 nibbles per byte → view as uint32
@@ -554,14 +554,18 @@ def to_mlx_qparams(
554554 q = qdata .view (torch .uint8 ) + offset
555555 Q = q .contiguous ().view (torch .uint32 ).reshape (rows , - 1 )
556556 else :
557- # General fallback for other bit widths
558- Q = (qdata .to (torch .int32 ) + offset ).reshape (- 1 , vals_per_uint32 )
559- shifts = torch .arange (0 , 32 , bits , dtype = torch .int32 )
560- shifted = Q << shifts
561- packed = shifted [:, 0 ]
562- for i in range (1 , vals_per_uint32 ):
563- packed = packed | shifted [:, i ]
564- Q = packed .view (torch .uint32 ).reshape (rows , - 1 )
557+ # Contiguous bit-packing for widths that don't divide 32 (e.g. 6-bit),
558+ # matching MLX's affine pack_and_quantize (ops.cpp): each value
559+ # contributes `bits` LSB-first bits to a continuous stream that is
560+ # chunked into uint32 words (also LSB-first), straddling word
561+ # boundaries -> (rows, cols*bits//32) uint32.
562+ q = qdata .to (torch .int32 ) + offset # 0 .. 2**bits-1
563+ bit_ids = torch .arange (bits , dtype = torch .int32 )
564+ stream = (q .unsqueeze (- 1 ) >> bit_ids ) & 1 # (rows, cols, bits) LSB-first
565+ stream = stream .reshape (rows , - 1 , 32 ) # (rows, n_words, 32)
566+ word_shifts = torch .arange (32 , dtype = torch .int64 )
567+ packed = (stream .to (torch .int64 ) << word_shifts ).sum (- 1 ) # (rows, n_words)
568+ Q = packed .to (torch .int32 ).contiguous ().view (torch .uint32 )
565569
566570 if compute_biases :
567571 B = - scale * (zero_point .to (scale .dtype ) + offset )
@@ -654,10 +658,11 @@ def parse_dequant_node(
654658 if group_size not in [16 , 32 , 64 , 128 ]:
655659 return None
656660
657- # TODO: MLX supports 3, 5, and 7, but we need to figure out the
658- # packing story in to_mlx_qparams to use them
661+ # MLX supports 2,3,4,5,6,8-bit affine quantization. to_mlx_qparams packs
662+ # 2/4/8 via fast paths and other widths (e.g. 6) via a general contiguous
663+ # bit-packer, so enable 6 here too.
659664 bits = (qmax - qmin + 1 ).bit_length () - 1
660- if bits not in [2 , 4 , 8 ]:
665+ if bits not in [2 , 4 , 6 , 8 ]:
661666 return None
662667 return qdata , scale , zero_point , group_size , bits , out_dtype , quantized_dim
663668
0 commit comments