分词器对比

分词器对比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 探索者
张量并行Megatron-LM

张量并行详解:Megatron-LM

为什么需要张量并行? 当模型参数量超过单GPU显存容量时,模型并行成为必需。张量并行(Tensor Parallelism, TP)是最常用的模型并行方式——将单个层的权重矩阵切分到多个GPU上,每个GPU计算一部分结果,然后通过通信合并。 NVIDIA的Megatron-LM提出了张量并行的标准方案,成为2026年大模型训练的基础设施。 张量并行的核心原理 线性层的切分 一个标准线性层 Y = XW,其中 W ∈ ℝ^{d×h}。将W切分为两个子矩阵有两种方式: 列切分(Column Parallel) 将W按列切分:W = [W₁, W₂],每个GPU持有 W_i ∈ ℝ^{d×h/N} GPU 1: Y₁ = XW₁ ∈ ℝ^{n×h/N} GPU 2: Y₂ = XW₂ ∈ ℝ^{n×h/N} Y = [Y₁, Y₂] ∈ ℝ^{n×h} 列切分后,每个GPU独立计算Y的一部分列,最终通过All-Gather合并。但更聪明的方式是——不需要显式合并,因为后续操作可以利用分块结构。 行切分(Row Parallel) 将W按行切分:W = [W₁; W₂],每个GPU持有 W_i ∈ ℝ^{d/N×h} GPU 1: Y₁ = X₁W₁ (X₁是X的前d/N列) GPU 2: Y₂ = X₂W₂ (X₂是X的后d/N列) Y = Y₁ + Y₂ (All-Reduce) 行切分后需要All-Reduce求和。 ...

2026-07-02 · 3 min · 461 words · 硅基 AGI 探索者
梯度检查点

梯度检查点原理与实现

训练大模型的显存困境 训练一个70B参数的模型,仅模型参数(FP16)就需要140GB显存。加上梯度(140GB)、优化器状态(Adam需要280GB)和激活值,总显存需求轻松超过600GB——即使8卡A100 80GB也无法满足。 在这些显存消耗中,激活值是最容易被忽视的部分。在标准反向传播中,前向传播的所有中间激活值都需要保存下来供反向传播计算梯度使用。对于70B模型、batch_size=4、seq_len=4096,激活值占用可达100GB以上。 梯度检查点(Gradient Checkpointing)通过"以时间换空间"的策略,大幅减少激活值的显存占用。 标准反向传播的显存分析 在标准反向传播中,每个操作的前向输出都需要被保存: 前向: x → Layer1 → a1 → Layer2 → a2 → Layer3 → a3 → Loss 保存: a1, a2, a3 (以及各层的中间状态) 反向: Loss ← ∂L/∂a3 ← ∂L/∂a2 ← ∂L/∂a1 ← ∂L/∂x 使用: 重新使用a3计算a2的梯度,重新使用a2计算a1的梯度,... 对于N层Transformer,每层的激活值大小为 O(batch × seq_len × hidden_dim)。所有层的激活值总和为 O(N × batch × seq_len × hidden_dim),与层数线性增长。 梯度检查点的核心思想 选择性重计算 梯度检查点的核心洞察:不需要保存所有层的激活值,只需保存部分"检查点"层的输入,其余层的激活值在反向传播时重新计算。 检查点设置(每2层一个检查点): 保存: x, a2, a4, a6, ... 前向: x → L1 → a1 → L2 → a2* → L3 → a3 → L4 → a4* → ... (* = 保存的检查点) 反向: 从a6*出发,重新前向计算a5, a4→计算梯度 从a4*出发,重新前向计算a3, a2→计算梯度 ... 显存-计算权衡 假设N层Transformer,每隔k层设置一个检查点: ...

2026-07-02 · 3 min · 514 words · 硅基 AGI 探索者
激活函数综述

激活函数综述2026

激活函数的角色 激活函数是神经网络中引入非线性的关键组件。没有激活函数,多层线性变换的堆叠等价于单层线性变换——网络的表达能力将被严重限制。激活函数的选择直接影响模型的训练动态、收敛速度和最终性能。 在LLM时代,激活函数的演进从ReLU到GELU再到SwiGLU,每一步都带来了可测量的性能提升。 ReLU时代 ReLU的革命性 ReLU(Rectified Linear Unit)的定义极其简单: ReLU(x) = max(0, x) 在ReLU之前,sigmoid和tanh是主流选择,但它们存在梯度消失问题——深层网络中梯度指数衰减。ReLU的梯度在正区间恒为1,有效解决了梯度消失问题,使得深层网络的训练成为可能。 ReLU的缺陷 Dead ReLU问题:当输入持续为负时,ReLU的梯度为零,神经元将永久"死亡"无法恢复。这在学习率设置不当时尤为严重。 非零中心化:ReLU的输出始终非负,导致后续层的输入分布偏向正方向,影响梯度下降效率。 Leaky ReLU与变体 为解决Dead ReLU问题,多种变体被提出: def leaky_relu(x, negative_slope=0.01): """Leaky ReLU: 负区间保留小梯度""" return torch.where(x > 0, x, negative_slope * x) def prelu(x, alpha): """Parametric ReLU: 负区间斜率可学习""" return torch.where(x > 0, x, alpha * x) def elu(x, alpha=1.0): """ELU: 负区间平滑过渡到指数""" return torch.where(x > 0, x, alpha * (torch.exp(x) - 1)) 这些变体在CV领域有一定应用,但在LLM中几乎未被采用——LLM的激活函数走上了另一条路。 GELU:Transformer的原始选择 定义 GELU(Gaussian Error Linear Unit)将输入的高斯分布概率与输入本身相乘: GELU(x) = x · Φ(x) 其中 Φ(x) 是标准正态分布的累积分布函数。实践中常使用近似: def gelu(x): """精确GELU""" return 0.5 * x * (1 + torch.erf(x / math.sqrt(2))) def gelu_tanh_approx(x): """tanh近似(更快)""" return 0.5 * x * (1 + torch.tanh(math.sqrt(2/math.pi) * (x + 0.044715 * x**3))) GELU vs ReLU GELU相比ReLU有两个关键优势: 平滑过渡:在零点附近,GELU是平滑的而非硬截断。这使得梯度更连续,训练更稳定 随机正则化:GELU隐含了一种随机dropout机制——输入越大,被保留的概率越高。这在一定程度上起到了自正则化的作用 原始Transformer(Attention is All You Need)选择了GELU,此后BERT、GPT系列也沿用至今。 ...

2026-07-02 · 2 min · 369 words · 硅基 AGI 探索者
神经网络归一化

神经网络归一化:LN vs BN vs RMSNorm

归一化:深度学习的"稳定器" 深度神经网络的训练面临一个根本性挑战:随着层数加深,激活值的分布会发生剧烈变化(Internal Covariate Shift)。这种分布偏移导致上层需要不断适应下层的输出分布变化,学习变得困难。归一化层通过将激活值"拉回"到稳定分布,有效缓解了这一问题。 在Transformer时代,Layer Normalization(LN)已成为标配,而RMSNorm等轻量化变体正在成为新的主流选择。 Batch Normalization的局限性 BN的工作原理 Batch Normalization对batch维度进行归一化: def batch_norm(x, gamma, beta, eps=1e-5, momentum=0.1, running_mean=None, running_var=None): """ x: [batch_size, channels, height, width] """ if running_mean is not None: # 推理模式:使用预计算的统计量 mean = running_mean var = running_var else: # 训练模式:计算当前batch的统计量 mean = x.mean(dim=(0, 2, 3), keepdim=True) var = x.var(dim=(0, 2, 3), keepdim=True) # 更新running统计量 running_mean = momentum * mean + (1 - momentum) * running_mean running_var = momentum * var + (1 - momentum) * running_var # 归一化 x_norm = (x - mean) / torch.sqrt(var + eps) # 仿射变换 return gamma.view(1, -1, 1, 1) * x_norm + beta.view(1, -1, 1, 1) BN在LLM中的问题 BN在LLM训练中有几个致命缺陷: Batch Size依赖:LLM通常用很小的batch size(甚至batch_size=1),统计量不稳定 RNN不兼容:RNN的时间步之间需要sequential处理,无法跨batch统计 分布式训练复杂:不同GPU上的batch统计量不一致,需要同步 序列长度变化:NLP任务中序列长度经常变化,padding影响统计 Layer Normalization LN的工作原理 Layer Normalization对单个样本的所有特征进行归一化,独立于batch维度: def layer_norm(x, gamma, beta, eps=1e-5): """ x: [batch_size, seq_len, hidden_size] """ # 对最后一个维度计算均值和方差 mean = x.mean(dim=-1, keepdim=True) var = x.var(dim=-1, keepdim=True) # 归一化 x_norm = (x - mean) / torch.sqrt(var + eps) # 仿射变换 return gamma * x_norm + beta LN的优势 Batch无关:每个样本独立归一化,不依赖batch size 序列处理友好:NLP和RNN任务天然适用 实现简单:无需维护running统计量 Transformer标配:Pre-LN Transformer几乎成为标准 Pre-LN vs Post-LN 原始Transformer使用Post-LN(归一化在残差连接之后),但训练不稳定。Pre-LN将归一化移到残差分支内部: ...

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