Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung)
92

Sản phẩm

NVIDIA ra mắt CUDA Rust: Hỗ trợ viết GPU kernel trực tiếp bằng ngôn ngữ Rust

(giờ Việt Nam)

Tóm tắt AI

NVIDIA chính thức hỗ trợ lập trình GPU bằng Rust, cho phép biên dịch trực tiếp kernel sang PTX thông qua hai lộ trình cuda-oxide và cutile-rs, giúp tối ưu hóa hiệu năng mà không cần thông qua các ngôn ngữ trung gian.

Bản dịch AI

Introducing CUDA Rust: Two Tracks for Writing GPU Kernels

Vào tháng 9 năm 2026, NVIDIA thông báo họ đang đẩy mạnh việc lập trình GPU gốc (native GPU programming) bằng Rust. CUDA C++ và CUDA Python là những bộ công cụ cấp doanh nghiệp đã hoàn thiện, và NVIDIA sẽ tiếp tục phát triển cũng như hoàn thiện CUDA Rust trong năm 2027 và xa hơn nữa.

Lớp hệ thống của AI bao gồm các công cụ suy luận (inference engines), hạ tầng phục vụ, trình điều khiển (drivers) và môi trường thực thi tác nhân (agent runtimes), và nó liên tục thay đổi khi các mô hình và kỹ thuật phát triển. Ngày càng nhiều thành phần trong đó được viết bằng Rust, ngôn ngữ giúp bắt trọn các loại lỗi ngay tại thời điểm biên dịch mà không làm giảm hiệu năng.

NVIDIA cũng là một phần của sự chuyển dịch đó vì cùng lý do trên. Trình điều khiển Nova Linux được viết bằng Rust. NVIDIA Dynamo được xây dựng trên lõi Rust. NVTX cũng có các Rust bindings.

GPU kernel là một ngoại lệ. Bạn có thể khởi chạy các kernel từ Rust, nhưng bản thân kernel thường phải được viết bằng một ngôn ngữ khác.

NVIDIA CUDA Rust thu hẹp khoảng cách đó. Các GPU kernel có thể được viết bằng Rust, biên dịch trực tiếp sang PTX, thay vì phải dùng một lớp bao bọc (wrapper) cho mã nguồn từ nơi khác.

Có hai hướng để sử dụng Rust, tương ứng với hai hướng mà bản thân CUDA đang có. SIMT là mô hình mà bạn đã quen viết trong CUDA C++ hoặc numba-cuda. Bạn chỉ định những gì một luồng (thread) thực hiện và khởi chạy hàng ngàn luồng như vậy. Tile là một mô hình lập trình mới hơn, cũng có sẵn trong C++ và Python. Tất cả các giao diện này cho phép bạn định nghĩa những gì một "tile" dữ liệu thực hiện, và trình biên dịch Tile IR sẽ lo phần còn lại.

Khi bạn chọn một hướng để xây dựng, hãy ưu tiên Tile trước. Trình biên dịch sẽ quyết định cách các tile ánh xạ lên từng kiến trúc, vì vậy mã nguồn của bạn không chứa các lựa chọn cụ thể cho từng kiến trúc. Bạn chỉ cần chuyển sang SIMT khi cần quyền kiểm soát đó hoặc muốn tự quản lý bộ nhớ và luồng.

Việc chọn ngôn ngữ nào là một câu hỏi tách biệt với việc chọn mô hình nào. Hãy sử dụng cách tiếp cận CUDA phù hợp nhất với ngăn xếp (stack) công nghệ hiện có của bạn. Hai dự án dưới đây dành cho trường hợp ngăn xếp đó là Rust. Chúng tôi dự định hỗ trợ khả năng tương tác giữa các ngôn ngữ, vì vậy lựa chọn này sẽ không ngăn cản bạn sử dụng các lựa chọn khác.

Dưới đây là cùng một kernel trên mỗi hướng, thực hiện phép cộng từng phần tử trên 1.024 số thực (floats). Cả hai đều là các chương trình hoàn chỉnh, đều chạy được và đều in ra cùng một dòng, vì vậy bạn có thể đọc chúng song song để thấy những gì thay đổi.

Hướng SIMT: cuda-oxide

cuda-oxide là một backend tạo mã (codegen) tùy chỉnh cho rustc. Nó can thiệp vào quá trình biên dịch, điều hướng các hàm #[kernel] thông qua Rust MIR, khung làm việc Pliron IR của cộng đồng, và LLVM IR xuống PTX, đồng thời chuyển mọi thứ khác cho backend tiêu chuẩn. Các phương ngữ (dialects) GPU trên nền Pliron là của chúng tôi. Các phương ngữ và mọi phép biến đổi đều nằm trong Rust cho đến khi backend LLVM tiêu chuẩn tiếp quản.

Bạn sẽ cần Linux, GPU có khả năng tính toán (compute capability) 8.0 trở lên, bộ công cụ CUDA (12.x trở lên), clang với các header libclang, và bộ công cụ nightly đã được ghim. Lệnh `cargo oxide doctor` sẽ kiểm tra tất cả những điều này, bao gồm cả LLVM hệ thống tùy chọn. Hãy cài đặt `cargo-oxide`, lệnh con của Cargo dùng để điều khiển quá trình build:

Sau đó, tạo khung dự án và chạy nó. Template này là một chương trình cộng vector hoàn chỉnh:

Lần chạy `cargo oxide run` đầu tiên sẽ xây dựng backend tạo mã, vì vậy hãy chờ đợi một chút. Các lần chạy sau sẽ sử dụng lại bộ nhớ đệm (cache).

Nó in ra PASSED: tất cả 1024 phần tử đều chính xác. Đây là toàn bộ chương trình đã thực hiện việc đó, chính xác là những gì `cargo oxide new` đã tạo ra, với các chú thích được thêm vào đây:

Mã nguồn host và device nằm trong cùng một tệp, build bằng một lệnh duy nhất và không cần kernel crate riêng biệt.

Hãy đọc chữ ký (signature) của kernel trước, vì nó mang toàn bộ lập luận về tính an toàn. `a` và `b` là các slice chia sẻ thông thường, mọi luồng đều có thể đọc được. `c` là một `DisjointSlice<f32>`, một kiểu dữ liệu cấp cho mỗi luồng quyền truy cập độc quyền vào phần tử của riêng nó và không gì khác. Nó tồn tại vì `&mut [f32]` không phải là hình dạng phù hợp cho công việc này. Mỗi luồng sẽ cần cùng một `&mut`, điều mà Rust từ chối một cách chính xác. `DisjointSlice` chia quyền mượn có thể thay đổi (mutable borrow) đó thành các phần riêng biệt cho từng luồng.

`thread::index_1d ` trả về một kiểu chỉ mục, không phải là một số nguyên thuần túy, và `c.get_mut(idx)` chỉ chấp nhận kiểu đó. Bạn nhận lại một `Option`, vì vậy trường hợp vượt quá giới hạn (out-of-bounds) là một nhánh mà bạn xử lý thay vì là một lỗi bộ nhớ mà bạn mới phát hiện sau này.

Việc khởi chạy được kiểm tra thay vì chỉ tin tưởng. `#[launch_contract]` khai báo rằng kernel này lập chỉ mục theo một chiều với các block 256 luồng. `prepare_vecadd` xác thực `LaunchConfig1D` của bạn dựa trên khai báo đó và các giới hạn thiết bị thực tế, đồng thời trả về một bằng chứng mà phương thức `vecadd` an toàn yêu cầu. Các kernel không có hợp đồng chỉ hiển thị các phương thức khởi chạy không an toàn (unsafe), vì một `LaunchConfig` thuần túy không nói lên điều gì về kernel mà nó đang khởi chạy.

Hướng Tile: cutile-rs

cutile-rs hoạt động ở cấp độ cao hơn một bậc. Bạn thực hiện các phép tính trên các tile thay vì các giá trị vô hướng (scalars). Mỗi block tile chạy phần thân kernel một lần như một luồng logic duy nhất trên một sub-tensor dữ liệu, và trình biên dịch quyết định bao nhiêu luồng GPU thực tế sẽ hỗ trợ nó. Macro `#[cutile::module]` nhúng AST của kernel vào tệp nhị phân host và biên dịch JIT thông qua CUDA Tile IR (trình biên dịch IR cấp tile của NVIDIA) khi kernel được cần đến lần đầu tiên.

Các yêu cầu nhẹ nhàng hơn so với hướng SIMT. Bạn cần GPU có khả năng tính toán 8.0 trở lên, CUDA 13.3, Rust stable 1.89 trở lên và Linux, nhưng không cần bộ công cụ nightly và không cần tự cài đặt LLVM.

cutile đã được xuất bản, vì vậy không cần phải clone:

Đây là cùng một phép cộng từng phần tử, được viết cho các tile. Hãy dán nó vào `src/main.rs` và chạy `cargo run`:

PASSED: tất cả 1024 phần tử đều chính xác

Hướng Tile đạt được kết quả tương tự trên Rust stable, và chữ ký của nó đưa ra lập luận an toàn tương tự. Lần này không có `DisjointSlice`. Việc phân vùng trên host chỉ cần thiết cho các tensor có thể thay đổi, và nó cấp cho mỗi block tile một sub-tensor có thể ghi mà không block tile nào khác có thể chồng lấp. Sự độc quyền đó chính là điều mà `&mut` đã đảm bảo.

Số -1 trong các hình dạng đầu vào là một giá trị đánh dấu (sentinel) thay vì là kích thước. Chiều đó được đọc từ tensor tại thời điểm khởi chạy, vì vậy hình dạng có thể thay đổi mà không cần biên dịch lại.

Dòng thú vị trên host là `.partition([128])`, và nó thực hiện ba công việc cùng một lúc. Nó hiện thực hóa tính độc quyền. Mỗi tile sở hữu khối 128 phần tử của riêng nó và không tile nào khác có thể chạm vào. Nó cố định hình học khởi chạy, vì 1.024 chia cho 128 là một lưới gồm 8 tile.

Lưới được xác định từ phân vùng thay vì được tính toán riêng biệt và kiểm tra dựa trên việc lập chỉ mục của kernel. Nó cũng cung cấp B, thứ không bao giờ được ghi tại vị trí gọi vì trình khởi chạy đọc độ rộng tile từ phân vùng. Đó là lý do tại sao một đầu ra `&mut` phải được phân vùng trước khi có thể được truyền đi.

Sau đó, hãy nhìn vào những gì lệnh khởi chạy trả về. `add` mà bạn gọi trên host là một trình khởi chạy được tạo bởi macro, không phải là hàm device ở trên. Nó chiếm quyền sở hữu cả ba tensor và trả lại chúng dưới dạng một tuple khi GPU hoàn tất. Đó là lý do tại sao có `.first `, để lấy đầu ra từ đó.

Không có gì chạy cho đến khi gặp `.sync_on(&stream)`. Mọi thứ trước đó chỉ là một mô tả lười biếng (lazy description), được ghi lại thay vì được gửi đi. Điều đó bao gồm các số một, số không, lệnh gọi kernel và thậm chí cả việc sao chép ngược lại host. Toàn bộ chương trình là một chuỗi với một điểm đồng bộ hóa duy nhất.

Những gì trình biên dịch bắt được

Cả hai kernel đều đưa ra cùng một khẳng định về bộ nhớ. Đầu vào của chúng được chia sẻ, và đầu ra của chúng thuộc về duy nhất một người ghi. Chúng chỉ khác nhau ở cấp độ mà chúng thực hiện điều đó, và liệu có cần một kiểu dữ liệu chuyên dụng để thực hiện hay không.

Điều đó quan trọng vì hàng ngàn luồng truy cập vào cùng các bộ đệm mà không có thứ tự đảm bảo. Khi hai trong số chúng truy cập cùng một địa chỉ và một trong số đó đang ghi, thứ tự sẽ quyết định kết quả. Những lỗi đó hiếm khi tái hiện theo yêu cầu, và chúng vượt qua các bài kiểm tra trước khi gây lỗi trong môi trường thực tế (production).

Việc truyền bộ đệm đầu ra của kernel SIMT làm một trong các đầu vào của chính nó sẽ không biên dịch được, bất kể kernel đó có thực sự xảy ra tranh chấp (race) hay không:

error[E0502]: không thể mượn `c_dev` dưới dạng có thể thay đổi vì nó cũng đang được mượn dưới dạng không thể thay đổi

Tình trạng alias tương tự ở phía Tile cũng không biên dịch được:

error[E0382]: sử dụng giá trị đã được di chuyển (moved): `z`

Cả hai ví dụ đều bắt được lỗi alias kinh điển ngay tại thời điểm biên dịch, và chúng vạch ra ranh giới ở những vị trí khác nhau. `cuda-oxide` kiểm tra từng lệnh gọi khởi chạy. Quyền sở hữu của `cutile-rs` theo dõi các tensor qua ranh giới khởi chạy, đây là khẳng định mạnh mẽ hơn trong hai khẳng định trên.

Đọc bài gốc

Bài viết được AI dịch và tổng hợp tự động từ Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung). Liên kết bài gốc ở phía trên. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.