12.6. Çoklu GPU için Özlü Uygulama
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
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)]
../_images/output_multiple-gpus-concise_2e111f_47_1.svg
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)]
../_images/output_multiple-gpus-concise_2e111f_50_1.svg

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)]
../_images/output_multiple-gpus-concise_2e111f_56_1.svg
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)]
../_images/output_multiple-gpus-concise_2e111f_59_1.svg

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

  1. 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?

  2. 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?

  3. npx.waitall()’ü atarsak ne olur? Paralellik için iki adıma kadar örtüşecek şekilde eğitimi nasıl değiştirirsiniz?

Tartışmalar

  1. 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?

  2. 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?

Tartışmalar