Boogu-Image / boogu /models /attention_processor.py
multimodalart's picture
multimodalart HF Staff
Add Boogu-Image-0.1-Edit ZeroGPU editing app (gr.Citrus)
42f1cf6 verified
Raw
History Blame Contribute Delete
50.8 kB
import math
import warnings
from typing import List, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import repeat
from ..utils.import_utils import is_flash_attn_available
if is_flash_attn_available():
from flash_attn import flash_attn_varlen_func
from flash_attn.bert_padding import index_first_axis, pad_input, unpad_input
else:
warnings.warn(
"Cannot import flash_attn, install flash_attn to use Flash2Varlen attention for better performance"
)
from diffusers.models.attention_processor import Attention
from .embeddings import apply_rotary_emb
class BooguImageDoubleStreamSelfAttnProcessorFlash2Varlen(nn.Module):
"""
Double-stream self-attention processor with flash attention and variable length sequences.
This processor implements double-stream attention where:
- Instruction and image features are processed separately to generate QKV
- QKV are concatenated and processed together for cross-modal attention
- Uses flash attention for efficient computation
- Supports both standard and causal attention masks
Args:
head_dim: Dimension of each attention head
num_attention_heads: Number of attention heads for queries
num_kv_heads: Number of key-value heads
qkv_bias: Whether to use bias in QKV linear layers
"""
def __init__(
self,
head_dim: int,
num_attention_heads: int,
num_kv_heads: int,
qkv_bias: bool = False,
) -> None:
"""Initialize the double-stream attention processor."""
super().__init__()
if not is_flash_attn_available():
raise ImportError(
"BooguImageDoubleStreamSelfAttnProcessorFlash2Varlen requires flash_attn. "
"Please install flash_attn."
)
# Calculate dimensions
self.head_dim = head_dim
self.num_attention_heads = num_attention_heads
self.num_kv_heads = num_kv_heads
query_dim = head_dim * num_attention_heads
kv_dim = head_dim * num_kv_heads
# Initialize separate Q, K, V linear layers for instruction and image
# Query uses num_attention_heads, Key/Value use num_kv_heads
self.img_to_q = nn.Linear(query_dim, query_dim, bias=qkv_bias)
self.img_to_k = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
self.img_to_v = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
self.instruct_to_q = nn.Linear(query_dim, query_dim, bias=qkv_bias)
self.instruct_to_k = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
self.instruct_to_v = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
# Additional output projection layers for instruction and image streams
self.instruct_out = nn.Linear(query_dim, query_dim, bias=qkv_bias)
self.img_out = nn.Linear(query_dim, query_dim, bias=qkv_bias)
# Initialize weights
self.initialize_weights()
# rank, world_size, worker, num_workers = pytorch_worker_info(None)
def initialize_weights(self) -> None:
"""
Initialize the weights of the double-stream attention processor.
Uses Xavier uniform initialization for linear layers and zero initialization for biases.
"""
# Initialize image stream QKV projection layers
nn.init.xavier_uniform_(self.img_to_q.weight)
nn.init.xavier_uniform_(self.img_to_k.weight)
nn.init.xavier_uniform_(self.img_to_v.weight)
# Initialize instruction stream QKV projection layers
nn.init.xavier_uniform_(self.instruct_to_q.weight)
nn.init.xavier_uniform_(self.instruct_to_k.weight)
nn.init.xavier_uniform_(self.instruct_to_v.weight)
# Initialize separate output projection layers
nn.init.xavier_uniform_(self.instruct_out.weight)
nn.init.xavier_uniform_(self.img_out.weight)
# Initialize biases if they exist
if self.img_to_q.bias is not None:
nn.init.zeros_(self.img_to_q.bias)
nn.init.zeros_(self.img_to_k.bias)
nn.init.zeros_(self.img_to_v.bias)
nn.init.zeros_(self.instruct_to_q.bias)
nn.init.zeros_(self.instruct_to_k.bias)
nn.init.zeros_(self.instruct_to_v.bias)
nn.init.zeros_(self.instruct_out.bias)
nn.init.zeros_(self.img_out.bias)
def _upad_input(
self,
query_layer: torch.Tensor,
key_layer: torch.Tensor,
value_layer: torch.Tensor,
attention_mask: torch.Tensor,
query_length: int,
num_heads: int,
) -> Tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
Tuple[torch.Tensor, torch.Tensor],
Tuple[int, int],
]:
"""
Unpad the input tensors for flash attention.
Same implementation as BooguImageAttnProcessorFlash2Varlen.
"""
def _get_unpad_data(
attention_mask: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor, int]:
"""Helper function to get unpadding data from attention mask."""
seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32)
indices = torch.nonzero(attention_mask.flatten(), as_tuple=False).flatten()
max_seqlen_in_batch = seqlens_in_batch.max().item()
cu_seqlens = F.pad(
torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0)
)
return indices, cu_seqlens, max_seqlen_in_batch
indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data(attention_mask)
batch_size, kv_seq_len, num_key_value_heads, head_dim = key_layer.shape
# Unpad key and value layers
key_layer = index_first_axis(
key_layer.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim),
indices_k,
)
value_layer = index_first_axis(
value_layer.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim),
indices_k,
)
# Handle different query length cases
if query_length == kv_seq_len:
query_layer = index_first_axis(
query_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim),
indices_k,
)
cu_seqlens_q = cu_seqlens_k
max_seqlen_in_batch_q = max_seqlen_in_batch_k
indices_q = indices_k
elif query_length == 1:
max_seqlen_in_batch_q = 1
cu_seqlens_q = torch.arange(
batch_size + 1, dtype=torch.int32, device=query_layer.device
)
indices_q = cu_seqlens_q[:-1]
query_layer = query_layer.squeeze(1)
else:
attention_mask = attention_mask[:, -query_length:]
query_layer, indices_q, cu_seqlens_q, max_seqlen_in_batch_q = unpad_input(
query_layer, attention_mask
)
return (
query_layer,
key_layer,
value_layer,
indices_q,
(cu_seqlens_q, cu_seqlens_k),
(max_seqlen_in_batch_q, max_seqlen_in_batch_k),
)
def _concat_instruction_image_features(
self,
img_hidden_states_list: List[torch.Tensor],
instruct_hidden_states_list: List[torch.Tensor],
encoder_seq_lengths: List[int],
seq_lengths: List[int],
) -> List[torch.Tensor]:
"""
Concatenate instruction (text & image) and reference image features (instruction first, then image).
Args:
img_hidden_states_list: List of image tensors [img_query, img_key, img_value]
instruct_hidden_states_list: List of instruction tensors [instruct_query, instruct_key, instruct_value]
encoder_seq_lengths: Instruction sequence lengths for each sample [B]
seq_lengths: Total sequence lengths for each sample [B]
Returns:
List of concatenated tensors [query, key, value]
"""
assert len(img_hidden_states_list) == len(instruct_hidden_states_list), (
f"Length mismatch: img_list={len(img_hidden_states_list)}, instruct_list={len(instruct_hidden_states_list)}"
)
batch_size = img_hidden_states_list[0].shape[0]
max_seq_len = max(seq_lengths)
concatenated_list = []
for img_tensor, instruct_tensor in zip(
img_hidden_states_list, instruct_hidden_states_list
):
# Ensure tensors are on the same device
device = img_tensor.device
if instruct_tensor.device != device:
instruct_tensor = instruct_tensor.to(device)
# Create output tensor with proper shape [B, max_seq_len, feature_dim]
feature_dim = img_tensor.shape[-1]
concatenated = img_tensor.new_zeros(batch_size, max_seq_len, feature_dim)
# Concatenate instruction first, then image for each sample
for i, (encoder_seq_len, seq_len) in enumerate(
zip(encoder_seq_lengths, seq_lengths)
):
# Place instruction tokens first
concatenated[i, :encoder_seq_len] = instruct_tensor[i, :encoder_seq_len]
# Place image tokens after instruction
concatenated[i, encoder_seq_len:seq_len] = img_tensor[
i, : seq_len - encoder_seq_len
]
concatenated_list.append(concatenated)
return concatenated_list
def _split_instruction_image_features(
self,
hidden_states_list: List[torch.Tensor],
encoder_seq_lengths: List[int],
seq_lengths: List[int],
) -> List[Tuple[torch.Tensor, torch.Tensor]]:
"""
Split concatenated features back to instruction and image features.
Inverse operation of _concat_instruction_image_features.
Args:
hidden_states_list: List of concatenated tensors (usually just one element)
encoder_seq_lengths: Instruction sequence lengths for each sample [B]
seq_lengths: Total sequence lengths for each sample [B]
Returns:
List of tuples, each containing (instruct_hidden_states, img_hidden_states)
"""
result_list = []
for hidden_states in hidden_states_list:
batch_size = hidden_states.shape[0]
feature_dim = hidden_states.shape[-1]
# Get maximum lengths for instruction and image
max_instruct_len = max(encoder_seq_lengths)
max_img_len = max(
seq_len - encoder_seq_len
for seq_len, encoder_seq_len in zip(seq_lengths, encoder_seq_lengths)
)
# Create output tensors [B, max_len, feature_dim]
instruct_hidden_states = hidden_states.new_zeros(
batch_size, max_instruct_len, feature_dim
)
img_hidden_states = hidden_states.new_zeros(
batch_size, max_img_len, feature_dim
)
# Split each sample back to instruction and image
for i, (encoder_seq_len, seq_len) in enumerate(
zip(encoder_seq_lengths, seq_lengths)
):
img_len = seq_len - encoder_seq_len
# Extract instruction portion
instruct_hidden_states[i, :encoder_seq_len] = hidden_states[
i, :encoder_seq_len
]
# Extract image portion
img_hidden_states[i, :img_len] = hidden_states[
i, encoder_seq_len:seq_len
]
result_list.append((instruct_hidden_states, img_hidden_states))
return result_list
def __call__(
self,
attn: Attention,
img_hidden_states: torch.Tensor,
instruct_hidden_states: torch.Tensor,
joint_attention_mask: Optional[torch.Tensor] = None,
rotary_emb: Optional[torch.Tensor] = None,
encoder_seq_lengths: List[
int
] = None, # [B] - Instruction sequence lengths for each sample
seq_lengths: List[int] = None, # [B] - Total sequence lengths for each sample
base_sequence_length: Optional[int] = None,
) -> torch.Tensor:
"""
Process double-stream self-attention computation with flash attention.
Args:
attn: Attention module
img_hidden_states: Image hidden states tensor [B, L_img, D]
instruct_hidden_states: Instruction hidden states tensor [B, L_instruct, D]
joint_attention_mask: Combined attention mask [B, L_total]
rotary_emb: Rotary embeddings for the joint sequence
encoder_seq_lengths: Instruction sequence lengths for each sample [B]
seq_lengths: Total sequence lengths for each sample [B]
base_sequence_length: Optional base sequence length for proportional attention
Returns:
torch.Tensor: Processed hidden states after attention computation
"""
batch_size = img_hidden_states.shape[0]
L_instruct = instruct_hidden_states.shape[1]
L_img = img_hidden_states.shape[1]
# Ensure Q, K, V linear layers are on the same device as input tensors
device = img_hidden_states.device
for layer in [
self.img_to_q,
self.img_to_k,
self.img_to_v,
self.instruct_to_q,
self.instruct_to_k,
self.instruct_to_v,
self.instruct_out,
self.img_out,
]:
if (
(layer.weight.device != device)
and (str(layer.weight.device).lower() != "meta")
and (str(device).lower() not in {"meta", "auto"})
):
layer = layer.to(device)
# Generate Q, K, V for image and instruction streams (NO head reshaping yet)
img_query = self.img_to_q(img_hidden_states) # [B, L_img, query_dim]
img_key = self.img_to_k(img_hidden_states) # [B, L_img, kv_dim]
img_value = self.img_to_v(img_hidden_states) # [B, L_img, kv_dim]
instruct_query = self.instruct_to_q(
instruct_hidden_states
) # [B, L_instruct, query_dim]
instruct_key = self.instruct_to_k(
instruct_hidden_states
) # [B, L_instruct, kv_dim]
instruct_value = self.instruct_to_v(
instruct_hidden_states
) # [B, L_instruct, kv_dim]
# Use helper function to concatenate QKV (instruction first, then image)
img_list = [img_query, img_key, img_value] # [B, L_img, feature_dim] each
instruct_list = [
instruct_query,
instruct_key,
instruct_value,
] # [B, L_instruct, feature_dim] each
concatenated_list = self._concat_instruction_image_features(
img_list, instruct_list, encoder_seq_lengths, seq_lengths
)
query, key, value = concatenated_list # [B, max_seq_len, feature_dim] each
# From here, follow exactly the same logic as BooguImageAttnProcessorFlash2Varlen
sequence_length = max(seq_lengths)
query_dim = query.shape[-1]
inner_dim = key.shape[-1]
head_dim = query_dim // attn.heads
dtype = query.dtype
# Get key-value heads
kv_heads = inner_dim // head_dim
# Reshape tensors for attention computation
query = query.view(batch_size, -1, attn.heads, head_dim)
key = key.view(batch_size, -1, kv_heads, head_dim)
value = value.view(batch_size, -1, kv_heads, head_dim)
# Apply Query-Key normalization
if attn.norm_q is not None:
query = attn.norm_q(query)
if attn.norm_k is not None:
key = attn.norm_k(key)
# Apply Rotary Position Embeddings
if rotary_emb is not None:
query = apply_rotary_emb(query, rotary_emb, use_real=False)
key = apply_rotary_emb(key, rotary_emb, use_real=False)
query, key = query.to(dtype), key.to(dtype)
# Calculate attention scale
if base_sequence_length is not None:
softmax_scale = (
math.sqrt(math.log(sequence_length, base_sequence_length)) * attn.scale
)
else:
softmax_scale = attn.scale
# Detect if we have a causal mask
is_causal = False
if joint_attention_mask is not None and joint_attention_mask.dim() == 3:
# Check if it's a lower triangular causal mask
# For efficiency, we only check the first sample
mask_sample = joint_attention_mask[0] # [seq_len, seq_len]
is_causal = torch.allclose(
mask_sample, torch.tril(torch.ones_like(mask_sample))
)
# Unpad input for flash attention
(
query_states,
key_states,
value_states,
indices_q,
cu_seq_lens,
max_seq_lens,
) = self._upad_input(
query, key, value, joint_attention_mask, sequence_length, attn.heads
)
cu_seqlens_q, cu_seqlens_k = cu_seq_lens
max_seqlen_in_batch_q, max_seqlen_in_batch_k = max_seq_lens
# Handle different number of heads
if kv_heads < attn.heads:
key_states = repeat(
key_states, "l h c -> l (h k) c", k=attn.heads // kv_heads
)
value_states = repeat(
value_states, "l h c -> l (h k) c", k=attn.heads // kv_heads
)
# Apply flash attention with causal parameter
attn_output_unpad = flash_attn_varlen_func(
query_states,
key_states,
value_states,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_in_batch_q,
max_seqlen_k=max_seqlen_in_batch_k,
dropout_p=0.0,
causal=is_causal, # Use detected causal setting
softmax_scale=softmax_scale,
)
# Pad output and apply final transformations
hidden_states = pad_input(
attn_output_unpad, indices_q, batch_size, sequence_length
)
hidden_states = hidden_states.flatten(-2)
hidden_states = hidden_states.type_as(query)
# Split hidden_states back to instruction and image, apply separate output projections, then merge
split_results = self._split_instruction_image_features(
[hidden_states], encoder_seq_lengths, seq_lengths
)
instruct_hidden_states, img_hidden_states = split_results[
0
] # [B, max_instruct_len, feature_dim], [B, max_img_len, feature_dim]
# Apply separate output projections for instruction and image
instruct_projected = self.instruct_out(
instruct_hidden_states
) # [B, max_instruct_len, feature_dim]
img_projected = self.img_out(img_hidden_states) # [B, max_img_len, feature_dim]
# Merge back to joint representation
merged_list = self._concat_instruction_image_features(
[img_projected], [instruct_projected], encoder_seq_lengths, seq_lengths
)
hidden_states = merged_list[0] # [B, max_seq_len, feature_dim]
# Apply final output projection
hidden_states = attn.to_out[0](hidden_states)
hidden_states = attn.to_out[1](hidden_states)
# rank, world_size, worker, num_workers = pytorch_worker_info(None)
return hidden_states
class BooguImageDoubleStreamSelfAttnProcessor(nn.Module):
"""
Double-stream self-attention processor without flash attention.
This processor implements double-stream attention where:
- Instruction and image features are processed separately to generate QKV
- QKV are concatenated and processed together for cross-modal attention
- Uses PyTorch's scaled_dot_product_attention for computation
- Supports both standard and causal attention masks
Args:
head_dim: Dimension of each attention head
num_attention_heads: Number of attention heads for queries
num_kv_heads: Number of key-value heads
qkv_bias: Whether to use bias in QKV linear layers
"""
def __init__(
self,
head_dim: int,
num_attention_heads: int,
num_kv_heads: int,
qkv_bias: bool = False,
) -> None:
"""Initialize the double-stream attention processor."""
super().__init__()
if not hasattr(F, "scaled_dot_product_attention"):
raise ImportError(
"BooguImageDoubleStreamSelfAttnProcessor requires PyTorch 2.0. "
"Please upgrade PyTorch to version 2.0 or later."
)
# Calculate dimensions
self.head_dim = head_dim
self.num_attention_heads = num_attention_heads
self.num_kv_heads = num_kv_heads
query_dim = head_dim * num_attention_heads
kv_dim = head_dim * num_kv_heads
# Initialize separate Q, K, V linear layers for instruction and image
# Query uses num_attention_heads, Key/Value use num_kv_heads
self.img_to_q = nn.Linear(query_dim, query_dim, bias=qkv_bias)
self.img_to_k = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
self.img_to_v = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
self.instruct_to_q = nn.Linear(query_dim, query_dim, bias=qkv_bias)
self.instruct_to_k = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
self.instruct_to_v = nn.Linear(query_dim, kv_dim, bias=qkv_bias)
# Additional output projection layers for instruction and image streams
self.instruct_out = nn.Linear(query_dim, query_dim, bias=qkv_bias)
self.img_out = nn.Linear(query_dim, query_dim, bias=qkv_bias)
# Initialize weights
self.initialize_weights()
def initialize_weights(self) -> None:
"""
Initialize the weights of the double-stream attention processor.
Uses Xavier uniform initialization for linear layers and zero initialization for biases.
"""
# Initialize image stream QKV projection layers
nn.init.xavier_uniform_(self.img_to_q.weight)
nn.init.xavier_uniform_(self.img_to_k.weight)
nn.init.xavier_uniform_(self.img_to_v.weight)
# Initialize instruction stream QKV projection layers
nn.init.xavier_uniform_(self.instruct_to_q.weight)
nn.init.xavier_uniform_(self.instruct_to_k.weight)
nn.init.xavier_uniform_(self.instruct_to_v.weight)
# Initialize separate output projection layers
nn.init.xavier_uniform_(self.instruct_out.weight)
nn.init.xavier_uniform_(self.img_out.weight)
# Initialize biases if they exist
if self.img_to_q.bias is not None:
nn.init.zeros_(self.img_to_q.bias)
nn.init.zeros_(self.img_to_k.bias)
nn.init.zeros_(self.img_to_v.bias)
nn.init.zeros_(self.instruct_to_q.bias)
nn.init.zeros_(self.instruct_to_k.bias)
nn.init.zeros_(self.instruct_to_v.bias)
nn.init.zeros_(self.instruct_out.bias)
nn.init.zeros_(self.img_out.bias)
def _concat_instruction_image_features(
self,
img_hidden_states_list: List[torch.Tensor],
instruct_hidden_states_list: List[torch.Tensor],
encoder_seq_lengths: List[int],
seq_lengths: List[int],
) -> List[torch.Tensor]:
"""
Concatenate instruction (text & image) and reference image features (instruction first, then image).
Args:
img_hidden_states_list: List of image tensors [img_query, img_key, img_value]
instruct_hidden_states_list: List of instruction tensors [instruct_query, instruct_key, instruct_value]
encoder_seq_lengths: Instruction sequence lengths for each sample [B]
seq_lengths: Total sequence lengths for each sample [B]
Returns:
List of concatenated tensors [query, key, value]
"""
assert len(img_hidden_states_list) == len(instruct_hidden_states_list), (
f"Length mismatch: img_list={len(img_hidden_states_list)}, instruct_list={len(instruct_hidden_states_list)}"
)
batch_size = img_hidden_states_list[0].shape[0]
max_seq_len = max(seq_lengths)
concatenated_list = []
for img_tensor, instruct_tensor in zip(
img_hidden_states_list, instruct_hidden_states_list
):
# Ensure tensors are on the same device
device = img_tensor.device
if instruct_tensor.device != device:
instruct_tensor = instruct_tensor.to(device)
# Create output tensor with proper shape [B, max_seq_len, feature_dim]
feature_dim = img_tensor.shape[-1]
concatenated = img_tensor.new_zeros(batch_size, max_seq_len, feature_dim)
# Concatenate instruction first, then image for each sample
for i, (encoder_seq_len, seq_len) in enumerate(
zip(encoder_seq_lengths, seq_lengths)
):
# Place instruction tokens first
concatenated[i, :encoder_seq_len] = instruct_tensor[i, :encoder_seq_len]
# Place image tokens after instruction
concatenated[i, encoder_seq_len:seq_len] = img_tensor[
i, : seq_len - encoder_seq_len
]
concatenated_list.append(concatenated)
return concatenated_list
def _split_instruction_image_features(
self,
hidden_states_list: List[torch.Tensor],
encoder_seq_lengths: List[int],
seq_lengths: List[int],
) -> List[Tuple[torch.Tensor, torch.Tensor]]:
"""
Split concatenated features back to instruction and image features.
Inverse operation of _concat_instruction_image_features.
Args:
hidden_states_list: List of concatenated tensors (usually just one element)
encoder_seq_lengths: Instruction sequence lengths for each sample [B]
seq_lengths: Total sequence lengths for each sample [B]
Returns:
List of tuples, each containing (instruct_hidden_states, img_hidden_states)
"""
result_list = []
for hidden_states in hidden_states_list:
batch_size = hidden_states.shape[0]
feature_dim = hidden_states.shape[-1]
# Get maximum lengths for instruction and image
max_instruct_len = max(encoder_seq_lengths)
max_img_len = max(
seq_len - encoder_seq_len
for seq_len, encoder_seq_len in zip(seq_lengths, encoder_seq_lengths)
)
# Create output tensors [B, max_len, feature_dim]
instruct_hidden_states = hidden_states.new_zeros(
batch_size, max_instruct_len, feature_dim
)
img_hidden_states = hidden_states.new_zeros(
batch_size, max_img_len, feature_dim
)
# Split each sample back to instruction and image
for i, (encoder_seq_len, seq_len) in enumerate(
zip(encoder_seq_lengths, seq_lengths)
):
img_len = seq_len - encoder_seq_len
# Extract instruction portion
instruct_hidden_states[i, :encoder_seq_len] = hidden_states[
i, :encoder_seq_len
]
# Extract image portion
img_hidden_states[i, :img_len] = hidden_states[
i, encoder_seq_len:seq_len
]
result_list.append((instruct_hidden_states, img_hidden_states))
return result_list
def __call__(
self,
attn: Attention,
img_hidden_states: torch.Tensor,
instruct_hidden_states: torch.Tensor,
joint_attention_mask: Optional[torch.Tensor] = None,
rotary_emb: Optional[torch.Tensor] = None,
encoder_seq_lengths: List[
int
] = None, # [B] - Instruction sequence lengths for each sample
seq_lengths: List[int] = None, # [B] - Total sequence lengths for each sample
base_sequence_length: Optional[int] = None,
) -> torch.Tensor:
"""
Process double-stream self-attention computation with PyTorch's scaled_dot_product_attention.
Args:
attn: Attention module
img_hidden_states: Image hidden states tensor [B, L_img, D]
instruct_hidden_states: Instruction hidden states tensor [B, L_instruct, D]
joint_attention_mask: Combined attention mask [B, L_total]
rotary_emb: Rotary embeddings for the joint sequence
encoder_seq_lengths: Instruction sequence lengths for each sample [B]
seq_lengths: Total sequence lengths for each sample [B]
base_sequence_length: Optional base sequence length for proportional attention
Returns:
torch.Tensor: Processed hidden states after attention computation
"""
batch_size = img_hidden_states.shape[0]
L_instruct = instruct_hidden_states.shape[1]
L_img = img_hidden_states.shape[1]
# Ensure Q, K, V linear layers are on the same device as input tensors
device = img_hidden_states.device
for layer in [
self.img_to_q,
self.img_to_k,
self.img_to_v,
self.instruct_to_q,
self.instruct_to_k,
self.instruct_to_v,
self.instruct_out,
self.img_out,
]:
if (
(layer.weight.device != device)
and (str(layer.weight.device).lower() != "meta")
and (str(device).lower() not in {"meta", "auto"})
):
layer = layer.to(device)
# Generate Q, K, V for image and instruction streams (NO head reshaping yet)
img_query = self.img_to_q(img_hidden_states) # [B, L_img, query_dim]
img_key = self.img_to_k(img_hidden_states) # [B, L_img, kv_dim]
img_value = self.img_to_v(img_hidden_states) # [B, L_img, kv_dim]
instruct_query = self.instruct_to_q(
instruct_hidden_states
) # [B, L_instruct, query_dim]
instruct_key = self.instruct_to_k(
instruct_hidden_states
) # [B, L_instruct, kv_dim]
instruct_value = self.instruct_to_v(
instruct_hidden_states
) # [B, L_instruct, kv_dim]
# Use helper function to concatenate QKV (instruction first, then image)
img_list = [img_query, img_key, img_value] # [B, L_img, feature_dim] each
instruct_list = [
instruct_query,
instruct_key,
instruct_value,
] # [B, L_instruct, feature_dim] each
concatenated_list = self._concat_instruction_image_features(
img_list, instruct_list, encoder_seq_lengths, seq_lengths
)
query, key, value = concatenated_list # [B, max_seq_len, feature_dim] each
# From here, follow exactly the same logic as BooguImageAttnProcessor
sequence_length = max(seq_lengths)
query_dim = query.shape[-1]
inner_dim = key.shape[-1]
head_dim = query_dim // attn.heads
dtype = query.dtype
# Get key-value heads
kv_heads = inner_dim // head_dim
# Reshape tensors for attention computation
query = query.view(batch_size, -1, attn.heads, head_dim)
key = key.view(batch_size, -1, kv_heads, head_dim)
value = value.view(batch_size, -1, kv_heads, head_dim)
# Apply Query-Key normalization
if attn.norm_q is not None:
query = attn.norm_q(query)
if attn.norm_k is not None:
key = attn.norm_k(key)
# Apply Rotary Position Embeddings
if rotary_emb is not None:
query = apply_rotary_emb(query, rotary_emb, use_real=False)
key = apply_rotary_emb(key, rotary_emb, use_real=False)
query, key = query.to(dtype), key.to(dtype)
# Calculate attention scale
if base_sequence_length is not None:
softmax_scale = (
math.sqrt(math.log(sequence_length, base_sequence_length)) * attn.scale
)
else:
softmax_scale = attn.scale
# scaled_dot_product_attention expects attention_mask shape to be
# (batch, heads, source_length, target_length)
if joint_attention_mask is not None:
joint_attention_mask = joint_attention_mask.bool()
if joint_attention_mask.dim() == 2:
# Standard mask [B, seq_len] -> [B, 1, 1, seq_len]
joint_attention_mask = joint_attention_mask.view(batch_size, 1, 1, -1)
elif joint_attention_mask.dim() == 3:
# Causal mask [B, seq_len, seq_len] -> [B, 1, seq_len, seq_len]
joint_attention_mask = joint_attention_mask.unsqueeze(1)
else:
raise ValueError(
f"Unsupported joint_attention_mask shape: {joint_attention_mask.shape}"
)
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
# explicitly repeat key and value to match query length, otherwise using enable_gqa=True results in MATH backend of sdpa in our test of pytorch2.6
key = key.repeat_interleave(query.size(-3) // key.size(-3), -3)
value = value.repeat_interleave(query.size(-3) // value.size(-3), -3)
hidden_states = F.scaled_dot_product_attention(
query, key, value, attn_mask=joint_attention_mask, scale=softmax_scale
)
hidden_states = hidden_states.transpose(1, 2).reshape(
batch_size, -1, attn.heads * head_dim
)
hidden_states = hidden_states.type_as(query)
# Split hidden_states back to instruction and image, apply separate output projections, then merge
split_results = self._split_instruction_image_features(
[hidden_states], encoder_seq_lengths, seq_lengths
)
instruct_hidden_states, img_hidden_states = split_results[
0
] # [B, max_instruct_len, feature_dim], [B, max_img_len, feature_dim]
# Apply separate output projections for instruction and image
instruct_projected = self.instruct_out(
instruct_hidden_states
) # [B, max_instruct_len, feature_dim]
img_projected = self.img_out(img_hidden_states) # [B, max_img_len, feature_dim]
# Merge back to joint representation
merged_list = self._concat_instruction_image_features(
[img_projected], [instruct_projected], encoder_seq_lengths, seq_lengths
)
hidden_states = merged_list[0] # [B, max_seq_len, feature_dim]
# Apply final output projection
hidden_states = attn.to_out[0](hidden_states)
hidden_states = attn.to_out[1](hidden_states)
return hidden_states
class BooguImageAttnProcessorFlash2Varlen:
"""
Processor for implementing scaled dot-product attention with flash attention and variable length sequences.
This processor implements:
- Flash attention with variable length sequences
- Rotary position embeddings (RoPE)
- Query-Key normalization
- Proportional attention scaling
Args:
None
"""
def __init__(self) -> None:
"""Initialize the attention processor."""
if not is_flash_attn_available():
raise ImportError(
"BooguImageAttnProcessorFlash2Varlen requires flash_attn. "
"Please install flash_attn."
)
def _upad_input(
self,
query_layer: torch.Tensor,
key_layer: torch.Tensor,
value_layer: torch.Tensor,
attention_mask: torch.Tensor,
query_length: int,
num_heads: int,
) -> Tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
Tuple[torch.Tensor, torch.Tensor],
Tuple[int, int],
]:
"""
Unpad the input tensors for flash attention.
Args:
query_layer: Query tensor of shape (batch_size, seq_len, num_heads, head_dim)
key_layer: Key tensor of shape (batch_size, seq_len, num_kv_heads, head_dim)
value_layer: Value tensor of shape (batch_size, seq_len, num_kv_heads, head_dim)
attention_mask: Attention mask tensor of shape (batch_size, seq_len) or (batch_size, seq_len, seq_len) for causal
query_length: Length of the query sequence
num_heads: Number of attention heads
Returns:
Tuple containing:
- Unpadded query tensor
- Unpadded key tensor
- Unpadded value tensor
- Query indices
- Tuple of cumulative sequence lengths for query and key
- Tuple of maximum sequence lengths for query and key
"""
def _get_unpad_data(
mask_2d: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor, int]:
"""Helper function to get unpadding data from a 2D attention mask [B, L]."""
seqlens_in_batch = mask_2d.sum(dim=-1, dtype=torch.int32)
indices = torch.nonzero(mask_2d.flatten(), as_tuple=False).flatten()
max_seqlen_in_batch = seqlens_in_batch.max().item()
cu_seqlens = F.pad(
torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0)
)
return indices, cu_seqlens, max_seqlen_in_batch
# Normalize attention mask: if a causal 3D mask is provided [B, L, L],
# convert it to a standard 2D padding mask [B, L] with True for valid tokens.
if attention_mask is not None and attention_mask.dim() == 3:
B, L, _ = attention_mask.shape
# For a proper lower-triangular causal mask, all first L positions are valid per sample.
# However, to be robust, infer per-sample effective lengths from the diagonal.
diag_valid = torch.diagonal(attention_mask, dim1=-2, dim2=-1)
lengths = diag_valid.sum(dim=-1, dtype=torch.int32) # [B]
mask_2d = torch.zeros(B, L, dtype=torch.bool, device=attention_mask.device)
for i in range(B):
if lengths[i].item() > 0:
mask_2d[i, : int(lengths[i].item())] = True
else:
mask_2d = attention_mask # already [B, L]
indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data(mask_2d)
batch_size, kv_seq_len, num_key_value_heads, head_dim = key_layer.shape
# Unpad key and value layers (shared path for both standard and causal cases)
key_layer = index_first_axis(
key_layer.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim),
indices_k,
)
value_layer = index_first_axis(
value_layer.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim),
indices_k,
)
# Handle different query length cases
if query_length == kv_seq_len:
query_layer = index_first_axis(
query_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim),
indices_k,
)
cu_seqlens_q = cu_seqlens_k
max_seqlen_in_batch_q = max_seqlen_in_batch_k
indices_q = indices_k
elif query_length == 1:
max_seqlen_in_batch_q = 1
cu_seqlens_q = torch.arange(
batch_size + 1, dtype=torch.int32, device=query_layer.device
)
indices_q = cu_seqlens_q[:-1]
query_layer = query_layer.squeeze(1)
else:
# Use the last query_length positions of the 2D mask
q_mask = mask_2d[:, -query_length:]
query_layer, indices_q, cu_seqlens_q, max_seqlen_in_batch_q = unpad_input(
query_layer, q_mask
)
return (
query_layer,
key_layer,
value_layer,
indices_q,
(cu_seqlens_q, cu_seqlens_k),
(max_seqlen_in_batch_q, max_seqlen_in_batch_k),
)
def __call__(
self,
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
image_rotary_emb: Optional[torch.Tensor] = None,
base_sequence_length: Optional[int] = None,
) -> torch.Tensor:
"""
Process attention computation with flash attention.
Args:
attn: Attention module
hidden_states: Hidden states tensor of shape (batch_size, seq_len, hidden_dim)
encoder_hidden_states: Encoder hidden states tensor
attention_mask: Optional attention mask tensor
image_rotary_emb: Optional rotary embeddings for image tokens
base_sequence_length: Optional base sequence length for proportional attention
Returns:
torch.Tensor: Processed hidden states after attention computation
"""
batch_size, sequence_length, _ = hidden_states.shape
# Get Query-Key-Value Pair
query = attn.to_q(hidden_states)
key = attn.to_k(encoder_hidden_states)
value = attn.to_v(encoder_hidden_states)
query_dim = query.shape[-1]
inner_dim = key.shape[-1]
head_dim = query_dim // attn.heads
dtype = query.dtype
# Get key-value heads
kv_heads = inner_dim // head_dim
# Reshape tensors for attention computation
query = query.view(batch_size, -1, attn.heads, head_dim)
key = key.view(batch_size, -1, kv_heads, head_dim)
value = value.view(batch_size, -1, kv_heads, head_dim)
# Apply Query-Key normalization
if attn.norm_q is not None:
query = attn.norm_q(query)
if attn.norm_k is not None:
key = attn.norm_k(key)
# Apply Rotary Position Embeddings
if image_rotary_emb is not None:
query = apply_rotary_emb(query, image_rotary_emb, use_real=False)
key = apply_rotary_emb(key, image_rotary_emb, use_real=False)
query, key = query.to(dtype), key.to(dtype)
# Calculate attention scale
if base_sequence_length is not None:
softmax_scale = (
math.sqrt(math.log(sequence_length, base_sequence_length)) * attn.scale
)
else:
softmax_scale = attn.scale
# Detect if we have a causal mask
is_causal = False
if attention_mask is not None and attention_mask.dim() == 3:
# Check if it's a lower triangular causal mask
# For efficiency, we only check the first sample
mask_sample = attention_mask[0] # [seq_len, seq_len]
is_causal = torch.allclose(
mask_sample, torch.tril(torch.ones_like(mask_sample))
)
# Unpad input for flash attention
(
query_states,
key_states,
value_states,
indices_q,
cu_seq_lens,
max_seq_lens,
) = self._upad_input(
query, key, value, attention_mask, sequence_length, attn.heads
)
cu_seqlens_q, cu_seqlens_k = cu_seq_lens
max_seqlen_in_batch_q, max_seqlen_in_batch_k = max_seq_lens
# Handle different number of heads
if kv_heads < attn.heads:
key_states = repeat(
key_states, "l h c -> l (h k) c", k=attn.heads // kv_heads
)
value_states = repeat(
value_states, "l h c -> l (h k) c", k=attn.heads // kv_heads
)
# Apply flash attention with causal parameter
attn_output_unpad = flash_attn_varlen_func(
query_states,
key_states,
value_states,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_in_batch_q,
max_seqlen_k=max_seqlen_in_batch_k,
dropout_p=0.0,
causal=is_causal, # Use detected causal setting
softmax_scale=softmax_scale,
)
# Pad output and apply final transformations
hidden_states = pad_input(
attn_output_unpad, indices_q, batch_size, sequence_length
)
hidden_states = hidden_states.flatten(-2)
hidden_states = hidden_states.type_as(query)
# Apply output projection
hidden_states = attn.to_out[0](hidden_states)
hidden_states = attn.to_out[1](hidden_states)
return hidden_states
class BooguImageAttnProcessor:
"""
Processor for implementing scaled dot-product attention with flash attention and variable length sequences.
This processor is optimized for PyTorch 2.0 and implements:
- Flash attention with variable length sequences
- Rotary position embeddings (RoPE)
- Query-Key normalization
- Proportional attention scaling
Args:
None
Raises:
ImportError: If PyTorch version is less than 2.0
"""
def __init__(self) -> None:
"""Initialize the attention processor."""
if not hasattr(F, "scaled_dot_product_attention"):
raise ImportError(
"BooguImageAttnProcessorFlash2Varlen requires PyTorch 2.0. "
"Please upgrade PyTorch to version 2.0 or later."
)
def __call__(
self,
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
image_rotary_emb: Optional[torch.Tensor] = None,
base_sequence_length: Optional[int] = None,
) -> torch.Tensor:
"""
Process attention computation with flash attention.
Args:
attn: Attention module
hidden_states: Hidden states tensor of shape (batch_size, seq_len, hidden_dim)
encoder_hidden_states: Encoder hidden states tensor
attention_mask: Optional attention mask tensor
image_rotary_emb: Optional rotary embeddings for image tokens
base_sequence_length: Optional base sequence length for proportional attention
Returns:
torch.Tensor: Processed hidden states after attention computation
"""
batch_size, sequence_length, _ = hidden_states.shape
# Get Query-Key-Value Pair
query = attn.to_q(hidden_states)
key = attn.to_k(encoder_hidden_states)
value = attn.to_v(encoder_hidden_states)
query_dim = query.shape[-1]
inner_dim = key.shape[-1]
head_dim = query_dim // attn.heads
dtype = query.dtype
# Get key-value heads
kv_heads = inner_dim // head_dim
# Reshape tensors for attention computation
query = query.view(batch_size, -1, attn.heads, head_dim)
key = key.view(batch_size, -1, kv_heads, head_dim)
value = value.view(batch_size, -1, kv_heads, head_dim)
# Apply Query-Key normalization
if attn.norm_q is not None:
query = attn.norm_q(query)
if attn.norm_k is not None:
key = attn.norm_k(key)
# Apply Rotary Position Embeddings
if image_rotary_emb is not None:
query = apply_rotary_emb(query, image_rotary_emb, use_real=False)
key = apply_rotary_emb(key, image_rotary_emb, use_real=False)
query, key = query.to(dtype), key.to(dtype)
# Calculate attention scale
if base_sequence_length is not None:
softmax_scale = (
math.sqrt(math.log(sequence_length, base_sequence_length)) * attn.scale
)
else:
softmax_scale = attn.scale
# sdpa expects attn_mask with shape (B, H, Q, K) as boolean (True keeps, False masks)
if attention_mask is not None:
attention_mask = attention_mask.bool()
if attention_mask.dim() == 2:
# Standard padding mask [B, L] -> [B, 1, 1, L]
attention_mask = attention_mask.view(batch_size, 1, 1, -1)
elif attention_mask.dim() == 3:
# Robust causal + padding mask construction
# Infer valid lengths from diagonal, then build lower-triangular mask within valid lengths
B, L, _ = attention_mask.shape
diag_valid = torch.diagonal(attention_mask, dim1=-2, dim2=-1)
lengths = diag_valid.sum(dim=-1) # [B]
arange_L = torch.arange(L, device=attention_mask.device)
# Padding masks for queries and keys: shape [B, L]
q_valid = arange_L.unsqueeze(0) < lengths.unsqueeze(1)
k_valid = q_valid # same lengths assumed
# Lower-triangular causal mask [L, L]
causal = torch.tril(
torch.ones(L, L, dtype=torch.bool, device=attention_mask.device)
)
# Combine: [B, L, L]
combined = causal & q_valid.unsqueeze(-1) & k_valid.unsqueeze(-2)
attention_mask = combined.unsqueeze(1) # [B, 1, L, L]
else:
raise ValueError(
f"Unsupported attention_mask shape: {attention_mask.shape}"
)
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
# explicitly repeat key and value to match query length, otherwise using enable_gqa=True results in MATH backend of sdpa in our test of pytorch2.6
key = key.repeat_interleave(query.size(-3) // key.size(-3), -3)
value = value.repeat_interleave(query.size(-3) // value.size(-3), -3)
hidden_states = F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, scale=softmax_scale
)
hidden_states = hidden_states.transpose(1, 2).reshape(
batch_size, -1, attn.heads * head_dim
)
hidden_states = hidden_states.type_as(query)
# Apply output projection
hidden_states = attn.to_out[0](hidden_states)
hidden_states = attn.to_out[1](hidden_states)
return hidden_states