Addax-Data-Science commited on
Commit
d9bb75c
·
verified ·
1 Parent(s): 7e3c191

Upload 162 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. DeepForestVisionV2.pth +3 -0
  2. LICENSE.md +66 -0
  3. dinov3/.DS_Store +0 -0
  4. dinov3/__init__.py +6 -0
  5. dinov3/checkpointer/__init__.py +18 -0
  6. dinov3/checkpointer/checkpointer.py +352 -0
  7. dinov3/configs/__init__.py +16 -0
  8. dinov3/configs/config.py +222 -0
  9. dinov3/configs/ssl_default_config.yaml +205 -0
  10. dinov3/configs/train/dinov3_vit7b16_gram_anchor.yaml +203 -0
  11. dinov3/configs/train/dinov3_vit7b16_high_res_adapt.yaml +224 -0
  12. dinov3/configs/train/dinov3_vit7b16_pretrain.yaml +172 -0
  13. dinov3/configs/train/dinov3_vitl16_lvd1689m_distilled.yaml +251 -0
  14. dinov3/configs/train/multi_distillation_test.yaml +27 -0
  15. dinov3/configs/train/multidist_tests/vitb_p16.yaml +7 -0
  16. dinov3/configs/train/multidist_tests/vits_p16.yaml +6 -0
  17. dinov3/configs/train/vitl_im1k_lin834.yaml +143 -0
  18. dinov3/data/__init__.py +12 -0
  19. dinov3/data/adapters.py +68 -0
  20. dinov3/data/augmentations.py +227 -0
  21. dinov3/data/collate.py +125 -0
  22. dinov3/data/datasets/__init__.py +9 -0
  23. dinov3/data/datasets/ade20k.py +88 -0
  24. dinov3/data/datasets/coco_captions.py +88 -0
  25. dinov3/data/datasets/decoders.py +40 -0
  26. dinov3/data/datasets/extended.py +46 -0
  27. dinov3/data/datasets/image_net.py +297 -0
  28. dinov3/data/datasets/image_net_22k.py +301 -0
  29. dinov3/data/loaders.py +237 -0
  30. dinov3/data/masking.py +94 -0
  31. dinov3/data/meta_loaders.py +78 -0
  32. dinov3/data/samplers.py +229 -0
  33. dinov3/data/transforms.py +165 -0
  34. dinov3/distributed/__init__.py +24 -0
  35. dinov3/distributed/torch_distributed_primitives.py +100 -0
  36. dinov3/distributed/torch_distributed_wrapper.py +351 -0
  37. dinov3/env/__init__.py +33 -0
  38. dinov3/eval/.DS_Store +0 -0
  39. dinov3/eval/accumulators.py +77 -0
  40. dinov3/eval/data.py +256 -0
  41. dinov3/eval/dense/depth/models/__init__.py +186 -0
  42. dinov3/eval/dense/depth/models/dpt_head.py +532 -0
  43. dinov3/eval/dense/depth/models/embed.py +52 -0
  44. dinov3/eval/dense/depth/models/encoder.py +124 -0
  45. dinov3/eval/dense/depth/utils.py +19 -0
  46. dinov3/eval/detection/config.py +65 -0
  47. dinov3/eval/detection/models/__init__.py +5 -0
  48. dinov3/eval/detection/models/backbone.py +117 -0
  49. dinov3/eval/detection/models/detr.py +462 -0
  50. dinov3/eval/detection/models/global_ape_decoder.py +321 -0
DeepForestVisionV2.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:21f12d6a6610efd8c69f04302b6dc7cdec3c8e75a8306e70348469af60c2c44e
3
+ size 343226287
LICENSE.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DINOv3 License
2
+
3
+ *Last Updated: August 19, 2025*
4
+
5
+ **“Agreement”** means the terms and conditions for use, reproduction, distribution and modification of the DINO Materials set forth herein.
6
+
7
+ **“DINO Materials”** means, collectively, Documentation and the models, software and algorithms, including machine-learning model code, trained model weights, inference-enabling code, training-enabling code, fine-tuning enabling code, and other elements of the foregoing distributed by Meta and made available under this Agreement.
8
+
9
+ **“Documentation”** means the specifications, manuals and documentation accompanying
10
+ DINO Materials distributed by Meta.
11
+
12
+ **“Licensee”** or **“you”** means you, or your employer or any other person or entity (if you are entering into this Agreement on such person or entity’s behalf), of the age required under applicable laws, rules or regulations to provide legal consent and that has legal authority to bind your employer or such other person or entity if you are entering in this Agreement on their behalf.
13
+
14
+ **“Meta”** or **“we”** means Meta Platforms Ireland Limited (if you are located in or, if you are an entity, your principal place of business is in the EEA or Switzerland) or Meta Platforms, Inc. (if you are located outside of the EEA or Switzerland).
15
+
16
+ **“Sanctions”** means any economic or trade sanctions or restrictions administered or enforced by the United States (including the Office of Foreign Assets Control of the U.S. Department of the Treasury (“OFAC”), the U.S. Department of State and the U.S. Department of Commerce), the United Nations, the European Union, or the United Kingdom.
17
+
18
+ **“Trade Controls”** means any of the following: Sanctions and applicable export and import controls.
19
+
20
+ By clicking “I Accept” below or by using or distributing any portion or element of the DINO Materials, you agree to be bound by this Agreement.
21
+
22
+ ## 1. License Rights and Redistribution.
23
+
24
+ a. <ins>Grant of Rights</ins>. You are granted a non-exclusive, worldwide, non-transferable and royalty-free limited license under Meta’s intellectual property or other rights owned by Meta embodied in the DINO Materials to use, reproduce, distribute, copy, create derivative works of, and make modifications to the DINO Materials.
25
+
26
+ b. <ins>Redistribution and Use</ins>.
27
+
28
+ i. Distribution of DINO Materials, and any derivative works thereof, are subject to the terms of this Agreement. If you distribute or make the DINO Materials, or any derivative works thereof, available to a third party, you may only do so under the terms of this Agreement and you shall provide a copy of this Agreement with any such DINO Materials.
29
+
30
+ ii. If you submit for publication the results of research you perform on, using, or otherwise in connection with DINO Materials, you must acknowledge the use of DINO Materials in your publication.
31
+
32
+ iii. Your use of the DINO Materials must comply with applicable laws and regulations, including Trade Control Laws and applicable privacy and data protection laws.
33
+
34
+ iv. Your use of the DINO Materials will not involve or encourage others to reverse engineer, decompile or discover the underlying components of the DINO Materials.
35
+
36
+ v. You are not the target of Trade Controls and your use of DINO Materials must comply with Trade Controls. You agree not to use, or permit others to use, DINO Materials for any activities subject to the International Traffic in Arms Regulations (ITAR) or end uses prohibited by Trade Controls, including those related to military or warfare purposes, nuclear industries or applications, espionage, or the development or use of guns or illegal weapons.
37
+
38
+ ## 2. User Support.
39
+
40
+ Your use of the DINO Materials is done at your own discretion; Meta does not process any information nor provide any service in relation to such use. Meta is under no obligation to provide any support services for the DINO Materials. Any support provided is “as is”, “with all faults”, and without warranty of any kind.
41
+
42
+ ## 3. Disclaimer of Warranty.
43
+
44
+ UNLESS REQUIRED BY APPLICABLE LAW, THE DINO MATERIALS AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED ON AN “AS IS” BASIS, WITHOUT WARRANTIES OF ANY KIND, AND META DISCLAIMS ALL WARRANTIES OF ANY KIND, BOTH EXPRESS AND IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING OR REDISTRIBUTING THE DINO MATERIALS AND ASSUME ANY RISKS ASSOCIATED WITH YOUR USE OF THE DINO MATERIALS AND ANY OUTPUT AND RESULTS.
45
+
46
+ ## 4. Limitation of Liability.
47
+
48
+ IN NO EVENT WILL META OR ITS AFFILIATES BE LIABLE UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, NEGLIGENCE, PRODUCTS LIABILITY, OR OTHERWISE, ARISING OUT OF THIS AGREEMENT, FOR ANY LOST PROFITS OR ANY DIRECT OR INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL, EXEMPLARY OR PUNITIVE DAMAGES, EVEN IF META OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY OF THE FOREGOING.
49
+
50
+ ## 5. Intellectual Property.
51
+
52
+ a. Subject to Meta’s ownership of DINO Materials and derivatives made by or for Meta, with respect to any derivative works and modifications of the DINO Materials that are made by you, as between you and Meta, you are and will be the owner of such derivative works and modifications.
53
+
54
+ b. If you institute litigation or other proceedings against Meta or any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the DINO Materials, outputs or results, or any portion of any of the foregoing, constitutes infringement of intellectual property or other rights owned or licensable by you, then any licenses granted to you under this Agreement shall terminate as of the date such litigation or claim is filed or instituted. You will indemnify and hold harmless Meta from and against any claim by any third party arising out of or related to your use or distribution of the DINO Materials.
55
+
56
+ ## 6. Term and Termination.
57
+
58
+ The term of this Agreement will commence upon your acceptance of this Agreement or access to the DINO Materials and will continue in full force and effect until terminated in accordance with the terms and conditions herein. Meta may terminate this Agreement if you are in breach of any term or condition of this Agreement. Upon termination of this Agreement, you shall delete and cease use of the DINO Materials. Sections 3, 4 and 7 shall survive the termination of this Agreement.
59
+
60
+ ## 7. Governing Law and Jurisdiction.
61
+
62
+ This Agreement will be governed and construed under the laws of the State of California without regard to choice of law principles, and the UN Convention on Contracts for the International Sale of Goods does not apply to this Agreement. The courts of California shall have exclusive jurisdiction of any dispute arising out of this Agreement.
63
+
64
+ ## 8. Modifications and Amendments.
65
+
66
+ Meta may modify this Agreement from time to time; provided that they are similar in spirit to the current version of the Agreement, but may differ in detail to address new problems or concerns. All such changes will be effective immediately. Your continued use of the DINO Materials after any modification to this Agreement constitutes your agreement to such modification. Except as provided in this Agreement, no modification or addition to any provision of this Agreement will be binding unless it is in writing and signed by an authorized representative of both you and Meta.
dinov3/.DS_Store ADDED
Binary file (10.2 kB). View file
 
dinov3/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ __version__ = "0.0.1"
dinov3/checkpointer/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from .checkpointer import (
7
+ CheckpointRetentionPolicy,
8
+ cleanup_checkpoint,
9
+ find_all_checkpoints,
10
+ find_latest_checkpoint,
11
+ init_fsdp_model_from_checkpoint,
12
+ init_model_from_checkpoint_for_evals,
13
+ keep_checkpoint_copy,
14
+ keep_last_n_checkpoints,
15
+ load_checkpoint,
16
+ register_dont_save_hooks,
17
+ save_checkpoint,
18
+ )
dinov3/checkpointer/checkpointer.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ """
7
+ Suggested file structure:
8
+
9
+ output_dir/
10
+ |-- ckpt/
11
+ | |-- 0/
12
+ | |-- 99/
13
+ | |-- 199/
14
+ | |-- 199_keep/
15
+ | |-- 299/
16
+ | `-- ...
17
+ `-- eval/
18
+ `-- 0/
19
+ `-- 99/
20
+ `-- ckpt/
21
+
22
+ Distributed checkpointer docs:
23
+ - https://pytorch.org/tutorials/recipes/distributed_checkpoint_recipe.html
24
+ - https://pytorch.org/docs/stable/distributed.checkpoint.html
25
+ """
26
+
27
+ import logging
28
+ import shutil
29
+ import subprocess
30
+ import tempfile
31
+ from enum import Enum
32
+ from pathlib import Path
33
+ from typing import List, Sequence, Set
34
+
35
+ import torch
36
+ import torch.distributed as dist
37
+ import torch.distributed.checkpoint as dcp
38
+ import torch.distributed.checkpoint.filesystem as dcpfs
39
+ import torch.distributed.checkpoint.state_dict as dcpsd
40
+ from torch.distributed.checkpoint.stateful import Stateful
41
+
42
+ logger = logging.getLogger("dinov3")
43
+
44
+
45
+ class CheckpointRetentionPolicy(Enum):
46
+ ALL = "all" # keep all checkpoints
47
+ BEST = "best"
48
+ LAST = "last"
49
+ LAST_AND_BEST = "last_and_best"
50
+ NONE = "none" # do not keep any checkpoints
51
+
52
+ @property
53
+ def keep_filters(self) -> Set[str]:
54
+ """Files that match these patterns are not deleted by cleanup"""
55
+ if self == CheckpointRetentionPolicy.LAST:
56
+ return set(["final"])
57
+ if self == CheckpointRetentionPolicy.BEST:
58
+ return set(["best"])
59
+ if self == CheckpointRetentionPolicy.LAST_AND_BEST:
60
+ return set(["final", "best"])
61
+ if self == CheckpointRetentionPolicy.ALL:
62
+ return set()
63
+ return set()
64
+
65
+ @property
66
+ def max_to_keep(self) -> int | None:
67
+ """
68
+ maximum "periodic" checkpoints to keep concurrently, ie. saved with `step` and not `save`. `None` for keep all
69
+ """
70
+ if self == CheckpointRetentionPolicy.ALL:
71
+ return None
72
+ return 1
73
+
74
+
75
+ def save_checkpoint(
76
+ ckpt_dir: str | Path, # output_dir/ckpt/199
77
+ *,
78
+ iteration: int | str,
79
+ model: torch.nn.Module,
80
+ optimizer: torch.optim.Optimizer | None = None,
81
+ overwrite: bool = True,
82
+ process_group: dist.ProcessGroup = None,
83
+ **others: Stateful,
84
+ ):
85
+ """Save a plain/DDP/FSDP/FSDP2 model, its optimizer, an integer iteration and other stateful objects."""
86
+ rank = torch.distributed.get_rank(group=process_group)
87
+
88
+ # Rank 0 checks if the checkpoint directory exists, but all ranks need to know if if exists,
89
+ # so they can raise an error when overwrite is False. If overwrite is True, rank 0 will delete it
90
+ # and other ranks wait for the deletion to finish.
91
+ ckpt_dir = Path(ckpt_dir)
92
+ ckpt_dir_exists = [ckpt_dir.exists() if rank == 0 else None]
93
+ src_rank = 0
94
+ if process_group is not None:
95
+ src_rank = torch.distributed.get_global_rank(group=process_group, group_rank=0)
96
+ torch.distributed.broadcast_object_list(ckpt_dir_exists, src=src_rank, group=process_group)
97
+ ckpt_dir_exists = ckpt_dir_exists[0]
98
+ if ckpt_dir_exists:
99
+ if overwrite:
100
+ if rank == 0:
101
+ if ckpt_dir.is_dir():
102
+ shutil.rmtree(ckpt_dir)
103
+ else:
104
+ ckpt_dir.unlink()
105
+ logger.info(f"Deleted: {ckpt_dir}")
106
+ torch.distributed.barrier(group=process_group)
107
+ else:
108
+ raise RuntimeError(f"Checkpoint already exists: {ckpt_dir}")
109
+
110
+ # Rank 0 creates a temporary directory for the checkpoint and broadcasts the name to all ranks.
111
+ ckpt_dir.parent.mkdir(parents=True, exist_ok=True)
112
+ ckpt_dir_tmp = [tempfile.mkdtemp(dir=ckpt_dir.parent, prefix=ckpt_dir.name) if rank == 0 else None]
113
+ torch.distributed.broadcast_object_list(ckpt_dir_tmp, src=src_rank, group=process_group)
114
+ ckpt_dir_tmp = Path(ckpt_dir_tmp[0])
115
+
116
+ to_save = {"iteration": iteration}
117
+ to_save["model"] = dcpsd.get_model_state_dict(model)
118
+ if optimizer is not None:
119
+ to_save["optimizer"] = dcpsd.get_optimizer_state_dict(model, optimizer)
120
+ to_save.update(others)
121
+ dcp.save(
122
+ to_save,
123
+ storage_writer=dcpfs.FileSystemWriter(ckpt_dir_tmp),
124
+ process_group=process_group,
125
+ )
126
+
127
+ # Rank 0 renames the temporary directory to the final checkpoint directory. All ranks wait for the rename.
128
+ if rank == 0:
129
+ ckpt_dir_tmp.rename(ckpt_dir)
130
+ torch.distributed.barrier()
131
+
132
+ logger.info(f"Saved: {ckpt_dir}")
133
+
134
+
135
+ def load_checkpoint(
136
+ ckpt_dir: str | Path, # output_dir/ckpt/199
137
+ *,
138
+ model: torch.nn.Module,
139
+ optimizer: torch.optim.Optimizer | None = None,
140
+ strict_loading: bool = True,
141
+ process_group: dist.ProcessGroup = None,
142
+ **others: Stateful,
143
+ ) -> int | None:
144
+ """
145
+ Load a plain/DDP/FSDP/FSDP2 model, its optimizer, an integer iteration and other stateful objects.
146
+ Can you take a checkpoint saved on N ranks and load it on M ranks? Sure you can!
147
+ Activation checkpointing and torch-compile can also be different between save and load, no problem.
148
+ """
149
+ ckpt_dir = Path(ckpt_dir)
150
+ to_load = {"iteration": None}
151
+ to_load["model"] = dcpsd.get_model_state_dict(model)
152
+ if optimizer is not None:
153
+ to_load["optimizer"] = dcpsd.get_optimizer_state_dict(model, optimizer)
154
+ to_load.update(others)
155
+ dcp.load(
156
+ to_load,
157
+ storage_reader=dcpfs.FileSystemReader(ckpt_dir),
158
+ planner=dcp.default_planner.DefaultLoadPlanner(allow_partial_load=not strict_loading),
159
+ process_group=process_group,
160
+ )
161
+ iteration = to_load["iteration"]
162
+ dcpsd.set_model_state_dict(model, to_load["model"])
163
+ if optimizer is not None:
164
+ dcpsd.set_optimizer_state_dict(model, optimizer, to_load["optimizer"])
165
+ logger.info(f"Loaded: {ckpt_dir}")
166
+ return iteration
167
+
168
+
169
+ def register_dont_save_hooks(module: torch.nn.Module, dont_save: Sequence[str]):
170
+ """
171
+ Registers save/load state dict hooks such that the weights in `dont_save` are not persisted in the checkpoint.
172
+
173
+ Typical use case: a classification model composed of a frozen backbone and a trainable head.
174
+ If the frozen backbone is loaded from torch hub, it does't make sense to save a copy of it in each checkpoint.
175
+ """
176
+
177
+ def state_dict_post_hook(module, state_dict, prefix, local_metadata):
178
+ # Remove frozen weights so they won't get saved.
179
+ # If this module is not the top-level module, its weights will have a prefix in the state dict.
180
+ nonlocal _dont_save
181
+ for k in _dont_save:
182
+ del state_dict[prefix + k]
183
+
184
+ def load_state_dict_pre_hook(
185
+ module,
186
+ state_dict,
187
+ prefix,
188
+ local_metadata,
189
+ strict,
190
+ missing_keys,
191
+ unexpected_keys,
192
+ error_msgs,
193
+ ):
194
+ # This pre hook exists only to pass the prefix to the post hook when loading the state dict.
195
+ nonlocal _prefix
196
+ assert _prefix is None
197
+ _prefix = prefix
198
+
199
+ def load_state_dict_post_hook(module, incompatible_keys):
200
+ # Remove the frozen weights from the missing keys so they don't raise an error.
201
+ nonlocal _prefix
202
+ assert _prefix is not None
203
+ to_remove = []
204
+ for missing_key in incompatible_keys.missing_keys:
205
+ k = missing_key.removeprefix(_prefix)
206
+ k = k.replace("_checkpoint_wrapped_module.", "") # Added by activation checkpointing
207
+ if k in _dont_save:
208
+ to_remove.append(missing_key)
209
+ for r in to_remove:
210
+ incompatible_keys.missing_keys.remove(r)
211
+ _prefix = None
212
+
213
+ _dont_save = set(name.replace("_checkpoint_wrapped_module.", "") for name in dont_save)
214
+ _prefix = None
215
+ module.register_state_dict_post_hook(state_dict_post_hook)
216
+ module.register_load_state_dict_pre_hook(load_state_dict_pre_hook)
217
+ module.register_load_state_dict_post_hook(load_state_dict_post_hook)
218
+
219
+
220
+ def find_all_checkpoints(ckpt_dir: Path | str) -> list[Path]:
221
+ """Find all checkpoints in a directory, i.e. subdirs with integer name. Sorted from first to last."""
222
+ ckpt_dir = Path(ckpt_dir)
223
+ if not ckpt_dir.is_dir():
224
+ return []
225
+ checkpoints = [p for p in ckpt_dir.iterdir() if p.is_dir() and _is_int(p.name)]
226
+ checkpoints.sort(key=lambda p: int(p.name))
227
+ return checkpoints
228
+
229
+
230
+ def find_latest_checkpoint(ckpt_dir: Path | str) -> Path | None:
231
+ """Find the latest checkpoint in a directory, i.e. the subdir with the highest integer name."""
232
+ checkpoints = find_all_checkpoints(ckpt_dir)
233
+ if len(checkpoints) == 0:
234
+ return None
235
+ return checkpoints[-1]
236
+
237
+
238
+ def keep_last_n_checkpoints(ckpt_dir: Path | str, n: int | None):
239
+ """In a directory with integer-named subdirs, keep only the n subdirs with the highest number."""
240
+ if n is None:
241
+ return
242
+ checkpoints = find_all_checkpoints(ckpt_dir)
243
+ for ckpt_dir in checkpoints[:-n]:
244
+ try:
245
+ shutil.rmtree(ckpt_dir)
246
+ logger.info(f"Deleted: {ckpt_dir}")
247
+ except Exception:
248
+ logger.exception(f"Failed to delete: {ckpt_dir}")
249
+
250
+
251
+ def keep_checkpoint_copy(src: Path | str):
252
+ """Copy a file/directory next to itself with a _keep suffix. Files are hardlinked."""
253
+ src = Path(src)
254
+ dst = src.parent / f"{src.name}_keep"
255
+ subprocess.check_output(["cp", "--recursive", "--link", src, dst])
256
+ logger.info(f"Copied: {src} -> {dst}")
257
+
258
+
259
+ def _is_int(s: str) -> bool:
260
+ try:
261
+ int(s)
262
+ return True
263
+ except ValueError:
264
+ return False
265
+
266
+
267
+ # Initialize a FSDP2 model from DCP or PyTorch standard checkpoint
268
+ def init_fsdp_model_from_checkpoint(
269
+ model: torch.nn.Module,
270
+ checkpoint_path: str,
271
+ skip_load_keys: List[str] | None = None,
272
+ keys_not_sharded: List[str] | None = None,
273
+ process_group: dist.ProcessGroup = None,
274
+ ):
275
+ if not Path(checkpoint_path).is_dir(): # PyTorch standard checkpoint
276
+ logger.info(f"Loading pretrained weights from {checkpoint_path}")
277
+ chkpt = torch.load(checkpoint_path, map_location="cpu")["teacher"]
278
+ from torch.distributed.device_mesh import DeviceMesh, init_device_mesh
279
+
280
+ if process_group is None:
281
+ world_mesh = init_device_mesh(
282
+ "cuda",
283
+ mesh_shape=(dist.get_world_size(),),
284
+ mesh_dim_names=("dp",),
285
+ )
286
+ else:
287
+ world_mesh = DeviceMesh.from_group(process_group, "cuda")
288
+ chkpt = {
289
+ key: (
290
+ torch.distributed.tensor.distribute_tensor(tensor, world_mesh, src_data_rank=None)
291
+ if not any(key_not_sharded in key for key_not_sharded in keys_not_sharded)
292
+ else tensor
293
+ )
294
+ for key, tensor in chkpt.items()
295
+ }
296
+ model.load_state_dict(
297
+ {
298
+ key: tensor
299
+ for key, tensor in chkpt.items()
300
+ if not any(skip_load_key in key for skip_load_key in skip_load_keys)
301
+ }
302
+ )
303
+ else: # DCP checkpoint
304
+ load_checkpoint(ckpt_dir=checkpoint_path, model=model, process_group=process_group)
305
+
306
+
307
+ # Initialize a standard non distributed PyTorch model from PyTorch standard checkpoint for evals
308
+ def init_model_from_checkpoint_for_evals(
309
+ model: torch.nn.Module, pretrained_weights: str | Path, checkpoint_key: str = None
310
+ ):
311
+ state_dict = torch.load(pretrained_weights, map_location="cpu")
312
+ if checkpoint_key is not None and checkpoint_key in state_dict:
313
+ logger.info(f"Take key {checkpoint_key} in provided checkpoint dict")
314
+ state_dict = state_dict[checkpoint_key]
315
+ # remove `module.` prefix
316
+ state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()}
317
+ # remove `backbone.` prefix induced by multicrop wrapper
318
+ state_dict = {k.replace("backbone.", ""): v for k, v in state_dict.items()}
319
+ msg = model.load_state_dict(state_dict, strict=False)
320
+ logger.info("Pretrained weights found at {} and loaded with msg: {}".format(pretrained_weights, msg))
321
+
322
+
323
+ def cleanup_checkpoint(ckpt_dir: str, checkpoint_retention_policy: CheckpointRetentionPolicy):
324
+ """
325
+ ckpt_dir is the directory containing each individual checkpoint directories (either at iteration, best (validation performance) or final)
326
+ |-- ckpt_dir/
327
+ | |-- 0/
328
+ | |--checkpoint.pth or dcp_sharded_checkpoint_dir
329
+ | |-- 99/
330
+ |--checkpoint.pth or dcp_sharded_checkpoint_dir
331
+ | |-- 199/
332
+ |--checkpoint.pth or dcp_sharded_checkpoint_dir
333
+ | |-- best/
334
+ |--checkpoint.pth or dcp_sharded_checkpoint_dir
335
+ | |-- 299/
336
+ |--checkpoint.pth or dcp_sharded_checkpoint_dir
337
+ | |-- final/
338
+ |--checkpoint.pth or dcp_sharded_checkpoint_dir
339
+ """
340
+ ckpt_dir = Path(ckpt_dir)
341
+ if not ckpt_dir.is_dir():
342
+ return []
343
+ checkpoint_filters = checkpoint_retention_policy.keep_filters
344
+ checkpoints = [p for p in ckpt_dir.iterdir() if p.is_dir()]
345
+ for checkpoint in checkpoints:
346
+ if checkpoint in checkpoint_filters:
347
+ continue
348
+ try:
349
+ shutil.rmtree(checkpoint)
350
+ logger.info(f"Deleted: {checkpoint}")
351
+ except Exception:
352
+ logger.exception(f"Failed to delete: {checkpoint}")
dinov3/configs/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from .config import (
7
+ DinoV3SetupArgs,
8
+ apply_scaling_rules_to_cfg,
9
+ exit_job,
10
+ get_cfg_from_args,
11
+ get_default_config,
12
+ setup_config,
13
+ setup_job,
14
+ setup_multidistillation,
15
+ write_config,
16
+ )
dinov3/configs/config.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ import math
8
+ import os
9
+ import pathlib
10
+ import sys
11
+ from dataclasses import dataclass, field
12
+ from datetime import timedelta
13
+ from typing import Any, List, Optional, Sequence, Tuple
14
+
15
+ from omegaconf import DictConfig, OmegaConf
16
+
17
+ import dinov3.distributed as distributed
18
+ from dinov3.logging import cleanup_logging, setup_logging
19
+ from dinov3.utils import fix_random_seeds, get_conda_env, get_sha
20
+
21
+ logger = logging.getLogger("dinov3")
22
+
23
+
24
+ @dataclass
25
+ class DinoV3SetupArgs:
26
+ config_file: str
27
+ pretrained_weights: str | None = None
28
+ shard_unsharded_model: bool = False
29
+ output_dir: str = ""
30
+ opts: List[Any] = field(default_factory=lambda: [])
31
+
32
+ def __post_init__(self):
33
+ # When loaded from benchmark.yaml, self.opts is a frozen omegaconf.ListConfig,
34
+ # which works everywhere except when we want to modify it or when
35
+ # we try to json-serialize it. So we convert it to a regular list here.
36
+ if OmegaConf.is_config(self.opts):
37
+ self.opts = OmegaConf.to_object(self.opts)
38
+
39
+
40
+ def apply_scaling_rules_to_cfg(cfg): # to fix
41
+ assert distributed.is_enabled(), "Setup distributed to get global size !"
42
+ if "schedules" in cfg:
43
+ # For schedules v2, the scaling rules are applied when building the schedules, the config is not modified
44
+ return cfg
45
+
46
+ if cfg.optim.scaling_rule == "linear_wrt_256":
47
+ old_lr = cfg.optim.lr
48
+ cfg.optim.lr *= cfg.train.batch_size_per_gpu * distributed.get_world_size() / 256.0
49
+ logger.info(f"linear scaling learning rate; old: {old_lr}, new: {cfg.optim.lr}")
50
+ elif cfg.optim.scaling_rule == "sqrt_wrt_1024":
51
+ old_lr = cfg.optim.lr
52
+ cfg.optim.lr *= 4 * math.sqrt(cfg.train.batch_size_per_gpu * distributed.get_world_size() / 1024.0)
53
+ logger.info(f"sqrt scaling learning rate; old: {old_lr}, new: {cfg.optim.lr}")
54
+ return cfg
55
+
56
+
57
+ def write_config(cfg, output_dir, name="config.yaml"):
58
+ logger.info(OmegaConf.to_yaml(cfg))
59
+ output_dir = os.path.abspath(output_dir)
60
+ saved_cfg_path = os.path.join(output_dir, name)
61
+ with open(saved_cfg_path, "w") as f:
62
+ OmegaConf.save(config=cfg, f=f)
63
+ return saved_cfg_path
64
+
65
+
66
+ def get_default_config() -> DictConfig:
67
+ p = pathlib.Path(__file__).parent / "ssl_default_config.yaml"
68
+ return OmegaConf.load(p)
69
+
70
+
71
+ def get_cfg_from_args(args: DinoV3SetupArgs, multidistillation=False, strict=True):
72
+ overrides = [*args.opts]
73
+ if args.output_dir is not None:
74
+ overrides.append(f"train.output_dir={os.path.realpath(args.output_dir)}")
75
+
76
+ # Config file
77
+ cfg = OmegaConf.load(args.config_file)
78
+
79
+ # Command line overrides
80
+ opts_cfg = OmegaConf.from_cli(overrides)
81
+
82
+ if multidistillation:
83
+ cfg = OmegaConf.merge(cfg, opts_cfg)
84
+ else:
85
+ # Default config
86
+ default_cfg = get_default_config()
87
+ if strict:
88
+ OmegaConf.set_struct(default_cfg, True)
89
+ cfg = OmegaConf.merge(default_cfg, cfg, opts_cfg)
90
+ return cfg
91
+
92
+
93
+ def setup_config(args: DinoV3SetupArgs, strict_cfg=True):
94
+ """
95
+ Create configs and perform basic setups.
96
+ """
97
+ # Create the cfg with OmegaConf
98
+ cfg = get_cfg_from_args(args, strict=strict_cfg)
99
+ # setup distributed, logging, and random seeds
100
+ logger.info("\n".join("%s: %s" % (k, str(v)) for k, v in sorted(dict(vars(args)).items())))
101
+ # dump config before modifying so it can be reloaded
102
+ if args.output_dir is not None:
103
+ write_config(cfg, args.output_dir)
104
+ # modify the config inplace by applying scaling rules
105
+ apply_scaling_rules_to_cfg(cfg)
106
+ return cfg
107
+
108
+
109
+ def _enumerate_all_subgroup_ranks(all_subgroup_rank_spans: Sequence[Tuple[int, int]]):
110
+ """Expands a specification of process subgroups from spans to enumerated ranks.
111
+
112
+ Args:
113
+ all_group_rank_spans: a sequence of rank spans (first rank, last rank),
114
+ one for each process group. Example: ((0, 1), (2, 3), (4, 7)).
115
+ """
116
+ for first, last in all_subgroup_rank_spans:
117
+ assert first <= last
118
+ return tuple(tuple(range(first, last + 1)) for first, last in all_subgroup_rank_spans)
119
+
120
+
121
+ def setup_multidistillation(args: DinoV3SetupArgs):
122
+ base_output_dir = args.output_dir
123
+ os.makedirs(args.output_dir, exist_ok=True)
124
+ # get config file for this rank
125
+ base_cfg = OmegaConf.load(args.config_file)
126
+ assert base_cfg.multidistillation.enabled
127
+
128
+ global_batch_size = base_cfg.multidistillation.global_batch_size
129
+
130
+ distributed.enable(overwrite=True)
131
+ seed = getattr(args, "seed", 0)
132
+ rank = distributed.get_rank()
133
+
134
+ # build process subgroups
135
+ all_subgroup_rank_spans = tuple(
136
+ (student.ranks_range[0], student.ranks_range[1] - 1) for student in base_cfg.multidistillation.students
137
+ )
138
+ all_subgroup_ranks = _enumerate_all_subgroup_ranks(all_subgroup_rank_spans)
139
+ distributed.new_subgroups(all_subgroup_ranks)
140
+
141
+ found = False
142
+ for student in base_cfg.multidistillation.students:
143
+ if rank in range(*student.ranks_range):
144
+ found = True
145
+ break
146
+ assert found, "rank of worker not in defined range"
147
+
148
+ name = student.name
149
+ config_path = student.config_path
150
+ n_gpus = student.ranks_range[1] - student.ranks_range[0]
151
+ assert global_batch_size % n_gpus == 0
152
+ total_n_gpus = distributed.get_world_size()
153
+
154
+ args.output_dir = os.path.join(base_output_dir, name)
155
+ args.opts += [f"train.output_dir={args.output_dir}"]
156
+ args.opts += [f"train.batch_size_per_gpu={global_batch_size // total_n_gpus}"]
157
+ args.config_file = os.path.abspath(config_path)
158
+ default_cfg = get_default_config()
159
+ cfg = OmegaConf.load(args.config_file)
160
+ cfg = OmegaConf.merge(default_cfg, cfg, base_cfg, OmegaConf.from_cli(args.opts))
161
+
162
+ global logger
163
+ setup_logging(output=args.output_dir, level=logging.INFO)
164
+
165
+ fix_random_seeds(seed + rank)
166
+
167
+ write_config(cfg, args.output_dir)
168
+ apply_scaling_rules_to_cfg(cfg)
169
+
170
+ return cfg
171
+
172
+
173
+ def setup_job(
174
+ output_dir: Optional[str] = None,
175
+ distributed_enabled: bool = True,
176
+ logging_enabled: bool = True,
177
+ seed: Optional[int] = 0,
178
+ restrict_print_to_main_process: bool = True,
179
+ distributed_timeout: timedelta | None = None,
180
+ ):
181
+ """
182
+ Setup methods that should be done in every fairvit job
183
+ Initializes logging, distributed, random seeds and other utilities.
184
+ """
185
+ if output_dir is not None:
186
+ output_dir = os.path.realpath(output_dir)
187
+ os.makedirs(output_dir, exist_ok=True)
188
+
189
+ if logging_enabled:
190
+ setup_logging(
191
+ output=output_dir,
192
+ level=logging.INFO,
193
+ log_to_stdout_only_in_main_process=restrict_print_to_main_process,
194
+ )
195
+
196
+ if distributed_enabled:
197
+ distributed.enable(
198
+ overwrite=True,
199
+ nccl_async_error_handling=True,
200
+ restrict_print_to_main_process=restrict_print_to_main_process,
201
+ timeout=distributed_timeout,
202
+ )
203
+
204
+ if seed is not None:
205
+ rank = distributed.get_rank()
206
+ fix_random_seeds(seed + rank)
207
+
208
+ logger = logging.getLogger("dinov3")
209
+ logger.info("git:\n {}\n".format(get_sha()))
210
+
211
+ # Log some python info
212
+ conda_env_name, conda_env_path = get_conda_env()
213
+ logger.info(f"conda env name: {conda_env_name}")
214
+ logger.info(f"conda env path: {conda_env_path}")
215
+ logger.info(f"python path: {sys.path}")
216
+
217
+
218
+ def exit_job(distributed_enabled: bool = True, logging_enabled: bool = True):
219
+ if distributed_enabled:
220
+ distributed.disable()
221
+ if logging_enabled:
222
+ cleanup_logging()
dinov3/configs/ssl_default_config.yaml ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL:
2
+ META_ARCHITECTURE: SSLMetaArch
3
+ DEVICE: cuda
4
+ WEIGHTS: ''
5
+ DTYPE: float32
6
+ compute_precision:
7
+ param_dtype: bf16
8
+ reduce_dtype: fp32
9
+ sharding_strategy: SHARD_GRAD_OP
10
+ dino:
11
+ loss_weight: 1.0
12
+ global_ignore_diagonal: true # Whether to ignore A-A and B-B global pairs, default as in DINOv2, ignored by SSLMetaArch
13
+ head_n_prototypes: 65536
14
+ head_bottleneck_dim: 256
15
+ head_norm_last_layer: false
16
+ head_nlayers: 3
17
+ head_hidden_dim: 2048
18
+ koleo_loss_weight: 0.1
19
+ koleo_loss_distributed: false
20
+ koleo_topk: 1
21
+ koleo_distributed_replicas: 0
22
+ koleo_distributed_loss_group_size: null # Size of the nearest neighbor set for distributed Koleo. If None, uses global batch size.
23
+ koleo_distributed_loss_group_data: true # group data from adjacent ranks to make sure koleo is applied on the same data distribution
24
+ force_weight_norm: false
25
+ reweight_dino_local_loss: false # If true, reweighting of DINO loss
26
+ local_loss_weight_schedule: # Schedule for local loss weight, enabled if reweight_dino_local_loss is true
27
+ start: 0.5
28
+ peak: 0.5
29
+ end: 0.5
30
+ warmup_epochs: 0
31
+ ibot:
32
+ loss_weight: 1.0
33
+ mask_sample_probability: 0.5
34
+ mask_ratio_min_max:
35
+ - 0.1
36
+ - 0.5
37
+ mask_random_circular_shift: false
38
+ force_masking_even_with_zero_weight: False
39
+ separate_head: true
40
+ head_n_prototypes: 65536
41
+ head_bottleneck_dim: 256
42
+ head_norm_last_layer: false
43
+ head_nlayers: 3
44
+ head_hidden_dim: 2048
45
+ gram:
46
+ use_loss: false # (bool) if true gram is used, else not
47
+ compute_stats: false # (bool): if true compute auxilliary stats
48
+ loss_weight: 1.0 # (float): weight of the loss
49
+ ema_teacher: false # (bool): using the EMA teacher as GRAM teacher
50
+ ckpt: null #(str): Checkpoint to the teacher
51
+ it_load_ema_teacher: -1 # (int): iteration at which the ema teacher is loaded into the gram teacher
52
+ rep_update: true # (bool): if true GRAM teacher updated every gram.update_frequency after iter gram.it_first_update steps
53
+ update_frequency: 50000 # (int): update frequency
54
+ it_first_update: 0 # (int): iteration of the first update
55
+ max_updates: null # (int): maximum number of updates to gram teacher. If None, it is unlimited
56
+ normalized: true # (bool): normalization of the features
57
+ img_level: false # (bool): if true GRAM computation at the image else, otherwise at the local batch level
58
+ remove_neg: false # (bool): if true remove the negative similarities before applying the loss
59
+ remove_only_teacher_neg: false # (bool): remove negative similarities of the teacher
60
+ tokens_used: all # (str): In [all, masked, unmasked]
61
+ global_teacher_resize_method: bicubic # Method for resizing the outputs of the gram teacher
62
+ global_teacher_resize_antialias: false # Whether to use antialiasing when resizing the outputs of the gram teacher
63
+ loss_weight_schedule: null # (dict): If not None, use a schedule for the loss weight instead of `loss_weight`
64
+ train:
65
+ batch_size_per_gpu: 64
66
+ dataset_path: ImageNet:split=TRAIN
67
+ data_config: null
68
+ output_dir: .
69
+ saveckp_freq: 20
70
+ seed: 0
71
+ num_workers: 10
72
+ OFFICIAL_EPOCH_LENGTH: 1250
73
+ monitor_gradient_norm: false
74
+ chunk_schedule: []
75
+ use_teacher_head: true
76
+ learn_from_teacher_tokens: false
77
+ centering: "sinkhorn_knopp" # or "sinkhorn_knopp"
78
+ checkpointing: false
79
+ checkpointing_full: false # aggressive checkpointing
80
+ compile: true
81
+ cudagraphs: false
82
+ sharded_eval_checkpoint: false
83
+ cache_dataset: false
84
+ student:
85
+ arch: vit_large
86
+ patch_size: 16
87
+ drop_path_rate: 0.3
88
+ layerscale: 1.0e-05
89
+ pretrained_weights: ''
90
+ ffn_layer: "mlp"
91
+ ffn_ratio: 4.0
92
+ resume_from_teacher_chkpt: ""
93
+ qkv_bias: true
94
+ proj_bias: true
95
+ ffn_bias: true
96
+ norm_layer: "layernorm"
97
+ n_storage_tokens: 0
98
+ mask_k_bias: false
99
+ untie_cls_and_patch_norms: false # If true, use separate norms for CLS/reg and patch/mask tokens
100
+ untie_global_and_local_cls_norm: false # If true, use separate norms for local and global crop CLS token during training
101
+ in_chans: 3
102
+ pos_embed_type: rope
103
+ pos_embed_rope_base: 100.0
104
+ pos_embed_rope_min_period: null
105
+ pos_embed_rope_max_period: null
106
+ pos_embed_rope_normalize_coords: separate # min, max, separate
107
+ pos_embed_rope_shift_coords: null
108
+ pos_embed_rope_jitter_coords: null
109
+ pos_embed_rope_rescale_coords: null
110
+ pos_embed_rope_dtype: bf16
111
+ fp8_enabled: False # Convert Linear layers to operate in fp8 precision
112
+ fp8_filter: "blocks" # Regex that must appear in module path; empty means everything
113
+ teacher:
114
+ momentum_teacher: 0.992
115
+ final_momentum_teacher: 1
116
+ warmup_teacher_temp: 0.04
117
+ teacher_temp: 0.07
118
+ warmup_teacher_temp_epochs: 30
119
+ in_chans: 3
120
+ distillation: # teacher
121
+ enabled: false
122
+ full_cfg_path: ""
123
+ checkpoint_path: ""
124
+ multidistillation:
125
+ enabled: false
126
+ hrft: # non-hrft'd student
127
+ enabled: false
128
+ checkpoint_path: "" # teacher_checkpoint path
129
+ optim:
130
+ epochs: 100
131
+ optimizer: adamw
132
+ weight_decay: 0.04
133
+ weight_decay_end: 0.4
134
+ lr: 0.001
135
+ warmup_epochs: 10
136
+ min_lr: 1.0e-06
137
+ schedule_trunc_extra: 0.0 # Compute the schedule for (1 + schedule_trunc_extra) steps and truncate, .25 is a good choice
138
+ clip_grad: 3.0
139
+ freeze_last_layer_epochs: 1
140
+ scaling_rule: sqrt_wrt_1024
141
+ patch_embed_lr_mult: 0.2
142
+ dino_head_wd_multiplier: 1.0
143
+ layerwise_decay: 0.9
144
+ multi_tensor_optim: true
145
+ dump_fsdp_weights_path: ""
146
+ adamw_beta1: 0.9
147
+ adamw_beta2: 0.999
148
+ crops:
149
+ global_crops_scale:
150
+ - 0.32
151
+ - 1.0
152
+ local_crops_number: 8
153
+ local_crops_scale:
154
+ - 0.05
155
+ - 0.32
156
+ global_crops_size: 224
157
+ local_crops_size: 96
158
+ global_local_crop_pairs_ratios: 1.0
159
+ gram_teacher_crops_size: null # If not None, return crops for gram teacher
160
+ localcrops_subset_of_globalcrops: false
161
+ share_color_jitter: false
162
+ horizontal_flips: true
163
+ gram_teacher_no_distortions: false # If True, no distortions are applied to gram teacher crops
164
+ rgb_mean:
165
+ - 0.485
166
+ - 0.456
167
+ - 0.406
168
+ rgb_std:
169
+ - 0.229
170
+ - 0.224
171
+ - 0.225
172
+ evaluation:
173
+ eval_period_iterations: 12500
174
+ low_freq_every: 5
175
+ config_files: # Must be in fairvit/eval/configs
176
+ high_freq: benchmark_high_frequency.yaml # More often
177
+ low_freq: benchmark_low_frequency.yaml # Less often
178
+ checkpointing:
179
+ period: 3750
180
+ max_to_keep: 3
181
+ keep_every: 99999999999999999 # Save a checkpoint every N iterations, regardless of max_to_keep and period
182
+
183
+ # Example of constant schedules with schedules v2
184
+ # # schedules:
185
+ # # lr:
186
+ # # start: 0.0
187
+ # # peak: 1e-3
188
+ # # end: 1e-6
189
+ # # warmup_epochs: 10
190
+ # # freeze_last_layer_epochs: 1
191
+ # # weight_decay:
192
+ # # start: 0.04
193
+ # # peak: 0.04
194
+ # # end: 0.04
195
+ # # warmup_epochs: 0
196
+ # # momentum:
197
+ # # start: 0.992
198
+ # # peak: 0.992
199
+ # # end: 0.992
200
+ # # warmup_epochs: 0
201
+ # # teacher_temp:
202
+ # # start: 0.04
203
+ # # peak: 0.07
204
+ # # end: 0.07
205
+ # # warmup_epochs: 30
dinov3/configs/train/dinov3_vit7b16_gram_anchor.yaml ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL:
2
+ META_ARCHITECTURE: SSLMetaArch
3
+ DEVICE: cuda
4
+ WEIGHTS: ''
5
+ DTYPE: float32
6
+ compute_precision:
7
+ param_dtype: bf16
8
+ reduce_dtype: fp32
9
+ sharding_strategy: SHARD_GRAD_OP
10
+ dino:
11
+ loss_weight: 1.0
12
+ global_ignore_diagonal: true
13
+ head_n_prototypes: 262144
14
+ head_bottleneck_dim: 512
15
+ head_norm_last_layer: false
16
+ head_nlayers: 3
17
+ head_hidden_dim: 8192
18
+ koleo_loss_weight: 0.1
19
+ koleo_loss_distributed: false
20
+ koleo_topk: 1
21
+ koleo_distributed_replicas: 0
22
+ koleo_distributed_loss_group_size: null
23
+ koleo_distributed_loss_group_data: true
24
+ force_weight_norm: false
25
+ reweight_dino_local_loss: true
26
+ local_loss_weight_schedule:
27
+ start: 1
28
+ peak: 1
29
+ end: 0.5
30
+ warmup_epochs: 1000
31
+ cosine_epochs: 1
32
+ ibot:
33
+ loss_weight: 1.0
34
+ mask_sample_probability: 0.5
35
+ mask_ratio_min_max:
36
+ - 0.1
37
+ - 0.5
38
+ mask_random_circular_shift: false
39
+ force_masking_even_with_zero_weight: false
40
+ separate_head: true
41
+ head_n_prototypes: 98304
42
+ head_bottleneck_dim: 384
43
+ head_norm_last_layer: false
44
+ head_nlayers: 3
45
+ head_hidden_dim: 4096
46
+ gram:
47
+ use_loss: true
48
+ compute_stats: false
49
+ loss_weight: 1.0
50
+ ema_teacher: false
51
+ ckpt: ignore
52
+ it_load_ema_teacher: -1
53
+ rep_update: true
54
+ update_frequency: 10000
55
+ it_first_update: 1010000
56
+ max_updates: 3
57
+ normalized: true
58
+ img_level: true
59
+ remove_neg: false
60
+ remove_only_teacher_neg: false
61
+ tokens_used: all
62
+ global_teacher_resize_method: bicubic
63
+ global_teacher_resize_antialias: false
64
+ loss_weight_schedule:
65
+ start: 0
66
+ peak: 0
67
+ end: 2.0
68
+ warmup_epochs: 1000
69
+ cosine_epochs: 1
70
+ train:
71
+ batch_size_per_gpu: 16
72
+ dataset_path: null
73
+ saveckp_freq: 20
74
+ seed: 0
75
+ num_workers: 10
76
+ OFFICIAL_EPOCH_LENGTH: 1000
77
+ monitor_gradient_norm: false
78
+ chunk_schedule: []
79
+ cache_dataset: true
80
+ use_teacher_head: true
81
+ learn_from_teacher_tokens: false
82
+ centering: sinkhorn_knopp
83
+ checkpointing: true
84
+ checkpointing_full: true
85
+ compile: true
86
+ cudagraphs: false
87
+ cell_augmentation: false
88
+ cell_augmentation_type: hpa
89
+ sharded_eval_checkpoint: true
90
+ student:
91
+ arch: vit_7b
92
+ patch_size: 16
93
+ drop_path_rate: 0.4
94
+ layerscale: 1.0e-05
95
+ patch_drop: 0.0
96
+ pretrained_weights: ''
97
+ ffn_layer: swiglu64
98
+ ffn_ratio: 3
99
+ resume_from_teacher_chkpt: ''
100
+ qkv_bias: false
101
+ proj_bias: true
102
+ ffn_bias: true
103
+ norm_layer: layernormbf16
104
+ n_storage_tokens: 4
105
+ untie_cls_and_patch_norms: false
106
+ untie_global_and_local_cls_norm: true
107
+ mask_k_bias: true
108
+ in_chans: 3
109
+ pos_embed_type: rope
110
+ pos_embed_rope_base: 100
111
+ pos_embed_rope_min_period: null
112
+ pos_embed_rope_max_period: null
113
+ pos_embed_rope_normalize_coords: separate
114
+ pos_embed_rope_shift_coords: null
115
+ pos_embed_rope_jitter_coords: null
116
+ pos_embed_rope_rescale_coords: 2
117
+ pos_embed_rope_dtype: fp32
118
+ fp8_enabled: true
119
+ fp8_filter: blocks
120
+ teacher:
121
+ momentum_teacher: null
122
+ final_momentum_teacher: null
123
+ warmup_teacher_temp: null
124
+ teacher_temp: null
125
+ warmup_teacher_temp_epochs: null
126
+ in_chans: 3
127
+ distillation:
128
+ enabled: false
129
+ full_cfg_path: ''
130
+ checkpoint_path: ''
131
+ multidistillation:
132
+ enabled: false
133
+ hrft:
134
+ enabled: false
135
+ checkpoint_path: ''
136
+ optim:
137
+ epochs: 1200
138
+ optimizer: adamw
139
+ weight_decay: null
140
+ weight_decay_end: null
141
+ lr: null
142
+ warmup_epochs: null
143
+ min_lr: null
144
+ schedule_trunc_extra: null
145
+ clip_grad: 30.0
146
+ freeze_last_layer_epochs: null
147
+ scaling_rule: sqrt_wrt_1024
148
+ patch_embed_lr_mult: 0.2
149
+ dino_head_wd_multiplier: 1.0
150
+ layerwise_decay: 0.98
151
+ multi_tensor_optim: true
152
+ dump_fsdp_weights_path: ''
153
+ adamw_beta1: 0.9
154
+ adamw_beta2: 0.99
155
+ crops:
156
+ global_crops_scale:
157
+ - 0.32
158
+ - 1.0
159
+ local_crops_number: 8
160
+ local_crops_scale:
161
+ - 0.05
162
+ - 0.32
163
+ global_crops_size: 256
164
+ local_crops_size: 112
165
+ gram_teacher_crops_size: 512
166
+ localcrops_subset_of_globalcrops: false
167
+ share_color_jitter: false
168
+ horizontal_flips: false
169
+ gram_teacher_no_distortions: true
170
+ rgb_mean:
171
+ - 0.485
172
+ - 0.456
173
+ - 0.406
174
+ rgb_std:
175
+ - 0.229
176
+ - 0.224
177
+ - 0.225
178
+ checkpointing:
179
+ period: 1000
180
+ max_to_keep: 3
181
+ keep_every: 50000
182
+ schedules:
183
+ lr:
184
+ start: 0
185
+ peak: 3.0e-05
186
+ end: 3.0e-05
187
+ warmup_epochs: 100
188
+ freeze_last_layer_epochs: 5
189
+ weight_decay:
190
+ start: 0.04
191
+ peak: 0.04
192
+ end: 0.04
193
+ warmup_epochs: 0
194
+ teacher_temp:
195
+ start: 0.04
196
+ peak: 0.07
197
+ end: 0.07
198
+ warmup_epochs: 100
199
+ momentum:
200
+ start: 0.999
201
+ peak: 0.999
202
+ end: 0.999
203
+ warmup_epochs: 0
dinov3/configs/train/dinov3_vit7b16_high_res_adapt.yaml ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL:
2
+ META_ARCHITECTURE: SSLMetaArch
3
+ DEVICE: cuda
4
+ WEIGHTS: ''
5
+ DTYPE: float32
6
+ compute_precision:
7
+ param_dtype: bf16
8
+ reduce_dtype: fp32
9
+ sharding_strategy: SHARD_GRAD_OP
10
+ dino:
11
+ loss_weight: 1.0
12
+ global_ignore_diagonal: true
13
+ head_n_prototypes: 262144
14
+ head_bottleneck_dim: 512
15
+ head_norm_last_layer: false
16
+ head_nlayers: 3
17
+ head_hidden_dim: 8192
18
+ koleo_loss_weight: 0.1
19
+ koleo_loss_distributed: true
20
+ koleo_topk: 1
21
+ koleo_distributed_replicas: 0
22
+ koleo_distributed_loss_group_size: 16
23
+ force_weight_norm: false
24
+ reweight_dino_local_loss: true
25
+ local_loss_weight_schedule:
26
+ start: 0.5
27
+ peak: 0.5
28
+ end: 0.5
29
+ warmup_epochs: 0
30
+ cosine_epochs: 0
31
+ koleo_distributed_loss_group_data: true
32
+ ibot:
33
+ loss_weight: 1.0
34
+ mask_sample_probability: 0.5
35
+ mask_ratio_min_max:
36
+ - 0.1
37
+ - 0.5
38
+ mask_random_circular_shift: false
39
+ force_masking_even_with_zero_weight: false
40
+ separate_head: true
41
+ head_n_prototypes: 98304
42
+ head_bottleneck_dim: 384
43
+ head_norm_last_layer: false
44
+ head_nlayers: 3
45
+ head_hidden_dim: 4096
46
+ gram:
47
+ use_loss: true
48
+ compute_stats: false
49
+ loss_weight: 1.0
50
+ ema_teacher: false
51
+ it_load_ema_teacher: -1
52
+ rep_update: false
53
+ update_frequency: 10000
54
+ it_first_update: 1010000
55
+ max_updates: 3
56
+ normalized: true
57
+ img_level: true
58
+ remove_neg: false
59
+ remove_only_teacher_neg: false
60
+ tokens_used: all
61
+ global_teacher_resize_method: bicubic
62
+ global_teacher_resize_antialias: false
63
+ loss_weight_schedule:
64
+ start: 1.5
65
+ peak: 1.5
66
+ end: 1.5
67
+ warmup_epochs: 0
68
+ cosine_epochs: 0
69
+ train:
70
+ batch_size_per_gpu: 8
71
+ dataset_path: null
72
+ saveckp_freq: 20
73
+ seed: 0
74
+ num_workers: 2
75
+ OFFICIAL_EPOCH_LENGTH: 1000
76
+ monitor_gradient_norm: false
77
+ chunk_schedule: []
78
+ cache_dataset: true
79
+ use_teacher_head: true
80
+ learn_from_teacher_tokens: false
81
+ centering: sinkhorn_knopp
82
+ checkpointing: true
83
+ checkpointing_full: true
84
+ compile: true
85
+ cudagraphs: false
86
+ cell_augmentation: false
87
+ cell_augmentation_type: hpa
88
+ sharded_eval_checkpoint: true
89
+ student:
90
+ arch: vit_7b
91
+ patch_size: 16
92
+ drop_path_rate: 0.4
93
+ layerscale: 1.0e-05
94
+ patch_drop: 0.0
95
+ pretrained_weights: ''
96
+ ffn_layer: swiglu64
97
+ ffn_ratio: 3
98
+ resume_from_teacher_chkpt: ''
99
+ qkv_bias: false
100
+ proj_bias: true
101
+ ffn_bias: true
102
+ norm_layer: layernormbf16
103
+ n_storage_tokens: 4
104
+ untie_cls_and_patch_norms: false
105
+ untie_global_and_local_cls_norm: true
106
+ mask_k_bias: true
107
+ in_chans: 3
108
+ pos_embed_type: rope
109
+ pos_embed_rope_base: 100
110
+ pos_embed_rope_min_period: null
111
+ pos_embed_rope_max_period: null
112
+ pos_embed_rope_normalize_coords: separate
113
+ pos_embed_rope_shift_coords: null
114
+ pos_embed_rope_jitter_coords: null
115
+ pos_embed_rope_rescale_coords: 2
116
+ pos_embed_rope_dtype: fp32
117
+ fp8_enabled: true
118
+ fp8_filter: blocks
119
+ teacher:
120
+ momentum_teacher: null
121
+ final_momentum_teacher: null
122
+ warmup_teacher_temp: null
123
+ teacher_temp: null
124
+ warmup_teacher_temp_epochs: null
125
+ in_chans: 3
126
+ distillation:
127
+ enabled: false
128
+ full_cfg_path: ''
129
+ checkpoint_path: ''
130
+ multidistillation:
131
+ enabled: false
132
+ hrft:
133
+ enabled: false
134
+ checkpoint_path: ''
135
+ optim:
136
+ epochs: 30
137
+ optimizer: adamw
138
+ weight_decay: null
139
+ weight_decay_end: null
140
+ lr: null
141
+ warmup_epochs: null
142
+ min_lr: null
143
+ schedule_trunc_extra: null
144
+ clip_grad: 30.0
145
+ freeze_last_layer_epochs: null
146
+ scaling_rule: sqrt_wrt_1024
147
+ patch_embed_lr_mult: 0.2
148
+ dino_head_wd_multiplier: 1.0
149
+ layerwise_decay: 0.98
150
+ multi_tensor_optim: true
151
+ dump_fsdp_weights_path: ''
152
+ adamw_beta1: 0.9
153
+ adamw_beta2: 0.99
154
+ crops:
155
+ global_crops_scale:
156
+ - 0.32
157
+ - 1.0
158
+ local_crops_number: 8
159
+ local_crops_scale:
160
+ - 0.05
161
+ - 0.32
162
+ global_crops_size:
163
+ - 512
164
+ - 768
165
+ - 768
166
+ - 768
167
+ - 768
168
+ local_crops_size:
169
+ - 112
170
+ - 112
171
+ - 168
172
+ - 224
173
+ - 336
174
+ global_local_crop_pairs_ratios:
175
+ - 0.3
176
+ - 0.3
177
+ - 0.3
178
+ - 0.05
179
+ - 0.05
180
+ gram_teacher_crops_size:
181
+ - 768
182
+ - 1152
183
+ - 1152
184
+ - 1152
185
+ - 1152
186
+ localcrops_subset_of_globalcrops: false
187
+ share_color_jitter: false
188
+ horizontal_flips: false
189
+ gram_teacher_no_distortions: true
190
+ rgb_mean:
191
+ - 0.485
192
+ - 0.456
193
+ - 0.406
194
+ rgb_std:
195
+ - 0.229
196
+ - 0.224
197
+ - 0.225
198
+ checkpointing:
199
+ period: 250
200
+ max_to_keep: 3
201
+ keep_every: 50000
202
+ schedules:
203
+ lr:
204
+ start: 0
205
+ peak: 0
206
+ end: 1.25e-05
207
+ warmup_epochs: 0
208
+ freeze_last_layer_epochs: 0
209
+ cosine_epochs: 10
210
+ weight_decay:
211
+ start: 0.04
212
+ peak: 0.04
213
+ end: 0.04
214
+ warmup_epochs: 0
215
+ teacher_temp:
216
+ start: 0.07
217
+ peak: 0.07
218
+ end: 0.07
219
+ warmup_epochs: 0
220
+ momentum:
221
+ start: 0.999
222
+ peak: 0.999
223
+ end: 0.999
224
+ warmup_epochs: 0
dinov3/configs/train/dinov3_vit7b16_pretrain.yaml ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL:
2
+ META_ARCHITECTURE: SSLMetaArch
3
+ DEVICE: cuda
4
+ WEIGHTS: ''
5
+ DTYPE: float32
6
+ compute_precision:
7
+ param_dtype: bf16
8
+ reduce_dtype: fp32
9
+ sharding_strategy: SHARD_GRAD_OP
10
+ dino:
11
+ loss_weight: 1.0
12
+ global_ignore_diagonal: true
13
+ head_n_prototypes: 262144
14
+ head_bottleneck_dim: 512
15
+ head_norm_last_layer: false
16
+ head_nlayers: 3
17
+ head_hidden_dim: 8192
18
+ koleo_loss_weight: 0.1
19
+ koleo_loss_distributed: false
20
+ koleo_topk: 1
21
+ koleo_distributed_replicas: 0
22
+ koleo_distributed_loss_group_size: null
23
+ force_weight_norm: false
24
+ ibot:
25
+ loss_weight: 1.0
26
+ mask_sample_probability: 0.5
27
+ mask_ratio_min_max:
28
+ - 0.1
29
+ - 0.5
30
+ mask_random_circular_shift: false
31
+ force_masking_even_with_zero_weight: false
32
+ separate_head: true
33
+ head_n_prototypes: 98304
34
+ head_bottleneck_dim: 384
35
+ head_norm_last_layer: false
36
+ head_nlayers: 3
37
+ head_hidden_dim: 4096
38
+ gram:
39
+ use_loss: false
40
+ compute_stats: false
41
+ train:
42
+ batch_size_per_gpu: 16
43
+ dataset_path: null
44
+ saveckp_freq: 20
45
+ seed: 0
46
+ num_workers: 10
47
+ OFFICIAL_EPOCH_LENGTH: 1000
48
+ monitor_gradient_norm: false
49
+ chunk_schedule: []
50
+ cache_dataset: true
51
+ use_teacher_head: true
52
+ learn_from_teacher_tokens: false
53
+ centering: sinkhorn_knopp
54
+ checkpointing: true
55
+ checkpointing_full: false
56
+ compile: true
57
+ cudagraphs: false
58
+ cell_augmentation: false
59
+ cell_augmentation_type: hpa
60
+ sharded_eval_checkpoint: true
61
+ student:
62
+ arch: vit_7b
63
+ patch_size: 16
64
+ drop_path_rate: 0.4
65
+ layerscale: 1.0e-05
66
+ patch_drop: 0.0
67
+ pretrained_weights: ''
68
+ ffn_layer: swiglu64
69
+ ffn_ratio: 3
70
+ resume_from_teacher_chkpt: ''
71
+ qkv_bias: false
72
+ proj_bias: true
73
+ ffn_bias: true
74
+ norm_layer: layernormbf16
75
+ n_storage_tokens: 4
76
+ untie_cls_and_patch_norms: false
77
+ untie_global_and_local_cls_norm: true
78
+ mask_k_bias: true
79
+ in_chans: 3
80
+ pos_embed_type: rope
81
+ pos_embed_rope_base: 100
82
+ pos_embed_rope_min_period: null
83
+ pos_embed_rope_max_period: null
84
+ pos_embed_rope_normalize_coords: separate
85
+ pos_embed_rope_shift_coords: null
86
+ pos_embed_rope_jitter_coords: null
87
+ pos_embed_rope_rescale_coords: 2
88
+ pos_embed_rope_dtype: fp32
89
+ fp8_enabled: true
90
+ fp8_filter: blocks
91
+ teacher:
92
+ momentum_teacher: null
93
+ final_momentum_teacher: null
94
+ warmup_teacher_temp: null
95
+ teacher_temp: null
96
+ warmup_teacher_temp_epochs: null
97
+ in_chans: 3
98
+ distillation:
99
+ enabled: false
100
+ full_cfg_path: ''
101
+ checkpoint_path: ''
102
+ multidistillation:
103
+ enabled: false
104
+ hrft:
105
+ enabled: false
106
+ checkpoint_path: ''
107
+ optim:
108
+ epochs: 1000
109
+ optimizer: adamw
110
+ weight_decay: null
111
+ weight_decay_end: null
112
+ lr: null
113
+ warmup_epochs: null
114
+ min_lr: null
115
+ schedule_trunc_extra: null
116
+ clip_grad: 30.0
117
+ freeze_last_layer_epochs: null
118
+ scaling_rule: sqrt_wrt_1024
119
+ patch_embed_lr_mult: 0.2
120
+ dino_head_wd_multiplier: 1.0
121
+ layerwise_decay: 0.98
122
+ multi_tensor_optim: true
123
+ dump_fsdp_weights_path: ''
124
+ adamw_beta1: 0.9
125
+ adamw_beta2: 0.99
126
+ crops:
127
+ global_crops_scale:
128
+ - 0.32
129
+ - 1.0
130
+ local_crops_number: 8
131
+ local_crops_scale:
132
+ - 0.05
133
+ - 0.32
134
+ global_crops_size: 256
135
+ local_crops_size: 112
136
+ localcrops_subset_of_globalcrops: false
137
+ share_color_jitter: false
138
+ horizontal_flips: false
139
+ rgb_mean:
140
+ - 0.485
141
+ - 0.456
142
+ - 0.406
143
+ rgb_std:
144
+ - 0.229
145
+ - 0.224
146
+ - 0.225
147
+ checkpointing:
148
+ period: 1000
149
+ max_to_keep: 3
150
+ keep_every: 50000
151
+ schedules:
152
+ lr:
153
+ start: 0
154
+ peak: 5.0e-05
155
+ end: 5.0e-05
156
+ warmup_epochs: 100
157
+ freeze_last_layer_epochs: 5
158
+ weight_decay:
159
+ start: 0.04
160
+ peak: 0.04
161
+ end: 0.04
162
+ warmup_epochs: 0
163
+ teacher_temp:
164
+ start: 0.04
165
+ peak: 0.07
166
+ end: 0.07
167
+ warmup_epochs: 100
168
+ momentum:
169
+ start: 0.994
170
+ peak: 0.994
171
+ end: 0.994
172
+ warmup_epochs: 0
dinov3/configs/train/dinov3_vitl16_lvd1689m_distilled.yaml ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL:
2
+ META_ARCHITECTURE: MultiDistillationMetaArch
3
+ DEVICE: cuda
4
+ WEIGHTS: ''
5
+ DTYPE: float32
6
+ compute_precision:
7
+ param_dtype: bf16
8
+ reduce_dtype: fp32
9
+ sharding_strategy: SHARD_GRAD_OP
10
+ dino:
11
+ loss_weight: 1.0
12
+ global_ignore_diagonal: true
13
+ head_n_prototypes: 262144
14
+ head_bottleneck_dim: 512
15
+ head_norm_last_layer: false
16
+ head_nlayers: 3
17
+ head_hidden_dim: 8192
18
+ koleo_loss_weight: 0.1
19
+ koleo_loss_distributed: false
20
+ koleo_topk: 1
21
+ koleo_distributed_replicas: 0
22
+ koleo_distributed_loss_group_size: null
23
+ koleo_distributed_loss_group_data: true
24
+ force_weight_norm: false
25
+ reweight_dino_local_loss: false
26
+ local_loss_weight_schedule:
27
+ start: 0.5
28
+ peak: 0.5
29
+ end: 0.5
30
+ warmup_epochs: 0
31
+ ibot:
32
+ loss_weight: 1.0
33
+ mask_sample_probability: 0.5
34
+ mask_ratio_min_max:
35
+ - 0.1
36
+ - 0.5
37
+ mask_random_circular_shift: false
38
+ force_masking_even_with_zero_weight: false
39
+ separate_head: true
40
+ head_n_prototypes: 98304
41
+ head_bottleneck_dim: 384
42
+ head_norm_last_layer: false
43
+ head_nlayers: 3
44
+ head_hidden_dim: 4096
45
+ coding_rate_loss:
46
+ use_cls_loss: false
47
+ cls_loss_weight: 0.2
48
+ use_masked_patches_loss: false
49
+ masked_patches_loss_weight: 0.1
50
+ epsilon: 8
51
+ gram:
52
+ use_loss: false
53
+ compute_stats: false
54
+ loss_weight: 1.0
55
+ ema_teacher: false
56
+ ckpt: null
57
+ it_load_ema_teacher: -1
58
+ rep_update: true
59
+ update_frequency: 50000
60
+ it_first_update: 0
61
+ max_updates: null
62
+ normalized: true
63
+ img_level: false
64
+ remove_neg: false
65
+ remove_only_teacher_neg: false
66
+ tokens_used: all
67
+ global_teacher_resize_method: bicubic
68
+ global_teacher_resize_antialias: false
69
+ loss_weight_schedule: null
70
+ train:
71
+ batch_size_per_gpu: 3
72
+ dataset_path: <TRAIN/DATASET>
73
+ output_dir: <OUTPUT/DIR>
74
+ saveckp_freq: 20
75
+ seed: 0
76
+ num_workers: 2
77
+ OFFICIAL_EPOCH_LENGTH: 1250
78
+ monitor_gradient_norm: false
79
+ chunk_schedule: []
80
+ cache_dataset: true
81
+ use_teacher_head: true
82
+ learn_from_teacher_tokens: false
83
+ centering: sinkhorn_knopp
84
+ checkpointing: true
85
+ checkpointing_full: true
86
+ compile: true
87
+ cudagraphs: false
88
+ cell_augmentation: false
89
+ cell_augmentation_type: hpa
90
+ sharded_eval_checkpoint: false
91
+ student:
92
+ arch: vit_large
93
+ patch_size: 16
94
+ drop_path_rate: 0.0
95
+ layerscale: 1.0e-05
96
+ drop_path_uniform: true
97
+ drop_path_shape: uniform
98
+ patch_drop: 0.0
99
+ pretrained_weights: ''
100
+ sin_cos_embeddings: false
101
+ fourier_embeddings: false
102
+ fourier_encoding_dim: 64
103
+ multiple_pos_embeddings: false
104
+ cls_pos_embedding: false
105
+ reg_pos_embedding: false
106
+ ffn_layer: mlp
107
+ ffn_ratio: 4.0
108
+ resume_from_teacher_chkpt: <PATH/TO/HRFT/TEACHER>
109
+ block_chunks: 0
110
+ qkv_bias: true
111
+ proj_bias: true
112
+ ffn_bias: true
113
+ norm_layer: layernormbf16
114
+ n_storage_tokens: 4
115
+ mask_attention: false
116
+ mask_register_attention: false
117
+ untie_cls_and_patch_norms: false
118
+ untie_global_and_local_cls_norm: false
119
+ interpolate_offset: 0.0
120
+ interpolate_antialias: true
121
+ mask_k_bias: true
122
+ init_std_cls: 0.02
123
+ init_std_reg: 0.02
124
+ rescale_weights_by_layer_id: false
125
+ in_chans: 3
126
+ pos_embed_grid_size: 48
127
+ pos_embed_type: ropenew
128
+ pos_embed_rope_gamma: 1.0
129
+ pos_embed_rope_init_multi_frequencies: false
130
+ pos_embed_rope_base: 100
131
+ pos_embed_rope_min_period: null
132
+ pos_embed_rope_max_period: null
133
+ pos_embed_rope_normalize_coords: separate
134
+ pos_embed_rope_shift_coords: null
135
+ pos_embed_rope_jitter_coords: null
136
+ pos_embed_rope_rescale_coords: 2
137
+ pos_embed_rope_dtype: bf16
138
+ sparse24_ranges: []
139
+ sparse24_filter:
140
+ - mlp
141
+ sparse24_default: false
142
+ fp8_enabled: false
143
+ fp8_filter: blocks
144
+ teacher:
145
+ momentum_teacher: 0.994
146
+ final_momentum_teacher: 1
147
+ warmup_teacher_temp: 0.04
148
+ teacher_temp: 0.07
149
+ warmup_teacher_temp_epochs: 120
150
+ in_chans: 3
151
+ distillation:
152
+ enabled: true
153
+ full_cfg_path: <PATH/TO/TEACHER/CONFIG/config.yaml>
154
+ checkpoint_path: <PATH/TO/TEACHER/checkpoint.pth>
155
+ multidistillation:
156
+ enabled: true
157
+ global_batch_size: 1920
158
+ students:
159
+ - name: vits_mlp4_4
160
+ config_path: <PATH/TO/STUDENT/CONFIG/vits_mlp4_4.yaml>
161
+ ranks_range:
162
+ - 0
163
+ - 48
164
+ - name: vitsp_swiglu6_1
165
+ config_path: <PATH/TO/STUDENT/CONFIG/vitsp_swiglu6_1.yaml>
166
+ ranks_range:
167
+ - 48
168
+ - 96
169
+ - name: vitb_mlp4_3
170
+ config_path: <PATH/TO/STUDENT/CONFIG/vitb_mlp4_3.yaml>
171
+ ranks_range:
172
+ - 96
173
+ - 176
174
+ - name: vitl_mlp4_1
175
+ config_path: <PATH/TO/STUDENT/CONFIG/vitl_mlp4_1.yaml>
176
+ ranks_range:
177
+ - 176
178
+ - 296
179
+ hrft:
180
+ enabled: false
181
+ checkpoint_path: ''
182
+ optim:
183
+ epochs: 20
184
+ optimizer: adamw
185
+ weight_decay: 0.04
186
+ weight_decay_end: 0.2
187
+ lr: 0.0002
188
+ warmup_epochs: 0
189
+ min_lr: 1.0e-06
190
+ schedule_trunc_extra: 0.0
191
+ clip_grad: 3.0
192
+ freeze_last_layer_epochs: 0
193
+ scaling_rule: sqrt_wrt_1024
194
+ patch_embed_lr_mult: 0.2
195
+ dino_head_wd_multiplier: 1.0
196
+ layerwise_decay: 0.99
197
+ multi_tensor_optim: true
198
+ dump_fsdp_weights_path: ''
199
+ adamw_beta1: 0.9
200
+ adamw_beta2: 0.999
201
+ crops:
202
+ global_crops_scale:
203
+ - 0.32
204
+ - 1.0
205
+ local_crops_number: 8
206
+ local_crops_scale:
207
+ - 0.05
208
+ - 0.32
209
+ global_crops_size: 256
210
+ local_crops_size: 112
211
+ global_local_crop_pairs_ratios: 1.0
212
+ gram_teacher_crops_size: 256
213
+ localcrops_subset_of_globalcrops: false
214
+ share_color_jitter: false
215
+ horizontal_flips: false
216
+ gram_teacher_no_distortions: false
217
+ rgb_mean:
218
+ - 0.485
219
+ - 0.456
220
+ - 0.406
221
+ rgb_std:
222
+ - 0.229
223
+ - 0.224
224
+ - 0.225
225
+ checkpointing:
226
+ period: 3750
227
+ max_to_keep: 3
228
+ keep_every: 99999999999999999
229
+ schedules:
230
+ weight_decay:
231
+ start: 0.04
232
+ peak: 0.04
233
+ end: 0.04
234
+ warmup_epochs: 0
235
+ teacher_temp:
236
+ start: 0.04
237
+ peak: 0.07
238
+ end: 0.07
239
+ warmup_epochs: 0
240
+ lr:
241
+ start: 0
242
+ peak: 0
243
+ end: 5.0e-05
244
+ warmup_epochs: 0
245
+ freeze_last_layer_epochs: 0
246
+ cosine_epochs: 10
247
+ momentum:
248
+ start: 0.994
249
+ peak: 0.994
250
+ end: 1.0
251
+ warmup_epochs: 0
dinov3/configs/train/multi_distillation_test.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL:
2
+ META_ARCHITECTURE: MultiDistillationMetaArch
3
+ multidistillation:
4
+ enabled: true
5
+ global_batch_size: 256
6
+ students:
7
+ - name: vits
8
+ config_path: dinov3/configs/train/multidist_tests/vits_p16.yaml
9
+ ranks_range:
10
+ - 0
11
+ - 4
12
+ - name: vitb
13
+ config_path: dinov3/configs/train/multidist_tests/vitb_p16.yaml
14
+ ranks_range:
15
+ - 4
16
+ - 8
17
+ distillation: # teacher
18
+ enabled: true
19
+ full_cfg_path: dinov3/configs/train/vitl_im1k_lin834.yaml
20
+ checkpoint_path: ignore
21
+ train:
22
+ dataset_path: ImageNet:split=TRAIN
23
+ cache_dataset: false
24
+ centering: "sinkhorn_knopp"
25
+ compile: true
26
+ ibot:
27
+ separate_head: true
dinov3/configs/train/multidist_tests/vitb_p16.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # this corresponds to the default config
2
+ train:
3
+ dataset_path: ImageNet:split=TRAIN
4
+ checkpointing: true
5
+ student:
6
+ drop_path_rate: 0.1
7
+ arch: vit_base
dinov3/configs/train/multidist_tests/vits_p16.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # this corresponds to the default config
2
+ train:
3
+ dataset_path: ImageNet:split=TRAIN
4
+ student:
5
+ drop_path_rate: 0.1
6
+ arch: vit_small
dinov3/configs/train/vitl_im1k_lin834.yaml ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tested on RSC: /checkpoint/dino/qas/rope/vitl16_im1k/
2
+ # gives 82.2 im1k-knn, 83.3 im1k-linear
3
+ # runs with a total batch size of 2048 (64/gpu, 4 nodes here)
4
+ # runs at 0.57s/iter
5
+ MODEL:
6
+ META_ARCHITECTURE: SSLMetaArch
7
+ DEVICE: cuda
8
+ WEIGHTS: ''
9
+ DTYPE: float32
10
+ compute_precision:
11
+ param_dtype: bf16
12
+ reduce_dtype: fp32
13
+ sharding_strategy: SHARD_GRAD_OP
14
+ dino:
15
+ loss_weight: 1.0
16
+ global_ignore_diagonal: true
17
+ head_n_prototypes: 65536
18
+ head_bottleneck_dim: 256
19
+ head_norm_last_layer: false
20
+ head_nlayers: 3
21
+ head_hidden_dim: 2048
22
+ koleo_loss_weight: 0.1
23
+ koleo_loss_distributed: false
24
+ koleo_topk: 1
25
+ koleo_distributed_replicas: 0
26
+ force_weight_norm: false
27
+ ibot:
28
+ loss_weight: 1.0
29
+ mask_sample_probability: 0.5
30
+ mask_ratio_min_max:
31
+ - 0.1
32
+ - 0.5
33
+ mask_random_circular_shift: false
34
+ force_masking_even_with_zero_weight: false
35
+ separate_head: true
36
+ head_n_prototypes: 65536
37
+ head_bottleneck_dim: 256
38
+ head_norm_last_layer: false
39
+ head_nlayers: 3
40
+ head_hidden_dim: 2048
41
+ train:
42
+ batch_size_per_gpu: 64
43
+ dataset_path: ImageNet:split=TRAIN
44
+ output_dir: /checkpoint/dino/qas/rope/vitl16_im1k
45
+ saveckp_freq: 20
46
+ seed: 0
47
+ num_workers: 10
48
+ OFFICIAL_EPOCH_LENGTH: 1250
49
+ monitor_gradient_norm: false
50
+ chunk_schedule: []
51
+ cache_dataset: true
52
+ use_teacher_head: true
53
+ learn_from_teacher_tokens: false
54
+ centering: sinkhorn_knopp
55
+ checkpointing: false
56
+ compile: true
57
+ cudagraphs: false
58
+ cell_augmentation: false
59
+ cell_augmentation_type: hpa
60
+ student:
61
+ arch: vit_large
62
+ patch_size: 16
63
+ drop_path_rate: 0.3
64
+ layerscale: 1.0e-05
65
+ patch_drop: 0.0
66
+ pretrained_weights: ''
67
+ ffn_layer: mlp
68
+ ffn_ratio: 4.0
69
+ resume_from_teacher_chkpt: ''
70
+ qkv_bias: true
71
+ proj_bias: true
72
+ ffn_bias: true
73
+ norm_layer: layernorm
74
+ n_storage_tokens: 0
75
+ mask_k_bias: false
76
+ in_chans: 3
77
+ pos_embed_type: rope
78
+ pos_embed_rope_base: 100.0
79
+ pos_embed_rope_min_period: null
80
+ pos_embed_rope_max_period: null
81
+ pos_embed_rope_normalize_coords: separate # min, max, separate
82
+ pos_embed_rope_shift_coords: null
83
+ pos_embed_rope_jitter_coords: null
84
+ pos_embed_rope_rescale_coords: null
85
+ pos_embed_rope_dtype: bf16
86
+ fp8_enabled: False # Convert Linear layers to operate in fp8 precision
87
+ fp8_filter: "blocks" # Regex that must appear in module path; empty means everything
88
+ teacher:
89
+ momentum_teacher: 0.992
90
+ final_momentum_teacher: 1
91
+ warmup_teacher_temp: 0.04
92
+ teacher_temp: 0.07
93
+ warmup_teacher_temp_epochs: 30
94
+ in_chans: 3
95
+ distillation:
96
+ enabled: false
97
+ full_cfg_path: ''
98
+ checkpoint_path: ''
99
+ multidistillation:
100
+ enabled: false
101
+ hrft:
102
+ enabled: false
103
+ checkpoint_path: ''
104
+ optim:
105
+ epochs: 100
106
+ optimizer: adamw
107
+ weight_decay: 0.04
108
+ weight_decay_end: 0.4
109
+ lr: 0.001
110
+ warmup_epochs: 10
111
+ min_lr: 1.0e-06
112
+ clip_grad: 3.0
113
+ freeze_last_layer_epochs: 1
114
+ scaling_rule: sqrt_wrt_1024
115
+ patch_embed_lr_mult: 0.2
116
+ dino_head_wd_multiplier: 1.0
117
+ layerwise_decay: 0.9
118
+ multi_tensor_optim: true
119
+ dump_fsdp_weights_path: ''
120
+ adamw_beta1: 0.9
121
+ adamw_beta2: 0.999
122
+ crops:
123
+ global_crops_scale:
124
+ - 0.32
125
+ - 1.0
126
+ local_crops_number: 8
127
+ local_crops_scale:
128
+ - 0.05
129
+ - 0.32
130
+ global_crops_size: 224
131
+ local_crops_size: 96
132
+ localcrops_subset_of_globalcrops: false
133
+ share_color_jitter: false
134
+ horizontal_flips: true
135
+ evaluation:
136
+ eval_period_iterations: 12500
137
+ low_freq_every: 5
138
+ config_files:
139
+ high_freq: benchmark_high_frequency.yaml
140
+ low_freq: benchmark_low_frequency.yaml
141
+ checkpointing:
142
+ period: 3750
143
+ max_to_keep: 3
dinov3/data/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from .adapters import DatasetWithEnumeratedTargets
7
+ from .augmentations import DataAugmentationDINO
8
+ from .collate import collate_data_and_cast
9
+ from .loaders import SamplerType, make_data_loader, make_dataset
10
+ from .meta_loaders import CombinedDataLoader
11
+ from .masking import MaskingGenerator
12
+ from .transforms import make_classification_eval_transform, make_classification_train_transform
dinov3/data/adapters.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from typing import Any, Optional, Tuple
7
+
8
+ from torch.utils.data import Dataset
9
+
10
+
11
+ def extend_samples_with_index(dataset_class):
12
+ class DatasetWithIndex(dataset_class):
13
+ def __init__(self, **kwargs) -> None:
14
+ root = dataset_class.get_root()
15
+ super().__init__(root=root, **kwargs)
16
+
17
+ def __getitem__(self, index: int):
18
+ image, target = super().__getitem__(index)
19
+ return image, target, index
20
+
21
+ return DatasetWithIndex
22
+
23
+
24
+ class DatasetWithEnumeratedTargets(Dataset):
25
+ """
26
+ If pad_dataset is set, pads based on torch's DistributedSampler implementation, which
27
+ with drop_last=False pads the last batch to be a multiple of the world size.
28
+ https://github.com/pytorch/pytorch/blob/main/torch/utils/data/distributed.py#L91
29
+ """
30
+
31
+ def __init__(self, dataset: Dataset, pad_dataset: bool = False, num_replicas: Optional[int] = None):
32
+ self._dataset = dataset
33
+ self._size = len(self._dataset)
34
+ self._padded_size = self._size
35
+ self._pad_dataset = pad_dataset
36
+ if self._pad_dataset:
37
+ assert num_replicas is not None, "num_replicas should be set if pad_dataset is True"
38
+ self._padded_size = num_replicas * ((len(dataset) + num_replicas - 1) // num_replicas)
39
+
40
+ def get_image_relpath(self, index: int) -> str:
41
+ assert self._pad_dataset or index < self._size
42
+ return self._dataset.get_image_relpath(index % self._size)
43
+
44
+ def get_image_data(self, index: int) -> bytes:
45
+ assert self._pad_dataset or index < self._size
46
+ return self._dataset.get_image_data(index % self._size)
47
+
48
+ def get_target(self, index: int) -> Tuple[Any, int]:
49
+ target = self._dataset.get_target(index % self._size)
50
+ if index >= self._size:
51
+ assert self._pad_dataset
52
+ return (-1, target)
53
+ return (index, target)
54
+
55
+ def get_sample_decoder(self, index: int) -> Any:
56
+ assert self._pad_dataset or index < self._size
57
+ return self._dataset.get_sample_decoder(index % self._size)
58
+
59
+ def __getitem__(self, index: int) -> Tuple[Any, Tuple[Any, int]]:
60
+ image, target = self._dataset[index % self._size]
61
+ if index >= self._size:
62
+ assert self._pad_dataset
63
+ return image, (-1, target)
64
+ target = index if target is None else target
65
+ return image, (index, target)
66
+
67
+ def __len__(self) -> int:
68
+ return self._padded_size
dinov3/data/augmentations.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+
8
+ import numpy as np
9
+ import torch
10
+ from torch import nn
11
+ from torchvision.transforms import v2
12
+
13
+ from dinov3.data.transforms import IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD, GaussianBlur, make_normalize_transform
14
+
15
+ logger = logging.getLogger("dinov3")
16
+
17
+
18
+ class DataAugmentationDINO(object):
19
+ def __init__(
20
+ self,
21
+ global_crops_scale,
22
+ local_crops_scale,
23
+ local_crops_number,
24
+ global_crops_size=224,
25
+ local_crops_size=96,
26
+ gram_teacher_crops_size=None,
27
+ gram_teacher_no_distortions=False,
28
+ teacher_no_color_jitter=False,
29
+ local_crops_subset_of_global_crops=False,
30
+ patch_size=16,
31
+ share_color_jitter=False,
32
+ horizontal_flips=True,
33
+ mean=IMAGENET_DEFAULT_MEAN,
34
+ std=IMAGENET_DEFAULT_STD,
35
+ ):
36
+ self.global_crops_scale = global_crops_scale
37
+ self.local_crops_scale = local_crops_scale
38
+ self.local_crops_number = local_crops_number
39
+ self.global_crops_size = global_crops_size
40
+ self.local_crops_size = local_crops_size
41
+ self.gram_teacher_crops_size = gram_teacher_crops_size
42
+ self.gram_teacher_no_distortions = gram_teacher_no_distortions
43
+ self.teacher_no_color_jitter = teacher_no_color_jitter
44
+ self.local_crops_subset_of_global_crops = local_crops_subset_of_global_crops
45
+ self.patch_size = patch_size
46
+ self.share_color_jitter = share_color_jitter
47
+ self.mean = mean
48
+ self.std = std
49
+
50
+ logger.info("###################################")
51
+ logger.info("Using data augmentation parameters:")
52
+ logger.info(f"global_crops_scale: {global_crops_scale}")
53
+ logger.info(f"local_crops_scale: {local_crops_scale}")
54
+ logger.info(f"local_crops_number: {local_crops_number}")
55
+ logger.info(f"global_crops_size: {global_crops_size}")
56
+ logger.info(f"local_crops_size: {local_crops_size}")
57
+ logger.info(f"gram_crops_size: {gram_teacher_crops_size}")
58
+ logger.info(f"gram_teacher_no_distortions: {gram_teacher_no_distortions}")
59
+ logger.info(f"teacher_no_color_jitter: {teacher_no_color_jitter}")
60
+ logger.info(f"local_crops_subset_of_global_crops: {local_crops_subset_of_global_crops}")
61
+ logger.info(f"patch_size if local_crops_subset_of_global_crops: {patch_size}")
62
+ logger.info(f"share_color_jitter: {share_color_jitter}")
63
+ logger.info(f"horizontal flips: {horizontal_flips}")
64
+ logger.info("###################################")
65
+
66
+ # Global crops and gram teacher crops can have different sizes. We first take a crop of the maximum size
67
+ # and then resize it to the desired size for global and gram teacher crops.
68
+ global_crop_max_size = max(global_crops_size, gram_teacher_crops_size if gram_teacher_crops_size else 0)
69
+
70
+ # random resized crop and flip
71
+ self.geometric_augmentation_global = v2.Compose(
72
+ [
73
+ v2.RandomResizedCrop(
74
+ global_crop_max_size,
75
+ scale=global_crops_scale,
76
+ interpolation=v2.InterpolationMode.BICUBIC,
77
+ ),
78
+ v2.RandomHorizontalFlip(p=0.5 if horizontal_flips else 0.0),
79
+ ]
80
+ )
81
+
82
+ resize_global = nn.Identity() # Resize transform applied to global crops after random crop
83
+ self.resize_global_post_transf = (
84
+ nn.Identity()
85
+ ) # Resize transform applied to global crops after all other transforms
86
+ self.resize_gram_teacher = None # Resize transform applied to crops for gram teacher
87
+ if gram_teacher_crops_size is not None:
88
+ # All resize transforms will do nothing if the crop size is already the desired size.
89
+ if gram_teacher_no_distortions:
90
+ # When there a no distortions for the gram teacher crop, we can resize before the distortions.
91
+ # This is the preferred order, because it keeps the image size for the augmentations consistent,
92
+ # which matters e.g. for GaussianBlur.
93
+ resize_global = v2.Resize(
94
+ global_crops_size,
95
+ interpolation=v2.InterpolationMode.BICUBIC,
96
+ )
97
+ else:
98
+ # When there a no distortions for the gram teacher crop, we need to resize after the distortions,
99
+ # because the distortions are shared between global and gram teacher crops.
100
+ self.resize_global_post_transf = v2.Resize(
101
+ global_crops_size,
102
+ interpolation=v2.InterpolationMode.BICUBIC,
103
+ )
104
+
105
+ self.resize_gram_teacher = v2.Resize(
106
+ gram_teacher_crops_size,
107
+ interpolation=v2.InterpolationMode.BICUBIC,
108
+ )
109
+
110
+ self.geometric_augmentation_local = v2.Compose(
111
+ [
112
+ v2.RandomResizedCrop(
113
+ local_crops_size,
114
+ scale=local_crops_scale,
115
+ interpolation=v2.InterpolationMode.BICUBIC,
116
+ ),
117
+ v2.RandomHorizontalFlip(p=0.5 if horizontal_flips else 0.0),
118
+ ]
119
+ )
120
+
121
+ # color distortions / blurring
122
+ color_jittering = v2.Compose(
123
+ [
124
+ v2.RandomApply(
125
+ [v2.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.2, hue=0.1)],
126
+ p=0.8,
127
+ ),
128
+ v2.RandomGrayscale(p=0.2),
129
+ ]
130
+ )
131
+
132
+ global_transfo1_extra = GaussianBlur(p=1.0)
133
+
134
+ global_transfo2_extra = v2.Compose(
135
+ [
136
+ GaussianBlur(p=0.1),
137
+ v2.RandomSolarize(threshold=128, p=0.2),
138
+ ]
139
+ )
140
+
141
+ local_transfo_extra = GaussianBlur(p=0.5)
142
+
143
+ # normalization
144
+ self.normalize = v2.Compose(
145
+ [
146
+ v2.ToImage(),
147
+ v2.ToDtype(torch.float32, scale=True),
148
+ make_normalize_transform(mean=mean, std=std),
149
+ ]
150
+ )
151
+
152
+ if self.share_color_jitter:
153
+ self.color_jittering = color_jittering
154
+ self.global_transfo1 = v2.Compose([resize_global, global_transfo1_extra, self.normalize])
155
+ self.global_transfo2 = v2.Compose([resize_global, global_transfo2_extra, self.normalize])
156
+ self.local_transfo = v2.Compose([local_transfo_extra, self.normalize])
157
+ else:
158
+ self.global_transfo1 = v2.Compose(
159
+ [resize_global, color_jittering, global_transfo1_extra, self.normalize]
160
+ )
161
+ self.global_transfo2 = v2.Compose(
162
+ [resize_global, color_jittering, global_transfo2_extra, self.normalize]
163
+ )
164
+ self.local_transfo = v2.Compose([color_jittering, local_transfo_extra, self.normalize])
165
+
166
+ def __call__(self, image):
167
+ output = {}
168
+ output["weak_flag"] = True # some residual from mugs
169
+
170
+ if self.share_color_jitter:
171
+ image = self.color_jittering(image)
172
+
173
+ # global crops:
174
+ im1_base = self.geometric_augmentation_global(image)
175
+ global_crop_1_transf = self.global_transfo1(im1_base)
176
+ global_crop_1 = self.resize_global_post_transf(global_crop_1_transf)
177
+
178
+ im2_base = self.geometric_augmentation_global(image)
179
+ global_crop_2_transf = self.global_transfo2(im2_base)
180
+ global_crop_2 = self.resize_global_post_transf(global_crop_2_transf)
181
+
182
+ output["global_crops"] = [global_crop_1, global_crop_2]
183
+
184
+ # global crops for teacher:
185
+ if self.teacher_no_color_jitter:
186
+ output["global_crops_teacher"] = [
187
+ self.normalize(im1_base),
188
+ self.normalize(im2_base),
189
+ ]
190
+ else:
191
+ output["global_crops_teacher"] = [global_crop_1, global_crop_2]
192
+
193
+ if self.gram_teacher_crops_size is not None:
194
+ # crops for gram teacher:
195
+ if self.gram_teacher_no_distortions:
196
+ gram_crop_1 = self.normalize(self.resize_gram_teacher(im1_base))
197
+ gram_crop_2 = self.normalize(self.resize_gram_teacher(im2_base))
198
+ else:
199
+ gram_crop_1 = self.resize_gram_teacher(global_crop_1_transf)
200
+ gram_crop_2 = self.resize_gram_teacher(global_crop_2_transf)
201
+ output["gram_teacher_crops"] = [gram_crop_1, gram_crop_2]
202
+
203
+ # local crops:
204
+ if self.local_crops_subset_of_global_crops:
205
+ _local_crops = [self.local_transfo(im1_base) for _ in range(self.local_crops_number // 2)] + [
206
+ self.local_transfo(im2_base) for _ in range(self.local_crops_number // 2)
207
+ ]
208
+
209
+ local_crops = []
210
+ offsets = []
211
+ gs = self.global_crops_size
212
+ ls = self.local_crops_size
213
+ for img in _local_crops:
214
+ rx, ry = np.random.randint(0, (gs - ls) // self.patch_size, 2) * self.patch_size
215
+ local_crops.append(img[:, rx : rx + ls, ry : ry + ls])
216
+ offsets.append((rx, ry))
217
+
218
+ output["local_crops"] = local_crops
219
+ output["offsets"] = offsets
220
+ else:
221
+ local_crops = [
222
+ self.local_transfo(self.geometric_augmentation_local(image)) for _ in range(self.local_crops_number)
223
+ ]
224
+ output["local_crops"] = local_crops
225
+ output["offsets"] = ()
226
+
227
+ return output
dinov3/data/collate.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import random
7
+
8
+ import torch
9
+
10
+
11
+ def collate_data_and_cast(
12
+ samples_list,
13
+ mask_ratio_tuple,
14
+ mask_probability,
15
+ dtype,
16
+ n_tokens=None,
17
+ mask_generator=None,
18
+ random_circular_shift=False,
19
+ local_batch_size=None,
20
+ ):
21
+ n_global_crops = len(samples_list[0][0]["global_crops"])
22
+ n_local_crops = len(samples_list[0][0]["local_crops"])
23
+
24
+ collated_global_crops = torch.stack(
25
+ [s[0]["global_crops"][i] for i in range(n_global_crops) for s in samples_list]
26
+ ) # [n_global_crops, B, ...]
27
+ collated_local_crops = torch.stack([s[0]["local_crops"][i] for i in range(n_local_crops) for s in samples_list])
28
+ if "gram_teacher_crops" in samples_list[0][0]:
29
+ collated_gram_teacher_crops = torch.stack(
30
+ [s[0]["gram_teacher_crops"][i] for i in range(n_global_crops) for s in samples_list]
31
+ ) # [n_global_crops, B, ...]
32
+ else:
33
+ collated_gram_teacher_crops = None
34
+
35
+ if local_batch_size is not None:
36
+ # multi-distillation case, number of masks is different because the number of samples masked
37
+ # is different of the number of samples passed into the teacher initially
38
+ B = n_global_crops * local_batch_size
39
+ else:
40
+ B = len(collated_global_crops)
41
+ N = n_tokens
42
+ n_samples_masked = int(B * mask_probability)
43
+ probs = torch.linspace(*mask_ratio_tuple, n_samples_masked + 1)
44
+ upperbound = 0
45
+ masks_list = []
46
+ for i in range(0, n_samples_masked):
47
+ prob_max = probs[i + 1]
48
+ mask = torch.BoolTensor(mask_generator(int(N * prob_max)))
49
+ if random_circular_shift: # apply le random circular shift to
50
+ shift_x, shift_y = (
51
+ random.randint(0, mask.shape[0] - 1),
52
+ random.randint(0, mask.shape[1] - 1),
53
+ )
54
+ mask = torch.roll(mask, (shift_x, shift_y), (0, 1))
55
+ masks_list.append(mask)
56
+ upperbound += int(N * prob_max)
57
+ for _ in range(n_samples_masked, B):
58
+ masks_list.append(torch.BoolTensor(mask_generator(0)))
59
+
60
+ random.shuffle(masks_list)
61
+
62
+ collated_masks = torch.stack(masks_list).flatten(1)
63
+ mask_indices_list = collated_masks.flatten().nonzero().flatten()
64
+
65
+ masks_weight = (1 / collated_masks.sum(-1).clamp(min=1.0)).unsqueeze(-1).expand_as(collated_masks)[collated_masks]
66
+
67
+ out = {
68
+ "collated_global_crops": collated_global_crops.to(dtype),
69
+ "collated_local_crops": collated_local_crops.to(dtype),
70
+ "collated_masks": collated_masks,
71
+ "mask_indices_list": mask_indices_list,
72
+ "masks_weight": masks_weight,
73
+ "upperbound": upperbound,
74
+ "n_masked_patches": torch.full((1,), fill_value=mask_indices_list.shape[0], dtype=torch.long),
75
+ }
76
+ if collated_gram_teacher_crops is not None:
77
+ out["collated_gram_teacher_crops"] = collated_gram_teacher_crops.to(dtype)
78
+ return out
79
+
80
+
81
+ # def get_batch_subset(collated_data_batch, target_bs):
82
+ def get_batch_subset(collated_data_batch, divide_by):
83
+ old_bs = collated_data_batch["collated_global_crops"].shape[0] // 2
84
+ target_bs = (old_bs + divide_by - 1) // divide_by
85
+ collated_global_crops = (
86
+ collated_data_batch["collated_global_crops"].unflatten(0, (2, old_bs)).narrow(1, 0, target_bs).flatten(0, 1)
87
+ )
88
+ collated_local_crops = (
89
+ collated_data_batch["collated_local_crops"].unflatten(0, (-1, old_bs)).narrow(1, 0, target_bs).flatten(0, 1)
90
+ )
91
+
92
+ masks_old_bs = collated_data_batch["collated_masks"].shape[0] // 2
93
+ masks_target_bs = masks_old_bs // divide_by
94
+ collated_masks = (
95
+ collated_data_batch["collated_masks"]
96
+ .unflatten(0, (2, masks_old_bs))
97
+ .narrow(1, 0, masks_target_bs)
98
+ .flatten(0, 1)
99
+ )
100
+ mask_indices_list = collated_masks.flatten().nonzero().flatten()
101
+
102
+ while mask_indices_list.shape[0] == 0:
103
+ _unbind = list(collated_data_batch["collated_masks"].unbind(0))
104
+ random.shuffle(_unbind)
105
+ _bind = torch.stack(_unbind, dim=0)
106
+ collated_masks = _bind.unflatten(0, (2, masks_old_bs)).narrow(1, 0, masks_target_bs).flatten(0, 1)
107
+ mask_indices_list = collated_masks.flatten().nonzero().flatten()
108
+
109
+ masks_weight = (1 / collated_masks.sum(-1).clamp(min=1.0)).unsqueeze(-1).expand_as(collated_masks)[collated_masks]
110
+ upperbound = collated_data_batch["upperbound"]
111
+
112
+ new_batch = {
113
+ "collated_global_crops": collated_global_crops,
114
+ "collated_local_crops": collated_local_crops,
115
+ "collated_masks": collated_masks,
116
+ "mask_indices_list": mask_indices_list,
117
+ "masks_weight": masks_weight,
118
+ "upperbound": upperbound,
119
+ "n_masked_patches": torch.full((1,), fill_value=mask_indices_list.shape[0], dtype=torch.long),
120
+ }
121
+
122
+ if "global_batch_size" in collated_data_batch.keys():
123
+ new_batch["global_batch_size"] = collated_data_batch["global_batch_size"] // divide_by
124
+
125
+ return new_batch
dinov3/data/datasets/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from .ade20k import ADE20K
7
+ from .coco_captions import CocoCaptions
8
+ from .image_net import ImageNet
9
+ from .image_net_22k import ImageNet22k
dinov3/data/datasets/ade20k.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import os
7
+ from enum import Enum
8
+ from typing import Any, Callable, List, Optional, Tuple, Union
9
+
10
+ from PIL import Image
11
+
12
+ from .decoders import Decoder, DenseTargetDecoder, ImageDataDecoder
13
+ from .extended import ExtendedVisionDataset
14
+
15
+
16
+ class _Split(Enum):
17
+ TRAIN = "train"
18
+ VAL = "val"
19
+
20
+ @property
21
+ def dirname(self) -> str:
22
+ return {
23
+ _Split.TRAIN: "training",
24
+ _Split.VAL: "validation",
25
+ }[self]
26
+
27
+
28
+ def _file_to_segmentation_path(file_name: str, segm_base_path: str) -> str:
29
+ file_name_noext = os.path.splitext(file_name)[0]
30
+ return os.path.join(segm_base_path, file_name_noext + ".png")
31
+
32
+
33
+ def _load_segmentation(root: str, split_file_names: List[str]):
34
+ segm_base_path = "annotations"
35
+ segmentation_paths = [_file_to_segmentation_path(file_name, segm_base_path) for file_name in split_file_names]
36
+ return segmentation_paths
37
+
38
+
39
+ def _load_file_paths(root: str, split: _Split) -> Tuple[List[str], List[str]]:
40
+ with open(os.path.join(root, f"ADE20K_object150_{split.value}.txt")) as f:
41
+ split_file_names = sorted(f.read().strip().split("\n"))
42
+
43
+ all_segmentation_paths = _load_segmentation(root, split_file_names)
44
+ file_names = [os.path.join("images", el) for el in split_file_names]
45
+ return file_names, all_segmentation_paths
46
+
47
+
48
+ class ADE20K(ExtendedVisionDataset):
49
+ Split = Union[_Split]
50
+ Labels = Union[Image.Image]
51
+
52
+ def __init__(
53
+ self,
54
+ split: "ADE20K.Split",
55
+ root: Optional[str] = None,
56
+ transforms: Optional[Callable] = None,
57
+ transform: Optional[Callable] = None,
58
+ target_transform: Optional[Callable] = None,
59
+ image_decoder: Decoder = ImageDataDecoder,
60
+ target_decoder: Decoder = DenseTargetDecoder,
61
+ ) -> None:
62
+ super().__init__(
63
+ root=root,
64
+ transforms=transforms,
65
+ transform=transform,
66
+ target_transform=target_transform,
67
+ image_decoder=image_decoder,
68
+ target_decoder=target_decoder,
69
+ )
70
+
71
+ self.image_paths, self.target_paths = _load_file_paths(root, split)
72
+
73
+ def get_image_data(self, index: int) -> bytes:
74
+ image_relpath = self.image_paths[index]
75
+ image_full_path = os.path.join(self.root, image_relpath)
76
+ with open(image_full_path, mode="rb") as f:
77
+ image_data = f.read()
78
+ return image_data
79
+
80
+ def get_target(self, index: int) -> Any:
81
+ target_relpath = self.target_paths[index]
82
+ target_full_path = os.path.join(self.root, target_relpath)
83
+ with open(target_full_path, mode="rb") as f:
84
+ target_data = f.read()
85
+ return target_data
86
+
87
+ def __len__(self) -> int:
88
+ return len(self.image_paths)
dinov3/data/datasets/coco_captions.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import json
7
+ import os
8
+ import random
9
+ from enum import Enum
10
+ from typing import Callable, Dict, List, Optional, Union
11
+
12
+ from .decoders import ImageDataDecoder, TargetDecoder
13
+ from .extended import ExtendedVisionDataset
14
+
15
+ # Dataset: https://www.kaggle.com/datasets/nikhil7280/coco-image-caption
16
+
17
+
18
+ class _Split(Enum):
19
+ TRAIN = "train"
20
+ VAL = "val"
21
+
22
+
23
+ def read_images_and_captions(root: str, split: _Split) -> List[Dict]:
24
+ image_dir = None
25
+ if _Split(split) == _Split.TRAIN:
26
+ annotations_full_path = os.path.join(
27
+ root, "annotations_trainval2014/annotations/captions_train2014.json"
28
+ )
29
+ image_dir = os.path.join(root, "train2014/train2014")
30
+ else:
31
+ annotations_full_path = os.path.join(
32
+ root, "annotations_trainval2017/annotations/captions_train2017.json"
33
+ )
34
+ image_dir = os.path.join(root, "val2017/val2017")
35
+ with open(annotations_full_path) as f:
36
+ all_annotations = json.load(f)
37
+ data = {}
38
+ for item in all_annotations["images"]:
39
+ id = item["id"]
40
+ data[id] = {
41
+ "id": None,
42
+ "image": os.path.join(image_dir, item["file_name"]),
43
+ "captions": [],
44
+ }
45
+ for item in all_annotations["annotations"]:
46
+ data[item["image_id"]]["id"] = item["image_id"]
47
+ data[item["image_id"]]["captions"].append(item["caption"])
48
+ return list(data.values())
49
+
50
+
51
+ class CocoCaptions(ExtendedVisionDataset):
52
+ Split = Union[_Split]
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ split: "CocoCaptions.Split",
58
+ root: Optional[str] = None,
59
+ transforms: Optional[Callable] = None,
60
+ transform: Optional[Callable] = None,
61
+ target_transform: Optional[Callable] = None,
62
+ ) -> None:
63
+ super().__init__(
64
+ root=root,
65
+ transforms=transforms,
66
+ transform=transform,
67
+ target_transform=target_transform,
68
+ image_decoder=ImageDataDecoder,
69
+ target_decoder=TargetDecoder,
70
+ )
71
+
72
+ self.image_captions = read_images_and_captions(root, split)
73
+
74
+ def get_image_relpath(self, index: int) -> str:
75
+ image_path = self.image_captions[index]["image"]
76
+ return image_path
77
+
78
+ def get_image_data(self, index: int) -> bytes:
79
+ image_path = self.get_image_relpath(index)
80
+ with open(image_path, mode="rb") as f:
81
+ image_data = f.read()
82
+ return image_data
83
+
84
+ def get_target(self, index: int) -> str:
85
+ return random.choice(self.image_captions[index]["captions"])
86
+
87
+ def __len__(self) -> int:
88
+ return len(self.image_captions)
dinov3/data/datasets/decoders.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from io import BytesIO
7
+ from typing import Any
8
+
9
+ from PIL import Image
10
+
11
+
12
+ class Decoder:
13
+ def decode(self) -> Any:
14
+ raise NotImplementedError
15
+
16
+
17
+ class ImageDataDecoder(Decoder):
18
+ def __init__(self, image_data: bytes) -> None:
19
+ self._image_data = image_data
20
+
21
+ def decode(self) -> Image:
22
+ f = BytesIO(self._image_data)
23
+ return Image.open(f).convert(mode="RGB")
24
+
25
+
26
+ class TargetDecoder(Decoder):
27
+ def __init__(self, target: Any):
28
+ self._target = target
29
+
30
+ def decode(self) -> Any:
31
+ return self._target
32
+
33
+
34
+ class DenseTargetDecoder(Decoder):
35
+ def __init__(self, image_data: bytes) -> None:
36
+ self._image_data = image_data
37
+
38
+ def decode(self) -> Image:
39
+ f = BytesIO(self._image_data)
40
+ return Image.open(f)
dinov3/data/datasets/extended.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from typing import Any, Tuple
7
+
8
+ from torchvision.datasets import VisionDataset
9
+
10
+ from .decoders import Decoder, ImageDataDecoder, TargetDecoder
11
+
12
+
13
+ class ExtendedVisionDataset(VisionDataset):
14
+ def __init__(
15
+ self,
16
+ image_decoder: Decoder = ImageDataDecoder,
17
+ target_decoder: Decoder = TargetDecoder,
18
+ *args,
19
+ **kwargs,
20
+ ) -> None:
21
+ super().__init__(*args, **kwargs) # type: ignore
22
+ self.image_decoder = image_decoder
23
+ self.target_decoder = target_decoder
24
+
25
+ def get_image_data(self, index: int) -> bytes:
26
+ raise NotImplementedError
27
+
28
+ def get_target(self, index: int) -> Any:
29
+ raise NotImplementedError
30
+
31
+ def __getitem__(self, index: int) -> Tuple[Any, Any]:
32
+ try:
33
+ image_data = self.get_image_data(index)
34
+ image = self.image_decoder(image_data).decode()
35
+ except Exception as e:
36
+ raise RuntimeError(f"can not read image for sample {index}") from e
37
+ target = self.get_target(index)
38
+ target = self.target_decoder(target).decode()
39
+
40
+ if self.transforms is not None:
41
+ image, target = self.transforms(image, target)
42
+
43
+ return image, target
44
+
45
+ def __len__(self) -> int:
46
+ raise NotImplementedError
dinov3/data/datasets/image_net.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import csv
7
+ import logging
8
+ import os
9
+ from enum import Enum
10
+ from typing import Callable, List, Optional, Tuple, Union
11
+
12
+ import numpy as np
13
+
14
+ from .decoders import ImageDataDecoder, TargetDecoder
15
+ from .extended import ExtendedVisionDataset
16
+
17
+ logger = logging.getLogger("dinov3")
18
+ _Target = int
19
+
20
+
21
+ class _Split(Enum):
22
+ TRAIN = "train"
23
+ VAL = "val"
24
+ TEST = "test" # NOTE: torchvision does not support the test split
25
+
26
+ @property
27
+ def length(self) -> int:
28
+ split_lengths = {
29
+ _Split.TRAIN: 1_281_167,
30
+ _Split.VAL: 50_000,
31
+ _Split.TEST: 100_000,
32
+ }
33
+ return split_lengths[self]
34
+
35
+ def get_dirname(self, class_id: Optional[str] = None) -> str:
36
+ return self.value if class_id is None else os.path.join(self.value, class_id)
37
+
38
+ def get_image_relpath(self, actual_index: int, class_id: Optional[str] = None) -> str:
39
+ dirname = self.get_dirname(class_id)
40
+ if self == _Split.TRAIN:
41
+ basename = f"{class_id}_{actual_index}"
42
+ else: # self in (_Split.VAL, _Split.TEST):
43
+ basename = f"ILSVRC2012_{self.value}_{actual_index:08d}"
44
+ return os.path.join(dirname, basename + ".JPEG")
45
+
46
+ def parse_image_relpath(self, image_relpath: str) -> Tuple[str, int]:
47
+ assert self != _Split.TEST
48
+ dirname, filename = os.path.split(image_relpath)
49
+ class_id = os.path.split(dirname)[-1]
50
+ basename, _ = os.path.splitext(filename)
51
+ actual_index = int(basename.split("_")[-1])
52
+ return class_id, actual_index
53
+
54
+
55
+ class ImageNet(ExtendedVisionDataset):
56
+ Target = Union[_Target]
57
+ Split = Union[_Split]
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ split: "ImageNet.Split",
63
+ root: str,
64
+ extra: str,
65
+ transforms: Optional[Callable] = None,
66
+ transform: Optional[Callable] = None,
67
+ target_transform: Optional[Callable] = None,
68
+ ) -> None:
69
+ super().__init__(
70
+ root=root,
71
+ transforms=transforms,
72
+ transform=transform,
73
+ target_transform=target_transform,
74
+ image_decoder=ImageDataDecoder,
75
+ target_decoder=TargetDecoder,
76
+ )
77
+ self._extra_root = extra
78
+ self._split = split
79
+
80
+ self._entries = None
81
+ self._class_ids = None
82
+ self._class_names = None
83
+
84
+ @property
85
+ def split(self) -> "ImageNet.Split":
86
+ return self._split
87
+
88
+ def _get_extra_full_path(self, extra_path: str) -> str:
89
+ return os.path.join(self._extra_root, extra_path)
90
+
91
+ def _load_extra(self, extra_path: str) -> np.ndarray:
92
+ extra_full_path = self._get_extra_full_path(extra_path)
93
+ return np.load(extra_full_path, mmap_mode="r")
94
+
95
+ def _save_extra(self, extra_array: np.ndarray, extra_path: str) -> None:
96
+ extra_full_path = self._get_extra_full_path(extra_path)
97
+ os.makedirs(self._extra_root, exist_ok=True)
98
+ np.save(extra_full_path, extra_array)
99
+
100
+ @property
101
+ def _entries_path(self) -> str:
102
+ return f"entries-{self._split.value.upper()}.npy"
103
+
104
+ @property
105
+ def _class_ids_path(self) -> str:
106
+ return f"class-ids-{self._split.value.upper()}.npy"
107
+
108
+ @property
109
+ def _class_names_path(self) -> str:
110
+ return f"class-names-{self._split.value.upper()}.npy"
111
+
112
+ def _get_entries(self) -> np.ndarray:
113
+ if self._entries is None:
114
+ self._entries = self._load_extra(self._entries_path)
115
+ assert self._entries is not None
116
+ return self._entries
117
+
118
+ def _get_class_ids(self) -> np.ndarray:
119
+ if self._split == _Split.TEST:
120
+ raise AssertionError("Class IDs are not available in TEST split")
121
+ if self._class_ids is None:
122
+ self._class_ids = self._load_extra(self._class_ids_path)
123
+ assert self._class_ids is not None
124
+ return self._class_ids
125
+
126
+ def _get_class_names(self) -> np.ndarray:
127
+ if self._split == _Split.TEST:
128
+ raise AssertionError("Class names are not available in TEST split")
129
+ if self._class_names is None:
130
+ self._class_names = self._load_extra(self._class_names_path)
131
+ assert self._class_names is not None
132
+ return self._class_names
133
+
134
+ def find_class_id(self, class_index: int) -> str:
135
+ class_ids = self._get_class_ids()
136
+ return str(class_ids[class_index])
137
+
138
+ def find_class_name(self, class_index: int) -> str:
139
+ class_names = self._get_class_names()
140
+ return str(class_names[class_index])
141
+
142
+ def get_image_data(self, index: int) -> bytes:
143
+ entries = self._get_entries()
144
+ actual_index = entries[index]["actual_index"]
145
+
146
+ class_id = self.get_class_id(index)
147
+
148
+ image_relpath = self.split.get_image_relpath(actual_index, class_id)
149
+ image_full_path = os.path.join(self.root, image_relpath)
150
+ with open(image_full_path, mode="rb") as f:
151
+ image_data = f.read()
152
+ return image_data
153
+
154
+ def get_target(self, index: int) -> Optional[Target]:
155
+ entries = self._get_entries()
156
+ class_index = entries[index]["class_index"]
157
+ return None if self.split == _Split.TEST else int(class_index)
158
+
159
+ def get_targets(self) -> Optional[np.ndarray]:
160
+ entries = self._get_entries()
161
+ return None if self.split == _Split.TEST else entries["class_index"]
162
+
163
+ def get_class_id(self, index: int) -> Optional[str]:
164
+ entries = self._get_entries()
165
+ class_id = entries[index]["class_id"]
166
+ return None if self.split == _Split.TEST else str(class_id)
167
+
168
+ def get_class_name(self, index: int) -> Optional[str]:
169
+ entries = self._get_entries()
170
+ class_name = entries[index]["class_name"]
171
+ return None if self.split == _Split.TEST else str(class_name)
172
+
173
+ def __len__(self) -> int:
174
+ entries = self._get_entries()
175
+ assert len(entries) == self.split.length
176
+ return len(entries)
177
+
178
+ def _load_labels(self, labels_path: str) -> List[Tuple[str, str]]:
179
+ labels_full_path = os.path.join(self.root, labels_path)
180
+ labels = []
181
+
182
+ try:
183
+ with open(labels_full_path, "r") as f:
184
+ reader = csv.reader(f)
185
+ for row in reader:
186
+ class_id, class_name = row
187
+ labels.append((class_id, class_name))
188
+ except OSError as e:
189
+ raise RuntimeError(f'can not read labels file "{labels_full_path}"') from e
190
+
191
+ return labels
192
+
193
+ def _dump_entries(self) -> None:
194
+ split = self.split
195
+ if split == ImageNet.Split.TEST:
196
+ dataset = None
197
+ sample_count = split.length
198
+ max_class_id_length, max_class_name_length = 0, 0
199
+ else:
200
+ labels_path = "labels.txt"
201
+ logger.info(f'loading labels from "{labels_path}"')
202
+ labels = self._load_labels(labels_path)
203
+
204
+ # NOTE: Using torchvision ImageFolder for consistency
205
+ from torchvision.datasets import ImageFolder
206
+
207
+ dataset_root = os.path.join(self.root, split.get_dirname())
208
+ dataset = ImageFolder(dataset_root)
209
+ sample_count = len(dataset)
210
+ max_class_id_length, max_class_name_length = -1, -1
211
+ for sample in dataset.samples:
212
+ _, class_index = sample
213
+ class_id, class_name = labels[class_index]
214
+ max_class_id_length = max(len(class_id), max_class_id_length)
215
+ max_class_name_length = max(len(class_name), max_class_name_length)
216
+
217
+ dtype = np.dtype(
218
+ [
219
+ ("actual_index", "<u4"),
220
+ ("class_index", "<u4"),
221
+ ("class_id", f"U{max_class_id_length}"),
222
+ ("class_name", f"U{max_class_name_length}"),
223
+ ]
224
+ )
225
+ entries_array = np.empty(sample_count, dtype=dtype)
226
+
227
+ if split == ImageNet.Split.TEST:
228
+ old_percent = -1
229
+ for index in range(sample_count):
230
+ percent = 100 * (index + 1) // sample_count
231
+ if percent > old_percent:
232
+ logger.info(f"creating entries: {percent}%")
233
+ old_percent = percent
234
+
235
+ actual_index = index + 1
236
+ class_index = np.uint32(-1)
237
+ class_id, class_name = "", ""
238
+ entries_array[index] = (actual_index, class_index, class_id, class_name)
239
+ else:
240
+ class_names = {class_id: class_name for class_id, class_name in labels}
241
+
242
+ assert dataset
243
+ old_percent = -1
244
+ for index in range(sample_count):
245
+ percent = 100 * (index + 1) // sample_count
246
+ if percent > old_percent:
247
+ logger.info(f"creating entries: {percent}%")
248
+ old_percent = percent
249
+
250
+ image_full_path, class_index = dataset.samples[index]
251
+ image_relpath = os.path.relpath(image_full_path, self.root)
252
+ class_id, actual_index = split.parse_image_relpath(image_relpath)
253
+ class_name = class_names[class_id]
254
+ entries_array[index] = (actual_index, class_index, class_id, class_name)
255
+
256
+ logger.info(f'saving entries to "{self._entries_path}"')
257
+ self._save_extra(entries_array, self._entries_path)
258
+
259
+ def _dump_class_ids_and_names(self) -> None:
260
+ split = self.split
261
+ if split == ImageNet.Split.TEST:
262
+ return
263
+
264
+ entries_array = self._load_extra(self._entries_path)
265
+
266
+ max_class_id_length, max_class_name_length, max_class_index = -1, -1, -1
267
+ for entry in entries_array:
268
+ class_index, class_id, class_name = (
269
+ entry["class_index"],
270
+ entry["class_id"],
271
+ entry["class_name"],
272
+ )
273
+ max_class_index = max(int(class_index), max_class_index)
274
+ max_class_id_length = max(len(str(class_id)), max_class_id_length)
275
+ max_class_name_length = max(len(str(class_name)), max_class_name_length)
276
+
277
+ class_count = max_class_index + 1
278
+ class_ids_array = np.empty(class_count, dtype=f"U{max_class_id_length}")
279
+ class_names_array = np.empty(class_count, dtype=f"U{max_class_name_length}")
280
+ for entry in entries_array:
281
+ class_index, class_id, class_name = (
282
+ entry["class_index"],
283
+ entry["class_id"],
284
+ entry["class_name"],
285
+ )
286
+ class_ids_array[class_index] = class_id
287
+ class_names_array[class_index] = class_name
288
+
289
+ logger.info(f'saving class IDs to "{self._class_ids_path}"')
290
+ self._save_extra(class_ids_array, self._class_ids_path)
291
+
292
+ logger.info(f'saving class names to "{self._class_names_path}"')
293
+ self._save_extra(class_names_array, self._class_names_path)
294
+
295
+ def dump_extra(self) -> None:
296
+ self._dump_entries()
297
+ self._dump_class_ids_and_names()
dinov3/data/datasets/image_net_22k.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import os
7
+ import warnings
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from functools import lru_cache
11
+ from gzip import GzipFile
12
+ from io import BytesIO
13
+ from mmap import ACCESS_READ, mmap
14
+ from typing import Any, Callable, List, Optional, Set, Tuple
15
+
16
+ import numpy as np
17
+
18
+ from .extended import ExtendedVisionDataset
19
+
20
+ _Labels = int
21
+
22
+ _DEFAULT_MMAP_CACHE_SIZE = 16 # Warning: This can exhaust file descriptors
23
+
24
+
25
+ @dataclass
26
+ class _ClassEntry:
27
+ block_offset: int
28
+ maybe_filename: Optional[str] = None
29
+
30
+
31
+ @dataclass
32
+ class _Entry:
33
+ class_index: int # noqa: E701
34
+ start_offset: int
35
+ end_offset: int
36
+ filename: str
37
+
38
+
39
+ class _Split(Enum):
40
+ TRAIN = "train"
41
+ VAL = "val"
42
+
43
+ @property
44
+ def length(self) -> int:
45
+ return {
46
+ _Split.TRAIN: 11_797_647,
47
+ _Split.VAL: 561_050,
48
+ }[self]
49
+
50
+ def entries_path(self):
51
+ return f"imagenet21kp_{self.value}.txt"
52
+
53
+
54
+ def _get_tarball_path(class_id: str) -> str:
55
+ return f"{class_id}.tar"
56
+
57
+
58
+ def _make_mmap_tarball(tarballs_root: str, mmap_cache_size: int):
59
+ @lru_cache(maxsize=mmap_cache_size)
60
+ def _mmap_tarball(class_id: str) -> mmap:
61
+ tarball_path = _get_tarball_path(class_id)
62
+ tarball_full_path = os.path.join(tarballs_root, tarball_path)
63
+ with open(tarball_full_path) as f:
64
+ return mmap(fileno=f.fileno(), length=0, access=ACCESS_READ)
65
+
66
+ return _mmap_tarball
67
+
68
+
69
+ class ImageNet22k(ExtendedVisionDataset):
70
+ _GZIPPED_INDICES: Set[int] = {
71
+ 841_545,
72
+ 1_304_131,
73
+ 2_437_921,
74
+ 2_672_079,
75
+ 2_795_676,
76
+ 2_969_786,
77
+ 6_902_965,
78
+ 6_903_550,
79
+ 6_903_628,
80
+ 7_432_557,
81
+ 7_432_589,
82
+ 7_813_809,
83
+ 8_329_633,
84
+ 10_296_990,
85
+ 10_417_652,
86
+ 10_492_265,
87
+ 10_598_078,
88
+ 10_782_398,
89
+ 10_902_612,
90
+ 11_203_736,
91
+ 11_342_890,
92
+ 11_397_596,
93
+ 11_589_762,
94
+ 11_705_103,
95
+ 12_936_875,
96
+ 13_289_782,
97
+ }
98
+ Labels = _Labels
99
+
100
+ def __init__(
101
+ self,
102
+ *,
103
+ root: str,
104
+ extra: str,
105
+ transforms: Optional[Callable] = None,
106
+ transform: Optional[Callable] = None,
107
+ target_transform: Optional[Callable] = None,
108
+ mmap_cache_size: int = _DEFAULT_MMAP_CACHE_SIZE,
109
+ ) -> None:
110
+ super().__init__(root, transforms, transform, target_transform)
111
+ self._extra_root = extra
112
+
113
+ entries_path = self._get_entries_path(root)
114
+ self._entries = self._load_extra(entries_path)
115
+
116
+ class_ids_path = self._get_class_ids_path(root)
117
+ self._class_ids = self._load_extra(class_ids_path)
118
+
119
+ self._gzipped_indices = ImageNet22k._GZIPPED_INDICES
120
+ self._mmap_tarball = _make_mmap_tarball(self._tarballs_root, mmap_cache_size)
121
+
122
+ def _get_entries_path(self, root: Optional[str] = None) -> str:
123
+ return "entries.npy"
124
+
125
+ def _get_class_ids_path(self, root: Optional[str] = None) -> str:
126
+ return "class-ids.npy"
127
+
128
+ def _find_class_ids(self, path: str) -> List[str]:
129
+ class_ids = []
130
+
131
+ with os.scandir(path) as entries:
132
+ for entry in entries:
133
+ root, ext = os.path.splitext(entry.name)
134
+ if ext != ".tar":
135
+ continue
136
+ class_ids.append(root)
137
+
138
+ return sorted(class_ids)
139
+
140
+ def _load_entries_class_ids(self, root: Optional[str] = None) -> Tuple[List[_Entry], List[str]]:
141
+ root = self.get_root(root)
142
+ entries: List[_Entry] = []
143
+ class_ids = self._find_class_ids(root)
144
+
145
+ for class_index, class_id in enumerate(class_ids):
146
+ path = os.path.join(root, "blocks", f"{class_id}.log")
147
+ class_entries = []
148
+
149
+ try:
150
+ with open(path) as f:
151
+ for line in f:
152
+ line = line.rstrip()
153
+ block, filename = line.split(":")
154
+ block_offset = int(block[6:])
155
+ filename = filename[1:]
156
+
157
+ maybe_filename = None
158
+ if filename != "** Block of NULs **":
159
+ maybe_filename = filename
160
+ _, ext = os.path.splitext(filename)
161
+ # assert ext == ".JPEG"
162
+
163
+ class_entry = _ClassEntry(block_offset, maybe_filename)
164
+ class_entries.append(class_entry)
165
+ except OSError as e:
166
+ raise RuntimeError(f'can not read blocks file "{path}"') from e
167
+
168
+ assert class_entries[-1].maybe_filename is None
169
+
170
+ for class_entry1, class_entry2 in zip(class_entries, class_entries[1:]):
171
+ assert class_entry1.block_offset <= class_entry2.block_offset
172
+ start_offset = 512 * class_entry1.block_offset
173
+ end_offset = 512 * class_entry2.block_offset
174
+ assert class_entry1.maybe_filename is not None
175
+ filename = class_entry1.maybe_filename
176
+ entry = _Entry(class_index, start_offset, end_offset, filename)
177
+ # Skip invalid image files (PIL throws UnidentifiedImageError)
178
+ if filename == "n06470073_47249.JPEG":
179
+ continue
180
+ entries.append(entry)
181
+
182
+ return entries, class_ids
183
+
184
+ def _load_extra(self, extra_path: str) -> np.ndarray:
185
+ extra_root = self._extra_root
186
+ extra_full_path = os.path.join(extra_root, extra_path)
187
+ return np.load(extra_full_path, mmap_mode="r")
188
+
189
+ def _save_extra(self, extra_array: np.ndarray, extra_path: str) -> None:
190
+ extra_root = self._extra_root
191
+ extra_full_path = os.path.join(extra_root, extra_path)
192
+ os.makedirs(extra_root, exist_ok=True)
193
+ np.save(extra_full_path, extra_array)
194
+
195
+ @property
196
+ def _tarballs_root(self) -> str:
197
+ return self.root
198
+
199
+ def find_class_id(self, class_index: int) -> str:
200
+ return str(self._class_ids[class_index])
201
+
202
+ def get_image_data(self, index: int) -> bytes:
203
+ entry = self._entries[index]
204
+ class_id = entry["class_id"]
205
+ class_mmap = self._mmap_tarball(class_id)
206
+
207
+ start_offset, end_offset = entry["start_offset"], entry["end_offset"]
208
+ try:
209
+ mapped_data = class_mmap[start_offset:end_offset]
210
+ data = mapped_data[512:] # Skip entry header block
211
+
212
+ if len(data) >= 2 and tuple(data[:2]) == (0x1F, 0x8B):
213
+ assert index in self._gzipped_indices, f"unexpected gzip header for sample {index}"
214
+ with GzipFile(fileobj=BytesIO(data)) as g:
215
+ data = g.read()
216
+ except Exception as e:
217
+ raise RuntimeError(f"can not retrieve image data for sample {index} " f'from "{class_id}" tarball') from e
218
+
219
+ return data
220
+
221
+ def get_target(self, index: int) -> Any:
222
+ return int(self._entries[index]["class_index"])
223
+
224
+ def get_targets(self) -> np.ndarray:
225
+ return self._entries["class_index"]
226
+
227
+ def get_class_id(self, index: int) -> str:
228
+ return str(self._entries[index]["class_id"])
229
+
230
+ def get_class_ids(self) -> np.ndarray:
231
+ return self._entries["class_id"]
232
+
233
+ def __getitem__(self, index: int) -> Tuple[Any, Any]:
234
+ with warnings.catch_warnings():
235
+ warnings.simplefilter("ignore")
236
+ return super().__getitem__(index)
237
+
238
+ def __len__(self) -> int:
239
+ return len(self._entries)
240
+
241
+ def _dump_entries(self, *args, **kwargs) -> None:
242
+ entries, class_ids = self._load_entries_class_ids(*args, **kwargs)
243
+
244
+ max_class_id_length, max_filename_length, max_class_index = -1, -1, -1
245
+ for entry in entries:
246
+ class_id = class_ids[entry.class_index]
247
+ max_class_index = max(entry.class_index, max_class_index)
248
+ max_class_id_length = max(len(class_id), max_class_id_length)
249
+ max_filename_length = max(len(entry.filename), max_filename_length)
250
+
251
+ dtype = np.dtype(
252
+ [
253
+ ("class_index", "<u4"),
254
+ ("class_id", f"U{max_class_id_length}"),
255
+ ("start_offset", "<u4"),
256
+ ("end_offset", "<u4"),
257
+ ("filename", f"U{max_filename_length}"),
258
+ ]
259
+ )
260
+ sample_count = len(entries)
261
+ entries_array = np.empty(sample_count, dtype=dtype)
262
+ for i, entry in enumerate(entries):
263
+ class_index = entry.class_index
264
+ class_id = class_ids[class_index]
265
+ start_offset = entry.start_offset
266
+ end_offset = entry.end_offset
267
+ filename = entry.filename
268
+ entries_array[i] = (
269
+ class_index,
270
+ class_id,
271
+ start_offset,
272
+ end_offset,
273
+ filename,
274
+ )
275
+
276
+ entries_path = self._get_entries_path(*args, **kwargs)
277
+ self._save_extra(entries_array, entries_path)
278
+
279
+ def _dump_class_ids(self, *args, **kwargs) -> None:
280
+ entries_path = self._get_entries_path(*args, **kwargs)
281
+ entries_array = self._load_extra(entries_path)
282
+
283
+ max_class_id_length, max_class_index = -1, -1
284
+ for entry in entries_array:
285
+ class_index, class_id = entry["class_index"], entry["class_id"]
286
+ max_class_index = max(int(class_index), max_class_index)
287
+ max_class_id_length = max(len(str(class_id)), max_class_id_length)
288
+
289
+ class_ids_array = np.empty(max_class_index + 1, dtype=f"U{max_class_id_length}")
290
+ for entry in entries_array:
291
+ class_index, class_id = entry["class_index"], entry["class_id"]
292
+ class_ids_array[class_index] = class_id
293
+ class_ids_path = self._get_class_ids_path(*args, **kwargs)
294
+ self._save_extra(class_ids_array, class_ids_path)
295
+
296
+ def _dump_extra(self, *args, **kwargs) -> None:
297
+ self._dump_entries(*args, *kwargs)
298
+ self._dump_class_ids(*args, *kwargs)
299
+
300
+ def dump_extra(self, root: Optional[str] = None) -> None:
301
+ return self._dump_extra(root)
dinov3/data/loaders.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ from enum import Enum
8
+ from typing import Any, Callable, List, Optional, TypeVar
9
+
10
+ import torch
11
+ from torch.utils.data import Sampler
12
+
13
+ from .datasets import ADE20K, CocoCaptions, ImageNet, ImageNet22k
14
+ from .samplers import EpochSampler, InfiniteSampler, ShardedInfiniteSampler
15
+
16
+ logger = logging.getLogger("dinov3")
17
+
18
+
19
+ class SamplerType(Enum):
20
+ DISTRIBUTED = 0
21
+ EPOCH = 1
22
+ INFINITE = 2
23
+ SHARDED_INFINITE = 3
24
+ SHARDED_INFINITE_NEW = 4
25
+
26
+
27
+ def _make_bool_str(b: bool) -> str:
28
+ return "yes" if b else "no"
29
+
30
+
31
+ def _make_sample_transform(
32
+ image_transform: Optional[Callable] = None,
33
+ target_transform: Optional[Callable] = None,
34
+ ):
35
+ def transform(sample):
36
+ image, target = sample
37
+ if image_transform is not None:
38
+ image = image_transform(image)
39
+ if target_transform is not None:
40
+ target = target_transform(target)
41
+ return image, target
42
+
43
+ return transform
44
+
45
+
46
+ def _parse_dataset_str(dataset_str: str):
47
+ tokens = dataset_str.split(":")
48
+
49
+ name = tokens[0]
50
+ kwargs = {}
51
+
52
+ for token in tokens[1:]:
53
+ key, value = token.split("=")
54
+ assert key in ("root", "extra", "split")
55
+ kwargs[key] = value
56
+
57
+ if name == "ImageNet":
58
+ class_ = ImageNet
59
+ if "split" in kwargs:
60
+ kwargs["split"] = ImageNet.Split[kwargs["split"]]
61
+ elif name == "ImageNet22k":
62
+ class_ = ImageNet22k
63
+ elif name == "ADE20K":
64
+ class_ = ADE20K
65
+ if "split" in kwargs:
66
+ kwargs["split"] = ADE20K.Split[kwargs["split"]]
67
+ elif name == "CocoCaptions":
68
+ class_ = CocoCaptions
69
+ if "split" in kwargs:
70
+ kwargs["split"] = CocoCaptions.Split[kwargs["split"]]
71
+ else:
72
+ raise ValueError(f'Unsupported dataset "{name}"')
73
+
74
+ return class_, kwargs
75
+
76
+
77
+ def make_dataset(
78
+ *,
79
+ dataset_str: str,
80
+ transform: Optional[Callable] = None,
81
+ target_transform: Optional[Callable] = None,
82
+ transforms: Optional[Callable] = None,
83
+ ):
84
+ """
85
+ Creates a dataset with the specified parameters.
86
+
87
+ Args:
88
+ dataset_str: A dataset string description (e.g. ImageNet:split=TRAIN).
89
+ transform: A transform to apply to images.
90
+ target_transform: A transform to apply to targets.
91
+ transforms: A transform to apply to both images and targets.
92
+
93
+ Returns:
94
+ The created dataset.
95
+ """
96
+ logger.info(f'using dataset: "{dataset_str}"')
97
+
98
+ class_, kwargs = _parse_dataset_str(dataset_str)
99
+ dataset = class_(transform=transform, target_transform=target_transform, transforms=transforms, **kwargs)
100
+
101
+ logger.info(f"# of dataset samples: {len(dataset):,d}")
102
+
103
+ # Aggregated datasets do not expose (yet) these attributes, so add them.
104
+ if not hasattr(dataset, "transform"):
105
+ dataset.transform = transform
106
+ if not hasattr(dataset, "target_transform"):
107
+ dataset.target_transform = target_transform
108
+ if not hasattr(dataset, "transforms"):
109
+ dataset.transforms = transforms
110
+
111
+ return dataset
112
+
113
+
114
+ def _make_sampler(
115
+ *,
116
+ dataset,
117
+ type: Optional[SamplerType] = None,
118
+ shuffle: bool = False,
119
+ seed: int = 0,
120
+ size: int = -1,
121
+ advance: int = 0,
122
+ ) -> Optional[Sampler]:
123
+ sample_count = len(dataset)
124
+
125
+ if type == SamplerType.INFINITE:
126
+ logger.info("sampler: infinite")
127
+ if size > 0:
128
+ raise ValueError("sampler size > 0 is invalid")
129
+ return InfiniteSampler(
130
+ sample_count=sample_count,
131
+ shuffle=shuffle,
132
+ seed=seed,
133
+ advance=advance,
134
+ )
135
+ elif type in (SamplerType.SHARDED_INFINITE, SamplerType.SHARDED_INFINITE_NEW):
136
+ logger.info("sampler: sharded infinite")
137
+ if size > 0:
138
+ raise ValueError("sampler size > 0 is invalid")
139
+ use_new_shuffle_tensor_slice = type == SamplerType.SHARDED_INFINITE_NEW
140
+ return ShardedInfiniteSampler(
141
+ sample_count=sample_count,
142
+ shuffle=shuffle,
143
+ seed=seed,
144
+ advance=advance,
145
+ use_new_shuffle_tensor_slice=use_new_shuffle_tensor_slice,
146
+ )
147
+ elif type == SamplerType.EPOCH:
148
+ logger.info("sampler: epoch")
149
+ if advance > 0:
150
+ raise NotImplementedError("sampler advance > 0 is not supported")
151
+ size = size if size > 0 else sample_count
152
+ logger.info(f"# of samples / epoch: {size:,d}")
153
+ return EpochSampler(
154
+ size=size,
155
+ sample_count=sample_count,
156
+ shuffle=shuffle,
157
+ seed=seed,
158
+ )
159
+ elif type == SamplerType.DISTRIBUTED:
160
+ logger.info("sampler: distributed")
161
+ if size > 0:
162
+ raise ValueError("sampler size > 0 is invalid")
163
+ if advance > 0:
164
+ raise ValueError("sampler advance > 0 is invalid")
165
+ return torch.utils.data.DistributedSampler(
166
+ dataset=dataset,
167
+ shuffle=shuffle,
168
+ seed=seed,
169
+ drop_last=False,
170
+ )
171
+
172
+ logger.info("sampler: none")
173
+ return None
174
+
175
+
176
+ T = TypeVar("T")
177
+
178
+
179
+ def make_data_loader(
180
+ *,
181
+ dataset,
182
+ batch_size: int,
183
+ num_workers: int,
184
+ shuffle: bool = True,
185
+ seed: int = 0,
186
+ sampler_type: Optional[SamplerType] = SamplerType.INFINITE,
187
+ sampler_size: int = -1,
188
+ sampler_advance: int = 0,
189
+ drop_last: bool = True,
190
+ persistent_workers: bool = False,
191
+ collate_fn: Optional[Callable[[List[T]], Any]] = None,
192
+ worker_init_fn: Optional[Callable[[List[T]], Any]] = None,
193
+ ):
194
+ """
195
+ Creates a data loader with the specified parameters.
196
+
197
+ Args:
198
+ dataset: A dataset (third party, LaViDa or WebDataset).
199
+ batch_size: The size of batches to generate.
200
+ num_workers: The number of workers to use.
201
+ shuffle: Whether to shuffle samples.
202
+ seed: The random seed to use.
203
+ sampler_type: Which sampler to use: EPOCH, INFINITE, SHARDED_INFINITE, SHARDED_INFINITE_NEW, DISTRIBUTED or None.
204
+ sampler_size: The number of images per epoch (when applicable) or -1 for the entire dataset.
205
+ sampler_advance: How many samples to skip (when applicable).
206
+ drop_last: Whether the last non-full batch of data should be dropped.
207
+ persistent_workers: maintain the workers Dataset instances alive after a dataset has been consumed once.
208
+ collate_fn: Function that performs batch collation
209
+ """
210
+
211
+ sampler = _make_sampler(
212
+ dataset=dataset,
213
+ type=sampler_type,
214
+ shuffle=shuffle,
215
+ seed=seed,
216
+ size=sampler_size,
217
+ advance=sampler_advance,
218
+ )
219
+
220
+ logger.info("using PyTorch data loader")
221
+ data_loader = torch.utils.data.DataLoader(
222
+ dataset,
223
+ sampler=sampler,
224
+ batch_size=batch_size,
225
+ num_workers=num_workers,
226
+ pin_memory=True,
227
+ drop_last=drop_last,
228
+ persistent_workers=persistent_workers,
229
+ collate_fn=collate_fn,
230
+ worker_init_fn=worker_init_fn,
231
+ )
232
+
233
+ try:
234
+ logger.info(f"# of batches: {len(data_loader):,d}")
235
+ except TypeError: # data loader has no length
236
+ logger.info("infinite data loader")
237
+ return data_loader
dinov3/data/masking.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import math
7
+ import random
8
+
9
+ import numpy as np
10
+
11
+
12
+ class MaskingGenerator:
13
+ def __init__(
14
+ self,
15
+ input_size,
16
+ num_masking_patches=None,
17
+ min_num_patches=4,
18
+ max_num_patches=None,
19
+ min_aspect=0.3,
20
+ max_aspect=None,
21
+ ):
22
+ if not isinstance(input_size, tuple):
23
+ input_size = (input_size,) * 2
24
+ self.height, self.width = input_size
25
+
26
+ self.num_patches = self.height * self.width
27
+ self.num_masking_patches = num_masking_patches
28
+
29
+ self.min_num_patches = min_num_patches
30
+ self.max_num_patches = num_masking_patches if max_num_patches is None else max_num_patches
31
+
32
+ max_aspect = max_aspect or 1 / min_aspect
33
+ self.log_aspect_ratio = (math.log(min_aspect), math.log(max_aspect))
34
+
35
+ def __repr__(self):
36
+ repr_str = "Generator(%d, %d -> [%d ~ %d], max = %d, %.3f ~ %.3f)" % (
37
+ self.height,
38
+ self.width,
39
+ self.min_num_patches,
40
+ self.max_num_patches,
41
+ self.num_masking_patches,
42
+ self.log_aspect_ratio[0],
43
+ self.log_aspect_ratio[1],
44
+ )
45
+ return repr_str
46
+
47
+ def get_shape(self):
48
+ return self.height, self.width
49
+
50
+ def _mask(self, mask, max_mask_patches):
51
+ delta = 0
52
+ for _ in range(10):
53
+ target_area = random.uniform(self.min_num_patches, max_mask_patches)
54
+ aspect_ratio = math.exp(random.uniform(*self.log_aspect_ratio))
55
+ h = int(round(math.sqrt(target_area * aspect_ratio)))
56
+ w = int(round(math.sqrt(target_area / aspect_ratio)))
57
+ if w < self.width and h < self.height:
58
+ top = random.randint(0, self.height - h)
59
+ left = random.randint(0, self.width - w)
60
+
61
+ num_masked = mask[top : top + h, left : left + w].sum()
62
+ # Overlap
63
+ if 0 < h * w - num_masked <= max_mask_patches:
64
+ for i in range(top, top + h):
65
+ for j in range(left, left + w):
66
+ if mask[i, j] == 0:
67
+ mask[i, j] = 1
68
+ delta += 1
69
+
70
+ if delta > 0:
71
+ break
72
+ return delta
73
+
74
+ def __call__(self, num_masking_patches=0):
75
+ mask = np.zeros(shape=self.get_shape(), dtype=bool)
76
+ mask_count = 0
77
+ while mask_count < num_masking_patches:
78
+ max_mask_patches = num_masking_patches - mask_count
79
+ max_mask_patches = min(max_mask_patches, self.max_num_patches)
80
+
81
+ delta = self._mask(mask, max_mask_patches)
82
+ if delta == 0:
83
+ break
84
+ else:
85
+ mask_count += delta
86
+
87
+ return self.complete_mask_randomly(mask, num_masking_patches)
88
+
89
+ def complete_mask_randomly(self, mask, num_masking_patches):
90
+ shape = mask.shape
91
+ m2 = mask.flatten()
92
+ to_add = np.random.choice(np.where(~m2)[0], size=num_masking_patches - m2.sum(), replace=False)
93
+ m2[to_add] = True
94
+ return m2.reshape(shape)
dinov3/data/meta_loaders.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ from typing import Any, Iterable, Iterator, List, Tuple, TypeVar
8
+
9
+ import numpy as np
10
+
11
+ logger = logging.getLogger("dinov3")
12
+ Loader = Iterable[List[Any]]
13
+ T = TypeVar("T")
14
+
15
+
16
+ class CombinedDataLoader:
17
+ """
18
+ Combines data loaders using the provided sampling ratios
19
+ """
20
+
21
+ GLOBAL_HOMOGENEOUS = 0
22
+ LOCAL_HOMOGENEOUS = 1
23
+
24
+ def __init__(
25
+ self,
26
+ loaders_with_ratios: Iterable[Tuple[Loader, float]],
27
+ batch_size: int,
28
+ combining_mode: int = 1,
29
+ seed: int = 65537,
30
+ name: str = None,
31
+ logging_period: int = 100,
32
+ ):
33
+ if combining_mode not in [self.GLOBAL_HOMOGENEOUS, self.LOCAL_HOMOGENEOUS]:
34
+ raise ValueError(f"Unsupported value of combining_mode ({combining_mode})")
35
+ loaders, ratios = zip(*loaders_with_ratios)
36
+ assert np.all([loader.batch_size == batch_size for loader in loaders]), (
37
+ f"All individual loaders must have the same batch size to the combined data loader for combining_mode={combining_mode}"
38
+ )
39
+ self.loaders = loaders
40
+ self.ratios = ratios
41
+ self.batch_size = batch_size
42
+ self.combining_mode = combining_mode
43
+ self.initial_seed = seed
44
+ self.name = name if name is not None else ""
45
+ self.logging_period = logging_period
46
+ if combining_mode == self.GLOBAL_HOMOGENEOUS:
47
+ logger.info(f"Initialize CDL {self.name} with seed={seed}")
48
+ self.seed = seed
49
+ self.rng = np.random.default_rng(seed=seed)
50
+ else:
51
+ logger.info(f"Initialize CDL {self.name} with random seed")
52
+ self.seed = 0
53
+ self.rng = np.random.default_rng()
54
+ self.loader_count = np.zeros(len(self.loaders))
55
+
56
+ def homogeneous_iterator(self) -> Iterator[List[Any]]:
57
+ iteration = 0
58
+ iters = [iter(loader) for loader in self.loaders]
59
+ while True:
60
+ iteration += 1
61
+ try:
62
+ idx = self.rng.choice(len(self.loaders), p=self.ratios)
63
+ self.loader_count[idx] += 1
64
+ if iteration % self.logging_period == 0:
65
+ logger.info(f"Empirical ratios: CDL {self.name} {self.loader_count / self.loader_count.sum()}")
66
+ yield next(iters[idx])
67
+ except StopIteration:
68
+ break
69
+
70
+ def heterogeneous_iterator(self) -> Iterator[List[Any]]:
71
+ pass
72
+
73
+ def __iter__(self) -> Iterator[List[Any]]:
74
+ if self.combining_mode in [self.GLOBAL_HOMOGENEOUS, self.LOCAL_HOMOGENEOUS]:
75
+ logger.info(f"Using homogeneous iterator for CDL {self.name}")
76
+ return self.homogeneous_iterator()
77
+ else:
78
+ raise ValueError(f"Unsupported value of combining_mode ({self.combining_mode})")
dinov3/data/samplers.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import itertools
7
+ import warnings
8
+ from typing import Any, Optional
9
+
10
+ import numpy as np
11
+ import torch
12
+ from torch.utils.data.sampler import Sampler
13
+
14
+ from dinov3.distributed import get_rank, get_world_size
15
+
16
+
17
+ class EpochSampler(Sampler):
18
+ def __init__(
19
+ self,
20
+ *,
21
+ size: int,
22
+ sample_count: int,
23
+ shuffle: bool = False,
24
+ seed: int = 0,
25
+ start: Optional[int] = None,
26
+ step: Optional[int] = None,
27
+ ):
28
+ self._size = size
29
+ self._sample_count = sample_count
30
+ self._shuffle = shuffle
31
+ self._seed = seed
32
+ self._start = get_rank() if start is None else start
33
+ self._step = get_world_size() if step is None else step
34
+ self._epoch = 0
35
+
36
+ def __iter__(self):
37
+ count = (self._size + self._sample_count - 1) // self._sample_count
38
+ tiled_indices = np.tile(np.arange(self._sample_count), count)
39
+ if self._shuffle:
40
+ seed = self._seed * self._epoch if self._seed != 0 else self._epoch
41
+ rng = np.random.default_rng(seed)
42
+ iterable = rng.choice(tiled_indices, self._size, replace=False)
43
+ else:
44
+ iterable = tiled_indices[: self._size]
45
+
46
+ yield from itertools.islice(iterable, self._start, None, self._step)
47
+
48
+ def __len__(self):
49
+ return (self._size - self._start + self._step - 1) // self._step
50
+
51
+ def set_epoch(self, epoch):
52
+ self._epoch = epoch
53
+
54
+
55
+ def _get_numpy_dtype(size: int) -> Any:
56
+ return np.int32 if size <= 2**31 else np.int64
57
+
58
+
59
+ def _get_torch_dtype(size: int) -> Any:
60
+ return torch.int32 if size <= 2**31 else torch.int64
61
+
62
+
63
+ def _generate_randperm_indices(*, size: int, generator: torch.Generator):
64
+ """Generate the indices of a random permutation."""
65
+ dtype = _get_torch_dtype(size)
66
+ # This is actually matching PyTorch's CPU implementation, see: https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/TensorFactories.cpp#L900-L921
67
+ perm = torch.arange(size, dtype=dtype)
68
+ for i in range(size):
69
+ j = torch.randint(i, size, size=(1,), generator=generator).item()
70
+
71
+ # Always swap even if no-op
72
+ value = perm[j].item()
73
+ perm[j] = perm[i].item()
74
+ perm[i] = value
75
+ yield value
76
+
77
+
78
+ class InfiniteSampler(Sampler):
79
+ def __init__(
80
+ self,
81
+ *,
82
+ sample_count: int,
83
+ shuffle: bool = False,
84
+ seed: int = 0,
85
+ start: Optional[int] = None,
86
+ step: Optional[int] = None,
87
+ advance: int = 0,
88
+ ):
89
+ self._sample_count = sample_count
90
+ self._seed = seed
91
+ self._shuffle = shuffle
92
+ self._start = get_rank() if start is None else start
93
+ self._step = get_world_size() if step is None else step
94
+ self._advance = advance
95
+
96
+ def __iter__(self):
97
+ if self._shuffle:
98
+ iterator = self._shuffled_iterator()
99
+ else:
100
+ iterator = self._iterator()
101
+
102
+ yield from itertools.islice(iterator, self._advance, None)
103
+
104
+ def _iterator(self):
105
+ assert not self._shuffle
106
+
107
+ while True:
108
+ iterable = range(self._sample_count)
109
+ yield from itertools.islice(iterable, self._start, None, self._step)
110
+
111
+ def _shuffled_iterator(self):
112
+ assert self._shuffle
113
+
114
+ # Instantiate a generator here (rather than in the ctor) to keep the class
115
+ # picklable (requirement of mp.spawn)
116
+ generator = torch.Generator().manual_seed(self._seed)
117
+
118
+ while True:
119
+ iterable = _generate_randperm_indices(size=self._sample_count, generator=generator)
120
+ yield from itertools.islice(iterable, self._start, None, self._step)
121
+
122
+
123
+ # The following function is somewhat equivalent to _new_shuffle_tensor_slice below,
124
+ # but avoids a full in-place random permutation generation.
125
+ def _shuffle_tensor_slice(
126
+ *, tensor: torch.Tensor, start: int = 0, step: int = 1, generator: torch.Generator
127
+ ) -> np.ndarray:
128
+ stop = len(tensor)
129
+ count = stop // step
130
+ drop_count = stop - step * count
131
+ if drop_count:
132
+ warnings.warn(f"# of dropped samples: {drop_count}", stacklevel=1)
133
+
134
+ dtype = _get_numpy_dtype(stop)
135
+ result = np.empty(count, dtype=dtype)
136
+
137
+ for i in range(count):
138
+ j = torch.randint(0, i + 1, size=(1,), generator=generator).item() if i > 0 else 0
139
+
140
+ result[i] = result[j]
141
+ result[j] = tensor[start + i * step].item()
142
+
143
+ return result
144
+
145
+
146
+ def _new_shuffle_tensor_slice(
147
+ *, tensor: torch.Tensor, start: int = 0, step: int = 1, generator: torch.Generator
148
+ ) -> np.ndarray:
149
+ stop = len(tensor)
150
+ count = stop // step
151
+ dtype = torch.int64 # Needed for using randperm result as indices
152
+ count = stop // step
153
+ drop_count = stop - step * count
154
+ if drop_count:
155
+ warnings.warn(f"# of dropped samples: {drop_count}", stacklevel=1)
156
+ indices = torch.randperm(count, dtype=dtype, generator=generator)
157
+ return tensor[start::step][indices].numpy()
158
+
159
+
160
+ def _make_seed(seed: int, start: int, iter_count: int) -> int:
161
+ # NOTE: Tried a few variants (including iter_count << 32), this one worked best.
162
+ return seed + start + (iter_count << 24)
163
+
164
+
165
+ class ShardedInfiniteSampler(Sampler):
166
+ def __init__(
167
+ self,
168
+ *,
169
+ sample_count: int,
170
+ shuffle: bool = False,
171
+ seed: int = 0,
172
+ start: Optional[int] = None,
173
+ step: Optional[int] = None,
174
+ advance: int = 0,
175
+ use_new_shuffle_tensor_slice: bool = False,
176
+ ):
177
+ self._sample_count = sample_count
178
+ self._seed = seed
179
+ self._shuffle = shuffle
180
+ self._start = get_rank() if start is None else start
181
+ self._step = get_world_size() if step is None else step
182
+ self._advance = advance
183
+ self._iter_count = 0
184
+ self._shuffle_tensor_slice_fn = (
185
+ _new_shuffle_tensor_slice if use_new_shuffle_tensor_slice else _shuffle_tensor_slice
186
+ )
187
+
188
+ def __iter__(self):
189
+ iter_count = self._advance // self._sample_count
190
+ if iter_count > 0:
191
+ self._advance -= iter_count * self._sample_count
192
+ self._iter_count += iter_count
193
+
194
+ if self._shuffle:
195
+ iterator = self._shuffled_iterator()
196
+ else:
197
+ iterator = self._iterator()
198
+
199
+ yield from itertools.islice(iterator, self._advance, None)
200
+
201
+ def _iterator(self):
202
+ assert not self._shuffle
203
+
204
+ while True:
205
+ iterable = range(self._sample_count)
206
+ yield from itertools.islice(iterable, self._start, None, self._step)
207
+
208
+ def _shuffled_iterator(self):
209
+ assert self._shuffle
210
+
211
+ # Instantiate a generator here (rather than in the ctor) to be keep the class
212
+ # picklable (requirement of mp.spawn)
213
+ generator = torch.Generator()
214
+
215
+ # Always shuffle everything first
216
+ generator.manual_seed(self._seed)
217
+ dtype = _get_torch_dtype(self._sample_count)
218
+ perm = torch.randperm(self._sample_count, dtype=dtype, generator=generator)
219
+
220
+ while True:
221
+ # Re-seed on each iteration to allow skipping whole permutations
222
+ seed = _make_seed(self._seed, self._start, self._iter_count)
223
+ generator.manual_seed(seed)
224
+
225
+ iterable = self._shuffle_tensor_slice_fn(
226
+ tensor=perm, start=self._start, step=self._step, generator=generator
227
+ )
228
+ yield from iterable
229
+ self._iter_count += 1
dinov3/data/transforms.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ from typing import Sequence
8
+
9
+ import torch
10
+ from torchvision.transforms import v2
11
+
12
+ logger = logging.getLogger("dinov3")
13
+
14
+
15
+ def make_interpolation_mode(mode_str: str) -> v2.InterpolationMode:
16
+ return {mode.value: mode for mode in v2.InterpolationMode}[mode_str]
17
+
18
+
19
+ class GaussianBlur(v2.RandomApply):
20
+ """
21
+ Apply Gaussian Blur to the PIL image.
22
+ """
23
+
24
+ def __init__(self, *, p: float = 0.5, radius_min: float = 0.1, radius_max: float = 2.0):
25
+ # NOTE: torchvision is applying 1 - probability to return the original image
26
+ keep_p = 1 - p
27
+ transform = v2.GaussianBlur(kernel_size=9, sigma=(radius_min, radius_max))
28
+ super().__init__(transforms=[transform], p=keep_p)
29
+
30
+
31
+ # Use timm's names
32
+ IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406)
33
+ IMAGENET_DEFAULT_STD = (0.229, 0.224, 0.225)
34
+
35
+ CROP_DEFAULT_SIZE = 224
36
+ RESIZE_DEFAULT_SIZE = int(256 * CROP_DEFAULT_SIZE / 224)
37
+
38
+
39
+ def make_normalize_transform(
40
+ mean: Sequence[float] = IMAGENET_DEFAULT_MEAN,
41
+ std: Sequence[float] = IMAGENET_DEFAULT_STD,
42
+ ) -> v2.Normalize:
43
+ return v2.Normalize(mean=mean, std=std)
44
+
45
+
46
+ def make_base_transform(
47
+ mean: Sequence[float] = IMAGENET_DEFAULT_MEAN,
48
+ std: Sequence[float] = IMAGENET_DEFAULT_STD,
49
+ ) -> v2.Normalize:
50
+ return v2.Compose(
51
+ [
52
+ v2.ToDtype(torch.float32, scale=True),
53
+ make_normalize_transform(mean=mean, std=std),
54
+ ]
55
+ )
56
+
57
+
58
+ # This roughly matches torchvision's preset for classification training:
59
+ # https://github.com/pytorch/vision/blob/main/references/classification/presets.py#L6-L44
60
+ def make_classification_train_transform(
61
+ *,
62
+ crop_size: int = CROP_DEFAULT_SIZE,
63
+ interpolation=v2.InterpolationMode.BICUBIC,
64
+ hflip_prob: float = 0.5,
65
+ mean: Sequence[float] = IMAGENET_DEFAULT_MEAN,
66
+ std: Sequence[float] = IMAGENET_DEFAULT_STD,
67
+ ):
68
+ transforms_list = [v2.ToImage(), v2.RandomResizedCrop(crop_size, interpolation=interpolation)]
69
+ if hflip_prob > 0.0:
70
+ transforms_list.append(v2.RandomHorizontalFlip(hflip_prob))
71
+ transforms_list.append(make_base_transform(mean, std))
72
+ transform = v2.Compose(transforms_list)
73
+ logger.info(f"Built classification train transform\n{transform}")
74
+ return transform
75
+
76
+
77
+ def make_resize_transform(
78
+ *,
79
+ resize_size: int,
80
+ resize_square: bool = False,
81
+ resize_large_side: bool = False, # Set the larger side to resize_size instead of the smaller
82
+ interpolation: v2.InterpolationMode = v2.InterpolationMode.BICUBIC,
83
+ ):
84
+ assert not (resize_square and resize_large_side), "These two options can not be set together"
85
+ if resize_square:
86
+ logger.info("resizing image as a square")
87
+ size = (resize_size, resize_size)
88
+ transform = v2.Resize(size=size, interpolation=interpolation)
89
+ return transform
90
+ elif resize_large_side:
91
+ logger.info("resizing based on large side")
92
+ transform = v2.Resize(size=None, max_size=resize_size, interpolation=interpolation)
93
+ return transform
94
+ else:
95
+ transform = v2.Resize(resize_size, interpolation=interpolation)
96
+ return transform
97
+
98
+
99
+ # Derived from make_classification_eval_transform() with more control over resize and crop
100
+ def make_eval_transform(
101
+ *,
102
+ resize_size: int = RESIZE_DEFAULT_SIZE,
103
+ crop_size: int = CROP_DEFAULT_SIZE,
104
+ resize_square: bool = False,
105
+ resize_large_side: bool = False, # Set the larger side to resize_size instead of the smaller
106
+ interpolation: v2.InterpolationMode = v2.InterpolationMode.BICUBIC,
107
+ mean: Sequence[float] = IMAGENET_DEFAULT_MEAN,
108
+ std: Sequence[float] = IMAGENET_DEFAULT_STD,
109
+ ) -> v2.Compose:
110
+ transforms_list = [v2.ToImage()]
111
+ resize_transform = make_resize_transform(
112
+ resize_size=resize_size,
113
+ resize_square=resize_square,
114
+ resize_large_side=resize_large_side,
115
+ interpolation=interpolation,
116
+ )
117
+ transforms_list.append(resize_transform)
118
+ if crop_size:
119
+ transforms_list.append(v2.CenterCrop(crop_size))
120
+ transforms_list.append(make_base_transform(mean, std))
121
+ transform = v2.Compose(transforms_list)
122
+ logger.info(f"Built eval transform\n{transform}")
123
+ return transform
124
+
125
+
126
+ # This matches (roughly) torchvision's preset for classification evaluation:
127
+ # https://github.com/pytorch/vision/blob/main/references/classification/presets.py#L47-L69
128
+ def make_classification_eval_transform(
129
+ *,
130
+ resize_size: int = RESIZE_DEFAULT_SIZE,
131
+ crop_size: int = CROP_DEFAULT_SIZE,
132
+ interpolation=v2.InterpolationMode.BICUBIC,
133
+ mean: Sequence[float] = IMAGENET_DEFAULT_MEAN,
134
+ std: Sequence[float] = IMAGENET_DEFAULT_STD,
135
+ ) -> v2.Compose:
136
+ return make_eval_transform(
137
+ resize_size=resize_size,
138
+ crop_size=crop_size,
139
+ interpolation=interpolation,
140
+ mean=mean,
141
+ std=std,
142
+ resize_square=False,
143
+ resize_large_side=False,
144
+ )
145
+
146
+
147
+ def voc2007_classification_target_transform(label, n_categories=20):
148
+ one_hot = torch.zeros(n_categories, dtype=int)
149
+ for instance in label.instances:
150
+ one_hot[instance.category_id] = True
151
+ return one_hot
152
+
153
+
154
+ def imaterialist_classification_target_transform(label, n_categories=294):
155
+ one_hot = torch.zeros(n_categories, dtype=int)
156
+ one_hot[label.attributes] = True
157
+ return one_hot
158
+
159
+
160
+ def get_target_transform(dataset_str):
161
+ if "VOC2007" in dataset_str:
162
+ return voc2007_classification_target_transform
163
+ elif "IMaterialist" in dataset_str:
164
+ return imaterialist_classification_target_transform
165
+ return None
dinov3/distributed/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ # isort: skip_file
7
+ from .torch_distributed_wrapper import (
8
+ disable_distributed as disable,
9
+ enable_distributed as enable,
10
+ get_default_process_group,
11
+ get_process_subgroup,
12
+ get_rank,
13
+ get_subgroup_rank,
14
+ get_subgroup_size,
15
+ get_world_size,
16
+ is_distributed_enabled as is_enabled,
17
+ is_main_process,
18
+ is_subgroup_main_process,
19
+ new_subgroups,
20
+ save_in_main_process,
21
+ TorchDistributedEnvironment,
22
+ )
23
+
24
+ from .torch_distributed_primitives import gather_all_tensors, reduce_dict
dinov3/distributed/torch_distributed_primitives.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import torch
9
+ import torch.distributed as dist
10
+ from torch.nn import functional as F
11
+
12
+ from .torch_distributed_wrapper import get_default_process_group, get_world_size
13
+
14
+
15
+ def reduce_dict(input_dict: Dict[Any, torch.Tensor], average: bool = True) -> Dict[Any, torch.Tensor]:
16
+ """
17
+ Reduce the values in the dictionary from all processes so that all processes
18
+ have the averaged results. Returns a dictionary with the same fields as
19
+ the input dictionary, after reduction.
20
+
21
+ Args:
22
+ input_dict (dict): all the values will be reduced
23
+ average (bool): whether to do average or sum
24
+ """
25
+ world_size = get_world_size()
26
+ if world_size <= 1:
27
+ return input_dict
28
+ with torch.no_grad():
29
+ names = []
30
+ values = []
31
+ # Sort the keys so that they are consistent across processes
32
+ for k in sorted(input_dict.keys()):
33
+ names.append(k)
34
+ values.append(input_dict[k])
35
+ stacked_values = torch.stack(values, dim=0)
36
+ dist.all_reduce(stacked_values)
37
+ if average:
38
+ stacked_values /= world_size
39
+ reduced_dict = {k: v for k, v in zip(names, stacked_values)}
40
+ return reduced_dict
41
+
42
+
43
+ def _simple_gather_all_tensors(result: torch.Tensor, group: Any, world_size: int) -> List[torch.Tensor]:
44
+ gathered_result = [torch.zeros_like(result) for _ in range(world_size)]
45
+ dist.all_gather(gathered_result, result, group)
46
+ return gathered_result
47
+
48
+
49
+ def gather_all_tensors(result: torch.Tensor, group: Optional[Any] = None) -> List[torch.Tensor]:
50
+ """
51
+ Copied from https://github.com/Lightning-AI/torchmetrics/blob/master/src/torchmetrics/utilities/distributed.py
52
+ Gather all tensors from several ddp processes onto a list that is broadcasted to all processes.
53
+
54
+ Works on tensors that have the same number of dimensions, but where each dimension may differ. In this case
55
+ tensors are padded, gathered and then trimmed to secure equal workload for all processes.
56
+
57
+ Args:
58
+ result: the value to sync
59
+ group: the process group to gather results from. Defaults to all processes (world)
60
+
61
+ Return:
62
+ list with size equal to the process group where element i corresponds to result tensor from process i
63
+ """
64
+ if group is None:
65
+ group = get_default_process_group()
66
+
67
+ # convert tensors to contiguous format
68
+ result = result.contiguous()
69
+
70
+ world_size = get_world_size()
71
+ dist.barrier(group=group)
72
+
73
+ # if the tensor is scalar, things are easy
74
+ if result.ndim == 0:
75
+ return _simple_gather_all_tensors(result, group, world_size)
76
+
77
+ # 1. Gather sizes of all tensors
78
+ local_size = torch.tensor(result.shape, device=result.device)
79
+ local_sizes = [torch.zeros_like(local_size) for _ in range(world_size)]
80
+ dist.all_gather(local_sizes, local_size, group=group)
81
+ max_size = torch.stack(local_sizes).max(dim=0).values
82
+ all_sizes_equal = all(all(ls == max_size) for ls in local_sizes)
83
+
84
+ # 2. If shapes are all the same, then do a simple gather:
85
+ if all_sizes_equal:
86
+ return _simple_gather_all_tensors(result, group, world_size)
87
+
88
+ # 3. If not, we need to pad each local tensor to maximum size, gather and then truncate
89
+ pad_dims = []
90
+ pad_by = (max_size - local_size).detach().cpu()
91
+ for val in reversed(pad_by):
92
+ pad_dims.append(0)
93
+ pad_dims.append(val.item())
94
+ result_padded = F.pad(result, pad_dims)
95
+ gathered_result = [torch.zeros_like(result_padded) for _ in range(world_size)]
96
+ dist.all_gather(gathered_result, result_padded, group)
97
+ for idx, item_size in enumerate(local_sizes):
98
+ slice_param = [slice(dim_size) for dim_size in item_size]
99
+ gathered_result[idx] = gathered_result[idx][slice_param]
100
+ return gathered_result
dinov3/distributed/torch_distributed_wrapper.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ import os
8
+ import random
9
+ import socket
10
+ import subprocess
11
+ from datetime import timedelta
12
+ from enum import Enum
13
+ from typing import List, Sequence
14
+
15
+ import torch
16
+ import torch.distributed as dist
17
+
18
+ logger = logging.getLogger("dinov3")
19
+
20
+ _DEFAULT_PROCESS_GROUP = None
21
+ _PROCESS_SUBGROUP = None
22
+ _BUILTIN_PRINT = None
23
+
24
+
25
+ def is_distributed_enabled() -> bool:
26
+ """
27
+ Returns:
28
+ True if distributed training is enabled.
29
+ """
30
+ return dist.is_available() and dist.is_initialized()
31
+
32
+
33
+ def get_rank(group=None) -> int:
34
+ """
35
+ Returns:
36
+ The rank of the current process within the specified process group.
37
+ """
38
+ if not is_distributed_enabled():
39
+ return 0
40
+ return dist.get_rank(group=group)
41
+
42
+
43
+ def get_world_size(group=None) -> int:
44
+ """
45
+ Returns:
46
+ The number of processes in the specified process group.
47
+ """
48
+ if not is_distributed_enabled():
49
+ return 1
50
+ return dist.get_world_size(group=group)
51
+
52
+
53
+ def is_main_process(group=None) -> bool:
54
+ """
55
+ Returns:
56
+ True if the current process is the main one in the specified process group.
57
+ """
58
+ return get_rank(group) == 0
59
+
60
+
61
+ def save_in_main_process(*args, **kwargs) -> None:
62
+ """Utility function to save only from the main process."""
63
+ group = kwargs.pop("group", None)
64
+ if not is_main_process(group):
65
+ return
66
+ torch.save(*args, **kwargs)
67
+
68
+
69
+ def _restrict_print_to_main_process() -> None:
70
+ """This function disables printing when not in the main process."""
71
+ import builtins as __builtin__
72
+
73
+ global _BUILTIN_PRINT
74
+ _BUILTIN_PRINT = __builtin__.print
75
+
76
+ def print(*args, **kwargs):
77
+ force = kwargs.pop("force", False)
78
+ if is_main_process() or force:
79
+ _BUILTIN_PRINT(*args, **kwargs)
80
+
81
+ __builtin__.print = print
82
+
83
+
84
+ def _get_master_port(seed: int = 0) -> int:
85
+ MIN_MASTER_PORT, MAX_MASTER_PORT = (20_000, 60_000)
86
+
87
+ master_port_str = os.environ.get("MASTER_PORT")
88
+ if master_port_str is None:
89
+ rng = random.Random(seed)
90
+ return rng.randint(MIN_MASTER_PORT, MAX_MASTER_PORT)
91
+
92
+ return int(master_port_str)
93
+
94
+
95
+ def _get_available_port() -> int:
96
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
97
+ # A "" host address means INADDR_ANY i.e. binding to all interfaces.
98
+ # Note this is not compatible with IPv6.
99
+ s.bind(("", 0))
100
+ port = s.getsockname()[1]
101
+ return port
102
+
103
+
104
+ def _parse_slurm_node_list(s: str) -> List[str]:
105
+ return subprocess.check_output(["scontrol", "show", "hostnames", s], text=True).splitlines()
106
+
107
+
108
+ class JobType(Enum):
109
+ TORCHELASTIC = "TorchElastic"
110
+ SLURM = "Slurm"
111
+ MANUAL = "manual"
112
+
113
+
114
+ class TorchDistributedEnvironment:
115
+ """
116
+ Helper class to get (and set) distributed job information from the
117
+ environment. Identifies and supports (in this order):
118
+ - TorchElastic,
119
+ - Slurm,
120
+ - Manual launch (single-node).
121
+ """
122
+
123
+ def __init__(self):
124
+ if "TORCHELASTIC_RUN_ID" in os.environ:
125
+ # TorchElastic job created with torchrun
126
+ self.job_id = os.environ["TORCHELASTIC_RUN_ID"]
127
+ self.job_type = JobType.TORCHELASTIC
128
+
129
+ self.master_addr = os.environ["MASTER_ADDR"]
130
+ self.master_port = int(os.environ["MASTER_PORT"])
131
+ self.rank = int(os.environ["RANK"])
132
+ self.world_size = int(os.environ["WORLD_SIZE"])
133
+ self.local_rank = int(os.environ["LOCAL_RANK"])
134
+ self.local_world_size = int(os.environ["LOCAL_WORLD_SIZE"])
135
+ elif "SLURM_JOB_ID" in os.environ:
136
+ # Slurm job created with sbatch, submitit, etc...
137
+ self.job_id = int(os.environ["SLURM_JOB_ID"])
138
+ self.job_type = JobType.SLURM
139
+
140
+ node_count = int(os.environ["SLURM_JOB_NUM_NODES"])
141
+ nodes = _parse_slurm_node_list(os.environ["SLURM_JOB_NODELIST"])
142
+ assert len(nodes) == node_count
143
+
144
+ self.master_addr = nodes[0]
145
+ self.master_port = _get_master_port(seed=self.job_id)
146
+ self.rank = int(os.environ["SLURM_PROCID"])
147
+ self.world_size = int(os.environ["SLURM_NTASKS"])
148
+ self.local_rank = int(os.environ["SLURM_LOCALID"])
149
+ self.local_world_size = self.world_size // node_count
150
+ else:
151
+ # Single node and single job launched manually
152
+ self.job_id = None
153
+ self.job_type = JobType.MANUAL
154
+
155
+ self.master_addr = "127.0.0.1"
156
+ self.master_port = _get_available_port()
157
+ self.rank = 0
158
+ self.world_size = 1
159
+ self.local_rank = 0
160
+ self.local_world_size = 1
161
+
162
+ assert self.rank < self.world_size
163
+ assert self.local_rank < self.local_world_size
164
+
165
+ def export(
166
+ self,
167
+ *,
168
+ overwrite: bool,
169
+ nccl_async_error_handling: bool = False,
170
+ ) -> "TorchDistributedEnvironment":
171
+ # See the "Environment variable initialization" section from
172
+ # https://pytorch.org/docs/stable/distributed.html for the complete list of
173
+ # environment variables required for the env:// initialization method.
174
+ env_vars = {
175
+ "MASTER_ADDR": self.master_addr,
176
+ "MASTER_PORT": str(self.master_port),
177
+ "RANK": str(self.rank),
178
+ "WORLD_SIZE": str(self.world_size),
179
+ "LOCAL_RANK": str(self.local_rank),
180
+ "LOCAL_WORLD_SIZE": str(self.local_world_size),
181
+ }
182
+ if nccl_async_error_handling:
183
+ env_vars.update(
184
+ {
185
+ "TORCH_NCCL_ASYNC_ERROR_HANDLING": "1", # "TORCH_" prefix added in PyTorch 2.2
186
+ }
187
+ )
188
+
189
+ if not overwrite:
190
+ for k, v in env_vars.items():
191
+ # Only check for difference with preset environment variables
192
+ if k not in os.environ:
193
+ continue
194
+ if os.environ[k] == v:
195
+ continue
196
+ raise RuntimeError(f"Cannot export environment variables as {k} is already set")
197
+
198
+ os.environ.update(env_vars)
199
+ return self
200
+
201
+ @property
202
+ def is_main_process(self) -> bool:
203
+ return self.rank == 0
204
+
205
+ def __str__(self):
206
+ return (
207
+ f"{self.job_type.value} job "
208
+ + (f"({self.job_id}) " if self.job_id else "")
209
+ + f"using {self.master_addr}:{self.master_port} " # noqa: E231
210
+ f"(rank={self.rank}, world size={self.world_size})"
211
+ )
212
+
213
+ def __repr__(self):
214
+ return (
215
+ f"{self.__class__.__name__}("
216
+ f"master_addr={self.master_addr}," # noqa: E231
217
+ f"master_port={self.master_port}," # noqa: E231
218
+ f"rank={self.rank}," # noqa: E231
219
+ f"world_size={self.world_size}," # noqa: E231
220
+ f"local_rank={self.local_rank}," # noqa: E231
221
+ f"local_world_size={self.local_world_size}"
222
+ ")"
223
+ )
224
+
225
+
226
+ def enable_distributed(
227
+ *,
228
+ set_cuda_current_device: bool = True,
229
+ overwrite: bool = False,
230
+ nccl_async_error_handling: bool = False,
231
+ restrict_print_to_main_process: bool = True,
232
+ timeout: timedelta | None = None,
233
+ ):
234
+ """Enable distributed mode.
235
+
236
+ Args:
237
+ set_cuda_current_device: If True, call torch.cuda.set_device() to set the
238
+ current PyTorch CUDA device to the one matching the local rank.
239
+ overwrite: If True, overwrites already set variables. Else fails.
240
+ nccl_async_error_handling: Enables NCCL asynchronous error handling. As a
241
+ side effect, this enables timing out PyTorch distributed operations
242
+ after a default 30 minutes delay).
243
+ restrict_print_to_main_process: If True, the print function of non-main processes
244
+ (ie rank>0) is disabled. Use print(..., force=True) to print anyway.
245
+ If False, nothing is changed and all processes can print as usual.
246
+ timeout: Timeout for operations executed against the process group.
247
+ Default value is 10 minutes for NCCL and 30 minutes for other backends.
248
+ """
249
+ global _DEFAULT_PROCESS_GROUP
250
+
251
+ if _DEFAULT_PROCESS_GROUP is not None:
252
+ raise RuntimeError("Distributed mode has already been enabled")
253
+
254
+ torch_env = TorchDistributedEnvironment()
255
+ logger.info(f"PyTorch distributed environment: {torch_env}")
256
+ torch_env.export(
257
+ overwrite=overwrite,
258
+ nccl_async_error_handling=nccl_async_error_handling,
259
+ )
260
+
261
+ if set_cuda_current_device:
262
+ torch.cuda.set_device(torch_env.local_rank)
263
+
264
+ dist.init_process_group(backend="nccl", timeout=timeout)
265
+ dist.barrier()
266
+
267
+ if restrict_print_to_main_process:
268
+ _restrict_print_to_main_process()
269
+
270
+ # Finalize setup
271
+ _DEFAULT_PROCESS_GROUP = torch.distributed.group.WORLD
272
+
273
+
274
+ def get_default_process_group():
275
+ return _DEFAULT_PROCESS_GROUP
276
+
277
+
278
+ def disable_distributed() -> None:
279
+ global _BUILTIN_PRINT
280
+ if _BUILTIN_PRINT is not None:
281
+ import builtins as __builtin__
282
+
283
+ __builtin__.print = _BUILTIN_PRINT
284
+
285
+ global _PROCESS_SUBGROUP
286
+ # checking here because get_process_subgroup can return _DEFAULT_PROCESS_GROUP
287
+ if _PROCESS_SUBGROUP is not None:
288
+ torch.distributed.destroy_process_group(_PROCESS_SUBGROUP)
289
+ _PROCESS_SUBGROUP = None
290
+
291
+ global _DEFAULT_PROCESS_GROUP
292
+ if _DEFAULT_PROCESS_GROUP is not None: # not initialized
293
+ torch.distributed.destroy_process_group(_DEFAULT_PROCESS_GROUP)
294
+ _DEFAULT_PROCESS_GROUP = None
295
+
296
+
297
+ def new_subgroups(all_subgroup_ranks: Sequence[Sequence[int]]):
298
+ """Create new process subgroups according to the provided specification.
299
+
300
+ Args:
301
+ all_subgroup_ranks: a sequence of rank sequences (first rank, ..., last rank),
302
+ one for each process subgroup. Example: ((0, 1), (2, 3), (4, 5, 6, 7)).
303
+
304
+ Note:
305
+ This is similar to the (non-documented) new_subgroups_by_enumeration().
306
+ This should be called once (and not sequentially) to create all subgroups.
307
+ """
308
+ all_ranks = tuple(rank for subgroup_ranks in all_subgroup_ranks for rank in subgroup_ranks)
309
+ rank = get_rank()
310
+ assert len(all_ranks) == len(set(all_ranks))
311
+ assert rank in all_ranks
312
+
313
+ global _PROCESS_SUBGROUP
314
+ assert _PROCESS_SUBGROUP is None
315
+
316
+ for subgroup_ranks in all_subgroup_ranks:
317
+ subgroup = torch.distributed.new_group(subgroup_ranks)
318
+ if rank in subgroup_ranks:
319
+ _PROCESS_SUBGROUP = subgroup
320
+
321
+
322
+ def get_process_subgroup():
323
+ """
324
+ Returns:
325
+ The process subgroup of this rank (or None).
326
+ """
327
+ return _PROCESS_SUBGROUP or _DEFAULT_PROCESS_GROUP
328
+
329
+
330
+ def get_subgroup_rank() -> int:
331
+ """
332
+ Returns:
333
+ The rank of the current process within its process subgroup.
334
+ """
335
+ return get_rank(group=get_process_subgroup())
336
+
337
+
338
+ def get_subgroup_size() -> int:
339
+ """
340
+ Returns:
341
+ The number of processes in the process subgroup
342
+ """
343
+ return get_world_size(group=get_process_subgroup())
344
+
345
+
346
+ def is_subgroup_main_process() -> bool:
347
+ """
348
+ Returns:
349
+ True if the current process is the main one within its process subgroup.
350
+ """
351
+ return get_rank(group=get_process_subgroup()) == 0
dinov3/env/__init__.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import contextlib
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from typing import Optional
11
+
12
+ import submitit.helpers
13
+
14
+ logger = logging.getLogger("dinov3")
15
+
16
+
17
+ @contextlib.contextmanager
18
+ def clean_env():
19
+ try:
20
+ # Hide torch.compile() variables from the launched evals
21
+ extra_names = ("TRITON_CACHE_DIR", "TORCHINDUCTOR_CACHE_DIR")
22
+ ctx = submitit.helpers.clean_env(extra_names=extra_names)
23
+ except TypeError as e:
24
+ logger.warning("Update submitit to the latest main branch\n%s", e)
25
+ ctx = submitit.helpers.clean_env()
26
+ with ctx:
27
+ yield
28
+
29
+
30
+ def set_triton_cache_dir(cache_dir: Optional[str] = None) -> None:
31
+ if cache_dir is None:
32
+ cache_dir = tempfile.mkdtemp()
33
+ os.environ["TRITON_CACHE_DIR"] = cache_dir
dinov3/eval/.DS_Store ADDED
Binary file (6.15 kB). View file
 
dinov3/eval/accumulators.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ from collections import defaultdict
8
+ from typing import Dict, List, Optional
9
+
10
+ import torch
11
+ from torch import Tensor
12
+
13
+ from dinov3.distributed import gather_all_tensors # Gathers tensors of different sizes
14
+
15
+ logger = logging.getLogger("dinov3")
16
+
17
+
18
+ def _cat_and_gather_tensor_list(tensor_list: List[Tensor]) -> Tensor:
19
+ local_cat = torch.cat(tensor_list)
20
+ return torch.cat(gather_all_tensors(local_cat))
21
+
22
+
23
+ class Accumulator:
24
+ def __init__(self) -> None:
25
+ pass
26
+
27
+ def update(self, preds: Tensor, target: Tensor, index: Tensor) -> None:
28
+ raise NotImplementedError
29
+
30
+ def accumulate(self) -> Optional[Dict[str, Tensor]]:
31
+ raise NotImplementedError
32
+
33
+
34
+ class NoOpAccumulator(Accumulator):
35
+ def __init__(self) -> None:
36
+ pass
37
+
38
+ def update(self, preds: Tensor, target: Tensor, index: Tensor) -> None:
39
+ pass
40
+
41
+ def accumulate(self) -> None:
42
+ return None
43
+
44
+
45
+ class ResultsAccumulator(Accumulator):
46
+ """
47
+ Accumulate predictions and targets across processes
48
+ """
49
+
50
+ def __init__(self) -> None:
51
+ self._local_values: Dict[str, List[Tensor]] = defaultdict(list)
52
+ self._gathered_values: Dict[str, Tensor] = {}
53
+ self._gathered = False
54
+
55
+ def update(self, preds: Tensor, target: Tensor, index: Tensor) -> None:
56
+ assert len(preds) == len(target) == len(index)
57
+ assert not self._gathered, "Tensors have already been gathered in this helper"
58
+ self._local_values["preds"].append(preds)
59
+ self._local_values["target"].append(target)
60
+ self._local_values["index"].append(index)
61
+ self._gathered = False
62
+
63
+ def _gather_tensors(self):
64
+ for k, tensor_list in self._local_values.items():
65
+ self._gathered_values[k] = _cat_and_gather_tensor_list(tensor_list)
66
+ self._gathered = True
67
+
68
+ def accumulate(self) -> Dict[str, Tensor]:
69
+ if not self._gathered:
70
+ self._gather_tensors()
71
+ preds, target, index = [self._gathered_values[k] for k in ["preds", "target", "index"]]
72
+ assert len(preds) == len(target) == len(index) and index.min() == 0
73
+ preds_ordered = torch.zeros((index.max() + 1, *preds.shape[1:]), dtype=preds.dtype, device=preds.device)
74
+ preds_ordered[index] = preds
75
+ target_ordered = torch.zeros((index.max() + 1, *target.shape[1:]), dtype=target.dtype, device=target.device)
76
+ target_ordered[index] = target
77
+ return {"preds": preds_ordered, "target": target_ordered}
dinov3/eval/data.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ from functools import lru_cache
8
+ from typing import Any, Callable, Optional
9
+
10
+ import numpy as np
11
+ import torch
12
+ from torch.utils.data import Subset
13
+ from torchvision.datasets.vision import StandardTransform
14
+
15
+ from dinov3.eval.utils import extract_features
16
+
17
+ logger = logging.getLogger("dinov3")
18
+
19
+
20
+ class SubsetEx(Subset):
21
+ def _get_actual_index(self, index: int) -> int:
22
+ return self.indices[index]
23
+
24
+ def get_target(self, index: int) -> Any:
25
+ actual_index = self._get_actual_index(index)
26
+ return self.dataset.get_target(actual_index)
27
+
28
+ @property
29
+ def transforms(self):
30
+ return self.dataset.transforms
31
+
32
+
33
+ def get_target_transform(dataset) -> Optional[Callable]:
34
+ if hasattr(dataset, "transforms"):
35
+ if isinstance(dataset.transforms, StandardTransform):
36
+ return dataset.transforms.target_transform
37
+ raise ValueError("Dataset has a non-standard .transforms property")
38
+ if hasattr(dataset, "target_transform"):
39
+ return dataset.target_transform
40
+ return None
41
+
42
+
43
+ @lru_cache(maxsize=1)
44
+ def get_labels(dataset) -> torch.Tensor:
45
+ """
46
+ Get the labels of a classification dataset, as a Tensor, using the `get_targets` method
47
+ if it is present or loading the labels one by one with `get_target`, if it exists.
48
+ If the dataset has a target transform, iterate over the whole dataset to get the
49
+ transformed labels for each element, then stack them as a torch tensor.
50
+ """
51
+ logger.info("Getting dataset labels ...")
52
+ if hasattr(dataset, "get_targets") or hasattr(dataset, "get_target"):
53
+ if hasattr(dataset, "get_targets"): # Returns a np.array
54
+ labels = dataset.get_targets()
55
+ elif hasattr(dataset, "get_target"):
56
+ labels = [dataset.get_target(i) for i in range(len(dataset))]
57
+ target_transform = get_target_transform(dataset)
58
+ if target_transform is not None:
59
+ labels = [target_transform(label) for label in labels]
60
+ else:
61
+ # Target transform is applied in this case
62
+ labels = [dataset[i][1] for i in range(len(dataset))]
63
+ return torch.stack([torch.tensor(label, dtype=int) for label in labels])
64
+
65
+
66
+ def get_num_classes(dataset) -> int:
67
+ """
68
+ Get the labels of a dataset and compute the number of classes
69
+ """
70
+ labels = get_labels(dataset)
71
+ if len(labels.shape) > 1:
72
+ return int(labels.shape[1])
73
+ return int(labels.max() + 1)
74
+
75
+
76
+ def create_class_indices_mapping(labels: torch.Tensor) -> dict[int, torch.Tensor]:
77
+ """
78
+ Efficiently creates a mapping between the labels and tensors containing
79
+ the indices of all the dataset elements that share this label.
80
+ In the case of multiple labels, it is not guaranteed that there
81
+ will be exactly the specified percentage of labels.
82
+ """
83
+ if len(labels.shape) > 1: # labels are a one-hot encoding
84
+ assert len(labels.shape) == 2
85
+ sorted_labels, indices = torch.nonzero(labels.T, as_tuple=True)
86
+ else:
87
+ sorted_labels, indices = torch.sort(labels, stable=True)
88
+ unique_labels, counts = torch.unique_consecutive(sorted_labels, return_counts=True)
89
+ mapping = dict(zip(unique_labels.tolist(), torch.split(indices, counts.tolist())))
90
+ return mapping
91
+
92
+
93
+ def _shuffle_dataset(dataset: torch.Tensor, seed: int = 0):
94
+ """
95
+ Shuffling a dataset by subsetting it with a random permutation of its indices
96
+ """
97
+ random_generator = torch.Generator()
98
+ random_generator.manual_seed(seed)
99
+ random_indices = torch.randperm(len(dataset), generator=random_generator)
100
+ return SubsetEx(dataset, random_indices)
101
+
102
+
103
+ def _subset_dataset_per_class(
104
+ class_indices_mapping: dict[int, torch.Tensor],
105
+ n_or_percent_per_class: int | float,
106
+ dataset_size: int,
107
+ seed: int = 0,
108
+ is_percent: bool = False,
109
+ ) -> torch.Tensor:
110
+ """
111
+ Helper function to select a percentage of a dataset, equally distributed across classes,
112
+ or to take the same number of elements from each class of the dataset.
113
+ Returns a boolean mask tensor being True at indices of selected elements
114
+ """
115
+
116
+ random_generator = torch.Generator()
117
+ random_generator.manual_seed(seed)
118
+
119
+ final_indices_bool = torch.zeros(dataset_size, dtype=bool)
120
+ for class_indices in class_indices_mapping.values():
121
+ # Select at least one element
122
+ n_for_class = max(int(len(class_indices) * n_or_percent_per_class), 1) if is_percent else n_or_percent_per_class
123
+ assert isinstance(n_for_class, int)
124
+ filtered_index = torch.randperm(len(class_indices), generator=random_generator)[:n_for_class]
125
+ final_indices_bool[class_indices[filtered_index]] = True
126
+ return final_indices_bool
127
+
128
+
129
+ def _multilabel_rebalance_subset(
130
+ class_indices_mapping: dict[int, torch.Tensor],
131
+ n_or_percent_per_class: int | float,
132
+ labels: torch.Tensor,
133
+ indices_bool: torch.Tensor,
134
+ dataset_size: int,
135
+ seed: int = 0,
136
+ ) -> torch.Tensor:
137
+ """
138
+ Helper function to refine a subset of a multi-label dataset (indices_bool)
139
+ to better match a target percentage of labels.
140
+ Returns a boolean mask tensor being True at indices of selected elements.
141
+ """
142
+
143
+ # Compute the number of selected labels in indices_bool
144
+ num_total_labels = labels.sum()
145
+ num_wanted_labels = int(num_total_labels * n_or_percent_per_class)
146
+ num_selected_labels = (labels[indices_bool] > 0).sum()
147
+ logger.info(f" {num_selected_labels} labels instead of {num_wanted_labels}")
148
+
149
+ # Compute a new percentage and new set selecting less images, therefore less labels, to match approximatelly the exact percentage of labels selected
150
+ n_or_percent_per_class = n_or_percent_per_class / (num_selected_labels / num_wanted_labels)
151
+ final_indices_bool = _subset_dataset_per_class(
152
+ class_indices_mapping, n_or_percent_per_class, dataset_size, seed, True
153
+ )
154
+
155
+ # Compute the number of labels finally used
156
+ num_selected_labels = (labels[final_indices_bool] > 0).sum()
157
+ logger.info(f" {num_selected_labels} labels instead of {num_wanted_labels}")
158
+
159
+ return final_indices_bool
160
+
161
+
162
+ def split_train_val_datasets(train_dataset, split_percentage: float = 0.1, shuffle_train: bool = True):
163
+ """
164
+ Splitting a percent of the train dataset to choose hyperparameters, taking the same percentage for each class.
165
+ If `shuffle` is False, taking the first elements of each class as the validaton set.
166
+ """
167
+ assert 0 < split_percentage < 1
168
+ logger.info(f"Selecting {int(split_percentage * 100)}% of the train dataset as the validation set")
169
+ if shuffle_train:
170
+ logger.info("Shuffling train dataset before splitting in train and validation sets")
171
+ train_dataset = _shuffle_dataset(train_dataset)
172
+ train_labels = get_labels(train_dataset)
173
+ class_indices_mapping = create_class_indices_mapping(train_labels)
174
+ val_mask = torch.zeros(len(train_labels), dtype=bool)
175
+ for class_indices in class_indices_mapping.values():
176
+ # If there is only one element, it goes in the train set
177
+ n_for_val = max(1, int(split_percentage * len(class_indices))) if len(class_indices) > 1 else 0
178
+ val_mask[class_indices[:n_for_val]] = True
179
+
180
+ val_dataset = SubsetEx(train_dataset, val_mask.nonzero().flatten())
181
+ train_dataset = SubsetEx(train_dataset, (~val_mask).nonzero().flatten())
182
+ return train_dataset, val_dataset
183
+
184
+
185
+ def create_train_dataset_dict(
186
+ train_dataset,
187
+ few_shot_eval: bool = False,
188
+ few_shot_k_or_percent: float | None = None,
189
+ few_shot_n_tries: int = 1,
190
+ ) -> dict[int, dict[int, Any]]:
191
+ """
192
+ Randomly split a dataset for few-shot evaluation, with `few_shot_k_or_percent` being
193
+ n elements or x% of a class. Produces a dict, which keys are number of random "tries"
194
+ and values are the dataset subset for this "try".
195
+
196
+ Format is {"nth-try": dataset}
197
+ """
198
+ if few_shot_eval is False:
199
+ assert few_shot_k_or_percent is None
200
+ assert few_shot_n_tries == 1
201
+ return {0: train_dataset}
202
+
203
+ assert few_shot_k_or_percent is not None
204
+ train_labels = get_labels(train_dataset)
205
+ class_indices_mapping = create_class_indices_mapping(train_labels)
206
+ train_dataset_dict: dict[int, Any] = {}
207
+ is_percent = few_shot_k_or_percent < 1
208
+ if not is_percent:
209
+ few_shot_k_or_percent = int(few_shot_k_or_percent)
210
+
211
+ for t in range(few_shot_n_tries):
212
+ t_subset_bool = _subset_dataset_per_class(
213
+ class_indices_mapping=class_indices_mapping,
214
+ n_or_percent_per_class=few_shot_k_or_percent,
215
+ dataset_size=len(train_labels),
216
+ is_percent=is_percent,
217
+ seed=t,
218
+ )
219
+ if len(train_labels.shape) > 1 and is_percent:
220
+ t_subset_bool = _multilabel_rebalance_subset(
221
+ class_indices_mapping=class_indices_mapping,
222
+ n_or_percent_per_class=few_shot_k_or_percent,
223
+ dataset_size=len(train_labels),
224
+ labels=train_labels,
225
+ indices_bool=t_subset_bool,
226
+ seed=t,
227
+ )
228
+ train_dataset_dict[t] = SubsetEx(train_dataset, t_subset_bool.nonzero().flatten())
229
+ return train_dataset_dict
230
+
231
+
232
+ def extract_features_for_dataset_dict(
233
+ model, dataset_dict: dict[int, dict[int, Any]], batch_size: int, num_workers: int, gather_on_cpu=False
234
+ ) -> dict[int, dict[str, torch.Tensor]]:
235
+ """
236
+ Extract features for each subset of dataset in the context of few-shot evaluations
237
+ """
238
+ few_shot_data_dict: dict[int, dict[str, torch.Tensor]] = {}
239
+ for try_n, dataset in dataset_dict.items():
240
+ features, labels = extract_features(model, dataset, batch_size, num_workers, gather_on_cpu=gather_on_cpu)
241
+ few_shot_data_dict[try_n] = {"train_features": features, "train_labels": labels}
242
+ return few_shot_data_dict
243
+
244
+
245
+ def pad_multilabel_and_collate(batch, pad_value=-1):
246
+ """
247
+ This method pads and collates a batch of (image, (index, target)) tuples, coming from
248
+ DatasetWithEnumeratedTargets, with targets that are list of potentially varying sizes.
249
+ The targets are padded to the length of the longest target list in the batch.
250
+ """
251
+ maxlen = max(len(targets) for _, (_, targets) in batch)
252
+ padded_batch = [
253
+ (image, (index, np.pad(targets, (0, maxlen - len(targets)), constant_values=pad_value)))
254
+ for image, (index, targets) in batch
255
+ ]
256
+ return torch.utils.data.default_collate(padded_batch)
dinov3/eval/dense/depth/models/__init__.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import torch
7
+ from dinov3.eval.dense.depth.utils import cast_to
8
+
9
+ from .dpt_head import DPTHead
10
+ from .encoder import BackboneLayersSet, DinoVisionTransformerWrapper, PatchSizeAdaptationStrategy
11
+
12
+
13
+ class FeaturesToDepth(torch.nn.Module):
14
+ def __init__(
15
+ self,
16
+ min_depth=0.001,
17
+ max_depth=80,
18
+ bins_strategy="linear",
19
+ norm_strategy="linear",
20
+ ):
21
+ """
22
+ Module which converts a feature maps into a depth map
23
+
24
+ Args:
25
+ min_depth (float): minimum depth, used to calibrate the depth range
26
+ max_depth (float): maximum depth, used to calibrate the depth range
27
+ bins_strategy (str): Choices are 'linear' or 'log', for Uniform or Scale Invariant distributions for depth bins.
28
+ See AdaBins [1] for more details.
29
+ norm_strategy (str): Choices are 'linear', 'softmax' or 'sigmoid', for the conversion of features to depth logits
30
+ scale_up (bool): If true, and only if regression by classification is not used, the result is multiplied by max_depth
31
+
32
+
33
+ Example:
34
+ x = depth_model(input_image) # N C H W
35
+ - If pure regression (C == 1), depth is obtained by scaling and/or shifting x
36
+ - If C > 1, bins are used:
37
+ Depth is obtained as a weighted sum of depth bins, where weights are predicted logits. (see AdaBins [1] for more details)
38
+
39
+ [1] AdaBins: https://github.com/shariqfarooq123/AdaBins
40
+ """
41
+ super().__init__()
42
+ self.min_depth = min_depth
43
+ self.max_depth = max_depth
44
+ assert bins_strategy in ["linear", "log"], "Support bins_strategy: linear, log"
45
+ assert norm_strategy in ["linear", "softmax", "sigmoid"], "Support norm_strategy: linear, softmax, sigmoid"
46
+
47
+ self.bins_strategy = bins_strategy
48
+ self.norm_strategy = norm_strategy
49
+
50
+ def forward(self, x):
51
+ n_bins = x.shape[1] # N n_bins H W
52
+ if n_bins > 1:
53
+ if self.bins_strategy == "linear":
54
+ bins = torch.linspace(self.min_depth, self.max_depth, n_bins, device=x.device)
55
+ elif self.bins_strategy == "log":
56
+ bins = torch.linspace(
57
+ torch.log(torch.tensor(self.min_depth)),
58
+ torch.log(torch.tensor(self.max_depth)),
59
+ n_bins,
60
+ device=x.device,
61
+ )
62
+ bins = torch.exp(bins)
63
+
64
+ # following Adabins, default linear
65
+ if self.norm_strategy == "linear":
66
+ logit = torch.relu(x)
67
+ eps = 0.1
68
+ logit = logit + eps
69
+ logit = logit / logit.sum(dim=1, keepdim=True)
70
+ elif self.norm_strategy == "softmax":
71
+ logit = torch.softmax(x, dim=1)
72
+ elif self.norm_strategy == "sigmoid":
73
+ logit = torch.sigmoid(x)
74
+ logit = logit / logit.sum(dim=1, keepdim=True)
75
+
76
+ output = torch.einsum("ikmn,k->imn", [logit, bins]).unsqueeze(dim=1)
77
+ else:
78
+ # standard regression
79
+ output = torch.relu(x) + self.min_depth
80
+ return output
81
+
82
+
83
+ def make_head(
84
+ embed_dims: int | list[int],
85
+ n_output_channels: int,
86
+ use_batchnorm: bool = False,
87
+ use_cls_token: bool = False,
88
+ # upsample: int = 4,
89
+ head_type: str = "linear",
90
+ **kwargs,
91
+ ) -> torch.nn.Module:
92
+
93
+ if isinstance(embed_dims, int):
94
+ embed_dims = [embed_dims]
95
+
96
+ if head_type == "linear":
97
+ raise NotImplementedError
98
+ elif head_type == "dpt":
99
+ decoder = DPTHead(
100
+ in_channels=embed_dims,
101
+ n_output_channels=n_output_channels,
102
+ readout_type="project" if use_cls_token else "ignore",
103
+ use_batchnorm=use_batchnorm,
104
+ **kwargs, # TODO add here post_process_channels, n_hidden_channels
105
+ )
106
+ else:
107
+ raise NotImplementedError("only linear and DPT head supported")
108
+ return decoder
109
+
110
+
111
+ class EncoderDecoder(torch.nn.Module):
112
+ def __init__(
113
+ self,
114
+ encoder: torch.nn.Module,
115
+ decoder: torch.nn.Module,
116
+ encoder_dtype=torch.float,
117
+ decoder_dtype=torch.float,
118
+ ):
119
+ super().__init__()
120
+ self.encoder = encoder
121
+ self.decoder = decoder
122
+ self.encoder_dtype = encoder_dtype
123
+ self.decoder_dtype = decoder_dtype
124
+ self.is_cuda = torch.cuda.is_available()
125
+
126
+ def forward(self, x):
127
+ with torch.autocast("cuda" if torch.cuda.is_available() else "cpu", enabled=False):
128
+ x = x.to(self.encoder_dtype)
129
+ x = self.encoder(x)
130
+ x = cast_to(x, self.decoder_dtype)
131
+ return self.decoder(x)
132
+
133
+
134
+ def build_depther(
135
+ backbone: torch.nn.Module,
136
+ backbone_out_layers: list[int] | BackboneLayersSet,
137
+ n_output_channels: int,
138
+ use_backbone_norm: bool = False,
139
+ use_batchnorm: bool = False,
140
+ use_cls_token: bool = False,
141
+ adapt_to_patch_size: PatchSizeAdaptationStrategy = PatchSizeAdaptationStrategy.CENTER_PADDING,
142
+ head_type: str = "dpt",
143
+ encoder_dtype: torch.dtype = torch.float,
144
+ decoder_dtype: torch.dtype = torch.float,
145
+ # depth args
146
+ min_depth: float = 0.001,
147
+ max_depth: float = 10.0,
148
+ bins_strategy: str = "linear",
149
+ norm_strategy: str = "linear",
150
+ **kwargs,
151
+ ):
152
+ encoder = DinoVisionTransformerWrapper(
153
+ backbone_model=backbone,
154
+ backbone_out_layers=backbone_out_layers,
155
+ use_backbone_norm=use_backbone_norm,
156
+ adapt_to_patch_size=adapt_to_patch_size,
157
+ )
158
+ encoder = encoder.to(encoder_dtype)
159
+ encoder.eval()
160
+
161
+ decoder = make_head(
162
+ encoder.embed_dims,
163
+ n_output_channels=n_output_channels,
164
+ use_batchnorm=use_batchnorm,
165
+ use_cls_token=use_cls_token,
166
+ head_type=head_type,
167
+ **kwargs,
168
+ )
169
+ decoder.eval()
170
+
171
+ features_to_depth = FeaturesToDepth(
172
+ min_depth=min_depth,
173
+ max_depth=max_depth,
174
+ bins_strategy=bins_strategy,
175
+ norm_strategy=norm_strategy,
176
+ )
177
+
178
+ return torch.nn.Sequential(
179
+ EncoderDecoder(
180
+ encoder,
181
+ decoder,
182
+ encoder_dtype=encoder_dtype,
183
+ decoder_dtype=decoder_dtype,
184
+ ),
185
+ features_to_depth,
186
+ )
dinov3/eval/dense/depth/models/dpt_head.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import torch
7
+ from torch import nn
8
+
9
+
10
+ def kaiming_init(
11
+ module: nn.Module,
12
+ a: float = 0,
13
+ mode: str = "fan_out",
14
+ nonlinearity: str = "relu",
15
+ bias: float = 0,
16
+ distribution: str = "normal",
17
+ ) -> None:
18
+ assert distribution in ["uniform", "normal"]
19
+ if hasattr(module, "weight") and module.weight is not None:
20
+ if distribution == "uniform":
21
+ nn.init.kaiming_uniform_(module.weight, a=a, mode=mode, nonlinearity=nonlinearity)
22
+ else:
23
+ nn.init.kaiming_normal_(module.weight, a=a, mode=mode, nonlinearity=nonlinearity)
24
+ if hasattr(module, "bias") and module.bias is not None:
25
+ nn.init.constant_(module.bias, bias)
26
+
27
+
28
+ def constant_init(module, val, bias=0):
29
+ if hasattr(module, "weight") and module.weight is not None:
30
+ nn.init.constant_(module.weight, val)
31
+ if hasattr(module, "bias") and module.bias is not None:
32
+ nn.init.constant_(module.bias, bias)
33
+
34
+
35
+ class ConvModule(nn.Module):
36
+ """A conv block that bundles conv/norm/activation layers.
37
+ This block simplifies the usage of convolution layers, which are commonly
38
+ used with a norm layer (e.g., BatchNorm) and activation layer (e.g., ReLU).
39
+ It is based upon three build methods: `build_conv_layer()`,
40
+ `build_norm_layer()` and `build_activation_layer()`.
41
+ Besides, we add some additional features in this module.
42
+ 1. Automatically set `bias` of the conv layer.
43
+ 2. Spectral norm is supported.
44
+ 3. More padding modes are supported. Before PyTorch 1.5, nn.Conv2d only
45
+ supports zero and circular padding, and we add "reflect" padding mode.
46
+ Args:
47
+ in_channels (int): Number of channels in the input feature map.
48
+ Same as that in ``nn._ConvNd``.
49
+ out_channels (int): Number of channels produced by the convolution.
50
+ Same as that in ``nn._ConvNd``.
51
+ kernel_size (int | tuple[int]): Size of the convolving kernel.
52
+ Same as that in ``nn._ConvNd``.
53
+ stride (int | tuple[int]): Stride of the convolution.
54
+ Same as that in ``nn._ConvNd``.
55
+ padding (int | tuple[int]): Zero-padding added to both sides of
56
+ the input. Same as that in ``nn._ConvNd``.
57
+ dilation (int | tuple[int]): Spacing between kernel elements.
58
+ Same as that in ``nn._ConvNd``.
59
+ groups (int): Number of blocked connections from input channels to
60
+ output channels. Same as that in ``nn._ConvNd``.
61
+ bias (bool | str): If specified as `auto`, it will be decided by the
62
+ norm_cfg. Bias will be set as True if `norm_cfg` is None, otherwise
63
+ False. Default: "auto".
64
+ conv_cfg (dict): Config dict for convolution layer. Default: None,
65
+ which means using conv2d.
66
+ norm_cfg (dict): Config dict for normalization layer. Default: None.
67
+ act_cfg (dict): Config dict for activation layer.
68
+ Default: dict(type='ReLU').
69
+ inplace (bool): Whether to use inplace mode for activation.
70
+ Default: True.
71
+ with_spectral_norm (bool): Whether use spectral norm in conv module.
72
+ Default: False.
73
+ padding_mode (str): If the `padding_mode` has not been supported by
74
+ current `Conv2d` in PyTorch, we will use our own padding layer
75
+ instead. Currently, we support ['zeros', 'circular'] with official
76
+ implementation and ['reflect'] with our own implementation.
77
+ Default: 'zeros'.
78
+ order (tuple[str]): The order of conv/norm/activation layers. It is a
79
+ sequence of "conv", "norm" and "act". Common examples are
80
+ ("conv", "norm", "act") and ("act", "conv", "norm").
81
+ Default: ('conv', 'norm', 'act').
82
+ """
83
+
84
+ _abbr_ = "conv_block"
85
+
86
+ def __init__(
87
+ self,
88
+ in_channels,
89
+ out_channels,
90
+ kernel_size,
91
+ stride=1,
92
+ padding=0,
93
+ dilation=1,
94
+ groups=1,
95
+ bias="auto",
96
+ conv_cfg=None,
97
+ norm_cfg=None,
98
+ act_cfg=dict(type="ReLU"),
99
+ inplace=True,
100
+ with_spectral_norm=False,
101
+ padding_mode="zeros",
102
+ order=("conv", "norm", "act"),
103
+ ):
104
+ super().__init__()
105
+ assert conv_cfg is None or isinstance(conv_cfg, dict)
106
+ assert norm_cfg is None or isinstance(norm_cfg, dict)
107
+ assert act_cfg is None or isinstance(act_cfg, dict)
108
+ official_padding_mode = ["zeros", "circular"]
109
+ self.conv_cfg = conv_cfg
110
+ self.norm_cfg = norm_cfg
111
+ self.act_cfg = act_cfg
112
+ self.inplace = inplace
113
+ self.with_spectral_norm = with_spectral_norm
114
+ self.with_explicit_padding = padding_mode not in official_padding_mode
115
+ self.order = order
116
+ assert isinstance(self.order, tuple) and len(self.order) == 3
117
+ assert set(order) == {"conv", "norm", "act"}
118
+
119
+ self.with_norm = norm_cfg is not None
120
+ self.with_activation = act_cfg is not None
121
+ # if the conv layer is before a norm layer, bias is unnecessary.
122
+ if bias == "auto":
123
+ bias = not self.with_norm
124
+ self.with_bias = bias
125
+
126
+ # if self.with_explicit_padding:
127
+ # pad_cfg = dict(type=padding_mode)
128
+ # self.padding_layer = build_padding_layer(pad_cfg, padding)
129
+ # to do Camille put back
130
+
131
+ # reset padding to 0 for conv module
132
+ conv_padding = 0 if self.with_explicit_padding else padding
133
+ # build convolution layer
134
+ self.conv = nn.Conv2d( # build_conv_layer(#conv_cfg,
135
+ in_channels,
136
+ out_channels,
137
+ kernel_size,
138
+ stride=stride,
139
+ padding=conv_padding,
140
+ dilation=dilation,
141
+ groups=groups,
142
+ bias=bias,
143
+ )
144
+ # export the attributes of self.conv to a higher level for convenience
145
+ self.in_channels = self.conv.in_channels
146
+ self.out_channels = self.conv.out_channels
147
+ self.kernel_size = self.conv.kernel_size
148
+ self.stride = self.conv.stride
149
+ self.padding = padding
150
+ self.dilation = self.conv.dilation
151
+ self.transposed = self.conv.transposed
152
+ self.output_padding = self.conv.output_padding
153
+ self.groups = self.conv.groups
154
+
155
+ if self.with_spectral_norm:
156
+ self.conv = nn.utils.spectral_norm(self.conv)
157
+
158
+ # # build normalization layers
159
+ if self.with_norm:
160
+ # norm layer is after conv layer
161
+ if order.index("norm") > order.index("conv"):
162
+ norm_channels = out_channels
163
+ else:
164
+ norm_channels = in_channels
165
+ # self.norm_name, norm = build_norm_layer(
166
+ # norm_cfg, norm_channels) # type: ignore
167
+ self.add_module("bn", torch.nn.SyncBatchNorm(norm_channels))
168
+ # if self.with_bias:
169
+ # if isinstance(norm, (_BatchNorm, _InstanceNorm)):
170
+ # warnings.warn(
171
+ # 'Unnecessary conv bias before batch/instance norm')
172
+ self.norm_name = "bn"
173
+ else:
174
+ self.norm_name = None # type: ignore
175
+
176
+ # build activation layer
177
+ if self.with_activation:
178
+ act_cfg_ = act_cfg.copy() # type: ignore
179
+ # nn.Tanh has no 'inplace' argument
180
+ if act_cfg_["type"] not in ["Tanh", "PReLU", "Sigmoid", "HSigmoid", "Swish", "GELU"]:
181
+ act_cfg_.setdefault("inplace", inplace)
182
+ self.activate = nn.ReLU() # build_activation_layer(act_cfg_)
183
+
184
+ # Use msra init by default
185
+ self.init_weights()
186
+
187
+ @property
188
+ def norm(self):
189
+ if self.norm_name:
190
+ return getattr(self, self.norm_name)
191
+ else:
192
+ return None
193
+
194
+ def init_weights(self):
195
+ # 1. It is mainly for customized conv layers with their own
196
+ # initialization manners by calling their own ``init_weights()``,
197
+ # and we do not want ConvModule to override the initialization.
198
+ # 2. For customized conv layers without their own initialization
199
+ # manners (that is, they don't have their own ``init_weights()``)
200
+ # and PyTorch's conv layers, they will be initialized by
201
+ # this method with default ``kaiming_init``.
202
+ # Note: For PyTorch's conv layers, they will be overwritten by our
203
+ # initialization implementation using default ``kaiming_init``.
204
+ if not hasattr(self.conv, "init_weights"):
205
+ if self.with_activation and self.act_cfg["type"] == "LeakyReLU":
206
+ nonlinearity = "leaky_relu"
207
+ a = self.act_cfg.get("negative_slope", 0.01)
208
+ else:
209
+ nonlinearity = "relu"
210
+ a = 0
211
+ kaiming_init(self.conv, a=a, nonlinearity=nonlinearity)
212
+ if self.with_norm:
213
+ constant_init(self.norm_name, 1, bias=0)
214
+
215
+ def forward(self, x: torch.Tensor, activate: bool = True, norm: bool = True, debug: bool = False) -> torch.Tensor:
216
+ for layer in self.order:
217
+ if debug:
218
+ breakpoint()
219
+ if layer == "conv":
220
+ if self.with_explicit_padding:
221
+ x = self.padding_layer(x)
222
+ x = self.conv(x)
223
+ elif layer == "norm" and norm and self.with_norm:
224
+ x = self.norm(x)
225
+ elif layer == "act" and activate and self.with_activation:
226
+ x = self.activate(x)
227
+ return x
228
+
229
+
230
+ class Interpolate(nn.Module):
231
+ def __init__(self, scale_factor, mode, align_corners=False):
232
+ super(Interpolate, self).__init__()
233
+ self.interp = nn.functional.interpolate
234
+ self.scale_factor = scale_factor
235
+ self.mode = mode
236
+ self.align_corners = align_corners
237
+
238
+ def forward(self, x):
239
+ x = self.interp(x, scale_factor=self.scale_factor, mode=self.mode, align_corners=self.align_corners)
240
+ return x
241
+
242
+
243
+ class UpConvHead(nn.Module):
244
+ """
245
+ A 3 layer Convolutional head with intermediate upsampling
246
+
247
+ Args:
248
+ - features (int): number of input channels
249
+ - n_output_channels (int, default=256): number of output channels
250
+ - n_hidden_channels (int, default=32): number of channels in hidden layer
251
+
252
+ The operations are
253
+ [
254
+ Conv3x3(features, features // 2),
255
+ 2x-Upsampling,
256
+ Conv3x3(features // 2, hidden_channels),
257
+ ReLU,
258
+ Conv1x1(hidden_channels, n_output_channels),
259
+ ]
260
+ """
261
+
262
+ def __init__(self, features, n_output_channels, n_hidden_channels=32):
263
+ super(UpConvHead, self).__init__()
264
+ self.n_output_channels = n_output_channels
265
+ self.head = nn.Sequential(
266
+ nn.Conv2d(features, features // 2, kernel_size=3, stride=1, padding=1),
267
+ Interpolate(scale_factor=2, mode="bilinear", align_corners=True),
268
+ nn.Conv2d(features // 2, n_hidden_channels, kernel_size=3, stride=1, padding=1),
269
+ nn.ReLU(),
270
+ nn.Conv2d(n_hidden_channels, n_output_channels, kernel_size=1, stride=1, padding=0),
271
+ )
272
+
273
+ def forward(self, x):
274
+ x = self.head(x)
275
+ return x
276
+
277
+
278
+ class ReassembleBlocks(nn.Module):
279
+ """ViTPostProcessBlock, process cls_token in ViT backbone output and
280
+ rearrange the feature vector to feature map.
281
+ Args:
282
+ in_channels (List): ViT feature channels.
283
+ Default: [1024, 1024, 1024, 1024].
284
+ out_channels (List): output channels of each stage.
285
+ Default: [128, 256, 512, 1024].
286
+ readout_type (str): Type of readout operation. Default: 'ignore'.
287
+ init_cfg (dict, optional): Initialization config dict. Default: None.
288
+ """
289
+
290
+ def __init__(
291
+ self,
292
+ in_channels=[1024, 1024, 1024, 1024],
293
+ out_channels=[128, 256, 512, 1024],
294
+ readout_type="project",
295
+ use_batchnorm=False,
296
+ ):
297
+ super(ReassembleBlocks, self).__init__()
298
+
299
+ assert readout_type in ["ignore", "add", "project"]
300
+ self.readout_type = readout_type
301
+
302
+ self.projects = nn.ModuleList(
303
+ [
304
+ ConvModule(
305
+ in_channels=in_channels[channel_index],
306
+ out_channels=out_channel,
307
+ kernel_size=1,
308
+ act_cfg=None,
309
+ )
310
+ for channel_index, out_channel in enumerate(out_channels)
311
+ ]
312
+ )
313
+
314
+ self.resize_layers = nn.ModuleList(
315
+ [
316
+ nn.ConvTranspose2d(
317
+ in_channels=out_channels[0], out_channels=out_channels[0], kernel_size=4, stride=4, padding=0
318
+ ),
319
+ nn.ConvTranspose2d(
320
+ in_channels=out_channels[1], out_channels=out_channels[1], kernel_size=2, stride=2, padding=0
321
+ ),
322
+ nn.Identity(),
323
+ nn.Conv2d(
324
+ in_channels=out_channels[3], out_channels=out_channels[3], kernel_size=3, stride=2, padding=1
325
+ ),
326
+ ]
327
+ )
328
+ if self.readout_type == "project":
329
+ self.readout_projects = nn.ModuleList()
330
+ for i in range(len(self.projects)):
331
+ self.readout_projects.append(nn.Sequential(nn.Linear(2 * in_channels[i], in_channels[i]), nn.GELU()))
332
+
333
+ self.batchnorm_layers = nn.ModuleList(
334
+ [nn.SyncBatchNorm(channel) if use_batchnorm else nn.Identity(channel) for channel in in_channels]
335
+ )
336
+
337
+ def forward(self, inputs):
338
+ assert isinstance(inputs, list)
339
+ out = []
340
+ for i, x in enumerate(inputs):
341
+ assert len(x) == 2
342
+ x, cls_token = x[0], x[1]
343
+ feature_shape = x.shape
344
+ if self.readout_type == "project":
345
+ x = x.flatten(2).permute((0, 2, 1))
346
+ readout = cls_token.unsqueeze(1).expand_as(x)
347
+ x = self.readout_projects[i](torch.cat((x, readout), -1))
348
+ x = x.permute(0, 2, 1).reshape(feature_shape)
349
+ elif self.readout_type == "add":
350
+ x = x.flatten(2) + cls_token.unsqueeze(-1)
351
+ x = x.reshape(feature_shape)
352
+ else:
353
+ pass
354
+ x = self.batchnorm_layers[i](x)
355
+ x = self.projects[i](x)
356
+ x = self.resize_layers[i](x)
357
+ out.append(x)
358
+ return out
359
+
360
+
361
+ class PreActResidualConvUnit(nn.Module):
362
+ """ResidualConvUnit, pre-activate residual unit.
363
+ Args:
364
+ in_channels (int): number of channels in the input feature map.
365
+ act_cfg (dict): dictionary to construct and config activation layer.
366
+ norm_cfg (dict): dictionary to construct and config norm layer.
367
+ stride (int): stride of the first block. Default: 1
368
+ dilation (int): dilation rate for convs layers. Default: 1.
369
+ init_cfg (dict, optional): Initialization config dict. Default: None.
370
+ """
371
+
372
+ def __init__(self, in_channels, act_cfg, norm_cfg, stride=1, dilation=1, init_cfg=None):
373
+ super(PreActResidualConvUnit, self).__init__() # init_cfg)
374
+ self.conv1 = ConvModule(
375
+ in_channels,
376
+ in_channels,
377
+ 3,
378
+ stride=stride,
379
+ padding=dilation,
380
+ dilation=dilation,
381
+ norm_cfg=norm_cfg,
382
+ act_cfg=act_cfg,
383
+ bias=False,
384
+ order=("act", "conv", "norm"),
385
+ )
386
+ self.conv2 = ConvModule(
387
+ in_channels,
388
+ in_channels,
389
+ 3,
390
+ padding=1,
391
+ norm_cfg=norm_cfg,
392
+ act_cfg=act_cfg,
393
+ bias=False,
394
+ order=("act", "conv", "norm"),
395
+ )
396
+
397
+ def forward(self, inputs):
398
+ inputs_ = inputs.clone()
399
+ x = self.conv1(inputs)
400
+ x = self.conv2(x)
401
+ return x + inputs_
402
+
403
+
404
+ class FeatureFusionBlock(nn.Module):
405
+ """FeatureFusionBlock, merge feature map from different stages.
406
+ Args:
407
+ in_channels (int): Input channels.
408
+ act_cfg (dict): The activation config for ResidualConvUnit.
409
+ norm_cfg (dict): Config dict for normalization layer.
410
+ expand (bool): Whether expand the channels in post process block.
411
+ Default: False.
412
+ align_corners (bool): align_corner setting for bilinear upsample.
413
+ Default: True.
414
+ init_cfg (dict, optional): Initialization config dict. Default: None.
415
+ """
416
+
417
+ def __init__(self, in_channels, act_cfg, norm_cfg, expand=False, align_corners=True, init_cfg=None):
418
+ super(FeatureFusionBlock, self).__init__() # init_cfg)
419
+ self.in_channels = in_channels
420
+ self.expand = expand
421
+ self.align_corners = align_corners
422
+ self.out_channels = in_channels
423
+ if self.expand:
424
+ self.out_channels = in_channels // 2
425
+ self.project = ConvModule(self.in_channels, self.out_channels, kernel_size=1, act_cfg=None, bias=True)
426
+ self.res_conv_unit1 = PreActResidualConvUnit(in_channels=self.in_channels, act_cfg=act_cfg, norm_cfg=norm_cfg)
427
+ self.res_conv_unit2 = PreActResidualConvUnit(in_channels=self.in_channels, act_cfg=act_cfg, norm_cfg=norm_cfg)
428
+
429
+ def forward(self, *inputs):
430
+ x = inputs[0]
431
+
432
+ if len(inputs) == 2:
433
+ if x.shape != inputs[1].shape:
434
+ res = torch.nn.functional.interpolate(
435
+ inputs[1],
436
+ size=(x.shape[2], x.shape[3]),
437
+ mode="bilinear",
438
+ align_corners=False,
439
+ )
440
+ else:
441
+ res = inputs[1]
442
+ x = x + self.res_conv_unit1(res)
443
+ x = self.res_conv_unit2(x) # ok
444
+
445
+ x = torch.nn.functional.interpolate(x, scale_factor=2, mode="bilinear", align_corners=self.align_corners)
446
+ # ok
447
+
448
+ x = self.project(x) # ok
449
+ return x
450
+
451
+
452
+ class DPTHead(nn.Module):
453
+ """Vision Transformers for Dense Prediction.
454
+ This head is implemented of `DPT <https://arxiv.org/abs/2103.13413>`_.
455
+ Args:
456
+ in_channels (List): The input dimensions of the ViT backbone.
457
+ Default: [1024, 1024, 1024, 1024].
458
+ channels (int): Channels after modules, before the task-specific module
459
+ (`conv_depth`). Default: 256.
460
+ post_process_channels (List): Out channels of post process conv
461
+ layers. Default: [96, 192, 384, 768].
462
+ readout_type (str): Type of readout operation. Default: 'ignore'.
463
+ expand_channels (bool): Whether expand the channels in post process
464
+ block. Default: False.
465
+ """
466
+
467
+ def __init__(
468
+ self,
469
+ in_channels=(1024, 1024, 1024, 1024),
470
+ channels=256,
471
+ post_process_channels=[128, 256, 512, 1024],
472
+ readout_type="project",
473
+ expand_channels=False,
474
+ n_output_channels=256,
475
+ use_batchnorm=False, # TODO
476
+ **kwargs,
477
+ ):
478
+ super(DPTHead, self).__init__(**kwargs)
479
+ self.channels = channels
480
+ self.n_output_channels = n_output_channels
481
+ self.in_channels = in_channels
482
+ self.expand_channels = expand_channels
483
+ self.norm_cfg = None # TODO CHECK THIS
484
+ self.reassemble_blocks = ReassembleBlocks(
485
+ in_channels=in_channels,
486
+ out_channels=post_process_channels,
487
+ readout_type=readout_type,
488
+ use_batchnorm=use_batchnorm,
489
+ )
490
+
491
+ self.post_process_channels = [
492
+ channel * (2**i) if expand_channels else channel for i, channel in enumerate(post_process_channels)
493
+ ]
494
+ self.convs = nn.ModuleList()
495
+ for channel in self.post_process_channels:
496
+ self.convs.append(ConvModule(channel, self.channels, kernel_size=3, padding=1, act_cfg=None, bias=False))
497
+ self.fusion_blocks = nn.ModuleList()
498
+ self.act_cfg = {"type": "ReLU"}
499
+ for _ in range(len(self.convs)):
500
+ self.fusion_blocks.append(FeatureFusionBlock(self.channels, self.act_cfg, self.norm_cfg))
501
+ self.fusion_blocks[0].res_conv_unit1 = None
502
+ self.project = ConvModule(self.channels, self.channels, kernel_size=3, padding=1, norm_cfg=self.norm_cfg)
503
+ self.num_fusion_blocks = len(self.fusion_blocks)
504
+ self.num_reassemble_blocks = len(self.reassemble_blocks.resize_layers)
505
+ self.num_post_process_channels = len(self.post_process_channels)
506
+ assert self.num_fusion_blocks == self.num_reassemble_blocks
507
+ assert self.num_reassemble_blocks == self.num_post_process_channels
508
+ self.conv_depth = UpConvHead(self.channels, self.n_output_channels)
509
+
510
+ def forward_features(self, inputs):
511
+ assert (
512
+ len(inputs) == self.num_reassemble_blocks
513
+ ), f"Expected {self.num_reassemble_blocks} inputs, got {len(inputs)}."
514
+ x = [inp for inp in inputs]
515
+
516
+ x = self.reassemble_blocks(x)
517
+ x = [self.convs[i](feature) for i, feature in enumerate(x)]
518
+ out = self.fusion_blocks[0](x[-1])
519
+
520
+ for i in range(1, len(self.fusion_blocks)):
521
+ out = self.fusion_blocks[i](out, x[-(i + 1)])
522
+
523
+ out = self.project(out)
524
+ return out
525
+
526
+ def forward(self, inputs):
527
+ out = self.forward_features(inputs)
528
+ return self.conv_depth(out)
529
+
530
+ def predict(self, inputs, rescale_to=(512, 512)):
531
+ out = self.forward_features(inputs)
532
+ return self.conv_depth.predict(out, rescale_to=rescale_to)
dinov3/eval/dense/depth/models/embed.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import itertools
7
+ import math
8
+
9
+ import torch
10
+
11
+
12
+ class CenterPadding(torch.nn.Module):
13
+ def __init__(self, multiple: int):
14
+ super().__init__()
15
+ self.multiple = multiple
16
+
17
+ def _get_pad(self, size):
18
+ new_size = math.ceil(size / self.multiple) * self.multiple
19
+ pad_size = new_size - size
20
+ pad_size_left = pad_size // 2
21
+ pad_size_right = pad_size - pad_size_left
22
+ return pad_size_left, pad_size_right
23
+
24
+ # @torch.inference_mode()
25
+ def forward(self, x):
26
+ # expected shapes are ... x H x W, usually B x C x H x W
27
+ pads = list(itertools.chain.from_iterable(self._get_pad(m) for m in x.shape[:-3:-1]))
28
+ output = torch.nn.functional.pad(x, pads)
29
+ return output
30
+
31
+ def __extra_repr__(self) -> str:
32
+ return f"multiple={self.multiple}"
33
+
34
+
35
+ class StretchToMultiple(torch.nn.Module):
36
+ def __init__(self, multiple: int):
37
+ super().__init__()
38
+ self.multiple = multiple
39
+
40
+ def forward(self, x):
41
+ # expected shapes are ... x H x W, usually B x C x H x W
42
+ *shape, C, H, W = x.shape
43
+ new_H = math.ceil(H / self.multiple) * self.multiple
44
+ new_W = math.ceil(W / self.multiple) * self.multiple
45
+ if new_H != H or new_W != W:
46
+ x = x.reshape(-1, C, H, W)
47
+ x = torch.nn.functional.interpolate(x, size=(new_H, new_W), mode="bilinear")
48
+ x = x.reshape(*shape, C, new_H, new_W)
49
+ return x
50
+
51
+ def __extra_repr__(self) -> str:
52
+ return f"multiple={self.multiple}"
dinov3/eval/dense/depth/models/encoder.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import logging
7
+ from enum import Enum
8
+ from typing import Sequence, Union
9
+
10
+ from dinov3.eval.dense.depth.models.embed import CenterPadding, StretchToMultiple
11
+ from torch import Tensor, nn
12
+
13
+ logger = logging.getLogger("dinov3")
14
+
15
+
16
+ class BackboneLayersSet(Enum):
17
+ # Set of intermediate layers to take from the backbone
18
+ LAST = "LAST" # extracting only the last layer
19
+ FOUR_LAST = "FOUR_LAST" # extracting the last 4 layers
20
+ FOUR_EVEN_INTERVALS = "FOUR_EVEN_INTERVALS" # extracting outputs every 1/4 of the total number of blocks
21
+
22
+
23
+ def _get_backbone_out_indices(
24
+ model: nn.Module,
25
+ backbone_out_layers: Union[list[int], BackboneLayersSet] = BackboneLayersSet.FOUR_EVEN_INTERVALS,
26
+ ):
27
+ """
28
+ Get indices for output layers of the ViT backbone. For now there are 3 options available:
29
+ BackboneLayersSet.LAST : only extract the last layer, used in segmentation tasks with a bn head.
30
+ BackboneLayersSet.FOUR_LAST : extract the last 4 layers, used in segmentation (multiscale setting)
31
+ BackboneLayersSet.FOUR_EVEN_INTERVALS : extract outputs every 1/4 of the total number of blocks
32
+ Reference outputs in 'FOUR_EVEN_INTERVALS' mode :
33
+ ViT/S (12 blocks): [2, 5, 8, 11]
34
+ ViT/B (12 blocks): [2, 5, 8, 11]
35
+ ViT/L (24 blocks): [5, 11, 17, 23] (correct), [4, 11, 17, 23] (incorrect)
36
+ ViT/g (40 blocks): [9, 19, 29, 39]
37
+ """
38
+ n_blocks = getattr(model, "n_blocks", 1)
39
+ if isinstance(backbone_out_layers, list):
40
+ out_indices = backbone_out_layers
41
+
42
+ if backbone_out_layers == BackboneLayersSet.LAST:
43
+ out_indices = [n_blocks - 1]
44
+ elif backbone_out_layers == BackboneLayersSet.FOUR_LAST:
45
+ out_indices = [i for i in range(n_blocks - 4, n_blocks)]
46
+ elif backbone_out_layers == BackboneLayersSet.FOUR_EVEN_INTERVALS:
47
+ # XXX: Force (incorrect) out indices for backward-compatibility (ViT/L only)
48
+ if n_blocks == 24:
49
+ out_indices = [4, 11, 17, 23]
50
+ else:
51
+ out_indices = [i * (n_blocks // 4) - 1 for i in range(1, 5)]
52
+ assert all([out_index < n_blocks for out_index in out_indices])
53
+ return out_indices
54
+
55
+
56
+ class PatchSizeAdaptationStrategy(Enum):
57
+ CENTER_PADDING = "center_padding"
58
+ STRETCH = "stretch"
59
+ NO_ADAPTATION = "never"
60
+
61
+
62
+ class DinoVisionTransformerWrapper(nn.Module):
63
+ """Vision Transformer."""
64
+
65
+ def __init__(
66
+ self,
67
+ backbone_model: nn.Module,
68
+ backbone_out_layers: Union[str, list[int]],
69
+ use_backbone_norm: bool = False,
70
+ adapt_to_patch_size: PatchSizeAdaptationStrategy = PatchSizeAdaptationStrategy.CENTER_PADDING,
71
+ ):
72
+ super().__init__()
73
+
74
+ self.final_norm = use_backbone_norm
75
+ self.backbone = backbone_model
76
+ self.backbone_out_indices = _get_backbone_out_indices(
77
+ self.backbone,
78
+ backbone_out_layers=(
79
+ backbone_out_layers if isinstance(backbone_out_layers, list) else BackboneLayersSet(backbone_out_layers)
80
+ ),
81
+ )
82
+
83
+ # If the backbone does not define embed_dims, use [embed_dim] * n_blocks
84
+ try:
85
+ embed_dims = self.backbone.embed_dims
86
+ except AttributeError:
87
+ embed_dim = self.backbone.embed_dim
88
+ n_blocks = self.backbone.n_blocks
89
+ logger.warning(f"Backbone does not define embed_dims, using {[embed_dim] * n_blocks=} instead")
90
+ embed_dims = [embed_dim] * n_blocks
91
+ self.embed_dims: Sequence[int] = [embed_dims[idx] for idx in self.backbone_out_indices]
92
+
93
+ # How to adapt input images to the patch size of the model?
94
+ try:
95
+ input_pad_size = self.backbone.input_pad_size
96
+ except AttributeError:
97
+ patch_size = self.backbone.patch_size
98
+ logger.warning(f"Backbone does not define input_pad_size, using {patch_size=} instead")
99
+ input_pad_size = patch_size
100
+ if adapt_to_patch_size is PatchSizeAdaptationStrategy.CENTER_PADDING:
101
+ self.patch_size_adapter = CenterPadding(input_pad_size)
102
+ elif adapt_to_patch_size is PatchSizeAdaptationStrategy.STRETCH:
103
+ self.patch_size_adapter = StretchToMultiple(input_pad_size)
104
+ elif adapt_to_patch_size is PatchSizeAdaptationStrategy.NO_ADAPTATION:
105
+ self.patch_size_adapter = nn.Identity()
106
+ else:
107
+ raise ValueError(f"Unknown value {adapt_to_patch_size=}")
108
+
109
+ # Freeze backbone
110
+ self.backbone.requires_grad_(False)
111
+
112
+ def forward(
113
+ self,
114
+ x: Tensor, # [B, rgb, H, W]
115
+ ) -> list[tuple[Tensor, Tensor]]:
116
+ x = self.patch_size_adapter(x)
117
+ outputs = self.backbone.get_intermediate_layers(
118
+ x,
119
+ n=self.backbone_out_indices,
120
+ reshape=True,
121
+ return_class_token=True,
122
+ norm=self.final_norm,
123
+ ) # List of (patch feats [B, C, h, w], class token [B, C])
124
+ return outputs
dinov3/eval/dense/depth/utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ import torch
7
+
8
+ def cast_to(o, dtype: torch.dtype):
9
+ if isinstance(o, torch.Tensor):
10
+ return o.to(dtype)
11
+ if isinstance(o, tuple):
12
+ return tuple(cast_to(u, dtype) for u in o)
13
+ if isinstance(o, list):
14
+ return list(cast_to(u, dtype) for u in o)
15
+ if isinstance(o, dict):
16
+ return {k: cast_to(u, dtype) for k, u in o.items()}
17
+ if isinstance(o, (bool, int, float, str, type(None))):
18
+ return o
19
+ raise NotImplementedError(f"Unsupported type: {type(o)}")
dinov3/eval/detection/config.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from .models.position_encoding import PositionEncoding
9
+
10
+
11
+ @dataclass(kw_only=True)
12
+ class DetectionHeadConfig:
13
+ num_classes: int = 91 # 91 classes in COCO
14
+ # Deformable DETR tricks
15
+ with_box_refine: bool = True
16
+ two_stage: bool = True
17
+ # DINO DETR tricks
18
+ mixed_selection: bool = True
19
+ look_forward_twice: bool = True # was default False
20
+ # Hybrid Matching tricks
21
+ k_one2many: int = 6 # was 5
22
+ lambda_one2many: float = 1.0
23
+ num_queries_one2one: int = 300 # number of query slots for one_to_one matching
24
+ num_queries_one2many: int = 1500 # was 0, number of query slots for one_to_many matching
25
+ """
26
+ Absolute coordinates & box regression reparameterization.
27
+ If true, we use absolute coordindates & reparameterization for bounding boxes.
28
+ """
29
+ reparam: bool = True
30
+ topk: int = 100
31
+
32
+ # * Backbone
33
+ # type of positional embedding to use on top of the image features
34
+ position_embedding: PositionEncoding = PositionEncoding.SINE
35
+ num_feature_levels: int = 1 # number of feature levels
36
+
37
+ # * Transformer
38
+ dec_layers: int = 6 # number of decoding layers in the transformer
39
+ dim_feedforward: int = 2048 # intermediate size of the feedforward layers in the transformer blocks
40
+ hidden_dim: int = 256 # size of the embeddings (dimension of the transformer)
41
+ dropout: float = 0.0 # dropout applied in the transformer, was 0.1
42
+ nheads: int = 8 # number of attention heads inside the transformer's attentions
43
+ norm_type: str = "pre_norm"
44
+
45
+ # Loss
46
+ aux_loss: bool = True # auxiliary decoding losses (loss at each layer)
47
+
48
+ # * dev: proposals
49
+ proposal_feature_levels: int = 4 # was 1
50
+ proposal_min_size: int = 50
51
+ # * dev decoder: global decoder
52
+ decoder_type: str = "global_rpe_decomp" # was deform
53
+ decoder_use_checkpoint: bool = False
54
+ decoder_rpe_hidden_dim: int = 512
55
+ decoder_rpe_type: str = "linear"
56
+
57
+ # Custom
58
+ add_transformer_encoder: bool = True
59
+ num_encoder_layers: int = 6
60
+ layers_to_use: list[int] | None = None
61
+ blocks_to_train: list[int] | None = None
62
+ n_windows_sqrt: int = 0
63
+ proposal_in_stride: int | None = None
64
+ proposal_tgt_strides: list[int] | None = None
65
+ backbone_use_layernorm: bool = False # whether to use layernorm on each layer of the backbone's features
dinov3/eval/detection/models/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
dinov3/eval/detection/models/backbone.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ # ------------------------------------------------------------------------
7
+ # Plain-DETR
8
+ # Copyright (c) 2023 Xi'an Jiaotong University & Microsoft Research Asia.
9
+ # Licensed under The MIT License [see LICENSE for details]
10
+ # ------------------------------------------------------------------------
11
+ # Deformable DETR
12
+ # Copyright (c) 2020 SenseTime. All Rights Reserved.
13
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
14
+ # ------------------------------------------------------------------------
15
+ # Modified from DETR (https://github.com/facebookresearch/detr)
16
+ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
17
+ # ------------------------------------------------------------------------
18
+
19
+ """
20
+ Backbone modules.
21
+ """
22
+ import logging
23
+ from typing import List, Optional, Union
24
+
25
+ import torch
26
+ import torch.nn.functional as F
27
+ from torch import nn
28
+
29
+ from ..util.misc import NestedTensor
30
+ from .position_encoding import build_position_encoding
31
+ from .utils import LayerNorm2D
32
+ from .windows import WindowsWrapper
33
+
34
+ logger = logging.getLogger("dinov3")
35
+
36
+
37
+ class DINOBackbone(nn.Module):
38
+ def __init__(
39
+ self,
40
+ backbone_model: nn.Module,
41
+ train_backbone: bool,
42
+ blocks_to_train: Optional[List[str]] = None,
43
+ layers_to_use: Union[int, List] = 1,
44
+ use_layernorm: bool = True,
45
+ ):
46
+ super().__init__()
47
+ self.backbone = backbone_model
48
+ self.blocks_to_train = blocks_to_train
49
+ self.patch_size = self.backbone.patch_size
50
+ self.use_layernorm = use_layernorm
51
+
52
+ for _, (name, parameter) in enumerate(self.backbone.named_parameters()):
53
+ train_condition = any(f".{b}." in name for b in self.blocks_to_train) if self.blocks_to_train else True
54
+ if (not train_backbone) or "mask_token" in name or (not train_condition):
55
+ parameter.requires_grad_(False)
56
+
57
+ self.strides = [self.backbone.patch_size]
58
+
59
+ # get embed_dim for each intermediate output
60
+ n_all_layers = self.backbone.n_blocks
61
+ blocks_to_take = (
62
+ range(n_all_layers - layers_to_use, n_all_layers) if isinstance(layers_to_use, int) else layers_to_use
63
+ )
64
+
65
+ # if models do not define embed_dims, repeat embed_dim n_blocks times
66
+ embed_dims = getattr(self.backbone, "embed_dims", [self.backbone.embed_dim] * self.backbone.n_blocks)
67
+ embed_dims = [embed_dims[i] for i in range(n_all_layers) if i in blocks_to_take]
68
+
69
+ if self.use_layernorm:
70
+ self.layer_norms = nn.ModuleList([LayerNorm2D(embed_dim) for embed_dim in embed_dims])
71
+
72
+ self.num_channels = [sum(embed_dims)]
73
+ self.layers_to_use = layers_to_use
74
+
75
+ def forward(self, tensor_list: NestedTensor):
76
+ xs = self.backbone.get_intermediate_layers(tensor_list.tensors, n=self.layers_to_use, reshape=True)
77
+ if self.use_layernorm:
78
+ xs = [ln(x).contiguous() for ln, x in zip(self.layer_norms, xs)]
79
+
80
+ xs = [torch.cat(xs, axis=1)]
81
+
82
+ out: list[NestedTensor] = []
83
+ for x in xs:
84
+ m = tensor_list.mask
85
+ assert m is not None
86
+ mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0]
87
+ out.append(NestedTensor(x, mask))
88
+ return out
89
+
90
+
91
+ class BackboneWithPositionEncoding(nn.Sequential):
92
+ def __init__(self, backbone, position_embedding):
93
+ super().__init__(backbone, position_embedding)
94
+ self.strides = backbone.strides
95
+ self.num_channels = backbone.num_channels
96
+
97
+ def forward(self, tensor_list: NestedTensor):
98
+ out: List[NestedTensor] = list(self[0](tensor_list))
99
+ pos = [self[1][idx](x).to(x.tensors.dtype) for idx, x in enumerate(out)]
100
+ return out, pos
101
+
102
+
103
+ def build_backbone(backbone_model, args):
104
+ position_embedding = build_position_encoding(args)
105
+ train_backbone = False
106
+ backbone = DINOBackbone(
107
+ backbone_model, train_backbone, args.blocks_to_train, args.layers_to_use, args.backbone_use_layernorm
108
+ )
109
+ if args.n_windows_sqrt > 0:
110
+ logger.info(f"Wrapping with {args.n_windows_sqrt} x {args.n_windows_sqrt} windows")
111
+ backbone = WindowsWrapper(
112
+ backbone, n_windows_w=args.n_windows_sqrt, n_windows_h=args.n_windows_sqrt, patch_size=backbone.patch_size
113
+ )
114
+ else:
115
+ logger.info("Not wrapping with windows")
116
+
117
+ return BackboneWithPositionEncoding(backbone, position_embedding)
dinov3/eval/detection/models/detr.py ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ # ------------------------------------------------------------------------
7
+ # Plain-DETR
8
+ # Copyright (c) 2023 Xi'an Jiaotong University & Microsoft Research Asia.
9
+ # Licensed under The MIT License [see LICENSE for details]
10
+ # ------------------------------------------------------------------------
11
+ # Deformable DETR
12
+ # Copyright (c) 2020 SenseTime. All Rights Reserved.
13
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
14
+ # ------------------------------------------------------------------------
15
+ # Modified from DETR (https://github.com/facebookresearch/detr)
16
+ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
17
+ # ------------------------------------------------------------------------
18
+
19
+ """
20
+ Deformable DETR model and criterion classes.
21
+ """
22
+ import math
23
+
24
+ import torch
25
+ import torch.nn.functional as F
26
+ from torch import nn
27
+
28
+ from ..util import box_ops
29
+ from ..util.misc import NestedTensor, _get_clones, inverse_sigmoid, nested_tensor_from_tensor_list
30
+ from .backbone import build_backbone
31
+ from .transformer import build_transformer
32
+
33
+
34
+ class PlainDETR(nn.Module):
35
+ """This is the Deformable DETR module that performs object detection"""
36
+
37
+ def __init__(
38
+ self,
39
+ backbone,
40
+ transformer,
41
+ num_classes,
42
+ num_feature_levels,
43
+ aux_loss=True,
44
+ with_box_refine=False,
45
+ two_stage=False,
46
+ num_queries_one2one=300,
47
+ num_queries_one2many=0,
48
+ mixed_selection=False,
49
+ ):
50
+ """Initializes the model.
51
+ Parameters:
52
+ backbone: torch module of the backbone to be used. See backbone.py
53
+ transformer: torch module of the transformer architecture. See transformer.py
54
+ num_classes: number of object classes
55
+ aux_loss: True if auxiliary decoding losses (loss at each decoder layer) are to be used.
56
+ with_box_refine: iterative bounding box refinement
57
+ two_stage: two-stage Deformable DETR
58
+ num_queries_one2one: number of object queries for one-to-one matching part
59
+ num_queries_one2many: number of object queries for one-to-many matching part
60
+ mixed_selection: a trick for Deformable DETR two stage
61
+
62
+ """
63
+ super().__init__()
64
+ num_queries = num_queries_one2one + num_queries_one2many
65
+ self.num_queries = num_queries
66
+ self.transformer = transformer
67
+ hidden_dim = transformer.d_model
68
+ self.class_embed = nn.Linear(hidden_dim, num_classes)
69
+ self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3)
70
+ self.num_feature_levels = num_feature_levels
71
+ if not two_stage:
72
+ self.query_embed = nn.Embedding(num_queries, hidden_dim * 2)
73
+ elif mixed_selection:
74
+ self.query_embed = nn.Embedding(num_queries, hidden_dim)
75
+ self.input_proj = nn.ModuleList(
76
+ [
77
+ nn.Sequential(
78
+ nn.Conv2d(backbone.num_channels[0], hidden_dim, kernel_size=1),
79
+ nn.GroupNorm(32, hidden_dim),
80
+ )
81
+ ]
82
+ )
83
+ self.backbone = backbone
84
+ self.aux_loss = aux_loss
85
+ self.with_box_refine = with_box_refine
86
+ self.two_stage = two_stage
87
+
88
+ prior_prob = 0.01
89
+ bias_value = -math.log((1 - prior_prob) / prior_prob)
90
+ self.class_embed.bias.data = torch.ones(num_classes) * bias_value
91
+ nn.init.constant_(self.bbox_embed.layers[-1].weight.data, 0)
92
+ nn.init.constant_(self.bbox_embed.layers[-1].bias.data, 0)
93
+ for proj in self.input_proj:
94
+ nn.init.xavier_uniform_(proj[0].weight, gain=1)
95
+ nn.init.constant_(proj[0].bias, 0)
96
+
97
+ # if two-stage, the last class_embed and bbox_embed is for region proposal generation
98
+ num_pred = (transformer.decoder.num_layers + 1) if two_stage else transformer.decoder.num_layers
99
+ if with_box_refine:
100
+ self.class_embed = _get_clones(self.class_embed, num_pred)
101
+ self.bbox_embed = _get_clones(self.bbox_embed, num_pred)
102
+ nn.init.constant_(self.bbox_embed[0].layers[-1].bias.data[2:], -2.0)
103
+ # hack implementation for iterative bounding box refinement
104
+ self.transformer.decoder.bbox_embed = self.bbox_embed
105
+ else:
106
+ nn.init.constant_(self.bbox_embed.layers[-1].bias.data[2:], -2.0)
107
+ self.class_embed = nn.ModuleList([self.class_embed for _ in range(num_pred)])
108
+ self.bbox_embed = nn.ModuleList([self.bbox_embed for _ in range(num_pred)])
109
+ self.transformer.decoder.bbox_embed = None
110
+ if two_stage:
111
+ # hack implementation for two-stage
112
+ self.transformer.decoder.class_embed = self.class_embed
113
+ for box_embed in self.bbox_embed:
114
+ nn.init.constant_(box_embed.layers[-1].bias.data[2:], 0.0)
115
+
116
+ self.num_queries_one2one = num_queries_one2one
117
+ self.mixed_selection = mixed_selection
118
+
119
+ def forward(self, samples: NestedTensor):
120
+ """The forward expects a NestedTensor, which consists of:
121
+ - samples.tensor: batched images, of shape [batch_size x 3 x H x W]
122
+ - samples.mask: a binary mask of shape [batch_size x H x W], containing 1 on padded pixels
123
+
124
+ It returns a dict with the following elements:
125
+ - "pred_logits": the classification logits (including no-object) for all queries.
126
+ Shape= [batch_size x num_queries x (num_classes + 1)]
127
+ - "pred_boxes": The normalized boxes coordinates for all queries, represented as
128
+ (center_x, center_y, height, width). These values are normalized in [0, 1],
129
+ relative to the size of each individual image (disregarding possible padding).
130
+ See PostProcess for information on how to retrieve the unnormalized bounding box.
131
+ - "aux_outputs": Optional, only returned when auxilary losses are activated. It is a list of
132
+ dictionnaries containing the two above keys for each decoder layer.
133
+ """
134
+ if not isinstance(samples, NestedTensor):
135
+ samples = nested_tensor_from_tensor_list(samples)
136
+ features, pos = self.backbone(samples)
137
+
138
+ srcs = []
139
+ masks = []
140
+ for layer, feat in enumerate(features):
141
+ src, mask = feat.decompose()
142
+ srcs.append(self.input_proj[layer](src))
143
+ masks.append(mask)
144
+ assert mask is not None
145
+
146
+ query_embeds = None
147
+ if not self.two_stage or self.mixed_selection:
148
+ query_embeds = self.query_embed.weight[0 : self.num_queries, :]
149
+
150
+ # make attn mask
151
+ """ attention mask to prevent information leakage
152
+ """
153
+ self_attn_mask = torch.zeros(
154
+ [
155
+ self.num_queries,
156
+ self.num_queries,
157
+ ],
158
+ dtype=bool,
159
+ device=src.device,
160
+ )
161
+ self_attn_mask[
162
+ self.num_queries_one2one :,
163
+ 0 : self.num_queries_one2one,
164
+ ] = True
165
+ self_attn_mask[
166
+ 0 : self.num_queries_one2one,
167
+ self.num_queries_one2one :,
168
+ ] = True
169
+
170
+ (
171
+ hs,
172
+ init_reference,
173
+ inter_references,
174
+ enc_outputs_class,
175
+ enc_outputs_coord_unact,
176
+ enc_outputs_delta,
177
+ output_proposals,
178
+ max_shape,
179
+ ) = self.transformer(srcs, masks, pos, query_embeds, self_attn_mask)
180
+
181
+ outputs_classes_one2one = []
182
+ outputs_coords_one2one = []
183
+ outputs_classes_one2many = []
184
+ outputs_coords_one2many = []
185
+ for lvl in range(hs.shape[0]):
186
+ if lvl == 0:
187
+ reference = init_reference
188
+ else:
189
+ reference = inter_references[lvl - 1]
190
+ reference = inverse_sigmoid(reference)
191
+ outputs_class = self.class_embed[lvl](hs[lvl])
192
+ tmp = self.bbox_embed[lvl](hs[lvl])
193
+ if reference.shape[-1] == 4:
194
+ tmp += reference
195
+ else:
196
+ assert reference.shape[-1] == 2
197
+ tmp[..., :2] += reference
198
+ outputs_coord = tmp.sigmoid()
199
+
200
+ outputs_classes_one2one.append(outputs_class[:, 0 : self.num_queries_one2one])
201
+ outputs_classes_one2many.append(outputs_class[:, self.num_queries_one2one :])
202
+
203
+ outputs_coords_one2one.append(outputs_coord[:, 0 : self.num_queries_one2one])
204
+ outputs_coords_one2many.append(outputs_coord[:, self.num_queries_one2one :])
205
+
206
+ outputs_classes_one2one = torch.stack(outputs_classes_one2one)
207
+ outputs_coords_one2one = torch.stack(outputs_coords_one2one)
208
+
209
+ outputs_classes_one2many = torch.stack(outputs_classes_one2many)
210
+ outputs_coords_one2many = torch.stack(outputs_coords_one2many)
211
+
212
+ out = {
213
+ "pred_logits": outputs_classes_one2one[-1],
214
+ "pred_boxes": outputs_coords_one2one[-1],
215
+ "pred_logits_one2many": outputs_classes_one2many[-1],
216
+ "pred_boxes_one2many": outputs_coords_one2many[-1],
217
+ }
218
+ if self.aux_loss:
219
+ out["aux_outputs"] = self._set_aux_loss(outputs_classes_one2one, outputs_coords_one2one)
220
+ out["aux_outputs_one2many"] = self._set_aux_loss(outputs_classes_one2many, outputs_coords_one2many)
221
+
222
+ if self.two_stage:
223
+ enc_outputs_coord = enc_outputs_coord_unact.sigmoid()
224
+ out["enc_outputs"] = {
225
+ "pred_logits": enc_outputs_class,
226
+ "pred_boxes": enc_outputs_coord,
227
+ }
228
+ return out
229
+
230
+ @torch.jit.unused
231
+ def _set_aux_loss(self, outputs_class, outputs_coord):
232
+ # this is a workaround to make torchscript happy, as torchscript
233
+ # doesn't support dictionary with non-homogeneous values, such
234
+ # as a dict having both a Tensor and a list.
235
+ return [{"pred_logits": a, "pred_boxes": b} for a, b in zip(outputs_class[:-1], outputs_coord[:-1])]
236
+
237
+
238
+ class PlainDETRReParam(PlainDETR):
239
+ def forward(self, samples: NestedTensor):
240
+ """The forward expects a NestedTensor, which consists of:
241
+ - samples.tensor: batched images, of shape [batch_size x 3 x H x W]
242
+ - samples.mask: a binary mask of shape [batch_size x H x W], containing 1 on padded pixels
243
+
244
+ It returns a dict with the following elements:
245
+ - "pred_logits": the classification logits (including no-object) for all queries.
246
+ Shape= [batch_size x num_queries x (num_classes + 1)]
247
+ - "pred_boxes": The normalized boxes coordinates for all queries, represented as
248
+ (center_x, center_y, height, width). These values are normalized in [0, 1],
249
+ relative to the size of each individual image (disregarding possible padding).
250
+ See PostProcess for information on how to retrieve the unnormalized bounding box.
251
+ - "aux_outputs": Optional, only returned when auxilary losses are activated. It is a list of
252
+ dictionnaries containing the two above keys for each decoder layer.
253
+ """
254
+ if not isinstance(samples, NestedTensor):
255
+ samples = nested_tensor_from_tensor_list(samples)
256
+ features, pos = self.backbone(samples)
257
+
258
+ srcs = []
259
+ masks = []
260
+ for layer, feat in enumerate(features):
261
+ src, mask = feat.decompose()
262
+ srcs.append(self.input_proj[layer](src))
263
+ masks.append(mask)
264
+ assert mask is not None
265
+
266
+ query_embeds = None
267
+ if not self.two_stage or self.mixed_selection:
268
+ query_embeds = self.query_embed.weight[0 : self.num_queries, :]
269
+
270
+ # make attn mask
271
+ """ attention mask to prevent information leakage
272
+ """
273
+ self_attn_mask = torch.zeros(
274
+ [
275
+ self.num_queries,
276
+ self.num_queries,
277
+ ],
278
+ dtype=bool,
279
+ device=src.device,
280
+ )
281
+ self_attn_mask[
282
+ self.num_queries_one2one :,
283
+ 0 : self.num_queries_one2one,
284
+ ] = True
285
+ self_attn_mask[
286
+ 0 : self.num_queries_one2one,
287
+ self.num_queries_one2one :,
288
+ ] = True
289
+
290
+ (
291
+ hs,
292
+ init_reference,
293
+ inter_references,
294
+ enc_outputs_class,
295
+ enc_outputs_coord_unact,
296
+ enc_outputs_delta,
297
+ output_proposals,
298
+ max_shape,
299
+ ) = self.transformer(srcs, masks, pos, query_embeds, self_attn_mask)
300
+
301
+ outputs_classes_one2one = []
302
+ outputs_coords_one2one = []
303
+ outputs_classes_one2many = []
304
+ outputs_coords_one2many = []
305
+
306
+ outputs_coords_old_one2one = []
307
+ outputs_deltas_one2one = []
308
+ outputs_coords_old_one2many = []
309
+ outputs_deltas_one2many = []
310
+
311
+ for lvl in range(hs.shape[0]):
312
+ if lvl == 0:
313
+ reference = init_reference
314
+ else:
315
+ reference = inter_references[lvl - 1]
316
+ outputs_class = self.class_embed[lvl](hs[lvl])
317
+ tmp = self.bbox_embed[lvl](hs[lvl])
318
+ if reference.shape[-1] == 4:
319
+ outputs_coord = box_ops.box_xyxy_to_cxcywh(box_ops.delta2bbox(reference, tmp, max_shape))
320
+ else:
321
+ raise NotImplementedError
322
+
323
+ outputs_classes_one2one.append(outputs_class[:, 0 : self.num_queries_one2one])
324
+ outputs_classes_one2many.append(outputs_class[:, self.num_queries_one2one :])
325
+
326
+ outputs_coords_one2one.append(outputs_coord[:, 0 : self.num_queries_one2one])
327
+ outputs_coords_one2many.append(outputs_coord[:, self.num_queries_one2one :])
328
+
329
+ outputs_coords_old_one2one.append(reference[:, : self.num_queries_one2one])
330
+ outputs_coords_old_one2many.append(reference[:, self.num_queries_one2one :])
331
+ outputs_deltas_one2one.append(tmp[:, : self.num_queries_one2one])
332
+ outputs_deltas_one2many.append(tmp[:, self.num_queries_one2one :])
333
+
334
+ outputs_classes_one2one = torch.stack(outputs_classes_one2one)
335
+ outputs_coords_one2one = torch.stack(outputs_coords_one2one)
336
+
337
+ outputs_classes_one2many = torch.stack(outputs_classes_one2many)
338
+ outputs_coords_one2many = torch.stack(outputs_coords_one2many)
339
+
340
+ out = {
341
+ "pred_logits": outputs_classes_one2one[-1],
342
+ "pred_boxes": outputs_coords_one2one[-1],
343
+ "pred_logits_one2many": outputs_classes_one2many[-1],
344
+ "pred_boxes_one2many": outputs_coords_one2many[-1],
345
+ "pred_boxes_old": outputs_coords_old_one2one[-1],
346
+ "pred_deltas": outputs_deltas_one2one[-1],
347
+ "pred_boxes_old_one2many": outputs_coords_old_one2many[-1],
348
+ "pred_deltas_one2many": outputs_deltas_one2many[-1],
349
+ }
350
+
351
+ if self.aux_loss:
352
+ out["aux_outputs"] = self._set_aux_loss(
353
+ outputs_classes_one2one, outputs_coords_one2one, outputs_coords_old_one2one, outputs_deltas_one2one
354
+ )
355
+ out["aux_outputs_one2many"] = self._set_aux_loss(
356
+ outputs_classes_one2many, outputs_coords_one2many, outputs_coords_old_one2many, outputs_deltas_one2many
357
+ )
358
+
359
+ if self.two_stage:
360
+ out["enc_outputs"] = {
361
+ "pred_logits": enc_outputs_class,
362
+ "pred_boxes": enc_outputs_coord_unact,
363
+ "pred_boxes_old": output_proposals,
364
+ "pred_deltas": enc_outputs_delta,
365
+ }
366
+ return out
367
+
368
+ @torch.jit.unused
369
+ def _set_aux_loss(self, outputs_class, outputs_coord, outputs_coord_old, outputs_deltas):
370
+ # this is a workaround to make torchscript happy, as torchscript
371
+ # doesn't support dictionary with non-homogeneous values, such
372
+ # as a dict having both a Tensor and a list.
373
+ return [
374
+ {
375
+ "pred_logits": a,
376
+ "pred_boxes": b,
377
+ "pred_boxes_old": c,
378
+ "pred_deltas": d,
379
+ }
380
+ for a, b, c, d in zip(outputs_class[:-1], outputs_coord[:-1], outputs_coord_old[:-1], outputs_deltas[:-1])
381
+ ]
382
+
383
+
384
+ class PostProcess(nn.Module):
385
+ """This module converts the model's output into the format expected by the coco api"""
386
+
387
+ def __init__(self, topk=100, reparam=False):
388
+ super().__init__()
389
+ self.topk = topk
390
+ self.reparam = reparam
391
+
392
+ @torch.no_grad()
393
+ def forward(self, outputs, target_sizes, original_target_sizes=None):
394
+ """Perform the computation
395
+ Parameters:
396
+ outputs: raw outputs of the model
397
+ target_sizes: tensor of dimension [batch_size x 2] containing the size of each images of the batch
398
+ For evaluation, this must be the original image size (before any data augmentation)
399
+ For visualization, this should be the image size after data augment, but before padding
400
+ """
401
+ out_logits, out_bbox = outputs["pred_logits"], outputs["pred_boxes"]
402
+
403
+ assert len(out_logits) == len(target_sizes)
404
+ assert target_sizes.shape[1] == 2
405
+ assert not self.reparam or original_target_sizes.shape[1] == 2
406
+
407
+ prob = out_logits.sigmoid()
408
+ topk_values, topk_indexes = torch.topk(prob.view(out_logits.shape[0], -1), self.topk, dim=1)
409
+ scores = topk_values
410
+ topk_boxes = topk_indexes // out_logits.shape[2]
411
+ labels = topk_indexes % out_logits.shape[2]
412
+ boxes = box_ops.box_cxcywh_to_xyxy(out_bbox)
413
+ boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4))
414
+
415
+ # and from relative [0, 1] to absolute [0, height] coordinates
416
+ img_h, img_w = target_sizes.unbind(1)
417
+ if self.reparam:
418
+ img_h, img_w = img_h[:, None, None], img_w[:, None, None] # [BS, 1, 1]
419
+ boxes[..., 0::2].clamp_(min=torch.zeros_like(img_w), max=img_w)
420
+ boxes[..., 1::2].clamp_(min=torch.zeros_like(img_h), max=img_h)
421
+ scale_h, scale_w = (original_target_sizes / target_sizes).unbind(1)
422
+ scale_fct = torch.stack([scale_w, scale_h, scale_w, scale_h], dim=1)
423
+ else:
424
+ scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1)
425
+ boxes = boxes * scale_fct[:, None, :]
426
+
427
+ results = [{"scores": s, "labels": l, "boxes": b} for s, l, b in zip(scores, labels, boxes)]
428
+
429
+ return results
430
+
431
+
432
+ class MLP(nn.Module):
433
+ """Very simple multi-layer perceptron (also called FFN)"""
434
+
435
+ def __init__(self, input_dim, hidden_dim, output_dim, num_layers):
436
+ super().__init__()
437
+ self.num_layers = num_layers
438
+ h = [hidden_dim] * (num_layers - 1)
439
+ self.layers = nn.ModuleList(nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim]))
440
+
441
+ def forward(self, x):
442
+ for i, layer in enumerate(self.layers):
443
+ x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x)
444
+ return x
445
+
446
+
447
+ def build_model(backbone_model, args):
448
+ backbone = build_backbone(backbone_model, args)
449
+ transformer = build_transformer(args)
450
+ model_class = PlainDETR if (not args.reparam) else PlainDETRReParam
451
+ return model_class(
452
+ backbone,
453
+ transformer,
454
+ num_classes=args.num_classes,
455
+ num_feature_levels=args.num_feature_levels,
456
+ aux_loss=args.aux_loss,
457
+ with_box_refine=args.with_box_refine,
458
+ two_stage=args.two_stage,
459
+ num_queries_one2one=args.num_queries_one2one,
460
+ num_queries_one2many=args.num_queries_one2many,
461
+ mixed_selection=args.mixed_selection,
462
+ )
dinov3/eval/detection/models/global_ape_decoder.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This software may be used and distributed in accordance with
4
+ # the terms of the DINOv3 License Agreement.
5
+
6
+ # ------------------------------------------------------------------------
7
+ # Plain-DETR
8
+ # Copyright (c) 2023 Xi'an Jiaotong University & Microsoft Research Asia.
9
+ # Licensed under The MIT License [see LICENSE for details]
10
+ # ------------------------------------------------------------------------
11
+ # -*- coding: utf-8 -*-
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.utils.checkpoint as checkpoint
15
+
16
+ from ..util.misc import _get_activation_fn, _get_clones, inverse_sigmoid
17
+
18
+
19
+ class GlobalCrossAttention(nn.Module):
20
+ def __init__(
21
+ self,
22
+ dim,
23
+ num_heads,
24
+ qkv_bias=True,
25
+ qk_scale=None,
26
+ attn_drop=0.0,
27
+ proj_drop=0.0,
28
+ ):
29
+ super().__init__()
30
+ self.dim = dim
31
+ self.num_heads = num_heads
32
+ head_dim = dim // num_heads
33
+ self.scale = qk_scale or head_dim**-0.5
34
+
35
+ self.q = nn.Linear(dim, dim, bias=qkv_bias)
36
+ self.k = nn.Linear(dim, dim, bias=qkv_bias)
37
+ self.v = nn.Linear(dim, dim, bias=qkv_bias)
38
+ self.attn_drop = nn.Dropout(attn_drop)
39
+ self.proj = nn.Linear(dim, dim)
40
+ self.proj_drop = nn.Dropout(proj_drop)
41
+ self.softmax = nn.Softmax(dim=-1)
42
+
43
+ def forward(
44
+ self,
45
+ query,
46
+ k_input_flatten,
47
+ v_input_flatten,
48
+ input_padding_mask=None,
49
+ ):
50
+ B_, N, C = k_input_flatten.shape
51
+ k = self.k(k_input_flatten).reshape(B_, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3)
52
+ v = self.v(v_input_flatten).reshape(B_, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3)
53
+ B_, N, C = query.shape
54
+ q = self.q(query).reshape(B_, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3)
55
+
56
+ attn_mask = None
57
+ if input_padding_mask is not None:
58
+ attn_mask = input_padding_mask[:, None, None] * -100
59
+ attn_mask = attn_mask.contiguous() # to enable efficient attention
60
+
61
+ x = torch.nn.functional.scaled_dot_product_attention(
62
+ query=q,
63
+ key=k,
64
+ value=v,
65
+ attn_mask=attn_mask,
66
+ dropout_p=self.attn_drop.p if self.training else 0,
67
+ scale=self.scale,
68
+ )
69
+ x = x.transpose(1, 2).reshape(B_, N, C)
70
+ x = self.proj(x)
71
+ x = self.proj_drop(x)
72
+ return x
73
+
74
+
75
+ class GlobalDecoderLayer(nn.Module):
76
+ def __init__(
77
+ self,
78
+ d_model=256,
79
+ d_ffn=1024,
80
+ dropout=0.1,
81
+ activation="relu",
82
+ n_heads=8,
83
+ norm_type="post_norm",
84
+ ):
85
+ super().__init__()
86
+
87
+ self.norm_type = norm_type
88
+
89
+ # global cross attention
90
+ self.cross_attn = GlobalCrossAttention(d_model, n_heads)
91
+ self.dropout1 = nn.Dropout(dropout)
92
+ self.norm1 = nn.LayerNorm(d_model)
93
+
94
+ # self attention
95
+ self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
96
+ self.dropout2 = nn.Dropout(dropout)
97
+ self.norm2 = nn.LayerNorm(d_model)
98
+
99
+ # ffn
100
+ self.linear1 = nn.Linear(d_model, d_ffn)
101
+ self.activation = _get_activation_fn(activation)
102
+ self.dropout3 = nn.Dropout(dropout)
103
+ self.linear2 = nn.Linear(d_ffn, d_model)
104
+ self.dropout4 = nn.Dropout(dropout)
105
+ self.norm3 = nn.LayerNorm(d_model)
106
+
107
+ @staticmethod
108
+ def with_pos_embed(tensor, pos):
109
+ return tensor if pos is None else tensor + pos
110
+
111
+ def forward_pre(
112
+ self,
113
+ tgt,
114
+ query_pos,
115
+ src,
116
+ src_pos_embed,
117
+ src_padding_mask=None,
118
+ self_attn_mask=None,
119
+ ):
120
+ # self attention
121
+ tgt2 = self.norm2(tgt)
122
+ q = k = self.with_pos_embed(tgt2, query_pos)
123
+ tgt2 = self.self_attn(
124
+ q.transpose(0, 1), k.transpose(0, 1), tgt2.transpose(0, 1), attn_mask=self_attn_mask, need_weights=False
125
+ )[0].transpose(0, 1)
126
+ tgt = tgt + self.dropout2(tgt2)
127
+
128
+ # global cross attention
129
+ tgt2 = self.norm1(tgt)
130
+ tgt2 = self.cross_attn(
131
+ self.with_pos_embed(tgt2, query_pos),
132
+ self.with_pos_embed(src, src_pos_embed),
133
+ src,
134
+ src_padding_mask,
135
+ )
136
+ tgt = tgt + self.dropout1(tgt2)
137
+
138
+ # ffn
139
+ tgt2 = self.norm3(tgt)
140
+ tgt2 = self.linear2(self.dropout3(self.activation(self.linear1(tgt2))))
141
+ tgt = tgt + self.dropout4(tgt2)
142
+
143
+ return tgt
144
+
145
+ def forward_post(
146
+ self,
147
+ tgt,
148
+ query_pos,
149
+ src,
150
+ src_pos_embed,
151
+ src_padding_mask=None,
152
+ self_attn_mask=None,
153
+ ):
154
+ # self attention
155
+ q = k = self.with_pos_embed(tgt, query_pos)
156
+ tgt2 = self.self_attn(
157
+ q.transpose(0, 1), k.transpose(0, 1), tgt.transpose(0, 1), attn_mask=self_attn_mask, need_weights=False
158
+ )[0].transpose(0, 1)
159
+ tgt = tgt + self.dropout2(tgt2)
160
+ tgt = self.norm2(tgt)
161
+
162
+ # cross attention
163
+ tgt2 = self.cross_attn(
164
+ self.with_pos_embed(tgt, query_pos),
165
+ self.with_pos_embed(src, src_pos_embed),
166
+ src,
167
+ src_padding_mask,
168
+ )
169
+ tgt = tgt + self.dropout1(tgt2)
170
+ tgt = self.norm1(tgt)
171
+
172
+ # ffn
173
+ tgt2 = self.linear2(self.dropout3(self.activation(self.linear1(tgt))))
174
+ tgt = tgt + self.dropout4(tgt2)
175
+ tgt = self.norm3(tgt)
176
+
177
+ return tgt
178
+
179
+ def forward(
180
+ self,
181
+ tgt,
182
+ query_pos,
183
+ src,
184
+ src_pos_embed,
185
+ src_padding_mask=None,
186
+ self_attn_mask=None,
187
+ ):
188
+ if self.norm_type == "pre_norm":
189
+ return self.forward_pre(tgt, query_pos, src, src_pos_embed, src_padding_mask, self_attn_mask)
190
+ if self.norm_type == "post_norm":
191
+ return self.forward_post(tgt, query_pos, src, src_pos_embed, src_padding_mask, self_attn_mask)
192
+
193
+
194
+ class GlobalDecoder(nn.Module):
195
+ def __init__(
196
+ self,
197
+ decoder_layer,
198
+ num_layers,
199
+ return_intermediate=False,
200
+ look_forward_twice=False,
201
+ use_checkpoint=False,
202
+ d_model=256,
203
+ norm_type="post_norm",
204
+ ):
205
+ super().__init__()
206
+ self.layers = _get_clones(decoder_layer, num_layers)
207
+ self.num_layers = num_layers
208
+ self.return_intermediate = return_intermediate
209
+ self.look_forward_twice = look_forward_twice
210
+ self.use_checkpoint = use_checkpoint
211
+ # hack implementation for iterative bounding box refinement and two-stage Deformable DETR
212
+ self.bbox_embed = None
213
+ self.class_embed = None
214
+
215
+ self.norm_type = norm_type
216
+ if self.norm_type == "pre_norm":
217
+ self.final_layer_norm = nn.LayerNorm(d_model)
218
+ else:
219
+ self.final_layer_norm = None
220
+
221
+ def _reset_parameters(self):
222
+ # stolen from Swin Transformer
223
+ def _init_weights(m):
224
+ if isinstance(m, nn.Linear):
225
+ nn.init.trunc_normal_(m.weight, std=0.02)
226
+ if isinstance(m, nn.Linear) and m.bias is not None:
227
+ nn.init.constant_(m.bias, 0)
228
+ elif isinstance(m, nn.LayerNorm):
229
+ nn.init.constant_(m.bias, 0)
230
+ nn.init.constant_(m.weight, 1.0)
231
+
232
+ self.apply(_init_weights)
233
+
234
+ def forward(
235
+ self,
236
+ tgt,
237
+ reference_points,
238
+ src,
239
+ src_pos_embed,
240
+ src_spatial_shapes,
241
+ src_level_start_index,
242
+ src_valid_ratios,
243
+ query_pos=None,
244
+ src_padding_mask=None,
245
+ self_attn_mask=None,
246
+ max_shape=None,
247
+ ):
248
+ output = tgt
249
+
250
+ intermediate = []
251
+ intermediate_reference_points = []
252
+ for lid, layer in enumerate(self.layers):
253
+ if self.use_checkpoint:
254
+ output = checkpoint.checkpoint(
255
+ layer,
256
+ output,
257
+ query_pos,
258
+ src,
259
+ src_pos_embed,
260
+ src_padding_mask,
261
+ self_attn_mask,
262
+ )
263
+ else:
264
+ output = layer(
265
+ output,
266
+ query_pos,
267
+ src,
268
+ src_pos_embed,
269
+ src_padding_mask,
270
+ self_attn_mask,
271
+ )
272
+
273
+ if self.final_layer_norm is not None:
274
+ output_after_norm = self.final_layer_norm(output)
275
+ else:
276
+ output_after_norm = output
277
+
278
+ # hack implementation for iterative bounding box refinement
279
+ if self.bbox_embed is not None:
280
+ tmp = self.bbox_embed[lid](output_after_norm)
281
+ if reference_points.shape[-1] == 4:
282
+ new_reference_points = tmp + inverse_sigmoid(reference_points)
283
+ new_reference_points = new_reference_points.sigmoid()
284
+ else:
285
+ assert reference_points.shape[-1] == 2
286
+ new_reference_points = tmp
287
+ new_reference_points[..., :2] = tmp[..., :2] + inverse_sigmoid(reference_points)
288
+ new_reference_points = new_reference_points.sigmoid()
289
+ reference_points = new_reference_points.detach()
290
+
291
+ if self.return_intermediate:
292
+ intermediate.append(output_after_norm)
293
+ intermediate_reference_points.append(
294
+ new_reference_points if self.look_forward_twice else reference_points
295
+ )
296
+
297
+ if self.return_intermediate:
298
+ return torch.stack(intermediate), torch.stack(intermediate_reference_points)
299
+
300
+ return output_after_norm, reference_points
301
+
302
+
303
+ def build_global_ape_decoder(args):
304
+ decoder_layer = GlobalDecoderLayer(
305
+ d_model=args.hidden_dim,
306
+ d_ffn=args.dim_feedforward,
307
+ dropout=args.dropout,
308
+ activation="relu",
309
+ n_heads=args.nheads,
310
+ norm_type=args.norm_type,
311
+ )
312
+ decoder = GlobalDecoder(
313
+ decoder_layer,
314
+ num_layers=args.dec_layers,
315
+ return_intermediate=True,
316
+ look_forward_twice=args.look_forward_twice,
317
+ use_checkpoint=args.decoder_use_checkpoint,
318
+ d_model=args.hidden_dim,
319
+ norm_type=args.norm_type,
320
+ )
321
+ return decoder