LLM 使用简介

2023-12-08 ai linux

主要是 Karpathy 的相关实现,通过简单的示例、资源介绍 LLM 相关的概念。

简介

大多数的 LLM 都采用了类似 GPT 的架构,基于 Transformer 由解码器组成的网络结构,采用自回归方式构建,只是在位置编码、归一化位置、激活函数等细节上各有不同。

当前大部分 LLM 模型都只使用了 Decoder 模块,相比 Encoder 来说,就是在计算 Q*K 时引入了 Mask 以确保当前位置只能关注前面已经生成的内容。

原始的 GPT2 包含了最基础的实现,通过 TenserFlow 开发,同时包含了类似分词器的能力。

这里基本都是 Karpathy 仓库的实现介绍,基于 TinyStories 数据集,原始数据可以从 HuggingFace 下载。

评估一个大模型时,会有如下的参数:

  • Parameters 参数数量,一般是以 Billion, B 十亿为单位,可以简单理解为模型神经元的数量,数量越多模型的处理信息能力越强,对数据中复杂关系的把握也越精准,同时训练和推理的成本也越高。

分词

HuggingFaceTransformer 包中包含了 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 上下载已经训练好的参数模型,包括了常用的 stories15Mstories42M 两个,然后通过如下命令简单运行。

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 代码。

示例

提供了 ShakespeareTinyStories 两个测试数据集 ,会首先尝试前者。

----- 下载一些已经提前准备好的数据集,或者进行预处理和分词
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 的文件格式,提供了更高效的数据存储和访问格式。

参考