diff --git a/NAM/activations.h b/NAM/activations.h index 1ba789c3..ac32ea31 100644 --- a/NAM/activations.h +++ b/NAM/activations.h @@ -302,9 +302,6 @@ class ActivationPReLU : public Activation // Matrix is organized as (channels, time_steps) unsigned long actual_channels = static_cast(matrix.rows()); - // Prepare the slopes for the current matrix size - std::vector slopes_for_channels = negative_slopes; - // Fail loudly if input has more channels than activation #ifndef NDEBUG if (actual_channels != negative_slopes.size()) @@ -321,7 +318,7 @@ class ActivationPReLU : public Activation // Apply the negative slope to all time steps in this channel for (int time_step = 0; time_step < matrix.cols(); time_step++) { - matrix(channel, time_step) = leaky_relu(matrix(channel, time_step), slopes_for_channels[channel]); + matrix(channel, time_step) = leaky_relu(matrix(channel, time_step), negative_slopes[channel]); } } } diff --git a/NAM/wavenet/model.cpp b/NAM/wavenet/model.cpp index 7fd9ed84..639195b9 100644 --- a/NAM/wavenet/model.cpp +++ b/NAM/wavenet/model.cpp @@ -427,8 +427,10 @@ long nam::wavenet::detail::LayerArray::get_receptive_field() const void nam::wavenet::detail::LayerArray::Process(const Eigen::MatrixXf& layer_inputs, const Eigen::MatrixXf& condition, const int num_frames) { - // Zero head inputs accumulator (first layer array) - this->_head_inputs.setZero(); + // Zero head inputs accumulator (first layer array). Only the first num_frames columns are ever + // read this call, so zeroing the whole maxBufferSize-wide buffer is wasted work when the host + // processes blocks smaller than the maximum it reserved. + this->_head_inputs.leftCols(num_frames).setZero(); ProcessInner(layer_inputs, condition, num_frames); } @@ -776,12 +778,12 @@ void nam::wavenet::WaveNet::process(NAM_SAMPLE** input, NAM_SAMPLE** output, con if (this->_post_stack_head != nullptr) { assert(final_head_outputs.rows() == this->_post_stack_head->in_channels()); - const int head_in = this->_post_stack_head->in_channels(); - for (int ch = 0; ch < head_in; ch++) - { - for (int s = 0; s < num_frames; s++) - this->_scaled_head_scratch(ch, s) = this->_head_scale * final_head_outputs(ch, s); - } + // _scaled_head_scratch is sized (in_channels, maxBufferSize), and the assert above pins + // final_head_outputs to the same row count, so this is a straight scaled copy of the block. + // Expressed as one Eigen expression rather than a nested loop: the manual loop walked the + // column-major matrix with a row-major access pattern, striding by in_channels per step. + this->_scaled_head_scratch.leftCols(num_frames).noalias() = + this->_head_scale * final_head_outputs.leftCols(num_frames); this->_post_stack_head->process(this->_scaled_head_scratch, num_frames); const Eigen::MatrixXf& head_out = this->_post_stack_head->get_last_output(); assert(head_out.rows() == out_channels); diff --git a/tools/run_tests.cpp b/tools/run_tests.cpp index 5699bf78..c0729a8f 100644 --- a/tools/run_tests.cpp +++ b/tools/run_tests.cpp @@ -3,6 +3,7 @@ #include #include "test/test_activations.cpp" +#include "test/test_activations_realtime_safe.cpp" #include "test/test_conv1d.cpp" #include "test/test_conv_1x1.cpp" #include "test/test_convnet.cpp" @@ -63,6 +64,9 @@ int main() test_activations::TestPReLU::test_wrong_size_array(); test_activations::TestPReLU::test_valid_array_size(); + test_activations_realtime_safe::test_prelu_apply_matrix_realtime_safe(); + test_activations_realtime_safe::test_prelu_apply_pointer_realtime_safe(); + // Typed ActivationConfig tests test_activations::TestTypedActivationConfig::test_simple_config(); test_activations::TestTypedActivationConfig::test_all_simple_types(); diff --git a/tools/test/test_activations_realtime_safe.cpp b/tools/test/test_activations_realtime_safe.cpp new file mode 100644 index 00000000..322aa387 --- /dev/null +++ b/tools/test/test_activations_realtime_safe.cpp @@ -0,0 +1,65 @@ +// Test to verify activation apply() overloads are real-time safe (no allocations/frees) + +#include +#include +#include +#include + +#include "NAM/activations.h" +#include "allocation_tracking.h" + +namespace test_activations_realtime_safe +{ +using namespace allocation_tracking; + +// PReLU's matrix overload is reached once per sample from the gating/blending activations, so a +// heap allocation here lands directly on the audio thread. +void test_prelu_apply_matrix_realtime_safe() +{ + const int channels = 8; + const int time_steps = 64; + + std::vector slopes; + for (int i = 0; i < channels; i++) + slopes.push_back(0.01f * static_cast(i + 1)); + + nam::activations::ActivationPReLU activation(slopes); + + Eigen::MatrixXf matrix(channels, time_steps); + matrix.setConstant(-1.0f); + + run_allocation_test_no_allocations( + nullptr, // No setup needed + [&]() { activation.apply(matrix); }, nullptr, // No teardown needed + "test_prelu_apply_matrix_realtime_safe"); + + // Each channel should have been scaled by its own slope. + for (int c = 0; c < channels; c++) + assert(std::abs(matrix(c, 0) - (-slopes[c])) < 1e-6f); +} + +// The pointer/length overload is the one used by the plain (non-gated) path. +void test_prelu_apply_pointer_realtime_safe() +{ + const int channels = 4; + const int time_steps = 32; + + std::vector slopes; + for (int i = 0; i < channels; i++) + slopes.push_back(0.05f * static_cast(i + 1)); + + nam::activations::ActivationPReLU activation(slopes); + + Eigen::MatrixXf matrix(channels, time_steps); + matrix.setConstant(-2.0f); + + run_allocation_test_no_allocations( + nullptr, // No setup needed + [&]() { activation.apply(matrix.data(), static_cast(channels) * time_steps); }, + nullptr, // No teardown needed + "test_prelu_apply_pointer_realtime_safe"); + + for (int c = 0; c < channels; c++) + assert(std::abs(matrix(c, 0) - (-2.0f * slopes[c])) < 1e-6f); +} +} // namespace test_activations_realtime_safe