From a4123f6c2e1e0151a29f6e9ca13ddfbaee74c1e6 Mon Sep 17 00:00:00 2001 From: Nathaniel Simard Date: Fri, 12 Jul 2024 10:15:17 -0400 Subject: [PATCH] Cube/doc/readme (#1904) --------- Co-authored-by: louisfd --- crates/burn-cube/README.md | 202 ++++++++++++++++++++++ crates/burn-cube/assets/CubeCL.webp | Bin 0 -> 33010 bytes crates/burn-cube/assets/cubecl.drawio.svg | 4 + crates/burn-cube/assets/logo.drawio.svg | 4 + 4 files changed, 210 insertions(+) create mode 100644 crates/burn-cube/README.md create mode 100644 crates/burn-cube/assets/CubeCL.webp create mode 100644 crates/burn-cube/assets/cubecl.drawio.svg create mode 100644 crates/burn-cube/assets/logo.drawio.svg diff --git a/crates/burn-cube/README.md b/crates/burn-cube/README.md new file mode 100644 index 0000000000..feea89f299 --- /dev/null +++ b/crates/burn-cube/README.md @@ -0,0 +1,202 @@ +
+ + +
+
+ +[![Rust Version](https://img.shields.io/badge/Rust-1.75.0+-blue)](https://releases.rs/docs/1.75.0) +![license](https://shields.io/badge/license-MIT%2FApache--2.0-blue) + +--- + +**Multi-platform high-performance compute language extension for Rust.** +
+ +
+ +## TL;DR + +With CubeCL, you can program your GPU using Rust leveraging zero-cost abstraction to create maintainable, flexible and optimal compute kernels. + +## Motivation + +The goal of CubeCL is to ease the pain of writing highly optimized compute kernels that are portable across hardware. +There is currently no adequate solution when you want optimal performance while still being multi-platform. +You either have to write custom kernels for different hardware, often with different languages such as CUDA, Metal, or ROCm. +To fix this, we created a Just-in-Time compiler with three core features: **automatic vectorization**, **comptime**, and **autotune**! + +These features are extremely useful for anyone writing high-performance kernels, even when portability is not a concern. +They improve code composability, reusability, testability, and maintainability, all while staying optimal. + +### Disclaimer & History + +CubeCL is currently in **alpha**. +The only supported runtimes are CUDA and WebGPU for now. +It's easy to add more GPU runtimes and we intend to support Metal, ROCm, and Vulkan; contributions are welcome! +We also want to have an optimized JIT CPU runtime with SIMD instructions, leveraging [Cranelift](https://cranelift.dev). + +While CubeCL is currently in use in [Burn](https://burn.dev), there are still a lot of rough edges; it isn't refined yet. +The project started as a WebGPU-only backend for Burn. +As we optimized it, we realized that we needed an intermediate representation (IR) that could be optimized then compiled to WGSL. +Having an IR made it easy to support another compilation target, so we made a CUDA runtime. +However, writing kernels directly in that IR wasn't easy, so we created a Rust frontend using the [syn](https://github.com/dtolnay/syn) crate. +Navigating the differences between CUDA and WebGPU, while leveraging both platforms, forced us to come up with general concepts that worked everywhere. +Hence, CubeCL was born! + +## Design + +CubeCL is designed around - you guessed it - Cubes! More specifically, it's based on cuboids, because not all axes are the same size. +Since all compute APIs need to map to the hardware, which are tiles that can be accessed using a 3D representation, our topology can easily be mapped to concepts from other APIs. + +
+ +### CubeCL - Topology + + +
+
+
+ +_A cube is composed of units, so a 3x3x3 cube has 27 units that can be accessed by their positions along the x, y, and z axes. +Similarly, a hyper-cube is composed of cubes, just as a cube is composed of units. +Each cube in the hyper-cube can be accessed by its position relative to the hyper-cube along the x, y, and z axes. +Hence, a hyper-cube of 3x3x3 will have 27 cubes. +In this example, the total number of working units would be 27 x 27 = 729._ + +
+Topology Equivalence 👇 +
+ +Since all topology variables are constant within the kernel entry point, we chose to use the Rust constant syntax with capital letters. +Often when creating kernels, we don't always care about the relative position of a unit within a cube along each axis, but often we only care about its position in general. +Therefore, each kind of variable also has its own axis-independent variable, which is often not present in other languages, except WebGPU with `local_invocation_index`. + +
+ +| CubeCL | CUDA | WebGPU | +| -------------- | ----------- | ---------------------- | +| CUBE_COUNT | N/A | N/A | +| CUBE_COUNT_X | gridDim.x | num_workgroups.x | +| CUBE_COUNT_Y | gridDim.y | num_workgroups.y | +| CUBE_COUNT_Z | gridDim.z | num_workgroups.z | +| CUBE_POS | N/A | N/A | +| CUBE_POS_X | blockIdx.x | workgroup.x | +| CUBE_POS_Y | blockIdx.y | workgroup.y | +| CUBE_POS_Z | blockIdx.z | workgroup.z | +| CUBE_DIM | N/A | N/A | +| CUBE_DIM_X | blockDim.x | workgroup_size.x | +| CUBE_DIM_Y | blockDim.y | workgroup_size.y | +| CUBE_DIM_Z | blockDim.z | workgroup_size.z | +| UNIT_POS | N/A | local_invocation_index | +| UNIT_POS_X | threadIdx.x | local_invocation_id.x | +| UNIT_POS_Y | threadIdx.y | local_invocation_id.y | +| UNIT_POS_Z | threadIdx.z | local_invocation_id.z | +| SUBCUBE_DIM | warpSize | subgroup_size | +| ABSOLUTE_POS | N/A | N/A | +| ABSOLUTE_POS_X | N/A | global_id.x | +| ABSOLUTE_POS_Y | N/A | global_id.y | +| ABSOLUTE_POS_Z | N/A | global_id.z | + +
+ +## Special Features + +#### Automatic Vectorization + +High-performance kernels should rely on SIMD instructions whenever possible, but doing so can quickly get pretty complicated! +With CubeCL, you can specify the vectorization factor of each input variable when launching a kernel. +Inside the kernel code, you still use only one type, which is dynamically vectorized and supports automatic broadcasting. +The runtimes are able to compile kernels and have all the necessary information to use the best instruction! +However, since the algorithmic behavior may depend on the vectorization factor, CubeCL allows you to access it directly in the kernel when needed, without any performance loss, using the comptime system! + +#### Comptime + +CubeCL isn't just a new compute language: though it feels like you are writing GPU kernels, you are, in fact, writing compiler plugins that you can fully customize! +Comptime is a way to modify the compiler IR at runtime when compiling a kernel for the first time. + +This enables lots of optimizations and flexibility without having to write many separate variants of the same kernels to ensure maximal performance. + +| Feature | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Instruction Specialization** | Not all instructions are available on all hardware, but when a specialized one exists, it should be enabled with a simple if statement. | +| **Automatic Vectorization** | When you can use SIMD instructions, you should! But since not all hardware supports the same vectorization factors, it can be injected at runtime! | +| **Loop Unrolling** | You may want multiple flavors of the same kernel, with loop unrolling for only a certain range of values. This can be configured easily with Comptime. | +| **Shape Specialization** | For deep learning kernels, it's often crucial to rely on different kernels for different input sizes; you can do it by passing the shape information as Comptime values. | +| **Compile Time Calculation** | In general, you can calculate a constant using Rust runtime properties and inject it into a kernel during its compilation, to avoid recalculating it during each execution. | + +#### Autotuning + +Autotuning drastically simplifies kernel selection by running small benchmarks at runtime to figure out the best kernels with the best configurations to run on the current hardware; an essential feature for portability. +This feature combines gracefully with comptime to test the effect of different comptime values on performance; sometimes it can be surprising! + +Even if the benchmarks may add some overhead when running the application for the first time, the information gets cached on the device and will be reused. +It is usually a no-brainer trade-off for throughput-oriented programs such as deep learning models. +You can even ship the autotune cache with your program, reducing cold start time when you have more control over the deployment target. + +## Example + +CubeCL is designed to be easy to use for Rust programmers: it relies on the same syntax and is fully integrated with the language. +You can simply add an attribute on the top of a Rust function for it to be executed on the GPU. + +```rust +#[cube(launch)] +fn gelu(input: &Array, output: &mut Array) { + if ABSOLUTE_POS < input.len() { + let x = input[ABSOLUTE_POS] + let gelu = x * (1 + erf(x / sqrt(2))) / 2; + output[ABSOLUTE_POS] = gelu; + } +} + +fn main() { + type Runtime = CudaRuntime; + + let device = Default::default(); + let client = Runtime::client(&device); + + let input_handle = client.create(f32::as_bytes(&[-1., 0., 1., 5.])); + let output_handle = client.empty(input.len() * core::mem::size_of::()); + + gelu::launch::( + client, + CubeCount::new(1, 1, 1), + CubeDim::new(4, 1, 1), + &input_handle, + &output_handle, + ); + + let output = client.read(output_handle.binding()).read_sync().unwrap(); + let output = f32::from_bytes(&output); + + // Should be [-0.1587, 0.0000, 0.8413, 5.0000] + println!("{output:?}"); +} + +``` + +The `cube` attribute generates the code that is needed to compile a kernel. +In the case above, the function `gelu_expand` and `gelu_launch` are automatically generated. +This allows you to compose Cube functions easily: + +```rust + +#[cube] +fn gelu_scalar(x: F) -> F { + x * (1 + erf(x / sqrt(2))) / 2 +} + +#[cube(launch)] +fn gelu(input: Array, mut output: Array) { + if ABSOLUTE_POS < input.shape(0) { + output[ABSOLUTE_POS] = gelu_scalar::(input[ABSOLUTE_POS]); + } +} +``` + +Note that you don't have to specify `launch` in a function that is only used by another Cube function. +In addition, you can have return types without problem, which isn't the case when you are writing an entry point to a kernel using the `launch` attribute. +The function `gelu_expand` will actually use `gelu_scalar_expand`, making it easy to combine your functions. + +## Resource + +If you have any questions or want to contribute, don't hesitate to join the [Discord](https://discord.gg/uPEBbYYDB6). diff --git a/crates/burn-cube/assets/CubeCL.webp b/crates/burn-cube/assets/CubeCL.webp new file mode 100644 index 0000000000000000000000000000000000000000..305c1b882c980a350de4b98e4acd347ec1b314cd GIT binary patch literal 33010 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
    6(QnA4bQXF}!E=CjTFzyprg-Mc=xIp$eUQ*UP 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 + + +
    (2, 2, 0)
    (1, 2, 0)
    (0, 2, 0)
    (2, 1, 0)
    (1, 1, 0)
    (0, 1, 0)
    (2, 0, 0)
    (1, 0, 0)
    (0, 0, 0)
    Cube
    [2, 2, 0]
    [1, 2, 0]
    [0, 2, 0]
    [2, 1, 0]
    [1, 1, 0]
    [0, 1, 0]
    [2, 0, 0]
    [1, 0, 0]
    Hyper-Cube
    (0, 0, 0)
    Unit
    (0, 0, 0)
    (0, 0, 0)
    diff --git a/crates/burn-cube/assets/logo.drawio.svg b/crates/burn-cube/assets/logo.drawio.svg new file mode 100644 index 0000000000..a83fc35206 --- /dev/null +++ b/crates/burn-cube/assets/logo.drawio.svg @@ -0,0 +1,4 @@ + + + +

    CubeCL

    \ No newline at end of file