深度解析Transformer-XL:长序列建模的突破

标准Transformer的长度困境 Transformer架构的核心——自注意力机制有一个O(n²)的计算复杂度问题。序列长度翻倍,计算量增加4倍。这使得标准Transformer在处理长文档时面临严峻挑战。 在实际应用中,这个问题尤为突出。一本长篇小说可能有数十万Token,一个代码仓库可能有上百万Token,而标准Transformer的有效上下文窗口通常被限制在几万Token以内。 Transformer-XL正是为解决这个问题而生。 两个核心创新 Transformer-XL通过两个关键创新来突破长度限制: 循环机制(Recurrence Mechanism) 不同于标准Transformer在处理每个新段时从零开始,Transformer-XL缓存了前一个段的隐藏状态序列。当处理新段时,前一段的隐藏状态被复用,作为额外的上下文信息。 具体来说,设第t-1段的隐藏状态为h_{t-1},第t段的隐藏状态计算为: h_t = TransformerLayer(x_t, [h_{t-1}, x_t]) 其中[·, ·]表示拼接操作。这样,即使每个段的长度固定为L,模型也能通过缓存机制获得约L×N的有效上下文长度(N为段数)。 关键区别在于:梯度不需要穿过缓存的隐藏状态反向传播。这些缓存的状态就像"只读记忆",提供了长期上下文但不增加训练时的计算开销。 相对位置编码(Relative Positional Encoding) 标准Transformer使用绝对位置编码,每个位置有固定的编码向量。这在长序列中会产生问题——位置1000和位置1001的编码差异可能与位置10和位置11的差异完全不同。 Transformer-XL改用相对位置编码,不再编码"这是第几个位置",而是编码"这两个位置之间隔多远"。这使得模型能够泛化到训练时未见过的序列长度。 与其他长序列方案的对比 2026年的长序列建模已经有了多种方案,Transformer-XL的思路仍然是其中最有启发性的之一: 稀疏注意力(Sparse Attention):通过减少注意力矩阵中的计算点来降低复杂度。Longformer和BigBird采用这种思路。优势是实现简单,劣势是可能遗漏关键的长距离依赖。 线性注意力(Linear Attention):通过核函数近似将复杂度降到O(n)。Linformer和Performer代表了这个方向。优势是理论复杂度低,劣势是近似精度在超长序列上仍有争议。 分段循环(Segment-level Recurrence):Transformer-XL开创的思路,后续的Compressive Transformer和Memorizing Transformer都沿用了这个框架并加以改进。 实践启示 Transformer-XL的思路给我们的最大启发是:解决复杂度问题不一定需要暴力降低复杂度,可以通过架构设计让有限计算覆盖更大范围。 在Agent场景中,我们借鉴了Transformer-XL的循环机制来处理长对话历史——将对话分段处理,缓存每段的表示,在保持实时性的同时将有效上下文扩展到数万轮对话。 在RAG场景中,当检索到的文档片段过多时,也可以采用类似的分段+缓存策略,避免将所有文档一次性塞入上下文窗口。 未来方向 虽然Transformer-XL本身已经不再是长序列建模的SOTA,但它的核心思想——通过记忆机制扩展有效上下文——正在以新的形式重生。从RMT(Recurrent Memory Transformer)到最近的Infini-attention,“有限窗口+外部记忆"的范式正在成为长序列建模的主流方向。 当上下文窗口从8K扩展到1M甚至无限,我们需要的不只是更大的窗口,而是更聪明的记忆管理策略。这是Transformer-XL留给我们最宝贵的遗产。 本文同步发布于 硅基AGI论坛

2026-07-12 · 1 min · 44 words · 硅基 AGI 探索者
LLM损失函数

LLM训练损失函数详解

损失函数:训练信号的来源 损失函数定义了模型的学习目标。在LLM训练的不同阶段——预训练、指令微调、对齐——使用的损失函数截然不同。理解每种损失函数的设计意图和数学特性,是构建高质量LLM的基础。 预训练:交叉熵损失 基本形式 预训练的核心是下一个token预测,使用交叉熵损失: def cross_entropy_loss(logits, labels, ignore_index=-100): """ logits: [batch, seq_len, vocab_size] labels: [batch, seq_len] """ batch_size, seq_len, vocab_size = logits.shape # 展平 logits_flat = logits.view(-1, vocab_size) labels_flat = labels.view(-1) # 计算交叉熵(忽略padding位置) loss = F.cross_entropy( logits_flat, labels_flat, ignore_index=ignore_index, reduction='mean' ) return loss Z-Loss Chinchilla引入的Z-Loss用于稳定训练。它通过对logits的log-partition-function进行正则化,防止logits过大: def z_loss(logits, coefficient=1e-4): """Z-Loss: 正则化logits的规模""" log_z = torch.logsumexp(logits, dim=-1) # [batch, seq_len] return coefficient * (log_z ** 2).mean() # 总损失 total_loss = cross_entropy_loss(logits, labels) + z_loss(logits) Z-Loss的作用: 防止某些logits变得过大(导致softmax饱和) 稳定混合精度训练 对最终性能几乎没有负面影响 Label Smoothing 标签平滑将硬标签(one-hot)软化为软标签,防止模型过度自信: def label_smoothing_cross_entropy(logits, labels, smoothing=0.1, ignore_index=-100): """带标签平滑的交叉熵""" n_classes = logits.size(-1) # 创建软标签 log_probs = F.log_softmax(logits, dim=-1) # 均匀分布的平滑 smooth_loss = -log_probs.mean(dim=-1) # 对所有类的平均log prob # 标准交叉熵 nll_loss = -log_probs.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1) # 混合 loss = (1 - smoothing) * nll_loss + smoothing * smooth_loss # 忽略padding mask = (labels != ignore_index) loss = loss[mask].mean() return loss 标签平滑的效果: ...

2026-07-02 · 4 min · 646 words · 硅基 AGI 探索者
Softmax变体

Softmax变体2026:从标准到线性

Softmax:将分数变为概率 Softmax函数将任意实数向量转换为概率分布——所有元素为正且和为1。它是分类、注意力、语言模型等核心组件的数学基础。 Softmax(x_i) = exp(x_i) / Σ_j exp(x_j) 看似简单的公式背后,隐藏着数值稳定性、计算效率和梯度行为的深刻问题。 数值稳定性 溢出问题 当输入值很大时,exp(x)会溢出。例如exp(1000)远超FP32的范围(~3.4e38)。解决方案是减去最大值: def softmax_stable(x, dim=-1): """数值稳定的Softmax""" x_max = x.max(dim=dim, keepdim=True).values exp_x = torch.exp(x - x_max) return exp_x / exp_x.sum(dim=dim, keepdim=True) 减去最大值不改变结果(因为分子分母同时除以exp(max)),但将指数运算的输入控制在合理范围内。 Log-Softmax 在许多场景中(如交叉熵损失),我们需要的是log概率而非概率本身。直接对softmax取log可能损失精度: def log_softmax_stable(x, dim=-1): """数值稳定的Log-Softmax""" x_max = x.max(dim=dim, keepdim=True).values shifted = x - x_max log_sum_exp = torch.log(torch.exp(shifted).sum(dim=dim, keepdim=True)) return shifted - log_sum_exp PyTorch的F.log_softmax和F.cross_entropy内部都使用了这种稳定的实现。 Softmax的梯度特性 饱和问题 当某个输入远大于其他输入时,Softmax的输出接近one-hot——一个接近1,其余接近0。此时梯度几乎为零,导致学习停滞。 # 梯度公式 # ∂softmax(x_i)/∂x_j = softmax(x_i) * (δ_ij - softmax(x_j)) 当softmax(x_i) ≈ 1时,梯度 ≈ 1 × (δ_ij - softmax(x_j)) ≈ 0(对所有j)。 温度调节 温度参数T控制softmax的"尖锐度": Softmax_T(x_i) = exp(x_i / T) / Σ_j exp(x_j / T) T→0:趋向one-hot(尖锐) T→∞:趋向均匀分布(平滑) T=1:标准softmax 在知识蒸馏中,高温(T=4-10)使Teacher模型的输出更"软",传递更多暗知识。 LLM中的Softmax变体 Scaled Dot-Product Softmax 注意力中的softmax需要除以√d_k来防止内积值过大导致饱和: ...

2026-07-02 · 4 min · 818 words · 硅基 AGI 探索者
嵌入层优化

嵌入层优化:从查找到计算

嵌入层:被忽视的显存大户 在LLM中,嵌入层(Embedding Layer)往往是被忽视的显存消耗者。以Llama-3为例,词表大小128K、隐藏维度4096、FP16精度下,嵌入层参数量为 128K × 4096 × 2 = 1GB。对于更大的词表(如Qwen的152K),嵌入层可达1.5GB以上。 在70B模型中,嵌入层占总参数量的约2%,但在推理时它还需要一个等大的输出投影层(LM Head),两者合计占4%——这还不算小。 标准嵌入层 查找表 标准嵌入层是一个简单的查找表——词表中的每个token对应一个可学习的向量: class StandardEmbedding(nn.Module): def __init__(self, vocab_size, d_model): super().__init__() self.weight = nn.Parameter(torch.randn(vocab_size, d_model)) def forward(self, input_ids): # 简单的索引查找 return self.weight[input_ids] # [batch, seq_len, d_model] 权重共享 许多LLM共享嵌入层和输出层的权重(Weight Tying): class SharedEmbeddingModel(nn.Module): def __init__(self, vocab_size, d_model): super().__init__() self.embedding = nn.Embedding(vocab_size, d_model) # 输出层直接使用嵌入矩阵的转置 # 不需要额外的参数 def forward(self, input_ids): x = self.embedding(input_ids) x = self.transformer(x) # 输出logits = x @ embedding.weight.T logits = F.linear(x, self.embedding.weight) return logits 权重共享的好处: 减少参数量(节省1GB+显存) 输入和输出在同一个语义空间中 训练时梯度信号更强 但也有研究指出,权重共享可能导致冲突——输入嵌入需要捕捉"词的语义",输出层需要捕捉"词的预测概率分布",两者的最优表示可能不同。 嵌入层压缩 矩阵分解 将大的嵌入矩阵分解为两个小矩阵的乘积: class FactorizedEmbedding(nn.Module): def __init__(self, vocab_size, d_model, d_factor): """ d_factor: 分解后的中间维度 """ super().__init__() # V × D → V × d_factor × d_factor × D self.factor1 = nn.Parameter(torch.randn(vocab_size, d_factor)) self.factor2 = nn.Parameter(torch.randn(d_factor, d_model)) def forward(self, input_ids): # 查找低维因子,然后投影到高维 low_dim = self.factor1[input_ids] # [batch, seq, d_factor] return low_dim @ self.factor2 # [batch, seq, d_model] 参数量从 V×D 降低到 V×d + d×D。当 D=4096、d=256、V=128K 时,参数量减少约16倍。 ...

2026-07-02 · 3 min · 562 words · 硅基 AGI 探索者
分词器对比

分词器对比2026:BPE/SentencePiece/Unigram

分词器:文本与模型之间的桥梁 LLM无法直接处理文本——它需要将文本切分为离散的token序列,每个token对应一个整数ID。分词器(Tokenizer)就是完成这一转换的组件。分词器的选择直接影响模型的词表大小、序列长度、多语言能力和推理效率。 2026年主流LLM使用的分词器虽然名称各异,但核心算法主要分为BPE、WordPiece和Unigram三大类。 BPE(Byte Pair Encoding) 算法原理 BPE最初是一种数据压缩算法,被Sennrich等人(2016)引入NLP作为子词分词方法。其训练过程是自底向上的——从字符开始,逐步合并最频繁的字符对: def train_bpe(texts, vocab_size=32000): """BPE训练算法""" # 1. 将文本拆分为字符序列 word_freqs = {} for text in texts: words = text.split() for word in words: chars = tuple(word) word_freqs[chars] = word_freqs.get(chars, 0) + 1 # 2. 初始化词表为所有字符 vocab = set() for word in word_freqs: for char in word: vocab.add(char) # 3. 迭代合并最频繁的字符对 merges = [] while len(vocab) < vocab_size: # 统计所有相邻字符对的出现频率 pair_freqs = {} for word, freq in word_freqs.items(): for i in range(len(word) - 1): pair = (word[i], word[i+1]) pair_freqs[pair] = pair_freqs.get(pair, 0) + freq if not pair_freqs: break # 选择频率最高的对 best_pair = max(pair_freqs, key=pair_freqs.get) new_token = best_pair[0] + best_pair[1] # 合并 merges.append(best_pair) vocab.add(new_token) # 更新词频 new_word_freqs = {} for word, freq in word_freqs.items(): new_word = merge_in_word(word, best_pair) new_word_freqs[new_word] = new_word_freqs.get(new_word, 0) + freq word_freqs = new_word_freqs return vocab, merges 编码过程 训练好的BPE分词器在编码新文本时,按训练时的合并顺序逐步合并: def bpe_encode(text, merges, special_tokens): """BPE编码""" tokens = [] for word in text.split(): # 先检查是否是特殊token if word in special_tokens: tokens.append(word) continue # 字符级开始 word_tokens = list(word) # 按合并顺序尝试合并 for (a, b) in merges: i = 0 while i < len(word_tokens) - 1: if word_tokens[i] == a and word_tokens[i+1] == b: word_tokens[i] = a + b del word_tokens[i+1] else: i += 1 tokens.extend(word_tokens) return tokens Byte-level BPE GPT-2引入的改进——在字节级别而非字符级别进行BPE。这使得词表可以处理任意UTF-8字符,彻底解决了未登录字符(OOV)问题: ...

2026-07-02 · 4 min · 734 words · 硅基 AGI 探索者
PagedAttention实现

PagedAttention实现细节

操作系统启发:虚拟内存 PagedAttention的核心灵感来自操作系统的虚拟内存管理。在OS中,进程的虚拟内存被分成固定大小的页(Page),物理内存不必连续分配。 PagedAttention将这一思想应用于KV Cache管理: 传统KV Cache: [token0, token1, ..., tokenN] 在物理显存中必须连续 PagedAttention KV Cache: [token0-15] → GPU显存块#37 [token16-31] → GPU显存块#102 [token32-47] → GPU显存块#58 ... (物理上不连续,逻辑上连续) 块式KV Cache存储 块结构定义 class KVBlock: def __init__(self, block_size, n_layers, n_kv_heads, head_dim, dtype=torch.float16): self.block_size = block_size # 每块存储的token数(如16) self.n_layers = n_layers self.n_kv_heads = n_kv_heads self.head_dim = head_dim # 每块的形状: [n_layers, block_size, n_kv_heads, head_dim] self.k = torch.zeros( n_layers, block_size, n_kv_heads, head_dim, dtype=dtype ) self.v = torch.zeros_like(self.k) self.ref_count = 0 # 引用计数(用于共享) self.last_used = 0 # 最后使用时间(LRU驱逐) 块表(Block Table) 每个请求有一个块表,将逻辑token位置映射到物理块: class BlockTable: def __init__(self, request_id, block_size): self.request_id = request_id self.block_size = block_size self.physical_blocks = [] # 物理块索引列表 self.logical_to_physical = {} # 逻辑位置 → 物理块索引 def get_physical_block(self, logical_pos): """获取逻辑位置的物理块""" block_idx = logical_pos // self.block_size offset = logical_pos % self.block_size if block_idx >= len(self.physical_blocks): return None, None # 尚未分配 return self.physical_blocks[block_idx], offset def append_block(self, physical_block_idx): """追加一个新物理块""" self.physical_blocks.append(physical_block_idx) 物理块管理 块池(Block Pool) 所有物理块由中央块池管理: ...

2026-07-02 · 4 min · 798 words · 硅基 AGI 探索者
连续批处理

连续批处理内部原理

静态批处理的效率问题 在传统LLM推理中,请求被分成固定大小的batch。当batch中某个请求完成后,其占用的GPU资源(显存、计算单元)在整个batch完成前都无法释放——即使该请求已经生成了EOS token。 这在大语言模型服务中造成了严重的资源浪费。用户请求的长度差异巨大——有的只需生成10个token,有的需要生成1000个token。静态批处理强迫"快请求等待慢请求",GPU计算能力被大量闲置。 连续批处理的核心思想 连续批处理(Continuous Batching,也称动态批处理)的核心创新是:不再等待整个batch完成,而是在每个生成步骤后动态重组batch。 当一个请求完成时,立即用新请求填充其位置,始终保持GPU满载。 与静态批处理对比 静态批处理(batch_size=4,完成时间120ms): 时刻0: [Req1, Req2, Req3, Req4] → 开始生成 时刻1: Req1完成(用时20ms),但仍在等待其他请求 时刻2: Req2完成(用时50ms),仍在等待 时刻3: Req3完成(用时80ms),仍在等待 时刻4: Req4完成(用时120ms),batch完成 → 实际吞吐:4个请求 / 120ms 连续批处理: 时刻0: [Req1, Req2, Req3, Req4] → 开始生成 时刻1: Req1完成 → 立即加入Req5 → [Req5, Req2, Req3, Req4] 时刻2: Req2完成 → 立即加入Req6 → [Req5, Req6, Req3, Req4] ... → 实际吞吐:~10个请求 / 120ms(约2.5倍提升!) vLLM中的连续批处理实现 请求状态机 每个请求在vLLM中经历以下状态: class RequestState(Enum): WAITING = 0 # 等待被调度 RUNNING = 1 # 正在生成 PREEMPTED = 2 # 被抢占(显存不足) FINISHED = 3 # 完成 class Request: def __init__(self, prompt, max_tokens): self.state = RequestState.WAITING self.prompt = prompt self.output_tokens = [] self.max_tokens = max_tokens self.current_step = 0 调度循环 vLLM的调度器在每一步动态决定哪些请求参与计算: ...

2026-07-02 · 3 min · 519 words · 硅基 AGI 探索者
混合分辨率多尺度处理

混合分辨率:多尺度处理

混合分辨率的核心动机 在传统的Transformer中,所有token都被平等对待——无论这个token是高频细节(如图像中的边缘像素)还是低频概览(如文档的整体结构)。混合分辨率(Mixture of Resolutions)打破了这一平等假设:不同的token使用不同的空间/时间分辨率进行处理。 这一思想在视觉-语言模型(VLM)中尤为重要。图像中的不同区域对理解的重要性不同——主体区域需要高分辨率,背景区域可以使用低分辨率。 多尺度Transformer的基本架构 分层Tokenization 将输入转换为不同尺度的token序列: def multi_scale_tokenize(image, scales=[1, 0.5, 0.25]): """ 将图像编码为多个尺度的token序列 scales: 不同尺度的缩放因子 """ token_sequences = [] for scale in scales: # 缩放图像 scaled = F.interpolate(image, scale_factor=scale) # 分块编码 patches = patchify(scaled, patch_size=16) tokens = vision_encoder(patches) token_sequences.append(tokens) # 拼接所有尺度的token return torch.cat(token_sequences, dim=1) 分辨率路由器 使用路由器将token分配到不同分辨率处理路径: class ResolutionRouter(nn.Module): def __init__(self, d_model, n_resolutions=3): super().__init__() self.router = nn.Linear(d_model, n_resolutions) self.resolution_embeddings = nn.Parameter( torch.randn(n_resolutions, d_model) ) def forward(self, x): """ x: [batch, seq_len, d_model] """ # 路由分数 router_logits = self.router(x) # [batch, seq_len, n_res] router_probs = F.softmax(router_logits, dim=-1) # 选择分辨率 resolution_ids = router_probs.argmax(dim=-1) # [batch, seq_len] # 添加分辨率嵌入 resolution_emb = self.resolution_embeddings[resolution_ids] x = x + resolution_emb return x, resolution_ids 视觉-语言模型中的应用 高分辨率区域识别 在VLM中,系统需要识别图像中哪些区域需要高分辨率处理: ...

2026-07-02 · 3 min · 543 words · 硅基 AGI 探索者
序列并行

序列并行:超长序列训练

序列并行:解决超长序列的显存瓶颈 张量并行(TP)将模型参数切分到多个GPU,流水线并行(PP)将模型层切分到多个GPU,但两者都没有解决序列长度带来的显存问题。 当序列长度达到128K甚至1M时,即使使用TP和PP,每个GPU上存储的激活值仍然是 O(seq_len × hidden_dim)。序列并行(Sequence Parallelism, SP)正是为了解决这个问题——将序列维度也切分到多个GPU上。 序列并行的核心思想 基本切分 将序列维度切分到N个GPU上,每个GPU只处理 seq_len/N 个token: 原始序列: [t0, t1, t2, ..., t_{L-1}] GPU 0: [t0, t1, ..., t_{L/N-1}] GPU 1: [t_{L/N}, ..., t_{2L/N-1}] ... GPU N-1: [t_{L-L/N}, ..., t_{L-1}] 通信需求 序列切分后,LayerNorm和Dropout等逐token操作可以独立进行(无需通信),但注意力计算需要完整的序列信息——每个token的Query需要与所有token的Key/Value计算注意力。 这是序列并行的核心挑战:如何在不重新聚合完整序列的前提下完成注意力计算? DeepSpeed-Ulysses 原理 DeepSpeed-Ulysses使用All-to-All通信来实现序列并行注意力: class UlyssesAttention(nn.Module): def __init__(self, d_model, n_heads, n_gpus): super().__init__() self.n_gpus = n_gpus self.local_heads = n_heads // n_gpus # 每个GPU只存储部分注意力头的参数 self.W_q = nn.Linear(d_model, self.local_heads * (d_model // n_heads)) self.W_k = nn.Linear(d_model, self.local_heads * (d_model // n_heads)) self.W_v = nn.Linear(d_model, self.local_heads * (d_model // n_heads)) self.W_o = nn.Linear(self.local_heads * (d_model // n_heads), d_model) def forward(self, x): # x的形状: [batch, local_seq_len, d_model] # 每个GPU持有序列的一部分 # 1. 本地计算QKV Q_local = self.W_q(x) # [batch, local_seq, local_heads * head_dim] K_local = self.W_k(x) V_local = self.W_v(x) # 2. All-to-All通信:重新排列序列和头维度 # 从 [batch, local_seq, local_heads, head_dim] # 到 [batch, global_seq, local_heads, head_dim] Q_global = all_to_all(Q_local, split_dim=1, concat_dim=2) K_global = all_to_all(K_local, split_dim=1, concat_dim=2) V_global = all_to_all(V_local, split_dim=1, concat_dim=2) # 3. 本地注意力计算(现在每个GPU有完整的序列,但只有部分头) attn_out = flash_attention(Q_global, K_global, V_global) # 4. All-to-All通信:将结果按序列切分回去 attn_local = all_to_all(attn_out, split_dim=2, concat_dim=1) # 5. 输出投影 output = self.W_o(attn_local) return output 通信分析 Ulysses需要2次All-to-All通信(前向)+ 2次All-to-All(反向)。 ...

2026-07-02 · 3 min · 430 words · 硅基 AGI 探索者
流水线并行

流水线并行2026:GPipe到PipeDream

流水线并行:将模型按层切分 张量并行在层内切分权重,流水线并行(Pipeline Parallelism, PP)在层间切分——将模型的不同层分配到不同GPU上。GPU 0持有第1-8层,GPU 1持有第9-16层,依次类推。 这种切分方式让每个GPU只需存储部分层的参数和激活值,极大降低了单GPU显存需求。但它引入了一个新问题:流水线气泡——不同GPU之间需要等待数据传递。 GPipe:朴素流水线 工作原理 GPipe将一个batch切分为多个micro-batch,然后像工厂流水线一样依次处理: 时间: 1 2 3 4 5 6 7 8 9 10 GPU 0: M1 M2 M3 M4 .. .. .. .. .. .. GPU 1: .. M1 M2 M3 M4 .. .. .. .. .. GPU 2: .. .. M1 M2 M3 M4 .. .. .. .. GPU 3: .. .. .. M1 M2 M3 M4 .. .. .. ↑ 前向完成 GPU 3: .. .. .. .. .. .. .. M4' M3' M2' M1' ← 反向 GPU 2: .. .. .. .. .. .. M4' M3' M2' M1' ... M1-M4是4个micro-batch的前向传播,M1’-M4’是反向传播。 ...

2026-07-02 · 3 min · 584 words · 硅基 AGI 探索者
鲁ICP备2026018361号