kernels: MXFP4 -> BF16 dequant kernel (verified vs numpy)

From-scratch CUDA kernel for gpt-oss expert weights: one thread per packed
byte decodes 2 FP4 (E2M1) codes, applies the per-32-block E8M0 scale
(2^(e-127)), and writes BF16 transposed into [IN, OUT] (IN = nblk*32) so it
drops straight into x @ W. dequant_mxfp4() wrapper takes raw GpuBuffers
(uint8 is not an xserv Tensor dtype). mxfp4-check bin dequants layer-0
expert-0 on GPU and matches tools/mxfp4_probe.py exactly:
  [0, 0, 0, -0.0625, 0, -0, -0.015625, -0.03125]

This lets experts stay MXFP4-resident on GPU (13GB, fits one 32GB card)
and be dequantized to a BF16 scratch right before each expert GEMM, instead
of holding 36GB of BF16 or uploading experts per token. Loader plumbing +
GPU MoE decode use it next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 21:38:52 +08:00
parent 403879959a
commit 7ebdd7c552
4 changed files with 171 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ pub use attention::{attention, decode_attention, flash_attention, paged_decode_a
pub use embedding::embedding;
pub use gemm::{batched_matmul, matmul, GemmBackend};
pub use layernorm::layernorm;
pub use quant::dequant_mxfp4;
pub use rmsnorm::{add_rmsnorm, rmsnorm};
pub use rope::{rope_inplace, RopeCache};
pub use softmax::softmax;

View File

@@ -0,0 +1,38 @@
//! MXFP4 dequantization (gpt-oss expert weights) -> BF16.
//!
//! The packed weights are uint8 (blocks + E8M0 scales), which is not an xserv
//! `Tensor` dtype, so the input is raw `GpuBuffer`s; the output is a BF16
//! `Tensor` in `[IN, OUT]` layout (IN = nblk*32), ready for `x @ W`.
use std::ffi::c_void;
use xserv_cuda::GpuBuffer;
use xserv_tensor::{DType, Device, Tensor};
unsafe extern "C" {
fn launch_mxfp4_dequant_bf16(
blocks: *const c_void, scales: *const c_void, out: *mut c_void,
out_dim: i32, nblk: i32, stream: *mut c_void,
);
}
/// Dequantize one expert's MXFP4 weight to a BF16 `[IN, OUT]` tensor
/// (IN = `nblk*32`), the transpose of the natural `[OUT, IN]`, so it drops into
/// `y = x[1,IN] @ W[IN,OUT]` with no extra transpose.
///
/// `blocks`: uint8 device buffer of `out_dim * nblk * 16` bytes.
/// `scales`: uint8 device buffer of `out_dim * nblk` bytes (E8M0 exponents).
pub fn dequant_mxfp4(
blocks: &GpuBuffer, scales: &GpuBuffer, out_dim: usize, nblk: usize, device: u32,
) -> Tensor {
let in_dim = nblk * 32;
let out = Tensor::empty(&[in_dim, out_dim], DType::BF16, Device::Cuda(device));
unsafe {
launch_mxfp4_dequant_bf16(
blocks.as_ptr() as *const c_void,
scales.as_ptr() as *const c_void,
out.data_ptr() as *mut c_void,
out_dim as i32, nblk as i32, std::ptr::null_mut(),
);
}
out
}