Add MONA adapters for DINOv3 + LoRA for DGTRS-CLIP text encoder

MONA (Multi-Cognitive One-Shot Nested Adaptation, CVPR 2025):
- 2 adapters per DINOv3 block (after MSA, after MLP) × 24 layers × 2 encoders
- MonaOp: parallel DWConv 3×3/5×5/7×7 + 1×1 projector
- ScaledLayerNorm + down(1024→64) + MonaOp + GELU + up(64→1024)
- 7M params per encoder, 14M total for drone+sat

LoRA (Low-Rank Adaptation):
- Q and V projections in all 12 DGTRS-CLIP transformer blocks
- rank=4, ~147K params total
- Replaces partial unfreeze (was ~7.6M for last block)

Both adapters run in fp32 (torch.amp.autocast disabled) to avoid
AMP gradient overflow. Frozen backbone layers still run in fp16.

Total trainable: 17.6M / 748M (2.35%)
  MONA (2×DINOv3): 14.0M
  LoRA (DGTRS):    147K
  TextFusionMLP:   3.4M
  Gates + logit:   3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
pikaliov
2026-04-21 19:24:01 +03:00
parent 0c41c1f017
commit a39f8a9655
2 changed files with 298 additions and 27 deletions

View File

@@ -25,6 +25,7 @@ LOGGER = logging.getLogger("caption_test.model")
coloredlogs.install(level="INFO", logger=LOGGER, fmt="%(asctime)s %(name)s %(levelname)s %(message)s")
from safetensors.torch import load_file as load_safetensors
from src.models.adapters import inject_lora_into_dgtrs, inject_mona_into_dinov3
from src.models.dgtrs.model import DGTRSTextEncoder, load_dgtrs_text_encoder, tokenize_dgtrs
from src.models.dual_encoder import GatedFusion, ProjectionHead
@@ -279,6 +280,8 @@ class AsymmetricEncoder(nn.Module):
lrsclip_path: str = "nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt",
init_gate: float = 0.7,
baseline_mode: bool = False,
mona_bottleneck: int = 64,
lora_rank: int = 4,
device: str = "cuda",
) -> None:
super().__init__()
@@ -286,17 +289,19 @@ class AsymmetricEncoder(nn.Module):
self.baseline_mode = baseline_mode
self.device = device
# Image encoders (frozen).
# Image encoders (frozen + MONA adapters).
self.drone_encoder = DINOv3ViT.from_pretrained(dino_web_path)
self.sat_encoder = DINOv3ViT.from_pretrained(dino_sat_path)
self._freeze(self.drone_encoder)
self._freeze(self.sat_encoder)
inject_mona_into_dinov3(self.drone_encoder, bottleneck=mona_bottleneck)
inject_mona_into_dinov3(self.sat_encoder, bottleneck=mona_bottleneck)
# Text encoder — official DGTRS architecture (partial unfreeze).
# Text encoder — official DGTRS architecture (frozen + LoRA).
if not baseline_mode:
self.text_encoder = load_dgtrs_text_encoder(lrsclip_path)
self._freeze(self.text_encoder)
self._unfreeze_text_last_block()
inject_lora_into_dgtrs(self.text_encoder, rank=lora_rank)
else:
self.text_encoder = None
@@ -317,33 +322,13 @@ class AsymmetricEncoder(nn.Module):
p.requires_grad = False
module.eval()
def _unfreeze_text_last_block(self) -> None:
"""Unfreeze last transformer block + text_projection + ln_final."""
if self.text_encoder is None:
return
# Last resblock.
for p in self.text_encoder.transformer.resblocks[-1].parameters():
p.requires_grad = True
# ln_final.
for p in self.text_encoder.ln_final.parameters():
p.requires_grad = True
# text_projection.
tp = self.text_encoder.text_projection
if isinstance(tp, nn.Parameter):
tp.requires_grad = True
elif isinstance(tp, nn.Module):
for p in tp.parameters():
p.requires_grad = True
def encode_drone(self, images: torch.Tensor) -> torch.Tensor:
"""Encode drone images. Returns [B, DINO_DIM]."""
with torch.no_grad():
return self.drone_encoder(images)
"""Encode drone images with MONA adapters. Returns [B, DINO_DIM]."""
return self.drone_encoder(images)
def encode_satellite(self, images: torch.Tensor) -> torch.Tensor:
"""Encode satellite images. Returns [B, DINO_DIM]."""
with torch.no_grad():
return self.sat_encoder(images)
"""Encode satellite images with MONA adapters. Returns [B, DINO_DIM]."""
return self.sat_encoder(images)
def encode_text_levels(
self,