อัลกอริทึมการเรียงลำดับด้วย CUDA
(ashwanirathee.com)- วิธีปรับปรุงประสิทธิภาพของอัลกอริทึมการเรียงลำดับด้วยการประมวลผลแบบขนานโดยใช้ CUDA
- สำรวจความเป็นไปได้ในการเพิ่มประสิทธิภาพด้วยการนำ merge sort พื้นฐานไปใช้งานด้วย CUDA
merge sort แบบเรียกซ้ำพื้นฐาน (การใช้งานบน CPU)
- เป็นอัลกอริทึมการเรียงลำดับที่แบ่งอาร์เรย์ออกเป็น 2 อาร์เรย์ย่อย จากนั้นเรียงลำดับแต่ละส่วนแล้วจึงรวมเข้าด้วยกัน
- แบ่งอาร์เรย์แบบเรียกซ้ำ และเมื่อขนาดเหลือ 1 จะทำขั้นตอนการรวม
- ประเด็นสำคัญในการใช้งาน
- ใช้
uint8_t→ ลดการใช้หน่วยความจำด้วยค่าขนาดเล็ก (0~255) - ใช้
long long→ รองรับอาร์เรย์ขนาดใหญ่ (สูงสุด 10¹⁸) - ตรวจสอบผลลัพธ์ด้วย
std::sortเพื่อเปรียบเทียบประสิทธิภาพ - ความซับซ้อนด้านเวลา: O(n log n)
- ความซับซ้อนด้านพื้นที่: O(n)
- ใช้
merge sort แบบเรียกซ้ำพื้นฐานใน CUDA
- ใช้รูปแบบเดียวกับการใช้งานบน CPU
- ออกแบบให้ขั้นตอนการรวมทำงานแบบขนานบน CUDA
- ประเด็นสำคัญในการใช้งาน
- ใช้
cudaMalloc,cudaMemcpy,cudaFree→ จัดสรรหน่วยความจำบน GPU และโอนย้ายข้อมูล merge<<<1, 1>>>(...)→ รันขั้นตอนการรวมแบบขนานcudaDeviceSynchronize()→ ซิงโครไนซ์จนกว่าการรวมจะเสร็จสิ้น- ปัญหาด้านประสิทธิภาพ → CUDA ไม่มีประสิทธิภาพกับการเรียกซ้ำ จึงจำเป็นต้องใช้แนวทางแบบวนซ้ำ
- ใช้
เปรียบเทียบการใช้งานบน CPU และ GPU
- ประสิทธิภาพลดลงเพราะการเรียกซ้ำทำงานบน CPU
- การเรียกซ้ำใน CUDA มีปัญหาเรื่องขนาดสแตกและ kernel execution overhead
- วิธีปรับปรุงประสิทธิภาพ: จำเป็นต้องเปลี่ยนเป็นแนวทางแบบวนซ้ำ (bottom-up)
merge sort แบบวนซ้ำจากล่างขึ้นบน (การใช้งานบน CPU)
- รวมอาร์เรย์ย่อยขนาดเล็กทีละขั้น → มีประสิทธิภาพมากกว่าใน CUDA
- เพิ่มขนาดของอาร์เรย์ที่นำมารวมเป็น 1, 2, 4, 8, … แล้วรวมต่อเนื่อง
- โครงสร้างโค้ดหลัก
MERGE_SORT(arr, temp, start, end) FOR sub_size ← 1 TO end STEP 2 × sub_size DO FOR left ← 0 TO end STEP 2 × sub_size DO mid ← MIN(left + sub_size - 1, end) right ← MIN(left + 2 * sub_size - 1, end) MERGE(arr, temp, left, mid, right) ENDFOR ENDFOR END MERGE_SORT - ประเด็นสำคัญในการใช้งาน
- หากขนาดอาร์เรย์ไม่เป็นจำนวนเท่าของ 2 จะแก้ปัญหาโดย clamp ดัชนี
- ทำขั้นตอนการรวมผ่านลูป
- มีศักยภาพในการเพิ่มประสิทธิภาพสูง
merge sort แบบวนซ้ำจากล่างขึ้นบน (การใช้งานบน CUDA)
- ปรับปรุงประสิทธิภาพด้วยการรัน merge sort แบบวนซ้ำให้ทำงานแบบขนาน
- เพื่อให้ขั้นตอนการรวมทำงานแบบขนาน จะคำนวณจำนวน thread และ block ก่อนแล้วจึงรัน
- โครงสร้างโค้ดหลัก
void mergeSort(uint8_t* arr, uint8_t* temp, long long n) { bool flipflop = true; long long size; for (size = 1; size < n; size *= 2) { numThreads = max(n / (2 * size), (long long)1); gridSize = (numThreads + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK; mergeKernel<<<gridSize, THREADS_PER_BLOCK>>>(flipflop ? arr : temp, flipflop ? temp : arr, size, n); CUDA_CHECK(cudaGetLastError()); CUDA_CHECK(cudaDeviceSynchronize()); flipflop = !flipflop; } if (!flipflop) CUDA_CHECK(cudaMemcpy(arr, temp, n * sizeof(uint8_t), cudaMemcpyDeviceToDevice)); } - ประเด็นสำคัญ
flipflop→ สลับตำแหน่งจัดเก็บผลลัพธ์ของการรวมgridSize,THREADS_PER_BLOCK→ ใช้สำหรับทำให้ขั้นตอนการรวมทำงานแบบขนานmergeKernel→ กำหนดงานรวมที่ไม่ซ้ำกันให้แต่ละ thread- จัดการดัชนีผ่านการคำนวณดัชนีของ thread และ block
ผลลัพธ์ด้านประสิทธิภาพ
- merge sort แบบ bottom-up (CUDA) → ปรับปรุงประสิทธิภาพได้ชัดเจน
- อาร์เรย์ขนาดเล็ก → CPU เร็วกว่า
- อาร์เรย์ขนาดใหญ่ → CUDA เหนือกว่าด้านประสิทธิภาพ
thrust::sort→ GPU มีประสิทธิภาพยอดเยี่ยมกับอาร์เรย์ขนาดใหญ่- การปรับปรุงประสิทธิภาพของ CUDA ถูก จำกัดด้วย overhead จากการโอนย้ายข้อมูล
บทสรุปและงานต่อไป
- ประสบความสำเร็จในการปรับปรุงประสิทธิภาพของ merge sort บนพื้นฐาน CUDA
- สิ่งสำคัญที่ได้เรียนรู้:
- เรียนรู้แนวคิดการประมวลผลแบบขนานของ CUDA และกลยุทธ์การจูนประสิทธิภาพ
- merge sort แบบวนซ้ำ → มีประสิทธิภาพบน CUDA มากกว่าแนวทางแบบเรียกซ้ำ
- พบคอขวดด้านประสิทธิภาพเฉพาะของ CUDA เช่น การซิงโครไนซ์ thread และการโอนย้ายหน่วยความจำ
- งานปรับปรุงในอนาคต:
- แยกและปรับแต่งงานระหว่าง CPU-GPU
- ทดสอบประสิทธิภาพกับอาร์เรย์ที่ใหญ่ขึ้น
- ผสาน
thrust::sortเข้ากับโค้ดที่พัฒนาขึ้นเอง - ปรับแต่งประสิทธิภาพด้วยการใช้ shared memory
2 ความคิดเห็น
ดูเหมือนว่าจะมีการทำเป็น radix sort ไว้ใน CUDA และผมก็เคยมีประสบการณ์ลองทำตามตัวอย่างอ้างอิงแบบเดียวกันเลยครับ
ความคิดเห็นจาก Hacker News
ไม่ใช่วิธีจัดเรียงบน GPU ที่เร็วที่สุด อัลกอริทึมที่เร็วที่สุดบน CUDA คือ Onesweep ซึ่งใช้เทคนิคซับซ้อนเพื่อดึงความขนานของ GPU มาใช้และก้าวข้ามข้อจำกัดต่าง ๆ
เช่นเดียวกับความเห็นอื่น ๆ อัลกอริทึมนี้ไม่เหมาะสม แม้อัลกอริทึมอย่าง Onesweep จะน่าทึ่ง แต่ก็เข้าใจได้ยาก
เป็นปัญหาแนวของเล่นที่หยิบมาลองเล่นได้สนุก หากใช้ตัวเลือกการปรับจูน thread ก็อาจช่วยเพิ่มประสิทธิภาพได้
ภาษา Futhark ทำให้อัลกอริทึมลักษณะนี้ใช้งานบน GPU ได้สะดวกขึ้น
ทำให้นึกถึงโปรเจ็กต์เล็ก ๆ ตอนเรียนมหาวิทยาลัยที่เคยเขียน bitonic sort บน CUDA
โน้ตที่อธิบายแนวคิดการทำ indexing ของ thread บน GPU นั้นดี
ชอบ implementation ของ radix sort ที่เร็ว
เคยพยายามใช้ร่วมกับ Unity แต่ไม่สามารถเอาชนะคอขวดจากการส่งข้อมูลได้
ถ้าจะให้คุ้มกับการจัดเรียงบน GPU ก็ต้องเป็นอาร์เรย์ขนาดใหญ่
สรุปแบบประหยัดเวลา: มีคนเขียนอัลกอริทึมจัดเรียงบน GPU ขึ้นมา แต่ช้า