VKВлад КуликовОткрытые лекции
RL для LLM12 / 13

Модуль 12. Инфраструктура RL для LLM: память, роллауты и асинхронность

Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.

Скачать файлы курса (.zip) 382,1 КБ

Архив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.

Логические роли и процессы

В одном эксперименте один процесс последовательно генерирует ответы, вычисляет награду и выполняет шаг оптимизатора. В другом эксперименте эти операции распределены между четырьмя службами.

  1. Назовите четыре логические роли RL-контура.

  2. Обязаны ли они быть четырьмя отдельными процессами?

  3. Какой инвариант данных должен сохраняться при любом размещении?

  4. Почему строка ответа и одно число награды не образуют достаточный контракт роллаута?

Показать ответ и решение

Четыре основные роли:

  1. тренер вычисляет функцию потерь, градиенты и шаг оптимизатора;

  2. движок генерации порождает действия политики и обслуживает кэш ключей и значений (key–value cache, KV-кэш);

  3. проверяющая система или среда вычисляет исход и служебный статус;

  4. оркестратор выбирает запросы, формирует группы, управляет очередями, версиями и восстановлением.

Они не обязаны быть отдельными процессами. На одном графическом процессоре (graphics processing unit, GPU) один процесс может по очереди выполнять все роли; на кластере одна роль может быть распределена между десятками работников.

Инвариант: каждая обучающая запись однозначно связывает действия с фактической политикой поведения и награду — с версией проверяющей системы или среды.

Строка и одно число не сохраняют токенизацию, маску токенов политики, логарифмы вероятностей поведения, версию весов, параметры декодирования, причину остановки и происхождение награды. Без них невозможно надёжно восстановить функцию потерь и причину сбоя.

Две независимые оси

Система использует отдельный сервер vLLM, но тренер запрашивает один пакет, ждёт его целиком, обучается, синхронизирует веса и только затем запрашивает следующий.

  1. Является ли размещение совместным или раздельным?

  2. Является ли логический алгоритм синхронным или асинхронным?

  3. Почему фраза «отдельный сервер автоматически даёт асинхронность» неверна?

  4. Приведите пример совместного размещения с буферизацией и пример раздельного размещения со строгим барьером.

Показать ответ и решение
  1. Размещение раздельное: генерация работает на отдельном сервере.

  2. Логический режим синхронный: между пакетами стоит строгий барьер.

  3. Асинхронность определяется не адресом процесса, а порядком сбора и потребления данных. Отдельный сервер может обслуживать строго последовательный протокол.

  4. Совместное размещение с буферизацией: один GPU по очереди генерирует и обучается, пока центральный процессор (central processing unit, CPU) готовит следующий набор запросов. Раздельное размещение со строгим барьером: отдельный пул vLLM генерирует пакет, затем ждёт завершения обучения и загрузки новой версии.

Профиль памяти

Коллега утверждает: «Adam всегда требует 16 байт на параметр, поэтому другого расчёта не нужно».

  1. Объясните, какие объекты могут образовать эти 16 байт.

  2. Назовите не менее четырёх причин, по которым фактический профиль отличается.

  3. Почему деление логической памяти на число участников полностью шардированного параллелизма данных (Fully Sharded Data Parallel, FSDP) не даёт точный пик памяти?

  4. Что меняет низкоранговая адаптация (Low-Rank Adaptation, LoRA), а что она не устраняет?

Показать ответ и решение

Распространённый 16-байтовый профиль может состоять из:

  • 2 байт bfloat16-параметра;
  • 2 байт bfloat16-градиента;
  • 8 байт двух float32-моментов Adam;
  • 4 байт отдельной float32-мастер-копии.

Профиль меняется из-за точности градиентов, наличия мастер-копии, вида оптимизатора, квантования состояний, выгрузки на CPU, обучения только адаптеров, шардирования и особенностей реализации.

Деление на число участников полностью шардированного параллелизма данных (Fully Sharded Data Parallel, FSDP) не учитывает реплицируемые объекты, временную сборку модулей, коммуникационные буферы, активации, логиты, фрагментацию и момент синхронизации.

Низкоранговая адаптация (Low-Rank Adaptation, LoRA) уменьшает число параметров с градиентами и состояниями оптимизатора. Она не устраняет базовые веса, активации прямого прохода, KV-кэш, временные буферы и необходимость доставить согласованную версию адаптеров движку генерации.

KV-кэш и длина роллаутов

Размер группы в групповой относительной оптимизации политики (Group Relative Policy Optimization, GRPO) увеличили с 8 до 32 при той же максимальной длине ответа.

  1. Как изменится полезная память кэша ключей и значений (key–value cache, KV-кэш), если все последовательности активны одновременно?

  2. Почему реальный рост может не совпасть с идеальной линейной формулой?

  3. Какие два механизма движка уменьшают потери памяти или повторную работу, но не отменяют хранения ключей и значений?

  4. Почему кэш общего префикса нельзя без проверки переносить через обновление весов?

Показать ответ и решение
  1. Для групповой относительной оптимизации политики (Group Relative Policy Optimization, GRPO) в идеальной формуле память вырастет в четыре раза: число одновременно активных последовательностей увеличилось с 8 до 32.

  2. Реальный рост зависит от завершившихся последовательностей, распределения длин, страниц распределителя памяти, тензорного параллелизма, резервов и повторного использования префикса.

  3. PagedAttention уменьшает фрагментацию управляемого KV-кэша; повторное использование префиксов или RadixAttention переиспользует общую запросную часть.

  4. Ключи и значения зависят от параметров модели. После обновления весов старый кэш представляет скрытые состояния старой политики и не является кэшем новой версии.

Атомарная версия политики

Длинный ответ начал генерироваться на версии 120. После половины токенов движок загрузил версию 121. В журнал записали только policy_version=121 и итоговый текст.

  1. В чём ошибка контракта?

  2. Назовите два безопасных режима обновления.

  3. Какие данные понадобятся, если обновление в середине ответа всё же разрешено?

  4. Почему одинаковые веса при разных top_p или температуре всё равно задают разные политики поведения?

Показать ответ и решение

В журнале одна версия приписана ответу, который фактически породила смесь двух наборов весов. Поэтому знаменатель отношения вероятностей и воспроизведение политики поведения неоднозначны.

Безопасные режимы:

  • завершить уже начатые ответы версией 120 и переключать только новые запросы;
  • хранить версию и логарифмы вероятностей каждого действия или хода, если смена между ходами является частью протокола.

При смене внутри ответа нужны поточечные версии, фактические логарифмы вероятностей после всех преобразований декодирования, атомарный порядок обновления и маска действий.

Температура, top-p и маски логитов меняют распределение после весов. Поэтому одинаковый трансформер не означает одинаковую политику поведения.

Задержка и рассогласование

Сразу после синхронизации движок и тренер имеют одну версию весов, но их логарифмы вероятностей одного и того же ответа немного различаются. Через десять шагов тренера очередь содержит ответы версии на восемь шагов старше.

  1. Как называется первая проблема?

  2. Как называется вторая?

  3. Какой контрольный эксперимент разделяет эти причины?

  4. Почему эффективный размер выборки (effective sample size, ESS) является диагностикой, но не доказательством несмещённости обновления?

Показать ответ и решение

Первая проблема — рассогласование обучения и генерации (training–inference mismatch, TIM): версия одна, численные вероятности различаются.

Вторая — задержка политики (policy lag): данные породила более старая версия.

Контроль: сразу после синхронизации, при нулевой задержке, прогнать одни и те же токены через движок и тренер и сравнить логарифмы вероятностей токен за токеном. Затем отдельно изменить возраст версии.

Эффективный размер выборки (effective sample size, ESS) показывает концентрацию весов: равные веса дают полный размер выборки, доминирующий вес уменьшает его. Но высокий ESS не доказывает правильность знаменателя, отсутствие смещения из-за клиппирования, корректность масок или валидность выбранного оценивателя для данных другой политики.

Очередь и медленные хвосты

Генераторы производят 14 миллионов токенов в минуту, а тренер устойчиво потребляет 10 миллионов. Одновременно 99-й процентиль (p99) длительности роллаута в шесть раз больше медианы.

  1. Какое первое условие устойчивости очереди нарушено?

  2. Что такое обратное давление (backpressure) в этой системе?

  3. Почему ограничение только числом записей слабее ограничения токенами или оценочной стоимостью?

  4. Почему безусловное отбрасывание самых медленных эпизодов может сместить обучающее распределение?

Показать ответ и решение
  1. Нарушено условие λ<μ\lambda<\mu: скорость поступления 14 млн токенов/мин выше скорости потребления 10 млн.

  2. Обратное давление ограничивает генераторы, когда очередь достигает порога: уменьшает выдачу задач, приостанавливает работников или меняет приоритет.

  3. Две записи могут отличаться по длине в сотни раз. Число записей плохо описывает память, вычислительную стоимость и возраст данных; токены или оценочная стоимость ближе к физической нагрузке.

  4. Длинные эпизоды могут быть систематически сложнее. Их отбрасывание меняет распределение задач и оставляет тренеру преимущественно простые примеры.

Контрольная точка и выбор стека

Команда сохранила только model.safetensors, затем перезапустила обучение с нулевым состоянием оптимизатора и новой версией проверяющей системы. В отчёте это назвали продолжением того же запуска.

  1. Почему это неверно?

  2. Перечислите не менее восьми компонентов семантически полной контрольной точки RL-запуска.

  3. Как проверить восстановление до настоящей аварии?

  4. Почему TRL, verl, OpenRLHF, NeMo RL, PRIME-RL, SkyRL и AReaL нельзя ранжировать одной строкой «лучше–хуже» без контракта задачи?

Показать ответ и решение

Это новый запуск с инициализацией из старых весов, а не продолжение прежнего процесса оптимизации. Сброшены моменты Adam, планировщик и контракт награды.

Семантически полная контрольная точка обычно включает:

  • веса модели и адаптеров;
  • состояние оптимизатора;
  • состояние планировщика скорости обучения;
  • глобальный шаг и версию политики;
  • состояния генераторов случайных чисел;
  • положение в наборе данных;
  • состояние учебной программы или планировщика запросов;
  • параметры нормировки наград и преимуществ;
  • версии токенизатора и шаблона диалога;
  • версию проверяющей системы и среды;
  • конфигурацию декодирования;
  • схему роллаута;
  • распределённую раскладку и рецепт восстановления очереди.

Проверка восстановления: сохранить состояние, принудительно остановить процессы, восстановить систему, выполнить контрольный пакет и сравнить версии, токенизацию, конечность потерь и статистику с допустимым диапазоном.

Библиотеки и системы оптимизируют разные уровни: готовый тренер, оркестрацию ролей, полностью асинхронный поток, агентные среды или конкретную систему распределённого обучения. Сравнение без модели, масштаба, синхронности, типа среды, требований к отказоустойчивости и навыков команды не имеет однозначного смысла.

Полное обучение и адаптеры

Модель содержит 7⋅1097\cdot10^9 параметров. Базовые веса хранятся в bfloat16, градиенты — в bfloat16, два момента Adam — в float32, отдельная мастер-копия — в float32.

  1. Найдите число байт на обучаемый параметр.

  2. Найдите логическую память полного обучения в GB.

  3. Найдите память только базовых bfloat16-весов в GB.

  4. Для низкоранговой адаптации (LoRA) добавляются и обучаются 20⋅10620\cdot10^6 параметров адаптеров, но все базовые веса остаются в памяти. Найдите логическую память при том же профиле состояний адаптера.

  5. Почему ни одно из этих чисел ещё не является пиком памяти графического процессора?

Показать ответ и решение

На обучаемый параметр приходится

2+2+4+4+4=16 байт.2+2+4+4+4=16\ \text{байт}.

Полное обучение:

7⋅109⋅16=112⋅109 байт=112 GB.7\cdot10^9\cdot16 =112\cdot10^9\ \text{байт} =112\ \text{GB}.

Базовые bfloat16-веса:

7⋅109⋅2=14 GB.7\cdot10^9\cdot2=14\ \text{GB}.

Для LoRA базовые веса занимают 14 GB. На каждый из 20 млн обучаемых параметров учитываются сам вес адаптера, градиент, два момента и мастер-копия:

2+2+8+4=16 байт.2+2+8+4=16\ \text{байт}.

Дополнительная память:

20⋅106⋅16=0.32 GB.20\cdot10^6\cdot16=0.32\ \text{GB}.

Итого:

14+0.32=14.32 GB.14+0.32=14.32\ \text{GB}.

Пик дополнительно содержит активации, временные буферы, логиты, KV-кэш, фрагментацию, реплики и фазу передачи весов.

KV-кэш

Для учебной архитектуры заданы:

L=32,Hkv=8,dh=128,b=2 байта.L=32,\qquad H_{kv}=8,\qquad d_h=128,\qquad b=2\ \text{байта}.
  1. Сколько байт требует один сохранённый токен для ключей и значений?

  2. Сколько кибибайт (KiB) это составляет?

  3. Найдите память для 64 последовательностей по 4096 токенов.

  4. Выразите результат в гибибайтах (GiB).

  5. Каков идеализированный локальный объём при равномерном шардировании KV-кэша по четырём участникам тензорного параллелизма?

Показать ответ и решение

На один токен:

2LHkvdhb=2⋅32⋅8⋅128⋅2=131072 байта.2LH_{kv}d_hb =2\cdot32\cdot8\cdot128\cdot2 =131072\ \text{байта}.

Поскольку один кибибайт (KiB) равен 10241024 байтам,

131072/1024=128 KiB.131072/1024=128\ \text{KiB}.

Число токенов:

64⋅4096=262144.64\cdot4096=262144.

Память:

262144⋅131072=34359738368 байт=32 GiB.262144\cdot131072 =34359738368\ \text{байт} =32\ \text{GiB}.

Здесь GiB — гибибайт, равный 2302^{30} байтам. При идеальном равномерном шардировании по четырём участникам:

32/4=8 GiB на участника.32/4=8\ \text{GiB на участника}.

Это полезная нагрузка без страниц, таблиц блоков, выравнивания и временных тензоров.

Последовательный и перекрытый конвейер

Генерация и проверка одного пакета занимают G=90G=90 с, обучение и синхронизация — T=10T=10 с.

  1. Найдите длительность одного последовательного пакета.

  2. Найдите период идеального двухступенчатого конвейера после заполнения.

  3. Найдите полное время 20 пакетов в последовательном режиме.

  4. Найдите полное время 20 пакетов в идеальном конвейере.

  5. Найдите ускорение по полному времени и предельное ускорение по периоду.

  6. Почему ускорение не близко к двум?

Показать ответ и решение

Один последовательный пакет:

G+T=90+10=100 с.G+T=90+10=100\ \text{с}.

Период после заполнения:

max⁡(G,T)=90 с.\max(G,T)=90\ \text{с}.

Двадцать последовательных пакетов:

20⋅100=2000 с.20\cdot100=2000\ \text{с}.

Идеальный конвейер:

G+T+(20−1)max⁡(G,T)=100+19⋅90=1810 с.G+T+(20-1)\max(G,T) =100+19\cdot90 =1810\ \text{с}.

Ускорение по полному времени:

2000/1810≈1.105.2000/1810\approx1.105.

Предельное ускорение по периоду:

100/90≈1.111.100/90\approx1.111.

Этапы несбалансированы: 90 секунд генерации нельзя скрыть десятисекундным обучением. Почти весь период определяется генерацией.

Медленный роллаут

Восемь параллельных роллаутов заняли

[4,4,5,5,5,6,6,20] с.[4,4,5,5,5,6,6,20]\ \text{с}.
  1. Найдите суммарную работу в GPU-секундах.

  2. Найдите время синхронной группы.

  3. Найдите доступную ёмкость прямоугольника «8 исполнителей × время группы».

  4. Найдите утилизацию и долю простоя.

  5. Как изменится утилизация, если роллаут длительностью 20 с можно безопасно ограничить 8 секундами? Объясните, почему такая операция не всегда статистически нейтральна.

Показать ответ и решение

Суммарная работа:

4+4+5+5+5+6+6+20=55 GPU-с.4+4+5+5+5+6+6+20=55\ \text{GPU-с}.

Группа завершается через

max⁡iTi=20 с.\max_iT_i=20\ \text{с}.

Ёмкость:

8⋅20=160 GPU-с.8\cdot20=160\ \text{GPU-с}.

Утилизация:

U=55/160=0.34375.U=55/160=0.34375.

Простой:

1−U=0.65625.1-U=0.65625.

После ограничения последнего роллаута 8 секундами работа равна 43 GPU-с, время группы — 8 с, ёмкость — 64 GPU-с:

U′=43/64=0.671875.U'=43/64=0.671875.

Утилизация выросла, но если длинные ответы соответствуют трудным задачам, правило усечения изменяет распределение данных и наблюдаемую награду.

Отношения вероятностей и ESS

Для четырёх последовательностей даны логарифмы отношений вероятностей:

ℓ=(0,log⁡2,log⁡2,log⁡8).\ell=(0,\log 2,\log 2,\log 8).
  1. Найдите веса wi=exp⁡(ℓi)w_i=\exp(\ell_i).

  2. Найдите ESS.

  3. Найдите нормированный ESS.

  4. Сравните с ESS четырёх одинаковых весов.

  5. Как изменится ответ, если последний логарифм отношения клиппировать сверху значением log⁡2\log 2?

  6. Что эти числа говорят и чего не доказывают?

Показать ответ и решение

Веса:

w=(1,2,2,8).w=(1,2,2,8).

Сумма равна 13, сумма квадратов —

1+4+4+64=73.1+4+4+64=73.

Поэтому

ESS⁡=13273=16973≈2.315.\operatorname{ESS} =\frac{13^2}{73} =\frac{169}{73} \approx2.315.

Нормированный ESS:

2.315/4≈0.579.2.315/4\approx0.579.

Для четырёх одинаковых весов ESS равен 4, нормированный — 1.

После клиппирования последнего логарифма сверху log⁡2\log2 все веса равны (1,2,2,2)(1,2,2,2):

ESS⁡=721+4+4+4=4913≈3.769,\operatorname{ESS} =\frac{7^2}{1+4+4+4} =\frac{49}{13} \approx3.769,

нормированный ESS равен примерно 0.942.

Клиппирование уменьшило концентрацию, но изменило оцениватель. ESS описывает распределение весов, а не гарантирует корректную целевую функцию или хорошее качество политики.

Синхронизация и контрольные точки

Синхронизация весов занимает 8 секунд. В учебной модели цена единицы средней задержки равна одной секунде на обновление. Рассматриваются интервалы синхронизации K∈{1,2,4,8,16}K\in\{1,2,4,8,16\}, а приближённая цена одного обновления равна

C(K)=8K+K−12.C(K)=\frac{8}{K}+\frac{K-1}{2}.
  1. Найдите C(K)C(K) для всех кандидатов и выберите минимум.

  2. Каковы максимальная и стационарная средняя задержки при выбранном KK?

  3. Для 10 обновлений выпишите точную последовательность задержек и её среднее.

Отдельно: запись контрольной точки занимает C=120C=120 с, среднее время между отказами равно M=86400M=86400 с.

  1. Найдите приближённый оптимальный интервал
I∗=2CM.I^*=\sqrt{2CM}.
  1. Переведите его в минуты.

  2. Какие существенные расходы эта простая модель не учитывает?

Показать ответ и решение

Вычислим:

KK8/K8/K(K−1)/2(K-1)/2C(K)C(K)
18.00.08.0
24.00.54.5
42.01.53.5
81.03.54.5
160.57.58.0

Минимум среди кандидатов достигается при K=4K=4.

Стационарная последовательность задержек равна (0,1,2,3)(0,1,2,3), поэтому максимум — 3, среднее —

(0+1+2+3)/4=1.5.(0+1+2+3)/4=1.5.

Для 10 обновлений:

(0,1,2,3,0,1,2,3,0,1).(0,1,2,3,0,1,2,3,0,1).

Сумма равна 13, точное конечновыборочное среднее — 1.3.

Оптимальный интервал между контрольными точками:

I∗=2⋅120⋅86400≈4553.68 с.I^*=\sqrt{2\cdot120\cdot86400} \approx4553.68\ \text{с}.

В минутах:

4553.68/60≈75.89 мин.4553.68/60\approx75.89\ \text{мин}.

Модель не учитывает время перезапуска, асинхронную запись, коррелированные отказы, стоимость сети и хранилища, частично потерянные очереди, несовместимость распределённой раскладки и изменение риска во времени.

Память модели и KV-кэша

Реализуйте функции:

Python
def model_state_bytes(
    total_parameters: int,
    trainable_parameters: int,
    parameter_bytes: int,
    gradient_bytes: int,
    optimizer_state_bytes: int,
    master_parameter_bytes: int,
) -> int:
    ...


def kv_cache_bytes(
    num_sequences: int,
    tokens_per_sequence: int,
    num_layers: int,
    num_kv_heads: int,
    head_dim: int,
    element_bytes: int,
) -> int:
    ...

Проверьте положительность размеров, условие 0 <= trainable_parameters <= total_parameters и опубликованные числа B1–B2.

Открытые проверки:

Python
profile = model_state_bytes(
    7_000_000_000,
    trainable_parameters=7_000_000_000,
    parameter_bytes=2,
    gradient_bytes=2,
    optimizer_state_bytes=8,
    master_parameter_bytes=4,
)
assert profile == 112_000_000_000
lora = model_state_bytes(
    7_020_000_000,
    trainable_parameters=20_000_000,
    parameter_bytes=2,
    gradient_bytes=2,
    optimizer_state_bytes=8,
    master_parameter_bytes=4,
)
assert lora == 14_320_000_000
assert lora < profile
Показать ответ и решение
Python
def model_state_bytes(
    total_parameters,
    trainable_parameters,
    parameter_bytes,
    gradient_bytes,
    optimizer_state_bytes,
    master_parameter_bytes,
):
    integers = [
        total_parameters,
        trainable_parameters,
        parameter_bytes,
        gradient_bytes,
        optimizer_state_bytes,
        master_parameter_bytes,
    ]
    if any(isinstance(x, bool) or not isinstance(x, int) for x in integers):
        raise ValueError("all arguments must be integers")
    if total_parameters <= 0 or not 0 <= trainable_parameters <= total_parameters:
        raise ValueError("invalid parameter counts")
    if min(integers[2:]) < 0:
        raise ValueError("byte counts must be non-negative")

    base = total_parameters * parameter_bytes
    trainable_extra = trainable_parameters * (
        gradient_bytes + optimizer_state_bytes + master_parameter_bytes
    )
    return base + trainable_extra


def kv_cache_bytes(
    num_sequences,
    tokens_per_sequence,
    num_layers,
    num_kv_heads,
    head_dim,
    element_bytes,
):
    values = [
        num_sequences,
        tokens_per_sequence,
        num_layers,
        num_kv_heads,
        head_dim,
        element_bytes,
    ]
    if any(isinstance(x, bool) or not isinstance(x, int) or x <= 0 for x in values):
        raise ValueError("all dimensions must be positive integers")
    return (
        2
        * num_sequences
        * tokens_per_sequence
        * num_layers
        * num_kv_heads
        * head_dim
        * element_bytes
    )

Проверка:

Python
assert model_state_bytes(7_000_000_000, 7_000_000_000, 2, 2, 8, 4) == 112_000_000_000
assert model_state_bytes(7_020_000_000, 20_000_000, 2, 2, 8, 4) == 14_320_000_000
assert kv_cache_bytes(64, 4096, 32, 8, 128, 2) == 32 * 1024**3

Время конвейера

Реализуйте:

Python
def pipeline_report(rollout_seconds: float, train_seconds: float, batches: int) -> dict[str, float]:
    """Верните sequential_total, pipeline_total, period и speedup."""

Для G=90G=90, T=10T=10, n=20n=20 функция должна вернуть последовательное время 2000 с и конвейерное время 1810 с.

Открытые проверки:

Python
pipe_one = pipeline_report(90.0, 10.0, 1)
assert pipe_one["pipeline_total"] == 100.0
assert pipe_one["speedup"] == 1.0
Показать ответ и решение
Python
def pipeline_report(rollout_seconds, train_seconds, batches):
    if rollout_seconds <= 0 or train_seconds <= 0:
        raise ValueError("stage times must be positive")
    if isinstance(batches, bool) or not isinstance(batches, int) or batches <= 0:
        raise ValueError("batches must be a positive integer")

    sequential = batches * (rollout_seconds + train_seconds)
    period = max(rollout_seconds, train_seconds)
    pipelined = rollout_seconds + train_seconds + (batches - 1) * period
    return {
        "sequential_total": sequential,
        "pipeline_total": pipelined,
        "period": period,
        "speedup": sequential / pipelined,
    }
Python
report = pipeline_report(90.0, 10.0, 20)
assert report["sequential_total"] == 2000.0
assert report["pipeline_total"] == 1810.0

Веса важности и ESS

Реализуйте:

Python
def importance_weights(current_logprobs, behavior_logprobs, max_log_ratio=None):
    ...


def effective_sample_size(weights, normalized=False):
    ...

Требования:

  • входы одномерны и одинаковой длины;
  • логарифмы вероятностей конечны и не положительны;
  • веса неотрицательны;
  • нулевой суммарный квадрат весов вызывает ValueError;
  • необязательное клиппирование выполняется в логарифмическом пространстве.

Открытые проверки:

Python
new = np.array([-0.4, -0.6])
old = np.array([-0.5, -0.5])
w = importance_weights(new, old)
assert np.allclose(w, np.exp(new - old))
assert np.allclose(importance_weights(old, old), 1.0)
Показать ответ и решение
Python
import numpy as np


def importance_weights(current_logprobs, behavior_logprobs, max_log_ratio=None):
    current = np.asarray(current_logprobs, dtype=float)
    behavior = np.asarray(behavior_logprobs, dtype=float)
    if current.ndim != 1 or current.shape != behavior.shape or current.size == 0:
        raise ValueError("inputs must be non-empty 1-D arrays of equal shape")
    if not np.isfinite(current).all() or not np.isfinite(behavior).all():
        raise ValueError("log probabilities must be finite")
    if np.any(current > 0) or np.any(behavior > 0):
        raise ValueError("log probabilities must be non-positive")

    log_ratio = current - behavior
    if max_log_ratio is not None:
        if not np.isfinite(max_log_ratio):
            raise ValueError("max_log_ratio must be finite")
        log_ratio = np.minimum(log_ratio, max_log_ratio)
    return np.exp(log_ratio)


def effective_sample_size(weights, normalized=False):
    w = np.asarray(weights, dtype=float)
    if w.ndim != 1 or w.size == 0 or not np.isfinite(w).all():
        raise ValueError("weights must be a finite non-empty vector")
    if np.any(w < 0):
        raise ValueError("weights must be non-negative")
    denom = float(np.dot(w, w))
    if denom == 0:
        raise ValueError("at least one weight must be positive")
    ess = float(w.sum() ** 2 / denom)
    return ess / w.size if normalized else ess

Проверка контракта роллаута

Реализуйте функцию, которая возвращает список ошибок для записи с обязательными полями:

text
sample_id, prompt_id, policy_version,
prompt_tokens, response_tokens, behavior_logprobs,
decoding, reward, stop_reason, verifier_version

Проверьте типы, неотрицательность версии и идентификаторов токенов, конечность логарифмов вероятностей и их выравнивание с response_tokens.

Сигнатура:

Python
def rollout_contract_errors(record: Mapping[str, Any]) -> list[str]:
    ...

Связь с предыдущей практикой. Сопоставьте эту запись одного продолжения с многоходовой траекторией из задания C5 Модуля 11 (validate_rollout_record). Переиспользуйте правила проверки, но явно преобразуйте формат: одного добавления sample_id, verifier_version и decoding недостаточно. Опишите соответствие полей и сведения, которые нужно сохранить в исходном агентном журнале.

Открытые проверки:

Python
record = {
    "sample_id": "s1", "prompt_id": "q1", "policy_version": 1,
    "prompt_tokens": [1, 2], "response_tokens": [3, 4],
    "behavior_logprobs": [-0.1, -0.2], "decoding": {"temperature": 1.0},
    "reward": {"task": 1.0}, "stop_reason": "eos", "verifier_version": "v1",
}
assert rollout_contract_errors(record) == []
assert rollout_contract_errors({}) != []
assert any("missing fields" in e for e in rollout_contract_errors({}))
Показать ответ и решение
Python
import math

REQUIRED = {
    "sample_id", "prompt_id", "policy_version",
    "prompt_tokens", "response_tokens", "behavior_logprobs",
    "decoding", "reward", "stop_reason", "verifier_version",
}


def rollout_contract_errors(record):
    if not isinstance(record, dict):
        return ["record must be a dict"]

    errors = []
    missing = sorted(REQUIRED - set(record))
    if missing:
        errors.append("missing fields: " + ", ".join(missing))

    version = record.get("policy_version")
    if isinstance(version, bool) or not isinstance(version, int) or version < 0:
        errors.append("policy_version must be a non-negative integer")

    for key in ("prompt_tokens", "response_tokens"):
        tokens = record.get(key)
        if not isinstance(tokens, list) or any(
            isinstance(token, bool) or not isinstance(token, int) or token < 0
            for token in (tokens or [])
        ):
            errors.append(f"invalid {key}")

    logps = record.get("behavior_logprobs")
    if not isinstance(logps, list) or any(
        not isinstance(value, (int, float))
        or not math.isfinite(value)
        or value > 0
        for value in (logps or [])
    ):
        errors.append("invalid behavior_logprobs")
    elif isinstance(record.get("response_tokens"), list) and len(logps) != len(record["response_tokens"]):
        errors.append("logprobs do not align with response_tokens")

    return errors

Полная эталонная версия дополнительно проверяет строки, вложенные отображения и выдаёт более точные сообщения. В М11 token_ids, policy_mask и turn_ids описывают всю траекторию, а policy_versions — версии её ходов. Здесь выберите одно продолжение, созданное одной поведенческой версией: prompt_tokens — контекст перед его генерацией, response_tokens — выбранные политикой токены этого продолжения, behavior_logprobs выровнены именно с ними. Одиночная числовая policy_version требует явного реестра версий; строку версии нельзя молча приводить к числу. prompt_id задаётся при формировании запроса. Преобразование rewards в reward и выбор stop_reason требуют объявленного правила кредитования и остановки данного продолжения; итог эпизода не следует автоматически приписывать каждому ходу. Идентификаторы среды, действия, наблюдения, происхождение токенов и флаги всей траектории остаются в исходном журнале, связанном с инфраструктурной записью. Это проекция на другую единицу данных, а не замена полного агентного контракта.

Периодическая синхронизация и контрольные точки

Реализуйте:

Python
def synchronization_schedule(sync_every: int, updates: int) -> list[int]:
    """Верните задержки 0, 1, ..., sync_every-1 с периодическим сбросом."""


def optimal_checkpoint_interval(checkpoint_seconds: float, mtbf_seconds: float) -> float:
    ...

Проверьте B6 и граничные случаи sync_every=1, updates=1.

Открытые проверки:

Python
schedule = synchronization_schedule(4, 10)
assert schedule == [0, 1, 2, 3, 0, 1, 2, 3, 0, 1]
Показать ответ и решение
Python
from math import sqrt


def synchronization_schedule(sync_every, updates):
    if isinstance(sync_every, bool) or not isinstance(sync_every, int) or sync_every <= 0:
        raise ValueError("sync_every must be positive")
    if isinstance(updates, bool) or not isinstance(updates, int) or updates <= 0:
        raise ValueError("updates must be positive")
    return [step % sync_every for step in range(updates)]


def optimal_checkpoint_interval(checkpoint_seconds, mtbf_seconds):
    if checkpoint_seconds <= 0 or mtbf_seconds <= 0:
        raise ValueError("times must be positive")
    return sqrt(2 * checkpoint_seconds * mtbf_seconds)
Python
assert synchronization_schedule(1, 4) == [0, 0, 0, 0]
assert synchronization_schedule(4, 10) == [0, 1, 2, 3, 0, 1, 2, 3, 0, 1]
assert abs(optimal_checkpoint_interval(120, 86400) - 4553.679830642467) < 1e-9

Диагностика по измерениям

Реализуйте правило, которое получает:

Python
metrics = {
    "trainer_idle_fraction": ...,
    "rollout_idle_fraction": ...,
    "verifier_fraction": ...,
    "weight_sync_fraction": ...,
    "queue_growth_per_minute": ...,
    "normalized_ess": ...,
    "oom_events": ...,
}

и возвращает проверяемые гипотезы, а не окончательный диагноз. Например:

  • большой простой тренера при неполной очереди → проверить генерацию, верификацию и подачу запросов;
  • большая доля синхронизации → измерить передачу и преобразование весов;
  • низкий ESS → разделить задержку политики и рассогласование обучения с генерацией;
  • нехватка памяти графического процессора (out of memory, OOM) → построить память по фазам.

Добавьте проверки диапазонов и тест для нейтрального случая, где явного узкого места нет.

Открытые проверки:

Python
neutral = diagnose_pipeline({
    "trainer_idle_fraction": 0.05, "rollout_idle_fraction": 0.05,
    "verifier_fraction": 0.05, "weight_sync_fraction": 0.05,
    "queue_growth_per_minute": 0.0, "normalized_ess": 0.9, "oom_events": 0,
})
assert neutral == ["inspect a stage-level trace"]
starved = diagnose_pipeline({
    "trainer_idle_fraction": 0.6, "rollout_idle_fraction": 0.05,
    "verifier_fraction": 0.1, "weight_sync_fraction": 0.05,
    "queue_growth_per_minute": 0.0, "normalized_ess": 0.9, "oom_events": 0,
})
assert len(starved) == 1
Показать ответ и решение
Python
def diagnose_pipeline(metrics):
    required = {
        "trainer_idle_fraction",
        "rollout_idle_fraction",
        "verifier_fraction",
        "weight_sync_fraction",
        "queue_growth_per_minute",
        "normalized_ess",
        "oom_events",
    }
    missing = required - set(metrics)
    if missing:
        raise ValueError("missing metrics: " + ", ".join(sorted(missing)))

    bounded = required - {"queue_growth_per_minute", "oom_events"}
    for key in bounded:
        if not 0 <= metrics[key] <= 1:
            raise ValueError(f"{key} must lie in [0, 1]")
    if metrics["oom_events"] < 0:
        raise ValueError("oom_events must be non-negative")

    hypotheses = []
    if metrics["trainer_idle_fraction"] > 0.25 and metrics["queue_growth_per_minute"] <= 0:
        hypotheses.append("check rollout, verifier, and input supply")
    if metrics["verifier_fraction"] > 0.30:
        hypotheses.append("profile the verifier critical path")
    if metrics["weight_sync_fraction"] > 0.20:
        hypotheses.append("profile weight transfer and conversion")
    if metrics["normalized_ess"] < 0.50:
        hypotheses.append("separate policy lag from training-inference mismatch")
    if metrics["oom_events"] > 0:
        hypotheses.append("measure phase-specific peaks, KV cache, activations, and transfer buffers")
    if not hypotheses:
        hypotheses.append("inspect a stage-level trace")
    return hypotheses

Главный принцип решения: выводы сформулированы как гипотезы, которые можно проверить измерением. По одной агрегированной метрике нельзя надёжно определить причину.