On entraîne un LLM avec peu de VRAM en réduisant ce qui reste en mémoire, ce qui circule pendant le calcul, ou les deux. QLoRA, DoRA, GaLore et FSDP avec ZeRO-3 ne règlent pas le même problème. Le vrai sujet, c’est de choisir le bon compromis.
Où part vraiment la mémoire ?
La mémoire part surtout dans cinq endroits : les poids du modèle, les gradients, les états d’optimiseur, les activations et les buffers temporaires. C’est là que la VRAM disparaît, pas dans un endroit magique.

La mémoire statique, c’est ce qui reste en place longtemps. Les poids du modèle, par exemple. Si vous chargez un LLM en GPU, ils occupent déjà un gros bloc. La mémoire transitoire, elle, arrive pendant le calcul. Les activations du forward, les gradients au backward, les buffers CUDA utilisés pour accélérer certaines opérations. C’est pour ça qu’un modèle peut se charger correctement, puis planter dès le backward. Le backward, c’est la passe qui calcule les gradients pour apprendre. Et là, PyTorch doit garder ou reconstruire plein d’informations intermédiaires.
Il faut aussi distinguer deux problèmes différents. Parfois, le goulot d’étranglement, c’est le calcul. La carte passe son temps à multiplier des matrices. Parfois, c’est la bande passante mémoire. La carte attend que les données arrivent depuis la VRAM, la RAM CPU ou le disque. Certaines méthodes économisent de la VRAM, mais ajoutent du calcul. D’autres déplacent le problème vers la RAM CPU ou le bus PCIe, le lien entre CPU et GPU. J’ai vu ça chez un client avec une petite carte NVIDIA : le fine-tuning passait avec des adaptateurs, mais dès qu’on offloadait trop vers le CPU, l’entraînement devenait lent à cause des allers-retours CPU GPU.
Ce petit diagnostic PyTorch aide à voir où la VRAM monte. Je le mets souvent avant de changer l’architecture, parce que ça évite de deviner.
import torch
def show_mem(label):
# Convertit les octets en Go pour lire vite.
allocated = torch.cuda.memory_allocated() / 1024**3
reserved = torch.cuda.memory_reserved() / 1024**3
peak = torch.cuda.max_memory_allocated() / 1024**3
print(f"{label}")
print(f" Allouée : {allocated:.2f} Go")
print(f" Réservée : {reserved:.2f} Go")
print(f" Pic : {peak:.2f} Go")
torch.cuda.reset_peak_memory_stats()
show_mem("Avant chargement")
# Chargez ici votre modèle et envoyez-le sur le GPU.
# model = ...
# model.to("cuda")
show_mem("Après chargement du modèle")
# Lancez ici votre forward.
# outputs = model(**batch)
# loss = outputs.loss
show_mem("Après forward")
# Lancez ici votre backward.
# loss.backward()
show_mem("Après backward")La différence entre mémoire allouée et réservée surprend souvent. Allouée veut dire vraiment utilisée par les tenseurs. Réservée veut dire gardée par PyTorch pour éviter de redemander sans arrêt de la mémoire au GPU.
| Composant mémoire | Pourquoi ça coûte cher | Méthode qui aide le plus |
| Poids du modèle | Ils doivent être présents pour faire tourner le LLM. | Quantization, QLoRA, chargement 4-bit. |
| Gradients | Ils apparaissent pendant le backward pour apprendre. | LoRA, DoRA, gradient checkpointing. |
| États d’optimiseur | Adam garde souvent plusieurs copies statistiques des paramètres. | GaLore, optimizers 8-bit, ZeRO. |
| Activations | Elles sont gardées pour recalculer les gradients. | Gradient checkpointing, séquences plus courtes. |
| Buffers temporaires | CUDA réserve de l’espace pour accélérer les calculs. | Batch plus petit, kernels optimisés, Flash Attention. |
La suite découle directement de ça. QLoRA et DoRA attaquent surtout les poids entraînables. GaLore réduit la pression des états d’optimiseur. FSDP et ZeRO-3 répartissent la mémoire globale entre plusieurs GPU ou plusieurs zones mémoire.
Quand utiliser QLoRA ou DoRA ?
J’utilise QLoRA ou DoRA quand je veux fine-tuner un grand modèle sans entraîner tous ses poids en pleine précision. Le modèle de base reste figé, souvent quantifié en 4 bits, puis déquantifié dynamiquement en BF16 pendant les calculs. Les adaptateurs de bas rang portent l’apprentissage. C’est ça qui fait chuter la VRAM nécessaire.

QLoRA, c’est le choix simple et solide. On prend un modèle quantifié en 4 bits, souvent avec NF4, un format pensé pour mieux représenter les poids des réseaux neuronaux. On ajoute la double quantification, qui compresse aussi certains paramètres de quantification. Puis les calculs se font en BF16, un format 16 bits plus stable que le FP16 dans pas mal de cas. Pas besoin de faire une démo mathématique, l’idée est simple : je garde le gros modèle compact, et j’entraîne seulement de petits adaptateurs LoRA.
DoRA va un cran plus loin. Au lieu d’adapter les poids comme LoRA classique, il sépare mieux la direction et la magnitude des poids. En clair, il contrôle à la fois “dans quelle direction j’ajuste” et “avec quelle intensité”. Ça donne souvent une meilleure adaptation, surtout quand la tâche est fine, mais c’est un peu plus complexe à régler et à déployer.
Dans beaucoup de projets business que je vois, le sujet n’est pas d’obtenir le meilleur score académique. C’est de livrer un modèle spécialisé qui tourne sur une machine réaliste. QLoRA est souvent le premier test que je lancerais avant de sortir l’artillerie distribuée.
Ce code sert à charger un modèle causal en 4 bits, préparer l’entraînement k-bit, ajouter des adaptateurs LoRA, puis vérifier combien de paramètres seront réellement entraînés.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model, TaskType
model_id = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto"
)
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules="all-linear",
bias="none"
# Pour tester DoRA avec PEFT récent :
# use_dora=True
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()Les limites existent. La déquantification ajoute du calcul. La qualité dépend beaucoup du rang LoRA, des modules ciblés, et du dataset. La fusion des adaptateurs ou le déploiement peuvent aussi varier selon le runtime utilisé.
| Approche | VRAM | Qualité attendue | Complexité | Cas d’usage |
| QLoRA | Très basse | Très bonne | Faible à moyenne | Premier fine-tuning réaliste sur GPU limité |
| DoRA | Basse à moyenne | Souvent meilleure | Moyenne | Adaptation plus fine quand QLoRA plafonne |
Pourquoi GaLore réduit l’optimiseur ?
GaLore réduit surtout la mémoire des états d’optimiseur en projetant les gradients dans un sous-espace de rang réduit. Contrairement aux adaptateurs, l’idée reste de travailler sur l’ensemble des paramètres, mais en compressant la représentation utilisée pour la mise à jour.

Le vrai sujet, c’est Adam. Avec Adam, on ne stocke pas seulement les poids du modèle. On stocke aussi des états d’optimiseur, notamment deux moments par paramètre. Dit simplement, Adam garde une mémoire de la direction moyenne du gradient et de sa variance. Sur un gros LLM, ça peut peser très lourd. Parfois plus lourd que ce qu’on imaginait au départ. J’ai déjà vu des entraînements bloquer non pas à cause du modèle lui-même, mais à cause de ces états invisibles dans le raisonnement initial.
GaLore, pour Gradient Low-Rank Projection, attaque ce point précis. Périodiquement, on estime une projection bas rang des gradients. Le rang, ici, c’est la taille du sous-espace dans lequel on accepte de représenter l’information utile. On fait ensuite la mise à jour dans cet espace réduit, ce qui limite la mémoire nécessaire pour les états de l’optimiseur.
Ce pseudo-code montre où se place l’idée dans une boucle PyTorch classique. La fonction de projection est volontairement conceptuelle. Ce n’est pas une API PyTorch officielle, ni une promesse sur une librairie précise.
for batch in dataloader:
optimizer.zero_grad()
outputs = model(
input_ids=batch["input_ids"],
attention_mask=batch["attention_mask"]
)
loss = outputs.loss
loss.backward()
# Étape conceptuelle GaLore.
# On projette certains gradients dans un sous-espace bas rang.
# Cette fonction représente l'idée, pas une API universelle.
if step % projection_update_frequency == 0:
update_low_rank_projection(model, rank=galore_rank)
apply_low_rank_gradient_projection(model)
optimizer.step()
step += 1Il existe des implémentations open source de GaLore, mais je les traiterais comme une option à vérifier selon votre stack, votre version de PyTorch, votre modèle et votre matériel. Pas comme une brique magique qu’on branche partout sans test.
Les points de vigilance sont assez concrets :
- Le rang choisi peut trop compresser l’information si vous descendez trop bas.
- La fréquence de mise à jour de la projection joue sur la stabilité et le coût.
- Les projections peuvent créer des pics de latence, surtout sur gros modèles.
- Les hyperparamètres restent sensibles, notamment le learning rate et le scheduler.
GaLore garde une logique de fine-tuning plus globale qu’un simple adaptateur, puisqu’on ne se limite pas à quelques petits modules ajoutés au modèle. Mais si la mémoire reste trop haute malgré ça, il faut passer à une autre famille de solutions. FSDP, pour Fully Sharded Data Parallel, répartit les paramètres et états entre GPU. ZeRO-3 fait pareil côté DeepSpeed, en partitionnant paramètres, gradients et états d’optimiseur. Là, on ne réduit plus seulement l’optimiseur, on répartit ou on déporte ce qui ne rentre plus.
Quand passer à FSDP et ZeRO-3 ?
Je passe à FSDP ou ZeRO-3 quand une seule carte ne peut plus garder les paramètres, gradients et états d’optimiseur en VRAM, même avec quantification ou réduction d’optimiseur. Là, on ne compresse plus seulement, on fragmente et on charge à la demande.

FSDP, côté PyTorch, veut dire Fully Sharded Data Parallel. ZeRO-3, côté DeepSpeed, fait une idée très proche. Les paramètres du modèle, les gradients et les états d’optimiseur sont découpés entre plusieurs GPU. On appelle ça du sharding. Chaque carte ne garde qu’un morceau. Quand une couche doit calculer, les morceaux nécessaires sont reconstruits temporairement, puis relâchés.
On peut aussi offloader vers la RAM CPU. Ça aide quand la VRAM est trop courte, mais ça se paie. Le passage GPU vers CPU passe souvent par PCIe, et PCIe est beaucoup plus lent que la VRAM. Sur un serveur NVLink, ça peut tourner correctement. Sur une machine PCIe classique, ça peut devenir le goulot d’étranglement. Je mesure toujours avant de conclure, parce que deux configs identiques sur le papier peuvent avoir un comportement très différent.
Ce squelette FSDP montre les trois réglages importants : découpage automatique des blocs Transformer, calcul en BF16, et offload CPU optionnel quand la VRAM ne suffit plus.
import os
import torch
from functools import partial
from transformers import AutoModelForCausalLM
from transformers.models.llama.modeling_llama import LlamaDecoderLayer
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import MixedPrecision, CPUOffload
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
torch.distributed.init_process_group("nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
torch_dtype=torch.bfloat16
)
wrap_policy = partial(
transformer_auto_wrap_policy,
transformer_layer_cls={LlamaDecoderLayer}
)
mixed_precision = MixedPrecision(
param_dtype=torch.bfloat16,
reduce_dtype=torch.bfloat16,
buffer_dtype=torch.bfloat16
)
cpu_offload = CPUOffload(offload_params=True)
model = FSDP(
model.to(local_rank),
auto_wrap_policy=wrap_policy,
mixed_precision=mixed_precision,
cpu_offload=cpu_offload,
device_id=local_rank
)Côté DeepSpeed, le fichier JSON fait la même promesse avec ZeRO stage 3. C’est lisible, mais pas magique : si l’offload CPU sature PCIe, l’entraînement ralentit.
{
"train_micro_batch_size_per_gpu": 1,
"gradient_accumulation_steps": 16,
"bf16": {
"enabled": true
},
"zero_optimization": {
"stage": 3,
"contiguous_gradients": true,
"overlap_comm": true,
"offload_param": {
"device": "cpu",
"pin_memory": true
},
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
}
}
}| Approche | VRAM | Vitesse | Complexité | Cas où je l’utiliserais |
| Mono GPU | Élevée | Très bonne | Faible | Petit modèle ou grosse carte |
| QLoRA | Très basse | Bonne | Moyenne | Fine-tuning efficace sans tout entraîner |
| GaLore | Basse | Bonne à moyenne | Moyenne | Réduire les états d’optimiseur |
| FSDP ou ZeRO-3 | Très basse par GPU | Variable | Élevée | Quand le modèle ne rentre plus autrement |
Quelle méthode choisir en pratique ?
Je choisis d’abord la méthode qui supprime le vrai goulot d’étranglement, pas celle qui a l’air la plus moderne. Si la VRAM explose à cause des poids du modèle, je pars sur QLoRA ou DoRA. Si c’est l’optimiseur qui prend trop de place, je regarde GaLore. Si tout dépasse, poids, gradients, états d’optimiseur, activations, là je passe sur FSDP ou ZeRO-3 avec offloading, mais avec prudence.
Pour un fine-tuning rapide sur une seule GPU limitée, mon choix par défaut, c’est QLoRA. On quantifie le modèle en 4 bits, puis on entraîne seulement de petits adaptateurs LoRA. C’est souvent le meilleur ratio simplicité, coût, résultat. Si la qualité bloque, je teste DoRA. C’est une variante qui sépare mieux la direction et l’amplitude des poids, donc parfois ça récupère un peu de finesse sans exploser la mémoire.
Si je veux un entraînement plus global, pas juste des adaptateurs, et que les états d’optimiseur prennent trop de VRAM, je regarde GaLore. L’idée est de compresser les gradients dans un sous-espace plus petit. C’est intéressant, mais je le garde pour les cas où QLoRA n’est pas suffisant.
Pour entraîner au-delà de la capacité réelle de la machine, FSDP ou ZeRO-3 peuvent sauver le projet. Ces méthodes découpent les poids, gradients et états d’optimiseur entre GPU, et peuvent décharger une partie sur la RAM CPU ou le disque. Ça marche, mais ce n’est pas magique. Si le bus PCIe est lent, l’offloading peut transformer l’entraînement en escargot.
Dans les missions IA que je vois, on perd souvent du temps à optimiser trop tôt. Le bon réflexe, c’est de mesurer la mémoire, faire un baseline court, puis changer un seul levier à la fois. Sinon on ne sait plus si le gain vient de la quantification, du batch size, du gradient accumulation ou de l’offloading.
Ma checklist avant de choisir :
- Taille du modèle, par exemple 7B, 13B, 34B ou plus.
- VRAM disponible sur une ou plusieurs GPU.
- RAM CPU disponible, surtout si offloading prévu.
- Type de bus, PCIe ou NVLink, parce que les transferts comptent beaucoup.
- Précision BF16 possible, utile pour stabiliser l’entraînement.
- Objectif du fine-tuning, adaptation légère ou vrai entraînement profond.
- Contrainte de déploiement, modèle quantifié accepté ou non.
- Temps acceptable, parce qu’économiser la VRAM coûte souvent du temps.
| Situation | Choix pratique |
| Une seule GPU limitée, fine-tuning rapide | QLoRA en premier |
| QLoRA marche mais la qualité plafonne | Tester DoRA |
| Optimiseur trop lourd, entraînement plus global | Regarder GaLore |
| Modèle trop gros pour la machine | FSDP ou ZeRO-3 avec offloading |
Alors on optimise quoi en premier ?
Je commencerais toujours par mesurer avant de choisir une méthode. QLoRA et DoRA sont très efficaces quand on veut spécialiser un LLM avec peu de VRAM sans toucher au modèle complet. GaLore devient intéressant quand les états d’optimiseur prennent trop de place. FSDP et ZeRO-3 avec offloading permettent d’aller plus loin, mais on paie en communication, surtout via PCIe. Le bon choix dépend du vrai goulot d’étranglement, pas de la mode du moment. Si vous avancez dans cet ordre, vous gagnez du temps, vous évitez les configurations fragiles, et vous entraînez des modèles utiles sur du matériel réaliste.
FAQ
- Peut-on vraiment fine-tuner un LLM avec une seule GPU ?
Oui, si le modèle reste raisonnable et si on utilise une approche comme QLoRA ou DoRA. On ne réentraîne pas tout le modèle en pleine précision. On fige les poids de base, on les compresse souvent en 4 bits, et on entraîne seulement des adaptateurs. - Quelle est la différence entre QLoRA et DoRA ?
QLoRA combine quantification 4 bits et adaptateurs LoRA pour économiser beaucoup de VRAM. DoRA reprend l’idée des adaptateurs, mais sépare mieux certains aspects des poids, notamment direction et magnitude. En pratique, DoRA peut améliorer la qualité, mais il ajoute aussi un peu de complexité. - GaLore remplace-t-il LoRA ?
Pas vraiment. LoRA et QLoRA réduisent surtout ce qu’on entraîne en ajoutant des adaptateurs. GaLore vise plutôt la mémoire consommée par les gradients et les états d’optimiseur grâce à une projection de faible rang. Les deux répondent à des problèmes proches, mais pas identiques. - Pourquoi FSDP ou ZeRO-3 peuvent ralentir l’entraînement ?
Parce qu’ils fragmentent et reconstruisent les paramètres, gradients et états d’optimiseur à la demande. Si une partie est offloadée en RAM CPU, les transferts passent souvent par PCIe. Ça permet de tenir en mémoire, mais la communication peut devenir le nouveau goulot d’étranglement. - Quelle méthode tester en premier avec peu de VRAM ?
Je testerais QLoRA en premier dans la majorité des cas. C’est souvent le meilleur compromis pour un fine-tuning rapide sur matériel limité. Si la qualité ne suffit pas, je regarderais DoRA. Si la mémoire d’optimiseur bloque, GaLore. Si le modèle dépasse encore, FSDP ou ZeRO-3.
A propos de l’auteur
Je suis Franck Scandolera, expert et formateur en tracking avancé server-side, Analytics Engineering, automatisation No/Low Code avec n8n, intégration de l’IA en entreprise et SEO/GEO. J’accompagne des équipes qui veulent passer de l’idée IA au système exploitable, avec des contraintes réelles de données, de coûts et d’infrastructure. J’ai travaillé avec des références comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Je dirige l’agence webAnalyste et l’organisme Formations Analytics. Si vous voulez cadrer ou industrialiser vos projets IA, contactez-moi.
⭐ Analytics engineer, Data Analyst et Automatisation IA indépendant ⭐
- Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
- Data Analyst & Analytics engineering : tracking avancé (GTM server, e-commerce, CAPI, RGPD), entrepôt de données (BigQuery, Snowflake, PostgreSQL, ClickHouse), modèles (Airflow, dbt, Dataform), dashboards décisionnels (Looker, Power BI, Metabase, SQL, Python).
- Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
- Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.






