Guide IA & Machine Learning

Guide de déploiement CoreML : de l'entraînement à la production sur Mac Mini M4

Le framework CoreML d'Apple offre une inférence ultra-rapide en exploitant le Neural Engine, le GPU et le CPU en synergie. Ce guide vous accompagne dans la conversion de modèles depuis PyTorch, TensorFlow et ONNX, leur optimisation pour le Neural Engine 16 cœurs du M4, et le déploiement d'API REST en production -- le tout sur un Mac Mini M4 dédié.

25 min de lecture Mis à jour en février 2025 Intermédiaire à avancé

1. Pourquoi CoreML sur Mac Mini M4 ?

CoreML est le framework natif de machine learning d'Apple, conçu pour extraire les performances maximales d'Apple Silicon. Contrairement aux frameworks ML génériques qui traitent le GPU comme un seul dispositif de calcul, CoreML distribue intelligemment les charges de travail entre le CPU, le GPU et le Neural Engine dédié à 16 cœurs -- exécutant souvent différentes parties d'un modèle sur différentes unités de calcul simultanément.

Neural Engine 16 cœurs

Le Neural Engine du M4 offre jusqu'à 38 TOPS (trillions d'opérations par seconde) pour les charges de travail int8 quantifiées. CoreML route automatiquement les couches compatibles -- convolutions, multiplications de matrices, normalisation -- vers le Neural Engine pour un débit maximal avec une consommation d'énergie minimale.

Architecture mémoire unifiée

Toutes les unités de calcul partagent le même pool de mémoire avec une bande passante allant jusqu'à 120 Go/s. Il n'y a pas de goulot d'étranglement PCIe ni de copie de données entre la mémoire CPU et GPU. Un Mac Mini M4 de 24 Go donne à chaque unité de calcul un accès direct aux 24 Go complets, permettant de charger des modèles plus importants que des configurations GPU discrètes équivalentes.

Dispatch automatique du calcul

Le compilateur CoreML analyse le graphe de votre modèle et attribue chaque opération à l'unité de calcul optimale. Les convolutions s'exécutent sur le Neural Engine, les opérations personnalisées se replient sur le GPU ou le CPU, et tout s'exécute comme un pipeline unifié. Vous obtenez une optimisation au niveau matériel sans effort manuel.

Efficacité énergétique à grande échelle

Un Mac Mini M4 exécutant l'inférence CoreML consomme 5-20 W de puissance système totale. Comparez cela aux 300-450 W pour un serveur GPU NVIDIA A100. Pour l'inférence en production en continu, cela se traduit par des coûts d'électricité considérablement réduits et aucun besoin d'infrastructure de refroidissement spécialisée.

Point clé : CoreML n'est pas réservé aux applications iOS. Avec les bindings Python via coremltools, vous pouvez convertir des modèles depuis n'importe quel framework majeur, exécuter l'inférence depuis des scripts Python et construire des API de production -- tout en exploitant le Neural Engine auquel la plupart des frameworks côté serveur ne peuvent pas accéder.

2. Convertir des modèles PyTorch vers CoreML

La bibliothèque coremltools d'Apple fournit un chemin de conversion direct des modèles PyTorch vers le format .mlpackage de CoreML. La conversion trace votre modèle avec des entrées d'exemple et traduit chaque opération en représentation interne CoreML.

Étape 1 : Installer les dépendances

# Create a virtual environment
python3 -m venv ~/coreml-env
source ~/coreml-env/bin/activate

# Install coremltools and PyTorch
pip install coremltools torch torchvision

# Verify installation
python3 -c "import coremltools as ct; print(ct.__version__)"
# 8.1

Étape 2 : Convertir un classificateur d'images PyTorch

import torch
import torchvision
import coremltools as ct

# Load a pretrained ResNet50 model
model = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.DEFAULT)
model.eval()

# Create a sample input (batch=1, channels=3, height=224, width=224)
example_input = torch.randn(1, 3, 224, 224)

# Trace the model with TorchScript
traced_model = torch.jit.trace(model, example_input)

# Convert to CoreML
coreml_model = ct.convert(
    traced_model,
    inputs=[ct.ImageType(
        name="image",
        shape=(1, 3, 224, 224),
        scale=1.0 / (255.0 * 0.226),
        bias=[-0.485 / 0.226, -0.456 / 0.226, -0.406 / 0.226],
        color_layout=ct.colorlayout.RGB
    )],
    classifier_config=ct.ClassifierConfig("imagenet_classes.txt"),
    compute_units=ct.ComputeUnit.ALL,  # Use Neural Engine + GPU + CPU
    minimum_deployment_target=ct.target.macOS15,
)

# Save the model
coreml_model.save("ResNet50.mlpackage")
print("Model saved: ResNet50.mlpackage")

Étape 3 : Convertir un modèle PyTorch personnalisé

import torch
import torch.nn as nn
import coremltools as ct

# Define a custom text embedding model
class TextEncoder(nn.Module):
    def __init__(self, vocab_size=30000, embed_dim=512, num_heads=8, num_layers=6):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.pos_encoding = nn.Parameter(torch.randn(1, 512, embed_dim))
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=embed_dim, nhead=num_heads, batch_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.output_proj = nn.Linear(embed_dim, 256)

    def forward(self, input_ids):
        x = self.embedding(input_ids) + self.pos_encoding[:, :input_ids.shape[1], :]
        x = self.transformer(x)
        x = x.mean(dim=1)  # Global average pooling
        return self.output_proj(x)

# Initialize and load your trained weights
model = TextEncoder()
# model.load_state_dict(torch.load("text_encoder_weights.pt"))
model.eval()

# Trace with example input
example_input = torch.randint(0, 30000, (1, 128))
traced_model = torch.jit.trace(model, example_input)

# Convert to CoreML
coreml_model = ct.convert(
    traced_model,
    inputs=[ct.TensorType(name="input_ids", shape=(1, 128), dtype=int)],
    outputs=[ct.TensorType(name="embedding")],
    compute_units=ct.ComputeUnit.ALL,
    minimum_deployment_target=ct.target.macOS15,
)

# Add metadata
coreml_model.author = "My Remote Mac"
coreml_model.short_description = "Text embedding model for semantic search"
coreml_model.version = "1.0.0"

coreml_model.save("TextEncoder.mlpackage")
print("Model saved: TextEncoder.mlpackage")

Étape 4 : Vérifier le modèle converti

import coremltools as ct
import numpy as np

# Load the converted model
model = ct.models.MLModel("ResNet50.mlpackage")

# Inspect model metadata
spec = model.get_spec()
print(f"Model type: {spec.WhichOneof('Type')}")
print(f"Inputs: {[inp.name for inp in spec.description.input]}")
print(f"Outputs: {[out.name for out in spec.description.output]}")

# Run a test prediction
from PIL import Image
img = Image.open("test_image.jpg").resize((224, 224))
prediction = model.predict({"image": img})
print(f"Top prediction: {prediction}")

# Check which compute units are available
print(f"Compute units: {model.compute_unit}")

3. Convertir des modèles TensorFlow vers CoreML

CoreML prend en charge la conversion depuis le format TensorFlow SavedModel, les modèles Keras .h5 et les modèles TensorFlow Lite .tflite. Le convertisseur coremltools gère l'ensemble complet des opérations TensorFlow, y compris les couches personnalisées.

Convertir un TensorFlow SavedModel

import coremltools as ct
import tensorflow as tf

# Load a TensorFlow SavedModel (e.g., EfficientNet trained on your data)
tf_model = tf.keras.applications.EfficientNetV2S(
    weights="imagenet",
    input_shape=(384, 384, 3)
)

# Convert to CoreML
coreml_model = ct.convert(
    tf_model,
    inputs=[ct.ImageType(
        name="image",
        shape=(1, 384, 384, 3),
        scale=1.0 / 255.0,
        color_layout=ct.colorlayout.RGB
    )],
    compute_units=ct.ComputeUnit.ALL,
    minimum_deployment_target=ct.target.macOS15,
)

coreml_model.save("EfficientNetV2S.mlpackage")
print("Saved EfficientNetV2S.mlpackage")

Convertir un modèle Keras H5

import coremltools as ct
import tensorflow as tf

# Load your custom Keras model
model = tf.keras.models.load_model("my_custom_model.h5")

# Print model summary to understand input/output shapes
model.summary()

# Convert with explicit input/output specifications
coreml_model = ct.convert(
    model,
    inputs=[ct.TensorType(name="features", shape=(1, 128))],
    outputs=[ct.TensorType(name="prediction")],
    compute_units=ct.ComputeUnit.ALL,
    minimum_deployment_target=ct.target.macOS15,
)

# Add model metadata
coreml_model.author = "ML Team"
coreml_model.license = "Proprietary"
coreml_model.short_description = "Customer churn prediction model v2.1"
coreml_model.version = "2.1.0"

coreml_model.save("ChurnPredictor.mlpackage")

Convertir un modèle TensorFlow Lite

import coremltools as ct

# Convert directly from a .tflite file
coreml_model = ct.convert(
    "object_detector.tflite",
    source="tensorflow",
    inputs=[ct.ImageType(
        name="image",
        shape=(1, 320, 320, 3),
        scale=1.0 / 255.0,
        color_layout=ct.colorlayout.RGB
    )],
    outputs=[
        ct.TensorType(name="boxes"),
        ct.TensorType(name="scores"),
        ct.TensorType(name="classes"),
    ],
    compute_units=ct.ComputeUnit.ALL,
    minimum_deployment_target=ct.target.macOS15,
)

coreml_model.save("ObjectDetector.mlpackage")
print("Saved ObjectDetector.mlpackage")

4. Convertir des modèles ONNX vers CoreML

ONNX (Open Neural Network Exchange) est un format universel vers lequel de nombreux frameworks peuvent exporter. Cela fait d'ONNX un format intermédiaire pratique pour convertir des modèles depuis des frameworks comme scikit-learn, XGBoost, ou même des pipelines d'entraînement C++ personnalisés.

Installer le support ONNX

# Install ONNX and onnxruntime for validation
pip install onnx onnxruntime coremltools

Convertir un modèle ONNX

import coremltools as ct
import onnx

# Load and validate the ONNX model
onnx_model = onnx.load("yolov8n.onnx")
onnx.checker.check_model(onnx_model)
print("ONNX model is valid")

# Inspect input/output shapes
for inp in onnx_model.graph.input:
    print(f"Input: {inp.name}, shape: {[d.dim_value for d in inp.type.tensor_type.shape.dim]}")
for out in onnx_model.graph.output:
    print(f"Output: {out.name}, shape: {[d.dim_value for d in out.type.tensor_type.shape.dim]}")

# Convert ONNX to CoreML
coreml_model = ct.converters.convert(
    "yolov8n.onnx",
    inputs=[ct.ImageType(
        name="images",
        shape=(1, 3, 640, 640),
        scale=1.0 / 255.0,
        color_layout=ct.colorlayout.RGB
    )],
    compute_units=ct.ComputeUnit.ALL,
    minimum_deployment_target=ct.target.macOS15,
)

coreml_model.short_description = "YOLOv8 Nano object detection model"
coreml_model.save("YOLOv8n.mlpackage")
print("Saved YOLOv8n.mlpackage")

Exporter PyTorch vers ONNX, puis vers CoreML

import torch
import coremltools as ct

# When direct PyTorch conversion fails, use ONNX as an intermediate step
model = torch.hub.load('pytorch/vision', 'detr_resnet50', pretrained=True)
model.eval()

dummy_input = torch.randn(1, 3, 800, 800)

# Step 1: Export to ONNX
torch.onnx.export(
    model,
    dummy_input,
    "detr_resnet50.onnx",
    input_names=["image"],
    output_names=["pred_logits", "pred_boxes"],
    opset_version=17,
    dynamic_axes={"image": {0: "batch"}}
)
print("Exported to ONNX")

# Step 2: Convert ONNX to CoreML
coreml_model = ct.converters.convert(
    "detr_resnet50.onnx",
    inputs=[ct.ImageType(name="image", shape=(1, 3, 800, 800), scale=1.0/255.0)],
    compute_units=ct.ComputeUnit.ALL,
    minimum_deployment_target=ct.target.macOS15,
)

coreml_model.save("DETR_ResNet50.mlpackage")
print("Saved DETR_ResNet50.mlpackage")

5. Optimiser pour le Neural Engine

Le Neural Engine offre des performances maximales avec des modèles quantifiés. L'application de la quantification post-entraînement, de la palettisation et de l'élagage peut réduire la taille du modèle de 4 à 8 fois et améliorer le débit du Neural Engine de 2 à 4 fois -- souvent avec une perte de précision négligeable.

Quantification Float16 (optimisation la plus simple)

import coremltools as ct
from coremltools.models.neural_network import quantization_utils

# Load the full-precision model
model = ct.models.MLModel("ResNet50.mlpackage")

# Quantize to float16 -- halves model size with virtually no quality loss
model_fp16 = quantization_utils.quantize_weights(model, nbits=16)
model_fp16.save("ResNet50_fp16.mlpackage")

# Check file sizes
import os
original_size = sum(
    os.path.getsize(os.path.join(dp, f))
    for dp, dn, fn in os.walk("ResNet50.mlpackage") for f in fn
)
optimized_size = sum(
    os.path.getsize(os.path.join(dp, f))
    for dp, dn, fn in os.walk("ResNet50_fp16.mlpackage") for f in fn
)
print(f"Original: {original_size / 1e6:.1f} MB")
print(f"Float16:  {optimized_size / 1e6:.1f} MB")
print(f"Reduction: {(1 - optimized_size/original_size)*100:.1f}%")

Quantification post-entraînement Int8

import coremltools as ct
import coremltools.optimize as cto
import numpy as np

# Load the model
model = ct.models.MLModel("ResNet50.mlpackage")

# Configure linear (int8) quantization with calibration data
op_config = cto.coreml.OpLinearQuantizerConfig(
    mode="linear_symmetric",
    dtype="int8",
    granularity="per_channel"
)

config = cto.coreml.OptimizationConfig(global_config=op_config)

# Prepare calibration data (representative samples from your dataset)
def load_calibration_data():
    """Load 100-200 representative samples for calibration."""
    calibration_samples = []
    for i in range(100):
        # Replace with your actual data loading
        sample = np.random.randn(1, 3, 224, 224).astype(np.float32)
        calibration_samples.append({"image": sample})
    return calibration_samples

# Apply post-training quantization
model_int8 = cto.coreml.linear_quantize_weights(
    model,
    config=config,
    sample_data=load_calibration_data()
)

model_int8.save("ResNet50_int8.mlpackage")
print("Saved int8 quantized model")

Palettisation (regroupement des poids)

import coremltools as ct
import coremltools.optimize as cto

# Palettization clusters weights into a small lookup table
# 4-bit palettization = 16 unique weight values per tensor
# Achieves ~4x compression with minimal accuracy loss

model = ct.models.MLModel("ResNet50.mlpackage")

# Configure palettization
op_config = cto.coreml.OpPalettizerConfig(
    mode="kmeans",
    nbits=4,              # 4-bit = 16 clusters, 2-bit = 4 clusters
    granularity="per_tensor"
)

config = cto.coreml.OptimizationConfig(global_config=op_config)

# Apply palettization
model_palettized = cto.coreml.palettize_weights(model, config=config)
model_palettized.save("ResNet50_palettized_4bit.mlpackage")

print("4-bit palettized model saved")
print("This model runs optimally on the Neural Engine")

Élagage (parcimonie)

import coremltools as ct
import coremltools.optimize as cto

# Pruning sets small weights to zero, enabling sparse computation
# The Neural Engine can skip zero-weight operations for speed gains

model = ct.models.MLModel("ResNet50.mlpackage")

# Configure magnitude-based pruning
op_config = cto.coreml.OpMagnitudePrunerConfig(
    target_sparsity=0.75,              # Remove 75% of smallest weights
    granularity="per_channel",
    block_size=None                     # Unstructured pruning
)

config = cto.coreml.OptimizationConfig(global_config=op_config)

# Apply pruning
model_pruned = cto.coreml.prune_weights(model, config=config)
model_pruned.save("ResNet50_pruned_75.mlpackage")

print("75% sparse model saved")

Pipeline d'optimisation combiné

import coremltools as ct
import coremltools.optimize as cto

# For maximum optimization, combine pruning + palettization + quantization
# This can achieve 8-16x compression with 1-2% accuracy loss

model = ct.models.MLModel("ResNet50.mlpackage")

# Step 1: Prune (set small weights to zero)
prune_config = cto.coreml.OptimizationConfig(
    global_config=cto.coreml.OpMagnitudePrunerConfig(target_sparsity=0.5)
)
model = cto.coreml.prune_weights(model, config=prune_config)
print("Step 1: Pruning complete (50% sparsity)")

# Step 2: Palettize (cluster remaining weights)
palette_config = cto.coreml.OptimizationConfig(
    global_config=cto.coreml.OpPalettizerConfig(mode="kmeans", nbits=4)
)
model = cto.coreml.palettize_weights(model, config=palette_config)
print("Step 2: Palettization complete (4-bit)")

# Save the fully optimized model
model.save("ResNet50_optimized.mlpackage")
print("Fully optimized model saved -- ready for Neural Engine deployment")

Conseil : Mesurez toujours la précision après l'optimisation. Commencez par le float16 (le plus sûr), puis essayez la quantification int8, puis la palettisation. Utilisez un ensemble de validation séparé et définissez un seuil de précision acceptable avant d'appliquer des optimisations agressives.

6. Créer une API REST pour l'inférence CoreML

Encapsuler votre modèle CoreML dans une API REST le rend accessible à tout client -- applications web, applications mobiles, microservices ou pipelines de traitement par lots. Voici des exemples prêts pour la production avec Flask et FastAPI.

Option A : serveur API Flask

# flask_coreml_server.py
# pip install flask pillow coremltools gunicorn

import io
import time
import coremltools as ct
from flask import Flask, request, jsonify
from PIL import Image

app = Flask(__name__)

# Load the CoreML model at startup (runs on Neural Engine)
print("Loading CoreML model...")
model = ct.models.MLModel(
    "ResNet50_optimized.mlpackage",
    compute_units=ct.ComputeUnit.ALL
)
print("Model loaded successfully")

@app.route("/health", methods=["GET"])
def health():
    return jsonify({"status": "healthy", "model": "ResNet50"})

@app.route("/predict", methods=["POST"])
def predict():
    if "image" not in request.files:
        return jsonify({"error": "No image file provided"}), 400

    # Read and preprocess the image
    image_file = request.files["image"]
    image = Image.open(io.BytesIO(image_file.read())).resize((224, 224))

    # Run inference with timing
    start = time.perf_counter()
    prediction = model.predict({"image": image})
    latency_ms = (time.perf_counter() - start) * 1000

    return jsonify({
        "prediction": prediction,
        "latency_ms": round(latency_ms, 2),
        "compute_unit": "neural_engine+gpu+cpu"
    })

@app.route("/predict/batch", methods=["POST"])
def predict_batch():
    """Process multiple images in a single request."""
    if "images" not in request.files:
        return jsonify({"error": "No image files provided"}), 400

    results = []
    files = request.files.getlist("images")

    start = time.perf_counter()
    for image_file in files:
        image = Image.open(io.BytesIO(image_file.read())).resize((224, 224))
        prediction = model.predict({"image": image})
        results.append(prediction)
    total_ms = (time.perf_counter() - start) * 1000

    return jsonify({
        "predictions": results,
        "total_latency_ms": round(total_ms, 2),
        "images_processed": len(results),
        "avg_latency_ms": round(total_ms / len(results), 2)
    })

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

# Production: gunicorn flask_coreml_server:app -w 2 -b 0.0.0.0:5000 --timeout 120

Option B : serveur FastAPI (asynchrone + documentation OpenAPI)

# fastapi_coreml_server.py
# pip install fastapi uvicorn python-multipart pillow coremltools

import io
import time
import asyncio
from concurrent.futures import ThreadPoolExecutor
import coremltools as ct
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from PIL import Image
from typing import List

app = FastAPI(
    title="CoreML Inference API",
    description="Production CoreML model serving on Mac Mini M4",
    version="1.0.0"
)

# Load model at startup
model = ct.models.MLModel(
    "ResNet50_optimized.mlpackage",
    compute_units=ct.ComputeUnit.ALL
)

# Thread pool for blocking CoreML calls
executor = ThreadPoolExecutor(max_workers=4)

def run_prediction(image_bytes: bytes) -> dict:
    """Run CoreML prediction in a thread (blocking call)."""
    image = Image.open(io.BytesIO(image_bytes)).resize((224, 224))
    start = time.perf_counter()
    result = model.predict({"image": image})
    latency = (time.perf_counter() - start) * 1000
    return {"prediction": result, "latency_ms": round(latency, 2)}

@app.get("/health")
async def health():
    return {"status": "healthy", "model": "ResNet50_optimized", "engine": "CoreML"}

@app.post("/predict")
async def predict(image: UploadFile = File(...)):
    if not image.content_type.startswith("image/"):
        raise HTTPException(status_code=400, detail="File must be an image")

    image_bytes = await image.read()
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(executor, run_prediction, image_bytes)
    return result

@app.post("/predict/batch")
async def predict_batch(images: List[UploadFile] = File(...)):
    loop = asyncio.get_event_loop()
    tasks = []
    for img in images:
        image_bytes = await img.read()
        tasks.append(loop.run_in_executor(executor, run_prediction, image_bytes))

    results = await asyncio.gather(*tasks)
    return {
        "predictions": list(results),
        "total_images": len(results)
    }

# Run: uvicorn fastapi_coreml_server:app --host 0.0.0.0 --port 8000 --workers 2

Tester l'API

# Test single prediction
curl -X POST http://localhost:8000/predict \
  -F "image=@test_image.jpg"

# Test batch prediction
curl -X POST http://localhost:8000/predict/batch \
  -F "images=@image1.jpg" \
  -F "images=@image2.jpg" \
  -F "images=@image3.jpg"

# Health check
curl http://localhost:8000/health

# Python client example
import requests

with open("test_image.jpg", "rb") as f:
    response = requests.post(
        "http://your-mac-mini:8000/predict",
        files={"image": f}
    )
print(response.json())
# {"prediction": {"classLabel": "golden_retriever", "confidence": 0.94}, "latency_ms": 3.2}

Service de type systemd avec launchd

# Create launchd plist for auto-start on boot
cat <<EOF > ~/Library/LaunchAgents/com.coreml.api.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.coreml.api</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/admin/coreml-env/bin/uvicorn</string>
        <string>fastapi_coreml_server:app</string>
        <string>--host</string>
        <string>0.0.0.0</string>
        <string>--port</string>
        <string>8000</string>
        <string>--workers</string>
        <string>2</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/Users/admin/coreml-api</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/var/log/coreml-api.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/coreml-api-error.log</string>
</dict>
</plist>
EOF

# Load the service
launchctl load ~/Library/LaunchAgents/com.coreml.api.plist

# Verify
curl http://localhost:8000/health

7. Benchmarks de performances

Ces benchmarks comparent l'inférence CoreML sur le Mac Mini M4 avec PyTorch MPS (Metal Performance Shaders) et l'exécution CPU uniquement. Tous les tests utilisent l'inférence sur une seule image avec un batch size de 1.

Classification d'images (ResNet50, entrée 224x224)

Runtime Précision Latence (ms) Débit (img/s) Puissance (W)
CoreML (Neural Engine) Int8 1.2 ms ~833 ~3W
CoreML (Neural Engine) Float16 2.1 ms ~476 ~4W
CoreML (GPU only) Float16 3.8 ms ~263 ~8W
PyTorch MPS (GPU) Float32 5.4 ms ~185 ~10W
PyTorch CPU Float32 18.6 ms ~54 ~12W

Détection d'objets (YOLOv8n, entrée 640x640)

Runtime Précision Latence (ms) Débit (img/s) mAP@0.5
CoreML (All Units) Float16 4.8 ms ~208 37.2%
CoreML (All Units) Int8 3.5 ms ~286 36.8%
PyTorch MPS (GPU) Float32 12.3 ms ~81 37.3%
PyTorch CPU Float32 45.7 ms ~22 37.3%

Exécutez vos propres benchmarks

import coremltools as ct
import numpy as np
import time

model = ct.models.MLModel("ResNet50_optimized.mlpackage", compute_units=ct.ComputeUnit.ALL)

# Warmup (first inference compiles the model for the Neural Engine)
from PIL import Image
dummy = Image.new("RGB", (224, 224))
for _ in range(10):
    model.predict({"image": dummy})

# Benchmark
latencies = []
for _ in range(1000):
    start = time.perf_counter()
    model.predict({"image": dummy})
    latencies.append((time.perf_counter() - start) * 1000)

latencies = np.array(latencies)
print(f"Mean latency:   {latencies.mean():.2f} ms")
print(f"Median latency: {np.median(latencies):.2f} ms")
print(f"P95 latency:    {np.percentile(latencies, 95):.2f} ms")
print(f"P99 latency:    {np.percentile(latencies, 99):.2f} ms")
print(f"Throughput:     {1000 / latencies.mean():.0f} images/sec")

Point clé : CoreML avec le Neural Engine offre un débit 3 à 4 fois supérieur à PyTorch MPS sur le même matériel, et 10 à 15 fois supérieur à l'inférence CPU uniquement. Le chemin int8 quantifié est le meilleur compromis -- inférence la plus rapide avec moins de 0,5 % de perte de précision pour la plupart des modèles.

8. Mise à l'échelle avec plusieurs modèles

Les déploiements en production nécessitent souvent de servir plusieurs modèles ou de gérer une forte concurrence. Vous pouvez utiliser nginx comme proxy inverse et équilibreur de charge entre plusieurs instances de Mac Mini M4, ou servir plusieurs modèles depuis une seule machine.

Serveur multi-modèles

# multi_model_server.py
import io
import time
import coremltools as ct
from fastapi import FastAPI, File, UploadFile, HTTPException
from PIL import Image

app = FastAPI(title="Multi-Model CoreML Server")

# Load multiple models at startup
models = {}

@app.on_event("startup")
async def load_models():
    print("Loading models...")
    models["resnet50"] = ct.models.MLModel(
        "ResNet50_optimized.mlpackage", compute_units=ct.ComputeUnit.ALL
    )
    models["yolov8"] = ct.models.MLModel(
        "YOLOv8n.mlpackage", compute_units=ct.ComputeUnit.ALL
    )
    models["efficientnet"] = ct.models.MLModel(
        "EfficientNetV2S.mlpackage", compute_units=ct.ComputeUnit.ALL
    )
    print(f"Loaded {len(models)} models: {list(models.keys())}")

@app.get("/models")
async def list_models():
    return {"models": list(models.keys())}

@app.post("/predict/{model_name}")
async def predict(model_name: str, image: UploadFile = File(...)):
    if model_name not in models:
        raise HTTPException(404, f"Model '{model_name}' not found. Available: {list(models.keys())}")

    image_data = Image.open(io.BytesIO(await image.read()))

    # Resize based on model requirements
    input_sizes = {"resnet50": (224, 224), "yolov8": (640, 640), "efficientnet": (384, 384)}
    image_data = image_data.resize(input_sizes.get(model_name, (224, 224)))

    start = time.perf_counter()
    result = models[model_name].predict({"image": image_data})
    latency = (time.perf_counter() - start) * 1000

    return {"model": model_name, "prediction": result, "latency_ms": round(latency, 2)}

# Run: uvicorn multi_model_server:app --host 0.0.0.0 --port 8000

Équilibreur de charge nginx sur plusieurs Mac Minis

# /etc/nginx/nginx.conf
# Install nginx: brew install nginx

upstream coreml_backend {
    # Round-robin across multiple Mac Mini M4 instances
    server 10.0.1.10:8000 weight=1;   # Mac Mini M4 #1
    server 10.0.1.11:8000 weight=1;   # Mac Mini M4 #2
    server 10.0.1.12:8000 weight=1;   # Mac Mini M4 #3

    # Health check: remove unhealthy backends
    keepalive 32;
}

server {
    listen 80;
    server_name api.yourdomain.com;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

    location / {
        limit_req zone=api burst=50 nodelay;

        proxy_pass http://coreml_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Timeout settings for ML inference
        proxy_connect_timeout 10s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;

        # Enable keepalive to backend
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    location /health {
        proxy_pass http://coreml_backend;
        access_log off;
    }
}

# Test config and start
# nginx -t
# nginx

Docker-Compose pour le développement local

# docker-compose.yml
# Note: CoreML requires macOS -- Docker containers run CPU-only inference
# For production, use launchd services directly on macOS

version: "3.8"
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - coreml-api

  coreml-api:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./models:/app/models
    environment:
      - MODEL_PATH=/app/models/ResNet50_optimized.mlpackage
      - WORKERS=2
    deploy:
      replicas: 2

9. Surveillance et observabilité

Les systèmes ML en production nécessitent une surveillance de la latence d'inférence, du débit, des taux d'erreur et de l'utilisation des ressources système. Voici comment instrumenter votre API CoreML avec des métriques Prometheus et une surveillance au niveau système.

Ajouter des métriques Prometheus à FastAPI

# pip install prometheus-client prometheus-fastapi-instrumentator

import io
import time
import coremltools as ct
from fastapi import FastAPI, File, UploadFile
from PIL import Image
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from starlette.responses import Response

app = FastAPI(title="CoreML API with Monitoring")

# Prometheus metrics
PREDICTIONS_TOTAL = Counter(
    "coreml_predictions_total",
    "Total number of predictions",
    ["model", "status"]
)
PREDICTION_LATENCY = Histogram(
    "coreml_prediction_latency_seconds",
    "Prediction latency in seconds",
    ["model"],
    buckets=[0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]
)
MODEL_LOAD_TIME = Gauge(
    "coreml_model_load_time_seconds",
    "Time taken to load the model",
    ["model"]
)
ACTIVE_REQUESTS = Gauge(
    "coreml_active_requests",
    "Number of currently active requests"
)

# Load model with timing
load_start = time.perf_counter()
model = ct.models.MLModel("ResNet50_optimized.mlpackage", compute_units=ct.ComputeUnit.ALL)
MODEL_LOAD_TIME.labels(model="resnet50").set(time.perf_counter() - load_start)

@app.get("/metrics")
async def metrics():
    return Response(content=generate_latest(), media_type="text/plain")

@app.post("/predict")
async def predict(image: UploadFile = File(...)):
    ACTIVE_REQUESTS.inc()
    try:
        img = Image.open(io.BytesIO(await image.read())).resize((224, 224))

        start = time.perf_counter()
        result = model.predict({"image": img})
        latency = time.perf_counter() - start

        PREDICTION_LATENCY.labels(model="resnet50").observe(latency)
        PREDICTIONS_TOTAL.labels(model="resnet50", status="success").inc()

        return {"prediction": result, "latency_ms": round(latency * 1000, 2)}
    except Exception as e:
        PREDICTIONS_TOTAL.labels(model="resnet50", status="error").inc()
        raise
    finally:
        ACTIVE_REQUESTS.dec()

# Run: uvicorn monitored_server:app --host 0.0.0.0 --port 8000

Configuration Prometheus

# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: "coreml-api"
    static_configs:
      - targets:
        - "10.0.1.10:8000"   # Mac Mini #1
        - "10.0.1.11:8000"   # Mac Mini #2
        - "10.0.1.12:8000"   # Mac Mini #3
    metrics_path: /metrics
    scrape_interval: 5s

  - job_name: "node-exporter"
    static_configs:
      - targets:
        - "10.0.1.10:9100"
        - "10.0.1.11:9100"
        - "10.0.1.12:9100"

Script de surveillance au niveau système

#!/bin/bash
# monitor_coreml.sh -- System health monitoring for CoreML inference servers
# Run with: ./monitor_coreml.sh

echo "=== CoreML Server Health Monitor ==="
echo "$(date)"
echo ""

# Memory usage (critical for CoreML model loading)
echo "--- Memory Usage ---"
vm_stat | head -10
echo ""
memory_pressure
echo ""

# CPU and GPU utilization
echo "--- CPU Usage ---"
top -l 1 -n 5 -stats pid,command,cpu,mem | head -10
echo ""

# GPU/Neural Engine power (indicates compute unit activity)
echo "--- GPU/Neural Engine Power ---"
sudo powermetrics --samplers gpu_power,ane_power -n 1 -i 2000 2>/dev/null | grep -E "(GPU|ANE|Neural)"
echo ""

# Disk usage (model files can be large)
echo "--- Disk Usage ---"
df -h / | tail -1
echo ""

# Network connections to API
echo "--- Active API Connections ---"
netstat -an | grep ":8000" | wc -l | xargs echo "Active connections on port 8000:"
echo ""

# API health check
echo "--- API Health Check ---"
curl -s -w "\nHTTP Status: %{http_code}\nResponse Time: %{time_total}s\n" \
  http://localhost:8000/health 2>/dev/null || echo "API is DOWN"

Requêtes pour le tableau de bord Grafana

# Useful PromQL queries for your Grafana dashboard:

# Average prediction latency (last 5 minutes)
rate(coreml_prediction_latency_seconds_sum[5m]) / rate(coreml_prediction_latency_seconds_count[5m])

# Predictions per second
rate(coreml_predictions_total[1m])

# P99 latency
histogram_quantile(0.99, rate(coreml_prediction_latency_seconds_bucket[5m]))

# Error rate percentage
rate(coreml_predictions_total{status="error"}[5m]) / rate(coreml_predictions_total[5m]) * 100

# Active concurrent requests
coreml_active_requests

10. Questions fréquentes

Puis-je utiliser CoreML depuis Python sans projet Xcode ?

Oui. Le package Python coremltools fournit des capacités complètes d'inférence. Vous pouvez charger des modèles .mlpackage et exécuter des prédictions directement depuis des scripts Python, des serveurs Flask/FastAPI ou des notebooks Jupyter. Aucun Xcode, Swift ou Objective-C requis.

CoreML utilise-t-il vraiment le Neural Engine sur Mac Mini M4 ?

Oui, lorsque vous définissez compute_units=ct.ComputeUnit.ALL, le compilateur CoreML route automatiquement les opérations compatibles vers le Neural Engine. Vous pouvez le vérifier en surveillant la consommation d'énergie avec sudo powermetrics --samplers ane_power -- vous verrez l'ANE (Apple Neural Engine) consommer de l'énergie pendant l'inférence.

Quels types de modèles fonctionnent le mieux avec CoreML sur Mac Mini M4 ?

CoreML excelle avec les réseaux de neurones convolutifs (classification d'images, détection d'objets, segmentation), les modèles transformers (NLP, vision transformers) et les réseaux feedforward standard. Le Neural Engine est particulièrement efficace pour les modèles int8 quantifiés avec des opérations de convolution et de multiplication matricielle. Les opérations personnalisées qui ne peuvent pas être mappées au Neural Engine se replient automatiquement sur le GPU ou le CPU.

Comment CoreML se compare-t-il à PyTorch avec MPS (Metal) ?

CoreML est généralement 2 à 4 fois plus rapide que PyTorch MPS pour l'inférence car il peut utiliser le Neural Engine (auquel PyTorch ne peut pas accéder) et applique des optimisations de graphe spécifiques au matériel au moment de la compilation. PyTorch MPS n'utilise que le GPU via les shaders Metal. Pour les charges de travail d'entraînement, PyTorch MPS est le meilleur choix puisque CoreML est réservé à l'inférence.

Puis-je convertir des LLM (grands modèles de langage) vers CoreML ?

C'est possible mais pas toujours pratique. CoreML prend en charge les architectures transformer, et Apple a démontré Stable Diffusion et certains modèles de langage fonctionnant sur CoreML. Cependant, pour les LLM spécifiquement, des frameworks comme MLX, Ollama et llama.cpp sont mieux optimisés pour la génération de texte autorégressive. CoreML excelle pour les modèles encodeur uniquement (BERT, embeddings) et les modèles de vision.

Combien de mémoire un modèle CoreML utilise-t-il à l'exécution ?

Les modèles CoreML utilisent approximativement la même quantité de mémoire que leur taille de fichier sur disque, plus un léger surcoût pour les activations intermédiaires et le runtime. Un ResNet50 en float16 utilise environ 50 Mo, une version int8 environ 25 Mo. Les 16 Go de mémoire unifiée du M4 peuvent confortablement servir plus de 10 modèles optimisés simultanément, ou quelques modèles plus grands comme EfficientNet ou les vision transformers.

Y a-t-il un délai de compilation à la première inférence ?

Oui. La première fois qu'un modèle CoreML s'exécute sur une configuration d'unité de calcul donnée, le système compile un plan d'exécution optimisé. Cela peut prendre 2 à 10 secondes selon la complexité du modèle. Les inférences suivantes sont quasi-instantanées. Pour les API de production, exécutez toujours une prédiction de préchauffage au démarrage pour absorber ce coût de compilation avant d'accepter du trafic.

Guides connexes

Déployez des modèles CoreML sur du matériel dédié

Obtenez un Mac Mini M4 dédié avec accélération Neural Engine. Exécutez l'inférence CoreML avec une latence inférieure à la milliseconde et sans coût par requête. À partir de 75 $/mois avec un essai gratuit de 7 jours.