MQL5'te CPU'dan GPU'ya: Araştırma, Optimizasyon ve Formasyon Analizinin Hızlandırılması için Pratik Bir OpenCL Çerçevesi
Giriş
MQL5'te CPU'dan GPU'ya geçiş genellikle bariz bir adım gibi görünür: grafik işlemcisi daha hızlı hesaplama yapabiliyorsa, alım-satım araştırması otomatik olarak hızlanmalıdır. Gerçekte ise durum çok daha karmaşıktır. GPU gerçekten de önemli kazanımlar sağlayabilir, ancak yalnızca görev paralel bir hesaplama modeline iyi uyduğunda. Aksi takdirde, herhangi bir hızlanma elde edemezsiniz, yalnızca aynı veya daha yüksek maliyetlerle daha karmaşık bir mimari elde edebilirsiniz.
Bu özellikle algoritmik alım-satım için önemlidir. Piyasa verilerinin analizi, parametreler üzerinde yineleme, büyük ölçekli hipotez testi ve tekrar eden formasyonların araştırılması genellikle büyük miktarlarda hesaplama gerektirir. GPU burada potansiyelini ortaya koyar. Aynı işlemin birden fazla eleman üzerinde gerçekleştirilmesi gereken durumlarda güçlüdür ve paralel işlem tamamlandıktan sonra sonuç toplanabilir. Bu tür senaryolarda, grafik kartı dekoratif bir eklenti olmaktan çıkar ve tam teşekküllü bir hesaplama kaynağı haline gelir.
Ancak GPU'nun da bir bedeli vardır. Hesaplamalara başlamadan önce verileri hazırlamalı, cihaza iletmeli, kernel'ın çalışmasını beklemeli ve sonucu geri döndürmeliyiz. Kompakt görevler için bu mantık çok ağır olabilir. CPU'nun hızlı ve gereksiz ek maliyetler olmadan çalıştığı alanlarda, hesaplamaları GPU'ya aktarmak hiçbir fayda sağlamaz. Hatta bazen, özellikle de görev sık sık değişiyorsa, esnek mantık gerektiriyorsa veya az miktarda veriyle ilgiliyse, engel bile olabilir.
MQL5 ortamında OpenCL, uygulama mantığı ile GPU arasında bir bağlantı görevi görür. Bu, hesaplamaların iş gücü gerektiren kısmını ana programın dışına taşımamıza ve GPU üzerinde toplu veri işlemeyi organize etmemize olanak tanır. Ancak OpenCL kendi başına sihirli bir hızlandırma düğmesi değildir. Yalnızca görev mimarisi en başından itibaren paralel hesaplamanın özelliklerini dikkate aldığında ve CPU ile GPU arasındaki veri alışverişini en aza indirdiğinde kullanışlıdır.
Burada GPU, ağır ve tekrarlayan işlemler için tasarlanmış ayrı bir hesaplama devresi seviyesi olarak kabul edilir. Bu yaklaşım, hesaplama hacminin araştırmacının bekleme toleransından daha hızlı büyüdüğü araştırma, optimizasyon ve formasyon bulma görevlerinde kullanışlıdır. Buradan çıkarılacak pratik sonuç basittir: öncelikle GPU'ya aktarılmaya tam olarak neyin değer olduğunu anlamamız gerekir ve ancak o zaman hızlandırmanın gerçek bir etkisi olmasını bekleyebiliriz.

Ortamın hazırlanması
OpenCL ile çalışmak hesaplamalarla değil, hazırlıklarla başlar. İlk olarak, programın kullanılabilir bir cihaz bulması, bir çalışma bağlamı oluşturması, kernel'ı hazırlaması ve veriler için bellek ayırması gerekir. Tüm bunlar teknik bir formalite gibi görünse de, bu aşamada genellikle önemli miktarda üretkenlik kaybedilir.
Asıl hata GPU'ya sıradan bir fonksiyon çağrısıymış gibi davranmaktır: çağırma, sonucu alma ve devam etme. Aslında böyle bir çağrının arkasında bir dizi eylem zinciri vardır. Bir bağlam oluşturmamız veya bağlamamız, bir program hazırlamamız, kernel'ı derlememiz, bellek ayırmamız, verileri aktarmamız ve ancak daha sonra hesaplamaya başlamamız gerekir. Bunu tekrar tekrar yaparsak, GPU hesaplamalar yerine hazırlıklar için çok fazla zaman harcayacaktır.
Bu nedenle, iyi bir uygulamada, yapılabilecek neredeyse her şey bir kez yapılır. Bağlam önceden oluşturulur ve daha sonra yeniden kullanılır. Kod değişmezse kernel bir kez derlenir. Ayrıca, gerekmedikçe bellek arabelleklerini yeniden oluşturmamak, bunları yeniden kullanmak daha iyidir. Bu yaklaşım ek yükü azaltır ve GPU ile çalışmayı gerçekten kullanışlı hale getirir.
Aynı durum veri aktarımı için de geçerlidir. GPU, sürekli olarak ileri geri gönderilen çok sayıda küçük görev için uygun değildir. Bu yaklaşımda zaman hesaplamalara değil, veri alışverişine harcanır. Verileri daha büyük gruplar halinde toplamak ve hesaplamaları daha az sıklıkla, ancak daha yüksek bir yük ile çalıştırmak çok daha verimlidir. Büyük bir yürütme neredeyse her zaman bir dizi küçük yürütmeden daha iyidir.
Bir başka yaygın sorun daha vardır - gereksiz senkronizasyon. Program her adımdan sonra durur ve GPU'nun tamamlanmasını beklerse cihaz boşta kalır. Bu da genel hızlanma etkisini azaltır. İşi, GPU'nun bir görevi alacağı, gereksiz duraklamalar olmadan tamamlayacağı ve sonucu yalnızca gerçekten ihtiyaç duyulduğunda geri döndüreceği şekilde yapılandırmak daha iyidir.
Bu özellikle MQL5 için önemlidir. Alım-satım programları gecikmelere karşı hassastır ve mimari özensizlik hızla fark edilir hale gelir. Eğer bağlam sürekli olarak yeniden oluşturuluyor, bellek düzensiz olarak tahsis ediliyor ve her hesaplamada kernel derleniyorsa, GPU bir hızlandırıcıdan ziyade bir gecikme kaynağına dönüşür.
Dolayısıyla buradaki temel pratik ilke çok basittir: önceden hazırlanabilecek her şey önceden hazırlanmalıdır. Yeniden kullanılabilecek hiçbir şey yeniden oluşturulmamalıdır. Ortam düzgün bir şekilde düzenlendiğinde, OpenCL ağır hesaplamaları hızlandırmaya gerçekten yardımcı olur. Bu disiplin olmadığında, hesaplamalar başlamadan önce bile fayda kolayca kaybedilir.
Bellek yönetimi
GPU'lar söz konusu olduğunda, birçok kişi öncelikle hesaplama gücü hakkında düşünür. Ancak pratikte, çoğu zaman hesaplamaların hızından çok, verilerin tam olarak nasıl sağlandığına bağlıdır. Çok hızlı bir GPU bile bellekle çalışmakta zorluk çekiyorsa iyi sonuçlar vermeyecektir.
OpenCL'de bellek, verilerin depolandığı bir yerden daha fazlasıdır. Performans doğrudan nasıl kullanıldığına bağlıdır. GPU'nun çeşitli bellek seviyeleri vardır. Bazıları daha hızlı, bazıları daha yavaş çalışır. Küçük dahili bellek alanlarına hızlı bir şekilde erişilebilir, ancak cihazın ana belleğine erişmek önemli ölçüde daha pahalıdır.
Bu önemli bir kuralı gerektirir: veriler, GPU'nun yavaş belleğe mümkün olduğunca az erişmesini sağlayacak şekilde düzenlenmelidir. Gereksiz okuma ve yazma ne kadar az olursa o kadar iyi. Aynı verilerin CPU ve GPU arasında gereksiz yere tekrar tekrar aktarılmaması özellikle önemlidir. Ana darboğaz genellikle hesaplamanın kendisinden ziyade veri alışverişidir.

Basitçe söylemek gerekirse GPU, büyük bir veri dizisini bir kez gönderebileceğiniz, birçok benzer işlemi gerçekleştirebileceğiniz ve ardından bitmiş sonucu geri döndürebileceğiniz görevleri sever. Programın sürekli olarak küçük veri parçaları göndermesi ve her adımdan sonra yanıt beklemesi durumundan hoşlanmaz. Bu modda hızlanma çabucak kaybolur.
Bu özellikle alım-satım amaçları açısından hassastır. Eğer bir program her adımda önce verileri hazırlar, sonra iletir, sonra sonucu bekler ve sonra her şeyi tekrarlarsa, performans istikrarsız olacaktır. Gerekli verileri önceden toplamak, tek bir blok halinde cihaza göndermek, hesaplamayı yapmak ve sonucu ana program tarafında kullanmak çok daha iyidir.
Burada rolleri doğru bir şekilde bölmek önemlidir. CPU kontrol merkezi rolünde kalır: verileri toplar, hesaplamaları başlatır ve nihai kararı verir. GPU, işin aynı işlemi birçok kez hızlı bir şekilde tekrarlamamız gereken kısmını üstlenir. Bu yapı, her şeyi tek bir cihaza aktarmaya çalışmaktan neredeyse her zaman daha güvenilir ve verimlidir.
Bir başka tipik hata daha vardır - çok küçük kernel başlatmaları. İlk bakışta bu kullanışlı görünüyor: yeni veriler geliyor ve hemen GPU'ya gönderiliyor. Ancak her başlatma aynı zamanda zamana da mal oluyor. Çok fazla başlatma yapılırsa, genel maliyetler tüm karı tüketmeye başlar. Bu nedenle, çoğu durumda GPU'yu daha az sıklıkta çalıştırmak, ancak ona daha büyük ve daha doğrudan bir görev vermek daha iyidir.
Sonuç olarak, bellekle çalışmak küçük bir teknik ayrıntı değil, performansın ana faktörlerinden biridir. Veriler doğru şekilde düzenlenirse GPU bir iş hattı gibi çalışır. Aksi takdirde, güçlü bir cihaz bile boşta kalacak ve gereksiz istek ve aktarımlarla zaman kaybedecektir.
Sonuç: GPU, yalnızca veriler kendisine doğru şekilde beslendiğinde hesaplamaları hızlandırır. Gereksiz iletimler, talepler ve küçük başlatmalar ne kadar az olursa, gerçek etki o kadar yüksek olur.
Bir program oluşturma
Uygulama programında CPU ve GPU birbiriyle rekabet etmez, birlikte çalışır. CPU kontrol merkezi olarak kalır - ilk verileri oluşturur, gerekli metodu çağırır, sonucu kabul eder ve yürütme süresini karşılaştırır. GPU yalnızca hesaplama kısmını üstlenir. Bu klasik bir yapıdır: tüm programı cihaza sürüklemek yerine, sadece gerçek paralelliğin olduğu bölümü vermeliyiz.
OpenCL kullanarak bir program oluşturmayı anlamanın en kolay yolu belirli bir örnek kullanmaktır. MQL5 standart kütüphanesi, matris çarpımının açıklayıcı bir uygulamasını içerir. Ana programda ilk olarak iki matris oluşturulur ve rastgele değerlerle doldurulur.
void OnStart() { //--- matrix A 1000x2000 int rows_a = 1000; int cols_a = 2000; //--- matrix B 2000x1000 int rows_b = cols_a; int cols_b = 1000; //--- matrix C 1000x1000 int rows_c = rows_a; int cols_c = cols_b; //--- matrix A: size=rows_a*cols_a int size_a = rows_a * cols_a; int size_b = rows_b * cols_b; int size_c = rows_c * cols_c; //--- prepare matrix A float matrix_a[]; ArrayResize(matrix_a, rows_a * cols_a); for(int i = 0; i < rows_a; i++) for(int j = 0; j < cols_a; j++) { matrix_a[i * cols_a + j] = (float)(10 * MathRand() / 32767); } //--- prepare matrix B float matrix_b[]; ArrayResize(matrix_b, rows_b * cols_b); for(int i = 0; i < rows_b; i++) for(int j = 0; j < cols_b; j++) { matrix_b[i * cols_b + j] = (float)(10 * MathRand() / 32767); }
Önce CPU üzerinde sıralı bir hesaplama çağrılır ve ardından aynı hesaplama GPU üzerinde gerçekleştirilir.
//--- CPU: calculate matrix product matrix_a*matrix_b float matrix_c_cpu[]; ulong time_cpu = 0; if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu)) { PrintFormat("Error in calculation on CPU. Error code=%d", GetLastError()); return; } //--- calculate matrix product using GPU float matrix_c_gpu_method1[]; float matrix_c_gpu_method2[]; ulong time_gpu_method1 = 0; ulong time_gpu_method2 = 0; if(!MatrixMult_GPU(matrix_a, matrix_b, matrix_c_gpu_method1, matrix_c_gpu_method2, rows_a, cols_a, cols_b, size_a, size_b, size_c, time_gpu_method1, time_gpu_method2)) { PrintFormat("Error in calculation on GPU. Error code=%d", GetLastError()); return; }
Hesaplamaların kendileri ayrı metotlarla gerçekleştirilir. GPU kısmı aynı problemin iki uygulamasında sunulmaktadır: basit ve optimize edilmiş. Bu, farkı kelimelerle değil, kodda ve yürütme süresinde hemen görmemizi sağlar.
CPU versiyonuna bakalım. Burada her şey son derece açık: klasik bir üçlü döngü. Ortaya çıkan matrisin her bir elemanı sırayla hesaplanır.
bool MatrixMult_CPU(const float &matrix_a[], const float &matrix_b[], float &matrix_c[], const int rows_a, const int cols_a, const int cols_b, ulong &time_cpu) { int size = rows_a * cols_b; if(ArrayResize(matrix_c, size) != size) return(false); //--- CPU calculation started time_cpu = GetMicrosecondCount(); for(int i = 0; i < rows_a; i++) { for(int j = 0; j < cols_b; j++) { float sum = 0.0; for(int k = 0; k < cols_a; k++) { sum += matrix_a[cols_a * i + k] * matrix_b[cols_b * k + j]; } matrix_c[cols_b * i + j] = sum; } } //--- CPU calculation finished time_cpu = ulong((GetMicrosecondCount() - time_cpu) / 1000); //--- return(true); }
Bu kod, görevin temel fikrini göstermektedir. İki matris vardır. Satır ve sütun bazında çarpımların toplamı yapılır. Sıralı bir yürütme gerçekleşir. CPU'da bu, şeffaf bir şekilde ve gereksiz bir hazırlık olmadan çalışır. Ancak ana sınırlama tam olarak burada yatmaktadır: matris büyüklüğü arttıkça, sıralı hesaplamalar giderek daha pahalı hale gelir.
Sırada GPU kısmı var. Burada ana ilkeyi vurgulamak önemlidir: Bu örnekte OpenCL ayrı bir metotta gizlenmiştir. Ana program temiz kalır ve tüm GPU işlemleri tek bir yerde toplanır.
bool MatrixMult_GPU(const float &matrix_a[], const float &matrix_b[], float &matrix1_c[], float &matrix2_c[], const int rows_a, const int cols_a, const int cols_b, const int size_a, const int size_b, const int size_c, ulong &time1_gpu, ulong &time2_gpu) { const int task_dimension = 2; //--- prepare matrices for result if(ArrayResize(matrix1_c, size_c) != size_c || ArrayResize(matrix2_c, size_c) != size_c) return(false); ArrayFill(matrix1_c, 0, size_c, (float)0.0); ArrayFill(matrix2_c, 0, size_c, (float)0.0);
Burada önemli bir şeyi hemen görebilirsiniz: iki GPU seçeneği için sonuç matrisleri önceden hazırlanmıştır. Bu, hesaplamaları bellek hazırlığı ile karıştırmamızı önler. Bu seviyede program zaten disiplinli bir şekilde yapılandırılmıştır: önce dizilerin tahsisi, ardından OpenCL gelir.
Ardından, OpenCL bağlamı oluşturulur ve başlatılır.
//--- OpenCL ulong timei_gpu = GetMicrosecondCount(); COpenCL OpenCL; if(!OpenCL.Initialize(cl_program, true)) { PrintFormat("Error in OpenCL initialization. Error code=%d", GetLastError()); return(false); }
OpenCL'in pratik anlamı burada ortaya çıkmaktadır. Bu noktaya kadar program geleneksel bir MQL5 koduydu. Şimdi GPU için hesaplama ortamını hazırlıyor. Ve özellikle önemli olan, bunun maliyetsiz bir işlem olmamasıdır. Başlatma süresini ayrı ayrı ölçüyoruz. Hızlandırmadan bahsetmeden önce, ortamın kendisini başlatmanın ne kadara mal olduğuna dürüstçe bakmamız gerekiyor.
Daha sonra iki kernel oluşturulur. İlki basit bir paralel varyanttır. İkincisi ise yerel gruplarla birlikte daha ileri düzeydedir.
//--- create kernels OpenCL.SetKernelsCount(2); OpenCL.KernelCreate(0, "MatrixMult_GPU1"); OpenCL.KernelCreate(1, "MatrixMult_GPU2");
Burada, yürütme süresinin büyük ölçüde OpenCL programında kullanılan algoritmanın kalitesine bağlı olduğunu belirtmek gerekir. Hesaplamayı basitçe paralelleştirebilir veya bellek yönetimini iyileştirebiliriz. Örnek, her iki seçeneği de içermekte ve bu da durumu özellikle netleştirmektedir.
Ardından, arabellekler hazırlanır. Girdi matrisleri cihaza kopyalanır ve sonuç için ayrı bir arabellek oluşturulur.
//--- create buffers OpenCL.SetBuffersCount(3); //--- if(!OpenCL.BufferFromArray(0, matrix_a, 0, size_a, CL_MEM_READ_ONLY)) { PrintFormat("Error in BufferFromArray for matrix A. Error code=%d", GetLastError()); return(false); } if(!OpenCL.BufferFromArray(1, matrix_b, 0, size_b, CL_MEM_READ_ONLY)) { PrintFormat("Error in BufferFromArray for matrix B. Error code=%d", GetLastError()); return(false); } if(!OpenCL.BufferCreate(2, size_c * sizeof(float), CL_MEM_WRITE_ONLY)) { PrintFormat("Error in BufferCreate for matrix C. Error code=%d", GetLastError()); return(false); }
Bu zaten gerçek bir çalışan GPU yapısıdır. Veriler cihaza aktarılır, orada hesaplanır ve ardından geri gönderilir. Hızlanmayı mümkün kılan da bu düzendir. Eğer bunu küçük parçalara ayırırsak, GPU hesaplamanın kendisinden ziyade hazırlık ve veri alışverişi için çok fazla zaman harcamaya başlayacaktır.
Bundan sonra, ilk kernel'ın argümanları ayarlanır.
//--- prepare arguments for kernel 0 int kernel_index = 0; OpenCL.SetArgumentBuffer(kernel_index, 0, 0); OpenCL.SetArgumentBuffer(kernel_index, 1, 1); OpenCL.SetArgumentBuffer(kernel_index, 2, 2); OpenCL.SetArgument(kernel_index, 3, rows_a); OpenCL.SetArgument(kernel_index, 4, cols_a); OpenCL.SetArgument(kernel_index, 5, cols_b); timei_gpu = ulong((GetMicrosecondCount() - timei_gpu) / 1000); PrintFormat("time of initialization GPU =%d ms", timei_gpu);
Buradaki mantık çok basittir: kernel veri arabelleklerini ve matris büyüklüklerini alır. OpenCL kernel'ı içinde, hangi iş parçacığının hangi elemandan sorumlu olduğuna karar verilir. Bu önemli bir noktadır: GPU kendi başına dizileri nasıl yorumlayacağını bilmez. Bu yapının ona açıkça iletilmesi gerekir.
Ardından problem büyüklüğü ayarlanır ve ilk hesaplama seçeneği başlatılır.
//--- set task dimension a_rows x b_cols uint global_work_size[2]; //--- set dimensions global_work_size[0] = rows_a; global_work_size[1] = cols_b; uint global_work_offset[2] = {0, 0}; //--- GPU calculation start kernel 0 time1_gpu = GetMicrosecondCount(); if(!OpenCL.Execute(kernel_index, task_dimension, global_work_offset, global_work_size)) { PrintFormat("Error in Execute. Error code=%d", GetLastError()); return(false); } if(!OpenCL.BufferRead(2, matrix1_c, 0, 0, size_c)) { PrintFormat("Error in BufferRead for matrix1 C. Error code=%d", GetLastError()); return(false); } //--- GPU calculation finished time1_gpu = ulong((GetMicrosecondCount() - time1_gpu) / 1000);
Bu ilk GPU seçeneğidir. Temel fikri gösterir: bir iş parçacığı bir çıktı elemanını hesaplar. Bu yapı zaten paralellik sağlar, ancak henüz bellek optimizasyonunun tüm olanaklarını kullanmaz. Bu yüzden örnekte yanında ikinci bir kernel vardır. Başlatmadan önce, argümanlar aynı şekilde belirtilir, ancak yürütme yöntemi farklıdır - yerel bir çalışma grubu ile.
//--- prepare arguments for kernel 1 kernel_index = 1; //--- set arguments OpenCL.SetArgumentBuffer(kernel_index, 0, 0); OpenCL.SetArgumentBuffer(kernel_index, 1, 1); OpenCL.SetArgumentBuffer(kernel_index, 2, 2); OpenCL.SetArgument(kernel_index, 3, rows_a); OpenCL.SetArgument(kernel_index, 4, cols_a); OpenCL.SetArgument(kernel_index, 5, cols_b); uint local_work_size[2]; local_work_size[0] = BLOCK_SIZE; local_work_size[1] = BLOCK_SIZE;
Pratik fark burada ortaya çıkmaktadır. İlk versiyon basitçe hesaplamayı paralelleştirir. İkincisi hesaplamayı bloklar halinde düzenler. Bu, ilk bakışta göründüğünden daha önemlidir. GPU sadece paralelliği değil, aynı zamanda iyi bir bellek organizasyonunu da sever. Dolayısıyla, bloklar ve yerel gruplar önemli faydalar sağlamaktadır.
İkinci kernel başlatma yöntemine çalışma grupları boyutunu ekleyelim.
//--- GPU calculation start, kernel1 time2_gpu = GetMicrosecondCount(); if(!OpenCL.Execute(kernel_index, task_dimension, global_work_offset, global_work_size, local_work_size)) { PrintFormat("Error in Execute. Error code=%d", GetLastError()); return(false); } if(!OpenCL.BufferRead(2, matrix2_c, 0, 0, size_c)) { PrintFormat("Error in BufferRead for matrix2 C. Error code=%d", GetLastError()); return(false); } //--- GPU calculation finished time2_gpu = ulong((GetMicrosecondCount() - time2_gpu) / 1000); //--- remove OpenCL objects OpenCL.Shutdown(); //--- return(true); }
Şimdi en ilginç kısım OpenCL kodunun içinde ne olduğudur. İlk kernel versiyonu olabildiğince basit görünüyor.
__kernel void MatrixMult_GPU1(__global float *matrix_a, __global float *matrix_b, __global float *matrix_c, int rows_a, int cols_a, int cols_b) { int i = get_global_id(0); int j = get_global_id(1); float sum = 0.0; for(int k = 0; k < cols_a; k++) { sum += matrix_a[cols_a * i + k] * matrix_b[cols_b * k + j]; } matrix_c[cols_b * i + j] = sum; }
Bu, matematiksel mantığın GPU'ya neredeyse birebir aktarımıdır. Her iş parçacığı kendi koordinatını alır ve sonuç matrisinin bir elemanını hesaplar.
İkinci versiyon daha ilginçtir. Yerel diziler ve iş parçacığı senkronizasyonu kullanır.
__kernel void MatrixMult_GPU2(__global float *matrix_a, __global float *matrix_b, __global float *matrix_c, int rows_a, int cols_a, int cols_b) { int group_i = get_group_id(0); int group_j = get_group_id(1); int i = get_local_id(0); int j = get_local_id(1); __local float submatrix_a[BLOCK_SIZE][BLOCK_SIZE]; __local float submatrix_b[BLOCK_SIZE][BLOCK_SIZE]; int offset_b = BLOCK_SIZE * group_i; int offset_a_start = cols_a * BLOCK_SIZE * group_j; float sum = (float)0.0;
Burada hesaplamanın farklı bir şekilde yapıldığı açıktır. İş parçacıkları gruplar halinde birleştirilir ve veriler bloğun dahili belleğine yüklenir. Bu, global belleğe yapılan çağrıların sayısını azaltır ve cihazın daha verimli çalışmasını sağlar.
Ardından, parçalar yüklenir ve iş parçacıkları senkronize edilir.
for(int offset_a = offset_a_start; offset_a < offset_a_start + cols_a; offset_a += BLOCK_SIZE, offset_b += BLOCK_SIZE * cols_b) { submatrix_a[i][j] = matrix_a[offset_a + cols_a * i + j]; submatrix_b[i][j] = matrix_b[offset_b + cols_b * i + j]; barrier(CLK_LOCAL_MEM_FENCE); for(int k = 0; k < BLOCK_SIZE; k++) sum += submatrix_a[i][k] * submatrix_b[k][j]; barrier(CLK_LOCAL_MEM_FENCE); }
İşte neden iki GPU uygulaması olduğu sorusunun cevabı. İlki paralelleştirmeyi gösterir. İkincisi bellek optimizasyonunu gösterir. Pratik çalışmalarda, hızlanmanın kaderini belirleyen de çoğu zaman tam olarak budur.
Elde edilen çıktı, çıktı dizisine geri ayarlanır.
int offset_c = BLOCK_SIZE * (cols_b * group_j + group_i);
matrix_c[offset_c + cols_b * i + j] = sum;
};
Bu iki kernel ana programda yürütme süresine göre karşılaştırılır ve hesaplamaların doğruluğu bir CPU seçeneği ile karşılaştırılarak kontrol edilir.
//--- calculate CPU/GPU ratio double CPU_GPU_ratio1 = 0; double CPU_GPU_ratio2 = 0; if(time_gpu_method1 != 0) CPU_GPU_ratio1 = 1.0 * time_cpu / time_gpu_method1; if(time_gpu_method2 != 0) CPU_GPU_ratio2 = 1.0 * time_cpu / time_gpu_method2; PrintFormat("time CPU=%d ms, time GPU global work groups =%d ms, CPU/GPU ratio: %f", time_cpu, time_gpu_method1, CPU_GPU_ratio1); PrintFormat("time CPU=%d ms, time GPU local work groups =%d ms, CPU/GPU ratio: %f", time_cpu, time_gpu_method2, CPU_GPU_ratio2); PrintFormat("time matrix CPU=%d ms", time_mat);
Bütünlük adına, karşılaştırmaya yerleşik matris işlemlerinin kullanımını da ekleyelim.
//--- matrix matrix<float> A, B, C; if(!A.Assign(matrix_a) || !B.Assign(matrix_b)) { PrintFormat("Error of copy data to matrices. Error code=%d", GetLastError()); return; } if(!A.Reshape(rows_a, cols_a) || !B.Reshape(rows_b, cols_b)) { PrintFormat("Error of copy data to matrices. Error code=%d", GetLastError()); return; } ulong time_mat = GetMicrosecondCount(); C = A.MatMul(B); time_mat = ulong((GetMicrosecondCount() - time_mat) / 1000);
Pratik bir deneyde, hibrit bir sistemdeki matris çarpımı süresi karşılaştırılmıştır: bir CPU ve iki GPU: NVIDIA GeForce RTX 4060 Laptop GPU ve Intel Iris Xe Graphics. CPU uygulaması, yaklaşık 2056 - 2180 ms'lik bir süre gösteren bir referans noktası olarak kullanılmıştır. Burada, yaklaşık 32-34 ms'lik istikrarlı bir süre gösteren optimize edilmiş matris işlemlerine dikkat çekmek gerekir; bu, bu sınıftaki bir CPU için vektörleştirilmiş işleme konusunda beklenen performans seviyesine yakın olarak kabul edilebilir.

Hesaplamalar GPU'ya aktarıldığında, OpenCL verimliliğinin hesaplama bloklarının organizasyonuna ne kadar bağlı olduğunu vurgulayan karışık sonuçlar elde edilmiştir. RTX 4060 durumunda, global çalışma grupları kullanıldığında, yürütme süresi yaklaşık 38 ms'dir ve bu da CPU matris işlemlerine göre bir avantaj sağlamaz. Ancak, yerel çalışma gruplarına geçiş durumu kökten değiştirdi - süre 11 ms'ye düştü. Bu, yerel bellekteki verilerin yeniden kullanılmasının global bellek üzerindeki baskıyı azalttığı ve GPU hesaplama birimlerinin daha verimli bir şekilde kullanılmasına izin verdiği katmanlı bir yaklaşımın tam teşekküllü çalışmasını göstermektedir.
Benzer, ancak daha belirgin bir eğilim entegre Intel Iris Xe'de de gözlenmektedir. Global gruplar modunda yürütme süresi 213 ms iken, yerel gruplar kullanıldığında bu süre 45 ms'ye düşmüştür. Ayrık GPU'nun gerisinde genel bir gecikmenin devam etmesine rağmen, göreceli hızlanma burada daha da belirgindir ve bu da daha az üretken GPU'nun bellek erişimi optimizasyonuna olan hassasiyetini vurgulamaktadır.
GPU başlatma süresi özel bir ilgiyi hak etmektedir. RTX 4060 için bu süre yaklaşık 99 ms iken Iris Xe için yaklaşık 4 ms'dir. Bu faktör, hesaplama hattının genel verimliliğini etkilediği için uygulama senaryolarında göz ardı edilemez.
Genel olarak sonuçlar, belleğe bağlı çalışma modundan hesaplama etkin çalışma moduna geçişin klasik bir resmini ortaya koymaktadır. CPU, görevin belirli ölçeğinde rekabetçi kalırken, GPU'lar avantajlarını yalnızca yerel hesaplama birimlerinin doğru organizasyonu ile ortaya koymaktadır. Bu durum özellikle RTX 4060'ta açıkça görülüyor; burada yetersiz ve optimize edilmiş uygulama arasındaki fark yaklaşık üç ila dört kata ulaşıyor ve bu da grafik hızlandırıcının verimsiz ve tam teşekküllü kullanımı arasındaki sınırı etkili bir şekilde tanımlıyor.
Mum formasyonlarını test etme
Matris çarpımıyla ilgili örnek, bir GPU'nun hesaplamaları tam olarak nasıl hızlandırabileceğini iyi bir şekilde göstermektedir. Ancak bu bir yatırımcı için yeterli değildir. Bu yaklaşımın gerçekten piyasa ile ilgili bir problemde kullanılıp kullanılamayacağını anlamak daha önemlidir. Yani bir sonraki adım soyut matematikten mum formasyon analizine geçmektir.
Mum formasyonları genellikle iyi bilinen adlara sahip hazır şekiller olarak tanımlanır. Bir resimde tanınmaları kolaydır ve kitaplarda genellikle inandırıcı görünürler. Ancak bu konuya biraz daha yakından bakacak olursak şu soru ortaya çıkmaktadır: bu tür formasyonlar gerçekte istatistikler tarafından ne ölçüde desteklenmektedir? Kesin kriterler nelerdir? Bir formasyonun teoride değil, gerçek veriler üzerinde işe yarayıp yaramadığını nasıl anlarsınız?
Kitaplardaki tanıdık şekilleri aramak yerine, probleme farklı bir şekilde yaklaşabilirsiniz. Piyasadaki hazır şablonları zorlamayın, daha önce benzer durumlarda nasıl davrandığına bakın.
Son birkaç mum dikkate alınır - yani mevcut piyasa durumu. Bu bizim referans formasyonumuz. Program daha sonra geçmişi gözden geçirir ve şekil olarak ona benzeyen alanları arar. Karşılaştırma formasyon adına göre değil, gerçek mum özelliklerine göre yapılır: gövde, üst gölge ve alt gölge. Dahası, hafif bir sapmaya izin verilir, çünkü piyasa neredeyse hiçbir zaman aynı formasyonu mükemmel bir şekilde tekrarlamaz.
Geçmişte benzer bir bölüm varsa, program herhangi bir tahminde bulunmaz, ancak sonucu kontrol eder. Bu durum için, Kar Al ve Zararı Durdur seviyelerinin yanı sıra bir zaman sınırı belirlenerek bir alım-satım işlemi simüle edilir. Daha sonra geçmişte bu tür durumların karla mı yoksa zararla mı sonuçlandığını hesaplarız.
İşte bu noktada OpenCL özellikle kullanışlı hale geliyor. Bu görev çok sayıda benzer kontrolden oluşur: geçmişin birçok bölümünü gözden geçirmemiz, bunları mevcut şablonla karşılaştırmamız ve her seçenek için işlem sonucunu hesaplamamız gerekir. CPU için bu mümkündür, ancak büyük miktarda veri ile hesaplamalar zorlaşır. GPU için ise tam tersine doğal bir yük söz konusudur: paralel olarak gerçekleştirilebilen birçok bağımsız hesaplama.
Dahası, makale sadece bir işlem parametresi kombinasyonunu değil, aynı anda bir dizi seçeneği test etmektedir. Yani, program aynı anda farklı Kar Al ve Zararı Durdur değerlerini dikkate alır. Bu, benzer piyasa koşullarında hangi parametrelerin en makul göründüğünü değerlendirmemizi sağlar.
OpenCL tarafındaki mantıkla başlayalım - görev burada gerçek şeklini alıyor. Bu tasarımda CPU şef olarak kalır, ancak tüm zor işler - arama, karşılaştırma, modelleme - GPU'ya gider.
Burada sahip olduğumuz şey bir analiz hattıdır.
__kernel void PatternStats3D(__global const float4 *price, __global const float *tp, __global const float *sl, __global float *global_stats, const int bars, const float tolerance, const int horizon) { const int lid = get_local_id(0); const int itp = get_global_id(1); const int isl = get_global_id(2); const int total_loc = get_local_size(0); const int tp_count = get_global_size(1); const int sl_count = get_global_size(2);
Kaynak veriler son derece kompakt bir şekilde düzenlenir. Geçmiş, float4 vektör dizisi olarak iletilir. Her veri girişi bir mumdur: Açılış, Yüksek, Düşük, Kapanış. Bu çok önemlidir. Verileri ayrı dizilere bölmüyoruz ve erişimi zorlaştırmıyoruz. GPU yoğun yapılarda daha iyi çalışıyor ve burada bu avantajdan tam olarak faydalanılıyor.
TP ve SL dizileri ayrı ayrı iletilir. Bu şekilde, görevin ikinci ve üçüncü boyutlarını hemen belirlemiş oluyoruz. Her bir iş parçacığı yalnızca kendi geçmiş bölümüyle değil, aynı zamanda belirli bir işlem parametreleri kombinasyonuyla da çalışır. Sonuç olarak, hesaplama uzayı üç boyutlu hale gelir: geçmiş × TP × SL.
Sonra çalışmanın kendisi başlar. Grup içindeki her iş parçacığı kendi yerel kimliğini alır ve grup büyüklüğüne eşit bir adımla geçmişi dolaşmaya başlar.
__local int buf_stat[BLOCK_SIZE][STAT_DIM]; int local_stat[STAT_DIM]; for(int i=0;i<STAT_DIM;i++) local_stat[i]=0; //--- float4 pattern[PATTERN_SIZE]; if(bars < (PATTERN_SIZE + horizon)) return; for(int i = 0; i < PATTERN_SIZE; i++) pattern[i] = price[bars - 1 - PATTERN_SIZE + i]; //--- border for(int i = lid; i < (bars - horizon - PATTERN_SIZE); i += total_loc) { bool match = true; //--- pattern check for(int k = 0; k < PATTERN_SIZE ; k++) { float4 a = price[i + k]; float body_a = a.w - a.x; float body_b = pattern[k].w - pattern[k].x; if(fabs(body_a - body_b) > tolerance) { match = false; break; } float upper_a = a.y - fmax(a.w, a.x); float upper_b = pattern[k].y - fmax(pattern[k].w, pattern[k].x); if(fabs(upper_a - upper_b) > tolerance) { match = false; break; } float lower_a = fmin(a.w, a.x) - a.z; float lower_b = fmin(pattern[k].w, pattern[k].x) - pattern[k].z; if(fabs(lower_a - lower_b) > tolerance) { match = false; break; } }
Bu klasik bir yöntemdir. Her çubuk için bir akış oluşturmuyoruz - bu çok pahalı olurdu. Bunun yerine, her iş parçacığı kendi veri şeridini işler. Gereksiz kernel başlatmaları olmadan yük eşit olarak dağıtılır.
Geçiş başlamadan önce bir standart oluşturulur. Geçmişten son mumları alalım. Bizi ilgilendiren mevcut piyasa budur. Dış formasyon yok. Tahmin yok. Sadece gerçek fiyat durumu.
Ardından kilit nokta geliyor: karşılaştırma. Geçmişteki her konum için, bölümün standarda benzer olup olmadığı kontrol edilir. Ayrıca, karşılaştırma mum yapısına dayanmaktadır:
- Gövde;
- Üst gölge;
- Alt gölge.
Ve tüm bunlar toleransla gerçekleşir. Bu ince ama önemli bir nüanstır. Tam bir eşleşme şartı aramıyoruz. Piyasa mükemmel kopyalar oluşturmaz. Biz biçimle ilgileniyoruz, piksel özdeşliğiyle değil.
En az bir eleman toleransın dışındaysa eşleşme reddedilir. Hızlı ve gereksiz hesaplamalar olmadan. Bir eşleşme bulunursa, ikinci kısım başlar - işlem modellemesi. Giriş noktası, formasyondan sonra yeni bir mumun açılışıdır.
//--- simulate a trade if(match) { local_stat[0] += 1; int open = i + PATTERN_SIZE; float4 bar = price[open]; float entry = bar.x; float tp_val = tp[itp]; float sl_val = sl[isl]; float buy_tp = entry + tp_val; float buy_sl = entry - sl_val; float sell_tp = entry - tp_val; float sell_sl = entry + sl_val; bool buy_tp_hit = 0, buy_sl_hit = 0; bool sell_tp_hit = 0, sell_sl_hit = 0; for(int k = 0; k < horizon; k++) { bar = price[open + k]; float high = bar.y; float low = bar.z; // SL is checked first (worst-case) buy_sl_hit |= (buy_tp_hit == 0) & (buy_sl_hit == 0) & (low <= buy_sl); buy_tp_hit |= (buy_tp_hit == 0) & (buy_sl_hit == 0) & (high >= buy_tp); sell_sl_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (high >= sell_sl); sell_tp_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (low <= sell_tp); if((buy_tp_hit | buy_sl_hit) & (sell_tp_hit | sell_sl_hit)) break; } // forced closing by time buy_sl_hit |= (buy_tp_hit == 0) & (buy_sl_hit == 0) & (entry > bar.w); buy_tp_hit |= (buy_tp_hit == 0) & (buy_sl_hit == 0) & (entry < bar.w); sell_sl_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (entry < bar.w); sell_tp_hit |= (sell_tp_hit == 0) & (sell_sl_hit == 0) & (entry > bar.w); //--- local_stat[1] += (int)buy_tp_hit; local_stat[2] += (int)buy_sl_hit; local_stat[3] += (int)sell_tp_hit; local_stat[4] += (int)sell_sl_hit; } }
Ardından, TP ve SL seviyeleri alış ve satış için hesaplanır. Ayrıntıya dikkat edin: her iki taraf da aynı anda sayılır. Bu, hesaplamalardan tasarruf sağlar ve piyasa davranışının tam bir resmini verir.
Ardından ufuk sınırlamasıyla geçmiş boyunca ileri bir geçiş başlatılır. Her adımda aşağıdakiler kontrol edilir:
- SL'ye ulaşılıp ulaşılmadığı;
- TP'ye ulaşılıp ulaşılmadığı.
Önce SL kontrol edilir. Bu bir tesadüf değil, en kötü durum senaryosunun kasıtlı bir varsayımıdır. Bu yaklaşım, değerlendirmeyi daha muhafazakar ve dolayısıyla gerçeğe daha yakın hale getirir.
Her iki taraf için de sonuç belirlendikten sonra döngü kırılır. Ekstra bir iş yapılmaz.
Ne TP ne de SL'ye ulaşılmazsa, pozisyon zamana göre zorla kapatılır. Bu da bir diğer önemli unsurdur. Açık işlem bırakmıyoruz - her durum bir sonuç üretmelidir.
Tüm sonuçlar local_stat özel dizisinde toplanır. Bu önemlidir. Bu aşamada senkronizasyon yoktur. Her bir iş parçacığı bağımsız ve hızlı bir şekilde çalışır.
Ardından, her şeyin inşa edildiği şeye geçiyoruz - yerel toplama. İlk BLOCK_SIZE iş parçacıkları sonuçlarını buf_stat'a yazar. Bu yerel bellektir, hızlıdır ve grup tarafından paylaşılır.
//--- write to 'local' if(lid < BLOCK_SIZE) for(int k = 0; k < STAT_DIM; k++) buf_stat[lid][k] = local_stat[k]; barrier(CLK_LOCAL_MEM_FENCE);
Sonrasında, arabellek büyüklüğünden daha fazla iş parçacığı varsa verileri sıkıştıran ek bir geçiş gelir. Bu, rastgele bir grup büyüklüğünü sabit bir küçültme penceresine zorlamanın düzgün bir yoludur.
for(int i = BLOCK_SIZE; i < total_loc; i += BLOCK_SIZE) { if(lid >= i && lid < (i + BLOCK_SIZE)) for(int k = 0; k < STAT_DIM; k++) buf_stat[lid-i][k] += local_stat[k]; barrier(CLK_LOCAL_MEM_FENCE); }
Bundan sonra, klasik azaltma gerçekleştirilir - adım azaltma ile ikili toplama.
//--- reduction for(int stride = BLOCK_SIZE / 2; stride > 0; stride >>= 1) { if(lid < stride) { for(int k = 0; k < STAT_DIM; k++) { buf_stat[lid][k] += buf_stat[lid + stride][k]; buf_stat[lid + stride][k] = 0; } } barrier(CLK_LOCAL_MEM_FENCE); }
Çıktı, tüm grup için toplu istatistikleri içeren bir akıştan oluşur. Son adımı gerçekleştirir - sonucu kaydeder.
//--- write the result if(lid == 0) { int idx = (itp * sl_count + isl) * STAT_DIM; int count = buf_stat[0][0]; global_stats[idx + 0] = count; float norm = (count > 0) ? (1.0f / ((float)count)) : 0.0f; for(int k = 1; k < STAT_DIM; k++) global_stats[idx + k] = buf_stat[0][k] * norm; } }
Burada önemli bir dönüşüm meydana gelir:
- Toplam eşleşme sayısı olduğu gibi tutulur;
- Kalan değerler olasılıklara indirgenir.
Hesaplama tamamlandığında, çıktı bir ham sayılar kümesi değil, hazır istatistiklerdir. Şunları görebiliriz:
- Geçmiş verilerde benzer bir durumun kaç kez meydana geldiğini;
- Kar Alın alış için ne sıklıkla tetiklendiğini;
- Zararı Durdurun ne sıklıkla tetiklendiğini;
- Satış işlemlerinin nasıl davrandığını;
- Hangi parametre kombinasyonlarının daha iyi göründüğünü.
Ana program daha sonra karar verir. Güzel bir formasyondan ziyade istatistiklere bakar. Çok az veri varsa sinyal yok sayılır. Örneklem yeterliyse, alış ve satış olasılıkları karşılaştırılır. Daha sonra Kar Al ve Zararı Durdur değerlerinin daha uygun değerleri seçilir. Ancak bundan sonra bir pozisyon açılabilir.
Bu önemli bir noktadır. Böyle bir yapıda, karar bir tahmine veya katı bir kurallar dizisine dayanmaz. Piyasanın genellikle benzer koşullar altında nasıl davrandığının test edilmesine dayanır. Bu yaklaşım karı garanti etmez, ancak sistemsel veri analizi fikriyle çok daha tutarlıdır.

Test sonuçları bunu doğrulamaktadır. Sistem mükemmel görünmüyor ve çok güzel bir varlık eğrisi göstermiyor. Bu iyi bile olabilir - elimizdeki aşırı uyumlu bir model değil, normal piyasa gürültüsü, bir dizi kayıp ve düşüşle karşılaşan oldukça basit bir çalışan yapıdır. Ancak yine de pozitif bir sonuç göstermektedir.
Burada özellikle önemli olan, nihai karın orta düzeyde olması değil, klasik göstergelere ve önceden belirlenmiş formasyonlara dayanmadan istatistiksel olarak anlamlı bir alım-satım mekanizması oluşturabilmiş olmamızdır. Bu yapıda OpenCL, böyle bir analizi zaman açısından pratik olarak mümkün kılan bir hızlandırıcı rolü oynamaktadır.
Başka bir deyişle, GPU burada strateji karlılığı için gerekli değildir. Çok sayıda geçmiş eşleşmeyi hızlı bir şekilde test etmek ve piyasa verilerini ölçülebilir bir hipoteze dönüştürmek için gereklidir.

Sonuç: OpenCL, mum formasyonu analizi için kullanışlıdır çünkü çeşitli benzer durumları hızlı bir şekilde incelememize, işlem sonuçlarını hesaplamamıza ve görsel tahmin yerine istatistiklere güvenmemize olanak tanır.
Sonuç
MQL5'te OpenCL, belirli bir görev sınıfı için bir araç olarak algılanmalıdır. Büyük bir veri dizisi, tekrarlanan işlemler ve hesaplamaları etkili bir şekilde paralelleştirme olanağı olduğunda değerini gösterir. Diğer tüm durumlarda, CPU daha rasyonel bir seçim olmaya devam etmektedir: daha basit, daha esnek ve küçük veya zayıf ölçeklenebilir hesaplamalar söz konusu olduğunda genellikle daha hızlıdır.
Makale, mimari sınırlamaların anlaşılmasından, hesaplamaların GPU'ya aktarılması için pratik bir yapıya kadar uzanıyor. Sonucun sadece GPU yetenekleri tarafından değil, aynı zamanda hesaplama devresinin organizasyon kalitesi tarafından da belirlendiği gösterilmiştir. Sık başlatmalar, gereksiz veri aktarımları ve çok küçük görevler tüm kazanımları kolayca tüketir. Aksine, bağlam yeniden kullanıldığında, büyük dizilerle çalışıldığında ve veriler dikkatlice aktarıldığında, GPU gerçek performansını göstermeye başlar.
Bu durum özellikle matris çarpımı örneğinde açıkça görülmektedir: yeterli miktarda veri ile paralel işleme, CPU'ya kıyasla kat kat artış sağlamaktadır. Ancak yapının pratik değeri sadece sentetik testlerde ortaya çıkmıyor. GPU, gerçek alım-satıma daha yakın bir görevde, toplu optimizasyonlar, hipotez testleri ve verilerde istikrarlı formasyonların aranmasıyla ilgili araştırmaları hızlandırmaya olanak tanır. İşte bu noktada grafik kartının kendi başına değil, hesaplama disiplinini ölçeklendirmenin bir aracı olarak yararlı olduğu ortaya çıkıyor.
Ana sonuç pratik olmaya devam etmektedir. GPU'ya aktarım bir alım-satım sistemini karlı hale getirmez ve anlamlı bir piyasa modelinin yerini almaz. Bunun yerine, CPU'da çok pahalı veya çok yavaş olacak numaralandırma ve analiz hacmine erişim sağlar. Gerçek değeri budur: mucizeler vaat etmek değil, MQL5'te daha geniş ve daha sistematik bir araştırma döngüsü sağlamak.
Makalede kullanılan programlar
| # | Ad | Tür | Açıklama |
|---|---|---|---|
| 1 | PatternStats.mq5 | Uzman Danışman | Test Uzman Danışmanı |
| 2 | PatternStats.cl | Kod Tabanı | OpenCL program kodu kütüphanesi |
MetaQuotes Ltd tarafından Rusçadan çevrilmiştir.
Orijinal makale: https://www.mql5.com/ru/articles/22242
Uyarı: Bu materyallerin tüm hakları MetaQuotes Ltd'ye aittir. Bu materyallerin tamamen veya kısmen kopyalanması veya yeniden yazdırılması yasaktır.
Yeni Raylara Adım Atın: MQL5'te Özel Göstergeler
MetaTrader 5 ve MQL5 Ekonomik Takvim: Haberler Nasıl Tekrarlanabilir Bir Alım-Satım Sistemine Dönüştürülür?
İşte Karışınızda Yeni MetaTrader 5 ve MQL5
Python + MetaTrader 5: Veriler, Özellikler ve Prototipler için Hızlı Araştırma Çerçevesi
- Ücretsiz alım-satım uygulamaları
- İşlem kopyalama için 8.000'den fazla sinyal
- Finansal piyasaları keşfetmek için ekonomik haberler
Web sitesi politikasını ve kullanım şartlarını kabul edersiniz