use std::path::Path;
use anyhow::{Context as _, Result, ensure};
use num_traits::ToPrimitive;
use crate::engine::gguf::{Gguf, TensorType};
use crate::engine::kernels::{
dequantize_row, dim_to_f32, f32_to_fp16, fp16_to_f32, matrix_argmax, matrix_vector,
matrix_vector_triple, rms_norm, softmax, vector_add,
};
use crate::engine::tokenizer::{Tokenizer, Utf8Decoder};
const LAYERS: usize = 24;
const HIDDEN: usize = 2_880;
const QUERY_HEADS: usize = 64;
const KEY_VALUE_HEADS: usize = 8;
const HEAD_DIMENSION: usize = 64;
const QUERY_SIZE: usize = QUERY_HEADS * HEAD_DIMENSION;
const KEY_VALUE_SIZE: usize = KEY_VALUE_HEADS * HEAD_DIMENSION;
const QUERY_GROUP: usize = QUERY_HEADS / KEY_VALUE_HEADS;
const EXPERTS: usize = 32;
const ACTIVE_EXPERTS: usize = 4;
const EXPERT_HIDDEN: usize = 2_880;
const VOCABULARY: usize = 201_088;
const SLIDING_WINDOW: usize = 128;
const EPSILON: f32 = 1.0e-5;
const ROPE_BASE: f32 = 150_000.0;
const ROPE_FACTOR: f32 = 32.0;
const ROPE_ORIGINAL_CONTEXT: f32 = 4_096.0;
const ROPE_BETA_FAST: f32 = 32.0;
const ROPE_BETA_SLOW: f32 = 1.0;
const SWIGLU_ALPHA: f32 = 1.702;
const SWIGLU_LIMIT: f32 = 7.0;
pub struct Generation {
pub text: String,
}
pub struct TextModel {
gguf: Gguf,
tokenizer: Tokenizer,
layers: Vec<LayerNames>,
}
struct LayerNames {
attention_norm: String,
query: String,
query_bias: String,
key: String,
key_bias: String,
value: String,
value_bias: String,
attention_output: String,
attention_output_bias: String,
attention_sinks: String,
feed_forward_norm: String,
router: String,
router_bias: String,
gate_experts: String,
gate_experts_bias: String,
up_experts: String,
up_experts_bias: String,
down_experts: String,
down_experts_bias: String,
}
impl LayerNames {
fn new(layer: usize) -> Self {
let prefix = format!("blk.{layer}");
Self {
attention_norm: format!("{prefix}.attn_norm.weight"),
query: format!("{prefix}.attn_q.weight"),
query_bias: format!("{prefix}.attn_q.bias"),
key: format!("{prefix}.attn_k.weight"),
key_bias: format!("{prefix}.attn_k.bias"),
value: format!("{prefix}.attn_v.weight"),
value_bias: format!("{prefix}.attn_v.bias"),
attention_output: format!("{prefix}.attn_output.weight"),
attention_output_bias: format!("{prefix}.attn_output.bias"),
attention_sinks: format!("{prefix}.attn_sinks.weight"),
feed_forward_norm: format!("{prefix}.post_attention_norm.weight"),
router: format!("{prefix}.ffn_gate_inp.weight"),
router_bias: format!("{prefix}.ffn_gate_inp.bias"),
gate_experts: format!("{prefix}.ffn_gate_exps.weight"),
gate_experts_bias: format!("{prefix}.ffn_gate_exps.bias"),
up_experts: format!("{prefix}.ffn_up_exps.weight"),
up_experts_bias: format!("{prefix}.ffn_up_exps.bias"),
down_experts: format!("{prefix}.ffn_down_exps.weight"),
down_experts_bias: format!("{prefix}.ffn_down_exps.bias"),
}
}
}
impl TextModel {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let gguf = Gguf::load(path).context("load GPT-OSS-20B GGUF")?;
validate_metadata(&gguf).context("validate GPT-OSS-20B metadata")?;
validate_tensors(&gguf).context("validate GPT-OSS-20B tensors")?;
let tokenizer = Tokenizer::from_gpt_oss(&gguf).context("load GPT-OSS tokenizer")?;
Ok(Self {
gguf,
tokenizer,
layers: (0..LAYERS).map(LayerNames::new).collect(),
})
}
pub fn generate(
&self,
prompt: &str,
max_new_tokens: usize,
max_context_tokens: usize,
) -> Result<Generation> {
ensure!(max_context_tokens != 0, "context limit must not be zero");
let prompt_ids = self
.tokenizer
.encode(prompt, true)
.context("tokenize prompt")?;
ensure!(
!prompt_ids.is_empty(),
"prompt must produce at least one token"
);
let requested = prompt_ids
.len()
.checked_add(max_new_tokens)
.context("requested token count overflow")?;
ensure!(
requested <= max_context_tokens,
"prompt ({}) plus generation ({max_new_tokens}) exceeds context limit {max_context_tokens}",
prompt_ids.len()
);
let mut cache = KvCache::new(max_context_tokens)?;
let mut scratch = AttentionScratch::new(max_context_tokens)?;
let mut hidden = None;
for (position, token) in prompt_ids.iter().copied().enumerate() {
let embedding = self.embedding(token)?;
hidden = Some(self.forward(&embedding, position, &mut cache, &mut scratch)?);
}
if max_new_tokens == 0 {
return Ok(Generation {
text: String::new(),
});
}
let mut token = self.greedy_token(hidden.as_deref().expect("nonempty prompt"))?;
let mut decoder = Utf8Decoder::default();
let mut text = String::new();
let mut generated_tokens = 0;
while generated_tokens < max_new_tokens && !self.tokenizer.is_eos(token) {
generated_tokens += 1;
if let Some(piece) = decoder.push(self.tokenizer.piece(token)?)? {
text.push_str(&piece);
}
if generated_tokens == max_new_tokens {
break;
}
let position = prompt_ids.len() + generated_tokens - 1;
let embedding = self.embedding(token)?;
let hidden = self.forward(&embedding, position, &mut cache, &mut scratch)?;
token = self.greedy_token(&hidden)?;
}
text.push_str(&decoder.finish()?);
Ok(Generation { text })
}
fn embedding(&self, token: u32) -> Result<Vec<f32>> {
let tensor = self.gguf.tensor("token_embd.weight")?;
let mut embedding = vec![0.0; HIDDEN];
dequantize_row(tensor.q8_row(usize::try_from(token)?)?, &mut embedding)?;
Ok(embedding)
}
fn forward(
&self,
input: &[f32],
position: usize,
cache: &mut KvCache,
scratch: &mut AttentionScratch,
) -> Result<Vec<f32>> {
ensure!(input.len() == HIDDEN, "decoder input width differs");
let rope = Rope::new(position);
let mut hidden = input.to_vec();
for (layer, names) in self.layers.iter().enumerate() {
let normalized = rms_norm(
&hidden,
HIDDEN,
self.gguf.tensor(&names.attention_norm)?.f32_slice()?,
EPSILON,
)?;
let (mut query, mut key, mut value) = matrix_vector_triple(
&self.gguf.tensor(&names.query)?,
&self.gguf.tensor(&names.key)?,
&self.gguf.tensor(&names.value)?,
&normalized,
)?;
add_bias(
&mut query,
self.gguf.tensor(&names.query_bias)?.f32_slice()?,
)?;
add_bias(&mut key, self.gguf.tensor(&names.key_bias)?.f32_slice()?)?;
add_bias(
&mut value,
self.gguf.tensor(&names.value_bias)?.f32_slice()?,
)?;
apply_rope(&mut query, &rope)?;
apply_rope(&mut key, &rope)?;
cache.layers[layer].append(&key, &value)?;
let sliding = layer % 2 == 0;
causal_gqa(
&query,
&cache.layers[layer],
self.gguf.tensor(&names.attention_sinks)?.f32_slice()?,
sliding,
scratch,
)?;
let mut projected =
matrix_vector(&self.gguf.tensor(&names.attention_output)?, &scratch.output)?;
add_bias(
&mut projected,
self.gguf
.tensor(&names.attention_output_bias)?
.f32_slice()?,
)?;
vector_add(&mut hidden, &projected)?;
let normalized = rms_norm(
&hidden,
HIDDEN,
self.gguf.tensor(&names.feed_forward_norm)?.f32_slice()?,
EPSILON,
)?;
let mixture = self.mixture_of_experts(&normalized, names)?;
vector_add(&mut hidden, &mixture)?;
}
rms_norm(
&hidden,
HIDDEN,
self.gguf.tensor("output_norm.weight")?.f32_slice()?,
EPSILON,
)
}
fn mixture_of_experts(&self, input: &[f32], names: &LayerNames) -> Result<Vec<f32>> {
let mut router = matrix_vector(&self.gguf.tensor(&names.router)?, input)?;
add_bias(
&mut router,
self.gguf.tensor(&names.router_bias)?.f32_slice()?,
)?;
let (indices, mut weights) = top_experts(&router);
softmax(&mut weights);
let gate_weights = self.gguf.tensor(&names.gate_experts)?;
let up_weights = self.gguf.tensor(&names.up_experts)?;
let down_weights = self.gguf.tensor(&names.down_experts)?;
let gate_biases = self.gguf.tensor(&names.gate_experts_bias)?;
let up_biases = self.gguf.tensor(&names.up_experts_bias)?;
let down_biases = self.gguf.tensor(&names.down_experts_bias)?;
let mut mixture = vec![0.0; HIDDEN];
for (expert, routing_weight) in indices.into_iter().zip(weights) {
let mut gate = matrix_vector(&gate_weights.matrix_slice(expert)?, input)?;
let mut up = matrix_vector(&up_weights.matrix_slice(expert)?, input)?;
add_bias(&mut gate, gate_biases.f32_row(expert)?)?;
add_bias(&mut up, up_biases.f32_row(expert)?)?;
for (gate, up) in gate.iter_mut().zip(&mut up) {
*gate = gate.min(SWIGLU_LIMIT);
*up = up.clamp(-SWIGLU_LIMIT, SWIGLU_LIMIT);
let glu = *gate / (1.0 + (-SWIGLU_ALPHA * *gate).exp());
*gate = (*up + 1.0) * glu;
}
let mut output = matrix_vector(&down_weights.matrix_slice(expert)?, &gate)?;
add_bias(&mut output, down_biases.f32_row(expert)?)?;
for (mixed, value) in mixture.iter_mut().zip(output) {
*mixed += routing_weight * value;
}
}
Ok(mixture)
}
fn greedy_token(&self, hidden: &[f32]) -> Result<u32> {
let output = self.gguf.tensor("output.weight")?;
let token = matrix_argmax(&output, hidden).context("compute greedy token")?;
u32::try_from(token).context("token ID exceeds u32")
}
}
fn add_bias(values: &mut [f32], bias: &[f32]) -> Result<()> {
ensure!(values.len() == bias.len(), "bias width differs");
values
.iter_mut()
.zip(bias)
.for_each(|(value, bias)| *value += bias);
Ok(())
}
fn top_experts(logits: &[f32]) -> ([usize; ACTIVE_EXPERTS], [f32; ACTIVE_EXPERTS]) {
let mut indices = [0; ACTIVE_EXPERTS];
let mut values = [f32::NEG_INFINITY; ACTIVE_EXPERTS];
for (index, value) in logits.iter().copied().enumerate() {
let position = values
.iter()
.position(|selected| value > *selected)
.unwrap_or(ACTIVE_EXPERTS);
if position < ACTIVE_EXPERTS {
values[position..].rotate_right(1);
indices[position..].rotate_right(1);
values[position] = value;
indices[position] = index;
}
}
(indices, values)
}
struct Rope {
cosine: [f32; HEAD_DIMENSION / 2],
sine: [f32; HEAD_DIMENSION / 2],
}
impl Rope {
fn new(position: usize) -> Self {
let attention_factor = 0.1 * ROPE_FACTOR.ln() + 1.0;
let low = correction_dimension(ROPE_BETA_FAST).max(0.0);
let high = correction_dimension(ROPE_BETA_SLOW).min(dim_to_f32(HEAD_DIMENSION - 1));
let mut rope = Self {
cosine: [0.0; HEAD_DIMENSION / 2],
sine: [0.0; HEAD_DIMENSION / 2],
};
for pair in 0..HEAD_DIMENSION / 2 {
let base_frequency =
ROPE_BASE.powf(-(dim_to_f32(2 * pair) / dim_to_f32(HEAD_DIMENSION)));
let ramp = ((dim_to_f32(pair) - low) / (high - low)).clamp(0.0, 1.0);
let frequency = base_frequency * (1.0 - ramp + ramp / ROPE_FACTOR);
let angle = position.to_f32().expect("usize maps to a finite f32") * frequency;
rope.cosine[pair] = angle.cos() * attention_factor;
rope.sine[pair] = angle.sin() * attention_factor;
}
rope
}
}
fn correction_dimension(rotations: f32) -> f32 {
dim_to_f32(HEAD_DIMENSION)
* (ROPE_ORIGINAL_CONTEXT / (rotations * 2.0 * std::f32::consts::PI)).ln()
/ (2.0 * ROPE_BASE.ln())
}
fn apply_rope(values: &mut [f32], rope: &Rope) -> Result<()> {
ensure!(
values.len().is_multiple_of(HEAD_DIMENSION),
"RoPE input width differs"
);
for head in values.chunks_exact_mut(HEAD_DIMENSION) {
let (first, second) = head.split_at_mut(HEAD_DIMENSION / 2);
for pair in 0..HEAD_DIMENSION / 2 {
let left = first[pair];
let right = second[pair];
first[pair] = left * rope.cosine[pair] - right * rope.sine[pair];
second[pair] = left * rope.sine[pair] + right * rope.cosine[pair];
}
}
Ok(())
}
#[derive(Default)]
struct LayerCache {
keys: Vec<u16>,
values: Vec<u16>,
}
impl LayerCache {
fn reserve(&mut self, tokens: usize) -> Result<()> {
let values = tokens
.checked_mul(KEY_VALUE_SIZE)
.context("key/value cache size overflow")?;
self.keys
.try_reserve_exact(values)
.context("reserve key cache")?;
self.values
.try_reserve_exact(values)
.context("reserve value cache")?;
Ok(())
}
fn append(&mut self, key: &[f32], value: &[f32]) -> Result<()> {
ensure!(
key.len() == KEY_VALUE_SIZE && value.len() == KEY_VALUE_SIZE,
"key/value width differs"
);
self.keys.extend(key.iter().copied().map(f32_to_fp16));
self.values.extend(value.iter().copied().map(f32_to_fp16));
Ok(())
}
fn token_count(&self) -> usize {
self.keys.len() / KEY_VALUE_SIZE
}
}
struct KvCache {
layers: Vec<LayerCache>,
}
impl KvCache {
fn new(tokens: usize) -> Result<Self> {
let mut layers = (0..LAYERS)
.map(|_| LayerCache::default())
.collect::<Vec<_>>();
for layer in &mut layers {
layer.reserve(tokens)?;
}
Ok(Self { layers })
}
}
#[derive(Default)]
struct AttentionScratch {
output: Vec<f32>,
scores: Vec<f32>,
}
impl AttentionScratch {
fn new(tokens: usize) -> Result<Self> {
let score_capacity = QUERY_HEADS
.checked_mul(tokens + 1)
.context("attention workspace overflow")?;
let mut scores = Vec::new();
scores
.try_reserve_exact(score_capacity)
.context("reserve attention workspace")?;
Ok(Self {
output: vec![0.0; QUERY_SIZE],
scores,
})
}
}
fn causal_gqa(
query: &[f32],
cache: &LayerCache,
sinks: &[f32],
sliding: bool,
scratch: &mut AttentionScratch,
) -> Result<()> {
ensure!(query.len() == QUERY_SIZE, "attention query width differs");
ensure!(sinks.len() == QUERY_HEADS, "attention sink count differs");
ensure!(
cache.keys.len() == cache.values.len(),
"key/value cache lengths differ"
);
let tokens = cache.token_count();
ensure!(tokens != 0, "attention cache is empty");
let first_token = if sliding {
tokens.saturating_sub(SLIDING_WINDOW)
} else {
0
};
let visible_tokens = tokens - first_token;
scratch.output.fill(0.0);
scratch
.scores
.resize(QUERY_HEADS * (visible_tokens + 1), 0.0);
let scale = dim_to_f32(HEAD_DIMENSION).sqrt().recip();
for query_head in 0..QUERY_HEADS {
let key_value_head = query_head / QUERY_GROUP;
let query_values = &query[query_head * HEAD_DIMENSION..(query_head + 1) * HEAD_DIMENSION];
let score_width = visible_tokens + 1;
let weights = &mut scratch.scores[query_head * score_width..(query_head + 1) * score_width];
for (offset, weight) in weights[..visible_tokens].iter_mut().enumerate() {
let token = first_token + offset;
let start = token * KEY_VALUE_SIZE + key_value_head * HEAD_DIMENSION;
*weight = query_values
.iter()
.zip(&cache.keys[start..start + HEAD_DIMENSION])
.map(|(query, key)| query * fp16_to_f32(*key))
.sum::<f32>()
* scale;
}
weights[visible_tokens] = sinks[query_head];
softmax(weights);
let output =
&mut scratch.output[query_head * HEAD_DIMENSION..(query_head + 1) * HEAD_DIMENSION];
for (offset, weight) in weights[..visible_tokens].iter().copied().enumerate() {
let token = first_token + offset;
let start = token * KEY_VALUE_SIZE + key_value_head * HEAD_DIMENSION;
for (channel, value) in output.iter_mut().enumerate() {
*value += weight * fp16_to_f32(cache.values[start + channel]);
}
}
}
ensure!(
scratch.output.iter().all(|value| value.is_finite()),
"attention output is not finite"
);
Ok(())
}
fn validate_metadata(gguf: &Gguf) -> Result<()> {
ensure!(
gguf.architecture() == "gpt-oss",
"expected gpt-oss architecture"
);
validate_u32(gguf, "gpt-oss.block_count", LAYERS)?;
validate_u32(gguf, "gpt-oss.embedding_length", HIDDEN)?;
validate_u32(gguf, "gpt-oss.feed_forward_length", EXPERT_HIDDEN)?;
validate_u32(gguf, "gpt-oss.attention.head_count", QUERY_HEADS)?;
validate_u32(gguf, "gpt-oss.attention.head_count_kv", KEY_VALUE_HEADS)?;
validate_u32(gguf, "gpt-oss.attention.key_length", HEAD_DIMENSION)?;
validate_u32(gguf, "gpt-oss.attention.value_length", HEAD_DIMENSION)?;
validate_u32(gguf, "gpt-oss.attention.sliding_window", SLIDING_WINDOW)?;
validate_u32(gguf, "gpt-oss.expert_count", EXPERTS)?;
validate_u32(gguf, "gpt-oss.expert_used_count", ACTIVE_EXPERTS)?;
validate_u32(gguf, "gpt-oss.expert_feed_forward_length", EXPERT_HIDDEN)?;
let epsilon = gguf.f32("gpt-oss.attention.layer_norm_rms_epsilon")?;
ensure!(
epsilon.to_bits() == EPSILON.to_bits(),
"RMS epsilon is {epsilon}, expected {EPSILON}"
);
let rope_base = gguf.f32("gpt-oss.rope.freq_base")?;
ensure!(
rope_base.to_bits() == ROPE_BASE.to_bits(),
"RoPE base is {rope_base}, expected {ROPE_BASE}"
);
ensure!(
gguf.strings("tokenizer.ggml.tokens")?.len() == VOCABULARY,
"vocabulary size differs"
);
ensure!(
gguf.string("tokenizer.ggml.model")? == "gpt2",
"expected GPT-2 tokenizer model"
);
ensure!(
gguf.string("tokenizer.ggml.pre")? == "gpt-4o",
"expected GPT-4o pre-tokenizer"
);
Ok(())
}
fn validate_u32(gguf: &Gguf, key: &str, expected: usize) -> Result<()> {
let value = gguf.u32(key)?;
ensure!(
value as usize == expected,
"{key} is {value}, expected {expected}"
);
Ok(())
}
fn validate_tensors(gguf: &Gguf) -> Result<()> {
validate_kind(
gguf,
"token_embd.weight",
&[HIDDEN, VOCABULARY],
TensorType::Q8_0,
)?;
validate_kind(gguf, "output_norm.weight", &[HIDDEN], TensorType::F32)?;
validate_kind(
gguf,
"output.weight",
&[HIDDEN, VOCABULARY],
TensorType::Q8_0,
)?;
for layer in 0..LAYERS {
let names = LayerNames::new(layer);
validate_kind(gguf, &names.attention_norm, &[HIDDEN], TensorType::F32)?;
validate_kind(gguf, &names.query, &[HIDDEN, QUERY_SIZE], TensorType::Q8_0)?;
validate_kind(gguf, &names.query_bias, &[QUERY_SIZE], TensorType::F32)?;
validate_kind(
gguf,
&names.key,
&[HIDDEN, KEY_VALUE_SIZE],
TensorType::Q8_0,
)?;
validate_kind(gguf, &names.key_bias, &[KEY_VALUE_SIZE], TensorType::F32)?;
validate_kind(
gguf,
&names.value,
&[HIDDEN, KEY_VALUE_SIZE],
TensorType::Q8_0,
)?;
validate_kind(gguf, &names.value_bias, &[KEY_VALUE_SIZE], TensorType::F32)?;
validate_kind(
gguf,
&names.attention_output,
&[QUERY_SIZE, HIDDEN],
TensorType::Q8_0,
)?;
validate_kind(
gguf,
&names.attention_output_bias,
&[HIDDEN],
TensorType::F32,
)?;
validate_kind(
gguf,
&names.attention_sinks,
&[QUERY_HEADS],
TensorType::F32,
)?;
validate_kind(gguf, &names.feed_forward_norm, &[HIDDEN], TensorType::F32)?;
validate_kind(gguf, &names.router, &[HIDDEN, EXPERTS], TensorType::F32)?;
validate_kind(gguf, &names.router_bias, &[EXPERTS], TensorType::F32)?;
for (weight, bias) in [
(&names.gate_experts, &names.gate_experts_bias),
(&names.up_experts, &names.up_experts_bias),
(&names.down_experts, &names.down_experts_bias),
] {
validate_kind(
gguf,
weight,
&[EXPERT_HIDDEN, HIDDEN, EXPERTS],
TensorType::Mxfp4,
)?;
validate_kind(gguf, bias, &[EXPERT_HIDDEN, EXPERTS], TensorType::F32)?;
}
}
Ok(())
}
fn validate_kind(gguf: &Gguf, name: &str, dimensions: &[usize], kind: TensorType) -> Result<()> {
let tensor = gguf.tensor(name)?;
ensure!(
tensor.dimensions() == dimensions,
"tensor `{name}` has dimensions {:?}, expected {dimensions:?}",
tensor.dimensions()
);
ensure!(
tensor.tensor_type() == kind,
"tensor `{name}` is {:?}, expected {kind:?}",
tensor.tensor_type()
);
Ok(())
}