AtasoyWeb
 
AtasoyWeb - Hüseyin Atasoy
Bir bilgisayar mühendisi için programlama dili, öğrendiklerini sınadığı, deneyler yaptığı bir laboratuardır ve mühendisler deneylerini, kestiremedikleri sonuçları gözlemlemek için değil, öngördükleri sonuçları doğrulamak için yapar...

MATLAB'in Sihirli Değneği: BLAS

MATLAB bazı doğrusal cebir işlemlerini nasıl oluyor da bu kadar hızlı yapabiliyor?

İlgili yazılar: CeNiN - Konvolüsyonel Yapay Sinir Ağı Kütüphanesi

Yakın zamanda .NET için bir konvolüsyonel yapay sinir ağı kütüphanesi yazmış ve kütüphaneyi açık kaynak kodlu olarak yayınlamıştım; CeNiN (GitHub). Ara ara, sıkıldıkça hala üstünde çalışıyorum. CeNiN'in ve MATLAB için yazılmış bir konvolüsyonel yapay sinir ağı aracı olan matconvnet'in, aynı ağ yapısı ile aynı görüntüyü sınıflandırma hızlarını karşılaştırdım. Fark fazla büyüktü. CeNiN'i yazmaya başlarken performans kaygım yoktu aslında ama bu fark kafama takıldı. MATLAB'teki araç niye benim yazdığım kodlardan çok daha hızlı çalışıyor?!

Ağın ileri beslenmesi aşamasında en çok zaman alan işlem konvolüsyon. Konvolüsyon düz mantıkla uygulandığında, zaman açısından maliyetli bir işlem ve her bir konvolüsyon katmanında çok sayıda filtre var. Örneğin bir görüntü, githubta paylaştığım ağların küçük olanının konvolüsyon katmanlarından geçerken yalnızca konvolüsyonlar için yapılan çarpma işlemlerinin toplam sayısı 724005632. Üstelik bu çarpma işlemleri kayan noktalı sayılar üzerinde yapılıyor.

Birkaç Optimizasyondan Sonra CeNiN

Projenin anasayfasında belirttiğim gibi, konvolüsyon katmanında çok sayıda filtrenin konvolüsyonlarını tek bir matris çarpımına indirgeyen bir yöntem de uyguladım. Dolayısıyla artık en çok zaman alan ve en çok optimize edilmesi gereken işlem matris çarpımı. CeNiN o haliyle de özellikle bellekteki sayılara erişmek için kullandığım indeksleme mantığı ve matris çarpımlarının işlemcinin harika bazı özelliklerinden mahrum olması nedeniyle hala MATLAB'le yarışacak durumda değildi.

Zaman maliyeti yüksek bir diğer kısım indeksleme. Tensörleri çok boyutlu halleriyle bellekte tutmanın yolu olmadığı için onları ardışıl düz bir bellek alanında tutuyorum. Dolayısıyla tensörlerden eleman çekerken tensörlerin hangi boyutundan hangi sıraya erişileceği bilgisini alıp bu sıranın bellekte nereye denk geleceğini hesaplamamız gerekiyor. Bu indeks çevirisi döngülerin her bir adımında tekrar tekrar yapılmak zorunda. Oysa ki içteki döngülerin tüm adımları bitene kadar dıştaki döngülerin çevirmekte olduğu boyutlardaki sıra hiç değişmiyor. Bu da en iç döngüde yapılan indeksleme esnasında, tekrarlı çarpma işlemleri yapılmasına sebep oluyor. Yani indekslemeyi prosedürel hale getirmek uğruna performanstan ödün vermiş oluyoruz. Ama artık asıl odağımız performans!

Yaptığım birkaç iyileştirmeyle sağlayabildiğim hız artışları şöyle (süreleri bir görüntünün, ağın tamamından geçmesi için harcanan süre cinsinden veriyorum ve işlemci: Intel Core i7-6500U 2.50GHz, RAM: 8GB):

  • Conv_1.cs: 7548 ms - Ağı test etmek için yazdığım ilk implementasyon, düz mantık
  • Conv_2.cs: 6763 ms - Her katmanda tüm konvolüsyonları tek bir matris çarpımına indirgiyoruz. Matris çarpma metodumuz yine klasik. Strassen metodu gibi bazı yöntemler var ama zaten indeksleme hızıyla ilgili problem yaşıyorken matrisleri alt matrislere bölmekle hiç uğraşmak istemedim.
  • Conv_3.cs: 1975 ms - Konvolüsyonu yine tek bir matris çarpımına indirgiyoruz ama bu sefer indekslemede tekrarlı çarpımlar yok. İndekslemeyi döngülerin en içtekinde bir indeksleyici fonksiyona yaptırmak yerine parçalara bölüyoruz. Her dış döngü, tek boyutlu indeksi kendi döndürdüğü boyuttaki artışın tek boyutta sebep olacağı fark kadar arttırıyor.
  • Conv.cs: 1031 ms - Yukarıdaki iyileştirmelere ek olarak filtreleri ayrı işlemci çekirdekleri üstünde aynı anda işlemcinin mantıksal çekirdek sayısı kadar filtreleme yürütülecek şekilde paralelleştiriyoruz. (Bu sürenin hesaplandığı bilgisayarın işlemcisinin 2 fiziksel çekirdeği var. Daha çok çekirdek, daha yüksek hız...)

Yeterli mi? Değil. Hala Matlab'in çok gerisindeyiz. Peki ama neden?

BLAS (Basic Linear Algebra Subprograms)

BLAS (temel doğrusal cebir altprogramları), ilk implementasyonu 1979'da Fortran ile yapılmış ve standartlaşıp farklı dillerle yazılagelmiş düşük seviyeli fonksiyonlara verilen ismin kısaltması. BLAS kütüphaneleri donanımlara bağlı derin optimizasyonlarla çalışan çok sayıda temel cebir fonksiyonu (vektörel işlemler, matris-vektör işlemleri, matris-matris işlemleri) içeriyor. Kütüphanelerdeki fonksiyonlar işlemcilerin SIMD (single instruction multiple data) komutlarını ve işlemci önbelleklerini optimum şekilde kullanarak normalden çok daha hızlı çalışabiliyorlar.

Popüler işlemci üreticilerinin kendi işlemci mimarilerine uygun BLAS implementasyonlarını içeren kendi kütüphaneleri var. Örneğin AMD'nin ACML (AMD Core Math Library)'si, Intel'in MKL (Math Kernel Library)'si var...

Intel, kaynak kodlarını kapalı tutuyor olsa da MKL'yi ücretsiz olarak dağıtıyor. Kodları göremiyoruz ama bu kütüphanelerdeki fonksiyonların çok hızlı çalışmasının temel sebebi fonksiyonların oldukça düşük seviyeli çekirdek işlemleri gerçekleştiriyor olmaları. MKL, örneğin ardışık çarpma işlemleri gerektiren matris çarpımı gibi bir işlemde, sayıları, işlemci önbelleğine tek tek değil bloklar halinde kopyalıyor. Böylece her çarpma işlemi için RAM'e uğrayıp iki sayı alıp önbelleğine atması gerekmiyor. Çok kısa süren ve çok sayıda tekrar eden bir işlemi 1 mikrosaniye bile kısaltsanız toplamda büyük bir süre kazancınız olur. Performans artışının bir diğer sebebi vektörizasyon ve SIMD komutları. İşlemcilerin komut setlerine eklenmiş bu komutlar sayesinde, aynı anda birden fazla sayı ikilisi üzerinde işlem yapılabiliyor. Ve tabi bir de çok çekirdek üzerinde paralel işlem yürütmenin de avantajı kullanılıyor...

MATLAB'in kendi fonksiyonlarına çok iyi optimizasyon uyguladığı bir gerçek ama pekçok fonksiyonunda, arkplanda, üstünde çalışılan işlemciye uygun bir BLAS (ve daha üst seviyeli işlevler için LAPACK) kütüphanesi kullanıyor. Yani aslında bazı işlemlerde MATLAB'in performansındaki büyünün kaynağı BLAS...

Bunu öğreninir öğrenmez CeNiN'de matris çarpımına BLAS desteği sağlayacak bir sınıf daha yazdım ve sonuç harika:

Conv.cs (BLAS desteği ile): 151 ms - İlk implementasyonda 7548 ms ile başlamıştık. Sonrasında 6763 ms, 1975 ms, 1031 ms ve BLAS desteği ile 151 ms. Tabi ki CeNiN'de optimize edilebilecek daha çok kısım var ama başta dediğim gibi, derdim aslında MATLAB'in nasıl bu kadar hızlı işlem yaptığını anlamaktı. Şimdilik bu kadarı benim için yeterli...

MKL'nin BLAS Fonksiyonlarını .NET'te Kullanma

Hem C# ile MKL'den fonksiyon çağırmayı örneklemiş olmak hem de işlem süresini MATLAB ile kıyaslamak için her iki dilde aynı boyutlarda iki matrisi çarpan kod parçaları yazalım. Bakalım MATLAB bu sefer de fark atabilecek mi?

C# ile başlayalım. Öncelikle kütüphanedeki fonksiyonu çağırırken kullanacağımız iki sabit değer var onları tanımlayalım:

public static int ColMajor = 102; 
public static int NoTrans = 111; 

Kütüphaneden çağıracağımız cblas_sgemm fonksiyonunu tanımlayalım:

[DllImport("mkl_rt.dll", CallingConvention = CallingConvention.Cdecl)]
        internal static extern void cblas_sgemm(
            int order, 
            int transA, 
            int transB,
            int m, 
            int n,
            int k,
            float alpha,
            float* A,
            int lda, 
            float* B, int ldb,
            float beta, 
            float* C,
            int ldc
        );

5000x400 ve 400x300 boyutlarındaki iki matrisi çarpalım:

int order = ColMajor;
int transA = NoTrans;
int transB = NoTrans;

int m = 5000, n = 300, k = 400;
int lda = k, ldb = n, ldc = n;
float alpha = 1, beta = 0;

float* A = (float*)Marshal.AllocHGlobal(sizeof(float) * m * k);
float* B = (float*)Marshal.AllocHGlobal(sizeof(float) * k * n);
float* C = (float*)Marshal.AllocHGlobal(sizeof(float) * m * n);

Stopwatch s = new Stopwatch(); s.Start();
cblas_sgemm(order, transA, transB, m, n, k, alpha, A, lda, B, ldb, beta, C, ldc);
// Yapılan işlem: C = apha * A * B + beta * C
s.Stop();
Console.PrintLine(s.ElapsedMilliseconds.ToString());

Ya da CeNiN için yazdığım Tensor sınıfıyla BLAS desteği açıkken aynı işlem:

CeNiN.Tensor.useMKLCBLAS_GEMM = true;
CeNiN.Tensor A = new CeNiN.Tensor(new int[] { m, k });
CeNiN.Tensor B = new CeNiN.Tensor(new int[] { k, n });
CeNiN.Tensor C = A * B;

MKL'nin BLAS desteği ile C#'ta bu matrislerin çarpımı yaklaşık 15-16 milisaniye sürüyor.

Şimdi MATLAB tarafına bakalım. Önce, kullanılan BLAS kütüphanesini ve versiyonunu görelim:

>> version -blas

ans =

    'Intel(R) Math Kernel Library Version 11.3.1 Product Build 20151021 for Intel(R) 64 architecture applications, CNR branch AVX2'

MATLAB'te C#'takilerle aynı boyutlara sahip iki matrisi çarpalım:

A=1000*single(rand(5000,400));
B=single(rand(400,300));
tic();
C=A*B;
toc();

Geçen süre 16 ms. Artık eşitiz...

Harici Bağlantılar

Sayfayı
Posted: 26 Mayıs 2019 Pazar, 12:16
Keywords: matlab, blas, matris çarpımı, gemm, intel mkl, lineer cebir, simd, doğrusal cebir

Leave Comment

 
You are replying to comment #-1. Click here if you want to cancel replying.

 

Comments

No approved comment.
 
 
Sayfa 37 sorgu ile 0.005 saniyede oluşturuldu.
Atasoy Blog v4 © 2008-2019 Hüseyin Atasoy