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 更改