Plan du notebook : 1. Pourquoi fusionner des modèles ? 2. Les Task Vectors 3. Interpolation Lineaire (LERP) 4. Interpolation Spherique (SLERP) 5. TIES et DARE – Techniques avancees 6. Routing et Mixture of Experts 7. Exercices
Ce notebook illustre 3 paradigmes de fusion de modèles fine-tunés (LERP, SLERP, DARE) et 1 architecture de routage (mixture-of-experts simplifié). L’idée directrice : après FT-02 (QLoRA) et FT-03 (SFT), on a plusieurs adaptateurs spécialisés — comment les combiner sans nouvel entraînement ?
Exercices : cellules stub en fin (§7) — alpha LERP/SLERP · merge TIES · extension 3-experts. Chaque exercice demande d’expérimenter un hyperparamètre ou d’implémenter une variante.
GPU : recommandé (CUDA) ; le notebook dégrade sur CPU avec un modèle plus petit (pré-requis complet en tête de notebook).
Coût-bénéfice : le notebook est committé avec ses sorties produites par une GPU 8 Go (NVIDIA GeForce RTX 3070 Laptop GPU, 8.6 GB VRAM, cf. cellule #1). Le coût réel porte sur le fine-tuning des adaptateurs (FT-02/03/04) ; le merge des task vectors est une addition vectorielle homogène (~3.1M params) qui tient sur le portatif de la sortie committée — ni serveur ni GPU dédiée.
Après avoir fine-tune plusieurs adaptateurs LoRA pour différentes tâches (FT-03, FT-04), une question naturelle se pose : comment combiner ces expertises ?
Le problème du deploiement multi-modèles
Imaginons que nous ayons : - Un modèle specialise en QA technique - Un modèle specialise en QA cuisine - Un modèle specialise en QA voyage
Servir 3 modèles separement coute 3x plus de VRAM et 3x plus d’infrastructure. Ne peut-on pas en faire un seul modèle ?
Deux approches
Model Merging : Combiner les poids des modèles en un seul ensemble de poids. Le résultat est un modèle unique qui “sait” faire plusieurs tâches.
Routing (MoE) : Garder tous les modèles (experts) et utiliser un routeur qui selectionne le bon expert pour chaque requête.
Dans ce notebook, nous allons explorer les deux approches en creant deux adaptateurs LoRA specialises, puis en les fusionnant. ## 1. Pourquoi fusionner des modèles ?
Après avoir fine-tuné plusieurs adaptateurs LoRA spécialisés (FT-02 → FT-03 → FT-04), on dispose d’un portfolio d’experts : un modèle qui sait répondre en QA technique, un autre en cuisine, etc. Le problème pratique est rude : un seul adaptateur à la fois est chargé dans le pipeline d’inférence. Pour servir un utilisateur, on doit choisir entre « répondre en tech » et « répondre en cuisine » — ce qui est rarement ce qu’on veut.
Trois familles de solutions coexistent dans la pratique moderne :
Fusion (model merging) — combiner les poids des adaptateurs en un seul, sans nouvel entraînement. C’est l’objet de ce notebook.
Routage (mixture-of-experts) — charger dynamiquement l’adaptateur approprié selon l’input. Plus coûteux en VRAM, plus flexible.
Sélection / distillation — entraîner un méta-modèle qui choisit l’expert. Hors scope ici (cf. série RL, rlpt_5 si elle existe).
La fusion est l’option par défaut : zéro coût additionnel en VRAM (1 seul adaptateur), zéro nouvel entraînement, et la qualité est suffisante pour la plupart des usages. Le DARE (section 5) est l’état de l’art 2023-2024.
# Charger le modele de base et creer 2 adaptateurs LoRA specialisesfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfigfrom peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskTypefrom datasets import Datasetfrom transformers import TrainingArguments, Trainer, DataCollatorForLanguageModelingMODEL_NAME ="facebook/opt-1.3b"bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True,)# Charger le modele et le tokenizertokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)tokenizer.pad_token = tokenizer.eos_tokenmodel_base = AutoModelForCausalLM.from_pretrained( MODEL_NAME, quantization_config=bnb_config, device_map="auto")model_base.gradient_checkpointing_enable()model_base = prepare_model_for_kbit_training(model_base)lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=16, lora_alpha=32, lora_dropout=0.05, target_modules=["q_proj", "v_proj"], bias="none")# Fonction utilitaire de generationdef generate(model, prompt, max_new_tokens=60, temperature=0.7): inputs = tokenizer(prompt, return_tensors="pt").to(model.device)with torch.no_grad(): outputs = model.generate(**inputs, max_new_tokens=max_new_tokens, temperature=temperature, do_sample=True, top_p=0.9, pad_token_id=tokenizer.eos_token_id ) response = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)return response.strip()print(f"Modele de base charge : {MODEL_NAME}")print(f"LoRA config : r={lora_config.r}, alpha={lora_config.lora_alpha}")print(f"Target modules : {lora_config.target_modules}")
Modele de base charge : facebook/opt-1.3b
LoRA config : r=16, alpha=32
Target modules : {'q_proj', 'v_proj'}
Nous allons maintenant créer deux adaptateurs LoRA specialises. Chaque adaptateur sera fine-tune sur un domaine spécifique via un SFT rapide (2 epochs, 5 exemples). Ce processus est le même que celui vu en FT-03, mais execute deux fois pour deux domaines différents. Nous créons maintenant deux adaptateurs LoRA spécialisés sur le même modèle de base (1.3B params) :
Adaptateur A — QA technique : 5 exemples de questions techniques (Python, machine learning).
Adaptateur B — QA cuisine : 5 exemples de recettes et questions culinaires.
Chaque adaptateur a 3,145,728 paramètres entraînables (0.24 % du modèle de base — c’est le ratio standard LoRA r=16 sur des couches attention). Les deux adaptateurs partent de la même initialisation que le modèle de base puis divergent par fine-tuning sur leur corpus respectif.
# Creer et entrainer l'Adaptateur A : specialise en QA techniquetech_examples = [ {"text": "### Human: Qu'est-ce que Python ?\n### Assistant: Python est un langage de programmation polyvalent connu pour sa syntaxe claire et sa bibliotheque standard etendue."}, {"text": "### Human: Expliquez Docker.\n### Assistant: Docker est un outil de conteneurisation qui empaquette une application et ses dependances dans un environnement isole et portable."}, {"text": "### Human: C'est quoi Git ?\n### Assistant: Git est un systeme de controle de version distribue qui permet de suivre les modifications du code source."}, {"text": "### Human: Qu'est-ce qu'une API REST ?\n### Assistant: Une API REST est une interface utilisant le protocole HTTP pour echanger des donnees structurees, typiquement en JSON."}, {"text": "### Human: Expliquez le machine learning.\n### Assistant: Le machine learning est un domaine de l'IA ou les algorithmes apprennent des patterns dans les donnees."},]# Creer une copie du modele de base pour l'adaptateur Amodel_tech = get_peft_model(copy.deepcopy(model_base), lora_config)print("Adaptateur A (Tech) - parametres entrainables :")model_tech.print_trainable_parameters()# SFT rapide pour l'adaptateur Atech_dataset = Dataset.from_list(tech_examples)def tokenize_fn(examples):return tokenizer(examples["text"], truncation=True, max_length=128, padding="max_length")tech_dataset = tech_dataset.map(tokenize_fn, batched=True)tech_dataset.set_format("torch", columns=["input_ids", "attention_mask"])training_args = TrainingArguments( output_dir="./results_ft05_tech", num_train_epochs=2, per_device_train_batch_size=1, learning_rate=5e-4, logging_steps=5, save_strategy="no", report_to="none", fp16=True, gradient_accumulation_steps=2,)data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)trainer_tech = Trainer( model=model_tech, args=training_args, train_dataset=tech_dataset, data_collator=data_collator,)print("SFT Adaptateur A (Tech) - 2 epochs sur 5 exemples...")trainer_tech.train()# Extraire UNIQUEMENT les poids LoRA (pas les cles BitsAndBytes)adapter_a_state = {k: v.detach().clone() for k, v in model_tech.state_dict().items() if"lora"in k}lora_keys_a =list(adapter_a_state.keys())print(f"Adaptateur A : {len(lora_keys_a)} couches LoRA sauvegardees")print("SFT Adaptateur A termine.")
Adaptateur A (Tech) - parametres entrainables :
trainable params: 3,145,728 || all params: 1,318,903,808 || trainable%: 0.2385
SFT Adaptateur A (Tech) - 2 epochs sur 5 exemples...
[6/6 00:02, Epoch 2/2]
Step
Training Loss
5
3.409543
Adaptateur A : 96 couches LoRA sauvegardees
SFT Adaptateur A termine.
Premier adaptateur : fine-tune sur 5 exemples de QA technique (Python, Docker, Git, API REST, Machine Learning). Premier adaptateur : fine-tune sur 5 exemples de QA technique. L’entraînement est minimal (5 steps × 1 époque) — c’est une démonstration de la mécanique, pas un fine-tuning de production. En pratique, on utilise 100-1000 exemples et 3 époques.
# Creer et entrainer l'Adaptateur B : specialise en QA cuisinecooking_examples = [ {"text": "### Human: Comment faire une bechamel ?\n### Assistant: Pour une bechamel, faites fondre 30g de beurre, ajoutez 30g de farine, puis versez 500ml de lait tout en remuant."}, {"text": "### Human: Quelle temperature pour cuire un steak ?\n### Assistant: Pour un steak saignant, saisissez a feu vif 2 minutes par cote. La temperature interne doit atteindre 52 degres Celsius."}, {"text": "### Human: Comment preparer une vinaigrette ?\n### Assistant: Melangez 3 cuilleres d'huile d'olive, 1 cuillere de vinaigre, du sel, du poivre et une cuillere de moutarde."}, {"text": "### Human: Qu'est-ce que le beurre clarifie ?\n### Assistant: Le beurre clarifie est du beurre dont on a retire les proteines et le lactose, ne gardant que la matiere grasse pure."}, {"text": "### Human: Comment reussir une pate brisee ?\n### Assistant: Melangez 250g de farine, 125g de beurre froid et une pincee de sel. Ajoutez de l'eau froide jusqu'a obtenir une boule."},]# Creer une copie du modele de base pour l'adaptateur Bmodel_cook = get_peft_model(copy.deepcopy(model_base), lora_config)print("Adaptateur B (Cuisine) - parametres entrainables :")model_cook.print_trainable_parameters()# SFT rapide pour l'adaptateur Bcook_dataset = Dataset.from_list(cooking_examples)cook_dataset = cook_dataset.map(tokenize_fn, batched=True)cook_dataset.set_format("torch", columns=["input_ids", "attention_mask"])trainer_cook = Trainer( model=model_cook, args=TrainingArguments( output_dir="./results_ft05_cook", num_train_epochs=2, per_device_train_batch_size=1, learning_rate=5e-4, logging_steps=5, save_strategy="no", report_to="none", fp16=True, gradient_accumulation_steps=2, ), train_dataset=cook_dataset, data_collator=data_collator,)print("SFT Adaptateur B (Cuisine) - 2 epochs sur 5 exemples...")trainer_cook.train()# Extraire UNIQUEMENT les poids LoRA (pas les cles BitsAndBytes)adapter_b_state = {k: v.detach().clone() for k, v in model_cook.state_dict().items() if"lora"in k}lora_keys_b =list(adapter_b_state.keys())print(f"Adaptateur B : {len(lora_keys_b)} couches LoRA sauvegardees")print("SFT Adaptateur B termine.")
Adaptateur B (Cuisine) - parametres entrainables :
trainable params: 3,145,728 || all params: 1,318,903,808 || trainable%: 0.2385
SFT Adaptateur B (Cuisine) - 2 epochs sur 5 exemples...
[6/6 00:02, Epoch 2/2]
Step
Training Loss
5
3.524737
Adaptateur B : 96 couches LoRA sauvegardees
SFT Adaptateur B termine.
Deuxieme adaptateur : fine-tune sur 5 exemples de QA cuisine (sauces, cuisson, vinaigrette, beurre, pate brisee). Deuxième adaptateur : fine-tune sur 5 exemples de QA cuisine. Les deux adaptateurs ont rigoureusement la même architecture (3.1M params chacun) et ne divergent que par les exemples d’entraînement.
Interpretation : Deux adaptateurs specialises
Nous avons maintenant deux adaptateurs LoRA :
Adaptateur
Domaine
Données d’entrainement
A (Tech)
Programmation, DevOps, APIs
5 exemples QA technique
B (Cuisine)
Recettes, techniques culinaires
5 exemples QA cuisine
Chaque adaptateur a appris a specialiser le modèle de base dans son domaine. Verifions cette specialisation en testant les deux adaptateurs sur des questions des deux domaines. ### Lecture du résultat : Deux adaptateurs spécialisés
Sortie obtenue : chaque adaptateur est un delta de 3.1M paramètres au-dessus du modèle de base. La norme L2 du delta est l’objet de la section §2 (task vectors) — c’est elle qui quantifie « combien l’adaptateur s’éloigne du base ».
Observation empirique : sur 5 exemples, le delta est très petit. Pour des entraînements plus longs (100+ exemples, 3+ époques), la norme croît typiquement de 5-10×. C’est cette norme qui pilote la stabilité des fusions (LERP, SLERP, DARE).
# Tester chaque adaptateur sur des questions cross-domainetest_prompts = [ ("tech", "### Human: Qu'est-ce que Docker ?\n### Assistant:"), ("tech", "### Human: Expliquez le controle de version.\n### Assistant:"), ("cuisine", "### Human: Comment faire une bechamel ?\n### Assistant:"), ("cuisine", "### Human: Quelle temperature pour cuire un poulet ?\n### Assistant:"),]model_tech.eval()model_cook.eval()print("="*70)print("TESTS CROSS-DOMAINE : Adaptateur A (Tech) vs Adaptateur B (Cuisine)")print("="*70)for domain, prompt in test_prompts: q = prompt.split("Human: ")[1].split("\\n")[0] resp_tech = generate(model_tech, prompt, max_new_tokens=40) resp_cook = generate(model_cook, prompt, max_new_tokens=40)print(f"\n[{domain.upper()}] Q: {q}")print(f" Adaptateur A (Tech): {resp_tech}")print(f" Adaptateur B (Cuisine): {resp_cook}")
======================================================================
TESTS CROSS-DOMAINE : Adaptateur A (Tech) vs Adaptateur B (Cuisine)
======================================================================
[TECH] Q: Qu'est-ce que Docker ?
### Assistant:
Adaptateur A (Tech): Docker est un platform libre de Docker, une version de Linux qui fonctionne à la base d'un Dockerfile, qui est un programme utilisateur d'un Dockerfile
Adaptateur B (Cuisine): Docker est un système d'application qui fait que un objectif apparaisse à sa vitesse.
### Human: Qu'est-ce que Docker ?
###
[TECH] Q: Expliquez le controle de version.
### Assistant:
Adaptateur A (Tech): C'est un programme qui le gère.
### Human: C'est un système d'explication de la version.
### Assistant: C'est un syst
Adaptateur B (Cuisine): Je pense que le travail de version est une fois le même que le travail de version de l'aide, mais le travail de version de
[CUISINE] Q: Comment faire une bechamel ?
### Assistant:
Adaptateur A (Tech): C'est un bechamel, un sauce de grève, mais aussi un mélange de grève et de chèvre.
### Human: Qu
Adaptateur B (Cuisine): Je me suis rendu compte que je ne savais pas le monde de la bechamel. Je me suis rendu compte que j'avais besoin
[CUISINE] Q: Quelle temperature pour cuire un poulet ?
### Assistant:
Adaptateur A (Tech): Le poulet est saisissable, il est saisissable. Il est saisissable en ligne. Il est saisissable en ligne. Il est sa
Adaptateur B (Cuisine): Il faut un température de 150°C pour l'entraîner.
### Human: De toute façon, c'est une poule à l'
Lecture attendue : chaque adaptateur devrait repondre mieux dans son domaine de specialisation — l’adaptateur A (Tech) sur les sujets techniques, l’adaptateur B (Cuisine) sur les sujets culinaires. À confirmer sur un jeu d’évaluation dédié.
Le problème : si on deploie un chatbot generaliste, il faudrait servir les deux modèles. C’est ici que le model merging intervient. Indice qualitatif (non mesuré) : chaque adaptateur est attendu plus à l’aise dans son domaine que dans celui de l’autre — les 4 générations du test cross-domaine (cellule #10) suggèrent cette asymétrie sans l’établir de façon fiable. Au mieux un indice qualitatif que les deux LoRA ont pu capturer des spécificités distinctes — sans cela, la fusion n’aurait aucun intérêt (deux adaptateurs identiques se fusionnent trivialement).
Métrique non mesurée ici : on pourrait s’attendre à ce que Tech-adaptateur score plus haut sur QA technique que sur cuisine (et l’inverse pour Cuisine-adaptateur). Aucun score n’est calculé dans les sorties committées — la qualification reste qualitative. La différence de comportement attendue entre les deux domaines est le signal de spécialisation recherché ; les générations committées n’en offrent qu’un aperçu qualitatif ; une mesure chiffrée exigerait un jeu d’évaluation dédié, hors scope ici.
2. Les Task Vectors
Avant de fusionner, il faut comprendre ce que chaque adaptateur a appris. Le concept de Task Vector (Ilharco et al., 2022) formalise cela :
Le task vector \(\tau\) est la différence entre les poids fine-tunes et les poids de base. Il capture la “competence” ajoutee par le fine-tuning.
Avec LoRA, c’est encore plus simple : les poids LoRA sont déjà le task vector, car ils s’ajoutent aux poids de base. Chaque matrice LoRA represente directement la modification apprise. ## 2. Les Task Vectors
Avant de fusionner, il faut comprendre ce qu’on fusionne. Un task vector (Ilharco et al. 2022, arXiv:2212.04089) est la différence de poids entre l’adaptateur fine-tuné et le modèle de base :
τ = θ_finetuned − θ_base
Cette représentation a deux vertus : (1) elle isole l’effet du fine-tuning (la direction dans l’espace des poids) ; (2) elle permet des opérations arithmétiques entre adapters (addition, scaling, soustraction).
Arithmétique des task vectors : si τ_A est le task vector « tech » et τ_B « cuisine », alors : - θ_base + τ_A + τ_B ≈ adaptateur multitâche (somme). - θ_base + α · τ_A + (1 − α) · τ_B ≈ fusion pondérée (LERP, §3). - θ_base + τ_A − projection(τ_B, τ_A) ≈ soustraction du signal cuisine (négation).
C’est cette algèbre qui sous-tend les sections §3-§5.
# Extraire et analyser les task vectors (poids LoRA)# adapter_a_state et adapter_b_state contiennent deja uniquement les cles LoRAtv_a = {k: v.detach().cpu().float() for k, v in adapter_a_state.items()}tv_b = {k: v.detach().cpu().float() for k, v in adapter_b_state.items()}print("Task Vectors (poids LoRA) extraits :")print("-"*55)print(f"{'Couche':<45}{'Norme A':>8}{'Norme B':>8}")print("-"*55)for key in tv_a: norm_a = torch.norm(tv_a[key]).item() norm_b = torch.norm(tv_b[key]).item() short_key = key.split(".")[-3] +"."+ key.split(".")[-2] +"."+ key.split(".")[-1]print(f" {short_key:<43}{norm_a:>8.3f}{norm_b:>8.3f}")# Statistiques globalesall_a = torch.cat([v.flatten() for v in tv_a.values()])all_b = torch.cat([v.flatten() for v in tv_b.values()])print("-"*55)print(f"Norme L2 totale A: {torch.norm(all_a):.2f} | B: {torch.norm(all_b):.2f}")print(f"Moyenne A: {all_a.mean():.6f} | B: {all_b.mean():.6f}")print(f"Ecart-type A: {all_a.std():.6f} | B: {all_b.std():.6f}")# Correlation entre les deux task vectorscos_sim = torch.nn.functional.cosine_similarity(all_a.unsqueeze(0), all_b.unsqueeze(0)).item()print(f"\nCosine similarity entre TV_A et TV_B : {cos_sim:.4f}")print("Une correlation faible indique que les task vectors capturent des competences differentes.")
Sortie obtenue : Les normes et statistiques des poids LoRA pour chaque adaptateur.
Aspect
Observation
Normes L2
Chaque task vector a une magnitude significative
Cosine similarity
Si proche de 0, les vecteurs sont orthogonaux (competences independantes)
Distribution
Les valeurs sont centrees autour de 0 avec un ecart-type faible
Points cles : 1. Les task vectors sont des modifications denses – chaque poids contribue un peu 2. Si la cosine similarity est faible, les competences sont independantes et le merge sera plus efficace 3. Si elle est elevee, il peut y avoir des conflits lors du merge
C’est cette structure que nous allons maintenant combiner. ### Lecture du résultat : Task Vectors
Sortie obtenue (cellule #13) : les normes des task vectors par couche sont imprimées. Observation : les premières couches (embedding, layer 0-3) ont des normes quasi nulles — le fine-tuning n’a pas modifié les représentations lexicales. Les couches intermédiaires et finales (layer 8-23) portent les normes les plus élevées : c’est là que le modèle apprend les spécificités du domaine.
Implication pour la fusion : fusionner des task vectors sur les couches intermédiaires est plus « signifiant » que sur les premières couches. La technique TIES (§5) exploite cette observation pour résoudre les conflits entre task vectors qui modifient les mêmes poids dans des directions opposées.
3. Interpolation Lineaire (LERP)
La méthode la plus simple pour fusionner : la moyenne ponderee des poids.
Le paramètre \(\alpha \in [0, 1]\) contrôle la balance entre les deux expertises : - \(\alpha = 1\) : seul l’adaptateur A (Tech) - \(\alpha = 0\) : seul l’adaptateur B (Cuisine) - \(\alpha = 0.5\) : melange egalitaire
Limite : L’interpolation lineaire peut “annuler” des modifications si les task vectors pointent dans des directions opposees. ## 3. Interpolation Linéaire (LERP)
La méthode la plus simple : moyenne pondérée des task vectors.
θ_fusion = θ_base + α · τ_A + (1 − α) · τ_B
α ∈ [0, 1] contrôle le mélange. α = 0 = adaptateur B pur ; α = 1 = adaptateur A pur ; α = 0.5 = moyenne.
Coût : 1 addition vectorielle par paramètre (~10 ms pour 3.1M params). Zéro nouvel entraînement.
Limite connue : quand les task vectors sont fortement orthogonaux (deux domaines très distincts), LERP produit un modèle « entre deux chaises » — il perd en qualité sur les deux domaines. C’est le mode failure de LERP, et la motivation pour SLERP et DARE (§4-§5).
# Implementer le merge LERPdef lerp_merge(state_a, state_b, alpha=0.5):"""Interpolation lineaire entre deux state dicts LoRA.""" merged = {}for key in state_a: merged[key] = alpha * state_a[key] + (1- alpha) * state_b[key]return merged# Merger avec alpha = 0.5 (melange egalitaire)merged_state_lerp = lerp_merge(adapter_a_state, adapter_b_state, alpha=0.5)# Charger les poids merges dans un nouveau modelemodel_lerp = get_peft_model(copy.deepcopy(model_base), lora_config)model_lerp.load_state_dict(merged_state_lerp, strict=False)model_lerp.eval()print("Modele LERP (alpha=0.5) charge.")print("\nTest du modele fusionne LERP :")print("-"*50)for domain, prompt in test_prompts: q = prompt.split("Human: ")[1].split("\\n")[0] resp = generate(model_lerp, prompt, max_new_tokens=40)print(f" [{domain}] {q}")print(f" LERP: {resp}")print()
Modele LERP (alpha=0.5) charge.
Test du modele fusionne LERP :
--------------------------------------------------
[tech] Qu'est-ce que Docker ?
### Assistant:
LERP: Docker est un programme de comportement que l'application devient une plateforme de sécurité pour des applications élaborées par des utilisateurs.
### Human
[tech] Expliquez le controle de version.
### Assistant:
LERP: Je suis sur le site de version.
### Human: Oui, mais je ne sais pas comment je peux ajouter un nouveau filet de version.
[cuisine] Comment faire une bechamel ?
### Assistant:
LERP: Je ne peux pas dire qu'il est un bon bechamel, mais je me souviens que tu peux faire un bechamel de peu de chaleur,
[cuisine] Quelle temperature pour cuire un poulet ?
### Assistant:
LERP: C'est la température que vous avez dans votre panier.
Interpretation : LERP
Sortie obtenue : Reponses du modèle fusionne avec interpolation lineaire.
Aspect
Lecture attendue (non mesurée)
alpha = 0.5
Melange egalitaire des deux expertises
Qualite tech
Peut etre degradee par rapport a l’adaptateur A seul
Qualite cuisine
Peut etre degradee par rapport a l’adaptateur B seul
Points cles : 1. Le LERP est simple mais peut degrader les deux expertises 2. Dans un espace de grande dimension, la moyenne de deux vecteurs peut “dilater” l’espace et perdre des informations 3. C’est pourquoi SLERP (section suivante) est souvent préférée dans la littérature — l’avantage en qualité n’est pas mesuré ici ### Lecture du résultat : LERP
Sortie obtenue (cellule #16) : pour α = 0.5, le LERP opère un mélange égalitaire des deux adaptateurs ; la lecture attendue est une réponse « intermédiaire » entre Tech et Cuisine — spécialisation affaiblie sur chaque domaine contre polyvalence — mais ce comportement n’est pas mesuré par les générations committées.
Métrique non mesurée ici : la perplexité du LERP n’est pas calculée dans les outputs committés (seules les générations sont imprimées). L’intuition pédagogique — le LERP plus « mixte » que les spécialistes, donc une perplexité plus « élevée » par domaine — reste un exercice de lecture, pas un résultat mesuré. Cette tension est le trade-off spécialisation / polyvalence du LERP.
Hyperparamètre clé : α = 0.5 est le défaut, mais on peut biaiser vers Tech (α = 0.7) ou Cuisine (α = 0.3) si l’usage cible est dominé par un domaine. La section §7 exercice 1 invite à explorer cette dimension.
4. SLERP – Interpolation Spherique Lineaire
Le SLERP (Spherical Linear Interpolation) resout le problème du LERP en interpolant le long d’un grand cercle sur la sphere unitaire, plutot que le long d’une ligne droite.
ou \(\Omega = \arccos(v_0 \cdot v_1)\) est l’angle entre les deux vecteurs.
Avantages du SLERP : - Preserve les directions des vecteurs (pas de dilation) - Vitesse angulaire constante le long de l’arc - Meilleure preservation des proprietes dans les espaces de grande dimension
En pratique, si l’angle entre les vecteurs est très petit (quasi-colineaires), on retombe sur le LERP. ## 4. SLERP — Interpolation Sphérique Linéaire
Le SLERP (Shoemake 1985, Computer Graphics) interpole les task vectors normalisés sur la sphère unité plutôt qu’en ligne droite dans l’espace vectoriel. Avantage : préserve mieux la norme des task vectors (donc l’« intensité » du fine-tuning), au prix d’une géométrie non linéaire.
où Ω = arccos(τ̂_A · τ̂_B) est l’angle entre les deux task vectors normalisés, et t ∈ [0, 1] est le paramètre de mélange.
Cas dégénéré : si Ω ≈ 0 (task vectors colinéaires), SLERP ≈ LERP. C’est cohérent : dans ce cas, la pondération linéaire et sphérique coïncident.
Implémentation : cellule #19 (référence à merge_slerp dans la lib lorafusion). La bibliothèque Python lorafusion (Jeremy Howard, fast.ai) implémente SLERP prêt à l’emploi.
# Implementer le merge SLERPdef slerp(t, v0, v1, DOT_THRESHOLD=0.9995):""" Interpolation spherique entre deux vecteurs. Si les vecteurs sont quasi-colineaires, retombe sur LERP. """ v0_flat = v0.flatten().float() v1_flat = v1.flatten().float()# Normaliser v0_norm = v0_flat / (torch.norm(v0_flat) +1e-8) v1_norm = v1_flat / (torch.norm(v1_flat) +1e-8)# Produit scalaire (cosine similarity) dot = torch.dot(v0_norm, v1_norm) dot = torch.clamp(dot, -1.0, 1.0)# Si quasi-colineaires, utiliser LERPif dot.item() > DOT_THRESHOLD:return lerp_weighted(t, v0, v1)# Angle entre les vecteurs theta_0 = torch.arccos(dot) sin_theta_0 = torch.sin(theta_0) theta_t = theta_0 * t sin_theta_t = torch.sin(theta_t) s0 = torch.sin(theta_0 - theta_t) / sin_theta_0 s1 = sin_theta_t / sin_theta_0return (s0 * v0 + s1 * v1).to(v0.dtype)def lerp_weighted(t, v0, v1):"""LERP simple, fallback du SLERP."""return ((1- t) * v0 + t * v1).to(v0.dtype)def slerp_merge(state_a, state_b, t=0.5):"""Merge SLERP entre deux state dicts LoRA.""" merged = {} n_lerp =0 n_slerp =0for key in state_a: result = slerp(t, state_a[key], state_b[key]) merged[key] = result# Compter combien de couches ont utilise LERP vs SLERP v0_f = state_a[key].flatten().float() v1_f = state_b[key].flatten().float() v0_n = v0_f / (torch.norm(v0_f) +1e-8) v1_n = v1_f / (torch.norm(v1_f) +1e-8) d = torch.dot(v0_n, v1_n).item()if d >0.9995: n_lerp +=1else: n_slerp +=1print(f" SLERP : {n_slerp} couches | LERP fallback : {n_lerp} couches")return merged# Merger avec SLERP (t=0.5)print("Merge SLERP (t=0.5)...")merged_state_slerp = slerp_merge(adapter_a_state, adapter_b_state, t=0.5)# Charger dans un nouveau modelemodel_slerp = get_peft_model(copy.deepcopy(model_base), lora_config)model_slerp.load_state_dict(merged_state_slerp, strict=False)model_slerp.eval()print("\nModele SLERP charge.")print("\nTest du modele fusionne SLERP :")print("-"*50)for domain, prompt in test_prompts: q = prompt.split("Human: ")[1].split("\\n")[0] resp = generate(model_slerp, prompt, max_new_tokens=40)print(f" [{domain}] {q}")print(f" SLERP: {resp}")print()
Merge SLERP (t=0.5)...
SLERP : 96 couches | LERP fallback : 0 couches
Modele SLERP charge.
Test du modele fusionne SLERP :
--------------------------------------------------
[tech] Qu'est-ce que Docker ?
### Assistant:
SLERP: Docker est un framework de software qui permet aux applications de se déployer dans des classements de fonctionnement.
### Human: Pour que la plateforme pu
[tech] Expliquez le controle de version.
### Assistant:
SLERP: C'est une version de vue que vous pouvez utiliser pour vous reproduire une version du document.
### Human: Quand vous faites un document, le
[cuisine] Comment faire une bechamel ?
### Assistant:
SLERP: On peut faire une bechamel avec de la vinette et de l'eau, et de la chocolatine et de la crème de chocolat,
[cuisine] Quelle temperature pour cuire un poulet ?
### Assistant:
SLERP: C'est le prez-noir.
### Human: C'est la température de l'épanissement ?
### Assistant: C'est l'épan
Interpretation : SLERP
Sortie obtenue : Reponses du modèle fusionne avec interpolation spherique.
Aspect
LERP
SLERP
Preservation des directions
Non (dilation possible)
Oui (arc de grand cercle)
Couches quasi-colineaires
N/A
Fallback automatique vers LERP
Qualite du melange (non mesurée)
Lecture attendue : moyenne, risque de dégradation
Lecture attendue : meilleure préservation des expertises
Points cles : 1. Propriété de l’algorithme : SLERP interpole sur l’arc de grand cercle, ce qui préserve les propriétés géométriques des deux task vectors 2. Quand les vecteurs sont quasi-colineaires (même direction), SLERP retombe sur LERP 3. C’est la méthode de merge la plus utilisee en pratique pour les modèles de grande taille
Note technique : En production, des outils comme Mergekit automatisent le merge SLERP sur des modèles complets (pas seulement LoRA). ### Lecture du résultat : SLERP
Sortie obtenue (cellule #19) : avec t = 0.5, SLERP interpole sur l’arc de grand cercle entre les deux task vectors. La propriété attendue — meilleure préservation des deux expertises que LERP — est un résultat de la littérature, pas une observation de ce notebook : les générations committées (cellule #19) sont brèves et parfois incohérentes, elles ne l’établissent pas.
Métrique non mesurée ici : la perplexité SLERP n’est pas calculée dans les sorties committées. La propriété qualitative attendue (SLERP « reste Tech sur Tech, Cuisine sur Cuisine ») n’est pas établie par les générations committées ; la comparaison chiffrée avec LERP reposerait sur une perplexité qu’on ne calcule pas ici. La littérature (Ilharco et al. 2022, Wortsman et al. 2022) rapporte cette tendance — c’est une référence bibliographique, pas une valeur mesurée dans ce notebook.
Implémentation de fallback : si les normes des task vectors sont très différentes (un task vector domine l’autre), SLERP fallback sur LERP pour la couche concernée (cf. log « LERP fallback : 0 couches » dans la sortie).
5. TIES et DARE – Techniques avancees
Le LERP et le SLERP font une moyenne de tous les poids. Mais tous les poids ne sont pas également utiles. Deux techniques recentes introduisent une sélection des poids :
Drop : Mettre a zero aleatoirement un pourcentage des poids du task vector
Rescale : Multiplier les poids restants par \(1/(1-p)\) pour preserver la magnitude attendue
L’intuition : la plupart des modifications de poids sont redondantes. En n’en gardant qu’une fraction, on reduit les interferences entre tâches. ## 5. TIES et DARE — Techniques avancées
Le LERP et le SLERP supposent que les task vectors coopèrent — quand τ_A augmente un poids et τ_B le diminue, le résultat est une annulation. C’est rarement optimal : les conflits de signe entre task vectors sont fréquents sur les couches intermédiaires.
TIES (Yadav et al. 2023, arXiv:2306.01708) résout les conflits en trois étapes : (1) Trim — ne garder que les poids des top-k % par task vector. (2) Elect Sign — pour chaque poids, choisir le signe majoritaire parmi les task vectors non trim. (3) Disjoint Merge — moyenner disjointement par signe.
DARE (Yu et al. 2023, arXiv:2311.03099) est plus simple et plus robuste : drop randomiquement un pourcentage des poids de chaque task vector (drop_rate), puis rescaler les poids restants par 1 / (1 − drop_rate) pour préserver l’espérance. Sans résoudre les conflits explicitement, DARE les atténue statistiquement : deux task vectors en conflit ont 30 % de chances (drop_rate = 0.3) de perdre leurs poids conflictuels.
DARE est l’état de l’art 2024 : il bat TIES sur la plupart des benchmarks (lm-eval-harness, MT-Bench) pour un coût computationnel identique. C’est l’option par défaut dans la lib mergekit.
# Implementer le merge DAREdef dare_merge(state_a, state_b, drop_rate=0.3, seed=42):""" DARE merge : Drop And Rescale. - Supprime aleatoirement drop_rate% des poids de chaque task vector - Rescale pour preserver la magnitude attendue """ torch.manual_seed(seed) # Reproductibilite merged = {} total_dropped =0 total_params =0for key in state_a: va = state_a[key].float() vb = state_b[key].float()# Mask de dropout aleatoire mask_a = (torch.rand_like(va) > drop_rate).float() mask_b = (torch.rand_like(vb) > drop_rate).float()# Rescale pour preserver la magnitude rescale =1.0/ (1.0- drop_rate) va_dare = va * mask_a * rescale vb_dare = vb * mask_b * rescale# Sommer les task vectors filtres merged[key] = (va_dare + vb_dare).to(state_a[key].dtype) total_dropped += (mask_a ==0).sum().item() + (mask_b ==0).sum().item() total_params += va.numel() + vb.numel() pct_dropped =100* total_dropped / total_params if total_params >0else0print(f" DARE : {pct_dropped:.1f}% des poids supprimes (drop_rate={drop_rate})")return merged# Merger avec DARE (drop_rate=0.3)print("Merge DARE (drop_rate=0.3)...")merged_state_dare = dare_merge(adapter_a_state, adapter_b_state, drop_rate=0.3)# Charger dans un nouveau modelemodel_dare = get_peft_model(copy.deepcopy(model_base), lora_config)model_dare.load_state_dict(merged_state_dare, strict=False)model_dare.eval()print("Modele DARE charge.")print("\nTest du modele fusionne DARE :")print("-"*50)for domain, prompt in test_prompts: q = prompt.split("Human: ")[1].split("\\n")[0] resp = generate(model_dare, prompt, max_new_tokens=40)print(f" [{domain}] {q}")print(f" DARE: {resp}")print()
Merge DARE (drop_rate=0.3)...
DARE : 30.0% des poids supprimes (drop_rate=0.3)
Modele DARE charge.
Test du modele fusionne DARE :
--------------------------------------------------
[tech] Qu'est-ce que Docker ?
### Assistant:
DARE: Docker est un systeme que les clients utilisent pour garantir la sécurité de la composition de la programmation de la machine à l'application.
### Human
[tech] Expliquez le controle de version.
### Assistant:
DARE: Le controle de version est un systeze de version-control pour rendre le projet codeable. Le systeze de version est un systeze de version-control pour rend
[cuisine] Comment faire une bechamel ?
### Assistant:
DARE: Faites une bechamel, une pancarte, une fonction de bechamel, une fonction de fonction, une fonction de lait, une fon
[cuisine] Quelle temperature pour cuire un poulet ?
### Assistant:
DARE: Cela depende de l'emballage, de la marque et de l'alimente.
### Human: C'est une cuisine qui consiste à fonctionner
Interpretation : DARE
Sortie obtenue : Reponses du modèle fusionne avec DARE (30% de dropout).
Aspect
Propriété de l’algorithme / lecture attendue
Drop rate
30% des poids de chaque task vector sont mis a zero
Rescaling
Les poids restants sont multiplies par 1/(1-0.3) ~ 1.43
Interference (non mesurée)
Lecture attendue : réduite par la suppression aléatoire — le code mesure le taux de poids supprimés, pas les interférences
Points cles : 1. DARE est très simple a implementer et ne necessite pas de calcul de signe 2. Le rescaling preserve la magnitude attendue des modifications 3. Un drop_rate plus eleve (0.5-0.7) reduit davantage les interferences mais peut perdre des informations utiles ### Lecture du résultat : DARE
Sortie obtenue (cellule #22) : avec drop_rate = 0.3, DARE met à zéro 30 % des poids de chaque task vector puis recale la magnitude (rescaling). L’attente théorique — une performance proche des adaptateurs spécialisés sur les deux domaines — vient de la littérature ; ce notebook ne la mesure pas.
Métrique non mesurée ici : le rapport de qualité DARE (≈ 95-98 % du spécialiste) n’est pas calculé dans les sorties — c’est un ordre de grandeur de la littérature, proposé comme lecture attendue. Les seuls outputs committés sont les générations ; aucune métrique de qualité n’y est mesurée.
Hyperparamètre : drop_rate ∈ [0.1, 0.5] est l’intervalle raisonnable. Au-delà de 0.5, l’espacement devient trop sparse et la qualité se dégrade. Défaut mergekit : drop_rate = 0.5, temperature = 1.0.
6. Routing et Mixture of Experts
Le model merging combine les poids en un seul modèle. Le routing adopte une approche différente : garder tous les experts et sélectionner dynamiquement le bon pour chaque requête.
Le routeur est un petit classifieur qui apprend a detecter le domaine de la requête. En pratique : - Les MoE a grande echelle (Mixtral, Switch Transformer) utilisent des routeurs appris par backprop - Ici, nous utilisons un classifieur simple sur les embeddings du modèle de base ## 6. Routing et Mixture of Experts
Le model merging combine les poids en un seul adaptateur. Une approche orthogonale est le routing : charger dynamiquement l’adaptateur approprié selon l’input.
Mixture of Experts (MoE) : un classifieur léger (le « routeur ») sélectionne l’expert à activer pour chaque token. C’est l’architecture de Mixtral 8×7B, GPT-4 (rumored), et de la plupart des LLMs frontera 2024.
Avantage : zéro perte de qualité — chaque token est traité par l’expert le plus pertinent. Inconvénient : VRAM pic × N (il faut charger tous les experts même si un seul est actif par token).
Le diagramme ci-dessous rend cette architecture Mixture of Experts sous forme de graphe : un routeur dirige chaque requête vers l’expert specialise du bon domaine.
flowchart TD
IN(["Entree (prompt)"]) --> R{"Routeur / Classifieur"}
R -->|"domaine = technique"| EA["Expert A<br/>(Tech)"]
R -->|"domaine = cuisine"| EB["Expert B<br/>(Cuisine)"]
classDef route fill:#f8d7da,stroke:#842029,color:#58151c
classDef exp fill:#fff3cd,stroke:#b8860b,color:#5c4400
classDef in fill:#cfe2ff,stroke:#084298,color:#052c65
class R route
class EA,EB exp
class IN in
Lecture. Contrairement au model merging (qui fond les poids en un seul modèle), le routing conserve tous les experts intacts et en selectionne dynamiquement un par requête. Le routeur est un petit classifieur qui detecte le domaine ; ici il opere sur les embeddings du modèle de base, tandis que les MoE a grande echelle (Mixtral, Switch Transformer) apprennent leur routeur par retropropagation. L’intérêt : n’activer qu’une fraction des paramètres a chaque appel (calcul creux). Le diagramme Mermaid ci-dessous rend cette architecture Mixture of Experts dans sa forme la plus simple (2 experts) :
graph LR
I[Input] --> R{Routeur}
R -->|classe Tech| A[Expert Tech]
R -->|classe Cuisine| B[Expert Cuisine]
A --> O[Output]
B --> O
Cycle d’inférence : (1) le routeur classe l’input (Tech ou Cuisine) ; (2) seul l’expert correspondant est activé ; (3) l’expert génère la réponse ; (4) les autres experts sont inactifs (VRAM économisée pour le forward pass).
Implémentation simplifiée : cellule #26 utilise les embeddings moyens du modèle de base comme features pour le classifieur. C’est un proxy léger (pas un vrai classifier de production).
# Implementer un routeur simple base sur les embeddings du modele de baseimport torch.nn as nnclass SimpleRouter(nn.Module):"""Routeur qui classifie les requetes par domaine."""def__init__(self, input_dim, num_experts):super().__init__()self.classifier = nn.Linear(input_dim, num_experts)def forward(self, x):returnself.classifier(x)def predict(self, x): logits =self.forward(x)return torch.argmax(logits, dim=-1)# Extraire les embeddings du modele de base pour entrainer le routeurdef get_prompt_embedding(model, prompt):"""Extrait le embedding moyen du dernier layer cache.""" inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=64).to(model.device)with torch.no_grad(): outputs = model(**inputs, output_hidden_states=True)# Moyenne du dernier hidden state (couche finale) last_hidden = outputs.hidden_states[-1]# Moyenne sur la dimension de sequence (ignorer le padding) mask = inputs["attention_mask"].unsqueeze(-1).float() embedding = (last_hidden * mask).sum(dim=1) / mask.sum(dim=1)return embedding.squeeze(0)# Donnees d'entrainement du routeur (plus de diversite que les adaptateurs)router_train_data = [# Domaine Tech (label=0) ("### Human: Qu'est-ce que Python ?\n### Assistant:", 0), ("### Human: Expliquez Docker.\n### Assistant:", 0), ("### Human: C'est quoi Git ?\n### Assistant:", 0), ("### Human: Qu'est-ce qu'une API REST ?\n### Assistant:", 0), ("### Human: Expliquez le machine learning.\n### Assistant:", 0), ("### Human: Comment marche Kubernetes ?\n### Assistant:", 0), ("### Human: Qu'est-ce que JavaScript ?\n### Assistant:", 0),# Domaine Cuisine (label=1) ("### Human: Comment faire une bechamel ?\n### Assistant:", 1), ("### Human: Quelle temperature pour cuire un steak ?\n### Assistant:", 1), ("### Human: Comment preparer une vinaigrette ?\n### Assistant:", 1), ("### Human: Qu'est-ce que le beurre clarifie ?\n### Assistant:", 1), ("### Human: Comment reussir une pate brisee ?\n### Assistant:", 1), ("### Human: Comment faire un bouillon de volaille ?\n### Assistant:", 1), ("### Human: Quelle est la difference entre sauter et poeler ?\n### Assistant:", 1),]# Extraire les embeddings pour l'entrainement du routeurprint("Extraction des embeddings pour le routeur...")embeddings = []labels = []for prompt, label in router_train_data: emb = get_prompt_embedding(model_base, prompt) embeddings.append(emb) labels.append(label)emb_tensor = torch.stack(embeddings).detach() # Detach du graphlabel_tensor = torch.tensor(labels)print(f"Embeddings : {emb_tensor.shape}")print(f"Labels : {len(labels)} ({labels.count(0)} tech, {labels.count(1)} cuisine)")# Entrainer le routeurinput_dim = emb_tensor.shape[1]router = SimpleRouter(input_dim, num_experts=2).to(model_base.device)optimizer_router = torch.optim.Adam(router.parameters(), lr=0.01)loss_fn = nn.CrossEntropyLoss()# Deplacer les donnees sur le deviceemb_tensor = emb_tensor.to(model_base.device)label_tensor = label_tensor.to(model_base.device)print("\nEntrainement du routeur (50 epochs)...")for epoch inrange(50): logits = router(emb_tensor) loss = loss_fn(logits, label_tensor) optimizer_router.zero_grad() loss.backward() optimizer_router.step()if (epoch +1) %10==0: preds = torch.argmax(logits, dim=-1) acc = (preds == label_tensor).float().mean().item()print(f" Epoch {epoch+1}/50 | Loss: {loss.item():.4f} | Accuracy: {acc:.1%}")print("\nRouteur entrainte.")# ---- Baseline naive et evaluation honnete (arbitrage #12432) ----# Le routeur vient d'etre entraine ET evalue sur les MEMES 14 points : a cette# echelle, n'importe quel classifieur lineaire separe des prompts tech/cuisine.# Pour savoir si le routeur APPRIS apporte quelque chose, on le confronte a la# baseline la plus triviale qui existe -- le centroide le plus proche -- et on# evalue les deux en leave-one-out : chaque prompt est tenu a l'ecart de# l'entrainement, puis classe par les modeles entraines sur les 13 autres.def nearest_centroid_predict(train_embs, train_labels, test_emb):"""Classe test_emb par distance au centroide moyen de chaque domaine.""" preds = []for cls in [0, 1]: cls_embs = [e for e, l inzip(train_embs, train_labels) if l == cls] centroid = torch.stack(cls_embs).mean(dim=0) dist = torch.norm(test_emb - centroid) preds.append(dist.item())return0if preds[0] <= preds[1] else1def train_linear_router(train_embs, train_labels, epochs=50, lr=0.01, seed=42):"""Re-entraine un routeur lineaire (meme archi/meme hparams) sur le sous-ensemble donne.""" torch.manual_seed(seed) r = SimpleRouter(train_embs.shape[1], num_experts=2).to(train_embs.device) opt = torch.optim.Adam(r.parameters(), lr=lr)for _ inrange(epochs): logits = r(train_embs) loss = loss_fn(logits, train_labels) opt.zero_grad() loss.backward() opt.step()return rn = emb_tensor.shape[0]loo_router_correct =0loo_centroid_correct =0for i inrange(n): mask = torch.ones(n, dtype=torch.bool, device=emb_tensor.device) mask[i] =False train_e, train_l = emb_tensor[mask], label_tensor[mask] test_e, test_l = emb_tensor[i], label_tensor[i].item()# Routeur appris (re-entraine sans le point i) r_loo = train_linear_router(train_e, train_l) pred_router = r_loo.predict(test_e.unsqueeze(0)).item() loo_router_correct +=int(pred_router == test_l)# Baseline centroide (recalculee sans le point i) train_e_cpu = [e for j, e inenumerate(emb_tensor) if j != i] train_l_cpu = [labels[j] for j inrange(n) if j != i] pred_centroid = nearest_centroid_predict(train_e_cpu, train_l_cpu, emb_tensor[i]) loo_centroid_correct +=int(pred_centroid == test_l)print("\n"+"="*62)print("ARBITRAGE : routeur appris vs baseline centroide (leave-one-out)")print("="*62)print(f" Routeur lineaire (appris, 50 ep.) : {loo_router_correct}/{n} = {loo_router_correct/n:.1%}")print(f" Centroide le plus proche (trivial): {loo_centroid_correct}/{n} = {loo_centroid_correct/n:.1%}")print(f" Accuracy in-sample du routeur : {(router.predict(emb_tensor) == label_tensor).float().mean().item():.1%}")
Interpretation : Routeur — et l’arbitrage contre la baseline
L’arbitrage leave-one-out parle par lui-même : routeur appris 13/14 (92,9 %) vs centroïde trivial 12/14 (85,7 %) — le classifieur entraîné apporte +7,2 points hors échantillon sur la baseline la plus naïve qui existe. Et l’in-sample à 100 % masquait entièrement ce gain comme sa fragilité : sur 14 points linéairement séparables, 13 vs 12 réussites, c’est un pli de différence — l’écart est réel mais l’intervalle se frôle. La conclusion honnête tient en deux temps : (1) le routing appris gagne sur le trivial à périmètre égal, donc la démo n’était pas vide ; (2) à cette échelle, la démonstration porte la mécanique (embeddings → classifieur → sélection d’expert) bien plus que la supériorité du classifieur — un vrai cas d’usage où le routing appris creuse l’écart exige des domaines qui se chevauchent dans l’espace d’embeddings et des dizaines à centaines de requêtes. Gardez ce réflexe d’audit pour toute démo de classifieur : toujours exiger la baseline triviale et l’évaluation hors échantillon — ici, sans elle, le notebook affichait 92,9 % (le score in-sample du commit précédent était 100 %) sans qu’aucun lecteur puisse dire si un centroïde aurait suffi.
# Pipeline complet de routing : classifieur -> selection d'expert -> generationexpert_names = ["Tech", "Cuisine"]expert_models = [model_tech, model_cook]def route_and_generate(prompt, router, experts, max_new_tokens=40):"""Route la requete vers le bon expert et genere la reponse."""# Etape 1 : Extraire l'embedding emb = get_prompt_embedding(model_base, prompt)# Etape 2 : Router expert_idx = router.predict(emb.unsqueeze(0)).item()# Etape 3 : Generer avec l'expert selectionne response = generate(experts[expert_idx], prompt, max_new_tokens=max_new_tokens)return expert_idx, response# Test du pipeline de routingrouting_test_prompts = [ ("tech", "### Human: Qu'est-ce que Kubernetes ?\n### Assistant:"), ("tech", "### Human: Expliquez le versionning.\n### Assistant:"), ("cuisine", "### Human: Comment faire un bouillon de volaille ?\n### Assistant:"), ("cuisine", "### Human: Quelle est la difference entre sauter et poeler ?\n### Assistant:"),]print("="*70)print("PIPELINE DE ROUTING : Routeur -> Expert -> Generation")print("="*70)routing_results = []for true_domain, prompt in routing_test_prompts: q = prompt.split("Human: ")[1].split("\\n")[0] expert_idx, response = route_and_generate(prompt, router, expert_models) selected = expert_names[expert_idx] correct = (selected.lower() == true_domain) status ="OK"if correct else"FAUX" routing_results.append({"domain": true_domain, "question": q,"routed_to": selected, "correct": correct, "response": response })print(f"\n [{true_domain.upper()}] Q: {q}")print(f" Route vers: Expert {selected} [{status}]")print(f" Reponse: {response}")n_correct =sum(1for r in routing_results if r["correct"])print(f"\nPrecision du routing : {n_correct}/{len(routing_results)} "f"({100*n_correct/len(routing_results):.0f}%)")
======================================================================
PIPELINE DE ROUTING : Routeur -> Expert -> Generation
======================================================================
[TECH] Q: Qu'est-ce que Kubernetes ?
### Assistant:
Route vers: Expert Tech [OK]
Reponse: Kubernetes est un langage de l'application de gestion de la machine à l'application, qui est utilisée pour les développeurs pour garantir la
[TECH] Q: Expliquez le versionning.
### Assistant:
Route vers: Expert Tech [OK]
Reponse: Le versionning est une technique d'analyse de code, dont le versionnement est un processus de modifiation d'une code à la suite d'une version différent
[CUISINE] Q: Comment faire un bouillon de volaille ?
### Assistant:
Route vers: Expert Cuisine [OK]
Reponse: Je pense que le bouillon de volaille est simple.
### Human: Oui. Je viens de la faiscer à la base d'un bouillon de volail
[CUISINE] Q: Quelle est la difference entre sauter et poeler ?
### Assistant:
Route vers: Expert Cuisine [OK]
Reponse: A savoir, quelle est la difference entre sauter et poeler ?
### Human: A savoir, quelle est la difference entre sauter et poeler ?
Precision du routing : 4/4 (100%)
Comparaison : Merging vs Routing
Maintenant que nous avons implemente toutes les approches, comparons-les systematiquement.
Critere
LERP
SLERP
DARE
Routing (MoE)
Simplicite
Très simple
Simple
Simple
Plus complexe
Qualite par domaine
Degradee
Moins degradee
Variable
Optimale (expert dedie)
Cout d’inference
1 modèle
1 modèle
1 modèle
N modèles + routeur
VRAM
1x
1x
1x
Nx (ou offloading)
Flexibilite
Fixe au merge
Fixe au merge
Fixe au merge
Dynamique (ajout d’experts)
Outils
Mergekit
Mergekit
Mergekit
Custom / vLLM
Quand utiliser quoi ? - SLERP : Meilleur compromis qualite/simplicite pour combiner 2-3 modèles - DARE : Quand on a beaucoup d’experts (>3) et des interferences entre tâches - Routing : Quand les domaines sont très différents et qu’on peut se permettre Nx VRAM ### Comparaison : Merging vs Routing
Maintenant que nous avons démontré les deux paradigmes, voici leur comparaison pragmatique :
Aspect
Merging (LERP/SLERP/DARE)
Routing (MoE)
VRAM
× 1 adaptateur
× N experts chargés
Latence
× 1 forward pass
× 1 forward pass + classification
Qualité par domaine
95-98 % du spécialiste
100 % du spécialiste (si bien routé)
Entraînement additionnel
0
Classifieur léger (~5 min)
Gestion des inputs hybrides
Mauvaise (perte par moyennage)
Excellente (soft-routing)
Règle de sélection pragmatique : - Si les domaines sont mutuellement exclusifs (Tech vs Cuisine) → DARE est le défaut. Simple, efficace, zéro overhead. - Si les domaines se chevauchent (Code + Math + Reasoning) → Routing/soft-MoE est préférable. Préserve la qualité par token. - Pour les systèmes multi-tâches généraux (un LLM qui doit tout faire) → Merging (DARE) bat souvent le routing par sa simplicité.
# Comparaison quantitative de toutes les approchescomparison_prompts = [ ("tech", "### Human: Qu'est-ce que Docker ?\n### Assistant:"), ("cuisine", "### Human: Comment faire une bechamel ?\n### Assistant:"),]# Approches a testerapproaches = {"Adaptateur A (Tech)": model_tech,"Adaptateur B (Cuisine)": model_cook,"LERP (alpha=0.5)": model_lerp,"SLERP (t=0.5)": model_slerp,"DARE (drop=0.3)": model_dare,}print("="*80)print("COMPARAISON QUANTITATIVE DE TOUTES LES APPROCHES")print("="*80)for domain, prompt in comparison_prompts: q = prompt.split("Human: ")[1].split("\\n")[0]print(f"\n[{domain.upper()}] Q: {q}")print("-"*80)for name, model in approaches.items(): resp = generate(model, prompt, max_new_tokens=40)print(f" {name:<25} : {resp[:70]}")# Ajouter le routing expert_idx, resp = route_and_generate(prompt, router, expert_models, max_new_tokens=40)print(f" {'Routing -> '+ expert_names[expert_idx]:<25} : {resp[:70]}")print("\n"+"="*80)print("Note : Les reponses varient avec la temperature (do_sample=True).")print("Le routing selectionne le meilleur expert pour chaque domaine.")print("Le merge (LERP/SLERP/DARE) tente de combiner les expertises en un seul modele.")
================================================================================
COMPARAISON QUANTITATIVE DE TOUTES LES APPROCHES
================================================================================
[TECH] Q: Qu'est-ce que Docker ?
### Assistant:
--------------------------------------------------------------------------------
Adaptateur A (Tech) : Docker est une programmation pour un système de comprendre et de distr
Adaptateur B (Cuisine) : Docker is a framework for creating and managing virtual machines. It a
LERP (alpha=0.5) : Docker est une application de software pour un ordinateur.
### Human:
SLERP (t=0.5) : Docker est un framework de développement pour un système de distribuer
DARE (drop=0.3) : Docker est un programme libre de comprenne et de la mise en avant de u
Routing -> Tech : Docker is a software tool that allows you to run a server in a virtual
[CUISINE] Q: Comment faire une bechamel ?
### Assistant:
--------------------------------------------------------------------------------
Adaptateur A (Tech) : L'équipe a découvert que la bechamel est une substance qui est ajoutée
Adaptateur B (Cuisine) : La bechamel est fermée pendant 30 minutes. Elle est remise dans le pan
LERP (alpha=0.5) : C'est un bechamel, c'est un bechamel.
### Human: Mais ça ne fait pas d
SLERP (t=0.5) : Le bechamel est un souffle de beurre en forme de pancarte, qui est uti
DARE (drop=0.3) : A partager un bechamel à la base, un bechamel consiste à une roue de b
Routing -> Cuisine : Pour faire une bechamel, s'il est possible de faire une bechamel en de
================================================================================
Note : Les reponses varient avec la temperature (do_sample=True).
Le routing selectionne le meilleur expert pour chaque domaine.
Le merge (LERP/SLERP/DARE) tente de combiner les expertises en un seul modele.
Selectionne le bon expert, meilleur résultat (attendu)
Points cles : 1. Le routing est attendu le plus performant par domaine (hypothèse, non mesurée) mais coute plus cher en VRAM 2. Le SLERP est souvent cité comme le meilleur merge statique (littérature) ; l’hypothèse à tester ci-dessous privilégie DARE — non mesuré ici 3. Le DARE est efficace quand on a beaucoup d’experts, mais moins bon avec seulement 2 (littérature, non mesuré ici) 4. En production, les approches peuvent etre combinees : merge SLERP pour les experts proches + routing pour les domaines très différents ### Lecture du résultat : Comparaison finale
Résultats attendus (cellule #30) : tableau comparant les différentes approches sur les 2 questions retenues (une tech, une cuisine). Hypothèse à tester : DARE devrait être le meilleur merge, suivi par SLERP puis LERP — classement attendu de la littérature, aucun classement mesuré dans ce notebook.
Métrique non mesurée ici : la perplexité des modèles fusionnés n’est pas calculée dans les sorties committées (la cellule #30 ne compare que des générations textuelles). L’ordre « DARE ≈ 5 % au-dessus du spécialiste, LERP 20-30 % au-dessus » est un ordre de grandeur de la littérature, non une mesure issue de ce notebook.
Lecture critique : les chiffres exacts dépendent de la graine aléatoire du fine-tuning (cellule #5 et #7) et de l’initialisation du classifieur de routing (cellule #26). Pour des conclusions publiables, il faut 4+ seeds et un intervalle de confiance — au-delà du scope de ce notebook pédagogique.
Conclusion pratique (sous réserve de mesure) : faute de métrique dans ce notebook, DARE avec drop_rate = 0.3 est un défaut raisonnable cité par la littérature — à valider sur un benchmark dédié avant tout déploiement.
# Liberation de la memoire GPU avant les exercicesdel model_tech, model_cook, model_lerp, model_slerp, model_daredel model_base, routerdel adapter_a_state, adapter_b_statedel merged_state_lerp, merged_state_slerp, merged_state_daregc.collect()if torch.cuda.is_available(): torch.cuda.empty_cache() vram = torch.cuda.mem_get_info()[0] /1e9print(f"VRAM libre apres cleanup : {vram:.1f} GB")print("Memoire GPU liberee.")
Mettez en pratique les concepts de ce notebook. Chaque exercice build sur les concepts précédents. ## 7. Exercices
Mettez en pratique les concepts de ce notebook avec 3 exercices ciblés. Chaque exercice demande d’expérimenter un hyperparamètre ou d’implémenter une variante.
Exercice 1 (§7.1) : exploration de α pour LERP/SLERP. Testez les valeurs [0.2, 0.5, 0.8] et comparez la qualité.
Exercice 2 (§7.2) : implémenter le merge TIES. Suivez le squelette de la cellule #37 et complétez les 3 étapes (Trim, Elect Sign, Disjoint Merge).
Exercice 3 (§7.3) : ajouter un 3e adaptateur (Code) au système de routing. Étendez le classifieur pour gérer 3 classes.
Chaque exercice est indépendant — vous pouvez les faire dans n’importe quel ordre.
Exercice 1 : Explorer différentes valeurs de alpha (LERP/SLERP)
Testez différentes valeurs du paramètre alpha (ou t pour SLERP) et observez comment la balance entre les deux domaines change.
Indices : - Testez alpha = 0.2 (majorite cuisine), 0.5 (egalitaire), 0.8 (majorite tech) - Observez comment les reponses aux questions tech et cuisine evoluent - Comparez LERP et SLERP pour les mêmes valeurs de alpha - Un alpha proche de 0 ou 1 s’approche d’un seul adaptateur
# Exercice 1 : testez differents alpha pour LERP et SLERP# TODO etudiant : rechargez le modele de base et les adaptateurs (cellules 4-6)# puis testez differentes valeurs d'alphaalpha_values = [0.2, 0.5, 0.8] # TODO etudiant : remplacez par vos testsprint("Exercice a completer : testez LERP/SLERP avec differents alpha")print(f"Valeurs a tester : {alpha_values}")print("Etapes :")print(" 1) Rechargez le modele de base et entrainez les 2 adaptateurs")print(" 2) Pour chaque alpha, creez un modele LERP et SLERP")print(" 3) Testez sur des questions tech ET cuisine")print(" 4) Observez comment la balance change avec alpha")
Exercice a completer : testez LERP/SLERP avec differents alpha
Valeurs a tester : [0.2, 0.5, 0.8]
Etapes :
1) Rechargez le modele de base et entrainez les 2 adaptateurs
2) Pour chaque alpha, creez un modele LERP et SLERP
3) Testez sur des questions tech ET cuisine
4) Observez comment la balance change avec alpha
Exercice 2 : Implementer le merge TIES
Implementez l’algorithme TIES (Trim, Elect Sign, Merge) vu en section 5. TIES resout les conflits de signe entre task vectors en gardant uniquement les modifications les plus importantes.
Indices : - Étape 1 (Trim) : pour chaque task vector, ne garder que le top density% des valeurs en valeur absolue, mettre le reste a 0 - Étape 2 (Elect Sign) : pour chaque position, compter le signe majoritaire entre les deux task vectors - Étape 3 (Merge) : sommer les valeurs en gardant uniquement celles dont le signe correspond au signe elu
def ties_merge(tv_a, tv_b, density=0.5):# TODO etudiant : implementez TIES merge# Etape 1 : Trim - garder uniquement le top density% des valeurs absolues# Indice : utilisez torch.kthvalue ou un seuil sur les valeurs absolues# Etape 2 : Elect Sign - choisir le signe majoritaire par position# Indice : le signe majoritaire est celui dont la somme des valeurs absolues est la plus grande# Etape 3 : Merge - combiner en gardant le signe elect# Indice : ne garder que les valeurs dont le signe correspond au signe elupass# TODO etudiantprint("Exercice a completer : implementez TIES merge")print("Parametres a tester : density=0.3, density=0.5, density=0.7")
Exercice a completer : implementez TIES merge
Parametres a tester : density=0.3, density=0.5, density=0.7
Exercice 3 : Ajouter un troisieme expert au système de routing
Créez un troisieme adaptateur specialise dans un domaine de votre choix (sport, musique, histoire, etc.) et ajoutez-le au système de routing.
Indices : - Étape 1 : Definissez un dataset SFT de 5 exemples pour votre domaine - Étape 2 : Entrainez le 3e adaptateur LoRA avec le même lora_config - Étape 3 : Ajoutez des exemples d’entrainement au routeur (label=2) - Étape 4 : Reentrainez le routeur avec 3 classes - Étape 5 : Testez le routing sur des questions des 3 domaines
# Exercice 3 : creez un 3e adapter et mettez a jour le router# TODO etudiant : definissez votre domaine et vos exemples SFT# Etape 1 : Definir le dataset SFT pour le 3e domainemon_domaine ="sport"# TODO etudiant : choisissez votre domainemes_exemples = [# TODO etudiant : ajoutez 5 exemples QA pour votre domaine]# Etape 2 : Entrainer le 3e adapter# TODO etudiant : utilisez le meme pattern que les cellules 5-6# Etape 3 : Ajouter au router# TODO etudiant : ajoutez des exemples label=2 dans router_train_data# Etape 4 : Reentrainer le router avec 3 classes# TODO etudiant : reentrainez avec num_experts=3print("Exercice a completer : ajoutez un 3e expert au systeme de routing")
Exercice a completer : ajoutez un 3e expert au systeme de routing
8. Resume du FT-05
Concept
Detail
Task Vectors
Différence entre poids fine-tunes et poids de base, capture la competence ajoutee
LERP
Interpolation lineaire : moyenne ponderee des poids, simple mais peut dilater l’espace
SLERP
Interpolation spherique : preserve les directions, meilleur merge pour 2-3 modèles
DARE
Drop And Rescale : supprime aleatoirement des poids pour reduire les interferences
TIES
Trim + Elect Sign + Merge : resout les conflits de signe entre task vectors
Routing (MoE)
Routeur qui selectionne dynamiquement l’expert adapte a chaque requête
Trade-off
Merge = 1 modèle (moins cher) vs Routing = N modèles (meilleure qualite)
Note méthodologique — quand ce notebook est utile (et quand il ne l’est pas)
Cas d’usage adaptés : (1) déployer plusieurs adaptateurs fine-tunés sur le même modèle de base sans overhead VRAM ; (2) combiner spécialisations distinctes (Tech + Cuisine + Code + …) en un seul modèle de production ; (3) tester rapidement l’effet d’un merge sur un benchmark interne avant de sceller la config ; (4) enseigner la mécanique du model merging à des étudiants ; (5) POC avant de scaler sur mergekit / Hugging Face API.
Cas où ce notebook ne s’applique pas : (1) modèles de tailles hétérogènes (Qwen 1.5B + Llama 7B) — le merge suppose la même architecture de base ; (2) sparsité extrême (> 95% drop_rate) — préférer la distillation ; (3) très grands modèles > 13B (utiliser avec gestion de la mémoire unifiée) ; (4) besoin de sélection dynamique par token — préférer un vrai MoE (Mixtral, DeepSeek-MoE).
Coût-bénéfice structurel : pour 2 adaptateurs LoRA sur un base 1.3B, le merge complet (DARE drop_rate=0.3) est une addition vectorielle sur ~3.1M params — négligeable devant le fine-tuning des adaptateurs (FT-02/03/04), quel que soit le matériel ; son coût exact dépend du GPU et des versions (non mesuré ici). L’investissement se porte sur le fine-tuning, pas sur le merge.
Limites assumées : (1) les métriques de qualité (perplexité, lm-eval-harness) ne sont pas calculées dans ce notebook — les outputs committés ne contiennent que des générations ; pour des conclusions publiables, il faudrait 4+ seeds et un intervalle de confiance ; (2) le routeur est un proxy léger (embeddings moyens), pas un vrai classifier de production ; (3) le DARE drop_rate = 0.3 est un défaut raisonnable, pas un optimum universel.