.. _sec_async:
Eşzamansız Hesaplama
====================
Günümüzün bilgisayarları, birden fazla CPU çekirdeği (genellikle
çekirdek başına birden fazla iş parçacığı), GPU başına birden çok işlem
öğesi ve genellikle cihaz başına birden çok GPU'dan oluşan son derece
paralel sistemlerdir. Kısacası, birçok farklı şeyi aynı anda, genellikle
farklı cihazlarda işleyebiliriz. Ne yazık ki Python, en azından ekstra
yardım almadan paralel ve eşzamansız kod yazmanın harika bir yolu
değildir. Sonuçta, Python tek iş parçacıklıdır ve bunun gelecekte
değişmesi pek olası değil. MXNet ve TensorFlow gibi derin öğrenme
çerçeveleri, performansı artırmak için *eşzamansız programlama* modeli
benimser, PyTorch ise Python'un kendi zamanlayıcısını kullanarak farklı
bir performans değişimine yol açar. PyTorch için varsayılan olarak GPU
işlemleri eşzamansızdır. GPU kullanan bir işlev çağırdığınızda, işlemler
belirli bir aygıta sıralanır, ancak daha sonrasına kadar zorunlu olarak
yürütülmez. Bu, CPU veya diğer GPU'lardaki işlemler de dahil olmak üzere
paralel olarak daha fazla hesaplama yürütmemize olanak tanır.
Bu nedenle, eşzamansız programlamanın nasıl çalıştığını anlamak,
hesaplama gereksinimlerini ve karşılıklı bağımlılıkları proaktif
azaltarak daha verimli programlar geliştirmemize yardımcı olur. Bu,
bellek yükünü azaltmamıza ve işlemci kullanımını artırmamıza olanak
tanır.
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
import os
import subprocess
import numpy
from d2l import mxnet as d2l
from mxnet import autograd, gluon, np, npx
from mxnet.gluon import nn
npx.set_np()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
import os
import subprocess
import numpy
import torch
from torch import nn
from d2l import torch as d2l
.. raw:: html
.. raw:: html
Arka İşlemci Üzerinden Eşzamanlama
----------------------------------
.. raw:: html
.. raw:: html
Isınmak için aşağıdaki basit örnek problemi göz önünde bulundurun:
Rastgele bir matris oluşturmak ve çarpmak istiyoruz. Farkı görmek için
NumPy ve ``mxnet.np``'te bunu yapalım.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('numpy'):
for _ in range(10):
a = numpy.random.normal(size=(1000, 1000))
b = numpy.dot(a, a)
with d2l.Benchmark('mxnet.np'):
for _ in range(10):
a = np.random.normal(size=(1000, 1000))
b = np.dot(a, a)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
numpy: 1.4168 sec
mxnet.np: 0.0154 sec
MXNet üzerinden yapılan kıyaslama çıktısı büyüklüğün kuvvetleri
mertebesinde daha hızlıdır. Her ikisi de aynı işlemcide çalıştırıldığı
için başka bir şey oluyor olmalı. MXNet'i geri dönmeden önce tüm arka
işlemciyi hesaplamasını bitirmeye zorlamak, daha önce ne olduğunu
gösterir: Ön işlemci, kontrolü Python'a geri verirken hesaplama arka
işlemci tarafından yürütülür.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark():
for _ in range(10):
a = np.random.normal(size=(1000, 1000))
b = np.dot(a, a)
npx.waitall()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Done: 1.2114 sec
Genel olarak, MXNet, örneğin Python aracılığıyla kullanıcılarla doğrudan
etkileşimler için bir ön işlemciye ve sistem tarafından hesaplamayı
gerçekleştirmek için kullanılan bir arka işlemciye sahiptir.
:numref:`fig_frontends` içinde gösterildiği gibi, kullanıcılar Python,
R, Scala ve C++ gibi çeşitli ön işlemci dillerinde MXNet programları
yazabilir. Kullanılan ön işlemci programlama dili ne olursa olsun, MXNet
programlarının yürütülmesi öncelikle C++ uygulamalarının arka
işlemcisinde gerçekleşir. Ön işlemci dili tarafından verilen işlemler
yürütme için arka işlemciye iletilir. Arka işlemci, sıraya alınmış
görevleri sürekli olarak toplayan ve yürüten kendi iş parçacıklarını
yönetir. Bunun çalışması için arka işlemcinin hesaplama çizgesindeki
çeşitli adımlar arasındaki bağımlılıkları takip edebilmesi gerektiğini
unutmayın. Bu nedenle, birbirine bağlı işlemleri paralel hale getirmek
mümkün değildir.
.. raw:: html
.. raw:: html
Isınmak için aşağıdaki basit örnek problemi göz önünde bulundurun:
Rastgele bir matris oluşturmak ve çarpmak istiyoruz. Farkı görmek için
bunu hem NumPy hem de PyTorch tensorunda yapalım. PyTorch ``tensor``'ün
bir GPU'da tanımlandığını unutmayın.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# GPU hesaplaması icin isinma
device = d2l.try_gpu()
a = torch.randn(size=(1000, 1000), device=device)
b = torch.mm(a, a)
with d2l.Benchmark('numpy'):
for _ in range(10):
a = numpy.random.normal(size=(1000, 1000))
b = numpy.dot(a, a)
with d2l.Benchmark('torch'):
for _ in range(10):
a = torch.randn(size=(1000, 1000), device=device)
b = torch.mm(a, a)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
numpy: 1.4291 sec
torch: 0.0011 sec
PyTorch üzerinden yapılan kıyaslama çıktısı büyüklüğün kuvvetleri
mertebesinde daha hızlıdır. NumPy nokta çarpımını CPU işlemcisinde
yürütülür ve PyTorch matris çarpımını GPU'da yürütülür ve bu nedenle
ikincisinin çok daha hızlı olması beklenir. Ama büyük zaman farkı, başka
bir şeyin döndüğünü gösteriyor. Varsayılan olarak, PyTorch'ta GPU
işlemleri eşzamansızdır. PyTorch'u geri dönmeden önce tüm hesaplamayı
bitirmeye zorlamak daha önce neler olduğunu gösterir: Hesaplama arka
işlemci tarafından yürütülür ve ön işlemci denetimi Python'a döndürür.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark():
for _ in range(10):
a = torch.randn(size=(1000, 1000), device=device)
b = torch.mm(a, a)
torch.cuda.synchronize(device)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Done: 0.0032 sec
Genel olarak, PyTorch, örneğin Python aracılığıyla kullanıcılarla
doğrudan etkileşimler için bir ön işlemciye ve sistem tarafından
hesaplamayı gerçekleştirmek için kullanılan bir arka işlemciye sahiptir.
:numref:`fig_frontends` içinde gösterildiği gibi, kullanıcılar Python,
R, Scala ve C++ gibi çeşitli ön işlemci dillerinde PyTorch programları
yazabilir. Kullanılan ön işlemci programlama dili ne olursa olsun,
PyTorch programlarının yürütülmesi öncelikle C++ uygulamalarının arka
işlemcisinde gerçekleşir. Ön yüz dili tarafından verilen işlemler
yürütme için arka işlemciye iletilir. Arka işlemci, sıraya alınmış
görevleri sürekli olarak toplayan ve yürüten kendi iş parçacıklarını
yönetir. Bunun çalışması için arka işlemcinin hesaplama çizgesindeki
çeşitli adımlar arasındaki bağımlılıkları takip edebilmesi gerektiğini
unutmayın. Bu nedenle, birbirine bağlı işlemleri paralel hale getirmek
mümkün değildir.
.. raw:: html
.. raw:: html
.. _fig_frontends:
.. figure:: ../img/frontends.png
:width: 300px
Programlama dili ön işlemcileri ve derin öğrenme çerçevesi arka
işlemcileri.
Bağımlılık çizgesini biraz daha iyi anlamak için başka bir basit örnek
probleme bakalım.
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = np.ones((1, 2))
y = np.ones((1, 2))
z = x * y + 2
z
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([[3., 3.]])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = torch.ones((1, 2), device=device)
y = torch.ones((1, 2), device=device)
z = x * y + 2
z
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([[3., 3.]], device='cuda:0')
.. raw:: html
.. raw:: html
.. _fig_asyncgraph:
.. figure:: ../img/asyncgraph.svg
Arka işlemci, hesaplama çizgesindeki çeşitli adımlar arasındaki
bağımlılıkları izler.
Yukarıdaki kod parçacığı da :numref:`fig_asyncgraph` içinde
gösterilmiştir. Python ön işlemci iş parçacığı ilk üç ifadeden birini
çalıştırdığında, görevi arka işlemci kuyruğuna döndürür. Son ifadenin
sonuçları *yazdırılmış* olması gerektiğinde, Python ön işlemci iş
parçacığı C++ arka işlemci iş parçacığının ``z`` değişkenin sonucunu
hesaplamayı tamamlamasını bekler. Bu tasarımın bir avantajı, Python ön
işlemci iş parçacığının gerçek hesaplamaları gerçekleştirmesine gerek
olmadığıdır. Böylece, Python'un performansından bağımsız olarak
programın genel performansı üzerinde çok etkisi yoktur.
:numref:`fig_threading`, ön işlemci ve arka işlemcinin nasıl
etkileşime girdiğini gösterir.
.. _fig_threading:
.. figure:: ../img/threading.svg
Ön ve arka işlemci etkileşimleri.
Engeller ve Engelleyiciler
--------------------------
.. raw:: html
.. raw:: html
Python'u tamamlanmasını beklemeye zorlayacak bir dizi işlem vardır:
- Açıkçası ``npx.waitall()``, hesaplama talimatlarının ne zaman
verildiğine bakılmaksızın tüm hesaplama tamamlanana kadar bekler.
Pratikte, kötü performansa yol açabileceğinden kesinlikle gerekli
olmadıkça bu operatörü kullanmak kötü bir fikirdir.
- Belirli bir değişken kullanılabilir olana kadar beklemek istiyorsak
``z.wait_to_read()``'i arayabiliriz. Bu durumda MXNet blokları, ``z``
değişkeni hesaplanıncaya kadar Python'a döner. Diğer hesaplamalar
daha sonra devam edebilir.
Bunun pratikte nasıl çalıştığını görelim.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('waitall'):
b = np.dot(a, a)
npx.waitall()
with d2l.Benchmark('wait_to_read'):
b = np.dot(a, a)
b.wait_to_read()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
waitall: 0.0203 sec
wait_to_read: 0.0191 sec
Her iki işlemin de tamamlanması yaklaşık aynı zaman alır. Bariz
engelleme işlemlerinin yanı sıra, *örtülü* engelleyicilerin farkında
olmanızı öneririz. Bir değişkenin yazdırılması, değişkenin
kullanılabilir olmasını gerektirir ve bu nedenle bir engelleyicidir. Son
olarak, ``z.asnumpy()`` aracılığıyla NumPy'ye dönüştürmeler ve
``z.item()`` aracılığıyla skalerlere dönüştürmeler, NumPy'nin
eşzamansızlık kavramı olmadığı için engelleniyor. ``print`` işlevi gibi
değerlere erişmesi gerekir.
MXNet'in kapsamından NumPy ve geri sık küçük miktarlarda verilerin
kopyalanması, aksi takdirde verimli bir kodun performansını yok
edebilir, çünkü bu tür her bir işlem, başka bir şey yapılabilmeden
*önce* ilgili terimi elde ederken gerekli tüm ara sonuçları
değerlendirmek için hesaplama çizgesi gerektirir.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('numpy conversion'):
b = np.dot(a, a)
b.asnumpy()
with d2l.Benchmark('scalar conversion'):
b = np.dot(a, a)
b.sum().item()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
numpy conversion: 0.0189 sec
scalar conversion: 0.0489 sec
.. raw:: html
.. raw:: html
Hesaplamayı İyileştirme
-----------------------
.. raw:: html
.. raw:: html
Çok iş parçacıklı bir sistemde (normal dizüstü bilgisayarlarda bile 4
veya daha fazla iş parçacığı vardır ve çok yuvalı sunucularda bu sayı
256'yı geçebilir) zamanlayıcı işlemlerinin ek yükü önemli hale
gelebilir. Bu nedenle hesaplamanın ve zamanlamanın eşzamansız ve paralel
olarak gerçekleşmesi son derece arzu edilir. Bunu yapmanın faydasını
göstermek için, bir değişkeni hem sırayla hem de eşzamansız olarak
birden çok kez 1 artırırsak ne olacağını görelim. Her toplama arasına
bir ``wait_to_read`` engelleyicisi ekleyerek eşzamanlı yürütme benzetimi
yapıyoruz.
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with d2l.Benchmark('synchronous'):
for _ in range(10000):
y = x + 1
y.wait_to_read()
with d2l.Benchmark('asynchronous'):
for _ in range(10000):
y = x + 1
npx.waitall()
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
synchronous: 1.6157 sec
asynchronous: 1.0861 sec
Python ön işlemci iş parçacığı ve C++ arka işlemci iş parçacığı
arasındaki biraz basitleştirilmiş bir etkileşim aşağıdaki gibi
özetlenebilir: 1. Ön işlemci, arka işlemcinin ``y = x + 1``'i hesaplama
görevini kuyruğa eklemesini emrediyor. 1. Arka işlemci daha sonra
hesaplama görevlerini kuyruktan alır ve gerçek hesaplamaları
gerçekleştirir. 1. Arka işlemci daha sonra hesaplama sonuçlarını ön
işlemciye döndürür. Bu üç aşamanın sürelerinin sırasıyla
:math:`t_1, t_2` ve :math:`t_3` olduğunu varsayalım. Eşzamansız
programlama kullanmazsak, 10000 hesaplamaları gerçekleştirmek için
alınan toplam süre yaklaşık :math:`10000 (t_1+ t_2 + t_3)`'dir.
Eşzamanlı programlama kullanılıyorsa, 10000 hesaplamayı gerçekleştirmek
için kullanılan toplam süre :math:`t_1 + 10000 t_2 + t_3`
(:math:`10000 t_2 > 9999t_1` varsayarak) azaltılabilir, çünkü ön işlemci
her döngü için hesaplama sonuçlarını döndürmede beklemek zorunda
değildir.
.. raw:: html
.. raw:: html
Özet
----
- Derin öğrenme çerçeveleri, Python ön işlemcisini yürütme arka
işlemcisinden ayırabilir. Bu, komutların arka işlemciye ve ilişkili
paralellik içine hızlı eşzamansız olarak eklenmesine olanak tanır.
- Eşzamansızlık oldukça duyarlı bir ön işlemciye yol açar. Ancak, aşırı
bellek tüketimine neden olabileceğinden görev kuyruğunu aşırı
doldurmamak için dikkatli olun. Ön işlemciyi ve arka işlemciyi
yaklaşık olarak senkronize ederken her minigrup için senkronize
edilmesi önerilir.
- Çip satıcıları, derin öğrenmenin verimliliği hakkında çok daha ince
tanımlanmış bir içgörü elde etmek için gelişmiş başarım çözümleme
araçları sunar.
.. raw:: html
.. raw:: html
- MXNet'in bellek yönetiminden Python'a dönüşümlerin arka işlemciyi
belirli değişken hazır olana kadar beklemeye zorlayacağını unutmayın.
``print``, ``asnumpy`` ve ``item`` gibi işlevlerin hepsi bu etkiye
sahiptir. Bu arzu edilebilir, ancak eşzamanlılığın dikkatsiz
kullanımı performansı mahvedebilir.
.. raw:: html
.. raw:: html
Alıştırmalar
------------
.. raw:: html
.. raw:: html
1. Yukarıda, eşzamansız hesaplama kullanmanın, 10000 hesaplamayı
gerçekleştirmek için gereken toplam süreyi
:math:`t_1 + 10000 t_2 + t_3`'a indirebileceğini belirtmiştik. Neden
burada :math:`10000 t_2 > 9999 t_1`'yi varsaymak zorundayız?
2. ``waitall`` ve ``wait_to_read`` arasındaki farkı ölçün. İpucu: Bir
dizi talimat uygulayın ve bir ara sonuç için senkronize edin.
`Tartışmalar `__
.. raw:: html
.. raw:: html
1. CPU'da, bu bölümdeki aynı matris çarpma işlemlerini karşılaştırın.
Arka işlemci üzerinden hala eşzamansızlık gözlemleyebilir misiniz?
`Tartışmalar `__
.. raw:: html
.. raw:: html