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.steps == 0 {
317 return Err("steps must be at least 1".into());
318 }
319 let t_all = Instant::now();
320 let mut tm = ZTimings::default();
321 let model = Arc::new(
322 cortiq_core::CmfModel::open(model_path)
323 .map_err(|e| format!("{}: {e}", model_path.display()))?,
324 );
325 if model.header.arch.arch_name != ARCH_NAME {
326 return Err(format!(
327 "{}: architecture '{}' is not {ARCH_NAME}",
328 model_path.display(),
329 model.header.arch.arch_name
330 ));
331 }
332 let vocab = model
333 .vocab
334 .as_deref()
335 .ok_or("Z-Image .cmf has no embedded tokenizer")?;
336 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
337 let defaults = ZDefaults::of(&model);
338 let do_cfg = p.guidance > 0.0;
339 let warm = crate::zimage::gpu_allowed().then(|| std::thread::spawn(crate::gpu::zimage_warmup));
341
342 let t0 = Instant::now();
345 let _stage = crate::gpu::image_stage_scope();
346 let dit = match std::env::var("CMF_ZIMAGE_DIT_DIR") {
347 Ok(dir) => ZImageDit::load_dir(Path::new(&dir))?,
348 Err(_) => ZImageDit::from_cmf(&model)?,
349 };
350 tm.dit_load = t0.elapsed().as_secs_f64();
351 let ids = prompt_ids(&tok, prompt, p.max_tokens);
352 let neg_text = p
353 .negative_prompt
354 .clone()
355 .unwrap_or_else(|| defaults.negative_prompt.clone());
356 let neg_ids = do_cfg.then(|| prompt_ids(&tok, &neg_text, p.max_tokens));
357 let t0 = Instant::now();
358 let overlap = crate::zimage::gpu_allowed()
359 && std::env::var("CMF_ZIMAGE_OVERLAP").as_deref() != Ok("0")
360 && std::env::var("CMF_ZIMAGE_TE_GPU").as_deref() != Ok("1");
361 let (te, preload) = std::thread::scope(|sc| {
362 let helper = overlap.then(|| {
363 sc.spawn(|| {
364 let t = Instant::now();
365 let ok = dit.preload_device();
366 (ok, t.elapsed().as_secs_f64())
367 })
368 });
369 let t = Instant::now();
370 let te = (|| -> Result<(Vec<f32>, Option<Vec<f32>>), String> {
371 let _cpu = (std::env::var("CMF_ZIMAGE_TE_GPU").as_deref() != Ok("1"))
379 .then(crate::gpu::pause_gpu);
380 let mut enc = crate::qwen3te::Qwen3Encoder::from_cmf(&model)?;
381 enc.set_exact_q8(std::env::var("CMF_ZIMAGE_TE_EXACT").as_deref() != Ok("0"));
385 let cap = enc.encode(&ids);
386 let ncap = neg_ids.as_ref().map(|n| enc.encode(n));
387 Ok((cap, ncap))
388 })();
389 let te_s = t.elapsed().as_secs_f64();
390 (te.map(|v| (v, te_s)), helper.map(|h| h.join().unwrap_or((false, 0.0))))
391 });
392 let ((cap, ncap), te_s) = te?;
393 if let Some(w) = warm {
394 let _ = w.join();
395 }
396 tm.text_encode = te_s;
397 let overlapped = t0.elapsed().as_secs_f64();
398 if prof_on() {
399 match preload {
400 Some((ok, s)) => eprintln!(
401 "zimage: text-encode {te_s:.3}s beside the plane upload {s:.3}s (ok {ok}) -> {overlapped:.3}s"
402 ),
403 None => eprintln!("zimage: text-encode {te_s:.3}s"),
404 }
405 }
406 let t0 = Instant::now();
407 let sig = crate::zimage::sigmas_torch_f32(p.steps, p.shift);
408 let t_models: Vec<f32> = sig[..p.steps]
409 .iter()
410 .map(|&s| crate::zimage::t_model(s))
411 .collect();
412 let mods = dit.mods_for_steps(&t_models);
413 let fscale = dit.final_scale_for_steps(&t_models);
414 let t_mods = t0.elapsed().as_secs_f64();
415 let per_mod = dit.cfg.n_mod_blocks() * 4 * dit.cfg.dim;
416 let dim = dit.cfg.dim;
417 static KEY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
418 let next_key = || KEY.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
419 let shape = ZShape::new(p.height, p.width, ids.len());
420 let cfg_at: Vec<bool> = (0..p.steps)
423 .map(|i| do_cfg && !(p.cfg_truncation <= 1.0 && t_models[i] > p.cfg_truncation))
424 .collect();
425 let dev = crate::zimage::gpu_allowed();
426 let mods_all = Some((&mods[..], &fscale[..]));
427 let mut prep = dit.prepare_host(&cap, shape, next_key(), dev)?;
428 let mut nprep = match (&ncap, &neg_ids) {
429 (Some(nc), Some(ni)) => Some(dit.prepare_host(
430 nc,
431 ZShape::new(p.height, p.width, ni.len()),
432 next_key(),
433 dev,
434 )?),
435 _ => None,
436 };
437 let t_caps = t0.elapsed().as_secs_f64();
438 let pair_key = next_key();
441 let pair_dev = match &nprep {
442 Some(np) if dev && cfg_at.iter().any(|&c| c) => {
443 dit.attach_device_pair(&prep, np, pair_key, mods_all)
444 }
445 _ => false,
446 };
447 let mut on_device = pair_dev;
448 if dev && (cfg_at.iter().any(|&c| !c) || (do_cfg && !pair_dev)) {
449 on_device |= dit.attach_device(&mut prep, mods_all);
450 }
451 if let Some(np) = nprep.as_mut() {
452 if dev && !pair_dev {
453 on_device |= dit.attach_device(np, mods_all);
454 }
455 }
456 if dev && !on_device {
457 eprintln!(
459 "zimage: the DiT runs on the CPU — expect minutes per image; \
460 CMF_ZIMAGE_PROF=1 prints the stages"
461 );
462 }
463 tm.prepare = t0.elapsed().as_secs_f64();
464 if prof_on() {
465 eprintln!(
466 "zimage prepare: mods {:.3}s · captions {:.3}s · device {:.3}s",
467 t_mods,
468 t_caps - t_mods,
469 tm.prepare - t_caps
470 );
471 }
472 let c = dit.cfg.in_channels;
473 let (lh, lw) = (shape.h_lat, shape.w_lat);
474 let pd = dit.geom().patch_dim;
475 let trace = std::env::var("CMF_ZIMAGE_TRACE").ok();
479 let vae_warm = {
483 let m = model.clone();
484 let dev = crate::zimage::gpu_allowed();
485 std::thread::spawn(move || -> Result<crate::vae::VaeDecoder, String> {
486 let vae = crate::vae::VaeDecoder::from_cmf(&m)?;
487 if dev && crate::gpu::enabled() {
488 crate::gpu::vae_prewarm(&vae.chain_args());
489 }
490 Ok(vae)
491 })
492 };
493 let mut latents: Vec<Vec<f32>> = Vec::with_capacity(p.num_images);
494 for img in 0..p.num_images {
495 let mut lat = gauss_latent(c * lh * lw, p.seed.wrapping_add(img as u64))?;
496 for i in 0..p.steps {
497 let ts = Instant::now();
498 let x_tok = crate::zimage::pad_rows_repeat_last(
499 &crate::zimage::patchify(&lat, c, lh, lw),
500 shape.n_img,
501 shape.n_img_p,
502 pd,
503 );
504 let m = &mods[i * per_mod..(i + 1) * per_mod];
505 let fs = &fscale[i * dim..(i + 1) * dim];
506 let apply_cfg = cfg_at[i];
507 let pair = if apply_cfg && pair_dev {
508 dit.step_pair_device(pair_key, shape.n_img, i, &x_tok, m, fs)
509 } else {
510 None
511 };
512 let (mut pred, neg) = match pair {
513 Some((a, b)) => (a, Some(b)),
514 None => (dit.step(&prep, i, &x_tok, m, fs), None),
515 };
516 if apply_cfg {
517 let neg = match neg {
518 Some(n) => n,
519 None => dit.step(nprep.as_ref().expect("negative prepared"), i, &x_tok, m, fs),
520 };
521 if let Some(d) = &trace {
522 trace_write(d, img, &format!("vpos_{i}"), &crate::zimage::unpatchify(&pred, c, lh, lw))?;
523 trace_write(d, img, &format!("vneg_{i}"), &crate::zimage::unpatchify(&neg, c, lh, lw))?;
524 }
525 cfg_combine(&mut pred, &neg, p.guidance, p.cfg_normalization);
526 }
527 let v = crate::zimage::unpatchify(&pred, c, lh, lw);
528 let dt = sig[i + 1] - sig[i];
529 for (x, &vv) in lat.iter_mut().zip(&v) {
530 *x += dt * (-vv);
531 }
532 if let Some(d) = &trace {
533 trace_write(d, img, &format!("v_{i}"), &v)?;
534 trace_write(d, img, &format!("lat_{}", i + 1), &lat)?;
535 }
536 tm.steps.push(ts.elapsed().as_secs_f64());
537 if prof_on() {
538 eprintln!(
539 "zimage: image {} step {}/{} {:.3}s{}",
540 img + 1,
541 i + 1,
542 p.steps,
543 ts.elapsed().as_secs_f64(),
544 if apply_cfg { " (cfg)" } else { "" }
545 );
546 }
547 progress(img, i + 1, p.steps);
548 }
549 if let Ok(path) = std::env::var("CMF_ZIMAGE_LATENT_OUT") {
550 let path = if p.num_images > 1 {
551 format!("{path}.{img}")
552 } else {
553 path
554 };
555 let b: Vec<u8> = lat.iter().flat_map(|v| v.to_le_bytes()).collect();
556 std::fs::write(&path, b).map_err(|e| format!("{path}: {e}"))?;
557 }
558 latents.push(lat);
559 }
560 drop(prep);
561 drop(nprep);
562 drop(dit);
563 crate::gpu::zimage_release_dit();
564 drop(_stage);
565
566 let t0 = Instant::now();
568 let mut out = Vec::with_capacity(latents.len());
569 {
570 let _stage = crate::gpu::image_stage_scope();
571 let vae = vae_warm.join().map_err(|_| "VAE loader panicked".to_string())??;
572 for (img, lat) in latents.iter().enumerate() {
573 if lat.iter().any(|v| !v.is_finite()) {
574 return Err(format!(
575 "image {img}: the final latent is not finite (an f16 overflow on the \
576 device path, or a corrupt file); rerun with CMF_ZIMAGE_GPU=0 to \
577 use the CPU DiT"
578 ));
579 }
580 let rgb = vae.decode_fast(lat, lh, lw);
581 let (h, w) = (p.height, p.width);
582 let plane = h * w;
583 let mut u8s = vec![0u8; plane * 3];
584 for px in 0..plane {
585 for ch in 0..3 {
586 let v = (rgb[ch * plane + px] / 2.0 + 0.5).clamp(0.0, 1.0);
587 u8s[px * 3 + ch] = (v * 255.0).round_ties_even() as u8;
589 }
590 }
591 out.push(ZImage {
592 rgb: u8s,
593 height: h,
594 width: w,
595 seed: p.seed.wrapping_add(img as u64),
596 });
597 }
598 }
599 tm.vae = t0.elapsed().as_secs_f64();
600 tm.total = t_all.elapsed().as_secs_f64();
601 if crate::zimage::gpu_allowed() {
602 crate::gpu::zimage_flush_pipelines();
603 }
604 if prof_on() {
605 eprintln!(
606 "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",
607 tm.text_encode,
608 tm.dit_load,
609 tm.prepare,
610 tm.steps.iter().sum::<f64>(),
611 tm.median_step(),
612 if do_cfg { 2 } else { 1 },
613 tm.vae,
614 tm.total
615 );
616 }
617 Ok((out, tm))
618}