aiflow-math-ink-06-intermediate / scripts /benchmark_math_ink_06_composite.py
cwLeeDev's picture
Add composite CPU latency and memory proxy
6ab8274 verified
Raw
History Blame Contribute Delete
7.62 kB
"""정정된 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()