use super::{DType, DeviceTensor, TensorDesc};
use crate::{
Error, Result,
inference::{Inference, ModelContext, PreparedModel},
loom::Specialization,
model::{Command, Dispatch, ModelSession},
plan_cache::PlanCache,
};
use std::sync::Arc;
pub struct TensorOps {
context: ModelContext,
finite: PlanCache<TensorDesc, PreparedModel>,
gather: PlanCache<(TensorDesc, usize), PreparedModel>,
}
impl TensorOps {
pub fn new(context: &ModelContext, capacity: usize) -> Result<Self> {
Ok(Self {
context: context.clone(),
finite: PlanCache::new(capacity, PreparedModel::is_idle)?,
gather: PlanCache::new(capacity, PreparedModel::is_idle)?,
})
}
pub fn gather_rows(&self, input: &DeviceTensor, indices: &[usize]) -> Result<DeviceTensor> {
self.context.validate(input)?;
let desc = input.desc();
if desc.shape().len() != 2
|| !desc.is_contiguous()
|| desc.bytes() > i32::MAX as usize
|| indices.iter().any(|&i| i >= desc.shape()[0])
{
return Err(Error::Message(
"gather requires contiguous rows and in-range indices".into(),
));
}
let output = TensorDesc::new(desc.dtype(), vec![indices.len(), desc.shape()[1]])?
.with_layout(desc.layout())?;
if output.is_empty() {
input.completion().wait()?;
return self.context.allocate(output);
}
let bucket = indices
.len()
.checked_next_power_of_two()
.ok_or_else(|| Error::Message("gather size overflow".into()))?;
let padded = TensorDesc::new(desc.dtype(), vec![bucket, desc.shape()[1]])?
.with_layout(desc.layout())?;
if padded.bytes() > i32::MAX as usize {
return Err(Error::Message(
"gather exceeds kernel indexing limits".into(),
));
}
let plan = self.gather.get_or_prepare((desc.clone(), bucket), || {
let mut model = ModelSession::in_context(&self.context)?;
let source = model.allocate(desc.bytes())?;
let ids_desc = TensorDesc::new(DType::U32, vec![bucket])?;
let ids = model.allocate(ids_desc.bytes())?;
let destination = model.allocate(padded.bytes())?;
let mut code = include_str!("gather.loom").to_owned();
for (name, value) in [
("INPUT", desc.bytes()),
("LAST_INPUT", desc.bytes() - 1),
("ROWS", desc.shape()[0]),
("LAST_ROW", desc.shape()[0] - 1),
("ROW_BYTES", desc.shape()[1] * desc.dtype().bytes()),
("BUCKET", bucket),
("LAST_INDEX", bucket - 1),
("OUTPUT", padded.bytes()),
("LAST_OUTPUT", padded.bytes() - 1),
("GRID", padded.bytes().div_ceil(256)),
] {
code = code.replace(&format!("@{name}@"), &value.to_string());
}
let kernel = unsafe { model.compile(&[(&code, Specialization::new("gather"))])? }[0];
unsafe {
model.freeze(&self.context)?.prepare(
&[Command::Dispatch(Dispatch::indices(
kernel,
[0],
[padded.bytes().div_ceil(256) as u32, 1, 1],
vec![source.read(), ids.read(), destination.write()],
))],
&[(source, desc.clone()), (ids, ids_desc)],
&[(destination, padded.clone())],
3,
)
}
})?;
let bytes: Vec<_> = indices
.iter()
.copied()
.chain(std::iter::repeat(indices[0]))
.take(bucket)
.flat_map(|i| (i as u32).to_le_bytes())
.collect();
let ids = self
.context
.upload(TensorDesc::new(DType::U32, vec![bucket])?, &bytes)?;
let result = plan.submit(&[input.clone(), ids])?;
result.outputs()[0].view(0, output)
}
pub fn prepare_finite(&self, input: &TensorDesc) -> Result<Arc<PreparedModel>> {
if input.dtype() != DType::F32
|| !input.is_contiguous()
|| input.is_empty()
|| input.elements() > i32::MAX as usize
{
return Err(Error::Message(
"finite check requires nonempty contiguous F32".into(),
));
}
self.finite.get_or_prepare(input.clone(), || {
let mut model = ModelSession::in_context(&self.context)?;
let src = model.allocate(input.bytes())?;
let mut current = src;
let mut count = input.elements();
let mut commands = Vec::new();
let mut first = true;
loop {
let out = count.div_ceil(256);
let dst = model.allocate(out*4)?;
let ty = if first {"f32"} else {"i32"};
let bad = if first {
"%abs = scalar.absf %value : f32\n%max = scalar.constant 3.402823466e38 : f32\n%finite = scalar.cmpf ole, %abs, %max : f32\n%bad = scf.select %finite, %fzero, %fone : f32"
} else { "%bad = scalar.uitofp %value : i32 to f32" };
let mut source = include_str!("finite.loom").replace("@TYPE@",ty).replace("@CHECK@",bad);
for (name,value) in [("GRID",out.div_ceil(64)),("INPUT",count),("OUTPUT",out),("LAST_INPUT",count-1),("LAST_OUTPUT",out-1)] {
source = source.replace(&format!("@{name}@"),&value.to_string());
}
let kernel = unsafe {model.compile(&[(&source,Specialization::new("finite"))])?}[0];
commands.push(Command::Dispatch(Dispatch::indices(kernel,[0],[out.div_ceil(64) as u32,1,1],vec![current.read(),dst.write()])));
current = dst;
if out == 1 { break; }
count = out;
first = false;
}
unsafe {model.freeze(&self.context)?.prepare(&commands,&[(src,input.clone())],&[(current,TensorDesc::new(DType::I32,vec![1])?)],3)}
})
}
pub fn finite(&self, input: &DeviceTensor) -> Result<Inference> {
self.context.validate(input)?;
self.prepare_finite(input.desc())?
.submit(std::slice::from_ref(input))
}
}