Qwertcoser1 час назадОбъяснить с
Запускаем LLM локально на Windows: WSL2, Docker, CUDA и vLLM
Уровень сложностиСреднийВремя на прочтение11 минОхват и читатели2.2KМашинное обучение*DevOps*GPGPU*Linux*IT-инфраструктура*ТуториалПереводАвтор оригинала: Harshit KumarБольшинство инструкций по локальному запуску LLM сводятся к одной-двум командам: вставьте их в терминал, дождитесь загрузки модели и готово. Такой подход работает ровно до первой ошибки. После этого становится непонятно, на каком из нескольких уровней возникла проблема: в Windows, WSL2, драйвере NVIDIA, Docker, CUDA или самом inference-сервере.
В этой статье развернем Qwen3-0.6B на обычной NVIDIA-видеокарте под Windows 11 с использованием WSL2, Docker, NVIDIA Container Toolkit и vLLM.
Задача не только в том, чтобы получить ответы от модели. Гораздо полезнее разобраться, как устроен весь стек, чтобы потом можно было диагностировать ошибки, менять модели и постепенно двигаться к инфраструктуре, похожей на production.
Как устроен стек
В нашем случае цепочка выглядит примерно так:
Каждый уровень зависит прежде всего от того, что находится непосредственно под ним. Благодаря этому инфраструктуру удобно проверять снизу вверх.
Сама модель напрямую с Windows не взаимодействует. Docker создает изолированное окружение, а NVIDIA Container Toolkit предоставляет контейнеру доступ к GPU.
Что понадобится
Для примера используется следующая конфигурация:
• Windows 11 с WSL2
• NVIDIA GPU с поддержкой CUDA и драйвером, совместимым с WSL2
• желательно от 16 ГБ оперативной памяти
• 30-50 ГБ свободного места.Отдельный вопрос - объем VRAM.
Я запускал этот стек на видеокарте с 4 ГБ видеопамяти, примерно уровня RTX 3050. Для экспериментов этого достаточно, но нужно учитывать, что VRAM расходуется не только на веса модели. За одну и ту же память конкурируют: веса, KV cache, CUDA context, runtime buffers и др. служебные структуры.
Модель на 0,6 млрд параметров на 4 ГБ запустить вполне реально.
Шаг 1. Устанавливаем WSL2 и Ubuntu
Открываем PowerShell от имени администратора:
wsl --installЕсли система попросит перезагрузиться, перезагружаемся.
После этого можно проверить состояние WSL и список доступных дистрибутивов:
wsl --status wsl --list --onlineЕсли Ubuntu еще не установлена:
wsl --install -d UbuntuЗапускаем WSL:
wslИ уже внутри Ubuntu проверяем систему:
uname -a
Шаг 2. Проверяем GPU внутри WSL
Находясь в Ubuntu, выполняем:
nvidia-smiЕсли все настроено правильно, команда должна показать установленную видеокарту, версию драйвера и информацию об использовании памяти.
Это одна из ключевых проверок всей установки.
Если nvidia-smi не работает уже здесь, переходить к Docker нет смысла. Проблема находится ниже по стеку.
В таком случае нужно проверять:
• Драйвер NVIDIA в Windows
• Версию и состояние WSL
• Доступность GPU внутри WSL2.Исправив этот уровень, двигаемся дальше.
Шаг 3. Обновляем Ubuntu
Обновим пакеты и установим базовые утилиты:
sudo apt update sudo apt upgrade -y
sudo apt install -y \
ca-certificates \
curl \
gnupg \
lsb-release \
git \
wget
Шаг 4. Устанавливаем Docker Engine
Для начала удалим пакеты, которые могут конфликтовать с официальной установкой Docker:
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
sudo apt-get remove -y $pkg
doneСоздаем каталог для ключей:
sudo install -m 0755 -d /etc/apt/keyringsДобавляем официальный GPG-ключ Docker:
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.ascТеперь подключаем официальный репозиторий:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullОбновляем список пакетов и устанавливаем Docker:
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-pluginПроверяем версию:
docker --versionИ запускаем тестовый контейнер:
sudo docker run hello-worldЕсли он отработал успешно, базовый Docker уже функционирует.
Docker без sudo
Чтобы не писать sudo перед каждой командой:
sudo usermod -aG docker $USER exitПосле этого заново открываем WSL и проверяем:
docker psЕсли команда выполняется без sudo, все настроено.
Шаг 5. Устанавливаем NVIDIA Container Toolkit
Docker сам по себе не дает контейнерам доступ к GPU. Эту связку обеспечивает NVIDIA Container Toolkit.
Добавляем ключ:
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor \
-o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpgДобавляем репозиторий:
curl -s -L \
https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.listУстанавливаем пакет:
sudo apt update
sudo apt install -y nvidia-container-toolkitТеперь настраиваем Docker runtime:
sudo nvidia-ctk runtime configure --runtime=dockerИ перезапускаем Docker:
sudo systemctl restart docker
Шаг 6. Самая важная проверка
До установки vLLM нужно убедиться, что GPU действительно доступен из контейнера.
Запускаем CUDA-контейнер:
docker run --rm --gpus all \
nvidia/cuda:12.8.1-base-ubuntu24.04 \
nvidia-smiЕсли внутри контейнера появился вывод nvidia-smi, цепочка работает целиком:
Docker image и кэш модели - разные вещи
На этом этапе легко перепутать два понятия: Docker image и файлы модели.
Docker image:
vllm/vllm-openai:latestсодержит программное окружение:
• Python;
• PyTorch;
• CUDA-библиотеки;
• vLLM;Поэтому образ занимает несколько гигабайт.
Отдельно существует кэш модели, в котором хранятся: веса, tokenizer и конфигурация модели:
~/.cache/huggingfaceИх удобно разделять.
Docker image содержит программную среду, а volume с кэшем - сами модели. Благодаря этому одну и ту же модель не приходится скачивать заново после пересоздания контейнера.
Загружаем официальный образ vLLM:
docker pull vllm/vllm-openai:latestУ официального образа уже настроен vllm serve как entrypoint.
Это означает, что всё, что мы указываем после имени image, передаётся как аргументы vllm serve. Повторно писать саму команду vllm serve не требуется.
Запускаем Qwen3-0.6B
Создадим локальный каталог кэша Hugging Face:
mkdir -p ~/.cache/huggingfaceЗапускаем контейнер:
docker run -d \
--gpus all \
--runtime nvidia \
--ipc=host \
-p 8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--name vllm-qwen \
vllm/vllm-openai:latest \
--model Qwen/Qwen3-0.6B \
--dtype half \
--gpu-memory-utilization 0.80 \
--max-model-len 2048Пока модель загружается, можно наблюдать за логами:
docker logs -f vllm-qwen
Что означают параметры запуска
--gpus all
Делает все доступные NVIDIA GPU видимыми внутри контейнера.
--runtime nvidia
Явно указывает NVIDIA runtime.
В современных конфигурациях запуск может работать и без этого аргумента, но явное указание не мешает.
--ipc=host
Контейнер использует IPC namespace хоста.
Это полезно для операций с shared memory, которые активно используют ML-фреймворки.
-p 8000:8000
Пробрасывает порт контейнера на хост.
После этого API доступен по адресу:
http://localhost:8000
-v ~/.cache/huggingface:/root/.cache/huggingface
Подключает локальный Hugging Face cache внутрь контейнера.
Без этого при пересоздании контейнера модель пришлось бы скачивать заново.
--dtype half
Использует FP16 вместо FP32.
Это уменьшает расход GPU memory на веса.
--gpu-memory-utilization 0.80
Сообщает vLLM, какую долю VRAM можно использовать.
Для карты на 4 ГБ значение 0.80 соответствует примерно 3,2 ГБ.
Эта память используется не только под веса. В неё также должны поместиться KV cache и структуры inference runtime.
--max-model-len 2048
Ограничивает максимальную длину контекста.
Для небольшой видеокарты этот параметр особенно важен, потому что длина последовательности напрямую влияет на размер KV cache.
Управляем контейнером
Посмотреть запущенные контейнеры:
docker psВсе контейнеры:
docker ps -aПосмотреть логи:
docker logs vllm-qwenОстановить:
docker stop vllm-qwenЗапустить снова:
docker start vllm-qwenУдалить контейнер:
docker rm vllm-qwen
Отправляем запросы модели
После запуска у нас есть OpenAI-compatible HTTP API.
Проверяем сервер
Health endpoint:
curl http://localhost:8000/healthСписок моделей:
curl http://localhost:8000/v1/models
Chat completion через curl
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer dummy" \
-d '{
"model": "Qwen/Qwen3-0.6B",
"messages": [
{
"role": "user",
"content": "Explain continuous batching."
}
],
"max_tokens": 200
}'
Работа через OpenAI Python SDK
Поскольку API совместим с OpenAI, можно использовать стандартный SDK.
Нужно только указать локальный base_url:
from openai import OpenAIclient = OpenAI( base_url="http://localhost:8000/v1", api_key="dummy",)response = client.chat.completions.create( model="Qwen/Qwen3-0.6B", messages=[ { "role": "user", "content": "Explain continuous batching in vLLM." } ], max_tokens=200,)print(response.choices[0].message.content)Настоящий API key локальному vLLM-серверу в такой конфигурации не требуется, поэтому используется условное значение dummy.
Streaming
В потоковом режиме пользователь получает текст по мере генерации:
stream = client.chat.completions.create( model="Qwen/Qwen3-0.6B", messages=[ { "role": "user", "content": "Explain KV cache in detail." } ], max_tokens=300, stream=True,)for chunk in stream: if chunk.choices: text = chunk.choices[0].delta.content if text: print(text, end="", flush=True)print()Для интерактивных приложений это обычно воспринимается заметно лучше, чем ожидание полного ответа.
Измеряем TTFT и общую задержку
После запуска модели уже интересно не только то, отвечает ли она вообще, но и насколько быстро.
Для начала можно измерить:
• Time To First Token; • полную end-to-end latency.Простой пример:
import timefrom openai import OpenAIclient = OpenAI( base_url="http://localhost:8000/v1", api_key="dummy",)start = time.perf_counter()stream = client.chat.completions.create( model="Qwen/Qwen3-0.6B", messages=[ { "role": "user", "content": "Explain KV cache." } ], max_tokens=200, stream=True,)first_token_time = Nonechunks = []for chunk in stream: if not chunk.choices: continue text = chunk.choices[0].delta.content if text: if first_token_time is None: first_token_time = time.perf_counter() chunks.append(text)end = time.perf_counter()ttft = first_token_time - start if first_token_time else Nonee2e = end - startprint("TTFT:", ttft)print("E2E:", e2e)print("".join(chunks))Здесь есть важное ограничение: один streaming chunk не обязательно соответствует одному токену, поэтому считать количество chunks и использовать его как число сгенерированных токенов нельзя.
Основные метрики inference
TTFT - Time To First Token
Время между отправкой запроса и появлением первого фрагмента ответа.
Высокий TTFT может быть связан с:
• длинным prompt;
• prefill;
• очередью запросов;
• конкуренцией за GPU;
• cache miss.Для пользователя именно TTFT определяет, насколько быстро система начинает реагировать.
TPOT - Time Per Output Token
Среднее время генерации одного выходного токена.
В упрощенном виде это время decode, делённое на количество сгенерированных токенов.
ITL - Inter-Token Latency
Интервал между последовательными токенами во время генерации.
End-to-end latency
Полное время от отправки запроса до получения последней части ответа. Именно эту величину в конечном счёте ощущает клиент.
Throughput
Количество токенов, обрабатываемых или генерируемых за единицу времени. В production приходится учитывать сразу обе стороны: и latency, и throughput.
Метрики vLLM
vLLM публикует Prometheus-compatible metrics.
Посмотреть часть из них можно так:
curl -s http://localhost:8000/metrics | \
grep -Ei "request|queue|cache|token|generation|prompt"Одновременно удобно наблюдать за GPU:
watch -n 1 'docker exec vllm-qwen nvidia-smi'Так становится видно, что происходит с VRAM и загрузкой GPU во время реальных запросов.
Куда исчезает VRAM
Упрощенно использование GPU memory можно представить так:
M_GPU ≈ M_weights + M_KV_cache + M_runtime + M_CUDA_overheadТо есть доступная видеопамять делится как минимум между:
• весами модели;
• KV cache;
• runtime structures;
• CUDA overhead.Для FP16 на один параметр приходится примерно 2 байта.
Соответственно, модель на 1 млрд параметров требует около 2 ГБ только для хранения сырых весов - без учета всего остального.
На небольшой видеокарте этого различия нельзя игнорировать.
Почему KV cache так быстро съедает память
Во время autoregressive generation трансформеру постоянно нужны результаты вычислений для предыдущих токенов. Вместо повторного вычисления Key и Value tensors на каждом новом шаге они сохраняются в KV cache.
В первом приближении его объём растет пропорционально:
bytes per element
× layers
× KV heads
× head dim
× sequence length
× active sequencesПоэтому особенно сильно на расход VRAM влияют:
• длина контекста
• количество одновременно активных последовательностей.Именно поэтому на небольшой видеокарте --max-model-len становится одним из первых параметров, которые приходится уменьшать.
Три механизма, за счет которых vLLM работает быстрее
Continuous batching
При статическом batching сначала формируется batch запросов, а затем он обрабатывается как единое целое. Для LLM это не всегда эффективно: разные запросы генерируют ответы разной длины и заканчиваются в разное время. Если один запрос уже завершился, а другой продолжает генерировать, часть ресурсов может простаивать.
Continuous batching позволяет динамически добавлять и удалять sequences во время работы. Освободившееся место можно сразу занять новым запросом, не дожидаясь завершения всей исходной группы.
Это повышает загрузку GPU и throughput.
Prefix caching
Представим несколько запросов с одинаковым началом:
system prompt + company policy + пользовательский вопрос №1и:
system prompt
+ company policy
+ пользовательский вопрос №2Значительная часть входа совпадает. Если вычисления для этого префикса уже выполнены, их можно переиспользовать вместо повторного prefill, что уменьшает объем лишней работы.
Chunked prefill
Очень длинный prompt может надолго занять GPU на стадии prefill. Chunked prefill делит его обработку на части, чтобы между ними движок мог выполнять decode других активных запросов.
Так уменьшается ситуация, когда один большой prompt блокирует остальные sequences.
Prefill и decode
Для понимания производительности inference полезно разделять две основные фазы.
Prefill
На этом этапе обрабатывается входной prompt и формируется начальный KV cache. Prefill сильно связан с TTFT. Чем больше вход, тем больше вычислений нужно выполнить до появления первого выходного токена.
Decode
После prefill модель генерирует ответ токен за токеном.
На этой стадии важную роль играют:
• пропускная способность памяти;
• размер KV cache;
• количество активных sequences.Если TTFT плохой, проблема может находиться в prefill. Если ответ начинает появляться быстро, но дальше генерируется медленно, смотреть нужно уже в сторону decode.
Диагностика: проверяем каждый слой отдельно
Самый полезный принцип в такой инфраструктуре - не пытаться чинить все сразу.
Идём снизу вверх:
Если один уровень не работает, сначала исправляем его и только потом переходим выше.
nvidia-smi не работает в WSL
Не нужно диагностировать Docker.
Сначала проверяем:
• драйвер NVIDIA в Windows
• версию WSL
• доступность GPU из Ubuntu.
Docker не видит GPU
Повторно запускаем тестовый CUDA-контейнер.
Проверяем наличие NVIDIA Container Toolkit:
nvidia-container-toolkitПри необходимости снова выполняем:
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
vLLM не хватает VRAM
Можно начать с уменьшения:
--gpu-memory-utilization 0.70Также попробовать сократить context length:
--max-model-len 1024Если и этого недостаточно, понадобится меньшая модель.
Контейнер сразу завершается
Проверяем все контейнеры:
docker ps -aЗатем смотрим логи:
docker logs vllm-qwen
Модель скачивается при каждом создании контейнера
Скорее всего, не подключен volume с Hugging Face cache:
-v ~/.cache/huggingface:/root/.cache/huggingface
От локального эксперимента к production
Один контейнер vLLM на домашнем компьютере - это лабораторная установка.
В production вокруг самого inference-server обычно появляется дополнительная инфраструктура:
Сам vLLM при этом остается ядром inference. Основные изменения происходят вокруг него.
Когда имеет смысл Docker Compose
Пока у нас один контейнер, docker run вполне достаточно.
Compose становится полезен, когда рядом появляются:
• Prometheus;
• Grafana;
• Redis;
• gateway;
• приложение;
• дополнительные сервисы.Для vLLM конфигурация может выглядеть так:
services: vllm: image: vllm/vllm-openai:latest
ports: - "8000:8000"
volumes: - ~/.cache/huggingface:/root/.cache/huggingface
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
command:
- --model
- Qwen/Qwen3-0.6B
- --dtype
- half
- --gpu-memory-utilization
- "0.80"
- --max-model-len
- "2048"Отдельно стоит быть осторожным с:
docker system pruneКоманда удаляет неиспользуемые: сети, изображения, контейнеры и build cacheПеред запуском лучше понимать, какие данные Docker считает неиспользуемыми.
Итог
В результате получился полноценный локальный inference stack:
Windows
+ WSL2
+ Docker
+ NVIDIA Container Toolkit
+ CUDA
+ vLLM
+ Qwen3-0.6B
=
локальный OpenAI-compatible LLM APIСама Qwen3-0.6B небольшая, но инфраструктурные принципы здесь те же, что и в более крупных системах.
Уже на видеокарте с 4 ГБ VRAM можно разобраться на практике с:
• управлением GPU memory;
• KV cache
• prefill и decode
• batching
• latency
• throughput
• метриками
• контейнеризацией inference.После этого переход к нескольким GPU, Kubernetes и распределенному inference становится гораздо понятнее: меняется масштаб, но не базовые идеи.
Чтобы не тратить время на ежедневный мониторинг десятков AI-релизов, я делаю это за вас: тестирую новые модели и обновления и публикую в ДругОпенсурса только то, что действительно стоит внимания. Там же короткие выводы из тестов и мои наблюдения о том, что реально полезно в работе.Теги:• vLLM
• qwen3
• Docker
• CUDA
• wsl2
• локальные llm
• GPU inference
• openai-compatible apiХабы:• Машинное обучение
• DevOps
• GPGPU
• Linux
• IT-инфраструктура
Получайте больше инсайтов о систематизации бизнеса
Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности
💬 Подписаться на канал→ Оригинальная статья