DeepInfer/scripts/compare_layers.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

113 lines
4.7 KiB
Python

"""Diff per-layer hidden states between DeepInfer and the reference implementation.
Both dumps keep only the final position of the prompt, reduced over the `hc_mult` copies, so
the comparison is one vector per layer. Reports cosine similarity, relative L2 error and the
ratio of norms, which together distinguish "slightly off" from "different direction".
python scripts/compare_layers.py --ref ref_layers.npz --mine deepinfer_layers.npz
"""
import argparse
import numpy as np
def rel_err(a: np.ndarray, b: np.ndarray) -> float:
return float(np.linalg.norm(a - b) / (np.linalg.norm(b) + 1e-12))
def cosine(a: np.ndarray, b: np.ndarray) -> float:
na, nb = np.linalg.norm(a), np.linalg.norm(b)
if na == 0 or nb == 0:
return float("nan")
return float(np.dot(a, b) / (na * nb))
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--ref", required=True)
parser.add_argument("--mine", required=True)
parser.add_argument("--tol", type=float, default=1e-2, help="first layer above this rel_err is the culprit")
args = parser.parse_args()
ref = np.load(args.ref)
mine = np.load(args.mine)
layers = sorted(int(k[5:]) for k in ref.files if k.startswith("layer") and k[5:].isdigit())
mine_layers = {int(k[5:]) for k in mine.files if k.startswith("layer") and k[5:].isdigit()}
missing = [i for i in layers if i not in mine_layers]
if missing:
print(f"WARNING: no dump from DeepInfer for layers {missing}")
# Routing first: expert ids are discrete, so a set mismatch is a definitive verdict and the
# weights/branch comparisons below only make sense once routing is known to agree.
if args.ref and args.mine:
for layer in range(4):
key = f"layer{layer}_topk_idx"
if key not in ref.files or key not in mine.files:
continue
r_idx, m_idx = ref[key].astype(np.int64), mine[key].astype(np.int64)
r_set = set(r_idx.reshape(-1).tolist())
m_set = set(m_idx.reshape(-1).tolist())
inter = r_set & m_set
print(
f"{key:>18} ref={sorted(r_set)}\n{'':>18} mine={sorted(m_set)}"
f"\n{'':>18} overlap={len(inter)}/{len(r_set)}"
f" {'IDENTICAL' if r_idx.shape == m_idx.shape and (r_idx == m_idx).all() else 'DIFFERS'}"
)
print()
for key in ("embed", "layer0_attn", "layer1_attn"):
if key in ref.files and key in mine.files:
r, m = ref[key].astype(np.float64), mine[key].astype(np.float64)
if r.shape == m.shape:
print(
f"{key:>12} cos={cosine(m, r):9.5f} rel_err={rel_err(m, r):9.2e}"
f" |mine|/|ref|={np.linalg.norm(m) / (np.linalg.norm(r) + 1e-12):8.4f}"
)
print()
print(f"{'layer':>5} {'cos':>9} {'rel_err':>9} {'|mine|/|ref|':>12}")
first_bad = None
for i in layers:
if i not in mine_layers:
continue
r, m = ref[f"layer{i}"].astype(np.float64), mine[f"layer{i}"].astype(np.float64)
if r.shape != m.shape:
print(f"{i:>5} shape mismatch ref={r.shape} mine={m.shape}")
continue
err, cos = rel_err(m, r), cosine(m, r)
ratio = float(np.linalg.norm(m) / (np.linalg.norm(r) + 1e-12))
flag = ""
if first_bad is None and err > args.tol:
first_bad = i
flag = " <-- first > tol"
print(f"{i:>5} {cos:9.5f} {err:9.2e} {ratio:12.4f}{flag}")
ref_tok = int(ref["next_token"][0]) if "next_token" in ref.files else None
print(f"\nreference next token: {ref_tok}")
if "final_row_logits" in ref.files:
rl = ref["final_row_logits"].astype(np.float64)
top = np.argsort(-rl)[:5]
print("reference top-5 tokens:", [int(x) for x in top], "logits:", [round(float(rl[x]), 3) for x in top])
# DeepInfer's logits are not dumped; derive its choice from the harness output instead.
print()
for key in sorted(k for k in ref.files if k.endswith(("_shared_out", "_combined", "_moe_out"))):
if key in mine.files and ref[key].shape == mine[key].shape:
r, m = ref[key].astype(np.float64), mine[key].astype(np.float64)
print(
f"{key:>18} cos={cosine(m, r):9.5f} rel_err={rel_err(m, r):9.2e}"
f" |mine|/|ref|={np.linalg.norm(m) / (np.linalg.norm(r) + 1e-12):8.4f}"
)
print()
if first_bad is None:
print(f"RESULT: every dumped layer is within rel_err <= {args.tol}; divergence is downstream")
else:
print(f"RESULT: first layer above rel_err {args.tol} is layer {first_bad}")
if __name__ == "__main__":
main()