154 lines
5.2 KiB
Python
154 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Inspect Core ML operation placement and estimated accelerator cost share."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import platform
|
|
import sys
|
|
from collections import Counter, defaultdict
|
|
from pathlib import Path
|
|
from typing import Any, Sequence
|
|
|
|
from purpose_data import DataError, write_json
|
|
|
|
|
|
def _device_category(device: Any) -> str:
|
|
name = type(device).__name__.lower()
|
|
description = str(device).lower()
|
|
combined = f"{name} {description}"
|
|
if "neural" in combined:
|
|
return "neuralEngine"
|
|
if "gpu" in combined:
|
|
return "gpu"
|
|
if "cpu" in combined:
|
|
return "cpu"
|
|
return "unknown"
|
|
|
|
|
|
def _compute_unit(coremltools: Any, requested: str) -> Any:
|
|
values = {
|
|
"all": coremltools.ComputeUnit.ALL,
|
|
"cpu-only": coremltools.ComputeUnit.CPU_ONLY,
|
|
"cpu-and-gpu": coremltools.ComputeUnit.CPU_AND_GPU,
|
|
"cpu-and-ne": coremltools.ComputeUnit.CPU_AND_NE,
|
|
}
|
|
return values[requested]
|
|
|
|
|
|
def inspect(args: argparse.Namespace) -> dict[str, Any]:
|
|
if not args.model.exists():
|
|
raise DataError(f"{args.model}: Core ML model is missing")
|
|
try:
|
|
import coremltools as ct
|
|
except ImportError as exc:
|
|
raise DataError(
|
|
"Core ML inspection requires requirements-coreml.txt on macOS"
|
|
) from exc
|
|
|
|
compiled = ct.models.utils.compile_model(str(args.model))
|
|
compute_plan = ct.models.compute_plan.MLComputePlan.load_from_path(
|
|
path=str(compiled),
|
|
compute_units=_compute_unit(ct, args.compute_units),
|
|
)
|
|
program = compute_plan.model_structure.program
|
|
if program is None or "main" not in program.functions:
|
|
raise DataError("Core ML package is not an ML Program with a main function")
|
|
operations = list(program.functions["main"].block.operations)
|
|
if not operations:
|
|
raise DataError("Core ML compute plan contains no operations")
|
|
|
|
preferred_counts: Counter[str] = Counter()
|
|
preferred_costs: dict[str, float] = defaultdict(float)
|
|
supported_counts: Counter[str] = Counter()
|
|
operation_reports = []
|
|
operations_with_usage = 0
|
|
operations_with_cost = 0
|
|
total_cost = 0.0
|
|
for operation in operations:
|
|
usage = compute_plan.get_compute_device_usage_for_mlprogram_operation(
|
|
operation
|
|
)
|
|
cost = compute_plan.get_estimated_cost_for_mlprogram_operation(operation)
|
|
preferred = "unknown"
|
|
supported: list[str] = []
|
|
if usage is not None:
|
|
operations_with_usage += 1
|
|
preferred = _device_category(usage.preferred_compute_device)
|
|
preferred_counts[preferred] += 1
|
|
supported = sorted(
|
|
{_device_category(device) for device in usage.supported_compute_devices}
|
|
)
|
|
supported_counts.update(supported)
|
|
weight = None
|
|
if cost is not None:
|
|
operations_with_cost += 1
|
|
weight = float(cost.weight)
|
|
total_cost += weight
|
|
preferred_costs[preferred] += weight
|
|
operation_reports.append(
|
|
{
|
|
"operatorName": str(operation.operator_name),
|
|
"preferredDevice": preferred,
|
|
"supportedDevices": supported,
|
|
"estimatedCostWeight": weight,
|
|
}
|
|
)
|
|
|
|
ane_operations = preferred_counts["neuralEngine"]
|
|
ane_cost = preferred_costs["neuralEngine"]
|
|
report = {
|
|
"schemaVersion": 1,
|
|
"model": str(args.model),
|
|
"coremltoolsVersion": ct.__version__,
|
|
"machine": platform.machine(),
|
|
"macOS": platform.mac_ver()[0],
|
|
"computeUnits": args.compute_units,
|
|
"operations": len(operations),
|
|
"operationsWithDeviceUsage": operations_with_usage,
|
|
"operationsWithEstimatedCost": operations_with_cost,
|
|
"preferredOperationCounts": dict(sorted(preferred_counts.items())),
|
|
"supportedOperationCounts": dict(sorted(supported_counts.items())),
|
|
"preferredEstimatedCosts": dict(sorted(preferred_costs.items())),
|
|
"neuralEngineOperationShare": (
|
|
ane_operations / operations_with_usage if operations_with_usage else 0.0
|
|
),
|
|
"neuralEngineEstimatedCostShare": (
|
|
ane_cost / total_cost if total_cost else 0.0
|
|
),
|
|
"operationDetails": operation_reports,
|
|
}
|
|
write_json(args.report, report)
|
|
print(
|
|
"Core ML placement: "
|
|
f"ANE operations={report['neuralEngineOperationShare']:.2%} "
|
|
f"ANE estimated cost={report['neuralEngineEstimatedCostShare']:.2%}"
|
|
)
|
|
return report
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--model", type=Path, required=True)
|
|
parser.add_argument("--report", type=Path, required=True)
|
|
parser.add_argument(
|
|
"--compute-units",
|
|
choices=("all", "cpu-only", "cpu-and-gpu", "cpu-and-ne"),
|
|
default="cpu-and-ne",
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
try:
|
|
inspect(args)
|
|
except (DataError, OSError, RuntimeError, ValueError) as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|