# Эксперимент 03. Локальный стенд: скорость, память, качество, интеграция Дата прогона: 2026-07-04. Статус: **первый замер выполнен** (ollama; llama.cpp `--n-cpu-moe` — следующий шаг). Основание: `docs/research/06-local-models.md`; задача 3 полигона. ## Стенд и методика - Железо: GTX 1070 8GB VRAM; WSL2 (RAM внутри WSL ~19GB из 32). - Рантайм: **ollama 0.30.11** в боевом конфиге владельца (как в `~/vojo-local-up.sh`): `OLLAMA_KEEP_ALIVE=-1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0`. llama.cpp (llama-server) на стенде не установлен. - Модели (установлены на стенде): `huihui_ai/qwen3-abliterated:8b` (5.0GB), `qwen3-vojo:latest` (тот же 8b + запечённые сэмплеры Qwen3: temp 0.7, top_p 0.8, top_k 20, repeat_penalty 1.1, num_ctx 16384), `huihui_ai/qwen3-abliterated:30b-a3b` (MoE 30B, 3B активных, 18GB). Для задачи 5 дотянут `bge-m3`. - Скрипт: `eval/local_bench.py` — 3 фрагмента (~1.4–1.5k знаков: Расёмон ja, Мелос ja, Моление о счастье zh) × перевод на русский через `/api/generate` (num_ctx 8192, temp 0.3), метрики из ответа ollama, пик VRAM — опрос nvidia-smi. Эталон DeepSeek API не снят (нет ключа) — добавится автоматически при появлении `DEEPSEEK_API_KEY` в `eval/.env`. - Грабли окружения, задокументированные по пути: системный прокси перехватывает запросы к 127.0.0.1 (`NO_PROXY=` — WinINET-нотация, не работает в curl/urllib/Go) — все локальные вызовы в обход прокси; `pkill -f` из inline-команд матчит собственный шелл. ## Скорость и память | Модель | Генерация, tok/s | Prefill, tok/s | Пик VRAM | Время на фрагмент | |---|---|---|---|---| | qwen3-abliterated:8b | **26.8–27.3** | 519–556 | ~6.6 GB (целиком в VRAM) | 63–93 с | | qwen3-vojo (8b, сэмплеры) | 26.5–26.7 | 521–599 | ~6.5 GB | 68–96 с | | qwen3-abliterated:30b-a3b | **9.9–10.0** | 73–302 | 7.6 GB + эксперты в RAM (mmap) | 164–278 с | Замечания: - 8b @ ~27 tok/s совпадает с референсом из vojo-плана (~28 tok/s на этом же железе). Холодная загрузка 8b ~14–20 с (из vojo-замеров), 30b — заметно дольше (первый фрагмент 278 с против 164 с у второго — разница в основном прогрев/подкачка 18GB с диска). - Замер RAM через MemAvailable недооценивает след 30b: веса подключаются mmap'ом и живут в page cache. Фактически модель занимает почти весь свободный запас WSL; параллельная тяжёлая нагрузка на RAM в момент работы 30b нежелательна. - Prefill 30b (73–302 tok/s) в разы медленнее 8b (~520) — для «прочитай много, выведи мало» ролей (скрининг, экстракция) 8b выгоднее и по этой оси. ## Расхождение с research/06: скорость MoE Док 06 обещает **~25–30 tok/s** для 35B-A3B через llama.cpp `--n-cpu-moe` на этом классе железа. Фактически **ollama даёт только ~10 tok/s** на 30B-A3B: её автоматический офлоад не эквивалентен ручному `--n-cpu-moe` (не держит все эксперты в RAM при закреплении attention/router на GPU). Вывод дока «рантайм — llama-server» подтверждается и по скорости, не только по параллелизму/контролю: **следующий шаг — поставить llama.cpp и перемерить** тем же скриптом (llama-server совместим по `/v1`; для замера через `/api/generate` добавить ветку). До этого 30b-a3b на ollama пригоден для несрочных фоновых ролей. Второе расхождение — **поколение моделей**: док 06 рекомендует Qwen3.5-9B / Qwen3.6-35B-A3B (март 2026), на стенде — предыдущее поколение Qwen3 (abliterated). huihui-ai уже публикует Huihui-Qwen3.5-35B-A3B-abliterated — при обновлении стенда качество ниже перемерить. ## Качество ja→ru / zh→ru (субъективно, с эталоном DeepSeek API) Переводы сохранены в `eval/data/local_bench//*.ru.txt`. Общая картина обоих размеров: **русский текст беглый и связный, но реалии и имена систематически ломаются**: - 8b, «Расёмон»: ворота 羅生門 → «Рождественская дверь» (!), 朱雀大路 прочитана по-китайски («улица Чжуцзянь»); 30b: «Рашимон» / «Чжуцзяньдаи» — лучше, но всё равно мимо традиционной передачи (Расёмон, дорога Судзаку). - zh, «Моление о счастье»: реалия 送灶 (проводы бога очага) → у 8b «проводили старый год», у 30b — «петухи на кухне» (галлюцинация); заголовок 祝福 у 30b — «Слава». - 30b местами хуже 8b в грамматике («одна сверчок», «по-временам») при чуть лучшей передаче смысла длинных фраз; на 10 tok/s это качество обходится втрое дороже по времени. - Мелос (ja, простая динамичная проза) — у обоих приемлемый черновик с мелкими сдвигами смысла («изгнать короля» вместо «убрать/убить», 十里 → «десять лин»). **Эталон DeepSeek API (deepseek-chat, снят 2026-07-04 после пополнения ключа, `eval/data/local_bench/deepseek-api/`) закрывает вопрос:** те же фрагменты за 13–22 с (~2.2–2.9k токенов ≈ $0.0005/фрагмент) переведены с корректными реалиями — «ворота Расёмон… на улице Судзаку», «с которой местами облупилась киноварь» (местные модели дали «Рождественская дверь»/«Рашимон», «улица Чжуцзянь», «красная краска»), заголовок 祝福 — «Новогоднее жертвоприношение» (правильно; локально — пропуск/«Слава»). Разрыв качественный, не градуальный: у API — уровень «черновик, пригодный редактору», у локальных — «черновик, требующий сверки с оригиналом». **Вердикт дока 06 подтверждён эмпирически с эталоном: локалка — «спецслужба», не переводчик.** Для финальных ролей перевода непригодна (реалии/имена — ровно то, что убивает доверие читателя); для ролей скрининга «горячести», экстракции терминов, чернового NSFW-перевода под обязательную редактуру, дешёвого гейта — скорость и качество достаточны. Открытый вопрос: перевод NSFW-фрагментов (единственная переводческая роль локалки) — проверить на реальных 18+ фрагментах владельца, когда появятся в корпусе; пока все замеры — SFW-классика. **Расширение пула моделей (обсуждено с владельцем, план):** все три чат-модели стенда — абляции одного семейства Qwen3, поэтому не измерена «цена абляции» и не представлено поколение 3.5. Очередь: (1) llama.cpp для 30b-a3b (×3 скорости уже установленной модели), (2) ванильный qwen3:8b — цена абляции на SFW-ролях, (3) Qwen3.5-9B + abliterated-вариант, (4) RuadaptQwen3 (до +100% скорости на ru-выходе — самой тяжёлой статье по эксперименту 01). Shisa V2 12B / Gemma 4 — отложены до методики пилота (задача 4). ## Интеграция в бэкенд: что взять из vojo/ai-bot Конспект локального контура ai-bot (файлы `provider_local.go`, `failover.go`, `httpllm.go`, `config.go`; план `docs/plans/local_llm_backend.md`): - **Адаптер**: локалка = ещё один OpenAI-совместимый `/v1/chat/completions` без стриминга; сэмплеры Qwen3 через `/v1`-слой ollama не пробрасываются (issue #11325) — **запекаются в Modelfile** (паттерн qwen3-vojo). Для llama-server — наоборот, расширить запрос полями `top_k/min_p/repeat_penalty` (omitempty). - **Health-проба**: `GET /v1/models` каждые 15 с, таймаут 3 с; старт в состоянии «нездорова»; down→up = 1 успешная проба, после сбоя запроса — 2 подряд (анти-флаппинг и защита от «cold-load abort loop»: аборт запроса отменяет загрузку модели). Проба не проверяет резидентность модели — это обязанность `OLLAMA_KEEP_ALIVE=-1`; для llama-server есть `/health`, `/slots`. - **Failover**: breaker открывается с одной неудачи (transport/5xx/timeout/429), восстановление только пробами; терминальные 4xx не маскируются облаком; пустой 2xx (thinking съел бюджет токенов) → повтор на облаке без trip. Ретраи транспорта: 3 попытки, backoff 0.5→8 с + джиттер. - **Критично для TextMachine — тайминги**: у vojo дедлайн локальной ноги 45 с (под короткие чат-ответы). Наши книжные чанки на локалке занимают **63–278 с** — профиль таймаутов нужен свой (лег-дедлайн ~300–600 с по роли/модели) плюс стриминг в интерфейсе клиента (уже запланирован в Р1), иначе фейловер будет ложно ронять здоровую локалку. - `max_tokens` у ollama покрывает thinking+ответ вместе — для thinking-режимов задавать увеличенный потолок (у vojo 1024; для перевода главы нужно 4–8k) или выключать thinking (`reasoning_effort: none`). ## Подготовка ко второму замеру (выполнено 2026-07-04, ~06:30) - **llama.cpp собран с CUDA**: `/home/ubuntu/llama.cpp/build/bin/llama-server` (+`llama-cli`; commit 2d973636). Без root: user-space CUDA toolkit 12.6 в `/home/ubuntu/cuda-12.6` (nvcc 12.6.85 из .deb-распаковки + cublas из PyPI-wheel, всего ~450MB загрузки), арх sm_61, rpath вшит — `LD_LIBRARY_PATH` не нужен. CPU-fallback: `build-cpu/bin/`. **GTX 1070 (Pascal) поддерживается CUDA 12.x, на CUDA 13 не обновлять** (Pascal выпилен). Флаги пересборки — в scratchpad-логах и здесь: `cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_COMPILER=$HOME/cuda-12.6/bin/nvcc -DCMAKE_CUDA_ARCHITECTURES=61 -DCMAKE_BUILD_RPATH=$L -DCMAKE_EXE_LINKER_FLAGS="-Wl,-rpath-link,$L -Wl,-rpath,$L"`, где `L=$HOME/cuda-12.6/targets/x86_64-linux/lib`. На GPU ещё не запускался (шёл бенчмарк) — первый прогон проверит подхват `libcuda.so.1` из `/usr/lib/wsl/lib`. - **Пул моделей расширен** (план выше выполнен досрочно): `qwen3:8b` (ваниль, 5.2GB), **`qwen3.5:9b`** (есть в реестре ollama; 9.7B, arch qwen35, vision+thinking; дефолт-сэмплеры реестра temp 1/top_p 0.95/presence 1.5 — для честного сравнения выровнять с qwen3), `huihui_ai/qwen3.5-abliterated:9b` (6.6GB), `ruadapt-qwen3:8b` (создан из официального RefalMachine/RuadaptQwen3-8B-**Hybrid**-GGUF Q4_K_M — Instruct-версии 8B не существует, hybrid = instruct с переключаемым thinking). - **WSL RAM поднят 20→26GB** (`C:\Users\lager\.wslconfig`, бэкап рядом) — вступит после `wsl --shutdown`. ## Следующие шаги 1. **[после рестарта WSL]** Перемерить 30b-a3b: llama-server с `--n-cpu-moe` (веса можно грузить прямо из ollama blob-store — блобы это GGUF; путь через `ollama show`) против ollama при 26GB RAM — проверка обещанных 25–30 tok/s. Добавить в `local_bench.py` ветку llama-server (он OpenAI-совместим по /v1). 2. ~~Снять эталон DeepSeek~~ — **сделано** (см. выше): разрыв качественный, локалка остаётся «спецслужбой». 3. ~~Расширить пул~~ — **скачано** (см. выше); прогнать `local_bench.py` по четырём новым моделям: цена абляции (qwen3 ваниль vs abliterated), поколение (qwen3 vs 3.5), ru-адаптация (ruadapt). NSFW-фрагменты владельца — для проверки единственной переводческой роли локалки. 4. Замер bge-m3 (скорость/качество retrieval) — в рамках задачи 5 (мини-бенчмарк эмбеддингов).