当前位置: 首页 > news >正文

ND4J的MemoryWorkspace

MemoryWorkspace 是 ND4J 中一个强大的内存管理机制,旨在显著提高性能并减少 Java 垃圾回收 (Garbage Collection, GC) 的开销。在深度学习和科学计算中,经常会创建大量临时的 INDArray 对象(ND4J 中的 N 维数组)。如果依赖标准的 Java GC 来管理这些对象的内存(尤其是堆外内存 Off-Heap Memory),频繁的 GC 暂停会严重影响性能。

MemoryWorkspace 通过以下方式解决这个问题:

  1. 预分配内存块: 它预先分配一大块内存(可以在 Java 堆上 On-Heap,但更常用的是在堆外 Off-Heap)。
  2. 作用域内分配: 在一个特定的 MemoryWorkspace 作用域 (scope) 内创建的所有 INDArray,它们的内存会从这个预分配的内存块中获取,而不是通过标准的分配流程。
  3. 快速释放/重用: 当离开 MemoryWorkspace 的作用域时(通常通过 try-with-resources 语句),该工作空间内分配的所有 INDArray 所占用的内存会被一次性标记为可重用,或者整个内存块被释放(取决于配置)。关键在于,这个过程绕过了对每个单独 INDArray 的 Java GC。这使得内存的“回收”非常快。

为什么需要 MemoryWorkspace?(Why Needed?)

  • 减少 GC 开销: 这是最主要的原因。避免了 JVM 对大量短暂 INDArray 对象进行垃圾回收的需要,从而减少了 GC 暂停时间,提高了计算的流畅度和整体吞吐量。
  • 提高分配速度: 从预分配的连续内存块中获取小块内存通常比系统调用或标准 JVM 分配更快。
  • 减少内存碎片: 尤其对于堆外内存,有助于更有效地管理和减少碎片。
  • 更可预测的性能: 减少了由 GC 引起的不可预测的性能抖动。

最常见和推荐的使用方式是结合 Java 的 try-with-resources 语句,这能确保工作空间在使用完毕后被正确关闭(内存被标记为可重用)。

import org.nd4j.linalg.api.buffer.DataType
import org.nd4j.linalg.api.memory.conf.WorkspaceConfiguration
import org.nd4j.linalg.api.memory.enums.AllocationPolicy
import org.nd4j.linalg.api.memory.enums.LearningPolicy
import org.nd4j.linalg.api.memory.enums.SpillPolicy
import org.nd4j.linalg.api.ndarray.INDArray
import org.nd4j.linalg.factory.Nd4j
import org.nd4j.linalg.ops.transforms.Transforms

object WorkspaceExample {
    @JvmStatic
    fun main(args: Array<String>) {

        // 1. 配置工作空间 (可选,可以使用默认配置)
        val wsConfig: WorkspaceConfiguration = WorkspaceConfiguration.builder()
            .initialSize((100 * 1024 * 1024).toLong()) // 初始大小 100MB
            .maxSize((500 * 1024 * 1024).toLong()) // 最大大小 500MB (如果需要增长)
            .policyAllocation(AllocationPolicy.STRICT) // 分配策略: STRICT (空间不足时报错)
            .policyLearning(LearningPolicy.OVER_TIME) // 学习策略: NONE (不自动调整大小)
            .policySpill(SpillPolicy.FAIL) // 溢出策略: FAIL (空间不足时失败)
            .build()

        // 定义工作空间名称
        val workspaceId = "MyCalculationWorkspace"

        // 2. 使用 try-with-resources 激活并使用工作空间
        try {
            Nd4j.getWorkspaceManager().getAndActivateWorkspace(wsConfig, workspaceId).use { ws ->

                // 在这个 try 块内创建的 INDArray 会使用 'MyCalculationWorkspace' 的内存
                val x: INDArray = Nd4j.rand(DataType.FLOAT, 1000, 1000) // 分配在工作空间内
                val y: INDArray = Nd4j.rand(DataType.FLOAT, 1000, 1000) // 分配在工作空间内
                println("X is allocated in workspace: " + x.isAttached) // 输出 true
                println("Current workspace size: " + ws.currentSize)

                // 计算结果 z 也会分配在当前活动的工作空间内
                val z = x.mmul(y)
                println("Z is allocated in workspace: " + z.isAttached) // 输出 true
                println("Workspace size after mmul: " + ws.currentSize)

                // 在这里可以进行更多的计算...
                val w = z.add(1.0) // w 也在工作空间内
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

        // 在 try 块外部,之前在工作空间中创建的 x, y, z, w 变量引用的是无效内存
        // 尝试访问它们通常会导致错误或未定义行为 (除非使用了 detach/leverage)
        // System.out.println(x); // !!! 危险操作 !!!

        // 如果需要一个不受工作空间影响的数组
        val outsideArray: INDArray = Nd4j.create(DataType.FLOAT, 10, 10) // 在工作空间外部创建,由 GC 管理
        println("Outside array is attached to workspace? " + outsideArray.isAttached) // 输出 false

        // 清理 (如果需要手动管理)
//         Nd4j.getWorkspaceManager().destroyAllWorkspacesForCurrentThread();
    }

    // 从工作空间返回数组
    fun calculationWithResult(input: INDArray): INDArray {
        Nd4j.getWorkspaceManager().getAndActivateWorkspace("ResultWorkspace").use { ws ->
            val tempResult = input.mul(2.0) // 在工作空间内
            val finalResult = tempResult.add(1.0) // 仍在工作空间内

            // 使用 detach() 将数组从工作空间中移出,使其生命周期由 GC 管理
            // detach() 会创建一个新的 INDArray (或只是改变其状态),其数据复制到常规内存中
            return finalResult.detach()
        }
        // finalResult 在这里是有效的,因为它被 detach 了
    }

    // 在迭代中使用工作空间 (例如 RNN)
    fun iterativeTask(): INDArray {
        var state: INDArray = Nd4j.zeros(DataType.FLOAT, 1, 10) // 初始状态在外部
        val iterWs = "IterativeWS"

        // 配置一个可重用的工作空间
        val iterConfig: WorkspaceConfiguration = WorkspaceConfiguration.builder()
            .initialSize((10 * 1024 * 1024).toLong()) // 10MB
            .policyLearning(LearningPolicy.FIRST_LOOP) // 让它在第一次循环时学习所需大小
            .build()

        for (i in 0..99) {
            Nd4j.getWorkspaceManager().getAndActivateWorkspace(iterConfig, iterWs).use { ws ->
                val input: INDArray = Nd4j.rand(DataType.FLOAT, 1, 10) // 本次迭代的输入,在工作空间内

                // 使用 leverage() 将上一次迭代的结果 (state) "带入" 当前工作空间作用域
                // leverage() 不会复制数据,它假设 state 的数据在下一次循环开始时
                // 仍然位于同一个工作空间内存区域(因为工作空间会被重用)
                // 如果 state 是第一次进入或者来自外部,leverage 可能会触发复制
                val prevState = state.leverage()

                state = Transforms.tanh(prevState.add(input))
            }
        }
        // 循环结束后,最终的 state 仍然指向工作空间内存。
        // 如果需要长期保留它,需要 detach()
        return state.detach()
    }
}

关键方法和概念:

  • Nd4j.getWorkspaceManager().getAndActivateWorkspace(…): 获取(如果不存在则创建)并激活一个工作空间。通常在 try-with-resources 中使用。
  • INDArray.isAttached(): 检查一个 INDArray 当前是否附加到某个活动的工作空间。
  • INDArray.detach(): 创建一个 INDArray 的副本(或仅改变其状态),该副本位于常规内存中(由 GC 管理),不再受工作空间生命周期影响。当需要将工作空间中的计算结果返回或传递到工作空间作用域之外时,必须使用它。
  • INDArray.leverage(): 将一个 INDArray 标记为可以在同一个工作空间的下一个周期(下一次激活)中使用。这在迭代计算(如 RNN 或优化循环)中非常有用,可以避免在每次迭代时都将状态 detach() 再重新引入的开销。它假设工作空间的内存在不同周期之间会被重用。
  • 嵌套工作空间 (Nested Workspaces): 可以在一个工作空间内打开另一个工作空间。内部工作空间可以有自己的内存,也可以配置为在空间不足时“溢出”到父工作空间。

配置选项 (WorkspaceConfiguration)

  • initialSize: 工作空间初始分配的内存大小。
  • maxSize: 工作空间可增长到的最大大小(如果 AllocationPolicy 或 SpillPolicy 允许增长)。
  • policyAllocation (AllocationPolicy):
    • STRICT: 如果请求的内存超过当前可用空间,则失败。
    • OVERALLOCATE: 允许分配超过 initialSize 的内存,但通常不超过 maxSize。
  • policySpill (SpillPolicy): 当工作空间内存不足时如何处理:
    • FAIL: 抛出异常。
    • REALLOCATE: 尝试重新分配更大的内存块(可能很慢)。
    • EXTERNAL: 临时在工作空间外部(常规内存)分配。
  • policyLearning (LearningPolicy): 自动调整工作空间大小的策略:
  • NONE: 不自动调整。
    • FIRST_LOOP: 根据第一次激活周期内的峰值内存使用量来调整 initialSize。
    • OVER_TIME: 在多个周期内动态调整大小(更复杂)。
http://www.dtcms.com/a/112204.html

相关文章:

  • [2018][note]用于超快偏振开关和动态光束分裂的all-optical有源THz超表——
  • 【FPGA基础学习】状态机思想实现流水灯
  • 推理模型与普通大模型如何选择?
  • vue组件开发:什么是VUE组件?
  • Redis核心机制-缓存、分布式锁
  • selectdb修改表副本
  • leetcode51-N皇后
  • SpringBoot异步任务实践指南:提升系统性能的利器
  • 《P1029 [NOIP 2001 普及组] 最大公约数和最小公倍数问题》
  • 数据集(Dataset)和数据加载器(DataLoader)-pytroch学习3
  • MySQL 索引原理
  • Koordinator-NodeInfoCollector
  • 微服务架构: SpringCloud服务注册与发现详解
  • P17_ResNeXt-50
  • Apache Struts2 漏洞(CVE-2017-5638)技术分析
  • 七、重学C++—静态多态(编译期)
  • Web Service技术
  • MySQL vs MSSQL 对比
  • AI——使用numpy
  • Java模板方法模式详解
  • Ansible Playbook 进阶探秘:Handlers、变量、循环及条件判断全解析
  • 【设计模式】原型模式:用“克隆”术让对象创建更灵活
  • 开放最短路径优先 - OSPF【LSA详细】
  • 政安晨【超级AI工作流】—— 基于COZE探索有趣的主题互动问答工作流(同宇宙儿童提问机)
  • AI 数理逻辑基础之统计学基本原理(上)
  • 【3】数据结构的双向链表章
  • 每日一题洛谷P8649 [蓝桥杯 2017 省 B] k 倍区间c++
  • 【嵌入式-stm32电位器控制以及旋转编码器控制LED亮暗】
  • DHCP协议和win server2022无脑配置DHCP
  • 残差神经网络(ResNet)概念解析与用法实例:简洁的图像处理任务