使用 DALL 生成的图像。D-R型
一、说明
想象一下:你终于被安排在一个很酷的新ML项目中,你正在训练你的经纪人计算一张照片中有多少只热狗,它的成功可能会让你的公司赚几十美元!
你会得到最新的热门对象检测模型,在你最喜欢的框架中实现,这个框架有很多GitHub明星,运行一些玩具示例,一个小时左右后,它就会像大学第三年复读的破产学生一样挑选出热狗,生活很好。
接下来的步骤是显而易见的,我们希望将其扩展到一些更难的问题,这意味着更多的数据,更大的模型,当然还有更长的训练时间。现在,您看到的是几天的培训,而不是几个小时。不过没关系,你已经忽略了团队的其他成员 3 周了,可能应该花一天时间处理积压的代码审查和已经积累起来的被动攻击性电子邮件。
一天后,你对你留给同事MR的有见地和绝对必要的吹毛求疵感到满意,结果发现在15个小时的训练后,你的表现一落千丈(因果报应很快)。
接下来的日子变成了试验、测试和实验的旋风,每个潜在的想法都需要一天以上的时间才能运行。这些很快就会开始累积数百美元的计算成本,所有这些都导致了一个大问题:我们如何才能让它更快、更便宜?
欢迎来到 ML 优化的情绪过山车!这里有一个简单的 4 步过程,可以扭转局面:
- 基准
- 简化
- 优化
- 重复
这是一个迭代过程,在继续下一个步骤之前,你会重复很多次,所以它不是一个 4 步系统,而更像是一个工具箱,但 4 步听起来更好。
二、 基准
“测量两次,切割一次”——有人明智。
您应该始终做的第一件事(也可能是第二件事)是分析您的系统。这可以很简单,比如只计时运行特定代码块所需的时间,也可以像执行完整的配置文件跟踪一样复杂。重要的是您有足够的信息来识别系统中的瓶颈。我根据我们所处的阶段进行多个基准测试,通常将其分为 2 种类型:高级基准测试和低级别基准测试。
2.1 高水平
这就是你将在每周的“我们有多?”会议上向你的老板展示的东西,并希望这些指标成为每次运行的一部分。这些将使您对系统运行的性能有一个高层次的了解。
每秒批次数 — 我们处理每个批次的速度有多快?这应该尽可能高
每秒步数 — (特定于 RL)我们在环境中生成数据的速度应该尽可能高。步骤时间和训练批次之间存在一些复杂的相互作用,我不会在这里讨论。
GPU 利用率 — 训练期间使用了多少 GPU?这应该始终接近 100%,如果不是,那么您有可以优化的空闲时间。
CPU 利用率 — 训练期间使用了多少 CPU?同样,这应该尽可能接近 100%。
FLOPS — 每秒浮点运算次数,可让您了解使用整个硬件的效率。
2.2 低电平
使用上述指标,您可以开始更深入地了解瓶颈可能在哪里。一旦你有了这些,你就要开始查看更细粒度的指标和分析。
时间剖析 — 这是运行的最简单且通常最有用的实验。像 cprofiler 这样的分析工具可用于鸟瞰整个每个组件的时序,也可以查看特定组件的时序。
内存分析 — 优化工具箱的另一个主要功能。大型系统需要大量内存,因此我们必须确保不会浪费任何内存!像memory-profiler这样的工具将帮助你缩小系统占用RAM的范围。
模型分析 — 像 Tensorboard 这样的工具附带了出色的分析工具,用于查看模型中哪些因素正在消耗您的性能。
网络分析 — 网络负载是造成系统瓶颈的常见罪魁祸首。有像 wireshark 这样的工具可以帮助您分析这一点,但老实说,我从不使用它。相反,我更喜欢对我的组件进行时间分析,并测量它在我的组件中花费的总时间,然后隔离来自网络 I/O 本身的时间。
请务必查看这篇来自 RealPython 的 Python 分析的精彩文章,了解更多信息!
三、简化
在分析中确定需要优化的区域后,请对其进行简化。剪掉除该部分以外的其他所有内容。不断将系统缩小到更小的部件,直到达到瓶颈。不要害怕在简化时进行分析,这将确保你在迭代时朝着正确的方向前进。不断重复此操作,直到找到瓶颈。
3.1 技巧
- 将其他组件替换为仅提供预期数据的存根和模拟函数。
- 使用函数或虚拟计算模拟繁重的函数。
sleep
- 使用虚拟数据来消除数据生成和处理的开销。
- 在迁移到分布式系统之前,从系统的本地单进程版本开始。
- 在一台机器上模拟多个节点和参与者,以消除网络开销。
- 找到系统每个部分的理论最大性能。如果系统中除此组件外的所有其他瓶颈都消失了,那么我们的预期性能如何?
- 再次剖析!每次简化系统时,请重新运行分析。
3.2 问题
一旦我们解决了瓶颈问题,我们就要回答一些关键问题
该组件的理论最大性能是多少?
如果我们已经充分隔离了瓶颈组件,那么我们应该能够回答这个问题。
我们离最大还有多远?
这种最优性差距将告诉我们我们的系统是如何优化的。现在,一旦我们将组件引入系统,可能会有其他硬约束,这很好,但至少要意识到差距是什么,这一点至关重要。
是否存在更深层次的瓶颈?
总是问自己这个问题,也许问题比你最初想象的要深,在这种情况下,我们重复基准测试和简化的过程。
四、优化
好的,假设我们已经确定了最大的瓶颈,现在我们进入有趣的部分,我们如何改进事情?我们通常应该考虑 3 个方面来寻找可能的改进
- 计算
- 通信
- 记忆
4.1 计算
为了减少计算瓶颈,我们需要考虑尽可能高效地使用我们正在使用的数据和算法。这显然是特定于项目的,可以做很多事情,但让我们看看一些好的经验法则。
并行化 — 确保并行执行尽可能多的工作。这是设计系统时的第一个重大胜利,可以极大地影响性能。看看矢量化、批处理、多线程和多处理等方法。
缓存 — 尽可能预计算和重用计算。许多算法可以利用重用预先计算的值,并为每个训练步骤保存关键计算。
卸载——我们都知道 Python 并不以速度着称。幸运的是,我们可以将关键计算卸载到C/C++等较低级别的语言。
硬件扩展 — 这是一种逃避,但当所有其他方法都失败时,我们总是可以投入更多的计算机来解决问题!
4.2 通信
任何有经验的工程师都会告诉你,沟通是交付成功项目的关键,这当然是指我们系统内的沟通(上帝保佑我们不得不与同事交谈)。一些好的经验法则是:
无空闲时间 — 必须始终使用所有可用的硬件,否则性能提升将一事无成。这通常是由于整个系统的通信的复杂性和开销。
保持本地 — 在迁移到分布式系统之前,尽可能长时间地将所有内容保留在一台计算机上。这样可以使系统保持简单,并避免分布式系统的通信开销。
异步>同步 — 确定可以异步完成的任何操作,这将有助于在移动数据的同时保持工作正常进行,从而减轻通信成本。
避免移动数据 — 将数据从 CPU 移动到 GPU 或从一个进程移动到另一个进程的成本很高!尽可能少地执行此操作,或者通过异步执行来减少此操作的影响。
4.3 记忆
最后但并非最不重要的一点是内存。上面提到的许多方面都有助于缓解瓶颈,但如果您没有可用的内存,这可能是不可能的!让我们看一些需要考虑的事项。
数据类型 — 保持这些类型尽可能小,有助于降低通信和内存成本,并且使用现代加速器,它还将减少计算。
缓存 — 与减少计算类似,智能缓存可以帮助您节省内存。但是,请确保缓存数据的使用频率足以证明缓存的合理性。
预分配 — 这不是我们在 Python 中习惯的东西,但严格预分配内存意味着你确切地知道你需要多少内存,降低碎片的风险,如果你能够写入共享内存,你将减少进程之间的通信!
垃圾回收 — 幸运的是,python 为我们处理了大部分问题,但重要的是要确保你不会在不需要它们的情况下将大值保留在范围内,或者更糟的是,具有可能导致内存泄漏的循环依赖关系。
懒惰 — 仅在必要时计算表达式。在 Python 中,您可以使用生成器表达式而不是列表推导式来执行可以延迟计算的操作。
五、重复
那么,我们什么时候完成呢?嗯,这真的取决于你的项目,要求是什么,以及你日益减少的理智最终需要多长时间才能被打破!
当您消除瓶颈时,您将获得用于优化系统的时间和精力的回报递减。当你经历这个过程时,你需要决定什么时候足够好。请记住,速度是达到目的的手段,不要陷入为了优化而优化的陷阱。如果它不会对用户产生影响,那么可能是时候继续前进了。
六、结论
构建大规模的ML系统是很困难的。这就像玩一个扭曲的“沃尔多在哪里”与黑暗之魂交叉的游戏。如果你设法找到了问题,你必须多次尝试才能解决它,你最终会把大部分时间都花在被踢屁股上,问自己“我为什么要在周五晚上做这件事?拥有一个简单而有原则的方法可以帮助你度过最后的 Boss 战,并品尝那些甜蜜、甜蜜的理论最大 FLOP。
多纳尔·伯恩