GGML多线程计算:OpenMP简介
OpenMP是什么
OpenMP是一套用于共享内存并行系统的多线程程序设计标准。通俗的将,它允许通过简单的编译器指令(#pragma)将原本串行执行的C/C++ for循环瞬间变成多线程并发执行,而不需要手动的调用pthread_create的创建、销毁和同步。
核心模型:Fork-Join模型是理解OpenMP的关键。
- Fork:主线程运行到并行区,Fork出一组线程。
- Join:任务完成后,线程组同步并合并,只剩下主线程继续执行。
核心语法:从串行到并行
并行循环
这是最常用的指令。它告诉编译器:“把下面这个 for 循环的迭代次数拆分,分配给不同的线程同时跑”。
void add_arrays(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
要让他在多核CPU上并行,只需要加上一行:
void add_arrays(float* a, float* b, float* c, int n) {
// 自动将 n 次循环拆分给当前可用的线程
#pragma omp parallel for
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i]; // 每个线程处理一部分 i
}
}
数据作用域
写并发最怕数据竞争(Data Race)。OpenMP 必须明确变量是“大家共用”还是“每人一份”。
- shared(x): 默认属性。所有线程读写同一个内存地址 x。比如输入的大矩阵。
- private(i): 每个线程都有自己独立的变量 i 副本,互不干扰。比如循环计数器。
#pragma omp parallel for private(i, j) shared(A, B, C)
for (int i = 0; i < rows; i++) {
// ...
}
规约
是计算“点积”(Dot Product)或“求和”时的神器。如果多个线程同时往同一个变量 sum 里加值,会冲突。 解决:使用 reduction(+:sum)。每个线程并在本地算自己的小 sum,最后 OpenMP 自动把所有线程的小 sum 加在一起。
float dot_product(float* a, float* b, int n) {
float sum = 0.0;
// 告诉编译器:sum 是归约变量,操作是加法
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; i++) {
sum += a[i] * b[i];
}
return sum;
}
循环是如何被切分的?
初学者最大的疑问通常是:多线程并发执行循环时,i 变量不会冲突吗?大家都执行 i++ 岂不是乱套了?答案是:不会。OpenMP 做的是“分地盘”,而不是“抢地盘”。
(1)空间划分
当编译器看到 #pragma omp parallel for 时,它并没有让多个线程去争夺同一个全局 i。相反,它把 0 到 n 这个迭代范围(Iteration Space)切成了若干块(Chunks),分给了不同的线程。
假设 n = 100,且有 4 个线程,默认调度下:
- 线程 0 负责 i 属于 [0, 24]
- 线程 1 负责 i 属于 [25, 49]
- 线程 2 负责 i 属于 [50, 74]
- 线程 3 负责 i 属于 [75, 99]
(2)变量私有化
在生成的底层代码中,循环变量 i 被自动标记为线程私有(Private)。
- 线程 1 里的 i 实际上是一个局部变量,它从 25 开始,自己累加到 49。
- 它完全看不到线程 0 的 i 是多少。
结论:所谓的并发,是大家在各自划定的跑道上同时跑,互不干扰。