DeepInfer/examples/neuron/reference_golden.py
whn09 8a5209ee65 Neuron basic mode — part 3: end-to-end check vs the official reference
- examples/neuron: tiny random V4.1 checkpoint in the real HF layout,
  pure-torch replacements for the official reference kernels, golden
  generation, a teacher-forced DeepInfer runner (LLM offline API) and a
  comparison tool. CPU mode: 25/27 greedy tokens match, worst logits
  cos 0.9985 / rel 0.056 against a reference self-noise floor of 0.032-0.060.
- model_loader: check parameters against the platform device, not is_cuda.
- deep_gemm paged MQA logits: do not dereference block-table slots past the
  context.
- bind_cpu: skip in Neuron CPU mode (1 core per NeuronCore share would
  serialize host compute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:20:02 +08:00

342 lines
18 KiB
Python

"""Ground truth for the Neuron port: run the OFFICIAL DeepSeek-V4.1 reference model on the host CPU.
The downloaded reference (`inference/model.py`, `engram.py`, `convert.py`, ...) is used unmodified; only its
TileLang `kernel` module is swapped for the pure-torch `ref_kernels_torch` (same numerics, see that file).
DSpark/MTP (`dspark_block_size=0`) and vision (`vision_n_layers=0`) are disabled: DeepInfer runs text-only with
DEEPINFER_USE_NEXTN_DECODE=0, and neither affects the backbone logits.
Steps:
1. convert the HF checkpoint with the official convert.py (MP=1, --expert-dtype fp4) unless already done;
2. build reference ModelArgs from the HF config.json (same mapping as the official inference/config.json);
3. per prompt (batch 1, fresh model): prefill the whole prompt, then N greedy decode steps (temperature 0,
no EOS stop), saving logits (fp32), greedy tokens, per-layer block outputs, indexer top-k and engram hashes.
Usage (on the box, host CPU only; TORCH_DEVICE_BACKEND_AUTOLOAD=0 keeps torch_neuronx/NRT out of the process):
TORCH_DEVICE_BACKEND_AUTOLOAD=0 ~/di/venv/bin/python -I examples/neuron/reference_golden.py \
--hf-ckpt ~/di/tiny_ckpt --ref-dir ~/di/ref_dsv41/inference --converted ~/di/tiny_ckpt_ref_mp1 \
--out ~/di/golden --summary-dir examples/neuron/golden
"""
from __future__ import annotations
import argparse
import dataclasses
import functools
import hashlib
import json
import os
import subprocess
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
PROMPTS = {
# short: no window wrap, no compressed top-k selection
"short": "The quick brown fox jumps over the lazy dog.",
# medium (~200 tokens): wraps the 128-token sliding window and makes both indexer top-k levels select
"medium": (
"In the early morning the harbor was quiet. Fishing boats rocked gently against the wooden piers, "
"their ropes creaking as the tide came in. An old man sat on an overturned crate, mending a net with "
"slow, careful hands. He had worked these waters for more than fifty years, and he could read the "
"weather in the color of the sky and the shape of the clouds. Today the sky was pale gray, and a thin "
"mist hung over the water. Somewhere a gull cried, and the sound echoed off the stone walls of the "
"warehouses. A young girl ran down the pier, her boots clattering on the boards. She stopped beside "
"the old man and asked him whether the boats would go out today. He looked at the horizon for a long "
"moment before answering. The sea, he said, decides that for us. We only listen."
),
}
# long (~700 tokens): more than candidate_topk_blocks * candidate_block_size = 512 compressed positions in the
# ratio-1 layers, so the candidate block pre-filter is active as well
PROMPTS["long"] = " ".join(
[
PROMPTS["medium"],
"The history of computing is a story of abstraction. Early machines were programmed by rewiring "
"panels and flipping switches, and every program was tied to the physical layout of the hardware. "
"Assembly languages gave names to instructions, and compilers later translated whole expressions "
"into sequences of those instructions. Operating systems hid the details of disks and terminals "
"behind files and streams, while virtual memory let each process pretend it owned the entire machine.",
"Networks added another layer. Packets travel through routers that know nothing about the "
"applications sending them, and protocols stacked on top of one another turn unreliable links into "
"dependable conversations. A web page that appears in a fraction of a second may involve dozens of "
"servers, caches and databases spread across several continents, each one following rules that were "
"agreed upon decades ago by people who could not have imagined how the system would grow.",
"Mathematics offers a similar lesson. A proof about prime numbers does not care whether the numbers "
"are written in decimal or binary, on paper or in silicon. The same theorem holds in every notation, "
"and that independence is exactly what makes it useful. Engineers borrow this idea when they define "
"interfaces: as long as both sides respect the contract, either side can be rewritten, optimized or "
"replaced without the other noticing.",
"Cooking, perhaps surprisingly, works the same way. A recipe names its ingredients and steps but "
"leaves the choice of pan, stove and kitchen to the cook. Bread rises because yeast consumes sugar "
"and releases gas, whether the dough is kneaded by hand in a village bakery or by a machine in a "
"factory. Understanding the underlying chemistry lets a baker adapt when the flour is different, the "
"air is humid, or the oven runs hot.",
"Music, too, depends on structure. A melody can be transposed into another key, played on a different "
"instrument, or slowed down to half its tempo, and listeners will still recognize it. Composers rely on "
"patterns of tension and release, repetition and variation, to guide the ear through a piece. The notes "
"on the page are only instructions; the performance brings them to life in a particular room on a "
"particular evening, never exactly the same twice.",
"Gardens change with every season. In spring the first green shoots push through the cold soil, and by "
"summer the beds are crowded with tomatoes, beans and sunflowers that lean toward the light. Autumn "
"brings the harvest and the slow work of clearing dead stems, while winter rests under frost and snow. "
"A patient gardener learns that each year is different and that the soil remembers what was done to it.",
"At the end of the day the harbor grew quiet again. The boats returned one by one, low in the water "
"with their catch, and the old man folded his net and walked slowly home along the shore.",
]
)
def hf_to_model_args(hf: dict) -> dict:
"""HF config.json -> reference ModelArgs kwargs (field mapping as in the official inference/config.json)."""
t = hf["text_config"]
v = hf["vision_config"]
rs = t["rope_scaling"]
return dict(
vocab_size=t["vocab_size"],
dim=t["hidden_size"],
moe_inter_dim=t["moe_intermediate_size"],
n_layers=t["num_hidden_layers"],
n_mtp_layers=t["num_nextn_predict_layers"],
dspark_block_size=t["dspark_block_size"],
dspark_noise_token_id=t["dspark_noise_token_id"],
dspark_target_layer_ids=tuple(t["dspark_target_layer_ids"]),
dspark_markov_rank=t["dspark_markov_rank"],
dspark_n_routed_experts=t["dspark_n_routed_experts"],
dspark_n_activated_experts=t["dspark_num_experts_per_tok"],
n_heads=t["num_attention_heads"],
n_routed_experts=t["n_routed_experts"],
n_shared_experts=t["n_shared_experts"],
n_activated_experts=t["num_experts_per_tok"],
score_func=t["scoring_func"],
norm_topk_prob=t["norm_topk_prob"],
route_scale=t["routed_scaling_factor"],
swiglu_limit=t["swiglu_limit"],
q_lora_rank=t["q_lora_rank"],
head_dim=t["head_dim"],
rope_head_dim=t["qk_rope_head_dim"],
norm_eps=t["rms_norm_eps"],
o_groups=t["o_groups"],
o_lora_rank=t["o_lora_rank"],
window_size=t["sliding_window"],
kv_source_layers=tuple(t["kv_source_layer_ids"]),
index_source_layers=tuple(t["index_source_layer_ids"]),
original_seq_len=rs["original_max_position_embeddings"],
rope_theta=t["rope_theta"],
rope_factor=rs["factor"],
beta_fast=rs["beta_fast"],
beta_slow=rs["beta_slow"],
index_n_heads=t["index_n_heads"],
index_head_dim=t["index_head_dim"],
index_topk=t["index_topk"],
candidate_source_layer=t["candidate_source_layer_id"],
candidate_topk_blocks=t["candidate_topk_blocks"],
candidate_block_size=t["candidate_block_size"],
hc_mult=t["hc_mult"],
hc_sinkhorn_iters=t["hc_sinkhorn_iters"],
hc_eps=t["hc_eps"],
engram_layer_ids=tuple(t["engram_layer_ids"]),
engram_vocab_size=t["engram_vocab_size"],
engram_num_embeddings=tuple(t["engram_num_embeddings"]),
engram_max_ngram_size=t["engram_max_ngram_size"],
engram_pad_id=t["engram_pad_token_id"],
engram_compressed_vocab_size=t["engram_compressed_vocab_size"],
engram_n_heads=t["engram_n_heads"],
engram_head_dim=t["engram_head_dim"],
dtype="fp8" if hf["quantization_config"]["quant_method"] == "fp8" else "bf16",
expert_dtype=hf["quantization_config"]["expert_dtype"],
compress_rope_theta=t["compress_rope_theta"],
compress_ratios=tuple(t["compress_ratios"]),
vision_n_layers=v["num_hidden_layers"],
vision_dim=v["hidden_size"],
vision_n_heads=v["num_attention_heads"],
vision_inter_dim=v["intermediate_size"],
vision_patch_size=v["patch_size"],
vision_rope_theta=v["rope_theta"],
vision_downsample_ratio=v["downsample_ratio"],
vision_max_n_token=v["max_image_tokens"],
vision_min_pixels=v["min_pixels"],
vision_max_wh_ratio=v["max_wh_ratio"],
image_token_id=hf["image_token_id"],
)
def convert(ref_dir: str, hf_ckpt: str, out: str) -> str:
path = os.path.join(out, "model0-mp1.safetensors")
if os.path.exists(path):
print(f"[golden] using existing converted checkpoint {path}")
return path
cmd = [sys.executable, "-I", os.path.join(ref_dir, "convert.py"), "--hf-ckpt-path", hf_ckpt, "--save-path", out,
"--model-parallel", "1", "--expert-dtype", "fp4"]
print("[golden] $", " ".join(cmd))
subprocess.run(cmd, check=True, cwd="/tmp")
return path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--hf-ckpt", default="~/di/tiny_ckpt")
ap.add_argument("--ref-dir", default="~/di/ref_dsv41/inference", help="copy of the official inference/ dir")
ap.add_argument("--converted", default="~/di/tiny_ckpt_ref_mp1")
ap.add_argument("--out", default="~/di/golden")
ap.add_argument("--summary-dir", default=os.path.join(HERE, "golden"))
ap.add_argument("--steps", type=int, default=8, help="greedy decode steps after the prefill")
ap.add_argument("--prompts", default=",".join(PROMPTS))
ap.add_argument("--threads", type=int, default=16)
ap.add_argument("--max-seq-len", type=int, default=2048)
ap.add_argument(
"--force-tokens-dir",
default=None,
help="teacher forcing: feed the `tokens` of <dir>/<prompt>.pt as decode inputs instead of this run's argmax "
"(used to measure the reference's own numerical noise floor against an existing golden)",
)
args = ap.parse_args()
for k in ("hf_ckpt", "ref_dir", "converted", "out", "summary_dir"):
setattr(args, k, os.path.abspath(os.path.expanduser(getattr(args, k))))
if args.force_tokens_dir:
args.force_tokens_dir = os.path.abspath(os.path.expanduser(args.force_tokens_dir))
os.makedirs(args.out, exist_ok=True)
os.makedirs(args.summary_dir, exist_ok=True)
import torch
torch.set_num_threads(args.threads)
assert "torch_neuronx" not in sys.modules, "run with TORCH_DEVICE_BACKEND_AUTOLOAD=0 (host CPU only)"
# ---- reference code: real model.py / engram.py, torch kernels instead of TileLang ----
sys.path.insert(0, HERE)
import ref_kernels_torch
sys.modules["kernel"] = ref_kernels_torch
sys.path.insert(1, args.ref_dir)
import engram as ref_engram
import model as ref_model
from safetensors.torch import load_file
from transformers import AutoTokenizer
assert ref_model.act_quant is ref_kernels_torch.act_quant
assert os.path.dirname(os.path.abspath(ref_model.__file__)) == args.ref_dir
# the compressed token map walks the whole vocab; build it once
ref_engram.build_compressed_token_map = functools.lru_cache(maxsize=None)(ref_engram.build_compressed_token_map)
ckpt_file = convert(args.ref_dir, args.hf_ckpt, args.converted)
hf_cfg = json.load(open(os.path.join(args.hf_ckpt, "config.json")))
margs = hf_to_model_args(hf_cfg)
margs.update(
max_batch_size=1,
max_seq_len=args.max_seq_len,
temperature=0.0,
vision_n_layers=0, # text only
dspark_block_size=0, # no DSpark / MTP modules
)
model_args = ref_model.ModelArgs(**margs)
print("[golden]", model_args)
tokenizer = AutoTokenizer.from_pretrained(args.converted)
bos = hf_cfg["bos_token_id"]
state = load_file(ckpt_file)
dropped = sorted(k for k in state if k.startswith(("vision.", "aligner.", "mtp.", "image_")) or k.endswith(".bias_vl"))
state = {k: v for k, v in state.items() if k not in set(dropped)}
print(f"[golden] {len(state)} tensors for the backbone; dropped {len(dropped)} (vision/aligner/mtp/image_*/bias_vl)")
torch.set_default_dtype(torch.bfloat16)
for name in args.prompts.split(","):
text = PROMPTS[name]
ids = [bos] + tokenizer.encode(text, add_special_tokens=False)
t0 = time.time()
torch.manual_seed(0)
with torch.device("cpu"):
model = ref_model.Transformer(model_args, tokenizer)
missing, unexpected = model.load_state_dict(state, strict=False)
assert not missing and not unexpected, (missing, unexpected)
model.eval()
rec = {"hidden": [], "indexer": [], "engram": []}
cur = {}
def block_hook(i):
def fn(mod, inp, out):
cur.setdefault("hidden", {})[i] = out[0].detach().clone()
return fn
def idx_hook(i):
def fn(mod, inp, out):
cur.setdefault("indexer", {})[i] = out.detach().clone()
return fn
for i, layer in enumerate(model.layers):
layer.register_forward_hook(block_hook(i))
if layer.attn.indexer is not None:
layer.attn.indexer.register_forward_hook(idx_hook(i))
if model.engram_hash is not None:
model.engram_hash.register_forward_hook(lambda m, i, o: cur.__setitem__("engram", o.detach().clone()))
forced = None
if args.force_tokens_dir:
ref = torch.load(os.path.join(args.force_tokens_dir, f"{name}.pt"), weights_only=False)
assert ref["prompt_ids"] == ids, "teacher-forcing golden was built from a different prompt"
forced = ref["tokens"]
tokens, fed, logits_all = [], [], []
x = torch.tensor([ids], dtype=torch.long)
pos = 0
for step in range(args.steps + 1):
cur.clear()
out_ids, logits, _ = model(x, pos)
pos += x.size(1)
nxt = int(logits[0].argmax())
assert nxt == int(out_ids[0])
tokens.append(nxt)
logits_all.append(logits[0].float().clone())
rec["hidden"].append(torch.stack([cur["hidden"][i][0] for i in range(len(model.layers))]).bfloat16())
rec["indexer"].append({k: v[0].int() for k, v in cur.get("indexer", {}).items()})
rec["engram"].append(cur["engram"][0] if "engram" in cur else None)
feed = forced[step] if forced is not None else nxt
fed.append(feed)
x = torch.tensor([[feed]], dtype=torch.long)
logits_all = torch.stack(logits_all)
dt = time.time() - t0
top2 = logits_all.topk(2, dim=-1)
margins = (top2.values[:, 0] - top2.values[:, 1]).tolist()
payload = {
"name": name,
"prompt_text": text,
"prompt_ids": ids,
"tokens": tokens, # argmax of each step: tokens[0] from the prefill, tokens[1:] from the decode steps
"fed_tokens": fed[:-1], # decode step i+1 was fed fed_tokens[i] (== tokens[i] unless teacher-forced)
"teacher_forced_from": args.force_tokens_dir,
"logits": logits_all, # [steps+1, vocab] fp32; row i predicts tokens[i]
"start_pos": [0] + [len(ids) + i for i in range(args.steps)],
"hidden": rec["hidden"], # per forward call: [n_layers, seqlen, hc_mult, dim] bf16 block outputs
"indexer_topk": rec["indexer"], # per forward call: {layer: [seqlen, topk] int32 (-1 = none, +window offset)}
"engram_hashes": rec["engram"], # per forward call: [seqlen, n_engram_layers, n_hash_cols] int64
"model_args": dataclasses.asdict(model_args),
"hf_config": hf_cfg,
}
pt = os.path.join(args.out, f"{name}.pt")
torch.save(payload, pt)
top5 = logits_all.topk(5, dim=-1)
summary = {
"name": name,
"prompt_text": text,
"prompt_ids": ids,
"n_prompt_tokens": len(ids),
"tokens": tokens,
"decoded": tokenizer.decode(tokens),
"top1_top2_margin": [round(m, 5) for m in margins],
"top5_ids": top5.indices.tolist(),
"top5_logits": [[round(v, 5) for v in row] for row in top5.values.tolist()],
"logits_l2": [round(v, 4) for v in logits_all.norm(dim=-1).tolist()],
"logits_mean": [round(v, 6) for v in logits_all.mean(dim=-1).tolist()],
"full_golden": pt,
"full_golden_sha256": hashlib.sha256(open(pt, "rb").read()).hexdigest(),
"seconds": round(dt, 1),
}
with open(os.path.join(args.summary_dir, f"{name}.json"), "w") as fh:
json.dump(summary, fh, indent=1)
print(f"[golden] {name}: {len(ids)} prompt tokens -> {tokens} ({tokenizer.decode(tokens)!r}); "
f"min top1-top2 margin {min(margins):.4f}; {dt:.1f}s; saved {pt}")
if __name__ == "__main__":
main()