agrobot_base/Python/OAK/datasets/oak-fcc-3/_12_benchmark_onnx.py

946 lines
28 KiB
Python
Raw Normal View History

2026-05-22 22:32:11 +00:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
_12_benchmark_onnx.py
Benchmark PyTorch vs ONNX Runtime para SegFormer OAK-FCC-3 Multi-Head.
Mede:
- PyTorch FP32
- PyTorch AMP/FP16
- ONNX Runtime CUDA ou CPU
Exemplos:
Benchmark ONNX cru 160x256:
python _12_benchmark_onnx.py --config config.json --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider cuda
Benchmark ONNX resized 640x1024:
python _12_benchmark_onnx.py --config config.json --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider cuda
TensorRT
python _12_benchmark_onnx.py --config config.json --max_samples 50 --warmup 10 --repeat 5 --device cuda --onnx_provider tensorrt --skip_torch_fp32 --skip_torch_amp
"""
from __future__ import annotations
import gc
import csv
import json
import time
import argparse
import importlib.util
from pathlib import Path
from typing import Optional, List, Dict, Tuple
import cv2
import numpy as np
import torch
import torch.nn as nn
# ============================================================
# Utils
# ============================================================
def load_json(path: str | Path) -> dict:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path: str | Path, data: dict):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def save_csv(path: str | Path, rows: List[dict]):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
if not rows:
return
keys = list(rows[0].keys())
with path.open("w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=keys)
w.writeheader()
w.writerows(rows)
def resolve_path(path_like: Optional[str], base: Optional[Path] = None) -> Optional[Path]:
if path_like is None:
return None
p = Path(path_like)
if p.is_absolute():
return p
if base is None:
base = Path.cwd()
return (base / p).resolve()
def import_train_module(train_script_path: str | Path):
train_script_path = Path(train_script_path)
if not train_script_path.exists():
raise FileNotFoundError(f"Script de treino não encontrado: {train_script_path}")
spec = importlib.util.spec_from_file_location(
"train_multihead_module",
str(train_script_path.resolve())
)
if spec is None or spec.loader is None:
raise RuntimeError(f"Não consegui importar o script: {train_script_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def synchronize_if_cuda(device: torch.device):
if device.type == "cuda":
torch.cuda.synchronize()
def clear_cuda():
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
def percentile(values: List[float], p: float) -> float:
if not values:
return 0.0
return float(np.percentile(np.asarray(values, dtype=np.float64), p))
def summarize_times(times_ms: List[float]) -> dict:
arr = np.asarray(times_ms, dtype=np.float64)
if arr.size == 0:
return {
"n": 0,
"mean_ms": 0.0,
"median_ms": 0.0,
"min_ms": 0.0,
"max_ms": 0.0,
"p95_ms": 0.0,
"p99_ms": 0.0,
"fps_mean": 0.0,
"fps_p95_latency": 0.0,
}
mean_ms = float(arr.mean())
p95_ms = float(np.percentile(arr, 95))
p99_ms = float(np.percentile(arr, 99))
return {
"n": int(arr.size),
"mean_ms": mean_ms,
"median_ms": float(np.median(arr)),
"min_ms": float(arr.min()),
"max_ms": float(arr.max()),
"p95_ms": p95_ms,
"p99_ms": p99_ms,
"fps_mean": float(1000.0 / mean_ms) if mean_ms > 0 else 0.0,
"fps_p95_latency": float(1000.0 / p95_ms) if p95_ms > 0 else 0.0,
}
def resolve_model_artifact_paths(
args,
config: dict,
config_dir: Path,
channels: int,
) -> Tuple[Path, Path, str]:
"""
Resolve checkpoint e ONNX.
Se --checkpoint ou --onnx forem informados, usa os caminhos informados.
Se ficarem vazios, monta a partir do config:
backup/{modelo}/{model_name}/{fusion_mode}_raw{channels}/{ckpt_name}.pt
backup/{modelo}/{model_name}/{fusion_mode}_raw{channels}/{ckpt_name}.onnx
ckpt_name vem de:
config["ckpt_test"] ou "best_score"
"""
model = config.get("modelo", "segformer_b1")
model_name = config.get("model_name", "target_teached")
fusion_mode = config.get("fusion_mode", "stacked")
ckpt_name = config.get("ckpt_test", "best_score")
# Usa o número real de canais selecionados,
# não necessariamente config["channels"].
ch = int(channels)
base_dir = config_dir / "backup" / model / model_name / f"{fusion_mode}_raw{ch}"
if args.checkpoint:
checkpoint_path = resolve_path(args.checkpoint, Path.cwd())
else:
checkpoint_path = base_dir / f"{ckpt_name}.pt"
if args.onnx:
onnx_path = resolve_path(args.onnx, Path.cwd())
else:
onnx_path = base_dir / f"{ckpt_name}.onnx"
if checkpoint_path is None or not checkpoint_path.is_file():
raise FileNotFoundError(
f"Checkpoint não encontrado: {checkpoint_path}\n"
f"Dica: informe --checkpoint ou ajuste config['ckpt_test']."
)
if onnx_path is None or not onnx_path.is_file():
raise FileNotFoundError(
f"ONNX não encontrado: {onnx_path}\n"
f"Dica: informe --onnx ou ajuste config['ckpt_test']."
)
return checkpoint_path.resolve(), onnx_path.resolve(), str(ckpt_name)
# ============================================================
# Dataset / normalização
# ============================================================
def collect_tensor_samples(root: Path, max_samples: int = 50, start_idx: int = 0) -> List[Path]:
tensor_paths: List[Path] = []
direct = root / "tensors"
if direct.is_dir():
tensor_paths.extend(sorted(direct.glob("*.npy")))
group_root = root / "group"
if group_root.is_dir():
for gdir in sorted(group_root.iterdir()):
tdir = gdir / "tensors"
if tdir.is_dir():
tensor_paths.extend(sorted(tdir.glob("*.npy")))
if not tensor_paths:
tensor_paths.extend(sorted(root.glob("**/tensors/*.npy")))
if not tensor_paths:
raise RuntimeError(f"Nenhum tensor .npy encontrado em: {root}")
start_idx = max(0, int(start_idx))
selected = tensor_paths[start_idx:]
if max_samples > 0:
selected = selected[:int(max_samples)]
return selected
def load_tensor(path: Path, channels: int, channel_indices: List[int]) -> np.ndarray:
arr = np.load(str(path)).astype(np.float32)
if arr.ndim != 3:
raise RuntimeError(f"Tensor inválido {path}: shape={arr.shape}, esperado 3D")
if arr.shape[0] in (3, 4, 5):
chw = arr
elif arr.shape[-1] in (3, 4, 5):
chw = np.transpose(arr, (2, 0, 1))
else:
raise RuntimeError(f"Tensor com layout inesperado: {path} shape={arr.shape}")
max_idx = max(channel_indices)
if chw.shape[0] <= max_idx:
raise RuntimeError(
f"Tensor {path} tem {chw.shape[0]} canais, "
f"mas precisa acessar índice {max_idx}."
)
chw = chw[channel_indices, :, :]
finite = np.isfinite(chw)
if finite.any():
mx = float(np.nanmax(chw[finite]))
if mx > 2.0 and mx <= 255.0:
chw = chw / 255.0
elif mx > 255.0:
chw = chw / 65535.0
chw = np.nan_to_num(chw, nan=0.0, posinf=1.0, neginf=0.0)
return np.clip(chw, 0.0, 1.0).astype(np.float32)
def find_norm_stats(config: dict, config_dir: Path, save_dir: Path, explicit: Optional[str]) -> Optional[Path]:
if explicit:
return resolve_path(explicit, Path.cwd())
W, H = config.get("resolucao", [1024, 640])
dataset_path = config_dir / "dataset"
candidates = [
dataset_path / f"{int(W)}x{int(H)}" / "group" / "norm_stats.json",
save_dir / "norm_stats.json",
config_dir / "backup" / config.get("modelo", "segformer_b1") / config.get("model_name", "test") / config.get("stats_source_tag", "stacked_raw5") / "norm_stats.json",
]
for p in candidates:
if p.is_file():
return p
return candidates[0]
def load_norm_stats(
path: Optional[Path],
channel_indices: List[int],
channel_names: List[str],
) -> Tuple[Optional[List[float]], Optional[List[float]], Optional[str]]:
if path is None or not path.is_file():
print("[NORM] Sem norm_stats. Usando tensor 0..1 sem padronização.")
return None, None, None
js = load_json(path)
mean = js.get("mean")
std = js.get("std")
names = js.get("channels", [])
if mean is None or std is None:
raise RuntimeError(f"norm_stats inválido, faltando mean/std: {path}")
max_idx = max(channel_indices)
if len(mean) <= max_idx or len(std) <= max_idx:
raise RuntimeError(
f"norm_stats incompatível: precisa índices={channel_indices}, "
f"mean={len(mean)} std={len(std)}"
)
mean_sel = [float(mean[i]) for i in channel_indices]
std_sel = [float(std[i]) for i in channel_indices]
if names:
names_sel = [names[i] for i in channel_indices]
else:
names_sel = channel_names
print(f"[NORM] usando {path}")
print(f"[NORM] channels={names_sel}")
print(f"[NORM] mean={mean_sel}")
print(f"[NORM] std ={std_sel}")
return mean_sel, std_sel, str(path)
def normalize_numpy_chw(chw: np.ndarray, mean: Optional[List[float]], std: Optional[List[float]]) -> np.ndarray:
if mean is None or std is None:
return chw.astype(np.float32)
mean_np = np.asarray(mean, dtype=np.float32).reshape(-1, 1, 1)
std_np = np.asarray(std, dtype=np.float32).reshape(-1, 1, 1)
std_np = np.clip(std_np, 1e-6, None)
return ((chw.astype(np.float32) - mean_np) / std_np).astype(np.float32)
def load_inputs_as_numpy(
samples: List[Path],
channels: int,
channel_indices: List[int],
mean: Optional[List[float]],
std: Optional[List[float]],
target_hw: Tuple[int, int],
normalize_input: bool = True,
) -> List[np.ndarray]:
H, W = target_hw
xs = []
for p in samples:
chw01 = load_tensor(
p,
channels=channels,
channel_indices=channel_indices,
)
if chw01.shape[-2:] != (H, W):
hwc = np.transpose(chw01, (1, 2, 0))
hwc = cv2.resize(hwc, (W, H), interpolation=cv2.INTER_LINEAR)
chw01 = np.transpose(hwc, (2, 0, 1)).astype(np.float32)
if normalize_input:
chw = normalize_numpy_chw(chw01, mean=mean, std=std)
else:
chw = chw01.astype(np.float32, copy=False)
x = np.expand_dims(chw, axis=0).astype(np.float32)
xs.append(x)
return xs
# ============================================================
# PyTorch
# ============================================================
class TorchTupleWrapper(nn.Module):
def __init__(self, model: nn.Module, output_heads: List[str]):
super().__init__()
self.model = model
self.output_heads = list(output_heads)
def forward(self, pixel_values: torch.Tensor):
outputs = self.model(pixel_values=pixel_values)
return tuple(outputs[h] for h in self.output_heads)
@torch.inference_mode()
def benchmark_torch(
model: nn.Module,
inputs_np: List[np.ndarray],
device: torch.device,
warmup: int,
repeat: int,
amp: bool,
label: str,
) -> Tuple[dict, List[dict]]:
model.eval()
times = []
rows = []
# Precarrega tensors na GPU para medir só inferência do modelo.
inputs_t = [
torch.from_numpy(x).to(device, non_blocking=True)
for x in inputs_np
]
if device.type == "cuda":
torch.cuda.synchronize()
print(f"\n[BENCH] {label} | warmup={warmup} repeat={repeat}")
# Warmup
for i in range(max(0, warmup)):
x = inputs_t[i % len(inputs_t)]
with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=amp and device.type == "cuda"):
_ = model(x)
synchronize_if_cuda(device)
# Medição
total_iter = len(inputs_t) * max(1, repeat)
idx = 0
for r in range(max(1, repeat)):
for sample_idx, x in enumerate(inputs_t):
synchronize_if_cuda(device)
t0 = time.perf_counter()
with torch.autocast(device_type="cuda", dtype=torch.float16, enabled=amp and device.type == "cuda"):
_ = model(x)
synchronize_if_cuda(device)
dt_ms = (time.perf_counter() - t0) * 1000.0
times.append(dt_ms)
rows.append({
"engine": label,
"repeat": r,
"sample_idx": sample_idx,
"iter_idx": idx,
"latency_ms": dt_ms,
})
idx += 1
if idx % 25 == 0 or idx == total_iter:
print(f" {idx:04d}/{total_iter:04d} | last={dt_ms:.2f}ms")
summary = summarize_times(times)
summary["engine"] = label
return summary, rows
# ============================================================
# ONNX Runtime
# ============================================================
def create_onnx_session(onnx_path: Path, provider: str):
import os
trt_home = os.environ.get("TRT_HOME", r"C:\dev\TensorRT-10.10.0.31")
for dll_dir in [
os.path.join(trt_home, "lib"),
os.path.join(trt_home, "bin"),
r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin",
]:
if os.path.isdir(dll_dir):
try:
os.add_dll_directory(dll_dir)
print(f"[DLL] add_dll_directory: {dll_dir}")
except Exception as e:
print(f"[DLL][WARN] falha em {dll_dir}: {e}")
try:
import onnxruntime as ort
except ImportError:
raise ImportError(
"onnxruntime não está instalado. Instale com:\n"
" pip install onnxruntime-gpu\n"
"ou CPU:\n"
" pip install onnxruntime"
)
available = ort.get_available_providers()
print(f"[ONNX] providers disponíveis: {available}")
provider = provider.lower()
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
if provider == "cuda":
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
elif provider == "cpu":
providers = ["CPUExecutionProvider"]
elif provider == "tensorrt":
cache_dir = onnx_path.parent / "trt_cache"
cache_dir.mkdir(parents=True, exist_ok=True)
trt_options = {
"device_id": 0,
# FP16: o ponto principal do nosso teste.
"trt_fp16_enable": True,
# Cache: evita rebuild do engine a cada execução.
"trt_engine_cache_enable": True,
"trt_engine_cache_path": str(cache_dir),
# Timing cache ajuda a acelerar builds futuros.
"trt_timing_cache_enable": True,
"trt_timing_cache_path": str(cache_dir),
# Workspace. 4GB é razoável para RTX 3070, ajuste se faltar VRAM.
"trt_max_workspace_size": 4 * 1024 * 1024 * 1024,
}
providers = [
("TensorrtExecutionProvider", trt_options),
"CUDAExecutionProvider",
"CPUExecutionProvider",
]
else:
providers = [provider]
# Checagem de disponibilidade, lidando com provider tuple.
requested_names = [
p[0] if isinstance(p, tuple) else p
for p in providers
]
providers_ok = [
p for p in providers
if (p[0] if isinstance(p, tuple) else p) in available
]
if not providers_ok:
raise RuntimeError(
f"Nenhum provider solicitado está disponível. "
f"Solicitado={requested_names}, disponível={available}"
)
session = ort.InferenceSession(
str(onnx_path),
sess_options=sess_options,
providers=providers_ok,
)
print(f"[ONNX] usando providers: {session.get_providers()}")
active_providers = session.get_providers()
if provider == "tensorrt" and "TensorrtExecutionProvider" not in active_providers:
raise RuntimeError(
"TensorRTExecutionProvider foi solicitado, mas não ficou ativo. "
f"Providers ativos: {active_providers}. "
"Provável causa: TensorRT não instalado, DLLs fora do PATH, "
"ou versão incompatível com onnxruntime-gpu."
)
if provider == "cuda" and "CUDAExecutionProvider" not in active_providers:
raise RuntimeError(
"CUDAExecutionProvider foi solicitado, mas não ficou ativo. "
f"Providers ativos: {active_providers}."
)
return session
def benchmark_onnx(
session,
inputs_np: List[np.ndarray],
warmup: int,
repeat: int,
label: str,
) -> Tuple[dict, List[dict]]:
input_name = session.get_inputs()[0].name
times = []
rows = []
print(f"\n[BENCH] {label} | warmup={warmup} repeat={repeat}")
# Warmup
for i in range(max(0, warmup)):
x = inputs_np[i % len(inputs_np)]
_ = session.run(None, {input_name: x})
# Medição
total_iter = len(inputs_np) * max(1, repeat)
idx = 0
for r in range(max(1, repeat)):
for sample_idx, x in enumerate(inputs_np):
t0 = time.perf_counter()
_ = session.run(None, {input_name: x})
dt_ms = (time.perf_counter() - t0) * 1000.0
times.append(dt_ms)
rows.append({
"engine": label,
"repeat": r,
"sample_idx": sample_idx,
"iter_idx": idx,
"latency_ms": dt_ms,
})
idx += 1
if idx % 25 == 0 or idx == total_iter:
print(f" {idx:04d}/{total_iter:04d} | last={dt_ms:.2f}ms")
summary = summarize_times(times)
summary["engine"] = label
return summary, rows
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="config.json")
parser.add_argument("--checkpoint", default="")
parser.add_argument("--onnx", default="")
parser.add_argument("--train-script", default="_8_train_multihead.py")
parser.add_argument("--labelmap", default="dataset/labelmap.txt")
parser.add_argument("--split_folder", default="val", choices=["train", "val", "test"])
parser.add_argument("--root_override", default=None)
parser.add_argument("--norm_stats", default=None)
parser.add_argument("--max_samples", type=int, default=50)
parser.add_argument("--start_idx", type=int, default=0)
parser.add_argument("--warmup", type=int, default=10)
parser.add_argument("--repeat", type=int, default=5)
parser.add_argument("--device", default="cuda", choices=["cuda", "cpu"])
parser.add_argument("--onnx_provider", default="cuda", choices=["cuda", "cpu", "tensorrt"])
parser.add_argument("--skip_torch_fp32", action="store_true")
parser.add_argument("--skip_torch_amp", action="store_true")
parser.add_argument("--skip_onnx", action="store_true")
parser.add_argument(
"--onnx_has_norm",
action="store_true",
help="Use quando o ONNX já inclui normalização interna. Nesse caso o ONNX recebe tensor 0..1 cru.",
)
parser.add_argument("--out_dir", default=None)
args = parser.parse_args()
config_path = resolve_path(args.config, Path.cwd())
train_script_path = resolve_path(args.train_script, Path.cwd())
labelmap_path = resolve_path(args.labelmap, Path.cwd())
if config_path is None or not config_path.is_file():
raise FileNotFoundError(f"Config não encontrado: {config_path}")
if train_script_path is None or not train_script_path.is_file():
raise FileNotFoundError(f"Train script não encontrado: {train_script_path}")
if labelmap_path is None or not labelmap_path.is_file():
raise FileNotFoundError(f"Labelmap não encontrado: {labelmap_path}")
config_dir = config_path.parent
config = load_json(config_path)
train_mod = import_train_module(train_script_path)
W, H = config.get("resolucao", [1024, 640])
W = int(W)
H = int(H)
backbone = config.get("backbone", "nvidia/mit-b1")
input_channel_names = train_mod.get_input_channel_names(config)
input_channel_indices = train_mod.get_input_channel_indices(config)
channels = len(input_channel_names)
checkpoint_path, onnx_path, ckpt_name = resolve_model_artifact_paths(
args=args,
config=config,
config_dir=config_dir,
channels=channels,
)
semantic_id2label, semantic_label2id, ignore_from_labelmap = train_mod.load_labelmap(
str(labelmap_path)
)
heads_config = train_mod.build_heads_config(
config,
ignore_index=int(ignore_from_labelmap)
)
heads_config["semantic"]["num_classes"] = int(len(semantic_id2label))
heads_config["semantic"]["ignore_index"] = int(ignore_from_labelmap)
output_heads = list(heads_config.keys())
save_dir = (
config_dir
/ "backup"
/ config.get("modelo", "segformer_b1")
/ config.get("model_name", "test")
/ f"{config.get('fusion_mode', 'stacked')}_raw{channels}"
)
norm_stats_path = find_norm_stats(
config=config,
config_dir=config_dir,
save_dir=save_dir,
explicit=args.norm_stats,
)
mean, std, norm_stats_used = load_norm_stats(
norm_stats_path,
channel_indices=input_channel_indices,
channel_names=input_channel_names,
)
if args.root_override:
root = resolve_path(args.root_override, Path.cwd())
else:
root = (config_dir / "dataset" / "split" / args.split_folder).resolve()
if root is None or not root.is_dir():
raise FileNotFoundError(f"Root de dados não encontrado: {root}")
samples = collect_tensor_samples(
root=root,
max_samples=args.max_samples,
start_idx=args.start_idx,
)
if args.out_dir:
out_dir = resolve_path(args.out_dir, Path.cwd())
else:
out_dir = onnx_path.parent / "benchmarks"
assert out_dir is not None
out_dir.mkdir(parents=True, exist_ok=True)
use_cuda = args.device == "cuda" and torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
if args.device == "cuda" and not torch.cuda.is_available():
print("[WARN] CUDA indisponível. Usando CPU no PyTorch.")
print("==========================================")
print("Benchmark PyTorch vs ONNX")
print(f"Config : {config_path}")
print(f"Checkpoint : {checkpoint_path}")
print(f"ONNX : {onnx_path}")
print(f"Root : {root}")
print(f"Samples : {len(samples)}")
print(f"Warmup : {args.warmup}")
print(f"Repeat : {args.repeat}")
print(f"Backbone : {backbone}")
print(f"Input shape : [1, {channels}, {H}, {W}]")
print(f"Channels : {input_channel_names} idx={input_channel_indices}")
print(f"Heads : {output_heads}")
print(f"Device : {device}")
print(f"ONNX provider: {args.onnx_provider}")
print(f"ONNX has norm: {args.onnx_has_norm}")
print(f"Out dir : {out_dir}")
print("==========================================")
if args.onnx_has_norm:
print("\n[DATA] Carregando inputs 0..1 crus na RAM...")
else:
print("\n[DATA] Carregando inputs normalizados na RAM...")
inputs_np = load_inputs_as_numpy(
samples=samples,
channels=channels,
channel_indices=input_channel_indices,
mean=mean,
std=std,
target_hw=(H, W),
normalize_input=not args.onnx_has_norm,
)
print(f"[DATA] Inputs carregados: {len(inputs_np)}")
summaries = []
all_rows = []
# ========================================================
# PyTorch
# ========================================================
need_torch = not args.skip_torch_fp32 or not args.skip_torch_amp
if need_torch:
print("\n[MODEL] Montando PyTorch...")
model = train_mod.build_model(
backbone=backbone,
channels=channels,
heads_config=heads_config,
semantic_id2label=semantic_id2label,
semantic_label2id=semantic_label2id,
)
ckpt = torch.load(str(checkpoint_path), map_location="cpu", weights_only=False)
if "model" not in ckpt:
raise RuntimeError("Checkpoint não contém chave 'model'.")
model.load_state_dict(ckpt["model"], strict=True)
model.to(device)
model.eval()
torch_model = TorchTupleWrapper(
model=model,
output_heads=output_heads,
).to(device)
torch_model.eval()
clear_cuda()
if not args.skip_torch_fp32:
summary, rows = benchmark_torch(
model=torch_model,
inputs_np=inputs_np,
device=device,
warmup=args.warmup,
repeat=args.repeat,
amp=False,
label="torch_fp32",
)
summaries.append(summary)
all_rows.extend(rows)
clear_cuda()
if not args.skip_torch_amp:
summary, rows = benchmark_torch(
model=torch_model,
inputs_np=inputs_np,
device=device,
warmup=args.warmup,
repeat=args.repeat,
amp=True,
label="torch_amp_fp16",
)
summaries.append(summary)
all_rows.extend(rows)
del torch_model
del model
clear_cuda()
# ========================================================
# ONNX
# ========================================================
if not args.skip_onnx:
print("\n[ONNX] Carregando sessão...")
session = create_onnx_session(
onnx_path=onnx_path,
provider=args.onnx_provider,
)
summary, rows = benchmark_onnx(
session=session,
inputs_np=inputs_np,
warmup=args.warmup,
repeat=args.repeat,
label=f"onnx_{args.onnx_provider}",
)
summaries.append(summary)
all_rows.extend(rows)
# ========================================================
# Relatório
# ========================================================
print("\n========== RESUMO ==========")
for s in summaries:
print(
f"{s['engine']:<16} "
f"n={s['n']:<4} "
f"mean={s['mean_ms']:.3f}ms "
f"median={s['median_ms']:.3f}ms "
f"p95={s['p95_ms']:.3f}ms "
f"p99={s['p99_ms']:.3f}ms "
f"fps_mean={s['fps_mean']:.2f} "
f"fps_p95={s['fps_p95_latency']:.2f}"
)
base_name = f"{onnx_path.stem}_{args.onnx_provider}"
report_json = out_dir / f"{base_name}_benchmark_report.json"
report_csv = out_dir / f"{base_name}_benchmark_rows.csv"
report = {
"config": str(config_path),
"checkpoint": str(checkpoint_path),
"ckpt_name": ckpt_name,
"onnx": str(onnx_path),
"onnx_has_norm": bool(args.onnx_has_norm),
"root": str(root),
"samples": len(samples),
"warmup": int(args.warmup),
"repeat": int(args.repeat),
"input_shape": [1, channels, H, W],
"input_channel_names": input_channel_names,
"input_channel_indices": input_channel_indices,
"heads": output_heads,
"norm_stats_used": norm_stats_used,
"onnx_provider": args.onnx_provider,
"device": str(device),
"summaries": summaries,
}
save_json(report_json, report)
save_csv(report_csv, all_rows)
print(f"\n[OK] JSON salvo em: {report_json}")
print(f"[OK] CSV salvo em : {report_csv}")
print("\nBenchmark finalizado.")
if __name__ == "__main__":
main()