-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathconfig.py
More file actions
1597 lines (1379 loc) · 69.7 KB
/
Copy pathconfig.py
File metadata and controls
1597 lines (1379 loc) · 69.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
import functools
import importlib
import json
import os
import warnings
from collections.abc import Callable, Mapping
from enum import Enum
from pathlib import Path
from typing import Any, ClassVar, Dict, Literal, Optional, TypeAlias
import torch
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator
from pydantic_core import PydanticUndefined
from torch.optim import Optimizer
from torch.optim.lr_scheduler import LRScheduler, ReduceLROnPlateau
EncoderName: TypeAlias = Literal["dinov2_windowed_small", "dinov2_windowed_base", "dinov2_registers_windowed_small"]
PathLikeStr: TypeAlias = str | Path
__all__ = [
"AugmentationBackend",
"ModelConfig",
"RFDETRBaseConfig",
"RFDETRLargeDeprecatedConfig",
"RFDETRNanoConfig",
"RFDETRSmallConfig",
"RFDETRMediumConfig",
"RFDETRLargeConfig",
"RFDETRSegPreviewConfig",
"RFDETRSegNanoConfig",
"RFDETRSegSmallConfig",
"RFDETRSegMediumConfig",
"RFDETRSegLargeConfig",
"RFDETRSegXLargeConfig",
"RFDETRSeg2XLargeConfig",
"RFDETRKeypointPreviewConfig",
"TrainConfig",
"SegmentationTrainConfig",
"KeypointTrainConfig",
]
#: Legacy augmentation-backend string aliases, mapped to their current form.
_LEGACY_AUGMENTATION_BACKEND_ALIASES: Dict[str, str] = {
"gpu": "kornia",
"tv": "torchvision",
"albu": "albumentations",
}
def _package_importable(module_name: str) -> bool:
"""Return ``True`` when *module_name* can be imported.
Args:
module_name: Dotted module path to probe (e.g. ``"kornia.augmentation"``).
Returns:
``True`` if the import succeeds, ``False`` on ``ImportError``.
"""
try:
importlib.import_module(module_name)
return True
except ImportError:
return False
class AugmentationBackend(str, Enum):
"""Concrete augmentation backend selector for ``TrainConfig.augmentation_backend``.
Only holds directly-usable, concrete backends — ``TV`` (torchvision), ``ALBU`` (Albumentations), and ``KORNIA``.
``GPU`` is a Python enum alias for ``KORNIA`` (same value ``"kornia"``): Kornia augmentation always runs on-device
(GPU), so the two names refer to the same backend; ``GPU`` exists only so legacy ``augmentation_backend="gpu"``
strings keep resolving correctly.
``"cpu"`` and ``"auto"`` are accepted as *input* strings (on ``TrainConfig.augmentation_backend`` and by
:meth:`from_str`) but are never stored or returned as a member of this enum — they are auto-pick sentinels resolved
to a concrete member at :meth:`from_str` call time. Resolution stays late (re-checked at dataset-build time against
whatever is installed in the current environment) rather than baked into ``TrainConfig`` at construction time, so a
saved config using ``"cpu"``/``"auto"`` remains portable across environments with different optional packages
installed. Pass a concrete value (``"torchvision"``, ``"albumentations"``, or ``"kornia"``) explicitly to pin the
backend regardless of environment.
"""
TV = "torchvision"
ALBU = "albumentations"
KORNIA = "kornia"
GPU = "kornia" # alias for KORNIA — backward compat name; kornia is always the GPU-side path
@classmethod
def from_str(cls, value: str, *, has_cuda: bool = False) -> "AugmentationBackend":
"""Resolve a string to a concrete backend, auto-picking the best installed one.
Legacy string aliases (``"gpu"``, ``"tv"``, ``"albu"``) are mapped to their current form
first. ``"cpu"`` auto-picks the best *installed* CPU backend: Albumentations > Kornia
(CPU) > torchvision. ``"auto"`` additionally prefers Kornia first when ``has_cuda=True``
and Kornia is installed, then falls back to the same CPU priority. The concrete backend
``"cpu"``/``"auto"`` resolve to can therefore vary across environments — pass
``"torchvision"`` explicitly to force torchvision regardless of what's installed.
Args:
value: Backend name string.
has_cuda: Whether a CUDA device is available. Only consulted for ``"auto"`` — callers
that care about CUDA-gated GPU selection (e.g. dataset builders) compute this via
their own fork-safe CUDA check and pass it in; this function does not probe CUDA
itself to avoid importing device-detection code from other modules.
Returns:
Concrete ``AugmentationBackend`` member.
Raises:
ValueError: When *value* is not a recognised backend name.
Examples:
>>> AugmentationBackend.from_str("torchvision")
<AugmentationBackend.TV: 'torchvision'>
>>> AugmentationBackend.from_str("gpu")
<AugmentationBackend.KORNIA: 'kornia'>
"""
value = _LEGACY_AUGMENTATION_BACKEND_ALIASES.get(value, value)
if value in ("cpu", "auto"):
if value == "auto" and has_cuda and cls._is_kornia_available():
return cls.KORNIA
if cls._is_albu_available():
return cls.ALBU
if cls._is_kornia_available():
return cls.KORNIA
return cls.TV
try:
return cls(value)
except ValueError:
raise ValueError(
f"Unknown augmentation_backend {value!r}; expected one of 'cpu', 'auto', 'torchvision', "
"'albumentations', 'kornia'."
) from None
@classmethod
@functools.lru_cache(maxsize=None)
def _is_albu_available(cls) -> bool:
"""Return ``True`` when Albumentations is importable.
Cached for the process lifetime — package installation state does not change at runtime.
Tests that need to simulate "not installed" should patch this method directly (e.g.
``patch.object(AugmentationBackend, "_is_albu_available", return_value=False)``) rather
than blocking the underlying import, since the cache is keyed on this method, not on the
import machinery.
Returns:
``True`` if ``albumentations`` can be imported.
"""
return _package_importable("albumentations")
@classmethod
@functools.lru_cache(maxsize=None)
def _is_kornia_available(cls) -> bool:
"""Return ``True`` when Kornia's augmentation module is importable.
Cached for the process lifetime — see :meth:`_is_albu_available` for the caching and test
rationale.
Returns:
``True`` if ``kornia.augmentation`` can be imported.
"""
return _package_importable("kornia.augmentation")
@classmethod
def _is_tv_available(cls) -> bool:
"""Return ``True`` — torchvision is a hard (non-optional) RF-DETR dependency.
Not cached: the result is a compile-time constant, not worth the caching machinery.
Returns:
Always ``True``.
"""
return True
class PretrainWeightsCompatibilityWarning(UserWarning):
"""Warning emitted when ``ModelConfig`` overrides are likely to prevent the variant's published pretrained weights
from loading into the model — leaving large portions of the model randomly initialized and typically producing much
lower accuracy."""
def _detect_device() -> str:
"""Detect the best available device **without** initialising the CUDA runtime.
``torch.cuda.is_available()`` creates a CUDA driver context that makes ``_is_in_bad_fork()`` return ``True`` in
child processes. This breaks fork-based DDP strategies (e.g. ``ddp_notebook``) in notebook environments.
We defer to :func:`torch.accelerator.current_accelerator` (PyTorch ≥ 2.4) when available — it queries the driver
through NVML without creating a primary context. On older builds we fall back to ``torch.cuda.is_available()``.
``check_available=True`` is required: without it ``current_accelerator()`` only reports the *compile-time*
accelerator, so the default CUDA wheel on a machine without an NVIDIA driver yields ``"cuda"`` and every model build
crashes with "Found no NVIDIA driver". The runtime check is NVML-backed and still avoids creating a CUDA context.
Builds whose ``current_accelerator`` predates the ``check_available`` kwarg get the same runtime verification via
``torch.accelerator.is_available``.
"""
accelerator = getattr(torch, "accelerator", None)
current_accelerator = getattr(accelerator, "current_accelerator", None)
if current_accelerator is not None:
try:
try:
accel = current_accelerator(check_available=True)
except TypeError:
accel = current_accelerator()
if accel is not None and not accelerator.is_available():
accel = None
if accel is not None:
return str(accel)
return "cpu"
except RuntimeError:
return "cpu"
# Fallback for PyTorch < 2.4 — this DOES create a CUDA driver context.
if torch.cuda.is_available():
return "cuda"
if torch.backends.mps.is_available():
return "mps"
return "cpu"
DEVICE: str = _detect_device()
_OPTIMIZER_MANAGED_KWARGS = {"params", "lr", "weight_decay", "fused"}
def _resolve_native_optimizer(name: str) -> type[Optimizer]:
"""Resolve a bare optimizer short name to a ``torch.optim`` optimizer class.
Only native ``torch.optim`` optimizers may be selected by short name; the match
is case-insensitive (``"adamw"`` → ``torch.optim.AdamW``, ``"sgd"`` → ``torch.optim.SGD``).
Any other optimizer must be given as a full dotted import path or a callable.
Args:
name: A bare optimizer name (no dotted import path).
Returns:
The matching ``torch.optim`` optimizer class.
Raises:
ValueError: If ``name`` is not a native ``torch.optim`` optimizer.
Examples:
>>> _resolve_native_optimizer("adamw") is torch.optim.AdamW
True
"""
target = name.strip().lower()
for attribute in dir(torch.optim):
candidate = getattr(torch.optim, attribute)
if isinstance(candidate, type) and issubclass(candidate, Optimizer) and attribute.lower() == target:
return candidate
raise ValueError(
f"Unknown native optimizer {name!r}. Short names must name a torch.optim optimizer "
"(e.g. 'adamw', 'sgd', 'adam'); use a full dotted import path or a callable for anything else."
)
def _is_managed_optimizer_name(optimizer: object) -> bool:
"""Return whether an optimizer config selects RF-DETR's managed construction.
Managed mode covers bare ``torch.optim`` short names (e.g. ``"adamw"``, ``"sgd"``);
RF-DETR injects ``lr`` and a signature-aware ``weight_decay`` there. A dotted import
path or a callable selects explicit mode, where the optimizer is built only from
``optimizer_kwargs`` (or the callable's own bound arguments).
Args:
optimizer: The ``TrainConfig.optimizer`` value.
Returns:
``True`` for managed short-name strings, ``False`` for dotted paths and callables.
Examples:
>>> _is_managed_optimizer_name("sgd")
True
>>> _is_managed_optimizer_name("torch.optim.AdamW")
False
"""
return isinstance(optimizer, str) and "." not in optimizer
def _desugar_optimizer_callable(
optimizer: Callable[..., Optimizer],
) -> tuple[str | None, dict[str, Any] | None, str | None]:
"""Decompose a callable optimizer into a serializable ``(dotted_path, kwargs)`` form.
Reconstructable callables — an importable top-level class or function, optionally
wrapped in ``functools.partial`` with JSON-serializable keyword arguments and no
positional arguments — desugar to a dotted import path plus keyword arguments that
round-trip through ``training_config.json``.
Args:
optimizer: A callable or ``functools.partial`` given as ``TrainConfig.optimizer``.
Returns:
``(dotted_path, kwargs, None)`` when reconstructable, otherwise
``(None, None, reason)`` where ``reason`` explains how to make it compatible.
"""
func: Any = optimizer
extracted_kwargs: dict[str, Any] = {}
if isinstance(optimizer, functools.partial):
if optimizer.args:
return None, None, "pass every functools.partial argument as a keyword, not positionally"
func = optimizer.func
extracted_kwargs = dict(optimizer.keywords or {})
module = getattr(func, "__module__", None)
qualname = getattr(func, "__qualname__", None)
if module is None or qualname is None or "<" in qualname:
return (
None,
None,
"define the optimizer as an importable top-level class or function (no lambda or nested definition)",
)
try:
json.dumps(extracted_kwargs)
except (TypeError, ValueError):
return (
None,
None,
"use only JSON-serializable functools.partial keyword arguments (no tensors, modules, or callables)",
)
return f"{module}.{qualname}", extracted_kwargs, None
_MANAGED_SCHEDULER_PRESETS = {"step", "cosine"}
_DEPRECATED_LR_FIELD_KWARGS = {"lr_drop": "lr_drop", "lr_min_factor": "min_factor"}
# Keys the managed "step" / "cosine" presets actually consume from lr_scheduler_kwargs.
_MANAGED_SCHEDULER_KWARGS = {"min_factor", "lr_drop"}
# ReduceLROnPlateau does not subclass LRScheduler but is a supported explicit scheduler.
SchedulerType: TypeAlias = LRScheduler | ReduceLROnPlateau
def _is_managed_scheduler_name(lr_scheduler: object) -> bool:
"""Return whether an lr_scheduler config selects an RF-DETR managed preset.
Managed presets are the built-in ``"step"`` and ``"cosine"`` schedules, which own warmup
and total-step sizing. A dotted import path or a callable instead selects an explicit
scheduler built from ``lr_scheduler_kwargs`` (or the callable's own bound arguments).
Args:
lr_scheduler: The ``TrainConfig.lr_scheduler`` value.
Returns:
``True`` for managed preset short names, ``False`` for dotted paths and callables.
Examples:
>>> _is_managed_scheduler_name("cosine")
True
>>> _is_managed_scheduler_name("torch.optim.lr_scheduler.StepLR")
False
"""
return isinstance(lr_scheduler, str) and lr_scheduler.strip().lower() in _MANAGED_SCHEDULER_PRESETS
def _desugar_scheduler_callable(
lr_scheduler: Callable[..., SchedulerType],
) -> tuple[str | None, dict[str, Any] | None, str | None]:
"""Decompose a callable lr_scheduler into a serializable ``(dotted_path, kwargs)`` form.
Reconstructable callables — an importable top-level class or function, optionally wrapped in
``functools.partial`` with JSON-serializable keyword arguments and no positional arguments —
desugar to a dotted import path plus keyword arguments that round-trip through
``training_config.json``. The optimizer is supplied at build time, never baked into the callable.
Args:
lr_scheduler: A callable or ``functools.partial`` given as ``TrainConfig.lr_scheduler``.
Returns:
``(dotted_path, kwargs, None)`` when reconstructable, otherwise
``(None, None, reason)`` where ``reason`` explains how to make it compatible.
"""
func: Any = lr_scheduler
extracted_kwargs: dict[str, Any] = {}
if isinstance(lr_scheduler, functools.partial):
if lr_scheduler.args:
return None, None, "pass every functools.partial argument as a keyword, not positionally"
func = lr_scheduler.func
extracted_kwargs = dict(lr_scheduler.keywords or {})
module = getattr(func, "__module__", None)
qualname = getattr(func, "__qualname__", None)
if module is None or qualname is None or "<" in qualname:
return (
None,
None,
"define the lr_scheduler as an importable top-level class or function (no lambda or nested definition)",
)
try:
json.dumps(extracted_kwargs)
except (TypeError, ValueError):
return (
None,
None,
"use only JSON-serializable functools.partial keyword arguments (no tensors, modules, or callables)",
)
return f"{module}.{qualname}", extracted_kwargs, None
class BaseConfig(BaseModel):
"""Base configuration class that validates input parameters against the defined model schema.
If any unknown fields are provided, a ValueError is raised listing the unknown and available parameters.
"""
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", validate_assignment=True)
@model_validator(mode="before")
@classmethod
def catch_typo_kwargs(cls, values: Any) -> Any:
if not isinstance(values, Mapping):
return values
if cls.model_config.get("extra") != "forbid":
return values
allowed_params = set(cls.model_fields.keys())
provided_params = set(values)
unknown_params = provided_params - allowed_params
if unknown_params:
unknown_params_list = ", ".join(f"'{param}'" for param in sorted(unknown_params))
allowed_params_list = ", ".join(sorted(allowed_params))
raise ValueError(
f"Unknown parameter(s): {unknown_params_list}. Available parameter(s): {allowed_params_list}."
)
return values
def __setattr__(self, name: str, value: Any) -> None:
if name.startswith("_") or name in type(self).model_fields:
super().__setattr__(name, value)
return
raise ValueError(f"Unknown attribute: '{name}'.")
class ModelConfig(BaseConfig):
"""Core architecture configuration for RF-DETR models.
Concrete subclasses (e.g. ``RFDETRBaseConfig``, ``RFDETRLargeConfig``) must supply every field
that has no default; direct instantiation of ``ModelConfig`` is unsupported.
Attributes:
encoder: Vision-transformer backbone identifier. Must be provided by concrete subclass.
out_feature_indexes: Encoder layer indices whose feature maps are forwarded to the decoder.
Must be provided by concrete subclass.
dec_layers: Number of transformer decoder layers. Must be provided by concrete subclass.
projector_scale: Feature-pyramid levels fed to the decoder cross-attention (subset of
``["P3", "P4", "P5"]``). Must be provided by concrete subclass.
hidden_dim: Width of the decoder hidden state. Must be provided by concrete subclass.
patch_size: ViT patch size used by the backbone. Must be provided by concrete subclass.
num_windows: Number of windowed-attention windows in the backbone. Must be provided by
concrete subclass.
sa_nheads: Number of heads in decoder self-attention. Must be provided by concrete
subclass.
ca_nheads: Number of heads in decoder cross-attention. Must be provided by concrete
subclass.
dec_n_points: Deformable attention points per head per level in the decoder. Must be
provided by concrete subclass.
resolution: Square input resolution (pixels). Must be provided by concrete subclass.
positional_encoding_size: Side length (in patches) of the sinusoidal positional grid.
Must be provided by concrete subclass.
num_queries: Number of object queries used during inference (and per group during
training). Defaults to ``300``.
num_classes: Number of output classes (background-free). Defaults to ``90`` (COCO).
group_detr: Number of duplicate query groups used during training for GroupPose-style
convergence acceleration. ``num_queries * group_detr`` predictions are produced in
training mode; ``num_queries`` in eval mode. ``num_queries`` must be divisible by
``group_detr``. Defaults to ``13``.
amp: Enable automatic mixed precision (bfloat16/float16). Defaults to ``True``.
compile: Compile the model with ``torch.compile`` for faster throughput. Defaults to
``False``.
pretrain_weights: Path or URL to pretrained checkpoint. ``None`` trains from scratch.
device: Target device string (e.g. ``"cuda"``, ``"cpu"``). Auto-detected if not set.
gradient_checkpointing: Trade compute for memory by checkpointing activations. Defaults
to ``False``.
"""
encoder: EncoderName
out_feature_indexes: list[int]
dec_layers: int
two_stage: bool = True
projector_scale: list[Literal["P3", "P4", "P5"]]
hidden_dim: int
patch_size: int
num_windows: int
sa_nheads: int
ca_nheads: int
dec_n_points: int
num_queries: int = 300
# NOTE:
# - ModelConfig is the authoritative source of `num_select` for PTL/inference; it is read via `build_namespace`.
# - Any `num_select` field on TrainConfig / SegmentationTrainConfig is deprecated and ignored by PTL/inference.
num_select: int = 300
postprocess_trace_alpha: float = Field(default=0.2, ge=0.0)
bbox_reparam: bool = True
lite_refpoint_refine: bool = True
layer_norm: bool = True
amp: bool = True
num_channels: int = Field(default=3, ge=1)
num_classes: int = 90
pretrain_weights: PathLikeStr | None = None
# torch.device values are accepted at validation time and normalized to string.
device: str = DEVICE
resolution: int
group_detr: int = 13
gradient_checkpointing: bool = False
compile: bool = False
fused_optimizer: bool = True
positional_encoding_size: int
ia_bce_loss: bool = True
cls_loss_coef: float = 1.0
segmentation_head: bool = False
oriented: bool = False
use_grouppose_keypoints: bool = False
keypoint_cross_attn: bool = True
inter_instance_kp_attn: bool = False
grouppose_keypoint_dim_downscale: int = 1
dual_projector: bool = False
dual_projector_kp_only: bool = False
num_keypoints_per_class: list[int] = Field(default_factory=list)
num_decoder_registers: int = 0
mask_downsample_ratio: int = 4
backbone_lora: bool = False
freeze_encoder: bool = False
license: str = "Apache-2.0"
model_name: str | None = Field(
default=None,
description=(
'Name of the model class stored in training checkpoints (e.g. ``"RFDETRLarge"``). '
"Set automatically by ``RFDETR.train()`` before saving. "
"Used by ``RFDETR.from_checkpoint()`` to resolve the correct subclass directly "
"without inspecting ``pretrain_weights``."
),
)
@model_validator(mode="after")
def _warn_deprecated_model_config_fields(self) -> "ModelConfig":
"""Emit DeprecationWarning when cls_loss_coef is explicitly set on ModelConfig.
``cls_loss_coef`` ownership is moving to ``TrainConfig`` (Item #3, v1.7). Setting it on ``ModelConfig`` is
deprecated. Use ``TrainConfig(cls_loss_coef=...)`` instead.
"""
if "cls_loss_coef" in self.model_fields_set:
# stacklevel=2 points into Pydantic internals rather than the user call
# site — this is unavoidable with @model_validator(mode="after") in
# Pydantic v2. The warning still fires correctly; the origin frame is
# less precise than ideal.
warnings.warn(
"ModelConfig.cls_loss_coef is deprecated since v1.7.0 and will be removed in v1.9.0. "
"Set cls_loss_coef on TrainConfig instead.",
DeprecationWarning,
stacklevel=2,
)
return self
@model_validator(mode="after")
def _sync_pe_with_resolution(self) -> "ModelConfig":
"""Auto-update positional_encoding_size when resolution is explicitly provided.
When a user provides a custom ``resolution`` at construction time (e.g., ``RFDETRLarge(resolution=640)``),
``positional_encoding_size`` is updated proportionally, provided the class-default PE is formula-derived
(``default_pe == default_resolution // patch_size``).
Configs with a pretrained-specific PE (e.g., ``RFDETRBaseConfig`` with ``positional_encoding_size=37`` for
DINOv2's native 518 px grid, while ``resolution=560``) are left unchanged.
"""
if "resolution" not in self.model_fields_set or "positional_encoding_size" in self.model_fields_set:
return self
cls = type(self)
default_resolution = cls.model_fields["resolution"].default
default_pe = cls.model_fields["positional_encoding_size"].default
default_patch_size = cls.model_fields["patch_size"].default
# Skip when any relevant default is not a concrete integer (abstract base
# class fields have no defaults; required fields use PydanticUndefined,
# not int).
if (
not isinstance(default_resolution, int)
or not isinstance(default_pe, int)
or not isinstance(default_patch_size, int)
):
return self
# Only update PE when the class default is formula-derived from the class
# default resolution and patch size.
if default_pe == default_resolution // default_patch_size:
self.positional_encoding_size = self.resolution // self.patch_size
return self
@model_validator(mode="after")
def _warn_pretrain_compatibility(self) -> "ModelConfig":
"""Warn when overrides are likely to prevent published pretrained weights from loading.
Three cases:
1. ``pretrain_weights`` was explicitly set to ``None`` and the variant
has a non-``None`` default → warn that the model is being initialised from scratch.
2. ``pretrain_weights`` was explicitly set to a non-``None`` custom path
→ suppress the architecture-override check (we cannot know the architecture stored in a user-supplied
checkpoint at config time). The load-time partial-load detector in
:func:`rfdetr.models.weights.load_pretrain_weights` covers this case by inspecting the checkpoint contents
directly.
3. ``pretrain_weights`` is the variant's published default → check
architecture-affecting fields against the variant defaults and emit a single consolidated warning listing
every load-breaking override.
The warning class is :class:`PretrainWeightsCompatibilityWarning` (a :class:`UserWarning` subclass), silenceable
via the standard ``warnings.filterwarnings`` machinery.
"""
cls = type(self)
fields_set = self.model_fields_set
pretrain_user_set = "pretrain_weights" in fields_set
if pretrain_user_set and self.pretrain_weights is None:
default_pretrain = cls.model_fields["pretrain_weights"].default
if default_pretrain is not PydanticUndefined and default_pretrain is not None:
warnings.warn(
f"{cls.__name__} was instantiated with pretrain_weights=None. "
f"The model will be initialised from scratch, which typically "
f"produces lower accuracy than fine-tuning from the published "
f"checkpoint ({default_pretrain!r}).",
PretrainWeightsCompatibilityWarning,
stacklevel=2,
)
return self
if pretrain_user_set and self.pretrain_weights is not None:
# Custom checkpoint: architecture overrides may match what the
# checkpoint was trained with. Defer to the load-time partial-load
# detector which can read the file.
# Exception: when the user explicitly passes the variant's own
# published-default path string (e.g. ``"rf-detr-nano.pth"``), it
# IS the published checkpoint — treat it as case 3 so architecture-
# override checks still apply. Compare after expand_path so bare
# filenames resolve to the same cache-dir path as self.pretrain_weights.
_default_pretrain = cls.model_fields["pretrain_weights"].default
if _default_pretrain is not None and _default_pretrain is not PydanticUndefined:
_expanded_default = cls.expand_path(_default_pretrain)
if self.pretrain_weights != _expanded_default:
return self
# Falls through to case-3 when the user passed the exact variant default.
else:
return self
# `pretrain_weights` is the variant's published default — check
# architecture overrides against the class defaults.
# Skip entirely when this variant has no published checkpoint (default
# is None/PydanticUndefined); warning would reference "(None)" which is
# misleading and confusing for users of the abstract base config.
_class_default_pretrain = cls.model_fields["pretrain_weights"].default
if _class_default_pretrain is None or _class_default_pretrain is PydanticUndefined:
return self
overrides: list[tuple[str, Any, Any]] = []
# Fields that, when explicitly overridden to any value other than the
# variant default, prevent the published checkpoint from loading cleanly.
# Includes major architecture knobs, "less obvious" knobs (bbox_reparam,
# lite_refpoint_refine, layer_norm, two_stage), defense-in-depth for
# fields that currently raise hard errors (patch_size, segmentation_head),
# and num_channels (loads via heuristic but result isn't real pretrained
# weights for the new input domain).
breaking_fields: tuple[str, ...] = (
"encoder",
"hidden_dim",
"dec_layers",
"num_windows",
"sa_nheads",
"ca_nheads",
"dec_n_points",
"out_feature_indexes",
"projector_scale",
"bbox_reparam",
"lite_refpoint_refine",
"layer_norm",
"two_stage",
"patch_size",
"segmentation_head",
"num_channels",
)
# Fields where only an *increase* above the variant default is load-breaking:
# num_queries / group_detr add slots whose shape differs — decrease is fine.
breaking_on_increase: tuple[str, ...] = (
"num_queries",
"group_detr",
)
for name in breaking_fields:
if name not in fields_set:
continue
field_info = cls.model_fields.get(name)
if field_info is None or field_info.is_required():
continue
default = field_info.default
if default is PydanticUndefined:
continue
current = getattr(self, name)
if current != default:
overrides.append((name, current, default))
for name in breaking_on_increase:
if name not in fields_set:
continue
field_info = cls.model_fields.get(name)
if field_info is None or field_info.is_required():
continue
default = field_info.default
if default is PydanticUndefined or not isinstance(default, int):
continue
current = getattr(self, name)
if isinstance(current, int) and current > default:
overrides.append((name, current, default))
# ``mask_downsample_ratio`` only affects segmentation models — skip on
# detector-only variants to avoid a misleading "weights won't load" warning.
if "mask_downsample_ratio" in fields_set and self.segmentation_head:
_mdr_info = cls.model_fields.get("mask_downsample_ratio")
if _mdr_info is not None and not _mdr_info.is_required():
_mdr_default = _mdr_info.default
if _mdr_default is not PydanticUndefined:
_mdr_current = self.mask_downsample_ratio
if _mdr_current != _mdr_default:
overrides.append(("mask_downsample_ratio", _mdr_current, _mdr_default))
if overrides:
default_pretrain = cls.model_fields["pretrain_weights"].default
lines = "\n".join(
f" {name}: {current!r} (variant default: {default!r})" for name, current, default in overrides
)
warnings.warn(
f"{cls.__name__} was instantiated with overrides that differ from the variant "
f"defaults in ways that prevent the published pretrained weights "
f"({default_pretrain!r}) from loading correctly:\n"
f"{lines}\n"
"Loading the checkpoint with this configuration will leave significant portions "
"of the model randomly initialised, which typically produces lower accuracy. "
"To suppress this warning: revert the override(s), pick a variant whose defaults "
"match, or pass pretrain_weights=None to acknowledge that you intend to train "
"from scratch.",
PretrainWeightsCompatibilityWarning,
stacklevel=2,
)
return self
@field_validator("pretrain_weights", mode="before")
@classmethod
def expand_path(cls, v: PathLikeStr | None) -> str | None:
"""Expand and resolve the pretrain_weights path.
Bare filenames (no directory component, e.g. ``rf-detr-base.pth``) are resolved to the model cache directory so
weights land in a stable, user-configurable location (``~/.roboflow/models`` by default, or the path set via the
``RF_HOME`` environment variable) instead of CWD.
Paths that already contain a directory separator (e.g. ``~/models/x.pth``, ``/abs/path/x.pth``,
``models/x.pth``) are normalised with ``os.path.realpath`` as before.
"""
if v is None:
return v
expanded = os.path.expanduser(os.fspath(v))
if not os.path.dirname(expanded):
# Bare filename → use model cache dir so weights don't land in CWD.
from rfdetr.assets.model_weights import get_model_cache_dir
return os.path.join(get_model_cache_dir(), expanded)
return os.path.realpath(expanded)
@field_validator("device", mode="before")
@classmethod
def _normalize_device(cls, v: Any) -> str:
"""Normalize supported device inputs to a canonical torch-style string.
Args:
v: Device specifier provided by callers. Supported values are
``str`` (for example ``"cpu"``, ``"cuda"``, ``"cuda:1"``) and ``torch.device``.
Returns:
Canonical string form of the parsed device (for example ``"cuda:1"``).
Raises:
ValueError: If a string value cannot be parsed as a valid torch device.
ValueError: If ``v`` is not a string or ``torch.device``.
"""
if isinstance(v, torch.device):
return str(v)
if isinstance(v, str):
try:
return str(torch.device(v))
except (TypeError, ValueError, RuntimeError) as exc:
raise ValueError(f"Invalid device specifier: {v!r}.") from exc
raise ValueError("device must be a string or torch.device.")
class RFDETRBaseConfig(ModelConfig):
"""The configuration for an RF-DETR Base model."""
encoder: EncoderName = "dinov2_windowed_small"
hidden_dim: int = 256
patch_size: int = 14
num_windows: int = 4
dec_layers: int = 3
sa_nheads: int = 8
ca_nheads: int = 16
dec_n_points: int = 2
num_queries: int = 300
num_select: int = 300
projector_scale: list[Literal["P3", "P4", "P5"]] = ["P4"]
out_feature_indexes: list[int] = [2, 5, 8, 11]
pretrain_weights: PathLikeStr | None = "rf-detr-base.pth"
resolution: int = 560
positional_encoding_size: int = 37
class RFDETRLargeDeprecatedConfig(RFDETRBaseConfig):
"""The configuration for an RF-DETR Large model."""
encoder: EncoderName = "dinov2_windowed_base"
hidden_dim: int = 384
sa_nheads: int = 12
ca_nheads: int = 24
dec_n_points: int = 4
projector_scale: list[Literal["P3", "P4", "P5"]] = ["P3", "P5"]
pretrain_weights: PathLikeStr | None = "rf-detr-large.pth"
class RFDETRNanoConfig(RFDETRBaseConfig):
"""The configuration for an RF-DETR Nano model."""
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 2
patch_size: int = 16
resolution: int = 384
positional_encoding_size: int = 24
pretrain_weights: PathLikeStr | None = "rf-detr-nano.pth"
class RFDETRSmallConfig(RFDETRBaseConfig):
"""The configuration for an RF-DETR Small model."""
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 3
patch_size: int = 16
resolution: int = 512
positional_encoding_size: int = 32
pretrain_weights: PathLikeStr | None = "rf-detr-small.pth"
class RFDETRMediumConfig(RFDETRBaseConfig):
"""The configuration for an RF-DETR Medium model."""
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 4
patch_size: int = 16
resolution: int = 576
positional_encoding_size: int = 36
pretrain_weights: PathLikeStr | None = "rf-detr-medium.pth"
# res 704, ps 16, 2 windows, 4 dec layers, 300 queries, ViT-S basis
class RFDETRLargeConfig(ModelConfig):
"""Configuration for the RF-DETR Large model variant."""
encoder: Literal["dinov2_windowed_small"] = "dinov2_windowed_small"
hidden_dim: int = 256
dec_layers: int = 4
sa_nheads: int = 8
ca_nheads: int = 16
dec_n_points: int = 2
num_windows: int = 2
patch_size: int = 16
projector_scale: list[Literal["P4",]] = ["P4"]
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_classes: int = 90
positional_encoding_size: int = 704 // 16
pretrain_weights: PathLikeStr | None = "rf-detr-large-2026.pth"
resolution: int = 704
# Explicit so populate_args and _build_args_from_configs agree.
# ModelConfig does not define these fields; without them the legacy path
# picks up populate_args defaults (num_select=100) while the PTL path falls
# back to TrainConfig.num_select (300), causing a postprocess mismatch.
num_queries: int = 300
num_select: int = 300
class RFDETRSegPreviewConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation Preview model."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 4
patch_size: int = 12
resolution: int = 432
positional_encoding_size: int = 36
num_queries: int = 200
num_select: int = 200
pretrain_weights: PathLikeStr | None = "rf-detr-seg-preview.pt"
num_classes: int = 90
class RFDETRSegNanoConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation Nano model variant."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 1
dec_layers: int = 4
patch_size: int = 12
resolution: int = 312
positional_encoding_size: int = 312 // 12
num_queries: int = 100
num_select: int = 100
pretrain_weights: PathLikeStr | None = "rf-detr-seg-nano.pt"
num_classes: int = 90
class RFDETRSegSmallConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation Small model variant."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 4
patch_size: int = 12
resolution: int = 384
positional_encoding_size: int = 384 // 12
num_queries: int = 100
num_select: int = 100
pretrain_weights: PathLikeStr | None = "rf-detr-seg-small.pt"
num_classes: int = 90
class RFDETRSegMediumConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation Medium model variant."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 5
patch_size: int = 12
resolution: int = 432
positional_encoding_size: int = 432 // 12
num_queries: int = 200
num_select: int = 200
pretrain_weights: PathLikeStr | None = "rf-detr-seg-medium.pt"
num_classes: int = 90
class RFDETRSegLargeConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation Large model variant."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 5
patch_size: int = 12
resolution: int = 504
positional_encoding_size: int = 504 // 12
num_queries: int = 200
num_select: int = 200
pretrain_weights: PathLikeStr | None = "rf-detr-seg-large.pt"
num_classes: int = 90
class RFDETRSegXLargeConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation XLarge model variant."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 6
patch_size: int = 12
resolution: int = 624
positional_encoding_size: int = 624 // 12
num_queries: int = 300
num_select: int = 300
pretrain_weights: PathLikeStr | None = "rf-detr-seg-xlarge.pt"
num_classes: int = 90
class RFDETRSeg2XLargeConfig(RFDETRBaseConfig):
"""Configuration for the RF-DETR Segmentation 2XLarge model variant."""
segmentation_head: bool = True
out_feature_indexes: list[int] = [3, 6, 9, 12]
num_windows: int = 2
dec_layers: int = 6
patch_size: int = 12
resolution: int = 768
positional_encoding_size: int = 768 // 12