RISHIVEL's picture
space deploy: fix zerogpu pickling
087643a
Raw
History Blame Contribute Delete
6.4 kB
# Fine-tunes RT-DETR on the prepared DocLayNet data. Every hyperparameter
# is set explicitly (nothing left to Ultralytics defaults) and the run
# writes a metadata file with GPU, wall-clock time and library versions,
# since the brief caps Part A at 50% if the run isn't reproducible.
#
# Usage:
# python scripts/train.py --data data/doclaynet/doclaynet.yaml
# python scripts/train.py --data ... --epochs 1 --smoke # CPU smoke test
from __future__ import annotations
import argparse
import json
import subprocess
import time
from pathlib import Path
RANDOM_SEED = 42
def _build_train_args(data: str, epochs: int, batch: int, imgsz: int, device: str) -> dict:
"""All the hyperparameters. The augmentation block is the part worth
reading - three of Ultralytics' defaults are actively wrong for
documents:
- fliplr 0.5 mirrors pages, which don't have a valid mirrored layout
(text direction, page numbers in fixed corners)
- flipud same problem, more obviously (upside-down page)
- mosaic composites 4 images into one, destroying whole-page
structure (header at top, footer at bottom) that the model needs
Kept small rotation + mild perspective - those model real scan/camera
skew and are the only hedge against the hidden eval set containing
photographed rather than rendered pages.
lr0=1e-4 not the usual YOLO 1e-2 - DETR-family transformer components
are unstable at high LR, and I'm fine-tuning from COCO weights, not
training from scratch."""
return {
"data": data,
"epochs": epochs,
"imgsz": imgsz,
"batch": batch,
"device": device,
"seed": RANDOM_SEED,
"deterministic": True,
"amp": True, # needed to fit RT-DETR-L at batch 8 in 16GB on a T4
"optimizer": "AdamW",
"lr0": 1e-4,
"lrf": 0.01,
"weight_decay": 1e-4,
"warmup_epochs": 3.0,
"cos_lr": True,
"save_period": 1, # checkpoint every epoch - Kaggle sessions can drop
"patience": 10,
"workers": 2, # Kaggle's few CPU cores - more workers stall
"cache": False, # 8k pages at 1025px won't fit in RAM
"val": True,
"plots": True,
# --- augmentation, see docstring above ---
"fliplr": 0.0,
"flipud": 0.0,
"mosaic": 0.0,
"degrees": 3.0,
"perspective": 0.0005,
"translate": 0.05,
"scale": 0.2,
"shear": 0.0,
"hsv_h": 0.0, # document colour isn't a useful signal
"hsv_s": 0.2,
"hsv_v": 0.2, # scan brightness genuinely varies
"erasing": 0.0, # occluding part of a region changes its class
}
def _git_commit() -> str:
try:
return subprocess.check_output(
["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL
).strip()
except Exception:
return "unknown"
def _write_run_metadata(out_dir: Path, train_args: dict, seconds: float) -> None:
"""Everything needed to reproduce this run. GPU name matters most -
"took 3 hours" means nothing without knowing what it ran on, and
Kaggle doesn't always give you the accelerator you asked for."""
import torch
import ultralytics
gpu_name, gpu_memory_gb = "cpu", None
if torch.cuda.is_available():
gpu_name = torch.cuda.get_device_name(0)
gpu_memory_gb = round(
torch.cuda.get_device_properties(0).total_memory / 1024**3, 1
)
metadata = {
"git_commit": _git_commit(),
"hyperparameters": train_args,
"hardware": {
"gpu": gpu_name,
"gpu_memory_gb": gpu_memory_gb,
"torch_version": torch.__version__,
"cuda_version": torch.version.cuda,
},
"ultralytics_version": ultralytics.__version__,
"wall_clock_seconds": round(seconds, 1),
"wall_clock_human": f"{seconds / 3600:.2f} h",
}
path = out_dir / "run_metadata.json"
path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
print(f"\nRun metadata written to {path}")
print(f" GPU: {gpu_name} ({gpu_memory_gb} GB)")
print(f" Time: {seconds / 3600:.2f} h")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--data", default="data/doclaynet/doclaynet.yaml")
parser.add_argument("--epochs", type=int, default=30)
parser.add_argument("--batch", type=int, default=8)
parser.add_argument("--imgsz", type=int, default=640)
parser.add_argument("--device", default="0")
parser.add_argument("--name", default="rtdetr_doclaynet")
parser.add_argument(
"--smoke", action="store_true",
help="tiny CPU run to prove the script works before spending GPU quota",
)
args = parser.parse_args()
if args.smoke:
args.epochs, args.batch, args.device, args.imgsz = 1, 2, "cpu", 320
args.name = "smoke"
from ultralytics import RTDETR
from ultralytics.utils import SETTINGS
# Ultralytics auto-registers a Ray Tune callback whenever `ray` is
# importable, no check for whether Tune is actually running. Kaggle
# ships ray pre-installed for other stuff, so this callback silently
# activates and crashes end-of-epoch calling a Ray internal API that
# doesn't exist in the installed version. Not using Ray Tune anywhere
# here, so just turn the integration off before .train() runs.
SETTINGS["raytune"] = False
# Starting from COCO weights, not scratch. None of my 11 classes exist
# in COCO so the detection head gets zero class knowledge for free -
# the pretrained backbone just gives generic visual features. Training
# from random init wouldn't converge in the epoch budget I have.
model = RTDETR("rtdetr-l.pt")
train_args = _build_train_args(args.data, args.epochs, args.batch, args.imgsz, args.device)
train_args["name"] = args.name
print(f"Training RT-DETR-L | {args.epochs} epochs | batch {args.batch} | {args.imgsz}px")
started = time.time()
results = model.train(**train_args)
elapsed = time.time() - started
out_dir = Path(results.save_dir)
_write_run_metadata(out_dir, train_args, elapsed)
print(f"\nBest weights: {out_dir / 'weights' / 'best.pt'}")
if __name__ == "__main__":
main()