""" GQA vs MLA Benchmark 脚本 对比 MiniMind GQA 与 MLA (Multi-head Latent Attention) 的性能差异: 1. 模型参数量 & KV-Cache 理论大小 2. 推理速度 (Prefill / Decode) 3. KV-Cache 显存占用 4. 训练吞吐量 5. 输出质量 (PPL) Usage: # 性能测试(随机权重) python scripts/benchmark_gqa_vs_mla.py [--device cuda:0] [--hidden_size 768] # 加载 checkpoint 测 PPL(必须提供 checkpoint 路径) python scripts/benchmark_gqa_vs_mla.py --device cuda:0 \ --gqa_ckpt out/pretrain_768.pth \ --mla_ckpt out/pretrain_768_mla.pth \ --data_path dataset/pretrain_data.jsonl \ --tokenizer_path model """ import argparse import gc import math import os import sys import time import torch sys.path.append(".") from model.model_minimind import MiniMindConfig, MiniMindForCausalLM from model.model_minimind_mla import MiniMindMLAConfig, MiniMindMLAForCausalLM # ──────────────────────────── helpers ──────────────────────────── def count_params(model): return sum(p.numel() for p in model.parameters()) def attention_params_per_layer(model): """统计第一个 transformer block 中 self_attn 的参数量""" block = model.model.layers[0] return sum(p.numel() for p in block.self_attn.parameters()) def fmt_num(n): if n >= 1e6: return f"{n / 1e6:.2f}M" if n >= 1e3: return f"{n / 1e3:.2f}K" return str(n) def fmt_mem(bytes_val): if bytes_val >= 1 << 30: return f"{bytes_val / (1 << 30):.2f} GB" if bytes_val >= 1 << 20: return f"{bytes_val / (1 << 20):.2f} MB" return f"{bytes_val:.0f} B" def warmup_forward(model, input_ids, device, n=3): """warmup: forward n 次,不计时""" model.train(False) for _ in range(n): with torch.no_grad(): model(input_ids.to(device)) torch.cuda.synchronize(device) def cleanup_gpu(device): gc.collect() torch.cuda.empty_cache() torch.cuda.reset_peak_memory_stats(device) # ────────────────────── model factories ────────────────────────── def build_dense_attention(hidden_size, attention_type): config = MiniMindConfig(hidden_size=hidden_size, attention_type=attention_type) return MiniMindForCausalLM(config) def build_mla(hidden_size, kv_lora_rank): config = MiniMindMLAConfig(hidden_size=hidden_size, kv_lora_rank=kv_lora_rank) return MiniMindMLAForCausalLM(config) # ────────────────────── Benchmark 1: 模型信息 ──────────────────── def benchmark_model_info(hidden_size): print("\n" + "=" * 70) print(" Benchmark 1: 模型信息对比") print("=" * 70) models = { "MHA": build_dense_attention(hidden_size, "mha"), "GQA": build_dense_attention(hidden_size, "gqa"), "MQA": build_dense_attention(hidden_size, "mqa"), "MLA(128)": build_mla(hidden_size, 128), "MLA(64)": build_mla(hidden_size, 64), "MLA(256)": build_mla(hidden_size, 256), } # Dense attention: KV-Cache floats/token/layer = 2 * num_kv_heads * head_dim gqa_config = MiniMindConfig(hidden_size=hidden_size, attention_type="gqa") kv_cache_gqa = 2 * gqa_config.num_key_value_heads * gqa_config.head_dim header = f"{'指标':<28}" + "".join(f"{name:>12}" for name in models) print(header) print("-" * len(header)) total_params = {} attn_params = {} kv_cache = {} for name, m in models.items(): total_params[name] = count_params(m) attn_params[name] = attention_params_per_layer(m) if "MLA" in name: rank = int(name.split("(")[1].rstrip(")")) kv_cache[name] = rank + m.config.rope_dim # kv_latent + k_rope else: kv_cache[name] = 2 * m.config.num_key_value_heads * m.config.head_dim row = f"{'总参数量':<28}" + "".join(f"{fmt_num(total_params[n]):>12}" for n in models) print(row) row = f"{'注意力参数量/层':<28}" + "".join(f"{fmt_num(attn_params[n]):>12}" for n in models) print(row) row = f"{'KV-Cache (floats/tok/layer)':<28}" + "".join(f"{kv_cache[n]:>12}" for n in models) print(row) row = f"{'KV-Cache 压缩比':<28}" + "".join( f"{kv_cache_gqa / kv_cache[n]:>11.1f}x" for n in models ) print(row) return models, kv_cache, kv_cache_gqa # ────────────────────── Benchmark 2: 推理速度 ──────────────────── def benchmark_inference_speed(models, device): print("\n" + "=" * 70) print(" Benchmark 2: 推理速度 (Prefill / Decode)") print("=" * 70) prompt_lens = [128, 512, 1024] batch_sizes = [1, 4] max_new_tokens = 128 for bs in batch_sizes: print(f"\n--- batch_size = {bs} ---") header = (f"{'模型':<16}{'prompt_len':>12}") for pl in prompt_lens: header += f"{'Prefill(ms)':>14}" for pl in prompt_lens: header += f"{'Decode(tok/s)':>16}" print(header) print("-" * len(header)) for name, model in models.items(): model = model.half().to(device).eval() row = f"{name:<16}" prefill_times = [] for pl in prompt_lens: input_ids = torch.randint(0, 6400, (bs, pl), device=device) warmup_forward(model, input_ids, device, n=2) torch.cuda.synchronize(device) t0 = time.perf_counter() with torch.no_grad(): model(input_ids) torch.cuda.synchronize(device) t1 = time.perf_counter() ms = (t1 - t0) * 1000 prefill_times.append(ms) row += f"{ms:>14.2f}" for i, pl in enumerate(prompt_lens): input_ids = torch.randint(0, 6400, (bs, pl), device=device) attn_mask = torch.ones(bs, pl, device=device) warmup_forward(model, input_ids, device, n=2) torch.cuda.synchronize(device) t0 = time.perf_counter() with torch.no_grad(): model.generate( input_ids=input_ids, attention_mask=attn_mask, max_new_tokens=max_new_tokens, use_cache=True, ) torch.cuda.synchronize(device) t1 = time.perf_counter() tokens_per_sec = max_new_tokens * bs / (t1 - t0) row += f"{tokens_per_sec:>16.1f}" print(row) del model cleanup_gpu(device) # ────────────────────── Benchmark 3: KV-Cache 显存 ─────────────── def benchmark_kv_cache_memory(models, device): print("\n" + "=" * 70) print(" Benchmark 3: KV-Cache 显存占用") print("=" * 70) context_lens = [256, 512, 1024, 2048, 4096] max_new_tokens = 64 bs = 1 header = f"{'模型':<16}" + "".join(f"{'ctx='+str(c):>12}" for c in context_lens) print(header) print("-" * len(header)) for name, model in models.items(): model = model.half().to(device).eval() row = f"{name:<16}" mem_results = [] for ctx_len in context_lens: cleanup_gpu(device) input_ids = torch.randint(0, 6400, (bs, ctx_len), device=device) attn_mask = torch.ones(bs, ctx_len, device=device) # 记录 generate 前的显存 torch.cuda.synchronize(device) mem_before = torch.cuda.max_memory_allocated(device) with torch.no_grad(): model.generate( input_ids=input_ids, attention_mask=attn_mask, max_new_tokens=max_new_tokens, use_cache=True, ) torch.cuda.synchronize(device) mem_after = torch.cuda.max_memory_allocated(device) delta = mem_after - mem_before mem_results.append(delta) row += f"{fmt_mem(delta):>12}" print(row) # 打印显存增长趋势 print(f" {'增长趋势':<14}", end="") for i in range(1, len(mem_results)): ratio = mem_results[i] / mem_results[i - 1] if mem_results[i - 1] > 0 else 0 print(f"{'×' + f'{ratio:.2f}':>12}", end="") print() del model cleanup_gpu(device) # ────────────────────── Benchmark 4: 训练吞吐量 ───────────────── def benchmark_training_throughput(models, device): print("\n" + "=" * 70) print(" Benchmark 4: 训练吞吐量 (forward + backward)") print("=" * 70) batch_size = 4 seq_len = 340 n_steps = 10 header = f"{'模型':<16}{'step时间(ms)':>16}{'steps/s':>12}{'tokens/s':>14}" print(header) print("-" * len(header)) for name, model in models.items(): model = model.float().to(device).train() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) # warmup for _ in range(3): input_ids = torch.randint(0, 6400, (batch_size, seq_len), device=device) labels = torch.randint(0, 6400, (batch_size, seq_len), device=device) outputs = model(input_ids, labels=labels) outputs.loss.backward() optimizer.step() optimizer.zero_grad() torch.cuda.synchronize(device) t0 = time.perf_counter() for _ in range(n_steps): input_ids = torch.randint(0, 6400, (batch_size, seq_len), device=device) labels = torch.randint(0, 6400, (batch_size, seq_len), device=device) outputs = model(input_ids, labels=labels) outputs.loss.backward() optimizer.step() optimizer.zero_grad() torch.cuda.synchronize(device) t1 = time.perf_counter() total_time = t1 - t0 step_ms = total_time / n_steps * 1000 steps_per_sec = n_steps / total_time tokens_per_sec = batch_size * seq_len * n_steps / total_time print(f"{name:<16}{step_ms:>16.2f}{steps_per_sec:>12.2f}{tokens_per_sec:>14.1f}") del model, optimizer cleanup_gpu(device) # ────────────────────── Benchmark 5: 输出质量 (PPL) ────────────── def benchmark_perplexity(args, device, hidden_size): print("\n" + "=" * 70) print(" Benchmark 5: 输出质量 (Perplexity)") print("=" * 70) from datasets import load_dataset from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_path) samples = load_dataset("json", data_files=args.data_path, split="train") # 取前 eval_samples 条,截取文本 max_seq = 512 all_tokens = [] for i in range(min(args.eval_samples, len(samples))): text = str(samples[i].get("text", "")) ids = tokenizer(text, add_special_tokens=False, max_length=max_seq, truncation=True).input_ids ids = [tokenizer.bos_token_id] + ids + [tokenizer.eos_token_id] all_tokens.append(ids) results = {} for ckpt_name, ckpt_path, config_cls, config_kwargs in [ ("GQA", args.gqa_ckpt, MiniMindConfig, {}), ("MLA(128)", args.mla_ckpt, MiniMindMLAConfig, {"kv_lora_rank": 128}), ("MLA(64)", args.mla_ckpt_r64, MiniMindMLAConfig, {"kv_lora_rank": 64}), ("MLA(256)", args.mla_ckpt_r256, MiniMindMLAConfig, {"kv_lora_rank": 256}), ]: if ckpt_path is None: continue if not os.path.isfile(ckpt_path): print(f" [SKIP] {ckpt_name}: checkpoint not found at {ckpt_path}") continue config_kw = {"hidden_size": hidden_size} config_kw.update(config_kwargs) config = config_cls(**config_kw) if isinstance(config, MiniMindMLAConfig): model = MiniMindMLAForCausalLM(config) else: model = MiniMindForCausalLM(config) state_dict = torch.load(ckpt_path, map_location="cpu") model.load_state_dict(state_dict, strict=True) model = model.half().to(device).eval() total_loss = 0.0 total_tokens = 0 batch_input_ids = [] batch_labels = [] for ids in all_tokens: pad_len = max_seq - len(ids) padded = ids + [tokenizer.pad_token_id] * pad_len labels = ids[1:] + [tokenizer.pad_token_id] * (pad_len + 1) # 标记 padding 为 -100 labels = [t if t != tokenizer.pad_token_id else -100 for t in labels] batch_input_ids.append(padded) batch_labels.append(labels) if len(batch_input_ids) == args.eval_batch_size: input_t = torch.tensor(batch_input_ids, dtype=torch.long, device=device) label_t = torch.tensor(batch_labels, dtype=torch.long, device=device) with torch.no_grad(): out = model(input_t, labels=label_t) # loss 是 cross_entropy 的 mean,乘以有效 token 数得到总 loss n_valid = (label_t != -100).sum().item() total_loss += out.loss.item() * n_valid total_tokens += n_valid batch_input_ids, batch_labels = [], [] # 处理剩余样本 if batch_input_ids: input_t = torch.tensor(batch_input_ids, dtype=torch.long, device=device) label_t = torch.tensor(batch_labels, dtype=torch.long, device=device) with torch.no_grad(): out = model(input_t, labels=label_t) n_valid = (label_t != -100).sum().item() total_loss += out.loss.item() * n_valid total_tokens += n_valid avg_nll = total_loss / total_tokens ppl = math.exp(avg_nll) results[ckpt_name] = ppl print(f" {ckpt_name:<16} Loss: {avg_nll:.4f} PPL: {ppl:.2f} (eval on {total_tokens} tokens)") del model, state_dict cleanup_gpu(device) if not results: print(" 没有可用的 checkpoint,跳过 PPL 测试") print(" 提示:使用 --gqa_ckpt / --mla_ckpt 参数指定权重路径") return {} # Markdown PPL 表格 print() cols = " | ".join(results.keys()) print(f"| Metric | {cols} |") sep = "|".join(["------"] * len(results)) print(f"|--------|{sep}|") print(f"| PPL | " + " | ".join(f"{results[k]:.2f}" for k in results) + " |") print() return results # ────────────────────── Markdown 汇总 ──────────────────────────── def print_markdown_summary(models_info, hidden_size): print("\n" + "=" * 70) print(" Markdown 汇总(可复制到简历 / README)") print("=" * 70) print() gqa_config = MiniMindConfig(hidden_size=hidden_size, attention_type="gqa") kv_cache_gqa = 2 * gqa_config.num_key_value_heads * gqa_config.head_dim print("### Model Info") print() print("| Metric | " + " | ".join(models_info.keys()) + " |") print("|--------|" + "|".join(["------"] * len(models_info)) + "|") total_params = {} attn_params = {} kv_cache = {} for name, m in models_info.items(): total_params[name] = count_params(m) attn_params[name] = attention_params_per_layer(m) if "MLA" in name: rank = int(name.split("(")[1].rstrip(")")) kv_cache[name] = rank + m.config.rope_dim # kv_latent + k_rope else: kv_cache[name] = 2 * m.config.num_key_value_heads * m.config.head_dim row = "| Total Params |" for n in models_info: row += f" {fmt_num(total_params[n])} |" print(row) row = "| Attn Params/Layer |" for n in models_info: row += f" {fmt_num(attn_params[n])} |" print(row) row = "| KV-Cache (floats/tok/layer) |" for n in models_info: row += f" {kv_cache[n]} |" print(row) row = "| KV-Cache Compression Ratio |" for n in models_info: row += f" {kv_cache_gqa / kv_cache[n]:.1f}x |" print(row) print() print("> hidden_size={}, num_layers={}, num_attention_heads={}, num_kv_heads={}, head_dim={}".format( gqa_config.hidden_size, gqa_config.num_hidden_layers, gqa_config.num_attention_heads, gqa_config.num_key_value_heads, gqa_config.head_dim )) print() # ────────────────────── main ───────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="GQA vs MLA Benchmark") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") parser.add_argument("--hidden_size", type=int, default=768) # Benchmark 5 (PPL) 参数 parser.add_argument("--gqa_ckpt", type=str, default=None, help="GQA checkpoint 路径 (e.g. out/pretrain_768.pth)") parser.add_argument("--mla_ckpt", type=str, default=None, help="MLA checkpoint 路径 (e.g. out/pretrain_768_mla.pth)") parser.add_argument("--mla_ckpt_r64", type=str, default=None, help="MLA(64) checkpoint 路径") parser.add_argument("--mla_ckpt_r256", type=str, default=None, help="MLA(256) checkpoint 路径") parser.add_argument("--data_path", type=str, default="dataset/pretrain_hq.jsonl", help="评估数据集路径") parser.add_argument("--tokenizer_path", type=str, default="model", help="tokenizer 路径") parser.add_argument("--eval_samples", type=int, default=500, help="评估样本数") parser.add_argument("--eval_batch_size", type=int, default=4, help="评估 batch size") args = parser.parse_args() device = torch.device(args.device) hidden_size = args.hidden_size has_ckpt = any([args.gqa_ckpt, args.mla_ckpt, args.mla_ckpt_r64, args.mla_ckpt_r256]) print("=" * 70) print(f" MiniMind GQA vs MLA Benchmark") print(f" hidden_size={hidden_size} device={device}") if has_ckpt: print(f" PPL evaluation: ENABLED (data={args.data_path})") print("=" * 70) # Benchmark 1: 模型信息(CPU 即可) models, kv_cache, kv_cache_gqa = benchmark_model_info(hidden_size) if device.type == "cuda": # Benchmark 2: 推理速度 models_gpu = { "MHA": build_dense_attention(hidden_size, "mha"), "GQA": build_dense_attention(hidden_size, "gqa"), "MQA": build_dense_attention(hidden_size, "mqa"), "MLA(128)": build_mla(hidden_size, 128), "MLA(64)": build_mla(hidden_size, 64), "MLA(256)": build_mla(hidden_size, 256), } benchmark_inference_speed(models_gpu, device) # Benchmark 3: KV-Cache 显存 models_gpu = { "MHA": build_dense_attention(hidden_size, "mha"), "GQA": build_dense_attention(hidden_size, "gqa"), "MQA": build_dense_attention(hidden_size, "mqa"), "MLA(128)": build_mla(hidden_size, 128), "MLA(64)": build_mla(hidden_size, 64), "MLA(256)": build_mla(hidden_size, 256), } benchmark_kv_cache_memory(models_gpu, device) # Benchmark 4: 训练吞吐量 models_gpu = { "MHA": build_dense_attention(hidden_size, "mha"), "GQA": build_dense_attention(hidden_size, "gqa"), "MQA": build_dense_attention(hidden_size, "mqa"), "MLA(128)": build_mla(hidden_size, 128), "MLA(64)": build_mla(hidden_size, 64), "MLA(256)": build_mla(hidden_size, 256), } benchmark_training_throughput(models_gpu, device) # Benchmark 5: PPL(需要提供 checkpoint 路径) if has_ckpt: benchmark_perplexity(args, device, hidden_size) else: print("\n" + "=" * 70) print(" Benchmark 5: 输出质量 (PPL) — SKIPPED") print("=" * 70) print(" 未指定 checkpoint,跳过 PPL 测试。") print(" 使用方法: python scripts/benchmark_gqa_vs_mla.py \\") print(" --gqa_ckpt out/pretrain_768.pth \\") print(" --mla_ckpt out/pretrain_768_mla.pth \\") print(" --data_path dataset/pretrain_hq.jsonl") else: print("\n[SKIP] Benchmark 2-5 需要 CUDA 设备,当前 device=cpu") # Markdown 汇总(使用 Benchmark 1 的 CPU 模型) print_markdown_summary(models, hidden_size) print("Done.") if __name__ == "__main__": main()