Skip to main content

burn_fusion/
backend.rs

1use crate::{
2    FusionTensor,
3    client::GlobalFusionClient,
4    stream::{Context, OrderedExecution},
5};
6use burn_backend::{
7    Backend, BackendGraph, BackendTypes, DType, DeviceOps, ExecutionError, MemoryPoolUsage,
8    ProfileDuration, ProfileOptions, ProfileToken, SlicedPoolReport, profile_with_tokens,
9    tensor::{BoolTensor, Device, FloatTensor, IntTensor, QuantizedTensor},
10};
11use burn_ir::{BackendIr, HandleContainer, OperationIr, TensorHandle, TensorIr};
12use burn_std::device_handle::CallError;
13use serde::{Serialize, de::DeserializeOwned};
14use std::marker::PhantomData;
15
16/// Get the client for the given device.
17pub fn get_client<B: FusionBackend>(device: &Device<B>) -> Client<B::FusionRuntime> {
18    GlobalFusionClient::load(device)
19}
20
21/// The fusion server could not run a task at all: it panicked, or is gone.
22pub(crate) fn server_error(err: CallError) -> ExecutionError {
23    ExecutionError::with_context(format!("the fusion server failed to run a task: {err:?}"))
24}
25
26/// Enable dynamic operation fusion on a backend that implements [fusion backend](crate::FusionBackend).
27#[derive(Clone, Debug, Default)]
28pub struct Fusion<B: FusionBackend> {
29    _backend: PhantomData<B>,
30}
31
32impl<B: FusionBackend> BackendTypes for Fusion<B> {
33    type Device = B::Device;
34
35    type FloatTensorPrimitive = FusionTensor<B::FusionRuntime>;
36    type IntTensorPrimitive = FusionTensor<B::FusionRuntime>;
37    type BoolTensorPrimitive = FusionTensor<B::FusionRuntime>;
38    type QuantizedTensorPrimitive = FusionTensor<B::FusionRuntime>;
39
40    // Fusion adds nothing to a captured graph: the fused kernels are recorded
41    // on the inner backend's stream, so the graph handle is the inner one.
42    type GraphPrimitive = B::GraphPrimitive;
43}
44
45impl<B: FusionBackend> Backend for Fusion<B> {
46    fn name(device: &Self::Device) -> String {
47        format!("fusion<{}>", B::name(device))
48    }
49
50    fn seed(device: &B::Device, seed: u64) {
51        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
52        let device = device.clone();
53        client.sync(move || B::seed(&device, seed));
54    }
55
56    fn sync(device: &Self::Device) -> Result<(), ExecutionError> {
57        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
58        let device = device.clone();
59        client
60            .try_sync(move || B::sync(&device))
61            .map_err(server_error)?
62    }
63
64    fn profile<O: Send + 'static>(
65        device: &Self::Device,
66        options: ProfileOptions,
67        func: impl FnOnce() -> O + Send,
68    ) -> Result<(O, ProfileDuration), ExecutionError> {
69        // The inner backend's closure-bracketed window would open on the
70        // *calling* thread's stream, but fused operations execute on the
71        // fusion server thread and launch on its stream — and only when the
72        // queue decides to. So the window is opened and closed from that
73        // thread instead, as two tasks in order with the registered
74        // operations. Neither touches the queue: what was still queued when
75        // the window opened may run inside it, and what is still queued when
76        // it closes stays out, unless the caller asked for a flush — the
77        // measurement never changes how the queue batches.
78        //
79        // An inner backend with no windows is measured the way it measures
80        // itself: between two syncs, which drain the queue as they go.
81        profile_with_tokens::<Self, O>(device, options, func)
82    }
83
84    fn profile_start(device: &Self::Device) -> Result<Option<ProfileToken>, ExecutionError> {
85        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
86        let device = device.clone();
87        client
88            .run(move || B::profile_start(&device))
89            .map_err(server_error)?
90    }
91
92    fn profile_end(
93        device: &Self::Device,
94        token: ProfileToken,
95        options: ProfileOptions,
96    ) -> Result<ProfileDuration, ExecutionError> {
97        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
98        let device = device.clone();
99        // The options go on down: a flush here drains this queue, and the
100        // inner backend's own queue (a remote server's) is its to drain.
101        match options.flushes() {
102            true => client.sync(move || B::profile_end(&device, token, options)),
103            false => client
104                .run(move || B::profile_end(&device, token, options))
105                .map_err(server_error)?,
106        }
107    }
108
109    /// Sent through the queue the window's own marks travelled, so it is
110    /// dropped in order with them — and never flushed: abandoning happens on
111    /// a panic, where draining the queue would run the very work that was
112    /// unwinding out from under it.
113    fn profile_abandon(device: &Self::Device, token: ProfileToken) {
114        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
115        let device = device.clone();
116        let _ = client.run(move || B::profile_abandon(&device, token));
117    }
118
119    fn ad_enabled(_device: &Self::Device) -> bool {
120        false
121    }
122
123    fn memory_persistent_allocations<
124        Output: Send,
125        Input: Send,
126        Func: Fn(Input) -> Output + Send,
127    >(
128        device: &Self::Device,
129        input: Input,
130        func: Func,
131    ) -> Output {
132        // The inner backend's closure-bracketed toggle would arm the *calling*
133        // thread's stream, but fused operations execute on the fusion server
134        // thread and allocate on its stream. Arm a standing mode from that
135        // thread instead — `sync` drains previously recorded operations
136        // first, keeping them out of the persistent window.
137        //
138        // Note that sync here doesn't call `B::sync`.
139        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
140        let armed = device.clone();
141        client.sync(move || B::memory_persistent(&armed, true));
142
143        // Record on this thread; the server drains the fused operations onto
144        // its (armed) stream as it goes.
145        let output = func(input);
146
147        // Drain the remaining recording inside the window (`sync` drains
148        // before running its closure), then disarm.
149        let disarmed = device.clone();
150        client.sync(move || B::memory_persistent(&disarmed, false));
151
152        output
153    }
154
155    fn memory_cleanup(device: &Self::Device) {
156        B::memory_cleanup(device)
157    }
158
159    fn memory_pool_report(device: &Self::Device) -> Option<Vec<SlicedPoolReport>> {
160        // Reads the *calling* stream, which is the one the layout was installed
161        // on and the one fused operations allocate from, so it hops exactly as
162        // the install does.
163        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
164        let device = device.clone();
165
166        client.sync(move || B::memory_pool_report(&device))
167    }
168
169    fn memory_pool_usage(device: &Self::Device) -> Option<MemoryPoolUsage> {
170        // Combined across the runtime's streams, so no hop is needed for the
171        // reading itself — but recorded operations have to be drained first or
172        // they are missing from it.
173        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
174        let device = device.clone();
175
176        client.sync(move || B::memory_pool_usage(&device))
177    }
178
179    fn staging<'a, Iter>(data: Iter, device: &Self::Device)
180    where
181        Iter: Iterator<Item = &'a mut burn_backend::TensorData>,
182    {
183        B::staging(data, device);
184    }
185
186    fn supports_dtype(device: &Self::Device, dtype: DType) -> bool {
187        B::supports_dtype(device, dtype)
188    }
189
190    fn dtype_usage(device: &Self::Device, dtype: DType) -> burn_backend::DTypeUsageSet {
191        B::dtype_usage(device, dtype)
192    }
193
194    fn device_count(type_id: u16) -> usize {
195        B::device_count(type_id)
196    }
197
198    fn flush(device: &Self::Device) -> Result<(), ExecutionError> {
199        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
200        let device = device.clone();
201        client
202            .try_sync(move || B::flush(&device))
203            .map_err(server_error)?
204    }
205
206    fn graph_prepare(device: &Self::Device) -> Result<(), ExecutionError> {
207        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
208        let device = device.clone();
209        // `client.sync` drains the lazy queue and runs the closure without a
210        // device sync — the inner backend then arms persistent allocation.
211        client.sync(move || B::graph_prepare(&device))
212    }
213
214    fn graph_start_capture(device: &Self::Device) -> Result<(), ExecutionError> {
215        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
216        let device = device.clone();
217        // Drain any pending fused operations so nothing leaks into the capture
218        // window before it opens, then begin capture on the same stream.
219        client.sync(move || B::graph_start_capture(&device))
220    }
221
222    fn graph_stop_capture(device: &Self::Device) -> Result<BackendGraph<Self>, ExecutionError> {
223        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
224        let device = device.clone();
225        // Drain the capture run's fused kernels onto the captured stream (this
226        // is where they actually launch), then close the capture.
227        client.sync(move || B::graph_stop_capture(&device))
228    }
229
230    unsafe fn graph_replay(
231        device: &Self::Device,
232        graph: &BackendGraph<Self>,
233    ) -> Result<(), ExecutionError> {
234        let client = GlobalFusionClient::<B::FusionRuntime>::load(device);
235        // Drain any queued fused operations onto the stream first (no device
236        // sync), then launch the captured graph after them.
237        client.sync(|| ());
238        // Safety: forwarded verbatim from this method's own contract.
239        unsafe { B::graph_replay(device, graph) }
240    }
241}
242
243/// The status of a [fuser](OperationFuser).
244#[derive(Clone, Debug, Copy, PartialEq, Eq)]
245pub enum FuserStatus {
246    /// No more operations can be fused.
247    Closed,
248    /// More operations can be fused.
249    Open,
250}
251
252/// The properties of a [fuser](OperationFuser).
253#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
254pub struct FuserProperties {
255    /// The score of the optimization, higher is better.
256    pub score: u64,
257    /// If the operation is ready to be executed.
258    pub ready: bool,
259}
260
261/// The fusion operation abstraction allows implementations to fuse many
262/// [tensor operations](OperationIr) into one, improving the performance of the backend.
263///
264///
265/// # Notes
266///
267/// The implementations are free to execute the registered operations the way they want to improve
268/// the speed and efficiency of the computational graph. It doesn't mean that all registered
269/// operations should be fused, but that another way of executing them is more efficient.
270///
271/// Also, it is important to return (FuserStatus::Closed) when no more registered operation can
272/// improve the performance.
273pub trait OperationFuser<O>: Send {
274    /// Register a new [tensor operation](OperationIr).
275    fn fuse(&mut self, operation: &OperationIr);
276    /// Finish the optimization and create a fusion operation.
277    fn finish(&mut self) -> O;
278    /// Reset the state.
279    fn reset(&mut self);
280    /// Return the builder [status](FuserStatus).
281    fn status(&self) -> FuserStatus;
282    /// Return the builder [properties](FuserProperties).
283    fn properties(&self) -> FuserProperties;
284    /// The number of operation fused.
285    fn len(&self) -> usize;
286    /// If no operations are fused.
287    fn is_empty(&self) -> bool {
288        self.len() == 0
289    }
290    /// Clone the optimization builder.
291    fn clone_dyn(&self) -> Box<dyn OperationFuser<O>>;
292}
293
294/// The number of operations contained in the data structure.
295pub trait NumOperations: core::fmt::Debug {
296    /// The number of registered operations.
297    fn len(&self) -> usize;
298    /// If the current optimization is empty.
299    fn is_empty(&self) -> bool {
300        self.len() == 0
301    }
302    /// The name of the optimization.
303    fn name(&self) -> &'static str;
304    /// The highest relative shape id this optimization names, if it names any.
305    /// `None` means the optimization names no relative shape, so it fits any stream.
306    fn max_relative_shape_id(&self) -> Option<usize> {
307        None
308    }
309}
310
311/// The optimization created from a [fuser](OperationFuser).
312pub trait Optimization<R: FusionRuntime>: Send + NumOperations {
313    /// Execute the optimization.
314    fn execute(&mut self, context: &mut Context<R::FusionHandle>, execution: &OrderedExecution<R>);
315
316    /// Returns the state that can be serialized.
317    fn to_state(&self) -> R::OptimizationState;
318    /// Create the optimization from the state.
319    fn from_state(device: &R::FusionDevice, state: R::OptimizationState) -> Self;
320}
321
322/// Type alias for `<R as FusionRuntime>::FusionDevice`.
323pub type FusionDevice<R> = <R as FusionRuntime>::FusionDevice;
324/// Type alias for `<R as FusionRuntime>::FusionHandle`.
325pub type FusionHandle<R> = <R as FusionRuntime>::FusionHandle;
326/// Client alias.
327pub type Client<R> = GlobalFusionClient<R>;
328
329/// Trait that defines a runtime that will benefits from fused operations.
330pub trait FusionRuntime: Send + Sync + Sized + core::fmt::Debug + 'static {
331    /// The state that can be serialized for an optimization.
332    type OptimizationState: Serialize + DeserializeOwned;
333    /// Optimization type for the backend.
334    type Optimization: Optimization<Self>;
335    /// Handle used to store tensor dynamically.
336    type FusionHandle: Clone + Send;
337    /// Device used by the runtime.
338    type FusionDevice: DeviceOps;
339
340    /// The list of fusers that will be used to optimize the computational graph.
341    fn fusers(device: Self::FusionDevice) -> Vec<Box<dyn OperationFuser<Self::Optimization>>>;
342
343    /// Create a cross-stream alias of `handle` to register under a fresh tensor id.
344    ///
345    /// Called by [`MultiStream::tag_shared_view`](crate::stream::MultiStream::tag_shared_view) when
346    /// a tensor is shared from one stream to another. The new handle must be an *independent*
347    /// container entry over the *same* backing buffer, so that consuming one alias (a `ReadWrite`
348    /// last-use that frees its handle) never frees the buffer out from under the other stream.
349    ///
350    /// The default just clones the handle, which is exactly right for local backends whose handle
351    /// is an `Arc`-style refcount over a device buffer — the clone is a new map entry sharing the
352    /// allocation. Backends whose handle is a *remote* resource (the router/remote backend, where
353    /// the handle is a thin id pointing at a server-side tensor) must override this: a bare clone
354    /// keeps the same server id, so all aliases collapse to one server handle and the first consume
355    /// frees it for everyone. Such backends allocate a fresh server id aliasing the same buffer.
356    fn alias_handle(handle: &Self::FusionHandle) -> Self::FusionHandle {
357        handle.clone()
358    }
359
360    /// Reclaim a `ReadWrite` (last-use) handle as the fusion engine drains a block.
361    ///
362    /// The default removes the container entry; the handle's own `Drop` releases the backend
363    /// resource. This is correct for local backends, whose handle is an `Arc`-style buffer refcount.
364    ///
365    /// A runtime whose handle is a *remote* resource must override this. The router/remote backend
366    /// frees a tensor by registering an `OperationIr::Drop`, but a block that ran **already** freed
367    /// it server-side — a replayed op (including any `Drop`) pops its `ReadWrite` inputs there. So
368    /// letting the handle's `Drop` run registers a *second*, redundant `Drop` for the same id (the
369    /// "unfused drop" traffic), and the override suppresses that re-registration.
370    ///
371    /// `ran` is what says whether that holds for *this* operation: a block is drained whether or
372    /// not its operations ran, and one that did not was never replayed, so the server still holds
373    /// its inputs and only the handle's own `Drop` will free them.
374    fn free_handle(
375        handles: &mut HandleContainer<Self::FusionHandle>,
376        tensor: &TensorIr,
377        ran: OperationRan,
378    ) {
379        let _ = ran;
380        handles.free(tensor);
381    }
382}
383
384/// Whether the operation that consumed a tensor ran.
385///
386/// A drained block has usually run everything in it, but an operation whose input held a
387/// [`TensorError`](burn_ir::TensorError) is skipped, and one that panicked stopped part way. Both
388/// are consumed by the queue all the same — the difference is only visible to a backend whose
389/// handles live somewhere the operation never reached.
390#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub enum OperationRan {
392    /// It ran to completion.
393    Yes,
394    /// It was skipped, or it failed part way through.
395    No,
396}
397
398/// Trait that allows an existing [backend](Backend) to specify graph optimizations using
399/// [operation fuser](crate::OperationFuser).
400///
401/// Collective operations on the remote/router interpreter path are driven through the
402/// [`DistributedOps`](burn_backend::distributed::DistributedOps) supertrait of [`Backend`], which
403/// every fusion backend satisfies unconditionally.
404pub trait FusionBackend:
405    BackendIr<Handle = FusionHandle<Self::FusionRuntime>, Device = FusionDevice<Self::FusionRuntime>>
406{
407    /// The runtime used for this backend.
408    type FusionRuntime: FusionRuntime;
409
410    /// Cast a float tensor and returns the resulting handle.
411    fn cast_float(tensor: FloatTensor<Self>, dtype: DType) -> Self::Handle;
412
413    /// Pointer to the full precision fusion backend.
414    type FullPrecisionBackend: FusionBackend<FusionRuntime = Self::FusionRuntime>;
415
416    /// Set the standing persistent-allocation mode of the device stream that
417    /// executes fused operations.
418    ///
419    /// `Fusion`'s `memory_persistent_allocations` cannot bracket the inner
420    /// allocations with a closure: fused operations are recorded on the
421    /// calling thread but execute later, on the fusion execution thread and
422    /// its own device stream. The execution thread arms/disarms this standing
423    /// mode instead (see the `Backend` impl). Defaults to a no-op for
424    /// backends without allocation modes.
425    fn memory_persistent(_device: &Self::Device, _enabled: bool) {}
426}
427
428// Fusion implements `BackendIr` to enable router backend usage.
429impl<B: FusionBackend> BackendIr for Fusion<B> {
430    type Handle = FusionTensor<B::FusionRuntime>;
431
432    fn float_tensor(handle: TensorHandle<Self::Handle>) -> FloatTensor<Self> {
433        handle.handle
434    }
435
436    fn int_tensor(handle: TensorHandle<Self::Handle>) -> IntTensor<Self> {
437        handle.handle
438    }
439
440    fn bool_tensor(handle: TensorHandle<Self::Handle>) -> BoolTensor<Self> {
441        handle.handle
442    }
443
444    fn quantized_tensor(handle: TensorHandle<Self::Handle>) -> QuantizedTensor<Self> {
445        handle.handle
446    }
447
448    fn float_tensor_handle(tensor: FloatTensor<Self>) -> Self::Handle {
449        tensor
450    }
451
452    fn int_tensor_handle(tensor: IntTensor<Self>) -> Self::Handle {
453        tensor
454    }
455
456    fn bool_tensor_handle(tensor: BoolTensor<Self>) -> Self::Handle {
457        tensor
458    }
459
460    fn quantized_tensor_handle(tensor: QuantizedTensor<Self>) -> Self::Handle {
461        tensor
462    }
463}