相同的LLM在「不同GPU上」会产生不同输出?为什么?
相同的LLM在「不同GPU上」会产生不同输出?为什么?
在大语言模型(LLMs)的部署及其相关的算力扩容过程中,更换GPU是否也可能会对模型的输出产生重大影响?这个问题的答案对于确保LLMs在不同硬件环境下的一致性和可靠性至关重要。本文通过实验展示了在相同配置下,使用Nvidia Tesla T4和Nvidia A10G两种不同GPU的Mistral-7b-v0.1模型对相同输入产生不同输出的现象,并深入分析了其原因。
大多数技术工程师都了解,依赖库或依赖组件的版本不同都可能会导致系统行为产生变化。但在大语言模型(Large Language Models)领域,由于算力需求巨大,在训练和推理任务中我们都极度依赖GPU。然而,很少有人真正意识到,更换GPU也会对LLMs的输出产生影响。
假如你想创建两个完全一致的开发环境:
- 可以指定依赖库或组件的版本。
- 可以使用Dockerization。
- 可以将LLMs的temperature设置为0。
- 可以选择任意的随机种子。但是,如果使用的不是完全相同的GPU型号,以上所有努力都将白费。
本文将进行一次实验来强调这一现象,说明差异出现的位置及其原因。
为什么要写这篇文章?
有一天,作者和一些人讨论为什么OpenAI和Anthropic的那些模型在设计时没有被构建为确定性的系统。作者解释说,它们可能采用了混合专家模型(Mixture of Experts, MoE)方法,偶尔不会将tokens路由给最优的专家模型,因为这些专家模型可能正忙于处理其他tokens,所以可能会导致模型响应的不一致。
另一个因素可能是OpenAI为了提高效率而对queries进行了批量处理。batches size会根据传入的queries数量而变化,可能会改变GPU的计算策略,从而导致不同的模型响应。
当有人指出,“不同的GPU也可能导致出现不同的模型响应,不是吗?”时,作者开始思考这个问题。仔细想一想……当我们使用OpenAI API时,实际上是有一台远程服务器帮我们执行计算并返回模型响应。现在,如果这台机器并非总是在相同的算力基础设施上运行,那么最终得到的模型响应就不会相同。
想到这一点,可能就会出现其他问题:
- 如果有一个在生产开发环境中运行的LLM app,并且需要将其扩展到拥有不同GPU的其他实例,是否会出现很严重的问题?
- 如果开发环境(development environment)中的GPU与生产环境(production environment)存在大量不同之处,会怎么样?
这些问题促使作者想设置一个实验来突出这一现象,并探究它可能造成的影响有多大。
配置实验环境
为了突出这一现象,作者将设置两个完全相同的开发环境,它们唯一的区别在于其所使用的GPU:第一个开发环境中使用的是Nvidia Tesla T4,第二个开发环境使用的便是Nvidia A10G。然后,将使用Mistral-7b-v0.1进行测试,看看会发生什么。
要在notebook中运行实验,请按照以下步骤操作。
配置开发环境(Setup the environment)
- 配置CUDA版本
- 配置transformers和其他依赖
- 设置随机种子(random seeds)
注释 1:
仅设置transformers.set_seed应该就足够了,但作者还是想要确保万无一失。
注释 2:
本例使用的是Python 3.10。
加载Mistral模型
要从Hugging Face中加载Mistral-7B-v0.1模型,需要在环境变量HF_TOKEN中设置Hugging Face tokens。
本文将会使用量化版本的模型,降低计算精度来减少GPU的内存占用。
使用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选择的,任何细微的变化都会引发一连串的连锁反应,就像蝴蝶效应(butterfly effect)一样。
请注意,这些模型响应并没有像提示词中所要求的那样基于所提供的上下文。LLMs并没有完全遵循指导性提示词(instructions),但这并不是很重要。
因为我们假设LLMs总是基于前面的tokens选择概率(probabilities)最高的token,所以可以肯定,区别在于如何在GPU上计算该概率(probabilities),下面让我们来看一看如何计算该概率~
计算tokens的选择概率(probabilities)
为了打印出每个被选中token的概率,将绕过常规处理流程(pipeline),直接使用tokenizer和model.generate方法。这样就能设置return_dict_in_generate=True和output_scores=True。接着,就可以进行计算(compute)操作、对其进行归一化操作(normalize),并将transition scores(译者注:在自然语言处理领域,尤其是使用自回归模型生成文本时,模型会为每个next token分配一个概率分数,这个分数反映了该token作为tokens序列中next token的可能性大小。)转换为概率(probabilities)。
上述代码会显示每个token的ID、解码后的token以及其对应的概率(probability)。此处只列出相关的模型输出内容,因为完整的内容非常长。
T4 Output:
A10G Output:
好了,现在事情变得越来越有趣了。T4和A10G上的概率值(probabilities)并不完全一致。一般情况下,这样并不会影响tokens的排序序列(无法在生成的tokens序列中察觉到任何不同),但有时候确实会造成影响。
例如,在T4模型中,“trust”出现的概率为18.74%,而在A10G上,“be”出现的概率则更高,达到了18.62%。从这一点来看,由于大语言模型的自回归特性,生成的内容将会出现偏差(diverge)。
注释:量化大语言模型会降低计算精度(calculation precision),导致这类差异变得更为常见。
现在,一个非常合理的问题就出现了:“为什么计算结果会因为GPU的不同而产生差异呢?”
为什么GPU不同,模型运算结果也不同?
虽然作者不是CUDA expert(译者注:这类人能够熟练使用CUDA C/C++编程语言来开发高性能的并行计算应用,并了解如何优化GPU上的计算任务来获得最佳性能。),但作者进行过一些研究。不同GPU之间的计算差异可以归因于以下几个因素:
并行计算处理(Parallel Computation Handling)
GPUs的特点是能够高效地并行处理大量的计算任务。然而,不同GPU在管理这些并行任务时可能会有所差异,从而影响到运算顺序以及内存的访问方式。
这一点非常重要,因为在编程过程中,即使是数值大小相差很大的简单加法也可能是non-associative,从而影响到精确计算(precise calculations)的准确性。所谓“Non-associativity”是指:(a + b) + c ≠ a + (b + c)。
因此,计算任务会被分割开来,独立进行处理,然后以non-associative方式组合在一起。因此,这些部分的内容如何重新组合会影响到最终结果。
这里有一个关于non-associative computation的简单示例:
对于大语言模型(LLMs),数百万次的计算可能会因为重复出现的微小误差而导致出现偏差(diverge),进而影响到序列生成过程中的字词选择。
硬件架构(Hardware Architecture)
不同型号的GPU,如Nvidia Tesla T4和Nvidia A10G,具备不同的硬件架构。这些硬件架构能够优化模型各个方面的性能,包括并行处理能力(parallel processing capabilities)、内存带宽(memory bandwidth)和计算单元(compute units)。
例如,T4模型采用了Turing架构,而A10G模型基于Ampere架构。
不同的模型架构意味着在浮点运算(floating-point arithmetic)、内存访问模式(memory access patterns)和其他底层操作上有着不同的实现方式。即使这些实现方式(implementations)存在细微差别,也可能会导致计算结果出现差异。
例如,与针对更高计算精度而进行优化的模型架构相比,为了计算速度而优化的模型架构可能会产生不同的模型响应,即便两者都在执行相同的浮点运算。
模型量化的影响(Quantization Effects)
通过模型量化(Quantizing)来降低计算精度可以节省内存资源和计算资源,但这样做也会引入额外的误差源(sources of error)。这些误差的影响因GPU对低精度运算(lower precision arithmetic)的处理方式不同而不同。
由于模型量化(quantization)过程中涉及到对数值的近似处理,因此不同的GPU在处理这些近似值时可能会有所差异,从而最终会导致token的预测概率产生变化。
使用多个GPU水平扩展LLMs时需要注意什么?
这个问题问得非常好,非常感谢!: )
如果只是简单地增加相同型号的GPU数量(例如,从单个A10G GPU扩展到拥有4个A10G GPU的实例),是否还有必要担心?
使用多个GPU进行推理时,有几种策略可供选择:
第一种策略是,如果模型可以装入GPU中,可以在每个GPU上加载一份模型副本。例如,如果向pipeline发送四条查询语句(queries),每条查询语句(queries)可以由不同的GPU来处理。这样,将得到与仅使用一个GPU时相同的输出内容,但吞吐量会有所提高。
第二种策略通常用于因为模型太大一个GPU无法装入的情况,可以采用模型分片策略(sharding),将模型的权重分布到各个GPU上。虽然从理论上讲,这种做法可能会因为计算(computation)的分布(distribution)和执行(execution)的不同而导致模型响应产生变化,但在实践测试中,使用模型切片技术得到的序列(sequences)和概率(probabilities)与单个GPU上得到的结果是一致的。作者猜测这是因为PyTorch在设计时考虑到了deterministic operations(译者注:那些每次在相同输入下都能产生相同输出的操作过程。)。
Conclusion
已经证明了,即便是相同的开发环境(environment)、系统配置(settings)和随机种子(seed),不同的GPU也会导致LLMs产生不同的结果。随着提示词长度的增长,这种不准确性(inaccuracies)也会随之增加,因为更长的提示词需要更多的算力,这会加剧不准确性(inaccuracies)的传播并促进两个GPU之间的差异。此外,在进行模型量化的情况下,这种效应更加显著。
作者并不是说这种情况一定是灾难性的,但这是我们在处理LLMs的部署时需要注意的一个因素。
如果我们开发时使用的GPU与生产环境中使用的GPU不同,应该设置测试实验确保性能仍然保持在可接受的范围内。如果我们计划将LLMs扩展到拥有不同GPU的新实例上,这一点也很重要。