- BrainTools - https://www.braintools.ru -

Большинство инструкций по локальному запуску LLM сводятся к одной-двум командам: вставьте их в терминал, дождитесь загрузки модели и готово. Такой подход работает ровно до первой ошибки [1]. После этого становится непонятно, на каком из нескольких уровней возникла проблема: в 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 ГБ оперативной памяти [2]
30-50 ГБ свободного места.
Отдельный вопрос – объем VRAM.
Я запускал этот стек на видеокарте с 4 ГБ видеопамяти, примерно уровня RTX 3050. Для экспериментов этого достаточно, но нужно учитывать, что VRAM расходуется не только на веса модели. За одну и ту же память конкурируют: веса, KV cache, CUDA context, runtime buffers и др. служебные структуры.
Модель на 0,6 млрд параметров на 4 ГБ запустить вполне реально.
Открываем PowerShell от имени администратора:
wsl --install
Если система попросит перезагрузиться, перезагружаемся.
После этого можно проверить состояние WSL и список доступных дистрибутивов:
wsl --status
wsl --list --online
Если Ubuntu еще не установлена:
wsl --install -d Ubuntu
Запускаем WSL:
wsl
И уже внутри Ubuntu проверяем систему:
uname -a
Находясь в Ubuntu, выполняем:
nvidia-smi
Если все настроено правильно, команда должна показать установленную видеокарту, версию драйвера и информацию об использовании памяти.
Это одна из ключевых проверок всей установки.
Если nvidia-smi не работает уже здесь, переходить к Docker нет смысла. Проблема находится ниже по стеку.
В таком случае нужно проверять:
Драйвер NVIDIA в Windows
Версию и состояние WSL
Доступность GPU внутри WSL2.
Исправив этот уровень, двигаемся дальше.
Обновим пакеты и установим базовые утилиты:
sudo apt update
sudo apt upgrade -y
sudo apt install -y
ca-certificates
curl
gnupg
lsb-release
git
wget
Для начала удалим пакеты, которые могут конфликтовать с официальной установкой 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 уже функционирует.
Чтобы не писать sudo перед каждой командой:
sudo usermod -aG docker $USER
exit
После этого заново открываем WSL и проверяем:
docker ps
Если команда выполняется без sudo, все настроено.
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
До установки vLLM нужно убедиться, что GPU действительно доступен из контейнера.
Запускаем CUDA-контейнер:
docker run --rm --gpus all
nvidia/cuda:12.8.1-base-ubuntu24.04
nvidia-smi
Если внутри контейнера появился вывод nvidia-smi, цепочка работает целиком:

На этом этапе легко перепутать два понятия: 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 не требуется.
Создадим локальный каталог кэша 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
Делает все доступные NVIDIA GPU видимыми внутри контейнера.
Явно указывает NVIDIA runtime.
В современных конфигурациях запуск может работать и без этого аргумента, но явное указание не мешает.
Контейнер использует IPC namespace хоста.
Это полезно для операций с shared memory, которые активно используют ML-фреймворки.
Пробрасывает порт контейнера на хост.
После этого API доступен по адресу:
http://localhost:8000
Подключает локальный Hugging Face cache внутрь контейнера.
Без этого при пересоздании контейнера модель пришлось бы скачивать заново.
Использует FP16 вместо FP32.
Это уменьшает расход GPU memory на веса.
Сообщает vLLM, какую долю VRAM можно использовать.
Для карты на 4 ГБ значение 0.80 соответствует примерно 3,2 ГБ.
Эта память используется не только под веса. В неё также должны поместиться KV cache и структуры inference runtime.
Ограничивает максимальную длину контекста.
Для небольшой видеокарты этот параметр особенно важен, потому что длина последовательности напрямую влияет на размер 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
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
}'
Поскольку 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.
В потоковом режиме пользователь получает текст по мере генерации:
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()
Для интерактивных приложений это обычно воспринимается заметно лучше, чем ожидание полного ответа.
После запуска модели уже интересно не только то, отвечает ли она вообще, но и насколько быстро.
Для начала можно измерить:
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 и использовать его как число сгенерированных токенов нельзя.

Время между отправкой запроса и появлением первого фрагмента ответа.
Высокий TTFT может быть связан с:
длинным prompt;
prefill;
очередью запросов;
конкуренцией за GPU;
cache miss.
Для пользователя именно TTFT определяет, насколько быстро система начинает реагировать [3].
Среднее время генерации одного выходного токена.
В упрощенном виде это время decode, делённое на количество сгенерированных токенов.
Интервал между последовательными токенами во время генерации.
Полное время от отправки запроса до получения последней части ответа. Именно эту величину в конечном счёте ощущает клиент.
Количество токенов, обрабатываемых или генерируемых за единицу времени. В production приходится учитывать сразу обе стороны: и latency, и throughput.
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 во время реальных запросов.
Упрощенно использование GPU memory можно представить так:
M_GPU ≈ M_weights + M_KV_cache + M_runtime + M_CUDA_overhead
То есть доступная видеопамять делится как минимум между:
весами модели;
KV cache;
runtime structures;
CUDA overhead.
Для FP16 на один параметр приходится примерно 2 байта.
Соответственно, модель на 1 млрд параметров требует около 2 ГБ только для хранения сырых весов – без учета всего остального.
На небольшой видеокарте этого различия нельзя игнорировать.
Во время autoregressive generation трансформеру постоянно нужны результаты вычислений для предыдущих токенов. Вместо повторного вычисления Key и Value tensors на каждом новом шаге они сохраняются в KV cache.
В первом приближении его объём растет пропорционально:
bytes per element
× layers
× KV heads
× head dim
× sequence length
× active sequences
Поэтому особенно сильно на расход VRAM влияют:
длина контекста
количество одновременно активных последовательностей.
Именно поэтому на небольшой видеокарте --max-model-len становится одним из первых параметров, которые приходится уменьшать.

При статическом batching сначала формируется batch запросов, а затем он обрабатывается как единое целое. Для LLM это не всегда эффективно: разные запросы генерируют ответы разной длины и заканчиваются в разное время. Если один запрос уже завершился, а другой продолжает генерировать, часть ресурсов может простаивать.
Continuous batching позволяет динамически добавлять и удалять sequences во время работы. Освободившееся место можно сразу занять новым запросом, не дожидаясь завершения всей исходной группы.
Это повышает загрузку GPU и throughput.
Представим несколько запросов с одинаковым началом:
system prompt
+ company policy
+ пользовательский вопрос №1
и:
system prompt
+ company policy
+ пользовательский вопрос №2
Значительная часть входа совпадает. Если вычисления для этого префикса уже выполнены, их можно переиспользовать вместо повторного prefill, что уменьшает объем лишней работы.
Очень длинный prompt может надолго занять GPU на стадии prefill. Chunked prefill делит его обработку на части, чтобы между ними движок мог выполнять decode других активных запросов.
Так уменьшается ситуация, когда один большой prompt блокирует остальные sequences.
Для понимания производительности inference полезно разделять две основные фазы.
На этом этапе обрабатывается входной prompt и формируется начальный KV cache. Prefill сильно связан с TTFT. Чем больше вход, тем больше вычислений нужно выполнить до появления первого выходного токена.
После prefill модель генерирует ответ токен за токеном.
На этой стадии важную роль играют:
пропускная способность памяти;
размер KV cache;
количество активных sequences.
Если TTFT плохой, проблема может находиться в prefill. Если ответ начинает появляться быстро, но дальше генерируется медленно, смотреть нужно уже в сторону decode.
Самый полезный принцип в такой инфраструктуре – не пытаться чинить все сразу.
Идём снизу вверх:

Если один уровень не работает, сначала исправляем его и только потом переходим выше.
Не нужно диагностировать Docker.
Сначала проверяем:
драйвер NVIDIA в Windows
версию WSL
доступность GPU из Ubuntu.
Повторно запускаем тестовый CUDA-контейнер.
Проверяем наличие NVIDIA Container Toolkit:
nvidia-container-toolkit
При необходимости снова выполняем:
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
Можно начать с уменьшения:
--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
Один контейнер vLLM на домашнем компьютере – это лабораторная установка.
В production вокруг самого inference-server обычно появляется дополнительная инфраструктура:

Сам vLLM при этом остается ядром inference. Основные изменения происходят вокруг него.
Пока у нас один контейнер, 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-релизов, я делаю это за вас: тестирую новые модели и обновления и публикую в ДругОпенсурса [4]только то, что действительно стоит внимания [5]. Там же короткие выводы из тестов и мои наблюдения о том, что реально полезно в работе.
Автор: Qwertcoser
Источник [6]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36585
URLs in this post:
[1] ошибки: http://www.braintools.ru/article/4192
[2] памяти: http://www.braintools.ru/article/4140
[3] реагировать: http://www.braintools.ru/article/1549
[4] ДругОпенсурса : https://t.me/tch_net
[5] внимания: http://www.braintools.ru/article/7595
[6] Источник: https://habr.com/ru/articles/1091060/?utm_campaign=1091060&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.