仕事の速さは、計算かメモリの遅い方で決まる
ボトルネックとは、仕事全体の速さを決めてしまう、いちばん遅い部分のことです。GPUでは、計算よりも、データを運ぶ時間の方が長くかかる仕事がよくあります。
このレッスンでは、運ぶ時間と計算の時間を見積もって、どちらが足を引っ張るかを判断します。ラボにGPUはなく、仮の装置の算数で確かめます。
このレッスンのゴール
メモ
運ぶ時間と計算の時間を比べて、仕事が計算とメモリのどちらに縛られるかを言える。
重要な言葉
| ことば | いみ | れい |
|---|---|---|
| ボトルネック | 全体の速さを決める、いちばん遅い部分 | |
| 帯域幅 | 1秒に運べるデータの量 | 毎秒500ギガバイト |
| 計算の速さ | 1秒にできる計算の回数 | |
| 転送 | データをメモリや装置の間で運ぶこと | |
| 連続したアクセス | 隣どうしの場所を、まとめて読むこと |
学生が入力するセルは、このプレビューでは空白です。
装置には、運ぶ速さと計算の速さの2つの限界がある
仕事をするには、データをメモリから運び、計算し、結果を書き戻します。運ぶ速さと、計算の速さは別々の限界です。
運ぶ時間は、バイト数 ÷ 帯域幅です。計算の時間は、計算の回数 ÷ 計算の速さです。長い方が、その仕事の時間を決めます。
仮の装置で、2つの仕事を比べる
| 仕事 | 運ぶバイト数 | 運ぶ時間 | 計算の時間 | 足を引っ張るもの |
|---|---|---|---|---|
| ベクトルの足し算 (1000万個) | 120000000 | 240.0マイクロ秒 | 1.0マイクロ秒 | メモリ |
| 行列の掛け算 (1000×1000) | 12000000 | 24.0マイクロ秒 | 200.0マイクロ秒 | 計算 |
学生が入力するセルは、このプレビューでは空白です。
運ぶ時間と計算の時間を見積もる
# 100万個のベクトルの足し算を、仮の装置で見積もります。
bandwidth = 500 * 10**9 # 1秒に運べるバイト数
rate = 10 * 10**12 # 1秒にできる足し算の数
n = 1000000
moved = 3 * n * 4 # 2つ読んで1つ書く、1個4バイト
memory_us = moved / bandwidth * 10**6
compute_us = n / rate * 10**6
print("運ぶのにかかる時間 (マイクロ秒):", round(memory_us, 1))
print("計算にかかる時間 (マイクロ秒):", round(compute_us, 1))
if memory_us > compute_us:
print("足を引っ張るのは: メモリ")
else:
print("足を引っ張るのは: 計算")
# n を 10 倍にして、2つの時間がどう変わるか見ましょう。
隣どうしを読めば、メモリは速く使える
メモリは、32バイトなどの線の単位でまとめて読まれます。8つのスレッドが隣どうしの数を読めば、線は1本で済みます。
8個おきに読むと、線は8本要ります。同じ8つの数でも、読む本数が違います。スレッドは、隣どうしを読むように並べます。
よくある間違い
注意
計算の速い装置なら、どの仕事も速くなると思いがちです。ベクトルの足し算のように、計算が少なく運ぶ量が多い仕事では、メモリが縛ります。速くするには、運ぶ量を減らす工夫が先です。
ラボの端末で、運ぶ時間と読む場所を見る
ラボにGPUはありません。1つ目のスクリプトは、仮の装置の算数です。毎秒500ギガバイト、毎秒10テラの足し算という、決めた数で計算します。
出力には英語の単語が出ます。bytes moved は運んだバイト数、limited by は足を引っ張るものです。時間は測りません。2つ目は、読む場所の違いを数えます。
運ぶ時間と計算の時間を比べる
docker exec jupyter python3 steps/transfer.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
読む場所の違いを見る
docker exec jupyter python3 steps/coalesce.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
さらに挑戦: 運ぶ回数を減らす
2つのベクトルを足して、さらに2倍するとします。2回に分けると、メモリを2往復します。
1回でまとめて計算できれば、運ぶバイト数はどうなるでしょうか。メモリが縛る仕事で、この工夫が効く理由を、自分の言葉で説明しましょう。