Changer de Base : Migrer ses Agents LoRA de Phi-3.5 vers Qwen2.5-3B
Dans la série LoRA Factory, nous avons construit une usine à agents spécialisés sur Phi-3.5-mini-instruct. Trois agents (OPNsense, WireGuard, CrowdSec), trois adapters LoRA, un pipeline d’entraînement automatisé.
Tout fonctionnait — jusqu’à ce que les limites du modèle de base se manifestent en production. Ce premier article de la série Agents en Production documente pourquoi et comment nous avons migré vers Qwen2.5-3B-Instruct.
Pourquoi changer de modèle de base ?
Phi-3.5-mini est un excellent modèle compact. Mais dans le contexte d’agents function-calling pilotés par un coordinateur LLM, deux problèmes sont apparus :
1. Qualité du function calling
Phi-3.5-mini génère des tool_calls corrects dans ~85-90% des cas sur nos benchmarks internes.
Qwen2.5-3B, malgré sa taille similaire (~3B paramètres), a été entraîné avec un accent particulier
sur le suivi d’instructions structurées et le function calling — ce qui correspond exactement
à notre cas d’usage.
2. Le problème du multi-LoRA Pour servir trois agents simultanément avec vLLM en mode multi-LoRA, tous les adapters doivent partager la même base. Nos trois LoRA étaient sur Phi-3.5-mini, ce qui était correct. Mais Phi-3.5-mini pose un problème de licence moins permissive pour la redistribution des adapters sur HuggingFace. Qwen2.5 est sous Apache 2.0.
Trois options d’architecture
Avant de migrer, trois approches ont été évaluées :
Option A — Base différente par agent
OPNsense → Qwen2.5-7B (plus puissant, 102 fonctions)
WireGuard → Qwen2.5-3B (suffisant, 11 fonctions)
CrowdSec → Qwen2.5-3B (suffisant, 15 fonctions)
Rejeté : multi-LoRA impossible (vLLM exige une base commune). Trois processus vLLM séparés = trois fois la consommation VRAM.
Option B — Base Qwen2.5-7B pour tous
OPNsense + WireGuard + CrowdSec → Qwen2.5-7B
Rejeté : 7B en 8-bit ≈ 7.24 Go. Avec le coordinateur (lui aussi Qwen2.5-3B), impossible de tout tenir sur 12 Go de VRAM. Et le 7B n’apporte pas de gain mesurable sur du function calling structuré vs le 3B fine-tuné.
Option C — Base Qwen2.5-3B pour tous ✅
OPNsense + WireGuard + CrowdSec → Qwen2.5-3B-Instruct (base commune)
Retenu : un seul moteur vLLM, hot-swap des adapters à la volée, budget VRAM compatible avec le coordinateur sur le même GPU.
Budget VRAM sur RTX 4070 Ti (12 Go)
La contrainte principale : coordinateur et tool-agent-server tournent sur le même GPU.
GPU total : 11.99 GiB
Tool-agent (0.45) : 5.40 GiB (Qwen2.5-3B + KV cache)
Coordinateur : 2.29 GiB (poids modèle)
Overhead CUDA : 1.13 GiB
─────────────────────────────
Peak estimé : 8.82 GiB
Marge disponible : 3.17 GiB → KV cache coordinateur
Avec VLLM_MAX_MODEL_LEN=8192 et COORDINATOR_GPU_UTIL=0.89, le coordinateur dispose
d’environ 1.85 Go de KV cache — suffisant pour les requêtes en production.
Règle pratique : ne jamais calculer le budget VRAM théoriquement. vLLM mesure la VRAM totale du GPU au démarrage (tous processus confondus), pas le delta propre à l’instance. Mesurer empiriquement avec
nvidia-smiavant de configurerGPU_UTIL.
Configuration de l’entraînement par agent
Les trois agents ne nécessitent pas le même rang LoRA :
| Agent | Fonctions | Dataset | Rang r | Epochs | Loss finale |
|---|---|---|---|---|---|
| OPNsense | 102 | 13 701 ex. | 64 | 3 | 0.2532 |
| WireGuard | 11 | 693 ex. | 32 | 3 | 0.7292 |
| CrowdSec | 15 | 250 ex. | 16 | 15 | 0.4637 |
Le rang est proportionnel à la complexité : OPNsense couvre 13 domaines fonctionnels distincts, WireGuard et CrowdSec restent mono-domaine.
Pour CrowdSec, le dataset de 250 exemples est petit — compensé par 15 epochs avec cosine scheduler pour maximiser l’extraction du signal disponible.
Particularité Qwen2.5 : le format de prompt
Qwen2.5 utilise le format ChatML (<|im_start|>/<|im_end|>), différent du format Phi-3
(<|system|>/<|end|>). Ce détail, anodin en apparence, est la source du bug le plus
spectaculaire de cette migration — documenté dans le prochain article.
La règle à retenir pour tout fine-tuning multi-modèles :
# ❌ Format hardcodé — catastrophique lors d'un changement de base
def format_example(messages):
return f"<|system|>\n{system}\n<|user|>\n{user}\n<|assistant|>\n"
# ✅ Déléguer au tokenizer — model-agnostic
def format_example(tokenizer, messages):
return tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
Ce que cette migration change en production
Avec les trois adapters sur la même base Qwen2.5-3B :
- Un seul processus vLLM au lieu de trois → économie de ~4 Go de VRAM
- Découverte automatique :
server.pyscanneloras/au démarrage et charge tous les adapters compatibles avecTOOL_AGENT_BASE_MODEL - Hot-swap transparent : le coordinateur route vers
opnsense,wireguardoucrowdsec; vLLM charge l’adapter correspondant sans recharger le backbone
# server.py — découverte automatique des adapters compatibles
def _discover_lora_adapters(loras_dir, base_model_name):
for lora_dir in loras_dir.iterdir():
config = lora_dir / "adapter" / "adapter_config.json"
if config.exists():
cfg = json.loads(config.read_text())
if cfg.get("base_model_name_or_path") == base_model_name:
yield lora_dir
Résultats
Après entraînement et validation (détaillés dans les articles suivants) :
| Agent | Score vérification | Durée entraînement |
|---|---|---|
| OPNsense | 102/102 — 100% | ~3h42 |
| WireGuard | 11/11 — 100% | ~7 min |
| CrowdSec | 15/15 — 100% | ~13 min |
Les trois adapters sont publiés sur HuggingFace : opnsense-qwen25-lora · wireguard-qwen25-lora · crowdsec-qwen25-lora
Prochain article : comment une loss correcte peut masquer une vérification catastrophique — le bug de format train/infer et son diagnostic.
