Yazan : Şadi Evren ŞEKER

Bilgisayar bilimlerinde, paralel programlama amacıyla kullanılan bir platformun ismidir. Çok çeşitli işletim sistemlerinde ve çok çeşitli dillerde paralel uygulama geliştirmeye olanak sağlar. Ancak temel çıkış sistemi Linux ve temel programlama dili olarak C dili kabul edilebilir.

İçerik
1. Paralel Programlamaya Giriş
2 MPI Kurulumu
3. MPI ile paralel program geliştirilmesi
4. MPI kodunun çalıştırılması

Buna karşılık windows, Unix gibi işletim sistemleri ve JAVA, C++, C# gibi programlama dillerinde de oldukça yoğun kullanıcıları bulunmaktadır. Ayrıca nadirde olsa bu sayılanlar dışındaki işletim sistemi ve programlama dillerinde geliştirme yapanlar bulunur.

Bu yazının amacı okuyucuya basit paralel programlama yaklaşımını kazandırmak, basit bir MPI kurulumu gerçekleştirme ve üzerinde basit bir uygulama çalıştıracak seviyeye getirmektir.

Paralel Programlamaya Giriş

MPI temel olarak birden fazla bilgisayarın üzerinde aynı programın çalıştırılması ve bu çalıştırma sırasında programların birbiri ile iletişime geçerek birbirinden veri transfer etmesine dayanır.

Temel olarak paralel programlamanın amacı bir bilgisayarda çok uzun sürede işlenen bir işi birden fazla bilgisayara bölerek aynı anda işletmek ve sonucu geri birleştirmektir (Bir anlamda algoritma analizindeki parçala fethet (divide and conquere) yaklaşımı olarak görülebilir)

Örneğin paralel programlama dünyasının en temel problemi olan matrix çarpımını ele alalım. Matrix çarpımı paralel programlamanın Merhaba Dünya (Hello World) uygulaması olarak düşünülebilir ve bu ortamda yazılan en basit program olarak görülebilir.

Matris boyutu büyüdükçe bir bilgisayardaki işlem süresi de uzar. Matrix parçalara bölünerek farklı bilgisayarlarda çarpılırsa bu durumda işlem süresi bölünerek büyür. Yani örneğin bir matrisin bir bilgisayarda çarpılması 1 saat alıyorsa 10 bilgisayarda 6 dakikaya yakın bir süre alır.

Paralel programlama ve paralal işleme (prallel processing) konusunda unutulmaması gereken bir nokta verinin ağ üzerinde bilgisayarlara dağıtılma zamanıdır. Dolayısıyla bir problemin bilgisayarlara dağıtılması her zaman süreyi bilgisayar sayısına bölmez. Hatta problem çok küçükse ve paralel yapılmaya çalışılıyorsa bu durumda ağ üzerindeki iletim süresi problemin çözüm süresini uzatabilir bile.

Ayrıca paralel programlama sırasında, problemin bilgisayarlara nasıl bölündüğü de oldukça önemlidir. Şayet problem birbirinden bağımsız parçalara bölünebiliyor ve birleştirmek veya bölmek için ayrıca işlem zaman almıyorsa bu durumda işleme süresi, topalam sürenin bilgisayara bölünmüş haline yakındır.

Ancak şayet problem bölündükten sonra diğer parçalardan bilgilere ihtiyaç duyuyorsa veya diğer bilgisayarların işlediği bilgileri bekliyorsa veya problemin bölünmesi veya birleştirilmesi çok fazla zaman alıyorsa bu durumda da zamanın tek bilgisayarla işlenen zamana yaklaştığını hatta ağ üzerine kaybedilen vakitten dolayı geçtiğini bile görebiliriz.

Yukarıda açıkladığım bu temel paralel programlama mantığının aslında MPI ile hiç ilgisi yoktur. MPI sadece yukarıda açıkladığım bu yaklaşımın programcı tarafından kodlanmasına yardımcı olur. Ancak yine de MPI kullanımını anlatmadan önce kısaca paralel programlama hakkında bilgi vermek istedim.

Şimdi MPI kurulumuna ve kullanılması konularına bakabiliriz.

MPI kurulumu

Daha önce de bahsedildiği üzere MPI kullanımı için çok farklı dil ve çok farklı ortam seçenekleri bulunmaktadır. Bu yazı kapsamında windows işletim sistemi üzerinde kullanılabilen MPICH2 paketi tanıltılacaktır.

MPICH2 paketi argonna üniversitesinde geliştirilen ve windows üzerine oldukça fazla kullanıcısı bulunan bir pakettir. Hatta bu paket esas alınarak daha kolay kullanım sağlayan farklı paketler de geliştirilmiştir.

MPICH2 paketini http://www.mcs.anl.gov/research/projects/mpich2/ adresinden indirebilirsiniz. Bilgisayar mimarinize uygun paketin indirdikten sonra aşağıdakine benzer bir kurulum ekranı belirir:

Bu ekranda krımızı daire içerisindeki Next düğmesine basılarak bir sonraki adıma geçilir:

Yukarıda MPICH2 kurulumu hakkında genel bilgiler içeren ekrandan Next düğmesi ile devam edilir:

Yukarıdaki bu adımda lisans sözleşmesini kabul edip Next düğmesine basılır:

Yukarıdaki ekranda, ileride kullanacağımız şifreyi giriyoruz. Bu şifre kendiliğinden “behappy” olarak geliyor. İstenirse bu şekilde bırakılabilir ancak bunun bir günvelik açığı olduğu unutulmamalı. Şifremizi girdikten sonra Next düğmesi ile devam ediliyor:

Yukarıdaki kurulum bilgileri ekranında, MPICH2’nin kurulmasını istediğimiz dizini belirtiyoruz. Ayrıca kullanıcı hakkı olarak sadece aktif kullanıcı veya bütün windows kullancıları için kurulumdan birisini seçiyoruz.

Dilenirse bu aşamada “Disk Cost” düğmesi ile de kurulum yapılabilecek diskler ve bu disklerde kurulumun kaplayacağı yer hakkında bilgi alınabilir.

Son olarak aşağıdaki ekrana Next Düğmesi ile geçiyoruz

Yukarıdaki bu son ekran MPICH2’nin kuruluma hazır olduğunu ve Next düğmesi ile kurulumun başlayacağını belirtiyor.

Next düğmesine basıyoruz ve kurulum başlıyor. Kurulum sırasında bir ilerleme çubuğu ile kurulum devam eder. Ayrıca bir iki kere konsolu açıp bazı komutlar çalıştırır. Bu işlemler sırasında en önemli adımlardan birisi de ateş duvarında (firewall) yetki verilmesi aşamasıdır. Şayet bir ateş duvarınız bulunuyorsa, yeni kurduğumuz bu program için yetkilendirme yapmanız gerekir. Örneğin bu ekran Windows Vista için aşağıdaki şekilde görülür ve Engellemeyi Kaldır seçeneği seçilmelidir.

Yukarıdaki ateş duvarı hakkı verildikten sonra MPICH2 kurulumu tamamlanır ve aşağıdaki şekilde kurulumun bittiğini belirten ekran görüntülenir:

Şayet kurulum başarıyla tamamlanırsa Başlat menüsünün altına MPICH2 grubu eklenir.

MPICH2’nin çalıştırılması için kurulum dizini altında bulunan bin isimli dizine girilmesi ve içerisindeki mpiexec komutunun çalıştırılması yeterlidir. Tabi mpiexec ile çalıştırmak için öncelikle bir de MPI ortamına uygun kodun yazılması gerekir.

Yukarıdaki şekilde kurulumu gerçekleştirdikten sonra MPI ile kod yazmaya başlayabiliriz.

MPI ile paralel program geliştirilmesi

Bu bölümde basit bir matrix çarpım programını yazacağız. Amacımız programın paralel olarak çalışmasıdır. Öncelike matris çarpımı yapan bu kod için gerekli kütüphaneleri kodumuzdan çağıralım:

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

Şayet mpi kurulumunuz başarılı olduysa mpi.h dosyasının çağrılmasında bir problem olmamalıdır. Ancak kodumuzu çalıştırırken ilave olarak bağlayıcıya (linker) bu kütüphaneyi tanıtmamız gerektiğini unutmayalım.

#define MASTER 0		/* taskid of first task */
#define FROM_MASTER 1		/* setting a message type */
#define FROM_WORKER 2		/* setting a message type */

Yukarıdai kodumuz kapsamında master (ana bilgisayarımız ve kodumuzu derleyip çalıştırdığımız bilgisayar olarak kabul ediyoruz) ve worker (işçi bilgisayarlar, yani programın üzerine dağıtıldığı ve sadece işlemci gücünden faydalandığımız bilgisayarlar) bilgisayarlar arasındaki mesajlaşma için kullanılan değişkenleri tanımlıyoruz. Bu değişken değerleri mesaj geçirilirken birer sinyal olarak kullanılacak ve mesajın kimden geldiğini belirtecektir.

MPI_Status status;

MPI_Status tipinden tanımlanan yukarıdaki değişken ise mesajlaşma sırasında sistemin durumu hakkındaki bilgileri tutarak gerektiğinde durum tespiti yapmamızı sağlar. Bu değişken de mesaj gönderme ve alma sırasında kullanılacaktır.

main(int argc, char **argv)
{
int numtasks,			/* number of tasks in partition */
    taskid,			/* a task identifier */
    numworkers,			/* number of worker tasks */

C dilinde geliştirdiğimiz programımızda main fonksiyonu ile koda başlıyoruz. tanımlanan numtasks değişkeni kaç işlem (process) çalıştığını tutacak.

MPI ile işimizi bilgisayarlara dağıtabildiğimiz gibi bir bilgisayar üzerinde de birden fazla iş (process) çalıştırmamız mümkündür. Dolayısyla örneğin 10 bilgisayarın bulunduğu bir ortamda her bilgisayarda 3’er iş çalıştırılırsa bu durumda toplam 30 iş çalışmış olur. Yani programımız sanki 30 farklı bilgisayarda çalışıyormuş gibi davranmak zorundadır.

Yukarıdaki taskid değişkenini hangi iş (process) olduğumuzu tutmak için kullanacağız. Daha önce de belirttiğimiz üzere birden fazla bilgisayar (yada işlemde) programımız kopyalanır. Yani aslında bütün bilgisayarlarda aynı kod çalışır. Çalışan kodun farkı veya yapılan işin farkını belirtebilmek için öncelikle kim olduğumuzu (hangi process olduğumuzu) bilmeliyiz. İşte bu taskid değişkenindeki bilgiyi yine MPI fonksiyonlarından alacağız ve programımız hangi işlem olduğuna göre farklı davranacak (matrisin hangi bölümünü çarptığımızı buradan öğrenceğiz.

numworkers değişkeni ise mevcut sistemde toplam kaç iş (process) olduğunu tutacak. Bu bilgi de MPI fonksiyonları ile elde edilen bir bilgi olacak.

MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &taskid);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
numworkers = numtasks-1;

Yukarıda ilk defa MPI fonksiyonlarını kullandık. MPI_Init fonksiyonu sistemi ilklendirir (initialize). Burada dikkat edilirse main fonksiyonunun parametreleri aynen geçirilmiştir. Bu parametreler konsoldan okunan program parametreleridir (console parameters).

Yukarıdaki ikinci fonksiyonda (MPI_Comm_rank) oluşturulan paralel dünyamızdan (ki bu dünya MPI kurulumu ile oluşturulmaktadır) taskid değişkenine rank (seviye) bilgisi okunmaktadır. Yani kısacası kaç numaralı iş (process) olduğumuz bu fonksiyondan atıf  ile çağırılarak (call by reference) öğrenilmiştir.

Benzer şekilde sistemdeki iş (process) sayısı da MPI_Comm_size fonksiyonu ile öğrenilmiştir. Bu sayı kurulumumuzdaki toplam işin miktarını vermektedir.

Hemen altında numworkers değişkeni numtasks değişkeninden 1 eksik olarak ayarlanmıştır. Bunun sebebi ana işin (process) toplam çalışan sayısından çıkarılmasıdır. Yani bütün işleri dağıtan ve sistemden bilgileri toplayan, bizim kullanıcı olarak doğrudan iletişimde olduğumuz ve hesaplama işini yapmayacak olan ana iş (master process) toplam işçi sayısıyndan çıkarılmıştır. Çünkü birazdan matrisi parçalara ayırıp işçilere (slaves) yollarken ve matrisin kaç parçaya bölüneceğine karar verirken bu sayının içerisinde ana işin bulunmaması gerekmektedir.

if (taskid == MASTER) {

Yukarıdaki if ile mevcut işin (process) ana iş olup olmadığını kontrol ediyoruz. Benzer şekilde

if (taskid > MASTER) {

Yukarıdaki satır ile de diğer işlerin (process) durumu belirtilir. Burada ilk değişken tanımlarımızı hatırlayacak olursak MASTER değişkeninin değeri 1 olarak tanımlanmıştı. Yani şayet bizim taskid değerimiz 1 ‘ise ana işlem (master process) diğer bütün durumlarda da çalışan (slave) olmuş oluyoruz.

Kodun tamamı aşağıda verilmiştir:

#include <stdio.h>
#include "mpi.h"
#define NRA 62 			/* number of rows in matrix A */
#define NCA 15			/* number of columns in matrix A */
#define NCB 7   		/* number of columns in matrix B */
#define MASTER 0		/* taskid of first task */
#define FROM_MASTER 1		/* setting a message type */
#define FROM_WORKER 2		/* setting a message type */

MPI_Status status;
main(int argc, char **argv)
{
int numtasks,			/* number of tasks in partition */
    taskid,			/* a task identifier */
    numworkers,			/* number of worker tasks */
    source,			/* task id of message source */
    dest,			/* task id of message destination */
    nbytes,			/* number of bytes in message */
    mtype,			/* message type */
    intsize,			/* size of an integer in bytes */
    dbsize,			/* size of a double float in bytes */
    rows,                      	/* rows of matrix A sent to each worker */
    averow, extra, offset,      /* used to determine rows sent to each worker */
    i, j, k,			/* misc */
    count;
double a[NRA][NCA], 		/* matrix A to be multiplied */
       b[NCA][NCB],      	/* matrix B to be multiplied */
       c[NRA][NCB];		/* result matrix C */

intsize = sizeof(int);
dbsize = sizeof(double);

MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &taskid);
MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
numworkers = numtasks-1;

/**************************** master task ************************************/
if (taskid == MASTER) {
  printf("Number of worker tasks = %dn",numworkers);
  for (i=0; i<NRA; i++)
    for (j=0; j<NCA; j++)
      a[i][j]= i+j;
  for (i=0; i<NCA; i++)
    for (j=0; j<NCB; j++)
      b[i][j]= i*j;

  /* send matrix data to the worker tasks */
  averow = NRA/numworkers;
  extra = NRA%numworkers;
  offset = 0;
  mtype = FROM_MASTER;
  for (dest=1; dest<=numworkers; dest++) {
    rows = (dest <= extra) ? averow+1 : averow;
    printf("   sending %d rows to task %dn",rows,dest);
    MPI_Send(&offset, 1, MPI_INT, dest, mtype, MPI_COMM_WORLD);
    MPI_Send(&rows, 1, MPI_INT, dest, mtype, MPI_COMM_WORLD);
    count = rows*NCA;
    MPI_Send(&a[offset][0], count, MPI_DOUBLE, dest, mtype, MPI_COMM_WORLD);
    count = NCA*NCB;
    MPI_Send(&b, count, MPI_DOUBLE, dest, mtype, MPI_COMM_WORLD);

    offset = offset + rows;
    }

  /* wait for results from all worker tasks */
  mtype = FROM_WORKER;
  for (i=1; i<=numworkers; i++)	{
    source = i;
    MPI_Recv(&offset, 1, MPI_INT, source, mtype, MPI_COMM_WORLD, &status);
    MPI_Recv(&rows, 1, MPI_INT, source, mtype, MPI_COMM_WORLD, &status);
    count = rows*NCB;
    MPI_Recv(&c[offset][0], count, MPI_DOUBLE, source, mtype, MPI_COMM_WORLD,
               &status);

    }

  /* print results */
  printf("Here is the result matrixn");
  for (i=0; i<NRA; i++) {
    printf("n");
    for (j=0; j<NCB; j++)
      printf("%6.2f   ", c[i][j]);
    }
  printf ("n");

  }  /* end of master section */

/**************************** worker task ************************************/
if (taskid > MASTER) {
  mtype = FROM_MASTER;
  source = MASTER;
  printf ("Master =%d, mtype=%dn", source, mtype);
  MPI_Recv(&offset, 1, MPI_INT, source, mtype, MPI_COMM_WORLD, &status);
  printf ("offset =%dn", offset);
  MPI_Recv(&rows, 1, MPI_INT, source, mtype, MPI_COMM_WORLD, &status);
  printf ("row =%dn", rows);
  count = rows*NCA;
  MPI_Recv(&a, count, MPI_DOUBLE, source, mtype, MPI_COMM_WORLD, &status);
  printf ("a[0][0] =%en", a[0][0]);
  count = NCA*NCB;
  MPI_Recv(&b, count, MPI_DOUBLE, source, mtype, MPI_COMM_WORLD, &status);
  printf ("b=n");
  for (k=0; k<NCB; k++)
    for (i=0; i<rows; i++) {
      c[i][k] = 0.0;
      for (j=0; j<NCA; j++)
        c[i][k] = c[i][k] + a[i][j] * b[j][k];
      }

  mtype = FROM_WORKER;
  printf ("after computern");
  MPI_Send(&offset, 1, MPI_INT, MASTER, mtype, MPI_COMM_WORLD);
  MPI_Send(&rows, 1, MPI_INT, MASTER, mtype, MPI_COMM_WORLD);
  MPI_Send(&c, rows*NCB, MPI_DOUBLE, MASTER, mtype, MPI_COMM_WORLD);
  printf ("after sendn");

  }  /* end of worker */
  MPI_Finalize();
} /* of main */

Yukarıdaki kod Dev-CPP ortamında mingw derleyicisi (compiler) ile test edilmiştir. Ayrıca kodun çalışması için aşağıdaki bölümü okuyabilirsiniz.

MPI kodunun çalıştırılması

Kodun çalıştırılması için yukarıda anlatıldığı gibi bir MPICH2 kurulumunun yapıldığını kabul ediyoruz. Ayrıca kodumuzu derlemek için Dev-CPP kullanacağız. Ancak kodlama hakkında tecrübesi olan bir okuyucu dilediği başka bir derleyici (compiler) kullanarak devam edebilir.

Yorumlar

  1. Ahmet ESER

    Çok güzel bir yazı olmuş bu konularda türkçe kaynak bulmak gerçekten sıkıntı oluyor. Teşekkür ederim devamını dilerim…

  2. complexity Article Author

    sadi kardeş merhaba
    ben yeni başladım MPI ‘ya. senin kodunu inceledim ve benzer birşey yazmaya çalıştım.
    amacım (master hariç, mesela 5 işlemci varsa) 50 karakterlik bir stringi bu
    işlemcilere bölüp herbirine 10 karakter dağıtmak ve herbirinin bu alt stringlerde
    “ali” sözcüğünü arattırmak..
    sonra tabiki bulan yine mastere haber veriyor..kodu aşağıdaki gibi:

    hatayı bulmamda yardımcı olur musunuz ?
    ______________________
    __________________________
    ______________________________

    #include
    #include “mpi.h”
    #define MASTER 0 /* taskid of first task */
    #define FROM_MASTER 1 /* setting a message type */
    #define FROM_WORKER 2 /* setting a message type */

    MPI_Status status;
    int main(int argc, char **argv)
    {
    int numtasks, /* number of tasks in partition */
    taskid, /* a task identifier */
    numworkers, /* number of worker tasks */
    source, /* task id of message source */
    dest, /* task id of message destination */
    mtype, /* message type */
    sizes, /* sizes of the string sent to each worker */
    avesize, extra, offset, /* used to determine sizes sent to each worker */
    i, j, k, /* misc */
    index,
    found; /* checks if “ali” is found by a worker processor or not */
    FILE *fp;
    char str[1284];

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &taskid);
    MPI_Comm_size(MPI_COMM_WORLD, &numtasks);
    numworkers = numtasks-1;

    /**************************** master task ************************************/

    if (taskid == MASTER) {
    printf(“Number of worker tasks = %dn”,numworkers);

    //////dosyayı acıp içindekileri str e geçiriyor
    if((fp = fopen(argv[ 1 ], “r”))==NULL) {
    printf(“Cannot open file.n”);
    exit(1);
    }

    while(!feof(fp)) {
    if(fgets(str, 1282, fp))
    printf(“The string is %s”, str);
    }
    fclose(fp);

    /* send string data to the worker tasks */
    avesize = strlen(str)/numworkers;
    extra = strlen(str)%numworkers;
    offset = 0;
    mtype = FROM_MASTER;
    for (dest=1; dest<=numworkers; dest++) { sizes = (dest <= extra) ? averow+1 : averow; printf("Sending %d characters of string to task %dn",sizes,dest); MPI_Send(&offset, 1, MPI_INT, dest, mtype, MPI_COMM_WORLD); MPI_Send(&sizes, 1, MPI_INT, dest, mtype, MPI_COMM_WORLD); MPI_Send(&str[offset], sizes, MPI_DOUBLE, dest, mtype, MPI_COMM_WORLD); offset = offset + sizes; } /* wait for results from all worker tasks */ mtype = FROM_WORKER; for (i=1; i MASTER) { mtype = FROM_MASTER; source = MASTER; printf ("Master =%d, mtype=%dn", source, mtype); MPI_Recv(&offset, 1, MPI_INT, source, mtype, MPI_COMM_WORLD, &status); MPI_Recv(&sizes, 1, MPI_INT, source, mtype, MPI_COMM_WORLD, &status); MPI_Recv(&a[offset], sizes, MPI_INT, source, mtype, MPI_COMM_WORLD, &status);/////////////////////////////// found=0; ///searching for ali in the substring in workers for (k=0; k

Ahmet ESER için bir cevap yazın Cevabı iptal et

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir