相同的LLM在「不同GPU上」会产生不同输出?为什么?
相同的LLM在「不同GPU上」会产生不同输出?为什么?
在大语言模型(LLMs)的部署及其相关的算力扩容过程中,更换GPU是否也可能会对模型的输出产生重大影响?这个问题的答案对于确保LLMs在不同硬件环境的一致性和可靠性至关重要。本文通过实验和理论分析,揭示了即使在相同的开发环境、系统配置和随机种子下,不同的GPU也会导致LLMs产生不同的模型输出。
为什么要写这篇文章?
有一天,作者与一些人讨论为什么OpenAI和Anthropic的那些模型在设计时没有被构建为确定性的系统。作者解释说,它们可能采用了混合专家模型(Mixture of Experts, MoE)方法,偶尔不会将tokens路由给最优的专家模型,因为这些专家模型可能正忙于处理其他tokens,所以可能会导致模型响应的不一致。
另一个因素可能是OpenAI为了提高效率而对queries进行了批量处理。batch size会根据传入的queries数量而变化,可能会改变GPU的计算策略,从而导致不同的模型响应。
当有人指出,“不同的GPU也可能导致出现不同的模型响应,不是吗?”时,作者开始思考这一现象。考虑到使用OpenAI API时,实际上是有一台远程服务器帮我们执行计算并返回模型响应。如果这台机器并非总是在相同的算力基础设施上运行,那么最终得到的模型响应就不会相同。
这一思考引发了一系列问题:
- 如果有一个在生产开发环境中运行的LLM app,并且需要将其扩展到拥有不同GPU的其他实例,是否会出现很严重的问题?
- 如果开发环境中的GPU与生产环境存在大量不同之处,会怎么样?
这些问题促使作者设置了一个实验来突出这一现象,并探究它可能造成的影响有多大。
配置实验环境
为了突出这一现象,作者设置两个完全相同的开发环境,它们唯一的区别在于其所使用的GPU:第一个开发环境中使用的是Nvidia Tesla T4,第二个开发环境使用的便是Nvidia A10G。然后,使用Mistral-7b-v0.1进行测试,看看会发生什么。
要在notebook中运行实验,请按照以下步骤操作。
2.1 配置开发环境(Setup the environment)
- 配置CUDA版本
- 配置transformers和其他依赖
- 设置随机种子(random seeds)
注释 1:
仅设置transformers.set_seed应该就足够了,但作者还是想要确保万无一失。
注释 2:
本例使用的是Python 3.10。
2.2 加载Mistral模型
要从Hugging Face中加载Mistral-7B-v0.1模型,需要在环境变量HF_TOKEN中设置Hugging Face tokens。
本文将会使用量化版本的模型,降低计算精度来减少GPU的内存占用。
2.3 使用transformers库中的pipeline
将使用transformers库中的pipeline来简化从大语言模型(LLMs)生成模型响应的过程。
为了确保模型输出是可预测和一致的,希望从大语言模型的vocabulary中持续预测出最有可能的tokens,因此可以将top_k设置为1或将temperature设置为接近0的值。
此外,为了简单起见,将把max_new_tokens参数设置为1,这样LLMs就能只用单个token完成提示词。
当给出提示词序列 “I enjoy walking in the” 时,大语言模型(LLMs)只会生成一个单词:“woods”。如果大语言模型(LLMs)正确地生成并输出了这个单词,就可以继续进行实验了。
实验结果:T4 vs A10G
为了能够使用这两块GPU,通过AWS SageMaker启动了ml.g4dn.xlarge (T4) 和 ml.g5.xlarge (A10G) 实例。
让我们尝试运行一个简单的query:
T4和A10G给模型响应是一样的:
到目前为止一切进展顺利。不过,这只是一个简短的query。在RAG(检索增强生成)的应用场景里,通常会处理成千上万个tokens。现在使用在Hugging Face上托管的llama-2-arxiv-papers-chunked数据集来进行更大规模的query测试。
在下面的代码示例中,模仿RAG的工作方式,使用数据集索引0、4518、4519和799处获取的文本片段。其中第4518和4519个数据块(chunks)讨论了“Llama 2”,而其他片段则没有提及。期待LLMs能基于这些上下文信息回答:“Llama 2有什么特别之处?”该提示词大概有1,400个tokens长。
T4模型的输出如下:
A10G模型的输出如下:
确实很有趣。乍一看,由于两个模型响应开头相同,区别却不太明显。但在“等等(etc)……”之后,两者就有所差异了。
T4模型输出如下:“etc… This also means you can trust the output more since everything inside will be consistent across different runs!…”
A10G模型输出如下:“etc… This also means you can be more confident when asking questions specifically related to topics covered within those texts…”
T4 Colab vs T4 SageMaker
想知道使用相同GPU的两个开发环境是否会产生相同的模型输出?进行了一系列测试,结果确实完全相同。
为什么相同的用户输入(inputs)和相同的LLMs在两个GPUs上生成的答案会如此不同?
最终,这些模型响应因为LLMs的自回归特性而变得截然不同。由于下一个token是根据之前的tokens选择的,任何细微的变化都会引发一连串的连锁反应,就像蝴蝶效应一样。
请注意,这些模型响应并没有像提示词中所要求的那样基于所提供的上下文。LLMs并没有完全遵循指导性提示词(instructions),但这并不是很重要。
因为我们假设LLMs总是基于前面的tokens选择概率最高的token,所以可以肯定,区别在于如何在GPU上计算该概率。下面来看一看如何计算该概率。
计算tokens的选择概率(probabilities)
为了打印出每个被选中token的概率,将绕过常规处理流程(pipeline),直接使用tokenizer和model.generate方法。这样就能设置return_dict_in_generate=True和output_scores=True。接着,可以进行计算操作、对其进行归一化操作,并将transition scores转换为概率。
上述代码会显示每个token的ID、解码后的token以及其对应的概率。此处只列出相关的模型输出内容,因为完整的内容非常长。
T4 Output:
A10G Output:
好了,现在事情变得越来越有趣了。T4和A10G上的概率值并不完全一致。一般情况下,这样并不会影响tokens的排序序列,但有时候确实会造成影响。
例如,在T4模型中,“trust”出现的概率为18.74%,而在A10G上,“be”出现的概率则更高,达到了18.62%。从这一点来看,由于大语言模型的自回归特性,生成的内容将会出现偏差。
注释:量化大语言模型会降低计算精度,导致这类差异变得更为常见。
现在,一个非常合理的问题就出现了:“为什么计算结果会因为GPU的不同而产生差异呢?”
为什么GPU不同,模型运算结果也不同?
虽然不是CUDA expert,但进行过一些研究。不同GPU之间的计算差异可以归因于以下几个因素:
并行计算处理(Parallel Computation Handling)
GPUs的特点是能够高效地并行处理大量的计算任务。然而,不同GPU在管理这些并行任务时可能会有所差异,从而影响到运算顺序以及内存的访问方式。
这一点非常重要,因为在编程过程中,即使是数值大小相差很大的简单加法也可能是non-associative,从而影响到精确计算的准确性。所谓“Non-associativity”是指:(a + b) + c ≠ a + (b + c)。
因此,计算任务会被分割开来,独立进行处理,然后以non-associative方式组合在一起。因此,这些部分的内容如何重新组合会影响到最终结果。
这里有一个关于non-associative computation的简单示例:
对于大语言模型(LLMs),数百万次的计算可能会因为重复出现的微小误差而导致出现偏差,进而影响到序列生成过程中的字词选择。
硬件架构(Hardware Architecture)
不同型号的GPU,如Nvidia Tesla T4和Nvidia A10G,具备不同的硬件架构。这些硬件架构能够优化模型各个方面的性能,包括并行处理能力、内存带宽和计算单元。
例如,T4模型采用了Turing架构,而A10G模型基于Ampere架构。
不同的模型架构意味着在浮点运算、内存访问模式和其他底层操作上有着不同的实现方式。即使这些实现方式存在细微差别,也可能会导致计算结果出现差异。
例如,与针对更高计算精度而进行优化的模型架构相比,为了计算速度而优化的模型架构可能会产生不同的模型响应,即便两者都在执行相同的浮点运算。
模型量化的影响(Quantization Effects)
通过模型量化来降低计算精度可以节省内存资源和计算资源,但这样做也会引入额外的误差源。这些误差的影响因GPU对低精度运算的处理方式不同而不同。
由于模型量化过程中涉及到对数值的近似处理,因此不同的GPU在处理这些近似值时可能会有所差异,从而最终会导致token的预测概率产生变化。
使用多个GPU水平扩展LLMs时需要注意什么?
这个问题问得非常好,非常感谢!
如果只是简单地增加相同型号的GPU数量(例如,从单个A10G GPU扩展到拥有4个A10G GPU的实例),是否还有必要担心?
使用多个GPU进行推理时,有几种策略可供选择:
- 第一种策略是,如果模型可以装入GPU中,可以在每个GPU上加载一份模型副本。例如,如果向pipeline发送四条查询语句,每条查询语句可以由不同的GPU来处理。这样,将得到与仅使用一个GPU时相同的输出内容,但吞吐量会有所提高。
- 第二种策略通常用于因为模型太大一个GPU无法装入的情况,可以采用模型分片策略(sharding),将模型的权重分布到各个GPU上。虽然从理论上讲,这种做法可能会因为计算的分布和执行的不同而导致模型响应产生变化,但在实践测试中,使用模型切片技术得到的序列和概率与单个GPU上得到的结果是一致的。猜测这是因为PyTorch在设计时考虑到了deterministic operations。
Conclusion
已经证明了,即便是相同的开发环境、系统配置和随机种子,不同的GPU也会导致LLMs产生不同的结果。随着提示词长度的增长,这种不准确性也会随之增加,因为更长的提示词需要更多的算力,这会加剧不准确性(inaccuracies)的传播并促进两个GPU之间的差异。此外,在进行模型量化的情况下,这种效应更加显著。
并不是说这种情况一定是灾难性的,但这是在处理LLMs的部署时需要注意的一个因素。
如果开发时使用的GPU与生产环境中使用的GPU不同,应该设置测试实验确保性能仍然保持在可接受的范围内。如果计划将LLMs扩展到拥有不同GPU的新实例上,这一点也很重要。