Skip to main content

nam_rs/models/convnet/
block.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
3
4//! ConvNet feed-forward block: Conv1D → BatchNorm → Activation.
5//!
6//! Each block is simpler than a WaveNet layer — there is no gating mechanism
7//! (no input_mixin / 1x1) and no rechannel projection. Output feeds directly
8//! into the next block or the post-stack head.
9
10use crate::common::diagnostics::NamErrorCode;
11use crate::math::common::{AlignedVec, SimdMath};
12use crate::models::a2::activations::ActivationType;
13use crate::models::wavenet::Conv1dDyn;
14use crate::models::wavenet::common::{WAVENET_MAX_NUM_FRAMES, WaveNetLayerState};
15
16use super::batch_norm::BatchNorm1D;
17
18/// A single ConvNet feed-forward block.
19///
20/// Processing pipeline:
21/// 1. Write input frames into the causal ring buffer
22/// 2. Run causal Conv1D on the ring buffer → scratch buffer
23/// 3. Apply BatchNorm (affine transform) in-place on scratch
24/// 4. Apply activation function in-place on scratch
25/// 5. Copy scratch to output
26#[derive(Clone)]
27#[repr(align(64))]
28pub struct ConvNetBlock {
29    /// Causal 1D convolution (runtime dimensions).
30    pub conv: Conv1dDyn,
31    /// Batch normalization (pre-fused affine transform).
32    pub bn: BatchNorm1D,
33    /// Activation function applied after batch norm.
34    pub activation: ActivationType,
35    /// Ring buffer state for causal convolution lookback.
36    pub state: WaveNetLayerState,
37    /// Scratch buffer for convolution output (out_ch * WAVENET_MAX_NUM_FRAMES).
38    scratch: AlignedVec<f32>,
39}
40
41impl ConvNetBlock {
42    /// Creates a new `ConvNetBlock` with zero-initialized weights.
43    ///
44    /// Weights must be populated via `set_conv_weights` / `set_conv_bias`
45    /// and `set_bn_params` by the dispatcher.
46    pub fn new(
47        in_ch: usize,
48        out_ch: usize,
49        kernel: usize,
50        dilation: usize,
51        do_bias: bool,
52        activation: ActivationType,
53        alloc_num: usize,
54    ) -> std::io::Result<Self> {
55        let num_blocks = out_ch.div_ceil(4);
56        let weights_len = num_blocks * kernel * in_ch * 4;
57
58        let conv = Conv1dDyn {
59            weights: AlignedVec::new(weights_len, 0.0f32).map_err(|e| {
60                std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}"))
61            })?,
62            bias: AlignedVec::new(out_ch, 0.0f32).map_err(|e| {
63                std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}"))
64            })?,
65            do_bias,
66            dilation,
67            in_ch,
68            out_ch,
69            num_blocks,
70            interleave_width: 4,
71            kernel,
72        };
73
74        let bn = BatchNorm1D::from_fused(out_ch, &vec![0.0f32; out_ch], &vec![0.0f32; out_ch])
75            .map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}")))?;
76
77        let receptive_field = (kernel - 1) * dilation;
78        let state = WaveNetLayerState::new(in_ch, receptive_field, alloc_num)?;
79
80        let scratch = AlignedVec::new(out_ch * WAVENET_MAX_NUM_FRAMES, 0.0f32)
81            .map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}")))?;
82
83        Ok(Self {
84            conv,
85            bn,
86            activation,
87            state,
88            scratch,
89        })
90    }
91
92    /// Returns the receptive field contribution of this block.
93    pub fn receptive_field(&self) -> usize {
94        (self.conv.kernel - 1) * self.conv.dilation
95    }
96
97    /// Loads convolution weights from a flat f32 slice.
98    pub fn set_conv_weights(&mut self, weights: &[f32]) {
99        let len = self.conv.weights.len().min(weights.len());
100        self.conv.weights[..len].copy_from_slice(&weights[..len]);
101    }
102
103    /// Loads convolution bias from a flat f32 slice.
104    pub fn set_conv_bias(&mut self, bias: &[f32]) {
105        let len = self.conv.bias.len().min(bias.len());
106        self.conv.bias[..len].copy_from_slice(&bias[..len]);
107    }
108
109    /// Loads pre-fused batch norm parameters.
110    pub fn set_bn_params(&mut self, scale: &[f32], offset: &[f32]) -> Result<(), NamErrorCode> {
111        self.bn = BatchNorm1D::from_fused(self.conv.out_ch, scale, offset)?;
112        Ok(())
113    }
114
115    /// Public dispatch wrapper that selects the optimal SIMD path.
116    ///
117    /// # Safety
118    /// Input and output slices must have sizes compatible with the block dimensions:
119    /// `input.len() == num_frames * in_ch`, `output.len() == num_frames * out_ch`.
120    #[inline(always)]
121    pub unsafe fn process_block(&mut self, input: &[f32], output: &mut [f32], num_frames: usize) {
122        unsafe {
123            crate::math::common::dispatch_simd!(
124                self,
125                process_block_internal,
126                input,
127                output,
128                num_frames
129            )
130        };
131    }
132
133    /// SIMD-dispatched processing kernel.
134    ///
135    /// # Safety
136    /// `input.len()` must equal `num_frames * in_ch`.
137    /// `output.len()` must equal `num_frames * out_ch`.
138    #[inline(always)]
139    pub unsafe fn process_block_internal<M: SimdMath>(
140        &mut self,
141        input: &[f32],
142        output: &mut [f32],
143        num_frames: usize,
144    ) {
145        let in_ch = self.conv.in_ch;
146        let out_ch = self.conv.out_ch;
147        let input_len = num_frames * in_ch;
148
149        let buf_start = self.state.buffer_start * in_ch;
150        self.state.layer_buffer[buf_start..buf_start + input_len]
151            .copy_from_slice(&input[..input_len]);
152
153        let scratch_slice = &mut self.scratch[..num_frames * out_ch];
154        unsafe {
155            self.conv.process_block::<M>(
156                &self.state.layer_buffer,
157                scratch_slice,
158                self.state.buffer_start,
159                num_frames,
160                None,
161            );
162        }
163
164        unsafe {
165            self.bn.process_simd::<M>(scratch_slice, num_frames);
166        }
167
168        unsafe {
169            self.activation.apply_simd::<M>(scratch_slice);
170        }
171
172        output[..num_frames * out_ch].copy_from_slice(scratch_slice);
173
174        self.state.advance_frames(num_frames, in_ch);
175    }
176
177    /// Public prewarm wrapper with SIMD dispatch.
178    #[cold]
179    pub fn prewarm(&mut self) {
180        unsafe {
181            crate::math::common::dispatch_simd!(self, prewarm_internal);
182        }
183    }
184
185    /// Fills the conv state buffer with a single frame of silence replicated
186    /// backward to cover the entire receptive field.
187    ///
188    /// # Safety
189    /// Must be called via `dispatch_simd!` macro. The state buffer must be
190    /// properly allocated and the ring buffer start pointer must be valid.
191    #[inline(always)]
192    pub unsafe fn prewarm_internal<M: SimdMath>(&mut self) {
193        let in_ch = self.conv.in_ch;
194        let out_ch = self.conv.out_ch;
195        let kernel = self.conv.kernel;
196        let dilation = self.conv.dilation;
197
198        let silence = vec![0.0f32; in_ch];
199        let buf_start = self.state.buffer_start * in_ch;
200
201        self.state.layer_buffer[buf_start..buf_start + in_ch].copy_from_slice(&silence);
202
203        let start_idx = buf_start;
204        let src_range = start_idx..start_idx + in_ch;
205        let max_offset = (kernel - 1) * dilation + 1;
206        for offset in 1..=max_offset {
207            let dst_idx = (self.state.buffer_start - offset) * in_ch;
208            self.state
209                .layer_buffer
210                .copy_within(src_range.clone(), dst_idx);
211        }
212
213        let scratch_slice = &mut self.scratch[..out_ch];
214        unsafe {
215            self.conv.process_single_frame::<M>(
216                &self.state.layer_buffer,
217                scratch_slice,
218                self.state.buffer_start,
219                None,
220            );
221        }
222        unsafe {
223            self.bn.process_simd::<M>(scratch_slice, 1);
224        }
225        unsafe {
226            self.activation.apply_simd::<M>(scratch_slice);
227        }
228
229        self.state.advance_frames(1, in_ch);
230    }
231}
232
233#[cfg(test)]
234#[path = "convnet_block_test.rs"]
235mod tests;