- บทความนี้อธิบายแนวคิดและการออกแบบในการแปลง โค้ด Python ล้วน ให้เป็น ไฟล์รันข้ามแพลตฟอร์ม ด้วยการ คอมไพล์ล่วงหน้า (AOT) โดยยกตัวอย่างเป็นกรณีศึกษา
- แนวคิดหลักคือไม่สร้าง JIT ใหม่หรือเขียนใหม่ด้วย C++ แต่ใช้ไปป์ไลน์ symbolic tracing → IR → สร้างโค้ด C++ → คอมไพล์หลายเป้าหมาย เพื่อสร้าง เคอร์เนลที่ปรับแต่งประสิทธิภาพแล้ว
- ใช้ type annotation ตาม PEP 484 เป็นจุดตั้งต้นของการเผยแพร่ชนิดข้อมูล และใช้ AI code generation สร้าง โอเปอเรเตอร์ C++ หลายร้อยตัวโดยอัตโนมัติ เพื่อรองรับการเรียกใช้ไลบรารีได้กว้างขวาง เช่น Numpy, OpenCV, PyTorch
- สำหรับฟังก์ชัน Python เดียวกัน ระบบจะ สร้างและแจกจ่ายเส้นทางการทำงานหลายแบบจำนวนมาก แล้วใช้ telemetry จากการวัดจริง เพื่อเลือก เวอร์ชันที่เร็วที่สุด ตามกลยุทธ์ เพิ่มประสิทธิภาพเชิงประจักษ์
- เป้าหมายคือมอบ ไบนารีพกพาขนาดเล็กและรวดเร็ว ที่ไม่พึ่งพาคอนเทนเนอร์ เพื่อใช้เป็นหน่วยสำหรับการแจกจ่ายที่ รันได้ทุกที่ ตั้งแต่เซิร์ฟเวอร์ เดสก์ท็อป มือถือ ไปจนถึงเว็บ
Foreword
- Python มีข้อดีด้านความเรียบง่ายและประสิทธิภาพในการพัฒนา แต่ก็มี ข้อจำกัดด้านสมรรถนะและความพกพาในเวิร์กโหลดหนัก
- บทความนี้โดย Yusuf Olokoba ในฐานะผู้เขียนรับเชิญ แนะนำการออกแบบคอมไพเลอร์ที่สร้าง ไฟล์รันที่เร็วและพกพาได้ โดยยัง คง Python ต้นฉบับไว้
- เป็นแนวทางที่มุ่งทำ การปรับแต่งเคอร์เนล ผ่านการจัดวางไปป์ไลน์ โดยไม่เพิ่ม JIT หรือรีไรต์ทั้งหมดเป็น C++
Introduction
- เป้าหมายคือคอมไพล์ Python ที่ไม่ต้องแก้ไขโค้ด แบบ AOT เต็มรูปแบบ ให้ ทำงานได้โดยไม่ต้องมีอินเทอร์พรีเตอร์ มี ความเร็วใกล้เคียง C/C++ และ รันได้บนทุกแพลตฟอร์ม
- ต่างจากความพยายามก่อนหน้าอย่าง Jython, RustPython, Numba, PyTorch, Mojo ที่เลือกแนวทาง แปลงโค้ดและสร้างเคอร์เนล แทนการ แทนที่ภาษา หรือแทนที่รันไทม์
- ฟังก์ชัน Python ที่คอมไพล์แล้วเหล่านี้ถูกใช้งานอยู่แล้วบน อุปกรณ์หลายพันเครื่องในทุกเดือน
Containers Are the Wrong Way to Distribute AI
- ในการดีพลอยใช้งานจริง คอนเทนเนอร์เป็นเพย์โหลดที่หนักเกินไป เพราะพ่วงทั้งอินเทอร์พรีเตอร์ แพ็กเกจ และสแนปช็อตของ OS ทำให้เกิด ความล่าช้าในการเริ่มต้น และ ข้อจำกัดด้านความพกพา
- ทางเลือกคือ ไฟล์รันแบบพึ่งพาตัวเองที่มีเฉพาะโมเดล ซึ่งให้ ขนาดเล็กกว่า เริ่มทำงานได้เร็วกว่า และรันได้ครอบคลุมทั้ง เซิร์ฟเวอร์ เดสก์ท็อป มือถือ และเว็บ
- แก่นของแนวคิดคือการเปลี่ยน หน่วยการแจกจ่าย จากสแนปช็อตของ OS ไปเป็น ไบนารีที่รันได้ด้วยตัวเอง
Arm64, Apple, and Unity: How It All Began
- ในช่วงที่ Apple เปลี่ยนผ่านสู่ arm64 มีกรณีศึกษาของ Unity ที่ใช้ IL2CPP แปลง CIL เป็น C++ เพื่อให้ คอมไพล์ไปยังทุกเป้าหมายได้ ซึ่งถูกนำมาใช้เป็นต้นแบบ
- จากนั้นจึงตั้งวิสัยทัศน์ว่าจะนำแนวคิดเดียวกันนี้มา ประยุกต์กับ Python เพื่อให้ได้เส้นทางโค้ดที่ รันได้ทุกที่
Sketching Out a Python Compiler
- การออกแบบระดับสูงประกอบด้วยขั้นตอน Python input → symbolic trace (IR) → สร้าง C++ → คอมไพล์หลายเป้าหมาย
- เหตุผลที่ไม่สร้าง object code โดยตรงจาก IR แต่เลือกใช้ C++ เป็นผลลัพธ์ขั้นกลาง ก็เพื่อใช้ประโยชน์จาก เส้นทางเร่งความเร็ว อย่าง CUDA, MLX, TensorRT, AMX ได้อย่างเต็มที่
- เป้าหมายคือให้ได้ สถาปัตยกรรมที่ขยายต่อได้ง่าย และสามารถแทรก เส้นทางที่เหมาะที่สุดตามฮาร์ดแวร์ ได้สะดวก
Building a Symbolic Tracer for Python
- การติดตามแบบเดิมที่อิง PyTorch FX มีข้อจำกัด เพราะ ต้องอาศัยการรันจริง และ รองรับเฉพาะโอเปอเรชันของ PyTorch
- จึงหันไปสร้าง symbolic tracer ที่อิงการพาร์ส AST เพื่อแปลง control flow และการตีความการเรียกใช้ ให้เป็น IR
- ปัจจุบัน tracer นี้รองรับความสามารถอย่าง static analysis, partial evaluation และการสังเกต live value ผ่านแซนด์บ็อกซ์
Lowering to C++ via Type Propagation
- เพื่อเชื่อม Python ที่มี dynamic typing เข้ากับ C++ ที่มี static typing จึงใช้เทคนิค type propagation
- เมื่อทราบชนิดข้อมูลของอาร์กิวเมนต์ขาเข้า ก็สามารถ อนุมานชนิดของตัวแปรระหว่างทางได้แบบกำหนดแน่นอน ตาม นิยามของโอเปอเรเตอร์
- จากนั้นจะแมปแต่ละโอเปอเรชันของ Python ไปยัง อิมพลีเมนเทชัน C++ ที่สอดคล้องกัน พร้อม เผยแพร่ชนิดข้อมูลตลอดทั้งฟังก์ชัน
Seeding the Type Propagation Process
- ระบบใช้ type annotation ตาม PEP 484 เป็นจุดตั้งต้นของการทำ type propagation
- แม้จะขัดกับหลักการ ไม่แก้ไขโค้ดต้นฉบับ แต่ก็ถูกมองว่าเป็น ทางประนีประนอมที่ยอมรับได้ เพื่อให้ได้ อินเทอร์เฟซที่กระชับและเข้ากันได้ดี
- นอกจากนี้ยังมีข้อจำกัด เช่น การจำกัดจำนวนชนิดข้อมูลในฟังก์ชันซิกเนเจอร์ เพื่อให้มั่นใจว่า อินเทอร์เฟซสำหรับผู้ใช้ยังคงเรียบง่าย
Building a Library of C++ Operators
- ไม่ได้จำเป็นต้องอิมพลีเมนต์ทุกฟังก์ชันเป็น C++ โดยตรง แต่ต้องมีการเขียนเองหรือสร้างอัตโนมัติสำหรับ leaf operation ที่ trace ต่อไม่ได้ เท่านั้น
- โค้ด Python จำนวนมากประกอบขึ้นจาก การผสมกันของโอเปอเรชันพื้นฐานเพียงไม่กี่ชนิด ทำให้ ชุดโอเปอเรเตอร์ที่ต้องครอบคลุม มีขนาดค่อนข้างเล็ก
- ด้วย การสร้างโค้ดด้วย LLM และโครงสร้างพื้นฐานสำหรับ ข้อจำกัด การทดสอบ และ conditional compilation จึงสามารถทำ อิมพลีเมนเทชันของฟังก์ชันหลายร้อยตัว ใน Numpy, OpenCV, PyTorch และอื่น ๆ ได้โดยอัตโนมัติ
Performance Optimization via Exhaustive Search
- จากบทเรียนที่ว่า การปรับแต่งประสิทธิภาพเป็นเรื่องเชิงประจักษ์เสมอ ระบบจึงสร้าง เวอร์ชันอิมพลีเมนเทชันทุกแบบที่เป็นไปได้ แล้วเลือกสิ่งที่ดีที่สุดจาก การวัดเปรียบเทียบจริง
- ตัวอย่างเช่น บน Apple Silicon แม้แต่การ resize เพียงอย่างเดียว ก็ยังสร้างได้หลายเส้นทาง เช่น Accelerate, vImage, Core Image, Metal และทำการแจกจ่าย ไบนารีหลายตัวที่ให้ความสามารถเดียวกัน
- ระบบเก็บ latency รายเส้นทาง ด้วย telemetry แบบละเอียด แล้วใช้ แบบจำลองทางสถิติ เพื่อทำนายและเลือก เวอร์ชันที่เร็วที่สุด
- ผลลัพธ์ในมุมผู้ใช้คือประสบการณ์การรันที่ เร็วขึ้นโดยอัตโนมัติเมื่อเวลาผ่านไป
Designing a User Interface for the Compiler
- เพื่อให้ประสบการณ์ของนักพัฒนา แทบไม่มีเส้นโค้งการเรียนรู้ จึงเลือกใช้ PEP 318 decorator
@compile เป็นอินเทอร์เฟซ
- CLI จะใช้ decorator นี้เป็น entrypoint เพื่อสำรวจ กราฟของโค้ดที่พึ่งพากัน และคอมไพล์มัน
- อาร์กิวเมนต์ของ decorator รองรับ tag, description, sandbox, metadata เพื่อสนับสนุน การทำซ้ำสภาพแวดล้อมและการระบุแบ็กเอนด์ เช่น ONNXRuntime, TensorRT, CoreML, IREE, QNN
Closing Thoughts
- ฟีเจอร์อย่าง exception, lambda, recursion, class ยังอยู่ในสถานะ รองรับบางส่วนหรือยังไม่รองรับ โดยเฉพาะในกรณีของ ชนิดข้อมูลเชิงประกอบและชนิดข้อมูลลำดับสูง ที่ยังต้องขยายความสามารถของ type propagation
- ประสบการณ์การดีบัก ก็ยังเป็นโจทย์สำคัญ เพราะการคอมไพล์แบบเน้นประสิทธิภาพทำให้ข้อมูลสัญลักษณ์ลดลงและ ติดตามปัญหาได้ยากขึ้น
std::span, concepts และ coroutines ของ C++20 เป็นฐานสำคัญ ขณะที่ std::generator, <stdfloat>, <stacktrace> ของ C++23 คาดว่าจะช่วยเรื่อง สตรีมมิง, half/bfloat16 และการติดตาม exception
- เป้าหมายสุดท้ายคือสร้าง ไฟล์รันขนาดเล็ก รวดเร็ว และปลอดภัยโดยไม่ต้องพึ่งคอนเทนเนอร์ เพื่อทำให้ เวิร์กโหลด AI อย่าง embedding หรือ detection กลายเป็นหน่วยการแจกจ่ายที่ รันได้ทุกที่
1 ความคิดเห็น
นึกว่าเป็นอะไรแบบ APE แต่ก็ไม่ใช่แบบนั้นนะ