use anyhow::{Context, Result, ensure};
use crate::backend::cpu;
use crate::gguf::GgufFile;
use crate::kv_cache::{InferenceState, LayerState};
use crate::tensor::DType;
#[doc(hidden)]
pub mod oracle_dump {
use std::cell::RefCell;
thread_local! {
static SINK: RefCell<Option<Vec<(String, f64)>>> = const { RefCell::new(None) };
}
pub fn begin() {
SINK.with(|s| *s.borrow_mut() = Some(Vec::new()));
}
pub fn take() -> Vec<(String, f64)> {
SINK.with(|s| s.borrow_mut().take().unwrap_or_default())
}
#[inline]
pub fn is_active() -> bool {
SINK.with(|s| s.borrow().is_some())
}
#[inline]
pub(crate) fn record(name: &str, data: &[f32]) {
SINK.with(|s| {
if let Some(buf) = s.borrow_mut().as_mut() {
buf.push((name.to_string(), data.iter().map(|&x| x as f64).sum()));
}
});
}
}
#[derive(Debug, Clone)]
pub(crate) struct WeightRef {
pub start: usize,
pub size: usize,
pub dtype: DType,
pub m: usize,
pub k: usize,
}
pub(crate) fn resolve_weight(gguf: &GgufFile, name: &str) -> Result<WeightRef> {
let info = gguf
.tensors
.get(name)
.with_context(|| format!("tensor not found: {name}"))?;
let start =
usize::try_from(info.offset).with_context(|| format!("tensor {name} offset overflow"))?;
ensure!(
info.size_bytes > 0,
"tensor {name} has unsupported GGML type {} ({}) — cera cannot run this file",
info.ggml_type_id,
crate::gguf::ggml_type_name(info.ggml_type_id)
);
let size = info.size_bytes;
let dtype = info.dtype;
let k = info.shape.first().copied().unwrap_or(1); let m = if info.shape.len() > 1 {
info.shape[1]
} else {
1
};
Ok(WeightRef {
start,
size,
dtype,
m,
k,
})
}
#[inline]
pub(crate) fn weight_data<'a>(gguf: &'a GgufFile, wref: &WeightRef) -> &'a [u8] {
&gguf.mmap_data()[wref.start..wref.start + wref.size]
}
pub(crate) fn gemv(gguf: &GgufFile, wref: &WeightRef, x: &[f32], y: &mut [f32]) {
let data = weight_data(gguf, wref);
cpu::gemv_dispatch(wref.dtype, data, x, y, wref.m, wref.k, None);
}
#[cfg(target_arch = "aarch64")]
pub(crate) fn gemv_preq(
gguf: &GgufFile,
wref: &WeightRef,
x_f32: &[f32],
q8s: &[f32],
q8q: &[i8],
y: &mut [f32],
) {
let data = weight_data(gguf, wref);
cpu::gemv_with_preq(wref.dtype, data, q8s, q8q, x_f32, y, wref.m, wref.k);
}
#[cfg(target_arch = "aarch64")]
pub(crate) fn quantize_to_scratch(x: &[f32], state: &mut InferenceState) {
assert_eq!(
x.len() % 32,
0,
"quantize_to_scratch: x.len() must be divisible by 32"
);
let nb = x.len() / 32;
state.scratch.q8_scales.resize(nb, 0.0);
state.scratch.q8_quants.resize(x.len(), 0);
unsafe {
crate::backend::simd::neon::quantize_f32_to_q8_0_neon(
x,
&mut state.scratch.q8_scales,
&mut state.scratch.q8_quants,
);
}
}
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64", feature = "blas"))]
pub(crate) fn batched_gemm_supports(dtype: DType, k: usize) -> bool {
match dtype {
DType::Q4_0 | DType::Q8_0 => {
cfg!(feature = "blas") || crate::backend::cpu::int8_gemm_available()
}
DType::Q4KM | DType::Q6K => k_quant_gemm_available() && k % 256 == 0,
_ => false,
}
}
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64", feature = "blas"))]
fn k_quant_gemm_available() -> bool {
#[cfg(feature = "blas")]
{
true
}
#[cfg(all(not(feature = "blas"), target_arch = "aarch64"))]
{
crate::backend::simd::neon::k_quant_gemm_available()
}
#[cfg(all(not(feature = "blas"), target_arch = "x86_64"))]
{
crate::backend::cpu::int8_gemm_available()
}
#[cfg(all(
not(feature = "blas"),
not(target_arch = "aarch64"),
not(target_arch = "x86_64")
))]
{
false
}
}
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64", feature = "blas"))]
pub(crate) fn warn_unbatchable(tensor: &str, dtype: DType) {
use std::sync::Mutex;
static SEEN: Mutex<Vec<DType>> = Mutex::new(Vec::new());
let mut guard = match SEEN.lock() {
Ok(g) => g,
Err(p) => p.into_inner(), };
if !guard.contains(&dtype) {
guard.push(dtype);
tracing::warn!(
"prefill fell back to the per-token path: `{tensor}` is {dtype:?}, which is \
not supported on the batched path for this model. Prefill will be several \
times slower than it should be."
);
}
}
#[cfg(feature = "blas")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn try_blas_prefill_gemm(
gguf: &GgufFile,
wref: &WeightRef,
b: &[f32],
out: &mut [f32],
m: usize,
n: usize,
k: usize,
dequant_scratch: &mut Vec<f32>,
) -> bool {
debug_assert_eq!(wref.m, m, "try_blas_prefill_gemm: weight m mismatch");
debug_assert_eq!(wref.k, k, "try_blas_prefill_gemm: weight k mismatch");
let data = weight_data(gguf, wref);
if dequant_scratch.len() < m * k {
dequant_scratch.resize(m * k, 0.0);
}
let dequant = &mut dequant_scratch[..m * k];
match wref.dtype {
DType::Q4_0 => crate::quant::dequantize_q4_0_matrix(data, m, k, dequant),
DType::Q4_1 => crate::quant::dequantize_q4_1_matrix(data, m, k, dequant),
DType::Q8_0 => crate::quant::dequantize_q8_0_matrix(data, m, k, dequant),
DType::Q4KM => crate::quant::dequantize_q4_k_m_matrix(data, m, k, dequant),
DType::Q6K => crate::quant::dequantize_q6_k_matrix(data, m, k, dequant),
_ => return false,
}
crate::backend::blas::sgemm_rowmajor_nn(m, n, k, dequant, b, out);
true
}
#[cfg(all(
any(target_arch = "aarch64", target_arch = "x86_64"),
not(feature = "blas")
))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn gemm_preq(
gguf: &GgufFile,
wref: &WeightRef,
b_scales: &[f32],
b_quants: &[i8],
out: &mut [f32],
m: usize,
n: usize,
k: usize,
) -> bool {
debug_assert_eq!(wref.m, m, "gemm_preq: weight m mismatch");
debug_assert_eq!(wref.k, k, "gemm_preq: weight k mismatch");
debug_assert_eq!(k % 32, 0, "gemm_preq: k ({k}) must be a multiple of 32");
debug_assert!(
b_scales.len() >= n * (k / 32) && b_quants.len() >= n * k,
"gemm_preq: input scratch too small (need {} scales / {} quants for n={n}, k={k})",
n * (k / 32),
n * k,
);
let data = weight_data(gguf, wref);
let b_scales = &b_scales[..n * (k / 32)];
let b_quants = &b_quants[..n * k];
let ran = cpu::gemm_preq_dispatch(wref.dtype, data, b_scales, b_quants, out, m, n, k);
if !ran {
report_uncomputed_gemm(wref.dtype, k);
}
ran
}
#[cfg(all(
any(target_arch = "aarch64", target_arch = "x86_64"),
not(feature = "blas")
))]
fn report_uncomputed_gemm(dtype: DType, k: usize) {
debug_assert!(
false,
"gemm_preq: no batched kernel ran for {dtype:?} (k={k}), but `batched_gemm_supports` \
admitted it — the gate and the kernel table have drifted. `out` is now stale."
);
tracing::error!(
"gemm_preq: no batched kernel for {dtype:?} (k={k}); the matmul was NOT computed \
and the output buffer holds stale data"
);
}
#[cfg(all(
any(target_arch = "aarch64", target_arch = "x86_64"),
not(feature = "blas")
))]
pub(crate) fn quantize_columns(
mat: &[f32],
dim: usize,
n: usize,
col: &mut [f32],
scales: &mut [f32],
quants: &mut [i8],
) {
debug_assert_eq!(
dim % 32,
0,
"quantize_columns: dim ({dim}) must be a multiple of 32"
);
debug_assert!(
mat.len() >= dim * n
&& col.len() >= dim
&& scales.len() >= n * (dim / 32)
&& quants.len() >= n * dim,
"quantize_columns: scratch too small for dim={dim}, n={n}",
);
let nb = dim / 32;
#[cfg(feature = "parallel")]
{
let min_cols = cpu::prequant_par_min_cols();
if n >= min_cols {
let mat_ptr = mat.as_ptr() as usize;
let quants_ptr = quants.as_mut_ptr() as usize;
cpu::par_rows_n(&mut scales[..n * nb], nb, min_cols, move |(j, sc)| {
let mat = mat_ptr as *const f32;
let qcol = (quants_ptr as *mut i8).wrapping_add(j * dim);
let mut blk = [0.0f32; 32];
for b in 0..nb {
for (t, bt) in blk.iter_mut().enumerate() {
*bt = unsafe { *mat.add((b * 32 + t) * n + j) };
}
let qs = unsafe { core::slice::from_raw_parts_mut(qcol.add(b * 32), 32) };
cpu::quantize_f32_to_q8_0_into(&blk, &mut sc[b..b + 1], qs);
}
});
return;
}
}
for j in 0..n {
for i in 0..dim {
col[i] = mat[i * n + j];
}
cpu::quantize_f32_to_q8_0_into(
&col[..dim],
&mut scales[j * nb..(j + 1) * nb],
&mut quants[j * dim..(j + 1) * dim],
);
}
}
pub(crate) fn dequantize_row_into(
gguf: &GgufFile,
wref: &WeightRef,
row_idx: usize,
out: &mut [f32],
) {
assert!(
row_idx < wref.m,
"dequantize_row: row_idx {row_idx} out of range (m={})",
wref.m
);
let data = weight_data(gguf, wref);
let block_size = wref.dtype.block_size();
assert_eq!(
wref.k % block_size,
0,
"dequantize_row: k ({}) is not a multiple of the {:?} block size ({block_size})",
wref.k,
wref.dtype,
);
let row_bytes = wref.k / block_size * wref.dtype.block_bytes();
let row_start = row_idx * row_bytes;
let row_data = &data[row_start..row_start + row_bytes];
match wref.dtype {
DType::Q6K => crate::quant::dequantize_q6_k_row(row_data, out),
DType::Q8_0 => crate::quant::dequantize_q8_0_row(row_data, out),
DType::Q4_0 => crate::quant::dequantize_q4_0_row(row_data, out),
DType::Q4_1 => crate::quant::dequantize_q4_1_row(row_data, out),
DType::Q4KM => crate::quant::dequantize_q4_k_m_row(row_data, out),
DType::Q5KM => crate::quant::dequantize_q5_k_row(row_data, out),
DType::F32 => {
let floats: &[f32] = bytemuck::cast_slice(row_data);
out.copy_from_slice(floats);
}
_ => panic!("unsupported embedding dtype: {:?}", wref.dtype),
}
}
pub(crate) fn dequantize_row(gguf: &GgufFile, wref: &WeightRef, row_idx: usize) -> Vec<f32> {
let mut out = vec![0.0f32; wref.k];
dequantize_row_into(gguf, wref, row_idx, &mut out);
out
}
#[cfg(any(
feature = "gpu",
all(feature = "metal", any(target_os = "macos", target_os = "ios"))
))]
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
pub(crate) fn dequantize_weight(gguf: &GgufFile, wref: &WeightRef) -> Vec<f32> {
let mut out = vec![0.0f32; wref.m * wref.k];
for row in 0..wref.m {
let row_out = &mut out[row * wref.k..(row + 1) * wref.k];
dequantize_row_into(gguf, wref, row, row_out);
}
out
}
pub(crate) struct AttnWeights<'a> {
pub attn_q: &'a WeightRef,
pub attn_k: &'a WeightRef,
pub attn_v: &'a WeightRef,
pub attn_output: &'a WeightRef,
}
pub(crate) struct AttnExtras<'a> {
pub qkv_bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
pub qk_norm: Option<(&'a [f32], &'a [f32])>,
}
#[derive(Clone, Copy)]
pub(crate) struct AttnDims<'a> {
pub hidden_size: usize,
pub n_heads: usize,
pub n_kv_heads: usize,
pub head_dim: usize,
pub rope_theta: f32,
pub rms_norm_eps: f32,
pub rope_type: cpu::RopeType,
pub attn_scale: Option<f32>,
pub rope_freqs: Option<&'a [f32]>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn forward_attn_block(
gguf: &GgufFile,
layer: usize,
weights: &AttnWeights,
extras: &AttnExtras,
dims: AttnDims<'_>,
hidden: &[f32],
pos: usize,
state: &mut InferenceState,
) {
let head_dim = dims.head_dim;
let n_heads = dims.n_heads;
let n_kv_heads = dims.n_kv_heads;
let hidden_size = dims.hidden_size;
let kv_dim = n_kv_heads * head_dim;
let q_dim = n_heads * head_dim;
let lora = state.lora.clone();
let q = &mut state.scratch.q[..q_dim];
let k = &mut state.scratch.k[..kv_dim];
let v = &mut state.scratch.v[..kv_dim];
#[cfg(target_arch = "aarch64")]
{
gemv_preq(
gguf,
weights.attn_q,
hidden,
&state.scratch.q8_scales,
&state.scratch.q8_quants,
q,
);
gemv_preq(
gguf,
weights.attn_k,
hidden,
&state.scratch.q8_scales,
&state.scratch.q8_quants,
k,
);
gemv_preq(
gguf,
weights.attn_v,
hidden,
&state.scratch.q8_scales,
&state.scratch.q8_quants,
v,
);
}
#[cfg(not(target_arch = "aarch64"))]
{
gemv(gguf, weights.attn_q, hidden, q);
gemv(gguf, weights.attn_k, hidden, k);
gemv(gguf, weights.attn_v, hidden, v);
}
if let Some((q_bias, k_bias, v_bias)) = extras.qkv_bias {
cpu::add_inplace(q, q_bias);
cpu::add_inplace(k, k_bias);
cpu::add_inplace(v, v_bias);
}
if let Some(lora) = &lora {
crate::lora::apply_attn_qkv(lora, layer, hidden, q, k, v, &mut state.scratch.lora_tmp);
}
if let Some((q_norm, k_norm)) = extras.qk_norm {
for h in 0..n_heads {
cpu::rmsnorm(
&mut q[h * head_dim..(h + 1) * head_dim],
q_norm,
dims.rms_norm_eps,
);
}
for h in 0..n_kv_heads {
cpu::rmsnorm(
&mut k[h * head_dim..(h + 1) * head_dim],
k_norm,
dims.rms_norm_eps,
);
}
}
match dims.rope_type {
cpu::RopeType::Neox => cpu::rope(q, k, pos, n_heads, n_kv_heads, head_dim, dims.rope_theta),
cpu::RopeType::Norm => cpu::rope_norm(
q,
k,
pos,
n_heads,
n_kv_heads,
head_dim,
dims.rope_theta,
dims.rope_freqs,
),
}
if let LayerState::Attention {
key_cache,
value_cache,
..
} = &mut state.layers[layer]
{
key_cache.extend_from_slice(&state.scratch.k[..kv_dim]);
value_cache.extend_from_slice(&state.scratch.v[..kv_dim]);
}
let group_size = n_heads / n_kv_heads;
let scale = dims
.attn_scale
.unwrap_or_else(|| 1.0 / (head_dim as f32).sqrt());
{
let (k_cache, v_cache) = match &state.layers[layer] {
LayerState::Attention {
key_cache,
value_cache,
..
} => (key_cache.as_slice(), value_cache.as_slice()),
_ => panic!("expected Attention state for layer {layer}"),
};
let seq_len = k_cache.len() / kv_dim;
let attn_out = &mut state.scratch.attn_out[..q_dim];
let q = &state.scratch.q[..q_dim];
let scores = &mut state.scratch.scores;
scores.resize(seq_len, 0.0);
for h in 0..n_heads {
let kv_h = h / group_size;
let q_head = &q[h * head_dim..(h + 1) * head_dim];
let kv_h_offset = kv_h * head_dim;
cpu::attn_scores(
q_head,
k_cache,
scores,
kv_dim,
kv_h_offset,
head_dim,
scale,
seq_len,
);
cpu::softmax_inplace(scores);
cpu::attn_values(
scores,
v_cache,
&mut attn_out[h * head_dim..(h + 1) * head_dim],
kv_dim,
kv_h_offset,
head_dim,
seq_len,
);
}
}
let out = &mut state.scratch.out[..hidden_size];
gemv(
gguf,
weights.attn_output,
&state.scratch.attn_out[..q_dim],
out,
);
if let Some(lora) = &lora
&& let Some(t) = lora.get(layer, crate::lora::LoraTarget::AttnOutput)
{
crate::lora::apply_decode(
t,
&state.scratch.attn_out[..q_dim],
out,
&mut state.scratch.lora_tmp,
);
}
}
pub(crate) struct FfnWeights<'a> {
pub ffn_gate: &'a WeightRef,
pub ffn_up: &'a WeightRef,
pub ffn_down: &'a WeightRef,
}
pub(crate) fn forward_ffn_block(
gguf: &GgufFile,
layer: usize,
weights: &FfnWeights,
hidden_size: usize,
intermediate_size: usize,
ffn_input: &[f32],
state: &mut InferenceState,
) {
let lora = state.lora.clone();
#[cfg(target_arch = "aarch64")]
{
gemv_preq(
gguf,
weights.ffn_gate,
ffn_input,
&state.scratch.q8_scales,
&state.scratch.q8_quants,
&mut state.scratch.gate[..intermediate_size],
);
gemv_preq(
gguf,
weights.ffn_up,
ffn_input,
&state.scratch.q8_scales,
&state.scratch.q8_quants,
&mut state.scratch.up[..intermediate_size],
);
}
#[cfg(not(target_arch = "aarch64"))]
{
gemv(
gguf,
weights.ffn_gate,
ffn_input,
&mut state.scratch.gate[..intermediate_size],
);
gemv(
gguf,
weights.ffn_up,
ffn_input,
&mut state.scratch.up[..intermediate_size],
);
}
if let Some(lora) = &lora {
if let Some(t) = lora.get(layer, crate::lora::LoraTarget::FfnGate) {
crate::lora::apply_decode(
t,
ffn_input,
&mut state.scratch.gate[..intermediate_size],
&mut state.scratch.lora_tmp,
);
}
if let Some(t) = lora.get(layer, crate::lora::LoraTarget::FfnUp) {
crate::lora::apply_decode(
t,
ffn_input,
&mut state.scratch.up[..intermediate_size],
&mut state.scratch.lora_tmp,
);
}
}
cpu::silu_mul_inplace(
&mut state.scratch.gate[..intermediate_size],
&state.scratch.up[..intermediate_size],
);
#[cfg(target_arch = "aarch64")]
{
let nb = intermediate_size / 32;
state.scratch.q8_scales.resize(nb, 0.0);
state.scratch.q8_quants.resize(intermediate_size, 0);
unsafe {
crate::backend::simd::neon::quantize_f32_to_q8_0_neon(
&state.scratch.gate[..intermediate_size],
&mut state.scratch.q8_scales,
&mut state.scratch.q8_quants,
);
}
gemv_preq(
gguf,
weights.ffn_down,
&state.scratch.gate[..intermediate_size],
&state.scratch.q8_scales,
&state.scratch.q8_quants,
&mut state.scratch.out[..hidden_size],
);
}
#[cfg(not(target_arch = "aarch64"))]
gemv(
gguf,
weights.ffn_down,
&state.scratch.gate[..intermediate_size],
&mut state.scratch.out[..hidden_size],
);
if let Some(lora) = &lora
&& let Some(t) = lora.get(layer, crate::lora::LoraTarget::FfnDown)
{
crate::lora::apply_decode(
t,
&state.scratch.gate[..intermediate_size],
&mut state.scratch.out[..hidden_size],
&mut state.scratch.lora_tmp,
);
}
}
#[cfg(all(
test,
target_arch = "aarch64",
not(feature = "blas"),
feature = "parallel"
))]
mod tests {
use super::*;
#[test]
fn quantize_columns_parallel_matches_serial() {
let dim = 256usize;
let n = 64usize; let nb = dim / 32;
let mut st = 0x9E37_79B9_7F4A_7C15u64;
let mut lcg = || {
st = st
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((st >> 33) as f32 / (1u64 << 31) as f32) - 1.0
};
let mat: Vec<f32> = (0..dim * n).map(|_| lcg()).collect();
let mut col = vec![0.0f32; dim];
let mut scales = vec![0.0f32; n * nb];
let mut quants = vec![0i8; n * dim];
quantize_columns(&mat, dim, n, &mut col, &mut scales, &mut quants);
let mut ref_scales = vec![0.0f32; n * nb];
let mut ref_quants = vec![0i8; n * dim];
let mut rc = vec![0.0f32; dim];
for j in 0..n {
for (i, ci) in rc.iter_mut().enumerate() {
*ci = mat[i * n + j];
}
unsafe {
crate::backend::simd::neon::quantize_f32_to_q8_0_neon(
&rc,
&mut ref_scales[j * nb..(j + 1) * nb],
&mut ref_quants[j * dim..(j + 1) * dim],
);
}
}
assert_eq!(
quants, ref_quants,
"parallel quantize_columns quants differ"
);
assert_eq!(
scales, ref_scales,
"parallel quantize_columns scales differ"
);
}
}