3434import torch .nn .functional as F
3535from executorch .backends .cuda .coalesced_int4_tensor import CudaCoalescedInt4Tensor
3636from executorch .backends .cuda .quantize_op_dispatch .int4_dispatch import _dequant_matmul
37- from executorch .examples .models .gemma4_31b .quant .pack_cuda import pack_linear_for_cuda
38- from executorch .examples .models .gemma4_31b .quant .quantize import (
37+ from executorch .examples .models .gemma4_31b .cuda_packers import pack_linear_for_cuda
38+ from executorch .extension .llm .export .int4 import ExportableInt4Tensor
39+ from executorch .extension .llm .export .quant .quantize import (
3940 dequantize_weight ,
4041 quantize_weight ,
4142)
42- from executorch .examples . models . gemma4_31b .quant .recipe import QuantConfig
43+ from executorch .extension . llm . export .quant .recipe import QuantConfig
4344
4445
4546def _require_cuda (tc : unittest .TestCase ) -> None :
@@ -48,22 +49,26 @@ def _require_cuda(tc: unittest.TestCase) -> None:
4849
4950
5051def _make_int4_linear (N , K , group_size = 128 , symmetric = False , bias = False ):
51- """Build an nn.Linear with Int4Tensor weight and return (module, bf16_ref_weight) .
52+ """Build an nn.Linear with ExportableInt4Tensor weight + bf16 ref weight .
5253
53- The bf16 reference is the original unquantized weight, so tests can
54- measure quantization error against the true value.
54+ Mirrors production: weights are converted to ExportableInt4Tensor (the
55+ canonical portable int4 form) before packing for CUDA. The bf16 reference
56+ is the original unquantized weight, so tests can measure quantization
57+ error against the true value.
5558 """
5659 w_bf16 = torch .randn (N , K , dtype = torch .bfloat16 )
5760 config = QuantConfig (
5861 bits = 4 , group_size = group_size , symmetric = symmetric , method = "min_max"
5962 )
60- int4_w = quantize_weight (w_bf16 , config )
63+ exportable_w = ExportableInt4Tensor .from_int4_tensor (
64+ quantize_weight (w_bf16 , config )
65+ )
6166
6267 # device="cuda" so the random init draws from the CUDA RNG to match the
6368 # same random weight as regular int4 dispatch and fit the same numerical
6469 # error tolerance.
6570 module = nn .Linear (K , N , bias = bias , dtype = torch .bfloat16 , device = "cuda" )
66- pack_linear_for_cuda (module , {"weight" : int4_w })
71+ pack_linear_for_cuda (module , {"weight" : exportable_w })
6772 module .cuda ()
6873 return module , w_bf16 .cuda ()
6974
@@ -184,9 +189,11 @@ def _check(self, out, ref, tol=0.15):
184189 def test_to_cuda (self ):
185190 w_bf16 = torch .randn (256 , 512 , dtype = torch .bfloat16 )
186191 config = QuantConfig (bits = 4 , group_size = 128 , symmetric = False , method = "min_max" )
187- int4_w = quantize_weight (w_bf16 , config )
192+ exportable_w = ExportableInt4Tensor .from_int4_tensor (
193+ quantize_weight (w_bf16 , config )
194+ )
188195 module = nn .Linear (512 , 256 , bias = False )
189- pack_linear_for_cuda (module , {"weight" : int4_w })
196+ pack_linear_for_cuda (module , {"weight" : exportable_w })
190197 module = module .to ("cuda" )
191198 x = torch .randn (1 , 512 , dtype = torch .bfloat16 , device = "cuda" )
192199 self ._check (module (x ), F .linear (x , w_bf16 .cuda ()))
@@ -228,6 +235,12 @@ def _make_int4_tensor(N, K, group_size=128, symmetric=False):
228235 return quantize_weight (w , config ), w
229236
230237
238+ def _make_exportable_int4_tensor (N , K , group_size = 128 , symmetric = False ):
239+ """Build an ``ExportableInt4Tensor`` (canonical portable int4) + bf16 ref."""
240+ t , w = _make_int4_tensor (N , K , group_size = group_size , symmetric = symmetric )
241+ return ExportableInt4Tensor .from_int4_tensor (t ), w
242+
243+
231244@contextlib .contextmanager
232245def _record_int4_plain_mm ():
233246 """Record calls to the decode custom op without needing a GPU.
@@ -278,8 +291,8 @@ def test_stock_int4tensor_does_not_route_to_int4_plain_mm(self):
278291
279292 def test_coalesced_tensor_routes_to_int4_plain_mm (self ):
280293 """CudaCoalescedInt4Tensor with M<=4 routes to the decode custom op."""
281- t , _ = _make_int4_tensor (16 , 256 , group_size = 32 )
282- c = CudaCoalescedInt4Tensor .from_int4_tensor (t )
294+ t , _ = _make_exportable_int4_tensor (16 , 256 , group_size = 32 )
295+ c = CudaCoalescedInt4Tensor .from_exportable_int4_tensor (t )
283296 x = torch .randn (1 , 256 , dtype = torch .bfloat16 ) # M=1 (decode regime)
284297 with _record_int4_plain_mm () as calls :
285298 out = F .linear (x , c )
@@ -288,8 +301,8 @@ def test_coalesced_tensor_routes_to_int4_plain_mm(self):
288301
289302 def test_coalesced_tensor_prefill_uses_dequant (self ):
290303 """M>4 uses inline dequant (no custom op) and is numerically correct."""
291- t , _ = _make_int4_tensor (16 , 256 , group_size = 32 )
292- c = CudaCoalescedInt4Tensor .from_int4_tensor (t )
304+ t , _ = _make_exportable_int4_tensor (16 , 256 , group_size = 32 )
305+ c = CudaCoalescedInt4Tensor .from_exportable_int4_tensor (t )
293306 x = torch .randn (8 , 256 , dtype = torch .bfloat16 ) # M=8 > 4 (prefill regime)
294307 with _record_int4_plain_mm () as calls :
295308 out = F .linear (x , c )
@@ -312,10 +325,10 @@ def test_square_shape_not_misrouted(self):
312325 F .linear (x , t )
313326 self .assertEqual (calls , [])
314327
315- def test_from_int4_tensor_transpose_correct (self ):
316- """from_int4_tensor owns the (n_groups, N) -> (N, n_groups) transpose."""
317- t , _ = _make_int4_tensor (24 , 256 , group_size = 64 )
318- c = CudaCoalescedInt4Tensor .from_int4_tensor (t )
328+ def test_from_exportable_int4_tensor_transpose_correct (self ):
329+ """from_exportable_int4_tensor owns the (n_groups, N) -> (N, n_groups) transpose."""
330+ t , _ = _make_exportable_int4_tensor (24 , 256 , group_size = 64 )
331+ c = CudaCoalescedInt4Tensor .from_exportable_int4_tensor (t )
319332 n_groups = 256 // 64
320333 self .assertEqual (tuple (t .scale .shape ), (n_groups , 24 )) # torchao layout
321334 self .assertEqual (tuple (c .scale .shape ), (24 , n_groups )) # coalesced layout
0 commit comments