1use crate::qwen_image::QwenImageTransformer;
5use crate::qwen_image_encoder::{Conditioning, QwenImageEncoder};
6use crate::qwen_image_vae::QwenImageVae;
7use crate::sampler::SplitMix64;
8use cortiq_core::CmfModel;
9use image::{RgbImage, imageops::FilterType};
10use serde::Deserialize;
11use std::path::{Path, PathBuf};
12
13pub struct QwenImagePaths {
14 pub transformer: PathBuf,
15 pub text_encoder: PathBuf,
16 pub vae: PathBuf,
17 pub scheduler: Option<PathBuf>,
18}
19
20pub struct QwenImageParams {
21 pub height: usize,
22 pub width: usize,
23 pub steps: usize,
24 pub true_cfg_scale: f32,
25 pub negative_prompt: Option<String>,
26 pub seed: u64,
27 pub reference_size: usize,
29 pub initial_latents: Option<PathBuf>,
31}
32
33impl Default for QwenImageParams {
34 fn default() -> Self {
35 Self {
36 height: 1024,
37 width: 1024,
38 steps: 40,
39 true_cfg_scale: 4.0,
40 negative_prompt: Some(" ".into()),
41 seed: 0,
42 reference_size: 1024,
43 initial_latents: None,
44 }
45 }
46}
47
48pub struct QwenImageOutput {
49 pub pixels: Vec<f32>,
51 pub height: usize,
52 pub width: usize,
53}
54
55impl QwenImageOutput {
56 pub fn save(&self, path: &Path) -> Result<(), String> {
57 let plane = self.width * self.height;
58 if self.pixels.len() != plane * 3 {
59 return Err("invalid output RGB plane lengths".into());
60 }
61 let mut bytes = Vec::with_capacity(plane * 3);
62 for p in 0..plane {
63 for c in 0..3 {
64 bytes.push((self.pixels[c * plane + p].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
65 }
66 }
67 if path
68 .extension()
69 .and_then(|s| s.to_str())
70 .is_some_and(|s| s.eq_ignore_ascii_case("ppm"))
71 {
72 let mut ppm = format!("P6\n{} {}\n255\n", self.width, self.height).into_bytes();
73 ppm.extend(bytes);
74 return std::fs::write(path, ppm).map_err(|e| e.to_string());
75 }
76 RgbImage::from_raw(self.width as u32, self.height as u32, bytes)
77 .ok_or("output image dimensions overflow")?
78 .save(path)
79 .map_err(|e| format!("{}: {e}", path.display()))
80 }
81}
82
83pub fn edit_files(
84 paths: &QwenImagePaths,
85 prompt: &str,
86 images: &[PathBuf],
87 params: &QwenImageParams,
88 progress: impl FnMut(&str, usize, usize),
89) -> Result<QwenImageOutput, String> {
90 let images = images
91 .iter()
92 .map(|path| {
93 image::open(path)
94 .map(|image| image.into_rgb8())
95 .map_err(|e| format!("{}: {e}", path.display()))
96 })
97 .collect::<Result<Vec<_>, _>>()?;
98 edit(paths, prompt, &images, params, progress)
99}
100
101#[derive(Clone, Debug, Deserialize)]
102#[serde(default)]
103pub struct FlowMatchConfig {
104 pub base_image_seq_len: usize,
105 pub max_image_seq_len: usize,
106 pub base_shift: f32,
107 pub max_shift: f32,
108 pub shift_terminal: f32,
109 pub num_train_timesteps: usize,
110 pub use_dynamic_shifting: bool,
111 pub time_shift_type: String,
112 pub invert_sigmas: bool,
113 pub stochastic_sampling: bool,
114 pub use_karras_sigmas: bool,
115 pub use_exponential_sigmas: bool,
116 pub use_beta_sigmas: bool,
117}
118
119impl Default for FlowMatchConfig {
120 fn default() -> Self {
121 Self {
122 base_image_seq_len: 256,
123 max_image_seq_len: 8192,
124 base_shift: 0.5,
125 max_shift: 0.9,
126 shift_terminal: 0.02,
127 num_train_timesteps: 1000,
128 use_dynamic_shifting: true,
129 time_shift_type: "exponential".into(),
130 invert_sigmas: false,
131 stochastic_sampling: false,
132 use_karras_sigmas: false,
133 use_exponential_sigmas: false,
134 use_beta_sigmas: false,
135 }
136 }
137}
138
139pub fn flow_match_sigmas(
142 steps: usize,
143 generated_tokens: usize,
144 config: &FlowMatchConfig,
145) -> Result<Vec<f32>, String> {
146 if steps < 2 || steps > 1000 || generated_tokens == 0 {
147 return Err("Qwen Image needs 2..=1000 steps and a nonempty latent grid".into());
148 }
149 if !config.use_dynamic_shifting
150 || config.time_shift_type != "exponential"
151 || config.invert_sigmas
152 || config.stochastic_sampling
153 || config.use_karras_sigmas
154 || config.use_exponential_sigmas
155 || config.use_beta_sigmas
156 || config.num_train_timesteps != 1000
157 || config.max_image_seq_len <= config.base_image_seq_len
158 || !config.base_shift.is_finite()
159 || !config.max_shift.is_finite()
160 || !(0.0..1.0).contains(&config.shift_terminal)
161 {
162 return Err(
163 "scheduler is not the supported Qwen Image exponential FlowMatch Euler contract".into(),
164 );
165 }
166 let slope = (config.max_shift as f64 - config.base_shift as f64)
167 / (config.max_image_seq_len - config.base_image_seq_len) as f64;
168 let mu = config.base_shift as f64
169 + slope * (generated_tokens as f64 - config.base_image_seq_len as f64);
170 let exp_mu = mu.exp() as f32;
171 let mut sigmas = (0..steps)
172 .map(|i| {
173 let sigma = (1.0 - i as f64 * (1.0 - 1.0 / steps as f64) / (steps - 1) as f64) as f32;
174 exp_mu / (exp_mu + (1.0 / sigma - 1.0))
175 })
176 .collect::<Vec<_>>();
177 if config.shift_terminal != 0.0 {
178 let scale = (1.0 - sigmas[steps - 1]) / (1.0 - config.shift_terminal);
179 if !scale.is_finite() || scale <= 0.0 {
180 return Err("invalid terminal-stretch scale".into());
181 }
182 for sigma in &mut sigmas {
183 *sigma = 1.0 - (1.0 - *sigma) / scale;
184 }
185 }
186 sigmas.push(0.0);
187 if sigmas.iter().any(|v| !v.is_finite()) || sigmas.windows(2).any(|w| w[0] <= w[1]) {
188 return Err("Qwen Image scheduler did not produce finite descending sigmas".into());
189 }
190 Ok(sigmas)
191}
192
193fn latent_len(channels: usize, height: usize, width: usize) -> Result<usize, String> {
194 channels
195 .checked_mul(height)
196 .and_then(|v| v.checked_mul(width))
197 .filter(|&v| v > 0)
198 .ok_or_else(|| "empty or overflowing latent dimensions".into())
199}
200
201pub fn pack_latents(
203 input: &[f32],
204 channels: usize,
205 height: usize,
206 width: usize,
207) -> Result<Vec<f32>, String> {
208 if height % 2 != 0 || width % 2 != 0 || input.len() != latent_len(channels, height, width)? {
209 return Err("latent packing needs even spatial dimensions and exact NCHW data".into());
210 }
211 let mut out = Vec::with_capacity(input.len());
212 for y in (0..height).step_by(2) {
213 for x in (0..width).step_by(2) {
214 for channel in 0..channels {
215 for dy in 0..2 {
216 for dx in 0..2 {
217 out.push(input[(channel * height + y + dy) * width + x + dx]);
218 }
219 }
220 }
221 }
222 }
223 Ok(out)
224}
225
226pub fn unpack_latents(
227 input: &[f32],
228 channels: usize,
229 height: usize,
230 width: usize,
231) -> Result<Vec<f32>, String> {
232 if height % 2 != 0 || width % 2 != 0 || input.len() != latent_len(channels, height, width)? {
233 return Err("latent unpacking needs even spatial dimensions and exact packed data".into());
234 }
235 let mut out = vec![0.0; input.len()];
236 let mut i = 0;
237 for y in (0..height).step_by(2) {
238 for x in (0..width).step_by(2) {
239 for channel in 0..channels {
240 for dy in 0..2 {
241 for dx in 0..2 {
242 out[(channel * height + y + dy) * width + x + dx] = input[i];
243 i += 1;
244 }
245 }
246 }
247 }
248 }
249 Ok(out)
250}
251
252#[derive(Deserialize)]
253struct VaeScale {
254 z_dim: usize,
255 latents_mean: Vec<f32>,
256 latents_std: Vec<f32>,
257}
258
259fn vae_scale(path: &Path) -> Result<VaeScale, String> {
260 let model = CmfModel::open(path).map_err(|e| e.to_string())?;
261 let config_name = if model.tensor("image.vae.config_json").is_some() {
262 "image.vae.config_json"
263 } else {
264 "image.config_json"
265 };
266 let config: VaeScale = serde_json::from_slice(
267 model
268 .tensor_bytes(config_name)
269 .map_err(|e| format!("VAE CMF image.config_json: {e}"))?,
270 )
271 .map_err(|e| format!("VAE configuration: {e}"))?;
272 if config.z_dim != 16
273 || config.latents_mean.len() != 16
274 || config.latents_std.len() != 16
275 || config.latents_mean.iter().any(|v| !v.is_finite())
276 || config
277 .latents_std
278 .iter()
279 .any(|v| !v.is_finite() || *v <= 0.0)
280 {
281 return Err(
282 "Qwen Image requires 16 finite VAE means and positive standard deviations".into(),
283 );
284 }
285 Ok(config)
286}
287
288fn embedded_scheduler(path: &Path) -> Result<Option<FlowMatchConfig>, String> {
289 let model = CmfModel::open(path).map_err(|e| e.to_string())?;
290 let Some(entry) = model.tensor("image.scheduler_config_json") else {
291 return Ok(None);
292 };
293 if entry.dtype != cortiq_core::TensorDtype::U8
294 || entry.shape.len() != 1
295 || entry.shape[0] != entry.n_elems()
296 {
297 return Err("embedded Qwen scheduler must be a one-dimensional U8 blob".into());
298 }
299 serde_json::from_slice(model.entry_bytes(entry))
300 .map(Some)
301 .map_err(|e| format!("embedded scheduler configuration: {e}"))
302}
303
304fn normalize_latents(data: &mut [f32], config: &VaeScale, decode: bool) -> Result<(), String> {
305 if data.is_empty() || config.z_dim == 0 || data.len() % config.z_dim != 0 {
306 return Err("invalid VAE channel planes".into());
307 }
308 let plane = data.len() / config.z_dim;
309 for (channel, row) in data.chunks_exact_mut(plane).enumerate() {
310 for x in row {
311 *x = if decode {
312 *x * config.latents_std[channel] + config.latents_mean[channel]
313 } else {
314 (*x - config.latents_mean[channel]) / config.latents_std[channel]
315 };
316 }
317 }
318 Ok(())
319}
320
321fn gaussian_noise(count: usize, seed: u64, path: Option<&Path>) -> Result<Vec<f32>, String> {
322 if let Some(path) = path {
323 let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
324 if bytes.len() != count * 4 {
325 return Err("initial latent file has the wrong size".into());
326 }
327 let data = bytes
328 .chunks_exact(4)
329 .map(|b| f32::from_le_bytes(b.try_into().unwrap()))
330 .collect::<Vec<_>>();
331 if data.iter().any(|v| !v.is_finite()) {
332 return Err("initial latents contain nonfinite values".into());
333 }
334 return Ok(data);
335 }
336 let mut rng = SplitMix64::new(seed);
337 let mut data = Vec::with_capacity(count);
338 while data.len() < count {
339 let a = ((rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64).max(1e-300);
340 let b = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
341 let radius = (-2.0 * a.ln()).sqrt();
342 let angle = 2.0 * std::f64::consts::PI * b;
343 data.push((radius * angle.cos()) as f32);
344 if data.len() < count {
345 data.push((radius * angle.sin()) as f32);
346 }
347 }
348 Ok(data)
349}
350
351fn validate_conditioning(value: &Conditioning) -> Result<(), String> {
352 if value.seq_len == 0
353 || value.hidden_size == 0
354 || value.hidden.len()
355 != value
356 .seq_len
357 .checked_mul(value.hidden_size)
358 .ok_or("conditioning size overflow")?
359 || value.hidden.iter().any(|v| !v.is_finite())
360 {
361 return Err("text encoder returned invalid or nonfinite conditioning".into());
362 }
363 Ok(())
364}
365
366fn rescale_cfg(conditional: &mut [f32], unconditional: &[f32], scale: f32) -> Result<(), String> {
368 if conditional.len() != unconditional.len() || conditional.len() % 64 != 0 || !scale.is_finite()
369 {
370 return Err("invalid packed CFG prediction shape or scale".into());
371 }
372 for (cond, uncond) in conditional
373 .chunks_exact_mut(64)
374 .zip(unconditional.chunks_exact(64))
375 {
376 let cond_norm = cond.iter().map(|v| v * v).sum::<f32>().sqrt();
377 for (c, u) in cond.iter_mut().zip(uncond) {
378 *c = *u + scale * (*c - *u);
379 }
380 let combined_norm = cond.iter().map(|v| v * v).sum::<f32>().sqrt();
381 if combined_norm > 0.0 {
382 for v in cond {
383 *v *= cond_norm / combined_norm;
384 }
385 }
386 }
387 Ok(())
388}
389
390pub fn edit(
392 paths: &QwenImagePaths,
393 prompt: &str,
394 images: &[RgbImage],
395 params: &QwenImageParams,
396 mut progress: impl FnMut(&str, usize, usize),
397) -> Result<QwenImageOutput, String> {
398 let (height, width) = (params.height / 16 * 16, params.width / 16 * 16);
399 if images.is_empty() || images.iter().any(|i| i.width() == 0 || i.height() == 0) {
400 return Err("Qwen Image Edit requires at least one reference image".into());
401 }
402 if height == 0
403 || width == 0
404 || height > 2048
405 || width > 2048
406 || params.reference_size < 32
407 || params.reference_size > 2048
408 || !params.true_cfg_scale.is_finite()
409 || params.true_cfg_scale < 0.0
410 {
411 return Err("invalid Qwen Image dimensions, reference size, or CFG scale".into());
412 }
413 for path in [&paths.transformer, &paths.text_encoder, &paths.vae] {
414 if !path.is_file() {
415 return Err(format!("missing Qwen Image component: {}", path.display()));
416 }
417 }
418 let scheduler = match &paths.scheduler {
419 Some(path) => serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?)
420 .map_err(|e| format!("scheduler configuration: {e}"))?,
421 None => embedded_scheduler(&paths.transformer)?.unwrap_or_default(),
422 };
423 let (lh, lw) = (height / 8, width / 8);
424 let sigmas = flow_match_sigmas(params.steps, lh * lw / 4, &scheduler)?;
425 let scale = vae_scale(&paths.vae)?;
426 progress("text encoder", 0, params.steps);
427 let (positive, negative) = {
428 let mut stage = crate::gpu::image_stage_scope();
429 let encoder = QwenImageEncoder::open(&paths.text_encoder)?;
430 stage.track_model(encoder.model_uid());
431 let positive = encoder.encode(prompt, images)?;
432 validate_conditioning(&positive)?;
433 let negative = if params.true_cfg_scale > 1.0 {
434 params
435 .negative_prompt
436 .as_deref()
437 .map(|p| encoder.encode(p, images))
438 .transpose()?
439 } else {
440 None
441 };
442 if let Some(value) = &negative {
443 validate_conditioning(value)?;
444 }
445 (positive, negative)
446 };
447 progress("reference VAE", 0, params.steps);
448 let mut shapes = vec![[1, lh / 2, lw / 2]];
449 let reference = {
450 let _stage = crate::gpu::image_stage_scope();
451 let vae = QwenImageVae::open(&paths.vae)?;
452 let mut packed = Vec::new();
453 for image in images {
454 let ratio = image.width() as f64 / image.height() as f64;
455 let w = (params.reference_size as f64 * ratio.sqrt() / 32.0)
456 .round_ties_even()
457 .max(1.0) as usize
458 * 32;
459 let h = (params.reference_size as f64 / ratio.sqrt() / 32.0)
460 .round_ties_even()
461 .max(1.0) as usize
462 * 32;
463 if w > 32768 || h > 32768 || packed.len() / 64 + w / 16 * (h / 16) + lh * lw / 4 > 16384
464 {
465 return Err("reference images exceed the Qwen Image token budget".into());
466 }
467 let resized = image::imageops::resize(image, w as u32, h as u32, FilterType::Lanczos3);
468 let mut pixels = vec![0.0; w * h * 3];
469 for (i, pixel) in resized.pixels().enumerate() {
470 for channel in 0..3 {
471 pixels[channel * w * h + i] = pixel[channel] as f32 / 127.5 - 1.0;
472 }
473 }
474 let mut latent = vae.encode_mean(&pixels, h, w)?;
475 if latent.len() != latent_len(16, h / 8, w / 8)? {
476 return Err("VAE encoder returned the wrong latent shape".into());
477 }
478 normalize_latents(&mut latent, &scale, false)?;
479 packed.extend(pack_latents(&latent, 16, h / 8, w / 8)?);
480 shapes.push([1, h / 16, w / 16]);
481 }
482 packed
483 };
484 let initial_path = params
485 .initial_latents
486 .clone()
487 .or_else(|| std::env::var_os("CMF_INIT_LATENT").map(PathBuf::from));
488 let noise = gaussian_noise(
489 latent_len(16, lh, lw)?,
490 params.seed,
491 initial_path.as_deref(),
492 )?;
493 let mut latents = pack_latents(&noise, 16, lh, lw)?;
494 drop(noise);
495 progress("denoiser", 0, params.steps);
496 {
497 let mut stage = crate::gpu::image_stage_scope();
498 let transformer = QwenImageTransformer::open(&paths.transformer)?;
499 stage.track_model(transformer.model_uid());
500 let mut input = Vec::with_capacity(latents.len() + reference.len());
501 for step in 0..params.steps {
502 input.clear();
503 input.extend_from_slice(&latents);
504 input.extend_from_slice(&reference);
505 let mut prediction = transformer.forward(
506 &input,
507 &positive.hidden,
508 &shapes,
509 positive.seq_len,
510 sigmas[step],
511 )?;
512 if prediction.len() != input.len() {
513 return Err("transformer output shape does not match packed image tokens".into());
514 }
515 prediction.truncate(latents.len());
516 if let Some(negative) = &negative {
517 let unconditional = transformer.forward(
518 &input,
519 &negative.hidden,
520 &shapes,
521 negative.seq_len,
522 sigmas[step],
523 )?;
524 if unconditional.len() != input.len() {
525 return Err("negative transformer output has the wrong shape".into());
526 }
527 rescale_cfg(
528 &mut prediction,
529 &unconditional[..latents.len()],
530 params.true_cfg_scale,
531 )?;
532 }
533 let dt = sigmas[step + 1] - sigmas[step];
534 for (x, dx) in latents.iter_mut().zip(prediction) {
535 *x += dt * dx;
536 }
537 if latents.iter().any(|v| !v.is_finite()) {
538 return Err(format!("nonfinite latent at step {}", step + 1));
539 }
540 progress("denoiser", step + 1, params.steps);
541 }
542 }
543 drop(reference);
544 drop(positive);
545 drop(negative);
546 progress("decode VAE", params.steps, params.steps);
547 let mut raw = unpack_latents(&latents, 16, lh, lw)?;
548 drop(latents);
549 normalize_latents(&mut raw, &scale, true)?;
550 let mut pixels = {
551 let _stage = crate::gpu::image_stage_scope();
552 QwenImageVae::open(&paths.vae)?.decode(&raw, lh, lw)?
553 };
554 if pixels.len() != latent_len(3, height, width)? || pixels.iter().any(|v| !v.is_finite()) {
555 return Err("VAE decoder returned invalid RGB pixels".into());
556 }
557 for value in &mut pixels {
558 *value = (*value * 0.5 + 0.5).clamp(0.0, 1.0);
559 }
560 Ok(QwenImageOutput {
561 pixels,
562 height,
563 width,
564 })
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 fn fixture() -> serde_json::Value {
571 serde_json::from_str(include_str!("../tests/fixtures/qwen_image_pipeline.json")).unwrap()
572 }
573 fn values(v: &serde_json::Value) -> Vec<f32> {
574 serde_json::from_value(v.clone()).unwrap()
575 }
576 fn close(got: &[f32], expected: &[f32], tolerance: f32) {
577 assert_eq!(got.len(), expected.len());
578 for (i, (a, b)) in got.iter().zip(expected).enumerate() {
579 assert!(
580 a.is_finite() && (a - b).abs() <= tolerance,
581 "element {i}: {a} != {b}"
582 );
583 }
584 }
585 #[test]
586 fn official_diffusers_flowmatch_schedules() {
587 let reference = fixture();
588 for case in reference["schedules"].as_array().unwrap() {
589 let result = flow_match_sigmas(
590 case["steps"].as_u64().unwrap() as usize,
591 case["tokens"].as_u64().unwrap() as usize,
592 &FlowMatchConfig::default(),
593 )
594 .unwrap();
595 close(&result, &values(&case["sigmas"]), 3e-7);
596 }
597 }
598 #[test]
599 fn official_diffusers_non_square_latent_layout() {
600 let reference = fixture();
601 close(
602 &pack_latents(&values(&reference["nchw"]), 2, 4, 6).unwrap(),
603 &values(&reference["packed"]),
604 0.0,
605 );
606 close(
607 &unpack_latents(&values(&reference["packed"]), 2, 4, 6).unwrap(),
608 &values(&reference["unpacked"]),
609 0.0,
610 );
611 assert!(pack_latents(&[0.0; 6], 1, 3, 2).is_err());
612 }
613 #[test]
614 fn official_diffusers_cfg_rescales_each_image_token() {
615 let reference = fixture();
616 let mut prediction = values(&reference["cond"]);
617 rescale_cfg(&mut prediction, &values(&reference["uncond"]), 4.0).unwrap();
618 close(&prediction, &values(&reference["cfg"]), 5e-7);
619 }
620}