GPU3枚積みPCの性能比較：空冷 vs 水冷


GPUを3枚積んだマシンを自作していろんな計算（GPGPU）してるんですが、なんかマシンの性能がいまいちな気がしてて、「やっぱGPUの冷却が悪いせいで性能が出てないんじゃないのか？」と、わかったところで色々めんどくさいから普段は考えないようにしていたんですが、ある日勇気を出して調べてみたら案の定その通りで、「これじゃあ高いグラボ買った意味ないよなぁ」との結論を得てしまったので、重い腰をあげて思い切って水冷化してみたら爆速PCが爆誕したので、今回の過程を一応記録しておきます。

# Before: 空冷マシン

とりあえず最初に自作したGPUマシンがこちら（以下、"空冷マシン"）。
![_MG_0180.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/0946ce43-d558-d01d-e883-33a79b172cd5.jpeg)
ASUS STRIX-GTX1080-A8G-GAMING（コアクロック: Base 1,670MHz /Boost 1,809MHz）を3枚積んで、無駄にでかいCPUクーラーまで積んでます。この時点で、外排気のGPUにしろよと言われそうですが、そんなことは買ったときは知らなかったのです。内排気のGPUを3枚も隙間なく重ねたことによって今回の熱問題が発生してしまっていることは否めないでしょう。ってことでGPUを複数枚ケース内に収めようと思っている人は、少なくとも外排気のGPUを購入したほうがよいでしょう。

https://chimolog.co/bto-gpu-fan-types/

# GPUの温度測定

https://proc-cpuinfo.fixstars.com/2017/10/gpu-temperature/
を参考に、温度計測を行いました。
cuBLASのSGEMMをひたすら回して(sgemm.cu)、その時の温度をpython(watchGPU.py)で出力していきます。

~~~python:watchGPU.py
# -*- coding: utf-8 -*-
import sys
import time
from datetime import datetime
from py3nvml.py3nvml import *

class Device:

    def __init__(self, device_id):
        self.__device = nvmlDeviceGetHandleByIndex(device_id)

    def temperature(self):
        return nvmlDeviceGetTemperature(self.__device, NVML_TEMPERATURE_GPU)

    def graphics_clock(self):
        return nvmlDeviceGetClockInfo(self.__device, NVML_CLOCK_GRAPHICS)

    def sm_clock(self):
        return nvmlDeviceGetClockInfo(self.__device, NVML_CLOCK_SM)

    def memory_clock(self):
        return nvmlDeviceGetClockInfo(self.__device, NVML_CLOCK_MEM)

    def fan_speed(self):
        return nvmlDeviceGetFanSpeed(self.__device)

    def power_usage(self):
        return nvmlDeviceGetPowerUsage(self.__device) * 1e-3


def main():
    nvmlInit()
    device0 = Device(0)
    device1 = Device(1)
    device2 = Device(2)
    while True:
        print('{:.0f} : {}C, {}MHz, {}MHz, {}%, {:.0f}W : {}C, {}MHz, {}MHz, {}%, {:.0f}W : {}C, {}MHz, {}MHz, {}%, {:.0f}W'.format(
            datetime.now().timestamp(),
            device0.temperature(),
            device0.sm_clock(),
            device0.memory_clock(),
            device0.fan_speed(),
            device0.power_usage(),
            device1.temperature(),
            device1.sm_clock(),
            device1.memory_clock(),
            device1.fan_speed(),
            device1.power_usage(),
            device2.temperature(),
            device2.sm_clock(),
            device2.memory_clock(),
            device2.fan_speed(),
            device2.power_usage()))
        time.sleep(1.0)
    nvmlShutdown()

if __name__ == '__main__':
    main()
~~~

~~~cuda:sgemm.cu
#include <cstdio>
#include <chrono>
#include <time.h>
#include <cublas_v2.h>
#include <thrust/device_vector.h>

inline double get_current_epoch(){
	timespec tp;
	clock_gettime(CLOCK_REALTIME, &tp);
	return tp.tv_sec + tp.tv_nsec * 1e-9;
}

int main(int argc, char *argv[]){
	namespace chrono = std::chrono;
	using duration_type = chrono::duration<double>;
	const int n = 8192;

	if(argc < 2){
		printf("Usage: %s device-id\n", argv[0]);
		return 0;
	}
	cudaSetDevice(atoi(argv[1]));

	cublasHandle_t handle;
	cublasCreate(&handle);

	const float alpha = 1.0f, beta = 0.0f;
	thrust::device_vector<float> A(n * n), B(n * n), C(n * n);
	while(true){
		const auto start = chrono::steady_clock::now();
		cublasSgemm(
			handle, CUBLAS_OP_N, CUBLAS_OP_N,
			n, n, n,
			&alpha,
			A.data().get(), n,
			B.data().get(), n,
			&beta,
			C.data().get(), n);
		cudaDeviceSynchronize();
		const auto stop = chrono::steady_clock::now();
		const auto duration = chrono::duration_cast<duration_type>(stop - start);
		const auto flops = (2.0 * n * n * n) / duration.count();
		const auto timestamp = get_current_epoch();
		// printf("%.9f\t%.3f\n", timestamp, flops * 1e-9);
	}

	cublasDestroy(handle);
	return 0;
}
~~~

ファイルを準備できたらコンパイルして実行します。

~~~bash
$ nvcc -lcublas -o sgemm sgemm.cu
$ pip install py3nvml
$ ./sgemm 0 & ./sgemm 1 & ./sgemm 2 &
$ python watchGPU.py
~~~

#測定結果（空冷マシン）

![gpu01_graph.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/ad7584b0-4f3d-a04b-9301-1edab45861e2.jpeg)

上からGPUの温度、クロック数、電力です。

①で示したところで一番上のGPUの温度が82℃に達し、出力が押さえられてクロック数が1,670MHzに減少します。といってもbaseクロックのスペック値がこの値なので、まぁ正常というところですが。その後②のところで真ん中のGPUの温度も82℃に達し、クロック数が1,670MHzとなります。さらに時間が立つと③のところで一番上のGPUの温度が93℃に達し、リミッターが発動するのか、出力が更に低下して不安定になり、それに伴ってクロック数も同様に低下して不安定になります。今回は30分弱で測定を終えましたが、さらに長時間の計算になると真ん中のGPUもリミッターが発動していた可能性が高いです。ちなみに、一番下のGPUは温度が67℃で1,898MHzを最後までキープしていました。

一番上と真ん中のGPUは吸気に十分な隙間がないことと、上に行くに従って下のGPUからの排熱が影響するので、GPUの冷却が十分に行われずリミッターが発動する温度まで上昇したと考えられます。一枚でもこのような状況になるとそれが足を引っ張って全体の計算時間を遅くしてしまうので、これではよくありません。

#水冷化

ということで水冷化することで改善を目指します。

水冷パーツは怪しげな中華製パーツをAmazonで購入することで低価格を実現しています。あとせっかくなのでCPUも水冷化しました。

これを、
![_MG_0182.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/4522400f-0519-4ac8-fcef-81fd55e82a96.jpeg)
こうして、
![_MG_0186.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/59de722e-78ca-d9c6-a3e7-fd7d7813cda7.jpeg)
こうだ！
![_MG_0187.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/532a3fbb-990e-38f3-b047-aaeb55f1b0fd.jpeg)
VRAMやVRMはヒートシンクをつけて空冷です。

水冷マシンとして生まれ変わった姿がこちら。
![_MG_0190.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/1abf304c-071c-b6d2-c0ac-4d110016d617.jpeg)
フィッティングやラジエーターも格安のものを選定し、リザーバーはそのへんに落ちていたアクリルのケースに穴を開けて代用することで、トータルで3万円を大幅に下回る金額で水冷化を実現できました。いい時代になったもんです。

#測定結果（水冷マシン）

![gpu01_WC_graph.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316719/3e7bb083-2ea8-46f8-5e72-19df4e2aae0b.jpeg)

一番上のGPUで、最高温度58℃でサチっており、クロック数も1,911MHzと空冷のときに比べて劇的な改善を見せました。Boost時のスペック値1,809MHzを大幅に上回っています。10分強の測定ですが、さらに長時間の計算になってもこれ以上温度は上がらなさそうです。一番上のGPUが少し温度が高いのは下のGPUからの排熱と、水路を直列につないでいることによると思われます。

#結論

やはりGPUを3枚密集させていたことでGPUの温度が上がり本来の性能が発揮されていなかったことがわかりました。水冷化することによって劇的な改善がなされ、なんとスペック値を大きく上回る爆速PCが爆誕しました。ただし、水漏れ＝死を意味するため、水漏れの恐怖とは常に戦っていかなければなりません。ケースに収めることにとらわれず、ライザーカードを使ったマイニングPCのようなラック運用が精神衛生上は良さそうです。

#参考
https://proc-cpuinfo.fixstars.com/2017/10/gpu-temperature/
https://chimolog.co/bto-gpu-fan-types/