Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
34748e6
chore: open 0.5.0-alpha.1 on develop
dkackman Sep 25, 2026
15e5852
chore(release): R14 - CI on develop, release.sh --next, e2e finds its…
dkackman Sep 25, 2026
7d73f11
fix(release): --next reopens develop in a temporary worktree
dkackman Sep 25, 2026
cfea71f
docs: SECURITY.md - report vulnerabilities privately, not as issues
dkackman Sep 25, 2026
0fcae35
fix(templates): video-with-audio-768p ran its turbo LoRA at 16x strength
dkackman Sep 26, 2026
7b5aade
feat(loras): a null model_name switches a lora off
dkackman Sep 26, 2026
60be3f3
format
dkackman Sep 26, 2026
205a5c6
fix(App): close drawer and scroll to top on navigation
dkackman Sep 26, 2026
e45bbee
docs: Mac (MPS) support implementation plan
dkackman Sep 26, 2026
3f81a58
fix(quantization): translate SDNQ devices and drop quantized matmul o…
dkackman Sep 26, 2026
1ad5999
fix(offload): drop group-offload CUDA streams when onloading elsewhere
dkackman Sep 26, 2026
2c62ac2
fix(segment): SAM2 post_process_masks takes original sizes only
dkackman Sep 26, 2026
fd3e415
feat(mps): report real unified-memory figures and the chip name
dkackman Sep 26, 2026
72884e5
fix(vram_estimate): check against the serving device, not every CUDA …
dkackman Sep 26, 2026
5bbaa26
perf(mps): make attention slicing opt-in; it slowed UNet attention 2.4x
dkackman Sep 26, 2026
5d2d1d2
fix(mps): let torch's CPU-fallback warning through the UserWarning fi…
dkackman Sep 26, 2026
d402565
fix(templates): runnable default inputs, one repo spelling, no withdr…
dkackman Sep 26, 2026
ed7d1fe
docs: Mac behavior, dw.test's real scope, stale autocast and music lines
dkackman Sep 26, 2026
b19b372
revert(mps): keep attention slicing automatic on MPS
dkackman Sep 26, 2026
e1011c8
fix(templates): replace four dead default input URLs
dkackman Sep 26, 2026
a3a69f0
fix(segment): write the GroundingDINO query the way it scores
dkackman Sep 26, 2026
644f0dd
fix(mps): default parallel checkpoint loading off on macOS
dkackman Sep 26, 2026
cf4f492
fix(mps): run float64 RoPE in float32 on MPS
dkackman Sep 26, 2026
c914629
docs(ltx-2.5 skill): the templates run on mps now
dkackman Sep 26, 2026
0eeeb0c
refactor(tests): streamline quantization test functions for MPS compa…
dkackman Sep 26, 2026
1b1934b
fix(transcribe): ask Whisper for timestamps past 30 seconds
dkackman Sep 26, 2026
e54e592
docs(minimax-music3 skill): the music template runs on mps
dkackman Sep 26, 2026
171fae9
fix(transcribe): ask only Whisper for timestamps on long clips
dkackman Sep 26, 2026
91abe3e
Merge pull request #470 from dkackman/feat/mac-mps-support
dkackman Sep 26, 2026
dc05ab4
fix(deploy): find the server by the port it listens on
dkackman Sep 26, 2026
b9c7ec1
fix(deploy): stop by port, but judge startup by the process
dkackman Sep 26, 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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ name: CI

on:
push:
branches: [master]
# develop too: the agent loop pushes it directly, with no PR, so without
# this nothing checks its range until the release PR (R14)
branches: [master, develop]
# v + semver only (glob, so the release job re-checks it exactly)
tags: ['v[0-9]*.[0-9]*.[0-9]*']
pull_request:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ name: CodeQL

on:
push:
branches: [master]
# develop too: the agent loop pushes it directly, with no PR, so without
# this nothing checks its range until the release PR (R14)
branches: [master, develop]
pull_request:
schedule:
# Weekly, as default setup ran it
Expand Down
20 changes: 16 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ python -m dw.run workflows/templates/text-to-image.json prompt="a cat" num_image
# Validate a workflow against schema
python -m dw.validate workflows/models/z-image.json

# Basic system test (torch, diffusers import check)
# System test - downloads SD 1.5 (a few GB) and generates one image
python -m dw.test

# Interactive REPL
Expand Down Expand Up @@ -235,7 +235,7 @@ SDNQ pre-quantized models use a different pattern: `pre_load_modules` imports sd

`dw/__init__.py` handles device detection (CUDA > MPS > CPU) and platform-specific optimizations:
- **CUDA**: TF32 matmul, cuDNN benchmark, deterministic mode (configurable via settings)
- **MPS**: `PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0` (use all unified memory), autocast warnings suppressed, attention slicing enabled by default
- **MPS**: `PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0` (use all unified memory), autocast warnings suppressed; attention slicing automatic (faster for SD 1.5's head dims, 2.4x slower for SDXL's - `disable_attention_slicing`)
- **CPU**: Warning displayed

Detection is overridden by the `DW_DEVICE` environment variable (single run) or the `device` setting (standing), either of which can name a specific accelerator such as `cuda:1`. Device placement is explicit throughout — no default torch device is set, since that would build models directly in VRAM and defeat offloading. Compare backends with `get_device_type()` rather than `== "cuda"`, which a device like `cuda:1` would fail.
Expand All @@ -244,6 +244,8 @@ A step can override the device it runs on: `device` in a pipeline `configuration

Every device a workflow names passes through `resolve_device()`, which translates a backend this machine does not have into the one it does and warns — a `cuda` workflow runs on a Mac and an `mps` one runs on a CUDA box. Only the backend is translated: an index survives when the backend matches (`cuda:1` on a single-GPU CUDA box stays a genuine error) and is dropped when it does not. `cpu` is never rewritten, since pinning a step to the CPU is how a GPU-specific problem gets ruled out. Translation happens before anything reads the backend, so the MPS accommodations (the sequential-offload downgrade, attention slicing, the compile skip) fire for a translated device too.

The same translation reaches the settings that carry a device or a CUDA-only feature, so a template written on the CUDA box runs unchanged on a Mac and a caller (an MCP agent included) never has to know the backend: SDNQ `quantization_device`/`return_device` go through `resolve_device()`, and `use_quantized_matmul(_conv)` is turned off on MPS, where it falls back to `torch._int_mm`, ~500x slower (`portable_quantization_arguments`, `config_objects.py`). Group-offload `use_stream`/`record_stream` are dropped when the onload device is not CUDA/XPU. `vram_estimate` checks against the serving device's own `cost` entries, else its `device_capacity_gb()` (Metal's recommended working set on a Mac), else every entry. `device_memory_stats()` reports real unified-memory figures on MPS. Each adaptation logs a warning, and torch's MPS CPU-fallback warning is let through the blanket `UserWarning` filter. `HF_ENABLE_PARALLEL_LOADING` defaults to off on macOS (`_parallel_loading_default`): diffusers' per-shard loader threads copying onto MPS at once segfaulted the LTX-2.5 SDNQ transformer load; it is chosen by platform because diffusers reads it at import, before dw can ask torch for a device.

A `components` entry can additionally set `residency: "on_demand"`, which rests the component on the CPU and wraps its `forward`/`encode`/`decode` to move it to the device around each call (`apply_on_demand_placement` in `pipeline.py`). The wrappers use `functools.wraps` because callers introspect the signature — MiniMax H3's denoiser picks its arguments from `signature(transformer.forward)`. It is mutually exclusive with `group_offload` on the same component, and like `group_offload` it suppresses the wholesale `pipeline.to(device)` at load.

Settings in `~/.diffusers_helper/settings.json`: `device`, `enable_tf32`, `cudnn_benchmark`, `cudnn_deterministic`, `log_level`, `log_filename`.
Expand Down Expand Up @@ -321,7 +323,7 @@ same reason - default setup cannot load a pack.
- **Cartesian product explosion** — multiple `previous_result` references multiply: 4 images × 3 masks = 12 iterations
- **Component sharing requires exact key matching** between `shared_components` and `reused_components`
- **Built-in workflows** need explicit argument mapping: `"prompt": "variable:prompt"`
- **MPS differences from CUDA**: no autocast, no bitsandbytes, no flash_attn, no triton, no torch.compile. Model offloading has less benefit on unified memory, and `"offload": "sequential"` is downgraded to `"model"` with a warning there (`place_component`) — per-submodule streaming hands back no residency when the CPU and the accelerator share one pool. `exclude_from_cpu_offload` is sequential-only and does not survive the downgrade.
- **MPS differences from CUDA**: no bitsandbytes, no flash_attn, no triton, no torch.compile (`torch.autocast("mps")` works on torch 2.14, but dw does not use it). Model offloading has less benefit on unified memory, and `"offload": "sequential"` is downgraded to `"model"` with a warning there (`place_component`) — per-submodule streaming hands back no residency when the CPU and the accelerator share one pool. `exclude_from_cpu_offload` is sequential-only and does not survive the downgrade.
- **`{}`-escaped strings** in JSON arguments: `"{nf4}"` stays as string `"nf4"`, without braces it would try to load as a type
- **A stored prompt's `text` may not begin with a reference prefix** (`variable:`, `previous_result:`, `constant:`, `asset:`, `output:`, `prompt:`) — the engine rejects it to prevent double resolution or iteration expansion
- **Audio+video muxing**: pipelines that generate audio alongside video (LTX-2) have the two muxed into one `video/mp4` file with PyAV in `result.py`
Expand Down Expand Up @@ -533,6 +535,16 @@ same reason - default setup cannot load a pack.
longer names it was replaced on purpose, and its record carries
`overridden_by` and drops the diagnosis. A variable no step reads is not
how the step was reached, so that case keeps the old wording
- **A null `model_name` switches a lora off** — a template's `loras` list
is fixed JSON, so the way a caller drops its adapter is to pass the
variable behind `model_name` as `null` (H3: `lora_model_name`).
`load_loras` skips the entry, `active_loras` (`dw/pipeline_processors/pipeline.py`)
keeps placement from deferring for it, and `adapter_warnings`/`warn_adapters`
(`dw/adapter_compatibility.py`, kind `lora_disabled`) say so at the path
the caller wrote, since a turbo lora's step count and shift no longer fit.
Before, nulling the variables validated clean, loaded the pipeline for
minutes and then died on `float(None)`. A null `scale`/`adapter_name`
takes its default
- **A deliverable with no audio headroom warns** — a track at or above
−0.5 dBFS is written anyway and said out loud (`warn_without_headroom`,
`dw/result.py`, kind `audio_no_headroom`), for both a saved audio file and
Expand Down Expand Up @@ -563,7 +575,7 @@ same reason - default setup cannot load a pack.
stops the next instance. The half that fixes this one: every template
whose deliverable ends in a `pair_audio` mux (`music-video`,
`assemble-and-score`, `dissolve-between-shots`) normalizes to **-3 dBFS**;
`music`, an mp3, keeps -1
`music`, an mp3, normalizes to -3 as well (#362)
- **A variable's bound is declared by the author, checked three times** — a
model's own rule about a value (H3's `num_frames` is `17 * n + 5` from 124
to 345) is a property of the model, so it lives in the workflow rather than
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ source ./activate
.\venv\scripts\activate
```

`python -m dw.test` confirms torch and diffusers import and shows which
accelerator was found.
`python -m dw.test` runs a small built-in workflow end to end - it downloads
SD 1.5 (a few GB) and generates one image on whichever accelerator was found.

**2. Start the engine.** Leave it running; everything else talks to it.

Expand Down
25 changes: 25 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Security policy

## Reporting a vulnerability

Please don't open a public issue for a security problem: the tracker is
public, and an issue publishes the way in before a fix exists.

Report it privately instead, through GitHub's
[Report a vulnerability](https://github.com/dkackman/diffusers-workflow/security/advisories/new)
form (the repository's **Security** tab, then **Advisories**). Only the
maintainer sees it. Include what you ran, what happened, and the version
(`dw --version`, or `get_server_info` over MCP).

Examples of what counts:
- a workflow that runs code or loads a class outside what an untrusted
workflow may;
- a path, URL or reference that reads or writes outside the directories the
server works in;
- a route or tool that discloses a secret, a server path or whether a file
exists;
- a destructive call that runs without its acknowledgement.

## Supported versions

Fixes go into the next release. The latest release is the one supported.
14 changes: 9 additions & 5 deletions docs/ACCELERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Prefer the pinned form for a compiled component - the per-call context manager s
}
```

Processes attention in slices to reduce memory at some cost to speed. Enabled automatically on MPS (unified memory benefits from slicing) unless `disable_attention_slicing` is set. Modular pipelines have no `enable_attention_slicing()` method - the setting is silently skipped rather than failing when the pipeline doesn't support it.
Processes attention in slices to reduce memory at some cost to speed. Automatic on MPS unless `disable_attention_slicing` is set, opt-in elsewhere. Only UNet/ControlNet models take it. On MPS, whether it helps depends on the model's attention head dim: MPS SDPA is slow at 40, 48 and 160 (slicing made SD 1.5 ~20% faster end to end) and fast at 64-128 (slicing made SDXL-shaped attention 2.4x slower - set `disable_attention_slicing` for SDXL-family models). Modular pipelines have no `enable_attention_slicing()` method - the setting is silently skipped rather than failing when the pipeline doesn't support it.

## torch.compile

Expand Down Expand Up @@ -344,9 +344,13 @@ For faster model downloads, optionally `pip install hf_transfer` and set `HF_HUB

Apple Silicon has narrower acceleration support than CUDA:

- No flash-attn, no Triton, no bitsandbytes - `attention_backend` is effectively CUDA-only; use `"native"`-family backends or leave it unset on MPS. `compile` is skipped with a warning (inductor support on MPS is immature).
- No `torch.autocast` support - autocast-related warnings from other libraries are suppressed automatically rather than surfaced.
- `enable_attention_slicing` is on by default (set `disable_attention_slicing` to turn it off).
- `float16` produces NaN values on Apple Silicon - use `float32` or `bfloat16` for `torch_dtype` instead; dw only warns, it doesn't override the dtype for you.
- No flash-attn, no Triton, no bitsandbytes - `attention_backend` is effectively CUDA-only; use `"native"`-family backends or leave it unset on MPS. `compile` is skipped with a warning (inductor support on MPS is immature). A workflow that loads a bitsandbytes checkpoint (`flux2-dev`, `multi-image-reference`) or a TorchAO int4 config (`flux-torchao`) has no Mac path yet.
- Workflows written for CUDA are adapted rather than refused: a `cuda` device becomes `mps`, SDNQ's `quantization_device` follows it and its quantized matmul is switched off (it runs through `torch._int_mm`, ~500x slower on MPS), group-offload CUDA streams are dropped, and `"offload": "sequential"` becomes `"model"`. Each change is logged as a warning.
- Attention slicing is automatic; set `disable_attention_slicing` for SDXL-family models (head dim 64), where it is 2.4x slower in attention. It helps SD 1.5.
- `float16` can produce NaN values on Apple Silicon - use `bfloat16`; dw only warns, it doesn't override the dtype for you.
- `PYTORCH_MPS_HIGH_WATERMARK_RATIO` defaults to `0.0` (use all unified memory) unless already set in the environment.
- Ops the MPS backend lacks: with `PYTORCH_ENABLE_MPS_FALLBACK=1` (set by the `fp4-fp8-for-torch-mps` package install.sh adds) they run on the CPU, and dw logs torch's warning when one does. Without it they raise.
- Memory figures are real: allocated, driver-reserved, and Metal's recommended working set as the total. `vram_estimate` checks a Mac against that total, since the catalog's `cost` entries are CUDA cards; the per-voxel figures were calibrated on CUDA.
- Cost estimates quoted on a Mac are the CUDA figures (`basis: "other_device"`) until the Mac's own runs build observed history - expect them to be optimistic.
- Parallel checkpoint loading (`HF_ENABLE_PARALLEL_LOADING`) defaults to off on macOS - concurrent loader threads copying onto MPS segfaulted a quantized load. Set it explicitly to override.
- Offloading has less benefit than on CUDA, since unified memory is already shared between CPU and GPU.
2 changes: 1 addition & 1 deletion docs/DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pyproject; install.ps1 installs it explicitly on Windows.

**Windows (CUDA):** bitsandbytes, kernels

**macOS (MPS):** fp4-fp8-for-torch-mps (FP8/FP4 dtype support for Metal), fluidtop
**macOS (MPS):** fp4-fp8-for-torch-mps (FP8/FP4 dtypes for Metal; autoloads on every torch import, overrides mm/linear/copy on the MPS dispatch key and sets PYTORCH_ENABLE_MPS_FALLBACK=1), fluidtop (a GPU/power TUI; needs sudo)

## Optional

Expand Down
12 changes: 12 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ passing. It covers more than CI: ruff over the whole repo rather than
```bash
scripts/release.sh 0.38.0
scripts/release.sh 0.38.0-alpha.1 "UI front end" # optional tag message
scripts/release.sh 0.38.0 --next 0.39.0-alpha.1 # and reopen develop
```

The script bumps `pyproject.toml` (the single source of the version —
Expand All @@ -145,6 +146,17 @@ a malformed version, a branch other than master, an existing tag, or a
dirty index (unstaged changes elsewhere are fine — the release commit
is path-limited to those two files).

`--next <version>` finishes the release on the other branch: it merges
`master` back into `develop` (a fast-forward when nothing landed there
since the release PR), sets `<version>` in the same two files, commits
`chore: open <version> on develop` and pushes `develop`, all in a temporary
worktree, so it works while `develop` is checked out elsewhere. Without it, do that by hand, or `develop` goes on reporting the
previous pre-release.

CI runs on every push to `develop` as well as `master` - the agent loop
pushes `develop` directly, with no PR - so a failure shows up against the
commit that caused it, not first on the release PR.

By hand, the equivalent is:

```bash
Expand Down
2 changes: 1 addition & 1 deletion docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ An autouse `_clear_task_model_cache` fixture clears `dw.tasks.model_cache` befor
## Quick Validation

```bash
# Verify torch and diffusers are working
# End-to-end check: downloads SD 1.5 and generates one image
python -m dw.test

# Validate a workflow against schema
Expand Down
12 changes: 9 additions & 3 deletions docs/WORKFLOW_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,12 @@ Attach one or more LoRAs to a pipeline with `loras`, a sibling of `configuration
]
```

- `model_name` — the LoRA's hub repo, required.
- `model_name` — the LoRA's hub repo, required. `null` switches the entry off: it is not
loaded and the step runs without it. A template's `loras` list is fixed JSON, so this is
how a caller drops its adapter - pass the variable behind `model_name` as `null`
(MiniMax H3: `lora_model_name`). Validation and the run both warn (`lora_disabled`).
A step-distillation (turbo) LoRA sets the step count and sigma shift the step runs at, so
raise those to the base model's schedule with it or the step runs undertrained.
- `weight_name` / `subfolder` — pick a specific weights file within the repo.
- `adapter_name` — name passed to `set_adapters()`. Defaults to the LoRA's index in the list.
- `scale` — the adapter's weight, passed to `set_adapters()`. Defaults to `1.0`.
Expand Down Expand Up @@ -1284,8 +1289,9 @@ stays alive for the steps that reuse it.
}
```

- `enable_attention_slicing` — process attention in slices to reduce memory. Enabled
automatically on MPS unless `disable_attention_slicing` is set.
- `enable_attention_slicing` — process attention in slices to reduce memory at some
cost to speed. Automatic on MPS unless `disable_attention_slicing` is set
(UNet/ControlNet models only; worth disabling for SDXL on a Mac).
- `attention_backend` — selects a diffusers attention backend (e.g. `"flash_hub"`) for
the duration of each pipeline call.
- `prompt_weighting` — enables A1111-style prompt weighting (`(word:1.5)`, `[word]`,
Expand Down
Loading
Loading