24 คะแนน โดย GN⁺ 2025-06-25 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 ความคิดเห็น

 
GN⁺ 2025-06-25
ความคิดเห็นบน 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 เอง แต่เป็นวิธีนำมันไปใช้กับการคำนวณด้านแมชชีนเลิร์นนิง

    • มีการชี้ว่าโดยเนื้อหาแล้วมันแทบจะเป็นเพียงสรุปทั่วไปของ CUDA มากกว่า และนอกจากตัวอย่าง relu กับการกล่าวถึง torch ก็แทบไม่เกี่ยวกับแมชชีนเลิร์นนิงเท่าไร
  • มีความเห็นว่าควรใช้สีที่มีคอนทราสต์อย่างแน่นอน เพื่อให้ตัวอักษรอ่านง่าย

    • แชร์ประสบการณ์การใช้ 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