1use crate::qtensor::QTensor;
10use crate::qwen_image_ops::Linear;
11use crate::qwen_image_vision::{prepare_image, PreparedImage, ProcessorConfig, VisionTower};
12use crate::tokenizer::Tokenizer;
13use cortiq_core::{CmfModel, TensorDtype};
14use image::RgbImage;
15use serde_json::Value;
16use std::path::Path;
17use std::sync::Arc;
18
19const PROMPT_TEMPLATE_HEAD: &str =
20 "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n";
21const PROMPT_TEMPLATE_TAIL: &str = "<|im_end|>\n<|im_start|>assistant\n";
22const DROP_PREFIX: usize = 64;
23const EPS: f64 = 1e-6;
24
25#[derive(Clone, Debug)]
28pub struct Conditioning {
29 pub hidden: Vec<f32>,
30 pub seq_len: usize,
31 pub hidden_size: usize,
32}
33
34struct MappedEmbedding {
35 model: Arc<CmfModel>,
36 idx: usize,
37 dtype: TensorDtype,
38 rows: usize,
39 cols: usize,
40}
41
42enum Embedding {
43 Mapped(MappedEmbedding),
44 Quant(QTensor),
45}
46
47impl Embedding {
48 fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
49 let idx = model
50 .tensor_index(name)
51 .ok_or_else(|| format!("missing embedding tensor '{name}'"))?;
52 let entry = &model.tensors[idx];
53 if entry.shape.len() != 2 {
54 return Err(format!(
55 "embedding tensor '{name}' must be rank 2, got shape {:?}",
56 entry.shape
57 ));
58 }
59 let rows = entry.shape[0];
60 let cols = entry.shape[1];
61 match entry.dtype {
62 TensorDtype::F32 | TensorDtype::F16 | TensorDtype::Bf16 => {
63 Ok(Self::Mapped(MappedEmbedding {
64 model: model.clone(),
65 idx,
66 dtype: entry.dtype,
67 rows,
68 cols,
69 }))
70 }
71 _ => Ok(Self::Quant(QTensor::from_model(model, name)?)),
72 }
73 }
74
75 fn rows(&self) -> usize {
76 match self {
77 Self::Mapped(e) => e.rows,
78 Self::Quant(q) => q.rows(),
79 }
80 }
81
82 fn cols(&self) -> usize {
83 match self {
84 Self::Mapped(e) => e.cols,
85 Self::Quant(q) => q.cols(),
86 }
87 }
88
89 fn row(&self, id: usize, dst: &mut [f32]) -> Result<(), String> {
90 if id >= self.rows() {
91 return Err(format!(
92 "token id {id} exceeds embedding rows {}",
93 self.rows()
94 ));
95 }
96 if dst.len() != self.cols() {
97 return Err(format!(
98 "embedding destination width {} != {}",
99 dst.len(),
100 self.cols()
101 ));
102 }
103 match self {
104 Self::Quant(q) => {
105 q.row_f32(id, dst);
106 Ok(())
107 }
108 Self::Mapped(e) => {
109 let entry = &e.model.tensors[e.idx];
110 let bytes = e.model.entry_bytes(entry);
111 let elem_bytes = match e.dtype {
112 TensorDtype::F32 => 4,
113 TensorDtype::F16 | TensorDtype::Bf16 => 2,
114 _ => unreachable!(),
115 };
116 let expected = e
117 .rows
118 .checked_mul(e.cols)
119 .and_then(|n| n.checked_mul(elem_bytes))
120 .ok_or_else(|| {
121 format!("embedding tensor '{}' byte size overflow", entry.name)
122 })?;
123 if bytes.len() != expected {
124 return Err(format!(
125 "embedding tensor '{}' has {} bytes, expected {}",
126 entry.name,
127 bytes.len(),
128 expected
129 ));
130 }
131 let start = id * e.cols * elem_bytes;
132 let src = &bytes[start..start + e.cols * elem_bytes];
133 for i in 0..e.cols {
134 let off = i * elem_bytes;
135 dst[i] = match e.dtype {
136 TensorDtype::F32 => {
137 f32::from_le_bytes(src[off..off + 4].try_into().unwrap())
138 }
139 TensorDtype::F16 => cortiq_core::quant::f16_to_f32(u16::from_le_bytes([
140 src[off],
141 src[off + 1],
142 ])),
143 TensorDtype::Bf16 => cortiq_core::quant::bf16_to_f32(u16::from_le_bytes([
144 src[off],
145 src[off + 1],
146 ])),
147 _ => unreachable!(),
148 };
149 }
150 Ok(())
151 }
152 }
153 }
154}
155
156struct TextLayer {
157 input_norm: Vec<f32>,
158 q: Linear,
159 q_bias: Vec<f32>,
160 k: Linear,
161 k_bias: Vec<f32>,
162 v: Linear,
163 v_bias: Vec<f32>,
164 o: Linear,
165 post_norm: Vec<f32>,
166 gate: Linear,
167 up: Linear,
168 down: Linear,
169 sliding: bool,
170}
171
172pub struct QwenImageEncoder {
175 model: Arc<CmfModel>,
176 tokenizer: Tokenizer,
177 processor: ProcessorConfig,
178 embed: Embedding,
179 layers: Vec<TextLayer>,
180 final_norm: Vec<f32>,
181 vision: VisionTower,
182 hidden: usize,
183 heads: usize,
184 kv_heads: usize,
185 head_dim: usize,
186 intermediate: usize,
187 eps: f64,
188 rope_theta: f64,
189 mrope_section: [usize; 3],
190 sliding_window: usize,
191 image_token_id: u32,
192 vision_start_token_id: u32,
193 vision_end_token_id: u32,
194}
195
196pub type Encoder = QwenImageEncoder;
199
200fn json_blob<'a>(model: &'a CmfModel, name: &str) -> Result<&'a [u8], String> {
201 let entry = model
202 .tensor(name)
203 .ok_or_else(|| format!("missing required U8 asset '{name}'"))?;
204 if entry.dtype != TensorDtype::U8 {
205 return Err(format!(
206 "asset '{name}' must use U8 storage, got {}",
207 entry.dtype.name()
208 ));
209 }
210 if entry.shape.len() != 1 || entry.shape[0] != entry.n_elems() {
211 return Err(format!("asset '{name}' must be a one-dimensional U8 blob"));
212 }
213 Ok(model.entry_bytes(entry))
214}
215
216fn json_blob_any<'a>(model: &'a CmfModel, names: &[&str]) -> Result<&'a [u8], String> {
221 for name in names {
222 if model.tensor(name).is_some() {
223 return json_blob(model, name);
224 }
225 }
226 Err(format!("missing required U8 asset '{}'", names[0]))
227}
228
229fn value_usize(v: &Value, key: &str) -> Result<usize, String> {
230 v.get(key)
231 .and_then(Value::as_u64)
232 .map(|x| x as usize)
233 .ok_or_else(|| format!("Qwen config missing integer '{key}'"))
234}
235
236fn value_usize_default(v: &Value, key: &str, default: usize) -> usize {
237 v.get(key)
238 .and_then(Value::as_u64)
239 .map(|x| x as usize)
240 .unwrap_or(default)
241}
242
243fn value_bool(v: &Value, key: &str, default: bool) -> bool {
244 v.get(key).and_then(Value::as_bool).unwrap_or(default)
245}
246
247fn text_config<'a>(root: &'a Value) -> &'a Value {
248 root.get("text_config").unwrap_or(root)
249}
250
251fn vision_config<'a>(root: &'a Value) -> Result<&'a Value, String> {
252 root.get("vision_config")
253 .ok_or_else(|| "Qwen2.5-VL config has no vision_config".into())
254}
255
256fn vector(model: &CmfModel, name: &str, expected: usize) -> Result<Vec<f32>, String> {
257 let v = crate::dit::cmf_f32(model, name)?;
258 if v.len() != expected {
259 return Err(format!(
260 "tensor '{name}' has {} values, expected {expected}",
261 v.len()
262 ));
263 }
264 Ok(v)
265}
266
267fn linear(model: &Arc<CmfModel>, name: &str, rows: usize, cols: usize) -> Result<Linear, String> {
268 let p = Linear::load(model, name)?;
269 if p.rows() != rows || p.cols() != cols {
270 return Err(format!(
271 "tensor '{name}' shape [{},{}] != expected [{rows},{cols}]",
272 p.rows(),
273 p.cols()
274 ));
275 }
276 Ok(p)
277}
278
279fn special_id(
280 root: &Value,
281 text: &Value,
282 tokenizer: &Tokenizer,
283 config_key: &str,
284 token: &str,
285) -> Result<u32, String> {
286 root.get(config_key)
287 .and_then(Value::as_u64)
288 .or_else(|| text.get(config_key).and_then(Value::as_u64))
289 .map(|v| v as u32)
290 .or_else(|| tokenizer.token_to_id(token))
291 .ok_or_else(|| format!("missing Qwen special token '{token}' / config key '{config_key}'"))
292}
293
294fn parse_mrope(text: &Value, head_dim: usize) -> Result<[usize; 3], String> {
295 let values = text
296 .get("rope_scaling")
297 .and_then(|v| v.get("mrope_section"))
298 .and_then(Value::as_array);
299 let mut out = if let Some(values) = values {
300 if values.len() != 3 {
301 return Err("rope_scaling.mrope_section must contain three integers".into());
302 }
303 [
304 values[0]
305 .as_u64()
306 .ok_or("mrope_section[0] is not an integer")? as usize,
307 values[1]
308 .as_u64()
309 .ok_or("mrope_section[1] is not an integer")? as usize,
310 values[2]
311 .as_u64()
312 .ok_or("mrope_section[2] is not an integer")? as usize,
313 ]
314 } else if head_dim == 128 {
315 [16, 24, 24]
319 } else {
320 return Err("Qwen2.5-VL config missing rope_scaling.mrope_section".into());
321 };
322 if out.iter().any(|&x| x == 0) || out.iter().sum::<usize>() != head_dim / 2 {
323 return Err(format!(
324 "mrope_section {:?} must be positive and sum to head_dim/2={}",
325 out,
326 head_dim / 2
327 ));
328 }
329 Ok(out)
330}
331
332impl QwenImageEncoder {
333 pub fn open(path: &Path) -> Result<Self, String> {
337 let model = Arc::new(CmfModel::open_sharded(path).map_err(|e| e.to_string())?);
338 let config_bytes = json_blob_any(
339 &model,
340 &["image.text_encoder.config_json", "image.config_json"],
341 )?;
342 let root: Value =
343 serde_json::from_slice(config_bytes).map_err(|e| format!("image.config_json: {e}"))?;
344 let text = text_config(&root);
345 let vision_cfg = vision_config(&root)?;
346 let processor =
347 ProcessorConfig::from_json(json_blob(&model, "image.processor_config_json")?)?;
348 let tokenizer = Tokenizer::from_bytes(json_blob(&model, "image.tokenizer_json")?)
349 .map_err(|e| format!("image.tokenizer_json: {e}"))?;
350 if let Some(entry) = model.tensor("image.tokenizer_config_json") {
354 if entry.dtype != TensorDtype::U8 {
355 return Err("image.tokenizer_config_json must use U8 storage".into());
356 }
357 serde_json::from_slice::<Value>(model.entry_bytes(entry))
358 .map_err(|e| format!("image.tokenizer_config_json: {e}"))?;
359 }
360
361 let hidden = value_usize(text, "hidden_size")?;
362 let intermediate = value_usize(text, "intermediate_size")?;
363 let n_layers = value_usize(text, "num_hidden_layers")?;
364 let heads = value_usize(text, "num_attention_heads")?;
365 let kv_heads = value_usize_default(text, "num_key_value_heads", heads);
366 let head_dim = hidden
367 .checked_div(heads)
368 .ok_or_else(|| "Qwen text heads must be positive".to_string())?;
369 if hidden == 0
370 || heads == 0
371 || hidden % heads != 0
372 || kv_heads == 0
373 || heads % kv_heads != 0
374 || head_dim < 2
375 || head_dim % 2 != 0
376 {
377 return Err(format!(
378 "invalid Qwen text geometry hidden={hidden}, heads={heads}, kv_heads={kv_heads}"
379 ));
380 }
381 if text
382 .get("hidden_act")
383 .and_then(Value::as_str)
384 .unwrap_or("silu")
385 != "silu"
386 {
387 return Err("Qwen2.5-VL text MLP requires hidden_act='silu'".into());
388 }
389 let eps = text
390 .get("rms_norm_eps")
391 .and_then(Value::as_f64)
392 .unwrap_or(EPS);
393 let rope_theta = text
394 .get("rope_theta")
395 .and_then(Value::as_f64)
396 .unwrap_or(1_000_000.0);
397 if !eps.is_finite() || eps <= 0.0 || !rope_theta.is_finite() || rope_theta <= 0.0 {
398 return Err("Qwen text epsilon/rope_theta must be finite and positive".into());
399 }
400 let mrope_section = parse_mrope(text, head_dim)?;
401 let layer_types = if let Some(a) = text.get("layer_types").and_then(Value::as_array) {
402 if a.len() != n_layers {
403 return Err(format!(
404 "layer_types length {} != num_hidden_layers {n_layers}",
405 a.len()
406 ));
407 }
408 a.iter()
409 .map(|v| {
410 v.as_str()
411 .map(str::to_owned)
412 .ok_or_else(|| "layer_types contains a non-string".to_string())
413 })
414 .collect::<Result<Vec<_>, _>>()?
415 } else {
416 let use_sliding = value_bool(text, "use_sliding_window", false);
417 let max_window_layers = value_usize_default(text, "max_window_layers", n_layers);
418 (0..n_layers)
419 .map(|i| {
420 if use_sliding && i >= max_window_layers {
421 "sliding_attention".into()
422 } else {
423 "full_attention".into()
424 }
425 })
426 .collect()
427 };
428 if layer_types
429 .iter()
430 .any(|kind| kind != "full_attention" && kind != "sliding_attention")
431 {
432 return Err("Qwen layer_types contains an unsupported attention type".into());
433 }
434 let sliding_window = value_usize_default(text, "sliding_window", 4096);
435 if layer_types.iter().any(|x| x == "sliding_attention") && sliding_window == 0 {
436 return Err("Qwen sliding_window must be positive".into());
437 }
438
439 let image_token_id =
440 special_id(&root, text, &tokenizer, "image_token_id", "<|image_pad|>")?;
441 let vision_start_token_id = special_id(
442 &root,
443 text,
444 &tokenizer,
445 "vision_start_token_id",
446 "<|vision_start|>",
447 )?;
448 let vision_end_token_id = special_id(
449 &root,
450 text,
451 &tokenizer,
452 "vision_end_token_id",
453 "<|vision_end|>",
454 )?;
455 for (token, id) in [
456 ("<|image_pad|>", image_token_id),
457 ("<|vision_start|>", vision_start_token_id),
458 ("<|vision_end|>", vision_end_token_id),
459 ] {
460 if let Some(tok_id) = tokenizer.token_to_id(token) {
461 if tok_id != id {
462 return Err(format!(
463 "config token id {id} for '{token}' disagrees with tokenizer id {tok_id}"
464 ));
465 }
466 }
467 }
468
469 let embed = Embedding::load(&model, "model.embed_tokens.weight")?;
470 if embed.cols() != hidden {
471 return Err(format!(
472 "model.embed_tokens width {} != text hidden {hidden}",
473 embed.cols()
474 ));
475 }
476 let mut layers = Vec::with_capacity(n_layers);
477 for i in 0..n_layers {
478 let p = format!("model.layers.{i}");
479 let q_rows = heads * head_dim;
480 let kv_rows = kv_heads * head_dim;
481 layers.push(TextLayer {
482 input_norm: vector(&model, &format!("{p}.input_layernorm.weight"), hidden)?,
483 q: linear(
484 &model,
485 &format!("{p}.self_attn.q_proj.weight"),
486 q_rows,
487 hidden,
488 )?,
489 q_bias: vector(&model, &format!("{p}.self_attn.q_proj.bias"), q_rows)?,
490 k: linear(
491 &model,
492 &format!("{p}.self_attn.k_proj.weight"),
493 kv_rows,
494 hidden,
495 )?,
496 k_bias: vector(&model, &format!("{p}.self_attn.k_proj.bias"), kv_rows)?,
497 v: linear(
498 &model,
499 &format!("{p}.self_attn.v_proj.weight"),
500 kv_rows,
501 hidden,
502 )?,
503 v_bias: vector(&model, &format!("{p}.self_attn.v_proj.bias"), kv_rows)?,
504 o: linear(
505 &model,
506 &format!("{p}.self_attn.o_proj.weight"),
507 hidden,
508 q_rows,
509 )?,
510 post_norm: vector(
511 &model,
512 &format!("{p}.post_attention_layernorm.weight"),
513 hidden,
514 )?,
515 gate: linear(
516 &model,
517 &format!("{p}.mlp.gate_proj.weight"),
518 intermediate,
519 hidden,
520 )?,
521 up: linear(
522 &model,
523 &format!("{p}.mlp.up_proj.weight"),
524 intermediate,
525 hidden,
526 )?,
527 down: linear(
528 &model,
529 &format!("{p}.mlp.down_proj.weight"),
530 hidden,
531 intermediate,
532 )?,
533 sliding: layer_types[i] == "sliding_attention",
534 });
535 }
536 let final_norm = vector(&model, "model.norm.weight", hidden)?;
537 let vision = VisionTower::from_cmf(&model, vision_cfg)?;
538 if vision.out_hidden != hidden {
539 return Err(format!(
540 "vision merger output {} != text hidden {hidden}; image features cannot be spliced",
541 vision.out_hidden
542 ));
543 }
544 Ok(Self {
545 model,
546 tokenizer,
547 processor,
548 embed,
549 layers,
550 final_norm,
551 vision,
552 hidden,
553 heads,
554 kv_heads,
555 head_dim,
556 intermediate,
557 eps,
558 rope_theta,
559 mrope_section,
560 sliding_window,
561 image_token_id,
562 vision_start_token_id,
563 vision_end_token_id,
564 })
565 }
566
567 pub fn hidden_size(&self) -> usize {
568 self.hidden
569 }
570
571 pub fn model_uid(&self) -> u64 {
575 self.model.uid()
576 }
577
578 fn rms_norm(&self, x: &[f32], w: &[f32], dst: &mut [f32]) {
579 let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
580 let inv = 1.0 / (ss + self.eps).sqrt();
581 for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
582 *d = (v as f64 * inv) as f32 * g;
583 }
584 }
585
586 fn build_prompt(
587 &self,
588 prompt: &str,
589 images: &[PreparedImage],
590 ) -> Result<(String, Vec<usize>), String> {
591 let mut image_text = String::new();
592 let mut token_counts = Vec::with_capacity(images.len());
593 for (i, image) in images.iter().enumerate() {
594 let count = image.image_tokens(self.processor.merge_size)?;
595 if count == 0 {
596 return Err(format!("image {} produced zero image tokens", i + 1));
597 }
598 token_counts.push(count);
599 image_text.push_str(&format!("Picture {}: <|vision_start|>", i + 1));
600 for _ in 0..count {
601 image_text.push_str("<|image_pad|>");
602 }
603 image_text.push_str("<|vision_end|>");
604 }
605 let mut out = String::with_capacity(
606 PROMPT_TEMPLATE_HEAD.len()
607 + image_text.len()
608 + prompt.len()
609 + PROMPT_TEMPLATE_TAIL.len(),
610 );
611 out.push_str(PROMPT_TEMPLATE_HEAD);
612 out.push_str(&image_text);
613 out.push_str(prompt);
614 out.push_str(PROMPT_TEMPLATE_TAIL);
615 Ok((out, token_counts))
616 }
617
618 fn locate_images(
619 &self,
620 ids: &[u32],
621 token_counts: &[usize],
622 features: &[Vec<f32>],
623 ) -> Result<Vec<ImagePlacement>, String> {
624 if token_counts.len() != features.len() {
625 return Err("image token/feature count mismatch".into());
626 }
627 let mut found = Vec::with_capacity(token_counts.len());
628 let mut cursor = 0usize;
629 for (image_idx, (&count, feature)) in token_counts.iter().zip(features).enumerate() {
630 if cursor >= ids.len() {
631 return Err(format!(
632 "image {} is missing vision_start token",
633 image_idx + 1
634 ));
635 }
636 let start_marker = ids[cursor..]
637 .iter()
638 .position(|&id| id == self.vision_start_token_id)
639 .map(|p| cursor + p)
640 .ok_or_else(|| format!("image {} is missing vision_start token", image_idx + 1))?;
641 let start = start_marker + 1;
642 if start + count >= ids.len() {
643 return Err(format!(
644 "image {} placeholder exceeds token sequence",
645 image_idx + 1
646 ));
647 }
648 if ids[start..start + count]
649 .iter()
650 .any(|&id| id != self.image_token_id)
651 {
652 return Err(format!(
653 "image {} placeholder run has a non-image token",
654 image_idx + 1
655 ));
656 }
657 if ids[start + count] != self.vision_end_token_id {
658 return Err(format!(
659 "image {} is missing vision_end after {} image tokens",
660 image_idx + 1,
661 count
662 ));
663 }
664 if feature.len() != count * self.hidden {
665 return Err(format!(
666 "image {} feature width {} != {} × {}",
667 image_idx + 1,
668 feature.len(),
669 count,
670 self.hidden
671 ));
672 }
673 found.push(ImagePlacement {
674 start,
675 count,
676 feature: feature.clone(),
677 });
678 cursor = start + count + 1;
679 }
680 let extra = ids[cursor..]
681 .iter()
682 .filter(|&&id| id == self.image_token_id)
683 .count();
684 if extra != 0 {
685 return Err("prompt contains more image placeholders than supplied images".into());
686 }
687 Ok(found)
688 }
689
690 fn mrope_positions(
691 &self,
692 ids: &[u32],
693 grids: &[Grid],
694 placements: &[ImagePlacement],
695 ) -> Result<Vec<[i64; 3]>, String> {
696 if grids.len() != placements.len() {
697 return Err("grid/placement count mismatch".into());
698 }
699 let mut pos = vec![[0i64; 3]; ids.len()];
700 let mut cursor = 0usize;
701 let mut prior_max = -1i64;
702 for (image_idx, (grid, placement)) in grids.iter().zip(placements).enumerate() {
703 if placement.start < cursor || placement.start + placement.count > ids.len() {
704 return Err(format!("image {image_idx} placement is outside prompt"));
705 }
706 let text_len = placement.start - cursor;
707 let st_idx = prior_max + 1;
708 for j in 0..text_len {
709 let v = st_idx + j as i64;
710 pos[cursor + j] = [v; 3];
711 }
712 let llm_h = grid.h / self.processor.merge_size;
713 let llm_w = grid.w / self.processor.merge_size;
714 let expected = grid
715 .t
716 .checked_mul(llm_h)
717 .and_then(|n| n.checked_mul(llm_w))
718 .ok_or_else(|| "image MRoPE grid overflow".to_string())?;
719 if expected != placement.count {
720 return Err(format!(
721 "image {image_idx} grid yields {expected} tokens, placeholder has {}",
722 placement.count
723 ));
724 }
725 for local in 0..placement.count {
726 let per_t = llm_h * llm_w;
727 let t = local / per_t;
728 let rem = local % per_t;
729 let h = rem / llm_w;
730 let w = rem % llm_w;
731 let base = text_len as i64 + st_idx;
732 pos[placement.start + local] = [base + t as i64, base + h as i64, base + w as i64];
733 }
734 let end = placement.start + placement.count;
735 prior_max = pos[cursor..end]
736 .iter()
737 .flat_map(|p| p.iter())
738 .copied()
739 .max()
740 .ok_or_else(|| "empty Qwen prompt segment".to_string())?;
741 cursor = end;
744 }
745 if cursor < ids.len() {
746 let st_idx = prior_max + 1;
747 for j in cursor..ids.len() {
748 let v = st_idx + (j - cursor) as i64;
749 pos[j] = [v; 3];
750 }
751 }
752 Ok(pos)
753 }
754
755 fn apply_text_rope(&self, q: &mut [f32], k: &mut [f32], positions: &[[i64; 3]]) {
756 Self::apply_mrope_rotary(
757 q,
758 k,
759 positions,
760 self.hidden,
761 self.heads,
762 self.kv_heads,
763 self.head_dim,
764 self.rope_theta,
765 self.mrope_section,
766 );
767 }
768 fn apply_mrope_rotary(
777 q: &mut [f32],
778 k: &mut [f32],
779 positions: &[[i64; 3]],
780 hidden: usize,
781 heads: usize,
782 kv_heads: usize,
783 head_dim: usize,
784 rope_theta: f64,
785 mrope_section: [usize; 3],
786 ) {
787 let half = head_dim / 2;
788 debug_assert_eq!(hidden, heads * head_dim);
789 debug_assert_eq!(q.len(), positions.len() * hidden);
790 debug_assert_eq!(k.len(), positions.len() * kv_heads * head_dim);
791 for (token, p) in positions.iter().enumerate() {
792 let mut cos = vec![0f32; head_dim];
793 let mut sin = vec![0f32; head_dim];
794 let mut axis_emb = vec![vec![0f32; head_dim]; 3];
800 let mut axis_sin = vec![vec![0f32; head_dim]; 3];
801 for axis in 0..3 {
802 for j in 0..half {
803 let freq = 1.0 / rope_theta.powf(2.0 * j as f64 / head_dim as f64);
804 let (s, c) = (p[axis] as f64 * freq).sin_cos();
805 axis_emb[axis][j] = c as f32;
806 axis_sin[axis][j] = s as f32;
807 axis_emb[axis][j + half] = c as f32;
808 axis_sin[axis][j + half] = s as f32;
809 }
810 }
811 let section_widths = [
812 mrope_section[0],
813 mrope_section[1],
814 mrope_section[2],
815 mrope_section[0],
816 mrope_section[1],
817 mrope_section[2],
818 ];
819 let mut offset = 0usize;
820 for (section, &width) in section_widths.iter().enumerate() {
821 let axis = section % 3;
822 let end = offset + width;
823 cos[offset..end].copy_from_slice(&axis_emb[axis][offset..end]);
824 sin[offset..end].copy_from_slice(&axis_sin[axis][offset..end]);
825 offset += width;
826 }
827 debug_assert_eq!(offset, head_dim);
828 for head in 0..heads {
829 let qv = &mut q
830 [token * hidden + head * head_dim..token * hidden + (head + 1) * head_dim];
831 for d in 0..half {
832 let (a, b) = (qv[d], qv[d + half]);
833 qv[d] = a * cos[d] - b * sin[d];
834 qv[d + half] = a * sin[d + half] + b * cos[d + half];
835 }
836 }
837 for head in 0..kv_heads {
838 let kv_offset = token * kv_heads * head_dim + head * head_dim;
839 let kv = &mut k[kv_offset..kv_offset + head_dim];
840 for d in 0..half {
841 let (a, b) = (kv[d], kv[d + half]);
842 kv[d] = a * cos[d] - b * sin[d];
843 kv[d + half] = a * sin[d + half] + b * cos[d + half];
844 }
845 }
846 }
847 }
848
849 fn text_forward(
850 &self,
851 ids: &[u32],
852 positions: &[[i64; 3]],
853 placements: &[ImagePlacement],
854 ) -> Result<Vec<f32>, String> {
855 if ids.is_empty() || ids.len() != positions.len() {
856 return Err("Qwen prompt token/position lengths do not match".into());
857 }
858 let n = ids.len();
859 let mut h = vec![0f32; n * self.hidden];
860 for (i, &id) in ids.iter().enumerate() {
861 self.embed
862 .row(id as usize, &mut h[i * self.hidden..(i + 1) * self.hidden])?;
863 }
864 for placement in placements {
865 h[placement.start * self.hidden..(placement.start + placement.count) * self.hidden]
866 .copy_from_slice(&placement.feature);
867 }
868 let pool = crate::pool::Pool::from_env();
869 let pool_ref = pool.as_deref();
870 let mut norm = vec![0f32; n * self.hidden];
871 let mut q = vec![0f32; n * self.hidden];
872 let mut k = vec![0f32; n * self.kv_heads * self.head_dim];
873 let mut v = vec![0f32; n * self.kv_heads * self.head_dim];
874 let mut attn = vec![0f32; n * self.hidden];
875 let mut proj = vec![0f32; n * self.hidden];
876 let mut gate = vec![0f32; n * self.intermediate];
877 let mut up = vec![0f32; n * self.intermediate];
878 let mut down = vec![0f32; n * self.hidden];
879 for layer in &self.layers {
880 for i in 0..n {
881 self.rms_norm(
882 &h[i * self.hidden..(i + 1) * self.hidden],
883 &layer.input_norm,
884 &mut norm[i * self.hidden..(i + 1) * self.hidden],
885 );
886 }
887 layer.q.forward(&norm, n, &mut q, pool_ref)?;
888 layer.k.forward(&norm, n, &mut k, pool_ref)?;
889 layer.v.forward(&norm, n, &mut v, pool_ref)?;
890 crate::qwen_image_ops::add_bias(&mut q, n, &layer.q_bias)?;
891 crate::qwen_image_ops::add_bias(&mut k, n, &layer.k_bias)?;
892 crate::qwen_image_ops::add_bias(&mut v, n, &layer.v_bias)?;
893 self.apply_text_rope(&mut q, &mut k, positions);
894 attn.fill(0.0);
895 let groups = self.heads / self.kv_heads;
896 for head in 0..self.heads {
897 let kv_head = head / groups;
898 for i in 0..n {
899 let first = if layer.sliding {
900 i.saturating_add(1).saturating_sub(self.sliding_window)
901 } else {
902 0
903 };
904 let qv = &q[i * self.hidden + head * self.head_dim
905 ..i * self.hidden + (head + 1) * self.head_dim];
906 let mut scores = vec![0f32; i - first + 1];
907 let mut mx = f32::NEG_INFINITY;
908 for (slot, j) in (first..=i).enumerate() {
909 let kv = &k[j * self.kv_heads * self.head_dim + kv_head * self.head_dim
910 ..j * self.kv_heads * self.head_dim + (kv_head + 1) * self.head_dim];
911 let score =
912 crate::attention::dot_f32(qv, kv) / (self.head_dim as f32).sqrt();
913 scores[slot] = score;
914 mx = mx.max(score);
915 }
916 let mut den = 0.0f32;
917 for score in &mut scores {
918 *score = (*score - mx).exp();
919 den += *score;
920 }
921 if den == 0.0 {
922 return Err("Qwen text attention softmax underflowed".into());
923 }
924 let inv = 1.0 / den;
925 let out = &mut attn[i * self.hidden + head * self.head_dim
926 ..i * self.hidden + (head + 1) * self.head_dim];
927 for (slot, j) in (first..=i).enumerate() {
928 let vv = &v[j * self.kv_heads * self.head_dim + kv_head * self.head_dim
929 ..j * self.kv_heads * self.head_dim + (kv_head + 1) * self.head_dim];
930 let weight = scores[slot] * inv;
931 for (o, &vv) in out.iter_mut().zip(vv) {
932 *o += weight * vv;
933 }
934 }
935 }
936 }
937 layer.o.forward(&attn, n, &mut proj, pool_ref)?;
938 for (a, &b) in h.iter_mut().zip(&proj) {
939 *a += b;
940 }
941 for i in 0..n {
942 self.rms_norm(
943 &h[i * self.hidden..(i + 1) * self.hidden],
944 &layer.post_norm,
945 &mut norm[i * self.hidden..(i + 1) * self.hidden],
946 );
947 }
948 layer.gate.forward(&norm, n, &mut gate, pool_ref)?;
949 layer.up.forward(&norm, n, &mut up, pool_ref)?;
950 for (g, &u) in gate.iter_mut().zip(&up) {
951 *g = (*g / (1.0 + (-*g).exp())) * u;
952 }
953 layer.down.forward(&gate, n, &mut down, pool_ref)?;
954 for (a, &b) in h.iter_mut().zip(&down) {
955 *a += b;
956 }
957 }
958 for i in 0..n {
959 self.rms_norm(
960 &h[i * self.hidden..(i + 1) * self.hidden],
961 &self.final_norm,
962 &mut norm[i * self.hidden..(i + 1) * self.hidden],
963 );
964 }
965 Ok(norm)
966 }
967
968 pub fn encode(&self, prompt: &str, images: &[RgbImage]) -> Result<Conditioning, String> {
972 let prepared: Vec<PreparedImage> = images
973 .iter()
974 .map(|image| prepare_image(image, &self.processor))
975 .collect::<Result<_, _>>()?;
976 let features = self.vision.forward(&prepared)?;
977 let (prompt_text, token_counts) = self.build_prompt(prompt, &prepared)?;
978 let ids = self.tokenizer.encode(&prompt_text);
979 if ids.iter().any(|&id| id as usize >= self.embed.rows()) {
980 return Err("Qwen tokenizer emitted an id outside the embedding table".into());
981 }
982 let mut grids = Vec::with_capacity(prepared.len());
983 for image in &prepared {
984 grids.push(Grid {
985 t: image.grid_t,
986 h: image.grid_h,
987 w: image.grid_w,
988 });
989 }
990 let placements = self.locate_images(&ids, &token_counts, &features)?;
991 let positions = self.mrope_positions(&ids, &grids, &placements)?;
992 let all = self.text_forward(&ids, &positions, &placements)?;
993 if all.len() != ids.len() * self.hidden || ids.len() <= DROP_PREFIX {
994 return Err(format!(
995 "Qwen final hidden shape {}×{} cannot drop required {}-token prefix",
996 ids.len(),
997 self.hidden,
998 DROP_PREFIX
999 ));
1000 }
1001 let hidden = all[DROP_PREFIX * self.hidden..].to_vec();
1002 Ok(Conditioning {
1003 seq_len: hidden.len() / self.hidden,
1004 hidden_size: self.hidden,
1005 hidden,
1006 })
1007 }
1008
1009 pub fn encode_pair(
1012 &self,
1013 prompt: &str,
1014 negative_prompt: &str,
1015 images: &[RgbImage],
1016 ) -> Result<(Conditioning, Conditioning), String> {
1017 Ok((
1018 self.encode(prompt, images)?,
1019 self.encode(negative_prompt, images)?,
1020 ))
1021 }
1022}
1023
1024struct ImagePlacement {
1025 start: usize,
1026 count: usize,
1027 feature: Vec<f32>,
1028}
1029
1030#[derive(Clone, Copy)]
1031struct Grid {
1032 t: usize,
1033 h: usize,
1034 w: usize,
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::{
1040 parse_mrope, QwenImageEncoder, DROP_PREFIX, PROMPT_TEMPLATE_HEAD, PROMPT_TEMPLATE_TAIL,
1041 };
1042 use serde_json::json;
1043
1044 #[test]
1045 fn canonical_template_and_prefix_contract_are_fixed() {
1046 assert!(PROMPT_TEMPLATE_HEAD.starts_with("<|im_start|>system\nDescribe"));
1047 assert!(PROMPT_TEMPLATE_TAIL.ends_with("<|im_start|>assistant\n"));
1048 assert_eq!(DROP_PREFIX, 64);
1049 }
1050
1051 #[test]
1052 fn tiny_hidden_width_uses_explicit_mrope_sections() {
1053 let cfg = json!({"rope_scaling":{"mrope_section":[1,1,2]}});
1054 assert_eq!(parse_mrope(&cfg, 8).unwrap(), [1, 1, 2]);
1055 assert!(parse_mrope(&json!({}), 8).is_err());
1056 }
1057
1058 #[test]
1059 fn canonical_mrope_repeats_sections_for_anisotropic_positions() {
1060 let hidden = 128;
1061 let heads = 1;
1062 let kv_heads = 1;
1063 let head_dim = 128;
1064 let rope_theta = 1_000_000.0;
1065 let sections = [16, 24, 24];
1066 let positions = [[3_i64, 5_i64, 7_i64]];
1067 let mut q: Vec<f32> = (0..hidden).map(|i| (i as f32 - 63.5) * 0.03125).collect();
1068 let mut k: Vec<f32> = (0..hidden)
1069 .map(|i| (63.5 - i as f32) * 0.017578125)
1070 .collect();
1071 let q_before = q.clone();
1072 let k_before = k.clone();
1073
1074 QwenImageEncoder::apply_mrope_rotary(
1075 &mut q, &mut k, &positions, hidden, heads, kv_heads, head_dim, rope_theta, sections,
1076 );
1077
1078 for &(d, axis) in &[(0usize, 0usize), (16, 1), (40, 2)] {
1083 let j = d as f64;
1084 let freq = 1.0 / rope_theta.powf(2.0 * j / head_dim as f64);
1085 let angle = positions[0][axis] as f64 * freq;
1086 let (sin, cos) = angle.sin_cos();
1087 let (a, b) = (q_before[d], q_before[d + head_dim / 2]);
1088 let expected_a = a as f64 * cos - b as f64 * sin;
1089 let expected_b = a as f64 * sin + b as f64 * cos;
1090 assert!((q[d] as f64 - expected_a).abs() < 2e-6);
1091 assert!((q[d + head_dim / 2] as f64 - expected_b).abs() < 2e-6);
1092 }
1093
1094 let norm_sq = |x: &[f32]| x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>();
1095 let q_norm_error = (norm_sq(&q) - norm_sq(&q_before)).abs() / norm_sq(&q_before);
1096 let k_norm_error = (norm_sq(&k) - norm_sq(&k_before)).abs() / norm_sq(&k_before);
1097 assert!(
1098 q_norm_error < 2e-6,
1099 "Q rotation changed norm by {q_norm_error:e}"
1100 );
1101 assert!(
1102 k_norm_error < 2e-6,
1103 "K rotation changed norm by {k_norm_error:e}"
1104 );
1105 }
1106}