Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e7ef9be
Improve Conv1D class to manage its own ring buffer
sdatkinson Jan 13, 2026
55c06ca
Fix RingBuffer to handle max lookback and add comprehensive tests
sdatkinson Jan 13, 2026
3eaccc7
Rename 'receptive field' to 'max lookback' in RingBuffer
sdatkinson Jan 13, 2026
6589535
Remove trailing whitespace (formatting cleanup)
sdatkinson Jan 13, 2026
e222741
Move RingBuffer and Conv1D to separate source and header files
sdatkinson Jan 13, 2026
3ea26af
Replace GetCapacity() with GetMaxBufferSize() in RingBuffer
sdatkinson Jan 13, 2026
d3f9180
Add assertions in RingBuffer::Rewind() to prevent aliasing
sdatkinson Jan 13, 2026
edbe0a5
Rename Conv1D::get_output to GetOutput
sdatkinson Jan 13, 2026
83b2e17
Remove _total_written tracking and GetReadPos() from RingBuffer
sdatkinson Jan 13, 2026
867462b
Refactor LayerArray::Process() to remove head_outputs parameter and a…
sdatkinson Jan 13, 2026
5b764e7
Refactor WaveNet LayerArray and remove _DilatedConv wrapper
sdatkinson Jan 13, 2026
ad166a2
Add comprehensive WaveNet tests organized by component
sdatkinson Jan 13, 2026
765620f
Refactor Conv1D and RingBuffer API, improve tests
sdatkinson Jan 13, 2026
b8cfca2
Complete ConvNet refactoring to use Conv1D ring buffer API
sdatkinson Jan 13, 2026
4f24fd2
Fix Eigen Block resize error in wavenet Layer Process
sdatkinson Jan 13, 2026
196e72a
Fix ConvNet test weight counts
sdatkinson Jan 13, 2026
e2be7f8
Add ConvNetBlock buffer management methods
sdatkinson Jan 13, 2026
6c4d009
Remove unneeded includes
sdatkinson Jan 13, 2026
a54cb2e
Remove unused _head_arrays from WaveNet class
sdatkinson Jan 13, 2026
91ce7a1
Remove unused code from WaveNet class
sdatkinson Jan 13, 2026
045adea
Optimize matrix operations and fix build warnings
sdatkinson Jan 13, 2026
7f45a7d
Add real-time safety test for WaveNet process() method
sdatkinson Jan 14, 2026
a1902f3
Add real-time safety tests for Conv1D, Layer, and LayerArray
sdatkinson Jan 14, 2026
078e043
Refine real-time tests to use full buffers and document RingBuffer usage
sdatkinson Jan 14, 2026
91f6764
Pass full buffers between WaveNet layers for real-time safety
sdatkinson Jan 14, 2026
03a4f73
Remove num_frames parameter from output getters, return full buffers
sdatkinson Jan 14, 2026
1d6f0ab
Untrack some files that were accidentally added
sdatkinson Jan 14, 2026
0892771
Remove accidentally-tracked files
sdatkinson Jan 14, 2026
85ce55d
Merge branch 'main' into 145-conv
sdatkinson Jan 14, 2026
127a8cd
Merge main into 145-conv
sdatkinson Jan 14, 2026
43a1fbb
Add missing cassert include to activations.h
sdatkinson Jan 14, 2026
7cb807f
Merge branch 'main' into 145-conv
sdatkinson Jan 14, 2026
63af57c
Fix missing include
sdatkinson Jan 14, 2026
1508fe7
Merge branch 'fix' into 145-conv
sdatkinson Jan 14, 2026
8877c21
Add --branch flag to benchmark_compare.sh to compare against differen…
sdatkinson Jan 14, 2026
5a02489
Merge branch 'fix' into 145-conv
sdatkinson Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@
*.exe
*.out
*.app

.vscode/
1 change: 1 addition & 0 deletions NAM/activations.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <cassert>
#include <string>
#include <cmath> // expf
#include <unordered_map>
Expand Down
132 changes: 132 additions & 0 deletions NAM/conv1d.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#include "conv1d.h"

namespace nam
{
// Conv1D =====================================================================

void Conv1D::set_weights_(std::vector<float>::iterator& weights)
{
if (this->_weight.size() > 0)
{
const long out_channels = this->_weight[0].rows();
const long in_channels = this->_weight[0].cols();
// Crazy ordering because that's how it gets flattened.
for (auto i = 0; i < out_channels; i++)
for (auto j = 0; j < in_channels; j++)
for (size_t k = 0; k < this->_weight.size(); k++)
this->_weight[k](i, j) = *(weights++);
}
for (long i = 0; i < this->_bias.size(); i++)
this->_bias(i) = *(weights++);
}

void Conv1D::set_size_(const int in_channels, const int out_channels, const int kernel_size, const bool do_bias,
const int _dilation)
{
this->_weight.resize(kernel_size);
for (size_t i = 0; i < this->_weight.size(); i++)
this->_weight[i].resize(out_channels,
in_channels); // y = Ax, input array (C,L)
if (do_bias)
this->_bias.resize(out_channels);
else
this->_bias.resize(0);
this->_dilation = _dilation;
}

void Conv1D::set_size_and_weights_(const int in_channels, const int out_channels, const int kernel_size,
const int _dilation, const bool do_bias, std::vector<float>::iterator& weights)
{
this->set_size_(in_channels, out_channels, kernel_size, do_bias, _dilation);
this->set_weights_(weights);
}

void Conv1D::SetMaxBufferSize(const int maxBufferSize)
{
_max_buffer_size = maxBufferSize;

// Calculate receptive field (maximum lookback needed)
const long kernel_size = get_kernel_size();
const long dilation = get_dilation();
const long receptive_field = kernel_size > 0 ? (kernel_size - 1) * dilation : 0;

const long in_channels = get_in_channels();

// Initialize input ring buffer
// Set max lookback before Reset so that Reset() can use it to calculate storage size
// Reset() will calculate storage size as: 2 * max_lookback + max_buffer_size
_input_buffer.SetMaxLookback(receptive_field);
_input_buffer.Reset(in_channels, maxBufferSize);

// Pre-allocate output matrix
const long out_channels = get_out_channels();
_output.resize(out_channels, maxBufferSize);
_output.setZero();
}


void Conv1D::Process(const Eigen::MatrixXf& input, const int num_frames)
{
// Write input to ring buffer
_input_buffer.Write(input, num_frames);

// Zero output before processing
_output.leftCols(num_frames).setZero();

// Process from ring buffer with dilation lookback
// After Write(), data is at positions [_write_pos, _write_pos+num_frames-1]
// For kernel tap k with offset, we need to read from _write_pos + offset
// The offset is negative (looking back), so _write_pos + offset reads from earlier positions
// The original process_() reads: input.middleCols(i_start + offset, ncols)
// where i_start is the current position and offset is negative for lookback
for (size_t k = 0; k < this->_weight.size(); k++)
{
const long offset = this->_dilation * (k + 1 - (long)this->_weight.size());
// Offset is negative (looking back)
// Read from position: _write_pos + offset
// Since offset is negative, we compute lookback = -offset to read from _write_pos - lookback
const long lookback = -offset;

// Read num_frames starting from write_pos + offset (which is write_pos - lookback)
auto input_block = _input_buffer.Read(num_frames, lookback);

// Perform convolution: output += weight[k] * input_block
_output.leftCols(num_frames).noalias() += this->_weight[k] * input_block;
}

// Add bias if present
if (this->_bias.size() > 0)
{
_output.leftCols(num_frames).colwise() += this->_bias;
}

// Advance ring buffer write pointer after processing
_input_buffer.Advance(num_frames);
}

void Conv1D::process_(const Eigen::MatrixXf& input, Eigen::MatrixXf& output, const long i_start, const long ncols,
const long j_start) const
{
// This is the clever part ;)
for (size_t k = 0; k < this->_weight.size(); k++)
{
const long offset = this->_dilation * (k + 1 - this->_weight.size());
if (k == 0)
output.middleCols(j_start, ncols).noalias() = this->_weight[k] * input.middleCols(i_start + offset, ncols);
else
output.middleCols(j_start, ncols).noalias() += this->_weight[k] * input.middleCols(i_start + offset, ncols);
}
if (this->_bias.size() > 0)
{
output.middleCols(j_start, ncols).colwise() += this->_bias;
}
}

long Conv1D::get_num_weights() const
{
long num_weights = this->_bias.size();
for (size_t i = 0; i < this->_weight.size(); i++)
num_weights += this->_weight[i].size();
return num_weights;
}
} // namespace nam
59 changes: 59 additions & 0 deletions NAM/conv1d.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include <Eigen/Dense>
#include <vector>
#include "ring_buffer.h"

namespace nam
{
class Conv1D
{
public:
Conv1D() { this->_dilation = 1; };
Conv1D(const int in_channels, const int out_channels, const int kernel_size, const int bias, const int dilation)
{
set_size_(in_channels, out_channels, kernel_size, bias, dilation);
};
void set_weights_(std::vector<float>::iterator& weights);
void set_size_(const int in_channels, const int out_channels, const int kernel_size, const bool do_bias,
const int _dilation);
void set_size_and_weights_(const int in_channels, const int out_channels, const int kernel_size, const int _dilation,
const bool do_bias, std::vector<float>::iterator& weights);
// Reset the ring buffer and pre-allocate output buffer
// :param sampleRate: Unused, for interface consistency
// :param maxBufferSize: Maximum buffer size for output buffer and to size ring buffer
void SetMaxBufferSize(const int maxBufferSize);
// Get the entire internal output buffer. This is intended for internal wiring
// between layers; callers should treat the buffer as pre-allocated storage
// and only consider the first `num_frames` columns valid for a given
// processing call. Slice with .leftCols(num_frames) as needed.
Eigen::MatrixXf& GetOutput() { return _output; }
const Eigen::MatrixXf& GetOutput() const { return _output; }
// Process input and write to internal output buffer
// :param input: Input matrix (channels x num_frames)
// :param num_frames: Number of frames to process
void Process(const Eigen::MatrixXf& input, const int num_frames);
// Process from input to output (legacy method, kept for compatibility)
// Rightmost indices of input go from i_start for ncols,
// Indices on output for from j_start (to j_start + ncols - i_start)
void process_(const Eigen::MatrixXf& input, Eigen::MatrixXf& output, const long i_start, const long ncols,
const long j_start) const;
long get_in_channels() const { return this->_weight.size() > 0 ? this->_weight[0].cols() : 0; };
long get_kernel_size() const { return this->_weight.size(); };
long get_num_weights() const;
long get_out_channels() const { return this->_weight.size() > 0 ? this->_weight[0].rows() : 0; };
int get_dilation() const { return this->_dilation; };
bool has_bias() const { return this->_bias.size() > 0; };

protected:
// conv[kernel](cout, cin)
std::vector<Eigen::MatrixXf> _weight;
Eigen::VectorXf _bias;
int _dilation;

private:
RingBuffer _input_buffer; // Ring buffer for input (channels x buffer_size)
Eigen::MatrixXf _output; // Pre-allocated output buffer (out_channels x maxBufferSize)
int _max_buffer_size = 0; // Stored maxBufferSize
};
} // namespace nam
Loading