Идея была простая: хочу 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>). Подставьте свои.
Архитектура: зачем столько слоёв
На первый взгляд цепочка выглядит избыточной. На деле каждый слой появился не от любви к усложнению, а потому что без него что-то не работало.
ВАШ КОМПЬЮТЕР (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. Разведка сервера
Прежде чем что-то ставить, надо понять, на чём вообще работаем:
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 и нормальный большой контекст.
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 ГБ). Качаем многопоточно:
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 ядра разорвут задачу. Замерил — и схватился за голову:
| Потоков | Скорость |
|---|---|
| 44 | 7,68 tok/s |
| 36 | 8,10 tok/s |
| 24 | 8,86 tok/s |
| 16 | 9,37 tok/s ← оптимум |
| 12 | 9,04 tok/s |
Цифры тут абсолютно ниже финальных 19,8 tok/s — это ранний замер на промежуточной модели без полного набора оптимизаций. Важна не абсолютная скорость, а относительная картина: 16 потоков обгоняют 44. Причина — генерация упирается в память, а не в вычисления. Когда потоки раскиданы по двум сокетам, они дерутся за шину памяти, и межсокетный обмен всё тормозит. Меньше потоков, меньше драки — выше скорость.
Но есть второй акт: обработка промпта (prefill) — наоборот, считает, а не ждёт память. Ей все ядра в радость. Отсюда два разных значения в конфиге: генерация на 16 потоках, prefill на всех.
| Флаг | Значение | Зачем |
|---|---|---|
-t 16 | потоки генерации | оптимум (память — бутылочное горло) |
-tb 44 | потоки prefill | все ядра, тут это уместно |
-ub 2048 | размер ubatch | крупнее батч → быстрее обработка длинных промптов |
-fa on | flash attention | скорость + экономия памяти |
-ctk q8_0 -ctv q8_0 | KV-кеш 8-бит | вдвое меньше памяти на 128K |
--run-time-repack | repack тензоров | ускорение 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
Сначала проверяем руками, что вообще живёт:
~/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:
[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.targetsudo 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.
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:
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:
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.
uv tool install --force "git+https://github.com/Alishahryar1/free-claude-code.git"
npm install -g @anthropic-ai/claude-codeКонфиг ~/.fcc/.env указывает на локальный бэкенд:
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 было некуда деваться, кроме вашего прокси.
Полная цепочка вызова, чтобы было видно, через сколько рук проходит запрос:
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-модели: половина не сработала на моём железе. Замеряйте.
Итог: полностью локальный, приватный, бесплатный после настройки стек, который сам поднимается после ребута. Облако и подписка не нужны. А вечера, потраченные на грабли, теперь сэкономлены вам — пользуйтесь.