The Jerusalem PostA dangerous habit: Legislating hatred one step at a time - opinionESPN'No way this guy's a backup': How Malik Willis prepared for his second chanceRTP DesportoMundial de Hóquei. Seleção feminina bate ColômbiaESPN DeportesVinícius, Brahim y otros convocados ya se entrenan con Real MadridBollywood HungamaNeil Bhoopalam wants to work with Rajkumar Hirani to explore family-oriented films: “It would be a good shift for me”Daily MaverickSOCCER INTEGRITY: Senegal vs Morocco Afcon final saga set to be heard in Swiss courtPunch2027: INEC lists states with most registered votersZDF heuteEntdecken Sie das ZDF-NachrichtenstudioSCMP ChinaUS firm to make battle-tested drones in Taiwan, boosting supply chain resilienceBillboardBillboard Latin Music Week 2026 Adds Rubén Blades, Anthony Ramos, Lenny Tavárez, Tito Nieves & More to LineupThe Hollywood ReporterEmmy Awards Enter Streaming Era: Prime Video, TV Academy Ink Six-Year DealThe RegisterZombie instructions on carefully constructed web pages could trick GitHub Copilot CLI into sharing secrets
The Daily Newsstand · Free, Always
Tuesday, October 6, 2026

[Перевод] Запускаем LLM локально на Windows: WSL2, Docker, CUDA и vLLM

Translate

Большинство инструкций по локальному запуску 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.

Что понадобится

Для примера используется следующая конфигурация:

  1. Windows 11 с WSL2

  2. NVIDIA GPU с поддержкой CUDA и драйвером, совместимым с WSL2

  3. желательно от 16 ГБ оперативной памяти

  4. 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 нет смысла. Проблема находится ниже по стеку.

В таком случае нужно проверять:

  1. Драйвер NVIDIA в Windows

  2. Версию и состояние WSL

  3. Доступность 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-релизов, я делаю это за вас: тестирую новые модели и обновления и публикую в ДругОпенсурса только то, что действительно стоит внимания. Там же короткие выводы из тестов и мои наблюдения о том, что реально полезно в работе.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.