//! CPU reference implementation of Falcon-H1 inference mathematics, not a CUDA backend. //! Checkpoint tensors are loaded from the simple FH1W/f32 pack produced by tools/export.py. //! Intended as a readable, testable baseline; not optimized for 0.5B CPU throughput. use std::collections::HashMap; use std::fs::File; use std::io::{self, Read}; #[derive(Clone, Debug)] pub struct Config { pub vocab: usize, pub hidden: usize, pub layers: usize, pub heads: usize, pub kv_heads: usize, pub head_dim: usize, pub mamba_heads: usize, pub mamba_dim: usize, pub state: usize, pub groups: usize, pub conv: usize, pub ff: usize, pub rope_theta: f32, pub eps: f32, pub embedding_multiplier: f32, pub key_multiplier: f32, pub attention_in_multiplier: f32, pub attention_out_multiplier: f32, pub ssm_in_multiplier: f32, pub ssm_out_multiplier: f32, pub ssm_multipliers: [f32;5], pub mlp_multipliers: [f32;2], pub lm_head_multiplier: f32, } impl Config { pub fn falcon_h1_05b() -> Self { Self { vocab:32784, hidden:1024, layers:36, heads:8, kv_heads:2, head_dim:64, mamba_heads:24, mamba_dim:1536, state:128, groups:1, conv:4, ff:2048, rope_theta:1e11, eps:1e-5, embedding_multiplier:5.656854249492381, key_multiplier:0.390625, attention_in_multiplier:1., attention_out_multiplier:0.9375, ssm_in_multiplier:1.25, ssm_out_multiplier:0.23570226039551587, ssm_multipliers:[0.3535533905932738,0.25,0.3535533905932738,0.5,0.3535533905932738], mlp_multipliers:[0.8838834764831844,0.5859375], lm_head_multiplier:0.0390625, }} pub fn demo() -> Self { Self { vocab:64, hidden:32, layers:2, heads:4, kv_heads:2, head_dim:8, mamba_heads:4, mamba_dim:32, state:8, groups:1, conv:4, ff:64, rope_theta:1e11, eps:1e-5, embedding_multiplier:1., key_multiplier:1., attention_in_multiplier:1., attention_out_multiplier:1., ssm_in_multiplier:1., ssm_out_multiplier:1., ssm_multipliers:[1.;5], mlp_multipliers:[1.;2], lm_head_multiplier:1., }} pub fn validate(&self) -> Result<(), String> { if self.heads==0 || self.kv_heads==0 || self.mamba_heads==0 || self.groups==0 || self.conv==0 || self.hidden==0 || self.vocab==0 || self.state==0 { return Err("zero dimension".into()) } if self.heads % self.kv_heads != 0 || self.head_dim % 2 != 0 { return Err("invalid GQA/RoPE shape".into()) } if self.mamba_dim % self.mamba_heads != 0 || self.mamba_heads % self.groups != 0 { return Err("invalid Mamba head/group shape".into()) } // Falcon-H1 attention is narrower than the hidden state: 8*64 != 1024 for 0.5B. if self.head_dim==0 || self.mamba_dim==0 || self.ff==0 { return Err("zero projection dimension".into()) } Ok(()) } } #[derive(Clone, Debug)] pub struct Tensor { pub shape: Vec, pub data: Vec } impl Tensor { fn len(&self) -> usize { self.data.len() } } pub type Weights = HashMap; fn bad(e: &str) -> io::Error { io::Error::new(io::ErrorKind::InvalidData,e) } fn take_u32(r:&mut impl Read) -> io::Result { let mut a=[0u8;4]; r.read_exact(&mut a)?; Ok(u32::from_le_bytes(a)) } /// f32 pack: magic FH1W, u32 count, then each tensor: u16 name_len, name, /// u8 ndim, u32 dims[ndim], little endian f32 values. No pickle/unsafe code. pub fn load_pack(path:&str)->io::Result{ let mut f=File::open(path)?; let mut magic=[0u8;4]; f.read_exact(&mut magic)?; if &magic!=b"FH1W" { return Err(bad("not a FH1W checkpoint")) } let count=take_u32(&mut f)? as usize; if count>50000 { return Err(bad("unreasonable tensor count")) } let mut weights=HashMap::new(); for _ in 0..count { let mut two=[0u8;2]; f.read_exact(&mut two)?;let n=u16::from_le_bytes(two) as usize; if n==0 || n>1024 { return Err(bad("invalid name length")) } let mut name=vec![0u8;n];f.read_exact(&mut name)?; let name=String::from_utf8(name).map_err(|_|bad("invalid utf8"))?; let mut one=[0u8];f.read_exact(&mut one)?;let d=one[0] as usize; if d==0 || d>4 { return Err(bad("invalid rank")) } let mut shape=vec![];let mut len=1usize; for _ in 0..d {let size=take_u32(&mut f)? as usize; len=len.checked_mul(size).ok_or_else(||bad("tensor overflow"))?; shape.push(size); } if len>1_000_000_000 {return Err(bad("unreasonable tensor size"))} let mut bytes=vec![0u8;len.checked_mul(4).ok_or_else(||bad("size overflow"))?]; f.read_exact(&mut bytes)?; let data=bytes.chunks_exact(4).map(|v|f32::from_le_bytes([v[0],v[1],v[2],v[3]])).collect(); if weights.insert(name,Tensor{shape,data}).is_some(){return Err(bad("duplicate tensor"))} } Ok(weights) } pub fn expected_shapes(c:&Config)->HashMap>{ let mut m=HashMap::new(); m.insert("model.embed_tokens.weight".into(),vec![c.vocab,c.hidden]); m.insert("model.final_layernorm.weight".into(),vec![c.hidden]); m.insert("lm_head.weight".into(),vec![c.vocab,c.hidden]); let cd=c.mamba_dim+2*c.groups*c.state; for l in 0..c.layers { let p=format!("model.layers.{l}."); for n in ["input_layernorm.weight","pre_ff_layernorm.weight"] {m.insert(format!("{p}{n}"),vec![c.hidden]);} for (n,r,k) in [ ("self_attn.q_proj.weight",c.heads*c.head_dim,c.hidden), ("self_attn.k_proj.weight",c.kv_heads*c.head_dim,c.hidden), ("self_attn.v_proj.weight",c.kv_heads*c.head_dim,c.hidden), ("self_attn.o_proj.weight",c.hidden,c.heads*c.head_dim), ("feed_forward.gate_proj.weight",c.ff,c.hidden), ("feed_forward.up_proj.weight",c.ff,c.hidden), ("feed_forward.down_proj.weight",c.hidden,c.ff), ("mamba.in_proj.weight",c.mamba_dim+cd+c.mamba_heads,c.hidden), ("mamba.out_proj.weight",c.hidden,c.mamba_dim), ] {m.insert(format!("{p}{n}"),vec![r,k]);} for n in ["mamba.dt_bias","mamba.A_log","mamba.D"]{m.insert(format!("{p}{n}"),vec![c.mamba_heads]);} m.insert(format!("{p}mamba.conv1d.weight"),vec![cd,1,c.conv]); m.insert(format!("{p}mamba.conv1d.bias"),vec![cd]); } m } pub fn validate_weights(c:&Config,w:&Weights)->Result<(),String>{ c.validate()?; for (k,s) in expected_shapes(c) { let t=w.get(&k).ok_or_else(||format!("missing tensor {k}"))?; if t.shape!=s { return Err(format!("bad shape for {k}: expected {s:?}, got {:?}",t.shape)) } if t.shape.iter().product::()!=t.len(){return Err(format!("bad data length for {k}"))} } Ok(()) } fn silu(x:f32)->f32 { x/(1.+(-x).exp()) } fn softplus(x:f32)->f32 { if x>20. {x} else {(1.+x.exp()).ln()} } fn linear(x:&[f32],w:&Tensor)->Vec{ let (rows,cols)=(w.shape[0],w.shape[1]);debug_assert_eq!(cols,x.len()); let mut y=vec![0.;rows]; for i in 0..rows {let mut v=0f32;for j in 0..cols{v+=w.data[i*cols+j]*x[j];}y[i]=v;} y } fn norm(x:&[f32],weight:&Tensor,eps:f32)->Vec{ let rms=(x.iter().map(|v|v*v).sum::()/(x.len() as f32)+eps).sqrt(); x.iter().zip(&weight.data).map(|(a,b)|a/rms*b).collect() } fn rope(x:&mut [f32],pos:usize,theta:f32){ let half=x.len()/2; for i in 0..half { let angle=(pos as f32)/theta.powf(2.*i as f32/x.len() as f32); let (s,c)=angle.sin_cos();let (a,b)=(x[i],x[i+half]); x[i]=a*c-b*s; x[i+half]=b*c+a*s; } } fn add_assign(x:&mut[f32],y:&[f32]) {for (a,b) in x.iter_mut().zip(y){*a+=b}} /// Reference full-prefix forward. Each layer runs GQA and Mamba-2 from the SAME /// RMS-normalized activations, then SwiGLU after a second RMSNorm. /// No KV/SSM caching yet; per-request inference recomputes the prefix. pub struct Model {pub config:Config, pub weights:Weights} impl Model { pub fn new(config:Config,weights:Weights)->Result{validate_weights(&config,&weights)?;Ok(Self{config,weights})} fn t(&self,key:&str)->&Tensor {&self.weights[key]} pub fn logits(&self,ids:&[usize])->Result,String>{ let c=&self.config; if ids.is_empty(){return Err("input_ids must not be empty".into())} if ids.len()>16384{return Err("sequence too long".into())} for &i in ids {if i>=c.vocab{return Err(format!("token {i} outside vocabulary"))}} let embeddings=self.t("model.embed_tokens.weight"); let mut x:Vec>=ids.iter().map(|id| embeddings.data[id*c.hidden..(id+1)*c.hidden].iter().map(|v|v*c.embedding_multiplier).collect()).collect(); let n=x.len();let s=c.state;let md=c.mamba_dim;let hd=md/c.mamba_heads; let cd=md+2*c.groups*s; for layer in 0..c.layers { let p=format!("model.layers.{layer}."); let normalized:Vec>=x.iter().map(|a|norm(a,self.t(&format!("{p}input_layernorm.weight")),c.eps)).collect(); // Mamba's causal depthwise conv and selective SSM scan. let mut conv_history=vec![vec![0f32;c.conv];cd]; // [head, head_dim, state] let mut recurrent=vec![0f32;c.mamba_heads*hd*s]; let mut mamba_out=vec![vec![0f32;c.hidden];n]; let in_proj=self.t(&format!("{p}mamba.in_proj.weight")); let out_proj=self.t(&format!("{p}mamba.out_proj.weight")); let cw=self.t(&format!("{p}mamba.conv1d.weight")); let cb=self.t(&format!("{p}mamba.conv1d.bias")); let a_log=self.t(&format!("{p}mamba.A_log")); let dt_bias=self.t(&format!("{p}mamba.dt_bias")); let d_skip=self.t(&format!("{p}mamba.D")); let m=c.ssm_multipliers; for time in 0..n { let input:Vec=normalized[time].iter().map(|v|v*c.ssm_in_multiplier).collect(); let mut projected=linear(&input,in_proj); for (i,v) in projected.iter_mut().enumerate(){ let scale=if inewest. for k in 0..c.conv{v+=cw.data[channel*c.conv+k]*conv_history[channel][k]} conv_output[channel]=silu(v); } let xx=&conv_output[..md]; let b=&conv_output[md..md+c.groups*s]; let cc=&conv_output[md+c.groups*s..]; let mut output=vec![0f32;md]; for h in 0..c.mamba_heads { let group=h/(c.mamba_heads/c.groups); let step=softplus(dt[h]+dt_bias.data[h]); let decay=(-a_log.data[h].exp()*step).exp(); for k in 0..hd { let feature=h*hd+k; let u=xx[feature]; let mut y=d_skip.data[h]*u; for st in 0..s { let i=feature*s+st; recurrent[i]=decay*recurrent[i]+step*b[group*s+st]*u; y+=cc[group*s+st]*recurrent[i]; } output[feature]=y*silu(gate[feature]); } } mamba_out[time]=linear(&output,out_proj).iter().map(|v|v*c.ssm_out_multiplier).collect(); } // GQA: full-prefix causal softmax, per-query RoPE and K multiplier. let mut q=vec![vec![0f32;c.heads*c.head_dim];n]; let mut k=vec![vec![0f32;c.kv_heads*c.head_dim];n]; let mut v=vec![vec![0f32;c.kv_heads*c.head_dim];n]; for t in 0..n { let a:Vec=normalized[t].iter().map(|x|x*c.attention_in_multiplier).collect(); q[t]=linear(&a,self.t(&format!("{p}self_attn.q_proj.weight"))); k[t]=linear(&a,self.t(&format!("{p}self_attn.k_proj.weight"))); v[t]=linear(&a,self.t(&format!("{p}self_attn.v_proj.weight"))); for head in 0..c.heads {rope(&mut q[t][head*c.head_dim..(head+1)*c.head_dim],t,c.rope_theta)} for head in 0..c.kv_heads { let chunk=&mut k[t][head*c.head_dim..(head+1)*c.head_dim]; for x in chunk.iter_mut(){*x*=c.key_multiplier;} rope(chunk,t,c.rope_theta); } } for t in 0..n { let mut y=vec![0f32;c.heads*c.head_dim]; for h in 0..c.heads { let kv=h/(c.heads/c.kv_heads); let qi=&q[t][h*c.head_dim..(h+1)*c.head_dim]; let mut scores=Vec::with_capacity(t+1); for j in 0..=t { let kj=&k[j][kv*c.head_dim..(kv+1)*c.head_dim]; scores.push(qi.iter().zip(kj).map(|(a,b)|a*b).sum::()/(c.head_dim as f32).sqrt()); } let mx=scores.iter().copied().fold(f32::NEG_INFINITY,f32::max); let exps:Vec=scores.iter().map(|z|(z-mx).exp()).collect(); let denom=exps.iter().sum::(); for j in 0..=t { let vv=&v[j][kv*c.head_dim..(kv+1)*c.head_dim]; for d in 0..c.head_dim{y[h*c.head_dim+d]+=exps[j]/denom*vv[d];} } } let attn=linear(&y,self.t(&format!("{p}self_attn.o_proj.weight"))); for i in 0..c.hidden {x[t][i]+=mamba_out[t][i]+c.attention_out_multiplier*attn[i];} } // pre-FF RMSNorm + SwiGLU with Falcon-H1 µP multipliers for t in 0..n { let a=norm(&x[t],self.t(&format!("{p}pre_ff_layernorm.weight")),c.eps); let up=linear(&a,self.t(&format!("{p}feed_forward.up_proj.weight"))); let gate=linear(&a,self.t(&format!("{p}feed_forward.gate_proj.weight"))); let z:Vec=up.iter().zip(&gate).map(|(u,g)|u*silu(g*c.mlp_multipliers[0])).collect(); let ff=linear(&z,self.t(&format!("{p}feed_forward.down_proj.weight"))); for i in 0..c.hidden{x[t][i]+=ff[i]*c.mlp_multipliers[1]} } } let final_h=norm(x.last().unwrap(),self.t("model.final_layernorm.weight"),c.eps); Ok(linear(&final_h,self.t("lm_head.weight")).iter().map(|z|z*c.lm_head_multiplier).collect()) } pub fn generate(&self,ids:&[usize],count:usize)->Result,String>{ if count>64 {return Err("max_new_tokens limited to 64 in CPU reference".into())} let mut sequence=ids.to_vec(); for _ in 0..count { let y=self.logits(&sequence)?; let next=y.iter().enumerate().max_by(|a,b|a.1.total_cmp(b.1)).unwrap().0; sequence.push(next); } Ok(sequence) } } /// Deterministic tiny tensors for API unit tests. Not a pretrained model. pub fn synthetic_weights(c:&Config)->Weights{ let mut w=HashMap::new();let mut seed:u64=0x9e3779b97f4a7c15; for (name,shape) in expected_shapes(c){ let mut data=vec![0f32;shape.iter().product()]; for v in &mut data {seed ^= seed>>12;seed ^= seed<<25;seed ^= seed>>27; let z=(seed.wrapping_mul(2685821657736338717)>>40) as f32/(1u32<<24) as f32; *v=(z-0.5)*0.08;} if name.ends_with("layernorm.weight") || name.ends_with("final_layernorm.weight") { data.fill(1.); } if name.ends_with("mamba.D") {data.fill(1.);} if name.ends_with("mamba.dt_bias") {data.fill(0.);} if name.ends_with("mamba.A_log") {for (i,x) in data.iter_mut().enumerate(){*x=((i+1) as f32).ln()}} w.insert(name,Tensor{shape,data}); } w } #[cfg(test)] mod tests { use super::*; #[test] fn test_falcon_dimensions(){assert!(Config::falcon_h1_05b().validate().is_ok());} #[test] fn test_cpu_logits(){let c=Config::demo();let m=Model::new(c.clone(),synthetic_weights(&c)).unwrap();let y=m.logits(&[1,2,3]).unwrap();assert_eq!(y.len(),c.vocab);assert!(y.iter().all(|x|x.is_finite()));} #[test] fn test_causal_prefix(){let c=Config::demo();let m=Model::new(c.clone(),synthetic_weights(&c)).unwrap(); let a=m.logits(&[1,2]).unwrap();let b=m.logits(&[0,1,2]).unwrap(); assert_eq!(a.len(),b.len()); // shifted positions legitimately alter RoPE assert!(m.logits(&[1,2,3]).is_ok());} #[test] fn test_determinism(){let c=Config::demo();let m=Model::new(c.clone(),synthetic_weights(&c)).unwrap(); assert_eq!(m.logits(&[3,5,7]).unwrap(),m.logits(&[3,5,7]).unwrap());} #[test] fn test_generate(){let c=Config::demo();let m=Model::new(c.clone(),synthetic_weights(&c)).unwrap(); assert_eq!(m.generate(&[1,2],2).unwrap().len(),4);} #[test] fn test_bad_token(){let c=Config::demo();let m=Model::new(c.clone(),synthetic_weights(&c)).unwrap(); assert!(m.logits(&[c.vocab]).is_err());} #[test] fn test_weight_validation(){let c=Config::demo();let mut w=synthetic_weights(&c); w.remove("model.embed_tokens.weight");assert!(Model::new(c,w).is_err());} } #[cfg(feature = "candle")] pub mod candle_backend;