from __future__ import annotations """MONA and LoRA adapters for DINOv3 and DGTRS-CLIP. MONA (Multi-Cognitive One-Shot Nested Adaptation, CVPR 2025): Vision-specific adapter with multi-scale depthwise convolutions. Applied to DINOv3 ViT-L/16 (after MSA and MLP in each block). Original: github.com/Leiyi-Hu/mona (MIT license). LoRA (Low-Rank Adaptation): Low-rank matrices on Q/V attention projections. Applied to DGTRS-CLIP text encoder (all 12 transformer blocks). """ import logging import math import coloredlogs import torch import torch.nn as nn import torch.nn.functional as F LOGGER = logging.getLogger("caption_test.adapters") coloredlogs.install(level="INFO", logger=LOGGER, fmt="%(asctime)s %(name)s %(levelname)s %(message)s") # --------------------------------------------------------------------------- # MONA adapter (for DINOv3 ViT — 2D vision tokens) # --------------------------------------------------------------------------- class MonaOp(nn.Module): """Multi-cognitive visual filter: parallel depthwise convs (3×3, 5×5, 7×7).""" def __init__(self, channels: int) -> None: super().__init__() self.conv3 = nn.Conv2d(channels, channels, kernel_size=3, padding=1, groups=channels) self.conv5 = nn.Conv2d(channels, channels, kernel_size=5, padding=2, groups=channels) self.conv7 = nn.Conv2d(channels, channels, kernel_size=7, padding=3, groups=channels) self.projector = nn.Conv2d(channels, channels, kernel_size=1) def forward(self, x: torch.Tensor) -> torch.Tensor: identity = x x = (self.conv3(x) + self.conv5(x) + self.conv7(x)) / 3.0 + identity return x + self.projector(x) class MonaAdapter(nn.Module): """MONA adapter module for ViT blocks. ScaledLayerNorm → Down(dim→bottleneck) → reshape 2D → MonaOp → reshape 1D → GELU → Dropout → Up(bottleneck→dim) + residual Args: dim: Input feature dimension (e.g. 1024 for ViT-L). bottleneck: Bottleneck dimension for down/up projections. dropout: Dropout rate. """ def __init__(self, dim: int = 1024, bottleneck: int = 64, dropout: float = 0.1) -> None: super().__init__() self.down = nn.Linear(dim, bottleneck) self.up = nn.Linear(bottleneck, dim) self.mona_op = MonaOp(bottleneck) self.norm = nn.LayerNorm(dim) self.dropout = nn.Dropout(p=dropout) # Scaled init: gamma ≈ 0 at start (near-identity), gammax ≈ 1. self.gamma = nn.Parameter(torch.ones(dim) * 1e-6) self.gammax = nn.Parameter(torch.ones(dim)) def forward(self, x: torch.Tensor, hw: tuple[int, int]) -> torch.Tensor: """Apply MONA adapter (runs in fp32 to avoid AMP overflow). Args: x: Token features [B, N, D] where N includes CLS + register + patch tokens. hw: (H, W) spatial shape of patch tokens. Returns: Adapted features [B, N, D] (same shape, residual connection). """ with torch.amp.autocast("cuda", enabled=False): return self._forward_fp32(x.float(), hw) def _forward_fp32(self, x: torch.Tensor, hw: tuple[int, int]) -> torch.Tensor: identity = x # Scaled LayerNorm. x = self.norm(x) * self.gamma + x * self.gammax x = self.down(x) # [B, N, bottleneck] B, N, C = x.shape H, W = hw n_special = N - H * W # CLS + register tokens # Separate special tokens (CLS, registers) from patch tokens. special = x[:, :n_special] # [B, n_special, C] patches = x[:, n_special:] # [B, H*W, C] # Reshape patches to 2D for convolutions. patches = patches.reshape(B, H, W, C).permute(0, 3, 1, 2) # [B, C, H, W] patches = self.mona_op(patches) patches = patches.permute(0, 2, 3, 1).reshape(B, H * W, C) # [B, H*W, C] # Recombine. x = torch.cat([special, patches], dim=1) # [B, N, C] x = F.gelu(x) x = self.dropout(x) x = self.up(x) # [B, N, D] return identity + x # --------------------------------------------------------------------------- # LoRA adapter (for DGTRS-CLIP text encoder — 1D text tokens) # --------------------------------------------------------------------------- class LoRALinear(nn.Module): """LoRA adapter for a linear layer: output = W·x + (B·A)·x. Args: in_features: Input dimension. out_features: Output dimension. rank: LoRA rank (low-rank decomposition dimension). alpha: LoRA scaling factor (effective scale = alpha / rank). """ def __init__( self, in_features: int, out_features: int, rank: int = 4, alpha: float = 1.0, ) -> None: super().__init__() self.rank = rank self.scale = alpha / rank # A: down-projection, B: up-projection. Init: A ~ N(0, 1/rank), B = 0. self.A = nn.Parameter(torch.randn(rank, in_features) / math.sqrt(rank)) self.B = nn.Parameter(torch.zeros(out_features, rank)) def forward(self, x: torch.Tensor) -> torch.Tensor: """Compute LoRA delta: (B @ A) @ x * scale. Always fp32.""" with torch.amp.autocast("cuda", enabled=False): x = x.float() return (x @ self.A.t() @ self.B.t()) * self.scale # --------------------------------------------------------------------------- # Injection functions — apply adapters to existing frozen models # --------------------------------------------------------------------------- def inject_mona_into_dinov3( model: nn.Module, bottleneck: int = 64, dropout: float = 0.1, ) -> int: """Inject MONA adapters into a frozen DINOv3ViT model. Adds two MonaAdapter modules per block (after attention, after MLP). Only adapter parameters are trainable. Args: model: DINOv3ViT model (already frozen). bottleneck: MONA bottleneck dimension. dropout: MONA dropout rate. Returns: Number of trainable adapter parameters added. """ dim = model.embed_dim n_adapters = 0 for block in model.layer: # Create adapters. block.mona_attn = MonaAdapter(dim, bottleneck, dropout) block.mona_mlp = MonaAdapter(dim, bottleneck, dropout) # Wrap the original forward to inject adapters. original_forward = block.forward def make_mona_forward(blk, orig_fwd): def mona_forward(x: torch.Tensor) -> torch.Tensor: B, N, C = x.shape # DINOv3: [CLS, 4 registers, H*W patches] n_special = 1 + 4 # CLS + registers n_patches = N - n_special H = W = int(math.sqrt(n_patches)) # Original block forward (attention + MLP with layer scale). x = x + blk.layer_scale1(blk.attention(blk.norm1(x))) x = blk.mona_attn(x, (H, W)) x = x + blk.layer_scale2(blk.mlp(blk.norm2(x))) x = blk.mona_mlp(x, (H, W)) return x return mona_forward block.forward = make_mona_forward(block, original_forward) n_adapters += 2 n_params = sum(p.numel() for n, p in model.named_parameters() if "mona" in n) LOGGER.info( "🔧 MONA injected: %d adapters (%d per block), %s trainable params (bottleneck=%d)", n_adapters, 2, f"{n_params:,}", bottleneck, ) return n_params def inject_lora_into_dgtrs( model: nn.Module, rank: int = 4, alpha: float = 1.0, ) -> int: """Inject LoRA adapters into DGTRS-CLIP text encoder attention layers. Adds LoRA to Q and V projections in each ResidualAttentionBlock. Original weights stay frozen, LoRA params are trainable. Args: model: DGTRSTextEncoder model. rank: LoRA rank. alpha: LoRA scaling factor. Returns: Number of trainable LoRA parameters added. """ n_adapted = 0 for i, block in enumerate(model.transformer.resblocks): attn = block.attn # nn.MultiheadAttention uses in_proj_weight [3*dim, dim] for Q, K, V. embed_dim = attn.embed_dim # LoRA on Q and V (not K — standard practice). block.lora_q = LoRALinear(embed_dim, embed_dim, rank, alpha) block.lora_v = LoRALinear(embed_dim, embed_dim, rank, alpha) # Wrap attention forward to add LoRA deltas. original_attention = block.attention def make_lora_attention(blk): def lora_attention(x: torch.Tensor) -> torch.Tensor: # x shape: [T, B, D] (sequence-first for nn.MultiheadAttention) attn_mod = blk.attn attn_mask = blk.attn_mask if attn_mask is not None: attn_mask = attn_mask.to(dtype=x.dtype, device=x.device) # Compute QKV using original in_proj_weight. T, B, D = x.shape # Original QKV. qkv = F.linear(x, attn_mod.in_proj_weight, attn_mod.in_proj_bias) q, k, v = qkv.chunk(3, dim=-1) # Add LoRA deltas to Q and V. # LoRA expects [B, T, D], but x is [T, B, D]. x_btd = x.permute(1, 0, 2) # [B, T, D] q = q + blk.lora_q(x_btd).permute(1, 0, 2) v = v + blk.lora_v(x_btd).permute(1, 0, 2) # Multi-head attention. num_heads = attn_mod.num_heads head_dim = D // num_heads q = q.reshape(T, B * num_heads, head_dim).transpose(0, 1) k = k.reshape(T, B * num_heads, head_dim).transpose(0, 1) v = v.reshape(T, B * num_heads, head_dim).transpose(0, 1) attn_output = F.scaled_dot_product_attention( q, k, v, attn_mask=attn_mask, ) attn_output = attn_output.transpose(0, 1).reshape(T, B, D) attn_output = attn_mod.out_proj(attn_output) return attn_output return lora_attention block.attention = make_lora_attention(block) n_adapted += 1 n_params = sum( p.numel() for n, p in model.named_parameters() if "lora" in n ) LOGGER.info( "🔧 LoRA injected: %d blocks, rank=%d, %s trainable params", n_adapted, rank, f"{n_params:,}", ) return n_params