SingleHeadAttention
class SingleHeadAttention(nn.Module):
# 单头注意力机制
def __init__(self, config):
super().__init__()
self.key = nn.Linear(config.n_embd, config.head_size)
self.value = nn.Linear(config.n_embd, config.head_size)
self.query = nn.Linear(config.n_embd, config.head_size)
self.head_size = config.head_size
# 尝试学习新的写法,attention_mask 通过 register_buffer 注册
# 因为不用计算 梯度,所以节约内存和显存,速度也更快
self.register_buffer(
'attention_mask',
torch.tril(
torch.ones(config.block_size, config.block_size)
))
self.dropout = nn.Dropout(config.dropout)
def forward(self, x):
batch_size, seq_len, hidden_size = x.size()
k = self.key(x)
v = self.value(x)
q = self.query(x)
weight = q @ k.transpose(-2, -1) # @ 就是 torch.matmul 的简化写法
# 一定要在 softmax 前除以 sqrt(head_size)
weight = weight.masked_fill(
self.attention_mask[:seq_len, :seq_len] == 0,
float('-inf')
) / math.sqrt(self.head_size) # 这里的 hidden_size 其实是 head_size,因为是单头
weight = F.softmax(weight, dim=-1)
weight = self.dropout(weight)
out = weight @ v
return out
细节理解
问题一
如果把attention_mask定义成一个普通的下三角张量,和这种方式有什么区别?
在 PyTorch 中,如果你将 attention_mask 定义为一个普通的下三角张量,而不是使用 register_buffer,会有以下几点区别:
模型状态(Model State)
- **使用 **
register_buffer:attention_mask会被视为模型的一部分,自动包含在模型的状态字典(state_dict)中。- 在保存和加载模型时,
attention_mask会随模型参数一同保存和恢复,确保模型在不同环境或训练阶段中保持一致。
- 定义为普通张量:
attention_mask不会被自动包含在模型的状态字典中。- 在保存和加载模型时,你需要手动处理
attention_mask的保存和加载,否则在加载模型后,attention_mask可能不会被正确恢复,导致模型行为不一致。
内存和计算效率
- **使用 **
register_buffer:- PyTorch 不会为
attention_mask计算梯度,因为缓冲区(buffer)默认不需要梯度。 - 这可以节省内存和显存,尤其是在训练大型模型时,因为不需要为
attention_mask分配梯度存储空间。 - 训练速度也会略有提升,因为跳过了不必要的梯度计算步骤。
- PyTorch 不会为
- 定义为普通张量:
- 如果你将
attention_mask定义为普通张量并在模型中使用,PyTorch 默认会为其分配梯度(除非你手动设置requires_grad=False)。 - 如果不设置
requires_grad=False,PyTorch 会为attention_mask计算梯度,这会浪费内存和计算资源,因为attention_mask是一个固定的掩码,不需要更新。 - 即使你设置
requires_grad=False,attention_mask仍然不会被自动包含在模型的状态字典中,你仍需手动处理其保存和加载。
- 如果你将
灵活性
- **使用 **
register_buffer:attention_mask作为模型的一部分,管理更加简单和统一。- 在模型迁移、部署或多 GPU 训练时,PyTorch 会自动处理缓冲区的分配和同步。
- 定义为普通张量:
- 你需要手动管理
attention_mask,例如在模型的__init__方法中创建,并在forward方法中使用。 - 在多 GPU 训练或模型迁移时,你可能需要额外处理
attention_mask的设备分配和同步。
- 你需要手动管理
总结
- **使用 **
register_buffer:- 更高效:避免不必要的梯度计算,节省内存和显存。
- 更一致:
attention_mask自动包含在模型的状态中,确保模型在保存和加载时完整迁移。 - 更简洁:PyTorch 自动管理缓冲区的设备和同步,减少手动操作。
- 定义为普通张量:
- 需要手动处理保存和加载,可能导致模型状态不一致。
- 如果不设置
requires_grad=False,会浪费资源计算不必要的梯度。 - 即使设置
requires_grad=False,仍然需要手动管理attention_mask的状态。
因此,对于像 attention_mask 这样固定的、不需要更新的张量,使用 register_buffer 是 PyTorch 推荐的做法,能够优化资源使用并确保模型的一致性。
问题二
weight = weight.masked_fill(
self.attention_mask[:seq_len, :seq_len] == 0,
float('-inf')
) / math.sqrt(self.head_size)
为什么这段代码需要[:seq_len, :seq_len]?
要搞清楚这个问题需要知道weight 的形状,weight 的形状是 (batch_size, seq_len, seq_len),因此在进行掩码时要切片。
DEEPSEEK:
在注意力机制中,使用self.attention_mask[:seq_len, :seq_len]进行切片是处理变长序列的关键技术,原因如下:
预分配掩码矩阵
代码中初始化时创建了一个固定大小的掩码矩阵:
torch.ones(config.block_size, config.block_size)
这里config.block_size是模型支持的最大序列长度(如512或1024)。这是一个预分配的方形矩阵,大小始终为[block_size, block_size]。
实际序列长度可变
在训练/推理时:
batch_size, seq_len, hidden_size = x.size()
seq_len(实际序列长度)可能小于block_size(最大长度)。例如:
block_size = 512- 当前输入
seq_len = 128
切片的作用
[:seq_len, :seq_len]从预分配的大矩阵中动态提取子矩阵:
self.attention_mask[:128, :128]
这保证了:
- 形状匹配:生成的掩码矩阵大小
[128, 128],与注意力权重weight的[batch, 128, 128]维度匹配 - 资源优化:避免为每个不同长度的序列重新创建新掩码
实际示例
假设:
block_size = 4(最大长度4)- 预分配掩码:
[[1, 0, 0, 0],
[1, 1, 0, 0],
[1, 1, 1, 0],
[1, 1, 1, 1]]
情况1:序列长度seq_len=2
切片后:
[[1, 0],
[1, 1]]
情况2:序列长度seq_len=3
切片后:
[[1, 0, 0],
[1, 1, 0],
[1, 1, 1]]
为什么必须切片?
如果不切片:
- 当
seq_len < block_size时:尝试用[512,512]的掩码处理[128,128]的权重 → 形状不匹配错误 - 当
seq_len > block_size时:超出预分配范围 → 索引越界错误
设计优势
- 计算高效:预分配一次,重复使用(避免每个batch重新计算tril)
- 内存友好:大矩阵常驻内存,动态切片无额外开销
- 灵活性:适应不同长度的输入序列
- 与位置编码兼容:配合可学习的位置编码处理变长序列
这种设计模式在Transformer实现中很常见,如HuggingFace的
BertModel也使用类似机制处理attention_mask。
评论(0)