GPU дәлдік қақпаны: Apple Metal-дағы fp32 бэктест қалай үнсіз қоқыс қайтарады
"Иллюзиясыз бэктестер" сериясының бір мақаласы.
Біз параметрлік свип-бэктестімізді GPU-ға көшірдік, және ол 2,796 есе жылдамдады. Ол қайтарған сандар шындыққа ұқсас еді. Сонымен қатар олар, алғашқы жұмыс нұсқасында, нағыз қоқыс болатын — екі жүз есеге қателескен — әрі ештеңе құламады, ешбір ескерту шықпады, сырттай ештеңе бұрыс көрінбеді.
Apple Silicon-дағы GPU-бэктестингті CPU кодында жоқ түрде қауіпті ететін қақпан осы: Metal-да float64 жоқ. Apple GPU-да бэктестіңіз тиетін әрбір сан — сіз оны сұрадыңыз ба, жоқ па — 32 биттік float. Ал жылжымалы орташаны векторлаудың ең азғырғыш жолы — өнімділікті ойлайтын әрбір квант қолға алатын O(n) префикс-қосынды трюгі — баға масштабында fp32 көтере алмайтын формулировканың дәл өзі. Ол қате шығармайды. Толық жылдамдықпен жұмыс істеп, 211 есе қателескен жылжымалы орташаға құрылған, шындыққа ұқсас эквити қисығын қолыңызға ұстатады.
Ең қызығы — шешімнің өзі, өйткені ол "көбірек бит қолдану" емес (мүмкін емес) және "мұқиятырақ болу" да емес (аңғал код онсыз да мұқият). Шешім — дәл сол математикалық шаманы басқа қосынды арқылы есептеу — әрбір аралық мәнді fp32 дәл көрсете алатындай кіші ұстайтын тікелей терезелік конволюция. Дәл сол WMA, жеті маңызды таңбаға дейін дәл сол нәтиже, әрі CPU-дағы бір ағынды numba-дан 55.9 есе жылдам. Бұл мақала — сол оқиғаның анатомиясы: Metal неге сізге fp32-ні мәжбүрлейді, айқын формулировка дәл қай жерде асып кетеді, дұрысы неге аспайды, және жылдам нұсқа мен адал fp64 нұсқасының сәйкестігін біз — қисықтарға көз жүгіртіп емес, мәмілелерді санап — қалай дәлелдедік.
Мұндағы барлық сандар Apple M2 Max-та өлшенген: 150,000 бар × 80 параметр комбинациясы, үш жүгіртудің ең жақсысы, репозиторийдегі scripts/bench_param_sweep.py (M5 GPU әдісі, коммит 97eadaf), scripts/engine_multitf_gpu.py (04d71e8) және scripts/GPU_NOTES.md жобалық жазбалары бойынша.
Metal-да float64 жоқ

CPU-да Python/numpy бэктесті әдепкі бойынша қос дәлдікте жұмыс істейді. float64 сізге 52 биттік мантисса береді: бүтін сандар 2⁵³ ≈ 9×10¹⁵-ке дейін дәл көрсетіледі, ал салыстырмалы дәлдік шамамен 1.1×10⁻¹⁶. Бұл туралы ойланбайсыз да, өйткені баға масштабында — BTC жабылу бағасы 30,000 шамасында, жинақталған қосынды 10¹⁴-ке жетеді — fp64-тің қоры артығымен жетеді.
Apple GPU-ы ондай қорды ұсынбайды, өйткені ол float64-ті мүлдем ұсынбайды. Metal Shading Language-де double типі жоқ; MLX, PyTorch-MPS және Apple Silicon-ға бағытталған кез келген басқа фреймворк осыны мұра етеді. Оны қосатын жалауша да, баяу-бірақ-дұрыс резервтік жол да жоқ. Есептеуіңіз GPU-ға тисе, ол fp32-де орындалады:
- Мантисса: 23 бит (жасырын жетекші битпен 24). Бүтін сандар тек 2²⁴ = 16,777,216 ≈ 1.6×10⁷-ге дейін ғана дәл.
- Салыстырмалы дәлдік: ~1.2×10⁻⁷. Шамамен жеті маңызды ондық таңба, одан артық емес.
Барлық мәселе — осы 1.6×10⁷ төбесінде. Он алты миллион жомарт естіледі — бірақ бэктест одан әлдеқайда үлкен аралық шамаларды үнемі құрап отыратынын байқаған сәтте бәрі өзгереді: аралық мән 1.6×10⁷-ден асқан бойда fp32 тіпті қатар тұрған бүтін сандарды да ажырата алмай қалады, сізге керек бөлшек құрылым туралы айтпағанда. Дәлдік бірте-бірте нашарламайды; ол жардан құлайды, ал ол жар — деректеріңіз екі рет ойланбастан аттап өтетін санның тұсында тұр.
Қақпанның мәні — fp32 дерлік әрқашан жеткілікті болатынында. Бэктестің басым бөлігі — бағалар, кірістіліктер, PnL, Шарп — жеті таңба молынан жететін диапазонда жайбарақат өмір сүреді. Сондықтан аңғал порт жұмыс істейді, смоук-тесттен өтеді, ақылға қонымды нәтиже шығарады. Ақау бір ғана операцияға шоғырланған — аралық шама 10⁷-ден асып кететін жалғыз жерге — және дәл сол операцияны бәрі бірінші кезекте векторлайды.
Азғырғыш формулировка: WMA бір префикс-қосынды өтуінде
Біздің стратегия Hull жылжымалы орташаларына сүйенеді. HMA — бір-біріне қабаттастырылған үш салмақталған жылжымалы орташа; HMA3 нұсқасында олар төртеу. Ұзындығы p терезедегі сызықтық ядролы салмақталған жылжымалы орташа мынадай:
150 мың бар бойынша мыңдаған параметр комбинациясын свиптегенде, бүкіл шығын дәл осы WMA конволюцияларына кетеді. Сондықтан инстинкт — CPU-да дұрыс инстинкт — префикс-қосындылар арқылы әрбір WMA-ны O(n·p) орнына O(n) ету. Екі жинақталған қосындыны бір рет алдын ала есептейсіз,
содан кейін кез келген терезенің сызықтық салмақталған қосындысы S1 мен S2-нің бірнеше айырымы мен индекс жылжуына түйіседі. Терезе сайын цикл жоқ, терезе сайын редукция жоқ — екі cumsum өтуі, және WMA-лардың бүкіл матрицасы массив арифметикасынан өздігінен шығады. Ол керемет векторланады, GPU-дың параллель-скан примитивіне тамаша түседі, ал fp64-те — дәлме-дәл дұрыс.
Сонымен бірге бұл — fp32-де жасай алатын ең сорақы нәрсе, және оның себебі S2-нің ішінде жасырынып жатыр.
S2 = mx.cumsum(j * price) # j is the global bar index: 0, 1, 2, ... , n-1
Мәселе — j · price мүшесінде. j 150,000-ға дейін өсіп, price 30,000 шамасында болғанда, жалғыз соңғы мүшенің өзі 4.5×10⁹, ал S2 — осындай 150,000 мүшенің жинақталған қосындысы. Ол баға масштабында қалмайды. Ол fp32 санай алудан баяғыда қалған аймаққа өрлеп кетеді.
Қай жерде асып кетеді: қақпанның арифметикасы

Шамалардың реттерін қатар қоялық, өйткені бүкіл ақау дәл осы жерде өмір сүреді.
S2 = cumsum(j · price) шамамен price · n²/2 ≈ 30,000 · (150,000)²/2 ≈ 3×10¹⁴-ке жетеді. Дөңгелектеп ~10¹⁴ делік. Енді fp32-нің дәл-бүтін төбесін еске түсіріңіз: ~1.6×10⁷. Жинақталған қосынды fp32 дәл көрсете алатын соңғы бүтін саннан жеті шама ретіне асып түседі.
Нақты алғанда бұл нені білдіреді? 10¹⁴ маңайында көрсетілетін көршілес екі fp32 санының арасы — соңғы разрядтың бір бірлігі, ULP — шамамен 2²³ ≈ 8×10⁶. Демек S2 10¹⁴ аймағына шыққан соң, ол тек ±8 миллион дәлдікпен ғана белгілі. Ол сақтайтын әрбір мән ~8×10⁶-ның ең жақын еселігіне дейін дөңгелектелген.
Енді WMA-ны қалпына келтіру осымен не істейтінін қараңыз. Бір терезенің салмақталған қосындысын шығару үшін көршілес екі S2 мәнін бір-бірінен аласыз (S1 түзетулерімен қоса). Ол екі S2 мәнінің әрқайсысы ~10¹⁴, әрқайсысында ±8×10⁶ дөңгелектеу шуы бар. Ал олардың шынайы айырымы — сізге шын керек терезелік шама — нормалаудан кейін бағаның өз ретіндегі, ~3×10⁴ шамасындағы WMA-ға сәйкес келеді. Сонымен арифметика мынадай:
Бұл — ең таза күйіндегі апатты қысқару (catastrophic cancellation): әрбір операндтың дөңгелектеу қатесі (±8×10⁶) сіз қалпына келтірмек болған жауаптың өзінен үлкен. Сигнал өзі алынып отырған сандардың шу деңгейінен кіші. Бірнеше таңба жоғалтып қана қоймайсыз — бәрін жоғалтасыз, ал қайтып келгеннің үстінде cumsum-ның жинақталған дөңгелектеуі үстемдік етеді.
Өлшенген салдары, GPU_NOTES.md-ден: 150 мың барда, баға ~30,000 болғанда осылай есептелген WMA үшін fp64-ке қатысты максималды салыстырмалы қате ~211-ге жетеді. 211 пайыз емес — 211 есе. Есептелген жылжымалы орташа шынайысынан екі шама ретіне алшақ болуы мүмкін. Ал мұны багтан гөрі қақпан ететін жері мынау: ол аяғына дейін жұмыс істеп, ақырлы, шындыққа ұқсас сандар қайтарады. Шексіздікке асып кету жоқ, NaN жоқ, ешқандай exception жоқ. 211 есе қателескен жылжымалы орташа бәрібір жылжымалы орташаға ұқсайды — ол тегіс, ол ақырлы, қысқару жеңіл өткен барларда шамамен дұрыс аумақта жүреді — сондықтан сенімді эталонмен тікелей салыстыру емес кез келген парасаттылық тексерісінен сүрінбей өтеді. Қолыңызда толық бэктест, толық эквити қисығы, "оңтайлы" параметрлердің толық жиынтығы — бәрі де ойдан шығарылған индикаторға құрылған.
Шешім — көбірек дәлдік емес, басқа қосынды

Қатені көрген соң алғашқы рефлекс — көбірек дәлдікке ұмтылу: fp64-те аккумуляциялау немесе компенсацияланған (Кахан) қосындылауды қолдану. Metal-да біріншісі жай ғана жоқ. Бірақ екеуі де қажет емес, өйткені мәселе ешқашан бит санында болған жоқ. Мәселе формулировкада болды. Префикс-қосынды трюгі 10¹⁴ масштабты аралық шамаларды өзі жасап алып, соңынан оларды кері азайтады; ол тудыратын алып шамалар — жауаптың емес, алгоритмнің артефактісі. Оларды мүлдем тудырмайтын формулировканы таңдаңыз — сонда fp32 әбден жеткілікті.
Ол формулировка — анықтаманың дәл өзі: тікелей терезелік конволюция. Екі глобал жинақталған қосындының орнына ұзындығы p сызықтық ядроны қатар бойымен сырғытып, сол орнында қосасыз. Әрбір шығыс — ең көбі p ≈ 200 мүшенің қосындысы, әрбір мүше — weight × price, мұнда салмақтардың қосындысы 1-ге нормаланған — сөйтіп әрбір мүше price / p ретінде болады, әрбір ішінара қосынды баға масштабының (~3×10⁴) маңында қалады, және бірде-бір аралық шама fp32 төбесіне алты шама ретіне де жақындамайды. Қысқаратын ештеңе жоқ, өйткені ештеңе үрленбеген.
MLX-те бұл — бір примитив, mx.conv1d, яғни GPU-лар дәл жылдам орындауға жаралған операция:
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)
Дәл сол WMA, математикалық тұрғыда префикс-қосынды нұсқасына да, CPU-дағы fp64 vec_wma/nb_wma-ға да тепе-тең. Бірақ енді өлшенген fp64-ке қатысты максималды салыстырмалы қате — 8.2×10⁻⁷ — fp32-нің ~1.2×10⁻⁷ шу деңгейінің дәл өзінде, жеті маңызды таңбалық сәйкестік. Қағаз жүзінде баяуырақ көрінетін формулировка (O(n) орнына O(n·p)) — жалғыз дұрысы, әрі — GPU барлар мен терезелер бойынша бір мезгілде параллельдейтін тығыз конволюция болғандықтан — ол сұмдық жылдам да. Біз салыстырмалы қатені 211-ден 8×10⁻⁷-ге дейін неше битпен қосатынымызды емес, қалай қосатынымызды өзгерту арқылы түсірдік.
Осы жолмен жүргенде шығатын екі практикалық жайт бар. Біріншісі: MLX NaN-дарды numpy сияқты conv1d арқылы өткізбейді, сондықтан жылыну аймағын (терезелік орташа әлі анықталмаған алғашқы p−1 барды) GPU-да NaN-мен белгілеу мүмкін емес. Оның қажеті де жоқ: әрбір қатардың жарамды басы аналитикалық түрде белгілі, жарамсыз префикстер ешқашан оқылмайтын нөлдермен толтырылады, ал NaN-толтыру кейін CPU-да қалпына келтіріледі — векторланған және numba нұсқаларымен битке-бит бірдей жарамдылық маскасы. Екіншісі: бүкіл свип бір cand_close қатарын бөлісіп, терезелерді комбинациялар арасында қарқынды қайта қолданады, сондықтан көп шығыс арналы бір батч conv1d свипке керек әрбір бірегей WMA-ны бір GPU шақыруында есептеп, бір mx.eval()-мен материалдандырады.
Түспегеніңізді дәлелдеу: мәміле саны бойынша паритет
Алдыңғы бөлім тудыруға тиіс ыңғайсыз сұрақ мынау: 211 есе қателескен WMA бәрібір WMA-ға ұқсайтын болса, 8×10⁻⁷ нұсқасының шынымен дұрыс екенін, тек нәзігірек түрде қате емес екенін қайдан білесіз? Көзбен байқай алмайсыз. Сізге конвейердің төменгі ағысындағы дискретті бөлігі әшкерелейтін инвариант керек — ал бэктест дәл сондай мінсіз инвариантты ұсынады: мәмілелер.
Сатыдағы қалған әдістер (M0–M4) толықтай fp64-те жұмыс істейді, сондықтан оларға қатаң эквиваленттік assert қоямыз — мәміле сандары бірдей, PnL atol=1e-6 дәлдікпен сәйкес. fp32 GPU әдісі (M5) оны құрылымы бойынша өте алмайды, ал соған бола assert-ті бәріне жайлап босаңсыту — дәл осы серия күресу үшін бар арамдықтың нағыз өзі болар еді. Сондықтан M5-ке жеке сандық паритет есебі берілген — report_equiv_fp32 — оның шығарылған мәмілелерін fp64 эталонымен салыстырады.
Қалдық алшақтықтың механизмін дәл атап өткен жөн, өйткені бұл — апатты қысқару емес, күткендей қарапайым, мардымсыз fp32 дөңгелектеуі. Стратегия екі Hull орташасының, h пен h3-тің, қиылысында іске қосылады. Бағасы ~30,000 индикатордағы ~1×10⁻⁶ салыстырмалы қате — ~0.03 абсолюттік діріл. Барлардың басым көпшілігінде екі қисық бір-бірінен одан алшағырақ, қиылысу бір мағыналы. Бірақ шекаралық барда — h − h3 өзі нөлден 0.03 ішінде жатқанда — сол діріл салыстырудың таңбасын аударып, бір қиылысуды бір барға жылжытып, бір мәмілені қосуы не алып тастауы мүмкін.
"Айырмашылығы бар комбинациялардың үлесі" неліктен түкке тұрғысыз денсаулық метрикасы екені осыдан көрінеді — біздің алғашқы паритет тексерісі дәл соны қолданып ұятқа қалды. 150 мың барда әрбір комбинацияда мыңдаған қиылысу бар, сондықтан кем дегенде бір шекаралық бар комбинациялардың дерлік әрқайсысында кездеседі — 80 комбинацияның 37-сі "өзгеше" шықты, бұл үрейлі естіледі әрі ештеңені білдірмейді. Маңызды метрика — қаншалықты өзгеше:
- Барлық 80 комбинация бойынша PnL айырмасы: max |Δ| = 1.843 пайыздық тармақ, максималды салыстырмалы мән = 1.25×10⁻²; құлату шегі — 5 п.т.
- Комбинация сайынғы мәміле санының ығысуы: max |Δn| = 4 мәміле мыңдаған мәміленің ішінен, максималды салыстырмалы мән = 2.5×10⁻³; құлату шегі — 1%.
- Жиынтығында: 479,016 мәміленің 90-ы ығысқан — 0.019%.
Жарты миллионға жуық мәміленің тоқсаны ғана, әрқайсысы — өлшемі бір баға тигінен де кіші дөңгелектеу дірілі жылжытқан шекаралық қиылысу, және бірде-біреуі құлату шектеріне жуықтамайды да. Бұл — дұрыс fp32 әдісінің қолтаңбасы: кіші, шектелген, түсіндірілетін алшақтық — және ол 211 салыстырмалы қатеден мүлдем басқа жаратылыс. Шектер "е, бұл жай fp32 ғой" деген желеуге жасырынған бүлінген формулировканы ұстау үшін бар; нақты айырмалар олардан бір шама реті төмен келеді. Эквити қисығы бола алмаған оракул — мәміле саны болды.
Ұтыс, және GPU көмегінің таусылар жері
Дұрыстық анықталған соң, жылдамдықты да айту керек — сосын оны адал түрде нақтылау керек, өйткені GPU артықшылығы конвейер бойында біркелкі емес.
Оқшауланған таза WMA конволюцияларында — бүкіл әдіс тездету үшін өмір сүретін операцияда — fp32 conv1d батчы бір ағынды numba-дан 55.9 есе жылдам, сол 8.2×10⁻⁷ салыстырмалы қатемен. Бұл — таза, алма мен алманы салыстырған GPU-мен-CPU саны: бірдей математика, компиляцияланған CPU кодының бір ағыны Metal GPU-ға қарсы.
Бірақ свип тек конволюциялардан тұрмайды. HMA/HMA3 матрицалары GPU-да есептелген соң, мәмілелерді әлі шығарып алу керек — әрбір комбинацияның қиылысулары бойынша O(n) жүріс — оны біз CPU-да fp64-те жасаймыз, мәміле семантикасын GPU-да қайта жазудың орнына басқа әдістердің дәл сол логикасын қайта қолданамыз. Соңынан-соңына дейінгі timed() көрсеткіші бәрін қамтиды: ядроның жылынуы шығарылған (numba компиляциясын шығарумен симметриялы), бірақ GPU→CPU тасымалы мен CPU-дағы мәміле шығару кіреді. 150 мың бар × 80 комбинация, үш жүгіртудің ең жақсысы, M2 Max:
| Әдіс | Wall-уақыт | Базалыққа қатысты үдеу | combos/s |
|---|---|---|---|
| M0 pandas + Python циклі* | 287.08s | 1.0× | 0.3 |
| M1 векторланған numpy | 3.14s | 91.5× | 25.5 |
| M2 numba (тізбекті) | 2.02s | 142.3× | 39.7 |
| M3 мультипроцесс + векторланған | 0.50s | 570.2× | 158.9 |
| M4 мультипроцесс + numba (12 ядро) | 0.33s | 882.5× | 245.9 |
| M5 MLX GPU (fp32) | 0.10s | 2796.0× | 779.2 |
*M0 біркелкі 5-комбинациялық іріктеуден экстраполяцияланған.
Толық движоктағы M5 779 combos/s береді — pandas базалығынан 2,796 есе, тізбекті numba-дан 19.6 есе және numba жүгіртіп отырған 12 ядролық CPU пулының өзінен (M4) 3.2 есе жылдам. Бір GPU машинадағы барлық CPU ядросын қоса алғанда үш есе басып озады.
Енді адал ескерту: соңынан-соңына дейінгі GPU артықшылығы (M2-ден 19.6 есе) тек конволюциялық артықшылықтан (numba-дан 55.9 есе) кіші екенін байқаңыз. Ол алшақтық — кестесімен дәл келген Амдал заңы. GPU конволюцияларды соншалық жойқын жеңгені сонша, олар тар жол болудан қалады; қалғаны — GPU мүлдем тездетпеген O(n) CPU мәміле шығаруы — енді M5 уақытының басым бөлігін алады. Бұл — осы сериядағы жылдамдық сатысы мен IPC салығы мақалалары қайта-қайта жеткізетін сабақ: белгілі бір шектен кейін ұтыс "жылдам бөлікті одан әрі жылдамдату" емес, оркестрация — деректер қайда жатыр, қай кезең енді тізбекті, құрылғы мен хост арасындағы тасымалға не төлеп отырсыз. Мәміле шығаруды арнайы Metal ядросына көшіретін гипотетикалық M6-ны қуалау тарылып бара жатқан CPU үлесін ғана қайтарар еді — сондықтан біз оны жасаған жоқпыз.
Жалпы сабақ: үнсіз сандық қоқыс — әдепкі күй
Hull орташалары мен MLX-тен бір қадам шегінейік, өйткені қақпан осы бір индикатордан әлдеқайда әріге жайылады.
GPU-бэктестингтің азғырғыш уәдесі — "бір үлкен матрица": әрбір параметр комбинациясын тензорға тізіп, бүкіл свипті бірнеше тығыз массив операциясы ретінде жүргізіп, темірге жегізу. Ол уәде шын — жоғарыдағы үдеулер шын. Бірақ ол астыңғы сандық режимді елеусіз өзгертеді, әрі бұл өзгеріс кодта көрінбейді. CPU-да сізді әдепкі баптаулар қорғады: fp64, NaN тарату, 10¹⁴-ке дейін еш байқамай жүгіре алатын cumsum. Дәл сол массив өрнегін Metal-ға көшіріңіз — сіз енді 1.6×10⁷ қатаң бүтін төбесі бар fp32-десіз, және кодтың дәлме-дәл сол жолы — cumsum(j * price) — дәлден қоқысқа айналады. Синтаксисте ешқандай ескерту жоқ. Компилятор риза. Шығыс ақырлы да шындыққа ұқсас. fp32 айқайлап құламайды; ол сыпайы түрде, сандармен құлайды.
Сізді шын қорғайтын үш әдет арзан:
- Кіріс пен шығысты ғана емес, аралық шамаларыңыздың қайда өмір сүретінін біліңіз. Кірістер (бағалар ~10⁴) де, шығыс (WMA ~10⁴) та fp32-нің дәл диапазонының ішінде еркін сыятын. Апат түгелімен жасырын аралық шамада (
S2~10¹⁴) болды — оны API да, типтер де көрінетін етпеді. Кез келген fp32 редукциясына сенер алдында ең үлкен ішінара қосынды неге жететінін сұраңыз — егер ол ~10⁷-ден асса, формулировканы өзгертіңіз. - Шамаларды шектеулі ұстайтын формулировкаларды таңдаңыз. Префикс-қосындының орнына тікелей конволюция; глобал сканның орнына локал терезелер; қосындыдан кейін емес, бұрын центрлеу/айырымдау. Үлкейтіп-алып-қысқарту — анти-паттерн. Дұрыс алгоритм көбіне қағаз жүзінде асимптотикасы нашарырақ көрінетін, бірақ кейін жоюға тура келетін шаманы ешқашан тудырмайтын алгоритм.
- fp64 оракулына қарсы дискретті инвариант арқылы валидациялаңыз. Қисықтарды салыстырмаңыз; квантталған әрі төменгі ағыстағы нәрсені салыстырыңыз — мәміле саны, қиылысу саны, позиция ауысу оқиғалары. Дискретті инвариант үнсіз 211 еселік қатені айқайлаған assertion қатесіне айналдырады, ал қолайлы 8×10⁻⁷ қатені кіші, шектелген, түсіндірілетін айырмаға айналдырады. Бұл — look-ahead bias-ты ұстайтын бір барлық ығыстыру тестіндегі тәртіптің дәл өзі: көрінбейтін ақауды көрінетінге айналдыратын арзан диагностика.
Мұның ешқайсысы экзотикалық сандық талдау емес. Бұл — жылдам бэктестке баяу, сенімді бэктест растамайынша сенбеу деген қарапайым тазалық ережесі, дәлдік он екі шама ретіне үнсіз түсіп кеткенін тіл ескертуден қалатын жалғыз жерге дейін созылған түрі ғана.
Қорытындылар

- Apple GPU-да float64 жоқ — бэктестіңіздегі әрбір GPU саны fp32. Бүтін сандар тек ~1.6×10⁷-ге дейін дәл, дәлдік ~1.2×10⁻⁷. Жалауша жоқ, резервтік жол жоқ. Бэктестің көбі бұған шыдайды; әдетте дәл бір операция шыдамайды.
- Префикс-қосынды WMA — нағыз қақпан.
cumsum(j · price)~10¹⁴-ке өрлейді — fp32-нің дәл төбесінен жеті шама ретіне асып — ал терезені қалпына келтіру дөңгелектеу қатесі (±8×10⁶) жауаптың өзін баяғыда басып озған екі осындай санды азайтуға мәжбүрлейді. Өлшенген максималды салыстырмалы қате: 211 есе. Ол ешқашан құламайды — шындыққа ұқсас қоқыс қайтарады. - Шешім — көбірек бит емес, басқа қосынды. Тікелей терезелік конволюция (
mx.conv1d) әрбір ішінара қосындыны баға масштабының маңында ұстайды, сөйтіп fp32 адал жеті таңбаны сақтайды: 8.2×10⁻⁷ салыстырмалы қате және бір ағынды numba-дан 55.9 есе жылдам. Metal-да fp64-ті сатып ала алмайсыз — және оның қажеті де жоқ. - Ешқашан қисықпен емес, дискретті инвариантпен тексеріңіз. Мәміле саны паритеті ұстап берді: fp32 conv1d fp64-пен 479,016 мәміленің 90-ында (0.019%) келіспеді — бәрі шекаралық қиылысулар, бәрі құлату шектерінен әлдеқайда төмен — дұрыс әдістің қолтаңбасы, 211 еселік қатеден айқын өзгеше. "Айырмашылығы бар комбинациялар үлесі" — алдамшы метрика; қаншалықты екенін өлшеңіз.
- Толық свип 779 combos/s береді — pandas базалығынан 2,796 есе, 12 ядролық CPU пулының өзінен 3.2 есе — бірақ соңынан-соңына дейінгі ұтыс (тізбекті numba-дан 19.6 есе) тек конволюциялық ұтыстан (55.9 есе) кіші, өйткені тар жол енді CPU-дағы мәміле шығаруында. Белгілі бір шектен кейін жылдамдық — арифметика емес, оркестрация.
GPU порты 2,796 есе жылдам болды және, алғашқы жұмыс нұсқасында, мүлдем қате болды — ал бұл екі фактінің бір-біріне еш қатысы жоқ еді. Жылдамдық шын болатын. Қоқыс — fp32 сыйғыза алмаған және ешбір қате хабарламасы атамайтын жасырын 10¹⁴ аралық шамасы еді. Егер бэктест күрт жылдамдап, сандар бұрынғыша дұрыс көрінсе — бұл растау емес. Metal-да "дұрыс көрінеді" деген — 211 салыстырмалы қатенің дәл өз кейпі.
Бұл — осы серия өрлеп келе жатқан сатының GPU баспалдағы: бэктест-движоктың жылдамдық сатысы, мультипроцессингтің IPC салығы, ағып кетулердің look-ahead таксономиясы және "жақсы" дегеннің өзі нені білдіретінін шешетін мақсат функциясының дизайны. Егер жылдамдық қате санды тез есептеуге жұмсалса, ол түкке тұрмайды.
Authors
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.