From 0e465d40cdc61f1c7a5df6cd3bc34c173d055dc3 Mon Sep 17 00:00:00 2001 From: Xuban <59646791+EHxuban11@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:57:38 +0200 Subject: [PATCH] Run RF-DETR at the settings its authors used for RF100-VL Our RF-DETR recipe was assembled from the rf-detr library defaults. Roboflow did not use those defaults for their own RF100-VL numbers. Their maintainer states the protocol in rf-detr#266: "default arguments but with batch size 16 and grad accum 1", posted three hours after the 1.2.0 release, so "default arguments" means the 1.2.0 defaults. Checked against the 1.2.0 tag rather than develop, because develop has drifted (batch_size now accepts "auto", amp_dtype became a selector): - physical_batch 4 -> 16. Effective batch was already 16, but via grad accum 4. That is not cosmetic: multi_scale draws a fresh resolution per forward, so accum 4 averages four scales into one optimizer step where Roboflow averaged one. - precision fp32 -> bfloat16. rfdetr/engine.py::get_autocast_args hardcodes dtype=torch.bfloat16 and ModelConfig.amp defaults True. fp32 matched no upstream setting. - cache "disk" dropped. A post-resize cache pins one resolution per image, which quietly defeats the multi_scale this recipe asks for. - workers 0 -> 2, matching upstream num_workers. Every other family already runs 4. - backbone_lr_mult removed. It was dead: LibreYOLO hardcodes Roboflow's lr_encoder 1.5e-4, lr_vit_layer_decay 0.8 and lr_component_decay 0.7 in models/rfdetr/nn.py, and the fallback that would have read this key is unreachable because the DINOv2 backbone exposes get_named_param_lr_pairs. A recipe is published as provenance, so a key we do not apply is a false claim about how the numbers were produced. - dense_oom_fallback 2 -> 4, keeping the rescue path proportional to the new base batch. Recorded in the recipe what cannot be matched: Roboflow initialised from private Objects365 checkpoints that were never released (they estimate ~0.1 mAP), and their published table predates their own fix for an incorrectly initialised head, so those numbers are not reproducible by any current code. Size l still steps down to batch 2 because 704 px at batch 16 does not fit 16 GB. Verified by a 2-epoch 8-GPU probe of rfdetr-s on this exact recipe: no OOM, peak 15524 MiB of 16 GB. Claude-Session: https://claude.ai/code/session_01Gy5rL27krK8eMZGCP7xozD --- tests/test_rf100vl_train.py | 31 +++++++++++++++++++++++++++- va_bench/recipes/rf100vl/rfdetr.json | 26 ++++++++++++++++------- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/tests/test_rf100vl_train.py b/tests/test_rf100vl_train.py index 3d87aef..267f31b 100644 --- a/tests/test_rf100vl_train.py +++ b/tests/test_rf100vl_train.py @@ -85,6 +85,11 @@ def test_generate_data_yaml_uses_all_splits_and_sorted_category_ids(tmp_path): # (ecdetseg/configs/ecdet/ecdet.yml), and LibreYOLO's ECConfig defaults to # amp with the inherited float16 dtype. fp32 would match neither. "ec": "fp16", + # rf-detr trains under autocast with the dtype hardcoded to torch.bfloat16 + # (rfdetr/engine.py::get_autocast_args at tag 1.2.0, the release current + # when Roboflow published their RF100-VL numbers) and ModelConfig.amp + # defaults to True. fp32 would be a deviation, not the conservative choice. + "rfdetr": "bfloat16", } @@ -146,11 +151,35 @@ def test_dense_rfdetr_selects_fallback_and_micro_dataset_keeps_one_batch(): } plan = rf100vl_train.select_batch_plan(recipe, spec, facts) assert plan["run_variant"] == "fallback" - assert plan["physical_batch"] == 2 + assert plan["physical_batch"] == 4 assert plan["effective_batch"] == 16 assert plan["expected_batches_per_epoch_minimum"] == 1 +def test_rfdetr_recipe_matches_roboflows_own_rf100vl_settings(): + """RF-DETR's authors state their RF100-VL numbers came from the rf-detr + defaults with one override: batch 16 and grad accum 1, not the library's + default batch 4 / accum 4. Autocast in rf-detr 1.2.0 is hardcoded to + bfloat16, so fp32 is a deviation too. Both are easy to reintroduce by + copying another family's recipe, and neither is visible in a result table, + so pin them here. + """ + recipe = rf100vl_train.load_recipe( + rf100vl_train.recipe_path_for_family("rfdetr"), + family="rfdetr", + ) + protocol = recipe["protocol"] + assert protocol["physical_batch"] == 16, "Roboflow ran batch 16, grad accum 1" + assert protocol["precision"] == "bfloat16", "rf-detr autocasts to bfloat16" + # A post-resize image cache pins one resolution per image, which silently + # defeats the multi_scale sampling this recipe asks for. + assert not protocol.get("cache", False) + assert recipe["train"]["multi_scale"] is True + # Sizes n/s/m inherit the batch; only l steps down, and only for VRAM. + for size in ("n", "s", "m"): + assert "physical_batch" not in recipe["sizes"][size] + + def test_rfdetr_l_oom_fallback_reduces_batch(): spec = get_spec("rfdetr-l") recipe = rf100vl_train.load_recipe( diff --git a/va_bench/recipes/rf100vl/rfdetr.json b/va_bench/recipes/rf100vl/rfdetr.json index 8054568..1a707b2 100644 --- a/va_bench/recipes/rf100vl/rfdetr.json +++ b/va_bench/recipes/rf100vl/rfdetr.json @@ -6,23 +6,36 @@ "commit": "3e3c9dffc9e4bd41a363f1dd6ca1c736e145a5d8", "paths": ["libreyolo/models/rfdetr/config.py"], "license": "MIT", - "normalization": "LibreYOLO RF-DETR fine-tune defaults under the RF100-VL fixed skeleton" + "normalization": "Roboflow's own RF100-VL settings for RF-DETR under the RF100-VL fixed skeleton", + "upstream_reference": { + "repository": "https://github.com/roboflow/rf-detr", + "ref": "1.2.0", + "paths": ["rfdetr/config.py", "rfdetr/engine.py"], + "license": "Apache-2.0", + "protocol_statement": "https://github.com/roboflow/rf-detr/issues/266#issuecomment-3110260396", + "quote": "The RF-DETR numbers for the current release are using the default arguments but with batch size 16 and grad accum 1", + "known_deviations": [ + "Roboflow initialised from private Objects365 checkpoints that were never released; we start from the public COCO rf-detr-*.pth. Maintainer estimates the gap at ~0.1 mAP.", + "Roboflow's published table predates their own fix for an incorrectly initialised detection head, so their numbers are not reproducible by any current code.", + "Size l runs physical_batch 2 because 704 px at batch 16 does not fit 16 GB; every other size runs Roboflow's batch 16 / grad accum 1.", + "Resize uses cv2 INTER_LINEAR without antialias." + ] + } }, "protocol": { "epochs": 100, "effective_batch": 16, - "physical_batch": 4, + "physical_batch": 16, "selection_metric": "valid_mAP50_95", "eval_interval": 1, "patience": 0, "ema": true, "seed": 0, - "precision": "fp32", - "cache": "disk" + "precision": "bfloat16" }, "dense_oom_fallback": { "max_annotations_per_image_threshold": 200, - "physical_batch": 2 + "physical_batch": 4 }, "sizes": { "n": {"imgsz": 384}, @@ -55,9 +68,8 @@ "multi_scale": true, "expanded_scales": true, "crop_resize_prob": 0.5, - "backbone_lr_mult": 0.1, "clip_max_norm": 0.1, "ema_decay": 0.993, - "workers": 0 + "workers": 2 } }