Идея была простая: хочу AI-ассистента в редакторе, но чтобы мой код и данные не уезжали в чужое облако. Без подписки, без «ваш запрос обрабатывается на наших серверах», без сюрпризов с изменением тарифов. А ещё — без видеокарты, потому что покупать GPU за полцены автомобиля ради побаловаться я не готов.

Спойлер: получилось. На обычном CPU-сервере крутится модель уровня 35B, отвечает со скоростью ~20 токенов в секунду, держит 128К контекста и работает в двух режимах — как чат-ассистент и как полноценный автономный агент, который сам пишет и правит файлы. Путь занял несколько вечеров и пять перепробованных моделей, три из которых оказались мусором. Рассказываю всё: железо, движок, оптимизации с замерами и грабли, на которых я честно потерял время.

Зачем вообще своя модель

  • Приватность. Код и данные остаются на своём сервере. Для security-исследований и чувствительного кода это не паранойя, а гигиена.
  • Без GPU. Всё на обычном CPU-сервере. Видеокарта не нужна — это меняет экономику в разы.
  • 128К контекста. Можно скармливать целые файлы и документы, а не нарезать их на кусочки.
  • Снятие ограничений. Модель abliterated (uncensored) не отказывает в задачах пентеста и reverse engineering. Для security-работы это необходимость, а не баловство.

Кому зайдёт: тем, у кого есть CPU-сервер без GPU; security-специалистам; и всем, кому интересно, как локальный LLM устроен изнутри, а не по верхам.

Что получилось в итоге

ПараметрЗначение
МодельQwen3.6-35B-A3B abliterated, MoE 35B / 3B активных
Контекст128K токенов
Скорость генерации~19,8 tok/s (CPU, без GPU)
Обработка промпта~278 tok/s
Нативный tool-callingда (агент-режим работает)
ДоступVSCode: Continue (чат) + Claude Code (автономный агент)
Стоимость работы0 ₽ после настройки

Требования

Сервер (то, на чём собрано):

  • CPU: Intel Xeon Gold 6154, AVX-512, без GPU. Под генерацию реально задействуются 16 ядер.
  • RAM: 40 ГБ (минимум 32 ГБ для модели 35B в Q4)
  • Диск: 52 ГБ (модель ~21 ГБ + система + кеши)
  • ОС: Ubuntu 24.04, доступ по SSH

Xeon — это пример, а не требование. Важен не бренд, а три вещи:

  • AVX-512 — даёт основной прирост скорости. Есть у Xeon Scalable (Skylake-SP+), Intel Core 11-го поколения и у AMD Ryzen 7000/9000 (Zen 4/5). А вот у десктопных Intel Core 12–14-го поколения его отрезали ради E-ядер — такой процессор мимо.
  • Память. Главное узкое место: генерация упирается не в вычисления, а в скорость памяти. У серверного Xeon 6–8 каналов, у десктопа — 2. Поэтому дома на том же AVX-512 будет 5–10 tok/s вместо 20. Не баг, а физика.
  • RAM — под модель 35B в Q4 нужно ~23 ГБ.

Для дома лучший вариант — AMD Ryzen 7000/9000.

Клиент: Windows 11 (PowerShell; на Linux/Mac аналогично), VSCode + Continue, Node.js, Python 3.12, uv, git, ssh.

Все секреты в командах — плейсхолдеры (<USER>, <SERVER_IP>, <PORT>). Подставьте свои.

Архитектура: зачем столько слоёв

На первый взгляд цепочка выглядит избыточной. На деле каждый слой появился не от любви к усложнению, а потому что без него что-то не работало.

text
        ВАШ КОМПЬЮТЕР (Windows)              УДАЛЁННЫЙ СЕРВЕР (Linux, CPU)
  VSCode + Continue ──┐
                      ├─ SSH-туннель ──→  SSE-прокси :8080 ─→ llama-server :8081
  Claude Code ─→ fcc ─┘  :11435 → :8080                       (ik_llama.cpp, Qwen3.6-35B)
       :8082
  • llama-server (ik_llama.cpp) — собственно движок, заточенный под MoE на CPU.
  • SSE-прокси — костыль, без которого Continue получает пустоту (об этом отдельная боль ниже).
  • SSH-туннель — шифрованный канал; модель наружу не торчит, слушает только localhost.
  • fcc (free-claude-code) — переводчик с Anthropic API на OpenAI, чтобы Claude Code думал, что говорит с настоящим Claude.

Шаг 1. Разведка сервера

Прежде чем что-то ставить, надо понять, на чём вообще работаем:

bash
ssh -p <PORT> <USER>@<SERVER_IP>

lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|NUMA'
nproc                          # число vCPU
free -h                        # RAM
df -h /                        # диск

На что смотреть:

  • AVX-512 — без него быстрый CPU-inference не получится; есть у Xeon Skylake и новее.
  • AMX — если повезло и это Sapphire Rapids (2023+), можно копать в сторону KTransformers. Если нет — обычный llama.cpp, и не страдайте.
  • Thread(s) per core — единица означает, что все потоки физические.
  • NUMA nodes — на multi-socket это критично для тюнинга потоков. Запомните цифру, она аукнется в шаге 4.

Шаг 2. Сборка движка ik_llama.cpp

Почему не Ollama, на которой все начинают? Ollama прекрасна для «запустить за минуту», но на MoE-моделях по CPU она оставляет половину скорости на полу. ik_llama.cpp умеет fused MoE и run-time-repack — это примерно вдвое быстрее на той же железке. Плюс нативный tool-calling через --jinja и нормальный большой контекст.

bash
sudo apt-get install -y build-essential cmake libcurl4-openssl-dev git
cd ~
git clone --depth 1 https://github.com/ikawrakow/ik_llama.cpp
cd ik_llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON -DGGML_AVX512=ON -DLLAMA_CURL=ON
cmake --build build --config Release -j $(nproc) --target llama-server llama-cli

Сборка — около 4 минут. Бинарь: ~/ik_llama.cpp/build/bin/llama-server. Флаг GGML_NATIVE=ON собирает под конкретный процессор, выжимая все его инструкции; GGML_AVX512=ON явно включает AVX-512.

Шаг 3. Выбор модели — и почему три из пяти оказались мусором

Это была самая нервная часть. Скачать модель на 20 ГБ, прогреть, прогнать — и обнаружить, что она битая. И так несколько раз.

МодельЧем закончилось
TrevorJS Gemma 4 26B uncensoredгенерит только <unused> токены — битые embeddings, в топку
Huihui Gemma 4 26B abliteratedработает, но reasoning слабый, галлюцинирует
Gemma 4 31B Dense hereticумная, но dense → ~2 tok/s на CPU, невыносимо медленно
Qwen3-Coder 30B-A3B abliteratedхорошая, MoE, заточена под код, 23,7 tok/s
Qwen3.6-35B-A3B abliteratedто, что осталось — поколение 3.6, умнее, 19,8 tok/s

Главный урок, оплаченный гигабайтами трафика: для CPU берите MoE с малым числом активных параметров (A3B = 3 млрд активных на токен). Скорость как у крохотной 3B-модели, а знания как у 35B — лучшее из обоих миров. И проверяйте перед скачиванием: если у модели на HuggingFace много загрузок или она есть в Ollama registry — скорее всего, рабочая. Если нет — вы рискуете повторить мой квест с <unused>-токенами.

Текущая рабочая модель — Huihui-Qwen3.6-35B-A3B-abliterated (GGUF) на HuggingFace, квант i1-Q4_K_M (~21 ГБ). Качаем многопоточно:

bash
sudo apt-get install -y aria2
URL="https://huggingface.co/mradermacher/Huihui-Qwen3.6-35B-A3B-abliterated-i1-GGUF/resolve/main/Huihui-Qwen3.6-35B-A3B-abliterated.i1-Q4_K_M.gguf"
aria2c -x 16 -s 16 -c -o qwen36.gguf -d ~ "$URL"

-x 16 -s 16 — 16 параллельных соединений, заметно бодрее, чем wget. -c — продолжить при обрыве: HuggingFace обожает рвать последний кусок на 99 %, просто запускайте ту же команду снова.

Отдельная подстава свежих моделей (Qwen3+, Gemma): по умолчанию это reasoning-модели. Они пишут «мысли» в reasoning_content, а поле content остаётся пустым — и вы сидите, гадаете, почему модель «молчит». Лечится флагом --reasoning off (шаг 5).

Шаг 4. CPU-оптимизации — где собака зарыта

Самое контринтуитивное открытие всего проекта: на CPU больше потоков не значит быстрее. Я был уверен, что 44 ядра разорвут задачу. Замерил — и схватился за голову:

ПотоковСкорость
447,68 tok/s
368,10 tok/s
248,86 tok/s
169,37 tok/s ← оптимум
129,04 tok/s

Цифры тут абсолютно ниже финальных 19,8 tok/s — это ранний замер на промежуточной модели без полного набора оптимизаций. Важна не абсолютная скорость, а относительная картина: 16 потоков обгоняют 44. Причина — генерация упирается в память, а не в вычисления. Когда потоки раскиданы по двум сокетам, они дерутся за шину памяти, и межсокетный обмен всё тормозит. Меньше потоков, меньше драки — выше скорость.

Но есть второй акт: обработка промпта (prefill) — наоборот, считает, а не ждёт память. Ей все ядра в радость. Отсюда два разных значения в конфиге: генерация на 16 потоках, prefill на всех.

ФлагЗначениеЗачем
-t 16потоки генерацииоптимум (память — бутылочное горло)
-tb 44потоки prefillвсе ядра, тут это уместно
-ub 2048размер ubatchкрупнее батч → быстрее обработка длинных промптов
-fa onflash attentionскорость + экономия памяти
-ctk q8_0 -ctv q8_0KV-кеш 8-битвдвое меньше памяти на 128K
--run-time-repackrepack тензоровускорение MoE на CPU
--reasoning offвыкл. reasoningпрямые ответы вместо пустого content
--prompt-cache FILEкеш промптаповторный prefill пропускается
-ngl 0слоёв на GPUноль, мы же без видеокарты

Что НЕ помогло (а я надеялся)

  • ngram speculative: 17 против 19,8 tok/s — стало хуже. Накладные расходы съели весь выигрыш. Помогло бы только на сильно повторяющемся тексте, но кто его генерирует.
  • draft-модель (Qwen2.5-0.5B в помощники): 35 % acceptance из-за разных поколений токенизатора. Вместо ускорения — тормоз.
  • KTransformers: вся его магия — в Intel AMX, а на Skylake его нет. Помечтал и закрыл вкладку.
  • Реально ускорили бы только MTP-версия весов или, сюрприз, видеокарта.

Мораль: не верьте обещаниям ускорителей на слово. Замеряйте на своём железе — половина «серебряных пуль» на вашей конфигурации окажется холостыми.

Шаг 5. Запуск и автостарт через systemd

Сначала проверяем руками, что вообще живёт:

bash
~/ik_llama.cpp/build/bin/llama-server \
  -m ~/qwen36.gguf \
  --jinja -c 131072 -t 16 -tb 44 -ub 2048 -fa on \
  -ctk q8_0 -ctv q8_0 --run-time-repack -ngl 0 \
  --reasoning off --prompt-cache ~/pcache.bin \
  --host 127.0.0.1 --port 8081

Первая загрузка 128К-контекста занимает 2–5 минут — не пугайтесь, это разово. Готовность: curl -s http://127.0.0.1:8081/health отдаст {"status":"ok"}.

Чтобы не запускать это руками после каждого ребута, отдаём systemd — /etc/systemd/system/llama-server.service:

ini
[Unit]
Description=ik_llama.cpp llama-server (Qwen3.6-35B-A3B, 128K)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=<USER>
WorkingDirectory=/home/<USER>
ExecStart=/home/<USER>/ik_llama.cpp/build/bin/llama-server \
  -m /home/<USER>/qwen36.gguf \
  --jinja -c 131072 -t 16 -tb 44 -ub 2048 -fa on \
  -ctk q8_0 -ctv q8_0 --run-time-repack -ngl 0 \
  --reasoning off --prompt-cache /home/<USER>/pcache.bin \
  --host 127.0.0.1 --port 8081
Restart=on-failure
RestartSec=10
Nice=-5

[Install]
WantedBy=multi-user.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now llama-server

Приятный бонус: llama-server держит модель в RAM, пока жив процесс. Грузится один раз при старте и больше не выгружается — в отличие от Ollama с её keep_alive, которая любит выгрузить модель ровно перед тем, как она вам понадобилась.

Шаг 6. SSE-прокси — день, который я почти потерял

А вот и обещанная боль. Подключаю Continue — и получаю пустые ответы. Ноль токенов. Сервер при этом отвечает, в curl всё видно. Полчаса я думал, что схожу с ума.

Разгадка: ik_llama.cpp шлёт потоковый ответ чуть-чуть не по стандарту OpenAI — пихает поле usage в каждый чанк и присылает content:null. Большинству клиентов плевать, но строгий парсер Continue на этом спотыкается и выдаёт пустоту. Лечится лёгким прокси на aiohttp: слушает :8080, форвардит на :8081, по дороге выкидывает usage из промежуточных чанков и content:null.

bash
sudo apt-get install -y python3-aiohttp
sudo systemctl enable --now sse-proxy

Важная деталь, которую я выстрадал отдельно: прокси должен стримить и эндпоинт /v1/messages (его дёргает Claude Code), а не только /chat/completions. Иначе агент получит ответ одним жирным блоком вместо потока — как будто завис, а потом разом всё выплюнул. Ключ — отдавать чанки по мере поступления (async for chunk in up.content.iter_any()), а не копить через await response.read().

Шаг 7. SSH-туннель с Windows

Модель наружу не торчит — пробрасываем порт через SSH:

powershell
ssh -p <PORT> -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes `
    -o StrictHostKeyChecking=accept-new `
    -L 11435:localhost:8080 -N <USER>@<SERVER_IP>

Один нюанс, который стоил мне очередных «почему опять нет ответа»: туннель должен быть ровно один. Если скрипт автостарта запустится дважды, два процесса начнут делить порт 11435, и HTTP поедет. Проверка простая — Get-Process ssh должен показывать один процесс. Автостарт при логине вешается на Task Scheduler.

Шаг 8. Continue.dev — режим чата

Continue — это чат-панель прямо в VSCode. Фрагмент ~/.continue/config.yaml:

yaml
models:
  - name: Qwen3.6 35B (server)
    provider: openai
    model: qwen36
    apiBase: http://localhost:11435/v1
    apiKey: none
    roles: [chat, edit, apply]      # БЕЗ autocomplete!
    capabilities: [tool_use]
    defaultCompletionOptions:
      temperature: 0.2              # низкая → стабильный формат tool-calls
      contextLength: 131072
      maxTokens: 8192

Работа: Ctrl+L — чат, Ctrl+I в файле — правка прямо в коде, @web запрос — поиск в интернете.

Не вешайте роль autocomplete на тяжёлую модель. Автодополнение дёргает её на каждую нажатую клавишу, забивает единственный слот сервера, и всё начинает тупить — включая ввод в терминале. Я попробовал. Один раз. Больше не хочу.

Шаг 9. Claude Code на локальной модели — автономный агент

Чат — это хорошо, но настоящая магия начинается, когда модель сама создаёт и правит файлы, запускает команды и доводит задачу до конца. Для этого берём free-claude-code — прокси, который подсовывает Claude Code локальную модель вместо Anthropic.

powershell
uv tool install --force "git+https://github.com/Alishahryar1/free-claude-code.git"
npm install -g @anthropic-ai/claude-code

Конфиг ~/.fcc/.env указывает на локальный бэкенд:

text
LLAMACPP_BASE_URL=http://localhost:11435/v1
MODEL=llamacpp/qwen36
ENABLE_MODEL_THINKING=false
ANTHROPIC_AUTH_TOKEN=<любой-локальный-токен>
HTTP_READ_TIMEOUT=600
Важный момент, который легко пропустить и попасть на деньги. Если Claude Code залогинен в ваш Anthropic-аккаунт, он проигнорирует ANTHROPIC_BASE_URL и пойдёт в платный Anthropic. Вы думаете, что гоняете локальную модель, а в биллинге тем временем капает «Opus, API Usage». Лечение — отдельный CLAUDE_CONFIG_DIR без сохранённого OAuth, чтобы Claude Code было некуда деваться, кроме вашего прокси.

Полная цепочка вызова, чтобы было видно, через сколько рук проходит запрос:

text
Claude Code → fcc:8082 → туннель:11435 → SSE-прокси:8080 → llama-server:8081 → Qwen3.6

Производительность: чего реально ждать

МетрикаЗначение
Генерация19,8 tok/s
Prefill (обработка промпта)278 tok/s
Холодная загрузка модели (128K)~2–5 мин (разово)
Prompt-cache (повторный промпт)5 токенов вместо 2 515
RAM под модель + KV~23 ГБ

Честные ожидания, без маркетинга:

  • Continue-чат отвечает за секунды — промпт небольшой.
  • Claude Code-агент на первом запросе сессии думает пару минут: системный промпт огромный (~18К токенов), его надо прожевать. Дальше выручает prompt-cache, и становится бодро.
  • ~20 tok/s — это потолок 35B-A3B на CPU без GPU. Хотите больше — это уже к видеокарте.

Выводы

  • MoE с малым active на CPU реально юзабельна — ~20 tok/s без GPU при знаниях уровня 35B. Ещё пару лет назад это звучало бы как фантастика.
  • Меньше потоков может быть быстрее. На multi-socket генерация упирается в память; оптимум надо замерять, а не угадывать (у меня 16 из 44).
  • ik_llama.cpp — правильный выбор для MoE на CPU. SSE-прокси к нему обязателен, если клиент строгий.
  • prompt-cache снимает главную боль больших системных промптов — без него Claude Code был бы невыносим.
  • Ускорители — не панацея. AMX, speculative, draft-модели: половина не сработала на моём железе. Замеряйте.

Итог: полностью локальный, приватный, бесплатный после настройки стек, который сам поднимается после ребута. Облако и подписка не нужны. А вечера, потраченные на грабли, теперь сэкономлены вам — пользуйтесь.