Skip to content

Commit cd64fc3

Browse files
olwangclaude
andcommitted
rsscript: Tensor.f32_to_le_bytes / f32_from_le_bytes (device-buffer serialization)
The fused-backend realize path needs to populate device exec-context buffers from host float data and read results back. The native bitcast can't be used (its f64 widening loses bits above 2^24), and the suite hand-encodes bytes literally. Adds exact f32<->little-endian-bytes conversion (f32::to_le_bytes / from_le_bytes), registered in runtime_abi, the reg-VM dispatch, and tensor.rssi. 2 round-trip tests (incl. a value above 2^24). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0524183 commit cd64fc3

4 files changed

Lines changed: 98 additions & 0 deletions

File tree

crates/rsscript/src/reg_vm/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2121,6 +2121,8 @@ enum RegIntrinsic {
21212121
TensorToF32Slice,
21222122
TensorShape,
21232123
TensorRank,
2124+
TensorF32ToLeBytes,
2125+
TensorF32FromLeBytes,
21242126
TensorMatmul,
21252127
TensorMatmulMetal,
21262128
TensorMetalAvailable,
@@ -5440,6 +5442,8 @@ fn qualified_intrinsic(namespace: &str, name: &str) -> Option<RegIntrinsic> {
54405442
("Tensor", "to_f32_slice") => Some(RegIntrinsic::TensorToF32Slice),
54415443
("Tensor", "shape") => Some(RegIntrinsic::TensorShape),
54425444
("Tensor", "rank") => Some(RegIntrinsic::TensorRank),
5445+
("Tensor", "f32_to_le_bytes") => Some(RegIntrinsic::TensorF32ToLeBytes),
5446+
("Tensor", "f32_from_le_bytes") => Some(RegIntrinsic::TensorF32FromLeBytes),
54435447
("Tensor", "matmul") => Some(RegIntrinsic::TensorMatmul),
54445448
("Tensor", "matmul_metal") => Some(RegIntrinsic::TensorMatmulMetal),
54455449
("Tensor", "metal_available") => Some(RegIntrinsic::TensorMetalAvailable),
@@ -9646,6 +9650,26 @@ impl RegVm {
96469650
let tensor = self.expect_tensor_ref(intrinsic_arg(&self.stack, base, args, 0)?)?;
96479651
Ok(VmValue::Int(rsscript_runtime::tensor_rank(&tensor)))
96489652
}
9653+
RegIntrinsic::TensorF32ToLeBytes => {
9654+
let data = expect_float_list_ref(intrinsic_arg(&self.stack, base, args, 0)?)?;
9655+
let bytes = rsscript_runtime::tensor_f32_to_le_bytes(&data);
9656+
Ok(VmValue::List(Rc::new(RefCell::new(
9657+
bytes.into_iter().map(VmValue::Int).collect(),
9658+
))))
9659+
}
9660+
RegIntrinsic::TensorF32FromLeBytes => {
9661+
let bytes = expect_int_list_ref(intrinsic_arg(&self.stack, base, args, 0)?)?;
9662+
Ok(json_result(
9663+
match rsscript_runtime::tensor_f32_from_le_bytes(&bytes) {
9664+
Ok(values) => Ok(VmValue::List(Rc::new(RefCell::new(
9665+
values.into_iter().map(VmValue::Float).collect(),
9666+
)))),
9667+
Err(error) => Err(tensor_error_value(
9668+
rsscript_runtime::tensor_error_message(&error),
9669+
)),
9670+
},
9671+
))
9672+
}
96499673
RegIntrinsic::TensorMatmul => {
96509674
let a = self.expect_tensor_ref(intrinsic_arg(&self.stack, base, args, 0)?)?;
96519675
let b = self.expect_tensor_ref(intrinsic_arg(&self.stack, base, args, 1)?)?;

crates/rsscript/src/runtime_abi.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,17 @@ const RUNTIME_INTRINSICS: &[RuntimeIntrinsic] = &[
403403
),
404404
runtime_intrinsic("Tensor", "shape", "rsscript_runtime::tensor_shape"),
405405
runtime_intrinsic("Tensor", "rank", "rsscript_runtime::tensor_rank"),
406+
// f32 <-> little-endian device-buffer bytes (fused-backend buffer plumbing)
407+
runtime_intrinsic(
408+
"Tensor",
409+
"f32_to_le_bytes",
410+
"rsscript_runtime::tensor_f32_to_le_bytes",
411+
),
412+
runtime_intrinsic(
413+
"Tensor",
414+
"f32_from_le_bytes",
415+
"rsscript_runtime::tensor_f32_from_le_bytes",
416+
),
406417
runtime_intrinsic("Tensor", "matmul", "rsscript_runtime::tensor_matmul"),
407418
// Metal GPU compute path
408419
runtime_intrinsic(

crates/runtime/src/tensor.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,40 @@ pub fn tensor_matmul(a: &RssTensor, b: &RssTensor) -> Result<RssTensor, TensorEr
203203
})
204204
}
205205

206+
/// Serialize f32 values (narrowed from the f64 host inputs) to little-endian bytes,
207+
/// 4 per value, returned as `i64` in `0..=255`. This is the exact byte layout a
208+
/// CPU/GPU device buffer expects — used by the fused-backend realize path to
209+
/// populate exec-context buffers from host float data (the native bitcast can't be
210+
/// used: its f64 widening loses bits above 2^24). Infallible.
211+
pub fn tensor_f32_to_le_bytes(data: &[f64]) -> Vec<i64> {
212+
let mut out = Vec::with_capacity(data.len() * 4);
213+
for &value in data {
214+
for byte in (value as f32).to_le_bytes() {
215+
out.push(byte as i64);
216+
}
217+
}
218+
out
219+
}
220+
221+
/// Inverse of [`tensor_f32_to_le_bytes`]: decode little-endian f32 bytes (length a
222+
/// multiple of 4; each `i64` is taken modulo 256) back to f64 values. Used to read
223+
/// a realized output device buffer back into host floats. Errors if the byte count
224+
/// is not a multiple of 4.
225+
pub fn tensor_f32_from_le_bytes(bytes: &[i64]) -> Result<Vec<f64>, TensorError> {
226+
if bytes.len() % 4 != 0 {
227+
return Err(TensorError::new(format!(
228+
"f32_from_le_bytes: byte length {} is not a multiple of 4",
229+
bytes.len()
230+
)));
231+
}
232+
let mut out = Vec::with_capacity(bytes.len() / 4);
233+
for chunk in bytes.chunks_exact(4) {
234+
let raw = [chunk[0] as u8, chunk[1] as u8, chunk[2] as u8, chunk[3] as u8];
235+
out.push(f32::from_le_bytes(raw) as f64);
236+
}
237+
Ok(out)
238+
}
239+
206240
/// Whether a Metal GPU is available for the native compute path (false off macOS
207241
/// or when no device is present). Lets the port choose the GPU vs CPU matmul.
208242
pub fn tensor_metal_available() -> bool {
@@ -2365,6 +2399,26 @@ mod tests {
23652399
);
23662400
}
23672401

2402+
#[test]
2403+
fn f32_le_bytes_round_trip() {
2404+
// 1.0 -> [0,0,128,63]; round-trips exactly, including a value above 2^24
2405+
// where the native i32 bitcast would lose precision.
2406+
let data = [1.0, 2.0, 3.0, 16_777_217.0, -0.5];
2407+
let bytes = tensor_f32_to_le_bytes(&data);
2408+
assert_eq!(bytes.len(), data.len() * 4);
2409+
assert_eq!(&bytes[0..4], &[0, 0, 128, 63]); // 1.0f little-endian
2410+
let back = tensor_f32_from_le_bytes(&bytes).unwrap();
2411+
for (g, e) in back.iter().zip(data.iter()) {
2412+
assert_eq!(*g as f32, *e as f32);
2413+
}
2414+
}
2415+
2416+
#[test]
2417+
fn f32_from_le_bytes_rejects_bad_length() {
2418+
let err = tensor_f32_from_le_bytes(&[0, 0, 128]).unwrap_err();
2419+
assert!(tensor_error_message(&err).contains("multiple of 4"));
2420+
}
2421+
23682422
#[test]
23692423
// Literal π/2 and π test inputs for the sin kernel; the lint wants the
23702424
// `consts::PI` constant, but these are deliberate fixed sample points.

packages/ml/interface/tensor.rssi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ pub native fn Tensor.shape(tensor: read Tensor) -> Result<fresh List<Int>, Tenso
2929
pub native fn Tensor.rank(tensor: read Tensor) -> Int
3030
effects(native)
3131

32+
// Serialize f32 values to little-endian device-buffer bytes (4 per value, each in
33+
// 0..=255). Used by the fused-backend realize path to fill exec-context buffers.
34+
pub native fn Tensor.f32_to_le_bytes(data: read List<Float>) -> fresh List<Int>
35+
effects(native)
36+
37+
// Inverse: decode little-endian f32 bytes (length a multiple of 4) to floats.
38+
pub native fn Tensor.f32_from_le_bytes(bytes: read List<Int>) -> Result<fresh List<Float>, TensorError>
39+
effects(native)
40+
3241
// 2-D × 2-D matrix multiply, contracting the inner dims: (m, k) × (k, n) -> (m,
3342
// n). Errors on non-rank-2 operands or an inner-dimension mismatch.
3443
pub native fn Tensor.matmul(

0 commit comments

Comments
 (0)