Aller au contenu principal

Versioning et rollback

Pourquoi versionner vos modèles

En production, vous allez itérer sur votre modèle fine-tuné : nouvelles données, hyperparamètres ajustés, nouveau modèle de base. Sans un système de versioning rigoureux, vous perdrez la traçabilité et la capacité de revenir en arrière en cas de régression.

Convention de nommage

Suffixes versionnés

Utilisez le paramètre suffix pour identifier chaque version :

# Convention : {nom}-v{version_majeure}.{version_mineure}
job = client.fine_tuning.jobs.create(
    training_file=training_file.id,
    model="gpt-5.3-mini",
    suffix="support-client-v2.1"
)

Schéma de version recommandé

  • v1.0 : premier modèle validé en production
  • v1.1 : correction de données (mêmes hyperparamètres)
  • v2.0 : changement de modèle de base ou de stratégie
  • v2.1 : ajustement d’hyperparamètres

Registre de modèles

Maintenez un registre centralisé de tous vos modèles :

import json
import datetime

class ModelRegistry:
    """Registre des modèles fine-tunés."""

    def __init__(self, chemin: str = "model_registry.json"):
        self.chemin = chemin
        try:
            with open(chemin, "r") as f:
                self.registre = json.load(f)
        except FileNotFoundError:
            self.registre = {"modeles": [], "production": None}

    def enregistrer(
        self,
        version: str,
        model_id: str,
        job_id: str,
        dataset_info: dict,
        hyperparams: dict,
        metriques: dict,
        notes: str = ""
    ):
        """Enregistre un nouveau modèle dans le registre."""
        entree = {
            "version": version,
            "model_id": model_id,
            "job_id": job_id,
            "date": datetime.datetime.now().isoformat(),
            "dataset": dataset_info,
            "hyperparametres": hyperparams,
            "metriques": metriques,
            "notes": notes,
            "statut": "candidat"
        }
        self.registre["modeles"].append(entree)
        self._sauvegarder()
        print(f"Modèle {version} enregistré : {model_id}")

    def promouvoir(self, version: str):
        """Promeut un modèle en production."""
        ancien = self.registre["production"]
        for m in self.registre["modeles"]:
            if m["version"] == version:
                m["statut"] = "production"
                self.registre["production"] = version
                break

        if ancien:
            for m in self.registre["modeles"]:
                if m["version"] == ancien:
                    m["statut"] = "retiré"

        self._sauvegarder()
        print(f"Production : {ancien} -> {version}")

    def rollback(self, version: str):
        """Rollback vers une version précédente."""
        self.promouvoir(version)
        print(f"Rollback effectué vers {version}")

    def modele_production(self) -> str:
        """Retourne l'ID du modèle en production."""
        version = self.registre["production"]
        for m in self.registre["modeles"]:
            if m["version"] == version:
                return m["model_id"]
        return None

    def historique(self):
        """Affiche l'historique des modèles."""
        print(f"{'Version':<12} {'Statut':<12} {'Date':<12} {'Loss val':<10}")
        print("-" * 50)
        for m in self.registre["modeles"]:
            date = m["date"][:10]
            loss = m["metriques"].get("validation_loss", "N/A")
            print(f"{m['version']:<12} {m['statut']:<12} {date:<12} {loss}")

    def _sauvegarder(self):
        with open(self.chemin, "w") as f:
            json.dump(self.registre, f, indent=2, ensure_ascii=False)

Utilisation du registre

registry = ModelRegistry()

# Enregistrer un nouveau modèle
registry.enregistrer(
    version="v2.1",
    model_id="ft:gpt-5.3-mini:org::support-v2.1:abc123",
    job_id="ftjob-xyz789",
    dataset_info={"train": 350, "validation": 45},
    hyperparams={"n_epochs": 3, "lr": "auto"},
    metriques={"validation_loss": 0.38, "precision_test": 0.93},
    notes="Ajout de 50 exemples correctifs pour les remboursements"
)

# Promouvoir en production
registry.promouvoir("v2.1")

# En cas de problème, rollback
registry.rollback("v2.0")

Rollback en production

Mécanisme de rollback rapide

import os

def deployer_modele(version: str, registry: ModelRegistry):
    """Déploie un modèle en mettant à jour la variable d'environnement."""
    model_id = None
    for m in registry.registre["modeles"]:
        if m["version"] == version:
            model_id = m["model_id"]
            break

    if not model_id:
        raise ValueError(f"Version {version} non trouvée")

    # Mettre à jour la configuration
    os.environ["OPENAI_FT_MODEL"] = model_id
    registry.promouvoir(version)

    print(f"Modèle déployé : {version} ({model_id})")
    return model_id

def rollback_rapide(registry: ModelRegistry):
    """Rollback vers la dernière version stable."""
    production = registry.registre["production"]
    versions = [
        m for m in registry.registre["modeles"]
        if m["version"] != production and m["statut"] != "retiré"
    ]

    if not versions:
        # Fallback sur le modèle de base
        os.environ["OPENAI_FT_MODEL"] = "gpt-5.3-mini"
        print("Rollback vers le modèle de base")
        return

    derniere_stable = versions[-1]
    deployer_modele(derniere_stable["version"], registry)

Sauvegarder les données d’entraînement

Versionnez aussi vos données, pas seulement vos modèles :

import shutil

def archiver_dataset(version: str, fichiers: list[str], dossier_archive: str):
    """Archive les données d'entraînement pour une version donnée."""
    dossier = f"{dossier_archive}/{version}"
    os.makedirs(dossier, exist_ok=True)

    for fichier in fichiers:
        shutil.copy2(fichier, dossier)

    print(f"Données archivées dans {dossier}")

archiver_dataset(
    "v2.1",
    ["train.jsonl", "validation.jsonl", "test.jsonl"],
    "archives/datasets"
)

Automatiser le pipeline

Script de déploiement complet

def pipeline_deploiement(
    training_file: str,
    validation_file: str,
    version: str,
    modele_base: str = "gpt-5.3-mini",
    hyperparams: dict = None
):
    """Pipeline complet : upload, train, évaluer, enregistrer."""
    client = OpenAI()
    registry = ModelRegistry()

    # 1. Upload
    print("Upload des données...")
    train = client.files.create(file=open(training_file, "rb"), purpose="fine-tune")
    val = client.files.create(file=open(validation_file, "rb"), purpose="fine-tune")

    # 2. Entraînement
    print("Lancement du fine-tuning...")
    params = {"training_file": train.id, "validation_file": val.id,
              "model": modele_base, "suffix": version}
    if hyperparams:
        params["hyperparameters"] = hyperparams

    job = client.fine_tuning.jobs.create(**params)

    # 3. Attendre la fin
    import time
    while True:
        job = client.fine_tuning.jobs.retrieve(job.id)
        if job.status in ("succeeded", "failed"):
            break
        time.sleep(30)

    if job.status == "failed":
        print(f"Échec : {job.error}")
        return None

    # 4. Enregistrer
    registry.enregistrer(
        version=version,
        model_id=job.fine_tuned_model,
        job_id=job.id,
        dataset_info={"train": training_file, "validation": validation_file},
        hyperparams=hyperparams or {},
        metriques={"trained_tokens": job.trained_tokens}
    )

    # 5. Archiver les données
    archiver_dataset(version, [training_file, validation_file], "archives/datasets")

    print(f"Pipeline terminé : {job.fine_tuned_model}")
    return job.fine_tuned_model

Supprimer les anciens modèles

Les modèles fine-tunés sont stockés chez OpenAI et peuvent être supprimés quand vous n’en avez plus besoin :

def nettoyer_anciens_modeles(registry: ModelRegistry, garder: int = 3):
    """Supprime les anciens modèles en gardant les N derniers."""
    modeles = registry.registre["modeles"]
    production = registry.registre["production"]

    candidats = [
        m for m in modeles
        if m["version"] != production
    ]

    a_supprimer = candidats[:-garder] if len(candidats) > garder else []

    for m in a_supprimer:
        try:
            client.models.delete(m["model_id"])
            print(f"Supprimé : {m['version']} ({m['model_id']})")
        except Exception as e:
            print(f"Erreur suppression {m['version']} : {e}")

Points clés à retenir

  • Utilisez des suffixes versionnés pour identifier chaque modèle
  • Maintenez un registre centralisé avec métriques et métadonnées
  • Implémentez un mécanisme de rollback rapide
  • Archivez les données d’entraînement avec chaque version
  • Nettoyez régulièrement les anciens modèles pour éviter les coûts inutiles
  • Automatisez le pipeline pour réduire les erreurs humaines