Files
xserv/tools/gptoss_dequant.py
Gahow Wang c7d0750c32 moe(wip): gpt-oss-20b groundwork — config fields, arch doc, MXFP4 tools
Phase 19 start. config.rs: explicit head_dim (gpt-oss=64) + MoE fields
(num_local_experts, num_experts_per_tok, swiglu_limit, sliding_window,
layer_types) with accessors; Qwen3/GPT-2 paths unchanged (fall back to
hidden/num_heads when head_dim absent).

docs/19-moe-gpt-oss.md: architecture + exact HF reference math (router
softmax-after-topk, interleaved clamped (up+1)*glu experts, attention
sinks, alternating sliding window, rotate_half RoPE theta=150000,
head_dim 64), verified tensor layout, MXFP4 dequant plan.
docs/MOE_PROGRESS.md: resume/handoff snapshot.

tools/mxfp4_probe.py: inspect safetensors + validate MXFP4 decode (done).
tools/gptoss_dequant.py: MXFP4 experts -> plain BF16 safetensors dir so
the existing loader reads it (no MXFP4 in Rust for the first pass).

Verified: llama.cpp (dash5, LLM_ARCH_OPENAI_MOE) runs the gpt-oss-20b
MXFP4 GGUF correctly (17*24 -> 408) = the correctness oracle. MXFP4 decode
validated in numpy. Model + GGUF staged on dash5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 21:01:53 +08:00

80 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Dequantize gpt-oss-20b MXFP4 expert weights -> a plain BF16 safetensors dir.
Only the expert MLPs are MXFP4 (`*_blocks` uint8 packed 4-bit + `*_scales` uint8
E8M0, block=32); everything else is already BF16. We decode experts to BF16 and
re-emit a standard HF-format dir so xserv's normal safetensors loader reads it
(keeps the first MoE pass free of any MXFP4 code in Rust).
Fused expert outputs (per layer i), matching HF `GptOssExperts` param shapes:
model.layers.{i}.mlp.experts.gate_up_proj [E, hidden, 2*inter] bf16
model.layers.{i}.mlp.experts.down_proj [E, inter, hidden] bf16
(*_bias tensors pass through unchanged)
NOTE on transpose: the MXFP4 `_blocks` decode to [E, OUT, IN] (out-major, the
contraction dim = nblk*32 last). HF's nn.Parameter for these is [E, IN, OUT]
(it does `x @ gate_up_proj`). We emit [E, IN, OUT] (transpose last two dims) so
the names/shapes match HF exactly and xserv can treat them uniformly.
Run on the GPU host (torch + the model + disk):
python3 tools/gptoss_dequant.py /opt/wjh/models/gpt-oss-20b /opt/wjh/models/gpt-oss-20b-bf16
"""
import sys, os, json, glob
import torch
from safetensors import safe_open
from safetensors.torch import save_file
# FP4 E2M1 code -> value (OCP MX). 16 entries.
FP4 = 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 dequant(blocks: torch.Tensor, scales: torch.Tensor) -> torch.Tensor:
"""blocks uint8 [..., nblk, 16], scales uint8 [..., nblk] -> bf16 [..., nblk*32]."""
blocks = blocks.to(torch.int64)
lo = blocks & 0xF
hi = (blocks >> 4) & 0xF
codes = torch.stack([lo, hi], dim=-1).reshape(*blocks.shape[:-1], blocks.shape[-1] * 2)
vals = FP4[codes] # [..., nblk, 32] f32
scale = torch.exp2(scales.to(torch.float32) - 127.0) # [..., nblk]
out = vals * scale[..., None]
out = out.reshape(*out.shape[:-2], out.shape[-2] * out.shape[-1])
return out.to(torch.bfloat16)
def main():
src, dst = sys.argv[1], sys.argv[2]
os.makedirs(dst, exist_ok=True)
wm = json.load(open(os.path.join(src, "model.safetensors.index.json")))["weight_map"]
shards = {}
for name, shard in wm.items():
shards.setdefault(shard, []).append(name)
out = {}
for shard in sorted(shards):
h = safe_open(os.path.join(src, shard), framework="pt")
keys = set(h.keys())
for name in shards[shard]:
if name.endswith("_scales"):
continue
if name.endswith("_blocks"):
base = name[:-len("_blocks")] # ...gate_up_proj / ...down_proj
sc = base + "_scales"
sc_h = h if sc in keys else safe_open(os.path.join(src, wm[sc]), framework="pt")
deq = dequant(h.get_tensor(name), sc_h.get_tensor(sc)) # [E, OUT, IN]
out[base] = deq.transpose(1, 2).contiguous() # [E, IN, OUT] (HF param layout)
print("dequant", base, tuple(out[base].shape), flush=True)
else:
out[name] = h.get_tensor(name) # already bf16/other; pass through
save_file(out, os.path.join(dst, "model.safetensors"), metadata={"format": "pt"})
for f in glob.glob(os.path.join(src, "*.json")) + glob.glob(os.path.join(src, "*.jinja")):
b = os.path.basename(f)
if b == "model.safetensors.index.json":
continue
open(os.path.join(dst, b), "wb").write(open(f, "rb").read())
print("DEQUANT_DONE ->", dst, flush=True)
if __name__ == "__main__":
main()