难度: 🔴 高级 预计学习时间: 3小时 前置知识: 第1-10章,特别是第9、10章
📋 概述
Checkpoint Timestamp (检查点时间戳,简称CTS) 是AMDGPU SVM子系统中的一个精巧的同步机制。想象一下:
- 🕐 时间屏障: 像路障一样阻止"新来的"页面错误
- ⏳ 精确控制: 基于纳秒级硬件时间戳
- 🔒 无锁设计: 避免锁开销,中断上下文安全
- 🎯 问题解决: 防止内存操作期间的竞争条件
本章深入探讨checkpoint_ts机制的设计、实现和应用。
15.1 为什么需要Checkpoint Timestamp
问题场景
想象一个危险的竞争场景:
时刻T1: 用户调用 munmap(addr, size)
驱动开始取消映射…
时刻T2: GPU 正好访问 addr
→ 触发页面错误
→ 驱动尝试恢复页面
时刻T3: 页面恢复完成
但munmap还在进行!
时刻T4: munmap 继续取消映射
→ 系统状态混乱!
问题本质:
为什么不能用锁?
// 错误尝试
mutex_lock(&some_lock);
unmap_pages();
mutex_unlock(&some_lock);
// 页面错误处理(在中断上下文)
handle_page_fault() {
mutex_lock(&some_lock); // 💥 中断上下文不能睡眠!
restore_page();
mutex_unlock(&some_lock);
}
锁机制的问题:
- 页面错误处理在中断上下文,不能使用会睡眠的锁
- Spin lock 性能差,且难以跨硬件边界
- 锁粒度难以控制
Checkpoint Timestamp 的解决方案
使用时间戳作为"屏障":
T0: munmap 开始,设置 checkpoint_ts = 当前时间戳
↓
├─ T1之前的页面错误(旧的): 允许处理 ✅
│
└─ T0之后的页面错误(新的): 丢弃 ❌
munmap 完成后,清除 checkpoint_ts
优势:
- ✅ 无锁: 单个64位写入是原子的
- ✅ 精确: 基于硬件时间戳,纳秒级精度
- ✅ 中断安全: 可在任何上下文使用
- ✅ 高性能: O(1)时间复杂度
一句话总结:checkpoint_ts 是 AMD KFD SVM 子系统中的一个关键同步机制,用于在内存管理操作期间同步和过滤 GPU 页面错误(page fault)处理。
15.2 数据结构
checkpoint_ts 字段
struct svm_range_list {
// 其他字段…
/* check point ts decides if page fault recovery need be dropped */
uint64_t checkpoint_ts[MAX_GPU_INSTANCE];
// 其他字段…
};
设计要点:
数组类型: 每个GPU独立的时间戳
├─ GPU 0: checkpoint_ts[0]
├─ GPU 1: checkpoint_ts[1]
└─ …
值的含义:
├─ 0: 无活跃检查点
└─ 非0: 检查点时间戳(48位)
中断处理环 (IH Ring)
时间戳来源于GPU的中断处理环:
struct amdgpu_ih_ring {
unsigned ring_size; // 环形缓冲区大小
uint32_t *ring; // 环形缓冲区
unsigned rptr; // 读指针
uint64_t wptr_addr; // 写指针地址
uint32_t *wptr_cpu; // 写指针CPU地址
// 用于checkpoint等待
wait_queue_head_t wait_process;
uint64_t processed_timestamp; // 已处理的时间戳
};
AMD GPU 使用多个中断环:
| ih | 主中断环,处理所有类型的中断 | 一般中断 |
| ih1 | 页面错误中断环,专门处理retry page fault | ✅ 主要使用 |
| ih_soft | 软件CAM,CAM (Content Addressable Memory) retry fault | ✅ CAM启用时 |
中断环中存放着中断向量(IV)条目,每个中断向量占32字节:
Offset DW 内容
—— — ———————————
+0 DW0 client_id | src_id | ring_id | vmid
+4 DW1 timestamp[31:0] ← 低32位
+8 DW2 timestamp[47:32] | ← 高16位
+12 DW3 pasid | node_id
+16 DW4 src_data[0]
+20 DW5 src_data[1]
+24 DW6 src_data[2]
+28 DW7 src_data[3]
时间戳特点:
- 48位: 足够长,回绕时间 > 数天
- 硬件生成: GPU自动在IV中填入
- 单调递增: 每个新IV的时间戳都更大
15.3 核心操作
该同步机制只有三个操作:设置检查点、检查和过滤、等待排空。重点理解cts设置、使用与清除的操作时机。
操作1: 设置检查点
场景: 在取消映射前获取ih中的最后一个IV的ts,设置到svms的cts。
static void svm_range_unmap_from_cpu(struct mm_struct *mm,
struct svm_range *prange,
unsigned long start,
unsigned long last)
{
struct svm_range_list *svms = prange->svms;
struct kfd_process *p = container_of(svms, struct kfd_process, svms);
/* 为每个GPU设置checkpoint */
for_each_set_bit(i, svms->bitmap_supported, p->n_pdds) {
struct kfd_process_device *pdd = p->pdds[i];
struct amdgpu_device *adev = pdd->dev->adev;
struct amdgpu_ih_ring *ih;
uint32_t checkpoint_wptr;
/* 步骤1: 检查硬件retry fault ring (ih1) */
if (adev->irq.ih1.ring_size) {
ih = &adev->irq.ih1;
checkpoint_wptr = amdgpu_ih_get_wptr(adev, ih);
// Ring非空:获取最新时间戳
if (ih->rptr != checkpoint_wptr) {
svms->checkpoint_ts[i] =
amdgpu_ih_decode_iv_ts(adev, ih, checkpoint_wptr, –1);
continue;
}
}
/* 步骤2: 检查软件CAM ring (ih_soft) */
if (adev->irq.ih_soft.ring_size) {
ih = &adev->irq.ih_soft;
checkpoint_wptr = amdgpu_ih_get_wptr(adev, ih);
if (ih->rptr != checkpoint_wptr)
svms->checkpoint_ts[i] =
amdgpu_ih_decode_iv_ts(adev, ih, checkpoint_wptr, –1);
}
}
/* 步骤3: 执行实际的取消映射 */
// … unmap operations …
}
流程图:
svm_range_unmap_from_cpu()
↓
对每个GPU:
├─ 获取 ih1.wptr
├─ wptr != rptr? (ring非空)
│ ├─ 是 → 解码wptr位置的时间戳
│ │ └─ checkpoint_ts[i] = 时间戳
│ └─ 否 → 检查 ih_soft
│ └─ 重复上述逻辑
↓
执行 GPU unmap 操作
关键点:
- 只在ring非空时设置(rptr != wptr)
- 使用 offset = -1 获取wptr位置的最后一个IV
- 优先级:ih1 > ih_soft
操作2: 检查和过滤
场景: 页面错误处理时,检查是否应该处理。
int svm_range_restore_pages(struct amdgpu_device *adev,
unsigned int pasid,
uint32_t vmid, uint32_t node_id,
uint64_t addr, uint64_t ts,
bool write_fault)
{
struct svm_range_list *svms;
int32_t gpuidx;
int r = 0;
// … 初始化和查找svms …
mutex_lock(&svms->lock);
/* 检查checkpoint */
if (svms->checkpoint_ts[gpuidx] != 0) {
// 判断:错误时间戳 >= checkpoint时间戳?
if (amdgpu_ih_ts_after_or_equal(ts, svms->checkpoint_ts[gpuidx])) {
/* 情况1: 新错误 → 返回-EAGAIN,GPU将重试 */
pr_debug("draining retry fault, drop fault 0x%llx\\n", addr);
r = –EAGAIN;// GPU会自动重试
goto out_unlock_svms;
} else {
/* 情况2: 旧错误 → 处理并清零 */
svms->checkpoint_ts[gpuidx] = 0;
}
}
/* 正常处理页面错误 */
prange = svm_range_from_addr(svms, addr, NULL);
// … 恢复页面 …
out_unlock_svms:
mutex_unlock(&svms->lock);
return r;
}
决策树:
checkpoint_ts == 0?
├─ 是 → 无检查点,正常处理
│
└─ 否 → 有检查点
├─ fault_ts >= checkpoint_ts?
│ ├─ 是 → 返回 -EAGAIN(新错误,暂时延迟处理)
│ │ GPU看到-EAGAIN会自动重试 🔄
│ │ 新的fault在checkpoint清除后被处理 ✅
│ │
│ └─ 否 → 处理,清零checkpoint_ts(旧错误,在unmap开始前)
└─
操作3: 等待排空
场景: 确保checkpoint之前的所有页面错误都已处理。
int amdgpu_ih_wait_on_checkpoint_process_ts(
struct amdgpu_device *adev,
struct amdgpu_ih_ring *ih)
{
uint32_t checkpoint_wptr;
uint64_t checkpoint_ts;
long timeout = HZ; // 1秒超时
if (!ih->enabled || adev->shutdown)
return –ENODEV;
/* 步骤1: 获取当前写指针 */
checkpoint_wptr = amdgpu_ih_get_wptr(adev, ih);
/* 步骤2: 内存屏障 */
rmb();
/* 步骤3: 解码checkpoint时间戳 */
checkpoint_ts = amdgpu_ih_decode_iv_ts(adev, ih, checkpoint_wptr, –1);
/* 步骤4: 等待条件满足 */
bool con = amdgpu_ih_ts_after(checkpoint_ts, ih->processed_timestamp) ||
ih->rptr == amdgpu_ih_get_wptr(adev, ih);
return wait_event_interruptible_timeout(ih->wait_process, con, timeout);
}
等待条件:
满足任一条件即返回:
1. processed_timestamp > checkpoint_ts
→ checkpoint之前的IV都已处理
2. rptr == wptr
→ Ring已空,所有IV都已处理
3. 超时 (1秒)
→ 可能硬件卡死
调用路径:
static void svm_range_drain_retry_fault(struct svm_range_list *svms)
{
struct kfd_process *p = container_of(svms, struct kfd_process, svms);
for_each_set_bit(i, svms->bitmap_supported, p->n_pdds) {
struct kfd_process_device *pdd = p->pdds[i];
/* 等待硬件ring */
amdgpu_ih_wait_on_checkpoint_process_ts(pdd->dev->adev,
pdd->dev->adev->irq.retry_cam_enabled ?
&pdd->dev->adev->irq.ih :
&pdd->dev->adev->irq.ih1);
/* 等待软件CAM ring */
if (pdd->dev->adev->irq.retry_cam_enabled)
amdgpu_ih_wait_on_checkpoint_process_ts(pdd->dev->adev,
&pdd->dev->adev->irq.ih_soft);
}
}
15.4 时间戳比较
48位时间戳设计
/* 检查 t1 是否在 t2 之后(严格大于) */
#define amdgpu_ih_ts_after(t1, t2) \\
(((int64_t)((t2) << 16) – (int64_t)((t1) << 16)) > 0LL)
/* 检查 t1 是否在 t2 之后或相等 */
#define amdgpu_ih_ts_after_or_equal(t1, t2) \\
(((int64_t)((t2) << 16) – (int64_t)((t1) << 16)) >= 0LL)
为什么左移16位?
原始48位时间戳:
0x0000XXXXXXXXXXXX
└─ 低48位有效
左移16位后:
0xXXXXXXXXXXXX0000
└─ 高48位有效
作为有符号int64_t:
– 正数范围: 0 ~ 2^47-1
– 负数范围: 2^47 ~ 2^48-1
– 差值运算自动处理回绕
时间戳回绕处理
示例1: 正常情况
t1 = 0x000000001000 // checkpoint
t2 = 0x000000002000 // fault
比较: amdgpu_ih_ts_after_or_equal(t2, t1)
= ((t1 << 16) – (t2 << 16)) >= 0
= (0x10000000 – 0x20000000) >= 0
= –0x10000000 >= 0
= false
结论: t2 不在 t1 之后或等于 → 处理页面错误
示例2: 回绕情况
t1 = 0xFFFFFFFFFFFF // checkpoint (接近最大值)
t2 = 0x000000000100 // fault (回绕后)
左移16位:
t1 << 16 = 0xFFFFFFFFFFFF0000
t2 << 16 = 0x0000001000000000
作为有符号整数:
t1 << 16 = –65536 (负数!)
t2 << 16 = 4294967296 (正数)
差值:
(t1 << 16) – (t2 << 16) = 负数 – 正数 = 更负
比较:
差值 >= 0? 否
结论: t2 在 t1 之后 → 丢弃页面错误
关键点:
- 左移16位将48位时间戳扩展为有符号64位
- 有符号整数运算自动处理溢出
- 支持2^48个时钟周期的回绕(数天到数周)
15.5 完整工作流程
流程图
┌─────────────────────────────────────┐
│ 用户空间: munmap(addr, size) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ MMU Notifier 回调 │
│ svm_range_cpu_invalidate_pagetables│
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤1: 设置检查点 │
│ svm_range_unmap_from_cpu │
│ ├─ 获取 ih.wptr │
│ ├─ 解码时间戳 │
│ └─ checkpoint_ts[i] = 时间戳 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤2: 取消GPU映射 │
│ svm_range_unmap_from_gpus │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤3: 排空旧的页面错误 │
│ svm_range_drain_retry_fault │
│ └─ amdgpu_ih_wait_on_… │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 步骤4: 取消CPU映射 │
│ unmap_single_vma() │
└─────────────────────────────────────┘
↓
完成
并发: GPU 访问同一地址
↓
┌─────────────────────────────────────┐
│ GPU 页面错误 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 硬件生成 IV,写入 IH Ring │
│ IV.timestamp = 当前时间 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 中断处理: amdgpu_ih_process │
│ 读取 IV,分发处理 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ svm_range_restore_pages │
│ ├─ 检查 checkpoint_ts │
│ ├─ IV.ts >= checkpoint_ts? │
│ │ ├─ 是 → 丢弃 (-EAGAIN) │
│ │ └─ 否 → 处理并清零checkpoint │
│ └─ 恢复页面映射 │
└─────────────────────────────────────┘
时间线示例
时刻 操作
────────────────────────────────────────────────
T0 GPU访问addr → 产生fault (ts=T0)
IV进入IH Ring,等待处理
T1 CPU: munmap(addr)
设置 checkpoint_ts = T1
T2 GPU再次访问addr → 产生fault (ts=T2, T2>T1)
IV进入IH Ring
T3 处理T0时刻的fault:
– 检查: T0 < T1 (checkpoint)
– 结论: 旧错误,处理
– 清零checkpoint_ts
– 恢复页面映射 ✅
T4 处理T2时刻的fault:
– 检查: T2 >= T1 (checkpoint)
– 结论: 新错误,暂时不处理
– 返回 -EAGAIN → GPU会重试 🔄
T5 munmap完成
checkpoint_ts清零
addr已取消映射
T6 GPU重试访问addr
– 产生新的fault (ts=T6, T6>T5)
– checkpoint已清除 → 正常处理
– 发现addr已取消映射
– 返回真正的错误 → 进程收到SIGSEGV
15.6 应用场景
场景1: 内存取消映射
// 用户空间
munmap(addr, size);
// 驱动内部
svm_range_cpu_invalidate_pagetables() {
svm_range_unmap_from_cpu();
↓
设置 checkpoint_ts
↓
阻止新的页面错误
↓
等待旧错误完成
↓
安全地取消映射
}
场景2: 页面迁移
// 迁移页面: RAM → VRAM
svm_migrate_ram_to_vram() {
// 设置checkpoint,停止GPU访问
设置 checkpoint_ts
↓
// 排空旧的页面错误
svm_range_drain_retry_fault()
↓
// 执行数据迁移
migrate_vma_setup()
copy_data()
migrate_vma_finalize()
↓
// 建立新映射
svm_range_map_to_gpu()
↓
// 清除checkpoint
checkpoint_ts = 0
}
场景3: 内存保护变更
// 修改保护属性
mprotect(addr, size, PROT_READ);
// 驱动处理
MMU notifier → svm_range_cpu_invalidate_pagetables()
↓
设置 checkpoint_ts
↓
阻止写页面错误
↓
更新页表权限 (只读)
↓
清除 checkpoint_ts
15.7 性能与优化
无锁设计
优势对比:
| 写入开销 | 1个原子写 | 数十个指令 | 数十个指令 |
| 读取开销 | 1个原子读 + 比较 | 加锁/解锁 | 加锁/解锁 |
| 中断安全 | ✅ | ❌ | ✅ |
| 并发性 | 高 | 低 | 中 |
| 死锁风险 | 无 | 有 | 有 |
时间复杂度
| 设置checkpoint | O(n_gpus) | 遍历所有GPU |
| 检查checkpoint | O(1) | 简单比较 |
| 时间戳比较 | O(1) | 位运算 |
| 等待排空 | O(1) 或超时 | 条件等待 |
空间开销
sizeof(checkpoint_ts) = 8 bytes × MAX_GPU_INSTANCE
= 8 × 16
= 128 bytes
// 相对于整个 svm_range_list (数KB),开销极小
15.8 调试与故障排查
启用调试
# 启用SVM调试
echo 'file kfd_svm.c +p' > /sys/kernel/debug/dynamic_debug/control
# 启用IH调试
echo 'file amdgpu_ih.c +p' > /sys/kernel/debug/dynamic_debug/control
# 查看日志
dmesg -w | grep -E 'checkpoint_ts|draining|retry fault'
关键调试输出
// kfd_svm.c
pr_debug("draining retry fault, drop fault 0x%llx\\n", addr);
pr_debug("checkpoint_ts[%d] = 0x%llx\\n", gpuidx, checkpoint_ts);
// amdgpu_ih.c
pr_debug("waiting: rptr=%u, wptr=%u, processed_ts=0x%llx\\n",
ih->rptr, wptr, ih->processed_timestamp);
常见问题
问题1: 页面错误被意外丢弃
症状: 应用hang或SIGSEGV
诊断:
# 检查checkpoint状态
cat /sys/kernel/debug/kfd/proc/<pid>/svm_ranges
# 查看dmesg
dmesg | grep 'drop fault'
可能原因:
- checkpoint_ts 未正确清零
- 时间戳回绕处理错误
- IH ring溢出
问题2: 等待超时
症状: amdgpu_ih_wait_on_checkpoint_process_ts 超时
诊断:
pr_debug("timeout: rptr=%u, wptr=%u, checkpoint_ts=0x%llx\\n",
ih->rptr, wptr, checkpoint_ts);
可能原因:
- IH处理被阻塞
- 中断未触发
- 硬件hang
💡 重点提示
"丢弃"不是永久拒绝: 返回-EAGAIN后,GPU会通过XNACK机制自动重试,直到checkpoint清除后成功处理。GPU不会因此崩溃!
无锁 ≠ 无同步: Checkpoint TS 虽然无锁,但通过时间戳实现了精确同步。
时间戳来自硬件: 利用GPU IH Ring的硬件时间戳,不需要软件时钟。
48位足够长: 即使高频率时钟,48位也支持数天的回绕周期。
每GPU独立: 多GPU系统中,每个GPU有独立的checkpoint,互不干扰。
清零很重要: 处理旧错误后必须清零checkpoint,避免回绕问题。
XNACK是前提: 这个机制依赖AMD GFX9+的XNACK特性,旧GPU不支持。
⚠️ 常见陷阱
❌ 陷阱1: “忘记清零checkpoint_ts”
- ✅ 正确: 处理旧错误后立即清零,避免时间戳回绕误判。
❌ 陷阱2: “假设时间戳不会回绕”
- ✅ 正确: 使用有符号整数运算,正确处理48位回绕。
❌ 陷阱3: “在checkpoint期间修改页表”
- ✅ 正确: 先设置checkpoint,再修改页表,最后等待排空。
❌ 陷阱4: “忽略等待超时”
- ✅ 正确: 超时可能意味着硬件问题,需要处理。
📝 实践练习
跟踪checkpoint流程:
# 启用调试,运行munmap,观察checkpoint设置和清除
时间戳比较实验:
// 实现自己的时间戳比较,验证回绕处理
思考题:
- 为什么不能用简单的标志位替代checkpoint_ts?
- 如果不清零checkpoint会发生什么?
- 多GPU场景下为什么需要独立的checkpoint?
- 如果GPU不支持XNACK,checkpoint机制还能工作吗?
- EAGAIN返回后,GPU最多会重试多少次?
📚 本章小结
- 核心机制: 基于硬件时间戳的无锁同步屏障
- 关键操作: 设置检查点、检查过滤、等待排空
- 时间戳比较: 48位、左移16位、有符号运算处理回绕
- 应用场景: munmap、迁移、mprotect等内存操作
- 性能优势: 无锁、O(1)、中断安全、高并发
Checkpoint Timestamp是AMDGPU SVM的精巧设计,通过时间维度的控制实现了高效的同步。
📖 扩展阅读
- AMDGPU IH(Interrupt Handler)实现分析
- AMDGPU/KFD IV(Interrupt Vector)实现分析
- AMDGPU页表机制深度分析
➡️ 相关章节
回顾以下章节可以更好理解checkpoint_ts:
- 👉 09-缺页处理机制 – 页面错误处理流程
- 👉 10-MMU Notifier集成 – CPU页表变化通知



