RCLane_2D_Detection / eval_checkpoints.py
huyluongngoc's picture
fix(RCLane): stabilize lane decode and ONNX inference
73a2d91
Raw
History Blame Contribute Delete
8.77 kB
"""Evaluate RCLane checkpoints on one CARLA split.
F1-only evaluation reads sparse lane annotations directly. With ``--with-loss``,
the dense GT cache is warmed once and shared by every checkpoint. Results are
written atomically after each model so a long sweep remains inspectable if it is
interrupted.
"""
import argparse
import gc
import glob
import hashlib
import json
import os
import re
import time
from pathlib import Path
import cv2
import torch
from loss import RCLaneLoss
from rclane import RCLane
from train import build_dataset_split, evaluate_f1, warm_cache
def checkpoint_order(path):
match = re.search(r"_e(\d+)\.pth$", os.path.basename(path))
return (int(match.group(1)) if match else 10**9, os.path.basename(path))
def save_results(path, records):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
with tmp.open("w") as handle:
json.dump(records, handle, indent=2, sort_keys=True)
handle.write("\n")
os.replace(tmp, path)
def state_dict_digest(state_dict):
"""Content hash used to avoid re-evaluating byte-identical model weights."""
digest = hashlib.sha256()
for name, value in state_dict.items():
tensor = value.detach().cpu().contiguous()
digest.update(name.encode())
digest.update(str(tensor.dtype).encode())
digest.update(str(tuple(tensor.shape)).encode())
# View raw storage as bytes so uncommon dtypes such as bfloat16 remain
# hashable even when NumPy cannot represent them directly.
digest.update(tensor.reshape(-1).view(torch.uint8).numpy().tobytes())
return digest.hexdigest()
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--data-root", default="data/dataset")
parser.add_argument(
"--eval-list",
required=True,
help="CARLA JSONL label file, relative to --data-root",
)
parser.add_argument(
"--checkpoints",
required=True,
help="checkpoint path or glob pattern (quote globs in the shell)",
)
parser.add_argument("--output", default="eval_results/checkpoint_eval.json")
parser.add_argument("--cache-dir", default="gt_cache_eval")
parser.add_argument("--vision", default="b0", choices=["b0", "b1", "b2"])
parser.add_argument("--device", default="cuda")
parser.add_argument("--batch", type=int, default=1)
parser.add_argument("--eval-batch", type=int, default=1)
parser.add_argument("--workers", type=int, default=11,
help="workers used once to warm the GT cache")
parser.add_argument("--eval-workers", type=int, default=2)
parser.add_argument("--eval-decode-workers", type=int, default=7)
parser.add_argument("--prefetch", type=int, default=2)
parser.add_argument("--eval-log-every", type=int, default=100)
parser.add_argument("--max-samples", type=int, default=None)
parser.add_argument("--no-amp", action="store_true")
parser.add_argument(
"--with-loss", action="store_true",
help="also encode dense GT and calculate loss; slower and unnecessary for F1 ranking",
)
parser.add_argument("--f1-iou-thresh", type=float, default=0.5)
parser.add_argument("--f1-lane-width", type=int, default=30)
parser.add_argument("--f1-eval-scale", type=float, default=0.25)
parser.add_argument("--decode-seg-threshold", type=float, default=0.5)
parser.add_argument("--decode-seed-threshold", type=float, default=None)
parser.add_argument("--decode-seed-min-dist", type=int, default=2)
parser.add_argument("--decode-score-thresh", type=float, default=0.10)
parser.add_argument("--decode-nms-iou", type=float, default=0.5)
parser.add_argument("--decode-max-seeds", type=int, default=1024)
parser.add_argument("--decode-nms-max-lanes", type=int, default=128)
parser.add_argument("--decode-nms-scale", type=float, default=0.25)
return parser.parse_args()
def main():
args = parse_args()
args.warm_cache = args.with_loss
args.eval_f1 = True
args.eval_skip_loss = not args.with_loss
args.eval_subset = args.max_samples
device = torch.device(args.device)
if device.type == "cuda" and not torch.cuda.is_available():
raise RuntimeError("CUDA requested but torch.cuda.is_available() is false")
if device.type == "cuda":
device = torch.device("cuda", device.index or 0)
torch.cuda.set_device(device)
torch.backends.cudnn.benchmark = True
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
torch.set_float32_matmul_precision("high")
torch.set_num_threads(1)
cv2.setNumThreads(1)
checkpoints = sorted(glob.glob(args.checkpoints), key=checkpoint_order)
if not checkpoints:
raise FileNotFoundError(f"no checkpoints match {args.checkpoints!r}")
dataset = build_dataset_split(
dataset="carla",
data_root=args.data_root,
list_file=args.eval_list,
cache_dir=args.cache_dir,
max_samples=args.max_samples,
)
print(f"dataset={args.eval_list} | samples={len(dataset)}")
print(f"checkpoints={len(checkpoints)} | device={device} | batch={args.eval_batch}")
warm_cache(dataset, args, device, rank=0, world_size=1, name="eval")
model = RCLane(vision=args.vision, img_size=(320, 800)).to(device)
criterion = RCLaneLoss()
use_amp = not args.no_amp and device.type == "cuda"
amp_dtype = torch.float16
results = []
seen_models = {}
for index, checkpoint_path in enumerate(checkpoints, 1):
started = time.time()
checkpoint = torch.load(
checkpoint_path, map_location="cpu", weights_only=False
)
epoch = int(checkpoint.get("epoch", -1))
step = int(checkpoint.get("step", -1))
model_digest = state_dict_digest(checkpoint["model"])
name = os.path.basename(checkpoint_path)
if model_digest in seen_models:
source = seen_models[model_digest]
record = {
**source,
"checkpoint": name,
"path": os.path.abspath(checkpoint_path),
"epoch": epoch,
"step": step,
"model_sha256": model_digest,
"reused_from": source["checkpoint"],
"wall_time": time.time() - started,
}
results.append(record)
save_results(args.output, results)
print(
f"[{index}/{len(checkpoints)}] {name}: identical weights to "
f"{source['checkpoint']}; reused F1={record['val_f1']:.6f}"
)
del checkpoint
gc.collect()
continue
model.load_state_dict(checkpoint["model"])
del checkpoint
gc.collect()
if device.type == "cuda":
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats(device)
print(f"[{index}/{len(checkpoints)}] evaluating {name} (epoch={epoch})")
metrics = evaluate_f1(
model, criterion, dataset, args, device, use_amp, amp_dtype,
rank=0, world_size=1,
)
record = {
"checkpoint": name,
"path": os.path.abspath(checkpoint_path),
"epoch": epoch,
"step": step,
"model_sha256": model_digest,
**{key: float(value) for key, value in metrics.items()},
"wall_time": time.time() - started,
}
if device.type == "cuda":
record["peak_cuda_memory_gib"] = (
torch.cuda.max_memory_allocated(device) / (1024 ** 3)
)
results.append(record)
seen_models[model_digest] = record
save_results(args.output, results)
loss_text = (
f"{record['val_loss']:.6f}" if "val_loss" in record else "skipped"
)
print(
f" {name}: F1={record['val_f1']:.6f} "
f"P={record['val_precision']:.6f} R={record['val_recall']:.6f} "
f"loss={loss_text} time={record['wall_time']:.1f}s "
f"VRAM={record.get('peak_cuda_memory_gib', 0.0):.2f}GiB"
)
ranked = sorted(
results,
key=lambda item: (
item["val_f1"], item["val_precision"], item["val_recall"],
-item.get("val_loss", float("inf")),
),
reverse=True,
)
print("ranking:")
for rank, item in enumerate(ranked, 1):
print(
f" {rank:2d}. {item['checkpoint']}: "
f"F1={item['val_f1']:.6f}"
)
print(f"best={ranked[0]['checkpoint']} | output={args.output}")
if __name__ == "__main__":
main()