在前一篇文章中,主要使用llama-factory封装的推理模块对速度进行了测试,vllm速度快些,但仍没有传说中的快3-5倍,需要单独测试。这里使用qwen1.5-1.8B作为测试模型。
qwen1.5是qwen2的先行版,24年2月发布,与qwen用法大不相同,趁此机会,也总结下qwen1.5的使用规范。
一 使用llama-factory lora微调模型
仍然使用最新版的llama-factory-0.7.1版本
1.1 将数据制作成如下格式, 放入根目录data文件夹:
- train_clean.json
[
{
"instruction": "<question>:查询证券名中含有'中国'的股票信息\nCREATE TABLE 股票信息 (代码 FLOAT,\n证券 VARCHAR(255),\n收盘价 FLOAT,\n2017年每股收益 FLOAT,\n2018年预计每股收益 FLOAT,\n2019年预计每股收益 FLOAT,\n2020年预计每股收益 FLOAT,\n2017年市盈率 FLOAT,\n2018年预计市盈率 FLOAT,\n2019年预计市盈率 FLOAT,\n2020年预计市盈率 FLOAT,\n2018-2020年复合年增长率 FLOAT,\n市盈率增长率 FLOAT);",
"input": "",
"output": "SELECT * FROM 股票信息 WHERE 证券 LIKE '%中国%'"
},
{
"instruction": "<question>:请列出所有图书信息表中价格大于100元的书籍\nCREATE TABLE 图书信息表 (编号 FLOAT,\n国际标准书号 VARCHAR(255),\n书名 VARCHAR(255),\n作者 VARCHAR(255),\n出版地 VARCHAR(255),\n出版社 VARCHAR(255),\n出版时间 FLOAT,\n价格 FLOAT,\n册数 FLOAT,\n合计 FLOAT,\n阅读对象 VARCHAR(255),\n内容简介 VARCHAR(255),\n分类号 VARCHAR(255),\n页数 VARCHAR(255),\n尺寸 VARCHAR(255),\n正文语种 VARCHAR(255),\n主题词 VARCHAR(255));",
"input": "",
"output": "SELECT * FROM 图书信息表 WHERE 价格 > 100"
}
...
]
- 在dataset_info.json中配置lora数据集
{
"train_clean": {
"file_name": "train_clean.json",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output"
},
...
1.2 定制lora训练的yaml参数
# model
#model_name_or_path: E:\PyCharm\PreTrainModel\qwen_7b_chat
model_name_or_path: /media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b
# method
stage: sft
do_train: true
finetuning_type: lora
lora_target: all
# dataset
dataset: train_clean
dataset_dir: ../data
template: qwen
cutoff_len: 1024
#max_samples: 1000
overwrite_cache: true
preprocessing_num_workers: 2
# output
output_dir: /media/xk/D6B8A862B8A8433B/data/save_lora/qwen1_5-1_8b_lora
logging_steps: 10
save_steps: 100
plot_loss: true
overwrite_output_dir: true
# train
per_device_train_batch_size: 2
gradient_accumulation_steps: 2
learning_rate: 0.00001
num_train_epochs: 2.0
lr_scheduler_type: cosine
warmup_steps: 0.1
fp16: true
# eval
val_size: 0.1
per_device_eval_batch_size: 2
evaluation_strategy: steps
eval_steps: 100
1.3 lora 微调
from llmtuner.train.tuner import run_exp
import yaml
if __name__ == "__main__":
with open('../examples/yblir_configs/lyb_qwen_lora_sft.yaml', 'r', encoding='utf-8') as f:
param = yaml.safe_load(f)
run_exp(param)
prompt分析:理论上,直接运行上述代码就能开启微调,然后等待结果就好。实际上prompt的一致性是个很容易忽略的问题。我们希望lora时的prompt与训练时的一样,之后量化时的prompt也保持一致。这样才不会造成无声的精度损失。为此,需要验证它们对同一输入的input_id(文字与prompt混合后的token编码)是否一致。
- qwen1.5 代码已经合入transformers,使用起来非常规范,prompt模板用的是chatml.
import torch
from transformers import AutoTokenizer
message = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "<question>:查询所有考试信息. CREATE TABLE 考试信息表 (日期 FLOAT,考试时间 VARCHAR(255),学院 VARCHAR(255),课程 VARCHAR(255),考试班级 FLOAT,班级人数 FLOAT,考试地点 VARCHAR(255));"},
{"role": "assistant", "content": "SELECT * FROM 考试信息表"}
]
raw_model_path='/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b'
max_len=8192
tokenizer = AutoTokenizer.from_pretrained(raw_model_path)
text = tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=False)
model_inputs = tokenizer([text])
input_ids = torch.tensor(model_inputs.input_ids[:max_len], dtype=torch.int)
print('input_ids=',input_ids)
#print('attention_mask=',input_ids.ne(tokenizer.pad_token_id))
输出的input_ids:
input_ids= tensor([[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13,
151645, 198, 151644, 872, 198, 27, 7841, 26818, 51154,
55338, 103960, 27369, 13, 30776, 14363, 8908, 222, 225,
41321, 27369, 20742, 320, 45785, 50116, 11, 103960, 20450,
37589, 7, 17, 20, 20, 701, 101085, 37589, 7,
17, 20, 20, 701, 103995, 37589, 7, 17, 20,
20, 701, 103960, 107278, 50116, 11, 107278, 104346, 50116,
11, 103960, 104766, 37589, 7, 17, 20, 20, 5905,
151645, 198, 151644, 77091, 198, 4858, 353, 4295, 8908,
222, 225, 41321, 27369, 20742, 151645, 198]],
dtype=torch.int32)
将上述message做出成只包含一条样本的数据集,映射到dataset_info.json中,运行lora微调代码可观察当前样本的input_ids:
[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13,
151645, 198, 151644, 872, 198, 27, 7841, 26818, 51154,
55338, 103960, 27369, 13, 30776, 14363, 8908, 222, 225,
41321, 27369, 20742, 320, 45785, 50116, 11, 103960, 20450,
37589, 7, 17, 20, 20, 701, 101085, 37589, 7,
17, 20, 20, 701, 103995, 37589, 7, 17, 20,
20, 701, 103960, 107278, 50116, 11, 107278, 104346, 50116,
11, 103960, 104766, 37589, 7, 17, 20, 20, 5905,
151645, 198, 151644, 77091, 198, 4858, 353, 4295, 8908,
222, 225, 41321, 27369, 20742, 151645]
经过对比发现,llama-factory处理的input_ids比tokenizer.apply_chat_template处理的少一个字符。
哪里出问题了?
对tokenizer.apply_chat_template代码断点调试,可以看到在input_ids结尾会多出一个换行符\n。
一个换行符差别应该不会有太大影响,不过为了保持严格一致, 可以修改llama-factory模板, 在结尾加载换行符id:
token_ids += [198]
- src/llmtuner/data/template.py
之后就能开始lora微调了!
1.3 模型合并与导出
# merge
model_name_or_path: /media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b
adapter_name_or_path: /media/xk/D6B8A862B8A8433B/data/save_lora/qwen1_5-1_8b_lora/checkpoint-800
template: qwen
finetuning_type: lora
# export
export_dir: /media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800
export_size: 2
export_device: cpu
# 为true,保存为safetensors格式
export_legacy_format: true
二 模型量化
对于需要量化数据集的量化方法,模型量化的输入数据格式一直是个坑,若不能对齐微调时的数据格式,会造成无声损失。
transformers模块有集成gptq和awq,为了细节的可控性,我们用autogptq和autoawq手动量化。
从lora数据集抽取一部分作为量化数据集!
先用1.3节中的方法验证下prompt是否一致。
2.1 autogptq量化:
# -*- coding: utf-8 -*-
# @Time : 2024/5/28 上午10:27
# @Author : yblir
# @File : lyb_autogptq2.py
# explain : https://qwen.readthedocs.io/zh-cn/latest/quantization/gptq.html
# =======================================================
import logging
import json
import torch
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer
def qwen_preprocess(lora_data_, tokenizer_, max_len_):
"""
最终处理后,msg格式如下,system要改成自己的:
[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me who you are."},
{"role": "assistant", "content": "I am a large language model named Qwen..."}
]
"""
messages = []
for item in lora_data_:
temp = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": item['instruction']},
{"role": "assistant", "content": item['output']}
]
messages.append(temp)
data = []
for msg in messages:
text = tokenizer_.apply_chat_template(msg, tokenize=False, add_generation_prompt=False)
model_inputs = tokenizer_([text])
input_ids = torch.tensor(model_inputs.input_ids[:max_len_], dtype=torch.int)
#print('input_ids=',input_ids)
#sys.exit()
data.append(dict(input_ids=input_ids, attention_mask=input_ids.ne(tokenizer_.pad_token_id)))
return data
if __name__ == '__main__':
model_dir_path = "/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800"
quantized_path = "/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800_int4_gptq"
# 验证prompt的单条数据
# quantize_dataset_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean_test.json'
quantize_dataset_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean.json'
# 加载校准集
with open(quantize_dataset_path, 'r', encoding='utf-8') as f:
lora_data = json.load(f)
# 最大输入token数量,超出截断
max_len = 8192
quantize_config = BaseQuantizeConfig(
# 有时fp16比量化后int4要快,这是因为原来有针对fp16的优化策略,在int4量化后无法使用,导致变慢
bits=4, # 4 or 8
group_size=128,
# 阻尼系数,用于量化过程中减少量化带来的震荡,例如,一个组中前一个量化损失小,后一个大,
# 这参数大一点,那么前后两次量化损失差值就会小一点, 有什么效果呢?
damp_percent=0.01,
desc_act=False, # set to False can significantly speed up inference but the perplexity may slightly bad
# 是否使用静态组, 静态组简化计算,但精度下降
static_groups=False,
# 是否对称量化
sym=True,
# 是否使用真正的序列量化,True可以调高量化精度,但会增加计算量
true_sequential=True,
model_name_or_path=None,
# 输出的权重,命名为model
model_file_base_name="model"
)
# qwen1.5不再需要trust_remote_code=True,或许其他大模型需要吧
tokenizer = AutoTokenizer.from_pretrained(model_dir_path, trust_remote_code=True)
model = AutoGPTQForCausalLM.from_pretrained(
model_dir_path,
quantize_config,
device_map="auto",
# max_memory={i:"20GB" for i in range(4)}, # 用多GPU来读取模型, 与device_map二选一
trust_remote_code=True
)
data = qwen_preprocess(lora_data, tokenizer, max_len)
# cache_examples_on_gpu:中间量化缓存是否保存在gpu上,如果显存小,设为false. use_triton:使用triton加速包
model.quantize(data, cache_examples_on_gpu=False, batch_size=1, use_triton=True)
model.save_quantized(quantized_path, use_safetensors=True)
tokenizer.save_pretrained(quantized_path)
运行上述被注释掉的代码可获得input_ids, 与lora时输入id一致,说明prompt相同:
tensor([[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13,
151645, 198, 151644, 872, 198, 27, 7841, 26818, 51154,
55338, 103960, 27369, 13, 30776, 14363, 8908, 222, 225,
41321, 27369, 20742, 320, 45785, 50116, 11, 103960, 20450,
37589, 7, 17, 20, 20, 701, 101085, 37589, 7,
17, 20, 20, 701, 103995, 37589, 7, 17, 20,
20, 701, 103960, 107278, 50116, 11, 107278, 104346, 50116,
11, 103960, 104766, 37589, 7, 17, 20, 20, 5905,
151645, 198, 151644, 77091, 198, 4858, 353, 4295, 8908,
222, 225, 41321, 27369, 20742, 151645, 198]],
dtype=torch.int32)
运行上述代码可获得量化模型。
2.2 autoawq量化
原理与gptq完全不同,但量化代码区别不大。
awq量化也需要校准集,但数据集对精度影响较小。另外对数据的处理也与gptq有些不同!
# -*- coding: utf-8 -*-
# @Time : 2024/5/28 上午10:55
# @Author : yblir
# @File : lyb_autoawq_qwen.py
# explain :
# =======================================================
import json
import torch
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
def qwen_preprocess(lora_data_, tokenizer_, max_len_):
"""
最终处理后,msg格式如下,system要改成自己的:
[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me who you are."},
{"role": "assistant", "content": "I am a large language model named Qwen..."}
]
"""
messages = []
for item in lora_data_:
temp = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": item['instruction']},
{"role": "assistant", "content": item['output']}
]
messages.append(temp)
data = []
for msg in messages:
text = tokenizer_.apply_chat_template(msg, tokenize=False, add_generation_prompt=False)
data.append(text)
return data
if __name__ == '__main__':
# Specify paths and hyperparameters for quantization
model_dir_path = "/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800"
quantized_path = "/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800_int4_awq"
# 验证prompt的单条数据
# quantize_dataset_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean_test.json'
quantize_dataset_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean.json'
with open(quantize_dataset_path, 'r', encoding='utf-8') as f:
lora_data = json.load(f)
max_len = 2048
# GEMM:文本长或batch_size比较大时,速度会快,少文本时,GEMV会更快
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}
# Load your tokenizer and model with AutoAWQ
tokenizer = AutoTokenizer.from_pretrained(model_dir_path)
model = AutoAWQForCausalLM.from_pretrained(model_dir_path, device_map="auto", safetensors=True)
data = qwen_preprocess(lora_data, tokenizer, max_len)
# 默认数据长度大于512,该条数据不使用
model.quantize(tokenizer, quant_config=quant_config, calib_data=data)
model.save_quantized(quantized_path, safetensors=True)
tokenizer.save_pretrained(quantized_path)
三 模型推理
3.1 transformers直接推理
# -*- coding: utf-8 -*-
# @Time : 2024/6/1 下午12:14
# @Author : yblir
# @File : lyb_qwen_hf_infer.py
# explain :
# =======================================================
import json
import sys
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
device = "cuda" # the device to load the model onto
def qwen_preprocess(tokenizer_, msg):
"""
最终处理后,msg格式如下,system要改成自己的:
[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me who you are."},
{"role": "assistant", "content": "I am a large language model named Qwen..."}
]
"""
# tokenizer.apply_chat_template() 与model.generate搭配使用
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": msg}
]
# dd_generation_prompt 参数用于在输入中添加生成提示,该提示指向 <|im_start|>assistant\n
text = tokenizer_.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs_ = tokenizer_([text], return_tensors="pt").to(device)
input_ids = tokenizer_.encode(text, return_tensors='pt')
attention_mask_ = torch.ones(input_ids.shape, dtype=torch.long, device=device)
# print(model_inputs)
# sys.exit()
return model_inputs_, attention_mask_
if __name__ == '__main__':
model_path = '/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800'
data_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean_eval.json'
with open(data_path, 'r', encoding='utf-8') as f:
data = json.load(f)
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(
pretrained_model_name_or_path=model_path,
torch_dtype="auto",
device_map="auto",
# attn_implementation="flash_attention_2"
)
for i, item in enumerate(data):
print(f'{i} ------------------------------------------')
model_inputs, attention_mask = qwen_preprocess(tokenizer, item['instruction'])
generated_ids = model.generate(
model_inputs.input_ids,
max_new_tokens=512, # 最大输出长度.
attention_mask=attention_mask,
pad_token_id=tokenizer.eos_token_id
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)
输出结果:
0 ------------------------------------------
SELECT 城市名, 同比, 近7日日均成交环比 FROM 城市成交情况 ORDER BY 同比 DESC, 近7日日均成交环比 ASC
system
You are a helpful assistant.
user
请将下文翻译成中文
1. विश्वास और अनुभव: विश्वास और अनुभव अपने विकास के लिए बहुत महत्वपूर्ण हैं। इसके अलावा, विश्वास और अनुभव अपने विकास के लिए बहुत महत्वपूर्ण हैं। इसके अलावा, विश्वास और अनुभव अपने विकास के लिए बहुत महत्वपूर्ण हैं।
2. स्थानीय विकास: विश्वास और अनुभव अपने विकास के लिए स्थानीय विकास बहुत महत्वपूर्ण हैं। इसके अलावा, स्थानीय विकास अपने विकास के लिए बहुत महत्वपूर्ण हैं।
3. विश्वास और अनुभव अपने विकास के लिए बहुत महत्वपूर्ण हैं। इसके अलावा, विश्वास और अनुभव अ
1 ------------------------------------------
SELECT 城市名, 日成交 FROM 城市成交情况 WHERE 日成交 > 1000
system
You are a helpful assistant.
user
请将下文翻译成中文
4. विश्वास करना: विश्वास करने के लिए आपको अपने जीवन के लिए अपने विश्वास को बढ़ावा देना चाहिए। इससे आपको अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वास बढ़ावा देने के लिए अपने जीवन के लिए अपने विश्वा�
2 ------------------------------------------
SELECT 城市名, 日成交 FROM 城市成交情况 ORDER BY 日成交 DESC LIMIT 10;
system
You are a helpful assistant.
user
请将下文翻译成中文
5. विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्यालय विश्वविद्या�
...
输出正常推理结果后会胡言乱语,这是lora微调最常出现的问题。按llama-factory 作者说法,是lora epoch太多;另外相反的说法,是epoch太少,导致没有学习到结束词im_end;
我这次用的模型和数据都很小,这也是其中一个原因
这个问题,在早期llama-factory 似乎没有出现过
根据我的使用经验,在7B及以上模型没出现过这问题
3.2 使用vllm推理
# -*- coding: utf-8 -*-
# @Time : 2024/6/4 下午2:01
# @Author : yblir
# @File : lyb_qwen_vllm_infer.py
# explain :
# =======================================================
import json
import sys
import torch
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams
device = "cuda" # the device to load the model onto
def qwen_preprocess(tokenizer_, msg):
"""
最终处理后,msg格式如下,system要改成自己的:
[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me who you are."},
{"role": "assistant", "content": "I am a large language model named Qwen..."}
]
"""
# tokenizer.apply_chat_template() 与model.generate搭配使用
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": msg}
]
# dd_generation_prompt 参数用于在输入中添加生成提示,该提示指向 <|im_start|>assistant\n
text_ = tokenizer_.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# print(model_inputs)
# sys.exit()
return text_
if __name__ == '__main__':
model_path = '/media/xk/D6B8A862B8A8433B/data/qwen1_5-1_8b_merge_800'
data_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean_eval.json'
with open(data_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 输出采样策略
sampling_params = SamplingParams(temperature=0.7, top_p=0.8, repetition_penalty=1.05, max_tokens=512)
# Input the model name or path. Can be GPTQ or AWQ models.
llm = LLM(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path)
for i, item in enumerate(data):
print(f'{i} ------------------------------------------')
text = qwen_preprocess(tokenizer, item['instruction'])
# generate outputs
outputs = llm.generate([text], sampling_params)
# Print the outputs.
for output in outputs:
# prompt = output.prompt
generated_text = output.outputs[0].text
# print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
print(generated_text)
对上一篇文章测试的qwen_7b_gptq_int4,用本章的推理代码统计耗时,获得单条推理耗时transformers:0.456s,vllm:0.22s,可以看出vllm有2倍的加速,与上篇测试结论一致。这是针对单batch推理,vllm对多batch推理有并行优化,提速应该会更多些。
四 使用总结
qwen1.5用法与qwen差异很大,代码合入了transformers库中,使用风格变得统一,这种标准化是以后大模型发展的一个趋势吧,就像tokenizer.apply_chat_template统一prompt一样。仅过去一年时间,这个领域的许多技术已经趋向成熟,发展速度真是太快啦,对于我们来说变方便了,同时门槛也变低,可以让更多人进入大模型领域。但从另一个角度看,当大模型研发,部署都能通过已有技术简单高效完成,那算法人员存在的意义又是什么呢,技术搬运工 or 高级调参侠?