欢迎来到机器学习优化的过山车!本文将通过4个简单的步骤介绍我优化任何机器学习系统以实现训练和推断的极速的过程。
想象一下:你终于参与了一个酷炫的机器学习项目,在其中你训练你的代理程序来统计一张照片中有多少个热狗,这可能会让你的公司赚到几十美元的成功!
你使用了你最喜欢的有很多GitHub星标的热门目标检测模型,并运行了一些玩具示例,大约一个小时后,它就像一个重读大三的穷学生一样精准地识别出了热狗,生活美满了。
接下来的步骤很明显,我们想要将其扩展到一些更难的问题,这意味着更多的数据,一个更大的模型,当然还有更长的训练时间。现在你要看的是数天的培训时间,而不是几个小时。不过没关系,你已经忽略了你的团队3周了,现在可能应该花一天时间处理积压的代码审查和带有间接攻击性的电子邮件。
一天后,你满怀对同事们合理且绝对必要的吹毛求疵的满意感回来,却发现性能崩溃了,在进行了15小时的训练之后(因果关系真快)。
接下来的几天变成了一场试验、测试和实验的旋风,每一个潜在的想法都需要花费超过一天的时间来运行。这些很快就积累了数百美元的计算成本,最终导致一个重要的问题:我们如何使这个过程更快、更便宜?
欢迎来到机器学习优化的情绪过山车!以下是一个简单的4步过程,可以改变局势:
- 基准测试
- 简化
- 优化
- 重复
这是一个迭代的过程,往往在进入下一步之前需要多次重复某些步骤,所以它不完全是一个4步系统,更像是一个工具箱,但是4个步骤听起来更好。
1 — 基准测试
“量力而行,量完之后动手”——聪明人。
首先(可能也是第二个)你应该做的事情是对你的系统进行性能测试。这可以是简单地计时某个具体代码块的运行时间,也可以是进行完整的性能分析。重要的是你有足够的信息来识别系统中的瓶颈。根据我们在处理过程中所做的工作,我进行了多次基准测试,并通常将其分解为两种类型:高级和低级基准测试。
高级
这是你在每周“我们有多惨?”例会上向老板展示的内容,你希望这些指标成为每次运行时的一部分。这些指标将让你对系统的性能有一个高级别的了解。
每秒处理批次数 — 我们有多快地处理每个批次?这个数字应尽可能高
每秒步数 —(强化学习特有)我们多快地在环境中进行步骤并生成数据?应尽可能高。这里有一些步长时间和训练批次之间复杂的相互作用,我不在这里详细介绍。
GPU利用率 — 训练过程中GPU的利用率有多高?应始终接近100%,如果不是,则表示有空闲时间可以进行优化。
CPU利用率 — 训练过程中CPU的利用率有多高?同样,应尽可能接近100%。
每秒浮点运算次数(FLOPS) — 这可以让你了解你如何有效地使用总体硬件。
低级
使用上述指标,你可以进一步深入分析你的性能瓶颈所在。一旦你获得了这些信息,你就可以开始查看更精细的指标和性能分析。
时间分析 — 这是最简单、也是最有用的测试实验。像cprofiler这样的分析工具可以用来从整体上或特定组件的时间角度查看每个组件的运行时间。
内存分析 – 优化工具箱中的另一项重要工具。大型系统需要大量内存,所以我们必须确保我们没有浪费任何内存!像memory-profiler这样的工具将帮助您缩小系统耗用内存的范围。
模型分析 – 像Tensorboard这样的工具提供了优秀的分析工具,可用于查看模型性能的具体情况。
网络分析 – 网络负载是拖慢系统的常见原因。有像wireshark这样的工具可帮助您进行分析,但老实说我从来不使用它。相反,我喜欢对组件进行时间分析,衡量组件内所需的总时间,然后分离出网络I/O本身所需的时间。
别忘了参考来自RealPython的有关Python性能分析的优秀文章,获取更多信息!
2 – 简化
一旦您确定了需要优化的分析领域,就进行简化处理。除了该部分外,剪除其他所有内容。逐步将系统缩小到更小的部分,直到找到瓶颈。在简化过程中进行分析,这将确保您朝着正确的方向迭代。反复重复此过程,直到找到瓶颈。
技巧
- 替换其他组件为仅提供期望数据的存根和模拟函数。
- 使用
sleep
函数或虚拟计算来模拟繁重的函数。 - 使用虚拟数据消除数据生成和处理的额外开销。
- 在转向分布式之前,从本地单进程版本开始。
- 在单台机器上模拟多个节点和角色以消除网络开销。
- 查找系统的每个部分的理论最大性能。假设除了这个部分之外,系统中的所有其他瓶颈都消失了,那么我们的预期性能是多少?
- 再次进行分析!每次简化系统时,重新运行分析。
问题
一旦我们确定了瓶颈,就有一些关键问题需要回答:
此组件的理论最大性能是多少?
如果我们已经成功将瓶颈组件隔离出来,那么我们应该能够回答这个问题。
与最大性能相差多远?
这个优化差距将告诉我们系统的优化程度。现在,可能情况是,一旦将组件重新引入系统,还会有其他硬约束条件,这也没有问题,但至少要了解差距是多少至关重要。
是否存在更深层次的瓶颈?
始终要自问这个问题,也许问题比您最初想象的更深层次,如果是这样,就要重复基准测试和简化的过程。
3 – 优化
好了,假设我们已经找到了最大的瓶颈,现在让我们来享受一下改进的乐趣!通常有3个方面我们应该寻找可能的改进点:
- 计算
- 通信
- 内存
计算
为了减少计算瓶颈,我们需要尽可能有效地处理数据和算法。这显然是项目特定的,可以做很多事情,但让我们看一下一些建议:
并行化 – 确保您尽可能地并行执行工作。这是设计系统的第一个关键步骤,可以极大地影响性能。考虑使用矢量化、批处理、多线程和多进程等方法。
缓存 – 在可能的情况下,对可以重复计算和重复使用的值进行预计算。许多算法可以利用重复使用预计算的值来为每个训练步骤节省关键计算。
卸载 – 我们都知道Python不以速度而闻名。幸运的是,我们可以将关键的计算任务卸载到低级语言如C/C++中。
硬件扩展 – 这种情况有点被动,但当其他方法都失败时,我们总可以通过增加更多计算机来解决问题!
沟通
任何经验丰富的工程师都会告诉你,沟通是交付成功项目的关键,当然,这指的是我们系统内部的沟通(万一不得不和同事交谈,那可真是天谴)。以下是一些好的原则:
无空闲时间 – 你所有可用的硬件都必须得到利用,否则你就浪费了性能的提升机会。这通常是由于系统内部通信的复杂性和开销造成的。
保持本地化 – 在转移到分布式系统之前,尽可能将所有内容保持在一台机器上。这既可以使系统简单,又可以避免分布式系统的通信开销。
异步 > 同步 – 找出可以以异步方式完成的任务,这将通过在数据移动的同时保持工作进行来减轻通信的开销。
避免数据移动 – 在CPU和GPU之间,或者在进程之间移动数据都是昂贵的!尽可能减少这种情况的发生,或者通过异步操作减轻其影响。
内存
最后是内存。上面提到的许多方面都有助于减轻瓶颈,但如果没有可用的内存,这可能是不可能的!让我们看看一些需要考虑的事情。
数据类型 – 将数据类型保持尽可能小,有助于减少通信和内存的开销,并且使用现代加速器时会减少计算量。
缓存 – 与减少计算类似,智能缓存可以帮助节省内存。但请确保缓存的数据经常被使用,以证明缓存的必要性。
预分配 – 这在Python中并不常见,但严格预分配内存意味着您确切知道所需的内存量,减少了碎片化风险,并且如果能够写入共享内存,将减少进程间的通信!
垃圾回收 – 幸运的是,Python已经为我们处理了大部分垃圾回收工作,但重要的是确保在不需要时不要保留大型值的作用域,或者更糟糕的是产生循环依赖,可能会导致内存泄漏。
偷懒 – 只在必要时评估表达式。在Python中,您可以使用生成器表达式而不是列表推导式对可以进行惰性评估的操作。
4 – 重复
那么,我们什么时候才会完成呢?嗯,这实际上取决于您的项目、需求以及在您的理智最终崩溃之前需要花费多长时间!
随着去除瓶颈,您在优化系统上投入的时间和努力会递减回报。在整个过程中,您需要决定什么是足够好。记住,速度只是达到目标的手段,不要困在只为进行优化而优化的陷阱中。如果对用户没有影响,那么可能是时候继续前进了。
总结
构建大规模的机器学习系统是困难的。这就像是玩一个扭曲的“哪儿藏着沃尔多”与“黑暗之魂”相结合的游戏。如果您设法找到问题,您将需要多次尝试才能克服它,并且您将大部分时间都被击败,问自己“为什么我在星期五晚上要做这个?”拥有简单而有原则的方法可以帮助您克服最后的Boss战,感受到那些甜蜜的、理论性的最高浮点运算速度。
ML中的行动 | Donal Byrne | Substack
该机器学习通讯提供未经请求的建议,实际见解和在快速学习中获得的经验教训……
donalbyrne.substack.com