DeepInfer/scripts/h20_expert_weight_check.py
loopyt 28fb291db1 style(sm90): satisfy ruff and add the missing type annotations
No behaviour change. `CONTRIBUTING.md` requires `ruff check --no-cache` and
`ruff format --check --no-cache` to pass, and the SM90 branch predates that requirement:
72 findings, all in the files added here.

Most of it is mechanical (`ruff format`), plus:
- annotations for the helper signatures (`torch.Tensor` / `int` / `Generator` / return types);
  hooks are typed as `Callable[[nn.Module, tuple, object], None]` rather than left bare;
- two `if`/`else` assignments become ternaries (SIM108), and the index read in
  `h20_real_requant_check.py` uses a context manager (SIM115).

Also corrects the `sparse_mla_fwd` docstring, which still described the old
`0 <= idx <= q_position` masking contract. Passing `is_causal=True` there is what produced the
all-NaN layer 0: indices address the assembled `[swa cache | current swa | compressed]` buffer,
so the row number is not a valid causal bound and the engine must pass False.
2026-10-21 17:09:08 +08:00

118 lines
5.2 KiB
Python

"""Compare our FP4->FP8 expert requantization against DeepSeek's own reference conversion.
`inference/convert.py::cast_e2m1fn_to_e4m3fn` is the authoritative implementation: it is what
produced the TP8 checkpoint that the reference model actually runs on. Two things are worth
checking there, and both matter because a wrong answer scrambles every routed expert while
leaving the (already-FP8) shared expert intact -- exactly the asymmetry the layer comparison
showed.
1. Equality of the *decoded* weights. Both paths should represent the same real values; any
difference is accuracy we are throwing away.
2. The scale grid. The reference keeps a per-32 e8m0 scale (relative offsets folded into the FP8
values), while the SM90 path re-quantizes on a per-128 fp32 grid. That is a legitimate design
difference only if the magnitude error stays small.
python scripts/h20_expert_weight_check.py --layer 0 --expert 370
"""
import argparse
import json
from pathlib import Path
import torch
from safetensors import safe_open
from deepinfer.h20_ops.quant import requant_expert_fp4, ue8m0_to_fp32
# Copied verbatim from inference/convert.py so the comparison uses the reference algorithm
# rather than a paraphrase of it.
FP4_TABLE = torch.tensor(
[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0],
dtype=torch.float32,
)
def cast_e2m1fn_to_e4m3fn(x: torch.Tensor, scale: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""DeepSeek's lossless FP4 -> FP8 cast (inference/convert.py)."""
assert x.dtype == torch.int8 and x.ndim == 2
out_dim, in_dim = x.size()
in_dim *= 2
fp8_block_size = 32
fp4_block_size = 32
assert in_dim % fp8_block_size == 0 and out_dim % fp8_block_size == 0
assert scale.size(0) == out_dim and scale.size(1) == in_dim // fp4_block_size
x = x.view(torch.uint8)
low = x & 0x0F
high = (x >> 4) & 0x0F
x = torch.stack([FP4_TABLE[low.long()], FP4_TABLE[high.long()]], dim=-1).flatten(2)
MAX_OFFSET_BITS = 6
b_out = out_dim // fp8_block_size
b_in = in_dim // fp8_block_size
x = x.view(b_out, fp8_block_size, b_in, fp8_block_size).transpose(1, 2)
scale = scale.float().view(b_out, fp8_block_size, b_in, -1).transpose(1, 2).flatten(2)
scale_max_offset_bits = scale.amax(dim=-1, keepdim=True) / (2**MAX_OFFSET_BITS)
offset = scale / scale_max_offset_bits
offset = offset.unflatten(-1, (fp8_block_size, -1)).repeat_interleave(fp4_block_size, dim=-1)
x = (x * offset).transpose(1, 2).reshape(out_dim, in_dim)
return x.to(torch.float8_e4m3fn), scale_max_offset_bits.squeeze(-1).to(torch.float8_e8m0fnu)
def read_tensor(hf_dir: Path, index: dict, name: str) -> torch.Tensor:
shard = index["weight_map"][name]
with safe_open(str(hf_dir / shard), framework="pt", device="cpu") as f:
return f.get_tensor(name)
def dequant_ref(fp8: torch.Tensor, sf_e8m0: torch.Tensor, block: int = 32) -> torch.Tensor:
"""Reference layout: (out, in) FP8, scale (out/32, in/32) e8m0 on a 32x32 grid."""
s = ue8m0_to_fp32(sf_e8m0)
s = s.repeat_interleave(block, 0).repeat_interleave(block, 1)[: fp8.shape[0], : fp8.shape[1]]
return fp8.float() * s
def dequant_ours(fp8: torch.Tensor, sf_fp32: torch.Tensor, block: int = 128) -> torch.Tensor:
"""SM90 layout: (out, in) FP8, scales (in/block, out/block) fp32, contraction-major."""
s = sf_fp32.t().repeat_interleave(block, 0).repeat_interleave(block, 1)[: fp8.shape[0], : fp8.shape[1]]
return fp8.float() * s
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--hf-dir", default="/data/models/DeepSeek-V4.1-Flash-1017")
parser.add_argument("--layer", type=int, default=0)
parser.add_argument("--expert", type=int, default=370)
args = parser.parse_args()
hf_dir = Path(args.hf_dir)
index = json.loads((hf_dir / "model.safetensors.index.json").read_text())
prefix = f"layers.{args.layer}.ffn.experts.{args.expert}."
for proj in ("w1", "w3", "w2"):
packed = read_tensor(hf_dir, index, prefix + proj + ".weight")
scale = read_tensor(hf_dir, index, prefix + proj + ".scale")
print(f"\n{proj}: packed {tuple(packed.shape)} {packed.dtype}, scale {tuple(scale.shape)} {scale.dtype}")
ref_fp8, ref_sf = cast_e2m1fn_to_e4m3fn(packed, scale)
ours_fp8, ours_sf = requant_expert_fp4(packed, scale, out_block=128)
# `requant_expert_fp4` returns (n_out, k) FP8 and a contraction-major scale; the reference
# returns (out, in) too, so the value grids line up directly.
ref_vals = dequant_ref(ref_fp8, ref_sf)
ours_vals = dequant_ours(ours_fp8, ours_sf)
assert ref_vals.shape == ours_vals.shape, f"{ref_vals.shape} vs {ours_vals.shape}"
num = (ours_vals - ref_vals).norm().item()
den = ref_vals.norm().item()
cos = torch.nn.functional.cosine_similarity(ours_vals.reshape(-1), ref_vals.reshape(-1), dim=0).item()
print(f" ref scale: e8m0 {tuple(ref_sf.shape)} ours: fp32 {tuple(ours_sf.shape)}")
print(f" |ref|={den:.4f} |ours|={ours_vals.norm().item():.4f} ratio={ours_vals.norm().item() / den:.5f}")
print(f" rel_err={num / den:.5f} cos={cos:.7f}")
print("\nEXPERT_WEIGHT_CHECK_DONE")
if __name__ == "__main__":
main()