1use crate::tokenizer::Tokenizer;
28use crate::zimage::{ZImageDit, ZShape};
29use std::path::Path;
30use std::sync::Arc;
31use std::time::Instant;
32
33pub const ARCH_NAME: &str = "z_image";
35pub const MAX_TOKENS: usize = 512;
38
39#[derive(Clone, Debug)]
42pub struct ZDefaults {
43 pub variant: String,
45 pub steps: usize,
46 pub guidance: f32,
47 pub shift: f32,
48 pub height: usize,
49 pub width: usize,
50 pub cfg_normalization: f32,
52 pub cfg_truncation: f32,
53 pub negative_prompt: String,
54 pub max_sequence_length: usize,
55}
56
57impl Default for ZDefaults {
58 fn default() -> Self {
59 Self {
60 variant: "turbo".into(),
61 steps: crate::zimage::DEFAULT_STEPS,
62 guidance: 0.0,
63 shift: crate::zimage::DEFAULT_SHIFT,
64 height: 1024,
65 width: 1024,
66 cfg_normalization: 0.0,
67 cfg_truncation: 1.0,
68 negative_prompt: String::new(),
69 max_sequence_length: MAX_TOKENS,
70 }
71 }
72}
73
74impl ZDefaults {
75 pub fn from_json(v: &serde_json::Value) -> Self {
76 let d = Self::default();
77 let f = |k: &str, dv: f32| v[k].as_f64().map(|x| x as f32).unwrap_or(dv);
78 let u = |k: &str, dv: usize| v[k].as_u64().map(|x| x as usize).unwrap_or(dv);
79 Self {
80 variant: v["variant"].as_str().unwrap_or(&d.variant).to_string(),
81 steps: u("steps", d.steps),
82 guidance: f("guidance", d.guidance),
83 shift: f("shift", d.shift),
84 height: u("height", d.height),
85 width: u("width", d.width),
86 cfg_normalization: match &v["cfg_normalization"] {
87 serde_json::Value::Bool(b) => *b as u8 as f32,
88 x => x.as_f64().map(|x| x as f32).unwrap_or(d.cfg_normalization),
89 },
90 cfg_truncation: f("cfg_truncation", d.cfg_truncation),
91 negative_prompt: v["negative_prompt"]
92 .as_str()
93 .unwrap_or(&d.negative_prompt)
94 .to_string(),
95 max_sequence_length: u("max_sequence_length", d.max_sequence_length),
96 }
97 }
98
99 pub fn of(model: &cortiq_core::CmfModel) -> Self {
101 model
102 .tensor_bytes("zimage.config_json")
103 .ok()
104 .and_then(|b| serde_json::from_slice::<serde_json::Value>(b).ok())
105 .map(|v| Self::from_json(&v))
106 .unwrap_or_default()
107 }
108}
109
110#[derive(Clone, Debug)]
113pub struct ZParams {
114 pub height: usize,
115 pub width: usize,
116 pub steps: usize,
117 pub seed: u64,
118 pub shift: f32,
119 pub max_tokens: usize,
120 pub guidance: f32,
122 pub negative_prompt: Option<String>,
124 pub cfg_normalization: f32,
126 pub cfg_truncation: f32,
128 pub num_images: usize,
130}
131
132impl Default for ZParams {
133 fn default() -> Self {
134 Self::from_defaults(&ZDefaults::default())
135 }
136}
137
138impl ZParams {
139 pub fn from_defaults(d: &ZDefaults) -> Self {
140 Self {
141 height: d.height,
142 width: d.width,
143 steps: d.steps,
144 seed: 42,
145 shift: d.shift,
146 max_tokens: d.max_sequence_length,
147 guidance: d.guidance,
148 negative_prompt: None,
149 cfg_normalization: d.cfg_normalization,
150 cfg_truncation: d.cfg_truncation,
151 num_images: 1,
152 }
153 }
154}
155
156pub struct ZImage {
158 pub rgb: Vec<u8>,
159 pub height: usize,
160 pub width: usize,
161 pub seed: u64,
162}
163
164impl ZImage {
165 pub fn save(&self, path: &Path) -> Result<(), String> {
167 if path
168 .extension()
169 .and_then(|s| s.to_str())
170 .is_some_and(|s| s.eq_ignore_ascii_case("ppm"))
171 {
172 let mut ppm = format!("P6\n{} {}\n255\n", self.width, self.height).into_bytes();
173 ppm.extend_from_slice(&self.rgb);
174 return std::fs::write(path, ppm).map_err(|e| e.to_string());
175 }
176 image::RgbImage::from_raw(self.width as u32, self.height as u32, self.rgb.clone())
177 .ok_or("output image dimensions overflow")?
178 .save(path)
179 .map_err(|e| format!("{}: {e}", path.display()))
180 }
181}
182
183pub fn chat_template(prompt: &str) -> String {
187 format!("<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n")
188}
189
190pub fn prompt_ids(tok: &Tokenizer, prompt: &str, max_tokens: usize) -> Vec<u32> {
192 let mut ids = tok.encode(&chat_template(prompt));
193 ids.truncate(max_tokens);
194 ids
195}
196
197fn gauss_latent(n: usize, seed: u64) -> Result<Vec<f32>, String> {
200 if let Ok(path) = std::env::var("CMF_INIT_LATENT") {
201 let b = std::fs::read(&path).map_err(|e| format!("{path}: {e}"))?;
202 if b.len() != n * 4 {
203 return Err(format!("{path}: {} floats, the latent needs {n}", b.len() / 4));
204 }
205 return Ok(b
206 .chunks_exact(4)
207 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
208 .collect());
209 }
210 let mut rng = crate::sampler::SplitMix64::new(seed);
211 let mut u = || (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
212 let mut out = Vec::with_capacity(n);
213 while out.len() < n {
214 let (a, b) = (u().max(1e-300), u());
215 let r = (-2.0 * a.ln()).sqrt();
216 let ang = 2.0 * std::f64::consts::PI * b;
217 out.push((r * ang.cos()) as f32);
218 if out.len() < n {
219 out.push((r * ang.sin()) as f32);
220 }
221 }
222 Ok(out)
223}
224
225fn trace_write(dir: &str, img: usize, name: &str, v: &[f32]) -> Result<(), String> {
226 let path = if img == 0 {
227 format!("{dir}/{name}.f32")
228 } else {
229 format!("{dir}/img{img}_{name}.f32")
230 };
231 let _ = std::fs::create_dir_all(dir);
232 let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
233 std::fs::write(&path, b).map_err(|e| format!("{path}: {e}"))
234}
235
236fn prof_on() -> bool {
237 std::env::var("CMF_ZIMAGE_PROF").is_ok_and(|v| v != "0")
238}
239
240pub fn cfg_combine(pos: &mut [f32], neg: &[f32], g: f32, norm_clip: f32) {
244 let pos_norm = if norm_clip > 0.0 {
245 pos.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>().sqrt()
246 } else {
247 0.0
248 };
249 for (p, &n) in pos.iter_mut().zip(neg) {
250 *p += g * (*p - n);
251 }
252 if norm_clip > 0.0 {
253 let new_norm = pos.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>().sqrt();
254 let max_norm = (pos_norm as f32 * norm_clip) as f64;
255 if new_norm > max_norm {
256 let f = (max_norm / new_norm) as f32;
257 for p in pos.iter_mut() {
258 *p *= f;
259 }
260 }
261 }
262}
263
264#[derive(Clone, Debug, Default)]
266pub struct ZTimings {
267 pub text_encode: f64,
268 pub dit_load: f64,
269 pub prepare: f64,
270 pub steps: Vec<f64>,
271 pub vae: f64,
272 pub total: f64,
273}
274
275impl ZTimings {
276 pub fn median_step(&self) -> f64 {
277 let mut s = self.steps.clone();
278 if s.is_empty() {
279 return 0.0;
280 }
281 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
282 s[s.len() / 2]
283 }
284}
285
286pub fn generate(
289 model_path: &Path,
290 prompt: &str,
291 p: &ZParams,
292 mut progress: impl FnMut(usize, usize),
293) -> Result<Vec<u8>, String> {
294 let p1 = ZParams {
295 num_images: 1,
296 ..p.clone()
297 };
298 let (mut imgs, _) = generate_images(model_path, prompt, &p1, |_, i, n| progress(i, n))?;
299 Ok(imgs.remove(0).rgb)
300}
301
302pub fn generate_images(
305 model_path: &Path,
306 prompt: &str,
307 p: &ZParams,
308 mut progress: impl FnMut(usize, usize, usize),
309) -> Result<(Vec<ZImage>, ZTimings), String> {
310 if p.height % 16 != 0 || p.width % 16 != 0 || p.height == 0 || p.width == 0 {
311 return Err(format!(
312 "height and width must be positive multiples of 16 (got {}x{})",
313 p.width, p.height
314 ));
315 }
316 if p.height > 4096 || p.width > 4096 {
319 return Err(format!(
320 "height and width must be at most 4096 (got {}x{})",
321 p.width, p.height
322 ));
323 }
324 if p.steps == 0 {
325 return Err("steps must be at least 1".into());
326 }
327 let t_all = Instant::now();
328 let mut tm = ZTimings::default();
329 let model = Arc::new(
330 cortiq_core::CmfModel::open(model_path)
331 .map_err(|e| format!("{}: {e}", model_path.display()))?,
332 );
333 if model.header.arch.arch_name != ARCH_NAME {
334 return Err(format!(
335 "{}: architecture '{}' is not {ARCH_NAME}",
336 model_path.display(),
337 model.header.arch.arch_name
338 ));
339 }
340 let vocab = model
341 .vocab
342 .as_deref()
343 .ok_or("Z-Image .cmf has no embedded tokenizer")?;
344 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
345 let defaults = ZDefaults::of(&model);
346 let do_cfg = p.guidance > 0.0;
347 let warm = crate::zimage::gpu_allowed().then(|| std::thread::spawn(crate::gpu::zimage_warmup));
349
350 let t0 = Instant::now();
353 let _stage = crate::gpu::image_stage_scope();
354 let dit = match std::env::var("CMF_ZIMAGE_DIT_DIR") {
355 Ok(dir) => ZImageDit::load_dir(Path::new(&dir))?,
356 Err(_) => ZImageDit::from_cmf(&model)?,
357 };
358 tm.dit_load = t0.elapsed().as_secs_f64();
359 let ids = prompt_ids(&tok, prompt, p.max_tokens);
360 let neg_text = p
361 .negative_prompt
362 .clone()
363 .unwrap_or_else(|| defaults.negative_prompt.clone());
364 let neg_ids = do_cfg.then(|| prompt_ids(&tok, &neg_text, p.max_tokens));
365 let t0 = Instant::now();
366 let overlap = crate::zimage::gpu_allowed()
367 && std::env::var("CMF_ZIMAGE_OVERLAP").as_deref() != Ok("0")
368 && std::env::var("CMF_ZIMAGE_TE_GPU").as_deref() != Ok("1")
369 && std::env::var("CMF_ZIMAGE_TE_DEV").is_err();
370 let (te, preload) = std::thread::scope(|sc| {
371 let helper = overlap.then(|| {
372 sc.spawn(|| {
373 let t = Instant::now();
374 let ok = dit.preload_device();
375 (ok, t.elapsed().as_secs_f64())
376 })
377 });
378 let t = Instant::now();
379 let te = (|| -> Result<(Vec<f32>, Option<Vec<f32>>), String> {
380 let te_dev = std::env::var("CMF_ZIMAGE_TE_DEV").ok();
395 let _cpu = (std::env::var("CMF_ZIMAGE_TE_GPU").as_deref() != Ok("1") && te_dev.is_none())
396 .then(crate::gpu::pause_gpu);
397 let mut enc = crate::qwen3te::Qwen3Encoder::from_cmf(&model)?;
398 enc.set_exact_q8(std::env::var("CMF_ZIMAGE_TE_EXACT").as_deref() != Ok("0"));
402 if let Some(spec) = &te_dev {
403 enc.set_device_ops(spec);
404 }
405 let cap = enc.encode(&ids);
406 let ncap = neg_ids.as_ref().map(|n| enc.encode(n));
407 Ok((cap, ncap))
408 })();
409 let te_s = t.elapsed().as_secs_f64();
410 (te.map(|v| (v, te_s)), helper.map(|h| h.join().unwrap_or((false, 0.0))))
411 });
412 let ((cap, ncap), te_s) = te?;
413 if let Some(w) = warm {
414 let _ = w.join();
415 }
416 tm.text_encode = te_s;
417 let overlapped = t0.elapsed().as_secs_f64();
418 if prof_on() {
419 match preload {
420 Some((ok, s)) => eprintln!(
421 "zimage: text-encode {te_s:.3}s beside the plane upload {s:.3}s (ok {ok}) -> {overlapped:.3}s"
422 ),
423 None => eprintln!("zimage: text-encode {te_s:.3}s"),
424 }
425 }
426 let t0 = Instant::now();
427 let sig = crate::zimage::sigmas_torch_f32(p.steps, p.shift);
428 let t_models: Vec<f32> = sig[..p.steps]
429 .iter()
430 .map(|&s| crate::zimage::t_model(s))
431 .collect();
432 let mods = dit.mods_for_steps(&t_models);
433 let fscale = dit.final_scale_for_steps(&t_models);
434 let t_mods = t0.elapsed().as_secs_f64();
435 let per_mod = dit.cfg.n_mod_blocks() * 4 * dit.cfg.dim;
436 let dim = dit.cfg.dim;
437 static KEY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
438 let next_key = || KEY.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
439 let shape = ZShape::new(p.height, p.width, ids.len());
440 let cfg_at: Vec<bool> = (0..p.steps)
443 .map(|i| do_cfg && !(p.cfg_truncation <= 1.0 && t_models[i] > p.cfg_truncation))
444 .collect();
445 let dev = crate::zimage::gpu_allowed();
446 let mods_all = Some((&mods[..], &fscale[..]));
447 let mut prep = dit.prepare_host(&cap, shape, next_key(), dev)?;
448 let mut nprep = match (&ncap, &neg_ids) {
449 (Some(nc), Some(ni)) => Some(dit.prepare_host(
450 nc,
451 ZShape::new(p.height, p.width, ni.len()),
452 next_key(),
453 dev,
454 )?),
455 _ => None,
456 };
457 let t_caps = t0.elapsed().as_secs_f64();
458 let pair_key = next_key();
461 let pair_dev = match &nprep {
462 Some(np) if dev && cfg_at.iter().any(|&c| c) => {
463 dit.attach_device_pair(&prep, np, pair_key, mods_all)
464 }
465 _ => false,
466 };
467 let mut on_device = pair_dev;
468 if dev && (cfg_at.iter().any(|&c| !c) || (do_cfg && !pair_dev)) {
469 on_device |= dit.attach_device(&mut prep, mods_all);
470 }
471 if let Some(np) = nprep.as_mut() {
472 if dev && !pair_dev {
473 on_device |= dit.attach_device(np, mods_all);
474 }
475 }
476 if dev && !on_device {
477 eprintln!(
479 "zimage: the DiT runs on the CPU — expect minutes per image; \
480 CMF_ZIMAGE_PROF=1 prints the stages"
481 );
482 }
483 tm.prepare = t0.elapsed().as_secs_f64();
484 if prof_on() {
485 eprintln!(
486 "zimage prepare: mods {:.3}s · captions {:.3}s · device {:.3}s",
487 t_mods,
488 t_caps - t_mods,
489 tm.prepare - t_caps
490 );
491 }
492 let c = dit.cfg.in_channels;
493 let (lh, lw) = (shape.h_lat, shape.w_lat);
494 let pd = dit.geom().patch_dim;
495 let trace = std::env::var("CMF_ZIMAGE_TRACE").ok();
499 let vae_warm = {
503 let m = model.clone();
504 let dev = crate::zimage::gpu_allowed();
505 std::thread::spawn(move || -> Result<crate::vae::VaeDecoder, String> {
506 let vae = crate::vae::VaeDecoder::from_cmf(&m)?;
507 if dev && crate::gpu::enabled() {
508 crate::gpu::vae_prewarm(&vae.chain_args());
509 }
510 Ok(vae)
511 })
512 };
513 let mut latents: Vec<Vec<f32>> = Vec::with_capacity(p.num_images);
514 for img in 0..p.num_images {
515 let mut lat = gauss_latent(c * lh * lw, p.seed.wrapping_add(img as u64))?;
516 for i in 0..p.steps {
517 let ts = Instant::now();
518 let x_tok = crate::zimage::pad_rows_repeat_last(
519 &crate::zimage::patchify(&lat, c, lh, lw),
520 shape.n_img,
521 shape.n_img_p,
522 pd,
523 );
524 let m = &mods[i * per_mod..(i + 1) * per_mod];
525 let fs = &fscale[i * dim..(i + 1) * dim];
526 let apply_cfg = cfg_at[i];
527 let pair = if apply_cfg && pair_dev {
528 dit.step_pair_device(pair_key, shape.n_img, i, &x_tok, m, fs)
529 } else {
530 None
531 };
532 let (mut pred, neg) = match pair {
533 Some((a, b)) => (a, Some(b)),
534 None => (dit.step(&prep, i, &x_tok, m, fs), None),
535 };
536 if apply_cfg {
537 let neg = match neg {
538 Some(n) => n,
539 None => dit.step(nprep.as_ref().expect("negative prepared"), i, &x_tok, m, fs),
540 };
541 if let Some(d) = &trace {
542 trace_write(d, img, &format!("vpos_{i}"), &crate::zimage::unpatchify(&pred, c, lh, lw))?;
543 trace_write(d, img, &format!("vneg_{i}"), &crate::zimage::unpatchify(&neg, c, lh, lw))?;
544 }
545 cfg_combine(&mut pred, &neg, p.guidance, p.cfg_normalization);
546 }
547 let v = crate::zimage::unpatchify(&pred, c, lh, lw);
548 let dt = sig[i + 1] - sig[i];
549 for (x, &vv) in lat.iter_mut().zip(&v) {
550 *x += dt * (-vv);
551 }
552 if let Some(d) = &trace {
553 trace_write(d, img, &format!("v_{i}"), &v)?;
554 trace_write(d, img, &format!("lat_{}", i + 1), &lat)?;
555 }
556 tm.steps.push(ts.elapsed().as_secs_f64());
557 if prof_on() {
558 eprintln!(
559 "zimage: image {} step {}/{} {:.3}s{}",
560 img + 1,
561 i + 1,
562 p.steps,
563 ts.elapsed().as_secs_f64(),
564 if apply_cfg { " (cfg)" } else { "" }
565 );
566 }
567 progress(img, i + 1, p.steps);
568 }
569 if let Ok(path) = std::env::var("CMF_ZIMAGE_LATENT_OUT") {
570 let path = if p.num_images > 1 {
571 format!("{path}.{img}")
572 } else {
573 path
574 };
575 let b: Vec<u8> = lat.iter().flat_map(|v| v.to_le_bytes()).collect();
576 std::fs::write(&path, b).map_err(|e| format!("{path}: {e}"))?;
577 }
578 latents.push(lat);
579 }
580 drop(prep);
581 drop(nprep);
582 drop(dit);
583 crate::gpu::zimage_release_dit();
584 drop(_stage);
585
586 let t0 = Instant::now();
588 let mut out = Vec::with_capacity(latents.len());
589 {
590 let _stage = crate::gpu::image_stage_scope();
591 let vae = vae_warm.join().map_err(|_| "VAE loader panicked".to_string())??;
592 for (img, lat) in latents.iter().enumerate() {
593 if lat.iter().any(|v| !v.is_finite()) {
594 return Err(format!(
595 "image {img}: the final latent is not finite (an f16 overflow on the \
596 device path, or a corrupt file); rerun with CMF_ZIMAGE_GPU=0 to \
597 use the CPU DiT"
598 ));
599 }
600 let rgb = vae.decode_fast(lat, lh, lw);
601 let (h, w) = (p.height, p.width);
602 let plane = h * w;
603 let mut u8s = vec![0u8; plane * 3];
604 for px in 0..plane {
605 for ch in 0..3 {
606 let v = (rgb[ch * plane + px] / 2.0 + 0.5).clamp(0.0, 1.0);
607 u8s[px * 3 + ch] = (v * 255.0).round_ties_even() as u8;
609 }
610 }
611 out.push(ZImage {
612 rgb: u8s,
613 height: h,
614 width: w,
615 seed: p.seed.wrapping_add(img as u64),
616 });
617 }
618 }
619 tm.vae = t0.elapsed().as_secs_f64();
620 tm.total = t_all.elapsed().as_secs_f64();
621 if crate::zimage::gpu_allowed() {
622 crate::gpu::zimage_flush_pipelines();
623 }
624 if prof_on() {
625 eprintln!(
626 "zimage stages: text-encode {:.2}s · dit-load {:.2}s · prepare {:.2}s · steps {:.2}s (median {:.3}s, {} forwards/step) · vae {:.2}s · total {:.2}s",
627 tm.text_encode,
628 tm.dit_load,
629 tm.prepare,
630 tm.steps.iter().sum::<f64>(),
631 tm.median_step(),
632 if do_cfg { 2 } else { 1 },
633 tm.vae,
634 tm.total
635 );
636 }
637 Ok((out, tm))
638}