Files
XingfenD c57a9bf9e6 添加KV Cache支持:实现prefill/decode阶段分离
- MultiHeadAttention类添加KV cache机制
- TransformerBlock支持use_cache参数
- SimpleTransformer新增prefill和decode方法
- 添加InferenceEngine推理引擎类
- 更新example.py演示推理过程
- 更新README.md文档
2026-07-16 15:07:47 +08:00

438 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
def softmax(x, axis=-1):
"""Softmax激活函数,用于将 logits 转换为概率分布"""
# 减去最大值防止数值溢出
e_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
return e_x / np.sum(e_x, axis=axis, keepdims=True)
def relu(x):
"""ReLU激活函数"""
return np.maximum(0, x)
class Linear:
"""全连接层(线性层)"""
def __init__(self, in_features, out_features):
# 使用He初始化权重
self.weight = np.random.randn(in_features, out_features) * np.sqrt(2.0 / in_features)
# 偏置初始化为0
self.bias = np.zeros(out_features)
def forward(self, x):
"""前向传播:y = xW + b"""
return x @ self.weight + self.bias
class LayerNorm:
"""层归一化(Layer Normalization"""
def __init__(self, d_model, eps=1e-6):
# 缩放参数
self.gamma = np.ones(d_model)
# 偏移参数
self.beta = np.zeros(d_model)
# 防止除零的小常数
self.eps = eps
def forward(self, x):
"""前向传播:对最后一个维度进行归一化"""
mean = np.mean(x, axis=-1, keepdims=True)
std = np.std(x, axis=-1, keepdims=True)
return self.gamma * (x - mean) / (std + self.eps) + self.beta
class MultiHeadAttention:
"""多头注意力机制(支持KV cache"""
def __init__(self, d_model, num_heads):
self.d_model = d_model # 模型维度
self.num_heads = num_heads # 注意力头数
self.d_k = d_model // num_heads # 每个头的维度
# 定义Q、K、V的线性变换层
self.W_q = Linear(d_model, d_model) # Query变换
self.W_k = Linear(d_model, d_model) # Key变换
self.W_v = Linear(d_model, d_model) # Value变换
self.W_o = Linear(d_model, d_model) # 输出变换
# KV cache:用于推理时缓存历史Key和Value
self.k_cache = None # 缓存的Key [batch_size, num_heads, seq_len, d_k]
self.v_cache = None # 缓存的Value [batch_size, num_heads, seq_len, d_k]
def clear_cache(self):
"""清除KV cache"""
self.k_cache = None
self.v_cache = None
def forward(self, Q, K, V, mask=None, use_cache=False):
"""
前向传播
Q: Query张量 [batch_size, seq_len, d_model]
K: Key张量 [batch_size, seq_len, d_model]
V: Value张量 [batch_size, seq_len, d_model]
mask: 注意力掩码
use_cache: 是否使用KV cache(推理时设为True
"""
batch_size = Q.shape[0]
seq_len = Q.shape[1]
# 线性变换并分割成多头
Q = self.W_q.forward(Q).reshape(batch_size, seq_len, self.num_heads, self.d_k).transpose(0, 2, 1, 3)
K = self.W_k.forward(K).reshape(batch_size, seq_len, self.num_heads, self.d_k).transpose(0, 2, 1, 3)
V = self.W_v.forward(V).reshape(batch_size, seq_len, self.num_heads, self.d_k).transpose(0, 2, 1, 3)
# KV cache逻辑
if use_cache:
if self.k_cache is None:
# Prefill阶段:首次计算,缓存完整的K和V
self.k_cache = K
self.v_cache = V
else:
# Decode阶段:拼接历史缓存和新的K、V
self.k_cache = np.concatenate([self.k_cache, K], axis=2)
self.v_cache = np.concatenate([self.v_cache, V], axis=2)
# 使用完整的K和V进行注意力计算
K = self.k_cache
V = self.v_cache
# 计算缩放点积注意力分数
# Q: [batch, heads, q_len, d_k]
# K: [batch, heads, kv_len, d_k]decode时kv_len > q_len
attn_scores = Q @ K.transpose(0, 1, 3, 2) / np.sqrt(self.d_k)
# 应用掩码(如果提供)
if mask is not None:
attn_scores = np.where(mask == 0, -1e9, attn_scores)
# Softmax得到注意力权重
attn_probs = softmax(attn_scores, axis=-1)
# 加权求和
attn_output = attn_probs @ V
# 拼接多头输出
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.d_model)
# 最终线性变换
output = self.W_o.forward(attn_output)
return output
class FeedForward:
"""前馈神经网络(两层全连接网络)"""
def __init__(self, d_model, d_ff):
# 第一层:扩展维度
self.linear1 = Linear(d_model, d_ff)
# 第二层:恢复维度
self.linear2 = Linear(d_ff, d_model)
def forward(self, x):
"""前向传播:线性 -> ReLU -> 线性"""
return self.linear2.forward(relu(self.linear1.forward(x)))
class TransformerBlock:
"""Transformer编码器块"""
def __init__(self, d_model, num_heads, d_ff):
# 多头自注意力层
self.attention = MultiHeadAttention(d_model, num_heads)
# 前馈网络层
self.feed_forward = FeedForward(d_model, d_ff)
# 两个层归一化
self.norm1 = LayerNorm(d_model)
self.norm2 = LayerNorm(d_model)
def forward(self, x, mask=None, use_cache=False):
"""
前向传播(残差连接 + 层归一化)
1. 自注意力 -> 残差连接 -> 层归一化
2. 前馈网络 -> 残差连接 -> 层归一化
x: 输入张量 [batch_size, seq_len, d_model]
mask: 注意力掩码
use_cache: 是否使用KV cache
"""
# 自注意力子层(Q=K=V,自注意力)
attn_output = self.attention.forward(x, x, x, mask, use_cache=use_cache)
x = self.norm1.forward(x + attn_output)
# 前馈网络子层
ff_output = self.feed_forward.forward(x)
x = self.norm2.forward(x + ff_output)
return x
def clear_cache(self):
"""清除该层的KV cache"""
self.attention.clear_cache()
class SimpleTransformer:
"""简化的Transformer编码器模型(支持prefill/decode分离)"""
def __init__(self, vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_len):
self.d_model = d_model
self.vocab_size = vocab_size
# 词嵌入层
self.embedding = np.random.randn(vocab_size, d_model) * 0.01
# 位置编码
self.positional_encoding = self._create_positional_encoding(max_seq_len, d_model)
# Transformer编码器层堆叠
self.transformer_blocks = [
TransformerBlock(d_model, num_heads, d_ff) for _ in range(num_layers)
]
def _create_positional_encoding(self, max_seq_len, d_model):
"""
创建正弦余弦位置编码
使用不同频率的正弦和余弦函数生成位置信息
"""
pe = np.zeros((max_seq_len, d_model))
position = np.arange(0, max_seq_len).reshape(-1, 1).astype(float)
div_term = np.exp(np.arange(0, d_model, 2).astype(float) * -(np.log(10000.0) / d_model))
pe[:, 0::2] = np.sin(position * div_term) # 偶数维度用正弦
pe[:, 1::2] = np.cos(position * div_term) # 奇数维度用余弦
return pe
def clear_cache(self):
"""清除所有层的KV cache"""
for block in self.transformer_blocks:
block.clear_cache()
def prefill(self, x, mask=None):
"""
Prefill阶段:处理完整的输入序列
x: 输入序列 [batch_size, seq_len]
mask: 注意力掩码
返回: 输出张量 [batch_size, seq_len, d_model]
"""
seq_len = x.shape[1]
# 词嵌入 + 缩放
x = self.embedding[x] * np.sqrt(self.d_model)
# 加上位置编码
x = x + self.positional_encoding[:seq_len, :]
# 通过所有Transformer块,使用KV cache
for block in self.transformer_blocks:
x = block.forward(x, mask, use_cache=True)
return x
def decode(self, x, position):
"""
Decode阶段:处理单个新token
x: 新token [batch_size, 1]
position: 当前token在序列中的位置
返回: 输出张量 [batch_size, 1, d_model]
"""
# 词嵌入 + 缩放
x = self.embedding[x] * np.sqrt(self.d_model)
# 加上位置编码(使用当前位置)
x = x + self.positional_encoding[position:position+1, :]
# 通过所有Transformer块,使用KV cache
for block in self.transformer_blocks:
x = block.forward(x, use_cache=True)
return x
def forward(self, x, mask=None):
"""
前向传播(不使用KV cache,用于训练)
x: 输入序列 [batch_size, seq_len]
mask: 注意力掩码
"""
seq_len = x.shape[1]
# 词嵌入 + 缩放
x = self.embedding[x] * np.sqrt(self.d_model)
# 加上位置编码
x = x + self.positional_encoding[:seq_len, :]
# 通过所有Transformer块
for block in self.transformer_blocks:
x = block.forward(x, mask)
return x
def count_parameters(self):
"""统计模型参数数量"""
count = 0
count += self.embedding.size # 嵌入层参数
for block in self.transformer_blocks:
# 注意力层参数
count += block.attention.W_q.weight.size + block.attention.W_q.bias.size
count += block.attention.W_k.weight.size + block.attention.W_k.bias.size
count += block.attention.W_v.weight.size + block.attention.W_v.bias.size
count += block.attention.W_o.weight.size + block.attention.W_o.bias.size
# 前馈网络参数
count += block.feed_forward.linear1.weight.size + block.feed_forward.linear1.bias.size
count += block.feed_forward.linear2.weight.size + block.feed_forward.linear2.bias.size
# 层归一化参数
count += block.norm1.gamma.size + block.norm1.beta.size
count += block.norm2.gamma.size + block.norm2.beta.size
return count
def create_padding_mask(seq, pad_idx=0):
"""
创建填充掩码
用于忽略序列中的填充位置(padding tokens
"""
return (seq != pad_idx).astype(float)[:, np.newaxis, np.newaxis, :]
def create_look_ahead_mask(size):
"""
创建前瞻掩码(下三角掩码)
用于解码器中,防止位置i看到i之后的信息
"""
mask = np.triu(np.ones((size, size)), k=1).astype(bool)
return (~mask).astype(float)
class InferenceEngine:
"""推理引擎:管理prefill和decode阶段"""
def __init__(self, model):
"""
初始化推理引擎
model: SimpleTransformer模型实例
"""
self.model = model
self.generated_tokens = [] # 已生成的token列表
self.current_position = 0 # 当前位置
def reset(self):
"""重置推理状态"""
self.model.clear_cache()
self.generated_tokens = []
self.current_position = 0
def prefill(self, input_ids):
"""
Prefill阶段:处理完整的输入序列
input_ids: 输入token序列 [batch_size, seq_len]
返回: 下一个token的logits [batch_size, vocab_size]
"""
self.reset()
batch_size = input_ids.shape[0]
seq_len = input_ids.shape[1]
# 创建前瞻掩码(防止看到未来信息)
mask = create_look_ahead_mask(seq_len)
# 执行prefill前向传播(使用KV cache
output = self.model.prefill(input_ids, mask)
# 获取最后一个位置的输出(用于预测下一个token)
last_output = output[:, -1, :] # [batch_size, d_model]
# 简单的logits计算:使用嵌入矩阵的转置作为输出投影
# logits = last_output @ embedding.T
logits = last_output @ self.model.embedding.T # [batch_size, vocab_size]
# 更新状态
self.current_position = seq_len
return logits
def decode_step(self, input_token):
"""
Decode阶段:处理单个新token
input_token: 新token [batch_size, 1]
返回: 下一个token的logits [batch_size, vocab_size]
"""
# 执行decode前向传播(使用KV cache
output = self.model.decode(input_token, self.current_position)
# 获取输出(单token,所以直接取[:, 0, :]
last_output = output[:, 0, :] # [batch_size, d_model]
# 计算logits
logits = last_output @ self.model.embedding.T # [batch_size, vocab_size]
# 更新位置
self.current_position += 1
return logits
def generate(self, input_ids, max_new_tokens=50, temperature=1.0):
"""
自回归生成文本
input_ids: 输入token序列 [batch_size, seq_len]
max_new_tokens: 最大生成token数
temperature: 温度参数(控制随机性)
返回: 完整的生成序列 [batch_size, seq_len + max_new_tokens]
"""
batch_size = input_ids.shape[0]
# 保存原始输入
generated = input_ids.copy()
# Prefill阶段
logits = self.prefill(input_ids)
# 采样第一个生成的token
next_token = self._sample(logits, temperature)
generated = np.concatenate([generated, next_token], axis=1)
# Decode阶段:逐个生成token
for i in range(max_new_tokens - 1):
logits = self.decode_step(next_token)
next_token = self._sample(logits, temperature)
generated = np.concatenate([generated, next_token], axis=1)
# 检查是否生成了结束符(这里用0作为结束符)
if np.all(next_token == 0):
break
return generated
def _sample(self, logits, temperature=1.0):
"""
从logits中采样
logits: [batch_size, vocab_size]
temperature: 温度参数
返回: 采样的token [batch_size, 1]
"""
# 应用温度缩放
logits = logits / temperature
# 计算概率分布
probs = softmax(logits, axis=-1)
# 按概率采样
batch_size = logits.shape[0]
next_tokens = np.zeros((batch_size, 1), dtype=int)
for i in range(batch_size):
# 使用numpy的random.choice采样
next_tokens[i, 0] = np.random.choice(logits.shape[1], p=probs[i])
return next_tokens
if __name__ == "__main__":
# 模型超参数
vocab_size = 1000 # 词汇表大小
d_model = 512 # 模型维度
num_heads = 8 # 注意力头数
num_layers = 6 # Transformer层数
d_ff = 2048 # 前馈网络隐藏层维度
max_seq_len = 100 # 最大序列长度
# 创建模型
model = SimpleTransformer(vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_len)
# 创建随机输入
batch_size = 2
seq_len = 10
x = np.random.randint(0, vocab_size, (batch_size, seq_len))
# 前向传播
output = model.forward(x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
print(f"模型参数量: {model.count_parameters():,}")