def test_normalize(self, op_type, has_relu_fusion, input_rank, is_axis_x_present):
        """
        Input graph:
            x -> quantize(scale_x) -> dequantize(scale_x) -|
                                                           |-> add/sub (-> relu) -> dequantize(scale_z) -> output
            y -> quantize(scale_y) -> dequantize(scale_y) -|

        Output graph:
            x -> quantize(scale_x) -> dequantize(scale_x/scale_y) -|
                                                                   |-> add/sub (-> relu) -> dequantize(scale_z/scale_y) -> output
            y -> quantize(scale_y) -> dequantize(1.0)             -|

        x and y may get swapped to have the one with scalar scale being new "y"
        """

        # if axis_x is present, then axis_y is not, vice versa,
        # so that one of scale_x or scale_y is scalar
        SHAPE = np.random.randint(1, 5, size=input_rank, dtype=np.int32)
        scale_x, zero_point_x, axis_x = self.generate_random_quantization_params(
            np.float32, np.int8, SHAPE, True, is_axis_x_present
        )
        scale_y, zero_point_y, axis_y = self.generate_random_quantization_params(
            np.float32, np.int8, SHAPE, True, not is_axis_x_present
        )
        scale_z, zero_point_z, axis_z = self.generate_random_quantization_params(
            np.float32, np.int8, SHAPE, True, True
        )

        @mb.program(
            input_specs=[
                mb.TensorSpec(shape=SHAPE, dtype=types.fp32),
                mb.TensorSpec(shape=SHAPE, dtype=types.fp32),
            ]
        )
        def prog(x, y):
            # quantize input
            quantize_x = mb.quantize(
                input=x, scale=scale_x, zero_point=zero_point_x, axis=axis_x, output_dtype="int8"
            )
            quantize_y = mb.quantize(
                input=y, scale=scale_y, zero_point=zero_point_y, axis=axis_y, output_dtype="int8"
            )
            # quantized binary op
            dequantize_x = mb.dequantize(
                input=quantize_x, scale=scale_x, zero_point=zero_point_x, axis=axis_x
            )
            dequantize_y = mb.dequantize(
                input=quantize_y, scale=scale_y, zero_point=zero_point_y, axis=axis_y
            )
            z = None
            if op_type == "add":
                z = mb.add(x=dequantize_x, y=dequantize_y)
            elif op_type == "sub":
                z = mb.sub(x=dequantize_x, y=dequantize_y)
            else:
                raise ValueError("unsupported op type")
            if has_relu_fusion:
                z = mb.relu(x=z)
            quantize_z = mb.quantize(
                input=z, scale=scale_z, zero_point=zero_point_z, axis=axis_z, output_dtype="int8"
            )
            # dequantize output
            dequantize_z = mb.dequantize(
                input=quantize_z, scale=scale_z, zero_point=zero_point_z, axis=axis_z
            )
            return dequantize_z

        # dequantize_x, dequantize_y, z
        prev_prog, _, _ = apply_pass_and_basic_check(
            prog, "common::distributive_quantized_binary_op_scale_normalization"
        )
        # dequantize_x, dequantize_y, dequantize_x_normalized, dequantize_y_normalized, z
        _, _, _ = apply_pass_and_basic_check(prog, "common::dead_code_elimination")
        # dequantize_x_normalized, dequantize_y_normalized, z

        scale_prev_dequantize_x = prev_prog.find_ops(op_type="dequantize")[0].scale.val
        scale_prev_dequantize_y = prev_prog.find_ops(op_type="dequantize")[1].scale.val
        scale_prev_quantize_z = prev_prog.find_ops(op_type="quantize")[-1].scale.val
        assert np.all(scale_prev_dequantize_x == scale_x)
        assert np.all(scale_prev_dequantize_y == scale_y)
        assert np.all(scale_prev_quantize_z == scale_z)

        scale_dequantize_x = prog.find_ops(op_type="dequantize")[0].scale.val
        scale_dequantize_y = prog.find_ops(op_type="dequantize")[1].scale.val
        scale_quantize_z = prog.find_ops(op_type="quantize")[-1].scale.val
        # if axis_x is present, then scale_y gets normalized
        # else, scale_x gets normalized, and x and y will get swapped
        assert np.all(
            scale_dequantize_x == scale_x / scale_y if is_axis_x_present else scale_y / scale_x
        )
        assert np.all(scale_dequantize_y == 1.0)
        assert np.all(
            scale_quantize_z == scale_z / scale_y if is_axis_x_present else scale_z / scale_x
        )

        prev_model = ct.convert(
            prev_prog,
            source="milinternal",
            convert_to="mlprogram",
            compute_precision=ct.precision.FLOAT32,
            minimum_deployment_target=ct.target.iOS17,
        )
        model = ct.convert(
            prog,
            source="milinternal",
            convert_to="mlprogram",
            compute_precision=ct.precision.FLOAT32,
            minimum_deployment_target=ct.target.iOS17,
        )

        x = self.generate_random_quantize_input(
            np.float32, np.int8, scale_x, zero_point_x, axis_x, SHAPE
        )
        y = self.generate_random_quantize_input(
            np.float32, np.int8, scale_y, zero_point_y, axis_y, SHAPE
        )
        prev_output = list(prev_model.predict({"x": x, "y": y}).values())[0]
        output = list(model.predict({"x": x, "y": y}).values())[0]
        assert np.all(prev_output == output)

        Input graph:
            x -> quantize(scale_x) -> dequantize(scale_x) -|
                                                           |-> add/sub (-> relu) -> dequantize(scale_z) -> output
            y -> quantize(scale_y) -> dequantize(scale_y) -|

        Output graph:
            x -> quantize(scale_x) -> dequantize(scale_x/scale_y) -|
                                                                   |-> add/sub (-> relu) -> dequantize(scale_z/scale_y) -> output
            y -> quantize(scale_y) -> dequantize(1.0)             -|

        x and y may get swapped to have the one with scalar scale being new "y"
        
coremltools/converters/mil/mil/passes/tests/test_quantization_passes.py