Add polygon eval scripts for local-model, editor and MoE benchmarks plus ollama launcher and provider config

This commit is contained in:
Claude (backend session) 2026-07-04 19:26:36 +03:00
parent 1985c16d23
commit ce71a1a752
6 changed files with 460 additions and 6 deletions

View file

@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Генератор страницы слепого сравнения редакторов (эксперимент 04).
Читает eval/data/editor_bench/<fragment>/*.txt, применяет слепую разметку A/B/C/D,
эмитит самодостаточный HTML (контент для Artifact-обёртки). Запуск:
eval/.venv/bin/python eval/build_editor_artifact.py <out.html>
"""
import html
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
BENCH = ROOT / "data" / "editor_bench"
# Слепая разметка: фиксированный перемешанный порядок (одинаков на всех фрагментах)
BLIND = [("A", "grok-4.3"), ("B", "glm-4.6"), ("C", "gemini-3.1-pro"), ("D", "kimi-k2.6")]
FRAGMENTS = [
("rashomon", "японский → русский", "Акутагава Рюноскэ, «Расёмон»", False),
("ahq", "китайский → русский", "Лу Синь, «Подлинная история А-Кью»", False),
("merosu", "японский → русский", "Дадзай Осаму, «Беги, Мелос!»", False),
("wells", "английский → русский", "Г. Уэллс, «Машина времени»", True),
]
# стоимость/латентность из results.json (последний прогон) — для секции-расшифровки
COST = {"grok-4.3": "~13 с, без «размышлений» — самый быстрый",
"glm-4.6": "~105 с (thinking off)",
"gemini-3.1-pro": "~6086 с, обязательный thinking",
"kimi-k2.6": "~150 с, ~11 000 токенов «размышлений» на абзац — самый дорогой"}
def paras(text: str) -> str:
text = text.strip()
blocks = re.split(r"\n\s*\n", text) or [text]
if len(blocks) == 1:
blocks = [b for b in text.split("\n") if b.strip()]
return "".join(f"<p>{html.escape(b.strip())}</p>" for b in blocks if b.strip())
def read(frag: str, name: str) -> str:
f = BENCH / frag / f"{name}.txt"
return f.read_text() if f.exists() else ""
def main() -> None:
out = Path(sys.argv[1])
S = []
S.append("<title>Сравнение редакторов ru-стиля · Эксперимент 04</title>")
S.append(f"<style>{CSS}</style>")
S.append('<div class="wrap">')
S.append('<header class="intro">')
S.append('<p class="eyebrow">TextMachine · полигон · эксперимент 04</p>')
S.append("<h1>Кто лучше правит русский стиль</h1>")
S.append('<p class="lede">Четыре модели-редактора получили один и тот же черновой перевод и '
'задание: превратить его в живую художественную прозу, ничего не переврав. '
'Версии обезличены — <strong>A, B, C, D</strong>. Оцени по двум осям:</p>')
S.append('<ul class="criteria">'
'<li><span class="k k-style">стиль</span> — какая русская проза читается живее, без канцелярита и калек;</li>'
'<li><span class="k k-fidel">верность</span> — кто не потерял смысл и не добавил отсебятины (сверяй со «смысловым якорем»).</li>'
'</ul>')
S.append('<p class="note">Названия моделей и цена — внизу, под ключом-расшифровкой, чтобы не влияли на оценку. '
'Фрагмент Уэллса — англ.→рус.: оригинал перед тобой, судишь и стиль, и верность напрямую.</p>')
S.append("</header>")
for i, (frag, direction, work, is_en) in enumerate(FRAGMENTS, 1):
anchor = read(frag, "source") if is_en else read(frag, "english_ref")
anchor_label = "Оригинал (английский)" if is_en else "Смысловой якорь — дословный английский перевод"
draft = read(frag, "draft")
S.append('<section class="frag">')
S.append(f'<div class="frag-head"><span class="fnum">{i:02d}</span>'
f'<div><p class="eyebrow">{html.escape(direction)}</p>'
f'<h2>{html.escape(work)}</h2></div>'
+ ('<span class="badge">твой полный контроль</span>' if is_en else '') +
'</div>')
S.append('<details class="context"><summary>Смысловой якорь и черновик</summary>'
'<div class="context-grid">')
S.append(f'<div class="ctx"><p class="ctx-label">{html.escape(anchor_label)}</p>'
f'<div class="prose muted">{paras(anchor)}</div></div>')
S.append(f'<div class="ctx"><p class="ctx-label">Черновой перевод (вход редактора)</p>'
f'<div class="prose muted">{paras(draft)}</div></div>')
S.append("</div></details>")
S.append('<div class="versions">')
for letter, model in BLIND:
txt = read(frag, model)
S.append(f'<article class="ver"><header class="ver-head"><span class="chip">{letter}</span></header>'
f'<div class="prose">{paras(txt)}</div></article>')
S.append("</div>")
S.append("</section>")
# ключ-расшифровка + стоимость (свёрнуто)
S.append('<details class="reveal"><summary>Показать расшифровку и стоимость</summary>')
S.append('<table><thead><tr><th>Метка</th><th>Модель</th><th>Латентность / цена редактуры</th></tr></thead><tbody>')
for letter, model in BLIND:
S.append(f'<tr><td class="chip-cell"><span class="chip sm">{letter}</span></td>'
f'<td class="mono">{html.escape(model)}</td><td>{html.escape(COST.get(model, ""))}</td></tr>')
S.append("</tbody></table>")
S.append('<p class="note">Черновик — DeepSeek (на китайском А-Кью DeepSeek «эхал» оригинал, '
'поэтому там черновик от GLM — деталь для бэкенда, на сравнение редакторов не влияет). '
'Латентность — на коротком фрагменте; в продакшене всё идёт батчем.</p>')
S.append("</details>")
S.append('<footer class="foot"><p>Выбор дефолтного редактора для Фазы 1. '
'Твоя оценка — золотой стандарт: LLM-судьи на художественном качестве расходятся с людьми.</p></footer>')
S.append("</div>")
out.write_text("\n".join(S), encoding="utf-8")
print("written", out, out.stat().st_size, "bytes")
CSS = r"""
:root{
--paper:#f7f6f3; --surface:#fffffe; --ink:#1c1b19; --muted:#6f6b64;
--hair:#e4e0d8; --accent:#9e2b25; --accent-soft:#f0e2df;
--style:#3f6f52; --fidel:#9e2b25;
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
--sans:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
--measure:64ch;
}
@media (prefers-color-scheme:dark){
:root{--paper:#17181a; --surface:#1f2124; --ink:#e9e7e2; --muted:#9a978f;
--hair:#31343a; --accent:#d67a72; --accent-soft:#33231f;
--style:#7db38f; --fidel:#d67a72;}
}
:root[data-theme="light"]{--paper:#f7f6f3; --surface:#fffffe; --ink:#1c1b19; --muted:#6f6b64;
--hair:#e4e0d8; --accent:#9e2b25; --accent-soft:#f0e2df; --style:#3f6f52; --fidel:#9e2b25;}
:root[data-theme="dark"]{--paper:#17181a; --surface:#1f2124; --ink:#e9e7e2; --muted:#9a978f;
--hair:#31343a; --accent:#d67a72; --accent-soft:#33231f; --style:#7db38f; --fidel:#d67a72;}
*{box-sizing:border-box;}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
line-height:1.5;-webkit-font-smoothing:antialiased;}
.wrap{max-width:1080px;margin:0 auto;padding:clamp(1.2rem,4vw,3.5rem) clamp(1rem,4vw,2rem) 4rem;}
.eyebrow{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
font-weight:600;margin:0 0 .5rem;}
h1{font-family:var(--serif);font-weight:600;font-size:clamp(1.9rem,5vw,3rem);line-height:1.08;
margin:.1rem 0 .8rem;text-wrap:balance;letter-spacing:-.01em;}
.lede{font-size:1.08rem;max-width:var(--measure);color:var(--ink);margin:0 0 1rem;}
.criteria{list-style:none;padding:0;margin:0 0 1.2rem;max-width:var(--measure);
display:flex;flex-direction:column;gap:.4rem;}
.criteria li{padding-left:.2rem;color:var(--muted);}
.k{display:inline-block;font-family:var(--sans);font-size:.72rem;font-weight:700;text-transform:uppercase;
letter-spacing:.08em;padding:.12em .5em;border-radius:3px;margin-right:.4em;color:#fff;}
.k-style{background:var(--style);} .k-fidel{background:var(--fidel);}
.note{font-size:.9rem;color:var(--muted);max-width:var(--measure);}
.intro{border-bottom:1px solid var(--hair);padding-bottom:2rem;margin-bottom:2.5rem;}
.frag{margin:0 0 3.5rem;}
.frag-head{display:flex;align-items:baseline;gap:1rem;margin-bottom:1.1rem;flex-wrap:wrap;}
.fnum{font-family:var(--serif);font-size:1.5rem;color:var(--accent);font-weight:600;
font-variant-numeric:tabular-nums;line-height:1;}
.frag-head h2{font-family:var(--serif);font-weight:600;font-size:1.4rem;margin:.1rem 0 0;letter-spacing:-.01em;}
.badge{margin-left:auto;align-self:center;font-size:.7rem;font-weight:600;text-transform:uppercase;
letter-spacing:.08em;color:var(--accent);background:var(--accent-soft);
padding:.28em .7em;border-radius:999px;}
.context{margin:0 0 1.3rem;border:1px solid var(--hair);border-radius:10px;background:var(--surface);}
.context>summary{cursor:pointer;padding:.75rem 1rem;font-size:.82rem;font-weight:600;
text-transform:uppercase;letter-spacing:.06em;color:var(--muted);list-style:none;}
.context>summary::-webkit-details-marker{display:none;}
.context>summary::before{content:"";color:var(--accent);}
.context[open]>summary::before{content:"";}
.context-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:0 1rem 1.1rem;}
.ctx-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.07em;color:var(--accent);
font-weight:600;margin:0 0 .4rem;}
@media(max-width:680px){.context-grid{grid-template-columns:1fr;}}
.versions{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem;}
@media(max-width:760px){.versions{grid-template-columns:1fr;}}
.ver{background:var(--surface);border:1px solid var(--hair);border-radius:12px;
padding:1.1rem 1.3rem 1.3rem;position:relative;}
.ver-head{margin:0 0 .5rem;}
.chip{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;
font-family:var(--serif);font-weight:700;font-size:1.05rem;color:#fff;background:var(--accent);
border-radius:50%;}
.chip.sm{width:1.6rem;height:1.6rem;font-size:.85rem;}
.prose{font-family:var(--serif);font-size:1.02rem;line-height:1.62;max-width:var(--measure);}
.prose p{margin:0 0 .75em;}
.prose p:last-child{margin-bottom:0;}
.prose.muted{color:var(--muted);font-size:.94rem;line-height:1.55;}
.reveal{margin:2rem 0 0;border:1px solid var(--hair);border-radius:10px;background:var(--surface);}
.reveal>summary{cursor:pointer;padding:.9rem 1.1rem;font-weight:600;color:var(--accent);list-style:none;}
.reveal>summary::-webkit-details-marker{display:none;}
.reveal>summary::before{content:"🔑 ";}
.reveal table{width:100%;border-collapse:collapse;margin:0 0 .5rem;}
.reveal th,.reveal td{text-align:left;padding:.55rem 1.1rem;border-top:1px solid var(--hair);font-size:.92rem;}
.reveal th{font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;}
.chip-cell{width:3rem;} .mono{font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:.88rem;}
.reveal .note{padding:0 1.1rem 1rem;}
.foot{margin-top:2.5rem;padding-top:1.5rem;border-top:1px solid var(--hair);color:var(--muted);font-size:.9rem;}
a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px;}
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;}}
"""
if __name__ == "__main__":
main()

164
eval/editor_bench.py Normal file
View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Эксперимент 04: бейк-офф редакторов ru-стиля (задача из PROGRESS, вопрос бэкенда №2).
Пайплайн: DeepSeek делает ЧЕРНОВИК перевода каждый редактор-кандидат полирует его
в живой литературный русский (чек-лист Норы Галь: убрать канцелярит, кальки, оживить
синтаксис), НЕ меняя смысл и не теряя предложений. Выходы сохраняются для сравнения
(человеческая оценка владельца золотой стандарт для худ. качества, LAIT).
Запуск: eval/.venv/bin/python eval/editor_bench.py
Выход: eval/data/editor_bench/<fragment>/{source,draft,<editor>}.txt + results.json
"""
from __future__ import annotations
import json
import re
import sys
import time
from pathlib import Path
CJK_RE = re.compile("[一-鿿぀-ヿ]")
def cjk_share(text: str) -> float:
return len(CJK_RE.findall(text)) / max(1, len(text))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import call_provider, load_env_file # noqa: E402
load_env_file()
ROOT = Path(__file__).resolve().parent
# Черновик — реальный черновой контур пайплайна
DRAFT = {"name": "deepseek", "base_url": "https://api.deepseek.com/v1",
"model": "deepseek-chat", "api_key_env": "DEEPSEEK_API_KEY",
"temperature": 0.3, "max_tokens": 3000}
# Фолбэк-черновик, если основной «эхнул» оригинал (deepseek-chat так делает на плотной zh-прозе)
DRAFT_FALLBACK = {"name": "glm-draft", "base_url": "https://api.z.ai/api/paas/v4",
"model": "glm-4.6", "api_key_env": "ZAI_API_KEY", "temperature": 0.3,
"max_tokens": 4000, "extra_body": {"thinking": {"type": "disabled"}}}
# Редакторы-кандидаты (дорогой тир; имена моделей сверены с /models 2026-07-04)
EDITORS = [
{"name": "glm-4.6", "base_url": "https://api.z.ai/api/paas/v4", "model": "glm-4.6",
"api_key_env": "ZAI_API_KEY", "extra_body": {"thinking": {"type": "disabled"}}},
{"name": "kimi-k2.6", "base_url": "https://api.moonshot.ai/v1", "model": "kimi-k2.6",
"api_key_env": "KIMI_API_KEY", "temperature": 1, "max_tokens": 16000},
# Kimi K2.6: только temp=1; ОЧЕНЬ многословная думалка (~11k reasoning-токенов на абзац!) →
# бюджет ≥16000, иначе content пуст (finish=length). Дорогой редактор: reasoning биллится как выход
{"name": "gemini-3.1-pro", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
"model": "gemini-3.1-pro-preview", "api_key_env": "GEMINI_API_KEY",
"max_tokens": 16000}, # обязательно-думающая: thinking не отключить; на длинном черновике 8k обрезал ответ
{"name": "grok-4.3", "base_url": "https://api.x.ai/v1", "model": "grok-4.3",
"api_key_env": "XAI_API_KEY"},
]
for e in EDITORS:
e.setdefault("temperature", 0.4)
e.setdefault("max_tokens", 4000)
# Фрагменты: (id, lang, файл, срез) — разные исходники и стили.
# wells — en→ru: владелец читает английский, поэтому оценит и стиль, И верность на 100%.
FRAGMENTS = [
("rashomon", "японского", "ja/akutagawa-rashomon.txt", 1400),
("ahq", "китайского", "zh/luxun-ah-q-ch1-4.txt", 1300), # нарративнее zhufu (тот deepseek эхает)
("merosu", "японского", "ja/dazai-hashire-merosu.txt", 1400),
("wells", "английского", "en/wells-time-machine-ch1-2.txt", 1400),
]
# Английский эталон-подстрочник для ja/zh — читаемый владельцем якорь смысла
# (сверить верность русских версий). Для en-исходника не нужен (сам оригинал английский).
ENREF_SYS = ("Translate this fiction fragment from {src} into faithful, literal English. "
"Preserve every sentence and all proper nouns/cultural terms. Output only the translation.")
DRAFT_SYS = ("Ты переводчик. Переведи фрагмент художественной прозы с {src} на русский "
"буквально и точно, сохрани все предложения. Выведи только перевод.")
EDITOR_SYS = (
"Ты — литературный редактор русского художественного перевода уровня издательства. "
"Тебе дают ОРИГИНАЛ (на {src}) и его ЧЕРНОВОЙ подстрочный перевод. Отредактируй "
"русский текст в живую художественную прозу: убери канцелярит и кальки с оригинала, "
"разнообразь синтаксис, добавь естественную русскую идиоматику и ритм — по духу школы "
"Норы Галь. СТРОГО сохрани смысл, все предложения, реалии и имена собственные из черновика. "
"Не добавляй отсебятины. Выведи ТОЛЬКО отредактированный русский текст, без комментариев."
)
def read_frag(rel: str, n: int) -> str:
return (ROOT / "data" / "samples" / rel).read_text()[:n]
def main() -> None:
out_dir = ROOT / "data" / "editor_bench"
out_dir.mkdir(parents=True, exist_ok=True)
results = []
for fid, src_name, rel, n in FRAGMENTS:
fdir = out_dir / fid
fdir.mkdir(exist_ok=True)
source = read_frag(rel, n)
(fdir / "source.txt").write_text(source)
print(f"\n=== {fid} ({src_name}) ===")
# 0. английский эталон смысла (для ja/zh; владелец сверяет верность по нему)
enref_path = fdir / "english_ref.txt"
if src_name != "английского" and not (enref_path.exists() and enref_path.read_text().strip()):
en_src = {"src": src_name.replace("японского", "Japanese").replace("китайского", "Chinese")}
text, err, _ = call_provider(DRAFT, ENREF_SYS.format(**en_src), source, timeout=180)
if text:
enref_path.write_text(text.strip())
print(f" english_ref: {len(text)} симв")
# 1. черновик DeepSeek
draft_path = fdir / "draft.txt"
if draft_path.exists() and draft_path.read_text().strip():
draft = draft_path.read_text()
print(" draft: из кэша")
else:
draft = None
for attempt in range(3): # ретрай на «эхо» (модель вернула оригинал вместо перевода)
t0 = time.time()
cand, err, usage = call_provider(
DRAFT, DRAFT_SYS.format(src=src_name), source, timeout=180)
if not cand:
print(f" draft ОШИБКА: {err[:120]}"); break
if cjk_share(cand) > 0.15:
print(f" draft эхо ({cjk_share(cand):.0%} CJK), ретрай {attempt+1}")
continue
draft = cand
draft_path.write_text(draft)
print(f" draft ({DRAFT['name']}): {time.time()-t0:.0f}с, {len(draft)} симв")
break
if not draft: # основной эхает стабильно → фолбэк-черновик (GLM)
cand, err, _ = call_provider(
DRAFT_FALLBACK, DRAFT_SYS.format(src=src_name), source, timeout=180)
if cand and cjk_share(cand) <= 0.15:
draft = cand
draft_path.write_text(draft)
print(f" draft (фолбэк {DRAFT_FALLBACK['name']}): {len(draft)} симв")
else:
print(" draft не удался даже фолбэком — фрагмент пропущен"); continue
# 2. каждый редактор полирует черновик
user = f"ОРИГИНАЛ ({src_name}):\n{source}\n\nЧЕРНОВОЙ ПЕРЕВОД:\n{draft}"
for ed in EDITORS:
ep = fdir / f"{ed['name']}.txt"
if ep.exists() and ep.read_text().strip():
print(f" {ed['name']}: из кэша"); continue
t0 = time.time()
text, err, usage = call_provider(ed, EDITOR_SYS.format(src=src_name), user, timeout=300)
if not text:
print(f" {ed['name']}: ОШИБКА/пусто — {err or 'empty'}")
results.append({"fragment": fid, "editor": ed["name"], "error": err or "empty"})
continue
ep.write_text(text.strip())
el = time.time() - t0
results.append({"fragment": fid, "editor": ed["name"], "elapsed_s": round(el, 1),
"chars": len(text), "usage": usage})
print(f" {ed['name']}: {el:.0f}с, {len(text)} симв")
(out_dir / "results.json").write_text(json.dumps(results, ensure_ascii=False, indent=1))
print(f"\nГотово. Выходы: {out_dir}/<fragment>/*.txt")
if __name__ == "__main__":
main()

43
eval/llama_moe_bench.sh Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Замер 30b-a3b MoE через llama-server с офлоадом экспертов на CPU (--n-cpu-moe / --cpu-moe).
# Сравнение с ollama (дал ~10 tok/s). Использование: bash eval/llama_moe_bench.sh [NCMOE]
# NCMOE пусто → --cpu-moe (все эксперты на CPU); число → --n-cpu-moe N (первые N слоёв).
set -u
BIN=/home/ubuntu/llama.cpp/build/bin/llama-server
GGUF=/home/ubuntu/.ollama/models/blobs/sha256-11b3b371aedb6c43d438ec010e66f9dfaa92127d64c22499891d9013ab7a6ee6
PORT=8080
LOG=/home/ubuntu/llama-moe.log
export NO_PROXY="localhost,127.0.0.1" no_proxy="localhost,127.0.0.1"
NCMOE_ARG="--cpu-moe"
[ $# -ge 1 ] && NCMOE_ARG="--n-cpu-moe $1"
pkill -f "llama-server" 2>/dev/null || true; sleep 2
echo "== старт llama-server: -ngl 99 $NCMOE_ARG (лог $LOG) =="
setsid nohup "$BIN" -m "$GGUF" -ngl 99 $NCMOE_ARG -c 8192 -fa on \
--host 127.0.0.1 --port $PORT --no-webui >"$LOG" 2>&1 < /dev/null &
# ждём готовности (загрузка 18GB весов — до ~90с)
for _ in $(seq 1 180); do
curl -s --noproxy '*' --max-time 1 "http://127.0.0.1:$PORT/health" 2>/dev/null | grep -q '"ok"' && break
sleep 1
done
if ! curl -s --noproxy '*' --max-time 2 "http://127.0.0.1:$PORT/health" | grep -q '"ok"'; then
echo "СЕРВЕР НЕ ПОДНЯЛСЯ:"; tail -15 "$LOG"; exit 1
fi
echo "== VRAM после загрузки =="; nvidia-smi --query-gpu=memory.used --format=csv,noheader
# Нарезка ПОСИМВОЛЬНО (не head -c — тот режет по байтам и рубит иероглиф → битый JSON)
BODY=$(/home/ubuntu/projects/textmachine/eval/.venv/bin/python -c '
import json
sys_p = "Ты профессиональный литературный переводчик с китайского на русский. Переведи фрагмент, сохрани все предложения. Выведи только перевод."
frag = open("/home/ubuntu/projects/textmachine/eval/data/samples/zh/luxun-zhufu.txt", encoding="utf-8").read()[:1500]
print(json.dumps({"model":"local","temperature":0.3,"max_tokens":2000,
"messages":[{"role":"system","content":sys_p},{"role":"user","content":frag}]}))')
echo "== генерация (zh→ru, 1500 знаков) =="
curl -s --noproxy '*' --max-time 600 "http://127.0.0.1:$PORT/v1/chat/completions" \
-H "Content-Type: application/json" -d "$BODY" > /home/ubuntu/llama-moe-resp.json
echo "-- tok/s из лога llama-server --"; grep -iE "prompt eval time|eval time|tokens per second" "$LOG" | tail -6
echo "-- первые 400 симв. перевода --"
/home/ubuntu/projects/textmachine/eval/.venv/bin/python -c "import json; print(json.load(open('/home/ubuntu/llama-moe-resp.json'))['choices'][0]['message']['content'][:400])"
pkill -f "llama-server" 2>/dev/null || true

View file

@ -19,6 +19,7 @@ import re
import subprocess import subprocess
import threading import threading
import time import time
import urllib.error
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
@ -95,16 +96,34 @@ class PeakSampler(threading.Thread):
time.sleep(1) time.sleep(1)
def ollama_generate(model: str, prompt: str, num_ctx: int, timeout: int = 1800) -> dict: def ollama_generate(model: str, prompt: str, num_ctx: int, timeout: int = 1800,
think: bool = False) -> dict:
# think=False (дефолт): у моделей 3.5/GLM thinking включён по умолчанию и съедает
# бюджет вывода на рассуждения (перевод обрезается/пустой) — для роли переводчика
# отключаем. think=True: явно включаем + большой num_predict, чтобы модель успела
# и порассуждать, и выдать перевод (замер влияния reasoning на качество).
# num_predict — потолок вывода, чтобы модель не убегала.
req = urllib.request.Request( req = urllib.request.Request(
f"{OLLAMA}/api/generate", f"{OLLAMA}/api/generate",
data=json.dumps({ data=json.dumps({
"model": model, "prompt": prompt, "stream": False, "model": model, "prompt": prompt, "stream": False, "think": think,
"options": {"num_ctx": num_ctx, "temperature": 0.3}, "options": {"num_ctx": num_ctx, "temperature": 0.3,
"num_predict": 6000 if think else 2048},
}).encode(), }).encode(),
headers={"Content-Type": "application/json"}) headers={"Content-Type": "application/json"})
with _NO_PROXY_OPENER.open(req, timeout=timeout) as resp: try:
return json.loads(resp.read()) with _NO_PROXY_OPENER.open(req, timeout=timeout) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
# не все модели принимают think=False (нет thinking-способности) — повтор без него
if e.code == 400:
body = json.loads(req.data)
body.pop("think", None)
req2 = urllib.request.Request(f"{OLLAMA}/api/generate", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
with _NO_PROXY_OPENER.open(req2, timeout=timeout) as resp:
return json.loads(resp.read())
raise
def strip_think(text: str) -> str: def strip_think(text: str) -> str:
@ -138,6 +157,8 @@ def main() -> None:
ap.add_argument("--models", help="через запятую; по умолчанию все установленные") ap.add_argument("--models", help="через запятую; по умолчанию все установленные")
ap.add_argument("--num-ctx", type=int, default=8192) ap.add_argument("--num-ctx", type=int, default=8192)
ap.add_argument("--out", default=str(ROOT / "data" / "local_bench")) ap.add_argument("--out", default=str(ROOT / "data" / "local_bench"))
ap.add_argument("--think", action="store_true",
help="включить thinking-режим (reasoning) + большой num_predict — замер влияния на качество")
args = ap.parse_args() args = ap.parse_args()
models = args.models.split(",") if args.models else installed_models() models = args.models.split(",") if args.models else installed_models()
@ -160,7 +181,7 @@ def main() -> None:
sampler.start() sampler.start()
t0 = time.time() t0 = time.time()
try: try:
r = ollama_generate(model, prompt, args.num_ctx) r = ollama_generate(model, prompt, args.num_ctx, think=args.think)
except Exception as e: except Exception as e:
sampler.stop_flag = True sampler.stop_flag = True
print(f" {fname}: ОШИБКА {type(e).__name__}: {str(e)[:200]}") print(f" {fname}: ОШИБКА {type(e).__name__}: {str(e)[:200]}")

16
eval/ollama-up.sh Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Запуск ollama в боевом конфиге стенда (как в ~/vojo-local-up.sh, без туннеля/релея).
# Флаги: держать модель в VRAM, flash attention, q8 KV-кэш. Прокси для localhost — в обход.
# Использование: bash eval/ollama-up.sh
set -u
LOG="${OLLAMA_LOG:-$HOME/ollama.log}"
pkill -f "ollama serve" 2>/dev/null || true
sleep 2
export OLLAMA_HOST=0.0.0.0:11434 OLLAMA_KEEP_ALIVE=-1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0
export NO_PROXY="localhost,127.0.0.1,0.0.0.0,::1" no_proxy="localhost,127.0.0.1,0.0.0.0,::1"
setsid nohup ollama serve >"$LOG" 2>&1 < /dev/null &
for _ in $(seq 1 40); do
curl -s --noproxy '*' --max-time 1 http://127.0.0.1:11434/api/version >/dev/null 2>&1 && break
sleep 0.5
done
curl -s --noproxy '*' http://127.0.0.1:11434/api/version && echo " <- ollama up (FA=1, KV=q8_0, keep_alive=-1); log: $LOG"

View file

@ -33,6 +33,14 @@
"extra_body": { "thinking": { "type": "disabled" } }, "extra_body": { "thinking": { "type": "disabled" } },
"_comment": "thinking у GLM-4.6 включён по умолчанию и не влезал в 180с — для перевода отключаем" "_comment": "thinking у GLM-4.6 включён по умолчанию и не влезал в 180с — для перевода отключаем"
}, },
{
"name": "kimi",
"base_url": "https://api.moonshot.ai/v1",
"model": "kimi-k2.6",
"api_key_env": "KIMI_API_KEY",
"rps_delay": 0.5,
"_comment": "Moonshot Kimi K2.6 — кандидат в редакторы zh-исходников (Р4); интернациональный endpoint api.moonshot.ai (не .cn)"
},
{ {
"name": "gemini", "name": "gemini",
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai",