Skip to content

LLM en local — Apple Silicon, Ollama, LM Studio, llamafile

TL;DR En 2026, un MacBook M3/M4 Pro avec 36-64 GB de RAM unifiée fait tourner confortablement Llama 3.3 8B, Qwen 2.5 14B, Mistral Small 24B (Q4-Q5), Phi 4 (14B). MLX (Apple) est le runtime natif, ultra-rapide sur GPU intégré. Ollama est le wrapper UX standard (1 commande pour pull + run), LM Studio offre une GUI, llamafile packe tout en 1 exécutable. Use cases reines : (1) avocat/médecin qui veut "data ne quitte jamais mon poste", (2) dev qui code offline (Cursor + local model), (3) démo client banque sans contrat data, (4) assistant perso (Obsidian, notes, recherche locale). Limite : qualité reste un cran sous la frontier cloud — un Mistral Small 24B Q5 tient la route sur de l'extraction/résumé mais reste loin de claude-opus-4-8 (5 $/25 $ par M tok, 1M ctx) ou même de claude-haiku-4-5 (1 $/5 $) sur du raisonnement multi-étapes — et le coût RAM est élevé. Mais en 2026, on bascule sur du local sans honte pour 60% des tâches "assistant". La décision senior n'est pas "local vs cloud" en absolu : c'est un routage par tâche, où le local absorbe le volume confidentiel/offline et le cloud encaisse le reste (voir le pattern hybride plus bas).


🧠 Mental model

              ┌──────────────────────────────────────────┐
              │  Pourquoi local plutôt que API ?         │
              ├──────────────────────────────────────────┤
              │ - Data sensible (avocat, médecin, M&A)   │
              │ - Pas de réseau (TGV, audience, mission) │
              │ - Coût zéro à l'usage                    │
              │ - Latence stable (pas de network var.)   │
              │ - Démo client "votre data reste chez vous"│
              └──────────────────────────────────────────┘

  Stack local 2026 :
  ┌────────────────────────────────────────────────────────┐
  │  Application (Cursor, app desktop, Obsidian plugin)    │
  └─────────────────────────┬──────────────────────────────┘
                            │ HTTP local localhost:11434
  ┌─────────────────────────▼──────────────────────────────┐
  │  Runtime local                                         │
  │  Ollama          → wrapper simple, REST API            │
  │  LM Studio       → GUI + serveur OpenAI-compatible     │
  │  llamafile       → single binary cross-OS              │
  │  llama.cpp       → bas niveau, Metal/CUDA/Vulkan       │
  │  MLX-LM (Apple)  → natif Apple Silicon, ultra-rapide   │
  └─────────────────────────┬──────────────────────────────┘

  ┌─────────────────────────▼──────────────────────────────┐
  │  Modèle GGUF / MLX (quantizé)                          │
  │  Mistral Small 24B Q5_K_M = 17 GB RAM                  │
  │  Llama 3.2 3B Q8 = 3.4 GB RAM                          │
  │  Phi 4 14B Q5_K_M = 10 GB RAM                          │
  │  Qwen 2.5 7B Q5_K_M = 5.5 GB RAM                       │
  └────────────────────────────────────────────────────────┘

Analogie : entre API cloud (= consultant externe que tu paies à la mission) et self-host datacenter (= équipe interne dédiée), le local laptop c'est l'assistant qui voyage avec toi — limité en capacité mais toujours là, sans coût marginal, et confidentialité absolue.


🛠️ Code minimal

Ollama (le standard)

bash
# Installer
brew install ollama
ollama serve  # daemon en arrière-plan

# Récupérer un modèle
ollama pull mistral-small:24b-instruct-2502-q5_K_M

# Chat
ollama run mistral-small "Explique-moi la TVA sur autoliquidation"

# Mode REST API (compat OpenAI partielle)
curl http://localhost:11434/api/chat -d '{
  "model": "mistral-small:24b-instruct-2502-q5_K_M",
  "messages": [{"role": "user", "content": "Hello"}],
  "stream": false
}'

MLX-LM (Apple Silicon natif)

bash
# Installation
pip install mlx-lm

# Génération directe
mlx_lm.generate --model mlx-community/Mistral-Small-Instruct-2502-4bit \
    --prompt "Explique le ratio liquidité immédiate" \
    --max-tokens 500

# Serveur OpenAI-compatible
mlx_lm.server --model mlx-community/Mistral-Small-Instruct-2502-4bit \
    --port 8080
python
# Python client
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="local")
resp = client.chat.completions.create(
    model="mlx-community/Mistral-Small-Instruct-2502-4bit",
    messages=[{"role": "user", "content": "Bonjour"}],
)

llamafile (1 fichier exécutable)

bash
# Télécharger un llamafile (modèle + runtime en 1 .exe)
wget https://huggingface.co/Mozilla/Mistral-Small-Instruct-2502-llamafile/resolve/main/mistral-small.llamafile
chmod +x mistral-small.llamafile
./mistral-small.llamafile --server --host 0.0.0.0 --port 8080

🎬 Cas d'usage concrets

1. Avocat solo Paris — assistant 100% local sur MacBook M4 Pro

Avocate en droit des affaires, 1 collaboratrice. Dossiers M&A et capital-investissement = data ultra-sensible (NDA stricts avec clients corporates). Pas question d'envoyer à OpenAI ou Anthropic. Setup : MacBook M4 Pro 48GB + Ollama + Mistral Small 24B Q5 + LM Studio. Cas d'usage : rédaction préliminaire de pactes d'associés, résumés de bilans, recherche dans archives clientes locales (RAG local avec Chroma + nomic-embed-text). Latence : 18 tok/s, acceptable. Pas de cloud, pas d'abonnement, conformité absolue. La cliente loue le setup à 12k€ HT pour le freelance (audit + install + formation 3 jours).

2. Médecin libéral — HDS = local plutôt que cloud HDS

Médecin généraliste rural. Veut un assistant pour reformuler CR de consultation et lui suggérer codes CIM-11. Cloud HDS = trop cher (300-800€/mois pour solo). Alternative : Mac mini M4 24GB en cabinet + Ollama + Mistral Small 7B FT médical (modèle communauté HuggingFace BioMistral) + plugin Obsidian pour ses notes patient. Zéro cloud, RGPD trivial (data jamais ne quitte le cabinet). Investissement : 1 800€ Mac mini + 4 jours setup freelance (4 800€). Run rate : 0.

3. POC client banque — démo "vos données ne quittent jamais notre laptop"

Freelance en avant-vente chez une grande banque française. Doit présenter un assistant IA pour analystes. La banque refuse tout cloud externe pour le POC. Setup démo : MacBook M3 Max 128GB + Ollama + Mistral Large 2 (123B Q4_K_M = ~ 65 GB) + scripts custom pour démo en live. Le client voit que rien ne sort de l'écran du laptop, ce qui débloque psychologiquement la conversation. Le contrat de prod (qui sera self-host Scaleway HDS) signe pour 600k€/an.

4. Développeur freelance — assistant code offline (TGV, café sans wifi)

Dev Python freelance qui travaille 30% en TGV / coworking. Setup Cursor + Continue.dev avec backend Ollama local (Qwen 2.5 Coder 14B Q5). Quand wifi : route vers Sonnet API. Quand offline : route vers Qwen local. UX continue, pas de frustration "j'ai pas de réseau". Bonus : data code client jamais envoyée à un provider (utile pour missions où le code est sensible).


🛠️ Exemple end-to-end — Tauri app pour avocat avec Ollama embarqué

Contexte : éditeur logiciel juridique veut packager une app desktop (Mac + Windows) pour avocats individuels et petits cabinets, avec assistant IA 100% local. Argument commercial : "votre data ne quitte JAMAIS votre poste". Cible : 3 500 abonnés × 49€/mois = 170k€ MRR.

Étape 1 — Choix techniques

  • Frontend : Tauri (Rust + WebView, plus léger qu'Electron, app ~ 30 MB)
  • Backend AI : Ollama embarqué (sidecar) + modèle Mistral Small 7B Q5 (téléchargé à la 1ère lancement, ~ 5.5 GB)
  • Stockage : SQLite chiffré pour notes/dossiers utilisateur
  • RAG local : SQLite-vss ou Chroma local + nomic-embed-text pour embeddings

Étape 2 — Architecture Tauri

my-legal-app/
├── src-tauri/
│   ├── src/
│   │   ├── main.rs              # entry point, démarre Ollama sidecar
│   │   ├── ollama_manager.rs    # spawn/monitor Ollama process
│   │   └── ai_commands.rs       # Tauri commands #[command]
│   ├── binaries/
│   │   ├── ollama-darwin-arm64  # embedded binaries per platform
│   │   ├── ollama-darwin-x64
│   │   └── ollama-win-x64.exe
│   └── tauri.conf.json
├── src/                          # React + Tailwind
│   ├── components/
│   ├── pages/
│   └── lib/ai.ts                 # client Ollama

Étape 3 — Code Rust : sidecar Ollama

rust
// src-tauri/src/ollama_manager.rs
use std::process::{Child, Command};
use std::sync::Mutex;
use tauri::api::path::app_data_dir;

pub struct OllamaState(pub Mutex<Option<Child>>);

#[tauri::command]
pub async fn start_ollama(app: tauri::AppHandle, state: tauri::State<'_, OllamaState>) -> Result<(), String> {
    let ollama_bin = app.path_resolver()
        .resolve_resource("binaries/ollama-darwin-arm64")
        .ok_or("binary not found")?;

    let data_dir = app_data_dir(&app.config()).unwrap();

    let child = Command::new(ollama_bin)
        .arg("serve")
        .env("OLLAMA_MODELS", data_dir.join("ollama_models"))
        .env("OLLAMA_HOST", "127.0.0.1:11434")
        .spawn()
        .map_err(|e| e.to_string())?;

    *state.0.lock().unwrap() = Some(child);
    Ok(())
}

#[tauri::command]
pub async fn ensure_model_pulled(model: String) -> Result<(), String> {
    let client = reqwest::Client::new();
    client.post("http://127.0.0.1:11434/api/pull")
        .json(&serde_json::json!({"name": model}))
        .send().await.map_err(|e| e.to_string())?;
    Ok(())
}

Étape 4 — Frontend : appel à Ollama

typescript
// src/lib/ai.ts
import { invoke } from "@tauri-apps/api/tauri";

const OLLAMA = "http://127.0.0.1:11434";
const MODEL = "mistral-small:7b-instruct-q5_K_M";

export async function bootstrap() {
  await invoke("start_ollama");
  await invoke("ensure_model_pulled", { model: MODEL });
}

export async function chat(messages: Message[]) {
  const r = await fetch(`${OLLAMA}/api/chat`, {
    method: "POST",
    body: JSON.stringify({ model: MODEL, messages, stream: true }),
  });

  const reader = r.body!.getReader();
  const decoder = new TextDecoder();
  let buf = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buf += decoder.decode(value);
    // parse NDJSON lines, yield to UI
  }
}

Étape 5 — RAG local (dossiers de l'avocat)

typescript
// indexer.ts — index les fichiers du cabinet
import { invoke } from "@tauri-apps/api/tauri";

async function embed(text: string): Promise<number[]> {
  const r = await fetch(`${OLLAMA}/api/embeddings`, {
    method: "POST",
    body: JSON.stringify({
      model: "nomic-embed-text",
      prompt: text,
    }),
  });
  const data = await r.json();
  return data.embedding;
}

async function indexDocument(path: string) {
  const text = await invoke<string>("read_file", { path });
  const chunks = chunkText(text, 800, 100);  // 800 tokens, overlap 100
  for (const chunk of chunks) {
    const v = await embed(chunk);
    await invoke("sqlite_vss_insert", { path, chunk, vector: v });
  }
}

async function search(query: string, k: number = 5) {
  const v = await embed(query);
  return invoke("sqlite_vss_search", { vector: v, k });
}

Étape 6 — UX d'onboarding

1. Premier lancement
   "Bienvenue. Pour activer l'assistant IA, nous devons télécharger
    un modèle (5.5 GB). Ce modèle restera intégralement sur votre Mac.
    [Démarrer le téléchargement]"

2. Download
   Progress bar Ollama API /api/pull (stream)

3. Test
   "Posez votre première question pour vérifier que tout fonctionne"

4. Indexation dossiers (optionnel)
   "Voulez-vous que l'assistant puisse rechercher dans vos dossiers ?
    Sélectionnez un dossier à indexer (les fichiers restent sur votre Mac)"

Étape 7 — Packaging et distribution

toml
# Cargo.toml + tauri.conf.json
{
  "tauri": {
    "bundle": {
      "identifier": "fr.legalsoft.assistant",
      "resources": ["binaries/*"],
      "macOS": {
        "minimumSystemVersion": "12.0",
        "signingIdentity": "Developer ID Application: LegalSoft SARL"
      },
      "windows": {
        "wix": {"language": "fr-FR"}
      }
    }
  }
}

Distribution : notarized DMG sur landing page, signed MSI pour Windows. Auto-update via Tauri updater (binaire signé). App finale ~ 35 MB (sans le modèle qui se télécharge au premier run).

Étape 8 — Mesure et ROI client (l'éditeur)

Coût dev initial :
  - Tauri app + intégration Ollama : 35j × 1200€ = 42 000€
  - RAG local + UX : 15j × 1200€ = 18 000€
  - QA, notarization, builds CI : 8j × 1200€ = 9 600€
  - Total : 69 600€

Run rate :
  - Maintenance/updates : 30k€/an
  - Support N1 (mutualisé) : 25k€/an

Revenus prévus :
  3500 abonnés × 49€/mois = 171 500€/mois = ~ 2M€/an

Marge brute énorme : pas de coût d'inference par utilisateur (tout local).
C'est exactement pourquoi le local est un game-changer pour le pricing SaaS B2B.

🎯 Patterns courants

  • Choisir le modèle par RAM : règle = RAM_GB ≥ params_B × 0.7 pour Q5. Donc 24B en Q5 ≈ 17 GB.
  • Pré-warmer le modèle : 1ère inference après load = 3-5s. Faire un ping "hello" au boot.
  • Streaming UX : sans streaming, l'utilisateur croit que c'est planté. Toujours streamer.
  • Fallback offline gracieux : détecter si Ollama process down → message clair + bouton restart.
  • Contexte court : sur local, garder ctx ≤ 8K tokens. Au-delà, latence explose (KV cache).
  • GGUF Q5_K_M : sweet spot qualité/taille. Q4 dégrade trop, Q6+ trop gros pour pas grand-chose.
  • MLX > llama.cpp sur Mac : 30-50% plus rapide sur M3/M4 Pro/Max.
  • Auto-update modèles : faire ollama pull mensuel pour récupérer améliorations.
  • Privacy by design : aucun télémétrie qui sort du laptop sans consentement explicite.

🧮 Mental model — la quantization, vraiment

C'est le concept que les juniors récitent ("Q4 c'est 4 bits") sans savoir raisonner dessus. Un staff engineer sait défendre le chiffre.

Ce qu'on quantize. Les poids d'un LLM sont des floats. En FP16, chaque poids = 2 octets. Un 24B en FP16 = 24e9 × 2 = 48 GB. Inutilisable sur un laptop. La quantization remplace ces floats par des entiers de N bits (+ un facteur d'échelle par bloc). Q4 ≈ 4 bits/poids + overhead → ~0.5 octet effectif → le 24B passe de 48 GB à ~14-17 GB.

Taille_RAM ≈ params_B × (bits_par_poids / 8) × overhead
           + KV_cache(ctx, batch)        ← souvent oublié, explose en contexte long

Le KV cache, le piège du débutant. La formule params_B × 0.7 ne couvre QUE les poids. Le KV cache (les clés/valeurs attention mises en cache pour ne pas recalculer) grandit linéairement avec le contexte. Sur un 24B à 8K tokens il est négligeable ; à 32K il peut ajouter plusieurs GB. C'est pourquoi le pitfall "ctx ≤ 8K sur local" n'est pas du folklore : au-delà, soit tu OOM, soit le runtime spill sur le swap et le débit s'effondre.

Pourquoi Q5_K_M et pas Q4 ni Q8. La dégradation n'est pas linéaire. Mesure-la avec la perplexité (à quel point le modèle est "surpris" par un texte de référence — plus bas = mieux) :

QuantBits eff.Δ perplexité vs FP16Verdict senior
Q8_0~8~+0.1%gaspillage : 2× la RAM pour un gain imperceptible
Q6_K~6~+0.3%OK si la RAM est là, rarement justifié
Q5_K_M~5.5~+0.6%sweet spot qualité/taille
Q4_K_M~4.5~+1.5%acceptable, sweet spot RAM si serré
Q3_K~3.5~+5-8%dégradation visible, hallucinations
Q2_K~2.5~+15-30%cassé, ne JAMAIS livrer en prod

Les _K_M désignent la quantization "K-quant medium" de llama.cpp : elle garde plus de précision sur les couches sensibles (attention, derniers blocs) et compresse agressivement le reste. C'est pour ça que Q4_K_M bat un Q4_0 "plat" à taille égale.

La règle qu'un senior applique : descends en quant jusqu'à ce que la perplexité ou un eval métier dépasse un seuil que tu as défini, pas un seuil de blog. Sur du français juridique, un Q4 qui hallucine un article de loi est inacceptable même si la perplexité globale est correcte — l'eval doit être sur ta tâche, pas sur WikiText.


🔀 Pattern senior — routage hybride local ↔ cloud

Le use case #4 (dev offline) le mentionne, mais c'est LE pattern de production. Tu n'opposes pas local et cloud : tu routes. Local pour le confidentiel/offline/volume ; claude-opus-4-8 (ou claude-haiku-4-5 pour le cheap) en escalade quand la tâche dépasse les capacités locales ou quand le réseau est là et la data non sensible.

Côté serveur, on utilise AsyncAnthropic (jamais le client sync dans un event loop), avec retries SDK, exceptions typées et logging d'usage pour le coût :

python
import asyncio
import httpx
from anthropic import (
    AsyncAnthropic,
    APIConnectionError,
    RateLimitError,
    APIStatusError,
)

# Client cloud : retries + timeout par appel. max_retries gère 429/5xx/overload.
cloud = AsyncAnthropic(max_retries=3, timeout=30.0)

OLLAMA = "http://127.0.0.1:11434/api/chat"
LOCAL_MODEL = "mistral-small:24b-instruct-2502-q5_K_M"


async def local_chat(prompt: str) -> str:
    """Inference locale via Ollama. Confidentiel, gratuit, offline-friendly."""
    async with httpx.AsyncClient(timeout=120.0) as client:
        r = await client.post(
            OLLAMA,
            json={
                "model": LOCAL_MODEL,
                "messages": [{"role": "user", "content": prompt}],
                "stream": False,
            },
        )
        r.raise_for_status()
        return r.json()["message"]["content"]


async def cloud_chat(prompt: str) -> str:
    """Escalade vers la frontier. On streame pour les gros outputs et on logue le coût."""
    async with cloud.messages.stream(
        model="claude-opus-4-8",
        max_tokens=4096,
        thinking={"type": "adaptive"},          # 4.8 : adaptatif, pas de budget_tokens
        messages=[{"role": "user", "content": prompt}],
    ) as stream:
        msg = await stream.get_final_message()
    u = msg.usage
    # Coût observable : input 5 $/Mtok, output 25 $/Mtok pour opus-4-8
    cost = u.input_tokens / 1e6 * 5 + u.output_tokens / 1e6 * 25
    print(f"[cloud] in={u.input_tokens} out={u.output_tokens} ~${cost:.4f}")
    return msg.content[0].text


async def route(prompt: str, *, sensitive: bool, offline: bool) -> str:
    """
    Politique de routage senior :
      - data sensible OU offline      → local, point.
      - sinon, on tente le cloud ;
        s'il est down/rate-limited/réseau coupé → fallback local gracieux.
    """
    if sensitive or offline:
        return await local_chat(prompt)
    try:
        return await cloud_chat(prompt)
    except (APIConnectionError, RateLimitError, APIStatusError) as e:
        # Fallback : on dégrade vers local plutôt que de faire échouer la requête.
        print(f"[route] cloud indisponible ({type(e).__name__}) → fallback local")
        return await local_chat(prompt)

Pourquoi ce design est "production-grade" :

  • asyncio.gather pour le parallélisme. Si tu dois traiter 50 résumés confidentiels en local, ne les fais pas en série — await asyncio.gather(*[local_chat(p) for p in prompts]) sature le GPU intégré sans bloquer. (Attention : le débit local est borné par la VRAM partagée — au-delà de ~2-3 requêtes concurrentes sur un Mac, tu thrashes le KV cache.)
  • Exceptions typées, pas de except Exception. RateLimitErrorAPIConnectionErrorAPIStatusError : un rate-limit se retry (le SDK le fait déjà), une coupure réseau bascule en local, une 400 est un bug à corriger.
  • usage logué = coût observable. Sans ça, tu découvres ta facture cloud en fin de mois. La règle : tout appel cloud logue input_tokens / output_tokens. Le local, lui, logue les tok/s et la RAM.
  • Le routage est une policy, pas un if éparpillé. Centralise la décision dans une fonction. Demain le critère change (taille de prompt, langue, latence cible) — un seul endroit à toucher.

⚠️ Sur 4.8 : pas de budget_tokens. L'ancienne forme thinking={"type": "enabled", "budget_tokens": N} renvoie HTTP 400 sur Opus 4.7/4.8. On utilise thinking={"type": "adaptive"} + éventuellement output_config={"effort": "low|medium|high"}. temperature/top_p sont aussi retirés sur ces modèles. Sonnet 4.6 / Haiku ne prennent pas de budget de thinking.


🔭 Observabilité d'un déploiement local

Le cloud te donne usage gratuitement. En local, tu es ton propre observability stack. Ce qu'un senior instrumente avant de livrer :

MétriquePourquoiComment
tok/s (génération)UX chat, dimensionnementOllama logue eval_count / eval_duration dans la réponse /api/chat
TTFT (time-to-first-token)Le user croit que c'est planté si >1.5stimestamp avant requête → premier delta du stream
RAM résidente + swapOOM = crash silencieux ou throttleActivity Monitor / psutil ; alerter si swap > 0
Température / throttle GPUSustained = thermal throttle après ~20 minpowermetrics (macOS) ; surveiller la fréquence GPU
Taux de fallback cloudSi ça explose, le local ne suffit pascompteur dans la route() ci-dessus
Quality driftUne maj de modèle peut régressereval métier en CI sur chaque ollama pull

Sans ces métriques, "ça marche sur mon Mac" devient "ça plante chez le client après 20 min de démo" — le scénario qui tue la confiance commerciale.


🔄 Versions & écosystème 2026

  • MLX (Apple) 0.20+ : runtime natif Apple Silicon, support training et inference. Bibliothèque officielle Apple.
  • MLX-LM : wrapper haut niveau pour text generation.
  • MLX-VLM : models vision-language sur Mac (Idefics, LLaVA, Qwen2-VL).
  • Ollama 0.5+ : structured output, vision models, multi-GPU, OpenAI-compatible API.
  • LM Studio 0.3+ : GUI mature, multi-modèles concurrents, MLX backend disponible.
  • llamafile (Mozilla) : un binary cross-OS, jusqu'à 70B sur RAM suffisante.
  • llama.cpp : moteur en C++ qui sous-tend tout l'écosystème, support Metal, Vulkan, ROCm, CUDA.
  • Continue.dev : extension VSCode/JetBrains qui route vers local ou cloud.
  • Cursor : route vers Ollama en local pour autocomplete privé (mode "Privacy").
  • Modèles 2026 qui tournent en local :
ModèleQuantRAMUse case
Llama 3.2 1BQ81.5 GBclassification, NER
Llama 3.2 3BQ83.4 GBassistant léger, edge
Phi 4 14BQ5_K_M10 GBreasoning, math
Qwen 2.5 7BQ5_K_M5.5 GBpolyvalent FR/EN
Qwen 2.5 14BQ5_K_M10 GBpolyvalent ++
Mistral Small 7B 2024Q5_K_M5 GBFR-friendly
Mistral Small 24B 2502Q5_K_M17 GBqualité Sonnet-1
DeepSeek-R1 Distill 14BQ510 GBreasoning
Qwen 2.5 Coder 14BQ510 GBdev offline
Llama 4 Maverick 17BQ513 GBtout-terrain 2026

⚠️ Pitfalls

  1. RAM insuffisante : Mac M3 16GB ne tient pas Mistral Small 24B. Vérifier params_B × 0.7 avant promesse client.
  2. Thermal throttling MacBook : sur Air ou Pro 14" sans ventilation forte, le GPU throttle après 20 min sustained. Préférer Mac mini ou Studio en cabinet.
  3. Modèles trop quantizés : Q2/Q3 hallucinent énormément. Ne JAMAIS aller sous Q4_K_M en prod.
  4. Ollama version drift : ollama pull ramène la dernière version par défaut, qui peut casser des comportements. Pinner les tags exacts.
  5. Pas de garde-fous offline : un modèle local peut halluciner librement (pas de guardrails cloud). Implémenter ses propres checks.
  6. Coût Apple non amorti : un client qui achète 5 MacBook M4 Pro 64GB à 4 800€ HT = 24k€ pour 5 utilisateurs. Sur 3 ans = 800€/user/mois équivalent. Pas toujours rentable vs cloud.
  7. Mises à jour sécurité oubliées : un Mac qui ne se patch pas = porte d'entrée. Imposer auto-update.
  8. Battery drain : génération soutenue draine la batterie en 1-2h. Prévenir l'utilisateur, ne pas générer en background non sollicité.
  9. Modèle pas FR-friendly : Llama 3.2 3B est moyen en français. Tester chaque modèle sur prompts FR avant de promettre.
  10. Confusion utilisateur : "l'IA est sur mon Mac" est un concept étranger. UX doit clairement séparer "stockage local" / "calcul local" / "réseau désactivé".

💰 Pricing / ROI client

Mission "intégrer Ollama dans app desktop" (freelance senior FR) :

  • Audit faisabilité + benchmark modèles : 4j × 1200€ = 4 800€
  • Intégration Tauri/Electron + sidecar : 12j × 1200€ = 14 400€
  • RAG local + UX + packaging : 10j × 1200€ = 12 000€
  • Tests cross-platform + signing : 5j × 1200€ = 6 000€
  • Total mission packageur : 37 200€

Mission "setup poste de travail avocat/médecin" :

  • Audit besoin + choix Mac + modèle : 1j × 1200€ = 1 200€
  • Installation + config + RAG perso : 2j × 1200€ = 2 400€
  • Formation 1/2 journée : 600€
  • Total mission solo : 4 200€ + matériel Mac ~ 4 800€ HT

Argument commercial client final :

  • Mensualité abonnement SaaS classique : 50-150€/user/mois
  • Setup local one-shot : 4 200€ + Mac payback ~ 18 mois
  • ROI = data sovereignty + pas de récurrent

🧪 Testing / Eval

  • Bench tokens/sec sur cibles : MacBook M3, M3 Pro, M4 Pro, Mac mini M4, M4 Max. Mesurer tok/s sur prompts standardisés.
  • Bench latence first token : critique pour UX chat.
  • Eval qualité FR : 50 prompts FR (technique, juridique, créatif), juger contre Sonnet de référence.
  • Stress test RAM : ouvrir 5 onglets Chrome + Spotify + l'app, vérifier que l'IA tourne encore.
  • Mode batterie : tester l'autonomie avec usage typique (4h éclatées) pour communiquer une attente réaliste.
  • Multi-user : si l'app est partagée (cabinet de 3), tester avec 3 conversations en parallèle.

🔁 Quand utiliser / éviter

Local quand :

  • Data ultra-sensible (avocat, médecin, M&A, brevet).
  • Use case offline (mobilité, terrain).
  • Argument commercial "data reste chez vous" différenciant.
  • Volume faible / utilisateur (assistant perso, pas chat haute fréquence).
  • Mac/PC haut de gamme disponible (RAM ≥ 24 GB).
  • Modèle 7B-30B suffit pour la tâche.

Évite local quand :

  • Besoin de qualité frontier (Opus, GPT-4o pro).
  • Volume élevé (chat haute fréquence = batterie morte + thermal).
  • Hardware utilisateur hétérogène/faible.
  • Tâche nécessite contexte long (> 16K tokens).
  • Tâche nécessite latence < 200ms (local Mac fait 500ms-2s).
  • Multi-utilisateurs sur même device.

🏋️ Exercices

Progressifs, du "fais marcher" au "défends le chiffre / casse-le puis répare-le". L'objectif n'est jamais de changer une constante — c'est de raisonner comme un staff engineer.

1. Bench de quantization sur TA tâche

Objectif : prouver, chiffres en main, quel niveau de quant tu peux livrer pour un use case donné.

Prends Mistral Small 24B en Q3_K_M, Q4_K_M, Q5_K_M et Q8_0. Construis un eval de 30 prompts métier (extraction d'entités sur des CR médicaux fictifs, ou résumé de contrats). Mesure pour chacun : tok/s, RAM résidente, et un score qualité (exactitude des entités extraites, jugé par claude-opus-4-8 en LLM-judge ou à la main). Trace la courbe qualité/RAM.

Indice/Solution : tu dois trouver un coude — souvent Q4_K_M ou Q5_K_M. Le piège : la perplexité globale peut être bonne alors que la qualité s'effondre sur ta tâche précise (un nom propre rare mal tokenizé). C'est pour ça qu'on évalue sur la tâche, pas sur WikiText. Conclus par UNE phrase : "pour ce client, on livre Q_, parce que ___".

2. Routage hybride résilient

Objectif : implémenter la route() du pattern senior et la rendre incassable.

Reprends le code de routage. Ajoute : (a) asyncio.gather pour traiter un batch de 20 prompts, (b) un timeout par appel local ET cloud, (c) un compteur de fallback exposé en métrique, (d) du prompt caching côté cloud (cache_control sur le préfixe système stable) pour ne pas repayer le contexte à chaque appel.

Indice/Solution : le cache_control va sur le bloc système stable (instructions figées), JAMAIS sur la partie variable (la question). Vérifie usage.cache_read_input_tokens > 0 au 2e appel — sinon un invalidateur silencieux (timestamp dans le system prompt ?) casse le cache. Pour gather, borne la concurrence locale avec un asyncio.Semaphore(2) : au-delà, tu thrashes le KV cache du Mac.

3. Casse-le : OOM en contexte long

Objectif : reproduire un crash de production, le diagnostiquer, le réparer.

Charge un 24B Q5 sur un Mac 36 GB. Envoie un prompt de 40K tokens (colle un long document). Observe : soit OOM, soit débit qui s'effondre (swap). Diagnostique avec le monitoring RAM/swap. Puis répare sans changer de modèle : réduis le contexte, chunke le doc + RAG local, ou passe en Q4 pour libérer de la RAM au KV cache.

Indice/Solution : le coupable est le KV cache, pas les poids. La "vraie" réparation prod est rarement "achète plus de RAM" — c'est repenser l'architecture (RAG au lieu de stuffer 40K dans le contexte). Calcule de tête combien de RAM le KV cache prend à 40K vs 8K et défends ton choix.

4. Défends le ROI face au CFO

Objectif : transformer une intuition technique en argument financier défendable.

Un client veut équiper 5 analystes. Compare sur 3 ans : (a) 5 MacBook M4 Pro 64GB local vs (b) abonnement cloud claude-opus-4-8 à volume estimé. Tu dois estimer le volume de tokens/analyste/jour, calculer la facture cloud (5 $/25 $ par M tok), amortir le matériel, et inclure le coût caché (maintenance, throttle, batterie, support N1).

Indice/Solution : il n'y a pas de bonne réponse universelle — c'est le point. Le local gagne sur le volume élevé + data sensible ; le cloud gagne sur le volume faible/variable. Le vrai livrable senior est le seuil : "au-dessus de X tokens/jour/user, le local s'amortit en Y mois". Le piège classique est d'oublier que le local a un coût marginal non-nul (élec, throttle, support).

5. Production-grade : Tauri app qui survit à une démo client

Objectif : durcir l'app embarquée du end-to-end pour qu'elle ne plante pas en live.

Reprends l'app Tauri. Ajoute : pré-warm du modèle au boot (ping "hello"), détection de process Ollama down + bouton restart, fallback gracieux si le modèle n'est pas encore pull, et un indicateur de throttle thermique qui prévient l'utilisateur. Simule une coupure (kill le process Ollama au milieu d'une génération).

Indice/Solution : la démo banque échoue toujours sur l'imprévu — pas sur le happy path. Le pré-warm élimine le "premier token à 5s" qui fait croire au plantage. Le kill-process doit donner un message clair, pas un spinner infini. Bonus : log le tok/s en continu pour détecter le throttle avant que le client ne le remarque.

6. (Hardcore) MLX vs llama.cpp — mesure et explique l'écart

Objectif : ne pas répéter "MLX est 30-50% plus rapide", le mesurer et l'expliquer.

Fais tourner le MÊME modèle (Mistral Small 24B) en MLX et en llama.cpp/Ollama sur le même Mac. Mesure tok/s en génération et TTFT. Puis explique l'écart : Metal kernels natifs MLX, layout mémoire, unified memory exploitée différemment.

Indice/Solution : l'écart varie selon la génération de puce (M3 vs M4) et la quant. MLX brille sur la génération soutenue grâce à ses kernels Metal optimisés et son graphe lazy ; llama.cpp est plus portable mais paie une couche d'abstraction. Si tu ne mesures PAS l'écart sur ta config, tu ne peux pas le promettre à un client — c'est la différence entre un chiffre de blog et un chiffre que tu signes.


🎤 En entretien

Q : "On veut un assistant IA pour des avocats, data ultra-sensible. Cloud ou local ?" R : Local par défaut pour la souveraineté data (rien ne quitte le poste), avec un routage qui n'escalade vers la frontier (claude-opus-4-8) que sur des tâches explicitement non-confidentielles — la décision est une policy centralisée, pas un choix binaire absolu.

Q : "Pourquoi Q5_K_M et pas Q8 ou Q4 ?" R : Q8 double la RAM pour un gain de perplexité imperceptible (~0.1%) ; Q4 économise la RAM mais dégrade visiblement sur les tâches sensibles (noms propres, faits précis). Q5_K_M est le coude qualité/taille — mais le seuil se mesure sur la tâche métier, pas sur WikiText.

Q : "Un 24B Q5 prend 17 GB. Pourquoi ça OOM à 32K de contexte sur un Mac 24 GB ?" R : Les 17 GB ne couvrent que les poids ; le KV cache grandit linéairement avec le contexte et peut ajouter plusieurs GB à 32K. La fix prod n'est pas "plus de RAM" mais repenser l'archi (RAG/chunking au lieu de stuffer le contexte).

Q : "Comment tu observes un déploiement 100% local, sans usage cloud ?" R : Tu deviens ton propre observability stack : tok/s et TTFT pour l'UX, RAM+swap pour l'OOM, fréquence GPU pour le throttle thermique, taux de fallback, et un eval métier en CI à chaque ollama pull pour détecter la régression qualité après une maj de modèle.


🔗 Liens

  • MLX (github.com/ml-explore/mlx) — Apple official
  • MLX-LM (github.com/ml-explore/mlx-lm)
  • Ollama (ollama.com)
  • LM Studio (lmstudio.ai)
  • llamafile (github.com/Mozilla-Ocho/llamafile)
  • llama.cpp (github.com/ggerganov/llama.cpp)
  • Hugging Face — mlx-community (modèles convertis MLX)
  • TheBloke (HF) — GGUF quantizations
  • Simon Willison — blog "local LLMs" (régulièrement à jour)
  • Fichier voisin : 04-vllm-tgi-llamacpp.md (servir des modèles serveur-side)
  • Fichier voisin : 05-self-host.md (alternative datacenter)
  • Section voisine : 09-verticales-fr/ (cas d'usage métiers FR)

Bibliothèque tech perso — Achref