PyTorch デザインノート : CUDA セマンティクス

PyTorch デザインノート : CUDA セマンティクス (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 05/25/2018 (0.4.0)

* 本ページは、PyTorch Doc Notes の – CUDA semantics を動作確認・翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

本文

torch.cuda は CUDA 演算をセットアップして実行するために使用されます。それは現在選択されている GPU を追跡し、そして貴方が割り当てた総ての CUDA tensor はデフォルトでそのデバイス上で作成されます。選択されたデバイスは torch.cuda.device コンテキスト・マネージャで変更可能です。

けれども、ひとたび tensor が割り当てられれば、選択したデバイスとは無関係にその上で演算を行なうことができて、そして結果は常に同じデバイス上に tensor として置かれます。

copy_() と (to()cuda() のような) copy-ライクな機能の他のメソッドを別にすれば、デフォルトでは cross-GPU 演算は許されていません。peer-to-peer メモリアクセスを有効にしない場合、異なるデバイスに渡り広がる tensor 上で ops を launch するどのような試みもエラーを上げるでしょう。

下でこれを示す小さいサンプルを見つけることができます :

cuda = torch.device('cuda')     # デフォルト CUDA デバイス
cuda0 = torch.device('cuda:0')
cuda2 = torch.device('cuda:2')  # GPU 2 (これらは 0-indexed です)

x = torch.tensor([1., 2.], device=cuda0)
# x.device is device(type='cuda', index=0)
y = torch.tensor([1., 2.]).cuda()
# y.device is device(type='cuda', index=0)

with torch.cuda.device(1):
    # tensor を GPU 1 に割り当てる
    a = torch.tensor([1., 2.], device=cuda)

    # tensor を CPU から GPU 1 に転送する
    b = torch.tensor([1., 2.]).cuda()
    # a.device and b.device are device(type='cuda', index=1)

    # tensor を転送するために ``Tensor.to`` もまた使用できます :
    b2 = torch.tensor([1., 2.]).to(device=cuda)
    # b.device and b2.device are device(type='cuda', index=1)

    c = a + b
    # c.device is device(type='cuda', index=1)

    z = x + y
    # z.device is device(type='cuda', index=0)

    # コンテキスト内でさえも、デバイスを指定することができます
    # (or give a GPU index to the .cuda call)
    d = torch.randn(2, device=cuda2)
    e = torch.randn(2).to(cuda2)
    f = torch.randn(2).cuda(cuda2)
    # d.device, e.device, and f.device are all device(type='cuda', index=2)

 

非同期実行

デフォルトでは、GPU 演算は非同期です。GPU を使用する関数を呼び出すとき、演算は特定のデバイスに enqueue されますが、少し後まで必ずしも実行はされません。これは、CPU や他の GPU 上の演算を含み、更なる計算を並列に実行することを可能にします、

一般的に、非同期計算の効果は呼び出し元には見えません、何故ならば (1) 各デバイスはキューイングされた順番で演算を実行し、そして (2) CPU と GPU 間または 2 つの GPU 間でデータをコピーするとき PyTorch は自動的に必要な同期を遂行するからです。こうして、計算は総ての演算が同期的に実行されたように進みます。

環境変数 CUDA_LAUNCH_BLOCKING=1 を設定することで同期計算を強制することができます。GPU 上でエラーが発生したときこれは便利です。(非同期実行では、演算が実際に実行される後までそのようなエラーはレポートされませんので、スタック・トレースはそれがリクエストされた場所を示しません。)

例外としては、copy_() のような幾つかの関数は明示的な async 引数を許容し、これは呼び出し元に不要であるときには同期をバイパスさせます。もう一つの例外は CUDA ストリームで、下で説明されます。

 

CUDA ストリーム

CUDA ストリーム は特定のデバイスに属する実行の線形シークエンスです。通常は一つを明示的に作成する必要はありません : デフォルトで、各デバイスはそれ自身の “default” ストリームを使用します。

各ストリームの内部の演算はそれらが作成された順序でシリアライズされますが、異なるストリームからの演算は (synchronize() または wait_stream() のような) 明示的な同期関数が使用されない限りは任意の相対順序で同時に実行可能です。例えば、次のコードは正しくありません :

cuda = torch.device('cuda')
s = torch.cuda.stream()  # Create a new stream.
A = torch.empty((100, 100), device=cuda).normal_(0.0, 1.0)
with torch.cuda.stream(s):
    # sum() may start execution before normal_() finishes!
    B = torch.sum(A)

上で説明したように、”current ストリーム” がデフォルトのストリームであるとき、データが動き回るときに PyTorch は自動的に必要な同期を遂行します。けれども、非デフォルトのストリームを使用するとき、適切な同期を確実にするのはユーザの責任です。

 

メモリ管理

PyTorch はメモリ割当てをスピードアップするためにキャッシュ・メモリ allocator を使用します。これはデバイス同期なしに高速メモリ割当て解除 (= deallocation) を可能にします。けれども、allocator により管理される未使用のメモリは依然として nvidia-smi で使用されているかのように示されます。tensor により占有されるメモリをモニタするために memory_allocated()max_memory_allocated() を使用することができて、そしてキャッシュ allocator により管理されるメモリをモニタするためには memory_cached()max_memory_cached() を使用できます。empty_cache() の呼び出しは総ての 未使用 のキャッシュ・メモリを PyTorch から解放し、その結果それらは他の GPU アプリケーションにより使用可能になります。けれども、tensor により占有された GPU メモリは解放されませんので PyTorch で利用可能な GPU メモリの総量を増やすことはできません。

 

ベストプラクティス

デバイス不可知 (= agnostic) なコード

PyTorch の構造により、デバイス-不可知 (CPU or GPU) なコードを明示的に各必要があるかもしれません ; サンプルはリカレント・ニューラルネットワークの初期隠れ状態として新しい tensor を作成するかもしれません。

最初のステップは GPU が使用されているか否かを決定します。一般的なパターンはユーザ引数を読み込むために Python の argparse モジュールを使用して is_available() と組み合わせて、CUDA を無効にするために使用可能なフラグを持ちます。以下では、args.device は結果として torch.device オブジェクトになりこれは tensor を CPU か CUDA に移すために利用可能です。

import argparse
import torch

parser = argparse.ArgumentParser(description='PyTorch Example')
parser.add_argument('--disable-cuda', action='store_true',
                    help='Disable CUDA')
args = parser.parse_args()
args.device = None
if not args.disable_cuda and torch.cuda.is_available():
    args.device = torch.device('cuda')
else:
    args.device = torch.device('cpu')

args.device を持つ今、望まれるデバイス上で Tensor を作成するためにそれを使用できます。

x = torch.empty((8, 42), device=args.device)
net = Network().to(device=args.device)

これは、デバイス不可知なコードを生成する多くのケースで使用できます。下は dataloader を使用するときのサンプルです :

cuda0 = torch.device('cuda:0')  # CUDA GPU 0
for i, x in enumerate(train_loader):
    x = x.to(cuda0)

システム上の複数の GPU で作業するとき、どの GPU が PyTorch で利用可能であるかを管理するために CUDA_VISIBLE_DEVICES 環境フラグが使用できます。上で言及したように、どの GPU 上で tensor が作成されるかを手動で制御するためには、ベストプラクティスは torch.cuda.device コンテキスト・マネージャを使用することです。

print("Outside device is 0")  # On device 0 (default in most scenarios)
with torch.cuda.device(1):
    print("Inside device is 1")  # On device 1
print("Outside device is still 0")  # On device 0

貴方が tensor を持ちそして同じデバイス上で同じ型の新しい tensor を作成することを望む場合、torch.Tensor.new_* メソッド (torch.Tensor 参照) を使用することができます。以前に言及した torch.* ファクトリ関数 (Creation Ops) が現在の GPU コンテキストと貴方が渡す attributes 引数に依拠する一方で、torch.Tensor.new_* メソッドはデバイスと tensor の他の属性を保存します。

これは、モジュールを作成してそこで forward パスの間に新しい tensor が内部的に作成される必要があるときの推奨される実践です。

cuda = torch.device('cuda')
x_cpu = torch.empty(2)
x_gpu = torch.empty(2, device=cuda)
x_cpu_long = torch.empty(2, dtype=torch.int64)

y_cpu = x_cpu.new_full([3, 2], fill_value=0.3)
print(y_cpu)

    tensor([[ 0.3000,  0.3000],
            [ 0.3000,  0.3000],
            [ 0.3000,  0.3000]])

y_gpu = x_gpu.new_full([3, 2], fill_value=-5)
print(y_gpu)

    tensor([[-5.0000, -5.0000],
            [-5.0000, -5.0000],
            [-5.0000, -5.0000]], device='cuda:0')

y_cpu_long = x_cpu_long.new_tensor([[1, 2, 3]])
print(y_cpu_long)

    tensor([[ 1,  2,  3]])

もう一つの tensor の同じ型とサイズの tensor を作成して、そしてそれを 1 かゼロで満たすことを望む場合、便利なヘルパー関数として ones_like() または zeros_like() が提供されます (それはまた Tensor の torch.device と torch.dtype を保存します)。

x_cpu = torch.empty(2, 3)
x_gpu = torch.empty(2, 3)

y_cpu = torch.ones_like(x_cpu)
y_gpu = torch.zeros_like(x_gpu)

 

ピン止めされたメモリ・バッファを使用する

GPU コピーへのホストはそれらがピン止めされた (page-locked) メモリをソースとするとき遥かにより速いです。CPU tensor とストレージは pin_memory() メソッドを晒していて、これはピン止めされた領域にデータを置くとともに、オブジェクトのコピーを返します。

また、ひとたび tensor かストレージをピン止めすれば、非同期 GPU コピーを使用することができます。追加の non_blocking=True 引数を cuda() 呼び出しに単に渡してください。これは計算を伴うデータ転送をオーバラップするために使用できます。

DataLoader に pin_memory=True をコンストラクタに渡すことによりピン止めされたメモリに置かれたバッチを返させることができます。

 

マルチプロセス処理の代わりに nn.DataParallel を使用する

バッチ化された入力とマルチ GPU を伴う殆どのユースケースは1 つ以上の GPU を利用するために DataParallel をデフォルトで使用するべきです。GIL を伴ってさえ、単一の Python プロセスはマルチ GPU を飽和させることができます。

バージョン 0.1.9 の時点で、GPU の巨大な数 (8+) は完全には利用できないかもしれません。けれども、これは既知の問題で活発な開発下にあります。いつものように、貴方のユースケースをテストしてください。

multiprocessing を伴う CUDA モデルの使用は重要な警告があります ; データ処理要件に正確に適合するようにケアがされない場合、貴方のプログラムは正しくないか未定義の挙動を多分持つようになるでしょう。

 

 

以上