本文所讨论的均为AMD公司的显卡产品
显卡:AMD Radeon RX 5500 - 8GB 显存
Linux:Linux-5.4.y (fc944ddc0b4a )
Arch: X86_64
AMD GPU 虚拟内存 现代的GPU内部都包含有一个类似MMU的内存管理单元,负责GPU侧的虚拟地址到物理的翻译。和CPU类似,GPU也有一个多级页表的数据结构来管理GPU页表的映射,每一个GPU进程有一份GPU页表,在提交Command的时候GPU驱动会把进程对应的GPU页表设置到硬件寄存器中,在细节上GPU的地址分配和映射和CPU稍有不同。
AMD GPU 的虚拟地址空间 (User Mode)
最新的AMD GPU 有48 bit VA 寻址能力,但是为了兼容32bit 系统4G地址空间,GPU 的虚拟地址被认为分成4个段:
AMD GPU VA RANGES
Start
End
Size
1. vamgr_32
0x0000_0000_0000_0000
0x0000_0000_FFFF_FFFF
4G
2. vamgr
0x0000_0000_1000_0000
0x0000_7FFF_FFFF_FFFF
128T - 4G
3. vamgr_high_32
0xFFFF_8000_0000_0000
0xFFFF_8000_FFFF_FFFF
4G
4. vamgr_high
0xFFFF_8001_0000_0000
0xFFFF_0000_0000_0000
128T - 4G
AMD GPU 的虚拟地址分配 (User Mode) GPU的虚拟地址空间被分为4个区间,进程可以按照自身需要选择使用哪段地址,GPU 用户态的虚拟地址由libdrm_amdgpu
分配和释放,内核的态的虚拟地址由KMD驱动管理分配,内核的地址分配和用户态稍有不同,后面会有介绍。
分配的虚拟地址区间不可以跨越上面的不同区域。
KMD会保留一部分VA
作为特殊用途,并通过ioctl
命令report给libdrm_amdgpu
。
用户态不需要关心VA
地址关联的内存是显存(VRAM)还是系统内存(GTT),内存的类型会通过GPU的页表(PTE
)隐藏掉,因此GTT和VRAM的寻址会被GPU VM硬件单元统一编址,简化用户态编程。
libdrm amdgpu vamgr 初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 drm_public int amdgpu_device_initialize (int fd, uint32_t *major_version, uint32_t *minor_version, amdgpu_device_handle *device_handle) { .... start = dev->dev_info.virtual_address_offset; max = MIN2(dev->dev_info.virtual_address_max, 0x100000000 ULL); amdgpu_vamgr_init(&dev->vamgr_32, start, max, dev->dev_info.virtual_address_alignment); start = max; max = MAX2(dev->dev_info.virtual_address_max, 0x100000000 ULL); amdgpu_vamgr_init(&dev->vamgr, start, max, dev->dev_info.virtual_address_alignment); start = dev->dev_info.high_va_offset; max = MIN2(dev->dev_info.high_va_max, (start & ~0xffffffff ULL) + 0x100000000 ULL); amdgpu_vamgr_init(&dev->vamgr_high_32, start, max, dev->dev_info.virtual_address_alignment); start = max; max = MAX2(dev->dev_info.high_va_max, (start & ~0xffffffff ULL) + 0x100000000 ULL); amdgpu_vamgr_init(&dev->vamgr_high, start, max, dev->dev_info.virtual_address_alignment); }
libdrm amdgpu va 分配 amdgpu_vamgr_find_va :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 static drm_private uint64_t amdgpu_vamgr_find_va(struct amdgpu_bo_va_mgr *mgr, uint64_t size, uint64_t alignment, uint64_t base_required) { struct amdgpu_bo_va_hole *hole , *n ; uint64_t offset = 0 , waste = 0 ; alignment = MAX2(alignment, mgr->va_alignment); size = ALIGN(size, mgr->va_alignment); if (base_required % alignment) return AMDGPU_INVALID_VA_ADDRESS; pthread_mutex_lock(&mgr->bo_va_mutex); LIST_FOR_EACH_ENTRY_SAFE_REV(hole, n, &mgr->va_holes, list ) { if (base_required) { if (hole->offset > base_required || (hole->offset + hole->size) < (base_required + size)) continue ; waste = base_required - hole->offset; offset = base_required; } else { offset = hole->offset; waste = offset % alignment; waste = waste ? alignment - waste : 0 ; offset += waste; if (offset >= (hole->offset + hole->size)) { continue ; } } if (!waste && hole->size == size) { offset = hole->offset; list_del(&hole->list ); free (hole); pthread_mutex_unlock(&mgr->bo_va_mutex); return offset; } if ((hole->size - waste) > size) { if (waste) { n = calloc (1 , sizeof (struct amdgpu_bo_va_hole)); n->size = waste; n->offset = hole->offset; list_add(&n->list , &hole->list ); } hole->size -= (size + waste); hole->offset += size + waste; pthread_mutex_unlock(&mgr->bo_va_mutex); return offset; } if ((hole->size - waste) == size) { hole->size = waste; pthread_mutex_unlock(&mgr->bo_va_mutex); return offset; } } pthread_mutex_unlock(&mgr->bo_va_mutex); return AMDGPU_INVALID_VA_ADDRESS; }
个人感觉这个代码是可以用 inteval-tree
进行优化一下可以提高下分配效率,效率可以从O(n)减少为O(log(n))
amdgpu_vamgr_find_va是libdrm
里分配VA的函数,该函数使用一个链表来管理VA的分配。
amdgpu_bo_va_mgr *mgr : 对应上面的4个区间之一
uint64_t size: VA分配大小
uint64_t alignment:地址对齐要求, 一般4k对齐可以提高效率
base_required:最低地址要求
AMD GPU 的虚拟地址空间 (Kernel Mode)
在内核态不需要考虑4G
空间问题,将整个256T虚拟地址空间划分为2个128T的虚拟地址空间,因为整个48bit的虚拟地址空间很大,内核将这部分虚拟地址空间又划分了几个不同的区域:
Type
Size
Note
VRAM
8G
映射 显存 到 KMD 虚拟地址空间
GART
512 M
映射 System Memory 到 KMD的虚拟地址空间
AGP
~~
映射 System Memory 到 KMD的虚拟地址空间
AGP是一个古老显卡的接口,现在已经基本不用,驱动里借用AGP完成一些特殊的功能。
KMD的虚拟地址和UMD稍有不同,通常来讲UMD需要通过GPU的页表来访问显存和GTT内存,GPU的页表创建和映射造成了很多不便,KMD将显存(VRAM)完整的映射到了GPU一个连续的虚拟地址空间,这个空间可以看做是显存的一个映射,这样做的好处是GPU显存的物理地址和KMD的虚拟地址只存在一个偏移,驱动在访问显存的时只需要加上一个偏移就可以访问显存,映射关系如下(KMD):
GPU VA = GPU PA + OFFSET
这个映射关系和Linux内核态的VA到PA的映射很相似。__pa(addr)
除了访问GPU显存,KMD还需要访问System Memory, 这部分和UMD一样,通过GPU的页表来完成到物理地址的映射。
因为KMD不需要使用大量的系统内存,所以VA的地址空间只使用了512M,这样仅仅可以通过一级页表就可以完成内核的地址映射。
VA宽度会决定页表的级数,同时也会影响页表的翻译效率,因此KMD将VA宽度限制为512M。
以上的地址信息可以在Kernel Log里查看:
1 2 3 [ 0.867957] amdgpu 0000:03:00.0: amdgpu: VRAM: 8176M 0x000000F400000000 - 0x000000F5FEFFFFFF (8176M used) [ 0.867957] amdgpu 0000:03:00.0: amdgpu: GART: 512M 0x0000000000000000 - 0x000000001FFFFFFF [ 0.867958] amdgpu 0000:03:00.0: amdgpu: AGP: 267419648M 0x000000F800000000 - 0x0000FFFFFFFFFFFF
AMD GPU 的虚拟地址分配 (Kernel Mode) GTT 虚拟地址分配 Linux DRM Framework 并没有提供一个完整的地址分配函数,这部分由KMD完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static int amdgpu_gtt_mgr_alloc (struct ttm_mem_type_manager *man, struct ttm_buffer_object *tbo, const struct ttm_place *place, struct ttm_mem_reg *mem) { struct amdgpu_device *adev = amdgpu_ttm_adev(man->bdev); struct amdgpu_gtt_mgr *mgr = man->priv; struct amdgpu_gtt_node *node = mem->mm_node; enum drm_mm_insert_mode mode ; unsigned long fpfn, lpfn; int r; if (amdgpu_gtt_mgr_has_gart_addr(mem)) return 0 ; if (place) fpfn = place->fpfn; else fpfn = 0 ; if (place && place->lpfn) lpfn = place->lpfn; else lpfn = adev->gart.num_cpu_pages; mode = DRM_MM_INSERT_BEST; if (place && place->flags & TTM_PL_FLAG_TOPDOWN) mode = DRM_MM_INSERT_HIGH; spin_lock(&mgr->lock); r = drm_mm_insert_node_in_range(&mgr->mm, &node->node, mem->num_pages, mem->page_alignment, 0 , fpfn, lpfn, mode); spin_unlock(&mgr->lock); if (!r) mem->start = node->node.start; return r; }
KMD驱动借助DRM
提供的helper函数drm_mm_insert_node_in_range
来完成地址分配,mem->start
就是返回的分配结果。
fpfn: First Page Frame Number
lpfn: Last Page Frame Number
mode = DRM_MM_INSERT_BEST: 地址分配策略,使用空闲区间中size最小的那个
drm_mm_xxxx 使用rbtree
在内核态实现了一组地址分配函数,通过这些函数DRM driver可以用它来管理不同类型的资源:
VA Address
VRAM Address
DRM BO VMA OFFSET Manager
VRAM 虚拟地址分配 KMD访问VRAM并不经过页表,因此不必为VRAM分配虚拟地址空间,仅仅将VRAM的地址加上一个偏移就可以访问。
GPU VA = GPU PA + OFFSET
AMD GPU 的物理地址空间 APU 的物理地址空间 APU没有独立的显存,APU将系统的一段连续内存映作为自己的显存,这部分内存被System BIOSreserve后并上报给OS,因此从OS的视角来看,系统确实了块内存,通俗来讲OS并不参与管理这部分内存。另外这部分显存通常不会掉电丢失数据,这一点和dGPU
不同。
dGPU的物理地址空间 dGPU有独立的显存,这部分显存由KMD驱动管理和分配,本人所使用的RX5500
显卡有8G显存,由于硬件和平台的一些限制,系统并不能访问全部的显存。
在关闭 Large Bar
功能后,通常只有256MB的空间被映射到PCIE BAR空间,也就意味着CPU只能直接访问256M的显存,访问其他部分显存需要借助其他硬件来实现。
可以通过下面的命令来查看BAR0
的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ lspci -d 1002: -vvvnnn 2f:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 14 [Radeon RX 5500/5500M / Pro 5500M] [1002:7340] (rev c5) (prog-if 00 [VGA controller]) Subsystem: Advanced Micro Devices, Inc. [AMD/ATI] Navi 14 [Radeon RX 5500/5500M / Pro 5500M] [1002:0b0c] Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+ Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx- Latency: 0, Cache Line Size: 64 bytes Interrupt: pin A routed to IRQ 83 Region 0: Memory at d0000000 (64-bit, prefetchable) [size=256M] Region 2: Memory at e0000000 (64-bit, prefetchable) [size=2M] Region 4: I/O ports at f000 [size=256] Region 5: Memory at fce00000 (32-bit, non-prefetchable) [size=512K] Expansion ROM at fce80000 [disabled] [size=128K] Capabilities: <access denied> Kernel driver in use: amdgpu
下面的命令可以帮你辅助验证这个BAR0就是你用的显存的最开始部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 $ sudo hexdump -n 128 /sys/kernel/debug/dri/0/amdgpu_vram 0000000 0028 0000 0038 0000 0001 0000 0000 0000 0000010 0001 0000 0000 0000 0001 0000 0028 0000 0000020 0010 0000 0000 0000 0007 0000 0000 0000 0000030 0780 0000 0440 0000 0000 0000 0000 0000 0000040 0000 0000 0000 0000 0000 0000 0000 0000 $ sudo memtool md -b 0xd0000000+128 d0000000: 28 00 00 00 38 00 00 00 01 00 00 00 00 00 00 00 (...8........... d0000010: 01 00 00 00 00 00 00 00 01 00 00 00 28 00 00 00 ............(... d0000020: 10 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 ................ d0000030: 80 07 00 00 40 04 00 00 00 00 00 00 00 00 00 00 ....@........... d0000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
细心的你还会在kernel log里观察到 efifb用的地址就是 BAR0
的地址
1 2 3 4 5 6 [ 0.374534] efifb: probing for efifb [ 0.374552] efifb: showing boot graphics [ 0.375508] efifb: framebuffer at 0xd0000000, using 8100k, total 8100k [ 0.375509] efifb: mode is 1920x1080x32, linelength=7680, pages=1 [ 0.375510] efifb: scrolling: redraw [ 0.375510] efifb: Truecolor: size=8:8:8:8, shift =24:16:8:0
你可以直接操作 0xd0000000 这个地址来完成屏幕的绘画,需要注意的是,这个操作只有在load amdgpu driver之前才可以,加载驱动后,fb driver会从efifb切换到amdgpu_fb,对应着2套不同的驱动。当启动Xserver之后,这个amdgpu_fb驱动会被KMS驱动替换掉。
AMD GPU 的物理地址分配 AMDGPU GPU的物理地址分配也是通过DRM Helperdrm_mm_insert_node_in_range
实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 static int amdgpu_vram_mgr_new (struct ttm_mem_type_manager *man, struct ttm_buffer_object *tbo, const struct ttm_place *place, struct ttm_mem_reg *mem) { struct amdgpu_device *adev = amdgpu_ttm_adev(man->bdev); struct amdgpu_vram_mgr *mgr = man->priv; struct drm_mm *mm = &mgr->mm; struct drm_mm_node *nodes ; enum drm_mm_insert_mode mode ; unsigned long lpfn, num_nodes, pages_per_node, pages_left; uint64_t vis_usage = 0 , mem_bytes; unsigned i; int r; lpfn = place->lpfn; if (!lpfn) lpfn = man->size; mem_bytes = (u64)mem->num_pages << PAGE_SHIFT; if (atomic64_add_return(mem_bytes, &mgr->usage) > adev->gmc.mc_vram_size) { atomic64_sub(mem_bytes, &mgr->usage); mem->mm_node = NULL ; return 0 ; } if (place->flags & TTM_PL_FLAG_CONTIGUOUS) { pages_per_node = ~0ul ; num_nodes = 1 ; } else { #ifdef CONFIG_TRANSPARENT_HUGEPAGE pages_per_node = HPAGE_PMD_NR; #else pages_per_node = (2UL << (20UL - PAGE_SHIFT)); #endif pages_per_node = max((uint32_t )pages_per_node, mem->page_alignment); num_nodes = DIV_ROUND_UP(mem->num_pages, pages_per_node); } nodes = kvmalloc_array((uint32_t )num_nodes, sizeof (*nodes), GFP_KERNEL | __GFP_ZERO); if (!nodes) { atomic64_sub(mem_bytes, &mgr->usage); return -ENOMEM; } mode = DRM_MM_INSERT_BEST; if (place->flags & TTM_PL_FLAG_TOPDOWN) mode = DRM_MM_INSERT_HIGH; mem->start = 0 ; pages_left = mem->num_pages; spin_lock(&mgr->lock); for (i = 0 ; pages_left >= pages_per_node; ++i) { unsigned long pages = rounddown_pow_of_two(pages_left); r = drm_mm_insert_node_in_range(mm, &nodes[i], pages, pages_per_node, 0 , place->fpfn, lpfn, mode); if (unlikely(r)) break ; vis_usage += amdgpu_vram_mgr_vis_size(adev, &nodes[i]); amdgpu_vram_mgr_virt_start(mem, &nodes[i]); pages_left -= pages; } for (; pages_left; ++i) { unsigned long pages = min(pages_left, pages_per_node); uint32_t alignment = mem->page_alignment; if (pages == pages_per_node) alignment = pages_per_node; r = drm_mm_insert_node_in_range(mm, &nodes[i], pages, alignment, 0 , place->fpfn, lpfn, mode); if (unlikely(r)) goto error; vis_usage += amdgpu_vram_mgr_vis_size(adev, &nodes[i]); amdgpu_vram_mgr_virt_start(mem, &nodes[i]); pages_left -= pages; } spin_unlock(&mgr->lock); atomic64_add(vis_usage, &mgr->vis_usage); mem->mm_node = nodes; return 0 ; error: while (i--) drm_mm_remove_node(&nodes[i]); spin_unlock(&mgr->lock); atomic64_sub(mem->num_pages << PAGE_SHIFT, &mgr->usage); kvfree(nodes); return r == -ENOSPC ? 0 : r; }
虽然都是通过drm_mm_insert_node_in_range()
的DRM helper实现的地址分配,但是分配细节上还是有一些不同的。gtt mgr
分配的是虚拟地址,是要求地址连续的,因此驱动只需要准备一个node
就可以接收分配结果,但是当分配VRAM的时候则没有这种要求,drm_mm
可以返回一组node
,这些node
的总容量满足分配要求就可以,但是有时候内核也需要分配一段连续的显存给GPU使用,这个时候就只需要分配一个node
就可以(参考 TTM_PL_FLAG_CONTIGUOUS)。 当需要分配的size
大于2M
的时候,KMD会将其拆开多个2M
的node,因为2M
的node恰好满足huge page
size,且GPU页表对2M
的size有特殊优化。
总结 相信你读到这里,已经对整个GPU内存管理有一个大概的了解,后面会更加深入的介绍AMD GPU内存管理的技术细节。
GPU的页表管理
CPU/GTT/VRAM状态的流转
GPU Page Fault
UMD Access VRAM
GPU VRAM Evict
GPU P2P (Peer To Peer)
IOMMU下的AMDGPU
…..