1use crate::ltxdit::{Shared, gelu_tanh, rows};
41use crate::pool::Pool;
42use crate::qtensor::QTensor;
43use crate::tokenizer::Tokenizer;
44use cortiq_core::CmfModel;
45use cortiq_core::types::TensorDtype;
46use std::sync::{Arc, Mutex};
47
48pub const ARCH_NAME: &str = "embedding_gemma2";
50pub const MAX_TOKENS: usize = 8192;
52pub const MATRYOSHKA_DIMS: [usize; 4] = [768, 512, 256, 128];
54const PACK_TOKENS: usize = 8192;
56const QBLOCK: usize = 128;
59const QBLOCK_FULL: usize = 512;
60const SHORT_SEQ: usize = 96;
63
64pub(crate) const EPS: f64 = 1e-6;
65
66#[cfg(target_os = "macos")]
68const CO_MIN_TOKENS: usize = 2048;
69
70#[cfg(target_os = "macos")]
76fn cpu_share() -> f64 {
77 static V: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
78 *V.get_or_init(|| {
79 std::env::var("CMF_EGEMMA2_CPU_SHARE")
80 .ok()
81 .and_then(|v| v.parse::<f64>().ok())
82 .map(|v| v.clamp(0.0, 0.9))
83 .unwrap_or(0.0)
84 })
85}
86
87pub(crate) mod prof {
90 use std::sync::atomic::{AtomicU64, Ordering};
91 pub static LIN: AtomicU64 = AtomicU64::new(0);
92 pub static ATTN: AtomicU64 = AtomicU64::new(0);
93 pub fn on() -> bool {
94 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
95 *ON.get_or_init(|| std::env::var("CMF_EGEMMA2_PROF").is_ok_and(|v| v == "1"))
96 }
97 pub fn add(c: &AtomicU64, t: std::time::Instant) {
98 c.fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
99 }
100 pub fn take(c: &AtomicU64) -> f64 {
101 c.swap(0, Ordering::Relaxed) as f64 / 1e3
102 }
103}
104
105pub const DEFAULT_PROMPTS: &[(&str, &str)] = &[
108 ("BitextMining", "task: search result | query: "),
109 ("Classification", "task: classification | query: "),
110 ("Clustering", "task: clustering | query: "),
111 ("CodeRetrieval", "task: code retrieval | query: "),
112 ("Document", "title: none | text: "),
113 ("FactChecking", "task: fact checking | query: "),
114 ("InstructionRetrieval", "task: code retrieval | query: "),
115 ("MultilabelClassification", "task: classification | query: "),
116 ("PairClassification", "task: sentence similarity | query: "),
117 ("QuestionAnswering", "task: question answering | query: "),
118 ("Reranking", "task: search result | query: "),
119 ("Retrieval", "task: search result | query: "),
120 ("Retrieval-document", "title: none | text: "),
121 ("Retrieval-query", "task: search result | query: "),
122 ("STS", "task: sentence similarity | query: "),
123 ("SearchQuery", "task: search result | query: "),
124 ("SentenceSimilarity", "task: sentence similarity | query: "),
125 ("Summarization", "task: sentence similarity | query: "),
126 ("document", "title: none | text: "),
127 ("query", "task: search result | query: "),
128];
129
130const DOC_PREFIX: &str = "title: none | text: ";
132
133pub fn is_embedding_gemma2(model: &CmfModel) -> bool {
135 model.header.arch.arch_name == ARCH_NAME
136}
137
138pub fn l2_normalize(v: &mut [f32]) {
142 let n = v
143 .iter()
144 .map(|&x| (x as f64) * (x as f64))
145 .sum::<f64>()
146 .sqrt();
147 if n > 0.0 {
148 for x in v.iter_mut() {
149 *x = (*x as f64 / n) as f32;
150 }
151 }
152}
153
154pub fn matryoshka(v: &[f32], dim: usize) -> Result<Vec<f32>, String> {
158 if !MATRYOSHKA_DIMS.contains(&dim) {
159 return Err(format!(
160 "dimensions {dim}: EmbeddingGemma 2 supports 768, 512, 256 or 128"
161 ));
162 }
163 if dim > v.len() {
164 return Err(format!("dimensions {dim} > embedding size {}", v.len()));
165 }
166 let mut out = v[..dim].to_vec();
167 l2_normalize(&mut out);
168 Ok(out)
169}
170
171pub fn mean_pool(x: &[f32], n: usize, d: usize) -> Vec<f32> {
173 let mut acc = vec![0f64; d];
174 for row in x.chunks_exact(d).take(n) {
175 for (a, &v) in acc.iter_mut().zip(row) {
176 *a += v as f64;
177 }
178 }
179 let inv = 1.0 / n.max(1) as f64;
180 acc.into_iter().map(|a| (a * inv) as f32).collect()
181}
182
183pub fn cosine(a: &[f32], b: &[f32]) -> f64 {
185 let (mut ab, mut aa, mut bb) = (0f64, 0f64, 0f64);
186 for (&x, &y) in a.iter().zip(b) {
187 ab += x as f64 * y as f64;
188 aa += x as f64 * x as f64;
189 bb += y as f64 * y as f64;
190 }
191 if aa == 0.0 || bb == 0.0 {
192 0.0
193 } else {
194 ab / (aa.sqrt() * bb.sqrt())
195 }
196}
197
198#[derive(Clone, Debug)]
200pub struct Prompts {
201 table: Vec<(String, String)>,
202}
203
204impl Default for Prompts {
205 fn default() -> Self {
206 Prompts {
207 table: DEFAULT_PROMPTS
208 .iter()
209 .map(|(a, b)| (a.to_string(), b.to_string()))
210 .collect(),
211 }
212 }
213}
214
215impl Prompts {
216 pub fn from_st_config(cfg: &serde_json::Value) -> Option<Prompts> {
218 let obj = cfg.get("prompts")?.as_object()?;
219 let mut table: Vec<(String, String)> = obj
220 .iter()
221 .filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
222 .collect();
223 table.sort();
224 if table.is_empty() {
225 None
226 } else {
227 Some(Prompts { table })
228 }
229 }
230
231 pub fn get(&self, name: &str) -> Option<&str> {
233 self.table
234 .iter()
235 .find(|(k, _)| k == name)
236 .or_else(|| {
237 self.table
238 .iter()
239 .find(|(k, _)| k.eq_ignore_ascii_case(name))
240 })
241 .map(|(_, v)| v.as_str())
242 }
243
244 pub fn names(&self) -> Vec<&str> {
245 self.table.iter().map(|(k, _)| k.as_str()).collect()
246 }
247
248 pub fn format(
256 &self,
257 text: &str,
258 prompt_name: Option<&str>,
259 title: Option<&str>,
260 prompt: Option<&str>,
261 ) -> Result<String, String> {
262 if let Some(p) = prompt {
263 if title.is_some() {
264 return Err("title applies to the Document prompt, not to a raw prompt".into());
265 }
266 return Ok(format!("{p}{text}"));
267 }
268 let prefix = match prompt_name {
269 Some(n) => self.get(n).ok_or_else(|| {
270 format!(
271 "unknown prompt name '{n}' (known: {})",
272 self.names().join(", ")
273 )
274 })?,
275 None if title.is_some() => DOC_PREFIX,
276 None => return Ok(text.to_string()),
277 };
278 match title {
279 Some(t) => {
280 let rest = prefix.strip_prefix("title: none").ok_or_else(|| {
281 format!(
282 "title applies to the Document prompt, not to '{}'",
283 prompt_name.unwrap_or("")
284 )
285 })?;
286 let t = t.trim();
287 let t = if t.is_empty() { "none" } else { t };
288 Ok(format!("title: {t}{rest}{text}"))
289 }
290 None => Ok(format!("{prefix}{text}")),
291 }
292 }
293}
294
295pub fn wrap_ids(ids: &[u32], bos: u32, eos: u32, max_tokens: usize) -> Vec<u32> {
299 let body = ids.len().min(max_tokens.saturating_sub(2));
300 let mut out = Vec::with_capacity(body + 2);
301 out.push(bos);
302 out.extend_from_slice(&ids[..body]);
303 out.push(eos);
304 out
305}
306
307pub(crate) enum Mat {
312 F32 {
313 w: Vec<f32>,
314 rows: usize,
315 cols: usize,
316 },
317 Q(QTensor),
318}
319
320impl Mat {
321 pub(crate) fn load(model: &Arc<CmfModel>, name: &str, dequant: bool) -> Result<Mat, String> {
324 let e = model
325 .tensor(name)
326 .ok_or_else(|| format!("missing tensor {name}"))?;
327 if dequant && e.shape.len() == 2 {
328 let (rows, cols) = (e.shape[0], e.shape[1]);
329 return Ok(Mat::F32 {
330 w: vecf(model, name)?,
331 rows,
332 cols,
333 });
334 }
335 Ok(match QTensor::from_model(model, name)? {
336 QTensor::F32 { data, rows, cols } => Mat::F32 {
337 w: data,
338 rows,
339 cols,
340 },
341 q => Mat::Q(q),
342 })
343 }
344
345 pub(crate) fn rows(&self) -> usize {
346 match self {
347 Mat::F32 { rows, .. } => *rows,
348 Mat::Q(q) => q.rows(),
349 }
350 }
351
352 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
354 pub(crate) fn f32_weights(&self) -> Option<&[f32]> {
355 match self {
356 Mat::F32 { w, .. } => Some(w),
357 Mat::Q(_) => None,
358 }
359 }
360
361 pub(crate) fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
362 let t0 = std::time::Instant::now();
363 let mut out = vec![0f32; n * self.rows()];
364 match self {
365 Mat::F32 { w, rows, cols } => gemm_nt(x, w, &mut out, n, *cols, *rows, pool),
366 Mat::Q(q) => q.matmat(x, n, &mut out, pool),
367 }
368 prof::add(&prof::LIN, t0);
369 out
370 }
371}
372
373pub(crate) fn accelerate() -> bool {
375 cfg!(target_os = "macos") && std::env::var("CMF_ACCEL").as_deref() != Ok("0")
376}
377
378pub(crate) fn gemm_nt(
384 x: &[f32],
385 w: &[f32],
386 y: &mut [f32],
387 n: usize,
388 k: usize,
389 m: usize,
390 pool: Option<&Pool>,
391) {
392 assert!(x.len() >= n * k && w.len() >= m * k && y.len() >= n * m);
393 if accelerate() {
394 return crate::fcd_ops::gemm_nt_host(x, w, y, n, k, m, pool);
395 }
396 gemm_nt_portable(x, w, y, n, k, m, pool)
397}
398
399fn gemm_nt_portable(
402 x: &[f32],
403 w: &[f32],
404 y: &mut [f32],
405 n: usize,
406 k: usize,
407 m: usize,
408 pool: Option<&Pool>,
409) {
410 assert!(x.len() >= n * k && w.len() >= m * k && y.len() >= n * m);
411 if n == 0 || m == 0 {
412 return;
413 }
414 let threads = pool.map(|p| p.n_workers()).unwrap_or(1).max(1);
417 let by_rows = n >= 32 * threads || n >= m;
418 let (len, unit) = if by_rows { (n, 32) } else { (m, 64) };
419 let chunk = len.div_ceil(threads).div_ceil(unit) * unit;
420 let blocks = len.div_ceil(chunk);
421 let ybase = y.as_mut_ptr() as usize;
423 let run = |b: usize| {
424 let yp = ybase as *mut f32;
425 let (lo, hi) = (b * chunk, ((b + 1) * chunk).min(len));
426 unsafe {
429 if by_rows {
430 matrixmultiply::sgemm(
431 hi - lo,
432 k,
433 m,
434 1.0,
435 x.as_ptr().add(lo * k),
436 k as isize,
437 1,
438 w.as_ptr(),
439 1,
440 k as isize,
441 0.0,
442 yp.add(lo * m),
443 m as isize,
444 1,
445 );
446 } else {
447 matrixmultiply::sgemm(
448 n,
449 k,
450 hi - lo,
451 1.0,
452 x.as_ptr(),
453 k as isize,
454 1,
455 w.as_ptr().add(lo * k),
456 1,
457 k as isize,
458 0.0,
459 yp.add(lo),
460 m as isize,
461 1,
462 );
463 }
464 }
465 };
466 if blocks <= 1 {
467 run(0);
468 } else {
469 rows(pool, blocks, &|s, e| (s..e).for_each(run));
470 }
471}
472
473enum Table {
476 Bf16 {
477 model: Arc<CmfModel>,
478 idx: usize,
479 rows: usize,
480 cols: usize,
481 },
482 Q(QTensor),
483}
484
485impl Table {
486 fn load(model: &Arc<CmfModel>, name: &str) -> Result<Table, String> {
487 let idx = model
488 .tensor_index(name)
489 .ok_or_else(|| format!("missing tensor {name}"))?;
490 let e = &model.tensors[idx];
491 if e.shape.len() != 2 {
492 return Err(format!("{name}: expected a 2-D table"));
493 }
494 if e.dtype == TensorDtype::Bf16 {
495 return Ok(Table::Bf16 {
496 model: model.clone(),
497 idx,
498 rows: e.shape[0],
499 cols: e.shape[1],
500 });
501 }
502 Ok(Table::Q(QTensor::from_model(model, name)?))
503 }
504
505 fn rows(&self) -> usize {
506 match self {
507 Table::Bf16 { rows, .. } => *rows,
508 Table::Q(q) => q.rows(),
509 }
510 }
511
512 fn row(&self, r: usize, dst: &mut [f32]) {
513 match self {
514 Table::Bf16 {
515 model, idx, cols, ..
516 } => {
517 let bytes = model.entry_bytes(&model.tensors[*idx]);
518 let src = &bytes[r * cols * 2..(r + 1) * cols * 2];
519 for (d, c) in dst.iter_mut().zip(src.chunks_exact(2)) {
520 *d = cortiq_core::quant::bf16_to_f32(u16::from_le_bytes([c[0], c[1]]));
521 }
522 }
523 Table::Q(q) => q.row_f32(r, dst),
524 }
525 }
526}
527
528pub(crate) fn vecf(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
529 crate::dit::cmf_f32(model, name)
530}
531
532struct Layer {
533 q: Mat,
534 k: Mat,
535 v: Mat,
536 o: Mat,
537 q_norm: Vec<f32>,
538 k_norm: Vec<f32>,
539 in_norm: Vec<f32>,
540 post_attn_norm: Vec<f32>,
541 pre_ff_norm: Vec<f32>,
542 post_ff_norm: Vec<f32>,
543 gate: Mat,
544 up: Mat,
545 down: Mat,
546 ple_gate: Mat,
547 ple_proj: Mat,
548 ple_norm: Vec<f32>,
549 ple_in: Mat,
551 scalar: f32,
552 head_dim: usize,
553 q_heads: usize,
554 kv_heads: usize,
555 window: Option<usize>,
557 theta: f32,
558}
559
560struct Rot {
563 cos: Vec<f32>,
564 sin: Vec<f32>,
565 half: usize,
566}
567
568impl Rot {
569 fn build(len: usize, head_dim: usize, theta: f32) -> Rot {
570 let half = head_dim / 2;
571 let inv: Vec<f32> = (0..half)
572 .map(|j| 1.0f32 / theta.powf((2 * j) as f32 / head_dim as f32))
573 .collect();
574 let mut cos = vec![0f32; len * half];
575 let mut sin = vec![0f32; len * half];
576 for p in 0..len {
577 for (j, &f) in inv.iter().enumerate() {
578 let a = (p as f32 * f) as f64;
579 cos[p * half + j] = a.cos() as f32;
580 sin[p * half + j] = a.sin() as f32;
581 }
582 }
583 Rot { cos, sin, half }
584 }
585
586 fn apply(&self, pos: usize, x: &mut [f32]) {
588 let h = self.half;
589 let (c, s) = (
590 &self.cos[pos * h..(pos + 1) * h],
591 &self.sin[pos * h..(pos + 1) * h],
592 );
593 for i in 0..h {
594 let (a, b) = (x[i], x[i + h]);
595 x[i] = a * c[i] - b * s[i];
596 x[i + h] = b * c[i] + a * s[i];
597 }
598 }
599}
600
601pub(crate) fn rms_rows(x: &[f32], w: Option<&[f32]>, d: usize, pool: Option<&Pool>) -> Vec<f32> {
603 let n = x.len() / d;
604 let mut out = vec![0f32; x.len()];
605 let dst = Shared(out.as_mut_ptr());
606 rows(pool, n, &|s, e| {
607 let o = unsafe { dst.at(s * d, (e - s) * d) };
608 for (orow, xrow) in o.chunks_exact_mut(d).zip(x[s * d..e * d].chunks_exact(d)) {
609 rms_into(xrow, w, orow);
610 }
611 });
612 out
613}
614
615#[inline]
616pub(crate) fn rms_into(x: &[f32], w: Option<&[f32]>, out: &mut [f32]) {
617 let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
618 let inv = 1.0 / (ss + EPS).sqrt();
619 match w {
620 Some(w) => {
621 for ((o, &v), &g) in out.iter_mut().zip(x).zip(w) {
622 *o = (v as f64 * inv) as f32 * g;
623 }
624 }
625 None => {
626 for (o, &v) in out.iter_mut().zip(x) {
627 *o = (v as f64 * inv) as f32;
628 }
629 }
630 }
631}
632
633pub(crate) fn add_normed(h: &mut [f32], y: &[f32], w: &[f32], d: usize, pool: Option<&Pool>) {
635 let n = h.len() / d;
636 let dst = Shared(h.as_mut_ptr());
637 rows(pool, n, &|s, e| {
638 let hh = unsafe { dst.at(s * d, (e - s) * d) };
639 let mut tmp = vec![0f32; d];
640 for (hrow, yrow) in hh.chunks_exact_mut(d).zip(y[s * d..e * d].chunks_exact(d)) {
641 rms_into(yrow, Some(w), &mut tmp);
642 for (a, &b) in hrow.iter_mut().zip(&tmp) {
643 *a += b;
644 }
645 }
646 });
647}
648
649#[inline]
655fn gelu_mul_slice(x: &mut [f32], y: &[f32]) {
656 #[allow(unused_mut)] let mut i = 0usize;
658 #[cfg(target_arch = "aarch64")]
659 unsafe {
661 use core::arch::aarch64::*;
662 debug_assert!(y.len() >= x.len());
663 let k = vdupq_n_f32(-2.0 * 0.797_884_56);
664 let c = vdupq_n_f32(0.044715);
665 let one = vdupq_n_f32(1.0);
666 while i + 4 <= x.len() {
667 let v = vld1q_f32(x.as_ptr().add(i));
668 let v3 = vmulq_f32(vmulq_f32(v, v), v);
669 let e = crate::attention::vexpq_f32(vmulq_f32(k, vfmaq_f32(v, c, v3)));
670 let g = vdivq_f32(v, vaddq_f32(one, e));
671 vst1q_f32(
672 x.as_mut_ptr().add(i),
673 vmulq_f32(g, vld1q_f32(y.as_ptr().add(i))),
674 );
675 i += 4;
676 }
677 }
678 for (a, &b) in x[i..].iter_mut().zip(&y[i..]) {
679 *a = gelu_tanh(*a) * b;
680 }
681}
682
683pub(crate) fn gelu_mul(a: &mut [f32], b: &[f32], pool: Option<&Pool>) {
685 let n = a.len();
686 let grain = 16384usize;
687 let dst = Shared(a.as_mut_ptr());
688 rows(pool, n.div_ceil(grain), &|s, e| {
689 let (lo, hi) = (s * grain, (e * grain).min(n));
690 let r = unsafe { dst.at(lo, hi - lo) };
691 gelu_mul_slice(r, &b[lo..hi]);
692 });
693}
694
695fn gemm_nt_strided(x: &[f32], w: &[f32], ldw: usize, y: &mut [f32], n: usize, k: usize, m: usize) {
697 assert!(m == 0 || w.len() >= (m - 1) * ldw + k);
698 assert!(x.len() >= n * k && y.len() >= n * m);
699 if !accelerate() {
700 if n > 0 && m > 0 {
701 unsafe {
704 matrixmultiply::sgemm(
705 n,
706 k,
707 m,
708 1.0,
709 x.as_ptr(),
710 k as isize,
711 1,
712 w.as_ptr(),
713 1,
714 ldw as isize,
715 0.0,
716 y.as_mut_ptr(),
717 m as isize,
718 1,
719 );
720 }
721 }
722 return;
723 }
724 if ldw == k {
725 return crate::fcd_ops::gemm_nt_host(x, &w[..m * k], y, n, k, m, None);
726 }
727 #[cfg(target_os = "macos")]
728 {
729 #[link(name = "Accelerate", kind = "framework")]
730 unsafe extern "C" {
731 fn cblas_sgemm(
732 order: i32,
733 ta: i32,
734 tb: i32,
735 m: i32,
736 n: i32,
737 k: i32,
738 alpha: f32,
739 a: *const f32,
740 lda: i32,
741 b: *const f32,
742 ldb: i32,
743 beta: f32,
744 c: *mut f32,
745 ldc: i32,
746 );
747 }
748 unsafe {
750 cblas_sgemm(
751 101,
752 111,
753 112,
754 n as i32,
755 m as i32,
756 k as i32,
757 1.0,
758 x.as_ptr(),
759 k as i32,
760 w.as_ptr(),
761 ldw as i32,
762 0.0,
763 y.as_mut_ptr(),
764 m as i32,
765 );
766 }
767 }
768}
769
770pub(crate) fn softmax(row: &mut [f32]) {
772 #[cfg(target_arch = "aarch64")]
773 {
774 crate::attention::softmax_row(row);
775 }
776 #[cfg(not(target_arch = "aarch64"))]
777 softmax_scalar(row);
778}
779
780#[cfg_attr(target_arch = "aarch64", allow(dead_code))]
781fn softmax_scalar(row: &mut [f32]) {
782 let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
783 let mut den = 0f64;
784 for r in row.iter_mut() {
785 *r = (*r - mx).exp();
786 den += *r as f64;
787 }
788 let inv = (1.0 / den) as f32;
789 for r in row.iter_mut() {
790 *r *= inv;
791 }
792}
793
794pub struct EmbeddingGemma2 {
799 #[cfg(target_os = "macos")]
802 gpu: Option<crate::gpu_metal::egemma2::TextGpu>,
803 host_only: std::sync::atomic::AtomicBool,
805 tok: Tokenizer,
806 prompts: Prompts,
807 embed: Table,
808 layers: Vec<Layer>,
809 ple_norm: Vec<f32>,
810 norm: Vec<f32>,
811 head: Mat,
812 hidden: usize,
813 embed_scale: f32,
814 n_ple: usize,
815 pub dim: usize,
816 pub max_tokens: usize,
817 pub bos: u32,
818 pub eos: u32,
819 pool: Option<Arc<Pool>>,
820 busy: Mutex<()>,
822 pub quant: String,
824}
825
826#[derive(Clone, Debug, Default)]
828pub struct TextInput {
829 pub text: String,
830 pub prompt_name: Option<String>,
831 pub title: Option<String>,
832 pub prompt: Option<String>,
834}
835
836impl TextInput {
837 pub fn plain(text: impl Into<String>) -> Self {
838 TextInput {
839 text: text.into(),
840 ..Default::default()
841 }
842 }
843}
844
845impl EmbeddingGemma2 {
846 pub fn load(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
849 if !is_embedding_gemma2(model) {
850 return Err(format!(
851 "not an EmbeddingGemma 2 file (arch '{}')",
852 model.header.arch.arch_name
853 ));
854 }
855 let prov = model
856 .header
857 .provenance
858 .clone()
859 .unwrap_or(serde_json::Value::Null);
860 let eg = &prov["embedding_gemma2"];
861 let tc = &eg["config"]["text_config"];
862 let g = |k: &str, d: u64| tc.get(k).and_then(|v| v.as_u64()).unwrap_or(d) as usize;
863 let hidden = g("hidden_size", 512);
864 let n_layers = g("num_hidden_layers", 24);
865 let n_heads = g("num_attention_heads", 4);
866 let head_dim = g("head_dim", 256);
867 let kv_heads = g("num_key_value_heads", 2);
868 let window = g("sliding_window", 512);
869 let types: Vec<bool> = match tc.get("layer_types").and_then(|v| v.as_array()) {
870 Some(a) => a
871 .iter()
872 .map(|s| s.as_str() == Some("full_attention"))
873 .collect(),
874 None => (0..n_layers).map(|i| i % 6 == 5).collect(),
875 };
876 let theta = |kind: &str, d: f64| {
877 tc["rope_parameters"][kind]["rope_theta"]
878 .as_f64()
879 .unwrap_or(d) as f32
880 };
881 let (theta_slide, theta_full) = (
882 theta("sliding_attention", 1e4),
883 theta("full_attention", 1e6),
884 );
885 let per_layer = &tc["per_layer_config"];
886 let ple_dim = g("hidden_size_per_layer_input", 512);
887 let dequant = !std::env::var("CMF_EGEMMA2_LOWMEM").is_ok_and(|v| v == "1");
892
893 let ple_all = vecf(
896 model,
897 "language_model.ple.per_layer_model_projection.weight",
898 )?;
899 if ple_all.len() != n_layers * ple_dim * hidden {
900 return Err(format!(
901 "per_layer_model_projection: {} values, expected {}",
902 ple_all.len(),
903 n_layers * ple_dim * hidden
904 ));
905 }
906
907 let mut layers = Vec::with_capacity(n_layers);
908 for i in 0..n_layers {
909 let p = format!("language_model.layers.{i}");
910 let full = types.get(i).copied().unwrap_or(i % 6 == 5);
911 let key = format!("{i:02}");
912 let lc = &per_layer[key.as_str()];
913 let hd = lc["head_dim"]
914 .as_u64()
915 .map(|v| v as usize)
916 .unwrap_or(if full { head_dim * 2 } else { head_dim });
917 let nkv = lc["num_key_value_heads"]
918 .as_u64()
919 .map(|v| v as usize)
920 .unwrap_or(if full { 1 } else { kv_heads });
921 let ld = |s: &str| Mat::load(model, &format!("{p}.{s}"), dequant);
922 let q = ld("self_attn.q_proj.weight")?;
923 let k = ld("self_attn.k_proj.weight")?;
924 if q.rows() != n_heads * hd || k.rows() != nkv * hd {
925 return Err(format!(
926 "layer {i}: q/k rows {}/{} do not match {n_heads}x{hd} / {nkv}x{hd}",
927 q.rows(),
928 k.rows()
929 ));
930 }
931 layers.push(Layer {
932 q,
933 k,
934 v: ld("self_attn.v_proj.weight")?,
935 o: ld("self_attn.o_proj.weight")?,
936 q_norm: vecf(model, &format!("{p}.self_attn.q_norm.weight"))?,
937 k_norm: vecf(model, &format!("{p}.self_attn.k_norm.weight"))?,
938 in_norm: vecf(model, &format!("{p}.input_layernorm.weight"))?,
939 post_attn_norm: vecf(model, &format!("{p}.post_attention_layernorm.weight"))?,
940 pre_ff_norm: vecf(model, &format!("{p}.pre_feedforward_layernorm.weight"))?,
941 post_ff_norm: vecf(model, &format!("{p}.post_feedforward_layernorm.weight"))?,
942 gate: ld("mlp.gate_proj.weight")?,
943 up: ld("mlp.up_proj.weight")?,
944 down: ld("mlp.down_proj.weight")?,
945 ple_gate: ld("ple_block.per_layer_input_gate.weight")?,
946 ple_proj: ld("ple_block.per_layer_projection.weight")?,
947 ple_norm: vecf(
948 model,
949 &format!("{p}.ple_block.post_per_layer_input_norm.weight"),
950 )?,
951 ple_in: Mat::F32 {
952 w: ple_all[i * ple_dim * hidden..(i + 1) * ple_dim * hidden].to_vec(),
953 rows: ple_dim,
954 cols: hidden,
955 },
956 scalar: vecf(model, &format!("{p}.layer_scalar"))?[0],
957 head_dim: hd,
958 q_heads: n_heads,
959 kv_heads: nkv,
960 window: if full { None } else { Some(window) },
961 theta: if full { theta_full } else { theta_slide },
962 });
963 }
964 drop(ple_all);
965
966 let vocab = model
967 .vocab
968 .as_deref()
969 .ok_or("EmbeddingGemma 2 file has no embedded tokenizer")?;
970 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
971 let cfg = &eg["config"];
972 let bos = tc["bos_token_id"]
973 .as_u64()
974 .or_else(|| cfg["bos_token_id"].as_u64())
975 .unwrap_or(2) as u32;
976 let eos = tc["eos_token_id"]
977 .as_u64()
978 .or_else(|| cfg["eos_token_id"].as_u64())
979 .unwrap_or(1) as u32;
980 let prompts = Prompts::from_st_config(&eg["sentence_transformers"]).unwrap_or_default();
981 let head = Mat::load(model, "language_model.embedding_projection.weight", true)?;
982 let embed = Table::load(model, "language_model.embed_tokens.weight")?;
983 let dim = head.rows();
984 #[cfg_attr(not(target_os = "macos"), allow(unused_mut))]
986 let mut enc = EmbeddingGemma2 {
987 #[cfg(target_os = "macos")]
988 gpu: None,
989 host_only: std::sync::atomic::AtomicBool::new(false),
990 tok,
991 prompts,
992 embed,
993 layers,
994 ple_norm: vecf(model, "language_model.ple.per_layer_projection_norm.weight")?,
995 norm: vecf(model, "language_model.norm.weight")?,
996 head,
997 hidden,
998 embed_scale: (hidden as f32).sqrt(),
999 n_ple: ple_dim,
1000 dim,
1001 max_tokens: eg["max_tokens"]
1002 .as_u64()
1003 .map(|v| v as usize)
1004 .unwrap_or(MAX_TOKENS),
1005 bos,
1006 eos,
1007 pool,
1008 busy: Mutex::new(()),
1009 quant: prov["quant"].as_str().unwrap_or("").to_string(),
1010 };
1011 #[cfg(target_os = "macos")]
1012 if dequant && crate::gpu_metal::egemma2::enabled() {
1013 match enc.device_encoder() {
1014 Ok(g) => enc.gpu = Some(g),
1015 Err(e) => tracing::info!("egemma2: Metal text path off ({e}); the CPU runs it"),
1016 }
1017 }
1018 Ok(enc)
1019 }
1020
1021 #[cfg(target_os = "macos")]
1023 fn device_encoder(&self) -> Result<crate::gpu_metal::egemma2::TextGpu, String> {
1024 use crate::gpu_metal::egemma2::{LayerSpec, TextGpu, TextSpec};
1025 fn f(m: &Mat) -> Result<&[f32], String> {
1026 m.f32_weights()
1027 .ok_or_else(|| "a quantized matrix (low-memory mode)".to_string())
1028 }
1029 let mut ropes: Vec<(usize, u32, Vec<f32>, Vec<f32>)> = Vec::new();
1030 let mut layers = Vec::with_capacity(self.layers.len());
1031 let mut inter = 0usize;
1032 for l in &self.layers {
1033 let key = (l.head_dim, l.theta.to_bits());
1034 let rope = match ropes.iter().position(|r| (r.0, r.1) == key) {
1035 Some(i) => i,
1036 None => {
1037 let r = Rot::build(self.max_tokens, l.head_dim, l.theta);
1038 ropes.push((key.0, key.1, r.cos, r.sin));
1039 ropes.len() - 1
1040 }
1041 };
1042 inter = l.gate.rows();
1043 layers.push(LayerSpec {
1044 q: f(&l.q)?,
1045 k: f(&l.k)?,
1046 v: f(&l.v)?,
1047 o: f(&l.o)?,
1048 gate: f(&l.gate)?,
1049 up: f(&l.up)?,
1050 down: f(&l.down)?,
1051 ple_in: f(&l.ple_in)?,
1052 ple_gate: f(&l.ple_gate)?,
1053 ple_proj: f(&l.ple_proj)?,
1054 q_norm: &l.q_norm,
1055 k_norm: &l.k_norm,
1056 in_norm: &l.in_norm,
1057 post_attn_norm: &l.post_attn_norm,
1058 pre_ff_norm: &l.pre_ff_norm,
1059 post_ff_norm: &l.post_ff_norm,
1060 ple_norm: &l.ple_norm,
1061 head_dim: l.head_dim,
1062 q_heads: l.q_heads,
1063 kv_heads: l.kv_heads,
1064 window: l.window,
1065 scalar: l.scalar,
1066 rope,
1067 });
1068 }
1069 TextGpu::new(&TextSpec {
1070 hidden: self.hidden,
1071 inter,
1072 n_ple: self.n_ple,
1073 eps: EPS as f32,
1074 ple_scale: (self.hidden as f32).powf(-0.5),
1075 ple_norm: &self.ple_norm,
1076 norm: &self.norm,
1077 layers,
1078 ropes: ropes.into_iter().map(|(hd, _, c, s)| (hd, c, s)).collect(),
1079 max_tokens: self.max_tokens,
1080 })
1081 }
1082
1083 pub fn force_host(&self, on: bool) {
1085 self.host_only
1086 .store(on, std::sync::atomic::Ordering::Relaxed);
1087 }
1088
1089 pub fn on_device(&self) -> bool {
1091 #[cfg(target_os = "macos")]
1092 let up = self.gpu.is_some();
1093 #[cfg(not(target_os = "macos"))]
1094 let up = false;
1095 up && !self.host_only.load(std::sync::atomic::Ordering::Relaxed)
1096 }
1097
1098 pub fn prompts(&self) -> &Prompts {
1099 &self.prompts
1100 }
1101
1102 pub fn tokenizer(&self) -> &Tokenizer {
1103 &self.tok
1104 }
1105
1106 pub fn format(&self, input: &TextInput) -> Result<String, String> {
1108 self.prompts.format(
1109 &input.text,
1110 input.prompt_name.as_deref(),
1111 input.title.as_deref(),
1112 input.prompt.as_deref(),
1113 )
1114 }
1115
1116 pub fn tokenize(&self, full_text: &str) -> Vec<u32> {
1118 wrap_ids(
1119 &self.tok.encode(full_text),
1120 self.bos,
1121 self.eos,
1122 self.max_tokens,
1123 )
1124 }
1125
1126 pub fn input_ids(&self, input: &TextInput) -> Result<Vec<u32>, String> {
1128 Ok(self.tokenize(&self.format(input)?))
1129 }
1130
1131 pub fn embed_texts(&self, inputs: &[TextInput]) -> Result<Vec<Vec<f32>>, String> {
1133 let ids: Vec<Vec<u32>> = inputs
1134 .iter()
1135 .map(|i| self.input_ids(i))
1136 .collect::<Result<_, _>>()?;
1137 self.embed_ids(&ids)
1138 }
1139
1140 pub fn embed_ids(&self, seqs: &[Vec<u32>]) -> Result<Vec<Vec<f32>>, String> {
1143 let vocab = self.embed.rows();
1144 for (i, s) in seqs.iter().enumerate() {
1145 if s.is_empty() {
1146 return Err(format!("input {i}: no tokens"));
1147 }
1148 if s.len() > self.max_tokens {
1149 return Err(format!(
1150 "input {i}: {} tokens > the model's {} token context",
1151 s.len(),
1152 self.max_tokens
1153 ));
1154 }
1155 if let Some(&bad) = s.iter().find(|&&t| t as usize >= vocab) {
1156 return Err(format!("input {i}: token id {bad} outside the vocabulary"));
1157 }
1158 }
1159 let _g = self.busy.lock().unwrap_or_else(|e| e.into_inner());
1160 let mut out: Vec<Option<Vec<f32>>> = vec![None; seqs.len()];
1161 let mut start = 0usize;
1163 while start < seqs.len() {
1164 let mut end = start;
1165 let mut toks = 0usize;
1166 while end < seqs.len() && (end == start || toks + seqs[end].len() <= PACK_TOKENS) {
1167 toks += seqs[end].len();
1168 end += 1;
1169 }
1170 let x = self.embed_rows(&seqs[start..end]);
1171 let lens: Vec<usize> = seqs[start..end].iter().map(|s| s.len()).collect();
1172 let pooled = self.forward_pooled_locked(x, &lens);
1173 for (k, v) in pooled.into_iter().enumerate() {
1174 out[start + k] = Some(v);
1175 }
1176 start = end;
1177 }
1178 Ok(out.into_iter().map(|v| v.unwrap()).collect())
1179 }
1180
1181 pub fn embed_rows(&self, seqs: &[Vec<u32>]) -> Vec<f32> {
1184 let d = self.hidden;
1185 let n: usize = seqs.iter().map(|s| s.len()).sum();
1186 let mut x = vec![0f32; n * d];
1187 for (row, &id) in x.chunks_exact_mut(d).zip(seqs.iter().flatten()) {
1188 self.embed.row(id as usize, row);
1189 for v in row.iter_mut() {
1190 *v *= self.embed_scale;
1191 }
1192 }
1193 x
1194 }
1195
1196 pub fn hidden(&self) -> usize {
1198 self.hidden
1199 }
1200
1201 pub fn embed_merged(&self, x: Vec<f32>, lens: &[usize]) -> Vec<Vec<f32>> {
1206 let _g = self.busy.lock().unwrap_or_else(|e| e.into_inner());
1207 self.forward_pooled_locked(x, lens)
1208 }
1209
1210 fn forward_pooled_locked(&self, x: Vec<f32>, lens: &[usize]) -> Vec<Vec<f32>> {
1211 #[cfg(target_os = "macos")]
1212 if let Some(g) = self.gpu.as_ref().filter(|_| self.on_device()) {
1213 let d = self.hidden;
1214 let head = |pooled: Vec<f32>| -> Vec<Vec<f32>> {
1215 let nseq = pooled.len() / d;
1216 let y = self.head.apply(&pooled, nseq, self.pool.as_deref());
1217 y.chunks_exact(self.dim)
1218 .map(|r| {
1219 let mut v = r.to_vec();
1220 l2_normalize(&mut v);
1221 v
1222 })
1223 .collect()
1224 };
1225 let n: usize = lens.iter().sum();
1229 let share = cpu_share();
1230 let mut split = lens.len();
1231 if share > 0.0 && lens.len() >= 2 && n >= CO_MIN_TOKENS {
1232 let want = (n as f64 * share) as usize;
1233 let mut tail = 0usize;
1234 while split > 1 && tail + lens[split - 1] <= want {
1235 split -= 1;
1236 tail += lens[split];
1237 }
1238 }
1239 let gpu_rows: usize = lens[..split].iter().sum();
1240 let (xg, xc) = x.split_at(gpu_rows * d);
1241 let (gpu, host) = std::thread::scope(|sc| {
1242 let job = sc.spawn(|| g.forward(xg, &lens[..split]));
1243 let host = (split < lens.len()).then(|| {
1244 let t = std::time::Instant::now();
1245 let v = self.forward_host(xc.to_vec(), &lens[split..]);
1246 (v, t.elapsed().as_secs_f64())
1247 });
1248 (job.join(), host)
1249 });
1250 match gpu {
1251 Ok(Ok(pooled)) => {
1252 let mut out = head(pooled);
1253 if let Some((v, secs)) = host {
1254 if prof::on() {
1255 eprintln!(
1256 "egemma2 co-run: host {} seqs / {} tokens in {:.1} ms beside the device",
1257 lens.len() - split,
1258 n - gpu_rows,
1259 secs * 1e3
1260 );
1261 }
1262 out.extend(v);
1263 }
1264 return out;
1265 }
1266 Ok(Err(e)) => {
1267 tracing::warn!("egemma2: Metal forward failed ({e}); the CPU runs it")
1268 }
1269 Err(_) => tracing::warn!("egemma2: Metal forward panicked; the CPU runs it"),
1270 }
1271 }
1272 self.forward_host(x, lens)
1273 }
1274
1275 fn forward_host(&self, x: Vec<f32>, lens: &[usize]) -> Vec<Vec<f32>> {
1277 let t_fwd = std::time::Instant::now();
1278 let _ = (prof::take(&prof::LIN), prof::take(&prof::ATTN));
1281 let pool = self.pool.as_deref();
1282 let d = self.hidden;
1283 let n: usize = lens.iter().sum();
1284 debug_assert_eq!(x.len(), n * d);
1285 let maxlen = lens.iter().copied().max().unwrap_or(0);
1286 let mut segs = Vec::with_capacity(lens.len());
1288 let mut pos = Vec::with_capacity(n);
1289 let mut off = 0usize;
1290 for &l in lens {
1291 segs.push((off, l));
1292 pos.extend(0..l);
1293 off += l;
1294 }
1295 let mut rots: Vec<((usize, u32), Rot)> = Vec::new();
1296 for l in &self.layers {
1297 let key = (l.head_dim, l.theta.to_bits());
1298 if !rots.iter().any(|(k, _)| *k == key) {
1299 rots.push((key, Rot::build(maxlen, l.head_dim, l.theta)));
1300 }
1301 }
1302 let ple_scale = (d as f32).powf(-0.5);
1303
1304 let mut h = x.clone();
1305 for l in &self.layers {
1306 let rot = &rots
1307 .iter()
1308 .find(|(k, _)| *k == (l.head_dim, l.theta.to_bits()))
1309 .unwrap()
1310 .1;
1311 let a = rms_rows(&h, Some(&l.in_norm), d, pool);
1313 let attn = self.attention(l, &a, n, &segs, &pos, rot, pool);
1314 add_normed(&mut h, &attn, &l.post_attn_norm, d, pool);
1315 let m = rms_rows(&h, Some(&l.pre_ff_norm), d, pool);
1317 let mut g = l.gate.apply(&m, n, pool);
1318 let u = l.up.apply(&m, n, pool);
1319 gelu_mul(&mut g, &u, pool);
1320 let f = l.down.apply(&g, n, pool);
1321 add_normed(&mut h, &f, &l.post_ff_norm, d, pool);
1322 let mut pin = l.ple_in.apply(&x, n, pool);
1324 for v in pin.iter_mut() {
1325 *v *= ple_scale;
1326 }
1327 let pin = rms_rows(&pin, Some(&self.ple_norm), self.n_ple, pool);
1328 let mut z = l.ple_gate.apply(&h, n, pool);
1329 gelu_mul(&mut z, &pin, pool);
1330 let z = l.ple_proj.apply(&z, n, pool);
1331 add_normed(&mut h, &z, &l.ple_norm, d, pool);
1332 for v in h.iter_mut() {
1333 *v *= l.scalar;
1334 }
1335 }
1336 let hn = rms_rows(&h, Some(&self.norm), d, pool);
1337 let out = segs
1338 .iter()
1339 .map(|&(s, l)| {
1340 let mean = mean_pool(&hn[s * d..(s + l) * d], l, d);
1341 let mut v = self.head.apply(&mean, 1, pool);
1342 l2_normalize(&mut v);
1343 v
1344 })
1345 .collect();
1346 let (lin, attn) = (prof::take(&prof::LIN), prof::take(&prof::ATTN));
1347 if prof::on() {
1348 let total = t_fwd.elapsed().as_secs_f64() * 1e3;
1349 eprintln!(
1350 "egemma2 forward: {n} tokens / {} seqs in {total:.1} ms — projections {lin:.1}, attention core {attn:.1}, rest {:.1}",
1351 lens.len(),
1352 total - lin - attn
1353 );
1354 }
1355 out
1356 }
1357
1358 #[allow(clippy::too_many_arguments)]
1361 fn attention(
1362 &self,
1363 l: &Layer,
1364 a: &[f32],
1365 n: usize,
1366 segs: &[(usize, usize)],
1367 pos: &[usize],
1368 rot: &Rot,
1369 pool: Option<&Pool>,
1370 ) -> Vec<f32> {
1371 let hd = l.head_dim;
1372 let (nq, nkv) = (l.q_heads, l.kv_heads);
1373 let (qw, kw) = (nq * hd, nkv * hd);
1374 let mut q = l.q.apply(a, n, pool);
1375 let mut k = l.k.apply(a, n, pool);
1376 let mut v = l.v.apply(a, n, pool);
1377 {
1379 let (qp, kp, vp) = (
1380 Shared(q.as_mut_ptr()),
1381 Shared(k.as_mut_ptr()),
1382 Shared(v.as_mut_ptr()),
1383 );
1384 rows(pool, n, &|s, e| {
1385 let mut tmp = vec![0f32; hd];
1386 for t in s..e {
1387 let qr = unsafe { qp.at(t * qw, qw) };
1388 for hh in qr.chunks_exact_mut(hd) {
1389 rms_into(hh, Some(&l.q_norm), &mut tmp);
1390 hh.copy_from_slice(&tmp);
1391 rot.apply(pos[t], hh);
1392 }
1393 let kr = unsafe { kp.at(t * kw, kw) };
1394 for hh in kr.chunks_exact_mut(hd) {
1395 rms_into(hh, Some(&l.k_norm), &mut tmp);
1396 hh.copy_from_slice(&tmp);
1397 rot.apply(pos[t], hh);
1398 }
1399 let vr = unsafe { vp.at(t * kw, kw) };
1400 for hh in vr.chunks_exact_mut(hd) {
1401 rms_into(hh, None, &mut tmp);
1402 hh.copy_from_slice(&tmp);
1403 }
1404 }
1405 });
1406 }
1407 let t_core = std::time::Instant::now();
1408 let mut out = vec![0f32; n * qw];
1409 let group = nq / nkv;
1410 let items: Vec<(usize, usize)> = segs
1415 .iter()
1416 .filter(|&&(_, len)| len <= SHORT_SEQ)
1417 .flat_map(|&(s0, len)| (0..nq).map(move |h| (s0 * nq + h, len)))
1418 .collect();
1419 if !items.is_empty() {
1420 let op = Shared(out.as_mut_ptr());
1421 let window = l.window;
1422 let (q, k, v) = (&q, &k, &v);
1423 rows(pool, items.len(), &|s, e| {
1424 let mut p = vec![0f32; SHORT_SEQ];
1425 for &(code, len) in &items[s..e] {
1426 let (s0, qh) = (code / nq, code % nq);
1427 let kvh = qh / group;
1428 for i in 0..len {
1429 let (lo, hi) = match window {
1430 Some(w) => (i.saturating_sub(w), (i + w + 1).min(len)),
1431 None => (0, len),
1432 };
1433 let qi = &q[(s0 + i) * qw + qh * hd..][..hd];
1434 let row = &mut p[..hi - lo];
1435 for (j, r) in (lo..hi).zip(row.iter_mut()) {
1436 *r =
1437 crate::attention::dot_f32(qi, &k[(s0 + j) * kw + kvh * hd..][..hd]);
1438 }
1439 softmax(row);
1440 let o = unsafe { op.at((s0 + i) * qw + qh * hd, hd) };
1442 o.iter_mut().for_each(|x| *x = 0.0);
1443 for (j, &w) in (lo..hi).zip(row.iter()) {
1444 crate::attention::axpy_f32(o, &v[(s0 + j) * kw + kvh * hd..][..hd], w);
1445 }
1446 }
1447 }
1448 });
1449 }
1450 let long: Vec<(usize, usize, usize)> = segs
1455 .iter()
1456 .filter(|&&(_, len)| len > SHORT_SEQ)
1457 .flat_map(|&(s0, len)| (0..nkv).map(move |kvh| (s0, len, kvh)))
1458 .collect();
1459 let op = Shared(out.as_mut_ptr());
1460 let (q, k, v) = (&q, &k, &v);
1461 let window = l.window;
1462 let attend = |s0: usize, len: usize, kvh: usize, inner: Option<&Pool>| {
1463 let mut kh = vec![0f32; len * hd];
1465 let mut vt = vec![0f32; hd * len];
1466 for t in 0..len {
1467 kh[t * hd..(t + 1) * hd].copy_from_slice(&k[(s0 + t) * kw + kvh * hd..][..hd]);
1468 let vr = &v[(s0 + t) * kw + kvh * hd..][..hd];
1469 for (c, &val) in vr.iter().enumerate() {
1470 vt[c * len + t] = val;
1471 }
1472 }
1473 let qblock = if window.is_some() {
1474 QBLOCK
1475 } else {
1476 QBLOCK_FULL
1477 };
1478 let mut qb = vec![0f32; qblock.min(len) * hd];
1479 for qh in kvh * group..(kvh + 1) * group {
1480 let mut i0 = 0usize;
1481 while i0 < len {
1482 let i1 = (i0 + qblock).min(len);
1483 let nb = i1 - i0;
1484 let (k0, k1) = match window {
1485 Some(w) => (i0.saturating_sub(w), (i1 - 1 + w + 1).min(len)),
1486 None => (0, len),
1487 };
1488 let kr = k1 - k0;
1489 for i in 0..nb {
1490 qb[i * hd..(i + 1) * hd]
1491 .copy_from_slice(&q[(s0 + i0 + i) * qw + qh * hd..][..hd]);
1492 }
1493 let mut sc = vec![0f32; nb * kr];
1494 gemm_nt(
1495 &qb[..nb * hd],
1496 &kh[k0 * hd..k1 * hd],
1497 &mut sc,
1498 nb,
1499 hd,
1500 kr,
1501 None,
1502 );
1503 let sp = Shared(sc.as_mut_ptr());
1505 rows(inner, nb, &|s, e| {
1506 for i in s..e {
1507 let row = unsafe { sp.at(i * kr, kr) };
1508 let qi = i0 + i;
1509 let (lo, hi) = match window {
1510 Some(w) => (qi.saturating_sub(w), (qi + w + 1).min(len)),
1511 None => (0, len),
1512 };
1513 let (lo, hi) = (lo - k0, hi - k0);
1514 softmax(&mut row[lo..hi]);
1515 row[..lo].iter_mut().for_each(|x| *x = 0.0);
1516 row[hi..].iter_mut().for_each(|x| *x = 0.0);
1517 }
1518 });
1519 let mut ob = vec![0f32; nb * hd];
1521 gemm_nt_strided(&sc, &vt[k0..], len, &mut ob, nb, kr, hd);
1522 for i in 0..nb {
1523 let o = unsafe { op.at((s0 + i0 + i) * qw + qh * hd, hd) };
1525 o.copy_from_slice(&ob[i * hd..(i + 1) * hd]);
1526 }
1527 i0 = i1;
1528 }
1529 }
1530 };
1531 if accelerate() || long.len() < 2 {
1532 for &(s0, len, kvh) in &long {
1533 attend(s0, len, kvh, pool);
1534 }
1535 } else {
1536 rows(pool, long.len(), &|s, e| {
1537 for &(s0, len, kvh) in &long[s..e] {
1538 attend(s0, len, kvh, None);
1539 }
1540 });
1541 }
1542 prof::add(&prof::ATTN, t_core);
1543 l.o.apply(&out, n, pool)
1544 }
1545}
1546
1547#[cfg(test)]
1548mod tests {
1549 use super::*;
1550
1551 #[test]
1552 fn matryoshka_truncates_and_renormalizes() {
1553 let mut v: Vec<f32> = (0..768)
1554 .map(|i| ((i * 37 % 101) as f32 - 50.0) / 7.0)
1555 .collect();
1556 l2_normalize(&mut v);
1557 for d in MATRYOSHKA_DIMS {
1558 let t = matryoshka(&v, d).unwrap();
1559 assert_eq!(t.len(), d);
1560 let n: f64 = t.iter().map(|&x| (x as f64) * (x as f64)).sum();
1561 assert!((n - 1.0).abs() < 1e-6, "dim {d}: |v|² = {n}");
1562 assert!(cosine(&t, &v[..d]) > 0.999_999);
1564 }
1565 assert_eq!(matryoshka(&v, 768).unwrap(), v);
1566 assert!(matryoshka(&v, 300).is_err());
1567 assert!(matryoshka(&v[..256], 512).is_err());
1568 }
1569
1570 #[test]
1573 fn portable_gemm_matches_naive() {
1574 let pool = crate::pool::Pool::new(4);
1575 let mut st = 7u64;
1576 let mut rnd = || {
1577 st = st
1578 .wrapping_mul(6364136223846793005)
1579 .wrapping_add(1442695040888963407);
1580 ((st >> 40) as f32 / (1u64 << 24) as f32) - 0.5
1581 };
1582 for (n, k, m) in [
1583 (1, 512, 1024),
1584 (3, 64, 200),
1585 (300, 96, 70),
1586 (517, 33, 129),
1587 (64, 512, 512),
1588 ] {
1589 let x: Vec<f32> = (0..n * k).map(|_| rnd()).collect();
1590 let w: Vec<f32> = (0..m * k).map(|_| rnd()).collect();
1591 let mut want = vec![0f32; n * m];
1592 for i in 0..n {
1593 for j in 0..m {
1594 want[i * m + j] = (0..k)
1595 .map(|t| x[i * k + t] as f64 * w[j * k + t] as f64)
1596 .sum::<f64>() as f32;
1597 }
1598 }
1599 for p in [None, Some(&pool)] {
1600 let mut y = vec![f32::NAN; n * m];
1601 gemm_nt_portable(&x, &w, &mut y, n, k, m, p);
1602 let err = y
1603 .iter()
1604 .zip(&want)
1605 .fold(0f32, |a, (g, w)| a.max((g - w).abs()));
1606 assert!(err < 1e-4, "{n}x{k}x{m}: {err}");
1607 }
1608 }
1609 let (n, k, m, ldw) = (5usize, 16usize, 7usize, 40usize);
1611 let x: Vec<f32> = (0..n * k).map(|_| rnd()).collect();
1612 let w: Vec<f32> = (0..m * ldw).map(|_| rnd()).collect();
1613 let mut y = vec![0f32; n * m];
1614 gemm_nt_strided(&x, &w, ldw, &mut y, n, k, m);
1615 for i in 0..n {
1616 for j in 0..m {
1617 let r: f32 = (0..k).map(|t| x[i * k + t] * w[j * ldw + t]).sum();
1618 assert!((y[i * m + j] - r).abs() < 1e-5);
1619 }
1620 }
1621 }
1622
1623 #[test]
1624 fn mean_pool_averages_rows() {
1625 let x = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1626 assert_eq!(mean_pool(&x, 3, 2), vec![3.0, 4.0]);
1627 assert_eq!(mean_pool(&x, 1, 2), vec![1.0, 2.0]);
1628 }
1629
1630 #[test]
1631 fn l2_normalize_unit_and_zero() {
1632 let mut v = vec![3.0f32, 4.0];
1633 l2_normalize(&mut v);
1634 assert_eq!(v, vec![0.6, 0.8]);
1635 let mut z = vec![0.0f32; 4];
1636 l2_normalize(&mut z);
1637 assert_eq!(z, vec![0.0; 4]);
1638 }
1639
1640 #[test]
1641 fn prompts_follow_sentence_transformers() {
1642 let p = Prompts::default();
1643 assert_eq!(
1644 p.format(
1645 "What causes the northern lights?",
1646 Some("SearchQuery"),
1647 None,
1648 None
1649 )
1650 .unwrap(),
1651 "task: search result | query: What causes the northern lights?"
1652 );
1653 assert_eq!(
1654 p.format("body", Some("Document"), None, None).unwrap(),
1655 "title: none | text: body"
1656 );
1657 assert_eq!(
1658 p.format("body", Some("Document"), Some("Aurora borealis"), None)
1659 .unwrap(),
1660 "title: Aurora borealis | text: body"
1661 );
1662 assert_eq!(
1664 p.format("body", None, Some("T"), None).unwrap(),
1665 "title: T | text: body"
1666 );
1667 assert_eq!(
1668 p.format("body", Some("document"), Some(""), None).unwrap(),
1669 "title: none | text: body"
1670 );
1671 assert_eq!(
1673 p.format("x", Some("sts"), None, None).unwrap(),
1674 "task: sentence similarity | query: x"
1675 );
1676 assert_eq!(
1677 p.format("x", Some("CodeRetrieval"), None, None).unwrap(),
1678 "task: code retrieval | query: x"
1679 );
1680 assert_eq!(p.format("plain", None, None, None).unwrap(), "plain");
1682 assert_eq!(
1684 p.format("x", Some("SearchQuery"), None, Some("custom: "))
1685 .unwrap(),
1686 "custom: x"
1687 );
1688 assert!(p.format("x", Some("NoSuchTask"), None, None).is_err());
1689 assert!(p.format("x", Some("SearchQuery"), Some("T"), None).is_err());
1690 }
1691
1692 #[test]
1693 fn prompts_read_the_st_config() {
1694 let cfg =
1695 serde_json::json!({"prompts": {"query": "q: ", "document": "title: none | text: "}});
1696 let p = Prompts::from_st_config(&cfg).unwrap();
1697 assert_eq!(p.get("query"), Some("q: "));
1698 assert_eq!(
1699 p.format("b", Some("document"), Some("t"), None).unwrap(),
1700 "title: t | text: b"
1701 );
1702 assert!(Prompts::from_st_config(&serde_json::json!({})).is_none());
1703 }
1704
1705 #[test]
1706 fn wrap_ids_adds_specials_and_caps() {
1707 assert_eq!(wrap_ids(&[5, 6], 2, 1, 8192), vec![2, 5, 6, 1]);
1708 let long: Vec<u32> = (10..30).collect();
1709 let w = wrap_ids(&long, 2, 1, 8);
1710 assert_eq!(w.len(), 8);
1711 assert_eq!(w[0], 2);
1712 assert_eq!(*w.last().unwrap(), 1);
1713 assert_eq!(&w[1..7], &long[..6]);
1714 }
1715
1716 #[test]
1717 fn rope_rotates_halves() {
1718 let r = Rot::build(4, 8, 10000.0);
1719 let mut x = vec![1.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1720 r.apply(0, &mut x);
1721 assert_eq!(x[0], 1.0); r.apply(1, &mut x);
1723 assert!((x[0] - 1f32.cos()).abs() < 1e-6);
1725 assert!((x[4] - 1f32.sin()).abs() < 1e-6);
1726 }
1727
1728 #[test]
1729 fn softmax_is_a_distribution() {
1730 let mut r = vec![1.0f32, 2.0, 3.0, -50.0];
1731 softmax(&mut r);
1732 let s: f32 = r.iter().sum();
1733 assert!((s - 1.0).abs() < 1e-6);
1734 assert!(r[2] > r[1] && r[1] > r[0]);
1735 }
1736}