主要是 Karpathy 的相关实现,通过简单的示例、资源介绍 LLM 相关的概念。
简介
大多数的 LLM 都采用了类似 GPT 的架构,基于 Transformer 由解码器组成的网络结构,采用自回归方式构建,只是在位置编码、归一化位置、激活函数等细节上各有不同。
当前大部分 LLM 模型都只使用了 Decoder 模块,相比 Encoder 来说,就是在计算 Q*K
时引入了 Mask 以确保当前位置只能关注前面已经生成的内容。
原始的 GPT2 包含了最基础的实现,通过 TenserFlow 开发,同时包含了类似分词器的能力。
这里基本都是 Karpathy 仓库的实现介绍,基于 TinyStories 数据集,原始数据可以从 HuggingFace 下载。
评估一个大模型时,会有如下的参数:
Parameters
参数数量,一般是以 Billion, B 十亿为单位,可以简单理解为模型神经元的数量,数量越多模型的处理信息能力越强,对数据中复杂关系的把握也越精准,同时训练和推理的成本也越高。
分词
在 HuggingFace
的 Transformer
包中包含了 AutoTokenizer.from_pretrained()
的实现,用于加载预训练的文本处理模型,用于将文本数据转换为可接受的输入格式,如参可以是类似 gpt2
这种通用模型,也可以从指定目录加载。
其中 tiktokenizer.vercel.app 包含了不同模型的分词可视化。
其它
代理设置
很多可能需要通过 git clone
下载预训练模型,国内可能会被墙,可以使用 hf-mirror.com 代理,除了常规页面上的使用方式,在通过 git
下载时直接将 huggingface.com
替换为 hf-mirror.com
即可。
llama2.c
简单的 llama2.c 模型,通过 Llama2
架构进行训练、推理,其中训练通过 PyTorch 实现,而推理则使用最简单的 C 实现,而且提供了脚本来转换 llama2 参数,
示例
包括了推理以及训练阶段。
推理
按照文档的介绍,直接从 HuggingFace 上下载已经训练好的参数模型,包括了常用的 stories15M 和 stories42M 两个,然后通过如下命令简单运行。
make run
./run stories15M.bin
其中常用的参数有:
-t 1.0
热度 (Temperature) 用来控制语言模型输出的随机度,高热度生成更难预料且富有创造性的结果,而第热度则更保守。-p 0.9
Top-p 核心采样 (Nucleus Sampling) 累积阈值超过该参数的最佳词汇,模型会从这组词汇中随即抽取以生成输出。-i "Your Prompt"
不同的提示语,会生成不同的文档内容。
训练
上述的模型参数国内可能无法下载,可以自己训练,主要是通过 Python 实现,国内可以直接搜索 TinyStories_all_data.tar.gz
关键字,有很多仓库可以下载。
----- 会从 HuggingFace 上下载并解压 TinyStories 数据集
python tinystories.py download
----- 进行分词
python tinystories.py pretokenize
----- 开始训练
python train.py
源码解析
其推理阶段很简单,会循环调用 forward()
sample()
decode()
生成,其中核心在 forward()
阶段。
main()
|-build_transformer()
| |-read_checkpoint() 读取配置、参数信息
| | |-memory_map_weights() 关键权重信息,这里包含了相关参数
| |-malloc_run_state()
|-build_tokenizer() 使用 Byte Pair Encoding, BPE
|-build_sampler()
|======> 上面基本构建了整个模型,参数采用 float 类型,如下根据具体场景调用
|-generate()
| |-encode() 对输入进行编码
| |====> 这里开始循环执行 Step 次
| |-forward()
| |-sample() 超过输入 Token 之后开始采样,如果是 BOS 则结束
| |-decode() 输出文本
|-chat()
如下所谓的 llama2
模型指的是 7B
参数规模。
通常隐藏层的维度要比输入层的维度要高。
其中核心的函数包括了:
rmsnorm
计算均方根标准化。matmul
矩阵乘法。softmax
其中的 seq_len
决定了在训练过程中生成文本的最大有效长度,在推理时,目前的策略是始终小于训练长度。
typedef struct { // Tiny 7B Mistral 7B
int dim; // 词向量维度,输入维度 8 288 4096 4096
int hidden_dim; // 隐藏层(FFN 前向网络)维度 24 768 14336 3.5*dim
int n_layers; // 层数 6 32 32
int n_heads; // number of query heads 4 6 32
int n_kv_heads; // number of key/value heads 2 6 8
int vocab_size; // 词表大小,英文通常是 32000 32000
int seq_len; // max sequence length 256
} Config;
// head_dim = dim / n_heads 每个头的维度 128
// window_size 4096
// context_len 8192
// 如下是计算过程中可能使用的变量
// kv_dim=dim*n_kv_heads/n_heads 2 288 8192
// head_size=dim/n_heads 2 48 8192
typedef struct {
// token embedding table
float* token_embedding_table; // (vocab_size, dim)
// weights for rmsnorms
float* rms_att_weight; // (layer, dim) RMSNorm 权重
float* rms_ffn_weight; // (layer, dim)
// weights for matmuls. note dim == n_heads * head_size
float* wq; // (layer, dim, n_heads * head_size)
float* wk; // (layer, dim, n_kv_heads * head_size)
float* wv; // (layer, dim, n_kv_heads * head_size)
float* wo; // (layer, n_heads * head_size, dim)
// weights for ffn
float* w1; // (layer, hidden_dim, dim)
float* w2; // (layer, dim, hidden_dim)
float* w3; // (layer, hidden_dim, dim)
// final rmsnorm
float* rms_final_weight; // (dim,)
// (optional) classifier weights for the logits, on the last layer
float* wcls;
} TransformerWeights;
typedef struct {
float *x; // activation at current time stamp (dim,)
float *xb; // same, but inside a residual branch (dim,)
float *xb2; // an additional buffer just for convenience (dim,)
float *hb; // buffer for hidden dimension in the ffn (hidden_dim,)
float *hb2; // buffer for hidden dimension in the ffn (hidden_dim,)
float *q; // query (dim,)
float *k; // key (dim,)
float *v; // value (dim,)
float *att; // buffer for scores/attention values (n_heads, seq_len)
float *logits; // output logits
// kv cache
float* key_cache; // (layer, seq_len, dim)
float* value_cache; // (layer, seq_len, dim)
} RunState;
typedef struct {
Config config; // 保存了神经网络整体结构
TransformerWeights weights; // 模型训练的结果
RunState state; // 保存的中间状态
int fd; // file descriptor for memory mapping
float* data; // memory mapped data pointer
ssize_t file_size; // size of the checkpoint file in bytes
} Transformer;
llm.c
相同作者的 llm.c 实现,相比之前的 nanoGPT 要更加简单,用来训练模型,实现相当简单,只有 1K 左右的 C 代码。
示例
提供了 Shakespeare
和 TinyStories
两个测试数据集 ,会首先尝试前者。
----- 下载一些已经提前准备好的数据集,或者进行预处理和分词
bash dev/download_starter_pack.sh
----- 生成 dev/data/tinystories/TinyStories_{train/val}.bin 文件
python dev/data/tinystories.py
----- 生成 dev/data/tinyshakespeare/tiny_shakespeare_{train/val}.bin 文件
python dev/data/tinyshakespeare.py
----- 然后编译训练
make train_gpt2
OMP_NUM_THREADS=8 ./train_gpt2
----- 用来测试 C 和 PyTorch 结果相同
make test_gpt2
./test_gpt2
源码解析
nanoGPT
纯 Python 实现。
vocab_size
词汇量大小context_length
上下文长度emb_dim
输入 Token 转换为向量大小n_heads
多头输入大小
常用简写。
Begin Of Sentence, BOS
句子开始;End Of Sentence, EOS
句子结束。
并行加速
Checkpoint 是降低 LLM 训练成本的关键技术,可以在失败后继续而非从头开始,而且可以在不同的阶段评估模型性能。
并行加速包括了数据并行 (Data Parallelism)、模型并行 (Model Parallelism)、流水线并行 (Pipeline Parallelism)。
其它
- tiktoken 由 OpenAI 开源的 Python 库,实现了 Byte Pair Encoding, BPE 算法,并对性能作了极大的优化。
残差连接
传统神经网络中,每一层的输出是对前一层的输出进行变换得到,而残差连接 (Residual Connection) 是将前一层的输出与后一层的输出相加得到,主要是为了解决深层网络中信息衰减和梯度消失的问题。
RLHF
GPT 的整个训练过程分成了三阶段,Base模型、微调 (SFT) 模型、RLHF 模型。
RoPE
在 LLM 中词出现的位置是很关键的因素,不同位置表达的语义可能天差地别,常规的包含了绝对位置编码和相对位置编码。另外,使用绝对位置编码时,可能会出现训练和预测 Token 长度不一致导致效果变差。
RoPE 实际就是选择某种计算方式,以通过绝对位置编码来表征相对位置编码。
GGUF
用于存储大模型预训练结果,相比 HuggingFace 和 Torch 的文件格式,提供了更高效的数据存储和访问格式。
参考
- Zero To Hero 由 Karpathy 录制的相当不错的入门视频,极力推荐。
- llama.com 官方网站,还可以参考 中文社区、Alpaca 以及 Cookbook 含很不错的介绍,模型部署、微调方法、RAG 等等。