tensor_wasm_exec/
auto_offload.rs1use tensor_wasm_jit::detector::{classify, BlockIR, DetectorConfig, DetectorVerdict, Op};
17use tensor_wasm_jit::ir::ElemType;
18use tracing::{debug, info, instrument};
19use wasmparser::{Operator, Parser, Payload};
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct OffloadVerdict {
24 pub function_index: u32,
26 pub verdict: DetectorVerdict,
28 pub op_count: usize,
30 pub v128_ratio: f32,
32}
33
34#[derive(Debug, thiserror::Error)]
36pub enum AnalyseError {
37 #[error("wasmparser: {0}")]
39 Parse(String),
40}
41
42fn op_to_detector_op(op: &Operator<'_>) -> Op {
50 use wasmparser::Operator::*;
51 match op {
52 V128Load { .. } => Op::Load,
53 V128Store { .. } => Op::Store,
54 F32Add | I32Add | I64Add | F64Add => Op::ScalarAdd,
55 F32Mul | I32Mul | I64Mul | F64Mul => Op::ScalarMul,
56 I32Load { .. } | I64Load { .. } | F32Load { .. } | F64Load { .. } => Op::Load,
57 I32Store { .. } | I64Store { .. } | F32Store { .. } | F64Store { .. } => Op::Store,
58 Br { .. }
59 | BrIf { .. }
60 | BrTable { .. }
61 | If { .. }
62 | Else
63 | Loop { .. }
64 | Block { .. } => Op::Branch,
65 Call { .. } | CallIndirect { .. } | ReturnCall { .. } => Op::Call,
66 F32x4Add => Op::V128Add {
67 lane_ty: ElemType::F32,
68 lanes: 4,
69 },
70 I32x4Add => Op::V128Add {
71 lane_ty: ElemType::I32,
72 lanes: 4,
73 },
74 F32x4Mul => Op::V128Mul {
75 lane_ty: ElemType::F32,
76 lanes: 4,
77 },
78 I32x4Mul => Op::V128Mul {
79 lane_ty: ElemType::I32,
80 lanes: 4,
81 },
82 F64x2Add | I64x2Add | I16x8Add | I8x16Add | F64x2Mul | I64x2Mul | I16x8Mul => Op::Other,
84 _ => Op::Other,
85 }
86}
87
88#[instrument(skip(wasm), fields(wasm_bytes = wasm.len()))]
95pub fn analyse(wasm: &[u8]) -> Result<Vec<OffloadVerdict>, AnalyseError> {
96 analyse_with_config(wasm, &DetectorConfig::default())
97}
98
99#[instrument(skip(wasm, cfg), fields(wasm_bytes = wasm.len()))]
108pub fn analyse_with_config(
109 wasm: &[u8],
110 cfg: &DetectorConfig,
111) -> Result<Vec<OffloadVerdict>, AnalyseError> {
112 let mut verdicts = Vec::new();
113 let mut func_idx: u32 = 0;
114 for payload in Parser::new(0).parse_all(wasm) {
115 let payload = payload.map_err(|e| AnalyseError::Parse(format!("{e}")))?;
116 if let Payload::CodeSectionEntry(body) = payload {
117 let mut ops = body
118 .get_operators_reader()
119 .map_err(|e| AnalyseError::Parse(format!("{e}")))?;
120 let mut detector_ops = Vec::new();
121 let mut saw_loop = false;
122 while !ops.eof() {
123 match ops.read() {
124 Ok(op) => {
125 if matches!(op, wasmparser::Operator::Loop { .. }) {
126 saw_loop = true;
127 }
128 detector_ops.push(op_to_detector_op(&op));
129 }
130 Err(_) => break,
131 }
132 }
133 let trip_count = if saw_loop { Some(128) } else { None };
138 let block = BlockIR::new(format!("func{func_idx}"), detector_ops.clone(), trip_count);
139 let v = classify(&block, cfg);
140 let op_count = detector_ops.len();
141 let v128 = detector_ops.iter().filter(|o| o.is_v128()).count();
142 let v128_ratio = if op_count == 0 {
143 0.0
144 } else {
145 v128 as f32 / op_count as f32
146 };
147 match v {
148 DetectorVerdict::Offload => info!(
149 target: "tensor_wasm_exec::auto_offload",
150 function = func_idx,
151 op_count,
152 v128_ratio,
153 "verdict: Offload"
154 ),
155 DetectorVerdict::KeepOnCpu => debug!(
156 target: "tensor_wasm_exec::auto_offload",
157 function = func_idx,
158 op_count,
159 v128_ratio,
160 "verdict: KeepOnCpu"
161 ),
162 }
163 verdicts.push(OffloadVerdict {
164 function_index: func_idx,
165 verdict: v,
166 op_count,
167 v128_ratio,
168 });
169 func_idx += 1;
170 }
171 }
172 Ok(verdicts)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 const TRIVIAL_WAT: &str = r#"(module (func (export "noop")))"#;
180
181 #[test]
182 fn analyse_noop_module() {
183 let wasm = wat::parse_str(TRIVIAL_WAT).unwrap();
184 let v = analyse(&wasm).expect("analyse");
185 assert_eq!(v.len(), 1);
186 assert_eq!(v[0].function_index, 0);
187 assert_eq!(v[0].verdict, DetectorVerdict::KeepOnCpu);
189 }
190
191 #[test]
192 fn analyse_v128_heavy_module() {
193 let wat = r#"
195 (module
196 (memory 1)
197 (func (export "hot")
198 (drop (v128.const i32x4 1 2 3 4))
199 (drop (v128.const i32x4 5 6 7 8))
200 (drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
201 (drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
202 (drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
203 (drop (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8)))
204 )
205 )
206 "#;
207 let wasm = wat::parse_str(wat).unwrap();
208 let v = analyse(&wasm).expect("analyse");
209 assert_eq!(v.len(), 1);
210 assert!(matches!(
213 v[0].verdict,
214 DetectorVerdict::Offload | DetectorVerdict::KeepOnCpu
215 ));
216 assert!(v[0].op_count > 0);
217 }
218
219 #[test]
220 fn analyse_invalid_wasm_returns_error() {
221 let bytes = [0x00, 0x61, 0x73, 0x6d, 0xff, 0xff, 0xff, 0xff];
222 let err = analyse(&bytes).unwrap_err();
223 assert!(matches!(err, AnalyseError::Parse(_)));
224 }
225
226 #[test]
227 fn straight_line_function_keeps_on_cpu_even_with_v128() {
228 let wat = r#"
231 (module
232 (memory 1)
233 (func (export "straight") (result v128)
234 (i32x4.add (v128.const i32x4 1 2 3 4) (v128.const i32x4 5 6 7 8))
235 )
236 )
237 "#;
238 let wasm = wat::parse_str(wat).unwrap();
239 let v = analyse(&wasm).expect("analyse");
240 assert_eq!(v.len(), 1);
241 assert_eq!(
242 v[0].verdict,
243 DetectorVerdict::KeepOnCpu,
244 "straight-line v128 must not trigger offload"
245 );
246 }
247}