pub struct CudaInfo {
pub device_count: usize,
pub first_device_name: Option<String>,
pub total_vram_bytes: u64,
pub free_vram_bytes: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum CudaError {
#[error("cudarc driver initialization failed: {0:?}")]
DriverInit(String),
#[error("NVRTC kernel compilation failed: {0:?}")]
KernelCompile(String),
#[error("kernel launch failed: {0:?}")]
Launch(String),
#[error("unsupported on the CUDA path: {0}")]
Unsupported(String),
}
pub fn probe() -> Option<CudaInfo> {
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(|| {
let dev = cudarc::driver::CudaDevice::new(0).ok()?;
let name = dev.name().ok();
let (free, total) = cudarc::driver::result::mem_get_info().ok()?;
Some(CudaInfo {
device_count: 1,
first_device_name: name,
total_vram_bytes: total as u64,
free_vram_bytes: free as u64,
})
});
std::panic::set_hook(previous_hook);
result.unwrap_or(None)
}
pub const Q5_K_MATVEC_COALESCED_KERNEL_SRC: &str = r#"
__device__ __forceinline__ float ferrox_f16_to_f32_q5co(unsigned short bits) {
unsigned int sign = (bits >> 15) & 0x1u;
unsigned int exp = (bits >> 10) & 0x1Fu;
unsigned int mant = bits & 0x3FFu;
float scale;
if (exp == 0) {
scale = ldexpf((float)mant, -24);
} else if (exp == 31) {
scale = mant ? __int_as_float(0x7fc00000) : __int_as_float(0x7f800000);
} else {
scale = ldexpf((float)(mant | 0x400), (int)exp - 25);
}
return sign ? -scale : scale;
}
__device__ __forceinline__ void ferrox_q4_k_scale_min_q5co(
int j, const unsigned char* scales, unsigned char* sc, unsigned char* m
) {
if (j < 4) {
*sc = scales[j] & 63;
*m = scales[j + 4] & 63;
} else {
*sc = (scales[j + 4] & 0x0F) | ((scales[j - 4] >> 6) << 4);
*m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4);
}
}
extern "C" __global__ void q5_k_matvec_coalesced(
const unsigned char* weights,
const float* x,
float* out,
int rows,
int row_bytes,
int n_blocks_per_row
) {
const int warps = blockDim.x / 32;
const int warp = threadIdx.x / 32;
const int lane = threadIdx.x % 32;
const int row = blockIdx.x * warps + warp;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
float acc = 0.0f;
for (int blk = 0; blk < n_blocks_per_row; ++blk) {
const unsigned char* block = row_ptr + (size_t)blk * 176;
const float d = ferrox_f16_to_f32_q5co(
(unsigned short)block[0] | ((unsigned short)block[1] << 8));
const float dmin = ferrox_f16_to_f32_q5co(
(unsigned short)block[2] | ((unsigned short)block[3] << 8));
const unsigned char* scales = block + 4;
const unsigned char* qh = block + 16;
const unsigned char* qs = block + 48;
const int x_base = blk * 256;
const unsigned char h = qh[lane];
#pragma unroll
for (int oi = 0; oi < 4; ++oi) {
unsigned char sc1, m1, sc2, m2;
ferrox_q4_k_scale_min_q5co(2 * oi, scales, &sc1, &m1);
ferrox_q4_k_scale_min_q5co(2 * oi + 1, scales, &sc2, &m2);
const float d1 = d * (float)sc1, min1 = dmin * (float)m1;
const float d2 = d * (float)sc2, min2 = dmin * (float)m2;
const unsigned char ql = qs[oi * 32 + lane];
const unsigned char u1 = (unsigned char)(1u << (2 * oi));
const unsigned char u2 = (unsigned char)(2u << (2 * oi));
const int xb = x_base + oi * 64;
const int hi1 = (h & u1) ? 16 : 0;
const int hi2 = (h & u2) ? 16 : 0;
acc += (d1 * (float)((ql & 0x0F) + hi1) - min1) * x[xb + lane];
acc += (d2 * (float)((ql >> 4) + hi2) - min2) * x[xb + 32 + lane];
}
}
#pragma unroll
for (int s = 16; s > 0; s >>= 1) {
acc += __shfl_down_sync(0xffffffff, acc, s);
}
if (lane == 0) out[row] = acc;
}
"#;
pub const Q8_0_MATVEC_COALESCED_KERNEL_SRC: &str = r#"
__device__ __forceinline__ float ferrox_f16_to_f32_q8co(unsigned short bits) {
unsigned int sign = (bits >> 15) & 0x1u;
unsigned int exp = (bits >> 10) & 0x1Fu;
unsigned int mant = bits & 0x3FFu;
float scale;
if (exp == 0) {
scale = ldexpf((float)mant, -24);
} else if (exp == 31) {
scale = mant ? __int_as_float(0x7fc00000) : __int_as_float(0x7f800000);
} else {
scale = ldexpf((float)(mant | 0x400), (int)exp - 25);
}
return sign ? -scale : scale;
}
extern "C" __global__ void q8_0_matvec_coalesced(
const unsigned char* weights,
const float* x,
float* out,
int rows,
int row_bytes,
int n_blocks_per_row
) {
const int warps = blockDim.x / 32;
const int warp = threadIdx.x / 32;
const int lane = threadIdx.x % 32;
const int row = blockIdx.x * warps + warp;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
float acc = 0.0f;
for (int blk = 0; blk < n_blocks_per_row; ++blk) {
const unsigned char* block = row_ptr + (size_t)blk * 34;
const float d = ferrox_f16_to_f32_q8co(
(unsigned short)block[0] | ((unsigned short)block[1] << 8));
const signed char q = (signed char)block[2 + lane];
acc += d * (float)q * x[blk * 32 + lane];
}
#pragma unroll
for (int s = 16; s > 0; s >>= 1) {
acc += __shfl_down_sync(0xffffffff, acc, s);
}
if (lane == 0) out[row] = acc;
}
"#;
pub const Q6_K_MATVEC_COALESCED_KERNEL_SRC: &str = r#"
__device__ __forceinline__ float ferrox_f16_to_f32_q6co(unsigned short bits) {
unsigned int sign = (bits >> 15) & 0x1u;
unsigned int exp = (bits >> 10) & 0x1Fu;
unsigned int mant = bits & 0x3FFu;
float scale;
if (exp == 0) {
scale = ldexpf((float)mant, -24);
} else if (exp == 31) {
scale = mant ? __int_as_float(0x7fc00000) : __int_as_float(0x7f800000);
} else {
scale = ldexpf((float)(mant | 0x400), (int)exp - 25);
}
return sign ? -scale : scale;
}
extern "C" __global__ void q6_k_matvec_coalesced(
const unsigned char* weights,
const float* x,
float* out,
int rows,
int row_bytes,
int n_blocks_per_row
) {
const int warps = blockDim.x / 32;
const int warp = threadIdx.x / 32;
const int lane = threadIdx.x % 32;
const int row = blockIdx.x * warps + warp;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
const int is = lane / 16;
float acc = 0.0f;
for (int blk = 0; blk < n_blocks_per_row; ++blk) {
const unsigned char* block = row_ptr + (size_t)blk * 210;
const float d = ferrox_f16_to_f32_q6co(
(unsigned short)block[208] | ((unsigned short)block[209] << 8));
const int x_base = blk * 256;
#pragma unroll
for (int half = 0; half < 2; ++half) {
const unsigned char* ql = block + half * 64;
const unsigned char* qh = block + 128 + half * 32;
const signed char* sc = (const signed char*)(block + 192 + half * 8);
const int xh = x_base + half * 128;
const int q1 = (int)((ql[lane] & 0x0F) | ((qh[lane] & 0x03) << 4)) - 32;
const int q2 = (int)((ql[lane + 32] & 0x0F) | (((qh[lane] >> 2) & 0x03) << 4)) - 32;
const int q3 = (int)((ql[lane] >> 4) | (((qh[lane] >> 4) & 0x03) << 4)) - 32;
const int q4 = (int)((ql[lane + 32] >> 4) | (((qh[lane] >> 6) & 0x03) << 4)) - 32;
acc += d * (float)sc[is] * (float)q1 * x[xh + lane];
acc += d * (float)sc[is + 2] * (float)q2 * x[xh + lane + 32];
acc += d * (float)sc[is + 4] * (float)q3 * x[xh + lane + 64];
acc += d * (float)sc[is + 6] * (float)q4 * x[xh + lane + 96];
}
}
#pragma unroll
for (int s = 16; s > 0; s >>= 1) {
acc += __shfl_down_sync(0xffffffff, acc, s);
}
if (lane == 0) out[row] = acc;
}
"#;
pub const Q4_K_MATVEC_COALESCED_KERNEL_SRC: &str = r#"
__device__ __forceinline__ float ferrox_f16_to_f32_co(unsigned short bits) {
unsigned int sign = (bits >> 15) & 0x1u;
unsigned int exp = (bits >> 10) & 0x1Fu;
unsigned int mant = bits & 0x3FFu;
float scale;
if (exp == 0) {
scale = ldexpf((float)mant, -24);
} else if (exp == 31) {
scale = mant ? __int_as_float(0x7fc00000) : __int_as_float(0x7f800000);
} else {
scale = ldexpf((float)(mant | 0x400), (int)exp - 25);
}
return sign ? -scale : scale;
}
__device__ __forceinline__ void ferrox_q4_k_scale_min_co(
int j, const unsigned char* scales, unsigned char* sc, unsigned char* m
) {
if (j < 4) {
*sc = scales[j] & 63;
*m = scales[j + 4] & 63;
} else {
*sc = (scales[j + 4] & 0x0F) | ((scales[j - 4] >> 6) << 4);
*m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4);
}
}
extern "C" __global__ void q4_k_matvec_coalesced(
const unsigned char* weights,
const float* x,
float* out,
int rows,
int row_bytes,
int n_blocks_per_row
) {
const int warps = blockDim.x / 32;
const int warp = threadIdx.x / 32;
const int lane = threadIdx.x % 32;
const int row = blockIdx.x * warps + warp;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
const int off = 4 * lane;
const int oi = lane / 8;
const int within = off % 32;
float acc = 0.0f;
for (int blk = 0; blk < n_blocks_per_row; ++blk) {
const unsigned char* block = row_ptr + (size_t)blk * 144;
const float d = ferrox_f16_to_f32_co(
(unsigned short)block[0] | ((unsigned short)block[1] << 8));
const float dmin = ferrox_f16_to_f32_co(
(unsigned short)block[2] | ((unsigned short)block[3] << 8));
const unsigned char* scales = block + 4;
const unsigned char* qs = block + 16;
unsigned char sc1, m1, sc2, m2;
ferrox_q4_k_scale_min_co(2 * oi, scales, &sc1, &m1);
ferrox_q4_k_scale_min_co(2 * oi + 1, scales, &sc2, &m2);
const float d1 = d * (float)sc1, min1 = dmin * (float)m1;
const float d2 = d * (float)sc2, min2 = dmin * (float)m2;
// The whole warp's 32 loads cover qs[0..128) contiguously.
const uchar4 w = *(const uchar4*)(qs + off);
const int xb = blk * 256 + oi * 64 + within;
const unsigned char wb[4] = { w.x, w.y, w.z, w.w };
#pragma unroll
for (int i = 0; i < 4; ++i) {
acc += (d1 * (float)(wb[i] & 0x0F) - min1) * x[xb + i];
acc += (d2 * (float)(wb[i] >> 4) - min2) * x[xb + 32 + i];
}
}
#pragma unroll
for (int s = 16; s > 0; s >>= 1) {
acc += __shfl_down_sync(0xffffffff, acc, s);
}
if (lane == 0) out[row] = acc;
}
"#;
static CUDA_DEVICE: std::sync::Mutex<Option<std::sync::Arc<cudarc::driver::CudaDevice>>> =
std::sync::Mutex::new(None);
pub(crate) fn shared_device() -> Result<std::sync::Arc<cudarc::driver::CudaDevice>, CudaError> {
let mut guard = CUDA_DEVICE.lock().unwrap();
if let Some(dev) = guard.as_ref() {
return Ok(dev.clone());
}
let dev =
cudarc::driver::CudaDevice::new(0).map_err(|e| CudaError::DriverInit(format!("{e:?}")))?;
*guard = Some(dev.clone());
Ok(dev)
}
static LOADED_MODULES: std::sync::Mutex<Option<std::collections::HashSet<&'static str>>> =
std::sync::Mutex::new(None);
pub(crate) fn ensure_module_loaded(
dev: &std::sync::Arc<cudarc::driver::CudaDevice>,
kernel_src: &str,
module_name: &'static str,
fn_name: &'static str,
) -> Result<(), CudaError> {
ensure_module_loaded_lazy(dev, module_name, fn_name, || kernel_src.to_string())
}
pub(crate) fn ensure_module_loaded_lazy(
dev: &std::sync::Arc<cudarc::driver::CudaDevice>,
module_name: &'static str,
fn_name: &'static str,
src: impl FnOnce() -> String,
) -> Result<(), CudaError> {
let mut guard = LOADED_MODULES.lock().unwrap();
let set = guard.get_or_insert_with(std::collections::HashSet::new);
if set.contains(module_name) {
return Ok(());
}
let ptx = cudarc::nvrtc::compile_ptx(src())
.map_err(|e| CudaError::KernelCompile(format!("{e:?}")))?;
dev.load_ptx(ptx, module_name, &[fn_name])
.map_err(|e| CudaError::KernelCompile(format!("{e:?}")))?;
set.insert(module_name);
Ok(())
}
#[allow(clippy::too_many_arguments)] fn launch_matvec(
kernel_src: &'static str,
module_name: &'static str,
fn_name: &'static str,
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
let dev = shared_device()?;
let d_x = dev
.htod_copy(x.to_vec())
.map_err(|e| CudaError::Launch(format!("{e:?}")))?;
let launch = MatvecLaunch {
kernel_src,
module_name,
fn_name,
weights,
rows,
row_bytes,
n_blocks_per_row,
};
let (d_out, _weights) = enqueue_matvec(&dev, &launch, &d_x)?;
dev.dtoh_sync_copy(&d_out)
.map_err(|e| CudaError::Launch(format!("{e:?}")))
}
fn coalesced_matvec_kernel(fn_name: &str) -> Option<(&'static str, &'static str, &'static str)> {
match fn_name {
"q4_k_matvec" => Some((
Q4_K_MATVEC_COALESCED_KERNEL_SRC,
"ferrox_q4_k_coalesced",
"q4_k_matvec_coalesced",
)),
"q5_k_matvec" => Some((
Q5_K_MATVEC_COALESCED_KERNEL_SRC,
"ferrox_q5_k_coalesced",
"q5_k_matvec_coalesced",
)),
"q6_k_matvec" => Some((
Q6_K_MATVEC_COALESCED_KERNEL_SRC,
"ferrox_q6_k_coalesced",
"q6_k_matvec_coalesced",
)),
"q8_0_matvec" => Some((
Q8_0_MATVEC_COALESCED_KERNEL_SRC,
"ferrox_q8_0_coalesced",
"q8_0_matvec_coalesced",
)),
_ => None,
}
}
const COALESCED_WARPS_PER_BLOCK: usize = 8;
fn matvec_launch_plan(
fn_name: &'static str,
kernel_src: &'static str,
module_name: &'static str,
rows: usize,
) -> (
&'static str,
&'static str,
&'static str,
cudarc::driver::LaunchConfig,
) {
match coalesced_matvec_kernel(fn_name) {
Some((src, module, entry)) => (
src,
module,
entry,
cudarc::driver::LaunchConfig {
grid_dim: (rows.div_ceil(COALESCED_WARPS_PER_BLOCK) as u32, 1, 1),
block_dim: ((COALESCED_WARPS_PER_BLOCK * 32) as u32, 1, 1),
shared_mem_bytes: 0,
},
),
None => (
kernel_src,
module_name,
fn_name,
cudarc::driver::LaunchConfig {
grid_dim: (rows as u32, 1, 1),
block_dim: (256, 1, 1),
shared_mem_bytes: 256 * std::mem::size_of::<f32>() as u32,
},
),
}
}
fn enqueue_matvec(
dev: &std::sync::Arc<cudarc::driver::CudaDevice>,
launch: &MatvecLaunch<'_>,
d_x: &cudarc::driver::CudaSlice<f32>,
) -> Result<
(
cudarc::driver::CudaSlice<f32>,
std::sync::Arc<ResidentCudaWeights>,
),
CudaError,
> {
use cudarc::driver::LaunchAsync;
let (src, module, entry, cfg) = matvec_launch_plan(
launch.fn_name,
launch.kernel_src,
launch.module_name,
launch.rows,
);
ensure_module_loaded(dev, src, module, entry)?;
let func = dev.get_func(module, entry).ok_or_else(|| {
CudaError::KernelCompile(format!("function '{entry}' not found after load_ptx"))
})?;
let d_weights = resident_cuda_weights(dev, launch.weights)?;
let mut d_out = dev
.alloc_zeros::<f32>(launch.rows)
.map_err(|e| CudaError::Launch(format!("output alloc: {e:?}")))?;
unsafe {
func.launch(
cfg,
(
&d_weights.slice,
d_x,
&mut d_out,
launch.rows as i32,
launch.row_bytes as i32,
launch.n_blocks_per_row as i32,
),
)
.map_err(|e| CudaError::Launch(format!("kernel {entry}: {e:?}")))?;
}
Ok((d_out, d_weights))
}
pub(crate) struct ResidentCudaWeights {
pub(crate) slice: cudarc::driver::CudaSlice<u8>,
#[allow(dead_code)] nbytes: usize,
}
unsafe impl Send for ResidentCudaWeights {}
unsafe impl Sync for ResidentCudaWeights {}
type CudaWeightCacheKey = (usize, usize);
type CudaWeightCacheMap =
std::collections::HashMap<CudaWeightCacheKey, std::sync::Arc<ResidentCudaWeights>>;
static CUDA_WEIGHT_CACHE: std::sync::Mutex<Option<CudaWeightCacheMap>> =
std::sync::Mutex::new(None);
pub(crate) fn resident_cuda_weights(
dev: &std::sync::Arc<cudarc::driver::CudaDevice>,
weights: &[u8],
) -> Result<std::sync::Arc<ResidentCudaWeights>, CudaError> {
let key = (weights.as_ptr() as usize, weights.len());
{
let guard = CUDA_WEIGHT_CACHE.lock().unwrap();
if let Some(cache) = guard.as_ref() {
if let Some(cached) = cache.get(&key) {
return Ok(cached.clone());
}
}
}
let mut guard = CUDA_WEIGHT_CACHE.lock().unwrap();
let cache = guard.get_or_insert_with(std::collections::HashMap::new);
if let Some(cached) = cache.get(&key) {
return Ok(cached.clone());
}
let slice = dev
.htod_copy(weights.to_vec())
.map_err(|e| CudaError::Launch(format!("{e:?}")))?;
let cached = std::sync::Arc::new(ResidentCudaWeights {
slice,
nbytes: weights.len(),
});
cache.insert(key, cached.clone());
Ok(cached)
}
pub use crate::matvec_kinds::codebook::{
IQ4_NL_MATVEC_KERNEL_SRC, IQ4_XS_MATVEC_KERNEL_SRC, MXFP4_MATVEC_KERNEL_SRC,
};
pub use crate::matvec_kinds::kquant::{
Q2_K_MATVEC_KERNEL_SRC, Q3_K_MATVEC_KERNEL_SRC, Q4_K_MATVEC_KERNEL_SRC, Q5_K_MATVEC_KERNEL_SRC,
Q6_K_MATVEC_KERNEL_SRC,
};
pub use crate::matvec_kinds::legacy::{
Q4_0_MATVEC_KERNEL_SRC, Q5_0_MATVEC_KERNEL_SRC, Q8_0_MATVEC_KERNEL_SRC,
};
pub fn matvec_launch_meta(kind_name: &str) -> Option<(&'static str, &'static str, &'static str)> {
crate::matvec_kinds::kind_by_name(kind_name).map(|k| (k.src, k.module_name, k.fn_name))
}
fn launch_matvec_by_kind(
kind_name: &str,
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
let (kernel_src, module_name, fn_name) = matvec_launch_meta(kind_name)
.ok_or_else(|| CudaError::Unsupported(format!("no CUDA matvec kernel for {kind_name}")))?;
launch_matvec(
kernel_src,
module_name,
fn_name,
weights,
x,
rows,
row_bytes,
n_blocks_per_row,
)
}
pub fn launch_q8_0_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q8_0", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q4_0_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q4_0", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q5_0_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q5_0", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q2_k_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q2_K", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q3_k_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q3_K", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_iq4_nl_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("IQ4_NL", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_iq4_xs_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("IQ4_XS", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_mxfp4_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("MXFP4", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q4_k_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q4_K", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q5_k_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q5_K", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub fn launch_q6_k_matvec(
weights: &[u8],
x: &[f32],
rows: usize,
row_bytes: usize,
n_blocks_per_row: usize,
) -> Result<Vec<f32>, CudaError> {
launch_matvec_by_kind("Q6_K", weights, x, rows, row_bytes, n_blocks_per_row)
}
pub struct MatvecLaunch<'a> {
pub kernel_src: &'static str,
pub module_name: &'static str,
pub fn_name: &'static str,
pub weights: &'a [u8],
pub rows: usize,
pub row_bytes: usize,
pub n_blocks_per_row: usize,
}
pub fn launch_matvec_multi(
x: &[f32],
launches: &[MatvecLaunch<'_>],
) -> Result<Vec<Vec<f32>>, CudaError> {
if launches.is_empty() {
return Ok(Vec::new());
}
let dev = shared_device()?;
let d_x = dev
.htod_copy(x.to_vec())
.map_err(|e| CudaError::Launch(format!("x upload: {e:?}")))?;
let mut weight_arcs = Vec::with_capacity(launches.len());
let mut d_outs = Vec::with_capacity(launches.len());
for launch in launches {
let (d_out, d_weights) = enqueue_matvec(&dev, launch, &d_x)?;
weight_arcs.push(d_weights);
d_outs.push(d_out);
}
drop(weight_arcs);
let mut results = Vec::with_capacity(d_outs.len());
for (i, d_out) in d_outs.into_iter().enumerate() {
let out = dev.dtoh_sync_copy(&d_out).map_err(|e| {
CudaError::Launch(format!("output download {}: {e:?}", launches[i].fn_name))
})?;
results.push(out);
}
Ok(results)
}
pub const SILU_MUL_KERNEL_SRC: &str = r#"
extern "C" __global__ void silu_mul_f32(
const float* gate,
const float* up,
float* out,
int n
) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
float g = gate[i];
out[i] = (g / (1.0f + expf(-g))) * up[i];
}
}
"#;
fn silu_mul_device(
dev: &std::sync::Arc<cudarc::driver::CudaDevice>,
gate: &cudarc::driver::CudaSlice<f32>,
up: &cudarc::driver::CudaSlice<f32>,
n: usize,
) -> Result<cudarc::driver::CudaSlice<f32>, CudaError> {
use cudarc::driver::LaunchAsync;
ensure_module_loaded(dev, SILU_MUL_KERNEL_SRC, "ferrox_silu_mul", "silu_mul_f32")?;
let func = dev
.get_func("ferrox_silu_mul", "silu_mul_f32")
.ok_or_else(|| {
CudaError::KernelCompile("function 'silu_mul_f32' not found after load_ptx".to_string())
})?;
let mut d_out = dev
.alloc_zeros::<f32>(n)
.map_err(|e| CudaError::Launch(format!("silu_mul out alloc: {e:?}")))?;
let block = 256u32;
let grid = (n as u32).div_ceil(block);
let cfg = cudarc::driver::LaunchConfig {
grid_dim: (grid.max(1), 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: 0,
};
unsafe {
func.launch(cfg, (gate, up, &mut d_out, n as i32))
.map_err(|e| CudaError::Launch(format!("silu_mul launch: {e:?}")))?;
}
Ok(d_out)
}
pub const FUSED_ADD_RMSNORM_KERNEL_SRC: &str = r#"
extern "C" __global__ void fused_add_rmsnorm_f32(
const float* x,
const float* residual,
const float* weight,
float* out,
int n,
float eps
) {
__shared__ float partial[256];
int tid = threadIdx.x;
int tg = blockDim.x;
float acc = 0.0f;
for (int i = tid; i < n; i += tg) {
float v = x[i] + residual[i];
acc += v * v;
}
partial[tid] = acc;
__syncthreads();
for (int stride = tg / 2; stride > 0; stride >>= 1) {
if (tid < stride) {
partial[tid] += partial[tid + stride];
}
__syncthreads();
}
float inv_rms = rsqrtf(partial[0] / (float)n + eps);
for (int i = tid; i < n; i += tg) {
float v = x[i] + residual[i];
out[i] = v * inv_rms * weight[i];
}
}
"#;
pub fn launch_fused_add_rmsnorm(
x: &[f32],
residual: &[f32],
weight: &[f32],
eps: f32,
) -> Result<Vec<f32>, CudaError> {
use cudarc::driver::LaunchAsync;
assert_eq!(x.len(), residual.len());
assert_eq!(x.len(), weight.len());
let n = x.len();
if n == 0 {
return Ok(Vec::new());
}
let dev = shared_device()?;
ensure_module_loaded(
&dev,
FUSED_ADD_RMSNORM_KERNEL_SRC,
"ferrox_fused_add_rmsnorm",
"fused_add_rmsnorm_f32",
)?;
let func = dev
.get_func("ferrox_fused_add_rmsnorm", "fused_add_rmsnorm_f32")
.ok_or_else(|| {
CudaError::KernelCompile(
"function 'fused_add_rmsnorm_f32' not found after load_ptx".to_string(),
)
})?;
let d_x = dev
.htod_copy(x.to_vec())
.map_err(|e| CudaError::Launch(format!("fused_add_rmsnorm x upload: {e:?}")))?;
let d_residual = dev
.htod_copy(residual.to_vec())
.map_err(|e| CudaError::Launch(format!("fused_add_rmsnorm residual upload: {e:?}")))?;
let d_weight = dev
.htod_copy(weight.to_vec())
.map_err(|e| CudaError::Launch(format!("fused_add_rmsnorm weight upload: {e:?}")))?;
let mut d_out = dev
.alloc_zeros::<f32>(n)
.map_err(|e| CudaError::Launch(format!("fused_add_rmsnorm out alloc: {e:?}")))?;
let block = 256u32;
let cfg = cudarc::driver::LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: 256 * std::mem::size_of::<f32>() as u32,
};
unsafe {
func.launch(
cfg,
(&d_x, &d_residual, &d_weight, &mut d_out, n as i32, eps),
)
.map_err(|e| CudaError::Launch(format!("fused_add_rmsnorm launch: {e:?}")))?;
}
dev.dtoh_sync_copy(&d_out)
.map_err(|e| CudaError::Launch(format!("fused_add_rmsnorm download: {e:?}")))
}
pub fn launch_dense_ffn_swiglu(
gate: &MatvecLaunch<'_>,
up: &MatvecLaunch<'_>,
down: &MatvecLaunch<'_>,
x: &[f32],
) -> Result<Vec<f32>, CudaError> {
let dev = shared_device()?;
let d_x = dev
.htod_copy(x.to_vec())
.map_err(|e| CudaError::Launch(format!("ffn x upload: {e:?}")))?;
let (d_gate, _wg) = enqueue_matvec(&dev, gate, &d_x)?;
let (d_up, _wu) = enqueue_matvec(&dev, up, &d_x)?;
let d_act = silu_mul_device(&dev, &d_gate, &d_up, gate.rows)?;
let (d_out, _wd) = enqueue_matvec(&dev, down, &d_act)?;
dev.dtoh_sync_copy(&d_out)
.map_err(|e| CudaError::Launch(format!("ffn out download: {e:?}")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_degrades_cleanly_regardless_of_real_hardware_presence() {
match probe() {
None => {} Some(info) => {
assert!(
info.device_count >= 1,
"reported a device but device_count=0"
);
assert!(
info.total_vram_bytes > 0,
"reported a device but total_vram_bytes=0"
);
assert!(
info.free_vram_bytes <= info.total_vram_bytes,
"free VRAM ({}) cannot exceed total ({})",
info.free_vram_bytes,
info.total_vram_bytes
);
}
}
}
fn real_q8_0_test_matrix(rows: usize, cols: usize) -> (Vec<u8>, Vec<f32>, Vec<f32>) {
let mut weights = Vec::new();
let mut all_rows_f32 = Vec::new();
for r in 0..rows {
let row: Vec<f32> = (0..cols)
.map(|i| (((r * cols + i) as f32) * 0.037).sin())
.collect();
weights.extend(ferrox_quant::quantize_q8_0(&row));
all_rows_f32.push(row);
}
let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.019).cos()).collect();
let expected: Vec<f32> = all_rows_f32
.iter()
.map(|row| ferrox_quant::dot_q8_0_f32_scalar(&ferrox_quant::quantize_q8_0(row), &x))
.collect();
(weights, x, expected)
}
#[test]
#[ignore = "requires real CUDA hardware -- verified passing on an RTX 3060 (vast.ai); run with --ignored on a CUDA-capable machine to re-verify"]
fn launch_q8_0_matvec_matches_cpu_reference() {
let rows = 4;
let cols = 64;
let row_bytes = (cols / ferrox_quant::Q8_0_BLOCK_ELEMS) * ferrox_quant::Q8_0_BLOCK_BYTES;
let (weights, x, expected) = real_q8_0_test_matrix(rows, cols);
let result = launch_q8_0_matvec(
&weights,
&x,
rows,
row_bytes,
cols / ferrox_quant::Q8_0_BLOCK_ELEMS,
)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(result.len(), expected.len());
for (i, (got, want)) in result.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-2,
"row {i}: GPU={got} CPU reference={want}"
);
}
}
#[test]
#[ignore = "requires real CUDA hardware -- verified passing on an RTX 3060 (vast.ai); run with --ignored on a CUDA-capable machine to re-verify"]
fn launch_q4_0_matvec_matches_cpu_reference() {
let rows = 4;
let cols = 64;
let blocks_per_row = cols / ferrox_quant::Q4_0_BLOCK_ELEMS;
let row_bytes = blocks_per_row * ferrox_quant::Q4_0_BLOCK_BYTES;
let mut weights = Vec::new();
for r in 0..rows {
for b in 0..blocks_per_row {
weights.extend_from_slice(
&half::f16::from_f32(0.05 + (r * blocks_per_row + b) as f32 * 0.01)
.to_le_bytes(),
);
for i in 0..16u8 {
let lo = (i + r as u8 + b as u8) % 16;
let hi = (15 - i + r as u8 + b as u8) % 16;
weights.push(lo | (hi << 4));
}
}
}
let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.09).sin()).collect();
let expected: Vec<f32> = (0..rows)
.map(|r| {
let row_bytes_slice = &weights[r * row_bytes..(r + 1) * row_bytes];
ferrox_quant::dot_q4_0_f32_scalar(row_bytes_slice, &x)
})
.collect();
let result = launch_q4_0_matvec(
&weights,
&x,
rows,
row_bytes,
cols / ferrox_quant::Q4_0_BLOCK_ELEMS,
)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(result.len(), expected.len());
for (i, (got, want)) in result.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-2,
"row {i}: GPU={got} CPU reference={want}"
);
}
}
#[test]
fn matvec_launch_meta_is_the_matvec_table() {
for k in crate::matvec_kinds::KINDS {
let (src, module, func) = matvec_launch_meta(k.name)
.unwrap_or_else(|| panic!("{}: in the table but not launchable", k.name));
assert_eq!(module, k.module_name, "{}", k.name);
assert_eq!(func, k.fn_name, "{}", k.name);
assert!(
src.contains(&format!("void {func}(")),
"{}: the source in {module} does not define {func}",
k.name
);
}
assert!(
matvec_launch_meta("Q4_1").is_none(),
"a kind with no kernel resolved to a CUDA matvec"
);
}
#[test]
fn the_q5_0_matvec_strides_by_the_real_block_geometry() {
assert_eq!(ferrox_quant::Q5_0_BLOCK_BYTES, 22);
assert_eq!(ferrox_quant::Q5_0_BLOCK_ELEMS, 32);
assert!(
Q5_0_MATVEC_KERNEL_SRC.contains("(size_t)b * 22"),
"the Q5_0 matvec does not stride by Q5_0_BLOCK_BYTES"
);
assert!(
Q5_0_MATVEC_KERNEL_SRC.contains("int base = b * 32;"),
"the Q5_0 matvec does not step the activation by Q5_0_BLOCK_ELEMS"
);
}
#[test]
#[ignore = "requires real CUDA hardware -- NEVER RUN: the Q5_0 matvec has never executed on a GPU. Run with --ignored on a CUDA-capable machine and record the result before any doc claims CUDA Q5_0 works"]
fn launch_q5_0_matvec_matches_cpu_reference() {
let rows = 4;
let cols = 64;
let blocks_per_row = cols / ferrox_quant::Q5_0_BLOCK_ELEMS;
let row_bytes = blocks_per_row * ferrox_quant::Q5_0_BLOCK_BYTES;
let mut weights = Vec::with_capacity(rows * row_bytes);
for r in 0..rows {
for b in 0..blocks_per_row {
let idx = (r * blocks_per_row + b) as u32;
weights.extend_from_slice(
&half::f16::from_f32(0.05 + idx as f32 * 0.01).to_le_bytes(),
);
let qh = 0x9E3D_7A51u32.wrapping_mul(idx + 1);
weights.extend_from_slice(&qh.to_le_bytes());
for i in 0..16u8 {
let lo = (i + r as u8 + b as u8) % 16;
let hi = (15 - i + r as u8 + b as u8) % 16;
weights.push(lo | (hi << 4));
}
}
}
assert_eq!(weights.len(), rows * row_bytes);
let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.09).sin()).collect();
let expected: Vec<f32> = (0..rows)
.map(|r| {
ferrox_quant::dot_q5_0_f32_scalar(&weights[r * row_bytes..(r + 1) * row_bytes], &x)
})
.collect();
let result = launch_q5_0_matvec(&weights, &x, rows, row_bytes, blocks_per_row)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(result.len(), expected.len());
for (i, (got, want)) in result.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-2,
"row {i}: GPU={got} CPU reference={want}"
);
}
}
fn pseudo_bytes(seed: u32, len: usize) -> Vec<u8> {
let mut state = seed.wrapping_mul(2654435761).wrapping_add(1);
(0..len)
.map(|_| {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
(state >> 16) as u8
})
.collect()
}
fn assert_close_relative(got: f32, want: f32, row: usize) {
if want.is_nan() {
assert!(
got.is_nan(),
"row {row}: CPU reference is NaN but GPU={got} is not"
);
return;
}
let tol = 1e-4 * want.abs().max(1.0);
assert!(
(got - want).abs() <= tol,
"row {row}: GPU={got} CPU reference={want} (relative tolerance {tol})"
);
}
fn real_k_quant_test_matrix(
rows: usize,
cols: usize,
block_bytes: usize,
scalar_dot: impl Fn(&[u8], &[f32]) -> f32,
) -> (Vec<u8>, Vec<f32>, Vec<f32>) {
let n_blocks_per_row = cols / 256;
let row_bytes = n_blocks_per_row * block_bytes;
let mut weights = Vec::with_capacity(rows * row_bytes);
for r in 0..rows {
weights.extend(pseudo_bytes(r as u32 + 1, row_bytes));
}
let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.021).sin()).collect();
let expected: Vec<f32> = (0..rows)
.map(|r| scalar_dot(&weights[r * row_bytes..(r + 1) * row_bytes], &x))
.collect();
(weights, x, expected)
}
#[test]
#[ignore = "requires real CUDA hardware -- verifies every coalesced matvec against its scalar twin"]
fn every_coalesced_matvec_matches_its_twin() {
type Twin = fn(&[u8], &[f32], usize) -> f32;
let cases: &[(&str, usize, usize, u32, Twin)] = &[
(
"Q4_K",
144,
256,
7,
crate::coalesced_twin::q4_k_matvec_coalesced_row,
),
(
"Q5_K",
176,
256,
17,
crate::coalesced_twin::q5_k_matvec_coalesced_row,
),
(
"Q6_K",
210,
256,
11,
crate::coalesced_twin::q6_k_matvec_coalesced_row,
),
(
"Q8_0",
34,
32,
23,
crate::coalesced_twin::q8_0_matvec_coalesced_row,
),
];
let rows = 37usize; let n_blocks_per_row = 3usize;
for (kind, block_bytes, per_block, seed, twin) in cases {
let row_bytes = n_blocks_per_row * block_bytes;
let cols = n_blocks_per_row * per_block;
let weights = pseudo_bytes(*seed, rows * row_bytes);
let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.021).sin()).collect();
let (src, module, fn_name) = super::matvec_launch_meta(kind)
.unwrap_or_else(|| panic!("{kind} has no matvec kernel"));
let got = super::launch_matvec(
src,
module,
fn_name,
&weights,
&x,
rows,
row_bytes,
n_blocks_per_row,
)
.unwrap_or_else(|e| panic!("{kind}: {e:?}"));
assert_eq!(got.len(), rows, "{kind}");
for r in 0..rows {
let row = &weights[r * row_bytes..(r + 1) * row_bytes];
let want = twin(row, &x, n_blocks_per_row);
assert!(
!(want.is_nan() ^ got[r].is_nan()),
"{kind} row {r}: GPU={} twin={want}",
got[r]
);
assert_close_relative(got[r], want, r);
}
}
}
#[test]
fn the_plan_pairs_each_kernel_with_its_own_geometry() {
let rows = 37usize;
for kind in ["Q4_K", "Q5_K", "Q6_K", "Q8_0"] {
let (src, module, fn_name) = super::matvec_launch_meta(kind).expect("kernel");
let (plan_src, plan_module, entry, cfg) =
super::matvec_launch_plan(fn_name, src, module, rows);
assert!(
entry.ends_with("_coalesced"),
"{kind}: plan chose {entry}, not the coalesced kernel"
);
assert!(
plan_src.contains(entry) && plan_module.ends_with("_coalesced"),
"{kind}: plan's source and module do not match {entry}"
);
let warps = super::COALESCED_WARPS_PER_BLOCK as u32;
assert_eq!(cfg.block_dim, (warps * 32, 1, 1), "{kind}: block_dim");
assert_eq!(cfg.shared_mem_bytes, 0, "{kind}: needs no shared memory");
assert!(
cfg.grid_dim.0 * warps >= rows as u32,
"{kind}: {} blocks x {warps} warps does not cover {rows} rows",
cfg.grid_dim.0
);
assert!(
(cfg.grid_dim.0 - 1) * warps < rows as u32,
"{kind}: {} blocks is more than the tail needs",
cfg.grid_dim.0
);
}
let (src, module, fn_name) = super::matvec_launch_meta("Q4_0").expect("kernel");
let (plan_src, plan_module, entry, cfg) =
super::matvec_launch_plan(fn_name, src, module, rows);
assert_eq!(entry, fn_name);
assert!(std::ptr::eq(plan_src, src) && std::ptr::eq(plan_module, module));
assert_eq!(cfg.grid_dim, (rows as u32, 1, 1), "one block per row");
assert_eq!(cfg.block_dim, (256, 1, 1));
assert!(
cfg.shared_mem_bytes > 0,
"the block-per-row kernels reduce through shared memory"
);
}
#[test]
fn every_coalesced_kernel_is_reachable_from_the_table() {
let src = include_str!("gpu.rs");
let mut found = 0usize;
for line in src.lines() {
let Some(rest) = line.strip_prefix(r#"extern "C" __global__ void "#) else {
continue;
};
let Some(entry) = rest.split('(').next() else {
continue;
};
let Some(base) = entry.strip_suffix("_coalesced") else {
continue;
};
found += 1;
let routed = super::coalesced_matvec_kernel(base).unwrap_or_else(|| {
panic!("kernel {entry} is not reachable: no table row for {base}")
});
assert_eq!(
routed.2, entry,
"table row for {base} names the wrong entry point"
);
assert!(
routed.0.contains(entry),
"table row for {base} points at a source that does not define {entry}"
);
}
assert!(
found >= 4,
"expected at least four coalesced kernels, found {found}"
);
}
#[test]
#[ignore = "requires real CUDA hardware -- verified passing on an RTX 3060 (vast.ai, 2026-07-31) with the relative-tolerance fix; run with --ignored on a CUDA-capable machine to re-verify"]
fn launch_q4_k_matvec_matches_cpu_reference() {
let rows = 3;
let cols = 256; let (weights, x, expected) = real_k_quant_test_matrix(
rows,
cols,
ferrox_quant::Q4_K_BLOCK_BYTES,
ferrox_quant::dot_q4_k_f32_scalar,
);
let result = launch_q4_k_matvec(&weights, &x, rows, ferrox_quant::Q4_K_BLOCK_BYTES, 1)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(result.len(), expected.len());
for (i, (got, want)) in result.iter().zip(expected.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
}
#[test]
#[ignore = "requires real CUDA hardware -- verified passing on an RTX 3060 (vast.ai, 2026-07-31) with the relative-tolerance fix; run with --ignored on a CUDA-capable machine to re-verify"]
fn launch_q5_k_matvec_matches_cpu_reference() {
let rows = 3;
let cols = 256; let (weights, x, expected) = real_k_quant_test_matrix(
rows,
cols,
ferrox_quant::Q5_K_BLOCK_BYTES,
ferrox_quant::dot_q5_k_f32_scalar,
);
let result = launch_q5_k_matvec(&weights, &x, rows, ferrox_quant::Q5_K_BLOCK_BYTES, 1)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(result.len(), expected.len());
for (i, (got, want)) in result.iter().zip(expected.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
}
#[test]
#[ignore = "requires real CUDA hardware -- 2026-07-31 real RTX 3060 run failed on row 1 with GPU=NaN CPU reference=NaN (assert_close_relative didn't treat NaN==NaN as agreement; fixed, but not yet re-verified on hardware); run with --ignored on a CUDA-capable machine to re-verify"]
fn launch_q6_k_matvec_matches_cpu_reference() {
let rows = 3;
let cols = 512; let (weights, x, expected) = real_k_quant_test_matrix(
rows,
cols,
ferrox_quant::Q6_K_BLOCK_BYTES,
ferrox_quant::dot_q6_k_f32_scalar,
);
let result = launch_q6_k_matvec(&weights, &x, rows, ferrox_quant::Q6_K_BLOCK_BYTES * 2, 2)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(result.len(), expected.len());
for (i, (got, want)) in result.iter().zip(expected.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
}
#[test]
#[ignore = "requires real CUDA hardware -- verifies that launch_matvec_multi (shared x upload) matches N sequential single-matvec launches; run with --ignored on a CUDA-capable machine"]
fn launch_matvec_multi_matches_sequential() {
let cols = 256;
let rows_a = 2;
let rows_b = 3;
let rows_c = 4;
let (weights_a, x, expected_a) = real_k_quant_test_matrix(
rows_a,
cols,
ferrox_quant::Q4_K_BLOCK_BYTES,
ferrox_quant::dot_q4_k_f32_scalar,
);
let (weights_b, _, expected_b) = real_k_quant_test_matrix(
rows_b,
cols,
ferrox_quant::Q4_K_BLOCK_BYTES,
ferrox_quant::dot_q4_k_f32_scalar,
);
let (weights_c, _, expected_c) = real_k_quant_test_matrix(
rows_c,
cols,
ferrox_quant::Q4_K_BLOCK_BYTES,
ferrox_quant::dot_q4_k_f32_scalar,
);
let launches = [
MatvecLaunch {
kernel_src: Q4_K_MATVEC_KERNEL_SRC,
module_name: "ferrox_q4_k",
fn_name: "q4_k_matvec",
weights: weights_a.as_slice(),
rows: rows_a,
row_bytes: ferrox_quant::Q4_K_BLOCK_BYTES,
n_blocks_per_row: 1,
},
MatvecLaunch {
kernel_src: Q4_K_MATVEC_KERNEL_SRC,
module_name: "ferrox_q4_k",
fn_name: "q4_k_matvec",
weights: weights_b.as_slice(),
rows: rows_b,
row_bytes: ferrox_quant::Q4_K_BLOCK_BYTES,
n_blocks_per_row: 1,
},
MatvecLaunch {
kernel_src: Q4_K_MATVEC_KERNEL_SRC,
module_name: "ferrox_q4_k",
fn_name: "q4_k_matvec",
weights: weights_c.as_slice(),
rows: rows_c,
row_bytes: ferrox_quant::Q4_K_BLOCK_BYTES,
n_blocks_per_row: 1,
},
];
let results = launch_matvec_multi(&x, &launches)
.expect("multi-matvec must succeed on real CUDA hardware");
assert_eq!(results.len(), 3);
assert_eq!(results[0].len(), expected_a.len());
for (i, (got, want)) in results[0].iter().zip(expected_a.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
assert_eq!(results[1].len(), expected_b.len());
for (i, (got, want)) in results[1].iter().zip(expected_b.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
assert_eq!(results[2].len(), expected_c.len());
for (i, (got, want)) in results[2].iter().zip(expected_c.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
}
#[test]
#[ignore = "requires real CUDA hardware -- run with --ignored on a CUDA-capable machine to verify the fused dense-FFN activation-residency path"]
fn launch_dense_ffn_swiglu_matches_sequential_cpu() {
let hidden_dim = 64; let ffn_dim = 96;
let make_row = |cols: usize, seed: f32| -> Vec<f32> {
(0..cols)
.map(|i| (((i as f32) - (cols as f32) / 2.0) * 0.013 * seed).sin())
.collect()
};
let build = |rows: usize, cols: usize, seed: f32| -> (Vec<u8>, usize) {
let mut packed = Vec::new();
for r in 0..rows {
packed.extend(ferrox_quant::quantize_q8_0(&make_row(
cols,
seed + r as f32,
)));
}
let row_bytes =
(cols / ferrox_quant::Q8_0_BLOCK_ELEMS) * ferrox_quant::Q8_0_BLOCK_BYTES;
(packed, row_bytes)
};
let (gate_w, gate_rb) = build(ffn_dim, hidden_dim, 1.0);
let (up_w, up_rb) = build(ffn_dim, hidden_dim, 2.0);
let (down_w, down_rb) = build(hidden_dim, ffn_dim, 3.0);
let x = make_row(hidden_dim, 0.7);
let blk = |cols: usize| cols / ferrox_quant::Q8_0_BLOCK_ELEMS;
let gate = MatvecLaunch {
kernel_src: Q8_0_MATVEC_KERNEL_SRC,
module_name: "ferrox_q8_0",
fn_name: "q8_0_matvec",
weights: gate_w.as_slice(),
rows: ffn_dim,
row_bytes: gate_rb,
n_blocks_per_row: blk(hidden_dim),
};
let up = MatvecLaunch {
kernel_src: Q8_0_MATVEC_KERNEL_SRC,
module_name: "ferrox_q8_0",
fn_name: "q8_0_matvec",
weights: up_w.as_slice(),
rows: ffn_dim,
row_bytes: up_rb,
n_blocks_per_row: blk(hidden_dim),
};
let down = MatvecLaunch {
kernel_src: Q8_0_MATVEC_KERNEL_SRC,
module_name: "ferrox_q8_0",
fn_name: "q8_0_matvec",
weights: down_w.as_slice(),
rows: hidden_dim,
row_bytes: down_rb,
n_blocks_per_row: blk(ffn_dim),
};
let gpu = launch_dense_ffn_swiglu(&gate, &up, &down, &x)
.expect("fused FFN must launch on real CUDA hardware");
let cpu_matvec = |w: &[u8], row_bytes: usize, rows: usize, act: &[f32]| -> Vec<f32> {
(0..rows)
.map(|r| {
ferrox_quant::dot_q8_0_f32_scalar(&w[r * row_bytes..(r + 1) * row_bytes], act)
})
.collect::<Vec<f32>>()
};
let g = cpu_matvec(&gate_w, gate_rb, ffn_dim, &x);
let u = cpu_matvec(&up_w, up_rb, ffn_dim, &x);
let act: Vec<f32> = g
.iter()
.zip(u.iter())
.map(|(gv, uv)| (gv / (1.0 + (-gv).exp())) * uv)
.collect();
let expected = cpu_matvec(&down_w, down_rb, hidden_dim, &act);
assert_eq!(gpu.len(), expected.len());
for (i, (got, want)) in gpu.iter().zip(expected.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
}
#[test]
#[ignore = "requires real CUDA hardware -- run with --ignored to verify fused_add_rmsnorm vs CPU rms_norm"]
fn launch_fused_add_rmsnorm_matches_cpu_reference() {
let n = 128;
let x: Vec<f32> = (0..n).map(|i| ((i as f32) * 0.031).sin()).collect();
let residual: Vec<f32> = (0..n).map(|i| ((i as f32) * 0.017).cos()).collect();
let weight: Vec<f32> = (0..n).map(|i| 1.0 + ((i as f32) * 0.003).sin()).collect();
let eps = 1e-5f32;
let sum: Vec<f32> = x.iter().zip(residual.iter()).map(|(a, b)| a + b).collect();
let mean_sq = sum.iter().map(|v| v * v).sum::<f32>() / n as f32;
let scale = 1.0 / (mean_sq + eps).sqrt();
let expected: Vec<f32> = sum
.iter()
.zip(weight.iter())
.map(|(v, w)| v * scale * w)
.collect();
let gpu = launch_fused_add_rmsnorm(&x, &residual, &weight, eps)
.expect("fused_add_rmsnorm must launch on CUDA hardware");
assert_eq!(gpu.len(), expected.len());
for (i, (got, want)) in gpu.iter().zip(expected.iter()).enumerate() {
assert_close_relative(*got, *want, i);
}
}
}