Skip to main content

burn_cubecl/fusion/
registry.rs

1use crate::CubeDevice;
2use burn_cubecl_fusion::optim::elemwise::{self, ElementWiseFuser, ElemwiseOptimization};
3use burn_cubecl_fusion::optim::matmul::{self, MatmulFuser, MatmulOptimization};
4use burn_cubecl_fusion::optim::nhwc_relayout::{self, NHWCRelayoutFuser, NHWCRelayoutOptimization};
5use burn_cubecl_fusion::optim::reduce::{self, ReduceFuser, ReduceOptimization, ReduceSettings};
6use burn_cubecl_fusion::optim::reduce_broadcasted::{
7    self, ReduceBroadcastedFuser, ReduceBroadcastedOptimization,
8};
9use burn_cubecl_fusion::optim::{CubeOptimization, CubeOptimizationState, FusedOperation};
10use burn_fusion::OperationFuser;
11use core::any::Any;
12use std::sync::{Mutex, OnceLock};
13
14/// A fuser competing for the operation segments of an execution stream,
15/// [finishing](OperationFuser::finish) into a [`CubeOptimization`]. Wraps any
16/// [`OperationFuser`].
17pub struct CubeFuser {
18    fuser: Box<dyn OperationFuser<CubeOptimization>>,
19}
20
21impl CubeFuser {
22    /// Wrap the fuser.
23    pub fn new(fuser: impl OperationFuser<CubeOptimization> + 'static) -> Self {
24        Self {
25            fuser: Box::new(fuser),
26        }
27    }
28}
29
30/// A user-provided fusion optimization: builds one [`CubeFuser`] per
31/// execution stream, competing with the built-in fusers. The fuser's
32/// [`finish`](OperationFuser::finish) wraps an implementation of
33/// [`FusedOperation`] — normally [`Self::Operation`], which the provided
34/// methods rely on.
35///
36/// Register a provider with [`register`] **at the start of the program**,
37/// before the first tensor operation on the fusion backend.
38pub trait OptimizationProvider: Send + Sync + 'static {
39    /// The fused operation the [fusers](Self::fuser) finish.
40    type Operation: FusedOperation;
41
42    /// Name identifying the optimization — the handle [`remove`] takes, and
43    /// the key serialized execution plans are restored by.
44    fn name(&self) -> &str {
45        Self::Operation::NAME
46    }
47
48    /// Build a fuser for a new execution stream on `device`.
49    fn fuser(&self, device: &CubeDevice) -> CubeFuser;
50
51    /// Recover an optimization produced by this provider's fuser from its
52    /// serialized state — the counterpart of [`FusedOperation::to_state`].
53    fn restore(&self, device: &CubeDevice, state: &CubeOptimizationState) -> CubeOptimization {
54        CubeOptimization::new(Self::Operation::from_state(device, state.decode()))
55    }
56}
57
58/// Object-safe view of an [`OptimizationProvider`], implemented for every one
59/// of them below. Private on purpose: the erasure, like the box holding it,
60/// is an implementation detail of the registry.
61trait DynProvider: Send + Sync {
62    fn fuser(&self, device: &CubeDevice) -> CubeFuser;
63    fn restore(&self, device: &CubeDevice, state: &CubeOptimizationState) -> CubeOptimization;
64}
65
66impl<P: OptimizationProvider> DynProvider for P {
67    fn fuser(&self, device: &CubeDevice) -> CubeFuser {
68        OptimizationProvider::fuser(self, device)
69    }
70
71    fn restore(&self, device: &CubeDevice, state: &CubeOptimizationState) -> CubeOptimization {
72        OptimizationProvider::restore(self, device, state)
73    }
74}
75
76/// The built-in optimizations: providers registered by default for every
77/// runtime, so removal and plan restoration treat them exactly like
78/// user-provided ones.
79struct ElemwiseProvider;
80struct MatmulProvider;
81struct ReduceProvider;
82struct ReduceBroadcastedProvider;
83struct NHWCRelayoutProvider;
84
85impl OptimizationProvider for ElemwiseProvider {
86    type Operation = ElemwiseOptimization;
87
88    fn fuser(&self, device: &CubeDevice) -> CubeFuser {
89        CubeFuser::new(ElementWiseFuser::new(device.clone()))
90    }
91}
92
93impl OptimizationProvider for MatmulProvider {
94    type Operation = MatmulOptimization;
95
96    fn fuser(&self, device: &CubeDevice) -> CubeFuser {
97        CubeFuser::new(MatmulFuser::new(device.clone()))
98    }
99}
100
101impl OptimizationProvider for ReduceProvider {
102    type Operation = ReduceOptimization;
103
104    fn fuser(&self, device: &CubeDevice) -> CubeFuser {
105        CubeFuser::new(ReduceFuser::new(device.clone(), ReduceSettings::Always))
106    }
107}
108
109impl OptimizationProvider for ReduceBroadcastedProvider {
110    type Operation = ReduceBroadcastedOptimization;
111
112    fn fuser(&self, device: &CubeDevice) -> CubeFuser {
113        CubeFuser::new(ReduceBroadcastedFuser::new(device.clone()))
114    }
115}
116
117impl OptimizationProvider for NHWCRelayoutProvider {
118    type Operation = NHWCRelayoutOptimization;
119
120    fn fuser(&self, device: &CubeDevice) -> CubeFuser {
121        CubeFuser::new(NHWCRelayoutFuser::new(device.clone()))
122    }
123}
124
125/// Names of the built-in fusion optimizations, in the order streams try them —
126/// the values [`remove`] accepts besides registered provider names.
127pub const BUILTIN_NAMES: [&str; 5] = [
128    elemwise::NAME,
129    matmul::NAME,
130    reduce::NAME,
131    reduce_broadcasted::NAME,
132    nhwc_relayout::NAME,
133];
134
135/// The default providers seeding a runtime's registry entry.
136fn builtins() -> Vec<(String, Slot)> {
137    vec![
138        slot(ElemwiseProvider),
139        slot(MatmulProvider),
140        slot(ReduceProvider),
141        slot(ReduceBroadcastedProvider),
142        slot(NHWCRelayoutProvider),
143    ]
144}
145
146fn slot(provider: impl OptimizationProvider) -> (String, Slot) {
147    let name = provider.name().to_string();
148    (name, Box::new(ProviderSlot(Box::new(provider))))
149}
150
151/// Error returned by [`register`] and [`remove`].
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum RegistryError {
154    /// The fusion backend service is already running, so the change could not
155    /// be applied consistently across streams.
156    ServiceRunning,
157    /// A provider with the same name is already registered.
158    DuplicateOptimization {
159        /// The conflicting name.
160        name: String,
161    },
162}
163
164impl core::fmt::Display for RegistryError {
165    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166        match self {
167            Self::ServiceRunning => write!(
168                f,
169                "the fusion backend service is already running; \
170                 register or remove fusion optimizations at the start of your program, \
171                 before the first tensor operation on the fusion backend"
172            ),
173            Self::DuplicateOptimization { name } => write!(
174                f,
175                "a fusion optimization named `{name}` is already registered"
176            ),
177        }
178    }
179}
180
181impl std::error::Error for RegistryError {}
182
183/// Register a user-defined fusion optimization.
184///
185/// Every execution stream created after this call includes the provider's
186/// fuser alongside the built-in ones; the fusion search treats them all
187/// equally, picking the best-scoring optimization per segment.
188///
189/// # Errors
190///
191/// Fails with [`RegistryError::ServiceRunning`] once the fusion backend
192/// service has started — call this at the start of the program, before the
193/// first tensor operation on the fusion backend — and with
194/// [`RegistryError::DuplicateOptimization`] when a provider with the same name
195/// is already registered, built-ins included.
196pub fn register(provider: impl OptimizationProvider) -> Result<(), RegistryError> {
197    let (name, slot) = slot(provider);
198    registry().lock().unwrap().register(name, slot)
199}
200
201/// Remove the fusion optimization named `name` — one of the built-ins
202/// ([`BUILTIN_NAMES`]) or a previously [`register`]ed provider.
203///
204/// Removing a name that matches nothing is not an error, so a removal stays
205/// valid when a built-in is renamed or retired.
206///
207/// # Errors
208///
209/// Fails with [`RegistryError::ServiceRunning`] once the fusion backend
210/// service has started; call this at the start of the program.
211pub fn remove(name: &str) -> Result<(), RegistryError> {
212    registry().lock().unwrap().remove(name)
213}
214
215/// Restore the optimization described by `state` through its provider's
216/// [`restore`](OptimizationProvider::restore).
217pub(crate) fn restore(device: &CubeDevice, state: CubeOptimizationState) -> CubeOptimization {
218    let registry = registry().lock().unwrap();
219    registry
220        .provider(&state.name)
221        .map(|slot| downcast(slot).restore(device, &state))
222        .unwrap_or_else(|| {
223            panic!(
224                "no fusion optimization named `{}` is registered; register its \
225                 provider before restoring serialized execution plans",
226                state.name,
227            )
228        })
229}
230
231/// The fusers for a new execution stream: one per registered provider — the
232/// built-ins minus the [`remove`]d ones, plus the user-registered ones. Seals
233/// the registry — streams only exist once the fusion service runs, and later
234/// registrations could not apply to the streams already built.
235pub(crate) fn fusers(device: &CubeDevice) -> Vec<Box<dyn OperationFuser<CubeOptimization>>> {
236    let mut registry = registry().lock().unwrap();
237    registry
238        .start()
239        .providers
240        .iter()
241        .map(|(_, slot)| downcast(slot).fuser(device).fuser)
242        .collect()
243}
244
245fn registry() -> &'static Mutex<Registry> {
246    static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
247    REGISTRY.get_or_init(|| Mutex::new(Registry::seeded(builtins)))
248}
249
250/// Recover the typed provider from a stored slot.
251fn downcast(slot: &Slot) -> &dyn DynProvider {
252    slot.downcast_ref::<ProviderSlot>()
253        .expect("every slot in the registry holds a provider")
254        .0
255        .as_ref()
256}
257
258/// Wraps a provider so it can live in the type-erased registry.
259struct ProviderSlot(Box<dyn DynProvider>);
260
261/// A type-erased provider stored in the non-generic registry core.
262type Slot = Box<dyn Any + Send + Sync>;
263
264/// The providers and the fusion-service flag; sealed against changes once the
265/// service starts.
266struct Registry {
267    providers: Vec<(String, Slot)>,
268    started: bool,
269}
270
271impl Registry {
272    fn seeded(defaults: impl FnOnce() -> Vec<(String, Slot)>) -> Self {
273        Self {
274            providers: defaults(),
275            started: false,
276        }
277    }
278
279    fn register(&mut self, name: String, slot: Slot) -> Result<(), RegistryError> {
280        self.ensure_open()?;
281        if self.providers.iter().any(|(other, _)| *other == name) {
282            return Err(RegistryError::DuplicateOptimization { name });
283        }
284        self.providers.push((name, slot));
285        Ok(())
286    }
287
288    fn remove(&mut self, name: &str) -> Result<(), RegistryError> {
289        self.ensure_open()?;
290        self.providers.retain(|(other, _)| other != name);
291        Ok(())
292    }
293
294    fn start(&mut self) -> &Self {
295        self.started = true;
296        self
297    }
298
299    fn provider(&self, name: &str) -> Option<&Slot> {
300        self.providers
301            .iter()
302            .find(|(other, _)| other == name)
303            .map(|(_, slot)| slot)
304    }
305
306    /// Registrations and removals only apply before the fusion service starts.
307    fn ensure_open(&self) -> Result<(), RegistryError> {
308        if self.started {
309            return Err(RegistryError::ServiceRunning);
310        }
311        Ok(())
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    fn slot() -> Slot {
320        Box::new(())
321    }
322
323    fn empty() -> Registry {
324        Registry::seeded(Vec::new)
325    }
326
327    fn seeded() -> Vec<(String, Slot)> {
328        vec![("builtin".into(), slot())]
329    }
330
331    #[test]
332    fn register_then_remove_round_trips() {
333        let mut registry = empty();
334
335        registry
336            .register("custom".into(), slot())
337            .expect("first registration succeeds");
338        registry.remove("custom").expect("removal succeeds");
339
340        // The provider is gone, so the same name registers again.
341        registry
342            .register("custom".into(), slot())
343            .expect("re-registration after removal succeeds");
344    }
345
346    #[test]
347    fn duplicate_names_are_rejected() {
348        let mut registry = empty();
349
350        registry.register("custom".into(), slot()).unwrap();
351        assert_eq!(
352            registry.register("custom".into(), slot()),
353            Err(RegistryError::DuplicateOptimization {
354                name: "custom".into()
355            })
356        );
357    }
358
359    #[test]
360    fn a_started_registry_is_sealed() {
361        let mut registry = empty();
362        registry.start();
363
364        assert_eq!(
365            registry.register("custom".into(), slot()),
366            Err(RegistryError::ServiceRunning)
367        );
368        assert_eq!(
369            registry.remove("builtin"),
370            Err(RegistryError::ServiceRunning)
371        );
372    }
373
374    #[test]
375    fn provider_lookup_by_name() {
376        let mut registry = empty();
377        registry.register("custom".into(), slot()).unwrap();
378
379        assert!(registry.provider("custom").is_some());
380        assert!(registry.provider("unknown").is_none());
381    }
382
383    #[test]
384    fn defaults_seed_the_registry() {
385        let registry = Registry::seeded(seeded);
386
387        assert!(registry.provider("builtin").is_some());
388    }
389
390    #[test]
391    fn defaults_are_removable_and_reserve_their_name() {
392        let mut registry = Registry::seeded(seeded);
393
394        // A seeded name is held like any other, so it cannot be registered over...
395        assert_eq!(
396            registry.register("builtin".into(), slot()),
397            Err(RegistryError::DuplicateOptimization {
398                name: "builtin".into()
399            })
400        );
401
402        // ...but it is not privileged either: removing it frees the name for a replacement.
403        registry.remove("builtin").unwrap();
404        assert!(registry.provider("builtin").is_none());
405        registry
406            .register("builtin".into(), slot())
407            .expect("the name is free after removal");
408    }
409}