Skip to main content

burn_router/
types.rs

1use alloc::format;
2use alloc::string::String;
3use burn_backend::{
4    DType, Shape, TensorData,
5    backend::{Backend, DeviceId, DeviceOps, ExecutionError},
6    try_read_sync,
7};
8use burn_ir::{BackendIr, OperationIr, TensorHandle, TensorId, TensorIr};
9use burn_std::future::DynFut;
10
11use crate::{
12    ByteBridge, DirectChannel, MultiBackendBridge, RouterTensor, Runner, RunnerChannel,
13    RunnerClient,
14};
15
16/// Implement multi backend types, with enums having one variant per backend.
17macro_rules! impl_multi_backend_types {
18    // Match the default backend and at least one other backend, with rest being optional
19    ($module_name:ident, $DefaultBackend:ident, $($OtherBackend:ident),+) => {
20        /// Module containing the essential types for multi-backend operations.
21        ///
22        /// - `Handle`: the type used to point to a tensor (defined for all backends).
23        /// - `MultiRunnerClient`: a client for multiple runners (each responsible to execute tensor operations on a given backend).
24        /// - `DirectChannel`: a local channel with direct connection to the backend runner clients.
25        /// - `ByteBridge`: a simple multi-backend bridge that transfers tensors via the underlying [tensor data](burn_backend::TensorData).
26        ///
27        /// Each enum type is defined with backend identifiers as variant names (e.g., `B1` and `B2` for dual backends).
28        pub mod $module_name {
29            use super::*;
30
31            /// The type that can be used to point to a tensor of any kind.
32            /// Each backend has its own variant.
33            pub enum Handle<$DefaultBackend: BackendIr, $($OtherBackend: BackendIr),+> {
34                #[allow(missing_docs)]
35                $DefaultBackend($DefaultBackend::Handle),
36                $(
37                    #[allow(missing_docs)]
38                    $OtherBackend($OtherBackend::Handle),
39                )+
40            }
41
42            /// The device type used by a backend.
43            /// Each backend has its own variant.
44            #[derive(Clone, Debug)]
45            pub enum MultiDevice<$DefaultBackend: Backend, $($OtherBackend: Backend),+> {
46                #[allow(missing_docs)]
47                $DefaultBackend($DefaultBackend::Device),
48                $(
49                    #[allow(missing_docs)]
50                    $OtherBackend($OtherBackend::Device),
51                )+
52            }
53            impl<$DefaultBackend: Backend, $($OtherBackend: Backend),+> PartialEq for MultiDevice<$DefaultBackend, $($OtherBackend),+> {
54                fn eq(&self, other: &Self) -> bool {
55                    match (self, other) {
56                        (Self::$DefaultBackend(lhs), Self::$DefaultBackend(rhs)) => lhs == rhs,
57                        $(
58                            (Self::$OtherBackend(lhs), Self::$OtherBackend(rhs)) => lhs == rhs,
59                        )+
60                        _ => false,
61                    }
62                }
63            }
64
65            // Default implementation always returns the first backend's device
66            impl<$DefaultBackend: Backend, $($OtherBackend: Backend),+> Default for MultiDevice<$DefaultBackend, $($OtherBackend),+> {
67                fn default() -> Self {
68                    Self::$DefaultBackend($DefaultBackend::Device::default())
69                }
70            }
71
72            impl<$DefaultBackend: Backend, $($OtherBackend: Backend),+> burn_std::device::Device for MultiDevice<$DefaultBackend, $($OtherBackend),+> {
73                fn from_id(_device_id: DeviceId) -> Self {
74                    // TODO: Should be fix with the new router backend.
75                    Default::default()
76                }
77
78                fn to_id(&self) -> DeviceId {
79                    match self {
80                        Self::$DefaultBackend(device) => device.id(),
81                        $(
82                            Self::$OtherBackend(device) => device.id(),
83                        )+
84                    }
85                }
86
87            }
88
89            impl<$DefaultBackend: Backend, $($OtherBackend: Backend),+> DeviceOps for MultiDevice<$DefaultBackend, $($OtherBackend),+> {}
90
91            /// A local client with multiple runners (each responsible to execute tensor operations on a given backend).
92            #[derive(Clone)]
93            pub enum MultiRunnerClient<$DefaultBackend: BackendIr, $($OtherBackend: BackendIr),+> {
94                #[allow(missing_docs)]
95                $DefaultBackend(Runner<$DefaultBackend>),
96                $(
97                    #[allow(missing_docs)]
98                    $OtherBackend(Runner<$OtherBackend>),
99                )+
100            }
101
102            impl<$DefaultBackend: BackendIr, $($OtherBackend: BackendIr),+> RunnerClient for MultiRunnerClient<$DefaultBackend, $($OtherBackend),+>
103            {
104               type Device = MultiDevice<$DefaultBackend, $($OtherBackend),+>;
105
106                fn register_op(&self, op: OperationIr) {
107                    match self {
108                        Self::$DefaultBackend(runner) => runner.register_op(op),
109                        $(
110                            Self::$OtherBackend(runner) => runner.register_op(op),
111                        )+
112                    }
113                }
114
115                fn read_tensor_async(&self, tensor: TensorIr) -> DynFut<Result<TensorData, ExecutionError>> {
116                    match self {
117                        Self::$DefaultBackend(runner) => runner.read_tensor_async(tensor),
118                        $(
119                            Self::$OtherBackend(runner) => runner.read_tensor_async(tensor),
120                        )+
121                    }
122                }
123
124                fn register_tensor_data(&self, data: TensorData) -> RouterTensor<Self> {
125                    match self {
126                        Self::$DefaultBackend(runner) => {
127                            let desc = runner.register_tensor_data_desc(data);
128                            RouterTensor::new(desc.id, desc.shape, desc.dtype, self.clone())
129                        }
130                        $(
131                            Self::$OtherBackend(runner) => {
132                                let desc = runner.register_tensor_data_desc(data);
133                                RouterTensor::new(desc.id, desc.shape, desc.dtype, self.clone())
134                            }
135                        )+
136                    }
137                }
138
139                fn device(&self) -> Self::Device {
140                    match self {
141                        Self::$DefaultBackend(runner) => MultiDevice::$DefaultBackend(runner.device()),
142                        $(
143                            Self::$OtherBackend(runner) => MultiDevice::$OtherBackend(runner.device()),
144                        )+
145                    }
146                }
147
148                fn sync(&self) -> Result<(), ExecutionError> {
149                    match self {
150                        Self::$DefaultBackend(runner) => runner.sync(),
151                        $(
152                            Self::$OtherBackend(runner) => runner.sync(),
153                        )+
154                    }
155                }
156
157                fn seed(&self, seed: u64) {
158                    match self {
159                        Self::$DefaultBackend(runner) => runner.seed(seed),
160                        $(
161                            Self::$OtherBackend(runner) => runner.seed(seed),
162                        )+
163                    }
164                }
165
166                fn create_empty_handle(&self) -> TensorId {
167                    match self {
168                        Self::$DefaultBackend(runner) => runner.create_empty_handle(),
169                        $(
170                            Self::$OtherBackend(runner) => runner.create_empty_handle(),
171                        )+
172                    }
173                }
174
175                fn dtype_usage(&self, dtype: burn_std::DType) -> burn_backend::DTypeUsageSet {
176                    match self {
177                        Self::$DefaultBackend(runner) => runner.dtype_usage(dtype),
178                        $(
179                            Self::$OtherBackend(runner) => runner.dtype_usage(dtype),
180                        )+
181                    }
182                }
183            }
184
185            impl<$DefaultBackend: BackendIr, $($OtherBackend: BackendIr),+, Br> RunnerChannel for DirectChannel<($DefaultBackend, $($OtherBackend),+), Br>
186            where
187                Br: MultiBackendBridge<TensorHandle = Handle<$DefaultBackend, $($OtherBackend),+>, Device = MultiDevice<$DefaultBackend, $($OtherBackend),+>>,
188            {
189                type Device = Br::Device;
190
191                type Bridge = Br;
192
193                type FloatElem = $DefaultBackend::FloatElem;
194                type IntElem = $DefaultBackend::IntElem;
195                type BoolElem = $DefaultBackend::BoolElem;
196
197                type Client = MultiRunnerClient<$DefaultBackend, $($OtherBackend),+>;
198
199                fn init_client(device: &Self::Device) -> Self::Client {
200                    match device {
201                        MultiDevice::$DefaultBackend(device) => MultiRunnerClient::$DefaultBackend(Runner::new(device.clone())),
202                        $(
203                            MultiDevice::$OtherBackend(device) => MultiRunnerClient::$OtherBackend(Runner::new(device.clone())),
204                        )+
205                    }
206                }
207
208                fn get_tensor_handle(
209                    tensor: &TensorIr,
210                    client: &Self::Client,
211                ) -> <Self::Bridge as MultiBackendBridge>::TensorHandle {
212                    match client {
213                        MultiRunnerClient::$DefaultBackend(runner) => Handle::$DefaultBackend(runner.get_tensor_handle(tensor)),
214                        $(
215                            MultiRunnerClient::$OtherBackend(runner) => Handle::$OtherBackend(runner.get_tensor_handle(tensor)),
216                        )+
217                    }
218                }
219
220                fn register_tensor(
221                    client: &Self::Client,
222                    handle: <Self::Bridge as MultiBackendBridge>::TensorHandle,
223                    shape: Shape,
224                    dtype: DType,
225                ) -> RouterTensor<Self::Client> {
226                    match client {
227                        MultiRunnerClient::$DefaultBackend(runner) => match handle {
228                            Handle::$DefaultBackend(handle) => runner.register_tensor(handle, shape, dtype, client.clone()),
229                            _ => unreachable!("Can't register tensor handle for another backend."),
230                        },
231                        $(
232                            MultiRunnerClient::$OtherBackend(runner) =>  match handle {
233                                Handle::$OtherBackend(handle) => runner.register_tensor(handle, shape, dtype, client.clone()),
234                                _ => unreachable!("Can't register tensor handle for another backend."),
235                            },
236                        )+
237                    }
238                }
239
240                fn name(_device: &Self::Device) -> String {
241                    let mut name = format!("{}", $DefaultBackend::name(&<$DefaultBackend::Device as Default>::default()));
242                    $(
243                        name.push_str(&format!(", {}", $OtherBackend::name(&<$OtherBackend::Device as Default>::default())));
244                    )+
245                    format!("direct<({})>", name)
246                }
247            }
248
249            impl<$DefaultBackend: BackendIr, $($OtherBackend: BackendIr),+> MultiBackendBridge for ByteBridge<($DefaultBackend, $($OtherBackend),+)> {
250                type TensorHandle = Handle<$DefaultBackend, $($OtherBackend),+>;
251                type Device = MultiDevice<$DefaultBackend, $($OtherBackend),+>;
252
253                fn change_backend_float(
254                    tensor: Self::TensorHandle,
255                    shape: Shape,
256                    target_device: &Self::Device,
257                ) -> Self::TensorHandle {
258                    multi_backend_match!(shape, (tensor, target_device) : $DefaultBackend, $($OtherBackend),+)
259                }
260
261                fn change_backend_int(
262                    tensor: Self::TensorHandle,
263                    shape: Shape,
264                    target_device: &Self::Device,
265                ) -> Self::TensorHandle {
266                    multi_backend_match!(shape, (tensor, target_device) : $DefaultBackend, $($OtherBackend),+)
267                }
268
269                fn change_backend_bool(
270                    tensor: Self::TensorHandle,
271                    shape: Shape,
272                    target_device: &Self::Device,
273                ) -> Self::TensorHandle {
274                    multi_backend_match!(shape, (tensor, target_device) : $DefaultBackend, $($OtherBackend),+)
275                }
276
277            }
278        }
279    };
280}
281
282macro_rules! bridge {
283    ($Backend:ident, $handle:expr, $device:expr, $shape:expr) => {{
284        // Bridge for the same backend
285        let tensor = $Backend::float_tensor(TensorHandle {
286            handle: $handle,
287            shape: $shape,
288        });
289        let tensor = $Backend::float_to_device(tensor, $device);
290        let handle = $Backend::float_tensor_handle(tensor);
291        Handle::$Backend(handle)
292    }};
293    ($BackendA:ident, $BackendB:ident, $handle:expr, $device:expr, $shape:expr) => {{
294        // Byte bridge between two backends
295        let tensor = $BackendA::float_tensor(TensorHandle { handle: $handle, shape: $shape });
296        let data = try_read_sync($BackendA::float_into_data(tensor)).unwrap().expect(
297            "Failed to read tensor data synchronously. This can happen on platforms that don't support blocking futures like WASM."
298        );
299        let tensor = $BackendB::float_from_data(data, $device);
300        let handle = $BackendB::float_tensor_handle(tensor);
301        Handle::$BackendB(handle)
302    }};
303}
304
305macro_rules! multi_backend_match {
306    ($shape:expr, ($handle:expr, $device:expr) : $DefaultBackend:ident, $($OtherBackend:ident),+) => {
307        multi_backend_match! (
308            @step
309            $shape,
310            ($handle, $device);
311            {
312                (Handle::$DefaultBackend(handle), MultiDevice::$DefaultBackend(device)) => bridge!($DefaultBackend, handle, device, $shape),
313                $(
314                    (Handle::$DefaultBackend(handle), MultiDevice::$OtherBackend(device)) => bridge!($DefaultBackend, $OtherBackend, handle, device, $shape),
315                    (Handle::$OtherBackend(handle), MultiDevice::$DefaultBackend(device)) => bridge!($OtherBackend, $DefaultBackend, handle, device, $shape),
316                    (Handle::$OtherBackend(handle), MultiDevice::$OtherBackend(device)) => bridge!($OtherBackend, handle, device, $shape),
317                )+
318            };
319            $($OtherBackend),+
320        )
321    };
322
323    (@step
324        $shape:expr,
325        $pats:tt;
326        { $($arms:tt)* };
327        $BackendA:ident,
328        $($OtherBackend:ident),+
329    ) => {
330        multi_backend_match! (
331            @step
332            $shape,
333            $pats;
334            {
335                $($arms)*
336                $(
337                    (Handle::$BackendA(handle), MultiDevice::$OtherBackend(device)) => bridge!($BackendA, $OtherBackend, handle, device, $shape),
338                    (Handle::$OtherBackend(handle), MultiDevice::$BackendA(device)) => bridge!($OtherBackend, $BackendA, handle, device, $shape),
339                )*
340            };
341            $($OtherBackend),*
342        )
343    };
344
345    (@step
346        $shape:expr,
347        ($handle:expr, $device:expr);
348        { $($arms:tt)* };
349        $($BackendA:ident)?
350    ) => {
351        match ($handle, $device) {
352            $($arms)*
353        }
354    };
355}
356
357// Implement multi-backend types and byte bridge for up to 4 backends
358impl_multi_backend_types!(duo, B1, B2);
359impl_multi_backend_types!(trio, B1, B2, B3);
360impl_multi_backend_types!(quad, B1, B2, B3, B4);
361
362#[cfg(not(target_os = "windows"))] // cannot find a wgpu adapter on windows CI
363#[cfg(test)]
364mod tests {
365    use burn_tensor::Tensor;
366
367    use super::*;
368    use crate::tests::TestBackend;
369
370    #[test]
371    fn should_support_dual_byte_bridge() {
372        let device1 = duo::MultiDevice::B1(Default::default());
373        let device2 = duo::MultiDevice::B2(Default::default());
374        let tensor1 = Tensor::<TestBackend, 1>::from_floats([1.0, 2.0, 3.0, 4.0], &device1);
375        let tensor2 = Tensor::<TestBackend, 1>::from_floats([5.0, 6.0, 7.0, 8.0], &device2);
376
377        let tensor1_2 = tensor1.clone().to_device(&device2);
378        tensor1.into_data().assert_eq(&tensor1_2.into_data(), true);
379
380        let tensor2_1 = tensor2.clone().to_device(&device1);
381        tensor2.into_data().assert_eq(&tensor2_1.into_data(), true);
382    }
383}