การวิเคราะห์เชิงลึกของไลบรารีโอเพนซอร์ส cuDNN ฟร้อนท์เอนด์: เส้นทางวิวัฒนาการจาก API ระดับล่างสู่การเขียนโปรแกรม GPU ประสิทธิภาพสูง

ในเฟรมเวิร์กการเรียนรู้เชิงลึก การดำเนินการคอนโวลูชัน การทำนอร์มัลไลเซชัน หรือการคำนวณแอทเทนชันหนึ่งครั้ง บนพื้นผิวแล้วเป็นเพียงการเรียกใช้ฟังก์ชันหนึ่งบรรทัดใน Python อย่างไรก็ตาม ในระดับล่างของ GPU เบื้องหลังสิ่งนี้กลับซ่อนกลไกที่ซับซ้อนทั้งหมดไว้: ตัวอธิบายเทนเซอร์ ตัวอธิบายโอเปอเรเตอร์ ชนิดข้อมูล ขั้นตอน พื้นที่ทำงาน ฮิวริสติก โครงร่างเคอร์เนล ความเข้ากันได้ของเวอร์ชัน และการจัดการข้อผิดพลาด เป็นต้น

การจัดการโดยตรงกับ cuDNN Backend API ก็เหมือนกับการถือแบบแปลนอะไหล่มาประกอบเครื่องยนต์ด้วยตนเอง: ศักยภาพด้านประสิทธิภาพมหาศาล แต่ภาระทางวิศวกรรมก็สูงลิ่วเช่นกัน

  • NVIDIA/cudnn-frontend: นี่คือจุดเริ่มต้นโอเพนซอร์สสมัยใหม่ที่ NVIDIA สร้างขึ้นสำหรับไลบรารี cuDNN มันไม่ใช่แค่ส่วนหน้าส่วนติดต่อเท่านั้น แต่ยังเป็นชุดรวมเคอร์เนลโอเพนซอร์สประสิทธิภาพสูงที่เพิ่มขึ้นเรื่อยๆ ครอบคลุมการคูณจุดแบบปรับขนาด (SDPA / Fast Attention) การคูณเมทริกซ์แบบกลุ่มที่ผสานรวมสำหรับการฝึก Mixture of Experts (MoE) และการผสานรวมนอร์มัลไลเซชันกับฟังก์ชันกระตุ้น เป็นต้น
  • โปรเจกต์นี้มีอินเทอร์เฟซ C++ แบบ Header-Only และอินเทอร์เฟซ Python (ผสานรวมกับ PyTorch ดั้งเดิม) ที่มุ่งเป้าไปที่ NVIDIA ซึ่งใช้ Graph API ของ cuDNN รองรับความแม่นยำ FP16, BF16, FP8 และ MXFP8 และเข้ากันได้กับสถาปัตยกรรม GPU Hopper (H100/H200) และ Blackwell (B200/GB200/GB300)
  • ที่เก็บโค้ด: https://github.com/NVIDIA/cudnn-frontend
  • เอกสารทางการ: https://docs.nvidia.com/deeplearning/cudnn/frontend/latest/
  • บทความนี้ประมาณ 7,000 คำ ใช้เวลาอ่าน 32 นาที ความยาวพอดแคสต์ 37 นาที

คำแนะนำที่เกี่ยวข้อง

โปรเจกต์ NVIDIA/cudnn-frontend มีเป้าหมายเพื่อแก้ไขข้อขัดแย้งหลักนี้ มันไม่ใช่แค่ “น้ำตาลสังเคราะห์” ธรรมดา แต่สร้างอินเทอร์เฟซ C++ แบบ Header-Only และ Python ที่เน้นกราฟโอเปอเรเตอร์ขึ้นบน cuDNN Backend API ผู้ใช้สามารถอธิบายขั้นตอนการคำนวณผ่าน Graph, Tensor, Operation และ Execution Plan ในขณะที่ส่วนเบื้องหลังจะดำเนินการตรวจสอบความถูกต้อง การสร้างแผนงานตัวเลือก การตัดสินใจสนับสนุน การสร้างและการดำเนินการโดยอัตโนมัติ

รูปด้านล่างแสดงประสิทธิภาพของการแพร่กระจายไปข้างหน้าและย้อนกลับสไตล์ Llama 3.1 ที่ใช้ causal mask บน GB300 แผนภูมิแบ่งออกเป็นสองส่วนคือ Forward และ Backward แกนนอนแสดงความยาวลำดับ (จาก 2048 ถึง 32768) แกนตั้งแสดงพลังการคำนวณ (TFLOPS) และเปรียบเทียบความแม่นยำสามแบบคือ BF16, MXFP8 และ FP8 ข้อมูลบ่งชี้ว่าเมื่อความยาวลำดับเพิ่มขึ้น พลังการคำนวณของทุกความแม่นยำมีแนวโน้มเพิ่มขึ้น โดยที่ความแม่นยำ FP8 มีข้อได้เปรียบเด่นชัดที่สุด: ในการดำเนินการไปข้างหน้า เมื่อความยาวลำดับคือ 32768 FP8 ทำได้ถึง 3108 TFLOPS และการดำเนินการย้อนกลับทำได้ 2128 TFLOPS ซึ่งสูงกว่า MXFP8 และ BF16 อย่างมีนัยสำคัญ MXFP8 มีประสิทธิภาพรองลงมา และโดยรวมก็ดีกว่า BF16 อย่างชัดเจนเช่นกัน สิ่งนี้แสดงให้เห็นอย่างเต็มที่ว่าในสถานการณ์ลำดับยาว ความแม่นยำต่ำ (FP8/MXFP8) สามารถเพิ่มประสิทธิภาพพลังการคำนวณได้อย่างมาก และประสิทธิภาพโดยรวมของการดำเนินการไปข้างหน้าดีกว่าการย้อนกลับ ซึ่งสอดคล้องกับลักษณะพลังการคำนวณของการฝึกอบรมเชิงลึก

ข้อมูลอีกชุดหนึ่งแสดงการแพร่กระจายไปข้างหน้าและย้อนกลับสไตล์ Deepseek v3 ซึ่งใช้ causal mask เช่นกัน (GB300) การกำหนดค่าทดสอบคือ batch=2, multi-head 128/128, มิติ 192/128 แบ่งเป็น Forward และ Backward เช่นกัน แกนนอนคือความยาวลำดับ (2048-32768) แกนตั้งคือพลังการคำนวณ (TFLOPS) เปรียบเทียบความแม่นยำ BF16, MXFP8, FP8 ผลลัพธ์แสดงให้เห็นว่าเมื่อความยาวลำดับเพิ่มขึ้น พลังการคำนวณของแต่ละความแม่นยำเพิ่มขึ้นอย่างมีนัยสำคัญ ข้อได้เปรียบของความแม่นยำ FP8 เด่นชัดที่สุด: การดำเนินการไปข้างหน้าที่ความยาวลำดับ 32768 ทำได้ถึง 3364 TFLOPS การดำเนินการย้อนกลับทำได้ 2406 TFLOPS ประสิทธิภาพเหนือกว่า MXFP8 และ BF16 อย่างมาก MXFP8 รองลงมา และก็ดีกว่า BF16 อย่างชัดเจนเช่นกัน สิ่งนี้ยืนยันอีกครั้งถึงการเพิ่มพลังการคำนวณของความแม่นยำต่ำในสถานการณ์ลำดับยาว และประสิทธิภาพโดยรวมของการดำเนินการไปข้างหน้าสูงกว่าการย้อนกลับ ซึ่งสอดคล้องกับลักษณะพลังการคำนวณของการฝึกอบรมโมเดล

บทความนี้จะเริ่มจากสถาปัตยกรรม โค้ด ห่วงโซ่การดำเนินการ และตัวอย่างเฉพาะ درد剖析ว่ามันเปลี่ยนความสามารถของเคอร์เนลประสิทธิภาพสูงให้เป็นอินเทอร์เฟซระบบที่สามารถรวมเข้าได้ แคชได้ และปรับแต่งได้อย่างไร

สารบัญบทความนี้

  • หนึ่ง: เริ่มต้นอย่างรวดเร็ว: เรียกใช้ก่อน แล้วค่อยทำความเข้าใจ
  • สอง: ปัญหาที่โปรเจกต์แก้ไขจริงๆ: ไม่ใช่การห่อหุ้ม API แต่เป็นการห่อหุ้มความซับซ้อน
    • 2.1 ความแข็งแกร่งและภาระหนักของ cuDNN backend API
    • 2.2 จาก wrapper สู่ชั้นอินเทอร์เฟซระบบ AI
  • สาม: โครงสร้างคลัง: สามสายหลัก C++ Frontend, Python Binding และ Samples
    • 3.1 การแบ่งหน้าที่ของไดเรกทอรีระดับบนสุด
    • 3.2 include: ตำแหน่งของนามธรรม C++ หลัก
    • 3.3 python: นำอินเทอร์เฟซกราฟสู่ระบบนิเวศ PyTorch
  • สี่: ปรัชญาการออกแบบ Graph API: ใช้คำอธิบายกราฟแบบ Declarative แทนการประกอบ Descriptor แบบ Procedural
    • 4.1 Graph คือ “Operator IR” ฝั่งผู้ใช้
    • 4.2 Fluent builder: ทำให้การตั้งค่าแอตทริบิวต์กลายเป็นความหมายแบบลูกโซ่ที่อ่านง่าย
  • ห้า: ห่วงโซ่การดำเนินการ: จากการประกาศเทนเซอร์สู่การดำเนินการเคอร์เนล GPU
    • 5.1 วงจรชีวิตหกขั้นตอน
    • 5.2 build(handle, heuristics) เป็นเส้นทางลัดที่ใช้บ่อย
  • หก: ตัวอย่าง SDPA: Flash Attention เข้าสู่อินเทอร์เฟซกราฟแบบรวมได้อย่างไร
    • 6.1 Attention ไม่ใช่ชุดเคอร์เนลเปลือยอีกต่อไป แต่เป็น operation เชิงความหมาย
    • 6.2 การผูกเทนเซอร์เอาต์พุตกับรันไทม์
  • เจ็ด: แคช รูปร่างไดนามิก และ CUDA Graph: ความสามารถทางวิศวกรรมสำหรับระบบจริง
    • 7.1 Graph key และ cache ที่ผู้ใช้ดูแล
    • 7.2 ExecutionPlanCache: แนวคิดการแคชแผนในระดับที่ต่ำกว่า
    • 7.3 รูปร่างไดนามิกและ KernelCache
  • แปด: อินเทอร์เฟซ Python: ห่อหุ้ม cuDNN Graph ให้เป็นวิธีการใช้งานที่ใกล้เคียงกับเฟรมเวิร์กมากขึ้น
    • 8.1 PyGraph ถือครองคุณสมบัติ Graph, handle, callback และ device
    • 8.2 Python execute: ส่งคืนการแมปจาก UID ไปยังพอยน์เตอร์ไปยัง C++
  • เก้า: ประสิทธิภาพและความถูกต้อง: รายละเอียดที่อินเทอร์เฟซประสิทธิภาพสูงต้องจัดการอย่างชัดเจน
    • 9.1 การแบ่งชั้นชนิดข้อมูล: IO, intermediate, compute
    • 9.2 layout และ stride: ขอบเขตร่วมของประสิทธิภาพและความถูกต้อง
    • 9.3 workspace: ส่วนหนึ่งของแผน ไม่ใช่ของแถม
    • 9.4 การจัดการข้อผิดพลาดและความเข้ากันได้ของเวอร์ชัน
  • สิบ: การเปรียบเทียบกับสถาปัตยกรรมคอมไพเลอร์: Frontend คือชุดไปป์ไลน์คอมไพล์น้ำหนักเบาสำหรับ GPU Operator
    • 10.1 Graph คือ IR, heuristics คือจุดเริ่มต้นการปรับให้เหมาะสมตามเป้าหมาย
    • 10.2 มันไม่ได้แทนที่เฟรมเวิร์กการเรียนรู้เชิงลึก แต่อยู่ระหว่างเฟรมเวิร์กกับเคอร์เนล
  • สิบเอ็ด: ขอบเขตการใช้งาน: เมื่อไหร่ควรใช้ เมื่อไหร่ไม่ควรใช้โดยตรง
    • 11.1 สถานการณ์ที่เหมาะสม
    • 11.2 สถานการณ์ที่ไม่เหมาะสม
  • บทสรุป: มันเปลี่ยนเคอร์เนลประสิทธิภาพสูงจากเครื่องมือผู้เชี่ยวชาญเป็นส่วนประกอบของระบบ

หนึ่ง: เริ่มต้นอย่างรวดเร็ว: เรียกใช้ก่อน แล้วค่อยทำความเข้าใจ

ตำแหน่งของโปรเจกต์และการเริ่มต้นใช้งานอย่างรวดเร็ว

เอกสาร README ของโปรเจกต์ cuDNN Frontend อธิบายตำแหน่งของมันอย่างชัดเจน: มันมีอินเทอร์เฟซ C++ แบบ Header-Only และอินเทอร์เฟซ Python ที่ให้บริการเฉพาะ cuDNN Graph API และครอบคลุมความสามารถของเคอร์เนลประสิทธิภาพสูงบนสถาปัตยกรรม GPU รุ่นใหม่ เช่น Hopper, Blackwell อย่างครบถ้วน จากเส้นทางการใช้งาน สามารถแบ่งออกเป็นสองทิศทางหลักคือ Python และ C++

สำหรับผู้ใช้ Python การเริ่มต้นใช้งานนั้นตรงไปตรงมามาก:

pip install nvidia-cudnn-frontend  

ข้อกำหนดสภาพแวดล้อมพื้นฐานรวมถึง Python 3.9 ขึ้นไป, NVIDIA Driver, CUDA Toolkit และ cuDNN ตามเอกสาร README ข้อกำหนดเวอร์ชัน cuDNN ขั้นต่ำคือ 8.5.0 คู่มือการติดตั้งโดยละเอียดเพิ่มเติมสามารถดูได้จากเอกสารทางการ: NVIDIA cuDNN Frontend Documentation[1]

สำหรับผู้ใช้ C++ จะได้รับประโยชน์หลักจากคุณสมบัติ Header-Only:

#include <cudnn_frontend.h>  

ในขณะคอมไพล์ เพียงแค่ชี้พาธ include ไปยังไดเรกทอรี include/ ในคลัง หากต้องการสร้าง Python binding หรือรันตัวอย่าง C++ จากซอร์สโค้ด README มีคำสั่งสร้างหลักดังนี้:

mkdir build && cd build  
cmake -DCUDNN_PATH=/path/to/cudnn -DCUDAToolkit_ROOT=/path/to/cuda ../  
cmake --build . -j16  
./bin/samples  

หากต้องการรันเพียงตัวอย่างเดียว samples/README.md ให้วิธีการเฉพาะ:

./bin/samples "Cached sdpa"  

ในขั้นตอนการดีบัก สามารถเปิดใช้งานฟังก์ชันบันทึกของ frontend ได้:

export CUDNN_FRONTEND_LOG_INFO=1  
export CUDNN_FRONTEND_LOG_FILE=stdout  

ขอแนะนำอย่างยิ่งให้ถือว่า README เป็น "แผนที่ทางเข้า": โค้ดตัวอย่าง C++ อยู่ที่ samples/cpp[2] โค้ดตัวอย่าง Python อยู่ที่ samples/python[3] และเคอร์เนล OSS ที่เปิดใหม่จะกระจุกตัวอยู่ใน python/cudnn[4]

unsetunsetสอง: คุณค่าหลักของโปรเจกต์: เหนือกว่าการห่อหุ้ม API การควบคุมความซับซ้อนunsetunset

2.1 ความแข็งแกร่งและภาระหนักของ cuDNN backend API

จุดแข็งของ cuDNN backend API คือมันสามารถแสดงออบเจกต์ระดับล่าง เช่น tensor, operation, operation graph, engine config, execution plan ได้อย่างละเอียดสูง แต่ข้อเสียก็เด่นชัดไม่แพ้กัน: ผู้ใช้ต้องสร้าง ตั้งค่า ทำให้เสร็จสิ้น ค้นหา กรอง และดำเนินการ descriptor จำนวนมากตามลำดับที่ถูกต้อง ข้อผิดพลาดในการตั้งค่ามิติ ขั้นตอน ชนิดข้อมูล workspace หรือแอตทริบิวต์ใดๆ อาจทำให้การสร้างกราฟล้มเหลว แผนไม่รองรับ หรือเกิดข้อผิดพลาดรันไทม์

คุณค่าของ cuDNN Frontend ไม่ใช่แค่การแทนที่ชื่อฟังก์ชันหนึ่งด้วยอีกชื่อหนึ่ง แต่เป็นการยกระดับกระบวนทัศน์การเขียนโปรแกรมทั้งหมดจาก "การเขียนโปรแกรม descriptor ระดับล่าง" ไปเป็น "การเขียนโปรแกรมโอเปอเรเตอร์แบบกราฟ" ตอนนี้ผู้ใช้ต้องเผชิญกับ:

  • graph::Graph: ใช้สำหรับอธิบายกราฟย่อยการคำนวณที่สามารถคอมไพล์ได้
  • Tensor_attributes: ใช้สำหรับอธิบาย name, uid, dim, stride, data type ของเทนเซอร์
  • Conv_fprop_attributes, SDPA_attributes, Pointwise_attributes ฯลฯ: ใช้สำหรับอธิบายความหมายของโอเปอเรเตอร์
  • validate / build_operation_graph / create_execution_plans / check_support / build_plans / execute: กำหนดวงจรชีวิตที่สมบูรณ์จากกราฟความหมายไปยังแผนที่ดำเนินการได้
  • variant_pack: ใช้สำหรับผูก UID ของเทนเซอร์เชิงตรรกะกับ device pointer จริงในขั้นตอนการดำเนินการ

สิ่งนี้คล้ายกับแนวคิดของคอมไพเลอร์ frontend: ผู้ใช้เขียน IR (Intermediate Representation) ที่ใกล้เคียงกับความหมาย มากกว่าการเขียนรหัสเครื่องโดยตรง Frontend จัดระเบียบเทนเซอร์และโอเปอเรเตอร์เป็นกราฟ จากนั้นให้ cuDNN backend เลือกแผนที่ดำเนินการได้ตามรุ่น GPU, ชนิดข้อมูล, เลย์เอาต์, การค้นหาแบบฮิวริสติก ฯลฯ

2.2 จากการห่อหุ้มอย่างง่ายสู่ชั้นอินเทอร์เฟซระบบ AI

README เน้นว่าโปรเจกต์นี้มี "Unified Graph API", "Ease of Use" และ "Performance" คำสำคัญสามคำนี้สามารถแปลงเป็นคำอธิบายที่มีความหมายในทางวิศวกรรมมากขึ้น:

  1. การแสดงออกของกราฟแบบรวม: การดำเนินการต่างๆ เช่น คอนโวลูชัน, การคูณเมทริกซ์, SDPA, นอร์มัลไลเซชัน, pointwise fusion สามารถจัดการได้ภายใต้วงจรชีวิตกราฟชุดเดียวกัน
  2. ลดโค้ด Boilerplate: ผู้ใช้ไม่จำเป็นต้องจัดการรายละเอียดทั้งหมดของ backend descriptor แต่ละตัวด้วยตนเอง
  3. การดำเนินการที่ปรับแต่งได้: ผ่านกลไกต่างๆ เช่น การค้นหาแบบฮิวริสติก, execution plans, workspace, การปรับแต่งอัตโนมัติ, แคช ทำให้คำอธิบายกราฟเดียวกันสามารถเลือกแผนการดำเนินการที่เหมาะสมที่สุดภายใต้ฮาร์ดแวร์และรูปร่างที่แตกต่างกัน

ดังนั้น cudnn-frontend จึง更像 "ชั้นปรับตัวสำหรับการคอมไพล์และการดำเนินการโอเปอเรเตอร์" ในระบบการเรียนรู้เชิงลึก: เชื่อมต่อกับเฟรมเวิร์ก โมเดล และระบบนิเวศ Python ด้านบน และเชื่อมต่อกับ cuDNN backend และ GPU kernel ด้านล่าง

unsetunsetสาม: โครงสร้างคลัง: สามสายหลัก C++ Frontend, Python Binding และ Samplesunsetunset

3.1 การแบ่งหน้าที่ของไดเรกทอรีระดับบนสุด

ไดเรกทอรีรากของคลังประกอบด้วยไดเรกทอรี include, python, samples, test, benchmark, tools เป็นต้น เมื่อรวมกับ README และ API ของไดเรกทอรี สามารถเข้าใจได้ว่าเป็นโครงสร้างห้าชั้น:

  • include/: ส่วนหลักของ C++ frontend แบบ Header-Only ประกอบด้วย legacy frontend API, graph API, backend descriptor wrapper, execution plan ฯลฯ
  • python/: Python binding และการห่อหุ้มที่สอดคล้องกับสไตล์ Python มากขึ้น ประกอบด้วย PyGraph, wrapper, OSS kernels และ PyTorch ops ทดลอง
  • samples/: ตัวอย่าง C++ และ Python ครอบคลุมสถานการณ์ต่างๆ เช่น convolution, matmul, SDPA, normalization, misc
  • benchmark/: สคริปต์และผลลัพธ์การประเมินประสิทธิภาพสำหรับสถานการณ์ต่างๆ เช่น attention, norm
  • test/: การทดสอบความถูกต้องและพฤติกรรมของอินเทอร์เฟซ

samples/README.md อธิบายขอบเขตของตัวอย่างได้อย่างชัดเจน: ตัวอย่าง C++ ครอบคลุม convolution, matmul, SDPA/Flash Attention, normalization, serialization, autotuning, CUDA graphs, deviceless AOT compilation เป็นต้น; ตัวอย่าง Python รวมถึง matmul epilogue, graph serialization, mixed precision, layer norm, SDPA forward/backward และ paged caches

3.2 include: ชั้นนามธรรม C++ หลัก

ไฟล์ต่างๆ เช่น include/cudnn_frontend_ExecutionPlan.h, include/cudnn_frontend_ExecutionPlanCache.h, include/cudnn_frontend/graph_interface.h แสดงให้เห็นโครงสร้างที่มีทั้ง API รุ่นเก่าและใหม่:

  • ประเภทหนึ่งคือ frontend wrapper แบบดั้งเดิม เช่น ExecutionPlan, OperationGraph
  • อีกประเภทหนึ่งคือ graph API ใหม่ทั้งหมด โดยมี cudnn_frontend::graph::Graph เป็นหลัก

ExecutionPlan_v8 แสดงถึง "แผนการดำเนินการที่เลือกแล้ว" ใน cuDNN backend ออบเจกต์นี้รองรับการสอบถามขนาดพื้นที่ทำงาน แท็ก คำอธิบายประกอบเชิงตัวเลขและพฤติกรรม และยังสามารถส่งออกเป็นรูปแบบ JSON ได้:

// ที่มา: include/cudnn_frontend_ExecutionPlan.h  
class ExecutionPlan_v8 : public BackendDescriptor {  
public:  
auto getWorkspaceSize(void) const -> int64_t {  
return workSpaceSize;  
}  

std::string const& getTag() const {  
return planTag;  
}  

void setExecutionTime(float time_) {  
execution_time_ms = time_;  
}  

float getExecutionTime() const {  
return execution_time_ms;  
}  
};  

โค้ดส่วนนี้เผยให้เห็นแนวคิดการออกแบบหลักของ Frontend: execution plan ไม่ใช่ตัวแปรชั่วคราว แต่เป็นออบเจกต์ที่สมบูรณ์ซึ่งสามารถสอบถาม อธิบาย แคช และเปรียบเทียบได้ ในระบบประสิทธิภาพสูง การรู้ว่า "กำลังใช้แผนใด ต้องใช้พื้นที่ทำงานเท่าใด มีพฤติกรรมเชิงตัวเลขเฉพาะหรือไม่" เป็นสิ่งสำคัญ

3.3 Python: นำอินเทอร์เฟซกราฟสู่ระบบนิเวศ PyTorch

ฝั่ง Python ไม่ได้ห่อหุ้มฟังก์ชัน C++ อย่างง่ายๆ คลาส Graph ใน python/cudnn/wrapper.py เน้นการผสานรวมกับ PyTorch การตรวจสอบและคอมไพล์อัตโนมัติ และการจัดการ tensor และ workspace ที่ง่ายขึ้น:

# ที่มา: python/cudnn/wrapper.py  
class Graph:  
"""Wrapper object for cuDNN computation graph"""  

def __init__(  
self,  
        *,  
handle=None,  
inputs=None,  
outputs=None,  
heuristics=None,  
workspace_alloc=True,  
        **kwargs,  
):  
if cudnn.backend_version() < 91200:  
raise RuntimeError("cuDNN version 9.12.0 or higher is required")  

self.__tensor_map = {}  
self.__tensor_in = OrderedDict()  
self.__tensor_out = OrderedDict()  
self.__heuristics = heuristics or [heur_mode.A, heur_mode.FALLBACK]  

การออกแบบชั้นนี้เหมาะอย่างยิ่งสำหรับการผสานรวมเฟรมเวิร์ก: ผู้ใช้สามารถแมป PyTorch tensor เป็น cudnn tensor ทำให้การสร้างกราฟ การคอมไพล์ และการจัดการ workspace ใกล้เคียงกับนิสัยการใช้งาน Python มากขึ้น

สี่: ปรัชญาการออกแบบ Graph API: ใช้คำอธิบายกราฟแบบ Declarative แทนการประกอบ Descriptor แบบ Procedural

4.1 Graph: "Operator IR" ฝั่งผู้ใช้

ใน Graph API ผู้ใช้ไม่จำเป็นต้องสร้าง backend descriptor ทีละตัว แต่ประกาศว่า "มีเทนเซอร์อะไรบ้าง" และ "เทนเซอร์เชื่อมต่อกันผ่านโอเปอเรเตอร์อะไร" จากตัวอย่าง convolution forward ผู้ใช้สร้าง Graph ก่อน ตั้งค่าชนิดข้อมูล จากนั้นสร้างเทนเซอร์ X, W สองตัว และสุดท้ายเรียก conv_fprop เพื่อรับ Y:

// ที่มา: samples/cpp/convolution/fprop.cpp  
auto graph = std::make_shared<fe::graph::Graph>();  
graph->set_io_data_type(fe::DataType_t::HALF)  
.set_compute_data_type(fe::DataType_t::FLOAT);  

auto X = graph->tensor(fe::graph::Tensor_attributes()  
.set_name("image")  
.set_dim({n, c, h, w})  
.set_stride({c * h * w, 1, c * w, c}));  

auto W = graph->tensor(fe::graph::Tensor_attributes()  
.set_name("filter")  
.set_dim({k, c, r, s})  
.set_stride({c * r * s, 1, c * s, c}));  

auto conv_options =  
fe::graph::Conv_fprop_attributes()  
.set_padding({0, 0})  
.set_stride({1, 1})  
.set_dilation({1, 1});  

auto Y = graph->conv_fprop(X, W, conv_options);  
Y->set_output(true);  

สิ่งที่สำคัญที่สุดที่นี่คือการตั้งค่า stride มิติของ X ในตัวอย่างคือ {n, c, h, w} แต่ stride กลับเป็น {c*h*w, 1, c*w, c} ซึ่งบ่งชี้ว่ามันใช้เลย์เอาต์หน่วยความจำแบบ channels-last ไม่ใช่เลย์เอาต์ NCHW contiguous แบบดั้งเดิม (ซึ่ง stride คือ {c*h*w, h*w, w, 1}) Frontend ไม่ได้บังคับซ่อนรายละเอียดเลย์เอาต์ แต่ให้ผู้ใช้แสดงเลย์เอาต์อย่างชัดเจน ซึ่งจะรักษาความยืดหยุ่นในการปรับแต่งประสิทธิภาพและหลีกเลี่ยงค่าใช้จ่ายที่ไม่สามารถควบคุมได้จากการแปลงโดยนัย

4.2 Fluent builder: ทำให้การตั้งค่าแอตทริบิวต์กลายเป็นความหมายแบบลูกโซ่ที่อ่านง่าย

API แบบลูกโซ่เช่น Tensor_attributes().set_name(...).set_dim(...).set_stride(...) เป็นรูปแบบ fluent builder ทั่วไป ข้อดีของมันไม่เพียงแต่ "สวยงาม" เท่านั้น แต่ยังสามารถรวมแอตทริบิวต์ทั้งหมดที่จำเป็นสำหรับ backend descriptor ไว้ในบล็อกการประกาศเดียว ซึ่งช่วยลดความเสี่ยงในการละเว้นพารามิเตอร์สำคัญ

สำหรับกราฟแบบผสานที่มีโครงสร้างซับซ้อน รูปแบบการเขียนโค้ดนี้มีข้อได้เปรียบเด่นชัดเป็นพิเศษ ยกตัวอย่าง "Convolution + Scaling + Bias + ReLU" (CSBR) แบบคลาสสิก ตัวอย่างนี้เชื่อมต่อการดำเนินการหลายอย่างเป็นสายข้อมูลที่ชัดเจน:

// ที่มา: samples/cpp/convolution/fprop.cpp  
auto conv_output = graph->conv_fprop(X, W, conv_options);  

auto scale_options = fe::graph::Pointwise_attributes()  
.set_mode(fe::PointwiseMode_t::MUL);  
auto scale_output = graph->pointwise(conv_output, S, scale_options);  

auto bias_options = fe::graph::Pointwise_attributes()  
.set_mode(fe::PointwiseMode_t::ADD);  
auto bias_output = graph->pointwise(scale_output, B, bias_options);  

auto relu_options = fe::graph::Pointwise_attributes()  
.set_mode(fe::PointwiseMode_t::RELU_FWD);  
auto Y = graph->pointwise(bias_output, relu_options);  
Y->set_output(true);  

หากวาดเป็นผังงาน โครงสร้างจะเป็นดังนี้:

X ──conv(W)──> conv_output ──mul(S)──> scale_output ──add(B)──> bias_output ──relu──> Y

นี่คือแนวคิดหลักของ Graph API: ผู้ใช้เพียงแค่อธิบาย "ความสัมพันธ์การพึ่งพาข้อมูล" โดยไม่ต้องกังวลว่า "แต่ละเคอร์เนลจะถูกจัดตารางเวลาอย่างไร" การดำเนินการเหล่านี้จะถูกรวมเข้าด้วยกันหรือไม่ เลือกใช้เอ็นจิ้นใด และต้องใช้พื้นที่ทำงานเท่าใด การตัดสินใจเหล่านี้ทั้งหมดจะถูกจัดการในขั้นตอน "การสร้างแผน" ในภายหลัง


ห้า: ห่วงโซ่การดำเนินการ: จากการประกาศเทนเซอร์สู่การดำเนินการเคอร์เนล GPU

5.1 วงจรชีวิตหกขั้นตอน

ตัวอย่าง convolution แสดงวงจรชีวิตที่สมบูรณ์ของกราฟอย่างชัดเจน:

// ที่มา: samples/cpp/convolution/fprop.cpp  
REQUIRE(graph->validate().is_good());  
REQUIRE(graph->build_operation_graph(handle).is_good());  
REQUIRE(graph->create_execution_plans({fe::HeurMode_t::A}).is_good());  
REQUIRE(graph->check_support().is_good());  
REQUIRE(graph->build_plans().is_good());  

จากนั้นเข้าสู่ขั้นตอนการดำเนินการ:

// ที่มา: samples/cpp/convolution/fprop.cpp  
std::unordered_map<int64_t, void *> variant_pack = {  
{X->get_uid(), x_tensor.devPtr},  
{W->get_uid(), w_tensor.devPtr},  
{Y->get_uid(), y_tensor.devPtr}  
};  

int64_t workspace_size = 0;  
REQUIRE(graph->get_workspace_size(workspace_size).is_good());  
Surface<int8_t> workspace(workspace_size);  

REQUIRE(graph->execute(handle, variant_pack, workspace.devPtr).is_good());  

สามารถเข้าใจกระบวนการทั้งหมดนี้เป็นห่วงโซ่หกขั้นตอน:

  1. validate: ตรวจสอบว่าคำอธิบายกราฟที่ผู้ใช้สร้างขึ้นมีความสอดคล้องและไม่มีข้อผิดพลาด
  2. build_operation_graph: แปลงกราฟ frontend เป็น operation graph ที่ cuDNN backend สามารถเข้าใจได้
  3. create_execution_plans: สร้างแผนการดำเนินการที่เป็นตัวเลือกตามอัลกอริทึมฮิวริสติก
  4. check_support: กรองแผนการดำเนินการที่ไม่รองรับโดยฮาร์ดแวร์ เวอร์ชัน รูปร่างเทนเซอร์ และชนิดข้อมูลปัจจุบัน
  5. build_plans: กำหนดแผนที่ดำเนินการได้ในที่สุด
  6. execute: ผูกพอยน์เตอร์อุปกรณ์จริงผ่าน variant pack และดำเนินการคำนวณ

กระบวนการนี้คล้ายกับหลักการทำงานของคอมไพเลอร์อย่างมาก: การตรวจสอบความหมาย frontend, การสร้าง IR, การปรับให้เหมาะสมตามเป้าหมาย, การตรวจสอบความถูกต้อง, การสร้างโค้ด และการผูกข้อมูลรันไทม์

5.2 build(handle, heuristics) เป็นเส้นทางลัดที่ใช้บ่อย

ในตัวอย่าง SDPA ใช้เมธอด build ที่กระชับกว่า:

// ที่มา: samples/cpp/sdpa/fp16_fwd.cpp  
auto graph = create_sdpa_forward_graph(  
b, h_q, h_k, h_v, s_q, s_kv, d_qk, d_v,  
attn_scale, generate_stats, causal_mask, padding_mask);  

REQUIRE(graph->build(handle, {fe::HeurMode_t::A}).is_good());  

สามารถมองว่านี่คือการห่อหุ้มขั้นตอนหลายขั้นตอนข้างต้น สำหรับผู้ใช้ทั่วไป เมธอด build สะดวกกว่า แต่สำหรับผู้ที่ต้องการรวมแ


⚠️ หมายเหตุ: เนื้อหาได้รับการแปลโดย AI และตรวจสอบโดยมนุษย์ หากมีข้อผิดพลาดโปรดแจ้ง

☕ สนับสนุนค่ากาแฟทีมงาน

หากคุณชอบบทความนี้ สามารถสนับสนุนเราได้ผ่าน PromptPay

PromptPay QR
SCAN TO PAY WITH ANY BANK

本文来自网络搜集,不代表คลื่นสร้างอนาคต立场,如有侵权,联系删除。转载请注明出处:https://www.itsolotime.com/th/archives/34861

Like (0)
Previous 1 day ago
Next 8 hours ago

相关推荐