AMD GPU 虚拟内存

本文所讨论的均为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, 0x100000000ULL);
amdgpu_vamgr_init(&dev->vamgr_32, start, max,
dev->dev_info.virtual_address_alignment);

start = max;
max = MAX2(dev->dev_info.virtual_address_max, 0x100000000ULL);
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 & ~0xffffffffULL) +
0x100000000ULL);
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 & ~0xffffffffULL) +
0x100000000ULL);
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)

amdgpu_virtual_address_kmd-Gart

在内核态不需要考虑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
/**
* amdgpu_gtt_mgr_alloc - allocate new ranges
*
* @man: TTM memory type manager
* @tbo: TTM BO we need this range for
* @place: placement flags and restrictions
* @mem: the resulting mem object
*
* Allocate the address space for a node.
*/
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
/**
* amdgpu_vram_mgr_new - allocate new ranges
*
* @man: TTM memory type manager
* @tbo: TTM BO we need this range for
* @place: placement flags and restrictions
* @mem: the resulting mem object
*
* Allocate VRAM for the given BO.
*/
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;

/* bail out quickly if there's likely not enough VRAM for this BO */
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
/* default to 2MB */
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
  • …..
作者

Wang Yang

发布于

2020-07-05

更新于

2020-07-05

许可协议