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

68 lines
15 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. Статус: **первый замер выполнен** (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 (мини-бенчмарк эмбеддингов).