Fix TextEncoder.unfreeze_last: compatible with both AutoModel (Qwen3Model.layers) and ForCausalLM (model.model.layers)
Browse files- train_mrjepa.py +26 -59
train_mrjepa.py
CHANGED
|
@@ -178,8 +178,6 @@ class VisualBackbone(nn.Module):
|
|
| 178 |
pretrained=True,
|
| 179 |
num_classes=0,
|
| 180 |
)
|
| 181 |
-
# DINOv3 via timm: forward_features returns [B, num_tokens, D]
|
| 182 |
-
# includes CLS + 4 registers + patches
|
| 183 |
self._skip = 5 # CLS + 4 reg
|
| 184 |
self._use_timm = True
|
| 185 |
else:
|
|
@@ -216,8 +214,8 @@ class VisualBackbone(nn.Module):
|
|
| 216 |
|
| 217 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 218 |
if self._use_timm:
|
| 219 |
-
feats = self.model.forward_features(x)
|
| 220 |
-
return feats[:, self._skip:]
|
| 221 |
else:
|
| 222 |
out = self.model(pixel_values=x)
|
| 223 |
return out.last_hidden_state[:, self._skip:]
|
|
@@ -245,7 +243,15 @@ class TextEncoder(nn.Module):
|
|
| 245 |
p.requires_grad = False
|
| 246 |
|
| 247 |
def unfreeze_last(self, n: int):
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
for i, lay in enumerate(layers):
|
| 250 |
if i >= len(layers) - n:
|
| 251 |
for p in lay.parameters():
|
|
@@ -253,7 +259,7 @@ class TextEncoder(nn.Module):
|
|
| 253 |
|
| 254 |
def forward(self, input_ids, attention_mask):
|
| 255 |
out = self.model(input_ids=input_ids, attention_mask=attention_mask)
|
| 256 |
-
return out.last_hidden_state
|
| 257 |
|
| 258 |
|
| 259 |
# ──────────────────────────────────────────────────────────
|
|
@@ -266,7 +272,7 @@ class EvidenceMemory(nn.Module):
|
|
| 266 |
self.queries = nn.Parameter(torch.randn(1, cfg.num_evidence_tokens, D) * 0.02)
|
| 267 |
self.vis_proj = nn.Sequential(nn.Linear(cfg.backbone_dim, D), nn.LayerNorm(D), nn.GELU(), nn.Linear(D, D))
|
| 268 |
self.txt_proj = nn.Sequential(nn.Linear(cfg.text_dim, D), nn.LayerNorm(D), nn.GELU(), nn.Linear(D, D))
|
| 269 |
-
self.mod_emb = nn.Embedding(2, D)
|
| 270 |
self.layers = nn.ModuleList([
|
| 271 |
PerceiverLayer(D, cfg.num_heads) for _ in range(cfg.num_cross_attn_layers)
|
| 272 |
])
|
|
@@ -351,9 +357,7 @@ class LatentRollout(nn.Module):
|
|
| 351 |
self.step_emb = nn.Parameter(torch.randn(cfg.K + 1, 1, D) * 0.02)
|
| 352 |
else:
|
| 353 |
self.step_emb = None
|
| 354 |
-
# Evidence dim projection to rollout dim if they differ
|
| 355 |
self.ev_proj = nn.Linear(cfg.evidence_dim, D) if cfg.evidence_dim != D else nn.Identity()
|
| 356 |
-
# Shared predictor (weight-tied across K steps)
|
| 357 |
self.predictor = nn.ModuleList([
|
| 358 |
PredictorBlock(D, cfg.predictor_heads, cfg.predictor_ffn, use_gate=cfg.use_evidence_gate)
|
| 359 |
for _ in range(cfg.num_predictor_layers)
|
|
@@ -374,7 +378,7 @@ class LatentRollout(nn.Module):
|
|
| 374 |
if self.step_emb is not None:
|
| 375 |
z = z + self.step_emb[k]
|
| 376 |
traj.append(z)
|
| 377 |
-
traj_t = torch.stack(traj, dim=1)
|
| 378 |
B2, Kp1, N, D2 = traj_t.shape
|
| 379 |
proj = self.out_proj(traj_t.reshape(-1, D2)).reshape(B2, Kp1, N, D2)
|
| 380 |
return traj_t, traj[-1], proj
|
|
@@ -431,7 +435,6 @@ class SIGReg(nn.Module):
|
|
| 431 |
|
| 432 |
|
| 433 |
class VICReg(nn.Module):
|
| 434 |
-
"""VICReg-style anti-collapse: variance + covariance regularization."""
|
| 435 |
def __init__(self, var_weight=1.0, cov_weight=0.04):
|
| 436 |
super().__init__()
|
| 437 |
self.var_w = var_weight
|
|
@@ -439,10 +442,8 @@ class VICReg(nn.Module):
|
|
| 439 |
|
| 440 |
def forward(self, z):
|
| 441 |
if z.dim() == 3: z = z.reshape(-1, z.size(-1))
|
| 442 |
-
# Variance: penalize if std drops below 1
|
| 443 |
std = z.std(dim=0)
|
| 444 |
var_loss = F.relu(1.0 - std).mean()
|
| 445 |
-
# Covariance: penalize off-diagonal correlations
|
| 446 |
z_c = z - z.mean(dim=0, keepdim=True)
|
| 447 |
N = z_c.size(0)
|
| 448 |
cov = (z_c.T @ z_c) / max(N - 1, 1)
|
|
@@ -460,7 +461,6 @@ class JEPALoss(nn.Module):
|
|
| 460 |
self.vicreg = VICReg(cfg.vicreg_var, cfg.vicreg_cov) if cfg.use_vicreg else None
|
| 461 |
|
| 462 |
def forward(self, pred_traj, target_traj, task_loss, gen_loss=None):
|
| 463 |
-
# Prediction loss (skip z0)
|
| 464 |
p = pred_traj[:, 1:]
|
| 465 |
t = target_traj[:, 1:]
|
| 466 |
if self.cfg.loss_fn == "smooth_l1":
|
|
@@ -515,8 +515,8 @@ class DiscriminativeHead(nn.Module):
|
|
| 515 |
q = self.pool_q.expand(B, -1, -1)
|
| 516 |
z_n = self.pool_n(z_final)
|
| 517 |
pooled, _ = self.pool_attn(q, z_n, z_n)
|
| 518 |
-
pooled = pooled.squeeze(1)
|
| 519 |
-
opt = self.opt_proj(opt_emb)
|
| 520 |
z_exp = pooled.unsqueeze(1).expand_as(opt)
|
| 521 |
combined = torch.cat([z_exp, opt, z_exp * opt], dim=-1)
|
| 522 |
logits = self.scorer(combined).squeeze(-1)
|
|
@@ -557,7 +557,6 @@ class ScienceQADataset(Dataset):
|
|
| 557 |
question = row["question"]
|
| 558 |
hint = row.get("hint", "")
|
| 559 |
|
| 560 |
-
# Build text: question + hint + options
|
| 561 |
text = question
|
| 562 |
if hint:
|
| 563 |
text += f" Context: {hint}"
|
|
@@ -585,18 +584,13 @@ def collate_fn(batch, transform, tokenizer, max_len, max_opts, device_hint="cpu"
|
|
| 585 |
all_choices.append(s["choices"])
|
| 586 |
answers.append(s["answer"])
|
| 587 |
|
| 588 |
-
# Image preprocessing
|
| 589 |
if hasattr(transform, '__call__'):
|
| 590 |
-
# timm transform
|
| 591 |
pixel_values = torch.stack([transform(img) for img in images])
|
| 592 |
else:
|
| 593 |
-
# HF processor
|
| 594 |
pixel_values = transform(images=images, return_tensors="pt")["pixel_values"]
|
| 595 |
|
| 596 |
-
# Text tokenization
|
| 597 |
tok = tokenizer(texts, padding="max_length", truncation=True, max_length=max_len, return_tensors="pt")
|
| 598 |
|
| 599 |
-
# Options: encode each option text
|
| 600 |
opt_texts = []
|
| 601 |
opt_mask = []
|
| 602 |
for choices in all_choices:
|
|
@@ -637,13 +631,11 @@ class MRJEPAModel(nn.Module):
|
|
| 637 |
self._use_rollout = cfg.K > 0
|
| 638 |
|
| 639 |
def encode_options(self, opt_ids, opt_mask_attn):
|
| 640 |
-
"""Encode option texts via text encoder, return CLS embeddings."""
|
| 641 |
with torch.no_grad():
|
| 642 |
-
h = self.txt(opt_ids, opt_mask_attn).float()
|
| 643 |
-
# Mean pooling over non-padding tokens
|
| 644 |
mask_expanded = opt_mask_attn.unsqueeze(-1).float()
|
| 645 |
pooled = (h * mask_expanded).sum(1) / mask_expanded.sum(1).clamp(min=1)
|
| 646 |
-
return pooled
|
| 647 |
|
| 648 |
def forward(self, pixel_values, input_ids, attention_mask,
|
| 649 |
opt_input_ids, opt_attention_mask, opt_mask, labels, batch_size, **kw):
|
|
@@ -651,18 +643,14 @@ class MRJEPAModel(nn.Module):
|
|
| 651 |
B = batch_size
|
| 652 |
O = self.cfg.max_options
|
| 653 |
|
| 654 |
-
# 1) Visual features (frozen)
|
| 655 |
with torch.no_grad():
|
| 656 |
-
vis_tok = self.vis(pixel_values).float()
|
| 657 |
|
| 658 |
-
# 2) Text features (frozen)
|
| 659 |
with torch.no_grad():
|
| 660 |
-
txt_tok = self.txt(input_ids, attention_mask).float()
|
| 661 |
|
| 662 |
-
|
| 663 |
-
evidence, _, ev_mask = self.evidence(vis_tok, txt_tok, attention_mask) # [B, N_ev, D_ev]
|
| 664 |
|
| 665 |
-
# 4) Rollout
|
| 666 |
if self._use_rollout:
|
| 667 |
traj, z_final, z_proj = self.rollout(evidence)
|
| 668 |
else:
|
|
@@ -674,18 +662,15 @@ class MRJEPAModel(nn.Module):
|
|
| 674 |
traj = z0.unsqueeze(1)
|
| 675 |
z_proj = self.rollout.out_proj(z0).unsqueeze(1)
|
| 676 |
|
| 677 |
-
# 5) Target encoder (JEPA targets)
|
| 678 |
target_proj = None
|
| 679 |
if self._use_jepa and self.training:
|
| 680 |
target_proj = self.target(vis_tok.detach(), txt_tok.detach(), attention_mask.detach())
|
| 681 |
|
| 682 |
-
|
| 683 |
-
opt_emb =
|
| 684 |
-
|
| 685 |
-
logits = self.disc(z_final, opt_emb, opt_mask) # [B, O]
|
| 686 |
task_loss = F.cross_entropy(logits, labels)
|
| 687 |
|
| 688 |
-
# 7) JEPA loss
|
| 689 |
if self._use_jepa and self.training and target_proj is not None:
|
| 690 |
losses = self.jepa_loss(z_proj, target_proj, task_loss)
|
| 691 |
else:
|
|
@@ -709,7 +694,6 @@ def train(cfg: Config):
|
|
| 709 |
|
| 710 |
os.makedirs(cfg.output_dir, exist_ok=True)
|
| 711 |
|
| 712 |
-
# ── Trackio ──
|
| 713 |
try:
|
| 714 |
import trackio
|
| 715 |
tracker = trackio.init(name=cfg.run_name, project="MR-JEPA")
|
|
@@ -718,18 +702,15 @@ def train(cfg: Config):
|
|
| 718 |
log.warning(f"Trackio init failed: {e}")
|
| 719 |
tracker = None
|
| 720 |
|
| 721 |
-
# ── Model ──
|
| 722 |
log.info("Building model...")
|
| 723 |
model = MRJEPAModel(cfg)
|
| 724 |
|
| 725 |
-
# Count params
|
| 726 |
total_p = sum(p.numel() for p in model.parameters())
|
| 727 |
train_p = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 728 |
log.info(f"Total params: {total_p:,} | Trainable: {train_p:,} ({100*train_p/total_p:.1f}%)")
|
| 729 |
|
| 730 |
model = model.to(device)
|
| 731 |
|
| 732 |
-
# ── Data ──
|
| 733 |
transform = model.vis.get_transform()
|
| 734 |
tokenizer = model.txt.tokenizer
|
| 735 |
|
|
@@ -746,7 +727,6 @@ def train(cfg: Config):
|
|
| 746 |
eval_dl = DataLoader(eval_ds, batch_size=cfg.batch_size, shuffle=False, num_workers=cfg.num_workers,
|
| 747 |
collate_fn=coll, pin_memory=True)
|
| 748 |
|
| 749 |
-
# ── Optimizer ──
|
| 750 |
trainable = [p for p in model.parameters() if p.requires_grad]
|
| 751 |
optimizer = AdamW(trainable, lr=cfg.lr, weight_decay=cfg.weight_decay)
|
| 752 |
total_steps = cfg.epochs * len(train_dl) // cfg.grad_accum
|
|
@@ -760,7 +740,6 @@ def train(cfg: Config):
|
|
| 760 |
|
| 761 |
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
|
| 762 |
|
| 763 |
-
# ── Training ──
|
| 764 |
log.info(f"Starting training: {cfg.epochs} epochs, {len(train_dl)} batches/epoch, "
|
| 765 |
f"grad_accum={cfg.grad_accum}, total_steps≈{total_steps}")
|
| 766 |
|
|
@@ -776,7 +755,6 @@ def train(cfg: Config):
|
|
| 776 |
optimizer.zero_grad()
|
| 777 |
|
| 778 |
for batch_idx, batch in enumerate(train_dl):
|
| 779 |
-
# Move to device
|
| 780 |
batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items()}
|
| 781 |
|
| 782 |
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=cfg.bf16 and device.type == "cuda"):
|
|
@@ -793,7 +771,6 @@ def train(cfg: Config):
|
|
| 793 |
model.update_target(global_step, total_steps)
|
| 794 |
global_step += 1
|
| 795 |
|
| 796 |
-
# Metrics
|
| 797 |
for k, v in losses.items():
|
| 798 |
if isinstance(v, torch.Tensor):
|
| 799 |
epoch_losses[k].append(v.item())
|
|
@@ -801,7 +778,6 @@ def train(cfg: Config):
|
|
| 801 |
epoch_correct += correct
|
| 802 |
epoch_total += batch["batch_size"]
|
| 803 |
|
| 804 |
-
# Log every 50 batches
|
| 805 |
if batch_idx % 50 == 0:
|
| 806 |
avg = {k: np.mean(v[-50:]) for k, v in epoch_losses.items()}
|
| 807 |
acc = epoch_correct / max(epoch_total, 1) * 100
|
|
@@ -825,7 +801,6 @@ def train(cfg: Config):
|
|
| 825 |
})
|
| 826 |
except: pass
|
| 827 |
|
| 828 |
-
# ── Eval ──
|
| 829 |
train_acc = epoch_correct / max(epoch_total, 1) * 100
|
| 830 |
eval_acc = evaluate(model, eval_dl, device, cfg)
|
| 831 |
log.info(f"=== Epoch {epoch} done | Train acc: {train_acc:.1f}% | Eval acc: {eval_acc:.1f}% ===")
|
|
@@ -844,7 +819,6 @@ def train(cfg: Config):
|
|
| 844 |
|
| 845 |
log.info(f"Training complete. Best eval accuracy: {best_acc:.1f}%")
|
| 846 |
|
| 847 |
-
# ── Push to Hub ──
|
| 848 |
if cfg.push_to_hub:
|
| 849 |
push_results(cfg, best_acc)
|
| 850 |
|
|
@@ -885,7 +859,6 @@ def save_checkpoint(model, cfg, epoch, acc, is_best=False):
|
|
| 885 |
|
| 886 |
|
| 887 |
def push_results(cfg, best_acc):
|
| 888 |
-
"""Push results summary to Hub."""
|
| 889 |
try:
|
| 890 |
from huggingface_hub import HfApi
|
| 891 |
api = HfApi()
|
|
@@ -916,7 +889,6 @@ def push_results(cfg, best_acc):
|
|
| 916 |
repo_id=cfg.hub_model_id,
|
| 917 |
repo_type="model",
|
| 918 |
)
|
| 919 |
-
# Upload best checkpoint
|
| 920 |
best_ckpt = os.path.join(cfg.output_dir, "checkpoint_best.pt")
|
| 921 |
if os.path.exists(best_ckpt):
|
| 922 |
api.upload_file(
|
|
@@ -946,8 +918,8 @@ def parse_args():
|
|
| 946 |
p.add_argument("--no_jepa", action="store_true")
|
| 947 |
p.add_argument("--no_rollout", action="store_true")
|
| 948 |
p.add_argument("--no_evidence_gate", action="store_true")
|
| 949 |
-
p.add_argument("--no_sigreg", action="store_true"
|
| 950 |
-
p.add_argument("--use_vicreg", action="store_true"
|
| 951 |
p.add_argument("--purist", action="store_true")
|
| 952 |
p.add_argument("--sigreg_weight", type=float, default=0.1)
|
| 953 |
p.add_argument("--max_train_samples", type=int, default=0)
|
|
@@ -982,11 +954,6 @@ def main():
|
|
| 982 |
|
| 983 |
if args.no_rollout:
|
| 984 |
cfg.K = 0
|
| 985 |
-
# JEPA requires a multi-step trajectory to supervise; with K=0 there is
|
| 986 |
-
# only z₀, so JEPA loss is undefined. This is intentional — the no_rollout
|
| 987 |
-
# ablation tests whether iterative refinement + JEPA together add value
|
| 988 |
-
# vs. a single-pass baseline. To test JEPA loss in isolation, use --no_jepa
|
| 989 |
-
# with K>0 instead.
|
| 990 |
cfg.use_jepa = False
|
| 991 |
|
| 992 |
if args.purist:
|
|
|
|
| 178 |
pretrained=True,
|
| 179 |
num_classes=0,
|
| 180 |
)
|
|
|
|
|
|
|
| 181 |
self._skip = 5 # CLS + 4 reg
|
| 182 |
self._use_timm = True
|
| 183 |
else:
|
|
|
|
| 214 |
|
| 215 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 216 |
if self._use_timm:
|
| 217 |
+
feats = self.model.forward_features(x)
|
| 218 |
+
return feats[:, self._skip:]
|
| 219 |
else:
|
| 220 |
out = self.model(pixel_values=x)
|
| 221 |
return out.last_hidden_state[:, self._skip:]
|
|
|
|
| 243 |
p.requires_grad = False
|
| 244 |
|
| 245 |
def unfreeze_last(self, n: int):
|
| 246 |
+
# Compatible with both AutoModel (Qwen3Model with .layers)
|
| 247 |
+
# and AutoModelForCausalLM (Qwen3ForCausalLM with .model.layers)
|
| 248 |
+
if hasattr(self.model, 'layers'):
|
| 249 |
+
layers = self.model.layers # AutoModel returns Qwen3Model directly
|
| 250 |
+
elif hasattr(self.model, 'model') and hasattr(self.model.model, 'layers'):
|
| 251 |
+
layers = self.model.model.layers # ForCausalLM wraps in .model
|
| 252 |
+
else:
|
| 253 |
+
log.warning("Could not find transformer layers in text encoder for unfreezing")
|
| 254 |
+
return
|
| 255 |
for i, lay in enumerate(layers):
|
| 256 |
if i >= len(layers) - n:
|
| 257 |
for p in lay.parameters():
|
|
|
|
| 259 |
|
| 260 |
def forward(self, input_ids, attention_mask):
|
| 261 |
out = self.model(input_ids=input_ids, attention_mask=attention_mask)
|
| 262 |
+
return out.last_hidden_state
|
| 263 |
|
| 264 |
|
| 265 |
# ──────────────────────────────────────────────────────────
|
|
|
|
| 272 |
self.queries = nn.Parameter(torch.randn(1, cfg.num_evidence_tokens, D) * 0.02)
|
| 273 |
self.vis_proj = nn.Sequential(nn.Linear(cfg.backbone_dim, D), nn.LayerNorm(D), nn.GELU(), nn.Linear(D, D))
|
| 274 |
self.txt_proj = nn.Sequential(nn.Linear(cfg.text_dim, D), nn.LayerNorm(D), nn.GELU(), nn.Linear(D, D))
|
| 275 |
+
self.mod_emb = nn.Embedding(2, D)
|
| 276 |
self.layers = nn.ModuleList([
|
| 277 |
PerceiverLayer(D, cfg.num_heads) for _ in range(cfg.num_cross_attn_layers)
|
| 278 |
])
|
|
|
|
| 357 |
self.step_emb = nn.Parameter(torch.randn(cfg.K + 1, 1, D) * 0.02)
|
| 358 |
else:
|
| 359 |
self.step_emb = None
|
|
|
|
| 360 |
self.ev_proj = nn.Linear(cfg.evidence_dim, D) if cfg.evidence_dim != D else nn.Identity()
|
|
|
|
| 361 |
self.predictor = nn.ModuleList([
|
| 362 |
PredictorBlock(D, cfg.predictor_heads, cfg.predictor_ffn, use_gate=cfg.use_evidence_gate)
|
| 363 |
for _ in range(cfg.num_predictor_layers)
|
|
|
|
| 378 |
if self.step_emb is not None:
|
| 379 |
z = z + self.step_emb[k]
|
| 380 |
traj.append(z)
|
| 381 |
+
traj_t = torch.stack(traj, dim=1)
|
| 382 |
B2, Kp1, N, D2 = traj_t.shape
|
| 383 |
proj = self.out_proj(traj_t.reshape(-1, D2)).reshape(B2, Kp1, N, D2)
|
| 384 |
return traj_t, traj[-1], proj
|
|
|
|
| 435 |
|
| 436 |
|
| 437 |
class VICReg(nn.Module):
|
|
|
|
| 438 |
def __init__(self, var_weight=1.0, cov_weight=0.04):
|
| 439 |
super().__init__()
|
| 440 |
self.var_w = var_weight
|
|
|
|
| 442 |
|
| 443 |
def forward(self, z):
|
| 444 |
if z.dim() == 3: z = z.reshape(-1, z.size(-1))
|
|
|
|
| 445 |
std = z.std(dim=0)
|
| 446 |
var_loss = F.relu(1.0 - std).mean()
|
|
|
|
| 447 |
z_c = z - z.mean(dim=0, keepdim=True)
|
| 448 |
N = z_c.size(0)
|
| 449 |
cov = (z_c.T @ z_c) / max(N - 1, 1)
|
|
|
|
| 461 |
self.vicreg = VICReg(cfg.vicreg_var, cfg.vicreg_cov) if cfg.use_vicreg else None
|
| 462 |
|
| 463 |
def forward(self, pred_traj, target_traj, task_loss, gen_loss=None):
|
|
|
|
| 464 |
p = pred_traj[:, 1:]
|
| 465 |
t = target_traj[:, 1:]
|
| 466 |
if self.cfg.loss_fn == "smooth_l1":
|
|
|
|
| 515 |
q = self.pool_q.expand(B, -1, -1)
|
| 516 |
z_n = self.pool_n(z_final)
|
| 517 |
pooled, _ = self.pool_attn(q, z_n, z_n)
|
| 518 |
+
pooled = pooled.squeeze(1)
|
| 519 |
+
opt = self.opt_proj(opt_emb)
|
| 520 |
z_exp = pooled.unsqueeze(1).expand_as(opt)
|
| 521 |
combined = torch.cat([z_exp, opt, z_exp * opt], dim=-1)
|
| 522 |
logits = self.scorer(combined).squeeze(-1)
|
|
|
|
| 557 |
question = row["question"]
|
| 558 |
hint = row.get("hint", "")
|
| 559 |
|
|
|
|
| 560 |
text = question
|
| 561 |
if hint:
|
| 562 |
text += f" Context: {hint}"
|
|
|
|
| 584 |
all_choices.append(s["choices"])
|
| 585 |
answers.append(s["answer"])
|
| 586 |
|
|
|
|
| 587 |
if hasattr(transform, '__call__'):
|
|
|
|
| 588 |
pixel_values = torch.stack([transform(img) for img in images])
|
| 589 |
else:
|
|
|
|
| 590 |
pixel_values = transform(images=images, return_tensors="pt")["pixel_values"]
|
| 591 |
|
|
|
|
| 592 |
tok = tokenizer(texts, padding="max_length", truncation=True, max_length=max_len, return_tensors="pt")
|
| 593 |
|
|
|
|
| 594 |
opt_texts = []
|
| 595 |
opt_mask = []
|
| 596 |
for choices in all_choices:
|
|
|
|
| 631 |
self._use_rollout = cfg.K > 0
|
| 632 |
|
| 633 |
def encode_options(self, opt_ids, opt_mask_attn):
|
|
|
|
| 634 |
with torch.no_grad():
|
| 635 |
+
h = self.txt(opt_ids, opt_mask_attn).float()
|
|
|
|
| 636 |
mask_expanded = opt_mask_attn.unsqueeze(-1).float()
|
| 637 |
pooled = (h * mask_expanded).sum(1) / mask_expanded.sum(1).clamp(min=1)
|
| 638 |
+
return pooled
|
| 639 |
|
| 640 |
def forward(self, pixel_values, input_ids, attention_mask,
|
| 641 |
opt_input_ids, opt_attention_mask, opt_mask, labels, batch_size, **kw):
|
|
|
|
| 643 |
B = batch_size
|
| 644 |
O = self.cfg.max_options
|
| 645 |
|
|
|
|
| 646 |
with torch.no_grad():
|
| 647 |
+
vis_tok = self.vis(pixel_values).float()
|
| 648 |
|
|
|
|
| 649 |
with torch.no_grad():
|
| 650 |
+
txt_tok = self.txt(input_ids, attention_mask).float()
|
| 651 |
|
| 652 |
+
evidence, _, ev_mask = self.evidence(vis_tok, txt_tok, attention_mask)
|
|
|
|
| 653 |
|
|
|
|
| 654 |
if self._use_rollout:
|
| 655 |
traj, z_final, z_proj = self.rollout(evidence)
|
| 656 |
else:
|
|
|
|
| 662 |
traj = z0.unsqueeze(1)
|
| 663 |
z_proj = self.rollout.out_proj(z0).unsqueeze(1)
|
| 664 |
|
|
|
|
| 665 |
target_proj = None
|
| 666 |
if self._use_jepa and self.training:
|
| 667 |
target_proj = self.target(vis_tok.detach(), txt_tok.detach(), attention_mask.detach())
|
| 668 |
|
| 669 |
+
opt_emb = self.encode_options(opt_input_ids, opt_attention_mask)
|
| 670 |
+
opt_emb = opt_emb.view(B, O, -1)
|
| 671 |
+
logits = self.disc(z_final, opt_emb, opt_mask)
|
|
|
|
| 672 |
task_loss = F.cross_entropy(logits, labels)
|
| 673 |
|
|
|
|
| 674 |
if self._use_jepa and self.training and target_proj is not None:
|
| 675 |
losses = self.jepa_loss(z_proj, target_proj, task_loss)
|
| 676 |
else:
|
|
|
|
| 694 |
|
| 695 |
os.makedirs(cfg.output_dir, exist_ok=True)
|
| 696 |
|
|
|
|
| 697 |
try:
|
| 698 |
import trackio
|
| 699 |
tracker = trackio.init(name=cfg.run_name, project="MR-JEPA")
|
|
|
|
| 702 |
log.warning(f"Trackio init failed: {e}")
|
| 703 |
tracker = None
|
| 704 |
|
|
|
|
| 705 |
log.info("Building model...")
|
| 706 |
model = MRJEPAModel(cfg)
|
| 707 |
|
|
|
|
| 708 |
total_p = sum(p.numel() for p in model.parameters())
|
| 709 |
train_p = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 710 |
log.info(f"Total params: {total_p:,} | Trainable: {train_p:,} ({100*train_p/total_p:.1f}%)")
|
| 711 |
|
| 712 |
model = model.to(device)
|
| 713 |
|
|
|
|
| 714 |
transform = model.vis.get_transform()
|
| 715 |
tokenizer = model.txt.tokenizer
|
| 716 |
|
|
|
|
| 727 |
eval_dl = DataLoader(eval_ds, batch_size=cfg.batch_size, shuffle=False, num_workers=cfg.num_workers,
|
| 728 |
collate_fn=coll, pin_memory=True)
|
| 729 |
|
|
|
|
| 730 |
trainable = [p for p in model.parameters() if p.requires_grad]
|
| 731 |
optimizer = AdamW(trainable, lr=cfg.lr, weight_decay=cfg.weight_decay)
|
| 732 |
total_steps = cfg.epochs * len(train_dl) // cfg.grad_accum
|
|
|
|
| 740 |
|
| 741 |
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
|
| 742 |
|
|
|
|
| 743 |
log.info(f"Starting training: {cfg.epochs} epochs, {len(train_dl)} batches/epoch, "
|
| 744 |
f"grad_accum={cfg.grad_accum}, total_steps≈{total_steps}")
|
| 745 |
|
|
|
|
| 755 |
optimizer.zero_grad()
|
| 756 |
|
| 757 |
for batch_idx, batch in enumerate(train_dl):
|
|
|
|
| 758 |
batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items()}
|
| 759 |
|
| 760 |
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=cfg.bf16 and device.type == "cuda"):
|
|
|
|
| 771 |
model.update_target(global_step, total_steps)
|
| 772 |
global_step += 1
|
| 773 |
|
|
|
|
| 774 |
for k, v in losses.items():
|
| 775 |
if isinstance(v, torch.Tensor):
|
| 776 |
epoch_losses[k].append(v.item())
|
|
|
|
| 778 |
epoch_correct += correct
|
| 779 |
epoch_total += batch["batch_size"]
|
| 780 |
|
|
|
|
| 781 |
if batch_idx % 50 == 0:
|
| 782 |
avg = {k: np.mean(v[-50:]) for k, v in epoch_losses.items()}
|
| 783 |
acc = epoch_correct / max(epoch_total, 1) * 100
|
|
|
|
| 801 |
})
|
| 802 |
except: pass
|
| 803 |
|
|
|
|
| 804 |
train_acc = epoch_correct / max(epoch_total, 1) * 100
|
| 805 |
eval_acc = evaluate(model, eval_dl, device, cfg)
|
| 806 |
log.info(f"=== Epoch {epoch} done | Train acc: {train_acc:.1f}% | Eval acc: {eval_acc:.1f}% ===")
|
|
|
|
| 819 |
|
| 820 |
log.info(f"Training complete. Best eval accuracy: {best_acc:.1f}%")
|
| 821 |
|
|
|
|
| 822 |
if cfg.push_to_hub:
|
| 823 |
push_results(cfg, best_acc)
|
| 824 |
|
|
|
|
| 859 |
|
| 860 |
|
| 861 |
def push_results(cfg, best_acc):
|
|
|
|
| 862 |
try:
|
| 863 |
from huggingface_hub import HfApi
|
| 864 |
api = HfApi()
|
|
|
|
| 889 |
repo_id=cfg.hub_model_id,
|
| 890 |
repo_type="model",
|
| 891 |
)
|
|
|
|
| 892 |
best_ckpt = os.path.join(cfg.output_dir, "checkpoint_best.pt")
|
| 893 |
if os.path.exists(best_ckpt):
|
| 894 |
api.upload_file(
|
|
|
|
| 918 |
p.add_argument("--no_jepa", action="store_true")
|
| 919 |
p.add_argument("--no_rollout", action="store_true")
|
| 920 |
p.add_argument("--no_evidence_gate", action="store_true")
|
| 921 |
+
p.add_argument("--no_sigreg", action="store_true")
|
| 922 |
+
p.add_argument("--use_vicreg", action="store_true")
|
| 923 |
p.add_argument("--purist", action="store_true")
|
| 924 |
p.add_argument("--sigreg_weight", type=float, default=0.1)
|
| 925 |
p.add_argument("--max_train_samples", type=int, default=0)
|
|
|
|
| 954 |
|
| 955 |
if args.no_rollout:
|
| 956 |
cfg.K = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
cfg.use_jepa = False
|
| 958 |
|
| 959 |
if args.purist:
|