Apply external review fixes: snapshot local overrides, gate-enabled guard, TM-key doc note, db-lock gitignore, techdebt disposition
This commit is contained in:
parent
56670e3d67
commit
9d78385c37
7 changed files with 152 additions and 46 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -19,6 +19,7 @@ backend/dist/
|
|||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
*.db.lock
|
||||
|
||||
# Редакторы/ОС
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func TestCheckRunnableRejectsPhase2Mechanics(t *testing.T) {
|
|||
{"c2 rejected", Pipeline{Core: "C2", Stages: []Stage{{Name: "draft", Role: "translator"}}}, "C2"},
|
||||
{"fanout rejected", Pipeline{Core: "C1", Fanout: Fanout{Candidates: 3}, Stages: []Stage{{Name: "draft", Role: "translator"}}}, "fanout"},
|
||||
{"judge rejected", Pipeline{Core: "C1", Stages: []Stage{{Name: "select", Role: "judge"}}}, "judge"},
|
||||
{"enabled gate rejected", Pipeline{Core: "C1", Gates: Gates{Coverage: CoverageGate{Enabled: true}}, Stages: []Stage{{Name: "draft", Role: "translator"}}}, "gates"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
err := c.p.CheckRunnable()
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ func (p *Pipeline) CheckRunnable() error {
|
|||
if p.Fanout.Candidates > 1 {
|
||||
return fmt.Errorf("pipeline fanout.candidates=%d не исполним в Фазе 0 (fan-out N кандидатов — механика раннера Фазы 2; C1 = один черновик)", p.Fanout.Candidates)
|
||||
}
|
||||
// QA-гейты — механика Фазы 1; раннер Фазы 0 их НЕ исполняет. Включённый
|
||||
// гейт молча пропустил бы негейченный вывод как done (против Р7) —
|
||||
// поэтому fail-loud, а не fail-open (находка внешнего ревью F5).
|
||||
if p.gatesEnabled() {
|
||||
return fmt.Errorf("pipeline gates включены, но не исполнимы в Фазе 0 (QA-гейты — механика Фазы 1; иначе вывод пройдёт негейченным и будет ложно помечен done)")
|
||||
}
|
||||
for _, st := range p.Stages {
|
||||
if st.Role == "judge" {
|
||||
return fmt.Errorf("pipeline stage %q role=judge не исполним в Фазе 0 (селектор получает N кандидатов — механика Фазы 2, а раннер гонит стадии линейно)", st.Name)
|
||||
|
|
|
|||
|
|
@ -134,6 +134,13 @@ func (r *Runner) snapshotID() (id, payload string, err error) {
|
|||
// чекпоинты (находка ревью). json.Marshal карты сортирует ключи →
|
||||
// детерминированный рендер.
|
||||
ModelExtra json.RawMessage `json:"model_extra,omitempty"`
|
||||
// Провайдер-уровневые оверрайды (local kind переопределяет wire
|
||||
// temperature/max_tokens ПОСЛЕ вычисления request-hash) — тоже влияют
|
||||
// на фактический запрос; без них правка providers.local.max_tokens
|
||||
// давала бы тот же snapshot и ложный checkpoint-hit на стендовом
|
||||
// local-пути (находка внешнего ревью F2).
|
||||
ProviderTemp float64 `json:"provider_temp,omitempty"`
|
||||
ProviderMaxTok int `json:"provider_max_tok,omitempty"`
|
||||
}
|
||||
snap := struct {
|
||||
BriefHash string `json:"brief_hash"`
|
||||
|
|
@ -160,6 +167,9 @@ func (r *Runner) snapshotID() (id, payload string, err error) {
|
|||
PromptVersion: st.PromptVersion, PromptSHA256: r.templates[st.Name].SHA256,
|
||||
Temperature: st.Temperature, Reasoning: st.Reasoning,
|
||||
}
|
||||
if prov, ok := r.Models.Providers[r.Models.Models[st.Model].Provider]; ok {
|
||||
ss.ProviderTemp, ss.ProviderMaxTok = prov.Temperature, prov.MaxTokens
|
||||
}
|
||||
if extra := r.Models.Models[st.Model].ExtraBody; len(extra) > 0 {
|
||||
raw, merr := json.Marshal(extra)
|
||||
if merr != nil {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,17 @@ timeout-путь «оплачено-но-потеряно» (≤1 вызов, у
|
|||
|
||||
Не блокирует Фазу 0 (принята на DeepSeek+Z.ai); ответы нужны к старту Фазы 1 (эскалация, дефолты ролей).
|
||||
|
||||
### Внешнее ревью Фазы 0 — принято (04.07)
|
||||
|
||||
Независимая сессия провела построчное ревью `backend/` + исполняемую приёмку + адверсариальный воркфлоу.
|
||||
**Вердикт: Фаза 0 принята, блокеров ноль.** Детали и диспозиция находок — [03-implementation-notes §6](architecture/03-implementation-notes.md).
|
||||
Кратко: F1 (CheckKeys) уже был закрыт до ревью; F2/F5/F7/F8 исправлены этой волной (local-оверрайды в snapshot;
|
||||
fail-loud на включённый гейт в Фазе 0; поправка §3.4 под подтверждённый ключ TM; `*.db.lock` в .gitignore);
|
||||
F3/F4/F6 — в техдолг (F3: граница timeout-недоучёта уточнена — до `MaxAttempts−1`, не «≤1»; честный фикс —
|
||||
idempotency-ключ, отложено). Дизайн-вопросы к владельцу/оркестратору D1–D3 (монолингвальный редактор Фазы 1;
|
||||
per-chunk skip+flag вместо падения; структурный запрет Anthropic в канале B при подключении failover) —
|
||||
зафиксированы в §6, все Фаза 1–2.
|
||||
|
||||
Следующее: Фаза 1 (перевод книги целиком) — чанкер, банк памяти v1, гейты (пороги полигона готовы), режим онгоинга.
|
||||
|
||||
## Полигон
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ whitespace/Unicode NFC, **до** pre-replace терминов (иначе под
|
|||
чёрный ход, что прямо запрещено Р6). `prompt_version`, `model`, `glossary_version` — метаданные записи,
|
||||
НЕ ключ. Открытые вопросы политики инвалидации — [НУЖНО РЕШЕНИЕ] п.2.
|
||||
|
||||
> **ПОПРАВКА (оркестратор ответил, PROGRESS/Р6 — источник истины).** Этот абзац устарел: оркестратор
|
||||
> зафиксировал ключ TM `(chunk_src_hash, lang_pair, model_id, prompt_version, brief_hash,
|
||||
> context_snapshot_hash)` — то есть **`model_id` и `prompt_version` ВХОДЯТ в ключ**, а не в метаданные
|
||||
> (тюнинг промпта/смена модели легитимно инвалидируют TM; перечанкование — by design). Имплементеру Фазы 1:
|
||||
> строить ключ TM по PROGRESS/Р6, не по букве этого абзаца. В Фазе 0 это неважно (cross-book TM не
|
||||
> реализован — есть только checkpoint/resume request-hash, который `model`+`prompt_version` уже включает
|
||||
> через snapshot).
|
||||
|
||||
### 3.5 Нейтральный интерфейс LLM (расширения к типам vojo)
|
||||
|
||||
- `Message.CacheBoundary bool` — конец стабильного блока префикса. Anthropic-адаптер вешает `cache_control`
|
||||
|
|
@ -220,12 +228,39 @@ snapshot-pinning гейт с `--resnapshot`; flock-владение проект
|
|||
- **Дневной потолок — пер-книжный** (ledger в файле проекта одной книги): `day_usd` сбрасывается в UTC-полночь
|
||||
и ограничивает burn-rate одной книги, но не «на оператора за день по всем проектам». Общий потолок оператора
|
||||
потребует shared spend-файла — отложено (раскладка «файл БД на книгу» из Р3/Р6); пометить в UI при мультикниге.
|
||||
- **Timeout-путь «оплачено, но потеряно»**: если провайдер начал биллить 2xx, а соединение оборвалось по
|
||||
таймауту до получения тела, вызов повторяется — недоучёт на один вызов. Это неустранимо для любой системы с
|
||||
retry-on-timeout и укладывается в честные «≤1 вызов» (гарантия Р6 — про kill -9, не про billing на брошенном
|
||||
соединении). `BilledDecodeError` закрывает случай, когда тело ЧАСТИЧНО дошло; чистый таймаут до тела — нет.
|
||||
- **Retryable-но-оплаченные вызовы не идемпотентны** (уточнено внешним ревью F3): 2xx с НЕусечённым
|
||||
нечитаемым телом и чистый таймаут/обрыв до тела после серверного биллинга — retryable, поэтому цепочка
|
||||
ретраев может оплатить **до `MaxAttempts−1` лишних вызовов** (не «≤1», как я записал раньше — граница
|
||||
занижена была). Потолок к ним слеп; краха нет, триггер редкий (повторный оплаченный сбой). Честный фикс —
|
||||
idempotency-ключ на стороне провайдера (поддерживают не все), а не «сделать всё терминальным» (уронит
|
||||
прогон на транзиентном сбое прокси). Обсудить с владельцем/оркестратором; отложено до Фазы 1.
|
||||
- **finish=length с непустым обрезанным черновиком** (F4): течёт в edit с `OK=true` без `Degraded`-флага
|
||||
(раннер ловит только пустоту). Асимметрия наблюдаемости — свернуть в редизайн обработки негодного вывода
|
||||
Фазы 1 (та же семья, что «пустой ответ»): `Degraded:"truncated_length"` + WARN, или skip+flag по `attempt`.
|
||||
- **`prices_checked` (120д) гейтит read-only `report` и $0-resume** (F6): они платных вызовов не делают, но
|
||||
падают на устаревших ценах. Мелкая связанность — проверять свежесть цен только там, где будет платный вызов.
|
||||
- **cache_ttl ↔ cache_write_per_m**: цена записи в кэш в models.yaml задаётся под ОДИН TTL (у нас 5m). Если
|
||||
оператор поставит `cache_ttl: 1h`, реальная запись стоит ×2 (не ×1.25) — недоучёт. Боевой конфиг 5m совпадает;
|
||||
при переходе на 1h требуется отдельная цена. Валидацию/вторую цену — Фаза 1 (когда включим 1h осознанно).
|
||||
- **Гонка `Runner.clients` map**: ленивая инициализация клиентов не синхронизирована — безопасна в Фазе 0
|
||||
(последовательный проход стадий), но параллельные чанки/fan-out Фазы 1–2 потребуют мьютекса/предзагрузки.
|
||||
|
||||
**Внешнее (независимое) ревью Фазы 0 (2026-07-04).** Отдельная сессия провела построчное чтение всего
|
||||
`backend/` + исполняемую проверку приёмки + адверсариальный воркфлоу (22 агента). **Вердикт: Фаза 0 принята,
|
||||
блокеров ноль**; двухволновое селфревью подтверждено закрывшим реальные вещи; детерминизм-машинерия признана
|
||||
пропорциональной (не переинженерена). Диспозиция находок (F-нумерация ревью):
|
||||
- **F1** (CheckKeys префлайтит эскалационные ключи) — **уже исправлено** до ревью (сужен до достижимых моделей:
|
||||
стадии всегда, эскалация — только при включённом гейте; бэкенд заводится на 2 ключах).
|
||||
- **F2** (local-оверрайды temp/max_tokens вне snapshot), **F5** (fail-open на `gates.enabled=true`),
|
||||
**F7** (устаревший §3.4 vs подтверждённый ключ TM), **F8** (`*.db.lock` в .gitignore) — **исправлено этой
|
||||
волной** (F2/F5 — тесты добавлены; F7 — поправка-сноска выше в §3.4).
|
||||
- **F3/F4/F6** — в техдолг выше (обоснованно отложены).
|
||||
- **Дизайн-вопросы к владельцу/оркестратору (D1–D3):** **D1** — редактор C1 видит исходник (`{{text}}` в
|
||||
editor.md); для Фазы 1+ сделать редактора монолингвальным (только `{{draft}}`), верность → coverage-гейт +
|
||||
билингвальный судья (совпадает с ролевой моделью Р2); в Фазе 0 билингвальный редактор — защитимый интерим до
|
||||
появления гейта/судьи, но `{{text}}` убрать, когда они приедут. **D2** — «пустой/негодный ответ = падение
|
||||
всего прогона» неверно для Фазы 1 (много чанков); нужен per-chunk skip+flag (совпадает с Р4: ложное
|
||||
срабатывание не должно убивать многочасовую джобу) — закроет механизм `attempt`. **D3** — изоляция канала B
|
||||
в failover не принуждается кодом (пара primary/fallback задаётся вызывающим); при подключении failover
|
||||
(Фаза 2) нужен структурный запрет Anthropic в канале B, иначе пустой ответ local-abliterated молча уйдёт в
|
||||
Anthropic. Все три — Фаза 1–2, не блокеры Ф0.
|
||||
|
|
|
|||
|
|
@ -1,68 +1,110 @@
|
|||
# Эксперимент 03. Локальный стенд: скорость, память, качество, интеграция
|
||||
# Эксперимент 03. Локальный стенд: скорость, память, качество
|
||||
|
||||
Дата прогона: 2026-07-04. Статус: **первый замер выполнен** (ollama; llama.cpp `--n-cpu-moe` — следующий шаг). Основание: `docs/research/06-local-models.md`; задача 3 полигона.
|
||||
Дата: 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)
|
||||
|
||||
- Железо: 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.4–1.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-команд матчит собственный шелл.
|
||||
1. **Вердикт research/06 подтверждён: локалка — «спецслужба», не переводчик.** Ни одна из 6 протестированных моделей не передаёт имена собственные и реалии правильно (羅生門 → «Расёмон», 送灶 → «проводы бога очага»), тогда как DeepSeek API берёт их за ~$0.0005/фрагмент. Разрыв качественный, не градуальный. Роли локалки: скрининг «горячести» 18+, извлечение терминов, эмбеддинги, черновой NSFW-перевод под обязательную редактуру — не финальный перевод.
|
||||
2. **8–9B dense — рабочая лошадка стенда: ~24–27 tok/s, влезает в VRAM целиком (~6.6 ГБ).** Этого достаточно для «спецслужбных» ролей.
|
||||
3. **30B-A3B (MoE) — потолок ~13.5 tok/s, «Расхождение» с research/06** (обещано 25–30). Узкое место — пропускная способность 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.4–1.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) → локальные вызовы в обход прокси.
|
||||
|
||||
## Скорость и память
|
||||
|
||||
| Модель | Генерация, tok/s | Prefill, tok/s | Пик VRAM | Время на фрагмент |
|
||||
Dense-модели (ollama, thinking off), генерация усреднена по 3 фрагментам:
|
||||
|
||||
| Модель | Генерация, tok/s | Prefill, tok/s | Пик VRAM | ток/симв (ru-выход) |
|
||||
|---|---|---|---|---|
|
||||
| qwen3-abliterated:8b | **26.8–27.3** | 519–556 | ~6.6 GB (целиком в VRAM) | 63–93 с |
|
||||
| qwen3-vojo (8b, сэмплеры) | 26.5–26.7 | 521–599 | ~6.5 GB | 68–96 с |
|
||||
| qwen3-abliterated:30b-a3b | **9.9–10.0** | 73–302 | 7.6 GB + эксперты в RAM (mmap) | 164–278 с |
|
||||
| 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 | 73–302 | 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 ~14–20 с (из vojo-замеров), 30b — заметно дольше (первый фрагмент 278 с против 164 с у второго — разница в основном прогрев/подкачка 18GB с диска).
|
||||
- Замер RAM через MemAvailable недооценивает след 30b: веса подключаются mmap'ом и живут в page cache. Фактически модель занимает почти весь свободный запас WSL; параллельная тяжёлая нагрузка на RAM в момент работы 30b нежелательна.
|
||||
- Prefill 30b (73–302 tok/s) в разы медленнее 8b (~520) — для «прочитай много, выведи мало» ролей (скрининг, экстракция) 8b выгоднее и по этой оси.
|
||||
- 8b @ ~27 tok/s совпадает с референсом vojo-плана (~28 tok/s на этом железе). Холодная загрузка 8b ~14–20 с; 30b — заметно дольше (подкачка 18 ГБ с диска).
|
||||
- Prefill 30b (73–302) в разы медленнее 8b (~520) — для ролей «прочитай много, выведи мало» (скрининг, экстракция) 8b выгоднее и по этой оси.
|
||||
- След 30b по RAM через MemAvailable недооценён (веса mmap'ятся в page cache); фактически модель занимает почти весь свободный запас WSL.
|
||||
|
||||
## Расхождение с research/06: скорость MoE
|
||||
|
||||
Док 06 обещает **~25–30 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 обещает **25–30 tok/s** для 35B-A3B через llama.cpp `--n-cpu-moe`. Фактический потолок на GTX 1070 — **~13.5 tok/s** (таблица выше). llama.cpp честно лучше ollama (+35% генерация, ×2–3 prefill, полный контроль VRAM), но упирается не в рантайм, а в **железо**: при a3b каждый токен читает эксперты, и те, что в CPU-RAM (DDR4 ~40 ГБ/с на Pascal-платформе), — узкое место. Перенос слоёв экспертов на GPU (VRAM 2.9→7.8 ГБ) поднял генерацию лишь 11→13.5. Обещанные 25–30 — вероятно, RTX 3060 + DDR5 (процитированный Medium-пост платформу не уточнял).
|
||||
|
||||
Второе расхождение — **поколение моделей**: док 06 рекомендует Qwen3.5-9B / Qwen3.6-35B-A3B (март 2026), на стенде — предыдущее поколение Qwen3 (abliterated). huihui-ai уже публикует Huihui-Qwen3.5-35B-A3B-abliterated — при обновлении стенда качество ниже перемерить.
|
||||
**Следствие для Р4:** llama.cpp как рантайм подтверждён, но 30b-a3b на этом железе — только фоновые роли (13.5 tok/s ≈ 2.5 мин на главу вывода). Интерактив и объём — на 8–9B dense или API.
|
||||
|
||||
## Качество ja→ru / zh→ru (субъективно, с эталоном DeepSeek API)
|
||||
## Качество перевода
|
||||
|
||||
Переводы сохранены в `eval/data/local_bench/<model>/*.ru.txt`. Общая картина обоих размеров: **русский текст беглый и связный, но реалии и имена систематически ломаются**:
|
||||
Все замеры — на SFW-классике (Акутагава, Дадзай, Лу Синь). Общая картина: **русский текст беглый и связный, но имена собственные и культурные реалии систематически ломаются** — а это ровно то, что убивает доверие читателя.
|
||||
|
||||
- 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/`) закрывает вопрос:** те же фрагменты за 13–22 с (~2.2–2.9k токенов ≈ $0.0005/фрагмент) переведены с корректными реалиями — «ворота Расёмон… на улице Судзаку», «с которой местами облупилась киноварь» (местные модели дали «Рождественская дверь»/«Рашимон», «улица Чжуцзянь», «красная краска»), заголовок 祝福 — «Новогоднее жертвоприношение» (правильно; локально — пропуск/«Слава»). Разрыв качественный, не градуальный: у API — уровень «черновик, пригодный редактору», у локальных — «черновик, требующий сверки с оригиналом».
|
||||
| Источник | 羅生門 → | Оценка |
|
||||
|---|---|---|
|
||||
| **DeepSeek API** (эталон) | «ворота Расёмон… на улице Судзаку» | верно |
|
||||
| qwen3.5-abliterated:9b | «Радзёмон» | ближе всех локальных, но мимо |
|
||||
| qwen3.5:9b | «под Радзёном» | мимо |
|
||||
| qwen3-abliterated:8b | «Рождественская дверь» (!) | грубая ошибка |
|
||||
| qwen3:8b (ваниль) | «арка Розенмон» | мимо |
|
||||
| qwen3-abliterated:30b-a3b | «Рашимон» | мимо |
|
||||
| ruadapt-qwen3:8b | «дерево с цветами-колокольчиками» | галлюцинация |
|
||||
|
||||
**Вердикт дока 06 подтверждён эмпирически с эталоном: локалка — «спецслужба», не переводчик.** Для финальных ролей перевода непригодна (реалии/имена — ровно то, что убивает доверие читателя); для ролей скрининга «горячести», экстракции терминов, чернового NSFW-перевода под обязательную редактуру, дешёвого гейта — скорость и качество достаточны. Открытый вопрос: перевод NSFW-фрагментов (единственная переводческая роль локалки) — проверить на реальных 18+ фрагментах владельца, когда появятся в корпусе; пока все замеры — SFW-классика.
|
||||
Аналогично реалия 送灶 (проводы бога очага) локально либо теряется («проводили старый год»), либо галлюцинирует («петухи на кухне»); DeepSeek даёт «проводы бога очага». Простая динамичная проза («Беги, Мелос!») переводится приемлемо всеми — провалы концентрируются на именах, реалиях, идиомах.
|
||||
|
||||
**Расширение пула моделей (обсуждено с владельцем, план):** все три чат-модели стенда — абляции одного семейства 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).
|
||||
**Эталон DeepSeek** (~2.2–2.9k токенов ≈ $0.0005/фрагмент, 13–22 с): уровень «черновик, пригодный редактору». Локальные: «черновик, требующий сверки с оригиналом». Разрыв качественный — не закрывается ни размером (30b не лучше 8b), ни поколением (3.5 лишь чуть точнее 3).
|
||||
|
||||
## Интеграция в бэкенд: что взять из vojo/ai-bot
|
||||
### Влияние thinking-режима на качество
|
||||
|
||||
Конспект локального контура ai-bot (файлы `provider_local.go`, `failover.go`, `httpllm.go`, `config.go`; план `docs/plans/local_llm_backend.md`):
|
||||
Гипотеза: reasoning может починить именно понимание (реалии, длинные конструкции). Проверка — те же фрагменты на qwen3.5:9b и abliterated с включённым thinking и достаточным бюджетом вывода (6000 токенов).
|
||||
|
||||
- **Адаптер**: локалка = ещё один 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 с (под короткие чат-ответы). Наши книжные чанки на локалке занимают **63–278 с** — профиль таймаутов нужен свой (лег-дедлайн ~300–600 с по роли/модели) плюс стриминг в интерфейсе клиента (уже запланирован в Р1), иначе фейловер будет ложно ронять здоровую локалку.
|
||||
- `max_tokens` у ollama покрывает thinking+ответ вместе — для thinking-режимов задавать увеличенный потолок (у vojo 1024; для перевода главы нужно 4–8k) или выключать thinking (`reasoning_effort: none`).
|
||||
> _РЕЗУЛЬТАТ БУДЕТ ВПИСАН ПОСЛЕ ПРОГОНА (идёт)._
|
||||
|
||||
## Подготовка ко второму замеру (выполнено 2026-07-04, ~06:30)
|
||||
## Сравнение пула dense-моделей
|
||||
|
||||
- **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. **Абляция бесплатна.** 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 (если будет) — финальная полировка русского стиля, где исходник уже не нужен, а не перевод.**
|
||||
|
||||
1. **[после рестарта WSL]** Перемерить 30b-a3b: llama-server с `--n-cpu-moe` (веса можно грузить прямо из ollama blob-store — блобы это GGUF; путь через `ollama show`) против ollama при 26GB RAM — проверка обещанных 25–30 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 (мини-бенчмарк эмбеддингов).
|
||||
## Следствия для бэкенда
|
||||
|
||||
- **Роли локалки** (подтверждено): скрининг «горячести» 18+ (до отправки в облако), извлечение кандидатов в глоссарий, эмбеддинги, черновой NSFW-перевод под обязательную редактуру, дешёвый судья-гейт. Не финальный перевод.
|
||||
- **Модельный выбор**: для понимающих ролей — qwen3.5-abliterated:9b (лучшее качество+свобода при равной цене); для скорости на массовых лёгких задачах — 8b; 30b-a3b — только фоновые несрочные задачи.
|
||||
- **Thinking-режим**: роль переводчика на Qwen3.5+/GLM/Gemini обязана явно управлять reasoning (`think:false` или адекватный `num_predict`) — иначе пустой/обрезанный вывод и латентность ×3–5. Найдено дважды: здесь и в refusal-бенчмарке (эксп. 02).
|
||||
- **Тайминги vojo**: дедлайн локальной ноги 45 с (под чат-ответы) несовместим с книжными чанками (63–278 с). Нужен свой профиль лег-таймаутов (~300–600 с по роли) + стриминг как 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`).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue