[音乐]
大家好,感谢大家来到这里。嗯,是的,今天我要讲的是 Ray Actors、视觉 Token 以及 Gill(吉莉)。我们将探讨如何构建一个多模态监督微调数据管道,以保持 GPU 的高负载运行。关于优化训练范式有很多讨论,但我想从整个生态系统所需的角度提供一个不同的视角——即究竟需要做什么才能让模型完成训练。因此,我今天要提出的问题是:目前的训练循环是否真的依赖于 GPU?它是否在等待 GPU 处理完毕?通过接下来的实验,我们将发现优化多模态训练吞吐量的难点并不一定在于内核(kernel)的优化,而在于确保 GPU 能够持续不断地获得数据供应。
接下来我们将概述本次演讲的主要内容。首先,我们会建立一个基线多模态训练管道,然后识别出瓶颈所在,并逐一解决。这些瓶颈大致包括四个方面:一是并发能力,即在将图像输入模型之前处理单张图像的速度有多快;二是预取(Prefetching),即如何确保数据能够按需获取;三是如何在不同进程间最小化数据的复制次数;四是额外的框架优化,即通过微调各种细节来提升性能。
我们要设定的前提是:众所周知,GPU 的成本高昂,因此在使用时我们通常希望最大化其利用率。多模态处理与纯文本不同,因为在实际将数据喂给模型之前,需要大量的 CPU 密集型操作。例如,使用 Pillow 库加载、调整大小或归一化图像;对于音频,需要进行大量的加载和裁剪操作;可以使用 NumPy 数组处理数据;而视频则是上述两者的某种结合。
此外,通常训练集群中可用的 CPU 数量远多于 GPU,但在这个管道中真正“饥饿”(资源不足)的却是 GPU。今天我们将引入一个称为“权重时间比”的概念,我将其公式化为:准备数据并传输到 GPU 所需的时间除以总管道时间(包括数据传输和训练本身)。分母代表整个管道的总耗时。
在我们的运行过程中,我们将看到利用率从 15% 提升到 90%。在基线状态下,85% 的时间都花在数据获取上;而在进行这些优化后,这一比例降至不到 10%。因此,本次演讲的重点并非 GPU 本身的目标,而是确保数据永远不是瓶颈。
以下是我们技术栈的一些背景信息:我们大致采用基于 Qwen-3VL 的模型,尝试在 Chat VQA 和多模态视觉语言模型(VLM)对话的范式下进行训练。简而言之,在 GPU 进行任何数学计算之前,我们必须加载数据、转换数据,然后将数据传输到 GPU。
这是我们的实际数据结构概览:我们将使用 JSONL 文件,图像通过相对路径链接到对应的示例位置。假设数据存储在 S3 中。例如,在这个例子中,“image_33471.jpeg”似乎是一张蓝色巴士的照片。
这是我们的系统布局:左侧是数据生成器,负责数据的加载、转换和传输。这被标记为“扇出”到 Megatron 的数据并行等级(ranks)。并行等级接收由协调器决定的数据分区,对其进行整理(collate),并在这些数据上执行前向传播和后向传播。Megatron 负责优化数据并行等级之间的模型权重,我们将其视为一个黑盒模块来工作。
让我们从简单的基线路径开始:对于每个示例,我们从对象存储中获取图像;对文本进行分词(tokenize);调整图像大小并归一化,然后将其输入模型。我们可以看到红色部分表示串行操作,即逐个获取图像并逐个处理它们。如果训练器请求的批次大小(batch size)为 256,则必须串行处理所有这些图像。
我们可以看到训练时间大约在 15 到 20 秒之间,然而实际获取数据所花费的时间超过 100 秒。这就是为什么我们的 GPU 利用率仅为 15%。从性能分析火焰图中可以看出,我们大部分时间都花在 S3 加载上,而在末尾可以看到一个长尾现象,这与使用 Pillow 库实际加载图像有关。
解决方案一:让我们尝试增加一些并发性。与其进行那些红色的串行处理,不如通过工作池(worker pool)来并行化它。例如,当请求 256 个示例时,我们可以尝试一次性处理所有这些样本。
这里有两种成本:一是获取图像,二是进行处理(包括解码、视觉预处理以及文本分词)。样本之间是相互独立的,这意味着它们具有“ embarrassingly parallelizable”(极易并行化)的特性。
在此处有一个设计决策:你想要采用何种形式的并发?异步编程适合 I/O 密集型工作,如进行 SQL 查询或执行网络请求。线程可用于释放 Python 全局解释器锁(GIL)的 CPU 密集型工作负载,但这并不简单,因为并非所有操作都会释放锁,你需要进行更多调查。常见的用法包括使用 Hugging Face Transformers 的分词器、Pillow 的转换 API 或某些 Pandas 操作。
进程更重量级,但能为工作负载提供完全隔离,从而利用机器上的多个核心。深入探讨最佳解决方案足以单独做一次演讲,网上有丰富资源可帮助你根据用例做出决定。对我们的方案而言,重要的是要匹配瓶颈所在,而不是简单地沿用以前有效的杠杆手段。因此,我们将选择异步 I/O 来获取图像,然后使用……
我们将 Ray Actors 作为处理器,也就是我们的多处理库。Ray Actors 本质上与通用进程相同,但它们为我们提供了跨工作节点通信的 API,这使得我们无需重新发明轮子。因此,进程的速度取决于其最慢环节。我们可以看到在第二个示例(粉色部分)中,我们的权重时间与数据处理时间的比例下降了。你可以看到每个批次的数据获取时间大约为 40 秒,这为我们带来了 25% 的加速。然而,正如你所见,随着步骤的增加,生成阶段出现了巨大的差距。
我们能否以某种方式限制瓶颈?即在请求之前先预置工作节点池来获取数据,也就是预先获取我们的数据。为此我们提出了方案二:建立一个后台线程提前填满队列,并始终保持在消费者之前。这意味着生产者生成示例的速度是消费者的倍数,因此训练器在请求数据时永远不会遇到瓶颈。当它需要数据时,数据立即可用。
预取有一个注意事项:它使得模型和数据加载器的检查点保存及恢复过程变得复杂,并且可能会经历冷启动期,即队列正在填充以准备训练。但如果你能绕过这些细节,我认为预取是一个好主意。确保生产者始终领先于消费者将为你带来巨大的收益。
出乎意料的是,我们实现了每个批次接近 0 秒的数据获取和处理时间,因为我们在训练器甚至需要数据之前就已经预先获取并处理了数据。因此,我们有效地解决了数据加载和转换的瓶颈。经过这两项缓解措施后,我们认为任务已完成。吞吐量提升,GPU 得到充分利用。
然而,我们观察到数据传输成本依然存在,事实上甚至增加了。每个样本都会将大量图像数组跨进程边界多次传输。每个样本在工作节点上被序列化,复制到协调器,然后再反序列化到训练器中。每个样本、每一步都有一次无人要求的内存复制。
这就导致数据生成器经常溢出,因为多模态张量非常大,每个样本达数兆字节。默认情况下,每个样本按值返回,这意味着这些昂贵的数组会在进程间被复制。对此的修复方法是使用对象存储。这是一种方便的机制,每个样本携带一个指向数据的引用(指针)。
驱动程序只持有这个指针。张量数据不会往返于主节点。Megatron 的数据并行排名可以直接调用 Ray Store,在需要时获取数据。因此,对于每一种并行性和并发解决方案的选择,你都必须考虑由此增加的通信成本。要据此明智地规划。
我们能够消除预取解决方案中多余的“放置对象”调用,这是通过使用 Ray Store 实现的。我们可以看到通信成本下降,等待时间的比例也随之降低。回顾一下:我们从串行基线开始;添加了数据加载和处理的并发;添加预取以领先于训练请求;并添加了 Ray Store 以最小化数据传输。
我们将权重时间比例从基线的 85% 降低到了 20%。你可以看到,顶部图表中的锯齿状模式在很大程度上得到了缓解,这意味着我们的 GPU 利用率提高了。当我们扩展规模并拥有良好的多模态训练管道时,让我们尝试将其扩展到生产运行。
在此过程中,我们观察到对于相同的管道,权重时间比例上升了,尽管我们只是扩展了一些数字。我们增加了四倍的数据并行环,添加了约 200 个数据流,批次大小变为原来的四倍。结果发现,由于数据生成器和 worker 池为了快速数据传输而共置在一起,导致该节点的网卡成为瓶颈。
这意味着当我们将所有工作和处理打包到特定节点时,其网络吞吐量无法跟上训练管道的需求。此时我们对所使用的框架进行了一些调查,发现默认情况下 Ray 倾向于在同一节点上调度 worker 并共置它们。这意味着在移动到另一台机器之前,很容易使一台计算机成为瓶颈。
因此,我们考虑了一种将整个过程解耦的可能性,让 worker 分布在多个节点上。它们可以独立进行图像处理和存储,并以惰性即时方式被我们的数据并行环检索和使用。这是我们要调整的两个参数标志:一个是零拷贝获取,另一个是分散调度机制。
在小型实验中,这两个功能单独都没有帮助,但在大型实验中它们产生了协同效应。配置可能在小规模下表现不佳,但在大尺度下可能表现出色。因此,你应该始终考虑如何在不同的扩展层级上进行消融实验。正是这两个标志极大地提高了我们数据处理管道的速度。
现在,这是我们最终完成的管道:一个并行的、急切的、感知数据访问模式的系统。它共享训练负载,使 GPU 保持温暖状态,并提供训练的平滑性和稳定性。总体而言,仅凭这两个标志,我们在大规模下实现了 50% 的额外吞吐量。这是由瓶颈决定的同一旋钮的反面判决。
以下是我们从这次演讲中得到的见解:GPU 很昂贵,但有时最大的战斗发生在 GPU 之外。首先问自己:你是受 CPU 限制还是受 GPU 限制?是受 I/O 限制还是计算受限?解决一个瓶颈通常会暴露出另一个。首先是我们遇到的吞吐量问题,然后是权重时间比例。
随后,瓶颈变成了内存访问成本,接着又演变为检索开销。众所周知,经典优化手段并非终点所在。并行处理和预取技术至关重要,我认为已有大量内容对此进行了阐述。然而,你所使用的框架语义以及面临的约束条件同样关键,而且规模的变化总会改变最优解。
诸如零拷贝浪费等参数的调优、以及工作进程的分散,只有在更大规模下才变得重要。在较小层级上,这些优化可能不会表现为明显的性能提升。总体而言,对基线进行剖析有助于你推动指标的改善。
好了,感谢各位参加本次演讲。如果大家有任何问题,我很乐意为大家解答。谢谢。