Multi Head Attention (MHA)

MHA MHA is the standard attention mechanism used in the original Transformer architecture. Instead of computing one attention over the entire representation space, MHA projects the input into several low-dimensional attention heads, performs attention independently in each head and then combines all output representations in to one final representation.

The important idea behind using multiple heads is that different heads can learn to attend according to different relationships (ex: syntactic dependencies, short and long-range context etc.). These is not guaranteed to happen but MHA provides separate learned subspace in which such behavior may emerge.

Each head learns three projection matrices:

which produce:

then we apply attention operation for each head as:

After computing all heads, we concatenate their outputs and project it using :

Implementation:

class MultiHeadAttentionBlock(nn.Module):
    
    def __init__(self, d_model: int, heads: int, dropout: float, rope = None):
        super().__init__()
        self.d_model = d_model
        self.h = heads
        
        assert self.d_model % self.h == 0, "d_model should be divisible by heads (h)"
        
        self.d_k = d_model // heads
        self.w_q = nn.Linear(d_model, d_model) # Wq
        self.w_k = nn.Linear(d_model, d_model) # Wk
        self.w_v = nn.Linear(d_model, d_model) # Wv
        
        self.w_o = nn.Linear(d_model, d_model) # Wo
        
        self.rope = rope
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, q, k, v, mask):
        query = self.w_q(q)  ## (B,S,d) --> (B,S,d)
        key = self.w_k(k)    ## (B,S,d) --> (B,S,d)
        value = self.w_v(v)  ## (B,S,d) --> (B,S,d)
        
        ## (B,S,d) --> (B,S,h,d_k) --> (B,h,S,d_k)
        query = query.view(query.shape[0], query.shape[1], self.h, self.d_k).transpose(1, 2)
        key = key.view(key.shape[0], key.shape[1], self.h, self.d_k).transpose(1, 2) 
        value = value.view(value.shape[0], value.shape[1], self.h, self.d_k).transpose(1, 2) 
        
        
        if self.rope is not None:
            query = self.rope(query)
            key = self.rope(key)
        
        x, _ = MultiHeadAttentionBlock.attention(query, key, value, mask, self.dropout)
        x = x.transpose(1, 2).contiguous().view(x.shape[0], -1, self.d_model) ## (B,h,S,d_k) ---> (B,S,h,d_k) --> (B,S,d)
        
        return self.w_o(x) ## (B,S,d) --> (B,S,d)
    
    @staticmethod
    def attention(query, key, value, mask, dropout: nn.Dropout):
        d_k = query.shape[-1]
        
        attention_scores = (query @ key.transpose(-2, -1)) / math.sqrt(d_k) ## (B,h,S,d_k) * (B,h,d_k,S) --> (B,h,S,S)
        if mask is not None:
            # dtype-aware: a hardcoded -1e9 becomes -inf in fp16/AMP and yields NaNs.
            attention_scores.masked_fill_(mask == 0, torch.finfo(attention_scores.dtype).min)
        
        attention_scores = attention_scores.softmax(dim=-1) ## (B,h,S,S)
        
        if dropout is not None:
            attention_scores = dropout(attention_scores)
        
        return (attention_scores @ value), attention_scores ## (B,h,S,S) * (B,h,S,d_k) --> (B,h,S,d_k)

Gains:

  • Parallelization
  • Long distance dependency

Drawbacks:

  • Quadratic computation in self-attention
  • Redundant heads
  • Slow inference - Large KV cache KV cache

Longformer Attention

To address the quadratic computation limitation of standard Transformer self-attention LongFormer architecture was proposed. LongFormer replaces full attention with sparse attention pattern. Instead of allowing every token to attend every other token, each token only attends to a carefully chosen subset of tokens.

Sliding-Window Attention

Sliding_Window

We introduce a fixed window of size . Each token only attends to tokens within a fixed local neighborhood around itself. So a token attends to on either side. So instead of computing attention over tokens we compute it for tokens.

This brings down the complexity to

Dilating Sliding-Window Attention

Dilating_Sliding_Window

Instead of selecting consecutive tokens in the local window, the model introduces a dilation i.e. tokens selected by the window are separated by gaps.

Earlier, with sliding window we attended to tokens for like:

now with dilation it becomes:

The number of attended tokens can remain approximately the same but the window now covers a large range of the sequence hence the model can access a wider contextual region without increasing the number of tokens attended to by each query.

Global Attention

Global_Attention

Both Dilated and Sliding-Window can’t provide arbitrary global connectivity hence Longformer introduces a small amount of global attending tokens.

A globally attending token can:

  • attend to every token in the sequence (row)
  • be attended to by every token in the sequence (column)

For global tokens the computational complexity is

The overall complexity of Longformer is

Drawbacks:

  • Potential for information loss
  • Task specific global attention studied only for QA, Conference Resolution, Document Classification
  • May not have significant improvement in tasks that require shorter local context.

Multi Query Attention (MQA)

Multi_Query_Attention MQA is a modification of MHA designed primarily to make Transformer inference more memory and bandwidth efficient. Instead of having a separate key and value projection for every head, all query heads share a single key and value head.

For a generated sequence of length , with heads and each head having dimension , the KV cache will contain elements per layer for MHA while for MQA it will be elements per layer. So we are reducing the KV cache size by when compared to MHA.

Gains:

  • Much lower memory-bandwidth requirements in the incremental setting

Drawbacks:

  • Reduced quality and capacity due to shared K & V
  • Address only reloading key-value during inference
  • Train a model just for faster inference?

Grouped Query Attention (GQA)

Grouped_Query_Attention

GQA is an intermediate form of attention between MHA and MQA. Its purpose is to reduce the memory and bandwidth cost of key-value cache during inference while attending better representational quality than MQA

The central idea is to keep multiple query heads but divide them into group and let each group of queries share only pair of key and value head. Thus, if there are query heads and KV groups, each group will contain query heads. The KV cache comes out to be instead of (MHA) so we are reducing the KV cache by a factor of

Implementation

class GroupedQueryAttentionBlock(nn.Module):
    
    def __init__(self, d_model: int, q_heads: int, kv_heads: int, dropout: float):
        super().__init__()
        assert d_model % q_heads == 0, "d_model should be divisible by q_heads"
        assert q_heads % kv_heads == 0, "q_heads should be divisible by kv_heads"
        
        self.d_model = d_model
        self.q_heads = q_heads
        self.kv_heads = kv_heads
        self.d_k = d_model // q_heads
        self.group_size = q_heads // kv_heads
        
        self.w_q = nn.Linear(d_model, d_model) # Wq
        self.w_k = nn.Linear(d_model, self.kv_heads*self.d_k) # Wk
        self.w_v = nn.Linear(d_model, self.kv_heads*self.d_k) # Wv
        self.w_o = nn.Linear(d_model, d_model) # Wo
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, q, k, v, mask):
        query = self.w_q(q)  ## (B,S,d) --> (B,S,d)
        key = self.w_k(k)    ## (B,S,d) --> (B,S,d_k*kv_heads)
        value = self.w_v(v)  ## (B,S,d) --> (B,S,d_k*kv_heads)
        
        ## (B,S,d) --> (B,S,h,d_k) --> (B,h,S,d_k)
        query = query.view(query.shape[0], query.shape[1], self.q_heads, self.d_k).transpose(1, 2)
        
        ## (B,S,d_k*kv_heads) --> (B,S,kv_heads,d_k) --> (B,kv_heads,S,d_k)
        key = key.view(key.shape[0], key.shape[1], self.kv_heads, self.d_k).transpose(1, 2) 
        value = value.view(value.shape[0], value.shape[1], self.kv_heads, self.d_k).transpose(1, 2)
        
        key = key.repeat_interleave(self.group_size, dim=1) ## (B,kv_heads,S,d_k) --> (B,h,S,d_k)
        value = value.repeat_interleave(self.group_size, dim=1) ## (B,kv_heads,S,d_k) --> (B,h,S,d_k)
 
 
        x, _ = GroupedQueryAttentionBlock.attention(query, key, value, mask, self.dropout)
        x = x.transpose(1, 2).contiguous().view(x.shape[0], -1, self.d_model) ## (B,h,S,d_k) ---> (B,S,h,d_k) --> (B,S,d)
        
        return self.w_o(x) ## (B,S,d) --> (B,S,d)
 
    @staticmethod
    def attention(query, key, value, mask, dropout: nn.Dropout):
        d_k = query.shape[-1]
        
        attention_scores = (query @ key.transpose(-2, -1)) / math.sqrt(d_k) ## (B,h,S,d_k) * (B,h,d_k,S) --> (B,h,S,S)
        if mask is not None:
            attention_scores.masked_fill_(mask == 0, torch.finfo(attention_scores.dtype).min)
        
        attention_scores = attention_scores.softmax(dim=-1) ## (B,h,S,S)
        
        if dropout is not None:
            attention_scores = dropout(attention_scores)
        
        return (attention_scores @ value), attention_scores ## (B,h,S,S) * (B,h,S,d_k) --> (B,h,S,d_k)

Gains:

  • Quality close to MHA
  • Comparable speed to MQA

Drawbacks:

  • More complex than MHA or MQA
  • How to find the optimal number of Groups - G?
  • Assignment of KV to right group

Multi Head Latent Attention (MLA)

Multi_Head_Latent_Attention Instead of reducing key-value heads like GQA, MLA compresses the key-value information into a much lower dimensional latent representation and caches that latent representation.

The hidden representation at time is , MLA first applies a down projection:

So we get which is a compressed latent representation containing the information needed for that is cached.

To recover keys and values during inference:

The model learns a shared low-dimensional latent representation from which the representations can be generated.

The same idea is applied to compress queries :

and recovered as:

compressions is motivated by inference-time KV-cache reduction whereas compression is additionally useful for reducing training memory.

[TODO]