def test_joint_prune_palettize_quantize_weights(
        self, compute_unit, backend, nbits, channel_group_size, target_sparsity
    ):
        """
        First prune to get sparse weight, and then palettize the non-sparse entries to get fp16
        lut, and then quantize the lut to make int8 lut.
        """
        model, inputs, torch_input_values, coreml_input_values = get_test_model_and_data_complex()
        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,
        )

        prune_config = cto.coreml.OptimizationConfig(
            global_config=cto.coreml.OpMagnitudePrunerConfig(
                target_sparsity=target_sparsity, weight_threshold=500
            )
        )
        palettize_config = cto.coreml.OptimizationConfig(
            global_config=cto.coreml.OpPalettizerConfig(
                mode="kmeans",
                nbits=nbits,
                granularity="per_grouped_channel",
                group_size=channel_group_size,
                weight_threshold=500,
            )
        )
        quant_config = cto.coreml.OptimizationConfig(
            global_config=cto.coreml.OpLinearQuantizerConfig(
                mode="linear",
                dtype="int8",
                granularity="per_tensor",
                weight_threshold=200,  # Need to be smaller than entries in lut (2**8=256).
            )
        )

        pytest_context_manager = nullcontext()
        if backend.opset_version < ct.target.iOS18:
            pytest_context_manager = pytest.raises(ValueError, match=self._ERR_MSG)

        with pytest_context_manager:
            mlmodel_pruned = cto.coreml.prune_weights(mlmodel, prune_config)
            mlmodel_joint_pruned_palettized = cto.coreml.palettize_weights(
                mlmodel_pruned, palettize_config, joint_compression=True
            )
            mlmodel_joint_pruned_palettized_quantized = cto.coreml.linear_quantize_weights(
                mlmodel_joint_pruned_palettized, quant_config, joint_compression=True
            )

        if backend.opset_version < ct.target.iOS18:
            return

        expected_ops = (
            [
                "constexpr_blockwise_shift_scale",
                "constexpr_lut_to_sparse",
                "constexpr_sparse_to_dense",
                "conv",
            ]
            * 2
            + ["reshape"]
            + [
                "constexpr_blockwise_shift_scale",
                "constexpr_lut_to_sparse",
                "constexpr_sparse_to_dense",
                "linear",
            ]
            * 2
            + ["constexpr_sparse_to_dense"]
            + [
                "constexpr_blockwise_shift_scale",
                "constexpr_lut_to_sparse",
                "constexpr_sparse_to_dense",
            ]
            * 2
            + ["lstm"]
        )
        if nbits < 4 and channel_group_size == 0:
            # The lut tensor is too small, which is stored as immediate values.
            expected_ops = [
                expected_op
                for expected_op in expected_ops
                if expected_op != "constexpr_blockwise_shift_scale"
            ]
        prog = mlmodel_joint_pruned_palettized_quantized._mil_program
        assert get_op_types_in_program(prog) == expected_ops

        for linear_op in prog.find_ops(op_type="linear"):
            assert linear_op.weight.op.op_type == "constexpr_sparse_to_dense"
        for conv_op in prog.find_ops(op_type="conv"):
            assert conv_op.weight.op.op_type == "constexpr_sparse_to_dense"

        for quantize_op in prog.find_ops(op_type="constexpr_blockwise_shift_scale"):
            assert types.builtin_to_string(quantize_op.data.dtype) == "int8"
            assert types.builtin_to_string(quantize_op.scale.dtype) == backend.precision
            assert types.builtin_to_string(quantize_op.offset.dtype) == "int8"
            assert quantize_op.outputs[0].child_ops[0].op_type == "constexpr_lut_to_sparse"

        for sparse_palettize_op in prog.find_ops(op_type="constexpr_lut_to_sparse"):
            assert (
                types.builtin_to_string(sparse_palettize_op.indices_nonzero_data.dtype)
                == f"uint{nbits}"
            )
            assert sparse_palettize_op.indices_mask.dtype == types.uint1
            assert (
                sparse_palettize_op.outputs[1].child_ops[0].op_type == "constexpr_sparse_to_dense"
            )

        for sparse_op in prog.find_ops(op_type="constexpr_sparse_to_dense"):
            assert sparse_op.mask.dtype == types.uint1
            assert types.builtin_to_string(sparse_op.nonzero_data.dtype) == backend.precision

        if _macos_version() >= (15, 0):
            verify_model_outputs(
                mlmodel,
                mlmodel_joint_pruned_palettized_quantized,
                coreml_input_values,
                atol=5e-4,
            )

        First prune to get sparse weight, and then palettize the non-sparse entries to get fp16
        lut, and then quantize the lut to make int8 lut.
        
coremltools/test/optimize/coreml/test_post_training_quantization.py