Skip to content

[BUGFIX] A2 fast path: split oversized calls instead of resizing - #3

Draft
squatchlr wants to merge 1 commit into
squatchfrom
submit/a2-oversized
Draft

squatchlr wants to merge 1 commit into
squatchfrom
submit/a2-oversized

Conversation

@squatchlr

Copy link
Copy Markdown
Collaborator

Related: sdatkinson#303, #NNN (the WaveNet half)

Summary

When a call has more frames than the reservation, A2FastModel::process()
calls SetMaxBufferSize(num_frames) (a2_fast.cpp:735–736). That is
memory-safe, but it allocates on the audio thread, and SetMaxBufferSize()
re-zeroes every layer and head history (:302–337), so the model loses its
state mid-stream. After such calls, A2.nam's output differed by up to 0.08
(A2-Lite) and 0.04 (A2-Full) from the same audio processed in
reservation-sized calls.

This PR splits the call into consecutive calls of at most the reserved size,
which is what #NNN does for the generic WaveNet. A model that was never
Reset() (size 0) still sizes itself on its first call, as before, since it
has no state to lose.

Tests

test_a2_fast.cpp gets test_oversized_call_matches_consecutive_calls for
A2-Lite and A2-Full. With a 32-frame reservation and 100-frame calls, the output
must equal that of 32/32/32/4-frame calls exactly, and no call may allocate. On
main it fails on the first call:
A2FastModel<3>::process oversized - Expected 0 allocations … Got 58 allocations.
One stale comment in test_process_realtime_safe, which said the grow path
"would legitimately reallocate", now points at the new test.

Verification

  • AppleClang 21: CI's steps pass (Debug, and Debug with
    NAM_USE_INLINE_GEMM). run_tests passes with -DNAM_ENABLE_A2_FAST=OFF
    (the test compiles out) and under ASan+UBSan.
  • Linux: clang 18.1.3 on x86-64, from the same Ubuntu 24.04 packages CI installs, with
    libstdc++ and with -DNAM_USE_LIBCXX_LINUX=ON: CI's steps pass in both Debug
    builds. run_tests also passes with -DNAM_ENABLE_A2_FAST=OFF.
  • GCC 14.2 (Raspberry Pi 5): run_tests passes.
  • Output within the reservation is unchanged. The same 36 renders as
    #NNN are byte-identical to main.

Compatibility

Hosts that stay within their reservation see no change. Hosts that exceed it
now keep the model's state and no longer allocate on the audio thread.

Not covered

A2FastModel::process() handled a call with more frames than the maximum
buffer size by calling SetMaxBufferSize(). That is memory-safe, but it
allocates on the audio thread, and SetMaxBufferSize() reinitialises
every history, so the model's state is dropped mid-stream: after such
calls, example_models/A2.nam's output differed by up to 0.1 from what
the host would get by making calls of the maximum buffer size itself.

Process such a call as consecutive calls of at most the maximum buffer
size instead. A model that was never Reset() still sizes itself for
its first call, as before; it has no state to keep.

The new test processes 100-frame calls with a 32-frame maximum buffer
size, for A2-Lite and A2-Full, and requires the output to equal that of
32/32/32/4-frame calls, with no allocation. Without the fix it fails on
the first call with 58 allocations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant