1use std::path::Path;
18use std::time::{Duration, Instant};
19
20use crate::pipeline::{
21 decoded_chw_to_rgb8, latent_seq_to_packed_nchw, PipelineError, TeSource, TextToImageOut,
22};
23use crate::png::encode_rgb8;
24use crate::sample;
25use crate::te::{Qwen3Tokenizer, TeWeights, TextEncoder};
26use crate::vae::{VaeDecoder, VaeWeights};
27use crate::{DitForward, DitWeights};
28
29const SEQ_TXT: usize = 512;
31
32#[derive(Debug, Clone)]
36pub struct RenderParams {
37 pub prompt: String,
39 pub seed: u64,
41 pub steps: usize,
43 pub width: usize,
45 pub height: usize,
47 pub guidance: f32,
49}
50
51impl Default for RenderParams {
52 fn default() -> Self {
53 Self {
54 prompt: String::new(),
55 seed: 42,
56 steps: 4,
57 width: 512,
58 height: 512,
59 guidance: 1.0,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy)]
67pub struct StageTimings {
68 pub te_encode: Duration,
70 pub dit_sample: Duration,
72 pub vae_decode: Duration,
74 pub png_encode: Duration,
76 pub total: Duration,
78}
79
80pub struct RenderOutcome {
82 pub image: TextToImageOut,
84 pub timings: StageTimings,
86}
87
88pub struct ImageSession {
91 dit_weights: DitWeights,
92 vae: VaeDecoder,
93 te_weights: TeWeights,
94 tokenizer: Qwen3Tokenizer,
95 in_channels: usize,
96 joint_dim: usize,
97}
98
99impl ImageSession {
100 pub fn load(
109 dit_gguf: &Path,
110 vae_weights: &Path,
111 te_source: &TeSource,
112 tokenizer_dir: &Path,
113 ) -> Result<Self, PipelineError> {
114 let dit_weights = DitWeights::open(dit_gguf)?;
115 let dcfg = dit_weights.config();
116 let in_channels = dcfg.in_channels as usize;
117 let joint_dim = dcfg.joint_attention_dim as usize;
118
119 let vae_loaded = VaeWeights::open(vae_weights)?;
121 let vae = VaeDecoder::from_weights(&vae_loaded)?;
122 drop(vae_loaded);
123
124 let te_weights = match te_source {
125 TeSource::Mlx4bit(p) => TeWeights::open_mlx_4bit(p)?,
126 TeSource::NpyDir(d) => TeWeights::open(d)?,
127 };
128 te_weights.set_resident(true);
129
130 let tokenizer = Qwen3Tokenizer::open(tokenizer_dir)?;
131
132 Ok(Self {
133 dit_weights,
134 vae,
135 te_weights,
136 tokenizer,
137 in_channels,
138 joint_dim,
139 })
140 }
141
142 pub fn warm(&self) -> Result<Duration, PipelineError> {
152 let t = Instant::now();
153 let _ = self.encode("warmup")?;
154 Ok(t.elapsed())
155 }
156
157 fn encode(&self, prompt: &str) -> Result<Vec<f32>, PipelineError> {
160 let toks = self.tokenizer.tokenize(prompt, SEQ_TXT)?;
161 let encoder = TextEncoder::new(&self.te_weights);
162 let out = encoder.forward(&toks.input_ids, &toks.attention_mask)?;
163 let cond = out.cond_7680()?;
164 let need = SEQ_TXT * self.joint_dim;
165 if cond.len() != need {
166 return Err(PipelineError::Shape(format!(
167 "TE cond len {} != SEQ_TXT*joint_dim ({SEQ_TXT}*{})",
168 cond.len(),
169 self.joint_dim
170 )));
171 }
172 Ok(cond)
173 }
174
175 pub fn render(&self, params: &RenderParams) -> Result<RenderOutcome, PipelineError> {
180 let t_total = Instant::now();
181
182 let (lat_h, lat_w) = sample::latent_grid(params.height, params.width);
183 let seq_img = lat_h * lat_w;
184
185 let t_te = Instant::now();
187 let cond_vec = self.encode(¶ms.prompt)?;
188 let te_encode = t_te.elapsed();
189
190 let t_dit = Instant::now();
192 let fwd = DitForward::new(&self.dit_weights);
193 let init = sample::create_noise(params.seed, params.height, params.width);
194 if init.len() != seq_img * self.in_channels {
195 return Err(PipelineError::Shape(format!(
196 "native noise len {} != {seq_img}*{} (in_channels vs PACKED_CHANNELS {})",
197 init.len(),
198 self.in_channels,
199 sample::PACKED_CHANNELS
200 )));
201 }
202 let img_ids = sample::img_ids(lat_h, lat_w);
203 let txt_ids = sample::txt_ids(SEQ_TXT);
204 let (timesteps, sigmas) = sample::flow_match_schedule(seq_img, params.steps);
205 let latent = fwd.sample(
206 &init, &cond_vec, &img_ids, &txt_ids, seq_img, SEQ_TXT, ×teps, &sigmas, None,
207 )?;
208 let dit_sample = t_dit.elapsed();
209
210 let t_vae = Instant::now();
212 let packed = latent_seq_to_packed_nchw(&latent, seq_img, self.in_channels, lat_h, lat_w)?;
213 let decoded = self
214 .vae
215 .decode_packed_latents(&packed, lat_h, lat_w, None)?;
216 let vae_decode = t_vae.elapsed();
217
218 let t_png = Instant::now();
220 let (w, h, rgb) = decoded_chw_to_rgb8(decoded.c, decoded.h, decoded.w, &decoded.data)?;
221 let png = encode_rgb8(w, h, &rgb)?;
222 let png_encode = t_png.elapsed();
223
224 Ok(RenderOutcome {
225 image: TextToImageOut {
226 png,
227 width: w,
228 height: h,
229 stage_cosines: Vec::new(),
230 },
231 timings: StageTimings {
232 te_encode,
233 dit_sample,
234 vae_decode,
235 png_encode,
236 total: t_total.elapsed(),
237 },
238 })
239 }
240}