diff --git a/backends/qualcomm/quantizer/annotators/htp_rules.py b/backends/qualcomm/quantizer/annotators/htp_rules.py index 14775b6600a..c68e855856e 100644 --- a/backends/qualcomm/quantizer/annotators/htp_rules.py +++ b/backends/qualcomm/quantizer/annotators/htp_rules.py @@ -842,15 +842,18 @@ def annotate(node: Node, quantization_config: QuantizationConfig) -> None: input_qspec_map = {} input_qspec = quantization_config.input_activation output_qspec = None - if input_qspec is not None: + if input_qspec is not None and _is_float_tensor(value): input_qspec_map[value] = input_qspec output_qspec = SharedQuantizationSpec((value, node)) - node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation( - input_qspec_map=input_qspec_map, - output_qspec=output_qspec, - _annotated=True, - ) + # A non-float value leaves nothing to quantize; leave the node unannotated + # rather than marking it annotated with an empty spec (its output stays non-float). + if len(input_qspec_map) > 0 or output_qspec is not None: + node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation( + input_qspec_map=input_qspec_map, + output_qspec=output_qspec, + _annotated=True, + ) @register_annotator( diff --git a/backends/qualcomm/quantizer/annotators/lpai_rules.py b/backends/qualcomm/quantizer/annotators/lpai_rules.py index 95864f308ce..fa68a9d3d8c 100644 --- a/backends/qualcomm/quantizer/annotators/lpai_rules.py +++ b/backends/qualcomm/quantizer/annotators/lpai_rules.py @@ -466,13 +466,20 @@ def annotate(node: Node, quantization_config: QuantizationConfig) -> None: value = node.args[2] input_qspec_map = {} - input_qspec_map[value] = quantization_config.input_activation - - node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation( - input_qspec_map=input_qspec_map, - output_qspec=SharedQuantizationSpec((value, node)), - _annotated=True, - ) + output_qspec = None + input_qspec = quantization_config.input_activation + if input_qspec is not None and _is_float_tensor(value): + input_qspec_map[value] = input_qspec + output_qspec = SharedQuantizationSpec((value, node)) + + # A non-float value leaves nothing to quantize; leave the node unannotated + # rather than marking it annotated with an empty spec (its output stays non-float). + if len(input_qspec_map) > 0 or output_qspec is not None: + node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation( + input_qspec_map=input_qspec_map, + output_qspec=output_qspec, + _annotated=True, + ) @register_annotator( diff --git a/backends/qualcomm/tests/test_passes.py b/backends/qualcomm/tests/test_passes.py index 7ba0e4f9de1..9004deda4f1 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -470,6 +470,54 @@ def test_decompose_hardsigmoid_backend_aware(self): f"hardsigmoid {'should' if should_decompose else 'should NOT'} be decomposed for {backend.name}", ) + def test_index_put_int64_value_not_quantized(self): + """QNN's IndexPut annotator must skip a non-float (int64) value arg. + + Regression for MoE (Mixtral) routing, where index_put's value is an int64 + arange: annotating it makes quantize_per_tensor assert a float input, so + to_executorch() fails. Only float tensors may be annotated. + Exercised on both the HTP and LPAI annotators, which share the guard. + """ + + class IndexPutInt64Value(torch.nn.Module): + def forward(self, x): + buf = torch.zeros(4, dtype=torch.long) + idx = torch.arange( + 4, dtype=torch.int64 + ) # int64 value written by index_put + buf = buf.index_put((torch.tensor([0, 1, 2, 3]),), idx) + return x + buf.to(torch.float32) + + module = IndexPutInt64Value().eval() + sample_input = (torch.randn(4),) + + def run_backend(backend): + quantizer = QnnQuantizer(backend=backend) + quantizer.set_default_quant_config(quant_dtype=QuantDtype.use_8a8w) + gm = ( + torch.export.export(module, sample_input) + .run_decompositions({}) + .module() + ) + prepared = prepare_pt2e(gm, quantizer) + prepared(*sample_input) + converted = convert_pt2e(prepared) + # Re-export runs the quantize_per_tensor meta kernel; before the dtype + # guard this raised "Expecting input to have dtype torch.float32" on the + # int64 value. + torch.export.export(converted, sample_input) + + # HTP is the core QNN quantizer backend; a failure here is a real regression, + # not something to swallow. LPAI additionally needs quantized_aot_lib, so only + # that missing optional dependency is allowed to skip the LPAI leg. + run_backend(QnnExecuTorchBackendType.kHtpBackend) + try: + run_backend(QnnExecuTorchBackendType.kLpaiBackend) + except Exception as e: + if "quantized_aot_lib" in str(e): + self.skipTest(f"LPAI quantizer unavailable: {e}") + raise + if __name__ == "__main__": unittest.main()