1717 via a module-level config, by name and by type, on a bare composite, on a
1818 mixed model with other quantized ops, and on a multi-tensor (q / k / v) SDPA
1919 composite; a distinct dtype on the composite config proves it outranks the
20- global spec at the boundary (``TestCompositeOpIOQuantization``).
20+ global spec at the boundary (``TestCompositeOpIOQuantization``). A proper
21+ subset of integer input indices selects exactly those positional args, which
22+ pins the index -> argument mapping the wildcard cannot
23+ (``test_composite_boundary_input_index_selects_those_args``).
2124
2225End-to-end lowering and execution tests live in
2326``tests/export/test_graph_mode_mlir_export.py::test_composite_externalize_export``.
2932import torch
3033import torch .nn as nn
3134from coreai_torch import ExternalizeSpec , _patch_model_for_externalization
32- from coreai_torch .composite_ops import SDPA , RMSNormImpl
3335
3436from coreai_opt import ExportBackend
3537from coreai_opt .quantization import (
4951 CompositeRMSNormModel ,
5052 CompositeRMSNormOnlyModel ,
5153 CompositeSDPAModel ,
54+ rmsnorm_externalize_spec ,
55+ sdpa_externalize_spec ,
5256)
5357from tests .test_utils .general import (
5458 assert_single_call_function_node ,
5761 is_coreai_quantize ,
5862)
5963
60- _RMSNORM_SPEC = ExternalizeSpec (
61- target_class = RMSNormImpl ,
62- composite_op_name = "rms_norm" ,
63- composite_attrs = ["axes" , "eps" ],
64- )
65- _SDPA_SPEC = ExternalizeSpec (
66- target_class = SDPA ,
67- composite_op_name = "scaled_dot_product_attention" ,
68- composite_attrs = ["scale" , "is_causal" , "window_size" ],
69- )
64+ _RMSNORM_SPEC = rmsnorm_externalize_spec ()
65+ _SDPA_SPEC = sdpa_externalize_spec ()
7066
7167
7268@pytest .mark .parametrize (
@@ -139,24 +135,34 @@ class TestCompositeOpIOQuantization:
139135 _COMPOSITE_ACT_DTYPE = torch .uint8
140136
141137 @classmethod
142- def _config (cls , spec : ExternalizeSpec , module_name : str , target_by : str ) -> QuantizerConfig :
138+ def _composite_act_spec (cls ) -> QuantizationSpec :
143139 # The composite config must use a dtype DISTINCT from the global
144140 # (default) activation dtype: a matching dtype collapses via observer
145141 # sharing into a vacuous no-op, so the composite's effect at the
146142 # boundary would not be observable.
147143 assert cls ._COMPOSITE_ACT_DTYPE != default_activation_quantization_spec ().dtype
148- composite_act = QuantizationSpec (
144+ return QuantizationSpec (
149145 dtype = cls ._COMPOSITE_ACT_DTYPE ,
150146 qscheme = QuantizationScheme .SYMMETRIC ,
151147 granularity = PerTensorGranularity (),
152148 )
149+
150+ @classmethod
151+ def _config (
152+ cls ,
153+ spec : ExternalizeSpec ,
154+ module_name : str ,
155+ target_by : str ,
156+ module_input_spec : dict | None = None ,
157+ ) -> QuantizerConfig :
158+ composite_act = cls ._composite_act_spec ()
153159 global_config = ModuleQuantizerConfig (
154160 op_state_spec = {"weight" : default_weight_quantization_spec ()},
155161 op_input_spec = {"*" : default_activation_quantization_spec ()},
156162 op_output_spec = {"*" : default_activation_quantization_spec ()},
157163 )
158164 composite_config = ModuleQuantizerConfig (
159- module_input_spec = {"*" : composite_act },
165+ module_input_spec = module_input_spec or {"*" : composite_act },
160166 module_output_spec = {"*" : composite_act },
161167 )
162168 if target_by == "name" :
@@ -172,12 +178,13 @@ def _finalize(
172178 spec : ExternalizeSpec ,
173179 module_name : str ,
174180 target_by : str ,
181+ module_input_spec : dict | None = None ,
175182 ) -> tuple [torch .fx .GraphModule , str ]:
176183 _patch_model_for_externalization (model , [spec ])
177184 op_name = model .get_submodule (module_name )._externalize_op_name
178185 target_substr = f"coreai_torch_ext.{ op_name } "
179186
180- quantizer = Quantizer (model , self ._config (spec , module_name , target_by ))
187+ quantizer = Quantizer (model , self ._config (spec , module_name , target_by , module_input_spec ))
181188 prepared = quantizer .prepare ((sample ,))
182189 assert_single_call_function_node (prepared , target_substr , stage = "prepared" )
183190
@@ -234,3 +241,72 @@ def test_composite_boundary_quantized(
234241 sample = torch .randn (2 , 4 , 32 , dtype = torch .float16 )
235242 finalized , target_substr = self ._finalize (model , sample , spec , module_name , target_by )
236243 self ._assert_boundary_quantized (finalized , target_substr , num_tensor_inputs )
244+
245+ @pytest .mark .parametrize ("target_by" , ["name" , "type" ])
246+ def test_composite_boundary_input_index_selects_those_args (self , target_by : str ) -> None :
247+ """Integer keys in ``module_input_spec`` quantize exactly those positional args
248+ for composite ops.
249+
250+ The unselected input is left unquantized rather than falling
251+ back to the global spec, because the composite is opaque to the
252+ op-pattern annotator and only a module-level config reaches its edges.
253+ """
254+ quantized_indices = (0 , 2 )
255+ num_tensor_inputs = 3
256+ model = CompositeSDPAModel ().eval ().half ()
257+ sample = torch .randn (2 , 4 , 32 , dtype = torch .float16 )
258+
259+ finalized , target_substr = self ._finalize (
260+ model ,
261+ sample ,
262+ _SDPA_SPEC ,
263+ "composite" ,
264+ target_by ,
265+ module_input_spec = {i : self ._composite_act_spec () for i in quantized_indices },
266+ )
267+
268+ composite = assert_single_call_function_node (finalized , target_substr , stage = "finalized" )
269+ tensor_inputs = [
270+ a for a in composite .args if isinstance (a , torch .fx .Node ) and a .op != "get_attr"
271+ ]
272+ assert len (tensor_inputs ) == num_tensor_inputs , (
273+ f"Expected { num_tensor_inputs } tensor inputs to { composite .name } , "
274+ f"got { [n .name for n in tensor_inputs ]} "
275+ )
276+
277+ # Each selected index must be fed by a dequantize whose producing
278+ # quantize carries the composite dtype; each unselected index must not
279+ # be quantized at all.
280+ for index , act_input in enumerate (tensor_inputs ):
281+ if index in quantized_indices :
282+ assert is_coreai_dequantize (act_input .target ), (
283+ f"input { index } was selected by module_input_spec but is not fed by "
284+ f"a dequantize: { act_input .target } "
285+ )
286+ input_dtype = get_quantize_dtype (act_input .args [0 ])
287+ assert input_dtype == self ._COMPOSITE_ACT_DTYPE , (
288+ f"input { index } was selected by module_input_spec but is quantized as "
289+ f"{ input_dtype } , expected the composite dtype { self ._COMPOSITE_ACT_DTYPE } "
290+ )
291+ else :
292+ assert not is_coreai_dequantize (act_input .target ), (
293+ f"input { index } was not selected by module_input_spec but is fed by "
294+ f"a dequantize: { act_input .target } "
295+ )
296+
297+ # module_output_spec stays the wildcard, so the composite's consumer is
298+ # quantized with the composite dtype regardless of which inputs were selected.
299+ consumers = list (composite .users )
300+ assert len (consumers ) == 1 , (
301+ f"expected the composite to have exactly one consumer, got "
302+ f"{ [n .name for n in consumers ]} "
303+ )
304+ consumer = consumers [0 ]
305+ assert is_coreai_quantize (consumer .target ), (
306+ f"the composite's consumer is not a quantize node: { consumer .target } "
307+ )
308+ consumer_dtype = get_quantize_dtype (consumer )
309+ assert consumer_dtype == self ._COMPOSITE_ACT_DTYPE , (
310+ f"the composite's output is quantized as { consumer_dtype } , expected the "
311+ f"composite dtype { self ._COMPOSITE_ACT_DTYPE } "
312+ )
0 commit comments