大纲
在这篇博文中,我们将展示如何优化基于 LibTorch 的推理引擎,通过减少内存使用和优化线程池策略来最大化吞吐量。我们将这些优化应用于音频数据的模式识别引擎,例如音乐和语音识别或声学指纹识别。这篇博文中讨论的优化可以将内存使用量减少 50%,并将推理的端到端延迟减少 37.5%。这些优化适用于计算机视觉和自然语言处理。
音频识别推理
音频识别 (AR) 引擎可用于识别声音模式。例如,从录音中识别鸟类的类型和物种、区分音乐和歌手的声音,或检测指示建筑物违规的异常声音。为了识别感兴趣的声音,AR 引擎通过 4 个阶段处理音频
- 文件验证:AR 引擎验证输入音频文件。
- 特征提取:从音频文件中的每个片段中提取特征。
- 推理:LibTorch 使用 CPU 或加速器执行推理。在我们的例子中,是 Elastic Cloud Compute (EC2) 实例上的 Intel 处理器。
- 后处理:后处理模型解码结果并计算分数,这些分数用于将推理输出转换为标签或转录。
在这 4 个步骤中,推理是计算量最大的步骤,根据模型的复杂性,可能占用高达 50% 的管道处理时间。这意味着此阶段的任何优化都会对整个管道产生重大影响。
使用并发优化音频识别引擎……并非如此简单
我们此处理管道的目标是通过处理将音频片段提取为标签或转录。输入数据是包含多个短声音片段(图 1 中的 S1 到 S6)的音频文件。输出数据对应于按时间戳排序的标签或转录。
图 1:带有片段边界的音频文件示例
每个片段都可以独立且无序地处理。这为并发和并行处理片段提供了机会,以优化整体推理吞吐量并最大化资源利用率。
实例上的并行化可以通过多线程(pThreads、std::threads、OpenMP)或多进程来实现。多线程优于多进程的优势在于能够使用共享内存。它使开发人员能够通过跨线程共享数据来最大限度地减少线程之间的数据重复;在我们的例子中是 AR 模型(图 2)。此外,减少内存使我们能够通过增加引擎线程的数量来并行运行更多管道,以便利用我们的 Amazon EC2 实例上的所有 vCPU(在我们的例子中为 c5.4xlarge,它提供 16 个 vCPU)。从理论上讲,我们预计会看到更高的硬件利用率和更高的 AR 引擎吞吐量。
图 2:多线程 AR 引擎
但我们发现这些假设是错误的。事实上,我们发现增加应用程序的线程数会导致每个音频片段的端到端延迟增加,并导致引擎吞吐量下降。例如,将并发数从 1 个线程增加到 5 个线程导致延迟增加了 4 倍,这对吞吐量的降低产生了成比例的影响。事实上,指标表明,在管道中,仅推理阶段的延迟就比单线程基线高 3 倍。
使用分析器,我们发现 CPU 自旋时间 增加,这可能是由于 CPU 过度订阅导致的,这会影响系统和应用程序性能。鉴于我们可以控制应用程序的多线程实现,我们选择深入研究堆栈,并识别与 LibTorch 默认设置的潜在冲突。
深入了解 LibTorch 的多线程及其对并发性的影响
LibTorch 在 CPU 上用于推理的并行实现基于 全局线程池。实现的示例包括 Inter-op 和 intra-op 并行性,可以根据模型的属性选择。在这两种情况下,都可以设置 每个线程池中的线程数,以优化延迟和吞吐量。
为了测试 LibTorch 的并行默认实现设置是否对我们的推理延迟产生反作用,我们在 16 个 vCPU 的机器上使用 35 分钟的音频文件进行了实验,将 LibTorch 线程间线程数保持恒定为 1(因为我们的模型没有利用 inter-op 线程池)。我们收集了以下数据,如图 3 和图 4 所示。
图 3:不同引擎线程数下的 CPU 利用率
图 4:不同引擎线程数下的处理时间
图 4 中的执行时间是处理给定音频文件所有片段的端到端处理时间。我们有 4 种不同的 LibTorch 线程内配置,分别是 1、4、8、16,并且我们针对每种线程内 LibTorch 配置将引擎线程数从 1 更改为 16。正如我们在图 3 中看到的那样,对于所有 LibTorch 线程内配置,CPU 利用率都随着引擎线程数的增加而增加。但正如我们在图 4 中看到的那样,CPU 利用率的增加并没有转化为更低的执行时间。我们发现,除了一个例外,在所有情况下,随着引擎线程数的增加,执行时间也随之增加。唯一的例外是线程内线程池大小为 1 的情况。
解决全局线程池问题
使用过多线程和全局线程池会导致性能下降并导致过度订阅问题。如果不禁用 LibTorch 全局线程池,则很难与多进程引擎的性能相媲美。
禁用 LibTorch 全局线程池非常简单,只需将 intra-op/inter-op 并行线程设置为 1,如下所示
at::set_num_threads(1) // Disables the intraop thread pool.
at::set_num_interop_threads(1). // Disables the interop thread pool.
如图 4 所示,当禁用 LibTorch 全局线程池时,测得的处理时间最低。
此解决方案在多种情况下提高了 AR 引擎的吞吐量。但是,当评估长数据集(负载测试中超过 2 小时的音频文件)时,我们发现引擎的内存占用开始逐渐增加。
优化内存使用
我们对系统进行了两个小时音频文件的负载测试,发现观察到的内存增加是多线程 LibTorch 推理中内存碎片的结果。我们使用 jemalloc 解决了这个问题,jemalloc 是一种通用的 malloc(3) 实现,它强调避免碎片和可扩展的并发支持。 使用 jemalloc,我们的峰值内存使用量平均减少了 34%,平均内存使用量减少了 53%。
图 5:使用相同输入文件,有无 jemalloc 的内存使用量随时间变化
总结
为了优化基于多线程 LibTorch 的推理引擎的性能,我们建议验证 LibTorch 中是否存在过度订阅问题。在我们的例子中,多线程引擎中的所有线程都共享 LibTorch 全局线程池,这导致了过度订阅问题。通过禁用全局线程池解决了这个问题:我们通过将线程设置为 1 来禁用了 interop 和 intraop 全局线程池。为了优化多线程引擎的内存,我们建议使用 Jemalloc 作为内存分配器工具,而不是默认的 malloc 函数。