1use crate::attention::gdn::{GatedDeltaNetState, sigmoid, softplus};
11use crate::attention::gdn_fused::{
12 GatedDeltaNetFusedScratch, conv1d_silu_fused, simd_decay_and_rank1_update, simd_gated_rms_norm,
13 simd_l2_normalize, simd_matvec_transpose,
14};
15use crate::forward::cpu::{elementwise_mul, silu_inplace};
16use crate::model::qwen35::{
17 ForwardScratch, GenerationEntryContract, GenerationPlan, GenerationPreparation, KvCache,
18 decode_tokens, prepare_generation, qwen35_rms_norm, resize, sample_token, should_stop_token,
19};
20use crate::model::qwen35_config::{GenerateConfig, GenerateOutput, Qwen35Config};
21use crate::rope::RopeTable;
22use crate::stop_reason::StopReason;
23use crate::tokenizer::bpe::BpeTokenizer;
24use crate::tokenizer::common::Tokenizer;
25use crate::vision::multimodal::Qwen35VisionRequest;
26use crate::weights::f16_weights::{
27 F16AttentionWeights, F16FeedForwardWeights, F16FullAttentionLayerWeights,
28 F16GatedDeltaNetWeights, F16ModelWeights, F16MoeLayerWeights, f16_to_f32_slice, matmul_bt_f16,
29};
30
31#[inline]
50pub fn gated_delta_net_step_fused_f16(
51 input: &[f32],
52 state: &mut GatedDeltaNetState,
53 weights: &F16GatedDeltaNetWeights,
54 cfg: &Qwen35Config,
55 scratch: &mut GatedDeltaNetFusedScratch,
56 output: &mut [f32],
57) {
58 let hidden = cfg.hidden_size;
59 let num_heads = cfg.linear_num_key_heads;
60 let value_heads = cfg.linear_num_value_heads();
61 let ratio = value_heads / num_heads;
62 let key_dim = cfg.linear_key_head_dim;
63 let value_dim = cfg.linear_value_head_dim;
64 let qkv_dim = cfg.linear_qkv_dim();
65 let output_dim = cfg.linear_output_dim();
66 let kernel_size = cfg.linear_conv_kernel_dim;
67
68 debug_assert!(input.len() >= hidden);
69 debug_assert!(output.len() >= hidden);
70
71 scratch.ensure_capacity(qkv_dim, output_dim, value_heads, key_dim, value_dim);
72
73 matmul_bt_f16(
75 input,
76 &weights.in_proj_qkv,
77 &mut scratch.qkv_proj[..qkv_dim],
78 1,
79 hidden,
80 qkv_dim,
81 );
82
83 matmul_bt_f16(
84 input,
85 &weights.in_proj_z,
86 &mut scratch.z_proj[..output_dim],
87 1,
88 hidden,
89 output_dim,
90 );
91
92 matmul_bt_f16(
93 input,
94 &weights.in_proj_b,
95 &mut scratch.beta_proj[..value_heads],
96 1,
97 hidden,
98 value_heads,
99 );
100
101 matmul_bt_f16(
102 input,
103 &weights.in_proj_a,
104 &mut scratch.alpha_proj[..value_heads],
105 1,
106 hidden,
107 value_heads,
108 );
109
110 for b in &mut scratch.beta_proj[..value_heads] {
112 *b = sigmoid(*b);
113 }
114
115 conv1d_silu_fused(
117 &scratch.qkv_proj[..qkv_dim],
118 &mut state.conv_buffer,
119 &weights.conv1d_weight,
120 &mut scratch.conv_output[..qkv_dim],
121 qkv_dim,
122 kernel_size,
123 );
124
125 let q_total = num_heads * key_dim;
127 let k_total = num_heads * key_dim;
128 let v_offset = q_total + k_total;
129 let scale = 1.0 / (key_dim as f32).sqrt();
130
131 for h in 0..value_heads {
132 let k_head = h / ratio;
133 let q_start = k_head * key_dim;
134 let k_start = q_total + k_head * key_dim;
135 let v_start = v_offset + h * value_dim;
136
137 scratch.q_head[..key_dim].copy_from_slice(&scratch.conv_output[q_start..q_start + key_dim]);
138 scratch.k_head[..key_dim].copy_from_slice(&scratch.conv_output[k_start..k_start + key_dim]);
139 let v = &scratch.conv_output[v_start..v_start + value_dim];
140
141 simd_l2_normalize(&mut scratch.q_head[..key_dim]);
143 simd_l2_normalize(&mut scratch.k_head[..key_dim]);
144
145 let a = weights.a_log[h].exp().min(f32::MAX);
148 let sp = softplus(scratch.alpha_proj[h] + weights.dt_bias[h]);
149 let g = (-a * sp).exp();
150
151 let s_offset = h * key_dim * value_dim;
152 let s = &mut state.s_matrices[s_offset..s_offset + key_dim * value_dim];
153
154 simd_matvec_transpose(
156 s,
157 &scratch.k_head[..key_dim],
158 &mut scratch.kv_mem[..value_dim],
159 key_dim,
160 value_dim,
161 );
162
163 let beta_h = scratch.beta_proj[h];
165 for ((d, &vj), &mem) in scratch.delta[..value_dim]
166 .iter_mut()
167 .zip(&v[..value_dim])
168 .zip(&scratch.kv_mem[..value_dim])
169 {
170 *d = (vj - mem * g) * beta_h;
171 }
172
173 simd_decay_and_rank1_update(
175 s,
176 &scratch.k_head[..key_dim],
177 &scratch.delta[..value_dim],
178 g,
179 key_dim,
180 value_dim,
181 );
182
183 let out_start = h * value_dim;
185 let out_head = &mut scratch.output_heads[out_start..out_start + value_dim];
186 simd_matvec_transpose(s, &scratch.q_head[..key_dim], out_head, key_dim, value_dim);
187 for val in out_head.iter_mut() {
188 *val *= scale;
189 }
190 }
191
192 let gamma = &weights.norm_weight[..value_dim];
195 debug_assert_eq!(gamma.len(), value_dim);
196
197 for h in 0..value_heads {
198 let start = h * value_dim;
199 let end = start + value_dim;
200 simd_gated_rms_norm(
201 &scratch.output_heads[start..end],
202 &scratch.z_proj[start..end],
203 gamma,
204 &mut scratch.gated_norm_buf[start..end],
205 cfg.rms_norm_eps,
206 );
207 }
208
209 matmul_bt_f16(
211 &scratch.gated_norm_buf[..output_dim],
212 &weights.out_proj,
213 &mut output[..hidden],
214 1,
215 output_dim,
216 hidden,
217 );
218}
219
220fn full_attention_step_f16(
229 weights: &F16FullAttentionLayerWeights,
230 cache_idx: usize,
231 position: usize,
232 kv_cache: &mut KvCache,
233 scratch: &mut ForwardScratch,
234 cfg: &Qwen35Config,
235 rope: &RopeTable,
236 hidden: usize,
237 mrope_cos_sin: Option<(&[f32], &[f32])>,
238) {
239 let input: Vec<f32> = scratch.attn_out[..hidden].to_vec();
241 let q_dim = cfg.full_q_dim();
242 let kv_dim = cfg.full_kv_dim();
243 let head_dim = cfg.head_dim;
244 let num_q_heads = cfg.num_attention_heads;
245 let num_kv_heads = cfg.num_key_value_heads;
246 let rope_dim = cfg.rope_dim();
247
248 let q_proj_dim = 2 * q_dim;
251 let mut q_and_gate = vec![0.0f32; q_proj_dim];
252 matmul_bt_f16(
253 &input,
254 &weights.q_proj,
255 &mut q_and_gate,
256 1,
257 hidden,
258 q_proj_dim,
259 );
260 let mut gate_z = vec![0.0f32; q_dim];
262 for h in 0..num_q_heads {
263 let src = h * head_dim * 2;
264 let dst = h * head_dim;
265 scratch.q_buf[dst..dst + head_dim].copy_from_slice(&q_and_gate[src..src + head_dim]);
266 gate_z[dst..dst + head_dim]
267 .copy_from_slice(&q_and_gate[src + head_dim..src + head_dim * 2]);
268 }
269 matmul_bt_f16(
270 &input,
271 &weights.k_proj,
272 &mut scratch.k_buf[..kv_dim],
273 1,
274 hidden,
275 kv_dim,
276 );
277 matmul_bt_f16(
278 &input,
279 &weights.v_proj,
280 &mut scratch.v_buf[..kv_dim],
281 1,
282 hidden,
283 kv_dim,
284 );
285
286 for h in 0..num_q_heads {
288 let start = h * head_dim;
289 qwen35_rms_norm(
290 &mut scratch.q_buf[start..start + head_dim],
291 &weights.q_norm,
292 head_dim,
293 cfg.rms_norm_eps,
294 );
295 }
296 for h in 0..num_kv_heads {
297 let start = h * head_dim;
298 qwen35_rms_norm(
299 &mut scratch.k_buf[start..start + head_dim],
300 &weights.k_norm,
301 head_dim,
302 cfg.rms_norm_eps,
303 );
304 }
305
306 let half = rope_dim / 2;
311 for h in 0..num_q_heads {
312 let start = h * head_dim;
313 if let Some((cos_row, sin_row)) = mrope_cos_sin {
314 for i in 0..half {
315 let cos_val = cos_row[i];
316 let sin_val = sin_row[i];
317 let x0 = scratch.q_buf[start + i];
318 let x1 = scratch.q_buf[start + half + i];
319 scratch.q_buf[start + i] = x0 * cos_val - x1 * sin_val;
320 scratch.q_buf[start + half + i] = x0 * sin_val + x1 * cos_val;
321 }
322 } else {
323 let base = position * half;
324 for i in 0..half {
325 let cos_val = rope.cos_at(base + i);
326 let sin_val = rope.sin_at(base + i);
327 let x0 = scratch.q_buf[start + i];
328 let x1 = scratch.q_buf[start + half + i];
329 scratch.q_buf[start + i] = x0 * cos_val - x1 * sin_val;
330 scratch.q_buf[start + half + i] = x0 * sin_val + x1 * cos_val;
331 }
332 }
333 }
334 for h in 0..num_kv_heads {
335 let start = h * head_dim;
336 if let Some((cos_row, sin_row)) = mrope_cos_sin {
337 for i in 0..half {
338 let cos_val = cos_row[i];
339 let sin_val = sin_row[i];
340 let x0 = scratch.k_buf[start + i];
341 let x1 = scratch.k_buf[start + half + i];
342 scratch.k_buf[start + i] = x0 * cos_val - x1 * sin_val;
343 scratch.k_buf[start + half + i] = x0 * sin_val + x1 * cos_val;
344 }
345 } else {
346 let base = position * half;
347 for i in 0..half {
348 let cos_val = rope.cos_at(base + i);
349 let sin_val = rope.sin_at(base + i);
350 let x0 = scratch.k_buf[start + i];
351 let x1 = scratch.k_buf[start + half + i];
352 scratch.k_buf[start + i] = x0 * cos_val - x1 * sin_val;
353 scratch.k_buf[start + half + i] = x0 * sin_val + x1 * cos_val;
354 }
355 }
356 }
357
358 kv_cache.append_kv(
360 cache_idx,
361 &scratch.k_buf[..kv_dim],
362 &scratch.v_buf[..kv_dim],
363 );
364 let cur_seq_len = kv_cache.seq_len + 1; let groups = num_q_heads / num_kv_heads;
368 let scale = 1.0 / (head_dim as f32).sqrt();
369
370 let k_cache = &kv_cache.k[cache_idx];
371 let v_cache = &kv_cache.v[cache_idx];
372
373 for qh in 0..num_q_heads {
374 let kvh = qh / groups;
375 let q_off = qh * head_dim;
376 let q = &scratch.q_buf[q_off..q_off + head_dim];
377
378 let scores_start = qh * cur_seq_len;
380
381 for t in 0..cur_seq_len {
382 let k_off = t * kv_dim + kvh * head_dim;
383 let mut dot = 0.0f32;
384 for d in 0..head_dim {
385 dot += q[d] * k_cache[k_off + d];
386 }
387 scratch.scores[scores_start + t] = dot * scale;
388 }
389
390 let row = &mut scratch.scores[scores_start..scores_start + cur_seq_len];
397 let (max_score, any_nan) = crate::attention::softmax_row::row_max_and_any_nan(row);
398 if crate::attention::softmax_row::row_fails_closed_pre_exp(max_score, any_nan) {
399 row.fill(0.0);
400 } else {
401 let mut sum_exp = 0.0f32;
402 for v in row.iter_mut() {
403 *v = (*v - max_score).exp();
404 sum_exp += *v;
405 }
406 crate::attention::softmax_row::finalize_row(row, sum_exp);
407 }
408
409 let ctx_off = qh * head_dim;
411 for d in 0..head_dim {
412 let mut sum = 0.0f32;
413 for t in 0..cur_seq_len {
414 let v_off = t * kv_dim + kvh * head_dim;
415 sum += scratch.scores[scores_start + t] * v_cache[v_off + d];
416 }
417 scratch.context[ctx_off + d] = sum;
418 }
419 }
420
421 for (ctx, &gz) in scratch.context[..q_dim].iter_mut().zip(&gate_z[..q_dim]) {
423 let sig = 1.0 / (1.0 + (-gz).exp());
424 *ctx *= sig;
425 }
426
427 matmul_bt_f16(
429 &scratch.context[..q_dim],
430 &weights.o_proj,
431 &mut scratch.attn_out[..hidden],
432 1,
433 q_dim,
434 hidden,
435 );
436}
437
438#[inline]
447fn ffn_step_f16(
448 gate_proj: &[u16],
449 up_proj: &[u16],
450 down_proj: &[u16],
451 scratch: &mut ForwardScratch,
452 inter: usize,
453 hidden: usize,
454) {
455 scratch.input_tmp[..hidden].copy_from_slice(&scratch.ffn_out[..hidden]);
456
457 matmul_bt_f16(
458 &scratch.input_tmp[..hidden],
459 gate_proj,
460 &mut scratch.gate_buf[..inter],
461 1,
462 hidden,
463 inter,
464 );
465 matmul_bt_f16(
466 &scratch.input_tmp[..hidden],
467 up_proj,
468 &mut scratch.up_buf[..inter],
469 1,
470 hidden,
471 inter,
472 );
473
474 silu_inplace(&mut scratch.gate_buf[..inter]);
475 elementwise_mul(&mut scratch.gate_buf[..inter], &scratch.up_buf[..inter]);
476
477 matmul_bt_f16(
478 &scratch.gate_buf[..inter],
479 down_proj,
480 &mut scratch.ffn_out[..hidden],
481 1,
482 inter,
483 hidden,
484 );
485}
486
487#[inline]
493fn moe_ffn_step_f16(moe: &F16MoeLayerWeights, scratch: &mut ForwardScratch, hidden: usize) {
494 let inter = moe.experts.intermediate_size;
495 let shared_inter = moe.shared_expert.intermediate_size;
496 let num_experts = moe.router.num_experts;
497 let top_k = moe.router.num_experts_per_tok;
498
499 debug_assert_eq!(moe.router.hidden_size, hidden);
500 debug_assert_eq!(moe.experts.num_experts, num_experts);
501 debug_assert_eq!(moe.experts.hidden_size, hidden);
502 debug_assert_eq!(moe.shared_expert.hidden_size, hidden);
503
504 scratch.input_tmp[..hidden].copy_from_slice(&scratch.ffn_out[..hidden]);
505
506 if scratch.router_logits.len() < num_experts {
507 scratch.router_logits.resize(num_experts, 0.0);
508 }
509 if scratch.router_selected.len() < top_k {
510 scratch.router_selected.resize(top_k, (usize::MAX, 0.0));
511 }
512
513 matmul_bt_f16(
515 &scratch.input_tmp[..hidden],
516 &moe.router.gate,
517 &mut scratch.router_logits[..num_experts],
518 1,
519 hidden,
520 num_experts,
521 );
522
523 let max_logit = scratch.router_logits[..num_experts]
525 .iter()
526 .copied()
527 .fold(f32::NEG_INFINITY, f32::max);
528 let mut denom = 0.0f32;
529 for v in &mut scratch.router_logits[..num_experts] {
530 *v = (*v - max_logit).exp();
531 denom += *v;
532 }
533 if denom > 0.0 {
534 for v in &mut scratch.router_logits[..num_experts] {
535 *v /= denom;
536 }
537 } else {
538 scratch.router_logits[..num_experts].fill(0.0);
549 }
550
551 for slot in &mut scratch.router_selected[..top_k] {
553 *slot = (usize::MAX, f32::NEG_INFINITY);
554 }
555 for (expert_id, prob) in scratch.router_logits[..num_experts]
556 .iter()
557 .copied()
558 .enumerate()
559 {
560 for rank in 0..top_k {
561 if prob > scratch.router_selected[rank].1 {
562 for shift in (rank + 1..top_k).rev() {
563 scratch.router_selected[shift] = scratch.router_selected[shift - 1];
564 }
565 scratch.router_selected[rank] = (expert_id, prob);
566 break;
567 }
568 }
569 }
570
571 let top_sum: f32 = scratch.router_selected[..top_k]
572 .iter()
573 .map(|(_, p)| *p)
574 .sum();
575 if top_sum > 0.0 {
576 for (_, prob) in &mut scratch.router_selected[..top_k] {
577 *prob /= top_sum;
578 }
579 }
580
581 scratch.expert_out[..hidden].fill(0.0);
582
583 for idx in 0..top_k {
584 let (expert_id, weight) = scratch.router_selected[idx];
585 if expert_id >= moe.experts.num_experts {
593 continue;
594 }
595 debug_assert_ne!(expert_id, usize::MAX);
596
597 let gate_up_stride = 2 * inter * hidden;
598 let gate_up_start = expert_id * gate_up_stride;
599 let down_start = expert_id * hidden * inter;
600
601 let gate_w = &moe.experts.gate_up_proj[gate_up_start..gate_up_start + inter * hidden];
603 let up_w = &moe.experts.gate_up_proj
604 [gate_up_start + inter * hidden..gate_up_start + 2 * inter * hidden];
605 let down_w = &moe.experts.down_proj[down_start..down_start + hidden * inter];
606
607 matmul_bt_f16(
608 &scratch.input_tmp[..hidden],
609 gate_w,
610 &mut scratch.gate_buf[..inter],
611 1,
612 hidden,
613 inter,
614 );
615 matmul_bt_f16(
616 &scratch.input_tmp[..hidden],
617 up_w,
618 &mut scratch.up_buf[..inter],
619 1,
620 hidden,
621 inter,
622 );
623
624 silu_inplace(&mut scratch.gate_buf[..inter]);
625 elementwise_mul(&mut scratch.gate_buf[..inter], &scratch.up_buf[..inter]);
626
627 scratch.down_input[..inter].copy_from_slice(&scratch.gate_buf[..inter]);
628 matmul_bt_f16(
629 &scratch.down_input[..inter],
630 down_w,
631 &mut scratch.ffn_out[..hidden],
632 1,
633 inter,
634 hidden,
635 );
636
637 for i in 0..hidden {
638 scratch.expert_out[i] += weight * scratch.ffn_out[i];
639 }
640 }
641
642 let shared = &moe.shared_expert;
643
644 let mut shared_gate_logit = [0.0f32; 1];
646 matmul_bt_f16(
647 &scratch.input_tmp[..hidden],
648 &shared.shared_expert_gate,
649 &mut shared_gate_logit,
650 1,
651 hidden,
652 1,
653 );
654 let shared_gate = sigmoid(shared_gate_logit[0]);
655
656 matmul_bt_f16(
657 &scratch.input_tmp[..hidden],
658 &shared.gate_proj,
659 &mut scratch.gate_buf[..shared_inter],
660 1,
661 hidden,
662 shared_inter,
663 );
664 matmul_bt_f16(
665 &scratch.input_tmp[..hidden],
666 &shared.up_proj,
667 &mut scratch.up_buf[..shared_inter],
668 1,
669 hidden,
670 shared_inter,
671 );
672
673 silu_inplace(&mut scratch.gate_buf[..shared_inter]);
674 elementwise_mul(
675 &mut scratch.gate_buf[..shared_inter],
676 &scratch.up_buf[..shared_inter],
677 );
678
679 scratch.down_input[..shared_inter].copy_from_slice(&scratch.gate_buf[..shared_inter]);
680 matmul_bt_f16(
681 &scratch.down_input[..shared_inter],
682 &shared.down_proj,
683 &mut scratch.ffn_out[..hidden],
684 1,
685 shared_inter,
686 hidden,
687 );
688
689 for i in 0..hidden {
690 scratch.expert_out[i] += shared_gate * scratch.ffn_out[i];
691 }
692
693 scratch.ffn_out[..hidden].copy_from_slice(&scratch.expert_out[..hidden]);
694}
695
696pub(crate) fn forward_step_f16(
708 weights: &F16ModelWeights,
709 cfg: &Qwen35Config,
710 rope: &RopeTable,
711 token_id: u32,
712 position: usize,
713 gdn_states: &mut [GatedDeltaNetState],
714 kv_cache: &mut KvCache,
715 scratch: &mut ForwardScratch,
716 injected_embedding: Option<&[f32]>,
717 mrope_cos_sin: Option<(&[f32], &[f32])>,
718) -> Result<(), crate::error::InferenceError> {
719 let hidden = cfg.hidden_size;
720
721 scratch.ensure_capacity(cfg, kv_cache.seq_len + 1);
722
723 match injected_embedding {
724 Some(row) => {
729 if row.len() != hidden {
730 return Err(crate::error::InferenceError::InvalidInput(format!(
731 "injected_embedding length {} does not match hidden_size {hidden}",
732 row.len()
733 )));
734 }
735 if let Some(bad) = row.iter().find(|v| !v.is_finite()) {
736 return Err(crate::error::InferenceError::InvalidInput(format!(
737 "injected_embedding contains a non-finite value: {bad}"
738 )));
739 }
740 scratch.hidden[..hidden].copy_from_slice(row);
741 }
742 None => {
743 let embed_start = token_id as usize * hidden;
745 f16_to_f32_slice(
746 &weights.embed_tokens[embed_start..embed_start + hidden],
747 &mut scratch.hidden[..hidden],
748 );
749 }
750 }
751
752 let mut linear_idx = 0usize;
753 let mut full_idx = 0usize;
754
755 for layer_i in 0..cfg.num_hidden_layers {
756 let (attn_weights, common) = &weights.layers[layer_i];
757
758 scratch.residual[..hidden].copy_from_slice(&scratch.hidden[..hidden]);
760
761 qwen35_rms_norm(
763 &mut scratch.hidden[..hidden],
764 &common.input_layernorm,
765 hidden,
766 cfg.rms_norm_eps,
767 );
768
769 match attn_weights {
771 F16AttentionWeights::Linear(gdn_w) => {
772 gated_delta_net_step_fused_f16(
773 &scratch.hidden[..hidden],
774 &mut gdn_states[linear_idx],
775 gdn_w,
776 cfg,
777 &mut scratch.gdn_scratch,
778 &mut scratch.attn_out[..hidden],
779 );
780 linear_idx += 1;
781 }
782 F16AttentionWeights::Full(full_w) => {
783 scratch.attn_out[..hidden].copy_from_slice(&scratch.hidden[..hidden]);
785 full_attention_step_f16(
786 full_w,
787 cache_idx_of(full_idx),
788 position,
789 kv_cache,
790 scratch,
791 cfg,
792 rope,
793 hidden,
794 mrope_cos_sin,
795 );
796 full_idx += 1;
797 }
798 }
799
800 for i in 0..hidden {
802 scratch.hidden[i] = scratch.residual[i] + scratch.attn_out[i];
803 }
804
805 scratch.residual[..hidden].copy_from_slice(&scratch.hidden[..hidden]);
807
808 qwen35_rms_norm(
810 &mut scratch.hidden[..hidden],
811 &common.post_attention_layernorm,
812 hidden,
813 cfg.rms_norm_eps,
814 );
815
816 scratch.ffn_out[..hidden].copy_from_slice(&scratch.hidden[..hidden]);
818 match &common.ffn {
819 F16FeedForwardWeights::Dense {
820 gate_proj,
821 up_proj,
822 down_proj,
823 } => {
824 ffn_step_f16(
825 gate_proj,
826 up_proj,
827 down_proj,
828 scratch,
829 cfg.intermediate_size,
830 hidden,
831 );
832 }
833 F16FeedForwardWeights::Moe(moe) => {
834 moe_ffn_step_f16(moe, scratch, hidden);
835 }
836 }
837
838 for i in 0..hidden {
840 scratch.hidden[i] = scratch.residual[i] + scratch.ffn_out[i];
841 }
842 }
843
844 qwen35_rms_norm(
846 &mut scratch.hidden[..hidden],
847 &weights.final_norm,
848 hidden,
849 cfg.rms_norm_eps,
850 );
851
852 resize(&mut scratch.logits, cfg.vocab_size);
857 matmul_bt_f16(
858 &scratch.hidden[..hidden],
859 &weights.embed_tokens,
860 &mut scratch.logits[..cfg.vocab_size],
861 1,
862 hidden,
863 cfg.vocab_size,
864 );
865
866 Ok(())
867}
868
869#[inline(always)]
871fn cache_idx_of(full_idx: usize) -> usize {
872 full_idx
873}
874
875pub fn generate_f16(
888 weights: &F16ModelWeights,
889 cfg: &Qwen35Config,
890 tokenizer: &BpeTokenizer,
891 rope: &RopeTable,
892 prompt: &str,
893 gen_cfg: &GenerateConfig,
894) -> Result<GenerateOutput, crate::error::InferenceError> {
895 let plan = match prepare_generation(
896 tokenizer,
897 prompt,
898 gen_cfg,
899 cfg.vocab_size,
900 rope.max_positions(),
901 GenerationEntryContract::StandaloneCpu,
902 )? {
903 GenerationPreparation::Ready(plan) => plan,
904 GenerationPreparation::Complete(output) => return Ok(output),
905 };
906 let GenerationPlan {
907 mut rng_state,
908 prompt_ids,
909 prompt_len,
910 ..
911 } = plan;
912
913 let num_linear = cfg.num_linear_attention_layers();
915 let num_full = cfg.num_full_attention_layers();
916 let mut gdn_states: Vec<GatedDeltaNetState> = (0..num_linear)
917 .map(|_| GatedDeltaNetState::new(cfg))
918 .collect();
919 let mut kv_cache = KvCache::new(num_full);
920 let mut scratch = ForwardScratch::new();
921
922 let mut generated_ids: Vec<u32> = Vec::with_capacity(gen_cfg.max_new_tokens);
923 let mut all_ids = prompt_ids.clone();
924
925 for (pos, &token_id) in prompt_ids.iter().enumerate() {
927 forward_step_f16(
928 weights,
929 cfg,
930 rope,
931 token_id,
932 pos,
933 &mut gdn_states,
934 &mut kv_cache,
935 &mut scratch,
936 None,
937 None,
938 )?;
939 if pos < prompt_len - 1 {
940 kv_cache.seq_len += 1;
941 }
942 }
943 kv_cache.seq_len = prompt_len;
944
945 let next_id = sample_token(
947 &scratch.logits[..cfg.vocab_size],
948 gen_cfg,
949 &all_ids,
950 &mut rng_state,
951 );
952
953 if should_stop_token(cfg, gen_cfg, next_id) {
954 return Ok(GenerateOutput {
955 text: String::new(),
956 token_ids: vec![],
957 prompt_tokens: prompt_len,
958 generated_tokens: 0,
959 stopped: true,
960 stop_reason: Some(StopReason::Eos),
961 token_logprobs: vec![],
962 });
963 }
964
965 generated_ids.push(next_id);
966 all_ids.push(next_id);
967
968 let mut stopped = false;
969 let mut stop_reason = StopReason::Length;
970 for _ in 1..gen_cfg.max_new_tokens {
972 let pos = kv_cache.seq_len;
973 let last_token = *all_ids
974 .last()
975 .expect("invariant: prompt or previous sample populated all_ids");
976
977 forward_step_f16(
978 weights,
979 cfg,
980 rope,
981 last_token,
982 pos,
983 &mut gdn_states,
984 &mut kv_cache,
985 &mut scratch,
986 None,
987 None,
988 )?;
989 kv_cache.seq_len += 1;
990
991 let next_id = sample_token(
992 &scratch.logits[..cfg.vocab_size],
993 gen_cfg,
994 &all_ids,
995 &mut rng_state,
996 );
997
998 if should_stop_token(cfg, gen_cfg, next_id) {
999 stopped = true;
1000 stop_reason = StopReason::Eos;
1001 break;
1002 }
1003
1004 generated_ids.push(next_id);
1005 all_ids.push(next_id);
1006 }
1007
1008 let text = decode_tokens(tokenizer, &generated_ids);
1010
1011 Ok(GenerateOutput {
1012 text,
1013 token_ids: generated_ids.clone(),
1014 prompt_tokens: prompt_len,
1015 generated_tokens: generated_ids.len(),
1016 stopped,
1017 stop_reason: Some(stop_reason),
1018 token_logprobs: vec![],
1019 })
1020}
1021
1022pub fn generate_multimodal_f16(
1042 weights: &F16ModelWeights,
1043 cfg: &Qwen35Config,
1044 request: &crate::vision::multimodal::Qwen35VisionRequest,
1045 gen_cfg: &GenerateConfig,
1046) -> Result<GenerateOutput, crate::error::InferenceError> {
1047 request.validate().map_err(|e| {
1048 crate::error::InferenceError::InvalidInput(format!(
1049 "multimodal request failed validation: {e}"
1050 ))
1051 })?;
1052
1053 if let Some(&bad_id) = request
1058 .input_ids
1059 .iter()
1060 .find(|&&id| id as usize >= cfg.vocab_size)
1061 {
1062 return Err(crate::error::InferenceError::InvalidInput(format!(
1063 "input_ids contains out-of-vocabulary token id {bad_id} (vocab_size={})",
1064 cfg.vocab_size
1065 )));
1066 }
1067
1068 let has_image = !request.image_grids.is_empty();
1069
1070 if has_image {
1076 let cfg_image_token_id = cfg.image_token_id.ok_or_else(|| {
1077 crate::error::InferenceError::InvalidInput(
1078 "multimodal request supplied but checkpoint has no image_token_id".to_string(),
1079 )
1080 })?;
1081 if cfg_image_token_id != request.image_token_id {
1082 return Err(crate::error::InferenceError::InvalidInput(format!(
1083 "request image_token_id {} does not match checkpoint image_token_id {cfg_image_token_id}",
1084 request.image_token_id
1085 )));
1086 }
1087 let vision_cfg = cfg.vision_config.as_ref().ok_or_else(|| {
1088 crate::error::InferenceError::InvalidInput(
1089 "multimodal request supplied but checkpoint has no vision_config".to_string(),
1090 )
1091 })?;
1092 if vision_cfg.spatial_merge_size != request.spatial_merge_size {
1093 return Err(crate::error::InferenceError::InvalidInput(format!(
1094 "request spatial_merge_size {} does not match checkpoint \
1095 vision_config.spatial_merge_size {}",
1096 request.spatial_merge_size, vision_cfg.spatial_merge_size
1097 )));
1098 }
1099 if request.decoder_hidden_size != cfg.hidden_size {
1100 return Err(crate::error::InferenceError::InvalidInput(format!(
1101 "request decoder_hidden_size {} does not match checkpoint hidden_size {}",
1102 request.decoder_hidden_size, cfg.hidden_size
1103 )));
1104 }
1105 if vision_cfg.out_hidden_size != cfg.hidden_size {
1106 return Err(crate::error::InferenceError::InvalidInput(format!(
1107 "checkpoint vision_config.out_hidden_size {} does not match decoder \
1108 hidden_size {}",
1109 vision_cfg.out_hidden_size, cfg.hidden_size
1110 )));
1111 }
1112 }
1113
1114 let (positions, tables) = request.build_mrope_tables(cfg)?;
1115
1116 let expected_rope_half = cfg.rope_dim() / 2;
1123 if tables.cos.iter().any(|row| row.len() != expected_rope_half)
1124 || tables.sin.iter().any(|row| row.len() != expected_rope_half)
1125 {
1126 return Err(crate::error::InferenceError::InvalidInput(format!(
1127 "M-RoPE table row width does not match decoder rotary half-width: expected \
1128 {expected_rope_half}"
1129 )));
1130 }
1131
1132 let prompt_ids = &request.input_ids;
1133 let prompt_len = prompt_ids.len();
1134 crate::model::qwen35::check_prompt_not_empty(prompt_len)?;
1135
1136 if gen_cfg.max_new_tokens == 0 {
1137 return Ok(GenerateOutput {
1138 text: String::new(),
1139 token_ids: vec![],
1140 prompt_tokens: prompt_len,
1141 generated_tokens: 0,
1142 stopped: false,
1143 stop_reason: Some(StopReason::Length),
1144 token_logprobs: vec![],
1145 });
1146 }
1147
1148 crate::model::qwen35::check_grammar_not_set(gen_cfg)?;
1149 crate::model::qwen35::check_logprobs_not_set(gen_cfg)?;
1150 crate::model::qwen35::check_stop_strings_not_set(gen_cfg)?;
1151 crate::model::qwen35::check_reasoning_budget_not_set(gen_cfg)?;
1152
1153 let max_context = cfg.max_position_embeddings;
1154 if prompt_len.saturating_add(gen_cfg.max_new_tokens) > max_context {
1155 return Err(crate::error::InferenceError::Inference(format!(
1156 "prompt ({prompt_len} tokens) plus max_new_tokens ({}) exceeds \
1157 model context window ({max_context})",
1158 gen_cfg.max_new_tokens
1159 )));
1160 }
1161
1162 let mut rng_state = match gen_cfg.seed {
1163 Some(s) => {
1164 if s == 0 {
1165 1
1166 } else {
1167 s
1168 }
1169 }
1170 None => {
1171 use std::time::SystemTime;
1172 let t = SystemTime::now()
1173 .duration_since(SystemTime::UNIX_EPOCH)
1174 .map(|d| d.as_nanos() as u64)
1175 .unwrap_or(0x12345678_9abcdef0);
1176 if t == 0 { 1 } else { t }
1177 }
1178 };
1179
1180 let num_linear = cfg.num_linear_attention_layers();
1181 let num_full = cfg.num_full_attention_layers();
1182 let mut gdn_states: Vec<GatedDeltaNetState> = (0..num_linear)
1183 .map(|_| GatedDeltaNetState::new(cfg))
1184 .collect();
1185 let mut kv_cache = KvCache::new(num_full);
1186 let mut scratch = ForwardScratch::new();
1187
1188 let rope = RopeTable::new(cfg.rope_dim(), max_context, cfg.rope_theta);
1198
1199 let mut generated_ids: Vec<u32> = Vec::with_capacity(gen_cfg.max_new_tokens);
1200 let mut all_ids = prompt_ids.clone();
1201
1202 let mut visual_row = 0usize;
1206 for (pos, &token_id) in prompt_ids.iter().enumerate() {
1207 let injected = if token_id == request.image_token_id {
1208 let start = visual_row * request.decoder_hidden_size;
1209 let end = start + request.decoder_hidden_size;
1210 visual_row += 1;
1211 Some(&request.post_merger_rows[start..end])
1212 } else {
1213 None
1214 };
1215 let cos_sin = if has_image {
1216 Some((tables.cos[pos].as_slice(), tables.sin[pos].as_slice()))
1217 } else {
1218 None
1219 };
1220
1221 forward_step_f16(
1222 weights,
1223 cfg,
1224 &rope,
1225 token_id,
1226 pos,
1227 &mut gdn_states,
1228 &mut kv_cache,
1229 &mut scratch,
1230 injected,
1231 cos_sin,
1232 )?;
1233 if pos < prompt_len - 1 {
1234 kv_cache.seq_len += 1;
1235 }
1236 }
1237 kv_cache.seq_len = prompt_len;
1238
1239 let next_id = sample_token(
1240 &scratch.logits[..cfg.vocab_size],
1241 gen_cfg,
1242 &all_ids,
1243 &mut rng_state,
1244 );
1245
1246 if should_stop_token(cfg, gen_cfg, next_id) {
1247 return Ok(GenerateOutput {
1248 text: String::new(),
1249 token_ids: vec![],
1250 prompt_tokens: prompt_len,
1251 generated_tokens: 0,
1252 stopped: true,
1253 stop_reason: Some(StopReason::Eos),
1254 token_logprobs: vec![],
1255 });
1256 }
1257
1258 generated_ids.push(next_id);
1259 all_ids.push(next_id);
1260
1261 let mut stopped = false;
1262 let mut stop_reason = StopReason::Length;
1263 for _ in 1..gen_cfg.max_new_tokens {
1267 let physical_pos = kv_cache.seq_len;
1268 let last_token = *all_ids
1269 .last()
1270 .expect("invariant: prompt or previous sample populated all_ids");
1271
1272 let decode_cos_sin;
1275 let mrope_cos_sin = if has_image {
1276 let decode_axis =
1277 crate::vision::qwen35_mrope::decode_position(physical_pos, positions.rope_delta)?;
1278 decode_cos_sin = request.build_decode_cos_sin(cfg, decode_axis)?;
1279 if decode_cos_sin.0.len() != expected_rope_half
1284 || decode_cos_sin.1.len() != expected_rope_half
1285 {
1286 return Err(crate::error::InferenceError::InvalidInput(format!(
1287 "decode-time M-RoPE row width does not match decoder rotary \
1288 half-width: expected {expected_rope_half}"
1289 )));
1290 }
1291 Some((decode_cos_sin.0.as_slice(), decode_cos_sin.1.as_slice()))
1292 } else {
1293 None
1294 };
1295
1296 forward_step_f16(
1297 weights,
1298 cfg,
1299 &rope,
1300 last_token,
1301 physical_pos,
1302 &mut gdn_states,
1303 &mut kv_cache,
1304 &mut scratch,
1305 None,
1306 mrope_cos_sin,
1307 )?;
1308 kv_cache.seq_len += 1;
1309
1310 let next_id = sample_token(
1311 &scratch.logits[..cfg.vocab_size],
1312 gen_cfg,
1313 &all_ids,
1314 &mut rng_state,
1315 );
1316
1317 if should_stop_token(cfg, gen_cfg, next_id) {
1318 stopped = true;
1319 stop_reason = StopReason::Eos;
1320 break;
1321 }
1322
1323 generated_ids.push(next_id);
1324 all_ids.push(next_id);
1325 }
1326
1327 Ok(GenerateOutput {
1328 text: String::new(),
1329 token_ids: generated_ids.clone(),
1330 prompt_tokens: prompt_len,
1331 generated_tokens: generated_ids.len(),
1332 stopped,
1333 stop_reason: Some(stop_reason),
1334 token_logprobs: vec![],
1335 })
1336}
1337
1338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1356pub enum PoolingStrategy {
1357 MeanVisualTokens,
1363 LastToken,
1367}
1368
1369pub fn prefill_hidden_states_f16(
1387 weights: &F16ModelWeights,
1388 cfg: &Qwen35Config,
1389 request: &Qwen35VisionRequest,
1390) -> Result<Vec<f32>, crate::error::InferenceError> {
1391 request.validate().map_err(|e| {
1392 crate::error::InferenceError::InvalidInput(format!(
1393 "multimodal request failed validation: {e}"
1394 ))
1395 })?;
1396
1397 if let Some(&bad_id) = request
1398 .input_ids
1399 .iter()
1400 .find(|&&id| id as usize >= cfg.vocab_size)
1401 {
1402 return Err(crate::error::InferenceError::InvalidInput(format!(
1403 "input_ids contains out-of-vocabulary token id {bad_id} (vocab_size={})",
1404 cfg.vocab_size
1405 )));
1406 }
1407
1408 let has_image = !request.image_grids.is_empty();
1409
1410 if has_image {
1413 let cfg_image_token_id = cfg.image_token_id.ok_or_else(|| {
1414 crate::error::InferenceError::InvalidInput(
1415 "multimodal request supplied but checkpoint has no image_token_id".to_string(),
1416 )
1417 })?;
1418 if cfg_image_token_id != request.image_token_id {
1419 return Err(crate::error::InferenceError::InvalidInput(format!(
1420 "request image_token_id {} does not match checkpoint image_token_id {cfg_image_token_id}",
1421 request.image_token_id
1422 )));
1423 }
1424 let vision_cfg = cfg.vision_config.as_ref().ok_or_else(|| {
1425 crate::error::InferenceError::InvalidInput(
1426 "multimodal request supplied but checkpoint has no vision_config".to_string(),
1427 )
1428 })?;
1429 if vision_cfg.spatial_merge_size != request.spatial_merge_size {
1430 return Err(crate::error::InferenceError::InvalidInput(format!(
1431 "request spatial_merge_size {} does not match checkpoint \
1432 vision_config.spatial_merge_size {}",
1433 request.spatial_merge_size, vision_cfg.spatial_merge_size
1434 )));
1435 }
1436 if request.decoder_hidden_size != cfg.hidden_size {
1437 return Err(crate::error::InferenceError::InvalidInput(format!(
1438 "request decoder_hidden_size {} does not match checkpoint hidden_size {}",
1439 request.decoder_hidden_size, cfg.hidden_size
1440 )));
1441 }
1442 if vision_cfg.out_hidden_size != cfg.hidden_size {
1443 return Err(crate::error::InferenceError::InvalidInput(format!(
1444 "checkpoint vision_config.out_hidden_size {} does not match decoder \
1445 hidden_size {}",
1446 vision_cfg.out_hidden_size, cfg.hidden_size
1447 )));
1448 }
1449 }
1450
1451 let prompt_len = request.input_ids.len();
1455 crate::model::qwen35::check_prompt_not_empty(prompt_len)?;
1456 let max_context = cfg.max_position_embeddings;
1457 if prompt_len > max_context {
1458 return Err(crate::error::InferenceError::Inference(format!(
1459 "prompt ({prompt_len} tokens) exceeds model context window ({max_context})"
1460 )));
1461 }
1462
1463 let (_positions, tables) = request.build_mrope_tables(cfg)?;
1464
1465 let expected_rope_half = cfg.rope_dim() / 2;
1466 if tables.cos.iter().any(|row| row.len() != expected_rope_half)
1467 || tables.sin.iter().any(|row| row.len() != expected_rope_half)
1468 {
1469 return Err(crate::error::InferenceError::InvalidInput(format!(
1470 "M-RoPE table row width does not match decoder rotary half-width: expected \
1471 {expected_rope_half}"
1472 )));
1473 }
1474
1475 let prompt_ids = &request.input_ids;
1476
1477 let num_linear = cfg.num_linear_attention_layers();
1478 let num_full = cfg.num_full_attention_layers();
1479 let mut gdn_states: Vec<GatedDeltaNetState> = (0..num_linear)
1480 .map(|_| GatedDeltaNetState::new(cfg))
1481 .collect();
1482 let mut kv_cache = KvCache::new(num_full);
1483 let mut scratch = ForwardScratch::new();
1484
1485 let rope = RopeTable::new(cfg.rope_dim(), max_context, cfg.rope_theta);
1489
1490 let hidden = cfg.hidden_size;
1491 let mut hidden_states: Vec<f32> = Vec::with_capacity(prompt_len * hidden);
1492
1493 let mut visual_row = 0usize;
1494 for (pos, &token_id) in prompt_ids.iter().enumerate() {
1495 let injected = if token_id == request.image_token_id {
1496 let start = visual_row * request.decoder_hidden_size;
1497 let end = start + request.decoder_hidden_size;
1498 visual_row += 1;
1499 Some(&request.post_merger_rows[start..end])
1500 } else {
1501 None
1502 };
1503 let cos_sin = if has_image {
1504 Some((tables.cos[pos].as_slice(), tables.sin[pos].as_slice()))
1505 } else {
1506 None
1507 };
1508
1509 forward_step_f16(
1510 weights,
1511 cfg,
1512 &rope,
1513 token_id,
1514 pos,
1515 &mut gdn_states,
1516 &mut kv_cache,
1517 &mut scratch,
1518 injected,
1519 cos_sin,
1520 )?;
1521
1522 hidden_states.extend_from_slice(&scratch.hidden[..hidden]);
1523
1524 if pos < prompt_len - 1 {
1525 kv_cache.seq_len += 1;
1526 }
1527 }
1528 kv_cache.seq_len = prompt_len;
1529
1530 Ok(hidden_states)
1531}
1532
1533fn mean_pool_rows(hidden_states: &[f32], hidden_size: usize, positions: &[usize]) -> Vec<f32> {
1538 debug_assert!(!positions.is_empty());
1539 let mut out = vec![0.0f32; hidden_size];
1540 for &p in positions {
1541 let row = &hidden_states[p * hidden_size..(p + 1) * hidden_size];
1542 for (o, &v) in out.iter_mut().zip(row) {
1543 *o += v;
1544 }
1545 }
1546 let n = positions.len() as f32;
1547 for o in &mut out {
1548 *o /= n;
1549 }
1550 out
1551}
1552
1553fn l2_normalize_owned(mut v: Vec<f32>) -> Vec<f32> {
1556 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
1557 if norm > 0.0 && norm.is_finite() {
1558 for x in &mut v {
1559 *x /= norm;
1560 }
1561 }
1562 v
1563}
1564
1565fn pool_hidden_states(
1570 hidden_states: &[f32],
1571 hidden_size: usize,
1572 seq_len: usize,
1573 image_pad_positions: &[usize],
1574 pooling: PoolingStrategy,
1575) -> Vec<f32> {
1576 match pooling {
1577 PoolingStrategy::LastToken => {
1578 hidden_states[(seq_len - 1) * hidden_size..seq_len * hidden_size].to_vec()
1579 }
1580 PoolingStrategy::MeanVisualTokens => {
1581 if image_pad_positions.is_empty() {
1582 let all: Vec<usize> = (0..seq_len).collect();
1583 mean_pool_rows(hidden_states, hidden_size, &all)
1584 } else {
1585 mean_pool_rows(hidden_states, hidden_size, image_pad_positions)
1586 }
1587 }
1588 }
1589}
1590
1591pub fn embed_image_f16(
1603 weights: &F16ModelWeights,
1604 cfg: &Qwen35Config,
1605 request: &Qwen35VisionRequest,
1606 pooling: PoolingStrategy,
1607) -> Result<Vec<f32>, crate::error::InferenceError> {
1608 let hidden_states = prefill_hidden_states_f16(weights, cfg, request)?;
1609 let seq_len = request.input_ids.len();
1610 let image_pad_positions: Vec<usize> = request
1611 .input_ids
1612 .iter()
1613 .enumerate()
1614 .filter(|&(_, &id)| id == request.image_token_id)
1615 .map(|(i, _)| i)
1616 .collect();
1617 let pooled = pool_hidden_states(
1618 &hidden_states,
1619 cfg.hidden_size,
1620 seq_len,
1621 &image_pad_positions,
1622 pooling,
1623 );
1624 Ok(l2_normalize_owned(pooled))
1625}
1626
1627pub fn embed_text_vlm_f16(
1647 weights: &F16ModelWeights,
1648 cfg: &Qwen35Config,
1649 tokenizer: &BpeTokenizer,
1650 prompt: &str,
1651 pooling: PoolingStrategy,
1652) -> Result<Vec<f32>, crate::error::InferenceError> {
1653 let input = tokenizer.tokenize(prompt);
1654 let prompt_ids: Vec<u32> = input.input_ids[..input.real_length].to_vec();
1655 crate::model::qwen35::check_prompt_not_empty(prompt_ids.len())?;
1656
1657 if let Some(&bad_id) = prompt_ids.iter().find(|&&id| id as usize >= cfg.vocab_size) {
1658 return Err(crate::error::InferenceError::InvalidInput(format!(
1659 "prompt contains out-of-vocabulary token id {bad_id} (vocab_size={})",
1660 cfg.vocab_size
1661 )));
1662 }
1663
1664 let image_token_id = cfg.image_token_id.unwrap_or(u32::MAX);
1669 if prompt_ids.contains(&image_token_id) {
1670 return Err(crate::error::InferenceError::InvalidInput(
1671 "tokenized prompt unexpectedly contains the checkpoint's image_token_id".to_string(),
1672 ));
1673 }
1674
1675 let request = Qwen35VisionRequest {
1676 input_ids: prompt_ids,
1677 image_grids: vec![],
1678 post_merger_rows: vec![],
1679 image_token_id,
1680 spatial_merge_size: cfg
1681 .vision_config
1682 .as_ref()
1683 .map(|v| v.spatial_merge_size)
1684 .unwrap_or(2),
1685 decoder_hidden_size: cfg.hidden_size,
1686 };
1687
1688 embed_image_f16(weights, cfg, &request, pooling)
1689}
1690
1691#[cfg(test)]
1696mod tests {
1697 use super::*;
1698
1699 #[test]
1700 #[allow(clippy::type_complexity)]
1701 fn test_f16_forward_compiles() {
1702 let cfg = Qwen35Config::qwen35_2b();
1705
1706 let _fn_ptr: fn(
1708 &F16ModelWeights,
1709 &Qwen35Config,
1710 &RopeTable,
1711 u32,
1712 usize,
1713 &mut [GatedDeltaNetState],
1714 &mut KvCache,
1715 &mut ForwardScratch,
1716 Option<&[f32]>,
1717 Option<(&[f32], &[f32])>,
1718 ) -> Result<(), crate::error::InferenceError> = forward_step_f16;
1719
1720 let _gdn_fn_ptr: fn(
1722 &[f32],
1723 &mut GatedDeltaNetState,
1724 &F16GatedDeltaNetWeights,
1725 &Qwen35Config,
1726 &mut GatedDeltaNetFusedScratch,
1727 &mut [f32],
1728 ) = gated_delta_net_step_fused_f16;
1729
1730 let _gen_fn_ptr: fn(
1732 &F16ModelWeights,
1733 &Qwen35Config,
1734 &BpeTokenizer,
1735 &RopeTable,
1736 &str,
1737 &GenerateConfig,
1738 ) -> Result<GenerateOutput, crate::error::InferenceError> = generate_f16;
1739
1740 assert!(cfg.num_full_attention_layers() > 0);
1742 assert!(cfg.num_linear_attention_layers() > 0);
1743 assert_eq!(
1744 cfg.num_full_attention_layers() + cfg.num_linear_attention_layers(),
1745 cfg.num_hidden_layers
1746 );
1747 }
1748
1749 #[test]
1759 fn test_full_attn_step_f16_rope_stride_half_parity() {
1760 use crate::model::qwen35_config::LayerType;
1761 use crate::weights::f16_weights::f32_to_f16_slice;
1762
1763 let head_dim: usize = 32;
1764 let num_q_heads: usize = 1;
1765 let num_kv_heads: usize = 1;
1766 let hidden: usize = 64;
1767 let q_dim = num_q_heads * head_dim;
1768 let kv_dim = num_kv_heads * head_dim;
1769 let position: usize = 3;
1770
1771 let cfg = Qwen35Config {
1772 hidden_size: hidden,
1773 num_hidden_layers: 2,
1774 vocab_size: 128,
1775 intermediate_size: 128,
1776 rms_norm_eps: 1e-6,
1777 num_attention_heads: num_q_heads,
1778 num_key_value_heads: num_kv_heads,
1779 head_dim,
1780 rope_theta: 10_000.0,
1781 partial_rotary_factor: 0.5, rope_parameters: None,
1783 linear_num_key_heads: 2,
1784 linear_num_value_heads: Some(2),
1785 linear_key_head_dim: 32,
1786 linear_value_head_dim: 32,
1787 linear_conv_kernel_dim: 4,
1788 num_experts: None,
1789 num_experts_per_tok: None,
1790 moe_intermediate_size: None,
1791 shared_expert_intermediate_size: None,
1792 output_router_logits: false,
1793 router_aux_loss_coef: None,
1794 tie_word_embeddings: true,
1795 full_attention_interval: 2,
1796 layer_types: vec![LayerType::LinearAttention, LayerType::FullAttention],
1797 layer_mask: vec![true; 2],
1798 eos_token_id: 127,
1799 max_position_embeddings: 512,
1800 mtp_num_hidden_layers: 0,
1801 mtp_use_dedicated_embeddings: false,
1802 quarot_rotation_seed: None,
1803 vision_config: None,
1804 image_token_id: None,
1805 video_token_id: None,
1806 vision_start_token_id: None,
1807 vision_end_token_id: None,
1808 };
1809
1810 let rope_dim = cfg.rope_dim(); let half = rope_dim / 2; let rope = RopeTable::new(rope_dim, 512, cfg.rope_theta);
1813
1814 let to_f16 = |src: &[f32]| -> Vec<u16> {
1816 let mut dst = vec![0u16; src.len()];
1817 f32_to_f16_slice(src, &mut dst);
1818 dst
1819 };
1820
1821 let mut k_proj_f32 = vec![0.0f32; kv_dim * hidden];
1823 for j in 0..kv_dim {
1824 k_proj_f32[j * hidden + j] = 1.0;
1825 }
1826
1827 let mut q_proj_f32 = vec![0.0f32; 2 * q_dim * hidden];
1831 for j in 0..q_dim {
1832 q_proj_f32[j * hidden + j] = 1.0;
1833 }
1834
1835 let weights = F16FullAttentionLayerWeights {
1836 q_proj: to_f16(&q_proj_f32),
1837 k_proj: to_f16(&k_proj_f32),
1838 v_proj: to_f16(&vec![0.0f32; kv_dim * hidden]),
1839 o_proj: to_f16(&vec![0.0f32; hidden * q_dim]),
1840 q_norm: vec![0.0f32; head_dim],
1841 k_norm: vec![0.0f32; head_dim],
1842 };
1843
1844 let input: Vec<f32> = (0..hidden).map(|i| (i as f32 + 1.0) * 0.07).collect();
1846
1847 let mut scratch = ForwardScratch::new();
1848 scratch.ensure_capacity(&cfg, 2);
1849 scratch.attn_out[..hidden].copy_from_slice(&input);
1850
1851 let mut kv_cache = KvCache::new(1);
1852 full_attention_step_f16(
1853 &weights,
1854 0,
1855 position,
1856 &mut kv_cache,
1857 &mut scratch,
1858 &cfg,
1859 &rope,
1860 hidden,
1861 None,
1862 );
1863
1864 let mut k_ref = vec![0.0f32; kv_dim];
1870 matmul_bt_f16(&input, &weights.k_proj, &mut k_ref, 1, hidden, kv_dim);
1871 qwen35_rms_norm(&mut k_ref, &weights.k_norm, head_dim, cfg.rms_norm_eps);
1872
1873 let base = position * half;
1875 for i in 0..half {
1876 let cos_val = rope.cos_at(base + i);
1877 let sin_val = rope.sin_at(base + i);
1878 let x0 = k_ref[i];
1879 let x1 = k_ref[half + i];
1880 k_ref[i] = x0 * cos_val - x1 * sin_val;
1881 k_ref[half + i] = x0 * sin_val + x1 * cos_val;
1882 }
1883
1884 let k_cached = &kv_cache.k[0][..kv_dim];
1885 let max_k_diff = k_cached
1886 .iter()
1887 .zip(k_ref.iter())
1888 .map(|(a, b)| (a - b).abs())
1889 .fold(0.0f32, f32::max);
1890
1891 assert!(
1892 max_k_diff < 1e-4,
1893 "cpu F16 K-loop stride-half RoPE diverges from reference: max_k_diff = {max_k_diff:.6}. \
1894 With interleaved pairing the diff is O(0.1-1). Bug: #392."
1895 );
1896
1897 let mut q_and_gate_ref = vec![0.0f32; 2 * q_dim];
1901 matmul_bt_f16(
1902 &input,
1903 &weights.q_proj,
1904 &mut q_and_gate_ref,
1905 1,
1906 hidden,
1907 2 * q_dim,
1908 );
1909 let mut q_ref = q_and_gate_ref[..q_dim].to_vec();
1910 qwen35_rms_norm(&mut q_ref, &weights.q_norm, head_dim, cfg.rms_norm_eps);
1911
1912 for i in 0..half {
1913 let cos_val = rope.cos_at(base + i);
1914 let sin_val = rope.sin_at(base + i);
1915 let x0 = q_ref[i];
1916 let x1 = q_ref[half + i];
1917 q_ref[i] = x0 * cos_val - x1 * sin_val;
1918 q_ref[half + i] = x0 * sin_val + x1 * cos_val;
1919 }
1920
1921 let max_q_diff = scratch.q_buf[..q_dim]
1922 .iter()
1923 .zip(q_ref.iter())
1924 .map(|(a, b)| (a - b).abs())
1925 .fold(0.0f32, f32::max);
1926
1927 assert!(
1928 max_q_diff < 1e-4,
1929 "cpu F16 Q-loop stride-half RoPE diverges from reference: max_q_diff = {max_q_diff:.6}. \
1930 With interleaved pairing the diff is O(0.1-1). Bug: #392."
1931 );
1932 }
1933
1934 #[test]
1940 fn test_full_attn_step_f16_nan_cached_score_fails_closed() {
1941 use crate::model::qwen35_config::LayerType;
1942 use crate::weights::f16_weights::f32_to_f16_slice;
1943
1944 let head_dim: usize = 32;
1945 let num_q_heads: usize = 1;
1946 let num_kv_heads: usize = 1;
1947 let hidden: usize = 64;
1948 let q_dim = num_q_heads * head_dim;
1949 let kv_dim = num_kv_heads * head_dim;
1950 let position: usize = 1;
1951
1952 let cfg = Qwen35Config {
1953 hidden_size: hidden,
1954 num_hidden_layers: 2,
1955 vocab_size: 128,
1956 intermediate_size: 128,
1957 rms_norm_eps: 1e-6,
1958 num_attention_heads: num_q_heads,
1959 num_key_value_heads: num_kv_heads,
1960 head_dim,
1961 rope_theta: 10_000.0,
1962 partial_rotary_factor: 0.5,
1963 rope_parameters: None,
1964 linear_num_key_heads: 2,
1965 linear_num_value_heads: Some(2),
1966 linear_key_head_dim: 32,
1967 linear_value_head_dim: 32,
1968 linear_conv_kernel_dim: 4,
1969 num_experts: None,
1970 num_experts_per_tok: None,
1971 moe_intermediate_size: None,
1972 shared_expert_intermediate_size: None,
1973 output_router_logits: false,
1974 router_aux_loss_coef: None,
1975 tie_word_embeddings: true,
1976 full_attention_interval: 2,
1977 layer_types: vec![LayerType::LinearAttention, LayerType::FullAttention],
1978 layer_mask: vec![true; 2],
1979 eos_token_id: 127,
1980 max_position_embeddings: 512,
1981 mtp_num_hidden_layers: 0,
1982 mtp_use_dedicated_embeddings: false,
1983 quarot_rotation_seed: None,
1984 vision_config: None,
1985 image_token_id: None,
1986 video_token_id: None,
1987 vision_start_token_id: None,
1988 vision_end_token_id: None,
1989 };
1990
1991 let rope = RopeTable::new(cfg.rope_dim(), 512, cfg.rope_theta);
1992
1993 let to_f16 = |src: &[f32]| -> Vec<u16> {
1994 let mut dst = vec![0u16; src.len()];
1995 f32_to_f16_slice(src, &mut dst);
1996 dst
1997 };
1998
1999 let mut k_proj_f32 = vec![0.0f32; kv_dim * hidden];
2004 for j in 0..kv_dim {
2005 k_proj_f32[j * hidden + j] = 1.0;
2006 }
2007 let mut v_proj_f32 = vec![0.0f32; kv_dim * hidden];
2008 for j in 0..kv_dim {
2009 v_proj_f32[j * hidden + j] = 1.0;
2010 }
2011 let mut q_proj_f32 = vec![0.0f32; 2 * q_dim * hidden];
2012 for j in 0..q_dim {
2013 q_proj_f32[j * hidden + j] = 1.0;
2014 }
2015
2016 let weights = F16FullAttentionLayerWeights {
2017 q_proj: to_f16(&q_proj_f32),
2018 k_proj: to_f16(&k_proj_f32),
2019 v_proj: to_f16(&v_proj_f32),
2020 o_proj: to_f16(&vec![0.0f32; hidden * q_dim]),
2021 q_norm: vec![0.0f32; head_dim],
2022 k_norm: vec![0.0f32; head_dim],
2023 };
2024
2025 let input: Vec<f32> = (0..hidden).map(|i| (i as f32 + 1.0) * 0.07).collect();
2026
2027 let mut scratch = ForwardScratch::new();
2028 scratch.ensure_capacity(&cfg, 2);
2029 scratch.attn_out[..hidden].copy_from_slice(&input);
2030
2031 let mut kv_cache = KvCache::new(1);
2033 let mut poisoned_k = vec![0.0f32; kv_dim];
2034 poisoned_k[0] = f32::NAN;
2035 let finite_v = vec![5.0f32; kv_dim];
2036 kv_cache.append_kv(0, &poisoned_k, &finite_v);
2037 kv_cache.seq_len = 1;
2038
2039 full_attention_step_f16(
2040 &weights,
2041 0,
2042 position,
2043 &mut kv_cache,
2044 &mut scratch,
2045 &cfg,
2046 &rope,
2047 hidden,
2048 None,
2049 );
2050
2051 assert!(
2052 scratch.context[..head_dim].iter().all(|&v| v == 0.0),
2053 "expected exact-zero context for a NaN-poisoned cached score, \
2054 got {:?}",
2055 &scratch.context[..head_dim]
2056 );
2057 }
2058
2059 #[test]
2060 fn test_gdn_f16_step_with_zeros() {
2061 let cfg = Qwen35Config::qwen35_2b();
2063 let hidden = cfg.hidden_size;
2064 let qkv_dim = cfg.linear_qkv_dim();
2065 let output_dim = cfg.linear_output_dim();
2066 let num_heads = cfg.linear_num_key_heads;
2067 let kernel_size = cfg.linear_conv_kernel_dim;
2068
2069 let weights = F16GatedDeltaNetWeights {
2070 in_proj_qkv: vec![0u16; qkv_dim * hidden],
2071 in_proj_qkv_rows: qkv_dim,
2072 in_proj_qkv_cols: hidden,
2073 in_proj_z: vec![0u16; output_dim * hidden],
2074 in_proj_z_rows: output_dim,
2075 in_proj_z_cols: hidden,
2076 in_proj_b: vec![0u16; num_heads * hidden],
2077 in_proj_b_rows: num_heads,
2078 in_proj_b_cols: hidden,
2079 in_proj_a: vec![0u16; num_heads * hidden],
2080 in_proj_a_rows: num_heads,
2081 in_proj_a_cols: hidden,
2082 a_log: vec![0.0f32; num_heads],
2083 dt_bias: vec![0.0f32; num_heads],
2084 conv1d_weight: vec![0.0f32; qkv_dim * kernel_size],
2085 conv_dim: qkv_dim,
2086 kernel_size,
2087 norm_weight: vec![0.0f32; output_dim],
2088 out_proj: vec![0u16; hidden * output_dim],
2089 out_proj_rows: hidden,
2090 out_proj_cols: output_dim,
2091 };
2092
2093 let mut state = GatedDeltaNetState::new(&cfg);
2094 let mut scratch = GatedDeltaNetFusedScratch::default();
2095 let input = vec![0.0f32; hidden];
2096 let mut output = vec![0.0f32; hidden];
2097
2098 gated_delta_net_step_fused_f16(
2099 &input,
2100 &mut state,
2101 &weights,
2102 &cfg,
2103 &mut scratch,
2104 &mut output,
2105 );
2106
2107 for &v in &output[..hidden] {
2109 assert_eq!(
2110 v, 0.0,
2111 "zero weights + zero input should produce zero output"
2112 );
2113 }
2114 }
2115
2116 #[test]
2126 fn test_moe_ffn_step_f16_nan_router_fails_closed_no_panic() {
2127 use crate::weights::f16_weights::{
2128 F16, F16MoeLayerWeights, F16MoeRouter, F16RoutedExperts, F16SharedExpert,
2129 };
2130 let num_experts = 4usize;
2131 let hidden = 4usize;
2132 let inter = 2usize;
2133 let shared_inter = 2usize;
2134 let top_k = 2usize;
2135
2136 let nan16 = F16::from_f32(f32::NAN).0;
2137 let zeros = |n: usize| vec![F16::from_f32(0.0).0; n];
2138
2139 let router = F16MoeRouter::new(
2141 vec![nan16; num_experts * hidden],
2142 num_experts,
2143 top_k,
2144 hidden,
2145 )
2146 .unwrap();
2147 let experts = F16RoutedExperts::new(
2148 zeros(num_experts * 2 * inter * hidden),
2149 zeros(num_experts * hidden * inter),
2150 num_experts,
2151 hidden,
2152 inter,
2153 )
2154 .unwrap();
2155 let shared = F16SharedExpert::new(
2156 zeros(shared_inter * hidden),
2157 zeros(shared_inter * hidden),
2158 zeros(hidden * shared_inter),
2159 zeros(hidden),
2160 hidden,
2161 shared_inter,
2162 )
2163 .unwrap();
2164 let moe = F16MoeLayerWeights {
2165 router,
2166 experts,
2167 shared_expert: shared,
2168 };
2169
2170 let mut scratch = ForwardScratch::new();
2171 let buf = inter.max(shared_inter);
2172 scratch.ffn_out.resize(hidden, 1.0);
2173 scratch.input_tmp.resize(hidden, 0.0);
2174 scratch.expert_out.resize(hidden, 0.0);
2175 scratch.gate_buf.resize(buf, 0.0);
2176 scratch.up_buf.resize(buf, 0.0);
2177 scratch.down_input.resize(buf, 0.0);
2178 scratch.router_logits.resize(num_experts, 0.0);
2179 scratch.router_selected.resize(top_k, (usize::MAX, 0.0));
2180
2181 moe_ffn_step_f16(&moe, &mut scratch, hidden);
2183
2184 assert!(
2185 scratch.router_selected[..top_k]
2186 .iter()
2187 .all(|(id, _)| *id < num_experts),
2188 "degenerate f16 router must not leave a usize::MAX sentinel selected"
2189 );
2190 }
2191
2192 #[test]
2200 fn test_moe_ffn_step_f16_finite_max_nan_tail_fails_closed() {
2201 use crate::weights::f16_weights::{
2202 F16, F16MoeLayerWeights, F16MoeRouter, F16RoutedExperts, F16SharedExpert,
2203 };
2204 let num_experts = 4usize;
2205 let hidden = 4usize;
2206 let inter = 2usize;
2207 let shared_inter = 2usize;
2208 let top_k = 2usize;
2209
2210 let nan16 = F16::from_f32(f32::NAN).0;
2211 let zeros = |n: usize| vec![F16::from_f32(0.0).0; n];
2212
2213 let mut gate = zeros(num_experts * hidden);
2216 for w in &mut gate[..hidden] {
2217 *w = nan16;
2218 }
2219 let router = F16MoeRouter::new(gate, num_experts, top_k, hidden).unwrap();
2220 let experts = F16RoutedExperts::new(
2221 zeros(num_experts * 2 * inter * hidden),
2222 zeros(num_experts * hidden * inter),
2223 num_experts,
2224 hidden,
2225 inter,
2226 )
2227 .unwrap();
2228 let shared = F16SharedExpert::new(
2229 zeros(shared_inter * hidden),
2230 zeros(shared_inter * hidden),
2231 zeros(hidden * shared_inter),
2232 zeros(hidden),
2233 hidden,
2234 shared_inter,
2235 )
2236 .unwrap();
2237 let moe = F16MoeLayerWeights {
2238 router,
2239 experts,
2240 shared_expert: shared,
2241 };
2242
2243 let mut scratch = ForwardScratch::new();
2244 let buf = inter.max(shared_inter);
2245 scratch.ffn_out.resize(hidden, 1.0);
2246 scratch.input_tmp.resize(hidden, 0.0);
2247 scratch.expert_out.resize(hidden, 0.0);
2248 scratch.gate_buf.resize(buf, 0.0);
2249 scratch.up_buf.resize(buf, 0.0);
2250 scratch.down_input.resize(buf, 0.0);
2251 scratch.router_logits.resize(num_experts, 0.0);
2252 scratch.router_selected.resize(top_k, (usize::MAX, 0.0));
2253
2254 moe_ffn_step_f16(&moe, &mut scratch, hidden);
2255
2256 assert!(
2257 scratch.router_logits[..num_experts]
2258 .iter()
2259 .all(|p| *p == 0.0),
2260 "finite-max + NaN-tail router row must fail closed to all-zero probs \
2261 (a max-only guard would miss this)"
2262 );
2263 }
2264
2265 fn zero_layer_f16_fixture() -> (Qwen35Config, F16ModelWeights, RopeTable, BpeTokenizer) {
2280 use std::collections::HashMap;
2281
2282 let hidden = 4usize;
2283 let vocab = 8usize;
2284
2285 let cfg = Qwen35Config {
2286 hidden_size: hidden,
2287 num_hidden_layers: 0,
2288 vocab_size: vocab,
2289 intermediate_size: 4,
2290 rms_norm_eps: 1e-6,
2291 num_attention_heads: 1,
2292 num_key_value_heads: 1,
2293 head_dim: 4,
2294 rope_theta: 10_000.0,
2295 partial_rotary_factor: 0.5,
2296 rope_parameters: None,
2297 linear_num_key_heads: 1,
2298 linear_num_value_heads: Some(1),
2299 linear_key_head_dim: 4,
2300 linear_value_head_dim: 4,
2301 linear_conv_kernel_dim: 4,
2302 num_experts: None,
2303 num_experts_per_tok: None,
2304 moe_intermediate_size: None,
2305 shared_expert_intermediate_size: None,
2306 output_router_logits: false,
2307 router_aux_loss_coef: None,
2308 tie_word_embeddings: true,
2309 full_attention_interval: 2,
2310 layer_types: vec![],
2311 layer_mask: vec![],
2312 eos_token_id: 5,
2315 max_position_embeddings: 512,
2316 mtp_num_hidden_layers: 0,
2317 mtp_use_dedicated_embeddings: false,
2318 quarot_rotation_seed: None,
2319 vision_config: None,
2320 image_token_id: None,
2321 video_token_id: None,
2322 vision_start_token_id: None,
2323 vision_end_token_id: None,
2324 };
2325
2326 let weights = F16ModelWeights {
2329 embed_tokens: vec![0u16; vocab * hidden],
2330 final_norm: vec![0.0f32; hidden],
2331 layers: vec![],
2332 };
2333
2334 let rope = RopeTable::new(2, 64, 10_000.0);
2336
2337 let mut vocab_map: HashMap<String, u32> = HashMap::new();
2338 for (i, c) in ["h", "e", "l", "o", "w", "r", "d", "!"].iter().enumerate() {
2339 vocab_map.insert((*c).to_string(), i as u32);
2340 }
2341 let merges = vec![
2342 ("h".to_string(), "e".to_string()),
2343 ("he".to_string(), "l".to_string()),
2344 ];
2345 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab_map, merges).unwrap();
2346
2347 (cfg, weights, rope, tokenizer)
2348 }
2349
2350 #[test]
2359 fn test_generate_f16_rejects_context_overflow() {
2360 let (cfg, weights, rope, tokenizer) = zero_layer_f16_fixture();
2361 let max_context = rope.max_positions(); let gen_cfg = GenerateConfig {
2364 max_new_tokens: max_context,
2365 ..Default::default()
2366 };
2367 let err = generate_f16(&weights, &cfg, &tokenizer, &rope, "hello", &gen_cfg)
2368 .expect_err("request beyond context window must error, not panic");
2369 let msg = format!("{err}");
2370 assert!(
2371 msg.contains("context window"),
2372 "error must name the context window; got: {msg}"
2373 );
2374 }
2375
2376 #[test]
2388 fn test_generate_f16_honors_stop_token_ids() {
2389 let (cfg, weights, rope, tokenizer) = zero_layer_f16_fixture();
2390
2391 let gen_cfg = GenerateConfig {
2392 max_new_tokens: 4,
2393 stop_token_ids: vec![0], temperature: 0.0, ..Default::default()
2396 };
2397
2398 let out = generate_f16(&weights, &cfg, &tokenizer, &rope, "h", &gen_cfg)
2399 .expect("generate_f16 must succeed with valid stop_token_ids");
2400
2401 assert_eq!(
2402 out.generated_tokens, 0,
2403 "generate_f16 must stop immediately when the first greedy token (0) \
2404 is in stop_token_ids — got {} generated tokens instead",
2405 out.generated_tokens
2406 );
2407 }
2408
2409 #[test]
2431 fn test_generate_f16_honors_stop_token_ids_decode_loop() {
2432 use crate::weights::f16_weights::f32_to_f16_slice;
2433 use std::collections::HashMap;
2434
2435 let hidden = 4usize;
2436 let vocab = 8usize;
2437
2438 let cfg = Qwen35Config {
2439 hidden_size: hidden,
2440 num_hidden_layers: 0,
2441 vocab_size: vocab,
2442 intermediate_size: 4,
2443 rms_norm_eps: 1e-6,
2444 num_attention_heads: 1,
2445 num_key_value_heads: 1,
2446 head_dim: 4,
2447 rope_theta: 10_000.0,
2448 partial_rotary_factor: 0.5,
2449 rope_parameters: None,
2450 linear_num_key_heads: 1,
2451 linear_num_value_heads: Some(1),
2452 linear_key_head_dim: 4,
2453 linear_value_head_dim: 4,
2454 linear_conv_kernel_dim: 4,
2455 num_experts: None,
2456 num_experts_per_tok: None,
2457 moe_intermediate_size: None,
2458 shared_expert_intermediate_size: None,
2459 output_router_logits: false,
2460 router_aux_loss_coef: None,
2461 tie_word_embeddings: true,
2462 full_attention_interval: 2,
2463 layer_types: vec![],
2464 layer_mask: vec![],
2465 eos_token_id: 5,
2467 max_position_embeddings: 512,
2468 mtp_num_hidden_layers: 0,
2469 mtp_use_dedicated_embeddings: false,
2470 quarot_rotation_seed: None,
2471 vision_config: None,
2472 image_token_id: None,
2473 video_token_id: None,
2474 vision_start_token_id: None,
2475 vision_end_token_id: None,
2476 };
2477
2478 let embed_f32: Vec<f32> = {
2483 let mut v = vec![0.0f32; vocab * hidden];
2484 v[0] = -1.0; v[1] = 1.0; v[hidden] = 1.0; v[hidden + 1] = 1.0; v
2489 };
2490 let mut embed_f16 = vec![0u16; vocab * hidden];
2491 f32_to_f16_slice(&embed_f32, &mut embed_f16);
2492
2493 let weights = F16ModelWeights {
2494 embed_tokens: embed_f16,
2495 final_norm: vec![-2.0f32, 0.0, 0.0, 0.0],
2496 layers: vec![],
2497 };
2498
2499 let rope = RopeTable::new(2, 64, 10_000.0);
2500
2501 let mut vocab_map: HashMap<String, u32> = HashMap::new();
2502 for (i, c) in ["h", "e", "l", "o", "w", "r", "d", "!"].iter().enumerate() {
2503 vocab_map.insert((*c).to_string(), i as u32);
2504 }
2505 let merges = vec![
2506 ("h".to_string(), "e".to_string()),
2507 ("he".to_string(), "l".to_string()),
2508 ];
2509 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab_map, merges).unwrap();
2510
2511 let gen_cfg = GenerateConfig {
2512 max_new_tokens: 10,
2513 stop_token_ids: vec![1], temperature: 0.0, ..Default::default()
2516 };
2517
2518 let out = generate_f16(&weights, &cfg, &tokenizer, &rope, "e", &gen_cfg)
2522 .expect("generate_f16 must succeed");
2523
2524 assert_eq!(
2525 out.generated_tokens, 1,
2526 "generate_f16 must stop at decode-loop step 1 when token 1 is in \
2527 stop_token_ids — got {} tokens; reverting only the decode-loop check \
2528 lets token 1 through and produces ≥ 2 tokens",
2529 out.generated_tokens
2530 );
2531 assert!(
2532 out.stopped,
2533 "generate_f16 must set stopped=true when the decode-loop stop fires"
2534 );
2535 }
2536
2537 #[test]
2554 fn generate_f16_rejects_empty_prompt() {
2555 use crate::error::InferenceError;
2556 use std::collections::HashMap;
2557
2558 let mut vocab: HashMap<String, u32> = HashMap::new();
2559 for (i, c) in ["h", "e", "l", "o"].iter().enumerate() {
2560 vocab.insert((*c).to_string(), i as u32);
2561 }
2562 let merges = vec![
2563 ("h".to_string(), "e".to_string()),
2564 ("he".to_string(), "l".to_string()),
2565 ];
2566 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, merges).unwrap();
2567
2568 let cfg = Qwen35Config::qwen35_2b();
2569 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2570 let weights = F16ModelWeights {
2571 embed_tokens: vec![],
2572 final_norm: vec![],
2573 layers: vec![],
2574 };
2575 let gen_cfg = GenerateConfig::default();
2576
2577 let result = generate_f16(&weights, &cfg, &tokenizer, &rope, "", &gen_cfg);
2578 assert!(
2579 matches!(result, Err(InferenceError::Inference(ref msg)) if msg.contains("empty prompt")),
2580 "generate_f16 must reject an empty prompt with Err(Inference(\"empty \
2581 prompt\")) (#856); got {result:?}"
2582 );
2583 }
2584
2585 #[test]
2596 fn generate_f16_rejects_grammar_config_before_sampling() {
2597 use crate::error::InferenceError;
2598 use crate::grammar::{GrammarEngine, GrammarSpec};
2599 use std::collections::HashMap;
2600 use std::sync::Arc;
2601
2602 let mut vocab: HashMap<String, u32> = HashMap::new();
2603 for (i, c) in ["h", "e", "l", "o"].iter().enumerate() {
2604 vocab.insert((*c).to_string(), i as u32);
2605 }
2606 let merges = vec![
2607 ("h".to_string(), "e".to_string()),
2608 ("he".to_string(), "l".to_string()),
2609 ];
2610 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, merges).unwrap();
2611
2612 let cfg = Qwen35Config::qwen35_2b();
2613 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2614 let weights = F16ModelWeights {
2615 embed_tokens: vec![],
2616 final_norm: vec![],
2617 layers: vec![],
2618 };
2619
2620 let spec = GrammarSpec::Gbnf("root ::= \"t\" | \"f\"\n".to_string());
2621 let grammar_vocab = vec![b"t".to_vec(), b"f".to_vec()];
2622 let engine =
2623 GrammarEngine::new(&spec, grammar_vocab).expect("trivial grammar must compile");
2624
2625 let gen_cfg = GenerateConfig {
2626 grammar: Some(Arc::new(engine)),
2627 ..Default::default()
2628 };
2629
2630 let result = generate_f16(&weights, &cfg, &tokenizer, &rope, "hello", &gen_cfg);
2631 assert!(
2632 matches!(result, Err(InferenceError::InvalidInput(_))),
2633 "generate_f16 must fail closed with InvalidInput when grammar is set (#397/#398); \
2634 got {result:?}"
2635 );
2636 }
2637
2638 #[test]
2645 fn generate_f16_rejects_stop_strings_config_before_sampling() {
2646 use crate::error::InferenceError;
2647 use std::collections::HashMap;
2648
2649 let mut vocab: HashMap<String, u32> = HashMap::new();
2650 for (i, c) in ["h", "e", "l", "o"].iter().enumerate() {
2651 vocab.insert((*c).to_string(), i as u32);
2652 }
2653 let merges = vec![
2654 ("h".to_string(), "e".to_string()),
2655 ("he".to_string(), "l".to_string()),
2656 ];
2657 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, merges).unwrap();
2658
2659 let cfg = Qwen35Config::qwen35_2b();
2660 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2661 let weights = F16ModelWeights {
2662 embed_tokens: vec![],
2663 final_norm: vec![],
2664 layers: vec![],
2665 };
2666
2667 let gen_cfg = GenerateConfig {
2668 stop_strings: vec!["</s>".to_string()],
2669 ..Default::default()
2670 };
2671
2672 let result = generate_f16(&weights, &cfg, &tokenizer, &rope, "hello", &gen_cfg);
2673 assert!(
2674 matches!(result, Err(InferenceError::InvalidInput(_))),
2675 "generate_f16 must fail closed with InvalidInput when stop_strings is set \
2676 (ADR-080 C3, #783); got {result:?}"
2677 );
2678 }
2679
2680 #[test]
2687 fn generate_f16_rejects_reasoning_budget_config_before_sampling() {
2688 use crate::error::InferenceError;
2689 use std::collections::HashMap;
2690
2691 let mut vocab: HashMap<String, u32> = HashMap::new();
2692 for (i, c) in ["h", "e", "l", "o"].iter().enumerate() {
2693 vocab.insert((*c).to_string(), i as u32);
2694 }
2695 let merges = vec![
2696 ("h".to_string(), "e".to_string()),
2697 ("he".to_string(), "l".to_string()),
2698 ];
2699 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, merges).unwrap();
2700
2701 let cfg = Qwen35Config::qwen35_2b();
2702 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2703 let weights = F16ModelWeights {
2704 embed_tokens: vec![],
2705 final_norm: vec![],
2706 layers: vec![],
2707 };
2708
2709 let gen_cfg = GenerateConfig {
2710 reasoning_budget: Some(16),
2711 ..Default::default()
2712 };
2713
2714 let result = generate_f16(&weights, &cfg, &tokenizer, &rope, "hello", &gen_cfg);
2715 assert!(
2716 matches!(result, Err(InferenceError::InvalidInput(_))),
2717 "generate_f16 must fail closed with InvalidInput when reasoning_budget is set \
2718 (ADR-080 C3, #783); got {result:?}"
2719 );
2720 }
2721
2722 #[test]
2734 fn generate_f16_max_new_tokens_zero_returns_empty() {
2735 use std::collections::HashMap;
2736
2737 let mut vocab: HashMap<String, u32> = HashMap::new();
2738 for (i, c) in ["h", "e", "l", "o"].iter().enumerate() {
2739 vocab.insert((*c).to_string(), i as u32);
2740 }
2741 let merges = vec![
2742 ("h".to_string(), "e".to_string()),
2743 ("he".to_string(), "l".to_string()),
2744 ];
2745 let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, merges).unwrap();
2746
2747 let cfg = Qwen35Config::qwen35_2b();
2748 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2749 let weights = F16ModelWeights {
2750 embed_tokens: vec![],
2751 final_norm: vec![],
2752 layers: vec![],
2753 };
2754
2755 let gen_cfg = GenerateConfig {
2756 max_new_tokens: 0,
2757 ..Default::default()
2758 };
2759
2760 let out = generate_f16(&weights, &cfg, &tokenizer, &rope, "hello", &gen_cfg)
2761 .expect("max_new_tokens=0 must succeed, not error");
2762
2763 assert_eq!(
2764 out.generated_tokens, 0,
2765 "max_new_tokens=0 must produce zero generated tokens"
2766 );
2767 assert!(
2768 out.token_ids.is_empty(),
2769 "max_new_tokens=0 must produce an empty token list"
2770 );
2771 assert_eq!(
2772 out.stop_reason,
2773 Some(StopReason::Length),
2774 "max_new_tokens=0 must report stop_reason=Length"
2775 );
2776 }
2777
2778 use crate::model::qwen35_config::{LayerType, RopeParams};
2784 use crate::weights::f16_weights::{
2785 F16CommonLayerWeights, F16FullAttentionLayerWeights, f32_to_f16_slice,
2786 };
2787
2788 fn tiny_vision_splice_model() -> (Qwen35Config, F16ModelWeights) {
2793 let hidden = 8usize;
2794 let vocab = 4usize;
2795
2796 let cfg = Qwen35Config {
2797 hidden_size: hidden,
2798 num_hidden_layers: 1,
2799 vocab_size: vocab,
2800 intermediate_size: 4,
2801 rms_norm_eps: 1e-6,
2802 num_attention_heads: 1,
2803 num_key_value_heads: 1,
2804 head_dim: hidden,
2805 rope_theta: 1.0e7,
2806 partial_rotary_factor: 1.0, rope_parameters: Some(RopeParams {
2808 rope_theta: 1.0e7,
2809 partial_rotary_factor: Some(1.0),
2810 mrope_section: Some(vec![2, 1, 1]),
2811 mrope_interleaved: Some(true),
2812 }),
2813 linear_num_key_heads: 2,
2814 linear_num_value_heads: Some(2),
2815 linear_key_head_dim: 32,
2816 linear_value_head_dim: 32,
2817 linear_conv_kernel_dim: 4,
2818 num_experts: None,
2819 num_experts_per_tok: None,
2820 moe_intermediate_size: None,
2821 shared_expert_intermediate_size: None,
2822 output_router_logits: false,
2823 router_aux_loss_coef: None,
2824 tie_word_embeddings: true,
2825 full_attention_interval: 1,
2826 layer_types: vec![LayerType::FullAttention],
2827 layer_mask: vec![true],
2828 eos_token_id: 999,
2829 max_position_embeddings: 512,
2830 mtp_num_hidden_layers: 0,
2831 mtp_use_dedicated_embeddings: false,
2832 quarot_rotation_seed: None,
2833 vision_config: None,
2834 image_token_id: Some(3),
2835 video_token_id: None,
2836 vision_start_token_id: None,
2837 vision_end_token_id: None,
2838 };
2839
2840 let to_f16 = |src: &[f32]| -> Vec<u16> {
2841 let mut dst = vec![0u16; src.len()];
2842 f32_to_f16_slice(src, &mut dst);
2843 dst
2844 };
2845 let identity = |rows: usize, cols: usize| -> Vec<f32> {
2846 let mut m = vec![0.0f32; rows * cols];
2847 for i in 0..rows.min(cols) {
2848 m[i * cols + i] = 1.0;
2849 }
2850 m
2851 };
2852
2853 let embed_tokens_f32: Vec<f32> = (0..vocab * hidden)
2854 .map(|k| ((k % 11) as f32) * 0.05 - 0.2)
2855 .collect();
2856
2857 let q_dim = hidden;
2858 let mut q_proj_f32 = vec![0.0f32; 2 * q_dim * hidden];
2859 q_proj_f32[..q_dim * hidden].copy_from_slice(&identity(q_dim, hidden));
2860
2861 let full_weights = F16FullAttentionLayerWeights {
2862 q_proj: to_f16(&q_proj_f32),
2863 k_proj: to_f16(&identity(hidden, hidden)),
2864 v_proj: to_f16(&identity(hidden, hidden)),
2865 o_proj: to_f16(&identity(hidden, q_dim)),
2866 q_norm: vec![0.0f32; hidden],
2867 k_norm: vec![0.0f32; hidden],
2868 };
2869
2870 let common = F16CommonLayerWeights {
2871 input_layernorm: vec![0.0f32; hidden],
2872 post_attention_layernorm: vec![0.0f32; hidden],
2873 ffn: F16FeedForwardWeights::Dense {
2874 gate_proj: to_f16(&vec![0.0f32; 4 * hidden]),
2875 up_proj: to_f16(&vec![0.0f32; 4 * hidden]),
2876 down_proj: to_f16(&vec![0.0f32; hidden * 4]),
2877 },
2878 };
2879
2880 let weights = F16ModelWeights {
2881 embed_tokens: to_f16(&embed_tokens_f32),
2882 final_norm: vec![0.0f32; hidden],
2883 layers: vec![(F16AttentionWeights::Full(full_weights), common)],
2884 };
2885
2886 (cfg, weights)
2887 }
2888
2889 #[test]
2895 fn injection_replaces_lookup_and_is_mutation_sensitive() {
2896 let (cfg, mut weights) = tiny_vision_splice_model();
2897 let hidden = cfg.hidden_size;
2898
2899 let run = |weights: &F16ModelWeights, injected: Option<&[f32]>| -> Vec<f32> {
2904 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2905 let mut gdn_states: Vec<GatedDeltaNetState> = vec![];
2906 let mut kv_cache = KvCache::new(cfg.num_full_attention_layers());
2907 let mut scratch = ForwardScratch::new();
2908 forward_step_f16(
2909 weights,
2910 &cfg,
2911 &rope,
2912 0,
2913 0,
2914 &mut gdn_states,
2915 &mut kv_cache,
2916 &mut scratch,
2917 injected,
2918 None,
2919 )
2920 .expect("forward step succeeds");
2921 scratch.hidden[..hidden].to_vec()
2922 };
2923
2924 let baseline = run(&weights, None);
2925
2926 let mut visual_row = vec![0.9f32, -0.4, 0.2, 0.6, -0.1, 0.3, 0.7, -0.8];
2927 let injected_hidden = run(&weights, Some(&visual_row));
2928 assert_ne!(
2929 injected_hidden, baseline,
2930 "an injected embedding must produce a different hidden state than the token-id lookup"
2931 );
2932
2933 visual_row[0] += 1.0;
2935 let mutated_visual_hidden = run(&weights, Some(&visual_row));
2936 assert_ne!(
2937 mutated_visual_hidden, injected_hidden,
2938 "mutating the supplied visual row must change the injected-slot hidden state"
2939 );
2940 visual_row[0] -= 1.0; let embed_start = hidden; let mutated_row_f32 = vec![1.0f32; hidden];
2946 f32_to_f16_slice(
2947 &mutated_row_f32,
2948 &mut weights.embed_tokens[embed_start..embed_start + hidden],
2949 );
2950 let after_table_mutation = run(&weights, Some(&visual_row));
2951 assert_eq!(
2952 after_table_mutation, injected_hidden,
2953 "mutating an unrelated embedding-table row must not affect the injected slot"
2954 );
2955
2956 let mut gdn_states: Vec<GatedDeltaNetState> = vec![];
2958 let mut kv_cache = KvCache::new(cfg.num_full_attention_layers());
2959 let mut scratch = ForwardScratch::new();
2960 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
2961 let short_row = vec![0.0f32; hidden - 1];
2962 assert!(
2963 forward_step_f16(
2964 &weights,
2965 &cfg,
2966 &rope,
2967 0,
2968 0,
2969 &mut gdn_states,
2970 &mut kv_cache,
2971 &mut scratch,
2972 Some(&short_row),
2973 None,
2974 )
2975 .is_err(),
2976 "wrong-length injected_embedding must be rejected"
2977 );
2978 let mut nan_row = vec![0.0f32; hidden];
2979 nan_row[2] = f32::NAN;
2980 assert!(
2981 forward_step_f16(
2982 &weights,
2983 &cfg,
2984 &rope,
2985 0,
2986 0,
2987 &mut gdn_states,
2988 &mut kv_cache,
2989 &mut scratch,
2990 Some(&nan_row),
2991 None,
2992 )
2993 .is_err(),
2994 "non-finite injected_embedding must be rejected"
2995 );
2996 }
2997
2998 #[test]
3003 fn mrope_cos_sin_applied_in_wired_forward_matches_builder() {
3004 use crate::vision::qwen35_mrope::{MRopePositions, build_cos_sin};
3005
3006 let (cfg, weights) = tiny_vision_splice_model();
3007 let hidden = cfg.hidden_size;
3008 let rope_params = cfg.rope_parameters.as_ref().unwrap();
3009 let mrope_section = rope_params.mrope_section.as_ref().unwrap();
3010
3011 let positions = MRopePositions {
3012 positions: vec![(2, 3, 5)],
3013 rope_delta: 0,
3014 };
3015 let tables = build_cos_sin(
3016 &positions,
3017 cfg.head_dim,
3018 rope_params.partial_rotary_factor.unwrap(),
3019 rope_params.rope_theta as f32,
3020 mrope_section,
3021 )
3022 .expect("builds tables");
3023 let (cos_row, sin_row) = (tables.cos[0].as_slice(), tables.sin[0].as_slice());
3024
3025 let rope = RopeTable::new(cfg.rope_dim(), 8, cfg.rope_theta);
3026 let mut gdn_states: Vec<GatedDeltaNetState> = vec![];
3027 let mut kv_cache = KvCache::new(cfg.num_full_attention_layers());
3028 let mut scratch = ForwardScratch::new();
3029
3030 forward_step_f16(
3031 &weights,
3032 &cfg,
3033 &rope,
3034 1,
3035 0,
3036 &mut gdn_states,
3037 &mut kv_cache,
3038 &mut scratch,
3039 None,
3040 Some((cos_row, sin_row)),
3041 )
3042 .expect("forward step succeeds");
3043
3044 let mut k_ref = vec![0.0f32; hidden];
3047 f16_to_f32_slice(&weights.embed_tokens[hidden..2 * hidden], &mut k_ref);
3048 let zero_norm = vec![0.0f32; hidden];
3049 qwen35_rms_norm(&mut k_ref, &zero_norm, hidden, cfg.rms_norm_eps);
3050 let half = cfg.rope_dim() / 2;
3051 for i in 0..half {
3052 let x0 = k_ref[i];
3053 let x1 = k_ref[half + i];
3054 k_ref[i] = x0 * cos_row[i] - x1 * sin_row[i];
3055 k_ref[half + i] = x0 * sin_row[i] + x1 * cos_row[i];
3056 }
3057
3058 let k_cached = &kv_cache.k[0][..hidden];
3059 let max_diff = k_cached
3060 .iter()
3061 .zip(k_ref.iter())
3062 .map(|(a, b)| (a - b).abs())
3063 .fold(0.0f32, f32::max);
3064 assert!(
3065 max_diff < 1e-3,
3066 "wired M-RoPE rotation diverges from build_cos_sin's own row: max_diff={max_diff}"
3067 );
3068 }
3069
3070 #[test]
3077 fn generate_multimodal_text_only_matches_plain_forward_bit_identical() {
3078 use crate::vision::multimodal::Qwen35VisionRequest;
3079
3080 let (cfg, weights) = tiny_vision_splice_model();
3081 let input_ids: Vec<u32> = vec![0, 1, 2, 0];
3082
3083 let request = Qwen35VisionRequest {
3084 input_ids: input_ids.clone(),
3085 image_grids: vec![],
3086 post_merger_rows: vec![],
3087 image_token_id: 3,
3088 spatial_merge_size: 2,
3089 decoder_hidden_size: cfg.hidden_size,
3090 };
3091
3092 let gen_cfg = GenerateConfig {
3093 max_new_tokens: 2,
3094 temperature: 0.0,
3095 seed: Some(1),
3096 stop_token_ids: vec![],
3097 ..Default::default()
3098 };
3099
3100 let multimodal_out = generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg)
3101 .expect("text-only multimodal generate succeeds");
3102
3103 let rope = RopeTable::new(cfg.rope_dim(), 512, cfg.rope_theta);
3106 let mut gdn_states: Vec<GatedDeltaNetState> = vec![];
3107 let mut kv_cache = KvCache::new(cfg.num_full_attention_layers());
3108 let mut scratch = ForwardScratch::new();
3109 for (pos, &token_id) in input_ids.iter().enumerate() {
3110 forward_step_f16(
3111 &weights,
3112 &cfg,
3113 &rope,
3114 token_id,
3115 pos,
3116 &mut gdn_states,
3117 &mut kv_cache,
3118 &mut scratch,
3119 None,
3120 None,
3121 )
3122 .expect("reference forward step succeeds");
3123 if pos < input_ids.len() - 1 {
3124 kv_cache.seq_len += 1;
3125 }
3126 }
3127 kv_cache.seq_len = input_ids.len();
3128
3129 let mut rng_state = 1u64;
3130 let mut all_ids = input_ids.clone();
3131 let mut ref_ids = Vec::new();
3132
3133 let next_id = sample_token(
3134 &scratch.logits[..cfg.vocab_size],
3135 &gen_cfg,
3136 &all_ids,
3137 &mut rng_state,
3138 );
3139 ref_ids.push(next_id);
3140 all_ids.push(next_id);
3141
3142 for _ in 1..gen_cfg.max_new_tokens {
3143 let pos = kv_cache.seq_len;
3144 let last_token = *all_ids.last().unwrap();
3145 forward_step_f16(
3146 &weights,
3147 &cfg,
3148 &rope,
3149 last_token,
3150 pos,
3151 &mut gdn_states,
3152 &mut kv_cache,
3153 &mut scratch,
3154 None,
3155 None,
3156 )
3157 .expect("reference decode step succeeds");
3158 kv_cache.seq_len += 1;
3159 let next_id = sample_token(
3160 &scratch.logits[..cfg.vocab_size],
3161 &gen_cfg,
3162 &all_ids,
3163 &mut rng_state,
3164 );
3165 ref_ids.push(next_id);
3166 all_ids.push(next_id);
3167 }
3168
3169 assert_eq!(
3170 multimodal_out.token_ids, ref_ids,
3171 "text-only generate_multimodal_f16 token ids must match the plain forward_step_f16 \
3172 reference bit-for-bit"
3173 );
3174 }
3175
3176 #[test]
3180 fn generate_multimodal_f16_rejects_invalid_request() {
3181 use crate::vision::multimodal::Qwen35VisionRequest;
3182
3183 let (cfg, weights) = tiny_vision_splice_model();
3184 let request = Qwen35VisionRequest {
3185 input_ids: vec![0, 3, 3, 1], image_grids: vec![crate::vision::qwen35_vit::GridThw { t: 1, h: 4, w: 4 }], post_merger_rows: vec![0.0f32; 2 * cfg.hidden_size],
3188 image_token_id: 3,
3189 spatial_merge_size: 2,
3190 decoder_hidden_size: cfg.hidden_size,
3191 };
3192 let gen_cfg = GenerateConfig {
3193 max_new_tokens: 1,
3194 ..Default::default()
3195 };
3196 assert!(
3197 generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg).is_err(),
3198 "a request whose image-pad count does not match its grid must be rejected"
3199 );
3200 }
3201
3202 #[test]
3206 fn generate_multimodal_f16_rejects_context_overflow() {
3207 use crate::vision::multimodal::Qwen35VisionRequest;
3208
3209 let (mut cfg, weights) = tiny_vision_splice_model();
3210 cfg.max_position_embeddings = 3;
3211 let request = Qwen35VisionRequest {
3212 input_ids: vec![0, 1, 2],
3213 image_grids: vec![],
3214 post_merger_rows: vec![],
3215 image_token_id: 3,
3216 spatial_merge_size: 2,
3217 decoder_hidden_size: cfg.hidden_size,
3218 };
3219 let gen_cfg = GenerateConfig {
3220 max_new_tokens: 5,
3221 ..Default::default()
3222 };
3223 assert!(
3224 generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg).is_err(),
3225 "prompt_len + max_new_tokens exceeding max_position_embeddings must be rejected"
3226 );
3227 }
3228
3229 #[test]
3237 fn generate_multimodal_f16_rejects_out_of_vocab_input_id() {
3238 use crate::vision::multimodal::Qwen35VisionRequest;
3239
3240 let (cfg, weights) = tiny_vision_splice_model();
3241 let request = Qwen35VisionRequest {
3242 input_ids: vec![0, 1, cfg.vocab_size as u32], image_grids: vec![],
3244 post_merger_rows: vec![],
3245 image_token_id: 3,
3246 spatial_merge_size: 2,
3247 decoder_hidden_size: cfg.hidden_size,
3248 };
3249 let gen_cfg = GenerateConfig {
3250 max_new_tokens: 1,
3251 ..Default::default()
3252 };
3253 let err = generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg)
3254 .expect_err("an out-of-vocabulary input_id must be rejected, not panic");
3255 assert!(
3256 matches!(err, crate::error::InferenceError::InvalidInput(_)),
3257 "expected InvalidInput, got {err:?}"
3258 );
3259 }
3260
3261 #[test]
3272 fn test_generate_f16_rejects_out_of_vocab_prompt_id() {
3273 use std::collections::HashMap;
3274
3275 let (cfg, weights, rope, _tokenizer) = zero_layer_f16_fixture();
3276
3277 let mut vocab_map: HashMap<String, u32> = HashMap::new();
3278 for (i, c) in ["h", "e", "l", "o", "w", "r", "d", "!"].iter().enumerate() {
3279 vocab_map.insert((*c).to_string(), i as u32);
3280 }
3281 vocab_map.insert("z".to_string(), cfg.vocab_size as u32);
3284 let mismatched_tokenizer = BpeTokenizer::from_vocab_and_merges(vocab_map, vec![])
3285 .expect("tokenizer with an OOV vocab entry still constructs");
3286
3287 let gen_cfg = GenerateConfig {
3288 max_new_tokens: 1,
3289 ..Default::default()
3290 };
3291 let err = generate_f16(&weights, &cfg, &mismatched_tokenizer, &rope, "z", &gen_cfg)
3292 .expect_err("an out-of-vocabulary prompt token id must be rejected, not panic");
3293 assert!(
3294 matches!(err, crate::error::InferenceError::InvalidInput(_)),
3295 "expected InvalidInput, got {err:?}"
3296 );
3297 }
3298
3299 fn tiny_vision_splice_model_with_vision_cfg() -> (Qwen35Config, F16ModelWeights) {
3304 use crate::model::qwen35_config::VisionModelConfig;
3305
3306 let (mut cfg, weights) = tiny_vision_splice_model();
3307 cfg.vision_config = Some(VisionModelConfig {
3308 depth: 1,
3309 hidden_size: 8,
3310 num_heads: 1,
3311 patch_size: 1,
3312 spatial_merge_size: 2,
3313 out_hidden_size: cfg.hidden_size,
3314 temporal_patch_size: 1,
3315 num_position_embeddings: 1,
3316 in_channels: 3,
3317 deepstack_visual_indexes: vec![],
3318 intermediate_size: None,
3319 });
3320 (cfg, weights)
3321 }
3322
3323 #[test]
3333 fn generate_multimodal_f16_rejects_mismatched_image_token_id() {
3334 use crate::vision::multimodal::Qwen35VisionRequest;
3335 use crate::vision::qwen35_vit::GridThw;
3336
3337 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3338 assert_eq!(cfg.image_token_id, Some(3));
3339
3340 let request = Qwen35VisionRequest {
3343 input_ids: vec![0, 2, 2, 2, 2, 1],
3344 image_grids: vec![GridThw { t: 1, h: 4, w: 4 }],
3345 post_merger_rows: vec![0.1f32; 4 * cfg.hidden_size],
3346 image_token_id: 2,
3347 spatial_merge_size: 2,
3348 decoder_hidden_size: cfg.hidden_size,
3349 };
3350 let gen_cfg = GenerateConfig {
3351 max_new_tokens: 1,
3352 ..Default::default()
3353 };
3354 let err = generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg)
3355 .expect_err("mismatched image_token_id must be rejected, not silently run");
3356 let msg = format!("{err}");
3357 assert!(
3358 msg.contains("image_token_id"),
3359 "error must name image_token_id; got: {msg}"
3360 );
3361 }
3362
3363 #[test]
3369 fn generate_multimodal_f16_rejects_mismatched_spatial_merge_size() {
3370 use crate::vision::multimodal::Qwen35VisionRequest;
3371 use crate::vision::qwen35_vit::GridThw;
3372
3373 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3374 assert_eq!(cfg.vision_config.as_ref().unwrap().spatial_merge_size, 2);
3375
3376 let mut input_ids = vec![0u32];
3379 input_ids.extend(std::iter::repeat_n(3u32, 16));
3380 input_ids.push(1);
3381 let request = Qwen35VisionRequest {
3382 input_ids,
3383 image_grids: vec![GridThw { t: 1, h: 4, w: 4 }],
3384 post_merger_rows: vec![0.1f32; 16 * cfg.hidden_size],
3385 image_token_id: 3,
3386 spatial_merge_size: 1,
3387 decoder_hidden_size: cfg.hidden_size,
3388 };
3389 let gen_cfg = GenerateConfig {
3390 max_new_tokens: 1,
3391 ..Default::default()
3392 };
3393 let err = generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg)
3394 .expect_err("mismatched spatial_merge_size must be rejected, not silently run");
3395 let msg = format!("{err}");
3396 assert!(
3397 msg.contains("spatial_merge_size"),
3398 "error must name spatial_merge_size; got: {msg}"
3399 );
3400 }
3401
3402 #[test]
3413 fn generate_multimodal_f16_rejects_mismatched_decoder_hidden_size() {
3414 use crate::vision::multimodal::Qwen35VisionRequest;
3415 use crate::vision::qwen35_vit::GridThw;
3416
3417 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3418 assert_eq!(cfg.hidden_size, 8);
3419
3420 let request = Qwen35VisionRequest {
3424 input_ids: vec![0, 3, 3, 3, 3, 1],
3425 image_grids: vec![GridThw { t: 1, h: 4, w: 4 }],
3426 post_merger_rows: vec![0.1f32; 4 * 4],
3427 image_token_id: 3,
3428 spatial_merge_size: 2,
3429 decoder_hidden_size: 4,
3430 };
3431 let gen_cfg = GenerateConfig {
3432 max_new_tokens: 1,
3433 ..Default::default()
3434 };
3435 let err = generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg)
3436 .expect_err("mismatched decoder_hidden_size must be rejected, not silently run");
3437 let msg = format!("{err}");
3438 assert!(
3439 msg.contains("decoder_hidden_size"),
3440 "error must name decoder_hidden_size (not just the unrelated downstream \
3441 injected-row-length message); got: {msg}"
3442 );
3443 }
3444
3445 #[test]
3458 fn generate_multimodal_f16_rejects_mismatched_mrope_row_width() {
3459 use crate::model::qwen35_config::RopeParams;
3460 use crate::vision::multimodal::Qwen35VisionRequest;
3461
3462 let (mut cfg, weights) = tiny_vision_splice_model();
3463 cfg.head_dim = 256;
3464 cfg.partial_rotary_factor = 0.5; cfg.rope_parameters = Some(RopeParams {
3466 rope_theta: 1.0e7,
3467 partial_rotary_factor: Some(0.25), mrope_section: Some(vec![11, 11, 10]),
3469 mrope_interleaved: Some(true),
3470 });
3471
3472 let request = Qwen35VisionRequest {
3476 input_ids: vec![0, 1, 2],
3477 image_grids: vec![],
3478 post_merger_rows: vec![],
3479 image_token_id: 3,
3480 spatial_merge_size: 2,
3481 decoder_hidden_size: cfg.hidden_size,
3482 };
3483 let gen_cfg = GenerateConfig {
3484 max_new_tokens: 1,
3485 ..Default::default()
3486 };
3487 let err = generate_multimodal_f16(&weights, &cfg, &request, &gen_cfg).expect_err(
3488 "a config whose rope_parameters and rope_dim() disagree on rotary width must be \
3489 rejected, not panic at attention-lane indexing",
3490 );
3491 assert!(
3492 matches!(err, crate::error::InferenceError::InvalidInput(_)),
3493 "expected InvalidInput, got {err:?}"
3494 );
3495 }
3496
3497 fn cosine(a: &[f32], b: &[f32]) -> f32 {
3502 assert_eq!(a.len(), b.len());
3503 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
3504 let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
3505 let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
3506 if na == 0.0 || nb == 0.0 {
3507 return 0.0;
3508 }
3509 dot / (na * nb)
3510 }
3511
3512 fn one_image_request(visual_rows: Vec<f32>) -> Qwen35VisionRequest {
3517 let mut input_ids = vec![0u32];
3518 input_ids.extend(std::iter::repeat_n(3u32, 4));
3519 input_ids.push(1);
3520 Qwen35VisionRequest {
3521 input_ids,
3522 image_grids: vec![crate::vision::qwen35_vit::GridThw { t: 1, h: 4, w: 4 }],
3523 post_merger_rows: visual_rows,
3524 image_token_id: 3,
3525 spatial_merge_size: 2,
3526 decoder_hidden_size: 8,
3527 }
3528 }
3529
3530 #[test]
3531 fn embed_image_f16_is_deterministic() {
3532 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3533 let request = one_image_request(vec![0.3f32; 4 * cfg.hidden_size]);
3534
3535 let v1 = embed_image_f16(&weights, &cfg, &request, PoolingStrategy::MeanVisualTokens)
3536 .expect("embed_image_f16 succeeds");
3537 let v2 = embed_image_f16(&weights, &cfg, &request, PoolingStrategy::MeanVisualTokens)
3538 .expect("embed_image_f16 succeeds");
3539 assert_eq!(v1, v2, "same input must produce an identical vector");
3540
3541 let v3 = embed_image_f16(&weights, &cfg, &request, PoolingStrategy::LastToken)
3542 .expect("embed_image_f16 succeeds");
3543 let v4 = embed_image_f16(&weights, &cfg, &request, PoolingStrategy::LastToken)
3544 .expect("embed_image_f16 succeeds");
3545 assert_eq!(
3546 v3, v4,
3547 "same input must produce an identical vector (LastToken)"
3548 );
3549 }
3550
3551 #[test]
3552 fn embed_image_f16_is_finite_and_unit_norm() {
3553 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3554 for pooling in [
3555 PoolingStrategy::MeanVisualTokens,
3556 PoolingStrategy::LastToken,
3557 ] {
3558 let request = one_image_request(vec![0.4f32; 4 * cfg.hidden_size]);
3559 let v = embed_image_f16(&weights, &cfg, &request, pooling)
3560 .expect("embed_image_f16 succeeds");
3561 assert_eq!(v.len(), cfg.hidden_size);
3562 assert!(
3563 v.iter().all(|x| x.is_finite()),
3564 "{pooling:?}: non-finite output"
3565 );
3566 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3567 assert!(
3568 (norm - 1.0).abs() < 1e-4,
3569 "{pooling:?}: expected unit norm, got {norm}"
3570 );
3571 }
3572 }
3573
3574 #[test]
3575 fn embed_image_f16_discriminates_different_images_but_matches_itself() {
3576 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3577
3578 let request_a = one_image_request(
3579 (0..4 * cfg.hidden_size)
3580 .map(|i| (i as f32) * 0.05 - 0.8)
3581 .collect(),
3582 );
3583 let request_b = one_image_request(
3584 (0..4 * cfg.hidden_size)
3585 .map(|i| -(i as f32) * 0.03 + 0.5)
3586 .collect(),
3587 );
3588
3589 let emb_a = embed_image_f16(
3590 &weights,
3591 &cfg,
3592 &request_a,
3593 PoolingStrategy::MeanVisualTokens,
3594 )
3595 .expect("embed_image_f16 succeeds");
3596 let emb_a_again = embed_image_f16(
3597 &weights,
3598 &cfg,
3599 &request_a,
3600 PoolingStrategy::MeanVisualTokens,
3601 )
3602 .expect("embed_image_f16 succeeds");
3603 let emb_b = embed_image_f16(
3604 &weights,
3605 &cfg,
3606 &request_b,
3607 PoolingStrategy::MeanVisualTokens,
3608 )
3609 .expect("embed_image_f16 succeeds");
3610
3611 let self_cos = cosine(&emb_a, &emb_a_again);
3612 assert!(
3613 (self_cos - 1.0).abs() < 1e-5,
3614 "an image embedded against itself must have cosine ~1.0, got {self_cos}"
3615 );
3616
3617 let cross_cos = cosine(&emb_a, &emb_b);
3618 assert!(
3619 cross_cos < 0.999,
3620 "two different images must not collapse to near-identical embeddings, got cosine {cross_cos}"
3621 );
3622 }
3623
3624 #[test]
3632 fn pool_hidden_states_wrong_positions_change_the_output() {
3633 let hidden_size = 4;
3634 let seq_len = 6;
3635 let hidden_states: Vec<f32> = (0..seq_len)
3638 .flat_map(|i| std::iter::repeat_n(i as f32, hidden_size))
3639 .collect();
3640
3641 let correct_positions = [1usize, 2, 3, 4];
3642 let off_by_one_positions = [2usize, 3, 4, 5];
3643
3644 let correct = pool_hidden_states(
3645 &hidden_states,
3646 hidden_size,
3647 seq_len,
3648 &correct_positions,
3649 PoolingStrategy::MeanVisualTokens,
3650 );
3651 let wrong = pool_hidden_states(
3652 &hidden_states,
3653 hidden_size,
3654 seq_len,
3655 &off_by_one_positions,
3656 PoolingStrategy::MeanVisualTokens,
3657 );
3658
3659 assert_ne!(
3660 correct, wrong,
3661 "pooling over an off-by-one-shifted position window must change the output"
3662 );
3663 }
3664
3665 #[test]
3666 fn embed_image_f16_wrong_pad_run_placement_changes_embedding() {
3667 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3674 let visual_rows = vec![0.25f32; 4 * cfg.hidden_size];
3675
3676 let correct = one_image_request(visual_rows.clone());
3677 let mut shifted_ids = vec![0u32, 2]; shifted_ids.extend(std::iter::repeat_n(3u32, 4));
3679 shifted_ids.push(1);
3680 let shifted = Qwen35VisionRequest {
3681 input_ids: shifted_ids,
3682 ..one_image_request(visual_rows)
3683 };
3684
3685 let emb_correct =
3686 embed_image_f16(&weights, &cfg, &correct, PoolingStrategy::MeanVisualTokens)
3687 .expect("embed_image_f16 succeeds");
3688 let emb_shifted =
3689 embed_image_f16(&weights, &cfg, &shifted, PoolingStrategy::MeanVisualTokens)
3690 .expect("embed_image_f16 succeeds");
3691
3692 assert_ne!(
3693 emb_correct, emb_shifted,
3694 "shifting the image-pad run's position in input_ids must change the pooled embedding"
3695 );
3696 }
3697
3698 #[test]
3699 fn embed_text_vlm_f16_is_deterministic_and_unit_norm() {
3700 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3701 let mut vocab_map = std::collections::HashMap::new();
3702 for (i, c) in ["a", "b", "c"].iter().enumerate() {
3703 vocab_map.insert((*c).to_string(), i as u32);
3704 }
3705 let tokenizer =
3706 BpeTokenizer::from_vocab_and_merges(vocab_map, vec![]).expect("tokenizer constructs");
3707
3708 for pooling in [
3709 PoolingStrategy::MeanVisualTokens,
3710 PoolingStrategy::LastToken,
3711 ] {
3712 let v1 = embed_text_vlm_f16(&weights, &cfg, &tokenizer, "abc", pooling)
3713 .expect("embed_text_vlm_f16 succeeds");
3714 let v2 = embed_text_vlm_f16(&weights, &cfg, &tokenizer, "abc", pooling)
3715 .expect("embed_text_vlm_f16 succeeds");
3716 assert_eq!(
3717 v1, v2,
3718 "{pooling:?}: same prompt must produce an identical vector"
3719 );
3720 assert_eq!(v1.len(), cfg.hidden_size);
3721 let norm: f32 = v1.iter().map(|x| x * x).sum::<f32>().sqrt();
3722 assert!(
3723 (norm - 1.0).abs() < 1e-4,
3724 "{pooling:?}: expected unit norm, got {norm}"
3725 );
3726 }
3727 }
3728
3729 #[test]
3730 fn embed_text_vlm_f16_and_embed_image_f16_share_the_same_space() {
3731 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3736 let mut vocab_map = std::collections::HashMap::new();
3737 vocab_map.insert("a".to_string(), 0u32);
3738 let tokenizer =
3739 BpeTokenizer::from_vocab_and_merges(vocab_map, vec![]).expect("tokenizer constructs");
3740
3741 let text_emb =
3742 embed_text_vlm_f16(&weights, &cfg, &tokenizer, "a", PoolingStrategy::LastToken)
3743 .expect("embed_text_vlm_f16 succeeds");
3744 let image_emb = embed_image_f16(
3745 &weights,
3746 &cfg,
3747 &one_image_request(vec![0.1f32; 4 * cfg.hidden_size]),
3748 PoolingStrategy::LastToken,
3749 )
3750 .expect("embed_image_f16 succeeds");
3751
3752 assert_eq!(text_emb.len(), image_emb.len());
3753 let cos = cosine(&text_emb, &image_emb);
3754 assert!(cos.is_finite());
3755 }
3756
3757 #[test]
3758 fn embed_text_vlm_f16_rejects_prompt_colliding_with_image_token_id() {
3759 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3760 assert_eq!(cfg.image_token_id, Some(3));
3761 let mut vocab_map = std::collections::HashMap::new();
3764 vocab_map.insert("z".to_string(), 3u32);
3765 let tokenizer =
3766 BpeTokenizer::from_vocab_and_merges(vocab_map, vec![]).expect("tokenizer constructs");
3767
3768 let err = embed_text_vlm_f16(&weights, &cfg, &tokenizer, "z", PoolingStrategy::LastToken)
3769 .expect_err("a prompt colliding with image_token_id must be rejected");
3770 assert!(matches!(err, crate::error::InferenceError::InvalidInput(_)));
3771 }
3772
3773 #[test]
3774 fn embed_image_f16_rejects_invalid_request() {
3775 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3776 let mut request = one_image_request(vec![0.1f32; 4 * cfg.hidden_size]);
3777 request.post_merger_rows.pop(); assert!(
3779 embed_image_f16(&weights, &cfg, &request, PoolingStrategy::MeanVisualTokens).is_err()
3780 );
3781 }
3782
3783 #[test]
3792 fn prefill_rejects_over_context_before_mrope_table_construction() {
3793 let (mut cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3794 let base = one_image_request(vec![0.1f32; 4 * cfg.hidden_size]);
3795 let request = Qwen35VisionRequest {
3796 input_ids: vec![0u32, 3, 1, 3, 3, 3, 1],
3799 image_grids: vec![
3800 crate::vision::qwen35_vit::GridThw { t: 1, h: 2, w: 4 },
3801 crate::vision::qwen35_vit::GridThw { t: 1, h: 2, w: 4 },
3802 ],
3803 ..base
3804 };
3805 request
3806 .validate()
3807 .expect("request must pass validation so only the builder would catch it");
3808 cfg.max_position_embeddings = request.input_ids.len() - 1;
3809
3810 let err = prefill_hidden_states_f16(&weights, &cfg, &request)
3811 .expect_err("over-context request must be rejected");
3812 let msg = err.to_string();
3813 assert!(
3814 msg.contains("context window"),
3815 "must fail on the context-window check, before M-RoPE table \
3816 construction; got: {msg}"
3817 );
3818 }
3819
3820 #[test]
3840 fn embed_image_f16_matches_independently_computed_golden_pool() {
3841 let (cfg, weights) = tiny_vision_splice_model_with_vision_cfg();
3842 let request = one_image_request(vec![0.37f32; 4 * cfg.hidden_size]);
3843
3844 let hidden_states = prefill_hidden_states_f16(&weights, &cfg, &request)
3845 .expect("prefill_hidden_states_f16 succeeds");
3846 let known_correct_pad_positions = [1usize, 2, 3, 4]; let golden = l2_normalize_owned(mean_pool_rows(
3848 &hidden_states,
3849 cfg.hidden_size,
3850 &known_correct_pad_positions,
3851 ));
3852
3853 let got = embed_image_f16(&weights, &cfg, &request, PoolingStrategy::MeanVisualTokens)
3854 .expect("embed_image_f16 succeeds");
3855
3856 assert_eq!(
3857 got, golden,
3858 "embed_image_f16 must pool over exactly the known-correct image-pad positions"
3859 );
3860 }
3861}