- GPU มีความเร็วในการคำนวณสูงกว่าความเร็วในการเข้าถึงหน่วยความจำอย่างมาก ทำให้ ลำดับชั้นของหน่วยความจำ กลายเป็นคอขวดของประสิทธิภาพ
- ตาม ความเข้มข้นของการคำนวณ (Arithmetic Intensity, AI) งานคำนวณจะแบ่งได้เป็นแบบติดข้อจำกัดที่หน่วยความจำและแบบติดข้อจำกัดที่การคำนวณ โดยจุดวิกฤตของ A100 GPU อยู่ที่ประมาณ 13 FLOPs/Byte
- กลยุทธ์หลักของการ ปรับแต่งประสิทธิภาพ ได้แก่ Fusion และ Tiling; Fusion ช่วยลดการไป-กลับของหน่วยความจำที่ไม่จำเป็น ส่วน Tiling ช่วยเพิ่มการนำข้อมูลกลับมาใช้ซ้ำให้สูงสุด
- การเข้าใจลักษณะเชิงโครงสร้างของฮาร์ดแวร์ GPU เช่น การซิงโครไนซ์, Coalesced Load, การแก้ปัญหา bank conflict เป็นสิ่งสำคัญต่อการเขียนเคอร์เนลสมรรถนะสูง
- ปัจจัยเพิ่มเติม เช่น Occupancy, การลด thread divergence, Quantization ล้วนส่งผลสำคัญต่อประสิทธิภาพจริง
โครงสร้างลำดับชั้นด้านคอมพิวต์และหน่วยความจำของ GPU
- โดยทั่วไป GPU มีความเร็วในการประมวลผลเชิงเลขสูงกว่าแบนด์วิดท์หน่วยความจำอย่างมาก
- ตัวอย่างเช่น NVIDIA A100 ให้ประสิทธิภาพราว 19.5 TFLOPS (เลขทศนิยมลอยตัว 32 บิต) แต่มีแบนด์วิดท์หน่วยความจำประมาณ 1.5TB/s
- ระหว่างที่อ่านข้อมูล 4 ไบต์ สามารถทำการคำนวณได้หลายสิบครั้ง ดังนั้นการเคลื่อนย้ายข้อมูลจึงเป็นคอขวดของประสิทธิภาพ
- Global Memory (VRAM) คือหน่วยความจำ off-chip ที่ช้าและเป็นที่อยู่ของข้อมูลทั้งหมด ขณะที่ Streaming Multiprocessor(SM) ทำหน้าที่ด้านการคำนวณ
- แต่ละ SM มี Shared Memory(SRAM) แบบ on-chip ความเร็วสูง ซึ่งสามารถใช้เป็น แคชที่โปรแกรมจัดการเองโดยตรง ได้
- Thread คือหน่วยการประมวลผลที่เล็กที่สุด และแต่ละ thread จะมีชุด register เฉพาะตัว
- 32 threads รวมกันเป็น Warp และ Block คือกริดของ threads ที่จะรันอยู่บน SM เดียวกัน
ช่วงประสิทธิภาพ: memory-bound vs compute-bound
- ประสิทธิภาพของ kernel จะอยู่ในสถานะแบบ memory-bound (ถูกจำกัดด้วยความเร็วการย้ายข้อมูล) หรือ compute-bound (ถูกจำกัดด้วยความสามารถในการคำนวณของ SM)
- ความเข้มข้นของการคำนวณ (AI) นิยามเป็น Total FLOPs / Total Bytes Accessed และเป็นตัวชี้วัดสำคัญ
- โมเดล Roofline: กราฟที่แกน x คือ AI และแกน y คือ FLOPS/s ใช้แสดงประสิทธิภาพที่ kernel ทำได้จริง
- ถ้า AI ต่ำและเป็น memory-bound จะอยู่บนเส้นทแยงมุม (ระดับแบนด์วิดท์หน่วยความจำ)
- ถ้า AI สูงและเป็น compute-bound จะอยู่บนเส้นแนวนอน (ระดับสมรรถนะการคำนวณสูงสุด)
- Ridge Point ของ A100 คือ 19.5 TFLOPS / 1.5 TB/s ≈ 13 FLOPs/Byte
- การเพิ่ม AI จะช่วยเพิ่มประสิทธิภาพ และอาจทำให้ kernel ไปถึงสถานะ compute-bound ได้
กลยุทธ์ในการเพิ่มความเข้มข้นของการคำนวณ
- โมเดลง่ายๆ: 1 thread คำนวณ C[i,j] 1 ค่า → AI = 0.25 (ต่ำมาก, memory-bound)
- แม้ให้ thread คำนวณไทล์ขนาด 2x2 ก็ยังได้ AI = 0.5 (ยังต่ำอยู่)
- หากต้องการเพิ่ม AI จำเป็นต้องให้หลาย threads ร่วมกันโหลดไทล์ขนาดใหญ่เข้า Shared Memory ในระดับ block เพื่อเพิ่มการใช้ข้อมูลซ้ำให้สูงสุด
- ด้วยความร่วมมือของ threads ภายใน block สามารถเพิ่ม AI > 13 เพื่อเข้าสู่ช่วง compute-bound ได้
สถานะแบบ overhead-bound
- อาจเกิด overhead ในกระบวนการที่ CPU (โฮสต์) มอบหมายงานให้ GPU
- หาก GPU kernel มีขนาดเล็กเกินไปหรือมีจำนวนมากเกินไป GPU อาจต้องรอรับงานและเกิดช่วงว่าง
- เฟรมเวิร์กสมัยใหม่ใช้ การรันแบบอะซิงโครนัส โดยคิว command stream ล่วงหน้าเพื่อลด overhead ให้ต่ำที่สุด
สองกลยุทธ์หลักเพื่อเพิ่มประสิทธิภาพ: Fusion และ Tiling
Operator Fusion
- ในงานคำนวณแบบง่ายเป็นสาย เช่น y = relu(x + 1) หากแต่ละโอเปอเรชันทำงานเป็นคนละ kernel ข้อมูลจะต้องไป-กลับ global memory
- Fusion รวมหลายโอเปอเรชันเข้าเป็น kernel เดียว โดยไม่ต้องเก็บค่ากลางลงใน global memory แต่คำนวณใน register แล้วเขียนเฉพาะผลลัพธ์สุดท้าย
- ตัวอย่าง: คอมไพเลอร์ JIT เช่น Triton, torch.compile Inductor สามารถทำสิ่งนี้อัตโนมัติ
Tiling
- ในงานคำนวณซับซ้อนอย่าง การคูณเมทริกซ์ โมเดลแบบ thread เดียวจะมี AI ต่ำ
- หลังแบ่งไทล์เป็นระดับ block แล้ว ทุก threads ใน block จะร่วมกันโหลดไทล์ข้อมูลเข้า Shared Memory เพื่อให้เกิดการใช้ข้อมูลซ้ำในระดับสูง
- การคำนวณยึดตามแพตเทิร์น 3 ขั้นตอนคือ "Load(โกลบอล -> Shared Memory) - Synchronize(ซิงโครไนซ์) - Compute(คำนวณ)"
Coalesced Load และการเวกเตอร์ไรซ์
- เมื่อต้องย้ายข้อมูลจาก global memory ไปยัง Shared Memory สิ่งสำคัญคือ Coalesced Access (32 threads ในวาร์ปเข้าถึงช่วง 128 ไบต์ที่ต่อเนื่องกัน)
- การเวกเตอร์ไรซ์ (เช่น
float4) เพื่อโหลดหลายค่าพร้อมกัน ช่วยประหยัดทรัพยากรฮาร์ดแวร์และใช้แบนด์วิดท์หน่วยความจำได้เต็มที่
- การจัดแนวข้อมูล (alignment) เป็นสิ่งจำเป็น และค่า K ของจำนวนไบต์ในเมทริกซ์ควรเป็นพหุคูณของ 4 เพื่อให้มีประสิทธิภาพ
Shared Memory bank และ bank conflict
- Shared Memory ประกอบด้วย bank อิสระ 32 ตัว ดังนั้น 32 threads ในวาร์ปควรเข้าถึงคนละ bank เพื่อหลีกเลี่ยงการชนกัน
- การเข้าถึงแบบแถว ไม่เกิดการชน แต่ การเข้าถึงแบบคอลัมน์ จะทำให้เกิดการชน (เข้าถึง bank เดียวกัน)
- สำหรับไทล์ของ B จะใช้กลยุทธ์ "โหลดแล้ว transpose" โดยเก็บแบบ transpose ลง Shared Memory เพื่อให้ตอนคำนวณเข้าถึงแบบแถวและหลีกเลี่ยง bank conflict
แพตเทิร์นการคำนวณ on-chip ความเร็วสูง
กลยุทธ์พื้นฐาน 1: 1 thread คำนวณ 1 output
- ภายใต้ข้อจำกัด BLOCK_DIM=32 ค่า AI สูงสุดคือ 8 จึงไม่สามารถเข้าสู่สถานะ compute-bound ได้
กลยุทธ์ 2: 1 thread คำนวณหลาย output
- เมื่อตั้งค่า BLOCK_DIM=16 และ TILE_DIM=64 จะให้ 1 thread คำนวณเอาต์พุต 4x4 → AI=16
- เพราะ AI>13 จึงสามารถทำประสิทธิภาพแบบ compute-bound ได้บน A100
- สามารถคำนวณได้อย่างมีประสิทธิภาพด้วยการโหลดแบบเวกเตอร์จาก Shared Memory เช่น
float4
ข้อจำกัดจริงของการทำไทล์: tile quantization
- หากขนาดเมทริกซ์ไม่เป็นพหุคูณของขนาดไทล์ block ตามขอบจะคำนวณพื้นที่ใหญ่กว่าที่ต้องใช้จริง (เป็นการคำนวณเกินจำเป็น) และต้องมีการ padding
- threads บริเวณขอบจะใช้เงื่อนไข guard เพื่อป้องกันการเข้าถึงหน่วยความจำที่ไม่จำเป็น แต่ลูปคำนวณยังรันเหมือนเดิม ทำให้เกิดการคำนวณขยะ (เช่น C += A * 0)
องค์ประกอบเพิ่มเติมในการจูนประสิทธิภาพ
Occupancy และการซ่อน latency
- เมื่อวาร์ปต้องรอนาน เช่น ระหว่างอ่านหน่วยความจำ SM จะสลับไปทำงานกับวาร์ปอื่นทันทีเพื่อลดเวลาว่าง (ซ่อน latency, latency hiding)
- หากจัดสรรหลาย Thread Block พร้อมกัน จะช่วยเพิ่ม occupancy และลดเวลารอ
- แต่ถ้า block หรือขนาดไทล์ใหญ่เกินไป จำนวน resident block จะลดลง ทำให้ occupancy ลดและประสิทธิภาพตก
ลด thread divergence
- หากเกิดการแตกแขนง if-else ภายในวาร์ป จะต้องรันทั้งสองเส้นทางแบบลำดับ ทำให้ประสิทธิภาพเชิงผลลดลงเหลือประมาณครึ่งหนึ่ง
- จึงควรลดการแตกแขนงด้วยโค้ดแบบ branchless เช่น min, max
Quantization
- เมื่อลดความละเอียดจาก FP32 → FP16/BFP16 ปริมาณการย้ายข้อมูลและจำนวนข้อมูลที่ประมวลผลได้จะเพิ่มขึ้นเป็น 2 เท่าในแต่ละด้าน
- บน A100 การคำนวณ FP16 ทำได้ถึง 312 TFLOPS (สูงสุดราว 16 เท่าเมื่อเทียบกับ 19.5 TFLOPS ของ FP32)
- Quantization ช่วยให้บรรลุได้พร้อมกันทั้งการขยับไปทางขวาบน Roofline ด้าน AI (ประสิทธิภาพหน่วยความจำ) และด้านบน (สมรรถนะการคำนวณสูงสุด)
สรุปภาพรวม
- ข้อจำกัดพื้นฐานของประสิทธิภาพ GPU เกิดจากความไม่สมดุลระหว่าง แบนด์วิดท์หน่วยความจำ และ ความสามารถในการคำนวณบนชิป
- การเพิ่มประสิทธิภาพทำได้ผ่าน การใช้ข้อมูลซ้ำให้สูงสุด (Tiling) และการลดทราฟฟิกหน่วยความจำของค่ากลาง (Fusion)
- จำเป็นต้องเข้าใจโครงสร้างฮาร์ดแวร์ (warp, bank, coalesced access, synchronization) เพื่อเขียนและปรับแต่ง kernel สมรรถนะสูง
- ในทางปฏิบัติ ปัจจัยเพิ่มเติมอย่าง occupancy, การลด branch divergence, quantization ส่งผลโดยตรงต่อความเร็วจริง
- การออกแบบงานคำนวณบน GPU สมรรถนะสูงต้องพิจารณาร่วมกันทั้งการเพิ่ม AI ในเชิงทฤษฎี การใช้ประโยชน์จากฮาร์ดแวร์ และการรองรับการจัดวาง/ขนาดข้อมูลจริง
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
สงสัยว่าการปรับแต่งโปรแกรมทั้งระบบในระดับคอมไพเลอร์ทำได้ดีแค่ไหน และรู้สึกว่าวิธีปัจจุบันที่ไล่ปรับแต่งสถาปัตยกรรม LLM ทีละตัวดูเหมือนจะตามไม่ทันอะไรบางอย่าง
แชร์ประสบการณ์ลองรัน llama.cpp และ vllm บน 4070 ตัวเดียวกันเพื่อประมวลผลพรอมป์ต์แบบ batch ให้มากขึ้น โดยตั้งแต่ batch 8 ขึ้นไป llama.cpp ช้าลงอย่างหนัก และแม้การใช้งาน GPU จะดูโอเค แต่จริง ๆ แล้วเกิดคอขวดขึ้น ส่วน vllm รับมือได้ดีกว่ามากอย่างชัดเจน
vllm ใช้ paged kv cache และเลย์เอาต์แบบ fully coalesced ที่ GPU ชอบ จึงให้ประสิทธิภาพที่เหมาะกับงาน batch ในขณะที่ llama.cpp ใช้ flat layout ที่ดีสำหรับพรอมป์ต์เดี่ยว ทำให้ในสถานการณ์ batch รูปแบบการเข้าถึงหน่วยความจำ L2 พังและความเร็วตก
แชร์ประสบการณ์ว่าเมื่อ interleave kv tensor ใน llama.cpp จาก [seq, head, dim] เป็น [head, seq, dim] เพื่อเลียนแบบวิธีที่ vllm ป้อนข้อมูลเข้า fused attention kernel ก็ทำให้ประสิทธิภาพการคำนวณดีขึ้นทันทีราว 2 เท่า
สาเหตุของคอขวดไม่ได้อยู่ที่ตัว GPU เอง แต่อยู่ที่วิธีเข้าถึง shared memory และการออกแบบ global read ซึ่ง vllm แก้จุดนี้ตรง ๆ ด้วยการเปลี่ยนเลย์เอาต์
ใช้เวลามากกว่า 2 วันกว่าจะวิเคราะห์คอขวดนี้ได้ และดูจากกราฟการใช้งาน GPU อย่างเดียวไม่สามารถรู้ได้ โดยส่วนใหญ่ต้องเรียนรู้จากการลองผิดลองถูก
ตั้งคำถามว่ามีวิธีไหนที่จะทำให้การทดลองแบบนี้ทำซ้ำได้ง่ายขึ้นในลักษณะ hot reload หรือไม่
มีการชี้ว่าถึงจะบอกว่า GPU ไม่ใช่คอขวด แต่ในความเป็นจริงความไม่มีประสิทธิภาพของ memory layout ก็เป็นคอขวดที่ทำให้ประสิทธิภาพการคำนวณของ GPU ลดลงอยู่ดี
กล่าวถึงโปรเจ็กต์ nano-vllm ที่พนักงาน deepseek เพิ่งเปิดเผยเมื่อวาน โดยมีโค้ดเพียง 1,200 บรรทัด แต่ทำความเร็วได้ดีกว่า vanilla vllm พร้อมแชร์ข่าว https://github.com/GeeeekExplorer/nano-vllm
ถามว่าได้ส่งเลย์เอาต์ที่เปลี่ยนใน llama.cpp เป็น pull request หรือยัง เพราะการดีขึ้น 2 เท่าน่าจะเป็นประโยชน์อย่างมากกับทุกคน
แนะนำให้ลองโปรเจ็กต์ ik_llama.cpp ด้วย https://github.com/ikawrakow/ik_llama.cpp
มองว่าเป็นบทความที่มีข้อมูลดี และเนื้อหานี้คือเรื่องของสิ่งที่ NVIDIA เลือกตอนพัฒนาสถาปัตยกรรม GPU โดยย้ำว่าอย่าเข้าใจผิดว่าเป็นความแตกต่างจากผู้ผลิตรายอื่น
ยกตัวอย่างว่า AMD Instinct MI300 มี ridge-point เปลี่ยนไปด้วยสเปกสูงสุด FP32 ที่ 160 TFLOPS และแบนด์วิดท์ HBM3/3E 6TB/s ซึ่งเท่ากับ 27 FLOPs/byte มากกว่าสองเท่าของ A100 ที่ 13 FLOPs/byte และหน่วยความจำ HBM ขนาดใหญ่ (128~256GB) ก็เปลี่ยน trade-off ที่เกิดขึ้นจริงระหว่าง tiling depth กับ occupancy ด้วย เพียงแต่ GPU แบบนี้มีราคาแพง และมี trade-off เรื่องไม่รองรับ CUDA
มีความเห็นว่าจนกว่า AMD จะใส่ใจกับซอฟต์แวร์ด้านคอมพิวต์มากกว่านี้ NVIDIA GPU ก็คงยังเป็นตัวเลือกที่มีบทบาทเด่นอยู่ดี
สปอยล์คือ สิ่งที่สำคัญจริง ๆ ไม่ใช่หลักการทำงานของ GPU เอง แต่เป็นวิธีนำมันไปใช้กับการคำนวณด้านแมชชีนเลิร์นนิง
มีความเห็นว่าควรใช้สีที่มีคอนทราสต์อย่างแน่นอน เพื่อให้ตัวอักษรอ่านง่าย
แชร์ประสบการณ์การใช้ font-weight: 300 โดยบอกว่านักออกแบบฝั่ง Mac ส่วนใหญ่พัฒนางานตามตัวเลือก font smoothing จึงตั้งค่าให้โดยทั่วไปดูเหมือน "normal" แต่บน Mac ฟอนต์บางจะถูกแสดงให้ดูหนาขึ้นครึ่งหนึ่ง ทำให้นักออกแบบมักใช้ฟอนต์ที่บางกว่าเพื่อให้ได้ความรู้สึกแบบ "ปกติ" พร้อมแชร์ลิงก์ที่เกี่ยวข้อง https://news.ycombinator.com/item?id=23553486
คาดว่าอาจเป็นไปได้ว่าผู้เขียนแก้ไขและจัดรูปแบบในโหมดมืด โดยระบุว่าถ้าเปิด edge://flags/#enable-force-dark แล้วลิงก์จะมองเห็นได้ดี
ชี้ว่าผู้อ่านต้องใช้ความพยายามมากเป็นพิเศษในการอ่านลิงก์และคอมเมนต์ใน code block และเสนอให้เพิ่มคอนทราสต์ แต่ก็ประเมินว่าคุณภาพของเนื้อหานั้นยอดเยี่ยมมาก
วิจารณ์ว่าเว็บไซต์ใช้ alpha transparency กับข้อความ ซึ่งเป็นความผิดพลาดใหญ่ที่ทำให้คอนทราสต์ลดลงอย่างรุนแรง
มีข้อเสนอว่าชื่อบทความควรใกล้เคียงกับ "ข้อเท็จจริงพื้นฐานเกี่ยวกับ Nvidia GPU" มากกว่า โดยอธิบายเพิ่มเติมว่าคำว่า WARP ก็เป็นคำเฉพาะของ Nvidia GPU ยุคใหม่ และ Nvidia GPU ราวปี 2003 ยังเป็นฮาร์ดแวร์สำหรับเรนเดอร์วิดีโอเกมเท่านั้น ซึ่งต่างจาก GPU สำหรับการประมวลผลอเนกประสงค์ในปัจจุบันโดยสิ้นเชิง สรุปแล้วเนื้อหาในโพสต์นี้ไม่ใช่คำอธิบายทั่วไปที่ใช้ได้กับ GPU ทุกตัว
มีความเห็นขอบคุณว่าเป็นสื่อสำหรับผู้เริ่มต้นที่ดีมาก โดยเล่าว่าตอนประกอบ AI PC เองต้องใช้เวลาหลายวันศึกษาข้อมูลเรื่อง GPU และบทความนี้ช่วยสรุปทั้งแก่นสำคัญที่จำเป็นต้องรู้และกรณีใช้งานมูลค่าสูงอย่าง generative AI ได้ดีมาก โดยเฉพาะไดอะแกรมลำดับชั้นหน่วยความจำของ A100 ที่มีประโยชน์มาก
สงสัยว่าทำไมถึงใช้ไดอะแกรม ASCII