# Copyright (c) Meta Platforms, Inc. and affiliates. # # This software may be used and distributed in accordance with # the terms of the DINOv3 License Agreement. import math import torch import torch.distributed as dist import torch.nn.functional as F from torch import nn from dinov3.distributed import get_process_subgroup, get_subgroup_size def lossfunc(t, s, temp): # noqa: F811 return torch.sum(t.float() * F.log_softmax(s.float() / temp, dim=-1), dim=-1) class SinkhornKnoppTeacher(nn.Module): """ NOTE: This is a module and not a function in the `iBOTPatchLoss` class This is because we want to torch.compile it, and torch.compil-ing a single function with the `@torch.compile` decorator is bad. It's better to `module.compile()` it, as we can control when we enable or disable compilation globally. """ @torch.no_grad() def forward(self, teacher_output, teacher_temp, n_masked_patches_tensor, n_iterations=3): teacher_output = teacher_output.float() # world_size = dist.get_world_size() if dist.is_initialized() else 1 Q = torch.exp(teacher_output / teacher_temp).t() # Q is K-by-B for consistency with notations from our paper # B = Q.shape[1] * world_size # number of samples to assign B = n_masked_patches_tensor dist.all_reduce(B, group=get_process_subgroup()) K = Q.shape[0] # how many prototypes # make the matrix sums to 1 sum_Q = torch.sum(Q) if dist.is_initialized(): dist.all_reduce(sum_Q, group=get_process_subgroup()) Q /= sum_Q for _ in range(n_iterations): # normalize each row: total weight per prototype must be 1/K sum_of_rows = torch.sum(Q, dim=1, keepdim=True) if dist.is_initialized(): dist.all_reduce(sum_of_rows, group=get_process_subgroup()) Q /= sum_of_rows Q /= K # normalize each column: total weight per sample must be 1/B Q /= torch.sum(Q, dim=0, keepdim=True) Q /= B Q *= B # the colomns must sum to 1 so that Q is an assignment return Q.t() class iBOTPatchLoss(nn.Module): def __init__(self, patch_out_dim, student_temp=0.1, center_momentum=0.9): super().__init__() self.student_temp = student_temp self.center_momentum = center_momentum self.register_buffer("center", torch.full((1, 1, patch_out_dim), math.nan)) self.updated = True self.reduce_handle = None self.len_teacher_patch_tokens = None self.async_batch_center = None self.sinkhorn_knopp_teacher = SinkhornKnoppTeacher() self.sinkhorn_knopp_teacher.compile() def init_weights(self) -> None: self.center.zero_() @torch.no_grad() def softmax_center_teacher(self, teacher_patch_tokens, teacher_temp, update_centers=True): if update_centers: self.apply_center_update() return F.softmax((teacher_patch_tokens - self.center) / teacher_temp, dim=-1) def forward(self, student_patch_tokens, teacher_patch_tokens, student_masks_flat): """ Cross-entropy between softmax outputs of the teacher and student networks. student_patch_tokens: (B, N, D) tensor teacher_patch_tokens: (B, N, D) tensor student_masks_flat: (B, N) tensor """ t = teacher_patch_tokens s = student_patch_tokens loss = lossfunc(t, s, self.student_temp) loss = torch.sum(loss * student_masks_flat.float(), dim=-1) / student_masks_flat.sum(dim=-1).clamp(min=1.0) return -loss.mean() def forward_masked( self, student_patch_tokens_masked, teacher_patch_tokens_masked, student_masks_flat, n_masked_patches=None, masks_weight=None, ): t = teacher_patch_tokens_masked s = student_patch_tokens_masked # loss = torch.sum(t * F.log_softmax(s / self.student_temp, dim=-1), dim=-1) loss = lossfunc(t, s, self.student_temp) if masks_weight is None: masks_weight = ( (1 / student_masks_flat.sum(-1).clamp(min=1.0)) .unsqueeze(-1) .expand_as(student_masks_flat)[student_masks_flat] ) if n_masked_patches is not None: loss = loss[:n_masked_patches] loss = loss * masks_weight return -loss.sum() / student_masks_flat.shape[0] @torch.no_grad() def update_center(self, teacher_patch_tokens): self.reduce_center_update(teacher_patch_tokens) @torch.no_grad() def reduce_center_update(self, teacher_patch_tokens): self.updated = False self.len_teacher_patch_tokens = len(teacher_patch_tokens) self.async_batch_center = torch.sum(teacher_patch_tokens.mean(1), dim=0, keepdim=True) if dist.is_initialized(): self.reduce_handle = dist.all_reduce(self.async_batch_center, async_op=True, group=get_process_subgroup()) @torch.no_grad() def apply_center_update(self): if self.updated is False: world_size = get_subgroup_size() if dist.is_initialized() else 1 if self.reduce_handle is not None: self.reduce_handle.wait() _t = self.async_batch_center / (self.len_teacher_patch_tokens * world_size) self.center = self.center * self.center_momentum + _t * (1 - self.center_momentum) self.updated = True