CUDA-oxide: คอมไพเลอร์ Rust-to-CUDA อย่างเป็นทางการของ Nvidia
(nvlabs.github.io)- cuda-oxide เป็นคอมไพเลอร์เชิงทดลองสำหรับเขียนเคอร์เนล SIMT GPU ด้วย Rust แบบ idiomatic ที่ใกล้เคียงความปลอดภัย และคอมไพล์โค้ด Rust มาตรฐานเป็น PTX ได้โดยตรง
- ใช้เพียง Rust โดยไม่มี DSL หรือ foreign-language binding และตั้งอยู่บนสมมติฐานว่าผู้ใช้เข้าใจ ownership, trait และ generic ส่วนบท async ก็ต้องมีความรู้เรื่อง
.awaitด้วย - v0.1.0 เป็นรีลีส อัลฟาระยะแรก จึงควรคาดว่าจะมีบั๊ก ฟีเจอร์ที่ยังไม่เสร็จสมบูรณ์ และการเปลี่ยนแปลง API แบบทำลายความเข้ากันได้
- ตัวอย่างรันด้วย
cargo oxide run vecaddและฟังก์ชัน#[kernel]ภายใน#[cuda_module]จะทำการบวกเวกเตอร์ด้วยthread::index_1d() #[cuda_module]จะฝัง device artifact ไว้ในโฮสต์ไบนารี และสร้างตัวโหลดแบบระบุชนิดพร้อมเมธอดรันแยกตามเคอร์เนล
วิธีใช้งานและโค้ดที่สร้างขึ้น
-
เริ่มต้นอย่างรวดเร็ว
- หลังเตรียมข้อกำหนดเบื้องต้นแล้ว ให้บิลด์และรันตัวอย่างด้วย
cargo oxide run vecadd - คำแนะนำการติดตั้งอยู่ที่ prerequisites
- ตัวอย่างจะกำหนดฟังก์ชัน
vecaddที่เป็น#[kernel]ภายในโมดูล#[cuda_module]แล้วใช้thread::index_1d()เพื่อรับอินเด็กซ์ จากนั้นเขียนค่าa[i] + b[i]ลงในDisjointSlice<f32> - ฝั่งโฮสต์จะใช้
CudaContext::new(0), default stream และkernels::load(&ctx)จากนั้นรันเคอร์เนลด้วยDeviceBuffer::from_host,DeviceBuffer::<f32>::zeroed,LaunchConfig::for_num_elems(1024) - ดึงผลลัพธ์กลับมาด้วย
c.to_host_vec(&stream)และตรวจสอบว่าresult[0] == 3.0
- หลังเตรียมข้อกำหนดเบื้องต้นแล้ว ให้บิลด์และรันตัวอย่างด้วย
-
การทำงานของ
#[cuda_module]#[cuda_module]จะฝัง device artifact ที่สร้างขึ้นไว้ในโฮสต์ไบนารี- สร้างฟังก์ชัน
kernels::loadแบบระบุชนิด และเมธอดรันแยกตามเคอร์เนล - หากต้องการโหลด sidecar artifact เฉพาะ หรือสร้างโค้ดรันแบบคัสตอม ก็ยังสามารถใช้ API ระดับล่างอย่าง
load_kernel_moduleและcuda_launch!ได้ต่อไป
ข้อกำหนดเบื้องต้นและทิศทาง
- cuda-oxide มีเป้าหมายเพื่อให้เขียน GPU kernel ด้วยระบบชนิดและโมเดล ownership ของ Rust โดยยกระดับความปลอดภัยเป็นเป้าหมายชั้นหนึ่ง
- เนื่องจาก GPU มีรายละเอียดที่ละเอียดอ่อน จึงควรอ่าน the safety model
- มันไม่ใช่ DSL แต่เป็น custom
rustccodegen backend ที่คอมไพล์ Rust ล้วนเป็น PTX - รองรับ การรันแบบอะซิงก์ โดยประกอบงาน GPU เป็นกราฟ
DeviceOperationที่ถูกประเมินผลแบบหน่วงเวลา จัดตารางลงบน stream pool และรอผลด้วย.await - ตั้งอยู่บนสมมติฐานว่าผู้ใช้คุ้นเคยกับ ownership, trait และ generic ของ Rust และในบทถัดไปเกี่ยวกับ async GPU programming ก็ต้องมีความรู้เรื่อง
async/.awaitและรันไทม์อย่าง tokio ด้วย - มีเอกสารอ้างอิงคือ The Rust Programming Language, Rust by Example, Async Book
- รีลีส v0.1.0 ยังอยู่ในช่วงอัลฟาระยะแรก และควรคาดว่าจะมี บั๊ก ฟีเจอร์ที่ยังไม่สมบูรณ์ และการเปลี่ยนแปลง API แบบทำลายความเข้ากันได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
โดยเฉพาะอยากรู้ว่า เวลา build เทียบกันแล้วเป็นอย่างไร เพราะ Rust CUDA crate ส่วนใหญ่พึ่ง CMake หรือการเรียก
nvccทำให้การคอมไพล์ช้าจนน่าทรมานได้เมื่อสัปดาห์ก่อนเพิ่งโปรไฟล์เวลา build แล้วเห็นว่าเครื่องมืออย่าง sccache ช่วยลดเวลา rebuild ได้มากด้วยการแคชผลลัพธ์ แต่ต้นทุนของการเรียก
nvccแบบ custom ก็ยังอยู่ ตัวอย่างเช่น candle ของ Hugging Face ก็เรียกคำสั่งnvccแบบ custom ตอนคอมไพล์ kernel: https://arpadvoros.com/posts/2026/05/05/speeding-up-rust-whi...ส่วนที่บอกว่า Rust CUDA crate ส่วนใหญ่มักเรียก CMake หรือ
nvccจนคอมไพล์ช้านั้น ส่วนตัวไม่ค่อยเจอหนักขนาดนั้น ถ้าดูcuda_setupcrate ที่ทำมาเพื่อจัดการ build script มันก็เป็นแค่build.rsธรรมดา จึงคอมไพล์ใหม่เฉพาะตอนที่ไฟล์เปลี่ยน และเมื่อเทียบกับโค้ดฝั่ง CPU ของ Rust แล้ว เวลาคอมไพล์ก็น้อยมากถ้าเป็นแบบนั้นก็คงดีมาก แต่ส่วนตัวคิดว่าน่าจะเป็น ตัวเสริม มากกว่า และก็อยากรู้เหมือนกันว่าจุดที่ทำให้ cuda-oxide ต่างออกไปคืออะไร นอกจากเรื่องที่ NVIDIA ควบคุมได้ทั้งหมด
Tile IR มีระดับนามธรรมสูงกว่าเล็กน้อย จึง target ได้ง่ายกว่ามาก และจะเสียเปรียบก็แค่เรื่องอย่าง epilogue fusion
[1] https://docs.nvidia.com/cuda/tile-ir/
[2] https://developer.nvidia.com/cuda/tile
ผมคิดว่าการเขียน GPU kernel โดยธรรมชาติแล้วไม่ปลอดภัย เพราะทั้งวิธีที่ฮาร์ดแวร์ทำงานและความจำเป็นที่ต้อง optimize จนสุด ทำให้สร้างภาษาที่ปลอดภัยได้ยากมาก
cudaFreeเองด้วยมืออย่างที่สอง ใน C++ อาร์กิวเมนต์
void*เป็นแค่อาร์เรย์ของพอยน์เตอร์และตรวจแค่จำนวน แต่ที่นี่บังคับอาร์กิวเมนต์ kernel ผ่านcuda_launch!อย่างที่สามคือปัญหา aliasing ของการเขียนแบบ mutable ใน C++ โค้ดที่สองเธรดขึ้นไปเขียน
out[i]ด้วยค่าiเดียวกันก็ยังคอมไพล์ได้ แต่DisjointSliceและThreadIndexไม่มี constructor แบบ public และมีให้ใช้แค่ APIindex_1d,index_2d,index_2d_runtimehttps://github.com/NVlabs/cuda-oxide/blob/2a03dfd9d5f3ecba52...อย่างที่สี่ ใน C++ คุณสามารถ
cuda memcpystd::stringหรือแทบจะ POD อะไรก็ได้จนสถานะพัง แต่ที่นี่รับได้แค่DisjointSlice, scalar, และ closure https://nvlabs.github.io/cuda-oxide/gpu-programming/memory-a...ดูรายละเอียดได้ที่ https://nvlabs.github.io/cuda-oxide/gpu-safety/the-safety-mo... และ https://nvlabs.github.io/cuda-oxide/gpu-programming/memory-a... แน่นอนว่ามันจับได้ไม่ทุกอย่าง แต่ดูเหมือนให้ guardrail ป้องกัน undefined behavior ได้มากกว่าไฟล์
.cuแบบ raw มากจะเหมาะเป็นภาษาสำหรับ GPU programming แค่ไหนคงต้องดูกันต่อไป แต่ก็ไม่น่าแปลกใจถ้าจะสร้าง API คล้าย DSL ที่ใช้เขียนโค้ดอย่างปลอดภัยได้พอสมควร ขณะเดียวกันก็ยังเข้าถึงความแปลกเฉพาะทางของ GPU ได้ครบ CUDA เองก็ดูเหมือนจะเป็นแบบนั้นไม่ใช่หรือ
งานที่ปลอดภัยแต่ขนานกัน และใส่ให้เข้ากับโมเดล
Send/Syncของ Rust ได้ไม่ง่ายนัก จำเป็นต้องยอมรับความขรุขระอยู่บ้างถ้าใช้ memory model หรือ ownership model ได้แบบ friction ต่ำก็ดี แต่ถ้าต้องแลกกับประสบการณ์ใช้งานที่แย่ลงมากก็ไม่เอา
baseline สำหรับผมคือแนวทางปัจจุบันของ Cudarc ที่ไม่ได้แทรกแซงการจัดการหน่วยความจำมากนัก เป็นแค่ไวยากรณ์เชิง imperative ที่ห่อ FFI ไว้ กับ build script ไม่กี่บรรทัดที่เรียก
nvccตอน kernel เปลี่ยนเอาจริงผมก็ชอบ Slang พอสมควร
[0]: https://shader-slang.org/
เช่น descriptor set, resource register, และข้อจำกัดของ dispatch
ภาษาสำหรับ shader ก็เป็นมิตรกับผู้ใช้มากกว่าในแง่ฟีเจอร์ด้วย แถม NVIDIA ก็ใช้ Slang ใน production อยู่แล้ว และคนกลุ่มนั้นคงไม่เขียน pipeline ของ shader ใหม่ด้วย Rust หรอก
ที่หาเจอมีแค่นี้
https://www.adacore.com/case-studies/nvidia-adoption-of-spar...
https://www.youtube.com/watch?v=2YoPoNx3L5E
ถึงอย่างนั้นก็ยังพยายามมองข้ามและไปอ่านเอกสารต่อ แต่พอเริ่มน่าสนใจจากการมี custom IR แล้วกลับมาเจอประโยคทำนองว่า “การทำ MLIR นั้นเป็น C++ ที่โรย TableGen พร้อม build system ที่ต้องคอมไพล์ LLVM ทั้งก้อน และ debugging session ที่ทำให้คุณตั้งคำถามกับเส้นทางอาชีพของตัวเอง” ก็ทำให้ยากจะมองวงการนี้อย่างจริงจังต่อไป
นี่ดูเหมือนเป็นตัวอย่าง กินอาหารหม้อเดียวกับที่ขาย ได้ตรงตามสไตล์บริษัทที่โหมกระแส AI พอดี
cuda-oxide ทำให้กรณีที่พบบ่อยอย่าง “หนึ่งเธรดเขียนหนึ่งองค์ประกอบ” ปลอดภัยเชิงโครงสร้าง ส่วนกรณีที่พบไม่บ่อยอย่าง shared memory, warp shuffle, และ hardware intrinsic จะต้องใช้
unsafeพร้อมสัญญาการใช้งานที่มีเอกสารกำกับ และฟีเจอร์แนวหน้าพวก TMA, tensor core, และการสื่อสารระดับคลัสเตอร์ ก็ปล่อยให้ทำแบบ manual ทั้งหมดตามความซับซ้อนของฮาร์ดแวร์แต่ทั้งหมดนี้ดู ไม่ค่อยเป็น Rust เท่าไร ใน Rust ถ้า abstraction เดิมไม่เข้ากับปัญหา ก็มักจะสร้าง abstraction ใหม่ที่ปลอดภัยขึ้นมา Rust for Linux ก็เป็นตัวอย่าง
ถ้ามันไม่ปลอดภัย แล้วจะใช้ Rust ไปทำไมกัน ผมโอเคกับการมี unsafe API ให้คนที่ต้องรีด performance ขั้นสุดท้ายใช้ แต่ไม่ควรให้สิ่งนั้นกลายเป็นค่าเริ่มต้น
มันทำให้นึกถึงไลบรารี user-space สำหรับ API อย่าง
io_uringหรือ Vulkan การออกแบบ safe API สำหรับของพวกนี้ยากมาก และก็มีความพยายามหลายครั้งที่สุดท้ายไม่ sound จริงเช่นเดียวกับกำแพงเรื่อง serialization/byte ระหว่างสองฝั่งนั้น
ตัวอย่างเช่น สงสัยว่าการตรวจขอบเขตของอาร์เรย์อาจทำให้ต้องใช้รีจิสเตอร์เพิ่มขึ้น จนลด concurrency ของ kernel ได้ไหม