เจาะลึก 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) การทำงานสามารถสรุปเป็นขั้นตอนสั้นๆ ได้ดังนี้:

  1. สร้าง Attention Scores: คำนวณ matmul(q, k.T)
  2. ปรับ Scale Scores: นำคะแนนที่ได้มาคูณด้วยค่า scale scores * scale
  3. ใช้ Causal Mask: นำ mask มาใช้กับคะแนน scores.masked_fill(mask, "-inf") (ในกรณีที่ต้องการบังคับลำดับการประมวลผล)
  4. Normalize Scores ด้วย Softmax: เพื่อให้ได้ Attention Weights softmax(scores)
  5. 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)

![Comparison of traces with and without in-place operation](ขอบคุณ แหล่งข้อมูล
https://huggingface.co/blog/assets/torch-attention-profile/inplacevsoutofplace.png)
(ภาพประกอบ: เปรียบเทียบ 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! สาเหตุหลักมาจาก:

  1. การ Upcast เป็น FP32: Math backend มักจะแปลง tensor เป็น FP32 ก่อนคำนวณ ทำให้ช้าลงและใช้หน่วยความจำมากขึ้นเมื่อเทียบกับการใช้ bfloat16 (bf16) ใน Naive Attention ที่ใช้ Tensor Cores ได้เต็มที่
  2. การสร้าง Mask ใหม่ทุกครั้ง: การใช้ is_causal=True ทำให้ SDPA สร้าง Mask ขึ้นมาใหม่ทุกครั้งที่เรียกใช้ แทนที่จะใช้ Mask ที่คำนวณไว้แล้วเหมือนใน Naive Attention
  3. การใช้ 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 ของ NVIDIA
  • bf16_aligned: ทำงานในโหมด bfloat16
  • 64x64: ขนาด tile
  • rf (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

เจาะลึก 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โครงสร้างพื้นฐานของ AttentionAttention ทำงานโดยอาศัยองค์ประกอบหลัก 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.TKernel mul สำหรับการ scaleOperation สำหรับการทำ maskingKernel 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)![Comparison of traces with and without in-place operation](https://huggingface.co/blog/assets/torch-attention-profile/inplacevsoutofplace.png)(ภาพประกอบ: เปรียบเทียบ 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 passScaled 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 operationsSDPA (Math Backend): เป็น Baseline ที่ปลอดภัยและถูกต้อง แต่ไม่เร็วที่สุดEfficient Backend (xformers) และ FlashAttention-2: คือตัวเลือกที่ให้ประสิทธิภาพสูงสุด โดยอาศัยเทคนิคการ Fused Kernel และการจัดการหน่วยความจำอย่างชาญฉลาดการเลือกใช้ PyTorch Profiler จะช่วยให้นักพัฒนาสามารถวิเคราะห์หาจุดคอขวดและปรับปรุงประสิทธิภาพของโมเดลได้อย่างตรงจุด ทำให้สามารถสร้างสรรค์ AI ที่ทรงพลังและทำงานได้รวดเร็วยิ่งขึ้น#PyTorch #Profiling #Attention #Transformer #AI #MachineLearninghttps://huggingface.co/blog/torch-attention-profile
Shared content
HUGGINGFACE.CO
Profiling in PyTorch (Part 3): Attention is all you profile
We’re on a journey to advance and democratize artificial intelligence through open source and open science.
6 Commentarii 0 Distribuiri 758 Views 0 previzualizare