Skip to main content

singe_kernel/cpu/
conv.rs

1//! Small convolution and layout-specific convolution helpers.
2
3use crate::{
4    error::{Error, Result},
5    utility::checked_element_count,
6};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct CausalConv1dConfig {
10    pub channels_in: usize,
11    pub channels_out: usize,
12    pub input_length: usize,
13    pub kernel_size: usize,
14    pub stride: usize,
15    pub dilation: usize,
16    pub groups: usize,
17    pub left_padding: usize,
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub struct Conv1dConfig {
22    pub channels_in: usize,
23    pub channels_out: usize,
24    pub input_length: usize,
25    pub kernel_size: usize,
26    pub stride: usize,
27    pub dilation: usize,
28    pub groups: usize,
29    pub left_padding: usize,
30    pub right_padding: usize,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct BatchedConv1dConfig {
35    pub batch: usize,
36    pub channels_in: usize,
37    pub channels_out: usize,
38    pub input_length: usize,
39    pub kernel_size: usize,
40    pub stride: usize,
41    pub dilation: usize,
42    pub groups: usize,
43    pub left_padding: usize,
44    pub right_padding: usize,
45    pub input_batch_stride: usize,
46    pub output_batch_stride: usize,
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct Conv1dIm2colConfig {
51    pub batch: usize,
52    pub channels_in: usize,
53    pub input_length: usize,
54    pub kernel_size: usize,
55    pub stride: usize,
56    pub dilation: usize,
57    pub groups: usize,
58    pub left_padding: usize,
59    pub right_padding: usize,
60    pub input_batch_stride: usize,
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum Conv1dActivation {
65    None,
66    Gelu,
67}
68
69impl CausalConv1dConfig {
70    pub fn output_length(self) -> Result<usize> {
71        validate_causal_conv1d_config(self)?;
72        conv1d_output_length(
73            self.input_length,
74            self.kernel_size,
75            self.stride,
76            self.dilation,
77            self.left_padding,
78            0,
79        )
80    }
81}
82
83impl Conv1dConfig {
84    pub fn output_length(self) -> Result<usize> {
85        validate_conv1d_config(self)?;
86        conv1d_output_length(
87            self.input_length,
88            self.kernel_size,
89            self.stride,
90            self.dilation,
91            self.left_padding,
92            self.right_padding,
93        )
94    }
95}
96
97impl BatchedConv1dConfig {
98    pub fn output_length(self) -> Result<usize> {
99        validate_batched_conv1d_config(self)?;
100        conv1d_output_length(
101            self.input_length,
102            self.kernel_size,
103            self.stride,
104            self.dilation,
105            self.left_padding,
106            self.right_padding,
107        )
108    }
109}
110
111impl Conv1dIm2colConfig {
112    pub fn output_length(self) -> Result<usize> {
113        validate_conv1d_im2col_config(self)?;
114        conv1d_output_length(
115            self.input_length,
116            self.kernel_size,
117            self.stride,
118            self.dilation,
119            self.left_padding,
120            self.right_padding,
121        )
122    }
123
124    pub fn output_values_per_batch(self) -> Result<usize> {
125        let output_length = self.output_length()?;
126        checked_element_count(
127            checked_element_count(self.channels_in, output_length)?,
128            self.kernel_size,
129        )
130    }
131}
132
133pub fn conv1d_causal(input: &[f32], weight: &[f32], config: CausalConv1dConfig) -> Vec<f32> {
134    let output_length = config.output_length().expect("output length");
135    let channels_in_per_group = config.channels_in / config.groups;
136    let channels_out_per_group = config.channels_out / config.groups;
137    let mut output = vec![0.0; config.channels_out * output_length];
138    for out_channel in 0..config.channels_out {
139        let group = out_channel / channels_out_per_group;
140        let input_channel_start = group * channels_in_per_group;
141        for out_pos in 0..output_length {
142            let mut sum = 0.0f32;
143            for group_input_channel in 0..channels_in_per_group {
144                let input_channel = input_channel_start + group_input_channel;
145                for kernel_index in 0..config.kernel_size {
146                    let raw_input_pos = out_pos * config.stride + kernel_index * config.dilation;
147                    if raw_input_pos < config.left_padding {
148                        continue;
149                    }
150                    let input_pos = raw_input_pos - config.left_padding;
151                    if input_pos >= config.input_length {
152                        continue;
153                    }
154                    let input_value = input[input_channel * config.input_length + input_pos];
155                    let weight_value = weight[(out_channel * channels_in_per_group
156                        + group_input_channel)
157                        * config.kernel_size
158                        + kernel_index];
159                    sum += input_value * weight_value;
160                }
161            }
162            output[out_channel * output_length + out_pos] = sum;
163        }
164    }
165    output
166}
167
168pub fn conv1d(input: &[f32], weight: &[f32], config: Conv1dConfig) -> Vec<f32> {
169    let output_length = config.output_length().expect("output length");
170    let channels_in_per_group = config.channels_in / config.groups;
171    let channels_out_per_group = config.channels_out / config.groups;
172    let mut output = vec![0.0; config.channels_out * output_length];
173    for out_channel in 0..config.channels_out {
174        let group = out_channel / channels_out_per_group;
175        let input_channel_start = group * channels_in_per_group;
176        for out_pos in 0..output_length {
177            let mut sum = 0.0f32;
178            for group_input_channel in 0..channels_in_per_group {
179                let input_channel = input_channel_start + group_input_channel;
180                for kernel_index in 0..config.kernel_size {
181                    let raw_input_pos = out_pos * config.stride + kernel_index * config.dilation;
182                    if raw_input_pos < config.left_padding {
183                        continue;
184                    }
185                    let input_pos = raw_input_pos - config.left_padding;
186                    if input_pos >= config.input_length {
187                        continue;
188                    }
189                    let input_value = input[input_channel * config.input_length + input_pos];
190                    let weight_value = weight[(out_channel * channels_in_per_group
191                        + group_input_channel)
192                        * config.kernel_size
193                        + kernel_index];
194                    sum += input_value * weight_value;
195                }
196            }
197            output[out_channel * output_length + out_pos] = sum;
198        }
199    }
200    output
201}
202
203pub fn conv1d_bias_activation(
204    input: &[f32],
205    weight: &[f32],
206    bias: &[f32],
207    config: Conv1dConfig,
208    activation: Conv1dActivation,
209) -> Vec<f32> {
210    let output_length = config.output_length().expect("output length");
211    let mut output = conv1d(input, weight, config);
212    for out_channel in 0..config.channels_out {
213        for out_pos in 0..output_length {
214            let value = &mut output[out_channel * output_length + out_pos];
215            *value += bias[out_channel];
216            *value = apply_activation(*value, activation);
217        }
218    }
219    output
220}
221
222pub fn conv1d_batched(input: &[f32], weight: &[f32], config: BatchedConv1dConfig) -> Vec<f32> {
223    let output_length = config.output_length().expect("output length");
224    let output_len = batched_reach(
225        config.batch,
226        config.output_batch_stride,
227        config.channels_out,
228        output_length,
229    );
230    let channels_in_per_group = config.channels_in / config.groups;
231    let channels_out_per_group = config.channels_out / config.groups;
232    let mut output = vec![0.0; output_len];
233    for batch in 0..config.batch {
234        let input_batch_base = batch * config.input_batch_stride;
235        let output_batch_base = batch * config.output_batch_stride;
236        for out_channel in 0..config.channels_out {
237            let group = out_channel / channels_out_per_group;
238            let input_channel_start = group * channels_in_per_group;
239            for out_pos in 0..output_length {
240                let mut sum = 0.0f32;
241                for group_input_channel in 0..channels_in_per_group {
242                    let input_channel = input_channel_start + group_input_channel;
243                    for kernel_index in 0..config.kernel_size {
244                        let raw_input_pos =
245                            out_pos * config.stride + kernel_index * config.dilation;
246                        if raw_input_pos < config.left_padding {
247                            continue;
248                        }
249                        let input_pos = raw_input_pos - config.left_padding;
250                        if input_pos >= config.input_length {
251                            continue;
252                        }
253                        let input_value = input
254                            [input_batch_base + input_channel * config.input_length + input_pos];
255                        let weight_value = weight[(out_channel * channels_in_per_group
256                            + group_input_channel)
257                            * config.kernel_size
258                            + kernel_index];
259                        sum += input_value * weight_value;
260                    }
261                }
262                output[output_batch_base + out_channel * output_length + out_pos] = sum;
263            }
264        }
265    }
266    output
267}
268
269pub fn conv1d_batched_im2col(input: &[f32], config: Conv1dIm2colConfig) -> Vec<f32> {
270    let output_length = config.output_length().expect("output length");
271    let channels_in_per_group = config.channels_in / config.groups;
272    let output_values_per_batch = config.output_values_per_batch().expect("output values");
273    let mut output = vec![0.0; config.batch * output_values_per_batch];
274    for batch in 0..config.batch {
275        let input_batch_base = batch * config.input_batch_stride;
276        let output_batch_base = batch * output_values_per_batch;
277        for group in 0..config.groups {
278            for out_pos in 0..output_length {
279                for group_input_channel in 0..channels_in_per_group {
280                    let input_channel = group * channels_in_per_group + group_input_channel;
281                    for kernel_index in 0..config.kernel_size {
282                        let output_offset = output_batch_base
283                            + (((group * output_length + out_pos) * channels_in_per_group
284                                + group_input_channel)
285                                * config.kernel_size
286                                + kernel_index);
287                        let raw_input_pos =
288                            out_pos * config.stride + kernel_index * config.dilation;
289                        if raw_input_pos < config.left_padding {
290                            continue;
291                        }
292                        let input_pos = raw_input_pos - config.left_padding;
293                        if input_pos >= config.input_length {
294                            continue;
295                        }
296                        output[output_offset] = input
297                            [input_batch_base + input_channel * config.input_length + input_pos];
298                    }
299                }
300            }
301        }
302    }
303    output
304}
305
306pub fn conv1d_batched_from_im2col(
307    columns: &[f32],
308    weight: &[f32],
309    config: BatchedConv1dConfig,
310) -> Vec<f32> {
311    let output_length = config.output_length().expect("output length");
312    let output_len = batched_reach(
313        config.batch,
314        config.output_batch_stride,
315        config.channels_out,
316        output_length,
317    );
318    let channels_in_per_group = config.channels_in / config.groups;
319    let channels_out_per_group = config.channels_out / config.groups;
320    let column_values_per_batch = config.channels_in * output_length * config.kernel_size;
321    let mut output = vec![0.0; output_len];
322    for batch in 0..config.batch {
323        let column_batch_base = batch * column_values_per_batch;
324        let output_batch_base = batch * config.output_batch_stride;
325        for out_channel in 0..config.channels_out {
326            let group = out_channel / channels_out_per_group;
327            for out_pos in 0..output_length {
328                let mut sum = 0.0f32;
329                for group_input_channel in 0..channels_in_per_group {
330                    for kernel_index in 0..config.kernel_size {
331                        let column_offset = column_batch_base
332                            + (((group * output_length + out_pos) * channels_in_per_group
333                                + group_input_channel)
334                                * config.kernel_size
335                                + kernel_index);
336                        let weight_offset = (out_channel * channels_in_per_group
337                            + group_input_channel)
338                            * config.kernel_size
339                            + kernel_index;
340                        sum += columns[column_offset] * weight[weight_offset];
341                    }
342                }
343                output[output_batch_base + out_channel * output_length + out_pos] = sum;
344            }
345        }
346    }
347    output
348}
349
350pub fn conv1d_causal_bias(
351    input: &[f32],
352    weight: &[f32],
353    bias: &[f32],
354    config: CausalConv1dConfig,
355) -> Vec<f32> {
356    let output_length = config.output_length().expect("output length");
357    let mut output = conv1d_causal(input, weight, config);
358    for out_channel in 0..config.channels_out {
359        for out_pos in 0..output_length {
360            output[out_channel * output_length + out_pos] += bias[out_channel];
361        }
362    }
363    output
364}
365
366pub fn conv1d_causal_bias_gelu(
367    input: &[f32],
368    weight: &[f32],
369    bias: &[f32],
370    config: CausalConv1dConfig,
371) -> Vec<f32> {
372    let mut output = conv1d_causal_bias(input, weight, bias, config);
373    for value in &mut output {
374        *value = gelu(*value);
375    }
376    output
377}
378
379pub fn conv1d_causal_bias_activation(
380    input: &[f32],
381    weight: &[f32],
382    bias: &[f32],
383    config: CausalConv1dConfig,
384    activation: Conv1dActivation,
385) -> Vec<f32> {
386    let output_length = config.output_length().expect("output length");
387    let mut output = conv1d_causal_bias(input, weight, bias, config);
388    for out_channel in 0..config.channels_out {
389        for out_pos in 0..output_length {
390            let value = &mut output[out_channel * output_length + out_pos];
391            *value = apply_activation(*value, activation);
392        }
393    }
394    output
395}
396
397fn batched_reach(batch: usize, batch_stride: usize, channels: usize, length: usize) -> usize {
398    (batch - 1) * batch_stride + channels * length
399}
400
401fn gelu(value: f32) -> f32 {
402    0.5 * value * (1.0 + (0.7978846 * (value + 0.044715 * value * value * value)).tanh())
403}
404
405fn apply_activation(value: f32, activation: Conv1dActivation) -> f32 {
406    match activation {
407        Conv1dActivation::None => value,
408        Conv1dActivation::Gelu => gelu(value),
409    }
410}
411
412fn validate_conv1d_config(config: Conv1dConfig) -> Result<()> {
413    validate_conv1d_shape(
414        config.channels_in,
415        config.channels_out,
416        config.input_length,
417        config.kernel_size,
418        config.stride,
419        config.dilation,
420        config.groups,
421    )
422}
423
424fn validate_batched_conv1d_config(config: BatchedConv1dConfig) -> Result<()> {
425    if config.batch == 0 || config.input_batch_stride == 0 || config.output_batch_stride == 0 {
426        return Err(Error::InvalidLength);
427    }
428    validate_conv1d_shape(
429        config.channels_in,
430        config.channels_out,
431        config.input_length,
432        config.kernel_size,
433        config.stride,
434        config.dilation,
435        config.groups,
436    )?;
437    let input_item_len = checked_element_count(config.channels_in, config.input_length)?;
438    let output_length = conv1d_output_length(
439        config.input_length,
440        config.kernel_size,
441        config.stride,
442        config.dilation,
443        config.left_padding,
444        config.right_padding,
445    )?;
446    let output_item_len = checked_element_count(config.channels_out, output_length)?;
447    if config.input_batch_stride < input_item_len || config.output_batch_stride < output_item_len {
448        return Err(Error::InvalidLength);
449    }
450    Ok(())
451}
452
453fn validate_conv1d_im2col_config(config: Conv1dIm2colConfig) -> Result<()> {
454    if config.batch == 0 || config.input_batch_stride == 0 {
455        return Err(Error::InvalidLength);
456    }
457    validate_conv1d_shape(
458        config.channels_in,
459        config.channels_in,
460        config.input_length,
461        config.kernel_size,
462        config.stride,
463        config.dilation,
464        config.groups,
465    )?;
466    let input_item_len = checked_element_count(config.channels_in, config.input_length)?;
467    if config.input_batch_stride < input_item_len {
468        return Err(Error::InvalidLength);
469    }
470    Ok(())
471}
472
473fn validate_causal_conv1d_config(config: CausalConv1dConfig) -> Result<()> {
474    validate_conv1d_shape(
475        config.channels_in,
476        config.channels_out,
477        config.input_length,
478        config.kernel_size,
479        config.stride,
480        config.dilation,
481        config.groups,
482    )
483}
484
485fn validate_conv1d_shape(
486    channels_in: usize,
487    channels_out: usize,
488    input_length: usize,
489    kernel_size: usize,
490    stride: usize,
491    dilation: usize,
492    groups: usize,
493) -> Result<()> {
494    if channels_in == 0
495        || channels_out == 0
496        || input_length == 0
497        || kernel_size == 0
498        || stride == 0
499        || dilation == 0
500        || groups == 0
501    {
502        return Err(Error::InvalidLength);
503    }
504    if !channels_in.is_multiple_of(groups) || !channels_out.is_multiple_of(groups) {
505        return Err(Error::InvalidLength);
506    }
507    effective_kernel_size(kernel_size, dilation)?;
508    Ok(())
509}
510
511fn conv1d_output_length(
512    input_length: usize,
513    kernel_size: usize,
514    stride: usize,
515    dilation: usize,
516    left_padding: usize,
517    right_padding: usize,
518) -> Result<usize> {
519    let padded = input_length
520        .checked_add(left_padding)
521        .and_then(|padded| padded.checked_add(right_padding))
522        .ok_or(Error::SizeOverflow)?;
523    let effective_kernel_size = effective_kernel_size(kernel_size, dilation)?;
524    if padded < effective_kernel_size {
525        return Ok(0);
526    }
527    Ok((padded - effective_kernel_size) / stride + 1)
528}
529
530fn effective_kernel_size(kernel_size: usize, dilation: usize) -> Result<usize> {
531    kernel_size
532        .checked_sub(1)
533        .ok_or(Error::SizeOverflow)?
534        .checked_mul(dilation)
535        .and_then(|size| size.checked_add(1))
536        .ok_or(Error::SizeOverflow)
537}
538
539/// Depthwise causal conv1d decode step with in-place rolling state and SiLU.
540///
541/// `input` and `out` are `[batch, channels]`. `conv_state` is
542/// `[batch, channels, kernel_size]` and is shifted left by one slot per
543/// channel before inserting the new input at `kernel_size - 1`. `weight` is
544/// `[channels, kernel_size]`, `bias` is `[channels]`, and the returned output
545/// is `silu(dot(updated_state, weight) + bias)`.
546pub fn causal_conv1d_update_silu(
547    conv_state: &[f32],
548    input: &[f32],
549    weight: &[f32],
550    bias: &[f32],
551    batch: usize,
552    channels: usize,
553    kernel_size: usize,
554) -> (Vec<f32>, Vec<f32>) {
555    let mut out = vec![0.0f32; batch * channels];
556    let mut next_state = conv_state.to_vec();
557    for batch_index in 0..batch {
558        for (channel, bias_value) in bias.iter().copied().enumerate().take(channels) {
559            let token_offset = batch_index * channels + channel;
560            let state_offset = (batch_index * channels + channel) * kernel_size;
561            let weight_offset = channel * kernel_size;
562            let mut dot = bias_value;
563            for kernel in 0..kernel_size {
564                let value = if kernel + 1 == kernel_size {
565                    input[token_offset]
566                } else {
567                    conv_state[state_offset + kernel + 1]
568                };
569                dot += value * weight[weight_offset + kernel];
570            }
571            out[token_offset] = dot / (1.0 + (-dot).exp());
572            for kernel in 0..kernel_size - 1 {
573                next_state[state_offset + kernel] = conv_state[state_offset + kernel + 1];
574            }
575            next_state[state_offset + kernel_size - 1] = input[token_offset];
576        }
577    }
578    (out, next_state)
579}
580
581/// Depthwise causal conv1d sequence prefill from an implicit zero state.
582///
583/// `input` and `out` use `[batch, channels, time]` layout. For each output
584/// position, values before the beginning of the sequence are zero-padded. The
585/// returned state is the final `[batch, channels, kernel_size]` rolling window,
586/// also left-padded with zeros when the input is shorter than the kernel.
587pub fn causal_conv1d_prefill_silu(
588    input: &[f32],
589    weight: &[f32],
590    bias: &[f32],
591    batch: usize,
592    channels: usize,
593    kernel_size: usize,
594    input_length: usize,
595    output_length: usize,
596) -> (Vec<f32>, Vec<f32>) {
597    let mut out = vec![0.0f32; batch * channels * output_length];
598    let mut final_state = vec![0.0f32; batch * channels * kernel_size];
599    for batch_index in 0..batch {
600        for (channel, bias_value) in bias.iter().copied().enumerate().take(channels) {
601            let input_offset = (batch_index * channels + channel) * input_length;
602            let output_offset = (batch_index * channels + channel) * output_length;
603            let weight_offset = channel * kernel_size;
604            let state_offset = (batch_index * channels + channel) * kernel_size;
605
606            for time in 0..output_length {
607                let mut dot = bias_value;
608                for kernel in 0..kernel_size {
609                    let input_time = time as isize + kernel as isize + 1 - kernel_size as isize;
610                    let value = if input_time >= 0 && (input_time as usize) < input_length {
611                        input[input_offset + input_time as usize]
612                    } else {
613                        0.0
614                    };
615                    dot += value * weight[weight_offset + kernel];
616                }
617                out[output_offset + time] = dot / (1.0 + (-dot).exp());
618            }
619
620            for kernel in 0..kernel_size {
621                let input_time = input_length as isize + kernel as isize - kernel_size as isize;
622                final_state[state_offset + kernel] =
623                    if input_time >= 0 && (input_time as usize) < input_length {
624                        input[input_offset + input_time as usize]
625                    } else {
626                        0.0
627                    };
628            }
629        }
630    }
631    (out, final_state)
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn causal_conv1d_prefill_matches_repeated_update_from_zero_state() {
640        let batch = 2usize;
641        let channels = 3usize;
642        let kernel_size = 5usize;
643        let input_length = 4usize;
644        let input = (0..batch * channels * input_length)
645            .map(|index| (index as f32 % 11.0) * 0.125 - 0.5)
646            .collect::<Vec<_>>();
647        let weight = (0..channels * kernel_size)
648            .map(|index| (index as f32 % 7.0) * 0.25 - 0.75)
649            .collect::<Vec<_>>();
650        let bias = (0..channels)
651            .map(|index| (index as f32 % 5.0) * 0.125 - 0.25)
652            .collect::<Vec<_>>();
653
654        let (actual_out, actual_state) = causal_conv1d_prefill_silu(
655            &input,
656            &weight,
657            &bias,
658            batch,
659            channels,
660            kernel_size,
661            input_length,
662            input_length,
663        );
664        let mut expected_out = vec![0.0f32; batch * channels * input_length];
665        let mut state = vec![0.0f32; batch * channels * kernel_size];
666        for time in 0..input_length {
667            let token = (0..batch * channels)
668                .map(|index| {
669                    let batch_index = index / channels;
670                    let channel = index % channels;
671                    input[(batch_index * channels + channel) * input_length + time]
672                })
673                .collect::<Vec<_>>();
674            let (step_out, next_state) = causal_conv1d_update_silu(
675                &state,
676                &token,
677                &weight,
678                &bias,
679                batch,
680                channels,
681                kernel_size,
682            );
683            for batch_index in 0..batch {
684                for channel in 0..channels {
685                    expected_out[(batch_index * channels + channel) * input_length + time] =
686                        step_out[batch_index * channels + channel];
687                }
688            }
689            state = next_state;
690        }
691
692        assert_eq!(actual_out, expected_out);
693        assert_eq!(actual_state, state);
694    }
695
696    #[test]
697    fn causal_conv1d_prefill_final_state_zero_pads_short_input() {
698        let input = [1.0f32, 2.0];
699        let weight = [1.0f32, 1.0, 1.0, 1.0];
700        let bias = [0.0f32];
701        let (_, state) = causal_conv1d_prefill_silu(&input, &weight, &bias, 1, 1, 4, 2, 2);
702        assert_eq!(state, vec![0.0, 0.0, 1.0, 2.0]);
703    }
704}