Почему DeepSeek дешев при масштабировании, но дорог для локального запуска
Почему DeepSeek-V3 якобы быстр и дешев для обслуживания в масштабе, но слишком медленен и дорог для локального запуска? Почему некоторые ИИ-модели медленно отвечают, но работают быстро после запуска?
О чем пойдет речь
Провайдеры ИИ-инференса часто говорят о фундаментальном компромиссе между пропускной способностью (throughput) и задержкой (latency): для любой конкретной модели вы можете обслуживать её либо с высокой пропускной способностью и высокой задержкой, либо с низкой пропускной способностью и низкой задержкой. Фактически, некоторые модели настолько естественно неэффективны для GPU, что на практике их приходится обслуживать с высокой задержкой, чтобы иметь хоть какую-то приемлемую пропускную способность (например, DeepSeek-V3).
Этот компромисс исходит из размера батча (batch size), который провайдер инференса выбирает для модели: не батчинг инференса внутри отдельного запроса1, а батчинг инференса среди десятков или сотен одновременных запросов пользователей. Это своеобразная особенность LLM на базе трансформеров: вычисление пакета завершений (completions) одновременно происходит почти так же быстро, как вычисление одного завершения. Почему так?
Что такое батчинг инференса?
GPU хороши в выполнении больших матричных умножений (GEMM, или «общие матричные умножения»). Скажем, у вас есть один токен, который вы хотите пропустить через модель (т.е. умножив на все её веса — другие детали архитектуры не важны). Вы выражаете это как вектор, соответствующий размерности (или скрытому размеру) модели (т.е. 1 x ширина её больших матриц весов), и перемножаете. Это 1 GEMM. Но если вы хотите пропустить десять токенов в пакете, это всё равно только один GEMM, потому что вы можете сложить токены в одну матрицу (10 x размерность модели). Это намного быстрее, чем делать десять чуть меньших GEMM. Так что реализация сервера инференса может выглядеть примерно так:
- Приходит запрос с промптом.
- Этот промпт предварительно заполняется (pre-filled) (пропускается через attention — позже мы увидим, как это тоже можно батчить2), формируя KV-кэш и матрицу размером с токен (1 x размер модели), которая в итоге станет предсказанным токеном3.
- Эта матрица размером с токен попадает в очередь.
- Сервер GPU забирает пакеты (например, по 128) из этой очереди, складывает их в матрицу 128 x размер модели и перемножает их через веса feed-forward модели.
- Конечный результат затем разделяется на 128 отдельных токенов.
- Токен для исходного запроса стримится обратно пользователю.
- Если этот токен не является токеном конца последовательности, возвращаемся к шагу 2, чтобы продолжить генерацию следующего токена в ответе.
Обратите внимание, что сервер решает, какой размер батча брать. Это компромисс между пропускной способностью и задержкой. Если вы не делаете батчинг и просто обрабатываете токены по одному, ни один пользователь никогда не ждет в очереди (шаг 3 выше), поэтому задержка низкая (при условии, что у вас достаточно GPU). Однако, если вы делаете большой батчинг, задержка высока, потому что пользователи будут ждать, пока наберется размер батча, но пропускная способность будет намного выше, потому что GPU используются более эффективно.
Почему GPU быстрее умножают большие матрицы один раз, чем маленькие матрицы много раз? Две причины. Во-первых, есть некоторые накладные расходы на отправку каждой команды на GPU, и одно большое умножение можно запустить одной командой. Во-вторых, каждая новая команда GPU включает извлечение весов из памяти, что может быть дорого для больших весов. Если вы запускаете много маленьких GEMM, вы можете потратить большую часть времени на пересылку весов в память и из неё, вместо вычислений.
Почему некоторые модели настроены на большие размеры батчей?
Обычно у сервера инференса есть «окно сбора», когда приходят запросы пользователей и ставятся в очередь. Чат-серверы обычно стремятся к 5-10 мс, но бэкенды с очень высоким батчингом могут доходить до 200 мс. Если новый запрос приходит в начале окна, он может прождать всю длительность окна перед обработкой4. Когда окно закрывается, все запросы в очереди пакетируются (т.е. все матрицы 1xразмер_модели объединяются в одну матрицу 128xразмер_модели), и этот пакет отправляется по конвейеру. Запуск такого пакета иногда называют «тиком».
Как предполагает объяснение выше, вы можете запустить любую модель с любым размером батча. В процессе батчинга нет ничего, что исключало бы какие-то типы моделей. Однако можно построить модель настолько GPU-неэффективно, что ей фактически понадобится батчинг, чтобы быть практичной.
Почему mixture of experts требует больших размеров батчей
Например, возьмем модель mixture-of-experts (как DeepSeek-V3 или, предположительно, оригинальный GPT-4). Вы можете получить сильную модель, обучив ее иметь сотни и сотни «экспертов»: отдельные блоки feed-forward весов, из которых слой маршрутизации выбирает подмножество, используемое для каждого токена. Но такая модель очень неэффективна для GPU. Мы можем понять почему: GPU хотят делать небольшое количество действительно больших матричных умножений, но если у вас много экспертов, вы вынуждены делать много маленьких умножений. Если вы не делаете инференс в батчах, это будет означать низкую пропускную способность.
Давайте подумаем, как «окно сбора» в 5 мс и 200 мс сработает для большой модели mixture-of-experts. Допустим, вы набрали десять запросов пользователей в этом 5 мс окне. Если у вас много экспертов, некоторые эксперты могут в конечном итоге работать только с одним или двумя токенами (т.е. размер батча для каждого эксперта будет намного ниже, чем общий набор запросов, который вы набрали в своем окне). Если же вы подождете 200 мс и наберете 4000 запросов пользователей, вы с гораздо большей вероятностью насытите всех своих экспертов. Ценой некоторой задержки вы гарантируете, что ваши GEMM будут большими, а ваши GPU будут постоянно использоваться на полную мощность.
Почему большие пайплайны требуют больших размеров батчей, чтобы избежать пузырей
Для больших моделей может быть проблемой вообще поддерживать активность GPU. Большие модели обычно имеют много слоев трансформеров: т.е. сотни матриц весов, составляющих feed-forward сеть. Единственный способ сделать быстрый инференс здесь — это пайплайнинг этих слоев: один GPU обрабатывает первые десять слоев, другой — следующие десять, и так далее. Иначе вы просто не сможете уместить все веса в памяти одного GPU, поэтому вы потратите кучу времени на свопинг весов в память и обратно, и это будет очень медленно. Во время инференса каждый токен (обычно в «микро-батче» из нескольких десятков токенов каждый) последовательно проходит через этот конвейер GPU.
Насколько эффективен ваш пайплайн, зависит от количества слоев и размера вашего окна сбора. Когда вы обрабатываете токены в окне во время «тика», у вас будут простаивающие GPU в начале (потому что GPU в более поздних слоях еще нечего обрабатывать) и еще немного простаивающих GPU в конце (когда в очереди больше нет токенов, GPU в ранних слоях должны будут ждать следующего «тика»). Эти периоды простоя иногда называют «разогревом» и «сливом». Если у вас много маленьких окон, вы потратите больше времени GPU на разогрев и слив, чем если бы у вас было меньше больших окон. Выбирая размер окна, вы таким образом напрямую выбираете между пропускной способностью и задержкой.
Если у вас куча слоев и ваше окно сбора очень короткое, вы можете иногда оказаться с меньшим количеством токенов для обработки, чем слоев. Это называется «пузырь пайплайна» — по сути, стадия «слива» начинается раньше обычного. Вы не можете устранить разогрев и слив (по причинам, обсуждаемым ниже, инференс должен работать последовательными «тиками»), но вы можете устранить пузыри пайплайна, сделав окно сбора достаточно длинным. Пузыри пайплайна могут быть абсолютно убийственными для пропускной способности модели, поэтому провайдеры инференса всегда устанавливают свои окна достаточно широкими, чтобы их избежать. Это добавляет заметную задержку для моделей с большим количеством слоев.
Разве нельзя просто держать очередь полной?
Почему провайдеры инференса не могут полностью устранить разогрев и слив, держа очередь GPU полной токенов? Другими словами, разве нельзя вообще отказаться от тиков и просто поддерживать поток микро-батчей токенов? Конечно, инференс каждого пользователя должен быть последовательным (поскольку вы не можете начать генерировать следующий токен, пока текущий не готов), но у крупных провайдеров инференса должно быть достаточно одновременного трафика, чтобы держать очередь полной отдельных запросов пользователей.
Признаюсь, мне трудно понять, почему это не должно быть возможно в теории. Насколько я могу судить, практическим барьером является то, как пакетируется шаг attention: если вы хотите пакетировать GEMM внимания, они все должны быть одной формы (т.е. одинаковое количество предыдущих токенов в последовательности). Поэтому вам приходится запускать группы одной формы одновременно, вместо того чтобы просто поддерживать одну очередь. Есть как минимум некоторые публичные исследования на этот счет, но я бы не удивился, если бы существовали более хитрые трюки для этого, которых я не видел.
Еще одна идея: если вам нужны тики для шага attention, почему бы просто не иметь систему attention-инференса на основе тиков и более эффективную непрерывную систему для FFN? Как я понимаю, причина в накладных расходах памяти:
- Поскольку вывод attention нужен для FFN, вам нужно где-то в памяти припарковать его, пока он ждет своего слота в очереди FFN, что быстро станет слишком дорогим.
- Современные стеки инференса способны объединять шаг attention и FFN в пару больших GEMM в одной «операции». Если вы делаете их на разных GPU, вам нужно запускать разные операции и пересылать веса в память и обратно.
Итог
- GPU наиболее эффективны на больших GEMM, поэтому складывание множества токенов в одно матричное умножение дает гораздо более высокую пропускную способность токенов, чем обработка их по одному.
- Во время декодирования attention может быть пакетирован только для токенов на одном и том же шаге, заставляя планировщики работать короткими «тиками». Сколько токенов вы упаковываете в один «тик» (т.е. как долго вы ждете сбора токенов) — это ваш размер батча.
- Это токены от разных пользователей. Вы не можете пакетировать токены от одного и того же пользователя, потому что вам нужны предыдущие токены для генерации следующего, поэтому батчинг требует большого объема трафика от разных пользователей.
- Большие батчи повышают задержку, потому что токены пользователей могут ждать до 200 мс, пока батч не заполнится достаточно для запуска, но они повышают пропускную способность, позволяя большие (и, следовательно, более эффективные) GEMM на шаге feed-forward.
- Модели с большим количеством слоев (например, длинные пайплайны) нуждаются в больших батчах, чтобы избежать пузырей пайплайна (обеспечивая, чтобы каждый тик содержал больше батчей, чем шагов пайплайна).
- Модели Mixture-of-Experts должны обслуживаться с высокой задержкой, чтобы быть эффективными: каждый эксперт видит только токены, направленные к нему, поэтому вам нужны большие глобальные батчи, чтобы держать каждого эксперта занятым.
- Провайдеры инференса выбирают размер батча/окна, который устраняет пузыри пайплайна и насыщает экспертов. Высокие размеры батчей дают вам больше пропускной способности ценой более высокой задержки, так как токены ждут заполнения тика.
- Некоторые модели (как DeepSeek), являющиеся mixture-of-experts с большим количеством слоев, таким образом требуют больших размеров батчей и высокой задержки, иначе пропускная способность падает с обрыва. Вот почему часто говорят, что вы не можете легко запустить DeepSeek для личного использования: потому что с одним пользователем, запускающим один инференс за раз, он работает с очень низкой эффективностью/пропускной способностью.
- Тот факт, что модели OpenAI и Anthropic быстро отвечают, предполагает, что либо:
- У их моделей более эффективная архитектура (не MoE, меньше слоев), либо
- У OpenAI/Anthropic есть какие-то очень хитрые трюки для обслуживания инференса, либо
- Они платят бешеные деньги за гораздо большее количество GPU, чем им строго необходимо.
UPD: Этот пост был опубликован на Hacker News с кучей комментариев. Я немного жалею, что не озаглавил этот пост иначе — он на самом деле не о запуске моделей на вашем собственном компьютере. Он о запуске моделей для личного использования, при условии, что у вас есть все GPU (т.е. компромисс батчинг/пропускная способность).
Footnotes
-
Одна из часто наблюдаемых сильных сторон трансформеров заключается в том, что они могут пакетировать prefill в рамках одного запроса пользователя. Когда вы передаете им длинный промпт, они могут обработать этот промпт сразу целиком благодаря тому, как работает механизм attention. Предыдущим рекуррентным моделям приходилось идти токен за токеном, что было намного медленнее (потому что это включало намного больше GEMM). Это не имеет ничего общего с тем видом батчинга, о котором я говорю в этом посте. Я говорю о том, как можно эффективно пакетировать инференс среди множества разных запросов пользователей, как только префиллинг завершен. ↩
-
Это также можно пакетировать, при условии, что вы пакетируете только операции attention с одинаковым количеством токенов в последовательности (т.е. каждая последовательность, предсказывающая четвертый токен, может быть спакетирована вместе). В противном случае размер матриц KV-кэша будет разным, поэтому вы не сможете легко объединить их в один пакет. Подробнее об этом позже. ↩
-
Технически генерируется не токен, а «логиты» (распределение вероятностей по всем возможным токенам). Я буду говорить «токен» здесь и далее, чтобы было проще. ↩
-
Обратите внимание, что на практике современные стеки инференса будут использовать «непрерывный батчинг», где пакет отправляется, как только он заполнен, вместо ожидания всей длины фиксированного временного окна. Однако инференс все равно выполняется пакетами, так что основной компромисс между пропускной способностью и задержкой тот же. ↩