Skip to main content

ferrotensor_backend/
tensor.rs

1//! GPU-resident tensors.
2
3use crate::pool::BufferPool;
4use crate::WgpuContext;
5use std::sync::Arc;
6use wgpu::{util::DeviceExt, Buffer};
7
8/// Data type for tensors - supports mixed precision training
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum DType {
11    /// 32-bit float (the default).
12    #[default]
13    FP32,
14    /// 16-bit float (half precision, for memory savings).
15    FP16,
16}
17
18impl DType {
19    /// Size of a single element in bytes
20    pub fn size_bytes(&self) -> usize {
21        match self {
22            DType::FP32 => 4,
23            DType::FP16 => 2,
24        }
25    }
26}
27
28/// A dense, row-major tensor stored in a GPU buffer.
29///
30/// Cloning is cheap: clones and [`reshaped`](Self::reshaped) views share
31/// the buffer.
32#[derive(Debug, Clone)]
33pub struct UnifiedTensor {
34    data: Arc<TensorData>,
35    /// Dimensions, outermost first.
36    pub shape: Vec<usize>,
37    /// Element type of the buffer.
38    pub dtype: DType, // Track tensor data type for mixed precision
39}
40
41#[derive(Debug)]
42struct TensorData {
43    buffer: Arc<Buffer>,
44    size_in_bytes: u64,
45    pool: Option<Arc<BufferPool>>,
46}
47
48impl Drop for TensorData {
49    fn drop(&mut self) {
50        if let Some(pool) = &self.pool {
51            pool.return_buffer(self.size_in_bytes, self.buffer.clone());
52        }
53    }
54}
55
56impl UnifiedTensor {
57    /// The underlying GPU buffer.
58    pub fn buffer(&self) -> &Buffer {
59        &self.data.buffer
60    }
61
62    /// Size of the tensor's data in bytes.
63    pub fn size_in_bytes(&self) -> u64 {
64        self.data.size_in_bytes
65    }
66
67    /// Uploads `data` to the start of the buffer.
68    pub fn write_data(&self, context: &WgpuContext, data: &[u8]) {
69        context.queue.write_buffer(self.buffer(), 0, data);
70    }
71}
72
73impl UnifiedTensor {
74    /// Uploads `data` as an `f32` tensor of the given shape.
75    pub fn new(context: &WgpuContext, data: &[f32], shape: &[usize]) -> Self {
76        let size_in_bytes = (data.len() * 4) as u64;
77        let buffer = context
78            .device
79            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
80                label: Some("Tensor"),
81                contents: bytemuck::cast_slice(data),
82                usage: wgpu::BufferUsages::STORAGE
83                    | wgpu::BufferUsages::COPY_DST
84                    | wgpu::BufferUsages::COPY_SRC,
85            });
86        Self {
87            data: Arc::new(TensorData {
88                buffer: Arc::new(buffer),
89                size_in_bytes,
90                pool: None,
91            }),
92            shape: shape.to_vec(),
93            dtype: DType::FP32,
94        }
95    }
96
97    /// An uninitialised `f32` tensor drawn from the context's buffer pool.
98    pub fn empty(context: &WgpuContext, shape: &[usize]) -> Self {
99        Self::empty_with_dtype(context, shape, DType::FP32)
100    }
101
102    /// An uninitialised tensor of `dtype` drawn from the buffer pool.
103    pub fn empty_with_dtype(context: &WgpuContext, shape: &[usize], dtype: DType) -> Self {
104        let size_in_bytes = (shape.iter().product::<usize>() * dtype.size_bytes()) as u64;
105        let buffer = context.pool.get(&context.device, size_in_bytes);
106        Self {
107            data: Arc::new(TensorData {
108                buffer,
109                size_in_bytes,
110                pool: Some(context.pool.clone()),
111            }),
112            shape: shape.to_vec(),
113            dtype,
114        }
115    }
116
117    /// A zero-filled `f32` tensor.
118    pub fn zeros(context: &WgpuContext, shape: &[usize]) -> Self {
119        let size = shape.iter().product::<usize>();
120        let data = vec![0.0f32; size];
121        Self::new(context, &data, shape)
122    }
123
124    /// Uploads raw bytes as an `f32`-typed tensor (e.g. packed quantised data).
125    pub fn from_bytes(context: &WgpuContext, data: &[u8], shape: &[usize]) -> Self {
126        Self::from_bytes_with_dtype(context, data, shape, DType::FP32)
127    }
128
129    /// Uploads raw bytes as a tensor of `dtype`.
130    pub fn from_bytes_with_dtype(
131        context: &WgpuContext,
132        data: &[u8],
133        shape: &[usize],
134        dtype: DType,
135    ) -> Self {
136        let size_in_bytes = data.len() as u64;
137        let buffer = context
138            .device
139            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
140                label: Some("Byte Tensor"),
141                contents: data,
142                usage: wgpu::BufferUsages::STORAGE
143                    | wgpu::BufferUsages::COPY_DST
144                    | wgpu::BufferUsages::COPY_SRC,
145            });
146        Self {
147            data: Arc::new(TensorData {
148                buffer: Arc::new(buffer),
149                size_in_bytes,
150                pool: None,
151            }),
152            shape: shape.to_vec(),
153            dtype,
154        }
155    }
156
157    /// Create an FP16 tensor (half precision for memory savings)
158    pub fn empty_fp16(context: &WgpuContext, shape: &[usize]) -> Self {
159        Self::empty_with_dtype(context, shape, DType::FP16)
160    }
161
162    /// Create a new tensor view with different shape but sharing the same underlying data
163    pub fn reshaped(&self, new_shape: &[usize]) -> Self {
164        // Validation: sizes must match
165        let current_elements: usize = self.shape.iter().product();
166        let new_elements: usize = new_shape.iter().product();
167        assert_eq!(
168            current_elements, new_elements,
169            "Cannot reshape tensor of size {} to {:?}",
170            current_elements, new_shape
171        );
172
173        Self {
174            data: self.data.clone(),
175            shape: new_shape.to_vec(),
176            dtype: self.dtype,
177        }
178    }
179
180    /// Copies the tensor back to the CPU, waiting for queued GPU work.
181    pub async fn to_vec(&self, context: &WgpuContext) -> Vec<f32> {
182        let size = self.size_in_bytes();
183        let staging = context.device.create_buffer(&wgpu::BufferDescriptor {
184            label: Some("Staging"),
185            size,
186            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
187            mapped_at_creation: false,
188        });
189        let mut encoder = context
190            .device
191            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
192        encoder.copy_buffer_to_buffer(&self.data.buffer, 0, &staging, 0, size);
193        context.queue.submit(Some(encoder.finish()));
194
195        let slice = staging.slice(..);
196        let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
197        slice.map_async(wgpu::MapMode::Read, move |_| tx.send(()).unwrap());
198        context.device.poll(wgpu::Maintain::Wait);
199        rx.receive().await;
200
201        let data = slice.get_mapped_range();
202        let res = bytemuck::cast_slice(&data).to_vec();
203        drop(data);
204        staging.unmap();
205        res
206    }
207}