GPU Hassasiyet Tuzağı: Apple Metal Üzerinde fp32 Backtest Nasıl Sessizce Çöp Değerler Döndürür
"İllüzyonsuz Backtest'ler" serisinin bir parçası.
Parametre-taraması backtest'imizi GPU'ya taşıdık ve 2.796× daha hızlı oldu. Döndürdüğü sayılar makuldü. Aynı zamanda, ilk çalışan sürümünde, tamamen çöptüler — iki yüz kat sapmayla — ve hiçbir şey çökmedi, hiçbir uyarı çıkmadı, hiçbir şey yanlış görünmedi.
İşte bu, Apple Silicon üzerinde GPU backtesting'i CPU kodunun olmadığı bir şekilde tehlikeli kılan tuzaktır: Metal'in float64'ü yoktur. Backtest'inizin bir Apple GPU üzerinde dokunduğu her sayı, siz istemiş olsanız da olmasanız da, 32-bit bir kayan noktalı sayıdır. Ve bir hareketli ortalamayı vektörleştirmenin tek en cazip yolu — performans odaklı her quant'ın uzandığı O(n) prefix-sum hilesi — tam olarak fp32'nin fiyat ölçeğinde hayatta kalamadığı formülasyondur. Hata vermez. Tam hızda çalışır ve size, 211 kat yanlış olan bir hareketli ortalama üzerine kurulmuş, makul görünen bir öz sermaye eğrisi verir.
Çözüm işin ilginç kısmıdır, çünkü ne "daha fazla bit kullan" (kullanamazsınız) ne de "daha dikkatli ol" (naif kod zaten dikkatlidir). Çözüm, aynı matematiksel niceliği farklı bir toplamla hesaplamaktır — doğrudan bir pencereli konvolüsyon — ki bu her ara değeri fp32'nin tam olarak temsil edebileceği kadar küçük tutar. Aynı WMA, yedi anlamlı basamağa kadar aynı sonuç ve CPU üzerinde tek iş parçacıklı numba'dan 55.9× daha hızlı. Bu makale otopsidir: Metal'in size neden fp32'yi dayattığı, bariz formülasyonun tam olarak nerede taştığı, doğru olanın neden taşmadığı ve — eğrileri gözle tahmin ederek değil, işlemleri sayarak — hızlı sürümle dürüst fp64 sürümünün uyuştuğunu nasıl kanıtladığımız.
Buradaki tüm sayılar bir Apple M2 Max üzerinde ölçülmüştür, 150.000 bar × 80 parametre kombinasyonu, üç denemenin en iyisi, deponun scripts/bench_param_sweep.py dosyasından (M5 GPU metodu, commit 97eadaf), scripts/engine_multitf_gpu.py (04d71e8) ve scripts/GPU_NOTES.md içindeki tasarım notlarından alınmıştır.
Metal'in float64'ü yoktur

Bir CPU üzerinde, bir Python/numpy backtest'i varsayılan olarak çift hassasiyette çalışır. float64 size 52-bit'lik bir mantis verir: tamsayılar 2⁵³ ≈ 9×10¹⁵'e kadar tam olarak temsil edilir ve bağıl hassasiyet yaklaşık 1.1×10⁻¹⁶'dır. Bunu neredeyse hiç düşünmezsiniz, çünkü fiyat ölçeğinde — 30.000 civarında bir BTC kapanışı, 10¹⁴'e ulaşan kümülatif bir toplam — fp64'ün fazlasıyla payı vardır.
Apple'ın GPU'su bu payı sunmaz, çünkü float64'ü hiç sunmaz. Metal Shading Language'in double tipi yoktur; MLX, PyTorch-MPS ve Apple Silicon'u hedefleyen diğer her framework bunu miras alır. Onu açmak için bir bayrak yoktur ve yavaş-ama-doğru bir yedek yoktur. Eğer hesaplamanız GPU'ya dokunuyorsa, fp32'de gerçekleşir:
- Mantis: 23 bit (örtük öndeki bit ile 24). Tamsayılar yalnızca 2²⁴ = 16.777.216 ≈ 1.6×10⁷'ye kadar tamdır.
- Bağıl hassasiyet: ~1.2×10⁻⁷. Kabaca yedi anlamlı ondalık basamak, daha fazlası değil.
O 1.6×10⁷ tavanı hikayenin tamamıdır. Kulağa cömert geliyor — on altı milyon — ta ki bir backtest'in rutin olarak bundan çok daha büyük ara nicelikler ürettiğini fark edene kadar; ve bir ara değer 1.6×10⁷'yi geçtiği anda, fp32 artık ardışık tamsayıları bile temsil edemez, ihtiyaç duyduğunuz kesirli yapıyı bir kenara bırakın. Hassasiyet zarifçe bozulmaz; bir uçurumdan düşer ve uçurum, verinizin ikinci bir düşünce olmadan aştığı bir sayıdadır.
Tuzak, fp32'nin neredeyse her zaman sorunsuz olmasıdır. Bir backtest'in çoğu — fiyatlar, getiriler, PnL, Sharpe — yedi basamağın bolca yettiği aralıkta rahatça yaşar. Bu yüzden naif taşıma çalışır, bir smoke test'ten geçer, makul görünen çıktı üretir. Başarısızlık tam olarak tek bir işleme, bir ara değerin 10⁷'yi aştığı tek yere lokalize olur ve o işlem herkesin ilk vektörleştirdiği işlemdir.
Cazip formülasyon: tek bir prefix-sum taramasında WMA
Stratejimiz Hull hareketli ortalamalarına dayanır. Bir HMA, birbirine bileşik üç ağırlıklı hareketli ortalamadır; HMA3 varyantı ise dörttür. p uzunluğunda bir pencere üzerinde doğrusal bir çekirdeğe sahip ağırlıklı hareketli ortalama şudur:
150 bin bar üzerinde binlerce parametre kombinasyonunu tararken, WMA konvolüsyonları maliyetin ta kendisidir. Bu yüzden içgüdü — bir CPU üzerinde doğru içgüdü — her WMA'yı prefix-sum kullanarak O(n·p) yerine O(n) yapmaktır. İki kümülatif toplamı bir kez önceden hesaplarsınız,
ve ardından herhangi bir pencerenin doğrusal ağırlıklı toplamı, S1 ve S2'nin birkaç farkına ve indeks kaydırmasına indirgenir. Pencere başına döngü yok, pencere başına indirgeme yok — iki cumsum geçişi ve WMA'ların tüm matrisi dizi aritmetiğinden düşer. Güzelce vektörleşir, bir GPU'nun paralel-tarama ilkeline mükemmel şekilde eşlenir ve fp64'te tam olarak doğrudur.
Ayrıca fp32'de yapabileceğiniz tek en kötü şeydir ve nedeni S2 içinde gizlidir.
S2 = mx.cumsum(j * price) # j is the global bar index: 0, 1, 2, ... , n-1
j · price terimi sorundur. j, 150.000'e kadar çıkarken ve price 30.000 civarındayken, tek başına son terim 4.5×10⁹'dur ve S2, bu tür 150.000 terimin koşan toplamıdır. Fiyat ölçeğinde kalmaz. fp32'nin zaten sayamaz hale geldiği bölgeye tırmanır.
Nerede taşar: tuzağın aritmetiği

Büyüklük mertebelerini yan yana koyalım, çünkü tüm başarısızlığın yaşadığı yer orasıdır.
S2 = cumsum(j · price) kabaca price · n²/2 ≈ 30.000 · (150.000)²/2 ≈ 3×10¹⁴'e ulaşır. Buna ~10¹⁴ diyelim. Şimdi fp32 tam-tamsayı tavanını hatırlayın: ~1.6×10⁷. Koşan toplam, fp32'nin tam olarak temsil edebileceği son tamsayıyı yedi büyüklük mertebesi aşar.
Bu somut olarak ne anlama gelir? 10¹⁴ civarında, iki temsil edilebilir fp32 sayısı arasındaki boşluk — son basamaktaki bir birim, yani ULP — yaklaşık 2²³ ≈ 8×10⁶'dır. Yani S2 bir kez 10¹⁴ aralığına çıktığında, yalnızca ±8 milyona kadar bilinir. Sakladığı her değer, ~8×10⁶'nın en yakın katına yuvarlanmıştır.
Şimdi WMA kurtarmanın bununla ne yaptığını izleyin. Tek bir pencerenin ağırlıklı toplamını çıkarmak için, iki komşu S2 değerini çıkarırsınız (artı S1 düzeltmeleri). O iki S2 değeri her biri ~10¹⁴'tür, her biri ±8×10⁶ yuvarlama gürültüsü taşır. Onların gerçek farkı — aslında istediğiniz pencereli nicelik — normalleştirmeden sonra, fiyatın kendisi mertebesinde bir WMA'ya karşılık gelir, ~3×10⁴. Yani aritmetik şudur:
Bu, en saf haliyle feci sadeleşmedir (catastrophic cancellation): her işlenenin yuvarlama hatası (±8×10⁶), kurtarmaya çalıştığınız cevaptan daha büyüktür. Sinyal, çıkarıldığı sayıların gürültü tabanından daha küçüktür. Birkaç basamak kaybetmeniz söz konusu değil — hepsini kaybedersiniz ve geri gelen şey, cumsum'ın birikmiş yuvarlamasının hakimiyetindedir.
Ölçülen sonuç, GPU_NOTES.md'den: 150 bin bar üzerinde fiyat ~30.000'de bu şekilde hesaplanan bir WMA için, fp64'e karşı maksimum bağıl hata ~211'e ulaşır. Yüzde 211 değil — 211×. Hesaplanan hareketli ortalama, gerçek olandan iki büyüklük mertebesi uzakta olabilir. Ve işte bunu bir hata değil de bir tuzak yapan kısım: tamamlanana kadar çalışır ve sonlu, makul sayılar döndürür. Sonsuza taşma yok, NaN yok, istisna yok. 211× yanlış olan bir hareketli ortalama hâlâ bir hareketli ortalama gibi görünür — pürüzsüzdür, sonludur, sadeleşmenin tesadüfen hafif olduğu barlarda kabaca doğru aralıktadır — bu yüzden güvenilir bir referansa karşı doğrudan bir karşılaştırma olmayan her akıl sağlığı kontrolünden geçer. Tam bir backtest, tam bir öz sermaye eğrisi, tam bir "optimal" parametre seti elde edersiniz; hepsi kurgu olan bir gösterge üzerine kurulu.
Çözüm daha fazla hassasiyet değil — farklı bir toplamdır

Refleks, hatayı gördüğünüz an, daha fazla hassasiyete uzanmaktır — fp64'te biriktirin veya telafili (Kahan) toplama kullanın. Metal üzerinde birincisi basitçe kullanılamaz. Ama ikisine de ihtiyacınız yok, çünkü sorun asla bit sayısı değildi. Sorun formülasyondu. Prefix-sum hilesi 10¹⁴-ölçekli ara değerler üretir ve sonra onları tekrar çıkararak azaltır; yarattığı büyüklükler algoritmanın bir yan ürünüdür, cevabın değil. Onları asla yaratmayan bir formülasyon seçin ve fp32 sorunsuz olur.
Bu formülasyon tanımın kendisidir: doğrudan pencereli konvolüsyon. İki global kümülatif toplam yerine, p uzunluğundaki doğrusal çekirdeği seri boyunca kaydırın ve yerinde toplayın. Her çıktı en fazla p ≈ 200 terimin toplamıdır ve her terim weight × price'tır; burada ağırlıklar toplamı 1 olacak şekilde normalleştirilmiştir — dolayısıyla her terim price / p mertebesindedir, her kısmi toplam fiyat ölçeği civarında (~3×10⁴) kalır ve hiçbir ara değer fp32 tavanının altı büyüklük mertebesi yakınına bile gelmez. Sadeleşecek hiçbir şey yoktur çünkü hiçbir şey hiç şişmedi.
MLX'te bu tek bir ilkeldir — mx.conv1d — ki bu tam olarak GPU'ların hızlı yapmak için tasarlandığı şeydir:
def _mx_wma_valid(x, period):
w = mx.arange(1, period + 1, dtype=mx.float32) / (period * (period + 1) / 2.0)
return mx.conv1d(x.reshape(1, -1, 1), w.reshape(1, period, 1), padding=0).reshape(-1)
Aynı WMA, prefix-sum sürümüyle ve CPU'nun fp64 vec_wma/nb_wma'sıyla matematiksel olarak özdeş. Ama şimdi ölçülen fp64'e karşı maksimum bağıl hata 8.2×10⁻⁷ — tam olarak ~1.2×10⁻⁷'lik fp32 gürültü tabanında, yedi anlamlı basamak uyum. Kağıt üzerinde daha yavaş görünen formülasyon (O(n) yerine O(n·p)), doğru olan tek formülasyondur ve — GPU'nun hem barlar hem pencereler boyunca aynı anda paralelleştirdiği yoğun bir konvolüsyon olduğu için — aynı zamanda göz kamaştırıcı derecede hızlıdır. Bağıl hatayı 211'den 8×10⁻⁷'ye, kaç bitle topladığımızı değil, nasıl topladığımızı değiştirerek düşürdük.
Bunu bu şekilde yapmaktan doğan iki pratik not. Birincisi, MLX bir conv1d üzerinden NaN'ları numpy'ın yaptığı gibi yaymaz, dolayısıyla ısınma bölgesi (ilk p−1 bar, burada bir pencereli ortalama tanımsızdır) GPU'da NaN ile işaretlenemez. Buna ihtiyacımız yok: her serinin geçerli başlangıcı analitik olarak bilinir, geçersiz önekler asla okunmayan sıfırlarla doldurulur ve NaN dolgusu sonradan CPU'da geri yüklenir — vektörleştirilmiş ve numba sürümleriyle bit-bit aynı geçerlilik maskesi. İkincisi, tüm tarama tek bir cand_close serisini paylaşır ve kombinasyonlar arasında pencereleri yoğun şekilde yeniden kullanır, dolayısıyla birçok çıktı kanalına sahip tek bir gruplu conv1d, taramanın ihtiyaç duyduğu her benzersiz WMA'yı tek bir GPU çağrısında hesaplar ve tek bir mx.eval() ile somutlaştırır.
İçine düşmediğinizi kanıtlamak: işlem sayısıyla parite
İşte son bölümün gündeme getirmesi gereken rahatsız edici soru: eğer 211× yanlış bir WMA hâlâ bir WMA gibi görünüyorsa, 8×10⁻⁷ sürümünün gerçekten doğru olduğunu, yalnızca daha ince bir şekilde yanlış olmadığını nasıl bilirsiniz? Bunu gözle tahmin edemezsiniz. Boru hattının aşağı akıştaki, ayrık bir kısmının açığa çıkardığı bir değişmeze ihtiyacınız var — ve bir backtest size mükemmel bir tane verir: işlemler.
Merdivendeki diğer GPU metotları (M0–M4) tamamen fp64'te çalışır, bu yüzden onları sıkı bir denklik assert'ine tabi tutarız — özdeş işlem sayıları, atol=1e-6 ile eşleşen PnL. fp32 GPU metodu (M5), tasarımı gereği bunu geçemez ve herkes için assert'i sessizce gevşeterek onu barındırmak, tam olarak bu serinin savaşmak için var olduğu türden bir dürüstsüzlük olurdu. Bu yüzden M5, kendi niceliksel parite raporunu alır, report_equiv_fp32, çıkarılan işlemlerini fp64 referansına karşı karşılaştırır.
Herhangi bir kalan uyuşmazlığın mekanizması tam olarak adlandırılmaya değer, çünkü bu feci sadeleşme değildir — beklediğiniz sıradan, minik fp32 yuvarlamasıdır. Strateji, iki Hull ortalamasının, h'ye karşı h3'ün kesişiminde tetiklenir. Fiyat ~30.000'deki bir gösterge üzerinde ~1×10⁻⁶'lık bir bağıl hata, ~0.03'lük mutlak bir salınımdır. Barların büyük çoğunluğunda iki eğri bundan daha uzaktadır ve kesişim belirsiz değildir. Ama bir sınırdaki barda — h − h3'ün kendisinin sıfırdan 0.03 içinde olduğu yerde — o salınım, karşılaştırmanın işaretini çevirerek bir kesişimi tek bir bar kaydırabilir, bir işlem ekleyebilir veya çıkarabilir.
Bu yüzden "farklılık gösteren kombinasyonların oranı" değersiz bir sağlık metriğidir ve ilk parite kontrolümüz bunu kullanarak kendini utandırdı. 150 bin barda her kombinasyonun binlerce kesişimi vardır, dolayısıyla neredeyse her kombinasyonda en az bir sınır barı görünür — 80 kombinasyonun 37'si "farklılık gösterdi", bu alarm verici gibi geliyor ve hiçbir şey ifade etmiyor. Önemli olan metrik ne kadar olduğudur:
- Tüm 80 kombinasyon boyunca PnL farkı: maks |Δ| = 1.843 yüzde puanı, maks bağıl = 1.25×10⁻²; çökme eşiği 5 y.p.
- Kombinasyon başına işlem-sayısı sapması: maks |Δn| = binlerce içinden 4 işlem, maks bağıl = 2.5×10⁻³; çökme eşiği %1.
- Toplamda: 479.016 işlemden 90 kaymış işlem — %0.019.
Yaklaşık yarım milyon işlemden doksanı, her biri bir fiyat tikinden daha küçük bir yuvarlama salınımıyla dürtülen bir sınır kesişimi, hiçbiri çökme eşiklerinin herhangi birinin yakınında değil. Bu, doğru olan bir fp32 metodunun imzasıdır — küçük, sınırlı, açıklanabilir uyuşmazlık — ve 211 bağıl hatasından tamamen farklı bir hayvandır. Eşikler, "eh, işte, sadece fp32" kılığına girmiş bozuk bir formülasyonu yakalamak için vardır; gerçek farklar onların bir büyüklük mertebesi altında gelir. İşlem sayısı, öz sermaye eğrisinin olmayı reddettiği kâhindir.
Kazanç ve GPU'nun yardımı kestiği yer
Doğruluk kurulduğunda, hız belirtilmeye değerdir — ve sonra dürüstçe nitelenmeye, çünkü GPU'nun avantajı boru hattı boyunca tekdüze değildir.
Saf WMA konvolüsyonlarında tek başına — tüm metodun hızlandırmak için var olduğu işlem — fp32 conv1d grubu tek iş parçacıklı numba'dan 55.9× daha hızlı çalışır, o 8.2×10⁻⁷ bağıl hatayla. Bu, temiz, elma-elmaya GPU-vs-CPU sayısıdır: aynı matematik, derlenmiş CPU kodunun bir iş parçacığına karşı Metal GPU.
Ama bir tarama yalnızca konvolüsyonlardan ibaret değildir. HMA/HMA3 matrisleri GPU'da hesaplandıktan sonra, işlemlerin hâlâ çıkarılması gerekir — her kombinasyonun kesişimleri üzerinde bir O(n) yürüyüşü — ve bunu CPU'da fp64'te yaparız, diğer metotların tam işlem semantiğini yeniden kullanarak, onları GPU'da yeniden uygulamak yerine. Uçtan-uca timed() rakamı her şeyi içerir: çekirdek ısınması hariç (numba'nın derlemesini hariç tutmakla simetrik), ama GPU→CPU transferi ve CPU işlem çıkarma dahil. 150 bin bar × 80 kombinasyon, üç denemenin en iyisi, M2 Max:
| Metot | Süre | Baz çizgisine göre hızlanma | komb/s |
|---|---|---|---|
| M0 pandas + Python loop* | 287.08s | 1.0× | 0.3 |
| M1 vectorized numpy | 3.14s | 91.5× | 25.5 |
| M2 numba (serial) | 2.02s | 142.3× | 39.7 |
| M3 multiprocess + vectorized | 0.50s | 570.2× | 158.9 |
| M4 multiprocess + numba (12 cores) | 0.33s | 882.5× | 245.9 |
| M5 MLX GPU (fp32) | 0.10s | 2796.0× | 779.2 |
*M0, tek biçimli 5 kombinasyonluk bir örnekten ekstrapole edilmiştir.
Tam-motorlu M5 779 komb/s yapar — pandas baz çizgisinin 2.796× üzerinde, seri numba'nın 19.6× üzerinde ve numba çalıştıran 12-çekirdekli CPU havuzunun tamamının (M4) 3.2× üzerinde. Bir GPU, makinenin sahip olduğu her CPU çekirdeğini üç katıyla yener.
Şimdi dürüst nitelendirme: uçtan-uca GPU avantajının (M2'ye göre 19.6×), yalnızca-konvolüsyon avantajından (numba'ya göre 55.9×) daha küçük olduğuna dikkat edin. O boşluk, Amdahl yasasının programa uygun şekilde gelmesidir. GPU konvolüsyonları o kadar tamamen yok eder ki, artık darboğaz olmaktan çıkarlar; geriye kalan — GPU'nun hiç hızlandırmadığı O(n) CPU işlem çıkarma — artık M5'in duvar saatine hakim olur. Bu, bu serinin hız-merdiveni ve IPC-vergisi parçalarının sürekli üzerine indiği aynı derstir: bir noktadan sonra, kazanç "hızlı kısmı daha hızlı yapmak" değil, orkestrasyondur — verinin nerede yaşadığı, hangi aşamanın artık seri olduğu, cihaz ile ana bilgisayar arasında taşımak için ne ödediğiniz. İşlem çıkarmayı özel bir Metal çekirdeğine iten varsayımsal bir M6'yı kovalamak, yalnızca küçülen CPU dilimini geri kazanırdı, ki bu yüzden onu inşa etmedik.
Genel ders: sessiz sayısal çöp varsayılandır
Hull ortalamalarından ve MLX'ten bir adım geri çekilin, çünkü tuzak bu tek göstergenin çok ötesine genelleşir.
GPU backtesting'in baştan çıkarıcı vaadi "tek büyük matris"tir: her parametre kombinasyonunu bir tensöre istifleyin, tüm taramayı bir avuç yoğun dizi işlemi olarak çalıştırın, donanımın onu yemesine izin verin. O vaat gerçektir — yukarıdaki hızlanmalar gerçektir. Ama altınızdaki sayısal rejimi sessizce değiştirir ve değişiklik kodda görünmezdir. CPU'da varsayılanlarınız sizi korudu: fp64, NaN yayılımı, fark etmeden 10¹⁴'e kadar koşabilen bir cumsum. Aynı dizi ifadesini Metal'e taşıyın ve sert bir 1.6×10⁷ tamsayı tavanıyla fp32'desiniz ve özdeş kod satırı — cumsum(j * price) — tamdan çöpe geçer. Sözdizimindeki hiçbir şey sizi uyarmaz. Derleyici memnun. Çıktı sonlu ve makul. fp32 gürültülü şekilde başarısız olmaz; kibarca, sayılarla başarısız olur.
Sizi gerçekten koruyan üç alışkanlık ucuzdur:
- Yalnızca girdilerinizin ve çıktılarınızın değil, ara değerlerinizin nerede yaşadığını bilin. Girdiler (fiyatlar ~10⁴) ve çıktılar (bir WMA ~10⁴) her ikisi de fp32'nin tam aralığının içinde rahatça yer alıyordu. Felaket tamamen, ne API'nin ne de tiplerin görünür kıldığı bir gizli ara değerdeydi (
S2~10¹⁴). Herhangi bir fp32 indirgemesine güvenmeden önce, en büyük kısmi toplamın neye ulaştığını sorun — ve ~10⁷'yi geçerse, formülasyonu değiştirin. - Büyüklükleri sınırlı tutan formülasyonları tercih edin. Prefix toplamları yerine doğrudan konvolüsyon; global taramalar yerine yerel pencereler; toplamadan sonra değil, toplamadan önce merkezleme/farklama. Önce-büyüt-sonra-sadeleştir, karşıt-desendir. Doğru algoritma, çoğunlukla kağıt üzerinde asimptotik olarak daha kötü görünen ama asla sadeleştirerek yok etmesi gereken bir nicelik üretmeyen algoritmadır.
- Ayrık bir değişmez aracılığıyla bir fp64 kâhinine karşı doğrulayın. Eğrileri karşılaştırmayın; kuantize edilmiş ve aşağı akıştaki bir şeyi karşılaştırın — işlem sayısı, kesişim sayısı, pozisyon-değişim olayları. Ayrık bir değişmez, sessiz bir 211× hatayı çığlık atan bir doğrulama başarısızlığına dönüştürür ve kabul edilebilir bir 8×10⁻⁷ hatayı küçük, sınırlı, açıklanabilir bir farka dönüştürür. Bu, ileriye-bakma önyargısı için tek-bar kaydırma testiyle aynı disiplindir: görünmez bir başarısızlığı görünür olana dönüştüren ucuz bir teşhis.
Bunların hiçbiri egzotik sayısal analiz değildir. Bu, yavaş, güvenilir bir tanesi onu onaylayana kadar hızlı bir backtest'e güvenmemenin sıradan hijyeni — dilin, hassasiyetin sessizce on iki büyüklük mertebesi düştüğü konusunda sizi uyarmayı bıraktığı tek yere genişletilmiş halidir.
Çıkarımlar

- Apple'ın GPU'sunun float64'ü yoktur — backtest'inizdeki her GPU sayısı fp32'dir. Tamsayılar yalnızca ~1.6×10⁷'ye kadar tam ve hassasiyet ~1.2×10⁻⁷'dir. Bayrak yok, yedek yok. Bir backtest'in çoğu buna dayanır; genellikle tam olarak bir işlem dayanmaz.
- Prefix-sum WMA tuzaktır.
cumsum(j · price), fp32'nin tam tavanının yedi mertebe ötesi olan ~10¹⁴'e tırmanır ve bir pencereyi kurtarmak, yuvarlama hatası (±8×10⁶) cevabı zaten cüceleştiren bu tür iki sayıyı çıkarmaya zorlar. Ölçülen maks bağıl hata: 211×. Asla çökmez — makul çöp döndürür. - Çözüm farklı bir toplamdır, daha fazla bit değil. Doğrudan bir pencereli konvolüsyon (
mx.conv1d) her kısmi toplamı fiyat ölçeği yakınında tutar, dolayısıyla fp32 yedi dürüst basamak tutar: 8.2×10⁻⁷ bağıl hata ve tek iş parçacıklı numba'dan 55.9× daha hızlı. Metal'de fp64 satın alamazsınız ve buna ihtiyacınız da yok. - Eğriyle değil, asla, ayrık bir değişmezle doğrulayın. İşlem-sayısı paritesi onu yakaladı: fp32 conv1d, fp64 ile 479.016 işlemden 90'ında (%0.019) uyuşmadı, hepsi sınır kesişimi, hepsi çökme eşiklerinin çok altında — doğru bir metodun imzası, 211× hatasından açıkça farklı. "Farklılık gösteren kombinasyonların oranı" bir tuzak metriktir; ne kadar olduğunu ölçün.
- Tam tarama 779 komb/s yapar — pandas baz çizgisinin 2.796× üzerinde, 12-çekirdekli CPU havuzunun tamamının 3.2× üzerinde — ama uçtan-uca kazanç (seri numba'nın 19.6× üzerinde), yalnızca-konvolüsyon kazancından (55.9×) daha küçüktür çünkü CPU işlem çıkarma artık darboğazdır. Bir noktadan sonra, hız aritmetik değil, orkestrasyondur.
GPU taşıması 2.796× daha hızlıydı ve ilk çalışan sürümünde tamamen yanlıştı — ve iki gerçeğin birbiriyle hiçbir ilgisi yoktu. Hız gerçekti. Çöp, fp32'nin tutamadığı ve hiçbir hata mesajının bahsetmeyeceği gizli bir 10¹⁴ ara değeriydi. Eğer bir backtest çarpıcı biçimde hızlanırsa ve sayılar hâlâ iyi görünürse, bu bir onay değildir. Metal'de, "iyi görünüyor", 211 bağıl hatanın tam olarak neye benzediğidir.
Bu, bu serinin tırmandığı merdivenin GPU basamağıdır: backtest-motoru hız merdiveni, çok-işlemenin IPC vergisi, sızıntıların ileriye-bakma taksonomisi ve "iyi"nin ne anlama geldiğine karar veren amaç-fonksiyonu tasarımı. Yanlış sayıyı hesaplamakta hızlıysa, hız değersizdir.
Yazarlar
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.