Skip to main content

runtime/models_v2/
jamba.rs

1//! Jamba Model V2 - Hybrid Mamba-Transformer with MoE
2//!
3//! This implements the Jamba architecture which features:
4//! - Interleaved Mamba and Attention layers
5//! - Mixture of Experts (MoE) in the MLP layers
6//! - State-space layers for efficient long-context processing
7//! - Attention layers for high-quality modeling
8//!
9//! Supports: AI21's Jamba models
10
11use crate::model_config;
12use super::traits::*;
13use anyhow::Result;
14use serde::{Serialize, Deserialize};
15
16/// Jamba model configuration using the model_config macro
17model_config!(JambaConfig {
18    vocab_size: usize = 65536,
19    hidden_size: usize = 4096,
20    intermediate_size: usize = 14336,
21    num_hidden_layers: usize = 32,
22    num_attention_heads: usize = 32,
23    num_key_value_heads: usize = 8,
24    hidden_act: String = "silu".to_string(),
25    max_position_embeddings: usize = 262144,
26    initializer_range: f32 = 0.02,
27    rms_norm_eps: f32 = 1e-6,
28    use_cache: bool = true,
29    pad_token_id: i64 = 0,
30    bos_token_id: i64 = 1,
31    eos_token_id: i64 = 2,
32    tie_word_embeddings: bool = false,
33    rope_theta: f32 = 10000.0,
34    attention_dropout: f32 = 0.0,
35    // Mamba parameters
36    d_state: usize = 16,
37    d_conv: usize = 4,
38    expand: usize = 2,
39    // MoE parameters
40    num_experts: usize = 16,
41    num_experts_per_tok: usize = 2,
42    // Hybrid architecture parameters
43    attn_layer_period: usize = 8,       // Attention layer every N layers
44    attn_layer_offset: usize = 4,       // Offset for first attention layer
45    use_mamba_preprocessing: bool = true,
46    mamba_d_inner: usize = 0,           // 0 = auto: hidden_size * expand
47    mamba_dt_rank: usize = 0,           // 0 = auto: ceil(hidden_size/16)
48});
49
50impl JambaConfig {
51    /// Create JambaConfig from GGUF model configuration
52    pub fn from_gguf_config(gguf: &crate::weight_loader_core::GGUFModelConfig) -> Self {
53        Self {
54            vocab_size: gguf.vocab_size,
55            hidden_size: gguf.hidden_size,
56            intermediate_size: gguf.intermediate_size,
57            num_hidden_layers: gguf.num_hidden_layers,
58            num_attention_heads: gguf.num_attention_heads,
59            num_key_value_heads: gguf.num_key_value_heads,
60            rms_norm_eps: gguf.rms_norm_eps,
61            rope_theta: gguf.rope_theta,
62            max_position_embeddings: gguf.max_position_embeddings,
63            ..Default::default()
64        }
65    }
66
67    /// Check if layer at index should use attention (vs Mamba)
68    pub fn is_attention_layer(&self, layer_idx: usize) -> bool {
69        (layer_idx + self.attn_layer_offset) % self.attn_layer_period == 0
70    }
71
72    /// Get the effective Mamba d_inner dimension
73    pub fn effective_mamba_d_inner(&self) -> usize {
74        if self.mamba_d_inner > 0 {
75            self.mamba_d_inner
76        } else {
77            self.hidden_size * self.expand
78        }
79    }
80
81    /// Get the effective Mamba dt_rank
82    pub fn effective_mamba_dt_rank(&self) -> usize {
83        if self.mamba_dt_rank > 0 {
84            self.mamba_dt_rank
85        } else {
86            ((self.hidden_size as f32 / 16.0).ceil() as usize).max(1)
87        }
88    }
89}
90
91/// Main Jamba model implementation
92pub struct JambaModelV2 {
93    config: JambaConfig,
94    device: Device,
95    embed_tokens: Tensor,
96    layers: Vec<JambaLayer>,
97    norm: Tensor,
98    lm_head: Tensor,
99}
100
101/// Layer type enum for Jamba hybrid architecture
102pub enum JambaLayerType {
103    Mamba(JambaMambaBlock),
104    Attention(JambaAttentionBlock),
105}
106
107/// Jamba layer - can be either Mamba or Attention based
108pub struct JambaLayer {
109    layer_type: JambaLayerType,
110    moe: JambaMoE,
111    input_layernorm: Tensor,
112    post_layernorm: Tensor,
113    config: JambaConfig,
114}
115
116/// Jamba Mamba block (selective state space)
117pub struct JambaMambaBlock {
118    in_proj: Tensor,
119    conv1d_weight: Tensor,
120    conv1d_bias: Option<Tensor>,
121    x_proj: Tensor,
122    dt_proj: Tensor,
123    dt_proj_bias: Option<Tensor>,
124    a_log: Tensor,
125    d: Tensor,
126    out_proj: Tensor,
127    d_inner: usize,
128    d_state: usize,
129    d_conv: usize,
130    dt_rank: usize,
131}
132
133/// Jamba attention block
134pub struct JambaAttentionBlock {
135    q_proj: Tensor,
136    k_proj: Tensor,
137    v_proj: Tensor,
138    o_proj: Tensor,
139    num_heads: usize,
140    num_key_value_heads: usize,
141    head_dim: usize,
142    scale: f32,
143}
144
145/// Jamba MoE layer
146pub struct JambaMoE {
147    router: Tensor,
148    experts: Vec<JambaExpert>,
149    num_experts: usize,
150    num_experts_per_tok: usize,
151}
152
153/// Single expert in Jamba MoE
154pub struct JambaExpert {
155    gate_proj: Tensor,
156    up_proj: Tensor,
157    down_proj: Tensor,
158}
159
160impl Model for JambaModelV2 {
161    type Config = JambaConfig;
162
163    fn new(config: JambaConfig) -> Result<Self> {
164        let device = Device::CPU;
165
166        let embed_tokens = ops_fn::zeros(
167            &[config.vocab_size, config.hidden_size],
168            DataType::Float32,
169            &device
170        )?;
171
172        let norm = ops_fn::zeros(
173            &[config.hidden_size],
174            DataType::Float32,
175            &device
176        )?;
177
178        let lm_head = if config.tie_word_embeddings {
179            embed_tokens.clone()
180        } else {
181            ops_fn::zeros(
182                &[config.hidden_size, config.vocab_size],
183                DataType::Float32,
184                &device
185            )?
186        };
187
188        let mut layers = Vec::with_capacity(config.num_hidden_layers);
189        for i in 0..config.num_hidden_layers {
190            layers.push(JambaLayer::new(&config, i, &device)?);
191        }
192
193        Ok(Self {
194            config,
195            device,
196            embed_tokens,
197            layers,
198            norm,
199            lm_head,
200        })
201    }
202
203    fn from_weights(config: JambaConfig, weights: ModelWeights) -> Result<Self> {
204        let mut model = Self::new(config)?;
205
206        if let Some(w) = weights.get("model.embed_tokens.weight") {
207            model.embed_tokens = w.clone();
208        }
209
210        if let Some(w) = weights.get("model.norm.weight") {
211            model.norm = w.clone();
212        }
213
214        if let Some(w) = weights.get("lm_head.weight") {
215            model.lm_head = ops_fn::transpose(w)?;
216        }
217
218        for (i, layer) in model.layers.iter_mut().enumerate() {
219            layer.load_weights(&weights, i)?;
220        }
221
222        Ok(model)
223    }
224
225    fn forward(&self, inputs: &ModelInputs) -> Result<ModelOutputs> {
226        match inputs {
227            ModelInputs::Text { input_ids, attention_mask, .. } => {
228                let mut hidden_states = ops_fn::embedding(input_ids, &self.embed_tokens)?;
229
230                for layer in &self.layers {
231                    hidden_states = layer.forward(
232                        &hidden_states,
233                        attention_mask.as_ref(),
234                    )?;
235                }
236
237                hidden_states = ops_fn::rms_norm(&hidden_states, &self.norm, self.config.rms_norm_eps)?;
238                let logits = ops_fn::matmul(&hidden_states, &self.lm_head)?;
239
240                Ok(ModelOutputs::Logits {
241                    logits,
242                    hidden_states: None,
243                })
244            }
245            _ => Err(anyhow::anyhow!("Jamba model only supports text inputs")),
246        }
247    }
248
249    fn generate(&self, prompt: &str, config: &GenerationConfig) -> Result<String> {
250        use crate::tokenizer::Tokenizer;
251        use rand::Rng;
252
253        let tokenizer = Tokenizer::new();
254        let mut tokens: Vec<u32> = tokenizer.encode(prompt);
255
256        for _ in 0..config.max_new_tokens {
257            let input_ids = Tensor::from_i64_slice(
258                &tokens.iter().map(|&t| t as i64).collect::<Vec<_>>(),
259                &[1, tokens.len()],
260                &self.device
261            )?;
262
263            let inputs = ModelInputs::text(input_ids);
264            let outputs = self.forward(&inputs)?;
265
266            let logits = match outputs {
267                ModelOutputs::Logits { logits, .. } => logits,
268                _ => return Err(anyhow::anyhow!("Expected logits output")),
269            };
270
271            let logits_candle = logits.to_candle()?;
272            let shape = logits_candle.dims();
273            let last_logits = logits_candle
274                .narrow(1, shape[1] - 1, 1)?
275                .squeeze(0)?
276                .squeeze(0)?;
277
278            let logits_vec: Vec<f32> = last_logits.to_vec1()?;
279
280            let next_token = if config.do_sample && config.temperature > 0.0 {
281                let scaled: Vec<f32> = logits_vec.iter()
282                    .map(|&x| x / config.temperature)
283                    .collect();
284
285                let max_val = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
286                let exp_sum: f32 = scaled.iter().map(|&x| (x - max_val).exp()).sum();
287                let probs: Vec<f32> = scaled.iter()
288                    .map(|&x| (x - max_val).exp() / exp_sum)
289                    .collect();
290
291                let mut rng = rand::thread_rng();
292                let random_val: f32 = rng.gen();
293                let mut cumulative = 0.0;
294                let mut sampled = 0u32;
295
296                for (idx, &prob) in probs.iter().enumerate() {
297                    cumulative += prob;
298                    if random_val <= cumulative {
299                        sampled = idx as u32;
300                        break;
301                    }
302                }
303                sampled
304            } else {
305                let mut max_idx = 0;
306                let mut max_val = logits_vec[0];
307                for (idx, &val) in logits_vec.iter().enumerate() {
308                    if val > max_val {
309                        max_val = val;
310                        max_idx = idx;
311                    }
312                }
313                max_idx as u32
314            };
315
316            if next_token == config.eos_token_id {
317                break;
318            }
319
320            tokens.push(next_token);
321        }
322
323        Ok(tokenizer.decode(&tokens))
324    }
325
326    fn config(&self) -> &Self::Config { &self.config }
327
328    fn memory_requirements(&self) -> MemoryRequirements {
329        let param_size = self.config.vocab_size * self.config.hidden_size +
330            self.config.num_hidden_layers * (
331                // Mamba/Attention parameters
332                4 * self.config.hidden_size * self.config.hidden_size +
333                // MoE parameters
334                self.config.num_experts * 3 * self.config.hidden_size * self.config.intermediate_size
335            );
336        let param_size = param_size * 4;
337
338        MemoryRequirements {
339            gpu_memory: param_size,
340            cpu_memory: param_size / 4,
341            kv_cache_memory: param_size / 8,
342            peak_memory: param_size + param_size / 2,
343        }
344    }
345
346    fn to_device(&mut self, device: &Device) -> Result<()> {
347        self.device = device.clone();
348        self.embed_tokens = self.embed_tokens.to_device(device)?;
349        self.norm = self.norm.to_device(device)?;
350        self.lm_head = self.lm_head.to_device(device)?;
351        for layer in &mut self.layers {
352            layer.to_device(device)?;
353        }
354        Ok(())
355    }
356}
357
358impl JambaLayer {
359    fn new(config: &JambaConfig, layer_idx: usize, device: &Device) -> Result<Self> {
360        let layer_type = if config.is_attention_layer(layer_idx) {
361            JambaLayerType::Attention(JambaAttentionBlock::new(config, device)?)
362        } else {
363            JambaLayerType::Mamba(JambaMambaBlock::new(config, device)?)
364        };
365
366        let moe = JambaMoE::new(config, device)?;
367
368        let input_layernorm = ops_fn::zeros(&[config.hidden_size], DataType::Float32, device)?;
369        let post_layernorm = ops_fn::zeros(&[config.hidden_size], DataType::Float32, device)?;
370
371        Ok(Self {
372            layer_type,
373            moe,
374            input_layernorm,
375            post_layernorm,
376            config: config.clone(),
377        })
378    }
379
380    fn forward(&self, hidden_states: &Tensor, attention_mask: Option<&Tensor>) -> Result<Tensor> {
381        let residual = hidden_states.clone();
382
383        // Pre-norm for mixer (Mamba or Attention)
384        let normed = ops_fn::rms_norm(hidden_states, &self.input_layernorm, self.config.rms_norm_eps)?;
385
386        // Apply mixer layer (Mamba or Attention)
387        let mixed = match &self.layer_type {
388            JambaLayerType::Mamba(mamba) => mamba.forward(&normed)?,
389            JambaLayerType::Attention(attn) => attn.forward(&normed, attention_mask, self.config.rope_theta)?,
390        };
391
392        // Residual connection
393        let hidden_states = ops_fn::add(&residual, &mixed)?;
394
395        // Pre-norm for MoE
396        let residual = hidden_states.clone();
397        let normed = ops_fn::rms_norm(&hidden_states, &self.post_layernorm, self.config.rms_norm_eps)?;
398
399        // Apply MoE
400        let moe_out = self.moe.forward(&normed)?;
401
402        // Residual connection
403        ops_fn::add(&residual, &moe_out)
404    }
405
406    fn load_weights(&mut self, weights: &ModelWeights, layer_idx: usize) -> Result<()> {
407        let prefix = format!("model.layers.{}", layer_idx);
408
409        if let Some(w) = weights.get(&format!("{}.input_layernorm.weight", prefix)) {
410            self.input_layernorm = w.clone();
411        }
412
413        if let Some(w) = weights.get(&format!("{}.post_attention_layernorm.weight", prefix)) {
414            self.post_layernorm = w.clone();
415        }
416
417        // Load mixer weights
418        match &mut self.layer_type {
419            JambaLayerType::Mamba(mamba) => mamba.load_weights(weights, layer_idx)?,
420            JambaLayerType::Attention(attn) => attn.load_weights(weights, layer_idx)?,
421        }
422
423        // Load MoE weights
424        self.moe.load_weights(weights, layer_idx)?;
425
426        Ok(())
427    }
428
429    fn to_device(&mut self, device: &Device) -> Result<()> {
430        self.input_layernorm = self.input_layernorm.to_device(device)?;
431        self.post_layernorm = self.post_layernorm.to_device(device)?;
432
433        match &mut self.layer_type {
434            JambaLayerType::Mamba(mamba) => mamba.to_device(device)?,
435            JambaLayerType::Attention(attn) => attn.to_device(device)?,
436        }
437
438        self.moe.to_device(device)?;
439        Ok(())
440    }
441}
442
443impl JambaMambaBlock {
444    fn new(config: &JambaConfig, device: &Device) -> Result<Self> {
445        let d_inner = config.effective_mamba_d_inner();
446        let dt_rank = config.effective_mamba_dt_rank();
447        let d_state = config.d_state;
448        let d_conv = config.d_conv;
449
450        let in_proj = ops_fn::zeros(&[config.hidden_size, d_inner * 2], DataType::Float32, device)?;
451        let conv1d_weight = ops_fn::zeros(&[d_inner, d_conv], DataType::Float32, device)?;
452        let conv1d_bias = Some(ops_fn::zeros(&[d_inner], DataType::Float32, device)?);
453        let x_proj = ops_fn::zeros(&[d_inner, dt_rank + d_state * 2], DataType::Float32, device)?;
454        let dt_proj = ops_fn::zeros(&[dt_rank, d_inner], DataType::Float32, device)?;
455        let dt_proj_bias = Some(ops_fn::zeros(&[d_inner], DataType::Float32, device)?);
456        let a_log = ops_fn::zeros(&[d_inner, d_state], DataType::Float32, device)?;
457        let d = ops_fn::zeros(&[d_inner], DataType::Float32, device)?;
458        let out_proj = ops_fn::zeros(&[d_inner, config.hidden_size], DataType::Float32, device)?;
459
460        Ok(Self {
461            in_proj,
462            conv1d_weight,
463            conv1d_bias,
464            x_proj,
465            dt_proj,
466            dt_proj_bias,
467            a_log,
468            d,
469            out_proj,
470            d_inner,
471            d_state,
472            d_conv,
473            dt_rank,
474        })
475    }
476
477    fn forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
478        let shape = hidden_states.shape();
479        let (batch_size, seq_len, _) = (shape[0], shape[1], shape[2]);
480
481        // 1. Input projection: [B, L, D] -> [B, L, 2*d_inner]
482        let projected = ops_fn::matmul(hidden_states, &self.in_proj)?;
483
484        // 2. Split into x and z
485        let proj_candle = projected.to_candle()?;
486        let x = proj_candle.narrow(2, 0, self.d_inner)?;
487        let z = proj_candle.narrow(2, self.d_inner, self.d_inner)?;
488
489        // 3. Apply causal Conv1D to x
490        let x_conv = self.apply_conv1d(&Tensor::from_candle(x.clone()), batch_size, seq_len)?;
491
492        // 4. Apply SiLU activation
493        let x_act = ops_fn::silu(&x_conv)?;
494
495        // 5. Selective scan (SSM)
496        let y = self.selective_scan(&x_act, batch_size, seq_len)?;
497
498        // 6. Gate with z (SiLU(z) * y)
499        let z_act = ops_fn::silu(&Tensor::from_candle(z))?;
500        let gated = ops_fn::mul(&y, &z_act)?;
501
502        // 7. Output projection
503        ops_fn::matmul(&gated, &self.out_proj)
504    }
505
506    fn apply_conv1d(&self, x: &Tensor, batch_size: usize, seq_len: usize) -> Result<Tensor> {
507        let x_candle = x.to_candle()?;
508        let w_candle = self.conv1d_weight.to_candle()?;
509
510        // Pad for causal convolution
511        let pad_len = self.d_conv - 1;
512        let zeros = candle_core::Tensor::zeros(
513            &[batch_size, pad_len, self.d_inner],
514            x_candle.dtype(),
515            x_candle.device()
516        )?;
517
518        let x_padded = candle_core::Tensor::cat(&[&zeros, &x_candle], 1)?;
519
520        let mut outputs = Vec::new();
521        for i in 0..seq_len {
522            let window = x_padded.narrow(1, i, self.d_conv)?;
523            let window_t = window.transpose(1, 2)?;
524            let conv_out = window_t.broadcast_mul(&w_candle)?;
525            let summed = conv_out.sum(2)?;
526            outputs.push(summed);
527        }
528
529        let result = candle_core::Tensor::stack(&outputs, 1)?;
530
531        let result = if let Some(ref bias) = self.conv1d_bias {
532            let b_candle = bias.to_candle()?;
533            result.broadcast_add(&b_candle)?
534        } else {
535            result
536        };
537
538        Ok(Tensor::from_candle(result))
539    }
540
541    fn selective_scan(&self, x: &Tensor, batch_size: usize, seq_len: usize) -> Result<Tensor> {
542        // Project to get delta, B, C
543        let dbc = ops_fn::matmul(x, &self.x_proj)?;
544        let dbc_candle = dbc.to_candle()?;
545
546        let dt_raw = dbc_candle.narrow(2, 0, self.dt_rank)?;
547        let b = dbc_candle.narrow(2, self.dt_rank, self.d_state)?;
548        let c = dbc_candle.narrow(2, self.dt_rank + self.d_state, self.d_state)?;
549
550        // Project dt
551        let dt_proj_candle = self.dt_proj.to_candle()?;
552        let dt = dt_raw.broadcast_matmul(&dt_proj_candle)?;
553
554        let dt = if let Some(ref bias) = self.dt_proj_bias {
555            let b_candle = bias.to_candle()?;
556            dt.broadcast_add(&b_candle)?
557        } else {
558            dt
559        };
560
561        let dt = softplus(&dt)?;
562
563        // Get A from A_log
564        let a_log_candle = self.a_log.to_candle()?;
565        let a = a_log_candle.exp()?.neg()?;
566
567        // Selective scan loop
568        let x_candle = x.to_candle()?;
569        let d_candle = self.d.to_candle()?;
570
571        let mut h = candle_core::Tensor::zeros(
572            &[batch_size, self.d_inner, self.d_state],
573            candle_core::DType::F32,
574            x_candle.device()
575        )?;
576
577        let mut outputs = Vec::new();
578
579        for t in 0..seq_len {
580            let x_t = x_candle.narrow(1, t, 1)?.squeeze(1)?;
581            let dt_t = dt.narrow(1, t, 1)?.squeeze(1)?;
582            let b_t = b.narrow(1, t, 1)?.squeeze(1)?;
583            let c_t = c.narrow(1, t, 1)?.squeeze(1)?;
584
585            let dt_expanded = dt_t.unsqueeze(2)?;
586            let dt_a = dt_expanded.broadcast_mul(&a)?;
587            let a_bar = dt_a.exp()?;
588
589            let b_expanded = b_t.unsqueeze(1)?;
590            let dt_b = dt_expanded.broadcast_mul(&b_expanded)?;
591
592            let x_expanded = x_t.unsqueeze(2)?;
593
594            let ah = a_bar.mul(&h)?;
595            let bx = dt_b.mul(&x_expanded.broadcast_as(dt_b.dims())?)?;
596            h = ah.add(&bx)?;
597
598            let c_expanded = c_t.unsqueeze(1)?;
599            let y_state = h.mul(&c_expanded.broadcast_as(h.dims())?)?.sum(2)?;
600
601            let y_skip = x_t.broadcast_mul(&d_candle)?;
602            let y_t = y_state.add(&y_skip)?;
603
604            outputs.push(y_t);
605        }
606
607        let result = candle_core::Tensor::stack(&outputs, 1)?;
608        Ok(Tensor::from_candle(result))
609    }
610
611    fn load_weights(&mut self, weights: &ModelWeights, layer_idx: usize) -> Result<()> {
612        let prefix = format!("model.layers.{}.mamba", layer_idx);
613
614        if let Some(w) = weights.get(&format!("{}.in_proj.weight", prefix)) {
615            self.in_proj = ops_fn::transpose(w)?;
616        }
617
618        if let Some(w) = weights.get(&format!("{}.conv1d.weight", prefix)) {
619            let w_candle = w.to_candle()?;
620            let dims = w_candle.dims();
621            if dims.len() == 3 && dims[1] == 1 {
622                let reshaped = w_candle.squeeze(1)?;
623                self.conv1d_weight = Tensor::from_candle(reshaped);
624            } else {
625                self.conv1d_weight = w.clone();
626            }
627        }
628        if let Some(w) = weights.get(&format!("{}.conv1d.bias", prefix)) {
629            self.conv1d_bias = Some(w.clone());
630        }
631
632        if let Some(w) = weights.get(&format!("{}.x_proj.weight", prefix)) {
633            self.x_proj = ops_fn::transpose(w)?;
634        }
635
636        if let Some(w) = weights.get(&format!("{}.dt_proj.weight", prefix)) {
637            self.dt_proj = w.clone();
638        }
639        if let Some(w) = weights.get(&format!("{}.dt_proj.bias", prefix)) {
640            self.dt_proj_bias = Some(w.clone());
641        }
642
643        if let Some(w) = weights.get(&format!("{}.A_log", prefix)) {
644            self.a_log = w.clone();
645        }
646
647        if let Some(w) = weights.get(&format!("{}.D", prefix)) {
648            self.d = w.clone();
649        }
650
651        if let Some(w) = weights.get(&format!("{}.out_proj.weight", prefix)) {
652            self.out_proj = ops_fn::transpose(w)?;
653        }
654
655        Ok(())
656    }
657
658    fn to_device(&mut self, device: &Device) -> Result<()> {
659        self.in_proj = self.in_proj.to_device(device)?;
660        self.conv1d_weight = self.conv1d_weight.to_device(device)?;
661        if let Some(ref mut bias) = self.conv1d_bias {
662            *bias = bias.to_device(device)?;
663        }
664        self.x_proj = self.x_proj.to_device(device)?;
665        self.dt_proj = self.dt_proj.to_device(device)?;
666        if let Some(ref mut bias) = self.dt_proj_bias {
667            *bias = bias.to_device(device)?;
668        }
669        self.a_log = self.a_log.to_device(device)?;
670        self.d = self.d.to_device(device)?;
671        self.out_proj = self.out_proj.to_device(device)?;
672        Ok(())
673    }
674}
675
676impl JambaAttentionBlock {
677    fn new(config: &JambaConfig, device: &Device) -> Result<Self> {
678        let head_dim = config.hidden_size / config.num_attention_heads;
679        let num_heads = config.num_attention_heads;
680        let num_key_value_heads = config.num_key_value_heads;
681
682        let q_proj = ops_fn::zeros(&[config.hidden_size, num_heads * head_dim], DataType::Float32, device)?;
683        let k_proj = ops_fn::zeros(&[config.hidden_size, num_key_value_heads * head_dim], DataType::Float32, device)?;
684        let v_proj = ops_fn::zeros(&[config.hidden_size, num_key_value_heads * head_dim], DataType::Float32, device)?;
685        let o_proj = ops_fn::zeros(&[num_heads * head_dim, config.hidden_size], DataType::Float32, device)?;
686
687        Ok(Self {
688            q_proj,
689            k_proj,
690            v_proj,
691            o_proj,
692            num_heads,
693            num_key_value_heads,
694            head_dim,
695            scale: (head_dim as f32).powf(-0.5),
696        })
697    }
698
699    fn forward(&self, hidden_states: &Tensor, _attention_mask: Option<&Tensor>, rope_theta: f32) -> Result<Tensor> {
700        let shape = hidden_states.shape();
701        let (batch_size, seq_len, _) = (shape[0], shape[1], shape[2]);
702
703        // Project Q, K, V
704        let q = ops_fn::matmul(hidden_states, &self.q_proj)?;
705        let k = ops_fn::matmul(hidden_states, &self.k_proj)?;
706        let v = ops_fn::matmul(hidden_states, &self.v_proj)?;
707
708        // Convert to candle tensors for attention operations
709        let q_candle = q.to_candle()?;
710        let k_candle = k.to_candle()?;
711        let v_candle = v.to_candle()?;
712
713        // Reshape: [batch, seq, heads*head_dim] -> [batch, seq, heads, head_dim] -> [batch, heads, seq, head_dim]
714        let q_reshaped = q_candle
715            .reshape(&[batch_size, seq_len, self.num_heads, self.head_dim])?
716            .transpose(1, 2)?;
717
718        let k_reshaped = k_candle
719            .reshape(&[batch_size, seq_len, self.num_key_value_heads, self.head_dim])?
720            .transpose(1, 2)?;
721
722        let v_reshaped = v_candle
723            .reshape(&[batch_size, seq_len, self.num_key_value_heads, self.head_dim])?
724            .transpose(1, 2)?;
725
726        // Apply RoPE
727        let (q_with_rope, k_with_rope) = apply_rope(&q_reshaped, &k_reshaped, seq_len, self.head_dim, rope_theta)?;
728
729        // Handle GQA - repeat K/V heads to match Q heads
730        let num_groups = self.num_heads / self.num_key_value_heads;
731        let (k_expanded, v_expanded) = if num_groups > 1 {
732            let k_rep = k_with_rope
733                .unsqueeze(2)?
734                .broadcast_as(&[batch_size, self.num_key_value_heads, num_groups, seq_len, self.head_dim])?
735                .reshape(&[batch_size, self.num_heads, seq_len, self.head_dim])?;
736            let v_rep = v_reshaped
737                .unsqueeze(2)?
738                .broadcast_as(&[batch_size, self.num_key_value_heads, num_groups, seq_len, self.head_dim])?
739                .reshape(&[batch_size, self.num_heads, seq_len, self.head_dim])?;
740            (k_rep, v_rep)
741        } else {
742            (k_with_rope, v_reshaped)
743        };
744
745        // Scaled dot-product attention
746        let k_t = k_expanded.transpose(2, 3)?;
747        let q_contiguous = q_with_rope.contiguous()?;
748        let k_contiguous = k_t.contiguous()?;
749
750        let scores = q_contiguous.matmul(&k_contiguous)?;
751        let scaled_scores = (scores * (self.scale as f64))?;
752
753        // Apply causal mask
754        let device = scaled_scores.device();
755        let causal_mask = {
756            let mut mask_data = vec![0.0f32; seq_len * seq_len];
757            for i in 0..seq_len {
758                for j in 0..seq_len {
759                    if j > i {
760                        mask_data[i * seq_len + j] = f32::NEG_INFINITY;
761                    }
762                }
763            }
764            candle_core::Tensor::from_vec(mask_data, &[1, 1, seq_len, seq_len], device)?
765        };
766
767        let masked_scores = scaled_scores.broadcast_add(&causal_mask)?;
768        let attention_weights = candle_nn::ops::softmax_last_dim(&masked_scores)?;
769
770        // Apply attention to values
771        let v_contiguous = v_expanded.contiguous()?;
772        let attn_output = attention_weights.matmul(&v_contiguous)?;
773
774        // Reshape back: [batch, heads, seq, head_dim] -> [batch, seq, heads * head_dim]
775        let attn_output = attn_output
776            .transpose(1, 2)?
777            .reshape(&[batch_size, seq_len, self.num_heads * self.head_dim])?;
778
779        let attn_output = Tensor::from_candle(attn_output);
780
781        // Output projection
782        ops_fn::matmul(&attn_output, &self.o_proj)
783    }
784
785    fn load_weights(&mut self, weights: &ModelWeights, layer_idx: usize) -> Result<()> {
786        let prefix = format!("model.layers.{}.self_attn", layer_idx);
787
788        if let Some(w) = weights.get(&format!("{}.q_proj.weight", prefix)) {
789            self.q_proj = ops_fn::transpose(w)?;
790        }
791        if let Some(w) = weights.get(&format!("{}.k_proj.weight", prefix)) {
792            self.k_proj = ops_fn::transpose(w)?;
793        }
794        if let Some(w) = weights.get(&format!("{}.v_proj.weight", prefix)) {
795            self.v_proj = ops_fn::transpose(w)?;
796        }
797        if let Some(w) = weights.get(&format!("{}.o_proj.weight", prefix)) {
798            self.o_proj = ops_fn::transpose(w)?;
799        }
800
801        Ok(())
802    }
803
804    fn to_device(&mut self, device: &Device) -> Result<()> {
805        self.q_proj = self.q_proj.to_device(device)?;
806        self.k_proj = self.k_proj.to_device(device)?;
807        self.v_proj = self.v_proj.to_device(device)?;
808        self.o_proj = self.o_proj.to_device(device)?;
809        Ok(())
810    }
811}
812
813impl JambaMoE {
814    fn new(config: &JambaConfig, device: &Device) -> Result<Self> {
815        let router = ops_fn::zeros(
816            &[config.hidden_size, config.num_experts],
817            DataType::Float32,
818            device
819        )?;
820
821        let mut experts = Vec::with_capacity(config.num_experts);
822        for _ in 0..config.num_experts {
823            experts.push(JambaExpert::new(config, device)?);
824        }
825
826        Ok(Self {
827            router,
828            experts,
829            num_experts: config.num_experts,
830            num_experts_per_tok: config.num_experts_per_tok,
831        })
832    }
833
834    fn forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
835        let shape = hidden_states.shape();
836        let (batch_size, seq_len, hidden_size) = (shape[0], shape[1], shape[2]);
837
838        // Compute router logits
839        let router_logits = ops_fn::matmul(hidden_states, &self.router)?;
840
841        // Get top-k experts along last dimension
842        let (topk_weights, topk_indices) = ops_fn::topk(&router_logits, self.num_experts_per_tok, -1)?;
843        let routing_weights = ops_fn::softmax(&topk_weights, -1)?;
844
845        // Flatten for processing
846        let hidden_flat = hidden_states.reshape(&[batch_size * seq_len, hidden_size])?;
847
848        // Extract routing data
849        let all_indices: Vec<i64> = topk_indices.to_candle()?.flatten_all()?.to_vec1()?;
850        let all_weights: Vec<f32> = routing_weights.to_candle()?.flatten_all()?.to_vec1()?;
851        let num_tokens = batch_size * seq_len;
852        let k = self.num_experts_per_tok;
853
854        // Initialize output
855        let mut output_candle = candle_core::Tensor::zeros(
856            &[num_tokens, hidden_size],
857            candle_core::DType::F32,
858            &candle_core::Device::Cpu
859        )?;
860
861        // Process each token
862        for tok_idx in 0..num_tokens {
863            let tok_hidden = hidden_flat.to_candle()?.narrow(0, tok_idx, 1)?;
864            let mut tok_output = candle_core::Tensor::zeros(
865                &[1, hidden_size],
866                candle_core::DType::F32,
867                &candle_core::Device::Cpu
868            )?;
869
870            let start = tok_idx * k;
871            for j in 0..k {
872                let expert_idx = all_indices[start + j] as usize;
873                let weight = all_weights[start + j];
874
875                if expert_idx < self.experts.len() {
876                    let expert_out = self.experts[expert_idx].forward(&Tensor::from_candle(tok_hidden.clone()))?;
877                    let weighted = expert_out.to_candle()?.affine(weight as f64, 0.0)?;
878                    tok_output = tok_output.add(&weighted)?;
879                }
880            }
881
882            output_candle = output_candle.slice_assign(&[tok_idx..tok_idx+1, 0..hidden_size], &tok_output)?;
883        }
884
885        // Reshape back
886        let output = Tensor::from_candle(output_candle);
887        output.reshape(&[batch_size, seq_len, hidden_size])
888    }
889
890    fn load_weights(&mut self, weights: &ModelWeights, layer_idx: usize) -> Result<()> {
891        let prefix = format!("model.layers.{}.block_sparse_moe", layer_idx);
892
893        if let Some(w) = weights.get(&format!("{}.gate.weight", prefix)) {
894            self.router = ops_fn::transpose(w)?;
895        }
896
897        for (i, expert) in self.experts.iter_mut().enumerate() {
898            expert.load_weights(weights, layer_idx, i)?;
899        }
900
901        Ok(())
902    }
903
904    fn to_device(&mut self, device: &Device) -> Result<()> {
905        self.router = self.router.to_device(device)?;
906        for expert in &mut self.experts {
907            expert.to_device(device)?;
908        }
909        Ok(())
910    }
911}
912
913impl JambaExpert {
914    fn new(config: &JambaConfig, device: &Device) -> Result<Self> {
915        let gate_proj = ops_fn::zeros(&[config.hidden_size, config.intermediate_size], DataType::Float32, device)?;
916        let up_proj = ops_fn::zeros(&[config.hidden_size, config.intermediate_size], DataType::Float32, device)?;
917        let down_proj = ops_fn::zeros(&[config.intermediate_size, config.hidden_size], DataType::Float32, device)?;
918
919        Ok(Self { gate_proj, up_proj, down_proj })
920    }
921
922    fn forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
923        let gate = ops_fn::matmul(hidden_states, &self.gate_proj)?;
924        let gate = ops_fn::silu(&gate)?;
925        let up = ops_fn::matmul(hidden_states, &self.up_proj)?;
926        let hidden = ops_fn::mul(&gate, &up)?;
927        ops_fn::matmul(&hidden, &self.down_proj)
928    }
929
930    fn load_weights(&mut self, weights: &ModelWeights, layer_idx: usize, expert_idx: usize) -> Result<()> {
931        let prefix = format!("model.layers.{}.block_sparse_moe.experts.{}", layer_idx, expert_idx);
932
933        if let Some(w) = weights.get(&format!("{}.w1.weight", prefix)) {
934            self.gate_proj = ops_fn::transpose(w)?;
935        }
936        if let Some(w) = weights.get(&format!("{}.w3.weight", prefix)) {
937            self.up_proj = ops_fn::transpose(w)?;
938        }
939        if let Some(w) = weights.get(&format!("{}.w2.weight", prefix)) {
940            self.down_proj = ops_fn::transpose(w)?;
941        }
942
943        Ok(())
944    }
945
946    fn to_device(&mut self, device: &Device) -> Result<()> {
947        self.gate_proj = self.gate_proj.to_device(device)?;
948        self.up_proj = self.up_proj.to_device(device)?;
949        self.down_proj = self.down_proj.to_device(device)?;
950        Ok(())
951    }
952}
953
954/// Softplus activation: log(1 + exp(x))
955fn softplus(x: &candle_core::Tensor) -> Result<candle_core::Tensor> {
956    let one = candle_core::Tensor::ones(x.dims(), x.dtype(), x.device())?;
957    let exp_x = x.exp()?;
958    let one_plus_exp = one.add(&exp_x)?;
959    Ok(one_plus_exp.log()?)
960}
961
962/// Apply Rotary Position Embedding (RoPE) to Q and K tensors
963fn apply_rope(
964    q: &candle_core::Tensor,
965    k: &candle_core::Tensor,
966    seq_len: usize,
967    head_dim: usize,
968    rope_theta: f32,
969) -> Result<(candle_core::Tensor, candle_core::Tensor)> {
970    let device = q.device();
971    let half_dim = head_dim / 2;
972
973    // Build position indices [0, 1, 2, ..., seq_len-1]
974    let positions: Vec<f32> = (0..seq_len).map(|i| i as f32).collect();
975    let positions = candle_core::Tensor::from_vec(positions, &[seq_len, 1], device)?;
976
977    // Build frequency indices [0, 1, 2, ..., half_dim-1]
978    let freq_indices: Vec<f32> = (0..half_dim).map(|i| i as f32).collect();
979    let freq_indices = candle_core::Tensor::from_vec(freq_indices, &[1, half_dim], device)?;
980
981    // Compute frequencies: 1 / (theta^(2i/head_dim))
982    let power = (freq_indices * (2.0 / head_dim as f64))?;
983    let theta_tensor = candle_core::Tensor::from_vec(vec![rope_theta], &[1], device)?
984        .broadcast_as(&[1, half_dim])?;
985    let freqs = theta_tensor.pow(&power)?;
986    let inv_freqs = (freqs.recip())?;
987
988    // Compute angles: positions * inv_freqs -> [seq_len, half_dim]
989    let angles = positions.broadcast_mul(&inv_freqs)?;
990
991    // Compute cos and sin
992    let cos = angles.cos()?;
993    let sin = angles.sin()?;
994
995    // Reshape for broadcasting: [1, 1, seq_len, half_dim]
996    let cos = cos.reshape(&[1, 1, seq_len, half_dim])?;
997    let sin = sin.reshape(&[1, 1, seq_len, half_dim])?;
998
999    // Apply rotary embedding to Q
1000    let q_shape = q.dims();
1001    let q_first_half = q.narrow(3, 0, half_dim)?;
1002    let q_second_half = q.narrow(3, half_dim, half_dim)?;
1003
1004    let cos_q = cos.broadcast_as(&[q_shape[0], q_shape[1], seq_len, half_dim])?;
1005    let sin_q = sin.broadcast_as(&[q_shape[0], q_shape[1], seq_len, half_dim])?;
1006
1007    let q_rotated_first = (q_first_half.broadcast_mul(&cos_q)?
1008        .sub(&q_second_half.broadcast_mul(&sin_q)?))?;
1009    let q_rotated_second = (q_first_half.broadcast_mul(&sin_q)?
1010        .add(&q_second_half.broadcast_mul(&cos_q)?))?;
1011
1012    let q_rotated = candle_core::Tensor::cat(&[&q_rotated_first, &q_rotated_second], 3)?;
1013
1014    // Apply rotary embedding to K
1015    let k_shape = k.dims();
1016    let k_first_half = k.narrow(3, 0, half_dim)?;
1017    let k_second_half = k.narrow(3, half_dim, half_dim)?;
1018
1019    let cos_k = cos.broadcast_as(&[k_shape[0], k_shape[1], seq_len, half_dim])?;
1020    let sin_k = sin.broadcast_as(&[k_shape[0], k_shape[1], seq_len, half_dim])?;
1021
1022    let k_rotated_first = (k_first_half.broadcast_mul(&cos_k)?
1023        .sub(&k_second_half.broadcast_mul(&sin_k)?))?;
1024    let k_rotated_second = (k_first_half.broadcast_mul(&sin_k)?
1025        .add(&k_second_half.broadcast_mul(&cos_k)?))?;
1026
1027    let k_rotated = candle_core::Tensor::cat(&[&k_rotated_first, &k_rotated_second], 3)?;
1028
1029    Ok((q_rotated, k_rotated))
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035
1036    #[test]
1037    fn test_jamba_config() {
1038        let config = JambaConfig::default();
1039        assert_eq!(config.vocab_size, 65536);
1040        assert_eq!(config.hidden_size, 4096);
1041        assert_eq!(config.num_experts, 16);
1042        assert_eq!(config.attn_layer_period, 8);
1043    }
1044
1045    #[test]
1046    fn test_jamba_layer_type_selection() {
1047        let config = JambaConfig {
1048            attn_layer_period: 4,
1049            attn_layer_offset: 2,
1050            ..Default::default()
1051        };
1052
1053        // With offset=2 and period=4:
1054        // Layer 2: (2+2) % 4 = 0 -> Attention
1055        // Layer 3: (3+2) % 4 = 1 -> Mamba
1056        // Layer 6: (6+2) % 4 = 0 -> Attention
1057        assert!(config.is_attention_layer(2));
1058        assert!(!config.is_attention_layer(3));
1059        assert!(config.is_attention_layer(6));
1060    }
1061
1062    #[test]
1063    fn test_jamba_model_creation() {
1064        let config = JambaConfig {
1065            vocab_size: 1000,
1066            hidden_size: 128,
1067            intermediate_size: 512,
1068            num_hidden_layers: 4,
1069            num_attention_heads: 4,
1070            num_key_value_heads: 2,
1071            num_experts: 4,
1072            num_experts_per_tok: 2,
1073            d_state: 8,
1074            d_conv: 4,
1075            attn_layer_period: 2,
1076            attn_layer_offset: 1,
1077            ..Default::default()
1078        };
1079
1080        let model = JambaModelV2::new(config).unwrap();
1081        assert_eq!(model.config().vocab_size(), 1000);
1082        assert_eq!(model.config().hidden_size(), 128);
1083        assert_eq!(model.config().num_layers(), 4);
1084    }
1085
1086    #[test]
1087    fn test_jamba_forward_pass() {
1088        let config = JambaConfig {
1089            vocab_size: 100,
1090            hidden_size: 64,
1091            intermediate_size: 256,
1092            num_hidden_layers: 2,
1093            num_attention_heads: 4,
1094            num_key_value_heads: 2,
1095            num_experts: 2,
1096            num_experts_per_tok: 1,
1097            d_state: 8,
1098            d_conv: 4,
1099            expand: 2,
1100            attn_layer_period: 2,
1101            attn_layer_offset: 1,
1102            ..Default::default()
1103        };
1104
1105        let model = JambaModelV2::new(config).unwrap();
1106        let input_ids = ops_fn::zeros(&[1, 4], DataType::Int64, &Device::CPU).unwrap();
1107        let inputs = ModelInputs::text(input_ids);
1108
1109        let outputs = model.forward(&inputs).unwrap();
1110        match outputs {
1111            ModelOutputs::Logits { logits, .. } => {
1112                assert_eq!(logits.shape(), &[1, 4, 100]);
1113            }
1114            _ => panic!("Expected logits output"),
1115        }
1116    }
1117}