Download scripts/train_b1_tc.py from dejanseo/fanout-diffusion: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/dejanseo/fanout-diffusion/resolve/main/scripts/train_b1_tc.py
- Command line
-
hf download hf://dejanseo/fanout-diffusion/scripts/train_b1_tc.py
-
curl -L -o train_b1_tc.py https://huggingface.co/dejanseo/fanout-diffusion/resolve/main/scripts/train_b1_tc.py
14.9 kB
| import argparse | |
| import math | |
| import sys | |
| import time | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| if str(ROOT) not in sys.path: | |
| sys.path.insert(0, str(ROOT)) | |
| import torch | |
| import torch.nn.functional as F | |
| from sentence_transformers import SentenceTransformer | |
| from torch.utils.data import DataLoader, TensorDataset | |
| from b1_tensor_core import B1Linear | |
| from src.r4t.b1_diffusion import B1EDMDenoiser | |
| from src.r4t.config import DiffusionConfig | |
| from src.r4t.diffusion import sample_edm | |
| from src.r4t.journal import ExperimentJournal | |
| from scripts.train_diffusion import ExponentialMovingAverage, diffusion_loss | |
| DATA_PATH = ROOT / "data" / "diffusion_dataset_540k.pt" | |
| TAXONOMY_PATH = ROOT / "data" / "taxonomy_embeddings.pt" | |
| CHECKPOINT_DIR = ROOT / "checkpoints" | |
| CHECKPOINT_DIR.mkdir(exist_ok=True) | |
| TEST_QUERIES = [ | |
| "running shoes", | |
| "wireless noise cancelling headphones", | |
| "organic gardening tools", | |
| ] | |
| def load_taxonomy_bank(device): | |
| if not TAXONOMY_PATH.exists(): | |
| return None, None | |
| tax_data = torch.load(TAXONOMY_PATH, map_location="cpu", weights_only=False) | |
| emb = F.normalize(tax_data["embeddings"].float(), dim=-1).to(device) | |
| names = tax_data["names"] | |
| return emb, names | |
| def evaluate_qualitative(model, embedder, tax_emb, tax_names, device, tracker, run_name, sampling_steps=8): | |
| model.eval() | |
| all_alignments = [] | |
| all_diversities = [] | |
| for q_idx, q_text in enumerate(TEST_QUERIES): | |
| with torch.no_grad(): | |
| z_q = embedder.encode([q_text], convert_to_tensor=True, normalize_embeddings=True, device=device).float() | |
| fanout = sample_edm(model, z_q, sampling_steps=sampling_steps, cfg_strength=0.1)[0] | |
| fanout = F.normalize(fanout, dim=-1) | |
| align_scores = torch.mm(fanout, z_q.T).squeeze(-1) | |
| mean_align = align_scores.mean().item() | |
| all_alignments.append(mean_align) | |
| sim_mat = torch.mm(fanout, fanout.T) | |
| mask = ~torch.eye(10, dtype=torch.bool, device=device) | |
| pairwise_sim = sim_mat[mask].mean().item() | |
| pairwise_div = 1.0 - pairwise_sim | |
| all_diversities.append(pairwise_div) | |
| decoded_subqueries = [] | |
| if tax_emb is not None: | |
| top_matches = torch.mm(fanout, tax_emb.T) | |
| best_indices = top_matches.argmax(dim=-1).tolist() | |
| for slot_idx, tax_idx in enumerate(best_indices): | |
| cat_name = tax_names[tax_idx] | |
| slot_sim = top_matches[slot_idx, tax_idx].item() | |
| decoded_subqueries.append(f"{cat_name} (sim: {slot_sim:.3f})") | |
| else: | |
| decoded_subqueries = [f"Vector #{i+1} [768-d]" for i in range(10)] | |
| tracker.log_sample( | |
| step=100 + q_idx, | |
| sample_type="fanout_query_expansion", | |
| input_payload={"query": q_text, "cfg_strength": 0.1, "sampling_steps": sampling_steps}, | |
| output_payload={ | |
| "subqueries": decoded_subqueries, | |
| "mean_prompt_align": mean_align, | |
| "pairwise_diversity": pairwise_div, | |
| }, | |
| target_payload=None, | |
| metadata={"run": run_name, "test_query_index": q_idx + 1, "sampling_steps": sampling_steps, "backend": "b1_tc"}, | |
| ) | |
| overall_align = float(sum(all_alignments) / max(1, len(all_alignments))) | |
| overall_div = float(sum(all_diversities) / max(1, len(all_diversities))) | |
| return overall_align, overall_div | |
| def benchmark_latency(model, device, sampling_steps=8): | |
| model.eval() | |
| dummy_q = torch.randn(1, 768, device=device) | |
| dummy_q = F.normalize(dummy_q, dim=-1) | |
| # Warmup | |
| with torch.no_grad(): | |
| sample_edm(model, dummy_q, sampling_steps=sampling_steps, cfg_strength=0.1) | |
| torch.cuda.synchronize() | |
| times = [] | |
| for _ in range(10): | |
| torch.cuda.synchronize() | |
| t0 = time.perf_counter() | |
| with torch.no_grad(): | |
| sample_edm(model, dummy_q, sampling_steps=sampling_steps, cfg_strength=0.1) | |
| torch.cuda.synchronize() | |
| times.append((time.perf_counter() - t0) * 1000.0) | |
| return float(sum(times) / len(times)) | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Train 1-Bit Tensor Core Diffusion Denoiser") | |
| parser.add_argument("--epochs", type=int, default=10, help="Number of training epochs") | |
| parser.add_argument("--lr", type=float, default=1.5e-4, help="Learning rate") | |
| parser.add_argument("--batch_size", type=int, default=128, help="Batch size") | |
| parser.add_argument("--run_name", type=str, default="Sweep B1-TC: 1-Bit Tensor Core (2L, 10ep)", help="Journal run name") | |
| parser.add_argument("--checkpoint_name", type=str, default="b1_tc_10ep_champion.pt", help="Checkpoint filename") | |
| parser.add_argument("--pure_1bit", action="store_true", help="Binarize all outer projections and time MLP to 1-bit") | |
| args = parser.parse_args() | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"Compute device: {device} ({torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'})") | |
| print(f"Loading dataset from {DATA_PATH}...") | |
| dataset_dict = torch.load(DATA_PATH, map_location="cpu", weights_only=False) | |
| queries = dataset_dict["query_embeddings"].float() | |
| targets = dataset_dict["targets"].float() | |
| sigma_data = float(dataset_dict.get("sigma_data", 0.0361)) | |
| N, L, D = targets.shape | |
| print(f"Loaded {N:,} samples: L={L}, D={D}, sigma_data={sigma_data:.4f}") | |
| # 90/10 split | |
| n_train = int(0.9 * N) | |
| train_queries, val_queries = queries[:n_train], queries[n_train:] | |
| train_targets, val_targets = targets[:n_train], targets[n_train:] | |
| train_dataset = TensorDataset(train_queries, train_targets) | |
| val_dataset = TensorDataset(val_queries, val_targets) | |
| batch_size = args.batch_size | |
| epochs = args.epochs | |
| lr = args.lr | |
| train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, pin_memory=True) | |
| val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, pin_memory=True) | |
| config = DiffusionConfig( | |
| sequence_length=L, | |
| embedding_dim=D, | |
| hidden_dim=512, | |
| mlp_dim=1024, | |
| heads=8, | |
| layers=2, | |
| dropout=0.1, | |
| sigma_min=0.0001, | |
| sigma_max=80.0, | |
| sigma_data=sigma_data, | |
| sampling_steps=8, | |
| ) | |
| mode_str = "Pure 1-Bit (End-to-End)" if args.pure_1bit else "Hybrid 1-Bit Core" | |
| print(f"\nInitializing B1EDMDenoiser with Hardware 1-Bit Tensor Cores ({mode_str}, backend='tc')...") | |
| model = B1EDMDenoiser(config, backend="tc", pure_1bit=args.pure_1bit).to(device) | |
| ema = ExponentialMovingAverage(model, decay=0.999) | |
| param_count = sum(p.numel() for p in model.parameters()) | |
| print(f"Initialized B1EDMDenoiser ({param_count / 1e6:.2f}M parameters).") | |
| run_name = args.run_name | |
| journal = ExperimentJournal() | |
| tracker = journal.start_run( | |
| name=run_name, | |
| experiment_name="1-Bit Tensor Core Innovation", | |
| task_type="diffusion", | |
| config={ | |
| "layers": 2, | |
| "hidden_dim": 512, | |
| "mlp_dim": 1024, | |
| "heads": 8, | |
| "lr": lr, | |
| "epochs": epochs, | |
| "batch_size": batch_size, | |
| "target_ordering": "cosine", | |
| "backend": "b1_tc", | |
| "precision": "1-bit_tc_ste", | |
| "dataset": "diffusion_dataset_540k.pt", | |
| }, | |
| tags=["1bit", "tensor_core", "ptx_mma", "2layer", "ste"], | |
| ) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) | |
| total_steps = len(train_loader) * epochs | |
| warmup_steps = len(train_loader) * 1 | |
| warmup_sched = torch.optim.lr_scheduler.LinearLR(optimizer, start_factor=0.05, total_iters=warmup_steps) | |
| cosine_sched = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=max(1, total_steps - warmup_steps), eta_min=lr * 0.05) | |
| scheduler = torch.optim.lr_scheduler.SequentialLR(optimizer, schedulers=[warmup_sched, cosine_sched], milestones=[warmup_steps]) | |
| best_val_loss = float("inf") | |
| best_checkpoint_path = CHECKPOINT_DIR / args.checkpoint_name | |
| use_amp = torch.cuda.is_available() | |
| amp_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 | |
| print("\nStarting 1-Bit Tensor Core STE Diffusion Training:") | |
| print(f" Epochs: {epochs} | Batch size: {batch_size} | Batches/epoch: {len(train_loader):,}") | |
| print(f" Peak LR: {lr:.2e} | Target checkpoint: {best_checkpoint_path}") | |
| print("=" * 75) | |
| t0 = time.time() | |
| for epoch in range(1, epochs + 1): | |
| ep_t0 = time.time() | |
| model.train() | |
| train_loss_total = 0.0 | |
| for b_queries, b_targets in train_loader: | |
| b_queries = b_queries.to(device, non_blocking=True) | |
| b_targets = b_targets.to(device, non_blocking=True) | |
| # Cosine sorting targets | |
| sims = torch.einsum("bd,bld->bl", F.normalize(b_queries, dim=-1), F.normalize(b_targets, dim=-1)) | |
| sorted_idx = torch.argsort(sims, dim=1, descending=True) | |
| b_targets = torch.gather(b_targets, 1, sorted_idx.unsqueeze(-1).expand(-1, -1, D)) | |
| optimizer.zero_grad() | |
| with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=use_amp): | |
| loss = diffusion_loss(model, b_targets, b_queries) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) | |
| optimizer.step() | |
| # BNN STE Best Practice: clamp latent weights to [-1.0, 1.0] | |
| with torch.no_grad(): | |
| for m in model.modules(): | |
| if isinstance(m, B1Linear): | |
| m.weight.clamp_(-1.0, 1.0) | |
| scheduler.step() | |
| ema.update(model) | |
| train_loss_total += loss.item() * len(b_queries) | |
| train_loss = train_loss_total / len(train_dataset) | |
| # Validation | |
| model.eval() | |
| val_loss_total = 0.0 | |
| val_gen = torch.Generator(device=device).manual_seed(1337) | |
| with torch.no_grad(): | |
| for b_queries, b_targets in val_loader: | |
| b_queries = b_queries.to(device, non_blocking=True) | |
| b_targets = b_targets.to(device, non_blocking=True) | |
| sims = torch.einsum("bd,bld->bl", F.normalize(b_queries, dim=-1), F.normalize(b_targets, dim=-1)) | |
| sorted_idx = torch.argsort(sims, dim=1, descending=True) | |
| b_targets = torch.gather(b_targets, 1, sorted_idx.unsqueeze(-1).expand(-1, -1, D)) | |
| with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=use_amp): | |
| v_loss = diffusion_loss(model, b_targets, b_queries, generator=val_gen) | |
| val_loss_total += v_loss.item() * len(b_queries) | |
| val_loss = val_loss_total / len(val_dataset) | |
| ep_time = time.time() - ep_t0 | |
| elapsed = time.time() - t0 | |
| is_best = val_loss < best_val_loss | |
| best_marker = " [BEST SAVED]" if is_best else "" | |
| print(f"Epoch [{epoch:2d}/{epochs:2d}] | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}{best_marker} | LR: {scheduler.get_last_lr()[0]:.2e} | Ep Time: {ep_time:.1f}s | Elapsed: {elapsed:.1f}s") | |
| if is_best: | |
| best_val_loss = val_loss | |
| torch.save({ | |
| "epoch": epoch, | |
| "config": config, | |
| "model_state_dict": model.state_dict(), | |
| "ema_state_dict": ema.state_dict(), | |
| "val_loss": val_loss, | |
| }, best_checkpoint_path) | |
| tracker.log_metrics( | |
| step=epoch * len(train_loader), | |
| epoch=epoch, | |
| train_loss=train_loss, | |
| val_loss=val_loss, | |
| lr=scheduler.get_last_lr()[0], | |
| is_best=is_best, | |
| ) | |
| print(f"\nTraining Complete! Best Val Loss: {best_val_loss:.4f}") | |
| # Load best EMA weights for evaluation | |
| ckpt = torch.load(best_checkpoint_path, map_location=device, weights_only=False) | |
| if "ema_state_dict" in ckpt and "shadow" in ckpt["ema_state_dict"]: | |
| shadow = ckpt["ema_state_dict"]["shadow"] | |
| model.load_state_dict({k: shadow[k].to(device) for k in shadow}) | |
| else: | |
| model.load_state_dict(ckpt["model_state_dict"]) | |
| model.eval() | |
| # Freeze weights for zero-overhead inference | |
| print("Freezing B1Linear weights for zero-overhead Tensor Core inference...") | |
| model.freeze_for_inference() | |
| if args.pure_1bit: | |
| export_dict = {"config": config, "weights": {}} | |
| for k, v in model.state_dict().items(): | |
| if "packed_weight" in k: | |
| export_dict["weights"][k] = v.cpu() | |
| elif "bias" in k or "norm" in k or "position" in k: | |
| export_dict["weights"][k] = v.to(torch.float16).cpu() | |
| out_sub1mb = CHECKPOINT_DIR / "champion_pure_1bit_sub1mb.pt" | |
| torch.save(export_dict, out_sub1mb) | |
| print(f"Exported Pure 1-Bit sub-1MB checkpoint: {out_sub1mb} ({out_sub1mb.stat().st_size / 1024:.1f} KB / {out_sub1mb.stat().st_size / (1024*1024):.2f} MB)") | |
| # Benchmarking latency | |
| lat_8 = benchmark_latency(model, device, sampling_steps=8) | |
| lat_16 = benchmark_latency(model, device, sampling_steps=16) | |
| print(f"Latency benchmarks: 8-Step Heun = {lat_8:.2f} ms | 16-Step Heun = {lat_16:.2f} ms") | |
| # Qualitative decoding | |
| tax_emb, tax_names = load_taxonomy_bank(device) | |
| print("Loading Google EmbeddingGemma embedder...") | |
| embedder = SentenceTransformer("google/embeddinggemma-300m", model_kwargs={"torch_dtype": torch.bfloat16}, device=device) | |
| align, div = evaluate_qualitative( | |
| model=model, | |
| embedder=embedder, | |
| tax_emb=tax_emb, | |
| tax_names=tax_names, | |
| device=device, | |
| tracker=tracker, | |
| run_name=run_name, | |
| sampling_steps=8, | |
| ) | |
| tracker.log_benchmark( | |
| latency_us=int(lat_8 * 1000), | |
| throughput_items_per_sec=1000.0 / lat_8 if lat_8 > 0 else 0.0, | |
| device_name=torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU", | |
| notes=f"1-Bit Tensor Core Denoiser (backend='tc', 8-step Heun); 16-step latency: {lat_16:.2f}ms", | |
| ) | |
| tracker.finish( | |
| status="completed", | |
| summary_metrics={ | |
| "best_val_loss": best_val_loss, | |
| "prompt_alignment": align, | |
| "pairwise_diversity": div, | |
| "sampling_latency_ms": lat_8, | |
| "sampling_latency_16step_ms": lat_16, | |
| "backend": "tc_1bit", | |
| "layers": 2, | |
| }, | |
| ) | |
| print(f"\nRun Complete: Val Loss={best_val_loss:.4f}, Align={align:.3f}, Div={div:.3f}, Latency(8-step)={lat_8:.2f}ms") | |
| if __name__ == "__main__": | |
| main() | |