NOPE LinkedIn

Catégories:
Blog

Changer de Base : Migrer ses Agents LoRA de Phi-3.5 vers Qwen2.5-3B

Changer de Base : Migrer ses Agents LoRA de Phi-3.5 vers Qwen2.5-3B image

Rubrique: Blog Tag: LoRA Tag: Qwen2.5 Tag: Phi-3 Tag: vLLM Tag: Multi-LoRA Tag: Fine-tuning Tag: GPU Tag: VRAM Tag: cybersécurité

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-smi avant de configurer GPU_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.py scanne loras/ au démarrage et charge tous les adapters compatibles avec TOOL_AGENT_BASE_MODEL
  • Hot-swap transparent : le coordinateur route vers opnsense, wireguard ou crowdsec ; 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.