1use crate::pool::Pool;
28use cortiq_core::CmfModel;
29use std::sync::Arc;
30
31const DILATION_CYCLE: [u32; 3] = [1, 2, 4];
33const EPS: f32 = 1e-6;
34
35pub struct PvConv {
38 pub w: Vec<f32>,
39 pub b: Vec<f32>,
40 pub cin: usize,
41 pub cout: usize,
42}
43
44#[derive(Default)]
46pub struct PvWeights {
47 pub convs: Vec<PvConv>,
48 pub gammas: Vec<Vec<f32>>,
49}
50
51#[derive(Clone, Copy, Debug)]
54pub enum PvStep {
55 Conv { wi: usize, src: u8, dst: u8, res: Option<u8>, dil: u32, h: usize, w: usize },
57 NormSilu { gi: usize, src: u8, dst: u8, c: usize, h: usize, w: usize },
59 Unshuffle { src: u8, dst: u8, c: usize, h: usize, w: usize },
61 Shuffle { src: u8, dst: u8, add: Option<u8>, c: usize, h: usize, w: usize },
63}
64
65pub struct PvPlan {
67 pub steps: Vec<PvStep>,
68 pub buf_elems: Vec<usize>,
69 pub input: u8,
70 pub output: u8,
71 pub out_elems: usize,
72}
73
74struct ResBlock {
75 norm1: usize,
76 conv1: usize,
77 dil: u32,
78 norm2: usize,
79 conv2: usize,
80}
81
82struct UStack {
84 widths: Vec<usize>,
85 conv_in: usize,
86 down_blocks: Vec<Vec<ResBlock>>,
87 downs: Vec<usize>,
88 ups: Vec<usize>,
89 up_blocks: Vec<Vec<ResBlock>>,
90 norm_out: usize,
91 conv_out: usize,
92}
93
94pub struct PrimalVae {
96 pub compression: usize,
97 pub latent_channels: usize,
98 weights: PvWeights,
99 encoder: UStack,
100 decoder: UStack,
101 key: u64,
103 pool: Option<Arc<Pool>>,
104}
105
106fn load_f32(model: &Arc<CmfModel>, name: &str) -> Result<Vec<f32>, String> {
107 crate::dit::cmf_f32(model, name)
108}
109
110fn shape(model: &CmfModel, name: &str) -> Option<Vec<usize>> {
111 model.tensor(name).map(|t| t.shape.clone())
112}
113
114fn push_conv(model: &Arc<CmfModel>, name: &str, cin_cout: Option<usize>, wts: &mut PvWeights) -> Result<usize, String> {
115 let s = shape(model, &format!("{name}.weight")).ok_or_else(|| format!("primal_vae: missing {name}.weight"))?;
116 if s.len() != 4 || s[2] != 3 || s[3] != 3 {
117 return Err(format!("primal_vae: {name} is not a 3×3 conv ({s:?})"));
118 }
119 if cin_cout.is_some_and(|c| s[0] != c || s[1] != c) {
120 return Err(format!("primal_vae: {name} is not a square conv of the level width"));
121 }
122 let w = load_f32(model, &format!("{name}.weight"))?;
123 let b = load_f32(model, &format!("{name}.bias"))?;
124 if b.len() != s[0] || w.len() != s[0] * s[1] * 9 {
125 return Err(format!("primal_vae: {name} shapes disagree"));
126 }
127 wts.convs.push(PvConv { w, b, cin: s[1], cout: s[0] });
128 Ok(wts.convs.len() - 1)
129}
130
131fn push_gamma(model: &Arc<CmfModel>, name: &str, c: usize, wts: &mut PvWeights) -> Result<usize, String> {
132 let g = load_f32(model, &format!("{name}.weight"))?;
133 if g.len() != c {
134 return Err(format!("primal_vae: {name} has {} channels, expected {c}", g.len()));
135 }
136 wts.gammas.push(g);
137 Ok(wts.gammas.len() - 1)
138}
139
140fn load_blocks(model: &Arc<CmfModel>, pre: &str, c: usize, wts: &mut PvWeights) -> Result<Vec<ResBlock>, String> {
141 let mut out = Vec::new();
142 for j in 0.. {
143 let p = format!("{pre}.{j}");
144 if model.tensor(&format!("{p}.conv1.weight")).is_none() {
145 break;
146 }
147 out.push(ResBlock {
148 norm1: push_gamma(model, &format!("{p}.norm1"), c, wts)?,
149 conv1: push_conv(model, &format!("{p}.conv1"), Some(c), wts)?,
150 dil: DILATION_CYCLE[j % DILATION_CYCLE.len()],
151 norm2: push_gamma(model, &format!("{p}.norm2"), c, wts)?,
152 conv2: push_conv(model, &format!("{p}.conv2"), Some(c), wts)?,
153 });
154 }
155 Ok(out)
156}
157
158impl UStack {
159 fn load(model: &Arc<CmfModel>, pre: &str, wts: &mut PvWeights) -> Result<Self, String> {
160 let conv_in = push_conv(model, &format!("{pre}.conv_in"), None, wts)?;
161 let mut widths = vec![wts.convs[conv_in].cout];
162 let mut downs = Vec::new();
163 while model.tensor(&format!("{pre}.downs.{}.weight", widths.len() - 1)).is_some() {
164 let i = widths.len() - 1;
165 let d = push_conv(model, &format!("{pre}.downs.{i}"), None, wts)?;
166 if wts.convs[d].cin != 4 * widths[i] {
167 return Err(format!("primal_vae: {pre}.downs.{i} does not take the unshuffled level"));
168 }
169 widths.push(wts.convs[d].cout);
170 downs.push(d);
171 }
172 let levels = widths.len();
173 let mut ups = Vec::new();
174 for i in 0..levels - 1 {
175 let u = push_conv(model, &format!("{pre}.ups.{i}"), None, wts)?;
176 if wts.convs[u].cin != widths[i + 1] || wts.convs[u].cout != 4 * widths[i] {
177 return Err(format!("primal_vae: {pre}.ups.{i} does not feed the shuffled level"));
178 }
179 ups.push(u);
180 }
181 let mut down_blocks = Vec::new();
182 for (i, &c) in widths.iter().enumerate() {
183 down_blocks.push(load_blocks(model, &format!("{pre}.down_blocks.{i}"), c, wts)?);
184 }
185 let mut up_blocks = Vec::new();
186 for (i, &c) in widths.iter().take(levels - 1).enumerate() {
187 up_blocks.push(load_blocks(model, &format!("{pre}.up_blocks.{i}"), c, wts)?);
188 }
189 let norm_out = push_gamma(model, &format!("{pre}.norm_out"), widths[0], wts)?;
190 let conv_out = push_conv(model, &format!("{pre}.conv_out"), None, wts)?;
191 Ok(UStack { widths, conv_in, down_blocks, downs, ups, up_blocks, norm_out, conv_out })
192 }
193
194 fn plan(&self, wts: &PvWeights, steps: &mut Vec<PvStep>, sizes: &mut Vec<usize>, src: u8, dst: u8, h: usize, w: usize) {
199 let need = |sizes: &mut Vec<usize>, b: u8, n: usize| {
200 if sizes.len() <= b as usize {
201 sizes.resize(b as usize + 1, 0);
202 }
203 sizes[b as usize] = sizes[b as usize].max(n);
204 };
205 let levels = self.widths.len();
206 let xb = |i: usize| if i == levels - 1 { 0u8 } else { 3 + i as u8 };
207 let (t, hb) = (1u8, 2u8);
208 let block = |steps: &mut Vec<PvStep>, sizes: &mut Vec<usize>, x: u8, b: &ResBlock, c: usize, h: usize, w: usize| {
209 need(sizes, t, c * h * w);
210 need(sizes, hb, c * h * w);
211 steps.push(PvStep::NormSilu { gi: b.norm1, src: x, dst: t, c, h, w });
212 steps.push(PvStep::Conv { wi: b.conv1, src: t, dst: hb, res: None, dil: b.dil, h, w });
213 steps.push(PvStep::NormSilu { gi: b.norm2, src: hb, dst: t, c, h, w });
214 steps.push(PvStep::Conv { wi: b.conv2, src: t, dst: x, res: Some(x), dil: 1, h, w });
215 };
216 let (mut h, mut w) = (h, w);
217 steps.push(PvStep::Conv { wi: self.conv_in, src, dst: xb(0), res: None, dil: 1, h, w });
218 need(sizes, xb(0), self.widths[0] * h * w);
219 for i in 0..levels {
220 for b in &self.down_blocks[i] {
221 block(steps, sizes, xb(i), b, self.widths[i], h, w);
222 }
223 if i < levels - 1 {
224 let c = self.widths[i];
225 steps.push(PvStep::Unshuffle { src: xb(i), dst: t, c, h, w });
226 need(sizes, t, c * h * w);
227 steps.push(PvStep::Conv { wi: self.downs[i], src: t, dst: xb(i + 1), res: None, dil: 1, h: h / 2, w: w / 2 });
228 h /= 2;
229 w /= 2;
230 need(sizes, xb(i + 1), self.widths[i + 1] * h * w);
231 }
232 }
233 for i in (0..levels - 1).rev() {
234 let up = &wts.convs[self.ups[i]];
235 steps.push(PvStep::Conv { wi: self.ups[i], src: xb(i + 1), dst: t, res: None, dil: 1, h, w });
236 need(sizes, t, up.cout * h * w);
237 steps.push(PvStep::Shuffle { src: t, dst: xb(i), add: Some(xb(i)), c: up.cout, h, w });
238 h *= 2;
239 w *= 2;
240 for b in &self.up_blocks[i] {
241 block(steps, sizes, xb(i), b, self.widths[i], h, w);
242 }
243 }
244 steps.push(PvStep::NormSilu { gi: self.norm_out, src: xb(0), dst: t, c: self.widths[0], h, w });
245 steps.push(PvStep::Conv { wi: self.conv_out, src: t, dst, res: None, dil: 1, h, w });
246 need(sizes, dst, wts.convs[self.conv_out].cout * h * w);
247 }
248}
249
250static NEXT_KEY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
251
252pub fn is_primal_vae(model: &CmfModel) -> bool {
254 model.header.arch.arch_name == "primal_vae"
255}
256
257impl PrimalVae {
258 pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
259 if !is_primal_vae(model) {
260 return Err("not a Primal VAE file (arch_name primal_vae)".into());
261 }
262 let mut weights = PvWeights::default();
263 let encoder = UStack::load(model, "encoder", &mut weights)?;
264 let decoder = UStack::load(model, "decoder", &mut weights)?;
265 let cin = weights.convs[encoder.conv_in].cin;
266 let compression = ((cin / 3) as f64).sqrt().round() as usize;
267 if compression * compression * 3 != cin || compression == 0 {
268 return Err(format!("primal_vae: conv_in takes {cin} channels, not 3·c²"));
269 }
270 let latent_channels = weights.convs[encoder.conv_out].cout;
271 if weights.convs[decoder.conv_in].cin != latent_channels || weights.convs[decoder.conv_out].cout != cin {
272 return Err("primal_vae: encoder and decoder disagree on the latent or pixel channels".into());
273 }
274 Ok(PrimalVae {
275 compression,
276 latent_channels,
277 weights,
278 encoder,
279 decoder,
280 key: NEXT_KEY.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
281 pool,
282 })
283 }
284
285 pub fn image_multiple(&self) -> usize {
287 self.compression * (1usize << (self.encoder.widths.len() - 1)).max(4)
288 }
289
290 fn plan(&self, encode: bool, h: usize, w: usize) -> PvPlan {
291 let mut steps = Vec::new();
292 let mut sizes = Vec::new();
293 let c2 = self.compression;
294 let (lh, lw) = (h / c2, w / c2);
295 let levels = self.encoder.widths.len();
297 let (inp, mid, out) = (3 + levels as u8, 4 + levels as u8, 5 + levels as u8);
298 if encode {
299 steps.push(PvStep::Unshuffle { src: inp, dst: mid, c: 3, h, w });
301 if c2 != 2 {
302 unreachable!("released models compress by 2");
303 }
304 self.encoder.plan(&self.weights, &mut steps, &mut sizes, mid, out, lh, lw);
305 let out_elems = self.latent_channels * lh * lw;
306 sizes.resize(sizes.len().max(out as usize + 1), 0);
307 sizes[inp as usize] = 3 * h * w;
308 sizes[mid as usize] = sizes[mid as usize].max(3 * h * w);
309 sizes[out as usize] = sizes[out as usize].max(out_elems);
310 PvPlan { steps, buf_elems: sizes, input: inp, output: out, out_elems }
311 } else {
312 self.decoder.plan(&self.weights, &mut steps, &mut sizes, inp, mid, lh, lw);
313 steps.push(PvStep::Shuffle { src: mid, dst: out, add: None, c: 3 * c2 * c2, h: lh, w: lw });
314 sizes.resize(sizes.len().max(out as usize + 1), 0);
315 sizes[inp as usize] = self.latent_channels * lh * lw;
316 sizes[out as usize] = 3 * h * w;
317 PvPlan { steps, buf_elems: sizes, input: inp, output: out, out_elems: 3 * h * w }
318 }
319 }
320
321 fn run(&self, plan: &PvPlan, input: &[f32]) -> Result<Vec<f32>, String> {
323 let mut out = vec![0f32; plan.out_elems];
324 if device_on() && crate::gpu::primal_vae_run(self.key, &self.weights, plan, input, &mut out) {
325 return Ok(out);
326 }
327 let mut bufs: Vec<Vec<f32>> = plan.buf_elems.iter().map(|&n| vec![0f32; n]).collect();
328 bufs[plan.input as usize][..input.len()].copy_from_slice(input);
329 for s in &plan.steps {
330 host_step(&self.weights, s, &mut bufs, self.pool.as_deref());
331 }
332 out.copy_from_slice(&bufs[plan.output as usize][..plan.out_elems]);
333 Ok(out)
334 }
335
336 fn check(&self, h: usize, w: usize) -> Result<(), String> {
337 let m = self.image_multiple();
338 if h == 0 || w == 0 || h % m != 0 || w % m != 0 {
339 return Err(format!("image sides must be multiples of {m}, got {w}x{h}"));
340 }
341 Ok(())
342 }
343
344 pub fn encode(&self, img: &[f32], h: usize, w: usize) -> Result<Vec<f32>, String> {
346 self.check(h, w)?;
347 if img.len() != 3 * h * w {
348 return Err("primal_vae: image is not [3, h, w]".into());
349 }
350 self.run(&self.plan(true, h, w), img)
351 }
352
353 pub fn decode(&self, z: &[f32], h: usize, w: usize) -> Result<Vec<f32>, String> {
356 self.check(h, w)?;
357 let c = self.compression;
358 if z.len() != self.latent_channels * (h / c) * (w / c) {
359 return Err("primal_vae: latent does not match the image size".into());
360 }
361 self.run(&self.plan(false, h, w), z)
362 }
363}
364
365fn device_on() -> bool {
367 std::env::var("CMF_PRIMAL_VAE_GPU").as_deref() != Ok("0") && crate::gpu::enabled_here()
368}
369
370const BAND_PIXELS: usize = 8192;
372
373fn host_conv(cv: &PvConv, src: &[f32], dst: &mut [f32], res: Option<&[f32]>, dil: usize, h: usize, w: usize, pool: Option<&Pool>) {
374 let (cin, cout) = (cv.cin, cv.cout);
375 let hw = h * w;
376 let k = cin * 9;
377 let mut panel = vec![0f32; BAND_PIXELS.min(hw) * k];
378 let mut yb = vec![0f32; BAND_PIXELS.min(hw) * cout];
379 let mut p0 = 0usize;
380 while p0 < hw {
381 let np = BAND_PIXELS.min(hw - p0);
382 let pp = crate::pool::SendMut::new(panel.as_mut_ptr());
383 let fill = |lo: usize, hi: usize| {
384 for i in lo..hi {
385 let p = p0 + i;
386 let (y, x) = ((p / w) as isize, (p % w) as isize);
387 let row = unsafe { std::slice::from_raw_parts_mut(pp.at(i * k), k) };
389 for ci in 0..cin {
390 let plane = &src[ci * hw..(ci + 1) * hw];
391 for tap in 0..9 {
392 let sy = y + (tap / 3) as isize * dil as isize - dil as isize;
393 let sx = x + (tap % 3) as isize * dil as isize - dil as isize;
394 row[ci * 9 + tap] = if sy < 0 || sx < 0 || sy >= h as isize || sx >= w as isize {
395 0.0
396 } else {
397 plane[sy as usize * w + sx as usize]
398 };
399 }
400 }
401 }
402 };
403 match pool {
404 Some(p) => p.run_rows(np, &fill),
405 None => fill(0, np),
406 }
407 crate::fcd_ops::gemm_nt_host(&panel[..np * k], &cv.w, &mut yb[..np * cout], np, k, cout, pool);
408 for i in 0..np {
409 let p = p0 + i;
410 for o in 0..cout {
411 let r = res.map_or(0.0, |r| r[o * hw + p]);
412 dst[o * hw + p] = yb[i * cout + o] + cv.b[o] + r;
413 }
414 }
415 p0 += np;
416 }
417}
418
419fn host_step(wts: &PvWeights, s: &PvStep, bufs: &mut [Vec<f32>], pool: Option<&Pool>) {
420 match *s {
423 PvStep::Conv { wi, src, dst, res, dil, h, w } => {
424 let cv = &wts.convs[wi];
425 let srcv = std::mem::take(&mut bufs[src as usize]);
426 let resv = res.map(|r| if r == src { srcv.clone() } else { bufs[r as usize].clone() });
427 let mut d = std::mem::take(&mut bufs[dst as usize]);
428 if src == dst {
429 d = vec![0f32; srcv.len().max(cv.cout * h * w)];
430 }
431 host_conv(cv, &srcv, &mut d, resv.as_deref(), dil as usize, h, w, pool);
432 if src != dst {
433 bufs[src as usize] = srcv;
434 }
435 bufs[dst as usize] = d;
436 }
437 PvStep::NormSilu { gi, src, dst, c, h, w } => {
438 let g = &wts.gammas[gi];
439 let hw = h * w;
440 let srcv = std::mem::take(&mut bufs[src as usize]);
441 let d = &mut bufs[dst as usize];
442 for p in 0..hw {
443 let mut ss = 0f32;
444 for ch in 0..c {
445 let v = srcv[ch * hw + p];
446 ss += v * v;
447 }
448 let inv = 1.0 / (ss / c as f32 + EPS).sqrt();
449 for ch in 0..c {
450 let v = srcv[ch * hw + p] * inv * g[ch];
451 d[ch * hw + p] = v / (1.0 + (-v).exp());
452 }
453 }
454 bufs[src as usize] = srcv;
455 }
456 PvStep::Unshuffle { src, dst, c, h, w } => {
457 let srcv = std::mem::take(&mut bufs[src as usize]);
458 let d = &mut bufs[dst as usize];
459 let (oh, ow) = (h / 2, w / 2);
460 for ch in 0..c {
461 for i in 0..2 {
462 for j in 0..2 {
463 let oc = ch * 4 + i * 2 + j;
464 for y in 0..oh {
465 for x in 0..ow {
466 d[(oc * oh + y) * ow + x] = srcv[(ch * h + 2 * y + i) * w + 2 * x + j];
467 }
468 }
469 }
470 }
471 }
472 bufs[src as usize] = srcv;
473 }
474 PvStep::Shuffle { src, dst, add, c, h, w } => {
475 let srcv = std::mem::take(&mut bufs[src as usize]);
476 let addv = add.map(|a| bufs[a as usize].clone());
477 let d = &mut bufs[dst as usize];
478 let (oh, ow, oc) = (2 * h, 2 * w, c / 4);
479 for ch in 0..oc {
480 for i in 0..2 {
481 for j in 0..2 {
482 let ic = ch * 4 + i * 2 + j;
483 for y in 0..h {
484 for x in 0..w {
485 let o = (ch * oh + 2 * y + i) * ow + 2 * x + j;
486 d[o] = srcv[(ic * h + y) * w + x] + addv.as_ref().map_or(0.0, |a| a[o]);
487 }
488 }
489 }
490 }
491 }
492 bufs[src as usize] = srcv;
493 }
494 }
495}
496
497pub fn rgb8_to_chw(data: &[u8], h: usize, w: usize) -> Vec<f32> {
499 let mut out = vec![0f32; 3 * h * w];
500 for (i, px) in data.chunks_exact(3).enumerate() {
501 for c in 0..3 {
502 out[c * h * w + i] = px[c] as f32 / 127.5 - 1.0;
503 }
504 }
505 out
506}
507
508pub fn chw_to_rgb8(x: &[f32], h: usize, w: usize) -> Vec<u8> {
510 let mut out = vec![0u8; 3 * h * w];
511 for i in 0..h * w {
512 for c in 0..3 {
513 let v = ((x[c * h * w + i] + 1.0) * 127.5).round().clamp(0.0, 255.0);
514 out[i * 3 + c] = v as u8;
515 }
516 }
517 out
518}
519
520pub fn psnr(a: &[f32], b: &[f32]) -> f64 {
523 let mut se = 0f64;
524 for (&x, &y) in a.iter().zip(b) {
525 let (x, y) = (((x + 1.0) * 127.5).clamp(0.0, 255.0) as f64, ((y + 1.0) * 127.5).clamp(0.0, 255.0) as f64);
526 se += (x - y) * (x - y);
527 }
528 let mse = se / a.len() as f64;
529 if mse == 0.0 {
530 f64::INFINITY
531 } else {
532 10.0 * (255.0f64 * 255.0 / mse).log10()
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 fn bufs_of(sizes: &[usize]) -> Vec<Vec<f32>> {
541 sizes.iter().map(|&n| vec![0f32; n]).collect()
542 }
543
544 #[test]
547 fn shuffles_invert_each_other_in_torch_order() {
548 let (c, h, w) = (2usize, 4usize, 6usize);
549 let x: Vec<f32> = (0..c * h * w).map(|v| v as f32).collect();
550 let mut bufs = bufs_of(&[c * h * w, c * h * w, c * h * w]);
551 bufs[0].copy_from_slice(&x);
552 let wts = PvWeights::default();
553 host_step(&wts, &PvStep::Unshuffle { src: 0, dst: 1, c, h, w }, &mut bufs, None);
554 assert_eq!(bufs[1][(1 * 2) * 3], x[1]);
556 host_step(&wts, &PvStep::Shuffle { src: 1, dst: 2, add: None, c: 4 * c, h: h / 2, w: w / 2 }, &mut bufs, None);
557 assert_eq!(bufs[2], x);
558 }
559
560 #[test]
563 fn dilated_conv_taps_and_padding() {
564 let (h, w) = (5usize, 5usize);
565 let x: Vec<f32> = (0..h * w).map(|v| v as f32 + 1.0).collect();
566 let mut wt = vec![0f32; 9];
567 wt[5] = 1.0; let cv = PvConv { w: wt, b: vec![0.5], cin: 1, cout: 1 };
569 let mut out = vec![0f32; h * w];
570 host_conv(&cv, &x, &mut out, None, 2, h, w, None);
571 assert_eq!(out[0], x[2] + 0.5);
572 assert_eq!(out[3], 0.5); let res = vec![1f32; h * w];
574 host_conv(&cv, &x, &mut out, Some(&res), 2, h, w, None);
575 assert_eq!(out[0], x[2] + 1.5);
576 }
577
578 #[test]
580 fn norm_silu_is_per_pixel() {
581 let (c, h, w) = (2usize, 1usize, 2usize);
582 let mut bufs = bufs_of(&[c * h * w, c * h * w]);
583 bufs[0].copy_from_slice(&[3.0, 1.0, 4.0, 1.0]); let wts = PvWeights { convs: vec![], gammas: vec![vec![1.0, 2.0]] };
585 host_step(&wts, &PvStep::NormSilu { gi: 0, src: 0, dst: 1, c, h, w }, &mut bufs, None);
586 let silu = |v: f32| v / (1.0 + (-v).exp());
587 let inv0 = 1.0 / ((9.0f32 + 16.0) / 2.0 + EPS).sqrt();
588 assert!((bufs[1][0] - silu(3.0 * inv0)).abs() < 1e-6);
589 assert!((bufs[1][2] - silu(4.0 * inv0 * 2.0)).abs() < 1e-6);
590 }
591}