1. 背景与核心原则
AMDGPU 驱动在做 RAM 和 VRAM 之间的数据迁移时,会通过 SDMA 引擎提交 buffer copy。对很多开发者来说,最容易混淆的是:SDMA 命令里的 src 和 dst 到底是什么地址?它们为什么有时看起来像 VRAM offset,有时又像 GART 地址?系统内存的 DMA address 为什么不能直接塞给 SDMA?
核心原则:SDMA copy packet 里的地址是 GPU MC/VMID0 可解析地址,不是 CPU 虚拟地址、CPU 物理地址或裸 DMA address。
理解这条原则需要先理解 SDMA 的硬件位置和它的地址解析路径(第 2 节)。在此基础上,才能理解为什么 RAM 和 VRAM 两侧的地址计算方式完全不同(第 3-5 节)。
2. 为什么:SDMA 的硬件位置与地址解析路径
这是理解整条地址转换链路最重要的问题。根本原因是:SDMA 是 GPU 内部的硬件引擎,它发出的内存访问请求首先进入 GPU 的地址解析体系,而不是 CPU 的地址解析体系。
2.1 SDMA 不在 CPU 上
SDMA 是 GPU die 里的 DMA engine。它不是 Linux kernel 的 memcpy(),也不是 CPU 侧的 DMA controller。它发出的读写请求走 GPU internal fabric,先到 GPU memory controller。硬件路径是:
SDMA packet address
-> GPU GMC/MMHUB address decode
-> 判断地址落在哪个 GPU aperture 或 GPU VM range
-> 访问 VRAM,或者通过 GART / GPU 页表访问系统内存
这和 CPU load/store 的地址路径不同:
CPU load/store:
CPU VA -> CPU page table -> CPU physical address -> RAM
SDMA copy:
SDMA packet address -> GPU GMC/MMHUB -> VRAM or GART/system memory
因此 CPU VA、CPU page table、CPU physical address 对 SDMA 都不是天然可见的。
2.2 GPU MC 地址空间布局
GPU MC 地址空间中划分了几个不同的 aperture:
GPU MC address space (48-bit)
+————————————————————-+
| |
| [gmc.vram_start, gmc.vram_end] |
| VRAM / FB aperture |
| SDMA accesses local VRAM directly |
| |
| [gmc.gart_start, gmc.gart_end] |
| GART aperture |
| SDMA address is translated through GART page table |
| to system-memory DMA addresses |
| |
| [gmc.agp_start, gmc.agp_end] |
| AGP aperture, used by specific platform paths |
| |
+————————————————————-+
在 amdgpu_gmc.h 中,注释也明确区分了 CPU 看到的 framebuffer MMIO aperture 和 GPU 看到的 gart_start / vram_start:
- aper_base / aper_size 是 CPU MMIO 视角,用于 CPU map framebuffer。
- gmc.vram_start / gmc.vram_end 是 GPU 视角下的 VRAM aperture。
- gmc.gart_start / gmc.gart_end 是 GPU 视角下的 GART aperture。
SDMA copy packet 关心的是后两者。
2.3 VRAM 为什么可以直接寻址
VRAM 本来就在 GPU MC 地址空间里。VRAM 通过 HBM/GDDR 控制器直接连接在 GPU die 上,MC 对 VRAM 有直接的物理总线访问。GMC 知道本地显存 aperture 的范围,所以驱动只需要:
sdma_vram_addr = vram_offset + gmc.vram_start
这个地址落在 GPU 的 VRAM aperture 内,SDMA/GMC 直接解释为"访问本 GPU 的 VRAM 某个位置"。
2.4 RAM 为什么必须经过 GART
系统内存挂在 CPU memory controller 后面,GPU 要访问它,必须通过 PCIe、xGMI、IOMMU 或平台互连。Linux DMA API 给驱动的 dma_addr_t 描述的是设备发起 DMA transaction 时应该访问的 bus address / IOVA,但 SDMA packet 的地址字段仍然需要是 GPU 侧可解析地址。
因此 AMDGPU 用 GART 做桥接:
GART MC address
-> GART PTE lookup
-> PTE contains DMA address / IOVA
-> GPU issues PCIe/xGMI/system-memory transaction
正确链路是:
SDMA src/dst = GART window MC address
GART PTE = DMA address / IOVA | PTE flags
而不是:
SDMA src/dst = dma_addr_t // WRONG!
2.5 为什么不能把 DMA address 直接给 SDMA
DMA address 属于设备 DMA / bus address domain。它描述的是"设备发起 DMA transaction 时应该访问哪个主机总线地址"。但 SDMA packet 的地址字段首先进入 GPU GMC 的地址解析逻辑。GMC 不会把一个任意 DMA address 当作系统内存地址直接发出去。
SDMA 需要一个 GPU MC address。只有当这个 MC address 落在 GART aperture 里时,GMC 才会查 GART PTE,并从 PTE 中拿到真正的 DMA address,然后发起 PCIe 事务。
SDMA packet address
= GART MC address
-> GMC detects address is inside GART aperture
-> GART PTE lookup
-> PTE contains DMA address / IOVA
-> GPU issues PCIe/xGMI/system-memory transaction
一句话总结:SDMA 看到 GPU 地址空间,是因为 SDMA 是 GPU 内部引擎,它的地址字段由 GPU 的 GMC/MMHUB 解析;VRAM 通过 VRAM aperture 直接可见,RAM 则必须先通过 GART 映射成 GPU 地址空间中的一段地址。
3. 全景:RAM↔VRAM copy 的端到端路径
在深入两侧的地址计算细节之前,先看完整的数据搬运路径全貌。

3.1 RAM→VRAM 完整流程
代码路径:
migration framework
-> copy_to_vram()
-> build sys[] from dma_addr[i].addr
-> build vram[] from ZONE_DEVICE pages
-> amdgpu_copy_memory_gart(…, FROM_RAM_TO_VRAM)
-> amdgpu_gart_map(…, sys, &gart_s, flags=0)
-> gart_d = amdgpu_direct_mapping_addr(adev, *vram)
-> amdgpu_copy_buffer(adev, entity, gart_s, gart_d, size, …)
核心代码:
sys[j] = pagemap_addr[i].addr;
vram[j] = ((u64)page_to_pfn(pages[i]) << PAGE_SHIFT) –
svm_dm->hpa_base;
r = amdgpu_gart_map(ring, entity, size, sys,
&gart_s, 0);
gart_d = amdgpu_direct_mapping_addr(adev, *vram);
r = amdgpu_copy_buffer(adev, entity, gart_s, gart_d,
size * PAGE_SIZE,
NULL, &next, true, 0);
端到端数据流图:
Application VA
-> CPU page table
-> system struct page
-> DMA API (dma_map_page)
-> dma_addr_t sys[i]
-> written into GART PTE
-> SDMA src = gmc.gart_start + gart_window_offs[0]
ZONE_DEVICE dev_page
-> page_to_pfn(dev_page) << PAGE_SHIFT
-> HPA
-> HPA – hpa_base
-> vram_offset
-> SDMA dst = vram_offset + gmc.vram_start
SDMA COPY(src = GART MC address, dst = VRAM MC address, size)
3.2 VRAM→RAM 完整流程
VRAM→RAM 与 RAM→VRAM 镜像对称,只是 source/destination 互换:
核心区别:
| RAM→VRAM | GART MC address | VRAM MC address | No |
| VRAM→RAM | VRAM MC address | GART MC address | Yes |
4. VRAM 侧地址详解
这里以KFD SVM的实现代码进行分析。
4.1 从 BO 分配到 ZONE_DEVICE PFN
SVM 迁移到 VRAM 时,驱动先分配一个 VRAM BO。BO 的物理显存位置由 TTM VRAM manager / buddy allocator 决定,表现为一个或多个 VRAM resource block。
KFD会把这些 VRAM block 转成 Linux migration framework 能理解的 ZONE_DEVICE PFN:
u64 pfn_base = PHYS_PFN(cursor.start + svm_dm->hpa_base);
pfn[i] = pfn_base + j;
这里:
- cursor.start 是 VRAM allocation 内部 offset。
- svm_dm->hpa_base 是 dev_pagemap 注册 ZONE_DEVICE 时使用的 host physical base。
- cursor.start + hpa_base 得到一个 Linux 可表示的 HPA。
- HPA 再转成 PFN,交给 migrate_vma_pages()。
注意:这个 PFN/HPA 是给 Linux 内核迁移框架和 struct page 管理使用的,不是 SDMA 地址。
4.2 从 ZONE_DEVICE page 反推 VRAM offset
当 copy_to_vram() 被调用时,参数 pages[i] 是目标 ZONE_DEVICE page。驱动需要从 page 反推它对应的 VRAM offset:
vram[j] = ((u64)page_to_pfn(pages[i]) << PAGE_SHIFT) – svm_dm->hpa_base;
公式是:
HPA = page_to_pfn(dev_page) << PAGE_SHIFT
VRAM offset = HPA – hpa_base
这一步只是把 Linux 的 ZONE_DEVICE page 表示还原为真实 VRAM offset。
4.3 从 VRAM offset 变成 SDMA 可用的 VRAM MC address
SDMA 不能直接使用 vram_offset。它需要的是 MC address,所以驱动继续加上 VRAM aperture base:
static u64 amdgpu_direct_mapping_addr(struct amdgpu_device *adev,
u64 vram_offset)
{
return vram_offset + amdgpu_ttm_domain_start(adev, TTM_PL_VRAM);
}
而 amdgpu_ttm_domain_start(adev, TTM_PL_VRAM) 返回:
case TTM_PL_VRAM:
return adev->gmc.vram_start;
所以最终公式是:
VRAM SDMA address = (page_to_pfn(dev_page) << PAGE_SHIFT)
– hpa_base
+ gmc.vram_start
或者分两步更清楚:
vram_offset = HPA – hpa_base
sdma_vram_addr = vram_offset + gmc.vram_start
5. RAM 侧地址详解
5.1 系统内存先被 DMA API 映射
RAM 侧输入来自 dma_addr。对 RAM->VRAM copy,是系统页面的 DMA address:
sys[j] = dma_addr[i].addr;
这里的 DMA address 可能等于 CPU physical address,也可能是 IOMMU 分配的 IOVA。驱动不能假设它是 CPU 物理地址,也不能把它当成 GPU MC address。
5.2 GART window 的 MC address
驱动使用预分配的 GART window。窗口地址由 amdgpu_compute_gart_address() 计算:
static inline u64 amdgpu_compute_gart_address(struct amdgpu_gmc *gmc,
struct amdgpu_ttm_buffer_entity *entity,
int index)
{
return gmc->gart_start + entity->gart_window_offs[index];
}
在 SVM migration copy 路径中,使用 window 0:
*gart_addr = amdgpu_compute_gart_address(&adev->gmc, entity, 0);
这就是 SDMA packet 中 RAM 侧的 src 或 dst。
5.3 GART PTE 如何指向系统内存
amdgpu_gart_map() 会生成一批 GART PTE,并用 SDMA job 把它们写到 GART page table 对应位置:
pte_flags = AMDGPU_PTE_VALID | AMDGPU_PTE_READABLE;
pte_flags |= AMDGPU_PTE_SYSTEM | AMDGPU_PTE_SNOOPED;
if (flags & AMDGPU_PTE_WRITEABLE)
pte_flags |= AMDGPU_PTE_WRITEABLE;
amdgpu_gart_map(adev, 0, npages, addr, pte_flags, cpu_addr);
其中 addr 就是 dma_addr_t *sys 数组。
GART PTE 的逻辑内容可以理解为:
GART PTE[N] = DMA_addr_of_sys_page_0 | VALID | READABLE | SYSTEM | SNOOPED
GART PTE[N+1] = DMA_addr_of_sys_page_1 | VALID | READABLE | SYSTEM | SNOOPED
…
对 RAM->VRAM copy,RAM 是 source,通常不需要 WRITEABLE。对 VRAM->RAM copy,RAM 是 destination,因此 GART PTE 需要 AMDGPU_PTE_WRITEABLE。
6. 容易混淆的两个 VRAM base
这两个字段经常被混淆。它们都可能看起来像"VRAM base",但使用场景不同。
| adev->gmc.vram_start | VMID0 / MC address space | SDMA、kernel move/copy 这类直接 MC 访问 |
| adev->vm_manager.vram_base_offset | GPU VM page table / MMHUB FB offset | 用户态 GPU PTE 中填入 VRAM page address |
在 amdgpu_svm_device_map() 中,驱动给 GPU PTE 返回的是:
addr = vram_offset + adev->vm_manager.vram_base_offset;
这是给 shader / user queue 在 VMID>0 下访问 migrated VRAM page 用的,不是给 SDMA copy packet 用的。
在 amdgpu_copy_memory_gart() 中,SDMA 复制 VRAM 数据使用的是:
gart_d = amdgpu_direct_mapping_addr(adev, *vram);
也就是 vram_offset + adev->gmc.vram_start。
两者可能在某些 ASIC/配置下数值相近甚至相等,但概念上不能混用。判断方法很简单:
- 写 GPU VM PTE:看 vm_manager.vram_base_offset。
- 给 SDMA copy packet:看 gmc.vram_start 或 amdgpu_ttm_domain_start(…, TTM_PL_VRAM)。
7. 实现细节
7.1 GART window 分批机制与锁保护
amdgpu_svm_copy_memory_gart() 每次最多处理 AMDGPU_GTT_MAX_TRANSFER_SIZE 页:
const u64 max_pages = AMDGPU_GTT_MAX_TRANSFER_SIZE;
size = min(max_pages, npages);
原因是 GART window 是一段固定大小的临时映射区域。驱动不是为每次 copy 动态分配无限大的 GART aperture,而是复用一个有限窗口:
batch 0:
GART window PTE -> sys[0..255]
SDMA copy 1MB
batch 1:
same GART window PTE -> sys[256..511]
SDMA copy next 1MB
…
这也是为什么代码里有:
mutex_lock(&entity->lock);
...
mutex_unlock(&entity->lock);
GART window 是共享资源,映射和 copy 期间需要互斥保护,否则不同 copy 操作可能互相改写窗口 PTE。
7.2 VRAM 连续性检查与 flush 策略
amdgpu_copy_to_vram() 会把连续页面合并成一个 SDMA copy。但这个连续性检查看的是 VRAM offset:
if (j > 0 && vram[j] != vram[j – 1] + PAGE_SIZE)
goto flush;
原因是 amdgpu_copy_buffer() 一次 copy 使用的是线性递增的 source/destination 地址。GART window 本身可以把一组系统页面映射成连续的 GART MC address,即使原始系统页面 DMA address 不连续也没关系,因为连续性由 GART PTE 提供。
但是 VRAM 侧没有一层 per-page remap。vram_offset + gmc.vram_start 是直接线性地址。如果两个目标 VRAM pages 在物理 VRAM 中不连续,就不能合并成一次线性 SDMA copy,必须 flush 前一批,再从新的 VRAM address 开始。
因此:
- RAM 侧可以通过 GART PTE 把非连续 DMA pages 映射成连续 GART window。
- VRAM 侧必须物理 VRAM offset 连续,才能合并为一次 SDMA copy。
8. 数值例子
假设:
gmc.gart_start = 0x0000_4000_0000_0000
entity->gart_window_offs[0] = 0x0000_0000_0200_0000
gmc.vram_start = 0x0000_8000_0000_0000
hpa_base = 0x0001_0000_0000_0000
dev_page PFN gives HPA = 0x0001_0000_1234_0000
pagemap_addr[0].addr = 0x0000_00ab_cdef_0000
VRAM 侧:
vram_offset = HPA – hpa_base
= 0x0001_0000_1234_0000 – 0x0001_0000_0000_0000
= 0x0000_0000_1234_0000
SDMA VRAM address = vram_offset + gmc.vram_start
= 0x0000_0000_1234_0000 + 0x0000_8000_0000_0000
= 0x0000_8000_1234_0000
RAM 侧:
GART window address = gmc.gart_start + gart_window_offs[0]
= 0x0000_4000_0000_0000 + 0x0000_0000_0200_0000
= 0x0000_4000_0200_0000
GART PTE[window_page_0] = 0x0000_00ab_cdef_0000 | VALID | READABLE | SYSTEM | SNOOPED
最终 SDMA packet:
COPY src = 0x0000_4000_0200_0000 (GART MC address -> 通过PTE访问系统内存)
dst = 0x0000_8000_1234_0000 (VRAM MC address -> 直接访问显存)
size = PAGE_SIZE
注意:0x0000_00ab_cdef_0000 这个 DMA address 没有直接出现在 SDMA copy packet 中。它出现在 GART PTE 中。
9. 常见误区
9.1 “SDMA src 是系统物理地址”
不是。RAM->VRAM 时,SDMA src 是 GART MC address。系统页面的 DMA address 被写进 GART PTE。
9.2 “DMA address 可以直接给 SDMA”
通常不可以。DMA address 是总线/IOMMU 视角;SDMA packet 地址是 GPU GMC 视角。中间需要 GART aperture 建立桥接。
9.3 “ZONE_DEVICE PFN 就是 VRAM 物理地址”
它是 Linux 内核为设备内存建立的 struct page 表示。它可以通过 hpa_base 反推 VRAM offset,但它不是 SDMA 使用的地址。
9.4 "vram_base_offset 和 gmc.vram_start "
vram_base_offset 用于 GPU VM PTE;gmc.vram_start 用于 VMID0/SDMA 直接 MC 访问。
9.5 “RAM pages 必须 DMA 连续才能一次 copy”
不一定。RAM 侧可以通过 GART PTE 映射成连续 GART window。一次 copy 真正要求的是 SDMA packet 中的地址线性连续:GART window 连续,VRAM offset 也连续。
10. 调试建议
10.1 打印关键地址
建议在 RAM->VRAM 路径中打印:
pr_debug("sys_dma=%pad vram_off=%#llx gart_src=%#llx vram_dst=%#llx\\n",
&sys[j], vram[j], gart_s, gart_d);
关键检查:
- vram[j] 是否小于 adev->gmc.real_vram_size。
- gart_s 是否落在 [gmc.gart_start, gmc.gart_end]。
- gart_d 是否落在 [gmc.vram_start, gmc.vram_end]。
- VRAM batch 内 vram[j] == vram[j – 1] + PAGE_SIZE。
10.2 检查 GART PTE flags
RAM source:
VALID | READABLE | SYSTEM | SNOOPED
RAM destination:
VALID | READABLE | WRITEABLE | SYSTEM | SNOOPED
如果 VRAM->RAM 出现写失败或数据未回写,优先检查 destination GART PTE 是否带 WRITEABLE。
10.3 区分 copy fault 和 mapping fault
如果 GPU shader 访问 migrated page 出问题,优先看 device_map() 和 GPU VM PTE 地址,即 vram_offset + vm_manager.vram_base_offset。
如果 SDMA copy 本身失败或数据错误,优先看 amdgpu_svm_copy_memory_gart(),即 GART window 和 vram_offset + gmc.vram_start。
这两个路径虽然都从同一个 ZONE_DEVICE page 反推出 vram_offset,但后续 base 不同。
11. 总结
SDMA 地址理解可以压缩成三句话:
最终公式:
RAM->VRAM:
src = gmc.gart_start + entity->gart_window_offs[0]
dst = ((page_to_pfn(dev_page) << PAGE_SHIFT) – hpa_base) + gmc.vram_start
VRAM->RAM:
src = ((page_to_pfn(dev_page) << PAGE_SHIFT) – hpa_base) + gmc.vram_start
dst = gmc.gart_start + entity->gart_window_offs[0]
GPU PTE for migrated VRAM page:
pte_addr = ((page_to_pfn(dev_page) << PAGE_SHIFT) – hpa_base)
+ vm_manager.vram_base_offset
把这三条分清楚,RAM/VRAM 两侧地址的计算逻辑就不会混淆。终于写完这一节,我的脑子也有点绕不过来了^ ^
附录 A: 术语速查表
| CPU VA | 应用或内核看到的虚拟地址 | 否 |
| CPU physical address | CPU 物理地址空间中的地址 | 否 |
| DMA address / IOVA | 设备通过 DMA API 得到的总线地址,可能经过 IOMMU 翻译 | 否,需先写入 GART PTE |
| MC address | GPU Memory Controller 侧地址空间中的地址 | 是 |
| GART address | MC 地址空间中 GART aperture 内的地址 | 是 |
| VRAM offset | VRAM 内部从 0 开始的字节偏移 | 否,需加 VRAM base |
| VRAM MC address | vram_offset + gmc.vram_start | 是 |
| HPA / ZONE_DEVICE PFN | Linux 为 VRAM page 建立的 host physical address / PFN 表示 | 否 |
| vram_base_offset | VM page table 中用于 VRAM PTE 的 base,常来自 MMHUB FB_OFFSET | 不用于 SDMA copy packet |
| gmc.vram_start | SDMA/VMID0 看到的 VRAM MC aperture 起点 | 用于 SDMA VRAM 地址 |


