use tensor_wasm_jit::detector::{classify, BlockIR, DetectorConfig, DetectorVerdict, Op};
use tensor_wasm_jit::ir::ElemType;
use tracing::{debug, info, instrument};
use wasmparser::{Operator, Parser, Payload};
#[derive(Debug, Clone, PartialEq)]
pub struct OffloadVerdict {
pub function_index: u32,
pub verdict: DetectorVerdict,
pub op_count: usize,
pub v128_ratio: f32,
}
#[derive(Debug, thiserror::Error)]
pub enum AnalyseError {
#[error("wasmparser: {0}")]
Parse(String),
}
fn op_to_detector_op(op: &Operator<'_>) -> Op {
use wasmparser::Operator::*;
match op {
V128Load { .. } => Op::Load,
V128Store { .. } => Op::Store,
F32Add | I32Add | I64Add | F64Add => Op::ScalarAdd,
F32Mul | I32Mul | I64Mul | F64Mul => Op::ScalarMul,
I32Load { .. } | I64Load { .. } | F32Load { .. } | F64Load { .. } => Op::Load,
I32Store { .. } | I64Store { .. } | F32Store { .. } | F64Store { .. } => Op::Store,
Br { .. }
| BrIf { .. }
| BrTable { .. }
| If { .. }
| Else
| Loop { .. }
| Block { .. } => Op::Branch,
Call { .. } | CallIndirect { .. } | ReturnCall { .. } => Op::Call,
F32x4Add => Op::V128Add {
lane_ty: ElemType::F32,
lanes: 4,
},
I32x4Add => Op::V128Add {
lane_ty: ElemType::I32,
lanes: 4,
},
F32x4Mul => Op::V128Mul {
lane_ty: ElemType::F32,
lanes: 4,
},
I32x4Mul => Op::V128Mul {
lane_ty: ElemType::I32,
lanes: 4,
},
F64x2Add | I64x2Add | I16x8Add | I8x16Add | F64x2Mul | I64x2Mul | I16x8Mul => Op::Other,
_ => Op::Other,
}
}
#[instrument(skip(wasm), fields(wasm_bytes = wasm.len()))]
pub fn analyse(wasm: &[u8]) -> Result<Vec<OffloadVerdict>, AnalyseError> {
analyse_with_config(wasm, &DetectorConfig::default())
}
#[instrument(skip(wasm, cfg), fields(wasm_bytes = wasm.len()))]
pub fn analyse_with_config(
wasm: &[u8],
cfg: &DetectorConfig,
) -> Result<Vec<OffloadVerdict>, AnalyseError> {
let mut verdicts = Vec::new();
let mut func_idx: u32 = 0;
for payload in Parser::new(0).parse_all(wasm) {
let payload = payload.map_err(|e| AnalyseError::Parse(format!("{e}")))?;
if let Payload::CodeSectionEntry(body) = payload {
let mut ops = body
.get_operators_reader()
.map_err(|e| AnalyseError::Parse(format!("{e}")))?;
let mut detector_ops = Vec::new();
let mut saw_loop = false;
while !ops.eof() {
match ops.read() {
Ok(op) => {
if matches!(op, wasmparser::Operator::Loop { .. }) {
saw_loop = true;
}
detector_ops.push(op_to_detector_op(&op));
}
Err(_) => break,
}
}
let trip_count = if saw_loop { Some(128) } else { None };
let block = BlockIR::new(format!("func{func_idx}"), detector_ops.clone(), trip_count);
let v = classify(&block, cfg);
let op_count = detector_ops.len();
let v128 = detector_ops.iter().filter(|o| o.is_v128()).count();
let v128_ratio = if op_count == 0 {
0.0
} else {
v128 as f32 / op_count as f32
};
match v {
DetectorVerdict::Offload => info!(
target: "tensor_wasm_exec::auto_offload",
function = func_idx,
op_count,
v128_ratio,
"verdict: Offload"
),
DetectorVerdict::KeepOnCpu => debug!(
target: "tensor_wasm_exec::auto_offload",
function = func_idx,
op_count,
v128_ratio,
"verdict: KeepOnCpu"
),
}
verdicts.push(OffloadVerdict {
function_index: func_idx,
verdict: v,
op_count,
v128_ratio,
});
func_idx += 1;
}
}
Ok(verdicts)
}
#[cfg(test)]
mod tests {
use super::*;
const TRIVIAL_WAT: &str = r#"(module (func (export "noop")))"#;
#[test]
fn analyse_noop_module() {
let wasm = wat::parse_str(TRIVIAL_WAT).unwrap();
let v = analyse(&wasm).expect("analyse");
assert_eq!(v.len(), 1);
assert_eq!(v[0].function_index, 0);
assert_eq!(v[0].verdict, DetectorVerdict::KeepOnCpu);
}
#[test]
fn analyse_v128_heavy_module() {
let wat = r#"
(module
(memory 1)
(func (export "hot")
(drop (v128.const i32x4 1 2 3 4))
(drop (v128.const i32x4 5 6 7 8))
(drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
(drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
(drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
(drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
)
)
"#;
let wasm = wat::parse_str(wat).unwrap();
let v = analyse(&wasm).expect("analyse");
assert_eq!(v.len(), 1);
assert!(matches!(
v[0].verdict,
DetectorVerdict::Offload | DetectorVerdict::KeepOnCpu
));
assert!(v[0].op_count > 0);
}
#[test]
fn analyse_invalid_wasm_returns_error() {
let bytes = [0x00, 0x61, 0x73, 0x6d, 0xff, 0xff, 0xff, 0xff];
let err = analyse(&bytes).unwrap_err();
assert!(matches!(err, AnalyseError::Parse(_)));
}
#[test]
fn straight_line_function_keeps_on_cpu_even_with_v128() {
let wat = r#"
(module
(memory 1)
(func (export "straight") (result v128)
(i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8))
)
)
"#;
let wasm = wat::parse_str(wat).unwrap();
let v = analyse(&wasm).expect("analyse");
assert_eq!(v.len(), 1);
assert_eq!(
v[0].verdict,
DetectorVerdict::KeepOnCpu,
"straight-line v128 must not trigger offload"
);
}
}