İ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.
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):
Yeterli mi? Değil. Hala Matlab'in çok gerisindeyiz. Peki ama neden?
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, arkaplanda, ü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 öğrenir öğ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...
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.useCBLAS_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...