use std::fmt;
use crate::ir::ElemType;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
V128Add {
lane_ty: ElemType,
lanes: u32,
},
V128Mul {
lane_ty: ElemType,
lanes: u32,
},
V128Fma {
lane_ty: ElemType,
lanes: u32,
},
ScalarAdd,
ScalarMul,
Load,
Store,
Branch,
Call,
Other,
}
impl Op {
pub fn is_v128(self) -> bool {
matches!(
self,
Op::V128Add { .. } | Op::V128Mul { .. } | Op::V128Fma { .. }
)
}
}
impl fmt::Display for Op {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Op::V128Add { lane_ty, lanes } => return write!(f, "v128.add.{lane_ty}x{lanes}"),
Op::V128Mul { lane_ty, lanes } => return write!(f, "v128.mul.{lane_ty}x{lanes}"),
Op::V128Fma { lane_ty, lanes } => return write!(f, "v128.fma.{lane_ty}x{lanes}"),
_ => {}
}
let s = match self {
Op::ScalarAdd => "scalar.add",
Op::ScalarMul => "scalar.mul",
Op::Load => "load",
Op::Store => "store",
Op::Branch => "branch",
Op::Call => "call",
Op::Other => "other",
Op::V128Add { .. } | Op::V128Mul { .. } | Op::V128Fma { .. } => unreachable!(),
};
f.write_str(s)
}
}
#[derive(Debug, Clone)]
pub struct BlockIR {
pub ops: Vec<Op>,
pub loop_trip_count: Option<u64>,
pub name: String,
}
impl BlockIR {
pub fn new(name: impl Into<String>, ops: Vec<Op>, loop_trip_count: Option<u64>) -> Self {
Self {
name: name.into(),
ops,
loop_trip_count,
}
}
pub fn v128_ratio(&self) -> f32 {
v128_ratio_of(&self.ops)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectorVerdict {
Offload,
KeepOnCpu,
}
#[derive(Debug, Clone, Copy)]
pub struct DetectorConfig {
pub v128_ratio_threshold: f32,
pub min_trip_count: u64,
}
impl Default for DetectorConfig {
fn default() -> Self {
Self {
v128_ratio_threshold: 0.8,
min_trip_count: 64,
}
}
}
pub fn classify(block: &BlockIR, cfg: &DetectorConfig) -> DetectorVerdict {
classify_ops(&block.ops, block.loop_trip_count, cfg)
}
pub fn classify_ops(
ops: &[Op],
loop_trip_count: Option<u64>,
cfg: &DetectorConfig,
) -> DetectorVerdict {
let ratio = v128_ratio_of(ops);
let trip_ok = loop_trip_count
.map(|n| n >= cfg.min_trip_count)
.unwrap_or(false);
if ratio >= cfg.v128_ratio_threshold && trip_ok {
DetectorVerdict::Offload
} else {
DetectorVerdict::KeepOnCpu
}
}
fn v128_ratio_of(ops: &[Op]) -> f32 {
if ops.is_empty() {
return 0.0;
}
let v128 = ops.iter().filter(|o| o.is_v128()).count();
v128 as f32 / ops.len() as f32
}
pub fn classify_default(block: &BlockIR) -> DetectorVerdict {
classify(block, &DetectorConfig::default())
}
#[cfg(feature = "cuda-oxide-backend")]
pub fn pliron_pipeline_enabled() -> bool {
std::env::var("TENSOR_WASM_PLIRON_PIPELINE")
.map(|v| !v.is_empty())
.unwrap_or(false)
}
#[cfg(feature = "cuda-oxide-backend")]
#[derive(Debug, Clone)]
pub struct PlironCandidate {
pub lowered: crate::lowered_ir::LoweredFunction,
}
#[cfg(feature = "cuda-oxide-backend")]
pub fn try_pliron_candidate(func: &cranelift_codegen::ir::Function) -> Option<PlironCandidate> {
if !pliron_pipeline_enabled() {
return None;
}
if crate::reject_list::check_function(func).is_some() {
return None;
}
crate::lowering_driver::lower_function(func)
.map(|lowered| PlironCandidate { lowered })
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn block(name: &str, ops: Vec<Op>, loop_n: Option<u64>) -> BlockIR {
BlockIR::new(name, ops, loop_n)
}
fn add() -> Op {
Op::V128Add {
lane_ty: ElemType::F32,
lanes: 4,
}
}
fn mul() -> Op {
Op::V128Mul {
lane_ty: ElemType::F32,
lanes: 4,
}
}
fn fma() -> Op {
Op::V128Fma {
lane_ty: ElemType::F32,
lanes: 4,
}
}
#[test]
fn mixed_v128_ratio_below_threshold_is_kept_on_cpu() {
let b = block(
"vector_add_loop",
vec![Op::Load, Op::Load, add(), add(), add(), add(), Op::Store],
Some(128),
);
assert_eq!(classify_default(&b), DetectorVerdict::KeepOnCpu);
}
#[test]
fn high_v128_ratio_offloaded() {
let b = block(
"matmul_inner",
vec![
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
mul(),
Op::Store,
],
Some(512),
);
assert_eq!(classify_default(&b), DetectorVerdict::Offload);
}
#[test]
fn pure_v128_matmul_tile_is_offloaded() {
let b = block(
"matmul_tile",
vec![
Op::Load,
Op::Load,
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
fma(),
Op::Store,
],
Some(256),
);
assert_eq!(classify_default(&b), DetectorVerdict::Offload);
}
#[test]
fn pure_v128_vector_mul_loop_is_offloaded() {
let b = block(
"vector_mul",
vec![
Op::Load,
mul(),
mul(),
mul(),
mul(),
add(),
add(),
add(),
add(),
Op::Store,
],
Some(1024),
);
assert_eq!(classify_default(&b), DetectorVerdict::Offload);
}
#[test]
fn dynamic_loop_not_offloaded_even_if_v128_heavy() {
let b = block(
"dynamic_loop",
vec![add(); 16],
None, );
assert_eq!(classify_default(&b), DetectorVerdict::KeepOnCpu);
}
#[test]
fn tiny_loop_not_offloaded() {
let b = block(
"tiny",
vec![add(); 16],
Some(8), );
assert_eq!(classify_default(&b), DetectorVerdict::KeepOnCpu);
}
#[test]
fn scalar_heavy_not_offloaded() {
let b = block(
"scalar",
vec![
Op::ScalarAdd,
Op::ScalarAdd,
Op::ScalarMul,
Op::Branch,
Op::Call,
Op::Load,
],
Some(1024),
);
assert_eq!(classify_default(&b), DetectorVerdict::KeepOnCpu);
}
#[test]
fn op_is_v128_taxonomy() {
assert!(add().is_v128());
assert!(mul().is_v128());
assert!(fma().is_v128());
assert!(Op::V128Add {
lane_ty: ElemType::I32,
lanes: 4
}
.is_v128());
assert!(!Op::ScalarAdd.is_v128());
assert!(!Op::Load.is_v128());
}
#[test]
fn op_display_carries_element_type() {
assert_eq!(add().to_string(), "v128.add.f32x4");
assert_eq!(
Op::V128Mul {
lane_ty: ElemType::I32,
lanes: 4
}
.to_string(),
"v128.mul.i32x4"
);
assert_eq!(Op::Load.to_string(), "load");
}
#[test]
fn config_threshold_tunable() {
let b = block("borderline", vec![add(), add(), Op::Load], Some(128));
assert_eq!(classify_default(&b), DetectorVerdict::KeepOnCpu);
let cfg = DetectorConfig {
v128_ratio_threshold: 0.6,
..DetectorConfig::default()
};
assert_eq!(classify(&b, &cfg), DetectorVerdict::Offload);
}
}
#[cfg(all(test, feature = "cuda-oxide-backend"))]
mod pliron_pipeline_tests {
use super::*;
use crate::lowering_test_support::function_with_binary_op;
use cranelift_codegen::ir::immediates::Offset32;
use cranelift_codegen::ir::instructions::InstructionData;
use cranelift_codegen::ir::{
types, AbiParam, Function, MemFlags, Opcode, Signature, UserFuncName, Value,
};
use cranelift_codegen::isa::CallConv;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn pliron_pipeline_disabled_by_default() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::remove_var("TENSOR_WASM_PLIRON_PIPELINE");
assert!(!pliron_pipeline_enabled());
}
#[test]
fn pliron_pipeline_enabled_when_env_var_set() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("TENSOR_WASM_PLIRON_PIPELINE", "1");
let enabled = pliron_pipeline_enabled();
std::env::remove_var("TENSOR_WASM_PLIRON_PIPELINE");
assert!(enabled);
}
#[test]
fn try_pliron_candidate_some_on_iadd_return() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("TENSOR_WASM_PLIRON_PIPELINE", "1");
let (func, _inst) = function_with_binary_op(Opcode::Iadd, types::I32);
let candidate = try_pliron_candidate(&func);
std::env::remove_var("TENSOR_WASM_PLIRON_PIPELINE");
let candidate = candidate.expect("iadd+return must lower");
assert!(!candidate.lowered.blocks.is_empty());
assert!(candidate.lowered.blocks[0].ops.len() >= 2);
}
#[test]
fn try_pliron_candidate_none_on_reject_list_hit() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("TENSOR_WASM_PLIRON_PIPELINE", "1");
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(types::I32));
let mut func =
Function::with_name_signature(UserFuncName::testcase("atomic_fn".as_bytes()), sig);
let block = func.dfg.make_block();
let _param = func.dfg.append_block_param(block, types::I32);
func.layout.append_block(block);
let inst = func.dfg.make_inst(InstructionData::LoadNoOffset {
opcode: Opcode::AtomicLoad,
flags: MemFlags::new(),
arg: Value::from_u32(0),
});
func.layout.append_inst(inst, block);
let candidate = try_pliron_candidate(&func);
std::env::remove_var("TENSOR_WASM_PLIRON_PIPELINE");
assert!(
candidate.is_none(),
"atomic_load must be reject-listed; got Some",
);
}
#[test]
fn try_pliron_candidate_none_when_disabled() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::remove_var("TENSOR_WASM_PLIRON_PIPELINE");
let (func, _inst) = function_with_binary_op(Opcode::Iadd, types::I32);
assert!(try_pliron_candidate(&func).is_none());
}
#[test]
fn offset_field_is_irrelevant_to_pliron_dispatch() {
let _ = Offset32::new(0);
}
}