À 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
Puis dans le dossier du projet :
uv add mlx-lm
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-3Best un petit modèle qui passe sur ma configuration -
Instructcar il sait déjà dialoguer, on va juste changer sa façon de parler -
Le modèle est quantizé (
4bit) -
mlx-communityest 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
-
mlx_lm.generatecharge 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
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.
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")
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
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
-
--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
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
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
# 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
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
# 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é.
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 !