Привет Хабр, с вами снова ServerFlow. И да, мы потратили 320 тысяч рублей, чтобы проверить, есть ли какой-то толк от Nvlink при развертывания нейросетей? Тесты мы провели на двух видеокартах Nvidia A5000 24GB, сначала объединённые NVLink, а после без него. Для наглядности теста нами была выбрана последняя языковая модель LLaMA 3.2 на 90 миллиардов параметров. Что у нас в итоге вышло – сможете узнать ниже в посте.
Мотивацией проведения этого теста послужила позиция Nvidia в отношении NVLink. Компания представляет эту технологию как премиальную функцию, доступную только для избранных видеокарт. NVLink имеет два ключевых преимущества: во-первых, пропускная способность протокола значительно выше, чем у PCI, а во-вторых, чипы видеокарт связаны напрямую в обход процессора. Теоретически, эти особенности должны обеспечивать более высокую скорость выполнения задач.
На текущий момент на рынке доступен PCI 5.0 X16 и его пропускная способность составляет 63GB/s. Для сравнения, даже у первого поколения NVLink максимальная пропускная способность составляла 160GB/s – почти в 2.5 раза быстрее. В нашем случае с NVIDIA A5000 картина следующая: видеокарты используют шину PCIe 4.0 x16 с пропускной способностью 32GB/s, в то время как их NVLink обеспечивает скорость передачи данных 112GB/s – более чем в три раза быстрее.
Однако стоит отметить важный момент: даже такая впечатляющая скорость NVLink всё равно значительно уступает пропускной способности памяти самих GPU (в 6-7 раз медленнее). Тем не менее, главное преимущество NVLink остается неизменным – это не только более высокая скорость передачи данных по сравнению с PCIe, но и возможность работы в обход процессора, что теоретически должно повышать эффективность при определенных сценариях использования.
Однако возникает важный вопрос: насколько критична эта разница в скорости для работы с нейросетями? В теории более быстрое соединение между GPU должно давать преимущество, особенно при параллельной обработке данных. И хотя технические характеристики NVLink выглядят впечатляюще, реальная производительность в задачах машинного обучения может существенно отличаться от теоретической.
Тестовый стенд в наших опытах остался неизменным. Для тех, кто пропустил наши прошлые эксперименты, а также чтобы освежить память постоянным читателям, напомню его характеристики.
Процессор: AMD EPYC™ 7502 (32 ядра / 64 потока, 2.5GHz-3.35GHz, 180W, 128MB L3)
Материнская плата: Supermicro H11SSL-I (Rev 2.0)
Система охлаждения: 4U башенного типа c TDP 240W
Оперативная память: 128GB (8 x 16GB, 3200 MHz, ECC REG)
Системный накопитель: Samsung PM9A1 1TB
Блок питания: Cougar BXM 1000 [CGR BX-1000]
Из изменений, как мы уже говорили в самом начале это две Nvidia A5000 24GB GDDR6 памяти и мостик NVLink, который будет участвовать в тестах.
В nvtop, отличий пока не особо заметно, не сказать бы что они и должны быть в простое, но этот излюбленный нами мониторинг видимо не умеет определять наличие NVLink.
Также стоит отметить, что с выхода мультимодальной большой языковой модели LLaMA 3.2 прошло уже достаточно много времени и её поддержка наконец-то появилась в популярном движке для инференса Ollama, а значит в этот раз нам не придётся писать скрипт для инференса на Python самостоятельно.
Для начала установим Ollama, делается это крайне просто –
curl -fsSL https://ollama.com/install.sh | sh
Следующий шаг — установка модели для тестирования эффективности NVLink. Благодаря увеличенному объему видеопамяти (как на каждой карте, так и суммарно) мы можем использовать действительно мощную модель — LLaMA 3.2 с 90 миллиардами параметров. Это серьезный шаг вперед по сравнению с нашими предыдущими тестами, где мы были ограничены в возможностях из-за меньшего объема доступной памяти.
В наших тестах мы используем версию модели с 4-битной квантизацией. Такой выбор не случаен: несмотря на внушительные 48 гигабайт суммарной видеопамяти наших карт, этого все равно недостаточно для работы с полноценной версией модели в формате FP16. Квантизация до 4 бит позволяет значительно уменьшить требования к памяти при сохранении приемлемого качества работы модели. Это оптимальный компромисс между производительностью и ресурсоемкостью для наших тестов.
ollama run llama3.2-vision:90b-instruct-q4_K_M
И запустим сервер Ollama
ollama serve
Плюс в этот раз мы будем тестировать не из консоли, а через Open Web UI, его поставим в виде Docker-контейера следующей командой –
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_U
RL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
После того как контейнер запустится, сразу перейдём по следующему адресу – http://0.0.0.0:8080/admin/settings
В первый раз потребуется создать аккаунт, да странно, но все данные хранятся локально, а не привязаны к какому-то сервису. По идее можно вовсе это отключить, но в рамках наших тестов, пусть будет по умолчанию.
В настройках, в вкладке соединение указываем адрес до сервера Ollama – http://127.0.0.1:11434
Чтобы избежать ошибок, перед началом тестирования убедитесь, что в настройках выбрана правильная модель. В нашем случае это LLaMA 3.2 90B — именно она будет использоваться для всех последующих тестов производительности.
Для первого теста мы попросили модель создать примитивный лендинг на основе скриншота реального сайта. Результат оказался не очень впечатляющим, но сейчас нас интересует именно производительность. Сгенерированный код доступен во вкладке 'artifacts', которая по умолчанию отображается справа. Если вкладка скрыта, её можно открыть через меню (три точки в правом верхнем углу).
При тестировании с включенным NVLink скорость генерации составила 2.82 токена в секунду. Последующие тесты с распознаванием изображений показали схожие результаты. Интересно, что при работе с чисто текстовыми запросами модель работает заметно быстрее — в среднем 3.75 токенов в секунду.
Были подозрения, что даже квантизированная модель на 90 миллиардов параметров полностью не может разместиться в 48Gb видеопамяти и Ollama частично отгрузит её в оперативную память, однако этого не произошло и процессор с оперативной памятью по большей части простаивали.
А вот мониторинг видеоускорителей в nvtop оказался более интересным, обе A5000 в процессе инференса судя по графиками потребления видеопамяти и загрузке чипов, большую часть времени не нагружались в равной степени. Мониторинг отображал картину которая больше походила на то, что по какой-то причине в один момент времени была в большей степени загружена либо одна, либо другая видеокарта, хотя иногда нагрузка и была поровну.
Продолжаем тестировать наши две A5000, но уже без NVLink, для интерактивности попробуем дать LLaMA задание посложнее, пусть вместо лендинга попробует написать клеточный аппарат Конвея, он же “Игра жизнь”. Для конкретики, пусть реализация будет на HTML+CSS+JS.
На удивление, в этот раз модель справилась куда лучше, код даже запустился с первого раза внутри Open Web UI. Но вот скорость инференса пока что в районе погрешности в сравнение с тем когда был установлен NVLink – 3.68 токенов в секунду, что лишь на 0.07 токенов медленнее, чем с NVLink, что всего лишь на 1.86% медленнее. В новых чатах, дабы не было погрешности на увеличившееся контекстное окно, результат остается стабильным, в пределах погрешности с незначительным преимуществом в пользу NVLink.
Интереса ради вернёмся к чату с клеточным аппаратом и попросим добавить функционал, чтобы можно было самостоятельно добавлять клетки к уже случайно сгенерированным. Но это ему сделать уже почему-то не удалось, хотя случайно созданные клетки всё ещё работают. Получившийся код ниже –
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Conway's Game of Life</title>
<style>
/* Simple styling for our grid */
.grid {
display: flex;
flex-wrap: wrap;
width: 400px; /* Adjust the size as needed */
}
.cell {
width: 10px; /* Cell size, adjust as needed */
height: 10px;
background-color: white;
border: 1px solid black;
}
.alive {
background-color: black;
}
</style>
</head>
<body>
<div id="grid"></div>
<button onclick="startGame()">Start Game</button>
<script>
// Grid dimensions
const gridSize = 40; // Adjust as needed, must be a square number for simplicity
let grid = [];
let intervalId;
let isDrawing = false;
// Function to create the initial grid and draw it on the page
function createGrid() {
const gridElement = document.getElementById('grid');
gridElement.innerHTML = ''; // Clear any existing content
for (let i = 0; i < gridSize * gridSize; i++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.addEventListener('click', handleCellClick);
gridElement.appendChild(cell);
}
// Initialize the grid state
grid = Array(gridSize).fill().map(() => Array(gridSize).fill(false));
}
// Function to draw the current state of the grid on the page
function drawGrid() {
const cells = document.querySelectorAll('.cell');
let index = 0;
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
if (grid[i][j]) {
cells[index].classList.add('alive');
} else {
cells[index].classList.remove('alive');
}
index++;
}
}
}
// Function to apply the rules of Conway's Game of Life
function nextGeneration() {
const newGrid = Array(gridSize).fill().map(() => Array(gridSize).fill(false));
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
let liveNeighbors = countLiveNeighbors(i, j);
if (grid[i][j] && (liveNeighbors === 2 || liveNeighbors === 3)) {
newGrid[i][j] = true;
} else if (!grid[i][j] && liveNeighbors === 3) {
newGrid[i][j] = true;
}
}
}
grid = newGrid;
}
// Function to count the number of live neighbors for a cell
function countLiveNeighbors(i, j) {
let count = 0;
const directions = [[-1,-1], [-1,0], [-1,1], [0,-1], [0,1], [1,-1], [1,0], [1,1]];
for (let k = 0; k < directions.length; k++) {
let ni = i + directions[k][0];
let nj = j + directions[k][1];
if (ni >= 0 && ni < gridSize && nj >= 0 && nj < gridSize) {
count += grid[ni][nj] ? 1 : 0;
}
}
return count;
}
// Function to handle cell click event
function handleCellClick(event) {
if (isDrawing) {
const cell = event.target;
const cells = document.querySelectorAll('.cell');
const index = Array.prototype.indexOf.call(cells, cell);
const i = Math.floor(index / gridSize);
const j = index % gridSize;
grid[i][j] = !grid[i][j];
drawGrid();
}
}
// Function to start the game
function startGame() {
isDrawing = false;
createGrid();
initializeRandomCells();
intervalId = setInterval(() => {
nextGeneration();
drawGrid();
}, 100);
}
// Function to initialize random cells
function initializeRandomCells() {
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
if (Math.random() < 0.2) {
grid[i][j] = true;
}
}
}
drawGrid();
}
// Add event listener to start drawing
document.addEventListener('keydown', (event) => {
if (event.key === 'd') {
isDrawing = !isDrawing;
}
});
createGrid();
</script>
</body>
</html>
Теперь проверим как быстро без NVLink модель справится с распознанием текста на скриншоте, для примера попросим описать содержание страницы на нашем сайте в разделе видеокарты для серверов –
И тут что называется, Остапа понесло, суть то страницы была распознана верно, но вот текст, один лишь заголовок. А скорость генерации – 3.14 токенов в секунду, что заметно быстрее, чем 2.82 с NVLink.
На английском результат вышел не сильно лучше, а по скорости, почему-то также быстрее, чем с NVLink – 3.03 токена в секунду.
Что же мы имеем в сухом остатке? NVLink, который по всем законам физики и маркетинга должен был дать существенный прирост в скорости обработки нейросетевых задач, показал себя довольно неоднозначно. В чистых текстовых запросах разница оказалась минимальной, а в задачах с распознаванием изображений система без NVLink и вовсе показала себя быстрее.
Однако важно понимать, что NVLink — это не универсальное решение для всех задач. Его реальная ценность проявляется в специфических сценариях, особенно при работе с моделями, которые слишком велики для размещения на одном GPU. Но даже здесь для получения значимого преимущества требуется тщательная оптимизация и специальная настройка под конкретную задачу. Это не та технология, которую можно просто подключить и сразу получить прирост производительности "из коробки".
Для тех, кто только начинает работу с нейросетями и планирует использовать несколько видеокарт, рекомендация простая: начните с базовой конфигурации двух карт без NVLink. Этого будет достаточно для большинства задач инференса и базового обучения. А вот энтузиастам, готовым глубоко погрузиться в оптимизацию и настройку моделей под свои специфические задачи, NVLink может предоставить дополнительные возможности для экспериментов.
Что касается наших Nvidia A5000 — несмотря на неоднозначные результаты с NVLink, сами видеокарты имеют крайне большой потенциал для интеграции в GPU сервера как по отдельности, так и в связке. Они достаточно современные чтобы поддерживать большую часть современных технологий, в особенности различные виды вычислений с плавающей запятой, а также имеют большое количество видеопамяти и самое главное для серверов – стандартизированные габариты и турбинная система охлаждения. В сегменте обычных потребителей за схожую цену можно взять к примеру RTX 4090, которая будет несколько мощнее в силу своей новизны, но из-за её габаритов и расположения разъёмов питания, в большую часть серверов её банально не получится разместить.
А что думаете вы? Есть ли у вас опыт использования NVLink в других сценариях? Особенно интересно услышать истории тех, кто работал над оптимизацией моделей специально под NVLink — в каких случаях удалось раскрыть потенциал этой технологии? Делитесь своими историями в комментариях.