diff --git a/MyIA.AI.Notebooks/RL/README.md b/MyIA.AI.Notebooks/RL/README.md index ac862b8620..15f53bf17d 100644 --- a/MyIA.AI.Notebooks/RL/README.md +++ b/MyIA.AI.Notebooks/RL/README.md @@ -64,7 +64,7 @@ Le RL se comprend mieux en voyant l'agent apprendre. Six visualisations suivent | pt-0c | [rlpt_0c_reward_hacking_case_study](rlpt_0c_reward_hacking_case_study.ipynb) | Anatomie d'un reward hacking **qui prend** — le pendant de `rlpt_3` : reward model Bradley-Terry appris sur des préférences à biais de longueur, PPO from scratch sur ce proxy, puis **bras de contrôle** (récompense longueur-contrôlée + oracle) qui identifie le biais comme cause. Mesures : qualité vraie 0,4545 → 0,0170 sous le proxy biaisé, contre 0,9905 sous contrôle ; basculement situé sur l'action de tête du proxy (argmax non correct à α ≈ 2,0), stable sur 4 graines | 35-40 min | | pt-0d | [rlpt_0d_reward_trainer_sota](rlpt_0d_reward_trainer_sota.ipynb) | Le **même** problème que `rlpt_0`, traité par le harnais industriel `trl.RewardTrainer` : monde, juge, architecture (5 441 paramètres) et seeds identiques, une seule variable — le harnais. Documente les **quatre exigences de contrat** non devinables (`num_labels=1`, `processing_class`, `gradient_checkpointing=False`, `self.post_init()`) et la traduction `y` → `chosen_ids`/`rejected_ids` ; verdict mesuré : `trl` **devance** la boucle maison (0.676 ± 0.007 vs 0.658 ± 0.008, écart +0.018 = 2.2σ ; Brier et Spearman meilleurs aussi) pour un coût en temps de **~10×** sur ce modèle minuscule, et l'évaluation honnête reste **entièrement à ré-écrire** — `trl` n'évalue pas pour vous | 35-40 min | | pt-0e | [rlpt_0e_trl_DPO_SOTA](rlpt_0e_trl_DPO_SOTA.ipynb) | `trl.DPOTrainer` (bras SOTA) contre DPO from scratch et reward model explicite de `rlpt_0` sur le **même monde et les mêmes paires de préférences**, à budget égal (même β, même lr, mêmes époques) — multi-seed {0,1,7,42} départagé au plafond de Bayes : verdict **B ≈ C > A** (DPO maison 0.679 / trl 0.678 / RM 0.661 vs plafond 0.695 ; B vs C indiscernables à ±0.008 près), marges implicites B↔C corrélées ρ≈0.98 = même math ; la différence robuste est le **coût fixe** de la pile (×22 sur jouet CPU) — inclut la garde de frontière prompt/complétion (piège trl : prompt sans espace finale) | 25-35 min | -| pt-0f | [rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison](rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb) | `trl.GRPOTrainer` (bras SOTA **en ligne**) contre le PPO maison de `rlpt_1` sur le **même monde et le même juge RM** (`rlpt_0`), à budget apparié (2 304 réponses évaluées par bras, même β_KL) — multi-seed {0,1,7,42} : verdict **INCONCLUSIVE** (gain maison 0.588±0.070 / trl 0.551±0.183, écart −0.037 < 2σ) ; la différence robuste est le **critic** (value net appris vs moyenne de groupe — la baseline est LE choix de design) et le coût d'écriture (60 vs 36 lignes, 61s vs 42s) — inclut le fait structurant mesuré dans l'env : `trl.PPOTrainer` a **disparu** de trl 1.10.0 (assert garde-frontière), la comparaison SOTA se fait contre le successeur factuel | 25-35 min | +| pt-0f | [rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison](rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb) | `trl.GRPOTrainer` (bras SOTA **en ligne**) contre le PPO maison de `rlpt_1` sur le **même monde et le même juge RM** (`rlpt_0`), à budget apparié (2 304 réponses évaluées par bras, même β_KL) — multi-seed {0,1,7,42} : verdict **INCONCLUSIVE** (gain maison 0.486±0.205 / trl 0.670±0.102, écart +0.184 < 2σ) ; la différence robuste est le **critic** (value net appris vs moyenne de groupe — la baseline est LE choix de design) et le coût d'écriture et de runtime (tableau comparatif section 7) — inclut le fait structurant mesuré dans l'env : `trl.PPOTrainer` a **quitté la racine** de trl (déplacé dans `trl.experimental.ppo`, assert garde-frontière sur la surface réelle) et le trainer historique **remis en service** en 3e bras (section 5bis : PPO complet critic + GAE, juge BT reconstruit au format lib, seed 0) | 30-45 min | | pt-1 | [rlpt_1_ppo_lm_rlhf](rlpt_1_ppo_lm_rlhf.html) | PPO pour alignement d'un petit LM (RLHF toy, from scratch, char-level) : reward model jouet, KL vs politique SFT de référence, multi-seed 4 — la signature RLHF, différenciée de rl_6c (PPO CartPole) et rl_6e (GRPO) | 40-45 min | | pt-2 | [rlpt_2_grpo_minimal](rlpt_2_grpo_minimal.html) | GRPO sur Qwen3.5-0.8B local (8 Go Viability), reward vérifiable, budget steps borné — le cœur « à la Deepseek » : group rollouts, avantage sans value net, pont #5105 | 45-55 min | | pt-3 | [rlpt_3_reward_hacking](rlpt_3_reward_hacking.html) | Reward hacking × inoculation, version compacte du capstone #5105 : le hack sur récompense vérifiable faillible, la détection rewardspy, l'inoculation comme variable expérimentale, verdict reproductible (seed fixée) | 35-40 min | diff --git a/MyIA.AI.Notebooks/RL/rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb b/MyIA.AI.Notebooks/RL/rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb index d1d3e0abdb..95d4ffe5c6 100644 --- a/MyIA.AI.Notebooks/RL/rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb +++ b/MyIA.AI.Notebooks/RL/rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb @@ -2,13 +2,13 @@ "cells": [ { "cell_type": "markdown", - "id": "bbdfe6dda3ce", + "id": "3ace6c299f50", "metadata": { "papermill": { - "duration": 0.004671, - "end_time": "2026-09-15T12:57:56.698304", + "duration": 0.013281, + "end_time": "2026-09-28T08:52:14.526974+00:00", "exception": false, - "start_time": "2026-09-15T12:57:56.693633", + "start_time": "2026-09-28T08:52:14.513693+00:00", "status": "completed" }, "tags": [] @@ -25,54 +25,72 @@ "à budget égal.\n", "\n", "Une découverte d'abord, mesurée dans la cellule d'environnement : **`trl.PPOTrainer`\n", - "n'existe plus**. L'issue #16063 (bloc B.6) demandait « `trl.PPOTrainer` sur la même\n", - "mini-tâche que rlpt_1 » — la lib a supprimé ce trainer en cours de route, et son pendent\n", - "industriel actuel est `trl.GRPOTrainer`. Ce notebook documente la succession, puis\n", - "compare :\n", + "a quitté la racine de la lib**. L'issue #16063 (bloc B.6) demandait « `trl.PPOTrainer`\n", + "sur la même mini-tâche que rlpt_1 » — la classe n'est plus exposée à la racine (le\n", + "pendent industriel y est `trl.GRPOTrainer`), mais elle n'a **pas** été supprimée :\n", + "elle vit désormais dans `trl.experimental.ppo`. Ce notebook documente la succession,\n", + "remet le trainer historique en service (section 5bis), puis compare :\n", "\n", "| Bras | Algorithme | Baseline de l'avantage | Pile |\n", "|---|---|---|---|\n", "| **Maison** | PPO-clip (recette `rlpt_1` transplantée) | value net **appris** $V(x)$ | torch pur |\n", "| **SOTA** | GRPO (group relative policy optimization) | **moyenne du groupe** de $G$ réponses | `trl` |\n", + "| **Historique** | PPO complet (critic + GAE, API Trainer) | value head **appris** $V(s_t)$ + GAE $\\lambda$ | `trl.experimental.ppo` |\n", "\n", - "Les deux bras résolvent le même problème — *réduire la variance d'une récompense terminale\n", + "Maison et GRPO résolvent le même problème — *réduire la variance d'une récompense terminale\n", "unique* — par deux baselines différentes. C'est LA différence pédagogique du notebook :\n", - "un critic entraîné, ou la structure du groupe elle-même." + "un critic entraîné, ou la structure du groupe elle-même. Le bras historique rend à la lib,\n", + "clé en main, ce que le bras maison écrivait à la main.\n" ] }, { "cell_type": "code", "execution_count": 1, - "id": "c8dad8cb4a4e", + "id": "869228b06226", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:57:56.710077Z", - "iopub.status.busy": "2026-09-15T12:57:56.710077Z", - "iopub.status.idle": "2026-09-15T12:58:03.065407Z", - "shell.execute_reply": "2026-09-15T12:58:03.064402Z" + "iopub.execute_input": "2026-09-28T08:52:14.553980Z", + "iopub.status.busy": "2026-09-28T08:52:14.553980Z", + "iopub.status.idle": "2026-09-28T08:52:26.346285Z", + "shell.execute_reply": "2026-09-28T08:52:26.344754Z" }, "papermill": { - "duration": 6.361797, - "end_time": "2026-09-15T12:58:03.065407", + "duration": 11.807649, + "end_time": "2026-09-28T08:52:26.347284+00:00", "exception": false, - "start_time": "2026-09-15T12:57:56.703610", + "start_time": "2026-09-28T08:52:14.539635+00:00", "status": "completed" }, "tags": [] }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\\miniconda3\\envs\\coursia-sae\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "python 3.11.16 | torch 2.14.0+cpu | transformers 5.17.0 | trl 1.9.2 | datasets 5.0.1\n", + "trainers trl (racine) : DPOTrainer, GRPOTrainer, KTOTrainer, RLOOTrainer, RewardTrainer, SFTTrainer\n", + "PPOTrainer a la racine : False\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "python 3.11.9 | torch 2.13.0+cpu | transformers 5.12.1 | trl 1.10.0 | datasets 5.0.0\n", - "trainers trl : DPOTrainer, DistillationTrainer, GRPOTrainer, KTOTrainer, RLOOTrainer, RewardTrainer, SFTTrainer\n", - "PPOTrainer present : False\n" + "PPOTrainer (reel) : trl.experimental.ppo.ppo_trainer\n" ] } ], "source": [ - "import sys, time, tempfile, platform\n", + "import os, sys, time, tempfile, platform\n", "import numpy as np\n", "import torch, torch.nn as nn, torch.nn.functional as F\n", "import transformers, trl, datasets\n", @@ -81,46 +99,61 @@ "print(f\"python {sys.version.split()[0]} | torch {torch.__version__} | transformers {transformers.__version__}\"\n", " f\" | trl {trl.__version__} | datasets {datasets.__version__}\")\n", "\n", - "# Le fait structurant : PPOTrainer a disparu de trl\n", + "# Le fait structurant : PPOTrainer a quitte la RACINE de trl, pas la lib\n", "exports = [x for x in dir(trl) if 'Trainer' in x]\n", - "print(\"trainers trl :\", ', '.join(sorted(exports)))\n", - "print(\"PPOTrainer present :\", hasattr(trl, 'PPOTrainer'))\n", - "assert not hasattr(trl, 'PPOTrainer'), \"si cette assert casse, trl a resuscite PPOTrainer -- relire le notebook\" " + "print(\"trainers trl (racine) :\", ', '.join(sorted(exports)))\n", + "print(\"PPOTrainer a la racine :\", hasattr(trl, 'PPOTrainer'))\n", + "os.environ.setdefault(\"TRL_EXPERIMENTAL_SILENCE\", \"1\") # le module est declare instable, on sait\n", + "from trl.experimental.ppo import PPOTrainer, PPOConfig # la surface ou la classe vit vraiment\n", + "print(\"PPOTrainer (reel) :\", PPOTrainer.__module__)\n", + "assert not hasattr(trl, 'PPOTrainer') and 'experimental' in PPOTrainer.__module__, \\\n", + " \"si cette assert casse, trl a reexpose PPOTrainer a la racine -- relire le notebook\"\n" ] }, { "cell_type": "markdown", "id": "2e783470", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.019072, + "end_time": "2026-09-28T08:52:26.382514+00:00", + "exception": false, + "start_time": "2026-09-28T08:52:26.363442+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ - "**Lecture chiffree — le socle de versions et l'absence qui organise le notebook.** `python 3.11.9 | torch 2.13.0+cpu | transformers 5.12.1 | trl 1.10.0 | datasets 5.0.0`, puis l'inventaire : `trainers trl : DPOTrainer, DistillationTrainer, GRPOTrainer, KTOTrainer, RLOOTrainer, RewardTrainer, SFTTrainer` et surtout `PPOTrainer present : False`. Cette derniere ligne explique l'architecture entiere du comparatif : trl a retire son PPOTrainer (deprecie fin 2024, supprime depuis), donc le bras \"PPO maison\" de la section 4 n'a PAS d'equivalent SOTA a appeler — la comparaison honnete possible aujourd'hui est une boucle PPO ecrite a la main contre le `trl.GRPOTrainer` officiel, sur le meme monde, le meme juge et le meme budget d'echantillons. Tout ce qui suit (bras maison, bras SOTA, multi-seed, verdict) decoule de cette ligne d'inventaire.\n" + "**Lecture chiffrée — le socle de versions et le fait qui organise le notebook.** `python 3.11.16 | torch 2.14.0+cpu | transformers 5.17.0 | trl 1.9.2 | datasets 5.0.1`, puis l'inventaire : `trainers trl (racine) : DPOTrainer, GRPOTrainer, KTOTrainer, RLOOTrainer, RewardTrainer, SFTTrainer` et surtout la paire `PPOTrainer a la racine : False` / `PPOTrainer (reel) : trl.experimental.ppo.ppo_trainer`. Ces deux lignes expliquent l'architecture entière du comparatif : trl a retiré PPOTrainer de sa racine (déprécié fin 2024) sans le supprimer — la classe vit désormais dans `trl.experimental.ppo`. La comparaison honnête possible aujourd'hui se joue donc en trois temps : une boucle PPO écrite à la main (section 4), le `trl.GRPOTrainer` officiel (section 5), et le trainer historique remis en service depuis son module d'accueil (section 5bis) — sur le même monde et le même budget de rollouts. Tout ce qui suit (bras maison, bras SOTA, bras historique, multi-seed, verdict) découle de ces deux lignes d'inventaire." ] }, { "cell_type": "markdown", - "id": "80b22faa0f08", + "id": "512bc3329e8b", "metadata": { "papermill": { - "duration": 0.003001, - "end_time": "2026-09-15T12:58:03.071922", + "duration": 0.017322, + "end_time": "2026-09-28T08:52:26.412111+00:00", "exception": false, - "start_time": "2026-09-15T12:58:03.068921", + "start_time": "2026-09-28T08:52:26.394789+00:00", "status": "completed" }, "tags": [] }, "source": [ - "`trl.PPOTrainer` a été retiré de la lib (deprecated fin 2024, supprimé depuis) : les\n", - "traineurs **en ligne** que trl expose aujourd'hui sont `GRPOTrainer` et `RLOOTrainer`.\n", - "Le « PPO-RLHF SOTA comparison » de l'issue se livre donc contre `GRPOTrainer` — le\n", - "successeur factuel, celui que la pratique industrielle a adopté (c'est le cœur de\n", - "`rlpt_2` côté main, et des notebooks `PT-11a/b` côté échelle réelle).\n", + "`trl.PPOTrainer` n'est plus exposé à la **racine** de la lib (déprécié fin 2024 ; la racine\n", + "ne porte plus que les traineurs en ligne « modernes » : `GRPOTrainer`, `RLOOTrainer`), mais\n", + "il n'a pas été supprimé : il vit désormais dans `trl.experimental.ppo` — **déplacé**, pas\n", + "enterré. Le « PPO-RLHF SOTA comparison » de l'issue se livre donc en trois temps : la boucle\n", + "maison, le `GRPOTrainer` que la pratique industrielle a adopté (c'est le cœur de `rlpt_2`\n", + "côté main, et des notebooks `PT-11a/b` côté échelle réelle), et le `PPOTrainer` historique\n", + "remis en service depuis son module d'exil (section 5bis).\n", "\n", "**GRPO en une phrase** : au lieu d'entraîner un critic $V(s)$ à prédire la récompense,\n", "on génère $G$ réponses au même prompt, et l'avantage de chacune est\n", "$A_i = (r_i - \\text{mean}(r_{1..G})) / \\text{std}(r_{1..G})$ — le groupe est sa propre\n", - "baseline. Pas de value net, pas de GAE : la variance se réduit par la structure même\n", - "de l'échantillonnage." + "baseline. Pas de value net, pas de GAE : la variance se réduit par la structure même de\n", + "l'échantillonnage.\n" ] }, { @@ -128,10 +161,10 @@ "id": "8ab87a238921", "metadata": { "papermill": { - "duration": 0.00257, - "end_time": "2026-09-15T12:58:03.078066", + "duration": 0.015462, + "end_time": "2026-09-28T08:52:26.448558+00:00", "exception": false, - "start_time": "2026-09-15T12:58:03.075496", + "start_time": "2026-09-28T08:52:26.433096+00:00", "status": "completed" }, "tags": [] @@ -151,16 +184,16 @@ "id": "33d25d11e1e6", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:58:03.088710Z", - "iopub.status.busy": "2026-09-15T12:58:03.087706Z", - "iopub.status.idle": "2026-09-15T12:58:03.126937Z", - "shell.execute_reply": "2026-09-15T12:58:03.126937Z" + "iopub.execute_input": "2026-09-28T08:52:26.488873Z", + "iopub.status.busy": "2026-09-28T08:52:26.487873Z", + "iopub.status.idle": "2026-09-28T08:52:26.584172Z", + "shell.execute_reply": "2026-09-28T08:52:26.584172Z" }, "papermill": { - "duration": 0.047009, - "end_time": "2026-09-15T12:58:03.128075", + "duration": 0.11593, + "end_time": "2026-09-28T08:52:26.586589+00:00", "exception": false, - "start_time": "2026-09-15T12:58:03.081066", + "start_time": "2026-09-28T08:52:26.470659+00:00", "status": "completed" }, "tags": [] @@ -214,7 +247,16 @@ { "cell_type": "markdown", "id": "1202dd97", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.022362, + "end_time": "2026-09-28T08:52:26.631730+00:00", + "exception": false, + "start_time": "2026-09-28T08:52:26.609368+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ "**Lecture chiffrée — les repères pour lire toutes les courbes.** `bruit de fond (politique uniforme sur le contenu) : 0.487` et `plafond glouton : -> a*8 + bonus = 3.20 | -> a*8 = 2.40`. Toute récompense de ce notebook se lit entre ces deux bornes : 0.487 = ce qu'une politique qui tire les lettres au hasard marque en moyenne (le bonus positionnel tombe rarement tout seul), 3.20 et 2.40 = le maximum atteignable en répondant systématiquement `a` à chaque position. Le 0.487 est une moyenne empirique sur 4 000 réponses tirées avec la graine 7, pas une espérance analytique : une nouvelle graine peut déplacer ce repère. Les deux plafonds correspondent à des prompts différents et ne sont donc pas deux scores concurrents pour une même réponse. Les gains mesurés plus bas (0.219 -> 0.815 par exemple) prennent leur sens contre le bruit de fond 0.487 : passer de 0.219 à 0.815, c'est traverser la ligne du hasard, pas toucher le plafond.\n" ] @@ -224,10 +266,10 @@ "id": "681f981fcc58", "metadata": { "papermill": { - "duration": 0.004241, - "end_time": "2026-09-15T12:58:03.135738", + "duration": 0.013369, + "end_time": "2026-09-28T08:52:26.668503+00:00", "exception": false, - "start_time": "2026-09-15T12:58:03.131497", + "start_time": "2026-09-28T08:52:26.655134+00:00", "status": "completed" }, "tags": [] @@ -245,10 +287,10 @@ "id": "a90eb0b68be3", "metadata": { "papermill": { - "duration": 0.005001, - "end_time": "2026-09-15T12:58:03.143743", + "duration": 0.018928, + "end_time": "2026-09-28T08:52:26.711379+00:00", "exception": false, - "start_time": "2026-09-15T12:58:03.138742", + "start_time": "2026-09-28T08:52:26.692451+00:00", "status": "completed" }, "tags": [] @@ -269,16 +311,16 @@ "id": "5a1cd228dd2c", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:58:03.153107Z", - "iopub.status.busy": "2026-09-15T12:58:03.153107Z", - "iopub.status.idle": "2026-09-15T12:58:08.505705Z", - "shell.execute_reply": "2026-09-15T12:58:08.504697Z" + "iopub.execute_input": "2026-09-28T08:52:26.756236Z", + "iopub.status.busy": "2026-09-28T08:52:26.754731Z", + "iopub.status.idle": "2026-09-28T08:52:30.283988Z", + "shell.execute_reply": "2026-09-28T08:52:30.282391Z" }, "papermill": { - "duration": 5.358817, - "end_time": "2026-09-15T12:58:08.506704", + "duration": 3.55253, + "end_time": "2026-09-28T08:52:30.285556+00:00", "exception": false, - "start_time": "2026-09-15T12:58:03.147887", + "start_time": "2026-09-28T08:52:26.733026+00:00", "status": "completed" }, "tags": [] @@ -288,7 +330,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "RM (seed 0) entraîné en 5.3s -- c'est le juge des deux bras\n" + "RM (seed 0) entraîné en 3.5s -- c'est le juge des deux bras\n" ] } ], @@ -345,10 +387,10 @@ "id": "6d79f4ccf525", "metadata": { "papermill": { - "duration": 0.005791, - "end_time": "2026-09-15T12:58:08.519002", + "duration": 0.021107, + "end_time": "2026-09-28T08:52:30.329188+00:00", "exception": false, - "start_time": "2026-09-15T12:58:08.513211", + "start_time": "2026-09-28T08:52:30.308081+00:00", "status": "completed" }, "tags": [] @@ -369,16 +411,16 @@ "id": "a56e093bfdc7", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:58:08.528876Z", - "iopub.status.busy": "2026-09-15T12:58:08.528372Z", - "iopub.status.idle": "2026-09-15T12:58:19.937723Z", - "shell.execute_reply": "2026-09-15T12:58:19.937723Z" + "iopub.execute_input": "2026-09-28T08:52:30.374498Z", + "iopub.status.busy": "2026-09-28T08:52:30.374498Z", + "iopub.status.idle": "2026-09-28T08:52:39.877779Z", + "shell.execute_reply": "2026-09-28T08:52:39.876774Z" }, "papermill": { - "duration": 11.416545, - "end_time": "2026-09-15T12:58:19.939236", + "duration": 9.527244, + "end_time": "2026-09-28T08:52:39.879287+00:00", "exception": false, - "start_time": "2026-09-15T12:58:08.522691", + "start_time": "2026-09-28T08:52:30.352043+00:00", "status": "completed" }, "tags": [] @@ -396,7 +438,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "politique non entraînée : vraie récompense 0.344 (le padding 'h' punit les bavardages prompt/EOS)\n" + "politique non entraînée : vraie récompense 0.219 (le padding 'h' punit les bavardages prompt/EOS)\n" ] } ], @@ -444,7 +486,16 @@ { "cell_type": "markdown", "id": "1e9a02d9", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.016457, + "end_time": "2026-09-28T08:52:39.914913+00:00", + "exception": false, + "start_time": "2026-09-28T08:52:39.898456+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ "**Lecture chiffree — le point zero avant tout entrainement.** `politique non entraînée : vraie récompense 0.344 (le padding 'h' punit les bavardages prompt/EOS)`. Deux informations dans une ligne. D'abord le niveau : 0.344, SOUS le bruit de fond 0.487 — la politique vierge fait pire que le hasard. La cause est dans la parenthese : les sequences sont paddees avec le token `h`, dont le poids dans W_TOK est negatif (la sortie le dit : il \"punit\"), donc chaque token de padding coute de la recompense — le modele non entraine \"bavarde\" (emet des tokens de prompt/EOS) et se fait sanctionner. A retenir pour la suite : ce notebook mesure toujours la VRAIE recompense (celle des poids, pas celle du reward model Bradley-Terry) sur ses evaluations — la divergence entre les deux est precisement ce que le multi-seed de la section 6 met en evidence, le juge appris n'etant qu'une approximation bruitee de la vraie.\n" ] @@ -454,10 +505,10 @@ "id": "7bddf3c393d3", "metadata": { "papermill": { - "duration": 0.00551, - "end_time": "2026-09-15T12:58:19.949309", + "duration": 0.02031, + "end_time": "2026-09-28T08:52:39.955885+00:00", "exception": false, - "start_time": "2026-09-15T12:58:19.943799", + "start_time": "2026-09-28T08:52:39.935575+00:00", "status": "completed" }, "tags": [] @@ -478,16 +529,16 @@ "id": "61b438903a3f", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:58:19.961205Z", - "iopub.status.busy": "2026-09-15T12:58:19.960701Z", - "iopub.status.idle": "2026-09-15T12:58:19.973967Z", - "shell.execute_reply": "2026-09-15T12:58:19.972959Z" + "iopub.execute_input": "2026-09-28T08:52:39.997085Z", + "iopub.status.busy": "2026-09-28T08:52:39.995538Z", + "iopub.status.idle": "2026-09-28T08:52:40.015853Z", + "shell.execute_reply": "2026-09-28T08:52:40.014845Z" }, "papermill": { - "duration": 0.020887, - "end_time": "2026-09-15T12:58:19.974473", + "duration": 0.044316, + "end_time": "2026-09-28T08:52:40.016854+00:00", "exception": false, - "start_time": "2026-09-15T12:58:19.953586", + "start_time": "2026-09-28T08:52:39.972538+00:00", "status": "completed" }, "tags": [] @@ -568,16 +619,16 @@ "id": "95eb00c38249", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:58:19.984086Z", - "iopub.status.busy": "2026-09-15T12:58:19.984086Z", - "iopub.status.idle": "2026-09-15T12:59:42.615901Z", - "shell.execute_reply": "2026-09-15T12:59:42.613862Z" + "iopub.execute_input": "2026-09-28T08:52:40.052593Z", + "iopub.status.busy": "2026-09-28T08:52:40.052083Z", + "iopub.status.idle": "2026-09-28T08:53:46.760779Z", + "shell.execute_reply": "2026-09-28T08:53:46.759206Z" }, "papermill": { - "duration": 82.640535, - "end_time": "2026-09-15T12:59:42.619058", + "duration": 66.745976, + "end_time": "2026-09-28T08:53:46.777506+00:00", "exception": false, - "start_time": "2026-09-15T12:58:19.978523", + "start_time": "2026-09-28T08:52:40.031530+00:00", "status": "completed" }, "tags": [] @@ -587,11 +638,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "[Bras maison | PPO clip + value net] 68.0s\n", - "vraie récompense : 0.219 -> 0.815 (bruit de fond 0.487)\n", - " it r_rm=-0.184 kl=0.042 vloss=0.826\n", - " it r_rm=+0.606 kl=0.462 vloss=1.216\n", - " it r_rm=+0.769 kl=0.863 vloss=1.526\n" + "[Bras maison | PPO clip + value net] 57.5s\n", + "vraie récompense : 0.252 -> 0.981 (bruit de fond 0.487)\n", + " it r_rm=-0.235 kl=0.026 vloss=0.879\n", + " it r_rm=+0.412 kl=0.381 vloss=1.378\n", + " it r_rm=+0.693 kl=0.896 vloss=1.736\n" ] } ], @@ -613,7 +664,16 @@ { "cell_type": "markdown", "id": "a6441220", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.019724, + "end_time": "2026-09-28T08:53:46.812409+00:00", + "exception": false, + "start_time": "2026-09-28T08:53:46.792685+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ "**Lecture chiffree — le bras maison en trois iterations : le juge change de camp.** `[Bras maison | PPO clip + value net] 68.0s`, `vraie récompense : 0.219 -> 0.815 (bruit de fond 0.487)`. Les lignes d'iteration imprimees : `it r_rm=-0.184 kl=0.042 vloss=0.826`, puis `it r_rm=+0.606 kl=0.462 vloss=1.216`, puis `it r_rm=+0.769 kl=0.863 vloss=1.526`. Ce que la lecture suivante ne detaille pas (elle traite du calage du value net) : (1) le SIGNE du jugement RM flip entre la premiere et la deuxieme ligne — -0.184 puis +0.606, le juge passe de rejet a approbation en une iteration d'ecart, avant de se rapprocher de son plateau (+0.769) ; (2) la KL accelere puis ralentit — x11 entre les deux premieres lignes (0.042 -> 0.462), puis seulement x1.9 (0.462 -> 0.863) : la derive se stabilise en meme temps que la recompense monte ; (3) la finale 0.815 vaut 1.67 fois le bruit de fond 0.487 — l'alignement est reel mais reste a mi-chemin du plafond glouton 3.20, il y a largement place pour ce que le seed 7 montrera plus bas (0.897) sans jamais l'atteindre.\n" ] @@ -623,10 +683,10 @@ "id": "d2b5f06d3016", "metadata": { "papermill": { - "duration": 0.005103, - "end_time": "2026-09-15T12:59:42.629322", + "duration": 0.023536, + "end_time": "2026-09-28T08:53:46.856171+00:00", "exception": false, - "start_time": "2026-09-15T12:59:42.624219", + "start_time": "2026-09-28T08:53:46.832635+00:00", "status": "completed" }, "tags": [] @@ -645,10 +705,10 @@ "id": "f720c4aa38b7", "metadata": { "papermill": { - "duration": 0.005251, - "end_time": "2026-09-15T12:59:42.639851", + "duration": 0.019047, + "end_time": "2026-09-28T08:53:46.899092+00:00", "exception": false, - "start_time": "2026-09-15T12:59:42.634600", + "start_time": "2026-09-28T08:53:46.880045+00:00", "status": "completed" }, "tags": [] @@ -677,16 +737,16 @@ "id": "6d176b5554e0", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:59:42.651883Z", - "iopub.status.busy": "2026-09-15T12:59:42.650876Z", - "iopub.status.idle": "2026-09-15T12:59:47.405965Z", - "shell.execute_reply": "2026-09-15T12:59:47.404468Z" + "iopub.execute_input": "2026-09-28T08:53:46.929726Z", + "iopub.status.busy": "2026-09-28T08:53:46.928725Z", + "iopub.status.idle": "2026-09-28T08:53:47.028169Z", + "shell.execute_reply": "2026-09-28T08:53:47.027165Z" }, "papermill": { - "duration": 4.761423, - "end_time": "2026-09-15T12:59:47.405965", + "duration": 0.116135, + "end_time": "2026-09-28T08:53:47.029170+00:00", "exception": false, - "start_time": "2026-09-15T12:59:42.644542", + "start_time": "2026-09-28T08:53:46.913035+00:00", "status": "completed" }, "tags": [] @@ -742,10 +802,10 @@ "id": "fd4d7f5c26f5", "metadata": { "papermill": { - "duration": 0.004578, - "end_time": "2026-09-15T12:59:47.415250", + "duration": 0.024315, + "end_time": "2026-09-28T08:53:47.073371+00:00", "exception": false, - "start_time": "2026-09-15T12:59:47.410672", + "start_time": "2026-09-28T08:53:47.049056+00:00", "status": "completed" }, "tags": [] @@ -763,134 +823,161 @@ "id": "55fbde19508f", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T12:59:47.424765Z", - "iopub.status.busy": "2026-09-15T12:59:47.424765Z", - "iopub.status.idle": "2026-09-15T13:00:32.179390Z", - "shell.execute_reply": "2026-09-15T13:00:32.178386Z" + "iopub.execute_input": "2026-09-28T08:53:47.147050Z", + "iopub.status.busy": "2026-09-28T08:53:47.146052Z", + "iopub.status.idle": "2026-09-28T08:54:32.100707Z", + "shell.execute_reply": "2026-09-28T08:54:32.099123Z" }, "papermill": { - "duration": 44.764645, - "end_time": "2026-09-15T13:00:32.183892", + "duration": 45.017559, + "end_time": "2026-09-28T08:54:32.117049+00:00", "exception": false, - "start_time": "2026-09-15T12:59:47.419247", + "start_time": "2026-09-28T08:53:47.099490+00:00", "status": "completed" }, "tags": [] }, "outputs": [ { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c20d5f809507466fb1d904e228af0bb5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Writing model shards: 0%| | 0/1 [00:00` masqués par l'attention, score relevé au\n", + "token qui précède le premier ``). On réentraîne donc un juge Bradley-Terry de la même\n", + "recette que la section 2 — mêmes paires dérivées de la vraie récompense avec écart minimal,\n", + "même perte BT — mais en `GPT2ForSequenceClassification`, sur des réponses brutes au lieu\n", + "du contenu filtré.\n", + "2. **Le pad ne peut plus être ``.** `generate()` reconstruit le masque d'attention par\n", + "`queries != pad_token_id` : avec pad = `` (id 0), toute query `` serait masquée\n", + "entièrement. Ce bras utilise donc un pad ``, et les `` générés en milieu de\n", + "réponse jouent le rôle de fin de séquence.\n", + "\n", + "Le budget reste apparié : 2 304 prompts × une passe = 2 304 completions, comme les\n", + "deux autres bras — chaque prompt est vu une fois, rollouts frais comme le bras maison. Le bras GRPO reste juste au-dessus — on compare maintenant les trois.\n" ] }, { "cell_type": "code", "execution_count": 9, - "id": "fd2f4513c6c4", + "id": "d3374b601849", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T13:00:32.206515Z", - "iopub.status.busy": "2026-09-15T13:00:32.205516Z", - "iopub.status.idle": "2026-09-15T13:02:24.433518Z", - "shell.execute_reply": "2026-09-15T13:02:24.433016Z" + "iopub.execute_input": "2026-09-28T08:54:32.253222Z", + "iopub.status.busy": "2026-09-28T08:54:32.253222Z", + "iopub.status.idle": "2026-09-28T08:55:59.796949Z", + "shell.execute_reply": "2026-09-28T08:55:59.795374Z" }, "papermill": { - "duration": 112.241398, - "end_time": "2026-09-15T13:02:24.440028", + "duration": 87.58289, + "end_time": "2026-09-28T08:55:59.814734+00:00", "exception": false, - "start_time": "2026-09-15T13:00:32.198630", + "start_time": "2026-09-28T08:54:32.231844+00:00", "status": "completed" }, "tags": [] }, "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "67324e8b603a4fecbe7b44f6e78e5c92", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Writing model shards: 0%| | 0/1 [00:000.815 (66s) | trl 0.219->0.880 (43s)\n", - "(les seeds restantes tournent -- cellule suivante)\n" + "RM-GPT2 (juge du bras historique) entraîné en 87.5s\n", + "accord du juge avec la vraie récompense (spearman, 300 réponses brutes) : 0.546\n" ] } ], "source": [ - "def run_seed(seed):\n", - " global RM_SEED_LOCAL\n", - " torch.manual_seed(seed)\n", - " RM_SEED_LOCAL = make_rm(seed)\n", - " out = {}\n", + "# 5bis -- etape 1 : le juge au format lib (backbone + tete .score(), la voie exacte de get_reward)\n", + "from transformers import AutoModelForSequenceClassification\n", "\n", - " t0 = time.time()\n", - " pol, ref = make_lm(seed), make_lm(seed)\n", - " value = ValueNet()\n", - " r0 = eval_policy(pol, seed=seed * 1000 + 123)\n", - " train_ppo_maison(pol, ref, value, RM_SEED_LOCAL, seed=seed)\n", - " out['maison'] = dict(r=eval_policy(pol, seed=seed * 1000 + 123), t=time.time() - t0, r0=r0)\n", + "tz_ppo = make_tz()\n", + "tz_ppo.pad_token = '' # un pad (id 0) masquerait les query dans generate()\n", "\n", - " t0 = time.time()\n", - " tr = train_grpo_trl(seed)\n", - " out['trl'] = dict(r=eval_policy(tr.model, seed=seed * 1000 + 123), t=time.time() - t0, r0=r0)\n", - " return out\n", + "def gpt2_cls_config():\n", + " return GPT2Config(vocab_size=len(TOK), n_positions=16, n_embd=64, n_layer=2, n_head=4,\n", + " num_labels=1, bos_token_id=0, eos_token_id=V[''], pad_token_id=0)\n", "\n", - "RES = {}\n", - "for s in SEEDS[:1]:\n", - " RES[s] = run_seed(s)\n", - " m, c = RES[s]['maison'], RES[s]['trl']\n", - " print(f\"seed {s}: maison {m['r0']:.3f}->{m['r']:.3f} ({m['t']:.0f}s) | \"\n", - " f\"trl {c['r0']:.3f}->{c['r']:.3f} ({c['t']:.0f}s)\")\n", - "print(\"(les seeds restantes tournent -- cellule suivante)\")" + "def conv_get_reward(x):\n", + " \"\"\"La convention de get_reward avec pad= : masques par l'attention, ids mis a 0,\n", + " positions compactees, score releve au token qui precede le premier (sinon dernier).\"\"\"\n", + " eos = (x == V[''])\n", + " xm, am = x.masked_fill(eos, 0), (~eos).long()\n", + " pos = am.cumsum(1) - am.long()\n", + " resp = eos[:, 1:]\n", + " first = torch.where(resp.any(1), resp.float().argmax(1), torch.full((x.shape[0],), float(LEN_R)))\n", + " return xm, am, pos, first.long()\n", + "\n", + "def rm_pos_scores(rm, x):\n", + " \"\"\"Score par la voie de la lib : backbone -> .score() -> position de fin effective.\"\"\"\n", + " xm, am, pos, last = conv_get_reward(x)\n", + " h = rm.transformer(input_ids=xm, attention_mask=am, position_ids=pos,\n", + " output_hidden_states=True, return_dict=True).hidden_states[-1]\n", + " return rm.score(h).squeeze(-1)[torch.arange(x.shape[0]), last]\n", + "\n", + "def contenu(ids):\n", + " \"\"\"Reponse brute -> contenu filtre + pad 'h', la convention de eval_policy.\"\"\"\n", + " c = [t for t in ids if 2 <= t <= 9][:LEN_R]\n", + " return np.asarray(c + [V['h']] * (LEN_R - len(c)))\n", + "\n", + "def make_pairs_raw(n_pairs, rng, min_gap=0.3, beta=1.0):\n", + " \"\"\"Meme recette BT que make_pairs, sur des reponses BRUTES (tout le vocab) :\n", + " c'est la sequence que get_reward verra. Vraie recompense toujours calculee sur le contenu filtre.\"\"\"\n", + " pw, y, pr = [], [], []\n", + " while len(pw) < n_pairs:\n", + " prompt = int(rng.integers(0, 2))\n", + " a, b = rng.integers(0, len(TOK), size=LEN_R), rng.integers(0, len(TOK), size=LEN_R)\n", + " ra, rb = true_reward(contenu(a), prompt), true_reward(contenu(b), prompt)\n", + " if abs(ra - rb) < min_gap:\n", + " continue\n", + " p_a = 1.0 / (1.0 + np.exp(-beta * (ra - rb)))\n", + " pw.append((a, b)); y.append(1 if rng.random() < p_a else 0); pr.append(prompt)\n", + " return pw, np.array(y), np.array(pr)\n", + "\n", + "def x_from_raw(raw_list, pr_list):\n", + " x = torch.zeros(len(raw_list), 1 + LEN_R, dtype=torch.long)\n", + " for i, (raw, p) in enumerate(zip(raw_list, pr_list)):\n", + " x[i, 0] = p; x[i, 1:] = torch.from_numpy(np.asarray(raw, dtype=np.int64))\n", + " return x\n", + "\n", + "def make_rm_gpt2(seed, n_pairs=4000, epochs=60, bs=256, lr=1e-3):\n", + " \"\"\"Juge Bradley-Terry de la recette rlpt_0, en GPT2ForSequenceClassification.\"\"\"\n", + " torch.manual_seed(seed + 10000)\n", + " rng = np.random.default_rng(seed + 10000)\n", + " pw, y, pr = make_pairs_raw(n_pairs, rng)\n", + " rm = AutoModelForSequenceClassification.from_config(gpt2_cls_config())\n", + " opt = torch.optim.Adam(rm.parameters(), lr=lr)\n", + " xa = x_from_raw([a for a, b in pw], pr); xb = x_from_raw([b for a, b in pw], pr)\n", + " yt = torch.tensor(y, dtype=torch.long)\n", + " for _ in range(epochs):\n", + " perm = torch.randperm(len(y)).tolist()\n", + " for i in range(0, len(y), bs):\n", + " idx = torch.tensor(perm[i:i + bs])\n", + " sa, sb = rm_pos_scores(rm, xa[idx]), rm_pos_scores(rm, xb[idx])\n", + " diff = torch.where(yt[idx] == 1, sa - sb, sb - sa)\n", + " loss = F.binary_cross_entropy_with_logits(diff, torch.ones(len(idx)))\n", + " opt.zero_grad(); loss.backward(); opt.step()\n", + " return rm\n", + "\n", + "t0 = time.time()\n", + "RM_GPT2 = make_rm_gpt2(0)\n", + "g = np.random.default_rng(31337)\n", + "raws = [g.integers(0, len(TOK), size=LEN_R) for _ in range(300)]\n", + "pids = [int(g.integers(0, 2)) for _ in range(300)]\n", + "with torch.no_grad():\n", + " s_rm = rm_pos_scores(RM_GPT2, x_from_raw(raws, pids)).numpy()\n", + "r_vrai = np.array([true_reward(contenu(r_), p) for r_, p in zip(raws, pids)])\n", + "print(f\"RM-GPT2 (juge du bras historique) entraîné en {time.time()-t0:.1f}s\")\n", + "print(f\"accord du juge avec la vraie récompense (spearman, 300 réponses brutes) :\"\n", + " f\" {spearmanr(s_rm, r_vrai).statistic:.3f}\")\n" ] }, { "cell_type": "markdown", - "id": "db34e148", - "metadata": {}, + "id": "3533fe8b7fd1", + "metadata": { + "papermill": { + "duration": 0.021983, + "end_time": "2026-09-28T08:55:59.855252+00:00", + "exception": false, + "start_time": "2026-09-28T08:55:59.833269+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ - "**Lecture chiffree — ce que run_seed rejoue, et pourquoi c'est la vraie robustesse.** La fonction relance TOUT le pipeline par graine : nouvelles paires de preferences, nouveau reward model (RM_SEED_LOCAL repose le sien), nouvelles politiques initiales — les barres `Writing model shards` et `Loading weights` (0/28) le rappellent a chaque relance. La consequence methodologique est la difference silencieuse de ce notebook avec un multi-seed ordinaire : la variabilite mesuree a la fin inclut celle du JUGE lui-meme, pas seulement celle de l'optimisation. Deux juges differents peuvent classer differemment les memes reponses ; un gain qui tient sur 4 seeds a juges independants est un gain qui survit a l'incertitude du juge — exactement la propriete qu'un reward model appris doit avoir en production, et que le bruitage Bradley-Terry des paires d'entrainement simule ici a petite echelle.\n" + "**Lecture chiffrée — le juge du bras historique.** 87,5 s d'entraînement (4000 paires Bradley-Terry, 60 epochs) pour un accord avec la vraie récompense de **spearman 0.546** sur 300 réponses brutes. Le contrat est plus rude que celui du RM custom de la section 2, qui juge le *contenu* filtré des réponses : l'interface `reward_model` de la lib évalue la séquence **telle qu'elle est générée** — pad `` compris, tout le vocabulaire — et relève le score à la position de fin de réponse détectée par la lib (avant le premier ``, sinon au dernier token). La question que pose la suite : un juge à 0.55 de corrélation suffit-il encore à piloter un PPO complet quand c'est la lib elle-même qui l'interroge ?" ] }, { "cell_type": "code", "execution_count": 10, - "id": "2ef484af5af7", + "id": "0784b8a86a18", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T13:02:24.457040Z", - "iopub.status.busy": "2026-09-15T13:02:24.456040Z", - "iopub.status.idle": "2026-09-15T13:07:34.684847Z", - "shell.execute_reply": "2026-09-15T13:07:34.684847Z" + "iopub.execute_input": "2026-09-28T08:55:59.898041Z", + "iopub.status.busy": "2026-09-28T08:55:59.898041Z", + "iopub.status.idle": "2026-09-28T08:56:21.831282Z", + "shell.execute_reply": "2026-09-28T08:56:21.830772Z" }, "papermill": { - "duration": 310.245365, - "end_time": "2026-09-15T13:07:34.693393", + "duration": 21.953484, + "end_time": "2026-09-28T08:56:21.832850+00:00", "exception": false, - "start_time": "2026-09-15T13:02:24.448028", + "start_time": "2026-09-28T08:55:59.879366+00:00", "status": "completed" }, "tags": [] }, "outputs": [ { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "38a86403feb74051ba46256e3b94bdcb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Writing model shards: 0%| | 0/1 [00:000.688 (59s) | trl 0.212->0.742 (41s)\n" + "{'eps': 457, 'objective/kl': '0.08054', 'objective/entropy': '10.2', 'objective/non_score_reward': '-0.004027', 'objective/rlhf_reward': '0.2672', 'objective/scores': '0.2712', 'policy/approxkl_avg': '0.0003591', 'policy/clipfrac_avg': '0', 'loss/policy_avg': '-0.004497', 'loss/value_avg': '0.3832', 'val/clipfrac_avg': '0', 'policy/entropy_avg': '2.35', 'val/ratio': '1.001', 'val/ratio_var': '7.332e-07', 'val/num_eos_tokens': 141, 'lr': '8.333e-05', 'episode': 576, 'epoch': '0.25'}\n" ] }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e8ee45eb38aa44a1ad97026d1d6ba442", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Writing model shards: 0%| | 0/1 [00:000.897 (62s) | trl 0.227->0.491 (43s)\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "823cf2a1ea004705a6abfd30f4ac96b0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Writing model shards: 0%| | 0/1 [00:00 0.959 (bruit de fond 0.487)\n", + "log trl-ppo : scores=0.496 kl=0.414 vf_loss=0.294 eps=418\n" ] + } + ], + "source": [ + "# 5bis -- etape 2 : le bras. La lib tient la boucle complete (rollout, score, GAE, PPO-clip\n", + "# policy ET value, penalite KL par token) ; on fournit policy, ref, juge, critic.\n", + "def train_ppo_trl(seed, n_prompts=2304, n_epochs=1, bs=192, lr=1e-4):\n", + " pol, ref = make_lm(seed), make_lm(seed)\n", + " value = AutoModelForSequenceClassification.from_config(gpt2_cls_config()) # critic from scratch\n", + " rng = np.random.default_rng(seed + 50000)\n", + " ds = Dataset.from_list([{\"input_ids\": [int(rng.integers(0, 2))]} for _ in range(n_prompts)])\n", + " cfg = PPOConfig(\n", + " output_dir=tempfile.mkdtemp(prefix='rlpt0f_ppo_'), # hors du dépôt\n", + " per_device_train_batch_size=bs,\n", + " num_train_epochs=n_epochs, # 2304 prompts x 1 passe = 2304 completions, budget apparié\n", + " learning_rate=lr, # même lr que le bras GRPO (même pile lib)\n", + " response_length=LEN_R,\n", + " stop_token_id=None, # réponse pleine longueur, comme les autres bras\n", + " temperature=1.0, # échantillonnage nu, comme le rollout maison\n", + " num_ppo_epochs=1, # une passe par rollout, comme le bras maison\n", + " num_mini_batches=4,\n", + " vf_coef=0.5, # même poids value que le bras maison\n", + " kl_coef=BETA_KL, # même coefficient KL que les deux autres bras\n", + " local_rollout_forward_batch_size=64,\n", + " logging_steps=4,\n", + " report_to=[], save_strategy='no', disable_tqdm=True,\n", + " num_sample_generations=0,\n", + " seed=seed, use_cpu=True,\n", + " )\n", + " tr = PPOTrainer(args=cfg, processing_class=tz_ppo, model=pol, ref_model=ref,\n", + " reward_model=RM_GPT2, train_dataset=ds, value_model=value)\n", + " tr.train()\n", + " return tr\n", + "\n", + "t0 = time.time()\n", + "torch.manual_seed(0)\n", + "r_avant_ppo = eval_policy(make_lm(0))\n", + "tr_ppo = train_ppo_trl(0)\n", + "t_ppotrl = time.time() - t0\n", + "r_ppotrl = eval_policy(tr_ppo.policy_model)\n", + "print(f\"[Bras historique | trl.experimental.ppo.PPOTrainer] {t_ppotrl:.1f}s\")\n", + "print(f\"vraie récompense : {r_avant_ppo:.3f} -> {r_ppotrl:.3f} (bruit de fond {fond:.3f})\")\n", + "last = [d for d in tr_ppo.state.log_history if 'objective/scores' in d][-1]\n", + "print(f\"log trl-ppo : scores={last['objective/scores']:.3f} kl={last['objective/kl']:.3f}\"\n", + " f\" vf_loss={last['loss/value_avg']:.3f} eps={last['eps']}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "b699d9fc888e", + "metadata": { + "papermill": { + "duration": 0.012272, + "end_time": "2026-09-28T08:56:21.867135+00:00", + "exception": false, + "start_time": "2026-09-28T08:56:21.854863+00:00", + "status": "completed" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'loss': '-0.3002', 'grad_norm': '0.7973', 'learning_rate': '7.917e-05', 'num_tokens': '3972', 'completions/mean_length': '7.004', 'completions/min_length': '1', 'completions/max_length': '9', 'completions/clipped_ratio': '0.5234', 'completions/mean_terminated_length': '4.809', 'completions/min_terminated_length': '1', 'completions/max_terminated_length': '9', 'rewards/reward_rm/mean': '-0.9658', 'rewards/reward_rm/std': '0.9682', 'reward': '-0.9658', 'reward_std': '0.9682', 'frac_reward_zero_std': '0', 'kl': '0.03884', 'entropy': '2.349', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3454', 'epoch': '0.6667'}\n" - ] + "tags": [] + }, + "source": [ + "**Lecture chiffrée — le bras historique, une passe.** Le run complet — 2304 prompts, 12 updates de 192, critic et GAE compris — tient en **14,8 s**, le plus rapide des trois bras : `.train()` garde toute la boucle en batch, et le critic comme le juge sont des GPT-2 mini. La vraie récompense passe de **0.219 à 0.959** (bruit de fond 0.487) : gain **+0.739 au seed 0**, à rapprocher des +0.486 ± 0.205 (maison) et +0.670 ± 0.102 (GRPO) mesurés sur 4 seeds en section 6 — mais seed 0 seul, sans dispersion mesurée. Deux lectures dans le log final (`scores=0.496 kl=0.414 vf_loss=0.294`) : `objective/scores` plafonne autour de 0.50 alors que la vraie récompense atteint 0.96 — le juge BT, entraîné sur des paires, donne une direction, pas une amplitude ; et `loss/value_avg` descend de 0.46 à 0.29 sur les 12 updates : le critic apprend en ligne, c'est la valeur ajoutée structurelle de ce bras par rapport à GRPO." + ] + }, + { + "cell_type": "markdown", + "id": "0fe52c2a072b", + "metadata": { + "papermill": { + "duration": 0.016309, + "end_time": "2026-09-28T08:56:21.899096+00:00", + "exception": false, + "start_time": "2026-09-28T08:56:21.882787+00:00", + "status": "completed" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'loss': '-0.209', 'grad_norm': '0.876', 'learning_rate': '6.806e-05', 'num_tokens': '6181', 'completions/mean_length': '7.629', 'completions/min_length': '1.625', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6289', 'completions/mean_terminated_length': '5.331', 'completions/min_terminated_length': '1.625', 'completions/max_terminated_length': '9', 'rewards/reward_rm/mean': '-0.7653', 'rewards/reward_rm/std': '0.9519', 'reward': '-0.7653', 'reward_std': '0.9519', 'frac_reward_zero_std': '0', 'kl': '0.06079', 'entropy': '2.327', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3929', 'epoch': '1'}\n" - ] + "tags": [] + }, + "source": [ + "## 6. Multi-seed {0, 1, 7, 42}\n", + "\n", + "Chaque seed **retire tout le pipeline** : nouvelles paires, nouveau RM, nouvelles\n", + "politiques. C'est la force du monde synthétique — la variabilité du juge lui-même\n", + "entre dans la mesure." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fd2f4513c6c4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-28T08:56:21.933153Z", + "iopub.status.busy": "2026-09-28T08:56:21.932154Z", + "iopub.status.idle": "2026-09-28T08:58:12.153265Z", + "shell.execute_reply": "2026-09-28T08:58:12.151755Z" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'loss': '-0.2136', 'grad_norm': '0.8476', 'learning_rate': '5.694e-05', 'num_tokens': '8372', 'completions/mean_length': '7.559', 'completions/min_length': '1.125', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6445', 'completions/mean_terminated_length': '4.928', 'completions/min_terminated_length': '1.125', 'completions/max_terminated_length': '8.875', 'rewards/reward_rm/mean': '-0.7404', 'rewards/reward_rm/std': '0.9413', 'reward': '-0.7404', 'reward_std': '0.9413', 'frac_reward_zero_std': '0', 'kl': '0.07795', 'entropy': '2.311', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3753', 'epoch': '1.333'}\n" - ] + "papermill": { + "duration": 110.257656, + "end_time": "2026-09-28T08:58:12.170782+00:00", + "exception": false, + "start_time": "2026-09-28T08:56:21.913126+00:00", + "status": "completed" }, + "tags": [] + }, + "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'loss': '-0.2191', 'grad_norm': '0.6562', 'learning_rate': '4.583e-05', 'num_tokens': '1.057e+04', 'completions/mean_length': '7.57', 'completions/min_length': '1.125', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6562', 'completions/mean_terminated_length': '4.712', 'completions/min_terminated_length': '1.125', 'completions/max_terminated_length': '8.375', 'rewards/reward_rm/mean': '-0.6665', 'rewards/reward_rm/std': '0.9685', 'reward': '-0.6665', 'reward_std': '0.9685', 'frac_reward_zero_std': '0', 'kl': '0.08858', 'entropy': '2.299', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3682', 'epoch': '1.667'}\n" + "seed 0: maison 0.252->0.981 (64s) | trl 0.252->0.956 (43s)\n", + "(les seeds restantes tournent -- cellule suivante)\n" ] + } + ], + "source": [ + "# Multi-seed : on coupe le bruit de lib (barres tqdm, warnings transformers) --\n", + "# les runs detailles avec leurs logs vivent deja en sections 4, 5 et 5bis.\n", + "import io, contextlib\n", + "import transformers as _tfm\n", + "_tfm.logging.set_verbosity_error()\n", + "_tfm.logging.disable_progress_bar()\n", + "\n", + "def run_seed(seed):\n", + " global RM_SEED_LOCAL\n", + " torch.manual_seed(seed)\n", + " RM_SEED_LOCAL = make_rm(seed)\n", + " out = {}\n", + "\n", + " t0 = time.time()\n", + " pol, ref = make_lm(seed), make_lm(seed)\n", + " value = ValueNet()\n", + " r0 = eval_policy(pol, seed=seed * 1000 + 123)\n", + " with contextlib.redirect_stdout(io.StringIO()): # logs par update maison : captures\n", + " train_ppo_maison(pol, ref, value, RM_SEED_LOCAL, seed=seed)\n", + " out['maison'] = dict(r=eval_policy(pol, seed=seed * 1000 + 123), t=time.time() - t0, r0=r0)\n", + "\n", + " t0 = time.time()\n", + " with contextlib.redirect_stdout(io.StringIO()): # dicts de log GRPO : captures\n", + " tr = train_grpo_trl(seed)\n", + " out['trl'] = dict(r=eval_policy(tr.model, seed=seed * 1000 + 123), t=time.time() - t0, r0=r0)\n", + " return out\n", + "\n", + "RES = {}\n", + "for s in SEEDS[:1]:\n", + " RES[s] = run_seed(s)\n", + " m, c = RES[s]['maison'], RES[s]['trl']\n", + " print(f\"seed {s}: maison {m['r0']:.3f}->{m['r']:.3f} ({m['t']:.0f}s) | \"\n", + " f\"trl {c['r0']:.3f}->{c['r']:.3f} ({c['t']:.0f}s)\")\n", + "print(\"(les seeds restantes tournent -- cellule suivante)\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "db34e148", + "metadata": { + "papermill": { + "duration": 0.012401, + "end_time": "2026-09-28T08:58:12.196289+00:00", + "exception": false, + "start_time": "2026-09-28T08:58:12.183888+00:00", + "status": "completed" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'loss': '-0.229', 'grad_norm': '0.8397', 'learning_rate': '3.472e-05', 'num_tokens': '1.276e+04', 'completions/mean_length': '7.562', 'completions/min_length': '1.25', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6484', 'completions/mean_terminated_length': '5.051', 'completions/min_terminated_length': '1.25', 'completions/max_terminated_length': '9', 'rewards/reward_rm/mean': '-0.5854', 'rewards/reward_rm/std': '0.9832', 'reward': '-0.5854', 'reward_std': '0.9832', 'frac_reward_zero_std': '0', 'kl': '0.09683', 'entropy': '2.29', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3801', 'epoch': '2'}\n" - ] + "tags": [] + }, + "source": [ + "**Lecture chiffree — ce que run_seed rejoue, et pourquoi c'est la vraie robustesse.** La fonction relance TOUT le pipeline par graine : nouvelles paires de preferences, nouveau reward model (RM_SEED_LOCAL repose le sien), nouvelles politiques initiales — les barres `Writing model shards` et `Loading weights` (0/28) le rappellent a chaque relance. La consequence methodologique est la difference silencieuse de ce notebook avec un multi-seed ordinaire : la variabilite mesuree a la fin inclut celle du JUGE lui-meme, pas seulement celle de l'optimisation. Deux juges differents peuvent classer differemment les memes reponses ; un gain qui tient sur 4 seeds a juges independants est un gain qui survit a l'incertitude du juge — exactement la propriete qu'un reward model appris doit avoir en production, et que le bruitage Bradley-Terry des paires d'entrainement simule ici a petite echelle.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "2ef484af5af7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-28T08:58:12.229978Z", + "iopub.status.busy": "2026-09-28T08:58:12.228979Z", + "iopub.status.idle": "2026-09-28T09:03:25.637060Z", + "shell.execute_reply": "2026-09-28T09:03:25.635017Z" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'loss': '-0.1912', 'grad_norm': '0.8303', 'learning_rate': '2.361e-05', 'num_tokens': '1.499e+04', 'completions/mean_length': '7.723', 'completions/min_length': '1.25', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6289', 'completions/mean_terminated_length': '5.601', 'completions/min_terminated_length': '1.25', 'completions/max_terminated_length': '8.875', 'rewards/reward_rm/mean': '-0.5513', 'rewards/reward_rm/std': '0.9701', 'reward': '-0.5513', 'reward_std': '0.9701', 'frac_reward_zero_std': '0', 'kl': '0.1063', 'entropy': '2.283', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3381', 'epoch': '2.333'}\n" - ] + "papermill": { + "duration": 313.450485, + "end_time": "2026-09-28T09:03:25.659307+00:00", + "exception": false, + "start_time": "2026-09-28T08:58:12.208822+00:00", + "status": "completed" }, + "tags": [] + }, + "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'loss': '-0.1821', 'grad_norm': '0.8611', 'learning_rate': '1.25e-05', 'num_tokens': '1.724e+04', 'completions/mean_length': '7.793', 'completions/min_length': '1.5', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6484', 'completions/mean_terminated_length': '5.622', 'completions/min_terminated_length': '1.5', 'completions/max_terminated_length': '9', 'rewards/reward_rm/mean': '-0.5003', 'rewards/reward_rm/std': '0.9165', 'reward': '-0.5003', 'reward_std': '0.9165', 'frac_reward_zero_std': '0', 'kl': '0.1089', 'entropy': '2.277', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3419', 'epoch': '2.667'}\n" + "seed 1: maison 0.266->0.590 (62s) | trl 0.266->0.798 (38s)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "{'loss': '-0.1746', 'grad_norm': '0.6092', 'learning_rate': '1.389e-06', 'num_tokens': '1.951e+04', 'completions/mean_length': '7.855', 'completions/min_length': '1.75', 'completions/max_length': '9', 'completions/clipped_ratio': '0.6523', 'completions/mean_terminated_length': '5.873', 'completions/min_terminated_length': '1.75', 'completions/max_terminated_length': '9', 'rewards/reward_rm/mean': '-0.4444', 'rewards/reward_rm/std': '0.9282', 'reward': '-0.4444', 'reward_std': '0.9282', 'frac_reward_zero_std': '0', 'kl': '0.1152', 'entropy': '2.274', 'clip_ratio/low_mean': '0', 'clip_ratio/high_mean': '0', 'clip_ratio/region_mean': '0', 'clip_ratio/low_min': '0', 'clip_ratio/high_max': '0', 'step_time': '0.3295', 'epoch': '3'}\n", - "{'train_runtime': '26.26', 'train_samples_per_second': '10.97', 'train_steps_per_second': '2.742', 'train_loss': '-0.2278', 'epoch': '3'}\n" + "seed 7: maison 0.335->0.981 (60s) | trl 0.335->0.967 (41s)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "seed 42: maison 0.341->0.949 (56s) | trl 0.341->1.089 (41s)\n", + "seed 42: maison 0.221->0.468 (55s) | trl 0.221->1.033 (48s)\n", "\n", - "gain maison : +0.588 ± 0.070 (temps moyen 61s)\n", - "gain trl : +0.551 ± 0.183 (temps moyen 42s)\n", - "écart trl-maison : -0.037 (2σ = 0.366) -> INCONCLUSIVE\n" + "gain maison : +0.486 ± 0.205 (temps moyen 60s)\n", + "gain trl : +0.670 ± 0.102 (temps moyen 42s)\n", + "écart trl-maison : +0.184 (2σ = 0.410) -> INCONCLUSIVE\n" ] } ], @@ -1497,20 +1551,29 @@ { "cell_type": "markdown", "id": "169570d3", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.026084, + "end_time": "2026-09-28T09:03:25.708194+00:00", + "exception": false, + "start_time": "2026-09-28T09:03:25.682110+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ - "**Lecture chiffree — les trois seeds cote a cote, et l'inversion du seed 7.** `seed 1: maison 0.212->0.688 (59s) | trl 0.212->0.742 (41s)` (GRPO devant), `seed 7: maison 0.227->0.897 (62s) | trl 0.227->0.491 (43s)` (GRPO rate son alignement : 0.491, a quatre milliemes du bruit de fond 0.487), `seed 42: maison 0.341->0.949 (56s) | trl 0.341->1.089 (41s)` (GRPO largement devant, la meilleure finale du notebook). Puis le resume : `gain maison : +0.588 ± 0.070 (temps moyen 61s)` et `écart trl-maison : -0.037 (2σ = 0.366) -> INCONCLUSIVE`. La lecture honnete : le seed 7 EST le resultat — un ecart moyen de -0.037 invisible, mais une dispersion qui fait que GRPO tantot depasse large le gain du bras maison (seed 42), tantot n'aligne pas (seed 7) ; le bras maison, lui, gagne dans les trois cas (0.688, 0.897, 0.949) avec une dispersion 2.6 fois plus petite (0.070 vs 0.183 au tableau final). Le 2σ = 0.366 englobe largement le -0.037 : aucune conclusion n'est permise, et c'est la conclusion.\n" + "**Lecture chiffrée — les quatre seeds côte à côte, et la leçon de dispersion.** `seed 0: maison 0.252->0.981 (64s) | trl 0.252->0.956 (43s)`, `seed 1: maison 0.266->0.590 (62s) | trl 0.266->0.798 (38s)`, `seed 7: maison 0.335->0.981 (60s) | trl 0.335->0.967 (41s)`, `seed 42: maison 0.221->0.468 (55s) | trl 0.221->1.033 (48s)`. Puis le résumé : `gain maison : +0.486 ± 0.205 (temps moyen 60s)`, `gain trl : +0.670 ± 0.102 (temps moyen 42s)` et `écart trl-maison : +0.184 (2σ = 0.410) -> INCONCLUSIVE`. La lecture honnête : l'écart moyen (+0.184) reste sous le 2σ (0.410), aucune conclusion statistique n'est permise — mais la **dispersion** est le résultat visible à l'œil nu dans les quatre lignes : le bras maison touche deux fois le sommet (0.981 aux seeds 0 et 7) et s'effondre deux fois (0.590 au seed 1, 0.468 au seed 42 — un gain réduit à +0.247, le plus faible du tableau), quand le GRPO s'aligne sur les quatre seeds (0.956 à 1.033) avec une volatilité deux fois plus faible (±0.102 vs ±0.205) et la meilleure finale absolue du notebook (1.033, seed 42). La régularité du GRPO contre la volatilité du bras maison : c'est la conclusion, et l'INCONCLUSIVE porte dessus — cet écart de régularité, lui, ne demande pas de test statistique pour être lu.\n" ] }, { "cell_type": "markdown", - "id": "7b9ba3243a4e", + "id": "2d4324f5f1d6", "metadata": { "papermill": { - "duration": 0.010007, - "end_time": "2026-09-15T13:07:34.714126", + "duration": 0.027616, + "end_time": "2026-09-28T09:03:25.765243+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.704119", + "start_time": "2026-09-28T09:03:25.737627+00:00", "status": "completed" }, "tags": [] @@ -1520,26 +1583,30 @@ "\n", "Lignes de code comptées sur les sources du notebook livré (lignes non vides des\n", "définitions, docstrings incluses) : bras maison = ValueNet + token_logps + rollout +\n", - "seqs_to_x + train_ppo_maison ; bras trl = reward_rm + train_grpo_trl (incluant\n", - "le bloc GRPOConfig et la construction du dataset)." + "seqs_to_x + train_ppo_maison = 60 ; bras trl = reward_rm + train_grpo_trl (incluant\n", + "le bloc GRPOConfig et la construction du dataset) = 36 ; bras historique = juge au\n", + "format lib (contenu, conv_get_reward, rm_pos_scores, make_pairs_raw, x_from_raw,\n", + "make_rm_gpt2) + train_ppo_trl = 85 — même convention (le juge compte dans le bras,\n", + "comme reward_rm compte dans le bras GRPO ; le RM custom de la section 2, lui, est\n", + "commun aux deux premiers bras et compté nulle part).\n" ] }, { "cell_type": "code", - "execution_count": 11, - "id": "ec10c1805316", + "execution_count": 13, + "id": "030b80552b53", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T13:07:34.734633Z", - "iopub.status.busy": "2026-09-15T13:07:34.733633Z", - "iopub.status.idle": "2026-09-15T13:07:34.740247Z", - "shell.execute_reply": "2026-09-15T13:07:34.739244Z" + "iopub.execute_input": "2026-09-28T09:03:25.822237Z", + "iopub.status.busy": "2026-09-28T09:03:25.822237Z", + "iopub.status.idle": "2026-09-28T09:03:25.830570Z", + "shell.execute_reply": "2026-09-28T09:03:25.829052Z" }, "papermill": { - "duration": 0.017121, - "end_time": "2026-09-15T13:07:34.740247", + "duration": 0.036173, + "end_time": "2026-09-28T09:03:25.831568+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.723126", + "start_time": "2026-09-28T09:03:25.795395+00:00", "status": "completed" }, "tags": [] @@ -1549,13 +1616,14 @@ "name": "stdout", "output_type": "stream", "text": [ - " PPO maison trl.GRPOTrainer\n", - "gain vraie récomp. 0.588 0.551\n", - "dispersion (std) 0.070 0.183\n", - "temps moyen / run 61s 42s\n", - "lignes de code 60 36\n", - "critic value net (appris) groupe (structural)\n", - "dépendances torch +trl, datasets\n" + " PPO maison trl.GRPOTrainer trl.PPOTrainer\n", + "gain vraie récomp. 0.486 0.670 0.739\n", + "dispersion (std) 0.205 0.102 (seed 0 seul)\n", + "temps moyen / run 60s 42s 15s\n", + "lignes de code 60 36 85\n", + "critic value net (appris)groupe (structural) value head + GAE\n", + "dépendances torch +trl, datasets+trl (experimental)\n", + "NB : PPO-trl mesuré au seed 0 uniquement (section 5bis) ; maison/GRPO sur 4 seeds (section 6)\n" ] } ], @@ -1563,22 +1631,33 @@ "# LOC pré-mesurées sur les sources du notebook livré (lignes non vides, docstrings incluses)\n", "loc_m = 60\n", "loc_c = 36\n", + "loc_p = 85\n", "\n", - "print(f\"{'':<24}{'PPO maison':>20}{'trl.GRPOTrainer':>20}\")\n", - "print(f\"{'gain vraie récomp.':<24}{gains_m.mean():>20.3f}{gains_c.mean():>20.3f}\")\n", - "print(f\"{'dispersion (std)':<24}{gains_m.std():>20.3f}{gains_c.std():>20.3f}\")\n", - "print(f\"{'temps moyen / run':<24}{ts_m.mean():>19.0f}s{ts_c.mean():>19.0f}s\")\n", - "print(f\"{'lignes de code':<24}{loc_m:>20}{loc_c:>20}\")\n", - "print(f\"{'critic':<24}{'value net (appris)':>20}{'groupe (structural)':>20}\")\n", - "print(f\"{'dépendances':<24}{'torch':>20}{'+trl, datasets':>20}\")" + "print(f\"{'':<24}{'PPO maison':>19}{'trl.GRPOTrainer':>19}{'trl.PPOTrainer':>19}\")\n", + "print(f\"{'gain vraie récomp.':<24}{gains_m.mean():>19.3f}{gains_c.mean():>19.3f}{r_ppotrl - r_avant_ppo:>19.3f}\")\n", + "print(f\"{'dispersion (std)':<24}{gains_m.std():>19.3f}{gains_c.std():>19.3f}{'(seed 0 seul)':>19}\")\n", + "print(f\"{'temps moyen / run':<24}{ts_m.mean():>18.0f}s{ts_c.mean():>18.0f}s{t_ppotrl:>18.0f}s\")\n", + "print(f\"{'lignes de code':<24}{loc_m:>19}{loc_c:>19}{loc_p:>19}\")\n", + "print(f\"{'critic':<24}{'value net (appris)':>19}{'groupe (structural)':>19}{'value head + GAE':>19}\")\n", + "print(f\"{'dépendances':<24}{'torch':>19}{'+trl, datasets':>19}{'+trl (experimental)':>19}\")\n", + "print(\"NB : PPO-trl mesuré au seed 0 uniquement (section 5bis) ; maison/GRPO sur 4 seeds (section 6)\")\n" ] }, { "cell_type": "markdown", "id": "57e07771", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.029588, + "end_time": "2026-09-28T09:03:25.889548+00:00", + "exception": false, + "start_time": "2026-09-28T09:03:25.859960+00:00", + "status": "completed" + }, + "tags": [] + }, "source": [ - "**Lecture chiffree — le tableau final, ligne par ligne.** Les libelles commutes : `gain vraie récomp.` — 0.588 contre 0.551, les deux bras apprennent autant en moyenne ; `dispersion (std)` — 0.070 contre 0.183, le bras maison est 2.6 fois plus reproductible ; `temps moyen / run` — 61 s contre 42 s, GRPO est 31 % plus rapide (pas de forward du value net) ; `lignes de code` — le SOTA divise le code par 1.7 ; `critic` — value net appris contre moyenne de groupe structurelle ; `dépendances` — torch seul contre torch + trl + datasets. C'est le contrat du from scratch rendu visible : payer 19 s par run pour un critic que l'on voit apprendre (la vloss de la section 4) et une dispersion divisee par 2.6, ou deleguer a trl pour la vitesse au prix d'une variance que rien ne rend visible — sauf un multi-seed, que ce notebook justement execute. L'INCONCLUSIVE final n'est pas un echec de la comparaison : c'est la mesure de ce que couterait de trancher.\n" + "**Lecture chiffrée — le tableau final, ligne par ligne.** `gain vraie récomp.` — 0.486 / 0.670 / 0.739 : les trois bras apprennent, le trainer historique affiche le meilleur gain mais au seed 0 seul, quand maison et GRPO sont mesurés sur 4 seeds ; `dispersion (std)` — 0.205 contre 0.102 : le bras maison est 2 fois moins reproductible que GRPO, le bras historique n'a pas de barre d'erreur (une seule passe) ; `temps moyen / run` — 60 s / 42 s / 15 s : GRPO est 30 % plus rapide que le bras maison (pas de forward du value net), le trainer historique est 4 fois plus rapide que le bras maison (tout en batch, une seule passe) ; `lignes de code` — 60 / 36 / 85 : le paradoxe du bras historique, son cœur `train_ppo_trl` est le plus court des trois, mais l'interface `reward_model` au format de la lib impose de reconstruire le juge — qui pèse à lui seul plus du double du cœur du trainer — : l'interface est le produit, pas le trainer ; `critic` — value net appris / moyenne de groupe structurelle / value head + GAE : trois réponses distinctes à la question de la baseline ; `dépendances` — torch seul / + trl, datasets / + trl (experimental, module déclaré instable). C'est le contrat du from scratch rendu visible en six lignes, avec l'INCONCLUSIVE de la section 6 en filigrane : trancher maison contre GRPO coûterait un multi-seed plus profond, et le bras historique — le plus rapide, au gain le plus haut sur sa seule passe — mériterait le même traitement avant tout verdict." ] }, { @@ -1586,10 +1665,10 @@ "id": "22046ce56a6a", "metadata": { "papermill": { - "duration": 0.008502, - "end_time": "2026-09-15T13:07:34.758962", + "duration": 0.026132, + "end_time": "2026-09-28T09:03:25.945271+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.750460", + "start_time": "2026-09-28T09:03:25.919139+00:00", "status": "completed" }, "tags": [] @@ -1619,13 +1698,13 @@ }, { "cell_type": "markdown", - "id": "084508c9ea2e", + "id": "3abb9ca2eedd", "metadata": { "papermill": { - "duration": 0.007677, - "end_time": "2026-09-15T13:07:34.774645", + "duration": 0.028433, + "end_time": "2026-09-28T09:03:25.992935+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.766968", + "start_time": "2026-09-28T09:03:25.964502+00:00", "status": "completed" }, "tags": [] @@ -1638,15 +1717,15 @@ "| La baseline est LE choix de design | value net appris (PPO) vs moyenne de groupe (GRPO) — même récompense terminale, même budget |\n", "| RLHF optimise le juge, pas le vrai | le gain se mesure en **vraie** récompense, jamais en score RM |\n", "| GRPO = PPO sans critic | mêmes gains à budget égal (section 6), zéro paramètre de value |\n", - "| `trl.PPOTrainer` a existé | supprimé upstream — une comparaison SOTA est **datée**, le notebook documente la succession |\n", - "| L'interface reward est le produit | `reward_funcs(prompts, completions, completion_ids)` : le juge est une fonction, d'où le RLVR |\n", + "| `trl.PPOTrainer` a existé | **déplacé** upstream vers `trl.experimental.ppo` (section 5bis), pas supprimé — une comparaison SOTA est **datée** |\n", + "| L'interface reward est le produit | `reward_funcs(prompts, completions, completion_ids)` côté GRPO, `reward_model` au format lib côté PPO : le juge épouse la pile |\n", "\n", "**Ce que ce notebook ajoute à la série** : `rlpt_1` écrivait PPO from scratch sur sa\n", "propre tâche ; `rlpt_0e` branchait le SOTA **offline** (DPO) sur le monde de `rlpt_0`.\n", "Ici la boucle fermée **juge → échantillonnage → politique** passe en ligne, le SOTA\n", "de l'époque (`GRPOTrainer`) est départagé contre la recette maison à budget exact de\n", - "rollouts, et la disparition de `PPOTrainer` — le sujet initial de l'issue — est\n", - "mesurée et documentée." + "rollouts, et le déplacement de `PPOTrainer` — le sujet initial de l'issue — est mesuré,\n", + "documenté, et le trainer historique remis en service (section 5bis).\n" ] }, { @@ -1654,10 +1733,10 @@ "id": "01c27b16f0fe", "metadata": { "papermill": { - "duration": 0.007581, - "end_time": "2026-09-15T13:07:34.790303", + "duration": 0.018204, + "end_time": "2026-09-28T09:03:26.038739+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.782722", + "start_time": "2026-09-28T09:03:26.020535+00:00", "status": "completed" }, "tags": [] @@ -1671,20 +1750,20 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "id": "e88af050127d", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T13:07:34.810043Z", - "iopub.status.busy": "2026-09-15T13:07:34.809043Z", - "iopub.status.idle": "2026-09-15T13:07:34.814434Z", - "shell.execute_reply": "2026-09-15T13:07:34.813429Z" + "iopub.execute_input": "2026-09-28T09:03:26.095396Z", + "iopub.status.busy": "2026-09-28T09:03:26.095396Z", + "iopub.status.idle": "2026-09-28T09:03:26.101767Z", + "shell.execute_reply": "2026-09-28T09:03:26.100255Z" }, "papermill": { - "duration": 0.01862, - "end_time": "2026-09-15T13:07:34.815436", + "duration": 0.036884, + "end_time": "2026-09-28T09:03:26.101767+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.796816", + "start_time": "2026-09-28T09:03:26.064883+00:00", "status": "completed" }, "tags": [] @@ -1712,20 +1791,20 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "id": "5125df3a2ddc", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T13:07:34.838621Z", - "iopub.status.busy": "2026-09-15T13:07:34.837617Z", - "iopub.status.idle": "2026-09-15T13:07:34.843517Z", - "shell.execute_reply": "2026-09-15T13:07:34.842514Z" + "iopub.execute_input": "2026-09-28T09:03:26.156528Z", + "iopub.status.busy": "2026-09-28T09:03:26.155519Z", + "iopub.status.idle": "2026-09-28T09:03:26.161917Z", + "shell.execute_reply": "2026-09-28T09:03:26.160911Z" }, "papermill": { - "duration": 0.017964, - "end_time": "2026-09-15T13:07:34.843517", + "duration": 0.036497, + "end_time": "2026-09-28T09:03:26.161917+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.825553", + "start_time": "2026-09-28T09:03:26.125420+00:00", "status": "completed" }, "tags": [] @@ -1753,20 +1832,20 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "id": "43e03e61a997", "metadata": { "execution": { - "iopub.execute_input": "2026-09-15T13:07:34.860333Z", - "iopub.status.busy": "2026-09-15T13:07:34.859332Z", - "iopub.status.idle": "2026-09-15T13:07:34.864380Z", - "shell.execute_reply": "2026-09-15T13:07:34.863376Z" + "iopub.execute_input": "2026-09-28T09:03:26.215586Z", + "iopub.status.busy": "2026-09-28T09:03:26.215586Z", + "iopub.status.idle": "2026-09-28T09:03:26.222316Z", + "shell.execute_reply": "2026-09-28T09:03:26.221276Z" }, "papermill": { - "duration": 0.013551, - "end_time": "2026-09-15T13:07:34.864883", + "duration": 0.032836, + "end_time": "2026-09-28T09:03:26.223324+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.851332", + "start_time": "2026-09-28T09:03:26.190488+00:00", "status": "completed" }, "tags": [] @@ -1797,10 +1876,10 @@ "id": "3db73a18a0ff", "metadata": { "papermill": { - "duration": 0.007656, - "end_time": "2026-09-15T13:07:34.883806", + "duration": 0.023101, + "end_time": "2026-09-28T09:03:26.270756+00:00", "exception": false, - "start_time": "2026-09-15T13:07:34.876150", + "start_time": "2026-09-28T09:03:26.247655+00:00", "status": "completed" }, "tags": [] @@ -1837,18 +1916,18 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.16" }, "papermill": { "default_parameters": {}, - "duration": 580.687979, - "end_time": "2026-09-15T13:07:36.010227", + "duration": 674.076681, + "end_time": "2026-09-28T09:03:27.503950+00:00", "environment_variables": {}, "exception": null, "input_path": "rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb", - "output_path": "rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison.ipynb", + "output_path": "rlpt_0f_comparaison_GRPO_TRL_et_PPO_maison_output.ipynb", "parameters": {}, - "start_time": "2026-09-15T12:57:55.322248", + "start_time": "2026-09-28T08:52:13.427269+00:00", "version": "2.6.0" }, "widgets": {