обновлен процесс обработки данных

This commit is contained in:
Даниил Грабарь
2026-10-26 11:29:03 +10:00
parent 1059b47978
commit 879d90605c
6 changed files with 219 additions and 32 deletions
+26 -2
View File
@@ -1,6 +1,7 @@
import argparse
import asyncio
import json
import signal
from pathlib import Path
import sys
@@ -8,6 +9,18 @@ from .config import load_config
from .pipeline import generate, export, validate
async def run_generation(config, *, show_progress, teacher_only=False):
loop = asyncio.get_running_loop()
task = asyncio.current_task()
previous = signal.getsignal(signal.SIGTERM)
loop.add_signal_handler(signal.SIGTERM, task.cancel)
try:
return await generate(config, show_progress=show_progress, teacher_only=teacher_only)
finally:
loop.remove_signal_handler(signal.SIGTERM)
signal.signal(signal.SIGTERM, previous)
def main():
parser = argparse.ArgumentParser(description='Датасет высокоуровневых планов сборки: full / step / branch')
parser.add_argument('--config', type=Path, default=Path(__file__).resolve().parents[1] / 'config.yaml')
@@ -15,6 +28,8 @@ def main():
parser.add_argument('--count', type=int, help='Точное суммарное число SFT-записей; 0 допустим')
parser.add_argument('--output-dir', type=Path, help='Папка корпуса (относительно текущего каталога)')
parser.add_argument('--dry-run', action='store_true', help='Показать настройки без записи и запросов')
parser.add_argument('--teacher-only', action='store_true', help='Сохранять только проверенные ответы LLM; останавливать генерацию при символической замене')
parser.add_argument('--allow-partial', action='store_true', help='Экспортировать накопленные записи, даже если цель ещё не достигнута')
parser.add_argument('--no-progress', action='store_true', help='Отключить индикаторы прогресса')
args = parser.parse_args()
try:
@@ -32,11 +47,20 @@ def main():
if config.teacher.enabled:
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parents[1] / '.env')
asyncio.run(generate(config, show_progress=not args.no_progress))
try:
asyncio.run(run_generation(config, show_progress=not args.no_progress, teacher_only=args.teacher_only))
except (Exception, KeyboardInterrupt, asyncio.CancelledError) as exc:
print(f'Генерация остановлена ({type(exc).__name__}). Экспорт накопленных данных…', file=sys.stderr)
try:
saved = export(config, show_progress=not args.no_progress, allow_partial=True)
print(f"Сохранено {saved['examples']} записей: {config.output_dir / 'sft'}", file=sys.stderr)
except Exception as export_error:
print(f'Экспорт не выполнен ({type(export_error).__name__}); контрольные точки остаются в {config.output_dir / "episodes.sqlite3"}', file=sys.stderr)
return 130 if isinstance(exc, (KeyboardInterrupt, asyncio.CancelledError)) else 1
result = export(config, show_progress=not args.no_progress)
result = {k: result[k] for k in ('examples', 'scenarios', 'counts', 'teacher_failures')}
elif args.command == 'export':
result = export(config, show_progress=not args.no_progress)
result = export(config, show_progress=not args.no_progress, allow_partial=args.allow_partial)
result = {k: result[k] for k in ('examples', 'scenarios', 'counts', 'teacher_failures')}
else:
result = validate(config)
+43 -20
View File
@@ -53,12 +53,13 @@ def storage(config):
db.close()
async def generate(config: Config, agent=None, *, show_progress=False):
async def generate(config: Config, agent=None, *, show_progress=False, teacher_only=False):
with storage(config) as db:
count = db.execute('SELECT COALESCE(SUM(rows), 0) FROM episodes').fetchone()[0]
index = int(db.execute("SELECT value FROM meta WHERE key='next_index'").fetchone()[0])
resume_until = db.execute('SELECT COALESCE(MAX(seq), 0) FROM episodes').fetchone()[0]
with ProgressDisplay('Генерация', config.target_examples, initial=count, enabled=show_progress) as progress:
if count >= config.target_examples:
if count >= config.target_examples and index >= resume_until:
return count
if config.teacher.enabled and config.teacher.fraction > 0 and agent is None:
from .teacher import create_agent
@@ -66,28 +67,49 @@ async def generate(config: Config, agent=None, *, show_progress=False):
agent = await create_agent(config.teacher, config.prompts)
batch_size = config.teacher.concurrency if agent is not None else 32
duplicates = 0
while count < config.target_examples:
while count < config.target_examples or index < resume_until:
# Never construct 500k coroutines or keep the entire dataset in RAM.
size = min(batch_size, max(1, config.target_examples - count))
episodes = [make_episode(i, config) for i in range(index, index + size)]
async def save_episode(episode):
nonlocal count, duplicates
# A restarted batch can contain responses already committed before cancellation.
if db.execute('SELECT 1 FROM episodes WHERE signature=?',
(episode['signature'],)).fetchone():
duplicates += 1
return
if agent is not None:
from .teacher import enrich
episode, _ = await enrich(episode, config.teacher, agent, config.prompts)
if teacher_only and (config.teacher.llm is None or
episode['source']['provider'] != config.teacher.llm.provider or
'teacher_error' in episode):
raise ValueError('Режим --teacher-only: ответ модели не прошёл проверку; символическая замена не сохранена')
rows = sum(1 for _ in training_rows(episode, config))
# No await between receiving a validated response and committing it.
with db:
cursor = db.execute('INSERT OR IGNORE INTO episodes(seq,signature,payload,rows) VALUES (?,?,?,?)',
(episode['candidate_index'] + 1, episode['signature'], canonical(episode), rows))
if cursor.rowcount:
count += rows
duplicates = 0
else:
duplicates += 1
progress.update(count, status='ожидание LLM' if agent is not None else '')
if agent is not None:
from .teacher import enrich
progress.update(status='ожидание LLM')
episodes = [item[0] for item in await asyncio.gather(*(
enrich(e, config.teacher, agent, config.prompts) for e in episodes))]
tasks = [asyncio.create_task(save_episode(episode)) for episode in episodes]
try:
await asyncio.gather(*tasks)
finally:
for task in tasks:
if not task.done():
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
# Advance only after the whole batch succeeds; holes are retried on resume.
index += size
with db:
for episode in episodes:
rows = sum(1 for _ in training_rows(episode, config))
cursor = db.execute('INSERT OR IGNORE INTO episodes(signature,payload,rows) VALUES (?,?,?)',
(episode['signature'], canonical(episode), rows))
if cursor.rowcount:
count += rows
duplicates = 0
else:
duplicates += 1
index = episode['candidate_index'] + 1
if count >= config.target_examples:
break
db.execute("UPDATE meta SET value=? WHERE key='next_index'", (str(index),))
if duplicates > 10000:
raise ValueError('Пространство сценариев исчерпано: расширьте диапазон компонентов или domains')
@@ -140,10 +162,10 @@ def file_digest(path):
return hasher.hexdigest()
def export(config, *, show_progress=False):
def export(config, *, show_progress=False, allow_partial=False):
with storage(config) as db, ProgressDisplay('Экспорт', config.target_examples, enabled=show_progress) as progress:
capacity = db.execute('SELECT COALESCE(SUM(rows), 0) FROM episodes').fetchone()[0]
if capacity < config.target_examples:
if capacity < config.target_examples and not allow_partial:
raise ValueError('Недостаточно сценариев: сначала выполните generate')
destination = config.output_dir / 'sft'
backup = config.output_dir / '.sft-previous'
@@ -175,6 +197,7 @@ def export(config, *, show_progress=False):
progress.update(status="контрольные суммы")
writer.close()
manifest = {'schema_version': VERSION, 'examples': total, 'scenarios': scenarios,
'partial': total < config.target_examples,
'examples_per_variant': {'text': total, 'structured': total},
'counts': {f'{split}/{mode}': n for (split, mode), n in sorted(writer.counts.items())},
'domains': dict(domains), 'sources': dict(sources), 'teacher_failures': failures,
+3
View File
@@ -4,6 +4,7 @@ import re
from typing import Literal
from pydantic import Field, field_validator
from pydantic_ai.exceptions import UnexpectedModelBehavior
from llm.config import ConfigModel
from .prompts import render
from .language import validate_texts, action_text
@@ -63,6 +64,8 @@ async def enrich(episode, config, agent, prompts):
except Exception as exc:
# API exceptions may contain request headers. Persist the class only.
error = {'scenario_id': episode['scenario_id'], 'error_type': type(exc).__name__, 'attempts': attempt + 1}
if not isinstance(exc, (ValueError, UnexpectedModelBehavior)) and attempt == config.retries:
raise RuntimeError(f'Запрос LLM не выполнен: {type(exc).__name__}. Генерация остановлена.') from None
if isinstance(exc, ValueError):
prompt = render(prompts.teacher_retry, canonical({'instruction': episode['instruction'], 'scene': teacher_scene,
'validation_error': str(exc)[:500]}))