1use std::sync::Arc;
2
3use crate::backend::wgpu::{OFFSET_ALIGN_BYTES, WgpuContext};
4use crate::device::Device;
5use crate::dtype::DType;
6use crate::error::{ForgeError, Result};
7use crate::shape::Shape;
8
9#[derive(Clone, Debug)]
10pub enum CpuStorage {
11 F32(Arc<Vec<f32>>),
12 U32(Arc<Vec<u32>>),
13}
14
15#[derive(Clone)]
16pub struct WgpuStorage {
17 pub ctx: Arc<WgpuContext>,
18 pub buf: Arc<wgpu::Buffer>,
19 pub offset: usize,
21}
22
23impl std::fmt::Debug for WgpuStorage {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "WgpuStorage(offset={})", self.offset)
26 }
27}
28
29#[derive(Clone, Debug)]
30pub enum Storage {
31 Cpu(CpuStorage),
32 Wgpu(WgpuStorage),
33}
34
35#[derive(Clone, Debug)]
37pub struct Tensor {
38 pub(crate) storage: Storage,
39 pub(crate) shape: Shape,
40 pub(crate) dtype: DType,
41}
42
43impl Tensor {
44 pub fn from_f32(data: &[f32], shape: impl Into<Shape>, device: &Device) -> Result<Tensor> {
45 let shape = shape.into();
46 if shape.numel() != data.len() {
47 return Err(ForgeError::Shape(format!(
48 "data length {} does not match shape {shape}",
49 data.len()
50 )));
51 }
52 let storage = match device {
53 Device::Cpu => Storage::Cpu(CpuStorage::F32(Arc::new(data.to_vec()))),
54 Device::Wgpu(ctx) => Storage::Wgpu(WgpuStorage {
55 ctx: ctx.clone(),
56 buf: Arc::new(ctx.upload(bytemuck::cast_slice(data))),
57 offset: 0,
58 }),
59 };
60 Ok(Tensor {
61 storage,
62 shape,
63 dtype: DType::F32,
64 })
65 }
66
67 pub fn from_u32(data: &[u32], shape: impl Into<Shape>, device: &Device) -> Result<Tensor> {
68 let shape = shape.into();
69 if shape.numel() != data.len() {
70 return Err(ForgeError::Shape(format!(
71 "data length {} does not match shape {shape}",
72 data.len()
73 )));
74 }
75 let storage = match device {
76 Device::Cpu => Storage::Cpu(CpuStorage::U32(Arc::new(data.to_vec()))),
77 Device::Wgpu(ctx) => Storage::Wgpu(WgpuStorage {
78 ctx: ctx.clone(),
79 buf: Arc::new(ctx.upload(bytemuck::cast_slice(data))),
80 offset: 0,
81 }),
82 };
83 Ok(Tensor {
84 storage,
85 shape,
86 dtype: DType::U32,
87 })
88 }
89
90 pub fn zeros(shape: impl Into<Shape>, device: &Device) -> Result<Tensor> {
91 let shape = shape.into();
92 Tensor::from_f32(&vec![0.0f32; shape.numel()], shape, device)
93 }
94
95 pub fn shape(&self) -> &Shape {
96 &self.shape
97 }
98
99 pub fn dtype(&self) -> DType {
100 self.dtype
101 }
102
103 pub fn device(&self) -> Device {
104 match &self.storage {
105 Storage::Cpu(_) => Device::Cpu,
106 Storage::Wgpu(s) => Device::Wgpu(s.ctx.clone()),
107 }
108 }
109
110 pub(crate) fn storage(&self) -> &Storage {
111 &self.storage
112 }
113
114 pub fn to_vec_f32(&self) -> Result<Vec<f32>> {
118 if self.dtype != DType::F32 {
119 return Err(ForgeError::Shape("to_vec_f32 on non-f32 tensor".into()));
120 }
121 match &self.storage {
122 Storage::Cpu(CpuStorage::F32(v)) => Ok(v.as_ref().clone()),
123 Storage::Cpu(_) => unreachable!("dtype checked above"),
124 #[cfg(not(target_arch = "wasm32"))]
125 Storage::Wgpu(s) => {
126 let bytes = s
127 .ctx
128 .readback(&s.buf, s.offset * 4, self.shape.numel() * 4)?;
129 Ok(bytemuck::pod_collect_to_vec(&bytes))
130 }
131 #[cfg(target_arch = "wasm32")]
132 Storage::Wgpu(_) => Err(ForgeError::Wgpu(
133 "sync readback unavailable on wasm32; use to_vec_f32_async".into(),
134 )),
135 }
136 }
137
138 pub fn to_vec_u32(&self) -> Result<Vec<u32>> {
139 if self.dtype != DType::U32 {
140 return Err(ForgeError::Shape("to_vec_u32 on non-u32 tensor".into()));
141 }
142 match &self.storage {
143 Storage::Cpu(CpuStorage::U32(v)) => Ok(v.as_ref().clone()),
144 Storage::Cpu(_) => unreachable!("dtype checked above"),
145 #[cfg(not(target_arch = "wasm32"))]
146 Storage::Wgpu(s) => {
147 let bytes = s
148 .ctx
149 .readback(&s.buf, s.offset * 4, self.shape.numel() * 4)?;
150 Ok(bytemuck::pod_collect_to_vec(&bytes))
151 }
152 #[cfg(target_arch = "wasm32")]
153 Storage::Wgpu(_) => Err(ForgeError::Wgpu(
154 "sync readback unavailable on wasm32; use to_vec_u32_async".into(),
155 )),
156 }
157 }
158
159 pub async fn to_vec_f32_async(&self) -> Result<Vec<f32>> {
162 if self.dtype != DType::F32 {
163 return Err(ForgeError::Shape("to_vec_f32 on non-f32 tensor".into()));
164 }
165 match &self.storage {
166 Storage::Cpu(CpuStorage::F32(v)) => Ok(v.as_ref().clone()),
167 Storage::Cpu(_) => unreachable!("dtype checked above"),
168 Storage::Wgpu(s) => {
169 let bytes = s
170 .ctx
171 .readback_async(&s.buf, s.offset * 4, self.shape.numel() * 4)
172 .await?;
173 Ok(bytemuck::pod_collect_to_vec(&bytes))
174 }
175 }
176 }
177
178 pub async fn to_vec_f32_batch(tensors: &[Tensor]) -> Result<Vec<Vec<f32>>> {
186 let mixed = || ForgeError::Shape("to_vec_f32_batch needs one device".into());
187 let mut ctx: Option<&Arc<WgpuContext>> = None;
188 for t in tensors {
189 if t.dtype != DType::F32 {
190 return Err(ForgeError::Shape("to_vec_f32 on non-f32 tensor".into()));
191 }
192 if let Storage::Wgpu(s) = &t.storage {
193 match ctx {
194 None => ctx = Some(&s.ctx),
195 Some(c) if Arc::ptr_eq(c, &s.ctx) => {}
196 Some(_) => return Err(mixed()),
197 }
198 }
199 }
200 let Some(ctx) = ctx else {
201 return tensors.iter().map(Tensor::to_vec_f32).collect();
203 };
204 let mut regions = Vec::with_capacity(tensors.len());
205 for t in tensors {
206 match &t.storage {
207 Storage::Wgpu(s) => {
208 regions.push((s.buf.as_ref(), s.offset * 4, t.shape.numel() * 4))
209 }
210 Storage::Cpu(_) => return Err(mixed()),
213 }
214 }
215 Ok(ctx
216 .readback_many_async(®ions)
217 .await?
218 .iter()
219 .map(|b| bytemuck::pod_collect_to_vec(b))
220 .collect())
221 }
222
223 pub async fn to_vec_u32_async(&self) -> Result<Vec<u32>> {
224 if self.dtype != DType::U32 {
225 return Err(ForgeError::Shape("to_vec_u32 on non-u32 tensor".into()));
226 }
227 match &self.storage {
228 Storage::Cpu(CpuStorage::U32(v)) => Ok(v.as_ref().clone()),
229 Storage::Cpu(_) => unreachable!("dtype checked above"),
230 Storage::Wgpu(s) => {
231 let bytes = s
232 .ctx
233 .readback_async(&s.buf, s.offset * 4, self.shape.numel() * 4)
234 .await?;
235 Ok(bytemuck::pod_collect_to_vec(&bytes))
236 }
237 }
238 }
239
240 pub fn reshape(&self, shape: impl Into<Shape>) -> Result<Tensor> {
242 let shape = shape.into();
243 if shape.numel() != self.shape.numel() {
244 return Err(ForgeError::Shape(format!(
245 "reshape {} -> {shape} changes element count",
246 self.shape
247 )));
248 }
249 Ok(Tensor {
250 storage: self.storage.clone(),
251 shape,
252 dtype: self.dtype,
253 })
254 }
255
256 pub fn narrow_rows(&self, start: usize, count: usize) -> Result<Tensor> {
259 if self.shape.rank() != 2 {
260 return Err(ForgeError::Shape(
261 "narrow_rows needs a rank-2 tensor".into(),
262 ));
263 }
264 let (rows, cols) = (self.shape.dim(0), self.shape.dim(1));
265 if start + count > rows {
266 return Err(ForgeError::Shape(format!(
267 "narrow_rows {start}+{count} out of bounds for {rows} rows"
268 )));
269 }
270 let shape = Shape::new(&[count, cols]);
271 match &self.storage {
272 Storage::Cpu(CpuStorage::F32(v)) => {
273 let slice = v[start * cols..(start + count) * cols].to_vec();
274 Ok(Tensor {
275 storage: Storage::Cpu(CpuStorage::F32(Arc::new(slice))),
276 shape,
277 dtype: self.dtype,
278 })
279 }
280 Storage::Cpu(CpuStorage::U32(v)) => {
281 let slice = v[start * cols..(start + count) * cols].to_vec();
282 Ok(Tensor {
283 storage: Storage::Cpu(CpuStorage::U32(Arc::new(slice))),
284 shape,
285 dtype: self.dtype,
286 })
287 }
288 Storage::Wgpu(s) => {
289 let offset = s.offset + start * cols;
290 if !(offset * self.dtype.size_bytes()).is_multiple_of(OFFSET_ALIGN_BYTES) {
291 return Err(ForgeError::Wgpu(format!(
292 "narrow_rows offset {offset} elems violates {OFFSET_ALIGN_BYTES}-byte alignment"
293 )));
294 }
295 Ok(Tensor {
296 storage: Storage::Wgpu(WgpuStorage {
297 ctx: s.ctx.clone(),
298 buf: s.buf.clone(),
299 offset,
300 }),
301 shape,
302 dtype: self.dtype,
303 })
304 }
305 }
306 }
307}