122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
"""Check the exported Spark decode step against the HF reference.
|
|||
|
|
|
||
|
|
Runs the reference over a C-token prompt, hands its KV cache to
|
||
|
|
SparkDecodeStep (sliding layers get only the last 512 entries, which is the
|
||
|
|
whole point of the hybrid layout) and compares next-token logits.
|
||
|
|
"""
|
||
|
|
import argparse, importlib.util, json, sys, types
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import torch
|
||
|
|
|
||
|
|
ROOT = Path("/data/zhoutaichang/embedding_infer/vllm-omni")
|
||
|
|
|
||
|
|
|
||
|
|
def _load(name, path):
|
||
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
||
|
|
m = importlib.util.module_from_spec(spec)
|
||
|
|
sys.modules[name] = m
|
||
|
|
spec.loader.exec_module(m)
|
||
|
|
return m
|
||
|
|
|
||
|
|
|
||
|
|
for pkg_name in ("vllm_omni", "vllm_omni.edge"):
|
||
|
|
mod = types.ModuleType(pkg_name)
|
||
|
|
mod.__path__ = []
|
||
|
|
sys.modules[pkg_name] = mod
|
||
|
|
_load("vllm_omni.edge.qnn_export", ROOT / "vllm_omni/edge/qnn_export.py")
|
||
|
|
se = _load("vllm_omni.edge.spark_export", ROOT / "vllm_omni/edge/spark_export.py")
|
||
|
|
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--model", default="XHToken/Spark-X2.5-1.7B")
|
||
|
|
ap.add_argument("--context", type=int, default=1024)
|
||
|
|
ap.add_argument("--cache-layout", default="auto", choices=["auto", "ring", "roll"])
|
||
|
|
ap.add_argument("--gelu", default="exact", choices=["exact", "tanh", "sigmoid"])
|
||
|
|
ap.add_argument("--out", default="export_parity.json")
|
||
|
|
a = ap.parse_args()
|
||
|
|
|
||
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
|
|
|
||
|
|
tok = AutoTokenizer.from_pretrained(a.model, trust_remote_code=True)
|
||
|
|
text = ("The quick brown fox jumps over the lazy dog. " * 400)
|
||
|
|
ids = tok(text, add_special_tokens=False)["input_ids"][: a.context + 1]
|
||
|
|
assert len(ids) == a.context + 1, f"only got {len(ids)} tokens"
|
||
|
|
|
||
|
|
m = AutoModelForCausalLM.from_pretrained(a.model, trust_remote_code=True,
|
||
|
|
dtype=torch.float32).eval()
|
||
|
|
import copy
|
||
|
|
with torch.no_grad():
|
||
|
|
pre = m(torch.tensor([ids[:-1]]), use_cache=True)
|
||
|
|
# The decode call appends to the cache in place, so snapshot it first.
|
||
|
|
cache = copy.deepcopy(pre.past_key_values)
|
||
|
|
ref = m(torch.tensor([[ids[-1]]]), past_key_values=pre.past_key_values,
|
||
|
|
use_cache=True)
|
||
|
|
ref_logits = ref.logits[0, -1].float().numpy()
|
||
|
|
|
||
|
|
def kv(i):
|
||
|
|
try:
|
||
|
|
return cache.layers[i].keys, cache.layers[i].values
|
||
|
|
except AttributeError:
|
||
|
|
return cache.key_cache[i], cache.value_cache[i]
|
||
|
|
|
||
|
|
hf_cfg = json.loads((Path(m.config._name_or_path) / "config.json").read_text()) \
|
||
|
|
if Path(str(m.config._name_or_path)).exists() else m.config.to_dict()
|
||
|
|
cfg = se.config_from_spark(hf_cfg, None, include_lm_head=True,
|
||
|
|
cache_layout=a.cache_layout, gelu_mode=a.gelu)
|
||
|
|
step = se.SparkDecodeStep(cfg)
|
||
|
|
|
||
|
|
from huggingface_hub import snapshot_download
|
||
|
|
wdir = snapshot_download(a.model, allow_patterns=["*.safetensors"])
|
||
|
|
print("loaded", se.load_spark_weights(step, wdir), "tensors")
|
||
|
|
step.eval()
|
||
|
|
|
||
|
|
C = a.context
|
||
|
|
pos = C
|
||
|
|
emb = m.model.embedding(torch.tensor([[ids[-1]]])).float()
|
||
|
|
cos_sw, sin_sw = se._angles(pos, cfg.rotary_dim(se.SLIDING), cfg.rope_theta[se.SLIDING], torch.float32)
|
||
|
|
cos_f, sin_f = se._angles(pos, cfg.rotary_dim(se.FULL), cfg.rope_theta[se.FULL], torch.float32)
|
||
|
|
sw_len = cfg.cache_len(se.SLIDING, C)
|
||
|
|
# Ring masks only cache slots; roll masks the rebuilt window (cache + new).
|
||
|
|
mask_sw = torch.zeros(1, 1, 1, sw_len + (1 if cfg.layout_for(se.SLIDING) == "ring" else 0))
|
||
|
|
mask_full = torch.zeros(1, 1, 1, C + 1)
|
||
|
|
|
||
|
|
shapes = {}
|
||
|
|
caches = []
|
||
|
|
for i, t in enumerate(cfg.layer_types):
|
||
|
|
k, v = kv(i)
|
||
|
|
shapes.setdefault(t, k.shape[2])
|
||
|
|
if t == se.SLIDING:
|
||
|
|
# transformers already truncates sliding layers to the window; take
|
||
|
|
# the newest sw_len entries and left-pad (masked out) if it holds fewer.
|
||
|
|
k, v = k[:, :, -sw_len:], v[:, :, -sw_len:]
|
||
|
|
have = k.shape[2]
|
||
|
|
if have < sw_len:
|
||
|
|
pad = sw_len - have
|
||
|
|
z = torch.zeros(k.shape[0], k.shape[1], pad, k.shape[3])
|
||
|
|
# No mask needed: the window roll drops exactly these pad slots.
|
||
|
|
k, v = torch.cat([z, k.float()], 2), torch.cat([z, v.float()], 2)
|
||
|
|
caches += [k.float(), v.float()]
|
||
|
|
print("hf cache lens by layer type:", shapes, "| sw_len used:", sw_len)
|
||
|
|
|
||
|
|
with torch.no_grad():
|
||
|
|
out = step(emb, cos_sw, sin_sw, cos_f, sin_f, mask_sw, mask_full, *caches)
|
||
|
|
got = out[0][0].float().numpy()
|
||
|
|
|
||
|
|
def snr(ref, got):
|
||
|
|
return 20 * np.log10(np.linalg.norm(ref) / max(np.linalg.norm(ref - got), 1e-12))
|
||
|
|
|
||
|
|
rep = {
|
||
|
|
"context": C,
|
||
|
|
"cache_layout": a.cache_layout,
|
||
|
|
"gelu_mode": a.gelu,
|
||
|
|
"logits_snr_db": round(float(snr(ref_logits, got)), 2),
|
||
|
|
"max_abs_diff": float(np.max(np.abs(ref_logits - got))),
|
||
|
|
"ref_top1": int(np.argmax(ref_logits)),
|
||
|
|
"export_top1": int(np.argmax(got)),
|
||
|
|
"top1_agree": int(np.argmax(ref_logits)) == int(np.argmax(got)),
|
||
|
|
"top5_agree": np.argsort(-ref_logits)[:5].tolist() == np.argsort(-got)[:5].tolist(),
|
||
|
|
"kv_bytes": se.kv_cache_bytes(cfg, C),
|
||
|
|
}
|
||
|
|
json.dump(rep, open(a.out, "w"), indent=2)
|
||
|
|
print(json.dumps(rep, indent=2))
|