Skip to main content

burn_cubecl_fusion/
base.rs

1use burn_fusion::stream::Context;
2use burn_std::{
3    DType, Metadata, Shape, Strides,
4    quantization::{QParamTensor, global_scale_dtype},
5    strides,
6};
7use cubecl::quant::scheme::{QuantScheme, ScaleDtype};
8use cubecl::{
9    client::Client,
10    ir::AddressType,
11    prelude::{TensorArg, TensorBinding},
12    zspace::Tiling,
13};
14
15/// Defines a fallback operation when fusion isn't possible.
16pub trait FallbackOperation: Send + Sync {
17    /// Executes the fallback procedure.
18    fn run(&self, context: &mut Context<CubeFusionHandle>);
19}
20
21/// Runtime parameters for quantization. Can be used to construct a scales handle from the base
22/// tensor handle.
23pub type QParams = burn_std::quantization::QParams<QParamTensor>;
24
25/// Handle to be used when fusing operations.
26pub struct CubeFusionHandle {
27    /// Compute client for jit.
28    pub client: Client,
29    /// The buffer where the data are stored.
30    pub handle: cubecl::server::Handle,
31    /// The device of the current tensor.
32    pub device: cubecl::Device,
33    /// The element type of the tensor.
34    pub dtype: DType,
35    /// The strides of the tensor.
36    pub strides: Strides,
37    /// How the tensor is stored: `None` for rows, under `strides` over the shape the IR states;
38    /// otherwise storage tiles, whose physical dims the IR cannot state (it knows the logical
39    /// shape only), so the handle carries them. A fused kernel reads rows and refuses a tiled
40    /// input.
41    pub tiles: Option<Tiles>,
42    /// Quantization runtime parameters, if applicable
43    pub qparams: Option<QParams>,
44}
45
46/// A storage-tiled buffer's physical shape and the tiling that folds it back into the logical
47/// shape the IR states.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct Tiles {
50    /// The buffer's own dims, the fragments of the logical ones.
51    pub shape: Shape,
52    /// How many fragments each logical dim is stored as.
53    pub tiling: Tiling,
54}
55
56impl core::fmt::Debug for CubeFusionHandle {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.write_fmt(format_args!(
59            "CubeFusionHandle {{ device: {:?}, runtime: {}}}",
60            self.device,
61            self.client.name(),
62        ))
63    }
64}
65
66impl Clone for CubeFusionHandle {
67    fn clone(&self) -> Self {
68        Self {
69            client: self.client.clone(),
70            handle: self.handle.clone(),
71            device: self.device.clone(),
72            strides: self.strides.clone(),
73            tiles: self.tiles.clone(),
74            dtype: self.dtype,
75            qparams: self.qparams.clone(),
76        }
77    }
78}
79
80unsafe impl Send for CubeFusionHandle {}
81unsafe impl Sync for CubeFusionHandle {}
82
83impl CubeFusionHandle {
84    /// Return the reference to a tensor handle, `shape` being the logical shape the IR states.
85    pub fn binding(self, shape: Shape) -> TensorBinding {
86        let (shape, tiling) = (self.physical_shape(shape), self.tiling());
87        TensorBinding {
88            handle: self.handle.binding(),
89            strides: self.strides,
90            shape,
91            tiling,
92        }
93    }
94
95    /// The buffer's own dims, `logical` being the shape the IR states: that shape for rows, the
96    /// fragments for storage tiles.
97    pub fn physical_shape(&self, logical: Shape) -> Shape {
98        match &self.tiles {
99            Some(tiles) => tiles.shape.clone(),
100            None => logical,
101        }
102    }
103
104    /// The buffer's metadata, `logical` being the shape the IR states: rows under that shape, or
105    /// the storage tiles the handle carries, folded back to it by their tiling.
106    pub fn metadata(&self, logical: Shape) -> Metadata {
107        match &self.tiles {
108            Some(tiles) => Metadata::new(tiles.shape.clone(), self.strides.clone())
109                .with_tiling(tiles.tiling)
110                .expect("a fusion handle's tiling describes its own rank"),
111            None => Metadata::new(logical, self.strides.clone()),
112        }
113    }
114
115    /// How many fragments each logical dim is stored as; untiled for rows.
116    pub fn tiling(&self) -> Tiling {
117        self.tiles
118            .as_ref()
119            .map(|tiles| tiles.tiling)
120            .unwrap_or(Tiling::UNTILED)
121    }
122
123    pub fn required_address_type(&self) -> AddressType {
124        match self.dtype {
125            DType::QFloat(scheme) => {
126                let len = self.handle.size() as usize * 8 / scheme.size_bits_value();
127                AddressType::from_len(len)
128            }
129            _ => AddressType::from_len(self.handle.size() as usize / self.dtype.size()),
130        }
131    }
132
133    /// Return the reference to a tensor argument.
134    pub fn into_tensor_arg(self, shape: Shape) -> TensorArg {
135        let handle = self.binding(shape);
136        handle.into_tensor_arg()
137    }
138
139    /// Construct a separate tensor for the quantization scales, if present
140    pub fn params(&self, scheme: QuantScheme) -> Option<Self> {
141        let qparams = self.qparams.as_ref()?;
142        // Only the block scale is threaded through below; a two-level scheme's per-tensor scale
143        // would be silently dropped, so refuse rather than build a handle short one factor.
144        assert!(
145            global_scale_dtype(&scheme).is_none(),
146            "fused kernels don't yet support a two-level scheme's per-tensor scale"
147        );
148        let mut handle = self.handle.clone();
149        handle.offset_start = Some(qparams.scales.offset_start as u64);
150        handle.offset_end = Some(qparams.scales.offset_end as u64);
151
152        Some(Self {
153            client: self.client.clone(),
154            handle,
155            device: self.device.clone(),
156            dtype: match scheme.scale_dtype() {
157                ScaleDtype::F32 => DType::F32,
158                ScaleDtype::F16 => DType::F16,
159                ScaleDtype::BF16 => DType::BF16,
160                ScaleDtype::UE8M0 | ScaleDtype::UE4M3 => unimplemented!("Not yet supported"),
161            },
162            strides: qparams.scales.metadata.strides().clone(),
163            tiles: None,
164            qparams: None,
165        })
166    }
167}
168
169pub(crate) fn strides_dyn_rank(shape: &[usize]) -> Strides {
170    let mut strides = strides![0; shape.len()];
171
172    let mut current = 1;
173    shape.iter().enumerate().rev().for_each(|(index, val)| {
174        strides[index] = current;
175        current *= val;
176    });
177
178    strides
179}