use tract_nnef::internal::*;
use super::cast_f32::{from_f32, to_f32_vec};
pub fn register(registry: &mut Registry) {
fn deserialize(
builder: &mut ModelBuilder,
invocation: &ResolvedInvocation,
) -> TractResult<Value> {
let inputs = ["input", "weight", "initial_state"]
.map(|name| invocation.named_arg_as(builder, name))
.into_iter()
.collect::<TractResult<TVec<_>>>()?;
builder.wire(CausalConv1dUpdate, &inputs)
}
fn serialize(
ast: &mut IntoAst,
node: &TypedNode,
_op: &CausalConv1dUpdate,
) -> TractResult<Option<Arc<RValue>>> {
let inputs: Vec<Arc<RValue>> = node.inputs.iter().map(|i| ast.mapping[i].clone()).collect();
Ok(Some(invocation("tract_transformers_causal_conv1d_update", &inputs, &[])))
}
registry.register_dumper(serialize);
for name in ["tract_transformers_causal_conv1d_update", "tract_qwen35_causal_conv1d_update"] {
registry.register_primitive(
name,
&[
TypeName::Scalar.tensor().named("input"),
TypeName::Scalar.tensor().named("weight"),
TypeName::Scalar.tensor().named("initial_state"),
],
&[("output", TypeName::Scalar.tensor()), ("final_state", TypeName::Scalar.tensor())],
deserialize,
);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CausalConv1dUpdate;
impl Op for CausalConv1dUpdate {
fn name(&self) -> StaticName {
"CausalConv1dUpdate".into()
}
op_as_typed_op!();
}
impl EvalOp for CausalConv1dUpdate {
op_out_of_plan!();
fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
ensure!(inputs.len() == 3, "causal conv update expects input, weight, state");
let input_shape: TVec<usize> = inputs[0].shape().into();
let weight_shape: TVec<usize> = inputs[1].shape().into();
let state_shape: TVec<usize> = inputs[2].shape().into();
ensure!(input_shape.len() == 3, "input must be [b, C, S], got {input_shape:?}");
let (b, channels, s_len) = (input_shape[0], input_shape[1], input_shape[2]);
let kernel_width = *weight_shape.last().context("conv weight must have a kernel axis")?;
ensure!(
weight_shape.iter().product::<usize>() == channels * kernel_width,
"weight must be [C, k], got {weight_shape:?}"
);
ensure!(
state_shape.len() == 3
&& state_shape[0] == b
&& state_shape[1] == channels
&& state_shape[2] == kernel_width,
"state must be [b, C, k], got {state_shape:?}"
);
let input = to_f32_vec(&inputs[0])?;
let weight = to_f32_vec(&inputs[1])?;
let state = to_f32_vec(&inputs[2])?;
let mut output = vec![0f32; input.len()];
let mut final_state = vec![0f32; state.len()];
let mut full = vec![0f32; kernel_width + s_len];
for bi in 0..b {
for c in 0..channels {
let ib = (bi * channels + c) * s_len;
let sb = (bi * channels + c) * kernel_width;
let wb = c * kernel_width;
full[..kernel_width].copy_from_slice(&state[sb..sb + kernel_width]);
full[kernel_width..].copy_from_slice(&input[ib..ib + s_len]);
for t in 0..s_len {
let mut sum = 0f32;
for tap in 0..kernel_width {
sum += weight[wb + tap] * full[t + 1 + tap];
}
output[ib + t] = sum / (1.0 + (-sum).exp());
}
final_state[sb..sb + kernel_width]
.copy_from_slice(&full[s_len..s_len + kernel_width]);
}
}
Ok(tvec![
from_f32(output, &input_shape, inputs[0].datum_type())?.into_tvalue(),
from_f32(final_state, &state_shape, inputs[2].datum_type())?.into_tvalue(),
])
}
}
impl TypedOp for CausalConv1dUpdate {
fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
ensure!(inputs.len() == 3);
let dts: Vec<DatumType> = inputs.iter().map(|i| i.datum_type).collect();
ensure!(
dts.iter().all(|dt| *dt == DatumType::F16)
|| dts.iter().all(|dt| *dt == DatumType::F32),
"causal conv update inputs must be uniformly f16 or uniformly f32, got {dts:?}"
);
ensure!(inputs[0].rank() == 3, "input must be [b, C, S]");
ensure!(inputs[2].rank() == 3, "state must be [b, C, k]");
Ok(tvec![inputs[0].without_value(), inputs[2].without_value()])
}
as_op!();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ops::test_utils::arb;
fn run(input: &Tensor, weight: &Tensor, state: &Tensor) -> TractResult<(Tensor, Tensor)> {
let outputs = CausalConv1dUpdate.eval(
&EvalContext::out_of_plan(),
tvec![
input.clone().into_tvalue(),
weight.clone().into_tvalue(),
state.clone().into_tvalue(),
],
)?;
Ok((outputs[0].clone().into_tensor(), outputs[1].clone().into_tensor()))
}
#[test]
fn multi_step_matches_sequential_single_steps() -> TractResult<()> {
let (b, channels, s_len, k) = (1, 6, 5, 4);
let input = arb(&[b, channels, s_len], 1);
let weight = arb(&[channels, k], 2);
let state0 = arb(&[b, channels, k], 3);
let (out_multi, final_multi) = run(&input, &weight, &state0)?;
let mut state = state0.clone();
let mut outs: Vec<Tensor> = vec![];
for t in 0..s_len {
let step = input.slice(2, t, t + 1)?;
let (o, st) = run(&step, &weight, &state)?;
outs.push(o);
state = st;
}
let seq_out = Tensor::stack_tensors(
2,
&outs.iter().map(|o| o.clone().into()).collect::<Vec<TValue>>(),
)?;
let seq_out = seq_out.into_shape(&[b, channels, s_len])?;
out_multi.close_enough(&seq_out, Approximation::Close)?;
final_multi.close_enough(&state, Approximation::Close)?;
Ok(())
}
}