- ในเชเดอร์ของ GPU โค้ดที่เลือกค่าด้วย ternary operator หรือ
ifแบบง่าย ๆ มักถูกจัดการเป็น conditional move (select) ไม่ใช่ conditional branch - แม้จะเปลี่ยนไปใช้
step()และการ mask ด้วยเลขคณิต ก็ไม่มี branch ให้กำจัดตั้งแต่แรก ดังนั้นสมมติฐานเรื่องที่เรียกว่า การปรับแต่งเพื่อตัด branch จึงไม่ถูกต้อง - เอาต์พุตของคอมไพเลอร์ AMD และ Microsoft แสดงคำสั่งเปรียบเทียบและ conditional mask/move โดยไม่เห็นคำสั่ง jump หรือ branch
- เวอร์ชัน
step()สร้าง mask0.0/1.0แล้วใช้การคูณและการบวกเพื่อประกอบผลลัพธ์ ทำให้มี การคำนวณที่ไม่จำเป็น เพิ่มขึ้นเมื่อเทียบกับโค้ดที่ทำ conditional move โดยตรง - GPU branch ที่ข้ามบล็อกการคำนวณขนาดใหญ่ตามเงื่อนไขยังคงมีประโยชน์ แต่สำหรับการเลือกค่าแบบง่าย การตรวจสอบ machine code ที่สร้างออกมาจะปลอดภัยกว่า
การเลือกค่าแบบง่ายไม่ใช่ GPU branch
- ฟังก์ชันตัวอย่าง
snap45()คำนวณx = abs(v.x)จากเวกเตอร์อินพุต แล้วคืนหนึ่งในผลลัพธ์vec2สามแบบด้วย ternary operator สองตัว - ตรรกะเดียวกันนี้ยังคงเหมือนเดิมแม้เขียนด้วยคำสั่ง
ifปกติ - “การปรับแต่ง” ที่เป็นปัญหาคือการเปลี่ยน ternary operator เป็น
step()และการประกอบผลรวมแบบถ่วงน้ำหนัก- สร้าง
w0,w1,w2ด้วยstep() - คำนวณ
res0,res1,res2แยกกัน - ประกอบผลลัพธ์สุดท้ายด้วย
w0*res0 + w1*res1 + w2*res2
- สร้าง
- การแปลงนี้เริ่มจากความเข้าใจผิดว่าโค้ดเดิมสร้าง conditional branch
- การเลือกค่าในรีจิสเตอร์แบบง่ายไม่เปลี่ยน instruction pointer และไม่ทำให้เกิดการทำนายผิดพลาด, pipeline flush หรือการทำให้ instruction cache ใช้ไม่ได้
- branch จริงบน GPU อาจเร็วและมีประโยชน์เมื่อข้ามบล็อกการคำนวณขนาดใหญ่ตามเงื่อนไข
- อย่างไรก็ตาม ในกรณีที่เลือกค่าแบบง่ายหรือผลลัพธ์ของการคำนวณเหมือนตัวอย่างนี้ โดยทั่วไปถือได้ว่าไม่มี branch เกิดขึ้นใน machine code ที่สร้างออกมา
ความแตกต่างที่เห็นได้จากเอาต์พุตของคอมไพเลอร์
- โค้ด GLSL เดิมที่ใช้ ternary operator ถูกคอมไพล์โดยคอมไพเลอร์ AMD เป็นคำสั่งเปรียบเทียบและ conditional mask
- การเปรียบเทียบ:
v_cmp_gt_f32,v_cmp_ngt_f32 - conditional mask:
v_cndmask_b32
- การเปรียบเทียบ:
- เอาต์พุตของคอมไพเลอร์ Microsoft ก็แสดงโครงสร้างเดียวกัน
- การเปรียบเทียบ:
lt - conditional move:
movc
- การเปรียบเทียบ:
- เอาต์พุตของคอมไพเลอร์ทั้งสองไม่มี คำสั่ง jump/branch
เหตุผลที่วิธี step() มีต้นทุนสูงกว่า
- วิธีที่ใช้
step()จะสร้าง mask0.0หรือ1.0ด้วย conditional move ก่อน แล้วจึง mask ผลลัพธ์ตัวเลือกหลายตัวด้วยการคูณและการบวก - โค้ดเดิมทำ conditional move กับค่าที่ต้องการโดยตรง จึงสิ้นเปลืองน้อยกว่าวิธี
step()ที่ต้องเพิ่มการสร้าง mask และการประกอบด้วยเลขคณิต - บนฮาร์ดแวร์หลายแบบ เวอร์ชันที่ใช้
step()อาจวัดได้ว่าช้ากว่าเวอร์ชันเดิมมาก - การเรียก
abs()บางส่วนใน GLSL ของโค้ดตัวอย่างไม่ได้เป็นคำสั่ง GPU แยกต่างหาก แต่เข้าไปเป็น instruction modifier ดังนั้นในกรณีนี้จึงมองได้ว่าการเรียกabs()แทบไม่มีต้นทุน - การแนะนำ
float a = mix(b, c, step(y, x));ให้เป็นการปรับแต่งของfloat a = x < y ? b : c;เป็นแนวทางที่ผิด
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
ข้อสรุปของบทความดูเหมือนจะถูกต้อง แต่ถ้าไม่ใช่แค่แสดงผลการสร้างโค้ดของเวอร์ชันที่ดีกว่าเท่านั้น แต่แสดง ผลการสร้างโค้ดของทั้งสองเวอร์ชัน ด้วย เหตุผลน่าจะหนักแน่นกว่านี้
ในข้อความที่ยกมาบอกว่า “เวอร์ชันที่อ้างว่าปรับให้เหมาะสมนั้นช้ากว่าเวอร์ชันเดิมมาก… สิ้นเปลืองการคูณสองครั้งกับการบวกหนึ่งหรือสองครั้ง… มาดู machine code ที่สร้างขึ้นกัน” แต่ในความเป็นจริงกลับแสดงเฉพาะเวอร์ชันที่ดีซึ่งไม่มีการคูณหรือการบวก
นั่นพิสูจน์ได้แค่ว่าเวอร์ชันที่ดีนั้นโอเค แต่ยังไม่ได้พิสูจน์ว่าเวอร์ชันที่แย่นั้นแย่กว่า
ถึงจะแสดงโค้ดที่สร้างขึ้นของอีกเวอร์ชัน ก็คงมีแนวโน้มแค่แสดงให้เห็นว่ามันยาวกว่า และฝั่งนั้นก็คงไม่คาดว่าจะเกิด branch เช่นกัน จึงไม่น่ามีคุณค่ามากนัก
ถ้ามีวิธีดี ๆ ในการรู้ว่าเมื่อไร
ifจะบังคับให้เกิด branch จริง และเมื่อไรจะไม่เกิด ก็คงดีเหตุผลที่ผู้คนใช้
mix/lerpซึ่งอาจแพงกว่า ก็เพราะกลัวว่าจะเกิด branch แม้จะต้องยอมรับ overhead เล็กน้อยก็ตามเป็นเรื่องดีที่โค้ดที่ชัดเจนที่สุดอย่าง
v = x > y ? a : b;ทำงานได้ดีจริง แต่การที่ syntaxifเดียวกันบางครั้งเป็น branch และบางครั้งไม่ใช่ ก็ทำให้ไม่สบายใจในบริบทที่ห้ามเกิด branch จริง ๆ ก็อยากให้
branch-ifกับ if แบบไม่ branch เป็น keyword คนละตัว และ keyword แบบไม่ branch ควรทำให้คอมไพล์ล้มเหลวถ้าคอมไพเลอร์สร้างแบบไม่มี branch ไม่ได้ ส่วน keyword แบบ branch ก็ควรเตือนถ้าสามารถสร้างได้โดยไม่ต้องมี branchตอนแรกน่าจะไม่อยากทำให้โปรแกรมเมอร์กลัว จึงซ่อน execution model แล้วอธิบายด้วย abstraction ที่เรียกว่า “เธรด” และหลังจากนั้นในการโปรโมต GPU ก็ยังใช้แนวว่า “มี CUDA thread จำนวนมหาศาล” ต่อไป
ผลก็คือเกิดความเชื่อแปลก ๆ ในการเขียนโค้ด GPU
จริง ๆ แล้ว หลายครั้งการมี branch ในโค้ดก็เป็นเรื่องดี และ branch เองก็เร็ว
ปัญหาคือ SIMD lane แต่ละ lane ไม่สามารถแยกออกไปคนละ branch ได้ ดังนั้นคอมไพเลอร์จึงปล่อยโค้ดทั้งสองฝั่งแทนการ branch แล้ว mask ผลลัพธ์ตามเงื่อนไข
ดังนั้นการคำนวณที่อิงจากค่า input ของ shader, vertex, ดัชนีของ compute shader ฯลฯ จะไม่ branch จริง แต่จะรันตามลำดับด้วย masking
ในตัวอย่างของบทความ ค่าทั้งสองฝั่งของตัวดำเนินการ
?ก็ถูกคำนวณทั้งหมด และโดยทั่วไปเงื่อนไขที่อิงกับค่า SIMD ก็เป็นแบบเดียวกันอย่างไรก็ตาม อาจมี branch แบบลัดเพื่อข้ามการคำนวณอย่างรวดเร็วเมื่อทุก lane มีค่าเดียวกันได้ แต่โดยทั่วไปแล้วจะคำนวณทั้งฝั่งจริงและเท็จ
มีเพียงเงื่อนไขที่อิงกับ scalar register คือค่าคงที่ของ shader หรือค่า uniform เท่านั้น ที่สร้าง branch จริง และ branch แบบนี้เร็วมาก
ตัวอย่างเช่น คำสั่ง CMOV ถูกนำมาใช้ในคอร์ P6 เมื่อปี 1995
branch มีต้นทุนสูงแม้บนสถาปัตยกรรม scalar และคอมไพเลอร์จะพยายามตัดสินให้ดีที่สุดว่าเมื่อใดควรใช้กลยุทธ์ทดแทน
บางครั้งก็ตัดสินผิด แต่ไม่ได้ผิดบ่อยมาก
conditional move คือค่าเริ่มต้น และ branch จริงคือการปรับประสิทธิภาพที่เป็นไปได้เฉพาะเมื่อเป็น uniform branch ที่ทั้ง workgroup ไปในทิศทางเดียวกัน
a = f(z); b = g(z); v = x > y ? a : b;ถ้าค่าใช้จ่ายในการเรียก
f()และg()ค่อนข้างสูง การจะปล่อยโค้ดแบบมีเงื่อนไข หรือคำนวณทั้งคู่แล้วค่อยเลือก ก็เป็นเรื่องของการแลกเปลี่ยนไม่ใช่การเลือกที่ง่าย และคอมไพเลอร์จะเป็นผู้ตัดสิน
อาจแยกฟังก์ชันทั้งหมดในโค้ดเหมือนระบายสีว่า branch ได้/ไม่ branch และทำให้ฟังก์ชันที่ทำเครื่องหมายว่าไม่ branch ต้องถูกคอมไพล์
ifเป็น conditional move และเรียกได้เฉพาะฟังก์ชันที่ไม่ branch เท่านั้นความเชื่อว่า “branch บน GPU ช้า” ส่วนใหญ่เกิดจากสมัย PlayStation 3 นานมาแล้ว ซึ่งตอนนั้นมันช้าจริงพอสมควร
PS3 ใช้ RSX GPU ของ NVIDIA และจำได้ว่าในเอกสารระบุว่า branch ใช้ 6 cycle แต่การวัดจริงมักช้ากว่านั้นเสมอ
แม้แต่ branch ที่ coherent อย่างสมบูรณ์ซึ่งทุก thread ใน warp ไปเส้นทางเดียวกันก็ยังเป็นแบบนั้น และ branch ที่ incoherent ก็ยิ่งช้ากว่า เพราะคำสั่ง
IFEHใช้ 6 cycle แถม GPU ต้องรัน branch ทั้งสองฝั่งผมคิดว่าความเชื่อที่สืบมาจนถึงวันนี้ว่า “branch บน GPU ช้า” เริ่มจากตรงนั้น
branch บน GPU สมัยนี้ โดยเฉพาะ branch ที่ coherent ค่อนข้างถูกแล้ว
overhead ของกลไก branch ในปัจจุบันอาจลดลงแล้ว แต่ข้อจำกัดทางกายภาพที่ throughput ของ branch แต่ละฝั่งจะลดลงตามสัดส่วนของ thread ที่ active ยังเหมือนเดิม
ถ้าทั้งสองฝั่งของ branch ถูก execute ทั้งคู่และความยาวของคำสั่งเท่ากัน ประสิทธิภาพเฉลี่ยของทั้งสองฝั่งจะลดลงอย่างน้อยครึ่งหนึ่ง
ดังนั้นความเชื่อว่า branch บน GPU ช้าจึงยังอยู่ได้นาน และในทางปฏิบัติก็เป็นเรื่องจริง
ถ้าเป็นไปได้ก็คุ้มที่จะพยายามจัดรูปปัญหาใหม่ให้ไม่มี branch มากขึ้น
เหตุผลหลักในการหลีกเลี่ยง dynamic branch ไม่ใช่เพราะ branch เองช้าโดยเนื้อแท้ แต่ใกล้เคียงกับเรื่องนี้มากกว่า
การปรับให้เหมาะสมแบบหลีกเลี่ยงการแตกแขนง ลักษณะนี้เคยได้ผลอยู่ช่วงหนึ่ง
ผมเคยโปรไฟล์บน Xbox 360 กับ GPU ในตัวของ Intel รุ่นเก่า แต่ตอนนี้ควรเลิกทำแบบนั้นได้แล้ว
การดึงบิตและการดำเนินการจำนวนเต็มอื่น ๆ ก็คล้ายกัน
เมื่อก่อนการจำลองด้วยคณิตศาสตร์แบบทศนิยมลอยตัวจะเร็วกว่า แต่ตอนนี้ GPU ทุกตัวมีการดำเนินการจำนวนเต็มที่รวดเร็วแล้ว
เช่น ถ้าดู RDNA2 ISA ซึ่งเป็นสถาปัตยกรรมของ PS5 และ Xbox Series S|X ดูเหมือนจะเห็นแค่คำสั่งสเกลาร์ 32 บิตสำหรับจำนวนเต็มเท่านั้น
[0] https://www.amd.com/content/dam/amd/en/documents/radeon-tech...
โค้ดที่ยกมานั้นเป็น โค้ดไร้การแตกแขนง อยู่แล้ว
คนที่ให้คำแนะนำดูเหมือนจะตัดสินโค้ดแตกแขนงจากแค่ว่าในซอร์สมีไวยากรณ์ที่ดูเหมือนเงื่อนไขหรือไม่ แล้วคิดว่าการเลี่ยงสิ่งนั้นคือการปรับให้เหมาะสม
บทความนี้ก็เกี่ยวข้อง: https://medium.com/@jasonbooth_86226/branching-on-a-gpu-18bf...
“ถ้าคุณถามอินเทอร์เน็ตว่าจะเขียน branch บน GPU อย่างไร เขาอาจพูดเหมือนคุณกำลังเปิดประตูนรกเชิญปีศาจเข้ามา ต้องหลีกเลี่ยงให้ได้ไม่ว่าจะต้องแลกด้วยอะไร และบอกว่าสามารถเลี่ยงได้ด้วยทริกคณิตศาสตร์ประหลาด ๆ อย่าง ternary operator หรือ
step()คำแนะนำส่วนใหญ่นี้ถ้ามองในแง่ดีก็ล้าสมัย และหลายครั้งก็ผิดไปเลย มาทำความเข้าใจให้ถูกกัน”โปรเซสเซอร์ก็เปลี่ยน คอมไพเลอร์ก็เปลี่ยน
ถ้ารายละเอียดพวกนี้สำคัญ วิธีที่ดีที่สุดคือแจกจ่ายหลายเวอร์ชันแล้วเลือกรุ่นที่ เร็วที่สุด ตอนรันไทม์
อย่างที่เคยพูดไปหลายครั้งแล้ว ผมเคยลบแอสเซมบลีที่เขียนเองออก แล้วแทนด้วย C ธรรมดาหรือโค้ดคล้าย ๆ กัน จนทำให้เร็วขึ้นมาก
แอสเซมบลีนั้นอาจเร็วกว่าเมื่อ 10–20 ปีก่อน แต่ตอนนี้สถานการณ์เปลี่ยนไปแล้ว
ไม่ค่อยรู้จักเกมหรือเอนจินที่ทำแบบนั้นจริง ๆ
โดยหลักการแล้วอาจทำได้
API ส่วนใหญ่ เช่น D3D, GL, Vulkan เปิดเผย performance counters และแม้ความน่าเชื่อถือจะแตกต่างกันไปตามผู้ผลิต แต่ก็สามารถสร้างฉากทดสอบตัวแทนแล้วเล่นซ้ำหลาย ๆ ครั้งเพื่อวัดการปรับให้เหมาะสมได้
แต่เกมจำนวนมากใช้ฉากที่สร้างแบบไดนามิกและเชดเดอร์ที่สร้างแบบไดนามิก ดังนั้นจำนวนชุดผสมที่ต้องทดสอบอาจกลายเป็นอุปสรรคได้
อาจต้องให้ผู้ใช้รอจนกว่า benchmark จะจบ
ถ้ามีฮาร์ดแวร์ ก็อาจวัดล่วงหน้าบน GPU หลายเจเนอเรชันของแต่ละผู้ผลิต แล้วฮาร์ดโค้ดเฉพาะการตัดสินใจสำคัญ ๆ ได้ แต่ผมไม่ค่อยรู้จัก โครงสร้างพื้นฐานเดิม แบบนั้น
มันดักเชดเดอร์ของเกมแล้วแทนที่ด้วยเชดเดอร์คัสตอมที่ NVIDIA ปรับให้เหมาะสมแล้ว
ดังนั้นในบันทึกการเปลี่ยนแปลงของไดรเวอร์ NVIDIA จึงเห็นข้อความทำนอง “ปรับแต่งเกม X ให้เร็วขึ้น 40%”
เราไม่สามารถใช้เวลากับแต่ละเชดเดอร์ได้ไม่จำกัด
ก็ต้องโปรไฟล์บนฮาร์ดแวร์ที่สนใจ แล้วถ้าวิธีที่เลือกช้ากว่าบนโปรเซสเซอร์ในอนาคตที่สมมติขึ้นมา ก็ช่วยไม่ได้
หวังได้แค่ว่าโปรเซสเซอร์นั้นจะเร็วพอจนไม่เป็นปัญหา
ดูเหมือนว่าความผิดพลาดและความสับสนที่บทความนี้พยายามแก้ไขจะเกิดซ้ำที่นี่ด้วย
บทความไม่ได้อ้างว่า conditional branch นั้นฟรี
ในมุมผม มันไม่ใช่บทความที่พูดถึงต้นทุนด้านประสิทธิภาพของโค้ดแตกแขนงด้วยซ้ำ
ประเด็นของบทความคือรูปแบบตรรกะเงื่อนไขที่ยกมาจะไม่ถูกคอมไพล์เป็นโค้ด conditional branch
และเราไม่ควรเผยแพร่คำแนะนำที่เป็นอันตรายว่าให้ฝืนซ่อนนิพจน์เงื่อนไขทุกอย่างที่มองเห็นต่อไป
สำหรับโค้ดแตกแขนงจริง ๆ นั้น เห็นได้ชัดว่าการรันโค้ดแตกแขนงซับซ้อนกว่า
ไม่มี branch ฟรี และถ้าหลีกเลี่ยง branch ได้ในขอบเขตที่สมเหตุสมผล โค้ดใด ๆ ก็มีโอกาสเร็วขึ้น
โชคดีที่โค้ดเดิมเป็น โค้ดไร้การแตกแขนง อยู่แล้ว
เช่นเคย ไม่มีมาตรวัดสากลที่จะบอกได้ว่าการปรับให้เหมาะสมนั้นคุ้มค่าหรือไม่
[0] ตรงนี้คำว่า “ที่มองเห็น” สำคัญ หมายถึงกรณีที่ไม่สนใจโค้ดที่ generate ออกมา แต่สนใจแค่ว่าซอร์สโค้ดดูไม่เหมือนเงื่อนไขหรือไม่
[1] แน่นอนว่านี่ไม่ใช่เรื่องโชคดี ผมเดาว่าคงมีใครสักคนส่งข้อเสนอปรับปรุงโค้ดเชดเดอร์ที่ดูชัดเจนแต่ผิดไปให้ IQ
แล้วทำไมคอมไพเลอร์ถึงไม่ฉลาดพอจะรู้ว่าเวอร์ชันที่ “ปรับให้เหมาะสมแล้ว” เป็นโค้ดเดียวกันล่ะ?
มันควรจะเข้าใจ
step()และปรับให้เหมาะสมแยกกรณีstep() = 0.0,step() == 1.0ได้ไม่ใช่หรือ?อย่างน้อยก็ตัดการคูณออกไปได้หนึ่งครั้ง ดังนั้นโดยทั่วไปแม้จะถูกเปลี่ยนเป็น conditional load/store หรืออย่างอื่น ก็น่าจะได้ประโยชน์เสมอ
คอมไพเลอร์บางตัวมีโอกาสสูงที่จะทำการปรับแบบนี้ในบางกรณี แต่ก็เป็นไปได้ชัดเจนเช่นกันที่จะเขียนเวอร์ชันที่คอมไพเลอร์ไม่เข้าใจ
การปรับให้เหมาะสมส่วนใหญ่เกิดขึ้นฝั่งไดรเวอร์ และงานที่ใช้เวลานานเกินไปจะปรากฏออกมาเป็น อาการสะดุดจากการคอมไพล์เชดเดอร์
ผมบอกไม่ได้ว่าการปรับให้เหมาะสมนี้เกิดขึ้นจริงหรือไม่ แต่เป็นปัจจัยที่ต้องพิจารณาเสมอ
เหตุผลที่เวอร์ชัน “เพิ่มประสิทธิภาพ” ที่เป็นปัญหากลับช้ากว่า เป็นเพราะฟังก์ชัน
step()จริง ๆ แล้วถูกอิมพลีเมนต์ประมาณนี้:float step( float x, float y ) { return x < y ? 1.0 : 0.0; }แล้วเราจะรู้ได้อย่างไรว่าฟังก์ชันของ OpenGL เรียกใช้ primitive ของ GPU หรือถูก emulation อยู่?
ผมเคยทำแบบนั้นกับ HLSL shader บ่อย ๆ และได้เรียนรู้เกี่ยวกับชุดคำสั่งเสมือนมามาก
ตัวอย่างเช่น น่าสนใจที่ GPU มีคำสั่ง
sincosแต่ฟังก์ชันตรีโกณมิติผกผันจะถูก emulation ระหว่างการคอมไพล์ถ้าประสิทธิภาพสำคัญ ก็อาจจำเป็นต้องรู้
แต่แค่ข้อเท็จจริงว่า
stepไม่ใช่คำสั่งเฉพาะทาง แต่ถูกอิมพลีเมนต์เป็นฟังก์ชันไลบรารีบนเงื่อนไข ไม่ได้บอกประสิทธิภาพเมื่อเทียบกับคำสั่งเฉพาะทาง ดังนั้นไม่จำเป็นต้องยึดติดกับตัวอิมพลีเมนต์มากเกินไปถ้าอยากรู้เรื่องสถาปัตยกรรม GPU ก็ดู disassembly, โค้ดไดรเวอร์โอเพนซอร์ส, LLVM และเอกสาร ISA ได้
ทุกครั้งที่ดู shader ที่ถูก decompile โดยรวมก็คล้ายกับสิ่งที่คิดในภาษา C
specification อย่าง OpenGL กำหนดพฤติกรรมของฟังก์ชัน built-in จำนวนมาก และ implementation ก็ทำให้เป็นไปตาม specification นั้นด้วยคำสั่ง assembly มาตรฐาน
ลองหาเว็บไซต์ออนไลน์ที่ decompile ไปยังหลายสถาปัตยกรรมดูได้
โดยปกติเราไม่จำเป็นต้องรู้หรือสนใจว่าฟังก์ชัน built-in ถูกอิมพลีเมนต์อย่างไร
ถ้าคุณกำลังสนใจอยู่ ก็น่าจะกำลังคิดเรื่องการ optimize และคำตอบตอนนั้นคือ “วัดแล้วตรวจสอบว่าอะไรดีกว่า”
ตามความหมายที่ผมเรียนมา conditional statement ก็คือ branch
ในระดับ machine code control flow ถูกเลือกระหว่าง runtime ดังนั้น conditional jump จึงเป็น branch ตามนิยาม
การใช้
step()ไม่ใช่การเปลี่ยนตรรกะให้เป็นเลขคณิต แต่เป็นเพียงการซ่อนตรรกะไว้ใน call ของฟังก์ชันไลบรารีเท่านั้นการที่
step()เป็นฟังก์ชัน built-in หรือเป็นฟังก์ชันที่ปรากฏในบทความคณิตศาสตร์ไม่ได้ทำให้ต่างออกไปในคณิตศาสตร์ นิยามของ
step()ก็เป็น conditional statement ตรง ๆถ้าจะ optimize ให้ถูกต้องโดยไม่มี conditional statement ต้องเลือกฟังก์ชันต่อเนื่องที่คล้ายผลลัพธ์ที่ต้องการ แล้วปรับพารามิเตอร์ให้เข้าใกล้เป้าหมายที่สุด
โดยทั่วไปจะเลือกพหุนาม รันวิธีประมาณค่าแบบวนซ้ำมาตรฐาน แล้วได้
f(x)ที่ไม่มี branch มีแค่การบวก·การคูณ และค่าคงที่ที่ “เจาะจงอย่างประหลาด”ส่วนที่ผู้เขียนยืนยันหนักแน่นว่า conditional move ไม่ใช่ “branch” นั้น ผมไม่ค่อยเข้าใจ
ที่
abs()ไม่ได้กลายเป็นคำสั่ง GPU แต่ลดรูปเป็น instruction modifier จนเหมือนฟรีได้ เป็นเพราะ representation แบบ two's complement ของจำนวนเต็มและ IEEE-754 floating point ทำให้จัดการบิตเครื่องหมายเป็นบิตบนสุดได้ดังนั้น
abs()ก็จบแค่ทำให้บิตบนสุดเป็น 0 เสมอ หรือ mask ตอนคำสั่งอ่านแต่
step()หรือ ternary operation ใด ๆ และเท่าที่ผมรู้ คำสั่ง conditional move ไม่ใช่กรณีพิเศษแบบนั้นความรู้พื้นฐานอย่าง
abs(),sqrt(), ฟังก์ชันตรีโกณมิติถือว่าใกล้เคียงความรู้มาตรฐาน ส่วนที่เหลือก็สงสัยว่าจะสำคัญแค่ไหนอยู่ดีstep()จำเป็นต้องมีเงื่อนไขอยู่ที่ไหนสักแห่ง และไม่ว่าจะทำเอง ฝากให้ไลบรารีทำ หรือฝากให้ฮาร์ดแวร์ทำ ลักษณะพื้นฐานก็ไม่เปลี่ยนผมเคยตกหลุมพรางนี้มาแล้ว
Claude หรือ ChatGPT ก็มักเสนอสิ่งนี้เป็นการ optimization
แต่ทุกครั้งที่ลองวัด ประสิทธิภาพกลับตกลง และบางครั้งตกลงค่อนข้างมากด้วย
LLM ก็แค่ทำซ้ำสิ่งที่อยู่ใน corpus สำหรับฝึก
ถ้าอินเทอร์เน็ตส่วนใหญ่แนะนำเนื้อหาผิด ๆ แบบ “optimization” ด้วย conditional move นี้ LLM ก็จะแนะนำแบบนั้นด้วย