GPU SVM 章节

一致的设计原则

  • migrate_to_ram 路径
    • 仅依赖于核心 MM 概念(迁移 PTE、页面引用和页面锁定)。

    • 除了硬件交互的锁之外,没有驱动程序特定的锁。 不需要这些锁,并且通常来说,发明驱动程序定义的锁来密封核心 MM 竞争是一个坏主意。

    • 在修复 do_swap_page 以锁定故障页面之前,发生了一个驱动程序特定的锁导致问题的示例。 如果足够多的线程读取故障页面,migrate_to_ram 中的驱动程序独占锁会产生稳定的活锁。

    • 支持部分迁移(即,尝试迁移的页面的子集实际上可以迁移,只有故障页面保证可以迁移)。

    • 驱动程序通过重试循环而不是锁定来处理混合迁移。

  • 驱逐
    • 驱逐被定义为将数据从 GPU 迁移回 CPU,而无需虚拟地址来释放 GPU 内存。

    • 仅查看物理内存数据结构和锁,而不是查看虚拟内存数据结构和锁。

    • 不查看 mm/vma 结构或依赖于这些结构被锁定。

    • 以上两点的理由是 CPU 虚拟地址可以随时更改,而物理页面保持稳定。

    • GPU 页表无效化(需要 GPU 虚拟地址)通过可以访问 GPU 虚拟地址的通知器来处理。

  • GPU 故障端
    • mmap_read 仅用于核心 MM 函数周围,这些函数需要此锁,并且应努力仅在 GPU SVM 层中获取 mmap_read 锁。

    • 大型重试循环用于处理与 gpu 页表锁/mmu 通知器范围锁/我们最终调用的任何东西下的 mmu 通知器的所有竞争。

    • 不应通过尝试持有锁来在故障端处理竞争(尤其是与并发驱逐或 migrate_to_ram 的竞争);相反,应使用重试循环处理它们。 一个可能的例外是在初始迁移到 VRAM 期间持有 BO 的 dma-resv 锁,因为这是一个明确定义的锁,可以在 mmap_read 锁下获取。

    • 上述方法的一个可能问题是,如果驱动程序具有严格的迁移策略,要求 GPU 访问发生在 GPU 内存中。 并发 CPU 访问可能导致由于无限重试而导致的活锁。 虽然当前 GPU SVM 的用户 (Xe) 没有这样的策略,但将来可能会添加。 理想情况下,这应该在核心 MM 端解决,而不是通过驱动程序侧锁解决。

  • 物理内存到虚拟回指针
    • 这不起作用,因为不应存在从物理内存到虚拟内存的指针。 mremap() 是核心 MM 更新虚拟地址而不通知驱动程序地址更改的一个示例,而驱动程序只接收到无效通知器。

    • 物理内存回指针 (page->zone_device_data) 应从分配到页面释放保持稳定。 安全地针对并发用户更新它将非常困难,除非页面是空闲的。

  • GPU 页表锁定
    • 通知器锁仅保护范围树,范围的页面有效状态(而不是由于更广泛的通知器导致的 seqno),页表条目和 mmu 通知器 seqno 跟踪,它不是防止竞争的全局锁。

    • 如上所述,所有竞争都通过大型重试处理。

基线设计概述

直接渲染管理器 (DRM) 的 GPU 共享虚拟内存 (GPU SVM) 层是 DRM 框架的一个组件,旨在管理 CPU 和 GPU 之间的共享虚拟内存。 它通过允许 CPU 和 GPU 虚拟地址空间之间的内存共享和同步,为 GPU 加速的应用程序实现高效的数据交换和处理。

关键 GPU SVM 组件

  • 通知器

    用于跟踪内存间隔并通知 GPU 更改,通知器的大小基于 GPU SVM 初始化参数,建议大小为 512M 或更大。 它们维护一个红黑树和一个属于通知器间隔内的范围列表。 通知器在 GPU SVM 红黑树和列表中被跟踪,并随着间隔内的范围的创建或销毁而动态地插入或删除。

  • 范围

    表示在 DRM 设备中映射并由 GPU SVM 管理的内存范围。 它们的大小基于块大小数组(它是 GPU SVM 初始化参数)和 CPU 地址空间。 在 GPU 故障时,选择适合故障 CPU 地址空间的最大对齐块作为范围大小。 范围预计会在 GPU 故障时动态分配,并在 MMU 通知器 UNMAP 事件时移除。 如上所述,范围在通知器的红黑树中被跟踪。

  • 操作

    定义驱动程序特定的 GPU SVM 操作的接口,例如范围分配、通知器分配和无效化。

  • 设备内存分配

    嵌入式结构,包含足够的信息供 GPU SVM 迁移到/从设备内存。

  • 设备内存操作

    定义驱动程序特定的设备内存操作接口,包括释放内存、填充 pfns 和复制到/从设备内存。

该层提供用于在 CPU 和 GPU 之间分配、映射、迁移和释放内存范围的接口。 它处理所有核心内存管理交互(DMA 映射、HMM 和迁移),并提供驱动程序特定的虚拟函数 (vfuncs)。 该基础架构足以构建 SVM 实现所需的预期驱动程序组件,如下详述。

预期驱动程序组件

  • GPU 页面错误处理程序

    用于基于故障地址创建范围和通知器,可以选择将范围迁移到设备内存,并创建 GPU 绑定。

  • 垃圾回收器

    用于取消映射和销毁范围的 GPU 绑定。 预计在通知器回调中的 MMU_NOTIFY_UNMAP 事件时将范围添加到垃圾回收器。

  • 通知器回调

    用于使范围的 GPU 绑定无效并 DMA 取消映射。

GPU SVM 处理核心 MM 交互的锁定,即根据需要锁定/解锁 mmap 锁。

GPU SVM 引入了一个全局通知器锁,它保护通知器的范围 RB 树和列表,以及范围的 DMA 映射和序列号。 GPU SVM 管理所有必需的锁定和解锁操作,除了驱动程序提交 GPU 绑定时重新检查范围的页面是否有效 (drm_gpusvm_range_pages_valid) 之外。 此锁对应于异构内存管理 (HMM)中提到的 driver->update 锁。 如果认为需要更细粒度的锁定,未来的修订版可能会从 GPU SVM 全局锁转换为每个通知器锁。

除了上述锁定之外,驱动程序还应实现一个锁来保护修改状态的核心 GPU SVM 函数调用,例如 drm_gpusvm_range_find_or_insert 和 drm_gpusvm_range_remove。 在代码示例中,此锁表示为“driver_svm_lock”。 还应该可以对单个 GPU SVM 中的并发 GPU 故障处理进行更细粒度的驱动程序侧锁定。 可以通过 drm_gpusvm_driver_set_lock 将“driver_svm_lock”添加到 GPU SVM 中。

迁移支持非常简单,允许在 RAM 和设备内存之间以范围粒度进行迁移。 例如,GPU SVM 目前不支持在一个范围内混合 RAM 和设备内存页面。 这意味着,在 GPU 故障时,整个范围可以迁移到设备内存,而在 CPU 故障时,整个范围将迁移到 RAM。 如果需要,将来可能会添加在一个范围内混合 RAM 和设备内存存储。

仅支持范围粒度的原因是:它简化了实现,并且范围大小由驱动程序定义,应该相对较小。

范围的部分取消映射(例如,CPU 取消映射 2M 中的 1M 导致 MMU_NOTIFY_UNMAP 事件)带来了一些挑战,主要挑战是范围的子集仍然具有 CPU 和 GPU 映射。 如果范围的后备存储位于设备内存中,则后备存储的子集具有引用。 一种选择是拆分范围和设备内存后备存储,但这的实现将非常复杂。 鉴于部分取消映射很少见,并且驱动程序定义的范围大小相对较小,因此 GPU SVM 不支持拆分范围。

由于不支持范围拆分,因此在部分取消映射范围后,预计驱动程序会使整个范围无效并销毁它。 如果范围具有设备内存作为其后备,则还应预期驱动程序将任何剩余的页面迁移回 RAM。

本节提供了三个关于如何构建预期驱动程序组件的示例:GPU 页面错误处理程序、垃圾回收器和通知器回调。

提供的通用代码不包括复杂迁移策略、优化无效化、细粒度驱动程序锁定或其他可能需要的驱动程序锁定(例如,DMA-resv 锁)的逻辑。

  1. GPU 页面错误处理程序

int driver_bind_range(struct drm_gpusvm *gpusvm, struct drm_gpusvm_range *range)
{
        int err = 0;

        driver_alloc_and_setup_memory_for_bind(gpusvm, range);

        drm_gpusvm_notifier_lock(gpusvm);
        if (drm_gpusvm_range_pages_valid(range))
                driver_commit_bind(gpusvm, range);
        else
                err = -EAGAIN;
        drm_gpusvm_notifier_unlock(gpusvm);

        return err;
}

int driver_gpu_fault(struct drm_gpusvm *gpusvm, unsigned long fault_addr,
                     unsigned long gpuva_start, unsigned long gpuva_end)
{
        struct drm_gpusvm_ctx ctx = {};
        int err;

        driver_svm_lock();
retry:
        // Always process UNMAPs first so view of GPU SVM ranges is current
        driver_garbage_collector(gpusvm);

        range = drm_gpusvm_range_find_or_insert(gpusvm, fault_addr,
                                                gpuva_start, gpuva_end,
                                                &ctx);
        if (IS_ERR(range)) {
                err = PTR_ERR(range);
                goto unlock;
        }

        if (driver_migration_policy(range)) {
                mmap_read_lock(mm);
                devmem = driver_alloc_devmem();
                err = drm_gpusvm_migrate_to_devmem(gpusvm, range,
                                                   devmem_allocation,
                                                   &ctx);
                mmap_read_unlock(mm);
                if (err)        // CPU mappings may have changed
                        goto retry;
        }

        err = drm_gpusvm_range_get_pages(gpusvm, range, &ctx);
        if (err == -EOPNOTSUPP || err == -EFAULT || err == -EPERM) {    // CPU mappings changed
                if (err == -EOPNOTSUPP)
                        drm_gpusvm_range_evict(gpusvm, range);
                goto retry;
        } else if (err) {
                goto unlock;
        }

        err = driver_bind_range(gpusvm, range);
        if (err == -EAGAIN)     // CPU mappings changed
                goto retry

unlock:
        driver_svm_unlock();
        return err;
}
  1. 垃圾回收器

void __driver_garbage_collector(struct drm_gpusvm *gpusvm,
                                struct drm_gpusvm_range *range)
{
        assert_driver_svm_locked(gpusvm);

        // Partial unmap, migrate any remaining device memory pages back to RAM
        if (range->flags.partial_unmap)
                drm_gpusvm_range_evict(gpusvm, range);

        driver_unbind_range(range);
        drm_gpusvm_range_remove(gpusvm, range);
}

void driver_garbage_collector(struct drm_gpusvm *gpusvm)
{
        assert_driver_svm_locked(gpusvm);

        for_each_range_in_garbage_collector(gpusvm, range)
                __driver_garbage_collector(gpusvm, range);
}
  1. 通知器回调

void driver_invalidation(struct drm_gpusvm *gpusvm,
                         struct drm_gpusvm_notifier *notifier,
                         const struct mmu_notifier_range *mmu_range)
{
        struct drm_gpusvm_ctx ctx = { .in_notifier = true, };
        struct drm_gpusvm_range *range = NULL;

        driver_invalidate_device_pages(gpusvm, mmu_range->start, mmu_range->end);

        drm_gpusvm_for_each_range(range, notifier, mmu_range->start,
                                  mmu_range->end) {
                drm_gpusvm_range_unmap_pages(gpusvm, range, &ctx);

                if (mmu_range->event != MMU_NOTIFY_UNMAP)
                        continue;

                drm_gpusvm_range_set_unmapped(range, mmu_range);
                driver_garbage_collector_add(gpusvm, range);
        }
}

可能的未来设计特性

  • 并发 GPU 故障
    • CPU 故障是并发的,因此并发 GPU 故障是有意义的。

    • 通过驱动程序 GPU 故障处理程序中的细粒度锁定应该是可能的。

    • 不需要预期的 GPU SVM 更改。

  • 具有混合系统页面和设备页面的范围
    • 如果需要,可以相当容易地添加到 drm_gpusvm_get_pages。

  • 多 GPU 支持
    • 正在进行中,预计在最初登陆 GPU SVM 后会有补丁。

    • 理想情况下,可以通过对 GPU SVM 进行很少或不进行任何更改来完成。

  • 放弃范围而支持 radix 树
    • 对于更快的通知器可能是可取的。

  • 复合设备页面
    • Nvidia、AMD 和 Intel 都同意,迁移设备层中昂贵的核心 MM 函数是性能瓶颈,拥有复合设备页面应通过减少这些昂贵调用的数量来帮助提高性能。

  • 用于迁移的更高阶 dma 映射
    • 4k dma 映射对 Intel 硬件上的迁移性能产生不利影响,更高阶 (2M) dma 映射应该有所帮助。

  • 在 GPU SVM 之上构建通用的 userptr 实现

  • 驱动程序侧 madvise 实现和迁移策略

  • 当这些落地时,从 Leon / Nvidia 中提取待处理的 dma-mapping API 更改