GRPO — RL без критика
Как DeepSeek убрали из PPO value-модель: группа ответов на один промпт, нормировка наград внутри группы, клиппинг, KL-оценка k3 — и почему это вдвое дешевле по памяти.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В уроке 6.3.2 вы видели цену классического RLHF с PPO (сам алгоритм — в уроке 6.0.3): в памяти одновременно живут четыре модели, две из них обучаются. Самая неудобная — критиккритик, value-модельEN critic / value modelСеть, предсказывающая ожидаемую награду из текущего состояния; в PPO служит baseline для каждого токена. (value-модель) . Он размером с саму LLM, его надо учить параллельно с политикой, а учить его трудно: награда приходит один раз, в конце ответа, и критику приходится угадывать по началу ответа, чем всё закончится.
В феврале 2024 года DeepSeek в статье о DeepSeekMath предложили выбросить критика совсем. Вместо него — простая идея из урока 6.3.1: baseline. Только считается он не обученной сетью, а честно, по выборке: сгенерируем на один и тот же вопрос несколько ответов и сравним каждый со средним. Алгоритм назвали GRPOGRPO (групповая относительная оптимизация политики)EN Group Relative Policy Optimization (GRPO)PPO без критика: на промпт генерируется группа из G ответов, преимущество — (r − среднее) / std по группе. DeepSeekMath, 2024. — Group Relative Policy Optimization. Через год именно он обучил DeepSeek-R1, а сегодня GRPO и его потомки — стандарт RL для рассуждений.
Аналогия — оценка «по кривой». Учитель не знает заранее, насколько трудна контрольная. Он смотрит, как её написал весь класс: «пятёрка» — это заметно выше среднего по классу, «двойка» — заметно ниже. Класс здесь — группа ответов модели на один промпт.
От PPO к GRPO
Заголовок раздела «От PPO к GRPO»flowchart LR subgraph PPO["PPO: 4 модели"] q1["Промпт q"] --> p1["Политика"] --> o1["1 ответ o"] o1 --> rm1["Reward model"] --> gae["GAE"] o1 --> v1["Критик V — обучается"] --> gae o1 --> ref1["Референс"] -->|"KL в награду"| gae gae --> a1["A_t на каждый токен"] end subgraph GRPO["GRPO: без критика"] q2["Промпт q"] --> p2["Политика"] --> og["G ответов o_1 … o_G"] og --> rm2["Награды r_1 … r_G: правила или RM"] --> norm["(r_i − mean) / std по группе"] norm --> a2["A_i — одно на весь ответ"] og --> ref2["Референс"] -->|"KL прямо в loss"| loss["Loss"] a2 --> loss end
Что изменилось:
- Нет критика. Baselineбазовая линия (baseline)EN baselineЧисло, которое вычитают из награды; не смещает градиент политики, но снижает его дисперсию. — средняя награда группы. Это эмпирическая оценка функции ценности из урока 6.0.1: «сколько обычно получает модель на этом вопросе».
- Нет GAEобобщённая оценка преимущества (GAE)EN generalized advantage estimation (GAE)Schulman и др., 2015. Смешивает оценки преимущества разной длины с коэффициентом λ. (урок 6.0.3). Преимуществопреимущество (advantage)EN advantageA = r − b или A(s, a) = Q(s, a) − V(s): насколько действие лучше среднего в этой ситуации. одно на весь ответ и одинаково для всех его токенов. Мы теряем покомпонентную «раздачу заслуг», зато избавляемся от самой капризной части PPO.
- KL переехал из награды в loss. В PPO штраф подмешивали к награде каждого токена (урок 6.3.1). В GRPO награда остаётся «чистой», а KL вычитается отдельным слагаемым в функции потерь.
- Награды могут быть правилами. В самой DeepSeekMath награду давала нейросетевая reward model. Но GRPO всё равно, откуда число: в R1-Zero (следующий урок) это уже проверка ответа программой — проверяемое вознаграждениепроверяемое вознаграждениеEN verifiable reward (RLVR). В статье есть и вариант с наградой за каждый шаг решения: её выставляет процессная модель вознаграждения (PRMпроцессная модель вознаграждения (PRM)EN process reward model (PRM)Оценивает каждый шаг рассуждения, а не только итоговый ответ. В DeepSeek-R1 описана как «неудачная попытка».), а преимущество токена равно сумме нормированных наград всех последующих шагов. Но в историю вошёл простой вариант — одна награда за весь ответ.
Групповое преимущество
Заголовок раздела «Групповое преимущество»На промпт текущая политика генерирует группу и получает награды . Преимущество:
Что здесь написано. Числитель — «насколько этот ответ лучше среднего по своей группе». Знаменатель — стандартное
отклонение наград группы (урок 1.3.1; здесь «популяционное»: делим на , как np.std по умолчанию; torch.std и TRL
делят на , и преимущества у них чуть меньше по модулю) —
приводит разброс к единице: неважно, награды от 0 до 1 или от −5 до 5, преимущества будут одного масштаба.
Пример для первой строки таблицы ниже: награды , среднее 0.25, отклонения ,
std , и .
Посчитаем для и награды «1 за верный ответ, 0 за неверный»:
| Награды группы | Среднее | std | Преимущества | Что делает градиент |
|---|---|---|---|---|
| 0.25 | 0.433 | сильно поднимает редкий верный ответ | ||
| 0.5 | 0.5 | симметрично | ||
| 0.75 | 0.433 | сильно опускает редкую ошибку | ||
| 0 | 0 | ничего: сигнала нет |
Последняя строка — главный «подводный камень» GRPO. Если все ответы группы одинаково хороши или одинаково плохи, сравнивать не с чем, и вся группа — сгенерированная, между прочим, дорогой ценой — не даёт градиента. Такие группы называют вырожденнымивырожденная группаEN degenerate group (zero-variance group)Все ответы группы получили одинаковую награду — все преимущества нулевые, градиента нет. (так их называет и README MiniMind). Вероятность, что группа «живая», при точности модели на этом промпте:
| Точность | |||||
|---|---|---|---|---|---|
| 0.02 или 0.98 | 0.04 | 0.08 | 0.15 | 0.28 | 0.73 |
| 0.1 или 0.9 | 0.18 | 0.34 | 0.57 | 0.81 | 0.999 |
| 0.5 | 0.5 | 0.88 | 0.99 | 1.0 | 1.0 |
Отсюда два практических правила. Первое: задачи должны быть по силам модели — ни слишком лёгкие, ни безнадёжные. Второе: группа побольше спасает трудные промпты. В DeepSeekMath брали , в DeepSeek-R1 — 16, в TRL по умолчанию 8. А DAPO (урок 6.4.3) просто выбрасывает вырожденные группы и досэмплирует новые.
Loss GRPO по частям
Заголовок раздела «Loss GRPO по частям»Соберём формулу из знакомых кусков.
Шаг 1. REINFORCE с групповым baseline. Для каждого токена ответа хотим увеличить пропорционально — это градиент политики из урока 6.3.1.
Шаг 2. Отношение вероятностей и клиппинг из PPO. Ответы сгенерированы «старой» политикой . Если по одному батчу делать несколько шагов, нужна поправка и клиппинг — всё как в PPO (уроки 6.0.3 и 6.3.2).
Шаг 3. Усреднение. Сначала среднее по токенам внутри ответа (множитель ), потом среднее по ответам.
Шаг 4. KL к референсу — вычитается на каждом токене с коэффициентом .
Целиком (формула (3) из DeepSeekMath):
Loss для оптимизатора — . Как читать: «для каждого ответа группы, для каждого его токена — клиппированное “поднять/опустить по преимуществу” минус штраф за уход от референса; усреднить по токенам ответа, потом по ответам».
KL-оценка k3
Заголовок раздела «KL-оценка k3»Точную KL-дивергенциюKL-дивергенция (дивергенция Кульбака — Лейблера)EN Kullback–Leibler divergenceKL(p ‖ q) = Σ p log(p / q) = H(p, q) − H(p) ≥ 0. Несимметрична, поэтому не настоящее расстояние. между распределениями над всеми возможными ответами не посчитать. Её оценивают по сэмплу — по токенам, которые модель действительно сгенерировала. Обозначим . Есть три популярные оценки (по блогу Джона Шульмана):
| Оценка | Формула | Несмещённая? | Знак |
|---|---|---|---|
| k1 | да | бывает отрицательной | |
| k2 | нет (но близка при малом расхождении) | ||
| k3 | да | всегда |
Оценка k1 — та самая разность логарифмов, которой вы считали KL в уроке 6.3.1. GRPO использует k3 — оценка KL «k3»оценка KL «k3»EN k3 KL estimatorρ − log ρ − 1, где ρ = π_ref / π_θ: несмещённая оценка KL(π_θ ‖ π_ref) по сэмплу, всегда ≥ 0. В verl — kl_loss_type=low_var_kl. (в статье — формула (4)). Почему она несмещённая: токены сэмплированы из , а . Значит, , и . А неотрицательна она потому, что для любого .
Пример с числами.
- Модель дала токену 0.9, референс — 0.3: , k1 , k3 .
- Наоборот, модель 0.3, референс 0.9: , k1 (отрицательная!), k3 .
Мы проверили на распределениях , и 100 тысячах сэмплов:
точная KL = 0.2197, среднее k1 = 0.2209, среднее k3 = 0.2190. Обе оценки попадают в цель, но стандартное отклонение
у k1 — 0.62, а у k3 — 0.24, в 2.6 раза меньше. И 40% значений k1 отрицательны: штраф, который на отдельных токенах
«поощряет» уход от референса, — плохая идея для обучения. В verl эта оценка так и называется: kl_loss_type=low_var_kl.
Сколько брать : в DeepSeekMath — 0.04, в R1 — уже 0.001. А DAPO и TRL по умолчанию ставят : для длинных рассуждений модель должна далеко уйти от исходной, и якорь мешает. Плюс без KL не нужна референсная модель — минус одна модель в памяти.
Почему это дешевле PPO — и где подвох
Заголовок раздела «Почему это дешевле PPO — и где подвох»Прикинем память для политики на 7B так же, как в уроке 6.3.2 (16 байт на обучаемый параметр, 2 байта на замороженный):
| Метод | Модели в памяти | Веса + оптимизатор |
|---|---|---|
| PPO | политика + критик (обучаются), референс + RM | ≈ 252 ГБ |
| GRPO с RM (как в DeepSeekMath) | политика + референс + RM | ≈ 140 ГБ |
| GRPO с правиловой наградой (R1-Zero) | политика + референс | ≈ 126 ГБ |
| GRPO, (DAPO, TRL по умолчанию) | только политика | ≈ 112 ГБ |
Экономия почти вдвое — и минус целая обучаемая сеть со своими гиперпараметрами. Но бесплатно ничего не бывает: на каждый промпт теперь нужно ответов, и роллаутроллаут (генерация ответов для RL)EN rolloutЭтап RL-шага, на котором текущая политика генерирует ответы на промпты. В GRPO — самая дорогая часть. дорожает в раз. В DeepSeekMath это 64 ответа до 1024 токенов на вопрос, батч — 1024 ответа (16 вопросов × 64). Генерация и раньше была самой медленной частью RL, а в GRPO её объём умножается на . Поэтому вся RL-инфраструктура 2025–2026 годов (vLLM и SGLang внутри verl, асинхронные системы вроде slime) оптимизирует прежде всего генерацию.
Пять методов в одной формуле
Заголовок раздела «Пять методов в одной формуле»В DeepSeekMath есть красивое наблюдение (§5.2): почти все методы пост-трейнинга дают градиент вида «коэффициент × ». Различаются данные и коэффициент:
| Метод | Чьи ответы | Коэффициент градиента |
|---|---|---|
| SFT | образцовые, от людей | 1 |
| RFT (rejection sampling fine-tuning) | сэмплы исходной модели, только верные | 1 для верных, неверные выброшены |
| Online RFT | сэмплы текущей модели, только верные | 1 для верных |
| PPO | сэмплы текущей модели | от критика |
| GRPO | группы сэмплов текущей модели | от группы, со знаком |
Эксперимент статьи: GRPO лучше Online RFT, а Online RFT лучше обычного RFT. Онлайн-данные важнее офлайн, а умение не только поощрять верное, но и наказывать неверное (отрицательный коэффициент) — важнее, чем кажется.
Практика
Заголовок раздела «Практика»Задание: групповые преимущества и GRPO loss
Заголовок раздела «Задание: групповые преимущества и GRPO loss»Реализуйте четыре функции на NumPy: групповые преимущества, KL-оценку k3, клиппированную цель и полный loss GRPO с маской паддинга. Тесты сверят значения с формулами этого урока.
import numpy as np
def group_advantages(rewards, eps=1e-6):
"""Групповые преимущества GRPO: (r - среднее группы) / (std группы + eps).
rewards — массив [B, G]: B промптов, на каждый G ответов.
Среднее и std считайте по каждой строке (axis=1, keepdims=True);
std — «популяционное» (ddof=0), как np.std по умолчанию.
Вернуть массив [B, G].
"""
# TODO: ваш код
...
def kl_k3(logp, logp_ref):
"""Оценка KL(π_θ ‖ π_ref) «k3» для каждого токена: ρ - log ρ - 1, где ρ = π_ref / π_θ.
logp, logp_ref — лог-вероятности токенов (числа или массивы одной формы).
Подсказка: log ρ = logp_ref - logp.
"""
# TODO: ваш код
...
def clipped_surrogate(logp, logp_old, adv, eps=0.2):
"""Клиппированная цель для каждого токена: min(ρ·A, clip(ρ, 1-eps, 1+eps)·A), ρ = π_θ / π_old.
logp, logp_old — [N, T]; adv — [N] (одно преимущество на весь ответ).
Вернуть массив [N, T].
"""
# TODO: ваш код
...
def grpo_loss(logp, logp_old, logp_ref, adv, mask, eps=0.2, beta=0.04):
"""Loss GRPO (со знаком минус — для минимизации), как в DeepSeekMath.
1) на каждом токене: clipped_surrogate(...) - beta * kl_k3(...);
2) среднее по токенам ОТВЕТА (mask = 1) внутри каждого ответа — множитель 1/|o_i|;
3) среднее по всем ответам; 4) поставить минус. Вернуть float.
"""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»:
print(group_advantages([[1, 0, 0, 0], [1, 1, 0, 0]]))
print(kl_k3(np.log(0.9), np.log(0.3))) # ≈ 0.4319
lp = np.log([[0.5, 0.4, 0.9], [0.2, 0.3, 0.6]])
print(grpo_loss(lp, lp, lp, [1.0, -1.0], [[1, 1, 1], [1, 1, 0]])) # 0.0 в начале шага
После решения нажмите «Запустить»: последняя строка напечатает 0.0. Это не ошибка — см. следующий раздел.
Локально: loss равен нулю, а модель учится
Заголовок раздела «Локально: loss равен нулю, а модель учится»Частый вопрос новичков, впервые запустивших GRPO в TRL или verl: «loss всё время около нуля — обучение сломано?» Проверим на PyTorch, что происходит. Возьмём группу из 4 ответов разной длины и посмотрим на градиент loss прямо по лог-вероятности каждого токена.
import torch
rewards = torch.tensor([1.0, 1.0, 0.0, 0.0]) # группа из G = 4 ответов: два верных, два нетlengths = torch.tensor([3, 6, 3, 6]) # длины ответов в токенахadv = (rewards - rewards.mean()) / (rewards.std(unbiased=False) + 1e-6) # unbiased=False — делим на G, как в формуле
# «Лог-вероятности» токенов: [4 ответа, 6 позиций]. Сделаем их листом графа,# чтобы посмотреть градиент loss прямо по log π(токена).torch.manual_seed(0)logp = torch.log(torch.rand(4, 6) * 0.5 + 0.4).requires_grad_(True)mask = (torch.arange(6)[None, :] < lengths[:, None]).float() # 1 — токен ответа, 0 — паддинг
ratio = torch.exp(logp - logp.detach()) # одно обновление на батч: π_old = π_θ, ρ ≡ 1surr = torch.min(ratio * adv[:, None], ratio.clamp(0.8, 1.2) * adv[:, None])loss = -((surr * mask).sum(1) / mask.sum(1)).mean() # формула DeepSeekMath (β = 0)loss.backward()
print(f"loss = {round(loss.item(), 4) + 0.0}")for i in range(4): w = logp.grad[i, 0].item() print(f"ответ {i}: A = {adv[i]:+.2f}, длина {lengths[i]}, d loss / d log π(токена) = {w:+.4f}")Вывод:
loss = 0.0ответ 0: A = +1.00, длина 3, d loss / d log π(токена) = -0.0833ответ 1: A = +1.00, длина 6, d loss / d log π(токена) = -0.0417ответ 2: A = -1.00, длина 3, d loss / d log π(токена) = +0.0833ответ 3: A = -1.00, длина 6, d loss / d log π(токена) = +0.0417Что видно:
- Loss = 0, потому что при каждый ответ вносит ровно , а нормированные преимущества группы в сумме дают ноль. Значение loss в GRPO почти ничего не говорит — смотрите на награду, длину ответов и KL.
- Градиент не ноль. Отрицательный знак означает, что оптимизатор будет поднимать токенов верных ответов, положительный — опускать у неверных. Формула веса: , например .
- Вес зависит от длины. Токен короткого неверного ответа «наказывается» вдвое сильнее, чем токен длинного неверного. Длинный неверный ответ отделывается лёгким испугом — и модель потихоньку учится, что ошибаться лучше многословно. Этот перекос множителя нашли в 2025 году и исправили в DAPO и Dr. GRPO — урок 6.4.3.
Проверь себя
Заголовок раздела «Проверь себя»- GRPO — PPO без критика: baseline — средняя награда группы из ответов, преимущество одно на весь ответ.
- Loss: клиппированное на каждом токене, минус KL-оценка k3; усреднение — по токенам ответа, затем по группе.
- Вырожденные группы (все награды равны) не дают сигнала: задачи должны быть по силам, а — достаточно большим.
- Экономия — минус критик (почти вдвое меньше памяти). Цена — в раз больше генерации.
- Множитель и деление на std вносят скрытые перекосы — их исправят в 2025 году.
Дальше: DeepSeek-R1-Zero — GRPO с правиловыми наградами прямо на базовой модели, рост длины рассуждений и «момент озарения».