Skip to main content

candle_transformers/models/z_image/
vae.rs

1//! Z-Image VAE (AutoEncoderKL) - Diffusers Format
2//!
3//! This VAE implementation uses the diffusers weight naming format,
4//! which is different from the Flux autoencoder original format.
5//!
6//! Key differences from Flux autoencoder:
7//! 1. Weight paths: `encoder.down_blocks.{i}.resnets.{j}.*` vs `encoder.down.{i}.block.{j}.*`
8//! 2. Attention naming: `to_q/to_k/to_v/to_out.0.*` vs `q/k/v/proj_out.*`
9//! 3. Shortcut naming: `conv_shortcut.*` vs `nin_shortcut.*`
10
11use candle::{Module, Result, Tensor, D};
12use candle_nn::{conv2d, group_norm, Conv2d, Conv2dConfig, GroupNorm, VarBuilder};
13
14// ==================== Config ====================
15
16/// VAE configuration
17#[derive(Debug, Clone, serde::Deserialize)]
18pub struct VaeConfig {
19    #[serde(default = "default_in_channels")]
20    pub in_channels: usize,
21    #[serde(default = "default_out_channels")]
22    pub out_channels: usize,
23    #[serde(default = "default_latent_channels")]
24    pub latent_channels: usize,
25    #[serde(default = "default_block_out_channels")]
26    pub block_out_channels: Vec<usize>,
27    #[serde(default = "default_layers_per_block")]
28    pub layers_per_block: usize,
29    #[serde(default = "default_scaling_factor")]
30    pub scaling_factor: f64,
31    #[serde(default = "default_shift_factor")]
32    pub shift_factor: f64,
33    #[serde(default = "default_norm_num_groups")]
34    pub norm_num_groups: usize,
35}
36
37fn default_in_channels() -> usize {
38    3
39}
40fn default_out_channels() -> usize {
41    3
42}
43fn default_latent_channels() -> usize {
44    16
45}
46fn default_block_out_channels() -> Vec<usize> {
47    vec![128, 256, 512, 512]
48}
49fn default_layers_per_block() -> usize {
50    2
51}
52fn default_scaling_factor() -> f64 {
53    0.3611
54}
55fn default_shift_factor() -> f64 {
56    0.1159
57}
58fn default_norm_num_groups() -> usize {
59    32
60}
61
62impl Default for VaeConfig {
63    fn default() -> Self {
64        Self::z_image()
65    }
66}
67
68impl VaeConfig {
69    /// Create configuration for Z-Image VAE
70    pub fn z_image() -> Self {
71        Self {
72            in_channels: 3,
73            out_channels: 3,
74            latent_channels: 16,
75            block_out_channels: vec![128, 256, 512, 512],
76            layers_per_block: 2,
77            scaling_factor: 0.3611,
78            shift_factor: 0.1159,
79            norm_num_groups: 32,
80        }
81    }
82}
83
84// ==================== Attention ====================
85
86fn scaled_dot_product_attention(q: &Tensor, k: &Tensor, v: &Tensor) -> Result<Tensor> {
87    let dim = q.dim(D::Minus1)?;
88    let scale_factor = 1.0 / (dim as f64).sqrt();
89    let attn_weights = (q.matmul(&k.t()?)? * scale_factor)?;
90    candle_nn::ops::softmax_last_dim(&attn_weights)?.matmul(v)
91}
92
93/// VAE Attention block (diffusers format)
94///
95/// Note: VAE attention uses Linear with bias (2D weight shape)
96/// Unlike Transformer attention which uses linear_no_bias
97#[derive(Debug, Clone)]
98struct Attention {
99    group_norm: GroupNorm,
100    to_q: candle_nn::Linear,
101    to_k: candle_nn::Linear,
102    to_v: candle_nn::Linear,
103    to_out: candle_nn::Linear,
104}
105
106impl Attention {
107    fn new(channels: usize, num_groups: usize, vb: VarBuilder) -> Result<Self> {
108        let group_norm = group_norm(num_groups, channels, 1e-6, vb.pp("group_norm"))?;
109        // VAE attention uses Linear with bias
110        let to_q = candle_nn::linear(channels, channels, vb.pp("to_q"))?;
111        let to_k = candle_nn::linear(channels, channels, vb.pp("to_k"))?;
112        let to_v = candle_nn::linear(channels, channels, vb.pp("to_v"))?;
113        let to_out = candle_nn::linear(channels, channels, vb.pp("to_out").pp("0"))?;
114        Ok(Self {
115            group_norm,
116            to_q,
117            to_k,
118            to_v,
119            to_out,
120        })
121    }
122}
123
124impl Module for Attention {
125    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
126        let residual = xs;
127        let (b, c, h, w) = xs.dims4()?;
128
129        // GroupNorm
130        let xs = xs.apply(&self.group_norm)?;
131
132        // (B, C, H, W) -> (B, H, W, C) -> (B*H*W, C)
133        let xs = xs.permute((0, 2, 3, 1))?.reshape((b * h * w, c))?;
134
135        // Linear projections
136        let q = xs.apply(&self.to_q)?; // (B*H*W, C)
137        let k = xs.apply(&self.to_k)?;
138        let v = xs.apply(&self.to_v)?;
139
140        // Reshape for attention: (B*H*W, C) -> (B, H*W, C) -> (B, 1, H*W, C)
141        let q = q.reshape((b, h * w, c))?.unsqueeze(1)?;
142        let k = k.reshape((b, h * w, c))?.unsqueeze(1)?;
143        let v = v.reshape((b, h * w, c))?.unsqueeze(1)?;
144
145        // Scaled dot-product attention
146        let xs = scaled_dot_product_attention(&q, &k, &v)?;
147
148        // (B, 1, H*W, C) -> (B*H*W, C)
149        let xs = xs.squeeze(1)?.reshape((b * h * w, c))?;
150
151        // Output projection
152        let xs = xs.apply(&self.to_out)?;
153
154        // (B*H*W, C) -> (B, H, W, C) -> (B, C, H, W)
155        let xs = xs.reshape((b, h, w, c))?.permute((0, 3, 1, 2))?;
156
157        // Residual connection
158        xs + residual
159    }
160}
161
162// ==================== ResnetBlock2D ====================
163
164/// ResNet block (diffusers format)
165#[derive(Debug, Clone)]
166struct ResnetBlock2D {
167    norm1: GroupNorm,
168    conv1: Conv2d,
169    norm2: GroupNorm,
170    conv2: Conv2d,
171    conv_shortcut: Option<Conv2d>,
172}
173
174impl ResnetBlock2D {
175    fn new(
176        in_channels: usize,
177        out_channels: usize,
178        num_groups: usize,
179        vb: VarBuilder,
180    ) -> Result<Self> {
181        let conv_cfg = Conv2dConfig {
182            padding: 1,
183            ..Default::default()
184        };
185
186        let norm1 = group_norm(num_groups, in_channels, 1e-6, vb.pp("norm1"))?;
187        let conv1 = conv2d(in_channels, out_channels, 3, conv_cfg, vb.pp("conv1"))?;
188        let norm2 = group_norm(num_groups, out_channels, 1e-6, vb.pp("norm2"))?;
189        let conv2 = conv2d(out_channels, out_channels, 3, conv_cfg, vb.pp("conv2"))?;
190
191        let conv_shortcut = if in_channels != out_channels {
192            Some(conv2d(
193                in_channels,
194                out_channels,
195                1,
196                Default::default(),
197                vb.pp("conv_shortcut"),
198            )?)
199        } else {
200            None
201        };
202
203        Ok(Self {
204            norm1,
205            conv1,
206            norm2,
207            conv2,
208            conv_shortcut,
209        })
210    }
211}
212
213impl Module for ResnetBlock2D {
214    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
215        let h = xs
216            .apply(&self.norm1)?
217            .apply(&candle_nn::Activation::Swish)?
218            .apply(&self.conv1)?
219            .apply(&self.norm2)?
220            .apply(&candle_nn::Activation::Swish)?
221            .apply(&self.conv2)?;
222
223        match &self.conv_shortcut {
224            Some(conv) => xs.apply(conv)? + h,
225            None => xs + h,
226        }
227    }
228}
229
230// ==================== DownEncoderBlock2D ====================
231
232#[derive(Debug, Clone)]
233struct Downsample2D {
234    conv: Conv2d,
235}
236
237impl Downsample2D {
238    fn new(channels: usize, vb: VarBuilder) -> Result<Self> {
239        let conv_cfg = Conv2dConfig {
240            stride: 2,
241            padding: 0,
242            ..Default::default()
243        };
244        let conv = conv2d(channels, channels, 3, conv_cfg, vb.pp("conv"))?;
245        Ok(Self { conv })
246    }
247}
248
249impl Module for Downsample2D {
250    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
251        // Manual padding: (0, 1, 0, 1) for right=1, bottom=1
252        let xs = xs.pad_with_zeros(D::Minus1, 0, 1)?; // width: right
253        let xs = xs.pad_with_zeros(D::Minus2, 0, 1)?; // height: bottom
254        xs.apply(&self.conv)
255    }
256}
257
258#[derive(Debug, Clone)]
259struct DownEncoderBlock2D {
260    resnets: Vec<ResnetBlock2D>,
261    downsampler: Option<Downsample2D>,
262}
263
264impl DownEncoderBlock2D {
265    fn new(
266        in_channels: usize,
267        out_channels: usize,
268        num_layers: usize,
269        num_groups: usize,
270        add_downsample: bool,
271        vb: VarBuilder,
272    ) -> Result<Self> {
273        let mut resnets = Vec::with_capacity(num_layers);
274        let vb_resnets = vb.pp("resnets");
275
276        for i in 0..num_layers {
277            let in_c = if i == 0 { in_channels } else { out_channels };
278            resnets.push(ResnetBlock2D::new(
279                in_c,
280                out_channels,
281                num_groups,
282                vb_resnets.pp(i),
283            )?);
284        }
285
286        let downsampler = if add_downsample {
287            Some(Downsample2D::new(
288                out_channels,
289                vb.pp("downsamplers").pp("0"),
290            )?)
291        } else {
292            None
293        };
294
295        Ok(Self {
296            resnets,
297            downsampler,
298        })
299    }
300}
301
302impl Module for DownEncoderBlock2D {
303    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
304        let mut h = xs.clone();
305        for resnet in &self.resnets {
306            h = h.apply(resnet)?;
307        }
308        if let Some(ds) = &self.downsampler {
309            h = h.apply(ds)?;
310        }
311        Ok(h)
312    }
313}
314
315// ==================== UpDecoderBlock2D ====================
316
317#[derive(Debug, Clone)]
318struct Upsample2D {
319    conv: Conv2d,
320}
321
322impl Upsample2D {
323    fn new(channels: usize, vb: VarBuilder) -> Result<Self> {
324        let conv_cfg = Conv2dConfig {
325            padding: 1,
326            ..Default::default()
327        };
328        let conv = conv2d(channels, channels, 3, conv_cfg, vb.pp("conv"))?;
329        Ok(Self { conv })
330    }
331}
332
333impl Module for Upsample2D {
334    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
335        let (_, _, h, w) = xs.dims4()?;
336        xs.upsample_nearest2d(h * 2, w * 2)?.apply(&self.conv)
337    }
338}
339
340#[derive(Debug, Clone)]
341struct UpDecoderBlock2D {
342    resnets: Vec<ResnetBlock2D>,
343    upsampler: Option<Upsample2D>,
344}
345
346impl UpDecoderBlock2D {
347    fn new(
348        in_channels: usize,
349        out_channels: usize,
350        num_layers: usize, // decoder has num_layers + 1 resnets per block
351        num_groups: usize,
352        add_upsample: bool,
353        vb: VarBuilder,
354    ) -> Result<Self> {
355        let mut resnets = Vec::with_capacity(num_layers + 1);
356        let vb_resnets = vb.pp("resnets");
357
358        for i in 0..=num_layers {
359            let in_c = if i == 0 { in_channels } else { out_channels };
360            resnets.push(ResnetBlock2D::new(
361                in_c,
362                out_channels,
363                num_groups,
364                vb_resnets.pp(i),
365            )?);
366        }
367
368        let upsampler = if add_upsample {
369            Some(Upsample2D::new(out_channels, vb.pp("upsamplers").pp("0"))?)
370        } else {
371            None
372        };
373
374        Ok(Self { resnets, upsampler })
375    }
376}
377
378impl Module for UpDecoderBlock2D {
379    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
380        let mut h = xs.clone();
381        for resnet in &self.resnets {
382            h = h.apply(resnet)?;
383        }
384        if let Some(us) = &self.upsampler {
385            h = h.apply(us)?;
386        }
387        Ok(h)
388    }
389}
390
391// ==================== UNetMidBlock2D ====================
392
393#[derive(Debug, Clone)]
394struct UNetMidBlock2D {
395    resnet_0: ResnetBlock2D,
396    attention: Attention,
397    resnet_1: ResnetBlock2D,
398}
399
400impl UNetMidBlock2D {
401    fn new(channels: usize, num_groups: usize, vb: VarBuilder) -> Result<Self> {
402        let resnet_0 =
403            ResnetBlock2D::new(channels, channels, num_groups, vb.pp("resnets").pp("0"))?;
404        let attention = Attention::new(channels, num_groups, vb.pp("attentions").pp("0"))?;
405        let resnet_1 =
406            ResnetBlock2D::new(channels, channels, num_groups, vb.pp("resnets").pp("1"))?;
407        Ok(Self {
408            resnet_0,
409            attention,
410            resnet_1,
411        })
412    }
413}
414
415impl Module for UNetMidBlock2D {
416    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
417        xs.apply(&self.resnet_0)?
418            .apply(&self.attention)?
419            .apply(&self.resnet_1)
420    }
421}
422
423// ==================== Encoder ====================
424
425/// VAE Encoder
426#[derive(Debug, Clone)]
427pub struct Encoder {
428    conv_in: Conv2d,
429    down_blocks: Vec<DownEncoderBlock2D>,
430    mid_block: UNetMidBlock2D,
431    conv_norm_out: GroupNorm,
432    conv_out: Conv2d,
433}
434
435impl Encoder {
436    pub fn new(cfg: &VaeConfig, vb: VarBuilder) -> Result<Self> {
437        let conv_cfg = Conv2dConfig {
438            padding: 1,
439            ..Default::default()
440        };
441        let conv_in = conv2d(
442            cfg.in_channels,
443            cfg.block_out_channels[0],
444            3,
445            conv_cfg,
446            vb.pp("conv_in"),
447        )?;
448
449        let mut down_blocks = Vec::with_capacity(cfg.block_out_channels.len());
450        let vb_down = vb.pp("down_blocks");
451
452        for (i, &out_channels) in cfg.block_out_channels.iter().enumerate() {
453            let in_channels = if i == 0 {
454                cfg.block_out_channels[0]
455            } else {
456                cfg.block_out_channels[i - 1]
457            };
458            let add_downsample = i < cfg.block_out_channels.len() - 1;
459            down_blocks.push(DownEncoderBlock2D::new(
460                in_channels,
461                out_channels,
462                cfg.layers_per_block,
463                cfg.norm_num_groups,
464                add_downsample,
465                vb_down.pp(i),
466            )?);
467        }
468
469        let mid_channels = *cfg.block_out_channels.last().unwrap();
470        let mid_block = UNetMidBlock2D::new(mid_channels, cfg.norm_num_groups, vb.pp("mid_block"))?;
471
472        let conv_norm_out = group_norm(
473            cfg.norm_num_groups,
474            mid_channels,
475            1e-6,
476            vb.pp("conv_norm_out"),
477        )?;
478        let conv_out = conv2d(
479            mid_channels,
480            2 * cfg.latent_channels,
481            3,
482            conv_cfg,
483            vb.pp("conv_out"),
484        )?;
485
486        Ok(Self {
487            conv_in,
488            down_blocks,
489            mid_block,
490            conv_norm_out,
491            conv_out,
492        })
493    }
494}
495
496impl Module for Encoder {
497    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
498        let mut h = xs.apply(&self.conv_in)?;
499        for block in &self.down_blocks {
500            h = h.apply(block)?;
501        }
502        h.apply(&self.mid_block)?
503            .apply(&self.conv_norm_out)?
504            .apply(&candle_nn::Activation::Swish)?
505            .apply(&self.conv_out)
506    }
507}
508
509// ==================== Decoder ====================
510
511/// VAE Decoder
512#[derive(Debug, Clone)]
513pub struct Decoder {
514    conv_in: Conv2d,
515    mid_block: UNetMidBlock2D,
516    up_blocks: Vec<UpDecoderBlock2D>,
517    conv_norm_out: GroupNorm,
518    conv_out: Conv2d,
519}
520
521impl Decoder {
522    pub fn new(cfg: &VaeConfig, vb: VarBuilder) -> Result<Self> {
523        let conv_cfg = Conv2dConfig {
524            padding: 1,
525            ..Default::default()
526        };
527        let mid_channels = *cfg.block_out_channels.last().unwrap();
528
529        let conv_in = conv2d(
530            cfg.latent_channels,
531            mid_channels,
532            3,
533            conv_cfg,
534            vb.pp("conv_in"),
535        )?;
536        let mid_block = UNetMidBlock2D::new(mid_channels, cfg.norm_num_groups, vb.pp("mid_block"))?;
537
538        // Decoder up_blocks order is reversed from encoder down_blocks
539        let reversed_channels: Vec<usize> = cfg.block_out_channels.iter().rev().cloned().collect();
540        let mut up_blocks = Vec::with_capacity(reversed_channels.len());
541        let vb_up = vb.pp("up_blocks");
542
543        for (i, &out_channels) in reversed_channels.iter().enumerate() {
544            let in_channels = if i == 0 {
545                mid_channels
546            } else {
547                reversed_channels[i - 1]
548            };
549            let add_upsample = i < reversed_channels.len() - 1;
550            up_blocks.push(UpDecoderBlock2D::new(
551                in_channels,
552                out_channels,
553                cfg.layers_per_block,
554                cfg.norm_num_groups,
555                add_upsample,
556                vb_up.pp(i),
557            )?);
558        }
559
560        let final_channels = *reversed_channels.last().unwrap();
561        let conv_norm_out = group_norm(
562            cfg.norm_num_groups,
563            final_channels,
564            1e-6,
565            vb.pp("conv_norm_out"),
566        )?;
567        let conv_out = conv2d(
568            final_channels,
569            cfg.out_channels,
570            3,
571            conv_cfg,
572            vb.pp("conv_out"),
573        )?;
574
575        Ok(Self {
576            conv_in,
577            mid_block,
578            up_blocks,
579            conv_norm_out,
580            conv_out,
581        })
582    }
583}
584
585impl Module for Decoder {
586    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
587        let mut h = xs.apply(&self.conv_in)?.apply(&self.mid_block)?;
588        for block in &self.up_blocks {
589            h = h.apply(block)?;
590        }
591        h.apply(&self.conv_norm_out)?
592            .apply(&candle_nn::Activation::Swish)?
593            .apply(&self.conv_out)
594    }
595}
596
597// ==================== DiagonalGaussian ====================
598
599/// Diagonal Gaussian distribution sampling (VAE reparameterization trick)
600#[derive(Debug, Clone)]
601pub struct DiagonalGaussian {
602    sample: bool,
603}
604
605impl DiagonalGaussian {
606    pub fn new(sample: bool) -> Self {
607        Self { sample }
608    }
609}
610
611impl Module for DiagonalGaussian {
612    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
613        let chunks = xs.chunk(2, 1)?; // Split along channel dimension
614        let mean = &chunks[0];
615        let logvar = &chunks[1];
616
617        if self.sample {
618            let std = (logvar * 0.5)?.exp()?;
619            mean + (std * mean.randn_like(0., 1.)?)?
620        } else {
621            Ok(mean.clone())
622        }
623    }
624}
625
626// ==================== AutoEncoderKL ====================
627
628/// Z-Image VAE (AutoEncoderKL) - Diffusers Format
629#[derive(Debug, Clone)]
630pub struct AutoEncoderKL {
631    encoder: Encoder,
632    decoder: Decoder,
633    reg: DiagonalGaussian,
634    scale_factor: f64,
635    shift_factor: f64,
636}
637
638impl AutoEncoderKL {
639    pub fn new(cfg: &VaeConfig, vb: VarBuilder) -> Result<Self> {
640        let encoder = Encoder::new(cfg, vb.pp("encoder"))?;
641        let decoder = Decoder::new(cfg, vb.pp("decoder"))?;
642        let reg = DiagonalGaussian::new(true);
643
644        Ok(Self {
645            encoder,
646            decoder,
647            reg,
648            scale_factor: cfg.scaling_factor,
649            shift_factor: cfg.shift_factor,
650        })
651    }
652
653    /// Encode image to latent space
654    /// xs: (B, 3, H, W) RGB image, range [-1, 1]
655    /// Returns: (B, latent_channels, H/8, W/8)
656    pub fn encode(&self, xs: &Tensor) -> Result<Tensor> {
657        let z = xs.apply(&self.encoder)?.apply(&self.reg)?;
658        (z - self.shift_factor)? * self.scale_factor
659    }
660
661    /// Decode latent to image
662    /// xs: (B, latent_channels, H/8, W/8)
663    /// Returns: (B, 3, H, W) RGB image, range [-1, 1]
664    pub fn decode(&self, xs: &Tensor) -> Result<Tensor> {
665        let xs = ((xs / self.scale_factor)? + self.shift_factor)?;
666        xs.apply(&self.decoder)
667    }
668
669    /// Get scaling factor
670    pub fn scale_factor(&self) -> f64 {
671        self.scale_factor
672    }
673
674    /// Get shift factor
675    pub fn shift_factor(&self) -> f64 {
676        self.shift_factor
677    }
678}
679
680impl Module for AutoEncoderKL {
681    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
682        self.decode(&self.encode(xs)?)
683    }
684}