> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreweave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Entraînement par RL

> Utilisez des bacs à sable comme environnements d’entraînement par apprentissage par renforcement.

Ce guide explique comment utiliser des bacs à sable sans serveur dans des workflows d’entraînement par apprentissage par renforcement (RL), où les modèles exécutent des appels d’outils dans des environnements isolés. Il s’adresse aux ingénieurs ML et aux chercheurs qui conçoivent des pipelines d’entraînement d’agents et ont besoin d’environnements éphémères et sécurisés pour évaluer les actions générées par les modèles et calculer les récompenses.

<h2 id="why-sandboxes-for-agent-tool-execution">
  Pourquoi utiliser des bacs à sable pour l’exécution d’outils des agents
</h2>

L’entraînement d’agents de code par RL nécessite d’exécuter des appels d’outils (commandes bash, opérations sur les fichiers) dans des environnements isolés. Le code généré par le modèle n’étant pas fiable, il peut modifier le système de fichiers de l’hôte, accéder au réseau ou produire des résultats non déterministes. Les bacs à sable fournissent des environnements isolés et éphémères dans lesquels les appels d’outils s’exécutent sans affecter l’hôte ni les autres rollouts.

Dans une boucle d’entraînement, le modèle génère des actions (appels d’outils), la sandbox les exécute, puis les observations sont renvoyées au modèle. La sandbox est conservée d’un appel d’outil à l’autre au sein d’un épisode : les modifications de fichiers et les paquets installés persistent ainsi d’une étape à l’autre. La récompense est calculée à partir de l’état final de la sandbox (par exemple, la réussite des tests) ou de la qualité de la trajectoire.

Grâce aux API d’étiquetage et de listage, il devient simple de nettoyer et de surveiller des runs d’entraînement comportant des milliers de bacs à sable.

<h2 id="prerequisites">
  Prérequis
</h2>

Avant de commencer, effectuez les étapes de configuration suivantes.

Définissez `CWSANDBOX_API_KEY` sur un
[jeton d’accès à l’API CoreWeave](/security/authn-authz/manage-api-access-tokens) :

```bash theme={"system"}
export CWSANDBOX_API_KEY="[API-ACCESS-TOKEN]"
```

Installez le SDK Python :

```bash theme={"system"}
uv pip install cwsandbox
```

Les exemples de cette page créent des bacs à sable à partir de `SandboxDefaults` et utilisent par défaut l’authentification CoreWeave. Pour utiliser une clé API Forge, configurez explicitement l’authentification en suivant les instructions de la page [Premiers pas](/fr/products/sandboxes/get-started), puis transmettez la valeur `auth` obtenue à chaque `SandboxDefaults(...)`.

<h2 id="core-pattern">
  Principe de base
</h2>

La configuration de base repose sur une boucle d’agent qui s’exécute sur votre infrastructure d’entraînement, tandis que les appels d’outils s’exécutent dans une sandbox.

```python theme={"system"}
import cwsandbox
from cwsandbox import Sandbox

def run_agent_episode(model, task: dict, sandbox: Sandbox) -> tuple[list, float]:
    """Run one agent episode, returning trajectory and reward."""
    messages = [{"role": "user", "content": task["prompt"]}]

    for step in range(task.get("max_steps", 10)):
        # Le modèle génère l’action suivante
        response = model.generate(messages)
        messages.append({"role": "assistant", "content": response})

        tool_calls = parse_tool_calls(response)
        if not tool_calls:
            break

        # Exécuter les appels d’outils dans la sandbox
        for tool in tool_calls:
            if tool.name == "bash":
                result = sandbox.exec(
                    ["bash", "-c", tool.command],
                    timeout_seconds=30.0,
                ).result()
                observation = f"exit={result.returncode}\n{result.stdout}{result.stderr}"
            elif tool.name == "read_file":
                content = sandbox.read_file(tool.path).result()
                observation = content.decode()
            elif tool.name == "write_file":
                sandbox.write_file(tool.path, tool.content.encode()).result()
                observation = "File written successfully"

            messages.append({"role": "tool", "name": tool.name, "content": observation})

    # Calculer la récompense à partir de l’état final de la sandbox
    test_result = sandbox.exec(task["test_command"]).result()
    reward = 1.0 if test_result.returncode == 0 else 0.0

    return messages, reward
```

La sandbox persiste d’un appel d’outil à l’autre au sein d’un épisode, si bien que les modifications de fichiers s’accumulent au fil du travail de l’agent.

<h3 id="training-step-with-parallel-episodes">
  Étape d’entraînement avec épisodes en parallèle
</h3>

Traitez un lot de tâches en utilisant une sandbox par épisode :

```python theme={"system"}
def training_step(model, batch: list[dict], session) -> list[float]:
    """Run agent episodes for a batch of tasks."""

    # Créer et prédémarrer tous les bacs à sable en parallèle
    sandboxes = [session.sandbox() for _ in batch]
    refs = [sb.start() for sb in sandboxes]
    [r.result() for r in refs]  # Attendre que tous les backends aient accepté

    trajectories = []
    rewards = []

    for task, sandbox in zip(batch, sandboxes):
        trajectory, reward = run_agent_episode(model, task, sandbox)
        trajectories.append(trajectory)
        rewards.append(reward)
        sandbox.stop()  # Nettoyage non-blocking

    # Les trajectoires et les récompenses alimentent la mise à jour de la politique
    return rewards
```

<h2 id="tagging-for-job-metadata">
  Étiquetage des métadonnées de tâche
</h2>

Les étiquettes vous permettent de filtrer et de retrouver les bacs à sable créés par vos tâches d’entraînement, ce qui devient indispensable lorsqu’un run unique peut lancer des milliers de bacs à sable. Ajoutez des métadonnées qui facilitent l’identification des bacs à sable lors du débogage ou du nettoyage :

```python theme={"system"}
import os
from cwsandbox import SandboxDefaults

def make_defaults(model_name: str) -> SandboxDefaults:
    return SandboxDefaults(
        container_image="python:3.11",
        tags=(
            f"training-run:{os.environ.get('TRAINING_RUN_ID', 'local')}",
            f"slurm-job:{os.environ.get('SLURM_JOB_ID', 'interactive')}",
            f"model:{model_name}",
            "rl-training",
        ),
    )
```

Métadonnées utiles à inclure dans les étiquettes :

| Modèle d’étiquette | Rôle |
| - | - |
| `training-run:{id}` | ID du run d’entraînement (issu de `TRAINING_RUN_ID`), pour filtrer par run d’entraînement |
| `slurm-job:{id}` | ID de la tâche Slurm (issu de la variable d’environnement `SLURM_JOB_ID`), pour le suivi des tâches du cluster |
| `model:{name}` | Nom du modèle ou du point de contrôle, pour les expériences portant sur plusieurs modèles |
| `env:{name}` | Environnement (dév, staging, prod), pour la gestion des ressources |

Les étiquettes de sandbox deviennent des libellés de pod Kubernetes, que la plateforme d’observabilité CoreWeave exploite pour le filtrage et les tableaux de bord.

<h2 id="try-the-reward-function-example">
  Essayer l’exemple de fonction de récompense
</h2>

Une intégration minimale qui calcule des récompenses d’exécution de code en exécutant le code en parallèle dans des sandbox.

**Fonctionnement :**

* Exécute un ensemble de complétions de code simples (arithmétique, opérations sur les chaînes de caractères, erreurs de syntaxe, erreurs d’exécution).
* Crée une sandbox par complétion afin de garantir l’isolation.
* Calcule des récompenses binaires : 1.0 en cas d’exécution réussie, 0.0 en cas d’échec.
* Affiche la progression au fur et à mesure de l’arrivée des résultats (les exécutions les plus rapides se terminent en premier).

**Utilisation des bacs à sable CoreWeave :**

L’exemple utilise `cwsandbox.wait()` pour traiter les résultats dès qu’ils sont disponibles :

```python theme={"system"}
# Créer les sandboxes et exécuter toutes les complétions en parallèle
processes = [
    session.sandbox().exec(
        ["python", "-c", code],
        timeout_seconds=EXECUTION_TIMEOUT_SECONDS,
    )
    for code in completions
]

# Collecter les résultats au fur et à mesure qu’ils se terminent
while pending:
    [process], pending = cwsandbox.wait(pending, num_returns=1)
    result = process.result()
    reward = 1.0 if result.returncode == 0 else 0.0
```

**Exécutez-le :**

```bash theme={"system"}
uv run examples/rl_training/reward_function.py
```

Aucune dépendance supplémentaire n’est requise. Aucun GPU n’est nécessaire.

**Sortie attendue :**

Les résultats s’affichent au fur et à mesure que les exécutions se terminent, si bien que les problèmes les plus rapides sont traités en premier :

```text theme={"system"}
RL Training Reward Function Example (job: cc92116b)
============================================================

Evaluating 5 completions...

Progress (results arrive as executions complete):
------------------------------------------------------------
  [1/5] Problem 1 (string-ops): PASS
  [2/5] Problem 3 (syntax-error): FAIL
  [3/5] Problem 2 (delayed-error): FAIL
  [4/5] Problem 0 (slow-sum): PASS
  [5/5] Problem 4 (slow-list): PASS
------------------------------------------------------------

Final summary (original order):
------------------------------------------------------------
  Problem 0 (slow-sum): reward=1.0 [PASS] OK
  Problem 1 (string-ops): reward=1.0 [PASS] OK
  Problem 2 (delayed-error): reward=0.0 [FAIL] OK
  Problem 3 (syntax-error): reward=0.0 [FAIL] OK
  Problem 4 (slow-list): reward=1.0 [PASS] OK
------------------------------------------------------------
Total reward: 3.0/5
Pass rate: 3/5 (60%)
```

<h2 id="trl-grpotrainer-integration">
  Intégration de TRL GRPOTrainer
</h2>

TRL utilise une interface de fonction de récompense dans laquelle chaque complétion est directement associée à une récompense. L’agent génère une complétion, puis la fonction de récompense l’exécute dans une sandbox.

L’approche standard utilise des balises XML `<answer>` pour extraire le code (comme dans les exemples mathématiques GRPO, qui utilisent `\boxed{}`) :

```python theme={"system"}
import cwsandbox
from cwsandbox import SandboxDefaults

session = cwsandbox.Session(defaults=SandboxDefaults(
    container_image="python:3.11",
    tags=("trl-grpo",),
))

def extract_xml_answer(text: str) -> str:
    if "<answer>" not in text:
        return ""
    return text.split("<answer>")[-1].split("</answer>")[0].strip()

def reward_fn(completions, **kwargs) -> list[float]:
    # Les datasets conversationnels transmettent des dicts de messages, les datasets standard des chaînes de caractères
    texts = [c[0]["content"] if isinstance(c, list) else c for c in completions]
    codes = [extract_xml_answer(t) for t in texts]
    code_indices = [(i, code) for i, code in enumerate(codes) if code]

    processes = [
        (i, session.sandbox().exec(
            ["python", "-c", code],
            timeout_seconds=30.0,
        ))
        for i, code in code_indices
    ]

    rewards = [0.0] * len(codes)
    for i, process in processes:
        try:
            rewards[i] = 1.0 if process.result().returncode == 0 else 0.0
        except Exception:
            pass

    return rewards
```

Cette approche convient à l’entraînement de modèles chargés de générer du code correct en un seul tour de conversation.

<h2 id="try-the-trl-grpo-integration-example">
  Essayer l’exemple d’intégration TRL GRPO
</h2>

Cet exemple utilise des bacs à sable CoreWeave avec le GRPOTrainer de TRL pour attribuer des récompenses fondées sur l’exécution du code.

**Ce que fait cet exemple :**

* Il charge un petit modèle (`Qwen/Qwen2.5-0.5B-Instruct`).
* Il crée un dataset jouet de problèmes de programmation simples.
* Il entraîne le modèle avec GRPO en calculant les récompenses dans des bacs à sable.
* Il exécute 10 étapes d’entraînement pour illustrer l’intégration.

**Utilisation des bacs à sable CoreWeave :**

La fonction de récompense extrait le code des balises `<answer>` (le schéma GRPO standard), crée des bacs à sable en parallèle via une Session, exécute chaque complétion et renvoie des récompenses binaires :

```python theme={"system"}
def extract_xml_answer(text: str) -> str:
    """Extract answer from XML-style <answer> tags."""
    if "<answer>" not in text:
        return ""
    answer = text.split("<answer>")[-1]
    answer = answer.split("</answer>")[0]
    return answer.strip()

def code_execution_reward(completions, **kwargs) -> list[float]:
    # Les datasets conversationnels transmettent des dicts de messages, les datasets standard des chaînes de caractères
    texts = [c[0]["content"] if isinstance(c, list) else c for c in completions]
    codes = [extract_xml_answer(t) for t in texts]

    # Créer les sandboxes et exécuter en parallèle le code non vide
    processes = [
        (i, session.sandbox().exec(
            ["python", "-c", code],
            timeout_seconds=EXECUTION_TIMEOUT_SECONDS,
        ))
        for i, code in enumerate(codes) if code
    ]

    # Collecter les récompenses (0.0 par défaut)
    rewards = [0.0] * len(codes)
    for i, process in processes:
        try:
            result = process.result()
            rewards[i] = 1.0 if result.returncode == 0 else 0.0
        except Exception:
            rewards[i] = 0.0

    return rewards
```

Les prompts utilisent un message système qui demande au modèle de formater le code avec des balises `<answer>` :

```python theme={"system"}
SYSTEM_PROMPT = """You solve coding problems by writing Python code.
Put your code inside <answer> tags like this: <answer>print("hello")</answer>
Only include the code, no explanations."""
```

La Session suit les bacs à sable et les nettoie à sa fermeture.

**Exécutez-le :**

```bash theme={"system"}
uv pip install trl==0.27.1 transformers==5.0.0 datasets==4.5.0 torch==2.10.0
uv run examples/rl_training/trl_grpo_integration.py
```

Un GPU est recommandé pour obtenir des performances correctes. Sans GPU, l’entraînement fonctionne, mais il est lent.

**Sortie attendue :**

```text theme={"system"}
TRL GRPO Integration Example (job: def67890)
============================================================

Loading model: Qwen/Qwen2.5-0.5B-Instruct
Creating toy dataset...
Dataset size: 5 problems

Setting up GRPOTrainer...

Starting training (10 steps)...
------------------------------------------------------------
  [CWSandbox] Reward call 1: 2 sandboxes, 0/2 passed
  [CWSandbox] Reward call 2: 1 sandboxes, 0/1 passed, 1 skipped (no code)
  [CWSandbox] Reward call 3: 0 sandboxes, 0/0 passed, 2 skipped (no code)
  [CWSandbox] Reward call 4: 2 sandboxes, 0/2 passed
  ...
  [CWSandbox] Reward call 8: 2 sandboxes, 1/2 passed
  [CWSandbox] Reward call 9: 2 sandboxes, 1/2 passed
  [CWSandbox] Reward call 10: 1 sandboxes, 0/1 passed, 1 skipped (no code)
[training logs]
------------------------------------------------------------

Training completed successfully!
```

**Comprendre la sortie :**

Le nombre de bacs à sable varie d’une étape à l’autre, car le script ne crée des bacs à sable que lorsque `extract_xml_answer()` trouve du code extractible dans la complétion du modèle. Lorsque le modèle génère du texte sans les balises `<answer>...</answer>` attendues, cette complétion est ignorée et reçoit une récompense de 0,0.

* `2 sandboxes, 0/2 passed` : le modèle a généré 2 blocs de code, dont l’exécution a échoué dans les deux cas.
* `1 sandboxes, 0/1 passed, 1 skipped (no code)` : le modèle a généré 1 bloc de code (en échec) et 1 complétion contenant uniquement du texte.
* `0 sandboxes, 0/0 passed, 2 skipped (no code)` : le modèle n’a généré de code extractible dans aucune des deux complétions.

Ce comportement est normal avec un petit modèle non entraîné. Au fil de l’entraînement, vous devriez constater moins de complétions ignorées et davantage de réussites.

<h2 id="error-handling-in-agent-episodes">
  Gestion des erreurs dans les épisodes d’agent
</h2>

Les opérations de sandbox peuvent échouer (délais d’expiration, fichiers manquants, arrêt de la sandbox), et la façon dont vous remontez ces échecs détermine ce que l’agent apprend. Renvoyez des observations qui décrivent le problème rencontré :

```python theme={"system"}
from cwsandbox import SandboxTimeoutError, SandboxFileError

def execute_tool(sandbox, tool) -> str:
    """Execute a tool call, returning an observation string."""
    try:
        if tool.name == "bash":
            result = sandbox.exec(
                ["bash", "-c", tool.command],
                timeout_seconds=30.0,
            ).result()
            return f"exit={result.returncode}\n{result.stdout}{result.stderr}"
        elif tool.name == "read_file":
            content = sandbox.read_file(tool.path).result()
            return content.decode()
        elif tool.name == "write_file":
            sandbox.write_file(tool.path, tool.content.encode()).result()
            return "File written successfully"
    except SandboxTimeoutError:
        return "Error: command timed out after 30 seconds"
    except SandboxFileError as e:
        return f"Error: {e}"
    except Exception as e:
        return f"Error: {type(e).__name__}: {e}"
```

Lors du calcul de la récompense, interceptez les exceptions et renvoyez une récompense par défaut plutôt que de les propager jusqu’à la boucle d’entraînement.

<h2 id="wb-metrics-integration">
  Intégration des métriques Weights & Biases
</h2>

Lorsque vous utilisez Weights & Biases pour l’entraînement, les Sessions cwsandbox journalisent automatiquement les métriques d’utilisation des sandboxes dans votre run wandb actif. Vous pouvez ainsi corréler l’état de santé des sandboxes (temps de démarrage, échecs d’exécution, appels d’outils par enregistrement de session) avec vos métriques d’entraînement, dans un même tableau de bord.

Les sections suivantes décrivent le fonctionnement de la détection automatique, la manière de contrôler explicitement la remontée des métriques, ainsi que les métriques suivies.

<h3 id="auto-detection">
  Détection automatique
</h3>

Si le paquet `wandb` est installé et qu'un run Weights & Biases est actif (`wandb.run` existe), la journalisation des métriques est activée automatiquement. Pour le suivi des expériences, installez le paquet facultatif et définissez une clé API Forge :

```bash theme={"system"}
uv pip install wandb
export FORGE_API_KEY="[FORGE-API-KEY]"
```

Transmettez explicitement la clé à `wandb.login()`. Cet exemple lit `FORGE_API_KEY`, mais le SDK ne la détecte pas automatiquement. L'authentification de la sandbox utilise toujours `CWSANDBOX_API_KEY` :

```python theme={"system"}
import os

import wandb
from cwsandbox import Session, SandboxDefaults

wandb.login(key=os.environ["FORGE_API_KEY"])
wandb.init(project="my-rl-training")
defaults = SandboxDefaults(container_image="python:3.11")
code = "print('Training tool call')"

# Journalisation des métriques activée automatiquement
with Session(defaults) as session:
    for step in range(3):
        sandbox = session.sandbox()
        # Les résultats d’exécution sont suivis automatiquement : aucun appel manuel n’est nécessaire
        result = sandbox.exec(["python", "-c", code]).result()
        # Journaliser les métriques à cette étape d’entraînement pour pouvoir les corréler
        session.log_metrics(step=step)
# Métriques finales journalisées à la fermeture de la session
```

<h3 id="explicit-control">
  Contrôle explicite
</h3>

Contrôlez l’envoi des métriques à l’aide du paramètre `report_to` :

```python theme={"system"}
# Activation explicite (crée un reporter ; les métriques sont journalisées lorsque wandb.run existe)
session = Session(defaults, report_to=["wandb"])

# Désactiver le reporting (même si un run wandb existe)
session = Session(defaults, report_to=[])

# Détection automatique (comportement par défaut)
session = Session(defaults, report_to=None)
```

<h3 id="metrics">
  Métriques
</h3>

Les métriques d’exécution sont suivies automatiquement à la fin de `exec()` :

| Métrique | Description |
| - | - |
| `cwsandbox/sandboxes_created` | Nombre total de bacs à sable créés via la session |
| `cwsandbox/executions` | Nombre total d’appels à exec() |
| `cwsandbox/exec_completed_ok` | Exécutions terminées (returncode=0) |
| `cwsandbox/exec_completed_nonzero` | Exécutions terminées (returncode!=0) |
| `cwsandbox/exec_failures` | Exécutions échouées (délais d’expiration, échecs de transport) |
| `cwsandbox/exec_completion_rate` | Proportion d’appels à exec() terminés avec returncode=0 |
| `cwsandbox/exec_failure_rate` | Proportion d’appels à exec() qui n’ont pas abouti |
| `cwsandbox/startup_count` | Nombre de temps de démarrage de sandbox enregistrés |
| `cwsandbox/avg_startup_seconds` | Temps de démarrage moyen d’une sandbox |
| `cwsandbox/min_startup_seconds` | Temps de démarrage minimum d’une sandbox |
| `cwsandbox/max_startup_seconds` | Temps de démarrage maximum d’une sandbox |

Le suivi est automatique. Appelez `exec()` sur n’importe quelle sandbox associée à une session. Appelez `session.log_metrics(step=N)` pour journaliser les métriques à des étapes d’entraînement précises :

```python theme={"system"}
def training_step(session, model, batch, step: int) -> list[float]:
    rewards = []
    for task in batch:
        sandbox = session.sandbox()
        # Métriques suivies automatiquement à la fin de l’exécution de exec()
        result = sandbox.exec(["python", "-c", task["code"]]).result()
        reward = 1.0 if result.returncode == 0 else 0.0
        rewards.append(reward)
        sandbox.stop()

    # Journaliser les métriques à cette étape d’entraînement pour pouvoir les corréler
    session.log_metrics(step=step)
    return rewards
```

Vous pouvez également accéder aux statistiques de chaque sandbox via la propriété `exec_stats` :

```python theme={"system"}
sandbox = session.sandbox()
result = sandbox.exec(["echo", "hello"]).result()
print(sandbox.exec_stats)  # {"exec_count": 1, "completed_ok": 1, "completed_nonzero": 0, "failures": 0}
```

<h3 id="per-sandbox-exec-metrics">
  Métriques exec par sandbox
</h3>

Les sessions dotées de l’intégration Weights & Biases suivent également des métriques par sandbox :

| Métrique | Description |
| - | - |
| `cwsandbox/avg_execs_per_sandbox` | Nombre moyen d’appels exec() par sandbox (utile pour mesurer les « appels d’outils par enregistrement de session ») |
| `cwsandbox/min_execs_per_sandbox` | Nombre minimum d’appels exec() parmi toutes les sandboxes |
| `cwsandbox/max_execs_per_sandbox` | Nombre maximum d’appels exec() parmi toutes les sandboxes |

Ce que ces métriques révèlent sur le comportement de l’agent :

* **Une valeur élevée de avg\_execs\_per\_sandbox** peut signaler des agents verbeux qui effectuent de nombreux appels d’outils par épisode.
* **Un écart important (max-min)** peut signaler un comportement de rollout irrégulier d’un épisode à l’autre.
* **Les tendances au fil des étapes d’entraînement** montrent comment le comportement de l’agent évolue à mesure que la politique s’améliore.

Exemple d’utilisation dans un tableau de bord :

* Tracez `avg_execs_per_sandbox` en fonction de l’étape d’entraînement pour suivre l’évolution de l’utilisation des outils au cours de l’entraînement.
* Déclenchez une alerte si `max_execs_per_sandbox` dépasse un seuil (agent incontrôlé effectuant un nombre excessif d’appels d’outils).
* Comparez l’écart min/max pour distinguer les épisodes où les agents restent bloqués dans des boucles de ceux qui se terminent rapidement.

Par défaut, `log_metrics()` réinitialise les compteurs après la journalisation. Définissez `reset=False` pour continuer à les cumuler :

```python theme={"system"}
session.log_metrics(step=step, reset=False)  # Poursuivre l’accumulation
```

Les métriques sont également journalisées automatiquement à la fermeture de la session : vous obtenez ainsi les métriques récapitulatives finales, même sans journalisation explicite.

<h2 id="monitoring-and-debugging">
  Surveillance et débogage
</h2>

Les approches suivantes vous permettent d’observer en temps réel ce que fait votre run d’entraînement et de capturer des informations qui facilitent le diagnostic des échecs.

<h3 id="counting-active-sandboxes">
  Compter les bacs à sable actifs
</h3>

Surveillez l’utilisation des bacs à sable pendant l’entraînement :

```python theme={"system"}
from cwsandbox import Sandbox, SandboxStatus

def count_active_sandboxes(run_id: str) -> dict:
    sandboxes = Sandbox.list(
        tags=[f"training-run:{run_id}"],
        status=[SandboxStatus.RUNNING, SandboxStatus.PENDING],
    ).result()

    return {
        "running": sum(1 for s in sandboxes if s.status == SandboxStatus.RUNNING),
        "pending": sum(1 for s in sandboxes if s.status == SandboxStatus.PENDING),
        "total": len(sandboxes),
    }
```

<h3 id="logging-execution-details">
  Journalisation des détails d’exécution
</h3>

Capturez les détails d’exécution pour déboguer le calcul des récompenses :

```python theme={"system"}
import logging

logger = logging.getLogger(__name__)

# Suppose que `session` a été créée au niveau du module

def logged_reward(completion: str, step: int) -> float:
    code = extract_xml_answer(completion)  # Extrait le code des balises <answer>

    sandbox = session.sandbox()
    sandbox.wait()
    result = sandbox.exec(
        ["python", "-c", code],
        timeout_seconds=30.0,
    ).result()

    logger.debug(
        "Reward computation",
        extra={
            "step": step,
            "sandbox_id": sandbox.sandbox_id,
            "returncode": result.returncode,
            "stdout_len": len(result.stdout),
            "stderr_len": len(result.stderr),
        },
    )

    sandbox.stop()
    return 1.0 if result.returncode == 0 else 0.0
```

<h2 id="multi-step-rollouts-with-art">
  Rollouts multi-étapes avec ART
</h2>

L’exemple TRL précédent utilise des bacs à sable pour une exécution en une seule passe : un sandbox par complétion, une seule exécution, puis le renvoi d’une récompense. Cette approche convient pour entraîner des modèles à générer du code correct du premier coup.

Les rollouts multi-étapes avec état fonctionnent différemment : l’agent effectue plusieurs actions au sein d’un même sandbox, qui conserve son état d’une action à l’autre. L’agent peut écrire un fichier, l’exécuter, observer l’erreur, modifier le fichier et réessayer, le tout dans le même sandbox.

Le répertoire `examples/rl_training/art/` illustre ce schéma sur le benchmark MBPP. Lorsqu’une solution échoue, l’agent reçoit un retour sur l’erreur et peut affiner son approche par itérations successives.

<h3 id="overview">
  Aperçu
</h3>

La suite de cette section présente ART, l’approche d’entraînement adoptée par l’exemple, les prérequis, l’installation, la procédure d’exécution de l’exemple et la manière dont les composants s’articulent.

[ART (Agent Reinforcement Trainer)](https://github.com/OpenPipe/ART) est un framework de RL open source développé par OpenPipe pour entraîner des agents multi-étapes avec GRPO. Cet exemple intègre les bacs à sable CoreWeave à ART :

* Utilise le paquet `art` (`openpipe-art`) pour la collecte de trajectoires et l’entraînement.
* Prend en charge deux backends : `LocalBackend` (GPU requis) ou `TinkerBackend` (sans GPU).
* Exécute du code par appel d’outils dans une sandbox CoreWeave.
* Calcule des récompenses binaires basées sur les résultats des cas de test MBPP.

<h3 id="training-approach-with-grpo-and-distillation">
  Approche d’entraînement avec GRPO et distillation
</h3>

Cet exemple combine la distillation et l’apprentissage par renforcement : un modèle plus performant génère des démonstrations, et un modèle plus petit apprend à reproduire celles qui aboutissent.

**Deux modèles interviennent :**

1. **Modèle d’inférence** (`--model`, par défaut : `gpt-5.1-codex-mini`) : génère des trajectoires pendant les rollouts. Ce modèle effectue des appels d’outils, consulte les résultats de la sandbox, corrige ses erreurs par itérations successives et soumet des solutions. Il n’est pas entraîné.

2. **Modèle de base** (`--base-model`, par défaut : `Qwen/Qwen3-8B`) : le modèle entraîné. Il reçoit les trajectoires générées par le modèle d’inférence et en tire des enseignements grâce à GRPO (Group Relative Policy Optimization).

**Fonctionnement :**

1. Le modèle d’inférence génère plusieurs trajectoires par problème, chacune comportant des appels d’outils exécutés dans une sandbox CoreWeave.
2. Chaque trajectoire reçoit une récompense binaire : 1.0 si les tests réussissent, 0.0 sinon.
3. Les trajectoires d’un même problème forment un groupe, au sein duquel GRPO les compare.
4. Le modèle de base (Qwen3-8B) est entraîné à privilégier les trajectoires les mieux récompensées par rapport aux autres.

Après l’entraînement, vous déployez Qwen3-8B avec les mêmes définitions d’outils. En imitant les trajectoires réussies du modèle d’inférence, il a appris à effectuer des appels d’outils similaires.

<h3 id="prerequisites-2">
  Prérequis
</h3>

| Mode | Exigences |
| - | - |
| Simulation à blanc | CPU uniquement |
| TinkerBackend | CPU uniquement (entraînement via l’API) |
| LocalBackend | GPU requis |

Variables d’environnement :

```bash theme={"system"}
export OPENAI_API_KEY="your-openai-key"
export ART_TINKER_API_KEY="your-tinker-key"  # requis pour --backend=tinker
export CWSANDBOX_API_KEY="[API-ACCESS-TOKEN]"  # authentification auprès de la sandbox
```

Pour journaliser dans Weights & Biases (facultatif), définissez une clé API Forge :

```bash theme={"system"}
export FORGE_API_KEY="[FORGE-API-KEY]"
```

ART 0.5.7 vérifie l’ancienne variable `WANDB_API_KEY` avant d’activer son intégration de journalisation. Ajoutez ce mappage de compatibilité dans `train.py`, avant la création du backend. La clé correspond à une clé API Forge :

```python theme={"system"}
import os

if api_key := os.environ.get("FORGE_API_KEY"):
    os.environ["WANDB_API_KEY"] = api_key
```

<h3 id="installation">
  Installation
</h3>

```bash theme={"system"}
uv pip install -r examples/rl_training/art/requirements.txt
```

Cette commande installe :

```text theme={"system"}
openpipe-art==0.5.7       # Framework ART
openai==2.15.0            # Inférence LLM
datasets==4.5.0           # Chargement de MBPP
wandb==0.24.0             # Journalisation facultative
```

Pour LocalBackend avec prise en charge des GPU, installez également :

```bash theme={"system"}
uv pip install "openpipe-art[backend]==0.5.7"
```

<h3 id="running-the-example">
  Exécuter l’exemple
</h3>

```bash theme={"system"}
# Simulation à blanc : valider la configuration sans lancer l’entraînement
uv run examples/rl_training/art/train.py --dry-run

# Entraîner avec TinkerBackend (aucun GPU local requis)
uv run examples/rl_training/art/train.py --backend tinker --num-problems 10

# Entraîner avec LocalBackend (GPU requis)
uv run examples/rl_training/art/train.py --backend local --num-problems 10
```

Sortie attendue :

```text theme={"system"}
ART Training with CWSandbox
========================================
Backend: tinker
Model: gpt-5.1-codex-mini
Base model: Qwen/Qwen3-8B
Problems: 10
Steps: 5
Trajectories per problem: 2
Project: cwsandbox-mbpp
Run name: train-001

Loading MBPP problems...
Loaded 10 problems

Creating tinker backend...
Creating trainable model...
Registering model with backend...

Starting training...

=== Step 1 ===
Collecting trajectories for 10 problems...
step 1: 100%|██████████| 10/10 [00:45<00:00]
Collected 20 trajectories, avg reward: 0.35
Training...
Training complete: step=1, metrics={'loss': 0.42}
...
```

<h3 id="configuration-options">
  Options de configuration
</h3>

| Option | Par défaut | Description |
| - | - | - |
| `--backend` | `local` | Backend d’entraînement : `local` (GPU) ou `tinker` (sans GPU) |
| `--model` | `gpt-5.1-codex-mini` | Modèle utilisé pour l’inférence |
| `--base-model` | `Qwen/Qwen3-8B` | Modèle de base à entraîner |
| `--num-problems` | `10` | Nombre de problèmes MBPP |
| `--num-steps` | `5` | Nombre d’étapes d’entraînement |
| `--trajectories-per-problem` | `2` | Trajectoires collectées par problème et par étape |
| `--base-url` | `None` | URL de base de l’API compatible avec OpenAI |
| `--project` | `cwsandbox-mbpp` | Nom du projet Weights & Biases |
| `--run-name` | `train-001` | Nom du run d’entraînement |
| `--learning-rate` | `1e-5` | Taux d’apprentissage |
| `--dry-run` | `false` | Valider la configuration sans lancer l’entraînement |

<h3 id="architecture">
  Architecture
</h3>

```text theme={"system"}
art/
├── train.py         # Boucle d’entraînement, CLI et configuration du backend ART
├── rollout.py       # Exécution multi-étapes dans la sandbox, construction de la Trajectory
├── tools.py         # Schémas d’outils pour execute_code et submit_solution
└── __init__.py
```

**Principaux imports d’ART :**

```python theme={"system"}
import art
from art.local import LocalBackend
from art.tinker import TinkerBackend

# Créer un modèle entraînable
model = art.TrainableModel(
    name="train-001",
    project="cwsandbox-mbpp",
    base_model="Qwen/Qwen3-8B",
)

# Collecter les trajectoires
groups = await art.gather_trajectory_groups(
    (collect_trajectories(problem) for problem in problems),
    pbar_desc="collecting",
)

# Entraîner
result = await backend.train(model, groups, learning_rate=1e-5)
```

**Le rollout renvoie `art.Trajectory` :**

```python theme={"system"}
from openai.types.chat import ChatCompletionToolParam

trajectory = art.Trajectory(
    messages_and_choices=messages_and_choices,  # Historique de la conversation
    tools=ROLLOUT_TOOLS,                        # Définitions d’outils
    reward=1.0 if passed else 0.0,              # Récompense binaire
    metadata={"task_id": problem.task_id},
)
return trajectory.finish()
```

**Modèle d’appel d’outils :**

Le rollout utilise l’appel d’outils compatible avec OpenAI, avec les deux outils suivants :

* `execute_code` : teste le code dans la sandbox et renvoie stdout/stderr.
* `submit_solution` : soumet la solution finale et exécute tous les cas de test.

```python theme={"system"}
ROLLOUT_TOOLS: list[ChatCompletionToolParam] = [
    {
        "type": "function",
        "function": {
            "name": "execute_code",
            "description": "Execute Python code in an isolated sandbox...",
            "parameters": {
                "type": "object",
                "properties": {"code": {"type": "string"}},
                "required": ["code"],
            },
        },
    },
    # Outil submit_solution...
]
```

Les bacs à sable sont tagués à des fins de suivi :

```python theme={"system"}
sandbox_defaults = SandboxDefaults(
    container_image="python:3.11",
    tags=("art-training", args.project, args.run_name),
)
```

<h3 id="data-flow">
  Flux de données
</h3>

Le pipeline d’entraînement comporte plusieurs composants :

1. **Machine locale** : le script d’entraînement (`train.py`) s’exécute sur votre machine ou votre serveur d’entraînement. Il charge les problèmes du dataset MBPP et orchestre la boucle d’entraînement.

2. **API OpenAI (inférence)** : pendant la collecte des trajectoires, le code du rollout appelle l’API OpenAI (ou un point de terminaison compatible) pour générer les réponses du modèle. Le modèle reçoit les définitions d’outils et renvoie des appels d’outils que le rollout exécute.

3. **Sandbox CoreWeave (exécution du code)** : chaque rollout utilise une seule sandbox CoreWeave, conservée pendant tous les appels d’outils. Lorsque le modèle appelle `execute_code` ou `submit_solution`, le code s’exécute dans cette sandbox. Les modifications de fichiers et l’état s’accumulent donc au fil des itérations de l’agent : il peut écrire un fichier, l’exécuter, constater une erreur et la corriger. La sandbox assure l’isolation, de sorte que le code non fiable généré par le modèle ne peut pas affecter l’hôte. Les résultats (stdout, stderr, code de sortie) sont renvoyés au rollout.

4. **Collecte des trajectoires** : le rollout accumule l’historique de la conversation (messages et résultats d’outils), ainsi que la récompense finale, dans un objet `art.Trajectory`. Plusieurs trajectoires portant sur un même problème forment un `art.TrajectoryGroup`.

5. **Backend d’entraînement** : les groupes de trajectoires collectés sont envoyés au backend d’entraînement. Avec `LocalBackend`, l’entraînement s’effectue sur votre GPU local. Avec `TinkerBackend`, les trajectoires sont téléversées vers le service Tinker de Thinking Machines, qui se charge de l’entraînement à distance. Aucun GPU local n’est alors nécessaire.
