aiflow-math-ink-06-intermediate / scripts /run_math_ink_06_p_formula_release.py
cwLeeDev's picture
Add fail-closed P formula release and student export
2dde02d verified
Raw
History Blame Contribute Delete
11 kB
"""P Formula 3-seed ํ•™์Šต๋ถ€ํ„ฐ single-student export๊นŒ์ง€ ์‹คํŒจ ํ์‡„๋กœ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ํ•œ๋‹ค."""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import subprocess
import sys
from typing import Any, Sequence
PROJECT_ROOT = Path(__file__).parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src"
for path in (PROJECT_ROOT, SOURCE_ROOT):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
from math_grid_drawer.research.external_corpus import read_jsonl
from math_grid_drawer.research.p_formula_gate06 import audit_p_formula_records06
from scripts.train_math_ink_06_p_formula_adapter import _file_sha25606
REQUIRED_SEEDS06 = (17, 31, 47)
def parse_seed_adapters06(values: Sequence[str]) -> dict[int, Path]:
"""ํ•„์š” ๋ณ€์ˆ˜: `seed=checkpoint` ๋ฌธ์ž์—ด. ์ž‘๋™ ์›๋ฆฌ: 17ยท31ยท47์˜ ์ค‘๋ณต ์—†๋Š” adapter mapping์„ ๋งŒ๋“ ๋‹ค."""
adapters: dict[int, Path] = {}
for value in values:
seed_text, separator, path_text = value.partition("=")
if not separator or not path_text:
raise ValueError("adapter๋Š” `17=path/to/adapter.pt` ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
try:
seed = int(seed_text)
except ValueError as error:
raise ValueError(f"adapter seed๊ฐ€ ์ •์ˆ˜๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค: {seed_text}") from error
if seed in adapters:
raise ValueError(f"adapter seed๊ฐ€ ์ค‘๋ณต๋˜์—ˆ์Šต๋‹ˆ๋‹ค: {seed}")
adapters[seed] = Path(path_text)
if set(adapters) != set(REQUIRED_SEEDS06):
raise ValueError("seed 17ยท31ยท47 adapter๊ฐ€ ์ •ํ™•ํžˆ ํ•˜๋‚˜์”ฉ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
return adapters
def build_p_formula_release_commands06(
*,
python: Path,
data: Path,
adapters: dict[int, Path],
output: Path,
recipe: dict[str, Any],
convert_litert: bool,
) -> list[list[str]]:
"""ํ•„์š” ๋ณ€์ˆ˜: ์‹คํ–‰๊ธฐยท๋™์ผ P corpusยทseed adapterยทrecipe. ์ž‘๋™ ์›๋ฆฌ: ํ•™์Šตโ†’์š”์•ฝโ†’์ฆ๋ฅ˜โ†’export ์ˆœ์„œ์˜ argv๋ฅผ ๊ณ ์ •ํ•œ๋‹ค."""
training = recipe["training"]
seed_gate = recipe["seed_gate"]
source_minimum = int(recipe["input"]["minimum_independent_sources"])
commands: list[list[str]] = []
for seed in REQUIRED_SEEDS06:
commands.append([
str(python),
str(PROJECT_ROOT / "scripts/train_math_ink_06_p_formula_adapter.py"),
"--data", str(data),
"--adapter", str(adapters[seed]),
"--output", str(output / f"seed{seed}"),
"--seed", str(seed),
"--epochs", str(training["epochs"]),
"--batch-size", str(training["batch_size"]),
"--learning-rate", str(training["learning_rate"]),
"--weight-decay", str(training["weight_decay"]),
"--exact-loss-weight", str(training["exact_loss_weight"]),
"--context-dropout", str(training["context_dropout"]),
"--hidden-size", str(recipe["model"]["hidden_size"]),
"--patience", str(training["patience"]),
"--minimum-independent-sources", str(source_minimum),
"--top1-minimum", str(seed_gate["exact_top1_minimum"]),
"--top5-minimum", str(seed_gate["exact_top5_minimum"]),
"--macro-f1-minimum", str(seed_gate["macro_f1_minimum"]),
"--writer-floor-minimum", str(seed_gate["writer_floor_minimum"]),
"--missing-drop-maximum-pp",
str(seed_gate["missing_metadata_drop_maximum_pp"]),
"--device", str(training["device"]),
])
summary = output / "three_seed_summary.json"
summary_command = [
str(python),
str(PROJECT_ROOT / "scripts/summarize_math_ink_06_p_formula_seeds.py"),
]
for seed in REQUIRED_SEEDS06:
summary_command.extend(["--report", str(output / f"seed{seed}/report.json")])
summary_command.extend(["--output", str(summary)])
commands.append(summary_command)
distillation = recipe["distillation"]
student_dir = output / "student"
distill_command = [
str(python),
str(PROJECT_ROOT / "scripts/distill_math_ink_06_p_formula_student.py"),
"--data", str(data),
]
for seed in REQUIRED_SEEDS06:
distill_command.extend([
"--teacher-report",
str(output / f"seed{seed}/report.json"),
])
distill_command.extend([
"--summary", str(summary),
"--student-adapter", str(adapters[int(distillation["student_seed"])]),
"--output", str(student_dir),
"--seed", str(distillation["student_seed"]),
"--epochs", str(training["epochs"]),
"--batch-size", str(training["batch_size"]),
"--learning-rate", str(training["learning_rate"]),
"--weight-decay", str(training["weight_decay"]),
"--hidden-size", str(distillation["student_formula_adapter_hidden_size"]),
"--temperature", str(distillation["temperature"]),
"--teacher-exact-weight", str(distillation["teacher_exact_weight"]),
"--teacher-family-weight", str(distillation["teacher_family_weight"]),
"--hard-exact-weight", str(distillation["hard_exact_weight"]),
"--hard-family-weight", str(distillation["hard_family_weight"]),
"--patience", str(training["patience"]),
"--minimum-independent-sources", str(source_minimum),
"--distillation-regression-maximum-pp",
str(distillation["maximum_teacher_to_student_regression_pp"]),
"--device", str(training["device"]),
])
commands.append(distill_command)
export_command = [
str(python),
str(PROJECT_ROOT / "scripts/export_math_ink_06_p_formula_student.py"),
"--student-checkpoint", str(student_dir / "p_formula_student_adapter.pt"),
"--data", str(data),
"--output", str(output / "export"),
]
if convert_litert:
export_command.append("--convert-litert")
commands.append(export_command)
return commands
def _write_json06(path: Path, value: dict[str, Any]) -> None:
"""ํ•„์š” ๋ณ€์ˆ˜: ๋ชฉํ‘œ ๊ฒฝ๋กœยทJSON ๊ฐ’. ์ž‘๋™ ์›๋ฆฌ: UTF-8 ์ž„์‹œ ํŒŒ์ผ์„ ์›์ž์ ์œผ๋กœ ๊ต์ฒดํ•œ๋‹ค."""
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".part")
temporary.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
temporary.replace(path)
def _require_gate06(path: Path, keys: Sequence[str]) -> dict[str, Any]:
"""ํ•„์š” ๋ณ€์ˆ˜: ๋‹จ๊ณ„ reportยท์ค‘์ฒฉ boolean ๊ฒฝ๋กœ. ์ž‘๋™ ์›๋ฆฌ: ์‚ฐ์ถœ๋ฌผ ๋ถ€์žฌ๋‚˜ false gate์—์„œ ์ฆ‰์‹œ ์ค‘๋‹จํ•œ๋‹ค."""
if not path.is_file():
raise RuntimeError(f"๋‹จ๊ณ„ report๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: {path}")
report = json.loads(path.read_text(encoding="utf-8"))
value: Any = report
for key in keys:
value = value[key]
if value is not True:
raise RuntimeError(f"์ •์‹ release gate๊ฐ€ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: {path} / {'.'.join(keys)}")
return report
def _parse_args() -> argparse.Namespace:
"""ํ•„์š” ๋ณ€์ˆ˜: ์‹ค์ œ P corpusยท์„ธ adapterยท์ถœ๋ ฅ. ์ž‘๋™ ์›๋ฆฌ: ์ •์‹ ๋˜๋Š” dry-run release CLI๋ฅผ ๋งŒ๋“ ๋‹ค."""
parser = argparse.ArgumentParser(description="Run Math Ink 0.6 P Formula release loop")
parser.add_argument("--data", type=Path, required=True)
parser.add_argument("--adapter", action="append", required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument(
"--recipe",
type=Path,
default=PROJECT_ROOT / "research/configs/MATH-INK-06-P-FORMULA-v1.json",
)
parser.add_argument("--python", type=Path, default=Path(sys.executable))
parser.add_argument("--convert-litert", action="store_true")
parser.add_argument("--dry-run", action="store_true")
return parser.parse_args()
def main() -> None:
"""ํ•„์š” ๋ณ€์ˆ˜: ์Šน์ธ P corpus์™€ seed๋ณ„ ์ œํ’ˆ adapter. ์ž‘๋™ ์›๋ฆฌ: ๊ฐ gate๋ฅผ ํ™•์ธํ•œ ๋’ค์—๋งŒ ๋‹ค์Œ ์™ธ๋ถ€ ํ”„๋กœ์„ธ์Šค๋ฅผ ์‹คํ–‰ํ•œ๋‹ค."""
args = _parse_args()
adapters = parse_seed_adapters06(args.adapter)
missing = [str(path) for path in (args.data, args.recipe, args.python, *adapters.values()) if not path.is_file()]
if missing:
raise FileNotFoundError(f"release ์ž…๋ ฅ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค: {missing}")
recipe = json.loads(args.recipe.read_text(encoding="utf-8"))
if recipe.get("recipe_id") != "MATH-INK-06-P-FORMULA-v1":
raise ValueError("์ง€์›ํ•˜์ง€ ์•Š๋Š” P Formula release recipe์ž…๋‹ˆ๋‹ค.")
records = list(read_jsonl(args.data))
audit = audit_p_formula_records06(
records,
minimum_independent_sources=int(recipe["input"]["minimum_independent_sources"]),
)
if not audit["eligible_for_product_evaluation"]:
raise ValueError("P Formula corpus๊ฐ€ ์ •์‹ release preflight๋ฅผ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.")
commands = build_p_formula_release_commands06(
python=args.python,
data=args.data,
adapters=adapters,
output=args.output,
recipe=recipe,
convert_litert=args.convert_litert,
)
plan = {
"schema": "aiflow-math-ink-06-p-formula-release-plan-v1",
"generated_at": datetime.now(timezone.utc).isoformat(),
"data": str(args.data),
"data_sha256": _file_sha25606(args.data),
"recipe": str(args.recipe),
"adapters": {str(seed): str(path) for seed, path in adapters.items()},
"commands": commands,
"dry_run": bool(args.dry_run),
"product_validation": False,
}
_write_json06(args.output / "release_plan.json", plan)
if args.dry_run:
print(json.dumps(plan, ensure_ascii=False, indent=2))
return
for index, command in enumerate(commands):
subprocess.run(command, cwd=PROJECT_ROOT, check=True)
if index < 3:
seed = REQUIRED_SEEDS06[index]
_require_gate06(args.output / f"seed{seed}/report.json", ("seed_gate", "passed"))
elif index == 3:
_require_gate06(
args.output / "three_seed_summary.json",
("decision", "student_distillation_allowed"),
)
elif index == 4:
_require_gate06(
args.output / "student/report.json",
("distillation_gate_passed",),
)
elif index == 5:
_require_gate06(
args.output / "export/export_manifest.json",
("torch_export_gate_passed",),
)
final = {
**plan,
"dry_run": False,
"completed": True,
"torch_export_gate_passed": True,
"litert_requested": bool(args.convert_litert),
"android_validation": False,
"product_validation": False,
"next_gate": "LiteRT parity and Android low/mid/high tier benchmark",
}
_write_json06(args.output / "release_report.json", final)
if __name__ == "__main__":
main()