[New Model] DeepSeek-V4-Flash: kt-kernel MXFP4 MoE + sglang hybrid inference (#1970)
Some checks are pending
Book-CI / test (push) Waiting to run
Book-CI / test-1 (push) Waiting to run
Book-CI / test-2 (push) Waiting to run
Deploy / deploy (macos-latest) (push) Waiting to run
Deploy / deploy (ubuntu-latest) (push) Waiting to run
Deploy / deploy (windows-latest) (push) Waiting to run
Release sglang-kt to PyPI / Build sglang-kt wheel (push) Waiting to run
Release sglang-kt to PyPI / Publish sglang-kt to PyPI (push) Blocked by required conditions

* [feat](kt-kernel): add MXFP4 MoE operator with E2M1 weights × BF16 activations

Implements AMX_FP4_MOE_TP based on the RAWINT4 (k2-moe) CRTP pattern.
FP4 E2M1 weights are nibble-packed and decoded via PSHUFB LUT, then
computed with BF16 activations using _mm512_dpbf16_ps. Supports weight-only
per-kgroup scaling (group_size=32) and tensor parallelism.

Includes a Python validation test covering uniform, alternating, ramp,
and random weight patterns.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [feat](kt-kernel): adapt MXFP4 MoE backend for DeepSeek-V4-Flash (#1950)

V4-Flash routed experts ship as native MXFP4 (E2M1 nibble + ue8m0 group
scale). Expose AMXFP4_KGroup_MOE through NativeMoEWrapper, add a loader
that handles V4's `layers.{L}.ffn.experts.{i}.{w1,w3,w2}.{weight,scale}`
naming and converts ue8m0 → bf16 via a lossless bit-cast, register the
model entry, and ship an end-to-end numerical validation script.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [perf](kt-kernel): MXFP4 MoE add mat-mat 4×4 tile, refine mat-vec reduce (#1957)

mat_mul_kgroup previously aliased to fp4_mat_vec_kgroup, leaving large
batches stuck on the per-token path. Implement fp4_mat_mat_kgroup as a
4×4 register tile (MB=NB=4, 16 zmm accumulators) so each PSHUFB decode
of four weight rows is reused across four tokens.

Refactor fp4_mat_vec_kgroup to accumulate four N-rows in parallel and
flush them with a new reduce4 helper, removing per-row reduce_add_ps
calls from the hot loop. Mark mxfp4_to_bf16_32 always_inline.

Add bench/bench_fp4_moe.py with --routing {balanced,concentrated} and
a backend registry so future kernels can be added without changing the
runner.

Dispatch thresholds, derived_init, GeneralMOEConfig handling,
load_weights, write_weights_to_buffer and the TP_MOE specialization are
unchanged.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(loader): avoid uint16 lshift in ue8m0->bf16 conversion

PyTorch CPU has no lshift kernel for UInt16, so the previous
`(scale_t.to(torch.uint16) << 7)` raised NotImplementedError when
loading any V4-Flash MXFP4 routed-expert scale tensor on the host.

Switch to int32 for the shift (kernel exists) and narrow to int16
afterwards. The shifted value max is 255<<7 = 32640, well within
int16 range, so the narrow is lossless. The .view(bfloat16) bit
pattern is identical (bf16 sign bit is always 0 for ue8m0 values).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(v4-flash): hybrid CPU/GPU recipe + bump kt-sglang submodule

Bumps third_party/sglang to kvcache-ai/sglang main (3cbd49c29) which now
contains DeepSeek V4 Flash model support + consumer-GPU (SM_120) portable
Triton/TileLang fallbacks (kt-sglang PR #38).

Adds doc/en/DeepSeek-V4-Flash.md tutorial: 8x RTX 5090 hybrid recipe with
the full launch command, OpenAI-compatible /generate + /v1/chat/completions
examples, and the kt chat CLI client.

---------

Co-authored-by: ouqingliang <1692110604@qq.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin F 2026-05-03 10:48:31 +08:00 committed by GitHub
parent fe06c4d355
commit 041bdfc636
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1902 additions and 2 deletions

View file

@ -17,6 +17,7 @@ KTransformers is a research project focused on efficient inference and fine-tuni
## 🔥 Updates
* **May 6, 2026**: KTransformers at [GOSIM Paris 2026](https://paris2026.gosim.org/zh/schedule/) — "Agentic AI on Edge" track. We'll present KT's inference performance on consumer hardware.
* **May 02, 2026**: DeepSeek-V4-Flash Support! ([Tutorial](./doc/en/DeepSeek-V4-Flash.md))
* **Apr 30, 2026**: KTransformers v0.6.1 refreshes kt-kernel inference and SFT docs with separate [Inference](./kt-kernel/README.md) and [SFT Quick Start](./doc/en/SFT/KTransformers-Fine-Tuning_Quick-Start.md) entry points.
* **Mar 26, 2026**: Support AVX2-only CPU backend for KT-Kernel inference. ([Tutorial](./doc/en/kt-kernel/AVX2-Tutorial.md))
* **Feb 13, 2026**: MiniMax-M2.5 Day0 Support! ([Tutorial](./doc/en/MiniMax-M2.5.md))

121
doc/en/DeepSeek-V4-Flash.md Normal file
View file

@ -0,0 +1,121 @@
# Running DeepSeek-V4-Flash with SGLang and KT-Kernel
This tutorial demonstrates how to run **DeepSeek-V4-Flash** model inference using SGLang integrated with KT-Kernel for CPU-GPU heterogeneous inference. The hybrid path splits MXFP4 routed experts between CPU (KT-Kernel `cpuinfer`) and GPU (sglang `kt-num-gpu-experts`), enabling deployment on consumer-grade hardware.
## Table of Contents
- [Running DeepSeek-V4-Flash with SGLang and KT-Kernel](#running-deepseek-v4-flash-with-sglang-and-kt-kernel)
- [Table of Contents](#table-of-contents)
- [Hardware Requirements](#hardware-requirements)
- [Prerequisites](#prerequisites)
- [Step 1: Download Model Weights](#step-1-download-model-weights)
- [Step 2: Launch SGLang Server](#step-2-launch-sglang-server)
- [Launch Command (8× RTX 5090 Example)](#launch-command-8-rtx-5090-example)
- [Step 3: Send Inference Requests](#step-3-send-inference-requests)
- [Decode](#decode)
- [Interactive Chat (kt chat)](#interactive-chat-kt-chat)
## Hardware Requirements
**Validated Configuration (this tutorial):**
- **GPU**: 8× NVIDIA RTX 5090 (32GB VRAM each, SM_120)
- **CPU**: x86 CPU with AVX512 support
- **RAM**: ≥256GB system memory
- **Storage**: ~340GB for model weights
**Supported GPU architectures** (auto-detected at startup; non-validated configurations should work but have not been benchmarked end-to-end):
| Arch | Compute Cap | MXFP4 MoE | NSA sparse MLA | Validated |
|------|------------|-----------|----------------|-----------|
| Hopper (H100 / H200) | SM_90 | triton_kernels | flash_mla wheel | — |
| Datacenter Blackwell (B100 / B200) | SM_100 | trtllm-fp4 | Triton fallback | — |
| Consumer Blackwell (RTX 5090) | SM_120 | triton_kernels | Triton fallback | ✓ |
| Ada Lovelace (RTX 4090 / L20 / L40) | SM_89 | triton_kernels | Triton fallback | — |
| Ampere (A100 / A6000) | SM_80 / SM_86 | triton_kernels | Triton fallback | — |
## Prerequisites
1. **KT-Kernel installed**:
```bash
git clone https://github.com/kvcache-ai/ktransformers.git
cd ktransformers
git submodule update --init --recursive
cd kt-kernel && ./install.sh
```
2. **SGLang installed** (kvcache-ai fork):
```bash
./install.sh # from ktransformers root
```
3. **CUDA 12.8+** and **flashinfer ≥ 0.6.9** (`flashinfer-python` and `flashinfer-cubin` must be the same version):
```bash
pip install --upgrade flashinfer-python flashinfer-cubin
```
This upgrade is required (even though `sglang-kt` pins `flashinfer_python==0.6.3`) because V4-Flash's MXFP4 MoE module imports `mxfp8_quantize`, `trtllm_fp4_block_scale_routed_moe`, etc., which only exist in flashinfer ≥ 0.6.9;
## Step 1: Download Model Weights
```bash
mkdir -p /path/to/models
huggingface-cli download deepseek-ai/DeepSeek-V4-Flash \
--local-dir /path/to/models/DeepSeek-V4-Flash
```
## Step 2: Launch SGLang Server
### Launch Command (8× RTX 5090 Example)
```bash
numactl --interleave=all python -m sglang.launch_server \
--host 127.0.0.1 \
--port 30000 \
--model /path/to/models/DeepSeek-V4-Flash \
--kt-weight-path /path/to/models/DeepSeek-V4-Flash \
--kt-method MXFP4 \
--kt-num-gpu-experts 144 \
--kt-cpuinfer 8 \
--kt-threadpool-count 2 \
--kt-gpu-prefill-token-threshold 4096 \
--kt-enable-dynamic-expert-update \
--tensor-parallel-size 8 \
--attention-backend flashinfer \
--mem-fraction-static 0.80 \
--chunked-prefill-size 2048 \
--max-running-requests 4 \
--max-total-tokens 32768 \
--watchdog-timeout 3000 \
--disable-shared-experts-fusion \
--cuda-graph-bs 1 2 4 \
--cuda-graph-max-bs 4 \
--trust-remote-code
```
It takes about 4-5 minutes to start the server (weight load + CUDA Graph capture).
See [KT-Kernel Parameters](https://github.com/kvcache-ai/ktransformers/tree/main/kt-kernel#kt-kernel-parameters) for detailed parameter tuning guidelines.
## Step 3: Send Inference Requests
### Decode
```bash
curl -s -X POST http://127.0.0.1:30000/generate \
-H "Content-Type: application/json" \
-d '{
"text": "Explain quantum computing in detail:",
"sampling_params": {"temperature": 0.0, "max_new_tokens": 256}
}'
```
### Interactive Chat (kt chat)
The `kt` CLI ships with an OpenAI-compatible chat client that talks to the SGLang server's `/v1/chat/completions` endpoint:
```bash
kt chat --host 127.0.0.1 --port 30000 --temperature 0.7 --max-tokens 2048
```

View file

@ -0,0 +1,313 @@
#!/usr/bin/env python
# coding=utf-8
"""Benchmark MXFP4 MoE kernel — V4-Flash shape, mat-vec / mat-mat 双路径覆盖。
Synthesizes V4-Flash-shaped MXFP4 weights (random nibbles + bf16 group scales),
runs the chosen backend over a list of batch sizes M, prints a throughput table.
Routing modes (决定是否触发 mat-mat 路径):
balanced token randperm(EXPERT_NUM)[:TOP_K]; 平均 per-expert m
M*top_k/expert_num. V4 真实路由分布. batch (M=1024)
平均触发 mat-mat (per-expert m 4).
concentrated 所有 M token 共用同一组 top_k expert; per-expert m = M.
M=4 立即触发 mat-mat 用来直观放大 mat-mat 性能优势.
Usage:
python bench/bench_fp4_moe.py --backend v1
python bench/bench_fp4_moe.py --backend v1 --routing concentrated
python bench/bench_fp4_moe.py --all --routing concentrated # 所有可用 backend 对比
`--backend` 是预留扩展点; 当前编译只绑定 v1 (AMXFP4_KGroup_MOE)要选 v2/v3
需要 ext_bindings 里加新绑定`--all` 会自动检测哪些 backend 可用
"""
import argparse
import json
import os
import platform
import subprocess
import sys
import time
import torch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "build"))
from kt_kernel import kt_kernel_ext # noqa: E402
# ----- V4-Flash MoE shape -----
HIDDEN = 4096
INTER = 2048
EXPERT_NUM = 256
TOP_K = 6
K_GROUP_SIZE = 32
# ----- bench knobs -----
# M=1024 时平均 per-expert m ≈ M*6/256 = 24, balanced 路由也能触发 mat-mat。
DEFAULT_M_LIST = [1, 4, 16, 64, 256, 1024]
WARMUP_ITER = 200
TEST_ITER = 2000
# ----- WorkerPool: 2 NUMA × 40 thread (matches bench_k2_moe_amx.py) -----
WORKER_NUMA = 2
WORKER_THREADS_PER_NUMA = 40
# ----- Backend registry: name → kt_kernel_ext.moe class (None = not bound) -----
BACKENDS = {
"v1": getattr(kt_kernel_ext.moe, "AMXFP4_KGroup_MOE", None),
# 预留扩展点; 加新 backend 时在 ext_bindings 绑定后这里加一行即可。
"v2": getattr(kt_kernel_ext.moe, "AMXFP4_KGroup_MOE_V2", None),
}
# OCP MXFP4 (E2M1) codepoints — same order as the kernel's LUT.
E2M1_VALUES = 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 quantize_mxfp4_tensor(weights: torch.Tensor, group_size: int):
"""[E, N, K] fp32/bf16 → packed nibbles uint8 [E, N, K/2] + bf16 scales [E, N, K/gs]."""
w = weights.to(torch.float32)
e, rows, cols = w.shape
assert cols % group_size == 0 and cols % 2 == 0
reshaped = w.view(e, rows, cols // group_size, group_size)
max_abs = reshaped.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
scales = (max_abs / 6.0).squeeze(-1)
normalized = reshaped / scales.unsqueeze(-1)
distances = torch.abs(normalized.unsqueeze(-1) - E2M1_VALUES.view(1, 1, 1, 1, 16))
nibbles = distances.argmin(dim=-1).to(torch.uint8).view(e, rows, cols // 2, 2)
lo = nibbles[..., 0]
hi = nibbles[..., 1]
packed = ((hi << 4) | lo).contiguous() # uint8 [E, N, K/2]
scales = scales.to(torch.bfloat16).contiguous() # bf16 [E, N, K/gs]
return packed, scales
def build_synth_weights():
torch.manual_seed(0)
gate = torch.randn((EXPERT_NUM, INTER, HIDDEN), dtype=torch.float32) / 100
up = torch.randn((EXPERT_NUM, INTER, HIDDEN), dtype=torch.float32) / 100
down = torch.randn((EXPERT_NUM, HIDDEN, INTER), dtype=torch.float32) / 100
gw, gs = quantize_mxfp4_tensor(gate, K_GROUP_SIZE)
uw, us = quantize_mxfp4_tensor(up, K_GROUP_SIZE)
dw, ds = quantize_mxfp4_tensor(down, K_GROUP_SIZE)
return {
"gate_w": gw, "up_w": uw, "down_w": dw,
"gate_s": gs, "up_s": us, "down_s": ds,
}
def build_moe(backend: str, weights, cpu_infer):
cls = BACKENDS.get(backend)
if cls is None:
raise RuntimeError(
f"backend={backend} not bound in this build. Available: "
f"{[k for k, v in BACKENDS.items() if v is not None]}"
)
cfg = kt_kernel_ext.moe.MOEConfig(EXPERT_NUM, TOP_K, HIDDEN, INTER, 0)
cfg.max_len = max(DEFAULT_M_LIST)
cfg.pool = cpu_infer.backend_
cfg.quant_config.bits = 4
cfg.quant_config.group_size = K_GROUP_SIZE
cfg.quant_config.zero_point = False
cfg.gate_projs = [[t.data_ptr() for t in weights["gate_w"]]]
cfg.up_projs = [[t.data_ptr() for t in weights["up_w"]]]
cfg.down_projs = [[t.data_ptr() for t in weights["down_w"]]]
cfg.gate_scales = [[t.data_ptr() for t in weights["gate_s"]]]
cfg.up_scales = [[t.data_ptr() for t in weights["up_s"]]]
cfg.down_scales = [[t.data_ptr() for t in weights["down_s"]]]
moe = cls(cfg)
p2l = torch.arange(EXPERT_NUM, dtype=torch.int64).contiguous()
cpu_infer.submit(moe.load_weights_task(p2l.data_ptr()))
cpu_infer.sync()
return moe
def make_expert_ids(M: int, routing: str) -> torch.Tensor:
"""[M, TOP_K] int64 (kernel forward_binding casts to const int64_t*)."""
if routing == "concentrated":
# 所有 M token 共用同组 top_k expert → per-expert m = M
hot = torch.randperm(EXPERT_NUM)[:TOP_K]
return hot.unsqueeze(0).expand(M, TOP_K).contiguous().to(torch.int64)
# balanced: 每 token 独立 randperm
return torch.stack(
[torch.randperm(EXPERT_NUM)[:TOP_K] for _ in range(M)]
).to(torch.int64).contiguous()
def bench_one_m(moe, cpu_infer, M: int, routing: str):
bsz = torch.tensor([M], dtype=torch.int32)
expert_ids = make_expert_ids(M, routing)
routing_w = torch.rand((M, TOP_K), dtype=torch.float32).contiguous()
x = (torch.randn((M, HIDDEN), dtype=torch.bfloat16) / 100).contiguous()
y = torch.empty((M, HIDDEN), dtype=torch.bfloat16).contiguous()
for _ in range(WARMUP_ITER):
cpu_infer.submit(moe.forward_task(
bsz.data_ptr(), TOP_K, expert_ids.data_ptr(),
routing_w.data_ptr(), x.data_ptr(), y.data_ptr(), False))
cpu_infer.sync()
start = time.perf_counter()
for _ in range(TEST_ITER):
cpu_infer.submit(moe.forward_task(
bsz.data_ptr(), TOP_K, expert_ids.data_ptr(),
routing_w.data_ptr(), x.data_ptr(), y.data_ptr(), False))
cpu_infer.sync()
total = time.perf_counter() - start
per_iter_us = total / TEST_ITER * 1e6
tok_per_s = M * TEST_ITER / total
unique_e = int(torch.unique(expert_ids).numel())
avg_m_per_expert = float(M * TOP_K) / max(unique_e, 1)
return {
"M": M, "iters": TEST_ITER, "total_s": total,
"per_iter_us": per_iter_us, "tokens_per_s": tok_per_s,
"unique_experts": unique_e, "avg_m_per_expert": avg_m_per_expert,
}
def run_backend(backend: str, weights, cpu_infer, m_list, routing):
print(f"\n[bench-fp4] backend={backend} routing={routing}")
moe = build_moe(backend, weights, cpu_infer)
rows = []
for M in m_list:
r = bench_one_m(moe, cpu_infer, M, routing)
rows.append(r)
print(f" M={M:>5} avg_m/expert={r['avg_m_per_expert']:>6.1f} "
f"per-iter={r['per_iter_us']:>9.1f} us tok/s={r['tokens_per_s']:>9.1f}")
return rows
def print_single_table(backend, rows, routing):
print(f"\n=== Summary ({backend}, routing={routing}) ===")
print(f"{'M':>5} {'avg_m':>6} {'per-iter us':>12} {'tok/s':>10}")
for r in rows:
print(f"{r['M']:>5} {r['avg_m_per_expert']:>6.1f} "
f"{r['per_iter_us']:>12.1f} {r['tokens_per_s']:>10.1f}")
def print_compare_table(all_rows: dict, routing: str):
backends = list(all_rows.keys())
if len(backends) < 2:
print_single_table(backends[0], all_rows[backends[0]], routing)
return
base = backends[0]
print(f"\n=== {' vs '.join(backends)} (routing={routing}, base={base}) ===")
header = f"{'M':>5} {'avg_m':>6}"
for be in backends:
header += f" {be + ' us':>10}"
for be in backends[1:]:
header += f" {be + '/' + base:>8}"
print(header)
n_rows = len(all_rows[base])
for i in range(n_rows):
line = f"{all_rows[base][i]['M']:>5} {all_rows[base][i]['avg_m_per_expert']:>6.1f}"
for be in backends:
line += f" {all_rows[be][i]['per_iter_us']:>10.1f}"
for be in backends[1:]:
ratio = all_rows[be][i]['per_iter_us'] / all_rows[base][i]['per_iter_us']
line += f" {ratio:>8.3f}"
print(line)
def get_git_commit():
try:
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
dirty = bool(subprocess.check_output(["git", "status", "--porcelain"]).decode().strip())
return {"commit": commit, "dirty": dirty}
except Exception as e:
return {"commit": None, "error": str(e)}
def get_system_info():
info = {"node": platform.node(), "system": platform.system()}
cpu_model = None
try:
with open("/proc/cpuinfo") as f:
for line in f:
if "model name" in line:
cpu_model = line.split(":", 1)[1].strip()
break
except Exception:
pass
info["cpu"] = cpu_model
info["cpu_cores"] = os.cpu_count()
return info
def main():
p = argparse.ArgumentParser()
p.add_argument("--backend", choices=list(BACKENDS.keys()), default="v1",
help="单 backend 模式(被 --all 覆盖)。当前可用: "
+ ",".join(k for k, v in BACKENDS.items() if v is not None))
p.add_argument("--all", action="store_true",
help="跑所有已绑定的 backend 并打印对比表(自动跳过未绑定的)")
p.add_argument("--routing", choices=["balanced", "concentrated"], default="balanced",
help="balanced=每 token randperm (V4 真实); "
"concentrated=所有 token 共用同组 top_k (per-expert m=M, 放大 mat-mat)")
p.add_argument("--m-list", type=str, default=None,
help=f"Comma-separated M values, default: {','.join(map(str, DEFAULT_M_LIST))}")
p.add_argument("--numa", type=int, default=WORKER_NUMA)
p.add_argument("--threads-per-numa", type=int, default=WORKER_THREADS_PER_NUMA)
args = p.parse_args()
m_list = [int(x) for x in args.m_list.split(",")] if args.m_list else DEFAULT_M_LIST
if args.all:
backends = [k for k, v in BACKENDS.items() if v is not None]
if not backends:
raise RuntimeError("No MXFP4 backend bound in this build.")
print(f"[bench-fp4] --all: detected backends = {backends}")
else:
if BACKENDS.get(args.backend) is None:
raise RuntimeError(
f"backend={args.backend} not bound. Available: "
f"{[k for k, v in BACKENDS.items() if v is not None]}"
)
backends = [args.backend]
print(f"[bench-fp4] shape=H{HIDDEN}/I{INTER}/E{EXPERT_NUM}/k{TOP_K}/gs{K_GROUP_SIZE} routing={args.routing}")
print(f"[bench-fp4] WorkerPool: numa={args.numa} threads_per_numa={args.threads_per_numa}")
print(f"[bench-fp4] m_list: {m_list}")
wp = kt_kernel_ext.WorkerPoolConfig()
wp.subpool_count = args.numa
wp.subpool_numa_map = list(range(args.numa))
wp.subpool_thread_count = [args.threads_per_numa] * args.numa
cpu_infer = kt_kernel_ext.CPUInfer(wp)
print("[bench-fp4] synthesizing MXFP4 weights …")
weights = build_synth_weights()
all_rows = {}
for be in backends:
all_rows[be] = run_backend(be, weights, cpu_infer, m_list, args.routing)
if len(backends) > 1:
print_compare_table(all_rows, args.routing)
else:
print_single_table(backends[0], all_rows[backends[0]], args.routing)
# JSONL log
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bench_fp4_moe.jsonl")
ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
git = get_git_commit()
sys_info = get_system_info()
with open(out_path, "a") as f:
for be, rows in all_rows.items():
record = {
"backend": be,
"routing": args.routing,
"shape": {"hidden": HIDDEN, "inter": INTER, "expert_num": EXPERT_NUM,
"top_k": TOP_K, "k_group_size": K_GROUP_SIZE},
"worker_pool": {"numa": args.numa, "threads_per_numa": args.threads_per_numa},
"rows": rows,
"git": git, "system": sys_info, "timestamp": ts,
}
f.write(json.dumps(record) + "\n")
print(f"\n[bench-fp4] appended {len(backends)} record(s) → {out_path}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,354 @@
import math
import os
import sys
from typing import Dict
sys.path.insert(0, os.path.dirname(__file__) + "/../build")
import torch
from kt_kernel import kt_kernel_ext
torch.manual_seed(42)
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
expert_num = 16
num_experts_per_tok = 8
layer_num = 1
CPUInfer = kt_kernel_ext.CPUInfer(40)
validation_iter = 3
k_group_size = 32
debug_print_count = 16
# Forward dispatch in do_gate_up_gemm uses `qlen > 4 * expert_num / top_k`
# (= 8 with these constants), so qlen=1 hits mat-vec and qlen=32 hits the
# mat-mat 4×4 register tile (per-expert avg m = qlen*top_k/expert_num = 16).
QLEN_LIST = [1, 32]
DISPATCH_THRESHOLD = 4 * expert_num / num_experts_per_tok
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
# E2M1 values: {0, ±0.5, ±1.0, ±1.5, ±2.0, ±3.0, ±4.0, ±6.0}
E2M1_VALUES = 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)
# Nibble encoding: index = 4-bit value
# 0b0000..0b0111 = positive, 0b1000..0b1111 = negative
E2M1_NIBBLE_MAP = torch.tensor([
0, # 0b0000 = 0.0
1, # 0b0001 = 0.5
2, # 0b0010 = 1.0
3, # 0b0011 = 1.5
4, # 0b0100 = 2.0
5, # 0b0101 = 3.0
6, # 0b0110 = 4.0
7, # 0b0111 = 6.0
8, # 0b1000 = -0.0
9, # 0b1001 = -0.5
10, # 0b1010 = -1.0
11, # 0b1011 = -1.5
12, # 0b1100 = -2.0
13, # 0b1101 = -3.0
14, # 0b1110 = -4.0
15, # 0b1111 = -6.0
], dtype=torch.int32)
def _pattern_uniform(groups: int) -> torch.Tensor:
return torch.full((groups,), 0.02, dtype=torch.float32)
def _pattern_alternating(groups: int) -> torch.Tensor:
vals = torch.full((groups,), 0.015, dtype=torch.float32)
vals[1::2] = 0.03
return vals
def _pattern_ramp(groups: int) -> torch.Tensor:
return torch.linspace(0.005, 0.04, steps=groups, dtype=torch.float32)
WEIGHT_PATTERNS = {
"uniform_scale": ("All k-groups share the same abs max / scale", _pattern_uniform),
"alternating_scale": ("Alternate small / large abs max per k-group", _pattern_alternating),
"ramp_scale": ("Linearly increasing abs max per k-group", _pattern_ramp),
"random": ("Random bf16 weights (baseline)", None),
}
def act_fn(x):
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
ret = torch.mm(intermediate, down_proj.t())
return ret
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
def quantize_mxfp4_tensor(weights: torch.Tensor, group_size: int):
"""
MXFP4 E2M1 quantization per k-group.
For each block of group_size (32) elements in K dimension:
scale = max_abs / 6.0
quantized = round(value / scale) to nearest E2M1 value
Args:
weights: [expert_num, rows (N), cols (K)] in bf16
Returns:
packed: int32 tensor storing 8 FP4 nibbles per int32, shape [expert_num, rows * (cols // 8)]
scales: bfloat16 tensor with shape [expert_num, rows * (cols // group_size)]
"""
weights_f32 = weights.to(torch.float32)
e, rows, cols = weights_f32.shape
if cols % group_size != 0 or cols % 2 != 0:
raise ValueError(f"cols ({cols}) must be divisible by group_size ({group_size}) and 2")
reshaped = weights_f32.view(e, rows, cols // group_size, group_size)
max_abs = reshaped.abs().amax(dim=-1, keepdim=True)
max_abs = torch.clamp(max_abs, min=1e-8)
scales = (max_abs / 6.0).squeeze(-1)
# Quantize: round(value / scale) to nearest E2M1 value
normalized = reshaped / scales.unsqueeze(-1)
# For each normalized value, find the closest E2M1 value
# E2M1_VALUES shape: [16]
e2m1_vals = E2M1_VALUES.view(1, 1, 1, 1, 16) # broadcast over (e, rows, groups, group_size, 16)
normalized_expanded = normalized.unsqueeze(-1) # (e, rows, groups, group_size, 1)
distances = torch.abs(normalized_expanded - e2m1_vals)
closest_indices = distances.argmin(dim=-1) # (e, rows, groups, group_size) — indices 0..15
# Dequantized values for reference
dequant = E2M1_VALUES[closest_indices].to(torch.float32) * scales.unsqueeze(-1)
dequant = dequant.view(e, rows, cols)
# Pack nibbles: each byte = (hi_nibble << 4) | lo_nibble
# Column-major: consecutive K elements are consecutive nibbles
# nibble at even K index goes to low nibble, odd K index goes to high nibble
# But wait — looking at the kernel's mxfp4_to_bf16_32:
# lo = packed & 0x0F, hi = (packed >> 4) & 0x0F
# And the interleaving is: [lo[0],hi[0], lo[1],hi[1], ...] = column order
# So for column-major layout: each byte has lo nibble = element at col c, hi nibble = element at col c+1
# But the weight buffer layout is: b_row[k_block] = 16 packed bytes covering 32 K elements
# With column-major: K is the innermost dimension
# For 32 elements per k_group: byte 0 = [K_0 | K_1], byte 1 = [K_2 | K_3], ..., byte 15 = [K_30 | K_31]
# low nibble = even K index, high nibble = odd K index
nibbles = closest_indices.to(torch.uint8) # 0..15, each is already a 4-bit value
nibbles = nibbles.view(e, rows, cols // 2, 2)
lo = nibbles[..., 0] # even K indices
hi = nibbles[..., 1] # odd K indices
packed_bytes = (hi << 4) | lo # low nibble first in memory (little-endian style)
# Pack 4 bytes into int32
bytes_view = packed_bytes.view(e, rows, cols // 8, 4)
packed_int32 = (
bytes_view[..., 0].to(torch.int32) |
(bytes_view[..., 1].to(torch.int32) << 8) |
(bytes_view[..., 2].to(torch.int32) << 16) |
(bytes_view[..., 3].to(torch.int32) << 24)
)
packed_int32 = packed_int32.view(e, rows, cols // 8).contiguous()
scales = scales.to(torch.bfloat16).contiguous().view(e, rows, cols // group_size).contiguous()
return packed_int32, scales, dequant
def build_structured_tensor(shape: torch.Size, pattern: str) -> torch.Tensor:
if pattern == "random":
torch.manual_seed(42)
return (torch.randn(shape, dtype=torch.bfloat16, device="cpu") / 100.0).contiguous()
e, rows, cols = shape
groups = cols // k_group_size
group_builder = WEIGHT_PATTERNS[pattern][1]
group_vals = group_builder(groups).to(torch.float32)
block = group_vals.view(1, 1, groups, 1).expand(e, rows, groups, k_group_size).clone()
row_signs = torch.where(
(torch.arange(rows) % 2 == 0),
torch.ones(rows, dtype=torch.float32),
-torch.ones(rows, dtype=torch.float32),
).view(1, rows, 1, 1)
col_offsets = torch.linspace(-0.0005, 0.0005, steps=k_group_size, dtype=torch.float32).view(1, 1, 1, k_group_size)
block = block * row_signs + col_offsets
return block.reshape(shape).to(torch.bfloat16).contiguous()
def prepare_mxfp4_quantized_weights(pattern: str) -> Dict[str, torch.Tensor]:
if pattern not in WEIGHT_PATTERNS:
raise ValueError(f"Unknown weight pattern: {pattern}")
gate_proj = build_structured_tensor((expert_num, intermediate_size, hidden_size), pattern)
up_proj = build_structured_tensor((expert_num, intermediate_size, hidden_size), pattern)
down_proj = build_structured_tensor((expert_num, hidden_size, intermediate_size), pattern)
gate_q, gate_scales, gate_dequant = quantize_mxfp4_tensor(gate_proj, k_group_size)
up_q, up_scales, up_dequant = quantize_mxfp4_tensor(up_proj, k_group_size)
down_q, down_scales, down_dequant = quantize_mxfp4_tensor(down_proj, k_group_size)
return {
"gate_qweight": gate_q.contiguous(),
"up_qweight": up_q.contiguous(),
"down_qweight": down_q.contiguous(),
"gate_scales": gate_scales.contiguous(),
"up_scales": up_scales.contiguous(),
"down_scales": down_scales.contiguous(),
"dequantized": {
"gate_proj": gate_dequant.to(torch.bfloat16).contiguous(),
"up_proj": up_dequant.to(torch.bfloat16).contiguous(),
"down_proj": down_dequant.to(torch.bfloat16).contiguous(),
},
}
def build_moes_from_quantized_data(quant_data: Dict[str, torch.Tensor]):
moes = []
with torch.inference_mode(mode=True):
for _ in range(layer_num):
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.quant_config.bits = 4
config.quant_config.group_size = k_group_size
config.quant_config.zero_point = False
config.gate_proj = quant_data["gate_qweight"].data_ptr()
config.up_proj = quant_data["up_qweight"].data_ptr()
config.down_proj = quant_data["down_qweight"].data_ptr()
config.gate_scale = quant_data["gate_scales"].data_ptr()
config.up_scale = quant_data["up_scales"].data_ptr()
config.down_scale = quant_data["down_scales"].data_ptr()
config.pool = CPUInfer.backend_
moe = kt_kernel_ext.moe.AMXFP4_KGroup_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
moes.append(moe)
return moes
def run_case(pattern: str, qlen: int) -> Dict[str, float]:
print("\n" + "=" * 70)
desc = WEIGHT_PATTERNS[pattern][0]
path = "mat-vec" if qlen <= DISPATCH_THRESHOLD else "mat-mat"
print(f"Running case: {pattern} -> {desc} (qlen={qlen}, path={path})")
print("=" * 70)
quant_data = prepare_mxfp4_quantized_weights(pattern)
moes = build_moes_from_quantized_data(quant_data)
dequant_weights = quant_data["dequantized"]
gate_bf16 = dequant_weights["gate_proj"]
up_bf16 = dequant_weights["up_proj"]
down_bf16 = dequant_weights["down_proj"]
diffs = []
with torch.inference_mode(mode=True):
for i in range(validation_iter):
torch.manual_seed(100 + i)
bsz_tensor = torch.tensor([qlen], device="cpu")
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.randn((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_tensor = torch.randn((qlen, hidden_size), dtype=torch.bfloat16).contiguous() / 100
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
moe = moes[i % layer_num]
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_tensor.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Torch reference: use dequantized weights
input_tensor_bf16 = input_tensor.to(torch.bfloat16)
t_output = moe_torch(input_tensor_bf16, expert_ids, weights, gate_bf16, up_bf16, down_bf16).to(
torch.bfloat16
)
t_output = t_output.flatten()
output = output.flatten()
diff = torch.mean(torch.abs(output.float() - t_output.float())) / (
torch.mean(torch.abs(t_output.float())) + 1e-12
)
diffs.append(diff.item())
print(f"[{pattern}] Iteration {i}: relative L1 diff = {diff:.4f}")
print(f" output {output[:debug_print_count]}")
print(f" t_output {t_output[:debug_print_count]}")
mean_diff = float(sum(diffs) / len(diffs))
max_diff = float(max(diffs))
min_diff = float(min(diffs))
return {"case": pattern, "description": desc, "mean": mean_diff, "max": max_diff, "min": min_diff}
def run_fp4_moe_test():
summary_rows = []
for qlen in QLEN_LIST:
path = "mat-vec" if qlen <= DISPATCH_THRESHOLD else "mat-mat"
print(f"\n##### qlen={qlen} path={path} #####")
for case_name in WEIGHT_PATTERNS.keys():
results = run_case(case_name, qlen)
results["qlen"] = qlen
results["path"] = path
summary_rows.append(results)
print("\n=== Case vs. Relative Error Summary ===")
print(f"{'Case':<20} {'qlen':>5} {'path':<8} {'Mean':>10} {'Max':>10} {'Min':>10}")
for row in summary_rows:
print(f"{row['case']:<20} {row['qlen']:>5} {row['path']:<8} "
f"{row['mean']*100:9.2f}% {row['max']*100:9.2f}% {row['min']*100:9.2f}%")
if __name__ == "__main__":
run_fp4_moe_test()

View file

@ -0,0 +1,178 @@
"""End-to-end MXFP4 MoE validation against the native DeepSeek-V4-Flash ckpt.
Loads layer-`LAYER_ID` experts via :class:`MXFP4SafeTensorLoader`, runs the AMX
FP4 backend, and compares against a torch reference that dequantizes the same
nibble-packed weights with the OCP E2M1 LUT.
Usage:
python test_fp4_moe_v4.py --weight-path /path/to/DeepSeek-V4-Flash [--layer 1]
"""
from __future__ import annotations
import argparse
import os
import sys
from typing import Tuple
import torch
# Allow running from kt-kernel/examples without install.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/build")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/python")
from kt_kernel import kt_kernel_ext # noqa: E402
from kt_kernel.utils.loader import MXFP4SafeTensorLoader # noqa: E402
# OCP E2M1 codepoints in our LUT order (matches operators/amx/fp4-moe.hpp).
E2M1_VALUES = 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 dequantize_mxfp4(weight_u8: torch.Tensor, scale_bf16: torch.Tensor, group_size: int) -> torch.Tensor:
"""Decode a [N, K/2] uint8 tensor of nibble-packed E2M1 with [N, K/gs] bf16
scales into a [N, K] bf16 weight tensor.
Layout (matches kernel's mxfp4_to_bf16_32): byte `b` low nibble = element K=2b,
high nibble = element K=2b+1.
"""
n, k_packed = weight_u8.shape
k = k_packed * 2
assert k % group_size == 0, f"K={k} must be divisible by group_size={group_size}"
assert scale_bf16.shape == (n, k // group_size)
lo = (weight_u8 & 0x0F).to(torch.long)
hi = ((weight_u8 >> 4) & 0x0F).to(torch.long)
nibbles = torch.stack([lo, hi], dim=-1).view(n, k) # interleave back to K order
decoded = E2M1_VALUES.to(weight_u8.device)[nibbles] # [N, K] fp32
scale_fp32 = scale_bf16.to(torch.float32)
scale_full = scale_fp32.repeat_interleave(group_size, dim=-1) # [N, K]
return (decoded * scale_full).to(torch.bfloat16).contiguous()
def reference_mlp(x: torch.Tensor, gate: torch.Tensor, up: torch.Tensor, down: torch.Tensor) -> torch.Tensor:
g = torch.mm(x, gate.t())
u = torch.mm(x, up.t())
silu = g / (1.0 + torch.exp(-g.float())).to(g.dtype)
return torch.mm(silu * u, down.t())
def reference_moe(
hidden: torch.Tensor,
expert_ids: torch.Tensor,
weights: torch.Tensor,
gate_w: torch.Tensor, # [E, N, K]
up_w: torch.Tensor,
down_w: torch.Tensor,
) -> torch.Tensor:
out = torch.zeros_like(hidden, dtype=torch.float32)
for tok in range(hidden.shape[0]):
for slot in range(expert_ids.shape[1]):
eid = int(expert_ids[tok, slot])
w = float(weights[tok, slot])
x = hidden[tok : tok + 1]
y = reference_mlp(x, gate_w[eid], up_w[eid], down_w[eid])
out[tok] += w * y[0].float()
return out.to(hidden.dtype)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser()
p.add_argument("--weight-path", required=True, help="Path to DeepSeek-V4-Flash safetensors directory.")
p.add_argument("--layer", type=int, default=1, help="Layer index to validate (default: 1).")
p.add_argument("--qlen", type=int, default=1, help="Number of tokens to test.")
p.add_argument("--top-k", type=int, default=6, help="num_experts_per_tok (V4 default 6).")
p.add_argument("--cpu-threads", type=int, default=32)
p.add_argument("--max-experts", type=int, default=0, help="Cap number of experts loaded (0 = all).")
return p.parse_args()
def main() -> int:
args = parse_args()
torch.manual_seed(0)
print(f"[V4-MXFP4] Loading layer {args.layer} from {args.weight_path}")
loader = MXFP4SafeTensorLoader(args.weight_path)
weights = loader.load_experts(f"model.layers.{args.layer}")
expert_num = len(weights["gate"])
if args.max_experts and args.max_experts < expert_num:
for k in ("gate", "up", "down", "gate_scale", "up_scale", "down_scale"):
weights[k] = weights[k][: args.max_experts]
expert_num = args.max_experts
print(f"[V4-MXFP4] expert_num={expert_num}")
gate0 = weights["gate"][0]
down0 = weights["down"][0]
intermediate_size = gate0.shape[0]
hidden_size = gate0.shape[1] * 2 # nibble-packed K
assert down0.shape == (hidden_size, intermediate_size // 2), f"unexpected down shape {down0.shape}"
group_size = hidden_size // weights["gate_scale"][0].shape[1]
print(f"[V4-MXFP4] hidden={hidden_size} inter={intermediate_size} gs={group_size}")
assert group_size == 32, "MXFP4 backend hard-codes group_size=32"
physical_to_logical = torch.arange(expert_num, dtype=torch.int64).contiguous()
# ----- AMX FP4 forward -----
cpu_infer = kt_kernel_ext.CPUInfer(args.cpu_threads)
cfg = kt_kernel_ext.moe.MOEConfig(expert_num, args.top_k, hidden_size, intermediate_size, 0)
cfg.layer_idx = args.layer
cfg.max_len = max(args.qlen, 1)
cfg.pool = cpu_infer.backend_
cfg.quant_config.bits = 4
cfg.quant_config.group_size = group_size
cfg.quant_config.zero_point = False
cfg.gate_projs = [[t.data_ptr() for t in weights["gate"]]]
cfg.up_projs = [[t.data_ptr() for t in weights["up"]]]
cfg.down_projs = [[t.data_ptr() for t in weights["down"]]]
cfg.gate_scales = [[t.data_ptr() for t in weights["gate_scale"]]]
cfg.up_scales = [[t.data_ptr() for t in weights["up_scale"]]]
cfg.down_scales = [[t.data_ptr() for t in weights["down_scale"]]]
moe = kt_kernel_ext.moe.AMXFP4_KGroup_MOE(cfg)
cpu_infer.submit(moe.load_weights_task(physical_to_logical.data_ptr()))
cpu_infer.sync()
qlen = args.qlen
top_k = args.top_k
bsz = torch.tensor([qlen], dtype=torch.int32)
expert_ids = torch.stack([torch.randperm(expert_num)[:top_k] for _ in range(qlen)]).to(torch.int32).contiguous()
routing = torch.randn((qlen, top_k), dtype=torch.float32).contiguous()
x = (torch.randn((qlen, hidden_size), dtype=torch.bfloat16) / 100).contiguous()
y_amx = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
cpu_infer.submit(
moe.forward_task(
bsz.data_ptr(), top_k, expert_ids.data_ptr(), routing.data_ptr(),
x.data_ptr(), y_amx.data_ptr(), False,
)
)
cpu_infer.sync()
# ----- Torch reference (dequantize same nibbles + scales) -----
print("[V4-MXFP4] Building torch reference (dequantizing all loaded experts)…")
gate_bf16 = torch.stack([dequantize_mxfp4(weights["gate"][i], weights["gate_scale"][i], group_size) for i in range(expert_num)])
up_bf16 = torch.stack([dequantize_mxfp4(weights["up"][i], weights["up_scale"][i], group_size) for i in range(expert_num)])
down_bf16 = torch.stack([dequantize_mxfp4(weights["down"][i], weights["down_scale"][i], group_size) for i in range(expert_num)])
y_ref = reference_moe(x, expert_ids, routing, gate_bf16, up_bf16, down_bf16)
diff = (y_amx.float() - y_ref.float()).abs()
rel = diff.mean() / (y_ref.float().abs().mean() + 1e-12)
print(f"[V4-MXFP4] mean abs diff = {diff.mean().item():.4e}")
print(f"[V4-MXFP4] max abs diff = {diff.max().item():.4e}")
print(f"[V4-MXFP4] rel mean diff = {rel.item()*100:.3f}%")
print(f"[V4-MXFP4] amx[:8] = {y_amx.flatten()[:8]}")
print(f"[V4-MXFP4] ref[:8] = {y_ref.flatten()[:8]}")
return 0 if rel.item() < 0.10 else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -44,6 +44,7 @@ static const bool _is_plain_ = false;
#if defined(__x86_64__) && defined(USE_AMX_AVX_KERNEL)
#include "operators/amx/awq-moe.hpp"
#include "operators/amx/bf16-moe.hpp" // Native BF16 MoE using CRTP pattern, with fallback for AVX512F
#include "operators/amx/fp4-moe.hpp" // MXFP4 MoE: FP4 E2M1 weights × BF16 activations
#include "operators/amx/fp8-moe.hpp" // FP8 MoE requires AVX512 BF16 support, with fallback for AVX512F+BW
#include "operators/amx/fp8-perchannel-moe.hpp" // FP8 Per-Channel MoE for GLM-4.7-FP8
#include "operators/amx/k2-moe.hpp"
@ -788,6 +789,9 @@ PYBIND11_MODULE(kt_kernel_ext, m) {
bind_moe_module<AMX_BF16_MOE_TP<amx::GemmKernel224BF16>>(moe_module, "AMXBF16_MOE");
bind_moe_module<AMX_FP8_MOE_TP<amx::GemmKernel224FP8>>(moe_module, "AMXFP8_MOE");
bind_moe_module<AMX_FP8_PERCHANNEL_MOE_TP<amx::GemmKernel224FP8PerChannel>>(moe_module, "AMXFP8PerChannel_MOE");
#endif
#if defined(__AVX512BF16__)
bind_moe_module<AMX_FP4_MOE_TP<amx::GemmKernel224MXFP4SmallKGroup>>(moe_module, "AMXFP4_KGroup_MOE");
#endif
// SFT MoE with LoRA support (BF16, INT8, INT4, AWQ, K2)
bind_moe_sft_module<AMX_SFT_MOE_TP<amx::GemmKernel224BF>>(moe_module, "AMXBF16_SFT_MOE");

View file

@ -0,0 +1,783 @@
/**
* @Description : MXFP4 MoE operator FP4 E2M1 weights × BF16 activations
* @Author : oql, Codex and Claude
* @Date : 2026-04-20
* @Version : 1.0.0
* @Copyright (c) 2024 by KVCache.AI, All Rights Reserved.
*
* Based on k2-moe.hpp (RAWINT4). Key differences from RAWINT4:
* Weight: FP4 E2M1 (nibble-packed, same layout) PSHUFB lookup BF16
* Act: BF16 direct (BufferABF16Impl, no online INT8 quantization)
* Dot prod: _mm512_dpbf16_ps (BF16×BF16FP32) instead of _mm512_dpbssd_epi32
* Scale: FP32 per-group scale (weight only, no activation scale)
**/
#ifndef CPUINFER_OPERATOR_AMX_FP4_MOE_H
#define CPUINFER_OPERATOR_AMX_FP4_MOE_H
#include "la/amx_raw_buffers.hpp" // BufferABF16Impl
#include "moe_base.hpp"
namespace amx {
// ============================================================================
// MXFP4 kernel: FP4 E2M1 weights × BF16 activations → FP32 output (AVX512)
// ============================================================================
struct GemmKernel224MXFP4SmallKGroup {
using dt = uint8_t;
using output_t = float;
static constexpr double ELEMENT_SIZE = 0.5;
static const int M_STEP = 1;
static const int N_STEP = 32;
static const int K_STEP = 32;
static inline const int N_BLOCK = 256;
static inline const int K_BLOCK = 7168;
static std::string name() { return "MXFP4_KGROUP"; }
static int recommended_nth(int n) { return (n + N_BLOCK - 1) / N_BLOCK; }
static std::pair<int, int> split_range_n(int n, int ith, int nth) {
int n_start = N_BLOCK * ith;
int n_end = std::min(n, N_BLOCK * (ith + 1));
return {n_start, n_end};
}
static void config() {}
// FP4 E2M1 → BF16 LUTs (16 entries each, for PSHUFB within 128-bit lanes)
// E2M1 values: {0, ±0.5, ±1.0, ±1.5, ±2.0, ±3.0, ±4.0, ±6.0}
alignas(16) static constexpr uint8_t fp4_bf16_lo[16] = {
0x00, 0x00, 0x80, 0xC0, 0x00, 0x40, 0x80, 0xC0, // 0..7 positive
0x00, 0x00, 0x80, 0xC0, 0x00, 0x40, 0x80, 0xC0}; // 8..15 negative
alignas(16) static constexpr uint8_t fp4_bf16_hi[16] = {
0x00, 0x3F, 0x3F, 0x3F, 0x40, 0x40, 0x40, 0x40, // 0..7 positive
0x80, 0xBF, 0xBF, 0xBF, 0xC0, 0xC0, 0xC0, 0xC0}; // 8..15 negative
// Convert 16 packed FP4 bytes (32 values = 1 k_group) → 32 BF16 values (__m512i)
// Output column order: [BF16(lo[0]),BF16(hi[0]), ..., BF16(lo[15]),BF16(hi[15])]
__attribute__((always_inline)) static inline __m512i mxfp4_to_bf16_32(__m128i packed) {
__m128i lo_mask = _mm_set1_epi8(0x0F);
__m128i lo = _mm_and_si128(packed, lo_mask);
__m128i hi = _mm_and_si128(_mm_srli_epi16(packed, 4), lo_mask);
__m128i lut_lo = _mm_load_si128((__m128i*)fp4_bf16_lo);
__m128i lut_hi = _mm_load_si128((__m128i*)fp4_bf16_hi);
// Look up low/high bytes for lo nibbles → 16 BF16 values
__m128i l_lo = _mm_shuffle_epi8(lut_lo, lo);
__m128i l_hi = _mm_shuffle_epi8(lut_hi, lo);
__m128i lo_bf16_0 = _mm_unpacklo_epi8(l_lo, l_hi); // BF16(lo[0..7])
__m128i lo_bf16_1 = _mm_unpackhi_epi8(l_lo, l_hi); // BF16(lo[8..15])
// Look up low/high bytes for hi nibbles → 16 BF16 values
__m128i h_lo = _mm_shuffle_epi8(lut_lo, hi);
__m128i h_hi = _mm_shuffle_epi8(lut_hi, hi);
__m128i hi_bf16_0 = _mm_unpacklo_epi8(h_lo, h_hi); // BF16(hi[0..7])
__m128i hi_bf16_1 = _mm_unpackhi_epi8(h_lo, h_hi); // BF16(hi[8..15])
// Interleave lo/hi at 16-bit: [lo[0],hi[0], lo[1],hi[1], ...] = column order
__m128i p0 = _mm_unpacklo_epi16(lo_bf16_0, hi_bf16_0); // cols 0..7
__m128i p1 = _mm_unpackhi_epi16(lo_bf16_0, hi_bf16_0); // cols 8..15
__m128i p2 = _mm_unpacklo_epi16(lo_bf16_1, hi_bf16_1); // cols 16..23
__m128i p3 = _mm_unpackhi_epi16(lo_bf16_1, hi_bf16_1); // cols 24..31
__m256i q0 = _mm256_inserti128_si256(_mm256_castsi128_si256(p0), p1, 1);
__m256i q1 = _mm256_inserti128_si256(_mm256_castsi128_si256(p2), p3, 1);
return _mm512_inserti64x4(_mm512_castsi256_si512(q0), q1, 1);
}
// Buffers
using BufferA = BufferABF16Impl<GemmKernel224MXFP4SmallKGroup>; // raw BF16, no quant
using BufferB = BufferBInt4KGroupImpl<GemmKernel224MXFP4SmallKGroup>; // nibble-packed FP4
using BufferC = BufferCReduceImpl<GemmKernel224MXFP4SmallKGroup>; // FP32 reduce
// 4 个 zmm 的 horizontal reduce → 4 个连续 fp32。
// 4 次 reduce_add_ps 之间无依赖,编译器/CPU 可并行调度。
__attribute__((always_inline)) static inline void
reduce4(__m512 s0, __m512 s1, __m512 s2, __m512 s3, float* dst) {
dst[0] = _mm512_reduce_add_ps(s0);
dst[1] = _mm512_reduce_add_ps(s1);
dst[2] = _mm512_reduce_add_ps(s2);
dst[3] = _mm512_reduce_add_ps(s3);
}
// mat-vec: M 个独立 tokenN 维 4 行一组累加,摊销 horizontal reduce。
static void fp4_mat_vec_kgroup(int m, int n, int k, int k_group_size, BufferA* ba, BufferB* bb, BufferC* bc,
int ith, int nth) {
auto [n_start, n_end] = split_range_n(n, ith, nth);
if (n_start >= n_end) return;
const int kg_count = k / 32;
for (int m_idx = 0; m_idx < m; m_idx++) {
float* c_row = bc->get_submat(m, n, m_idx, n_start);
__m512bh* a_row = (__m512bh*)ba->get_submat(m, k, m_idx, 0);
int n_pos = n_start;
// 主循环: N 维 4 行一组
for (; n_pos + 4 <= n_end; n_pos += 4) {
__m128i* w0 = (__m128i*)bb->get_submat(n, k, n_pos + 0, 0);
__m128i* w1 = (__m128i*)bb->get_submat(n, k, n_pos + 1, 0);
__m128i* w2 = (__m128i*)bb->get_submat(n, k, n_pos + 2, 0);
__m128i* w3 = (__m128i*)bb->get_submat(n, k, n_pos + 3, 0);
const float* s0 = bb->get_scale(n, n_pos + 0, k, 0);
const float* s1 = bb->get_scale(n, n_pos + 1, k, 0);
const float* s2 = bb->get_scale(n, n_pos + 2, k, 0);
const float* s3 = bb->get_scale(n, n_pos + 3, k, 0);
__m512 acc0 = _mm512_setzero_ps();
__m512 acc1 = _mm512_setzero_ps();
__m512 acc2 = _mm512_setzero_ps();
__m512 acc3 = _mm512_setzero_ps();
for (int g = 0; g < kg_count; g++) {
const __m512bh a = a_row[g];
const __m512bh d0 = (__m512bh)mxfp4_to_bf16_32(w0[g]);
const __m512bh d1 = (__m512bh)mxfp4_to_bf16_32(w1[g]);
const __m512bh d2 = (__m512bh)mxfp4_to_bf16_32(w2[g]);
const __m512bh d3 = (__m512bh)mxfp4_to_bf16_32(w3[g]);
acc0 = _mm512_fmadd_ps(_mm512_set1_ps(s0[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, d0), acc0);
acc1 = _mm512_fmadd_ps(_mm512_set1_ps(s1[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, d1), acc1);
acc2 = _mm512_fmadd_ps(_mm512_set1_ps(s2[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, d2), acc2);
acc3 = _mm512_fmadd_ps(_mm512_set1_ps(s3[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, d3), acc3);
}
reduce4(acc0, acc1, acc2, acc3, c_row + (n_pos - n_start));
}
// N 尾巴: N % 4 != 0 时单行 fallback
for (; n_pos < n_end; n_pos++) {
__m128i* w = (__m128i*)bb->get_submat(n, k, n_pos, 0);
const float* s = bb->get_scale(n, n_pos, k, 0);
__m512 acc = _mm512_setzero_ps();
for (int g = 0; g < kg_count; g++) {
const __m512bh a = a_row[g];
const __m512bh d = (__m512bh)mxfp4_to_bf16_32(w[g]);
acc = _mm512_fmadd_ps(_mm512_set1_ps(s[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, d), acc);
}
c_row[n_pos - n_start] = _mm512_reduce_add_ps(acc);
}
}
}
// mat-mat: 4×4 register tile (M_TILE=4, N_TILE=4 → 16 累加器)。
// 每 K-group 解码 4 行 N 一次, 被 4 个 token 共享 → PSHUFB 解码开销 / 4。
// M / N 尾巴回退到 mat-vec 单 token 内层 (V4 chunked-prefill 16/32/64 整数倍, 极少触发)。
static void fp4_mat_mat_kgroup(int m, int n, int k, int k_group_size, BufferA* ba, BufferB* bb, BufferC* bc,
int ith, int nth) {
auto [n_start, n_end] = split_range_n(n, ith, nth);
if (n_start >= n_end) return;
const int kg_count = k / 32;
constexpr int MB = 4;
constexpr int NB = 4;
int m_pos = 0;
for (; m_pos + MB <= m; m_pos += MB) {
__m512bh* a_rows[MB] = {
(__m512bh*)ba->get_submat(m, k, m_pos + 0, 0),
(__m512bh*)ba->get_submat(m, k, m_pos + 1, 0),
(__m512bh*)ba->get_submat(m, k, m_pos + 2, 0),
(__m512bh*)ba->get_submat(m, k, m_pos + 3, 0),
};
int n_pos = n_start;
for (; n_pos + NB <= n_end; n_pos += NB) {
__m128i* w0 = (__m128i*)bb->get_submat(n, k, n_pos + 0, 0);
__m128i* w1 = (__m128i*)bb->get_submat(n, k, n_pos + 1, 0);
__m128i* w2 = (__m128i*)bb->get_submat(n, k, n_pos + 2, 0);
__m128i* w3 = (__m128i*)bb->get_submat(n, k, n_pos + 3, 0);
const float* s0 = bb->get_scale(n, n_pos + 0, k, 0);
const float* s1 = bb->get_scale(n, n_pos + 1, k, 0);
const float* s2 = bb->get_scale(n, n_pos + 2, k, 0);
const float* s3 = bb->get_scale(n, n_pos + 3, k, 0);
__m512 acc[MB][NB];
for (int i = 0; i < MB; i++)
for (int j = 0; j < NB; j++) acc[i][j] = _mm512_setzero_ps();
for (int g = 0; g < kg_count; g++) {
// 4 行权重解码一次, MB 个 token 共享
const __m512bh d0 = (__m512bh)mxfp4_to_bf16_32(w0[g]);
const __m512bh d1 = (__m512bh)mxfp4_to_bf16_32(w1[g]);
const __m512bh d2 = (__m512bh)mxfp4_to_bf16_32(w2[g]);
const __m512bh d3 = (__m512bh)mxfp4_to_bf16_32(w3[g]);
const __m512 sv0 = _mm512_set1_ps(s0[g]);
const __m512 sv1 = _mm512_set1_ps(s1[g]);
const __m512 sv2 = _mm512_set1_ps(s2[g]);
const __m512 sv3 = _mm512_set1_ps(s3[g]);
#define V_FMA_ROW(M_I) do { \
const __m512bh a = a_rows[M_I][g]; \
acc[M_I][0] = _mm512_fmadd_ps(sv0, _mm512_dpbf16_ps(_mm512_setzero_ps(), a, d0), acc[M_I][0]); \
acc[M_I][1] = _mm512_fmadd_ps(sv1, _mm512_dpbf16_ps(_mm512_setzero_ps(), a, d1), acc[M_I][1]); \
acc[M_I][2] = _mm512_fmadd_ps(sv2, _mm512_dpbf16_ps(_mm512_setzero_ps(), a, d2), acc[M_I][2]); \
acc[M_I][3] = _mm512_fmadd_ps(sv3, _mm512_dpbf16_ps(_mm512_setzero_ps(), a, d3), acc[M_I][3]); \
} while (0)
V_FMA_ROW(0);
V_FMA_ROW(1);
V_FMA_ROW(2);
V_FMA_ROW(3);
#undef V_FMA_ROW
}
for (int i = 0; i < MB; i++) {
float* c_row = bc->get_submat(m, n, m_pos + i, n_start);
reduce4(acc[i][0], acc[i][1], acc[i][2], acc[i][3], c_row + (n_pos - n_start));
}
}
// N 尾巴: 单 N 列 × MB token (V4 不触发)
for (; n_pos < n_end; n_pos++) {
__m128i* w = (__m128i*)bb->get_submat(n, k, n_pos, 0);
const float* s = bb->get_scale(n, n_pos, k, 0);
for (int i = 0; i < MB; i++) {
float* c_row = bc->get_submat(m, n, m_pos + i, n_start);
__m512 acc = _mm512_setzero_ps();
for (int g = 0; g < kg_count; g++) {
acc = _mm512_fmadd_ps(_mm512_set1_ps(s[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(),
a_rows[i][g],
(__m512bh)mxfp4_to_bf16_32(w[g])),
acc);
}
c_row[n_pos - n_start] = _mm512_reduce_add_ps(acc);
}
}
}
// M 尾巴: M 不是 MB 倍数时余下 token, 退回单 token mat-vec 内层 (V4 不触发)
for (int mi = m_pos; mi < m; mi++) {
float* c_row = bc->get_submat(m, n, mi, n_start);
__m512bh* a_row = (__m512bh*)ba->get_submat(m, k, mi, 0);
int n_pos = n_start;
for (; n_pos + 4 <= n_end; n_pos += 4) {
__m128i* w0 = (__m128i*)bb->get_submat(n, k, n_pos + 0, 0);
__m128i* w1 = (__m128i*)bb->get_submat(n, k, n_pos + 1, 0);
__m128i* w2 = (__m128i*)bb->get_submat(n, k, n_pos + 2, 0);
__m128i* w3 = (__m128i*)bb->get_submat(n, k, n_pos + 3, 0);
const float* s0 = bb->get_scale(n, n_pos + 0, k, 0);
const float* s1 = bb->get_scale(n, n_pos + 1, k, 0);
const float* s2 = bb->get_scale(n, n_pos + 2, k, 0);
const float* s3 = bb->get_scale(n, n_pos + 3, k, 0);
__m512 a0 = _mm512_setzero_ps(), a1 = _mm512_setzero_ps(),
a2 = _mm512_setzero_ps(), a3 = _mm512_setzero_ps();
for (int g = 0; g < kg_count; g++) {
const __m512bh a = a_row[g];
a0 = _mm512_fmadd_ps(_mm512_set1_ps(s0[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, (__m512bh)mxfp4_to_bf16_32(w0[g])), a0);
a1 = _mm512_fmadd_ps(_mm512_set1_ps(s1[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, (__m512bh)mxfp4_to_bf16_32(w1[g])), a1);
a2 = _mm512_fmadd_ps(_mm512_set1_ps(s2[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, (__m512bh)mxfp4_to_bf16_32(w2[g])), a2);
a3 = _mm512_fmadd_ps(_mm512_set1_ps(s3[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(), a, (__m512bh)mxfp4_to_bf16_32(w3[g])), a3);
}
reduce4(a0, a1, a2, a3, c_row + (n_pos - n_start));
}
for (; n_pos < n_end; n_pos++) {
__m128i* w = (__m128i*)bb->get_submat(n, k, n_pos, 0);
const float* s = bb->get_scale(n, n_pos, k, 0);
__m512 acc = _mm512_setzero_ps();
for (int g = 0; g < kg_count; g++) {
acc = _mm512_fmadd_ps(_mm512_set1_ps(s[g]),
_mm512_dpbf16_ps(_mm512_setzero_ps(),
a_row[g],
(__m512bh)mxfp4_to_bf16_32(w[g])),
acc);
}
c_row[n_pos - n_start] = _mm512_reduce_add_ps(acc);
}
}
}
};
// Dispatch functions
inline void vec_mul_kgroup(int m, int n, int k, int k_group_size,
std::shared_ptr<GemmKernel224MXFP4SmallKGroup::BufferA> ba,
std::shared_ptr<GemmKernel224MXFP4SmallKGroup::BufferB> bb,
std::shared_ptr<GemmKernel224MXFP4SmallKGroup::BufferC> bc, int ith, int nth) {
GemmKernel224MXFP4SmallKGroup::fp4_mat_vec_kgroup(m, n, k, k_group_size, ba.get(), bb.get(), bc.get(), ith, nth);
}
inline void mat_mul_kgroup(int m, int n, int k, int k_group_size,
std::shared_ptr<GemmKernel224MXFP4SmallKGroup::BufferA> ba,
std::shared_ptr<GemmKernel224MXFP4SmallKGroup::BufferB> bb,
std::shared_ptr<GemmKernel224MXFP4SmallKGroup::BufferC> bc, int ith, int nth) {
GemmKernel224MXFP4SmallKGroup::fp4_mat_mat_kgroup(m, n, k, k_group_size, ba.get(), bb.get(), bc.get(), ith, nth);
}
} // namespace amx
// ============================================================================
// AMX_FP4_MOE_TP — CRTP class, identical structure to AMX_K2_MOE_TP
// ============================================================================
template <class T = amx::GemmKernel224MXFP4SmallKGroup>
class AMX_FP4_MOE_TP : public AMX_MOE_BASE<T, AMX_FP4_MOE_TP<T>> {
using Base = AMX_MOE_BASE<T, AMX_FP4_MOE_TP<T>>;
using Base::config_;
using Base::down_ba_;
using Base::down_bb_;
using Base::down_bc_;
using Base::gate_bb_;
using Base::gate_bc_;
using Base::gate_up_ba_;
using Base::m_local_num_;
using Base::tp_part_idx;
using Base::up_bb_;
using Base::up_bc_;
public:
using typename Base::input_t;
using typename Base::output_t;
AMX_FP4_MOE_TP() = default;
AMX_FP4_MOE_TP(GeneralMOEConfig config, int tp_part_idx_ = 0) : Base(config, tp_part_idx_) {}
void derived_init() {
auto& quant_config = config_.quant_config;
if (quant_config.group_size == 0 || quant_config.zero_point) {
throw std::runtime_error("MXFP4 MoE only supports KGroup FP4");
}
printf("Creating AMX_FP4_MOE_TP %d at numa %d\n", tp_part_idx, numa_node_of_cpu(sched_getcpu()));
}
~AMX_FP4_MOE_TP() = default;
// BufferA: raw BF16, no group_size needed
size_t buffer_a_required_size_impl(size_t m, size_t k) const { return T::BufferA::required_size(m, k); }
size_t buffer_b_required_size_impl(size_t n, size_t k) const {
return T::BufferB::required_size(n, k, config_.quant_config.group_size);
}
size_t buffer_c_required_size_impl(size_t m, size_t n) const { return T::BufferC::required_size(m, n); }
std::shared_ptr<typename T::BufferA> make_buffer_a_impl(size_t m, size_t k, void* data) const {
return std::make_shared<typename T::BufferA>(m, k, data);
}
std::shared_ptr<typename T::BufferB> make_buffer_b_impl(size_t n, size_t k, void* data) const {
return std::make_shared<typename T::BufferB>(n, k, config_.quant_config.group_size, data);
}
std::shared_ptr<typename T::BufferC> make_buffer_c_impl(size_t m, size_t n, void* data) const {
return std::make_shared<typename T::BufferC>(m, n, data);
}
void do_gate_up_gemm(bool do_up, int expert_idx, int ith, int nth, int qlen) {
auto& group_size = config_.quant_config.group_size;
int m = m_local_num_[expert_idx];
auto& ba = gate_up_ba_[expert_idx];
auto& bb = do_up ? up_bb_[expert_idx] : gate_bb_[expert_idx];
auto& bc = do_up ? up_bc_[expert_idx] : gate_bc_[expert_idx];
if (qlen > 4 * config_.expert_num / config_.num_experts_per_tok) {
amx::mat_mul_kgroup(m, config_.intermediate_size, config_.hidden_size, group_size, ba, bb, bc, ith, nth);
} else {
amx::vec_mul_kgroup(m, config_.intermediate_size, config_.hidden_size, group_size, ba, bb, bc, ith, nth);
}
}
void do_down_gemm(int expert_idx, int ith, int nth, int qlen) {
auto& group_size = config_.quant_config.group_size;
int m = m_local_num_[expert_idx];
if (qlen > 4 * config_.expert_num / config_.num_experts_per_tok) {
amx::mat_mul_kgroup(m, config_.hidden_size, config_.intermediate_size, group_size, down_ba_[expert_idx],
down_bb_[expert_idx], down_bc_[expert_idx], ith, nth);
} else {
amx::vec_mul_kgroup(m, config_.hidden_size, config_.intermediate_size, group_size, down_ba_[expert_idx],
down_bb_[expert_idx], down_bc_[expert_idx], ith, nth);
}
}
void load_weights() {
auto& quant_config = config_.quant_config;
const uint64_t* physical_to_logical_map = (const uint64_t*)config_.physical_to_logical_map;
auto pool = config_.pool->get_subpool(tp_part_idx);
if (quant_config.group_size == 0 || quant_config.zero_point)
throw std::runtime_error("MXFP4 MoE only support KGroup FP4.");
if (config_.gate_scale == nullptr) throw std::runtime_error("MXFP4 MoE only support load native weight.");
int nth = T::recommended_nth(config_.intermediate_size);
pool->do_work_stealing_job(
nth * config_.expert_num, nullptr,
[this, nth, physical_to_logical_map](int task_id) {
uint64_t expert_idx = task_id / nth;
uint64_t logical_expert_id = expert_map(physical_to_logical_map, expert_idx);
int ith = task_id % nth;
gate_bb_[expert_idx]->from_raw_mat(
(uint8_t*)config_.gate_proj +
((logical_expert_id * config_.intermediate_size * config_.hidden_size) >> 1),
ith, nth);
up_bb_[expert_idx]->from_raw_mat(
(uint8_t*)config_.up_proj + ((logical_expert_id * config_.intermediate_size * config_.hidden_size) >> 1),
ith, nth);
},
nullptr);
nth = T::recommended_nth(config_.hidden_size);
pool->do_work_stealing_job(
nth * config_.expert_num, nullptr,
[this, nth, physical_to_logical_map](int task_id) {
uint64_t expert_idx = task_id / nth;
uint64_t logical_expert_id = expert_map(physical_to_logical_map, expert_idx);
int ith = task_id % nth;
down_bb_[expert_idx]->from_raw_mat(
(uint8_t*)config_.down_proj +
((logical_expert_id * config_.hidden_size * config_.intermediate_size) >> 1),
ith, nth);
},
nullptr);
pool->do_work_stealing_job(
config_.expert_num, nullptr,
[this, physical_to_logical_map](int task_id) {
uint64_t expert_idx = task_id;
uint64_t logical_expert_id = expert_map(physical_to_logical_map, expert_idx);
size_t scale_elem_count = (config_.hidden_size * config_.intermediate_size) / config_.quant_config.group_size;
convert_or_copy(gate_bb_[expert_idx]->d,
(ggml_bf16_t*)config_.gate_scale + (logical_expert_id * scale_elem_count), scale_elem_count);
convert_or_copy(up_bb_[expert_idx]->d,
(ggml_bf16_t*)config_.up_scale + (logical_expert_id * scale_elem_count), scale_elem_count);
convert_or_copy(down_bb_[expert_idx]->d,
(ggml_bf16_t*)config_.down_scale + (logical_expert_id * scale_elem_count), scale_elem_count);
},
nullptr);
}
static inline void fast_memcpy(void* __restrict dst, const void* __restrict src, size_t bytes) {
uint8_t* d = (uint8_t*)dst;
const uint8_t* s = (const uint8_t*)src;
size_t chunks = bytes / 64;
for (size_t i = 0; i < chunks; i++) {
__m512i data = _mm512_loadu_si512((__m512i*)s);
_mm512_storeu_si512((__m512i*)d, data);
d += 64;
s += 64;
}
if (bytes -= chunks * 64) std::memcpy(d, s, bytes);
}
static inline void fast_fp32_to_bf16(ggml_bf16_t* __restrict dst, const float* __restrict src, size_t count) {
size_t i = 0;
for (; i + 32 <= count; i += 32) {
__m512 v0 = _mm512_loadu_ps(src + i);
__m512 v1 = _mm512_loadu_ps(src + i + 16);
__m512i i0 = _mm512_srli_epi32(_mm512_castps_si512(v0), 16);
__m512i i1 = _mm512_srli_epi32(_mm512_castps_si512(v1), 16);
__m512i packed = _mm512_packus_epi32(i0, i1);
__m512i permuted = _mm512_permutexvar_epi64(_mm512_set_epi64(7, 5, 3, 1, 6, 4, 2, 0), packed);
_mm512_storeu_si512((__m512i*)(dst + i), permuted);
}
for (; i < count; i++) dst[i] = ggml_fp32_to_bf16(src[i]);
}
void write_weights_to_buffer(int gpu_tp_count, int cpu_tp_count, int expert_id, const GeneralMOEConfig& full_config,
const std::vector<uintptr_t>& w13_weight_ptrs,
const std::vector<uintptr_t>& w13_scale_ptrs,
const std::vector<uintptr_t>& w2_weight_ptrs,
const std::vector<uintptr_t>& w2_scale_ptrs) const {
const int group_size = config_.quant_config.group_size;
auto pool = config_.pool->get_subpool(tp_part_idx);
size_t cpu_tp_weight_elem_count = (size_t)config_.intermediate_size * config_.hidden_size;
size_t cpu_tp_weight_bytes = cpu_tp_weight_elem_count / 2;
size_t cpu_tp_scale_elem_count = cpu_tp_weight_elem_count / group_size;
size_t gpu_tp_weight_elem_count = (size_t)full_config.intermediate_size * full_config.hidden_size / gpu_tp_count;
size_t gpu_tp_weight_bytes = gpu_tp_weight_elem_count / 2;
size_t gpu_tp_scale_elem_count = gpu_tp_weight_elem_count / group_size;
if (cpu_tp_count >= gpu_tp_count) {
int target_gpu_tp = tp_part_idx / (cpu_tp_count / gpu_tp_count);
int local_idx = tp_part_idx % (cpu_tp_count / gpu_tp_count);
uint8_t* w13_weight_dst = (uint8_t*)w13_weight_ptrs[target_gpu_tp];
ggml_bf16_t* w13_scale_dst = (ggml_bf16_t*)w13_scale_ptrs[target_gpu_tp];
uint8_t* w2_weight_dst = (uint8_t*)w2_weight_ptrs[target_gpu_tp];
ggml_bf16_t* w2_scale_dst = (ggml_bf16_t*)w2_scale_ptrs[target_gpu_tp];
size_t offset_in_gpu_weight = local_idx * cpu_tp_weight_bytes;
size_t offset_in_gpu_scale = local_idx * cpu_tp_scale_elem_count;
constexpr int NUM_WEIGHT_TASKS = 8;
constexpr int MIN_COLS_PER_TASK = 128;
int num_down_tasks = std::max(1, (int)config_.hidden_size / MIN_COLS_PER_TASK);
num_down_tasks = std::min(num_down_tasks, 32);
int total_tasks = NUM_WEIGHT_TASKS * 2 + num_down_tasks + 2;
size_t weight_chunk_size = (cpu_tp_weight_bytes + NUM_WEIGHT_TASKS - 1) / NUM_WEIGHT_TASKS;
weight_chunk_size = (weight_chunk_size + 63) & ~63ULL;
pool->do_work_stealing_job(
total_tasks, nullptr,
[&, this, num_down_tasks, expert_id, weight_chunk_size, offset_in_gpu_weight, offset_in_gpu_scale,
gpu_tp_weight_bytes, gpu_tp_scale_elem_count, w13_weight_dst, w13_scale_dst, w2_weight_dst, w2_scale_dst,
group_size](int task_id) {
if (task_id < NUM_WEIGHT_TASKS) {
int chunk_idx = task_id;
size_t start = chunk_idx * weight_chunk_size;
size_t end = std::min(start + weight_chunk_size, cpu_tp_weight_bytes);
if (start < end)
fast_memcpy(w13_weight_dst + offset_in_gpu_weight + start, (uint8_t*)gate_bb_[expert_id]->b + start,
end - start);
} else if (task_id < NUM_WEIGHT_TASKS * 2) {
int chunk_idx = task_id - NUM_WEIGHT_TASKS;
size_t start = chunk_idx * weight_chunk_size;
size_t end = std::min(start + weight_chunk_size, cpu_tp_weight_bytes);
if (start < end)
fast_memcpy(w13_weight_dst + offset_in_gpu_weight + gpu_tp_weight_bytes + start,
(uint8_t*)up_bb_[expert_id]->b + start, end - start);
} else if (task_id < NUM_WEIGHT_TASKS * 2 + num_down_tasks) {
int chunk_idx = task_id - NUM_WEIGHT_TASKS * 2;
size_t cols_per_chunk = (config_.hidden_size + num_down_tasks - 1) / num_down_tasks;
size_t col_start = chunk_idx * cols_per_chunk;
size_t col_end = std::min(col_start + cols_per_chunk, (size_t)config_.hidden_size);
size_t weight_per_col = config_.intermediate_size >> 1;
size_t scale_per_col = config_.intermediate_size / group_size;
size_t gpu_weight_stride = (full_config.intermediate_size / gpu_tp_count) >> 1;
size_t gpu_scale_stride = (full_config.intermediate_size / gpu_tp_count) / group_size;
size_t gpu_weight_slice_offset = local_idx * weight_per_col;
size_t gpu_scale_slice_offset = local_idx * scale_per_col;
for (size_t col = col_start; col < col_end; col++) {
fast_memcpy(w2_weight_dst + col * gpu_weight_stride + gpu_weight_slice_offset,
(uint8_t*)down_bb_[expert_id]->b + col * weight_per_col, weight_per_col);
fast_fp32_to_bf16(w2_scale_dst + col * gpu_scale_stride + gpu_scale_slice_offset,
down_bb_[expert_id]->d + col * scale_per_col, scale_per_col);
}
} else if (task_id == NUM_WEIGHT_TASKS * 2 + num_down_tasks) {
fast_fp32_to_bf16(w13_scale_dst + offset_in_gpu_scale, gate_bb_[expert_id]->d, cpu_tp_scale_elem_count);
} else {
fast_fp32_to_bf16(w13_scale_dst + offset_in_gpu_scale + gpu_tp_scale_elem_count, up_bb_[expert_id]->d,
cpu_tp_scale_elem_count);
}
},
nullptr);
} else {
int gpu_tps_per_cpu_tp = gpu_tp_count / cpu_tp_count;
int start_gpu_tp = tp_part_idx * gpu_tps_per_cpu_tp;
size_t data_per_gpu_tp_weight = cpu_tp_weight_bytes / gpu_tps_per_cpu_tp;
size_t data_per_gpu_tp_scale = cpu_tp_scale_elem_count / gpu_tps_per_cpu_tp;
constexpr int NUM_WEIGHT_TASKS = 8;
constexpr int MIN_COLS_PER_TASK = 128;
int num_down_tasks = std::max(1, (int)config_.hidden_size / MIN_COLS_PER_TASK);
num_down_tasks = std::min(num_down_tasks, 32);
int tasks_per_gpu_tp = NUM_WEIGHT_TASKS * 2 + num_down_tasks + 2;
int total_tasks = tasks_per_gpu_tp * gpu_tps_per_cpu_tp;
size_t weight_chunk_size = (data_per_gpu_tp_weight + NUM_WEIGHT_TASKS - 1) / NUM_WEIGHT_TASKS;
weight_chunk_size = (weight_chunk_size + 63) & ~63ULL;
pool->do_work_stealing_job(
total_tasks, nullptr,
[&, this, gpu_tps_per_cpu_tp, start_gpu_tp, data_per_gpu_tp_weight, data_per_gpu_tp_scale, num_down_tasks,
tasks_per_gpu_tp, expert_id, weight_chunk_size, gpu_tp_weight_bytes, gpu_tp_scale_elem_count,
group_size](int task_id) {
int local_gpu_idx = task_id / tasks_per_gpu_tp;
int task_type = task_id % tasks_per_gpu_tp;
int gpu_tp_idx = start_gpu_tp + local_gpu_idx;
uint8_t* w13_weight_dst = (uint8_t*)w13_weight_ptrs[gpu_tp_idx];
ggml_bf16_t* w13_scale_dst = (ggml_bf16_t*)w13_scale_ptrs[gpu_tp_idx];
uint8_t* w2_weight_dst = (uint8_t*)w2_weight_ptrs[gpu_tp_idx];
ggml_bf16_t* w2_scale_dst = (ggml_bf16_t*)w2_scale_ptrs[gpu_tp_idx];
size_t cpu_offset_weight = local_gpu_idx * data_per_gpu_tp_weight;
size_t cpu_offset_scale = local_gpu_idx * data_per_gpu_tp_scale;
if (task_type < NUM_WEIGHT_TASKS) {
int chunk_idx = task_type;
size_t start = chunk_idx * weight_chunk_size;
size_t end = std::min(start + weight_chunk_size, data_per_gpu_tp_weight);
if (start < end)
fast_memcpy(w13_weight_dst + start, (uint8_t*)gate_bb_[expert_id]->b + cpu_offset_weight + start,
end - start);
} else if (task_type < NUM_WEIGHT_TASKS * 2) {
int chunk_idx = task_type - NUM_WEIGHT_TASKS;
size_t start = chunk_idx * weight_chunk_size;
size_t end = std::min(start + weight_chunk_size, data_per_gpu_tp_weight);
if (start < end)
fast_memcpy(w13_weight_dst + gpu_tp_weight_bytes + start,
(uint8_t*)up_bb_[expert_id]->b + cpu_offset_weight + start, end - start);
} else if (task_type < NUM_WEIGHT_TASKS * 2 + num_down_tasks) {
int chunk_idx = task_type - NUM_WEIGHT_TASKS * 2;
size_t cols_per_chunk = (config_.hidden_size + num_down_tasks - 1) / num_down_tasks;
size_t col_start = chunk_idx * cols_per_chunk;
size_t col_end = std::min(col_start + cols_per_chunk, (size_t)config_.hidden_size);
size_t weight_per_gpu_col = (config_.intermediate_size / gpu_tps_per_cpu_tp) >> 1;
size_t scale_per_gpu_col = (config_.intermediate_size / gpu_tps_per_cpu_tp) / group_size;
for (size_t col = col_start; col < col_end; col++) {
size_t col_offset_weight = (col * config_.intermediate_size / 2) +
(local_gpu_idx * data_per_gpu_tp_weight / config_.hidden_size);
size_t col_offset_scale = (col * (config_.intermediate_size / group_size)) +
(local_gpu_idx * data_per_gpu_tp_scale / config_.hidden_size);
fast_memcpy(w2_weight_dst + col * weight_per_gpu_col,
(uint8_t*)down_bb_[expert_id]->b + col_offset_weight, weight_per_gpu_col);
fast_fp32_to_bf16(w2_scale_dst + col * scale_per_gpu_col, down_bb_[expert_id]->d + col_offset_scale,
scale_per_gpu_col);
}
} else if (task_type == NUM_WEIGHT_TASKS * 2 + num_down_tasks) {
fast_fp32_to_bf16(w13_scale_dst, gate_bb_[expert_id]->d + cpu_offset_scale, data_per_gpu_tp_scale);
} else {
fast_fp32_to_bf16(w13_scale_dst + gpu_tp_scale_elem_count, up_bb_[expert_id]->d + cpu_offset_scale,
data_per_gpu_tp_scale);
}
},
nullptr);
}
}
};
// ============================================================================
// TP_MOE specialization for AMX_FP4_MOE_TP
// ============================================================================
template <typename K>
class TP_MOE<AMX_FP4_MOE_TP<K>> : public TP_MOE<AMX_MOE_BASE<K, AMX_FP4_MOE_TP<K>>> {
public:
using Base = TP_MOE<AMX_MOE_BASE<K, AMX_FP4_MOE_TP<K>>>;
using Base::Base;
void load_weights() override {
auto& config = this->config;
auto& tps = this->tps;
auto& tp_count = this->tp_count;
auto pool = config.pool;
const uint64_t* physical_to_logical_map = (const uint64_t*)config.physical_to_logical_map;
bool use_per_expert_ptrs = !config.gate_projs.empty();
if (config.gate_projs.empty() && config.gate_scale == nullptr)
throw std::runtime_error("MXFP4 MoE only supports Packed FP4 with KGroup Scale");
printf("From %s\n", use_per_expert_ptrs ? "per-expert pointers (gate_projs)" : "Packed FP4 with KGroup Scale");
int& group_size = config.quant_config.group_size;
pool->dispense_backend()->do_numa_job([&, this](int i) {
auto& tpc = tps[i]->config_;
size_t weight_elem_count = tpc.intermediate_size * tpc.hidden_size;
size_t scales_elem_count = (tpc.hidden_size / group_size) * tpc.intermediate_size;
tpc.gate_proj = new uint8_t[(tpc.expert_num * weight_elem_count) / 2];
tpc.up_proj = new uint8_t[(tpc.expert_num * weight_elem_count) / 2];
tpc.down_proj = new uint8_t[(tpc.expert_num * weight_elem_count) / 2];
tpc.gate_scale = new ggml_bf16_t[tpc.expert_num * scales_elem_count];
tpc.up_scale = new ggml_bf16_t[tpc.expert_num * scales_elem_count];
tpc.down_scale = new ggml_bf16_t[tpc.expert_num * scales_elem_count];
if (use_per_expert_ptrs) {
pool->get_subpool(i)->do_work_stealing_job(
tpc.expert_num, nullptr,
[&, i](int expert_id_) {
size_t expert_id = expert_map(physical_to_logical_map, expert_id_);
uint8_t* src_gate = (uint8_t*)config.gate_projs[0][expert_id];
uint8_t* src_up = (uint8_t*)config.up_projs[0][expert_id];
uint8_t* src_down = (uint8_t*)config.down_projs[0][expert_id];
ggml_bf16_t* src_gate_scale = (ggml_bf16_t*)config.gate_scales[0][expert_id];
ggml_bf16_t* src_up_scale = (ggml_bf16_t*)config.up_scales[0][expert_id];
ggml_bf16_t* src_down_scale = (ggml_bf16_t*)config.down_scales[0][expert_id];
memcpy((uint8_t*)tpc.gate_proj + ((expert_id * weight_elem_count) >> 1),
src_gate + ((i * weight_elem_count) >> 1), (weight_elem_count >> 1));
memcpy((uint8_t*)tpc.up_proj + ((expert_id * weight_elem_count) >> 1),
src_up + ((i * weight_elem_count) >> 1), (weight_elem_count >> 1));
memcpy((ggml_bf16_t*)tpc.gate_scale + (expert_id * scales_elem_count),
src_gate_scale + (i * scales_elem_count), sizeof(ggml_bf16_t) * scales_elem_count);
memcpy((ggml_bf16_t*)tpc.up_scale + (expert_id * scales_elem_count),
src_up_scale + (i * scales_elem_count), sizeof(ggml_bf16_t) * scales_elem_count);
for (size_t col = 0; col < config.hidden_size; col++) {
memcpy((uint8_t*)tpc.down_proj + ((expert_id * weight_elem_count + col * tpc.intermediate_size) >> 1),
src_down + ((col * config.intermediate_size + i * tpc.intermediate_size) >> 1),
(tpc.intermediate_size >> 1));
memcpy((ggml_bf16_t*)tpc.down_scale +
(expert_id * scales_elem_count + col * (tpc.intermediate_size / group_size)),
src_down_scale +
(col * (config.intermediate_size / group_size) + i * (tpc.intermediate_size / group_size)),
sizeof(ggml_bf16_t) * (tpc.intermediate_size / group_size));
}
},
nullptr);
} else {
if (tpc.load == false) {
pool->get_subpool(i)->do_work_stealing_job(
tpc.expert_num, nullptr,
[&, i](int expert_id_) {
size_t expert_id = expert_map(physical_to_logical_map, expert_id_);
memcpy((uint8_t*)tpc.gate_proj + ((expert_id * weight_elem_count) >> 1),
(uint8_t*)config.gate_proj +
((expert_id * config.intermediate_size * config.hidden_size + i * weight_elem_count) >> 1),
(weight_elem_count >> 1));
memcpy((uint8_t*)tpc.up_proj + ((expert_id * weight_elem_count) >> 1),
(uint8_t*)config.up_proj +
((expert_id * config.intermediate_size * config.hidden_size + i * weight_elem_count) >> 1),
(weight_elem_count >> 1));
memcpy((ggml_bf16_t*)tpc.gate_scale + (expert_id * scales_elem_count),
(ggml_bf16_t*)config.gate_scale +
(expert_id * (config.hidden_size / group_size) * config.intermediate_size +
i * scales_elem_count),
sizeof(ggml_bf16_t) * scales_elem_count);
memcpy((ggml_bf16_t*)tpc.up_scale + (expert_id * scales_elem_count),
(ggml_bf16_t*)config.up_scale +
(expert_id * (config.hidden_size / group_size) * config.intermediate_size +
i * scales_elem_count),
sizeof(ggml_bf16_t) * scales_elem_count);
for (size_t col = 0; col < config.hidden_size; col++) {
memcpy((uint8_t*)tpc.down_proj + ((expert_id * weight_elem_count + col * tpc.intermediate_size) >> 1),
(uint8_t*)config.down_proj + ((expert_id * config.intermediate_size * config.hidden_size +
col * config.intermediate_size + i * tpc.intermediate_size) >>
1),
(tpc.intermediate_size >> 1));
memcpy((ggml_bf16_t*)tpc.down_scale +
(expert_id * scales_elem_count + col * (tpc.intermediate_size / group_size)),
(ggml_bf16_t*)config.down_scale +
((expert_id * (config.intermediate_size / group_size) * config.hidden_size) +
col * (config.intermediate_size / group_size) + i * (tpc.intermediate_size / group_size)),
sizeof(ggml_bf16_t) * (tpc.intermediate_size / group_size));
}
},
nullptr);
}
}
printf("TP %d load weight done.\n", i);
});
DO_TPS_LOAD_WEIGHTS(pool);
pool->dispense_backend()->do_numa_job([&, this](int i) {
auto& tpc = tps[i]->config_;
delete[] (uint8_t*)(tpc.gate_proj);
delete[] (uint8_t*)(tpc.up_proj);
delete[] (uint8_t*)(tpc.down_proj);
delete[] (ggml_bf16_t*)(tpc.gate_scale);
delete[] (ggml_bf16_t*)(tpc.up_scale);
delete[] (ggml_bf16_t*)(tpc.down_scale);
});
this->weights_loaded = true;
}
void write_weight_scale_to_buffer(int gpu_tp_count, int expert_id, const std::vector<uintptr_t>& w13_weight_ptrs,
const std::vector<uintptr_t>& w13_scale_ptrs,
const std::vector<uintptr_t>& w2_weight_ptrs,
const std::vector<uintptr_t>& w2_scale_ptrs) {
if (!this->weights_loaded) throw std::runtime_error("Not Loaded");
if (this->tps.empty()) throw std::runtime_error("No TP parts initialized");
if (w13_weight_ptrs.size() != gpu_tp_count || w13_scale_ptrs.size() != gpu_tp_count ||
w2_weight_ptrs.size() != gpu_tp_count || w2_scale_ptrs.size() != gpu_tp_count)
throw std::runtime_error("Pointer arrays size must match gpu_tp_count");
this->config.pool->dispense_backend()->do_numa_job([&, this](int i) {
this->tps[i]->write_weights_to_buffer(gpu_tp_count, this->tp_count, expert_id, this->config, w13_weight_ptrs,
w13_scale_ptrs, w2_weight_ptrs, w2_scale_ptrs);
});
}
};
#endif // CPUINFER_OPERATOR_AMX_FP4_MOE_H

View file

@ -81,6 +81,26 @@ BUILTIN_MODELS: list[ModelInfo] = [
description="DeepSeek R1-0528 reasoning model (May 2025, improved reasoning depth)",
description_zh="DeepSeek R1-0528 推理模型2025年5月改进的推理深度",
),
ModelInfo(
name="DeepSeek-V4-Flash",
hf_repo="deepseek-ai/DeepSeek-V4-Flash",
aliases=["deepseek-v4-flash", "deepseek-v4", "dsv4", "v4-flash", "v4"],
type="moe",
default_params={
"kt-method": "MXFP4",
"kt-gpu-prefill-token-threshold": 4096,
"attention-backend": "flashinfer",
"max-total-tokens": 100000,
"max-running-requests": 16,
"chunked-prefill-size": 32768,
"mem-fraction-static": 0.80,
"watchdog-timeout": 3000,
"served-model-name": "DeepSeek-V4-Flash",
"disable-shared-experts-fusion": True,
},
description="DeepSeek V4-Flash MoE model (native MXFP4 experts, MQA + sparse index attention)",
description_zh="DeepSeek V4-Flash MoE 模型(原生 MXFP4 专家MQA + 稀疏索引注意力)",
),
ModelInfo(
name="Kimi-K2-Thinking",
hf_repo="moonshotai/Kimi-K2-Thinking",
@ -368,6 +388,19 @@ def compute_deepseek_v3_gpu_experts(tensor_parallel_size: int, vram_per_gpu_gb:
return total_vram // 3
def compute_deepseek_v4_gpu_experts(tensor_parallel_size: int, vram_per_gpu_gb: float) -> int:
"""Compute kt-num-gpu-experts for DeepSeek-V4-Flash.
V4 uses MXFP4 experts (~0.5 bytes/param vs V3 FP8's 1 byte/param) so each GPU
can hold ~2x more experts per VRAM unit than V3 at the same fragmentation.
"""
per_gpu_gb = 16
if vram_per_gpu_gb < per_gpu_gb:
return 0
total_vram = int(tensor_parallel_size * (vram_per_gpu_gb - per_gpu_gb))
return total_vram * 2 // 3
def compute_kimi_k2_thinking_gpu_experts(tensor_parallel_size: int, vram_per_gpu_gb: float) -> int:
"""Compute kt-num-gpu-experts for Kimi K2 Thinking."""
per_gpu_gb = 16
@ -393,6 +426,7 @@ MODEL_COMPUTE_FUNCTIONS: dict[str, Callable[[int, float], int]] = {
"DeepSeek-V3-0324": compute_deepseek_v3_gpu_experts,
"DeepSeek-V3.2": compute_deepseek_v3_gpu_experts, # Same as V3-0324
"DeepSeek-R1-0528": compute_deepseek_v3_gpu_experts, # Same as V3-0324
"DeepSeek-V4-Flash": compute_deepseek_v4_gpu_experts,
"Kimi-K2-Thinking": compute_kimi_k2_thinking_gpu_experts,
"MiniMax-M2": compute_minimax_m2_gpu_experts,
"MiniMax-M2.1": compute_minimax_m2_gpu_experts, # Same as M2

View file

@ -40,6 +40,7 @@ INFERENCE_METHODS = frozenset(
"BF16", # BF16 native MoE
"FP8_PERCHANNEL", # Per-channel FP8
"GPTQ_INT4", # GPTQ INT4
"MXFP4", # MXFP4 (E2M1 nibble + ue8m0 group scale, e.g. DeepSeek-V4-Flash routed experts)
"LLAMAFILE", # GGUF format
"MOE_INT4",
"MOE_INT8", # General kernel
@ -312,7 +313,7 @@ def _create_inference_wrapper(
# Select backend based on method
if method in ["AMXINT4", "AMXINT8"]:
backend_cls = AMXMoEWrapper
elif method in ["RAWINT4", "FP8", "BF16", "FP8_PERCHANNEL", "GPTQ_INT4"]:
elif method in ["RAWINT4", "FP8", "BF16", "FP8_PERCHANNEL", "GPTQ_INT4", "MXFP4"]:
backend_cls = NativeMoEWrapper
elif method == "LLAMAFILE":
backend_cls = LlamafileMoEWrapper

View file

@ -11,6 +11,7 @@ from .loader import (
FP8SafeTensorLoader,
BF16SafeTensorLoader,
GPTQSafeTensorLoader,
MXFP4SafeTensorLoader,
)
from kt_kernel_ext.moe import MOEConfig
import kt_kernel_ext.moe as _moe_mod
@ -18,6 +19,7 @@ import kt_kernel_ext.moe as _moe_mod
AMXInt4_MOE = getattr(_moe_mod, "AMXInt4_MOE", None)
AMXInt8_MOE = getattr(_moe_mod, "AMXInt8_MOE", None)
AMXInt4_KGroup_MOE = getattr(_moe_mod, "AMXInt4_KGroup_MOE", None)
AMXFP4_KGroup_MOE = getattr(_moe_mod, "AMXFP4_KGroup_MOE", None)
AMXFP8_MOE = getattr(_moe_mod, "AMXFP8_MOE", None)
AMXBF16_MOE = getattr(_moe_mod, "AMXBF16_MOE", None)
AMXFP8PerChannel_MOE = getattr(_moe_mod, "AMXFP8PerChannel_MOE", None)
@ -31,6 +33,7 @@ AVXVNNI256RawInt4_MOE = getattr(_moe_mod, "AVXVNNI256RawInt4_MOE", None)
_HAS_AMXINT4_SUPPORT = AMXInt4_MOE is not None
_HAS_AMXINT8_SUPPORT = AMXInt8_MOE is not None
_HAS_RAWINT4_SUPPORT = AMXInt4_KGroup_MOE is not None
_HAS_MXFP4_SUPPORT = AMXFP4_KGroup_MOE is not None
_HAS_FP8_SUPPORT = AMXFP8_MOE is not None
_HAS_BF16_SUPPORT = AMXBF16_MOE is not None
_HAS_FP8_PERCHANNEL_SUPPORT = AMXFP8PerChannel_MOE is not None
@ -495,6 +498,12 @@ class NativeMoEWrapper(BaseMoEWrapper):
"Please recompile kt_kernel_ext with GPTQ INT4 support enabled.\n"
"AVX-VNNI-256 will be selected automatically when available on the current CPU."
)
if method == "MXFP4" and not _HAS_MXFP4_SUPPORT:
raise RuntimeError(
"MXFP4 backend not available. Required ISA:\n"
" - AVX512F + AVX512BW + AVX512_BF16\n"
"Please recompile kt_kernel_ext with AVX512 + BF16 enabled."
)
super().__init__(
layer_idx=layer_idx,
@ -525,6 +534,8 @@ class NativeMoEWrapper(BaseMoEWrapper):
NativeMoEWrapper._native_loader_instance = BF16SafeTensorLoader(weight_path)
elif method == "GPTQ_INT4":
NativeMoEWrapper._native_loader_instance = GPTQSafeTensorLoader(weight_path)
elif method == "MXFP4":
NativeMoEWrapper._native_loader_instance = MXFP4SafeTensorLoader(weight_path)
else:
raise NotImplementedError(f"Unsupported method for NativeMoEWrapper: {method}")
self.loader = NativeMoEWrapper._native_loader_instance
@ -592,6 +603,10 @@ class NativeMoEWrapper(BaseMoEWrapper):
self.up_scales = [t.to(torch.float32).contiguous() for t in weights["up_scale"]]
self.down_scales = [t.to(torch.float32).contiguous() for t in weights["down_scale"]]
assert self.gate_scales[0].dtype == torch.float32, "Expected float32 scales for FP8_PERCHANNEL"
elif self.method == "MXFP4":
# ue8m0 is losslessly representable in bf16 (8-bit exponent, 0 mantissa);
# the loader has already done that conversion.
assert self.gate_scales[0].dtype == torch.bfloat16, "Expected bf16 scales for MXFP4"
t2 = time.time()
@ -649,6 +664,14 @@ class NativeMoEWrapper(BaseMoEWrapper):
f"{_AVXVNNI256_RAW_INT4_MAX_GROUP_SIZE}; AVX2 (AVX2RawInt4_MOE) is used as the final fallback."
)
self.moe = backend_cls(moe_config)
elif self.method == "MXFP4":
# MXFP4: E2M1 nibble-packed weights, ue8m0/bf16 per-32 group scale
# (e.g. DeepSeek-V4-Flash routed experts)
group_size = self.hidden_size // self.gate_scales[0].shape[1]
moe_config.quant_config.bits = 4
moe_config.quant_config.group_size = group_size
moe_config.quant_config.zero_point = False
self.moe = AMXFP4_KGroup_MOE(moe_config)
elif self.method == "FP8":
moe_config.quant_config.bits = 8
moe_config.quant_config.group_size = 128

View file

@ -1231,3 +1231,91 @@ class GPTQSafeTensorLoader(FP8SafeTensorLoader):
"up_scale": up_scales,
"down_scale": down_scales,
}
class MXFP4SafeTensorLoader(SafeTensorLoader):
"""Loader for native MXFP4 expert weights (DeepSeek-V4-Flash format).
Per expert layout:
{base}.ffn.experts.{i}.w1.weight I8 [N, K/2] nibble-packed E2M1 (gate)
{base}.ffn.experts.{i}.w1.scale F8_E8M0 [N, K/32] ue8m0 group scale
{base}.ffn.experts.{i}.w3.{weight,scale} up
{base}.ffn.experts.{i}.w2.{weight,scale} down
V4 ckpt keys are not prefixed with ``model.``; we also probe the stripped form so
callers can keep passing ``base_key="model.layers.{L}"``. ue8m0 bf16 is a lossless
bit shift (both have an 8-bit exponent and zero mantissa for ue8m0), and the AMX
FP4 backend already consumes bf16 scales.
"""
EXPERTS_PATH_TPL = "{base}.ffn.experts"
PROJ_NAMES = ("w1", "w3", "w2") # (gate, up, down)
def _experts_prefix_candidates(self, base_key: str) -> list[str]:
candidates = [self.EXPERTS_PATH_TPL.format(base=base_key)]
if base_key.startswith("model."):
candidates.append(self.EXPERTS_PATH_TPL.format(base=base_key[len("model.") :]))
return list(dict.fromkeys(candidates))
@staticmethod
def _ue8m0_to_bf16(scale_t: torch.Tensor) -> torch.Tensor:
if scale_t.dtype != torch.uint8:
scale_t = scale_t.view(torch.uint8)
# bf16 = [sign(1) | exp(8) | mant(7)]; setting mant=0, exp=e gives 2^(e-127),
# which is exactly the value encoded by ue8m0 for e ∈ [1, 254]. e=0 → bf16 +0
# (acceptable: ue8m0=0 represents 2^-127, below bf16 normal range), e=255 → +inf.
# Compute in int32 then narrow to int16 (max value is 255<<7=32640, fits int16),
# because torch CPU has no lshift kernel for uint16.
return (scale_t.to(torch.int32) << 7).to(torch.int16).view(torch.bfloat16).contiguous()
def load_experts(self, base_key: str, device: str = "cpu"):
gate_name, up_name, down_name = self.PROJ_NAMES
prefix = None
expert_count = 0
for cand in self._experts_prefix_candidates(base_key):
expert_count = 0
while self.has_tensor(f"{cand}.{expert_count}.{gate_name}.weight"):
expert_count += 1
if expert_count > 0:
prefix = cand
break
if prefix is None:
raise ValueError(
f"No MXFP4 experts found under any of: {self._experts_prefix_candidates(base_key)}"
)
gate_weights = [None] * expert_count
up_weights = [None] * expert_count
down_weights = [None] * expert_count
gate_scales = [None] * expert_count
up_scales = [None] * expert_count
down_scales = [None] * expert_count
for exp_id in range(expert_count):
for proj, dst in (
(gate_name, gate_weights),
(up_name, up_weights),
(down_name, down_weights),
):
w = self.load_tensor(f"{prefix}.{exp_id}.{proj}.weight", device).contiguous()
if w.dtype != torch.uint8:
w = w.view(torch.uint8)
dst[exp_id] = w
for proj, dst in (
(gate_name, gate_scales),
(up_name, up_scales),
(down_name, down_scales),
):
s = self.load_tensor(f"{prefix}.{exp_id}.{proj}.scale", device)
dst[exp_id] = self._ue8m0_to_bf16(s)
print(f"[MXFP4SafeTensorLoader] Loaded {expert_count} experts from {prefix}")
return {
"gate": gate_weights,
"up": up_weights,
"down": down_weights,
"gate_scale": gate_scales,
"up_scale": up_scales,
"down_scale": down_scales,
}

2
third_party/sglang vendored

@ -1 +1 @@
Subproject commit 537eb762b0881071a0e098bd78666fe052b83deb
Subproject commit 3cbd49c291c5d1b968c81308647de34134790e07