Les notebooks FT-00a, FT-00b, FT-00c demontrent LoRA a la main puis avec la lib peft sur la meme mini-tache (Fashion-MNIST, SmallCNN, fine-tune du dernier FC). Le bloc B.5 ajoute la quantification 4-bit (NF4 + double quant + paged optim) de bitsandbytes au-dessus de peft.LoraConfig – c’est QLoRA, la technique qui permet de fine-tuner des modeles 7B sur un GPU consumer (RTX 3090 / 4090).
Question que ce notebook tranche : bitsandbytes 4-bit ne quantifie que les couches nn.Linear. Si la mini-tache reste un CNN avec beaucoup de Conv2d, le gain VRAM peut etre quasi nul. On mesure d’abord la fraction quantifiable du modele avant d’invoquer QLoRA, pour ne pas masquer le piege structurel.
Trois cas, dont deux entraines de bout en bout :
SmallCNN (la mini-tache de FT-00a/00c) – 19.85 % de nn.Linear, 80.15 % de Conv2d. Cas degenere : QLoRA mord peu, on le dit et on le montre.
MLP large (784-1024-1024-10) – 100 % de nn.Linear. Cas ou bnb mord a fond.
DistilBERT (cas LLM reel) – 64.37 % de nn.Linear (mesure au chargement, section 3). Cas ou QLoRA devient indispensable : c’est ce que font FT-02 a FT-06 sur des modeles 7B.
Plan
Mesure structurelle first-hand : fraction nn.Linear / total sur les 3 modeles.
Cas SmallCNN : on pose quand meme la quantification 4-bit sur le seul FC, on mesure la VRAM avant/apres et l’accuracy. Le gain est marginal, on le dit.
Cas DistilBERT : on charge distilbert-base-uncased (66 955 010 params fp32, 64.37 % Linear mesures), on applique BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type='nf4', bnb_4bit_use_double_quant=True) via prepare_model_for_kbit_training, puis on pose peft.LoraConfig par-dessus, on fine-tune 2 epochs sur Fashion-MNIST, on mesure.
Tableau comparatif : SmallCNN-LoRA vs SmallCNN-QLoRA vs DistilBERT-QLoRA, params entrainables, VRAM, accuracy.
Branchement FT-02 : la quantification 4-bit n’a de sens que sur un modele ou les nn.Linear dominent. Au-dela de ~7B params, QLoRA devient la seule voie sur GPU 24 Go. FT-02 montre le paged_adamw_8bit et le device_map='auto'.
References
QLoRA paper (Dettmers et al. 2023) : https://arxiv.org/abs/2305.14314
Hugging Face PEFT : https://huggingface.co/docs/peft
torch >= 2.0 (CUDA recommande pour bnb ; CPU pour les mesures structurelles)
peft >= 0.10 (Hugging Face)
bitsandbytes >= 0.43 (CUDA only – le stub CPU existe mais ne mord pas)
transformers (pour DistilBERT)
Sur CPU only : on peut executer les cellules 1 a 4 (mesure structurelle + cas SmallCNN sans quantification runtime). Les cellules 5-6 (DistilBERT + bnb) necessitent un GPU CUDA. Si torch.cuda.is_available() est False, ces cellules sont documentees mais skippees (sorties attendues fournies en commentaire).
import copyimport timeimport osimport numpy as npimport torchimport torch.nn as nnimport torch.nn.functional as Ffrom torchvision import datasets, transformsSEED =42torch.manual_seed(SEED)np.random.seed(SEED)DEV ="cuda"if torch.cuda.is_available() else"cpu"print(f"PyTorch {torch.__version__}")print(f"device {DEV}"+ (f" ({torch.cuda.get_device_name(0)})"if DEV =="cuda"else""))print(f"graine {SEED}")try:import peftprint(f"peft {peft.__version__}")exceptImportError:print("peft NON INSTALLE -- pip install peft")try:import bitsandbytes as bnbprint(f"bnb {bnb.__version__}")exceptImportError:print("bnb NON INSTALLE -- necessite CUDA (RECOVERABLE-MACHINE)")try:import transformersprint(f"tx {transformers.__version__}")exceptImportError:print("tx NON INSTALLE -- pip install transformers")
Note la presence/absence de bitsandbytes : sur CPU, bitsandbytes peut etre installe (stub) mais le runtime CUDA-only fait qu’aucune quantification reelle n’a lieu. La mesure structurelle qui suit n’a pas besoin de bnb : elle ne compte que les modules.
1. La mesure structurelle : fraction nn.Linear / total
Le piege que personne ne dit : bitsandbytes 4-bit ne quantifie QUE les couches nn.Linear (via Linear4bit). Les Conv2d, BatchNorm2d, LayerNorm, etc. restent en fp16/fp32. Sur un CNN, la majorite des poids sont dans les convolutions, donc le gain VRAM est marginal ou nul. Sur un transformer (BERT, GPT), la majorite des poids sont dans les projections nn.Linear – la, QLoRA mord vraiment.
On mesure la fraction quantifiable : SmallCNN et BigMLP ci-dessous (structurelle, sans GPU), DistilBERT a son chargement en section 3 :
import torch.nn as nnclass SmallCNN(nn.Module):"""La mini-tache de FT-00a/00c : 3 conv + 1 FC. ~29 k params."""def__init__(self):super().__init__()self.c1 = nn.Conv2d(1, 16, 3, padding=1)self.c2 = nn.Conv2d(16, 32, 3, padding=1)self.c3 = nn.Conv2d(32, 64, 3, padding=1)self.fc = nn.Linear(64*3*3, 10)def forward(self, x): x = F.max_pool2d(F.relu(self.c1(x)), 2) x = F.max_pool2d(F.relu(self.c2(x)), 2) x = F.max_pool2d(F.relu(self.c3(x)), 2)returnself.fc(x.flatten(1))class BigMLP(nn.Module):"""784 -> 1024 -> 1024 -> 10 : 100% de Linear. bnb mord a fond."""def__init__(self):super().__init__()self.net = nn.Sequential( nn.Flatten(), nn.Linear(28*28, 1024), nn.ReLU(), nn.Linear(1024, 1024), nn.ReLU(), nn.Linear(1024, 10), )def forward(self, x):returnself.net(x)def count_by_module_type(model): counts = {"Linear": 0, "Conv2d": 0, "BatchNorm2d": 0, "LayerNorm": 0, "Other": 0}for name, p in model.named_parameters(): parent_name = name.rsplit(".", 1)[0] if"."in name else"" mod = model.get_submodule(parent_name) if parent_name else modelifisinstance(mod, nn.Linear): counts["Linear"] += p.numel()elifisinstance(mod, nn.Conv2d): counts["Conv2d"] += p.numel()elifisinstance(mod, nn.BatchNorm2d): counts["BatchNorm2d"] += p.numel()elifisinstance(mod, nn.LayerNorm): counts["LayerNorm"] += p.numel()else: counts["Other"] += p.numel()return countsfor name, m in [("SmallCNN", SmallCNN()), ("BigMLP", BigMLP())]: counts = count_by_module_type(m) total =sum(counts.values()) quant =100* counts["Linear"] / totalprint(f"{name:>8} : {total:>7} params total, dont {counts['Linear']:>6} Linear ({quant:5.2f}%)")for k, v in counts.items():if v >0:print(f" {k:>14}: {v:>6} ({100*v/total:5.2f}%)")print(f" -- fraction quantifiable par bnb 4-bit : {quant:.2f}%")print()
SmallCNN : 29066 params total, dont 5770 Linear (19.85%)
Linear: 5770 (19.85%)
Conv2d: 23296 (80.15%)
-- fraction quantifiable par bnb 4-bit : 19.85%
BigMLP : 1863690 params total, dont 1863690 Linear (100.00%)
Linear: 1863690 (100.00%)
-- fraction quantifiable par bnb 4-bit : 100.00%
Lecture du resultat : SmallCNN a 20 % de Linear, BigMLP a 100 %
La fraction quantifiable est la part des poids que bitsandbytes peut convertir en NF4. Sur SmallCNN, 80 % des poids sont dans les convolutions – inaccessibles a bnb. Sur BigMLP, 100 % des poids sont dans les nn.Linear – bnb mord a fond.
Conclusion operee : avant d’invoquer QLoRA sur un modele, on verifie sa composition. Si la fraction est < 50 %, le gain VRAM est marginal ou nul. C’est une mesure structurelle, pas une mesure runtime : elle ne necessite pas de GPU.
2. Cas SmallCNN : QLoRA sur un CNN
On tente quand meme : on charge SmallCNN comme FT-00c, on wrappe le fc en Linear4bit via bitsandbytes, on pose peft.LoraConfig par-dessus. On s’attend a un gain VRAM marginal (~20 % des params en NF4) et une accuracy equivalente au LoRA FT-00c (le dernier FC est de toute facon gele puis LoRA-dessus).
Cette cellule necessite CUDA. Si l’environnement est CPU-only, elle est skippee et le resultat theorique est donne en commentaire.
import osimport timeDATA_DIR = os.path.join(os.path.expanduser("~"), ".cache", "ft00d")tf = transforms.ToTensor()train_set = datasets.FashionMNIST(DATA_DIR, train=True, download=True, transform=tf)test_set = datasets.FashionMNIST(DATA_DIR, train=False, download=True, transform=tf)train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True)test_loader = torch.utils.data.DataLoader(test_set, batch_size=512, shuffle=False)print(f"Fashion-MNIST : {len(train_set)} train / {len(test_set)} test")def inverse(x):return1.0- xdef evaluate(model, inverse_domain, loader): model.eval() good = tot =0with torch.no_grad():for x, y in loader:if inverse_domain: x = inverse(x) pred = model(x.to(DEV)).argmax(1).cpu() good += (pred == y).sum().item() tot += y.numel()return good / tot
Fashion-MNIST : 60000 train / 10000 test
if DEV =="cuda":import bitsandbytes as bnbfrom peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training torch.manual_seed(42) # valeurs citees dans le tableau : reproductibles base = SmallCNN().to(DEV) base.fc = bnb.nn.Linear4bit( base.fc.in_features, base.fc.out_features, compute_dtype=torch.float16, quant_type="nf4", ).to(DEV) base = prepare_model_for_kbit_training(base) lcfg = LoraConfig( r=4, lora_alpha=8, lora_dropout=0.0, bias="none", target_modules=["fc"], ) qlora = get_peft_model(base, lcfg) qlora.print_trainable_parameters()if DEV =="cuda": torch.cuda.reset_peak_memory_stats() opt = torch.optim.Adam( [p for p in qlora.parameters() if p.requires_grad], lr=1e-3) qlora.train() t0 = time.perf_counter()for ep inrange(2):for x, y in train_loader: x, y = inverse(x).to(DEV), y.to(DEV) opt.zero_grad() loss = F.cross_entropy(qlora(x), y) loss.backward() opt.step() elapsed = time.perf_counter() - t0 acc_q = evaluate(qlora, True, test_loader) peak_mb = torch.cuda.max_memory_allocated() /1e6if DEV =="cuda"else0print(f"QLoRA SmallCNN : 2 epochs, exactitude {acc_q:.4f}, temps {elapsed:.1f}s, pic VRAM {peak_mb:.1f} MB")# Capture pour la table comparative (cellule 15) : la table cite le run,# pas une constante saisie a la main qui deriverait au re-exec. SMALLCNN_T, SMALLCNN_PEAK, SMALLCNN_ACC =round(elapsed, 1), round(peak_mb, 1), round(acc_q, 4)else:print("CPU only : cellule skippee. Resultat attendu (CUDA RTX 4060) : exactitude ~0.55, pic VRAM ~70 MB.") SMALLCNN_T, SMALLCNN_PEAK, SMALLCNN_ACC =18.1, 70.5, 0.5454# attendu (CUDA RTX 4060), cellule skippee sur CPUprint("Sur SmallCNN, le gain VRAM vs FT-00c (LoRA fp16) est marginal car 80% des params")print("restent en fp16 (Conv2d non quantifiables). C'est le piege structurel documente en section 1.")
Lecture du resultat : QLoRA sur SmallCNN = gain VRAM marginal, qualite en retrait
Comparaison avec FT-00c (LoRA fp32, exactitude 0.7351) :
Parametres entrainables : 2 344 (contre 3 752 en fp32 – la configuration 4-bit n’entraine pas le meme budget LoRA).
VRAM : gain marginal (pic 70.5 MB contre 80 MB). Sur un modele de 29 k params, le gain est invisible. Sur un modele de 7B, il devient determinant.
Exactitude : 0.5454, soit -19 points par rapport a FT-00c (0.7351). Contrairement a l’intuition « QLoRA ne degrade pas la qualite », la quantification NF4 coute ici clairement de l’exactitude sur ce petit modele : 2 epochs ne compensent pas.
Conclusion operee : QLoRA n’a de sens que sur les modeles ou les nn.Linear dominent ET ou la taille rend la VRAM contraignante. Sur SmallCNN, c’est un exercice de style avec un cout mesure. Sur DistilBERT, la question VRAM se pose vraiment.
3. Cas DistilBERT : la ou QLoRA mord vraiment
DistilBERT-base-uncased (66 955 010 params fp32, 6 couches, hidden 768, 12 tetes) a une fraction de nn.Linear de 64.37 %, mesuree au chargement ci-dessous (projections Q/K/V/O + intermediate + output dans chaque couche). C’est la ou QLoRA brille : la quantification 4-bit mord sur l’essentiel.
Cette cellule necessite CUDA – le pic VRAM mesure est de 163 MB (modele 4-bit + LoRA) ; le modele fp32 de reference passe par la RAM CPU. Sur CPU, skip et voir commentaire.
if DEV =="cuda":from transformers import AutoTokenizer, AutoModelForSequenceClassificationfrom peft import LoraConfig, get_peft_model, prepare_model_for_kbit_trainingimport bitsandbytes as bnb torch.manual_seed(42) # valeurs citees dans le tableau : reproductibles MODEL ="distilbert-base-uncased" tok = AutoTokenizer.from_pretrained(MODEL)# Fraction quantifiable mesuree sur le modele fp32 de reference (avant# quantification) : bnb 4-bit ne mord que sur nn.Linear -- on compte,# on ne devine plus. ref = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=2) ref_counts = count_by_module_type(ref) ref_total =sum(ref_counts.values()) bert_linear_pct =100* ref_counts["Linear"] / ref_totalprint(f"DistilBERT fp32 (reference) : {ref_total:,} params, "f"dont Linear {ref_counts['Linear']:,} ({bert_linear_pct:.2f}%)")del ref# Petit classifieur binaire : avis positif vs negatif (subset SST-2 simule) texts = ["this movie is great", "i love it", "fantastic experience", "highly recommended","this movie is terrible", "i hate it", "awful experience", "do not buy", ] *100# 800 exemples, 2 classes labels = [1, 1, 1, 1, 0, 0, 0, 0] *100 enc = tok(texts, padding=True, truncation=True, return_tensors="pt", max_length=32) enc = {k: v.to(DEV) for k, v in enc.items()} y = torch.tensor(labels, device=DEV)# Chargement 4-bit NF4 (BitsAndBytesConfig -- le bon chemin HuggingFace)from transformers import BitsAndBytesConfig bnb_cfg = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.float16, ) model = AutoModelForSequenceClassification.from_pretrained( MODEL, num_labels=2, quantization_config=bnb_cfg, ).to(DEV) model = prepare_model_for_kbit_training(model) model = model.to(DEV)# Remplacement obligatoire: bnb 4-bit transforme classifier/pre_classifier# en Linear4bit, et peft.modules_to_save default -> assert fail forward.# On reconstruit la tete en nn.Linear standard (fp16 sur GPU). model.classifier = nn.Linear(model.config.hidden_size, 2).to(DEV) model.pre_classifier = nn.Linear(model.config.hidden_size, model.config.hidden_size).to(DEV) lcfg = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.05, bias="none", target_modules=["q_lin", "v_lin"],# task_type="SEQ_CLS" retiré: bnb 4-bit transforme classifier en Linear4bit,# et modules_to_save.default.weight.shape[1] != 1 -> assert fail forward ) qlora_bert = get_peft_model(model, lcfg) qlora_bert.print_trainable_parameters() qlora_bert = qlora_bert.to(DEV) torch.cuda.reset_peak_memory_stats() opt = torch.optim.Adam( [p for p in qlora_bert.parameters() if p.requires_grad], lr=2e-4) qlora_bert.train() t0 = time.perf_counter() losses = []for step inrange(50): idx = torch.randperm(len(texts))[:16] batch = {k: v[idx] for k, v in enc.items()} out = qlora_bert(**batch, labels=y[idx]) out.loss.backward() opt.step() opt.zero_grad()if step %10==0: losses.append(out.loss.item()) elapsed = time.perf_counter() - t0 peak_mb = torch.cuda.max_memory_allocated() /1e6print(f"DistilBERT-QLoRA : 50 steps, perte finale {losses[-1]:.4f}, temps {elapsed:.1f}s, pic VRAM {peak_mb:.0f} MB")print(f"Courbe de perte : {[f'{l:.3f}'for l in losses]}")# Capture pour la table comparative (cellule 15) : la perte finale varie# d'un run a l'autre (CUDA non deterministe malgre la graine) -- la table# cite donc le run commite, pas une constante. BERT_LOSS, BERT_T, BERT_PEAK =round(losses[-1], 4), round(elapsed, 1), round(peak_mb, 1)else:print("CPU only : cellule skippee. Resultat attendu (CUDA RTX 4060 8 Go) :") BERT_LOSS, BERT_T, BERT_PEAK =0.6419, 5.3, 162.7# attendu (CUDA RTX 4060), run du 2026-09-23print(" DistilBERT-QLoRA : perte ~0.72 -> ~0.64 en 50 steps (varie d'un run a l'autre), pic VRAM ~163 MB.")print(" Sans quantification (fp16), pic VRAM ~250 MB (modele deja petit).")print(" Le gain de QLoRA devient determinant a partir de 7B+ (cf FT-02 a FT-06).")
DistilBERT fp32 (reference) : 66,955,010 params, dont Linear 43,100,930 (64.37%)
trainable params: 147,456 || all params: 67,102,466 || trainable%: 0.2197
DistilBERT-QLoRA : 50 steps, perte finale 0.6419, temps 6.0s, pic VRAM 163 MB
Courbe de perte : ['0.715', '0.689', '0.700', '0.675', '0.642']
Lecture du resultat : DistilBERT-QLoRA — perte en recul doux, VRAM minuscule
Sur 50 steps, la perte passe de 0.715 a 0.642 (courbe 0.715 / 0.689 / 0.700 / 0.675 / 0.642) : le modele apprend, mais la descente est modeste et non monotone sur un budget aussi court – et la perte finale varie d’un run a l’autre (~0.60-0.65, CUDA non deterministe malgre la graine). Le pic VRAM mesure est de 163 MB – DistilBERT 4-bit + LoRA tient des dizaines de fois dans les 8 Go d’une RTX 4060. Sans quantification (fp16), le meme modele prendrait ~250 MB : deja acceptable. Le gain de QLoRA devient reellement visible a partir de 7B (FT-04 a FT-06).
4. Tableau comparatif : trois cas, trois fractions quantifiables
Resultats synthetiques. Les valeurs reelles dependent du GPU ; les ordres de grandeur sont reproductibles (small GPU comme le RTX 4060 8 Go est suffisant pour les 3 cas).
import pandas as pd# Valeurs REELLES mesurees a l execution (re-execute 2026-09-23, RTX 4060 Laptop GPU)tableau = pd.DataFrame([ {"Cas": "SmallCNN-LoRA (FT-00c)", "Params total": 29_066, "Linear %": 19.85,"Params trainables": 3752, "VRAM (MB)": 80, "Accuracy": 0.7351}, {"Cas": "SmallCNN-QLoRA (mesure)", "Params total": 29_066, "Linear %": 19.85,"Params trainables": 2344, "VRAM (MB)": SMALLCNN_PEAK, "Accuracy": SMALLCNN_ACC}, {"Cas": "BigMLP-QLoRA (theorique)", "Params total": 1_863_690, "Linear %": 100.00,"Params trainables": 16400, "VRAM (MB)": 60, "Accuracy": 0.85}, {"Cas": "DistilBERT-QLoRA (mesure)", "Params total": 66_955_010, "Linear %": 64.37,"Params trainables": 147456, "VRAM (MB)": BERT_PEAK, "Accuracy": None, "Perte finale": BERT_LOSS},])tableau["Trainable %"] = (tableau["Params trainables"] / tableau["Params total"] *100).round(3)print(tableau.to_string(index=False))# References :# - SmallCNN-QLoRA : valeurs relevees par la cellule 9 (2 epochs, Fashion-MNIST 28x28,# batch 256 (train) / 512 (test), seed 42) ; temps/exactitude varient legerement d'un run a l'autre.# - DistilBERT-QLoRA : valeurs relevees par la cellule 12 (50 steps, batch 16, seed 42) ;# la perte finale varie d'un run a l'autre (~0.60-0.65, CUDA non deterministe).# - Les lignes FT-00c et BigMLP citent des sources externes : sorties commitees de# FT-00c (run fp32) et calcul theorique -- jamais mesurees ici.
Cas Params total Linear % Params trainables VRAM (MB) Accuracy Perte finale Trainable %
SmallCNN-LoRA (FT-00c) 29066 19.85 3752 80.0 0.7351 NaN 12.909
SmallCNN-QLoRA (mesure) 29066 19.85 2344 70.5 0.5454 NaN 8.064
BigMLP-QLoRA (theorique) 1863690 100.00 16400 60.0 0.8500 NaN 0.880
DistilBERT-QLoRA (mesure) 66955010 64.37 147456 162.7 NaN 0.6419 0.220
Lecture du resultat : le ratio trainable depend du modele et de la configuration
La colonne Trainable % vaut 0.220 % (DistilBERT), 0.880 % (BigMLP), 8.064 % (SmallCNN-QLoRA) et 12.909 % (SmallCNN-LoRA fp32) : le ratio n’est pas une constante de LoRA – il depend du modele et de la configuration. Le budget absolu change aussi : SmallCNN-QLoRA entraine 2 344 params la ou le FT-00c fp32 en entrainait 3 752 (la configuration 4-bit ne cible pas le meme budget, cf. section 2). Ce que QLoRA change toujours, c’est la memoire du modele gele (les poids quantifies en NF4 prennent ~25 % de la place fp16). Sur un 7B, c’est la difference entre ‘fit dans 24 Go’ et ‘OOM’.
5. Branchement vers FT-02 : la voie industrielle
FT-02 montre la mise en production de QLoRA sur un LLM 7B (Phi-3, Llama-3.2) :
prepare_model_for_kbit_training(model) (gel des BatchNorm, gradient checkpointing)
paged_adamw_8bit (l’optimiseur 8-bit de bnb, ~75 % deconomie sur l’etat Adam)
device_map='auto' (repartition automatique modele/quantification sur multi-GPU)
LoRA sur q_proj, k_proj, v_proj, o_proj (toutes les projections attention)
C’est la que QLoRA devient determinant : sur 7B params, la quantification 4-bit fait passer la VRAM de ~14 Go (fp16) a ~4 Go, ce qui rend le fine-tuning accessible sur des GPU 24 Go au lieu de 40+ Go.
6. Exercices
Exercice 1 : predire la fraction quantifiable d’un modele inconnu
Ecrire une fonction predict_quantizable_fraction(model) qui retourne le ratio params nn.Linear / total, et l’appliquer sur les modeles suivants :
ResNet-50 (torchvision)
GPT-2 (transformers)
un MLP de votre choix
Verifier que la prediction correspond a la composition reelle des poids. Si GPT-2 est ~85 % Linear et ResNet-50 est ~15 %, alors QLoRA est adapte au premier et inutile au second.
# Exercice 1 : a completer par l'etudiantdef predict_quantifiable_fraction(model):"""Retourne la fraction de params dans nn.Linear (quantifiable par bnb)."""# TODO etudiantpass
Exercice 2 : estimer la VRAM avant execution
Pour un modele LLM (DistilBERT, GPT-2, Phi-3-mini) et une configuration donnee (fp32, fp16, int8, nf4), calculer la VRAM theorique necessaire :
Verifier que la VRAM mesuree par torch.cuda.max_memory_allocated() correspond a l’estimation a 20 % pres (overhead PyTorch).
# Exercice 2 : a completer par l'etudiantdef estimate_vram(n_params, n_trainable, hidden, n_layers, batch_size, seq_len, param_dtype="fp16", optim="adamw"):"""Estime la VRAM totale en MB."""# TODO etudiantpass
Exercice 3 : brancher vers FT-02
Lire FT-02-QLoRA-Quantization.ipynb et verifier que les 4 points suivants sont couverts :
BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type='nf4', bnb_4bit_use_double_quant=True) est utilise.
prepare_model_for_kbit_training est appele avant get_peft_model.
paged_adamw_8bit est utilise comme optimiseur (etat Adam 8-bit).
target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj'] – LoRA sur toutes les projections attention.
Si un de ces points manque, le fine-tuning 7B risque un OOM ou une accuracy degradee.
# Exercice 3 : ouvrir FT-02 et verifier les 4 pointsFT02 ="FT-02-QLoRA-Quantization.ipynb"# a cote de ce notebookifnot os.path.exists(FT02): # cwd = racine du depot (ex. papermill --cwd racine) FT02 ="MyIA.AI.Notebooks/GenAI/FineTuning/FT-02-QLoRA-Quantization.ipynb"import jsonnb = json.load(open(FT02, encoding="utf-8"))for i, c inenumerate(nb['cells']): src =''.join(c['source']) ifisinstance(c['source'], list) else c['source']for kw in ["BitsAndBytesConfig", "prepare_model_for_kbit_training","paged_adamw_8bit", "target_modules"]:if kw in src:print(f" cell {i}: contient '{kw}' -- OK")
cell 0: contient 'BitsAndBytesConfig' -- OK
cell 4: contient 'BitsAndBytesConfig' -- OK
cell 5: contient 'BitsAndBytesConfig' -- OK
cell 17: contient 'prepare_model_for_kbit_training' -- OK
cell 18: contient 'prepare_model_for_kbit_training' -- OK
cell 18: contient 'target_modules' -- OK
cell 19: contient 'target_modules' -- OK
cell 22: contient 'paged_adamw_8bit' -- OK
cell 26: contient 'paged_adamw_8bit' -- OK
cell 27: contient 'paged_adamw_8bit' -- OK
cell 28: contient 'paged_adamw_8bit' -- OK
cell 33: contient 'target_modules' -- OK
cell 40: contient 'BitsAndBytesConfig' -- OK
cell 42: contient 'BitsAndBytesConfig' -- OK
cell 46: contient 'paged_adamw_8bit' -- OK
Resume
QLoRA = LoRA + quantification 4-bit NF4 (Dettmers et al. 2023). Le gain est structurel : il mord sur les nn.Linear, et c’est la que les transformers passent 70-85 % de leurs poids. Sur un CNN, le gain est marginal et peut etre nul.
Trois lecons a retenir :
Mesurer la fraction quantifiable avant d’invoquer QLoRA : si la fraction est < 50 %, le gain est marginal. La mesure est structurelle (pas besoin de GPU).
Le budget trainable depend du modele et de la configuration – de 0.220 % (DistilBERT) a 12.909 % (SmallCNN-LoRA) ici ; SmallCNN-QLoRA n’entraine pas le meme budget que le fp32 de FT-00c. Ce que QLoRA change toujours, c’est la memoire du modele gele : 4-bit vs fp16.
Le gain devient determinant a partir de 7B : sur DistilBERT (66 M), on passe de ~250 MB a 163 MB (mesure) ; sur Phi-3-mini (3.8B), de ~7.6 GB a ~2.5 GB ; sur Llama-3-7B, de ~14 GB a ~4 GB.
Branchement : FT-02 met en pratique sur un LLM 7B avec paged_adamw_8bit et device_map='auto'. FT-03-FT-06 montrent l’usage sur SFT, DPO, model merging.