pub mod call;
pub mod emit;
pub mod emit_styles;
pub mod hir_build;
pub mod ir;
pub mod lower;
pub mod nodeop;
pub mod onnx;
pub mod run;
pub mod verify;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub use call::Lowered;
pub use ir::TorchIr;
pub use verify::ParityReport;
pub fn load_ir(dir: &Path) -> Result<TorchIr> {
let path = dir.join("torch-ir.json");
let text =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let ir: TorchIr =
serde_json::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
anyhow::ensure!(
ir.format == "rlx-torch-ir",
"unexpected IR format {:?}",
ir.format
);
Ok(ir)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleMeta {
pub name: String,
pub inputs: Vec<BundleIo>,
pub output_count: usize,
pub zero_params: Vec<(String, usize)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleIo {
pub name: String,
pub shape: Vec<usize>,
pub dtype: String,
}
pub fn emit_bundle(src_dir: &Path, bundle_dir: &Path, lo: &Lowered) -> Result<()> {
std::fs::create_dir_all(bundle_dir)?;
let hir = hir_build::build_hir(lo)?;
let json = rlx_ir::hir_to_json(&hir).context("serializing HIR")?;
std::fs::write(bundle_dir.join("model.hir.json"), json)?;
std::fs::copy(
src_dir.join("weights.safetensors"),
bundle_dir.join("weights.safetensors"),
)
.context("copying weights into bundle")?;
let meta = BundleMeta {
name: lo.name.clone(),
inputs: lo
.inputs
.iter()
.map(|i| BundleIo {
name: i.name.clone(),
shape: i.shape.clone(),
dtype: format!("{:?}", i.dtype).to_lowercase(),
})
.collect(),
output_count: lo.outputs.len(),
zero_params: lo
.zero_params
.iter()
.map(|z| (z.key.clone(), z.shape.iter().product()))
.collect(),
};
std::fs::write(
bundle_dir.join("meta.json"),
serde_json::to_string_pretty(&meta)?,
)?;
Ok(())
}
#[cfg(feature = "runtime")]
pub fn run_bundle(bundle_dir: &Path, inputs: &[(String, Vec<f32>)]) -> Result<Vec<Vec<f32>>> {
run_bundle_on(bundle_dir, inputs, rlx_runtime::Device::Cpu)
}
#[cfg(feature = "runtime")]
pub fn run_bundle_on(
bundle_dir: &Path,
inputs: &[(String, Vec<f32>)],
device: rlx_runtime::Device,
) -> Result<Vec<Vec<f32>>> {
let hir = rlx_ir::hir_from_json(&std::fs::read_to_string(bundle_dir.join("model.hir.json"))?)
.context("deserializing HIR")?;
let meta: BundleMeta =
serde_json::from_str(&std::fs::read_to_string(bundle_dir.join("meta.json"))?)?;
let weights = run::load_safetensors_f32(&bundle_dir.join("weights.safetensors"))?;
let mut params: std::collections::HashMap<String, Vec<f32>> = std::collections::HashMap::new();
for (k, (data, _)) in &weights {
params.insert(k.clone(), data.clone());
}
for (k, numel) in &meta.zero_params {
params.insert(k.clone(), vec![0.0f32; *numel]);
}
run::run_cpu(hir, ¶ms, &[], inputs, device)
}
#[derive(Debug, Clone)]
pub struct ConvertOptions {
pub emit_bundle: bool,
pub emit_crate: bool,
pub verify: bool,
pub crate_name: Option<String>,
pub rlx_root: PathBuf,
pub emit_style: emit_styles::EmitStyle,
pub device: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConvertReport {
pub model: String,
pub num_inputs: usize,
pub num_params: usize,
pub num_instrs: usize,
pub bundle_dir: Option<String>,
pub crate_dir: Option<String>,
pub parity: Option<ParityReport>,
}
pub fn convert(dir: &Path, opts: &ConvertOptions) -> Result<ConvertReport> {
let ir = load_ir(dir)?;
let lo = lower::lower(&ir)?;
let mut report = ConvertReport {
model: lo.name.clone(),
num_inputs: lo.inputs.len(),
num_params: lo.params.len() + lo.zero_params.len(),
num_instrs: lo.instrs.len(),
bundle_dir: None,
crate_dir: None,
parity: None,
};
if opts.emit_bundle {
let bundle_dir = dir.join("bundle");
emit_bundle(dir, &bundle_dir, &lo)?;
report.bundle_dir = Some(bundle_dir.display().to_string());
}
if opts.emit_crate {
let crate_name = opts
.crate_name
.clone()
.unwrap_or_else(|| format!("rlx-{}", sanitize_crate(&lo.name)));
let crate_dir = dir.join(&crate_name);
emit::emit_crate(
&crate_dir,
&lo,
&crate_name,
&opts.rlx_root,
opts.emit_style,
)?;
report.crate_dir = Some(crate_dir.display().to_string());
}
if opts.verify {
#[cfg(feature = "runtime")]
{
let dev = run::parse_device(&opts.device)?;
let hir = hir_build::build_hir(&lo)?;
report.parity = Some(verify::verify(dir, &lo, hir, dev)?);
}
#[cfg(not(feature = "runtime"))]
{
anyhow::bail!("--verify requires the `runtime` feature");
}
}
Ok(report)
}
fn sanitize_crate(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect()
}
#[cfg(all(test, feature = "runtime"))]
mod tests {
use super::*;
const LINEAR_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "linear",
"inputs": [{"id": "x", "shape": [1, 2], "dtype": "f32"}],
"weights": [
{"id": "p_w", "key": "w", "shape": [2, 2], "dtype": "f32", "kind": "param"},
{"id": "p_b", "key": "b", "shape": [2], "dtype": "f32", "kind": "param"}
],
"nodes": [
{"id": "addmm", "op": "aten.addmm.default",
"args": [{"ref": "p_b"}, {"ref": "x"}, {"ref": "p_w"}],
"out": {"shape": [1, 2], "dtype": "f32"}}
],
"outputs": [{"ref": "addmm", "shape": [1, 2], "dtype": "f32"}]
}"#;
fn linear_params() -> std::collections::HashMap<String, Vec<f32>> {
let mut p = std::collections::HashMap::new();
p.insert("w".to_string(), vec![1.0, 2.0, 3.0, 4.0]);
p.insert("b".to_string(), vec![10.0, 20.0]);
p
}
#[test]
fn lower_build_run_linear() {
let ir: TorchIr = serde_json::from_str(LINEAR_IR).unwrap();
let lo = lower::lower(&ir).unwrap();
assert_eq!(lo.inputs.len(), 1);
assert_eq!(lo.params.len(), 2);
assert_eq!(lo.instrs.len(), 2);
let hir = hir_build::build_hir(&lo).unwrap();
let params = linear_params();
let out = run::run_cpu(
hir,
¶ms,
&[],
&[("x".to_string(), vec![5.0, 6.0])],
rlx_runtime::Device::Cpu,
)
.unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].len(), 2);
assert!((out[0][0] - 33.0).abs() < 1e-4, "got {:?}", out[0]);
assert!((out[0][1] - 54.0).abs() < 1e-4, "got {:?}", out[0]);
}
#[test]
fn hir_json_roundtrip_runs_identically() {
let ir: TorchIr = serde_json::from_str(LINEAR_IR).unwrap();
let lo = lower::lower(&ir).unwrap();
let params = linear_params();
let hir = hir_build::build_hir(&lo).unwrap();
let direct = run::run_cpu(
hir,
¶ms,
&[],
&[("x".to_string(), vec![5.0, 6.0])],
rlx_runtime::Device::Cpu,
)
.unwrap();
let hir2 = hir_build::build_hir(&lo).unwrap();
let json = rlx_ir::hir_to_json(&hir2).unwrap();
let restored = rlx_ir::hir_from_json(&json).unwrap();
let round = run::run_cpu(
restored,
¶ms,
&[],
&[("x".to_string(), vec![5.0, 6.0])],
rlx_runtime::Device::Cpu,
)
.unwrap();
assert_eq!(direct, round);
}
#[test]
fn unsupported_op_is_reported() {
let ir_json = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "m",
"inputs": [{"id": "x", "shape": [2], "dtype": "f32"}],
"weights": [],
"nodes": [{"id": "n", "op": "aten.some_exotic_op.default",
"args": [{"ref": "x"}], "out": {"shape": [2], "dtype": "f32"}}],
"outputs": [{"ref": "n", "shape": [2], "dtype": "f32"}]
}"#;
let ir: TorchIr = serde_json::from_str(ir_json).unwrap();
let err = lower::lower(&ir).unwrap_err().to_string();
assert!(err.contains("some_exotic_op"), "err was: {err}");
}
fn run_ir(ir_json: &str, params: &[(&str, Vec<f32>)], input: (&str, Vec<f32>)) -> Vec<f32> {
let ir: TorchIr = serde_json::from_str(ir_json).unwrap();
let lo = lower::lower(&ir).unwrap();
let hir = hir_build::build_hir(&lo).unwrap();
let pmap: std::collections::HashMap<String, Vec<f32>> = params
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let out = run::run_cpu(
hir,
&pmap,
&[],
&[(input.0.to_string(), input.1)],
rlx_runtime::Device::Cpu,
)
.unwrap();
out.into_iter().next().unwrap()
}
const GN_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "gn",
"inputs": [{"id": "x", "shape": [1, 2, 1, 2], "dtype": "f32"}],
"weights": [
{"id": "p_g", "key": "gn_gamma", "shape": [2], "dtype": "f32", "kind": "param"},
{"id": "p_b", "key": "gn_beta", "shape": [2], "dtype": "f32", "kind": "param"}
],
"nodes": [
{"id": "gn", "op": "aten.native_group_norm.default",
"args": [{"ref": "x"}, {"ref": "p_g"}, {"ref": "p_b"},
{"int": 1}, {"int": 2}, {"int": 2}, {"int": 1}, {"float": 1e-5}],
"out": [{"shape": [1,2,1,2], "dtype": "f32"},
{"shape": [1,1], "dtype": "f32"},
{"shape": [1,1], "dtype": "f32"}]},
{"id": "gi", "op": "_getitem", "args": [{"ref": "gn"}, {"int": 0}],
"out": {"shape": [1,2,1,2], "dtype": "f32"}}
],
"outputs": [{"ref": "gi", "shape": [1,2,1,2], "dtype": "f32"}]
}"#;
#[test]
fn native_group_norm_matches_reference() {
let out = run_ir(
GN_IR,
&[("gn_gamma", vec![1.0, 1.0]), ("gn_beta", vec![0.0, 0.0])],
("x", vec![1.0, 2.0, 3.0, 4.0]),
);
let inv = 1.0f32 / (1.25f32 + 1e-5).sqrt();
let want = [-1.5 * inv, -0.5 * inv, 0.5 * inv, 1.5 * inv];
assert_eq!(out.len(), 4);
for (g, w) in out.iter().zip(want) {
assert!((g - w).abs() < 1e-4, "group_norm {out:?} vs {want:?}");
}
}
const UP2_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "up2",
"inputs": [{"id": "x", "shape": [1, 1, 2, 2], "dtype": "f32"}],
"weights": [],
"nodes": [
{"id": "up", "op": "aten.upsample_nearest2d.default",
"args": [{"ref": "x"}, {"list": [{"int": 4}, {"int": 4}]}],
"out": {"shape": [1,1,4,4], "dtype": "f32"}}
],
"outputs": [{"ref": "up", "shape": [1,1,4,4], "dtype": "f32"}]
}"#;
#[test]
fn upsample_nearest_2x_doubles_pixels() {
let out = run_ir(UP2_IR, &[], ("x", vec![1.0, 2.0, 3.0, 4.0]));
#[rustfmt::skip]
let want = vec![
1.0, 1.0, 2.0, 2.0,
1.0, 1.0, 2.0, 2.0,
3.0, 3.0, 4.0, 4.0,
3.0, 3.0, 4.0, 4.0,
];
assert_eq!(out, want);
}
const UP4_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "up4",
"inputs": [{"id": "x", "shape": [1, 1, 1, 1], "dtype": "f32"}],
"weights": [],
"nodes": [
{"id": "up", "op": "aten.upsample_nearest2d.vec",
"args": [{"ref": "x"}, {"none": true}, {"list": [{"float": 4.0}, {"float": 4.0}]}],
"out": {"shape": [1,1,4,4], "dtype": "f32"}}
],
"outputs": [{"ref": "up", "shape": [1,1,4,4], "dtype": "f32"}]
}"#;
#[test]
fn upsample_nearest_4x_chains_two_steps() {
let lo = lower::lower(&serde_json::from_str::<TorchIr>(UP4_IR).unwrap()).unwrap();
assert_eq!(lo.instrs.len(), 2, "4× should chain two 2× resizes");
let out = run_ir(UP4_IR, &[], ("x", vec![5.0]));
assert_eq!(out, vec![5.0; 16]);
}
const PAD_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "pad",
"inputs": [{"id": "x", "shape": [1, 1, 2, 2], "dtype": "f32"}],
"weights": [],
"nodes": [
{"id": "pd", "op": "aten.constant_pad_nd.default",
"args": [{"ref": "x"}, {"list": [{"int": 1}, {"int": 2}]}, {"float": 7.0}],
"out": {"shape": [1,1,2,5], "dtype": "f32"}}
],
"outputs": [{"ref": "pd", "shape": [1,1,2,5], "dtype": "f32"}]
}"#;
#[test]
fn constant_pad_nd_last_axis_fills_constant() {
let out = run_ir(PAD_IR, &[], ("x", vec![1.0, 2.0, 3.0, 4.0]));
assert_eq!(out, vec![7.0, 1.0, 2.0, 7.0, 7.0, 7.0, 3.0, 4.0, 7.0, 7.0]);
}
fn run_ir_n(ir_json: &str, inputs: &[(&str, Vec<f32>)]) -> Vec<f32> {
let ir: TorchIr = serde_json::from_str(ir_json).unwrap();
let lo = lower::lower(&ir).unwrap();
let hir = hir_build::build_hir(&lo).unwrap();
let ins: Vec<(String, Vec<f32>)> = inputs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let out = run::run_cpu(
hir,
&Default::default(),
&[],
&ins,
rlx_runtime::Device::Cpu,
)
.unwrap();
out.into_iter().next().unwrap()
}
const SPLIT_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "split",
"inputs": [{"id": "x", "shape": [1, 4], "dtype": "f32"}], "weights": [],
"nodes": [
{"id": "sp", "op": "aten.split.Tensor", "args": [{"ref": "x"}, {"int": 2}, {"int": 1}],
"out": [{"shape": [1,2], "dtype": "f32"}, {"shape": [1,2], "dtype": "f32"}]},
{"id": "g1", "op": "_getitem", "args": [{"ref": "sp"}, {"int": 1}],
"out": {"shape": [1,2], "dtype": "f32"}}],
"outputs": [{"ref": "g1", "shape": [1,2], "dtype": "f32"}]
}"#;
#[test]
fn split_tensor_second_piece() {
let out = run_ir(SPLIT_IR, &[], ("x", vec![1.0, 2.0, 3.0, 4.0]));
assert_eq!(out, vec![3.0, 4.0]);
}
const BADDBMM_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "badd",
"inputs": [{"id": "a", "shape": [1,2,2], "dtype": "f32"},
{"id": "b", "shape": [1,2,2], "dtype": "f32"}],
"weights": [],
"nodes": [
{"id": "e", "op": "aten.empty.memory_format",
"args": [{"list": [{"int": 1}, {"int": 2}, {"int": 2}]}],
"out": {"shape": [1,2,2], "dtype": "f32"}},
{"id": "bd", "op": "aten.baddbmm.default",
"args": [{"ref": "e"}, {"ref": "a"}, {"ref": "b"}],
"kwargs": {"beta": {"float": 0.0}, "alpha": {"float": 2.0}},
"out": {"shape": [1,2,2], "dtype": "f32"}}],
"outputs": [{"ref": "bd", "shape": [1,2,2], "dtype": "f32"}]
}"#;
#[test]
fn baddbmm_beta0_alpha_scaled_matmul() {
let out = run_ir_n(
BADDBMM_IR,
&[
("a", vec![1.0, 2.0, 3.0, 4.0]),
("b", vec![1.0, 0.0, 0.0, 1.0]),
],
);
assert_eq!(out, vec![2.0, 4.0, 6.0, 8.0]);
}
const ONES_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "ones",
"inputs": [{"id": "x", "shape": [2], "dtype": "f32"}], "weights": [],
"nodes": [
{"id": "o", "op": "aten.ones_like.default", "args": [{"ref": "x"}],
"out": {"shape": [2], "dtype": "f32"}},
{"id": "y", "op": "aten.add.Tensor", "args": [{"ref": "x"}, {"ref": "o"}],
"out": {"shape": [2], "dtype": "f32"}}],
"outputs": [{"ref": "y", "shape": [2], "dtype": "f32"}]
}"#;
const DYN_BATCH_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "dynlin",
"inputs": [{"id": "x", "shape": [2, 4], "dtype": "f32", "dynamic": [0, -1]}],
"weights": [{"id": "p_w", "key": "w", "shape": [4, 2], "dtype": "f32", "kind": "param"}],
"nodes": [{"id": "mm", "op": "aten.mm.default", "args": [{"ref": "x"}, {"ref": "p_w"}],
"out": {"shape": [2, 2], "dtype": "f32"}}],
"outputs": [{"ref": "mm", "shape": [2, 2], "dtype": "f32"}]
}"#;
#[test]
fn dynamic_batch_runs_at_multiple_sizes() {
let lo = lower::lower(&serde_json::from_str::<TorchIr>(DYN_BATCH_IR).unwrap()).unwrap();
assert!(lo.inputs[0].is_dynamic(), "input axis 0 should be dynamic");
let params: std::collections::HashMap<String, Vec<f32>> = [(
"w".to_string(),
vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
)]
.into_iter()
.collect();
for batch in [2usize, 4usize] {
let hir = hir_build::build_hir(&lo).unwrap();
let x: Vec<f32> = (0..batch * 4).map(|i| i as f32).collect();
let binding = rlx_ir::DimBinding::from_pairs(&[(0, batch)]);
let out = run::run_dynamic(
hir,
¶ms,
&[("x".to_string(), x.clone())],
rlx_runtime::Device::Cpu,
&binding,
)
.unwrap();
assert_eq!(out[0].len(), batch * 2, "batch {batch} output size");
for r in 0..batch {
let (e0, e1) = (x[r * 4] + x[r * 4 + 2], x[r * 4 + 1] + x[r * 4 + 3]);
assert!(
(out[0][r * 2] - e0).abs() < 1e-4 && (out[0][r * 2 + 1] - e1).abs() < 1e-4,
"batch {batch} row {r}: got {:?} want [{e0},{e1}]",
&out[0][r * 2..r * 2 + 2]
);
}
}
}
const SUM_ALL_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "sumall",
"inputs": [{"id": "x", "shape": [2, 3], "dtype": "f32"}],
"weights": [],
"nodes": [{"id": "s", "op": "aten.sum.dim_IntList", "args": [{"ref": "x"}, {"list": []}],
"out": {"shape": [], "dtype": "f32"}}],
"outputs": [{"ref": "s", "shape": [], "dtype": "f32"}]
}"#;
#[test]
fn sum_empty_dims_reduces_all() {
let out = run_ir(SUM_ALL_IR, &[], ("x", vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]));
assert_eq!(out.len(), 1, "empty-dim sum should reduce to a scalar");
assert!(
(out[0] - 21.0).abs() < 1e-5,
"sum of 1..6 = 21, got {out:?}"
);
}
#[test]
fn ones_like_fills_ones() {
let out = run_ir(ONES_IR, &[], ("x", vec![5.0, 7.0]));
assert_eq!(out, vec![6.0, 8.0]);
}
const PS_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "ps",
"inputs": [{"id": "x", "shape": [1, 4, 1, 2], "dtype": "f32"}], "weights": [],
"nodes": [
{"id": "ps", "op": "aten.pixel_shuffle.default", "args": [{"ref": "x"}, {"int": 2}],
"out": {"shape": [1,1,2,4], "dtype": "f32"}}],
"outputs": [{"ref": "ps", "shape": [1,1,2,4], "dtype": "f32"}]
}"#;
#[test]
fn pixel_shuffle_rearranges() {
let out = run_ir(
PS_IR,
&[],
("x", vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]),
);
assert_eq!(out, vec![0.0, 2.0, 1.0, 3.0, 4.0, 6.0, 5.0, 7.0]);
}
const BILIN_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "bl",
"inputs": [{"id": "x", "shape": [1, 1, 2, 2], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "bl", "op": "aten.upsample_bilinear2d.default",
"args": [{"ref": "x"}, {"list": [{"int": 4}, {"int": 4}]}, {"bool": false}],
"out": {"shape": [1,1,4,4], "dtype": "f32"}}],
"outputs": [{"ref": "bl", "shape": [1,1,4,4], "dtype": "f32"}]
}"#;
#[test]
fn upsample_bilinear_align_corners_false() {
let out = run_ir(BILIN_IR, &[], ("x", vec![1.0, 2.0, 3.0, 4.0]));
#[rustfmt::skip]
let want = vec![
1.0, 1.25, 1.75, 2.0,
1.5, 1.75, 2.25, 2.5,
2.5, 2.75, 3.25, 3.5,
3.0, 3.25, 3.75, 4.0,
];
assert_eq!(out.len(), 16);
for (g, w) in out.iter().zip(&want) {
assert!((g - w).abs() < 1e-5, "bilinear {out:?} vs {want:?}");
}
}
const BICUBIC_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "bc",
"inputs": [{"id": "x", "shape": [1, 1, 1, 4], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "bc", "op": "aten.upsample_bicubic2d.default",
"args": [{"ref": "x"}, {"list": [{"int": 1}, {"int": 7}]}, {"bool": true}],
"out": {"shape": [1,1,1,7], "dtype": "f32"}}],
"outputs": [{"ref": "bc", "shape": [1,1,1,7], "dtype": "f32"}]
}"#;
#[test]
fn upsample_bicubic_align_corners_true() {
let out = run_ir(BICUBIC_IR, &[], ("x", vec![0.0, 1.0, 2.0, 3.0]));
let want = [0.0, 0.40625, 1.0, 1.5, 2.0, 2.59375, 3.0];
assert_eq!(out.len(), 7);
for (g, w) in out.iter().zip(want) {
assert!((g - w).abs() < 1e-5, "bicubic {out:?} vs {want:?}");
}
}
const BILIN_AA_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "aa",
"inputs": [{"id": "x", "shape": [1, 1, 1, 4], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "aa", "op": "aten._upsample_bilinear2d_aa.default",
"args": [{"ref": "x"}, {"list": [{"int": 1}, {"int": 2}]}, {"bool": false}],
"out": {"shape": [1,1,1,2], "dtype": "f32"}}],
"outputs": [{"ref": "aa", "shape": [1,1,1,2], "dtype": "f32"}]
}"#;
#[test]
fn upsample_bilinear_aa_downsample() {
let out = run_ir(BILIN_AA_IR, &[], ("x", vec![1.0, 2.0, 3.0, 4.0]));
let want = [12.0f32 / 7.0, 23.0f32 / 7.0];
assert_eq!(out.len(), 2);
for (g, w) in out.iter().zip(want) {
assert!((g - w).abs() < 1e-5, "aa {out:?} vs {want:?}");
}
}
fn gridsample_ir(mode: i64, pad: i64, ac: bool, wo: usize) -> String {
format!(
r#"{{
"format": "rlx-torch-ir", "version": 1, "model_name": "gs",
"inputs": [{{"id": "x", "shape": [1,1,2,2], "dtype": "f32"}},
{{"id": "g", "shape": [1,1,{wo},2], "dtype": "f32"}}], "weights": [],
"nodes": [{{"id": "gs", "op": "aten.grid_sampler_2d.default",
"args": [{{"ref": "x"}}, {{"ref": "g"}}, {{"int": {mode}}}, {{"int": {pad}}}, {{"bool": {ac}}}],
"out": {{"shape": [1,1,1,{wo}], "dtype": "f32"}}}}],
"outputs": [{{"ref": "gs", "shape": [1,1,1,{wo}], "dtype": "f32"}}]
}}"#
)
}
#[test]
fn grid_sample_bilinear_zeros() {
let ir = gridsample_ir(0, 0, true, 3);
let out = run_ir_n(
&ir,
&[
("x", vec![1.0, 2.0, 3.0, 4.0]),
("g", vec![0.0, 0.0, -1.0, -1.0, 3.0, 3.0]),
],
);
let want = [2.5f32, 1.0, 0.0];
for (o, w) in out.iter().zip(want) {
assert!((o - w).abs() < 1e-5, "bilinear/zeros {out:?} vs {want:?}");
}
}
#[test]
fn grid_sample_nearest() {
let ir = gridsample_ir(1, 0, true, 3);
let out = run_ir_n(
&ir,
&[
("x", vec![1.0, 2.0, 3.0, 4.0]),
("g", vec![-1.0, -1.0, 1.0, 1.0, 0.4, 0.4]),
],
);
let want = [1.0f32, 4.0, 4.0];
for (o, w) in out.iter().zip(want) {
assert!((o - w).abs() < 1e-5, "nearest {out:?} vs {want:?}");
}
}
#[test]
fn grid_sample_border() {
let ir = gridsample_ir(0, 1, true, 1);
let out = run_ir_n(
&ir,
&[("x", vec![1.0, 2.0, 3.0, 4.0]), ("g", vec![3.0, 3.0])],
);
assert!((out[0] - 4.0).abs() < 1e-5, "border {out:?}");
}
#[test]
fn grid_sample_reflection() {
let ir = gridsample_ir(0, 2, true, 3);
let out = run_ir_n(
&ir,
&[
("x", vec![1.0, 2.0, 3.0, 4.0]),
("g", vec![3.0, 3.0, 2.0, 2.0, -3.0, -3.0]),
],
);
let want = [1.0f32, 2.5, 4.0];
for (o, w) in out.iter().zip(want) {
assert!((o - w).abs() < 1e-5, "reflection {out:?} vs {want:?}");
}
}
const BICUBIC_GS_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "gsb",
"inputs": [{"id": "x", "shape": [1,1,4,4], "dtype": "f32"},
{"id": "g", "shape": [1,1,2,2], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "gs", "op": "aten.grid_sampler_2d.default",
"args": [{"ref": "x"}, {"ref": "g"}, {"int": 2}, {"int": 1}, {"bool": true}],
"out": {"shape": [1,1,1,2], "dtype": "f32"}}],
"outputs": [{"ref": "gs", "shape": [1,1,1,2], "dtype": "f32"}]
}"#;
#[test]
fn grid_sample_bicubic_constant() {
let out = run_ir_n(
BICUBIC_GS_IR,
&[("x", vec![5.0; 16]), ("g", vec![-0.3, 0.2, 0.5, -0.1])],
);
for o in &out {
assert!((o - 5.0).abs() < 1e-4, "bicubic const {out:?}");
}
}
#[cfg(any(feature = "metal", feature = "mlx"))]
fn run_ir_on(ir_json: &str, input: (&str, Vec<f32>), device: &str) -> Vec<f32> {
let ir: TorchIr = serde_json::from_str(ir_json).unwrap();
let lo = lower::lower(&ir).unwrap();
let hir = hir_build::build_hir(&lo).unwrap();
let dev = run::parse_device(device).unwrap();
run::run_cpu(
hir,
&Default::default(),
&[],
&[(input.0.to_string(), input.1)],
dev,
)
.unwrap()
.into_iter()
.next()
.unwrap()
}
#[cfg(any(feature = "metal", feature = "mlx"))]
fn run_ir_on_n(ir_json: &str, inputs: &[(&str, Vec<f32>)], device: &str) -> Vec<f32> {
let ir: TorchIr = serde_json::from_str(ir_json).unwrap();
let lo = lower::lower(&ir).unwrap();
let hir = hir_build::build_hir(&lo).unwrap();
let dev = run::parse_device(device).unwrap();
let ins: Vec<(String, Vec<f32>)> = inputs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
run::run_cpu(hir, &Default::default(), &[], &ins, dev)
.unwrap()
.into_iter()
.next()
.unwrap()
}
#[cfg(feature = "metal")]
#[test]
fn grid_sample_metal_matches_cpu() {
let x = vec![1.0f32, 2.0, 3.0, 4.0];
let g = vec![0.3f32, -0.2, -0.7, 0.6, 1.4, -1.4];
for (mode, pad) in [(0i64, 0i64), (1, 0), (0, 1), (0, 2)] {
let ir = gridsample_ir(mode, pad, false, 3);
let cpu = run_ir_on_n(&ir, &[("x", x.clone()), ("g", g.clone())], "cpu");
let gpu = run_ir_on_n(&ir, &[("x", x.clone()), ("g", g.clone())], "metal");
for (a, b) in gpu.iter().zip(&cpu) {
assert!(
(a - b).abs() < 1e-4,
"metal mode={mode} pad={pad} {gpu:?} vs {cpu:?}"
);
}
}
}
#[cfg(feature = "mlx")]
#[test]
fn grid_sample_mlx_matches_cpu() {
let x = vec![1.0f32, 2.0, 3.0, 4.0];
let g = vec![0.3f32, -0.2, -0.7, 0.6, 1.4, -1.4];
for (mode, pad) in [(0i64, 0i64), (1, 0), (0, 1), (0, 2)] {
let ir = gridsample_ir(mode, pad, false, 3);
let cpu = run_ir_on_n(&ir, &[("x", x.clone()), ("g", g.clone())], "cpu");
let gpu = run_ir_on_n(&ir, &[("x", x.clone()), ("g", g.clone())], "mlx");
for (a, b) in gpu.iter().zip(&cpu) {
assert!(
(a - b).abs() < 1e-4,
"mlx mode={mode} pad={pad} {gpu:?} vs {cpu:?}"
);
}
}
}
#[cfg(feature = "metal")]
#[test]
fn bilinear_metal_matches_cpu() {
let x = vec![1.0f32, 2.0, 3.0, 4.0];
let cpu = run_ir_on(BILIN_IR, ("x", x.clone()), "cpu");
let gpu = run_ir_on(BILIN_IR, ("x", x), "metal");
for (a, b) in gpu.iter().zip(&cpu) {
assert!((a - b).abs() < 1e-4, "metal {gpu:?} vs cpu {cpu:?}");
}
}
#[cfg(feature = "metal")]
#[test]
fn bicubic_metal_matches_cpu() {
let x = vec![0.0f32, 1.0, 2.0, 3.0];
let cpu = run_ir_on(BICUBIC_IR, ("x", x.clone()), "cpu");
let gpu = run_ir_on(BICUBIC_IR, ("x", x), "metal");
for (a, b) in gpu.iter().zip(&cpu) {
assert!((a - b).abs() < 1e-4, "metal {gpu:?} vs cpu {cpu:?}");
}
}
#[cfg(feature = "metal")]
#[test]
fn bilinear_aa_metal_matches_cpu() {
let x = vec![1.0f32, 2.0, 3.0, 4.0];
let cpu = run_ir_on(BILIN_AA_IR, ("x", x.clone()), "cpu");
let gpu = run_ir_on(BILIN_AA_IR, ("x", x), "metal");
for (a, b) in gpu.iter().zip(&cpu) {
assert!((a - b).abs() < 1e-4, "metal {gpu:?} vs cpu {cpu:?}");
}
}
#[cfg(feature = "mlx")]
#[test]
fn bilinear_mlx_matches_cpu() {
let x = vec![1.0f32, 2.0, 3.0, 4.0];
let cpu = run_ir_on(BILIN_IR, ("x", x.clone()), "cpu");
let gpu = run_ir_on(BILIN_IR, ("x", x), "mlx");
for (a, b) in gpu.iter().zip(&cpu) {
assert!((a - b).abs() < 1e-4, "mlx {gpu:?} vs cpu {cpu:?}");
}
}
const SDPA_PUB_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "sd",
"inputs": [{"id": "q", "shape": [1,1,2,2], "dtype": "f32"},
{"id": "k", "shape": [1,1,2,2], "dtype": "f32"},
{"id": "v", "shape": [1,1,2,2], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "sd", "op": "aten.scaled_dot_product_attention.default",
"args": [{"ref": "q"}, {"ref": "k"}, {"ref": "v"}],
"out": {"shape": [1,1,2,2], "dtype": "f32"}}],
"outputs": [{"ref": "sd", "shape": [1,1,2,2], "dtype": "f32"}]
}"#;
const SDPA_FLASH_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "fa",
"inputs": [{"id": "q", "shape": [1,1,2,2], "dtype": "f32"},
{"id": "k", "shape": [1,1,2,2], "dtype": "f32"},
{"id": "v", "shape": [1,1,2,2], "dtype": "f32"}], "weights": [],
"nodes": [
{"id": "fa", "op": "aten._scaled_dot_product_flash_attention.default",
"args": [{"ref": "q"}, {"ref": "k"}, {"ref": "v"}],
"out": [{"shape": [1,1,2,2], "dtype": "f32"}, {"shape": [1,1,2], "dtype": "f32"}]},
{"id": "g0", "op": "_getitem", "args": [{"ref": "fa"}, {"int": 0}],
"out": {"shape": [1,1,2,2], "dtype": "f32"}}],
"outputs": [{"ref": "g0", "shape": [1,1,2,2], "dtype": "f32"}]
}"#;
#[test]
fn sdpa_flash_variant_matches_public() {
let qkv = [
("q", vec![1.0, 0.0, 0.0, 1.0]),
("k", vec![1.0, 0.0, 0.0, 1.0]),
("v", vec![1.0, 2.0, 3.0, 4.0]),
];
let pubv = run_ir_n(SDPA_PUB_IR, &qkv);
let flash = run_ir_n(SDPA_FLASH_IR, &qkv);
assert_eq!(pubv.len(), 4);
for (a, b) in flash.iter().zip(&pubv) {
assert!((a - b).abs() < 1e-6, "flash {flash:?} vs public {pubv:?}");
}
}
const LRELU_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "lr",
"inputs": [{"id": "x", "shape": [4], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "lr", "op": "aten.leaky_relu.default",
"args": [{"ref": "x"}, {"float": 0.1}], "out": {"shape": [4], "dtype": "f32"}}],
"outputs": [{"ref": "lr", "shape": [4], "dtype": "f32"}]
}"#;
#[test]
fn leaky_relu_negative_slope() {
let out = run_ir(LRELU_IR, &[], ("x", vec![-2.0, -1.0, 0.0, 3.0]));
let want = [-0.2f32, -0.1, 0.0, 3.0];
for (g, w) in out.iter().zip(want) {
assert!((g - w).abs() < 1e-6, "leaky_relu {out:?} vs {want:?}");
}
}
const HSWISH_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "hw",
"inputs": [{"id": "x", "shape": [4], "dtype": "f32"}], "weights": [],
"nodes": [{"id": "hw", "op": "aten.hardswish.default", "args": [{"ref": "x"}],
"out": {"shape": [4], "dtype": "f32"}}],
"outputs": [{"ref": "hw", "shape": [4], "dtype": "f32"}]
}"#;
#[test]
fn hardswish_matches_reference() {
let out = run_ir(HSWISH_IR, &[], ("x", vec![-4.0, 0.0, 3.0, 1.0]));
let want = [0.0f32, 0.0, 3.0, 2.0 / 3.0];
for (g, w) in out.iter().zip(want) {
assert!((g - w).abs() < 1e-6, "hardswish {out:?} vs {want:?}");
}
}
const MFILL_IR: &str = r#"{
"format": "rlx-torch-ir", "version": 1, "model_name": "mf",
"inputs": [{"id": "x", "shape": [4], "dtype": "f32"},
{"id": "t", "shape": [4], "dtype": "f32"}], "weights": [],
"nodes": [
{"id": "m", "op": "aten.gt.Tensor", "args": [{"ref": "x"}, {"ref": "t"}],
"out": {"shape": [4], "dtype": "bool"}},
{"id": "mf", "op": "aten.masked_fill.Scalar",
"args": [{"ref": "x"}, {"ref": "m"}, {"float": 9.0}],
"out": {"shape": [4], "dtype": "f32"}}],
"outputs": [{"ref": "mf", "shape": [4], "dtype": "f32"}]
}"#;
#[test]
fn masked_fill_scalar() {
let out = run_ir_n(
MFILL_IR,
&[
("x", vec![1.0, 2.0, 3.0, 4.0]),
("t", vec![2.0, 2.0, 2.0, 2.0]),
],
);
assert_eq!(out, vec![1.0, 2.0, 9.0, 9.0]);
}
}