tract-metal 0.23.6

Tiny, no-nonsense, self contained, TensorFlow and ONNX inference
use crate::encoder::EncoderExt;
use crate::{LibraryName, MetalStream};
use metal::MTLSize;
use tract_core::internal::*;
use tract_gpu::tensor::DeviceTensor;

pub fn dispatch_eval(
    stream: &MetalStream,
    input: &DeviceTensor,
    weight: &DeviceTensor,
    state: &DeviceTensor,
    output: &DeviceTensor,
    final_state: &DeviceTensor,
) -> TractResult<()> {
    ensure!(input.datum_type() == DatumType::F16);
    ensure!(weight.datum_type() == DatumType::F16 && state.datum_type() == DatumType::F16);
    let channels = input.len();
    let kernel_width = *weight.shape().last().context("conv weight must have a kernel axis")?;
    ensure!(kernel_width == 4, "Qwen3.5 requires a four-tap convolution");
    ensure!(weight.len() == channels * kernel_width);
    ensure!(state.len() == channels * kernel_width);
    ensure!(output.shape() == input.shape() && output.datum_type() == DatumType::F16);
    ensure!(final_state.shape() == state.shape() && final_state.datum_type() == DatumType::F16);
    for tensor in [input, weight, state, output, final_state] {
        stream.retain_tensor(tensor);
    }
    let pipeline = stream.load_pipeline(LibraryName::GdnRecurrent, "causal_conv1d_update_f16")?;
    let command_buffer = stream.command_buffer();
    command_buffer.encode(|encoder| {
        encoder.set_compute_pipeline_state(&pipeline);
        encoder.set_metal_tensor(0, input, metal::MTLResourceUsage::Read);
        encoder.set_metal_tensor(1, weight, metal::MTLResourceUsage::Read);
        encoder.set_metal_tensor(2, state, metal::MTLResourceUsage::Read);
        encoder.set_metal_tensor(3, output, metal::MTLResourceUsage::Write);
        encoder.set_metal_tensor(4, final_state, metal::MTLResourceUsage::Write);
        let channels = channels as i32;
        let kernel_width = kernel_width as i32;
        encoder.set_bytes(5, size_of::<i32>() as u64, &channels as *const i32 as *const _);
        encoder.set_bytes(6, size_of::<i32>() as u64, &kernel_width as *const i32 as *const _);
        encoder.dispatch_threads(
            MTLSize { width: channels as u64, height: 1, depth: 1 },
            MTLSize { width: 256.min(channels) as u64, height: 1, depth: 1 },
        );
    });
    Ok(())
}

pub fn metal_causal_conv1d_update_launch(
    input: &DeviceTensor,
    weight: &DeviceTensor,
    state: &DeviceTensor,
    output: &DeviceTensor,
    final_state: &DeviceTensor,
) -> TractResult<()> {
    crate::with_metal_stream(|stream| {
        dispatch_eval(stream, input, weight, state, output, final_state)
    })
}

crate::register_metal_op!(
    tract_transformers::ops::causal_conv1d_update::CausalConv1dUpdate,
    |_source, _node, _op| {
        Ok(Some(Box::new(tract_gpu::ops::causal_conv1d_update::GpuCausalConv1dUpdate {
            backend_name: "Metal",
            dispatch: metal_causal_conv1d_update_launch,
        })))
    }
);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::utils::with_borrowed_metal_stream;
    use tract_gpu::tensor::{DeviceTensorExt, IntoDevice};

    #[test]
    fn qwen35_conv_update_matches_cpu() -> TractResult<()> {
        with_borrowed_metal_stream(|stream| {
            let channels = 257usize;
            let input_f = (0..channels).map(|i| ((i % 17) as f32 - 8.0) / 32.0).collect::<Vec<_>>();
            let weight_f =
                (0..channels * 4).map(|i| ((i % 13) as f32 - 6.0) / 64.0).collect::<Vec<_>>();
            let state_f =
                (0..channels * 4).map(|i| ((i % 11) as f32 - 5.0) / 32.0).collect::<Vec<_>>();
            let cvt = |v: &[f32]| v.iter().copied().map(f16::from_f32).collect::<Vec<_>>();
            let input = Tensor::from_shape(&[1, channels], &cvt(&input_f))?.into_device()?;
            let weight = Tensor::from_shape(&[channels, 4], &cvt(&weight_f))?.into_device()?;
            let state = Tensor::from_shape(&[1, channels, 4], &cvt(&state_f))?.into_device()?;
            let output = DeviceTensor::uninitialized_dt(DatumType::F16, input.shape())?;
            let next = DeviceTensor::uninitialized_dt(DatumType::F16, state.shape())?;
            dispatch_eval(stream, &input, &weight, &state, &output, &next)?;
            stream.wait_until_completed()?;
            let output = output.to_host()?.into_tensor();
            let next = next.to_host()?.into_tensor();
            let got = unsafe { output.as_slice_unchecked::<f16>() };
            let got_state = unsafe { next.as_slice_unchecked::<f16>() };
            for c in 0..channels {
                let base = c * 4;
                assert_eq!(&got_state[base..base + 3], &cvt(&state_f[base + 1..base + 4]));
                assert_eq!(got_state[base + 3], f16::from_f32(input_f[c]));
                let sum = (0..3).map(|t| state_f[base + t + 1] * weight_f[base + t]).sum::<f32>()
                    + input_f[c] * weight_f[base + 3];
                assert_eq!(got[c], f16::from_f32(sum / (1.0 + (-sum).exp())));
            }
            Ok(())
        })
    }
}