Skip to content
42 changes: 42 additions & 0 deletions backends/apple/coreml/compiler/coreml_preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

import coremltools as ct
import coremltools.optimize as cto
from coremltools.converters.mil import Builder as mb
from coremltools.converters.mil.mil.passes.graph_pass import AbstractGraphPass
from coremltools.converters.mil.mil.passes.pass_registry import register_pass
from executorch.backends.apple.coreml import executorchcoreml
from executorch.backends.apple.coreml.compiler.enumerated_shape_utils import (
_get_ct_inputs,
Expand Down Expand Up @@ -67,6 +70,44 @@ def _gathers_sharing_a_weight(mlmodel: ct.models.MLModel) -> List[str]:
return shared


@register_pass(namespace="executorch", override=True, name="copy_reused_outputs")
class _CopyReusedOutputs(AbstractGraphPass):
"""
Gives each output of the program a value of its own.

coremltools converts ops such as clone into nothing, and keeps a tile that
changes nothing, such as one from a same-shape expand, only when it makes
an output. So a partition can return one of its own inputs, or one value
twice, directly or through such a tile. Core ML then hands back one of
those outputs wrong, such as all zeros or the bytes of another output.
"""

def apply(self, prog):
for function in prog.functions.values():
seen = set(function.inputs.values())
outputs, tiles = [], []
for output in function.outputs:
if (
output.op is not None
and output.op.op_type == "tile"
and output.op.x.sym_type == output.sym_type
):
tiles.append(output.op)
output = output.op.x

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the tile's input isn't otherwise reused, the output becomes output.op.x and the tile op is left with no users. Since this pass runs last, nothing removes it, so it ends up in the serialized program. Harmless, but you could remove it here (or run dead_code_elimination after this pass).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. With the default pass list, coremltools already removes it: after this pass it runs its backend_mlprogram pipeline, which includes dead_code_elimination. But a custom pass list without dead_code_elimination skips that pass in the backend pipeline too, and then the tile stays. So the next push removes the replaced tile in this pass, and test_same_shape_expand_output checks that with such a list.

if output in seen:
with function:
copy = mb.identity(x=output)
# coremltools wants every op to carry the source program's
# scopes, which set_outputs, unlike a var replacement, skips.
function._copy_scope_info(output, copy)
output = copy
seen.add(output)
outputs.append(output)
function.set_outputs(outputs)
# A pass list without dead_code_elimination would keep such a tile.
function.remove_ops([op for op in tiles if not op.outputs[0].child_ops])


class COMPILE_SPEC_KEYS(Enum):
COMPUTE_UNITS = "compute_units"
MODEL_TYPE = "model_type"
Expand Down Expand Up @@ -565,6 +606,7 @@ def _convert_to_mlmodel(
)
compute_units = CoreMLBackend.compute_unit_from_compile_specs(compile_specs)
pass_pipeline = CoreMLBackend.pass_pipeline_from_compile_specs(compile_specs)
pass_pipeline.append_pass("executorch::copy_reused_outputs")
enumerated_shapes = CoreMLBackend.enumerated_shapes_from_compile_specs(
compile_specs
)
Expand Down
Loading
Loading