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 Q8_0_MATVEC_KERNEL_SRC: &str = r#"
extern "C" __global__ void q8_0_matvec(
const unsigned char* weights, // [rows * row_bytes]
const float* x, // [cols]
float* out, // [rows]
int rows,
int row_bytes,
int n_blocks_per_row
) {
int row = blockIdx.x;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
__shared__ float partial[256];
float acc = 0.0f;
for (int b = threadIdx.x; b < n_blocks_per_row; b += blockDim.x) {
const unsigned char* block = row_ptr + b * 34;
unsigned short bits = (unsigned short)block[0] | ((unsigned short)block[1] << 8);
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);
}
if (sign) scale = -scale;
int base = b * 32;
float block_acc = 0.0f;
#pragma unroll
for (int i = 0; i < 32; i++) {
signed char q = (signed char)block[2 + i];
block_acc += (float)q * x[base + i];
}
acc += block_acc * scale;
}
partial[threadIdx.x] = acc;
__syncthreads();
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (threadIdx.x < stride) {
partial[threadIdx.x] += partial[threadIdx.x + stride];
}
__syncthreads();
}
if (threadIdx.x == 0) {
out[row] = partial[0];
}
}
"#;
pub const Q4_0_MATVEC_KERNEL_SRC: &str = r#"
extern "C" __global__ void q4_0_matvec(
const unsigned char* weights, // [rows * row_bytes]
const float* x, // [cols]
float* out, // [rows]
int rows,
int row_bytes,
int n_blocks_per_row
) {
int row = blockIdx.x;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
__shared__ float partial[256];
float acc = 0.0f;
for (int b = threadIdx.x; b < n_blocks_per_row; b += blockDim.x) {
const unsigned char* block = row_ptr + b * 18;
unsigned short bits = (unsigned short)block[0] | ((unsigned short)block[1] << 8);
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);
}
if (sign) scale = -scale;
int base = b * 32;
float block_acc = 0.0f;
#pragma unroll
for (int i = 0; i < 16; i++) {
unsigned char byte = block[2 + i];
int lo = (int)(byte & 0x0F) - 8;
int hi = (int)((byte >> 4) & 0x0F) - 8;
block_acc += (float)lo * x[base + i];
block_acc += (float)hi * x[base + i + 16];
}
acc += block_acc * scale;
}
partial[threadIdx.x] = acc;
__syncthreads();
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (threadIdx.x < stride) {
partial[threadIdx.x] += partial[threadIdx.x + stride];
}
__syncthreads();
}
if (threadIdx.x == 0) {
out[row] = partial[0];
}
}
"#;
pub const Q4_K_MATVEC_KERNEL_SRC: &str = r#"
extern "C" __device__ float ferrox_f16_to_f32(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" __device__ void ferrox_q4_k_scale_min(
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(
const unsigned char* weights, // [rows * row_bytes], row_bytes = n_blocks_per_row * 144
const float* x, // [cols]
float* out, // [rows]
int rows,
int row_bytes,
int n_blocks_per_row
) {
int row = blockIdx.x;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
__shared__ float partial[256];
float acc = 0.0f;
for (int blk = threadIdx.x; blk < n_blocks_per_row; blk += blockDim.x) {
const unsigned char* block = row_ptr + blk * 144;
unsigned short d_bits = (unsigned short)block[0] | ((unsigned short)block[1] << 8);
unsigned short dmin_bits = (unsigned short)block[2] | ((unsigned short)block[3] << 8);
float d = ferrox_f16_to_f32(d_bits);
float dmin = ferrox_f16_to_f32(dmin_bits);
const unsigned char* scales = block + 4;
const unsigned char* qs = block + 16;
int x_base = blk * 256;
int is = 0, q_off = 0, base = 0;
#pragma unroll
for (int oi = 0; oi < 4; oi++) {
unsigned char sc1, m1, sc2, m2;
ferrox_q4_k_scale_min(is, scales, &sc1, &m1);
ferrox_q4_k_scale_min(is + 1, scales, &sc2, &m2);
float d1 = d * (float)sc1, min1 = dmin * (float)m1;
float d2 = d * (float)sc2, min2 = dmin * (float)m2;
#pragma unroll
for (int l = 0; l < 32; l++) {
acc += (d1 * (float)(qs[q_off + l] & 0x0F) - min1) * x[x_base + base + l];
}
#pragma unroll
for (int l = 0; l < 32; l++) {
acc += (d2 * (float)(qs[q_off + l] >> 4) - min2) * x[x_base + base + 32 + l];
}
q_off += 32;
base += 64;
is += 2;
}
}
partial[threadIdx.x] = acc;
__syncthreads();
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (threadIdx.x < stride) {
partial[threadIdx.x] += partial[threadIdx.x + stride];
}
__syncthreads();
}
if (threadIdx.x == 0) {
out[row] = partial[0];
}
}
"#;
pub const Q5_K_MATVEC_KERNEL_SRC: &str = r#"
extern "C" __device__ float ferrox_f16_to_f32(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" __device__ void ferrox_q4_k_scale_min(
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(
const unsigned char* weights, // [rows * row_bytes], row_bytes = n_blocks_per_row * 176
const float* x, // [cols]
float* out, // [rows]
int rows,
int row_bytes,
int n_blocks_per_row
) {
int row = blockIdx.x;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
__shared__ float partial[256];
float acc = 0.0f;
for (int blk = threadIdx.x; blk < n_blocks_per_row; blk += blockDim.x) {
const unsigned char* block = row_ptr + blk * 176;
unsigned short d_bits = (unsigned short)block[0] | ((unsigned short)block[1] << 8);
unsigned short dmin_bits = (unsigned short)block[2] | ((unsigned short)block[3] << 8);
float d = ferrox_f16_to_f32(d_bits);
float dmin = ferrox_f16_to_f32(dmin_bits);
const unsigned char* scales = block + 4;
const unsigned char* qh = block + 16;
const unsigned char* qs = block + 48;
int x_base = blk * 256;
int is = 0;
unsigned char u1 = 1, u2 = 2;
#pragma unroll
for (int oi = 0; oi < 4; oi++) {
unsigned char sc1, m1, sc2, m2;
ferrox_q4_k_scale_min(is, scales, &sc1, &m1);
ferrox_q4_k_scale_min(is + 1, scales, &sc2, &m2);
float d1 = d * (float)sc1, min1 = dmin * (float)m1;
float d2 = d * (float)sc2, min2 = dmin * (float)m2;
const unsigned char* ql = qs + oi * 32;
int xb = x_base + oi * 64;
#pragma unroll
for (int l = 0; l < 32; l++) {
int hi = (qh[l] & u1) ? 16 : 0;
acc += (d1 * (float)((ql[l] & 0x0F) + hi) - min1) * x[xb + l];
}
#pragma unroll
for (int l = 0; l < 32; l++) {
int hi = (qh[l] & u2) ? 16 : 0;
acc += (d2 * (float)((ql[l] >> 4) + hi) - min2) * x[xb + 32 + l];
}
is += 2;
u1 <<= 2;
u2 <<= 2;
}
}
partial[threadIdx.x] = acc;
__syncthreads();
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (threadIdx.x < stride) {
partial[threadIdx.x] += partial[threadIdx.x + stride];
}
__syncthreads();
}
if (threadIdx.x == 0) {
out[row] = partial[0];
}
}
"#;
pub const Q6_K_MATVEC_KERNEL_SRC: &str = r#"
extern "C" __device__ float ferrox_f16_to_f32(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(
const unsigned char* weights, // [rows * row_bytes], row_bytes = n_blocks_per_row * 210
const float* x, // [cols]
float* out, // [rows]
int rows,
int row_bytes,
int n_blocks_per_row
) {
int row = blockIdx.x;
if (row >= rows) return;
const unsigned char* row_ptr = weights + (size_t)row * row_bytes;
__shared__ float partial[256];
float acc = 0.0f;
for (int blk = threadIdx.x; blk < n_blocks_per_row; blk += blockDim.x) {
const unsigned char* block = row_ptr + blk * 210;
const unsigned char* ql_full = block;
const unsigned char* qh_full = block + 128;
const unsigned char* sc_full = block + 192;
unsigned short d_bits = (unsigned short)block[208] | ((unsigned short)block[209] << 8);
float d = ferrox_f16_to_f32(d_bits);
int x_base = blk * 256;
#pragma unroll
for (int half = 0; half < 2; half++) {
const unsigned char* ql = ql_full + half * 64;
const unsigned char* qh = qh_full + half * 32;
const unsigned char* sc = sc_full + half * 8;
int xh_base = x_base + half * 128;
#pragma unroll
for (int l = 0; l < 32; l++) {
int is = l / 16;
int q1 = (int)((ql[l] & 0x0F) | ((qh[l] & 0x03) << 4)) - 32;
int q2 = (int)((ql[l + 32] & 0x0F) | (((qh[l] >> 2) & 0x03) << 4)) - 32;
int q3 = (int)((ql[l] >> 4) | (((qh[l] >> 4) & 0x03) << 4)) - 32;
int q4 = (int)((ql[l + 32] >> 4) | (((qh[l] >> 6) & 0x03) << 4)) - 32;
acc += d * (float)(signed char)sc[is] * (float)q1 * x[xh_base + l];
acc += d * (float)(signed char)sc[is + 2] * (float)q2 * x[xh_base + l + 32];
acc += d * (float)(signed char)sc[is + 4] * (float)q3 * x[xh_base + l + 64];
acc += d * (float)(signed char)sc[is + 6] * (float)q4 * x[xh_base + l + 96];
}
}
}
partial[threadIdx.x] = acc;
__syncthreads();
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (threadIdx.x < stride) {
partial[threadIdx.x] += partial[threadIdx.x + stride];
}
__syncthreads();
}
if (threadIdx.x == 0) {
out[row] = partial[0];
}
}
"#;
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 _reuse = take_resident_activation_if_matches(x);
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)?;
let out = dev
.dtoh_sync_copy(&d_out)
.map_err(|e| CudaError::Launch(format!("{e:?}")))?;
Ok(out)
}
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;
ensure_module_loaded(dev, launch.kernel_src, launch.module_name, launch.fn_name)?;
let func = dev
.get_func(launch.module_name, launch.fn_name)
.ok_or_else(|| {
CudaError::KernelCompile(format!(
"function '{}' not found after load_ptx",
launch.fn_name
))
})?;
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:?}")))?;
let cfg = cudarc::driver::LaunchConfig {
grid_dim: (launch.rows as u32, 1, 1),
block_dim: (256, 1, 1),
shared_mem_bytes: 256 * std::mem::size_of::<f32>() as u32,
};
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 {}: {e:?}", launch.fn_name)))?;
}
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 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(
Q8_0_MATVEC_KERNEL_SRC,
"ferrox_q8_0",
"q8_0_matvec",
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(
Q4_0_MATVEC_KERNEL_SRC,
"ferrox_q4_0",
"q4_0_matvec",
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(
Q4_K_MATVEC_KERNEL_SRC,
"ferrox_q4_k",
"q4_k_matvec",
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(
Q5_K_MATVEC_KERNEL_SRC,
"ferrox_q5_k",
"q5_k_matvec",
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(
Q6_K_MATVEC_KERNEL_SRC,
"ferrox_q6_k",
"q6_k_matvec",
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 struct DeviceAct {
slice: cudarc::driver::CudaSlice<f32>,
len: usize,
}
unsafe impl Send for DeviceAct {}
unsafe impl Sync for DeviceAct {}
impl DeviceAct {
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
pub fn upload_act(x: &[f32]) -> Result<DeviceAct, CudaError> {
let dev = shared_device()?;
let slice = dev
.htod_copy(x.to_vec())
.map_err(|e| CudaError::Launch(format!("act upload: {e:?}")))?;
Ok(DeviceAct {
slice,
len: x.len(),
})
}
pub fn download_act(act: &DeviceAct) -> Result<Vec<f32>, CudaError> {
let dev = shared_device()?;
dev.dtoh_sync_copy(&act.slice)
.map_err(|e| CudaError::Launch(format!("act download: {e:?}")))
}
pub fn matvec_into(launch: &MatvecLaunch<'_>, x: &DeviceAct) -> Result<DeviceAct, CudaError> {
let dev = shared_device()?;
let (d_out, _weights) = enqueue_matvec(&dev, launch, &x.slice)?;
Ok(DeviceAct {
slice: d_out,
len: launch.rows,
})
}
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:?}")))
}
#[allow(dead_code)] #[derive(Clone, Copy)]
struct ResidentActivation {
ptr: *const f32,
len: usize,
}
thread_local! {
static RESIDENT_ACT: std::cell::Cell<Option<ResidentActivation>> = const { std::cell::Cell::new(None) };
}
pub fn set_resident_activation(x: &[f32]) {
RESIDENT_ACT.set(Some(ResidentActivation {
ptr: x.as_ptr(),
len: x.len(),
}));
}
pub fn clear_resident_activation() {
RESIDENT_ACT.set(None);
}
fn take_resident_activation_if_matches(x: &[f32]) -> bool {
RESIDENT_ACT
.take()
.is_some_and(|res| res.len == x.len() && res.ptr == x.as_ptr())
}
#[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}"
);
}
}
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 -- 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 DeviceAct residency vs the host-wrapper matvec"]
fn matvec_into_matches_host_wrapper() {
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 host = launch_q8_0_matvec(&weights, &x, rows, row_bytes, cols / 32)
.expect("host-wrapper matvec must launch");
let d_x = upload_act(&x).expect("upload");
let launch = MatvecLaunch {
kernel_src: Q8_0_MATVEC_KERNEL_SRC,
module_name: "ferrox_q8_0",
fn_name: "q8_0_matvec",
weights: weights.as_slice(),
rows,
row_bytes,
n_blocks_per_row: cols / 32,
};
let d_out = matvec_into(&launch, &d_x).expect("device matvec");
let device = download_act(&d_out).expect("download");
assert_eq!(host.len(), device.len());
for (i, (h, d)) in host.iter().zip(device.iter()).enumerate() {
assert_close_relative(*d, *h, 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);
}
}
}