GERONIMOBLOG

How NVIDIA's Sana Generates Images

Autoencoder, diffusion transformer, and the leap to 4K

Modern text-to-image models like DALL-E, Stable Diffusion, and Midjourney have revolutionized how we create images. Recently, NVIDIA introduced Sana, a remarkably efficient text-to-image model that achieves high-quality results with significantly fewer parameters than its competitors.

Left: Generation results of Sana 1.6B. Source: Figure 1a, Sana paper. Right:
Left: Generation results of Sana 1.6B. Source: Figure 1a, Sana paper. Right:

In this post, we dissect Sana’s key components to understand how it works — starting with its innovative autoencoder: the Deep Compression Autoencoder (DC-AE).

This is a slow walk through code and images, click here to jump ahead to the jupyter notebook.

How Text-to-Image Models Work

Image generating diffuser models typically consist of three main components:

  1. A text encoder that understands your prompt.
  2. An autoencoder that compresses images into a compact representation.
  3. A backbone model (usually a vision transformer) that gradually transforms random noise into your desired image.

Think of it like this: The text encoder is your translator, the autoencoder is your compression tool, and the backbone is the artist doing the actual drawing.

Why Do We Need Autoencoders?

Working with full-resolution images (say, 512x512 pixels) is computationally expensive. That’s where autoencoders (AEs) come in — they compress images into a more manageable form (called latent space) where the actual image generation happens. After generation, the AE decompresses the result back into a full image.

Traditional autoencoders like the one used in Stable Diffusion achieve 8x spatial compression. Sana’s Deep Compression Autoencoder (DC-AE) is an innovation by the HAN lab and pushes this further, offering spatial compression ratios of 32x, 64x, and even 128x.

Spatial compression ratio? For example, a compression ratio of 64x would transform an input image of 1024x1024 pixels to a latent representation of only 16x16 (pixels?) while maintaining image quality. Well, mostly. Let’s take a look.

Meet the DC-AE Family

The DC-AE model comes in several variants:

DC-AE weights are accessible on HuggingFace.
DC-AE weights are accessible on HuggingFace.

Each variant produces different latent representations of an image.

Spatial compression ratios:

Training variants:

Side note: While Sana has a terrible licence, it seems that its DC-AE is licensed under Apache 2.

Let’s Test Drive DC-AE!

I’ve created a simple Python script to test these autoencoders.

Input image on the very left, remaining images are the results of encoding and decoding the input image with one of the DC-AE variants.
Input image on the very left, remaining images are the results of encoding and decoding the input image with one of the DC-AE variants.

Requirements

The code below runs on an M3 Macbook with 24 GB RAM but will also work on NVIDIA GPUs and less VRAM.

Make sure to install the latest version of HuggingFace diffusers:

pip install -U diffusers

Load the Autoencoder

from diffusers import AutoencoderDC
import torch

device="mps" # change to 'cuda' for NVIDIA GPUs

# Load the DC-AE f32c32-sana in 16bit precision
ae=AutoencoderDC.from_pretrained("mit-han-lab/dc-ae-f32c32-sana-1.0-diffusers", torch_dtype=torch.float16).to(device).eval()

Prepare an Image

from PIL import Image
from io import BytesIO
import requests

URL="https://github.com/geronimi73/3090_shorts/blob/main/assets/3.jpg?raw=True"

image = Image.open(BytesIO(requests.get(URL).content))

That’s a random 1000x665px image with 3 color channels (Red, Green, Blue).

To save VRAM, we resize and center-crop 512x512 pixels and normalize the pixel values to 0–1.

import torchvision.transforms as T

transform = T.Compose([
    T.Resize(512, antialias=True),
    # This will center crop to 512x512 from the resized image
    T.CenterCrop(512),
    T.ToTensor(),
    T.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
    lambda x: x.to(dtype=torch.float16)
])

image_tensor = transform(image)[None].to(device)

The result is a tensor of size torch.Size([1, 3, 512, 512])(dimensions: color channels, width, height). image_tensor will be the input for the encoder.

Encode: Transform Image to Latent Representation

with torch.no_grad():
    latent = ae.encode(image_tensor)

We are using the f32c32 encoder which downsizes height and width 32 times and produces latents with 32 channels. Our image had 512x512 pixels (and 3 channels), the latent therefore has a size of torch.Size([1, 32, 16, 16]).

That’s a considerable compression! You can think about it like this: The input image of the two foxes (512x512x3) is basically a list of 786,432 numbers. The latent representation of that image is only 8,192 numbers (32x16x16).

You could also think of the latent representation being a 16x16 image with 32 channels:

Two foxes in latent space. Transported with dc-ae-f32c32-sana.
Two foxes in latent space. Transported with dc-ae-f32c32-sana.

So it’s a lot less pixels but more information per pixel. Only god knows what each of these channels mean to the autoencoder.

Decode

Let’s bring the foxes back from latent space.

from torchvision.utils import save_image

with torch.no_grad():
    image_out = ae.decode(latent.latent).sample

image_out = torch.clamp_(image_out, -1, 1)    # clamp, because output of is AE sometimes out of [-1;1] for some reason
save_image(image_out * 0.5 + 0.5, "demo_dc_ae.png") # normalize to 0-1
Two foxes back from latent space.
Two foxes back from latent space.

Honestly, it blows my mind how this little information represented in the latent is transformed back into such a rich image.

Results

Let’s process a few more images and look at the results. Here is a gallery of test images showing the compression quality of each AE variant.

Input image on the very left, other images are results of encoding and decoding with the stated DC-AE variant.
Input image on the very left, other images are results of encoding and decoding with the stated DC-AE variant.

While playing with these autoencoders, I noticed they sometimes struggle with tiny details — small text gets a bit mushy, and cat’s eyes don’t always come out quite right.

Kittens before and after going to latent space with Deep Compression Autoencoder (dc-ae-f32c32-sana-1.0)
Kittens before and after going to latent space with Deep Compression Autoencoder (dc-ae-f32c32-sana-1.0)

But hey, don’t just take my word for it! I’m sharing the code so you can experiment yourself — your observations with different images might vary, and that’s part of understanding these models’ real-world capabilities.

All code used for the autoencoder is available in a jupyter notebook. The tests were run on an M3 MacBook using PyTorch’s MPS backend with PyTorch 2.5.1 and Diffusers 0.32.1.

Sana’s Diffusion Transformer

Let’s look at Sana’s specification as presented in their paper:

To reduce computational complexity, we replace the traditional softmax attention with ReLU linear attention

and

To further improve training efficiency, we replace the original MLP-FFN with Mix-FFN. The Mix-FFN consists of an inverted residual block, a 3×3 depth-wise convolution, and a Gated Linear Unit (GLU)

Figure 5b, Sana paper
Figure 5b, Sana paper

Furthermore, they found that the architecture could be further simplified:

We are surprised that we can remove Positional Embedding without any loss in performance .. In contrast to previous DiT-based methods that mostly use absolute PE, learnable PE, and RoPE, we propose NoPE, the first design that entirely omits positional embedding in DiT.

Finally, they use custom kernels:

we use Triton (Tillet et al., 2019) to fuse kernels for both the forward and backward passes of the linear attention blocks to speed up training and inference

Architecture details

Sana comes in two sizes: 0.6B and 1.6B parameters.

Our Sana-0.6B only contains 590M parameters, and the number of layers and channels is almost identical to those of the original DiT-XL and PixArt-Σ. Our Sana-1.6B increases the parameters to 1.6B, with 20 layers and 2240 channels per layer, and increases the channels to 5600 in FFN. We believe that keeping the model layers between 20 and 30 strikes a good balance between efficiency and quality

To sum up, the diffusion model used in Sana is an optimized version of DiT-XL it seems.

EfficientViT Blocks

Sana is the product of a collaboration between NVIDIA and the HAN lab at MIT, the last author on the Sana paper is Song Han. On the website they state that Sana is built on top of components published by the HAN lab (under the permissive Apache-2.0 license):

[2024/10/21] DC-AE and EfficientViT block are used in our latest text-to-image diffusion model SANA! Check the project page for more details.

Let’s have a closer look at the Sana components in torch:

from diffusers import SanaTransformer2DModel
import torch

transformer = SanaTransformer2DModel.from_config(
    "Efficient-Large-Model/Sana_600M_512px_diffusers", 
    subfolder="transformer", 
    torch_dtype=torch.bfloat16,
)
SanaTransformer2DModel(
  (patch_embed): PatchEmbed(
    (proj): Conv2d(32, 1152, kernel_size=(1, 1), stride=(1, 1))
  )
  (time_embed): AdaLayerNormSingle(
    (emb): PixArtAlphaCombinedTimestepSizeEmbeddings(
      (time_proj): Timesteps()
      (timestep_embedder): TimestepEmbedding(
        (linear_1): Linear(in_features=256, out_features=1152, bias=True)
        (act): SiLU()
        (linear_2): Linear(in_features=1152, out_features=1152, bias=True)
      )
    )
    (silu): SiLU()
    (linear): Linear(in_features=1152, out_features=6912, bias=True)
  )
  (caption_projection): PixArtAlphaTextProjection(
    (linear_1): Linear(in_features=2304, out_features=1152, bias=True)
    (act_1): GELU(approximate='tanh')
    (linear_2): Linear(in_features=1152, out_features=1152, bias=True)
  )
  (caption_norm): RMSNorm()
  (transformer_blocks): ModuleList(
    (0-27): 28 x SanaTransformerBlock(
      (norm1): LayerNorm((1152,), eps=1e-06, elementwise_affine=False)
      (attn1): Attention(
        (to_q): Linear(in_features=1152, out_features=1152, bias=False)
        (to_k): Linear(in_features=1152, out_features=1152, bias=False)
        (to_v): Linear(in_features=1152, out_features=1152, bias=False)
        (to_out): ModuleList(
          (0): Linear(in_features=1152, out_features=1152, bias=True)
          (1): Dropout(p=0.0, inplace=False)
        )
      )
      (norm2): LayerNorm((1152,), eps=1e-06, elementwise_affine=False)
      (attn2): Attention(
        (to_q): Linear(in_features=1152, out_features=1152, bias=True)
        (to_k): Linear(in_features=1152, out_features=1152, bias=True)
        (to_v): Linear(in_features=1152, out_features=1152, bias=True)
        (to_out): ModuleList(
          (0): Linear(in_features=1152, out_features=1152, bias=True)
          (1): Dropout(p=0.0, inplace=False)
        )
      )
      (ff): GLUMBConv(
        (nonlinearity): SiLU()
        (conv_inverted): Conv2d(1152, 5760, kernel_size=(1, 1), stride=(1, 1))
        (conv_depth): Conv2d(5760, 5760, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), groups=5760)
        (conv_point): Conv2d(2880, 1152, kernel_size=(1, 1), stride=(1, 1), bias=False)
      )
    )
  )
  (norm_out): LayerNorm((1152,), eps=1e-06, elementwise_affine=False)
  (proj_out): Linear(in_features=1152, out_features=32, bias=True)
)

Make sure to use the latest version of diffusers, I used 0.32.1 here.

This is one of the 28 transformer blocks used in Sana 0.6B:

transformer.transformer_blocks[0]
SanaTransformerBlock(
  (norm1): LayerNorm((1152,), eps=1e-06, elementwise_affine=False)
  (attn1): Attention(
    (to_q): Linear(in_features=1152, out_features=1152, bias=False)
    (to_k): Linear(in_features=1152, out_features=1152, bias=False)
    (to_v): Linear(in_features=1152, out_features=1152, bias=False)
    (to_out): ModuleList(
      (0): Linear(in_features=1152, out_features=1152, bias=True)
      (1): Dropout(p=0.0, inplace=False)
    )
  )
  (norm2): LayerNorm((1152,), eps=1e-06, elementwise_affine=False)
  (attn2): Attention(
    (to_q): Linear(in_features=1152, out_features=1152, bias=True)
    (to_k): Linear(in_features=1152, out_features=1152, bias=True)
    (to_v): Linear(in_features=1152, out_features=1152, bias=True)
    (to_out): ModuleList(
      (0): Linear(in_features=1152, out_features=1152, bias=True)
      (1): Dropout(p=0.0, inplace=False)
    )
  )
  (ff): GLUMBConv(
    (nonlinearity): SiLU()
    (conv_inverted): Conv2d(1152, 5760, kernel_size=(1, 1), stride=(1, 1))
    (conv_depth): Conv2d(5760, 5760, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), groups=5760)
    (conv_point): Conv2d(2880, 1152, kernel_size=(1, 1), stride=(1, 1), bias=False)
  )
)
Sana architecture overview. Source: Figure 5, Sana paper
Sana architecture overview. Source: Figure 5, Sana paper

Attention is performed two times, attn1 is self-attention and attn2 is cross-attention with the encoded text prompt (=output of Gemma 2 2B).

# https://github.com/huggingface/diffusers/blob/fdcbbdf0bb4fb6ae3c2b676af525fced84aa9850/src/diffusers/models/transformers/sana_transformer.py#L107

# 1. Self Attention
self.norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=norm_eps)
self.attn1 = Attention(
    query_dim=dim,
    heads=num_attention_heads,
    dim_head=attention_head_dim,
    dropout=dropout,
    bias=attention_bias,
    cross_attention_dim=None,
    processor=SanaLinearAttnProcessor2_0(),
)

# 2. Cross Attention
if cross_attention_dim is not None:
    self.norm2 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine, eps=norm_eps)
    self.attn2 = Attention(
        query_dim=dim,
        cross_attention_dim=cross_attention_dim,
        heads=num_cross_attention_heads,
        dim_head=cross_attention_head_dim,
        dropout=dropout,
        bias=True,
        out_bias=attention_out_bias,
        processor=AttnProcessor2_0(),
    )

# 3. Feed-forward
self.ff = GLUMBConv(dim, dim, mlp_ratio, norm_type=None, residual_connection=False)

Image Generation

Diffusion models work by turning noise into images. This is an iterative process, noise is removed in a stepwise manner to turn pure noise into an image.

The process looks like this:

The purpose of the Diffusion Transformer is to predict the level of noise in a given image.

Let’s step through this process in torch

Generate a noisy latent

The Deep Compression AutoEncoder (DC-AE) variant of Sana dc-ae-f32c32-sana-1.0 works with 32 channels and a compression factor of 32, as covered above.

Let’s generate a 1024x1024px image. The latents would therefore have the dimensions [32, 32, 32] (=32 channels, 1024/8 width, 1024/8 height).

We want the noise to be normal distributed and use randn:

import torch 

# first dimension is the batch size=1
latents = torch.randn(1, 32, 32, 32)

If we would decode this noisy latent right away, the image would look like this:

import torch
import torchvision.transforms as T
from diffusers import AutoencoderDC

def decode_latent(latent, ae):
    with torch.no_grad():
        image_out = ae.decode(latent).sample.squeeze().to("cpu")    
    image_out = torch.clamp_(image_out, -1, 1)    # clamp, because output of is AE sometimes out of [-1;1] for some reason
    image_out = image_out * 0.5 + 0.5 # normalize to 0-1    
    return T.ToPILImage()(image_out.float())

model = "Efficient-Large-Model/Sana_600M_1024px_diffusers"
dcae = AutoencoderDC.from_pretrained(model, subfolder="vae", torch_dtype=torch.bfloat16).to(device)

# DC-AE scaling factor, see https://huggingface.co/Efficient-Large-Model/Sana_600M_1024px_diffusers/blob/main/vae/config.json
dcae_scalingf = 0.41407

decode_latent(latents.to(torch.bfloat16).to(device) / dcae_scalingf, dcae)
Random noise. This is how it starts.
Random noise. This is how it starts.

Let’s turn this noise into an image!

As I mentioned, the noise removal process is guided by the Diffusion Transformer. But the DiT needs more than just the noisy latent. We have to somehow tell the DiT what kind of image to generate. This is why we need a text encoder.

Meet Gemma, the Text Encoder!

To guide the diffusion process, a prompt like “a beautiful mountain landscape” is also sent to latent space as an additional input to the DiT.

Sana uses Google’s Gemma 2, a large language model (LLM), to turn prompts into a meaningful representation.

We first tokenize the prompt and then encode it.

from transformers import Gemma2Model, GemmaTokenizerFast

model = "Efficient-Large-Model/Sana_600M_1024px_diffusers"

tokenizer = GemmaTokenizerFast.from_pretrained(
  model, 
  subfolder="tokenizer", 
  torch_dtype=torch.bfloat16
)

text_encoder = Gemma2Model.from_pretrained(
  model, 
  subfolder="text_encoder", 
  torch_dtype=torch.bfloat16
).to(device)

Note: Don’t forget to set device, to mps in case you are on Apple Silicon or to cuda if you work with an NVIDIA GPU.

Sana accepts text encodings of a fixed length of 300 tokens which is why we truncate and pad:

prompt = "a beautiful mountain landscape"
tokenizer.padding_side = "right"
prompt_tok = tokenizer(
    prompt, 
    padding="max_length", 
    truncation=True, 
    max_length=300, 
    add_special_tokens=True,
    return_tensors="pt", 
).to(device)

“a beautiful mountain landscape” tokenized looks like this:

tensor([[     2, 235250,   4964,   8180,  15487, 235269,   2686,  54385, .. ]])

Let’s put that through the LLM:

with torch.no_grad():
    prompt_encoded=text_encoder(**prompt_tok)
BaseModelOutputWithPast(last_hidden_state=tensor([[[-0.1543, -0.0226,  0.3066,  ..., -1.9141, -0.1387, -0.6641],
         [ 0.4473,  1.1641, -1.0703,  ...,  2.3125,  0.2832, -0.6914],
         [ 1.3984,  0.1582, -0.0962,  ...,  0.3809, -0.9453, -1.6172],
         ...,
         [ 1.8594,  0.4160, -1.1094,  ..., -1.6250,  0.4473, -1.3359],
         [ 1.8906,  0.4902, -1.0703,  ..., -1.6094,  0.4121, -1.2891],
         [ 1.9141,  0.5234, -1.0469,  ..., -1.6094,  0.4512, -1.2656]]],
       device='mps:0', dtype=torch.bfloat16), past_key_values=HybridCache(), hidden_states=None, attentions=None)

LLMs predict the next token in a sequence of tokens. But we don’t care about the next token, we want the token embeddings enriched with all the semantic information in the prompt. This information is stored in last_hidden_state of the response (the BaseModelOutputWithPast object).

The tensor last_hidden_state is of shape [1, 300, 2304] where 1 is the batch size (we only have a single prompt), 300 is the number of tokens (fixed after truncate and pad), and 2304 is the hidden dimension of Gemma 2.

Let’s feed this encoded prompt into the Diffusion Transformer.

Note: Actually the hidden dimension of the DiT is 1152, and not 2304. The PixArtAlphaTextProjection takes care of this “translation”, it is a projection module which uses two linear layers and an activation function to translate Gemma’s encoded prompt into the latent space of the DiT.

Removing a bit of noise

# First denoising step out of a total of 10 

latents = torch.randn(1, 32, 32, 32, dtype=torch.bfloat16, generator = torch.manual_seed(32)).to(device)

# Schedule 10 total steps
scheduler.set_timesteps(10)
timesteps = scheduler.timesteps.to(device).to(torch.bfloat16)

# Working in a jupyter notebook which is why I use display()
display(decode_latent(latents / dcae_scalingf, dcae).resize((512,512)))

# Predict noise
with torch.no_grad():
    noise_pred = transformer(
        latents, 
        encoder_hidden_states=prompt_encoded.last_hidden_state, 
        timestep=timesteps[0].unsqueeze(0), 
        encoder_attention_mask=prompt_tok.attention_mask, 
        return_dict=False
    )[0]

# Remove predicted noise
latents = scheduler.step(noise_pred, t, latents, return_dict=False)[0]

display(decode_latent(latents / dcae_scalingf, dcae).resize((512,512)))
Left: Noisy latent. Right: After 1 out of 10 steps of noise removal.
Left: Noisy latent. Right: After 1 out of 10 steps of noise removal.

Not much happening. Let’s look at the remaining 9 steps:

Steps 2 through 10 of noise removal.
Steps 2 through 10 of noise removal.

Fascinating how this landscape slowly emerges from random noise!

Animated steps.
Animated steps.

Note: This is just a very basic implementation of what SanaPipeline does without the guidance or the complex human instructions (CHI) feature.

The latent representation can be thought of as an image with height and width of 32 and 32 “colors” (channels) instead of the three colors (red, green, blue) we are used to. Let’s take a look at what the final latent of the mountain landscape looks like.

import matplotlib.pyplot as plt

def show_latent(latent):
    fig, axes = plt.subplots(4, 8, figsize=(20, 10))
    
    for i, ax in enumerate(axes.flat):
        channel = latent[0, i].detach().cpu().to(torch.float32)
        channel_min, channel_max = channel.min(), channel.max()
        channel_normalized = (channel - channel_min) / (channel_max - channel_min)
        
        ax.imshow(channel_normalized, cmap='viridis')
        ax.axis('off')
        ax.set_title(f'Ch {i+1}')

show_latent(latents_intermediate[-1])
Left: Visualization of each channel of the denoised latent. Right: Same latent decoded by DC-AE.
Left: Visualization of each channel of the denoised latent. Right: Same latent decoded by DC-AE.

Summary: DC-AE and DiT

We’ve now walked through both halves of Sana: the Deep Compression Autoencoder that squeezes images down to a fraction of their pixel count, and the linear-attention Diffusion Transformer that turns noise into a meaningful latent — optimized through ReLU linear attention, Mix-FFN, and their “NoPE” approach that removes positional embeddings entirely. Starting with pure random noise, we watched the model gradually carve out a mountain landscape through repeated cycles of noise prediction and removal.

Scaling Up to 4K

The Sana team just released models that can generate images up to 4096×4096 pixels. And here’s the kicker: it can do this in about 40 seconds on a regular GPU like an RTX 4090 with 24 GB VRAM. That’s pretty fast for such high-resolution images.

Examples

Left: Sana 1k model, Right: 4k model. Top row: generated image. Bottom row: Center crop. Prompt: Photo of a ultra realistic sailing ship, dramatic light, pale sunrise, cinematic lighting, battered, low angle, trending on artstation, 4k, hyper realistic, focused, extreme details, unreal engine 5, cinematic, masterpiece, art by studio ghibli, intricate artwork by john william turner. Full size images at g-ronimo/sana_samples_1–4k
Left: Sana 1k model, Right: 4k model. Top row: generated image. Bottom row: Center crop. Prompt: Photo of a ultra realistic sailing ship, dramatic light, pale sunrise, cinematic lighting, battered, low angle, trending on artstation, 4k, hyper realistic, focused, extreme details, unreal engine 5, cinematic, masterpiece, art by studio ghibli, intricate artwork by john william turner. Full size images at g-ronimo/sana_samples_1–4k
A fantasy treehouse designed by Gaudi and Hundertwasser, wide-angle view.
A fantasy treehouse designed by Gaudi and Hundertwasser, wide-angle view.
candid photo of a teenage girl with short curly red hair, intricate jewelry, holding a bouquet of flowers, interacting with nature, High angle view, on a rocky edge of a cliff overlooking a misty forested valley at dawn, Cinematic lighting with moody ambiance, From above in a high angle, Lumix GH5, in style of by Vivian Maier
candid photo of a teenage girl with short curly red hair, intricate jewelry, holding a bouquet of flowers, interacting with nature, High angle view, on a rocky edge of a cliff overlooking a misty forested valley at dawn, Cinematic lighting with moody ambiance, From above in a high angle, Lumix GH5, in style of by Vivian Maier
lifestyle photo of a young man wearing a suit and tie, striking tattoos, standing in front of a vintage car, striking a confident pose, High angle view, on a serene beach at golden hour, Chiaroscuro with dramatic shadows, Eye level, Bolex H16, in style of by Diane Arbus
lifestyle photo of a young man wearing a suit and tie, striking tattoos, standing in front of a vintage car, striking a confident pose, High angle view, on a serene beach at golden hour, Chiaroscuro with dramatic shadows, Eye level, Bolex H16, in style of by Diane Arbus
lifestyle photo of a elderly man with a beard and glasses, intricate jewelry, playing a musical instrument, in mid-dance move, High angle view, amidst a field of wildflowers under a clear blue sky, Golden hour light casting a warm glow, Dutch angle, Aaton LTR, in style of by Steve McCurry
lifestyle photo of a elderly man with a beard and glasses, intricate jewelry, playing a musical instrument, in mid-dance move, High angle view, amidst a field of wildflowers under a clear blue sky, Golden hour light casting a warm glow, Dutch angle, Aaton LTR, in style of by Steve McCurry

Find all the 150 4096x4096px images I generated here.

Notes:

How to Use It

Here’s how you can use the 4K model with the Hugging Face diffusers library.

You will need a GPU with 24 GB VRAM, in theory this could also be Apple silicon like my M3 MacBook but in practice Sana’s license prohibits inference on non-NVIDIA GPUs.

Rent a GPU

If you don’t own a GPU, rent a GeForce RTX 4090 on the cloud computing platform RunPod for 0.34$ per hour. If you don’t have a RunPod account yet, use my referral link for signup to support my tinkering.

Deploy on RunPod. Go to RunPod Console -> Click Deploy -> Choose Community Cloud (cheaper) -> Choose RTX 4090 -> Deploy at very bottom of page.
Deploy on RunPod. Go to RunPod Console -> Click Deploy -> Choose Community Cloud (cheaper) -> Choose RTX 4090 -> Deploy at very bottom of page.
Control your pod. Go to RunPod Console -> Select your pod and click Connect -> Open Jupyter Lab -> Create a new Python3 notebook -> Enter commands below and click Run ▶
Control your pod. Go to RunPod Console -> Select your pod and click Connect -> Open Jupyter Lab -> Create a new Python3 notebook -> Enter commands below and click Run ▶

Install Packages

You will need the following packages, also make sure to use the latest version of diffusers :

!pip install git+https://github.com/huggingface/diffusers
!pip install transformers accelerate 

Generate 🧨

import torch
from diffusers import DiffusionPipeline

pipe = DiffusionPipeline.from_pretrained(
    "Efficient-Large-Model/Sana_1600M_4Kpx_BF16_diffusers", 
    torch_dtype=torch.bfloat16,
    variant="bf16",
)
pipe.to("cuda" if torch.cuda.is_available() else "mps" if torch.mps.is_available() else "cpu")

# attention fails with bfloat16 due to dtype mismatch
pipe.vae.to(torch.float32) 

# tiled (V)AE, otherwise OOM on 24 GB VRAM
# +increase tile_sample_min_{height,width}, default is 448 -> visible artifacts with 4x4k img
pipe.vae.enable_tiling(tile_sample_min_height=1024, tile_sample_min_width=1024)

image = pipe(
    prompt="A melting apple",
    height=4096,
    width=4096,
    guidance_scale=5.0,
)[0]

Just a few lines of code thanks to the hard work of a lot of people.

A True Team Effort

What’s particularly exciting about Sana’s development is the collaboration between different teams:

Just in the past few weeks, they’ve fixed numerous bugs, added LoRA fine-tuning support, and implemented new features like 8-bit quantization. It’s amazing to see how fast development moves when teams work together like this. Thank you!

The Fine Print

As amazing as this sounds, there are some important things to note:

License Restrictions

Limitations

Like many AI image models, Sana struggles with

Conclusion

Despite the limitations, Sana’s 4K model is a significant achievement. The ability to generate such high-resolution images in seconds rather than minutes shows how far we’ve come in optimizing these models. While the license restrictions are unfortunate, the technical achievements here are impressive and point to an exciting future for text-to-image generation.