mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 08:46:21 +08:00
`requant_expert_fp4` expanded the UE8M0 scale with `_expand_blocks`, which repeats *both*
axes. That is right for dense weights, whose scales are `(n_out/32, k/32)`, but expert scales
are already per-output-channel: `(n_out, k/32)`. Expanding both axes therefore treated
`(2304, 160)` as `(72, 5)` and handed every one of the 2304 output channels the scales of the
first 72 rows, scrambling each expert's weights.
This is what the layer comparison was pointing at: `shared_expert` matched the reference
(cos 0.9987) because it goes through `requant_dense_fp8`, which uses `_expand_blocks`
correctly, while `combined` -- routed experts plus shared -- was off by 3.6e-1.
Verified against the authoritative implementation, `inference/convert.py`'s
`cast_e2m1fn_to_e4m3fn`, on layer 0 / expert 370:
w1 cos 0.8936 -> 1.0000 rel_err 5.35e-1 -> 2.84e-2
w3 cos 0.8960 -> 0.9997 rel_err 5.26e-1 -> 2.69e-2
w2 cos 0.8964 -> 0.9996 rel_err 5.20e-1 -> 2.65e-2
The residual ~2.7% is the per-32 -> per-128 requantization loss, not a defect; the reference
is lossless because it folds the relative per-32 offset into the FP8 values.
End to end on the 11-token chat prompt:
before: [17607, 41175, 389, 666] "中国的首都是**"
after: [17607, 2793, 3139, 666, 6127, 666, 320, 1] "中国的首都是**北京**。"
ref: [17607, 2793, 3139, 6127, 320] "中国的首都是北京。"
The answer is now correct and the first three tokens match the reference exactly. The remaining
difference is markdown emphasis (`**`), so some numerical drift still moves the top-1 on the
occasional position.
Also adds two checks that would have caught this immediately:
`scripts/h20_expert_weight_check.py` diffs our requantization against DeepSeek's own converter,
and `scripts/h20_quant_check.py` pins the E2M1 table, nibble order and UE8M0 bias. The nibble
order and table turn out to match the reference; only the scale broadcast was wrong.
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
"""Dump the reference model's per-layer hidden state for one prompt.
|
|
|
|
Counterpart to DeepInfer's `DEEPINFER_DUMP_LAYERS`, so the two can be diffed layer by layer.
|
|
The reference residual stream is `hc_mult` parallel copies, so each layer is reduced with
|
|
`mean(dim=1)` -- the same convention `debug_dump.record` uses -- and only the final position
|
|
is kept, because that is the only row generation depends on.
|
|
|
|
torchrun --nproc-per-node 8 ref_layer_dump.py --ids 0,128803,... --out /data/deepinfer-h20/ref
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import torch
|
|
import torch.distributed as dist
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from safetensors.torch import load_model # noqa: E402
|
|
from transformers import AutoTokenizer # noqa: E402
|
|
|
|
from model import ModelArgs, Transformer # noqa: E402
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--ckpt-path", required=True)
|
|
parser.add_argument("--config", required=True)
|
|
parser.add_argument("--ids", required=True)
|
|
parser.add_argument("--out", default="/data/deepinfer-h20/ref")
|
|
parser.add_argument("--moe_layers", type=int, default=2, help="dump MoE internals for this many layers")
|
|
args = parser.parse_args()
|
|
|
|
ids = [int(x) for x in args.ids.replace(" ", "").split(",") if x]
|
|
rank = int(os.getenv("RANK", "0"))
|
|
local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
torch.cuda.set_device(local_rank)
|
|
if world_size > 1:
|
|
dist.init_process_group("nccl")
|
|
|
|
torch.set_default_dtype(torch.bfloat16)
|
|
torch.set_num_threads(8)
|
|
with open(args.config) as f:
|
|
model_args = ModelArgs(**json.load(f))
|
|
model_args.temperature = 0.0
|
|
model_args.max_batch_size = 1
|
|
model_args.max_seq_len = 4096
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained(args.ckpt_path)
|
|
with torch.device("cuda"):
|
|
model = Transformer(model_args, tokenizer)
|
|
load_model(model, os.path.join(args.ckpt_path, f"model{rank}-mp{world_size}.safetensors"))
|
|
torch.set_default_device("cuda")
|
|
|
|
captured: dict[str, np.ndarray] = {}
|
|
|
|
def make_hook(layer_id: int):
|
|
def hook(_module, _inputs, output):
|
|
# Block.forward returns (x, pre_mix); x is [b, s, hc_mult, dim].
|
|
x = output[0] if isinstance(output, tuple) else output
|
|
captured[f"layer{layer_id}"] = x.mean(dim=2)[0, -1].float().cpu().numpy()
|
|
|
|
return hook
|
|
|
|
def make_attn_hook(layer_id: int):
|
|
def hook(_module, _inputs, output):
|
|
# The attention sub-layer output is already collapsed: [b, s, dim].
|
|
x = output[0] if isinstance(output, tuple) else output
|
|
captured[f"layer{layer_id}_attn"] = x[0, -1].float().cpu().numpy()
|
|
|
|
return hook
|
|
|
|
def embed_hook(_module, _inputs, output):
|
|
captured["embed"] = output[0, -1].float().cpu().numpy()
|
|
|
|
def make_gate_hook(layer_id: int):
|
|
def hook(_module, _inputs, output):
|
|
# Gate.forward returns (weights [n, topk], indices [n, topk]).
|
|
weights, indices = output
|
|
captured[f"layer{layer_id}_topk_idx"] = indices[-1].to(torch.int32).cpu().numpy()
|
|
captured[f"layer{layer_id}_topk_w"] = weights[-1].float().cpu().numpy()
|
|
|
|
return hook
|
|
|
|
def make_shared_hook(layer_id: int):
|
|
def hook(_module, _inputs, output):
|
|
captured[f"layer{layer_id}_shared_out"] = output.view(-1, output.shape[-1])[-1].float().cpu().numpy()
|
|
|
|
return hook
|
|
|
|
def make_moe_hook(layer_id: int):
|
|
def hook(_module, _inputs, output):
|
|
# The MoE output already includes the shared expert, matching our `_combined`.
|
|
captured[f"layer{layer_id}_combined"] = output.view(-1, output.shape[-1])[-1].float().cpu().numpy()
|
|
|
|
return hook
|
|
|
|
model.embed.register_forward_hook(embed_hook)
|
|
for layer_id, layer in enumerate(model.layers):
|
|
layer.register_forward_hook(make_hook(layer_id))
|
|
layer.attn.register_forward_hook(make_attn_hook(layer_id))
|
|
if layer_id < args.moe_layers:
|
|
layer.ffn.gate.register_forward_hook(make_gate_hook(layer_id))
|
|
layer.ffn.shared_experts.register_forward_hook(make_shared_hook(layer_id))
|
|
layer.ffn.register_forward_hook(make_moe_hook(layer_id))
|
|
|
|
tokens = torch.tensor([ids], dtype=torch.long)
|
|
with torch.inference_mode():
|
|
output_ids, logits, _ = model.forward(tokens, 0)
|
|
|
|
captured["final_row_logits"] = logits[0, -1].float().cpu().numpy()
|
|
captured["next_token"] = np.array([int(output_ids[-1])], dtype=np.int32)
|
|
|
|
if rank == 0:
|
|
out = Path(args.out)
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
path = out / "ref_layers.npz"
|
|
np.savez(path, **captured)
|
|
print(f"wrote {len(captured)} tensors to {path}", flush=True)
|
|
print(json.dumps({"next_token": int(output_ids[-1]), "layers": len(model.layers)}), flush=True)
|
|
|
|
if world_size > 1:
|
|
dist.destroy_process_group()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|