Skip to main content

burn_cubecl_fusion/optim/
base.rs

1use crate::{CubeFusionHandle, FallbackOperation};
2use burn_fusion::stream::Context;
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4
5/// The runnable result of fusing a segment of tensor operations — the single
6/// trait the built-in fusion optimizations and user-defined ones implement
7/// alike. A fuser's [`finish`](burn_fusion::OperationFuser::finish) wraps its
8/// fused operation in a [`CubeOptimization`], the type the fusion runtime
9/// executes.
10///
11/// User-defined fused operations are registered through the optimization
12/// registry in `burn-cubecl` (`fusion::register`).
13pub trait FusedOperation: Send + 'static {
14    /// Name of the fused operation — the key serialized execution plans are
15    /// restored by, also shown in diagnostics and fusion logs.
16    const NAME: &'static str;
17
18    /// The serializable state of the fused operation.
19    type State: Serialize + DeserializeOwned;
20
21    /// The number of operations fused.
22    fn num_ops_fused(&self) -> usize;
23
24    /// The highest relative shape id this operation's traces name, if any. See
25    /// [`NumOperations::max_relative_shape_id`](burn_fusion::NumOperations::max_relative_shape_id).
26    fn max_relative_shape_id(&self) -> Option<usize> {
27        None
28    }
29
30    /// Run the fused operation. `fallback` builds the unfused operation at
31    /// the given index within the segment, for implementations that need to
32    /// run part of it unfused (autotune fallbacks).
33    fn run(
34        &mut self,
35        context: &mut Context<CubeFusionHandle>,
36        fallback: &dyn Fn(usize) -> Box<dyn FallbackOperation>,
37    );
38
39    /// The state of the fused operation, from which
40    /// [`from_state`](Self::from_state) recovers it.
41    fn to_state(&self) -> Self::State;
42
43    /// Recover the fused operation from its [state](Self::to_state).
44    fn from_state(device: &cubecl::Device, state: Self::State) -> Self;
45}
46
47/// A fusion optimization ready to run on an execution stream: what fusers
48/// finish and the fusion runtime executes. Wraps any [`FusedOperation`].
49pub struct CubeOptimization {
50    optimization: Box<dyn DynFusedOperation>,
51}
52
53impl CubeOptimization {
54    /// Wrap the fused operation.
55    pub fn new(optimization: impl FusedOperation) -> Self {
56        Self {
57            optimization: Box::new(optimization),
58        }
59    }
60
61    /// The optimization's [name](FusedOperation::NAME).
62    pub fn name(&self) -> &'static str {
63        self.optimization.name()
64    }
65
66    /// The number of operations fused.
67    pub fn num_ops_fused(&self) -> usize {
68        self.optimization.num_ops_fused()
69    }
70
71    /// Run the fused operation. See [`FusedOperation::run`].
72    pub fn run(
73        &mut self,
74        context: &mut Context<CubeFusionHandle>,
75        fallback: &dyn Fn(usize) -> Box<dyn FallbackOperation>,
76    ) {
77        self.optimization.run(context, fallback)
78    }
79
80    /// Serialize the optimization: its name plus its
81    /// [state](FusedOperation::to_state) encoded as bytes.
82    pub fn to_state(&self) -> CubeOptimizationState {
83        self.optimization.to_state()
84    }
85}
86
87impl core::fmt::Debug for CubeOptimization {
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        write!(f, "{} ({} ops)", self.name(), self.num_ops_fused())
90    }
91}
92
93impl burn_fusion::NumOperations for CubeOptimization {
94    fn len(&self) -> usize {
95        self.num_ops_fused()
96    }
97
98    fn max_relative_shape_id(&self) -> Option<usize> {
99        self.optimization.max_relative_shape_id()
100    }
101
102    fn name(&self) -> &'static str {
103        Self::name(self)
104    }
105}
106
107/// Object-safe view of a [`FusedOperation`], implemented for every one of
108/// them below. Private on purpose: the erasure, like the box holding it, is an
109/// implementation detail of [`CubeOptimization`].
110trait DynFusedOperation: Send {
111    fn name(&self) -> &'static str;
112    fn num_ops_fused(&self) -> usize;
113    fn max_relative_shape_id(&self) -> Option<usize>;
114    fn run(
115        &mut self,
116        context: &mut Context<CubeFusionHandle>,
117        fallback: &dyn Fn(usize) -> Box<dyn FallbackOperation>,
118    );
119    fn to_state(&self) -> CubeOptimizationState;
120}
121
122impl<O: FusedOperation> DynFusedOperation for O {
123    fn name(&self) -> &'static str {
124        O::NAME
125    }
126
127    fn num_ops_fused(&self) -> usize {
128        FusedOperation::num_ops_fused(self)
129    }
130
131    fn max_relative_shape_id(&self) -> Option<usize> {
132        FusedOperation::max_relative_shape_id(self)
133    }
134
135    fn run(
136        &mut self,
137        context: &mut Context<CubeFusionHandle>,
138        fallback: &dyn Fn(usize) -> Box<dyn FallbackOperation>,
139    ) {
140        FusedOperation::run(self, context, fallback)
141    }
142
143    fn to_state(&self) -> CubeOptimizationState {
144        CubeOptimizationState::new(O::NAME, &FusedOperation::to_state(self))
145    }
146}
147
148/// Serializable state of a fusion optimization: its name plus its own state
149/// encoded as bytes, so one type covers built-in and user-defined
150/// optimizations alike.
151#[derive(Serialize, Deserialize, Debug)]
152pub struct CubeOptimizationState {
153    /// The optimization's [name](FusedOperation::NAME) — the key
154    /// restoration dispatches on.
155    pub name: String,
156    state: Vec<u8>,
157}
158
159impl CubeOptimizationState {
160    /// Encode the state of the optimization named `name`.
161    pub fn new(name: &str, state: &impl Serialize) -> Self {
162        Self {
163            name: name.to_string(),
164            state: {
165                let mut bytes = Vec::new();
166                ciborium::into_writer(state, &mut bytes)
167                    .expect("fusion optimization state must be serializable");
168                bytes
169            },
170        }
171    }
172
173    /// Decode the optimization's state.
174    ///
175    /// # Panics
176    ///
177    /// When the bytes don't decode as `T` — the state was produced by a
178    /// different optimization sharing the name, or by an incompatible version.
179    pub fn decode<T: DeserializeOwned>(&self) -> T {
180        ciborium::from_reader(self.state.as_slice()).unwrap_or_else(|err| {
181            panic!(
182                "state of fusion optimization `{}` failed to decode: {err}",
183                self.name
184            )
185        })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn state_round_trips_through_bytes() {
195        let state = CubeOptimizationState::new("custom", &vec![3u32, 7]);
196
197        assert_eq!(state.name, "custom");
198        assert_eq!(state.decode::<Vec<u32>>(), vec![3, 7]);
199    }
200}