|
5 | 5 | import json
|
6 | 6 | import shutil
|
7 | 7 | import uuid
|
| 8 | +from dataclasses import asdict, dataclass |
| 9 | +from enum import Enum |
8 | 10 |
|
9 | 11 | from pathlib import Path
|
10 | 12 |
|
11 |
| -from typing import final, List |
| 13 | +from typing import Dict, final, List |
12 | 14 |
|
13 | 15 | import coremltools as ct
|
14 | 16 | import executorchcoreml
|
|
21 | 23 | from executorch.exir.backend.compile_spec_schema import CompileSpec
|
22 | 24 |
|
23 | 25 |
|
| 26 | +class COMPILE_SPEC_KEYS(Enum): |
| 27 | + COMPUTE_UNITS = "compute_units" |
| 28 | + MODEL_TYPE = "model_type" |
| 29 | + MIN_DEPLOYMENT_TARGET = "min_deployment_target" |
| 30 | + MODEL_COMPUTE_PRECISION = "model_compute_precision" |
| 31 | + |
| 32 | + |
| 33 | +@dataclass |
| 34 | +class ModelMetadata: |
| 35 | + # The model input names. |
| 36 | + inputNames: List[str] |
| 37 | + # The model output names. |
| 38 | + outputNames: List[str] |
| 39 | + # The model identifier. |
| 40 | + identifier: str |
| 41 | + |
| 42 | + |
24 | 43 | @final
|
25 | 44 | class CoreMLBackend(BackendDetails):
|
| 45 | + class MODEL_TYPE(Enum): |
| 46 | + MODEL = "model" |
| 47 | + COMPILED_MODEL = "compiled_model" |
| 48 | + |
| 49 | + @staticmethod |
| 50 | + def generate_model_type_compile_spec(model_type: MODEL_TYPE) -> CompileSpec: |
| 51 | + """ |
| 52 | + Returns the compile spec representing the given model type. |
| 53 | +
|
| 54 | + If the model type is ``MODEL_TYPE.Model`` then the ``CoreMLBackend`` returns |
| 55 | + the in-memory representation of the ``mlpackage`` contents. |
| 56 | +
|
| 57 | + If the model type is ``MODEL_TYPE.COMPILED_MODEL`` then the ``CoreMLBackend`` compiles the model |
| 58 | + and returns the in-memory representation of ``mlmodelc`` (compiled model) contents. |
| 59 | + """ |
| 60 | + return CompileSpec( |
| 61 | + COMPILE_SPEC_KEYS.MODEL_TYPE.value, model_type.value.encode("utf-8") |
| 62 | + ) |
| 63 | + |
| 64 | + @staticmethod |
| 65 | + def model_type_from_compile_specs(compile_specs: List[CompileSpec]) -> MODEL_TYPE: |
| 66 | + """ |
| 67 | + Returns the model type by parsing the list of compile specs. |
| 68 | + """ |
| 69 | + for compile_spec in compile_specs: |
| 70 | + if compile_spec.key == COMPILE_SPEC_KEYS.MODEL_TYPE.value: |
| 71 | + return CoreMLBackend.MODEL_TYPE(compile_spec.value.decode("utf-8")) |
| 72 | + |
| 73 | + return CoreMLBackend.MODEL_TYPE.MODEL |
| 74 | + |
| 75 | + @staticmethod |
| 76 | + def generate_compute_precision_compile_spec( |
| 77 | + compute_precision: ct.precision, |
| 78 | + ) -> CompileSpec: |
| 79 | + """ |
| 80 | + Returns the compile spec representing the model compute precision, for additional details |
| 81 | + please refer to the documentation for ``coremltools.precision``. |
| 82 | + """ |
| 83 | + return CompileSpec( |
| 84 | + COMPILE_SPEC_KEYS.MODEL_COMPUTE_PRECISION.value, |
| 85 | + compute_precision.value.encode("utf-8"), |
| 86 | + ) |
| 87 | + |
| 88 | + @staticmethod |
| 89 | + def model_compute_precision_from_compile_specs( |
| 90 | + compile_specs: List[CompileSpec], |
| 91 | + ) -> ct.precision: |
| 92 | + """ |
| 93 | + Returns the model's compute precision by parsing the list of compile specs. |
| 94 | + """ |
| 95 | + for compile_spec in compile_specs: |
| 96 | + if compile_spec.key == COMPILE_SPEC_KEYS.MODEL_COMPUTE_PRECISION.value: |
| 97 | + return ct.precision(compile_spec.value.decode("utf-8")) |
| 98 | + |
| 99 | + return ct.precision.FLOAT16 |
| 100 | + |
| 101 | + @staticmethod |
| 102 | + def generate_minimum_deployment_target_compile_spec( |
| 103 | + min_deployment_target: ct.target, |
| 104 | + ) -> CompileSpec: |
| 105 | + """ |
| 106 | + Returns the compile spec representing the minimum deployment target on which the model can run, |
| 107 | + for additional details please refer to the documentation for ``coremltools.target``. |
| 108 | + """ |
| 109 | + return CompileSpec( |
| 110 | + COMPILE_SPEC_KEYS.MIN_DEPLOYMENT_TARGET.value, |
| 111 | + str(min_deployment_target.value).encode("utf-8"), |
| 112 | + ) |
| 113 | + |
| 114 | + @staticmethod |
| 115 | + def min_deployment_target_from_compile_specs( |
| 116 | + compile_specs: List[CompileSpec], |
| 117 | + ) -> ct.target: |
| 118 | + """ |
| 119 | + Returns the minimum deployment target by parsing the list of compile specs. |
| 120 | + """ |
| 121 | + for compile_spec in compile_specs: |
| 122 | + if compile_spec.key == COMPILE_SPEC_KEYS.MIN_DEPLOYMENT_TARGET.value: |
| 123 | + compile_spec_value: int = int(compile_spec.value.decode("utf-8")) |
| 124 | + return ct.target(compile_spec_value) |
| 125 | + |
| 126 | + return ct.target.iOS15 |
| 127 | + |
26 | 128 | @staticmethod
|
27 |
| - def to_bytes(mlmodel): |
28 |
| - dir_path = Path("tmp") |
29 |
| - model_dir_path = dir_path / "lowered_module" |
30 |
| - Path(model_dir_path).mkdir(parents=True, exist_ok=True) |
31 |
| - model_path = model_dir_path / "model.mlpackage" |
32 |
| - mlmodel.save(model_path) |
33 |
| - |
34 |
| - # save model metdata |
35 |
| - spec = mlmodel.get_spec() |
36 |
| - input_names = [input.name for input in spec.description.input] |
37 |
| - output_names = [output.name for output in spec.description.output] |
| 129 | + def generate_compute_unit_compile_spec( |
| 130 | + compute_unit: ct.ComputeUnit, |
| 131 | + ) -> CompileSpec: |
| 132 | + """ |
| 133 | + Returns the compile spec representing the compute units on which the model can run, for additional details |
| 134 | + please refer to the documentation for ``coremltools.ComputeUnit`. |
| 135 | + """ |
| 136 | + return CompileSpec( |
| 137 | + COMPILE_SPEC_KEYS.COMPUTE_UNITS.value, |
| 138 | + compute_unit.name.lower().encode("utf-8"), |
| 139 | + ) |
| 140 | + |
| 141 | + @staticmethod |
| 142 | + def generate_compile_specs( |
| 143 | + compute_unit: ct.ComputeUnit = ct.ComputeUnit.ALL, |
| 144 | + minimum_deployment_target: ct.target = ct.target.iOS15, |
| 145 | + compute_precision: ct.precision = ct.precision.FLOAT16, |
| 146 | + model_type: MODEL_TYPE = MODEL_TYPE.MODEL, |
| 147 | + ) -> List[CompileSpec]: |
| 148 | + """ |
| 149 | + Returns the list of compile specs that's used by CoreMLBackend to lower the module. |
| 150 | + """ |
| 151 | + compile_specs: List[CompileSpec] = [] |
| 152 | + compile_specs.append( |
| 153 | + CoreMLBackend.generate_compute_unit_compile_spec(compute_unit) |
| 154 | + ) |
| 155 | + compile_specs.append( |
| 156 | + CoreMLBackend.generate_minimum_deployment_target_compile_spec( |
| 157 | + minimum_deployment_target |
| 158 | + ) |
| 159 | + ) |
| 160 | + compile_specs.append( |
| 161 | + CoreMLBackend.generate_compute_precision_compile_spec(compute_precision) |
| 162 | + ) |
| 163 | + compile_specs.append(CoreMLBackend.generate_model_type_compile_spec(model_type)) |
| 164 | + |
| 165 | + return compile_specs |
| 166 | + |
| 167 | + @staticmethod |
| 168 | + def model_metadata_from_spec(model_spec: ct.proto.Model_pb2) -> Dict[str, str]: |
| 169 | + input_names: List[str] = [input.name for input in model_spec.description.input] |
| 170 | + output_names = [output.name for output in model_spec.description.output] |
38 | 171 | identifier = uuid.uuid4()
|
39 | 172 |
|
40 |
| - model_metadata = { |
41 |
| - "inputNames": input_names, |
42 |
| - "outputNames": output_names, |
43 |
| - "identifier": str(identifier), |
44 |
| - } |
| 173 | + return ModelMetadata( |
| 174 | + inputNames=input_names, outputNames=output_names, identifier=str(identifier) |
| 175 | + ) |
| 176 | + |
| 177 | + @staticmethod |
| 178 | + def to_bytes(mlmodel: ct.models.MLModel, model_type: MODEL_TYPE) -> bytes: |
| 179 | + dir_path: Path = Path("tmp") |
| 180 | + model_dir_path: Path = dir_path / "lowered_module" |
| 181 | + model_spec: ct.proto.Model_pb2 = mlmodel.get_spec() |
| 182 | + model_metadata: ModelMetadata = CoreMLBackend.model_metadata_from_spec( |
| 183 | + model_spec |
| 184 | + ) |
| 185 | + match model_type: |
| 186 | + case CoreMLBackend.MODEL_TYPE.MODEL: |
| 187 | + # Store model. |
| 188 | + model_path = model_dir_path / "model.mlpackage" |
| 189 | + mlmodel.save(model_path) |
45 | 190 |
|
46 |
| - # store metadata |
| 191 | + case CoreMLBackend.MODEL_TYPE.COMPILED_MODEL: |
| 192 | + # Store compiled model |
| 193 | + model_path = model_dir_path / "model.mlmodelc" |
| 194 | + compiled_model_path = mlmodel.get_compiled_model_path() |
| 195 | + |
| 196 | + shutil.copytree( |
| 197 | + compiled_model_path, |
| 198 | + str(model_path.resolve()), |
| 199 | + dirs_exist_ok=True, |
| 200 | + ) |
| 201 | + |
| 202 | + # Store model metadata. |
47 | 203 | model_metadata_path = Path(model_dir_path) / "metadata.json"
|
48 |
| - json_object = json.dumps(model_metadata) |
| 204 | + model_metadata_json = json.dumps(asdict(model_metadata)) |
49 | 205 | with open(model_metadata_path, "w") as outfile:
|
50 |
| - outfile.write(json_object) |
| 206 | + outfile.write(model_metadata_json) |
51 | 207 |
|
52 | 208 | # flatten directory contents and convert it to bytes
|
53 | 209 | flattened_bytes = executorchcoreml.flatten_directory_contents(
|
54 | 210 | str(model_dir_path.resolve())
|
55 | 211 | )
|
| 212 | + |
56 | 213 | shutil.rmtree(str(model_dir_path.resolve()))
|
57 | 214 | return flattened_bytes
|
58 | 215 |
|
59 | 216 | @classmethod
|
60 |
| - # pyre-ignore |
61 | 217 | def preprocess(
|
62 | 218 | cls,
|
63 | 219 | edge_program: ExportedProgram,
|
64 |
| - module_compile_spec: List[CompileSpec], |
| 220 | + module_compile_specs: List[CompileSpec], |
65 | 221 | ) -> PreprocessResult:
|
| 222 | + model_type: CoreMLBackend.MODEL_TYPE = ( |
| 223 | + CoreMLBackend.model_type_from_compile_specs( |
| 224 | + module_compile_specs, |
| 225 | + ) |
| 226 | + ) |
| 227 | + |
| 228 | + model_compute_precision: ct.precision = ( |
| 229 | + CoreMLBackend.model_compute_precision_from_compile_specs( |
| 230 | + module_compile_specs |
| 231 | + ) |
| 232 | + ) |
| 233 | + |
| 234 | + minimum_deployment_target: ct.target = ( |
| 235 | + CoreMLBackend.min_deployment_target_from_compile_specs(module_compile_specs) |
| 236 | + ) |
| 237 | + |
| 238 | + skip_model_load: bool = False |
| 239 | + match model_type: |
| 240 | + case CoreMLBackend.MODEL_TYPE.MODEL: |
| 241 | + skip_model_load = True |
| 242 | + |
| 243 | + case CoreMLBackend.MODEL_TYPE.COMPILED_MODEL: |
| 244 | + skip_model_load = False |
| 245 | + |
66 | 246 | mlmodel = ct.convert(
|
67 | 247 | model=edge_program,
|
68 | 248 | source="pytorch",
|
69 | 249 | convert_to="mlprogram",
|
70 | 250 | pass_pipeline=ct.PassPipeline.DEFAULT,
|
71 |
| - skip_model_load=True, |
| 251 | + skip_model_load=skip_model_load, |
| 252 | + compute_precision=model_compute_precision, |
| 253 | + minimum_deployment_target=minimum_deployment_target, |
72 | 254 | )
|
73 |
| - flattened_bytes = CoreMLBackend.to_bytes(mlmodel) |
| 255 | + |
| 256 | + processed_bytes = CoreMLBackend.to_bytes(mlmodel, model_type=model_type) |
74 | 257 | return PreprocessResult(
|
75 |
| - processed_bytes=flattened_bytes, |
| 258 | + processed_bytes=processed_bytes, |
76 | 259 | )
|
0 commit comments