CoreMLModel
CoreMLModel(path: str, input_names: list[str], output_names: list[str], compute_units: ComputeUnits = ComputeUnits.ALL, precision: str = 'FLOAT32', io_precision: str = 'FLOAT32', output_shapes: dict[str, tuple[int, ...]] | None = None, image_input: ImageInput | None = None, classifier: Classifier | None = None, function_name: str = '', deployment_target: object = None, noise: list[tuple[str, tuple[int, ...], str, Tensor | None]] | None = None, traced_outputs: dict[str, Tensor] | None = None)A Core ML package written by Lucid, loaded and ready to run.
Holds the .mlpackage on disk plus the compiled model Core ML
produced from it. Compilation happens once per package content, not
once per handle: the compiled model is kept in Lucid's cache and every
later handle on the same content — in this process or another — opens
it, which also lets Core ML reuse the bundle it specialised the model
into rather than writing another (see lucid.coreml.empty_cache).
Attributes
pathstr.mlpackage this handle opened.input_names, output_nameslist of strpredict accepts a tuple in this order or a mapping.noise_inputslist of tuple(name, shape) for each input that stands in for a random
draw, when the export lifted one — see
lucid.coreml.Draws. Empty otherwise. A caller who
passes nothing for these gets a fresh sample.deployment_targetDeploymentTargetcompute_unitsComputeUnitsCPU_AND_NE
whatever the request, because Core ML's GPU path unpacks small
palettes incorrectly.precisionstr"FLOAT32" or "FLOAT16", as the package was written — the
body's precision. "UNKNOWN" for a package that does not
record it: one Lucid did not write, or wrote before it did.io_precisionstr"FLOAT16" makes predict convert float32
inputs on the way in and return float16 tensors.palettizedboolimage_inputImageInput or NoneclassifierClassifier or Noneclassify rather than predict.Examples
>>> import shutil, tempfile
>>> import lucid, lucid.nn as nn, lucid.coreml as cml
>>> model = nn.Sequential(
... nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(),
... nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(16, 10),
... ).eval()
>>> x, room = lucid.randn(1, 3, 32, 32), tempfile.mkdtemp()
>>> package = cml.export(model, x, f"{room}/m.mlpackage")
>>> package.predict(x).shape
(1, 10)
>>> package.verify(model, x) < 1e-5 # float32; a float16 package is nearer 1e-3
True
>>> package.close()
A handle owns a compiled model, so close it — or use it as a context
manager if you only need it for one call.
>>> with cml.load(f"{room}/m.mlpackage") as reopened:
... print(reopened.predict(x).shape)
(1, 10)
>>> shutil.rmtree(room)Used by 1
Constructors
1__init__
→None__init__(path: str, input_names: list[str], output_names: list[str], compute_units: ComputeUnits = ComputeUnits.ALL, precision: str = 'FLOAT32', io_precision: str = 'FLOAT32', output_shapes: dict[str, tuple[int, ...]] | None = None, image_input: ImageInput | None = None, classifier: Classifier | None = None, function_name: str = '', deployment_target: object = None, noise: list[tuple[str, tuple[int, ...], str, Tensor | None]] | None = None, traced_outputs: dict[str, Tensor] | None = None)Properties
2Instance methods
9How long one prediction takes, measured the way it should be.
The first calls are not the model: Core ML defers work to them — specialising for the units it was given, laying out weights the accelerator wants — and a timing that includes them reports the setup. Hence a warmup that is thrown away, and a median over repeats rather than a mean, since a scheduling hiccup on a shared machine moves a mean and not a median.
Measured on an M1 Pro with a ResNet-18 at 224 square: 18.6 ms eager, 4.5 ms as a float32 package on the CPU, 2.7 ms at float16 on the CPU, and 1.5 ms with the Neural Engine allowed — so the accelerator is worth about 12x against eager and 3x against the same package on the CPU. Those are this machine's numbers and will not be yours, which is why this exists rather than a documented figure.
Parameters
Returns
LatencyMedian and best of the timed calls, in milliseconds.
Raises
ValueErrorrepeats is not positive.Run a classifier package and read back what it names.
Parameters
Returns
tuple[str, dict[str, float]]The winning label, and every label with its probability.
Raises
TypeErrorRelease the compiled model.
The compiled model itself stays in Lucid's cache for the next
handle on the same package — see lucid.coreml.empty_cache.
A prediction still running on another thread finishes with the
model it took, which stays loaded, and protected from eviction,
until it returns. Safe to call twice, so a finally beside a
with is fine.
compute_plan()Which device Core ML assigns each operation to.
Requires macOS 14.4+; an empty plan there means unknown, not unaccelerated.
Run the model.
Inputs must be host tensors: Core ML reads host memory, and moving
a Metal tensor here would hide a copy the caller did not ask for.
Move it explicitly with .to("cpu").
Parameters
Returns
A state's current value, as a CPU tensor of its element type.
Parameters
namestrstate_names.Returns
TensorA copy; writing to it does not change the state.
Raises
ValueErrorForget everything the package has accumulated.
A state persists across predictions by design, so starting a fresh sequence has to be asked for; there is no other way back to the value it began at.
Raises
ValueErrorLargest difference against the eager model.
Shapes agreeing is not evidence: a package missing a layer has the right shape and returns plausible numbers. This runs both and compares values — every output, not just the first, since a detector that exported its class scores and dropped its boxes would otherwise pass.
The default is an absolute difference, which is only interpretable against outputs of a known size. A model whose outputs differ in magnitude makes that trap easy to fall into: RealNVP returns a latent of order 1 beside a log-probability of order 1e4, so the absolute worst is set by the second, and dividing it by the first reads as a 4% error when every output agrees to 1e-6.
relative=True scales each output's difference by that
output's own magnitude — but only down to one, which is the
other half of the same trap: VQ-VAE's latent has values around
2e-3, and dividing float32 noise by that reads as 3e-4 when the
difference is 5e-7. Dividing by max(scale, 1) is relative
where relative means something and absolute where it does not,
which is the same bargain a tolerance pair makes.
Parameters
Returns
floatThe worst max|coreml - eager| across the outputs, or the
worst of those divided by each output's own scale when
relative. Expect ~1e-7 for a float32 export and ~1e-3
relative for float16.
Notes
For an image export the pixel buffer is eight bits per channel,
so anything but whole numbers in [0, 255] is rounded on the
way in and the two sides see different pixels. That is refused
rather than reported: for randn values the rounding is most
of the signal and the answer would be around 3e-1, which reads
as a broken export. Feed pixels and the comparison is the usual
one.
Overwrite a state — to seed a stream, or to restore a saved one.
A state was write-only from outside the package: a stream whose first step differs from the rest (a video decoder that skips its temporal upsampling on frame 0) could not be seeded, and what one had accumulated could not be saved or inspected.
Parameters
namestrstate_names.valueTensorRaises
ValueErrorDunder methods
3__enter__()Return the handle, so a package can be opened in a with.
A handle owns a compiled model and a directory Core ML wrote it
into, and every use of one in this codebase was already a
try/finally around close.
Returns
CoreMLModelThis handle.
Close the handle, whether the block ended well or not.
Parameters
kindtype or NonevalueBaseException or NonetraceTracebackType or None