batched_gemm_fp8 rebuilt the cuBLASLt matmul descriptor, four matrix layouts, a preference, and a 4-byte scale alloc, AND ran the algo heuristic search — once per expert, per GEMM, per layer, on every forward (~1500 heuristic searches per decoded token). FP8 decode ran at 27.0 ms/tok vs BF16 18.8 ms, i.e. slower than the path it was meant to accelerate. Cache the full plan (descriptor + layouts + heuristically-chosen algo) in a thread-local map keyed by (M, N, K) so the heuristic runs once per shape and is reused across experts and forwards; allocate the 1.0 scale buffer once; pass each expert's weight scale via the cuBLASLt B-scale device pointer instead of folding it into alpha (identical FP32-epilogue precision, and no host readback of b_scales). The per-expert loop now issues only cublasLtMatmul. Measured on dash5 (gpt-oss-20b, TP=2, 5090): FP8 decode TPOT 27.0 -> 17.9 ms, now faster than BF16 (18.8 ms); GSM8K-200 accuracy unchanged (FP8 93.0% vs BF16 90.5%, within noise). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
428 lines
16 KiB
Rust
428 lines
16 KiB
Rust
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::ffi::c_void;
|
|
use xserv_cuda::GpuBuffer;
|
|
use xserv_tensor::{DType, Tensor};
|
|
|
|
// ============================================================
|
|
// FFI: custom CUDA kernels
|
|
// ============================================================
|
|
|
|
unsafe extern "C" {
|
|
fn launch_dequant_fp8e4m3_to_bf16(
|
|
src: *const c_void,
|
|
scales: *const c_void,
|
|
dst: *mut c_void,
|
|
num_experts: i32, rows: i32, cols: i32,
|
|
stream: *mut c_void,
|
|
);
|
|
fn launch_quantize_bf16_to_fp8e4m3_rowwise(
|
|
src: *const c_void,
|
|
dst: *mut c_void,
|
|
scales: *mut c_void,
|
|
num_rows: i32, cols: i32,
|
|
stream: *mut c_void,
|
|
);
|
|
fn launch_rowwise_scale_bf16(
|
|
data: *mut c_void,
|
|
scales: *const c_void,
|
|
num_rows: i32, cols: i32,
|
|
stream: *mut c_void,
|
|
);
|
|
}
|
|
|
|
// ============================================================
|
|
// FFI: cuBLASLt
|
|
// ============================================================
|
|
|
|
type CublasLtHandle = *mut c_void;
|
|
type CublasLtMatmulDesc = *mut c_void;
|
|
type CublasLtMatrixLayout = *mut c_void;
|
|
type CublasLtMatmulPreference = *mut c_void;
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
struct CublasLtMatmulAlgo {
|
|
data: [u64; 8],
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct CublasLtMatmulHeuristicResult {
|
|
algo: CublasLtMatmulAlgo,
|
|
workspace_size: usize,
|
|
state: i32,
|
|
_reserved: [f32; 4],
|
|
}
|
|
|
|
unsafe extern "C" {
|
|
fn cublasLtCreate(handle: *mut CublasLtHandle) -> i32;
|
|
fn cublasLtDestroy(handle: CublasLtHandle) -> i32;
|
|
fn cublasLtMatmulDescCreate(desc: *mut CublasLtMatmulDesc, compute_type: i32, scale_type: i32) -> i32;
|
|
fn cublasLtMatmulDescDestroy(desc: CublasLtMatmulDesc) -> i32;
|
|
fn cublasLtMatmulDescSetAttribute(desc: CublasLtMatmulDesc, attr: i32, buf: *const c_void, size: usize) -> i32;
|
|
fn cublasLtMatrixLayoutCreate(layout: *mut CublasLtMatrixLayout, dtype: i32, rows: u64, cols: u64, ld: i64) -> i32;
|
|
fn cublasLtMatrixLayoutDestroy(layout: CublasLtMatrixLayout) -> i32;
|
|
fn cublasLtMatrixLayoutSetAttribute(layout: CublasLtMatrixLayout, attr: i32, buf: *const c_void, size: usize) -> i32;
|
|
fn cublasLtMatmulPreferenceCreate(pref: *mut CublasLtMatmulPreference) -> i32;
|
|
fn cublasLtMatmulPreferenceDestroy(pref: CublasLtMatmulPreference) -> i32;
|
|
fn cublasLtMatmulPreferenceSetAttribute(pref: CublasLtMatmulPreference, attr: i32, buf: *const c_void, size: usize) -> i32;
|
|
fn cublasLtMatmulAlgoGetHeuristic(
|
|
handle: CublasLtHandle, desc: CublasLtMatmulDesc,
|
|
a_layout: CublasLtMatrixLayout, b_layout: CublasLtMatrixLayout,
|
|
c_layout: CublasLtMatrixLayout, d_layout: CublasLtMatrixLayout,
|
|
pref: CublasLtMatmulPreference,
|
|
requested: i32,
|
|
results: *mut CublasLtMatmulHeuristicResult,
|
|
found: *mut i32,
|
|
) -> i32;
|
|
fn cublasLtMatmul(
|
|
handle: CublasLtHandle, desc: CublasLtMatmulDesc,
|
|
alpha: *const c_void,
|
|
a: *const c_void, a_layout: CublasLtMatrixLayout,
|
|
b: *const c_void, b_layout: CublasLtMatrixLayout,
|
|
beta: *const c_void,
|
|
c: *const c_void, c_layout: CublasLtMatrixLayout,
|
|
d: *mut c_void, d_layout: CublasLtMatrixLayout,
|
|
algo: *const CublasLtMatmulAlgo,
|
|
workspace: *mut c_void, workspace_size: usize,
|
|
stream: *mut c_void,
|
|
) -> i32;
|
|
}
|
|
|
|
// cuBLASLt constants
|
|
const CUBLAS_COMPUTE_32F: i32 = 68;
|
|
const CUDA_R_32F: i32 = 0;
|
|
const CUDA_R_16BF: i32 = 14;
|
|
const CUDA_R_8F_E4M3: i32 = 28;
|
|
|
|
// MatmulDesc attributes
|
|
const CUBLASLT_MATMUL_DESC_A_SCALE_POINTER: i32 = 17;
|
|
const CUBLASLT_MATMUL_DESC_B_SCALE_POINTER: i32 = 18;
|
|
const CUBLASLT_MATMUL_DESC_A_SCALE_MODE: i32 = 31;
|
|
const CUBLASLT_MATMUL_DESC_B_SCALE_MODE: i32 = 32;
|
|
|
|
// MatrixLayout attributes
|
|
const CUBLASLT_MATRIX_LAYOUT_BATCH_COUNT: i32 = 5;
|
|
const CUBLASLT_MATRIX_LAYOUT_STRIDED_BATCH_OFFSET: i32 = 6;
|
|
|
|
// Scale modes
|
|
const CUBLASLT_MATMUL_MATRIX_SCALE_SCALAR: i32 = 0;
|
|
const CUBLASLT_MATMUL_MATRIX_SCALE_OUTER_VEC_32F: i32 = 3;
|
|
|
|
// MatmulPreference attributes
|
|
const CUBLASLT_MATMUL_PREF_MAX_WORKSPACE_BYTES: i32 = 1;
|
|
|
|
const WORKSPACE_BYTES: usize = 32 * 1024 * 1024;
|
|
|
|
const CUBLASLT_MATMUL_DESC_TRANSA: i32 = 3;
|
|
|
|
/// A fully-prepared FP8 matmul plan for one (M, N, K) shape: the matmul
|
|
/// descriptor, the four matrix layouts, and the heuristically-chosen algo.
|
|
/// Built once per shape and reused across every expert and every forward
|
|
/// pass — the heuristic search and descriptor/layout creation are the
|
|
/// expensive parts, so doing them once instead of per-expert-per-layer is
|
|
/// the difference between FP8 being faster or slower than BF16.
|
|
#[derive(Clone, Copy)]
|
|
struct Fp8Plan {
|
|
desc: CublasLtMatmulDesc,
|
|
a_layout: CublasLtMatrixLayout,
|
|
b_layout: CublasLtMatrixLayout,
|
|
c_layout: CublasLtMatrixLayout,
|
|
d_layout: CublasLtMatrixLayout,
|
|
algo: CublasLtMatmulAlgo,
|
|
workspace_size: usize,
|
|
}
|
|
|
|
struct CublasLtContext {
|
|
handle: CublasLtHandle,
|
|
workspace: GpuBuffer,
|
|
/// Persistent device scalar holding 1.0, used as the A/B scale pointer
|
|
/// placeholder. Allocated once instead of per-expert.
|
|
one_buf: GpuBuffer,
|
|
/// Cache of prepared matmul plans keyed by (M, N, K).
|
|
plans: HashMap<(usize, usize, usize), Fp8Plan>,
|
|
}
|
|
|
|
impl CublasLtContext {
|
|
fn new() -> Self {
|
|
let mut handle = std::ptr::null_mut();
|
|
let status = unsafe { cublasLtCreate(&mut handle) };
|
|
assert_eq!(status, 0, "cublasLtCreate failed: {status}");
|
|
let workspace = GpuBuffer::alloc(WORKSPACE_BYTES).expect("alloc cublasLt workspace");
|
|
let mut one_buf = GpuBuffer::alloc(4).expect("alloc cublasLt fp8 scale");
|
|
one_buf.copy_from_host(&1.0f32.to_le_bytes()).expect("init fp8 scale");
|
|
Self { handle, workspace, one_buf, plans: HashMap::new() }
|
|
}
|
|
|
|
/// Get the cached plan for (m, n, k), building (and caching) it on first use.
|
|
fn plan(&mut self, m: usize, n: usize, k: usize) -> Fp8Plan {
|
|
if let Some(p) = self.plans.get(&(m, n, k)) {
|
|
return *p;
|
|
}
|
|
let one_ptr = self.one_buf.as_ptr() as *const c_void;
|
|
let plan = unsafe { build_fp8_plan(self.handle, one_ptr, m, n, k) };
|
|
self.plans.insert((m, n, k), plan);
|
|
plan
|
|
}
|
|
}
|
|
|
|
impl Drop for CublasLtContext {
|
|
fn drop(&mut self) {
|
|
// Tear down cached plans before destroying the handle.
|
|
for (_, p) in self.plans.drain() {
|
|
unsafe {
|
|
cublasLtMatrixLayoutDestroy(p.a_layout);
|
|
cublasLtMatrixLayoutDestroy(p.b_layout);
|
|
cublasLtMatrixLayoutDestroy(p.c_layout);
|
|
cublasLtMatrixLayoutDestroy(p.d_layout);
|
|
cublasLtMatmulDescDestroy(p.desc);
|
|
}
|
|
}
|
|
if !self.handle.is_null() {
|
|
unsafe { cublasLtDestroy(self.handle) };
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build an FP8 matmul plan for one (m, n, k) shape. See `batched_gemm_fp8`
|
|
/// for the row-major → cuBLASLt col-major layout mapping (transA=T, transB=N,
|
|
/// m_lt=N, n_lt=M, k_lt=K). The B-scale pointer is initialised to `one_ptr`
|
|
/// and overwritten per-expert at call time.
|
|
unsafe fn build_fp8_plan(
|
|
handle: CublasLtHandle,
|
|
one_ptr: *const c_void,
|
|
m: usize,
|
|
n: usize,
|
|
k: usize,
|
|
) -> Fp8Plan {
|
|
let m_lt = n as u64;
|
|
let n_lt = m as u64;
|
|
let k_lt = k as u64;
|
|
|
|
let mut desc: CublasLtMatmulDesc = std::ptr::null_mut();
|
|
cublasLtMatmulDescCreate(&mut desc, CUBLAS_COMPUTE_32F, CUDA_R_32F);
|
|
|
|
// transA=T (required for FP8 on Blackwell)
|
|
let trans_a: i32 = 1;
|
|
cublasLtMatmulDescSetAttribute(desc, CUBLASLT_MATMUL_DESC_TRANSA, &trans_a as *const i32 as _, 4);
|
|
let ptr_sz = std::mem::size_of::<*const c_void>();
|
|
cublasLtMatmulDescSetAttribute(desc, CUBLASLT_MATMUL_DESC_A_SCALE_POINTER, &one_ptr as *const _ as _, ptr_sz);
|
|
cublasLtMatmulDescSetAttribute(desc, CUBLASLT_MATMUL_DESC_B_SCALE_POINTER, &one_ptr as *const _ as _, ptr_sz);
|
|
|
|
// "A" layout (weights, transposed): physical (K, N) col-major, ld=K
|
|
let mut a_layout: CublasLtMatrixLayout = std::ptr::null_mut();
|
|
cublasLtMatrixLayoutCreate(&mut a_layout, CUDA_R_8F_E4M3, k_lt, m_lt, k as i64);
|
|
// "B" layout (activations): physical (K, M) col-major, ld=K
|
|
let mut b_layout: CublasLtMatrixLayout = std::ptr::null_mut();
|
|
cublasLtMatrixLayoutCreate(&mut b_layout, CUDA_R_8F_E4M3, k_lt, n_lt, k as i64);
|
|
// "C"/"D" layout (output): physical (N, M) col-major, ld=N
|
|
let mut c_layout: CublasLtMatrixLayout = std::ptr::null_mut();
|
|
cublasLtMatrixLayoutCreate(&mut c_layout, CUDA_R_16BF, m_lt, n_lt, m_lt as i64);
|
|
let mut d_layout: CublasLtMatrixLayout = std::ptr::null_mut();
|
|
cublasLtMatrixLayoutCreate(&mut d_layout, CUDA_R_16BF, m_lt, n_lt, m_lt as i64);
|
|
|
|
let mut pref: CublasLtMatmulPreference = std::ptr::null_mut();
|
|
cublasLtMatmulPreferenceCreate(&mut pref);
|
|
let ws_bytes = WORKSPACE_BYTES as u64;
|
|
cublasLtMatmulPreferenceSetAttribute(pref, CUBLASLT_MATMUL_PREF_MAX_WORKSPACE_BYTES, &ws_bytes as *const u64 as _, 8);
|
|
|
|
let mut heuristic = std::mem::zeroed::<CublasLtMatmulHeuristicResult>();
|
|
let mut found: i32 = 0;
|
|
let status = cublasLtMatmulAlgoGetHeuristic(
|
|
handle, desc, a_layout, b_layout, c_layout, d_layout,
|
|
pref, 1, &mut heuristic, &mut found,
|
|
);
|
|
assert!(status == 0 && found > 0,
|
|
"cublasLtMatmulAlgoGetHeuristic failed for FP8 GEMM (m={m}, n={n}, k={k}): status={status}, found={found}");
|
|
cublasLtMatmulPreferenceDestroy(pref);
|
|
|
|
Fp8Plan {
|
|
desc, a_layout, b_layout, c_layout, d_layout,
|
|
algo: heuristic.algo,
|
|
workspace_size: heuristic.workspace_size,
|
|
}
|
|
}
|
|
|
|
thread_local! {
|
|
static CUBLASLT_CTX: RefCell<CublasLtContext> = RefCell::new(CublasLtContext::new());
|
|
}
|
|
|
|
// ============================================================
|
|
// Public API
|
|
// ============================================================
|
|
|
|
/// Dequantize a 3D FP8 E4M3 tensor to BF16 using per-expert FP32 scales.
|
|
///
|
|
/// src: [num_experts, rows, cols] FP8E4M3, contiguous, GPU
|
|
/// scales: [num_experts] F32, contiguous, GPU
|
|
///
|
|
/// Returns: [num_experts, rows, cols] BF16
|
|
pub fn dequant_fp8_to_bf16(src: &Tensor, scales: &Tensor) -> Tensor {
|
|
assert_eq!(src.ndim(), 3, "dequant_fp8_to_bf16: src must be 3D");
|
|
assert_eq!(src.dtype(), DType::FP8E4M3);
|
|
assert!(src.is_contiguous());
|
|
assert_eq!(scales.ndim(), 1);
|
|
assert_eq!(scales.dtype(), DType::F32);
|
|
assert!(scales.is_contiguous());
|
|
|
|
let num_experts = src.shape()[0];
|
|
let rows = src.shape()[1];
|
|
let cols = src.shape()[2];
|
|
assert_eq!(scales.shape()[0], num_experts);
|
|
|
|
let out = Tensor::empty(&[num_experts, rows, cols], DType::BF16, src.device());
|
|
|
|
unsafe {
|
|
launch_dequant_fp8e4m3_to_bf16(
|
|
src.data_ptr() as *const c_void,
|
|
scales.data_ptr() as *const c_void,
|
|
out.data_ptr() as *mut c_void,
|
|
num_experts as i32, rows as i32, cols as i32,
|
|
std::ptr::null_mut(),
|
|
);
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
/// Dynamically quantize a contiguous BF16 tensor to FP8 E4M3 with per-row scales.
|
|
///
|
|
/// src: [num_rows, cols] or [batch, rows, cols] BF16, contiguous, GPU
|
|
/// Treats the tensor as 2D (flattens leading dims into num_rows).
|
|
///
|
|
/// Returns: (fp8_data [same shape] FP8E4M3, scales [total_rows] F32)
|
|
pub fn quantize_bf16_to_fp8_rowwise(src: &Tensor) -> (Tensor, Tensor) {
|
|
assert_eq!(src.dtype(), DType::BF16);
|
|
assert!(src.is_contiguous());
|
|
assert!(src.ndim() >= 2);
|
|
|
|
let cols = src.shape()[src.ndim() - 1];
|
|
let num_rows: usize = src.shape()[..src.ndim() - 1].iter().product();
|
|
|
|
let fp8_out = Tensor::empty(src.shape(), DType::FP8E4M3, src.device());
|
|
let scales = Tensor::empty(&[num_rows], DType::F32, src.device());
|
|
|
|
unsafe {
|
|
launch_quantize_bf16_to_fp8e4m3_rowwise(
|
|
src.data_ptr() as *const c_void,
|
|
fp8_out.data_ptr() as *mut c_void,
|
|
scales.data_ptr() as *mut c_void,
|
|
num_rows as i32, cols as i32,
|
|
std::ptr::null_mut(),
|
|
);
|
|
}
|
|
|
|
(fp8_out, scales)
|
|
}
|
|
|
|
/// FP8 batched GEMM via cuBLASLt (transA=T required on Blackwell).
|
|
///
|
|
/// Computes: C[b] = scale_a[b] * scale_b[b] * (A_fp8[b] @ B_fp8_T[b]^T)
|
|
/// effectively C[b] = A[b, M, K] @ W[b, K, N] but W is stored transposed
|
|
/// as [b, N, K] for cuBLASLt FP8 compatibility.
|
|
///
|
|
/// a_fp8: [batch, M, K] FP8E4M3 (activations, quantized per-row)
|
|
/// a_scales: [batch * M] F32 (per-token activation scales, applied post-GEMM)
|
|
/// b_fp8_t: [batch, N, K] FP8E4M3 (weights, TRANSPOSED for cuBLASLt)
|
|
/// b_scales: [batch] F32 (per-expert scalar weight scales, applied in-GEMM)
|
|
///
|
|
/// Returns: [batch, M, N] BF16
|
|
pub fn batched_gemm_fp8(
|
|
a_fp8: &Tensor,
|
|
a_scales: &Tensor,
|
|
b_fp8_t: &Tensor,
|
|
b_scales: &Tensor,
|
|
) -> Tensor {
|
|
assert_eq!(a_fp8.ndim(), 3);
|
|
assert_eq!(b_fp8_t.ndim(), 3);
|
|
assert_eq!(a_fp8.dtype(), DType::FP8E4M3);
|
|
assert_eq!(b_fp8_t.dtype(), DType::FP8E4M3);
|
|
assert!(a_fp8.is_contiguous() && b_fp8_t.is_contiguous());
|
|
assert_eq!(a_fp8.shape()[0], b_fp8_t.shape()[0]);
|
|
// b_fp8_t is [batch, N, K] transposed, so b_fp8_t.shape[2] == K == a_fp8.shape[2]
|
|
assert_eq!(a_fp8.shape()[2], b_fp8_t.shape()[2]);
|
|
|
|
let batch = a_fp8.shape()[0];
|
|
let m = a_fp8.shape()[1]; // tokens
|
|
let k = a_fp8.shape()[2]; // hidden
|
|
let n = b_fp8_t.shape()[1]; // out_dim (from transposed weight)
|
|
|
|
// a_scales: [batch * M] per-token activation scales (applied post-GEMM, per row).
|
|
// b_scales: [batch] per-expert scalar weight scales (applied in-GEMM via B-scale ptr).
|
|
assert_eq!(a_scales.shape()[0], batch * m);
|
|
assert_eq!(b_scales.shape()[0], batch);
|
|
|
|
let c = Tensor::empty(&[batch, m, n], DType::BF16, a_fp8.device());
|
|
|
|
// Strides (in bytes) for one expert slice
|
|
let stride_a = m * k; // FP8: 1 byte per elem
|
|
let stride_b = n * k; // FP8: 1 byte per elem (transposed: [N, K])
|
|
let stride_c = m * n * 2; // BF16: 2 bytes per elem
|
|
|
|
CUBLASLT_CTX.with(|cell| {
|
|
let mut ctx = cell.borrow_mut();
|
|
let handle = ctx.handle;
|
|
let ws_ptr = ctx.workspace.as_ptr() as *mut c_void;
|
|
// Build (or fetch) the cached plan for this shape — heuristic search and
|
|
// descriptor/layout creation happen once per (m, n, k), not per-expert.
|
|
let plan = ctx.plan(m, n, k);
|
|
|
|
// alpha=1, beta=0. Per-expert weight scale is supplied via the cuBLASLt
|
|
// B-scale pointer (device, scalar): cuBLASLt computes in the FP32 epilogue
|
|
// D = (1.0 * A_fp8) @ (b_scale[e] * B_fp8)^T = b_scale[e] * (A_fp8 @ B_fp8^T)
|
|
// Per-token activation scale (a_scale) is applied post-GEMM (per row).
|
|
let alpha: f32 = 1.0;
|
|
let beta: f32 = 0.0;
|
|
let ptr_sz = std::mem::size_of::<*const c_void>();
|
|
|
|
for e in 0..batch {
|
|
let a_ptr = unsafe { (a_fp8.data_ptr() as *const u8).add(e * stride_a) as *const c_void };
|
|
let b_ptr = unsafe { (b_fp8_t.data_ptr() as *const u8).add(e * stride_b) as *const c_void };
|
|
let c_ptr = unsafe { (c.data_ptr() as *mut u8).add(e * stride_c) as *mut c_void };
|
|
// Device pointer to this expert's scalar weight scale (FP32, 4 bytes).
|
|
let b_scale_ptr = unsafe { (b_scales.data_ptr() as *const u8).add(e * 4) as *const c_void };
|
|
|
|
unsafe {
|
|
cublasLtMatmulDescSetAttribute(
|
|
plan.desc, CUBLASLT_MATMUL_DESC_B_SCALE_POINTER,
|
|
&b_scale_ptr as *const _ as _, ptr_sz,
|
|
);
|
|
let status = cublasLtMatmul(
|
|
handle, plan.desc,
|
|
&alpha as *const f32 as _,
|
|
b_ptr, // cuBLASLt "A" = weights
|
|
plan.a_layout,
|
|
a_ptr, // cuBLASLt "B" = activations
|
|
plan.b_layout,
|
|
&beta as *const f32 as _,
|
|
c_ptr, // C (unused with beta=0)
|
|
plan.c_layout,
|
|
c_ptr, // D = output
|
|
plan.d_layout,
|
|
&plan.algo,
|
|
ws_ptr,
|
|
plan.workspace_size,
|
|
std::ptr::null_mut(),
|
|
);
|
|
assert_eq!(status, 0, "cublasLtMatmul FP8 failed for expert {e}: status={status}");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Post-GEMM: multiply each row of c by its activation scale.
|
|
// c is [batch, M, N] BF16. a_scales is [batch * M] F32.
|
|
// This recovers the per-token scale that was divided out during quantization.
|
|
let total_rows = (batch * m) as i32;
|
|
let cols = n as i32;
|
|
unsafe {
|
|
launch_rowwise_scale_bf16(
|
|
c.data_ptr() as *mut c_void,
|
|
a_scales.data_ptr() as *const c_void,
|
|
total_rows, cols,
|
|
std::ptr::null_mut(),
|
|
);
|
|
}
|
|
|
|
c
|
|
}
|