def materialize_dynamic_shape_mlmodel(
    dynamic_shape_mlmodel: "_ct.models.MLModel",
    function_name_to_materialization_map: _Dict[str, _Dict[str, _Tuple[int]]],
    destination_path: str,
    source_function_name: str = "main",
) -> None:
    """
    Given a dynamic-shape mlmodel, materialize symbols to create fixed-shape functions,
    then save as an .mlpackage to destination path.
    To save memory, the pymil program of input dynamic-shape mlmodel is re-used.
    Constant deduplication across functions is performed to allow weight sharing.

    Parameters
    ----------
    dynamic_shape_mlmodel : ct.models.MLModel
        A dynamic-shape mlmodel to be materialized

    function_name_to_materialization_map: Dict[str, Dict[str, Tuple[int]]]
        A dictionary specifying the name of new functions to be created,
        and for each new function what is the new fixed shapes for inputs.
        If a new function has the same name as an old function,
        then the old function will be overridden

    destination_path : str
        The saved .mlpackage model path

    source_function_name: str
        The name of the source symbolic-shape function to be materialized, default = main

    Examples
    --------
    .. sourcecode:: python

        from coremltools.utils import materialize_dynamic_shape_mlmodel

        # A dynamic-shape mlmodel you have converted
        dynamic_shape_mlmodel: ct.models.MLModel

        # As an example, let us assume the inputs are
        # 1. ``input_ids (1, query_length)``
        # 2. ``mask (query_length, context_length)``
        function_name_to_materialization_map = {
            "function_name_to_materialization_map": {
                "materialization_2_3": {"input_ids": (1, 2), "mask": (2, 3)},
                "materialization_4_5": {"input_ids": (1, 4), "mask": (4, 5)},
            }
        }

        materialize_dynamic_shape_mlmodel(
            dynamic_shape_mlmodel,
            function_name_to_materialization_map,
            "materialized_model.mlpackage",
        )

    To make prediction from the materialized mlmodel, load the desired materialized function

    .. sourcecode:: python

        materialization_2_3 = ct.models.MLModel(
            "materialized_model.mlpackage", function_name="materialization_2_3"
        )
        materialization_4_5 = ct.models.MLModel(
            "materialized_model.mlpackage", function_name="materialization_4_5"
        )

    See Also
    --------
    coremltools.converters.mil.mil.passes.defs.experiment.materialize_symbolic_shape_program

    """
    if not isinstance(dynamic_shape_mlmodel, _ct.models.MLModel):
        raise ValueError(
            "Dynamic shape mlmodel must be type of ct.models.MLModel, "
            f"but got {type(dynamic_shape_mlmodel)}"
        )
    for input in dynamic_shape_mlmodel._spec.description.input:
        if input.type.WhichOneof("Type") != "multiArrayType":
            raise NotImplementedError("Only tensor input is handled yet")
    for output in dynamic_shape_mlmodel._spec.description.output:
        if output.type.WhichOneof("Type") != "multiArrayType":
            raise NotImplementedError("Only tensor output is handled yet")

    if dynamic_shape_mlmodel._mil_program is not None:
        dynamic_shape_prog = dynamic_shape_mlmodel._mil_program
    else:
        dynamic_shape_prog = _milproto_to_pymil.load(
            dynamic_shape_mlmodel._spec,
            dynamic_shape_mlmodel._spec.specificationVersion,
            dynamic_shape_mlmodel.weights_dir,
        )

    # Materialize symbolic shapes, then run all optimization passes
    pass_pipeline = _ct.PassPipeline.DEFAULT
    pass_pipeline.insert_pass(0, "common::materialize_symbolic_shape_program")
    pass_pipeline.set_options(
        "common::materialize_symbolic_shape_program",
        {
            "function_name_to_materialization_map": function_name_to_materialization_map,
            "source_function_name": source_function_name,
        },
    )
    _PassPipelineManager.apply_pipeline(dynamic_shape_prog, pass_pipeline)

    # If source function is the only function in source model,
    # and source function is replaced with materialization,
    # and materialization does not create other functions,
    # and source function name is "main",
    # then we will end up with a unifunction model
    # Core ML distinguishs "unifunction model" and "multifunction model with only 1 function"
    if (
        len(dynamic_shape_prog.functions) == 1
        and len(function_name_to_materialization_map) == 1
        and source_function_name in function_name_to_materialization_map
        and source_function_name == "main"
    ):
        dynamic_shape_prog.export_as_multifunction = False
    else:
        dynamic_shape_prog.export_as_multifunction = True

    # Multifunciton is added in iOS 18, so
    # * if export multifunction, then specification version has to be iOS 18+
    # * else, specification version can be the same as original version
    specification_version = dynamic_shape_mlmodel._spec.specificationVersion
    if dynamic_shape_prog.export_as_multifunction:
        specification_version = max(_ct.target.iOS18, specification_version)

    dynamic_shape_prog.skip_all_passes = True
    materialized_mlmodel = _mil_convert(
        dynamic_shape_prog,
        convert_from="milinternal",
        convert_to="mlprogram",
        specification_version=specification_version,
        compute_units=_ct.ComputeUnit.CPU_ONLY,
        skip_model_load=True,
    )
    materialized_mlmodel.save(destination_path)

    Given a dynamic-shape mlmodel, materialize symbols to create fixed-shape functions,
    then save as an .mlpackage to destination path.
    To save memory, the pymil program of input dynamic-shape mlmodel is re-used.
    Constant deduplication across functions is performed to allow weight sharing.

    Parameters
    ----------
    dynamic_shape_mlmodel : ct.models.MLModel
        A dynamic-shape mlmodel to be materialized

    function_name_to_materialization_map: Dict[str, Dict[str, Tuple[int]]]
        A dictionary specifying the name of new functions to be created,
        and for each new function what is the new fixed shapes for inputs.
        If a new function has the same name as an old function,
        then the old function will be overridden

    destination_path : str
        The saved .mlpackage model path

    source_function_name: str
        The name of the source symbolic-shape function to be materialized, default = main

    Examples
    --------
    .. sourcecode:: python

        from coremltools.utils import materialize_dynamic_shape_mlmodel

        # A dynamic-shape mlmodel you have converted
        dynamic_shape_mlmodel: ct.models.MLModel

        # As an example, let us assume the inputs are
        # 1. ``input_ids (1, query_length)``
        # 2. ``mask (query_length, context_length)``
        function_name_to_materialization_map = {
            "function_name_to_materialization_map": {
                "materialization_2_3": {"input_ids": (1, 2), "mask": (2, 3)},
                "materialization_4_5": {"input_ids": (1, 4), "mask": (4, 5)},
            }
        }

        materialize_dynamic_shape_mlmodel(
            dynamic_shape_mlmodel,
            function_name_to_materialization_map,
            "materialized_model.mlpackage",
        )

    To make prediction from the materialized mlmodel, load the desired materialized function

    .. sourcecode:: python

        materialization_2_3 = ct.models.MLModel(
            "materialized_model.mlpackage", function_name="materialization_2_3"
        )
        materialization_4_5 = ct.models.MLModel(
            "materialized_model.mlpackage", function_name="materialization_4_5"
        )

    See Also
    --------
    coremltools.converters.mil.mil.passes.defs.experiment.materialize_symbolic_shape_program

    
coremltools/models/utils.py