Модуль 6. LLM как политика: токены, логарифмы вероятностей и KL
Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.
Скачать файлы курса (.zip) 382,1 КБАрхив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.
Причина остановки и семантика задачи
Контракт конкретной задачи говорит: выбранный моделью EOS означает естественное завершение ответа, а достижение max_new_tokens — внешнее ограничение генерации. Система вернула два ответа:
-
модель выбрала EOS на 47-м новом токене;
-
генерация остановилась на 64-м токене из-за
max_new_tokens=64, хотя EOS не был выбран.
Для каждого случая укажите stop_reason, terminated и truncated. Затем объясните:
- почему те же сырые причины остановки могли бы интерпретироваться иначе в другой задаче;
- почему длина текста не восстанавливает причину остановки;
- почему
stop_reasonнужно хранить отдельно от двух булевых флагов.
Показать ответ и решение
При заданном контракте:
-
stop_reason="eos",terminated=True,truncated=False; -
stop_reason="length",terminated=False,truncated=True.
Но эти значения следуют из контракта задачи, а не из названия события само по себе. Например, специальная стоп-строка может означать естественный конец структурированного ответа, а может быть лишь техническим разделителем, после которого среда должна продолжить эпизод. Аналогично, истинное терминальное событие может совпасть с границей буфера, поэтому в общем контракте не следует запрещать одновременные флаги автоматически.
Одинаковая длина может возникнуть после EOS, лимита, тайм-аута или внешнего прерывания. Поэтому нужно хранить исходную машинно читаемую причину и отдельно отображать её в семантику terminated/truncated, принятую для данной задачи.
Достаточно ли префикса
Рассмотрите два процесса.
-
LLM генерирует обычный текст без внешних вызовов.
-
LLM выдаёт вызов веб-поиска, а среда возвращает результат, который зависит от текущего состояния сети.
В каком процессе состояние достаточно для марковской модели? Что нужно добавить во втором процессе? Отделите действия модели от наблюдений среды.
Показать ответ и решение
Для обычной генерации состояние достаточно, если шаблон запроса и все ограничения генерации фиксированы. Переход после токена — детерминированное добавление к префиксу.
Во втором процессе нужно включить в состояние или наблюдение результат инструмента и релевантное внешнее состояние. Вызов инструмента является действием модели; возвращённый документ — наблюдение среды. Его нельзя считать выбранным токеном политики только потому, что интерфейс сериализовал всё в один текст.
Ошибка на один токен
Дан тензор логитов формы и input_ids формы . Разработчик вычислил
selected = log_softmax(logits).gather(-1, input_ids[..., None])без сдвига. Почему размерности выглядят правдоподобно, но смысл результата неверен? Напишите правильное соответствие между позицией логитов и целевым токеном.
Показать ответ и решение
Логиты на позиции предсказывают токен , а не токен . Без сдвига результат сопоставляет логитам уже известный входной токен той же позиции. Правильная пара:
В коде используются logits[:, :-1, :] и input_ids[:, 1:].
Что сообщает PPL
Модель A имеет меньшую PPL ответа, чем модель B. Можно ли заключить, что:
-
A чаще даёт правильные ответы;
-
A полезнее пользователю;
-
A лучше предсказывает выбранные токены при той же токенизации, шаблоне и маске?
Для каждого утверждения дайте ответ и назовите минимум две причины, по которым PPL нельзя без оговорок сравнивать между разными экспериментами.
Показать ответ и решение
-
Правильность — нет.
-
Пользовательская полезность — нет.
-
Среднее качество предсказания выбранных токенов при одинаковом протоколе — да: меньшая PPL означает меньшую среднюю отрицательную логарифмическую вероятность.
Сравнение ломается при другой токенизации, маске, шаблоне диалога, наборе данных или основании логарифма. Кроме того, вероятное продолжение может быть фактически неверным.
«Отрицательный KL»
Для одного сэмплированного токена получено
Означает ли это, что KL между политиками отрицателен? Объясните различие между точным условным KL и одним выборочным логарифмом отношения.
Показать ответ и решение
Нет. Точный условный KL — ожидание по всем действиям текущей политики и неотрицателен:
Один сэмплированный логарифм отношения может быть отрицательным, если базовая политика дала выбранному действию большую вероятность. Только среднее при правильном распределении сэмплирования оценивает KL.
Условия поддержки для k3
Пусть , и
Разберите по отдельности два требования:
-
когда конечен на сэмплах из ;
-
когда верно .
Почему для конечного несмещённого тождества в записанной форме нужна совместимая поддержка распределений? Приведите примеры нарушения в каждом направлении.
Показать ответ и решение
Чтобы был конечен на всех точках, которые может породить , необходимо
то есть . Иначе при , имеем , а .
Для контрольной вариаты используется
Это равно единице только если не имеет массы вне носителя , то есть . Следовательно, для конечного точного тождества в дискретной записи удобно требовать общую положительную поддержку.
Два контрпримера:
- , : , потому что масса на второй точке никогда не наблюдается;
- , : вторая точка встречается под , но , поэтому KL бесконечен.
Численное сглаживание может изменить задачу, но не восстанавливает отсутствующую информацию бесплатно.
Какая политика породила токен
Трансформер выдал базовые логиты. Затем движок применил температурное масштабирование, запретил несколько токенов, выполнил top-p и сэмплировал действие. Тренер сохранил логарифм вероятности из исходного softmax.
Какую ошибку это создаёт в отношении вероятностей PPO? Какие сведения необходимо сохранить, чтобы восстановить фактическую поведенческую политику?
Показать ответ и решение
Знаменатель PPO должен быть вероятностью действия под фактической поведенческой политикой после температуры, запретов и top-p. Базовый softmax даёт другой логарифм вероятности и, при усечении, другой носитель. Отношение вероятностей становится систематически неверным.
Нужно хранить версию весов, версию токенизатора и шаблон, температуру, все маски и штрафы, параметры top-k, top-p и min-p, порядок обработчиков, правила остановки, случайное зерно генератора (seed) и фактический old_logprob после преобразований.
Минимальный контракт роллаута
Вы проектируете пакет данных для RL с проверяемой наградой (reinforcement learning with verifiable rewards, RLVR). Выберите из списка поля, без которых нельзя надёжно воспроизвести функцию потерь:
- текст ответа;
response_ids;response_mask;old_logprobs;policy_version;sampling_config;reward_raw;terminated,truncated,stop_reason;- цвет интерфейса пользователя.
Объясните роль каждого выбранного поля. Почему одного текста ответа недостаточно?
Показать ответ и решение
Нужны response_ids, response_mask, old_logprobs, policy_version, sampling_config, reward_raw, а также terminated, truncated, stop_reason. Текст полезен для аудита, но не заменяет токены.
- идентификаторы фиксируют точные действия;
- маска отделяет действия политики от запроса и токенов дополнения;
- старые логарифмы задают знаменатель отношения;
- версия весов и настройки восстанавливают поведенческую политику;
- исходная награда отделяется от регуляризаторов;
- причина остановки определяет границу эпизода.
Цвет интерфейса не влияет на функцию потерь. Повторная токенизация одного текста может измениться из-за другой версии токенизатора или шаблона.
Логарифм вероятности и PPL
Три активных токена ответа имели вероятности
Вычислите:
-
логарифм вероятности всей последовательности;
-
среднюю отрицательную логарифмическую вероятность;
-
PPL.
Как изменится сумма, если добавить четвёртый токен с вероятностью , и как изменится PPL?
Показать ответ и решение
Средняя отрицательная логарифмическая вероятность:
Следовательно,
Токен с вероятностью добавляет , поэтому сумма не меняется. Но среднее делится уже на четыре, и PPL уменьшается до . Это показывает зависимость нормировки от того, какие токены включены в маску.
Цепное правило KL на дереве глубины два
На первом шаге политики имеют распределения
После первого действия условные распределения второго действия равны
Вычислите
и проверьте, что это значение совпадает с KL между совместными распределениями двух действий.
Показать ответ и решение
Корневой вклад:
Ожидаемый условный вклад:
Итого
Прямое построение четырёх совместных вероятностей даёт то же число; это конечный пример цепного правила.
k1, k2 и k3
Для вычислите , и при:
-
;
-
.
Какой оцениватель меняет знак? Какие два остаются неотрицательными?
Показать ответ и решение
При :
При :
Знак меняет k1. k2 и k3 неотрицательны.
Ядро top-p
Базовое распределение уже отсортировано:
Используется top-p с порогом , причём токен, на котором накопленная масса впервые достигает порога, включается.
-
Какое множество токенов останется?
-
Каково перенормированное распределение?
-
Что изменится при пороге ?
Показать ответ и решение
При пороге минимальный префикс — первые два токена, поскольку . После перенормировки:
При пороге двух токенов недостаточно, поэтому добавляется третий. Масса равна , а распределение становится
Поправка после усечения
Top-p оставил множество с базовой массой . Выбранный токен имел базовую вероятность .
-
Найдите его вероятность и логарифм вероятности после перенормировки.
-
На сколько новый логарифм вероятности больше базового?
-
Во сколько раз будет ошибочно отношение вероятностей последовательности из четырёх таких позиций, если каждый раз использовать базовый логарифм вместо фактического?
Показать ответ и решение
Вероятность выбранного токена:
Логарифм:
Он больше базового на
Для четырёх позиций ошибка в отношении последовательности равна
Направление зависит от того, в числителе или знаменателе использован неверный логарифм; модуль ошибки почти двукратный.
Нарушение поддержки
Поведенческая и целевая политики на трёх действиях равны
Можно ли оценить обычным отношением по данным из ? Какое действие создаёт проблему? Что можно изменить в сборе данных или целевой политике?
Показать ответ и решение
Нет. Для третьего действия
не определено, а данные из никогда не содержат это действие. Можно собирать данные политикой с полной поддержкой, увеличить исследование или ограничить целевую политику носителем поведения. Одной численной стабилизации отношения недостаточно: отсутствующая информация не появляется от клиппинга.
Устойчивый log_softmax
Реализуйте функцию:
import numpy as np
def stable_log_softmax(logits: np.ndarray, axis: int = -1) -> np.ndarray:
raise NotImplementedErrorОна должна корректно работать для логитов порядка , сохранять форму и нормировать вероятности вдоль заданной оси с точностью не хуже в float64. Вход не должен содержать NaN или бесконечности.
Открытые проверки:
logits = np.array([[1000.0, 1001.0, 999.0], [-1000.0, -999.0, -1001.0]])
result = stable_log_softmax(logits, axis=-1)
assert result.shape == logits.shape
np.testing.assert_allclose(np.exp(result).sum(axis=-1), np.ones(2), atol=1e-12, rtol=0.0)
np.testing.assert_allclose(result[0] - result[1], np.zeros(3), atol=1e-12)Показать ответ и решение
import numpy as np
def stable_log_softmax(logits: np.ndarray, axis: int = -1) -> np.ndarray:
z = np.asarray(logits, dtype=np.float64)
if z.ndim == 0:
raise ValueError("logits must have at least one dimension")
if not np.all(np.isfinite(z)):
raise ValueError("logits must be finite")
if isinstance(axis, bool) or not isinstance(axis, (int, np.integer)):
raise TypeError("axis must be an integer")
axis = int(axis)
if axis < 0:
axis += z.ndim
if not 0 <= axis < z.ndim:
raise ValueError("axis outside array dimensions")
maximum = np.max(z, axis=axis, keepdims=True)
shifted = z - maximum
log_normalizer = np.log(np.exp(shifted).sum(axis=axis, keepdims=True))
return shifted - log_normalizerВычитание максимума не меняет softmax, но не позволяет экспоненте переполниться. Логарифм нормировочной суммы вычисляется уже по неположительным сдвинутым логитам.
Причинный сдвиг и маска ответа
Реализуйте самодостаточную функцию, которая принимает model_logits формы [B,L,V], input_ids формы [B,L] и маску токенов ответа той же формы. Она должна вернуть:
-
выбранные логарифмы вероятностей для целей
input_ids[:, 1:]; -
сдвинутую маску
response_token_mask[:, 1:].
Сигнатура:
import numpy as np
def causal_response_logprobs(
model_logits: np.ndarray,
input_ids: np.ndarray,
response_token_mask: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
raise NotImplementedErrorОткрытые проверки:
logits = np.zeros((1, 3, 4))
input_ids = np.array([[0, 1, 2]])
response_mask = np.array([[0, 1, 1]])
selected, shifted = causal_response_logprobs(logits, input_ids, response_mask)
assert selected.shape == (1, 2) and shifted.shape == (1, 2)
assert np.allclose(selected, -np.log(4.0))
assert np.array_equal(shifted, np.array([[1, 1]], dtype=bool))Показать ответ и решение
import numpy as np
def _stable_log_softmax(logits: np.ndarray, axis: int = -1) -> np.ndarray:
z = np.asarray(logits, dtype=np.float64)
if not np.all(np.isfinite(z)):
raise ValueError("logits must be finite")
maximum = np.max(z, axis=axis, keepdims=True)
shifted = z - maximum
return shifted - np.log(np.exp(shifted).sum(axis=axis, keepdims=True))
def causal_response_logprobs(
model_logits: np.ndarray,
input_ids: np.ndarray,
response_token_mask: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
logits = np.asarray(model_logits, dtype=np.float64)
ids = np.asarray(input_ids, dtype=np.int64)
mask = np.asarray(response_token_mask, dtype=bool)
if logits.ndim != 3:
raise ValueError("model_logits must have shape [B, L, V]")
if ids.ndim != 2 or ids.shape != logits.shape[:2] or mask.shape != ids.shape:
raise ValueError("input_ids and response_token_mask must match logits[:2]")
if logits.shape[1] < 2:
raise ValueError("the sequence must contain at least two positions")
vocabulary_size = logits.shape[-1]
if np.any(ids < 0) or np.any(ids >= vocabulary_size):
raise ValueError("token id outside vocabulary")
shifted_logprobs = _stable_log_softmax(logits[:, :-1, :], axis=-1)
targets = ids[:, 1:]
selected = np.take_along_axis(shifted_logprobs, targets[..., None], axis=-1)[..., 0]
return selected, mask[:, 1:]Логиты в позиции предсказывают токен в позиции , поэтому и идентификаторы токенов, и маска ответа должны быть сдвинуты одинаково.
Метрики последовательности
По выбранным логарифмам вероятностей и маске вычислите для каждого ответа:
- число активных токенов;
- сумму логарифмов вероятностей;
- среднюю отрицательную логарифмическую вероятность;
- перплексию.
Пустая маска должна приводить к ValueError. Верните явно типизированный объект:
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class SequenceMetrics:
token_count: np.ndarray
sum_logprob: np.ndarray
mean_nll: np.ndarray
perplexity: np.ndarray
def masked_sequence_metrics(
selected_logprobs: np.ndarray,
mask: np.ndarray,
) -> SequenceMetrics:
raise NotImplementedErrorОткрытые проверки:
metrics = masked_sequence_metrics(np.array([[-0.1, -0.2, -1.0]]), np.array([[0, 1, 1]]))
assert metrics.token_count[0] == 2
assert abs(metrics.sum_logprob[0] - (-1.2)) < 1e-12
assert abs(metrics.mean_nll[0] - 0.6) < 1e-12
assert abs(metrics.perplexity[0] - np.exp(0.6)) < 1e-9Показать ответ и решение
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class SequenceMetrics:
token_count: np.ndarray
sum_logprob: np.ndarray
mean_nll: np.ndarray
perplexity: np.ndarray
def masked_sequence_metrics(
selected_logprobs: np.ndarray,
mask: np.ndarray,
) -> SequenceMetrics:
logprobs = np.asarray(selected_logprobs, dtype=np.float64)
active = np.asarray(mask, dtype=bool)
if logprobs.shape != active.shape:
raise ValueError("selected_logprobs and mask must have the same shape")
if logprobs.ndim == 1:
logprobs = logprobs[None, :]
active = active[None, :]
elif logprobs.ndim != 2:
raise ValueError("expected shape [B, T] or [T]")
if np.any(~np.isfinite(logprobs[active])):
raise ValueError("active log-probabilities must be finite")
token_count = active.sum(axis=1).astype(np.int64)
if np.any(token_count == 0):
raise ValueError("empty sequence mask")
sum_logprob = np.where(active, logprobs, 0.0).sum(axis=1)
mean_nll = -sum_logprob / token_count
return SequenceMetrics(
token_count=token_count,
sum_logprob=sum_logprob,
mean_nll=mean_nll,
perplexity=np.exp(mean_nll),
)Точный KL и оцениватели k1–k3
Реализуйте две самодостаточные функции:
import numpy as np
def exact_kl_from_logits(p_logits: np.ndarray, q_logits: np.ndarray) -> np.ndarray:
raise NotImplementedError
def kl_sample_values(log_q: np.ndarray, log_p: np.ndarray) -> dict[str, np.ndarray]:
raise NotImplementedErrorПервая возвращает по последней оси. Во второй положите и верните выборочные значения k1, k2 и k3.
Открытые проверки:
z = np.array([[1.0, -1.0], [0.5, 0.5]])
np.testing.assert_allclose(exact_kl_from_logits(z, z), np.zeros(2), atol=1e-14)
values = kl_sample_values(np.array([-0.4, -1.2]), np.array([-0.5, -0.7]))
assert set(values) == {"k1", "k2", "k3"}
assert np.all(values["k3"] >= -1e-14)Показать ответ и решение
import numpy as np
def _stable_log_softmax(logits: np.ndarray, axis: int = -1) -> np.ndarray:
z = np.asarray(logits, dtype=np.float64)
if not np.all(np.isfinite(z)):
raise ValueError("logits must be finite")
maximum = np.max(z, axis=axis, keepdims=True)
shifted = z - maximum
return shifted - np.log(np.exp(shifted).sum(axis=axis, keepdims=True))
def exact_kl_from_logits(p_logits: np.ndarray, q_logits: np.ndarray) -> np.ndarray:
p_logits = np.asarray(p_logits, dtype=np.float64)
q_logits = np.asarray(q_logits, dtype=np.float64)
if p_logits.shape != q_logits.shape:
raise ValueError("p_logits and q_logits must have the same shape")
log_p = _stable_log_softmax(p_logits, axis=-1)
log_q = _stable_log_softmax(q_logits, axis=-1)
p = np.exp(log_p)
return np.sum(p * (log_p - log_q), axis=-1)
def kl_sample_values(log_q: np.ndarray, log_p: np.ndarray) -> dict[str, np.ndarray]:
log_q = np.asarray(log_q, dtype=np.float64)
log_p = np.asarray(log_p, dtype=np.float64)
if log_q.shape != log_p.shape or not (np.all(np.isfinite(log_q)) and np.all(np.isfinite(log_p))):
raise ValueError("log_q and log_p must be finite arrays of the same shape")
log_ratio = log_p - log_q
ratio = np.exp(log_ratio)
return {
"k1": -log_ratio,
"k2": 0.5 * log_ratio**2,
"k3": ratio - 1.0 - log_ratio,
}Неотрицательность k3 следует из неравенства для ; небольшой отрицательный результат порядка машинной точности допустимо отсечь только при отображении, но не следует скрывать крупную численную ошибку.
Температура, top-k и top-p
Реализуйте три самостоятельных преобразования распределения. Температура должна быть положительной; после top-k и top-p распределение перенормируется. Используйте стабильную сортировку, чтобы при равных вероятностях меньший идентификатор токена оставался раньше.
import numpy as np
def temperature_distribution(logits: np.ndarray, temperature: float) -> np.ndarray:
raise NotImplementedError
def top_k_distribution(logits: np.ndarray, k: int) -> np.ndarray:
raise NotImplementedError
def top_p_distribution(
logits: np.ndarray,
top_p: float,
*,
min_tokens_to_keep: int = 1,
) -> np.ndarray:
raise NotImplementedErrorОткрытые проверки:
logits = np.array([2.0, 1.0, 0.0, -1.0])
warm = temperature_distribution(logits, 2.0)
assert abs(warm[0] - 0.455054) < 1e-6
assert abs(float(warm.sum()) - 1.0) < 1e-12
kept = top_k_distribution(logits, 2)
assert np.allclose(kept[2:], 0.0)
assert abs(kept[0] - 0.731059) < 1e-6
nucleus = top_p_distribution(logits, 0.8)
assert np.allclose(nucleus, kept)Показать ответ и решение
import numpy as np
def _softmax_1d(logits: np.ndarray, temperature: float = 1.0) -> np.ndarray:
z = np.asarray(logits, dtype=np.float64)
if z.ndim != 1 or z.size == 0 or not np.all(np.isfinite(z)):
raise ValueError("logits must be a non-empty finite one-dimensional array")
if not np.isfinite(temperature) or temperature <= 0.0:
raise ValueError("temperature must be a positive finite number")
scaled = z / temperature
shifted = scaled - np.max(scaled)
probabilities = np.exp(shifted)
return probabilities / probabilities.sum()
def _renormalize_support(probabilities: np.ndarray, keep: np.ndarray) -> np.ndarray:
out = np.zeros_like(probabilities, dtype=np.float64)
out[keep] = probabilities[keep]
mass = float(out.sum())
if mass <= 0.0:
raise ValueError("the retained support has zero probability mass")
return out / mass
def temperature_distribution(logits: np.ndarray, temperature: float) -> np.ndarray:
return _softmax_1d(logits, temperature)
def top_k_distribution(logits: np.ndarray, k: int) -> np.ndarray:
probabilities = _softmax_1d(logits)
if isinstance(k, bool) or not isinstance(k, (int, np.integer)) or not (1 <= int(k) <= probabilities.size):
raise ValueError("k must be an integer between 1 and the vocabulary size")
order = np.argsort(-probabilities, kind="stable")
return _renormalize_support(probabilities, order[: int(k)])
def top_p_distribution(
logits: np.ndarray,
top_p: float,
*,
min_tokens_to_keep: int = 1,
) -> np.ndarray:
probabilities = _softmax_1d(logits)
if not np.isfinite(top_p) or not (0.0 < top_p <= 1.0):
raise ValueError("top_p must lie in (0, 1]")
if (
isinstance(min_tokens_to_keep, bool)
or not isinstance(min_tokens_to_keep, (int, np.integer))
or not (1 <= int(min_tokens_to_keep) <= probabilities.size)
):
raise ValueError("min_tokens_to_keep must be a valid positive integer")
order = np.argsort(-probabilities, kind="stable")
cumulative = np.cumsum(probabilities[order])
threshold_count = int(np.searchsorted(cumulative, top_p, side="left")) + 1
keep_count = max(int(min_tokens_to_keep), threshold_count)
return _renormalize_support(probabilities, order[:keep_count])Если затем нужен логарифм усечённого распределения, нулевым вероятностям следует сопоставить явно. Эти три функции проверяют отдельные операции; в полном декодере их можно последовательно компоновать в документированном порядке.
Проверка пакета роллаутов
Реализуйте валидатор пакета со следующими полями:
response_ids, response_mask,
old_logprobs, ref_logprobs,
sequence_rewards,
terminated, truncated, stop_reasonПроверьте:
- совпадение форм токеновых полей;
- форму последовательностных полей
[B]; - непустую непрерывную маску каждого ответа;
- конечные логарифмы вероятностей на активных позициях;
NaNна позициях дополнения;- конечность наград;
- непустую строку
stop_reasonдля каждого ответа.
Не вводите универсальный запрет на одновременные terminated=True и truncated=True: такое совпадение может быть допустимо по контракту задачи. Валидатор проверяет структуру, а семантическое отображение причины остановки выполняется отдельно.
Сигнатура:
def validate_rollout_batch(batch: Mapping[str, Any]) -> RolloutBatchSummary:
...Открытая проверка:
batch = {
"response_ids": np.array([[5, 6], [7, 8]]),
"response_mask": np.array([[1, 1], [1, 0]]),
"old_logprobs": np.array([[-0.1, -0.2], [-0.3, np.nan]]),
"ref_logprobs": np.array([[-0.2, -0.3], [-0.4, np.nan]]),
"sequence_rewards": np.array([1.0, 0.0]),
"terminated": np.array([True, False]),
"truncated": np.array([False, True]),
"stop_reason": np.array(["eos", "length"], dtype=object),
}
summary = validate_rollout_batch(batch)
assert summary.batch_size == 2 and summary.active_tokens == 3
assert summary.terminated_sequences == 1 and summary.truncated_sequences == 1Показать ответ и решение
from dataclasses import dataclass
from typing import Any, Mapping
import numpy as np
@dataclass(frozen=True)
class RolloutBatchSummary:
batch_size: int
max_response_tokens: int
active_tokens: int
terminated_sequences: int
truncated_sequences: int
mean_response_length: float
def validate_rollout_batch(batch: Mapping[str, Any]) -> RolloutBatchSummary:
required = {
"response_ids", "response_mask", "old_logprobs", "ref_logprobs",
"sequence_rewards", "terminated", "truncated", "stop_reason",
}
missing = sorted(required - set(batch))
if missing:
raise ValueError("missing fields: " + ", ".join(missing))
ids = np.asarray(batch["response_ids"])
mask = np.asarray(batch["response_mask"], dtype=bool)
old = np.asarray(batch["old_logprobs"], dtype=np.float64)
ref = np.asarray(batch["ref_logprobs"], dtype=np.float64)
rewards = np.asarray(batch["sequence_rewards"], dtype=np.float64)
terminated = np.asarray(batch["terminated"], dtype=bool)
truncated = np.asarray(batch["truncated"], dtype=bool)
reasons = np.asarray(batch["stop_reason"], dtype=object)
if ids.ndim != 2 or not (ids.shape == mask.shape == old.shape == ref.shape):
raise ValueError("token-level arrays must align")
batch_size, max_tokens = ids.shape
if not (rewards.shape == terminated.shape == truncated.shape == reasons.shape == (batch_size,)):
raise ValueError("sequence-level arrays must have shape (batch,)")
lengths = mask.sum(axis=1).astype(int)
if np.any(lengths <= 0):
raise ValueError("every response must contain an active token")
expected = np.arange(max_tokens)[None, :] < lengths[:, None]
if not np.array_equal(mask, expected):
raise ValueError("response mask must be a contiguous right-padded prefix")
if np.any(~np.isfinite(old[mask])) or np.any(~np.isfinite(ref[mask])):
raise ValueError("active log-probabilities must be finite")
if np.any(np.isfinite(old[~mask])) or np.any(np.isfinite(ref[~mask])):
raise ValueError("padding log-probabilities must be NaN")
if np.any(~np.isfinite(rewards)):
raise ValueError("rewards must be finite")
if any(not isinstance(x, str) or not x.strip() for x in reasons.tolist()):
raise ValueError("every stop_reason must be a non-empty string")
return RolloutBatchSummary(
batch_size=int(batch_size),
max_response_tokens=int(max_tokens),
active_tokens=int(mask.sum()),
terminated_sequences=int(terminated.sum()),
truncated_sequences=int(truncated.sum()),
mean_response_length=float(lengths.mean()),
)Отсутствие запрета на совпадение флагов намеренно. Проверку того, соответствует ли конкретная комбинация контракту среды, следует выполнять отдельным семантическим валидатором, использующим stop_reason и правила задачи.