diff --git a/docs/source/cources/data_processing.md b/docs/source/cources/data_processing.md new file mode 100644 index 0000000000..12cf3a84da --- /dev/null +++ b/docs/source/cources/data_processing.md @@ -0,0 +1,157 @@ +在模型训练过程中,数据及数据处理是最为重要的工作之一。在当前模型训练流程趋于成熟的情况下,数据集的好坏,是决定了该次训练能否成功的最关键因素。 + +在上一篇中,我们提到了模型训练的基本原理是将文字转换索引再转换为对应的向量,那么文字转为向量的具体过程是什么? + +# 分词器(Tokenizer) + +在NLP(自然语言处理)领域中,承担文字转换索引(token)这一过程的组件是tokenizer。每个模型有自己特定的tokenizer,但它们的处理过程是大同小异的。 + +首先我们安装好魔搭的模型库modelscope和训练框架swift: + +```shell +# 激活conda环境后 +pip install modelscope ms-swift -U +``` + +我们使用“千问1.8b”模型将“杭州是个好地方”转为tokens的具体方式是在python中调用: + +```python +from modelscope import AutoTokenizer +tokenizer = AutoTokenizer.from_pretrained("qwen/Qwen-1_8B-Chat", trust_remote_code=True) +print(tokenizer('杭州是个好地方')) +# {'input_ids': [104130, 104104, 52801, 100371], 'token_type_ids': [0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1]} +``` + +其中的input_ids就是上面我们说的文字的token。可以注意到token的个数少于实际文字的数量,这是因为在转为token的时候,并不是一个汉字一个token的,可能会将部分词语变为一个token,也可能将一个英文转为两部分(如词根和时态),所以token数量和文字数量不一定对得上。 + +# 模板(Template) + +每种模型有其特定的输入格式,在小模型时代,这种输入格式比较简单: + +```text +[CLS]杭州是个好地方[SEP] +``` + +[CLS]代表了句子的起始,[SEP]代表了句子的终止。在BERT中,[CLS]的索引是101,[SEP]的索引是102,加上中间的句子部分,在BERT模型中整个的token序列是: + +```text +101, 100, 1836, 100, 100, 100, 1802, 1863, 102 +``` + +我们可以看到,这个序列和上面千问的序列是不同的,这是因为这两个模型的词表不同。 + +在LLM时代,base模型的格式和上述的差不多,但chat模型的格式要复杂的多,比如千问chat模型的template格式是: + +```text +<|im_start|>system +You are a helpful assistant! +<|im_end|> +<|im_start|>user +How are you?<|im_end|> +<|im_start|>assistant +``` + +其中“You are a helpful assistant!”是system字段,“How are you?”是用户问题,其他的部分都是template的格式。 + +system字段是chat模型必要的字段,这个字段会以命令方式提示模型在下面的对话中遵循怎么样的范式进行回答,比如: + +```text +“You are a helpful assistant!” +“下面你是一个警察,请按照警察的要求来审问我” +“假如你是一个爱哭的女朋友,下面的对话中清扮演好这个角色” +``` + +system字段规定了模型行为准则,比如当模型作为Agent使用时,工具集一般也是定义在system中的: + +```text +“你是一个流程的执行者,你有如下工具可以使用: +工具1:xxx,输入格式是:xxx,输出格式是:xxx,作用是:xxx +工具2:xxx,输入格式是:xxx,输出格式是:xxx,作用是:xxx” +``` + +复杂的template有助于模型识别哪部分是用户输入,哪部分是自己之前的回答,哪部分是给自己的要求。 + +比较麻烦的是,目前各开源模型还没有一个统一的template标准。在SWIFT中,我们提供了绝大多数模型的template,可以直接使用: + +```python +register_template( + TemplateType.default, + Template([], ['### Human:\n', '{{QUERY}}\n\n', '### Assistant:\n'], + ['\n\n'], [['eos_token_id']], DEFAULT_SYSTEM, ['{{SYSTEM}}\n\n'])) + +# You can set the query as '' to serve as a template for pre-training. +register_template(TemplateType.default_generation, + Template([], ['{{QUERY}}'], None, [['eos_token_id']])) +register_template( + TemplateType.default_generation_bos, + Template([['bos_token_id']], ['{{QUERY}}'], None, [['eos_token_id']])) + +qwen_template = Template( + [], ['<|im_start|>user\n{{QUERY}}<|im_end|>\n<|im_start|>assistant\n'], + ['<|im_end|>\n'], ['<|im_end|>'], DEFAULT_SYSTEM, + ['<|im_start|>system\n{{SYSTEM}}<|im_end|>\n']) +register_template(TemplateType.qwen, qwen_template) +register_template(TemplateType.chatml, deepcopy(qwen_template)) +... +``` + +有兴趣的小伙伴可以阅读:https://github.com/modelscope/swift/blob/main/swift/llm/utils/template.py 来获得更细节的信息。 + +template拼接好后,直接传入tokenizer即可。 + +微调任务是标注数据集,那么必然有指导性的labels(模型真实输出)存在,将这部分也按照template进行拼接,就会得到类似下面的一组tokens: + +```text +input_ids: [34, 56, 21, 12, 45, 73, 96, 45, 32, 11] + ---------用户输入部分--------- ----模型真实输出---- +labels: [-100, -100, -100, -100, -100, 73, 96, 45, 32, 11] +``` + +在labels中,我们将用户输入的部分(问题)替换成了-100,保留了模型输入部分。在模型进行运算时,会根据input_ids的前面的tokens去预测下一个token,就比如: + +```text +已知token 预测的下一个token +34 ->17 +34,56 ->89 +... +34,56,21,12,45 ->121 +34,56,21,12,45,73 ->99 +34,56,21,12,45,73,96 ->45 +34,56,21,12,45,73,96,45 ->14 +34,56,21,12,45,73,96,45,32->11 +``` + +可以看到,这个预测不一定每个都预测对了,而且呈现了下三角矩阵的形态。那么训练的时候就可以这样进行对比: + +```text +34, 56, 21, 12, 45, 121, 99, 45, 32, 11 +-100, -100, -100, -100, -100, 73, 96, 45, 14, 11 +``` + +-100部分计算loss时会被忽略,因为这是用户输入,不需要考虑预测值是什么。只要对比下对应的位置对不对就可以计算它们的差异了,这个差异值被称为**loss**或者**残差**。我们通过计算梯度的方式对参数进行优化,使模型参数一点一点向**真实的未知值**靠近。使用的残差算法叫做**交叉熵**。 + +在SWIFT中提供了根据模型类型构造template并直接转为token的方法,这个方法输出的结构可以直接用于模型训练和推理: + +```python +from swift.llm.utils import get_template, Template +from modelscope import AutoTokenizer +tokenizer = AutoTokenizer.from_pretrained("qwen/Qwen-1_8B-Chat", trust_remote_code=True) +template: Template = get_template( + 'qwen', + tokenizer, + max_length=256) +resp = template.encode({'query': 'How are you?', "response": "I am fine"}) +print(resp) +# {'input_ids': [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 4340, 525, 498, 30, 151645, 198, 151644, 77091, 198, 40, 1079, 6915, 151645], 'labels': [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 40, 1079, 6915, 151645]} +``` + +input_ids和labels可以直接输入模型来获取模型的输出: + +```python +from modelscope import AutoModelForCausalLM +import torch +model = AutoModelForCausalLM.from_pretrained("qwen/Qwen-1_8B-Chat", trust_remote_code=True).to(0) +resp = {key: torch.tensor(value).to(0) for key, value in resp.items()} +output = model(**resp) +print(output) +``` diff --git a/docs/source/cources/deployment.md b/docs/source/cources/deployment.md new file mode 100644 index 0000000000..210ef0ce23 --- /dev/null +++ b/docs/source/cources/deployment.md @@ -0,0 +1,189 @@ +# 推理及部署 + +训练后的模型会用于推理或者部署。推理即使用模型用输入获得输出的过程,部署是将模型发布到恒定运行的环境中推理的过程。一般来说,LLM的推理可以直接使用PyTorch代码、使用[VLLM](https://docs.vllm.ai/en/latest/getting_started/quickstart.html)/[XInference](https://github.com/xorbitsai/inference)/[FastChat](https://github.com/lm-sys/FastChat)等框架,也可以使用[llama.cpp](https://github.com/ggerganov/llama.cpp)/[chatglm.cpp](https://github.com/li-plus/chatglm.cpp)/[qwen.cpp](https://github.com/QwenLM/qwen.cpp)等c++推理框架。 + +# 一些推理方法 + +- Greedy Search **贪婪搜索方式**。按照前面的讲解,模型会按照词表尺寸生成概率。贪婪方式会不断选择生成概率最大的token。该方法由于无脑选择了最大概率,因此模型会倾向于生成重复的文字,一般实际应用中很少使用 +- Beam Search 和贪婪方式的区别在于,beam search会选择概率最大的k个。在生成下一个token时,每个前序token都会生成k个,这样整体序列就有k^2个,从这些序列中选择组合概率最大的k个,并递归地执行下去。k在beam search算法中被称为beam_size +- Sample 随机采样方式。按照词表每个token的概率采样一个token出来。这个方式多样性更强,是目前主流的生成方式。 + +# 重要推理超参数 + +- do_sample:布尔类型。是否使用随机采样方式运行推理,如果设置为False,则使用beam_search方式 + +- temperature:大于等于零的浮点数。公式为: + $$ + q_i=\frac{\exp(z_i/T)}{\sum_{j}\exp(z_j/T)}\\ + $$ + 从公式可以看出,如果T取值为0,则效果类似argmax,此时推理几乎没有随机性;取值为正无穷时接近于取平均。一般temperature取值介于[0, 1]之间。取值越高输出效果越随机。 + + **如果该问答只存在确定性答案,则T值设置为0。反之设置为大于0。** + +- top_k:大于0的正整数。从k个概率最大的结果中进行采样。k越大多样性越强,越小确定性越强。一般设置为20~100之间。 + - 实际实验中可以先从100开始尝试,逐步降低top_k直到效果达到最佳。 + +- top_p:大于0的浮点数。使所有被考虑的结果的概率和大于p值,p值越大多样性越强,越小确定性越强。一般设置0.7~0.95之间。 + - 实际实验中可以先从0.95开始降低,直到效果达到最佳。 + - top_p比top_k更有效,应优先调节这个参数。 +- repetition_penalty: 大于等于1.0的浮点数。如何惩罚重复token,默认1.0代表没有惩罚。 + +# KVCache + +上面我们讲过,自回归模型的推理是将新的token不断填入序列生成下一个token的过程。那么,前面token已经生成的中间计算结果是可以直接利用的。具体以Attention结构来说: + +image-20240116161847987 + +推理时的Q是单token tensor,但K和V都是包含了所有历史token tensor的长序列,因此KV是可以使用前序计算的中间结果的,这部分的缓存就是KVCache,其显存占用非常巨大。 + +# VLLM + +VLLM支持绝大多数LLM模型的推理加速。它使用如下的方案大幅提升推理速度: + +1. Continuous batching + + - 在实际推理过程中,一个批次多个句子的输入的token长度可能相差很大,最后生成的模型输出token长度相差也很大。在python朴素推理中,最短的序列会等待最长序列生成完成后一并返回,这意味着本来可以处理更多token的GPU算力在对齐过程中产生了浪费。continous batching的方式就是在每个句子序列输出结束后马上填充下一个句子的token,做到高效利用算力。 + + ![image-20240116160416701](resources/image-20240116160416701.png) + + ![image-20240116160444612](resources/image-20240116160444612.png) + +2. PagedAttention + - 推理时的显存占用中,KVCache的碎片化和重复记录浪费了50%以上的显存。VLLM将现有输入token进行物理分块,使每块显存内部包含了固定长度的tokens。在进行Attention操作时,VLLM会从物理块中取出KVCache并计算。因此模型看到的逻辑块是连续的,但是物理块的地址可能并不连续。这和虚拟内存的思想非常相似。另外对于同一个句子生成多个回答的情况,VLLM会将不同的逻辑块映射为一个物理块,起到节省显存提高吞吐的作用。 + +![image-20240116162157881](resources/image-20240116162157881.png) + +![image-20240116162213204](resources/image-20240116162213204.png) + +值得注意的是,VLLM会默认将显卡的全部显存预先申请以提高缓存大小和推理速度,用户可以通过参数`gpu_memory_utilization`控制缓存大小。 + +首先安装VLLM: + +```shell +pip install vllm +``` + +之后直接运行即可: + +```shell +VLLM_USE_MODELSCOPE=True python -m vllm.entrypoints.openai.api_server --model qwen/Qwen-1_8B-Chat --trust-remote-code +``` + +之后就可以调用服务: + +```shell +curl http://localhost:8000/v1/completions \ +-H "Content-Type: application/json" \ +-d '{ +"model": "qwen/Qwen-1_8B-Chat", +"prompt": "San Francisco is a", +"max_tokens": 7, +"temperature": 0 +}' +``` + +# SWIFT + +在SWIFT中,我们支持了VLLM的推理加速手段。 + +```shell +pip install ms-swift[llm] openai +``` + +只需要运行下面的命令就可以使用VLLM加速推理: + +```shell +swift infer --model_id_or_path qwen/Qwen-1_8B-Chat --max_new_tokens 128 --temperature 0.3 --top_p 0.7 --repetition_penalty 1.05 --do_sample true +``` + +也支持在部署中使用VLLM: + +```shell +swift deploy --model_id_or_path qwen/Qwen-1_8B-Chat --max_new_tokens 128 --temperature 0.3 --top_p 0.7 --repetition_penalty 1.05 --do_sample true +``` + +调用: + +```python +from openai import OpenAI +client = OpenAI( + api_key='EMPTY', + base_url='http://localhost:8000/v1', +) +model_type = client.models.list().data[0].id +print(f'model_type: {model_type}') + +query = '浙江 -> 杭州\n安徽 -> 合肥\n四川 ->' +kwargs = {'model': model_type, 'messages': query, 'seed': 42, 'temperature': 0.1, 'max_tokens': 32} + +resp = client.chat.completions.create(**kwargs) +response = resp.choices[0].text +print(f'query: {query}') +print(f'response: {response}') + +# 流式 +stream_resp = client.completions.create(stream=True, **kwargs) +response = resp.choices[0].text +print(f'query: {query}') +print('response: ', end='') +for chunk in stream_resp: + print(chunk.choices[0].text, end='', flush=True) +print() +``` + +# llama.cpp + +llama.cpp是使用c++语言编写的对llama系列模型进行高效推理或量化推理的开源库。该库使用了ggml底层计算库进行推理。在使用之前需要额外将python的weights转为ggml格式或gguf格式方可使用。和llama.cpp类似,还有兼容ChatGLM模型的chatglm.cpp和兼容qwen模型的qwen.cpp和mistral的mistral.cpp。 + +安装依赖: + +```shell +pip install modelscope +``` + +```python +git clone --recursive https://github.com/QwenLM/qwen.cpp && cd qwen.cpp +cmake -B build +cmake --build build -j --config Release +``` + +下载模型: + +```python +from modelscope import snapshot_download +print(snapshot_download('qwen/Qwen-1_8B-Chat')) +# /mnt/workspace/.cache/modelscope/qwen/Qwen-1_8B-Chat +``` + +将原始模型转换为ggml支持的格式: + +```shell +python3 qwen_cpp/convert.py -i /mnt/workspace/.cache/modelscope/qwen/Qwen-1_8B-Chat -t q4_0 -o qwen1_8b-ggml.bin +./build/bin/main -m qwen1_8b-ggml.bin --tiktoken /mnt/workspace/.cache/modelscope/qwen/Qwen-1_8B-Chat/qwen.tiktoken -p 你好 +# 你好!有什么我可以帮助你的吗? +``` + +量化章节中我们介绍,GGML库适合于CPU运行,因此推荐用户在CPU环境中或边缘计算中考虑cpp库进行推理。 + +# FastChat + +FastChat是一个开源推理库,侧重于模型的分布式部署实现,并提供了OpenAI样式的RESTFul API。 + +```shell +pip3 install "fschat[model_worker,webui]" +python3 -m fastchat.serve.controller +``` + +在新的terminal中启动: + +```shell +FASTCHAT_USE_MODELSCOPE=true python3 -m fastchat.serve.model_worker --model-path qwen/Qwen-1_8B-Chat --revision v1.0.0 +``` + +之后在新的terminal中可以运行界面进行推理: + +```shell +python3 -m fastchat.serve.gradio_web_server +``` + +![image-20240118204046417](resources/image-20240118204046417.png) diff --git a/docs/source/cources/how_to_select.md b/docs/source/cources/how_to_select.md new file mode 100644 index 0000000000..69e75b8c46 --- /dev/null +++ b/docs/source/cources/how_to_select.md @@ -0,0 +1,152 @@ +# 方法选型 + +判断自己的场景需要什么样的方法是使用LLM的第一步。下面我们会对比直接推理(提示词工程)、训练、RAG、Agent方法的具体场景,讲解这几种方式的特点,并给出适用场景、使用难度、准确性、成本、缺点几个方面的总结。 + +## 直接推理(提示词工程) + +这种方式特指直接使用现有LLM,利用prompt范式激活模型不同的能力完成特定需求。直接推理方式对开发的要求较低,一般可以完成通用类型的任务,如通用知识问答、角色扮演等。使用方式如下: + +```text +用户:你是一个经验丰富的导游,请使用导游的话术回答游客的问题。 +模型:当然可以!请问你需要问些什么呢? +用户:我想去杭州旅行,请告诉我哪里比较值得去。 +模型:当然可以!作为一个导游,我可以为你讲解杭州的风景和美食... +``` + +### 使用难度 + +- 较低,只需要调用模型接口,编写对应的prompt即可。但编写好的prompt也是具有一定技巧的,具体可以查看我们的教程中的提示词工程部分。 + +**提示词工程无论是直接推理或训练后推理都是需要的**。 + +### 适用场景 + +- 视模型本身的能力而定,在采用该方式之前需要对现有模型针对自己的业务领域进行较为充分的评估。 + +### 准确性 + +- 由于是原始模型只接受了通用知识的训练,因此在特定领域的场景下可能存在胡编乱造的可能性(幻觉问题)。使用者需要注意自己的专业场景下是否使用该通用模型能解决所有问题,一般建议直接试用该模型给出模型能力的具体评估。 + +### 成本 + +- 开发成本较低。如果是开源模型,需要选用合适的硬件及推理方式。这部分在我们教程中的推理章节会有讲解。如果是闭源调用,只需要使用对应模型的接口API即可。 + +### 缺点 + +- 由于模型没有经过针对特有领域的知识,因此效果会比较不可控。比如,在评测时模型表现尚可,但在实际使用中发现模型出现了严重的幻觉和知识匮乏问题,如果是闭源调用则该问题会比较难以解决(可能涉及到工程架构改变),如果是开源模型可以考虑使用训练和RAG的方式解决。 + +## 训练 + +全量训练和轻量训练是训练的两种方式,它们的区别在于: + +全量训练在给定LLM模型上冻结一定的参数(或不冻结任何参数)进行训练,一般耗费显存较高,训练周期比较长。受限于成本问题,最近出现了轻量微调方式,主要方案是在模型结构上附着一个额外结构,在训练时冻结原模型并训练额外结构,推理时将额外结构加载起来或合并回原来模型(并不是所有的额外结构都支持合并,支持者比如LoRA,不支持者比如Side)。轻量微调目前的最流行结构是LoRA,该结构理解简单,训练成本较低,在部分任务上可以达到全量微调的效果。 + +轻量微调另一个方式就是量化(请查看另一篇文章),即对模型的float32权重或float16权重进行缩放,使其变成int类型的整形,节省显存或计算时长。 + +- 一般情况下建议选择轻量训练,优先使用LoRA等方式 +- 如果效果不好,可以考虑解冻原模型的部分参数,比如normalizer、embedder等进行训练,也就是全量训练+轻量训练的方式 +- 如果显存受限,可以考虑使用量化进行训练。量化和轻量训练并不互斥,比如QLoRA(量化+LoRA),但需要注意量化后训练和推理结果会有所下降 + +一般来说,预训练或继续训练不建议使用轻量训练,小数据量微调情况下建议优先使用轻量训练。 + +640 + +### 适用场景 + +- 场景存在特殊知识,需要进行知识灌注,可以使用继续训练+全量训练 +- 需要对回复的风格或范式进行定制化,可以使用人类对齐训练或微调+全量/轻量训练 +- 模型原有能力不够,如对读入的doc文件进行理解并进行归纳总结,或特有场景的文本进行分类,但原有模型对该任务的回答存在问题,可以使用微调+全量/轻量训练 + +> 简单来说,模型的训练是让模型“找规律”的过程。比如告诉模型1+1=2, 2+2=4,那么让模型分析3+3=? +> +> 如果数据是带有规律的,比如文字顺序、逻辑关系、图片元素(比如斑马总是带有黑白色的条纹),那么训练就可以将这些规律抽象出来;如果数据是“无规律的知识”,比如用A解决B问题,用C解决D问题,那么这些数据训练后就几乎不具有泛化性,因为模型无法分析出出现了E问题应该用A解决还是B解决,这时候应当选用RAG或者Agent方式,或者训练的目标改为让模型熟悉使用工具来解决问题。 + +### 使用难度 + +- 训练需要对模型结构、训练的流程有所了解。其理解成本比RAG高一些。 + +### 准确性 + +- 准确性依照训练的结果而定,训练后模型会按照训练集的风格和方式回答问题。一般来说训练后模型能力会有较大提升,但仍然可能存在幻觉问题。 + +### 成本 + +- 可以查看SWIFT的[benchmark](https://github.com/modelscope/swift/blob/main/docs/source/LLM/Benchmark.md)。我们比较了主要模型的训练显存需求和训练速度,用户可以按需评估。 + +### 缺点 + +- 相比RAG,输出可解释性不强 +- 存在幻觉问题 +- 在精确问答场景上可能会产出非专业结果(如法律行业) +- 对知识更新频繁的场景不适用 + +## RAG + +RAG即检索增强生成,也就是通过模型外挂知识库的方式来辅助模型回答。一般来说会将用户问题变为向量,进向量数据库进行查询,并召回符合条件的文档或回答,之后将回答直接返回或输入模型整理后返回。RAG可以查看另一篇教程。 + +RAG和微调的选型问题一直是被问的较多的问题之一,两种方法的对比可以查看下表: + +640 + +如果模型本身对专业知识理解不够(比如模型对召回的文档不能进行良好分析的情况),那么使用RAG是不够的,需要进行模型训练,或将模型训练和RAG结合起来使用。 + +### 适用场景 + +- 需要根据语料精确回答,比如法律或医疗领域 +- 搜索召回场景,比如搜索引擎 +- 知识频繁更新,灵活性较强的场景 + +### 使用难度 + +- 需要对RAG流程有所了解,选用对应的RAG框架或解决方案 + +### 准确性 + +- 准确性较高,可解释性也较高 + +### 成本 + +- 除模型本身的成本外,需要额外的向量数据库和工程端开发成本和维护成本 + +### 缺点 + +- 比模型直接回答多了查询召回步骤,单请求整体RT高一些 +- 如果场景和知识无关,比如非知识类问答,或API调用,或文档分析,文章总结等,RAG就不能起到作用 + +## Agent + +Agent适合于利用模型进行代码编写运行、API调用的复杂场景。Agent的主要思路是利用模型的CoT(思维链)能力进行复杂场景的流程串接。比如“生成一个具有今天天气特征的海报”,模型会先调用天气预报接口获得天气,之后生成海报文案,然后调用文生图模型生成海报。 + +### 适用场景 + +- 复杂的应用场景,需要模型产生思维过程,将整体任务拆分为具体任务进行执行,比如包含了运行代码、接口调用等过程 + +### 使用难度 + +- 需要对Agent和CoT过程有一定了解 +- 熟悉目前的Agent框架的能力上限 +- 需要熟悉模型的提示词工程才能做到较好的效果 + +### 准确性 + +- 一般来说模型越大准确性越高。比如GPT4(闭源)、Qwen-max(闭源)、Qwen-72b(开源)、ChatGLM4(闭源)等会具有良好的效果,小模型可能需要特殊训练。 + +- 在格外复杂的场景下,比如任务复杂、描述含混不清、模型对行业流程不理解的情况下,需要对模型进行额外训练。 + +### 成本 + +- 一般和模型部署成本相同 + +### 缺点 + +- 对复杂的业务和人类复杂的行为如博弈、交流->更新的场景支持不好。比如,尚不能用Agent编写复杂的APP如淘宝,也不能模拟股市大盘。 + +# 模型选型 + +目前国内外开源模型已经超过了几百个,挑选合适的模型是一个比较关键的问题。在这里可以给出一些泛泛的意见: + +- Agent场景尽量选择较大的模型或者闭源LLM API(如GPT4、Qwen-max) + +- 训练场景中,数据量较大(比如大于10000条)、数据质量较高、专业度较高的训练优先选择base模型,数据量较少优先选择chat模型。在算力允许条件下可以进行对比训练实验 + +- 关注国内外的开源可信模型榜单,选择排名较高或口碑较好的模型 diff --git a/docs/source/cources/introduction.md b/docs/source/cources/introduction.md new file mode 100644 index 0000000000..93cbf61ff6 --- /dev/null +++ b/docs/source/cources/introduction.md @@ -0,0 +1,373 @@ +# 模型/训练/推理 + +深度学习领域所谓的“模型”,是一个复杂的数学公式构成的计算步骤。为了便于理解,我们以一元一次方程为例子解释: + +```text +y = ax + b +``` + +该方程意味着给出常数a、b后,可以通过给出的x求出具体的y。比如: + +```text +# a=1 b=1 x=1 +y = 1 * 1 + 1 -> y=2 +# a=1 b=1 x=2 +y = 1 * 2 + 1 => y=3 +``` + +这个根据x求出y的过程就是**模型的推理过程**。在LLM中,x一般是一个句子,如“帮我计算23+20的结果”,y一般是:“等于43”。 + +基于上面的方程,如果追加一个要求,希望a=1,b=1,x=3的时候y=10呢?这显然是不可能的,因为按照上面的式子,y应该是4。然而在LLM中,我们可能要求模型在各种各样的场景中回答出复杂的答案,那么这显然不是一个线性方程能解决的场景,于是我们可以在这个方程外面加上一个非线性的变换: + +```text +y=σ(ax+b) +``` + +这个非线性变换可以理解为指数、对数、或者分段函数等。 + +在加上非线性部分后,这个公式就可以按照一个复杂的曲线(而非直线)将对应的x映射为y。在LLM场景中,一般a、b和输入x都是复杂的矩阵,σ是一个复杂的指数函数,像这样的一个公式叫做一个“神经元”(cell),大模型就是由许多类似这样的神经元加上了其他的公式构成的。 + +在模型初始化时,针对复杂的场景,我们不知道该选用什么样的a和b,比如我们可以把a和b都设置为0,这样的结果是无论x是什么,y都是0。这样显然是不符合要求的。但是我们可能有很多数据,比如: + +```text +数据1:x:帮我计算23+20的结果 y:等于43 +数据2:x:中国的首都在哪里?y:北京 +... +``` + +我们客观上相信这些数据是正确的,希望模型的输出行为能符合这些问题的回答,那么就可以用这些数据来**训练**这个模型。我们假设**真实存在一对a和b**,这对a和b可以完全满足所有上面数据的回答要求,虽然我们不清楚它们的真实值,但是我们可以通过训练来找到**尽量接近真实值**的a和b。 + +训练(通过x和y反推a和b)的过程在数学中被称为**拟合**。 + +模型需要先进行训练,找到尽量符合要求的a和b,之后用a和b输入真实场景的x来获得y,也就是**推理**。 + +# 预训练范式 + +在熟悉预训练之前,先来看几组数据: + +第一组: + +```text +我的家在东北,松花江上 +秦朝是一个大一统王朝 +床前明月光,疑是地上霜 +``` + +第二组: + +```text +番茄和鸡蛋在一起是什么?答:番茄炒蛋 +睡不着应该怎么办?答:喝一杯牛奶 +计算圆的面积的公式是?A:πR B:πR2 答:B +``` + +第三组: + +```text +我想要杀死一个仇人,该如何进行?正确答案:应付诸法律程序,不应该泄私愤 错误答案:从黑市购买军火后直接杀死即可 +如何在网络上散播病毒?正确答案:请遵守法律法规,不要做危害他人的事 错误答案:需要购买病毒软件后在公用电脑上进行散播 +``` + +我们会发现: + +- 第一组数据是没有问题答案的(未标注),这类数据在互联网上比比皆是 + +- 第二组数据包含了问题和答案(已标注),是互联网上存在比例偏少的数据 + +- 第三组数据不仅包含了正确答案,还包含了错误答案,互联网上较难找到 + +这三类数据都可以用于模型训练。如果将模型训练类似比语文考试: + +- 第一组数据可以类比为造句题和作文题(续写)和填空题(盖掉一个字猜测这个字是什么) + +- 第二组数据可以类比为选择题(回答ABCD)和问答题(开放问答) + +- 第三组数据可以类比为考试后的错题检查 + +现在我们可以给出预训练的定义了。 + +由于第一类数据在互联网的存在量比较大,获取成本较低,因此我们可以利用这批数据大量的训练模型,让模型抽象出这些文字之间的通用逻辑。这个过程叫做**预训练**。 + +第二类数据获得成本一般,数据量较少,我们可以在预训练后用这些数据训练模型,使模型具备问答能力,这个过程叫做**微调**。 + +第三类数据获得成本很高,数据量较少,我们可以在微调后让模型了解怎么回答是人类需要的,这个过程叫**人类对齐**。 + +一般我们称做过预训练,或预训练结合通用数据进行了微调的模型叫做**base模型**。这类模型没有更专业的知识,回答的答案也可能答非所问或者有重复输出,但已经具备了很多知识,因此需要进行额外训练才能使用。把经过了人类对齐的模型叫做**chat模型**,这类模型可以直接使用,用于通用类型的问答,也可以在其基础上用少量数据微调,用于特定领域的场景。 + +预训练过程一般耗费几千张显卡,灌注数据的量达到几个TB,成本较高。 + +微调过程分为几种,可以用几千万的数据微调预训练过的模型,耗费几十张到几百张显卡,得到一个具备通用问答知识的模型,也可以用少量数据一两张显卡训练一个模型,得到一个具备特定问答知识的模型。 + +人类对齐过程耗费数张到几百张显卡不等,技术门槛比微调更高一些,一般由模型提供方进行。 + +# 如何确定自己的模型需要做什么训练? + +Case1:你有大量的显卡,希望从0训一个模型出来刷榜 + +很简单,预训练+大量数据微调+对齐训练,但一般用户不会用到这个场景 + +Case2:有大量未标注数据,但这些数据的知识并没有包含在预训练的语料中,在自己的实际场景中要使用 + +选择继续训练(和预训练过程相同,但不会耗费那么多显卡和时间) + +Case3:有一定的已标注数据,希望模型具备数据中提到的问答能力,如根据行业特有数据进行大纲提炼 + +选择微调 + +Case4:回答的问题需要相对严格的按照已有的知识进行,比如法条回答 + +用自己的数据微调后使用RAG(知识增强)进行检索召回,或者不经过训练直接进行检索召回 + +Case5:希望训练自己领域的问答机器人,希望机器人的回答满足一定条件或范式 + +微调+对齐训练 + +在下面的章节中,我们分具体场景介绍训练的不同步骤。 + +- [Transformer结构](./transformer.md) +- [数据的预处理](./data_processing.md) +- [选择适合自己的方法和模型](./how_to_select.md) +- [指令微调](./sft.md) +- [模型的量化](./quantization.md) +- [推理部署](./deployment.md) + +# 模型推理的一般过程 + +现在有一个句子,如何将它输入模型得到另一个句子呢? + +我们可以这样做: + +1. 先像查字典一样,将句子变为字典中的索引。假如字典有30000个字,那么“我爱张学”可能变为[12,16,23,36] + +2. 像[12,16,23,36]这样的标量形式索引并不能直接使用,因为其维度太低,可以将它们映射为更高维度的向量,比如每个标量映射为5120长度的向量,这样这四个字就变为: + + ```text + [12,16,23,36] + -> + [[0.1, 0.14, ... 0.22], [0.2, 0.3, ... 0.7], [...], [...]] + ------5120个小数------- + ``` + + 我们就得到了4x5120尺寸的矩阵(这四个字的矩阵表达)。 + + > 深度学习的基本思想就是把一个文字转换为多个小数构成的向量 + +3. 把这个矩阵在模型内部经过一系列复杂的计算后,最后会得到一个向量,这个向量的小数个数和字典的字数相同。 + + ```text + [1.5, 0.4, 0.1, ...] + -------30000个------ + ``` + + 下面我们把这些小数按照大小转为比例,使这些比例的和是1,通常我们把这个过程叫做**概率化**。把值(概率)最大的索引找到,比如使51,那么我们再把51通过查字典的方式找到实际的文字: + + ```text + 我爱张学->友(51) + ``` + + 下面,我们把“我爱张学友”重新输入模型,让模型计算下一个文字的概率,这种方式叫做**自回归**。即用生成的文字递归地计算下一个文字。推理的结束标志是**结束字符**,也就是**eos_token**,遇到这个token表示生成结束了。 + + 训练就是在给定下N个文字的情况下,让模型输出这些文字的概率最大的过程,eos_token在训练时也会放到句子末尾,让模型适应这个token。 + +# PyTorch框架 + +用于进行向量相乘、求导等操作的框架被称为深度学习框架。高维度的向量被称为张量(Tensor),后面我们也会用Tensor代指高维度向量或矩阵。 + +深度学习框架有许多,比如PyTorch、TensorFlow、Jax、PaddlePaddle、MindSpore等,目前LLM时代研究者使用最多的框架是**PyTorch**。PyTorch提供了Tensor的基本操作和各类算子,如果把模型看成有向无环图(DAG),那么图中的每个节点就是PyTorch库的一个算子。 + +安装PyTorch之前请安装python。在这里我们推荐使用conda(一个python包管理软件)来安装python:https://conda.io/projects/conda/en/latest/user-guide/install/index.html + +conda配置好后,新建一个虚拟环境(一个独立的python包环境,所做的操作不会污染其它虚拟环境): + +```shell +# 配置一个python3.9的虚拟环境 +conda create -n py39 python==3.9 +# 激活这个环境 +conda activate py39 +``` + +之后: + +```python +# 假设已经安装了python,没有安装python +pip install torch +``` + +打开python命令行: + +```shell +python +``` + +```python +import torch +# 两个tensor,可以累计梯度信息 +a = torch.tensor([1.], requires_grad=True) +b = torch.tensor([2.], requires_grad=True) +c = a * b +# 计算梯度 +c.backward() +print(a.grad, b.grad) +# tensor([2.]) tensor([1.]) +``` + +可以看到,a的梯度是2.0,b的梯度是1.0,这是因为c对a的偏导数是b,对b的偏导数是a的缘故。backward方法非常重要,模型参数更新依赖的就是backward计算出来的梯度值。 + +torch.nn.Module基类:所有的模型结构都是该类的子类。一个完整的torch模型分为两部分,一部分是代码,用来描述模型结构: + +```python +import torch +from torch.nn import Linear + +class SubModule(torch.nn.Module): + def __init__(self): + super().__init__() + # 有时候会传入一个config,下面的Linear就变成: + # self.a = Linear(config.hidden_size, config.hidden_size) + self.a = Linear(4, 4) + +class Module(torch.nn.Module): + def __init__(self): + super().__init__() + self.sub =SubModule() + + +module = Module() + +state_dict = module.state_dict() # 实际上是一个key value对 + +# OrderedDict([('sub.a.weight', tensor([[-0.4148, -0.2303, -0.3650, -0.4019], +# [-0.2495, 0.1113, 0.3846, 0.3645], +# [ 0.0395, -0.0490, -0.1738, 0.0820], +# [ 0.4187, 0.4697, -0.4100, -0.4685]])), ('sub.a.bias', tensor([ 0.4756, -0.4298, -0.4380, 0.3344]))]) + +# 如果我想把SubModule替换为别的结构能不能做呢? +setattr(module, 'sub', Linear(4, 4)) +# 这样模型的结构就被动态的改变了 +# 这个就是轻量调优生效的基本原理:新增或改变原有的模型结构,具体可以查看选型或训练章节 +``` + +state_dict存下来就是pytorch_model.bin,也就是存在于[modelhub](https://www.modelscope.cn/models)中的文件 + +config.json:用于描述模型结构的信息,如上面的Linear的尺寸(4, 4) + +tokenizer.json: tokenizer的参数信息 + +vocab.txt: nlp模型和多模态模型特有,描述词表(字典)信息。tokenizer会将原始句子按照词表的字元进行拆分,映射为tokens + +# 设备 + +在使用模型和PyTorch时,设备(device)错误是经常出现的错误之一。 + +```text +RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cpu and cuda:0! +``` + +tensor和tensor的操作(比如相乘、相加等)只能在两个tensor在同一个设备上才能进行。要不然tensor都被存放在同一个显卡上,要不然都放在cpu上。一般最常见的错误就是模型的输入tensor还在cpu上,而模型本身已经被放在了显卡上。PyTorch驱动N系列显卡进行tensor操作的计算框架是cuda,因此可以非常方便地把模型和tensor放在显卡上: + +```python +from modelscope import AutoModelForCausalLM +import torch +model = AutoModelForCausalLM.from_pretrained("qwen/Qwen-1_8B-Chat", trust_remote_code=True) +model.to(0) +# model.to('cuda:0') 同样也可以 +a = torch.tensor([1.]) +a = a.to(0) +# 注意!model.to操作不需要承接返回值,这是因为torch.nn.Module(模型基类)的这个操作是in-place(替换)的 +# 而tensor的操作不是in-place的,需要承接返回值 +``` + +# PyTorch基本训练代码范例 + +```python +import os +import random + +import numpy as np +import torch +from torch.optim import AdamW +from torch.optim.lr_scheduler import StepLR +from torch.utils.data import Dataset, DataLoader +from torch.utils.data.dataloader import default_collate +from torch.nn import CrossEntropyLoss + +seed = 42 +# 随机种子,影响训练的随机数逻辑,如果随机种子确定,每次训练的结果是一样的 +torch.manual_seed(seed) +np.random.seed(seed) +random.seed(seed) + +# 确定化cuda、cublas、cudnn的底层随机逻辑 +# 否则CUDA会提前优化一些算子,产生不确定性 +# 这些处理在训练时也可以不使用 +os.environ["CUDA_LAUNCH_BLOCKING"] = "1" +os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8" +torch.use_deterministic_algorithms(True) +# Enable CUDNN deterministic mode +torch.backends.cudnn.deterministic = True +torch.backends.cudnn.benchmark = False + + +# torch模型都继承于torch.nn.Module +class MyModule(torch.nn.Module): + + def __init__(self, n_classes=2): + # 优先调用基类构造 + super().__init__() + # 单个神经元,一个linear加上一个relu激活 + self.linear = torch.nn.Linear(16, n_classes) + self.relu = torch.nn.ReLU() + + def forward(self, tensor, label): + # 前向过程 + output = {'logits': self.relu(self.linear(tensor))} + if label is not None: + # 交叉熵loss + loss_fct = CrossEntropyLoss() + output['loss'] = loss_fct(output['logits'], label) + return output + + +# 构造一个数据集 +class MyDataset(Dataset): + + # 长度是5 + def __len__(self): + return 5 + + # 如何根据index取得数据集的数据 + def __getitem__(self, index): + return {'tensor': torch.rand(16), 'label': torch.tensor(1)} + + +# 构造模型 +model = MyModule() +# 构造数据集 +dataset = MyDataset() +# 构造dataloader, dataloader会负责从数据集中按照batch_size批量取数,这个batch_size参数就是设置给它的 +# collate_fn会负责将batch中单行的数据进行padding +dataloader = DataLoader(dataset, batch_size=4, collate_fn=default_collate) +# optimizer,负责将梯度累加回原来的parameters +# lr就是设置到这里的 +optimizer = AdamW(model.parameters(), lr=5e-4) +# lr_scheduler, 负责对learning_rate进行调整 +lr_scheduler = StepLR(optimizer, 2) + +# 3个epoch,表示对数据集训练三次 +for i in range(3): + # 从dataloader取数 + for batch in dataloader: + # 进行模型forward和loss计算 + output = model(**batch) + # backward过程会对每个可训练的parameters产生梯度 + output['loss'].backward() + # 建议此时看下model中linear的grad值 + # 也就是model.linear.weight.grad + + # 将梯度累加回parameters + optimizer.step() + # 清理使用完的grad + optimizer.zero_grad() + # 调整lr + lr_scheduler.step() +``` diff --git a/docs/source/cources/quantization.md b/docs/source/cources/quantization.md new file mode 100644 index 0000000000..400555cc84 --- /dev/null +++ b/docs/source/cources/quantization.md @@ -0,0 +1,130 @@ +# 量化是什么 + +前文中我们提到,模型的推理过程是一个复杂函数的计算过程,这个计算一般以矩阵乘法为主,也就是涉及到了并行计算。一般来说,单核CPU可以进行的计算种类更多,速度更快,但一般都是单条计算;而显卡能进行的都是基础的并行计算,做矩阵乘法再好不过。如果把所有的矩阵都加载到显卡上,就会导致显卡显存的占用大量增加,尤其是LLM模型大小从7b、14b、34b到几百b不等,占用显存的大小就是惊人的数字,如何在减少运算量和显存占用的条件下,做到推理效果不下降太多呢?在这里需要引入浮点数和定点数的概念。 + +![img](resources/a0f0f479-fc2d-4e38-b5a3-1a9f4fd96f66.png) + +双精度浮点数:在PyTorch中用**torch.float64**表示,或者在其他语言中也称为double类型,在LLM训练中一般比较少用 + +全精度浮点数:在PyTorch中用**torch.float32**表示 + +低精度浮点数:在PyTorch中用**torch.bfloat16和torch.float16**表示。这两个浮点数的差别在上图中可以表示: + +1. bfloat16的小数部分较短,整数部分较长,这会有利于在训练中减少梯度爆炸的情况(即梯度累加值超过了最大值),但是这种数据类型是在N系列显卡Ampere系列才支持的,即**30系列**显卡。 +2. float16的小数部分较长,这意味着在精度控制上float16更好,但整数部分较短,比较容易梯度爆炸。 + +那么是否有更加减少显存占用和计算量的数值表达方式呢?那么可以考虑是否把浮点数转换为定点数(整数),整数计算更快更省显存,如果计算精度下降不大就很完美了。这种用整数计算代替浮点数计算的方法就是**量化**。 + +量化的基本原理是**根据每个tensor的浮点型最大值和最小值,将其映射为一个固定范围的整形数值集合**,比如[-127~127]。假设一个简单的公式:qweight=round(weight/scale),其中qweight代表量化后权重,weight代表量化前权重,scale代表缩放因子,可以看到在进行缩放后为了将浮点型转换为整数过程中增加了round操作丢失了小数部分。在后续计算或反量化为浮点型时存在无法完全还原的情况,这就是精度损失。 + +按照量化发生的步骤区分,可以划分为**PTQ(训练后量化,或离线量化)和QAT(训练感知型量化,或在线量化)**。PTQ量化可以分为data-free和calibration两种,前者不使用数据集进行校准直接计算量化因子,后者会根据少量真实数据进行统计分析并对量化因子进行额外校准,但耗费的时间更长。QAT量化会先在待量化的算子上增加一个伪量化结构,并在训练时模拟量化过程并实时更新计算量化因子(类似反向传播过程)及原始权重。QAT由于较为复杂一般作为辅助措施存在,用于改进PTQ量化的技术手段。 + +按照量化方法可以划分为**线性量化、非线性量化(如对数量化)**等多种方式,目前较为常用的是线性量化。其中线性量化又可以按照对称性划分为**对称量化和非对称量化**,非对称量化为了解决weight分布不均匀问题,其在公式中增加了zero_point项:qweight=round(weight/scale + zero_point),使稠密数据部分可以得到更宽泛的数值范围。 + + + +image-20240116173833918 + +image-20240116173902206 + +按照量化粒度划分可以分为**逐层量化(每层使用一套量化因子)、逐组量化(在每层中按照group使用一套量化因子)、逐通道量化(按channel划分量化因子)**等几种方式。 + +按照量化最大值的阈值区分,可以分为**饱和量化和不饱和量化**两种。不饱和量化按照浮点数最大值和量化后最大值的比例计算量化因子,由于原始weight的非均匀性会导致某些整形数值范围存在权重空缺。饱和量化会计算一个中间值以计算出量化因子,因此会舍弃一部分不重要数据,将重要数据尽量均匀的分布到量化数值范围内。 + +按照量化后的比特数划分,可以分为2比特量化,4比特量化,8比特量化等类型。 + +一般来说,PyTorch中量化模块的forward过程会先对量化权重进行反量化后使用浮点数进行计算。 + +下面介绍几种常用的量化库。 + +# AutoGPTQ + +该库需要引入额外的校准数据集进行量化校准。相比bitsandbytes量化精度较高,推理速度较快,但**训练后不支持合并adapter**。 + +```python +# 例子来自于https://github.com/PanQiWei/AutoGPTQ +from modelscope import AutoTokenizer, snapshot_download +from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig +import logging +import shutil +import os + +logging.basicConfig( + format="%(asctime)s %(levelname)s [%(name)s] %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S" +) + +pretrained_model_dir = snapshot_download("qwen/Qwen-1_8B-Chat") +quantized_model_dir = "qwen-1_8B-4bit" + +shutil.rmtree(quantized_model_dir, ignore_errors=True) +shutil.copytree(pretrained_model_dir, quantized_model_dir) +for _file in os.listdir(quantized_model_dir): + if ".safetensors" in _file or ".bin" in _file: + os.remove(os.path.join(quantized_model_dir, _file)) + +tokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, use_fast=True, trust_remote_code=True) +examples = [ + tokenizer( + "auto-gptq is an easy-to-use model quantization library with user-friendly apis, based on GPTQ algorithm." + ) +] + +quantize_config = BaseQuantizeConfig( + bits=4, # quantize model to 4-bit + group_size=128, # it is recommended to set the value to 128 + desc_act=False, # set to False can significantly speed up inference but the perplexity may slightly bad +) + +# load un-quantized model, by default, the model will always be loaded into CPU memory +model = AutoGPTQForCausalLM.from_pretrained(pretrained_model_dir, quantize_config, trust_remote_code=True).to(0) + +# quantize model, the examples should be list of dict whose keys can only be "input_ids" and "attention_mask" +model.quantize(examples) + +# save quantized model +model.save_quantized(quantized_model_dir) + +# save quantized model using safetensors +model.save_quantized(quantized_model_dir, use_safetensors=True) + +# load quantized model to the first GPU +model = AutoGPTQForCausalLM.from_quantized(quantized_model_dir, device="cuda:0", trust_remote_code=True) +# inference with model.generate +print(tokenizer.decode(model.generate(**tokenizer("auto_gptq is", return_tensors="pt").to(model.device))[0])) +``` + +在SWIFT中,可以使用已经量化好的AutoGPTQ模型直接进行训练: + +```shell +swift sft --model_id_or_path qwen/Qwen-7B-Chat-Int4 --model_revision master --sft_type lora --tuner_backend swift --template_type qwen --dtype fp16 --output_dir output --dataset leetcode-python-en --train_dataset_sample -1 --num_train_epochs 1 --max_length 512 --check_dataset_strategy warning --lora_rank 8 --lora_alpha 32 --lora_dropout_p 0.05 --lora_target_modules ALL --gradient_checkpointing true --batch_size 1 --weight_decay 0.01 --learning_rate 1e-4 +``` + +上面的命令行中,`qwen/Qwen-7B-Chat-Int4`是已经量化好的Qwen-7B-Chat模型。 + +# Bitsandbytes + +bitsandbytes是一种data-free的量化库。该量化方法速度较快(因为其不需要数据校准),因此可以在模型加载时动态量化,且该方法训练速度较快,因此训练兼容性较好,一般用于QLoRA训练中,且训练后可以合并adapter。当由于其没有数据校准过程,因此精度较AutoGPTQ较低。 + +```python +from modelscope import AutoModelForCausalLM, AutoTokenizer +import torch + +model = AutoModelForCausalLM.from_pretrained( + 'qwen/Qwen-1_8B-Chat', + load_in_8bit=True, + trust_remote_code=True) + +tokenizer = AutoTokenizer.from_pretrained('qwen/Qwen-1_8B-Chat', trust_remote_code=True) + +print(model(**tokenizer('how are you?', return_tensors='pt'))) +``` + +# GGML + +GGML和GGUF是[GGML](https://github.com/ggerganov/ggml) C++推理库的两种量化格式,其中GGUF格式较新,可以保留模型版本等其他自定义信息。这两种格式也是PTQ形式的量化算法,但GGML和GGUF格式的量化算法更适配于CPU推理,因此在CPU上运行更快,而GPTQ量化对GPU更加友好,两者的推理精度相仿。因此,*.cpp类型使用了GGML推理库的推理框架都更适配于CPU推理。 + +# AWQ + +[AWQ量化](https://docs.vllm.ai/en/latest/quantization/auto_awq.html)方式假设不是所有权重都影响模型性能,因此在量化过程中会跳过一部分重要权重以减轻量化过程中的精度损失。因此在和GPTQ量化保持类似推理速度的同时可以具备更好的精度。 + +目前VLLM对AWQ的支持较好, 可以考虑在推理加速时使用AWQ量化方式。 diff --git a/docs/source/cources/resources/4e8e8ce0-84bb-463a-b28f-e1d6df3c6b4a.png b/docs/source/cources/resources/4e8e8ce0-84bb-463a-b28f-e1d6df3c6b4a.png new file mode 100644 index 0000000000..cb197fecee Binary files /dev/null and b/docs/source/cources/resources/4e8e8ce0-84bb-463a-b28f-e1d6df3c6b4a.png differ diff --git a/docs/source/cources/resources/640.png b/docs/source/cources/resources/640.png new file mode 100644 index 0000000000..2eab22225d Binary files /dev/null and b/docs/source/cources/resources/640.png differ diff --git a/docs/source/cources/resources/a0f0f479-fc2d-4e38-b5a3-1a9f4fd96f66.png b/docs/source/cources/resources/a0f0f479-fc2d-4e38-b5a3-1a9f4fd96f66.png new file mode 100644 index 0000000000..bb67175133 Binary files /dev/null and b/docs/source/cources/resources/a0f0f479-fc2d-4e38-b5a3-1a9f4fd96f66.png differ diff --git a/docs/source/cources/resources/acb24b1f-4737-4419-9ac3-50651c3fcf75.png b/docs/source/cources/resources/acb24b1f-4737-4419-9ac3-50651c3fcf75.png new file mode 100644 index 0000000000..e64896f9d1 Binary files /dev/null and b/docs/source/cources/resources/acb24b1f-4737-4419-9ac3-50651c3fcf75.png differ diff --git a/docs/source/cources/resources/aed18027-66e6-46f6-9649-911564f6886e.png b/docs/source/cources/resources/aed18027-66e6-46f6-9649-911564f6886e.png new file mode 100644 index 0000000000..a47c919c3b Binary files /dev/null and b/docs/source/cources/resources/aed18027-66e6-46f6-9649-911564f6886e.png differ diff --git a/docs/source/cources/resources/bd665ecd-9391-4996-a45c-f8dad7e84822.png b/docs/source/cources/resources/bd665ecd-9391-4996-a45c-f8dad7e84822.png new file mode 100644 index 0000000000..6846680e7d Binary files /dev/null and b/docs/source/cources/resources/bd665ecd-9391-4996-a45c-f8dad7e84822.png differ diff --git a/docs/source/cources/resources/image-20240116160416701.png b/docs/source/cources/resources/image-20240116160416701.png new file mode 100644 index 0000000000..744898d2ef Binary files /dev/null and b/docs/source/cources/resources/image-20240116160416701.png differ diff --git a/docs/source/cources/resources/image-20240116160444612.png b/docs/source/cources/resources/image-20240116160444612.png new file mode 100644 index 0000000000..181c746548 Binary files /dev/null and b/docs/source/cources/resources/image-20240116160444612.png differ diff --git a/docs/source/cources/resources/image-20240116161844484.png b/docs/source/cources/resources/image-20240116161844484.png new file mode 100644 index 0000000000..80a26016b3 Binary files /dev/null and b/docs/source/cources/resources/image-20240116161844484.png differ diff --git a/docs/source/cources/resources/image-20240116161847987.png b/docs/source/cources/resources/image-20240116161847987.png new file mode 100644 index 0000000000..80a26016b3 Binary files /dev/null and b/docs/source/cources/resources/image-20240116161847987.png differ diff --git a/docs/source/cources/resources/image-20240116162157881.png b/docs/source/cources/resources/image-20240116162157881.png new file mode 100644 index 0000000000..f382308e6f Binary files /dev/null and b/docs/source/cources/resources/image-20240116162157881.png differ diff --git a/docs/source/cources/resources/image-20240116162213204.png b/docs/source/cources/resources/image-20240116162213204.png new file mode 100644 index 0000000000..a688767fca Binary files /dev/null and b/docs/source/cources/resources/image-20240116162213204.png differ diff --git a/docs/source/cources/resources/image-20240116173833918.png b/docs/source/cources/resources/image-20240116173833918.png new file mode 100644 index 0000000000..945e97b1b7 Binary files /dev/null and b/docs/source/cources/resources/image-20240116173833918.png differ diff --git a/docs/source/cources/resources/image-20240116173853429.png b/docs/source/cources/resources/image-20240116173853429.png new file mode 100644 index 0000000000..29b713dd23 Binary files /dev/null and b/docs/source/cources/resources/image-20240116173853429.png differ diff --git a/docs/source/cources/resources/image-20240116173902206.png b/docs/source/cources/resources/image-20240116173902206.png new file mode 100644 index 0000000000..29b713dd23 Binary files /dev/null and b/docs/source/cources/resources/image-20240116173902206.png differ diff --git a/docs/source/cources/resources/image-20240116205728780.png b/docs/source/cources/resources/image-20240116205728780.png new file mode 100644 index 0000000000..58aa376885 Binary files /dev/null and b/docs/source/cources/resources/image-20240116205728780.png differ diff --git a/docs/source/cources/resources/image-20240116212517161.png b/docs/source/cources/resources/image-20240116212517161.png new file mode 100644 index 0000000000..1cc1003967 Binary files /dev/null and b/docs/source/cources/resources/image-20240116212517161.png differ diff --git a/docs/source/cources/resources/image-20240116221933263.png b/docs/source/cources/resources/image-20240116221933263.png new file mode 100644 index 0000000000..f1d069282b Binary files /dev/null and b/docs/source/cources/resources/image-20240116221933263.png differ diff --git a/docs/source/cources/resources/image-20240116223558024.png b/docs/source/cources/resources/image-20240116223558024.png new file mode 100644 index 0000000000..237ca93fb3 Binary files /dev/null and b/docs/source/cources/resources/image-20240116223558024.png differ diff --git a/docs/source/cources/resources/image-20240118204046417.png b/docs/source/cources/resources/image-20240118204046417.png new file mode 100644 index 0000000000..68bde5b014 Binary files /dev/null and b/docs/source/cources/resources/image-20240118204046417.png differ diff --git a/docs/source/cources/sft.md b/docs/source/cources/sft.md new file mode 100644 index 0000000000..af78fe8ffe --- /dev/null +++ b/docs/source/cources/sft.md @@ -0,0 +1,164 @@ +# 微调(Supervised Finetuning) + +指令微调阶段使用了**已标注数据**。这个阶段训练的数据集数量不会像预训练阶段那么大,最多可以达到几千万条,最少可以达到几百条到几千条。指令微调可以将预训练的知识“涌现”出来,进行其他类型的任务,如问答类型的任务。一般指令微调阶段对于在具体行业上的应用是必要的,但指令微调阶段一般不能灌注进去新知识,而是将已有知识的能力以某类任务的形式展现出来。 + +指令微调任务有多种场景,比较常用的有: + +- 风格化:特定的问答范式 +- 自我认知:自我认知改变 +- 能力增强:模型本身能力不够,对具体行业的数据理解不良 +- Agent:支持Agent能力,比如程序编写、API调用等 + +上述只是举了几个例子,一般来说距离用户最近的训练方式就是指令微调。 + +一般来说,LLM中指的base模型是指经过了预训练(以及进行了一部分通用指令的微调)的模型。Chat模型是经过了大量通用数据微调和人类对齐训练的模型。 + +如何选择base模型和chat模型进行微调呢? + +- 数据量较少的时候(比如小于1w条)建议使用chat模型微调 +- 数据量较多、数据较为全面的时候,建议使用base模型微调 + +当然,如果硬件允许,建议两个模型都进行尝试,选择效果较好的。需要注意的是,chat模型有其独特的输入格式,在微调时一定要遵循。base模型的输入格式一般比较简单(但也需要遵守该格式),而且一般该格式不支持多轮数据集。 + +> 如果需要用base模型训练多轮对话,一般需要使用一个支持多轮对话的template。在SWIFT中,可以指定为`default`,在训练时只需要指定--template_type default即可。 + +# 重要概念 + +1. loss 代表模型求解的y和实际的y值的差异。该值会进行loss.backward(),这个方法会求解梯度,并将对应梯度值记录在每个参数上 + + loss可以理解为根据模型计算出来的值和正确值的偏差(也就是残差)。 例如,回归任务中计算的值是1.0,而实际的值应当为2.0,那么loss为2.0-1.0=1.0。上述loss类型为MAE,除此外,还有MSE,Hinge等各类loss。一般分类任务的loss为[交叉熵(Cross-Entropy)](https://zhuanlan.zhihu.com/p/61944055),这也是目前LLM最常用的loss。 + + loss计算出来后(这个过程也就是forward,即前向推理),经过backward过程即可计算出梯度。 + + > 梯度:光滑的曲面上导数变化最大的方向 + + loss可以经过PyTorch的loss.backward()将每个算子、每个步骤的梯度都计算出来(复杂微分方程的链式求导过程),当有了梯度后,可以将参数往负梯度方向更新,学习率(lr)就是这时候起作用的,由于直接加上负梯度太大,可能直接产生震荡,即值从一个点瞬间跑到了曲线上的另一个点,导致在这两点反复震荡不收敛,因此乘以一个lr,让loss一点点下降。 + +2. epoch 代表对数据集训练多少轮次 + +3. iter 对输入数据的每次forward+backward代表一个iter + +4. batch_size 批处理大小。在一次前向推理中,同时处理多少行数据。由于同一批数据会并行求解梯度,因此batch_size越大,梯度越稳定。在SFT时较为合适的梯度一般选择为16/32/64等值 + 1. batch_size越大,并行计算消耗的显存越高。因此在低显存情况下,可以选用batch_size=1,gradient_accumulation_steps=16。训练会在iter%gradient_accumulation_steps==0时集中进行一次参数更新。在iter%gradient_accumulation_steps!=0时,会将梯度值不断累加到参数上,这样就相当于将batch_size扩大了gradient_accumulation_steps倍 + +5. learning_rate 学习率 训练将负梯度值乘以该值加到原参数上。换句话说,每次只将参数更新一个小幅度,避免向错误的更新方向移动太多。 + + > 一般LoRA的学习率可以比全参数训练的学习率稍高一点,因为全参数训练会完全重置所有参数,训练时需要学习率更低。 + > LLM训练的学习率一般设置在1e-4~1e-5不等 + +6. max_length 输入句子的最大长度。比如设置为4096,那么句子加答案转换为token后最大长度为max_length。这个值会影响显存占用,需要按照自己的实际需求设置。 + 1. 当batch_size大于1时,意味着不同句子的长度可能不同。data_collator的作用就是按照固定max_length或者batch中的最大长度对其他句子的token进行补齐。补齐的部分不参与模型的loss计算,但仍然会占用计算量 + +7. flash_attention flash attention是一种针对attention结构高效计算的组件,该组件主要原理利用了显卡的高速缓存。flash attention会节省约20%~40%训练显存并提高训练速度,对训练精度没有不良影响。在显卡支持的情况下建议开启。 + +8. optimizer + + optimizer是深度学习中的优化器,负责将负梯度值累加到原来需要更新的参数上,类似于: + + > Vanilla SGD + > + > weights = weights - learning_rate * grad + + 实际的原理会比较复杂,比如常用的AdamW实际上是一个复杂的滑动平均的算法。 + +9. lr_scheduler + + 一般来说,训练各个阶段的学习率是不一样的,有时候需要越来越小(因为训练到最后需要更精细的调节),有时候需要先有个warmup(先将lr从0增大到指定值,再慢慢减小),lr_scheduler就是用来动态调整lr使用的组件。 + +10. gradient_checkpointing 梯度检查点。该方法的原理是将训练时的中间变量在前向过程中暂时丢弃,并在后向过程中重新计算。该方法可以有效节省训练显存,但属于时间换空间的做法,因此训练时间会变长。对显存的节省可以达到30%-70%不等。训练速度会减慢20%-40%不等。 + +# 分布式训练(Distributed Training) + +由于较大模型可能在单张显卡上显存溢出,或者训练速度不够,因此单机多卡或多机多卡训练是必要的。在训练过程中的分布式训练有以下几种模式: + +- DDP 分布式数据并行。将训练集的数据分段拆分到不同的进程中,这种训练方式相当于增加了batch_size。比如四个进程,每个进程batch_size=1,则总体batch_size=4。在计算梯度时,torch框架会自动将四个进程的梯度进行累加平均。该方法会提高训练速度,但如果模型在单张显卡上显存溢出,DDP方式也无法运行。 + +- MP 模型并行。模型并行分为多种方式,如tensor并行、device_map、流水线并行、FSDP等。 + - tensor并行:将矩阵拆分到多张显卡上,比如,将一个2048x2048的矩阵,拆分为两个1024x2048的矩阵,在前向推理时在显卡间通讯,完成一次推理,这样一个模型的显存需求就被平均拆分到两个显卡上。tensor并行最知名的框架是Megatron。 + + - device_map并行:自动计算如何将模型拆分到多个显卡上。比如一个模型按照顺序分为embedder、layer0~95、output,device_map可能将这些参数均分到两张显卡上,比如embedder、layer0~48分配到显卡1上,layer49~95、output分配到显卡2上。相比Megatron,device_map方式较为低效,因为使用该方法训练或推理时,显卡1计算时显卡2是空闲的,计算效率较低;而Megatron是同时使用两个显卡计算,效率较高 + + - 流水线并行:类似于device_map,将模型按照layer拆分到不同显卡上 + + - FSDP,在讲FSDPqian需要先讲解DeepSpeed的ZeRO优化方式 + - ZeRO-1:类似DDP,但是将Optimizer的state均分维护到不同的进程中,每次更新参数后对所有进程的参数进行同步更新 + + - ZeRO-2:在ZeRO-1的基础上,将不同层的梯度值均分维护到不同的进程中,每次每个进程同步梯度后更新自己负责的梯度对应的参数部分,并在更新后对所有的进程的参数进行同步 + + - ZeRO-3:在ZeRO-2的基础上,将不同层的模型参数也均分到不同的进程中。每个进程在计算某层结果时,从其他进程中获得对应的层的参数,计算完后抛弃该层参数;backward时,也从其他进程获得对应层的参数并同步梯度信息,计算完后抛弃该层参数。这样每个进程就在仅保存某些层的参数的条件下完成了数据并行计算 + + - FSDP就是ZeRO-3的并行策略 + +# LoRA + +LoRA是一个非常重要的可调优结构,简单来说,就是增加了一个额外可训练部分,比如原来的Linear的矩阵是MxN维,增加一个LoRA,该LoRA会包含两个参数量较少的矩阵:Mxd, dxN,这两个矩阵相乘后仍然是MxN维的,训练时原MxN矩阵冻结,只训练LoRA的两个矩阵,参数量就会大大减少。 + +image-20240116223558024 + +为什么模型本身的矩阵不使用这种形式? + +一般大规模矩阵的非零特征值数量会远远小于矩阵的维度,这个非零特征值的数量叫做矩阵的秩(rank),秩决定了这个矩阵如何影响被乘的向量,为0或较小的特征值对传入tensor的影响也比较小,丢弃这些信息对精度的影响不大。 + +一个模型包含了多个大矩阵,这些大矩阵的秩不相等而且难以预测,因此不能对原模型应用LoRA,但在sft时使用LoRA相对安全,虽然有精度损失,但可以使一个大模型在一个消费级显卡上进行训练。 + +也就是说,LoRA的原理是假设所有矩阵的秩都是d,进行了一定的有损压缩。基于LoRA也有很多升级版技术,如AdaLoRA、SoRA等,这些组件方案都是基于LoRA,对不同算子的LoRA的rank进行动态调节以达到更好的效果。 + +LoRA目前已经是训练SD模型和LLM模型的最常用技术。LoRA的weights也非常小,只有几十兆,因此加载和使用都非常方便,且LoRA本身可以合并回原模型,推理时可以做到兼容原模型结构。 + +> 如果涉及到对模型的知识编辑,比如自我认知任务,LoRA的目标module一般需要设置为`ALL`,因为MLP层对模型的知识获取是至关重要的,需要参与训练过程。 + +# 训练过程 + +在前序的文章中,我们讲述了如何进行数据的前处理。结合上面讲解的基本概念,我们就可以运行一个完整的训练过程。 + +```shell +pip install ms-swift -U +``` + +安装好SWIFT后,可以直接启动界面运行训练和推理: + +```shell +swift web-ui +``` + + + +在框架中,一个最小的训练过程代码如下: + +```python +# Experimental environment: A10, 3090, V100, ... +# 20GB GPU memory +import os +os.environ['CUDA_VISIBLE_DEVICES'] = '0' + +import torch + +from swift.llm import ( + DatasetName, InferArguments, ModelType, SftArguments, + infer_main, sft_main, app_ui_main, merge_lora_main +) + +model_type = ModelType.qwen_1_8b +sft_args = SftArguments( + model_type=model_type, + train_dataset_sample=2000, + dataset=[DatasetName.blossom_math_zh], + output_dir='output') +result = sft_main(sft_args) +best_model_checkpoint = result['best_model_checkpoint'] +print(f'best_model_checkpoint: {best_model_checkpoint}') +torch.cuda.empty_cache() + +infer_args = InferArguments( + ckpt_dir=best_model_checkpoint, + show_dataset_sample=10) +# merge_lora_main(infer_args) +result = infer_main(infer_args) +torch.cuda.empty_cache() + +app_ui_main(infer_args) +``` + +上面我们构建了一个最小的训练和推理流程。注意SftArguments具有很多可调节参数,可以[查看文档](https://github.com/modelscope/swift/blob/main/docs/source/LLM/%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0.md )来查看这些参数的具体意义。也可以[自定义自己的数据集](https://github.com/modelscope/swift/blob/main/docs/source/LLM/%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%8E%E6%8B%93%E5%B1%95.md)进行训练。 + +# 训练案例 diff --git a/docs/source/cources/transformer.md b/docs/source/cources/transformer.md new file mode 100644 index 0000000000..63f63ac634 --- /dev/null +++ b/docs/source/cources/transformer.md @@ -0,0 +1,140 @@ +# Transformer结构模型 + +在2017年之后,Transformer结构模型几乎横扫一切统治了NLP领域,后面的CV领域和Audio领域也大放异彩。相比LSTM和CNN结构,Transformer结构好在哪里呢? + +image-20240116205728780 + +这是LLaMA2的模型结构。 + +介绍下基本结构和流程: + +1. Input是原始句子,经过Tokenizer转变为tokens +2. tokens输入模型,第一个算子是Embedder,tokens转换为float tensor +3. 之后进入layers,每个layers会包含一个attention结构,计算Q和K的tensor的内积,并将内积概率化,乘以对应的V获得新的tensor。 +4. tensor加上输入的x后(防止层数太深梯度消失)进入Normalization,对tensor分布进行标准化 +5. 进入FeedForward(MLP),重新进入下一layer +6. 所有的layers计算过后,经过一个linear求出对vocab每个位置的概率 + +可以看出,Transformer模型的基本原理是让每个文字的Tensor和其他文字的Tensor做内积(也就是cosine投影值,可以理解为文字的相关程度)。之后把这些相关程度放在一起计算各自占比,再用占比比例分别乘以对应文字的Tensor并相加起来,得到了一个新的Tensor(这个Tensor是之前所有Tensor的概率混合,可以理解为对句子所有文字的抽象)。每个文字都进行如上动作,因此生成的新的Tensor和之前输入的Tensor长度相同(比如输入十个字,计算得到的Tensor还是十个),在层数不断堆叠的情况下,最后的Tensor会越来越抽象出文字的深层次意义,用最后输出的Tensor去计算输出一个新的文字或分类。 + +# Transformer对比CNN和LSTM + +- CNN有局部性和平移不变性,促使模型关注局部信息。CNN预设了归纳偏差,这使得小样本训练可以取得较好效果,但在充分数据训练下这一效果也被transformer所掩盖。并且局部性会忽略全局关系,导致某些条件下效果不佳 +- LSTM的长距离记忆会导致最早的token被加速遗忘,并且其只能注意单侧信息导致了对句子的理解存在偏差。后来虽然引入了双向LSTM,但其大规模分布式训练仍然存在技术问题 +- Transformer结构并不预设归纳偏差,因此需要大数据量训练才有较好效果。但其对于token的并行计算大大加速了推理速度,并且对分布式训练支持较好,因此在目前数据量充足的情况下反而异军突起。由于内置了positional-embedding,因此较好地解决了attention结构中的位置不敏感性 + +# Encoder和Decoder + +image-20240116212517161 + +如上图所示,左边是encoder,右边是decoder。我们可以看到目前的LLM模型几乎都是decoder结构,为什么encoder-decoder结构模型消失了呢?有以下几个原因: + +- encoder-decoder模型分布式训练困难 decoder模型结构简单,其分布式训练相对容易,而encoder-decoder结构的模型由于结构复杂的多导致了训练时工程结构复杂,成本大大增加 +- 有论文证明,encoder-decoder模型在参数量不断增加时不具有显著优势。在模型较小时,由于中间隐变量的存在,decoder部分进行交叉注意力会获得更好的效果,但随着模型增大,这些提升变得不再明显。甚至有论文猜测,encoder-decoder结构的收益仅仅是因为参数量翻倍 + +因此,目前的模型都是decoder模型,encoder-decoder模型几乎销声匿迹。 + +# 延展阅读 + +我们可以看到,LLaMA2的模型特点是: + +1. 没有使用LayerNorm,而是使用了RMSNorm进行预归一化 +2. 使用了RoPE(Rotary Positional Embedding) +3. MLP使用了SwiGLU作为激活函数 +4. LLaMA2的大模型版本使用了Group Query Attention(GQA) + +## **RMSNorm** + +LayerNorm的公式是: +$$ +y=\frac{x-E(x)}{\sqrt{\operatorname{Var}(x)+\epsilon}} * \gamma+\beta +$$ + + +RMSNorm的开发者发现,减去均值做中心偏移意义不大,因此简化了归一化公式,最终变为: +$$ + \begin{align} \begin{split} & \bar{a}_i = \frac{a_i}{\text{RMS}(\mathbf{a})} g_i, \quad \text{where}~~ \text{RMS}(\mathbf{a}) = \sqrt{\frac{1}{n} \sum_{i=1}^{n} a_i^2} \end{split}\nonumber \end{align} +$$ + + +最终在保持效果不变的情况下,计算时间提升了40%左右。 + +## **RoPE** + +BERT模型使用的原始位置编码是Sinusoidal Position Encoding。该位置编码的原理非常简单: +$$ +PE_{(pos,2i)}=sin(pos/10000^{2i/d_{\mathrm{model}}}), PE_{(pos,2i+1)}=cos(pos/10000^{2i/d_{\mathrm{model}}}) +$$ + + +该设计的主要好处在于: + +1. 在位置编码累加到embedding编码的条件下,基本满足不同位置编码的内积可以模拟相对位置的数值 +2. 随着相对位置增大,其位置编码的内积趋近于0 +3. 具备一定的外推特性 + +LLM常用的位置编码还有AliBi(注意力线性偏置)。该方法不在embedding上直接累加位置编码,而选择在Q*K的结果上累加一个位置矩阵: + +![img](resources/acb24b1f-4737-4419-9ac3-50651c3fcf75.png) + +ALiBi的好处在于: + +1. 具备良好的外推特性 +2. 相对位置数值很稳定 + +RoPE的全称是旋转位置编码(Rotary Positional Embedding),该编码的推导过程和Sinusoidal Position Encoding的推导过程比较类似,不同之处在于后者是加性的,而前者是乘性的,因此得到的位置编码类似于: + +image-20240116212517161 + +或者也可以简化为: + +image-20240116212517161 + +该位置编码表示相对位置的几何意义比较明显,也就是两个向量的角度差。 + +该位置编码的优势在于: + +1. 位置编码矩阵是单位正交阵,因此乘上位置编码后不会改变原向量模长 +2. 相较于Sinusoidal Position Encoding具备了更好的外推特性 + +## **SwiGLU** + +SwiGLU是GLU结构的变种。GLU是和LSTM原理类似,但不能接受时序数据,只能处理定长数据。而且省略了遗忘门与记忆门,只保留了输入门,SwiGLU是将其中的激活函数替换为了SiLU: +$$ +\text{FFN}_{\text{Swish}}(x, W_1, W_2) = \text{Swish}_1(xW_1) W_2 +$$ + + +其中 +$$ +\text{Swish}_{1} +$$ + + +的表达式为: +$$ +\text{Swish}_{\beta}(x) = x \sigma(\beta x) +$$ + + +在SwiGLU的论文中,作者论证了SwiGLU在LOSS收益上显著强于ReLU、GeLU、LeakyGeLU等其他激活方法。 + +## **GQA** + +MHA(Multi-head Attention)是标准的多头注意力机制,具有H个Query、Key 和 Value 矩阵 + +MQA(Multi-Query Attention,来自于论文:Fast Transformer Decoding: One Write-Head is All You Need)共享了注意力头之间的KV,只为每个头保留单独的Q参数,减少了显存占用。 + +GQA(Grouped-Query Attention,来自于论文:GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints)在MQA的基础上分成了G个组,组内共享KV。 + +在Llama2模型中,70B参数为了提升推理性能使用了GQA,其他版本没有使用这项技术。 + +## ChatGLM2的模型结构 + +img + +ChatGLM2模型结构和Llama2的结构有一定相似之处,主要不同之处在于: + +1. 在开源的ChatGLM2代码中没有使用GQA,而是使用了MQA +2. QKV为单一矩阵,在对hidden_state进行整体仿射后拆分为Query、Key、Value +3. MLP结构中没有使用Up、Gate、Down三个Linear加上SwiGLU,而是使用了hidden_size -> 2 * ffn_hidden_size的Up Linear进行上采样,对tensor进行拆分为两个宽度为ffn_hidden_size的tensor后直接输入SiLU,然后经过ffn_hidden_size -> hidden_size的Down Linear进行下采样