.. _sec_auto_para:
Otomatik Paralellik
===================
Derin öğrenme çerçeveleri (örneğin, MXNet ve PyTorch) arka işlemcide
otomatik olarak hesaplama çizgeleri oluşturur. Bir hesaplama çizgesi
kullandığından, sistem tüm bağımlılıkların farkındadır ve hızı artırmak
için paralel olarak birden çok birbirine bağımlı olmayan görevi seçici
olarak yürütebilir. Örneğin, :numref:`sec_async` içinde
:numref:`fig_asyncgraph` bağımsız olarak iki değişkeni ilkler. Sonuç
olarak sistem bunları paralel olarak yürütmeyi seçebilir.
Genellikle, tek bir operatör tüm CPU veya tek bir GPU'da tüm hesaplama
kaynaklarını kullanır. Örneğin, ``dot`` operatörü, tek bir makinede
birden fazla CPU işlemcisi olsa bile, tüm CPU'lardaki tüm çekirdekleri
(ve iş parçacıklarını) kullanır. Aynısı tek bir GPU için de geçerlidir.
Bu nedenle paralelleştirme, tek aygıtlı bilgisayarlar için o kadar
yararlı değildir. Birden fazla cihazla işler daha önemli hale gelir.
Paralelleştirme genellikle birden fazla GPU arasında en alakadar olmakla
birlikte, yerel CPU'nun eklenmesi performansı biraz artıracaktır.
Örneğin, bkz. :cite:`Hadjis.Zhang.Mitliagkas.ea.2016`, bir GPU ve
CPU'yu birleştiren bilgisayarla görme modellerini eğitmeye odaklanır.
Otomatik olarak paralelleştirilen bir çerçevenin kolaylığı sayesinde
aynı hedefi birkaç satır Python kodunda gerçekleştirebiliriz. Daha geniş
bir şekilde, otomatik paralel hesaplama konusundaki tartışmamız, hem
CPU'ları hem de GPU'ları kullanarak paralel hesaplamaya ve aynı zamanda
hesaplama ve iletişimin paralelleşmesine odaklanmaktadır.
Bu bölümdeki deneyleri çalıştırmak için en az iki GPU'ya ihtiyacımız
olduğunu unutmayın.
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from d2l import mxnet as d2l
from mxnet import np, npx
npx.set_np()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
import torch
from d2l import torch as d2l
.. raw:: html
.. raw:: html
GPU'larda Paralel Hesaplama
---------------------------
Test etmek için bir referans iş yükü tanımlayarak başlayalım: Aşağıdaki
``run`` işlevi iki değişkene, ``x_gpu1`` ve ``x_gpu2``, ayrılan verileri
kullanarak seçeceğimiz cihazda 10 matris-matris çarpımı gerçekleştirir.
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
devices = d2l.try_all_gpus()
def run(x):
return [x.dot(x) for _ in range(50)]
x_gpu1 = np.random.uniform(size=(4000, 4000), ctx=devices[0])
x_gpu2 = np.random.uniform(size=(4000, 4000), ctx=devices[1])
Şimdi işlevi verilere uyguluyoruz. Önbelleğe almanın sonuçlarda bir rol
oynamadığından emin olmak için, ölçümden önce bunlardan herhangi birine
tek bir geçiş yaparak cihazları ısıtırız.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
run(x_gpu1) # iki cihazi da isindir
run(x_gpu2)
npx.waitall()
with d2l.Benchmark('GPU1 time'):
run(x_gpu1)
npx.waitall()
with d2l.Benchmark('GPU2 time'):
run(x_gpu2)
npx.waitall()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
GPU1 time: 0.5090 sec
GPU2 time: 0.4995 sec
Her iki görev arasındaki ``waitall`` ifadesini kaldırırsak, sistem her
iki cihazda da otomatik olarak hesaplamayı paralel hale getirmekte
serbesttir.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('GPU1 & GPU2'):
run(x_gpu1)
run(x_gpu2)
npx.waitall()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
GPU1 & GPU2: 0.5062 sec
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
devices = d2l.try_all_gpus()
def run(x):
return [x.mm(x) for _ in range(50)]
x_gpu1 = torch.rand(size=(4000, 4000), device=devices[0])
x_gpu2 = torch.rand(size=(4000, 4000), device=devices[1])
Şimdi işlevi verilere uyguluyoruz. Önbelleğe almanın sonuçlarda bir rol
oynamadığından emin olmak için, ölçmeden önce her ikisine de tek bir
geçiş yaparak cihazları ısıtırız. ``torch.cuda.synchronize()``, CUDA
cihazındaki tüm akışlardaki tüm çekirdeklerin tamamlaması için bekler.
Senkronize etmemiz gereken bir ``device`` argümanı alır. Aygıt bağımsız
değişkeni ``None`` (varsayılan) ise, ``current_device()`` tarafından
verilen geçerli aygıtı kullanır.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
run(x_gpu1)
run(x_gpu2) # bütün cihazlari isindir
torch.cuda.synchronize(devices[0])
torch.cuda.synchronize(devices[1])
with d2l.Benchmark('GPU1 time'):
run(x_gpu1)
torch.cuda.synchronize(devices[0])
with d2l.Benchmark('GPU2 time'):
run(x_gpu2)
torch.cuda.synchronize(devices[1])
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
GPU1 time: 0.5000 sec
GPU2 time: 0.5136 sec
Her iki görev arasındaki ``synchronize`` ifadesini kaldırırsak, sistem
her iki cihazda da otomatik olarak hesaplamayı paralel hale getirmekte
serbesttir.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('GPU1 & GPU2'):
run(x_gpu1)
run(x_gpu2)
torch.cuda.synchronize()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
GPU1 & GPU2: 0.5039 sec
.. raw:: html
.. raw:: html
Yukarıdaki durumda, toplam yürütme süresi, parçalarının toplamından daha
azdır, çünkü derin öğrenme çerçevesi, kullanıcı adına gelişmiş kod
gerektirmeden her iki GPU cihazında hesaplamayı otomatik olarak planlar.
Paralel Hesaplama ve İletişim
-----------------------------
Çoğu durumda, CPU ve GPU arasında veya farklı GPU'lar arasında, yani
farklı cihazlar arasında veri taşımamız gerekir. Örneğin, bu durum,
gradyanların birden fazla hızlandırıcı kart üzerinden toplamamız gereken
dağıtılmış optimizasyonu gerçekleştirmek istediğimizde ortaya çıkar.
Bunu GPU'da hesaplayarak benzetim yapalım ve sonuçları CPU'ya geri
kopyalayalım.
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def copy_to_cpu(x):
return [y.copyto(npx.cpu()) for y in x]
with d2l.Benchmark('Run on GPU1'):
y = run(x_gpu1)
npx.waitall()
with d2l.Benchmark('Copy to CPU'):
y_cpu = copy_to_cpu(y)
npx.waitall()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Run on GPU1: 0.5300 sec
Copy to CPU: 2.4141 sec
Bu biraz verimsiz. Listenin geri kalanı hala hesaplanırken ``y``'nin
parçalarını CPU'ya kopyalamaya başlayabileceğimizi unutmayın. Bu durum,
örneğin, bir minigrup işlemindeki gradyanı hesapladığımızda ortaya
çıkar. Bazı parametrelerin gradyanları diğerlerinden daha erken
kullanılabilir olacaktır. Bu nedenle, GPU hala çalışırken PCI-Express
veri yolu bant genişliğini kullanmaya başlamak bize avantaj sağlar. Her
iki parça arasında ``waitall``'i kaldırmak, bu senaryoyu benzetmemize
olanak tanır.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('Run on GPU1 and copy to CPU'):
y = run(x_gpu1)
y_cpu = copy_to_cpu(y)
npx.waitall()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Run on GPU1 and copy to CPU: 2.4141 sec
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def copy_to_cpu(x, non_blocking=False):
return [y.to('cpu', non_blocking=non_blocking) for y in x]
with d2l.Benchmark('Run on GPU1'):
y = run(x_gpu1)
torch.cuda.synchronize()
with d2l.Benchmark('Copy to CPU'):
y_cpu = copy_to_cpu(y)
torch.cuda.synchronize()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Run on GPU1: 0.5059 sec
Copy to CPU: 2.3354 sec
Bu biraz verimsiz. Listenin geri kalanı hala hesaplanırken ``y``'nin
parçalarını CPU'ya kopyalamaya başlayabileceğimizi unutmayın. Bu durum,
örneğin, bir minigrup işlemindeki (backprop) gradyanı hesapladığımızda
ortaya çıkar. Bazı parametrelerin gradyanları diğerlerinden daha erken
kullanılabilir olacaktır. Bu nedenle, GPU hala çalışırken PCI-Express
veri yolu bant genişliğini kullanmaya başlamak ize avantaj sağlar.
PyTorch'ta, ``to()`` ve ``copy_()`` gibi çeşitli işlevler, gereksiz
olduğunda çağrı yapanın senkronizasyonu atlamasını sağlayan açık bir
"non\_blocking" argümanını kabul eder. ``non_blocking=True`` ayarı bu
senaryoyu benzetmemize izin verir.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('Run on GPU1 and copy to CPU'):
y = run(x_gpu1)
y_cpu = copy_to_cpu(y, True)
torch.cuda.synchronize()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Run on GPU1 and copy to CPU: 1.8978 sec
.. raw:: html
.. raw:: html
Her iki işlem için gereken toplam süre (beklendiği gibi) parçalarının
toplamından daha azdır. Farklı bir kaynak kullandığı için bu görevin
paralel hesaplamadan farklı olduğunu unutmayın: CPU ve GPU'lar
arasındaki veri yolu. Aslında, her iki cihazda da işlem yapabilir ve
iletişim kurabiliriz, hepsini aynı anda. Yukarıda belirtildiği gibi,
hesaplama ve iletişim arasında bir bağımlılık vardır: ``y[i]`` CPU'ya
kopyalanmadan önce hesaplanmalıdır. Neyse ki, sistem toplam çalışma
süresini azaltmak için ``y[i]``'i hesaplarken ``y[i-1]``'i
kopyalayabilir.
:numref:`fig_twogpu` içinde gösterildiği gibi, bir CPU ve iki GPU
üzerinde eğitim yaparken basit bir iki katmanlı MLP için hesaplama
çizgesinin ve bağımlılıklarının bir gösterimi ile sonlandırıyoruz.
Bundan kaynaklanan paralel programı manuel olarak planlamak oldukça acı
verici olurdu. Eniyileme için çizge tabanlı bir hesaplama arka
işlemcisine sahip olmanın avantajlı olduğu yer burasıdır.
.. _fig_twogpu:
.. figure:: ../img/twogpu.svg
Bir CPU ve iki GPU üzerindeki iki katmanlı MLP'nin hesaplama çizgesi
ve bağımlılıkları.
Özet
----
- Modern sistemler, birden fazla GPU ve CPU gibi çeşitli cihazlara
sahiptir. Paralel, eşzamansız olarak kullanılabilirler.
- Modern sistemler ayrıca PCI Express, depolama (genellikle katı hal
sürücüleri veya ağlar aracılığıyla) ve ağ bant genişliği gibi
iletişim için çeşitli kaynaklara sahiptir. En yüksek verimlilik için
paralel olarak kullanılabilirler.
- Arka işlemci, otomatik paralel hesaplama ve iletişim yoluyla
performansı artırabilir.
Alıştırmalar
------------
1. Bu bölümde tanımlanan ``run`` işlevinde sekiz işlem gerçekleştirildi.
Aralarında bağımlılık yoktur. Derin öğrenme çerçevesinin bunları
otomatik olarak paralel yürüteceğini görmek için bir deney
tasarlayın.
2. Tek bir operatörün iş yükü yeterince küçük olduğunda,
paralelleştirmede tek bir CPU veya GPU'da bile yardımcı olabilir.
Bunu doğrulamak için bir deney tasarlayın.
3. CPU'ları, GPU'ları ve her iki aygıt arasındaki iletişimde paralel
hesaplamayı kullanan bir deney tasarlayın.
4. Kodunuzun verimli olduğunu doğrulamak için NVIDIA
`Nsight `__ gibi
bir hata ayıklayıcısı kullanın.
5. Daha karmaşık veri bağımlılıkları içeren hesaplama görevleri
tasarlayın ve performansı artırırken doğru sonuçları elde edip
edemeyeceğinizi görmek için deneyler çalıştırın.
.. raw:: html
.. raw:: html
`Tartışmalar `__
.. raw:: html
.. raw:: html
`Tartışmalar `__
.. raw:: html
.. raw:: html