[ggerganov/llama.cpp]简单的转换脚本在 Linux 上耗尽了 tmpfs 空间

2024-07-05 923 views
8

默认情况下,Linux 会阻止 tmpfs 使用超过 50% 的可用系统内存。这通常是一件好事,但简单的转换脚本会在保存输出文件之前将所有张量数据写入 tmpfs,如果转换后的模型大于系统 RAM 的 50%(ref),则会导致此异常:

Traceback (most recent call last):
  File "/home/cebtenzzre/src/forks/llama.cpp/convert-baichuan-hf-to-gguf.py", line 279, in <module>
    gguf_writer.add_tensor(new_name, data)
  File "/home/cebtenzzre/src/forks/llama.cpp/gguf-py/gguf/gguf.py", line 622, in add_tensor
    tensor.tofile(self.temp_file)
OSError: Not enough free space to write 140247040 bytes

这很烦人。您可以设置 TMPDIR=/var/tmp 来解决此问题,但您需要两倍于输出文件大小的可用磁盘空间 - 而我并不总是有这样的空间。

解决这个问题的最小改动是提供一种在 Linux 上有效禁用 use_temp_file 的方法,同时仍然支持使用 /var/tmp(如果需要)。这样,我就可以利用 100% 的 RAM 和交换空间来转换这些模型。如果我们选择这种方式,我们应该确保转换后的张量数据在写入时被释放,以避免不必要的交换——目前还没有。

我们不能仅仅改变简单的脚本来进行动态转换,因为它们一次只加载一个 pytorch 文件,因此对输入张量进行两次传递会产生很高的 I/O 成本。

我们可以将 LazyUnpickler 作为 gguf 模块的一部分。这样,我们可以显著减少内存使用量,避免将整个模型加载到内存中,同时希望非 LLaMA 转换脚本仍然相对简单。

@ggerganov 您觉得怎样?

回答

2

如果我正确读取了代码,那么该临时文件仅被写入,无需按顺序查找,直到它被倒回并复制到输出文件为止。

如果这是正确的,就 Linux 而言,使用临时文件进行“只写”工作负载实际上不应该有任何好处,因为内核缓存已经以类似的方式工作,并且最终写入文件仍然需要花费很长时间。

然而,如果临时文件确实提供了性能优势,那么其他一些东西一定是非最优的。

Pythonopen似乎允许指定缓冲区大小,但这并未使用gguf.py,并且根据文档,这意味着使用“默认”操作系统缓冲,其大小可能是以 kb 为单位。

我可以想象,指定缓冲区大小open(例如 100M)可能会允许摆脱临时文件。

2

我不能 100% 确定使用临时文件有什么好处。

使用临时文件的唯一原因是在 Linux 以外的平台(*BSD、macOS、Windows)上,这些平台上的临时文件夹通常不是 ramdisk。这允许您转换输出大小大于 RAM+交换空间的模型。但在典型的 Linux 安装中,除非您将 TMPDIR 设置为磁盘,否则 use_temp_file 没有任何好处。

0

我们可以将 LazyUnpickler 作为 gguf 模块的一部分。这样,我们可以显著减少内存使用量,避免将整个模型加载到内存中,同时希望非 LLaMA 转换脚本仍然相对简单。

这听起来是最好的选择,但我不确定实现起来有多难,以及我们是否可以实现一个足够简单的 API 以便在简单的转换脚本中重用它

3

有一种方法可以最大程度地避免临时文件和内存问题,那就是只numpy.memmap处理文件。PyTorch 文件是 ZIP 文件,但它们实际上从未压缩过数据,而只是存储数据。numpy 还支持将映射作为写时复制,因此您可以执行诸如排列张量数据之类的操作,而不会影响仍可以只读打开的底层文件。参考:https: //numpy.org/doc/stable/reference/generated/numpy.memmap.html

但是有一个限制,即在 32 位系统上只能映射最大 2GB 的文件。这可以接受吗?

8

但是有一个限制,即在 32 位系统上只能映射最大 2GB 的文件。这可以接受吗?

据我所知,32 位 Linux 上的 Python 是使用 64 位文件偏移量 ( -D_FILE_OFFSET_BITS=64) 构建的,因此限制实际上是 4GB。这也是 CPU 加载的实际限制,所以可能没问题。

编辑:无论如何,我们在 convert.py 中使用 mmap 来存储 safetensors 文件。

7

限制实际上是4GB。

numpy 参考资料明确memmap指出它仅支持 2GB,因此它们的包装器可能会增加一些额外的限制。(但如果有必要,也可以绕过这一点,而只使用 Python mmap。)

该函数的源代码在这里:https://github.com/numpy/numpy/blob/main/numpy/core/memmap.py - 如果你想看一看它是否有理由只支持 32 位上的 2GB,它很短。(看起来使用该函数的主要原因是因为它简化了使用偏移量,而偏移量mmap通常必须是页面大小的倍数。)

9

numpy.memmap文件

不幸的是,这种方法不适用于 convert.py,因为原始 LLaMA 张量被拆分到多个文件中,必须连接起来。而且我不希望在 convert.py 和 gguf 包中使用不同版本的 LazyUnpickler。

0

这种方法不适用于 convert.py,因为原始 LLaMA 张量被拆分到多个文件中,必须连接起来。

为什么不呢?就像我提到的那样,它可以是mmapCoW,因此对数据的操作没有任何限制。我唯一想到的是,可能有必要使该部分更懒惰(如果还没有的话),因为提前连接/排列所有张量基本上与将它们加载到内存中相同。

我不希望在 convert.py 和 gguf 包之间有不同版本的 LazyUnpickler。

当然,一个地方应该只有一个版本。但我不确定这与通过加载数据(mmap而不是从文件中读取数据)有什么关系。

5

LazyUnpickler 已经等到最后一秒才从磁盘加载张量数据 - 它构建一个“加载”函数链,然后在需要数据时调用它们。据我所知,mmap 不会更简单(它需要额外的努力来找到 zip 文件中的偏移量,而这不是 API 直接公开的)或在 convert.py 中节省大量内存使用量。

0

需要额外的努力才能找到 zip 文件内的偏移量,而 API 不会直接公开该偏移量)

这是事实,但这并不是太难,因为它确实暴露了本地文件头的偏移量(基本上你只需要找到名称长度和额外部分的长度,原始数据就会正好在那之后)。

或者据我所知,在 convert.py 中节省大量内存使用量。

那么,您就不会用尽临时空间,因为不再需要使用该方法了。对吗?

此外,在连接张量时,序列中第一个张量的数据不会被写入,因此它基本上与只读 mmap 相同。我不确定置换是否会触及每个元素。gguf 接口可能可以稍微改变一下,以支持获取需要保存的张量数组列表,这样就可能根本不需要将所有内容连接到一个大的 numpy 数组中。

5

convert.py 不使用 tmpfs,它直接调用 GGUFWriter.write_tensor_data。此问题涉及调用 GGUFWriter.add_tensor 的简单脚本。

0

我想知道是否有更好的临时解决方案是设置dir为模型目录,而不是$TMPDIR。我认为用户的 tmpfs 很可能无法容纳整个模型,而“正常”分区2*model size在悲观情况下更有可能适合。

4

Python 的临时文件处理功能应该尊重环境变量TMPDIR,所以我认为您需要做的就是在文档中添加一些内容,以使其更明显地表明将在默认临时目录中创建大文件,并解释用户如何根据需要进行更改。

1

我不确定您是否理解我的意思。Python 的SpooledTemporaryFile在构造函数中公开dir参数,并允许更改默认行为。我觉得在这种情况下这是有道理的,因为(未量化的)模型通常为 9GB+。在大多数情况下,整体tmpfs有 ~8-16GB(默认值为 50% 的 RAM,不包括交换)。我认为最好覆盖默认值,而不是让它失败。这个问题最近让我很沮丧。当您转换较大的模型时,它通常会在运行 30-40 分钟后在接近尾声时失败,您必须从头开始。当然最好的解决方案是使用LazyUnpickler,但这是一种更复杂的方法。

6

我不确定你是否明白我的意思。

我的意思是,已经有一个惯例允许用户设置临时文件的存储位置,SpooledTemporaryFile据我所知,这个惯例是被遵守的。所以我们不需要覆盖这个行为,我们只需要让用户知道他们可能需要这样做。明白我的意思了吗?

我实际上认为覆盖该行为并使用其他临时目录可能会很奇怪。

0

SpooledTemporaryFile 的要点实际上是 dir 应该放在磁盘上 - 它被设计为使用内存直到达到某个阈值,然后写入 dir,假设 dir 有更多空间,而不是更少。否则,您只会使用 RAM 而根本不使用文件对象。

TMPDIR 是与 NamedTemporaryFile 等保持一致的默认值,但在 Linux 上,我们真的不应该假脱机到 /tmp - 我们应该使用当前目录(就像 transformers 那样)或 /var/tmp。