1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
use crate::math::common::AlignedVec;
use crate::math::common::SimdMath;
use crate::models::a2::activations::ActivationType;
use super::common::{WAVENET_MAX_NUM_FRAMES, WaveNetLayerState};
use super::conv1d_dyn::Conv1dDyn;
use crate::loader::nam_json::model::HeadConfig;
/// Post-stack head sub-object for WaveNet / ConvNet architectures.
///
/// Contains a causal Conv1D + activation that processes the signal
/// after the stack of layer arrays, before the final `head_scale` gain.
/// Mirrors the `_Head` structure in NAMCore's `convnet.h:108-118`.
#[derive(Clone)]
#[repr(align(64))]
pub struct PostStackHead {
/// Causal 1D convolution (dynamic runtime dimensions).
pub conv: Conv1dDyn,
/// Activation function applied after convolution.
pub activation: ActivationType,
/// Ring buffer state for causal convolution lookback.
pub state: WaveNetLayerState,
/// Scratch buffer for convolution output (out_ch * WAVENET_MAX_NUM_FRAMES).
scratch: AlignedVec<f32>,
}
impl PostStackHead {
/// Creates a new `PostStackHead` from the parsed `HeadConfig` and the
/// input channel count from the last layer array.
///
/// Missing fields in `HeadConfig` fall back to sensible defaults:
/// - `channels` → `in_channels` (same as the last array's head projection)
/// - `out_channels` → 1 (mono output)
/// - `kernel_size` → 3
/// - `bias` → false
/// - `activation` → "Tanh"
///
/// Weight and bias arrays are zero-initialized and must be populated
/// by the dispatcher via `set_weights` and `set_bias`.
pub fn from_config(config: &HeadConfig, in_channels: usize) -> std::io::Result<Self> {
let channels = config.channels.unwrap_or(in_channels);
let out_channels = config.out_channels.unwrap_or(1);
let kernel = config.kernel_size.unwrap_or(3);
let do_bias = config.bias.unwrap_or(false);
let activation = parse_activation(config.activation.as_deref().unwrap_or("Tanh"));
let num_blocks = out_channels.div_ceil(4);
let weights_len = num_blocks * kernel * channels * 4;
let bias_len = out_channels;
let weights = AlignedVec::new(weights_len, 0.0f32)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}")))?;
let bias = AlignedVec::new(bias_len, 0.0f32)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}")))?;
let receptive_field = kernel;
let state = WaveNetLayerState::new(channels, receptive_field, 0)?;
let conv = Conv1dDyn {
weights,
bias,
do_bias,
dilation: 1,
in_ch: channels,
out_ch: out_channels,
num_blocks,
interleave_width: 4,
kernel,
};
let scratch = AlignedVec::new(out_channels * WAVENET_MAX_NUM_FRAMES, 0.0f32)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, format!("{e}")))?;
Ok(Self {
conv,
activation,
state,
scratch,
})
}
/// Returns the receptive field contribution of this head (kernel size).
/// Must be added to the global model receptive field for prewarm.
pub fn receptive_field(&self) -> usize {
self.conv.kernel
}
/// Number of output channels produced by this head.
pub fn out_channels(&self) -> usize {
self.conv.out_ch
}
/// Number of input channels expected by this head.
pub fn in_channels(&self) -> usize {
self.conv.in_ch
}
/// Loads convolution weights from a flat f32 slice.
pub fn set_weights(&mut self, weights: &[f32]) {
let len = self.conv.weights.len().min(weights.len());
self.conv.weights[..len].copy_from_slice(&weights[..len]);
}
/// Loads convolution bias from a flat f32 slice, if present.
pub fn set_bias(&mut self, bias: &[f32]) {
let len = self.conv.bias.len().min(bias.len());
self.conv.bias[..len].copy_from_slice(&bias[..len]);
}
/// Public dispatch wrapper that selects the optimal SIMD path.
///
/// # Safety
/// Input and output slices must have sizes compatible with the head dimensions:
/// `input.len() == num_frames * in_ch`, `output.len() == num_frames * out_ch`.
/// The ring buffer state must have been properly initialized (via `prewarm` or
/// sufficient prior processing) to cover the causal receptive field.
#[inline(always)]
pub unsafe fn process_block(&mut self, input: &[f32], output: &mut [f32], num_frames: usize) {
unsafe {
crate::math::common::dispatch_simd!(
self,
process_block_internal,
input,
output,
num_frames
)
};
}
/// SIMD-dispatched processing kernel.
///
/// Writes `num_frames` of input into the ring buffer, runs the causal
/// Conv1D, applies activation, and writes results to output.
///
/// Input layout: frame-interleaved `[f0_c0, f0_c1, ..., f1_c0, ...]`.
/// Output layout: frame-interleaved `[f0_c0, f0_c1, ..., f1_c0, ...]`.
///
/// # Safety
/// `input` and `output` must have sizes `num_frames * in_ch` and
/// `num_frames * out_ch` respectively. The ring buffer must have been
/// properly initialized.
#[inline(always)]
pub unsafe fn process_block_internal<M: SimdMath>(
&mut self,
input: &[f32],
output: &mut [f32],
num_frames: usize,
) {
let in_ch = self.conv.in_ch;
let out_ch = self.conv.out_ch;
let input_len = num_frames * in_ch;
let buf_start = self.state.buffer_start * in_ch;
unsafe {
core::ptr::copy_nonoverlapping(
input.as_ptr(),
self.state.layer_buffer.as_mut_ptr().add(buf_start),
input_len,
);
}
let scratch_slice = &mut self.scratch[..num_frames * out_ch];
unsafe {
self.conv.process_block::<M>(
&self.state.layer_buffer,
scratch_slice,
self.state.buffer_start,
num_frames,
None,
);
}
unsafe {
self.activation.apply_simd::<M>(scratch_slice);
}
unsafe {
core::ptr::copy_nonoverlapping(
scratch_slice.as_ptr(),
output.as_mut_ptr(),
num_frames * out_ch,
);
}
self.state.advance_frames(num_frames, in_ch);
}
/// Public prewarm wrapper with SIMD dispatch.
#[cold]
pub fn prewarm(&mut self) {
unsafe {
crate::math::common::dispatch_simd!(self, prewarm_internal);
}
}
/// Fills the conv state buffer with a single frame of silence replicated
/// backward to cover the entire receptive field.
///
/// # Safety
/// Must be called via `dispatch_simd!` macro. The state buffer must be
/// properly allocated and the ring buffer start pointer must be valid.
#[inline(always)]
pub unsafe fn prewarm_internal<M: SimdMath>(&mut self) {
let in_ch = self.conv.in_ch;
let out_ch = self.conv.out_ch;
let kernel = self.conv.kernel;
let buf_start = self.state.buffer_start * in_ch;
self.state.layer_buffer[buf_start..buf_start + in_ch].fill(0.0);
let start_idx = self.state.buffer_start * in_ch;
let src_range = start_idx..start_idx + in_ch;
for offset in 1..=kernel {
let dst_idx = (self.state.buffer_start - offset) * in_ch;
self.state
.layer_buffer
.copy_within(src_range.clone(), dst_idx);
}
let scratch_slice = &mut self.scratch[..out_ch];
unsafe {
self.conv.process_single_frame::<M>(
&self.state.layer_buffer,
scratch_slice,
self.state.buffer_start,
None,
);
}
unsafe {
self.activation.apply_simd::<M>(scratch_slice);
}
self.state.advance_frames(1, in_ch);
}
}
/// Maps an activation function name string to an `ActivationType`.
///
/// Supported values match the variant names of `ActivationType`:
/// `"Tanh"`, `"HardTanh"`, `"FastTanh"`, `"ReLU"`, `"Sigmoid"`,
/// `"SiLU"`, `"HardSwish"`, `"Softsign"`.
///
/// Unrecognized strings fall back to `ActivationType::Tanh`.
pub fn parse_activation(name: &str) -> ActivationType {
match name {
"Tanh" => ActivationType::Tanh,
"HardTanh" => ActivationType::HardTanh,
"FastTanh" => ActivationType::FastTanh,
"ReLU" => ActivationType::ReLU,
"Sigmoid" => ActivationType::Sigmoid,
"SiLU" => ActivationType::SiLU,
"HardSwish" => ActivationType::HardSwish,
"Softsign" => ActivationType::Softsign,
_ => ActivationType::Tanh,
}
}
#[cfg(test)]
#[path = "post_stack_head_test.rs"]
mod tests;