写点什么

相同的 LLM 在「不同 GPU 上」会产生不同输出?为什么?

作者:Baihai IDP
  • 2024-08-09
    湖南
  • 本文字数:5631 字

    阅读完需:约 18 分钟

相同的 LLM 在「不同 GPU 上」会产生不同输出?为什么?

编者按: 在大语言模型(LLMs)的部署及其相关的算力扩容过程中,更换 GPU 是否也可能会对模型的输出产生重大影响?这个问题的答案对于确保 LLMs 在不同硬件环境下的一致性和可靠性至关重要。

我们今天为大家带来的这篇文章,作者的核心观点是:即使在相同的开发环境、系统配置和随机种子下,不同的 GPU 也会导致 LLMs 产生不同的模型输出。

作者通过实验证明,在使用 Nvidia Tesla T4 和 Nvidia A10G 两种不同 GPU 的情况下,Mistral-7b-v0.1 模型对相同的输入产生了不同的输出。这种差异主要源于 GPU 在并行计算处理、硬件架构和模型量化的影响等方面的不同。随着提示词长度的增加,这种不准确性会被放大,因为更长的提示词需要进行更多计算,从而加剧了不准确性的传播。在使用多个 GPU 扩展时,如果采用模型分片策略,理论上可能会因计算分布的不同而导致结果产生变化,但实践中 PyTorch 的设计似乎保证了结果的一致性。


作者 | Anis Zakari


编译 | 岳扬


大多数技术工程师都了解,依赖库或依赖组件的版本不同都可能会导致系统行为产生变化。但在大语言模型(Large Language Models)领域,由于算力需求巨大,在训练和推理任务中我们都极度依赖 GPU。然而,很少有人真正意识到,更换 GPU 也会对 LLMs 的输出产生影响。


假如你想创建两个完全一致的开发环境:


  • 可以指定依赖库或组件的版本。

  • 可以使用 Dockerization。

  • 可以将 LLMs 的 temperature 设置为 0。

  • 可以选择任意的随机种子。 但是,如果使用的不是完全相同的 GPU 型号,以上所有努力都将白费。


本文将进行一次实验来强调这一现象,说明差异出现的位置及其原因。


Note:如果对实验过程的重现或具体代码不感兴趣,可以跳过本文展示代码片段,直接阅读“7. 为什么同样的 inputs 和同样的 LLMs 在两块不同 GPU 上生成的模型响应会有如此大的差别?”这部分内容。即便不看前面的代码片段,Conclusion 部分仍然有助于我们理解其中的原理。

01 为什么要写这篇文章?

有一天,我和一些人讨论为什么 OpenAI 和 Anthropic 的那些模型在设计时没有被构建为确定性的系统。我解释说,它们可能采用了混合专家模型(Mixture of Experts, MoE)方法<sup>[1]</sup>,偶尔不会将 tokens 路由给最优的专家模型,因为这些专家模型可能正忙于处理其他 tokens,所以可能会导致模型响应的不一致。


另一个因素可能是 OpenAI 为了提高效率而对 queries 进行了批量处理。batches size 会根据传入的 queries 数量而变化,可能会改变 GPU 的计算策略,从而导致不同的模型响应。


当有人指出,“不同的 GPU 也可能导致出现不同的模型响应,不是吗?”时,我们之间的对话开始变得耐人寻味起来了。


仔细想一想……当我们使用 OpenAI API 时,实际上是有一台远程服务器帮我们执行计算并返回模型响应。现在,如果这台机器并非总是在相同的算力基础设施上运行,那么最终得到的模型响应就不会相同。


想到这一点,可能就会出现其他问题:


  • 如果有一个在生产开发环境中运行的 LLM app,并且需要将其扩展到拥有不同 GPU 的其他实例,是否会出现很严重的问题?

  • 如果开发环境(development environment)中的 GPU 与生产环境(production environment)存在大量不同之处,会怎么样?


这些问题促使我想设置一个实验来突出这一现象,并探究它可能造成的影响有多大。

02 配置实验环境

为了突出这一现象,我将设置两个完全相同的开发环境,它们唯一的区别在于其所使用的 GPU:第一个开发环境中使用的是 Nvidia Tesla T4,第二个开发环境使用的便是 Nvidia A10G。然后,我们将使用 Mistral-7b-v0.1 进行测试,看看会发生什么。


要在 notebook 中运行实验,请按照以下步骤操作。

2.1 配置开发环境(Setup the environment)

1. 配置 CUDA 版本


2. 配置 transformers 和其他依赖


3. 设置随机种子(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)正确地生成并输出了这个单词,我们就可以继续进行实验了。

03 实验结果: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…”

04 T4 Colab vs T4 SageMaker

想知道使用相同 GPU 的两个开发环境是否会产生相同的模型输出?我进行了一系列测试,结果确实完全相同。

05 为什么相同的用户输入(inputs)和相同的 LLMs 在两个 GPUs 上生成的答案会如此不同?

最终,这些模型响应因为 LLMs 的自回归特性而变得截然不同。由于下一个 token 是根据之前的 tokens 选择的,任何细微的变化都会引发一连串的连锁反应,就像蝴蝶效应(butterfly effect)一样。


请注意,这些模型响应并没有像提示词中所要求的那样基于所提供的上下文。LLMs 并没有完全遵循指导性提示词(instructions),但这并不是很重要。


因为我们假设 LLMs 总是基于前面的 tokens 选择概率(probabilities)最高的 token,所以我们可以肯定,区别在于如何在 GPU 上计算该概率(probabilities),下面让我们来看一看如何计算该概率~

06 计算 tokens 的选择概率(probabilities)

为了打印出每个被选中 token 的概率,我们将绕过常规处理流程(pipeline),直接使用 tokenizer 和 model.generate 方法。这样我们就能设置 return_dict_in_generate=Trueoutput_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 的不同而产生差异呢?”

07 为什么 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<sup>[2]</sup> 架构,而 A10G 模型基于 Ampere<sup>[3]</sup> 架构。


不同的模型架构意味着在浮点运算(floating-point arithmetic)、内存访问模式(memory access patterns)和其他底层操作上有着不同的实现方式。即使这些实现方式(implementations)存在细微差别,也可能会导致计算结果出现差异。


例如,与针对更高计算精度而进行优化的模型架构相比,为了计算速度而优化的模型架构可能会产生不同的模型响应,即便两者都在执行相同的浮点运算。


模型量化的影响(Quantization Effects):


通过模型量化(Quantizing)来降低计算精度可以节省内存资源和计算资源,但这样做也会引入额外的误差源(sources of error)。这些误差的影响因 GPU 对低精度运算(lower precision arithmetic)的处理方式不同而不同。


由于模型量化(quantization)过程中涉及到对数值的近似处理,因此不同的 GPU 在处理这些近似值时可能会有所差异,从而最终会导致 token 的预测概率产生变化。

08 使用多个 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(译者注:那些每次在相同输入下都能产生相同输出的操作过程。)。

09 Conclusion

我们已经证明了,即便是相同的开发环境(environment)、系统配置(settings)和随机种子(seed),不同的 GPU 也会导致 LLMs 产生不同的结果。随着提示词长度的增长,这种不准确性(inaccuracies)也会随之增加,因为更长的提示词需要更多的算力,这会加剧不准确性(inaccuracies)的传播并促进两个 GPU 之间的差异。此外,在进行模型量化的情况下,这种效应更加显著。


我并不是说这种情况一定是灾难性的,但这是我们在处理 LLMs 的部署时需要注意的一个因素。


如果我们开发时使用的 GPU 与生产环境中使用的 GPU 不同,应该设置测试实验确保性能仍然保持在可接受的范围内。如果我们计划将 LLMs 扩展到拥有不同 GPU 的新实例上,这一点也很重要。


如果你坚持读到了最后,那我可太高兴了,希望你会喜欢这篇文章。如果你喜欢的话,希望你能给我点赞,鼓励我继续写作,也欢迎在评论区中分享你的想法。




Anis Zakari


I’m a passionate ML/AI Engineer based in Paris. I am particularly interested in NLP, LLM, Software Engineering and Cloud Engineering subjects

文中链接

[1]https://standardscaler.com/2024/03/06/the-non-determinism-of-openai-and-anthropic-models/


[2]https://www.nvidia.com/fr-fr/geforce/turing/


[3]https://www.nvidia.com/fr-fr/data-center/ampere-architecture/


原文链接:


https://medium.com/@cpdough/building-ai-agents-lessons-learned-over-the-past-year-41dc4725d8e5

发布于: 刚刚阅读数: 4
用户头像

Baihai IDP

关注

AI训推云平台:GPUaaS, MLOPs, MaaS 2021-08-31 加入

IDP是AI训推云平台,旨在为企业和机构提供算力资源、模型构建与模型应用于一体的平台解决方案,帮助企业高效快速构建专属AI及大模型。 在这里见证IDP的蜕变成长,共探行业和AI技术的迭代发展!

评论

发布
暂无评论
相同的 LLM 在「不同 GPU 上」会产生不同输出?为什么?_AI_Baihai IDP_InfoQ写作社区