Skip to main content

ferrotensor_backend/engine/
mod.rs

1//! The compute engine: compiles the WGSL kernels and dispatches them.
2
3use crate::{UnifiedTensor, WgpuContext};
4use std::borrow::Cow;
5use wgpu::{util::DeviceExt, Buffer, Device};
6
7mod attention;
8mod elementwise;
9mod matmul;
10mod misc;
11mod norm;
12mod quant;
13mod training;
14
15pub use attention::AttentionOptions;
16pub use training::AdamParams;
17
18/// The full WGSL source compiled into the engine's shader module.
19fn shader_source() -> String {
20    let kernels_src = include_str!("../shaders/kernels.wgsl");
21    let matmul_src = include_str!("../shaders/matmul.wgsl");
22    let flash_v2_src = include_str!("../shaders/flash_attention_v2.wgsl");
23    let flash_v2_bw_src = include_str!("../shaders/flash_attention_v2_backward.wgsl");
24    format!("{kernels_src}\n{matmul_src}\n{flash_v2_src}\n{flash_v2_bw_src}")
25}
26
27/// A compiled compute pipeline together with its bind group layout.
28///
29/// The layout is derived from the shader (`layout: None`), so it always
30/// matches the bindings the entry point actually uses.
31struct Kernel {
32    pipeline: wgpu::ComputePipeline,
33    layout: wgpu::BindGroupLayout,
34}
35
36impl Kernel {
37    fn new(device: &Device, module: &wgpu::ShaderModule, entry_point: &str) -> Self {
38        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
39            label: Some(entry_point),
40            layout: None,
41            module,
42            entry_point,
43        });
44        let layout = pipeline.get_bind_group_layout(0);
45        Self { pipeline, layout }
46    }
47}
48
49/// Workgroup grid for `groups` 1-D workgroups. Grids wider than 65535 (the
50/// per-dimension limit) spill into y; the kernels rebuild a linear index.
51fn grid(groups: u32) -> (u32, u32, u32) {
52    const MAX: u32 = 65535;
53    if groups <= MAX {
54        (groups, 1, 1)
55    } else {
56        (MAX, groups.div_ceil(MAX), 1)
57    }
58}
59
60/// Grid of 64-wide workgroups covering `n` elements.
61fn groups64(n: usize) -> (u32, u32, u32) {
62    grid((n as u32).div_ceil(64))
63}
64
65/// Creates a uniform buffer holding `bytes`.
66fn uniform(ctx: &WgpuContext, bytes: &[u8]) -> Buffer {
67    ctx.device
68        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
69            label: Some("Uniform"),
70            contents: bytes,
71            usage: wgpu::BufferUsages::UNIFORM,
72        })
73}
74
75/// Compiled WGSL kernels and the methods that dispatch them.
76///
77/// Every method records and submits its own command buffer and returns
78/// without waiting; reading a result (e.g. with
79/// [`UnifiedTensor::to_vec`](crate::UnifiedTensor::to_vec)) waits for the GPU.
80/// Tensors are dense, row-major `f32` unless stated otherwise.
81pub struct ComputeEngine {
82    matmul: Kernel,
83    rope: Kernel,
84    rms_norm: Kernel,
85    silu: Kernel,
86    softmax: Kernel,
87    add: Kernel,
88    mul: Kernel,
89    sub: Kernel,
90    add_bw: Kernel,
91    sub_bw: Kernel,
92    mul_bw: Kernel,
93    silu_bw: Kernel,
94    sgd: Kernel,
95    adamw: Kernel,
96    mse_loss: Kernel,
97    mse_loss_bw: Kernel,
98    relu: Kernel,
99    relu_bw: Kernel,
100    softmax_bw: Kernel,
101    dequant: Kernel,
102    top_k: Kernel,
103    affine_dequant: Kernel,
104    conv1d: Kernel,
105    ce_loss: Kernel,
106    ce_loss_bw: Kernel,
107    grad_norm: Kernel,
108    grad_clip: Kernel,
109    silu_gate: Kernel,
110    silu_gate_bw: Kernel,
111    rms_residual: Kernel,
112    grad_accum: Kernel,
113    grad_scale: Kernel,
114    flash_attn_v2: Kernel,
115    flash_attn_v2_bw_dq: Kernel,
116    flash_attn_v2_bw_dkdv: Kernel,
117}
118
119impl ComputeEngine {
120    /// Compiles every kernel for `ctx`'s device.
121    pub fn new(ctx: &WgpuContext) -> Self {
122        let combined_src = shader_source();
123        let module = ctx
124            .device
125            .create_shader_module(wgpu::ShaderModuleDescriptor {
126                label: Some("Kernels"),
127                source: wgpu::ShaderSource::Wgsl(Cow::Owned(combined_src)),
128            });
129        let k = |entry: &str| Kernel::new(&ctx.device, &module, entry);
130
131        Self {
132            matmul: k("matmul_blocked"),
133            rope: k("rope"),
134            rms_norm: k("rms_norm"),
135            silu: k("silu"),
136            softmax: k("softmax"),
137            add: k("add"),
138            mul: k("mul"),
139            sub: k("sub"),
140            add_bw: k("add_backward"),
141            sub_bw: k("sub_backward"),
142            mul_bw: k("mul_backward"),
143            silu_bw: k("silu_backward_real"),
144            sgd: k("sgd_update"),
145            adamw: k("adamw_update"),
146            mse_loss: k("mse_loss"),
147            mse_loss_bw: k("mse_loss_backward"),
148            relu: k("relu"),
149            relu_bw: k("relu_backward"),
150            softmax_bw: k("softmax_backward"),
151            dequant: k("dequantize"),
152            top_k: k("top_k"),
153            affine_dequant: k("affine_dequantize"),
154            conv1d: k("conv1d"),
155            ce_loss: k("cross_entropy_loss"),
156            ce_loss_bw: k("cross_entropy_backward"),
157            grad_norm: k("gradient_norm_squared"),
158            grad_clip: k("clip_grad_by_norm"),
159            silu_gate: k("silu_gate_fused"),
160            silu_gate_bw: k("silu_gate_backward"),
161            rms_residual: k("rms_norm_residual"),
162            grad_accum: k("gradient_accumulate"),
163            grad_scale: k("gradient_scale"),
164            flash_attn_v2: k("flash_attention_v2_metal"),
165            flash_attn_v2_bw_dq: k("flash_bwd_dq"),
166            flash_attn_v2_bw_dkdv: k("flash_bwd_dk_dv"),
167        }
168    }
169
170    /// Binds `entries` (binding index, buffer) to `kernel` and dispatches
171    /// `groups` workgroups in its own command buffer.
172    fn run(
173        &self,
174        ctx: &WgpuContext,
175        kernel: &Kernel,
176        entries: &[(u32, &Buffer)],
177        groups: (u32, u32, u32),
178    ) {
179        let entries: Vec<wgpu::BindGroupEntry<'_>> = entries
180            .iter()
181            .map(|(binding, buf)| wgpu::BindGroupEntry {
182                binding: *binding,
183                resource: buf.as_entire_binding(),
184            })
185            .collect();
186        let bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
187            label: None,
188            layout: &kernel.layout,
189            entries: &entries,
190        });
191        let mut encoder = ctx
192            .device
193            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
194        {
195            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
196            pass.set_pipeline(&kernel.pipeline);
197            pass.set_bind_group(0, &bg, &[]);
198            pass.dispatch_workgroups(groups.0, groups.1, groups.2);
199        }
200        ctx.queue.submit(Some(encoder.finish()));
201    }
202
203    /// Copies all of `src` into `dst`, starting at byte offset `dst_offset`.
204    pub fn copy_tensor(
205        &self,
206        ctx: &WgpuContext,
207        src: &UnifiedTensor,
208        dst: &UnifiedTensor,
209        dst_offset: usize,
210    ) {
211        let mut encoder = ctx
212            .device
213            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
214        encoder.copy_buffer_to_buffer(
215            src.buffer(),
216            0,
217            dst.buffer(),
218            dst_offset as u64,
219            src.size_in_bytes(),
220        );
221        ctx.queue.submit(Some(encoder.finish()));
222    }
223}
224
225/// A 4-byte storage buffer bound where a kernel needs a buffer that is unused.
226fn dummy_storage(ctx: &WgpuContext) -> Buffer {
227    ctx.device
228        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
229            label: Some("Dummy Storage"),
230            contents: &[0u8; 4],
231            usage: wgpu::BufferUsages::STORAGE,
232        })
233}
234
235#[cfg(test)]
236mod shader_tests {
237    /// Parses and validates the combined WGSL with naga, so shader errors
238    /// fail `cargo test` without needing a GPU.
239    #[test]
240    fn combined_wgsl_validates() {
241        let src = super::shader_source();
242        let module = match naga::front::wgsl::parse_str(&src) {
243            Ok(m) => m,
244            Err(e) => panic!("WGSL parse error:\n{}", e.emit_to_string(&src)),
245        };
246        let mut validator = naga::valid::Validator::new(
247            naga::valid::ValidationFlags::all(),
248            naga::valid::Capabilities::empty(),
249        );
250        if let Err(e) = validator.validate(&module) {
251            panic!("WGSL validation error:\n{}", e.emit_to_string(&src));
252        }
253    }
254}