性能调优#

注意

对于典型用例,默认的 DALI 配置开箱即用,性能良好,您无需查看本节。

线程亲和性#

此功能允许您将 DALI 线程绑定到指定的 CPU。线程亲和性避免了工作线程在核心之间跳转的开销,并提高了 CPU 密集型工作负载的性能。您可以使用 DALI_AFFINITY_MASK 环境变量设置 DALI CPU 线程亲和性,该变量是以逗号分隔的 CPU ID 列表,将分配给相应的 DALI 线程。DALI 线程的数量在 pipeline 构建期间由 num_threads 参数设置,set_affinity 启用 CPU 工作线程的线程亲和性。

注意

出于性能原因,混合 nvidia.dali.fn.decoders.image() 操作符(基于 nvJPEG)会自行创建线程,并且这些线程始终是绑定的。

DALI_AFFINITY_MASK 中,如果线程数高于 CPU ID 的数量,则应用以下过程

  1. 线程被分配给 CPU ID,直到 DALI_AFFINITY_MASK 中的所有 CPU ID 都被分配完毕。

  2. 对于剩余的线程,将使用 nvmlDeviceGetCpuAffinity 中的 CPU ID。

示例

# assuming that DALI uses num_threads=5
DALI_AFFINITY_MASK="3,5,6,10"

此示例将线程 0 设置为 CPU 3,线程 1 设置为 CPU 5,线程 2 设置为 CPU 6,线程 3 设置为 CPU 10,线程 4 设置为 nvmlDeviceGetCpuAffinity 返回的 CPU ID。

内存消耗#

DALI 使用以下内存类型

  • 主机

  • 主机页锁定

  • GPU

分配和释放 GPU 和主机页锁定(或pinned)内存需要设备同步。因此,在可能的情况下,DALI 避免重新分配这些类型的内存。使用这些存储类型分配的缓冲区仅在现有缓冲区太小而无法容纳请求的形状时才会增长。当内存需求变得稳定且不再需要分配时,此策略减少了内存管理操作的总数并提高了处理速度。

相比之下,普通主机内存的分配和释放相对廉价。为了减少主机内存消耗,当新的请求大小小于旧大小的一部分时,缓冲区可能会缩小。这称为缩小阈值。它可以调整为介于 0(从不缩小)和 1(始终缩小)之间的值。默认值为 0.9。该值可以通过 DALI_HOST_BUFFER_SHRINK_THRESHOLD 环境变量控制,或者通过调用 nvidia.dali.backend.SetHostBufferShrinkThreshold 函数在 Python 中设置。

在处理过程中,DALI 处理样本批次。对于 GPU 和某些 CPU 操作符,每个批次都存储为连续内存并一次处理,这减少了必要的分配次数。对于某些无法提前计算其输出大小的 CPU 操作符,批次存储为单独分配的样本向量。

例如,如果您的批次由九个 480p 图像和一个 4K 图像随机顺序组成,则连续分配可以容纳这些批次的所有可能组合。另一方面,存储为单独缓冲区的 CPU 批次需要在多次迭代后为每个样本保留 4K 分配。保持操作符输出的 GPU 缓冲区可以增长到最大可能批次的大小,而非连续 CPU 缓冲区可以达到数据集中最大样本大小乘以批次中样本数量的大小。

主机和 GPU 缓冲区都具有可配置的增长因子。如果因子大于 1,并且可能避免后续重新分配。默认情况下禁用此功能,增长因子设置为 1。增长因子可以使用 DALI_HOST_BUFFER_GROWTH_FACTORDALI_DEVICE_BUFFER_GROWTH_FACTOR 环境变量以及 nvidia.dali.backend.SetHostBufferGrowthFactornvidia.dali.backend.SetDeviceBufferGrowthFactor Python API 函数来控制。为方便起见,可以使用 DALI_BUFFER_GROWTH_FACTOR 环境变量和 nvidia.dali.backend.SetBufferGrowthFactor Python 函数为主机和 GPU 缓冲区设置相同的增长因子。

分配器配置#

DALI 为各种类型的内存使用几种类型的内存资源。

对于常规主机内存,DALI 对小分配使用(对齐的)malloc,对大分配使用自定义内存池(以防止 mallocmmap 的昂贵调用)。可以直接使用 malloc 分配的主机分配的最大大小可以通过设置 DALI_MALLOC_POOL_THRESHOLD 环境变量进行自定义。如果未指定,则该值要么从控制 malloc 的环境变量派生,要么(如果未找到)使用 32M 的默认值。

对于主机pinned内存,DALI 在 cudaMallocHost 之上使用流感知内存池。虽然不鼓励直接使用 cudaMallocHost,但可以通过在环境中指定 DALI_USE_PINNED_MEM_POOL=0 来强制使用。

对于设备内存,DALI 使用构建在 CUDA VMM 资源之上的流感知内存池(如果平台支持 VMM)。它可以更改为 cudaMallocAsync 甚至普通的 cudaMalloc。为了完全跳过内存池并使用 cudaMalloc(不推荐),请设置 DALI_USE_DEVICE_MEM_POOL=0。设置 DALI_USE_CUDA_MALLOC_ASYNC=1 以使用 cudaMallocAsync 代替 DALI 的内部内存池。当使用内存池(DALI_USE_DEVICE_MEM_POOL=1 或未设置)时,您可以通过设置 DALI_USE_VMM=0 来禁用 VMM 的使用。这将导致 cudaMalloc 用作内部内存池的上游内存资源。

使用 cudaMallocAsync 通常会导致执行速度稍慢,但它可以实现与其他使用相同分配方法的库共享内存池。

警告

禁用内存池将导致性能急剧下降。此选项仅用于调试目的。

由于 cudaFree 中的悲观同步,禁用 CUDA VMM 可能会降低性能,并且由于碎片化影响 cudaMalloc,可能会导致内存不足错误。

内存池预分配#

DALI 使用多个内存池 - 每个 CUDA 设备一个,加上一个用于pinned主机内存的全局池。通常,这些池按需增长。增长可能会导致吞吐量暂时下降。在对性能至关重要的应用程序中,可以通过预先分配池来避免这种情况。

nvidia.dali.backend.PreallocateDeviceMemory(bytes: int, device_id: int) None#

在给定设备上预分配内存

该函数确保在调用后,可以从池中分配以 bytes 给出的内存量(无需进一步向操作系统请求)。

在 DALI pipeline 运行时调用此函数通常是安全的,但不应用于为已经运行的 pipeline 预分配内存 - 这可能会导致内存争用,并可能在 pipeline 中触发内存不足错误。

nvidia.dali.backend.PreallocatePinnedMemory(bytes: int) None#

预分配非分页(pinned)主机内存

该函数确保在调用后,可以从池中分配以 bytes 给出的内存量(无需进一步向操作系统请求)。

在 DALI pipeline 运行时调用此函数通常是安全的,但不应用于为已经运行的 pipeline 预分配内存 - 这可能会导致内存争用,并可能在 pipeline 中触发内存不足错误。

释放内存池#

DALI 使用的内存池是全局的,并在 pipeline 之间共享。pipeline 的销毁不会导致内存返回给操作系统 - 它会保留供将来的 pipeline 使用。要释放当前未使用的内存,您可以调用 nvidia.dali.backend.ReleaseUnusedMemory()

nvidia.dali.backend.ReleaseUnusedMemory() None#

从内存池中释放未使用的块。

仅释放完全空闲的块。该函数从所有设备池以及主机pinned内存池中释放内存。

在 DALI pipeline 运行时使用此函数是安全的。

操作符缓冲区预调整大小#

当您可以精确预测 DALI 运行期间的内存消耗时,此功能可以帮助您微调处理 pipeline。好处之一是可以避免某些重新分配的开销。

DALI 使用中间缓冲区在处理图中的操作符之间传递数据。此缓冲区的容量会增加以容纳新数据,但永远不会减少。但是,有时即使是这种有限数量的分配也可能仍然影响 DALI 性能。如果您知道每个操作符缓冲区需要多少内存,则可以添加提示以在首次运行 pipeline 之前预先分配缓冲区。

以下参数可用

  • bytes_per_sample pipeline 参数,它接受一个值,该值全局用于所有操作符的所有缓冲区。

  • 每个操作符参数 bytes_per_sample_hint,它接受一个值或值列表。

当提供一个值时,它用于操作符的所有输出缓冲区。当提供列表时,每个操作符输出缓冲区都预先调整为相应的大小。要确定每个操作符需要的内存输出量,请完成以下任务

  1. 通过将 enable_memory_stats 设置为 True 来创建 pipeline。

  2. 通过调用 pipeline 上的 nvidia.dali.Pipeline.executor_statistics() 方法查询 pipeline 的操作符输出内存统计信息。

max_real_memory_size 值表示批次中最大的张量,对于每次样本分配内存而不是在当时为整个批次分配内存的输出,或者当分配是连续的时,表示平均张量大小。此值应提供给 bytes_per_sample_hint

预取队列深度#

DALI pipeline 允许缓冲一个或多个批次的数据,这在处理时间因批次而异时非常重要。默认预取深度为 2。您可以使用 prefetch_queue_depth pipeline 参数更改此值。如果默认预取深度值未隐藏变化,我们建议您提前预取更多数据。

注意

增加队列深度也会增加内存消耗。

读取器微调#

选定的读取器使用环境变量来微调其关于文件存储访问模式的行为。在大多数情况下,默认参数应提供良好的性能,但在某些特定系统属性(文件系统类型)中,可能需要相应地调整它们。

  • DALI_GDS_CHUNK_SIZE - 调整单个 GDS 读取请求的大小。

    适用于 nvidia.dali.fn.readers.numpy() 操作符的 GPU 后端。默认值为 2MB。它必须是一个数字,可以选择后跟“k”或“M”,是 2 的幂,并且不大于 16MB。对于不同的值,可以实现最佳性能,具体取决于文件系统和 GDS 版本。

  • DALI_ODIRECT_ALIGNMENT - 调整 O_DIRECT 对齐方式。

    仅适用于公开 use_o_direct 参数的读取器,例如 nvidia.dali.fn.readers.numpy() 操作符的 CPU 后端。默认值为 4KB。它必须是一个数字,可以选择后跟“k”或“M”,是 2 的幂,并且不大于 16MB。最小值取决于文件系统。有关更多信息,请参阅 Linux Open 调用手册页,O_DIRECT 部分

  • DALI_ODIRECT_LEN_ALIGNMENT - 调整 O_DIRECT 读取长度对齐方式。

    仅适用于公开 use_o_direct 参数的读取器,例如 nvidia.dali.fn.readers.numpy() 操作符的 CPU 后端。默认值为 4KB。它必须是一个数字,可以选择后跟“k”或“M”,是 2 的幂,并且不大于 16MB。最小值取决于文件系统。有关更多信息,请参阅 Linux Open 调用手册页,O_DIRECT 部分

  • DALI_ODIRECT_CHUNK_SIZE - 调整单个 O_DIRECT 读取请求的大小。

    仅适用于公开 use_o_direct 参数的读取器,例如 nvidia.dali.fn.readers.numpy() 操作符的 CPU 后端。默认值为 2MB。它必须是一个数字,可以选择后跟“k”或“M”,是 2 的幂,并且不大于 16MB,并且不小于 DALI_ODIRECT_LEN_ALIGNMENT。对于不同的值,可以实现最佳性能,具体取决于文件系统。