เจาะลึก PyTorch Profiler: วิเคราะห์ประสิทธิภาพ Attention Mechanism 🔍
การทำความเข้าใจและปรับปรุงประสิทธิภาพของโค้ด PyTorch เป็นสิ่งสำคัญอย่างยิ่ง โดยเฉพาะอย่างยิ่งเมื่อต้องจัดการกับโมเดลที่ซับซ้อนและมีขนาดใหญ่ ในซีรีส์ "Profiling in PyTorch" เราได้สำรวจวิธีการอ่าน trace และใช้ข้อมูลนั้นเพื่อปรับปรุงโค้ด ในบทความที่สามนี้ เราจะเจาะลึกหัวใจสำคัญของสถาปัตยกรรม Transformer นั่นคือ Attention Mechanism
ความสำคัญของ Attention Mechanism ในโมเดลยุคใหม่
Attention Mechanism ได้รับการยอมรับอย่างกว้างขวางว่าเป็นองค์ประกอบสำคัญที่ทำให้โมเดลอย่าง Transformers ประสบความสำเร็จอย่างสูงในงานด้าน Natural Language Processing (NLP) และ Computer Vision แม้ว่าจะมีชื่อเสียงในเรื่องความซับซ้อนเชิงเวลาแบบยกกำลังสอง (quadratic-time complexity) แต่ก็มีเทคนิคและกลยุทธ์มากมายที่ถูกพัฒนาขึ้นเพื่อทำให้มันทำงานได้อย่างรวดเร็วและมีประสิทธิภาพ
เป้าหมายของเราในบทความนี้ไม่ใช่การอธิบายทุกเทคนิคอย่างละเอียด แต่เป็นการแสดงให้เห็นว่า แต่ละเทคนิคส่งผลต่อประสิทธิภาพอย่างไรเมื่อมองผ่าน PyTorch Profiler
โครงสร้างพื้นฐานของ Attention
Attention ทำงานโดยอาศัยองค์ประกอบหลัก 3 อย่าง คือ Queries (q), Keys (k), และ Values (v) การทำงานสามารถสรุปเป็นขั้นตอนสั้นๆ ได้ดังนี้:
- สร้าง Attention Scores: คำนวณ
matmul(q, k.T) - ปรับ Scale Scores: นำคะแนนที่ได้มาคูณด้วยค่า scale
scores * scale - ใช้ Causal Mask: นำ mask มาใช้กับคะแนน
scores.masked_fill(mask, "-inf")(ในกรณีที่ต้องการบังคับลำดับการประมวลผล) - Normalize Scores ด้วย Softmax: เพื่อให้ได้ Attention Weights
softmax(scores) - Reweight Values: นำ Attention Weights ไปคูณกับ Values
matmul(attn, v)
จะเห็นได้ว่า Attention คือการรวมกันของ Primitive Operations หลายอย่าง ซึ่งบางอย่างเราคุ้นเคยกันดีแล้ว (เช่น matmul) และบางอย่างก็สามารถระบุได้ง่าย
วิเคราะห์ Naive Attention ด้วย PyTorch Profiler
ลองมาเขียน Attention แบบง่ายๆ (Naive Attention) ใน PyTorch แล้วนำไป profiling กัน
สิ่งที่คาดหวังว่าจะเห็นใน Profiler Trace:
- Kernel
matmulสำหรับq . k.T - Kernel
mulสำหรับการ scale - Operation สำหรับการทำ masking
- Kernel
matmulสำหรับatten . v
เมื่อเปิดดู trace ของ attnfwd (การเรียกใช้ฟังก์ชัน forward ที่เรากำหนดเอง) เราจะพบว่ามี Operation ตามที่คาดการณ์ไว้จริง ๆ ทั้ง matmul, maskedfill (สำหรับการ masking) และ softmax
การแกะรอย GPU Lane: ทำความเข้าใจ Kernel ที่ถูกเรียกใช้
เมื่อเราขยายส่วน GPU lane ของ profiler trace เราจะเห็น Kernel ที่ถูกเรียกใช้จริง ๆ สำหรับแต่ละขั้นตอน:
matmul(query and key)softmax(สร้าง attention weights)matmul(attention weights and values)
แต่ที่น่าสนใจคือ เราพบ Kernel ที่ชื่อว่า memcpy ซึ่งไม่ใช่ส่วนหนึ่งของขั้นตอนคำนวณหลักที่คาดหวังไว้
In-place Operations: พลิกเกมประหยัดทรัพยากร 💾
Kernel memcpy นี้มักเกิดขึ้นเมื่อ PyTorch ทำงานแบบ Out-of-place คือสร้างสำเนาของ tensor ขึ้นมาใหม่ก่อนทำการคำนวณ ในกรณีนี้ masked_fill เป็นส่วนที่ทำให้เกิด memcpy
ลองเปลี่ยนมาใช้ In-place operation โดยเปลี่ยนจาก maskedfill เป็น maskedfill (สังเกตขีดล่าง ที่บ่งบอกว่าเป็น in-place operation)

(ภาพประกอบ: เปรียบเทียบ trace ของการทำ masking แบบ out-of-place และ in-place)
เมื่อตรวจสอบ trace อีกครั้ง จะพบว่า:
- CPU Lane: จำนวน CPU ops ที่เกี่ยวข้องกับการ masking ลดลงอย่างเห็นได้ชัด
- GPU Lane: Kernel
memcpyหายไป!
การเปลี่ยนแปลงเพียงเล็กน้อยนี้ ช่วยลดการทำงานที่ไม่จำเป็นลงได้อย่างมาก ซึ่งหากนำไปใช้ในโมเดลขนาดใหญ่ที่มี Attention ซ้ำหลายชั้น ประหยัดทั้งเวลาและหน่วยความจำได้อย่างมหาศาล
ข้อควรจำ: การใช้ in-place operations จะปลอดภัยเมื่อไม่มีการคำนวณ gradient (เช่น ในโหมด torch.no_grad()) เพราะการเขียนทับข้อมูลเดิมอาจส่งผลต่อการคำนวณ backward pass
Scaled Dot Product Attention (SDPA): การรวมพลังของ PyTorch 🚀
PyTorch ได้รวมเอาเทคนิคต่างๆ ในการคำนวณ Attention ไว้ในฟังก์ชันเดียวคือ torch.nn.functional.scaleddotproduct_attention (SDPA) ฟังก์ชันนี้มีความสามารถในการเลือก backend ที่เหมาะสมที่สุดโดยอัตโนมัติเพื่อให้ได้ประสิทธิภาพสูงสุด
SDPA มีหลาย backend ที่แตกต่างกัน เช่น:
- Math backend: เป็น reference implementation ที่ตรงไปตรงมา แต่ช้าที่สุด
- FlashAttention-2: Implementation จาก Tri Dao ที่มีประสิทธิภาพสูง
- Efficient backend (xformers): Implementation ที่พัฒนาต่อยอดจาก Meta xformers
เมื่อ SDPA ทำงานช้ากว่าที่คาด? 🤔
สิ่งที่น่าประหลาดใจคือ เมื่อเราลองใช้ SDPA แบบง่ายๆ (โดยตั้งค่า is_causal=True) พบว่ามัน ช้ากว่า Naive Attention ที่เราเขียนขึ้นเองถึง 3.7 เท่า!
เมื่อเจาะลึก trace จะพบว่า Math backend ของ SDPA เรียกใช้ Kernel จำนวนมากถึง 20 Kernel ต่อ forward pass! สาเหตุหลักมาจาก:
- การ Upcast เป็น FP32: Math backend มักจะแปลง tensor เป็น FP32 ก่อนคำนวณ ทำให้ช้าลงและใช้หน่วยความจำมากขึ้นเมื่อเทียบกับการใช้ bfloat16 (bf16) ใน Naive Attention ที่ใช้ Tensor Cores ได้เต็มที่
- การสร้าง Mask ใหม่ทุกครั้ง: การใช้
is_causal=Trueทำให้ SDPA สร้าง Mask ขึ้นมาใหม่ทุกครั้งที่เรียกใช้ แทนที่จะใช้ Mask ที่คำนวณไว้แล้วเหมือนใน Naive Attention - การใช้
safesoftmax: เพื่อป้องกัน NaN ในกรณีที่ row ถูก mask ทั้งหมด SDPA เลือกใช้safesoftmaxซึ่งมี overhead มากกว่าaten::softmaxธรรมดา
สรุป: Math backend ของ SDPA ถูกออกแบบมาเพื่อความถูกต้องและครอบคลุมทุกกรณี (dtype-safe, NaN-safe) ทำให้เป็น baseline ที่ดี แต่ไม่ใช่ backend ที่เร็วที่สุด
ก้าวสู่ประสิทธิภาพสูงสุด: FlashAttention และ Efficient Backend ✨
เมื่อเปรียบเทียบกับ Math backend ที่มี 20 Kernels:
- Efficient Backend: เรียกใช้เพียง 1 Kernel เท่านั้น! คือ
fmhacutlassFbf16aligned64x64rfsm80ชื่อ Kernel นี้บ่งบอกถึง: fmha(fused multi-head attention): รวมทุก primitive ops เป็นหนึ่งเดียวcutlassF: สร้างบน CUTLASS template ของ NVIDIAbf16_aligned: ทำงานในโหมด bfloat1664x64: ขนาด tilerf(register file): เก็บข้อมูลใน register ที่เร็วที่สุดsm80: คอมไพล์สำหรับสถาปัตยกรรม Ampere (A100)
- FlashAttention-2 Backend: ก็เป็นอีกทางเลือกที่ยอดเยี่ยม โดยเป็น implementation ที่มีประสิทธิภาพสูงจาก Tri Dao
หัวใจสำคัญของ FlashAttention: คือการจัดการกับปัญหาคอขวดของ HBM (High Bandwidth Memory) ซึ่งเกิดจากการอ่าน-เขียนข้อมูลปริมาณมหาศาลระหว่างการคำนวณ Attention แทนที่จะคำนวณ Matrix เต็มๆ แล้วค่อยๆ ประมวลผล FlashAttention จะประมวลผลข้อมูลเป็นส่วนๆ (tiles) และใช้เทคนิค "online softmax" เพื่อลดการเข้าถึง HBM ทำให้เร็วขึ้นอย่างมาก
สรุป: การเลือกใช้ Attention ให้เหมาะสม
การทำความเข้าใจว่า Attention Mechanism ทำงานอย่างไร และแต่ละส่วนมีผลต่อประสิทธิภาพอย่างไรผ่าน PyTorch Profiler เป็นสิ่งสำคัญอย่างยิ่ง
- Naive Attention: ช่วยให้เห็นภาพรวมของ Primitive Operations และผลของการใช้ In-place operations
- SDPA (Math Backend): เป็น Baseline ที่ปลอดภัยและถูกต้อง แต่ไม่เร็วที่สุด
- Efficient Backend (xformers) และ FlashAttention-2: คือตัวเลือกที่ให้ประสิทธิภาพสูงสุด โดยอาศัยเทคนิคการ Fused Kernel และการจัดการหน่วยความจำอย่างชาญฉลาด
การเลือกใช้ PyTorch Profiler จะช่วยให้นักพัฒนาสามารถวิเคราะห์หาจุดคอขวดและปรับปรุงประสิทธิภาพของโมเดลได้อย่างตรงจุด ทำให้สามารถสร้างสรรค์ AI ที่ทรงพลังและทำงานได้รวดเร็วยิ่งขึ้น
#PyTorch #Profiling #Attention #Transformer #AI #MachineLearning
ขอบคุณ แหล่งข้อมูล
https://huggingface.co/blog/torch-attention-profile