NewMarco's picture
Update app.py
48e1362 verified
Raw
History Blame Contribute Delete
17.3 kB
"""
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 索引
# ══════════════════════════════════════════════════════════════════
@spaces.GPU(duration=60)
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()