/ Blog

Paralel Hesaplama Temelleri: MPI, OpenMP ve GPU Parallelizm

Paralel programlama paradigmaları: MPI (dağıtık bellek), OpenMP (paylaşımlı bellek) ve CUDA/OpenCL GPU hesaplama.

Modern bilimsel hesaplama ve mühendislik uygulamalarında sıklıkla karşılaşılan problemler, tek bir işlemci çekirdeğinin kapasitesini çoktan aşmış durumdadır. İklim modelleme, moleküler dinamik, hesaplamalı akışkanlar dinamiği (CFD) veya derin öğrenme eğitimi gibi alanlarda gerçekçi sonuçlar elde edebilmek için paralel hesaplama kaçınılmaz bir gereklilik haline gelmiştir. Bu yazıda, paralel programlamanın üç temel paradigmasını — MPI, OpenMP ve GPU parallelizmi — teknik bir perspektiften ele alacağız.

Paralel Hesaplamanın Temelleri

Bir problemi paralel olarak çözmek, onu daha küçük parçalara bölmek ve bu parçaları eş zamanlı işlemek demektir. Ancak bu yaklaşım, iki kritik kavramı beraberinde getirir: veri bağımlılıkları ve iletişim maliyeti.

Amdahl Yasası, bir programın paralel hale getirilebilecek kısmının ne kadar iyileştirilirse iyileştirilsin, seri kalan kısım tarafından kısıtlandığını ifade eder. Matematiksel olarak:

Hızlanma = 1 / ( (1 - P) + P/N )

Burada P paralel hale getirilebilen kod oranı, N ise işlemci sayısıdır. Örneğin kodun yalnızca %80’i paralel hale getirilebiliyorsa, sonsuz işlemci kullanılsa dahi teorik maksimum hızlanma yalnızca 5 kattır. Bu nedenle paralel programlamaya başlamadan önce profil analizi ve darboğaz tespiti kritik bir adımdır.

MPI: Dağıtık Bellek Parallelizmi

MPI (Message Passing Interface), birden fazla düğümün (node) yer aldığı HPC kümelerinde en yaygın kullanılan paralel programlama standardıdır. Her işlem (process) kendi bellek alanına sahiptir; işlemler arasındaki veri paylaşımı açık mesaj gönderme/alma çağrıları aracılığıyla gerçekleşir.

MPI’nin Temel Özellikleri

  • Dağıtık bellek modeli: Her süreç bağımsız bir adres alanında çalışır.
  • Ağ üzerinden iletişim: InfiniBand veya Ethernet aracılığıyla düğümler arası haberleşme.
  • Ölçeklenebilirlik: Binlerce hatta yüz binlerce çekirdekte çalışabilir.
  • Taşınabilirlik: C, C++, Fortran dillerinde yaygın desteğe sahiptir.

Basit Bir MPI Örneği

Aşağıdaki C kodu, her MPI sürecinin kendi sıra numarasını ve toplam süreç sayısını yazdırdığı minimal bir örneği göstermektedir:

#include <mpi.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    int rank, size;

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    printf("Merhaba, ben süreç %d / %d\n", rank, size);

    /* Süreç 0 tüm diğer süreçlerden veri toplar */
    double local_sum = (double)rank;
    double global_sum = 0.0;

    MPI_Reduce(&local_sum, &global_sum, 1, MPI_DOUBLE,
               MPI_SUM, 0, MPI_COMM_WORLD);

    if (rank == 0) {
        printf("Toplam: %.1f\n", global_sum);
    }

    MPI_Finalize();
    return 0;
}

MPI_Reduce gibi kolektif iletişim işlemleri, veriyi tüm süreçlerden toplayarak bir kökte birleştirir. Bu tür işlemler, elle yazılan noktadan noktaya (point-to-point) iletişime kıyasla hem daha performanslı hem de daha okunabilirdir.

MPI’de Dikkat Edilmesi Gereken Noktalar

Ölçek büyüdükçe iletişim gecikmeleri (latency) ve bant genişliği kısıtlamaları kritik hale gelir. Mesaj boyutları küçük tutulmalı, mümkün olduğunda asenkron iletişim (MPI_Isend, MPI_Irecv) kullanılmalıdır. Ayrıca yük dengesizliği (load imbalance), tüm sistemin en yavaş sürece bağlı kalmasına neden olacağından veri bölümleme stratejisi dikkatle tasarlanmalıdır.

OpenMP: Paylaşımlı Bellek Parallelizmi

OpenMP, tek bir düğüm içindeki çok çekirdekli işlemcileri kullanmak için tasarlanmış bir uygulama programlama arayüzüdür. Pragma direktifleri ile mevcut C/C++ veya Fortran koduna kolayca entegre edilebilmesi, onu yeni başlayanlar için de erişilebilir kılar.

OpenMP ile Döngü Paralelleştirme

#include <omp.h>
#include <stdio.h>

int main() {
    int n = 1000000;
    double sum = 0.0;

    /* Döngü iş parçacıkları arasında otomatik bölünür */
    #pragma omp parallel for reduction(+:sum) schedule(static)
    for (int i = 0; i < n; i++) {
        sum += i * 0.001;
    }

    printf("Toplam: %.2f\n", sum);
    printf("Kullanilan is parcacigi sayisi: %d\n",
           omp_get_max_threads());
    return 0;
}

reduction(+:sum) direktifi, her iş parçacığının kendi yerel toplamını hesaplamasını ve döngü sonunda atomik olarak birleştirilmesini sağlar. schedule(static) ise döngü iterasyonlarını eşit parçalara bölerek iş parçacıklarına dağıtır.

MPI ve OpenMP’yi Birlikte Kullanmak: Hibrit Programlama

Büyük ölçekli HPC uygulamalarında iki paradigma birlikte kullanılır. Bu hibrit MPI+OpenMP yaklaşımında:

  • Her düğüme bir MPI süreci atanır.
  • Her süreç içinde OpenMP iş parçacıkları, düğümdeki tüm çekirdekleri kullanır.
  • MPI iletişim trafiği azalır, bellek kullanımı optimize edilir.

Bu yaklaşım özellikle NUMA (Non-Uniform Memory Access) mimarisine sahip çok soketli sistemlerde belirgin performans artışı sağlar.

GPU Parallelizmi: CUDA ve OpenCL

Grafik işleme birimleri (GPU), binlerce küçük çekirdeğe sahip massively parallel mimarileriyle, veri paralel (data-parallel) problemler için CPU’lardan çok daha yüksek teorik teraflop performansı sunar. Özellikle matris çarpımı, görüntü işleme ve sinir ağı eğitimi gibi yüksek aritmetik yoğunluklu işlemlerde belirgin hızlanmalar elde edilir.

CUDA: NVIDIA GPU Programlama

CUDA (Compute Unified Device Architecture), NVIDIA GPU’larına yönelik en yaygın kullanılan GPU programlama modelidir. Programcı, GPU üzerinde çalışacak kernel fonksiyonlarını tanımlar:

// GPU kerneli: vektör toplama
__global__ void vector_add(float *a, float *b, float *c, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

// Ana program
int main() {
    int n = 1 << 20;  // 1M eleman
    size_t bytes = n * sizeof(float);

    float *d_a, *d_b, *d_c;
    cudaMalloc(&d_a, bytes);
    cudaMalloc(&d_b, bytes);
    cudaMalloc(&d_c, bytes);

    // Veriyi host'tan device'a kopyala
    // cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice);

    int blockSize = 256;
    int gridSize = (n + blockSize - 1) / blockSize;

    vector_add<<<gridSize, blockSize>>>(d_a, d_b, d_c, n);
    cudaDeviceSynchronize();

    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
    return 0;
}

GPU programlamanın en kritik performans faktörü bellek bant genişliğidir. CPU-GPU arası veri transferi (PCIe üzerinden) ciddi bir gecikme kaynağıdır; bu nedenle veriyi mümkün olduğunca uzun süre GPU belleğinde tutmak, hesaplama/transfer oranını optimize etmek büyük önem taşır.

OpenCL: Taşınabilir Heterojen Hesaplama

OpenCL, NVIDIA, AMD ve Intel GPU’ları ile FPGA’lar dahil geniş bir donanım yelpazesini destekleyen açık standarttır. CUDA’ya kıyasla daha verbose (ayrıntılı) bir API’ye sahip olsa da vendor bağımlılığı olmaksızın taşınabilirlik sağlar.

Paradigmaları Karşılaştırma

Hangi yaklaşımın kullanılacağı büyük ölçüde problemin yapısına, donanım altyapısına ve mevcut kod tabanına bağlıdır:

KriterMPIOpenMPGPU (CUDA/OpenCL)
Bellek modeliDağıtıkPaylaşımlıAyrı (host + device)
ÖlçekBinlerce düğümTek düğüm, çok çekirdekTek/çok GPU
Programlama zorluğuOrta-yüksekDüşük-ortaOrta-yüksek
En iyi kullanımBüyük ölçekli küme hesaplamaDöngü paralelleştirmeVeri paralel, yüksek aritmetik
Tipik uygulamaCFD, FEM, iklim modeliBilimsel hesaplama, simülasyonDerin öğrenme, moleküler dinamik

Pratik Tavsiyeler

Profil analizi önce gelir. Kodunuzun nerede zaman harcadığını belirlemeden paralelleştirmeye çalışmak, yanlış yerde optimizasyon yapmaya neden olur. gprof, Intel VTune veya NVIDIA Nsight araçları bu süreçte değerlidir.

Küçük adımlarla ilerleyin. Tüm kodu aynı anda paralel hale getirmeye çalışmak hataları tespit etmeyi güçleştirir. Tek bir kritik döngüyü veya fonksiyonu paralel hale getirip doğruladıktan sonra ilerlemek daha güvenlidir.

Iletişim/hesaplama dengesine dikkat edin. Özellikle MPI’da iletişim için harcanan sürenin hesaplamaya oranı performansı doğrudan etkiler. Gerektiğinde mesaj birleştirme (message aggregation) ve asenkron iletişim kullanın.

Bellek erişim kalıplarını optimize edin. Özellikle GPU’larda birleşik (coalesced) bellek erişimi, önbellek (cache) dostu veri yapıları ve paylaşımlı bellek (shared memory) kullanımı belirleyici rol oynar.

Sonuç

MPI, OpenMP ve GPU parallelizmi birbirinin alternatifi değil, tamamlayıcı araçlardır. Modern HPC sistemleri çoğunlukla bu üç paradigmayı hibrit biçimde kullanır: düğümler arası iletişim için MPI, düğüm içi paralellik için OpenMP ve yoğun veri paralel işlemler için GPU. Doğru paradigmayı seçmek ve etkin bir şekilde uygulamak ise ciddi bir mühendislik uzmanlığı gerektirmektedir.

Mevasis olarak paralel hesaplama altyapısı kurulumu, uygulama optimizasyonu ve HPC kümesi tasarımı konularında size destek olmaktan memnuniyet duyarız. İletişim için formu doldurun.