DeepInfer/examples/neuron/reference_dspark.py
whn09 1dce2c4995 Neuron bench: vllm-neuron#71's sonnet prompts, prompt/output dumps; DSpark draft ground truth
- bench_neuron.py --dataset vllm-sonnet: vLLM's SonnetDataset as #71's bench
  builds it for the completions backend ("Pick as many lines..." + random poem
  lines, chat-formatted; --chat_tokenizer for a template the checkpoint lacks).
  --dataset sonnet: a contiguous text window. --dump: prompt/output token ids.
- reference_dspark.py: the official inference/model.py forward_spec on the host
  CPU (tiny checkpoint, teacher-forced on reference_golden.py tokens): each
  decode step's 5 drafts and their logits.
- run_deepinfer_tiny.py RUN_TINY_DRAFTS=1 prints every draft block.

DeepInfer's drafts match the reference on host eager, on device and with
attention TP: every divergence is a near tie (top-1/top-2 gap 0.001-0.055).
With #71's prompt format the acceptance is #71's: 2.8 tokens a step at 512
(first position 65-79%); a raw text window gave 1.4 (continuations not in the
context), random token ids 3.85 (repetitive outputs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:16:54 +08:00

255 lines
14 KiB
Python

"""DSpark draft ground truth: the OFFICIAL DeepSeek-V4.1 reference (inference/model.py forward_spec) on the host CPU.
Teacher-forced on reference_golden.py tokens; records each decode step\'s 5 drafts. Shares the loading code of
reference_golden.py (copied below the docstring).
Original reference_golden.py docstring follows.
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("--golden-dir", default="~/di/golden", help="reference_golden.py outputs (tokens to feed)")
ap.add_argument("--out", default="~/di/golden_dspark")
ap.add_argument("--prompts", default="short,long")
ap.add_argument("--threads", type=int, default=16)
ap.add_argument("--max-seq-len", type=int, default=2048)
args = ap.parse_args()
for k in ("hf_ckpt", "ref_dir", "converted", "golden_dir", "out"):
setattr(args, k, os.path.abspath(os.path.expanduser(getattr(args, k))))
os.makedirs(args.out, 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)"
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
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)
model_args = ref_model.ModelArgs(**margs)
tokenizer = AutoTokenizer.from_pretrained(args.converted)
state = load_file(ckpt_file)
dropped = sorted(k for k in state if k.startswith(("vision.", "aligner.", "image_")) or k.endswith(".bias_vl"))
state = {k: v for k, v in state.items() if k not in set(dropped)}
torch.set_default_dtype(torch.bfloat16)
for name in args.prompts.split(","):
gold = torch.load(os.path.join(args.golden_dir, f"{name}.pt"), weights_only=False)
ids, gtok = list(gold["prompt_ids"]), list(gold["tokens"])
torch.manual_seed(0)
with torch.device("cpu"):
model = ref_model.Transformer(model_args, tokenizer)
missing, unexpected = model.load_state_dict(state, strict=False)
# the MTP stages share the backbone's embed / head (assigned in Transformer.__init__)
missing = [k for k in missing if not (k.startswith("mtp.") and k.endswith((".embed.weight", ".head.weight")))]
assert not missing and not unexpected, (missing, unexpected)
model.eval()
x = torch.tensor([ids], dtype=torch.long)
out_ids, logits, main_hidden = model(x, 0)
model.forward_spec(out_ids, main_hidden, 0) # seeds the DSpark window KV
pos = len(ids)
drafts, draft_logits = [], []
# decode step k is fed golden token k (teacher forcing); its drafts follow the token it predicts
for k in range(len(gtok) - 1):
x = torch.tensor([[gtok[k]]], dtype=torch.long)
out_ids, logits, main_hidden = model(x, pos)
res = model.forward_spec(out_ids, main_hidden, pos)
pos += 1
spec_ids, spec_logits, conf = res
drafts.append(spec_ids[0, 1:].tolist())
draft_logits.append(spec_logits[0].float().clone())
print(f"[dspark] {name} step {k}: fed {gtok[k]}, main argmax {int(out_ids[0])} (golden {gtok[k + 1]}), "
f"drafts {drafts[-1]}", flush=True)
torch.save({"name": name, "prompt_ids": ids, "fed": gtok[:-1], "drafts": drafts, "draft_logits": draft_logits},
os.path.join(args.out, f"{name}.pt"))
if __name__ == "__main__":
main()