Lien-Feng's picture
Upload 211 files
c3f98a1 verified
Raw
History Blame Contribute Delete
7.89 kB
"""Model and hardware profile for the Methods section.
Measures parameter count, GFLOPs, checkpoint size, single-slice latency and
batched throughput for each backbone on the machine it is run on, and writes a
JSON/Markdown pair the manuscript can quote directly. Latency is timed over a
configurable number of consecutive axial slices of a real scan held in memory,
with CUDA synchronisation around every call, so the figure is reproducible and
the sample size is recorded alongside it rather than asserted in prose.
Usage
-----
python scripts/06_profile.py [--n 200] [--weights path/to/best.pt]
"""
from __future__ import annotations
import argparse
import json
import platform
import sys
import time
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from luna_rev import config as cfg
from luna_rev.io_luna import all_uids, normalize_hu, read_volume
def measure_latency(model, images, device, warmup: int = 20) -> dict:
"""Per-slice latency at batch size 1, including pre/post-processing."""
import torch
for img in images[:warmup]:
model.predict([img], imgsz=cfg.IMG_SIZE, conf=cfg.INFER_CONF,
max_det=cfg.INFER_MAX_DET, device=device, verbose=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
times = []
for img in images:
t0 = time.perf_counter()
model.predict([img], imgsz=cfg.IMG_SIZE, conf=cfg.INFER_CONF,
max_det=cfg.INFER_MAX_DET, device=device, verbose=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
times.append((time.perf_counter() - t0) * 1000.0)
a = np.array(times)
return {"n": len(a), "mean_ms": float(a.mean()), "sd_ms": float(a.std(ddof=1)),
"median_ms": float(np.median(a)), "p95_ms": float(np.percentile(a, 95))}
def measure_throughput(model, images, device, batch: int) -> dict:
"""Batched throughput, which is what whole-volume screening actually uses."""
import torch
chunks = [images[i:i + batch] for i in range(0, len(images), batch)]
model.predict(chunks[0], imgsz=cfg.IMG_SIZE, conf=cfg.INFER_CONF,
max_det=cfg.INFER_MAX_DET, device=device, verbose=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
t0 = time.perf_counter()
n = 0
for ch in chunks:
model.predict(ch, imgsz=cfg.IMG_SIZE, conf=cfg.INFER_CONF,
max_det=cfg.INFER_MAX_DET, device=device, verbose=False)
n += len(ch)
if torch.cuda.is_available():
torch.cuda.synchronize()
dt = time.perf_counter() - t0
return {"batch": batch, "slices": n, "seconds": round(dt, 3),
"slices_per_second": round(n / dt, 1),
"seconds_per_scan_256_slices": round(256 * dt / n, 2)}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--n", type=int, default=200, help="slices used for timing")
ap.add_argument("--weights", default=None)
ap.add_argument("--models", nargs="*", default=["yolo11n.pt", "yolo26n.pt"])
args = ap.parse_args()
import torch
import ultralytics
from ultralytics import YOLO
# model_info() returns early (bare `return`) when verbose=False, so query the
# underlying helpers directly rather than unpacking a None.
from ultralytics.utils.torch_utils import get_flops, get_num_params
# Real CT slices, preloaded so disk I/O never enters the timing.
uid = all_uids()[0]
vol, _ = read_volume(uid)
vol_u8 = normalize_hu(vol)
zs = np.linspace(0, vol_u8.shape[0] - 1, min(args.n, vol_u8.shape[0]), dtype=int)
images = [np.stack([vol_u8[max(z - 1, 0)], vol_u8[z],
vol_u8[min(z + 1, vol_u8.shape[0] - 1)]], axis=-1) for z in zs]
while len(images) < args.n:
images += images[:args.n - len(images)]
device = cfg.HW.device
report = {
"platform": {
"os": platform.platform(),
"cpu": platform.processor(),
"logical_cpus": __import__("os").cpu_count(),
"gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else "cpu",
"gpu_total_memory_GB": round(torch.cuda.get_device_properties(0).total_memory / 1e9, 2)
if torch.cuda.is_available() else None,
"torch": torch.__version__,
"ultralytics": ultralytics.__version__,
"python": platform.python_version(),
},
"settings": {"imgsz": cfg.IMG_SIZE, "conf": cfg.INFER_CONF,
"max_det": cfg.INFER_MAX_DET, "amp": cfg.HW.amp},
"models": {},
}
targets = [(Path(args.weights).stem, args.weights)] if args.weights else [
(m.replace(".pt", ""), str(cfg.WEIGHTS_CACHE / m)) for m in args.models]
for name, path in targets:
model = YOLO(path)
n_params = get_num_params(model.model)
flops = get_flops(model.model, imgsz=cfg.IMG_SIZE)
entry = {
# Recorded by name: an absolute path from the machine that produced
# the profile is of no use to a reader and leaks a local layout.
"weights": Path(path).name,
"size_MB": round(Path(path).stat().st_size / 1e6, 2),
"parameters": int(n_params),
"gflops_at_imgsz": round(float(flops), 2),
"end2end_nms_free": bool(getattr(model.model, "end2end", False)),
"latency_batch1": measure_latency(model, images, device),
}
for b in (16, 32, 64):
entry[f"throughput_batch{b}"] = measure_throughput(model, images, device, b)
report["models"][name] = entry
lat = entry["latency_batch1"]
print(f"{name:10s} params={entry['parameters']:,} GFLOPs={entry['gflops_at_imgsz']} "
f"latency={lat['mean_ms']:.2f}+/-{lat['sd_ms']:.2f} ms "
f"throughput@64={entry['throughput_batch64']['slices_per_second']} slice/s")
out_json = cfg.RESULTS_DIR / "model_profile.json"
out_json.write_text(json.dumps(report, indent=1), encoding="utf-8")
lines = ["# Model and hardware profile", "",
f"- Platform: {report['platform']['os']}",
f"- GPU: {report['platform']['gpu']} "
f"({report['platform']['gpu_total_memory_GB']} GB)",
f"- torch {report['platform']['torch']}, "
f"ultralytics {report['platform']['ultralytics']}, "
f"Python {report['platform']['python']}",
f"- Input size: {cfg.IMG_SIZE} x {cfg.IMG_SIZE} (native LUNA16 resolution)", "",
"| Model | Params | GFLOPs | Size (MB) | Latency b=1 (ms) | Throughput b=64 (slice/s) | s / 256-slice scan |",
"|---|---:|---:|---:|---:|---:|---:|"]
for name, e in report["models"].items():
lines.append(
f"| {name} | {e['parameters']:,} | {e['gflops_at_imgsz']} | {e['size_MB']} | "
f"{e['latency_batch1']['mean_ms']:.2f} +/- {e['latency_batch1']['sd_ms']:.2f} | "
f"{e['throughput_batch64']['slices_per_second']} | "
f"{e['throughput_batch64']['seconds_per_scan_256_slices']} |")
lines += ["", f"Latency measured over {args.n} consecutive axial slices of a real LUNA16 "
"scan preloaded into RAM, batch size 1, timing includes Ultralytics "
"pre-processing, forward pass and post-processing, with CUDA "
"synchronisation around every call."]
out_md = cfg.RESULTS_DIR / "model_profile.md"
out_md.write_text("\n".join(lines), encoding="utf-8")
print(f"\nWrote {out_json}\n {out_md}")
return 0
if __name__ == "__main__":
raise SystemExit(main())