Spaces:
Sleeping
Sleeping
| """ | |
| Expert Co-activation Geometry — ZeroGPU 60s 适配版 | |
| ==================================================== | |
| 核心假设: MoE router 是否自发学到互补性? | |
| co-activated expert pair 的 head_sim < 随机 pair 的 head_sim | |
| 执行策略(适配 60s 限制): | |
| Step 1 (CPU) : 预计算并缓存各层的 64×64 head_sim 矩阵 | |
| Step 2 (GPU) : 仅做 forward pass + 捕获 routing 索引,存到全局 | |
| Step 3 (CPU) : 读取缓存矩阵 + routing 结果,做全部统计分析 | |
| Hook 位置(已通过 transformers 5.3.0 源码验证): | |
| layer.mlp.gate.forward 返回 (router_logits, router_scores, router_indices) | |
| router_indices shape: [seq_len, top_k=8] | |
| → hook output[2] 即为每 token 选中的 expert 索引 | |
| """ | |
| import torch | |
| import numpy as np | |
| import json, os, gc | |
| from itertools import combinations | |
| from scipy import stats | |
| import gradio as gr | |
| import spaces | |
| from huggingface_hub import hf_hub_download | |
| from safetensors.torch import load_file | |
| from transformers import AutoTokenizer, OlmoeForCausalLM | |
| from datasets import load_dataset | |
| # ══════════════════════════════════════════════════════════════════ | |
| # 全局配置 | |
| # ══════════════════════════════════════════════════════════════════ | |
| REPO_ID = "allenai/OLMoE-1B-7B-0924" | |
| N_LAYERS = 16 | |
| N_EXPERTS = 64 | |
| TOP_K = 8 | |
| HEAD_RATIO = 0.01 | |
| PROJ = "down_proj" | |
| CACHE_DIR = "/data/olmoe_cache" | |
| os.makedirs(CACHE_DIR, exist_ok=True) | |
| # 跨调用的全局 routing 结果 | |
| _routing_results: dict = {} | |
| # ══════════════════════════════════════════════════════════════════ | |
| # 工具函数(CPU) | |
| # ══════════════════════════════════════════════════════════════════ | |
| def get_shard_for_key(key: str): | |
| index_path = hf_hub_download( | |
| repo_id=REPO_ID, filename="model.safetensors.index.json" | |
| ) | |
| with open(index_path) as f: | |
| index = json.load(f) | |
| return index["weight_map"].get(key) | |
| def load_expert_weight(layer_idx: int, expert_idx: int, proj: str = PROJ): | |
| key = f"model.layers.{layer_idx}.mlp.experts.{expert_idx}.{proj}.weight" | |
| shard = get_shard_for_key(key) | |
| if shard is None: | |
| return None | |
| fp = hf_hub_download(repo_id=REPO_ID, filename=shard) | |
| sd = load_file(fp, device="cpu") | |
| W = sd[key].clone() | |
| del sd | |
| return W | |
| def head_sim_from_U(U1, U2, k_head: int) -> float: | |
| b1 = U1[:, :k_head] | |
| b2 = U2[:, :k_head] | |
| return torch.linalg.svdvals(b1.T @ b2).mean().item() | |
| # ══════════════════════════════════════════════════════════════════ | |
| # Step 1: 预计算 head_sim 矩阵(CPU) | |
| # ══════════════════════════════════════════════════════════════════ | |
| def build_headsim_matrix(layer_idx: int, proj: str = PROJ): | |
| cache_path = os.path.join(CACHE_DIR, f"headsim_L{layer_idx}_{proj}.npy") | |
| if os.path.exists(cache_path): | |
| print(f"[Layer {layer_idx}] Cache hit.") | |
| return np.load(cache_path) | |
| print(f"[Layer {layer_idx}] Loading {N_EXPERTS} expert weights...") | |
| Us, r_val = [], None | |
| for e in range(N_EXPERTS): | |
| W = load_expert_weight(layer_idx, e, proj) | |
| if W is None: | |
| return None | |
| W = W.to(torch.float32) | |
| if r_val is None: | |
| r_val = min(W.shape) | |
| U, _, _ = torch.linalg.svd(W, full_matrices=False) | |
| Us.append(U) | |
| del W | |
| k_head = max(1, int(r_val * HEAD_RATIO)) | |
| print(f"[Layer {layer_idx}] r={r_val}, k_head={k_head}. Computing pairs...") | |
| mat = np.eye(N_EXPERTS, dtype=np.float32) | |
| for i, j in combinations(range(N_EXPERTS), 2): | |
| sim = head_sim_from_U(Us[i], Us[j], k_head) | |
| mat[i, j] = sim | |
| mat[j, i] = sim | |
| np.save(cache_path, mat) | |
| print(f"[Layer {layer_idx}] Saved.") | |
| return mat | |
| def step1_run(layers_str: str, proj: str, progress=gr.Progress()) -> str: | |
| try: | |
| layers = [int(x.strip()) for x in layers_str.split(",") if x.strip().isdigit()] | |
| except Exception: | |
| return "❌ Invalid layer list." | |
| results = [] | |
| for i, l in enumerate(layers): | |
| progress(i / len(layers), desc=f"Layer {l}...") | |
| mat = build_headsim_matrix(l, proj) | |
| if mat is None: | |
| results.append(f"Layer {l}: ❌ Failed") | |
| continue | |
| od = mat[np.triu_indices(N_EXPERTS, k=1)] | |
| results.append( | |
| f"Layer {l}: ✅ mean={od.mean():.5f} std={od.std():.5f} " | |
| f"min={od.min():.5f} max={od.max():.5f}" | |
| ) | |
| return "\n".join(results) | |
| # ══════════════════════════════════════════════════════════════════ | |
| # Step 2: Forward pass(GPU)— 只抓 routing 索引 | |
| # ══════════════════════════════════════════════════════════════════ | |
| def step2_forward_pass(n_tokens: int, layers_str: str) -> str: | |
| global _routing_results | |
| _routing_results.clear() | |
| try: | |
| layers = [int(x.strip()) for x in layers_str.split(",") if x.strip().isdigit()] | |
| except Exception: | |
| return "❌ Invalid layer list." | |
| # 加载模型 | |
| print("Loading tokenizer + model...") | |
| tokenizer = AutoTokenizer.from_pretrained(REPO_ID) | |
| model = OlmoeForCausalLM.from_pretrained( | |
| REPO_ID, | |
| torch_dtype=torch.float16, | |
| low_cpu_mem_usage=True, | |
| ).cuda() | |
| model.eval() | |
| # 准备文本 | |
| print("Preparing text...") | |
| dataset = load_dataset("wikitext", "wikitext-2-raw-v1", | |
| split="test", trust_remote_code=True) | |
| text = " ".join(t for t in dataset["text"][:100] if t.strip()) | |
| tokens = tokenizer(text, return_tensors="pt", | |
| truncation=True, max_length=n_tokens) | |
| input_ids = tokens["input_ids"].to(model.device) | |
| actual_len = input_ids.shape[1] | |
| print(f"Tokens: {actual_len}") | |
| # 注册 hooks | |
| # gate.forward → (router_logits, router_scores, router_indices) | |
| # router_indices: [seq_len, top_k=8] | |
| raw_log = {l: [] for l in layers} | |
| hooks = [] | |
| for layer_idx in layers: | |
| gate = model.model.layers[layer_idx].mlp.gate | |
| def make_hook(lidx): | |
| def hook(module, inp, output): | |
| # output[2] = router_indices | |
| indices = output[2].detach().cpu() | |
| raw_log[lidx].append(indices) | |
| return hook | |
| h = gate.register_forward_hook(make_hook(layer_idx)) | |
| hooks.append(h) | |
| # Forward | |
| print("Forward pass...") | |
| with torch.no_grad(): | |
| _ = model(input_ids) | |
| for h in hooks: | |
| h.remove() | |
| # 合并结果到全局 | |
| for l in layers: | |
| if raw_log[l]: | |
| _routing_results[l] = torch.cat(raw_log[l], dim=0).numpy() | |
| print(f"Layer {l}: {_routing_results[l].shape[0]} tokens captured") | |
| else: | |
| print(f"Layer {l}: ⚠️ No data!") | |
| del model | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| captured = [l for l in layers if l in _routing_results] | |
| not_captured = [l for l in layers if l not in _routing_results] | |
| msg = f"✅ Forward pass done. Tokens: {actual_len}\n" | |
| msg += f"Layers captured: {captured}\n" | |
| if not_captured: | |
| msg += f"⚠️ Missing layers: {not_captured}\n" | |
| msg += "\nRun Step 3 to compute statistics." | |
| return msg | |
| # ══════════════════════════════════════════════════════════════════ | |
| # Step 3: 统计分析(纯 CPU) | |
| # ══════════════════════════════════════════════════════════════════ | |
| def step3_statistics(layers_str: str, proj: str) -> str: | |
| try: | |
| layers = [int(x.strip()) for x in layers_str.split(",") if x.strip().isdigit()] | |
| except Exception: | |
| return "❌ Invalid layer list." | |
| missing_mat = [l for l in layers if not os.path.exists( | |
| os.path.join(CACHE_DIR, f"headsim_L{l}_{proj}.npy"))] | |
| missing_rout = [l for l in layers if l not in _routing_results] | |
| if missing_mat: | |
| return f"❌ head_sim matrices missing for layers {missing_mat}. Run Step 1." | |
| if missing_rout: | |
| return f"❌ Routing data missing for layers {missing_rout}. Run Step 2." | |
| lines = [ | |
| "=" * 82, | |
| "Expert Co-activation Geometry — Statistical Results", | |
| "=" * 82, | |
| "H1: co-activated pairs have LOWER head_sim than random (complementarity)", | |
| "", | |
| f"{'Layer':>5} | {'CoAct μ':>9} | {'Rand μ':>9} | " | |
| f"{'Δ':>9} | {'z':>7} | {'p (1-tail)':>11} | Verdict", | |
| "─" * 82, | |
| ] | |
| for l in layers: | |
| mat = np.load(os.path.join(CACHE_DIR, f"headsim_L{l}_{proj}.npy")) | |
| decisions = _routing_results[l] # [n_tokens, 8] | |
| # co-activated sims | |
| coact_sims = [] | |
| for row in decisions: | |
| experts = [int(e) for e in row] | |
| for i, j in combinations(experts, 2): | |
| coact_sims.append(float(mat[i, j])) | |
| coact_sims = np.array(coact_sims) | |
| # random baseline | |
| off_diag = mat[np.triu_indices(N_EXPERTS, k=1)] | |
| rand_mean = off_diag.mean() | |
| rand_std = off_diag.std() | |
| coact_mean = coact_sims.mean() | |
| n = len(coact_sims) | |
| # z-test, left-tail | |
| z = (coact_mean - rand_mean) / (rand_std / np.sqrt(n)) | |
| p = float(stats.norm.cdf(z)) | |
| delta = coact_mean - rand_mean | |
| if p < 0.001 and delta < 0: verdict = "✅ COMPLEMENTARY p<0.001" | |
| elif p < 0.05 and delta < 0: verdict = "✅ COMPLEMENTARY p<0.05" | |
| elif p > 0.95 and delta > 0: verdict = "❌ REDUNDANT" | |
| else: verdict = "➖ NO SIGNAL" | |
| lines.append( | |
| f"{l:>5} | {coact_mean:>9.5f} | {rand_mean:>9.5f} | " | |
| f"{delta:>+9.5f} | {z:>7.2f} | {p:>11.6f} | {verdict}" | |
| ) | |
| lines.append( | |
| f" | CoAct p10={np.percentile(coact_sims,10):.5f} " | |
| f"p50={np.percentile(coact_sims,50):.5f} " | |
| f"p90={np.percentile(coact_sims,90):.5f} n={n}" | |
| ) | |
| lines.append( | |
| f" | Random p10={np.percentile(off_diag,10):.5f} " | |
| f"p50={np.percentile(off_diag,50):.5f} " | |
| f"p90={np.percentile(off_diag,90):.5f} " | |
| f"n_tokens={decisions.shape[0]}" | |
| ) | |
| lines.append("") | |
| lines += [ | |
| "=" * 82, | |
| "Δ < 0 & p < 0.05 → Router learned complementarity (research hypothesis confirmed)", | |
| "Δ > 0 & p > 0.95 → Router selects redundant experts", | |
| "|Δ| ≈ 0 → No geometric preference in routing", | |
| "=" * 82, | |
| ] | |
| return "\n".join(lines) | |
| # ══════════════════════════════════════════════════════════════════ | |
| # Matrix Summary(随时可查,CPU) | |
| # ══════════════════════════════════════════════════════════════════ | |
| def matrix_summary(layers_str: str, proj: str) -> str: | |
| try: | |
| layers = [int(x.strip()) for x in layers_str.split(",") if x.strip().isdigit()] | |
| except Exception: | |
| return "❌ Invalid layer list." | |
| lines = ["head_sim Matrix Summary", "=" * 50] | |
| for l in layers: | |
| p = os.path.join(CACHE_DIR, f"headsim_L{l}_{proj}.npy") | |
| if not os.path.exists(p): | |
| lines.append(f"\nLayer {l}: ❌ Not computed") | |
| continue | |
| mat = np.load(p) | |
| od = mat[np.triu_indices(N_EXPERTS, k=1)] | |
| upper = np.triu(mat, k=1) | |
| idx_max = np.unravel_index(upper.argmax(), mat.shape) | |
| upper_masked = np.where(upper > 0, upper, np.inf) | |
| idx_min = np.unravel_index(upper_masked.argmin(), mat.shape) | |
| lines += [ | |
| f"\nLayer {l}:", | |
| f" mean={od.mean():.5f} std={od.std():.5f}", | |
| f" min={od.min():.5f} max={od.max():.5f}", | |
| f" p10={np.percentile(od,10):.5f} " | |
| f"p50={np.percentile(od,50):.5f} " | |
| f"p90={np.percentile(od,90):.5f}", | |
| f" Most similar: E{idx_max[0]:02d} vs E{idx_max[1]:02d} " | |
| f"sim={mat[idx_max]:.5f}", | |
| f" Most different: E{idx_min[0]:02d} vs E{idx_min[1]:02d} " | |
| f"sim={mat[idx_min]:.5f}", | |
| ] | |
| return "\n".join(lines) | |
| # ══════════════════════════════════════════════════════════════════ | |
| # Gradio UI | |
| # ══════════════════════════════════════════════════════════════════ | |
| with gr.Blocks(title="Expert Co-activation Geometry") as demo: | |
| gr.Markdown(""" | |
| # 🔬 Expert Co-activation Geometry | |
| **Research Question**: Does OLMoE's router *systematically* co-activate | |
| geometrically *complementary* expert pairs (low head_sim)? | |
| | Step | Hardware | What it does | | |
| |------|----------|--------------| | |
| | 1 | CPU | Precompute & cache 64×64 head_sim matrices | | |
| | 2 | GPU (≤60s) | Forward pass on WikiText-2, capture routing indices | | |
| | 3 | CPU | Statistical analysis: co-activated vs random | | |
| """) | |
| with gr.Tabs(): | |
| with gr.Tab("⚙️ Step 1 · head_sim Matrices (CPU)"): | |
| gr.Markdown("Each layer ≈ 3–5 min. Cached to disk — runs once only.") | |
| with gr.Row(): | |
| s1_layers = gr.Textbox(value="0,7,15", | |
| label="Layers (0–15, comma-separated)") | |
| s1_proj = gr.Dropdown(["down_proj","up_proj","gate_proj"], | |
| value="down_proj", label="Projection") | |
| s1_btn = gr.Button("▶ Compute Matrices", variant="primary") | |
| s1_out = gr.Textbox(label="Output", lines=10) | |
| s1_btn.click(fn=step1_run, inputs=[s1_layers, s1_proj], outputs=[s1_out]) | |
| with gr.Tab("🚀 Step 2 · Forward Pass (GPU ≤60s)"): | |
| gr.Markdown( | |
| "Loads OLMoE, runs WikiText-2 forward pass, captures routing. \n" | |
| "Keep tokens ≤ 128 to stay within 60 s. \n" | |
| "Results stored in memory → run Step 3 immediately after." | |
| ) | |
| with gr.Row(): | |
| s2_layers = gr.Textbox(value="0,7,15", label="Layers to hook") | |
| s2_ntokens = gr.Slider(64, 256, value=128, step=64, | |
| label="Tokens (≤128 recommended for 60s)") | |
| s2_btn = gr.Button("▶ Run Forward Pass", variant="primary") | |
| s2_out = gr.Textbox(label="Status", lines=10) | |
| s2_btn.click(fn=step2_forward_pass, | |
| inputs=[s2_ntokens, s2_layers], outputs=[s2_out]) | |
| with gr.Tab("📊 Step 3 · Statistics (CPU)"): | |
| gr.Markdown("Run immediately after Step 2 (routing data lives in memory).") | |
| with gr.Row(): | |
| s3_layers = gr.Textbox(value="0,7,15", label="Layers") | |
| s3_proj = gr.Dropdown(["down_proj","up_proj","gate_proj"], | |
| value="down_proj", label="Projection") | |
| s3_btn = gr.Button("▶ Analyse", variant="primary") | |
| s3_out = gr.Textbox(label="Results", lines=35) | |
| s3_btn.click(fn=step3_statistics, | |
| inputs=[s3_layers, s3_proj], outputs=[s3_out]) | |
| with gr.Tab("🗂️ Matrix Summary"): | |
| gr.Markdown("Inspect cached matrices — no GPU needed.") | |
| with gr.Row(): | |
| sm_layers = gr.Textbox(value="0,7,15", label="Layers") | |
| sm_proj = gr.Dropdown(["down_proj","up_proj","gate_proj"], | |
| value="down_proj", label="Projection") | |
| sm_btn = gr.Button("▶ Show Summary", variant="secondary") | |
| sm_out = gr.Textbox(label="Summary", lines=25) | |
| sm_btn.click(fn=matrix_summary, | |
| inputs=[sm_layers, sm_proj], outputs=[sm_out]) | |
| demo.launch() |