Download scripts/benchmark_comparison.py from brianschwabauer/latent-space-language-diffusion-model: direct link, hf CLI and curl.
- Browser
- Download file 20.9 kB
-
https://huggingface.co/brianschwabauer/latent-space-language-diffusion-model/resolve/main/scripts/benchmark_comparison.py
- Command line
-
hf download hf://brianschwabauer/latent-space-language-diffusion-model/scripts/benchmark_comparison.py
-
curl -L -o benchmark_comparison.py https://huggingface.co/brianschwabauer/latent-space-language-diffusion-model/resolve/main/scripts/benchmark_comparison.py
20.9 kB
| """ | |
| Head-to-head benchmark: MDLM v3 vs AR Control Model. | |
| Compares: | |
| 1. Perplexity (held-out, same data) | |
| 2. Forward-pass throughput (TPS at batch 1/8/32) | |
| 3. Generation speed (tokens/sec, latency) | |
| 4. Text quality (oracle log-prob via Qwen3, repetition score) | |
| 5. Sample outputs (side-by-side) | |
| Both models use the same tokenizer, same data, same parameter budget (~201M). | |
| Only architecture differs: masked diffusion (bidirectional) vs autoregressive (causal). | |
| """ | |
| import json | |
| import sys | |
| import time | |
| import math | |
| import argparse | |
| import numpy as np | |
| from pathlib import Path | |
| import torch | |
| import torch.nn.functional as F | |
| REPO = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(REPO / "src")) | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| CHECKPOINT_DIR = REPO / "checkpoints" | |
| RESULTS_DIR = REPO / "results" | |
| DATA_DIR = REPO / "data" | |
| from mdlm_bpe_v3 import ( | |
| MDLMConfig, MDLMBPEV3, BPETokenizer as MDLMTokenizer, | |
| forward_mask_bpe, mdlm_loss, sample_semi_ar, | |
| ) | |
| from ar_control import ( | |
| ARConfig, ARControlModel, BPETokenizer as ARTokenizer, | |
| ar_loss, sample_ar, measure_perplexity as ar_measure_ppl, | |
| ) | |
| def load_mdlm(): | |
| """Load trained MDLM v3.""" | |
| tok = MDLMTokenizer() | |
| config = MDLMConfig( | |
| vocab_size=tok.vocab_size, | |
| d_model=1024, n_heads=16, n_layers=10, max_seq_len=256, | |
| ) | |
| model = MDLMBPEV3(config, pad_id=tok.pad_id, mask_id=tok.mask_id).to(DEVICE) | |
| ckpt = torch.load(CHECKPOINT_DIR / "mdlm_bpe_v3_best.pt", map_location=DEVICE, weights_only=False) | |
| model.load_state_dict(ckpt["model_state"]) | |
| model.eval() | |
| return model, tok, ckpt | |
| def load_ar(): | |
| """Load trained AR Control.""" | |
| tok = ARTokenizer() | |
| config = ARConfig( | |
| vocab_size=tok.vocab_size, | |
| d_model=1024, n_heads=16, n_layers=15, max_seq_len=256, | |
| ) | |
| model = ARControlModel(config, pad_id=tok.pad_id).to(DEVICE) | |
| ckpt = torch.load(CHECKPOINT_DIR / "ar_control_best.pt", map_location=DEVICE, weights_only=False) | |
| model.load_state_dict(ckpt["model_state"]) | |
| model.eval() | |
| return model, tok, ckpt | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # 1. Perplexity | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def eval_mdlm_perplexity(model, tokens, tokenizer, batch_size=32): | |
| """MDLM perplexity via masked CE loss. | |
| MDLM's loss is computed only on masked positions. For fair comparison, | |
| we use the same eval procedure as the original MDLM training: sample | |
| random timesteps, mask tokens, compute CE on masked positions only. | |
| """ | |
| model.eval() | |
| total_loss = 0.0 | |
| total_masked = 0 | |
| mask_id = tokenizer.mask_id | |
| n = tokens.shape[0] | |
| with torch.no_grad(): | |
| for i in range(0, n, batch_size): | |
| batch = tokens[i:i+batch_size].to(DEVICE) | |
| bsz = batch.shape[0] | |
| t = torch.rand(bsz, device=DEVICE) | |
| masked, mask_pos = forward_mask_bpe(batch, t, mask_id) | |
| with torch.amp.autocast('cuda', dtype=torch.bfloat16): | |
| logits = model(masked, t) | |
| mask_flat = mask_pos.reshape(-1) | |
| if mask_flat.sum() == 0: | |
| continue | |
| logits_flat = logits.reshape(-1, logits.shape[-1]) | |
| tokens_flat = batch.reshape(-1) | |
| loss = F.cross_entropy( | |
| logits_flat[mask_flat], tokens_flat[mask_flat], | |
| reduction='sum', | |
| ) | |
| total_loss += loss.item() | |
| total_masked += mask_flat.sum().item() | |
| avg_loss = total_loss / max(total_masked, 1) | |
| ppl = math.exp(min(avg_loss, 15)) | |
| return avg_loss, ppl | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # 2. Forward-pass throughput | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def benchmark_throughput_mdlm(model, vocab_size, seq_len=128, batches=[1, 8, 32]): | |
| """Measure MDLM forward-pass TPS.""" | |
| model.eval() | |
| results = {} | |
| mask_id = 1 | |
| with torch.no_grad(): | |
| for bs in batches: | |
| tokens = torch.randint(0, vocab_size, (bs, seq_len), device=DEVICE) | |
| t = torch.rand(bs, device=DEVICE) | |
| # Warmup | |
| for _ in range(3): | |
| with torch.amp.autocast('cuda', dtype=torch.bfloat16): | |
| _ = model(tokens, t) | |
| torch.cuda.synchronize() | |
| n_iters = 20 | |
| start = time.time() | |
| for _ in range(n_iters): | |
| with torch.amp.autocast('cuda', dtype=torch.bfloat16): | |
| _ = model(tokens, t) | |
| torch.cuda.synchronize() | |
| elapsed = time.time() - start | |
| tps = bs * seq_len * n_iters / elapsed | |
| latency_ms = elapsed / n_iters * 1000 | |
| results[f"batch_{bs}"] = {"tps": tps, "latency_ms": latency_ms, "steps": n_iters} | |
| return results | |
| def benchmark_throughput_ar(model, vocab_size, seq_len=128, batches=[1, 8, 32]): | |
| """Measure AR forward-pass TPS (full sequence, teacher forcing).""" | |
| model.eval() | |
| results = {} | |
| with torch.no_grad(): | |
| for bs in batches: | |
| tokens = torch.randint(0, vocab_size, (bs, seq_len), device=DEVICE) | |
| inp = tokens[:, :-1] | |
| # Warmup | |
| for _ in range(3): | |
| with torch.amp.autocast('cuda', dtype=torch.bfloat16): | |
| _ = model(inp) | |
| torch.cuda.synchronize() | |
| n_iters = 20 | |
| start = time.time() | |
| for _ in range(n_iters): | |
| with torch.amp.autocast('cuda', dtype=torch.bfloat16): | |
| _ = model(inp) | |
| torch.cuda.synchronize() | |
| elapsed = time.time() - start | |
| tps = bs * (seq_len - 1) * n_iters / elapsed | |
| latency_ms = elapsed / n_iters * 1000 | |
| results[f"batch_{bs}"] = {"tps": tps, "latency_ms": latency_ms, "steps": n_iters} | |
| return results | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # 3. Generation speed | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def benchmark_generation_mdlm(model, tokenizer, n_samples=5, seq_len=128): | |
| """Measure MDLM generation speed (full-parallel + semi-AR).""" | |
| model.eval() | |
| results = {"full_parallel": [], "semi_ar": []} | |
| prompts = [ | |
| "Climate change is one of the biggest challenges", | |
| "The future of artificial intelligence depends on", | |
| "Education systems around the world need to", | |
| " Renewable energy sources such as solar and wind", | |
| "The economic impact of the pandemic has", | |
| ] | |
| for i in range(n_samples): | |
| prompt = prompts[i % len(prompts)] | |
| prompt_ids = tokenizer.encode(prompt, add_special=False) | |
| # Full-parallel (block_size = seq_len) | |
| torch.cuda.synchronize() | |
| start = time.time() | |
| text = sample_semi_ar(model, tokenizer, prompt_ids=prompt_ids, | |
| seq_len=seq_len, n_samples=1, | |
| block_size=seq_len, temperature=0.7) | |
| torch.cuda.synchronize() | |
| elapsed = time.time() - start | |
| n_tokens = len(tokenizer.encode(text[0], add_special=False)) | |
| results["full_parallel"].append({ | |
| "text": text[0][:200], | |
| "tokens": n_tokens, | |
| "time_s": elapsed, | |
| "tps": n_tokens / max(elapsed, 0.001), | |
| }) | |
| # Semi-AR (block_size = 4) | |
| torch.cuda.synchronize() | |
| start = time.time() | |
| text = sample_semi_ar(model, tokenizer, prompt_ids=prompt_ids, | |
| seq_len=seq_len, n_samples=1, | |
| block_size=4, temperature=0.7) | |
| torch.cuda.synchronize() | |
| elapsed = time.time() - start | |
| n_tokens = len(tokenizer.encode(text[0], add_special=False)) | |
| results["semi_ar"].append({ | |
| "text": text[0][:200], | |
| "tokens": n_tokens, | |
| "time_s": elapsed, | |
| "tps": n_tokens / max(elapsed, 0.001), | |
| }) | |
| return results | |
| def benchmark_generation_ar(model, tokenizer, n_samples=5, max_new_tokens=64): | |
| """Measure AR generation speed (one token at a time with KV cache).""" | |
| model.eval() | |
| results = [] | |
| prompts = [ | |
| "Climate change is one of the biggest challenges", | |
| "The future of artificial intelligence depends on", | |
| "Education systems around the world need to", | |
| " Renewable energy sources such as solar and wind", | |
| "The economic impact of the pandemic has", | |
| ] | |
| for i in range(n_samples): | |
| prompt = prompts[i % len(prompts)] | |
| prompt_ids = tokenizer.encode(prompt, add_special=False) | |
| torch.cuda.synchronize() | |
| start = time.time() | |
| text = sample_ar(model, tokenizer, prompt_ids=prompt_ids, | |
| max_new_tokens=max_new_tokens, | |
| temperature=0.7, top_p=0.95) | |
| torch.cuda.synchronize() | |
| elapsed = time.time() - start | |
| n_tokens = len(tokenizer.encode(text, add_special=False)) | |
| results.append({ | |
| "text": text[:200], | |
| "tokens": n_tokens, | |
| "time_s": elapsed, | |
| "tps": n_tokens / max(elapsed, 0.001), | |
| }) | |
| return results | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # 4. Quality metrics (repetition score, diversity) | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def repetition_score(text): | |
| """1.0 = no repetition, 0.0 = heavy repetition. | |
| Computes ratio of unique bigrams to total bigrams. | |
| """ | |
| words = text.split() | |
| if len(words) < 2: | |
| return 1.0 | |
| bigrams = list(zip(words[:-1], words[1:])) | |
| unique = len(set(bigrams)) | |
| total = len(bigrams) | |
| return unique / total if total > 0 else 1.0 | |
| def distinct_n(text, n=1): | |
| """Distinct-N metric: unique n-grams / total n-grams.""" | |
| words = text.split() | |
| if len(words) < n: | |
| return 1.0 | |
| ngrams = list(zip(*[words[i:] for i in range(n)])) | |
| unique = len(set(ngrams)) | |
| total = len(ngrams) | |
| return unique / total if total > 0 else 1.0 | |
| def compute_quality_metrics(text): | |
| """Compute text quality metrics.""" | |
| return { | |
| "repetition_score": round(repetition_score(text), 3), | |
| "distinct_1": round(distinct_n(text, 1), 3), | |
| "distinct_2": round(distinct_n(text, 2), 3), | |
| "n_words": len(text.split()), | |
| "n_chars": len(text), | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # 5. Oracle log-prob (Qwen3 teacher forcing) | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def oracle_log_prob(text, qwen_model, qwen_tokenizer, device=DEVICE): | |
| """Compute mean per-token log-probability under Qwen3-0.6B. | |
| Higher (closer to 0) = more coherent text. | |
| """ | |
| inputs = qwen_tokenizer(text, return_tensors="pt", truncation=True, | |
| max_length=256).to(device) | |
| with torch.no_grad(): | |
| outputs = qwen_model(**inputs) | |
| logits = outputs.logits[:, :-1, :] # predict token t+1 | |
| target = inputs.input_ids[:, 1:] | |
| log_probs = F.log_softmax(logits, dim=-1) | |
| token_log_probs = log_probs.gather(2, target.unsqueeze(-1)).squeeze(-1) | |
| return token_log_probs.mean().item() | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # Main benchmark | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def run_benchmark(include_oracle=False): | |
| print("=" * 70) | |
| print("HEAD-TO-HEAD BENCHMARK: MDLM v3 vs AR CONTROL") | |
| print("=" * 70) | |
| # Load models | |
| print("\nLoading MDLM v3...") | |
| mdlm_model, mdlm_tok, mdlm_ckpt = load_mdlm() | |
| mdlm_params = sum(p.numel() for p in mdlm_model.parameters()) | |
| print(f" {mdlm_params:,} params ({mdlm_params/1e6:.1f}M)") | |
| print("Loading AR Control...") | |
| ar_model, ar_tok, ar_ckpt = load_ar() | |
| ar_params = sum(p.numel() for p in ar_model.parameters()) | |
| print(f" {ar_params:,} params ({ar_params/1e6:.1f}M)") | |
| # Load holdout data | |
| print("\nLoading holdout data...") | |
| tokens = np.load(DATA_DIR / "train_tokens_v3_128.npy", mmap_mode='r') | |
| holdout = torch.from_numpy(np.array(tokens[:1000], dtype=np.int16)).long() | |
| print(f" {len(holdout)} sequences for perplexity eval") | |
| # 1. Perplexity | |
| print("\n--- 1. PERPLEXITY ---") | |
| print(" MDLM (masked CE)...") | |
| mdlm_loss_val, mdlm_ppl = eval_mdlm_perplexity(mdlm_model, holdout, mdlm_tok) | |
| print(f" MDLM: loss={mdlm_loss_val:.4f} PPL={mdlm_ppl:.1f}") | |
| print(" AR (next-token CE)...") | |
| ar_loss_val, ar_ppl = ar_measure_ppl(ar_model, holdout) | |
| print(f" AR: loss={ar_loss_val:.4f} PPL={ar_ppl:.1f}") | |
| # 2. Forward throughput | |
| print("\n--- 2. FORWARD-PASS THROUGHPUT ---") | |
| print(" MDLM...") | |
| mdlm_tps = benchmark_throughput_mdlm(mdlm_model, mdlm_tok.vocab_size) | |
| for k, v in mdlm_tps.items(): | |
| print(f" {k}: {v['tps']:,.0f} TPS ({v['latency_ms']:.1f}ms)") | |
| print(" AR...") | |
| ar_tps = benchmark_throughput_ar(ar_model, ar_tok.vocab_size) | |
| for k, v in ar_tps.items(): | |
| print(f" {k}: {v['tps']:,.0f} TPS ({v['latency_ms']:.1f}ms)") | |
| # 3. Generation speed | |
| print("\n--- 3. GENERATION SPEED ---") | |
| print(" MDLM (full-parallel + semi-AR)...") | |
| mdlm_gen = benchmark_generation_mdlm(mdlm_model, mdlm_tok, n_samples=5) | |
| fp_tps = [r["tps"] for r in mdlm_gen["full_parallel"]] | |
| sar_tps = [r["tps"] for r in mdlm_gen["semi_ar"]] | |
| print(f" Full-parallel: {np.mean(fp_tps):.1f} tok/s avg") | |
| print(f" Semi-AR: {np.mean(sar_tps):.1f} tok/s avg") | |
| print(" AR (sequential with KV cache)...") | |
| ar_gen = benchmark_generation_ar(ar_model, ar_tok, n_samples=5) | |
| ar_gen_tps = [r["tps"] for r in ar_gen] | |
| print(f" AR: {np.mean(ar_gen_tps):.1f} tok/s avg") | |
| # 4. Quality | |
| print("\n--- 4. TEXT QUALITY ---") | |
| prompts = [ | |
| "Climate change is one of the biggest challenges", | |
| "The future of artificial intelligence depends on", | |
| "Education systems around the world need to", | |
| ] | |
| mdlm_samples = [] | |
| ar_samples = [] | |
| for prompt in prompts: | |
| prompt_ids = mdlm_tok.encode(prompt, add_special=False) | |
| mdlm_text = sample_semi_ar(mdlm_model, mdlm_tok, prompt_ids=prompt_ids, | |
| seq_len=128, n_samples=1, | |
| block_size=128, temperature=0.7)[0] | |
| ar_text = sample_ar(ar_model, ar_tok, prompt_ids=prompt_ids, | |
| max_new_tokens=64, temperature=0.7, top_p=0.95) | |
| mdlm_q = compute_quality_metrics(mdlm_text) | |
| ar_q = compute_quality_metrics(ar_text) | |
| mdlm_samples.append({"prompt": prompt, "text": mdlm_text[:300], "quality": mdlm_q}) | |
| ar_samples.append({"prompt": prompt, "text": ar_text[:300], "quality": ar_q}) | |
| print(f"\n Prompt: {prompt}") | |
| print(f" MDLM: {mdlm_text[:150]}") | |
| print(f" rep={mdlm_q['repetition_score']} d1={mdlm_q['distinct_1']} d2={mdlm_q['distinct_2']}") | |
| print(f" AR: {ar_text[:150]}") | |
| print(f" rep={ar_q['repetition_score']} d1={ar_q['distinct_1']} d2={ar_q['distinct_2']}") | |
| # 5. Oracle (optional) | |
| oracle_results = None | |
| if include_oracle: | |
| print("\n--- 5. ORACLE LOG-PROB (Qwen3-0.6B) ---") | |
| try: | |
| from transformers import AutoModelForCausalLM, AutoTokenizer as HFTokenizer | |
| print(" Loading Qwen3-0.6B...") | |
| qwen_tok = HFTokenizer.from_pretrained("Qwen/Qwen3-0.6B") | |
| qwen_model = AutoModelForCausalLM.from_pretrained( | |
| "Qwen/Qwen3-0.6B", torch_dtype=torch.bfloat16 | |
| ).to(DEVICE).eval() | |
| mdlm_oracle = [] | |
| ar_oracle = [] | |
| for s in mdlm_samples: | |
| lp = oracle_log_prob(s["text"], qwen_model, qwen_tok) | |
| mdlm_oracle.append(lp) | |
| for s in ar_samples: | |
| lp = oracle_log_prob(s["text"], qwen_model, qwen_tok) | |
| ar_oracle.append(lp) | |
| print(f" MDLM oracle LP: {np.mean(mdlm_oracle):.3f}") | |
| print(f" AR oracle LP: {np.mean(ar_oracle):.3f}") | |
| oracle_results = { | |
| "mdlm": mdlm_oracle, "ar": ar_oracle, | |
| "mdlm_mean": float(np.mean(mdlm_oracle)), | |
| "ar_mean": float(np.mean(ar_oracle)), | |
| } | |
| except Exception as e: | |
| print(f" Oracle skipped: {e}") | |
| # Compile results | |
| results = { | |
| "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| "gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU", | |
| "mdlm": { | |
| "params": mdlm_params, | |
| "config": mdlm_ckpt.get("config", {}), | |
| "perplexity": mdlm_ppl, | |
| "eval_loss": mdlm_loss_val, | |
| "throughput": mdlm_tps, | |
| "generation": { | |
| "full_parallel_tps": float(np.mean(fp_tps)), | |
| "semi_ar_tps": float(np.mean(sar_tps)), | |
| }, | |
| "samples": mdlm_samples, | |
| }, | |
| "ar": { | |
| "params": ar_params, | |
| "config": ar_ckpt.get("config", {}), | |
| "perplexity": ar_ppl, | |
| "eval_loss": ar_loss_val, | |
| "throughput": ar_tps, | |
| "generation": { | |
| "tps": float(np.mean(ar_gen_tps)), | |
| }, | |
| "samples": ar_samples, | |
| }, | |
| "oracle": oracle_results, | |
| } | |
| # Summary table | |
| print("\n" + "=" * 70) | |
| print("SUMMARY") | |
| print("=" * 70) | |
| print(f"{'Metric':<30} {'MDLM v3':>15} {'AR Control':>15}") | |
| print("-" * 62) | |
| print(f"{'Parameters':<30} {mdlm_params/1e6:>14.1f}M {ar_params/1e6:>14.1f}M") | |
| print(f"{'Layers':<30} {'10 (+AdaLN)':>15} {'15':>15}") | |
| print(f"{'Perplexity':<30} {mdlm_ppl:>15.1f} {ar_ppl:>15.1f}") | |
| print(f"{'Eval loss':<30} {mdlm_loss_val:>15.4f} {ar_loss_val:>15.4f}") | |
| for bs in [1, 8, 32]: | |
| k = f"batch_{bs}" | |
| m_tps = mdlm_tps[k]["tps"] | |
| a_tps = ar_tps[k]["tps"] | |
| speedup = m_tps / a_tps if a_tps > 0 else 0 | |
| print(f"{'Fwd TPS (batch=' + str(bs) + ')':<30} {m_tps:>15,.0f} {a_tps:>15,.0f} ({speedup:.1f}x)") | |
| print(f"{'Gen TPS (best mode)':<30} {max(np.mean(fp_tps), np.mean(sar_tps)):>15.1f} {np.mean(ar_gen_tps):>15.1f}") | |
| if oracle_results: | |
| print(f"{'Oracle log-prob':<30} {oracle_results['mdlm_mean']:>15.3f} {oracle_results['ar_mean']:>15.3f}") | |
| # Save | |
| output_path = RESULTS_DIR / "comparison_benchmark.json" | |
| with open(output_path, "w") as f: | |
| json.dump(results, f, indent=2, default=str) | |
| print(f"\nResults saved to {output_path}") | |
| return results | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--oracle", action="store_true", help="Include Qwen3 oracle scoring") | |
| args = parser.parse_args() | |
| run_benchmark(include_oracle=args.oracle) | |