LLM推理4:vllm和HF推理结果不一致

LLM推理4:vllm和HF推理结果不一致

LLM推理4:vllm和HF推理结果不一致

目录

收起

vllm和HF推理结果不一致

OPTDecoderLayer过程

Sampler过程

tokenizer影响

结论

vllm代码修改步骤说明

相关讨论

vllm介绍可参考: ?

vllm和HF推理结果不一致

看到很多人说同样的模型、参数和prompt条件下,vllm推理和Huggingface推理结果不一致。

因此详细查看了代码,过程如下:

1、首先参考这个issue,写两份python代码。一个调用vllm,一个调用huggingface。

 # Load model directly
from transformers import AutoTokenizer, AutoModelForCausalLM

tokenizer = AutoTokenizer.from_pretrained("../../models/opt-125m")
model = AutoModelForCausalLM.from_pretrained("../../models/opt-125m",from_tf=True).cuda().half()

prompt = [
    "Hello, what is apple?  ",
]

input_ids = tokenizer(prompt, return_tensors="pt").input_ids.cuda()

generated_ids = model.generate(input_ids, do_sample=False, repetition_penalty=1.2, max_new_tokens=500)

texts = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)

print(texts)


""" 推理结果
I'm a bit confused.\nApple is the company that makes iPhones and iPads. They are also known for their Macs.\nI thought they were called Apple Pencils?\nThey're called Apple Pencils."
"""
from vllm import LLM, SamplingParams

prompts = [
    "Hello, what is apple?  "
]

# Create a sampling params object.
sampling_params = SamplingParams(
        temperature=0,
        max_tokens=500, 
        frequency_penalty=1.2
        )

# Create an LLM.
llm = LLM(model="../../models/opt-125m", block_size=8, gpu_memory_utilization= 0.8)

for i in range(1):
    outputs = llm.generate(prompts, sampling_params)
    # Print the outputs.
    for output in outputs:
        prompt = output.prompt
        generated_text = output.outputs[0].text
        token_ids = output.outputs[0].token_ids
        print(generated_text)


""" 推理结果
I'm a bit confused.\nApple is the company that makes iPhones, iPads, and Macs.  Apple also owns the iPhone line of computers and tablets.
"""

可以看出两种方式的推理结果并不一致,下面进行代码分析

OPTDecoderLayer过程

运行generate()函数,当经过self_attn_layer_norm前处理,准备进行attention计算时,huggingface的hidden_states为[1,9,768],vllm的输入为[16,768](vllm的输入token长度也为9,会先padding到16,在attention处理时再根据实际长度(9)截断)

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

这时候hidden_states的值是完全相同的。

但hidden_states经过qkv_proj得到的query、key和value是不一致的。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

query数据

然后attention得到的attn_output结果也是不一致的

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

attn_output部分数据

再经过依次decoder处理后,可看出不一致的部分更多。随着处理的次数越多,误差越来越大。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

猜测为当误差累积到一定程度,采样结果开始不一致。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

那区别就在attention部分计算。其中vllm使用的是自定义的OPTAttention,其中attention部分调用了xformers库(xformers调用的flash-attention库)。huggingface的实现在modeling_opt.py的OPTDecoderLayer中。

通过打印qkv的线性层权重进行对比,权重完全相同,并没有区别。而huggingface中使用torch默认的线性变换函数nn.Linear()。vllm中的线性变换使用的ColumnParallelLinear()和RowParallelLinear(),调用的也是torch库的nn.Linear()函数。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

唯一区别是vllm中使用qkv_proj[768,2304]表示,而HF中使用了q_proj、k_proj、v_proj三个Linear[768,768]表示。

通过将vllm中的qkv_proj改为三个单独的nn.Linear()

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

测试得query、key、value结果和HF完全一致。 说明Linear()的参数对计算精度有影响!!!

当相同的query、key、value进行attention计算后,vllm和HF的计算结果有所区别,说明vllm的PagedAttention实现(调用的xformers)和HF的OPTAttention实现也有区别

Sampler过程

huggingface中的input_ids经过decoder得到的outputs为[1, 9, 768],通过lm_head线性层转换为[1, 9, 50272],取最后一组数据得到logits[1,50272],然后argmax处理生成得到next_tokens。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

将next_tokens结果cat到input_ids末端,重新进行下一轮推理。

而vllm中会先中hidden_status[16,768]中获取目标hidden_states[1,768](若token数量为9,则第9组数据就是推理得到的token结果),然后经过线性层转换得到logits[1,50272](这部分处理过程和HF原理上没有区别)。经过采样参数筛选,最终得到next_tokens。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

将上一次推理得到next_tokens作为唯一输入计算query、key和value,利用前一轮计算好的key和value缓存,继续推理next_tokens。

最大区别在于采样策略,HF只采用了重复性惩罚+argmax处理,而vllm中对logit根据top-p、top_k、temperatures参数进行了处理。

其中HF的代码如下,惩罚系数为1.2

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

HF的logits惩罚处理

将vllm代码修改为如下所示。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

vllm采样代码修改

见证奇迹的时刻来了!

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

推理结果对比

虽然vllm也有惩罚处理,但是处理过程和HF并不一致。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

vllm的logits惩罚处理

tokenizer影响

根据这个issue测了下tokenizer的影响:

推理结果没有区别。

结论

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

1、将vllm中的qkv_proj[768,2304]改成三个单独的q_proj[768,768]、k_proj、v_proj,生成结果会一点区别。说明Linear()的参数量对计算精度有影响(可忽略不计)。

2、vllm中第一次multi_query_kv_attention()调用xformers的PagedAttention()、xformers调用flash-attention的mha_varlen_fwd(),后续的single_query_cached_kv_attention调用的自定义cuda实现。和huggingface的OPTAttention()实现不一致(影响幅度很小,主要影响不在这里)。

3、采样方法会影响推理结果。将vllm的惩罚处理改为和HF的惩罚处理一致,推理完全结果相同!!!

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致
www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

有人对此做了评测:

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

vllm代码修改步骤说明

可以根据下面思路手动修改代码,

!!!下面的修改仅适用于vllm单prompt处理,多个prompt处理时结果和HuggingFace不一致(改起来比较麻烦,大家有需要的可以自己改)!!!

也可以直接借助下面这个git库重新安装vllm,这个github库好像完全修复了这个问题(我没试,从

听说 )。

1、由于huggingface官方在对logits进行惩罚处理时需要全部的input_ids,而vllm采样处理时没有input_ids参数,因此在model_executor\input_metadata.py的InputMetadata类中增加self.input_ids_cur变量保存当前的input_ids参数

        self.max_context_len = max_context_len
        self.block_tables = block_tables
        self.input_ids_cur = None  # add. 保存当前输入的input_ids

        self.num_prompts = len(prompt_lens)
        self.num_prompt_tokens = sum(prompt_lens)
        self.num_generation_tokens = context_lens.shape[0]

每次推理时,对input_ids_cur进行赋值(在worker/worker.py文件中有两处需要修改)

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

在model_executor/layers/文件中的_apply_penalties()函数,增加input_metadata参数,用于传入当前处理的input_ids

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致

2、由于huggingface官方在对logits进行惩罚处理时需要全部的input_ids,而vllm第一次将用户输入的input_ids处理后,保存kv缓存数据。后续只输入已推理完成的最后一个prompt的input_ids。因此需要增加中间变量,整理全部的input_ids。

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致
global_input_ids = []  # add. 增加全局变量存储所有的input_ids
def _apply_penalties(
    logits: torch.Tensor,
    output_tokens: List[List[int]],
    presence_penalties: List[float],
    frequency_penalties: List[float],
    vocab_size: int,
    input_metadata: InputMetadata,  # add. 增加参数
) -> torch.Tensor:
    num_seqs = logits.shape[0]
    # Collect the indices of sequences that have non-zero penalties.
    if torch.max(input_metadata.input_ids_cur) > 0:  # add. 将当前处理的input_ids拼接到历史input_ids上
        global global_input_ids
        global_input_ids+=input_metadata.input_ids_cur.cpu().numpy().tolist()[0:input_metadata.num_valid_tokens]

    indices = []

3、上述修改旨在获取全部的input_ids,然后根据修改惩罚系数处理logits

www.zeeklog.com  - LLM推理4:vllm和HF推理结果不一致
    # We follow the definition in OpenAI API.
    # Refer to https://platform.openai.com/docs/api-reference/parameter-details
    # logits[indices] -= frequency_penalties.unsqueeze(dim=1) * bin_counts
    all_input_ids = torch.Tensor(global_input_ids).type(  # add. 参考HuggingFace的惩罚处理方式
        input_metadata.input_ids_cur.dtype).to(
            input_metadata.input_ids_cur.device).unsqueeze(dim=0)
    logits_ = torch.gather(logits, 1, all_input_ids)
    logits_ = torch.where(logits_ < 0, logits_ * frequency_penalties[0], logits_ / frequency_penalties[0])
    logits.scatter_(1, all_input_ids, logits_)
    
    presence_mask = (bin_counts > 0.0).to(dtype=logits.dtype)
    logits[indices] -= presence_penalties.unsqueeze(dim=1) * presence_mask

相关讨论

Read more

深入理解 Proxy 和 Object.defineProperty

在JavaScript中,对象是一种核心的数据结构,而对对象的操作也是开发中经常遇到的任务。在这个过程中,我们经常会使用到两个重要的特性:Proxy和Object.defineProperty。这两者都允许我们在对象上进行拦截和自定义操作,但它们在实现方式、应用场景和灵活性等方面存在一些显著的区别。本文将深入比较Proxy和Object.defineProperty,包括它们的基本概念、使用示例以及适用场景,以帮助读者更好地理解和运用这两个特性。 1. Object.defineProperty 1.1 基本概念 Object.defineProperty 是 ECMAScript 5 引入的一个方法,用于直接在对象上定义新属性或修改已有属性。它的基本语法如下: javascript 代码解读复制代码Object.defineProperty(obj, prop, descriptor); 其中,obj是目标对象,prop是要定义或修改的属性名,descriptor是一个描述符对象,用于定义属性的特性。 1.2 使用示例 javascript 代码解读复制代码//

By Ne0inhk

Proxy 和 Object.defineProperty 的区别

Proxy 和 Object.defineProperty 是 JavaScript 中两个不同的特性,它们的作用也不完全相同。 Object.defineProperty 允许你在一个对象上定义一个新属性或者修改一个已有属性。通过这个方法你可以精确地定义属性的特征,比如它是否可写、可枚举、可配置等。该方法的使用场景通常是需要在一个对象上创建一个属性,然后控制这个属性的行为。 Proxy 也可以用来代理一个对象,但是相比于 Object.defineProperty,它提供了更加强大的功能。使用 Proxy 可以截获并重定义对象的基本操作,比如访问属性、赋值、函数调用等等。在这些操作被执行之前,可以通过拦截器函数对这些操作进行拦截和修改。因此,通过 Proxy,你可以完全重写一个对象的默认行为。该方法的使用场景通常是需要对一个对象的行为进行定制化,或者需要在对象上添加额外的功能。 对比 以下是 Proxy 和 Object.defineProperty 的一些区别对比: 方面ProxyObject.defineProperty语法使用 new Proxy(target,

By Ne0inhk