Hello! Welcome to your next lesson in our journey to master diffusion models.
In our previous lesson, we analyzed the crucial role of the text encoder. We established that the choice of encoder, whether it's the visual-semantic specialist CLIP or the language-savvy T5, determines what kind of numerical information the diffusion model receives. We saw how these encoders convert a text prompt into a set of embedding vectors.
Now, we'll connect the dots. The learning outcome for this lesson is to implement text conditioning using cross-attention mechanisms. We will move from the what to the how: how are these text embeddings actually injected into the U-Net to guide the denoising process from random noise to a coherent image? For you as an ML engineer, mastering this mechanism is fundamental. It's the engine of text-to-image generation, and understanding its implementation is key to building, debugging, and customizing these powerful models.
From Self-Attention to Cross-Attention
Before we can have our model "attend" to a text prompt, let's briefly revisit the concept of attention within the U-Net itself. As you know from your prior work, many U-Net architectures for diffusion use self-attention. In self-attention, each pixel's feature vector in a given layer queries all other pixel features in the same layer. This allows the model to capture long-range spatial dependencies within the image being generated—for example, ensuring that two eyes in a portrait are stylistically consistent. The Query (Q), Key (K), and Value (V) all originate from the same source: the image's intermediate feature map.
Cross-attention adapts this powerful mechanism for conditioning. Instead of the image attending to itself, the image attends to an external source of information—our text prompt. The core idea is:
- The Query (Q) is derived from the image's feature map (the
xin the U-Net). - The Key (K) and Value (V) are derived from the external context, which in our case is the sequence of text embedding vectors from the text encoder.
This allows each spatial location in the nascent image to "look at" the entire text prompt and decide which words or concepts are most relevant to it, pulling in that information to guide its denoising.
To build a strong intuition for this process, let's watch a video that provides an excellent visual breakdown.
Cross Attention | Method Explanation | Math Explained
The YouTube channel Outlier provides a very clear, visual explanation of how self-attention works and how it is modified to become cross-attention. This will give you a solid conceptual foundation.
Please watch the following two segments: Self-Attention Explained (01:09 - 07:35): Pay attention to how Q, K, and V are all derived from the input pixels and how the final output is a weighted average of pixel values. This is a great refresher. Cross-Attention Explained (07:35 - 11:12): This is the crucial part. Focus on the key change: Q still comes from the image pixels, but K and V now come from the text embeddings. Observe how this enables each pixel to calculate its relevance to each word in the prompt.
The key takeaway is this directional flow of information: the image queries the text, not the other way around. This is what allows the text to guide, or "condition," the image generation process.
Implementing the Cross-Attention Module
With the theory in place, let's move to a practical implementation. A cross-attention mechanism is typically encapsulated in its own nn.Module. We're going to examine a clean, well-structured implementation.
Decoding Stable Diffusion: A Hands-On Implementation ...
The blog post 'Decoding Stable Diffusion' provides a concise and clear implementation of a CrossAttention module in PyTorch. We'll use this as our reference implementation.
Please study the CrossAttention class implementation in the article. You don't need to read the surrounding text for now, just focus on the Python code block.
Let's dissect the code you just reviewed. It elegantly captures the concept we discussed.
class CrossAttention(nn.Module):
def __init__(self, n_heads: int, d_embed: int, d_cross: int, ...):
super().__init__()
# Q is projected from the image feature dimension (d_embed)
self.q_proj = nn.Linear(d_embed, d_embed, ...)
# K and V are projected from the context dimension (d_cross, e.g., 768 for CLIP)
self.k_proj = nn.Linear(d_cross, d_embed, ...)
self.v_proj = nn.Linear(d_cross, d_embed, ...)
self.out_proj = nn.Linear(d_embed, d_embed, ...)
# ...
def forward(self, x, y):
# x (latent): (Batch_size, Seq_Len_Q, Dim_Q) -> From U-Net
# y (context): (Batch_size, Seq_Len_KV, Dim_KV) -> From Text Encoder
input_shape = x.shape
# ... (multi-head setup) ...
q = self.q_proj(x)
k = self.k_proj(y)
v = self.v_proj(y)
# ... (transpose for multi-head attention) ...
weight = q @ k.transpose(-1, -2)
weight /= math.sqrt(self.d_head)
weight = F.softmax(weight, dim=-1)
output = weight @ v
# ... (reshape and final projection) ...
return output
Notice the critical lines:
- In
__init__,self.q_projis initialized to taked_embed(the dimension of the U-Net's internal feature maps) as input. self.k_projandself.v_projare initialized to taked_cross(the dimension of the text embeddings) as input.- In
forward(self, x, y),qis created fromx(the image features), whilekandvare created fromy(the text context).
This perfectly implements the directional attention flow we need for text-to-image generation.
Test your understanding!
In the CrossAttention module's forward(self, x, y) method, x represents the image feature map and y represents the text embeddings. If you were to swap them and call it as forward(y, x), what would that conceptually represent? Would this be a useful mechanism for text-to-image generation?
Show answer
Conceptually, forward(y, x) would mean that each word token in the text prompt (y) is forming a query to "attend to" the spatial features of the image (x). Each word would be asking, "Which parts of the image are most relevant to me?"
This mechanism is not useful for text-to-image generation. For text-to-image, we need the image generation process to be guided by the text. The image features need to query the text for guidance.
However, this swapped attention flow is precisely what you would use for a task like image captioning. In that scenario, the model processes an image and generates text, so each generated word needs to be grounded in the visual content of the image.
Integrating Cross-Attention into the U-Net
Now that we have our CrossAttention module, the next step is to integrate it into the U-Net architecture. Typically, this is done by adding a cross-attention layer within the main building blocks (e.g., ResNet or Transformer blocks) of the U-Net.
The following video provides a complete, from-scratch walkthrough of adding class, spatial, and finally, text conditioning to a latent diffusion model. We'll focus on the text conditioning part.
Stable Diffusion from Scratch in PyTorch | Conditional Latent Diffusion Models
The 'ExplainingAI' video 'Stable Diffusion from Scratch' is a fantastic resource that details the full implementation process. We'll focus on the sections where cross-attention is explained and then integrated into the U-Net.
Please watch these two key segments: Cross-Attention Theory (31:22 - 39:14): This will be a quick reinforcement of the concept, but this time framed specifically within the context of the U-Net architecture we're about to modify. Code Implementation (39:14 - 47:16): This is the core of the lesson. Pay close attention to: The changes in the config file to include text conditioning parameters. How the DownBlock (and by extension, other blocks) is modified to optionally create and use a CrossAttention layer. How the context (the text embeddings) is passed into the forward method of the main U-Net and then threaded down into the individual blocks. The changes in the training script to load a text encoder (like BERT or CLIP) and prepare the context embeddings for the model.
As the video demonstrates, the integration involves a few key steps:
- Modify the U-Net Blocks: The building blocks of your U-Net (often called
ResnetBlockorDownBlock/UpBlock) are modified. Alongside the existingResnetandSelfAttentionlayers, you add aCrossAttentionlayer. - Update Forward Passes: The
forwardmethod of these blocks is updated to accept an additionalcontextargument. Thiscontextis passed to the cross-attention layer. - Thread Context Through U-Net: The main
Unet'sforwardmethod is also updated to accept thecontext. It then passes this context down to each of its blocks during the forward pass. - Prepare Context in Training Loop: Before calling the U-Net, your training script is responsible for taking the text prompts, using the pre-trained text encoder to convert them into embedding vectors, and then passing these vectors as the
contextargument to the U-Net.
The MinImagen article provides another great visualization of how the conditioning vectors are prepared and used. The diagram under the "Generating Text Conditioning Vectors" section (in resource LINK, which you can refer to in the repo) clearly shows the text embeddings (text_embeds) being processed and concatenated with time tokens to form the final context tensor that is fed into the cross-attention layers.
Conclusion
Today, we've bridged the gap between language and pixels. You have learned how to implement the core mechanism that allows a diffusion model to understand and generate images from text prompts.
Key Takeaways:
- Cross-Attention is the Mechanism for Conditioning: It adapts the self-attention paradigm to allow the model to incorporate external information.
- Directional Information Flow: For text-to-image, the image features (Query) must attend to the text embeddings (Key and Value) to receive guidance.
- Architectural Integration: Cross-attention modules are typically added into the main blocks of the U-Net (e.g., alongside ResNet and self-attention layers).
- Implementation Steps: The process involves creating a
CrossAttentionmodule, modifying the U-Net blocks to include it, and updating theforwardpasses to thread thecontext(text embeddings) through the network.
Next Lesson Preview:
Now that we can condition our model on a prompt, the next step is to gain finer control over the output. What if we want to explicitly discourage a concept? What if we want to make the model follow the prompt more strongly? In the next lesson, we will explore powerful techniques like negative prompting and prompt re-weighting, which build directly upon the cross-attention framework you've learned today to refine and direct the creative process.