Attention and the versions of it: Layer by Layer

In the last blog, we talked about the Attention Mechanism and how it revolutionized the field of Natural Language Processing. But the vanilla Attention Mechanism has its limitations. In this blog, we will discuss the different versions of the Attention Mechanism and how they overcome these limitations.

SDPA and Multi - Head Attention
Here are some of the attention types mentioned in my last blog:
- Self Attention
- Bidirectional Attention
- Multi-Query Attention
- Grouped Query Attention
- Infini-Attention
- Sliding Window Attention
- Flash Attention
Each of these attention types has its own advantages and disadvantages. The choice of attention type depends on the problem at hand and the computational resources available. Let's discuss some of the attention types in detail.
I have written explanations of each line of the code for better understanding. If you wish to skip the explanation, you may not open the expanders.
1. Self Attention

Self Attention
Self-Attention is the bread and butter of many modern NLP models. In this mechanism, every token in a sentence can interact with every other token. It’s like a networking event where everyone gets to chat with everyone else!
Power Points & Problems
- Power: Captures global dependencies.
- Problem: Time complexity being O(n2) makes it computationally expensive for long sequences.
Code Example: Self-Attention Class
1import torch
2import torch.nn as nn
3from torch.nn import functional as F
4
5
6class SelfAttention(nn.Module):
7 """One head of self-attention."""
8
9 def __init__(self, n_embd, head_size, block_size, dropout):
10 super().__init__()
11 self.key = nn.Linear(n_embd, head_size, bias=False)
12 self.query = nn.Linear(n_embd, head_size, bias=False)
13 self.value = nn.Linear(n_embd, head_size, bias=False)
14 self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))
15 self.dropout = nn.Dropout(dropout)
16
17 def forward(self, x):
18 B, T, C = x.shape
19 k = self.key(x)
20 q = self.query(x)
21 wei = q @ k.transpose(-2, -1) * k.shape[-1] ** -0.5
22 wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf'))
23 wei = F.softmax(wei, dim=-1)
24 wei = self.dropout(wei)
25 v = self.value(x)
26 out = wei @ v
27 return outApplications: Self-Attention is used in models like Transformer and GPT-3.

Multi Head Attention
This is the implementation of the single head of the self-attention mechanism that was given in the "Attention is All You Need" paper. The same paper also mentions that when multiple heads are used, we get performance improvements. This is because each head can learn different aspects of the data. This is where the Multi-Head Attention comes into play. Multi head attention is the same as self attention but with multiple parallel heads.
2. Bidirectional Attention

Bidirectional Attention difference with Self Attention
Bidirectional Attention allows tokens to look both left and right. Imagine being at a party where you’re not just scanning the room to your left—you’re scanning every corner. This method is especially useful for capturing context from both directions.
Power Points & Problems
- Enhanced context understanding by attending to both past and future tokens.
- Problem: Can introduce noise from irrelevant future context if not handled properly.
Code Example: Bidirectional Attention Class
1class BidirectionalAttention(nn.Module):
2 def __init__(self, embed_size):
3 super(BidirectionalAttention, self).__init__()
4 self.W_q = nn.Linear(embed_size, embed_size)
5 self.W_k = nn.Linear(embed_size, embed_size)
6 self.W_v = nn.Linear(embed_size, embed_size)
7
8 def forward(self, x):
9 Q = self.W_q(x)
10 K = self.W_k(x)
11 V = self.W_v(x)
12 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(x.size(-1))
13 attention = torch.softmax(scores, dim=-1)
14 return torch.matmul(attention, V)Applications: Bidirectional Attention is used in models like BERT and T5.
3. Multi - Query Attention

Multi Query Attention
Multi-Query Attention streamlines computation by sharing keys and values across heads, while still allowing separate queries. It’s like ordering one giant pizza and letting everyone take a slice instead of making multiple orders efficient, yet flexible.
Power Points & Problems
- Reduces computation by sharing key and value matrices.
- Problem: Can lead to less diversity in attention patterns across heads.
Code Example: Multi-Query Attention Class
1class MultiQueryAttention(nn.Module):
2 def __init__(self, embed_size, heads):
3 super(MultiQueryAttention, self).__init__()
4 self.embed_size = embed_size
5 self.heads = heads
6 self.head_dim = embed_size // heads
7 self.W_q = nn.Linear(embed_size, embed_size)
8 self.W_kv = nn.Linear(embed_size, embed_size * 2)
9 self.fc_out = nn.Linear(embed_size, embed_size)
10
11 def forward(self, x):
12 batch_size, seq_length, _ = x.shape
13 Q = self.W_q(x)
14 kv = self.W_kv(x)
15 kv = kv.view(batch_size, seq_length, 2, self.embed_size)
16 K, V = kv[:, :, 0, :], kv[:, :, 1, :]
17 Q = Q.view(batch_size, seq_length, self.heads, self.head_dim).transpose(1, 2)
18 K = K.view(batch_size, seq_length, self.heads, self.head_dim).transpose(1, 2)
19 V = V.view(batch_size, seq_length, self.heads, self.head_dim).transpose(1, 2)
20 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
21 attention = torch.softmax(scores, dim=-1)
22 out = torch.matmul(attention, V)
23 out = out.transpose(1, 2).contiguous().view(batch_size, seq_length, self.embed_size)
24 return self.fc_out(out)Applications: Multi-Query Attention is used in models like PaLM and Falcon.
4. Grouped Query Attention

Grouped Query Attention
Grouped Query Attention (GQA) is an efficient variant of multi-head attention that reduces computational cost while maintaining strong performance.
Instead of each query attending to separate key-value heads, GQA groups queries together and shares key-value projections within each group. This reduces memory usage and speeds up inference, making it particularly useful for large-scale models like LLaMA and GPT variants.
GQA strikes a balance between Multi-Query Attention (MQA), which uses a single shared key-value pair, and full Multi-Head Attention (MHA), which has independent key-value pairs for each head. By leveraging grouped keys and values, GQA achieves high efficiency with minimal accuracy loss, making it widely used in modern LLMs for scalable and cost-effective deployment.
1class GroupedQueryAttention(nn.Module):
2 def __init__(self, embed_size, num_heads, num_groups):
3 super(GroupedQueryAttention, self).__init__()
4 self.embed_size = embed_size
5 self.num_heads = num_heads
6 self.num_groups = num_groups
7 self.head_dim = embed_size // num_heads
8
9 assert num_heads % num_groups == 0, "Number of heads must be divisible by number of groups"
10
11 self.W_q = nn.Linear(embed_size, embed_size)
12 self.W_kv = nn.Linear(embed_size, embed_size * 2) # Shared keys and values across groups
13 self.fc_out = nn.Linear(embed_size, embed_size)
14
15 def forward(self, x):
16 batch_size, seq_length, _ = x.shape
17
18 # Compute queries normally
19 Q = self.W_q(x)
20 Q = Q.view(batch_size, seq_length, self.num_heads, self.head_dim).transpose(1, 2) # (B, heads, L, head_dim)
21
22 # Compute shared keys and values across groups
23 kv = self.W_kv(x)
24 kv = kv.view(batch_size, seq_length, 2, self.num_groups, self.embed_size // self.num_groups)
25 K, V = kv[:, :, 0, :, :], kv[:, :, 1, :, :]
26
27 # Expand keys and values to match number of heads per group
28 heads_per_group = self.num_heads // self.num_groups
29 K = K.repeat_interleave(heads_per_group, dim=2)
30 V = V.repeat_interleave(heads_per_group, dim=2)
31
32 # Compute attention
33 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
34 attention = torch.softmax(scores, dim=-1)
35 out = torch.matmul(attention, V)
36
37 # Reshape and project output
38 out = out.transpose(1, 2).contiguous().view(batch_size, seq_length, self.embed_size)
39 return self.fc_out(out)Applications: Grouped Query Attention is used in models like LLaMA 2 - 70B and Mistral 7B.
5. Infini-Attention

Infini Attention
Infini-Attention tackles the problem of long sequences by processing infinitely long inputs with bounded memory and computation. Think of it as an attention mechanism that can handle a marathon of tokens without breaking a sweat.
This make LLM scalable to very long sequences. But the complexity of design and potential approximations might hurt precision.
Code Example: Infini-Attention Class
1class InfiniAttention(nn.Module):
2 def __init__(self,
3 seq_len: int,
4 emb_dim: int,
5 d_head: int,
6 n_head: int,
7 n_segments: int,
8 is_causal: Optional[bool] = True,
9 update: Optional[str] = 'linear',
10 device: Optional[str] = 'cpu',
11 scale:Optional[int] = 10
12 ):
13 super().__init__()
14
15 """
16 Args:
17 seq_len: Sequence length of the inputs.
18 n_segments: Number of segments (must be divisible to seq_len).
19 n_head: Number of attention heads.
20 emb_dim: Embedding dimension of the input.
21 d_head: Embedding dimension of each head.
22 is_causal: Whether the model causal or not.
23 device: cuda or cpu.
24 """
25 if update not in ['linear', 'delta']:
26 raise ValueError('Update takes only one of these parameters - linear, delta')
27
28 assert seq_len % n_segments == 0, 'seq_len must be divisible to n_segments'
29 assert emb_dim % n_head == 0, 'emb_dim must be divisible to n_head'
30
31 self.seq_len = seq_len
32 self.n_segments = n_segments
33 self.n_head = n_head
34 self.emb_dim = emb_dim
35 self.d_head = d_head
36 self.is_causal = is_causal
37 self.use_rope = use_rope
38 self.update = update
39 self.device = device
40 self.scale = scale
41 self.beta = nn.Parameter(torch.zeros((1,self.n_head,1,1))) # -> A learnable scalar from the paper.
42 self.q = nn.Linear(emb_dim, emb_dim, device=device)
43 self.k = nn.Linear(emb_dim, emb_dim, device=device)
44 self.v = nn.Linear(emb_dim, emb_dim, device=device)
45 self.o = nn.Linear(emb_dim, emb_dim, device=device)
46 self.elu = nn.ELU()
47 self.freq_cis = RoPE.compute_freq_cis(emb_dim, seq_len, 10000.0, device=device)
48 self.register_buffer('causal', torch.tril(torch.ones(seq_len // n_segments, seq_len // n_segments, device=device)))
49
50 def forward(self, x: torch.Tensor) -> torch.Tensor:
51
52 batch_size, _, _ = x.size()
53
54 #There was no guide for initialization for the parameters below, so I just initialize them fron zero.
55 memory = torch.zeros((self.n_head, self.d_head, self.d_head)).to(self.device)
56 z = torch.zeros((self.n_head, self.d_head, 1)).to(self.device)
57
58 query = self.q(x)
59 key = self.k(x)
60 value = self.v(x)
61
62 query = query.view(batch_size, self.n_head, self.n_segments, self.seq_len // self.n_segments, self.d_head)
63 key = key.view(batch_size, self.n_head, self.n_segments, self.seq_len // self.n_segments, self.d_head)
64 value = value.view(batch_size, self.n_head, self.n_segments, self.seq_len // self.n_segments, self.d_head)
65
66 output = []
67
68 for idx in range(self.n_segments):
69
70 sigma_q = self.elu(query[:, :, idx, :, :]) + 1.0
71 sigma_k = self.elu(key[:, :, idx, :, :]) + 1.0
72 A_mem = (sigma_q @ memory) / ((sigma_q @ z) + 1e-6) # Adding 1e-6 for preventing division to 0
73
74 A_dot = query[:, :, idx, :, :] @ key[:, :, idx, :, :].transpose(-2, -1)
75
76 if self.is_causal:
77 A_dot.masked_fill_(self.causal == 0, float('-inf'))
78
79 A_dot = F.softmax(A_dot / torch.sqrt(torch.tensor(self.d_head)), dim = -1)
80 A_dot = A_dot @ value[:, :, idx, :, :]
81
82 attention = (F.sigmoid(self.beta*self.scale) * A_mem) + ((1 - F.sigmoid(self.beta*self.scale)) * A_dot)
83
84 #Update
85 if self.update == 'linear':
86 memory = memory + (sigma_k.transpose(-2, -1) @ value[:, :, idx, :, :])
87 else:
88 delta = (sigma_k @ memory) / ((sigma_k @ z) + 1e-6)
89 memory = memory + (sigma_k.transpose(-2, -1) @ (value[:, :, idx, :, :] - delta))
90
91 z = z + sigma_k.sum(dim = -2, keepdim = True)
92
93 output.append(attention)
94
95 attention = torch.concat(output, dim = 2).view(batch_size, self.seq_len, self.emb_dim)
96 return self.o(attention)Credit: https://github.com/vmarinowski/infini-attention
Although Infini-Attention is a powerful tool for handling long sequences, it's not yet a widely adopted feature in mainstream LLMs. The complexity of the design and potential approximations may hinder its widespread use. However, as the demand for processing longer sequences grows, Infini-Attention could become a key component in future models.
6. Sliding Window Attention

Sliding Window Attention
Sliding Window or Local Attention restricts each token’s view to a fixed window. It’s like a gossip session where you only hear what’s happening in your immediate vicinity—not the entire neighborhood.
This method reduces the computational complexity of attention by limiting the interactions between tokens. By focusing on local context, it can efficiently handle long sequences without sacrificing performance.
Coding Example: Sliding Window Attention Class
1class SlidingWindowAttention(nn.Module):
2 def __init__(self, embed_size, window_size):
3 super(SlidingWindowAttention, self).__init__()
4 self.embed_size = embed_size
5 self.window_size = window_size
6 self.W_q = nn.Linear(embed_size, embed_size)
7 self.W_k = nn.Linear(embed_size, embed_size)
8 self.W_v = nn.Linear(embed_size, embed_size)
9
10 def forward(self, x):
11 batch_size, seq_length, _ = x.shape
12 Q = self.W_q(x)
13 K = self.W_k(x)
14 V = self.W_v(x)
15 outputs = []
16 for i in range(seq_length):
17 start = max(0, i - self.window_size)
18 end = min(seq_length, i + self.window_size + 1)
19 Q_i = Q[:, i:i+1, :]
20 K_window = K[:, start:end, :]
21 V_window = V[:, start:end, :]
22 scores = torch.matmul(Q_i, K_window.transpose(-2, -1)) / math.sqrt(x.size(-1))
23 attention = torch.softmax(scores, dim=-1)
24 outputs.append(torch.matmul(attention, V_window))
25 return torch.cat(outputs, dim=1)Sliding Window Attention is a popular choice for handling long sequences in models like Mistral 7B, Longformer.
7. Flash Attention

Flash Attention
Flash Attention is a performance-oriented reimplementation of vanilla attention. It leverages efficient memory layouts and GPU-friendly operations to calculate attention with minimal overhead. In short, it’s the speedster of the attention family—designed to give you all the power of vanilla attention but at lightning speed.
Flash Attention (and it's versions 2 and 3) are mathematically the same as vanilla attention, but implemented efficiently to reduce memory and speed up computations on GPUs.

Flash Attention Illustration in HuggingFace Documentation
Standard attention mechanism uses High Bandwidth Memory (HBM) to store, read and write keys, queries and values. HBM is large in memory, but slow in processing, meanwhile SRAM is smaller in memory, but faster in operations.
In the standard attention implementation, the cost of loading and writing keys, queries, and values from HBM is high. It loads keys, queries, and values from HBM to GPU on-chip SRAM, performs a single step of the attention mechanism, writes it back to HBM, and repeats this for every single attention step. Instead, Flash Attention loads keys, queries, and values once, fuses the operations of the attention mechanism, and writes them back.
It uses vanilla attention, but with optimizations like:
- Kernel Fusion: It fuses multiple operations (matrix multiplications, softmax, and scaling) into a single optimized GPU kernel, reducing unnecessary memory access.
- Tiling: Flash Attention divides the attention matrices into smaller blocks (tiles) to optimize memory access and computation efficiency. This allows processing the input in smaller, manageable chunks, reducing the need to store the entire attention matrix in memory.
- Recomputation: Instead of storing intermediate results, Flash Attention recomputes them as needed, minimizing memory consumption.
- Memory Efficiency: By processing in blocks and recomputing, Flash Attention reduces memory usage from quadratic to linear with respect to sequence length, making it suitable for long sequences.
- Numerical Stability: Flash Attention employs techniques like the "max trick" to maintain numerical stability during the softmax computation, ensuring accurate results even with large inputs.
- IO Complexity: Flash Attention is designed to minimize the number of accesses to GPU high bandwidth memory (HBM), making it an efficient choice for various GPU sizes.
I would like to include flash attention code over here. But then I realized that it is too long to include. So I will just include the link to the code.
Flash Attention Official Repository
Flash Attention being an efficient reimplementation of vanilla attention, it's widely used in models like Mistral 7B, LlaMA 2 - 70B.
HuggingFace's Transformers library also has an attribute to enable Flash Attention. If a model supports Flash Attention attn_implementation="flash_attention_2".
Conclusion
Phew that was a bit too much, I know! But I hope you enjoyed the journey through the world of attention mechanisms. From vanilla attention to Flash Attention, we've covered a wide range of attention variants that power modern deep learning models.
Attention mechanisms are the backbone of modern deep learning models, enabling them to capture complex relationships in data. By understanding the inner workings of attention, you can appreciate the nuances of different attention mechanisms and their impact on model performance.
Whether you're building a new model or fine-tuning an existing one, choosing the right attention mechanism can significantly affect the efficiency and effectiveness of your model. Each attention variant has its strengths and weaknesses, making it crucial to select the one that best suits your use case.
If you're interested in learning more about attention mechanisms and their applications, check out the following resources:
- Attention Is All You Need explained by Yannic Kilcher - Video
- The Illustrated Transformer by Jay Alammar - Blog
- Attention in transformers, step by step by 3Blue1Brown - Video
- Variants of Multi-head attention: Multi-query (MQA) and Grouped-query attention (GQA) - Video
- Sliding Window Attention Explained - Blog
- Flash Attention explained by Umar Jamil - Video
See you in the next blog! Until then, keep learning and exploring the fascinating world of deep learning.
If this resonated, pass it along.