Takip et

Büyük Modelleri Tek Bir Makineye Sığdıramadığınızda Ne Yapmalısınız?

Büyük Modelleri Tek Bir Makineye Sığdıramadığınızda Ne Yapmalısınız?

Günümüzde yapay zeka dünyası, inanılmaz boyutlara ulaşan büyük dil modelleri (LLM’ler) ile çalkalanıyor. Milyarlarca, hatta trilyonlarca parametreye sahip bu modeller, inanılmaz yetenekler sergilese de, eğitimleri ve hatta çıkarımları tek bir güçlü GPU’ya veya sunucuya sığdırmak neredeyse imkansız hale geliyor. İşte tam da bu noktada, dağıtık hesaplama teknikleri devreye giriyor. Bu rehberde, büyük modellerin eğitimini ve kullanımını mümkün kılan iki temel paralellik stratejisi olan Tensor Paralelliği ve Pipeline Paralelliği’ni derinlemesine inceleyeceğiz. Bu teknikleri adım adım öğrenecek, gerçek dünya senaryolarında nasıl kullanıldıklarını görecek ve kendi projelerinizde bu gücü nasıl kullanabileceğinizi keşfedeceksiniz.

Özellikle LLM’lerin hızla geliştiği bu dönemde, modellerin giderek büyümesi, mevcut donanım sınırlarını zorluyor. Tek bir GPU’nun bellek kapasitesi ve işlem gücü, bu devasa ağ yapılarını barındırmak için yetersiz kalıyor. Bu durum, araştırmacıları ve mühendisleri, eğitimi ve çıkarımı daha küçük, yönetilebilir parçalara bölerek birden fazla işlem biriminde çalıştırma yöntemleri aramaya itiyor. Tensor paralelliği ve pipeline paralelliği, bu soruna çözüm sunan en etkili yaklaşımlardan ikisidir.

Bu makale, bu karmaşık konuları anlaşılır bir dille ele alacak. Konuya tamamen yabancı olanlar için temel kavramlardan başlayarak, daha deneyimli kullanıcılar için ileri düzey ipuçlarına kadar geniş bir yelpazede bilgi sunacağız. Amacımız, okuyucuların bu güçlü teknikleri hem kavramsal olarak anlamalarını hem de pratik uygulamalarda kullanabilmelerini sağlamaktır. Hazırsanız, büyük modellerin dünyasında bir yolculuğa çıkalım ve dağıtık eğitim stratejilerinin inceliklerini keşfedelim.

Tensor Paralelliği ve Pipeline Paralelliği: Temel Farklar Nelerdir?

Büyük ölçekli derin öğrenme modellerinin eğitiminde karşılaşılan en büyük zorluklardan biri, modelin kendisinin ve eğitim sırasında üretilen ara aktivasyonların tek bir işlem biriminin belleğine sığmamasıdır. Bu sorunu çözmek için iki ana paralellik stratejisi öne çıkar: Tensor Paralelliği ve Pipeline Paralelliği. Her ikisi de modeli birden fazla cihaza dağıtmayı hedefler, ancak bunu yapma biçimleri temelden farklıdır.

Tensor Paralelliği, modelin tek bir katmanındaki büyük matris çarpımlarını veya diğer tensör operasyonlarını birden fazla cihaza bölerek çalışır. Örneğin, bir fully-connected katmanındaki ağırlık matrisi çok büyükse, bu matrisin satırları veya sütunları farklı cihazlara atanabilir. Her cihaz, kendi alt matrisleri üzerinde hesaplama yapar ve ardından sonuçlar birleştirilir. Bu yaklaşım, özellikle büyük matris çarpımlarının yoğun olduğu Transformer mimarilerinde oldukça etkilidir. Avantajı, iletişim maliyetlerinin genellikle katman içi operasyonlarla sınırlı olmasıdır, bu da daha düşük gecikme süresi sağlayabilir. Ancak, her cihazın aynı katmanın tamamını işlemesi gerektiği için, katmanın kendisinin cihaz belleğine sığabilmesi gerekir. Eğer bir katmanın tamamı bile tek bir cihaza sığmıyorsa, tensor paralelliği tek başına yeterli olmayabilir.

Öte yandan, Pipeline Paralelliği, modelin farklı katmanlarını farklı cihazlara atayarak çalışır. Model, bir dizi sıralı aşamaya (pipeline) bölünür ve her aşama bir veya daha fazla cihazda çalışır. Veri, ilk aşamaya girer, işlenir, ardından ara sonuçlar bir sonraki aşamaya gönderilir ve bu işlem modelin sonuna kadar devam eder. Bu, bir üretim hattına benzetilebilir; her istasyon (cihaz) kendi görevini yapar ve ürünü (veri) bir sonraki istasyona iletir. Pipeline paralelliğinin ana avantajı, tek bir cihazın belleğine sığmayacak kadar büyük modelleri eğitebilmesidir. Çünkü her cihaz sadece modelin bir alt kümesini (bir dizi katmanı) barındırır. Dezavantajı ise, aşamalar arasındaki veri aktarımı ve “baloncuk” (bubble) adı verilen, bazı cihazların boşta kaldığı zaman dilimleri nedeniyle potansiyel olarak daha yüksek gecikme süresidir.

Bu iki strateji genellikle birbirini dışlamaz; aksine, genellikle birlikte kullanılırlar. Örneğin, çok büyük bir modelde hem pipeline paralelliği ile katmanlar farklı cihazlara dağıtılır, hem de her bir cihaz grubunda tensor paralelliği ile katman içindeki büyük matrisler daha da küçük parçalara bölünerek birden fazla GPU’ya yayılabilir. Bu hibrit yaklaşımlar, en büyük ve en karmaşık modelleri eğitebilmek için en etkili çözümleri sunar.

Tensor Paralelliği: Katmanları Bölmenin Gücü

Tensor paralelliği, adından da anlaşılacağı gibi, bir sinir ağının katmanlarındaki tensör (veya matris) operasyonlarını birden fazla işlem birimi (genellikle GPU) arasında bölme stratejisidir. Bu yaklaşım, özellikle derin öğrenme modellerinin kalbinde yer alan büyük matris çarpımlarını daha küçük, yönetilebilir parçalara ayırarak paralel işlemeyi hedefler. Bir katmandaki ağırlık matrisinin veya girdi tensörünün çok büyük olması durumunda, bu büyük tensörleri farklı cihazlara dağıtarak her cihazın sadece bir kısmını işlemesini sağlarız.

En yaygın tensor paralelliği uygulamalarından biri, bir fully-connected (tam bağlı) katmanındaki ağırlık matrisini bölmektir. Bir tam bağlı katman, girdiyi bir ağırlık matrisi ile çarpar ve ardından bir bias ekler. Matematiksel olarak bu işlem $Y = XW + b$ şeklinde ifade edilebilir, burada $X$ girdi tensörü, $W$ ağırlık matrisi, $b$ bias vektörü ve $Y$ çıktı tensörüdür. Eğer $W$ matrisi çok büyükse, onu sütunlara veya satırlara göre bölebiliriz.

Sütunlara Göre Bölme (Column Parallelism): Ağırlık matrisi $W$ sütunlarına göre bölündüğünde, $W = [W_1 | W_2 | … | W_n]$ olur. Her cihaz $i$, girdiyi $X$ ile sadece kendi alt matrisi $W_i$ üzerinde çarpar: $Y_i = XW_i$. Bu durumda, her cihazın çıktısı $Y_i$ olur ve tüm cihazların çıktıları birleştirilerek nihai çıktı $Y$ elde edilir. Bu bölme türü, genellikle her cihazın çıktısının birleştirilmesini gerektirir, bu da bir iletişim adımı anlamına gelir.

Satırlara Göre Bölme (Row Parallelism): Ağırlık matrisi $W$ satırlarına göre bölündüğünde, $W = \begin{bmatrix} W_1 \\ W_2 \\ … \\ W_n \end{bmatrix}$ olur. Bu durumda, girdi $X$ öncelikle tüm cihazlara yayılır. Her cihaz $i$, kendi alt matrisi $W_i$ ile çarparak bir ara çıktı üretir: $Z_i = XW_i$. Ancak bu, tam bir katman çıktısı değildir. Nihai çıktı $Y$ elde etmek için, tüm cihazlardan gelen ara çıktılar toplanır: $Y = \sum Z_i$. Bu bölme türü, girdi verisinin tüm cihazlara dağıtılmasını ve çıktıların toplanmasını gerektirir.

Transformer modellerinde, özellikle self-attention mekanizmasındaki büyük matris çarpımları ve feed-forward ağları, tensor paralelliği için ideal adaylardır. Örneğin, bir self-attention katmanındaki sorgu (Q), anahtar (K) ve değer (V) projeksiyon matrisleri veya feed-forward ağındaki ilk doğrusal katmanın ağırlık matrisi, tensor paralelliği ile bölünebilir. Bu, tek bir katmanın hesaplama yükünü birden fazla GPU’ya yayarak, o katmanın daha hızlı işlenmesini sağlar.

Tensor paralelliğinin uygulanması, dikkatli bir senkronizasyon ve iletişim yönetimi gerektirir. Cihazlar arasında veri alışverişi (örneğin, matris çarpımının sonuçlarını birleştirmek veya ara sonuçları toplamak için) ağ bant genişliği ve gecikme süresi açısından darboğazlar oluşturabilir. Bu nedenle, tensor paralelliği genellikle aynı düğüm içindeki GPU’lar arasında, yani yüksek hızlı interconnect’ler (NVLink gibi) ile birbirine bağlı GPU’lar arasında daha etkilidir. Farklı düğümlerdeki GPU’lar arasında tensor paralelliği uygulamak, iletişim maliyetleri nedeniyle genellikle daha az tercih edilir.

Vaka Analizi: Megatron-LM ve Tensor Paralelliği
NVIDIA’nın Megatron-LM kütüphanesi, büyük dil modellerini eğitmek için tensor paralelliğini etkili bir şekilde kullanan öncü projelerden biridir. Megatron-LM, Transformer modellerinin self-attention ve feed-forward modüllerindeki büyük doğrusal katmanları sütunlara göre bölerek tensor paralelliğini uygular. Bu sayede, tek bir GPU’nun belleğine sığmayan modellerin eğitimi mümkün hale gelir. Örneğin, 3456 boyutlu gizli durum vektörüne sahip bir modelde, feed-forward katmanındaki ara boyut 13824 olabilir. Bu, tek bir matris çarpımında devasa bir ağırlık matrisi anlamına gelir. Megatron-LM, bu matrisi birden fazla GPU’ya dağıtarak hesaplama süresini önemli ölçüde azaltır. Kütüphane, bu paralelleştirme stratejisini, verimliliği artırmak için pipeline paralelliği ile birleştirerek daha da ölçeklenebilir hale getirir.

Pipeline Paralelliği: Modeli Sıralı Aşamalarla İşlemek

Pipeline paralelliği, modeli ardışık katmanlar halinde düşünüp, bu katmanları farklı cihazlara atayarak çalışır. Bu yaklaşım, bir üretim hattına benzetilebilir. Modelin giriş verisi, hattın ilk cihazına ulaşır, orada işlenir ve ara sonuçlar bir sonraki cihaza aktarılır. Bu süreç, modelin son katmanına ulaşana kadar devam eder. Her cihaz, modelin belirli bir bölümünden sorumludur ve bu bölümü tamamladığında, veriyi bir sonraki cihaza iletir.

Pipeline paralelliğinin temel amacı, tek bir cihazın belleğine sığmayacak kadar büyük modelleri eğitebilmektir. Çünkü her cihaz, modelin tamamını değil, sadece bir alt kümesini (bir dizi katmanı) barındırır. Örneğin, 100 katmanlı bir modelimiz varsa, bunu 10 cihaza dağıtabiliriz; her cihaz 10 katmandan sorumlu olur. Bu, toplam bellek gereksinimini önemli ölçüde azaltır.

Pipeline paralelliğinin en büyük zorluklarından biri, cihazların verimliliğini en üst düzeye çıkarmaktır. Eğer veri, bir cihazdan diğerine sırayla aktarılırsa, ilk cihazlar işlemleri bitirdikten sonra, son cihazlar işlemleri bitirene kadar boşta kalabilir. Bu durum, “pipeline bubble” (boru hattı balonu) olarak adlandırılır ve verimliliği düşürür. Bu baloncukları azaltmak için, pipeline paralelliği genellikle mikro-batching (mikro-yığınlama) ile birlikte kullanılır.

Mikro-Batching: Pipeline paralelliğinde, tam veri yığını (batch) yerine, veri yığını daha küçük parçalara (mikro-batch’ler) bölünür. Bu mikro-batch’ler, pipeline boyunca sıralı olarak gönderilir. Örneğin, 8 mikro-batch’imiz varsa ve 4 cihazımız varsa, ilk mikro-batch cihaz 1’e gider, işlenir ve cihaz 2’ye geçer. Bu sırada, ikinci mikro-batch cihaz 1’e girer. Bu şekilde, pipeline boyunca birden fazla mikro-batch aktif olabilir, bu da cihazların sürekli meşgul kalmasını sağlar ve pipeline balonlarını azaltır.

Pipeline paralelliğinin uygulanması, cihazlar arasında veri aktarımı ve senkronizasyon gerektirir. Her aşamanın çıktısı, bir sonraki aşamanın girdisi olur. Bu, ileri geçiş (forward pass) sırasında gerçekleşir. Geri yayılım (backward pass) sırasında ise, hata gradyanları ters yönde akar. Bu gradyanların doğru bir şekilde toplanması ve cihazlar arasında iletilmesi kritik öneme sahiptir.

Kademeli Pipeline Paralelliği (Interleaved Pipeline Parallelism): Daha gelişmiş pipeline paralelliği teknikleri, katmanları daha ince parçalara bölerek ve cihazlara daha karmaşık bir şekilde atayarak verimliliği daha da artırır. Örneğin, “interleaved pipeline parallelism” (kademeli pipeline paralelliği), modelin ardışık katmanlarını farklı cihaz gruplarına atayabilir. Bu, daha iyi iş yükü dengelemesi ve daha az pipeline balonu sağlayabilir.

Vaka Analizi: GPipe ve Pipeline Paralelliği
Google tarafından geliştirilen GPipe, pipeline paralelliğini uygulamak için popüler bir kütüphanedir. GPipe, bir modeli ardışık aşamalara böler ve her aşamayı bir veya daha fazla cihaza atar. Mikro-batching kullanarak, pipeline balonlarını azaltmayı ve cihazların verimliliğini artırmayı hedefler. GPipe, büyük modellerin eğitimini, tek bir cihazın belleğine sığmayacak şekilde dağıtarak mümkün kılar. Örneğin, bir GPT-3 benzeri modeli eğitirken, modelin farklı bölümleri farklı GPU gruplarına atanır ve veri bu gruplar arasında akış halinde işlenir. Bu, milyarlarca parametreye sahip modellerin bile dağıtık bir sistem üzerinde eğitilmesine olanak tanır.

Hibrit Paralellik: Tensor ve Pipeline’ı Birlikte Kullanmak

Günümüzde, en büyük ve en karmaşık derin öğrenme modellerini (örneğin, GPT-3, LaMDA, PaLM gibi milyarlarca hatta trilyonlarca parametreye sahip modeller) eğitmek için sadece tensor paralelliği veya sadece pipeline paralelliği yeterli değildir. Bu modellerin gerektirdiği muazzam hesaplama gücü ve bellek kapasitesi, her iki stratejinin de birlikte kullanıldığı hibrit paralellik yaklaşımlarını zorunlu kılar.

Hibrit paralellik, modelin farklı yönlerini farklı paralellik stratejileriyle ölçeklendirmeyi içerir. En yaygın hibrit yaklaşım, veri paralelliği (data parallelism) ile tensor ve pipeline paralelliğini birleştirmektir. Veri paralelliğinde, modelin tam bir kopyası her işlem biriminde bulunur ve her birim, veri kümesinin farklı bir alt kümesi üzerinde çalışır. Gradyanlar toplanır ve model güncellenir. Bu, modelin kendisi tek bir cihaza sığdığında kullanılır.

Ancak, model tek bir cihaza sığmadığında, veri paralelliği ile birlikte tensor ve pipeline paralelliği devreye girer. Bu durumda, genellikle şu şekilde bir hiyerarşi kurulur:

  1. Pipeline Paralelliği (Düğüm Düzeyinde): En üst düzeyde, modelin katmanları ardışık aşamalara bölünür ve bu aşamalar farklı düğümlere (sunuculara) atanır. Her düğüm, modelin belirli bir bölümünden sorumludur.
  2. Tensor Paralelliği (Düğüm İçi Düzeyde): Her bir düğüm içinde, birden fazla GPU bulunur. Eğer modelin bir aşaması, tek bir düğümün tüm GPU’larına sığmayacak kadar büyükse, tensor paralelliği devreye girer. Bu aşamadaki büyük tensör operasyonları, düğüm içindeki GPU’lar arasında bölünür.
  3. Veri Paralelliği (Küme Düzeyinde): Tüm bu pipeline ve tensor paralelliği ile yapılandırılmış model kopyaları, veri paralelliği için birden fazla kez çoğaltılabilir. Her kopya, veri kümesinin farklı bir alt kümesi üzerinde çalışır ve gradyanlar toplanarak model güncellenir.

Bu üçlü paralellik stratejisi, en büyük modelleri bile dağıtık bir hesaplama kümesinde verimli bir şekilde eğitmeyi mümkün kılar. Örneğin, 1000 adet GPU’ya sahip bir kümede, bu stratejiler şu şekilde uygulanabilir:

  • Model, 10 aşamaya bölünür (pipeline paralelliği).
  • Her aşama 10 düğüme dağıtılır (her düğümde 10 GPU var).
  • Her düğüm içindeki 10 GPU, aşamanın hesaplamalarını tensor paralelliği ile paylaşır.
  • Toplamda 100 adet bu şekilde yapılandırılmış model kopyası (her biri 10 GPU’dan oluşur) veri kümesinin farklı alt kümeleri üzerinde veri paralelliği ile çalışır.

Bu karmaşık ama güçlü yapı, hem modelin bellek gereksinimini hem de hesaplama yükünü dağıtarak ölçeklenebilirliği en üst düzeye çıkarır. Ancak, bu hibrit yaklaşımların uygulanması ve yönetimi oldukça karmaşıktır ve özel yazılım kütüphaneleri (örneğin, DeepSpeed, Megatron-LM, FairScale) gerektirir.

Vaka Analizi: DeepSpeed ve ZeRO
Microsoft’un DeepSpeed kütüphanesi, büyük modellerin eğitimi için tasarlanmış kapsamlı bir optimizasyon ve paralellik çerçevesidir. DeepSpeed’in en dikkat çekici özelliklerinden biri ZeRO (Zero Redundancy Optimizer) adı verilen bellek optimizasyonudur. ZeRO, veri paralelliği sırasında model durumlarını (parametreler, gradyanlar, optimizer durumları) farklı cihazlara dağıtarak bellek gereksinimini önemli ölçüde azaltır. ZeRO’nun farklı seviyeleri (ZeRO-1, ZeRO-2, ZeRO-3), bu durumların ne kadarının paylaşıldığını belirler. ZeRO-3, parametreleri, gradyanları ve optimizer durumlarını tüm cihazlara dağıtarak en yüksek bellek tasarrufunu sağlar. DeepSpeed, bu ZeRO optimizasyonlarını tensor ve pipeline paralelliği ile birleştirerek, trilyonlarca parametreye sahip modellerin bile tek bir makineye sığdırılmadan eğitilmesini mümkün kılar.

Uygulamalı Kısım: Tensor Paralelliği ile Basit Bir Örnek

Bu bölümde, tensor paralelliğinin temel mantığını anlamak için basit bir PyTorch örneği üzerinden ilerleyeceğiz. Burada, bir fully-connected (tam bağlı) katmanındaki ağırlık matrisini sütunlara göre böleceğiz. Amacımız, kodun nasıl çalıştığını ve verinin nasıl dağıtıldığını göstermektir. Gerçek dünya senaryolarında bu işlem çok daha karmaşık olabilir ve genellikle özel kütüphaneler tarafından yönetilir.

Öncelikle, gerekli kütüphaneleri içe aktaralım ve birkaç varsayımsal GPU tanımlayalım. Bu örnek, tek bir makinede birden fazla GPU olduğunu varsayacaktır. Eğer birden fazla makine kullanılıyorsa, dağıtık ortam kurulumu ve torch.distributed modülünün kullanılması gerekecektir.

import torch
 import torch.nn as nn
 import torch.distributed as dist
 from torch.nn.parallel import DistributedDataParallel as DDP
 import os

 # Varsayımsal dağıtık ortam kurulumu (tek makine için)
 # Gerçek bir dağıtık eğitimde bu değerler farklı olacaktır.
 os.environ['MASTER_ADDR'] = 'localhost'
 os.environ['MASTER_PORT'] = '29500' # Rastgele bir port

 # GPU sayısını belirleyelim (örneğin, 2 GPU)
 world_size = 2
 rank = int(os.environ.get('RANK', 0)) # Bu örnekte manuel olarak ayarlanacak
 local_rank = int(os.environ.get('LOCAL_RANK', rank)) # Bu örnekte manuel olarak ayarlanacak

 # Gerçek bir senaryoda bu komutlar terminalden çalıştırılır:
 # torchrun --nproc_per_node=2 your_script.py

 # Bu örnek için manuel rank ve local_rank ataması:
 # Eğer bu kod tek başına çalıştırılıyorsa, her çalıştırmada farklı bir rank atayabilirsiniz.
 # Örneğin, terminalde python your_script.py komutunu çalıştırırken,
 # rank 0 için ayrı, rank 1 için ayrı çalıştırabilirsiniz.
 # Veya daha basit bir yaklaşım olarak, tek bir script içinde simüle edebiliriz.

 # Bu örnek, tek bir script içinde simülasyon yapacaktır.
 # Gerçek dağıtık uygulamada bu 'torch.distributed.launch' veya 'torchrun' ile yönetilir.

 if torch.cuda.is_available():
  torch.cuda.set_device(local_rank)
  dist.init_process_group("nccl", rank=rank, world_size=world_size)
  print(f"Rank {rank} on device {local_rank} initialized.")
 else:
  print("CUDA not available. This example requires GPUs.")
  exit()

Şimdi, tensor paralelliğini uygulayacağımız basit bir fully-connected katmanı oluşturalım. Bir tam bağlı katman, girdi vektörünü bir ağırlık matrisi ile çarpar. Eğer bu ağırlık matrisi çok büyükse, onu sütunlara bölebiliriz.

Ağırlık matrisini sütunlara göre böldüğümüzde, her GPU kendi alt matrisi ile çarpım yapacak ve ardından sonuçları birleştirecektir. Bu birleştirme işlemi için all_gather gibi dağıtık iletişim operasyonları kullanılır.

# Katmanın giriş ve çıkış boyutları
 input_dim = 1024
 output_dim = 2048 # Bu boyutu bölmek istediğimizi varsayalım

 # Ağırlık matrisini sütunlara böleceğiz.
 # Her GPU, output_dim'in bir kısmından sorumlu olacak.
 # Örneğin, world_size = 2 ise, her GPU output_dim / 2 boyutunda bir çıktı üretecek.
 output_dim_per_gpu = output_dim // world_size

 # Basit bir tam bağlı katman
 class ColumnParallelLinear(nn.Module):
  def __init__(self, input_features, output_features, bias=True):
  super().__init__()
  self.input_features = input_features
  self.output_features = output_features
  self.use_bias = bias

  # Ağırlık matrisini sütunlara göre böleceğiz.
  # Her GPU, output_features'ın bir kısmından sorumlu olacak.
  self.weight = nn.Parameter(torch.Tensor(input_features, output_features))
  if bias:
  self.bias = nn.Parameter(torch.Tensor(output_features))
  else:
  self.register_parameter('bias', None)

  self.reset_parameters()

  # Bu katmanı dağıtık hale getireceğiz.
  # Her GPU sadece kendi sütunlarından sorumlu olacak.
  self.weight_split = torch.split(self.weight, output_dim_per_gpu, dim=1)
  if self.bias is not None:
  self.bias_split = torch.split(self.bias, output_dim_per_gpu, dim=0)

  # Her GPU'da sadece kendi parçasını tutacağız.
  self.weight_gpu = self.weight_split[rank].clone().detach().requires_grad_(True)
  if self.bias is not None:
  self.bias_gpu = self.bias_split[rank].clone().detach().requires_grad_(True)
  else:
  self.bias_gpu = None

  # Girdiyi tüm GPU'lara yayınlayacağız.
  # Çıktıları ise tüm GPU'lardan toplayacağız.

  def reset_parameters(self):
  # Keras'tan ilham alan başlatma
  nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
  if self.bias is not None:
  fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
  bound = 1 / math.sqrt(fan_in)
  nn.init.uniform_(self.bias, -bound, bound)

  def forward(self, x):
  # Girdi x'in boyutu (batch_size, input_dim) olmalı.
  # Bu girdi x'i tüm GPU'lara yayınlamalıyız.
  if self.bias is not None:
  # Girdiyi tüm GPU'lara yayınla
  x_gathered = [torch.zeros_like(x) for _ in range(world_size)]
  dist.all_gather(x_gathered, x)
  x_gathered = torch.cat(x_gathered, dim=0) # Bu aslında doğru değil, x'in kendisi broadcast edilmeli.
  # Doğrusu:
  # x_gpu = x.clone().detach() # Sadece mevcut GPU'nun girdisi
  # dist.broadcast(x_gpu, src=0) # Kaynak rank'tan diğerlerine yayınla

  # Daha basit bir broadcast simülasyonu:
  if rank == 0:
  x_broadcast = x.clone().detach()
  else:
  x_broadcast = torch.zeros_like(x)
  dist.broadcast(x_broadcast, src=0)

  # Kendi ağırlık matrisi ile çarpım yap
  output_gpu = torch.matmul(x_broadcast, self.weight_gpu)
  if self.bias_gpu is not None:
  output_gpu += self.bias_gpu

  # Çıktıları tüm GPU'lardan topla
  output_all = [torch.zeros_like(output_gpu) for _ in range(world_size)]
  dist.all_gather(output_all, output_gpu)
  output = torch.cat(output_all, dim=1) # Sütunlar boyunca birleştir

  return output

 # Örnek kullanım
 batch_size = 32
 input_tensor = torch.randn(batch_size, input_dim)

 # Eğer GPU yoksa veya simülasyon yapmıyorsak bu kısmı atla
 if torch.cuda.is_available():
  input_tensor = input_tensor.to(local_rank)

  # Tensor paralelliği uygulanan katmanı oluştur
  # Normalde, bu katman bir modelin parçası olurdu.
  # Örneğin, bir Transformer'ın feed-forward katmanındaki ilk lineer katman.
  parallel_layer = ColumnParallelLinear(input_dim, output_dim, bias=True).to(local_rank)

  # İleri geçiş
  output_tensor = parallel_layer(input_tensor)

  print(f"Rank {rank}: Input shape: {input_tensor.shape}")
  print(f"Rank {rank}: Output shape: {output_tensor.shape}")

  # Çıktının doğru olup olmadığını kontrol edelim (sadece rank 0 için)
  if rank == 0:
  # Karşılaştırma için standart bir katman oluşturalım
  standard_layer = nn.Linear(input_dim, output_dim, bias=True).to(local_rank)
  standard_layer.weight.data = parallel_layer.weight.data # Ağırlıkları eşitle
  standard_layer.bias.data = parallel_layer.bias.data # Biasları eşitle

  standard_output = standard_layer(input_tensor)
  print(f"Standard layer output shape: {standard_output.shape}")

  # Çıktıları karşılaştır
  if torch.allclose(output_tensor, standard_output, atol=1e-5):
  print("Tensor parallelism output matches standard layer output.")
  else:
  print("Tensor parallelism output DOES NOT match standard layer output.")

 # Dağıtık ortamı temizle
 dist.destroy_process_group()

Yukarıdaki kod, ColumnParallelLinear adında basit bir modül tanımlar. Bu modül, ağırlık matrisini sütunlara böler. Her GPU, girdiyi alır, kendi ağırlık alt matrisi ile çarpar, bias ekler ve ardından bu yerel çıktıları all_gather ile birleştirerek nihai çıktıyı oluşturur. Bu, tensor paralelliğinin temel mantığını gösterir: hesaplama yükünü ve bellek gereksinimini birden fazla cihaza yaymak.

Gerçek dünya uygulamalarında, bu türden bir paralellik genellikle Megatron-LM veya DeepSpeed gibi kütüphaneler tarafından otomatik olarak yönetilir. Bu kütüphaneler, karmaşık model mimarilerinde tensor paralelliğini verimli bir şekilde uygulamak için optimize edilmiş iletişim stratejileri ve bellek yönetimi sunar.

Önemli Notlar:

  • Yukarıdaki örnek, temel kavramları göstermek amacıyla basitleştirilmiştir. Gerçek dağıtık eğitimde, torch.distributed paketinin daha gelişmiş özellikleri ve torchrun gibi araçlar kullanılır.
  • Bu örnekte, girdi x‘in broadcast edilmesi yerine all_gather ile toplanması ve ardından cat ile birleştirilmesi gösterilmiştir. Daha verimli bir yaklaşım, girdiyi dist.broadcast ile ana kaynaktan (genellikle rank 0) diğerlerine yayınlamaktır.
  • Geri yayılım (backward pass) bu örnekte gösterilmemiştir, ancak tensor paralelliği geri yayılım sırasında da gradyanların doğru bir şekilde toplanmasını ve dağıtılmasını gerektirir.

İleri Düzey: Optimizasyonlar ve İpuçları

Tensor ve pipeline paralelliği, büyük modelleri eğitmek için güçlü araçlar olsa da, verimliliği en üst düzeye çıkarmak ve performans darboğazlarını aşmak için dikkate alınması gereken birçok ince ayar ve optimizasyon bulunmaktadır. Bu bölümde, daha deneyimli kullanıcılar için bazı ileri düzey ipuçları ve stratejiler sunacağız.

1. İletişim Maliyetlerini Azaltma:

  • Çapraz Düğüm (Cross-Node) İletişimi: Tensor paralelliği genellikle aynı düğüm içindeki GPU’lar arasında (NVLink gibi yüksek hızlı bağlantılarla) daha etkilidir. Eğer tensor paralelliğini farklı düğümler arasında uyguluyorsanız, ağ bant genişliği ve gecikme süresi büyük bir darboğaz olabilir. Bu durumda, iletişim operasyonlarını toplu hale getirmek (örneğin, birden fazla küçük iletiyi tek bir büyük ileti haline getirmek) ve daha az sıklıkta iletişim kurmak faydalı olabilir.
  • Gradient Accumulation: Pipeline paralelliğinde oluşan “pipeline bubble”ları azaltmak için mikro-batching kullanılır. Bununla birlikte, daha da ileri giderek, gradyan biriktirme (gradient accumulation) tekniği ile küçük mikro-batch’ler üzerinde hesaplanan gradyanlar toplanır ve daha sonra toplu olarak geri yayılım yapılır. Bu, iletişim sıklığını azaltır ve bellek kullanımını optimize eder.
  • All-Reduce vs. All-Gather: Tensor paralelliğinde, eğer çıktılar toplanacaksa all_gather kullanılır. Eğer gradyanlar toplanacaksa all_reduce kullanılır. all_reduce, veriyi toplar ve tüm katılımcılara dağıtır, bu da genellikle all_gather‘dan daha verimlidir çünkü ek bir kopyalama adımı gerektirmez.

2. Bellek Optimizasyonu:

  • ZeRO Optimizer (DeepSpeed): Daha önce bahsedildiği gibi, ZeRO optimizer, veri paralelliği sırasında parametreleri, gradyanları ve optimizer durumlarını dağıtarak bellek kullanımını dramatik şekilde azaltır. ZeRO-3, en agresif bellek tasarrufu sağlayanıdır.
  • Offloading: Eğer GPU belleği hala yetersiz kalıyorsa, bazı model durumları (örneğin, parametreler veya optimizer durumları) CPU belleğine veya hatta NVMe SSD’lere taşınabilir (offloading). Bu, bellek kapasitesini artırır ancak işlem süresini de artırabilir.
  • Activation Checkpointing (Gradient Checkpointing): Bu teknik, geri yayılım sırasında hesaplanan ara aktivasyonları saklamak yerine, geri yayılım sırasında tekrar hesaplar. Bu, bellek kullanımını önemli ölçüde azaltır, ancak ileri geçiş süresini bir miktar artırır. Büyük modellerde genellikle tercih edilen bir yöntemdir.

3. Hibrit Paralellik Stratejilerinin Ayarlanması:

  • Model Paralelliği (Tensor + Pipeline) ve Veri Paralelliği Dengesi: En iyi performansı elde etmek için tensor, pipeline ve veri paralelliği arasındaki doğru dengeyi bulmak kritiktir. Genellikle, modelin büyüklüğüne ve mevcut donanıma bağlı olarak bu oranlar ayarlanır. Örneğin, çok geniş katmanlara sahip modeller tensor paralelliğinden daha fazla yararlanırken, çok derin modeller pipeline paralelliğinden daha fazla yararlanır.
  • Kademeli Pipeline Paralelliği (Interleaved Pipeline): Standart pipeline paralelliği yerine, katmanları daha karmaşık bir şekilde cihazlara dağıtan kademeli pipeline teknikleri, daha iyi iş yükü dengelemesi ve daha az pipeline balonu sağlayabilir.

4. Kütüphane ve Araç Seçimi:

  • DeepSpeed, Megatron-LM, FairScale: Bu kütüphaneler, dağıtık eğitim için optimize edilmiş çözümler sunar. Kendi paralellik stratejilerinizi sıfırdan uygulamak yerine, bu kütüphaneleri kullanmak, geliştirme süresini kısaltır ve performansı artırır.
  • Hugging Face Transformers ve Accelerate: Hugging Face’in Transformers kütüphanesi, birçok büyük dil modelinin kolayca kullanılabilmesini sağlar. Accelerate kütüphanesi ise, bu modelleri dağıtık ortamlarda eğitmek için basit bir arayüz sunar ve arka planda DeepSpeed gibi araçları kullanabilir.

5. Profiling ve Hata Ayıklama:

  • Performans Profiling: PyTorch Profiler, TensorBoard Profiler gibi araçlar kullanarak eğitim sürecindeki darboğazları belirleyin. İletişim süreleri, hesaplama süreleri ve bellek kullanımı gibi metrikleri izleyin.
  • Dağıtık Hata Ayıklama: Dağıtık sistemlerde hata ayıklama zordur. torch.distributed.barrier() gibi senkronizasyon noktaları eklemek, adımların nerede takıldığını anlamaya yardımcı olabilir. Loglama stratejilerini dikkatli kullanmak da önemlidir.

Bu ileri düzey teknikler, büyük modellerin eğitimini daha verimli ve ölçeklenebilir hale getirerek, yapay zeka alanındaki sınırları zorlamamıza olanak tanır.

Sonuç ve Sıkça Sorulan Sorular

Büyük dil modellerinin giderek büyümesiyle birlikte, tek bir makinenin sınırlarını aşan eğitim stratejileri kaçınılmaz hale gelmiştir. Bu rehberde, bu zorlukların üstesinden gelmek için kullanılan iki temel paralellik tekniği olan Tensor Paralelliği ve Pipeline Paralelliği’ni derinlemesine inceledik. Tensor paralelliği, katman içindeki büyük tensör operasyonlarını birden fazla cihaza bölerek hesaplama yükünü dağıtırken, pipeline paralelliği modelin katmanlarını farklı cihazlara atayarak daha büyük modellerin belleğe sığmasını sağlar.

Bu iki tekniğin genellikle birlikte kullanıldığı hibrit paralellik yaklaşımları, özellikle veri paralelliği ile birleştiğinde, milyarlarca hatta trilyonlarca parametreye sahip modellerin bile etkili bir şekilde eğitilmesine olanak tanır. DeepSpeed ve Megatron-LM gibi gelişmiş kütüphaneler, bu karmaşık stratejileri uygulamak ve bellek ile iletişim optimizasyonları sağlamak için güçlü araçlar sunmaktadır.

Yapay zeka modelleri daha da karmaşıklaştıkça ve büyüdükçe, bu dağıtık eğitim tekniklerinin önemi artmaya devam edecektir. Bu rehberin, bu güçlü araçları anlamanıza ve kendi projelerinizde kullanmanıza yardımcı olmasını umuyoruz.

Sıkça Sorulan Sorular (SSS)

Tensor paralelliği ve Pipeline paralelliği arasındaki temel fark nedir?
Tensor paralelliği, bir katmanın içindeki büyük matris çarpımları gibi tensör operasyonlarını birden fazla cihaza böler. Pipeline paralelliği ise, modelin farklı katmanlarını farklı cihazlara atayarak modeli sıralı aşamalara böler.
Hangi paralellik stratejisi daha iyidir?
Tek bir “en iyi” strateji yoktur. Seçim, modelin mimarisine, boyutuna ve mevcut donanıma bağlıdır. Genellikle, en büyük modeller için tensor ve pipeline paralelliğinin hibrit bir kombinasyonu kullanılır. Eğer model tek bir cihaza sığıyorsa, veri paralelliği yeterli olabilir.
Pipeline paralelliğindeki “pipeline bubble” nedir ve nasıl azaltılır?
Pipeline bubble, pipeline paralelliğinde bazı cihazların boşta kaldığı zaman dilimidir. Bu, verinin bir aşamadan diğerine akışındaki gecikmelerden kaynaklanır. Mikro-batching (veriyi küçük parçalara bölmek) ve gradyan biriktirme gibi tekniklerle azaltılabilir.
Tensor paralelliği için en uygun donanım nedir?
Tensor paralelliği, özellikle aynı düğüm içindeki GPU’lar arasında (NVLink gibi yüksek hızlı bağlantılarla) daha etkilidir. Farklı düğümler arasındaki tensor paralelliği, ağ bant genişliği ve gecikme süresi nedeniyle daha maliyetli olabilir.
Büyük modelleri eğitmek için hangi kütüphaneler önerilir?
DeepSpeed, Megatron-LM, FairScale ve Hugging Face’in Accelerate kütüphanesi, büyük modellerin dağıtık eğitimini kolaylaştıran popüler ve güçlü araçlardır.
Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık