Skip to main content

cubecl_wgpu/compiler/wgsl/
compiler.rs

1use super::shader::ComputeShader;
2use crate::compiler::wgsl::{
3    self, EnableFeaturesPass,
4    builtin::{LowerBuiltinsPass, WorkgroupLayout},
5    lower::LowerOpsWgslPass,
6    metadata::declare_info,
7    rewrite_args, shared_memory_size, types,
8};
9use cubecl_server::kernel::BufferIOAttr;
10
11use cubecl_core::{
12    WgpuCompilationOptions,
13    codegen::KernelDump,
14    post_processing::{
15        checked_io::{CheckedIo, CheckedIoPass},
16        minifloat::{
17            Fp8Container, LowerMinifloatCast, LowerMinifloatCastPass, LowerMinifloatCompare,
18            LowerMinifloatComparePass,
19        },
20        saturating::LowerSaturatingArithmeticPass,
21        unroll::UnrollPass,
22    },
23};
24use cubecl_environment::backtrace::BackTrace;
25use cubecl_ir::{
26    ContextExt,
27    features::EnumSet,
28    pliron::{
29        builtin::ops::{FuncOp, ModuleOp},
30        operation::verify_operation,
31        opts::{dce::DCEPass, mem2reg::Mem2RegPass},
32        printable::Printable,
33    },
34    prelude::{AnalysisManager, NestedOpsPass, Op, OpPass, Pass, Passes},
35    rewrite::{CanonicalizePass, LowerOpsPass, SimplifyOpsPass},
36    settings::Dim3,
37};
38use cubecl_opt::passes::{
39    annotate_buffer_visibility::AnnotateGlobalVisibilityPass, inst_combine::InstCombinePass,
40    sccp::SCCPPass, simple_cse::SimpleCSEPass, sroa::SROAPass,
41};
42use cubecl_server::compiler::CompilationError;
43use cubecl_server::kernel;
44
45const MAX_VECTOR_SIZE: usize = 4;
46
47#[derive(Clone, Copy)]
48pub struct KernelInfo {
49    pub cube_dim: Dim3,
50    /// How the cube is laid out as a workgroup.
51    pub layout: WorkgroupLayout,
52}
53
54/// Wgsl Compiler.
55#[derive(Clone, Default)]
56pub struct WgslCompiler;
57
58impl core::fmt::Debug for WgslCompiler {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str("WgslCompiler")
61    }
62}
63
64impl cubecl_core::Compiler for WgslCompiler {
65    type Representation = ComputeShader;
66    type CompilationOptions = WgpuCompilationOptions;
67
68    fn buffer_io(repr: &Self::Representation) -> Option<Vec<BufferIOAttr>> {
69        Some(repr.io.clone())
70    }
71
72    fn compile(
73        &mut self,
74        shader: kernel::KernelDefinition,
75        compilation_options: &Self::CompilationOptions,
76    ) -> Result<Self::Representation, CompilationError> {
77        self.compile_shader(shader, compilation_options)
78    }
79
80    fn extension(&self) -> &'static str {
81        "wgsl"
82    }
83
84    fn lang_tag(&self) -> &'static str {
85        "wgsl"
86    }
87}
88
89impl WgslCompiler {
90    fn compile_shader(
91        &mut self,
92        value: kernel::KernelDefinition,
93        compilation_options: &WgpuCompilationOptions,
94    ) -> Result<wgsl::ComputeShader, CompilationError> {
95        #[cfg(target_family = "wasm")]
96        crate::compiler::wgsl::ops::wasm_inventory_root();
97
98        let errors = value.body.pop_errors();
99        if !errors.is_empty() {
100            let mut reason = "Can't compile wgsl kernel".to_string();
101            for error in errors {
102                reason += error.as_str();
103                reason += "\n";
104            }
105
106            return Err(CompilationError::Validation {
107                reason,
108                backtrace: BackTrace::capture(),
109            });
110        }
111
112        let dump = KernelDump::new(&value.settings.kernel_name);
113
114        let module = value.body.state().module;
115        let entry_func = value.body.state().entry_func;
116        let module_op = module.get_operation();
117        let mut ctx = value.body.into_context().expect("Should be unique");
118        ctx.set_aux_ty(value.info);
119        let cube_dim = value.settings.cube_dim;
120        let layout = WorkgroupLayout::new(&ctx, module_op, &cube_dim);
121        ctx.set_aux_ty(KernelInfo { cube_dim, layout });
122        ctx.set_aux_ty(*compilation_options);
123
124        dump.write("initial.plir", || module_op.disp(&ctx).to_string());
125
126        verify_operation(module_op, &ctx)?;
127        types::check_fp8_lanes(&ctx, module_op)?;
128
129        let mut analyses = AnalysisManager::default();
130        analyses.set_config(dump.pass_config());
131
132        let mut passes = OpPass::<ModuleOp, Passes>::default();
133        let mut func_passes = OpPass::<FuncOp, Passes>::default();
134
135        func_passes.add_pass(LowerOpsPass::default());
136        func_passes.add_pass(SROAPass);
137        func_passes.add_pass(CheckedIoPass::new(CheckedIo::new(
138            value.settings.execution_mode,
139            value.settings.kernel_name.clone(),
140        )));
141        func_passes.add_pass(UnrollPass::new(MAX_VECTOR_SIZE));
142        // After the unroll so that an fp8 vector is at most one word, see `types.rs`.
143        func_passes.add_pass(LowerMinifloatCastPass::new(LowerMinifloatCast::new(
144            EnumSet::empty(),
145            Fp8Container::Words,
146        )));
147        func_passes.add_pass(LowerMinifloatComparePass::new(LowerMinifloatCompare::new(
148            Fp8Container::Words,
149        )));
150
151        func_passes.add_pass(LowerOpsWgslPass::default());
152        func_passes.add_pass(LowerSaturatingArithmeticPass::default());
153        func_passes.add_pass(LowerBuiltinsPass);
154
155        func_passes.add_pass(SCCPPass);
156        func_passes.add_pass(InstCombinePass::default());
157        func_passes.add_pass(SimpleCSEPass::without_memory());
158        func_passes.add_pass(SimplifyOpsPass::default());
159        func_passes.add_pass(DCEPass);
160        func_passes.add_pass(SROAPass);
161
162        // SCCP/DCE may unlock more mem2reg opportunities, and vice versa. So we do a sandwich.
163        func_passes.add_pass(Mem2RegPass);
164
165        func_passes.add_pass(SROAPass);
166        func_passes.add_pass(SCCPPass);
167        func_passes.add_pass(SimpleCSEPass::with_memory());
168        func_passes.add_pass(SimplifyOpsPass::default());
169        func_passes.add_pass(CanonicalizePass::default());
170        func_passes.add_pass(DCEPass);
171
172        passes.add_pass(NestedOpsPass::new(func_passes));
173        passes.add_pass(AnnotateGlobalVisibilityPass);
174        passes.add_pass(EnableFeaturesPass);
175
176        passes.run(module_op, &mut ctx, &mut analyses)?;
177
178        // Before `rewrite_args` erases the arguments and widens their
179        // visibility for the shader.
180        let io = cubecl_core::ir::attributes::buffer_io_by_position(&ctx, entry_func)
181            .into_iter()
182            .collect();
183        let buffers = rewrite_args(&mut ctx, entry_func);
184        declare_info(&mut ctx, entry_func, buffers.len());
185        let shared_memory_size = shared_memory_size(&ctx, module_op);
186
187        verify_operation(module.get_operation(), &ctx)?;
188
189        Ok(ComputeShader {
190            buffers,
191            io,
192            shared_memory_size,
193            ctx,
194        })
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use cubecl_core::{self as cubecl, Compiler, prelude::*};
201    use cubecl_ir::{metadata::Info, settings::Dim3};
202
203    use cubecl_server::kernel::KernelDefinition;
204
205    use super::WgslCompiler;
206
207    /// Reads a unit's axes and nothing of its plane.
208    #[cube]
209    fn axes() {
210        let _ = UNIT_POS_X + UNIT_POS_Y + UNIT_POS_Z;
211    }
212
213    /// Reads a unit's axes and its plane.
214    #[cube]
215    fn axes_and_plane() {
216        let _ = UNIT_POS_X + UNIT_POS_Y + UNIT_POS_Z + PLANE_POS;
217    }
218
219    /// Reads a unit's axes and sums over its plane, without reading which plane or lane it is.
220    #[cube]
221    fn axes_and_plane_sum() {
222        let _ = plane_sum(UNIT_POS_X + UNIT_POS_Y + UNIT_POS_Z);
223    }
224
225    /// The `@workgroup_size` line of the WGSL a kernel of `body` compiles to in cubes of
226    /// `cube_dim`.
227    fn workgroup_size(cube_dim: Dim3, body: fn(&Scope)) -> String {
228        let settings = KernelSettings::new(cube_dim, ExecutionMode::Checked, AddressType::U32);
229        let scope = Scope::root(settings.clone());
230        body(&scope);
231        let definition = KernelDefinition {
232            body: scope,
233            settings,
234            info: Info::default(),
235        };
236        let shader = WgslCompiler
237            .compile(definition, &Default::default())
238            .expect("the kernel compiles");
239        shader
240            .to_string()
241            .lines()
242            .find(|line| line.starts_with("@compute"))
243            .expect("an entry point")
244            .to_string()
245    }
246
247    #[test]
248    fn a_cube_spanning_y_without_planes_keeps_its_shape() {
249        assert_eq!(
250            workgroup_size(Dim3::new_3d(8, 4, 2), axes::expand),
251            "@compute @workgroup_size(8, 4, 2)"
252        );
253    }
254
255    #[test]
256    fn a_cube_spanning_y_with_planes_launches_flat() {
257        assert_eq!(
258            workgroup_size(Dim3::new_3d(8, 4, 2), axes_and_plane::expand),
259            "@compute @workgroup_size(64, 1, 1)"
260        );
261    }
262
263    #[test]
264    fn a_cube_spanning_y_with_plane_ops_but_no_plane_ids_keeps_its_shape() {
265        assert_eq!(
266            workgroup_size(Dim3::new_3d(8, 4, 2), axes_and_plane_sum::expand),
267            "@compute @workgroup_size(8, 4, 2)"
268        );
269    }
270
271    #[test]
272    fn a_cube_along_x_with_planes_keeps_its_shape() {
273        assert_eq!(
274            workgroup_size(Dim3::new_1d(64), axes_and_plane::expand),
275            "@compute @workgroup_size(64, 1, 1)"
276        );
277    }
278}