Skip to content

feat(plugins): out-of-tree model extensions for the sa3 family - #331

Open
ryanontheinside wants to merge 2 commits into
mainfrom
feat/plugins-pr
Open

ryanontheinside wants to merge 2 commits into
mainfrom
feat/plugins-pr

Conversation

@ryanontheinside

Copy link
Copy Markdown
Collaborator

Lets an installed Python package extend a loaded model, out of tree, without
DEMON knowing anything about what the package does.

The core never branches on an extension's concrete type. It sees a spec, a
config schema, namespaced knobs and a lifecycle object. Selection is
startup-only and operator-controlled: a browser client can never name a
plugin, a module path, a checkpoint or a config file.

Import tiers

Tier Where Stability
1 acestep.plugins Versioned by PLUGIN_API_VERSION.
2 acestep.engine.sa3_internals Documented, tested subset of vendored SA3 internals, with its own API_VERSION and a structural canary test.
0 everything else under acestep.* Internal. Importing it is a bug in the plugin.

Tier 2 exists because an extension that adds modules participating in the
transformer's computation cannot be written against an abstract API. That
coupling is real, so it gets one public, tested address rather than being
spread across a package. A vendored model bump then breaks one file with a
clear message instead of failing silently somewhere else.

Plugin API 2

decorate_conditioning receives a SourceView (latent, waveform, sample
rate) rather than a bare latent. This is a deliberately breaking change: an
API-1 runtime would bind the view where it expected a latent, so the version
gate has to reject it rather than let it run.

The waveform rides along because DEMON already holds it. An extension whose
condition derives from audio rather than from a latent would otherwise have
to decode the latent back to audio, spending real time per swap to recover
something that was in memory a moment earlier.

Notes for review

  • An installed extension forces the DiT to run eager. Prebuilt TensorRT
    plans contain only the stock graph, so running them would silently ignore
    the extension. The windowed VAE decoder keeps TensorRT.
  • Conditioning is never mutated in place; decorate_conditioning returns a
    new bundle.
  • With no plugin installed, every path is unchanged.

Tests

tests/unit: 655 passed, 4 skipped.

tests/unit/test_lora_facade_session.py has 4 failures that are
pre-existing on main and untouched by this branch (the test file's blob
is byte-identical to main's). They are fixture rot: session.py reads
pending_register and the test never sets it.

Adds a versioned plugin system so a model extension can be developed and
shipped outside this repository, plus the generic SA3 engine improvements
it depends on. Nothing here is specific to any concrete extension: the
core treats an extension's configuration, controls and metrics as opaque
and never branches on its type. With no extension selected, every path is
byte-for-byte the stock one.

Generic SA3 engine work, independently useful:

- SA3Context.diffusion_objective, and sampler selection driven by it.
  The previous code assumed every SA3 checkpoint was post-trained, which
  silently mis-samples a plain rectified-flow base.
- --sa3-base-checkpoint, so a non-catalog checkpoint can be evaluated
  without installing it into the managed models tree.
- Context cache keys cover checkpoint CONTENT identity (size, mtime), not
  just path. A file its producer rewrites in place had a stable path and
  different weights, and the cache served whichever loaded first.
- Bounds-checked argument parsing; a trailing flag raised IndexError.
- acestep/engine/sa3_internals.py: a Tier-2 contract giving the vendored
  stable_audio_3 coupling one address, with a structural canary test that
  fails in public CI when the vendored layout moves.

The plugin system (acestep/plugins/):

- Entry-point discovery only. An explicitly selected plugin that is
  missing or incompatible fails startup; an unselected broken plugin is
  isolated. Silently dropping a requested model extension would produce
  plausible, wrong output.
- Selection is startup-only and operator-controlled. That is a safety
  property (a client can never name a module, checkpoint or config path)
  and a mechanical one: the knob manifest ships once on the ready frame.
- Extension controls are plain KnobSpecs composed in SA3Backend.knob_specs
  and validated by the same coercion as core knobs. This matters because
  coerce_knob_values passes unknown names through unclamped by design, so
  a control that reached the client but not the spec map would be accepted
  from the wire unvalidated, with no error. Tests pin both directions.
- Controls must be namespaced plugin_<plugin_id>_<control>. The namespace
  is the whole anti-shadowing defense, in preference to a blocklist of
  core names that would go stale and could not enumerate the dynamically
  named knobs at all.
- Lifecycle: veto before load, fingerprint into the cache key, install
  after load, close at teardown. A partial install unwinds to fully stock
  rather than serving a half-hooked model.
- Extension knobs are float/int only, because the browser panel binds
  enum/bool by name and would render anything else disabled.

Docs in docs/PLUGINS.md.
… source

decorate_conditioning received only the encoded source latent. That is
enough for an extension whose condition IS the latent, and insufficient
for one whose condition is derived from the source audio: recovering the
audio from inside an extension costs a full decode per swap and cannot
return what the encoder discarded.

The hook now receives a SourceView carrying the latent, the pre-encode
waveform, and its sample rate. Every field is optional, because both
absences are real: a session created without a source has no latent, and
a source re-anchored from a latent DEMON did not encode has no waveform.

PLUGIN_API_VERSION goes to 2. This is deliberately breaking rather than
additive - a version-1 runtime's hook would bind the view where it
expected a latent, and DEMON would hand it conditioning that looks fine
and is wrong. The version check refuses it at load instead.

SA3Backend retains the (sample_rate, waveform) pair the create path
already built, and handle_swap_source replaces it together with the
anchor: the two halves of the view must always describe the same audio,
or an extension re-decorates the new anchor against the old waveform.
Nothing on the denoising path reads the retained waveform.

Docs get a "Conditioning on the session source" section, including the
rule that a condition must be sized from the session's own geometry
rather than a constant.
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