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