Windows / Linux GPU 虚拟内存布局差异与 CUDA 越界访问行为

Windows / Linux GPU 虚拟内存布局差异与 CUDA 越界访问行为


1. 问题背景

一个 CUDA 程序中, 某个 buffer 被分配了 1 MB (262,144 floats), 但 kernel 写入了 256 MB (67,108,864 floats), 产生 255 MB 的越界写入。

同一个 buffer overflow, 两种完全不同的运行表现:

平台 GPU 驱动模式 越界写入行为 运行结果
Windows WDDM 写入相邻有效分配, 无 fault 静默数据损坏, 程序继续
Linux TCC / NVIDIA Driver 写入命中 guard page, GPU MMU fault cudaErrorIllegalAddress, 程序崩溃

本文档详细解释两种平台的 GPU 虚拟内存管理机制差异, 以及为什么同一个 bug 在 Windows 上"不发生"而在 Linux 上"发生"。


2. GPU 虚拟内存基础

2.1 GPU MMU (Memory Management Unit)

现代 NVIDIA GPU 拥有独立的 MMU, 实现 GPU 虚拟地址空间 (GPU Virtual Address Space, VA space) 到 物理显存 (VRAM) 的映射:

复制代码
GPU 虚拟地址空间 (VA)              GPU 物理显存 (VRAM)
┌──────────────────┐              ┌──────────────────┐
│ VA: 0x1000       │─── 页表 ───→ │ PA: 0x0000       │
│ [有效映射]        │              │ [物理页 A]        │
├──────────────────┤              ├──────────────────┤
│ VA: 0x2000       │              │ PA: 0x1000       │
│ [未映射]          │  ─── ✗ ───  │                  │
├──────────────────┤              │                  │
│ VA: 0x3000       │─── 页表 ───→ │ PA: 0x2000       │
│ [有效映射]        │              │ [物理页 B]        │
└──────────────────┘              └──────────────────┘
  • 有效映射: 页表中存在该 VA → PA 的映射, GPU 访问正常
  • 未映射 : 页表中不存在该 VA 的映射, GPU 访问触发 MMU fault
  • MMU fault 会被 GPU 报告为 cudaErrorIllegalAddress ("an illegal memory access was encountered")

2.2 cudaMalloc 的底层行为

cudaMalloc 在两个平台上都返回一个 GPU 虚拟地址, 但底层的内存分配机制完全不同:

Windows (WDDM) Linux (NVIDIA Driver)
底层分配器 Windows GPU memory allocator (section objects) NVIDIA driver + mmap
页表管理 Windows kernel MMU + GPU MMU Linux kernel MMU + GPU MMU
分配间隔 无 guard page 有 guard page
虚拟地址连续性 相邻分配紧密排列 相邻分配间有未映射间隔

3. Windows: WDDM 模式

3.1 WDDM (Windows Display Driver Model)

Windows 上的 CUDA 使用 WDDM 作为 GPU 驱动模型。WDDM 是 Windows 的标准图形驱动模型, GPU 内存由 Windows 内核的内存管理器统一管理。

3.2 分配机制

cudaMalloc 在 WDDM 模式下的调用链:

复制代码
cudaMalloc(N)
  → CUDA runtime (cudart)
    → NVIDIA WDDM driver
      → Windows GPU memory allocator
        → DxgkCbAllocateMemory / section objects
          → 在 GPU VA space 中分配一段连续地址
          → 建立 VA → VRAM 的页表映射
      → 返回 GPU VA pointer

WDDM 的关键特性:

  1. Section Object 模型 : 每个 cudaMalloc 分配对应一个 Windows section object, GPU VA 从一个共享地址空间中分配

  2. 无 guard page : Windows GPU memory allocator 不在相邻分配间插入未映射的保护页 。两个连续 cudaMalloc 返回的虚拟地址是紧密相邻的:

    cudaMalloc(256 MB) → VA = 0x10000000 (buffer_1)
    cudaMalloc(256 MB) → VA = 0x20000000 (buffer_2) ← 紧邻 buffer_1
    cudaMalloc( 1 MB) → VA = 0x30000000 (buffer_3) ← 紧邻 buffer_2
    cudaMalloc(256 MB) → VA = 0x30100000 (buffer_4) ← 紧邻 buffer_3

  3. 延迟映射 (lazy mapping) : WDDM 可能不会立即建立 VA → VRAM 的页表映射, 而是在首次访问时触发缺页中断。但一旦建立, 这些映射就是有效的 --- 即使越界访问, 只要 VA 有映射, GPU MMU 就不会 fault。

3.3 越界访问行为

假设程序分配了 4 个 buffer, 其中 buffer_3 (1 MB) 远小于实际需要的 256 MB:

复制代码
GPU 虚拟地址空间 (Windows WDDM):
┌────────────────────────────────────────────────┐
│ buffer_1          256 MB   [有效映射]            │  VA: 0x10000000
│                                                 │
├────────────────────────────────────────────────┤
│ buffer_2          256 MB   [有效映射]            │  VA: 0x20000000
│                                                 │
├────────────────────────────────────────────────┤
│ buffer_3            1 MB   [有效映射]            │  VA: 0x30000000
│                                                 │  ← kernel 写 256 MB
│                                                 │  ← 越界 255 MB
├────────────────────────────────────────────────┤  ← 无 guard page!
│ buffer_4          256 MB   [有效映射]            │  VA: 0x30100000
│                                                 │  ← 越界的 255 MB 写入这里
├────────────────────────────────────────────────┤
│ ...                                             │
└────────────────────────────────────────────────┘

当 kernel 向 buffer_3 (1 MB) 写入 256 MB 数据时:

  • 前 1 MB : 写入 buffer_3 的有效映射区域 → 正常
  • 后 255 MB : 写入 buffer_4 的有效映射区域 → GPU MMU 看到有效 VA 映射, 不触发 fault

GPU MMU 只检查 "这个虚拟地址是否有页表映射", 不检查 "这个虚拟地址是否属于当前分配的 buffer"。因此, 越界写入静默地覆盖了 buffer_4 的数据, 但程序不会崩溃。

3.4 为什么 Windows 这样设计

WDDM 的无 guard page 设计是 Windows GPU 内存管理器的实现选择, 可能原因包括:

  • 性能: guard page 需要额外的页表空间和 MMU 查找开销, Windows 选择了更紧凑的布局
  • 共享地址空间: WDDM 的 GPU VA 空间是多个进程共享的, Windows 使用 section object 模型, 分配按需从地址空间中划分, 不额外插入间隔
  • 历史原因: WDDM 最初为图形渲染设计, 渲染管线通常不会越界访问, 所以不需要 guard page 保护

4. Linux: NVIDIA Driver + mmap 模式

4.1 NVIDIA 专有驱动

Linux 上的 CUDA 使用 NVIDIA 专有驱动 (nvidia.ko), 不使用 Linux 内核的 KMS/GPU 框架。NVIDIA 驱动内部实现了独立的 GPU 内存管理。

4.2 分配机制

cudaMalloc 在 Linux 模式下的调用链:

复制代码
cudaMalloc(N)
  → CUDA runtime (cudart)
    → NVIDIA driver (nvidia.ko)
      → rm_alloc_memory (RM API)
        → 在 GPU VA space 中分配地址区间
        → 调用 Linux kernel mmap 建立用户空间映射
        → 在 GPU MMU 页表中建立 VA → VRAM 映射
        → 在分配区间末尾插入 unmapped guard page
      → 返回 GPU VA pointer

NVIDIA Linux 驱动的关键特性:

  1. mmap 集成 : NVIDIA 驱动通过 Linux 内核的 mmap 机制管理 GPU 内存映射。每个 cudaMalloc 对应一次 mmap 调用, 在进程地址空间中建立映射

  2. Guard page (保护页) : NVIDIA 驱动在每次分配的末尾 插入未映射的虚拟地址区间 (guard page)。相邻分配之间有一段没有页表映射的地址空间:

    cudaMalloc(256 MB) → VA = 0x7f0000000000 (buffer_1)
    VA = 0x7f0010000000 ← guard page (未映射)
    cudaMalloc(256 MB) → VA = 0x7f0010100000 (buffer_2)
    VA = 0x7f0020100000 ← guard page (未映射)
    cudaMalloc( 1 MB) → VA = 0x7f0020110000 (buffer_3)
    VA = 0x7f0020120000 ← guard page (未映射) ★
    cudaMalloc(256 MB) → VA = 0x7f0020130000 (buffer_4)

  3. 独立页表 : NVIDIA 驱动维护自己的 GPU MMU 页表, 每个 cudaMalloc 对应页表中的一个有效区间, 区间之外是未映射的

4.3 越界访问行为

复制代码
GPU 虚拟地址空间 (Linux NVIDIA driver):
┌────────────────────────────────────────────────┐
│ buffer_1          256 MB   [有效映射]            │
│                                                 │
├────────────────────────────────────────────────┤
│ ████████ guard page ████████ [未映射] ████████  │  ← 保护间隔
├────────────────────────────────────────────────┤
│ buffer_2          256 MB   [有效映射]            │
│                                                 │
├────────────────────────────────────────────────┤
│ ████████ guard page ████████ [未映射] ████████  │  ← 保护间隔
├────────────────────────────────────────────────┤
│ buffer_3            1 MB   [有效映射]            │
│                                                 │  ← kernel 写 256 MB
├────────────────────────────────────────────────┤  ← 命中 guard page!
│ ████████ guard page ████████ [未映射] ████████  │  ← GPU MMU fault!
│     → cudaErrorIllegalAddress                   │
├────────────────────────────────────────────────┤
│ buffer_4          256 MB   [有效映射]            │  ← 永远不会到达
│                                                 │
├────────────────────────────────────────────────┤
│ ...                                             │
└────────────────────────────────────────────────┘

当 kernel 向 buffer_3 (1 MB) 写入 256 MB 数据时:

  • 前 1 MB : 写入 buffer_3 的有效映射区域 → 正常
  • 第 1 MB + 1 byte : 写入 guard page 的未映射区域 → GPU MMU 检测到未映射 VA, 触发 fault
  • GPU 进入 error state , 报告 cudaErrorIllegalAddress
  • buffer_4 及后续所有数据不会被覆盖 (fault 阻止了写入)

4.4 为什么 Linux 有 guard page

NVIDIA Linux 驱动插入 guard page 的设计可能原因:

  • 安全性: guard page 提供了硬件级别的 buffer overflow 检测, 帮助开发者尽早发现越界访问
  • mmap 语义 : Linux mmap 天然支持非连续映射, 不同 mmap 区间之间是未映射的地址空间。NVIDIA 驱动利用了这一特性
  • 调试便利 : NVIDIA 在 Linux 上提供了更严格的内存保护, 配合 compute-sanitizer 等工具可以精确定位越界
  • UMA / NUMA 兼容: Linux 的内存管理需要处理更复杂的 NUMA 场景, guard page 是分配器粒度对齐的副产物

4.5 NVIDIA 驱动分配粒度

NVIDIA Linux 驱动的 cudaMalloc 有一个分配粒度 (allocation granularity) 概念:

  • GPU MMU 使用 4 KB 或更大的页大小

  • 每次 cudaMalloc 内部会向上对齐到驱动粒度 (通常是 2 MB 或更大, 取决于 GPU 架构)

  • 对齐后剩余的空间自然成为 guard page

    cudaMalloc(1 MB):
    请求大小: 1 MB
    实际分配: 2 MB (向上对齐到粒度)
    有效映射: 1 MB
    剩余空间: 1 MB (未映射, 作为 guard page)

这意味着即使没有显式的 guard page, 分配对齐也会在相邻分配间产生未映射的间隔。


5. 对比: 同一次越界写入的两种命运

5.1 完整时间线

buffer_3 (1 MB) 溢出为例, 以下是同一 kernel 在两种平台上的执行过程:

复制代码
                          Windows (WDDM)                    Linux (NVIDIA driver)
                          ──────────────                    ─────────────────────
GPU VA 布局:
                          buffer_3 1MB [有效]               buffer_3 1MB [有效]
                          buffer_4 256MB [有效]             ███ guard page ███ [未映射]
                                                            buffer_4 256MB [有效]

Kernel 写入:
  写 0..1MB (buffer_3)    ✓ 正常                            ✓ 正常
  写 1MB..256MB (越界)    ✓ 写入 buffer_4                  ✗ 命中 guard page
                          (有效 VA, MMU 不 fault)           (无效 VA, MMU fault!)

GPU 状态:
                          正常, 继续执行                    error state
                          buffer_4 被覆盖                   buffer_4 未被覆盖

后续 kernel:
                          正常执行 (但数据已损坏)            全部返回 cudaErrorIllegalAddress
                                                            (GPU 进入 error state 后所有
                                                             操作都返回该错误)

同步点 (cudaMemcpy):
                          正常完成                           返回 cudaErrorIllegalAddress
                          (感知不到错误)                     (感知到延迟的错误)

5.2 数据损坏范围对比

复制代码
                    Windows                          Linux
                    ────────                          ─────
buffer_3 (1 MB)     ✓ 被正确写入                      ✓ 被正确写入
                    ─── 越界写入 255 MB ──             ─── guard page 阻止越界 ──
buffer_4            ✗ 被覆写 255 MB                   ✓ 未被覆写
                    (被 buffer_3 的越界数据覆盖)

后果:
                    后续计算使用被污染的数据            程序崩溃, 无输出
                    整个 pipeline 输出错误
                    (但程序不报错!)

6. CUDA 异步执行与错误延迟

6.1 异步 Kernel Launch

CUDA kernel launch 是异步 的 --- host 端调用 <<<grid, block>>>() 后立即返回, GPU 在后台执行:

复制代码
Host 线程:
  ──→ kernel<<<>>>()  ──→ 立即返回 (不等 GPU 完成)
  │                      │
  │                      ↓ (GPU 在后台排队执行)
  │                      │
  ├─→ kernel<<<>>>()  ──→ 立即返回
  │                      │
  ├─→ cudaMemcpy()    ──→ **同步点**: 等待 GPU 完成所有排队操作
  │                      │
  │                      ↓
  │                   此时才能感知 GPU 上的错误

6.2 错误延迟机制

GPU 上的 fault 不会立即通知 host。错误被记录在 GPU 的 error state 中, host 端在以下同步点才能感知:

同步点 说明
cudaMemcpy / cudaMemcpyAsync + cudaStreamSynchronize 拷贝数据时等待 GPU 完成
cudaDeviceSynchronize() 等待所有 GPU 操作完成
cudaStreamSynchronize() 等待指定 stream 完成
cudaEventSynchronize() 等待指定 event 完成
cudaGetLastError() 返回最近一次 CUDA 调用的错误 (可能包含延迟的 kernel error)

6.3 为什么错误出现在下一个 Stage 而不是出错的 Stage

假设 pipeline 包含两个处理阶段, Stage A 中某 kernel 写入 buffer_3 (1 MB) 但实际写入了 256 MB, 产生越界:

复制代码
Pipeline:
  ... → Stage A → Stage B → ...

Stage A::Execute():
  ├─ kernel<<<>>>()               → 异步 launch, host 立即返回
  │   (GPU: buffer_3 overflow → MMU fault → GPU error state)
  │
  ├─ kernel<<<>>>()              → 异步 launch
  │   (GPU: 已在 error state, kernel 不执行)
  │
  │   ... 后续所有 kernel launch 都是异步的 ...
  │
  └─ return (无 cudaDeviceSynchronize)
      (GPU error state 持续, 但 host 不知道)

Stage B::Execute():
  ├─ Save / checkpoint
  │   └─ func_copy_to_host()
  │       └─ cudaMemcpy(D→H)       → **同步点**
  │           (host 等待 GPU 完成所有排队操作)
  │           (GPU 返回 error state 中的错误)
  │           (cudaMemcpy 返回 cudaErrorIllegalAddress)
  │           (host 端终于感知到错误!)
  │           (但此时已经远离真正的出错点)

cudaMemcpy 是同步操作 , 它会等待 GPU 完成所有排队的操作。此时 GPU 已经处于 error state (因为 Stage A 的 kernel fault), cudaMemcpy 返回 cudaErrorIllegalAddress

但日志显示错误来自 Stage B 的 copy-to-host 函数, 而不是 Stage A 的越界 kernel:

复制代码
[debug] [Stage B Execute] started
[error] [cudaMemcpy] failed, size: 268435456
        → "an illegal memory access was encountered"

6.4 错误检查宏为什么不捕获错误

Stage A 的 kernel launch 后通常有错误检查:

cpp 复制代码
kernel<<<grid, block>>>(..., buffer_3, ...);
CHECK_ERROR(cudaGetLastError(), "kernel description");

典型错误检查宏的展开:

cpp 复制代码
#define CHECK_ERROR(call, msg) do {
    cudaError_t err = call;     // = cudaGetLastError()
    if (err != cudaSuccess) {
        LOG_ERROR("[CUDA ERROR]: %s", msg);
        // ← 只记录日志, 不 return, 不 throw, 不 break
    }
} while(0)

错误检查宏只记录日志, 不终止执行 。即使 cudaGetLastError() 返回了错误, 程序仍然继续执行后续的 kernel launch。

异步模式 下, cudaGetLastError() 在 kernel launch 后立即调用时, 通常只返回 launch 本身的错误 (如 invalid configuration), 不包含 kernel 执行中的错误 (因为 kernel 还没执行完)。kernel 执行中的 fault (如越界访问) 要到同步点才能被 host 感知。


7. CUDA_LAUNCH_BLOCKING=1 的作用

7.1 强制同步执行

设置环境变量 CUDA_LAUNCH_BLOCKING=1 后, CUDA runtime 将每个 kernel launch 变成同步的:

复制代码
CUDA_LAUNCH_BLOCKING=0 (默认, 异步):
  kernel<<<>>>()  ──→ host 立即返回
                     GPU 在后台排队执行
                     错误延迟到下一个同步点

CUDA_LAUNCH_BLOCKING=1 (同步):
  kernel<<<>>>()  ──→ host 等待 GPU 执行完成
                     GPU 执行完毕后返回
                     如果 kernel 内部 fault, 立即在此处返回错误

7.2 同步模式下的错误定位

复制代码
Pipeline (CUDA_LAUNCH_BLOCKING=1):

Stage A::Execute():
  ├─ kernel_1<<<>>>()
  │   → host 等待 GPU 完成
  │   → GPU 完成, 无 error
  │   → ✓
  │
  ├─ kernel_2<<<>>>()
  │   → host 等待 GPU 完成
  │   → ✓
  │
  ├─ kernel_3<<<>>>() (write → buffer_3)              ← 出错点!
  │   → host 等待 GPU 完成
  │   → GPU 执行: 写入 buffer_3 (1 MB) → 正常
  │   → GPU 执行: 写入 buffer_3+1MB → 命中 guard page → MMU fault!
  │   → GPU 进入 error state, kernel 中止
  │   → host 端 cudaGetLastError() 返回 cudaErrorIllegalAddress
  │   → ✓ 精确定位到 kernel_3 的调用位置
  │
  ├─ 后续所有 kernel<<<>>>()
  │   → GPU 已在 error state, kernel 不执行
  │   → cudaGetLastError() 立即返回 cudaErrorIllegalAddress
  │   → 日志中全部报告 "illegal memory access"

7.3 实际日志示例

CUDA_LAUNCH_BLOCKING=1 模式下的日志 (精确报告出错位置):

复制代码
[error] [CUDA ERROR]: LINE: 471
  | INFO: an illegal memory access was encountered
  | MSG: kernel_3                                ← 精确出错点!

[error] [CUDA ERROR]: LINE: 498
  | MSG: kernel_4                                ← 级联失败
[error] [CUDA ERROR]: LINE: 504
  | MSG: kernel_5
  ... (后续全部报同样的错误)

8. Windows TCC 模式

8.1 TCC (Tesla Compute Cluster) 模式

除了 WDDM, Windows 上还有 TCC 模式 (Tesla Compute Cluster mode)。TCC 是 NVIDIA 专为计算 GPU (Tesla/Quadro/RTX A 系列) 设计的驱动模式, 绕过 Windows 图形栈:

WDDM TCC
适用 GPU GeForce / 消费级 Tesla / Quadro / RTX A 系列
Windows 桌面渲染
与 Linux 行为 不同 接近
Guard page (类似 Linux)
cudaMalloc 行为 Windows GPU allocator NVIDIA driver (类似 Linux)

重要: 如果 Windows GPU 运行在 TCC 模式, 其行为可能接近 Linux, guard page 可能存在, 越界访问可能触发 fault。

但如果开发环境使用的是 GeForce 或 WDDM 模式的 GPU, 则表现是 WDDM 的"无 guard page"行为。

8.2 如何检查 TCC 模式

bash 复制代码
nvidia-smi -q | grep "TCC Mode"

如果显示 "TCC Mode: Enabled", 则 GPU 在 TCC 模式下运行; 如果显示 "WDDM" 或 "TCC Mode: Disabled", 则在 WDDM 模式下运行。


9. 总结

9.1 核心差异

复制代码
                    Windows (WDDM)              Linux (NVIDIA driver)
                    ──────────────              ─────────────────────
分配器              Windows GPU allocator       NVIDIA driver + mmap
guard page          无                          有
分配间隔            紧密排列                    有未映射间隔
越界写入 →          写入相邻有效分配             命中 guard page
GPU MMU fault       不触发 (VA 有效)            触发 (VA 未映射)
结果                静默数据损坏                 cudaErrorIllegalAddress

9.2 为什么 Windows "不 crash"

不是 Windows 更"健壮", 而是 Windows 的 GPU MMU 检测不到越界:

  • GPU MMU 只检查虚拟地址是否有页表映射
  • WDDM 的相邻分配紧密排列, 越界写入的 VA 落在相邻分配的有效映射中
  • MMU 看到 "有效 VA" → 不 fault → 越界写入静默完成

9.3 为什么 Linux "crash"

不是 Linux 更"脆弱", 而是 Linux 的 GPU MMU 能检测到越界:

  • NVIDIA 驱动在相邻分配间插入 guard page (未映射的 VA)
  • 越界写入的 VA 落在 guard page 中, 没有页表映射
  • MMU 看到 "无效 VA" → fault → cudaErrorIllegalAddress

9.4 教训

  1. Windows 上能跑 ≠ 代码正确 --- WDDM 模式下越界访问不报错, 但数据已被损坏。Windows 的"正常运行"可能掩盖了严重的内存 bug。

  2. 必须在 Linux 上做 CUDA 越界检测 --- Linux 的 guard page 机制是天然的 buffer overflow 检测器。如果代码在 Linux 上能跑, 说明至少没有越界访问。

  3. CUDA_LAUNCH_BLOCKING=1 是定位异步错误的第一工具 --- 默认模式下错误延迟到下一个同步点, 报错位置远离出错位置。同步模式下每个 kernel 等待完成, 错误在出错 kernel 本身报告。

  4. compute-sanitizer 是终极检测工具 --- compute-sanitizer --tool memcheck 可以在指令级别检测越界访问, 即使在 Windows 上也能发现 buffer overflow。它是唯一能在 WDDM 模式下检测越界访问的工具。

  5. 在分配时进行 assert 验证 --- 在内存分配函数中添加 assert(kernel_write_size <= buffer_size) 可以在开发阶段尽早发现缓冲区大小错误, 不依赖运行时 fault。

相关推荐
暴力求解3 小时前
Linux网络---传输层协议TCP(二)
linux·服务器·开发语言·网络·tcp/ip
ltl4 小时前
序列化格式深度对比:Protobuf、FlatBuffers、Cap’n Proto
linux
lengjingzju4 小时前
编译与调试完全指南—第17章 总结
linux
IT技术分享社区4 小时前
Windows桌面时钟推荐:始终置顶、无级缩放、支持全屏游戏显示时间
windows·微软技术·电脑技巧·桌面美化·电脑干货
ltl4 小时前
容器网络性能真相:veth vs macvlan vs eBPF 数据面
linux
我不会插花弄玉5 小时前
9.进程间通信(上)【由浅入深-Linux】
linux·共享内存·进程通信
CSDN121106 小时前
麒麟 V10 安装 Node-RED、IEC 104 协议插件及界面汉化
linux·编辑器·vim
晨枫阳7 小时前
@changesets/cli是什么?哪些情况下需要使用?怎么使用
linux·运维·ubuntu
公子小六8 小时前
基于.NET的Windows窗体编程之WinForms音频控件
windows·microsoft·c#·.net·音视频·winforms