Files
Mini-Nav/mini-nav/compressors/train.py
SikongJueluo 4ea567adba feat(compressors): add JSONL training metrics logging with CLI controls
- Add write_training_metrics() in new compressors/training_metrics.py
  for appending epoch/step/lr/component rows as JSON Lines
- Wire --metrics-path and --log-every CLI options into train.py, passing
  them to the training loop so metrics rows are written every N steps
- Accept absolute metrics paths or paths relative to output directory
- Add quantization component to loss log alongside existing distill/contrastive
- Replace inline torch.device() with get_device() utility
- Add test_hash_training_metrics.py covering multi-row JSONL append

Infrastructure:
- Pin torch 2.7.1 + CUDA 12.8 index for Linux/Windows in pyproject.toml
- Add .justfile rsync upload recipe with .stignore exclusion
- Exclude **/__marimo__ from rsync in .stignore

Dependencies updated: numpy 2.4.5, pandas 3.0.3, black 26.5.0,
click 8.4.0, contourpy, etc.
2026-05-17 14:57:10 +08:00

200 lines
6.7 KiB
Python

"""Training script for hash compressor."""
import os
import torch
import torch.nn.functional as F
from compressors import HashCompressor, HashLoss
from compressors.training_metrics import write_training_metrics
from configs import cfg_manager
from datasets import load_dataset
from rich.progress import BarColumn, Progress, TextColumn, TimeRemainingColumn
from torch import nn
from torch.utils.data import DataLoader
from transformers import AutoImageProcessor, AutoModel
from utils import get_device
def save_checkpoint(model: nn.Module, optimizer, epoch, step, path="checkpoint.pt"):
config = cfg_manager.get()
path = config.output.directory / path
ckpt = {
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"epoch": epoch,
"step": step,
}
torch.save(ckpt, path)
print(f"✅ Saved checkpoint to {path}")
def load_checkpoint(model: nn.Module, optimizer, path="checkpoint.pt"):
ckpt = torch.load(path, map_location="cpu")
model.load_state_dict(ckpt["model"])
optimizer.load_state_dict(ckpt["optimizer"])
start_epoch = ckpt["epoch"]
start_step = ckpt["step"]
print(f"✅ Loaded checkpoint from {path}")
print(f"➡️ Resume from epoch={start_epoch}, step={start_step}")
return start_epoch, start_step
def train(
epoch_size: int = 10,
batch_size: int = 64,
lr: float = 1e-4,
checkpoint_path: str = "hash_checkpoint.pt",
metrics_path: str = "hash_training_metrics.jsonl",
log_every: int = 1,
):
"""Train hash compressor with batch-level retrieval loss.
Args:
epoch_size: Number of epochs to train
batch_size: Batch size for training
lr: Learning rate
checkpoint_path: Path to save/load checkpoints
metrics_path: JSONL metrics path, relative to output directory unless absolute
log_every: Write metrics every N global steps; values <= 0 disable metrics logging
"""
# Auto detect device
device = get_device()
# Global variables
save_every = 500
start_epoch = 0
global_step = 0
# Load dataset
ds_train = load_dataset("uoft-cs/cifar10", split="train").with_format("torch")
dataloader = DataLoader(
ds_train, batch_size=batch_size, shuffle=True, num_workers=4
)
# Load processor
processor = AutoImageProcessor.from_pretrained(
"facebook/dinov2-large", device_map=device
)
# Load DINO model (frozen)
dino = AutoModel.from_pretrained("facebook/dinov2-large", device_map=device)
dino.eval()
for p in dino.parameters():
p.requires_grad = False
# Load hash compressor
compressor = HashCompressor(input_dim=1024, hash_bits=512).to(device)
# Load loss function
loss_fn = HashLoss(
contrastive_weight=1.0,
distill_weight=0.5,
quant_weight=0.01,
temperature=0.2,
)
# Load optimizer
optimizer = torch.optim.AdamW(compressor.parameters(), lr=lr)
# Auto load checkpoint
output_dir = cfg_manager.get().output.directory
metrics_file = output_dir / metrics_path
if os.path.isabs(metrics_path):
metrics_file = metrics_path
if os.path.exists(output_dir / checkpoint_path):
start_epoch, global_step = load_checkpoint(
compressor, optimizer, output_dir / checkpoint_path
)
try:
progress = Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeRemainingColumn(),
)
with progress:
for epoch in range(start_epoch, epoch_size):
task_id = progress.add_task(
f"Epoch [{epoch + 1}/{epoch_size}]", total=len(dataloader)
)
for batch in dataloader:
progress.update(task_id, advance=1)
global_step += 1
# ---- training step ----
imgs = batch["img"]
# ---- teacher forward ----
with torch.no_grad():
inputs = processor(imgs, return_tensors="pt").to(device)
teacher_tokens = dino(**inputs).last_hidden_state # [B,N,1024]
teacher_embed = teacher_tokens.mean(dim=1)
teacher_embed = F.normalize(teacher_embed, dim=-1) # [B,1024]
# ---- student forward ----
logits, hash_codes, bits = compressor(teacher_tokens)
# ---- generate positive mask ----
labels = batch["label"]
# positive_mask[i,j] = True if labels[i] == labels[j]
positive_mask = labels.unsqueeze(0) == labels.unsqueeze(1) # [B, B]
# ---- loss ----
total_loss, components = loss_fn(
logits=logits,
hash_codes=hash_codes,
teacher_embed=teacher_embed,
positive_mask=positive_mask,
)
# ---- backward ----
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
# ---- logging ----
progress.update(
task_id,
description=f"Epoch [{epoch + 1}/{epoch_size}] "
f"loss={components['total']:.4f} "
f"cont={components['contrastive']:.2f} "
f"distill={components['distill']:.3f} "
f"quant={components['quantization']:.3f}",
)
if log_every > 0 and global_step % log_every == 0:
write_training_metrics(
metrics_file,
epoch=epoch + 1,
step=global_step,
lr=optimizer.param_groups[0]["lr"],
components=components,
)
# ---- periodic save ----
if global_step % save_every == 0:
save_checkpoint(
compressor, optimizer, epoch, global_step, checkpoint_path
)
except KeyboardInterrupt:
print("\n⚠️ Training interrupted, saving checkpoint...")
save_checkpoint(compressor, optimizer, epoch, global_step, checkpoint_path)
print("✅ Checkpoint saved. Exiting.")
return
# Save final model
torch.save(compressor.state_dict(), output_dir / "hash_compressor.pt")
print("✅ Final hash compressor saved")