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»Выгрузка на CPU (CPU offloading) переносит часть вычислений и частей модели на CPU, чтобы снизить потребление памяти GPU. Это гораздо медленнее любого другого метода здесь — в основном потому, что данные всё время приходится перемещать между устройствами.
Пример — ZeRO-Offload от DeepSpeed, который распределяет параметры между CPU и GPU (в дополнение к другим оптимизациям из статьи о ZeRO-2). На CPU передаются градиенты, состояния оптимизатора и вычисления с параметрами в fp32 во время оптимизации, а на GPU остаются параметры в fp16 и прямой и обратный проходы — так используется память CPU и вычисления GPU при минимальном обмене между ними.
Модель помещается на GPU, но я всё равно получаю OOM!
Заголовок раздела «Модель помещается на GPU, но я всё равно получаю OOM!»Скорее всего, проблема в размере контекста.
Мы советуем:
- проверить, действительно ли модель помещается на GPU, загрузив фиктивные данные для инференса. У этих данных должен быть достаточно большой контекст (типичный для вашей задачи);
- уменьшить размер батча или отключить автоматический подбор размера батча, если он включён: он может случайно привести к ошибке OOM;
- в целом подавать примеры модели в порядке убывания длины контекста, чтобы модель упала сразу, если контекст слишком велик, а не через X часов работы.
Отладка разбора математики: LaTeX на бенчмарке MATH
Заголовок раздела «Отладка разбора математики: LaTeX на бенчмарке MATH»Разбирать LaTeX трудно. Это становится проблемой, когда от модели ожидают ответ в , — как в бенчмарке MATH.
Этот бенчмарк использует для записи математических вычислений и символов. Оценка, казалось бы, сводится к тому,
чтобы разобрать и сравнить эталон и вывод модели. Оказывается, правильного способа разбирать нет (на рисунке
оригинала — фрагмент документации sympy, где это прямо признаётся).
lm-evaluation-harness использует sympy (библиотеку Python для символьной математики),
чтобы разбирать LaTeX и сравнивать выражения. Если с помощью sympy разобрать эталонные ответы и сравнить их с самими
собой, точность получается всего около 0,94. Как так? Оказывается, sympy не может разобрать некоторые (правильные
в ) выражения.
Например:
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}~~~~~~~~~~~^(Сообщения парсера оставлены как в оригинале: «не удалось разобрать одно из …, ожидался один из символов ’]’», «ожидалось что-то другое», «я этого не понимаю».)
Как это обойти?
Заголовок раздела «Как это обойти?»Можно либо переписать грамматику , добавив нужные возможности в код, либо добавить в свой код ручные проверки, улучшающие оценки модели. Едва не провалившись в глубокую кроличью нору, мы решили, что достаточно добавить в код проверки сравнением строк (в оригинале показан дифф исправления в LM Evaluation Harness).
Результаты
Заголовок раздела «Результаты»Таблица сравнивает старые и новые результаты первых 25 моделей («Сравнение исходного и исправленного парсера на бенчмарке MATH»).
| Модель | Оценка: исходный | Оценка: исправленный | Место: исходный | Место: исправленный |
|---|---|---|---|---|
| rombodawg/Rombos-LLM-V2.5-Qwen-72b | 47,58 | 50,68 | 1 | 1 |
| MaziyarPanahi/calme-2.2-qwen2-72b | 41,16 | 43,43 | 2 | 2 |
| arcee-ai/Arcee-Nova | 40,48 | 42,90 | 3 | 3 |
| fblgit/TheBeagle-v2beta-32B-MGS | 39,43 | 42,52 | 4 | 4 |
| rombodawg/Rombos-LLM-V2.5-Qwen-32b | 39,12 | 41,99 | 5 | 5 |
| dnhkng/RYS-XLarge | 38,97 | 41,24 | 6 | 6 |
| dfurman/CalmeRys-78B-Orpo-v0.1 | 37,92 | 40,71 | 8 | 7 |
| MaziyarPanahi/calme-2.2-rys-78b | 37,92 | 39,95 | 8 | 9 |
| MaziyarPanahi/calme-2.4-rys-78b | 37,69 | 40,41 | 9 | 8 |
| MaziyarPanahi/calme-2.3-rys-78b | 36,56 | 38,97 | 10 | 10 |
| MaziyarPanahi/calme-2.1-rys-78b | 36,40 | 38,90 | 11 | 11 |
| Qwen/Qwen2.5-72B | 36,10 | 38,67 | 12 | 12 |
| MaziyarPanahi/calme-2.1-qwen2-72b | 36,03 | 38,07 | 13 | 15 |
| Qwen/Qwen2-Math-72B-Instruct | 35,95 | 38,14 | 14 | 14 |
| dfurman/Qwen2-72B-Orpo-v0.1 | 35,42 | 38,14 | 15 | 13 |
| abacusai/Smaug-Qwen2-72B-Instruct | 35,35 | 37,46 | 16 | 19 |
| anthracite-org/magnum-v1-72b | 35,27 | 37,69 | 18 | 16 |
| alpindale/magnum-72b-v1 | 35,27 | 37,69 | 18 | 16 |
| Qwen/Qwen2-72B-Instruct | 35,12 | 37,69 | 19 | 18 |
| dnhkng/RYS-XLarge-base | 34,67 | 37,16 | 20 | 20 |
| Undi95/MG-FinalMix-72B | 33,61 | 36,10 | 22 | 21 |
| abacusai/Dracarys-72B-Instruct | 33,61 | 35,65 | 22 | 22 |
| Qwen/Qwen2.5-32B | 32,85 | 35,50 | 23 | 23 |
| anthracite-org/magnum-v2-72b | 31,65 | 34,06 | 24 | 24 |
| dnhkng/RYS-Huge-bnb-4bit | 31,57 | 33,84 | 25 | 25 |
Отладка воспроизводимости
Заголовок раздела «Отладка воспроизводимости»Допустим, вы прочитали техотчёт о новой классной модели и хотите воспроизвести её результаты на своей машине… но не получается? Разберёмся почему.
Другая кодовая база
Заголовок раздела «Другая кодовая база»Чтобы воспроизвести оценки до десятых, сначала нужно убедиться, что вы используете ровно ту же кодовую базу, что и статья.
Обычно это значит либо использовать код оценки, предоставленный авторами, либо стандартную реализацию в эталонной
библиотеке вроде 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_evalv1 многие задачи назывались просто генеративнымexact match, и можно было подумать, что предсказание сравнивается с эталоном как есть. Но в коде предсказание сначала проходило нормализацию (удаление пунктуации, приведение чисел к единому виду и т. д.). Это, конечно, сильно меняет результаты. (Вlm_evalv2 название нормализации теперь входит в название большинства метрик.) → Это одна из самых лёгких ошибок, особенно в задачах, требующих большой нормализации и постобработки ответа, вроде математических оценок (где ответ нужно извлечь из сгенерированного объяснения).
- Вернёмся к примеру с
Другой промпт
Заголовок раздела «Другой промпт»Здесь в игру вступают три основных фактора.
Сам промпт
Заголовок раздела «Сам промпт»Формат промпта может — и будет — сильно менять оценки.
Например, для вопросов с выбором ответа распространены форматы, различающиеся только тем, как показаны варианты:
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
Заголовок раздела «Примеры few-shot»С примерами few-shot легко ошибиться в двух вещах (если не уверены, что это, см. раздел об инференсе в части 1).
Очевидно, нужно использовать столько же примеров few-shot, сколько в эталонной задаче.
Но нужно использовать и ровно те же примеры, что и в модели, с которой вы сравниваетесь: другие примеры изменят результаты (что не очень удивительно, если считать, что одни примеры лучше выражают задачу, чем другие). Возможно, удивительнее другое: нужны не только те же примеры, но и тот же порядок. Меняя порядок тех же примеров, мы видели разницу до 3 пунктов на некоторых подмножествах MMLU (результаты — здесь, третья цветная сетка).
Здесь тоже важно следить за случайными зёрнами.
Другие параметры генерации
Заголовок раздела «Другие параметры генерации»Для генеративных оценок следите, чтобы:
- использовался тот же токен конца предложения;
- модели разрешалось генерировать то же число токенов;
- при сэмплировании использовались те же зерно и температура.
Другая загрузка модели
Заголовок раздела «Другая загрузка модели»Источники различий, которые мы наблюдали:
- другое железо: PyTorch не гарантирует воспроизводимость недетерминированных операций на разном железе;
- другие библиотеки: например, при
transformersиvllmв качестве бэкенда инференса матричные вычисления устроены не совсем одинаково; - другой размер батча: в нескольких библиотеках оценки и бэкендах моделей задокументировано, что разный размер батча меняет результаты инференса; для полностью воспроизводимых оценок фиксируйте размер батча, хотя из-за памяти это не всегда возможно;
- другая точность загрузки весов: пониженная точность экономит память и стоимость инференса, но меняет числовые результаты, ведь вы используете другую версию весов.
Что сделать руками
Заголовок раздела «Что сделать руками»Это не часть оригинала — короткий практикум от переводчика.
- Возьмите маленькую модель (Qwen2.5-0.5B-Instruct) и 100 вопросов из ARC. Оцените её дважды: по правдоподобию (сравнение логарифмов вероятностей вариантов) и генеративно (ответ буквой). Сравните результаты.
- Повторите генеративную оценку с шаблоном чата и без него, с 0 и 5 примерами few-shot, с двумя разными порядками примеров. Сведите разброс в таблицу — это ваша личная «отладка воспроизводимости».
- Загрузите ту же модель в
bfloat16и в 4 битах и проверьте, как меняются ответы и оценка.
Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| 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 | зерно, температура, токен конца |