How Transformer LLMs Work

语言模型发展历史

Non-transformer-models -> Encoder-Only -> Decoder-Only -> Encoder-Decoder Models

典型的语言模型任务

  • 从非结构化文本出发
  • 经过语言 AI 模型处理
  • 完成三类任务
    • 文本生成
    • Embeddings
    • 分类任务

文本的 Embeddings 表示

Bag-of-Words

  • 输入文本用空格来进行分割,得到一个单词数组
    • 这个过程叫 Tokenization
    • 单词数组称为 Tokens
  • 所有 Unique 的 Tokens 集合称为 Vocalbulary
  • Bag-of-Words:统计单词的频次,得到输入文本的树枝表示(Numeric Representation)
    • Vocalbulary:[that, is, a, cute, dog, my, cat]
    • Input1 向量表示: [1, 1, 1, 1, 1, 0, 0]
    • Input2 向量表示: [0, 1, 0, 1, 0, 1, 1]

Vector Embeddings

  • Bag-of-Words:不考虑文本的语义和上下文
  • Word2Vec:通过 Neural Network 把单词的含义表示在 Vector Embeddings 中
  • Neural Network 神经网络:
    • 输入层、隐藏层、输出层
    • 层与层之间有不同的连接权重 Weights

通过输入数据来训练 Neural Network,Word2Vec 学习到两个单词作为「邻居」同时出现的概率。

得到一个单词(比如 cats)的 Embeddings 数值表示,含义相近的单词距离较近。

输入单词、文本、文章经过 Tokenization 和 Word2Vec 得到对应的 Embeddings 表示。注意 Tokenizer 并不总是按照空格分割,比如 vocalization tokenization 后是 vocal + ##ization,这个原因是字典的规模是固定的。

Tokenizer Python 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from transformers import AutoTokenizer

# A list of colors in RGB for representing the tokens
colors = [
'102;194;165', '252;141;98', '141;160;203',
'231;138;195', '166;216;84', '255;217;47'
]

def show_tokens(sentence: str, tokenizer_name: str):
""" Show the tokens each separated by a different color """

# Load the tokenizer and tokenize the input
tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
token_ids = tokenizer(sentence).input_ids

# Extract vocabulary length
print(f"Vocab length: {len(tokenizer)}")

# Print a colored list of tokens
for idx, t in enumerate(token_ids):
print(
f'\x1b[0;30;48;2;{colors[idx % len(colors)]}m' +
tokenizer.decode(t) +
'\x1b[0m',
end=' '
)

sentence = """
English and CAPITALIZATION
🎵 鸟
show_tokens False None elif == >= else: two tabs:" " Three tabs: " "
12.0*50=600
"""
show_tokens(sentence, "bert-base-cased")
show_tokens(sentence, "Xenova/gpt-4")
show_tokens(sentence, "Qwen/Qwen2-VL-7B-Instruct")

Python 代码输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vocab length: 28996
[CLS] English and CA ##PI ##TA ##L ##I ##Z ##AT ##ION [UNK] [UNK] show _ token ##s F ##als ##e None el ##if = = > = else : two ta ##bs : " " Three ta ##bs : " " 12 . 0 * 50 = 600 [SEP]

Vocab length: 100263
English and CAPITAL IZATION
� � � � � �
show _tokens False None elif == >= else : two tabs :" " Three tabs : " "
12 . 0 * 50 = 600

Vocab length: 151657
English and CAPITAL IZATION
🎵 � � �
show _tokens False None elif == >= else : two tabs :" " Three tabs : " "
1 2 . 0 * 5 0 = 6 0 0

基于 Attention 的 Context Encoding/Decoding

  • Word2Vec:是一个单词的静态 Embeddings
  • RNN(Recurrent Neural Netowks):循环神经网络
    • Encoder RNN:用来编码语言
    • Decoder RNN:用来生成语言
  • 左边的例子:英语 -> 荷兰语

自回归模型:利用变量过去的值来预测未来值的建模方法,核心思想是“用历史预测未来”。

在上面英语到荷兰语的翻译的案例中,每一个迭代过程会生成一个 Token,在下一个迭代会把历史升成的 Tokens 作为输入再去生成下一个 Token。

Context Embedding:单个 Embeding 难以表示长序列输入文本

Attention模型聚焦在部分输入文本上,在特定的 Context 下调节 Embedings 权重

把 Attention 机制应用到 Decoder 阶段,我们可以用每个原始单词的 Embedding 作为输入,所有输入单词的 Embeddings 都传递给 Decoder。使用整个序列的 Embeddings 的方式会比单个 Context Embedding 获得更好的生成效果

Transformer 机制

Transformer 机制是在 「Attention is All you Need」论文中首先提出,仅仅基于 Attention 而不依赖 RNN 模型,这种机制通过在训练阶段的并行获得比 RNN 显著更高的计算效率,

  • Transformer 包含多个 Encoder/Decoder
  • Encoder 阶段用输入文本的 Attention 来更新序列的 Embeddings(Self-Attention),增加更多的上下文信息,通过前馈神经网络来获得输入序列的 Embedding
  • Decoder 阶段采用类似 Encoder 阶段的 Attention 机制更新 Embeddings(Masked self-attention),不同之处在于会忽略 Attention 矩阵右上角的值,更新后的 Embeddings 会强化输入 Token 排在前面,这样在生成文本时进行信息剪枝

这种基于 Encoder-Decoder 的 Transformer 架构适用于语言翻译场景,在 2018 年提出的 Bert(Bi-directional Encoder Representations from Transformers)。Bert 是 Encoder-Only 架构,同样基于 Self-Attention 机制,用于生成输入文本的 Contextualized Embeddings,Bert 会额外用一个 「CLS Token」(或者叫 Classification Token)针对不同的语言任务类型进行 Fine Tune。

Bert 模型使用 Masked Language Modeling 机制来训练。首先从输入文本序列中随机 Mask 部分单词,使用模型来预估这些 Masked 的单词,通过解构 Masked 单词的过程来训练模型,整个过程包含两个步骤:

  • 在大量数据上应用 Masked Language Modeling,这个过程称为 Pre-Training 预训练
  • 针对不同的场景去 Fine-Tune 模型,包括 Classification、NER、Paraphrase Identification 等

生成式模型(Generative Models)机制有所不同,输入文本序列随机初始化一组 Embeddings,通过 Decoder-Only Transformer 来生成下一个单词。最早的实现称为 GPT-1(Generative Pre-Trained transformer),GPT 没有使用 Encoder。语言模型当前最常见的就是 GPT 代表的生成式模型和 BERT 代表的表征模型。

生成式模型在计算中存在 Context Length 约束,即模型处理 Token 的长度,GPT-1 的最大 Context Length 是 512,意味着模型在给定时间内仅能处理 512 Tokens,这里包含了 Output 中生成且后续追加到 Input 的 Tokens。

生成式模型随着参数规模的增加,逐步显现出 LLM(Large Language Model)的强大。参数规模从 GPT-1 的 117Millon -> GPT-3 的 175 Billion,随着参数规模增加模型能力逐步增强,

2022 年随着 ChatGPT 发布,生成式模型开始快速增长,包括各种开源的生成式模型,模型应用也开始爆发。

Transformer LLM 架构剖析

Transformer LLMs 可以基于 Prompt 来生成输出文本,文本生成每个迭代会输出一个 Token。Transformer LLM 架构主要包含三个部分:

  • Tokenizer:输入文本到 Token & Embedding 表示,使用一个固定 size 的 vocalbulary 把输入拆解成多个 chunk。假设 vocalbulary 的 size 是 5w,那么对于每个输入 token 会通过 5w 维的向量来表示
  • Transformer Blocks:Transformer 机制相比过往的 RNN 更为强大的其中一个核心在于并行,不同 token 的处理能高效并行;其次是首字 token generation 之后,新生成的 token 会 append 到输入 token 中,前面的 token 都可以利用上一轮 Cache 的计算来加速,这个 Cache 称为 KV Caching。
  • LM Head:从 vocalbulary 中预测出不同 token 的输出概率分布,使用不同的 decoding strategy 选择出最终的 ouput token
    • Greedy decoding:选择 token 概率最大,temperature=0
    • Top-P:从 Top-P 中选择,增加一些随机性,temperature>0,好处是输出文本更符合人的表达方式

Transformer Blocks

Transformer Blocks 链式处理流程,每个 Block 包含 Self-Attention 和 Feed Forward Neural Network 两部分:

  • Self-Attention:Attention 帮助模型去关注 Context 上下文,主要包含 Relevance Scoring 和 Combining Information 两部分,包含投影矩阵:Query、Key、Value 矩阵

    • Relevance Scoring:Query Vector 和当前处理的 Token 相关,Keys Vector 和之前的 Tokens 相关,两者矩阵相乘得到不同单词的 Relevance Scores,所有 Scores 想加为 100%
    • Combining Information:Values Vector 和每个 Token 相关,Relevance Scores 和 Values Vector 相乘得到加权 Values 矩阵,加权 Values 矩阵进行累加,得到 Attention 处理后的 Embedding
  • Feed Forward Neural Network:用 Neural Network 来预估输入序列的下一个单词,可以理解成基于训练数据把文本序列关系编码到模型参数中

Transformer Python 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

# Load model and tokenizer
tokenizer = AutoTokenizer.from_pretrained(
"../models/microsoft/Phi-3-mini-4k-instruct"
)
model = AutoModelForCausalLM.from_pretrained(
"../models/microsoft/Phi-3-mini-4k-instruct",
device_map="cpu",
torch_dtype="auto",
trust_remote_code=True,
)

# Create a pipeline
generator = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
return_full_text=False,
do_sample=False,
)

prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened. "
output = generator(prompt)
print(output[0]['generated_text'])

输出文本:

1
2
3
4
5
6
7
8
9
Email to Sarah:

Subject: Sincere Apologies for the Gardening Mishap


Dear Sarah,


I hope this message finds you well. I am writing to express my deepest ap

基于 Prompt 生成单个单词:

1
2
3
4
5
6
7
8
9
prompt = "The capital of France is"
# Tokenize the input prompt
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
# Get the output of the model before the lm_head
model_output = model.model(input_ids)
# Get the output of the lm_head
lm_head_output = model.lm_head(model_output[0])
token_id = lm_head_output[0,-1].argmax(-1)
tokenizer.decode(token_id)

输出文本:

1
Paris

Transformer 演进

从 2017 年提出来的 Transformer Decoder,过去几年主要的演进:

  • Rotary Embeddings(RoPE):对于训练数据的处理,「多文档 + Padding 补齐」相比「单文档 + Padding 补齐」处理效率更高,对于前者在 Attention 计算过程中需要使用 Rotary Embeddings,用于在 Self-Attention 的 Relevance Scoring 阶段去补充不同 Document 的位置信息。
  • Mixture of Experts(MoE):在 FFNN 阶段使用多个子模型来提升 LLM 的效果(用 Sparse Model 来替代 Dense Model),在每一个 Layer 会把 Input Route 到一个或者多个 Experts 进行处理。Router 本身也是一个轻量级的 FFNN,用于计算不同 Experts 的概率,选择概率最高或者增加一些随机性(Top-N)。
    • 优势:推理阶段的显存要求更低、推理性能更高、模型架构灵活性高
    • 不足:模型加载阶段显存要求更高,更高的 overfitting 风险、训练阶段带来更大的挑战

MoE 模型的参数量可能会更大,意味着加载模型需要更大的显存,但因为在推理过程中仅仅部分 Experts 会被激活,推理效率和效果往往会更高。以 Mixtral 8x7B 模型为例:

  • Embeddings/Attention/Router/LM Head 等参数是共享的,Experts 参数量共 45B(单个 Expert 参数 5.6B),加载模型总计需要 Load 46.7B 模型参数
  • 推理阶段 Router 会选择两个 Expert 进行处理,Experts 参数量共 11.2B,推理阶段总计计算 12.8B 模型参数,大大减少推理的计算量

阅读 SIGMOD 2024 一篇来自 Purdue University 的论文,关于存算分离数据库,做一些关键信息的摘要记录

简介

存算分离架构在云数据库中广泛使用,包括 Amazon Aurora/MicroSoft Socrates/Google AlloyDB/Alibaba PolarDB/Huawer Taurus。
传统非存算分离的架构,会把数据库的存储和计算聚合在同一物理机器上;而存算分离架构下,计算通过网络来访问存储,这样的设计能够独立去扩容计算或存储,在提升资源利用率、降低成本、故障快速恢复上有优势。
存算分离数据库通常会满足以下设计原则:

设计原则 P1:存储计算引擎隔离

  • 存储引擎(包括 Logging 和 Storage)和计算引擎(包括 SQL Layer、Buffering、Transaction)部署在不同的物理节点
  • 这种分离架构核心是把存储访问变成远程/共享存储,如果没有缓冲层,远程访问的性能会非常差

设计原则 P2:Log as the Database(LogDB)

  • 为了降低计算引擎-存储引擎之间的网络开销,除了适用 Buffering,存算分离架构通常会引入 Log as the Database 设计
  • 仅在事务提交时把 WAL 同步到存储层,而不去同步数据 Page,减少网络上需要传输的数据
  • 存储引擎层通过异步回放 WAL 来获得真实的数据 Page

设计原则 P3:Shared-Storage 架构

  • Shared-Storage 是相对于 Shared-Nothing 来讲的,指不同的计算引擎共享一份存储引擎数据,减少拷贝和异动数据
  • 因为主从计算节点的同步延时,从节点可能需要读取老版本数据,Shared-Storage 需要支持 Multi-Version Pages

这里明确几个实现上的细节:

  • 讨论 P2 LogDB 的时候,通常是做了 P1 存储计算引擎分离
  • 讨论 P3 Shared-Storage 的时候,通常是做了 P1 存储计算引擎分离和 P2 LogDB

架构实现

XLog 是 PostgreSQL 中的 WAL

单体架构

下图是 PostgreSQL(v13.0) 的架构,数据库整体跑在单个节点上

远程盘

  • 存储引擎和计算引擎拆分到不同的节点上,之间走网络通信
  • 读写流程和单体架构没区别,核心是本地读写改成远程读写
  • 远程盘的优势是独立扩容,其次是 LogDB 架构的基础

Log as the Database(LogDB)

  • 远程盘因为网络开销表现会比较差,优化一方面是引入 Buffering,其次就是 LogDB
  • 写路径在事务提交阶段,把 WAL 发送给存储节点,实际的数据 Page 通过在存储节点异步回放 WAL 生产(Step a/b)
  • 读路径计算节点先检查 Local Buffer,Cache Miss 时从存储节点加载 Page 数据,如果此时 Page 数据尚未完成回放,会同步开始回放(Step1)
  • 传统数据库也会有异步刷脏的设计,仅从架构角度不能说 LogDB 传输 Log 而非 Page 的方式,一定比单体架构性能高

多版本 LogDB(LogDB-MV)

  • Shared-Storage 架构下不同计算节点共享同一个存储层,假设此时是单 Primary-多 Secondary 计算节点
    • Primary 节点支持读写事务
    • Secondary 节点仅支持只读事务
  • Primary 把数据更新异步同步给 Secondary,因为延时 Secondary 可能读取老版本 Page,这个称为 GetPage@LSNmailto:GetPage@LSN
  • 存储节点回放 WAL 时保存 Page 的多个版本
    • 存储引擎维护 VersionMap,PageID-LSM 的映射(Step a)
    • WAL 会拆分程 miniWAL,每个 miniWAL 仅包含一个 Page 的修改,回放阶段会把多个 Page 数据用 PageID+LSN 作为 Key 插入 RocksDB
    • GetPage@LSNmailto:GetPage@LSN 请求首先同步等待回放进程完成读请求 LSN 的处理,然后从 VersionMap 中获取指定 PageID 的 Version LSN 列表, 从 RocksDB 中把小于等于请求 LSN 的 Page 数据加载出来,正常最终的 Page 数据
  • 多版本的支持增加了 RocksDB 的写压力

为了加速读路径,LogDB-MV 提供 Filtered Replay 和 SmartReplay 两种优化思路。

  • FilteredReplay:GetPage@LSN 阶段,通过 QuickScan 跳过和当前 Page 无关的 LSN,加速读
  • SmartReplay:GetPage@LSN 阶段只去回放 Page 相关的 LSN,不同 Page 的回放可以多进程并行

测试数据

测试设置

  • 计算节点 x 16(1 写 15 读)
    • 写节点配置:Intel Xeon Gold 6330 CPU(2.0GHZ), 250GB DRAM, 1.5TB NVMe SSD
    • 读节点配置:Intel Xeon Silver CPU (2.3 GHz), 64GB DRAM, and a 900GB NVMe SSD
  • 存储节点 x 3(也测试了一版 6 存储节点,模拟 Aurora 架构)
    • 节点配置:Intel Xeon Platinum 8368 CPU(2.4GHZ), 188GB DRAM, 1.5TB NVMe SSD
  • 测试环境:Ubuntu 22.04, 10Gb TCP/IP 网络
  • 测试数据:SysBench 和 TPC-C
    • SysBench:2000 张表,每张表 20w 行数据,整体数据库大小 96GB

读写性能

单体架构 vs 远程盘

  • 计算节点 Buffer 越大,读性能越高
  • 计算节点 Buffer 超过 700MB 后,增加 Buffer 对写性能提升不大
    • 主要是生成的脏页在 700MB 左右
    • 在 Heavy workload 下,这个阈值会稍高
  • 写性能在远程盘情况下,劣化严重

远程盘 vs LogDB

  • 读性能相当
  • LogDB 在写性能上优化明显,特别是 Heavy workload 下最大提升 2.5X(8GB buffer)
  • Ligh workload 下,两种架构写性能相当,原因是低负载下有足够的时间去异步刷脏,同时也意味着 LogDB 本身并没有提升写性能
  • Heavy workload 下,远程盘架构刷脏吞吐不够会阻塞写请求,而 LogDB 没有刷脏进而获得吞吐提升

写后读场景

  • 持续 5min 写入
    • LogDB 落后远程盘 20.3%
    • LogDB-MV 落后远程盘 66.2%
    • Gap 主要是回放日志的开销

LogDB vs. LogDB-MV

  • Multi-Version 主要影响写,不影响读性能
    • 一条 WAL 对应多个 Page,都要插入 RocksDB
  • 移除写放大问题,Multi-Version 的写性能提升 37%

读性能水平扩展

  • 水平扩容计算节点提升吞吐

FR vs. SR

  • 只读请求不受 FR/SR 影响,优化混合读写场景
  • FR 大幅度减少 LSN 回放,相比 LogDB-MV 写吞吐提升 50%