DeepInfer/scripts/h20_expert_weight_check.py
loopyt 7c6fdc7c6e fix(sm90): expand expert scales on the contraction axis only
`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.
2026-10-21 16:50:31 +08:00

120 lines
5.3 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()