textmachine/docs/experiments/03-local-stand.md

121 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Эксперимент 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. **89B dense — рабочая лошадка стенда: ~2427 tok/s, влезает в VRAM целиком (~6.6 ГБ).** Этого достаточно для «спецслужбных» ролей.
3. **30B-A3B (MoE) — потолок ~13.5 tok/s, «Расхождение» с research/06** (обещано 2530). Узкое место — пропускная способность 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.41.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 | 73302 | 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 ~1420 с; 30b — заметно дольше (подкачка 18 ГБ с диска).
- Prefill 30b (73302) в разы медленнее 8b (~520) — для ролей «прочитай много, выведи мало» (скрининг, экстракция) 8b выгоднее и по этой оси.
- След 30b по RAM через MemAvailable недооценён (веса mmap'ятся в page cache); фактически модель занимает почти весь свободный запас WSL.
## Расхождение с research/06: скорость MoE
Док 06 обещает **2530 tok/s** для 35B-A3B через llama.cpp `--n-cpu-moe`. Фактический потолок на GTX 1070 — **~13.5 tok/s** (таблица выше). llama.cpp честно лучше ollama (+35% генерация, ×23 prefill, полный контроль VRAM), но упирается не в рантайм, а в **железо**: при a3b каждый токен читает эксперты, и те, что в CPU-RAM (DDR4 ~40 ГБ/с на Pascal-платформе), — узкое место. Перенос слоёв экспертов на GPU (VRAM 2.9→7.8 ГБ) поднял генерацию лишь 11→13.5. Обещанные 2530 — вероятно, RTX 3060 + DDR5 (процитированный Medium-пост платформу не уточнял).
**Следствие для Р4:** llama.cpp как рантайм подтверждён, но 30b-a3b на этом железе — только фоновые роли (13.5 tok/s ≈ 2.5 мин на главу вывода). Интерактив и объём — на 89B 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.22.9k токенов ≈ $0.0005/фрагмент, 1322 с): уровень «черновик, пригодный редактору». Локальные: «черновик, требующий сверки с оригиналом». Разрыв качественный — не закрывается ни размером (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`) — иначе пустой/обрезанный вывод и латентность ×35. Найдено дважды: здесь и в refusal-бенчмарке (эксп. 02).
- **Тайминги vojo**: дедлайн локальной ноги 45 с (под чат-ответы) несовместим с книжными чанками (63278 с). Нужен свой профиль лег-таймаутов (~300600 с по роли) + стриминг как 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`).