文章目录
- 新发现
 - 前言
 - 1. Why this article ?
 - 2. Setup the experimentation
 - 3. The experiment results:A100/A10/3090
 - 4. Why is it different?
 - 5. Why do the calculation differ depending on the GPU ?
 - 结论
 
新发现
  最近在做RAG相关的工作,偶然间发现,生产环境(A100)与测试环境(A10)大模型推理的结果不太一致,后来做了进一步实验发现,相同的数据,相同的推理代码,相同的模型以及相同的软件版本环境,不同的GPU下,推理结果不完全一致。具体如下:
   A100:

  A10:

  3090:

  然后在Medium上发现了一篇博客,博主也分享了他的发现,与我的情况一模一样,在此做个记录。
前言
  大多数技术人员都知道,依赖项的不同版本可能会导致行为不同。然而,在大型语言模型领域,由于我们需要大量的计算资源,因此在训练和推理任务中我们严重依赖于GPU。然而,很少有人真正意识到更换GPU也会影响LLM的输出。
   当然,你可以创造两个完全相同的环境,可以设置依赖项版本,可以使用Dockerization,可以将LLM的温度系数设置为0,也可以设定任何你想要的seed。归根结底,除非你没有使用完全相同的GPU模型,否则这些都不会起作用。
   在这篇文章中,我将通过一个实验来强调这一现象,该实验显示了差异发生的位置和原因。
1. Why this article ?
  有一天,作者和一些人讨论为什么OpenAI和Anthropic模型在设计上不是确定性的。作者解释说,他们可能会使用混合专家(MoE, Mixture of Experts)方法,偶尔不会将token路由给最优专家,因为这些专家忙于处理其他token,这会导致答案不一致。
   另一个因素可能是OpenAI的批量查询效率。这些批处理的大小可以根据传入查询的数量而变化,这可以改变GPU计算策略,从而导致不同的结果。
   但也有人指出:“不同的GPU也可能导致不同的结果,不是吗?”
   当你使用OpenAI API时,在某个地方有一台远程机器代表你运行计算并返回结果。现在,如果机器并不总是运行在相同的硬件上,那么最终得到的模型响应可能就不会相同。
   考虑到这一点,可能就会出现其他问题:
   (1)如果我在生产环境中有一个LLM应用程序,并且我需要扩展到具有不同GPU的其他生产环境上,这是否会出现很严重的问题?
   (2)如果开发环境中的GPU与生产环境中的GPU不同,会怎么样?
   基于这些问题,作者做了一个实验,看看它的影响有多大。
  Note: 以下是博主的真实实验。
2. Setup the experimentation
主要环境库版本及其它参数:
# 依赖库
python        3.10.12
cuda          12.1
torch		  2.0.1
transformers  4.40.2
accelerate    0.26.1
# GPU型号
NVIDIA A100 40GB
NVIDIA A10  24GB
NVIDIA GeForce RTX 3090 24GB
# LLM
InternLM2.5-7B-Chat
# pip镜像
-i https://pypi.tuna.tsinghua.edu.cn/simple
 
与作者的一致,这里也统一设置随机数:
# Set seeds for reproducibility
random_seed = 42
np_seed = 42
torch_seed = 42
transformers_seed = 42
random.seed(random_seed)
np.random.seed(np_seed)
torch.manual_seed(torch_seed)
set_seed(transformers_seed)
 
3. The experiment results:A100/A10/3090
  模型使用的是书生·浦语InternLM2.5-7B-Chat,测试代码如下:
# -*- coding: utf-8 -*-
# Author  : liyanpeng
# Email   : yanpeng.li@cumt.edu.cn
# Datetime: 2024/8/31 13:08
# Filename: llm_prob_test.py
import os
import random
import numpy as np
import torch
from transformers import set_seed
from transformers import AutoTokenizer, AutoModelForCausalLM
from prettytable import PrettyTable
# Set seeds for reproducibility
random_seed = 42
np_seed = 42
torch_seed = 42
transformers_seed = 42
random.seed(random_seed)
np.random.seed(np_seed)
torch.manual_seed(torch_seed)
set_seed(transformers_seed)
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
device = "cuda"
model_path = '/data/liyanpeng/huggingface/internlm/internlm2-chat-7b'
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto", trust_remote_code=True,
                                             torch_dtype=torch.float16)
model = model.eval()
query = '你好'
prompt = """<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n"""
prompt = prompt.format(query=query)
response, _ = model.chat(tokenizer, query, do_sample=False, temperature=0,
                         history=[], meta_instruction='')
print(response)
 
  我们看下不同型号GPU下,模型的输出:
# A100
你好!很高兴为你服务。有什么我可以帮助你的吗?
# A10
你好!很高兴为你服务。有什么我可以帮助你的吗?
# 3090
你好!很高兴为你服务。有什么我可以帮助你的吗?
 
嗯哼,看着没啥问题,模型在三个不同型号的输出是一致的。现在将输入字符长度增加一些,再看看模型的输出,具体如下:
query = '2012年Hinton团队提出了AlexNet,它是首个充分利用GPU的并行能力训练出的卷积神经网络,并在ImageNet比赛一举夺魁,获得了85%的准确率,其准确分辨相似图像的能力惊艳了世界。如今,大部分的神经网络训练都离不开了GPU,对此,你有什么看法?(请注意,回复的内容长度不要超过300字,观点总结在一起,不要分开表述)'
 
由于输出的文本很长,为了方便对比,直接放在了表格中:

   可以看到,相同的模型,相同的输入,在不同的硬件上,输出结果是有差异的。
4. Why is it different?
  为什么相同的输入和相同的LLM在不同的GPU上生成的内容是不同的?
   很容易回答,由于LLM的自回归性质,下一个token是基于前一个token选择的,任何微小的变化都会引起级联反应,从而导致蝴蝶效应,最终生成的内容是不同的。
   而token的选择是基于概率来选择的,比如贪婪采样,在每一步,模型会选择概率最高的token作为下一个输出。
   为了让模型输出时可以看到每个token的概率,不在使用官方的chat函数,对代码做了更改,增加了return_dict_in_generate参数和output_scores参数,具体如下:
inputs = tokenizer([prompt], return_tensors="pt").to(device)
eos_token_id = [tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids(["<|im_end|>"])[0]]
outputs = model.generate(**inputs, max_new_tokens=512, do_sample=False, temperature=0,
                         eos_token_id=eos_token_id, return_dict_in_generate=True, output_scores=True)
                         
input_length = inputs.input_ids.shape[1]
generated_tokens = outputs.sequences[:, input_length:]
output_ids = generated_tokens[0].cpu().tolist()
response = tokenizer.decode(output_ids, skip_special_tokens=True)
response = response.split("<|im_end|>")[0]
print(response)
transition_scores = model.compute_transition_scores(outputs.sequences, outputs.scores, normalize_logits=True)
table = PrettyTable(["token id", "token str", "probability"])
for tok, score in zip(generated_tokens[0], transition_scores[0]):
    table.add_row([f'{tok:d}', tokenizer.decode(tok), f'{np.exp(score.cpu().numpy()):.2%}'])
print(table)
 
  以A100和A10的对比结果为例,这里截取了部分内容,可以看下每个token的输出概率:
# A100
+----------+-------------+-------------+
| token id |  token str  | probability |
+----------+-------------+-------------+
|   ...    |     ...     |     ...     |
|  70939   |     推动    |    52.30%   |
|  60362   |      了     |    99.34%   |
|  49145   |     GPU     |    75.64%   |
|  71071   |     硬件    |    27.03%   | <-- here
|  60381   |      和     |    35.73%   |
|  68367   |     软件    |    64.54%   |
|  74657   |    技术的   |    69.70%   |
|  68705   |     不断    |    50.39%   |
|  70212   |     进步    |    54.20%   |
|  60355   |      。     |    96.46%   |
|   ...    |     ...     |     ...     |
+----------+-------------+-------------+
# A10
+----------+-------------+-------------+
| token id |  token str  | probability |
+----------+-------------+-------------+
|   ...    |     ...     |     ...     |
|  70939   |     推动    |    51.97%   |
|  60362   |      了     |    99.35%   |
|  49145   |     GPU     |    75.36%   |
|  75075   |     架构    |    31.85%   | <-- here
|  60354   |      的     |    44.99%   |
|  68705   |     不断    |    36.70%   |
|  70386   |     优化    |    84.05%   |
|  60353   |      ,     |    95.13%   |
|  60367   |      以     |    42.60%   |
|  70188   |     适应    |    70.76%   |
|  70907   |     深度    |    25.57%   |
|  68352   |     学习    |    81.61%   |
|  71370   |    的需求   |    75.34%   |
|  60355   |      。     |    99.96%   |
|   ...    |     ...     |     ...     |
+----------+-------------+-------------+
 
  可以看到,A100和A10输出的概率并不完全相同。通常情况下,这不会影响token的顺序,但在某些情况下有影响。例如,在A100上"硬件"的概率为27.03%,而在A10上的"架构"的概率为31.85%,由于大模型自回归的性质,从这个token开始,输出的内容就不太一样了。
5. Why do the calculation differ depending on the GPU ?
  为什么不同GPU的计算会有所不同?
   GPU之间的不同计算可以归因于以下几个因素:
   (1)并行计算处理
   GPU都是关于高效并行处理大量计算的。但是,不同的GPU在管理并行任务时可能会有所不同,从而影响操作顺序和内存访问。这很重要,因为在编程中,数量级相差很大的数字即使是简单相加也可能是非结合的(non-associative),从而导致精确计算中的潜在不准确性。
 
non-associative,通俗的理解为不满足结合律。
比如下面的这个例子:
import torch
# Define three floating-point numbers in bfloat16 with a large difference in magnitude
a = torch.tensor(1e10, dtype=torch.bfloat16)
b = torch.tensor(-1e10, dtype=torch.bfloat16)
c = torch.tensor(1.0, dtype=torch.bfloat16)
# Calculate the sums in different orders
sum1 = (a + b) + c
sum2 = a + (b + c)
# Print the results in bfloat16
print(f"(a + b) + c in bfloat16: {sum1}")
# >>> 1.0
print(f"a + (b + c) in bfloat16: {sum2}")
# >>> 0.0
 
  对于LLM来说,数百万次的计算可能会导致由于小的重复不准确而产生偏差,这会影响序列生成过程中的词汇选择。
   (2)硬件架构
   不同的GPU硬件,如NVIDIA Tesla T4和NVIDIA A10,具有不同的硬件架构。这些架构旨在优化性能的各个方面,包括并行处理能力、内存带宽和计算单元。例如,T4使用的是图灵架构,而A10基于安培架构。不同的架构意味着浮点运算、内存访问模式和其他低级操作的不同实现方式。即使这些实现中存在微小的差异,也可能导致计算结果的变化。例如,针对更高精度优化的架构可能与针对速度优化的架构产生不同的结果,即使两者都执行相同的浮点运算。
A100、A10与3090都是Ampere架构。
  (3)量化影响
   量化后的模型可以减少其精度以节省内存和计算资源,但它也引入了额外的误差来源。这些误差的影响可能会根据GPU处理较低精度算术的方式而有所不同。由于量化涉及对数字的近似处理,不同的GPU可能会以不同的方式处理这些近似值,从而导致token预测概率的变化。
结论
  很有趣的发现,博主本人对CUDA的具体计算原理了解的不多,但让我想起了以前写算子时,对数据进行不同的切片计算效率不一,结果一致,所以对上述的发现更倾向于第一种,即不同硬件上计算时,由于LLM的层数很多,导致在传递时,某些数值不满足了结合律,从而出现了计算的偏差,然后偏差继续向前传播,导致结果差别越来越大。
   对这一现象,我觉得还需要再研究一下。


















