🍊 Впихиваем невпихуемое. Видеопамять в GPUStack, vLLM и KV-cache
(монстр-текст 😐)
Недавно на работе немного пришлось поиграть в тетрис. Две видеокарты вышли из чата, но сервисы от этого работать перестать не должны. Значит, оставшиеся модели нужно как-то перераспределить по живым GPU и попытаться впихнуть невпихуемое.
И вот тут началось интересное.
Одна из моделей — Qwen — при развёртывании в GPUStack как будто фантомно требовала заметно больше памяти, чем потом реально занимала. На карте вроде бы есть свободная VRAM, по расчётам модель должна помещаться, а GPUStack говорит: нет, места недостаточно.
В итоге сначала пришлось освободить всё помещение, запустить Qwen, а уже потом аккуратно подселять к ней остальных.
Часть проблемы оказалась связана с тем, что модель VL. При развёртывании она прогоняет изображение максимально допустимого разрешения, из-за чего на старте потребление памяти временно раздувается. При обычной работе таких эксцессов уже не возникает.
А дальше выяснился ещё один нюанс — поведение vLLM и параметра:
Интуитивно хочется думать так: поставил 0.5 — значит разрешил модели занять половину той памяти, которая сейчас свободна.
Но работает это иначе.
vLLM считает эту долю от полного объёма VRAM видеокарты, а не от памяти, которая свободна в данный момент.
Например, если на GPU 24 ГБ:
gpu_memory_utilization = 0.5
означает бюджет примерно в 12 ГБ.
Если при этом свободно только 10 ГБ, это не превращается в «ну тогда дадим новой модели 5 ГБ». vLLM всё равно ориентируется на свои 12 ГБ.
И эти 12 ГБ — это не просто веса модели.
Если совсем грубо:
бюджет vLLM = weights + activations + overhead + KV-cache
Веса особо никуда не денутся, поэтому уменьшение gpu-memory-utilization — это не способ магически «сжать» модель. В первую очередь мы начинаем откусывать память у KV-cache и остальной рабочей памяти.
Из-за этого некоторые модели действительно удаётся довольно прилично поджать. Например, с Whisper у меня это сработало вполне успешно.
А с другими можно крутить параметр вниз сколько угодно: в какой-то момент веса плюс необходимый runtime overhead просто перестают помещаться в заданный бюджет. И всё — модель говорит: спасибо, я лучше вообще не запущусь.
Если очень упрощённо — это память модели о том, что она уже успела «прочитать».
Когда LLM генерирует следующий токен, attention нужны данные по предыдущим токенам контекста. Можно было бы на каждом шаге пересчитывать всё заново, но это было бы очень дорого.
Поэтому уже вычисленные Key и Value складываются в кэш и потом переиспользуются.
Отсюда простая зависимость:
больше контекст → больше KV-cache → больше VRAM
Причём дело не только в длине одного запроса. Важно ещё и количество запросов, которые модель обрабатывает одновременно. Поэтому модель, которая спокойно живёт при одном пользователе, под нормальной нагрузкой уже может начать заметно сильнее есть память.
И вот тут как раз становится понятно, за что мы платим, уменьшая gpu-memory-utilization.
Сама модель легче не становится. Мы просто оставляем меньше памяти под KV-cache и всё остальное вокруг неё.
То есть модель в GPU вроде бы влезла — отлично. Но дальше где-то придётся расплачиваться: количеством одновременно обрабатываемых токенов и запросов, запасом под длинный контекст или всем сразу.
Никакой бесплатной памяти тут, к сожалению, не появляется. 🚫
(монстр-текст 😐)
Недавно на работе немного пришлось поиграть в тетрис. Две видеокарты вышли из чата, но сервисы от этого работать перестать не должны. Значит, оставшиеся модели нужно как-то перераспределить по живым GPU и попытаться впихнуть невпихуемое.
И вот тут началось интересное.
Одна из моделей — Qwen — при развёртывании в GPUStack как будто фантомно требовала заметно больше памяти, чем потом реально занимала. На карте вроде бы есть свободная VRAM, по расчётам модель должна помещаться, а GPUStack говорит: нет, места недостаточно.
В итоге сначала пришлось освободить всё помещение, запустить Qwen, а уже потом аккуратно подселять к ней остальных.
Часть проблемы оказалась связана с тем, что модель VL. При развёртывании она прогоняет изображение максимально допустимого разрешения, из-за чего на старте потребление памяти временно раздувается. При обычной работе таких эксцессов уже не возникает.
А дальше выяснился ещё один нюанс — поведение vLLM и параметра:
--gpu-memory-utilization 🍊
Интуитивно хочется думать так: поставил 0.5 — значит разрешил модели занять половину той памяти, которая сейчас свободна.
Но работает это иначе.
vLLM считает эту долю от полного объёма VRAM видеокарты, а не от памяти, которая свободна в данный момент.
Например, если на GPU 24 ГБ:
gpu_memory_utilization = 0.5
означает бюджет примерно в 12 ГБ.
Если при этом свободно только 10 ГБ, это не превращается в «ну тогда дадим новой модели 5 ГБ». vLLM всё равно ориентируется на свои 12 ГБ.
И эти 12 ГБ — это не просто веса модели.
Если совсем грубо:
бюджет vLLM = weights + activations + overhead + KV-cache
Веса особо никуда не денутся, поэтому уменьшение gpu-memory-utilization — это не способ магически «сжать» модель. В первую очередь мы начинаем откусывать память у KV-cache и остальной рабочей памяти.
Из-за этого некоторые модели действительно удаётся довольно прилично поджать. Например, с Whisper у меня это сработало вполне успешно.
А с другими можно крутить параметр вниз сколько угодно: в какой-то момент веса плюс необходимый runtime overhead просто перестают помещаться в заданный бюджет. И всё — модель говорит: спасибо, я лучше вообще не запущусь.
А что вообще такое KV-cache? 😐
Если очень упрощённо — это память модели о том, что она уже успела «прочитать».
Когда LLM генерирует следующий токен, attention нужны данные по предыдущим токенам контекста. Можно было бы на каждом шаге пересчитывать всё заново, но это было бы очень дорого.
Поэтому уже вычисленные Key и Value складываются в кэш и потом переиспользуются.
Отсюда простая зависимость:
больше контекст → больше KV-cache → больше VRAM
Причём дело не только в длине одного запроса. Важно ещё и количество запросов, которые модель обрабатывает одновременно. Поэтому модель, которая спокойно живёт при одном пользователе, под нормальной нагрузкой уже может начать заметно сильнее есть память.
И вот тут как раз становится понятно, за что мы платим, уменьшая gpu-memory-utilization.
Сама модель легче не становится. Мы просто оставляем меньше памяти под KV-cache и всё остальное вокруг неё.
То есть модель в GPU вроде бы влезла — отлично. Но дальше где-то придётся расплачиваться: количеством одновременно обрабатываемых токенов и запросов, запасом под длинный контекст или всем сразу.
Никакой бесплатной памяти тут, к сожалению, не появляется. 🚫