เมื่อ GPU คุ้มค่าจริง: Roofline ของการ Sweep พารามิเตอร์ ที่ตัวเลขพาดหัว 167x แท้จริงคือ 27x อัลกอริทึม คูณ 6.2x ฮาร์ดแวร์
ส่วนหนึ่งของซีรีส์ "Backtests Without Illusions"
บันไดความเร็ว จบลงด้วยข้อสรุปที่จงใจให้รู้สึกไม่จบสมบูรณ์ เราพา parameter sweep ขนาด 80 combo จาก 69.9 วินาทีของ pandas ลงมาเหลือ 0.23 วินาทีของ parallel numba บน CPU แล็ปท็อป — วัดได้จริง 298x — แล้วก็โต้แย้งว่า GPU ไม่ใช่ ขั้นบันไดที่ขาดหายไป ช่องความคิดเห็นไม่ยอมรับเรื่องนี้แบบเงียบๆ และก็ถูกต้องแล้วที่ไม่ยอม "ไม่ใช่ขั้นที่ขาดหายไป" เป็นข้อกล่าวอ้างเกี่ยวกับ workload เดียว ที่ขนาดเดียว มันไม่ใช่กฎธรรมชาติ บทความนี้จึงทำในสิ่งที่ซื่อสัตย์ นั่นคือเอา GPU ขึ้นแท่นทดสอบ
ผลลัพธ์ไม่ใช่คำตัดสิน แต่เป็น เส้นโค้ง ความได้เปรียบของ GPU เหนือ CPU ไม่ใช่ตัวเลขเดียวที่พิมพ์ลงสไลด์ได้ มันเป็นฟังก์ชันของปริมาณงานที่คุณส่งให้มันต่อการเรียกหนึ่งครั้ง บนการ precompute indicator แบบ multi-timeframe ของเรา ความเร็วของ GPU เทียบกับ CPU engine วิ่งตั้งแต่ 54.5x เมื่อเราขอ combination พารามิเตอร์ทีละหนึ่ง ไปจนถึง 359.6x เมื่อเราขอ 61 พร้อมกัน เดียวกันคือ kernel เดียวกันคือข้อมูล เดียวกันคือฮาร์ดแวร์ — สิ่งเดียวที่เปลี่ยนคือ batch benchmark ที่รายงานตัวเลขใดตัวเลขหนึ่งในสองตัวนี้แล้วซ่อนอีกตัวไว้ ไม่ได้กำลังวัด GPU มันกำลังเลือกพาดหัวข่าว
และแม้แต่ 359.6x ก็ไม่ใช่สิ่งที่มันดูเหมือน ลองแกะออกดู แล้วพาดหัวขนาดใหญ่ของ multi-timeframe ที่ 167x จะแยกออกเป็น 27x ของอัลกอริทึม — การเขียนใหม่ที่ทำให้ CPU เร็วขึ้น 27x ด้วยเช่นกัน — คูณกับ 6.2x ของฮาร์ดแวร์จริง GPU ไม่ได้เป็นผู้สร้าง 27x นั้น คณิตศาสตร์ต่างหากที่ทำ บทความนี้ว่าด้วยการแยกสองสิ่งนี้ออกจากกัน เพราะการเอามันมาปนกันคือวิธีที่การ์ดจอราคา 2,000 ดอลลาร์ถูกขายให้แก้ปัญหาที่ git commit แก้ได้ฟรีๆ
ที่มาของข้อมูล: ตัวเลขทั้งหมดด้านล่างวัดบน Apple M2 Max, fp32 บน Metal GPU ผ่าน MLX เทียบกับ fp64 numba บน CPU สิบสอง thread จาก script benchmark engine_multitf_gpu.py และ bench_param_sweep.py ของเรา แต่ละอันถูกคุมด้วย equivalence check ที่ยืนยันว่า path ของ GPU และ CPU ให้ trade เดียวกัน ไม่มีเปเปอร์ประกอบสาธารณะสำหรับบทความนี้ — script คือบันทึกหลักฐาน
คำถามคือ Roofline ไม่ใช่ Benchmark

เหตุผลที่คำถาม "GPU เร็วกว่าแค่ไหน" ไม่มีคำตอบเดียวคือ roofline model (Williams, Waterman & Patterson, 2009) ประสิทธิภาพที่ processor ทำได้จริงถูกจำกัดด้วยเพดานสองอันพร้อมกัน: เพดานราบที่กำหนดโดย peak compute (FLOP/s) และเพดานลาดเอียงที่กำหนดโดย memory bandwidth คูณ arithmetic intensity — จำนวนการดำเนินการที่มีประโยชน์ต่อไบต์ที่ถูกเคลื่อนย้าย งานที่ราคาถูกต่อไบต์อยู่ใต้เพดานลาดเอียงและถูกจำกัดโดย bandwidth งานที่มีความหนาแน่นต่อไบต์สูงจะไปถึงเพดานราบและถูกจำกัดโดย compute เพดานราบของ GPU สูงตระหง่านเหนือของ CPU มาก แต่เพดานลาดเอียงของมัน และที่สำคัญกว่านั้นคือ ต้นทุนคงที่ต่อการเรียก ของมัน ไม่ได้ลดขนาดลงได้อย่างสง่างามเท่ากัน
สำหรับ parameter sweep มีเทอมที่สามครอบงำด้านซ้ายของกราฟ นั่นคือ overhead จากการเปิดใช้งานและการถ่ายโอนข้อมูล ทุกการเรียก GPU ต้องจ่ายราคาที่ค่อนข้างคงที่ — dispatch kernel เคลื่อนย้าย input ข้ามขอบเขตหน่วยความจำ (แบบ unified บน Apple Silicon แต่ก็ไม่ได้ฟรี) เคลื่อนย้ายผลลัพธ์กลับ ให้จำลอง wall time สำหรับ batch ของ combination เป็น
โดยที่ คือ overhead คงที่นั้น คือต้นทุนส่วนเพิ่มต่อ combo ของ GPU และ คือของ CPU speedup จึงเป็น
สมการเดียวนี้อธิบายบทความทั้งบทความ ที่ เล็ก ในตัวส่วนกดอัตราส่วนลง — คุณจ่ายเพื่อปลุก GPU ขึ้นมาแต่แทบไม่ได้ใช้มันเลย เมื่อ โตขึ้น คุณ amortize ข้าม combo จำนวนมากขึ้น และ speedup ไต่ขึ้นเข้าใกล้ asymptote ของมันคือ ซึ่งเป็นอัตราส่วนฮาร์ดแวร์ ที่แท้จริง จุดกึ่งกลางอยู่ที่ ซึ่งเป็น "ridge point" ในพื้นที่ batch ที่บอกว่า sweep ของคุณต้องกว้างแค่ไหนก่อนที่ GPU จะได้ครึ่งหนึ่งของศักยภาพที่มันมี sweep ขนาดไม่กี่สิบ combo อยู่ไกลไปทางซ้ายของ ridge นั้นมาก sweep ขนาดหลายพัน combo อยู่บนส่วนราบที่ GPU ในที่สุดก็ได้เพดานของมันคุ้มค่า
ดังนั้นคำถามที่ถูกต้องจึงไม่เคยเป็น "GPU เร็วกว่าไหม" แต่เป็น "sweep ของฉันอยู่ทางขวาของ ridge หรือเปล่า และงานต่อ combo ของฉันถูกจำกัดโดย compute มากพอที่จะไปถึงเพดานราบเมื่อไปถึงจุดนั้นหรือไม่" ทั้งสองข้อต้องเป็นจริงพร้อมกัน ส่วนที่เหลือของบทความนี้วัดว่าเกณฑ์เหล่านั้นตกอยู่ตรงไหนจริงๆ
คำตัดสินของ Single-Timeframe: GPU ชนะแบบเฉียดฉิว

เริ่มด้วย workload ที่บันไดความเร็วใช้: sweep HMA/HMA3 แบบ single-timeframe จำนวน 80 combination บน 150,000 บาร์ เราเพิ่มขั้นที่หกให้บันไดนั้น — M5, indicator บน Apple GPU ผ่าน MLX โดยยังคงสกัด trade บน CPU เหมือนเดิม วอร์มอัพแล้ว best of three ผ่าน equivalence gate สีเขียว:
| Method | เวลาจริง | เทียบ pandas | Combos/s |
|---|---|---|---|
| M0 pandas + loop | 287.08s | 1.0x | 0.3 |
| M2 numba (1 core) | 2.02s | 142x | 39.7 |
| M4 mp + numba (12 cores) | 0.33s | 883x | 245.9 |
| M5 MLX GPU (fp32) | 0.10s | 2796x | 779.2 |
เทียบกับ serial baseline แบบไร้เดียงสา GPU ดูยิ่งใหญ่ — 2,796x แต่นั่นคือการเปรียบเทียบที่คนซื่อสัตย์ไม่ควรทำ เพราะมันเอา GPU implementation ที่ดีไปเทียบกับ CPU implementation ที่แย่ที่สุด เอา GPU ไปเทียบกับ CPU ที่คุณจะใช้งานจริง — kernel เดียวกันบนสิบสอง core ทั้งหมด, M4 — แล้วชัยชนะจะยุบลงเหลือ 3.2x ที่สุขุมกว่ามาก (779 เทียบกับ 246 combo ต่อวินาที) การ์ดจอทั้งใบ ที่รัน sweep ทั้งหมด ชนะ pool CPU สิบสอง core ด้วยปัจจัยสาม
3.2 ไม่ใช่ศูนย์ แต่ก็ไม่ใช่เหตุผลที่ใครจะซื้อ GPU และนี่คือสิ่งที่ roofline ทำนายไว้พอดีสำหรับ sweep ที่แคบขนาดนี้: 80 combo อยู่ทางซ้ายของ ridge overhead คงที่จากการเปิดและถ่ายโอนข้อมูล ยังคงเป็นสัดส่วนที่มีนัยสำคัญของงาน 0.10 วินาที เราจึงไม่มีวันไปถึง asymptote เลย ที่แย่กว่านั้นคือ ส่วนหนึ่งของต้นทุนต่อ combo คือ O(n) trade-extraction pass ซึ่งเราจงใจปล่อยไว้บน CPU — เทอมที่ GPU ไม่สามารถเร่งได้เลย (จะอธิบายว่าทำไมในหัวข้อถัดไป) สำหรับ research loop แบบ single-timeframe ขนาดนี้ คำตัดสินดั้งเดิมของบันไดความเร็วยังคงยืนหยัด: GPU ไม่ใช่ขั้นบันไดที่ขาดหายไป parallel numba พาคุณไปถึง 0.23–0.33 วินาทีอยู่แล้ว และการลดมันลงเหลือ 0.10 ไม่ใช่สิ่งที่ปลดล็อกนักวิจัย การจัดออเคสตราของ sweep ต่างหากที่ทำ
คำที่น่าสนใจในคำตัดสินนั้นคือ ขนาดนี้ เลื่อนไปทางขวาตามแกน batch แล้วเรื่องราวจะเปลี่ยนไป
ต้นทุนอยู่ที่ไหนจริงๆ
ก่อนจะขยาย batch ให้ดูก่อนว่าเรากำลังจ่ายเงินให้อะไรจริงๆ เพราะ roofline จะให้รางวัลก็ต่อเมื่อส่วนที่แพงถูกจำกัดโดย compute โปรไฟล์ sweep แล้วเกือบทั้งหมดคือสิ่งเดียวกัน: weighted-moving-average convolution HMA คือ WMA สามตัว HMA3 คือสี่ตัว ทุก combination รันมันซ้ำบนทั้งซีรีส์ การสกัด trade — เดินผ่าน indicator array สองตัว หาจุดกลับเครื่องหมายของ hma - hma3 บันทึก fill — คือ pass O(n) ราคาถูกครั้งเดียว sweep นี้คืองาน convolution ที่สวมชุดกลยุทธ์การเทรด
การแบ่งนั้นตรงกับ regime ทั้งสองของ roofline พอดี:
- Convolution ถูกจำกัดโดย compute ราคาแต่ละราคาถูกอ่านเข้าไปใน window sum ที่ซ้อนทับกันจำนวนมาก ดังนั้น arithmetic intensity — จำนวนการดำเนินการต่อไบต์ที่เคลื่อนย้าย — จึงสูง งานนี้เอื้อมไปหาเพดาน compute แบบราบ และเพดานราบของ GPU คือตัวที่สูงตระหง่าน ยิ่งไปกว่านั้น window ยังซ้อนทับข้าม combination ด้วย: WMA ความยาว 40 มีประโยชน์กับ combo หลายสิบตัว ดังนั้นการ precompute แบบ batch จึงแชร์มันครั้งเดียวแทนที่จะคำนวณใหม่ทุก combo การทำ batch ไม่ได้แค่ amortize overhead จากการเปิดใช้งาน แต่ยกระดับ arithmetic intensity ด้วยการใช้ load ซ้ำ นี่คือส่วนที่ควรอยู่บน GPU
- การสกัด trade ถูกจำกัดโดย bandwidth และมีสาขาเยอะ pass แบบ sequential ครั้งเดียว, branch ที่ขึ้นกับข้อมูลทุกครั้งที่เกิด crossing, แทบไม่มีการใช้ซ้ำเลย arithmetic intensity ของมันอยู่ใกล้พื้น และ control flow ของมันเป็นปฏิปักษ์กับอุปกรณ์ SIMD การผลักมันไปที่ GPU จะได้กำไรน้อยแต่เสียเยอะ มันจึงอยู่บน CPU ต่อไป ซึ่งหมายความว่ามันคือ ส่วนหางแบบ serial ของ Amdahl's law — พื้นคงที่ที่ speedup ของ GPU ไม่มีวันเจาะทะลุได้ และเป็นส่วนหนึ่งของเหตุผลที่ตัวเลข single-TF อิ่มตัวที่ 3.2x
มีบทเรียนที่สองที่คมชัดกว่า ซ่อนอยู่ใน kernel เวอร์ชัน multi-timeframe และนี่คือที่มาของ 27x ที่เราสัญญาไว้ตลอดว่าจะอธิบาย engine multi-TF จัดเรียง HMA ของ timeframe ที่สูงกว่าให้ตรงกับ index 1-นาทีฐาน โดยไม่มี look-ahead เขียนแบบตรงไปตรงมา นั่นคืองาน O(length^1.5) ต่อบาร์ — คำนวณ moving average ของ timeframe ที่สูงกว่าใหม่ทุกบาร์ฐาน แต่ HMA ที่จัดเรียงแล้วนั้นเป็นเชิงเส้นใน buffer สั้นๆ ของแท่งเทียนไม่กี่แท่งล่าสุดของ timeframe ที่สูงกว่าที่ปิดแล้ว บวกกับราคาปิดที่กำลังวิ่งอยู่ ดังนั้นการคำนวณต่อบาร์ทั้งหมดจึงยุบเหลือ weight vector คงที่ตัวเดียว: conv1d หนึ่งครั้งบนซีรีส์ของ แท่งเทียนที่ปิดแล้ว ตามด้วย gather แบบ O(n) การดำเนินการซ้ำซ้อนหลายร้อยล้านครั้งกลายเป็น convolution บนซีรีส์ที่สั้นกว่ามาก
การยุบตัวนั้นเป็นชัยชนะเชิงอัลกอริทึม ไม่ใช่ชัยชนะเชิงฮาร์ดแวร์ มันคือสูตรที่ดีกว่า มันรันบน GPU ได้ และมันก็รันบน CPU ได้ดีพอกัน — np.correlate บวก gather ใน fp64 จำไว้ให้แม่น: ปัจจัยเดียวที่ใหญ่ที่สุดในพาดหัว multi-TF คือการเขียนใหม่ที่ใช้ได้กับเครื่องที่ไม่มี GPU เลยด้วยซ้ำ เมื่อเราแยกส่วน 167x ในที่สุด นี่แหละคือ 27x นั้น
ความได้เปรียบเพิ่มขึ้นตามขนาด Batch

ตอนนี้มาถึงการวัดที่ roofline บอกให้เราทำ เอาแกนที่แพงที่สุด — การ precompute HMA ของ timeframe สูงกว่าที่จัดเรียงแล้วบนซีรีส์ฐาน 1-นาที ซึ่งเป็นสตรีมแท่งเทียนที่ยาวที่สุดที่เรามี — แล้วป้อนให้ GPU ด้วยจำนวน length combination ที่เพิ่มขึ้นต่อการเรียก baseline ของ CPU ที่นี่คือ engine production ที่ซื่อสัตย์: numba กับ prange ข้ามสิบสอง core ทั้งหมด สำหรับแต่ละ batch เราวัดทั้งคู่แล้วเอาอัตราส่วน
| Batch (combo/call) | Speedup ของ GPU เทียบ CPU engine 12-core |
|---|---|
| 1 | 54.5x |
| 2 | 102.5x |
| 4 | 129.5x |
| 8 | 187.4x |
| 16 | 267.4x |
| 32 | 245.0x |
| 61 | 359.6x |
นี่คือ ที่วาดจากการวัดจริง ที่ GPU นำอยู่แล้ว 54.5x — เพราะการเปรียบเทียบนี้เทียบกับ engine แบบต่อบาร์แบบไร้เดียงสา ดังนั้นการยุบตัวเชิงอัลกอริทึมจึงถูกฝังไว้แล้วแม้ที่ combo เดียว — แต่มันยังห่างไกลจากเพดานของมันมาก: overhead คงที่ยังครอบงำการเรียกแบบ combo เดียวอยู่ เพิ่ม batch เป็นสองเท่าและ speedup เกือบเพิ่มเป็นสองเท่าที่ 102.5x ที่ มันคือ 267.4x ที่ มันคือ 359.6x และยังเห็นได้ชัดว่ากำลังไต่ขึ้นต่อไป ความได้เปรียบเพิ่มขึ้นตามขนาดของปัญหา นั่นคือประโยคที่สำคัญที่สุดเพียงประโยคเดียวเกี่ยวกับ GPU และ parameter sweep และมันตรงกันข้ามกับวิธีที่ speedup ของ GPU มักถูกอ้างถึงโดยสิ้นเชิง — ราวกับว่ามันเป็นคุณสมบัติคงที่ของชิป
มีข้อสังเกตด้านความซื่อสัตย์สองข้อ เพราะนี่คือซีรีส์ Backtests Without Illusions และตารางแบบ monotone เรียบร้อยสมควรได้รับความสงสัยเสมอ
ข้อแรก จุดตก: อ่านได้ 245.0x ซึ่งต่ำกว่า 267.4x ที่ นั่นไม่ใช่ noise ที่ควรกวาดไว้ใต้พรม — มันคือ artifact ของขอบเขต chunk conv1d ของเราบรรจุ 32 length ลงใน chunk ของ kernel หนึ่งอัน ดังนั้น จึงเติมเต็ม chunk เดียวพอดีโดยไม่มีที่ว่างเหลือ ในขณะที่ ล้นไปยัง chunk ที่สองซึ่งบังเอิญทำให้อุปกรณ์อิ่มตัวได้ดีกว่า ประเด็นของ roofline คือแนวโน้ม และอุปกรณ์จริงมี quantization step อยู่ในตัว เรารายงานความสั่นไหวนั้นแทนที่จะเลือกเฉพาะตัวเลขที่สวย
ข้อที่สอง และสำคัญกว่านั้น: 54.5x และ 359.6x ทั้งคู่เทียบกับ CPU engine แบบไร้เดียงสา และไม่มีตัวไหนเป็นชัยชนะจากฮาร์ดแวร์ ตัวเลขทั้งสองยังมีการยุบตัวเชิงอัลกอริทึม 27x ฝังอยู่ ถ้าคุณสลับ baseline ของ CPU ไปเป็นอัลกอริทึมที่ยุบแล้วรันบน CPU — สูตรเดียวกัน fp64 ทั้งสองฝั่งถูก optimize แล้ว — ทุกแถวจะหดลงประมาณปัจจัยนั้น ซึ่งนั่นคือการแยกส่วนที่หัวข้อถัดไปจะทำให้แม่นยำ
การแยกส่วนที่ซื่อสัตย์: 27x อัลกอริทึม คูณ 6.2x ฮาร์ดแวร์

ในการแยกอัลกอริทึมออกจากซิลิคอน คุณต้องวัดสามเส้นทางบน grid multi-timeframe เดียวกัน ไม่ใช่สองเส้นทาง benchmark จึงรัน:
- cpu-engine — engine numba production, HMA จัดเรียงต่อบาร์ข้ามทุก core baseline แบบไร้เดียงสาแต่ parallel
- cpu-collapsed — weight vector ที่ยุบแล้ว,
np.correlateบวก gather, fp64, บน CPU ฮาร์ดแวร์เดียวกับ (1) แต่อัลกอริทึมดีกว่า - gpu-mlx — weight ที่ยุบแล้วเป็น
conv1dแบบ batch บน Metal GPU, fp32 อัลกอริทึมเดียวกับ (2) แต่ฮาร์ดแวร์ต่างกัน
เรียงมันไว้แล้วพาดหัว multi-timeframe เต็มรูปแบบก็แยกตัวประกอบได้อย่างสะอาด:
ปัจจัยด้านซ้าย 27x คืออัลกอริทึม — การยุบตัวจากต่อบาร์เป็น convolution จากหัวข้อก่อนหน้า มันไม่เกี่ยวอะไรกับ GPU เลย implement มันใน numpy แล้วแล็ปท็อป CPU ของคุณจะเร็วขึ้น 27x บน workload นี้ด้วยราคาแค่การ refactor ปัจจัยด้านขวา 6.2x คือฮาร์ดแวร์ — ชัยชนะที่ซื่อสัตย์แบบ like-for-like ของ Metal GPU เหนืออัลกอริทึมที่ optimize แล้วตัวเดียวกันบน CPU สิบสอง core 6.2x นั้นคือส่วนเดียวที่คุณต้องการ GPU จริงๆ เพื่อให้ได้มา
นี่คือแก่นคำสอนทั้งหมดของบทความนี้ที่พูดออกมาเป็นเลขคณิต เมื่อ vendor benchmark, README ของ library หรือเพื่อนร่วมงานที่ตื่นเต้นแสดง "167x บน GPU" ให้คุณ ปฏิกิริยาสะท้อนกลับควรเป็นคำถามเดียว: CPU baseline คืออะไร ถ้า baseline คือ implementation แบบไร้เดียงสา — และมันมักจะเป็นแบบนั้นเสมอ เพราะ baseline ที่ช้าทำให้สไลด์ดูดีกว่า — พาดหัวส่วนใหญ่ก็คือชัยชนะเชิงอัลกอริทึมที่ CPU ก็มีสิทธิ์ได้เช่นกัน และมีแค่ส่วนที่เหลือเท่านั้นที่เป็นฮาร์ดแวร์ ที่นี่ส่วนที่เหลือคือ 6.2x คำกล่าวอ้าง 167x จึงประเมินสูงเกินจริงเกี่ยวกับส่วนสนับสนุนของฮาร์ดแวร์ไปประมาณ 27 เท่า
และสังเกตว่าปัจจัยฮาร์ดแวร์เองก็ขยับตามขนาดของปัญหาด้วย บน sweep single-timeframe ขนาดเล็ก ชัยชนะ GPU-เทียบ-CPU-ที่ดีที่สุดที่แท้จริงคือ 3.2x บนการ precompute multi-timeframe ขนาดใหญ่กว่ามันคือ 6.2x — ชิปสองตัวเดียวกัน ความได้เปรียบด้านฮาร์ดแวร์เพิ่มขึ้นเกือบสองเท่า เพียงเพราะ workload ที่ใหญ่กว่าผลักดันให้สูงขึ้นบน roofline เข้าใกล้เพดาน compute แบบราบของ GPU มากกว่าที่ CPU จะตามทัน ความได้เปรียบด้านฮาร์ดแวร์ก็ไม่ใช่ค่าคงที่เช่นกัน มันคือจุดหนึ่งบนเส้นโค้งที่ไต่ขึ้นเส้นเดียวกัน และวิธีที่คุณจะขยับไปทางขวาบนเส้นโค้งนั้นคือทำให้ batch ใหญ่ขึ้นและงานต่อ combo หนาแน่นขึ้น
แนวทางตัดสินใจ: Sweep ต้องกว้างแค่ไหน

พับ roofline กลับเป็นการตัดสินใจที่คุณทำได้ก่อนจะใช้เงิน GPU คุ้มค่าเมื่อเงื่อนไขทั้งสองของ roofline เป็นจริงพร้อมกัน: sweep ของคุณอยู่ทางขวาของ batch ridge ( ดังนั้น overhead คงที่จากการเปิดใช้งานและถ่ายโอนถูก amortize แล้ว) และงานต่อ combo ของคุณถูกจำกัดโดย compute (arithmetic intensity หนาแน่นพอที่จะไปถึงเพดานราบ ไม่ใช่ pass O(n) บางเบา) พูดให้เจาะจงจากสิ่งที่เราวัดได้:
- combo ไม่กี่สิบตัวของกลยุทธ์ single-timeframe: ข้าม GPU ไปเลย คุณอยู่ทางซ้ายของ ridge ชัยชนะที่ซื่อสัตย์เหนือ parallel numba คือประมาณ 3.2x บนงานที่ใช้เวลาแค่หนึ่งในสิบวินาทีอยู่แล้ว คอขวดไม่ใช่ kernel มันคือทุกอย่างรอบๆ มัน
- combo หลายพันตัว หรือ precompute แบบ multi-timeframe / multi-indicator จริงๆ: GPU คุ้มค่าที่จะมี overhead ถูก amortize convolution ที่แชร์กันยกระดับ arithmetic intensity และชัยชนะด้านฮาร์ดแวร์ไต่ขึ้นถึง 6.2x และยังคงเพิ่มขึ้นตาม batch นี่คือ regime ที่ GPU เปลี่ยน sweep ข้ามคืนให้กลายเป็นแค่เวลาพักดื่มกาแฟ
- ไต่บันได CPU ก่อน — มันถูกกว่าและมาก่อน 298x บน CPU และการยุบตัวเชิงอัลกอริทึม 27x นั้นฟรีหรือเกือบฟรี และมันคือข้อกำหนดเบื้องต้น ไม่ใช่ทางเลือก: 6.2x ของ GPU อยู่บนฐานของอัลกอริทึมที่ยุบแล้ว ซึ่งคุณต้องเขียนอยู่ดี GPU ที่ติดเข้ากับ pipeline แบบไร้เดียงสาส่วนใหญ่แล้ววัดความไร้เดียงสานั้นเอง
ยังมีภาษีอีกอย่างบนสาขา GPU ที่ไม่เกี่ยวกับความเร็วเลย และคุณต้องคิดรวมเข้าไปในราคา: Metal GPU ของ Apple ไม่มี fp64 เลย ทุกอย่างรันใน fp32 ความแม่นยำสัมพัทธ์ประมาณ 1.2e-7 สิ่งนี้ฆ่ากลเม็ดตำรับสำหรับ moving average เร็ว — prefix-sum WMA แบบ O(n) — เพราะที่สเกลราคาประมาณ 30,000 บน 150,000 บาร์ running sum จะไปถึงประมาณ 1e14 ซึ่งเลยขอบเขต integer ที่ปลอดภัยของ fp32 ไปเจ็ดลำดับขนาด เราวัด relative error ได้สูงถึงประมาณ 2e2 (ปัจจัยของสองร้อยเท่า ไม่ใช่สองเปอร์เซ็นต์) สูตรที่ใช้งานได้จริงคือ direct windowed convolution ซึ่งแต่ละ window sum เป็นจำนวนเทอมที่มีขนาดใกล้เคียงกันจำนวนจำกัด และ fp32 ยังคงแม่นยำอยู่ที่ประมาณ 8e-7 แม้กระนั้น กลยุทธ์ที่ตัดสินใจจากเครื่องหมายของ hma - hma3 ก็ยังจะพลิก crossing เป็นครั้งคราวบนบาร์ที่อยู่ในเขตแดน ที่เส้นโค้งทั้งสองเกือบแตะกัน เพราะการปัดเศษของ fp32 ทำให้เกิดความเอียงบนความเสมอกันที่เฉียดฉิว นั่นเป็นเหตุผลที่ path ของ GPU มาพร้อม equivalence gate ที่วัดว่า trade เบี่ยงเบนไปมากแค่ไหน — PnL delta เป็น basis point, การเปลี่ยนแปลงสัมพัทธ์ของจำนวน trade — แทนที่จะยืนยัน output ที่เหมือนกันทุก bit ซึ่งมันไม่มีวันให้ได้ ในการรันของเรา ความเบี่ยงเบนนั้นคือ fill ที่เปลี่ยนไป 90 รายการจากทั้งหมด 479,016 (0.019%) ซึ่งอยู่ในขอบเขตที่ยอมรับได้สบายๆ แต่ภาระนั้นเป็นเรื่องจริง: การไปหา GPU หมายถึงการเป็นเจ้าของเรื่องราวความเท่าเทียมเชิงตัวเลข ไม่ใช่แค่นาฬิกาที่เร็วกว่า ต้นทุนทางวิศวกรรมนั้นก็เป็นส่วนหนึ่งของจุดคุ้มทุนด้วยเช่นกัน
ตัวเลขมีรูปทรงแบบ Apple แต่เส้นโค้งไม่ใช่
ทุกตัวเลขข้างต้นมาจาก Apple M2 Max: อุปกรณ์ unified-memory ที่ GPU และ CPU แชร์ pool เดียวกัน และ GPU ที่มีแค่ fp32 ไม่มี double precision การ์ด NVIDIA หรือ AMD แบบ discrete จะเปลี่ยนค่าคงที่เหล่านี้ และควรพูดให้ชัดว่าแต่ละอย่างขยับไปทางไหน เพราะรูปทรงของข้อโต้แย้งยังคงอยู่แม้ตัวเลขจะไม่เหมือนเดิม
- Overhead การถ่ายโอน แย่ลง ไม่ใช่ดีขึ้น การ์ด discrete อยู่หลัง PCIe ดังนั้น input และ result จึงต้องคัดลอกข้ามบัสจริงๆ ซึ่ง unified memory หลีกเลี่ยงได้ นั่นดัน batch ridge ไปทางขวา — คุณต้องการ sweep ที่กว้างกว่าเดิมอีกก่อนที่ GPU แบบ discrete จะ amortize การเปิดใช้งานของมันได้ ขอบซ้ายของ roofline ชันกว่าบนอุปกรณ์ PCIe ไม่ใช่นุ่มนวลกว่า
- เพดานราบ สูงขึ้น GPU ระดับ data-center มี FLOP/s และ bandwidth มากกว่า GPU แบบ integrated มาก ดังนั้นชัยชนะฮาร์ดแวร์แบบ asymptotic บน sweep ที่อิ่มตัวจึงใหญ่กว่า 6.2x ของเรา รางวัลสำหรับการไปถึงฝั่งขวาของเส้นโค้งเพิ่มขึ้น ค่าธรรมเนียมสำหรับการนั่งอยู่ฝั่งซ้ายก็เพิ่มขึ้นเช่นกัน
- fp64 กลับมา และ prefix-sum trick ก็กลับมาด้วย บนการ์ดที่มี double precision จริง prefix-sum WMA แบบ O(n) ใช้งานได้อีกครั้ง และ parity gate ก็สามารถขันให้แน่นเข้าใกล้ bit-exact ได้ ภาษี fp32 เฉพาะที่เราจ่ายไป — direct convolution แทน prefix sum, gate ที่วัดความเบี่ยงเบนแทนที่จะ assert — เป็นรายละเอียดเฉพาะของ Apple Silicon ไม่ใช่กฎสากล
ไม่มีอะไรในนี้เปลี่ยนแก่นของบทความ บนอุปกรณ์ใดก็ตาม : overhead คงที่ที่คุณต้อง amortize, asymptote ที่คุณเข้าใกล้ได้จากฝั่งขวาเท่านั้น ค่าคงที่คือฮาร์ดแวร์ เส้นโค้งคือเลขคณิต วัด , , และ ของคุณเองด้วย batch sweep ห้าบรรทัดก่อนที่คุณจะเชื่อพาดหัวของใครก็ตาม — รวมถึงของเราด้วย
จุดที่บทความนี้เชื่อมโยงถึง
นี่คือการวัดครั้งที่สี่ในซีรีส์ย่อยเล็กๆ เกี่ยวกับที่มาจริงๆ ของความเร็ว backtest และชิ้นส่วนต่างๆ ประกอบกันเป็นข้อโต้แย้งเดียวเกี่ยวกับ สิ่งที่ควร optimize ก่อนจะใช้เงิน:
- บันไดความเร็ว ไต่จาก pandas ไปสู่ parallel numba เป็น 298x บน CPU เพียงอย่างเดียว และทิ้ง GPU ไว้เป็นคำถามเปิด บทความนี้ตอบคำถามนั้น: GPU เป็นขั้นบันไดที่ห้าที่จริงแต่มีเงื่อนไข คุ้มค่า 3.2x–6.2x เหนือขั้น CPU สูงสุด และเฉพาะเมื่อ sweep กว้างพอที่จะไต่ roofline เท่านั้น
- ภาษี IPC ทำการเคลื่อนไหวแบบเดียวกันในทิศทางตรงข้าม — วัดว่าการออกจาก process มีต้นทุนเท่าไร — และไปถึงข้อสรุปรูปทรงเดียวกัน: ขอบเขต (socket, การเปิดใช้งาน GPU) ราคาถูก ภาษีอยู่ที่ ความถี่และความจุกจิก ที่คุณข้ามมัน batch การเรียก GPU ของคุณด้วยเหตุผลเดียวกับที่คุณ batch IPC ของคุณ: เพื่อ amortize ต้นทุนคงที่ต่อการข้ามแต่ละครั้ง
- Parquet cache แบบรวม คือเวอร์ชันฝั่ง CPU ของสิ่งที่ GPU precompute ทำ — คำนวณ indicator ที่ใช้ร่วมกันครั้งเดียว แล้วใช้ซ้ำข้ามทุก combo GPU แค่เอาหลักการใช้ซ้ำ-และ-batch นั้นไปใช้บนซิลิคอน
- และ fp32 parity gate ก็คือปัญหา backtest-live parity แบบย่อส่วน: ในจังหวะที่ fast path ของคุณคำนวณอะไรบางอย่างที่ต่างจาก reference เล็กน้อย คุณเป็นหนี้บัญชีที่วัดปริมาณได้ของความเบี่ยงเบนนั้น ไม่ใช่การโบกมือผ่านๆ
หลักการที่เชื่อมโยงกันคือหลักการเดียวกับที่ซีรีส์ทั้งหมดนี้ผลักดัน: วัดสิ่งที่คุณกำลังถูกขายจริงๆ speedup คืออัตราส่วน และอัตราส่วนมีทั้งตัวเศษและตัวส่วน ความผิดหวังส่วนใหญ่จาก GPU มาจากตัวส่วน — CPU baseline — ที่ถูกเลือกมาเพื่อให้ดูดี และการสูญเปล่าส่วนใหญ่ของ GPU มาจากการรัน sweep ที่เล็กเกินกว่าจะออกจากขอบซ้ายของ roofline
ประเด็นสำคัญ
- Speedup ของ GPU คือเส้นโค้ง ไม่ใช่ตัวเลข บนการ precompute multi-timeframe ของเรา ความได้เปรียบเหนือ CPU วิ่งจาก 54.5x ที่หนึ่ง combo ต่อการเรียก ไปจนถึง 359.6x ที่หกสิบเอ็ด — ชิปเดียวกัน ข้อมูลเดียวกัน ตัวเลขเดียวใดๆ ก็เป็นแค่จุดหนึ่งบนเส้นโค้งนั้น ให้ถามว่ามันวัดที่ขนาด batch เท่าไร
- สอบถาม CPU baseline เสมอ พาดหัว multi-timeframe ที่ 167x แยกตัวประกอบออกมาได้อย่างสะอาดเป็น 27x ของอัลกอริทึม (การยุบตัวจากต่อบาร์เป็น convolution ซึ่งเร่ง CPU ได้มากพอกัน) คูณกับ 6.2x ของฮาร์ดแวร์จริง ชัยชนะที่เป็นธรรมของ GPU-เทียบ-CPU-ที่ดีที่สุดที่นี่คือ 3.2x สำหรับ single-timeframe, 6.2x สำหรับ multi — ไม่ใช่ 167x
- ความได้เปรียบเพิ่มขึ้นตามขนาดปัญหา และปัจจัยฮาร์ดแวร์ก็เช่นกัน batch ที่ใหญ่ขึ้นและงานต่อ combo ที่หนาแน่นขึ้นผลักคุณขึ้นไปบน roofline: ชัยชนะฮาร์ดแวร์ที่ซื่อสัตย์เองก็เพิ่มขึ้นจาก 3.2x เป็น 6.2x เพียงเพราะทำให้ workload ใหญ่ขึ้น sweep ขนาดเล็กอยู่ทางซ้ายของ ridge และแทบไม่ได้ประโยชน์
- แก้อัลกอริทึมและไต่บันได CPU ก่อน — ชัยชนะของ GPU อยู่บนฐานของมัน ไม่ใช่แทนที่มัน 6.2x นั้นวัดเทียบกับอัลกอริทึมที่ยุบแล้วซึ่งคุณต้องเขียนอยู่ดี ติด GPU เข้ากับ pipeline แบบไร้เดียงสา แล้วส่วนใหญ่ที่คุณวัดคือความไร้เดียงสานั้น ไม่ใช่ซิลิคอน
- การไปหา GPU หมายถึงการเป็นเจ้าของเรื่องราวความเท่าเทียมเชิงตัวเลข ไม่มี fp64 บน Metal, กลเม็ด prefix-sum WMA ตายที่สเกลราคา (relative error ประมาณ 2e2) และกลยุทธ์แบบ sign-of-crossing พลิกบนบาร์ที่อยู่ในเขตแดน จัดส่ง equivalence gate ที่วัดปริมาณความเบี่ยงเบนเป็น basis point นับต้นทุนทางวิศวกรรมนั้นไว้ในจุดคุ้มทุนของคุณด้วย
เมื่อใครก็ตามบอกคุณว่า GPU ทำให้ backtest ของพวกเขาเร็วขึ้นร้อยเท่า พวกเขาแทบไม่ได้บอกอะไรคุณเลย ถามพวกเขาถึงขนาด batch และ CPU baseline แล้วตัวเลขร้อยเท่านั้นมักจะคลี่คลายกลายเป็นชัยชนะฮาร์ดแวร์หลักเดียวหลักเดียวที่ห่อหุ้มชัยชนะเชิงอัลกอริทึมที่พวกเขาสามารถได้มาฟรีๆ — ซึ่งคุ้มค่าที่จะมี บน sweep ที่กว้างพอ ด้วยเหตุผลตรงตามที่ roofline บอกไว้ ไม่มีเหตุผลอื่นเพิ่มเติมอีกแม้แต่ข้อเดียว
ผู้เขียน
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.