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}