Skip to main content

nam_rs/models/a2/model/static/
process.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//! WaveNet A2 static model — processing methods.
5//!
6//! ## Block Size Contract
7//!
8//! Any input size ≤ `max_buffer_size` is safe: processing is internally chunked
9//! into sub-blocks of ≤ `WAVENET_MAX_NUM_FRAMES` (64), matching the kernel scratch
10//! buffer capacity. Exceeding `max_buffer_size` causes silent truncation: only the
11//! first `max_buffer_size` frames are processed and the remaining are left as zeros.
12//! This matches the CLAP/audio host contract which guarantees
13//! `block_size <= max_block_size` negotiated at activation.
14//!
15//! The `dispatch_simd!` macro evaluates `M::ISA` (compile-time constant
16//! through the `SimdMath` trait) to monomorphize the full forward pass,
17//! eliminating per-frame `is_x86_feature_detected` branches and the dynamic
18//! read of `SIMD_MATH` in the inner loop.
19
20use crate::math::common::SimdMath;
21use crate::models::wavenet::common::WAVENET_MAX_NUM_FRAMES;
22
23use super::super::super::params::{A2_HEAD_KERNEL_SIZE, A2_NUM_LAYERS};
24use super::WaveNetA2;
25
26impl<const CH: usize> WaveNetA2<CH> {
27    /// Full forward pass through the A2 model.
28    ///
29    /// Processes `input` samples and writes to `output`.
30    /// Requires layers to be populated via `set_weights`.
31    /// Outputs silence until weights are loaded.
32    ///
33    /// ## Ring buffer architecture
34    ///
35    /// Layer history uses `MirroredBuffer<f32>` with power-of-2 sizes.
36    /// Writes go to unmasked positions in the 2× virtual mapping; reads are
37    /// branchless because the mirror maps `[S, 2S)` → `[0, S)`. When
38    /// `buffer_start` approaches the 2× boundary, it rewinds by subtracting
39    /// `ring_size`. No `copy_within` / memmove on the hot path.
40    ///
41    /// Head accumulator uses a plain `AlignedVec` with pow2 mask (`& ring_mask`).
42    /// A pre-write memmove preserves `K-1` tail samples when the ring is about
43    /// to overflow, keeping the write-positions unmasked for vectorized stores.
44    pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
45        unsafe {
46            crate::math::common::dispatch_simd!(self, process_internal, input, output);
47        }
48    }
49
50    /// Monomorphized inner loop — see [`process`](Self::process) for contract.
51    #[inline(always)]
52    unsafe fn process_internal<M: SimdMath>(&mut self, input: &[f32], output: &mut [f32]) {
53        let total = input.len();
54        if total == 0 {
55            return;
56        }
57
58        output[..total].fill(0.0);
59
60        if self.layers.is_empty() {
61            self.head_write_pos = (self.head_write_pos + total) & self.head_ring_mask;
62            return;
63        }
64
65        debug_assert!(
66            total <= self.max_buffer_size,
67            "process: input ({total}) > max_buffer_size ({}) — host violated block-size contract",
68            self.max_buffer_size
69        );
70        let nf_total = total.min(self.max_buffer_size);
71
72        let mut pos = 0;
73        while pos < nf_total {
74            let nf = (nf_total - pos).min(WAVENET_MAX_NUM_FRAMES);
75
76            self.rechannel_prescale(input, pos, nf);
77            let head_wp = self.advance_head_ring(nf);
78
79            for li in 0..A2_NUM_LAYERS {
80                unsafe {
81                    self.layer_forward_dispatch::<M>(li, nf, input, pos, head_wp);
82                }
83            }
84
85            self.head_finalize(head_wp, nf, &mut output[pos..pos + nf]);
86            pos += nf;
87        }
88    }
89
90    /// Phase 0: input rechannel pre-scaling — `input × rechannel_w_f32 → layer_in`.
91    #[inline(always)]
92    fn rechannel_prescale(&mut self, input: &[f32], pos: usize, nf: usize) {
93        if CH == 8 {
94            use core::arch::x86_64::{
95                _mm256_load_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_store_ps,
96            };
97            unsafe {
98                let rw_vec = _mm256_load_ps(self.rechannel_w_f32.as_ptr());
99                for (f, &x) in input[pos..pos + nf].iter().enumerate() {
100                    let x_vec = _mm256_set1_ps(x);
101                    let res = _mm256_mul_ps(rw_vec, x_vec);
102                    _mm256_store_ps(self.layer_in.as_mut_ptr().add(f * 8), res);
103                }
104            }
105        } else {
106            for (f, &x) in input[pos..pos + nf].iter().enumerate() {
107                let base = f * CH;
108                for c in 0..CH {
109                    self.layer_in[base + c] = self.rechannel_w_f32[c] * x;
110                }
111            }
112        }
113    }
114
115    /// Advances the head accumulator ring buffer.
116    ///
117    /// When the write cursor plus `nf` would overflow the ring capacity,
118    /// the tail `K-1` samples are memmove'd to the start and the write
119    /// position wraps around. Returns the (possibly wrapped) write position
120    /// for use by the layer loop.
121    #[inline(always)]
122    fn advance_head_ring(&mut self, nf: usize) -> usize {
123        let head_keep = A2_HEAD_KERNEL_SIZE - 1;
124        let head_cap = self.head_ring_mask + 1;
125        if self.head_write_pos + nf > head_cap {
126            let keep_start = self.head_write_pos - head_keep;
127            let keep_bytes = head_keep * CH;
128            let src = keep_start * CH;
129            self.head_accum.copy_within(src..src + keep_bytes, 0);
130            self.head_write_pos = head_keep;
131        }
132        self.head_write_pos
133    }
134
135    /// Per-layer forward dispatch for a single layer index.
136    ///
137    /// # Safety
138    ///
139    /// Caller must ensure `li < A2_NUM_LAYERS` and that `nf` frames of valid
140    /// data are available at `input[pos..pos+nf]`. Internal conv/film/head
141    /// accesses assume caller-verified buffer capacities.
142    #[inline(always)]
143    unsafe fn layer_forward_dispatch<M: SimdMath>(
144        &mut self,
145        li: usize,
146        nf: usize,
147        input: &[f32],
148        pos: usize,
149        head_wp: usize,
150    ) {
151        let is_first = li == 0;
152        let is_last = li == A2_NUM_LAYERS - 1;
153        let ch = CH;
154        let ring_size = self.layer_ring_sizes[li];
155        let lookback = self.layer_lookbacks[li];
156        let max_lookback_cols = lookback / ch;
157        let bs = self.layer_buffer_starts[li];
158
159        debug_assert!(bs >= lookback);
160        debug_assert!(bs + nf * ch <= ring_size * 2);
161
162        // Copy layer_in → history buffer, then apply conv_pre_film.
163        {
164            let buf = &mut self.layer_buffers[li];
165            buf[bs..bs + nf * ch].copy_from_slice(&self.layer_in[..nf * ch]);
166            for f in 0..nf {
167                if let Some(ref mut film) = self.layers[li].conv_pre_film {
168                    unsafe {
169                        film.process(
170                            &mut buf[bs + f * ch..bs + (f + 1) * ch],
171                            &input[pos + f..pos + f + 1],
172                        );
173                    }
174                }
175            }
176        }
177
178        if bs + nf * ch + self.max_buffer_size * ch > ring_size * 2 {
179            self.layer_buffer_starts[li] = bs + nf * ch - ring_size;
180        } else {
181            self.layer_buffer_starts[li] = bs + nf * ch;
182        }
183
184        {
185            let history = &self.layer_buffers[li][bs - lookback..bs + nf * ch];
186            let layer = &mut self.layers[li];
187
188            let conv_ch = layer.conv_ch.as_ref();
189            let mixin_w = &layer.mixin_w;
190            let l1x1_w = &layer.l1x1_w;
191            let l1x1_b = &layer.l1x1_b;
192
193            let mut film_block = super::super::super::film::FilmBlock {
194                conv_pre_film: layer.conv_pre_film.as_mut(),
195                conv_post_film: layer.conv_post_film.as_mut(),
196                input_mixin_pre_film: layer.input_mixin_pre_film.as_mut(),
197                input_mixin_post_film: layer.input_mixin_post_film.as_mut(),
198                activation_pre_film: layer.activation_pre_film.as_mut(),
199                activation_post_film: layer.activation_post_film.as_mut(),
200                layer1x1_post_film: layer.layer1x1_post_film.as_mut(),
201                head1x1_post_film: layer.head1x1_post_film.as_mut(),
202            };
203
204            if let Some(conv_ch) = conv_ch {
205                match conv_ch {
206                    super::super::super::layer::A2ConvCh::Ch3(ch3_conv) => unsafe {
207                        super::super::super::conv1d_ch3::layer_forward_ch3_block(
208                            ch3_conv,
209                            mixin_w,
210                            l1x1_w,
211                            l1x1_b,
212                            &mut film_block,
213                            false,
214                            history,
215                            max_lookback_cols,
216                            nf,
217                            &input[pos..pos + nf],
218                            &mut self.head_accum,
219                            head_wp,
220                            &mut self.layer_in,
221                            is_first,
222                            is_last,
223                        );
224                    },
225                    super::super::super::layer::A2ConvCh::Ch8(ch8_conv) => unsafe {
226                        match M::ISA {
227                            crate::math::common::InstructionSet::Avx512
228                            | crate::math::common::InstructionSet::Avx512VnniBf16 => {
229                                super::super::super::conv1d_ch8::layer_forward_ch8_block_simdmath::<
230                                    M,
231                                >(
232                                    ch8_conv,
233                                    mixin_w,
234                                    l1x1_w,
235                                    l1x1_b,
236                                    &mut film_block,
237                                    false,
238                                    history,
239                                    max_lookback_cols,
240                                    nf,
241                                    &input[pos..pos + nf],
242                                    &mut self.head_accum,
243                                    head_wp,
244                                    &mut self.layer_in,
245                                    is_first,
246                                    is_last,
247                                );
248                            }
249                            _ => {
250                                super::super::super::conv1d_ch8::layer_forward_ch8_block(
251                                    ch8_conv,
252                                    mixin_w,
253                                    l1x1_w,
254                                    l1x1_b,
255                                    &mut film_block,
256                                    false,
257                                    history,
258                                    max_lookback_cols,
259                                    nf,
260                                    &input[pos..pos + nf],
261                                    &mut self.head_accum,
262                                    head_wp,
263                                    &mut self.layer_in,
264                                    is_first,
265                                    is_last,
266                                );
267                            }
268                        }
269                    },
270                }
271                return;
272            }
273            // Fallback: per-frame path using the generic A2Conv1d enum.
274            #[cfg(any(test, feature = "dynamic-engine"))]
275            {
276                use super::super::super::params::A2_LEAKY_SLOPE;
277
278                debug_assert!(self.z_scratch.len() >= ch);
279                for f in 0..nf {
280                    let frame_idx = max_lookback_cols + f;
281
282                    // 1. Dilated conv → z_buf.
283                    unsafe {
284                        layer.conv.process_single_frame::<M>(
285                            history,
286                            &mut self.z_scratch[..ch],
287                            frame_idx,
288                            None,
289                        );
290                    }
291
292                    // 1b. FiLM post-conv.
293                    let cond = &input[pos + f..pos + f + 1];
294                    if let Some(ref mut film) = film_block.conv_post_film {
295                        unsafe {
296                            film.process(&mut self.z_scratch[..ch], cond);
297                        }
298                    }
299
300                    // 2. Input mixin — input_mixin_pre_film applied to condition (self-modulation).
301                    let cond_val = input[pos + f];
302                    let cond_for_mixin = if let Some(ref mut film) = film_block.input_mixin_pre_film
303                    {
304                        let mut modulated = cond_val;
305                        let orig = cond_val;
306                        unsafe {
307                            film.process(
308                                core::slice::from_mut(&mut modulated),
309                                core::slice::from_ref(&orig),
310                            );
311                        }
312                        modulated
313                    } else {
314                        cond_val
315                    };
316                    for c in 0..ch {
317                        self.z_scratch[c] += mixin_w[c] * cond_for_mixin;
318                    }
319
320                    // 2b. FiLM post-mixin.
321                    if let Some(ref mut film) = film_block.input_mixin_post_film {
322                        unsafe {
323                            film.process(&mut self.z_scratch[..ch], cond);
324                        }
325                    }
326                    if let Some(ref mut film) = film_block.activation_pre_film {
327                        unsafe {
328                            film.process(&mut self.z_scratch[..ch], cond);
329                        }
330                    }
331
332                    // 3. LeakyReLU.
333                    for z in self.z_scratch.iter_mut().take(ch) {
334                        if *z < 0.0 {
335                            *z *= A2_LEAKY_SLOPE;
336                        }
337                    }
338
339                    // 3b. FiLM post-activation.
340                    if let Some(ref mut film) = film_block.activation_post_film {
341                        unsafe {
342                            film.process(&mut self.z_scratch[..ch], cond);
343                        }
344                    }
345
346                    // 4. Head accumulator.
347                    let head_off = (head_wp + f) * ch;
348                    if is_first {
349                        self.head_accum[head_off..head_off + ch]
350                            .copy_from_slice(&self.z_scratch[..ch]);
351                    } else {
352                        for (c, z) in self.z_scratch.iter().enumerate().take(ch) {
353                            self.head_accum[head_off + c] += *z;
354                        }
355                    }
356
357                    // 5. L1x1 residual.
358                    if !is_last {
359                        let base = f * ch;
360                        for c in 0..ch {
361                            let mut sum = l1x1_b[c];
362                            for u in 0..ch {
363                                sum += l1x1_w[u * ch + c] * self.z_scratch[u];
364                            }
365                            self.layer_in[base + c] += sum;
366                        }
367                        if let Some(ref mut film) = film_block.layer1x1_post_film {
368                            unsafe {
369                                film.process(&mut self.layer_in[base..base + ch], cond);
370                            }
371                        }
372                    }
373                }
374            }
375            #[cfg(not(any(test, feature = "dynamic-engine")))]
376            {
377                // RT-safe fallback: this branch is unreachable per set_weights
378                // invariant (A2 layers always have CH=3 or CH=8 conv). Retained
379                // as a panic-free guard for future model format changes — never
380                // panics on the audio thread.
381                if let Some(ref rt) = self.rt_status {
382                    rt.set_flag(crate::common::spsc::RT_STATUS_A2_FALLBACK_TRIGGERED);
383                }
384                debug_assert!(
385                    false,
386                    "A2 layers always have ch3 or ch8 conv; \
387                     scalar fallback triggered — silencing layer output"
388                );
389                self.z_scratch[..ch].fill(0.0);
390                for f in 0..nf {
391                    let head_off = (head_wp + f) * ch;
392                    if is_first {
393                        self.head_accum[head_off..head_off + ch]
394                            .copy_from_slice(&self.z_scratch[..ch]);
395                    }
396                    if !is_last {
397                        let base = f * ch;
398                        for c in 0..ch {
399                            let mut sum = l1x1_b[c];
400                            for u in 0..ch {
401                                sum += l1x1_w[u * ch + c] * self.z_scratch[u];
402                            }
403                            self.layer_in[base + c] += sum;
404                        }
405                        if let Some(ref mut film) = film_block.layer1x1_post_film {
406                            unsafe {
407                                film.process(
408                                    &mut self.layer_in[base..base + ch],
409                                    &input[pos + f..pos + f + 1],
410                                );
411                            }
412                        }
413                    }
414                }
415            }
416        }
417    }
418
419    /// Finalizes the head convolution and advances the head write position.
420    #[inline(always)]
421    fn head_finalize(&mut self, head_wp: usize, nf: usize, output: &mut [f32]) {
422        self.head_write_pos = (head_wp + nf) & self.head_ring_mask;
423
424        if let Some(ref head) = self.head_conv {
425            head.process(
426                &self.head_accum,
427                self.head_write_pos,
428                self.head_ring_mask,
429                nf,
430                output,
431            );
432        }
433    }
434}