.. _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
mxnetpytorch
.. 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
mxnetpytorch
.. 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
mxnetpytorch
.. 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