欢迎光临
我们一直在努力

15 - SVM Checkpoint Timestamp 同步机制

难度: 🔴 高级 预计学习时间: 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 继续取消映射
→ 系统状态混乱!

问题本质:

  • 时序竞争: GPU页面错误和CPU内存操作并发
  • 状态不一致: 页面同时被恢复和取消映射
  • 重复错误: 取消映射后的访问应该被推迟
  • 为什么不能用锁?

    // 错误尝试
    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 使用多个中断环:

    Ring用途Checkpoint 使用
    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 性能与优化

    无锁设计

    优势对比:

    特性Checkpoint TSMutexSpinlock
    写入开销 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页表变化通知
    赞(0)
    未经允许不得转载:171主机测评 » 15 - SVM Checkpoint Timestamp 同步机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址