1use crate::builder::{
34 build_gemma_decode_graph_sized, build_gemma_decode_hir_dynamic_ext,
35 build_gemma_decode_hir_sized_ext, build_gemma_graph_sized_last_logits,
36 build_gemma_graph_sized_last_logits_hidden, build_gemma_prefill_hidden_hir_dynamic_ext,
37 build_gemma_prefill_hir_dynamic_ext,
38};
39use crate::config::GemmaConfig;
40use crate::rope::{resolve_inv_freq, rope_slice};
41use anyhow::{Context, Result};
42use rlx_core::autoregressive::{
43 KvCacheState, kv_from_prefill_outputs_per_layer, run_bucketed_kv_decode_hir_scratch,
44 split_decode_logits_kv, split_decode_logits_kv_aux,
45};
46use rlx_core::flow_bridge::compile_options_from_profile;
47use rlx_core::gpu_kv::{
48 GpuKvBinding, device_supports_gpu_kv, run_bucketed_kv_decode_gpu_hir, sync_gpu_kv_to_host,
49};
50use rlx_core::weight_loader::WeightLoader;
51use rlx_core::weight_map::WeightMap;
52use rlx_flow::CompileProfile;
53use rlx_ir::DimBinding;
54use rlx_ir::logical_kernel::KernelDispatchConfig;
55use rlx_qwen3::sampling::{SampleOpts, sample_token};
56use rlx_runtime::compile_cache::{
57 BucketedCompileCache, CacheRunInput, CompileCache, DynamicDimCompileCache,
58};
59use rlx_runtime::{CompileOptions, Device, Session};
60use std::collections::HashMap;
61use std::path::Path;
62
63pub fn decode_profile_for_device(device: Device) -> CompileProfile {
65 metal_decode_profile(device, CompileProfile::gemma_decode())
66}
67
68fn metal_thunk_decode_requested() -> bool {
72 std::env::var("RLX_GEMMA_METAL_THUNK_DECODE")
73 .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes"))
74}
75
76pub(crate) fn metal_decode_compile_guard<R, F>(device: Device, decode: bool, f: F) -> R
77where
78 F: FnOnce() -> R,
79{
80 if decode && metal_thunk_decode_requested() {
81 if device == Device::Metal {
82 rlx_ir::env::set("RLX_DISABLE_MPSGRAPH", "1");
83 let out = f();
84 rlx_ir::env::unset("RLX_DISABLE_MPSGRAPH");
85 out
86 } else {
87 f()
88 }
89 } else {
90 f()
91 }
92}
93
94fn metal_unfused_decode_requested() -> bool {
97 std::env::var("RLX_GEMMA_METAL_UNFUSED_DECODE")
98 .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes"))
99}
100
101fn metal_decode_profile(device: Device, mut profile: CompileProfile) -> CompileProfile {
102 if device == Device::Metal && metal_unfused_decode_requested() {
103 profile.fusion.skip = true;
104 profile.backend.metal.skip_fusion = true;
105 profile.backend.metal.unfuse_regions = true;
106 }
107 profile
108}
109
110pub struct GemmaGenerator {
116 cfg: GemmaConfig,
117 weights_cache: HashMap<String, (Vec<f32>, Vec<usize>)>,
122 tokens: Vec<u32>,
123 device: Device,
124 cache: Option<KvCacheState>,
128 prefill_compile_cache: Option<CompileCache>,
132 prefill_dynamic_cache: Option<DynamicDimCompileCache>,
134 embed_prefill_compile_cache: Option<CompileCache>,
136 embed_prefill_dynamic_cache: Option<DynamicDimCompileCache>,
137 decode_compile_cache: Option<BucketedCompileCache>,
143 decode_dynamic_cache: Option<DynamicDimCompileCache>,
144 inv_freq: Vec<f64>,
146 prefill_profile: CompileProfile,
148 decode_profile: CompileProfile,
150 pending_prefill_embeds: Option<Vec<f32>>,
152 pending_prefill_attn_bias: Option<Vec<f32>>,
153 use_gpu_kv: bool,
155 gpu_kv_binding: GpuKvBinding,
156 decode_scratch: DecodeKvScratch,
158 decode_inputs: DecodeInputScratch,
159 aux_hidden_layer_ids: Vec<usize>,
166 last_aux: Option<Vec<Vec<f32>>>,
170}
171
172#[derive(Default)]
174struct DecodeInputScratch {
175 mask: Vec<f32>,
176 cos: Vec<f32>,
177 sin: Vec<f32>,
178}
179
180#[derive(Default)]
182struct DecodeKvScratch {
183 padded_k: Vec<Vec<f32>>,
184 padded_v: Vec<Vec<f32>>,
185 bucket_upper: usize,
186}
187
188impl DecodeInputScratch {
189 fn fill_mask(&mut self, past_seq: usize, upper: usize) {
190 if self.mask.len() != upper + 1 {
191 self.mask.resize(upper + 1, 0.0);
192 }
193 for (i, m) in self.mask.iter_mut().enumerate().take(upper + 1) {
194 *m = if i < past_seq || i == upper { 1.0 } else { 0.0 };
195 }
196 }
197
198 fn fill_rope(&mut self, inv_freq: &[f64], pos: usize) {
199 let half = inv_freq.len();
200 self.cos.resize(half, 0.0);
201 self.sin.resize(half, 0.0);
202 for (i, &freq) in inv_freq.iter().enumerate() {
203 let angle = pos as f64 * freq;
204 let (s, c) = angle.sin_cos();
205 self.cos[i] = c as f32;
206 self.sin[i] = s as f32;
207 }
208 }
209}
210
211impl DecodeKvScratch {
212 fn ensure_bucket(&mut self, upper: usize, kv_dims: &[usize]) {
213 if self.bucket_upper == upper && self.padded_k.len() == kv_dims.len() {
214 return;
215 }
216 self.bucket_upper = upper;
217 self.padded_k = kv_dims.iter().map(|&d| vec![0.0_f32; upper * d]).collect();
218 self.padded_v = kv_dims.iter().map(|&d| vec![0.0_f32; upper * d]).collect();
219 }
220}
221
222fn gemma_use_gpu_kv(device: Device) -> bool {
223 if !device_supports_gpu_kv(device) {
224 return false;
225 }
226 match std::env::var("RLX_GEMMA_GPU_KV").ok().as_deref() {
227 Some("0") | Some("false") | Some("no") => false,
228 Some("1") | Some("true") | Some("yes") => true,
229 _ => false,
231 }
232}
233
234impl GemmaGenerator {
235 pub fn from_loader(
238 cfg: GemmaConfig,
239 loader: &mut dyn WeightLoader,
240 device: Device,
241 ) -> Result<Self> {
242 let keys = loader.remaining_keys();
243 let arch_hint: Option<String> = loader.arch_hint().map(|s| s.to_string());
249 let mut weights_cache = HashMap::with_capacity(keys.len());
250 for k in keys {
251 let v = loader
252 .take(&k)
253 .with_context(|| format!("draining weight {k}"))?;
254 let canonical = match arch_hint.as_deref() {
260 Some(a) => rlx_core::weight_loader::gguf_to_hf_name_for_arch(&k, a)
261 .unwrap_or_else(|| k.clone()),
262 None => rlx_core::weight_loader::gguf_to_hf_name(&k).unwrap_or_else(|| k.clone()),
263 };
264 weights_cache.insert(canonical, v);
265 }
266 let rope_factors = weights_cache
267 .get("rope_freqs.weight")
268 .map(|(d, _)| d.as_slice());
269 let inv_freq = resolve_inv_freq(&cfg, rope_factors);
270 Ok(Self {
271 cfg,
272 weights_cache,
273 tokens: Vec::new(),
274 device,
275 cache: None,
276 prefill_compile_cache: None,
277 prefill_dynamic_cache: None,
278 embed_prefill_compile_cache: None,
279 embed_prefill_dynamic_cache: None,
280 decode_compile_cache: None,
281 decode_dynamic_cache: None,
282 inv_freq,
283 prefill_profile: CompileProfile::gemma_prefill(),
284 decode_profile: metal_decode_profile(device, CompileProfile::gemma_decode()),
285 pending_prefill_embeds: None,
286 pending_prefill_attn_bias: None,
287 use_gpu_kv: gemma_use_gpu_kv(device),
288 gpu_kv_binding: GpuKvBinding::default(),
289 decode_scratch: DecodeKvScratch::default(),
290 decode_inputs: DecodeInputScratch::default(),
291 aux_hidden_layer_ids: Vec::new(),
292 last_aux: None,
293 })
294 }
295
296 pub fn set_aux_hidden_layer_ids(&mut self, ids: Vec<usize>) {
304 let mut v: Vec<usize> = ids
308 .into_iter()
309 .filter(|i| *i < self.cfg.num_hidden_layers)
310 .collect();
311 v.sort_unstable();
312 v.dedup();
313 self.aux_hidden_layer_ids = v;
314 self.last_aux = None;
315 }
316
317 pub fn take_last_aux(&mut self) -> Option<Vec<Vec<f32>>> {
319 self.last_aux.take()
320 }
321
322 pub fn aux_enabled(&self) -> bool {
324 !self.aux_hidden_layer_ids.is_empty()
325 }
326
327 fn reset_gpu_kv_binding(&mut self) {
328 self.gpu_kv_binding = GpuKvBinding::default();
329 }
330
331 pub fn from_loader_at(
334 cfg: GemmaConfig,
335 loader: &mut dyn WeightLoader,
336 device: Device,
337 weights_path: &Path,
338 ) -> Result<Self> {
339 let mut g = Self::from_loader(cfg, loader, device)?;
340 g.prefill_profile = crate::gemma_profile_near_weights(weights_path, false);
341 g.decode_profile = metal_decode_profile(
342 device,
343 crate::gemma_profile_near_weights(weights_path, true),
344 );
345 Ok(g)
346 }
347
348 pub fn with_compile_profiles(
350 mut self,
351 prefill: CompileProfile,
352 decode: CompileProfile,
353 ) -> Self {
354 self.prefill_profile = prefill;
355 self.decode_profile = metal_decode_profile(self.device, decode);
356 self
357 }
358
359 pub fn prefill_profile(&self) -> &CompileProfile {
360 &self.prefill_profile
361 }
362
363 pub fn decode_profile(&self) -> &CompileProfile {
364 &self.decode_profile
365 }
366
367 fn profile_compile_options(&self, decode: bool) -> CompileOptions {
368 let profile = if decode {
369 &self.decode_profile
370 } else {
371 &self.prefill_profile
372 };
373 compile_options_from_profile(profile, self.device, KernelDispatchConfig::default())
374 }
375
376 fn compile_graph_profiled(
377 &self,
378 session: &Session,
379 graph: rlx_ir::Graph,
380 ) -> Result<rlx_runtime::CompiledGraph> {
381 let opts = self.profile_compile_options(false);
382 Ok(session.compile_with(graph, &opts))
383 }
384
385 fn compile_graph_profiled_decode(
386 &self,
387 session: &Session,
388 graph: rlx_ir::Graph,
389 ) -> Result<rlx_runtime::CompiledGraph> {
390 Ok(metal_decode_compile_guard(self.device, true, || {
391 session.compile_with(graph, &self.profile_compile_options(true))
392 }))
393 }
394
395 pub fn with_prefill_cache(mut self, capacity: usize) -> Self {
400 self.prefill_compile_cache = Some(CompileCache::new(self.device, capacity));
401 self.embed_prefill_compile_cache = Some(CompileCache::new(self.device, capacity));
402 self.prefill_dynamic_cache = None;
403 self.embed_prefill_dynamic_cache = None;
404 self
405 }
406
407 pub fn with_dynamic_prefill_cache(mut self, capacity: usize) -> Self {
409 self.prefill_dynamic_cache = Some(DynamicDimCompileCache::new(self.device, capacity));
410 self.embed_prefill_dynamic_cache = Some(DynamicDimCompileCache::new(self.device, capacity));
411 self.prefill_compile_cache = None;
412 self.embed_prefill_compile_cache = None;
413 self
414 }
415
416 pub fn with_decode_cache(mut self, max_past: usize) -> Self {
426 let cache = BucketedCompileCache::power_of_two_ladder(
427 self.device,
428 1,
429 max_past.max(1) as u64,
430 );
431 self.decode_compile_cache = Some(cache);
432 self.decode_dynamic_cache = None;
433 self
434 }
435
436 pub fn with_dynamic_decode_cache(mut self, capacity: usize) -> Self {
438 self.decode_dynamic_cache = Some(DynamicDimCompileCache::new(self.device, capacity));
439 self.decode_compile_cache = None;
440 self
441 }
442
443 fn inference_dynamic_decode() -> bool {
444 std::env::var("RLX_GEMMA_DYNAMIC_DECODE").is_ok_and(|v| {
445 v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes")
446 })
447 }
448
449 pub fn with_inference_caches(mut self, max_seq: usize) -> Self {
453 let decode_horizon = max_seq.saturating_add(16).max(32);
456 self = self.with_dynamic_prefill_cache(16);
457 if Self::inference_dynamic_decode() {
458 self.with_dynamic_decode_cache(32)
459 } else {
460 self.with_decode_cache(decode_horizon)
461 }
462 }
463
464 pub fn sync_device(&mut self) {
467 if let Some(c) = &mut self.prefill_compile_cache {
468 c.sync_all();
469 }
470 if let Some(c) = &mut self.embed_prefill_compile_cache {
471 c.sync_all();
472 }
473 if let Some(c) = &mut self.prefill_dynamic_cache {
474 c.sync_all();
475 }
476 if let Some(c) = &mut self.embed_prefill_dynamic_cache {
477 c.sync_all();
478 }
479 if let Some(c) = &mut self.decode_compile_cache {
480 c.sync_all();
481 }
482 if let Some(c) = &mut self.decode_dynamic_cache {
483 c.sync_all();
484 }
485 rlx_runtime::device_ext::drain_device(self.device);
486 }
487
488 pub fn from_path(cfg: GemmaConfig, path: &str, device: Device) -> Result<Self> {
491 let mut loader = rlx_core::weight_loader::load_from_path(path)?;
492 Self::from_loader(cfg, loader.as_mut(), device)
493 }
494
495 pub fn from_path_with_mtp(
503 cfg: GemmaConfig,
504 path: &str,
505 device: Device,
506 include_mtp: bool,
507 ) -> Result<Self> {
508 if path.ends_with(".gguf") {
512 let mut gguf = rlx_core::weight_loader::GgufLoader::from_file(path)?;
513 gguf.include_mtp(include_mtp);
514 Self::from_loader(cfg, &mut gguf, device)
515 } else {
516 Self::from_path(cfg, path, device)
517 }
518 }
519
520 pub fn prefill(&mut self, prompt_ids: &[u32]) {
524 self.tokens.clear();
525 self.tokens.extend_from_slice(prompt_ids);
526 self.cache = None;
527 self.reset_gpu_kv_binding();
528 }
529
530 pub fn prefill_from_embeds(
533 &mut self,
534 prompt_ids: &[u32],
535 embeds: &[f32],
536 attn_bias: Option<Vec<f32>>,
537 ) -> Result<()> {
538 let h = self.cfg.hidden_size;
539 if embeds.len() != prompt_ids.len() * h {
540 anyhow::bail!(
541 "prefill_from_embeds: embeds len {} != {} tokens × hidden {}",
542 embeds.len(),
543 prompt_ids.len(),
544 h
545 );
546 }
547 if let Some(ref bias) = attn_bias {
548 let seq = prompt_ids.len();
549 let nh = self.cfg.num_attention_heads;
550 let expected = seq * seq * nh;
551 if bias.len() != expected {
552 anyhow::bail!(
553 "prefill_from_embeds: attn_bias len {} != batch×heads×seq² ({expected})",
554 bias.len()
555 );
556 }
557 }
558 self.prefill(prompt_ids);
559 self.pending_prefill_embeds = Some(embeds.to_vec());
560 self.pending_prefill_attn_bias = attn_bias;
561 Ok(())
562 }
563
564 pub fn weights_cache(&self) -> &HashMap<String, (Vec<f32>, Vec<usize>)> {
566 &self.weights_cache
567 }
568
569 pub fn step(&mut self, opts: SampleOpts) -> Result<u32> {
573 if self.tokens.is_empty() {
574 anyhow::bail!("step() called with empty token history; call prefill() first");
575 }
576 let seq = self.tokens.len();
577 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
578 let (graph, params) = build_gemma_graph_sized_last_logits(
579 &self.cfg, &mut wm, 1, seq, false,
580 )?;
581 let session = Session::new(self.device);
582 let mut compiled = self.compile_graph_profiled(&session, graph)?;
583 for (name, data) in ¶ms {
584 compiled.set_param(name, data);
585 }
586 let ids_f32: Vec<f32> = self.tokens.iter().map(|&i| i as f32).collect();
587 let outputs = compiled.run(&[("input_ids", ids_f32.as_slice())]);
588 let logits = outputs
589 .into_iter()
590 .next()
591 .context("compiled.run returned no outputs")?;
592
593 let vocab = self.cfg.vocab_size;
594 let expected = vocab;
595 if logits.len() < expected {
596 anyhow::bail!(
597 "logits length {} < expected {} (last logits, seq {seq}, vocab {vocab})",
598 logits.len(),
599 expected
600 );
601 }
602 let last_row = &logits[..vocab];
604 let tok = sample_token(last_row, opts) as u32;
605 self.tokens.push(tok);
606 Ok(tok)
607 }
608
609 pub fn generate(&mut self, n: usize, opts: SampleOpts) -> Result<Vec<u32>> {
612 let start = self.tokens.len();
613 for _ in 0..n {
614 self.step(opts)?;
615 }
616 Ok(self.tokens[start..].to_vec())
617 }
618
619 pub fn step_cached(&mut self, opts: SampleOpts) -> Result<u32> {
629 if self.tokens.is_empty() {
630 anyhow::bail!("step_cached() called with empty token history; call prefill() first");
631 }
632 if self.cache.is_none() {
633 let tok = self.seed_cache_from_prompt(opts)?;
637 return Ok(tok);
638 }
639 let cache = self.cache.as_ref().unwrap();
640 let past_seq = cache.past_len;
641 if self.tokens.len() <= past_seq {
642 anyhow::bail!(
643 "cache invariant violated: tokens.len() {} <= past_len {}",
644 self.tokens.len(),
645 past_seq
646 );
647 }
648 let input_tok = self.tokens[past_seq];
649
650 let aux_active = self.aux_enabled();
654 let (logits, new_k, new_v) = if aux_active {
655 let (logits, k, v, aux) = self.decode_step_oneshot_with_aux(past_seq, input_tok)?;
656 self.last_aux = Some(aux);
657 (logits, k, v)
658 } else if self.decode_dynamic_cache.is_some() {
659 self.decode_step_dynamic(past_seq, input_tok)?
660 } else if self.decode_compile_cache.is_some()
661 && self
662 .decode_compile_cache
663 .as_ref()
664 .unwrap()
665 .bucket_for(past_seq as u64)
666 .is_some()
667 {
668 self.decode_step_bucketed(past_seq, input_tok)?
669 } else {
670 self.decode_step_oneshot(past_seq, input_tok)?
671 };
672
673 let cache_mut = self.cache.as_mut().unwrap();
674 cache_mut.past_len = past_seq + 1;
675 cache_mut.layers_k = new_k;
676 cache_mut.layers_v = new_v;
677
678 let vocab = self.cfg.vocab_size;
679 if logits.len() != vocab {
680 anyhow::bail!("decode logits length {} != vocab {}", logits.len(), vocab);
681 }
682 let tok = sample_token(&logits, opts) as u32;
683 self.tokens.push(tok);
684 Ok(tok)
685 }
686
687 #[allow(clippy::type_complexity)]
690 fn decode_step_oneshot(
691 &mut self,
692 past_seq: usize,
693 input_tok: u32,
694 ) -> Result<(Vec<f32>, Vec<Vec<f32>>, Vec<Vec<f32>>)> {
695 let cache = self.cache.as_ref().unwrap();
696
697 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
698 let (graph, params) =
699 build_gemma_decode_graph_sized(&self.cfg, &mut wm, 1, past_seq)?;
700 let session = Session::new(self.device);
701 let mut compiled = self.compile_graph_profiled_decode(&session, graph)?;
702 for (name, data) in ¶ms {
703 compiled.set_param(name, data);
704 }
705
706 let input_ids_f32 = [input_tok as f32];
707 let key_strs: Vec<String> = (0..self.cfg.num_hidden_layers)
708 .flat_map(|i| [format!("past_k_{i}"), format!("past_v_{i}")])
709 .collect();
710 let mut inputs: Vec<(&str, &[f32])> =
711 Vec::with_capacity(1 + 2 * self.cfg.num_hidden_layers);
712 inputs.push(("input_ids", input_ids_f32.as_slice()));
713 for i in 0..self.cfg.num_hidden_layers {
714 inputs.push((&key_strs[2 * i], cache.layers_k[i].as_slice()));
715 inputs.push((&key_strs[2 * i + 1], cache.layers_v[i].as_slice()));
716 }
717
718 let outputs = compiled.run(&inputs);
719 split_decode_logits_kv(outputs, self.cfg.num_hidden_layers)
720 }
721
722 #[allow(clippy::type_complexity)]
728 fn decode_step_oneshot_with_aux(
729 &mut self,
730 past_seq: usize,
731 input_tok: u32,
732 ) -> Result<(Vec<f32>, Vec<Vec<f32>>, Vec<Vec<f32>>, Vec<Vec<f32>>)> {
733 let cache = self.cache.as_ref().unwrap();
734 let aux_ids = self.aux_hidden_layer_ids.clone();
735 let num_aux = aux_ids.len();
736
737 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
738 let opts = crate::flow::GemmaDecodeOpts {
739 batch: 1,
740 past_seq,
741 dynamic_past: false,
742 use_custom_mask: false,
743 profile: None,
744 aux_hidden_layer_ids: aux_ids,
745 };
746 let (graph, params) = crate::flow::build_gemma_decode_graph(&self.cfg, &mut wm, &opts)?;
747 let session = Session::new(self.device);
748 let mut compiled = self.compile_graph_profiled_decode(&session, graph)?;
749 for (name, data) in ¶ms {
750 compiled.set_param(name, data);
751 }
752
753 let input_ids_f32 = [input_tok as f32];
754 let key_strs: Vec<String> = (0..self.cfg.num_hidden_layers)
755 .flat_map(|i| [format!("past_k_{i}"), format!("past_v_{i}")])
756 .collect();
757 let mut inputs: Vec<(&str, &[f32])> =
758 Vec::with_capacity(1 + 2 * self.cfg.num_hidden_layers);
759 inputs.push(("input_ids", input_ids_f32.as_slice()));
760 for i in 0..self.cfg.num_hidden_layers {
761 inputs.push((&key_strs[2 * i], cache.layers_k[i].as_slice()));
762 inputs.push((&key_strs[2 * i + 1], cache.layers_v[i].as_slice()));
763 }
764
765 let outputs = compiled.run(&inputs);
766 split_decode_logits_kv_aux(outputs, self.cfg.num_hidden_layers, num_aux)
767 }
768
769 #[allow(clippy::type_complexity)]
770 fn decode_step_dynamic(
771 &mut self,
772 past_seq: usize,
773 input_tok: u32,
774 ) -> Result<(Vec<f32>, Vec<Vec<f32>>, Vec<Vec<f32>>)> {
775 let cache = self.cache.as_ref().unwrap();
776 let binding = DimBinding::batch_past_seq(1, past_seq);
777 let opts = self
778 .profile_compile_options(true)
779 .dim_binding(binding.clone());
780 let cache_dyn = self
781 .decode_dynamic_cache
782 .as_mut()
783 .ok_or_else(|| anyhow::anyhow!("dynamic decode without cache"))?;
784 let needs_upload = !cache_dyn.contains(past_seq as u64);
785 let cfg = self.cfg.clone();
786 let weights_cache = self.weights_cache.clone();
787 let max_past = self.cfg.max_position_embeddings;
788 let compiled = metal_decode_compile_guard(self.device, true, || {
789 cache_dyn.get_or_specialize(
790 past_seq as u64,
791 &binding,
792 || {
793 let mut wm = WeightMap::from_tensors(weights_cache);
794 build_gemma_decode_hir_dynamic_ext(&cfg, &mut wm, 1, max_past)
795 .expect("dynamic decode HIR")
796 .0
797 },
798 &opts,
799 )
800 })?;
801 if needs_upload {
802 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
803 let (_, params) = build_gemma_decode_hir_dynamic_ext(&self.cfg, &mut wm, 1, max_past)?;
804 for (name, data) in ¶ms {
805 compiled.set_param(name, data);
806 }
807 }
808
809 let (cos, sin) = compute_rope_slice(&self.inv_freq, past_seq);
810 let input_ids_f32 = [input_tok as f32];
811 let key_strs: Vec<String> = (0..self.cfg.num_hidden_layers)
812 .flat_map(|i| [format!("past_k_{i}"), format!("past_v_{i}")])
813 .collect();
814 let mut inputs: Vec<(&str, &[f32])> =
815 Vec::with_capacity(3 + 2 * self.cfg.num_hidden_layers);
816 inputs.push(("input_ids", input_ids_f32.as_slice()));
817 inputs.push(("rope_cos", cos.as_slice()));
818 inputs.push(("rope_sin", sin.as_slice()));
819 for i in 0..self.cfg.num_hidden_layers {
820 inputs.push((&key_strs[2 * i], cache.layers_k[i].as_slice()));
821 inputs.push((&key_strs[2 * i + 1], cache.layers_v[i].as_slice()));
822 }
823 let outputs = compiled.run(&inputs);
824 split_decode_logits_kv(outputs, self.cfg.num_hidden_layers)
825 }
826
827 #[allow(clippy::type_complexity)]
828 fn decode_step_bucketed(
829 &mut self,
830 past_seq: usize,
831 input_tok: u32,
832 ) -> Result<(Vec<f32>, Vec<Vec<f32>>, Vec<Vec<f32>>)> {
833 let kv_dims = self.per_layer_kv_dims();
834 let n_layers = self.cfg.num_hidden_layers;
835 let decode_opts = self.profile_compile_options(true);
836 let upper = self
837 .decode_compile_cache
838 .as_ref()
839 .and_then(|cache_dec| {
840 cache_dec.bucket_for(past_seq as u64).map(|idx| {
841 cache_dec
842 .buckets()
843 .nth(idx)
844 .map(|r| (r.end - 1) as usize)
845 .unwrap_or(past_seq)
846 })
847 })
848 .unwrap_or(past_seq);
849
850 self.decode_scratch.ensure_bucket(upper, &kv_dims);
851 self.decode_inputs.fill_mask(past_seq, upper);
852 self.decode_inputs.fill_rope(&self.inv_freq, past_seq);
853
854 let input_ids_f32 = [input_tok as f32];
855 let fixed = [
856 CacheRunInput {
857 name: "input_ids",
858 data: &input_ids_f32,
859 row_inner: None,
860 },
861 CacheRunInput {
862 name: "rope_cos",
863 data: &self.decode_inputs.cos,
864 row_inner: None,
865 },
866 CacheRunInput {
867 name: "rope_sin",
868 data: &self.decode_inputs.sin,
869 row_inner: None,
870 },
871 CacheRunInput {
872 name: "mask",
873 data: &self.decode_inputs.mask,
874 row_inner: None,
875 },
876 ];
877
878 if self.use_gpu_kv && self.decode_compile_cache.is_some() {
879 let key = past_seq as u64;
880 let upper_u = upper as u64;
881 let prev_upper = self.gpu_kv_binding.upper;
882 let bucket_changed = prev_upper != 0 && prev_upper != upper_u;
883 let handles_live = self
884 .decode_compile_cache
885 .as_mut()
886 .and_then(|c| c.compiled_for_key_mut(key))
887 .map(|cg| cg.has_gpu_handle("past_k_0"))
888 .unwrap_or(false);
889 let refresh_kv = matches!(self.device, Device::Gpu | Device::Metal)
890 || bucket_changed
891 || !handles_live;
892 let cfg = self.cfg.clone();
893 let weights = self.weights_cache.clone();
894 let logits = {
895 let cache_dec = self.decode_compile_cache.as_mut().unwrap();
896 let cache_mut = self.cache.as_mut().unwrap();
897 metal_decode_compile_guard(self.device, true, || {
898 run_bucketed_kv_decode_gpu_hir(
899 cache_dec,
900 key,
901 past_seq,
902 cache_mut,
903 &mut self.gpu_kv_binding,
904 self.cfg.kv_proj_dim(),
905 n_layers,
906 &fixed,
907 move |upper| {
908 let mut wm = WeightMap::from_tensors(weights.clone());
909 build_gemma_decode_hir_sized_ext(&cfg, &mut wm, 1, upper as usize, true)
910 .expect("gemma bucketed decode HIR")
911 },
912 &decode_opts,
913 refresh_kv,
914 )
915 })?
916 };
917 if let Some(compiled) = self
918 .decode_compile_cache
919 .as_mut()
920 .and_then(|c| c.compiled_for_key_mut(key))
921 {
922 let cache_mut = self.cache.as_mut().unwrap();
923 sync_gpu_kv_to_host(compiled, cache_mut, self.cfg.kv_proj_dim(), n_layers)?;
924 }
925 let next_key = (past_seq + 1) as u64;
926 let next_upper = self
927 .decode_compile_cache
928 .as_ref()
929 .and_then(|cache| {
930 cache
931 .bucket_for(next_key)
932 .and_then(|idx| cache.buckets().nth(idx).map(|r| (r.end - 1) as usize))
933 })
934 .unwrap_or(upper);
935 if next_upper != upper {
936 self.reset_gpu_kv_binding();
937 }
938 let cache_mut = self.cache.as_ref().unwrap();
939 let new_k = cache_mut.layers_k.clone();
940 let new_v = cache_mut.layers_v.clone();
941 return Ok((logits, new_k, new_v));
942 }
943
944 let cfg = self.cfg.clone();
945 let weights = self.weights_cache.clone();
946 let cache_dec = self.decode_compile_cache.as_mut().unwrap();
947 let kv_cache = self.cache.as_ref().unwrap();
948 let DecodeKvScratch {
949 padded_k, padded_v, ..
950 } = &mut self.decode_scratch;
951 metal_decode_compile_guard(self.device, true, || {
952 run_bucketed_kv_decode_hir_scratch(
953 cache_dec,
954 past_seq,
955 kv_cache,
956 &kv_dims,
957 n_layers,
958 padded_k,
959 padded_v,
960 &fixed,
961 |upper| {
962 let mut wm = WeightMap::from_tensors(weights.clone());
963 build_gemma_decode_hir_sized_ext(&cfg, &mut wm, 1, upper as usize, true)
964 .expect("gemma bucketed decode HIR")
965 },
966 &decode_opts,
967 )
968 })
969 }
970
971 #[allow(clippy::unnecessary_unwrap)]
975 fn run_prefill_with_cache(
976 &mut self,
977 batch: usize,
978 seq: usize,
979 ids_f32: &[f32],
980 ) -> Result<Vec<Vec<f32>>> {
981 if self.prefill_dynamic_cache.is_some() {
982 let binding = DimBinding::batch_seq(batch, seq);
983 let opts = compile_options_from_profile(
984 &self.prefill_profile,
985 self.device,
986 KernelDispatchConfig::default(),
987 )
988 .dim_binding(binding.clone());
989 let cache = self.prefill_dynamic_cache.as_mut().expect("checked");
990 let needs_upload = !cache.contains(seq as u64);
991 let cfg = self.cfg.clone();
992 let weights_cache = self.weights_cache.clone();
993 let max_seq = self.cfg.max_position_embeddings;
994 let compiled = cache.get_or_specialize(
995 seq as u64,
996 &binding,
997 || {
998 let mut wm = WeightMap::from_tensors(weights_cache);
999 build_gemma_prefill_hir_dynamic_ext(&cfg, &mut wm, batch, max_seq, true)
1000 .expect("dynamic prefill HIR")
1001 .0
1002 },
1003 &opts,
1004 )?;
1005 if needs_upload {
1006 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
1007 let (_, params) =
1008 build_gemma_prefill_hir_dynamic_ext(&self.cfg, &mut wm, batch, max_seq, true)?;
1009 for (name, data) in ¶ms {
1010 compiled.set_param(name, data);
1011 }
1012 }
1013 let last_idx = vec![(seq - 1) as f32];
1014 Ok(compiled.run(&[("input_ids", ids_f32), ("last_token_idx", &last_idx)]))
1015 } else if self.prefill_compile_cache.is_some() {
1016 let key = ((batch as u64) << 32) | (seq as u64);
1017 let opts = self.profile_compile_options(false);
1018 if !self.prefill_compile_cache.as_ref().unwrap().contains(key) {
1019 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
1020 let (graph, params) = build_gemma_graph_sized_last_logits(
1021 &self.cfg, &mut wm, batch, seq, true,
1022 )?;
1023 {
1024 let compiled = self
1025 .prefill_compile_cache
1026 .as_mut()
1027 .unwrap()
1028 .get_or_compile_with_options(key, || graph, &opts);
1029 for (name, data) in ¶ms {
1030 compiled.set_param(name, data);
1031 }
1032 }
1033 }
1034 let compiled = self
1035 .prefill_compile_cache
1036 .as_mut()
1037 .unwrap()
1038 .get_or_compile_with_options(key, || unreachable!("just populated above"), &opts);
1039 Ok(compiled.run(&[("input_ids", ids_f32)]))
1040 } else {
1041 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
1042 let (graph, params) = build_gemma_graph_sized_last_logits(
1043 &self.cfg, &mut wm, batch, seq, true,
1044 )?;
1045 let session = Session::new(self.device);
1046 let opts = self.profile_compile_options(false);
1047 let mut compiled = session.compile_with(graph, &opts);
1048 for (name, data) in ¶ms {
1049 compiled.set_param(name, data);
1050 }
1051 Ok(compiled.run(&[("input_ids", ids_f32)]))
1052 }
1053 }
1054
1055 fn run_prefill_hidden_with_cache(
1056 &mut self,
1057 batch: usize,
1058 seq: usize,
1059 hidden: &[f32],
1060 attn_bias: Option<&[f32]>,
1061 ) -> Result<Vec<Vec<f32>>> {
1062 if self.cfg.use_bidirectional_vision() && attn_bias.is_none() {
1063 anyhow::bail!(
1064 "multimodal prefill requires attn_bias when use_bidirectional_attention=vision"
1065 );
1066 }
1067 let mut inputs: Vec<(&str, &[f32])> = vec![("prefill_hidden", hidden)];
1068 if let Some(bias) = attn_bias {
1069 inputs.push(("attn_bias", bias));
1070 }
1071 let embed_compile_opts = self.profile_compile_options(false);
1072 if let Some(cache) = &mut self.embed_prefill_dynamic_cache {
1073 let binding = DimBinding::batch_seq(batch, seq);
1074 let opts = compile_options_from_profile(
1075 &self.prefill_profile,
1076 self.device,
1077 KernelDispatchConfig::default(),
1078 )
1079 .dim_binding(binding.clone());
1080 let needs_upload = !cache.contains(seq as u64);
1081 let cfg = self.cfg.clone();
1082 let weights_cache = self.weights_cache.clone();
1083 let max_seq = self.cfg.max_position_embeddings;
1084 let compiled = cache.get_or_specialize(
1085 seq as u64,
1086 &binding,
1087 || {
1088 let mut wm = WeightMap::from_tensors(weights_cache);
1089 build_gemma_prefill_hidden_hir_dynamic_ext(&cfg, &mut wm, batch, max_seq, true)
1090 .expect("dynamic hidden prefill HIR")
1091 .0
1092 },
1093 &opts,
1094 )?;
1095 if needs_upload {
1096 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
1097 let (_, params) = build_gemma_prefill_hidden_hir_dynamic_ext(
1098 &self.cfg, &mut wm, batch, max_seq, true,
1099 )?;
1100 for (name, data) in ¶ms {
1101 compiled.set_param(name, data);
1102 }
1103 }
1104 let last_idx = vec![(seq - 1) as f32];
1105 let mut dyn_inputs = inputs.clone();
1106 dyn_inputs.push(("last_token_idx", &last_idx));
1107 Ok(compiled.run(&dyn_inputs))
1108 } else if let Some(cache) = &mut self.embed_prefill_compile_cache {
1109 let key = ((batch as u64) << 32) | (seq as u64);
1110 let opts = &embed_compile_opts;
1111 if !cache.contains(key) {
1112 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
1113 let (graph, params) = build_gemma_graph_sized_last_logits_hidden(
1114 &self.cfg, &mut wm, batch, seq, true,
1115 )?;
1116 {
1117 let compiled = cache.get_or_compile_with_options(key, || graph, opts);
1118 for (name, data) in ¶ms {
1119 compiled.set_param(name, data);
1120 }
1121 }
1122 }
1123 let compiled = cache.get_or_compile_with_options(
1124 key,
1125 || unreachable!("just populated above"),
1126 opts,
1127 );
1128 Ok(compiled.run(&inputs))
1129 } else {
1130 let mut wm = WeightMap::from_tensors(self.weights_cache.clone());
1131 let (graph, params) =
1132 build_gemma_graph_sized_last_logits_hidden(&self.cfg, &mut wm, batch, seq, true)?;
1133 let session = Session::new(self.device);
1134 let opts = self.profile_compile_options(false);
1135 let mut compiled = session.compile_with(graph, &opts);
1136 for (name, data) in ¶ms {
1137 compiled.set_param(name, data);
1138 }
1139 Ok(compiled.run(&inputs))
1140 }
1141 }
1142
1143 pub fn generate_from_embeds(
1145 &mut self,
1146 prompt_ids: &[u32],
1147 embeds: &[f32],
1148 n: usize,
1149 opts: SampleOpts,
1150 ) -> Result<Vec<u32>> {
1151 self.generate_from_embeds_with_bias(prompt_ids, embeds, None, n, opts)
1152 }
1153
1154 pub fn generate_from_embeds_with_bias(
1155 &mut self,
1156 prompt_ids: &[u32],
1157 embeds: &[f32],
1158 attn_bias: Option<Vec<f32>>,
1159 n: usize,
1160 opts: SampleOpts,
1161 ) -> Result<Vec<u32>> {
1162 self.prefill_from_embeds(prompt_ids, embeds, attn_bias)?;
1163 self.generate_cached(n, opts)
1164 }
1165
1166 pub fn generate_from_embeds_with(
1168 &mut self,
1169 prompt_ids: &[u32],
1170 embeds: &[f32],
1171 n: usize,
1172 opts: SampleOpts,
1173 on_token: impl FnMut(u32),
1174 ) -> Result<Vec<u32>> {
1175 self.generate_from_embeds_with_bias_and_callback(
1176 prompt_ids, embeds, None, n, opts, on_token,
1177 )
1178 }
1179
1180 pub fn generate_from_embeds_with_bias_and_callback(
1181 &mut self,
1182 prompt_ids: &[u32],
1183 embeds: &[f32],
1184 attn_bias: Option<Vec<f32>>,
1185 n: usize,
1186 opts: SampleOpts,
1187 on_token: impl FnMut(u32),
1188 ) -> Result<Vec<u32>> {
1189 self.prefill_from_embeds(prompt_ids, embeds, attn_bias)?;
1190 self.generate_cached_with(n, opts, on_token)
1191 }
1192
1193 pub fn generate_cached(&mut self, n: usize, opts: SampleOpts) -> Result<Vec<u32>> {
1195 self.generate_cached_with(n, opts, |_| {})
1196 }
1197
1198 pub fn generate_cached_with(
1205 &mut self,
1206 n: usize,
1207 opts: SampleOpts,
1208 mut on_token: impl FnMut(u32),
1209 ) -> Result<Vec<u32>> {
1210 let start = self.tokens.len();
1211 for _ in 0..n {
1212 let tok = self.step_cached(opts)?;
1213 on_token(tok);
1214 }
1215 Ok(self.tokens[start..].to_vec())
1216 }
1217
1218 fn seed_cache_from_prompt(&mut self, opts: SampleOpts) -> Result<u32> {
1223 let seq = self.tokens.len();
1224 let batch = 1usize;
1225 let kv_dims = self.per_layer_kv_dims();
1226
1227 let outputs = if let Some(embeds) = self.pending_prefill_embeds.take() {
1228 let bias = self.pending_prefill_attn_bias.take();
1229 self.run_prefill_hidden_with_cache(batch, seq, &embeds, bias.as_deref())?
1230 } else {
1231 let ids_f32: Vec<f32> = self.tokens.iter().map(|&i| i as f32).collect();
1232 self.run_prefill_with_cache(batch, seq, &ids_f32)?
1233 };
1234 let (logits, kv) = kv_from_prefill_outputs_per_layer(
1235 outputs,
1236 batch,
1237 seq,
1238 &kv_dims,
1239 self.cfg.num_hidden_layers,
1240 )?;
1241 self.cache = Some(kv);
1242
1243 let vocab = self.cfg.vocab_size;
1244 let needed = vocab;
1245 if logits.len() < needed {
1246 anyhow::bail!("prefill logits length {} < {}", logits.len(), needed);
1247 }
1248 let last_row = &logits[..vocab];
1249 let tok = sample_token(last_row, opts) as u32;
1250 self.tokens.push(tok);
1251 Ok(tok)
1252 }
1253
1254 pub fn tokens(&self) -> &[u32] {
1256 &self.tokens
1257 }
1258
1259 pub fn config(&self) -> &GemmaConfig {
1260 &self.cfg
1261 }
1262
1263 pub fn prefill_get_last_logits(&mut self, context: &[u32]) -> Result<Vec<f32>> {
1271 if context.is_empty() {
1272 anyhow::bail!("prefill_get_last_logits: empty context");
1273 }
1274 self.tokens.clear();
1275 self.tokens.extend_from_slice(context);
1276 self.cache = None;
1277 self.reset_gpu_kv_binding();
1278
1279 let seq = context.len();
1280 let batch = 1usize;
1281 let kv_dims = self.per_layer_kv_dims();
1282
1283 let ids_f32: Vec<f32> = context.iter().map(|&i| i as f32).collect();
1284 let outputs = self.run_prefill_with_cache(batch, seq, &ids_f32)?;
1285 let (logits, kv) = kv_from_prefill_outputs_per_layer(
1286 outputs,
1287 batch,
1288 seq,
1289 &kv_dims,
1290 self.cfg.num_hidden_layers,
1291 )?;
1292 self.cache = Some(kv);
1293
1294 let vocab = self.cfg.vocab_size;
1295 let needed = vocab;
1296 if logits.len() < needed {
1297 anyhow::bail!("logits short: {} < {}", logits.len(), needed);
1298 }
1299 Ok(logits[..vocab].to_vec())
1300 }
1301
1302 pub fn decode_get_logits(&mut self, input: u32) -> Result<Vec<f32>> {
1310 if self.cache.is_none() {
1311 anyhow::bail!(
1312 "decode_get_logits: cache not seeded; call prefill_get_last_logits first"
1313 );
1314 }
1315 self.tokens.push(input);
1316 let seq = self.tokens.len();
1317 let batch = 1usize;
1318 let kv_dims = self.per_layer_kv_dims();
1319 let ids_f32: Vec<f32> = self.tokens.iter().map(|&i| i as f32).collect();
1320 let outputs = self.run_prefill_with_cache(batch, seq, &ids_f32)?;
1321 let (logits, kv) = kv_from_prefill_outputs_per_layer(
1322 outputs,
1323 batch,
1324 seq,
1325 &kv_dims,
1326 self.cfg.num_hidden_layers,
1327 )?;
1328 self.cache = Some(kv);
1329 let vocab = self.cfg.vocab_size;
1330 Ok(logits[..vocab].to_vec())
1331 }
1332
1333 fn per_layer_kv_dims(&self) -> Vec<usize> {
1336 (0..self.cfg.num_hidden_layers)
1337 .map(|i| self.cfg.layer_num_kv_heads(i) * self.cfg.layer_head_dim(i))
1338 .collect()
1339 }
1340}
1341
1342impl Drop for GemmaGenerator {
1343 fn drop(&mut self) {
1344 if self.device == Device::Metal {
1345 self.sync_device();
1346 }
1347 }
1348}
1349
1350fn compute_rope_slice(inv_freq: &[f64], pos: usize) -> (Vec<f32>, Vec<f32>) {
1354 rope_slice(inv_freq, pos)
1355}
1356
1357#[cfg(test)]
1358mod tests {
1359 use super::*;
1360 use crate::config::GemmaConfig;
1361 use crate::rope::{build_rope_tables, resolve_inv_freq, rope_slice};
1362 use rlx_flow::CompileProfile;
1363
1364 fn tiny_cfg() -> GemmaConfig {
1365 let mut cfg = GemmaConfig::tiny_test();
1366 cfg.vocab_size = 16;
1367 cfg.head_dim = Some(8);
1368 cfg
1369 }
1370
1371 fn synthetic_tensors(cfg: &GemmaConfig) -> HashMap<String, (Vec<f32>, Vec<usize>)> {
1372 let h = cfg.hidden_size;
1373 let q_dim = cfg.q_proj_dim();
1374 let kv_dim = cfg.kv_proj_dim();
1375 let int_dim = cfg.intermediate_size;
1376 let mut t: HashMap<String, (Vec<f32>, Vec<usize>)> = HashMap::new();
1377 let pat = |n: usize, salt: u32| -> Vec<f32> {
1380 (0..n)
1381 .map(|i| {
1382 let x = ((i as u32).wrapping_mul(2654435761).wrapping_add(salt)) >> 8;
1383 (x as f32 / (1u32 << 24) as f32) - 0.5
1384 })
1385 .collect()
1386 };
1387 t.insert(
1388 "model.embed_tokens.weight".into(),
1389 (pat(cfg.vocab_size * h, 1), vec![cfg.vocab_size, h]),
1390 );
1391 for i in 0..cfg.num_hidden_layers {
1392 let lp = format!("model.layers.{i}");
1393 t.insert(
1394 format!("{lp}.input_layernorm.weight"),
1395 (pat(h, 100 + i as u32), vec![h]),
1396 );
1397 t.insert(
1398 format!("{lp}.post_attention_layernorm.weight"),
1399 (pat(h, 200 + i as u32), vec![h]),
1400 );
1401 t.insert(
1402 format!("{lp}.self_attn.q_proj.weight"),
1403 (pat(q_dim * h, 300 + i as u32), vec![q_dim, h]),
1404 );
1405 t.insert(
1406 format!("{lp}.self_attn.k_proj.weight"),
1407 (pat(kv_dim * h, 400 + i as u32), vec![kv_dim, h]),
1408 );
1409 t.insert(
1410 format!("{lp}.self_attn.v_proj.weight"),
1411 (pat(kv_dim * h, 500 + i as u32), vec![kv_dim, h]),
1412 );
1413 t.insert(
1414 format!("{lp}.self_attn.o_proj.weight"),
1415 (pat(h * q_dim, 600 + i as u32), vec![h, q_dim]),
1416 );
1417 t.insert(
1418 format!("{lp}.mlp.gate_proj.weight"),
1419 (pat(int_dim * h, 900 + i as u32), vec![int_dim, h]),
1420 );
1421 t.insert(
1422 format!("{lp}.mlp.up_proj.weight"),
1423 (pat(int_dim * h, 1000 + i as u32), vec![int_dim, h]),
1424 );
1425 t.insert(
1426 format!("{lp}.mlp.down_proj.weight"),
1427 (pat(h * int_dim, 1100 + i as u32), vec![h, int_dim]),
1428 );
1429 }
1430 t.insert("model.norm.weight".into(), (pat(h, 2000), vec![h]));
1431 t.insert(
1432 "lm_head.weight".into(),
1433 (pat(cfg.vocab_size * h, 3000), vec![cfg.vocab_size, h]),
1434 );
1435 t
1436 }
1437
1438 fn synthetic_weights(cfg: &GemmaConfig) -> WeightMap {
1439 WeightMap::from_tensors(synthetic_tensors(cfg))
1440 }
1441
1442 #[test]
1443 fn generator_drains_loader_and_runs_one_step() {
1444 let cfg = tiny_cfg();
1445 let mut wm = synthetic_weights(&cfg);
1446 let mut gn = GemmaGenerator::from_loader(cfg.clone(), &mut wm, Device::Cpu).unwrap();
1447 assert_eq!(wm.len(), 0, "loader should be drained");
1448 gn.prefill(&[1, 2, 3]);
1449 let t = gn.step(SampleOpts::greedy()).unwrap();
1450 assert!((t as usize) < cfg.vocab_size);
1451 assert_eq!(gn.tokens().len(), 4);
1452 }
1453
1454 #[test]
1455 fn generate_n_appends_n_tokens() {
1456 let cfg = tiny_cfg();
1457 let mut wm = synthetic_weights(&cfg);
1458 let mut gn = GemmaGenerator::from_loader(cfg.clone(), &mut wm, Device::Cpu).unwrap();
1459 gn.prefill(&[5, 6]);
1460 let new_tokens = gn.generate(3, SampleOpts::greedy()).unwrap();
1461 assert_eq!(new_tokens.len(), 3);
1462 assert_eq!(gn.tokens().len(), 5);
1463 for t in &new_tokens {
1464 assert!((*t as usize) < cfg.vocab_size);
1465 }
1466 }
1467
1468 #[test]
1469 fn step_cached_with_aux_populates_last_aux() {
1470 let cfg = tiny_cfg();
1475 let mut wm = synthetic_weights(&cfg);
1476 let mut gn = GemmaGenerator::from_loader(cfg.clone(), &mut wm, Device::Cpu).unwrap();
1477
1478 let n_layers = cfg.num_hidden_layers;
1483 let mut aux_ids: Vec<usize> = (0..n_layers).take(3).collect();
1484 aux_ids.sort_unstable();
1485 aux_ids.dedup();
1486 gn.set_aux_hidden_layer_ids(aux_ids.clone());
1487 assert!(gn.aux_enabled());
1488
1489 gn.prefill(&[1, 2, 3]);
1490 let _ = gn.step_cached(SampleOpts::greedy()).unwrap();
1494 assert!(
1495 gn.take_last_aux().is_none(),
1496 "prefill-seed call should not populate aux"
1497 );
1498 let _ = gn.step_cached(SampleOpts::greedy()).unwrap();
1500 let aux = gn.take_last_aux().expect("aux populated after decode step");
1501 assert_eq!(aux.len(), aux_ids.len(), "one row per aux id");
1502 for row in &aux {
1503 assert_eq!(row.len(), cfg.hidden_size, "aux row = hidden_size f32s");
1504 assert!(row.iter().all(|v| v.is_finite()));
1505 }
1506 assert!(
1507 gn.take_last_aux().is_none(),
1508 "take_last_aux consumes the buffer"
1509 );
1510
1511 let _ = gn.step_cached(SampleOpts::greedy()).unwrap();
1512 let aux2 = gn.take_last_aux().expect("aux on next step");
1513 assert_eq!(aux2.len(), aux_ids.len());
1514 }
1515
1516 #[test]
1517 fn aux_disabled_after_clear_ids() {
1518 let cfg = tiny_cfg();
1519 let mut wm = synthetic_weights(&cfg);
1520 let mut gn = GemmaGenerator::from_loader(cfg, &mut wm, Device::Cpu).unwrap();
1521 gn.set_aux_hidden_layer_ids(vec![0, 1]);
1522 assert!(gn.aux_enabled());
1523 gn.set_aux_hidden_layer_ids(Vec::new());
1524 assert!(!gn.aux_enabled());
1525 assert!(gn.take_last_aux().is_none());
1527 }
1528
1529 #[test]
1530 fn step_without_prefill_errors() {
1531 let cfg = tiny_cfg();
1532 let mut wm = synthetic_weights(&cfg);
1533 let mut gn = GemmaGenerator::from_loader(cfg, &mut wm, Device::Cpu).unwrap();
1534 let r = gn.step(SampleOpts::greedy());
1535 assert!(r.is_err());
1536 }
1537
1538 fn max_abs_diff(a: &[f32], b: &[f32]) -> f32 {
1539 a.iter()
1540 .zip(b.iter())
1541 .map(|(x, y)| (x - y).abs())
1542 .fold(0f32, f32::max)
1543 }
1544
1545 #[test]
1546 fn prefill_logits_unchanged_with_kv_export() {
1547 let cfg = tiny_cfg();
1548 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1549
1550 let mut wm_a = synthetic_weights(&cfg);
1551 let mut wm_b = synthetic_weights(&cfg);
1552 let (graph_a, params_a) =
1553 build_gemma_graph_sized_last_logits(&cfg, &mut wm_a, 1, 4, false).unwrap();
1554 let (graph_b, params_b) =
1555 build_gemma_graph_sized_last_logits(&cfg, &mut wm_b, 1, 4, true).unwrap();
1556 let session = Session::new(Device::Cpu);
1557 let opts = CompileOptions::new();
1558 let mut ca = session.compile_with(graph_a, &opts);
1559 let mut cb = session.compile_with(graph_b, &opts);
1560 for (n, d) in ¶ms_a {
1561 ca.set_param(n, d);
1562 }
1563 for (n, d) in ¶ms_b {
1564 cb.set_param(n, d);
1565 }
1566 let ids: Vec<f32> = prompt.iter().map(|&i| i as f32).collect();
1567 let la = ca.run(&[("input_ids", &ids)])[0].clone();
1568 let lb = cb.run(&[("input_ids", &ids)])[0].clone();
1569 let d = max_abs_diff(&la, &lb);
1570 assert!(d < 1e-5, "kv export changed prefill logits: max_abs={d:.6}");
1571 }
1572
1573 #[test]
1574 fn incremental_decode_logits_match_full_prefill() {
1575 let cfg = tiny_cfg();
1576 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1577
1578 let mut wm_a = synthetic_weights(&cfg);
1579 let mut gn_a = GemmaGenerator::from_loader(cfg.clone(), &mut wm_a, Device::Cpu).unwrap();
1580 let tok = gn_a
1581 .prefill_get_last_logits(&prompt)
1582 .map(|l| sample_token(&l, SampleOpts::greedy()) as u32)
1583 .unwrap();
1584
1585 let mut extended = prompt.clone();
1586 extended.push(tok);
1587
1588 let mut wm_b = synthetic_weights(&cfg);
1589 let mut gn_b = GemmaGenerator::from_loader(cfg.clone(), &mut wm_b, Device::Cpu).unwrap();
1590 let full = gn_b.prefill_get_last_logits(&extended).unwrap();
1591
1592 let mut wm_c = synthetic_weights(&cfg);
1593 let mut gn_c = GemmaGenerator::from_loader(cfg.clone(), &mut wm_c, Device::Cpu).unwrap();
1594 gn_c.prefill_get_last_logits(&prompt).unwrap();
1595 let incremental = gn_c.decode_get_logits(tok).unwrap();
1596
1597 let d = max_abs_diff(&full, &incremental);
1598 assert!(
1599 d < 1e-2,
1600 "decode+KV vs full prefill max_abs={d:.6} (tok={tok})"
1601 );
1602 }
1603
1604 fn run_prefill_kv(
1605 cfg: &GemmaConfig,
1606 wm: &mut WeightMap,
1607 seq: usize,
1608 ids: &[u32],
1609 ) -> Vec<Vec<f32>> {
1610 run_prefill_kv_with_options(cfg, wm, seq, ids, &kv_export_compile_options(true))
1611 }
1612
1613 fn kv_export_compile_options(prefill: bool) -> CompileOptions {
1614 let profile = if prefill {
1615 CompileProfile::gemma_prefill()
1616 } else {
1617 CompileProfile::gemma_decode()
1618 };
1619 compile_options_from_profile(&profile, Device::Cpu, KernelDispatchConfig::default())
1620 }
1621
1622 fn run_prefill_kv_with_options(
1623 cfg: &GemmaConfig,
1624 wm: &mut WeightMap,
1625 seq: usize,
1626 ids: &[u32],
1627 opts: &CompileOptions,
1628 ) -> Vec<Vec<f32>> {
1629 let ids_f32: Vec<f32> = ids.iter().map(|&i| i as f32).collect();
1630 let (graph, params) = build_gemma_graph_sized_last_logits(cfg, wm, 1, seq, true).unwrap();
1631 let session = Session::new(Device::Cpu);
1632 let mut compiled = session.compile_with(graph, opts);
1633 for (n, d) in ¶ms {
1634 compiled.set_param(n, d);
1635 }
1636 let outputs = compiled.run(&[("input_ids", &ids_f32)]);
1637 let n_layers = cfg.num_hidden_layers;
1638 assert_eq!(outputs.len(), 1 + 2 * n_layers);
1639 let mut kv = Vec::with_capacity(2 * n_layers);
1640 let mut iter = outputs.into_iter().skip(1);
1641 for _ in 0..n_layers {
1642 kv.push(iter.next().unwrap());
1643 kv.push(iter.next().unwrap());
1644 }
1645 kv
1646 }
1647
1648 #[test]
1649 fn decode_graph_bakes_rope_slice_length() {
1650 let cfg = tiny_cfg();
1651 let past_seq = 4usize;
1652 let half = cfg.head_dim() / 2;
1653 let mut wm = synthetic_weights(&cfg);
1654 let (_, params) = build_gemma_decode_graph_sized(&cfg, &mut wm, 1, past_seq).unwrap();
1655 let cos = params
1656 .get("decode.rope.cos")
1657 .expect("decode.rope.cos param");
1658 let sin = params
1659 .get("decode.rope.sin")
1660 .expect("decode.rope.sin param");
1661 assert_eq!(
1662 cos.len(),
1663 half,
1664 "cos param should be one row (half={half}), got {}",
1665 cos.len()
1666 );
1667 assert_eq!(sin.len(), half);
1668 for key in params.keys() {
1669 assert!(
1670 !key.starts_with("rope."),
1671 "decode graph must not include prefill rope table param {key}"
1672 );
1673 }
1674 let inv = resolve_inv_freq(&cfg, None);
1675 let (c_ref, s_ref) = rope_slice(&inv, past_seq);
1676 let d = max_abs_diff(cos, &c_ref) + max_abs_diff(sin, &s_ref);
1677 assert!(d < 1e-6, "baked rope mismatch: {d}");
1678 }
1679
1680 #[test]
1681 fn decode_graph_all_rope_use_baked_cos() {
1682 use rlx_ir::Op;
1683 let cfg = tiny_cfg();
1684 let mut wm = synthetic_weights(&cfg);
1685 let (graph, _) = build_gemma_decode_graph_sized(&cfg, &mut wm, 1, 4).unwrap();
1686 for node in graph.nodes() {
1687 if let Op::Rope { .. } = &node.op {
1688 let cos_id = node.inputs[1];
1689 let cos_node = &graph.node(cos_id);
1690 match &cos_node.op {
1691 Op::Param { name } => assert_eq!(
1692 name, "decode.rope.cos",
1693 "decode RoPE must use baked decode.rope.cos, got {name}"
1694 ),
1695 other => panic!("decode RoPE cos input is {other:?}, expected Param"),
1696 }
1697 }
1698 }
1699 }
1700
1701 #[test]
1702 fn decode_graph_rope_cos_is_single_row() {
1703 use rlx_ir::Op;
1704 let cfg = tiny_cfg();
1705 let past_seq = 4usize;
1706 let half = cfg.head_dim() / 2;
1707 let mut wm = synthetic_weights(&cfg);
1708 let (graph, _) = build_gemma_decode_graph_sized(&cfg, &mut wm, 1, past_seq).unwrap();
1709 let mut rope_cos_lens = Vec::new();
1710 for node in graph.nodes() {
1711 if let Op::Rope { .. } = &node.op {
1712 let cos_shape = &graph.node(node.inputs[1]).shape;
1713 let rows = if cos_shape.rank() >= 2 {
1714 cos_shape.dim(0).unwrap_static()
1715 } else {
1716 1
1717 };
1718 rope_cos_lens.push(rows);
1719 }
1720 }
1721 assert!(!rope_cos_lens.is_empty(), "decode graph has no RoPE nodes");
1722 for rows in &rope_cos_lens {
1723 assert_eq!(
1724 *rows, 1,
1725 "decode RoPE cos must be single-row [1, half], got {rows} rows"
1726 );
1727 }
1728 assert_eq!(half, cfg.head_dim() / 2);
1729 }
1730
1731 #[test]
1732 fn prefill_kv_matches_extended_prefix() {
1733 let cfg = tiny_cfg();
1734 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1735 let tok = 6u32;
1736 let mut extended = prompt.clone();
1737 extended.push(tok);
1738
1739 let mut wm_prompt = synthetic_weights(&cfg);
1740 let prompt_kv = run_prefill_kv(&cfg, &mut wm_prompt, 4, &prompt);
1741 let mut wm_ext = synthetic_weights(&cfg);
1742 let ext_kv = run_prefill_kv(&cfg, &mut wm_ext, 5, &extended);
1743
1744 let kv_dim = cfg.kv_proj_dim();
1745 for layer in 0..cfg.num_hidden_layers {
1746 let k_prompt = &prompt_kv[2 * layer];
1747 let k_ext = &ext_kv[2 * layer];
1748 let prefix_len = 4 * kv_dim;
1749 assert_eq!(k_prompt.len(), prefix_len);
1750 assert_eq!(k_ext.len(), 5 * kv_dim);
1751 let d = max_abs_diff(k_prompt, &k_ext[..prefix_len]);
1752 assert!(
1753 d < 1e-4,
1754 "layer {layer} prefill K prefix vs extended K max_abs={d:.6}"
1755 );
1756 }
1757 }
1758
1759 #[test]
1760 fn decode_rope_slice_matches_prefill_table_row() {
1761 let cfg = tiny_cfg();
1762 let inv = resolve_inv_freq(&cfg, None);
1763 let (cos_tab, sin_tab) = build_rope_tables(&inv, cfg.max_position_embeddings);
1764 let half = inv.len();
1765 for pos in [3usize, 4, 5] {
1766 let (c, s) = rope_slice(&inv, pos);
1767 let off = pos * half;
1768 let d = max_abs_diff(&c, &cos_tab[off..off + half])
1769 + max_abs_diff(&s, &sin_tab[off..off + half]);
1770 assert!(d < 1e-6, "rope_slice mismatch at pos {pos}: {d}");
1771 }
1772 }
1773
1774 #[test]
1775 fn prefill_kv_export_correct_with_fusion() {
1776 let cfg = tiny_cfg();
1777 let tok = 6u32;
1778 let ids = [1u32, 2, 3, 5, tok];
1779 let opts = kv_export_compile_options(true);
1780 let mut wm_one = synthetic_weights(&cfg);
1781 let one_kv = run_prefill_kv_with_options(&cfg, &mut wm_one, 1, &[tok], &opts);
1782 let mut wm_ext = synthetic_weights(&cfg);
1783 let ext_kv = run_prefill_kv_with_options(&cfg, &mut wm_ext, 5, &ids, &opts);
1784 let kv_dim = cfg.kv_proj_dim();
1785 let d = max_abs_diff(&ext_kv[1][4 * kv_dim..], &one_kv[1][..kv_dim]);
1786 assert!(d < 1e-4, "KV export mismatch with profile fusion: {d:.6}");
1787
1788 let mut wm_default = synthetic_weights(&cfg);
1789 let default_kv =
1790 run_prefill_kv_with_options(&cfg, &mut wm_default, 5, &ids, &CompileOptions::new());
1791 let d_default = max_abs_diff(&default_kv[1][4 * kv_dim..], &one_kv[1][..kv_dim]);
1792 assert!(
1793 d_default < 1e-4,
1794 "KV export mismatch with default fusion (got {d_default:.6})"
1795 );
1796 }
1797
1798 #[test]
1799 fn decode_oneshot_kv_suffix_matches_extended() {
1800 let cfg = tiny_cfg();
1801 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1802 let tok = 6u32;
1803 let mut extended = prompt.clone();
1804 extended.push(tok);
1805
1806 let opts = kv_export_compile_options(false);
1807 let mut wm_ext = synthetic_weights(&cfg);
1808 let ext_kv = run_prefill_kv_with_options(&cfg, &mut wm_ext, 5, &extended, &opts);
1809
1810 let mut wm = synthetic_weights(&cfg);
1811 let mut gn = GemmaGenerator::from_loader(cfg.clone(), &mut wm, Device::Cpu).unwrap();
1812 gn.prefill_get_last_logits(&prompt).unwrap();
1813
1814 let mut wm_d = synthetic_weights(&cfg);
1815 let (graph, params) = build_gemma_decode_graph_sized(&cfg, &mut wm_d, 1, 4).unwrap();
1816 let session = Session::new(Device::Cpu);
1817 let mut compiled = session.compile_with(graph, &opts);
1818 for (n, d) in ¶ms {
1819 compiled.set_param(n, d);
1820 }
1821 let cache = gn.cache.as_ref().unwrap();
1822 let key_strs: Vec<String> = (0..cfg.num_hidden_layers)
1823 .flat_map(|i| [format!("past_k_{i}"), format!("past_v_{i}")])
1824 .collect();
1825 let input_ids = [tok as f32];
1826 let mut inputs: Vec<(&str, &[f32])> = vec![("input_ids", input_ids.as_slice())];
1827 for i in 0..cfg.num_hidden_layers {
1828 inputs.push((&key_strs[2 * i], cache.layers_k[i].as_slice()));
1829 inputs.push((&key_strs[2 * i + 1], cache.layers_v[i].as_slice()));
1830 }
1831 let outputs = compiled.run(&inputs);
1832 let kv_dim = cfg.kv_proj_dim();
1833 let k_dec = &outputs[1][4 * kv_dim..];
1834
1835 let d = max_abs_diff(k_dec, &ext_kv[0][4 * kv_dim..]);
1836 assert!(
1837 d < 1e-3,
1838 "decode oneshot layer0 K suffix vs extended max_abs={d:.6}"
1839 );
1840 }
1841
1842 #[test]
1843 fn decode_logits_match_extended_prefill_after_one_token() {
1844 let cfg = tiny_cfg();
1845 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1846 let tok = 6u32;
1847
1848 let mut extended = prompt.clone();
1849 extended.push(tok);
1850
1851 let mut wm_a = synthetic_weights(&cfg);
1852 let mut gn_a = GemmaGenerator::from_loader(cfg.clone(), &mut wm_a, Device::Cpu).unwrap();
1853 let full = gn_a.prefill_get_last_logits(&extended).unwrap();
1854
1855 let mut wm_b = synthetic_weights(&cfg);
1856 let mut gn_b = GemmaGenerator::from_loader(cfg.clone(), &mut wm_b, Device::Cpu).unwrap();
1857 gn_b.prefill_get_last_logits(&prompt).unwrap();
1858 let inc = gn_b.decode_get_logits(tok).unwrap();
1859
1860 let d = max_abs_diff(&full, &inc);
1861 assert!(d < 1e-2, "decode vs extended prefill max_abs={d:.6}");
1862 }
1863
1864 #[test]
1865 fn cached_second_token_matches_naive() {
1866 let cfg = tiny_cfg();
1867 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1868
1869 let mut wm_n = synthetic_weights(&cfg);
1870 let mut gn_n = GemmaGenerator::from_loader(cfg.clone(), &mut wm_n, Device::Cpu).unwrap();
1871 gn_n.prefill(&prompt);
1872 let n0 = gn_n.step(SampleOpts::greedy()).unwrap();
1873 let n1 = gn_n.step(SampleOpts::greedy()).unwrap();
1874
1875 let mut wm_c = synthetic_weights(&cfg);
1876 let mut gn_c = GemmaGenerator::from_loader(cfg.clone(), &mut wm_c, Device::Cpu).unwrap();
1877 gn_c.prefill(&prompt);
1878 let c = gn_c.generate_cached(2, SampleOpts::greedy()).unwrap();
1879
1880 assert_eq!(c[0], n0, "first generated token");
1881 assert_eq!(c[1], n1, "second generated token (decode step)");
1882 }
1883
1884 #[test]
1885 fn cached_matches_naive_on_greedy() {
1886 let cfg = tiny_cfg();
1893 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1894 let steps = 4;
1895
1896 let mut wm_n = synthetic_weights(&cfg);
1897 let mut gn_naive =
1898 GemmaGenerator::from_loader(cfg.clone(), &mut wm_n, Device::Cpu).unwrap();
1899 gn_naive.prefill(&prompt);
1900 let naive_tokens = gn_naive.generate(steps, SampleOpts::greedy()).unwrap();
1901
1902 let mut wm_c = synthetic_weights(&cfg);
1903 let mut gn_cached =
1904 GemmaGenerator::from_loader(cfg.clone(), &mut wm_c, Device::Cpu).unwrap();
1905 gn_cached.prefill(&prompt);
1906 let cached_tokens = gn_cached
1907 .generate_cached(steps, SampleOpts::greedy())
1908 .unwrap();
1909
1910 assert_eq!(
1911 cached_tokens, naive_tokens,
1912 "cached vs naive token mismatch — KV cache or kernel-Lq!=Lk bug"
1913 );
1914 }
1915
1916 #[test]
1917 fn cached_step_advances_cache_invariant() {
1918 let cfg = tiny_cfg();
1919 let mut wm = synthetic_weights(&cfg);
1920 let mut gn = GemmaGenerator::from_loader(cfg.clone(), &mut wm, Device::Cpu).unwrap();
1921 gn.prefill(&[1, 2, 3]);
1922 let _ = gn.step_cached(SampleOpts::greedy()).unwrap();
1923 assert_eq!(gn.tokens().len(), 4);
1925 assert_eq!(gn.cache.as_ref().unwrap().past_len, 3);
1926 let _ = gn.step_cached(SampleOpts::greedy()).unwrap();
1927 assert_eq!(gn.tokens().len(), 5);
1929 assert_eq!(gn.cache.as_ref().unwrap().past_len, 4);
1930 }
1931
1932 #[test]
1933 fn bucketed_decode_matches_oneshot() {
1934 let cfg = tiny_cfg();
1940 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1941 let steps = 6;
1942
1943 let mut wm_one = synthetic_weights(&cfg);
1944 let mut gn_one =
1945 GemmaGenerator::from_loader(cfg.clone(), &mut wm_one, Device::Cpu).unwrap();
1946 gn_one.prefill(&prompt);
1947 let oneshot_tokens = gn_one.generate_cached(steps, SampleOpts::greedy()).unwrap();
1948
1949 let mut wm_buc = synthetic_weights(&cfg);
1950 let mut gn_buc = GemmaGenerator::from_loader(cfg.clone(), &mut wm_buc, Device::Cpu)
1951 .unwrap()
1952 .with_decode_cache(32);
1953 gn_buc.prefill(&prompt);
1954 let bucketed_tokens = gn_buc.generate_cached(steps, SampleOpts::greedy()).unwrap();
1955
1956 assert_eq!(
1957 bucketed_tokens, oneshot_tokens,
1958 "bucketed-cache decode diverged from one-shot decode — \
1959 mask, padding, or output-slice bug"
1960 );
1961 }
1962
1963 #[test]
1964 fn prefill_compile_cache_does_not_change_output() {
1965 let cfg = tiny_cfg();
1966 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1967 let mut wm_a = synthetic_weights(&cfg);
1968 let mut gn_a = GemmaGenerator::from_loader(cfg.clone(), &mut wm_a, Device::Cpu).unwrap();
1969 gn_a.prefill(&prompt);
1970 let a = gn_a.generate_cached(4, SampleOpts::greedy()).unwrap();
1971
1972 let mut wm_b = synthetic_weights(&cfg);
1973 let mut gn_b = GemmaGenerator::from_loader(cfg.clone(), &mut wm_b, Device::Cpu)
1974 .unwrap()
1975 .with_prefill_cache(4);
1976 gn_b.prefill(&prompt);
1977 let b = gn_b.generate_cached(4, SampleOpts::greedy()).unwrap();
1978
1979 assert_eq!(a, b, "enabling prefill_cache must not change output");
1980 }
1981
1982 #[test]
1983 fn dynamic_decode_matches_oneshot() {
1984 let cfg = tiny_cfg();
1985 let prompt: Vec<u32> = vec![1, 2, 3, 5];
1986 let steps = 6;
1987
1988 let mut wm_one = synthetic_weights(&cfg);
1989 let mut gn_one =
1990 GemmaGenerator::from_loader(cfg.clone(), &mut wm_one, Device::Cpu).unwrap();
1991 gn_one.prefill(&prompt);
1992 let oneshot_tokens = gn_one.generate_cached(steps, SampleOpts::greedy()).unwrap();
1993
1994 let mut wm_dyn = synthetic_weights(&cfg);
1995 let mut gn_dyn = GemmaGenerator::from_loader(cfg.clone(), &mut wm_dyn, Device::Cpu)
1996 .unwrap()
1997 .with_dynamic_decode_cache(8);
1998 gn_dyn.prefill(&prompt);
1999 let dynamic_tokens = gn_dyn.generate_cached(steps, SampleOpts::greedy()).unwrap();
2000
2001 assert_eq!(
2002 dynamic_tokens, oneshot_tokens,
2003 "dynamic past_seq decode diverged from one-shot decode"
2004 );
2005 }
2006
2007 #[test]
2008 fn dynamic_prefill_matches_oneshot() {
2009 let cfg = tiny_cfg();
2010 let prompt: Vec<u32> = vec![1, 2, 3, 5];
2011 let steps = 4;
2012
2013 let mut wm_one = synthetic_weights(&cfg);
2014 let mut gn_one =
2015 GemmaGenerator::from_loader(cfg.clone(), &mut wm_one, Device::Cpu).unwrap();
2016 gn_one.prefill(&prompt);
2017 let oneshot_tokens = gn_one.generate_cached(steps, SampleOpts::greedy()).unwrap();
2018
2019 let mut wm_dyn = synthetic_weights(&cfg);
2020 let mut gn_dyn = GemmaGenerator::from_loader(cfg.clone(), &mut wm_dyn, Device::Cpu)
2021 .unwrap()
2022 .with_dynamic_prefill_cache(8);
2023 gn_dyn.prefill(&prompt);
2024 let dynamic_tokens = gn_dyn.generate_cached(steps, SampleOpts::greedy()).unwrap();
2025
2026 assert_eq!(
2027 dynamic_tokens, oneshot_tokens,
2028 "dynamic seq prefill diverged from one-shot prefill"
2029 );
2030 }
2031
2032 #[test]
2033 fn dynamic_prefill_and_decode_matches_oneshot() {
2034 let cfg = tiny_cfg();
2035 let prompt: Vec<u32> = vec![1, 2, 3, 5];
2036 let steps = 6;
2037
2038 let mut wm_one = synthetic_weights(&cfg);
2039 let mut gn_one =
2040 GemmaGenerator::from_loader(cfg.clone(), &mut wm_one, Device::Cpu).unwrap();
2041 gn_one.prefill(&prompt);
2042 let oneshot_tokens = gn_one.generate_cached(steps, SampleOpts::greedy()).unwrap();
2043
2044 let mut wm_dyn = synthetic_weights(&cfg);
2045 let mut gn_dyn = GemmaGenerator::from_loader(cfg.clone(), &mut wm_dyn, Device::Cpu)
2046 .unwrap()
2047 .with_dynamic_prefill_cache(8)
2048 .with_dynamic_decode_cache(8);
2049 gn_dyn.prefill(&prompt);
2050 let dynamic_tokens = gn_dyn.generate_cached(steps, SampleOpts::greedy()).unwrap();
2051
2052 assert_eq!(
2053 dynamic_tokens, oneshot_tokens,
2054 "dynamic prefill+decode diverged from one-shot path"
2055 );
2056 }
2057
2058 #[test]
2059 fn greedy_is_deterministic_across_runs() {
2060 let cfg = tiny_cfg();
2061 let weights = synthetic_weights(&cfg);
2062 let mk = || {
2063 let mut wm = WeightMap::from_tensors(weights_as_hashmap(&weights));
2064 GemmaGenerator::from_loader(cfg.clone(), &mut wm, Device::Cpu).unwrap()
2065 };
2066 let mut a = mk();
2067 let mut b = mk();
2068 a.prefill(&[1, 2, 3]);
2069 b.prefill(&[1, 2, 3]);
2070 let ta = a.generate(4, SampleOpts::greedy()).unwrap();
2071 let tb = b.generate(4, SampleOpts::greedy()).unwrap();
2072 assert_eq!(ta, tb);
2073 }
2074
2075 fn weights_as_hashmap(wm: &WeightMap) -> HashMap<String, (Vec<f32>, Vec<usize>)> {
2076 let _ = wm; let cfg = tiny_cfg();
2082 let mut new = synthetic_weights(&cfg);
2083 let keys: Vec<String> = new.keys().map(|s| s.to_string()).collect();
2084 let mut out = HashMap::new();
2085 for k in keys {
2086 out.insert(k.clone(), new.take(&k).unwrap());
2087 }
2088 out
2089 }
2090}