Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@

# The attention backend for every model that sets none (default native). A
# workflow's per-component "attention_backend" overrides it. Same values as
# that setting: native, flash_hub, sage_hub, _native_cudnn, ...
# that setting: native, flash_hub, sage_hub, _native_cudnn, ... Leave it
# native: it reaches every model on the server, and sage cannot serve all of
# them (Music3's head_dim-256 decoder, LTX's attention mask). Pin sage_hub per
# workflow instead - the MiniMax-H3 templates take attention_backend=sage_hub.
# DIFFUSERS_ATTN_BACKEND=native

# Refuse any pipeline or component that would load custom code from the Hub.
Expand Down
2 changes: 2 additions & 0 deletions docs/ACCELERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ A component can also pin its backend persistently instead, via `set_attention_ba

Prefer the pinned form for a compiled component - the per-call context manager switches implementations under the compiled graph and forces a recompile on every run.

Either form stays on what it names. diffusers' `set_attention_backend` also makes its backend the process-wide default, which the persistent worker would keep for every later model; dw restores the default after pinning, so an H3 `sage_hub` pin cannot reach a Music3 or LTX job that follows it. The server-wide default is `DIFFUSERS_ATTN_BACKEND` in `.env` - leave it `native`, since it reaches every model and sage cannot serve all of them.

## Attention Slicing

```json
Expand Down
6 changes: 6 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ hallucinations as lyrics.
so `triton` stays clearable after the first run (#837).
- `deploy.sh` restarts a `dw.serve` run as a loaded launchd daemon on
macOS.
- A component's `attention_backend` pin stays on that component. diffusers
also made it the process-wide default, so after an H3 run with
`attention_backend=sage_hub` the worker sent every later model through
sage, and Music3 failed with `Unsupported head_dim: 256` (LTX's attention
mask is refused the same way). Music3 and LTX templates need no pin of
their own.

### 0.11.1

Expand Down
22 changes: 21 additions & 1 deletion dw/pipeline_processors/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def configure_components(pipeline, configuration, default_device, reused_compone
logger.info(
f"Setting {component_name} attention backend: {component_attention_backend}"
)
component.set_attention_backend(component_attention_backend)
set_component_attention_backend(component, component_attention_backend)

# Compile last - the graph must capture final dtypes, adapters,
# quantization, and offload hooks
Expand All @@ -143,6 +143,26 @@ def configure_components(pipeline, configuration, default_device, reused_compone
)


def set_component_attention_backend(component, backend):
"""Pin one component's attention backend without changing anyone else's.

diffusers' set_attention_backend writes the backend onto the component's own
processors, then also makes it the process-wide active backend - the default
for every model that pins none. The persistent worker outlives the job, so an
H3 transformer pinned to sage_hub would route every later model through sage,
and models sage cannot serve (Music3's head_dim-256 audio decoder, LTX's
attention mask) crash. Restoring the active backend keeps the pin on the
component it was asked for; its processors pass their own backend explicitly.
"""
from diffusers.models.attention_dispatch import _AttentionBackendRegistry

active_backend = _AttentionBackendRegistry._active_backend
try:
component.set_attention_backend(backend)
finally:
_AttentionBackendRegistry.set_active_backend(active_backend)


def enable_tiling(component, component_name, component_configuration):
"""Turn on tiled decoding for a component whose configuration asks for it.

Expand Down
19 changes: 14 additions & 5 deletions tests/test_disk_caches.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,18 @@ def tree(root):
return sorted(str(p.relative_to(root)) for p in root.rglob("*"))


def parent_tree(tmp_path):
"""What is under tmp_path, minus the server's own workspace files: the
job history's SQLite -shm/-wal sidecars come and go with its connection,
and outputs/ is created on the server's schedule, so a traversal test
checks the parent for losses among the rest, not for equality."""
return {
p
for p in tree(tmp_path)
if not p.startswith("jobs.sqlite") and not p.startswith("outputs")
}


class TestCacheRoutes:
def test_list_reports_both_caches(self, api, tmp_path):
root, (total, count) = marked_root(tmp_path)
Expand Down Expand Up @@ -436,17 +448,14 @@ def test_traversal_name_touches_nothing(self, api, tmp_path, status):
victim.parent.mkdir()
victim.write_bytes(b"x" * 8)
before_root = tree(root)
before_parent = tree(tmp_path)
before_parent = parent_tree(tmp_path)

response = api.delete("/api/caches/..%2Fx")

assert response.status_code == status
assert tree(root) == before_root
assert victim.read_bytes() == b"x" * 8
# The server's own workspace lives under tmp_path too (outputs, the
# job history's -shm/-wal), and it adds files there on its own
# schedule, so the parent is checked for losses, not for equality
assert set(before_parent) <= set(tree(tmp_path))
assert before_parent <= parent_tree(tmp_path)


# ------------------------------------------------------- quantized entries
Expand Down
19 changes: 19 additions & 0 deletions tests/test_modular_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,25 @@ def test_attention_backend_is_pinned_on_the_component(self):

pipeline.transformer.set_attention_backend.assert_called_once_with("flash_hub")

def test_attention_backend_pin_leaves_the_process_default_alone(self):
# diffusers' set_attention_backend also sets the process-wide active
# backend, which the persistent worker keeps for every later model -
# an H3 sage_hub pin crashed Music3's head_dim-256 decoder that way
from diffusers.models.attention_dispatch import _AttentionBackendRegistry

def leaky_pin(backend):
_AttentionBackendRegistry.set_active_backend(backend)

pipeline = MagicMock()
pipeline.transformer.set_attention_backend.side_effect = leaky_pin
before = _AttentionBackendRegistry._active_backend
try:
self.configure({"transformer": {"attention_backend": "sage_hub"}}, pipeline)

assert _AttentionBackendRegistry._active_backend == before
finally:
_AttentionBackendRegistry.set_active_backend(before)

def test_component_device_override_governs_the_mps_skip(self, caplog):
# The step may pin this component to another device than the pipeline's
pipeline = MagicMock()
Expand Down
Loading