Большие языковые модели (LLM) произвели настоящую революцию в мире ML. Все больше компаний стремятся так или иначе извлечь из них пользу. Например, в Selectel мы оцениваем рациональность развертывания частной LLM для помощи сотрудникам техподдержки в поиске ответов на вопросы клиентов. Эту задачу мы решили совместить с тестом нового железа — видеокарты Ada с 48 ГБ RAM. В соперники ей выбрали А100 на 40 ГБ.
Сразу оговоримся, что нормально обучить LLM на одной GPU практически невозможно, но в качестве теста производительности такая задача вполне подходит. Под катом рассказываем, как мы проводили тест-драйв двух GPU и к каким выводам пришли.
Используйте навигацию, если не хотите читать текст полностью:
→ Почему нельзя просто взять самые мощные GPU
→ За что мы любим А100 и А6000 Ada
→ Тест — два GPU решают одни и те же задачи в одинаковых условиях
→ Результаты теста
→ Заключение
Почему нельзя просто взять самые мощные GPU
Опираясь только на заявленные производителем характеристики видеокарт, легко попасть в ловушку, посчитав, что чем больше у карты памяти и CUDA-ядер, а также чем шире шина, тем лучше. С одной стороны, очевидно, что более сложные задачи требуют больше памяти. Кроме RAM GPU на скорость и сложность расчетов влияют еще и ядра. С другой стороны, покупать или арендовать карту с топовыми характеристиками на все случаи жизни — не лучшая идея. И вот почему…
Это дорого
Для обучения больших моделей и работы с массивными датасетами нужны действительно дорогие видеокарты. Цена выбранной конфигурации часто заставляет задуматься: чуть меньше памяти и ядер замедлят работу, но будет ли это замедление настолько критичным, чтобы переплачивать миллионы каждый месяц? К тому же, несколько лишних гигабайт могут так и не пригодиться.
Ваши ресурсы будут простаивать
Вы купили одну или несколько видеокарт или арендовали готовый сервер с GPU. Конечно, вы тут же загрузили его на 100% обучением ML-моделей. Но GPU вряд ли будет работать в режиме 24/7/365, даже если решение сложных задач поставить на поток.
Скорее всего, картина будет такой. Дата-сайентист забрал себе весь ресурс, но использовал GPU, скажем, четыре часа в сутки. В остальное время он обучал модель на CPU, пил кофе или решал другие задачи. Чтобы оплаченные ресурсы в это время не пропадали, их стоит отдать другим специалистам. И тут мы плавно подходим к следующему пункту.
Придется учитывать нюансы шеринга GPU
Шеринг GPU — отличный ход, когда нужно одновременно решать несколько задач, ни одна из которых не требует всех доступных ресурсов. Берем карту, делим ее на несколько изолированных кусочков, в которых есть своя память, ядра, кэш и прочее, и отдаем по одному каждому из дата-сайентистов под его задачу. Но, конечно, не все так просто.
Во-первых, каждый из способов шеринга имеет свои плюсы и минусы. Во-вторых, одна и та же технология, например MIG, может поделить А100 максимум на семь логических блоков, а А30 — максимум на четыре.
Резюмируем: если вы не готовы к радикальным мерам, например к эксплуатации нескольких десятков А100 или H100, подключенных к материнской плате через NVSwitch, вопрос выбора железа для ML-задач станет весьма непростым. По крайней мере, одних только технических характеристик железа для принятия решения мало, так как важно обращать внимание на дополнительные возможности GPU в плане совместного использования и мониторинга эксплуатации. А идти эмпирическим путем, перебирая все возможные варианты, долго и дорого.
За что мы любим А100 и А6000 Ada
До недавнего времени мы бы не задумываясь выбрали уже проверенную А100. У этой карты GPU с 6 912 CUDA-ядрами, 40 ГБ видеопамяти, поддержкой Tensor Cores и технологий виртуализации. Отличное решение для облачных вычислений, рендеринга, обучения нейронных сетей и моделирования. Достаточно следить за подсказками в PyTorch, ориентироваться на свободный объем памяти и наслаждаться. Кроме того, две А100 можно объединить через NVLink, чтобы создать настоящего «серверного монстра».
Однако недавно мы получили доступ к новой видеокарте — А6000 Ada. При беглом взгляде на ее характеристики можно предположить, что это А100 на стероидах: в миниатюрном корпусе спрятаны 10 752 CUDA-ядер и 48 ГБ видеопамяти. При этом тепловыделение А6000 Ada составляет 300 Вт, тогда как у А100 — 400. А одно из ключевых различий карт в том, что A6000 Ada не поддерживает подключение через NVLink и шеринг через MIG — то есть это устройства разного уровня функциональности.
Мы видим, что по характеристикам А6000 Ada выглядит перспективнее, чем А100. Но она дороже, следовательно, возникает закономерный вопрос: стоит ли переплачивать? Чтобы найти ответ, мы взяли LLM в трех размерах и устроили тест-драйв нашим видеокартам.
Тест — два GPU решают одни и те же задачи в одинаковых условиях
Видеокарты мы тестируем в дообучении (файнтюнинге) большой языковой модели в разных размерах:
В идеале для обучения LLM на каждый миллиард ее параметров нужно около 24 ГБ видеопамяти — в RAM помещается не только модель, но и другие компоненты: градиенты, состояния оптимизатора, временные буферы и так далее. То есть используя только одну видеокарту, как в нашем эксперименте, мы не сможем завершить обучение очень многих LLM. Выходы есть разные, например объединить две карты через NVLink (или большее количество карт через NVSwitch), применить квантование или другой способ уменьшения размера модели. И так как нас интересует честный тест каждой карты, объединять их и увеличивать вычислительные ресурсы мы не будем, а пойдем как раз по пути квантования (и столкнемся с нехваткой памяти, но об этом позже).
Итак, модели загружаются в квантованном виде с помощью библиотеки BitSandBytes. Чтобы обучение в принципе было возможно, применяем подход LoRA, имплементированный в библиотеке peft. Конфигурация LoRA остается неизменной (ранг множителей = 16) за исключением запуска обучения самой большой модели на А100 — мы снизили ранг разложенных матриц, чтобы сэкономить память и сделать возможной хотя бы загрузку в память GPU модели и батча.
Подготовка рабочего окружения
Подготовим рабочее окружение для проведения тестов. Нам нужно установить довольно много пакетов, но это делается буквально двумя скриптами.
Сначала запускаем скрипт apt.sh. С его помощью:
- отключаем обновления ядер для Nvidia- и Linux-пакетов;
- обновляем список пакетов;
- устанавливаем новые пакеты (для использования репозитория по https, проверки достоверности соединений SSL, передачи данных с сервера и на сервер при помощи различных протоколов, работы с репозиториями и генерации паролей).
#!/bin/bash
set -e
set -o xtrace
# Отключаем автообновления ядра
cat < /etc/apt/apt.conf.d/51unattended-upgrades
Unattended-Upgrade::Package-Blacklist {
"nvidia-";
"linux-";
};
EOF
apt update
apt install -y \
apt-transport-https \
ca-certificates \
curl \
software-properties-common \
pwgen
Скрипт apt.sh.
Далее запускаем скрипт nvidia-drivers-install.sh. С его помощью мы:
- устанавливаем пакеты linux-header (они нужны для сборки DKMS, в виде которого поставляется драйвер out-off-tree; этот драйвер в свою очередь нужен для работы GPU Nvidia);
- устанавливаем драйверы Nvidia;
- устанавливаем nvitop, jupyter и другие инструменты.
#!/bin/bash
set -e
set -o xtrace
# Опционально, возможно понадобится удаление устаревшего пакета
# linux-version list
# uname -r
# dpkg --list | grep -E -i --color 'linux-image|linux-headers'
# apt-get --purge autoremove
# apt --purge autoremove
# uname -a
# apt purge linux-image-5.4.0-166-generic
# dpkg --list | grep -E -i --color 'linux-image|linux-headers'
# Install Nvidia drivers for all kernels
for kernel in $(linux-version list); do
apt install -y "linux-headers-${kernel}"
done
apt install -y nvidia-driver-510 htop python3-pip git
pip install nvitop==1.3.1 jupyter==1.0.0 accelerate==0.21.0 peft==0.4.0 bitsandbytes==0.40.2 transformers==4.31.0 trl==0.4.7 scipy==1.11.3 tensorboard==2.15.1 evaluate==0.4.1 scikit-learn==1.3.2
Скрипт nvidia-drivers-install.sh.
В итоге получается вот такой env…
absl-py==2.0.0
accelerate==0.21.0
aiohttp==3.8.6
aiosignal==1.3.1
anyio==4.0.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==2.4.1
async-lru==2.0.4
async-timeout==4.0.3
attrs==23.1.0
Babel==2.13.1
beautifulsoup4==4.12.2
bitsandbytes==0.40.2
bleach==6.1.0
blinker==1.4
cachetools==5.3.2
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.2
comm==0.2.0
command-not-found==0.3
contourpy==1.2.0
cryptography==3.4.8
cycler==0.12.1
datasets==2.14.7
dbus-python==1.2.18
debugpy==1.8.0
decorator==5.1.1
defusedxml==0.7.1
dill==0.3.7
distro==1.7.0
distro-info==1.1+ubuntu0.1
evaluate==0.4.1
exceptiongroup==1.1.3
executing==2.0.1
fastjsonschema==2.19.0
filelock==3.13.1
fonttools==4.45.0
fqdn==1.5.1
frozenlist==1.4.0
fsspec==2023.10.0
google-auth==2.23.4
google-auth-oauthlib==1.1.0
grpcio==1.59.2
httplib2==0.20.2
huggingface-hub==0.19.2
idna==3.4
importlib-metadata==4.6.4
ipykernel==6.26.0
ipython==8.17.2
ipywidgets==8.1.1
isoduration==20.11.0
jedi==0.19.1
jeepney==0.7.1
Jinja2==3.1.2
joblib==1.3.2
json5==0.9.14
jsonpointer==2.4
jsonschema==4.19.2
jsonschema-specifications==2023.11.1
jupyter==1.0.0
jupyter-console==6.6.3
jupyter-events==0.9.0
jupyter-lsp==2.2.0
jupyter_client==8.6.0
jupyter_core==5.5.0
jupyter_server==2.10.0
jupyter_server_terminals==0.4.4
jupyterlab==4.0.8
jupyterlab-pygments==0.2.2
jupyterlab-widgets==3.0.9
jupyterlab_server==2.25.1
keyring==23.5.0
kiwisolver==1.4.5
language-selector==0.1
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
Markdown==3.5.1
MarkupSafe==2.1.3
matplotlib==3.8.2
matplotlib-inline==0.1.6
mistune==3.0.2
more-itertools==8.10.0
mpmath==1.3.0
multidict==6.0.4
multiprocess==0.70.15
nbclient==0.9.0
nbconvert==7.11.0
nbformat==5.9.2
nest-asyncio==1.5.8
netifaces==0.11.0
networkx==3.2.1
notebook==7.0.6
notebook_shim==0.2.3
numpy==1.26.2
nvidia-cublas-cu12==12.1.3.1
nvidia-cuda-cupti-cu12==12.1.105
nvidia-cuda-nvrtc-cu12==12.1.105
nvidia-cuda-runtime-cu12==12.1.105
nvidia-cudnn-cu12==8.9.2.26
nvidia-cufft-cu12==11.0.2.54
nvidia-curand-cu12==10.3.2.106
nvidia-cusolver-cu12==11.4.5.107
nvidia-cusparse-cu12==12.1.0.106
nvidia-ml-py==12.535.133
nvidia-nccl-cu12==2.18.1
nvidia-nvjitlink-cu12==12.3.52
nvidia-nvtx-cu12==12.1.105
nvitop==1.3.1
oauthlib==3.2.0
overrides==7.4.0
packaging==23.2
pandas==2.1.3
pandocfilters==1.5.0
parso==0.8.3
peft==0.4.0
pexpect==4.8.0
Pillow==10.1.0
platformdirs==4.0.0
prometheus-client==0.18.0
prompt-toolkit==3.0.41
protobuf==4.23.4
psutil==5.9.6
ptyprocess==0.7.0
pure-eval==0.2.2
pyarrow==14.0.1
pyarrow-hotfix==0.5
pyasn1==0.5.0
pyasn1-modules==0.3.0
pycparser==2.21
Pygments==2.16.1
PyGObject==3.42.1
PyJWT==2.3.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyparsing==2.4.7
python-apt==2.4.0+ubuntu2
python-dateutil==2.8.2
python-json-logger==2.0.7
pytz==2023.3.post1
PyYAML==5.4.1
pyzmq==25.1.1
qtconsole==5.5.0
QtPy==2.4.1
referencing==0.31.0
regex==2023.10.3
requests==2.31.0
requests-oauthlib==1.3.1
responses==0.18.0
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.12.0
rsa==4.9
safetensors==0.4.0
scikit-learn==1.3.2
scipy==1.11.3
screen-resolution-extra==0.0.0
SecretStorage==3.3.1
Send2Trash==1.8.2
six==1.16.0
sklearn==0.0.post11
sniffio==1.3.0
soupsieve==2.5
ssh-import-id==5.11
stack-data==0.6.3
sympy==1.12
tensorboard==2.15.1
tensorboard-data-server==0.7.2
termcolor==2.3.0
terminado==0.18.0
threadpoolctl==3.2.0
tinycss2==1.2.1
tokenizers==0.13.3
tomli==2.0.1
torch==2.1.0
tornado==6.3.3
tqdm==4.66.1
traitlets==5.13.0
transformers==4.31.0
triton==2.1.0
trl==0.4.7
types-python-dateutil==2.8.19.14
typing_extensions==4.8.0
tzdata==2023.3
ubuntu-advantage-tools==8001
ufw==0.36.1
unattended-upgrades==0.1
uri-template==1.3.0
urllib3==2.1.0
wadllib==1.3.6
wcwidth==0.2.10
webcolors==1.13
webencodings==0.5.1
websocket-client==1.6.4
Werkzeug==3.0.1
widgetsnbextension==4.0.9
xkit==0.0.0
xxhash==3.4.1
yarl==1.9.2
zipp==1.0.0
Теперь у нас есть виртуальный сервер с драйверами Nvidia, набор инструментов, пайтоновское окружение с библиотеками, необходимыми для запуска LLM.
Запуск jupyter-notebook
Для тестирования использовалась тренировочная часть набора данных mlabonne/guanaco-llama2-1k с 1 000 текстов объемом от 58 до 11 400 символов.
Распределение размеров текстов показано на графике:
Результаты теста
Разумеется, обе видеокарты мы тестировали с использованием одного и того же скрипта. Результаты обучения каждой модели и результаты генерации текста мы свели в таблицы.
Ошибка Out of Memory (CUDA OOM) говорит о том, что видеокарте не хватило памяти для выполнения задачи. Здесь мы не будем подробно детализировать загрузку RAM, так как цель теста — сравнить возможности видеокарт, а не найти оптимальный способ обучения LLM. Если вам интересно узнать больше о том, как расходуется память GPU при обучении больших языковых моделей, рекомендуем эту статью.
Читая все таблицы ниже, стоит учитывать, что процентовка памяти и использования ядер GPU берутся из утилиты nvitop.
Обучение meta-llama/Llama-2-7b-chat-hf
Как мы видим, при работе с небольшими батчами обе видеокарты успешно справляются с нагрузкой. Кстати, сравнение наглядно показывает, почему нельзя опираться только на технические характеристики при выборе видеокарты — обладающая меньшим объемом памяти и количеством ядер А100 справилась с одним из тестов куда лучше, чем более мощная А6000 Ada, хотя и задействовала свой вычислительный ресурс на 97,7%.
Впрочем, при отгрузке батчей по 25 и 27 текстов А100 уже ушла в CUDA OOM, тогда как А6000 Ada продолжила работать, и мы завершили обучение модели. Поэтому однозначный вывод сформулировать не получится. Если карта стабильно будет отдаваться целиком под одну задачу, а батчи датасетов будут содержать не более 20 текстов, стоит присмотреться к А100. Однако в пользу А6000 Ada говорит тот факт, что больший объем памяти позволит тестировать больше вариантов размера батча, которые могут влиять на сходимость обучения.
Обучение meta-llama/Llama-2-13b-chat-hf
Как и в случае с предыдущей моделью, до момента исчерпания доступной памяти А100 показывает очень хорошие результаты, опережая в скорости обучении А6000 Ada. Мы приблизились к пороговому значению RAM на А100 (96,3% доступной памяти) только при отгрузке батчей по 15 текстов.
Попытка отгружать батчи по 25 и более текстов отправила обеих испытуемых в CUDA OOM. Очевидно, при использовании только одной видеокарты, будь то А100 или А6000 Ada, не хватит ее памяти. Да, причина все та же: слишком тяжелые батчи вместе с большой моделью и необходимыми для обучения компонентами не помещаются ни в 40, ни в 48 ГБ.
Обучение meta-llama/Llama-2-70b-chat-hf
Этот тест мы провели скорее из любопытства, чтобы узнать, справится ли одна А6000 Ada с обучением модели с 70 миллиардами параметров. Вывод: справится, но только если мы будем отдавать ей по одному тексту за раз. Попытка отдавать по два текста в батче ведет к исчерпанию RAM.
От А100 мы не ждали многого. Здесь одного квантования мало — чтобы сэкономить немного памяти и просто загрузить модель, пришлось понизить ранг разложенных матриц до двух и восьми (тот самый случай изменения конфигурации LoRA, о котором шла речь в начале) и также отгружать по одному тексту. Однако с практической точки зрения в этом нет смысла — целесообразнее взять либо одну А6000 Ada, либо две А100 и NVLink.
Генерация текста
С точки зрения генерации текста А6000 Ada оказалась лидером в каждом из тестов.
Ожидаемо, при увеличении количества токенов становится все более заметной разница в скорости вывода данных. Для наглядности мы преобразовали табличные значения в графики. Независимо от размера обучаемой модели и количества токенов А6000 Ada предсказуемо оказывается быстрее.
Как мы уже отметили, обучение самой большой модели, meta-llama/Llama-2-70b-chat-hf, на А100 удалось запустить только при изменении конфигурации LoRA. Но даже в этом случае оно не было завершено. Вместе с тем, это ни в коем случае не говорит о непригодности А100 для генерации текста большими языковыми моделями. В рамках тестирования мы рассмотрели не все варианты. Например, не стали объединять две А100 через NVLink.
Заключение
Напрашивается очевидный вывод: А6000 Ada обладает более высокими характеристиками, чем А100. Для ресурсоемких задач используйте ее. Но ради такого вывода не стоило и проводить тест.
- Использование А100 оправданно в обучении «легких» и «средних» LLM при отгрузке небольших батчей — эта видеокарта справляется с задачами за то же время, что А6000 Ada, или даже быстрее.
- А6000 Ada в силу большего объема памяти и количества ядер оставляет простор для экспериментов с размерами батчей.
- С генеративными задачами (ради которых все и затевается) А6000 Ada справляется быстрее, поэтому для работы с уже обученными LLM эта карта выглядит интересно;
- Отсутствие поддержи MIG и NVLink накладывает ограничения на эксплуатацию А6000 Ada — это отличное железо для задач, под которые она будет отдаваться целиком и которые будет решать в одиночку. Если актуален шеринг ресурсов или объединение карт для увеличения вычислительных мощностей, стоит рассмотреть А100.
Как видите, у нас нет однозначного вывода о том, какая карта лучше подойдет для работы с LLM. Мы провели тестирование, чтобы увидеть реальные возможности А100 в сравнении с А6000 Ada в одинаковых условиях. Выбор той или иной модели GPU стоит делать исходя из количества и сложности реальных задач, возможностей шеринга ресурсов и, конечно, доступных финансов.
Впрочем, мы планируем запустить еще несколько тестов различных конфигураций GPU для решения ML-задач. В комментариях предлагайте кандидатов для тестирования.
Возможно, эти тексты тоже вас заинтересуют:
→ AMD решила пойти по пути Nvidia и выпустила урезанный ИИ-чип. Но что-то пошло не так
→ Питомцы и их айтишники: 6 историй ко дню кошек
→ Что такое Data diode и зачем он нужен?