Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions docs/source/cources/data_processing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
在模型训练过程中,数据及数据处理是最为重要的工作之一。在当前模型训练流程趋于成熟的情况下,数据集的好坏,是决定了该次训练能否成功的最关键因素。
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这些内容计划放到modelscope网站吗

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

后面会移出这个repo,可能单独建一个教程的github repo,并且把教程贴到官网的文档中


在上一篇中,我们提到了模型训练的基本原理是将文字转换索引再转换为对应的向量,那么文字转为向量的具体过程是什么?

# 分词器(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)
```
189 changes: 189 additions & 0 deletions docs/source/cources/deployment.md
Original file line number Diff line number Diff line change
@@ -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结构来说:

<img src="resources/image-20240116161847987.png" alt="image-20240116161847987" style="zoom:33%;" />

推理时的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)
Loading