Перейти к содержимому
Ступень 77.2 Квантование и локальный запуск

Запускаем Qwen/DeepSeek локально

Как выбрать модель под свою память, запустить её через Ollama или llama.cpp, поднять OpenAI-совместимый сервер и честно измерить токены в секунду на своём компьютере.

УРОКstage-7/7-2-quantization/02-local-llm.mdx2 чУрок 2 из 2
После урока вы сможете
  • Подобрать модель и квантование под объём RAM и VRAM с учётом KV-кэша и оценить скорость ещё до скачивания
  • Запустить Qwen3 через Ollama и llama.cpp, в том числе с частичной выгрузкой слоёв и экспертов MoE в оперативную память
  • Подключиться к локальному OpenAI-совместимому серверу из Python и измерить TTFT, скорость prefill и decode

Локальная модельлокальная LLMEN local LLMМодель, которая работает на вашем компьютере без облака — бесплатно, офлайн и без передачи данных третьим лицам. работает бесплатно, без интернета и без отправки ваших данных кому-либо: код, документы, переписку можно давать ей без опаски. Её не ограничивают тарифы, лимиты и доступность облачных API в вашей стране. И она открыта для экспериментов: можно смотреть логиты, менять сэмплирование (урок 4.1.3), подключать свой LoRA-адаптер из урока 6.2.2. Весь код ступени 8 (RAG, агенты) тоже работает с локальной моделью.

В этом уроке вы соберёте всё изученное в модуле в практический навык:

  1. по формулам из уроков 7.1.1 и 7.2.1 выберете модель и квантование под своё железо;
  2. запустите Qwen3 через Ollama — за пять минут;
  3. запустите её же через llama.cpp — с полным контролем над памятью и GPU;
  4. подключитесь к OpenAI-совместимому APIOpenAI-совместимый APIEN OpenAI-compatible APIHTTP-интерфейс в формате OpenAI Chat Completions (/v1/chat/completions). Его отдают Ollama, llama-server, vLLM, SGLang, а также DeepSeek и Qwen в облаке — код клиента не меняется. и измерите скорость своим бенчмарком.
ИнструментЧто этоДля чего
Ollamaсервер с каталогом моделей, построенный на ggml/llama.cppбыстрый старт, ступень 8 курса
llama.cppдвижок на C/C++: CPU, CUDA, Metal, Vulkan; формат GGUFGGUF (формат файлов llama.cpp)EN GGUFОдин файл с весами (обычно квантованными), токенизатором, шаблоном чата и метаданными. Типы квантования — Q8_0, Q6_K, Q4_K_M и др. Используют llama.cpp, Ollama, LM Studio.полный контроль, замеры, выгрузка на CPU
LM Studioграфическое приложение (llama.cpp, на Mac ещё MLX)если не хочется терминала
vLLM, SGLangсерверы для GPU (урок 7.1.2)много пользователей, ваши 4 карты
LMDeployсервер Shanghai AI LabQwen и InternLM в 4 битах на GPU
KTransformersгибрид CPU и GPU для огромных MoEDeepSeek-V3/R1 на одной GPU и большой RAM

CUDA — интерфейс для видеокарт NVIDIA, Metal — для Mac на чипах Apple, Vulkan — универсальный интерфейс, через который работают и карты AMD и Intel; MLX — библиотека Apple для её собственных чипов.

Рекомендация: начните с Ollama, для экспериментов и замеров перейдите на llama.cpp, а для сервера на всю семью или команду — на vLLM или SGLang.

Модели нужно уместить три вещи:

память≈N⋅бит на вес8⏟веса+KV-кэш на токен⋅контекст⏟урок 5.2.1+0.5–1.5 ГБ⏟буферы.\begin{aligned} \text{память} \approx{} & \underbrace{N \cdot \frac{\text{бит на вес}}{8}}_{\text{веса}} + \underbrace{\text{KV-кэш на токен} \cdot \text{контекст}}_{\text{урок 5.2.1}} \\ & + \underbrace{0.5\text{–}1.5 \text{ ГБ}}_{\text{буферы}}. \end{aligned}

Пример: Qwen3-8B (8.2 млрд параметров; 36 слоёв, 8 KV-голов размера 128). Веса в Q4_K_M: 8.2⋅4.9/8≈58.2 \cdot 4.9 / 8 \approx 5 ГБ. Кэш на токен: 2⋅36⋅8⋅128⋅2=147 4562 \cdot 36 \cdot 8 \cdot 128 \cdot 2 = 147\,456 байт ≈ 144 КиБ. На 8 тысячах токенов контекста — 1.2 ГБ, на 32 тысячах — 4.8 ГБ. Итого 7–11 ГБ в зависимости от контекста.

Где всё это лежит, решает скорость. Из урока 7.1.1: decode читает все веса на каждый токен, так что скорость ≈ пропускная способность памяти / байт весов. Если модель не влезла в видеопамять, часть слоёв остаётся в оперативной памяти и считается на CPU — это выгрузка слоёвраспределение слоёв между GPU и CPU (offload)EN layer offloadingЧасть слоёв модели лежит в видеопамяти, остальные — в оперативной памяти. В llama.cpp регулируется флагом -ngl. Время токена складывается из чтения обеих частей, и медленная оперативная память быстро становится главным слагаемым.. Время токена тогда складывается из двух частей:

t≈bGPUWGPU+bCPUWCPU.t \approx \frac{b_{\text{GPU}}}{W_{\text{GPU}}} + \frac{b_{\text{CPU}}}{W_{\text{CPU}}}.

Видеопамять в 5–20 раз быстрее оперативной, поэтому даже небольшая доля слоёв на CPU становится главным слагаемым. Qwen3-32B в Q4_K_M (20 ГБ) на RTX 3060 с 12 ГБ: если бы влезла целиком, дала бы около 12 токенов/с, а с половиной слоёв в DDR4 — около 3. Маленькая модель целиком в видеопамяти обычно лучше большой, выгруженной наполовину. Исключение — MoE, о нём ниже.

Соберём это в планировщик: какая модель влезет и с какой скоростью пойдёт. Код на чистом Python:

# Какая модель поместится и с какой скоростью пойдёт: планировщик «на салфетке»
BPW = {"Q8_0": 8.5, "Q6_K": 6.6, "Q5_K_M": 5.7, "Q4_K_M": 4.9, "Q3_K_M": 3.9} # ≈ бит на вес в GGUF
# имя: (всего параметров, млрд; активных, млрд; слоёв; KV-голов; размер головы)
MODELS = {
"Qwen3-0.6B": (0.6, 0.6, 28, 8, 128),
"Qwen3-8B": (8.2, 8.2, 36, 8, 128),
"Qwen3-14B": (14.8, 14.8, 40, 8, 128),
"Qwen3-30B-A3B": (30.5, 3.3, 48, 4, 128),
"Qwen3-32B": (32.8, 32.8, 64, 8, 128),
"Llama-3.3-70B": (70.6, 70.6, 80, 8, 128),
}
# видеопамять, ГБ; её пропускная способность, ГБ/с; свободная RAM, ГБ; пропускная способность RAM, ГБ/с
MACHINES = {
"ноутбук 16 ГБ": (0, 1, 12, 89.6), # DDR5, 2 канала; ~4 ГБ съедает система
"RTX 3060 12 ГБ": (12, 360, 24, 51.2), # DDR4, 2 канала
"4× RTX 4080 32 ГБ": (128, 717, 112, 204.8), # слои по картам: в один поток работает одна карта за раз
}
def memory_gb(model, quant, context=8192, overhead_gb=1.0):
"""Веса + KV-кэш (FP16) на context токенов + буферы движка."""
total_b, _, layers, kv_heads, head_dim = MODELS[model]
weights = total_b * BPW[quant] / 8
kv = 2 * layers * kv_heads * head_dim * context * 2 / 1e9
return weights + kv + overhead_gb
def tokens_per_sec(model, quant, machine, efficiency=0.7):
"""Скорость decode в один поток. None — модель не помещается в память совсем."""
vram, vram_bw, ram, ram_bw = MACHINES[machine]
need = memory_gb(model, quant)
if need > vram + ram:
return None
on_gpu = min(need, vram) / need # доля модели в видеопамяти
total_b, active_b, *_ = MODELS[model] # *_ — «остальные поля кортежа нам здесь не нужны»
read_gb = active_b * BPW[quant] / 8 # за токен читаем только активные веса (важно для MoE)
seconds = read_gb * on_gpu / vram_bw + read_gb * (1 - on_gpu) / ram_bw
return efficiency / seconds
print(f"{'модель':<14}{'квант':<8}{'ГБ':>6}" + "".join(f"{m:>20}" for m in MACHINES))
for model, quant in [("Qwen3-0.6B", "Q8_0"), ("Qwen3-8B", "Q4_K_M"), ("Qwen3-14B", "Q4_K_M"),
("Qwen3-30B-A3B", "Q4_K_M"), ("Qwen3-32B", "Q4_K_M"), ("Llama-3.3-70B", "Q4_K_M")]:
row = f"{model:<14}{quant:<8}{memory_gb(model, quant):>6.1f}"
for machine in MACHINES:
speed = tokens_per_sec(model, quant, machine)
row += f"{'не влезет':>20}" if speed is None else f"{speed:>14.0f} ток/с"
print(row)
модель квант ГБ ноутбук 16 ГБ RTX 3060 12 ГБ 4× RTX 4080 32 ГБ
Qwen3-0.6B Q8_0 2.6 98 ток/с 395 ток/с 787 ток/с
Qwen3-8B Q4_K_M 7.2 12 ток/с 50 ток/с 100 ток/с
Qwen3-14B Q4_K_M 11.4 7 ток/с 28 ток/с 55 ток/с
Qwen3-30B-A3B Q4_K_M 20.5 не влезет 36 ток/с 248 ток/с
Qwen3-32B Q4_K_M 23.2 не влезет 3 ток/с 25 ток/с
Llama-3.3-70B Q4_K_M 46.9 не влезет не влезет 12 ток/с

Это оценки сверху с коэффициентом 0.7. Для крошечных моделей они особенно оптимистичны: там скорость упирается уже не в память, а в накладные расходы на каждый шаг. Добавьте в MACHINES своё железо — пропускную способность памяти найдёте в спецификации видеокарты или посчитаете для RAM: каналы × 8 байт × частота в МТ/с (мегатрансферах в секунду — это число стоит в названии памяти: DDR5-5600 значит 5600 МТ/с). Проверка для ноутбука из таблицы: 2⋅8⋅5600=89 6002 \cdot 8 \cdot 5600 = 89\,600 МБ/с, то есть 89.6 ГБ/с.

MoE — лучший друг локального запуска. Посмотрите на Qwen3-30B-A3B: памяти ей нужно как 30B-модели, а скорость — как у 3B, потому что на токен работают только 3.3 млрд активных параметровактивные параметрыEN activated (active) parametersСколько параметров MoE-модели реально участвует в обработке одного токена. «A22B» в названии Qwen3-235B-A22B — 22 млрд активных из 235 млрд. (этот расчёт вы уже видели в уроке 7.1.1). Даже на RTX 3060, где половина модели лежит в RAM, оценка — 36 токенов/с против 3 у плотной Qwen3-32B. Больше того, можно выгружать в RAM именно экспертов, а внимание и общие слои держать на GPU — так и работают KTransformers и флаг -ot в llama.cpp (ниже).

Ориентиры по железу (контекст 8–16 тысяч токенов, Q4_K_M, если не сказано иное):

ЖелезоКомфортноНа пределе
Ноутбук 8–16 ГБ RAM без GPUQwen3-0.6B … 4BQwen3-8B, около 10 токенов/с
GPU 8 ГБQwen3-4B; Qwen3-8B с контекстом до ~8KQwen3-30B-A3B с экспертами в RAM
GPU 12–16 ГБQwen3-14BQwen3-30B-A3B с частью экспертов в RAM
GPU 24 ГБQwen3-30B-A3B, Qwen3-14B в Q8_0Qwen3-32B с контекстом до 4–8K
Ваша машина, 4 × 32 ГБ70B в Q4–Q8; Qwen3-32B и 30B-A3B в BF16 через vLLMQwen3-235B-A22B в Q3 (≈115 ГБ) или Q4 (≈144 ГБ, часть в RAM); DeepSeek-V3/R1 — только версии около 2 бит и ниже, с частью весов в RAM

На октябрь 2026 в тех же классах есть модели новее Qwen3 (апрель 2025): Qwen3.6-35B-A3B вместо Qwen3-30B-A3B (ollama run qwen3.6:35b, около 24 ГБ в Q4_K_M), плотные Qwen3.6-27B и Qwen3.8-27B вместо Qwen3-32B (qwen3.6:27b, около 19 ГБ), а также Gemma 4 от Google (gemma4). Команды урока те же, меняется имя модели; llama.cpp серию Qwen3.5 и новее поддерживает (README QwenLM/Qwen3.8). Одна поправка к расчёту: внимание у этих Qwen гибридное, и KV-кэш растёт только у каждого четвёртого слоя (у Qwen3.8-27B — у 16 из 64), поэтому формула кэша выше для них даёт оценку с запасом.

  1. Установите. На Linux:

    Окно терминала
    curl -fsSL https://ollama.com/install.sh | sh

    Для Windows и macOS скачайте установщик с ollama.com. После установки в фоне работает сервер на порту 11434.

  2. Запустите модель. Первый запуск скачает около 5 ГБ:

    Окно терминала
    ollama run qwen3:8b --verbose

    Откроется чат в терминале. Флаг --verbose после каждого ответа печатает статистику: prompt eval rate — скорость prefill, eval rate — скорость decode в токенах в секунду. Сравните её с оценкой планировщика. Qwen3 по умолчанию сначала «думает». Чтобы получить быстрый ответ без рассуждений, допишите в конец сообщения /no_think — это штатный переключатель моделей Qwen3.

  3. Проверьте, где модель. В другом терминале:

    Окно терминала
    ollama ps # какие модели загружены и сколько процентов на GPU / CPU
    ollama list # что скачано

    Строка 100% GPU — всё в видеопамяти. Что-то вроде 45%/55% CPU/GPU — часть слоёв выгружена, ждите падения скорости.

  4. Настройте контекст. В чате: /set parameter num_ctx 16384. Для сервера — параметр num_ctx в запросе или в Modelfile (текстовом файле настроек модели в Ollama). Переменные окружения сервера: OLLAMA_NUM_PARALLEL (сколько запросов обслуживать параллельно), OLLAMA_FLASH_ATTENTION=1 и OLLAMA_KV_CACHE_TYPE=q8_0 (KV-кэш в 8 битах — вдвое меньше памяти; работает только вместе с Flash Attention).

Ollama удобна, но скрывает настройки. llama.cpp даёт их все. Соберите его (нужны git, cmake и компилятор C++; для GPU — CUDA Toolkit):

Окно терминала
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DGGML_CUDA=ON # без видеокарты NVIDIA: просто cmake -B build
cmake --build build --config Release -j

Запустите сервер, скачав GGUF прямо с Hugging Face (официальный репозиторий Qwen, квантование Q4_K_M):

Окно терминала
./build/bin/llama-server -hf Qwen/Qwen3-8B-GGUF:Q4_K_M -ngl 99 -c 16384 --port 8080

Откройте в браузере http://localhost:8080 — там встроенный веб-чат. Главные флаги:

ФлагЧто делает
-m файл.gguf / -hf репозиторий:квантлокальный файл или скачивание с Hugging Face
-ngl Nсколько слоёв положить в видеопамять; 99 — все
-c Nдлина контекста (размер KV-кэша)
-np Nчисло параллельных слотов (пользователей); контекст делится между ними
-fa onFlash Attention — меньше памяти и быстрее на длинном контексте (в свежих версиях по умолчанию auto)
-ctk q8_0 -ctv q8_0KV-кэш в 8 битах (для V нужен -fa)
-t Nчисло потоков CPU для слоёв, оставшихся в RAM
-ts 1,1 / -sm rowкак делить модель между несколькими GPU
-ot "регулярка=CPU"куда положить конкретные тензоры (по регулярному выражению для их имён), например экспертов MoE

Пропускную способность своего железа меряйте штатным бенчмарком:

Окно терминала
./build/bin/llama-bench -m qwen3-8b-q4_k_m.gguf -ngl 99 -p 512 -n 128

В таблице будут строки pp512 — prefill промпта в 512 токенов — и tg128 — генерация 128 токенов (decode), обе в токенах в секунду. pp будет в десятки раз больше tg: это две фазы из урока 7.1.1 в цифрах.

MoE с экспертами в RAM. Если Qwen3-30B-A3B не влезает в видеопамять, отправьте в оперативную память только тензоры экспертов (их имена содержат _exps), а внимание и остальное оставьте на GPU. В регулярном выражении ниже точка означает «любой символ», а .* — «любая последовательность символов», так что .ffn_.*_exps. совпадает с именами вроде blk.5.ffn_up_exps.weight:

Окно терминала
./build/bin/llama-server -hf Qwen/Qwen3-30B-A3B-GGUF:Q4_K_M -ngl 99 -ot ".ffn_.*_exps.=CPU" -c 16384

В свежих версиях то же делает флаг --n-cpu-moe N (экспертов первых N слоёв — на CPU). Точный синтаксис проверяйте в llama-server --help своей версии: проект развивается очень быстро.

Ollama, llama-server, vLLM и SGLang говорят на одном языке — формате Chat Completions из урока 8.1.1. Меняется только адрес:

Серверbase_url по умолчанию
Ollamahttp://localhost:11434/v1
llama-serverhttp://localhost:8080/v1
vLLMhttp://localhost:8000/v1
SGLanghttp://localhost:30000/v1

Проверка из терминала (curl — утилита для HTTP-запросов; -H задаёт заголовок, -d — тело запроса в формате JSON):

Окно терминала
curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model": "qwen3", "messages": [{"role": "user", "content": "Привет! Кто ты? /no_think"}]}'

Из Python работает клиент openai из урока 8.1.1: OpenAI(base_url="http://localhost:11434/v1", api_key="local"). Ключ локальный сервер обычно не проверяет, но строка нужна библиотеке.

llama-bench меряет движок «в вакууме». Пользователь же видит скорость через API: с HTTP, токенизацией и чат-шаблоном. Напишем свой бенчмарк — только на стандартной библиотеке Python, чтобы он работал с любым сервером из таблицы выше. Он отправляет запрос со стримингом и засекает:

  • TTFT — от отправки до первого токена;
  • prefill — входных токенов, делённых на TTFT (имеет смысл только для длинных промптов);
  • decode — (n−1)/(tпоследний−tпервый)(n - 1) / (t_{\text{последний}} - t_{\text{первый}}), где nn — число выходных токенов.
# bench_llm.py — замер TTFT и скорости генерации у любого OpenAI-совместимого сервера (только стандартная библиотека)
import json
import sys
import time
import urllib.request
BASE_URL = "http://localhost:11434/v1" # Ollama; llama-server — :8080/v1, vLLM — :8000/v1, SGLang — :30000/v1
MODEL = sys.argv[1] if len(sys.argv) > 1 else "qwen3:8b"
def bench(prompt, max_tokens=256):
body = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
"temperature": 0,
"stream": True,
"stream_options": {"include_usage": True}, # попросим счётчики токенов в последнем чанке
}
req = urllib.request.Request(
BASE_URL + "/chat/completions",
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json", "Authorization": "Bearer local"},
)
t0 = time.perf_counter()
t_first = t_last = None
chunks, usage = 0, None
with urllib.request.urlopen(req, timeout=600) as resp:
for raw in resp: # ответ приходит строками Server-Sent Events
line = raw.decode("utf-8").strip()
if not line.startswith("data:"):
continue
data = line[len("data:"):].strip()
if data == "[DONE]":
break
msg = json.loads(data)
usage = msg.get("usage") or usage
delta = msg["choices"][0].get("delta", {}) if msg.get("choices") else {}
# у «думающих» моделей рассуждение может идти в отдельном поле — считаем и его
if any(delta.get(k) for k in ("content", "reasoning_content", "reasoning")):
t_last = time.perf_counter()
t_first = t_first or t_last # время запоминаем только у первого чанка с текстом
chunks += 1
n_out = usage["completion_tokens"] if usage else chunks
n_in = usage["prompt_tokens"] if usage else None
ttft = t_first - t0
decode = (n_out - 1) / (t_last - t_first) if n_out > 1 else float("nan")
return n_in, n_out, ttft, decode
if __name__ == "__main__":
long_text = "Карп прыгает через врата дракона и становится драконом. " * 300
tests = [("короткий промпт", "Назови три столицы Азии."),
("длинный промпт", "Перескажи в двух фразах: " + long_text)]
for run in range(2):
for name, prompt in tests:
# номер прогона в начале промпта — чтобы сервер не взял готовый KV-кэш (урок 7.1.2)
n_in, n_out, ttft, decode = bench(f"[{run}] {prompt}")
prefill = f"{n_in / ttft:7.0f}" if n_in else " ?"
# n_in!s — превратить в строку: если сервер не прислал usage, n_in = None
print(f"{name:<16} вход {n_in!s:>5} ток. | TTFT {ttft:6.2f} с | prefill ≈{prefill} ток/с | "
f"выход {n_out:>4} ток. | decode {decode:6.1f} ток/с")

Запуск: python bench_llm.py qwen3:8b (для llama-server поменяйте BASE_URL и имя модели). Формат вывода (здесь числа от тестового сервера-заглушки, а не от настоящей модели — у вас будут свои):

короткий промпт вход 7 ток. | TTFT 0.12 с | prefill ≈ 61 ток/с | выход 40 ток. | decode 48.8 ток/с
длинный промпт вход 4207 ток. | TTFT 0.31 с | prefill ≈ 13490 ток/с | выход 40 ток. | decode 48.6 ток/с

Чего ждать на RTX 4080 с Qwen3-8B в Q4_K_M — по формулам урока 7.1.1, а не по замеру: потолок decode 717/5≈143717 / 5 \approx 143 токена/с, реально порядка 100–120; prefill длинного промпта — тысячи токенов в секунду; число входных токенов будет больше, чем слов в промпте, потому что сервер добавляет chat-шаблон. Если ваш замер сильно ниже оценки, проверьте ollama ps: возможно, часть слоёв уехала на CPU.

Как читать результаты:

  • decode почти не зависит от длины промпта — это скорость «печати». Сравните её с потолком «пропускная способность / размер весов» и посчитайте эффективность: на GPU хорошие движки дают 60–85%.
  • TTFT растёт с длиной промпта, а prefill на длинном промпте в десятки раз быстрее decode — та же асимметрия фаз, что в llama-bench.
  • Уберите номер прогона из промпта и запустите дважды: на втором запуске TTFT длинного промпта резко упадёт — сервер взял его KV-кэш готовым (префикс-кэш, урок 7.1.2).

Заведите в журнале таблицу: железо, модель, квантование, контекст, TTFT, decode, потолок, эффективность. Через неделю вы будете с первого взгляда понимать, что даст новая модель на вашем компьютере.

Проверь себя6 вопросов
1 Модель на 14.8 млрд параметров в Q4_K_M (≈4.9 бита на вес). Сколько ГБ занимают её веса? Округлите до десятых.
2 Что делает флаг -ngl 99 в llama.cpp?
3 Веса 10 ГБ: 8 ГБ в видеопамяти (400 ГБ/с), 2 ГБ в RAM (50 ГБ/с). Какой потолок скорости decode (токенов/с)? Округлите до целого.
4 Что верно про Qwen3-30B-A3B при локальном запуске? Выберите все верные.Можно выбрать несколько ответов
5 Какой base_url указать клиенту openai, чтобы говорить с Ollama на этом же компьютере?
6 Что скачает команда ollama run deepseek-r1:7b?
  • Память модели ≈ веса (параметры × бит на вес / 8) + KV-кэш на нужный контекст + буферы. Считайте до скачивания.
  • Скорость decode ≈ пропускная способность памяти / байт весов. Выгрузка слоёв в RAM складывает времена, поэтому лучше меньшая модель целиком в видеопамяти. Исключение — MoE: экспертов можно держать в RAM.
  • Ollama — старт за пять минут (ollama run, ollama ps, следите за num_ctx); llama.cpp — полный контроль (-ngl, -c, -ot, llama-bench).
  • Все локальные серверы отдают OpenAI-совместимый API — код из ступени 8 работает без изменений.
  • Свой бенчмарк меряет TTFT, prefill и decode; сравнивайте с потолком и ведите журнал.

Дальше: модуль 7.3 — как честно измерить, насколько хорошо модель отвечает, а не только как быстро: бенчмарки, их ловушки и собственные эвалы.