quantization: W8A8 FP8 compute via cuBLASLt tensor cores
Replace the W8A16 dequant→BF16-GEMM path with native FP8×FP8→BF16 GEMM using cuBLASLt on Blackwell (RTX 5090). Both weights (static FP8 E4M3) and activations (dynamically quantized per-row) are processed directly on FP8 tensor cores. Key implementation details: - cuBLASLt on Blackwell requires transA=T for FP8, so expert weights are transposed during model loading ([E,K,N] → [E,N,K]) - Per-row activation quantization kernel (absmax/448 → FP8 E4M3) - Post-GEMM row-wise rescaling recovers per-token precision - Per-expert loop (not batched) due to cuBLASLt FP8 scale constraints The same FP8 quantized model files work — no re-quantization needed. Activation quantization happens dynamically at inference time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,10 +47,11 @@ struct GptOssBlock {
|
||||
expert_gate_up_bias: Tensor, // [local_experts, 2*inter]
|
||||
expert_down_wt: Tensor, // [local_experts, inter, hidden] BF16
|
||||
expert_down_bias: Tensor, // [local_experts, hidden]
|
||||
// FP8 quantized expert weights (Some when running FP8 W8A16)
|
||||
expert_gate_up_fp8: Option<Tensor>, // [local_experts, hidden, 2*inter] FP8E4M3
|
||||
// FP8 quantized expert weights (Some when running FP8 W8A8)
|
||||
// Transposed layout [E, N, K] for cuBLASLt FP8 (Blackwell requires transA=T)
|
||||
expert_gate_up_fp8: Option<Tensor>, // [local_experts, 2*inter, hidden] FP8E4M3
|
||||
expert_gate_up_scale: Option<Tensor>,// [local_experts] F32
|
||||
expert_down_fp8: Option<Tensor>, // [local_experts, inter, hidden] FP8E4M3
|
||||
expert_down_fp8: Option<Tensor>, // [local_experts, hidden, inter] FP8E4M3
|
||||
expert_down_scale: Option<Tensor>, // [local_experts] F32
|
||||
local_experts: usize,
|
||||
// Activation params
|
||||
@@ -183,9 +184,12 @@ impl GptOss {
|
||||
let expert_down_scale_gpu;
|
||||
|
||||
if is_fp8 {
|
||||
// FP8 path: load quantized weights and scales
|
||||
expert_gate_up_fp8 = Some(slice_expert_range_3d_raw(&gate_up_3d, expert_start, local_experts, hidden, inter2).to_device(dev));
|
||||
expert_down_fp8 = Some(slice_expert_range_3d_raw(&down_3d, expert_start, local_experts, inter, hidden).to_device(dev));
|
||||
// FP8 W8A8 path: load and TRANSPOSE weights for cuBLASLt (requires transA=T on Blackwell).
|
||||
// Original: [E, K, N] → Transposed: [E, N, K]
|
||||
let gu_sliced = slice_expert_range_3d_raw(&gate_up_3d, expert_start, local_experts, hidden, inter2);
|
||||
let dn_sliced = slice_expert_range_3d_raw(&down_3d, expert_start, local_experts, inter, hidden);
|
||||
expert_gate_up_fp8 = Some(transpose_3d_inner_raw(&gu_sliced, local_experts, hidden, inter2).to_device(dev));
|
||||
expert_down_fp8 = Some(transpose_3d_inner_raw(&dn_sliced, local_experts, inter, hidden).to_device(dev));
|
||||
// Scales: [num_experts] F32 → slice to [local_experts]
|
||||
let gu_s = gate_up_scale.expect("FP8 model missing gate_up_proj_scale");
|
||||
let d_s = down_scale.expect("FP8 model missing down_proj_scale");
|
||||
@@ -255,7 +259,7 @@ impl GptOss {
|
||||
eprintln!("gpt-oss: detected LayerNorm bias — using LayerNorm instead of RMSNorm");
|
||||
}
|
||||
if is_fp8 {
|
||||
eprintln!("gpt-oss: FP8 E4M3 quantized expert weights detected (W8A16 mode)");
|
||||
eprintln!("gpt-oss: FP8 E4M3 quantized expert weights detected (W8A8 cuBLASLt mode)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,12 +519,15 @@ impl GptOss {
|
||||
let x_rep = xserv_kernels::moe::moe_replicate(x, local_experts);
|
||||
|
||||
// 4. Batched GEMM gate_up: [E, tokens, hidden] @ [E, hidden, 2*inter] → [E, tokens, 2*inter]
|
||||
let gate_up_wt = if let Some(ref fp8) = layer.expert_gate_up_fp8 {
|
||||
xserv_kernels::quantization::dequant_fp8_to_bf16(fp8, layer.expert_gate_up_scale.as_ref().unwrap())
|
||||
let gate_up = if let Some(ref wt_fp8_t) = layer.expert_gate_up_fp8 {
|
||||
// W8A8: quantize activations with per-expert scalar scale, use cuBLASLt FP8 GEMM
|
||||
let (x_fp8, x_scales) = xserv_kernels::quantization::quantize_bf16_to_fp8_rowwise(&x_rep);
|
||||
xserv_kernels::quantization::batched_gemm_fp8(
|
||||
&x_fp8, &x_scales, wt_fp8_t, layer.expert_gate_up_scale.as_ref().unwrap(),
|
||||
)
|
||||
} else {
|
||||
layer.expert_gate_up_wt.clone()
|
||||
xserv_kernels::moe::batched_gemm_strided(&x_rep, &layer.expert_gate_up_wt)
|
||||
};
|
||||
let gate_up = xserv_kernels::moe::batched_gemm_strided(&x_rep, &gate_up_wt);
|
||||
|
||||
// 5. Bias add: gate_up += expert_gate_up_bias (in-place)
|
||||
xserv_kernels::moe::moe_bias_add_3d(&gate_up, &layer.expert_gate_up_bias);
|
||||
@@ -534,12 +541,15 @@ impl GptOss {
|
||||
let activated = activated_flat.reshape(&[local_experts, num_tokens, inter]);
|
||||
|
||||
// 7. Batched GEMM down: [E, tokens, inter] @ [E, inter, hidden] → [E, tokens, hidden]
|
||||
let down_wt = if let Some(ref fp8) = layer.expert_down_fp8 {
|
||||
xserv_kernels::quantization::dequant_fp8_to_bf16(fp8, layer.expert_down_scale.as_ref().unwrap())
|
||||
let down = if let Some(ref wt_fp8) = layer.expert_down_fp8 {
|
||||
// W8A8: quantize post-GLU activations to FP8, use cuBLASLt FP8 GEMM
|
||||
let (act_fp8, act_scales) = xserv_kernels::quantization::quantize_bf16_to_fp8_rowwise(&activated);
|
||||
xserv_kernels::quantization::batched_gemm_fp8(
|
||||
&act_fp8, &act_scales, wt_fp8, layer.expert_down_scale.as_ref().unwrap(),
|
||||
)
|
||||
} else {
|
||||
layer.expert_down_wt.clone()
|
||||
xserv_kernels::moe::batched_gemm_strided(&activated, &layer.expert_down_wt)
|
||||
};
|
||||
let down = xserv_kernels::moe::batched_gemm_strided(&activated, &down_wt);
|
||||
|
||||
// 8. Bias add: down += expert_down_bias (in-place)
|
||||
xserv_kernels::moe::moe_bias_add_3d(&down, &layer.expert_down_bias);
|
||||
@@ -636,6 +646,27 @@ fn shard_1d(t: &Tensor, rank: usize, world: usize) -> Tensor {
|
||||
Tensor::from_slice(&shard, &[local])
|
||||
}
|
||||
|
||||
/// Transpose the inner two dimensions of a [batch, rows, cols] tensor → [batch, cols, rows].
|
||||
/// Works on raw bytes (any dtype). CPU-only.
|
||||
fn transpose_3d_inner_raw(t: &Tensor, batch: usize, rows: usize, cols: usize) -> Tensor {
|
||||
assert_eq!(t.ndim(), 3);
|
||||
assert_eq!(t.shape(), &[batch, rows, cols]);
|
||||
let host = t.to_device(Device::Cpu);
|
||||
let es = t.dtype().size_bytes();
|
||||
let raw = host.as_raw_bytes();
|
||||
let mut out = vec![0u8; batch * cols * rows * es];
|
||||
for b in 0..batch {
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
let src_off = (b * rows * cols + r * cols + c) * es;
|
||||
let dst_off = (b * cols * rows + c * rows + r) * es;
|
||||
out[dst_off..dst_off + es].copy_from_slice(&raw[src_off..src_off + es]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Tensor::from_raw_bytes(&out, &[batch, cols, rows], t.dtype())
|
||||
}
|
||||
|
||||
/// Extract experts [start..start+count) from a [num_experts, rows, cols] 3D tensor (any dtype, raw bytes).
|
||||
fn slice_expert_range_3d_raw(t: &Tensor, start: usize, count: usize, rows: usize, cols: usize) -> Tensor {
|
||||
assert_eq!(t.ndim(), 3);
|
||||
|
||||
Reference in New Issue
Block a user