docs(cookbooks): add COCO2017 full-dataset training cookbook (#1384)

* add COCO2017 full-dataset training cookbook
* tune COCO2017 notebook for GPU throughput
* download COCO2017 before env setup
* fail loudly on incomplete COCO2017 download
* correct augmentation-backend and auto-batch defaults
* tune COCO2017 for high-end GPUs
* address review feedback on COCO2017 cookbook and AGENTS.md

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Jirka Borovec
2026-08-24 10:59:44 +02:00
committed by GitHub
parent f7ebc74dc6
commit ed02dc834a
5 changed files with 406 additions and 3 deletions
+2 -2
View File
@@ -220,9 +220,9 @@ uv run twine check --strict dist/*
**Augmentations:**
- Default training, validation, prediction, and export preprocessing use torchvision-native transforms.
- **Training** uses torchvision-native transforms **unless Albumentations is installed**`augmentation_backend="cpu"` (the default) then auto-selects Albumentations and injects the default `AUG_CONFIG`, even when `aug_config=None`. Identical training code therefore resolves differently across environments; pass `augmentation_backend="torchvision"` to pin the torchvision pipeline regardless of what is installed. This backend selection only reaches the dataset builders: `_route_transforms` chooses Albumentations only for `image_set == "train"`, so validation always stays on torchvision, and prediction (`src/rfdetr/detr.py`) and export (`src/rfdetr/export/main.py`) call torchvision preprocessing directly — do not change inference/export behavior based on the training backend.
- Custom non-empty `aug_config` values on the CPU path use Albumentations and require `rfdetr[augment]`.
- `augmentation_backend="gpu"` uses Kornia and requires `rfdetr[augment]`; `augmentation_backend="auto"` falls back to CPU when CUDA or Kornia is unavailable.
- `augmentation_backend="auto"` resolves to Kornia when CUDA and Kornia are available, falling back to CPU otherwise; `augmentation_backend="gpu"` pins Kornia and requires `rfdetr[augment]`.
**Model Architecture:**
+1
View File
@@ -59,3 +59,4 @@ For newly added or updated notebooks, write markdown cells in plain, notebook-po
| `fine-tune_segmentation.ipynb` | Fine-Tune RF-DETR Instance Segmentation | v1.8.2 |
| `inference-latency-benchmark.ipynb` | Inference Latency Benchmark | v1.8.2 |
| `pytorch-lightning.ipynb` | Training with PyTorch Lightning | v1.6.0 |
| `train-coco2017.ipynb` | Train RF-DETR Nano on COCO2017 | v1.10.0 |
+6
View File
@@ -1,4 +1,10 @@
cards:
- href: train-coco2017/
name: "Train RF-DETR Nano on COCO2017"
labels: [TRAINING, COCO2017, PYTORCH LIGHTNING]
version: v1.10.0
author: Borda
description: "Download the full public COCO2017 dataset and train RF-DETR Nano for 40 epochs end to end on a high-end GPU (A100 / H100 / RTX PRO 6000 — e.g. Colab Pro+ with an A100 runtime), using shipped defaults that already carry the latest training-throughput optimizations."
- href: export-coreml/
name: "Export to Native CoreML & Run Inference"
labels: [EXPORT, INFERENCE, DEPLOY]
+391
View File
@@ -0,0 +1,391 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "62c7a28d",
"metadata": {},
"source": [
"# Train RF-DETR Nano on COCO2017 (high-end GPU)\n",
"\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/rf-detr/blob/develop/docs/cookbooks/train-coco2017.ipynb)\n",
"\n",
"Downloads the full public **COCO2017** dataset and trains **RF-DETR Nano** for 40 epochs, end to end. This\n",
"notebook is **sized for a high-end GPU** — an A100, an H100, or an RTX PRO 6000 (e.g. Colab Pro+ with an A100\n",
"runtime, or a GCP G4 VM) — and its batch size, RAM-disk step, and worker count all assume that class of machine.\n",
"It still runs on a smaller GPU (T4/L4): switch `batch_size` back to `\"auto\"` in the training cell and the rest\n",
"adapts, but expect the run to take far longer, and skip the RAM-disk move on a low-RAM host — it needs ~19 GB\n",
"free in `/dev/shm`.\n",
"\n",
"The throughput work is all in the shipped defaults — the notebook adds host-adaptive knobs (`num_workers`,\n",
"`seed`), a batch size measured on high-end hardware, and a learning-rate schedule sized for a 40-epoch run, each\n",
"explained where it is set.\n",
"\n",
"Kept deliberately minimal: download, train, plot the metrics `CSVLogger` wrote during training. For dataset\n",
"preview, checkpoint saving, and inference visualization, see `fine-tune_detection.ipynb`.\n",
"\n",
"## Why this notebook is simple on purpose\n",
"\n",
"Recent releases moved several training-throughput fixes directly into the default configuration, so a stock run\n",
"picks them up automatically:\n",
"\n",
"- **Validation forwards one model per epoch, not two.** The base-model forward pass used to run alongside the EMA\n",
" forward every validation batch; it is now skipped when `use_ema=True` (the default).\n",
"- **`grad_accum_steps` now defaults to `1`** instead of `4`. Gradient accumulation is an explicit opt-in — raise\n",
" `batch_size` for your GPU first, and reach for accumulation only when memory forces a smaller physical batch.\n",
" Measured on one L4: `batch_size=16, grad_accum_steps=1` ran 27% faster per epoch than `batch_size=4,\n",
" grad_accum_steps=4` at the same nominal effective batch, with equal mAP.\n",
"- **`eval_batch_size`** decouples the validation/test dataloaders from the training micro-batch size, so a small\n",
" training batch no longer forces small (slower) evaluation batches.\n",
"- **COCO mAP computation reads each image's detection scores once instead of once per detection.**\n",
"- **The transformer skips materializing tensors it would otherwise reuse unchanged** on the single-feature-level\n",
" path that Nano (and every current detection size) uses by default.\n",
"- **Pre-training sanity-check validation is skipped by default.**\n",
"\n",
"None of this needs a flag — it is what `RFDETRNano().train()` already does. TensorBoard logging is turned off so\n",
"the notebook does not require the `loggers` extra; `CSVLogger` is always on and is what section 5 plots.\n",
"\n",
"## Scope and honest expectations\n",
"\n",
"COCO2017 has 118,287 training images and 5,000 validation images — two to three orders of magnitude larger than\n",
"the small Roboflow Universe datasets in the other fine-tuning cookbooks. There is no COCO2017-scale timing\n",
"measurement in this repository yet, so no fixed \"N minutes per epoch\" number is given here. **Time your own first\n",
"epoch** (the progress bar prints per-epoch elapsed time) before committing GPU time to all 40. As a rough anchor:\n",
"an internal L4 measurement on a ~6,900-image dataset with `rfdetr-small` at batch 16 took ~324 s/epoch\n",
"(train + validation); COCO2017 has about 17x more training images and 2.5x more validation images, and\n",
"`rfdetr-nano` is smaller and runs at a lower resolution than `rfdetr-small`, which pulls the other way. Expect the\n",
"full 40-epoch run to take multiple hours even on a fast GPU.\n",
"\n",
"> **On the target hardware (A100/H100/RTX PRO 6000) the full 40-epoch run fits comfortably in one session**, and\n",
"> Colab Pro+ background execution keeps it alive with the browser closed. Training still checkpoints every\n",
"> `checkpoint_interval` epochs (10 by default) to `last.ckpt` as a safety net: if a session does disconnect,\n",
"> reopen the notebook, set `train_config.resume` to that file's path, and re-run the training cell to continue.\n",
"> On a free-tier T4/L4 the session is likely to end before 40 epochs do — plan on resuming.\n",
"\n",
"COCO2017 also needs about 19 GB of disk once downloaded (the archives are deleted right after extraction to avoid\n",
"a ~38 GB peak). Confirm your Colab runtime has that much free space before starting the download."
]
},
{
"cell_type": "markdown",
"id": "8685391b",
"metadata": {},
"source": [
"## 1 - Download COCO2017\n",
"\n",
"The download comes first because it is by far the longest step and needs nothing installed — only `wget` and\n",
"`unzip`, both already on a Colab runtime. Anything the environment setup below does to the session (including a\n",
"runtime restart, if pip asks for one) leaves the extracted dataset on disk untouched.\n",
"\n",
"Plain `wget`/`unzip` into the standard COCO layout RF-DETR's `dataset_file=\"coco\"` loader expects:\n",
"`coco2017/train2017/`, `coco2017/val2017/`, `coco2017/annotations/instances_{train,val}2017.json`.\n",
"\n",
"Each archive is downloaded, extracted, and deleted before the next one starts, so the disk never holds more than\n",
"one archive alongside the extracted data. The `&&` chaining is deliberate: a notebook shell cell does **not** stop\n",
"on a failed command, so a bare sequence would run `unzip` on a truncated download and `rm` on a failed extraction,\n",
"then scroll past — the failure would only surface much later, as a missing-image error part-way into training.\n",
"`wget -c` resumes an interrupted download instead of restarting it, and `unzip -o` overwrites files already\n",
"extracted, so re-running this cell after a disconnected Colab session replaces any partially extracted file. The\n",
"closing `df -h` shows the disk headroom left; the extracted dataset needs about 19 GB."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "86e582e8",
"metadata": {
"title": "[bash]"
},
"outputs": [],
"source": [
"!mkdir -p datasets/coco2017\n",
"!cd datasets/coco2017 && wget -c -q --show-progress http://images.cocodataset.org/zips/train2017.zip && unzip -o -q train2017.zip && rm -f train2017.zip\n",
"!cd datasets/coco2017 && wget -c -q --show-progress http://images.cocodataset.org/zips/val2017.zip && unzip -o -q val2017.zip && rm -f val2017.zip\n",
"!cd datasets/coco2017 && wget -c -q --show-progress http://images.cocodataset.org/annotations/annotations_trainval2017.zip && unzip -o -q annotations_trainval2017.zip && rm -f annotations_trainval2017.zip\n",
"!df -h datasets/coco2017"
]
},
{
"cell_type": "markdown",
"id": "e54c87fb",
"metadata": {},
"source": [
"Verify the extraction before spending GPU time on it. COCO2017 ships 118,287 training and 5,000 validation\n",
"images; a short count catches a truncated download or an out-of-disk extraction here, at the cost of a few\n",
"seconds, instead of several epochs later when the training loop first reaches a missing file."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "71280763",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"\n",
"COCO_ROOT = Path(\"datasets/coco2017\")\n",
"EXPECTED_IMAGES = {\"train2017\": 118_287, \"val2017\": 5_000}\n",
"\n",
"for split, expected in EXPECTED_IMAGES.items():\n",
" found = sum(1 for _ in (COCO_ROOT / split).glob(\"*.jpg\"))\n",
" assert found == expected, (\n",
" f\"{COCO_ROOT / split} holds {found} images, expected {expected}. \"\n",
" \"Re-run the download cell above; check the output of `df -h` for a full disk.\"\n",
" )\n",
" annotations = COCO_ROOT / \"annotations\" / f\"instances_{split}.json\"\n",
" assert annotations.is_file(), f\"Missing {annotations}. Re-run the download cell above.\"\n",
"\n",
"print(\"COCO2017 is complete.\")"
]
},
{
"cell_type": "markdown",
"id": "29e2dc9d",
"metadata": {},
"source": [
"## 2 - Set up the environment\n",
"\n",
"The training-throughput work described above has not shipped in a tagged release yet, so this cell installs from\n",
"the `develop` branch. Once a release containing it is out, replace this with\n",
"`pip install -q \"rfdetr[train,augment,visual]>=1.10.0\"`.\n",
"\n",
"**GPU required.** Training needs a CUDA GPU, and this notebook's batch size assumes a high-end one — in Colab:\n",
"**Runtime → Change runtime type → A100 GPU** (Pro/Pro+ plans; also enable High-RAM so the `/dev/shm` move below\n",
"fits)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "af7ed6ba",
"metadata": {
"title": "[bash]"
},
"outputs": [],
"source": [
"!pip install -q \"rfdetr[train,augment,visual] @ git+https://github.com/roboflow/rf-detr.git@develop\""
]
},
{
"cell_type": "markdown",
"id": "030d2c6a",
"metadata": {},
"source": [
"## 3 - Move the dataset to /dev/shm\n",
"\n",
"`/dev/shm` is Linux's tmpfs RAM disk — after the move, every DataLoader read is a memory access with no storage\n",
"I/O at all. On the high-end hosts this notebook targets, RAM is plentiful (a GCP G4 shape carries 180+ GB, an\n",
"A100 Colab runtime 80+ GB; `/dev/shm` is sized to half of RAM by default), so the ~19 GB dataset fits. It is a\n",
"real win there because cloud persistent disks throttle throughput in proportion to provisioned size — a typical\n",
"100-200 GB boot disk serves 118k random-access JPEG reads per epoch slowly. JPEG *decode* cost is unaffected by\n",
"where the bytes come from; that CPU cost is what the high `num_workers` below parallelizes away.\n",
"\n",
"A move (not a copy) keeps a single instance of the dataset on the machine. Two consequences to know:\n",
"\n",
"- **tmpfs does not survive a runtime restart** — which is why this cell sits *after* the pip install (pip can\n",
" restart the runtime). If the session restarts later anyway, the dataset is gone: re-run the download cell.\n",
"- **If `/dev/shm` is too small, `mv` fails part-way** and leaves files split across both locations. Recover with\n",
" `mv /dev/shm/coco2017/* datasets/coco2017/` and train from disk — or better, avoid it: on Colab pick a\n",
" High-RAM runtime; `df -h /dev/shm` shows capacity before you commit.\n",
"\n",
"The `test -d` guard makes the cell safe to re-run: once the dataset already sits in `/dev/shm`, the `mv` is\n",
"skipped instead of nesting a second copy inside it."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5d1ee6ac",
"metadata": {
"title": "[bash]"
},
"outputs": [],
"source": [
"!df -h /dev/shm\n",
"!test -d /dev/shm/coco2017 || mv datasets/coco2017 /dev/shm/coco2017"
]
},
{
"cell_type": "markdown",
"id": "bd6645a5",
"metadata": {},
"source": [
"## 4 - Load the model\n",
"\n",
"`RFDETRNano()` loads the released COCO-pretrained Nano checkpoint by default — this notebook continues training\n",
"from it rather than from random initialization. Training from scratch is not the recommended path for RF-DETR;\n",
"continuing from the pretrained checkpoint on the same dataset it was pretrained on still exercises the full\n",
"40-epoch training loop and every optimization listed above, which is what this notebook demonstrates.\n",
"\n",
"`num_classes` is left at its default of `90` — the standard COCO category-id space RF-DETR's `dataset_file=\"coco\"`\n",
"loader and the pretrained checkpoint both already use, so it does not need to be passed explicitly."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4e42f49c",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from pathlib import Path\n",
"\n",
"import torch\n",
"\n",
"from rfdetr import RFDETRNano\n",
"from rfdetr.visualize.training import plot_loss_metrics, plot_map_metrics\n",
"\n",
"if not torch.cuda.is_available():\n",
" raise RuntimeError(\"This notebook requires a CUDA GPU. In Colab: Runtime -> Change runtime type -> A100 GPU.\")\n",
"\n",
"# Repeated from the verification cell so this section still runs on its own after a runtime restart.\n",
"COCO_ROOT = Path(\"datasets/coco2017\")\n",
"RAMDISK_ROOT = Path(\"/dev/shm/coco2017\")\n",
"if RAMDISK_ROOT.is_dir():\n",
" COCO_ROOT = RAMDISK_ROOT\n",
"OUTPUT_DIR = \"output/det_coco2017_nano\"\n",
"EPOCHS = 40\n",
"# Capped: measured on a 48-core host, uncapped workers sat ~15% CPU-utilized while holding ~70% of RAM.\n",
"NUM_WORKERS = min(os.cpu_count() or 2, 16)\n",
"# Batch 128 was measured at ~80% GPU memory on a high-end (80+ GB) GPU — sized for an A100, H100, or\n",
"# RTX PRO 6000. On a smaller GPU replace this with \"auto\" and the probe finds the largest batch that fits.\n",
"BATCH_SIZE = 128\n",
"\n",
"model = RFDETRNano() # type: ignore[no-untyped-call]"
]
},
{
"cell_type": "markdown",
"id": "6bfb5398",
"metadata": {},
"source": [
"## 5 - Train\n",
"\n",
"`model.train()` builds the `TrainConfig` and hands the rest to PyTorch Lightning: forward pass, bipartite\n",
"matching loss, weight updates, learning-rate scheduling, and periodic COCO mAP validation.\n",
"\n",
"`CSVLogger` appends one row of metrics per epoch to `output_dir/metrics.csv`, plotted below. The progress bar\n",
"reports elapsed time per epoch — use the first completed epoch to size how long the remaining 39 will take on your\n",
"GPU before deciding whether to let this run uninterrupted or resume it across multiple sessions.\n",
"\n",
"To **resume an interrupted run**, add `resume=f\"{OUTPUT_DIR}/last.ckpt\"` to the call below and re-run this cell.\n",
"`last.ckpt` is rewritten every epoch (the archive `checkpoint_<epoch>.ckpt` files follow `checkpoint_interval`,\n",
"10 by default), so a disconnect costs at most the epoch in flight.\n",
"\n",
"### The five non-default arguments\n",
"\n",
"- **`batch_size=128`** — measured at ~80% of GPU memory on an 80+ GB GPU, leaving headroom for the multi-scale\n",
" ladder's largest resolution and for memory fragmentation over a long run. Two things to know when changing it:\n",
" the default `lr=1e-4` was tuned around an effective batch of 16, and this notebook leans on cosine-plus-warmup\n",
" rather than rescaling `lr` for the 8x larger batch — if you tune, the linear-scaling heuristic is the place to\n",
" start. And on a smaller GPU, set `batch_size=\"auto\"`: the probe finds the largest safe micro-batch and raises\n",
" `grad_accum_steps` toward an effective batch of 16 (`\"auto\"` works only through `model.train()`; building the\n",
" Lightning modules by hand requires a concrete integer).\n",
"- **`num_workers`** — the default is `2`, which is fine for the few-thousand-image datasets in the other\n",
" cookbooks and far too low for COCO2017: 118k JPEGs have to be decoded and resized every epoch, and two worker\n",
" processes cannot keep a modern GPU fed. But more is not simply better — on a 48-core host with this exact\n",
" configuration, 48 workers sat at ~15% CPU while eating ~70% of RAM: once the GPU is the bottleneck, extra\n",
" workers add nothing but memory (each worker process carries its own copy of the 118k-image annotation index,\n",
" plus `prefetch_factor` batches of decoded tensors queued per worker). Capping at 16 feeds the GPU with\n",
" headroom; if the GPU ever waits on data, raise the cap until epoch time stops improving.\n",
"- **`seed=0`** — seeds Python, NumPy, and torch through Lightning's `seed_everything(..., workers=True)`, which\n",
" also gives each dataloader worker a distinct, reproducible stream. Note that RF-DETR's own\n",
" `seed_all` helper is *not* used here: besides seeding, it sets `cudnn.deterministic=True` and\n",
" `torch.use_deterministic_algorithms(True)`, forcing slow deterministic kernels for exactly the scatter and\n",
" grid-sample backward passes deformable attention leans on. Reproducible seeding is worth having; deterministic\n",
" kernels are not, in a notebook about throughput.\n",
"- **`lr_scheduler=\"cosine\"`** and **`warmup_epochs=1`** — the default schedule is `\"step\"` with `lr_drop=100`,\n",
" i.e. a 10x drop after epoch 100. In a 40-epoch run that drop never happens and the learning rate stays flat\n",
" from first step to last, so the mAP curve plateaus noisily with no final refinement. Cosine annealing is sized\n",
" from the run's own total step count, so it decays correctly no matter how many epochs or how large a batch the\n",
" GPU ends up with, and the one-epoch linear warmup keeps the first steps stable.\n",
"\n",
"### Levers deliberately not pulled\n",
"\n",
"Two further speedups exist but change the recipe rather than the schedule, so this notebook leaves them alone.\n",
"With `multi_scale=True` (the default) and `do_random_resize_via_padding=False`, training runs at the largest\n",
"scale of the multi-scale ladder — 544 px for Nano, while validation and inference stay at 384 px. Setting\n",
"`multi_scale=False` trains at 384 px instead (roughly half the pixels) and additionally unblocks\n",
"`RFDETRNano(compile=True)`, which is skipped whenever multi-scale is on because the varying input shapes\n",
"retrigger compilation. Separately, `augmentation_backend=\"gpu\"` moves augmentation off the CPU workers — a\n",
"rescue for CPU-starved runtimes (a 2-core Colab), and exactly backwards on this notebook's target: with the CPU\n",
"mostly idle and the GPU saturated, it would add work to the bottleneck and take it away from idle cores."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0c2750b4",
"metadata": {},
"outputs": [],
"source": [
"model.train(\n",
" dataset_file=\"coco\",\n",
" dataset_dir=COCO_ROOT,\n",
" output_dir=OUTPUT_DIR,\n",
" epochs=EPOCHS,\n",
" batch_size=BATCH_SIZE,\n",
" num_workers=NUM_WORKERS,\n",
" seed=0,\n",
" lr_scheduler=\"cosine\",\n",
" warmup_epochs=1,\n",
" tensorboard=False,\n",
" progress_bar=\"tqdm\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "b1d9811f",
"metadata": {},
"source": [
"## 6 - Plot CSVLogger metrics\n",
"\n",
"RF-DETR's `CSVLogger` writes `val/mAP_50_95`, `val/mAP_50`, and `val/mAP_75` to `metrics.csv` (plus the\n",
"`train/loss` family and `ema_` variants), and the plotting helpers below discover those columns automatically.\n",
"`val/mAP_50_95` is the primary metric — standard COCO bounding-box mAP averaged across IoU thresholds\n",
"0.50-0.95. `val/mAP_50` rises fastest and is the clearest early signal of whether training is progressing at\n",
"all; `val/mAP_75` reflects localization precision, not just whether objects are found."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "094f17bc",
"metadata": {},
"outputs": [],
"source": [
"from IPython.display import display\n",
"from matplotlib import pyplot as plt\n",
"\n",
"METRICS_CSV = f\"{OUTPUT_DIR}/metrics.csv\"\n",
"\n",
"loss_figure = plot_loss_metrics(METRICS_CSV)\n",
"display(loss_figure)\n",
"plt.close(loss_figure)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1b41af7",
"metadata": {},
"outputs": [],
"source": [
"map_figure = plot_map_metrics(METRICS_CSV)\n",
"display(map_figure)\n",
"plt.close(map_figure)"
]
}
],
"metadata": {
"jupytext": {
"cell_metadata_filter": "title,-all",
"main_language": "python",
"notebook_metadata_filter": "-all"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+6 -1
View File
@@ -1026,7 +1026,12 @@ class TrainConfig(BaseConfig):
# accepts no "auto": it is never probed, and an explicit value stays usable even when batch_size="auto"
# has not been resolved.
eval_batch_size: int | None = None
auto_batch_target_effective: int = 16 # global effective batch size target, divided across devices and nodes
# Global effective batch size target, divided across devices and nodes. This is a floor, not a cap: the probe
# only raises grad_accum_steps to *reach* it (see recommend_grad_accum_steps), and never shrinks the micro-batch
# to hold it. Once the probed micro-batch already meets or exceeds this value, grad_accum_steps stays at 1 and
# the effective batch is simply whatever fit in memory — so it grows with VRAM while lr stays put. Pin
# batch_size to a concrete integer when training semantics must match across GPUs of different sizes.
auto_batch_target_effective: int = 16
# Auto-batch probe: worst-case assumptions when batch_size="auto".
auto_batch_max_targets_per_image: int = 100
auto_batch_ema_headroom: float = 0.7 # scale safe batch by this when use_ema=True (EMA uses extra memory)