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

17 KiB
Raw Blame History

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

Дата: 2026-07-04. Задача 3 полигона. Основание: 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): проба 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).