agentic-rl-main / scripts /analysis /image_checker_timing_report.py
Jack04810's picture
Add files using upload-large-folder tool
d13981b verified
Raw History Blame Contribute Delete
10.8 kB
#!/usr/bin/env python
"""Summarize OPD image-checker smoke timing from logs and visual artifacts."""
from __future__ import annotations
import argparse
import ast
import json
import math
import re
from collections import Counter
from pathlib import Path
from typing import Iterable, Any
SUMMARY_KEYS = (
"visual/ic_latency_ms",
"visual/checker_latency_ms",
"visual/refiner_latency_ms",
"visual/ic_calls",
"visual/checker_calls",
"visual/refiner_calls",
"visual/teacher_batch_calls",
"visual/ic_batch_calls",
"visual/checker_batch_calls",
"visual/refiner_batch_calls",
"visual/checker_high",
"visual/checker_medium",
"visual/checker_low",
"visual/checker_parse_failure",
"visual/checker_image_missing",
"visual/checker_aux_evidence_used",
"visual/fallback_checker",
"visual/fallback_refiner",
)
_STEP_RE = re.compile(r"step_(\d+)")
_COMPLETION_MODE_COUNTS_RE = re.compile(r"completion_mode_counts=(\{.*?\})(?:\s*\||$)")
def _as_float(value: Any, default: float = 0.0) -> float:
try:
out = float(value)
except (TypeError, ValueError):
return default
if not math.isfinite(out):
return default
return out
def _ratio(num: float, den: float) -> float:
den = _as_float(den)
if den <= 0.0:
return 0.0
return round(_as_float(num) / den, 4)
def _parse_visual_batch_line(line: str) -> dict[str, float] | None:
if "[VISUAL-BATCH]" not in line or "generate_summary" not in line:
return None
fields: dict[str, float] = {}
for part in line.split(" | ")[1:]:
if "=" not in part:
continue
key, value = part.split("=", 1)
key = key.strip()
if key in SUMMARY_KEYS:
fields[key] = _as_float(value.strip())
return fields
def parse_log_files(log_files: Iterable[Path]) -> list[dict[str, float]]:
batches: list[dict[str, float]] = []
for path in log_files:
if not path or not path.exists():
continue
with path.open("r", encoding="utf-8", errors="replace") as handle:
for line in handle:
parsed = _parse_visual_batch_line(line)
if parsed is not None:
batches.append(parsed)
return batches
def parse_trainer_completion_modes(log_files: Iterable[Path]) -> dict[str, int]:
counts: Counter[str] = Counter()
for path in log_files:
if not path or not path.exists():
continue
with path.open("r", encoding="utf-8", errors="replace") as handle:
for line in handle:
match = _COMPLETION_MODE_COUNTS_RE.search(line)
if not match:
continue
raw = match.group(1)
try:
payload = json.loads(raw)
except json.JSONDecodeError:
try:
payload = ast.literal_eval(raw)
except (ValueError, SyntaxError):
continue
if not isinstance(payload, dict):
continue
for key, value in payload.items():
counts[str(key)] += int(_as_float(value))
return dict(sorted(counts.items()))
def _step_from_path(path: Path) -> int:
for parent in (path.parent, *path.parents):
match = _STEP_RE.fullmatch(parent.name)
if match:
return int(match.group(1))
return -1
def _score_label(score: Any) -> str:
value = _as_float(score)
if value >= 0.75:
return "high"
if value >= 0.25:
return "medium"
return "low"
def _artifact_files(output_dir: Path | None) -> list[Path]:
if output_dir is None:
return []
root = output_dir / "visual_supervision"
if not root.exists():
return []
return sorted(root.glob("step_*/rank*.jsonl"))
def parse_artifacts(output_dir: Path | None) -> dict[str, Any]:
checker_labels: Counter[str] = Counter()
routes: Counter[str] = Counter()
route_x_checker: Counter[str] = Counter()
checker_by_sample: dict[tuple[int, int], str] = {}
false_high = 0
false_low = 0
checker_rows = 0
route_rows = 0
for path in _artifact_files(output_dir):
step = _step_from_path(path)
with path.open("r", encoding="utf-8", errors="replace") as handle:
for raw in handle:
try:
row = json.loads(raw)
except json.JSONDecodeError:
continue
kind = str(row.get("kind", ""))
sample_idx = int(_as_float(row.get("sample_idx"), -1))
key = (step, sample_idx)
if kind == "checker":
label = str(row.get("label") or _score_label(row.get("score"))).lower()
if label == "image_missing":
label = "low"
checker_by_sample[key] = label
checker_labels[label] += 1
checker_rows += 1
elif kind == "route":
route = str(row.get("route") or "unknown").lower()
label = checker_by_sample.get(key) or _score_label(row.get("checker_score"))
routes[route] += 1
route_x_checker[f"{route}:{label}"] += 1
route_rows += 1
answer = _as_float(row.get("answer_reward"))
if label == "high" and answer <= 0.0:
false_high += 1
if label == "low" and answer > 0.0:
false_low += 1
return {
"artifact_checker_rows": checker_rows,
"artifact_route_rows": route_rows,
"checker_labels": dict(sorted(checker_labels.items())),
"routes": dict(sorted(routes.items())),
"route_x_checker": dict(sorted(route_x_checker.items())),
"false_high_count": false_high,
"false_low_count": false_low,
}
def build_report(*, log_files: Iterable[Path], output_dir: Path | None = None) -> dict[str, Any]:
log_file_list = [Path(path) for path in log_files]
batches = parse_log_files(log_file_list)
report: dict[str, Any] = {"log_batches": len(batches)}
for key in SUMMARY_KEYS:
report[key] = round(sum(_as_float(batch.get(key)) for batch in batches), 4)
report["derived/checker_ms_per_call"] = _ratio(
report["visual/checker_latency_ms"],
report["visual/checker_calls"],
)
report["derived/checker_ms_per_batch"] = _ratio(
report["visual/checker_latency_ms"],
report["visual/checker_batch_calls"],
)
report["derived/ic_ms_per_call"] = _ratio(
report["visual/ic_latency_ms"],
report["visual/ic_calls"],
)
report["derived/refiner_ms_per_call"] = _ratio(
report["visual/refiner_latency_ms"],
report["visual/refiner_calls"],
)
report["trainer_completion_modes"] = parse_trainer_completion_modes(log_file_list)
report.update(parse_artifacts(output_dir))
return report
def _fmt_counter(mapping: dict[str, int]) -> str:
if not mapping:
return "none"
return ",".join(f"{key}={value}" for key, value in sorted(mapping.items()))
def format_report(report: dict[str, Any]) -> str:
lines = [
(
"[IMAGE-CHECKER-TIMING] "
f"log_batches={int(report.get('log_batches', 0))} "
f"checker_latency_ms={report.get('visual/checker_latency_ms', 0.0)} "
f"checker_calls={report.get('visual/checker_calls', 0.0)} "
f"checker_batch_calls={report.get('visual/checker_batch_calls', 0.0)} "
f"checker_ms_per_call={report.get('derived/checker_ms_per_call', 0.0)} "
f"checker_ms_per_batch={report.get('derived/checker_ms_per_batch', 0.0)}"
),
(
"[IMAGE-CHECKER-TIMING] "
f"ic_latency_ms={report.get('visual/ic_latency_ms', 0.0)} "
f"ic_calls={report.get('visual/ic_calls', 0.0)} "
f"ic_batch_calls={report.get('visual/ic_batch_calls', 0.0)} "
f"ic_ms_per_call={report.get('derived/ic_ms_per_call', 0.0)} "
f"refiner_latency_ms={report.get('visual/refiner_latency_ms', 0.0)} "
f"refiner_calls={report.get('visual/refiner_calls', 0.0)} "
f"refiner_batch_calls={report.get('visual/refiner_batch_calls', 0.0)} "
f"refiner_ms_per_call={report.get('derived/refiner_ms_per_call', 0.0)}"
),
(
"[IMAGE-CHECKER-TIMING] "
f"checker_labels={_fmt_counter(report.get('checker_labels', {}))} "
f"routes={_fmt_counter(report.get('routes', {}))} "
f"trainer_completion_modes={_fmt_counter(report.get('trainer_completion_modes', {}))} "
f"route_x_checker={_fmt_counter(report.get('route_x_checker', {}))}"
),
(
"[IMAGE-CHECKER-TIMING] "
f"parse_failure={report.get('visual/checker_parse_failure', 0.0)} "
f"image_missing={report.get('visual/checker_image_missing', 0.0)} "
f"aux_used={report.get('visual/checker_aux_evidence_used', 0.0)} "
f"fallback_checker={report.get('visual/fallback_checker', 0.0)} "
f"false_high_count={report.get('false_high_count', 0)} "
f"false_low_count={report.get('false_low_count', 0)}"
),
]
return "\n".join(lines)
def _discover_logs(log_dir: Path | None, pattern: str) -> list[Path]:
if log_dir is None or not log_dir.exists():
return []
return sorted(log_dir.glob(pattern), key=lambda p: p.stat().st_mtime)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--log-file", action="append", default=[], help="Training log file. Repeatable.")
parser.add_argument("--log-dir", type=Path, default=None, help="Directory to scan when --log-file is absent.")
parser.add_argument("--output-dir", type=Path, default=None, help="Training output_dir containing visual_supervision.")
parser.add_argument(
"--pattern",
default="train_opd_7b_dyme_probe_image_checker*.log",
help="Log glob used with --log-dir.",
)
parser.add_argument("--json", action="store_true", help="Print JSON instead of text.")
args = parser.parse_args()
log_files = [Path(p) for p in args.log_file]
if not log_files:
log_files = _discover_logs(args.log_dir, args.pattern)
report = build_report(log_files=log_files, output_dir=args.output_dir)
if args.json:
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
else:
print(format_report(report))
return 0
if __name__ == "__main__":
raise SystemExit(main())