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

15 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).

Подготовка ко второму замеру (выполнено 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 — проверка обещанных 2530 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 (мини-бенчмарк эмбеддингов).