概述
传统大语言模型(LLM)依赖分词器(tokenizer),将文本切分为 BPE、WordPiece 等子词单元。然而分词器存在根本性局限:
- 预定义词表限制:无法处理超出词表的字符(如新词、稀有语言)
- 语言偏见:不同语言的词表效率差异大
- 鲁棒性差:拼写错误、字符替换可完全破坏 token
- 训练-推理不一致:新词表的引入会完全失效
字节级 Transformer 通过直接在字节层面建模消除这些限制。Meta 提出的 Byte Latent Transformer (BLT) (Pagnoni et al., arXiv 2024) 1 是最新突破:通过动态分块与潜在表示,在保持性能的同时显著提升效率。
本文档深入讲解 BLT 的核心思想、架构细节、数学推导与实现。
1. 字节级建模的动机
1.1 分词器的根本局限
问题 1:预定义词表
传统 LLM 的输入是 BPE 词表中的 token id。词表大小通常 32K-256K:
"Hello" → [15496, 0]
"Hello world" → [15496, 995, 0]
但如果输入包含生僻字或新词:
"supercalifragilisticexpialidocious" → [?] # 超出词表
问题 2:低资源语言
不同语言在词表中的表示效率差异巨大:
| 语言 | 词表效率 |
|---|---|
| 英语 | 100% |
| 中文 | 60-70% |
| 阿拉伯语 | 40-50% |
| 斯瓦希里语 | 20-30% |
结果:低资源语言需要更多 token 处理相同语义。
问题 3:鲁棒性问题
轻微字符扰动(如拼写错误)会完全改变 token 序列:
"Hello" → [15496]
"Helo" → [402, 1753] # 完全不同的 token 序列
这导致模型对输入扰动极其敏感。
1.2 字节级建模的优势
直接处理字节(byte)序列:
"Hello" → [72, 101, 108, 108, 111] # ASCII 码
"你好" → [228, 189, 160, 229, 165, 189] # UTF-8 字节
优势:
- 通用性:覆盖所有 UTF-8 字符
- 无词表依赖:无需预定义词表
- 鲁棒性:字符级扰动只改变少量字节
- 跨语言一致:所有语言用相同的字节级处理
1.3 字节级建模的挑战
字节级建模也面临挑战:
- 序列变长:1 个 BPE token ≈ 4 字节,导致 4 倍序列长度
- 计算成本:Transformer 复杂度 , 翻倍意味着 4 倍计算
- 短程依赖主导:大部分字节级依赖是局部的
解决思路:动态分块(dynamic patching)+ 潜在 Transformer。
2. 字节级 Transformer 的演进
2.1 ByT5 (Xue et al., 2022)
直接用 Transformer 处理字节序列:
[byte1, byte2, ..., byteN] → Transformer → [output1, ..., outputN]
问题:序列长度爆炸,计算成本高。
2.2 早期字节级模型的优化
MegaByte (Yu et al., 2023)
将字节分组为”块”(patch),每块约 4-8 字节:
[byte1..byte8] → patch1 → latent1
[byte9..byte16] → patch2 → latent2
...
每个 patch 通过全局 Transformer 处理(latent 级别),patch 内通过局部模型处理(字节级别)。
Charformer (Tay et al., 2022)
通过梯度哈希动态分块:
Bytes → [byte_pairs] → pool → [patches]
分块大小由梯度信号决定,可学习。
2.3 BLT 的突破
BLT 在 MegaByte 的基础上提出:
- 熵驱动的动态分块:基于下一字节的预测难度分块
- 三阶段架构:本地编码器、潜在 Transformer、本地解码器
- 计算效率:相比 MegaByte 节省 50% FLOPs
3. BLT 的核心思想
3.1 关键洞察
洞察 1:不同位置的字节需要的”处理深度”不同
- 简单模式(如常见单词):少量计算即可预测
- 复杂模式(如新词、稀有字符):需要更多计算
洞察 2:可以基于预测难度动态分配计算
3.2 动态分块
BLT 使用下一字节熵(next-byte entropy)作为分块信号:
其中 是下一字节, 是已观察的字节序列。
直觉:
- 简单模式( 低):字节序列可预测,可以分块
- 复杂模式( 高):字节序列不确定,需要细分
3.3 分块算法
算法 1:熵驱动的分块
def entropy_based_patching(byte_sequence, model, threshold):
patches = []
current_patch = []
for byte in byte_sequence:
# 预测下一字节的分布
next_byte_logits = model.predict(current_patch + [byte])
entropy = compute_entropy(softmax(next_byte_logits))
if entropy > threshold:
# 当前字节预测困难,作为新分块的开始
if current_patch:
patches.append(current_patch)
current_patch = [byte]
else:
# 当前字节容易预测,加入当前分块
current_patch.append(byte)
if current_patch:
patches.append(current_patch)
return patches关键参数:
threshold:分块阈值,控制块大小- 阈值越高 → 块越大 → 计算越少(但精度可能下降)
- 阈值越低 → 块越小 → 计算越多(精度高)
4. BLT 架构详解
4.1 三阶段架构
输入字节序列
↓
[本地编码器] → 字节级表示
↓
[动态分块] → patch 序列
↓
[潜在 Transformer] → patch 级表示
↓
[动态扩展] → 字节级表示
↓
[本地解码器] → 预测下一字节
4.2 本地编码器(Local Encoder)
将字节映射到高维表示:
使用轻量级 Transformer 或 CNN,保持在字节级别。
4.3 动态分块模块
基于熵阈值分块:
class DynamicPatcher(nn.Module):
def __init__(self, entropy_model, threshold):
super().__init__()
self.entropy_model = entropy_model
self.threshold = threshold
def forward(self, byte_embeddings):
"""
byte_embeddings: (B, N, D) 字节级嵌入
返回: (B, M, D) patch 级嵌入
"""
B, N, D = byte_embeddings.shape
# 计算每个位置的熵
entropies = self.entropy_model(byte_embeddings) # (B, N)
# 根据熵确定分块边界
# 高熵位置为新分块的开始
boundaries = entropies > self.threshold # (B, N)
boundaries[:, 0] = True # 第一个字节总是新分块
# 池化得到 patch 级表示
patches = []
for b in range(B):
patch_starts = boundaries[b].nonzero().squeeze(-1)
patch_ends = torch.cat([patch_starts[1:], torch.tensor([N])])
batch_patches = []
for start, end in zip(patch_starts, patch_ends):
# 平均池化
patch = byte_embeddings[b, start:end].mean(dim=0)
batch_patches.append(patch)
patches.append(torch.stack(batch_patches))
# Padding 到相同长度
max_patches = max(p.size(0) for p in patches)
padded = torch.zeros(B, max_patches, D, device=byte_embeddings.device)
for b, p in enumerate(patches):
padded[b, :p.size(0)] = p
return padded4.4 潜在 Transformer(Latent Transformer)
处理 patch 序列的核心模块:
可以使用标准的 Transformer 架构,但由于 patch 数远少于字节数,计算量大幅减少。
4.5 本地解码器(Local Decoder)
将 patch 级表示扩展回字节级:
class LocalDecoder(nn.Module):
def __init__(self, hidden_dim, num_bytes=256):
super().__init__()
self.byte_head = nn.Linear(hidden_dim, num_bytes)
def forward(self, patch_repr, patch_boundaries):
"""
patch_repr: (B, M, D)
patch_boundaries: (B, N) 每个字节属于哪个 patch
返回: (B, N, num_bytes) 字节级预测
"""
B, M, D = patch_repr.shape
N = patch_boundaries.size(1)
# 将 patch 表示扩展到字节级别
byte_repr = patch_repr[:, patch_boundaries] # (B, N, D)
# 预测下一字节
logits = self.byte_head(byte_repr)
return logits4.6 完整架构
class ByteLatentTransformer(nn.Module):
"""完整 BLT 架构"""
def __init__(self, byte_dim, latent_dim, num_layers_local_enc,
num_layers_latent, num_layers_local_dec,
num_heads=8, num_bytes=256, entropy_threshold=0.5):
super().__init__()
# 字节嵌入
self.byte_embedding = nn.Embedding(num_bytes, byte_dim)
# 本地编码器
self.local_encoder = LocalEncoder(
byte_dim, num_layers_local_enc, num_heads
)
# 熵模型(用于分块)
self.entropy_model = EntropyModel(byte_dim)
self.patcher = DynamicPatcher(self.entropy_model, entropy_threshold)
# patch 投影
self.patch_projection = nn.Linear(byte_dim, latent_dim)
# 潜在 Transformer
self.latent_transformer = LatentTransformer(
latent_dim, num_layers_latent, num_heads
)
# patch 扩展
self.patch_expander = PatchExpander(latent_dim)
# 本地解码器
self.local_decoder = LocalDecoder(latent_dim, num_bytes)
def forward(self, input_bytes):
"""
input_bytes: (B, N) 字节序列
返回: (B, N, num_bytes) 下一字节预测
"""
# 1. 字节嵌入
x = self.byte_embedding(input_bytes) # (B, N, byte_dim)
# 2. 本地编码
x = self.local_encoder(x) # (B, N, byte_dim)
# 3. 动态分块
patches, patch_boundaries = self.patcher(x)
# patches: (B, M, byte_dim)
# patch_boundaries: (B, N)
# 4. patch 投影
latent = self.patch_projection(patches) # (B, M, latent_dim)
# 5. 潜在 Transformer
latent = self.latent_transformer(latent) # (B, M, latent_dim)
# 6. patch 扩展
byte_latent = self.patch_expander(latent, patch_boundaries)
# (B, N, latent_dim)
# 7. 本地解码
logits = self.local_decoder(byte_latent, patch_boundaries)
# (B, N, num_bytes)
return logits5. 数学分析
5.1 信息论视角
设输入字节序列为 ,BLT 的目标是最小化:
BLT 通过动态分块实现:
- 计算量最小化:低熵区域用大块
- 性能最大化:高熵区域用小块
5.2 分块大小的理论分析
设分块阈值为 ,平均块大小为 。FLOPs 与 的关系:
其中:
- :本地编/解码器单步计算量(常数)
- :潜在 Transformer 单步计算量
- :patch 数
越大 → patch 数越少 → 计算越省
但 不能太大,否则损失信息。最佳 由数据分布决定。
5.3 与传统 Transformer 的对比
设输入序列长度为 :
- 传统 Transformer: FLOPs
- BLT: ≈
节省比例: 倍。
对于 :64 倍 FLOPs 节省。
6. 训练策略
6.1 训练目标
标准语言模型目标:
6.2 熵模型训练
熵模型与主模型联合训练:
但更常见的是固定熵模型(或使用轻量级模型),仅训练主模型。
6.3 课程学习
训练过程中逐步增大块大小:
def get_block_size(epoch, max_epoch, min_block=2, max_block=16):
"""动态调整块大小"""
progress = epoch / max_epoch
block_size = min_block + (max_block - min_block) * progress
return int(block_size)6.4 多任务训练
BLT 可以联合训练多种任务:
- 语言建模:下一字节预测
- 分类任务:序列级标签预测
- 序列到序列:翻译、摘要等
7. 性能评估
7.1 与传统 LLM 的对比
| 模型 | Token 化 | 参数量 | FLOPs | 性能 |
|---|---|---|---|---|
| Llama 3 8B | BPE | 8B | 85.0 (MMLU) | |
| BLT 8B | 字节 | 8B | 85.4 (MMLU) | |
| BLT 8B (efficient) | 字节 | 8B | 84.5 (MMLU) |
关键结果:BLT 在相似性能下节省大量 FLOPs。
7.2 鲁棒性测试
字符扰动下的性能:
| 模型 | 干净数据 | 字符扰动 |
|---|---|---|
| Llama 3 8B | 100% | 23% |
| BLT 8B | 100% | 78% |
BLT 的鲁棒性显著优于传统 LLM。
7.3 多语言性能
在相同 token 数预算下:
| 模型 | 英语 | 中文 | 阿拉伯语 | 斯瓦希里语 |
|---|---|---|---|---|
| Llama 3 8B | 100% | 95% | 82% | 65% |
| BLT 8B | 100% | 98% | 90% | 88% |
BLT 在低资源语言上优势明显。
8. 扩展与应用
8.1 BLT 的扩展
多模态 BLT
将字节扩展到视觉 token、音频 token:
class MultimodalBLT(nn.Module):
def __init__(self, image_patch_dim, audio_frame_dim, byte_dim):
super().__init__()
self.image_encoder = ImageToBytes(image_patch_dim, byte_dim)
self.audio_encoder = AudioToBytes(audio_frame_dim, byte_dim)
self.blt = ByteLatentTransformer(byte_dim, ...)指令微调 BLT
标准指令微调方法:
def train_blt_instruction(model, dataset):
for batch in dataset:
# 输入:指令 + 字节序列
# 输出:响应字节序列
loss = model.forward(batch.input_bytes, batch.target_bytes)
loss.backward()
optimizer.step()8.2 应用场景
场景 1:代码生成
字节级模型对代码更鲁棒:
- 缩进变化不影响 token 化
- 特殊字符正确处理
场景 2:低资源语言翻译
字节级建模天然适合低资源语言。
场景 3:DNA/蛋白质序列
生物学序列天然是”字节级”(A、T、C、G),BLT 可直接应用。
8.3 工业部署
推理优化
class BLTInference:
def __init__(self, model, tokenizer_bytes):
self.model = model
self.byte_tokenizer = tokenizer_bytes
def generate(self, prompt_bytes, max_length=512):
bytes_seq = prompt_bytes
for _ in range(max_length):
# 字节级预测
logits = self.model(bytes_seq)
next_byte = sample(logits[:, -1])
bytes_seq = torch.cat([bytes_seq, next_byte], dim=1)
return bytes_seqKV Cache 优化
BLT 的 patch 序列较短,KV Cache 占用显著降低:
| 模型 | 序列长度 | KV Cache 大小 |
|---|---|---|
| 传统 LLM | 2048 | 16 MB |
| BLT | 2048/8 = 256 | 2 MB |
9. 局限与挑战
9.1 计算效率
虽然 BLT 比纯字节 Transformer 高效,但仍需要:
- 动态分块的额外计算
- 本地编/解码器
未来工作需要进一步优化。
9.2 长度泛化
BLT 的动态分块依赖训练时见过的模式。对于极长序列可能需要重新训练。
9.3 多语言平衡
虽然字节级建模避免了词表偏见,但仍需要平衡不同语言的数据分布。
9.4 与现有 LLM 生态的兼容性
字节级模型无法直接使用现有的 token 化工具链:
- 数据预处理需要重写
- 评测工具需要适配
- 部署基础设施需要更新
10. 未来展望
10.1 趋势 1:完全无 Token 化
未来可能所有模型都不再使用分词器:
- 字节级(BLT)
- 像素级(图像 LLM)
- 采样级别(音频 LLM)
10.2 趋势 2:自适应计算
BLT 的动态分块是自适应计算的早期形式。未来可能:
- 每个位置独立决定计算量
- 基于任务的难度动态调整
10.3 趋势 3:跨模态统一
字节级、像素级、采样级的统一表示,使真正的多模态 LLM 成为可能。
10.4 趋势 4:硬件协同设计
针对字节级模型的硬件优化:
- 专用 BLT 加速器
- 字节级 SIMD 指令
- 低精度字节运算
11. 总结
11.1 BLT 的核心贡献
- 理论创新:动态分块的信息论解释
- 架构创新:三阶段架构(本地编/潜在 TF/本地解)
- 效率突破:相比字节 Transformer 节省 50-90% FLOPs
- 鲁棒性提升:抗字符扰动能力大幅提升
- 多语言友好:字节级天然跨语言
11.2 BLT 的实践意义
- 降低部署门槛:无需维护分词器
- 提升可解释性:字符级可解释
- 扩展应用范围:支持更多语言和模态
- 改善长尾性能:稀有字符、罕见模式
11.3 BLT 的局限
- 计算效率仍有提升空间
- 长度泛化待解决
- 工业生态需要重新构建
11.4 BLT 与其他方法的对比
| 特性 | BPE Tokenizer | ByT5 | BLT |
|---|---|---|---|
| 词表依赖 | 是 | 否 | 否 |
| 计算效率 | 高 | 低 | 中高 |
| 性能 | 高 | 中 | 高 |
| 鲁棒性 | 低 | 高 | 高 |
| 多语言 | 差 | 中 | 好 |
参考
Footnotes
-
Pagnoni et al., “Byte Latent Transformer: Patches Scale Better Than Tokens”, arXiv 2412.09871, 2024 (ACL 2025) ↩