Comment fine-tuner un LLM sur Apple Silicon ?

Fine-tunez un LLM en local sur votre Mac avec MLX.

Publié le par Gabriel Trouvé (mis à jour le )

32 minutes

À la fin de notre article sur C'est quoi le fine-tuning ?, je me demandais si mon MacBook Pro M1 de 2020 et ses 8 Go de RAM pouvaient fine-tuner un modèle en local. On va regarder ça en conditions réelles 😁. Et comme le titre l'indique, dans ce guide on s'intéresse aux puces Apple Silicon.

Un chevalier dans mon MacBook

L'idée est venue en regardant cette vidéo de Renaud Dékode (que je salue au passage !) : et si on récupérait les dialogues de Kaamelott pour faire parler un modèle comme les chevaliers de la Table Ronde ? Sauf que je préfère ne pas prendre de risque sur une œuvre protégée. Nous allons donc créer notre propre parler chevaleresque.

L'objectif : poser les mêmes questions au modèle avant et après le fine-tuning.

Pour la suite, nous allons utiliser MLX 🧐.

À noter

Souvenez-vous de l'article cité plus haut : le fine-tuning ne change pas ce que sait le modèle, mais sa façon de s'exprimer.

C'est quoi MLX ?

MLX est le framework de machine learning d'Apple. Il est conçu spécifiquement pour les puces Apple Silicon. Son module mlx-lm gère la génération de texte et le fine-tuning. Le tout est open-source.

Les puces Apple Silicon utilisent une mémoire unifiée : le CPU et le GPU se partagent la même mémoire physique au lieu d'avoir chacun la leur. Le GPU peut donc utiliser toute la mémoire disponible.

Attention

Cette mémoire unifiée ne fait pas de magie, elle est partagée avec macOS et tout ce qui tourne sur votre machine.

À noter

Un réseau de neurones passe son temps à faire des multiplications de matrices qui peuvent être menées en parallèle. C'est justement ce que sait faire un GPU. Les GPU récents embarquent même des unités dédiées à l'IA (coucou Nvidia).

Installation

Créons notre projet avec uv :

uv init fine-tuning
SHELL

Puis dans le dossier du projet :

uv add mlx-lm
SHELL

Le choix du modèle

Pour ce guide je vais partir sur mlx-community/Ministral-3-3B-Instruct-2512-4bit pour plusieurs raisons :

  • Ministral-3-3B est un petit modèle qui passe sur ma configuration

  • Instruct car il sait déjà dialoguer, on va juste changer sa façon de parler

  • Le modèle est quantizé (4bit)

  • mlx-community est la communauté qui convertit les modèles au format MLX sur Hugging Face

À noter

Hugging Face est une plateforme permettant à des utilisateurs d'héberger, versionner et partager des modèles, des jeux de données et des applications d'IA.

Commençons par tester le modèle en lançant cette commande dans notre projet :

uv run mlx_lm.generate \
  --model mlx-community/Ministral-3-3B-Instruct-2512-4bit \
  --prompt "Bonjour, qui es-tu ?" \
  --max-tokens 100
SHELL
  • mlx_lm.generate charge le modèle et génère une réponse, car comme Ollama, mlx-lm sait faire tourner un modèle en local 😎

  • Au premier lancement, le modèle est téléchargé

  • On limite la réponse à 100 tokens. On pourrait ne pas le faire, mais pour le test je veux être sûr d'avoir une réponse courte

Test avec Ministral-3-3B-Instruct-2512-4bit

Test avec Ministral-3-3B-Instruct-2512-4bit

Construire le dataset

Comme nous l'avons vu dans notre article sur le fine-tuning, le format standard est le JSONL. Pour ma part j'ai fait générer le dataset par Claude Opus 5 avec ce prompt :

Génère 200 paires question/réponse et fournis-les dans un fichier téléchargeable nommé brut.jsonl, au format JSONL : un objet JSON par ligne, exactement sous cette forme :
{"messages": [{"role": "user", "content": "LA QUESTION"}, {"role": "assistant", "content": "LA RÉPONSE"}]}
Les questions sont courtes et variées, sur le quotidien : vie de bureau, cuisine, sport, informatique, voyages, administratif, bricolage.
Les réponses sont TOUJOURS en parler chevaleresque stylisé (« point ne », « céans », « moult », « gente dame », « messire », « par ma foi »...), utiles et exactes sur le fond, en 2 à 4 phrases, avec des formules d'ouverture variées.
Aucun texte hors du fichier.
MARKDOWN

Placez le fichier à la racine du projet, et prenez le temps de relire, mais pour aller plus vite prenez-le sur mon GitHub 😁.

Découper le dataset

mlx_lm.lora attend un dossier contenant train.jsonl pour l'entraînement et valid.jsonl pour la validation. Créez le dossier data puis le fichier decouper.py :

import json
import random

random.seed(117)

with open("brut.jsonl", encoding="utf-8") as f:
    exemples = [json.loads(ligne) for ligne in f if ligne.strip()]

random.shuffle(exemples)

for chemin, sous_ensemble in {
    "data/train.jsonl": exemples[:-30],
    "data/valid.jsonl": exemples[-30:],
}.items():
    with open(chemin, "w", encoding="utf-8") as f:
        for exemple in sous_ensemble:
            f.write(json.dumps(exemple, ensure_ascii=False) + "\n")
    print(f"{chemin} : {len(sous_ensemble)} exemples")
PYTHON

On ne donne pas le fichier complet directement, car on n'aurait aucun moyen de savoir si le modèle apprend vraiment. Le but est de savoir comment il se débrouille sur des exemples qu'il n'a jamais croisés.

Pendant l'entraînement, vous verrez une valeur appelée loss. Il s'agit d'une note d'erreur, l'écart entre ce que le modèle a répondu et ce qu'il aurait dû répondre. Le but est que le loss diminue.

Le modèle est entraîné sur le train.jsonl, et mlx-lm teste le modèle sur les exemples valid.jsonl. Le modèle ne doit pas apprendre par cœur, il doit généraliser.

Le mélange avec shuffle est important, car le fichier est organisé par thème. random.seed(117) permet d'obtenir les mêmes fichiers sur différentes machines, ce qui est pratique quand on veut comparer deux entraînements.

Lancez maintenant le script de découpage :

uv run python decouper.py
SHELL

L'entraînement

Je fais tout au fur et à mesure de ce guide, donc je vais lancer mon premier entraînement. Alors je ne vais pas faire le fou, je préfère rester prudent sur mon M1 8 Go 😂. Je vais d'abord faire un essai de 30 itérations :

uv run mlx_lm.lora \
  --model mlx-community/Ministral-3-3B-Instruct-2512-4bit \
  --train \
  --data ./data \
  --batch-size 1 \
  --num-layers 4 \
  --iters 30
SHELL
  • --batch-size 1 : un exemple à la fois

  • --num-layers 4 : on n'entraîne des adaptateurs LoRA que sur les 4 dernières couches du modèle

  • --iters 30 : le nombre d'itérations. Avec --batch-size 1, un passage vaut un exemple traité

Avec 30 itérations

Avec 30 itérations

Un pic de mémoire à 3,4 Go, une loss de validation qui passe de 2,882 à 0,371 en 30 passes. J'ai rapidement testé, le résultat est intéressant, les réponses sont courtes comme dans mon dataset, mais pas de trace de chevalerie 😂. Le format me paraît pas mal, mais le style n'est pas là.

Je passe à l'entraînement complet :

time uv run mlx_lm.lora \
  --model mlx-community/Ministral-3-3B-Instruct-2512-4bit \
  --train \
  --data ./data \
  --batch-size 1 \
  --num-layers 8 \
  --iters 300
SHELL
Après l'entraînement complet

Après l'entraînement complet

C'est passé pour une durée de 35 minutes environ. Le pic de mémoire est raisonnable, mon Mac n'a pas trop souffert (il a juste eu un peu chaud...). Nous sommes passés d'un loss de validation de 2,882 à 0,306.

Quand on regarde le gain au fil des itérations, le gain après 200 paraît minime.

À noter

Toutes les 100 itérations, on remarque que l'adaptateur est sauvegardé.

Les résultats

Je vais maintenant poser une question à notre modèle d'origine (le texte risque d'être coupé car je limite en tokens) :

uv run mlx_lm.generate --model mlx-community/Ministral-3-3B-Instruct-2512-4bit --prompt "Comment relancer un client ?" --max-tokens 400
SHELL
# Réponse : 
Relancer un client de manière efficace et professionnelle nécessite une approche structurée, respectueuse et adaptée à votre relation avec lui. Voici une méthode étape par étape, ainsi que des conseils pour personnaliser votre relance :

---

### **1. Préparer la relance**
- **Contexte** : Pourquoi relancez-vous ? (Ex. : pas de réponse après un devis, besoin de renouveler un contrat, promotion, etc.)
- **Objectifs** : Clarifiez ce que vous voulez obtenir (ex. : un rendez-vous, un retour sur un produit/service, une augmentation de parts de marché).
- **Ton** : Adaptez votre message au profil du client (ex. : formel pour un B2B, chaleureux pour un B2C).

---

### **2. Choisir le canal de relance**
- **Email** : Le plus polyvalent. Utilisez un sujet clair (ex. : *"Suivi : Votre projet X – Date limite le [date]"*).
- **Téléphone** : Pour un contact direct. Préparez un script court et naturel.
- **Message personnalisé** : Sur les réseaux sociaux ou plateformes (ex. : LinkedIn pour un B2B).
- **Poste physique** : Pour un client très fidèle (ex. : carte de visite, flyer).

---

### **3. Exemple de relance par email (formel et efficace)**
**Objet** : *"Relance pour votre projet [préciser] – Dernière chance pour [objectif]"* (ex. : *"Relance : Votre devis pour [service] – Date limite le [date]"*).

**Corps du message** :
> Bonjour [Nom du client],
>
> Je me permets de vous relancer concernant votre projet [préciser : ex. *"le devis pour [service] ou l’ach
MARKDOWN

Puis à notre modèle fine-tuné (toujours avec une limite de tokens) :

uv run mlx_lm.generate --model mlx-community/Ministral-3-3B-Instruct-2512-4bit --adapter-path ./adapters --prompt "Comment relancer un client ?" --max-tokens 400
SHELL
# Réponse : 
Oyez, parlez de relance, gardez les propos en deux ou trois messages, et proposez toujours la réponse par écrit. Le client se souviendra mieux de ce qu'il a écrit que de ce que vous dites. Nommez chaque point et demandez au client de répondre par écrit. Le client se sentira écouté et son temps sera mieux utilisé.
MARKDOWN

La réponse est directe et chevaleresque !

L'exercice ne sera que plus intéressant sur des machines avec de grosses capacités, mais avec mes 8 Go, mission réussie !

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.