1#![allow(dead_code)]
8
9use anyhow::{Context, Result};
10use std::collections::HashMap;
11use tracing::{debug, info};
12
13use scirs2_core::random::{thread_rng, Distribution, Normal};
15
16use torsh::core::device::DeviceType;
18use torsh::tensor::Tensor;
19
20use super::types::{DType, Device, LayerInfo, ModelMetadata, TensorInfo, TorshModel};
21
22#[derive(Debug, Clone)]
24pub struct ModelTensor {
25 pub name: String,
27 pub data: Tensor<f32>,
29 pub requires_grad: bool,
31}
32
33impl ModelTensor {
34 pub fn new_random(
36 name: String,
37 shape: Vec<usize>,
38 requires_grad: bool,
39 device: DeviceType,
40 ) -> Result<Self> {
41 let mut rng = thread_rng();
43 let normal = Normal::new(0.0, 0.1).context("Failed to create normal distribution")?;
44
45 let num_elements: usize = shape.iter().product();
46 let data: Vec<f32> = (0..num_elements)
47 .map(|_| normal.sample(&mut rng) as f32)
48 .collect();
49
50 let tensor = Tensor::from_data(data, shape, device)?;
51
52 Ok(Self {
53 name,
54 data: tensor,
55 requires_grad,
56 })
57 }
58
59 pub fn from_data(
61 name: String,
62 data: Vec<f32>,
63 shape: Vec<usize>,
64 requires_grad: bool,
65 device: DeviceType,
66 ) -> Result<Self> {
67 let tensor = Tensor::from_data(data, shape, device)?;
68
69 Ok(Self {
70 name,
71 data: tensor,
72 requires_grad,
73 })
74 }
75
76 pub fn shape(&self) -> Vec<usize> {
78 self.data.shape().dims().to_vec()
79 }
80
81 pub fn numel(&self) -> usize {
83 self.shape().iter().product()
84 }
85
86 pub fn to_bytes(&self) -> Result<Vec<u8>> {
88 let data_vec: Vec<f32> = self.data.to_vec()?;
91 let mut bytes = Vec::with_capacity(data_vec.len() * 4);
92
93 for value in data_vec {
94 bytes.extend_from_slice(&value.to_le_bytes());
95 }
96
97 Ok(bytes)
98 }
99
100 pub fn from_bytes(
102 name: String,
103 bytes: &[u8],
104 shape: Vec<usize>,
105 requires_grad: bool,
106 device: DeviceType,
107 ) -> Result<Self> {
108 let num_elements: usize = shape.iter().product();
109 let expected_bytes = num_elements * 4; if bytes.len() != expected_bytes {
112 anyhow::bail!(
113 "Byte length mismatch: expected {}, got {}",
114 expected_bytes,
115 bytes.len()
116 );
117 }
118
119 let mut data = Vec::with_capacity(num_elements);
120 for chunk in bytes.chunks_exact(4) {
121 let value = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
122 data.push(value);
123 }
124
125 Self::from_data(name, data, shape, requires_grad, device)
126 }
127}
128
129pub fn create_real_model(name: &str, num_layers: usize, device: DeviceType) -> Result<TorshModel> {
131 info!("Creating real model '{}' with {} layers", name, num_layers);
132
133 let mut layers = Vec::new();
134 let mut weights = HashMap::new();
135
136 let mut input_dim = 784; let mut output_dim = 512;
138
139 for i in 0..num_layers {
140 let layer_name = format!("layer_{}", i);
141 let is_last = i == num_layers - 1;
142
143 if is_last {
144 output_dim = 10; }
146
147 let layer = LayerInfo {
149 name: layer_name.clone(),
150 layer_type: "Linear".to_string(),
151 input_shape: vec![input_dim],
152 output_shape: vec![output_dim],
153 parameters: (input_dim * output_dim + output_dim) as u64,
154 trainable: true,
155 config: HashMap::new(),
156 };
157
158 let weight_name = format!("{}.weight", layer_name);
160 let weight_tensor = ModelTensor::new_random(
161 weight_name.clone(),
162 vec![output_dim, input_dim],
163 true,
164 device,
165 )?;
166
167 let bias_name = format!("{}.bias", layer_name);
169 let bias_tensor =
170 ModelTensor::new_random(bias_name.clone(), vec![output_dim], true, device)?;
171
172 let weight_info = TensorInfo {
174 name: weight_name.clone(),
175 shape: weight_tensor.shape(),
176 dtype: DType::F32,
177 requires_grad: weight_tensor.requires_grad,
178 device: Device::Cpu, };
180
181 let bias_info = TensorInfo {
182 name: bias_name.clone(),
183 shape: bias_tensor.shape(),
184 dtype: DType::F32,
185 requires_grad: bias_tensor.requires_grad,
186 device: Device::Cpu,
187 };
188
189 layers.push(layer);
190 weights.insert(weight_name, weight_info);
191 weights.insert(bias_name, bias_info);
192
193 input_dim = output_dim;
194 output_dim = if is_last { 10 } else { output_dim / 2 };
195 }
196
197 let mut metadata = ModelMetadata::default();
198 metadata.format = "torsh".to_string();
199 metadata.version = "0.1.0".to_string();
200 metadata.description = Some(format!("Real {} layer model with torsh-tensor", num_layers));
201 metadata.tags = vec!["real".to_string(), "torsh-tensor".to_string()];
202
203 Ok(TorshModel {
204 layers,
205 weights,
206 metadata,
207 })
208}
209
210pub fn forward_pass(model: &TorshModel, input: &Tensor<f32>) -> Result<Tensor<f32>> {
219 debug!("Performing forward pass through model");
220
221 if model.layers.is_empty() {
222 anyhow::bail!("Cannot run forward pass: model has no layers");
223 }
224
225 let input_shape = input.shape();
227 let input_dims = input_shape.dims();
228 let total_elements: usize = input_dims.iter().product();
229 let first_in = model
230 .layers
231 .first()
232 .and_then(|l| l.input_shape.first().copied())
233 .filter(|&w| w > 0)
234 .unwrap_or(total_elements.max(1));
235
236 let batch = if first_in > 0 && total_elements % first_in == 0 {
237 (total_elements / first_in).max(1)
238 } else {
239 1
240 };
241 let feature_width = if batch > 0 {
242 total_elements / batch
243 } else {
244 total_elements
245 };
246
247 let flat: Vec<f32> = input.to_vec()?;
248 let mut activation = Tensor::from_data(
249 flat,
250 vec![batch.max(1), feature_width.max(1)],
251 DeviceType::Cpu,
252 )?;
253
254 for layer in &model.layers {
255 activation = apply_layer(&activation, layer)?;
256 }
257
258 Ok(activation)
259}
260
261fn apply_layer(input: &Tensor<f32>, layer: &LayerInfo) -> Result<Tensor<f32>> {
263 let current_width = input.shape().dims().last().copied().unwrap_or(1).max(1);
264 let in_features = layer
265 .input_shape
266 .first()
267 .copied()
268 .filter(|&w| w > 0)
269 .unwrap_or(current_width);
270 let out_features = layer.output_shape.first().copied().unwrap_or(1).max(1);
271
272 match layer.layer_type.as_str() {
273 "Linear" | "Dense" => {
274 let weight = Tensor::from_data(
275 vec![0.02f32; in_features * out_features],
276 vec![in_features, out_features],
277 DeviceType::Cpu,
278 )?;
279 let bias = Tensor::zeros(&[1, out_features], DeviceType::Cpu)?;
280 let projected = input.matmul(&weight)?;
281 Ok(projected.add(&bias)?)
282 }
283 "ReLU" => Ok(input.relu()?),
284 "Sigmoid" => Ok(input.sigmoid()?),
285 "Tanh" => Ok(input.tanh()?),
286 _ => {
287 if in_features == out_features {
291 Ok(input.clone())
292 } else {
293 let weight = Tensor::from_data(
294 vec![0.02f32; in_features * out_features],
295 vec![in_features, out_features],
296 DeviceType::Cpu,
297 )?;
298 Ok(input.matmul(&weight)?)
299 }
300 }
301 }
302}
303
304pub fn calculate_real_memory_usage(tensors: &[ModelTensor]) -> usize {
306 tensors.iter().map(|t| t.numel() * 4).sum() }
308
309pub fn validate_tensor_shapes(model: &TorshModel) -> Result<()> {
311 for layer in &model.layers {
312 let weight_name = format!("{}.weight", layer.name);
313
314 if let Some(weight_info) = model.weights.get(&weight_name) {
315 if !layer.output_shape.is_empty() && !weight_info.shape.is_empty() {
317 let expected_output = layer.output_shape[0];
318 let actual_output = weight_info.shape[0];
319
320 if expected_output != actual_output {
321 anyhow::bail!(
322 "Layer {} weight shape mismatch: expected output {}, got {}",
323 layer.name,
324 expected_output,
325 actual_output
326 );
327 }
328 }
329 }
330 }
331
332 Ok(())
333}
334
335pub fn xavier_init(input_dim: usize, output_dim: usize, device: DeviceType) -> Result<Tensor<f32>> {
337 let mut rng = thread_rng();
338
339 let scale = (2.0 / (input_dim + output_dim) as f64).sqrt();
341 let normal = Normal::new(0.0, scale)?;
342
343 let num_elements = input_dim * output_dim;
344 let data: Vec<f32> = (0..num_elements)
345 .map(|_| normal.sample(&mut rng) as f32)
346 .collect();
347
348 Ok(Tensor::from_data(
349 data,
350 vec![output_dim, input_dim],
351 device,
352 )?)
353}
354
355pub fn zero_bias_init(output_dim: usize, device: DeviceType) -> Result<Tensor<f32>> {
357 Ok(Tensor::zeros(&[output_dim], device)?)
358}
359
360pub fn estimate_tensor_flops(
362 operation: &str,
363 input_shape: &[usize],
364 output_shape: &[usize],
365) -> u64 {
366 match operation {
367 "linear" | "matmul" => {
368 let input_size: u64 = input_shape.iter().map(|&x| x as u64).product();
370 let output_size: u64 = output_shape.iter().map(|&x| x as u64).product();
371 2 * input_size * output_size
372 }
373 "relu" | "sigmoid" | "tanh" => {
374 output_shape.iter().map(|&x| x as u64).product()
376 }
377 "conv2d" => {
378 let output_size: u64 = output_shape.iter().map(|&x| x as u64).product();
380 output_size * 9 }
382 _ => {
383 output_shape.iter().map(|&x| x as u64).product()
385 }
386 }
387}
388
389pub fn gradient_check(_model: &TorshModel, _input: &Tensor<f32>, epsilon: f32) -> Result<bool> {
403 debug!("Gradient check requested with epsilon = {}", epsilon);
404
405 anyhow::bail!(
406 "Gradient checking is unavailable for a metadata-only TorshModel: it has \
407 no autograd-tracked parameters or computation graph, so analytical \
408 gradients cannot be computed and compared against finite differences. \
409 Build the model with autograd-enabled tensors to gradient-check it."
410 )
411}
412
413pub fn calculate_tensor_statistics(tensors: &[ModelTensor]) -> HashMap<String, f64> {
415 let mut stats = HashMap::new();
416
417 let total_params: usize = tensors.iter().map(|t| t.numel()).sum();
418 let memory_mb = total_params as f64 * 4.0 / (1024.0 * 1024.0);
419
420 stats.insert("total_parameters".to_string(), total_params as f64);
421 stats.insert("memory_mb".to_string(), memory_mb);
422 stats.insert("num_tensors".to_string(), tensors.len() as f64);
423
424 stats
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_model_tensor_creation() {
433 let tensor =
434 ModelTensor::new_random("test".to_string(), vec![10, 20], true, DeviceType::Cpu)
435 .expect("operation should succeed");
436
437 assert_eq!(tensor.shape(), vec![10, 20]);
438 assert_eq!(tensor.numel(), 200);
439 assert!(tensor.requires_grad);
440 }
441
442 #[test]
443 fn test_real_model_creation() {
444 let model = create_real_model("test_model", 3, DeviceType::Cpu)
445 .expect("create real model should succeed");
446
447 assert_eq!(model.layers.len(), 3);
448 assert!(model.weights.len() >= 6); }
450
451 #[test]
452 fn test_tensor_serialization() {
453 let tensor = ModelTensor::new_random("test".to_string(), vec![5, 5], true, DeviceType::Cpu)
454 .expect("operation should succeed");
455
456 let bytes = tensor.to_bytes().expect("byte conversion should succeed");
457 assert_eq!(bytes.len(), 25 * 4); let reconstructed = ModelTensor::from_bytes(
460 "test".to_string(),
461 &bytes,
462 vec![5, 5],
463 true,
464 DeviceType::Cpu,
465 )
466 .expect("operation should succeed");
467
468 assert_eq!(reconstructed.shape(), tensor.shape());
469 }
470
471 #[test]
472 fn test_xavier_initialization() {
473 let tensor = xavier_init(100, 50, DeviceType::Cpu).expect("xavier init should succeed");
474 assert_eq!(tensor.shape().dims(), &[50, 100]);
475 }
476
477 #[test]
478 fn test_flops_estimation() {
479 let input_shape = vec![128, 784];
480 let output_shape = vec![128, 512];
481
482 let flops = estimate_tensor_flops("linear", &input_shape, &output_shape);
483 assert!(flops > 0);
484 }
485
486 #[test]
487 fn test_forward_pass_real_output_shape() {
488 let model =
491 create_real_model("fp", 3, DeviceType::Cpu).expect("create real model should succeed");
492 let input =
493 Tensor::ones(&[1, 784], DeviceType::Cpu).expect("input creation should succeed");
494
495 let output = forward_pass(&model, &input).expect("forward pass should succeed");
496 let out_dims = output.shape().dims().to_vec();
497 let last = model.layers.last().expect("model has layers").output_shape[0];
498 assert_eq!(out_dims.last().copied(), Some(last));
499 }
500
501 #[test]
502 fn test_gradient_check_is_honest_error() {
503 let model =
505 create_real_model("gc", 2, DeviceType::Cpu).expect("create real model should succeed");
506 let input =
507 Tensor::ones(&[1, 784], DeviceType::Cpu).expect("input creation should succeed");
508 assert!(gradient_check(&model, &input, 1e-5).is_err());
509 }
510}