# Эксперимент 03. Локальный стенд: скорость, память, качество

Дата: 2026-07-04. Задача 3 полигона. Основание: [research/06-local-models.md](../research/06-local-models.md).
Скрипты: `eval/local_bench.py` (dense-модели через ollama), `eval/llama_moe_bench.sh` (MoE через llama.cpp). Сырые переводы: `eval/data/local_bench*/`.

## Итог (TL;DR)

1. **Вердикт research/06 подтверждён: локалка — «спецслужба», не переводчик.** Ни одна из 6 протестированных моделей не передаёт имена собственные и реалии правильно (羅生門 → «Расёмон», 送灶 → «проводы бога очага»), тогда как DeepSeek API берёт их за ~$0.0005/фрагмент. Разрыв качественный, не градуальный. Роли локалки: скрининг «горячести» 18+, извлечение терминов, эмбеддинги, черновой NSFW-перевод под обязательную редактуру — не финальный перевод.
2. **8–9B dense — рабочая лошадка стенда: ~24–27 tok/s, влезает в VRAM целиком (~6.6 ГБ).** Этого достаточно для «спецслужбных» ролей.
3. **30B-A3B (MoE) — потолок ~13.5 tok/s, «Расхождение» с research/06** (обещано 25–30). Узкое место — пропускная способность CPU-RAM, а не рантайм. Годен только для несрочных фоновых ролей (~2.5 мин на главу).
4. **Абляция (снятие цензуры) — бесплатна**: qwen3.5 обычная и abliterated идентичны по скорости и SFW-качеству. Для 18+-ролей берём abliterated без штрафа.
5. **RuAdapt — быстрый по русскому выходу (−40% токенов), но понимание исходника разрушено** → редактор ru-стиля, не переводчик.
6. **Грабли для бэкенда**: у моделей с thinking-режимом (Qwen3.5+, GLM, Gemini) роль переводчика обязана явно управлять reasoning — иначе пустой/обрезанный вывод.

## Стенд

- Железо: GTX 1070 8GB VRAM; WSL2, RAM поднят до 26GB (`.wslconfig`).
- ollama 0.30.11 в боевом конфиге (`OLLAMA_KEEP_ALIVE=-1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0`); скрипт запуска — `eval/ollama-up.sh`.
- llama.cpp собран с CUDA (детали сборки — приложение А).
- Методика: 3 фрагмента ja/zh→ru (~1.4–1.5k знаков: Расёмон, «Беги, Мелос!», «Моление о счастье»), перевод на русский, temp 0.3; метрики tok/s из ответа ollama, пик VRAM — опрос nvidia-smi. Эталон — DeepSeek API (deepseek-chat) на тех же фрагментах.
- Грабли окружения: системный прокси перехватывает 127.0.0.1 (`NO_PROXY=<local>` — WinINET-нотация, не работает в curl/urllib/Go) → локальные вызовы в обход прокси.

## Скорость и память

Dense-модели (ollama, thinking off), генерация усреднена по 3 фрагментам:

| Модель | Генерация, tok/s | Prefill, tok/s | Пик VRAM | ток/симв (ru-выход) |
|---|---|---|---|---|
| qwen3:8b (ваниль) | 27.1 | ~520 | 6.6 GB | 0.37 |
| qwen3-abliterated:8b | 27.0 | ~530 | 6.6 GB | 0.37 |
| qwen3-vojo (8b + сэмплеры) | 26.6 | ~550 | 6.5 GB | 0.37 |
| ruadapt-qwen3:8b | 27.0 | ~500 | 6.6 GB | **0.22** |
| qwen3.5:9b | 24.2 | ~518 | ~6.8 GB | 0.28 |
| qwen3.5-abliterated:9b | 24.5 | ~520 | ~6.8 GB | 0.30 |

MoE-модель 30B-A3B (18 ГБ весов, 3B активных) — по рантаймам:

| Рантайм / конфиг | Генерация, tok/s | Prefill, tok/s | VRAM |
|---|---|---|---|
| ollama (авто-офлоад) | 10.0 | 73–302 | 7.6 GB |
| llama.cpp `--cpu-moe` (все эксперты на CPU) | 11.2 | 105 | 2.9 GB |
| llama.cpp `--n-cpu-moe 36` (12/48 слоёв на GPU) | 12.8 | 171 | 7.3 GB |
| llama.cpp `--n-cpu-moe 33` (15/48 слоёв на GPU, потолок VRAM) | **13.5** | 208 | ~7.8 GB |

Замечания:
- 8b @ ~27 tok/s совпадает с референсом vojo-плана (~28 tok/s на этом железе). Холодная загрузка 8b ~14–20 с; 30b — заметно дольше (подкачка 18 ГБ с диска).
- Prefill 30b (73–302) в разы медленнее 8b (~520) — для ролей «прочитай много, выведи мало» (скрининг, экстракция) 8b выгоднее и по этой оси.
- След 30b по RAM через MemAvailable недооценён (веса mmap'ятся в page cache); фактически модель занимает почти весь свободный запас WSL.

## Расхождение с research/06: скорость MoE

Док 06 обещает **25–30 tok/s** для 35B-A3B через llama.cpp `--n-cpu-moe`. Фактический потолок на GTX 1070 — **~13.5 tok/s** (таблица выше). llama.cpp честно лучше ollama (+35% генерация, ×2–3 prefill, полный контроль VRAM), но упирается не в рантайм, а в **железо**: при a3b каждый токен читает эксперты, и те, что в CPU-RAM (DDR4 ~40 ГБ/с на Pascal-платформе), — узкое место. Перенос слоёв экспертов на GPU (VRAM 2.9→7.8 ГБ) поднял генерацию лишь 11→13.5. Обещанные 25–30 — вероятно, RTX 3060 + DDR5 (процитированный Medium-пост платформу не уточнял).

**Следствие для Р4:** llama.cpp как рантайм подтверждён, но 30b-a3b на этом железе — только фоновые роли (13.5 tok/s ≈ 2.5 мин на главу вывода). Интерактив и объём — на 8–9B dense или API.

## Качество перевода

Все замеры — на SFW-классике (Акутагава, Дадзай, Лу Синь). Общая картина: **русский текст беглый и связный, но имена собственные и культурные реалии систематически ломаются** — а это ровно то, что убивает доверие читателя.

Контрольная реалия 羅生門 (ворота Расёмон) по моделям:

| Источник | 羅生門 → | Оценка |
|---|---|---|
| **DeepSeek API** (эталон) | «ворота Расёмон… на улице Судзаку» | верно |
| qwen3.5-abliterated:9b | «Радзёмон» | ближе всех локальных, но мимо |
| qwen3.5:9b | «под Радзёном» | мимо |
| qwen3-abliterated:8b | «Рождественская дверь» (!) | грубая ошибка |
| qwen3:8b (ваниль) | «арка Розенмон» | мимо |
| qwen3-abliterated:30b-a3b | «Рашимон» | мимо |
| ruadapt-qwen3:8b | «дерево с цветами-колокольчиками» | галлюцинация |

Аналогично реалия 送灶 (проводы бога очага) локально либо теряется («проводили старый год»), либо галлюцинирует («петухи на кухне»); DeepSeek даёт «проводы бога очага». Простая динамичная проза («Беги, Мелос!») переводится приемлемо всеми — провалы концентрируются на именах, реалиях, идиомах.

**Эталон DeepSeek** (~2.2–2.9k токенов ≈ $0.0005/фрагмент, 13–22 с): уровень «черновик, пригодный редактору». Локальные: «черновик, требующий сверки с оригиналом». Разрыв качественный — не закрывается ни размером (30b не лучше 8b), ни поколением (3.5 лишь чуть точнее 3).

### Влияние thinking-режима на качество

Гипотеза (владельца): reasoning может починить понимание — реалии, длинные конструкции. Проверка на qwen3.5:9b (ja→ru, «Расёмон»).

**Мешает тесный контекст, не «зависание».** При `num_ctx 8192` (temp 0.3 и 0.6) think уходит в развёрнутый мета-анализ задачи (~7000 токенов рассуждений на английском: «Analyze the Request: Role… Task…»), забивает всё окно и обрывается по `length` **до перевода** — `response` пуст (детектор видел «0 символов»). Рассуждения при этом реально пишутся — просто ollama кладёт их в отдельное поле `thinking`.

**С достаточным контекстом think завершается и улучшает реалии.** `num_ctx 16384`, temp 0.6: 518 с, ~11k токенов рассуждений (39к символов) → перевод получен (2605 симв), `done_reason: stop`. Качество реалий заметно выше, чем без think:

| Реалия | без think | с think | эталон DeepSeek |
|---|---|---|---|
| 羅生門 | «Радзёмон» | **«Рашо-мон»** (узнаваемо) | «Расёмон» |
| 朱雀大路 | «улица Чжуцзянь» (кит. чтение) | **«улица Сёкаку»** (яп. чтение) | «улица Судзаку» |

Reasoning вытащил модель из грубых ошибок (унрекогнайзабл-транслит, китайское чтение кандзи) — **гипотеза частично подтвердилась: think трогает реалии.** Но: (1) даже с think локаль ниже API («Рашо-мон/Сёкаку» ≠ «Расёмон/Судзаку»); (2) цена запретительна — **8,6 мин и ~11k токенов рассуждений на 1400-знаковый фрагмент** (вебновелла 500 глав ≈ 200 ч локального счёта против 15 с/$0.0005 у DeepSeek за более точный результат). **Вывод: локально think для перевода нежизнеспособен по латентности.**

**Проброс гипотезы в облако (новая проверка для бэклога):** раз reasoning чинит реалии, включить think на быстрых облачных черновике/редакторе (DeepSeek/GLM, где это секунды) может поднять качество — проверить thinking-ON vs OFF по качеству (не только скорости) на облачном контуре.

## Сравнение пула dense-моделей

Выводы из таблицы «Скорость и память» + качества:

1. **Абляция бесплатна.** qwen3.5 vanilla vs abliterated: скорость идентична (24.2 vs 24.5), SFW-качество — тоже (abliterated даже чуть точнее на 羅生門). → для 18+-скрининга/чернового перевода берём abliterated без штрафа. (Доказательство слабое — 3 SFW-фрагмента; на explicit перепроверить.)
2. **Поколение 3.5 > 3 умеренно**: лучше понимает исходник (Радзё против Розен), на ~25% экономнее по токенам (0.28 vs 0.37 ток/симв), ценой −10% скорости.
3. **RuAdapt: токен-выигрыш подтверждён, качество перевода — нет.** 0.22 ток/симв (−40% токенов на тот же русский текст против ваниль-8b — заявленный выигрыш; при равных 27 tok/s это ~40% меньше времени на главу русского вывода). Но 羅生門 → «дерево с колокольчиками», zh-фрагмент — бессвязица: ru-адаптация переучила токенизатор/эмбеддинги и деградировала понимание zh/ja. **Роль RuAdapt (если будет) — финальная полировка русского стиля, где исходник уже не нужен, а не перевод.**

## Следствия для бэкенда

- **Роли локалки** (подтверждено): скрининг «горячести» 18+ (до отправки в облако), извлечение кандидатов в глоссарий, эмбеддинги, черновой NSFW-перевод под обязательную редактуру, дешёвый судья-гейт. Не финальный перевод.
- **Модельный выбор**: для понимающих ролей — qwen3.5-abliterated:9b (лучшее качество+свобода при равной цене); для скорости на массовых лёгких задачах — 8b; 30b-a3b — только фоновые несрочные задачи.
- **Thinking-режим**: роль переводчика на Qwen3.5+/GLM/Gemini обязана явно управлять reasoning (`think:false` или адекватный `num_predict`) — иначе пустой/обрезанный вывод и латентность ×3–5. Найдено дважды: здесь и в refusal-бенчмарке (эксп. 02).
- **Тайминги vojo**: дедлайн локальной ноги 45 с (под чат-ответы) несовместим с книжными чанками (63–278 с). Нужен свой профиль лег-таймаутов (~300–600 с по роли) + стриминг как keep-alive транспорта (уже в Р1). Иначе фейловер ложно роняет здоровую локалку.
- **Адаптер**: локалка = OpenAI-совместимый `/v1/chat/completions`; сэмплеры Qwen3 через `/v1`-слой ollama не пробрасываются (issue #11325) → запекать в Modelfile (паттерн qwen3-vojo) либо расширить запрос полями `top_k/min_p/repeat_penalty` для llama-server. `max_tokens` у ollama покрывает thinking+ответ вместе.
- **Health/failover** (из vojo, [architecture/03-implementation-notes.md](../architecture/03-implementation-notes.md)): проба `GET /v1/models` не проверяет резидентность модели (это обязанность `KEEP_ALIVE=-1`); для llama-server есть `/health`, `/slots`.

## Открытые вопросы / следующие шаги

- **NSFW-перевод** — единственная переводческая роль локалки — ещё не проверен (все замеры SFW). Ждёт explicit-фрагментов владельца.
- Замер bge-m3 (retrieval для банка памяти) — задача 5.
- При выходе abliterated Qwen3.6-35B-A3B — перемерить тяжёлую локалку.

## Приложение А. Сборка llama.cpp (CUDA, без root)

`/home/ubuntu/llama.cpp/build/bin/llama-server` (+`llama-cli`), commit 2d973636. User-space CUDA toolkit 12.6 в `/home/ubuntu/cuda-12.6` (nvcc из .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 выпилен).
Команда пересборки: `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`.
Модель 30b для llama.cpp берётся прямо из ollama blob-store (блоб = GGUF; путь через `ollama show --modelfile`).
