CPU 缓存一致性

有两个独立的线程,一个线程读写 var1, 一个线程读写 var2。这两个线程的读写会相互影响吗?

1
2
3
4
5
struct SharedData {
char var1;
// double magic;
char var2;
};

下面我们来做个实验

实验

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
// test.cpp
#include <iostream>
#include <thread>


struct SharedData {
char var1;
// double magic;
char var2;
};

SharedData data;

void Thread1() {
for (int i = 0; i < 100000000; ++i) {
data.var1++;
}
}

void Thread2() {
for (int i = 0; i < 100000000; ++i) {
data.var2++;
}
}

int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);

t1.join();
t2.join();

return 0;
}

实验一

将上面的代码保存为 test.cpp, 然后用 g++ test.cpp -lpthread 编译, 运行 10 次统计平均运行时间

1
2
3
4
5
6
7
8
9
10
11
$ for i in {1..10}; do (time ./a.out); done;
./a.out 1.93s user 0.01s system 191% cpu 1.015 total
./a.out 1.42s user 0.06s system 180% cpu 0.819 total
./a.out 0.85s user 0.16s system 142% cpu 0.706 total
./a.out 1.12s user 0.06s system 186% cpu 0.633 total
./a.out 1.56s user 0.00s system 182% cpu 0.861 total
./a.out 1.22s user 0.01s system 143% cpu 0.857 total
./a.out 1.67s user 0.02s system 185% cpu 0.911 total
./a.out 1.68s user 0.01s system 182% cpu 0.924 total
./a.out 1.63s user 0.02s system 175% cpu 0.941 total
./a.out 1.68s user 0.02s system 184% cpu 0.919 total

total 平均时间为 0.8366 秒

实验二

然后将 SharedData 里面的 // double magic; 注释打开

1
2
3
4
5
struct SharedData {
char var1;
double magic;
char var2;
};

重新编译运行

1
2
3
4
5
6
7
8
9
10
11
$ for i in {1..10}; do (time ./a.out); done;
./a.out 0.71s user 0.00s system 191% cpu 0.369 total
./a.out 0.67s user 0.01s system 175% cpu 0.390 total
./a.out 0.65s user 0.00s system 154% cpu 0.420 total
./a.out 0.68s user 0.00s system 175% cpu 0.389 total
./a.out 0.69s user 0.00s system 184% cpu 0.374 total
./a.out 0.70s user 0.00s system 183% cpu 0.381 total
./a.out 0.67s user 0.02s system 180% cpu 0.385 total
./a.out 0.65s user 0.04s system 161% cpu 0.425 total
./a.out 0.63s user 0.00s system 116% cpu 0.540 total
./a.out 0.70s user 0.00s system 182% cpu 0.386 total

total 平均时间为 0.4269 秒

结果

实验一耗时几乎是实验二的两倍,为什么?这里就涉及到 CPU 缓存行的概念

缓存行

缓存行是计算机体系结构中的基本缓存单元,通常是一组相邻的内存位置。当一个线程修改了共享的内存位置时,它会将整个缓存行加载到CPU缓存中。

在上述实验中, SharedData 结构中的两个 char 变量 var1 和 var2 可能处于相同的缓存行,因为它们是相邻的。当一个线程修改 var1 时,整个缓存行被加载到该线程的 CPU 缓存中。如果另一个线程正在修改var2,它会导致缓存行无效(缓存失效),从而迫使其它的线程重主存重新加载最新的数据。

// double magic; 被注释打开时,结构的大小变大,可能使 var1 和 var2 不再在同一个缓存行上。这样,两个线程可以独立地修改各自的变量,减少了缓存失效的可能性。

缓存一致性

CPU缓存一致性是指多个处理器或核心之间共享数据时,确保它们看到的数据是一致的。在多核处理器系统中,每个核心都有自己的缓存,当一个核心修改了共享数据时,其他核心可能仍然持有旧的缓存值。为了保证数据的一致性,需要采取一些机制来同步各个核心之间的缓存。

MESI协议是一种常见的缓存一致性协议,它定义了四种状态,分别是:

  1. (M)Modified:缓存行被修改,并且是唯一的拥有者,与主内存不一致。如果其他缓存需要该数据,必须先写回主内存。
  2. (E)Exclusive:缓存行是唯一的拥有者,与主内存一致,且未被修改。其他缓存可以直接读取这个缓存行,而不需要从主内存读取。
  3. (S)Shared:缓存行是共享的,与主内存一致,且未被修改。多个缓存可以同时拥有相同的缓存行。
  4. (I)Invalid:缓存行无效,不能被使用。可能是因为其他缓存修改了这个行,导致当前缓存的数据不再有效。

状态的变化可以通过以下例子来说明:

假设有两个核心,A 和 B,它们共享某个数据的缓存行:

  1. 初始状态:A 和 B 的缓存都标记为 Invalid(I),因为还没有任何核心读取或修改这个数据。
  2. 核心A读取数据:A 将缓存行标记为 Exclusive(E),表示A是唯一的拥有者,并且数据与主内存一致。
  3. 核心B读取数据:由于 A 是唯一的拥有者,B 可以直接从 A 的缓存行中读取数据,此时B 的缓存也标记为 Shared(S)。
  4. 核心A修改数据:A 将缓存行标记为 Modified(M),表示数据已被修改且A是唯一的拥有者。同时,A 会通知其他缓存失效,因为此时数据在 A 的缓存中已不一致。
  5. 核心B尝试读取数据:由于 A 将数据标记为 Modified,B 的缓存行变为 Invalid(I),B 需要从主内存重新读取最新的数据。

PyTorch 与 C++ Torch 混合编程问题排查

问题

在公司的一个项目中,需要同时使用 C++ 和 Python 的一些接口。项目的主要语言是 Python,通过 pybind11 调用 C++ 的接口。然而,在实际运行过程中出现了 torch 符号未定义的问题,尽管 Python 和 C++ 使用的 torch 都是相同版本(PyTorch 调用的也是 C++ 的 so),理论上不应该出现这样的问题。

排查

尽管两个 torch 版本相同,但它们的安装方式不同。PyTorch 是通过 pip 安装的,而 C++ Libtorch 是通过源码编译的方式安装的。虽然两者版本相同,但通过使用 nm 查看符号表发现两个 torch.so 的符号表确实不一样。最终排查发现,由于 Dual ABI 的原因,libtorch 在编译时采用的是 cxx11 ABI,而 Python 中的 torch 使用的是 Pre-cxx11 ABI,两者版本的符号不兼容,导致冲突问题。

解决方案

  1. 重新编译 C++ 项目相关的代码和库,全部使用旧 ABI。然而,使用旧 ABI 编译的可行性尚待调研。考虑到目前几乎所有的代码都是以 cxx11 编译,包括很多系统库也选择了 cxx11 的 ABI,因此全部重新编译并不现实。该方案被否决。
  2. 重新编译 Python 中的 torch,使用新的 ABI。

RSA加密原理

模运算

非对称加密解密所用到的一个非常重要的特性就是模运算不可逆

比如算 $ 3^3 \text{ } mod \text{ } 7 $ 的值, 很容易算出余数为 27 - 21 = 6

但是知道 $ 3^{\color{green}{x}} \text{ } mod \text{ } 7 = 6 $, 然后求 $x$ 的值呢,那么就只能挨个去试了,但是如果式子中的数字稍微大那么一点,比如
$$
520^{\color{green}{x}} \text{ } mod \text{ } 1011 = 721
$$

再挨个去试就很不现实了。

试想有3个数字, ${\color{green}{e}},{\color{red}{d}}, {\color{blue}{n}}$ 满足下面的条件

$$
m^{\color{green}{e}} \text{ } mod \text{ } {\color{blue}{n}} = c \\
c^{\color{red}{d}} \text{ } mod \text{ } {\color{blue}{n}} = m
$$

m 可以表示是要加密的信息,c 表示加密后的密文, 那么就可以通过这个规则对信息进行加解密了,其中

  • $({\color{green}{e}}, {\color{blue}{n}})$ 用来当做 公钥
  • $({\color{red}{d}}, {\color{blue}{n}})$ 用来当做 私钥

上面公式经过一些变化可以得到

$$
{m^{\color{green}{e}{\color{red}{d}}} \text{ } mod \text{ } {\color{blue}{n}} = m}
$$

如何选取这个 ${\color{green}{e}}$ 和 ${\color{red}{d}}$ 便成了非对称加密中的最关键的问题, 这就提到了欧拉定理

欧拉函数

讲欧拉定理前,先说下什么是欧拉函数
对于正整数N, 欧兰函数 $\phi(n)$ 的值等于从 1 到 N 中与 N 互质的数的个数

互质: 如果两个整数它们的最大公约数为 1, 则这两个数互质。

比如

正整数 N 与 N 互质的数 欧拉函数值
2 {1} $\phi(2)$ = 1
3 {1, 2} $\phi(3)$ = 2
4 {1, 3} $\phi(4)$ = 2
5 {1, 2, 3, 4} $\phi(5)$ = 4
6 {1, 5} $\phi(6)$ = 2
7 {1, 2, 3, 4, 5, 6} $\phi(7)$ = 6

对于质数来说, 小于它的数都与它互质,所以对于质数 $n$, 有 $\phi(n) = n - 1$

欧拉定理

对于互质的正整数 $m$、$n$,则 $m$ 的 $\phi{(n)}$ 次方与$1$同余,模为 $n$, 即

$$
m^{\phi(n)} \equiv 1 \text{ } (mod\text{ }{n})
$$

我们对公式进行一些简单的变换,等式两端同时取 $k$ 次幂, 即

$$
(m^{\phi(n)})^k = 1^k \text{ } (mod \text{ }n)
$$

等同于

$$
m^{k\phi(n)} = 1 \text{ } (mod \text{ }n)
$$

两端同时乘以 $m$
$$
m^{k\phi(n)} * m = 1 * m \text{ } (mod \text{ }n)
$$

再简化
$$
m^{k\phi(n)+1} = m \text{ } (mod \text{ }n)
$$

最后将模运算写在等式的左边
$$
m^{k\phi(n)+1}\text{ } mod \text{ }n = m
$$

然后和之前的加解密公式对应起来
$$
{m^{\color{green}{e}{\color{red}{d}}} \text{ } mod \text{ } {\color{blue}{n}} = m}
$$

我们可以将 ${\color{red}{d}}$ 与 ${\color{green}{e}}$ 的关系表实证下面这种形式

$$
{\color{green}{e}}{\color{red}{d}} = k\phi({\color{blue}{n}})+1
$$

$$
{\color{red}{d}} = \frac{k\phi({\color{blue}{n}})+1}{\color{green}{e}}
$$

我们可以通过选取这里的 $k$,$\color{blue}{n}$,$\color{green}{e}$ 来计算用于解密的密钥 $\color{red}{d}$

前面讲欧拉函数的时候我们讲到,对于互质的数,有 $\phi(n) = n - 1$ , 除此之外欧拉函数还有一个重要的特性,对于互质的 $p$ 和 $q$
$$
\phi(p * q) = \phi(p) * \phi(q)
$$

我们选取 $p=3, q=11$ 因此

$$
{\color{red}{d}} = \frac{k * 20+1}{\color{green}{e}}
$$

我们找一个与 $20$ 互斥的较小的数 $3$ 当做 $\color{green}{e}$, 然后尝试找满足等式的 ${\color{red}{d}}$,当 $k=1$的时候,存在 ${\color{red}{d}} = 7$ 满足条件,然后就可以把公钥 ${\color{green}{e}} = 3, {\color{blue}{n}} = 20$ 公布当做公钥,自己保留 ${\color{red}{d}} = 7$ 当做私钥。

至此就生成了不容易被破解非对称的密钥。

ubuntu 更新为清华源

将 ubuntu 默认源换为 清华源

1
sed -i 's/http:\/\/\(archive\|security\).ubuntu.com\/ubuntu\//http:\/\/mirrors.tuna.tsinghua.edu.cn\/ubuntu\//g' /etc/apt/sources.list

一键安装 Docker

安装 docker

1
2
3
4
5
6
7
8
9
sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu $(lsb_release -cs) stable" -y
sudo apt update
sudo apt --fix-broken install -y docker.io
sudo groupadd docker
sudo gpasswd -a $USER docker
sudo systemctl restart docker

安装 nvidia-docker

1
2
3
4
5
6
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update
sudo apt-get install -y nvidia-docker2
sudo systemctl restart docker

验证是否安装成功

1
docker run -ti --gpus=all ubuntu:18.04 nvidia-smi

nvidia-driver

1
2
3
4
5
6
7
8
9
10
11
# 先卸载
sudo apt-get --purge remove "*nvidia*"

# 查看 可用驱动版本
apt list | grep nvidia-driver

# 选择一个版本安装, 比如 530
sudo apt install nvidia-driver-530

# 重启机器
sudo reboot

其他设置

锁定内核版本

锁定内核当前使用版本,否则系统有可能自动更新内核,那样 nvidia-driver 也要重新安装

1
sudo apt-mark hold linux-image-$(uname -r)