# Reference copy of OpenMOSS-Team/MOSS-Image-SFT pipeline_moss_image.py (Apache-2.0,
# https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT), unmodified below this header, kept as .txt so formatters leave it byte-identical. Test data only: the parity
# tests run it as the numerical reference for core/inference/diffusion_moss_image.py; Studio never imports it.
"""MOSSImage modeling and text-to-image pipeline (no Motor/TorchTitan imports).

Dependencies: torch>=2.9, diffusers>=0.38, transformers>=5.3, safetensors,
accelerate, Pillow. Copy this file anywhere and import it normally.

    import torch
    # Import MOSSImagePipeline from this file for component-level loading.
    pipe = MOSSImagePipeline.from_components_pretrained(
        transformer_path="exported_transformer",  # save_pretrained directory
        text_encoder_path="Qwen/Qwen3.5-9B",
        vae_path="black-forest-labs/FLUX.2-klein-4B",
    ).to("cuda")
    pipe("A cat", generator=torch.Generator("cuda").manual_seed(42)).images[0].save("cat.png")

Default guidance uses CFG 3 and skips block 22 only in the negative branch
(zero-based main DiT block indices; two forwards per sampling step):
    pipe("A cat", guidance_scale=3.0, uncond_skip_layers=[22])
    pipe("A cat", uncond_skip_layers=[])  # disable negative-branch skipping
Optional additional positive-branch SLG adds a third forward:
    pipe("A cat", slg_layers=[24], slg_scale=3.0)
The additional SLG layer/scale above illustrates the API, not a quality-tuned setting.

For a consolidated training state dict, create MOSSImageTransformer and
call load_training_state_dict(state), then save_pretrained("exported_transformer").
Select raw or EMA weights explicitly before calling it; distributed checkpoint
shards/optimizer states are not model state dicts. Defaults match the finalized dense MOSSImage model,
not MoE, CFG-distilled, video, or reference-image variants. Prompt strings are
already-rendered captions (dataset-specific quality/domain tags are not added).

First-release defaults: bf16 DiT/text-encoder weights, fp32 RoPE frequencies
and phase/sin/cos calculation, then RoPE cast to the video activation dtype.
Initial Gaussian noise is sampled directly in fp32, including with bf16 weights.
CFG and Euler latent updates use fp32; VAE decoding defaults to fp32. Scheduler
shift defaults to 5.0. Exact numerical parity with the original fp32-master
inference pipeline is not a release requirement.
"""
from __future__ import annotations

import math
from pathlib import Path

import torch
from torch import nn
from torch.nn import functional as F
from diffusers import AutoencoderKLFlux2, DiffusionPipeline, FlowMatchEulerDiscreteScheduler
from diffusers.schedulers.scheduling_utils import SchedulerMixin  # Required by custom-component discovery.
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.image_processor import VaeImageProcessor
from diffusers.models.modeling_utils import ModelMixin
from diffusers.models.modeling_outputs import Transformer2DModelOutput
from diffusers.pipelines.pipeline_utils import ImagePipelineOutput
from diffusers.utils.torch_utils import randn_tensor
from transformers import AutoModel, AutoTokenizer


class _RMSNorm(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(dim))

    def forward(self, x):
        y = x.float() * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + 1e-6)
        return y.to(x.dtype) * self.weight


def _situ(hidden, gate):
    return (4.0 * torch.tanh(gate / 4.0) * torch.sigmoid(gate)) * (25.0 * torch.tanh(hidden / 25.0))


def _validate_skip_layers(layers, depth, name='skip_layers'):
    layers = () if layers is None else tuple(layers)
    if any(type(index) is not int or not 0 <= index < depth for index in layers):
        raise ValueError(f'{name} must contain integer block indices in [0, {depth}).')
    return frozenset(layers)


def _rotate(x, rope):
    sin, cos = rope.chunk(2, -1)
    sin = torch.cat((sin, sin), -1).unsqueeze(-2)
    cos = torch.cat((cos, cos), -1).unsqueeze(-2)
    a, b = x.chunk(2, -1)
    return x * cos + torch.cat((-b, a), -1) * sin


def _attention(q, k, v, rope, heads):
    shape = (*q.shape[:2], heads, q.shape[-1] // heads)
    q = _rotate(q.reshape(shape), rope).transpose(1, 2)
    k = _rotate(k.reshape(shape), rope).transpose(1, 2)
    v = v.reshape(shape).transpose(1, 2)
    return F.scaled_dot_product_attention(q.to(v.dtype), k.to(v.dtype), v).transpose(1, 2).flatten(2)


class _TextBlock(nn.Module):
    def __init__(self, dim, ffn_dim, heads):
        super().__init__()
        self.heads = heads
        self.norm1, self.norm2 = _RMSNorm(dim), _RMSNorm(dim)
        self.q_norm, self.k_norm = _RMSNorm(dim // heads), _RMSNorm(dim // heads)
        self.qkv = nn.Linear(dim, 3 * dim)
        self.proj = nn.Linear(dim, dim)
        self.ffn_gate, self.ffn_up = nn.Linear(dim, ffn_dim), nn.Linear(dim, ffn_dim)
        self.ffn_down = nn.Linear(ffn_dim, dim)

    def forward(self, x, rope):
        q, k, v = self.qkv(self.norm1(x)).chunk(3, -1)
        q = self.q_norm(q.unflatten(-1, (self.heads, -1))).flatten(2)
        k = self.k_norm(k.unflatten(-1, (self.heads, -1))).flatten(2)
        x = x + self.proj(_attention(q, k, v, rope, self.heads))
        y = self.norm2(x)
        return x + self.ffn_down(_situ(self.ffn_up(y), self.ffn_gate(y)))


class _TextProjection(nn.Module):
    def __init__(self, text_dim, dim, ffn_dim, heads):
        super().__init__()
        self.proj = nn.Linear(text_dim, dim)
        self.blocks = nn.ModuleList([_TextBlock(dim, ffn_dim, heads) for _ in range(2)])

    def forward(self, x, rope):
        x = self.proj(x.to(self.proj.weight.dtype))
        for block in self.blocks:
            x = block(x, rope)
        return x


class _Block(nn.Module):
    def __init__(self, dim, ffn_dim, heads):
        super().__init__()
        self.heads = heads
        self.norm1, self.norm2 = _RMSNorm(dim), _RMSNorm(dim)
        self.q_norm, self.k_norm = _RMSNorm(dim // heads), _RMSNorm(dim // heads)
        self.proj = nn.ModuleDict({
            name: nn.Linear(dim, dim, bias=False) for name in ('q', 'k', 'v', 'o')
        })
        self.proj['ffn_up_gate'] = nn.Linear(dim, 2 * ffn_dim, bias=False)
        self.proj['ffn_down'] = nn.Linear(ffn_dim, dim, bias=False)

    def forward(self, x, rope, modulation):
        shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = modulation.unbind(1)
        y = self.norm1(x) * (1 + scale_a[:, None]) + shift_a[:, None]
        dtype = self.proj['q'].weight.dtype
        y = y.to(dtype)
        q = self.q_norm(self.proj['q'](y).unflatten(-1, (self.heads, -1))).flatten(2)
        k = self.k_norm(self.proj['k'](y).unflatten(-1, (self.heads, -1))).flatten(2)
        x = x + self.proj['o'](_attention(q, k, self.proj['v'](y), rope, self.heads)) * gate_a[:, None]
        y = self.norm2(x) * (1 + scale_f[:, None]) + shift_f[:, None]
        hidden, gate = self.proj['ffn_up_gate'](y.to(dtype)).chunk(2, -1)
        return x + self.proj['ffn_down'](_situ(hidden, gate)) * gate_f[:, None]


class _RoPE(nn.Module):
    def __init__(self, head_dim, base):
        super().__init__()
        self.num_bands = head_dim // 8
        self.base = base
        self.register_buffer('bands', self._default_bands(torch.device('cpu')))

    def _default_bands(self, device):
        # Match training's CPU fp32 construction, including under meta init.
        exponent = torch.arange(self.num_bands, device='cpu', dtype=torch.float32) / self.num_bands
        return (1 / (self.base ** exponent)).to(device)

    def _apply(self, fn, recurse=True):
        super()._apply(fn, recurse=recurse)
        # .float() cannot recover frequencies already rounded by .to(bf16).
        self.bands = self._default_bands(self.bands.device)
        return self

    def _load_from_state_dict(self, *args, **kwargs):
        super()._load_from_state_dict(*args, **kwargs)
        # Frequencies are derived constants; retain the key for checkpoint compatibility.
        self.bands = self._default_bands(self.bands.device)

    def forward(self, height, width, text_length, device):
        # Diffusers/Accelerate can install tensors directly, bypassing load hooks.
        if self.bands.dtype != torch.float32:
            self.bands = self._default_bands(device)
        # Coordinates: (r, f, h, w); sequence order is image followed by text.
        h, w = torch.meshgrid(torch.arange(height, device=device), torch.arange(width, device=device), indexing='ij')
        image = torch.stack((torch.zeros_like(h), torch.full_like(h, text_length), h, w), -1).flatten(0, 1).float()
        image[:, 2] -= (height - 1) / 2
        image[:, 3] -= (width - 1) / 2
        text = torch.zeros(text_length, 4, device=device, dtype=torch.float32)
        text[:, 1] = torch.arange(text_length, device=device)
        phase = torch.cat((image, text), 0).unsqueeze(-1) * self.bands
        return torch.cat((phase.sin(), phase.cos()), -2).flatten(-2).unsqueeze(0)


class MOSSImageTransformer(ModelMixin, ConfigMixin):
    """MOSSImage DiT with training-compatible state-dict names and SDPA attention.

    Each sample is evaluated at its true text length, including text refinement;
    padded tokens cannot influence either text or joint attention.
    """
    @register_to_config
    def __init__(self, dim=3072, ffn_dim=8192, num_heads=24, num_layers=42,
                 text_dim=4096, freq_dim=256, latent_channels=128, rope_base=10000.0):
        super().__init__()
        if dim % num_heads or (dim // num_heads) % 8 or freq_dim % 2:
            raise ValueError('Require dim divisible by heads, head_dim divisible by 8, and even freq_dim.')
        self.video_embedder = nn.Linear(latent_channels, dim)
        self.text_embedder = _TextProjection(text_dim, dim, ffn_dim, num_heads)
        self.rope = _RoPE(dim // num_heads, rope_base)
        self.time_embedding = nn.Sequential(nn.Linear(freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
        self.shared_block_modulation = nn.Linear(dim, 6 * dim)
        self.layers = nn.ModuleList([_Block(dim, ffn_dim, num_heads) for _ in range(num_layers)])
        self.final_norm_video = _RMSNorm(dim)
        self.final_linear_video = nn.Linear(dim, latent_channels, bias=False)
        self.video_head_modulation = nn.Linear(dim, 2 * dim)

    def load_training_state_dict(self, state_dict, *, prefix=''):
        """Load a consolidated raw/EMA model mapping, strictly; no silent key drops."""
        if prefix:
            if any(not key.startswith(prefix) for key in state_dict):
                raise ValueError('All keys must start with the explicit prefix.')
            state_dict = {key[len(prefix):]: value for key, value in state_dict.items()}
        return self.load_state_dict(state_dict, strict=True)

    def forward(self, hidden_states, timestep, encoder_hidden_states,
                encoder_attention_mask=None, return_dict=True, skip_layers=None):
        skip_layers = _validate_skip_layers(skip_layers, len(self.layers))
        if hidden_states.ndim != 4 or hidden_states.shape[1] != self.config.latent_channels:
            raise ValueError('hidden_states must be [B, latent_channels, H, W].')
        batch, _, height, width = hidden_states.shape
        if encoder_hidden_states.ndim != 3 or encoder_hidden_states.shape[0] != batch:
            raise ValueError('encoder_hidden_states must be [B, S, text_dim].')
        mask = encoder_attention_mask
        if mask is None:
            mask = torch.ones(encoder_hidden_states.shape[:2], dtype=torch.bool, device=hidden_states.device)
        if mask.shape != encoder_hidden_states.shape[:2] or not torch.all((mask == 0) | (mask == 1)):
            raise ValueError('encoder_attention_mask must be a binary [B, S] mask.')
        times = torch.as_tensor(timestep, device=hidden_states.device, dtype=torch.float32).reshape(-1)
        times = times.expand(batch)
        # Training computes the sinusoid in float64 and feeds float32 into its MLP.
        half = self.config.freq_dim // 2
        freq = 10000 ** (-torch.arange(half, device=times.device, dtype=torch.float64) / half)
        phase = times.double()[:, None] * freq
        embedding = torch.cat((phase.cos(), phase.sin()), -1).float()
        time = self.time_embedding(embedding.to(self.time_embedding[0].weight.dtype)).float()
        mod = self.shared_block_modulation(time.to(self.shared_block_modulation.weight.dtype)).view(batch, 6, -1)
        head = self.video_head_modulation(time.to(self.video_head_modulation.weight.dtype)).view(batch, 2, -1)
        outputs = []
        for index in range(batch):
            context = encoder_hidden_states[index:index + 1, mask[index].bool()]
            if context.shape[1] == 0:
                raise ValueError('Each sample needs at least one valid text token.')
            video = hidden_states[index:index + 1].flatten(2).transpose(1, 2)
            video = self.video_embedder(video.to(self.video_embedder.weight.dtype).contiguous())
            # Match the reference activation dtype under fp32 weights + autocast.
            rope = self.rope(height, width, context.shape[1], hidden_states.device).to(video.dtype)
            text = self.text_embedder(context, rope[:, height * width:])
            x = torch.cat((video, text), 1)
            for layer_index, layer in enumerate(self.layers):
                if layer_index not in skip_layers:
                    x = layer(x, rope, mod[index:index + 1])
            x = self.final_norm_video(x[:, :height * width].to(self.final_norm_video.weight.dtype))
            x = x * (1 + head[index:index + 1, 1, None]) + head[index:index + 1, 0, None]
            x = self.final_linear_video(x.to(self.final_linear_video.weight.dtype))
            outputs.append(x.transpose(1, 2).reshape(1, -1, height, width))
        sample = torch.cat(outputs)
        return Transformer2DModelOutput(sample=sample) if return_dict else (sample,)


class MOSSImageScheduler(FlowMatchEulerDiscreteScheduler):
    """Euler integration using the training project's V2, nearest-grid schedule."""
    @register_to_config
    def __init__(self, num_train_timesteps=1000, shift=5.0, training_shift=1.0):
        super().__init__(num_train_timesteps=num_train_timesteps, shift=1.0)
        self.register_to_config(shift=shift, training_shift=training_shift)

    def set_timesteps(self, num_inference_steps=50, device=None):
        if num_inference_steps < 1 or self.config.shift <= 0 or self.config.training_shift <= 0:
            raise ValueError('Steps and shifts must be positive.')
        total = self.config.num_train_timesteps
        def grid(count, shift):
            lower = shift / (total + shift - 1)
            values = torch.linspace(1, lower, count)
            return shift * values / (1 + (shift - 1) * values)
        sigmas = grid(num_inference_steps, self.config.shift)
        training = grid(total, self.config.training_shift).flip(0)
        upper = torch.searchsorted(training, sigmas).clamp(max=total - 1)
        lower = (upper - 1).clamp(min=0)
        nearest = torch.where((sigmas - training[lower]).abs() <= (training[upper] - sigmas).abs(), lower, upper)
        sigmas = torch.unique_consecutive(training[nearest])
        # Our sigmas have already been shifted and snapped. Avoid a second shift.
        self._shift = 1.0
        super().set_timesteps(device=device, sigmas=sigmas.tolist())


class MOSSImagePipeline(DiffusionPipeline):
    model_cpu_offload_seq = 'text_encoder->transformer->vae'
    _optional_components = ['text_encoder', 'tokenizer', 'vae']

    def __init__(self, transformer: MOSSImageTransformer,
                 scheduler: MOSSImageScheduler, text_encoder=None, tokenizer=None, vae=None):
        super().__init__()
        self.register_modules(transformer=transformer, scheduler=scheduler,
                              text_encoder=text_encoder, tokenizer=tokenizer, vae=vae)
        self.image_processor = VaeImageProcessor(vae_scale_factor=16)

    @classmethod
    def from_components_pretrained(cls, transformer_path, text_encoder_path, vae_path,
                                   torch_dtype=torch.bfloat16, vae_dtype=torch.float32,
                                   local_files_only=False):
        common = dict(local_files_only=local_files_only)
        transformer = MOSSImageTransformer.from_pretrained(transformer_path, torch_dtype=torch_dtype, **common)
        tokenizer = AutoTokenizer.from_pretrained(text_encoder_path, **common)
        text_encoder = AutoModel.from_pretrained(text_encoder_path, dtype=torch_dtype, **common).eval()
        subfolder = None if (Path(vae_path) / 'config.json').is_file() else 'vae'
        vae = AutoencoderKLFlux2.from_pretrained(vae_path, subfolder=subfolder, torch_dtype=vae_dtype, **common).eval()
        return cls(transformer.eval(), MOSSImageScheduler(), text_encoder, tokenizer, vae)

    @torch.no_grad()
    def encode_prompt(self, prompt, max_sequence_length=4096):
        if self.text_encoder is None or self.tokenizer is None:
            raise ValueError('Text encoder/tokenizer required for string prompts.')
        prompts = [prompt] if isinstance(prompt, str) else list(prompt)
        if not prompts or not all(isinstance(p, str) for p in prompts):
            raise ValueError('prompt must be a string or a nonempty list of strings.')
        rows = []
        # Unpadded individual forwards also avoid padding effects in Qwen GDN.
        for value in prompts:
            formatted = self.tokenizer.apply_chat_template(
                [{'role': 'user', 'content': value}], tokenize=False,
                add_generation_prompt=True, enable_thinking=True)
            inputs = self.tokenizer(formatted, return_tensors='pt', truncation=True,
                                    max_length=max_sequence_length).to(self._execution_device)
            output = self.text_encoder(**inputs, use_cache=False, return_dict=True)
            rows.append(output.last_hidden_state[0])
        length = max(row.shape[0] for row in rows)
        embeds = rows[0].new_zeros(len(rows), length, rows[0].shape[-1])
        mask = torch.zeros(len(rows), length, device=embeds.device, dtype=torch.bool)
        for index, row in enumerate(rows):
            embeds[index, :row.shape[0]] = row
            mask[index, :row.shape[0]] = True
        return embeds, mask

    @torch.no_grad()
    def __call__(self, prompt=None, *, height=1024, width=1024, num_inference_steps=50,
                 guidance_scale=3.0, negative_prompt='', num_images_per_prompt=1,
                 generator=None, latents=None, prompt_embeds=None, prompt_attention_mask=None,
                 negative_prompt_embeds=None, negative_prompt_attention_mask=None,
                 max_sequence_length=4096, output_type='pil', return_dict=True,
                 uncond_skip_layers=(22,), slg_layers=None, slg_scale=0.0,
                 slg_start=0.0, slg_stop=1.0):
        """Generate images with optional full-block skipping (zero-based indices).

        uncond_skip_layers defaults to (22,) and replaces CFG's negative-prompt
        prediction with a skipped-block prediction, retaining two forwards per
        step. Pass None or an empty sequence to disable skipping. slg_layers
        adds a skipped positive-prompt forward and the fp32 correction
        slg_scale * (cond - cond_skip) to CFG. With guidance_scale=1, active
        SLG uses cond_skip + slg_scale * (cond - cond_skip), matching Diffusers'
        default SkipLayerGuidance formulation, with two forwards and no negative
        branch. Both options can combine. slg_scale=1 then gives normal cond.
        Both uncond skipping and SLG are active where
        slg_start <= step_index / num_steps < slg_stop;
        the interval follows denoising order, from initial noise to final image.
        Outside it, inference uses normal CFG without skipping.
        slg_scale=0 or empty slg_layers disables the additional forward.
        """
        uncond_skip_layers = _validate_skip_layers(
            uncond_skip_layers, len(self.transformer.layers), 'uncond_skip_layers')
        slg_layers = _validate_skip_layers(slg_layers, len(self.transformer.layers), 'slg_layers')
        if not math.isfinite(slg_scale) or slg_scale < 0:
            raise ValueError('slg_scale must be finite and nonnegative.')
        if not 0 <= slg_start <= slg_stop <= 1:
            raise ValueError('Require 0 <= slg_start <= slg_stop <= 1.')
        if height <= 0 or width <= 0 or height % 16 or width % 16:
            raise ValueError('height and width must be positive multiples of 16.')
        if output_type not in {'pil', 'np', 'pt', 'latent'}:
            raise ValueError('output_type must be pil, np, pt or latent.')
        if num_images_per_prompt < 1 or not math.isfinite(guidance_scale) or guidance_scale < 0:
            raise ValueError('Invalid image count or guidance scale.')
        if not 1 <= max_sequence_length <= 4096:
            raise ValueError('max_sequence_length must be between 1 and 4096.')
        if (prompt is None) == (prompt_embeds is None):
            raise ValueError('Provide exactly one of prompt and prompt_embeds.')
        device = self._execution_device
        if prompt_embeds is None:
            prompt_embeds, prompt_attention_mask = self.encode_prompt(prompt, max_sequence_length)
        batch = prompt_embeds.shape[0]
        def prepare(embeds, mask):
            if embeds.ndim != 3 or embeds.shape[0] != batch:
                raise ValueError('Prompt embeddings must be [B, S, C] with matching batch size.')
            if mask is None:
                mask = torch.ones(embeds.shape[:2], dtype=torch.bool, device=embeds.device)
            return (embeds.to(device).repeat_interleave(num_images_per_prompt, 0),
                    mask.to(device).repeat_interleave(num_images_per_prompt, 0))
        positive, positive_mask = prepare(prompt_embeds, prompt_attention_mask)
        do_cfg = guidance_scale != 1.0
        if do_cfg:
            if negative_prompt_embeds is None:
                negatives = [negative_prompt] * batch if isinstance(negative_prompt, str) else negative_prompt
                if len(negatives) != batch:
                    raise ValueError('negative_prompt must match prompt batch size.')
                negative_prompt_embeds, negative_prompt_attention_mask = self.encode_prompt(negatives, max_sequence_length)
            negative, negative_mask = prepare(negative_prompt_embeds, negative_prompt_attention_mask)
        shape = (batch * num_images_per_prompt, self.transformer.config.latent_channels, height // 16, width // 16)
        if latents is None:
            # Sample in fp32: promoting bf16 noise afterward cannot restore its precision.
            latents = randn_tensor(shape, generator=generator, device=device, dtype=torch.float32)
        elif tuple(latents.shape) != shape:
            raise ValueError(f'latents must have shape {shape}.')
        else:
            latents = latents.to(device=device, dtype=torch.float32)
        self.scheduler.set_timesteps(num_inference_steps, device=device)
        num_steps = len(self.scheduler.timesteps)
        for step_index, timestep in enumerate(self.progress_bar(self.scheduler.timesteps)):
            skip_active = slg_start <= step_index / num_steps < slg_stop
            # CFG must combine fp32 predictions, even when the DiT runs in bf16.
            # Converting only the combined velocity would preserve rounding errors.
            prediction = self.transformer(latents, timestep, positive, positive_mask).sample.float()
            conditional = prediction
            if do_cfg:
                skip_kwargs = {'skip_layers': uncond_skip_layers} if uncond_skip_layers and skip_active else {}
                uncond = self.transformer(latents, timestep, negative, negative_mask, **skip_kwargs).sample.float()
                prediction = uncond + guidance_scale * (prediction - uncond)
            if slg_layers and slg_scale > 0 and skip_active:
                skipped = self.transformer(
                    latents, timestep, positive, positive_mask, skip_layers=slg_layers).sample.float()
                if not do_cfg:
                    prediction = skipped
                prediction = prediction + slg_scale * (conditional - skipped)
            latents = self.scheduler.step(prediction, timestep, latents, return_dict=False)[0]
        if output_type == 'latent':
            images = latents
        else:
            if self.vae is None:
                raise ValueError('A FLUX.2 VAE is required for decoded output.')
            z = latents.to(dtype=self.vae.dtype)
            bn = self.vae.bn
            eps = self.vae.config.batch_norm_eps
            z = z * (bn.running_var + eps).sqrt().to(z).view(1, -1, 1, 1) + bn.running_mean.to(z).view(1, -1, 1, 1)
            b, c, h, w = z.shape
            z = z.reshape(b, c // 4, 2, 2, h, w).permute(0, 1, 4, 2, 5, 3).reshape(b, c // 4, 2 * h, 2 * w)
            images = self.vae.decode(z, return_dict=False)[0].clamp(-1, 1)
            images = self.image_processor.postprocess(images, output_type=output_type)
        self.maybe_free_model_hooks()
        return ImagePipelineOutput(images=images) if return_dict else (images,)
