def test_quantization_conv_transpose_axis(
        self, compute_unit, backend, mode, nbits, granularity
    ):
        """The conv_transpose has [Cin, Cout, ...], which is different from conv."""
        (
            model,
            inputs,
            torch_input_values,
            coreml_input_values,
        ) = get_test_model_and_data_conv_transpose()
        torchmodel = torch.jit.trace(model, torch_input_values)
        mlmodel = ct.convert(
            torchmodel,
            inputs=inputs,
            convert_to="mlprogram",
            minimum_deployment_target=backend.opset_version,
            compute_precision=ct.precision.FLOAT16
            if backend.precision == "fp16"
            else ct.precision.FLOAT32,
            compute_units=compute_unit,
        )

        dtype_str = f"int{nbits}"
        op_config = cto.coreml.OpLinearQuantizerConfig(
            mode=mode, dtype=dtype_str, granularity=granularity
        )
        config = cto.coreml.OptimizationConfig(global_config=op_config)

        if backend.opset_version < ct.target.iOS18:
            if granularity == "per_block":
                with pytest.raises(ValueError, match=self._ERR_MSG.format("per_block")):
                    cto.coreml.linear_quantize_weights(mlmodel, config)
                return
            if nbits == 4:
                with pytest.raises(ValueError, match=self._ERR_MSG.format("4-bit")):
                    cto.coreml.linear_quantize_weights(mlmodel, config)
                return

        mlmodel_quantized = cto.coreml.linear_quantize_weights(mlmodel, config)

        # Verify ops.
        quant_op_name = (
            "constexpr_blockwise_shift_scale"
            if backend.opset_version >= ct.target.iOS18
            else "constexpr_affine_dequantize"
        )
        if backend.precision == "fp16":
            # For fp16 precision there is no extra cast op inserted.
            expected_ops = [
                quant_op_name,
                "conv_transpose",
                "conv_transpose",
                "add",
            ]
        else:
            expected_ops = [
                quant_op_name,
                "cast",
                "conv_transpose",
                "conv_transpose",
                "add",
                "cast",
            ]
        assert get_op_types_in_program(mlmodel_quantized._mil_program) == expected_ops

        # Verify quantization ops are on the expected axis.
        quantize_op = mlmodel_quantized._mil_program.functions["main"].find_ops(
            op_type=quant_op_name
        )[0]
        if backend.opset_version >= ct.target.iOS18:
            assert types.builtin_to_string(quantize_op.data.dtype) == dtype_str
            if granularity == "per_tensor":
                expected_scale_shape = (1, 1, 1, 1)
            elif granularity == "per_channel":
                # The weight has shape [64, 32, 2, 2], and the second axis is output channel.
                expected_scale_shape = (1, 32, 1, 1)
            else:
                # The per_block has default block_size 32.
                expected_scale_shape = (64 // 32, 32, 1, 1)
            assert quantize_op.scale.shape == expected_scale_shape

        if _macos_version() >= (15, 0):
            verify_model_outputs(mlmodel, mlmodel_quantized, coreml_input_values, atol=2e-2)
The conv_transpose has [Cin, Cout, ...], which is different from conv.
coremltools/test/optimize/coreml/test_post_training_quantization.py