Skip to main content

ruda_test_utils/test_tensor/
base.rs

1use ruda_kernel::dsl as kernel_dsl;
2use ruda_test_runtime::TestRuntime;
3use ruda_kernel::dsl::client::ComputeClient;
4use ruda_kernel::dsl::ir::ElemType;
5use ruda_kernel::dsl::ir::StorageType;
6use ruda_kernel::dsl::prelude::RudaPrimitive;
7use ruda_kernel::library::tensor::TensorHandle;
8use ruda_kernel::dsl::zspace::Shape;
9use ruda_kernel::dsl::zspace::Strides;
10use ruda_core::quant::scheme::QuantScheme;
11use ruda_kernel::quantization::scheme::QuantStore;
12
13use crate::test_tensor::{
14    arange::build_arange,
15    custom::build_custom,
16    eye::build_eye,
17    host_data::{HostData, HostDataType},
18    quant::apply_quantization,
19    random::build_random,
20    strides::StrideSpec,
21    zeros::build_zeros,
22};
23
24#[derive(Clone)]
25/// Information about a quantized tensor in tests.
26/// This allows marking a tensor as quantized for the kernel dispatcher
27/// while keeping the original unquantized data on the host for reference.
28pub struct QuantizationInfo {
29    /// The scale tensor on the device.
30    pub scale: TensorHandle<TestRuntime>,
31    /// The quantization scheme (e.g., Symmetric, Tensor-wise, etc.)
32    pub scheme: QuantScheme,
33    /// The original unquantized shape of the tensor.
34    pub shape: Shape,
35}
36
37#[derive(Clone)]
38/// A test tensor which might be marked as quantized.
39///
40/// This structure couples the device handle, the host reference data,
41/// and optional quantization metadata. If `quantization` is `Some`,
42/// the handle on the device is expected to contain quantized data
43/// (unless it's a dummy quantization for testing purposes).
44pub struct TestTensor {
45    /// The device handle.
46    pub handle: TensorHandle<TestRuntime>,
47    /// The host data, usually stored in f32 for easy reference comparison.
48    pub host: HostData,
49    /// Optional quantization info.
50    pub quantization: Option<QuantizationInfo>,
51}
52
53#[derive(Clone, Debug)]
54pub enum InputDataType {
55    Standard(StorageType),
56    Quantized(QuantScheme),
57}
58
59impl From<StorageType> for InputDataType {
60    fn from(dtype: StorageType) -> Self {
61        InputDataType::Standard(dtype)
62    }
63}
64
65impl From<ruda_kernel::dsl::ir::ElemType> for InputDataType {
66    fn from(elem: ruda_kernel::dsl::ir::ElemType) -> Self {
67        InputDataType::Standard(StorageType::Scalar(elem))
68    }
69}
70
71impl InputDataType {
72    pub fn storage_type(&self) -> StorageType {
73        match self {
74            InputDataType::Standard(dtype) => *dtype,
75            InputDataType::Quantized(scheme) => {
76                let elem = ElemType::from_quant_value(scheme.value);
77
78                match scheme.store {
79                    QuantStore::Native => StorageType::Scalar(elem),
80                    QuantStore::PackedNative(_) => {
81                        // Uses the format's inherent packing factor (e.g., E2M1x2)
82                        StorageType::Packed(elem, scheme.native_packing())
83                    }
84                    QuantStore::PackedU32(_) => {
85                        // Usually represents multiple small quants in a 32-bit register
86                        // factor would be 4 for 8-bit, 8 for 4-bit, etc.
87                        let factor = scheme.num_quants();
88                        StorageType::Packed(elem, factor)
89                    }
90                }
91            }
92        }
93    }
94
95    pub fn is_quantized(&self) -> bool {
96        matches!(self, InputDataType::Quantized(_))
97    }
98
99    pub fn scheme(&self) -> Option<QuantScheme> {
100        match self {
101            InputDataType::Quantized(scheme) => Some(*scheme),
102            _ => None,
103        }
104    }
105}
106
107pub struct TestInput {
108    base_spec: BaseInputSpec,
109    data_kind: DataKind,
110    input_dtype: InputDataType,
111}
112
113pub enum DataKind {
114    Arange {
115        scale: Option<f32>,
116    },
117    Eye,
118    Zeros,
119    Random {
120        seed: u64,
121        distribution: Distribution,
122    },
123    Custom {
124        data: Vec<f32>,
125    },
126}
127
128impl TestInput {
129    /// Start a fluent builder for a test input.
130    ///
131    /// Defaults: `dtype = f32`, `stride = RowMajor`. Call `.dtype(_)` /
132    /// `.stride(_)` to override, then a finalizer such as `.arange()`,
133    /// `.eye()`, `.zeros()`, `.uniform(seed, lo, hi)`, `.bernoulli(seed, p)`,
134    /// or `.custom(data)` to produce a [`TestInput`] ready to generate.
135    pub fn builder(
136        client: ComputeClient<TestRuntime>,
137        shape: impl Into<Shape>,
138    ) -> TestInputBuilder {
139        TestInputBuilder::new(client, shape.into())
140    }
141
142    pub fn new(
143        client: ComputeClient<TestRuntime>,
144        shape: impl Into<Shape>,
145        dtype: impl Into<InputDataType>,
146        stride_spec: StrideSpec,
147        data_kind: DataKind,
148    ) -> Self {
149        let dtype = dtype.into();
150        let storage_type = match &dtype {
151            InputDataType::Standard(dtype) => *dtype,
152            InputDataType::Quantized(_scheme) => {
153                // For quantized input, the initial data is generated as f32 (Standard)
154                // then it will be quantized in generate_test_tensor.
155                f32::as_type_native_unchecked().storage_type()
156            }
157        };
158
159        let base_spec = BaseInputSpec {
160            client,
161            shape: shape.into(),
162            dtype: storage_type,
163            stride_spec,
164        };
165
166        Self {
167            base_spec,
168            data_kind,
169            input_dtype: dtype,
170        }
171    }
172
173    pub fn generate_with_f32_host_data(self) -> (TensorHandle<TestRuntime>, HostData) {
174        self.generate_host_data(HostDataType::F32)
175    }
176
177    pub fn generate_with_bool_host_data(self) -> (TensorHandle<TestRuntime>, HostData) {
178        self.generate_host_data(HostDataType::Bool)
179    }
180
181    pub fn generate_test_tensor(self) -> TestTensor {
182        let input_dtype = self.input_dtype.clone();
183        let client = self.base_spec.client.clone();
184        let (handle, host) = self.generate_with_f32_host_data();
185
186        let mut tensor = TestTensor {
187            handle,
188            host,
189            quantization: None,
190        };
191
192        if let InputDataType::Quantized(scheme) = input_dtype {
193            apply_quantization(&client, &mut tensor, scheme);
194        }
195
196        tensor
197    }
198
199    pub fn f32_host_data(self) -> HostData {
200        self.generate_host_data(HostDataType::F32).1
201    }
202
203    pub fn bool_host_data(self) -> HostData {
204        self.generate_host_data(HostDataType::Bool).1
205    }
206
207    // Public API returning only TensorHandle
208    pub fn generate_without_host_data(self) -> TensorHandle<TestRuntime> {
209        self.generate()
210    }
211
212    pub fn generate(self) -> TensorHandle<TestRuntime> {
213        let (shape, strides, dtype) = (
214            self.base_spec.shape.clone(),
215            self.base_spec.strides(),
216            self.base_spec.dtype,
217        );
218
219        let mut handle = match self.data_kind {
220            DataKind::Arange { scale } => build_arange(self.base_spec, scale),
221            DataKind::Eye => build_eye(self.base_spec),
222            DataKind::Random { seed, distribution } => {
223                build_random(self.base_spec, seed, distribution)
224            }
225            DataKind::Zeros => build_zeros(self.base_spec),
226            DataKind::Custom { data } => build_custom(self.base_spec, data),
227        };
228        handle.metadata.shape = shape;
229        handle.metadata.strides = strides;
230        handle.dtype = dtype;
231
232        handle
233    }
234
235    fn generate_host_data(
236        self,
237        host_data_type: HostDataType,
238    ) -> (TensorHandle<TestRuntime>, HostData) {
239        let client = self.base_spec.client.clone();
240
241        let tensor_handle = self.generate();
242        let host_data =
243            HostData::from_tensor_handle(&client, tensor_handle.clone(), host_data_type);
244
245        (tensor_handle, host_data)
246    }
247}
248
249pub struct BaseInputSpec {
250    pub client: ComputeClient<TestRuntime>,
251    pub shape: Shape,
252    pub dtype: StorageType,
253    pub stride_spec: StrideSpec,
254}
255
256impl BaseInputSpec {
257    pub(crate) fn strides(&self) -> Strides {
258        self.stride_spec.compute_strides(&self.shape)
259    }
260}
261
262pub struct RandomInputSpec {
263    pub seed: u64,
264    pub distribution: Distribution,
265}
266
267#[derive(Copy, Clone)]
268pub enum Distribution {
269    /// Uniform random over `[lower, upper]`.
270    Uniform(f32, f32),
271    /// Bernoulli random with probability `prob` of `1`.
272    Bernoulli(f32),
273    /// Normal (Gaussian) random with the given `mean` and `std`.
274    Normal { mean: f32, std: f32 },
275}
276
277/// Fluent builder for [`TestInput`].
278///
279/// Use [`TestInput::builder`] to start one. The builder holds the shape,
280/// dtype, and stride spec. Call a finalizer (`arange`, `eye`, `zeros`,
281/// `uniform`, `bernoulli`, `random`, `custom`) to produce a [`TestInput`]
282/// ready to generate a tensor handle, host data, or test tensor.
283///
284/// # Example
285///
286/// ```ignore
287/// use ruda_test_utils::{TestInput, StrideSpec, Distribution};
288///
289/// let (handle, host) = TestInput::builder(client, [4, 4])
290///     .stride(StrideSpec::ColMajor)
291///     .uniform( 0, -1.0, 1.0)
292///     .generate_with_f32_host_data();
293/// ```
294pub struct TestInputBuilder {
295    client: ComputeClient<TestRuntime>,
296    shape: Shape,
297    dtype: Option<InputDataType>,
298    stride_spec: StrideSpec,
299}
300
301impl TestInputBuilder {
302    fn new(client: ComputeClient<TestRuntime>, shape: Shape) -> Self {
303        Self {
304            client,
305            shape,
306            dtype: None,
307            stride_spec: StrideSpec::RowMajor,
308        }
309    }
310
311    /// Override the dtype. Defaults to f32.
312    pub fn dtype(mut self, dtype: impl Into<InputDataType>) -> Self {
313        self.dtype = Some(dtype.into());
314        self
315    }
316
317    /// Override the stride layout. Defaults to [`StrideSpec::RowMajor`].
318    pub fn stride(mut self, stride_spec: StrideSpec) -> Self {
319        self.stride_spec = stride_spec;
320        self
321    }
322
323    fn finalize(self, data_kind: DataKind) -> TestInput {
324        let dtype = self.dtype.unwrap_or_else(|| {
325            InputDataType::Standard(f32::as_type_native_unchecked().storage_type())
326        });
327        TestInput::new(self.client, self.shape, dtype, self.stride_spec, data_kind)
328    }
329
330    /// `0, 1, 2, …` in row-major order.
331    pub fn arange(self) -> TestInput {
332        self.finalize(DataKind::Arange { scale: None })
333    }
334
335    /// `arange` with each value multiplied by `scale`.
336    pub fn arange_scaled(self, scale: f32) -> TestInput {
337        self.finalize(DataKind::Arange { scale: Some(scale) })
338    }
339
340    /// Identity matrix (1 on the diagonal, 0 elsewhere).
341    pub fn eye(self) -> TestInput {
342        self.finalize(DataKind::Eye)
343    }
344
345    /// All-zeros tensor.
346    pub fn zeros(self) -> TestInput {
347        self.finalize(DataKind::Zeros)
348    }
349
350    /// Random tensor with a custom [`Distribution`].
351    pub fn random(self, seed: u64, distribution: Distribution) -> TestInput {
352        self.finalize(DataKind::Random { seed, distribution })
353    }
354
355    /// Uniform random in `[lo, hi]`.
356    pub fn uniform(self, seed: u64, lo: f32, hi: f32) -> TestInput {
357        self.random(seed, Distribution::Uniform(lo, hi))
358    }
359
360    /// Bernoulli random with probability `p` of 1.
361    pub fn bernoulli(self, seed: u64, p: f32) -> TestInput {
362        self.random(seed, Distribution::Bernoulli(p))
363    }
364
365    /// Normal (Gaussian) random with the given `mean` and `std`.
366    pub fn normal(self, seed: u64, mean: f32, std: f32) -> TestInput {
367        self.random(seed, Distribution::Normal { mean, std })
368    }
369
370    /// Tensor populated from an explicit row-major `Vec<f32>`.
371    pub fn custom(self, data: Vec<f32>) -> TestInput {
372        self.finalize(DataKind::Custom { data })
373    }
374
375    /// Evenly-spaced values from `start` to `end` inclusive, populated in
376    /// row-major order. The number of points equals the tensor's element count.
377    ///
378    /// Equivalent to NumPy's `np.linspace(start, end, num=shape.numel()).reshape(shape)`.
379    pub fn linspace(self, start: f32, end: f32) -> TestInput {
380        let num_elems: usize = self.shape.iter().product();
381        let data = if num_elems == 0 {
382            Vec::new()
383        } else if num_elems == 1 {
384            vec![start]
385        } else {
386            let step = (end - start) / (num_elems - 1) as f32;
387            (0..num_elems).map(|i| start + step * i as f32).collect()
388        };
389        self.finalize(DataKind::Custom { data })
390    }
391}