#![allow(clippy::all, clippy::pedantic, clippy::restriction, clippy::nursery)]
use crate::error::{WhisperError, WhisperResult};
use crate::simd;
pub const I8_MAX: f32 = 127.0;
pub const I4_MAX: f32 = 7.0;
pub const MIN_SCALE: f32 = 1e-10;
#[derive(Debug, Clone)]
pub struct QuantizedTensor {
pub data: Vec<i8>,
pub scale: f32,
pub zero_point: i8,
pub shape: Vec<usize>,
}
impl QuantizedTensor {
#[must_use]
pub fn from_f32(data: &[f32], shape: Vec<usize>) -> Self {
let (quantized, scale) = quantize_f32_to_i8(data);
Self {
data: quantized,
scale,
zero_point: 0,
shape,
}
}
#[must_use]
pub fn to_f32(&self) -> Vec<f32> {
dequantize_i8_to_f32(&self.data, self.scale)
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
#[must_use]
pub fn numel(&self) -> usize {
self.shape.iter().product()
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedTensorQ4K {
data: Vec<u8>,
n_values: usize,
shape: Vec<usize>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedTensorQ4K {
pub const SUPER_BLOCK_BYTES: usize = 144;
pub const VALUES_PER_BLOCK: usize = 256;
#[must_use]
pub fn from_raw(data: Vec<u8>, shape: Vec<usize>) -> Self {
let n_values = shape.iter().product();
Self {
data,
n_values,
shape,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.n_values
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.n_values == 0
}
#[must_use]
pub fn shape(&self) -> &[usize] {
&self.shape
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn dequantize(&self) -> Vec<f32> {
crate::realizar_inference::dequantize_q4_k(&self.data)
.unwrap_or_else(|_| vec![0.0; self.n_values])
}
#[must_use]
pub fn raw_data(&self) -> &[u8] {
&self.data
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedLinearQ4K {
weight: QuantizedTensorQ4K,
bias: Option<Vec<f32>>,
in_features: usize,
out_features: usize,
cached_weights_t: Option<Vec<f32>>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedLinearQ4K {
#[must_use]
pub fn from_raw(
weight_data: Vec<u8>,
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
let n_values = out_features * in_features;
let weight = QuantizedTensorQ4K::from_raw(weight_data, vec![out_features, in_features]);
Self {
weight: QuantizedTensorQ4K {
data: weight.data,
n_values,
shape: vec![out_features, in_features],
},
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
cached_weights_t: None,
}
}
pub fn finalize_weights(&mut self) {
if self.cached_weights_t.is_some() {
return; }
let weights = self.weight.dequantize();
let weights_t = simd::transpose(&weights, self.out_features, self.in_features);
self.cached_weights_t = Some(weights_t);
}
#[must_use]
pub fn is_finalized(&self) -> bool {
self.cached_weights_t.is_some()
}
#[must_use]
pub const fn in_features(&self) -> usize {
self.in_features
}
#[must_use]
pub const fn out_features(&self) -> usize {
self.out_features
}
#[must_use]
pub fn memory_size(&self) -> usize {
let weight_size = self.weight.memory_bytes();
let bias_size = self.bias.as_ref().map_or(0, |b| b.len() * 4);
weight_size + bias_size
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let weights_t: std::borrow::Cow<'_, [f32]> = if let Some(ref cached) = self.cached_weights_t
{
std::borrow::Cow::Borrowed(cached)
} else {
let weights = self.weight.dequantize();
std::borrow::Cow::Owned(simd::transpose(
&weights,
self.out_features,
self.in_features,
))
};
let mut output = simd::matmul(
input,
&weights_t,
batch_size,
self.in_features,
self.out_features,
);
if let Some(ref bias) = self.bias {
simd::broadcast_add_inplace(&mut output, bias, batch_size, self.out_features);
}
Ok(output)
}
pub fn forward_fused(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let mut output = Vec::with_capacity(batch_size * self.out_features);
for b in 0..batch_size {
let input_slice = &input[b * self.in_features..(b + 1) * self.in_features];
let batch_output = crate::realizar_inference::fused_q4k_parallel_matvec(
self.weight.raw_data(),
input_slice,
self.in_features,
self.out_features,
)
.map_err(|e| WhisperError::Model(format!("Fused Q4K matvec failed: {e}")))?;
output.extend(batch_output);
}
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedTensorQ5K {
data: Vec<u8>,
n_values: usize,
shape: Vec<usize>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedTensorQ5K {
pub const SUPER_BLOCK_BYTES: usize = 176;
pub const VALUES_PER_BLOCK: usize = 256;
#[must_use]
pub fn from_raw(data: Vec<u8>, shape: Vec<usize>) -> Self {
let n_values = shape.iter().product();
Self {
data,
n_values,
shape,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.n_values
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.n_values == 0
}
#[must_use]
pub fn shape(&self) -> &[usize] {
&self.shape
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn dequantize(&self) -> Vec<f32> {
crate::realizar_inference::dequantize_q5_k(&self.data)
.unwrap_or_else(|_| vec![0.0; self.n_values])
}
#[must_use]
pub fn raw_data(&self) -> &[u8] {
&self.data
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedLinearQ5K {
weight: QuantizedTensorQ5K,
bias: Option<Vec<f32>>,
in_features: usize,
out_features: usize,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedLinearQ5K {
#[must_use]
pub fn from_raw(
weight_data: Vec<u8>,
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
let n_values = out_features * in_features;
Self {
weight: QuantizedTensorQ5K {
data: weight_data,
n_values,
shape: vec![out_features, in_features],
},
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
}
}
#[must_use]
pub const fn in_features(&self) -> usize {
self.in_features
}
#[must_use]
pub const fn out_features(&self) -> usize {
self.out_features
}
#[must_use]
pub fn memory_size(&self) -> usize {
let weight_size = self.weight.memory_bytes();
let bias_size = self.bias.as_ref().map_or(0, |b| b.len() * 4);
weight_size + bias_size
}
pub fn forward_fused(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let mut output = Vec::with_capacity(batch_size * self.out_features);
for b in 0..batch_size {
let input_slice = &input[b * self.in_features..(b + 1) * self.in_features];
let batch_output = crate::realizar_inference::fused_q5k_parallel_matvec(
self.weight.raw_data(),
input_slice,
self.in_features,
self.out_features,
)
.map_err(|e| WhisperError::Model(format!("Fused Q5K matvec failed: {e}")))?;
output.extend(batch_output);
}
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedTensorQ6K {
data: Vec<u8>,
n_values: usize,
shape: Vec<usize>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedTensorQ6K {
pub const SUPER_BLOCK_BYTES: usize = 210;
pub const VALUES_PER_BLOCK: usize = 256;
#[must_use]
pub fn from_raw(data: Vec<u8>, shape: Vec<usize>) -> Self {
let n_values = shape.iter().product();
Self {
data,
n_values,
shape,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.n_values
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.n_values == 0
}
#[must_use]
pub fn shape(&self) -> &[usize] {
&self.shape
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn dequantize(&self) -> Vec<f32> {
crate::realizar_inference::dequantize_q6_k(&self.data)
.unwrap_or_else(|_| vec![0.0; self.n_values])
}
#[must_use]
pub fn raw_data(&self) -> &[u8] {
&self.data
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedLinearQ6K {
weight: QuantizedTensorQ6K,
bias: Option<Vec<f32>>,
in_features: usize,
out_features: usize,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedLinearQ6K {
#[must_use]
pub fn from_raw(
weight_data: Vec<u8>,
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
let n_values = out_features * in_features;
Self {
weight: QuantizedTensorQ6K {
data: weight_data,
n_values,
shape: vec![out_features, in_features],
},
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
}
}
#[must_use]
pub const fn in_features(&self) -> usize {
self.in_features
}
#[must_use]
pub const fn out_features(&self) -> usize {
self.out_features
}
#[must_use]
pub fn memory_size(&self) -> usize {
let weight_size = self.weight.memory_bytes();
let bias_size = self.bias.as_ref().map_or(0, |b| b.len() * 4);
weight_size + bias_size
}
pub fn forward_fused(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let mut output = Vec::with_capacity(batch_size * self.out_features);
for b in 0..batch_size {
let input_slice = &input[b * self.in_features..(b + 1) * self.in_features];
let batch_output = crate::realizar_inference::fused_q6k_parallel_matvec(
self.weight.raw_data(),
input_slice,
self.in_features,
self.out_features,
)
.map_err(|e| WhisperError::Model(format!("Fused Q6K matvec failed: {e}")))?;
output.extend(batch_output);
}
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedTensorQ2K {
data: Vec<u8>,
n_values: usize,
shape: Vec<usize>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedTensorQ2K {
pub const SUPER_BLOCK_BYTES: usize = 196;
pub const VALUES_PER_BLOCK: usize = 256;
pub const BITS_PER_WEIGHT: f32 = 6.125;
#[must_use]
pub fn from_raw(data: Vec<u8>, shape: Vec<usize>) -> Self {
let n_values = shape.iter().product();
Self {
data,
n_values,
shape,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.n_values
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.n_values == 0
}
#[must_use]
pub fn shape(&self) -> &[usize] {
&self.shape
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn compression_ratio(&self) -> f32 {
let f32_size = self.n_values * 4;
if self.data.is_empty() {
return 1.0;
}
f32_size as f32 / self.data.len() as f32
}
#[must_use]
pub fn dequantize(&self) -> Vec<f32> {
let mut result = Vec::with_capacity(self.n_values);
let n_blocks = self.n_values.div_ceil(Self::VALUES_PER_BLOCK);
for block_idx in 0..n_blocks {
let block_start = block_idx * Self::SUPER_BLOCK_BYTES;
if block_start + Self::SUPER_BLOCK_BYTES > self.data.len() {
let remaining = self.n_values - result.len();
result.extend(core::iter::repeat(0.0).take(remaining));
break;
}
let block = &self.data[block_start..block_start + Self::SUPER_BLOCK_BYTES];
let scale_bits = u16::from_le_bytes([block[0], block[1]]);
let min_bits = u16::from_le_bytes([block[2], block[3]]);
let scale = f16_to_f32(scale_bits);
let min = f16_to_f32(min_bits);
let weights_start = 4;
let outliers_start = 68;
for i in 0..Self::VALUES_PER_BLOCK {
if result.len() >= self.n_values {
break;
}
let byte_idx = weights_start + (i / 4);
let bit_offset = (i % 4) * 2;
let q2 = (block[byte_idx] >> bit_offset) & 0x03;
let outlier_byte_idx = outliers_start + (i / 2);
let outlier_offset = (i % 2) * 4;
let q4_high = (block[outlier_byte_idx] >> outlier_offset) & 0x0F;
let combined = q2 as i8 + ((q4_high as i8) << 2);
let value = min + (combined as f32) * scale;
result.push(value);
}
}
result
}
#[must_use]
pub fn raw_data(&self) -> &[u8] {
&self.data
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedLinearQ2K {
weight: QuantizedTensorQ2K,
bias: Option<Vec<f32>>,
in_features: usize,
out_features: usize,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedLinearQ2K {
#[must_use]
pub fn from_raw(
weight_data: Vec<u8>,
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
let n_values = out_features * in_features;
Self {
weight: QuantizedTensorQ2K {
data: weight_data,
n_values,
shape: vec![out_features, in_features],
},
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
}
}
#[must_use]
pub const fn in_features(&self) -> usize {
self.in_features
}
#[must_use]
pub const fn out_features(&self) -> usize {
self.out_features
}
#[must_use]
pub fn compression_ratio(&self) -> f32 {
self.weight.compression_ratio()
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"Input length {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let weights = self.weight.dequantize();
let mut output = vec![0.0; batch_size * self.out_features];
for b in 0..batch_size {
let input_row = &input[b * self.in_features..(b + 1) * self.in_features];
for o in 0..self.out_features {
let weight_row = &weights[o * self.in_features..(o + 1) * self.in_features];
let mut sum = 0.0f32;
for i in 0..self.in_features {
sum += input_row[i] * weight_row[i];
}
output[b * self.out_features + o] = sum;
}
}
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
}
#[inline]
fn f16_to_f32(bits: u16) -> f32 {
let sign = ((bits >> 15) as u32) << 31;
let exp = ((bits >> 10) & 0x1F) as u32;
let frac = (bits & 0x3FF) as u32;
let f32_bits = if exp == 0 {
if frac == 0 {
sign
} else {
let mut e = 1u32;
let mut f = frac;
while f & 0x400 == 0 {
f <<= 1;
e += 1;
}
sign | ((127 - 15 + 1 - e) << 23) | ((f & 0x3FF) << 13)
}
} else if exp == 31 {
sign | (0xFF << 23) | (frac << 13) } else {
sign | ((exp + 127 - 15) << 23) | (frac << 13)
};
f32::from_bits(f32_bits)
}
#[inline]
fn f32_to_f16(value: f32) -> u16 {
let bits = value.to_bits();
let sign = ((bits >> 16) & 0x8000) as u16;
let exp = ((bits >> 23) & 0xFF) as i32;
let frac = bits & 0x7FFFFF;
if exp == 255 {
return sign | 0x7C00 | if frac != 0 { 0x200 } else { 0 }; }
let unbiased = exp - 127;
if unbiased > 15 {
return sign | 0x7C00; }
if unbiased < -14 {
return sign; }
sign | (((unbiased + 15) as u16) << 10) | ((frac >> 13) as u16)
}
#[cfg(feature = "realizar-inference")]
pub fn quantize_to_q2k(data: &[f32], shape: Vec<usize>) -> QuantizedTensorQ2K {
let n_values = data.len();
let n_blocks = n_values.div_ceil(QuantizedTensorQ2K::VALUES_PER_BLOCK);
let mut raw_data = Vec::with_capacity(n_blocks * QuantizedTensorQ2K::SUPER_BLOCK_BYTES);
for block_idx in 0..n_blocks {
let start = block_idx * QuantizedTensorQ2K::VALUES_PER_BLOCK;
let end = (start + QuantizedTensorQ2K::VALUES_PER_BLOCK).min(n_values);
let block_values = &data[start..end];
let min = block_values.iter().cloned().fold(f32::INFINITY, f32::min);
let max = block_values
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
let range = max - min;
let scale = if range > 1e-10 { range / 63.0 } else { 1e-10 };
raw_data.extend_from_slice(&f32_to_f16(scale).to_le_bytes());
raw_data.extend_from_slice(&f32_to_f16(min).to_le_bytes());
let mut q_values = Vec::with_capacity(QuantizedTensorQ2K::VALUES_PER_BLOCK);
for &v in block_values {
let q = ((v - min) / scale).round().clamp(0.0, 63.0) as u8;
q_values.push(q);
}
while q_values.len() < QuantizedTensorQ2K::VALUES_PER_BLOCK {
q_values.push(0);
}
let mut weights_bytes = [0u8; 64];
for (i, &q) in q_values.iter().enumerate() {
let q2 = q & 0x03; let byte_idx = i / 4;
let bit_offset = (i % 4) * 2;
weights_bytes[byte_idx] |= q2 << bit_offset;
}
raw_data.extend_from_slice(&weights_bytes);
let mut outlier_bytes = [0u8; 128];
for (i, &q) in q_values.iter().enumerate() {
let q4 = (q >> 2) & 0x0F; let byte_idx = i / 2;
let bit_offset = (i % 2) * 4;
outlier_bytes[byte_idx] |= q4 << bit_offset;
}
raw_data.extend_from_slice(&outlier_bytes);
}
QuantizedTensorQ2K {
data: raw_data,
n_values,
shape,
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedFeedForward {
fc1: QuantizedLinearQ4K,
fc2: QuantizedLinearQ4K,
d_model: usize,
d_ff: usize,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedFeedForward {
#[must_use]
pub fn new(fc1_data: Vec<u8>, fc2_data: Vec<u8>, d_model: usize, d_ff: usize) -> Self {
Self {
fc1: QuantizedLinearQ4K::from_raw(fc1_data, None, d_model, d_ff),
fc2: QuantizedLinearQ4K::from_raw(fc2_data, None, d_ff, d_model),
d_model,
d_ff,
}
}
#[must_use]
pub const fn d_model(&self) -> usize {
self.d_model
}
#[must_use]
pub const fn d_ff(&self) -> usize {
self.d_ff
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.fc1.memory_size() + self.fc2.memory_size()
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let hidden = self.fc1.forward_fused(input)?;
let activated = crate::simd::gelu(&hidden);
self.fc2.forward_fused(&activated)
}
pub fn finalize_weights(&mut self) {
self.fc1.finalize_weights();
self.fc2.finalize_weights();
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedMultiHeadAttention {
n_heads: usize,
d_model: usize,
d_head: usize,
w_q: QuantizedLinearQ4K,
w_k: QuantizedLinearQ4K,
w_v: QuantizedLinearQ4K,
w_o: QuantizedLinearQ4K,
scale: f32,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedMultiHeadAttention {
#[must_use]
pub fn new_random(n_heads: usize, d_model: usize) -> Self {
assert!(
d_model % n_heads == 0,
"d_model ({d_model}) must be divisible by n_heads ({n_heads})"
);
let d_head = d_model / n_heads;
let super_block_bytes = 144usize;
let super_blocks_per_row = d_model.div_ceil(256);
let bytes_per_row = super_blocks_per_row * super_block_bytes;
let data_size = d_model * bytes_per_row;
let create_projection =
|| QuantizedLinearQ4K::from_raw(vec![0u8; data_size], None, d_model, d_model);
Self {
n_heads,
d_model,
d_head,
w_q: create_projection(),
w_k: create_projection(),
w_v: create_projection(),
w_o: create_projection(),
scale: 1.0 / (d_head as f32).sqrt(),
}
}
#[must_use]
pub fn new(
n_heads: usize,
d_model: usize,
w_q: QuantizedLinearQ4K,
w_k: QuantizedLinearQ4K,
w_v: QuantizedLinearQ4K,
w_o: QuantizedLinearQ4K,
) -> Self {
assert!(
d_model % n_heads == 0,
"d_model ({d_model}) must be divisible by n_heads ({n_heads})"
);
let d_head = d_model / n_heads;
Self {
n_heads,
d_model,
d_head,
w_q,
w_k,
w_v,
w_o,
scale: 1.0 / (d_head as f32).sqrt(),
}
}
#[must_use]
pub fn n_heads(&self) -> usize {
self.n_heads
}
#[must_use]
pub fn d_model(&self) -> usize {
self.d_model
}
#[must_use]
pub fn d_head(&self) -> usize {
self.d_head
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.w_q.memory_size()
+ self.w_k.memory_size()
+ self.w_v.memory_size()
+ self.w_o.memory_size()
}
#[allow(clippy::needless_range_loop)]
pub fn forward(
&self,
query: &[f32],
key: &[f32],
value: &[f32],
mask: Option<&[f32]>,
) -> WhisperResult<Vec<f32>> {
let q_len = query.len() / self.d_model;
let kv_len = key.len() / self.d_model;
let q = self.w_q.forward_fused(query)?;
let k = self.w_k.forward_fused(key)?;
let v = self.w_v.forward_fused(value)?;
let mut output = vec![0.0f32; q_len * self.d_model];
for head in 0..self.n_heads {
let head_offset = head * self.d_head;
for qi in 0..q_len {
let mut scores = Vec::with_capacity(kv_len);
for ki in 0..kv_len {
let mut score = 0.0f32;
for d in 0..self.d_head {
let q_idx = qi * self.d_model + head_offset + d;
let k_idx = ki * self.d_model + head_offset + d;
score += q[q_idx] * k[k_idx];
}
score *= self.scale;
if let Some(m) = mask {
let mask_idx = qi * kv_len + ki;
if mask_idx < m.len() {
score += m[mask_idx];
}
}
scores.push(score);
}
let max_score = scores.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let exp_scores: Vec<f32> = scores.iter().map(|s| (s - max_score).exp()).collect();
let sum: f32 = exp_scores.iter().sum();
let attn_weights: Vec<f32> = exp_scores.iter().map(|e| e / sum).collect();
for d in 0..self.d_head {
let mut val = 0.0f32;
for ki in 0..kv_len {
let v_idx = ki * self.d_model + head_offset + d;
val += attn_weights[ki] * v[v_idx];
}
output[qi * self.d_model + head_offset + d] = val;
}
}
}
self.w_o.forward_fused(&output)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedDecoderBlock {
pub self_attn: crate::model::attention::MultiHeadAttention,
pub ln1: crate::model::encoder::LayerNorm,
pub cross_attn: crate::model::attention::MultiHeadAttention,
pub ln2: crate::model::encoder::LayerNorm,
pub ffn: QuantizedFeedForward,
pub ln3: crate::model::encoder::LayerNorm,
d_model: usize,
d_ff: usize,
n_heads: usize,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedDecoderBlock {
#[must_use]
pub fn new(
d_model: usize,
n_heads: usize,
d_ff: usize,
fc1_data: Vec<u8>,
fc2_data: Vec<u8>,
) -> Self {
Self {
self_attn: crate::model::attention::MultiHeadAttention::new(n_heads, d_model),
ln1: crate::model::encoder::LayerNorm::new(d_model),
cross_attn: crate::model::attention::MultiHeadAttention::new(n_heads, d_model),
ln2: crate::model::encoder::LayerNorm::new(d_model),
ffn: QuantizedFeedForward::new(fc1_data, fc2_data, d_model, d_ff),
ln3: crate::model::encoder::LayerNorm::new(d_model),
d_model,
d_ff,
n_heads,
}
}
#[must_use]
pub const fn d_model(&self) -> usize {
self.d_model
}
#[must_use]
pub const fn d_ff(&self) -> usize {
self.d_ff
}
#[must_use]
pub const fn n_heads(&self) -> usize {
self.n_heads
}
#[must_use]
pub fn ffn_memory_bytes(&self) -> usize {
self.ffn.memory_bytes()
}
pub fn forward(
&self,
x: &[f32],
encoder_output: &[f32],
causal_mask: Option<&[f32]>,
) -> WhisperResult<Vec<f32>> {
let normed = self.ln1.forward(x)?;
let attn_out = self.self_attn.forward(&normed, causal_mask)?;
let mut residual: Vec<f32> = x.iter().zip(attn_out.iter()).map(|(a, b)| a + b).collect();
let normed = self.ln2.forward(&residual)?;
let cross_out = self
.cross_attn
.forward_cross(&normed, encoder_output, None)?;
for (r, c) in residual.iter_mut().zip(cross_out.iter()) {
*r += c;
}
let normed = self.ln3.forward(&residual)?;
let ffn_out = self.ffn.forward(&normed)?;
for (r, f) in residual.iter_mut().zip(ffn_out.iter()) {
*r += f;
}
Ok(residual)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct FullyQuantizedDecoderBlock {
pub self_attn: QuantizedMultiHeadAttention,
pub ln1: crate::model::encoder::LayerNorm,
pub cross_attn: QuantizedMultiHeadAttention,
pub ln2: crate::model::encoder::LayerNorm,
pub ffn: QuantizedFeedForward,
pub ln3: crate::model::encoder::LayerNorm,
d_model: usize,
d_ff: usize,
n_heads: usize,
}
#[cfg(feature = "realizar-inference")]
impl FullyQuantizedDecoderBlock {
#[must_use]
pub fn new_random(n_heads: usize, d_model: usize, d_ff: usize) -> Self {
let super_block_bytes = 144usize;
let fc1_blocks_per_row = d_model.div_ceil(256);
let fc1_bytes_per_row = fc1_blocks_per_row * super_block_bytes;
let fc1_data = vec![0u8; d_ff * fc1_bytes_per_row];
let fc2_blocks_per_row = d_ff.div_ceil(256);
let fc2_bytes_per_row = fc2_blocks_per_row * super_block_bytes;
let fc2_data = vec![0u8; d_model * fc2_bytes_per_row];
Self {
self_attn: QuantizedMultiHeadAttention::new_random(n_heads, d_model),
ln1: crate::model::encoder::LayerNorm::new(d_model),
cross_attn: QuantizedMultiHeadAttention::new_random(n_heads, d_model),
ln2: crate::model::encoder::LayerNorm::new(d_model),
ffn: QuantizedFeedForward::new(fc1_data, fc2_data, d_model, d_ff),
ln3: crate::model::encoder::LayerNorm::new(d_model),
d_model,
d_ff,
n_heads,
}
}
#[must_use]
pub const fn d_model(&self) -> usize {
self.d_model
}
#[must_use]
pub const fn d_ff(&self) -> usize {
self.d_ff
}
#[must_use]
pub const fn n_heads(&self) -> usize {
self.n_heads
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.self_attn.memory_bytes() + self.cross_attn.memory_bytes() + self.ffn.memory_bytes()
}
pub fn forward(&self, input: &[f32], encoder_output: &[f32]) -> WhisperResult<Vec<f32>> {
let mut residual = input.to_vec();
let normed = self.ln1.forward(&residual)?;
let self_attn_out = self.self_attn.forward(&normed, &normed, &normed, None)?;
for (r, s) in residual.iter_mut().zip(self_attn_out.iter()) {
*r += s;
}
let normed = self.ln2.forward(&residual)?;
let cross_out = self
.cross_attn
.forward(&normed, encoder_output, encoder_output, None)?;
for (r, c) in residual.iter_mut().zip(cross_out.iter()) {
*r += c;
}
let normed = self.ln3.forward(&residual)?;
let ffn_out = self.ffn.forward(&normed)?;
for (r, f) in residual.iter_mut().zip(ffn_out.iter()) {
*r += f;
}
Ok(residual)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct FullyQuantizedDecoder {
n_layers: usize,
d_model: usize,
n_heads: usize,
d_ff: usize,
blocks: Vec<FullyQuantizedDecoderBlock>,
ln_post: crate::model::encoder::LayerNorm,
token_embedding: Vec<f32>,
positional_embedding: Vec<f32>,
n_vocab: usize,
max_len: usize,
}
#[cfg(feature = "realizar-inference")]
impl FullyQuantizedDecoder {
#[must_use]
pub fn new_random(
n_layers: usize,
d_model: usize,
n_heads: usize,
d_ff: usize,
n_vocab: usize,
max_len: usize,
) -> Self {
let blocks = (0..n_layers)
.map(|_| FullyQuantizedDecoderBlock::new_random(n_heads, d_model, d_ff))
.collect();
let token_embedding = vec![0.0f32; n_vocab * d_model];
let positional_embedding = vec![0.0f32; max_len * d_model];
Self {
n_layers,
d_model,
n_heads,
d_ff,
blocks,
ln_post: crate::model::encoder::LayerNorm::new(d_model),
token_embedding,
positional_embedding,
n_vocab,
max_len,
}
}
#[must_use]
pub const fn n_layers(&self) -> usize {
self.n_layers
}
#[must_use]
pub const fn d_model(&self) -> usize {
self.d_model
}
#[must_use]
pub const fn n_vocab(&self) -> usize {
self.n_vocab
}
#[must_use]
pub const fn n_heads(&self) -> usize {
self.n_heads
}
#[must_use]
pub const fn d_ff(&self) -> usize {
self.d_ff
}
#[must_use]
pub fn block_memory_bytes(&self) -> usize {
self.blocks.iter().map(|b| b.memory_bytes()).sum()
}
#[must_use]
pub fn create_kv_cache(&self) -> crate::model::decoder::DecoderKVCache {
crate::model::decoder::DecoderKVCache::new(self.n_layers, self.d_model, self.max_len)
}
#[allow(clippy::needless_range_loop)]
pub fn forward_one_fully_quantized(
&self,
token: u32,
encoder_output: &[f32],
cache: &mut crate::model::decoder::DecoderKVCache,
) -> WhisperResult<Vec<f32>> {
let pos = cache.seq_len();
if pos >= self.max_len {
return Err(WhisperError::Model(format!(
"position {} exceeds max {}",
pos, self.max_len
)));
}
let token_idx = token as usize;
if token_idx >= self.n_vocab {
return Err(WhisperError::Model(format!(
"token {} exceeds vocab size {}",
token_idx, self.n_vocab
)));
}
let mut x: Vec<f32> = (0..self.d_model)
.map(|i| {
self.token_embedding[token_idx * self.d_model + i]
+ self.positional_embedding[pos * self.d_model + i]
})
.collect();
for block in &self.blocks {
x = block.forward(&x, encoder_output)?;
}
x = self.ln_post.forward(&x)?;
let mut logits = vec![0.0f32; self.n_vocab];
for v in 0..self.n_vocab {
let mut sum = 0.0f32;
for d in 0..self.d_model {
sum += x[d] * self.token_embedding[v * self.d_model + d];
}
logits[v] = sum;
}
cache.increment_seq_len();
Ok(logits)
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedDecoder {
n_layers: usize,
d_model: usize,
n_heads: usize,
d_ff: usize,
blocks: Vec<QuantizedDecoderBlock>,
ln_post: crate::model::encoder::LayerNorm,
token_embedding: Vec<f32>,
positional_embedding: Vec<f32>,
n_vocab: usize,
max_len: usize,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedDecoder {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
n_layers: usize,
d_model: usize,
n_heads: usize,
d_ff: usize,
n_vocab: usize,
max_len: usize,
ffn_data: Vec<(Vec<u8>, Vec<u8>)>,
) -> Self {
assert_eq!(ffn_data.len(), n_layers, "FFN data must match n_layers");
let blocks = ffn_data
.into_iter()
.map(|(fc1, fc2)| QuantizedDecoderBlock::new(d_model, n_heads, d_ff, fc1, fc2))
.collect();
let token_embedding = vec![0.0f32; n_vocab * d_model];
let positional_embedding = vec![0.0f32; max_len * d_model];
Self {
n_layers,
d_model,
n_heads,
d_ff,
blocks,
ln_post: crate::model::encoder::LayerNorm::new(d_model),
token_embedding,
positional_embedding,
n_vocab,
max_len,
}
}
#[must_use]
pub const fn n_layers(&self) -> usize {
self.n_layers
}
#[must_use]
pub const fn d_model(&self) -> usize {
self.d_model
}
#[must_use]
pub const fn n_vocab(&self) -> usize {
self.n_vocab
}
#[must_use]
pub const fn n_heads(&self) -> usize {
self.n_heads
}
#[must_use]
pub const fn d_ff(&self) -> usize {
self.d_ff
}
#[must_use]
pub fn ffn_memory_bytes(&self) -> usize {
self.blocks.iter().map(|b| b.ffn_memory_bytes()).sum()
}
#[must_use]
pub fn create_kv_cache(&self) -> crate::model::decoder::DecoderKVCache {
crate::model::decoder::DecoderKVCache::new(self.n_layers, self.d_model, self.max_len)
}
#[allow(clippy::needless_range_loop)]
pub fn forward_one_quantized(
&self,
token: u32,
encoder_output: &[f32],
cache: &mut crate::model::decoder::DecoderKVCache,
) -> WhisperResult<Vec<f32>> {
let pos = cache.seq_len();
if pos >= self.max_len {
return Err(WhisperError::Model(format!(
"position {} exceeds max {}",
pos, self.max_len
)));
}
let token_idx = token as usize;
if token_idx >= self.n_vocab {
return Err(WhisperError::Model(format!(
"token {} exceeds vocab size {}",
token_idx, self.n_vocab
)));
}
let mut x: Vec<f32> = (0..self.d_model)
.map(|i| {
self.token_embedding[token_idx * self.d_model + i]
+ self.positional_embedding[pos * self.d_model + i]
})
.collect();
for block in &self.blocks {
x = block.forward(&x, encoder_output, None)?;
}
x = self.ln_post.forward(&x)?;
let mut logits = vec![0.0f32; self.n_vocab];
for v in 0..self.n_vocab {
let mut sum = 0.0f32;
for d in 0..self.d_model {
sum += x[d] * self.token_embedding[v * self.d_model + d];
}
logits[v] = sum;
}
cache.increment_seq_len();
Ok(logits)
}
}
#[must_use]
pub fn quantize_f32_to_i8(data: &[f32]) -> (Vec<i8>, f32) {
if data.is_empty() {
return (Vec::new(), 1.0);
}
let max_abs = data.iter().map(|x| x.abs()).fold(0.0_f32, |a, b| a.max(b));
let scale = if max_abs < MIN_SCALE {
1.0
} else {
max_abs / I8_MAX
};
let quantized: Vec<i8> = data
.iter()
.map(|&x| {
let q = (x / scale).round();
q.clamp(-128.0, 127.0) as i8
})
.collect();
(quantized, scale)
}
#[must_use]
pub fn dequantize_i8_to_f32(data: &[i8], scale: f32) -> Vec<f32> {
data.iter().map(|&q| f32::from(q) * scale).collect()
}
#[must_use]
pub fn quantize_f32_to_i8_per_channel(
data: &[f32],
n_channels: usize,
channel_size: usize,
) -> (Vec<i8>, Vec<f32>) {
if data.is_empty() || n_channels == 0 {
return (Vec::new(), Vec::new());
}
let mut quantized = Vec::with_capacity(data.len());
let mut scales = Vec::with_capacity(n_channels);
for ch in 0..n_channels {
let start = ch * channel_size;
let end = start + channel_size;
let channel_data = &data[start..end.min(data.len())];
let (q, scale) = quantize_f32_to_i8(channel_data);
quantized.extend(q);
scales.push(scale);
}
(quantized, scales)
}
#[must_use]
pub fn dequantize_i8_to_f32_per_channel(
data: &[i8],
scales: &[f32],
channel_size: usize,
) -> Vec<f32> {
let mut result = Vec::with_capacity(data.len());
for (ch, &scale) in scales.iter().enumerate() {
let start = ch * channel_size;
let end = (start + channel_size).min(data.len());
for &q in &data[start..end] {
result.push(f32::from(q) * scale);
}
}
result
}
#[derive(Debug, Clone)]
pub struct QuantizedTensorInt4 {
pub data: Vec<u8>,
pub scale: f32,
pub len: usize,
pub shape: Vec<usize>,
}
impl QuantizedTensorInt4 {
#[must_use]
pub fn from_f32(data: &[f32], shape: Vec<usize>) -> Self {
let (quantized, scale) = quantize_f32_to_i4_packed(data);
Self {
data: quantized,
scale,
len: data.len(),
shape,
}
}
#[must_use]
pub fn to_f32(&self) -> Vec<f32> {
dequantize_i4_packed_to_f32(&self.data, self.scale, self.len)
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn memory_size(&self) -> usize {
self.data.len() + 4 }
#[must_use]
pub fn numel(&self) -> usize {
self.shape.iter().product()
}
#[must_use]
pub fn unpack(&self) -> Vec<i8> {
unpack_i4_to_i8(&self.data, self.len)
}
}
#[inline]
fn pack_i4(value: i8) -> u8 {
(value as u8) & 0x0F
}
#[inline]
fn unpack_i4(nibble: u8) -> i8 {
let val = nibble & 0x0F;
if val >= 8 {
(val as i8) - 16
} else {
val as i8
}
}
#[must_use]
pub fn quantize_f32_to_i4_packed(data: &[f32]) -> (Vec<u8>, f32) {
if data.is_empty() {
return (Vec::new(), 1.0);
}
let max_abs = data.iter().map(|x| x.abs()).fold(0.0_f32, |a, b| a.max(b));
let scale = if max_abs < MIN_SCALE {
1.0
} else {
max_abs / I4_MAX
};
let packed_len = data.len().div_ceil(2);
let mut packed = vec![0u8; packed_len];
for (i, &x) in data.iter().enumerate() {
let q = (x / scale).round().clamp(-8.0, 7.0) as i8;
let nibble = pack_i4(q);
let byte_idx = i / 2;
if i % 2 == 0 {
packed[byte_idx] |= nibble;
} else {
packed[byte_idx] |= nibble << 4;
}
}
(packed, scale)
}
#[must_use]
pub fn dequantize_i4_packed_to_f32(data: &[u8], scale: f32, len: usize) -> Vec<f32> {
let mut result = Vec::with_capacity(len);
for (i, &byte) in data.iter().enumerate() {
let low_idx = i * 2;
if low_idx < len {
let q = unpack_i4(byte & 0x0F);
result.push(f32::from(q) * scale);
}
let high_idx = i * 2 + 1;
if high_idx < len {
let q = unpack_i4(byte >> 4);
result.push(f32::from(q) * scale);
}
}
result
}
#[must_use]
pub fn unpack_i4_to_i8(data: &[u8], len: usize) -> Vec<i8> {
let mut result = Vec::with_capacity(len);
for (i, &byte) in data.iter().enumerate() {
let low_idx = i * 2;
if low_idx < len {
result.push(unpack_i4(byte & 0x0F));
}
let high_idx = i * 2 + 1;
if high_idx < len {
result.push(unpack_i4(byte >> 4));
}
}
result
}
#[must_use]
pub fn quantize_f32_to_i4(data: &[f32]) -> (Vec<i8>, f32) {
if data.is_empty() {
return (Vec::new(), 1.0);
}
let max_abs = data.iter().map(|x| x.abs()).fold(0.0_f32, |a, b| a.max(b));
let scale = if max_abs < MIN_SCALE {
1.0
} else {
max_abs / I4_MAX
};
let quantized: Vec<i8> = data
.iter()
.map(|&x| (x / scale).round().clamp(-8.0, 7.0) as i8)
.collect();
(quantized, scale)
}
#[must_use]
pub fn dequantize_i4_to_f32(data: &[i8], scale: f32) -> Vec<f32> {
data.iter().map(|&q| f32::from(q) * scale).collect()
}
#[derive(Debug, Clone)]
pub struct QuantizedLinearInt4 {
pub weight: QuantizedTensorInt4,
pub bias: Option<Vec<f32>>,
pub in_features: usize,
pub out_features: usize,
}
impl QuantizedLinearInt4 {
#[must_use]
pub fn from_f32(
weight: &[f32],
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
Self {
weight: QuantizedTensorInt4::from_f32(weight, vec![out_features, in_features]),
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
}
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let weights = self.weight.to_f32();
let weights_t = simd::transpose(&weights, self.out_features, self.in_features);
let mut output = simd::matmul(
input,
&weights_t,
batch_size,
self.in_features,
self.out_features,
);
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
pub fn forward_quantized(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let (input_q, input_scale) = quantize_f32_to_i4(input);
let weight_q = self.weight.unpack();
let mut output_acc = vec![0i32; batch_size * self.out_features];
for b in 0..batch_size {
for o in 0..self.out_features {
let mut sum = 0i32;
for i in 0..self.in_features {
sum += i32::from(input_q[b * self.in_features + i])
* i32::from(weight_q[o * self.in_features + i]);
}
output_acc[b * self.out_features + o] = sum;
}
}
let scale = input_scale * self.weight.scale;
let mut output: Vec<f32> = output_acc.iter().map(|&x| (x as f32) * scale).collect();
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
#[must_use]
pub fn memory_size(&self) -> usize {
let weight_size = self.weight.data.len(); let bias_size = self.bias.as_ref().map_or(0, |b| b.len() * 4);
weight_size + bias_size + 4 }
}
#[must_use]
pub fn quantization_error_i4(original: &[f32], quantized: &[i8], scale: f32) -> f32 {
if original.is_empty() {
return 0.0;
}
let reconstructed = dequantize_i4_to_f32(quantized, scale);
let mse: f32 = original
.iter()
.zip(reconstructed.iter())
.map(|(o, r)| (o - r).powi(2))
.sum::<f32>()
/ original.len() as f32;
mse
}
#[must_use]
pub fn compute_sqnr_i4(original: &[f32], quantized: &[i8], scale: f32) -> f32 {
if original.is_empty() {
return 0.0;
}
let signal_power: f32 = original.iter().map(|x| x.powi(2)).sum::<f32>() / original.len() as f32;
let noise_power = quantization_error_i4(original, quantized, scale);
if noise_power < MIN_SCALE {
return f32::INFINITY;
}
10.0 * (signal_power / noise_power).log10()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WeightPrecision {
Int4,
Int8,
}
impl core::fmt::Display for WeightPrecision {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Int4 => write!(f, "int4"),
Self::Int8 => write!(f, "int8"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActivationPrecision {
Float32,
}
impl core::fmt::Display for ActivationPrecision {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Float32 => write!(f, "fp32"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MixedPrecisionConfig {
pub weight_precision: WeightPrecision,
pub activation_precision: ActivationPrecision,
}
impl Default for MixedPrecisionConfig {
fn default() -> Self {
Self::int4_fp32()
}
}
impl MixedPrecisionConfig {
#[must_use]
pub const fn int4_fp32() -> Self {
Self {
weight_precision: WeightPrecision::Int4,
activation_precision: ActivationPrecision::Float32,
}
}
#[must_use]
pub const fn int8_fp32() -> Self {
Self {
weight_precision: WeightPrecision::Int8,
activation_precision: ActivationPrecision::Float32,
}
}
#[must_use]
pub fn description(&self) -> String {
format!(
"{} weights, {} activations",
self.weight_precision, self.activation_precision
)
}
}
#[derive(Debug, Clone)]
enum QuantizedWeights {
Int4(QuantizedTensorInt4),
Int8(QuantizedTensor),
}
impl QuantizedWeights {
fn to_f32(&self) -> Vec<f32> {
match self {
Self::Int4(t) => t.to_f32(),
Self::Int8(t) => t.to_f32(),
}
}
fn memory_size(&self) -> usize {
match self {
Self::Int4(t) => t.memory_size(),
Self::Int8(t) => t.data.len() + 4, }
}
}
#[derive(Debug, Clone)]
pub struct MixedPrecisionLinear {
weights: QuantizedWeights,
bias: Option<Vec<f32>>,
in_features: usize,
out_features: usize,
config: MixedPrecisionConfig,
}
impl MixedPrecisionLinear {
#[must_use]
pub fn from_f32_with_config(
weight: &[f32],
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
config: MixedPrecisionConfig,
) -> Self {
let weights = match config.weight_precision {
WeightPrecision::Int4 => QuantizedWeights::Int4(QuantizedTensorInt4::from_f32(
weight,
vec![out_features, in_features],
)),
WeightPrecision::Int8 => QuantizedWeights::Int8(QuantizedTensor::from_f32(
weight,
vec![out_features, in_features],
)),
};
Self {
weights,
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
config,
}
}
#[must_use]
pub const fn in_features(&self) -> usize {
self.in_features
}
#[must_use]
pub const fn out_features(&self) -> usize {
self.out_features
}
#[must_use]
pub const fn weight_precision(&self) -> WeightPrecision {
self.config.weight_precision
}
#[must_use]
pub fn memory_size(&self) -> usize {
let weight_size = self.weights.memory_size();
let bias_size = self.bias.as_ref().map_or(0, |b| b.len() * 4);
weight_size + bias_size
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let weights = self.weights.to_f32();
let weights_t = simd::transpose(&weights, self.out_features, self.in_features);
let mut output = simd::matmul(
input,
&weights_t,
batch_size,
self.in_features,
self.out_features,
);
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
}
#[derive(Debug, Clone)]
pub struct QuantizedLinear {
pub weight: QuantizedTensor,
pub bias: Option<Vec<f32>>,
pub in_features: usize,
pub out_features: usize,
}
impl QuantizedLinear {
#[must_use]
pub fn from_f32(
weight: &[f32],
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
Self {
weight: QuantizedTensor::from_f32(weight, vec![out_features, in_features]),
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
}
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let weights = self.weight.to_f32();
let weights_t = simd::transpose(&weights, self.out_features, self.in_features);
let mut output = simd::matmul(
input,
&weights_t,
batch_size,
self.in_features,
self.out_features,
);
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
pub fn forward_quantized(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let (input_q, input_scale) = quantize_f32_to_i8(input);
let mut output_acc = vec![0i32; batch_size * self.out_features];
for b in 0..batch_size {
for o in 0..self.out_features {
let mut sum = 0i32;
for i in 0..self.in_features {
sum += i32::from(input_q[b * self.in_features + i])
* i32::from(self.weight.data[o * self.in_features + i]);
}
output_acc[b * self.out_features + o] = sum;
}
}
let scale = input_scale * self.weight.scale;
let mut output: Vec<f32> = output_acc.iter().map(|&x| (x as f32) * scale).collect();
if let Some(ref bias) = self.bias {
for b in 0..batch_size {
for o in 0..self.out_features {
output[b * self.out_features + o] += bias[o];
}
}
}
Ok(output)
}
#[must_use]
pub fn memory_size(&self) -> usize {
let weight_size = self.weight.data.len(); let bias_size = self.bias.as_ref().map_or(0, |b| b.len() * 4);
weight_size + bias_size + 4 }
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedTensorQ8_0 {
data: Vec<u8>,
n_values: usize,
shape: Vec<usize>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedTensorQ8_0 {
pub const BLOCK_BYTES: usize = 34;
pub const VALUES_PER_BLOCK: usize = 32;
#[must_use]
pub fn from_f32(data: &[f32], shape: Vec<usize>) -> Self {
let n_values = data.len();
let n_blocks = n_values.div_ceil(Self::VALUES_PER_BLOCK);
let mut raw_data = Vec::with_capacity(n_blocks * Self::BLOCK_BYTES);
for block_idx in 0..n_blocks {
let start = block_idx * Self::VALUES_PER_BLOCK;
let end = (start + Self::VALUES_PER_BLOCK).min(n_values);
let block_data = &data[start..end];
let max_abs = block_data.iter().map(|x| x.abs()).fold(0.0_f32, f32::max);
let scale = max_abs / 127.0;
let scale_f16_bits = f32_to_f16(scale);
raw_data.extend_from_slice(&scale_f16_bits.to_le_bytes());
for &val in block_data {
let q = if scale > MIN_SCALE {
(val / scale).round().clamp(-127.0, 127.0) as i8
} else {
0
};
raw_data.push(q as u8);
}
let padding_count = Self::VALUES_PER_BLOCK - block_data.len();
raw_data.resize(raw_data.len() + padding_count, 0);
}
Self {
data: raw_data,
n_values,
shape,
}
}
#[must_use]
pub fn from_raw(data: Vec<u8>, shape: Vec<usize>) -> Self {
let n_values = shape.iter().product();
Self {
data,
n_values,
shape,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.n_values
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.n_values == 0
}
#[must_use]
pub fn shape(&self) -> &[usize] {
&self.shape
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn dequantize(&self) -> Vec<f32> {
crate::realizar_inference::dequantize_q8_0(&self.data)
.unwrap_or_else(|_| self.dequantize_fallback())
}
fn dequantize_fallback(&self) -> Vec<f32> {
let mut result = Vec::with_capacity(self.n_values);
let n_blocks = self.n_values.div_ceil(Self::VALUES_PER_BLOCK);
for block_idx in 0..n_blocks {
let block_start = block_idx * Self::BLOCK_BYTES;
let scale_bits =
u16::from_le_bytes([self.data[block_start], self.data[block_start + 1]]);
let scale = f16_to_f32(scale_bits);
let values_start = block_start + 2;
let values_end = (block_idx + 1) * Self::VALUES_PER_BLOCK;
let values_to_read = (values_end.min(self.n_values)
- block_idx * Self::VALUES_PER_BLOCK)
.min(Self::VALUES_PER_BLOCK);
for i in 0..values_to_read {
let q = self.data[values_start + i] as i8;
result.push(q as f32 * scale);
}
}
result
}
#[must_use]
pub fn raw_data(&self) -> &[u8] {
&self.data
}
#[must_use]
pub fn compression_ratio(&self) -> f32 {
let original_bytes = self.n_values * 4; if self.data.is_empty() {
return 1.0;
}
original_bytes as f32 / self.data.len() as f32
}
}
#[cfg(feature = "realizar-inference")]
#[derive(Debug, Clone)]
pub struct QuantizedLinearQ8_0 {
weight: QuantizedTensorQ8_0,
bias: Option<Vec<f32>>,
in_features: usize,
out_features: usize,
cached_weights_t: Option<Vec<f32>>,
}
#[cfg(feature = "realizar-inference")]
impl QuantizedLinearQ8_0 {
#[must_use]
pub fn from_f32(
weights: &[f32],
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
let tensor = QuantizedTensorQ8_0::from_f32(weights, vec![out_features, in_features]);
Self {
weight: tensor,
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
cached_weights_t: None,
}
}
#[must_use]
pub fn from_raw(
weight_data: Vec<u8>,
bias: Option<&[f32]>,
in_features: usize,
out_features: usize,
) -> Self {
let n_values = out_features * in_features;
Self {
weight: QuantizedTensorQ8_0 {
data: weight_data,
n_values,
shape: vec![out_features, in_features],
},
bias: bias.map(|b| b.to_vec()),
in_features,
out_features,
cached_weights_t: None,
}
}
pub fn finalize_weights(&mut self) {
if self.cached_weights_t.is_some() {
return;
}
let weights = self.weight.dequantize();
let weights_t = simd::transpose(&weights, self.out_features, self.in_features);
self.cached_weights_t = Some(weights_t);
}
#[must_use]
pub fn is_finalized(&self) -> bool {
self.cached_weights_t.is_some()
}
#[must_use]
pub const fn in_features(&self) -> usize {
self.in_features
}
#[must_use]
pub const fn out_features(&self) -> usize {
self.out_features
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
let weight_bytes = self.weight.memory_bytes();
let bias_bytes = self.bias.as_ref().map_or(0, |b| b.len() * 4);
let cache_bytes = self.cached_weights_t.as_ref().map_or(0, |c| c.len() * 4);
weight_bytes + bias_bytes + cache_bytes
}
#[must_use]
pub fn compression_ratio(&self) -> f32 {
self.weight.compression_ratio()
}
pub fn forward(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
let batch_size = input.len() / self.in_features;
if input.len() % self.in_features != 0 {
return Err(WhisperError::Model(format!(
"input size {} not divisible by in_features {}",
input.len(),
self.in_features
)));
}
let weights_t: std::borrow::Cow<'_, [f32]> = if let Some(ref cached) = self.cached_weights_t
{
std::borrow::Cow::Borrowed(cached)
} else {
let weights = self.weight.dequantize();
std::borrow::Cow::Owned(simd::transpose(
&weights,
self.out_features,
self.in_features,
))
};
let mut output = simd::matmul(
input,
&weights_t,
batch_size,
self.in_features,
self.out_features,
);
if let Some(ref bias) = self.bias {
simd::broadcast_add_inplace(&mut output, bias, batch_size, self.out_features);
}
Ok(output)
}
pub fn forward_int8(&self, input: &[f32]) -> WhisperResult<Vec<f32>> {
self.forward(input)
}
}
#[must_use]
pub fn quantization_error(original: &[f32], quantized: &[i8], scale: f32) -> f32 {
if original.is_empty() {
return 0.0;
}
let reconstructed = dequantize_i8_to_f32(quantized, scale);
let mse: f32 = original
.iter()
.zip(reconstructed.iter())
.map(|(o, r)| (o - r).powi(2))
.sum::<f32>()
/ original.len() as f32;
mse
}
#[must_use]
pub fn compute_sqnr(original: &[f32], quantized: &[i8], scale: f32) -> f32 {
if original.is_empty() {
return 0.0;
}
let signal_power: f32 = original.iter().map(|x| x.powi(2)).sum::<f32>() / original.len() as f32;
let noise_power = quantization_error(original, quantized, scale);
if noise_power < MIN_SCALE {
return f32::INFINITY;
}
10.0 * (signal_power / noise_power).log10()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quantize_empty() {
let (q, scale) = quantize_f32_to_i8(&[]);
assert!(q.is_empty());
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_quantize_zeros() {
let data = vec![0.0; 10];
let (q, scale) = quantize_f32_to_i8(&data);
assert_eq!(q.len(), 10);
assert!(q.iter().all(|&x| x == 0));
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_quantize_max_value() {
let data = vec![127.0];
let (q, scale) = quantize_f32_to_i8(&data);
assert_eq!(q[0], 127);
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_quantize_negative() {
let data = vec![-127.0];
let (q, scale) = quantize_f32_to_i8(&data);
assert_eq!(q[0], -127);
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_quantize_small_values() {
let data = vec![0.1, -0.1, 0.05, -0.05];
let (q, scale) = quantize_f32_to_i8(&data);
assert_eq!(q.len(), 4);
let expected_scale = 0.1 / 127.0;
assert!((scale - expected_scale).abs() < 1e-6);
}
#[test]
fn test_quantize_roundtrip() {
let data = vec![1.0, -1.0, 0.5, -0.5, 0.0];
let (q, scale) = quantize_f32_to_i8(&data);
let reconstructed = dequantize_i8_to_f32(&q, scale);
for (orig, recon) in data.iter().zip(reconstructed.iter()) {
assert!((orig - recon).abs() < 0.02, "orig={orig}, recon={recon}");
}
}
#[test]
fn test_quantized_tensor_from_f32() {
let data = vec![1.0, 2.0, 3.0, 4.0];
let tensor = QuantizedTensor::from_f32(&data, vec![2, 2]);
assert_eq!(tensor.len(), 4);
assert_eq!(tensor.shape, vec![2, 2]);
assert_eq!(tensor.numel(), 4);
}
#[test]
fn test_quantized_tensor_roundtrip() {
let data = vec![1.0, -2.0, 3.0, -4.0, 5.0, -6.0];
let tensor = QuantizedTensor::from_f32(&data, vec![2, 3]);
let reconstructed = tensor.to_f32();
for (orig, recon) in data.iter().zip(reconstructed.iter()) {
let error = (orig - recon).abs() / orig.abs().max(1.0);
assert!(error < 0.1, "Relative error too high: {error}");
}
}
#[test]
fn test_quantized_tensor_empty() {
let tensor = QuantizedTensor::from_f32(&[], vec![0]);
assert!(tensor.is_empty());
assert_eq!(tensor.len(), 0);
}
#[test]
fn test_per_channel_quantize() {
let data = vec![1.0, 2.0, 3.0, 10.0, 20.0, 30.0];
let (q, scales) = quantize_f32_to_i8_per_channel(&data, 2, 3);
assert_eq!(q.len(), 6);
assert_eq!(scales.len(), 2);
assert!(scales[1] > scales[0]);
}
#[test]
fn test_per_channel_roundtrip() {
let data = vec![1.0, 2.0, 3.0, 10.0, 20.0, 30.0];
let (q, scales) = quantize_f32_to_i8_per_channel(&data, 2, 3);
let reconstructed = dequantize_i8_to_f32_per_channel(&q, &scales, 3);
for (orig, recon) in data.iter().zip(reconstructed.iter()) {
let error = (orig - recon).abs() / orig.abs().max(1.0);
assert!(error < 0.1, "orig={orig}, recon={recon}");
}
}
#[test]
fn test_quantized_linear_forward() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let linear = QuantizedLinear::from_f32(&weight, None, 2, 2);
let input = vec![1.0, 2.0];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.0).abs() < 0.1);
assert!((output[1] - 2.0).abs() < 0.1);
}
#[test]
fn test_quantized_linear_with_bias() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let bias = vec![0.5, -0.5];
let linear = QuantizedLinear::from_f32(&weight, Some(&bias), 2, 2);
let input = vec![1.0, 2.0];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.5).abs() < 0.1);
assert!((output[1] - 1.5).abs() < 0.1);
}
#[test]
fn test_quantized_linear_batch() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let linear = QuantizedLinear::from_f32(&weight, None, 2, 2);
let input = vec![1.0, 2.0, 3.0, 4.0]; let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), 4);
}
#[test]
fn test_quantized_linear_error() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let linear = QuantizedLinear::from_f32(&weight, None, 2, 2);
let input = vec![1.0, 2.0, 3.0]; let result = linear.forward(&input);
assert!(result.is_err());
}
#[test]
fn test_quantized_forward_vs_dequantized() {
let weight = vec![0.5, -0.3, 0.2, 0.8, -0.1, 0.4, 0.6, -0.2, 0.3];
let linear = QuantizedLinear::from_f32(&weight, None, 3, 3);
let input = vec![1.0, 2.0, 3.0];
let output1 = linear.forward(&input).expect("forward");
let output2 = linear.forward_quantized(&input).expect("forward_quantized");
for (o1, o2) in output1.iter().zip(output2.iter()) {
assert!((o1 - o2).abs() < 0.5, "o1={o1}, o2={o2}");
}
}
#[test]
fn test_quantized_linear_memory_size() {
let weight = vec![0.0; 1024]; let bias = vec![0.0; 32];
let linear = QuantizedLinear::from_f32(&weight, Some(&bias), 32, 32);
let size = linear.memory_size();
assert_eq!(size, 1024 + 128 + 4);
}
#[test]
fn test_quantization_error_zero() {
let data = vec![0.0; 10];
let (q, scale) = quantize_f32_to_i8(&data);
let error = quantization_error(&data, &q, scale);
assert!(error < 1e-10);
}
#[test]
fn test_quantization_error_small() {
let data = vec![1.0, -1.0, 0.5, -0.5];
let (q, scale) = quantize_f32_to_i8(&data);
let error = quantization_error(&data, &q, scale);
assert!(error < 0.01, "error={error}");
}
#[test]
fn test_sqnr_high_for_small_error() {
let data: Vec<f32> = (0..100).map(|i| (i as f32) / 10.0).collect();
let (q, scale) = quantize_f32_to_i8(&data);
let sqnr = compute_sqnr(&data, &q, scale);
assert!(sqnr > 30.0, "SQNR too low: {sqnr}");
}
#[test]
fn test_sqnr_empty() {
let sqnr = compute_sqnr(&[], &[], 1.0);
assert!((sqnr - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_quantize_saturation() {
let data = vec![1000.0];
let (q, scale) = quantize_f32_to_i8(&data);
assert_eq!(q[0], 127);
assert!((scale - 1000.0 / 127.0).abs() < 1e-3);
}
#[test]
fn test_quantize_very_small_scale() {
let data = vec![1e-15, -1e-15];
let (q, _scale) = quantize_f32_to_i8(&data);
assert!(q.iter().all(|&x| x == 0));
}
#[test]
fn test_quantize_mixed_magnitude() {
let data = vec![100.0, 0.001, -50.0, 0.0001];
let (q, scale) = quantize_f32_to_i8(&data);
let reconstructed = dequantize_i8_to_f32(&q, scale);
assert!((reconstructed[0] - 100.0).abs() < 2.0);
assert!((reconstructed[2] - (-50.0)).abs() < 2.0);
assert!(reconstructed[1].abs() < 2.0);
}
#[test]
fn test_i4_pack_unpack() {
for val in -8i8..=7i8 {
let packed = pack_i4(val);
let unpacked = unpack_i4(packed);
assert_eq!(unpacked, val, "Pack/unpack failed for {val}");
}
}
#[test]
fn test_i4_quantize_empty() {
let (q, scale) = quantize_f32_to_i4(&[]);
assert!(q.is_empty());
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_i4_quantize_zeros() {
let data = vec![0.0; 10];
let (q, scale) = quantize_f32_to_i4(&data);
assert_eq!(q.len(), 10);
assert!(q.iter().all(|&x| x == 0));
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_i4_quantize_max_value() {
let data = vec![7.0];
let (q, scale) = quantize_f32_to_i4(&data);
assert_eq!(q[0], 7);
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_i4_quantize_negative() {
let data = vec![-7.0];
let (q, scale) = quantize_f32_to_i4(&data);
assert_eq!(q[0], -7);
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_i4_quantize_roundtrip() {
let data = vec![1.0, -1.0, 0.5, -0.5, 0.0, 0.7, -0.7];
let (q, scale) = quantize_f32_to_i4(&data);
let reconstructed = dequantize_i4_to_f32(&q, scale);
for (orig, recon) in data.iter().zip(reconstructed.iter()) {
assert!((orig - recon).abs() < 0.2, "orig={orig}, recon={recon}");
}
}
#[test]
fn test_i4_packed_empty() {
let (packed, scale) = quantize_f32_to_i4_packed(&[]);
assert!(packed.is_empty());
assert!((scale - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_i4_packed_even_length() {
let data = vec![1.0, -1.0, 2.0, -2.0];
let (packed, scale) = quantize_f32_to_i4_packed(&data);
assert_eq!(packed.len(), 2);
let reconstructed = dequantize_i4_packed_to_f32(&packed, scale, data.len());
assert_eq!(reconstructed.len(), 4);
}
#[test]
fn test_i4_packed_odd_length() {
let data = vec![1.0, -1.0, 2.0, -2.0, 3.0];
let (packed, scale) = quantize_f32_to_i4_packed(&data);
assert_eq!(packed.len(), 3);
let reconstructed = dequantize_i4_packed_to_f32(&packed, scale, data.len());
assert_eq!(reconstructed.len(), 5);
}
#[test]
fn test_i4_packed_roundtrip() {
let data = vec![0.5, -0.3, 0.7, -0.1, 0.4, -0.6, 0.2];
let (packed, scale) = quantize_f32_to_i4_packed(&data);
let reconstructed = dequantize_i4_packed_to_f32(&packed, scale, data.len());
for (orig, recon) in data.iter().zip(reconstructed.iter()) {
assert!((orig - recon).abs() < 0.2, "orig={orig}, recon={recon}");
}
}
#[test]
fn test_i4_unpack_to_i8() {
let data = vec![1.0, -2.0, 3.0, -4.0, 5.0];
let (packed, _scale) = quantize_f32_to_i4_packed(&data);
let unpacked = unpack_i4_to_i8(&packed, data.len());
assert_eq!(unpacked.len(), 5);
assert!(unpacked.iter().all(|&x| x >= -8 && x <= 7));
}
#[test]
fn test_quantized_tensor_i4_from_f32() {
let data = vec![1.0, 2.0, 3.0, 4.0];
let tensor = QuantizedTensorInt4::from_f32(&data, vec![2, 2]);
assert_eq!(tensor.len(), 4);
assert_eq!(tensor.shape, vec![2, 2]);
assert_eq!(tensor.numel(), 4);
assert_eq!(tensor.data.len(), 2);
}
#[test]
fn test_quantized_tensor_i4_roundtrip() {
let data = vec![1.0, -2.0, 3.0, -4.0, 5.0, -6.0];
let tensor = QuantizedTensorInt4::from_f32(&data, vec![2, 3]);
let reconstructed = tensor.to_f32();
assert_eq!(reconstructed.len(), 6);
for (orig, recon) in data.iter().zip(reconstructed.iter()) {
let error = (orig - recon).abs() / orig.abs().max(1.0);
assert!(error < 0.3, "Relative error too high: {error}");
}
}
#[test]
fn test_quantized_tensor_i4_empty() {
let tensor = QuantizedTensorInt4::from_f32(&[], vec![0]);
assert!(tensor.is_empty());
assert_eq!(tensor.len(), 0);
}
#[test]
fn test_quantized_tensor_i4_memory_savings() {
let data = vec![0.0; 1000];
let tensor_i8 = QuantizedTensor::from_f32(&data, vec![1000]);
let tensor_i4 = QuantizedTensorInt4::from_f32(&data, vec![1000]);
let i8_size = tensor_i8.data.len();
let i4_size = tensor_i4.data.len();
assert_eq!(i8_size, 1000); assert_eq!(i4_size, 500); }
#[test]
fn test_quantized_tensor_i4_unpack() {
let data = vec![1.0, -1.0, 2.0, -2.0];
let tensor = QuantizedTensorInt4::from_f32(&data, vec![4]);
let unpacked = tensor.unpack();
assert_eq!(unpacked.len(), 4);
}
#[test]
fn test_quantized_linear_i4_forward() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let linear = QuantizedLinearInt4::from_f32(&weight, None, 2, 2);
let input = vec![1.0, 2.0];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.0).abs() < 0.5, "output[0]={}", output[0]);
assert!((output[1] - 2.0).abs() < 0.5, "output[1]={}", output[1]);
}
#[test]
fn test_quantized_linear_i4_with_bias() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let bias = vec![0.5, -0.5];
let linear = QuantizedLinearInt4::from_f32(&weight, Some(&bias), 2, 2);
let input = vec![1.0, 2.0];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.5).abs() < 0.5);
assert!((output[1] - 1.5).abs() < 0.5);
}
#[test]
fn test_quantized_linear_i4_batch() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let linear = QuantizedLinearInt4::from_f32(&weight, None, 2, 2);
let input = vec![1.0, 2.0, 3.0, 4.0]; let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), 4);
}
#[test]
fn test_quantized_linear_i4_error() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let linear = QuantizedLinearInt4::from_f32(&weight, None, 2, 2);
let input = vec![1.0, 2.0, 3.0]; let result = linear.forward(&input);
assert!(result.is_err());
}
#[test]
fn test_quantized_linear_i4_forward_vs_dequantized() {
let weight = vec![0.5, -0.3, 0.2, 0.8, -0.1, 0.4, 0.6, -0.2, 0.3];
let linear = QuantizedLinearInt4::from_f32(&weight, None, 3, 3);
let input = vec![1.0, 2.0, 3.0];
let output1 = linear.forward(&input).expect("forward");
let output2 = linear.forward_quantized(&input).expect("forward_quantized");
for (o1, o2) in output1.iter().zip(output2.iter()) {
assert!((o1 - o2).abs() < 1.0, "o1={o1}, o2={o2}");
}
}
#[test]
fn test_quantized_linear_i4_memory_size() {
let weight = vec![0.0; 1024]; let bias = vec![0.0; 32];
let linear_i8 = QuantizedLinear::from_f32(&weight, Some(&bias), 32, 32);
let linear_i4 = QuantizedLinearInt4::from_f32(&weight, Some(&bias), 32, 32);
let size_i8 = linear_i8.memory_size();
let size_i4 = linear_i4.memory_size();
assert_eq!(size_i8, 1024 + 128 + 4);
assert_eq!(size_i4, 512 + 128 + 4);
}
#[test]
fn test_quantization_error_i4_zero() {
let data = vec![0.0; 10];
let (q, scale) = quantize_f32_to_i4(&data);
let error = quantization_error_i4(&data, &q, scale);
assert!(error < 1e-10);
}
#[test]
fn test_quantization_error_i4_larger_than_i8() {
let data = vec![1.0, -1.0, 0.5, -0.5];
let (q_i8, scale_i8) = quantize_f32_to_i8(&data);
let error_i8 = quantization_error(&data, &q_i8, scale_i8);
let (q_i4, scale_i4) = quantize_f32_to_i4(&data);
let error_i4 = quantization_error_i4(&data, &q_i4, scale_i4);
assert!(error_i4 >= error_i8, "i4_err={error_i4}, i8_err={error_i8}");
}
#[test]
fn test_sqnr_i4_lower_than_i8() {
let data: Vec<f32> = (0..100).map(|i| (i as f32) / 10.0).collect();
let (q_i8, scale_i8) = quantize_f32_to_i8(&data);
let sqnr_i8 = compute_sqnr(&data, &q_i8, scale_i8);
let (q_i4, scale_i4) = quantize_f32_to_i4(&data);
let sqnr_i4 = compute_sqnr_i4(&data, &q_i4, scale_i4);
assert!(sqnr_i4 < sqnr_i8, "i4_sqnr={sqnr_i4}, i8_sqnr={sqnr_i8}");
assert!(sqnr_i4 > 15.0, "SQNR too low: {sqnr_i4}");
}
#[test]
fn test_sqnr_i4_empty() {
let sqnr = compute_sqnr_i4(&[], &[], 1.0);
assert!((sqnr - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_mixed_precision_config_default() {
let config = MixedPrecisionConfig::default();
assert_eq!(config.weight_precision, WeightPrecision::Int4);
assert_eq!(config.activation_precision, ActivationPrecision::Float32);
}
#[test]
fn test_mixed_precision_config_int4_fp32() {
let config = MixedPrecisionConfig::int4_fp32();
assert_eq!(config.weight_precision, WeightPrecision::Int4);
assert_eq!(config.activation_precision, ActivationPrecision::Float32);
}
#[test]
fn test_mixed_precision_config_int8_fp32() {
let config = MixedPrecisionConfig::int8_fp32();
assert_eq!(config.weight_precision, WeightPrecision::Int8);
assert_eq!(config.activation_precision, ActivationPrecision::Float32);
}
#[test]
fn test_mixed_precision_linear_from_config() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let config = MixedPrecisionConfig::int4_fp32();
let linear = MixedPrecisionLinear::from_f32_with_config(&weight, None, 2, 2, config);
assert_eq!(linear.in_features(), 2);
assert_eq!(linear.out_features(), 2);
assert_eq!(linear.weight_precision(), WeightPrecision::Int4);
}
#[test]
fn test_mixed_precision_linear_forward_int4_fp32() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let config = MixedPrecisionConfig::int4_fp32();
let linear = MixedPrecisionLinear::from_f32_with_config(&weight, None, 2, 2, config);
let input = vec![1.0_f32, 2.0_f32];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.0).abs() < 0.5, "output[0]={}", output[0]);
assert!((output[1] - 2.0).abs() < 0.5, "output[1]={}", output[1]);
}
#[test]
fn test_mixed_precision_linear_forward_int8_fp32() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let config = MixedPrecisionConfig::int8_fp32();
let linear = MixedPrecisionLinear::from_f32_with_config(&weight, None, 2, 2, config);
let input = vec![1.0_f32, 2.0_f32];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.0).abs() < 0.2, "output[0]={}", output[0]);
assert!((output[1] - 2.0).abs() < 0.2, "output[1]={}", output[1]);
}
#[test]
fn test_mixed_precision_memory_savings() {
let weight = vec![0.0_f32; 4096];
let linear_int4 = MixedPrecisionLinear::from_f32_with_config(
&weight,
None,
64,
64,
MixedPrecisionConfig::int4_fp32(),
);
let linear_int8 = MixedPrecisionLinear::from_f32_with_config(
&weight,
None,
64,
64,
MixedPrecisionConfig::int8_fp32(),
);
let size_int4 = linear_int4.memory_size();
let size_int8 = linear_int8.memory_size();
assert!(size_int4 < size_int8, "int4={size_int4}, int8={size_int8}");
let ratio = size_int4 as f32 / size_int8 as f32;
assert!(ratio < 0.7, "Memory ratio should be < 0.7, got {ratio}");
}
#[test]
fn test_mixed_precision_accuracy_comparison() {
let weight = vec![0.5, -0.3, 0.2, 0.8, -0.1, 0.4, 0.6, -0.2, 0.3];
let input = vec![1.0, 2.0, 3.0];
let linear_int4 = MixedPrecisionLinear::from_f32_with_config(
&weight,
None,
3,
3,
MixedPrecisionConfig::int4_fp32(),
);
let linear_int8 = MixedPrecisionLinear::from_f32_with_config(
&weight,
None,
3,
3,
MixedPrecisionConfig::int8_fp32(),
);
let output_int4 = linear_int4.forward(&input).expect("forward int4");
let output_int8 = linear_int8.forward(&input).expect("forward int8");
assert_eq!(output_int4.len(), 3);
assert_eq!(output_int8.len(), 3);
for val in &output_int4 {
assert!(val.is_finite(), "int4 output not finite");
}
for val in &output_int8 {
assert!(val.is_finite(), "int8 output not finite");
}
}
#[test]
fn test_mixed_precision_with_bias() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let bias = vec![0.5, -0.5];
let config = MixedPrecisionConfig::int4_fp32();
let linear = MixedPrecisionLinear::from_f32_with_config(&weight, Some(&bias), 2, 2, config);
let input = vec![1.0_f32, 2.0_f32];
let output = linear.forward(&input).expect("forward");
assert!((output[0] - 1.5).abs() < 0.5);
assert!((output[1] - 1.5).abs() < 0.5);
}
#[test]
fn test_mixed_precision_batch_forward() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let config = MixedPrecisionConfig::int4_fp32();
let linear = MixedPrecisionLinear::from_f32_with_config(&weight, None, 2, 2, config);
let input = vec![1.0, 2.0, 3.0, 4.0]; let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), 4);
}
#[test]
fn test_mixed_precision_error_invalid_input() {
let weight = vec![1.0, 0.0, 0.0, 1.0];
let config = MixedPrecisionConfig::int4_fp32();
let linear = MixedPrecisionLinear::from_f32_with_config(&weight, None, 2, 2, config);
let input = vec![1.0, 2.0, 3.0]; let result = linear.forward(&input);
assert!(result.is_err());
}
#[test]
fn test_weight_precision_display() {
assert_eq!(format!("{}", WeightPrecision::Int4), "int4");
assert_eq!(format!("{}", WeightPrecision::Int8), "int8");
}
#[test]
fn test_activation_precision_display() {
assert_eq!(format!("{}", ActivationPrecision::Float32), "fp32");
}
#[test]
fn test_mixed_precision_config_description() {
let config = MixedPrecisionConfig::int4_fp32();
let desc = config.description();
assert!(desc.contains("int4"));
assert!(desc.contains("fp32"));
}
#[test]
fn test_i4_quantize_saturation() {
let data = vec![100.0];
let (q, scale) = quantize_f32_to_i4(&data);
assert_eq!(q[0], 7);
assert!((scale - 100.0 / 7.0).abs() < 1e-3);
}
#[test]
fn test_i4_quantize_negative_saturation() {
let data = vec![-8.0];
let (q, _scale) = quantize_f32_to_i4(&data);
assert!(q[0] <= -7);
let data2 = vec![7.0, -9.0]; let (q2, _) = quantize_f32_to_i4(&data2);
assert!(q2[1] >= -8);
}
#[test]
fn test_i4_single_element() {
let data = vec![0.5];
let (packed, scale) = quantize_f32_to_i4_packed(&data);
assert_eq!(packed.len(), 1);
let reconstructed = dequantize_i4_packed_to_f32(&packed, scale, 1);
assert_eq!(reconstructed.len(), 1);
assert!((reconstructed[0] - 0.5).abs() < 0.2);
}
#[cfg(feature = "realizar-inference")]
mod q4k_tests {
use super::*;
#[test]
fn test_q4k_tensor_creation() {
let super_block_bytes = 144;
let n_values = 256;
let raw_data = vec![0u8; super_block_bytes];
let tensor = QuantizedTensorQ4K::from_raw(raw_data, vec![n_values]);
assert_eq!(tensor.len(), n_values);
assert_eq!(tensor.shape(), &[n_values]);
}
#[test]
fn test_q4k_dequantize_produces_values() {
let super_block_bytes = 144;
let n_values = 256;
let mut raw_data = vec![0u8; super_block_bytes];
raw_data[0] = 0x00;
raw_data[1] = 0x3C;
let tensor = QuantizedTensorQ4K::from_raw(raw_data, vec![n_values]);
let dequantized = tensor.dequantize();
assert_eq!(dequantized.len(), n_values);
assert!(dequantized.iter().all(|x: &f32| x.is_finite()));
}
#[test]
fn test_q4k_memory_savings() {
let n_values = 256 * 4; let super_block_bytes = 144;
let raw_data = vec![0u8; super_block_bytes * 4];
let tensor = QuantizedTensorQ4K::from_raw(raw_data, vec![n_values]);
let q4k_bytes = tensor.memory_bytes();
let f32_bytes = n_values * 4;
let compression_ratio = f32_bytes as f64 / q4k_bytes as f64;
assert!(
compression_ratio > 6.0,
"Expected >6x compression, got {:.2}x",
compression_ratio
);
}
#[test]
fn test_q4k_linear_creation() {
let super_block_bytes = 144usize;
let in_features = 256usize;
let out_features = 64usize;
let n_values = in_features * out_features; let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let bias = vec![0.0f32; out_features];
let linear =
QuantizedLinearQ4K::from_raw(raw_data, Some(&bias), in_features, out_features);
assert_eq!(linear.in_features(), in_features);
assert_eq!(linear.out_features(), out_features);
}
#[test]
fn test_q4k_linear_forward_shape() {
let super_block_bytes = 144usize;
let in_features = 256usize;
let out_features = 64usize;
let n_values = in_features * out_features;
let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let linear = QuantizedLinearQ4K::from_raw(raw_data, None, in_features, out_features);
let input = vec![0.1f32; in_features];
let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), out_features);
}
#[test]
fn test_q4k_linear_memory_vs_f32() {
let super_block_bytes = 144usize;
let in_features = 512usize;
let out_features = 512usize;
let n_values = in_features * out_features; let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let linear = QuantizedLinearQ4K::from_raw(raw_data, None, in_features, out_features);
let q4k_bytes = linear.memory_size();
let f32_bytes = n_values * 4;
let compression_ratio = f32_bytes as f64 / q4k_bytes as f64;
assert!(
compression_ratio > 6.0,
"Expected >6x compression, got {:.2}x",
compression_ratio
);
}
#[test]
fn test_fused_q4k_matvec_shape() {
let super_block_bytes = 144usize;
let in_features = 256usize;
let out_features = 64usize;
let n_values = in_features * out_features;
let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let linear = QuantizedLinearQ4K::from_raw(raw_data, None, in_features, out_features);
let input = vec![0.1f32; in_features];
let output = linear.forward_fused(&input).expect("forward_fused");
assert_eq!(output.len(), out_features);
}
#[test]
fn test_fused_q4k_matvec_matches_dequant() {
let super_block_bytes = 144usize;
let in_features = 256usize;
let out_features = 64usize;
let n_values = in_features * out_features;
let n_blocks = n_values.div_ceil(256);
let mut raw_data = vec![0u8; super_block_bytes * n_blocks];
for block in 0..n_blocks {
let offset = block * super_block_bytes;
raw_data[offset] = 0x00;
raw_data[offset + 1] = 0x3C; }
let bias = vec![0.1f32; out_features];
let linear =
QuantizedLinearQ4K::from_raw(raw_data, Some(&bias), in_features, out_features);
let input: Vec<f32> = (0..in_features).map(|i| (i as f32) * 0.01).collect();
let baseline = linear.forward(&input).expect("forward");
let fused = linear.forward_fused(&input).expect("forward_fused");
assert_eq!(baseline.len(), fused.len());
for (i, (b, f)) in baseline.iter().zip(fused.iter()).enumerate() {
let diff = (*b - *f).abs();
assert!(
diff < 1e-4,
"Mismatch at index {}: baseline={}, fused={}, diff={}",
i,
b,
f,
diff
);
}
}
#[test]
fn test_fused_q4k_batch_forward() {
let super_block_bytes = 144usize;
let in_features = 256usize;
let out_features = 64usize;
let batch_size = 4usize;
let n_values = in_features * out_features;
let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let linear = QuantizedLinearQ4K::from_raw(raw_data, None, in_features, out_features);
let input = vec![0.1f32; in_features * batch_size];
let output = linear.forward_fused(&input).expect("forward_fused");
assert_eq!(output.len(), out_features * batch_size);
}
#[test]
fn test_q5k_tensor_creation() {
let super_block_bytes = 176usize;
let n_values = 256usize;
let raw_data = vec![0u8; super_block_bytes];
let tensor = QuantizedTensorQ5K::from_raw(raw_data, vec![n_values]);
assert_eq!(tensor.len(), n_values);
assert_eq!(tensor.shape(), &[n_values]);
assert_eq!(tensor.memory_bytes(), super_block_bytes);
}
#[test]
fn test_q5k_linear_forward_fused() {
let super_block_bytes = 176usize;
let in_features = 256usize;
let out_features = 64usize;
let n_values = in_features * out_features;
let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let linear = QuantizedLinearQ5K::from_raw(raw_data, None, in_features, out_features);
let input = vec![0.1f32; in_features];
let output = linear.forward_fused(&input).expect("forward_fused");
assert_eq!(output.len(), out_features);
}
#[test]
fn test_q6k_tensor_creation() {
let super_block_bytes = 210usize;
let n_values = 256usize;
let raw_data = vec![0u8; super_block_bytes];
let tensor = QuantizedTensorQ6K::from_raw(raw_data, vec![n_values]);
assert_eq!(tensor.len(), n_values);
assert_eq!(tensor.shape(), &[n_values]);
assert_eq!(tensor.memory_bytes(), super_block_bytes);
}
#[test]
fn test_q6k_linear_forward_fused() {
let super_block_bytes = 210usize;
let in_features = 256usize;
let out_features = 64usize;
let n_values = in_features * out_features;
let n_blocks = n_values.div_ceil(256);
let raw_data = vec![0u8; super_block_bytes * n_blocks];
let linear = QuantizedLinearQ6K::from_raw(raw_data, None, in_features, out_features);
let input = vec![0.1f32; in_features];
let output = linear.forward_fused(&input).expect("forward_fused");
assert_eq!(output.len(), out_features);
}
#[test]
fn test_k_quant_compression_ratios() {
let n_values = 256 * 4usize;
let q4k_data = vec![0u8; 144 * 4];
let q4k = QuantizedTensorQ4K::from_raw(q4k_data, vec![n_values]);
let q5k_data = vec![0u8; 176 * 4];
let q5k = QuantizedTensorQ5K::from_raw(q5k_data, vec![n_values]);
let q6k_data = vec![0u8; 210 * 4];
let q6k = QuantizedTensorQ6K::from_raw(q6k_data, vec![n_values]);
let f32_bytes = n_values * 4;
assert!(q4k.memory_bytes() < q5k.memory_bytes());
assert!(q5k.memory_bytes() < q6k.memory_bytes());
assert!(q6k.memory_bytes() < f32_bytes);
let q4k_ratio = f32_bytes as f64 / q4k.memory_bytes() as f64;
let q5k_ratio = f32_bytes as f64 / q5k.memory_bytes() as f64;
let q6k_ratio = f32_bytes as f64 / q6k.memory_bytes() as f64;
assert!(q4k_ratio > 6.5, "Q4K should have >6.5x compression");
assert!(q5k_ratio > 5.5, "Q5K should have >5.5x compression");
assert!(q6k_ratio > 4.5, "Q6K should have >4.5x compression");
}
#[test]
fn test_quantized_ffn_creation() {
let d_model = 256usize;
let d_ff = 1024usize;
let super_block_bytes = 144usize;
let fc1_values = d_model * d_ff;
let fc1_blocks = fc1_values.div_ceil(256);
let fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let fc2_values = d_ff * d_model;
let fc2_blocks = fc2_values.div_ceil(256);
let fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
let ffn = QuantizedFeedForward::new(fc1_data, fc2_data, d_model, d_ff);
assert_eq!(ffn.d_model(), d_model);
assert_eq!(ffn.d_ff(), d_ff);
}
#[test]
fn test_quantized_ffn_forward() {
let d_model = 256usize;
let d_ff = 1024usize;
let seq_len = 4usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
let ffn = QuantizedFeedForward::new(fc1_data, fc2_data, d_model, d_ff);
let input = vec![0.1f32; seq_len * d_model];
let output = ffn.forward(&input).expect("forward");
assert_eq!(output.len(), seq_len * d_model);
}
#[test]
fn test_quantized_ffn_memory_reduction() {
let d_model = 384usize; let d_ff = 1536usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
let ffn = QuantizedFeedForward::new(fc1_data, fc2_data, d_model, d_ff);
let q4k_bytes = ffn.memory_bytes();
let fp32_bytes = (d_model * d_ff + d_ff * d_model) * 4;
let ratio = q4k_bytes as f64 / fp32_bytes as f64;
assert!(
ratio < 0.15,
"Q4K FFN should use <15% of FP32 memory, got {:.1}%",
ratio * 100.0
);
}
#[test]
fn test_quantized_ffn_output_finite() {
let d_model = 256usize;
let d_ff = 1024usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let mut fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let mut fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
for block in 0..fc1_blocks {
fc1_data[block * super_block_bytes] = 0x00;
fc1_data[block * super_block_bytes + 1] = 0x3C; }
for block in 0..fc2_blocks {
fc2_data[block * super_block_bytes] = 0x00;
fc2_data[block * super_block_bytes + 1] = 0x3C;
}
let ffn = QuantizedFeedForward::new(fc1_data, fc2_data, d_model, d_ff);
let input = vec![0.5f32; d_model];
let output = ffn.forward(&input).expect("forward");
assert!(
output.iter().all(|x: &f32| x.is_finite()),
"All outputs must be finite"
);
}
#[test]
fn test_quantized_decoder_block_creation() {
let d_model = 256usize;
let d_ff = 1024usize;
let n_heads = 4usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
let block = QuantizedDecoderBlock::new(d_model, n_heads, d_ff, fc1_data, fc2_data);
assert_eq!(block.d_model(), d_model);
assert_eq!(block.d_ff(), d_ff);
assert_eq!(block.n_heads(), n_heads);
}
#[test]
fn test_quantized_decoder_block_forward() {
let d_model = 256usize;
let d_ff = 1024usize;
let n_heads = 4usize;
let seq_len = 4usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
let block = QuantizedDecoderBlock::new(d_model, n_heads, d_ff, fc1_data, fc2_data);
let x = vec![0.1f32; seq_len * d_model];
let encoder_output = vec![0.1f32; seq_len * d_model];
let output = block.forward(&x, &encoder_output, None).expect("forward");
assert_eq!(output.len(), seq_len * d_model);
}
#[test]
fn test_quantized_decoder_block_output_finite() {
let d_model = 256usize;
let d_ff = 1024usize;
let n_heads = 4usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let mut fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let mut fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
for block in 0..fc1_blocks {
fc1_data[block * super_block_bytes + 1] = 0x3C;
}
for block in 0..fc2_blocks {
fc2_data[block * super_block_bytes + 1] = 0x3C;
}
let block = QuantizedDecoderBlock::new(d_model, n_heads, d_ff, fc1_data, fc2_data);
let x = vec![0.5f32; d_model];
let encoder_output = vec![0.5f32; d_model];
let output = block.forward(&x, &encoder_output, None).expect("forward");
assert!(
output.iter().all(|v: &f32| v.is_finite()),
"All outputs must be finite"
);
}
#[test]
fn test_quantized_decoder_block_memory_savings() {
let d_model = 384usize; let d_ff = 1536usize;
let n_heads = 6usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let fc1_data = vec![0u8; super_block_bytes * fc1_blocks];
let fc2_data = vec![0u8; super_block_bytes * fc2_blocks];
let block = QuantizedDecoderBlock::new(d_model, n_heads, d_ff, fc1_data, fc2_data);
let ffn_q4k_bytes = block.ffn_memory_bytes();
let ffn_fp32_bytes = (d_model * d_ff + d_ff * d_model) * 4;
let ratio = ffn_q4k_bytes as f64 / ffn_fp32_bytes as f64;
assert!(
ratio < 0.15,
"Q4K FFN should use <15% of FP32 memory, got {:.1}%",
ratio * 100.0
);
}
#[test]
#[ignore = "Allocates large model - run with --ignored"]
fn test_quantized_decoder_creation() {
let n_layers = 4usize;
let d_model = 256usize;
let d_ff = 1024usize;
let n_heads = 4usize;
let n_vocab = 51865usize;
let max_len = 448usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let ffn_data: Vec<(Vec<u8>, Vec<u8>)> = (0..n_layers)
.map(|_| {
(
vec![0u8; super_block_bytes * fc1_blocks],
vec![0u8; super_block_bytes * fc2_blocks],
)
})
.collect();
let decoder =
QuantizedDecoder::new(n_layers, d_model, n_heads, d_ff, n_vocab, max_len, ffn_data);
assert_eq!(decoder.n_layers(), n_layers);
assert_eq!(decoder.d_model(), d_model);
assert_eq!(decoder.n_vocab(), n_vocab);
}
#[test]
#[ignore = "Allocates large model - run with --ignored"]
fn test_quantized_decoder_forward_one() {
let n_layers = 2usize;
let d_model = 256usize;
let d_ff = 1024usize;
let n_heads = 4usize;
let n_vocab = 1000usize; let max_len = 64usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let ffn_data: Vec<(Vec<u8>, Vec<u8>)> = (0..n_layers)
.map(|_| {
(
vec![0u8; super_block_bytes * fc1_blocks],
vec![0u8; super_block_bytes * fc2_blocks],
)
})
.collect();
let decoder =
QuantizedDecoder::new(n_layers, d_model, n_heads, d_ff, n_vocab, max_len, ffn_data);
let mut cache = decoder.create_kv_cache();
let encoder_output = vec![0.1f32; 10 * d_model];
let token = 1u32;
let logits = decoder
.forward_one_quantized(token, &encoder_output, &mut cache)
.expect("forward_one_quantized");
assert_eq!(logits.len(), n_vocab);
}
#[test]
#[ignore = "Allocates large model - run with --ignored"]
fn test_quantized_decoder_output_finite() {
let n_layers = 2usize;
let d_model = 256usize;
let d_ff = 1024usize;
let n_heads = 4usize;
let n_vocab = 100usize;
let max_len = 64usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let ffn_data: Vec<(Vec<u8>, Vec<u8>)> = (0..n_layers)
.map(|_| {
let mut fc1 = vec![0u8; super_block_bytes * fc1_blocks];
let mut fc2 = vec![0u8; super_block_bytes * fc2_blocks];
for b in 0..fc1_blocks {
fc1[b * super_block_bytes + 1] = 0x3C;
}
for b in 0..fc2_blocks {
fc2[b * super_block_bytes + 1] = 0x3C;
}
(fc1, fc2)
})
.collect();
let decoder =
QuantizedDecoder::new(n_layers, d_model, n_heads, d_ff, n_vocab, max_len, ffn_data);
let mut cache = decoder.create_kv_cache();
let encoder_output = vec![0.5f32; 5 * d_model];
let logits = decoder
.forward_one_quantized(1, &encoder_output, &mut cache)
.expect("forward");
assert!(
logits.iter().all(|v: &f32| v.is_finite()),
"All logits must be finite"
);
}
#[test]
#[ignore = "Allocates large model - run with --ignored"]
fn test_quantized_decoder_memory_savings() {
let n_layers = 4usize;
let d_model = 384usize; let d_ff = 1536usize;
let n_heads = 6usize;
let n_vocab = 51865usize;
let max_len = 448usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let ffn_data: Vec<(Vec<u8>, Vec<u8>)> = (0..n_layers)
.map(|_| {
(
vec![0u8; super_block_bytes * fc1_blocks],
vec![0u8; super_block_bytes * fc2_blocks],
)
})
.collect();
let decoder =
QuantizedDecoder::new(n_layers, d_model, n_heads, d_ff, n_vocab, max_len, ffn_data);
let ffn_q4k_bytes = decoder.ffn_memory_bytes();
let ffn_fp32_bytes = n_layers * (d_model * d_ff + d_ff * d_model) * 4;
let ratio = ffn_q4k_bytes as f64 / ffn_fp32_bytes as f64;
assert!(
ratio < 0.15,
"Q4K FFN should use <15% of FP32 memory, got {:.1}%",
ratio * 100.0
);
}
#[test]
#[ignore = "Allocates large model - run with --ignored"]
fn test_quantized_decoder_token_generation_time() {
use std::time::Instant;
let n_layers = 4usize;
let d_model = 384usize; let d_ff = 1536usize;
let n_heads = 6usize;
let n_vocab = 51865usize;
let max_len = 448usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let ffn_data: Vec<(Vec<u8>, Vec<u8>)> = (0..n_layers)
.map(|_| {
let mut fc1 = vec![0u8; super_block_bytes * fc1_blocks];
let mut fc2 = vec![0u8; super_block_bytes * fc2_blocks];
for b in 0..fc1_blocks {
fc1[b * super_block_bytes + 1] = 0x3C;
}
for b in 0..fc2_blocks {
fc2[b * super_block_bytes + 1] = 0x3C;
}
(fc1, fc2)
})
.collect();
let decoder =
QuantizedDecoder::new(n_layers, d_model, n_heads, d_ff, n_vocab, max_len, ffn_data);
let mut cache = decoder.create_kv_cache();
let encoder_output = vec![0.1f32; 10 * d_model];
let _ = decoder.forward_one_quantized(1, &encoder_output, &mut cache);
cache.clear();
let start = Instant::now();
let num_tokens = 10;
for i in 0..num_tokens {
let _ = decoder.forward_one_quantized((i + 1) as u32, &encoder_output, &mut cache);
}
let elapsed = start.elapsed();
let ms_per_token = elapsed.as_millis() as f64 / num_tokens as f64;
println!(
"Quantized decoder: {:.2}ms per token ({} tokens in {:?})",
ms_per_token, num_tokens, elapsed
);
assert!(
ms_per_token > 0.0,
"Token generation should take measurable time"
);
}
#[test]
#[ignore = "Allocates large model - run with --ignored"]
fn test_quantized_memory_reduction_validation() {
let n_layers = 4usize;
let d_model = 384usize;
let d_ff = 1536usize;
let n_heads = 6usize;
let n_vocab = 51865usize;
let max_len = 448usize;
let super_block_bytes = 144usize;
let fc1_blocks = (d_model * d_ff).div_ceil(256);
let fc2_blocks = (d_ff * d_model).div_ceil(256);
let ffn_data: Vec<(Vec<u8>, Vec<u8>)> = (0..n_layers)
.map(|_| {
(
vec![0u8; super_block_bytes * fc1_blocks],
vec![0u8; super_block_bytes * fc2_blocks],
)
})
.collect();
let decoder =
QuantizedDecoder::new(n_layers, d_model, n_heads, d_ff, n_vocab, max_len, ffn_data);
let ffn_q4k_bytes = decoder.ffn_memory_bytes();
let ffn_fp32_bytes = n_layers * (d_model * d_ff + d_ff * d_model) * 4;
let embedding_bytes = n_vocab * d_model * 4; let pos_embedding_bytes = max_len * d_model * 4;
let attention_bytes_per_layer = 4 * d_model * d_model * 4;
let total_attention_bytes = n_layers * attention_bytes_per_layer;
let fp32_total =
ffn_fp32_bytes + embedding_bytes + pos_embedding_bytes + total_attention_bytes;
let q4k_total =
ffn_q4k_bytes + embedding_bytes + pos_embedding_bytes + total_attention_bytes;
let ffn_reduction = 1.0 - (ffn_q4k_bytes as f64 / ffn_fp32_bytes as f64);
let total_reduction = 1.0 - (q4k_total as f64 / fp32_total as f64);
let ffn_fraction = ffn_fp32_bytes as f64 / fp32_total as f64;
println!("Memory Analysis:");
println!(
" FFN FP32: {:.2} MB ({:.1}% of model)",
ffn_fp32_bytes as f64 / 1_000_000.0,
ffn_fraction * 100.0
);
println!(" FFN Q4K: {:.2} MB", ffn_q4k_bytes as f64 / 1_000_000.0);
println!(" FFN reduction: {:.1}%", ffn_reduction * 100.0);
println!(
" Embeddings: {:.2} MB ({:.1}% of model - not quantized)",
(embedding_bytes + pos_embedding_bytes) as f64 / 1_000_000.0,
(embedding_bytes + pos_embedding_bytes) as f64 / fp32_total as f64 * 100.0
);
println!(
" Attention: {:.2} MB ({:.1}% of model - FP32 for accuracy)",
total_attention_bytes as f64 / 1_000_000.0,
total_attention_bytes as f64 / fp32_total as f64 * 100.0
);
println!(" Total FP32: {:.2} MB", fp32_total as f64 / 1_000_000.0);
println!(" Total Q4K: {:.2} MB", q4k_total as f64 / 1_000_000.0);
println!(" Total reduction: {:.1}%", total_reduction * 100.0);
assert!(
ffn_reduction > 0.85,
"FFN should be reduced by >85%, got {:.1}%",
ffn_reduction * 100.0
);
let expected_total_reduction = ffn_fraction * ffn_reduction;
println!(
" Expected total reduction: {:.1}% (ffn_fraction * ffn_reduction)",
expected_total_reduction * 100.0
);
assert!(
total_reduction > 0.10,
"Total model should be reduced by >10%, got {:.1}%",
total_reduction * 100.0
);
assert!(
(total_reduction - expected_total_reduction).abs() < 0.02,
"Total reduction {:.1}% should be within 2% of expected {:.1}%",
total_reduction * 100.0,
expected_total_reduction * 100.0
);
}
#[test]
fn test_rtf_theoretical_improvement() {
let baseline_rtf = 3.92_f64;
let decoder_fraction = 0.801_f64; let ffn_fraction_of_decoder = 0.6_f64;
let memory_speedup = 2.0_f64;
let ffn_speedup = memory_speedup;
let decoder_speedup =
1.0 / ((1.0 - ffn_fraction_of_decoder) + ffn_fraction_of_decoder / ffn_speedup);
let total_speedup =
1.0 / ((1.0 - decoder_fraction) + decoder_fraction / decoder_speedup);
let new_rtf = baseline_rtf / total_speedup;
println!("RTF Improvement Analysis:");
println!(" Baseline RTF: {:.2}x", baseline_rtf);
println!(" FFN speedup (memory): {:.2}x", ffn_speedup);
println!(" Decoder speedup: {:.2}x", decoder_speedup);
println!(" Total speedup: {:.2}x", total_speedup);
println!(" Projected RTF: {:.2}x", new_rtf);
assert!(
new_rtf < baseline_rtf,
"Q4K should improve RTF: {:.2}x should be < {:.2}x",
new_rtf,
baseline_rtf
);
let target_rtf = 3.0_f64;
println!(" Target RTF: {:.2}x", target_rtf);
println!(" Meets target: {}", new_rtf < target_rtf);
}
#[test]
fn test_quantized_attention_creation() {
let n_heads = 6;
let d_model = 384;
let attn = QuantizedMultiHeadAttention::new_random(n_heads, d_model);
assert_eq!(attn.n_heads(), n_heads, "Number of heads mismatch");
assert_eq!(attn.d_model(), d_model, "Model dimension mismatch");
assert_eq!(attn.d_head(), d_model / n_heads, "Head dimension mismatch");
}
#[test]
fn test_quantized_attention_forward() {
let n_heads = 6;
let d_model = 384;
let seq_len = 1;
let attn = QuantizedMultiHeadAttention::new_random(n_heads, d_model);
let input = vec![0.1f32; d_model * seq_len];
let output = attn
.forward(&input, &input, &input, None)
.expect("forward should succeed");
assert_eq!(output.len(), d_model * seq_len, "Output length mismatch");
assert!(
output.iter().all(|x: &f32| x.is_finite()),
"Output contains non-finite values"
);
}
#[test]
fn test_quantized_attention_output_shape() {
let n_heads = 6;
let d_model = 384;
let attn = QuantizedMultiHeadAttention::new_random(n_heads, d_model);
for seq_len in [1, 4, 16, 64] {
let input = vec![0.1f32; d_model * seq_len];
let output = attn
.forward(&input, &input, &input, None)
.expect("forward should succeed");
assert_eq!(
output.len(),
d_model * seq_len,
"Output shape mismatch for seq_len={seq_len}"
);
}
let q_len = 1;
let kv_len = 1500;
let query = vec![0.1f32; d_model * q_len];
let key_value = vec![0.1f32; d_model * kv_len];
let output = attn
.forward(&query, &key_value, &key_value, None)
.expect("cross-attention should succeed");
assert_eq!(
output.len(),
d_model * q_len,
"Cross-attention output shape mismatch"
);
}
#[test]
fn test_quantized_attention_memory_savings() {
let n_heads = 6;
let d_model = 384;
let attn = QuantizedMultiHeadAttention::new_random(n_heads, d_model);
let fp32_bytes = 4 * d_model * d_model * 4;
let q4k_bytes = attn.memory_bytes();
let reduction = 1.0 - (q4k_bytes as f64 / fp32_bytes as f64);
println!("Attention Memory Analysis:");
println!(" FP32: {:.2} MB", fp32_bytes as f64 / 1_000_000.0);
println!(" Q4K: {:.2} MB", q4k_bytes as f64 / 1_000_000.0);
println!(" Reduction: {:.1}%", reduction * 100.0);
assert!(
reduction > 0.80,
"Attention should be reduced by >80%, got {:.1}%",
reduction * 100.0
);
}
#[test]
fn test_fully_quantized_block_creation() {
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let block = FullyQuantizedDecoderBlock::new_random(n_heads, d_model, d_ff);
assert_eq!(block.d_model(), d_model, "Model dimension mismatch");
assert_eq!(block.d_ff(), d_ff, "FFN dimension mismatch");
assert_eq!(block.n_heads(), n_heads, "Number of heads mismatch");
}
#[test]
fn test_fully_quantized_block_forward() {
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let seq_len = 1;
let encoder_len = 1500;
let block = FullyQuantizedDecoderBlock::new_random(n_heads, d_model, d_ff);
let decoder_input = vec![0.1f32; d_model * seq_len];
let encoder_output = vec![0.1f32; d_model * encoder_len];
let output = block
.forward(&decoder_input, &encoder_output)
.expect("forward");
assert_eq!(output.len(), d_model * seq_len, "Output shape mismatch");
assert!(
output.iter().all(|x: &f32| x.is_finite()),
"Output contains non-finite values"
);
}
#[test]
fn test_fully_quantized_block_memory_savings() {
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let block = FullyQuantizedDecoderBlock::new_random(n_heads, d_model, d_ff);
let attn_fp32 = 4 * d_model * d_model * 4; let ffn_fp32 = 2 * d_model * d_ff * 4; let fp32_bytes = 2 * attn_fp32 + ffn_fp32;
let q4k_bytes = block.memory_bytes();
let reduction = 1.0 - (q4k_bytes as f64 / fp32_bytes as f64);
println!("Fully Quantized Block Memory Analysis:");
println!(" FP32: {:.2} MB", fp32_bytes as f64 / 1_000_000.0);
println!(" Q4K: {:.2} MB", q4k_bytes as f64 / 1_000_000.0);
println!(" Reduction: {:.1}%", reduction * 100.0);
assert!(
reduction > 0.75,
"Block should be reduced by >75%, got {:.1}%",
reduction * 100.0
);
}
#[test]
fn test_fully_quantized_block_multi_token() {
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let encoder_len = 1500;
let block = FullyQuantizedDecoderBlock::new_random(n_heads, d_model, d_ff);
let encoder_output = vec![0.1f32; d_model * encoder_len];
for seq_len in [1, 4, 16] {
let decoder_input = vec![0.1f32; d_model * seq_len];
let output = block
.forward(&decoder_input, &encoder_output)
.expect("forward");
assert_eq!(
output.len(),
d_model * seq_len,
"Output shape mismatch for seq_len={seq_len}"
);
}
}
#[test]
#[ignore = "Heavy: allocates large quantized decoder"]
fn test_fully_quantized_decoder_creation() {
let n_layers = 4;
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let n_vocab = 51865;
let max_len = 448;
let decoder = FullyQuantizedDecoder::new_random(
n_layers, d_model, n_heads, d_ff, n_vocab, max_len,
);
assert_eq!(decoder.n_layers(), n_layers, "Layer count mismatch");
assert_eq!(decoder.d_model(), d_model, "Model dimension mismatch");
assert_eq!(decoder.n_vocab(), n_vocab, "Vocab size mismatch");
}
#[test]
#[ignore = "Heavy: allocates large quantized decoder"]
fn test_fully_quantized_decoder_forward_one() {
let n_layers = 4;
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let n_vocab = 51865;
let max_len = 448;
let encoder_len = 1500;
let decoder = FullyQuantizedDecoder::new_random(
n_layers, d_model, n_heads, d_ff, n_vocab, max_len,
);
let encoder_output = vec![0.1f32; d_model * encoder_len];
let mut cache = decoder.create_kv_cache();
let token = 50258u32; let logits = decoder
.forward_one_fully_quantized(token, &encoder_output, &mut cache)
.expect("forward_one");
assert_eq!(logits.len(), n_vocab, "Logits length mismatch");
assert!(
logits.iter().all(|x: &f32| x.is_finite()),
"Logits contain non-finite values"
);
}
#[test]
#[ignore = "Heavy: allocates large quantized decoder"]
fn test_fully_quantized_decoder_memory_savings() {
let n_layers = 4;
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let n_vocab = 51865;
let max_len = 448;
let decoder = FullyQuantizedDecoder::new_random(
n_layers, d_model, n_heads, d_ff, n_vocab, max_len,
);
let attn_fp32_per_block = 2 * 4 * d_model * d_model * 4; let ffn_fp32_per_block = 2 * d_model * d_ff * 4;
let fp32_blocks = n_layers * (attn_fp32_per_block + ffn_fp32_per_block);
let q4k_blocks = decoder.block_memory_bytes();
let reduction = 1.0 - (q4k_blocks as f64 / fp32_blocks as f64);
println!("Fully Quantized Decoder Memory Analysis:");
println!(" FP32 blocks: {:.2} MB", fp32_blocks as f64 / 1_000_000.0);
println!(" Q4K blocks: {:.2} MB", q4k_blocks as f64 / 1_000_000.0);
println!(" Block reduction: {:.1}%", reduction * 100.0);
assert!(
reduction > 0.75,
"Blocks should be reduced by >75%, got {:.1}%",
reduction * 100.0
);
}
#[test]
#[ignore = "Long-running performance benchmark (~90s) - run explicitly with --ignored"]
fn test_fully_quantized_decoder_token_generation_time() {
let n_layers = 4;
let n_heads = 6;
let d_model = 384;
let d_ff = 1536;
let n_vocab = 51865;
let max_len = 448;
let encoder_len = 1500;
let decoder = FullyQuantizedDecoder::new_random(
n_layers, d_model, n_heads, d_ff, n_vocab, max_len,
);
let encoder_output = vec![0.1f32; d_model * encoder_len];
let mut cache = decoder.create_kv_cache();
let _ = decoder.forward_one_fully_quantized(50258, &encoder_output, &mut cache);
cache = decoder.create_kv_cache();
let n_tokens = 10;
let start = std::time::Instant::now();
for i in 0..n_tokens {
let token = (50258 + i) as u32;
let _ = decoder
.forward_one_fully_quantized(token, &encoder_output, &mut cache)
.expect("forward_one");
}
let elapsed = start.elapsed();
let ms_per_token = elapsed.as_secs_f64() * 1000.0 / n_tokens as f64;
println!(
"Fully quantized decoder: {:.2}ms per token ({} tokens in {:?})",
ms_per_token, n_tokens, elapsed
);
assert!(elapsed.as_secs() < 60, "Token generation too slow");
}
}
#[cfg(feature = "realizar-inference")]
mod q2k_tests {
use super::*;
#[test]
fn test_q2k_tensor_creation() {
let super_block_bytes = 100;
let n_values = 256;
let raw_data = vec![0u8; super_block_bytes];
let tensor = QuantizedTensorQ2K::from_raw(raw_data, vec![n_values]);
assert_eq!(tensor.len(), n_values);
assert_eq!(tensor.shape(), &[n_values]);
assert!(!tensor.is_empty());
}
#[test]
fn test_q2k_compression_ratio() {
let n_values = 256 * 4; let super_block_bytes = 196;
let raw_data = vec![0u8; super_block_bytes * 4];
let tensor = QuantizedTensorQ2K::from_raw(raw_data, vec![n_values]);
let compression = tensor.compression_ratio();
assert!(
compression > 5.0,
"Q2_K compression ratio should be >5x, got {compression:.2}x"
);
}
#[test]
fn test_q2k_quantize_roundtrip() {
let original: Vec<f32> = (0..256).map(|i| (i as f32 - 128.0) / 64.0).collect();
let shape = vec![256];
let quantized = quantize_to_q2k(&original, shape);
let dequantized = quantized.dequantize();
assert_eq!(dequantized.len(), original.len());
let mut max_error = 0.0f32;
for (orig, deq) in original.iter().zip(dequantized.iter()) {
let error = (orig - deq).abs();
max_error = max_error.max(error);
}
let range = original.iter().cloned().fold(f32::NEG_INFINITY, f32::max)
- original.iter().cloned().fold(f32::INFINITY, f32::min);
let relative_error = max_error / range;
assert!(
relative_error < 0.05,
"Q2_K roundtrip error too high: {relative_error:.4}"
);
}
#[test]
fn test_q2k_linear_forward() {
let in_features = 64;
let out_features = 32;
let n_values = in_features * out_features;
let weights: Vec<f32> = (0..n_values)
.map(|i| ((i % 17) as f32 - 8.0) / 16.0)
.collect();
let shape = vec![out_features, in_features];
let quantized = quantize_to_q2k(&weights, shape);
let linear = QuantizedLinearQ2K::from_raw(
quantized.raw_data().to_vec(),
None,
in_features,
out_features,
);
let input = vec![1.0f32; in_features];
let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), out_features);
assert!(output.iter().all(|x| x.is_finite()));
}
#[test]
fn test_fp16_conversion_roundtrip() {
let test_values = [0.0, 1.0, -1.0, 0.5, 100.0, -100.0];
for &v in &test_values {
let fp16 = f32_to_f16(v);
let back = f16_to_f32(fp16);
let error = if v.abs() > 1e-6 {
(v - back).abs() / v.abs()
} else {
(v - back).abs()
};
assert!(error < 0.01, "fp16 error for {v}: {error}");
}
}
}
#[cfg(feature = "realizar-inference")]
mod q8_0_tests {
use super::*;
#[test]
fn test_q8_0_tensor_from_f32() {
let data = vec![0.5_f32; 64]; let tensor = QuantizedTensorQ8_0::from_f32(&data, vec![64]);
assert_eq!(tensor.len(), 64);
assert!(!tensor.is_empty());
assert_eq!(tensor.memory_bytes(), 68);
}
#[test]
fn test_q8_0_tensor_dequantize_roundtrip() {
let data: Vec<f32> = (0..128).map(|i| (i as f32 - 64.0) / 100.0).collect();
let tensor = QuantizedTensorQ8_0::from_f32(&data, vec![128]);
let dequantized = tensor.dequantize();
assert_eq!(dequantized.len(), 128);
let max_error: f32 = data
.iter()
.zip(dequantized.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
assert!(max_error < 0.01, "Q8_0 max error too large: {max_error}");
}
#[test]
fn test_q8_0_tensor_compression_ratio() {
let data = vec![0.5_f32; 1024];
let tensor = QuantizedTensorQ8_0::from_f32(&data, vec![1024]);
let ratio = tensor.compression_ratio();
assert!(ratio > 3.5, "Compression ratio should be ~4x: {ratio}");
}
#[test]
fn test_q8_0_linear_forward() {
let in_features = 64;
let out_features = 32;
let weights: Vec<f32> = (0..in_features * out_features)
.map(|i| ((i % 13) as f32 - 6.0) / 100.0)
.collect();
let linear = QuantizedLinearQ8_0::from_f32(&weights, None, in_features, out_features);
let input = vec![1.0f32; in_features];
let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), out_features);
assert!(output.iter().all(|x| x.is_finite()));
}
#[test]
fn test_q8_0_linear_with_bias() {
let in_features = 32;
let out_features = 16;
let weights = vec![0.1f32; in_features * out_features];
let bias = vec![0.5f32; out_features];
let linear =
QuantizedLinearQ8_0::from_f32(&weights, Some(&bias), in_features, out_features);
let input = vec![1.0f32; in_features];
let output = linear.forward(&input).expect("forward");
for &o in &output {
assert!(o > 3.0, "Output should include bias: {o}");
}
}
#[test]
fn test_q8_0_linear_finalize_weights() {
let weights = vec![0.5f32; 256];
let mut linear = QuantizedLinearQ8_0::from_f32(&weights, None, 16, 16);
assert!(!linear.is_finalized());
linear.finalize_weights();
assert!(linear.is_finalized());
let mem_bytes = linear.memory_bytes();
assert!(mem_bytes > 256); }
#[test]
fn test_q8_0_linear_forward_int8() {
let weights = vec![0.1f32; 64];
let linear = QuantizedLinearQ8_0::from_f32(&weights, None, 8, 8);
let input = vec![1.0f32; 8];
let out1 = linear.forward(&input).expect("forward");
let out2 = linear.forward_int8(&input).expect("forward_int8");
assert_eq!(out1.len(), out2.len());
for (a, b) in out1.iter().zip(out2.iter()) {
assert!((a - b).abs() < 1e-6, "INT8 and regular should match");
}
}
#[test]
fn test_q8_0_linear_batch_forward() {
let in_features = 32;
let out_features = 16;
let batch_size = 4;
let weights = vec![0.1f32; in_features * out_features];
let linear = QuantizedLinearQ8_0::from_f32(&weights, None, in_features, out_features);
let input = vec![1.0f32; batch_size * in_features];
let output = linear.forward(&input).expect("forward");
assert_eq!(output.len(), batch_size * out_features);
}
#[test]
fn test_q8_0_vs_f32_accuracy() {
let in_features = 64;
let out_features = 32;
let weights: Vec<f32> = (0..in_features * out_features)
.map(|i| ((i % 17) as f32 - 8.0) / 16.0)
.collect();
let q8_linear =
QuantizedLinearQ8_0::from_f32(&weights, None, in_features, out_features);
let input: Vec<f32> = (0..in_features).map(|i| (i as f32) / 100.0).collect();
let q8_output = q8_linear.forward(&input).expect("q8_forward");
let weights_t = simd::transpose(&weights, out_features, in_features);
let f32_output = simd::matmul(&input, &weights_t, 1, in_features, out_features);
let max_error: f32 = q8_output
.iter()
.zip(f32_output.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
assert!(
max_error < 0.1,
"Q8_0 vs F32 max error too large: {max_error}"
);
}
}
}