编程笔记

lifelong learning & practice makes perfect

GPU 如此优秀,为什么我们仍然需要使用 CPU?

这段 2009 年的旧视频最近在 Twitter 上疯传。它旨在让观众直观地了解 CPU 和 GPU 之间的区别。

你可以在这里观看,时长 90 秒:

视频

这个想法是,CPU 和 GPU 在绘画决斗中正面交锋。
CPU 花了整整 30 秒才画出一个非常基本的笑脸:

图像 15

然后 GPU 瞬间画出了蒙娜丽莎:

图像 16

从这段视频中可以得出一个结论:CPU 速度慢,而 GPU 速度快。虽然这是事实,但其中有很多视频没有给出的细微差别。

当我们说 GPU 的性能比 CPU 高得多时,我们指的是一个名为 TFLOPS 的指标,它本质上衡量的是处理器在一秒内可以执行多少万亿次的数学运算。例如,Nvidia A100 GPU 可以执行 9.7 TFLOPS(每秒 9.7 万亿次运算),而最近的 Intel 24 核处理器可以执行 0.33 TFLOPS。这意味着一个中等水平的 GPU 至少比最强大的 CPU 快 30 倍。

但是我的 MacBook 中的芯片(Apple M3 芯片)包含 CPU 和 GPU。为什么?我们不能直接抛弃这些速度慢得可怕的 CPU 吗?

让我们定义两种类型的程序:顺序程序 和 _并行程序_。

顺序程序 是指所有指令必须一个接一个运行的程序。以下是一个示例。

1
2
3
4
5
6
7
8
9
def sequential_calculation():
a = 0
b = 1

for _ in range(100):

a, b = b, a + b

return b

在这里,我们连续 100 次使用前两个数字计算下一个数字。这个程序的重要特性是每个步骤都依赖于它之前的两个步骤。如果你要手动进行此计算,你不能告诉朋友,“你计算第 51 步到第 100 步,而我从第 1 步开始”,因为他们需要第 49 步和第 50 步的结果才能开始计算第 51 步。每个步骤都需要知道序列中前面的两个数字。

并行程序 是指可以同时执行多个指令的程序,因为它们不依赖于彼此的结果。以下是一个示例:

1
2
3
4
5
6
7
8
def parallel_multiply():
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
results = []

for n in numbers:
results.append(n * 2)

return results

在这种情况下,我们进行十次完全独立的乘法运算。重要的是顺序无关紧要。如果你想和朋友分担工作,你可以说,“你乘以奇数,而我乘以偶数。”你可以单独且同时工作并获得准确的结果。

实际上,这种划分是一种错误的二分法。大多数大型实际应用程序都包含顺序代码和并行代码的混合。事实上,每个程序都会有一部分指令是_可并行化的_。

例如,假设我们有一个程序运行 20 次计算。前 10 个是必须按顺序计算的斐波那契数,但后面的 10 个计算可以并行运行。我们会说这个程序是“50% 可并行化的”,因为一半的指令可以独立完成。为了说明这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def half_parallelizeable():
# Part 1: Sequential Fibonacci calculation
a, b = 0, 1
fibonacci_list = [a, b]
for _ in range(8): # Calculate 8 more numbers
a, b = b, a + b
fibonacci_list.append(b)

# Part 2: Each step is independent
parallel_results = []
for n in fibonacci_list:
parallel_results.append(n * 2)

return fibonacci_list, parallel_results

前半部分必须是顺序的 - 每个斐波那契数都依赖于它之前的两个数。但是后半部分可以获取完整的列表并独立地将每个数字加倍。

你不能在没有先计算出第 6 个和第 7 个数字的情况下计算出第 8 个斐波那契数,但是一旦你有了完整的序列,你就可以将加倍操作分配给尽可能多的可用工作者。

总的来说,CPU 更适合_顺序程序_,而 GPU 更适合_并行程序_。这是因为 CPU 和 GPU 的基本设计差异。

CPU 具有少量的大型核心(Apple 的 M3 具有 8 核 CPU),而 GPU 具有许多小型核心(Nvidia 的 H100 GPU 具有数千个核心)。

这就是为什么 GPU 非常适合运行高度并行的程序 - 它们有数千个简单的核心,可以同时对不同的数据片段执行相同的操作。

渲染视频游戏图形是一个需要许多简单重复计算的应用程序。想象一下你的视频游戏屏幕是一个巨大的像素矩阵。当你突然将你的角色向右转时,所有这些像素都需要重新计算为新的颜色值。幸运的是,屏幕顶部的像素的计算与屏幕底部的像素的计算是独立的。因此,计算可以分布在 GPU 的数千个核心中。这就是为什么 GPU 对于游戏如此重要。

在矩阵乘以 10,000 个独立数字等高度并行任务中,CPU 比 GPU 慢得多。然而,它们擅长复杂的顺序处理和复杂的决策。

将 CPU 核心想象成一家繁忙餐厅厨房里的主厨。这位厨师可以:

  • 当一位 VIP 客人带着特殊的饮食要求到达时,立即调整他们的烹饪计划
  • 在准备精美的酱汁和检查烤蔬菜之间无缝切换
  • 通过重新组织整个厨房的工作流程来处理意外情况,例如停电
  • 协调多道菜肴,以便它们在恰当的时间热腾腾地新鲜送达
  • 在处理数十个不同完成状态的订单时保持食品质量

相比之下,GPU 核心就像一百个擅长重复性任务的厨师 - 他们可以在两秒钟内切一个洋葱,但他们不能有效地管理整个厨房。如果你要求 GPU 处理晚餐服务不断变化的需求,它会很吃力。

这就是为什么 CPU 对于运行计算机的操作系统至关重要。现代计算机面临着不断发生的不可预测事件:应用程序启动和停止、网络连接断开、文件被访问以及用户在屏幕上随机点击。CPU 擅长在保持系统响应能力的同时处理所有这些任务。它可以立即从帮助 Chrome 渲染网页切换到处理 Zoom 视频通话,再到处理新的 USB 设备连接 - 所有这些都同时跟踪系统资源并确保每个应用程序都获得公平的关注。

因此,虽然 GPU 擅长并行处理,但 CPU 仍然因其处理复杂逻辑和适应变化条件的独特能力而至关重要。像 Apple 的 M3 这样的现代芯片两者兼具:结合了 CPU 的灵活性和 GPU 的计算能力。

事实上,更准确的绘画视频版本会显示 CPU 管理图像下载和内存分配,然后再用 GPU 快速渲染像素。

原文

欢迎关注我的其它发布渠道