Skip to main content

cubek_convolution/definition/
cost.rs

1use cubecl::{
2    client::Client,
3    ir::ElemType,
4    throughput::{ThroughputKey, ThroughputMode},
5    tune::Work,
6};
7use cubek_matmul::definition::{MatmulCost, MatmulGlobalElems};
8
9/// Minimal representation of a 2D convolution's cost: the shapes it moves and
10/// the element types it moves them in.
11#[derive(Debug, Clone)]
12pub struct Conv2dCost {
13    /// Batch, input channels, and the input's spatial extent.
14    pub batch: usize,
15    pub channels_in: usize,
16    pub spatial_in: [usize; 2],
17    /// Output channels and the filter's spatial extent.
18    pub channels_out: usize,
19    pub kernel: [usize; 2],
20    /// The output's spatial extent, which stride, padding and dilation decide.
21    pub spatial_out: [usize; 2],
22    /// Elements of the bias, zero when the convolution has none.
23    pub bias_elems: usize,
24    /// Global element types of the operands.
25    pub elems: MatmulGlobalElems,
26}
27
28impl Conv2dCost {
29    /// The implicit gemm this convolution is: one output pixel per row, one
30    /// output channel per column, contracting over the filter's input window.
31    ///
32    /// Its traffic is not the gemm's, since the operands are the maps and the
33    /// filter rather than the unfolded matrices, so only the compute comes
34    /// from here.
35    fn gemm(&self) -> MatmulCost {
36        let [h_out, w_out] = self.spatial_out;
37        let [k_h, k_w] = self.kernel;
38
39        MatmulCost {
40            batches: 1,
41            m: self.batch * h_out * w_out,
42            n: self.channels_out,
43            k: self.channels_in * k_h * k_w,
44            elems: self.elems.clone(),
45        }
46    }
47
48    /// Calculates the compute operations and compulsory memory traffic for the
49    /// convolution.
50    pub fn work(&self) -> Work {
51        let (read, written) = self.traffic();
52
53        Work {
54            compute_ops: self.compute_ops(),
55            bytes: read + written,
56        }
57    }
58
59    /// Operations the contraction performs, `2 * k - 1` per output element.
60    pub fn compute_ops(&self) -> usize {
61        self.gemm().compute_ops()
62    }
63
64    /// Compulsory global traffic in bytes, split by direction, which
65    /// [`work`](Self::work) sums.
66    ///
67    /// The maps and the filter are each moved once. An unfolded operand would
68    /// re-read every overlapping window, which is the implementation's choice
69    /// rather than the convolution's cost.
70    pub fn traffic(&self) -> (usize, usize) {
71        let [h_in, w_in] = self.spatial_in;
72        let [h_out, w_out] = self.spatial_out;
73        let [k_h, k_w] = self.kernel;
74
75        let input = self.batch * self.channels_in * h_in * w_in;
76        let filter = self.channels_out * self.channels_in * k_h * k_w;
77        let output = self.batch * self.channels_out * h_out * w_out;
78
79        (
80            input * self.elems.lhs.size()
81                + filter * self.elems.rhs.size()
82                // A bias is added to the accumulator, so it is stored in the
83                // output's type rather than the filter's.
84                + self.bias_elems * self.elems.out.size(),
85            output * self.elems.out.size(),
86        )
87    }
88
89    /// The probe the contraction's arithmetic runs on, which is the matmul's:
90    /// an accelerated convolution issues the same MMA a gemm does.
91    pub fn compute_key(&self, client: &Client) -> ThroughputKey {
92        self.gemm().compute_key(client)
93    }
94}
95
96/// Minimal representation of a depthwise convolution's cost. Each channel has
97/// its own filter and contracts over nothing else, so this is not the implicit
98/// gemm [`Conv2dCost`] describes.
99#[derive(Debug, Clone, Copy)]
100pub struct DepthwiseCost {
101    pub batch: usize,
102    pub channels: usize,
103    /// The input's spatial extent, square.
104    pub size: usize,
105    /// The output's spatial extent, square.
106    pub out_size: usize,
107    /// The filter's spatial extent, square.
108    pub kernel: usize,
109    /// Element type of the maps and the filter.
110    pub dtype: ElemType,
111}
112
113impl DepthwiseCost {
114    /// Calculates the compute operations and compulsory memory traffic for the
115    /// convolution.
116    pub fn work(&self) -> Work {
117        let (read, written) = self.traffic();
118
119        Work {
120            compute_ops: self.compute_ops(),
121            bytes: read + written,
122        }
123    }
124
125    /// `2 * taps - 1` per output element: one multiply per tap and the adds
126    /// that join them.
127    pub fn compute_ops(&self) -> usize {
128        let taps = self.kernel * self.kernel;
129
130        self.batch * self.out_size * self.out_size * self.channels * (2 * taps).saturating_sub(1)
131    }
132
133    /// Compulsory global traffic in bytes, split by direction, which
134    /// [`work`](Self::work) sums.
135    pub fn traffic(&self) -> (usize, usize) {
136        let maps = self.batch * self.channels;
137        let filter = self.channels * self.kernel * self.kernel;
138        let size = self.dtype.size();
139
140        (
141            (maps * self.size * self.size + filter) * size,
142            maps * self.out_size * self.out_size * size,
143        )
144    }
145
146    /// A depthwise pass contracts over one filter window and issues no MMA, so
147    /// its arithmetic is the scalar peak's.
148    pub fn compute_key(&self) -> ThroughputKey {
149        ThroughputKey {
150            mode: ThroughputMode::ComputeDirect { dtype: self.dtype },
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use cubecl::ir::{ElemType, FloatKind};
159
160    fn cost() -> Conv2dCost {
161        let f16 = ElemType::Float(FloatKind::F16);
162
163        Conv2dCost {
164            batch: 2,
165            channels_in: 3,
166            spatial_in: [8, 8],
167            channels_out: 4,
168            kernel: [3, 3],
169            spatial_out: [6, 6],
170            bias_elems: 0,
171            elems: MatmulGlobalElems {
172                lhs: f16,
173                rhs: f16,
174                out: f16,
175            },
176        }
177    }
178
179    #[test]
180    fn contracts_over_the_filter_window_for_every_output_pixel() {
181        let outputs = 2 * 6 * 6 * 4;
182        let taps = 3 * 3 * 3;
183
184        assert_eq!(cost().compute_ops(), outputs * (2 * taps - 1));
185    }
186
187    #[test]
188    fn moves_the_maps_and_the_filter_once_each() {
189        let (read, written) = cost().traffic();
190
191        assert_eq!(read, (2 * 3 * 8 * 8 + 4 * 3 * 3 * 3) * 2);
192        assert_eq!(written, 2 * 4 * 6 * 6 * 2);
193    }
194
195    #[test]
196    fn a_bias_is_read_alongside_the_filter() {
197        let mut with_bias = cost();
198        with_bias.bias_elems = 4;
199
200        assert_eq!(with_bias.traffic().0, cost().traffic().0 + 4 * 2);
201    }
202}