减少 55% 代码行数、 CPU 和 GPU 峰值内存,加速训练推理!

关键词:机器学习系统;软件膨胀;Negativa-ML;共享库;GPU 代码

软件膨胀(Software bloat)是指软件在运行时未被使用的代码和功能。对于机器学习(ML)系统而言软件膨胀是造成其技术债务的主要因素,会导致性能下降和资源浪费

在这项工作中,我们提出了 Negativa-ML,这是一种通过分析机器学习框架的共享库识别和消除软件膨胀的新工具

我们的方法包含了检测和定位 GPU 代码不必要代码的创新技术,而 GPU 代码是现有研究忽略的关键领域,我们使用四个流行的机器学习框架,在 10 个工作负载上对 300 多个共享库进行了 Negativa-ML 评估。

结果表明,机器学习框架在 GPU 和 CPU 代码方面都存在严重的膨胀问题,其中  GPU 代码是机器学习框架中软件膨胀的主要来源。平均而言,Negativa-ML 可将:

  • GPU 代码大小减少多达 75%
  • CPU 代码减少多达 72%
  • 总文件大小减少多达 55%

通过去膨胀处理,我们分别实现了 CPU 峰值内存使用量、GPU 峰值内存使用量和执行时间最多减少 74.6%、69.6% 和 44.6%。

image.png

图片

图片

一、引言

从个性化推荐(Ko 等人,2022 年)到医疗诊断(Bhardwaj 等人,2017 年),再到自动驾驶汽车(Parekh 等人,2022 年),机器学习正在彻底改变几乎每个工业领域。除了实现自然语言理解和内容生成的大语言模型(LLMs)(OpenAI 团队,2024 年;Touvron 等人,2023 年 b),较小的模型现在也广泛应用于许多场景,从机器人和自动驾驶汽车到摄像头和移动电话。随着这些模型在数量、规模和复杂性方面的增长,管理和优化机器学习系统变得越来越具有挑战性。

  • 大语言模型通常包含数十亿个参数,需要大量的计算资源和海量的训练数据。
  • 另一方面,较小的模型通常部署在资源受限的环境中。

为了管理这种复杂性,机器学习系统引入了大量的技术开销,加剧了这些系统中软件膨胀的问题(Zhang 等人,2024 年)。

软件膨胀是指程序在运行时不必要的代码,通常是由不影响核心功能的额外函数、库或功能引起的。软件膨胀会引发一系列问题,包括性能下降、资源使用增加和安全漏洞。

虽然软件膨胀可能影响任何类型的软件,但机器学习系统特别容易受到影响,因为它们不仅存在传统代码的所有维护问题,还存在一系列特定于机器学习的问题,如边界侵蚀、数据依赖等等(Sculley 等人,2015 年)。随着机器学习模型规模和复杂性的增长,机器学习系统中的软件膨胀也在增加,导致了额外的低效率,阻碍了运行时性能,并增加了部署成本,尤其是在资源受限的环境中(Zhang 等人,2024 年;Jiang 等人,2020 年)。

机器学习系统的核心是机器学习框架,如 TensorFlow(Abadi 等人,2016 年)和 PyTorch(Paszke 等人,2019 年)。这些框架为模型训练和推理提供了必要的库和工具。机器学习框架通常使用多种编程语言开发,其中 C++ 和 CUDA 用于实现核心功能,以最大化性能。C++ 和 CUDA 代码都被编译成共享库。Python 作为前端,封装这些核心功能,并提高了框架对开发者的易用性。然而,随着这些框架带来的易用性和灵活性,它们也引入了 “框架税”(Fernandez 等人,2023 年)—— 这些框架会带来性能开销,并削弱新硬件和模型架构进步带来的好处。此外,集成这些框架还会导致二进制文件变大,并给较小的 GPU 带来不必要的开销(Jiang 等人,2020 年)。

本文中,我们旨在通过去膨胀(debloating,即去除软件膨胀部分)来识别和减少机器学习框架中的软件膨胀,具体是在机器学习共享库中进行。共享库封装了机器学习框架的核心功能,并构成了机器学习框架大小的大部分。这些库的大小可能从几百兆字节到几吉字节不等。对机器学习共享库进行去膨胀面临以下挑战

  • 机器学习框架依赖一些专有库,如 cuDNN(nvi,b)和 cuBLAS(nvi,c)。这些库不是开源的,因此无法进行源代码分析
  • 机器学习框架包含在 CPU 和 GPU 上运行的代码。现有研究忽略了 GPU 代码,并且没有方法来测量和减少其中的软件膨胀
  • 虽然 CPU 代码具有明确的结构,并且之前已经有研究对其中的软件膨胀进行了探讨(Brown 等人,2024 年),但  GPU 代码的结构并未公开,这使得分析和去膨胀变得困难

为了解决这些挑战,我们提出了 Negativa-ML,这是一种用于识别和去除机器学习共享库中 CPU 和 GPU 代码中软件膨胀的工具。利用对机器学习系统如何在 CPU 和 GPU 上执行机器学习工作负载的深入理解,我们提出了一种新方法,以低性能开销检测机器学习工作负载使用的 GPU 代码。随后,我们在机器学习共享库中定位这些 GPU 代码所占据的文件范围。这需要深入了解 GPU 代码的编译方式以及它们在共享库中的组织方式,由于 GPU 代码结构缺乏公开规范,这具有挑战性。最后,我们利用之前开发的去膨胀工具(Zhang 和 Ali-Eldin,2025 年),根据定位的文件范围从机器学习共享库中去除软件膨胀。我们的贡献如下:

  • 我们提出了一种以低性能开销检测机器学习工作负载使用的 GPU 代码的方法
  • 我们分析了机器学习共享库中 GPU 代码的结构,并提出了一种定位使用的 GPU 代码文件范围的方法。
  • 我们扩展了之前开发的去膨胀工具(Zhang 和 Ali-Eldin,2025 年),以去除机器学习共享库中 CPU 和 GPU 代码的软件膨胀。
  • 我们在四个机器学习框架、十个机器学习工作负载、三个模型和 300 多个共享库上对 Negativa-ML 进行了评估,并对结果进行了深入分析。我们的评估展示了机器学习框架中软件膨胀的程度、原因以及软件膨胀带来的开销。

我们的实验评估表明:

  1. 机器学习框架存在严重的软件膨胀,72% 的 CPU 代码和 75% 的 GPU 代码对于目标机器学习工作负载来说是不必要的**。
  2. 10% 的共享库占了机器学习框架总软件膨胀的 90%。这种软件膨胀不仅增加了存储开销,还会随着 CPU 内存使用量、GPU 内存使用量的增加和执行时间的延长而降低运行时性能。

二、背景与相关工作

本节介绍机器学习框架、机器学习共享库、软件膨胀和去膨胀的相关背景知识。

2.1 机器学习框架

机器学习框架是提供构建、训练和部署机器学习模型所需基本工具的软件库。一些机器学习框架是通用的,旨在支持各种机器学习模型的训练和推理。而其他一些则针对特定用例进行了优化,如大语言模型推理。两个最受欢迎的通用机器学习框架是 TensorFlow(Abadi 等人,2016)和 PyTorch(Paszke 等人,2019),它们在工业界和学术界都被广泛采用。尽管这两个框架适用于各种机器学习模型,但大语言模型的兴起对机器学习框架提出了新的要求,如高效的键值缓存管理(Kwon 等人,2023;Sinha 等人,2024)。大语言模型推理对延迟高度敏感且资源需求大,极具挑战性。为了满足这些需求,人们提出了专门为大语言模型推理设计的新框架(LMDeploy Contributors,2023;Kwon 等人,2023;Wolf 等人,2020;hug)。

机器学习框架的核心功能被打包成共享库。共享库是一个包含可在不同程序间共享的机器码的文件。共享库占机器学习框架总大小的大部分。例如,在 PyTorch 和 TensorFlow 中,共享库分别占框架总大小的 93%和 75%。

共享库的标准文件格式是可执行和可链接格式(ELF)(ora),它将文件组织成各个部分,如.text.data。一般的共享库在.text部分只包含在 CPU 上运行的代码。然而,机器学习共享库的独特之处在于它们还包含设计用于在 GPU 上运行的代码。在机器学习共享库中,这种 GPU 代码通常包含在另一个名为.nv fatbin的部分中。

图片

与传统共享库相比,GPU 代码(也称为设备代码)显著增加了机器学习共享库的大小。

PyTorch 的核心共享库torch.soGPU 版本大小为 881MB,而 CPU 版本为 482MB,几乎是 CPU 版本的两倍。此外,GPU 代码在这些库中占很大比例。分析 PyTorch 中四个最大的共享库,如图 1 所示,GPU 代码占每个共享库大小的 68%到超过 91%之间。相比之下,CPU 代码(也称为主机代码)在这些库中只占一小部分。尽管 GPU 代码构成了机器学习共享库的大部分,但现有研究尚未对 GPU 代码中的膨胀问题进行研究。

2.2 软件膨胀

软件膨胀主要是由软件中不必要的代码引起的,可分为两种类型(Brown 等人,2024):

  • I 型膨胀是普遍不必要的,例如死代码和不可达代码
  • II 型膨胀取决于最终用途,代码是否属于 II 型膨胀取决于具体用例,例如不必要的程序功能可能被使用该共享库的其他程序使用。

软件膨胀存在于整个现代软件栈中,从操作系统(Quach 等人,2017;Kuo 等人,2020)、共享库(Ziegler 等人,2019;Quach 等人,2018;Biswas 等人,2021),到可执行文件(Navas 和 Gehani,2023;Ahmad 等人,2021;Qian 等人,2019),甚至容器化应用程序(Rastogi 等人,2017;Zhang 等人,2024)。

软件膨胀会导致软件随着时间推移在大小和复杂性上增加,从而导致性能下降、内存消耗增加、启动时间变长和安全漏洞增多,却没有任何益处。最近,机器学习系统中的软件膨胀问题越来越受到关注。Sculley 等人(Sculley 等人,2015)指出,由于一系列特定于机器学习的问题,机器学习系统容易积累隐藏的技术债务。Zhang 等人(Zhang 等人,2024)表明,容器化的机器学习应用程序存在严重的膨胀问题,浪费存储资源和网络带宽,增加攻击面,并减缓部署过程。

2.3 软件去膨胀

软件去膨胀是从软件中去除不必要代码以提高效率和减少资源消耗的过程。已经为传统软件开发了许多去膨胀工具,可根据其去膨胀目标进行分类:源代码、二进制代码和容器化应用程序。

  • 源代码去膨胀工具根据特定的使用场景,直接从源代码中消除未使用的代码,如死代码或不可达代码(Brown 和 Pande,2019;Ye 等人,2021;Azad 等人,2019)。
  • 二进制去膨胀工具对软件二进制文件(包括共享库和可执行文件)进行操作,以去除不必要的函数或指令(Qian 等人,2019;Ahmad 等人,2021;Ziegler 等人,2019;Agadakos 等人,2019)。
  • 容器去膨胀工具针对容器化应用程序,去除容器镜像中的不必要文件(Rastogi 等人,2017;Zhang 等人,2023)。

这些工具大多去除特定工作负载未使用的代码,即 II 型膨胀(Qian 等人,2019;Ahmad 等人,2021;Brown 和 Pande,2019;Rastogi 等人,2017;Azad 等人,2019)。

虽然对传统软件的去膨胀研究已经很广泛,但据我们所知,所有现有工作都只关注代码仅在 CPU 上运行的传统应用程序。在机器学习框架中占很大比例的 GPU 代码仍未得到研究。

专注于二进制去膨胀的工具的一个主要问题是它们通常不可靠(Brown 等人,2024)。为了解决其可靠性问题,我们最近开发了 Negativa,这是一个仅对 CPU 代码进行去膨胀的工具(Zhang 和 Ali-Eldin,2025)。

Negativa 在对共享库进行去膨胀方面证明了其有效性,它对使用目标共享库的工作负载进行分析,然后去除工作负载未使用的任何代码。Negativa 的去膨胀过程包括三个阶段:检测、定位和压缩。

  • 在检测阶段,它识别工作负载使用的 CPU 函数;
  • 在定位阶段,它在共享库中定位使用的 CPU 函数;
  • 在压缩阶段,它从共享库中删除未使用的函数,只保留使用的函数。

这项工作中,我们扩展了 Negativa,引入了 Negativa-ML,这是我们开发的一种能够对 GPU 代码进行去膨胀的技术。此外,我们基于 Negativa 的功能,对机器学习共享库中的 CPU 代码也进行去膨胀。基于 Negativa 的三阶段去膨胀过程,我们在检测和定位阶段引入了检测和定位使用的 GPU 代码的新方法。然后,Negativa 的压缩阶段被重新用于去除未使用的 GPU 代码。

三、方法

图 2 展示了 Negativa-ML 的概述。

图片

Negativa-ML 由两个组件组成:内核检测器和内核定位器。在执行机器学习工作负载期间,会使用机器学习框架的许多共享库。

  • 内核检测器监控工作负载的内核执行情况,并记录使用的内核名称。
  • 内核定位器分析共享库,提取其中的 GPU 代码,并在共享库中找到使用的内核的文件范围。

随后,这些范围被传递给 Negativa 的压缩模块进行去膨胀。最后,生成一个减少了 CPU 代码和 GPU 代码的去膨胀后的机器学习共享库。

3.1 内核检测器

内核检测器负责检测机器学习工作负载使用的内核。虽然像 Nsight Systems(NSys)(nvi,f)这样的现有工具可以对 GPU 代码进行分析,但它们主要是为在运行时分析和调试 GPU 性能而设计的,并非用于内核检测。

为了提供全面的信息,这些工具通常会在每次调用内核时记录数据,这给目标应用程序带来了很高的性能开销。然而,对于内核检测,我们只关心确定某个内核是否被使用,不需要重复的调用数据,因为这会导致不必要的开销。

为了提供更高效的解决方案,我们提出了一种新颖的轻量级方法,专门用于检测机器学习共享库中使用的内核,实现低性能开销。由于内核在 GPU 上执行,一种直观的方法是直接监控 GPU 执行情况。然而,这种方法不可行,因为无法直接访问 GPU 上的代码执行。

我们的方法利用了对机器学习系统如何在 CPU 和 GPU 上执行机器学习工作负载的理解。考虑到 CPU 和 GPU 之间的交互,首先,CPU 启动一个或多个内核,然后由 CPU 启动的内核可能会也可能不会启动其他内核。由另一个内核启动的内核称为 GPU 启动内核。这些内核形成一个内核调用图,图的起始点是 CPU 启动内核。内核检测器只检测 CPU 启动内核。通过监控 CPU 代码,我们可以识别从 CPU 启动的内核,并将它们视为“已使用”。这使我们能够在不需要 GPU 级监控的情况下检测已使用的内核

为了从 CPU 启动一个内核,CUDA 驱动程序必须首先调用 CPU 函数cuModuleGetFunction。该函数将要启动的内核名称作为输入之一。它返回一个函数句柄,用于执行内核。

此外,无论内核执行多少次,cuModuleGetFunction对每个内核只调用一次,这使其非常适合我们对已使用内核的检测。受这一观察的启发,我们使用 Nvidia CUPTI API(nvi,d)对cuModuleGetFunction进行挂钩(hook,一种在程序运行时改变程序行为的技术,这里指插入代码获取内核名称)。

这个挂钩记录传递给该函数的内核名称,这些内核随后被视为已使用的内核。内核检测器只拦截每个 CPU 启动内核一次,不拦截 GPU 启动内核。因此,其性能开销低于像 NSys 这样的分析工具。

图片

图 3 展示了内核检测器工作流程的一个示例。当一个机器学习工作负载调用matmul内核时,它首先调用位于 CPU 代码中的cuModuleGetFunction来启动该内核。内核检测器拦截对cuModuleGetFunction的调用,并记录内核名称,在这个例子中标记为已使用的内核matmul。这个内核可能会在 GPU 代码中启动其他内核并形成一个内核调用图。然而,这些 GPU 启动内核不会被内核检测器检测到。

内核检测器输出一个 CPU 启动内核的名称列表。这些内核被视为已使用的内核,并被传递给内核定位器进行进一步处理。下一节将介绍,GPU 启动内核也由内核定位器处理。

3.2 内核定位器

内核定位器定位机器学习共享库中使用的内核的文件范围(起始和结束文件偏移量)。它识别出共享库中需要保留的文件范围列表。

在  GPU 代码中定位内核的困难在于,GPU 代码的结构没有公开规范。找到使用的内核的确切位置需要对 GPU 代码进行深入分析,这可能会过度依赖特定版本的 CUDA 工具包,并且容易出错。此外,由于内核检测器不会检测 GPU 启动内核,内核定位器必须妥善处理这些内核,以避免误删。

为了解决这些问题,我们提出了一种在 GPU 代码中大致定位使用的内核的方法。我们不是寻找内核的确切位置,而是找到包含内核的 cubin 文件(CUDA 二进制文件,包含内核代码)的位置。如果一个内核由另一个内核启动,这两个内核会被编译到同一个 cubin 文件中(nvi,e)。

基于这一观察,我们可以推断,如果一个 cubin 文件包含一个 CPU 启动内核,它也包含从该 CPU 启动内核开始的内核调用图中的所有内核,包括那些 GPU 启动内核。因此,保留包含 CPU 启动内核的 cubin 文件,也会保留从该 CPU 启动内核开始的内核调用图中的所有内核,包括调用图中的 GPU 启动内核。

为了确定一个 cubin 文件是否包含 CPU 启动内核,我们使用cuobjdump工具(nvi,a)从共享库中提取 cubin 文件列表。对于每个 cubin 文件,我们使用cuobjdump提取其中包含的内核。如果一个 cubin 文件包含一个已使用的 CPU 启动内核,则整个 cubin 文件需要保留。这样做的同时,我们也保留了 cubin 文件中的 GPU 启动内核。下一步是在共享库文件中定位 cubin 文件,即找到 cubin 文件占用的文件范围。这需要了解共享库中 GPU 代码的结构。如图 4 所示,共享库中的 GPU 代码被组织成一个区域列表。每个区域包括一个区域头和一个元素列表。每个元素包括一个元素头和一个 cubin 文件。通过cuobjdump提取的 cubin 文件在其文件名中具有从 1 开始的索引。这个索引等于共享库中包含该 cubin 文件的元素的索引。利用这个索引,我们可以将 cubin 文件映射到共享库中的相应元素。这样,我们就可以定位 cubin 文件占用的文件范围。

图片

为了保持共享库的完整性,内核定位器保留或删除包含 cubin 文件的整个元素。最后,内核定位器使用以下标准来确定是否保留一个元素:元素头有一个名为compute-capability的字段,它显示了 cubin 文件编译所针对的 GPU 架构。我们发现只有与 GPU 架构匹配的元素才能加载到 GPU 内存中。因此,如果一个元素与机器学习工作负载正在运行的 GPU 架构匹配,并且包含一个有已使用 CPU 启动内核的 cubin 文件,那么我们保留该元素。

图 4 还展示了共享库中内核、cubin 文件和元素之间关系的一个示例。被内核检测器检测到的已使用内核matmul包含在图中所示的一个 cubin 文件中。该内核可能会启动其他内核,形成一个内核调用图。内核调用图中的所有内核都包含在同一个 cubin 文件中。而这个 cubin 文件又包含在共享库的一个元素中。通过保留整个元素,我们确保了内核调用图中的所有内核都被保留,包括调用图中的 GPU 启动内核。

压缩:满足标准的元素所占用的文件范围会保留在机器学习共享库中,其余部分则被删除。这个过程由 Negativa 的压缩阶段处理。在这个阶段,未使用的文件被清零。然后,Negativa 将文件偏移量映射到原始共享库加载到内存时的原始内存地址,以保持内存地址的有效性。关于压缩过程的更多细节可以在(Zhang 和 Ali-Eldin,2025)中找到。

四、实验

我们使用四个机器学习框架评估 Negativa-ML 的去膨胀效果:两个通用机器学习框架 PyTorch 和 TensorFlow,因为它们应用广泛;还有两个大语言模型推理框架 vLLM(Kwon 等人,2023 年)和 Transformers(Wolf 等人,2020 年),因为它们具有最先进的性能。使用这些框架,我们运行各种机器学习工作负载,包括不同流行模型的训练或推理,以识别每个工作负载中不必要的代码。

图片

表 1 展示了工作负载的详细信息。对于机器学习模型,我们选择了三个不同规模的模型:

  • 一个小型模型 MobileNetV2(Sandler 等人,2018 年),它是一个拥有 430 万个参数的计算机视觉模型;
  • 一个中型模型 Transformer(Vaswani,2017 年),它是一个拥有 6500 万个参数的自然语言处理模型;
  • 还有一个大型模型 Llama-2-7b-chat-hf(简称为 Llama2)(Touvron 等人,2023 年 a),它是一个拥有 70 亿个参数的大语言模型。

总共使用四个机器学习框架执行了 10 个工作负载。我们没有将模型训练至收敛,因为我们的主要目标是评估框架中的膨胀情况,而不是对模型进行完整训练。由于训练主要涉及重复迭代,训练几个 epoch(训练过程中数据集完整遍历一次称为一个 epoch)就足以获得具有代表性的结果。

表 1 中所有工作负载均在一个 AWS 实例上运行,该实例配备 16 个 CPU、64GB 内存和一个 NVIDIA T4 GPU。对于 vLLM 和 Transformers 框架,我们还使用 8×A100 GPU 评估了另外 9 个大语言模型,以便在不同的硬件设置下进一步评估膨胀情况。

4.1 机器学习框架中膨胀情况概述

我们运行表 1 中列出的工作负载,使用 Negativa-ML 对工作负载使用的共享库进行去膨胀处理。去膨胀后,我们使用去膨胀后的共享库重新运行工作负载,以验证去膨胀的正确性。具体来说,我们首先比较原始工作负载和去膨胀后工作负载的输出,确认它们基本相同。然后,我们比较原始工作负载和去膨胀后工作负载的最终指标,如测试损失、验证损失或生成文本。我们发现输出和最终指标没有差异,这表明去膨胀过程不会影响工作负载的正确性。由于篇幅原因,我们省略了这些比较。

然后,我们比较去膨胀前后共享库的总文件大小,以评估文件大小的减少情况。此外,对于 CPU 代码,我们比较去膨胀前后的代码大小和函数数量。对于 GPU 代码,我们还分析代码大小的减少情况和删除的元素数量。结果如表 2 所示

图片

对于小型模型 MobileNetV2,使用 PyTorch 进行训练涉及 113 个共享库,总大小为 3762MB。去膨胀后,总文件大小减少了 55%。值得注意的是,CPU 代码大小减少了 68%,93%的函数被删除。GPU 代码大小减少了 75%,98%的元素被删除。GPU 代码在总文件大小和文件大小减少量中也占主导地位。使用 PyTorch 对 MobileNetV2 进行推理也显示出类似的减少情况。

对于相同的 MobileNetV2 模型,与 PyTorch 相比,TensorFlow 使用的共享库更多,训练时使用 253 个库,推理时使用 251 个库。总文件大小和减少量比 PyTorch 小。TensorFlow 中的 CPU 代码呈现出有趣的结果:它的大小和函数数量都比 PyTorch 大,但减少幅度较小,这表明 TensorFlow 比 PyTorch 使用了更多的 CPU 代码和函数。鉴于 PyTorch 可以用更少的 CPU 代码和函数训练(推理)相同的模型,这表明 TensorFlow 的 CPU 代码中可能存在不必要的函数调用,即那些被使用但对目标机器学习工作负载没有实际贡献的函数。TensorFlow 中的 GPU 代码在大小和元素数量上也有显著减少,超过 99%的元素被删除

对于中型模型 Transformer,PyTorch 和 TensorFlow 都显示出与 MobileNetV2 类似的结果,这表明不同的模型对机器学习框架中的膨胀情况影响不大。对于大型模型 Llama2,使用 vLLM 和 Transformers 框架进行推理,尽管框架不同,但文件大小、CPU 代码和 GPU 代码的减少仍然很显著。

在所有机器学习框架中,GPU 代码的大小都比 CPU 代码大得多。在所有框架中,GPU 代码在大小和元素数量上的减少幅度也更大。特别是,在所有工作负载中,GPU 代码中元素数量的减少超过 97%,这突出表明 GPU 代码比 CPU 代码膨胀得多

总结:所有机器学习框架在 CPU 代码(≥46%)和 GPU 代码(≥66%)方面都有显著减少。GPU 代码的膨胀程度明显高于 CPU 代码,是机器学习框架中膨胀的主要来源。

4.2 共享库层面的分析

在本节中,我们更深入地研究共享库层面 CPU 和 GPU 代码中膨胀的分布情况。对于工作负载中使用的每个共享库,我们计算 CPU 代码大小减少的百分比函数数量减少的百分比。同样,对于每个共享库,我们也计算 GPU 代码大小减少的百分比和元素数量减少的百分比。如果一个共享库没有 GPU 代码,我们将其从 GPU 代码分析中排除。

图片

  • 图 5a 展示了 CPU 和 GPU 代码大小减少分布的小提琴图。CPU 和 GPU 代码的分布模式显示出明显差异。**CPU 代码大小减少的中位数约为 25%,许多共享库的减少幅度在 0%到 10%之间。相比之下,GPU 代码大小减少的中位数几乎为 80%**,明显高于 CPU 代码,而且其分布也更集中。
  • 图 5b 描绘了函数和元素数量减少的分布情况。值得注意的是,所有共享库的元素减少幅度都超过 80%。GPU 代码大小和元素数量减少的集中分布突出表明,在所有共享库中,GPU 代码比 CPU 代码膨胀得多

图片

对于每个工作负载,我们按绝对文件大小减少量对共享库进行降序排序。我们发现,对于所有机器学习框架,前 10%的共享库贡献了总大小减少量的 90%以上。CPU 代码大小和 GPU 代码大小的减少也遵循类似的模式。例如,如图 6 所示,PyTorch 训练 MobileNetV2 工作负载的帕累托图显示,在 113 个共享库中,前 8 个库占总文件大小减少量的 90%。这一结果表明,膨胀遵循幂律分布,少数共享库包含了大部分膨胀。

总结:机器学习框架中 50%的共享库 GPU 代码减少幅度超过 80%。10%的共享库贡献了总大小减少量的 90%以上。
4.3 函数和元素层面的分析

为了更深入地分析,表 3 展示了每个工作负载中使用的最大共享库的减少情况

图片

运行不同模型时,所有四个机器学习框架使用的最大共享库为torch_cuda.sotensorflow_cc.so二者之一。这两个提供机器学习框架核心功能的库,在文件大小、GPU 代码和 CPU 代码方面也有显著减少。

  • 对于torch_cuda.so,文件大小、CPU 代码大小和 GPU 代码大小的减少幅度分别为 76%、91%和 82%。
  • tensorflow_cc.so中的 CPU 代码也呈现出我们在 4.1 节中讨论的有趣结果:它的 CPU 代码大小和函数数量都比torch_cuda.so大得多,但减少幅度更小。tensorflow_cc.so中 GPU 代码的减少幅度与torch_cuda.so一样显著。

在表中,torch_cuda.so被三个机器学习框架 PyTorch、vLLM 和 Transformers 使用,涉及 6 个工作负载。我们收集每个工作负载在torch_cuda.so中使用的函数,以比较它们的相似性。由于 vLLM 使用的torch_cuda.so版本不同,我们将其从分析中排除。

因此,我们总共收集了五个不同工作负载使用的五组函数对于每对函数集,我们根据以下公式计算它们的杰卡德相似度(Jaccard Similarity,用于衡量两个集合的相似程度,值为 1 表示两个集合完全相同,值为 0 表示两个集合没有交集):

其中 A 和 B 是两个工作负载使用的两组函数。我们也对五个工作负载使用的内核计算了杰卡德相似度。结果如表 4 所示。我们对tensorflow_cc.so也进行了相同的分析,结果类似,见表 9。

图片

图片

表 4 左下角显示了每对工作负载之间内核的相似度。相似度相当低,这表明不同工作负载使用的内核差异很大。然而,如表 4 右上角所示,每对工作负载之间函数的相似度非常高。所有对的相似度都在 0.7 以上,这表明即使工作负载使用不同的模型和框架,它们也使用了许多相同的函数

考虑到在五个工作负载中,每个工作负载实际使用的torch_cuda.so库中的函数不到 10%,这表明一小部分函数就足以运行各种工作负载。

我们还研究了 GPU 代码中删除元素的原因。Negativa-ML 删除一个元素的原因有以下两种:

  • 原因一,元素与 GPU 架构不匹配;
  • 原因二,元素与 GPU 架构匹配,但没有任何使用的内核。

图片

图 7 显示,对于所有工作负载,超过 80%的删除元素是由于原因一,即元素与 GPU 架构不匹配。这些不匹配元素的原因是,这些共享库中的 GPU 代码是为支持各种 GPU 架构而构建的,导致在特定 GPU 上运行工作负载时存在许多不必要的元素。例如,在我们的实验中,我们观察到 PyTorch 框架中的一个共享库包含针对 6 种不同 GPU 架构的元素。如果该库部署在特定的 GPU 架构上,那么针对其他五种架构的元素就是不必要的。这暗示了膨胀的一个新原因:软件膨胀可能源于硬件。

总结:不同工作负载在核心共享库中使用了许多相同的函数。GPU 代码中删除的大多数元素是由于 GPU 架构不匹配。
4.4 运行时性能分析

在本节中,我们评估去膨胀后运行时性能的改进情况。最初,每个工作负载使用原始共享库执行 10 次。然后,基于之前的发现,即少数共享库贡献了大部分膨胀,我们用去膨胀后的版本替换前 8 个最大的共享库,并重新运行工作负载 10 次。然后,我们比较两次运行的平均运行时性能。表 5 总结了使用去膨胀后的共享库实现的平均运行时性能改进

图片

使用去膨胀后的库运行工作负载减少了内存使用(包括 CPU 和 GPU)和执行时间。在 PyTorch 推理 MobileNetV2 工作负载中观察到最好的改进效果,CPU 峰值内存减少了 74.6%,GPU 峰值内存减少了 69.6%,执行时间减少了 44.6%。推理工作负载通常比训练工作负载显示出更好的改进效果。在所有工作负载中,CPU 峰值内存的平均绝对减少量为 2501MB,GPU 峰值内存为 443MB,执行时间为 2.6 秒。值得注意的是,无论实际执行时间长短,绝对执行时间减少量都相对恒定。这种改进源于代码大小的减少,这减少了将代码加载到内存所需的时间。对于对冷启动延迟敏感的任务,如无服务器机器学习应用程序,这种执行时间的改进尤其有意义

总结:对共享库进行去膨胀显著减少了 CPU 和 GPU 的内存使用。将代码加载到内存所需的时间也减少了,从而缩短了执行时间。

4.5 不同 GPU 上的评估

在本节中,我们展示了使用不同 GPU 对两个大语言模型推理框架 vLLM 和 Transformers 进行去膨胀的结果。我们首先在单个 H100 GPU 上评估 vLLM/Transformers 对 Llama2 的推理工作负载。评估在 eager-loading(在应用程序启动时将所有内核加载到内存中)和 lazy-loading(仅在需要时加载内核,以减少内存占用)两种模式下进行。

图片

表 6 展示了实现的大小减少情况。对于相同的框架,两种加载模式下文件大小、CPU 代码大小、函数数量和 GPU 代码大小的减少情况相似。

图片

表 7 展示了运行时性能的改进情况。在 eager-loading 模式下,去膨胀导致的 CPU 内存减少比 lazy-loading 模式更大,而两种模式下 GPU 内存减少情况相似。两种加载模式下执行时间都有所减少。将这些结果与使用 T4 GPU 获得的结果(表 2 和表 5)进行比较,我们观察到在不同 GPU 上都有一致的减少。这表明 Negativa-ML 在各种 GPU 架构上都能有效地对共享库进行去膨胀

接下来,我们展示了使用 vLLM 和 Transformers 对其他机器学习工作负载进行去膨胀的结果。我们从 Hugging Face Open LLM Leaderboard(Beeching 等人,2023 年)中选择前 9 个大语言模型,并使用 8×A100 40GB GPU 进行分布式推理,将它们部署在这两个框架上。此设置旨在评估在不同硬件设置下分布式推理的去膨胀效果。结果详见下面附录中的表 10

图片

使用 8 个 GPU 进行分布式推理的去膨胀结果与单 GPU 推理的结果同样显著。对于 8 个 GPU 的推理,vLLM 使用的共享库比单 GPU 推理的结果略少,而 Transformers 使用的库数量几乎相同。文件大小、CPU 代码大小、函数数量和 GPU 代码大小的减少情况与使用单 GPU 的结果非常接近。然而,GPU 代码中元素数量的减少幅度比单 GPU 推理时要低,这表明分布式推理在 GPU 代码中使用了更多的内核。此外,不同模型的减少指标相似,再次表明不同模型对机器学习框架中的膨胀情况影响不大。

总结:Negativa-ML 在不同 GPU 和分布式推理设置下都能有效地对机器学习共享库进行去膨胀。
4.6 Negativa-ML 的性能

在本节中,我们从对共享库进行去膨胀所需时间的角度评估 Negativa-ML 的性能。我们首先评估端到端时间,即从运行目标机器学习工作负载到获得去膨胀后的共享库所需的时间。然后,我们专门评估内核检测器给目标机器学习工作负载带来的开销。

图片

表 8 显示了 Negativa-ML 对每个工作负载的共享库进行去膨胀所需的端到端时间。时间因三个因素而异:工作负载的原始执行时间、涉及的共享库数量以及工作负载使用的函数(内核)数量。我们注意到,这些时间是对工作负载中的所有库进行去膨胀所需的时间。如果只对单个库进行去膨胀,所需时间将显著缩短。此外,去膨胀过程是一次性开销,可以在例如部署前的预处理阶段进行。

为了评估内核检测器对目标机器学习工作负载带来的性能开销,我们首先运行 PyTorch 训练 MobileNetV2 工作负载 10 次,并记录平均执行时间。接下来,我们再次运行相同的工作负载,这次分别启用内核检测器和 NSys 的跟踪功能,各运行 10 次。然后,我们比较三种设置下的平均执行时间。原始工作负载的平均执行时间为 180 秒。启用内核检测器后,执行时间增加到 253 秒,开销为 41%。相比之下,使用 NSys 进行跟踪使执行时间增加到 407 秒,开销为 126%。内核检测器带来的开销明显低于 NSys,使其成为检测已使用内核的更实用选择,特别是对于像机器学习训练这样的长时间运行的工作负载。

总结:去膨胀是一次性开销,可以在部署前的预处理阶段进行。内核检测器在首次运行时带来 41%的开销,明显低于传统跟踪工具(如 NSys)带来的 126%的开销。

五、讨论

正如 Sculley 等人(Sculley 等人,2015 年)所强调的,由于一系列机器学习特有的问题,机器学习系统存在隐藏的技术债务。我们的工作通过揭示机器学习框架中隐藏的膨胀问题,进一步凸显了这种技术债务。软件膨胀在软件行业中是一个长期存在的问题。虽然现有的去膨胀研究主要集中在传统软件上,但机器学习系统中的膨胀问题却没有得到充分的理解。机器学习系统的独特之处在于它们同时包含 CPU 和 GPU 代码,这导致了严重的膨胀。我们对四个机器学习框架、十个工作负载以及约 300 个共享库的评估表明,这些框架中存在显著的膨胀,CPU 代码的大小最多可减少 72%,GPU 代码最多可减少 75%。此外,与传统软件不同,机器学习框架的 GPU 代码膨胀更为严重。所有这些膨胀不仅导致存储需求增加、内存使用量上升,还会延长执行时间。

在我们的评估中,只有一小部分代码被持续使用。这表明,一个工作负载未使用的代码很可能对其他工作负载也是不必要的。此外,现有研究仅关注未使用的代码,却忽略了 “已使用的膨胀(used bloat)”,即工作负载执行了但对性能或功能没有实际贡献的代码。例如,在使用特定优化器训练模型时,该优化器可能会通过大量非必要的函数调用来初始化上下文。与 PyTorch 相比,TensorFlow 的 CPU 代码规模更大,但减少的幅度却更小,这可能表明其中存在 “已使用的膨胀”。这种 “已使用的膨胀” 尤其有害,因为它会被执行,从而消耗稀缺的内存、CPU 和 GPU 资源。而且,由于它实际上在被执行,所以更难被检测到。未来的研究可以聚焦于识别和消除这种 “已使用的膨胀”。

机器学习框架中的共享库比传统共享库大得多(Zhang 等人,2024 年),正如本文所示,这主要是由于 GPU 代码的存在。由于存储和网络带宽是边缘数据中心的关键瓶颈(Richins 等人,2021 年),通过去膨胀实现的文件大小减小有助于缓解这些瓶颈。

六、结论

我们提出了一种通过去除共享库中 CPU 和 GPU 代码中不必要代码来对机器学习框架进行去膨胀的新方法。

  • 我们的方法首先通过运行机器学习工作负载(如训练或推理模型)来检测 GPU 代码中使用的内核名称。
  • 然后,我们在共享库中定位已使用的内核,并删除未使用的代码。

我们基于现有的去膨胀工具实现了我们的方法,并在四个机器学习框架、十个工作负载以及 300 多个共享库上对其进行了评估。我们的评估表明,机器学习框架存在严重的膨胀,这些框架中的共享库大小最多可减少 55%。

我们发现,对于目标机器学习工作负载,高达 75%的 GPU 代码和 99%的 GPU 元素是不必要的高达 72%的 CPU 代码和 93%的 CPU 函数也是不必要的

这种膨胀不仅增加了存储开销,还降低了运行时性能。对这些框架进行去膨胀后,峰值内存使用量、峰值 GPU 内存使用量和启动时间分别最多可减少 74.6%、69.6%和 44.6%。

参考文献

图片

图片

END

作者:Huaifeng Zhang
来源:卡巴拉花园

推荐阅读

欢迎大家点赞留言,更多 Arm 技术文章动态请关注极术社区嵌入式AI专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
18966
内容数
1476
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息