Перейти к содержимому
ПереводыHugging Face

LLM Evaluation Guidebook, часть 3: отладка инференса и воспроизводимости

Перевод самой практичной главы LLM Evaluation Guidebook: что делать, если модель медленная или не помещается в память, почему разбор LaTeX ломает оценку на MATH и почему не удаётся воспроизвести чужие результаты бенчмарков.

Третья часть перевода LLM Evaluation Guidebook (лицензия CC BY-NC-SA 4.0; перевод распространяется на тех же условиях). Предыдущие части: 1 — инференс, токенизация и автоматические бенчмарки, 2 — LLM-as-a-judge и оценка людьми. Автор называет эту главу «самой плотно-практической частью руководства».

Если вам нужна абсолютная воспроизводимость (на конкретном железе и с конкретным оценочным промптом), вы, вероятно, используете батч размера 1. Но переход к большим батчам, скорее всего, ускорит оценку (если хватит памяти).

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

Не все библиотеки инференса работают с одинаковой скоростью, и одни реализации оптимизированы лучше других. Придётся немного поэкспериментировать, чтобы найти самые быстрые; если вы используете PyTorch, советую посмотреть чек-лист оптимизации инференса.

Если модель очень медленная, можно уменьшить её размер, снизив точность вычислений. Модель во float32 считает очень точно (32 бита на каждое число!), но это очень затратно по памяти и вычислениям; переход на bfloat16 или float16 (половинная точность) должен ускорить модель вдвое ценой потери точности, которая почти не должна иметь значения. Если нужно ещё быстрее, модель можно квантовать сильнее, до 8 или 4 бит (например, с gptq или bitsandbytes): n-битные матричные вычисления должны быть быстрее, а модель займёт ещё меньше памяти (впрочем, некоторые библиотеки квантования бывают медленными, так что проверяйте на своих задачах!).

Минимальную теоретическую память для загрузки модели (а значит, и нужное железо) можно оценить такой формулой:

<память (в ГБ)> = <число параметров (в млрд)> * <множитель точности>

В байте 8 бит, поэтому нужная память — это число параметров, умноженное на число байт на один параметр. Множитель точности — 4 для float32, 2 для float16 или bfloat16, 1 для 8 бит, 0,5 для 4 бит и т. д.

Вот и всё!

На практике я бы советовала <память (в ГБ)> = <число параметров (в млрд)> * (<множитель точности> * 110%), чтобы был запас: инференсу нужно чуть больше памяти, чем просто загрузка модели (нужно ещё загружать батчи).

Что делать, если модель не помещается на GPU?

Заголовок раздела «Что делать, если модель не помещается на GPU?»

Первое очевидное — играть с <множителем точности> выше: переход с float32 на 4 бита уменьшает требования к памяти в 8 раз! Но слишком низкая точность может ухудшить результаты, поэтому для некоторых моделей (особенно среднего размера) лучше остаться на float16 или 8 битах. (Квантование, похоже, меньше влияет на качество очень больших моделей — возможно, из-за избыточности информации.)

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

Два основных вида параллелизма модели:

  • Конвейерный параллелизм: модель режется по целым слоям, и слои распределяются по разным GPU. Поскольку выход слоя 1 — вход слоя 2, выполнение замедляется: GPU простаивают в ожидании — это называют «пузырём» (и данные нужно передавать с одной GPU на другую). Пузырь можно уменьшить, разбив вход на батчи поменьше. Конвейерный параллелизм встраивается в PyTorch через библиотеку PiPPy, и именно её accelerate использует «под капотом».
  • Тензорный параллелизм: модель режется на уровне матричных вычислений — матрицы делятся по строкам или столбцам, а итог объединяется. Это невероятно эффективно, пока все GPU в одном узле (чтобы избежать узких мест межузловой сети), но непросто в реализации. Хорошие реализации есть в библиотеке vllm. Ускорение даёт безумное.

Лучший документ о видах параллелизма (включая параллелизм по данным для ускорения) — здесь. (Подробно о них — в нашем переводе Ultra-Scale Playbook.)

Выгрузка на CPU (CPU offloading) переносит часть вычислений и частей модели на CPU, чтобы снизить потребление памяти GPU. Это гораздо медленнее любого другого метода здесь — в основном потому, что данные всё время приходится перемещать между устройствами.

Пример — ZeRO-Offload от DeepSpeed, который распределяет параметры между CPU и GPU (в дополнение к другим оптимизациям из статьи о ZeRO-2). На CPU передаются градиенты, состояния оптимизатора и вычисления с параметрами в fp32 во время оптимизации, а на GPU остаются параметры в fp16 и прямой и обратный проходы — так используется память CPU и вычисления GPU при минимальном обмене между ними.

Модель помещается на GPU, но я всё равно получаю OOM!

Заголовок раздела «Модель помещается на GPU, но я всё равно получаю OOM!»

Скорее всего, проблема в размере контекста.

Мы советуем:

  1. проверить, действительно ли модель помещается на GPU, загрузив фиктивные данные для инференса. У этих данных должен быть достаточно большой контекст (типичный для вашей задачи);
  2. уменьшить размер батча или отключить автоматический подбор размера батча, если он включён: он может случайно привести к ошибке OOM;
  3. в целом подавать примеры модели в порядке убывания длины контекста, чтобы модель упала сразу, если контекст слишком велик, а не через X часов работы.

Отладка разбора математики: LaTeX на бенчмарке MATH

Заголовок раздела «Отладка разбора математики: LaTeX на бенчмарке MATH»

Разбирать LaTeX трудно. Это становится проблемой, когда от модели ожидают ответ в LaTeX\LaTeX, — как в бенчмарке MATH.

Этот бенчмарк использует LaTeX\LaTeX для записи математических вычислений и символов. Оценка, казалось бы, сводится к тому, чтобы разобрать и сравнить эталон и вывод модели. Оказывается, правильного способа разбирать LaTeX\LaTeX нет (на рисунке оригинала — фрагмент документации sympy, где это прямо признаётся).

lm-evaluation-harness использует sympy (библиотеку Python для символьной математики), чтобы разбирать LaTeX и сравнивать выражения. Если с помощью sympy разобрать эталонные ответы и сравнить их с самими собой, точность получается всего около 0,94. Как так? Оказывается, sympy не может разобрать некоторые (правильные в LaTeX\LaTeX) выражения.

Например:

couldn't parse one of [0,1) or [0,1), I expected one of these: ']'
[0,1)
~~^
couldn't parse one of (-\iny,-5]\cup[5,\iny) or (-\iny,-5]\cup[5,\iny), I expected something else here
(-\iny,-5]\cup[5,\iny)
~~~~~~^
couldn't parse one of -\frac{1}{{}2x} or -\frac{1}{{}2x}, I don't understand this
-\frac{1}{{}2x}
~~~~~~~~~~~^

(Сообщения парсера оставлены как в оригинале: «не удалось разобрать одно из …, ожидался один из символов ’]’», «ожидалось что-то другое», «я этого не понимаю».)

Можно либо переписать грамматику LaTeX\LaTeX, добавив нужные возможности в код, либо добавить в свой код ручные проверки, улучшающие оценки модели. Едва не провалившись в глубокую кроличью нору, мы решили, что достаточно добавить в код проверки сравнением строк (в оригинале показан дифф исправления в LM Evaluation Harness).

Таблица сравнивает старые и новые результаты первых 25 моделей («Сравнение исходного и исправленного парсера на бенчмарке MATH»).

МодельОценка: исходныйОценка: исправленныйМесто: исходныйМесто: исправленный
rombodawg/Rombos-LLM-V2.5-Qwen-72b47,5850,6811
MaziyarPanahi/calme-2.2-qwen2-72b41,1643,4322
arcee-ai/Arcee-Nova40,4842,9033
fblgit/TheBeagle-v2beta-32B-MGS39,4342,5244
rombodawg/Rombos-LLM-V2.5-Qwen-32b39,1241,9955
dnhkng/RYS-XLarge38,9741,2466
dfurman/CalmeRys-78B-Orpo-v0.137,9240,7187
MaziyarPanahi/calme-2.2-rys-78b37,9239,9589
MaziyarPanahi/calme-2.4-rys-78b37,6940,4198
MaziyarPanahi/calme-2.3-rys-78b36,5638,971010
MaziyarPanahi/calme-2.1-rys-78b36,4038,901111
Qwen/Qwen2.5-72B36,1038,671212
MaziyarPanahi/calme-2.1-qwen2-72b36,0338,071315
Qwen/Qwen2-Math-72B-Instruct35,9538,141414
dfurman/Qwen2-72B-Orpo-v0.135,4238,141513
abacusai/Smaug-Qwen2-72B-Instruct35,3537,461619
anthracite-org/magnum-v1-72b35,2737,691816
alpindale/magnum-72b-v135,2737,691816
Qwen/Qwen2-72B-Instruct35,1237,691918
dnhkng/RYS-XLarge-base34,6737,162020
Undi95/MG-FinalMix-72B33,6136,102221
abacusai/Dracarys-72B-Instruct33,6135,652222
Qwen/Qwen2.5-32B32,8535,502323
anthracite-org/magnum-v2-72b31,6534,062424
dnhkng/RYS-Huge-bnb-4bit31,5733,842525

Допустим, вы прочитали техотчёт о новой классной модели и хотите воспроизвести её результаты на своей машине… но не получается? Разберёмся почему.

Чтобы воспроизвести оценки до десятых, сначала нужно убедиться, что вы используете ровно ту же кодовую базу, что и статья.

Обычно это значит либо использовать код оценки, предоставленный авторами, либо стандартную реализацию в эталонной библиотеке вроде lm_eval от EleutherAI или lighteval от Hugging Face. Если же исходный код оценки не опубликован, то, увы, точно воспроизвести результаты вам вряд ли удастся.

Чтобы понять, какие расхождения возникают при разных реализациях, почитайте этот пост (⭐), который мы написали вместе с командой оценки Hugging Face. В нём разобраны различия между тремя распространёнными реализациями оценки MMLU (в lm_eval, в helm и в исходной реализации авторов) и то, как они меняют оценки моделей.

Примечание: именно поэтому команда Hugging Face запустила Open LLM Leaderboard — чтобы получить единообразные сравнения оценок моделей и сопоставлять их с внутренними экспериментами.

Вот что, как мы заметили, легко испортить даже при одной и той же кодовой базе:

  • Разные случайные зёрна.
    • Обычно инференс зависит от случайного зерна меньше, чем обучение. Но зёрна всё же могут влиять на некоторые операции CUDA (см. страницу PyTorch о воспроизводимости) и менять предсказания, если стратегия генерации не жадная. Они могут влиять и на промпт при few-shot, и на некоторые функции пред- и постобработки. → Крошечное изменение может дать разницу в пару пунктов.
  • На деле разные метрики. Метрики могут на практике различаться, даже если называются одинаково. Примеры:
    • если исходная реализация — exact match по правдоподобию (вычисляются логарифмы вероятностей разных возможных ответов), а вы используете генеративный exact match (сравниваете только жадную генерацию с эталоном), оценки не совпадут;
    • мы также видели в кодовых базах оценки немало задач, объявленных как exact match, но на деле считавших prefix exact match (сравнивается только начало генерации с эталоном), suffix exact match (наоборот) или quasi exact match (точное совпадение после нормализации). → Поэтому нельзя полагаться только на название метрики — нужно смотреть код.
  • Разная нормализация.
    • Вернёмся к примеру с exact match: в lm_eval v1 многие задачи назывались просто генеративным exact match, и можно было подумать, что предсказание сравнивается с эталоном как есть. Но в коде предсказание сначала проходило нормализацию (удаление пунктуации, приведение чисел к единому виду и т. д.). Это, конечно, сильно меняет результаты. (В lm_eval v2 название нормализации теперь входит в название большинства метрик.) → Это одна из самых лёгких ошибок, особенно в задачах, требующих большой нормализации и постобработки ответа, вроде математических оценок (где ответ нужно извлечь из сгенерированного объяснения).

Здесь в игру вступают три основных фактора.

Формат промпта может — и будет — сильно менять оценки.

Например, для вопросов с выбором ответа распространены форматы, различающиеся только тем, как показаны варианты:

Question: <текст вопроса>
Choices:
Вариант 1Вариант 2Вариант 3
A. <Вариант A>(A) <Вариант A><Вариант A>
B. <Вариант B>(B) <Вариант B><Вариант B>
C. <Вариант C>(C) <Вариант C><Вариант C>
D. <Вариант D>(D) <Вариант D><Вариант D>
Answer:

а предсказывать нужно либо A/B/C/D, либо <Вариант A/B/C/D>.

Эти промпты семантически эквивалентны — в них ровно то же содержание, — но они всё равно могут давать разницу в несколько пунктов для одной и той же модели. Мы проводили эксперименты здесь (разница до 7 пунктов для одной модели), и в статье наблюдали похожее.

Некоторые задачи начинаются с промпта задачи (например, Следующие вопросы касаются <тема>) — его наличие или отсутствие тоже влияет на оценки.

Эта отличная статья ⭐ показывает и побочный эффект: многие модели теперь обучаются так, что переобучаются на промпты и форматы ответов бенчмарков в ущерб умению подстраиваться под другие промпты при оценке.

Мы наблюдали это на Open LLM Leaderboard 2 у моделей Llama 3.1. Они давали правильные ответы в нашей оценке MATH-Hard, но получали низкие оценки: не могли следовать шаблону из few-shot, потому что переобучились на формат промптов и ответов GSM8K (другого математического бенчмарка).

Чат-модели обычно прошли обучение или дообучение на инструкциях и предпочтениях. На этом этапе они научились следовать определённым шаблонам при инференсе. Например, шаблон может требовать начинать диалог с общего промпта (system prompt, системного промпта) с определённым префиксом (обычно System: ). Этот промпт даёт модели высокоуровневые указания — например, описание персонажа или общий стиль ответов. Реплики диалога могут также требовать ключевых слов-префиксов: User для запросов и Assistant для ответов.

При few-shot нужно ещё решить, подавать примеры многоходовым диалогом (имитируя реплики пользователя и ассистента) или все сразу (одним промптом пользователя).

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

С примерами few-shot легко ошибиться в двух вещах (если не уверены, что это, см. раздел об инференсе в части 1).

Очевидно, нужно использовать столько же примеров few-shot, сколько в эталонной задаче.

Но нужно использовать и ровно те же примеры, что и в модели, с которой вы сравниваетесь: другие примеры изменят результаты (что не очень удивительно, если считать, что одни примеры лучше выражают задачу, чем другие). Возможно, удивительнее другое: нужны не только те же примеры, но и тот же порядок. Меняя порядок тех же примеров, мы видели разницу до 3 пунктов на некоторых подмножествах MMLU (результаты — здесь, третья цветная сетка).

Здесь тоже важно следить за случайными зёрнами.

Для генеративных оценок следите, чтобы:

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

Источники различий, которые мы наблюдали:

  • другое железо: PyTorch не гарантирует воспроизводимость недетерминированных операций на разном железе;
  • другие библиотеки: например, при transformers и vllm в качестве бэкенда инференса матричные вычисления устроены не совсем одинаково;
  • другой размер батча: в нескольких библиотеках оценки и бэкендах моделей задокументировано, что разный размер батча меняет результаты инференса; для полностью воспроизводимых оценок фиксируйте размер батча, хотя из-за памяти это не всегда возможно;
  • другая точность загрузки весов: пониженная точность экономит память и стоимость инференса, но меняет числовые результаты, ведь вы используете другую версию весов.

Это не часть оригинала — короткий практикум от переводчика.

  1. Возьмите маленькую модель (Qwen2.5-0.5B-Instruct) и 100 вопросов из ARC. Оцените её дважды: по правдоподобию (сравнение логарифмов вероятностей вариантов) и генеративно (ответ буквой). Сравните результаты.
  2. Повторите генеративную оценку с шаблоном чата и без него, с 0 и 5 примерами few-shot, с двумя разными порядками примеров. Сведите разброс в таблицу — это ваша личная «отладка воспроизводимости».
  3. Загрузите ту же модель в bfloat16 и в 4 битах и проверьте, как меняются ответы и оценка.
ENRU
batch size, data parallelismразмер батча, параллелизм по данным
precision factorмножитель точности (байт на параметр)
quantizationквантование
model parallelism: pipeline, tensorпараллелизм модели: конвейерный, тензорный
CPU offloadingвыгрузка на CPU
OOM (out of memory)нехватка памяти
parser, normalizationпарсер (разбор), нормализация
exact / prefix / quasi exact matchточное совпадение / совпадение начала / совпадение после нормализации
reproducibilityвоспроизводимость
seed, temperature, end-of-sentence tokenзерно, температура, токен конца