Intel Discrete GPU - Memory - Map / Page Fault

• 11 分钟阅读 • intel gpu

一块显存分配完成后,通常要根据它的使用场景来决定是否经过 CPU 或 GPU 的访问,一旦涉及到某一方的访问,那么必然要在访问前做地址映射,所谓映射在系统底层的视角来看的话,或许也可以称之为对应页表的填充。

当前独立显存加入到整个系统后,访存的链路将变得稍显复杂。

CPU / GPU 均可以访问系统内存和独立显存,独立显存通过 PCIE 接入总线后,在 CPU 的视角中,仅是多了份总线地址空间,CPU 可通过 MMU 直接访问。
独立显存在 GPU 板载集成,可通过内部总线直接访问。需要注意的是,GPU 也存在类 MMU 的 IP,它一般是厂商自己制造,在 Intel 独显中,它被称之为 GAM;
而 GPU 访问系统内存时,则需要经过自身的 GAM 与系统中外设专用的 SMMU 才能到达目标系统内存,这其中就涉及到两层页表。

大致访存链路如下,

理清大致的系统结构后,我们步入软件的世界,来看看 Xe 驱动中是怎样将显存映射到 CPU / GPU 的,
先从 CPU 映射开始看。

CPU MAP

当 Kernel 中完成一次显存 BO 分配时,还要与 DRM 子系统进行关联后,才能被 CPU MAP 操作时被找到,其关联的桥梁为drm_vma_offset_node,这个 offset 是针对一个drm_device而言,也就是在 Xe 驱动中,这个vma offset空间只有一个,一块 BO 插入到空间的接口为drm_vma_offset_add

除此之外,BO 分配完成后还会通过drm_gem_handle_create创建 Handle 并将其传回来用户空间,用户空间将这个 Handle 作为 fd 来找到 BO;需要注意的是,Handle 的作用域只有 Xe 的内部,DRM 查找 BO 要根据vma offset

用户空间仅能与 DRM 子系统直接对接,那么,CPU 映射一块显存就需要两步,第一步是通过 fd 转换为vma offset,第二步是通过 offset 继而 mmap;

以下是 Mesa 项目中对 BO 分配及 CPU MAP 的一个示例流程,

struct drm_xe_gem_create gem_create = {
    .size        = size_in_bytes,
    .cpu_caching = DRM_XE_GEM_CPU_CACHING_WB,
    .placement   = 1u << ec->devinfo->mem.sram.mem.instance,
};
intel_ioctl(ec->fd, DRM_IOCTL_XE_GEM_CREATE, &gem_create);

struct drm_xe_gem_mmap_offset mm = {
    .handle = gem_create.handle,
};
intel_ioctl(ec->fd, DRM_IOCTL_XE_GEM_MMAP_OFFSET, &mm);

bo->handle = gem_create.handle;
bo->map    = mmap(NULL, size_in_bytes, PROT_READ | PROT_WRITE,
            MAP_SHARED, ec->fd, mm.offset);

映射流程会通过 DRM 最终到达 Xe mmap 中,Xe 中没有做过多的操作,重点在于将对应 vma 的 ops 配置为xe_gem_vm_ops,其 fault 成员为xe_gem_fault

CPU Fault

当 CPU 访问所映射的 BO 空间后将会触发 Page Fault,那么xe_gem_fault将会被调用,首先内部会通过 vma 中关联的信息取出 BO,
先通过ttm_io_prot取出 BO 分配时所配置的 Cache 策略,然后根据 BO 的存在位置做出不同的操作流程,

BO 存在于系统内存,则

BO 存在于独立显存,刚

最终,我们已经拿到的内存对应的 PFN 和 PROT,那么则可以通过vmf_insert_pfn_prot来完成 CPU 侧的映射页表填充。

CPU 缺页这里还存在个小优化,它是 prefault,因为显存的分配和使用往往是大小对应的,我们可以假设一块 BO 在 CPU 需要访问时会被大面积访问到,那到预先 Page Fault 的确能够节省很多开销。在 Xe 驱动中,num_prefault的大小默认配置为TTM_BO_VM_NUM_PREFAULT = 16

GPU MAP

GPU 映射显存的过程要比 CPU 映射复杂些,因为在显存分配章节我们曾提过,BO 的背后所代表的实际显存,可能不仅仅是 Xe 内部分配的,它还可能是 DMA-BUF 或 USER-PTR,然而 CPU 映射这两种情况的话再普通不过,我们没有提及。但在 GPU 映射的场景,它们的映射与 Xe 驱动的实现息息相关。

BO 背后显存实际类型共有 VRAM / SRAM / DMA-BUF / USER-PTR, 我们在上面提到过,除 VRAM 类型外,像其它三种类似都会存储在系统内存中,而 GPU 到达系统内存的映射不仅需要 GPU 内部的映射,还要外系统 SMMU 的映射。

先来看看三种类型的 SMMU 映射时机是什么阶段,

几种涉及 SMMU 映射类型的流程,现在已大致表述完成。其实 vm bind 过程中还有一个很重要的概念没有被提及,它是xe_vma,一段地址空间的映射需要 VA 和 PA,xe_vma的作用是管理其中的 VA;其核心成员为drm_gpuva,它作为与 DRM 子系统连接的桥梁。

需要注意的是,xe_vma在这里只负责管理 VA,其中并不包含 VA 的分配,因为在 Xe GPU 软件实现中,VA 的分配是在用户态驱动来完成。在 Mesa Xe 用户驱动实现中,VA 往往通过util_vma_heap_alloc_addr来分配。

每一个xe_vma会通过vm_bind_ioctl_ops_parse解析用户下发的请求所构建成的drm_gpuva_ops并创建,因为xe_vma承载着实际显存的 Bind 使命,它要与所有使用到的显存都存在联系,

以上流程,完成了 GPU 映射显存时的部分准备工作。现在开始 GPU 自身页表的映射构建流程,上面有提及,这一块的流程可称之为vm bind
Vm bind 首先要解析出用户的 bind 请求,单次请求中的 Bind 显存数可以是多个,除 USER-PTR 外,用户均需要通过 Gem Handle 指定所需 Bind 显存目标。

以下是 Mesa 项目中对 BO 做 GPU MAP 的一个示例流程,

struct drm_xe_vm_bind_op bind_ops[] = {
    {
        .op        = DRM_XE_VM_BIND_OP_MAP,
        .obj       = ec->bo.batch.handle,
        .addr      = ec->bo.batch.addr,
        .range     = EXECUTOR_BO_SIZE,
        .pat_index = ec->devinfo->pat.cached_coherent.index,
    },
};

struct drm_xe_sync bind_syncs[] = {
    {
        .type   = DRM_XE_SYNC_TYPE_SYNCOBJ,
        .handle = sync_handles[0],
        .flags  = DRM_XE_SYNC_FLAG_SIGNAL,
    },
};

struct drm_xe_vm_bind bind = {
    .vm_id           = ec->xe.vm_id,
    .num_binds       = ARRAY_SIZE(bind_ops),
    .vector_of_binds = (uintptr_t)bind_ops,
    .num_syncs       = 1,
    .syncs           = (uintptr_t)bind_syncs,
};

intel_ioctl(ec->fd, DRM_IOCTL_XE_VM_BIND, &bind);

千呼万唤,基于以上前置工作,可以开始最重要的映射 GPU 内部页表映射环节。

映射流程可拆解为两步,

GPU Fault

Intel GPU 的运行模式一般分为两类,

其流程大致如下,

有没有想过xe_vma_userptr_check_repin是怎样判断出 USER-PTR 对应的内存有被 Invalidate 的?其实这是一个 MMU 的在 Invalidate 时的一个 Notify,Xe 驱动中有注册vma_userptr_invalidate回调,如果有发生 invalidate 则 seq 会有更新。

文章标签: intel gpu

上一篇 : Intel Discrete GPU - Memory - Migrate
下一篇 : Intel Discrete GPU - Memory - Alloc
阅读进度 0%