大模型学习(二)大模型vLLM推理
首先查看Qwen的README
可以看到使用modelscope调用一个大模型非常简单:
from modelscope import AutoModelForCausalLM, AutoTokenizer
from modelscope import GenerationConfig
# 可选的模型包括: "qwen/Qwen-7B-Chat", "qwen/Qwen-14B-Chat"
tokenizer = AutoTokenizer.from_pretrained("qwen/Qwen-7B-Chat", trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained("qwen/Qwen-7B-Chat", device_map="auto", trust_remote_code=True, fp16=True).eval()
model.generation_config = GenerationConfig.from_pretrained("Qwen/Qwen-7B-Chat", trust_remote_code=True) # 可指定不同的生成长度、top_p等相关超参
response, history = model.chat(tokenizer, "你好", history=None)
print(response)
response, history = model.chat(tokenizer, "浙江的省会在哪里?", history=history)
print(response)
response, history = model.chat(tokenizer, "它有什么好玩的景点", history=history)
print(response)
只需要加载分词器,模型以及一些配置信息等,即可对话大模型了。可以看到上述对话中,不断将history传入到模型中,实现一个多轮对话的效果。但是当前的情况下只适合单机一个人使用,此时你传入的字符串,会经过chat方法映射成tokenID序列交给模型推理;当把模型交给推理服务端,作为一个服务的时候,此时将没有chat方法了,只会加载模型本身,模型的输入就是tokenID的序列。
当我们在服务端是需要加速模型推理时,可以使用vLLM。它可以实现加载一次模型,并且在多线程的方式实现排队,对外提供http服务。当多个并发请求到来时,它会在内存里面把多个请求的prompt拼装成一个batch送入到达模型里面进行批推理,实现更高的吞吐。
安装vLLM库:
pip install vllm
tips:在魔搭社区可以看到Qwen1.8B模型有多个版本:

这里主要分文预训练模型和Chat模型,其中预训练模型是在超大规模的预训练数据上进行训练得到。预训练数据类型多样,覆盖广泛,包括大量网络文本、专业书籍、代码等。它与Chat模型的区别是:
1.8B预训练版本,训练数据的方式:
首先给出语料,例如:鸟纲的特征是有羽毛、喙没有牙齿、蛋有硬壳、高代谢率、心脏有四室、轻盈但结实的骨骼。所有鸟类的前肢都进化成翼,大部分也能够飞翔。它们有独特的消化系统及呼吸系统,很适合飞行。
模型输入:鸟纲的特征是有羽毛、喙没有牙齿、蛋有硬壳、高代谢率、心脏有四室、轻盈但结实的骨骼。
模型输出:所有鸟类的前肢都进化成翼,大部分也能够飞翔。它们有独特的消化系统及呼吸系统,很适合飞行。<|endoftext|>
它相当于将语料拆分成前后两段,给模型前一段文字,然后去预测后面一段文字。而Chat版本是在预训练版本进行微调训练数据,这个数据通常需要人工标注:
模型输入:<|im_start|>system\nyou are a helperful assitant!\n<|im_end|><|im_start|>user\n了解鸟类特征吗?\n<|im_end|><|im_start|>assitant:\n
模型输出:鸟纲的特征是有羽毛、喙没有牙齿、蛋有硬壳、高代谢率、心脏有四室、轻盈但结实的骨骼。<|im_end|><|endoftext|>
可以看到模型的输入和输出都是有一定格式控制的。首先是系统指令(给你一个身份),其次是用户的问题,最后是助手的回答,模型输出的内容就是助手的回答。当你需要进行微调时,你可以修改适用于自己的系统指令。
vLLM加载
了解完Qwen的模型输入输出结构后,接下来学习vLLM是如何进行推理的。
首先定义Qwen的特殊token:
# 通义千问的特殊token
IMSTART='<|im_start|>'
IMEND='<|im_end|>'
ENDOFTEXT='<|endoftext|>' # EOS以及PAD都是它
接下来就是加载本地或者网络模型以及相关的内容:
if os.path.exists(model_dir):
local_model_dir = model_dir
print(f"Using local model directory: {local_model_dir}")
else:
print(f"Model not found locally, downloading from ModelScope: {model_dir}")
local_model_dir = snapshot_download(model_dir)
# 模型下载
snapshot_download(model_dir)
self.generation_config = GenerationConfig.from_pretrained(model_dir,trust_remote_code=True)
# 加载分词器
self.tokenizer=AutoTokenizer.from_pretrained(model_dir,trust_remote_code=True)
self.tokenizer.eos_token_id=self.generation_config.eos_token_id
准备好推理终止词,遇到这些词停止继续推理:
self.stop_words_ids=[self.tokenizer.im_start_id,self.tokenizer.im_end_id,self.tokenizer.eos_token_id]
# stop_words_ids:{} [151644, 151645, 151643]
使用vLLM加载模型:
os.environ['VLLM_USE_MODELSCOPE']='True' #这是一个环境变量设置,告诉vLLM 框架:“我接下来要加载的模型可能来自 ModelScope(阿里云的模型开放平台),请启用对 ModelScope 模型格式的支持。
self.model=LLM(model=model_dir,
tokenizer=model_dir,
tensor_parallel_size=tensor_parallel_size,
trust_remote_code=True,
quantization=quantization,
gpu_memory_utilization=gpu_memory_utilization, # 0.6
dtype=dtype)
tips:当发现模型启动时显存不足时,可能与gpu_memory_utilization有关。
vLLM模型推理
接下来进行聊天推理:
def chat(self,query,history=None,system="You are a helpful assistant.",extra_stop_words_ids=[]):
函数可以传入问题,历史对话,系统指令以及额外的推理停止词(如果你想在你指定的地方停止推理)。
首先构造promt:
prompt_text,prompt_tokens=_build_prompt(self.generation_config,self.tokenizer,query,history=history,system=system)
构造完成会返回提示词文本和编码tokenID序列,打印结果:
提问:你好
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
你好<|im_end|>
<|im_start|>assistant
[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 108386, 151645, 198, 151644, 77091, 198]
接下来配置vLLM请求:
sampling_params=SamplingParams(stop_token_ids=stop_words_ids,
early_stopping=False,
top_p=self.generation_config.top_p,
top_k=-1 if self.generation_config.top_k == 0 else self.generation_config.top_k,
temperature=self.generation_config.temperature,
repetition_penalty=self.generation_config.repetition_penalty,
max_tokens=self.generation_config.max_new_tokens)
这些就是模型的配置参数等信息,然后调用vLLM执行推理:
# 调用vLLM执行推理(批次大小1)
req_outputs=self.model.generate(prompt_token_ids=[prompt_tokens],sampling_params=sampling_params,use_tqdm=False) # use_tqdm禁止进度条
req_output=req_outputs[0]
将prompt的tokenID序列和相关配置参数传入进行推理得到结果。打印返回信息查看结构:
# transformer模型的原生返回, 打开注释看一下原始推理结果
print("req_output:",req_output)
print("req_outputs:",req_outputs)
raw_response=req_output.outputs[0].text
print("raw_response:",raw_response)
#req_output: RequestOutput(request_id=0, prompt=None, prompt_token_ids=[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 108386, 151645, 198, 151644, 77091, 198], encoder_prompt=None, encoder_prompt_token_ids=None, prompt_logprobs=None, outputs=[CompletionOutput(index=0, text='你好!很高兴为你解答问题。有什么我可以帮助你的吗?', token_ids=array('l', [108386, 6313, 112169, 106184, 106185, 86119, 1773, 104139, 109944, 100364, 103929, 101037, 11319, 151645]), cumulative_logprob=None, logprobs=None, finish_reason=stop, stop_reason=151645)], finished=True, metrics=RequestMetrics(arrival_time=1762318798.8988488, last_token_time=1762318798.8988488, first_scheduled_time=1762318798.899235, first_token_time=1762318798.9658892, time_in_queue=0.00038623809814453125, finished_time=1762318799.0134034, scheduler_time=0.001102657988667488, model_forward_time=None, model_execute_time=None), lora_request=None)
#req_outputs: [RequestOutput(request_id=0, prompt=None, prompt_token_ids=[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 108386, 151645, 198, 151644, 77091, 198], encoder_prompt=None, encoder_prompt_token_ids=None, prompt_logprobs=None, outputs=[CompletionOutput(index=0, text='你好!很高兴为你解答问题。有什么我可以帮助你的吗?', token_ids=array('l', [108386, 6313, 112169, 106184, 106185, 86119, 1773, 104139, 109944, 100364, 103929, 101037, 11319, 151645]), cumulative_logprob=None, logprobs=None, finish_reason=stop, stop_reason=151645)], finished=True, metrics=RequestMetrics(arrival_time=1762318798.8988488, last_token_time=1762318798.8988488, first_scheduled_time=1762318798.899235, first_token_time=1762318798.9658892, time_in_queue=0.00038623809814453125, finished_time=1762318799.0134034, scheduler_time=0.001102657988667488, model_forward_time=None, model_execute_time=None), lora_request=None)]
#raw_response: 你好!很高兴为你解答问题。有什么我可以帮助你的吗?
再移除返回的tokenID序列的停用词,再进行解码:
# 移除停用词
response_token_ids=remove_stop_words(req_output.outputs[0].token_ids,stop_words_ids)
response=self.tokenizer.decode(response_token_ids)
在模型生成的 token 序列中,一旦遇到预设的“停止词”(如对话结束标记),就立即截断后续所有内容,避免把控制标记或多余内容暴露给用户。
最后整理历史对话:
# 整理历史对话
history.append((query,response))
return response,history
到此,使用vLLM模型推理的过程就结束了。
补充,接下来详细解读_build_prompt方法:
首先按ChatML格式构造千问的Prompt,下面是一个简单的CharML对话示例:
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
Hello!<|im_end|>
<|im_start|>assistant
Hi! How can I help you today?<|im_end|>
<|im_start|>user
What's the weather?<|im_end|>
接下来,我们学习如何构建这么一个ChatML的Prompt:
# 包裹发言内容的token
im_start,im_start_tokens='<|im_start|>',[tokenizer.im_start_id]
im_end,im_end_tokens='<|im_end|>',[tokenizer.im_end_id]
# 换行符token
nl_tokens=tokenizer.encode("\n")
定义一个方法,用于编码system/user/assistant的一段发言, 格式{role}\n{content}:
def _tokenize_str(role,content): # 返回元组,下标0是文本,下标1是token ids
return f"{role}\n{content}",tokenizer.encode(role)+nl_tokens+tokenizer.encode(content)
计算剩余token数:
left_token_space=generation_config.max_window_size
然后构造Prompt,这是重头戏,由三部分组成,头部+腰部+尾部,最重要的是头尾部,所以优先构造:
# prompt头部: system发言
system_text_part,system_tokens_part=_tokenize_str("system", system) # system_tokens_part --> system\nYou are a helpful assistant.
system_text=f'{im_start}{system_text_part}{im_end}'
system_tokens=im_start_tokens+system_tokens_part+im_end_tokens # <|im_start|>system\nYou are a helpful assistant.<|im_end|>
left_token_space-=len(system_tokens)
# system_text_part: system\nYou are a helpful assistant.
# system_text: <|im_start|>system\nYou are a helpful assistant.<|im_end|>
构造尾部:
# prompt尾部: user发言和assistant引导
query_text_part,query_tokens_part=_tokenize_str('user', query)
query_tokens_prefix=nl_tokens+ im_start_tokens
query_tokens_suffix=im_end_tokens+nl_tokens+im_start_tokens+tokenizer.encode('assistant')+nl_tokens
if len(query_tokens_prefix)+len(query_tokens_part)+len(query_tokens_suffix)>left_token_space: # query太长截断
query_token_len=left_token_space-len(query_tokens_prefix)-len(query_tokens_suffix)
query_tokens_part=query_tokens_part[:query_token_len] #对用户提问部分进行截断
query_text_part=tokenizer.decode(query_tokens_part)
query_tokens=query_tokens_prefix+query_tokens_part+query_tokens_suffix
query_text=f"\n{im_start}{query_text_part}{im_end}\n{im_start}assistant\n"
left_token_space-=len(query_tokens)
# query_text: <|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n
构造腰部:
# prompt腰部: 历史user+assitant对话
history_text,history_tokens='',[]
for hist_query,hist_response in reversed(history): # 优先采用最近的对话历史
hist_query_text,hist_query_tokens_part=_tokenize_str("user",hist_query) # user\n历史提问
hist_response_text,hist_response_tokens_part=_tokenize_str("assistant",hist_response) # assistant\n历史回答
# 生成本轮对话
cur_history_tokens=nl_tokens+im_start_tokens+hist_query_tokens_part+im_end_tokens+nl_tokens+im_start_tokens+hist_response_tokens_part+im_end_tokens
cur_history_text=f"\n{im_start}{hist_query_text}{im_end}\n{im_start}{hist_response_text}{im_end}"
# 储存多轮对话
if len(cur_history_tokens)<=left_token_space:
history_text=cur_history_text+history_text
history_tokens=cur_history_tokens+history_tokens
left_token_space-=len(cur_history_tokens)
else:
break
# cur_history_text: <|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n你好!很高兴为你解答问题。有什么我可以帮助你的吗?<|im_end|>
这里有个小细节,遍历历史的时候是反向遍历,优先选择最近的对话历史。
最后生成完整的Prompt:
# 生成完整Prompt
prompt_str=f'{system_text}{history_text}{query_text}'
prompt_tokens=system_tokens+history_tokens+query_tokens
# prompt_str: <|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n你好!很高兴为你解答问题。有什么我可以帮助你的吗?<|im_end|>\n<|im_start|>user\n你是谁?<|im_end|>\n<|im_start|>assistant
停用词清理代码,从后往前清理:
# 停用词清理
def remove_stop_words(token_ids,stop_words_ids):
token_ids=copy.deepcopy(token_ids)
while len(token_ids)>0:
if token_ids[-1] in stop_words_ids:
token_ids.pop(-1)
else:
break
return token_ids
更多推荐
所有评论(0)