GPU ждал не вычислений, а CPU: Moondream отыграла 35% на B200

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

Вкратце

При генерации токенов GPU часто простаивает не из-за нехватки вычислений, а потому что CPU ещё не закончил подготовку следующего токена. Moondream называет разрыв «пузырём GPU» и переписала движок Photon так, чтобы его спрятать: следующий проход вперёд накладывается на сэмплинг предыдущего.

Выигрыш растёт вместе с мощностью машины. На RTX 3090 приём даёт около 6,5%, а на B200, где синхронизация с CPU занимает бóльшую долю крошечного прохода в 2,4 мс, уже 35,4%. Инженер по инференсу возразил: цифра так велика лишь потому, что модель маленькая.

GPU простаивал, и виноват был CPU

Обложка: как лопнуть пузырь GPU
Обложка: как лопнуть пузырь GPU

Инженерный пост Moondream начинается с контринтуитивного замера: во время генерации токенов GPU часто не делает ничего, и дело не в нехватке вычислений. Он ждёт, пока CPU закончит подготовку следующего токена. Этот простой авторы называют пузырём GPU (разрыв в конвейере вычислений, а не финансовый), и весь пост посвящён тому, как закрыть его внутри Photon, их движка инференса, чтобы выйти на почти реальное время в vision-language инференсе: примерно 33 мс на NVIDIA B200.

Наивный цикл декодирования строго последователен: CPU готовит шаг, запускает проход вперёд, ждёт его, сэмплирует токен, потом начинает заново. На каждой передаче между CPU и GPU кто-то из них простаивает.

Сравнение таймлайнов: последовательные передачи сверху, наложенное исполнение снизу
Сравнение таймлайнов: последовательные передачи сверху, наложенное исполнение снизу

Верхняя часть таймлайна — это проблема: работа CPU и работа GPU идут по очереди. Нижняя показывает цель: пока GPU перемалывает шаг t+1, CPU уже фиксирует и сэмплирует шаг t. Никто не ждёт.

Два слота и копирование за спиной GPU

Чтобы наложить шаги, нужно куда-то положить входы и выходы шага t+1, не затирая данные шага t, которые CPU ещё читает. Photon использует пинг-понг-слоты: два чередующихся набора DecodeSlot, каждый хранит подготовку входов, вывод логитов, сэмплированные токены и учёт KV-кэша. Нечётные и чётные шаги используют разные слоты, поэтому GPU может писать в один, пока CPU читает другой.

Пинг-понг-слоты: два чередующихся набора буферов с отдельными потоками вычислений и копирования
Пинг-понг-слоты: два чередующихся набора буферов с отдельными потоками вычислений и копирования

Второй приём — отложенное копирование. Чтение сэмплированного токена с GPU на CPU обычно принудительно вызывает синхронизацию. Photon выносит это копирование device-to-host в отдельный CUDA-поток, то есть в упорядоченную очередь, которую GPU разбирает по порядку и в которой работа из разных потоков может накладываться, так что копирование токена идёт, пока GPU уже занят следующим проходом.

Отложенное копирование: поток копирования работает независимо от потока вычислений
Отложенное копирование: поток копирования работает независимо от потока вычислений

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

Планировщик выполняет каждый цикл как тик продвижения из трёх фаз: запустить проход для шага t+1, зафиксировать шаг t, затем завершить сэмплинг для t+1. Порядок важен: маски ограниченного декодирования зависят только от уже зафиксированного состояния, но не от результатов, которые ещё в полёте на GPU.

Тик продвижения: запуск прохода t+1, фиксация t, завершение сэмплинга t+1
Тик продвижения: запуск прохода t+1, фиксация t, завершение сэмплинга t+1

Этот конвейер порождает коварный класс багов. Последовательность может достичь токена конца последовательности на шаге t, когда она уже упакована в батч шага t+1 на GPU. Убить её сразу значило бы испортить батч в полёте. Photon оставляет её живой как зомби: флаг finalized помечает её завершённой после EOS или лимита длины, а счётчик inflight_refs отслеживает, сколько шагов в полёте всё ещё на неё ссылаются. Слот освобождается только когда счётчик обнуляется.

Жизненный цикл зомби: EOS на шаге t, продолжает жить как зомби в t+1, освобождается при обнулении inflight_refs
Жизненный цикл зомби: EOS на шаге t, продолжает жить как зомби в t+1, освобождается при обнулении inflight_refs

Цифры: выигрыш растёт вместе с GPU

Замеренная стоимость шага делится на проход вперёд и шаг сэмплинга. Чем быстрее GPU, тем сильнее фиксированные накладные расходы CPU доминируют в периоде:

Железо Конфигурация Проход (мс) Сэмплинг (мс) Период (мс)
3090 1 stream 4.87 0.20 5.10
3090 8 streams 6.66 0.27 6.97
3090 32 streams 10.24 0.26 10.52
B200 1 stream 2.45 0.14 2.63
B200 8 streams 3.12 0.14 3.30
B200 32 streams 3.80 0.14 3.98

На фоне блокирующего базового варианта преимущество конвейерного цикла растёт от единиц процентов на 3090 до трети с лишним на B200:

Железо Конфигурация Блокирующий (мс) Конвейерный (мс) Прогноз Замер
3090 1 stream 5.44 5.10 +5.7% +6.5%
3090 8 streams 7.52 6.97 +7.6% +7.8%
3090 32 streams 11.74 10.52 +11.1% +11.6%
B200 1 stream 3.11 2.63 +17.2% +17.6%
B200 8 streams 4.04 3.30 +22.2% +21.9%
B200 32 streams 5.55 3.98 +39.1% +35.4%
Таймлайн декодирования на B200: конвейер держит GPU занятым практически весь период
Таймлайн декодирования на B200: конвейер держит GPU занятым практически весь период

Замеренный таймлайн B200 подтверждает замысел: с конвейером GPU занят практически весь период, пузырь выдавлен.

Тред: отличный приём или артефакт маленькой модели?

Самый острый ответ пришёл от человека внутри дисциплины, который прочитал те же таблицы и вынес противоположный урок.

«блог хорош, но на нём куча отпечатков CODEX, и к тому же он очень привязан к размеру конкретной модели… проход вперёд в 2,4 мс — это крохи, а 9 млрд параметров помещаются на одном GPU. Большие модели ближе к 30–40 мс. Синхронизация CPU-GPU — это 1–2 мс… куда менее важно, чем, например, планирование вычислений/коммуникаций или оптимизация ядер». — инженер, работающий в этой области

Вот несущая оговорка: синхронизация CPU в 1–2 мс — это 35% от прохода в 2,4 мс и едва 5% от прохода в 40 мс. Именно поэтому колонка B200 выглядит настолько лучше колонки 3090. Чем больше модель, тем меньше доля этого пузыря в общем времени.

Бóльшая же часть треда зацепилась за заголовок. Бывший основатель ML-компании прямо описал путаницу:

«Это правда, но я никогда не слышал, чтобы это называли пузырём GPU… большинство слышит „GPU bubble“ и думает про какой-нибудь финансовый пузырь». — бывший основатель ML-компании и стартап-CTO

Другой читатель высказался резче, назвав такое имя намеренной наживкой на термин, который означает обвал рынка, и предложил вместо него GPU-CPU pipeline stall. Один комментатор предложил спасительную аналогию: кавитация винта на корабле, где скорость притока воды и форма лопасти решают, сколько эффективности стоят пузыри. Здесь GPU играет роль винта, а работа — приток воды.

Вывод для того, кто выкатывает инференс: приём окупается, когда проход вперёд достаточно мал, чтобы накладные расходы CPU были заметной его долей. На большой MoE-модели инсайдер из треда прав: сначала стоит потратить силы на наложение коммуникаций и на ядра.

Получайте такие разборы каждый день

Главные истории Hacker News — на email, в Telegram или RSS.

Следующая новость · 6 из 26 Читать дальше
📈Biz

Доля труда в доходах США — минимум с войны. Вердикт ФРБ Нью-Йорка: ничего нового

Доля работников в национальном доходе — самая низкая со времён Второй мировой, на 1,6 пункта ниже, чем до пандемии. Федеральный резервный банк Нью-Йорка проверил, отличается ли нынешний спад от прежних, и ответил на собственный заголовок: нет.

★ 461 · 💬 498 · libertystreeteconomics.newyorkfed.org · 4 мин