quantization: add FP8 E4M3 W8A16 for gpt-oss MoE expert weights
Store expert gate_up_proj and down_proj weights in FP8 E4M3 (1 byte/elem) with per-expert FP32 scale factors. At inference, a fused CUDA kernel dequantizes to BF16 before the existing cuBLAS batched GEMM. Results on gpt-oss-20b (50-problem GSM8K subset): - FP8 TP=1: 47/50 = 94.0% (single RTX 5090, ~25 GB VRAM) - BF16 TP=2: 47/50 = 94.0% (requires 2× RTX 5090, ~39 GB total) No measurable accuracy degradation. Model size: 41.8 GB → 22.7 GB (−46%). New files: - tools/quantize_fp8.py: offline BF16→FP8 conversion script - csrc/quantization/dequant_fp8.cu: per-expert-scale dequant kernel - crates/xserv-kernels/src/quantization.rs: Rust FFI wrapper - tools/eval_gsm8k_batch.sh: GSM8K accuracy evaluation harness Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ pub enum DType {
|
||||
F32,
|
||||
F16,
|
||||
BF16,
|
||||
FP8E4M3,
|
||||
}
|
||||
|
||||
impl DType {
|
||||
@@ -13,6 +14,7 @@ impl DType {
|
||||
DType::F32 => 4,
|
||||
DType::F16 => 2,
|
||||
DType::BF16 => 2,
|
||||
DType::FP8E4M3 => 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +23,7 @@ impl DType {
|
||||
DType::F32 => "f32",
|
||||
DType::F16 => "f16",
|
||||
DType::BF16 => "bf16",
|
||||
DType::FP8E4M3 => "fp8e4m3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,25 @@ impl Tensor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a tensor from raw bytes. Used for dtypes without a Rust type
|
||||
/// (e.g. FP8 E4M3) where we store the bit pattern as-is.
|
||||
pub fn from_raw_bytes(data: &[u8], shape: &[usize], dtype: DType) -> Self {
|
||||
let numel: usize = shape.iter().product();
|
||||
assert_eq!(
|
||||
data.len(),
|
||||
numel * dtype.size_bytes(),
|
||||
"raw bytes length {} != expected {} (numel={} * elem_size={})",
|
||||
data.len(), numel * dtype.size_bytes(), numel, dtype.size_bytes()
|
||||
);
|
||||
Self {
|
||||
storage: Storage::cpu(data.to_vec()),
|
||||
shape: Dims::from_slice(shape),
|
||||
strides: shape::contiguous_strides(shape),
|
||||
offset: 0,
|
||||
dtype,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zeros(shape: &[usize], dtype: DType, device: Device) -> Self {
|
||||
let numel = shape::num_elements(shape);
|
||||
let len_bytes = numel * dtype.size_bytes();
|
||||
@@ -87,6 +106,7 @@ impl Tensor {
|
||||
DType::F32 => Self::from_slice(&vec![1.0f32; numel], shape),
|
||||
DType::F16 => Self::from_slice(&vec![half::f16::from_f32(1.0); numel], shape),
|
||||
DType::BF16 => Self::from_slice(&vec![half::bf16::from_f32(1.0); numel], shape),
|
||||
DType::FP8E4M3 => panic!("ones() not supported for FP8E4M3"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +285,17 @@ impl Tensor {
|
||||
unsafe { std::slice::from_raw_parts(bytes[start..].as_ptr() as *const T, len) }
|
||||
}
|
||||
|
||||
/// Raw byte access for dtypes without a Rust type (e.g. FP8).
|
||||
pub fn as_raw_bytes(&self) -> &[u8] {
|
||||
assert!(self.is_contiguous(), "as_raw_bytes requires contiguous");
|
||||
assert_eq!(self.device(), Device::Cpu, "as_raw_bytes requires CPU");
|
||||
let bytes = self.storage.as_cpu_bytes();
|
||||
let elem_size = self.dtype.size_bytes();
|
||||
let start = self.offset * elem_size;
|
||||
let len = self.numel() * elem_size;
|
||||
&bytes[start..start + len]
|
||||
}
|
||||
|
||||
/// Raw pointer to storage start (for GPU kernel launch).
|
||||
pub fn data_ptr(&self) -> *const u8 {
|
||||
match self.device() {
|
||||
|
||||
Reference in New Issue
Block a user