"""정정된 Math Ink 0.6 composite online/raster CPU 지연을 대표 입력으로 측정한다.""" from __future__ import annotations import argparse from datetime import datetime, timezone import json import math from pathlib import Path import statistics import sys import time import torch 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.math_ink_06 import MathInk06Engine from scripts.export_math_ink_06_litert import _load_representative_inputs06 def percentile_nearest_rank06(values: list[float], percentile: float) -> float: """필요 변수: 측정값·0~1 percentile. 작동 원리: 모바일 p95와 동일한 nearest-rank 값을 반환한다.""" if not values or not 0.0 <= percentile <= 1.0: raise ValueError("percentile 입력이 유효하지 않습니다.") ordered = sorted(values) index = max(0, min(len(ordered) - 1, math.ceil(percentile * len(ordered)) - 1)) return float(ordered[index]) def module_state_bytes06(*modules: torch.nn.Module) -> int: """필요 변수: model·adapter module. 작동 원리: 중복 storage를 한 번만 세어 실제 tensor state bytes를 계산한다.""" seen: set[tuple[int, int]] = set() total = 0 for module in modules: for tensor in [*module.parameters(), *module.buffers()]: storage = tensor.untyped_storage() key = (storage.data_ptr(), storage.nbytes()) if key in seen: continue seen.add(key) total += storage.nbytes() return total def _rss_bytes06() -> int | None: """필요 변수: 없음. 작동 원리: psutil이 있으면 현재 process RSS를 반환하고 없으면 명시적으로 결측 처리한다.""" try: import psutil except ImportError: return None return int(psutil.Process().memory_info().rss) def _measure06(callable_, inputs: list[tuple[torch.Tensor, ...]], warmup: int) -> dict: """필요 변수: 고정 inference callable·대표 입력·warmup. 작동 원리: 표본별 wall latency와 output checksum을 측정한다.""" with torch.inference_mode(): for arguments in inputs[:max(1, min(warmup, len(inputs)))]: callable_(*arguments) latencies, checksum = [], 0 observed_rss = _rss_bytes06() for arguments in inputs: started = time.perf_counter() output = callable_(*arguments) latencies.append((time.perf_counter() - started) * 1000.0) primary = output[0] if isinstance(output, tuple) else output checksum = (checksum * 131 + int(primary.argmax(dim=-1)[0])) % 2_147_483_647 current_rss = _rss_bytes06() if current_rss is not None: observed_rss = max(observed_rss or 0, current_rss) return { "samples": len(latencies), "mean_ms": statistics.fmean(latencies), "p50_ms": percentile_nearest_rank06(latencies, 0.50), "p95_ms": percentile_nearest_rank06(latencies, 0.95), "maximum_ms": max(latencies), "output_checksum": checksum, "observed_process_rss_bytes": observed_rss, } def main() -> None: """필요 변수: composite artifact·대표 cache. 작동 원리: thread별 두 inference 경로를 독립 측정해 JSON으로 남긴다.""" parser = argparse.ArgumentParser(description="Benchmark Math Ink 0.6 composite CPU") parser.add_argument("--checkpoint", type=Path, required=True) parser.add_argument("--adapter-checkpoint", type=Path, required=True) parser.add_argument("--representative-inputs", type=Path, required=True) parser.add_argument("--threads", type=int, action="append", default=None) parser.add_argument("--samples", type=int, default=76) parser.add_argument("--warmup", type=int, default=5) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() requested_threads = args.threads or [1, 2, 4] if any(value <= 0 for value in requested_threads): raise ValueError("CPU thread는 양수여야 합니다.") torch.set_num_interop_threads(1) engine = MathInk06Engine( args.checkpoint, adapter_checkpoint=args.adapter_checkpoint, device="cpu", ) online_inputs, raster_inputs = _load_representative_inputs06(args.representative_inputs) online_inputs = online_inputs[:args.samples] raster_inputs = raster_inputs[:args.samples] def online_forward(sequence: torch.Tensor): """필요 변수: canonical sequence. 작동 원리: 실제 runtime online composite branch를 호출한다.""" return engine.model.forward_online(engine.online_adapter(sequence)) def raster_forward(raster: torch.Tensor): """필요 변수: raster. 작동 원리: 실제 runtime virtual stroke·raster adapter·fusion을 호출한다.""" output = engine._forward_raster_composite06(raster) return engine.fuse_raster_output(output)[0] baseline_rss = _rss_bytes06() rows = [] for thread_count in requested_threads: torch.set_num_threads(thread_count) online = _measure06(online_forward, online_inputs, args.warmup) raster = _measure06(raster_forward, raster_inputs, args.warmup) rows.append({ "threads": thread_count, "online": online, "raster": raster, "proxy_gates": { "online_p95_le_50ms": online["p95_ms"] <= 50.0, "raster_p95_le_200ms": raster["p95_ms"] <= 200.0, }, }) observed_rss_values = [ int(metrics["observed_process_rss_bytes"]) for row in rows for metrics in (row["online"], row["raster"]) if metrics["observed_process_rss_bytes"] is not None ] maximum_observed_rss = max(observed_rss_values) if observed_rss_values else None inference_rss_growth = ( max(0, maximum_observed_rss - baseline_rss) if maximum_observed_rss is not None and baseline_rss is not None else None ) state_bytes = module_state_bytes06(engine.model, engine.composite_adapter) model_plus_inference = ( state_bytes + inference_rss_growth if inference_rss_growth is not None else None ) report = { "schema": "aiflow-math-ink-06-composite-cpu-benchmark-v1", "generated_at": datetime.now(timezone.utc).isoformat(), "torch_version": torch.__version__, "platform": sys.platform, "model_version": engine.model_version, "model_state_bytes": state_bytes, "baseline_process_rss_bytes": baseline_rss, "maximum_observed_process_rss_bytes": maximum_observed_rss, "inference_rss_growth_bytes": inference_rss_growth, "model_state_plus_inference_growth_bytes": model_plus_inference, "memory_proxy_gate_le_100mib": ( model_plus_inference <= 100 * 1024 * 1024 if model_plus_inference is not None else None ), "rows": rows, "interpretation_limit": ( "Windows PyTorch CPU proxy이며 Android LiteRT·배터리·delegate 성능 판정이 아니다." ), "product_validation": False, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text( json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) print(json.dumps(report, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()