Skip to content

Fix thread safety in fast_tanh toggle - #178

Open
Ray0907 wants to merge 1 commit into
sdatkinson:mainfrom
Ray0907:fix/thread-safe-activation
Open

Fix thread safety in fast_tanh toggle#178
Ray0907 wants to merge 1 commit into
sdatkinson:mainfrom
Ray0907:fix/thread-safe-activation

Conversation

@Ray0907

@Ray0907 Ray0907 commented Jan 13, 2026

Copy link
Copy Markdown

Summary

  • Fix thread safety issue in fast_tanh toggle by using std::atomic instead of modifying shared state
  • Remove mutable global _activations map modification that could cause data races

Test plan

  • Verify fast_tanh enable/disable works correctly in single-threaded context
  • Test concurrent access from multiple threads toggling fast_tanh
  • Confirm no performance regression with atomic load in hot path

  Use atomic<bool> and runtime check instead of modifying shared map.

@sdatkinson sdatkinson left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

If you can check to make sure that this doesn't clash with the real-time safety of any of the nets, then this seems ok.

Also, some testing to cement the intent of the change would be appreciated.

Comment thread NAM/activations.cpp
{
if (_activations.find(name) == _activations.end())
// Return FastTanh when Tanh is requested and fast_tanh mode is enabled
if (name == "Tanh" && using_fast_tanh.load(std::memory_order_relaxed))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this change make the WaveNet's getting of the sigmoid activation for its gating activation no longer real-time safe?

activations::Activation::get_activation("Sigmoid")->apply(this->_z.block(channels, i, channels, 1));

@Ray0907

Ray0907 commented Jan 15, 2026

Copy link
Copy Markdown
Author

Thanks for the review!

Real-time safety:

For non-"Tanh" lookups like WaveNet's "Sigmoid":

  if (name == "Tanh" && using_fast_tanh.load(...))
  //  "Sigmoid" != "Tanh" → short-circuits, atomic load never executed

It just hits the string comparison (false), then goes straight to find() - same as before, actually one fewer map lookup since we reuse the iterator now.

For "Tanh" lookups, the atomic load is lock-free with memory_order_relaxed - compiles down to a plain memory read on x86/ARM.

One thing I did consider: _activations.at("Fasttanh") technically can throw, though "Fasttanh" is guaranteed to exist. If you'd prefer, I can cache the pointer or use find() instead to be more defensive.

Tests:

Happy to add tests - what would you like to see? Unit tests for toggle behavior, multi-threaded stress tests, or both?

dfernandes83 added a commit to dfernandes83/NeuralAmpModelerCore that referenced this pull request Aug 15, 2026
…urrent access

Local fork for Absolute Stereo NAM. Investigating upstream issue sdatkinson#303 ("Loading certain A1
models after A2 models can cause audio crackling/dropouts") led here by a different route:
nam::activations::Activation::_activations is a process-wide static std::unordered_map, mutated
by enable_fast_tanh()/disable_fast_tanh()/enable_lut()/disable_lut() and read by
get_activation() -- with no synchronization anywhere.

The downstream plugin (NeuralAmpModeler.cpp) calls enable_fast_tanh() unconditionally in its
constructor (inherited from the original upstream scaffold, PR sdatkinson#178). get_activation() runs
during model construction on this project's background staging thread (never during real-time
process() -- confirmed by checking every call site: LSTM/ConvNet/WaveNet layer constructors
only). Because this state is process-wide rather than per-instance, one plugin instance being
constructed (a write, on the main thread) can race with a *different*, already-running
instance's background staging thread reading the same map (get_activation(), mid model load) --
an ordinary multi-instance/parallel-bus scenario, not a contrived one, and previously an
unsynchronized write/read race on a std::unordered_map: undefined behavior.

Added a static std::mutex guarding every access to _activations and its *_bak backup pointers:
get_activation() (both overloads that touch the map directly), enable_fast_tanh(),
disable_fast_tanh(), enable_lut(), disable_lut(). Never taken from any real-time process() path,
so this adds no audio-thread lock.

Whether this is *the* cause of sdatkinson#303 is not established -- the reporter's own plugin code isn't
available to inspect, and their symptom is a real bug that could have several explanations, but
it's a real design pattern (unscoped global mutable activation state, exactly the kind of thing a
per-model LUT calibration bug -- "calibrate for one model, then a different model reads the same
global entry" -- would look like) worth flagging as a lead upstream, independent of whether it's
confirmed to be the exact mechanism.

Validated: full downstream plugin build (VST3+app) + complete automated test suite pass;
RealtimePathBenchmark/ModelCaptureLatencyBenchmark checksums byte-identical to baseline (adds
synchronization only, no behavior change under the single-threaded call pattern every existing
test exercises).
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.

2 participants