Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

本页目录

异构计算——CPU 与 GPU 如何协同工作 / Heterogeneous Computing with CPUs and GPUs ​

📅 创建时间:2026-06-02 🏷️ 标签:#异构计算 #CPU #GPU #流水线 #异步执行 #PinnedMemory 📚 前置知识:[[04-cuda-programming-model]](CUDA 编程) [[08-mpi-cluster-hpc]](集群通信) 📚 相关知识:[[10-dl-training-optimization]](训练优化实战)


先抓住直觉 ​

CPU 与 GPU 不是竞争关系,而是一条流水线的不同工位。最差的情况是 CPU 准备数据时 GPU 闲着,GPU 计算时传输通道又闲着;协同优化就是让准备、搬运和计算尽量重叠。

  • 必须理解:异步执行、流水线、Pinned Memory 为何能帮助传输。
  • 用到再查:Stream API、Unified Memory 提示函数和双缓冲模板。
  • 读完能回答:为什么 GPU 利用率低不一定是 GPU Kernel 的问题?

场景:CPU 和 GPU 谁才是瓶颈? ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  训练一个 epoch,耗时分析:                               │
│                                                             │
│  GPU Forward/Backward:  10 秒(90%)                    │
│  CPU 数据加载:         1 秒(9%)                        │
│  CPU→GPU 数据传输:    0.1 秒(1%)                      │
│                                                             │
│  看起来 GPU 占据了大部分时间。                           │
│  但为什么 GPU 利用率只有 60%?                          │
│                                                             │
│  原因:CPU 数据加载太慢,GPU 经常在等待数据!           │
│                                                             │
│  本章学习如何让 CPU 和 GPU 协同工作,最大化效率。     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第1节:异构系统的基本架构 ​

CPU-GPU 数据流 ​

┌─────────────────────────────────────────────────────────────┐
│                    CPU-GPU 异构系统                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                        CPU                                  │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                                                      │  │
│  │   数据加载    数据预处理    调度控制    日志记录    │  │
│  │   (SSD)      (OpenMP)     (CUDA API)              │  │
│  │                                                      │  │
│  └────────────────────────────┬────────────────────────┘  │
│                               │ PCIe                        │
│                               ↓                             │
│  ┌────────────────────────────┐                           │
│  │       GPU 显存 (HBM)       │                           │
│  │  ┌──────────────────────┐  │                           │
│  │  │  Forward  |  Backward │  │  ← GPU 计算            │
│  │  │  Kernel   |  Kernel   │  │                           │
│  │  └──────────────────────┘  │                           │
│  └────────────────────────────┘                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键问题:
1. 数据从 SSD → CPU → GPU,链路长
2. 如果 CPU 太慢,GPU 等待
3. 需要 CPU 和 GPU 并行工作
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

异步执行原理 ​

cpp
// ❌ 同步执行:CPU 等 GPU,浪费时间
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);
kernel<<<...>>>(d_data);  // GPU 执行时 CPU 空闲
cudaMemcpy(h_result, d_result, size, cudaMemcpyDeviceToHost);

// ================================================================
// ✅ 异步执行:CPU 和 GPU 并行工作
// ================================================================
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);

// CPU 做数据加载(stream1)
cudaMemcpyAsync(d_data, h_data, size, cudaMemcpyHostToDevice, stream1);

// GPU 执行 kernel(stream1)
kernel<<<blocks, threads, 0, stream1>>>(d_data);

// CPU 同时做其他事情(不阻塞!)
preprocess_next_batch_cpu();  // CPU 预处理下一批
log_to_disk();                 // CPU 写日志

// 最后才等待 GPU 完成
cudaStreamSynchronize(stream1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

第2节:Pinned Memory——加速 CPU-GPU 传输 ​

Pageable vs Pinned Memory ​

┌─────────────────────────────────────────────────────────────┐
│                    Pinned Memory(页锁定内存)                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Pageable Memory(普通内存):                              │
│  - 操作系统可以把这块内存换页到磁盘(swap)                │
│  - CPU 访问可能触发缺页中断                                │
│  - GPU 传输前需要先拷贝到临时缓冲区                        │
│  → 传输慢                                                  │
│                                                             │
│  Pinned Memory(页锁定内存):                             │
│  - 永远保持在物理内存,不会被换页                         │
│  - GPU 可以直接传输,不需要临时拷贝                        │
│  → 传输快 2-3 倍!但会占用系统内存                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Pinned Memory 代码 ​

cpp
// 普通方式(pageable memory)
float* h_data = (float*)malloc(size);  // pageable
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);

// ================================================================
// Pinned Memory 方式
// ================================================================
float* h_data_pinned;
cudaMallocHost(&h_data_pinned, size);  // 分配页锁定内存

// 使用方式一样,但传输更快
cudaMemcpy(d_data, h_data_pinned, size, cudaMemcpyHostToDevice);

// 释放
cudaFreeHost(h_data_pinned);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

异步传输 + Pinned Memory ​

cpp
// 最佳实践:Pinned Memory + Async + Double Buffering
cudaStream_t stream;
cudaStreamCreate(&stream);

// 两套缓冲区,轮换使用
float *d_buf[2], *h_buf[2];
for (int i = 0; i < 2; i++) {
    cudaMalloc(&d_buf[i], size);
    cudaMallocHost(&h_buf[i], size);  // pinned memory
}

int buf_idx = 0;
bool first_iteration = true;

// 主循环
while (has_more_data()) {
    // CPU 加载下一批数据到当前缓冲区
    load_data(h_buf[buf_idx], size);

    // CPU→GPU 异步拷贝(数据加载完成后)
    cudaMemcpyAsync(d_buf[buf_idx], h_buf[buf_idx],
                    size, cudaMemcpyHostToDevice, stream);

    // 如果不是第一次,等待上一个 kernel 完成
    if (!first_iteration) {
        int prev_idx = 1 - buf_idx;
        cudaStreamSynchronize(stream);  // 等待前一批 GPU 计算完成
        // 处理前一批的结果
        process_results(d_buf[prev_idx]);
    }

    // 启动当前批次的 kernel(与下一次 CPU 加载并行!)
    kernel<<<blocks, threads, 0, stream>>>(d_buf[buf_idx], size);

    buf_idx = 1 - buf_idx;  // 切换缓冲区
    first_iteration = false;
}
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

第3节:CUDA Streams 实现流水线 ​

数据加载流水线 ​

┌─────────────────────────────────────────────────────────────┐
│                    3-Stage 流水线                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Time →                                                    │
│                                                             │
│  Stage 1: [Load GPU0][Load GPU1][Load GPU2][Load GPU3]...│
│  Stage 2:     [Preprocess][Preprocess][Preprocess]...   │
│  Stage 3:         [GPU Fwd][GPU Fwd][GPU Fwd]...        │
│                                                             │
│  理想情况:3 个阶段并行,满载运转                          │
│                                                             │
│  如果数据加载太慢(Stage 2 瓶颈):                        │
│  Stage 1: [Load][Load][Load][Load][Load][Load]...       │
│  Stage 2:      [Pre ][Pre ][Pre ][Pre ][Pre ][Pre ]    │
│  Stage 3:           [Fwd ][Fwd ][Fwd ][Fwd ]            │
│             ─────────→ GPU 大量空闲等待                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

多 Stream 实现流水线 ​

cpp
// 3-Stream 流水线实现
cudaStream_t stream_load, stream_preprocess, stream_compute;
cudaStreamCreate(&stream_load);
cudaStreamCreate(&stream_preprocess);
cudaStreamCreate(&stream_compute);

// 使用事件控制流水线顺序
cudaEvent_t preprocess_done, compute_done;

int batch = 0;
while (true) {
    // Stage 1: CPU 加载数据到 pinned memory
    load_data_cpu(h_pinned[batch % 2]);

    // 等待 preprocess 完成,才能开始下一次加载
    if (batch > 0) {
        cudaEventSynchronize(preprocess_done);
    }

    // 异步拷贝到 GPU
    cudaMemcpyAsync(d_pinned[batch % 2], h_pinned[batch % 2],
                     size, cudaMemcpyHostToDevice, stream_load);

    // Stage 2: GPU 预处理(resize, normalize 等)
    preprocess<<<blocks, threads, 0, stream_preprocess>>>(
        d_pinned[batch % 2], d_preprocessed[batch % 2]);
    cudaEventRecord(preprocess_done, stream_preprocess);

    // 等待 preprocess 完成,才能开始计算
    cudaEventSynchronize(preprocess_done);

    // Stage 3: GPU 训练
    train<<<blocks, threads, 0, stream_compute>>>(
        d_preprocessed[batch % 2], model_params);
    cudaEventRecord(compute_done, stream_compute);

    batch++;
}
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

第4节:统一内存的异构使用 ​

┌─────────────────────────────────────────────────────────────┐
│                    Unified Memory 在异构场景的应用              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景:CPU 和 GPU 交替访问同一块数据                       │
│                                                             │
│  例如:                                                    │
│  - CPU 加载数据 → GPU 处理 → CPU 读取结果 → CPU 写日志   │
│  - 循环很多次                                             │
│                                                             │
│  传统方式:每次都需要 cudaMemcpy                         │
│  Unified Memory:自动迁移,无需手动管理                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cpp
// Unified Memory 方式
float* data;
cudaMallocManaged(&data, size);

// CPU 处理
preprocess_cpu(data, size);

// GPU 处理(OS 自动把数据迁移到 GPU)
cudaMemcpy(data, size, cudaMemcpyHostToDevice);  // 显式提示迁移
train<<<blocks, threads>>>(data, size);
cudaDeviceSynchronize();

// CPU 读取结果(OS 自动把数据迁移回 CPU)
read_results_cpu(data, size);

// 性能提示:预取到 GPU
cudaMemPrefetchAsync(data, size, 0);  // deviceId=0 = GPU
train<<<blocks, threads>>>(data, size);
cudaDeviceSynchronize();

// 预取回 CPU
cudaMemPrefetchAsync(data, size, cudaCpuDeviceId);
read_results_cpu(data, size);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第5节:CPU 和 GPU 的任务分配策略 ​

┌─────────────────────────────────────────────────────────────┐
│                    CPU vs GPU 任务分配                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  适合 CPU:                                                │
│  - IO 密集型:磁盘读写、网络通信                          │
│  - 分支密集型:if-else 多的逻辑                          │
│  - 串行任务:数据预处理、格式转换                         │
│  - 任务量小的任务:启动 GPU 开销不划算                   │
│                                                             │
│  适合 GPU:                                                │
│  - 计算密集型:矩阵乘法、卷积                             │
│  - 数据并行:大量相同操作                                  │
│  - 算术强度高:计算量 >> 内存访问量                     │
│                                                             │
│  实际训练中的分配:                                       │
│  ┌─────────────────────────────────────────────────────┐  │
│  │ CPU: 数据加载 → Tokenize → Resize → Normalize    │  │
│  │                          ↓ (Pinned Memory + Async)  │  │
│  │ GPU: Embedding → Transformer → Loss → Backward   │  │
│  │                    ↑ (CUDA Stream 并行)            │  │
│  │ CPU: 日志记录 → Checkpoint → Metrics 计算        │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                             │
│  核心原则:CPU 和 GPU 永远不要同时闲着!                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ cudaStreamCreate/cudaEventCreate 的具体参数
✅ Pinned Memory 的具体使用限制
✅ Unified Memory prefetch 的具体 API

必须理解:
🔴 CPU-GPU 数据传输是瓶颈,需要异步化
🔴 Pinned Memory 比普通内存传输快 2-3 倍
🔴 Double Buffering:两套缓冲区轮换,CPU 和 GPU 并行工作
🔴 CUDA Stream 实现流水线:Load → Preprocess → Compute
🔴 任务分配:CPU 做 IO/预处理,GPU 做计算密集型任务
1
2
3
4
5
6
7
8
9
10
11

学习状态:🟡 开始学习

最后更新于:

Pager
下一篇← 系统与高性能 / Systems & Performance

持续记录,持续成长

Copyright © Tidenflow