FAIE / expert_diff.py
abuz8studios's picture
FAIE engine code + docs (no weights)
421f9b7 verified
Raw History Blame Contribute Delete
8.47 kB
"""Verify the EXPERT / FFN path against PyTorch. This is the one part never checked.
MLA was verified cos 1.000000. The expert path never was. Evidence it is the real
suspect: at L8 the FFN adds only 0.224 to a 0.811 residual, and disabling ALL routed
experts changes the final residual by 3%. If expert output magnitude is far too
small, attention dominates and the representation collapses — which is exactly the
observed failure.
Checks, in order of how damning they'd be:
1. Q2_K / Q3_K dequant vs a独立 numpy dequant (catches a shared v1/v2 scale bug)
2. One expert's SwiGLU forward: kernel vs PyTorch
3. Shared-expert forward: kernel vs PyTorch
4. Whole MoE layer delta: kernel vs PyTorch
"""
import sys, ctypes, io
sys.path.insert(0, r"E:\ABU\NEOCORTEX")
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
import numpy as np
import torch
from gguf import GGUFReader
from faie_kernel import load_kernel
from faie_generate import (FAIEInference, dequant_any, HIDDEN_DIM, EXPERT_FFN_DIM,
N_EXPERTS, TOP_K)
import faie_generate
MODEL = r"E:\ABU\MODELS\llm\glm-5.2-ream\GLM-5.2-REAM-Q2_K.gguf"
FAIE_DIR = r"E:\ABU\NEOCORTEX\faie_model"
VP = ctypes.c_void_p
DEV = "cuda:0"
L = 8 # the layer where the collapse starts
def cos(a, b):
a, b = a.flatten().float(), b.flatten().float()
return float(torch.dot(a, b) / (a.norm() * b.norm() + 1e-12))
print("loading...")
reader = GGUFReader(MODEL, "r")
lib = load_kernel()
eng = FAIEInference(reader, lib, FAIE_DIR, n_layers=78)
faie_generate.FAIEInference._moe_ffn_warm = lambda self, x, li: lib.full_moe_layer_v2(
VP(x.data_ptr()), li, VP(self._norm_buf.data_ptr()),
VP(self._gate_buf.data_ptr()), VP(self._mega_temp.data_ptr()))
eng.forward_single_token(1)
lib.sync_device(); torch.cuda.synchronize()
lib.set_router_scale.argtypes = [ctypes.c_float]
# ---------- 1. dequant sanity: kernel matvec vs PyTorch dense matvec ----------
print(f"\n=== 1. Q2_K matvec: kernel vs PyTorch dequant (layer {L} shared gate) ===")
t_sg = eng._get_tensor(f"blk.{L}.ffn_gate_shexp.weight")
W = torch.from_numpy(np.ascontiguousarray(dequant_any(t_sg))).to(DEV).float() # [2048,6144]
print(f" shared gate dequant shape {list(W.shape)} rms={float((W*W).mean().sqrt()):.6f}")
torch.manual_seed(0)
x = (torch.randn(HIDDEN_DIM, device=DEV) * 0.05).float()
ref = W @ x
sh = eng._sh_cache.get(L)
y = torch.zeros(W.shape[0], dtype=torch.float32, device=DEV)
eng.cmv.q2k(eng.ffn_gpu[L]["sh_gate"]["gpu"] if "sh_gate" in eng.ffn_gpu.get(L, {}) else None,
x, y, W.shape[0], HIDDEN_DIM) if False else None
# use the registered shared-expert pointer directly via the generic matvec
lw = eng.ffn_gpu.get(L, {})
key = "sh_gate" if "sh_gate" in lw else ("gate" if "gate" in lw else None)
if key:
eng._matvec(lw[key], x, y)
lib.sync_device(); torch.cuda.synchronize()
print(f" kernel matvec vs PyTorch: cos={cos(ref, y):+.6f}"
f" |ref|={float(ref.norm()):.4f} |ker|={float(y.norm()):.4f}"
f" ratio={float(y.norm()/ref.norm()):.4f}")
else:
print(f" ffn_gpu[{L}] keys: {list(lw.keys())} (no shared gate found)")
# ---------- 2. one routed expert SwiGLU: kernel vs PyTorch ----------
print(f"\n=== 2. routed expert SwiGLU (layer {L}) ===")
gt = eng._get_tensor(f"blk.{L}.ffn_gate_exps.weight")
ut = eng._get_tensor(f"blk.{L}.ffn_up_exps.weight")
dt = eng._get_tensor(f"blk.{L}.ffn_down_exps.weight")
from faie_generate import dequant_q2k_fast, dequant_q3k_fast
from faie_generate import resolve_qtype
def one_expert(t, e, rows, cols):
"""Dequantize a SINGLE expert slice. Dequantizing all 128 is 6 GB and OOMs."""
qt = resolve_qtype(t.tensor_type)
raw = np.ascontiguousarray(t.data[e]).tobytes()
n = rows * cols
f = dequant_q2k_fast(raw, n) if qt == "Q2_K" else dequant_q3k_fast(raw, n)
return torch.from_numpy(np.asarray(f, dtype=np.float32).reshape(rows, cols).copy()).to(DEV)
def expert_fwd(e, h):
g_ = one_expert(gt, e, EXPERT_FFN_DIM, HIDDEN_DIM) @ h
u_ = one_expert(ut, e, EXPERT_FFN_DIM, HIDDEN_DIM) @ h
mid = (g_ * torch.sigmoid(g_)) * u_
return one_expert(dt, e, HIDDEN_DIM, EXPERT_FFN_DIM) @ mid
E = 0
Ge = one_expert(gt, E, EXPERT_FFN_DIM, HIDDEN_DIM)
Ue = one_expert(ut, E, EXPERT_FFN_DIM, HIDDEN_DIM)
De = one_expert(dt, E, HIDDEN_DIM, EXPERT_FFN_DIM)
print(f" per-expert shapes G{list(Ge.shape)} U{list(Ue.shape)} D{list(De.shape)}")
g = Ge @ x
u = Ue @ x
h = (g * torch.sigmoid(g)) * u
ref_e = De @ h
del Ge, Ue, De
print(f" PyTorch expert{E}: |gate|={float(g.norm()):.4f} |up|={float(u.norm()):.4f}"
f" |silu*up|={float(h.norm()):.4f} |out|={float(ref_e.norm()):.4f}")
gw, uw, dw = eng._ensure_expert_cached(L, E)
gb = torch.zeros(EXPERT_FFN_DIM, dtype=torch.float32, device=DEV)
ub = torch.zeros(EXPERT_FFN_DIM, dtype=torch.float32, device=DEV)
ob = torch.zeros(HIDDEN_DIM, dtype=torch.float32, device=DEV)
eng.cmv.expert_swiglu(gw, uw, dw, x, gb, ub, ob)
lib.sync_device(); torch.cuda.synchronize()
print(f" kernel expert{E}: |out|={float(ob.norm()):.4f}"
f" cos={cos(ref_e, ob):+.6f} ratio={float(ob.norm()/(ref_e.norm()+1e-12)):.4f}")
# ---------- 3. whole MoE layer delta ----------
print(f"\n=== 3. whole MoE layer {L}: kernel delta vs PyTorch delta ===")
ffn_w = eng.ffn_norms[L].float()
router = eng.routers[L].float() # [128, 6144]
bias = eng.expert_biases.get(L)
hn = x / torch.sqrt((x * x).mean() + 1e-5) * ffn_w
logits = router @ hn
sc = torch.sigmoid(logits)
sel = sc + (bias.float() if bias is not None else 0)
idx = torch.topk(sel, TOP_K).indices
w = sc[idx]
w = w / w.sum()
routed = torch.zeros(HIDDEN_DIM, device=DEV)
for k in range(TOP_K):
routed += float(w[k]) * expert_fwd(int(idx[k]), hn)
torch.cuda.empty_cache()
sg = torch.from_numpy(np.ascontiguousarray(dequant_any(
eng._get_tensor(f"blk.{L}.ffn_gate_shexp.weight")))).to(DEV).float()
su = torch.from_numpy(np.ascontiguousarray(dequant_any(
eng._get_tensor(f"blk.{L}.ffn_up_shexp.weight")))).to(DEV).float()
sd = torch.from_numpy(np.ascontiguousarray(dequant_any(
eng._get_tensor(f"blk.{L}.ffn_down_shexp.weight")))).to(DEV).float()
a = sg @ hn; b = su @ hn
shared = sd @ ((a * torch.sigmoid(a)) * b)
del sg, su, sd
torch.cuda.empty_cache()
print(f" PyTorch routed(sum w=1) |{float(routed.norm()):.4f}| shared |{float(shared.norm()):.4f}|")
for scale in (1.0, 2.5):
tot = routed * scale + shared
print(f" PyTorch rscale={scale}: |ffn delta| = {float(tot.norm()):.4f}")
if scale == 2.5:
ref_full = tot.clone()
else:
ref_1 = tot.clone()
# Make every PyTorch-selected expert resident first, or the kernel silently skips
# them and the comparison is measuring residency, not correctness.
for k in range(TOP_K):
eng._ensure_expert_cached(L, int(idx[k]))
lib.reset_expert_counters()
# CRITICAL: the preamble CUDA graph is captured against eng._x_buf. Passing any
# other buffer makes the graph replay read STALE data and silently route on the
# wrong vector. Must stage the input into _x_buf itself.
for scale in (1.0, 2.5):
lib.set_router_scale(ctypes.c_float(scale))
eng._x_buf.copy_(x)
xk = eng._x_buf
eng._moe_ffn_warm(xk, L)
lib.sync_device(); torch.cuda.synchronize()
d = xk - x
r = ref_1 if scale == 1.0 else ref_full
print(f" kernel rscale={scale}: |ffn delta| = {float(d.norm()):.4f}"
f" cos vs matching PyTorch = {cos(r, d):+.6f}"
f" ratio = {float(d.norm()/(r.norm()+1e-12)):.4f}")
req, exe = lib.get_experts_requested(), lib.get_experts_executed()
print(f" residency during those 2 calls: {exe}/{req}")
# What did the KERNEL's own router pick? Only this layer ran, so the shared
# topk buffer is unambiguous here.
k_ids = eng._topk_ids_dev.tolist()
k_val = eng._topk_vals_dev.tolist()
print(f"\n PyTorch top-8 : {idx.tolist()}")
print(f" kernel top-8 : {k_ids}")
print(f" overlap : {len(set(idx.tolist()) & set(k_ids))}/8")
print(f" PyTorch w : {[round(float(v),4) for v in w]}")
print(f" kernel w : {[round(float(v),4) for v in k_val]}")
print(f"\n If the id sets differ, the ROUTER is the bug. If they match but cos is ~0,")
print(f" the assembly (ordering / weight pairing / buffer reuse) is the bug.")
lib.set_router_scale(ctypes.c_float(2.5))
print("\n top-8 experts PyTorch picked:", idx.tolist())
print(" weights:", [round(float(v),4) for v in w])