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