"""Train compact RemoteCLIP with bidirectional InfoNCE.""" import importlib.util import json import os import random from pathlib import Path import numpy as np import torch import yaml from torch import distributed as dist from torch.nn.parallel import DistributedDataParallel from torch.utils.data import DataLoader, Dataset, DistributedSampler ROOT = Path(__file__).resolve().parents[1] def load_model_class(): spec = importlib.util.spec_from_file_location("remoteclip_model", ROOT / "model" / "remoteclip.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.RemoteCLIP class PairDataset(Dataset): def __init__(self, path): archive = np.load(path) self.images = archive["train_images"] self.tokens = archive["train_tokens"] self.data_source = str(archive["data_source"]) if "data_source" in archive.files else "unknown" self.protocol = str(archive["protocol"]) if "protocol" in archive.files else "unknown" def __len__(self): return len(self.images) def __getitem__(self, index): return torch.from_numpy(self.images[index]), torch.from_numpy(self.tokens[index]) def main(): with (ROOT / "conf" / "config.yaml").open(encoding="utf-8") as handle: config = yaml.safe_load(handle) data_path = ROOT / config["data"]["path"] if not data_path.exists(): raise FileNotFoundError( f"Missing training data: {data_path.relative_to(ROOT)}. " "Run `python scripts/fake_data.py` first." ) world_size = int(os.environ.get("WORLD_SIZE", "1")) local_rank = int(os.environ.get("LOCAL_RANK", "0")) if world_size > 1: dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo") device = torch.device(f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu") if device.type == "cuda": torch.cuda.set_device(local_rank) seed = config["seed"] + local_rank random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) dataset = PairDataset(data_path) sampler = DistributedSampler(dataset, shuffle=True) if world_size > 1 else None loader = DataLoader( dataset, batch_size=config["train"]["batch_size"], shuffle=sampler is None, sampler=sampler, num_workers=config["train"]["num_workers"], ) RemoteCLIP = load_model_class() model = RemoteCLIP( vocabulary_size=config["data"]["vocabulary_size"], context_length=config["data"]["context_length"], **config["model"], ).to(device) if world_size > 1: model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None) optimizer = torch.optim.AdamW( model.parameters(), lr=config["train"]["learning_rate"], weight_decay=config["train"]["weight_decay"], ) history = [] for epoch in range(config["train"]["epochs"]): if sampler is not None: sampler.set_epoch(epoch) model.train() total = 0.0 for images, tokens in loader: output = model(images.to(device), tokens.to(device)) optimizer.zero_grad(set_to_none=True) output["loss"].backward() optimizer.step() total += output["loss"].item() loss = total / len(loader) history.append({"epoch": epoch + 1, "contrastive_loss": loss}) if local_rank == 0: print(f"epoch={epoch + 1} contrastive_loss={loss:.6f}") if local_rank == 0: checkpoint = ROOT / config["paths"]["checkpoint"] metrics = ROOT / config["paths"]["training_metrics"] checkpoint.parent.mkdir(parents=True, exist_ok=True) metrics.parent.mkdir(parents=True, exist_ok=True) base_model = model.module if hasattr(model, "module") else model torch.save( { "model": base_model.state_dict(), "config": config, "data_source": dataset.data_source, "protocol": dataset.protocol, }, checkpoint, ) metrics.write_text( json.dumps( {"history": history, "data_source": dataset.data_source, "protocol": dataset.protocol}, indent=2, ) + "\n", encoding="utf-8", ) print( f"checkpoint={checkpoint.relative_to(ROOT)} data_source={dataset.data_source} " f"protocol={dataset.protocol}" ) if world_size > 1: dist.destroy_process_group() if __name__ == "__main__": main()