use anyhow::{Context, Result};
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"))?;
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(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::Q8_0 => crate::quant::dequantize_q8_0_matrix(data, m, k, dequant),
_ => return false,
}
crate::backend::blas::sgemm_rowmajor_nn(m, n, k, dequant, b, out);
true
}
#[cfg(all(target_arch = "aarch64", 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];
match wref.dtype {
DType::Q4_0 => unsafe {
crate::backend::simd::neon::gemm_q4_0_q8_0_neon(data, b_scales, b_quants, out, m, n, k);
true
},
DType::Q8_0 => unsafe {
crate::backend::simd::neon::gemm_q8_0_q8_0_neon(data, b_scales, b_quants, out, m, n, k);
true
},
_ => false,
}
}
#[cfg(all(target_arch = "aarch64", 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!(
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;
for j in 0..n {
for i in 0..dim {
col[i] = mat[i * n + j];
}
unsafe {
crate::backend::simd::neon::quantize_f32_to_q8_0_neon(
&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 row_bytes = wref.k / wref.dtype.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::Q4KM => crate::quant::dequantize_q4_k_m_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,
);
}
}