ruda_test_utils/test_tensor/
io.rs1use ruda_kernel::dsl as kernel_dsl;
27use std::fs::File;
28use std::io::{self, BufReader, BufWriter, Read, Write};
29use std::path::Path;
30
31use ruda_kernel::dsl::zspace::Shape;
32use ruda_kernel::dsl::zspace::Strides;
33
34use crate::test_tensor::host_data::{HostData, HostDataVec};
35
36const MAGIC: &[u8; 4] = b"CKHD";
37const VERSION: u8 = 1;
38
39const TAG_F32: u8 = 0;
40const TAG_I32: u8 = 1;
41const TAG_BOOL: u8 = 2;
42
43pub fn write_host_data(path: &Path, data: &HostData) -> io::Result<u64> {
48 let f = File::create(path)?;
49 let mut w = BufWriter::new(f);
50
51 w.write_all(MAGIC)?;
52 w.write_all(&[VERSION])?;
53
54 let (tag, elem_count) = match &data.data {
55 HostDataVec::F32(v) => (TAG_F32, v.len()),
56 HostDataVec::I32(v) => (TAG_I32, v.len()),
57 HostDataVec::Bool(v) => (TAG_BOOL, v.len()),
58 };
59 w.write_all(&[tag])?;
60
61 let rank = data.shape.as_slice().len();
62 w.write_all(&(rank as u32).to_le_bytes())?;
63 for d in data.shape.as_slice() {
64 w.write_all(&(*d as u64).to_le_bytes())?;
65 }
66 let strides_slice: &[usize] = &data.strides;
67 if strides_slice.len() != rank {
68 return Err(io::Error::new(
69 io::ErrorKind::InvalidInput,
70 format!(
71 "strides rank {} != shape rank {}",
72 strides_slice.len(),
73 rank,
74 ),
75 ));
76 }
77 for s in strides_slice {
78 w.write_all(&(*s as u64).to_le_bytes())?;
79 }
80 w.write_all(&(elem_count as u64).to_le_bytes())?;
81
82 match &data.data {
83 HostDataVec::F32(v) => w.write_all(bytemuck::cast_slice(v))?,
84 HostDataVec::I32(v) => w.write_all(bytemuck::cast_slice(v))?,
85 HostDataVec::Bool(v) => {
86 for b in v {
89 w.write_all(&[u8::from(*b)])?;
90 }
91 }
92 }
93
94 w.flush()?;
95 Ok(w.into_inner()
96 .map_err(|e| e.into_error())?
97 .metadata()?
98 .len())
99}
100
101pub fn read_host_data(path: &Path) -> io::Result<HostData> {
107 let f = File::open(path)?;
108 let mut r = BufReader::new(f);
109
110 let mut magic = [0u8; 4];
111 r.read_exact(&mut magic)?;
112 if &magic != MAGIC {
113 return Err(invalid("wrong magic — file is not a HostData blob"));
114 }
115 let version = read_u8(&mut r)?;
116 if version != VERSION {
117 return Err(invalid(format!(
118 "unsupported HostData file version: {version} (expected {VERSION})"
119 )));
120 }
121 let tag = read_u8(&mut r)?;
122 let rank = read_u32(&mut r)? as usize;
123
124 let mut shape_dims = Vec::with_capacity(rank);
125 for _ in 0..rank {
126 shape_dims.push(read_u64(&mut r)? as usize);
127 }
128 let mut stride_dims = Vec::with_capacity(rank);
129 for _ in 0..rank {
130 stride_dims.push(read_u64(&mut r)? as usize);
131 }
132 let elem_count = read_u64(&mut r)? as usize;
133
134 let data = match tag {
135 TAG_F32 => {
136 let mut buf = vec![0u8; elem_count * std::mem::size_of::<f32>()];
137 r.read_exact(&mut buf)?;
138 let mut v = Vec::with_capacity(elem_count);
141 for chunk in buf.chunks_exact(4) {
142 v.push(f32::from_le_bytes(chunk.try_into().unwrap()));
143 }
144 HostDataVec::F32(v)
145 }
146 TAG_I32 => {
147 let mut buf = vec![0u8; elem_count * std::mem::size_of::<i32>()];
148 r.read_exact(&mut buf)?;
149 let mut v = Vec::with_capacity(elem_count);
150 for chunk in buf.chunks_exact(4) {
151 v.push(i32::from_le_bytes(chunk.try_into().unwrap()));
152 }
153 HostDataVec::I32(v)
154 }
155 TAG_BOOL => {
156 let mut buf = vec![0u8; elem_count];
157 r.read_exact(&mut buf)?;
158 HostDataVec::Bool(buf.into_iter().map(|b| b != 0).collect())
159 }
160 other => return Err(invalid(format!("unknown HostData dtype tag: {other}"))),
161 };
162
163 Ok(HostData {
164 data,
165 shape: Shape::from(shape_dims),
166 strides: Strides::new(&stride_dims),
167 })
168}
169
170fn read_u8<R: Read>(r: &mut R) -> io::Result<u8> {
171 let mut b = [0u8; 1];
172 r.read_exact(&mut b)?;
173 Ok(b[0])
174}
175
176fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
177 let mut b = [0u8; 4];
178 r.read_exact(&mut b)?;
179 Ok(u32::from_le_bytes(b))
180}
181
182fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
183 let mut b = [0u8; 8];
184 r.read_exact(&mut b)?;
185 Ok(u64::from_le_bytes(b))
186}
187
188fn invalid<E: Into<String>>(msg: E) -> io::Error {
189 io::Error::new(io::ErrorKind::InvalidData, msg.into())
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 fn round_trip(label: &str, data: HostData) {
197 let dir =
198 std::env::temp_dir().join(format!("ruda-test-utils-iotest-{}", std::process::id(),));
199 std::fs::create_dir_all(&dir).unwrap();
200 let path = dir.join(format!("blob-{label}.bin"));
201 write_host_data(&path, &data).unwrap();
202 let read_back = read_host_data(&path).unwrap();
203 assert_eq!(data.shape, read_back.shape);
204 assert_eq!(data.strides, read_back.strides);
205 match (&data.data, &read_back.data) {
206 (HostDataVec::F32(a), HostDataVec::F32(b)) => assert_eq!(a, b),
207 (HostDataVec::I32(a), HostDataVec::I32(b)) => assert_eq!(a, b),
208 (HostDataVec::Bool(a), HostDataVec::Bool(b)) => assert_eq!(a, b),
209 _ => panic!("dtype mismatch on round-trip"),
210 }
211 let _ = std::fs::remove_file(&path);
212 }
213
214 #[test]
215 fn round_trip_f32() {
216 round_trip(
217 "f32",
218 HostData {
219 data: HostDataVec::F32(vec![1.0, -2.0, std::f32::consts::PI, 0.5, 0.0]),
220 shape: Shape::from(vec![5]),
221 strides: Strides::new(&[1]),
222 },
223 );
224 }
225
226 #[test]
227 fn round_trip_i32_2d() {
228 round_trip(
229 "i32",
230 HostData {
231 data: HostDataVec::I32(vec![1, 2, 3, 4, 5, 6]),
232 shape: Shape::from(vec![2, 3]),
233 strides: Strides::new(&[3, 1]),
234 },
235 );
236 }
237
238 #[test]
239 fn round_trip_bool() {
240 round_trip(
241 "bool",
242 HostData {
243 data: HostDataVec::Bool(vec![true, false, true, true, false]),
244 shape: Shape::from(vec![5]),
245 strides: Strides::new(&[1]),
246 },
247 );
248 }
249}