Asymmetric encoders + MONA all 24 blocks + 1024-dim + hard negatives

Architecture changes:
- Asymmetric DINOv3: WEB (drone) + SAT (satellite) with separate MONA
- MONA on all 24 blocks per encoder (was last 12)
- Remove projection, native 1024-dim retrieval space (was 512)
- Total: 748M params, 17.6M trainable (2.35%)

Hard negative memory bank:
- MoCo-style FIFO queue of 4096 detached gallery embeddings
- Each batch: B in-batch + Q queue negatives in InfoNCE
- Queue updated after each forward pass

Training config:
- batch_size=8, grad_accum=8, effective_batch=64
- eval_every=1 (eval + train recall every epoch)
- Max bs=24 with grad checkpointing on RTX 4090

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
pikaliov
2026-04-24 08:47:33 +03:00
parent 75a4350d18
commit 04d5307221
5 changed files with 142 additions and 41 deletions

View File

@@ -31,6 +31,7 @@ from tqdm import tqdm
from src.datasets.gtauav_dataset import GTAUAVDataset, collate_gtauav_batch
from src.losses.multi_infonce import InfoNCELoss
from src.losses.hard_negatives import NegativeMemoryBank
from src.training.plot_metrics import generate_plots
from src.training.trackers import ExperimentTracker
from src.training.grad_monitor import compute_gradient_norms, log_gradient_summary
@@ -73,7 +74,7 @@ class TrainConfigGTAUAV:
lrsclip_path: str = _LRSCLIP
init_gate: float = 0.7
baseline_mode: bool = False
shared_encoder: bool = True # single DINOv3 WEB for both branches (saves ~4-5 GB)
shared_encoder: bool = False # asymmetric: WEB (drone) + SAT (satellite)
gradient_checkpointing: bool = True # trade compute for VRAM (allows larger batch)
# Training.
@@ -99,6 +100,7 @@ class TrainConfigGTAUAV:
weight_q2g: float = 0.6
weight_g2q: float = 0.4
learnable_temperature: bool = True
neg_bank_size: int = 4096 # hard negative memory bank size (0 = disabled)
# Tracking & diagnostics.
use_wandb: bool = False
@@ -405,6 +407,7 @@ def train(cfg: TrainConfigGTAUAV) -> None:
shared_encoder=cfg.shared_encoder,
device=cfg.device,
).to(cfg.device)
LOGGER.info("embed_dim=%d", model.embed_dim)
# --- Gradient checkpointing (trade compute for VRAM) ---
if cfg.gradient_checkpointing:
@@ -446,6 +449,12 @@ def train(cfg: TrainConfigGTAUAV) -> None:
cfg.tau_init,
)
# Hard negative memory bank.
neg_bank = None
if cfg.neg_bank_size > 0:
neg_bank = NegativeMemoryBank(size=cfg.neg_bank_size, dim=model.embed_dim).to(cfg.device)
LOGGER.info("Negative memory bank: size=%d, dim=%d", cfg.neg_bank_size, model.embed_dim)
# Data — separate transforms for train (augmented) and eval (clean).
drone_train_tf = get_drone_train_transform(image_size=256)
sat_train_tf = get_satellite_train_transform(image_size=256)
@@ -599,13 +608,19 @@ def train(cfg: TrainConfigGTAUAV) -> None:
sat_caption_l2=batch["sat_caption_l2"],
sat_caption_l3=batch["sat_caption_l3"],
)
# Loss in fp32 (learnable temperature gradient overflows in fp16).
# Loss in fp32 with optional hard negative queue.
queue_neg = neg_bank.get_queue() if neg_bank is not None else None
loss_dict = loss_fn(
embeddings=embeddings,
epoch=epoch,
total_epochs=cfg.epochs,
queue_negatives=queue_neg,
)
# Enqueue current gallery embeddings (detached).
if neg_bank is not None:
neg_bank.enqueue(embeddings["gallery"].detach())
# Scale loss by accumulation steps so gradients average correctly.
raw_loss = float(loss_dict["total"].item()) # save before backward
total_loss = loss_dict["total"] / accum