12.6. Çoklu GPU için Özlü Uygulama¶ Open the notebook in SageMaker Studio Lab
Her yeni model için sıfırdan paralellik uygulamak eğlenceli değildir. Ayrıca, yüksek performans için eşzamanlama araçlarının optimize edilmesinde önemli fayda vardır. Aşağıda, derin öğrenme çerçevelerinin üst düzey API’lerini kullanarak bunun nasıl yapılacağını göstereceğiz. Matematik ve algoritmalar Section 12.5’ içindekiler ile aynıdır. Şaşırtıcı olmayan bir şekilde, bu bölümün kodunu çalıştırmak için en az iki GPU’ya ihtiyacınız olacaktır.
from d2l import mxnet as d2l
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
import torch
from torch import nn
from d2l import torch as d2l
12.6.1. Basit Örnek Bir Ağ¶
Hala yeterince kolay ve hızlı eğitilen Section 12.5 içindeki LeNet’ten biraz daha anlamlı bir ağ kullanalım. Bir ResNet-18 türevini (He et al., 2016) seçiyoruz. Girdi imgeleri küçük olduğundan onu biraz değiştiriyoruz. Özellikle, Section 7.6 içindekinden farkı, başlangıçta daha küçük bir evrişim çekirdeği, uzun adım ve dolgu kullanmamızdır. Ayrıca, maksimum ortaklama katmanını kaldırıyoruz.
#@save
def resnet18(num_classes):
"""Biraz değiştirilmiş ResNet-18 modeli."""
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.Sequential()
for i in range(num_residuals):
if i == 0 and not first_block:
blk.add(d2l.Residual(
num_channels, use_1x1conv=True, strides=2))
else:
blk.add(d2l.Residual(num_channels))
return blk
net = nn.Sequential()
# Bu model daha küçük bir evrişim çekirdeği, adım ve dolgu kullanır ve
# maksimum ortaklama katmanını kaldırır.
net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1),
nn.BatchNorm(), nn.Activation('relu'))
net.add(resnet_block(64, 2, first_block=True),
resnet_block(128, 2),
resnet_block(256, 2),
resnet_block(512, 2))
net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
return net
#@save
def resnet18(num_classes, in_channels=1):
"""Biraz değiştirilmiş ResNet-18 modeli."""
def resnet_block(in_channels, out_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(d2l.Residual(in_channels, out_channels,
use_1x1conv=True, strides=2))
else:
blk.append(d2l.Residual(out_channels, out_channels))
return nn.Sequential(*blk)
# Bu model daha küçük bir evrişim çekirdeği, adım ve dolgu kullanır ve
# maksimum ortaklama katmanını kaldırır.
net = nn.Sequential(
nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU())
net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1)))
net.add_module("fc", nn.Sequential(nn.Flatten(),
nn.Linear(512, num_classes)))
return net
12.6.2. Ağ İlkleme¶
initialize
işlevi, seçeceğimiz bir cihazda parametreleri ilklememizi
sağlar. İlkleme yöntemleri üzerinde bir tazeleme için bkz.
Section 4.8. Özellikle kullanışlı olan şey, ağı
aynı anda birden çok cihazda ilklememize de izin vermesidir. Bunun
pratikte nasıl çalıştığını deneyelim.
net = resnet18(10)
# GPU'ların bir listesini alın
devices = d2l.try_all_gpus()
# Ağın tüm parametrelerini ilklet
net.initialize(init=init.Normal(sigma=0.01), ctx=devices)
Section 12.5 içinde tanıtılan split_and_load
işlevini
kullanarak, bir minigrup veriyi bölebilir ve bölümleri devices
değişkeni tarafından sağlanan cihazlar listesine kopyalayabiliriz. Ağ
örneği otomatik olarak, ileri yayılmanın değerini hesaplamak için
uygun GPU’yu kullanır. Burada 4 gözlem oluşturuyoruz ve bunları GPU’lara
bölüyoruz.
x = np.random.uniform(size=(4, 1, 28, 28))
x_shards = gluon.utils.split_and_load(x, devices)
net(x_shards[0]), net(x_shards[1])
[21:44:32] src/operator/nn/./cudnn/./cudnn_algoreg-inl.h:97: Running performance tests to find the best convolution algorithm, this can take a while... (set the environment variable MXNET_CUDNN_AUTOTUNE_DEFAULT to 0 to disable)
(array([[ 2.2610241e-06, 2.2046002e-06, -5.4046800e-06, 1.2869973e-06,
5.1373172e-06, -3.8298003e-06, 1.4339025e-07, 5.4683437e-06,
-2.8279198e-06, -3.9651122e-06],
[ 2.0698640e-06, 2.0084669e-06, -5.6382505e-06, 1.0498463e-06,
5.5506434e-06, -4.1065477e-06, 6.0830189e-07, 5.4521765e-06,
-3.7365030e-06, -4.1891644e-06]], ctx=gpu(0)),
array([[ 2.4629817e-06, 2.6015512e-06, -5.4362617e-06, 1.2938234e-06,
5.6387926e-06, -4.1360113e-06, 3.5758956e-07, 5.5125256e-06,
-3.1957316e-06, -4.2976335e-06],
[ 1.9431664e-06, 2.2600416e-06, -5.2698178e-06, 1.4807433e-06,
5.4830948e-06, -3.9678903e-06, 7.5754087e-08, 5.6764366e-06,
-3.2530247e-06, -4.0943946e-06]], ctx=gpu(1)))
Veriler ağdan geçtiğinde, ilgili parametreler verilerin geçtiği cihazda ilkletilir. Bu, ilkleme işleminin cihaz başına temelinde gerçekleştiği anlamına gelir. İlkleme için GPU 0 ve GPU 1’i seçtiğimizden, ağ CPU’da değil, yalnızca orada ilkletilir. Aslında, parametreler CPU’da mevcut bile değildir. Parametreleri yazdırarak ve ortaya çıkabilecek hataları gözlemleyerek bunu doğrulayabiliriz.
weight = net[0].params.get('weight')
try:
weight.data()
except RuntimeError:
print('not initialized on cpu')
weight.data(devices[0])[0], weight.data(devices[1])[0]
not initialized on cpu
(array([[[ 0.01382882, -0.01183044, 0.01417865],
[-0.00319718, 0.00439528, 0.02562625],
[-0.00835081, 0.01387452, -0.01035946]]], ctx=gpu(0)),
array([[[ 0.01382882, -0.01183044, 0.01417865],
[-0.00319718, 0.00439528, 0.02562625],
[-0.00835081, 0.01387452, -0.01035946]]], ctx=gpu(1)))
Ardından, doğruluğu değerlendirme kodunu birden çok cihazda paralel
olarak çalışan bir kodla değiştirelim. Bu, Section 6.6 içinden
gelen evaluate_accuracy_gpu
işlevinin yerini alır. Temel fark, ağı
çağırmadan önce bir minigrubu bölmemizdir. Diğer her şey temelde
aynıdır.
#@save
def evaluate_accuracy_gpus(net, data_iter, split_f=d2l.split_batch):
"""Birden çok GPU kullanarak bir veri kümesindeki bir modelin doğruluğunu hesaplayın."""
# Cihaz listesini sorgula
devices = list(net.collect_params().values())[0].list_ctx()
# Doğru tahmin sayısı, tahmin sayısını
metric = d2l.Accumulator(2)
for features, labels in data_iter:
X_shards, y_shards = split_f(features, labels, devices)
# Paralel olarak çalıştır
pred_shards = [net(X_shard) for X_shard in X_shards]
metric.add(sum(float(d2l.accuracy(pred_shard, y_shard)) for
pred_shard, y_shard in zip(
pred_shards, y_shards)), labels.size)
return metric[0] / metric[1]
Eğitim döngüsünün içindeki ağı ilkleteceğiz. İlkleme yöntemleri üzerinde bir tazeleme için bkz. Section 4.8.
net = resnet18(10)
# GPU'ların bir listesini alın
devices = d2l.try_all_gpus()
# Ağı eğitim döngüsü içinde ilkleteceğiz
12.6.3. Eğitim¶
Daha önce olduğu gibi, eğitim kodunun verimli paralellik için birkaç temel işlevi yerine getirmesi gerekir:
Ağ parametrelerinin tüm cihazlarda ilklenmesi gerekir.
Veri kümesi üzerinde yineleme yaparken minigruplar tüm cihazlara bölünmelidir.
Kaybı ve gradyanı cihazlar arasında paralel olarak hesaplarız.
Gradyanlar toplanır ve parametreler buna göre güncellenir.
Sonunda, ağın nihai performansını bildirmek için doğruluğu (yine paralel olarak) hesaplıyoruz. Eğitim rutini, verileri bölmemiz ve toplamamız gerekmesi dışında, önceki bölümlerdeki uygulamalara oldukça benzer.
def train(num_gpus, batch_size, lr):
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
ctx = [d2l.try_gpu(i) for i in range(num_gpus)]
net.initialize(init=init.Normal(sigma=0.01), ctx=ctx, force_reinit=True)
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'learning_rate': lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
timer, num_epochs = d2l.Timer(), 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
for epoch in range(num_epochs):
timer.start()
for features, labels in train_iter:
X_shards, y_shards = d2l.split_batch(features, labels, ctx)
with autograd.record():
ls = [loss(net(X_shard), y_shard) for X_shard, y_shard
in zip(X_shards, y_shards)]
for l in ls:
l.backward()
trainer.step(batch_size)
npx.waitall()
timer.stop()
animator.add(epoch + 1, (evaluate_accuracy_gpus(net, test_iter),))
print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch '
f'on {str(ctx)}')
def train(net, num_gpus, batch_size, lr):
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
devices = [d2l.try_gpu(i) for i in range(num_gpus)]
def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
# Set the model on multiple GPUs
net = nn.DataParallel(net, device_ids=devices)
trainer = torch.optim.SGD(net.parameters(), lr)
loss = nn.CrossEntropyLoss()
timer, num_epochs = d2l.Timer(), 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
for epoch in range(num_epochs):
net.train()
timer.start()
for X, y in train_iter:
trainer.zero_grad()
X, y = X.to(devices[0]), y.to(devices[0])
l = loss(net(X), y)
l.backward()
trainer.step()
timer.stop()
animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),))
print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch '
f'on {str(devices)}')
Bunun pratikte nasıl çalıştığını görelim. Isınma olarak ağı tek bir GPU’da eğitelim.
train(num_gpus=1, batch_size=256, lr=0.1)
test acc: 0.93, 13.3 sec/epoch on [gpu(0)]
train(net, num_gpus=1, batch_size=256, lr=0.1)
test acc: 0.91, 13.6 sec/epoch on [device(type='cuda', index=0)]
Sonra eğitim için 2 GPU kullanıyoruz. Section 12.5 içinde değerlendirilen LeNet ile karşılaştırıldığında, ResNet-18 modeli oldukça daha karmaşıktır. Paralelleşmenin avantajını gösterdiği yer burasıdır. Hesaplama zamanı, parametreleri eşzamanlama zamanından anlamlı bir şekilde daha büyüktür. Paralelleştirme için ek yük daha az alakalı olduğundan, bu ölçeklenebilirliği artırır.
train(num_gpus=2, batch_size=512, lr=0.2)
test acc: 0.93, 8.3 sec/epoch on [gpu(0), gpu(1)]
train(net, num_gpus=2, batch_size=512, lr=0.2)
test acc: 0.88, 8.2 sec/epoch on [device(type='cuda', index=0), device(type='cuda', index=1)]
12.6.4. Özet¶
Gluon, bir bağlam listesi sağlayarak birden çok cihazda model ilkleme için en temel özellikleri sağlar.
Veriler, verilerin bulunabileceği cihazlarda otomatik olarak değerlendirilir.
O cihazdaki parametrelere erişmeye çalışmadan önce her cihazdaki ağları ilklemeye özen gösterin. Aksi takdirde bir hatayla karşılaşırsınız.
Optimizasyon algoritmaları otomatik olarak birden fazla GPU üzerinde toplanır.
12.6.5. Alıştırmalar¶
Bu bölümde ResNet-18 kullanılıyor. Farklı dönemleri, toplu iş boyutlarını ve öğrenme oranlarını deneyin. Hesaplama için daha fazla GPU kullanın. Bunu 16 GPU ile (örn. bir AWS p2.16xlarge örneğinde) denerseniz ne olur?
Bazen, farklı cihazlar farklı bilgi işlem gücü sağlar. GPU’ları ve CPU’yu aynı anda kullanabiliriz. İşi nasıl bölmeliyiz? Çabaya değer mi? Neden? Neden olmasın?
npx.waitall()
’ü atarsak ne olur? Paralellik için iki adıma kadar örtüşecek şekilde eğitimi nasıl değiştirirsiniz?
Bu bölümde ResNet-18 kullanılıyor. Farklı dönemleri, toplu iş boyutlarını ve öğrenme oranlarını deneyin. Hesaplama için daha fazla GPU kullanın. Bunu 16 GPU ile (örn. bir AWS p2.16xlarge örneğinde) denerseniz ne olur?
Bazen, farklı cihazlar farklı bilgi işlem gücü sağlar. GPU’ları ve CPU’yu aynı anda kullanabiliriz. İşi nasıl bölmeliyiz? Çabaya değer mi? Neden? Neden olmasın?