textmachine/docs/experiments/03-local-stand.md
Claude (backend session) 9bb00d49f2 Initial commit: documentation, eval polygon, backend step 0 verdict
TextMachine project repository (AI translation of literary books).
Includes: v2 architecture decisions, MVP plan, research 01-12,
polygon experiments 01-03, backend-session revalidation verdict
(03-implementation-notes.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:50:59 +03:00

13 KiB
Raw Blame History

Эксперимент 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.41.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=<local> — WinINET-нотация, не работает в curl/urllib/Go) — все локальные вызовы в обход прокси; pkill -f из inline-команд матчит собственный шелл.

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

Модель Генерация, tok/s Prefill, tok/s Пик VRAM Время на фрагмент
qwen3-abliterated:8b 26.827.3 519556 ~6.6 GB (целиком в VRAM) 6393 с
qwen3-vojo (8b, сэмплеры) 26.526.7 521599 ~6.5 GB 6896 с
qwen3-abliterated:30b-a3b 9.910.0 73302 7.6 GB + эксперты в RAM (mmap) 164278 с

Замечания:

  • 8b @ ~27 tok/s совпадает с референсом из vojo-плана (~28 tok/s на этом же железе). Холодная загрузка 8b ~1420 с (из vojo-замеров), 30b — заметно дольше (первый фрагмент 278 с против 164 с у второго — разница в основном прогрев/подкачка 18GB с диска).
  • Замер RAM через MemAvailable недооценивает след 30b: веса подключаются mmap'ом и живут в page cache. Фактически модель занимает почти весь свободный запас WSL; параллельная тяжёлая нагрузка на RAM в момент работы 30b нежелательна.
  • Prefill 30b (73302 tok/s) в разы медленнее 8b (~520) — для «прочитай много, выведи мало» ролей (скрининг, экстракция) 8b выгоднее и по этой оси.

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

Док 06 обещает ~2530 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/<model>/*.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/) закрывает вопрос: те же фрагменты за 1322 с (~2.22.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 с (под короткие чат-ответы). Наши книжные чанки на локалке занимают 63278 с — профиль таймаутов нужен свой (лег-дедлайн ~300600 с по роли/модели) плюс стриминг в интерфейсе клиента (уже запланирован в Р1), иначе фейловер будет ложно ронять здоровую локалку.
  • max_tokens у ollama покрывает thinking+ответ вместе — для thinking-режимов задавать увеличенный потолок (у vojo 1024; для перевода главы нужно 48k) или выключать thinking (reasoning_effort: none).

Следующие шаги

  1. Поставить llama.cpp (llama-server), перемерить 30b-a3b с --n-cpu-moe — проверка обещанных 2530 tok/s (главное условие полезности «тяжёлой» локалки).
  2. Снять эталон DeepSeekсделано (см. выше): разрыв качественный, локалка остаётся «спецслужбой».
  3. Расширить пул по плану выше (ваниль 8b → Qwen3.5 → RuadaptQwen3), NSFW-фрагменты владельца — для проверки единственной переводческой роли локалки.
  4. Замер bge-m3 (скорость/качество retrieval) — в рамках задачи 5 (мини-бенчмарк эмбеддингов).