From 5198e4781548f49700e1b8806be48ca6c3bfe6f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 07:44:30 +0000 Subject: [PATCH 01/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Attach=20Ramp=20Kit=20overlay=20with=20empty=20defaul?= =?UTF-8?q?t=20MCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy Cursor overlay files onto this fork so Cloud launches do not prompt for Hub OAuth or stdio cwd. mcp.json has empty mcpServers; opt-in lives in mcp.optional.json. Does not modify upstream AGENTS.md or .ai/. Co-authored-by: ale93.moro --- .cursor/commands/scaffold.md | 56 +++++++++++++++++++++++ .cursor/commands/search-docs.md | 24 ++++++++++ .cursor/environment.json | 6 +++ .cursor/hooks.json | 11 +++++ .cursor/hooks/convention-gate.sh | 7 +++ .cursor/hooks/convention_gate.py | 61 +++++++++++++++++++++++++ .cursor/mcp-diffusers-docs.py | 69 +++++++++++++++++++++++++++++ .cursor/mcp-diffusers-docs.sh | 5 +++ .cursor/mcp.json | 3 ++ .cursor/mcp.optional.json | 15 +++++++ .cursor/rules/00-conventions.mdc | 38 ++++++++++++++++ .cursor/rules/10-scheduler.mdc | 35 +++++++++++++++ .cursor/skills/search-docs/SKILL.md | 15 +++++++ .cursorignore | 10 +++++ .gitignore | 7 +-- OVERLAY.md | 36 +++++++++++++++ 16 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 .cursor/commands/scaffold.md create mode 100644 .cursor/commands/search-docs.md create mode 100644 .cursor/environment.json create mode 100644 .cursor/hooks.json create mode 100755 .cursor/hooks/convention-gate.sh create mode 100644 .cursor/hooks/convention_gate.py create mode 100755 .cursor/mcp-diffusers-docs.py create mode 100755 .cursor/mcp-diffusers-docs.sh create mode 100644 .cursor/mcp.json create mode 100644 .cursor/mcp.optional.json create mode 100644 .cursor/rules/00-conventions.mdc create mode 100644 .cursor/rules/10-scheduler.mdc create mode 100644 .cursor/skills/search-docs/SKILL.md create mode 100644 .cursorignore create mode 100644 OVERLAY.md diff --git a/.cursor/commands/scaffold.md b/.cursor/commands/scaffold.md new file mode 100644 index 000000000..20ae8700c --- /dev/null +++ b/.cursor/commands/scaffold.md @@ -0,0 +1,56 @@ +# /scaffold — first contribution on the REAL library checkout + +Usage: `/scaffold ` + +This workspace is `huggingface/diffusers` (fork). Overlay kit is `ramp-kit/` +(cloned from alex-16moro/diffuser_agent). Do not overwrite `.ai/` or root `AGENTS.md`. + +`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`EulerLite` +→ `EulerLiteScheduler`, `scheduling_euler_lite.py`). + +## 0. Ground first + +Prefer **this checkout's source** over memory and over MCP: + +- `src/diffusers/schedulers/scheduling_euler_discrete.py` +- `src/diffusers/schedulers/scheduling_ddpm.py` + +Those files are the contract. `ramp-kit/conventions/rules.yaml` / the gate is +authoritative if anything disagrees (the philosophy doc is stale). + +Optional docs CLI (MCP is opt-in; `.cursor/mcp.json` is empty by default): + +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" +``` + +Cite provenance if you run that. Do not read or copy +`ramp-kit/examples/candidate_scheduler/` into the new files. + +## 1. Paths (library layout, not the kit stand-in) + +- Implementation: `src/diffusers/schedulers/scheduling_euler_lite.py` +- Test: `tests/schedulers/test_scheduling_euler_lite.py` + +## 2. Copy from the overlay templates + +- `ramp-kit/templates/scheduler/scheduling_TEMPLATE.py` → implementation +- Rename `TemplateScheduler` → `$2Scheduler` +- Leave `TODO(engineer)` in `step`. Do not invent Euler math. + +## 3. TEST001 + +Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS`. +The test must mention `set_timesteps` and `step`, and (TEST002) include +assertions, same-seed determinism, and shape/dtype checks. + +## 4. Gate (file-scoped — do not `--all` this library) + +```bash +python3 ramp-kit/tools/convention_check.py ramp-kit/examples/candidate_scheduler +python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_euler_lite.py +python3 -m unittest tests.schedulers.test_scheduling_euler_lite -v +``` + +Catch-early uses the kit fixture path. Stop at 0 blocking. Do not fabricate numerics. +Do not open a PR against huggingface/diffusers — PR this fork. diff --git a/.cursor/commands/search-docs.md b/.cursor/commands/search-docs.md new file mode 100644 index 000000000..9bfe92bf0 --- /dev/null +++ b/.cursor/commands/search-docs.md @@ -0,0 +1,24 @@ +# /search-docs — library docs in THIS checkout (optional) + +Usage: `/search-docs ` + +Prefer source + gate first (MCP is opt-in; `.cursor/mcp.json` is empty): + +- `src/diffusers/schedulers/scheduling_euler_discrete.py` +- `src/diffusers/schedulers/scheduling_ddpm.py` +- `ramp-kit/conventions/rules.yaml` + +Then, optional CLI (same server as MCP, no OAuth): + +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query $1 +``` + +If `$1` is empty: + +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" +``` + +Provenance should say `diffusers checkout`, not `bundled snapshot`. If the +MCP tool `search_docs` is in your list, call that instead. diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 000000000..1f7395a6a --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,6 @@ +{ + "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && pip install -r ramp-kit/requirements.txt && python3 ramp-kit/tools/docs_mcp_server.py --selftest", + "repositoryDependencies": [ + "github.com/alex-16moro/diffuser_agent" + ] +} diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 000000000..033390d68 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "hooks": { + "afterFileEdit": [ + { + "command": ".cursor/hooks/convention-gate.sh", + "timeout": 20 + } + ] + } +} diff --git a/.cursor/hooks/convention-gate.sh b/.cursor/hooks/convention-gate.sh new file mode 100755 index 000000000..63a8672bd --- /dev/null +++ b/.cursor/hooks/convention-gate.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Cursor afterFileEdit hook. Project hooks run from the repo root. +# Reads the afterFileEdit JSON payload on stdin and runs the same +# convention gate as CI (`tools/convention_check.py`). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +exec python3 "$ROOT/.cursor/hooks/convention_gate.py" diff --git a/.cursor/hooks/convention_gate.py b/.cursor/hooks/convention_gate.py new file mode 100644 index 000000000..d273d6eb6 --- /dev/null +++ b/.cursor/hooks/convention_gate.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""afterFileEdit hook — run the same convention gate CI uses. + +Cursor sends JSON on stdin: + {"file_path": "", "edits": [...]} + +This is a notification (the write already happened). Findings are printed +to stderr so the agent can fix them immediately. We always exit 0 so a +red gate never crashes the editor loop; blocking findings still show up. +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +_kit_tools = None +for _cand in (ROOT / "ramp-kit" / "tools", ROOT / "tools"): + if (_cand / "convention_check.py").is_file(): + _kit_tools = _cand + break +sys.path.insert(0, str(_kit_tools or (ROOT / "tools"))) + +from convention_check import check_file, load_rules, render_human # noqa: E402 + +# Don't fire the gate on the kit's own machinery or the teaching fixture +# (the fixture is scanned explicitly by `make demo`). +_SKIP_SUBSTR = ( + "/tools/", + "/.cursor/", + "/_templates/", + "/templates/", + "/__pycache__/", + "/ramp-kit/examples/candidate_scheduler/", +) + + +def main() -> int: + raw = sys.stdin.read() + if not raw.strip(): + return 0 + try: + payload = json.loads(raw) + except json.JSONDecodeError: + return 0 + file_path = payload.get("file_path") or "" + path = Path(file_path) + if path.suffix != ".py" or not path.is_file(): + return 0 + posix = path.as_posix() + if any(s in posix for s in _SKIP_SUBSTR): + return 0 + findings = check_file(path, load_rules()) + sys.stderr.write(render_human(findings, 1)) + sys.stderr.flush() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.cursor/mcp-diffusers-docs.py b/.cursor/mcp-diffusers-docs.py new file mode 100755 index 000000000..2b6aa4f7f --- /dev/null +++ b/.cursor/mcp-diffusers-docs.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Launch diffusers-docs MCP from any cwd. + +Cloud Agent stdio cannot set `cwd` and does not expand `${workspaceFolder}`. +This launcher walks cwd, git root, /workspace, and its own path until it finds +`tools/docs_mcp_server.py`, then execs it. Desktop and Cloud then share one +command: `python3 -u .cursor/mcp-diffusers-docs.py`. +""" +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + + +def _candidates(): + out: list[Path] = [] + here = Path(__file__).resolve().parent + out.append(here.parent) # repo root when this file lives in .cursor/ + out.append(here.parent / "ramp-kit") + cwd = Path.cwd().resolve() + out.extend([cwd, cwd / "ramp-kit", *cwd.parents]) + for key in ("CURSOR_PROJECT_DIR", "CURSOR_WORKSPACE", "WORKSPACE"): + val = os.environ.get(key) + if val: + out.append(Path(val)) + out.append(Path("/workspace")) + try: + top = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + if top: + out.append(Path(top)) + except (OSError, subprocess.CalledProcessError): + pass + seen: set[Path] = set() + for raw in out: + try: + path = raw.resolve() + except OSError: + continue + if path in seen: + continue + seen.add(path) + yield path + + +def main(argv: list[str] | None = None) -> int: + extra = [a for a in (argv if argv is not None else sys.argv[1:]) if a != "--serve"] + for root in _candidates(): + script = root / "tools" / "docs_mcp_server.py" + if script.is_file(): + os.chdir(root) + sys.argv = [str(script), "--serve", *extra] + # runpy would work; exec keeps stdin/stdout as the MCP pipes. + os.execv(sys.executable, [sys.executable, "-u", str(script), "--serve", *extra]) + sys.stderr.write( + "diffusers-docs MCP: could not find tools/docs_mcp_server.py " + f"(cwd={Path.cwd()}). From the repo root run: " + "python3 -u .cursor/mcp-diffusers-docs.py\n" + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.cursor/mcp-diffusers-docs.sh b/.cursor/mcp-diffusers-docs.sh new file mode 100755 index 000000000..852a35067 --- /dev/null +++ b/.cursor/mcp-diffusers-docs.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Thin wrapper around the Python launcher (Cloud dashboard can use either). +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" +exec python3 -u "$DIR/mcp-diffusers-docs.py" --serve "$@" diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 000000000..da39e4ffa --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,3 @@ +{ + "mcpServers": {} +} diff --git a/.cursor/mcp.optional.json b/.cursor/mcp.optional.json new file mode 100644 index 000000000..ba79ce1a4 --- /dev/null +++ b/.cursor/mcp.optional.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "diffusers-docs": { + "type": "stdio", + "command": "python3", + "args": ["-u", "/workspace/.cursor/mcp-diffusers-docs.py"], + "env": { + "PYTHONUNBUFFERED": "1" + } + }, + "huggingface": { + "url": "https://huggingface.co/mcp" + } + } +} diff --git a/.cursor/rules/00-conventions.mdc b/.cursor/rules/00-conventions.mdc new file mode 100644 index 000000000..44cef080a --- /dev/null +++ b/.cursor/rules/00-conventions.mdc @@ -0,0 +1,38 @@ +--- +description: diffusers contribution conventions enforced by the Ramp Kit. Applied to all edits. +alwaysApply: true +--- + + +# diffusers conventions (registry v0.7.0) + +You are contributing to `huggingface/diffusers`. Follow these conventions. +They mirror the project's own `.ai/` rules and are enforced by +`tools/convention_check.py` — the same gate that runs in CI, so producing +code that violates a **block** rule will fail the build. + +Use the `diffusers-docs` MCP tool (`search_docs`) to ground answers in the +library's current docs before scaffolding. If that tool is **not** in your +tool list (Cloud Agents skip project `.cursor/mcp.json` unless the launch +MCP dropdown has stdio `python3 -u .cursor/mcp-diffusers-docs.py`), run +`/search-docs `, the `search-docs` skill, or +`python3 tools/docs_mcp_server.py --query "..."` — same server. +Do not pull in code or context from outside this repo's approved +boundaries (see `.cursorignore`). + +## Blocking conventions (must satisfy) +- **REPRO001 — Randomness threads through a generator, never global RNG.** Use `randn_tensor(shape, generator=generator, device=..., dtype=...)` and thread `generator` through the call chain. +- **DEVICE001 — No hardcoded CUDA/device placement.** Respect the module's existing device: `.to(sample.device)` or accept a `device` argument. +- **DEPR001 — No deprecated/moved import paths.** Use the current import path shown in the check output. +- **MUT001 — No mutable default arguments.** Default to None and initialise inside the body. + +## Advisory conventions (should satisfy) +- **DEPR002 — Avoid deprecated kwargs (verify per release).** Prefer the newer kwarg, but confirm against your pinned diffusers version. +- **COPY001 — # Copied from markers are well-formed.** Format: `# Copied from diffusers.. with A->B`. +- **LOG001 — Library code logs, it does not print().** Use `logger = logging.get_logger(__name__)` and `logger.info(...)`. +- **DOC001 — Public methods have docstrings.** Add a Google-style docstring with Args/Returns. +- **CUST001 — No debugging leftovers committed.** Remove breakpoint()/pdb before committing. + +Component-specific rules auto-attach when you open a matching file +(e.g. a scheduler). Prefer copying an existing in-repo example with a +`# Copied from` marker over inventing a new pattern. diff --git a/.cursor/rules/10-scheduler.mdc b/.cursor/rules/10-scheduler.mdc new file mode 100644 index 000000000..96a442a25 --- /dev/null +++ b/.cursor/rules/10-scheduler.mdc @@ -0,0 +1,35 @@ +--- +description: Extra diffusers conventions for schedulers. +globs: ["src/diffusers/schedulers/scheduling_*.py", "examples/**/scheduling_*.py", "tests/schedulers/test_scheduling_*.py"] +alwaysApply: false +--- + + +# Scheduler conventions (auto-attached to scheduler files) + +### SCHED001 — Schedulers inherit SchedulerMixin and ConfigMixin (block, owner: architect) +Schedulers are swappable via ConfigMixin.from_config and serializable via SchedulerMixin. A scheduler that skips these can't be loaded, saved, or swapped like every other scheduler — it breaks the pipeline contract. +*How:* Declare `class XScheduler(SchedulerMixin, ConfigMixin):`. + +### SCHED002 — Schedulers implement the step() / set_timesteps() contract (block, owner: architect) +Every denoising loop calls set_timesteps(...) once, then step(...) each iteration. VERIFIED against current source: DDPMScheduler and EulerDiscreteScheduler both define `set_timesteps(self, num_inference_steps, device=None, ...)` and `step(self, model_output, timestep, sample, generator=None, return_dict=True)`. The philosophy doc's older "set_num_inference_steps" name is stale; the code is canonical. +*How:* Implement `set_timesteps(self, num_inference_steps, device=None)` and `step(self, model_output, timestep, sample, generator=None, return_dict=True) -> SchedulerOutput`. + +### SCHED003 — Scheduler __init__ is decorated with @register_to_config (block, owner: architect) +@register_to_config captures constructor args into the config so from_pretrained / save_pretrained round-trip correctly. Without it, config is empty and the scheduler cannot be reconstructed from the Hub. +*How:* Add `@register_to_config` directly above `def __init__`. + +### IMPORT001 — Schedulers stay self-contained (no heavy util imports) (warn, owner: architect) +The single-file policy keeps schedulers readable and swappable. Pulling in pipeline internals or deep util chains couples them to the rest of the library and violates the philosophy reviewers enforce. +*How:* Copy the small helper in-file with a `# Copied from` marker instead of importing deep internals. + +### TEST001 — New schedulers/models ship with a matching test file (block, owner: qa) +"No quality testing = no merge." A new scheduler with no test cannot move toward deployment. Presence is not enough: the test must actually exercise set_timesteps and step, otherwise a dummy file would satisfy the gate. +*How:* Generate a contract test from tests/_templates/scheduler_test.py that asserts set_timesteps and step. + +### TEST002 — Scheduler tests assert behaviour, not just presence (block, owner: qa) +Presence of a test file is not enough. A test_* function with zero assertions is a dummy. New scheduler tests must include a same-seed determinism assertion and a shape/dtype assertion so CI actually pins the contract TEST001 only names. +*How:* Each test_* function must assert. Include same-seed determinism (torch.equal) and shape plus dtype checks (see tests/_templates/scheduler_test.py). + +Reference implementation: `examples/scaffolded_scheduler/scheduling_ddpm_lite.py`. +Start from the scaffold: `/scaffold scheduler ` — do not start from a blank file. diff --git a/.cursor/skills/search-docs/SKILL.md b/.cursor/skills/search-docs/SKILL.md new file mode 100644 index 000000000..75fd9c762 --- /dev/null +++ b/.cursor/skills/search-docs/SKILL.md @@ -0,0 +1,15 @@ +--- +name: search-docs +description: Optional docs CLI for this diffusers checkout. Prefer scheduler source + the gate; MCP is opt-in. +--- + +# Search this library's docs + +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query "" +``` + +Cite provenance (`diffusers checkout` vs bundled snapshot). Prefer reading +`src/diffusers/schedulers/scheduling_euler_discrete.py` and +`scheduling_ddpm.py` first. `ramp-kit/conventions/rules.yaml` is the gate. +`.cursor/mcp.json` is empty by default. diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 000000000..5e38f392a --- /dev/null +++ b/.cursorignore @@ -0,0 +1,10 @@ +# Approved context boundary for the overlay agent. + +# Known-bad teaching fixture (in the cloned kit). Gate it; do not copy it. +ramp-kit/examples/candidate_scheduler/ + +.git/ +**/__pycache__/ +**/*.pyc +.venv/ +ramp-kit/.git/ diff --git a/.gitignore b/.gitignore index 7b156e460..befbeba85 100644 --- a/.gitignore +++ b/.gitignore @@ -125,8 +125,7 @@ dmypy.json .vs .vscode -# Cursor -.cursor +# Cursor — upstream ignored this; overlay tracks .cursor (OVERLAY.md) # Pycharm .idea @@ -185,4 +184,6 @@ wandb # AI agent generated symlinks /.agents/skills -/.claude/skills \ No newline at end of file +/.claude/skills +# Ramp Kit overlay clone (see OVERLAY.md) +ramp-kit/ diff --git a/OVERLAY.md b/OVERLAY.md new file mode 100644 index 000000000..ec43dcbd8 --- /dev/null +++ b/OVERLAY.md @@ -0,0 +1,36 @@ +# Ramp Kit overlay (customer convention-as-code) + +This checkout is a **fork of huggingface/diffusers**. The overlay lives in a +separate repo: [alex-16moro/diffuser_agent](https://github.com/alex-16moro/diffuser_agent). + +Cloud Agent install clones it to `ramp-kit/` (gitignored). Do not edit +upstream `AGENTS.md` / `.ai/` — those stay Hugging Face's. + +PRs from this overlay are **fork-demo only, not for upstream**. Keep them draft +and titled `[fork demo — not for upstream]`. Overlay clearance is the customer +gate (`ramp-kit/tools/convention_check.py` on the new file). Upstream GitHub +Actions may go red; that is expected — we do not claim Hugging Face's CI. + +## Grounding (default path — no MCP) + +1. Read `src/diffusers/schedulers/scheduling_euler_discrete.py` and + `scheduling_ddpm.py` (code beats the philosophy doc). +2. Run the gate. `ramp-kit/conventions/rules.yaml` is authoritative. +3. Optional CLI docs query (same server as MCP, no OAuth): + +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" +``` + +`.cursor/mcp.json` is **empty by default** so Cloud launches do not hit Hub +OAuth or stdio cwd failures. Opt-in servers: `.cursor/mcp.optional.json`. + +## Demo (Cloud Agent) + +1. On the **kit** first: `make demo-maintain` — one YAML edit, five surfaces. +2. Then launch on **this fork** (`alex-16moro/diffusers`), overlay branch. +3. Scaffold writes `src/diffusers/schedulers/scheduling_euler_lite.py` here. + Gate: `python3 ramp-kit/tools/convention_check.py ` (never `--all`). +4. Open the PR **on this fork**, not on huggingface/diffusers. + +Catch-early fixture: `ramp-kit/examples/candidate_scheduler` — do not copy it. From 4acd8777247c2ce056d005d016d2a2025ed913e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 09:46:39 +0000 Subject: [PATCH 02/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Scaffold=20EulerLiteScheduler=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a contract-only EulerLiteScheduler (set_timesteps + step) and matching signature/behavioral tests. Sampler math is left as TODO(engineer). Co-authored-by: alex-16moro --- .../schedulers/scheduling_euler_lite.py | 70 +++++++++++ .../schedulers/test_scheduling_euler_lite.py | 113 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 src/diffusers/schedulers/scheduling_euler_lite.py create mode 100644 tests/schedulers/test_scheduling_euler_lite.py diff --git a/src/diffusers/schedulers/scheduling_euler_lite.py b/src/diffusers/schedulers/scheduling_euler_lite.py new file mode 100644 index 000000000..6f497f343 --- /dev/null +++ b/src/diffusers/schedulers/scheduling_euler_lite.py @@ -0,0 +1,70 @@ +# TEMPLATE — the /scaffold command copies this to +# src/diffusers/schedulers/scheduling_.py and renames the class. +# It is convention-correct (passes the gate); the numerical method is left as a +# TODO because that is the engineer's actual work, not something to fabricate. +from typing import Optional, Tuple, Union + +import torch + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import SchedulerMixin, SchedulerOutput + + +class EulerLiteScheduler(SchedulerMixin, ConfigMixin): + """Contract-only Euler-style scheduler scaffold (no sampler math yet).""" + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + ): + self.num_train_timesteps = num_train_timesteps + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps) + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + self.timesteps = torch.arange(num_train_timesteps - 1, -1, -1) + self.num_inference_steps: Optional[int] = None + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device, None] = None): + """Set the discrete timesteps used for the denoising loop. + + Args: + num_inference_steps: Number of diffusion steps used at inference. + device: Device the timesteps should be moved to. + """ + self.num_inference_steps = num_inference_steps + step = self.config.num_train_timesteps // num_inference_steps + timesteps = (torch.arange(0, num_inference_steps) * step).round()[::-1].clone() + self.timesteps = timesteps.to(device) if device is not None else timesteps + + def step( + self, + model_output: torch.Tensor, + timestep: int, + sample: torch.Tensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """Predict the sample at the previous timestep. + + Args: + model_output: Direct output from the learned diffusion model. + timestep: The current discrete timestep in the diffusion chain. + sample: A current instance of a sample created by the diffusion process. + generator: A torch.Generator for reproducible sampling. + return_dict: Whether to return a SchedulerOutput or a plain tuple. + + Returns: + SchedulerOutput or tuple with the predicted previous sample. + """ + noise = torch.randn( + sample.shape, generator=generator, device=sample.device, dtype=sample.dtype + ) + # TODO(engineer): replace this placeholder with the real update rule. + prev_sample = sample - model_output + 0.0 * noise + + if not return_dict: + return (prev_sample,) + return SchedulerOutput(prev_sample=prev_sample) diff --git a/tests/schedulers/test_scheduling_euler_lite.py b/tests/schedulers/test_scheduling_euler_lite.py new file mode 100644 index 000000000..f58abb32d --- /dev/null +++ b/tests/schedulers/test_scheduling_euler_lite.py @@ -0,0 +1,113 @@ +"""TEMPLATE — copy to tests/schedulers/test_scheduling_.py and set the +two constants below. Runs with zero installs (structural + signature contract) +and adds behavioral checks when torch + diffusers are available. + +See examples/scaffolded_scheduler + tests/schedulers/test_scheduling_ddpm_lite.py +for a filled-in example. +""" +import ast +import importlib.util +import unittest +from pathlib import Path + +# ---- edit these two for your scheduler ------------------------------------- +TARGET = Path(__file__).resolve().parents[2] / "src" / "diffusers" / "schedulers" / "scheduling_euler_lite.py" +CLASS = "EulerLiteScheduler" +# ---------------------------------------------------------------------------- + + +def _class_node(): + for node in ast.walk(ast.parse(TARGET.read_text())): + if isinstance(node, ast.ClassDef) and node.name == CLASS: + return node + raise AssertionError(f"{CLASS} not found in {TARGET}") + + +def _method(node, name): + for n in node.body: + if isinstance(n, ast.FunctionDef) and n.name == name: + return n + return None + + +class TestStructuralContract(unittest.TestCase): + """Thin structural sanity — the convention gate is the primary owner.""" + + @classmethod + def setUpClass(cls): + cls.node = _class_node() + + def test_inherits_required_mixins(self): + bases = {getattr(b, "id", getattr(b, "attr", "")) for b in self.node.bases} + self.assertEqual({"SchedulerMixin", "ConfigMixin"} & bases, {"SchedulerMixin", "ConfigMixin"}) + + +class TestSignatureContract(unittest.TestCase): + """Deeper than the gate: verify the SIGNATURES, not just method presence.""" + + @classmethod + def setUpClass(cls): + cls.node = _class_node() + + def test_set_timesteps_signature(self): + m = _method(self.node, "set_timesteps") + self.assertIsNotNone(m, "set_timesteps missing") + args = [a.arg for a in m.args.args] + self.assertIn("num_inference_steps", args, + "set_timesteps must accept num_inference_steps (verified against upstream source)") + self.assertIn("device", args, "set_timesteps must accept device") + + def test_step_signature(self): + m = _method(self.node, "step") + self.assertIsNotNone(m, "step missing") + args = [a.arg for a in m.args.args] + for expected in ("model_output", "timestep", "sample", "generator"): + self.assertIn(expected, args, f"step must accept {expected} (upstream contract)") + + +class TestBehavioralContract(unittest.TestCase): + """Real behaviour — runs in CI with torch; skipped cleanly offline.""" + + def setUp(self): + if importlib.util.find_spec("torch") is None: + self.skipTest("torch not installed; behavioral contract runs in CI") + if importlib.util.find_spec("diffusers") is None: + self.skipTest("diffusers not installed; behavioral contract runs in CI") + spec = importlib.util.spec_from_file_location(CLASS, TARGET) + self.mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.mod) + self.Scheduler = getattr(self.mod, CLASS) + + def test_config_roundtrip(self): + s = self.Scheduler(num_train_timesteps=500) + rebuilt = self.Scheduler.from_config(s.config) + self.assertEqual(rebuilt.config.num_train_timesteps, 500) + + def test_timesteps_count(self): + s = self.Scheduler() + s.set_timesteps(25) + self.assertEqual(len(s.timesteps), 25) + + def test_output_type(self): + import torch + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + out = s.step(torch.ones_like(sample), 1, sample, generator=torch.Generator().manual_seed(0)) + self.assertTrue(hasattr(out, "prev_sample")) + self.assertEqual(out.prev_sample.shape, sample.shape) + self.assertEqual(out.prev_sample.dtype, sample.dtype) + + def test_same_seed_same_output(self): + import torch + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + mo = torch.ones_like(sample) + a = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample + b = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample + self.assertTrue(torch.equal(a, b)) + + +if __name__ == "__main__": + unittest.main() From b9ed91e3561b2ca4fd56e0ba09cef42523dfe292 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 09:52:01 +0000 Subject: [PATCH 03/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Implement=20EulerLiteScheduler=20Euler=20ODE=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder step() with the first-order Euler update from EulerDiscreteScheduler (epsilon prediction, leading timesteps, terminal sigma 0). Add a unit test that pins prev_sample = sample + (sigma_next - sigma) * epsilon. Co-authored-by: alex-16moro --- .../schedulers/scheduling_euler_lite.py | 101 +++++++++++++++--- .../schedulers/test_scheduling_euler_lite.py | 15 +++ 2 files changed, 99 insertions(+), 17 deletions(-) diff --git a/src/diffusers/schedulers/scheduling_euler_lite.py b/src/diffusers/schedulers/scheduling_euler_lite.py index 6f497f343..512b021ec 100644 --- a/src/diffusers/schedulers/scheduling_euler_lite.py +++ b/src/diffusers/schedulers/scheduling_euler_lite.py @@ -1,7 +1,17 @@ -# TEMPLATE — the /scaffold command copies this to -# src/diffusers/schedulers/scheduling_.py and renames the class. -# It is convention-correct (passes the gate); the numerical method is left as a -# TODO because that is the engineer's actual work, not something to fabricate. +# Copyright 2025 Katherine Crowson and The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Optional, Tuple, Union import torch @@ -11,7 +21,25 @@ class EulerLiteScheduler(SchedulerMixin, ConfigMixin): - """Contract-only Euler-style scheduler scaffold (no sampler math yet).""" + """First-order Euler ODE sampler over a linear-beta sigma schedule. + + Lite subset of [`EulerDiscreteScheduler`]: epsilon prediction, leading timesteps, and a + terminal sigma of 0. The update is the Karras Euler step from + `scheduling_euler_discrete.py` (no ancestral noise, no Karras/exponential/beta sigma + conversions). + + This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass + documentation for the generic methods the library implements for all schedulers such as + loading and saving. + + Args: + num_train_timesteps (`int`, defaults to `1000`): + The number of diffusion steps to train the model. + beta_start (`float`, defaults to `0.0001`): + The starting `beta` value of the linear schedule. + beta_end (`float`, defaults to `0.02`): + The final `beta` value of the linear schedule. + """ @register_to_config def __init__( @@ -20,24 +48,32 @@ def __init__( beta_start: float = 0.0001, beta_end: float = 0.02, ): - self.num_train_timesteps = num_train_timesteps - self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps) + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) self.alphas = 1.0 - self.betas self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + # Same sigma definition as EulerDiscreteScheduler: sqrt((1 - bar_alpha) / bar_alpha). + self.sigmas_train = ((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5 self.timesteps = torch.arange(num_train_timesteps - 1, -1, -1) + self.sigmas = torch.cat([self.sigmas_train.flip(0), torch.zeros(1)]) self.num_inference_steps: Optional[int] = None def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device, None] = None): """Set the discrete timesteps used for the denoising loop. + Leading spacing matches `EulerDiscreteScheduler` with `timestep_spacing="leading"`: + `t = (arange(num_inference_steps) * (num_train_timesteps // num_inference_steps))` + reversed. Sigmas are the training-schedule values at those indices plus a trailing 0. + Args: num_inference_steps: Number of diffusion steps used at inference. device: Device the timesteps should be moved to. """ self.num_inference_steps = num_inference_steps - step = self.config.num_train_timesteps // num_inference_steps - timesteps = (torch.arange(0, num_inference_steps) * step).round()[::-1].clone() + step_ratio = self.config.num_train_timesteps // num_inference_steps + timesteps = (torch.arange(0, num_inference_steps) * step_ratio).round().flip(0).to(torch.long) self.timesteps = timesteps.to(device) if device is not None else timesteps + sigmas = self.sigmas_train[self.timesteps.cpu()] + self.sigmas = torch.cat([sigmas, torch.zeros(1, dtype=sigmas.dtype)]) def step( self, @@ -47,23 +83,54 @@ def step( generator: Optional[torch.Generator] = None, return_dict: bool = True, ) -> Union[SchedulerOutput, Tuple]: - """Predict the sample at the previous timestep. + """Predict the sample at the previous timestep with a first-order Euler ODE step. + + For epsilon prediction this is the update in `EulerDiscreteScheduler.step` with + `s_churn=0` (no stochastic churn, so `generator` is unused): + + x0 = sample - sigma * epsilon + d = (sample - x0) / sigma + prev_sample = sample + d * (sigma_next - sigma) Args: - model_output: Direct output from the learned diffusion model. + model_output: Direct output from the learned diffusion model (epsilon). timestep: The current discrete timestep in the diffusion chain. sample: A current instance of a sample created by the diffusion process. - generator: A torch.Generator for reproducible sampling. + generator: A torch.Generator for reproducible sampling. The Euler ODE step is + deterministic; this argument exists to match the scheduler `step` contract. return_dict: Whether to return a SchedulerOutput or a plain tuple. Returns: SchedulerOutput or tuple with the predicted previous sample. """ - noise = torch.randn( - sample.shape, generator=generator, device=sample.device, dtype=sample.dtype - ) - # TODO(engineer): replace this placeholder with the real update rule. - prev_sample = sample - model_output + 0.0 * noise + schedule = self.timesteps + if isinstance(timestep, torch.Tensor): + t_val = timestep.to(device=schedule.device, dtype=schedule.dtype).reshape(-1)[0] + else: + t_val = torch.tensor(timestep, device=schedule.device, dtype=schedule.dtype) + + matches = (schedule == t_val).nonzero() + if len(matches) > 0: + step_index = int(matches[0].item()) + sigma = self.sigmas[step_index] + sigma_next = self.sigmas[step_index + 1] + else: + t_int = int(t_val.item()) + sigma = self.sigmas_train[t_int] + if t_int > 0: + sigma_next = self.sigmas_train[t_int - 1] + else: + sigma_next = torch.zeros((), dtype=sigma.dtype, device=sigma.device) + + # Upcast to avoid precision issues when computing prev_sample (EulerDiscreteScheduler). + sample = sample.to(torch.float32) + sigma = sigma.to(device=sample.device, dtype=sample.dtype) + sigma_next = sigma_next.to(device=sample.device, dtype=sample.dtype) + + pred_original_sample = sample - sigma * model_output.to(sample.dtype) + derivative = (sample - pred_original_sample) / sigma + prev_sample = sample + derivative * (sigma_next - sigma) + prev_sample = prev_sample.to(model_output.dtype) if not return_dict: return (prev_sample,) diff --git a/tests/schedulers/test_scheduling_euler_lite.py b/tests/schedulers/test_scheduling_euler_lite.py index f58abb32d..780fecb93 100644 --- a/tests/schedulers/test_scheduling_euler_lite.py +++ b/tests/schedulers/test_scheduling_euler_lite.py @@ -108,6 +108,21 @@ def test_same_seed_same_output(self): b = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample self.assertTrue(torch.equal(a, b)) + def test_euler_ode_epsilon_update(self): + import torch + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + model_output = torch.ones_like(sample) + t = int(s.timesteps[0].item()) + out = s.step(model_output, t, sample, generator=torch.Generator().manual_seed(0)).prev_sample + sigma = s.sigmas[0].to(device=sample.device, dtype=torch.float32) + sigma_next = s.sigmas[1].to(device=sample.device, dtype=torch.float32) + expected = sample.to(torch.float32) + (sigma_next - sigma) * model_output.to(torch.float32) + self.assertEqual(out.shape, sample.shape) + self.assertEqual(out.dtype, sample.dtype) + self.assertTrue(torch.allclose(out, expected.to(out.dtype))) + if __name__ == "__main__": unittest.main() From 6cb0837060790fd63aa843d6509cca0012def1ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:00:36 +0000 Subject: [PATCH 04/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Export=20EulerLiteScheduler=20from=20public=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register EulerLiteScheduler in the lazy import maps so `from diffusers import EulerLiteScheduler` works. Co-authored-by: alex-16moro --- src/diffusers/__init__.py | 2 ++ src/diffusers/schedulers/__init__.py | 2 ++ src/diffusers/utils/dummy_pt_objects.py | 15 +++++++++++++++ tests/schedulers/test_scheduling_euler_lite.py | 8 ++++++++ 4 files changed, 27 insertions(+) diff --git a/src/diffusers/__init__.py b/src/diffusers/__init__.py index 2825e9888..b7f45c83a 100644 --- a/src/diffusers/__init__.py +++ b/src/diffusers/__init__.py @@ -447,6 +447,7 @@ "EntropyBoundSchedulerOutput", "EulerAncestralDiscreteScheduler", "EulerDiscreteScheduler", + "EulerLiteScheduler", "FlowMapEulerDiscreteScheduler", "FlowMatchEulerDiscreteScheduler", "FlowMatchHeunDiscreteScheduler", @@ -1325,6 +1326,7 @@ EntropyBoundSchedulerOutput, EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, + EulerLiteScheduler, FlowMapEulerDiscreteScheduler, FlowMatchEulerDiscreteScheduler, FlowMatchHeunDiscreteScheduler, diff --git a/src/diffusers/schedulers/__init__.py b/src/diffusers/schedulers/__init__.py index c0e46ef44..b803cbcb8 100644 --- a/src/diffusers/schedulers/__init__.py +++ b/src/diffusers/schedulers/__init__.py @@ -60,6 +60,7 @@ _import_structure["scheduling_entropy_bound"] = ["EntropyBoundScheduler", "EntropyBoundSchedulerOutput"] _import_structure["scheduling_euler_ancestral_discrete"] = ["EulerAncestralDiscreteScheduler"] _import_structure["scheduling_euler_discrete"] = ["EulerDiscreteScheduler"] + _import_structure["scheduling_euler_lite"] = ["EulerLiteScheduler"] _import_structure["scheduling_flow_map_euler_discrete"] = ["FlowMapEulerDiscreteScheduler"] _import_structure["scheduling_flow_match_euler_discrete"] = ["FlowMatchEulerDiscreteScheduler"] _import_structure["scheduling_flow_match_heun_discrete"] = ["FlowMatchHeunDiscreteScheduler"] @@ -144,6 +145,7 @@ from .scheduling_entropy_bound import EntropyBoundScheduler, EntropyBoundSchedulerOutput from .scheduling_euler_ancestral_discrete import EulerAncestralDiscreteScheduler from .scheduling_euler_discrete import EulerDiscreteScheduler + from .scheduling_euler_lite import EulerLiteScheduler from .scheduling_flow_map_euler_discrete import FlowMapEulerDiscreteScheduler from .scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler from .scheduling_flow_match_heun_discrete import FlowMatchHeunDiscreteScheduler diff --git a/src/diffusers/utils/dummy_pt_objects.py b/src/diffusers/utils/dummy_pt_objects.py index 3434c6416..11e55c975 100644 --- a/src/diffusers/utils/dummy_pt_objects.py +++ b/src/diffusers/utils/dummy_pt_objects.py @@ -3471,6 +3471,21 @@ def from_pretrained(cls, *args, **kwargs): requires_backends(cls, ["torch"]) +class EulerLiteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + class FlowMapEulerDiscreteScheduler(metaclass=DummyObject): _backends = ["torch"] diff --git a/tests/schedulers/test_scheduling_euler_lite.py b/tests/schedulers/test_scheduling_euler_lite.py index 780fecb93..96d2f1a33 100644 --- a/tests/schedulers/test_scheduling_euler_lite.py +++ b/tests/schedulers/test_scheduling_euler_lite.py @@ -41,6 +41,14 @@ def test_inherits_required_mixins(self): bases = {getattr(b, "id", getattr(b, "attr", "")) for b in self.node.bases} self.assertEqual({"SchedulerMixin", "ConfigMixin"} & bases, {"SchedulerMixin", "ConfigMixin"}) + def test_public_export(self): + root = Path(__file__).resolve().parents[2] + schedulers_init = (root / "src" / "diffusers" / "schedulers" / "__init__.py").read_text() + package_init = (root / "src" / "diffusers" / "__init__.py").read_text() + self.assertIn("scheduling_euler_lite", schedulers_init) + self.assertIn("EulerLiteScheduler", schedulers_init) + self.assertIn("EulerLiteScheduler", package_init) + class TestSignatureContract(unittest.TestCase): """Deeper than the gate: verify the SIGNATURES, not just method presence.""" From 55ba3c5732821795e0168fd9e312960885bc61f2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:12:03 +0000 Subject: [PATCH 05/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Add=20file-scoped=20Ramp=20Kit=20overlay=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub check-run for changed scheduler/test files only. Does not run convention_check --all. Does not replace inherited Hugging Face workflows. Co-authored-by: ale93.moro --- .github/scripts/overlay_pr_gate.py | 102 +++++++++++++++++++++++++ .github/workflows/ramp-kit-overlay.yml | 36 +++++++++ OVERLAY.md | 8 ++ 3 files changed, 146 insertions(+) create mode 100644 .github/scripts/overlay_pr_gate.py create mode 100644 .github/workflows/ramp-kit-overlay.yml diff --git a/.github/scripts/overlay_pr_gate.py b/.github/scripts/overlay_pr_gate.py new file mode 100644 index 000000000..24cacccbe --- /dev/null +++ b/.github/scripts/overlay_pr_gate.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""File-scoped overlay gate for a diffusers-fork PR. + +Never runs convention_check --all. Changed scheduler/test files only. +Exit code = convention_check blocking count (0 = pass). No matching +files → skip 0. + + OVERLAY_GATE_FILES space-separated paths (tests / CI override) + OVERLAY_GATE_BASE git sha to diff against (PR base or previous push) + OVERLAY_GATE_HEAD git sha (default HEAD) + OVERLAY_KIT path to the ramp-kit clone (default: ./ramp-kit or this repo) +""" +from __future__ import annotations + +import os +import re +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve() +# Copied onto the fork at .github/scripts/; lives in the kit at tools/. +KIT_CANDIDATES = [] +env_kit = os.environ.get("OVERLAY_KIT") +if env_kit: + KIT_CANDIDATES.append(Path(env_kit)) +KIT_CANDIDATES.extend( + [ + Path.cwd() / "ramp-kit", + HERE.parent.parent if HERE.parent.name == "tools" else HERE.parent.parent.parent, + Path.cwd(), + ] +) + +RELEVANT = re.compile( + r"(?:^|/)(?:scheduling_[^/]+\.py|test_scheduling_[^/]+\.py)$" +) + + +def _kit() -> Path: + for cand in KIT_CANDIDATES: + check = cand / "tools" / "convention_check.py" + if check.is_file(): + return cand + sys.stderr.write( + "overlay_pr_gate: cannot find ramp-kit/tools/convention_check.py " + "(clone alex-16moro/diffuser_agent as ramp-kit/)\n" + ) + sys.exit(2) + + +def _git_changed(base: str, head: str) -> list[str]: + if not base or set(base) <= {"0"}: + return [] + proc = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=ACMR", f"{base}...{head}"], + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + sys.stderr.write(proc.stderr) + return [] + return [ln.strip() for ln in proc.stdout.splitlines() if ln.strip()] + + +def changed_files() -> list[str]: + override = os.environ.get("OVERLAY_GATE_FILES", "").strip() + if override: + return override.split() + base = os.environ.get("OVERLAY_GATE_BASE") or os.environ.get("BASE_SHA") or "" + head = os.environ.get("OVERLAY_GATE_HEAD") or "HEAD" + return _git_changed(base, head) + + +def is_relevant(path: str) -> bool: + return bool(RELEVANT.search(path.replace("\\", "/"))) + + +def main(argv=None) -> int: + if argv and "--all" in argv: + sys.stderr.write("overlay_pr_gate: refusing --all on the library fork\n") + return 2 + kit = _kit() + check = kit / "tools" / "convention_check.py" + files = [ + f + for f in changed_files() + if is_relevant(f) and Path(f).is_file() + ] + if not files: + print("ramp-kit overlay: no scheduler/test files in this change; skip") + return 0 + print("ramp-kit overlay: file-scoped gate (never --all)") + for f in files: + print(f" {f}") + proc = subprocess.run([sys.executable, str(check), *files]) + return proc.returncode + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/.github/workflows/ramp-kit-overlay.yml b/.github/workflows/ramp-kit-overlay.yml new file mode 100644 index 000000000..fa8983e09 --- /dev/null +++ b/.github/workflows/ramp-kit-overlay.yml @@ -0,0 +1,36 @@ +# Ramp Kit overlay CI for the huggingface/diffusers FORK. +# File-scoped convention_check on changed scheduler/test files only. +# Never convention_check --all. Never replace inherited Hugging Face workflows. +# Copied by tools/attach_library.py to .github/workflows/ramp-kit-overlay.yml. +name: ramp-kit-overlay +on: + pull_request: + push: + branches: [main] +jobs: + overlay-gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Clone ramp-kit (customer gate, not the library) + run: | + if [ ! -d ramp-kit/.git ]; then + git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit + fi + pip install -r ramp-kit/requirements.txt + - name: File-scoped overlay gate + env: + OVERLAY_KIT: ${{ github.workspace }}/ramp-kit + OVERLAY_GATE_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + OVERLAY_GATE_HEAD: HEAD + run: | + SCRIPT=".github/scripts/overlay_pr_gate.py" + if [ ! -f "$SCRIPT" ]; then + SCRIPT="ramp-kit/tools/overlay_pr_gate.py" + fi + python3 "$SCRIPT" diff --git a/OVERLAY.md b/OVERLAY.md index ec43dcbd8..5b0bbfbaa 100644 --- a/OVERLAY.md +++ b/OVERLAY.md @@ -34,3 +34,11 @@ OAuth or stdio cwd failures. Opt-in servers: `.cursor/mcp.optional.json`. 4. Open the PR **on this fork**, not on huggingface/diffusers. Catch-early fixture: `ramp-kit/examples/candidate_scheduler` — do not copy it. + +## Overlay CI (file-scoped) + +`.github/workflows/ramp-kit-overlay.yml` runs `convention_check` on **changed** +`scheduling_*.py` / `test_scheduling_*.py` only. It does **not** run `--all`. +It does **not** replace inherited Hugging Face workflows. GitHub runs this +only after it exists on the PR base (`main`). Kit `convention-gate.yml` stays +in the kit repo. From ca51a7101e61dcf3d13b0f624d0ad2c00b62ac28 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:14:26 +0000 Subject: [PATCH 06/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Add=20EulerLiteScheduler=20API=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register EulerLiteScheduler in the scheduler toctree, overview table, and a short autodoc page. Co-authored-by: alex-16moro --- docs/source/en/_toctree.yml | 2 ++ docs/source/en/api/schedulers/euler_lite.md | 18 ++++++++++++++++++ docs/source/en/api/schedulers/overview.md | 1 + tests/schedulers/test_scheduling_euler_lite.py | 8 ++++++++ 4 files changed, 29 insertions(+) create mode 100644 docs/source/en/api/schedulers/euler_lite.md diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index 525734a2e..c2673d6d1 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -790,6 +790,8 @@ title: EulerAncestralDiscreteScheduler - local: api/schedulers/euler title: EulerDiscreteScheduler + - local: api/schedulers/euler_lite + title: EulerLiteScheduler - local: api/schedulers/flow_map_euler_discrete title: FlowMapEulerDiscreteScheduler - local: api/schedulers/flow_match_euler_discrete diff --git a/docs/source/en/api/schedulers/euler_lite.md b/docs/source/en/api/schedulers/euler_lite.md new file mode 100644 index 000000000..79dfae112 --- /dev/null +++ b/docs/source/en/api/schedulers/euler_lite.md @@ -0,0 +1,18 @@ + + +# EulerLiteScheduler + +A lite first-order Euler ODE sampler. Same epsilon update as [`EulerDiscreteScheduler`] (`s_churn=0`, leading timesteps, terminal sigma 0) without Karras/exponential/beta sigma conversions. + +## EulerLiteScheduler +[[autodoc]] EulerLiteScheduler diff --git a/docs/source/en/api/schedulers/overview.md b/docs/source/en/api/schedulers/overview.md index a57e99a3e..330f2f7dd 100644 --- a/docs/source/en/api/schedulers/overview.md +++ b/docs/source/en/api/schedulers/overview.md @@ -38,6 +38,7 @@ Many schedulers are implemented from the [k-diffusion](https://github.com/crowso | DPM adaptive | N/A | | | DPM fast | N/A | | | Euler | [`EulerDiscreteScheduler`] | | +| Euler Lite | [`EulerLiteScheduler`] | lite Euler ODE (`s_churn=0`, leading timesteps) | | Euler a | [`EulerAncestralDiscreteScheduler`] | | | Heun | [`HeunDiscreteScheduler`] | | | LMS | [`LMSDiscreteScheduler`] | | diff --git a/tests/schedulers/test_scheduling_euler_lite.py b/tests/schedulers/test_scheduling_euler_lite.py index 96d2f1a33..a741a1a26 100644 --- a/tests/schedulers/test_scheduling_euler_lite.py +++ b/tests/schedulers/test_scheduling_euler_lite.py @@ -49,6 +49,14 @@ def test_public_export(self): self.assertIn("EulerLiteScheduler", schedulers_init) self.assertIn("EulerLiteScheduler", package_init) + def test_docs_page(self): + root = Path(__file__).resolve().parents[2] + docs_page = root / "docs" / "source" / "en" / "api" / "schedulers" / "euler_lite.md" + toctree = (root / "docs" / "source" / "en" / "_toctree.yml").read_text() + self.assertTrue(docs_page.is_file()) + self.assertIn("EulerLiteScheduler", docs_page.read_text()) + self.assertIn("api/schedulers/euler_lite", toctree) + class TestSignatureContract(unittest.TestCase): """Deeper than the gate: verify the SIGNATURES, not just method presence.""" From 70249b00b8aa689bf9207bd1596ecd432f75bdb0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:47:11 +0000 Subject: [PATCH 07/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Scope=20/scaffold=20to=20registry-checked=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy the overlay templates and stop at a file-scoped 0-blocking gate. Do not send the first contribution through Euler/DDPM source or docs search. Co-authored-by: ale93.moro --- .cursor/commands/scaffold.md | 54 +++++++++++++++----------------- .cursor/rules/00-conventions.mdc | 17 +++++----- OVERLAY.md | 40 +++++++++++++---------- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/.cursor/commands/scaffold.md b/.cursor/commands/scaffold.md index 20ae8700c..ebf3507eb 100644 --- a/.cursor/commands/scaffold.md +++ b/.cursor/commands/scaffold.md @@ -5,52 +5,50 @@ Usage: `/scaffold ` This workspace is `huggingface/diffusers` (fork). Overlay kit is `ramp-kit/` (cloned from alex-16moro/diffuser_agent). Do not overwrite `.ai/` or root `AGENTS.md`. -`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`EulerLite` -→ `EulerLiteScheduler`, `scheduling_euler_lite.py`). +`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`HeunLite` +→ `HeunLiteScheduler`, `scheduling_heun_lite.py`). -## 0. Ground first +Done is only what `ramp-kit/conventions/rules.yaml` checks. Copy the templates. +Do not start from library scheduler source and do not search docs for this PR. -Prefer **this checkout's source** over memory and over MCP: +## 0. Ground in the registry -- `src/diffusers/schedulers/scheduling_euler_discrete.py` -- `src/diffusers/schedulers/scheduling_ddpm.py` +Read `ramp-kit/conventions/rules.yaml`. Blocking ids for this contribution: +SCHED001, SCHED002, SCHED003, REPRO001, DEVICE001, DEPR001, MUT001, TEST001, +TEST002. -Those files are the contract. `ramp-kit/conventions/rules.yaml` / the gate is -authoritative if anything disagrees (the philosophy doc is stale). +The overlay templates already satisfy them. Stay inside `.cursorignore`. Do not +read or copy `ramp-kit/examples/candidate_scheduler/`. -Optional docs CLI (MCP is opt-in; `.cursor/mcp.json` is empty by default): - -```bash -python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" -``` +## 1. Paths (library layout, not the kit stand-in) -Cite provenance if you run that. Do not read or copy -`ramp-kit/examples/candidate_scheduler/` into the new files. +Convert `$2` to snake_case (`HeunLite` → `heun_lite`). The class is `$2Scheduler`. -## 1. Paths (library layout, not the kit stand-in) +- Implementation: `src/diffusers/schedulers/scheduling_.py` +- Test: `tests/schedulers/test_scheduling_.py` -- Implementation: `src/diffusers/schedulers/scheduling_euler_lite.py` -- Test: `tests/schedulers/test_scheduling_euler_lite.py` +If `scheduling_euler_lite.py` already exists, do not overwrite it — use a new `$2`. ## 2. Copy from the overlay templates - `ramp-kit/templates/scheduler/scheduling_TEMPLATE.py` → implementation - Rename `TemplateScheduler` → `$2Scheduler` -- Leave `TODO(engineer)` in `step`. Do not invent Euler math. +- Keep `TODO(engineer)` in `step()`. Do not replace the placeholder. -## 3. TEST001 +## 3. Copy the contract test (TEST001 / TEST002) -Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS`. -The test must mention `set_timesteps` and `step`, and (TEST002) include -assertions, same-seed determinism, and shape/dtype checks. +Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS` +only. Do not add files or tests the template does not already contain. ## 4. Gate (file-scoped — do not `--all` this library) ```bash -python3 ramp-kit/tools/convention_check.py ramp-kit/examples/candidate_scheduler -python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_euler_lite.py -python3 -m unittest tests.schedulers.test_scheduling_euler_lite -v +python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_.py +python3 -m unittest tests.schedulers.test_scheduling_ -v ``` -Catch-early uses the kit fixture path. Stop at 0 blocking. Do not fabricate numerics. -Do not open a PR against huggingface/diffusers — PR this fork. +Fix every **blocking** finding. Stop at 0 blocking. Behavioral skips without +torch are success — do not edit `step()` to make them pass. + +Do not open a PR against huggingface/diffusers — PR this fork, **draft, base +`main`**, those two files only. diff --git a/.cursor/rules/00-conventions.mdc b/.cursor/rules/00-conventions.mdc index 44cef080a..83d8b8f7b 100644 --- a/.cursor/rules/00-conventions.mdc +++ b/.cursor/rules/00-conventions.mdc @@ -11,12 +11,12 @@ They mirror the project's own `.ai/` rules and are enforced by `tools/convention_check.py` — the same gate that runs in CI, so producing code that violates a **block** rule will fail the build. -Use the `diffusers-docs` MCP tool (`search_docs`) to ground answers in the -library's current docs before scaffolding. If that tool is **not** in your -tool list (Cloud Agents skip project `.cursor/mcp.json` unless the launch -MCP dropdown has stdio `python3 -u .cursor/mcp-diffusers-docs.py`), run -`/search-docs `, the `search-docs` skill, or -`python3 tools/docs_mcp_server.py --query "..."` — same server. +The first contribution is what `conventions/rules.yaml` checks. Copy the +templates (`/scaffold`) and stop at 0 blocking findings from +`tools/convention_check.py` on the new files. Do not add files the +registry does not scan. Docs search (`search_docs` / `/search-docs`) is +optional and is not a deliverable. + Do not pull in code or context from outside this repo's approved boundaries (see `.cursorignore`). @@ -34,5 +34,6 @@ boundaries (see `.cursorignore`). - **CUST001 — No debugging leftovers committed.** Remove breakpoint()/pdb before committing. Component-specific rules auto-attach when you open a matching file -(e.g. a scheduler). Prefer copying an existing in-repo example with a -`# Copied from` marker over inventing a new pattern. +(e.g. a scheduler). Start from `/scaffold` templates, not from a +library scheduler file. Use a `# Copied from` marker only when you +intentionally duplicate a small helper. diff --git a/OVERLAY.md b/OVERLAY.md index 5b0bbfbaa..cada42df5 100644 --- a/OVERLAY.md +++ b/OVERLAY.md @@ -13,32 +13,38 @@ Actions may go red; that is expected — we do not claim Hugging Face's CI. ## Grounding (default path — no MCP) -1. Read `src/diffusers/schedulers/scheduling_euler_discrete.py` and - `scheduling_ddpm.py` (code beats the philosophy doc). -2. Run the gate. `ramp-kit/conventions/rules.yaml` is authoritative. -3. Optional CLI docs query (same server as MCP, no OAuth): +The first contribution is what `ramp-kit/conventions/rules.yaml` checks: -```bash -python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" -``` +1. Copy `ramp-kit/templates/scheduler/scheduling_TEMPLATE.py` and + `ramp-kit/tests/_templates/scheduler_test.py`. +2. Run the file-scoped gate on the new scheduler file. Never `--all`. + +Docs search and reading `scheduling_euler_discrete.py` / `scheduling_ddpm.py` +are how the *kit* verified the YAML. They are not part of the first PR. `.cursor/mcp.json` is **empty by default** so Cloud launches do not hit Hub OAuth or stdio cwd failures. Opt-in servers: `.cursor/mcp.optional.json`. ## Demo (Cloud Agent) -1. On the **kit** first: `make demo-maintain` — one YAML edit, five surfaces. -2. Then launch on **this fork** (`alex-16moro/diffusers`), overlay branch. -3. Scaffold writes `src/diffusers/schedulers/scheduling_euler_lite.py` here. - Gate: `python3 ramp-kit/tools/convention_check.py ` (never `--all`). -4. Open the PR **on this fork**, not on huggingface/diffusers. +1. On the **kit** first: catch-early on `examples/candidate_scheduler`, then + `make demo-maintain` if you show maintainability. +2. Then launch on **this fork** (`alex-16moro/diffusers`), branch `main`. +3. Scaffold writes `src/diffusers/schedulers/scheduling_.py` here + (HeunLite if EulerLite already exists). Gate: + `python3 ramp-kit/tools/convention_check.py ` (never `--all`). +4. Open the PR **on this fork**, not on huggingface/diffusers, **draft, base + `main`**, those two files only. Catch-early fixture: `ramp-kit/examples/candidate_scheduler` — do not copy it. ## Overlay CI (file-scoped) -`.github/workflows/ramp-kit-overlay.yml` runs `convention_check` on **changed** -`scheduling_*.py` / `test_scheduling_*.py` only. It does **not** run `--all`. -It does **not** replace inherited Hugging Face workflows. GitHub runs this -only after it exists on the PR base (`main`). Kit `convention-gate.yml` stays -in the kit repo. +Attach copies `.github/workflows/ramp-kit-overlay.yml`. It runs +`convention_check` on **changed** `scheduling_*.py` / `test_scheduling_*.py` +only. It does **not** run `--all`. It does **not** replace inherited +Hugging Face workflows. + +GitHub runs workflows that already exist on the PR **base** (usually +`main`). Land this file on `main` before you expect a check-run. The kit +workflow `convention-gate.yml` stays in the kit repo. From 0a1fdf42641366904666d9a0bcda8c4d8ef70c52 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:01:53 +0000 Subject: [PATCH 08/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Add=20HeunLiteScheduler=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract-only overlay template copy (set_timesteps + step). Sampler math left as TODO(engineer). EulerLiteScheduler already lives on main. Co-authored-by: alex-16moro --- .../schedulers/scheduling_heun_lite.py | 70 +++++++++++ tests/schedulers/test_scheduling_heun_lite.py | 113 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 src/diffusers/schedulers/scheduling_heun_lite.py create mode 100644 tests/schedulers/test_scheduling_heun_lite.py diff --git a/src/diffusers/schedulers/scheduling_heun_lite.py b/src/diffusers/schedulers/scheduling_heun_lite.py new file mode 100644 index 000000000..6c5a9e301 --- /dev/null +++ b/src/diffusers/schedulers/scheduling_heun_lite.py @@ -0,0 +1,70 @@ +# TEMPLATE — the /scaffold command copies this to +# src/diffusers/schedulers/scheduling_.py and renames the class. +# It is convention-correct (passes the gate); the numerical method is left as a +# TODO because that is the engineer's actual work, not something to fabricate. +from typing import Optional, Tuple, Union + +import torch + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import SchedulerMixin, SchedulerOutput + + +class HeunLiteScheduler(SchedulerMixin, ConfigMixin): + """One-line summary of the scheduler (replace TemplateScheduler with HeunLiteScheduler).""" + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + ): + self.num_train_timesteps = num_train_timesteps + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps) + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + self.timesteps = torch.arange(num_train_timesteps - 1, -1, -1) + self.num_inference_steps: Optional[int] = None + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device, None] = None): + """Set the discrete timesteps used for the denoising loop. + + Args: + num_inference_steps: Number of diffusion steps used at inference. + device: Device the timesteps should be moved to. + """ + self.num_inference_steps = num_inference_steps + step = self.config.num_train_timesteps // num_inference_steps + timesteps = (torch.arange(0, num_inference_steps) * step).round()[::-1].clone() + self.timesteps = timesteps.to(device) if device is not None else timesteps + + def step( + self, + model_output: torch.Tensor, + timestep: int, + sample: torch.Tensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """Predict the sample at the previous timestep. + + Args: + model_output: Direct output from the learned diffusion model. + timestep: The current discrete timestep in the diffusion chain. + sample: A current instance of a sample created by the diffusion process. + generator: A torch.Generator for reproducible sampling. + return_dict: Whether to return a SchedulerOutput or a plain tuple. + + Returns: + SchedulerOutput or tuple with the predicted previous sample. + """ + noise = torch.randn( + sample.shape, generator=generator, device=sample.device, dtype=sample.dtype + ) + # TODO(engineer): replace this placeholder with the real update rule. + prev_sample = sample - model_output + 0.0 * noise + + if not return_dict: + return (prev_sample,) + return SchedulerOutput(prev_sample=prev_sample) diff --git a/tests/schedulers/test_scheduling_heun_lite.py b/tests/schedulers/test_scheduling_heun_lite.py new file mode 100644 index 000000000..61e18fb0d --- /dev/null +++ b/tests/schedulers/test_scheduling_heun_lite.py @@ -0,0 +1,113 @@ +"""TEMPLATE — copy to tests/schedulers/test_scheduling_.py and set the +two constants below. Runs with zero installs (structural + signature contract) +and adds behavioral checks when torch + diffusers are available. + +See examples/scaffolded_scheduler + tests/schedulers/test_scheduling_ddpm_lite.py +for a filled-in example. +""" +import ast +import importlib.util +import unittest +from pathlib import Path + +# ---- edit these two for your scheduler ------------------------------------- +TARGET = Path(__file__).resolve().parents[2] / "src" / "diffusers" / "schedulers" / "scheduling_heun_lite.py" +CLASS = "HeunLiteScheduler" +# ---------------------------------------------------------------------------- + + +def _class_node(): + for node in ast.walk(ast.parse(TARGET.read_text())): + if isinstance(node, ast.ClassDef) and node.name == CLASS: + return node + raise AssertionError(f"{CLASS} not found in {TARGET}") + + +def _method(node, name): + for n in node.body: + if isinstance(n, ast.FunctionDef) and n.name == name: + return n + return None + + +class TestStructuralContract(unittest.TestCase): + """Thin structural sanity — the convention gate is the primary owner.""" + + @classmethod + def setUpClass(cls): + cls.node = _class_node() + + def test_inherits_required_mixins(self): + bases = {getattr(b, "id", getattr(b, "attr", "")) for b in self.node.bases} + self.assertEqual({"SchedulerMixin", "ConfigMixin"} & bases, {"SchedulerMixin", "ConfigMixin"}) + + +class TestSignatureContract(unittest.TestCase): + """Deeper than the gate: verify the SIGNATURES, not just method presence.""" + + @classmethod + def setUpClass(cls): + cls.node = _class_node() + + def test_set_timesteps_signature(self): + m = _method(self.node, "set_timesteps") + self.assertIsNotNone(m, "set_timesteps missing") + args = [a.arg for a in m.args.args] + self.assertIn("num_inference_steps", args, + "set_timesteps must accept num_inference_steps (verified against upstream source)") + self.assertIn("device", args, "set_timesteps must accept device") + + def test_step_signature(self): + m = _method(self.node, "step") + self.assertIsNotNone(m, "step missing") + args = [a.arg for a in m.args.args] + for expected in ("model_output", "timestep", "sample", "generator"): + self.assertIn(expected, args, f"step must accept {expected} (upstream contract)") + + +class TestBehavioralContract(unittest.TestCase): + """Real behaviour — runs in CI with torch; skipped cleanly offline.""" + + def setUp(self): + if importlib.util.find_spec("torch") is None: + self.skipTest("torch not installed; behavioral contract runs in CI") + if importlib.util.find_spec("diffusers") is None: + self.skipTest("diffusers not installed; behavioral contract runs in CI") + spec = importlib.util.spec_from_file_location(CLASS, TARGET) + self.mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.mod) + self.Scheduler = getattr(self.mod, CLASS) + + def test_config_roundtrip(self): + s = self.Scheduler(num_train_timesteps=500) + rebuilt = self.Scheduler.from_config(s.config) + self.assertEqual(rebuilt.config.num_train_timesteps, 500) + + def test_timesteps_count(self): + s = self.Scheduler() + s.set_timesteps(25) + self.assertEqual(len(s.timesteps), 25) + + def test_output_type(self): + import torch + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + out = s.step(torch.ones_like(sample), 1, sample, generator=torch.Generator().manual_seed(0)) + self.assertTrue(hasattr(out, "prev_sample")) + self.assertEqual(out.prev_sample.shape, sample.shape) + self.assertEqual(out.prev_sample.dtype, sample.dtype) + + def test_same_seed_same_output(self): + import torch + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + mo = torch.ones_like(sample) + a = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample + b = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample + self.assertTrue(torch.equal(a, b)) + + +if __name__ == "__main__": + unittest.main() From f0a1a93b718c415e0b1fed4879b3a1ff2e831683 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:09:11 +0000 Subject: [PATCH 09/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Wire=20stdio=20diffusers-docs=20MCP=20for=20Cloud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default mcp.json is search_docs over stdio, not Hub HTTP. Cloud dropdown is diffusers-docs-mcp; the launcher finds ramp-kit from any cwd. Co-authored-by: ale93.moro --- .cursor/commands/search-docs.md | 12 +++------ .cursor/environment.json | 3 ++- .cursor/mcp-diffusers-docs.py | 42 ++++++++++++++++++++++++----- .cursor/mcp.json | 11 +++++++- .cursor/mcp.optional.json | 2 +- .cursor/skills/search-docs/SKILL.md | 10 +++---- OVERLAY.md | 9 +++++-- 7 files changed, 63 insertions(+), 26 deletions(-) diff --git a/.cursor/commands/search-docs.md b/.cursor/commands/search-docs.md index 9bfe92bf0..92b618bbf 100644 --- a/.cursor/commands/search-docs.md +++ b/.cursor/commands/search-docs.md @@ -2,13 +2,8 @@ Usage: `/search-docs ` -Prefer source + gate first (MCP is opt-in; `.cursor/mcp.json` is empty): - -- `src/diffusers/schedulers/scheduling_euler_discrete.py` -- `src/diffusers/schedulers/scheduling_ddpm.py` -- `ramp-kit/conventions/rules.yaml` - -Then, optional CLI (same server as MCP, no OAuth): +Prefer `ramp-kit/conventions/rules.yaml` + the gate. MCP is stdio +`diffusers-docs` (Cloud dropdown: `diffusers-docs-mcp`). No Hub HTTP. ```bash python3 ramp-kit/tools/docs_mcp_server.py --query $1 @@ -20,5 +15,4 @@ If `$1` is empty: python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" ``` -Provenance should say `diffusers checkout`, not `bundled snapshot`. If the -MCP tool `search_docs` is in your list, call that instead. +If the MCP tool `search_docs` is in your list, call that instead. diff --git a/.cursor/environment.json b/.cursor/environment.json index 1f7395a6a..5117351f3 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,5 +1,6 @@ { - "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && pip install -r ramp-kit/requirements.txt && python3 ramp-kit/tools/docs_mcp_server.py --selftest", + "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && pip install -r ramp-kit/requirements.txt && bash ramp-kit/.cursor/install-docs-mcp.sh && python3 ramp-kit/tools/docs_mcp_server.py --selftest", + "start": "bash ramp-kit/.cursor/install-docs-mcp.sh", "repositoryDependencies": [ "github.com/alex-16moro/diffuser_agent" ] diff --git a/.cursor/mcp-diffusers-docs.py b/.cursor/mcp-diffusers-docs.py index 2b6aa4f7f..9da6c2916 100755 --- a/.cursor/mcp-diffusers-docs.py +++ b/.cursor/mcp-diffusers-docs.py @@ -2,9 +2,13 @@ """Launch diffusers-docs MCP from any cwd. Cloud Agent stdio cannot set `cwd` and does not expand `${workspaceFolder}`. -This launcher walks cwd, git root, /workspace, and its own path until it finds -`tools/docs_mcp_server.py`, then execs it. Desktop and Cloud then share one -command: `python3 -u .cursor/mcp-diffusers-docs.py`. +The dashboard command is often `python3 -u .cursor/mcp-diffusers-docs.py`, +spawned from the workspace root (e.g. `/agent`), not the git repo. This +launcher walks cwd, git root, `/agent/repos/*`, `/workspace`, and its own +path until it finds `tools/docs_mcp_server.py`, then execs it. + +Desktop and Cloud share: `python3 -u .cursor/mcp-diffusers-docs.py`. +Cloud PATH fallback after install: `diffusers-docs-mcp`. """ from __future__ import annotations @@ -14,6 +18,17 @@ from pathlib import Path +def _children(base: Path): + try: + if not base.is_dir(): + return + for child in base.iterdir(): + if child.is_dir(): + yield child + except OSError: + return + + def _candidates(): out: list[Path] = [] here = Path(__file__).resolve().parent @@ -25,7 +40,19 @@ def _candidates(): val = os.environ.get(key) if val: out.append(Path(val)) - out.append(Path("/workspace")) + out.append(Path(val) / "ramp-kit") + for base in ( + Path("/agent"), + Path("/agent/repos"), + Path("/workspace"), + Path("/workspace/repos"), + here.parent / "repos", + cwd / "repos", + ): + out.append(base) + for child in _children(base): + out.append(child) + out.append(child / "ramp-kit") try: top = subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], @@ -34,6 +61,7 @@ def _candidates(): ).strip() if top: out.append(Path(top)) + out.append(Path(top) / "ramp-kit") except (OSError, subprocess.CalledProcessError): pass seen: set[Path] = set() @@ -55,12 +83,12 @@ def main(argv: list[str] | None = None) -> int: if script.is_file(): os.chdir(root) sys.argv = [str(script), "--serve", *extra] - # runpy would work; exec keeps stdin/stdout as the MCP pipes. os.execv(sys.executable, [sys.executable, "-u", str(script), "--serve", *extra]) sys.stderr.write( "diffusers-docs MCP: could not find tools/docs_mcp_server.py " - f"(cwd={Path.cwd()}). From the repo root run: " - "python3 -u .cursor/mcp-diffusers-docs.py\n" + f"(cwd={Path.cwd()} file={Path(__file__).resolve()}). " + "Install the PATH shim: bash .cursor/install-docs-mcp.sh " + "then spawn `diffusers-docs-mcp`.\n" ) return 1 diff --git a/.cursor/mcp.json b/.cursor/mcp.json index da39e4ffa..cbf2bc837 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -1,3 +1,12 @@ { - "mcpServers": {} + "mcpServers": { + "diffusers-docs": { + "type": "stdio", + "command": "python3", + "args": ["-u", ".cursor/mcp-diffusers-docs.py"], + "env": { + "PYTHONUNBUFFERED": "1" + } + } + } } diff --git a/.cursor/mcp.optional.json b/.cursor/mcp.optional.json index ba79ce1a4..9080a649d 100644 --- a/.cursor/mcp.optional.json +++ b/.cursor/mcp.optional.json @@ -3,7 +3,7 @@ "diffusers-docs": { "type": "stdio", "command": "python3", - "args": ["-u", "/workspace/.cursor/mcp-diffusers-docs.py"], + "args": ["-u", ".cursor/mcp-diffusers-docs.py"], "env": { "PYTHONUNBUFFERED": "1" } diff --git a/.cursor/skills/search-docs/SKILL.md b/.cursor/skills/search-docs/SKILL.md index 75fd9c762..ebcd8c2e3 100644 --- a/.cursor/skills/search-docs/SKILL.md +++ b/.cursor/skills/search-docs/SKILL.md @@ -1,6 +1,6 @@ --- name: search-docs -description: Optional docs CLI for this diffusers checkout. Prefer scheduler source + the gate; MCP is opt-in. +description: Optional docs CLI for this diffusers checkout. Prefer the gate; MCP is stdio search_docs. --- # Search this library's docs @@ -9,7 +9,7 @@ description: Optional docs CLI for this diffusers checkout. Prefer scheduler sou python3 ramp-kit/tools/docs_mcp_server.py --query "" ``` -Cite provenance (`diffusers checkout` vs bundled snapshot). Prefer reading -`src/diffusers/schedulers/scheduling_euler_discrete.py` and -`scheduling_ddpm.py` first. `ramp-kit/conventions/rules.yaml` is the gate. -`.cursor/mcp.json` is empty by default. +Cite provenance (`diffusers checkout` vs bundled snapshot). +`ramp-kit/conventions/rules.yaml` is the gate. +`.cursor/mcp.json` is stdio `diffusers-docs` only (no Hub HTTP). +Cloud dropdown: `diffusers-docs-mcp`. diff --git a/OVERLAY.md b/OVERLAY.md index cada42df5..150386a4e 100644 --- a/OVERLAY.md +++ b/OVERLAY.md @@ -22,8 +22,13 @@ The first contribution is what `ramp-kit/conventions/rules.yaml` checks: Docs search and reading `scheduling_euler_discrete.py` / `scheduling_ddpm.py` are how the *kit* verified the YAML. They are not part of the first PR. -`.cursor/mcp.json` is **empty by default** so Cloud launches do not hit Hub -OAuth or stdio cwd failures. Opt-in servers: `.cursor/mcp.optional.json`. +`.cursor/mcp.json` ships **stdio `diffusers-docs` only** (`python3 -u +.cursor/mcp-diffusers-docs.py`). Do not enable Hub HTTP MCP (OAuth). Cloud +dropdown: `diffusers-docs-mcp` (PATH shim from `install-docs-mcp.sh`) or the +same python3 command. The launcher finds `ramp-kit/tools/docs_mcp_server.py` +from `/agent` as well as the repo root. + +Hub HTTP and extra servers stay in `.cursor/mcp.optional.json`. ## Demo (Cloud Agent) From f0349bd0e850c656b45c6d23b858b591f384ef78 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 17:09:26 +0000 Subject: [PATCH 10/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Point=20overlay=20/scaffold=20at=20library=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new engineer on this fork should read Hugging Face AGENTS.md/.ai/ and invoke make style / make quality / check_dummies, not the old two-files-only recipe. Attach copies overlay/scaffold.md. Does not overwrite AGENTS.md or .ai/ and does not disable inherited workflows. Co-authored-by: ale93.moro --- .cursor/commands/scaffold.md | 80 +++++++++++++++++++++++++++--------- .cursor/environment.json | 8 +++- OVERLAY.md | 36 +++++++++------- 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/.cursor/commands/scaffold.md b/.cursor/commands/scaffold.md index ebf3507eb..110ce7f7b 100644 --- a/.cursor/commands/scaffold.md +++ b/.cursor/commands/scaffold.md @@ -3,52 +3,94 @@ Usage: `/scaffold ` This workspace is `huggingface/diffusers` (fork). Overlay kit is `ramp-kit/` -(cloned from alex-16moro/diffuser_agent). Do not overwrite `.ai/` or root `AGENTS.md`. +(cloned from alex-16moro/diffuser_agent). -`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`HeunLite` -→ `HeunLiteScheduler`, `scheduling_heun_lite.py`). +Root `AGENTS.md` and `.ai/` are Hugging Face's agent guide. **Read them. Do not +overwrite them.** They are how the library itself wants code to look (`make +style`, `make quality`, `# Copied from`, self-review). This overlay adds the +first-contribution recipe and a customer file-scoped gate. It does not replace +the library guide. -Done is only what `ramp-kit/conventions/rules.yaml` checks. Copy the templates. -Do not start from library scheduler source and do not search docs for this PR. +Do not disable or guard inherited GitHub workflows. -## 0. Ground in the registry +`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`PNDMLite` +→ `PNDMLiteScheduler`, `scheduling_pndm_lite.py`). If `scheduling_.py` +already exists, pick a new unused `$2`. + +Copy the overlay templates. Do not start from library scheduler source. Keep +`TODO(engineer)` in `step()`. + +## 0. Ground in the registry **and** the library guide Read `ramp-kit/conventions/rules.yaml`. Blocking ids for this contribution: SCHED001, SCHED002, SCHED003, REPRO001, DEVICE001, DEPR001, MUT001, TEST001, TEST002. -The overlay templates already satisfy them. Stay inside `.cursorignore`. Do not -read or copy `ramp-kit/examples/candidate_scheduler/`. +Read root `AGENTS.md` (and `.ai/` only as that guide directs). The templates +already satisfy the overlay registry. Library CI (`make style` / `make quality` +/ copies / dummies) is also required before the PR. + +Stay inside `.cursorignore`. Do not read or copy +`ramp-kit/examples/candidate_scheduler/`. ## 1. Paths (library layout, not the kit stand-in) -Convert `$2` to snake_case (`HeunLite` → `heun_lite`). The class is `$2Scheduler`. +Convert `$2` to snake_case (`PNDMLite` → `pndm_lite`). The class is `$2Scheduler`. - Implementation: `src/diffusers/schedulers/scheduling_.py` - Test: `tests/schedulers/test_scheduling_.py` -If `scheduling_euler_lite.py` already exists, do not overwrite it — use a new `$2`. +Do not overwrite `scheduling_euler_lite.py` or `scheduling_heun_lite.py`. -## 2. Copy from the overlay templates +## 2. Copy from the overlay templates — replace every placeholder - `ramp-kit/templates/scheduler/scheduling_TEMPLATE.py` → implementation - Rename `TemplateScheduler` → `$2Scheduler` -- Keep `TODO(engineer)` in `step()`. Do not replace the placeholder. +- Keep `TODO(engineer)` in `step()`. Do not replace that placeholder. +- After the copy, the new files must not contain `TEMPLATE —`, `CHANGE_ME`, + `ChangeMeScheduler`, or `TemplateScheduler`. +- Add a `# Copied from` marker only when a block is actually copied. ## 3. Copy the contract test (TEST001 / TEST002) -Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS` -only. Do not add files or tests the template does not already contain. +Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS`. +Replace every other template token so the test names `$2Scheduler`. + +## 4. Register the scheduler (library public API) + +Alphabetically insert `$2Scheduler` in: + +- `src/diffusers/schedulers/__init__.py` — `_import_structure["scheduling_"]` + and the matching TYPE_CHECKING import +- `src/diffusers/__init__.py` — the torch schedulers lazy-import list and the + TYPE_CHECKING import from `.schedulers` + +Then invoke (do not hand-write dummy classes): + +```bash +python utils/check_dummies.py --fix_and_overwrite +``` -## 4. Gate (file-scoped — do not `--all` this library) +## 5. File-scoped convention gate (never `--all`) ```bash python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_.py python3 -m unittest tests.schedulers.test_scheduling_ -v ``` -Fix every **blocking** finding. Stop at 0 blocking. Behavioral skips without -torch are success — do not edit `step()` to make them pass. +Fix every **blocking** finding. Stop at 0 blocking. Do not edit `step()` math. + +## 6. Library CI — invoke until green, then open the PR + +These are the library's own checks from `AGENTS.md`. Iterate until all exit 0. +Do not reimplement them: + +```bash +make style +make quality +python utils/check_copies.py && python utils/check_dummies.py && python utils/check_repo.py +python3 -m unittest tests.schedulers.test_scheduling_ -v +``` -Do not open a PR against huggingface/diffusers — PR this fork, **draft, base -`main`**, those two files only. +Only then open a **draft** PR on this fork, **base `main`**. Never PR +huggingface/diffusers. Title prefix: `[fork demo — not for upstream]`. diff --git a/.cursor/environment.json b/.cursor/environment.json index 5117351f3..6c58945fa 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,7 +1,13 @@ { - "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && pip install -r ramp-kit/requirements.txt && bash ramp-kit/.cursor/install-docs-mcp.sh && python3 ramp-kit/tools/docs_mcp_server.py --selftest", + "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && bash ramp-kit/.cursor/cloud-install.sh", "start": "bash ramp-kit/.cursor/install-docs-mcp.sh", "repositoryDependencies": [ "github.com/alex-16moro/diffuser_agent" + ], + "mcpServerAllowlist": [ + { + "name": "diffusers-docs", + "command": "diffusers-docs-mcp" + } ] } diff --git a/OVERLAY.md b/OVERLAY.md index 150386a4e..e1bfa6f29 100644 --- a/OVERLAY.md +++ b/OVERLAY.md @@ -3,24 +3,33 @@ This checkout is a **fork of huggingface/diffusers**. The overlay lives in a separate repo: [alex-16moro/diffuser_agent](https://github.com/alex-16moro/diffuser_agent). -Cloud Agent install clones it to `ramp-kit/` (gitignored). Do not edit -upstream `AGENTS.md` / `.ai/` — those stay Hugging Face's. +Cloud Agent install clones it to `ramp-kit/` (gitignored). + +Root `AGENTS.md` and `.ai/` stay Hugging Face's agent guide. Do not overwrite +them. A new engineer's agent should follow that guide **and** this overlay so +generated code is library-CI aligned by default (`make style`, `make quality`, +copies/dummies) plus the customer file-scoped gate. PRs from this overlay are **fork-demo only, not for upstream**. Keep them draft -and titled `[fork demo — not for upstream]`. Overlay clearance is the customer -gate (`ramp-kit/tools/convention_check.py` on the new file). Upstream GitHub -Actions may go red; that is expected — we do not claim Hugging Face's CI. +and titled `[fork demo — not for upstream]`. Do not disable inherited GitHub +workflows. Jobs that need Hugging Face's private runners or size labels may +still be red on this personal fork; that is repo-hosting, not a reason to skip +`make quality`. ## Grounding (default path — no MCP) -The first contribution is what `ramp-kit/conventions/rules.yaml` checks: +The first contribution: 1. Copy `ramp-kit/templates/scheduler/scheduling_TEMPLATE.py` and - `ramp-kit/tests/_templates/scheduler_test.py`. -2. Run the file-scoped gate on the new scheduler file. Never `--all`. + `ramp-kit/tests/_templates/scheduler_test.py`. Replace every template token. + Keep `TODO(engineer)` in `step()`. +2. Register the class in `schedulers/__init__.py`, `diffusers/__init__.py`, and + dummy objects (`python utils/check_dummies.py --fix_and_overwrite`). +3. File-scoped overlay gate on the new file. Never `--all`. +4. Invoke the library's own tooling until exit 0: `make style`, `make quality`, + `python utils/check_copies.py && python utils/check_dummies.py && python utils/check_repo.py`. -Docs search and reading `scheduling_euler_discrete.py` / `scheduling_ddpm.py` -are how the *kit* verified the YAML. They are not part of the first PR. +Do not copy `ramp-kit/examples/candidate_scheduler/`. `.cursor/mcp.json` ships **stdio `diffusers-docs` only** (`python3 -u .cursor/mcp-diffusers-docs.py`). Do not enable Hub HTTP MCP (OAuth). Cloud @@ -35,11 +44,10 @@ Hub HTTP and extra servers stay in `.cursor/mcp.optional.json`. 1. On the **kit** first: catch-early on `examples/candidate_scheduler`, then `make demo-maintain` if you show maintainability. 2. Then launch on **this fork** (`alex-16moro/diffusers`), branch `main`. -3. Scaffold writes `src/diffusers/schedulers/scheduling_.py` here - (HeunLite if EulerLite already exists). Gate: - `python3 ramp-kit/tools/convention_check.py ` (never `--all`). +3. `/scaffold scheduler `. Follow `.cursor/commands/scaffold.md` + (copied from the overlay). Register + library CI until green. Never `--all`. 4. Open the PR **on this fork**, not on huggingface/diffusers, **draft, base - `main`**, those two files only. + `main`**, only after those library gates pass. Catch-early fixture: `ramp-kit/examples/candidate_scheduler` — do not copy it. From b8e0af9fc8d715e687162359efc8707efaeef514 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 17:35:29 +0000 Subject: [PATCH 11/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Empty=20the=20default=20overlay=20MCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attach the kit overlay so .cursor/mcp.json is {"mcpServers":{}}. Delete the docs MCP launchers and optional Hub JSON. Do not touch AGENTS.md or .ai/. Grounding is the library guide plus optional ramp-kit docs CLI --query. Co-authored-by: ale93.moro --- .cursor/commands/scaffold.md | 16 ++++- .cursor/commands/search-docs.md | 8 +-- .cursor/environment.json | 7 --- .cursor/mcp-diffusers-docs.py | 97 ----------------------------- .cursor/mcp-diffusers-docs.sh | 5 -- .cursor/mcp.json | 13 +--- .cursor/mcp.optional.json | 15 ----- .cursor/rules/00-conventions.mdc | 5 +- .cursor/skills/search-docs/SKILL.md | 7 +-- OVERLAY.md | 11 ++-- 10 files changed, 28 insertions(+), 156 deletions(-) delete mode 100755 .cursor/mcp-diffusers-docs.py delete mode 100755 .cursor/mcp-diffusers-docs.sh delete mode 100644 .cursor/mcp.optional.json diff --git a/.cursor/commands/scaffold.md b/.cursor/commands/scaffold.md index 110ce7f7b..e7a0f63d9 100644 --- a/.cursor/commands/scaffold.md +++ b/.cursor/commands/scaffold.md @@ -26,9 +26,19 @@ Read `ramp-kit/conventions/rules.yaml`. Blocking ids for this contribution: SCHED001, SCHED002, SCHED003, REPRO001, DEVICE001, DEPR001, MUT001, TEST001, TEST002. -Read root `AGENTS.md` (and `.ai/` only as that guide directs). The templates -already satisfy the overlay registry. Library CI (`make style` / `make quality` -/ copies / dummies) is also required before the PR. +Read root `AGENTS.md` and `.ai/` (library guide / review-rules / skills). Do +not overwrite them. Also read the reference source those files name. Optional +keyword search of this checkout's docs: + +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query "set_timesteps" +``` + +That CLI is a fallback, not a deliverable. Default `.cursor/mcp.json` has no +servers. + +The templates already satisfy the overlay registry. Library CI (`make style` / +`make quality` / copies / dummies) is also required before the PR. Stay inside `.cursorignore`. Do not read or copy `ramp-kit/examples/candidate_scheduler/`. diff --git a/.cursor/commands/search-docs.md b/.cursor/commands/search-docs.md index 92b618bbf..f8ed8cc88 100644 --- a/.cursor/commands/search-docs.md +++ b/.cursor/commands/search-docs.md @@ -1,9 +1,9 @@ -# /search-docs — library docs in THIS checkout (optional) +# /search-docs — optional keyword search of THIS checkout's docs Usage: `/search-docs ` -Prefer `ramp-kit/conventions/rules.yaml` + the gate. MCP is stdio -`diffusers-docs` (Cloud dropdown: `diffusers-docs-mcp`). No Hub HTTP. +Prefer `ramp-kit/conventions/rules.yaml`, root `AGENTS.md`, `.ai/`, and the +reference source those files name. Default `.cursor/mcp.json` has no servers. ```bash python3 ramp-kit/tools/docs_mcp_server.py --query $1 @@ -14,5 +14,3 @@ If `$1` is empty: ```bash python3 ramp-kit/tools/docs_mcp_server.py --query "scheduler set_timesteps step SchedulerMixin register_to_config" ``` - -If the MCP tool `search_docs` is in your list, call that instead. diff --git a/.cursor/environment.json b/.cursor/environment.json index 6c58945fa..8b5a0d140 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,13 +1,6 @@ { "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && bash ramp-kit/.cursor/cloud-install.sh", - "start": "bash ramp-kit/.cursor/install-docs-mcp.sh", "repositoryDependencies": [ "github.com/alex-16moro/diffuser_agent" - ], - "mcpServerAllowlist": [ - { - "name": "diffusers-docs", - "command": "diffusers-docs-mcp" - } ] } diff --git a/.cursor/mcp-diffusers-docs.py b/.cursor/mcp-diffusers-docs.py deleted file mode 100755 index 9da6c2916..000000000 --- a/.cursor/mcp-diffusers-docs.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -"""Launch diffusers-docs MCP from any cwd. - -Cloud Agent stdio cannot set `cwd` and does not expand `${workspaceFolder}`. -The dashboard command is often `python3 -u .cursor/mcp-diffusers-docs.py`, -spawned from the workspace root (e.g. `/agent`), not the git repo. This -launcher walks cwd, git root, `/agent/repos/*`, `/workspace`, and its own -path until it finds `tools/docs_mcp_server.py`, then execs it. - -Desktop and Cloud share: `python3 -u .cursor/mcp-diffusers-docs.py`. -Cloud PATH fallback after install: `diffusers-docs-mcp`. -""" -from __future__ import annotations - -import os -import subprocess -import sys -from pathlib import Path - - -def _children(base: Path): - try: - if not base.is_dir(): - return - for child in base.iterdir(): - if child.is_dir(): - yield child - except OSError: - return - - -def _candidates(): - out: list[Path] = [] - here = Path(__file__).resolve().parent - out.append(here.parent) # repo root when this file lives in .cursor/ - out.append(here.parent / "ramp-kit") - cwd = Path.cwd().resolve() - out.extend([cwd, cwd / "ramp-kit", *cwd.parents]) - for key in ("CURSOR_PROJECT_DIR", "CURSOR_WORKSPACE", "WORKSPACE"): - val = os.environ.get(key) - if val: - out.append(Path(val)) - out.append(Path(val) / "ramp-kit") - for base in ( - Path("/agent"), - Path("/agent/repos"), - Path("/workspace"), - Path("/workspace/repos"), - here.parent / "repos", - cwd / "repos", - ): - out.append(base) - for child in _children(base): - out.append(child) - out.append(child / "ramp-kit") - try: - top = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], - stderr=subprocess.DEVNULL, - text=True, - ).strip() - if top: - out.append(Path(top)) - out.append(Path(top) / "ramp-kit") - except (OSError, subprocess.CalledProcessError): - pass - seen: set[Path] = set() - for raw in out: - try: - path = raw.resolve() - except OSError: - continue - if path in seen: - continue - seen.add(path) - yield path - - -def main(argv: list[str] | None = None) -> int: - extra = [a for a in (argv if argv is not None else sys.argv[1:]) if a != "--serve"] - for root in _candidates(): - script = root / "tools" / "docs_mcp_server.py" - if script.is_file(): - os.chdir(root) - sys.argv = [str(script), "--serve", *extra] - os.execv(sys.executable, [sys.executable, "-u", str(script), "--serve", *extra]) - sys.stderr.write( - "diffusers-docs MCP: could not find tools/docs_mcp_server.py " - f"(cwd={Path.cwd()} file={Path(__file__).resolve()}). " - "Install the PATH shim: bash .cursor/install-docs-mcp.sh " - "then spawn `diffusers-docs-mcp`.\n" - ) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.cursor/mcp-diffusers-docs.sh b/.cursor/mcp-diffusers-docs.sh deleted file mode 100755 index 852a35067..000000000 --- a/.cursor/mcp-diffusers-docs.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# Thin wrapper around the Python launcher (Cloud dashboard can use either). -set -euo pipefail -DIR="$(cd "$(dirname "$0")" && pwd)" -exec python3 -u "$DIR/mcp-diffusers-docs.py" --serve "$@" diff --git a/.cursor/mcp.json b/.cursor/mcp.json index cbf2bc837..8c3bf0d26 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -1,12 +1 @@ -{ - "mcpServers": { - "diffusers-docs": { - "type": "stdio", - "command": "python3", - "args": ["-u", ".cursor/mcp-diffusers-docs.py"], - "env": { - "PYTHONUNBUFFERED": "1" - } - } - } -} +{"mcpServers":{}} diff --git a/.cursor/mcp.optional.json b/.cursor/mcp.optional.json deleted file mode 100644 index 9080a649d..000000000 --- a/.cursor/mcp.optional.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "mcpServers": { - "diffusers-docs": { - "type": "stdio", - "command": "python3", - "args": ["-u", ".cursor/mcp-diffusers-docs.py"], - "env": { - "PYTHONUNBUFFERED": "1" - } - }, - "huggingface": { - "url": "https://huggingface.co/mcp" - } - } -} diff --git a/.cursor/rules/00-conventions.mdc b/.cursor/rules/00-conventions.mdc index 83d8b8f7b..de6ad125e 100644 --- a/.cursor/rules/00-conventions.mdc +++ b/.cursor/rules/00-conventions.mdc @@ -14,8 +14,9 @@ code that violates a **block** rule will fail the build. The first contribution is what `conventions/rules.yaml` checks. Copy the templates (`/scaffold`) and stop at 0 blocking findings from `tools/convention_check.py` on the new files. Do not add files the -registry does not scan. Docs search (`search_docs` / `/search-docs`) is -optional and is not a deliverable. +registry does not scan. Ground in this registry and, on the library, +`.ai/` plus reference source. Optional: +`python3 tools/docs_mcp_server.py --query "..."` — not a deliverable. Do not pull in code or context from outside this repo's approved boundaries (see `.cursorignore`). diff --git a/.cursor/skills/search-docs/SKILL.md b/.cursor/skills/search-docs/SKILL.md index ebcd8c2e3..a776a8ecb 100644 --- a/.cursor/skills/search-docs/SKILL.md +++ b/.cursor/skills/search-docs/SKILL.md @@ -1,6 +1,6 @@ --- name: search-docs -description: Optional docs CLI for this diffusers checkout. Prefer the gate; MCP is stdio search_docs. +description: Optional docs CLI for this diffusers checkout. Prefer AGENTS.md, .ai/, and the gate. Default mcp.json has no servers. --- # Search this library's docs @@ -10,6 +10,5 @@ python3 ramp-kit/tools/docs_mcp_server.py --query "" ``` Cite provenance (`diffusers checkout` vs bundled snapshot). -`ramp-kit/conventions/rules.yaml` is the gate. -`.cursor/mcp.json` is stdio `diffusers-docs` only (no Hub HTTP). -Cloud dropdown: `diffusers-docs-mcp`. +`ramp-kit/conventions/rules.yaml` is the gate. Root `AGENTS.md` and `.ai/` +are the library guide. Default `.cursor/mcp.json` has no servers. diff --git a/OVERLAY.md b/OVERLAY.md index e1bfa6f29..e9c00cdbf 100644 --- a/OVERLAY.md +++ b/OVERLAY.md @@ -31,13 +31,12 @@ The first contribution: Do not copy `ramp-kit/examples/candidate_scheduler/`. -`.cursor/mcp.json` ships **stdio `diffusers-docs` only** (`python3 -u -.cursor/mcp-diffusers-docs.py`). Do not enable Hub HTTP MCP (OAuth). Cloud -dropdown: `diffusers-docs-mcp` (PATH shim from `install-docs-mcp.sh`) or the -same python3 command. The launcher finds `ramp-kit/tools/docs_mcp_server.py` -from `/agent` as well as the repo root. +Default `.cursor/mcp.json` is `{"mcpServers":{}}`. Ground in root `AGENTS.md`, +`.ai/`, and the reference source those files name. Optional keyword search: -Hub HTTP and extra servers stay in `.cursor/mcp.optional.json`. +```bash +python3 ramp-kit/tools/docs_mcp_server.py --query "set_timesteps" +``` ## Demo (Cloud Agent) From 1818c4ec13257db86bd81f41c3352a109c8e0a6c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 17:42:56 +0000 Subject: [PATCH 12/14] =?UTF-8?q?[fork=20demo=20=E2=80=94=20not=20for=20up?= =?UTF-8?q?stream]=20Add=20PNDMLiteScheduler=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy the ramp-kit scheduler template and contract test, register PNDMLiteScheduler in the public API, and add autodoc so library consistency checks can pass. Keep TODO(engineer) in step(). Co-authored-by: ale93.moro --- docs/source/en/_toctree.yml | 2 + docs/source/en/api/schedulers/pndm_lite.md | 18 +++ src/diffusers/__init__.py | 2 + src/diffusers/schedulers/__init__.py | 2 + .../schedulers/scheduling_pndm_lite.py | 87 +++++++++++++ src/diffusers/utils/dummy_pt_objects.py | 15 +++ tests/schedulers/test_scheduling_pndm_lite.py | 116 ++++++++++++++++++ 7 files changed, 242 insertions(+) create mode 100644 docs/source/en/api/schedulers/pndm_lite.md create mode 100644 src/diffusers/schedulers/scheduling_pndm_lite.py create mode 100644 tests/schedulers/test_scheduling_pndm_lite.py diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index c2673d6d1..2340358ab 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -818,6 +818,8 @@ title: MiniMaxH3Scheduler - local: api/schedulers/pndm title: PNDMScheduler + - local: api/schedulers/pndm_lite + title: PNDMLiteScheduler - local: api/schedulers/repaint title: RePaintScheduler - local: api/schedulers/score_sde_ve diff --git a/docs/source/en/api/schedulers/pndm_lite.md b/docs/source/en/api/schedulers/pndm_lite.md new file mode 100644 index 000000000..44df0d3da --- /dev/null +++ b/docs/source/en/api/schedulers/pndm_lite.md @@ -0,0 +1,18 @@ + + +# PNDMLiteScheduler + +Discrete scheduler scaffold. Sampler math stays in `step()` as TODO(engineer). + +## PNDMLiteScheduler +[[autodoc]] PNDMLiteScheduler diff --git a/src/diffusers/__init__.py b/src/diffusers/__init__.py index b7f45c83a..ddac428c1 100644 --- a/src/diffusers/__init__.py +++ b/src/diffusers/__init__.py @@ -462,6 +462,7 @@ "LCMScheduler", "LTXEulerAncestralRFScheduler", "MiniMaxH3Scheduler", + "PNDMLiteScheduler", "PNDMScheduler", "RePaintScheduler", "SASolverScheduler", @@ -1341,6 +1342,7 @@ LCMScheduler, LTXEulerAncestralRFScheduler, MiniMaxH3Scheduler, + PNDMLiteScheduler, PNDMScheduler, RePaintScheduler, SASolverScheduler, diff --git a/src/diffusers/schedulers/__init__.py b/src/diffusers/schedulers/__init__.py index b803cbcb8..cb3ff87ed 100644 --- a/src/diffusers/schedulers/__init__.py +++ b/src/diffusers/schedulers/__init__.py @@ -75,6 +75,7 @@ _import_structure["scheduling_ltx_euler_ancestral_rf"] = ["LTXEulerAncestralRFScheduler"] _import_structure["scheduling_minimax_h3"] = ["MiniMaxH3Scheduler"] _import_structure["scheduling_pndm"] = ["PNDMScheduler"] + _import_structure["scheduling_pndm_lite"] = ["PNDMLiteScheduler"] _import_structure["scheduling_repaint"] = ["RePaintScheduler"] _import_structure["scheduling_sasolver"] = ["SASolverScheduler"] _import_structure["scheduling_scm"] = ["SCMScheduler"] @@ -160,6 +161,7 @@ from .scheduling_ltx_euler_ancestral_rf import LTXEulerAncestralRFScheduler from .scheduling_minimax_h3 import MiniMaxH3Scheduler from .scheduling_pndm import PNDMScheduler + from .scheduling_pndm_lite import PNDMLiteScheduler from .scheduling_repaint import RePaintScheduler from .scheduling_sasolver import SASolverScheduler from .scheduling_scm import SCMScheduler diff --git a/src/diffusers/schedulers/scheduling_pndm_lite.py b/src/diffusers/schedulers/scheduling_pndm_lite.py new file mode 100644 index 000000000..80be2405f --- /dev/null +++ b/src/diffusers/schedulers/scheduling_pndm_lite.py @@ -0,0 +1,87 @@ +# Copyright 2026 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Tuple, Union + +import torch + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import SchedulerMixin, SchedulerOutput + + +class PNDMLiteScheduler(SchedulerMixin, ConfigMixin): + """Discrete scheduler scaffold. Sampler math stays in `step()` as TODO(engineer).""" + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + ): + self.num_train_timesteps = num_train_timesteps + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps) + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + self.timesteps = torch.arange(num_train_timesteps - 1, -1, -1) + self.num_inference_steps: Optional[int] = None + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device, None] = None): + """Set the discrete timesteps used for the denoising loop. + + Args: + num_inference_steps (`int`): + Number of diffusion steps used at inference. + device (`str` or `torch.device`, *optional*): + Device the timesteps tensor should be moved to. + """ + self.num_inference_steps = num_inference_steps + step = self.config.num_train_timesteps // num_inference_steps + timesteps = (torch.arange(0, num_inference_steps) * step).round().long() + # torch.Tensor has no [::-1]; reverse with flip (see DDPM-style spacing). + timesteps = torch.flip(timesteps, dims=[0]) + self.timesteps = timesteps.to(device) if device is not None else timesteps + + def step( + self, + model_output: torch.Tensor, + timestep: int, + sample: torch.Tensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """Predict the sample at the previous timestep. + + Args: + model_output (`torch.Tensor`): + Direct output from the learned diffusion model. + timestep (`int`): + Current discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + Current sample. + generator (`torch.Generator`, *optional*): + PRNG for reproducible noise. Never use a global RNG. + return_dict (`bool`, *optional*, defaults to `True`): + Whether to return a [`SchedulerOutput`] or a plain tuple. + + Returns: + [`SchedulerOutput`] or `tuple`: Predicted previous sample. + """ + noise = torch.randn(sample.shape, generator=generator, device=sample.device, dtype=sample.dtype) + # TODO(engineer): replace this placeholder with the real update rule. + prev_sample = sample - model_output + 0.0 * noise + + if not return_dict: + return (prev_sample,) + return SchedulerOutput(prev_sample=prev_sample) diff --git a/src/diffusers/utils/dummy_pt_objects.py b/src/diffusers/utils/dummy_pt_objects.py index 11e55c975..ce4d4d806 100644 --- a/src/diffusers/utils/dummy_pt_objects.py +++ b/src/diffusers/utils/dummy_pt_objects.py @@ -3696,6 +3696,21 @@ def from_pretrained(cls, *args, **kwargs): requires_backends(cls, ["torch"]) +class PNDMLiteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + class PNDMScheduler(metaclass=DummyObject): _backends = ["torch"] diff --git a/tests/schedulers/test_scheduling_pndm_lite.py b/tests/schedulers/test_scheduling_pndm_lite.py new file mode 100644 index 000000000..813a49ce9 --- /dev/null +++ b/tests/schedulers/test_scheduling_pndm_lite.py @@ -0,0 +1,116 @@ +"""Contract tests for PNDMLiteScheduler. + +Structural and signature tests run with no extra installs. Behavioral checks +run when torch and diffusers are importable. +""" + +import ast +import importlib.util +import unittest +from pathlib import Path + + +TARGET = Path(__file__).resolve().parents[2] / "src" / "diffusers" / "schedulers" / "scheduling_pndm_lite.py" +CLASS = "PNDMLiteScheduler" + + +def _class_node(): + for node in ast.walk(ast.parse(TARGET.read_text())): + if isinstance(node, ast.ClassDef) and node.name == CLASS: + return node + raise AssertionError(f"{CLASS} not found in {TARGET}") + + +def _method(node, name): + for n in node.body: + if isinstance(n, ast.FunctionDef) and n.name == name: + return n + return None + + +class TestStructuralContract(unittest.TestCase): + """Thin structural sanity — the convention gate is the primary owner.""" + + @classmethod + def setUpClass(cls): + cls.node = _class_node() + + def test_inherits_required_mixins(self): + bases = {getattr(b, "id", getattr(b, "attr", "")) for b in self.node.bases} + self.assertEqual({"SchedulerMixin", "ConfigMixin"} & bases, {"SchedulerMixin", "ConfigMixin"}) + + +class TestSignatureContract(unittest.TestCase): + """Deeper than the gate: verify the SIGNATURES, not just method presence.""" + + @classmethod + def setUpClass(cls): + cls.node = _class_node() + + def test_set_timesteps_signature(self): + m = _method(self.node, "set_timesteps") + self.assertIsNotNone(m, "set_timesteps missing") + args = [a.arg for a in m.args.args] + self.assertIn( + "num_inference_steps", + args, + "set_timesteps must accept num_inference_steps (verified against upstream source)", + ) + self.assertIn("device", args, "set_timesteps must accept device") + + def test_step_signature(self): + m = _method(self.node, "step") + self.assertIsNotNone(m, "step missing") + args = [a.arg for a in m.args.args] + for expected in ("model_output", "timestep", "sample", "generator"): + self.assertIn(expected, args, f"step must accept {expected} (upstream contract)") + + +class TestBehavioralContract(unittest.TestCase): + """Real behaviour — runs in CI with torch; skipped cleanly offline.""" + + def setUp(self): + if importlib.util.find_spec("torch") is None: + self.skipTest("torch not installed; behavioral contract runs in CI") + if importlib.util.find_spec("diffusers") is None: + self.skipTest("diffusers not installed; behavioral contract runs in CI") + spec = importlib.util.spec_from_file_location(CLASS, TARGET) + self.mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.mod) + self.Scheduler = getattr(self.mod, CLASS) + + def test_config_roundtrip(self): + s = self.Scheduler(num_train_timesteps=500) + rebuilt = self.Scheduler.from_config(s.config) + self.assertEqual(rebuilt.config.num_train_timesteps, 500) + + def test_timesteps_count(self): + s = self.Scheduler() + s.set_timesteps(25) + self.assertEqual(len(s.timesteps), 25) + + def test_output_type(self): + import torch + + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + out = s.step(torch.ones_like(sample), 1, sample, generator=torch.Generator().manual_seed(0)) + self.assertTrue(hasattr(out, "prev_sample")) + self.assertEqual(out.prev_sample.shape, sample.shape) + self.assertEqual(out.prev_sample.dtype, sample.dtype) + + def test_same_seed_same_output(self): + import torch + + s = self.Scheduler() + s.set_timesteps(10) + sample = torch.zeros(1, 3, 8, 8) + mo = torch.ones_like(sample) + a = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample + b = s.step(mo, 1, sample, generator=torch.Generator().manual_seed(0)).prev_sample + self.assertTrue(torch.equal(a, b)) + + +if __name__ == "__main__": + unittest.main() From 0397eea68e602a200c855e0d581b0edd60be881e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 17:45:37 +0000 Subject: [PATCH 13/14] Apply make style to existing lite scheduler files make style reformats HeunLite/EulerLite sources and tests and sorts the scheduler toctree so PNDMLiteScheduler is alphabetical. Needed for `make quality` (ruff format / doc-builder) to exit 0. Co-authored-by: ale93.moro --- docs/source/en/_toctree.yml | 4 +-- .../schedulers/scheduling_euler_lite.py | 26 ++++++++----------- .../schedulers/scheduling_heun_lite.py | 4 +-- .../schedulers/test_scheduling_euler_lite.py | 12 +++++++-- tests/schedulers/test_scheduling_heun_lite.py | 11 ++++++-- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index 2340358ab..9087ee94b 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -816,10 +816,10 @@ title: LMSDiscreteScheduler - local: api/schedulers/minimax_h3 title: MiniMaxH3Scheduler - - local: api/schedulers/pndm - title: PNDMScheduler - local: api/schedulers/pndm_lite title: PNDMLiteScheduler + - local: api/schedulers/pndm + title: PNDMScheduler - local: api/schedulers/repaint title: RePaintScheduler - local: api/schedulers/score_sde_ve diff --git a/src/diffusers/schedulers/scheduling_euler_lite.py b/src/diffusers/schedulers/scheduling_euler_lite.py index 512b021ec..903394703 100644 --- a/src/diffusers/schedulers/scheduling_euler_lite.py +++ b/src/diffusers/schedulers/scheduling_euler_lite.py @@ -23,14 +23,12 @@ class EulerLiteScheduler(SchedulerMixin, ConfigMixin): """First-order Euler ODE sampler over a linear-beta sigma schedule. - Lite subset of [`EulerDiscreteScheduler`]: epsilon prediction, leading timesteps, and a - terminal sigma of 0. The update is the Karras Euler step from - `scheduling_euler_discrete.py` (no ancestral noise, no Karras/exponential/beta sigma - conversions). + Lite subset of [`EulerDiscreteScheduler`]: epsilon prediction, leading timesteps, and a terminal sigma of 0. The + update is the Karras Euler step from `scheduling_euler_discrete.py` (no ancestral noise, no Karras/exponential/beta + sigma conversions). - This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass - documentation for the generic methods the library implements for all schedulers such as - loading and saving. + This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic + methods the library implements for all schedulers such as loading and saving. Args: num_train_timesteps (`int`, defaults to `1000`): @@ -60,9 +58,9 @@ def __init__( def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device, None] = None): """Set the discrete timesteps used for the denoising loop. - Leading spacing matches `EulerDiscreteScheduler` with `timestep_spacing="leading"`: - `t = (arange(num_inference_steps) * (num_train_timesteps // num_inference_steps))` - reversed. Sigmas are the training-schedule values at those indices plus a trailing 0. + Leading spacing matches `EulerDiscreteScheduler` with `timestep_spacing="leading"`: `t = + (arange(num_inference_steps) * (num_train_timesteps // num_inference_steps))` reversed. Sigmas are the + training-schedule values at those indices plus a trailing 0. Args: num_inference_steps: Number of diffusion steps used at inference. @@ -85,12 +83,10 @@ def step( ) -> Union[SchedulerOutput, Tuple]: """Predict the sample at the previous timestep with a first-order Euler ODE step. - For epsilon prediction this is the update in `EulerDiscreteScheduler.step` with - `s_churn=0` (no stochastic churn, so `generator` is unused): + For epsilon prediction this is the update in `EulerDiscreteScheduler.step` with `s_churn=0` (no stochastic + churn, so `generator` is unused): - x0 = sample - sigma * epsilon - d = (sample - x0) / sigma - prev_sample = sample + d * (sigma_next - sigma) + x0 = sample - sigma * epsilon d = (sample - x0) / sigma prev_sample = sample + d * (sigma_next - sigma) Args: model_output: Direct output from the learned diffusion model (epsilon). diff --git a/src/diffusers/schedulers/scheduling_heun_lite.py b/src/diffusers/schedulers/scheduling_heun_lite.py index 6c5a9e301..e8e4b5c81 100644 --- a/src/diffusers/schedulers/scheduling_heun_lite.py +++ b/src/diffusers/schedulers/scheduling_heun_lite.py @@ -59,9 +59,7 @@ def step( Returns: SchedulerOutput or tuple with the predicted previous sample. """ - noise = torch.randn( - sample.shape, generator=generator, device=sample.device, dtype=sample.dtype - ) + noise = torch.randn(sample.shape, generator=generator, device=sample.device, dtype=sample.dtype) # TODO(engineer): replace this placeholder with the real update rule. prev_sample = sample - model_output + 0.0 * noise diff --git a/tests/schedulers/test_scheduling_euler_lite.py b/tests/schedulers/test_scheduling_euler_lite.py index a741a1a26..278c07ff2 100644 --- a/tests/schedulers/test_scheduling_euler_lite.py +++ b/tests/schedulers/test_scheduling_euler_lite.py @@ -5,11 +5,13 @@ See examples/scaffolded_scheduler + tests/schedulers/test_scheduling_ddpm_lite.py for a filled-in example. """ + import ast import importlib.util import unittest from pathlib import Path + # ---- edit these two for your scheduler ------------------------------------- TARGET = Path(__file__).resolve().parents[2] / "src" / "diffusers" / "schedulers" / "scheduling_euler_lite.py" CLASS = "EulerLiteScheduler" @@ -69,8 +71,11 @@ def test_set_timesteps_signature(self): m = _method(self.node, "set_timesteps") self.assertIsNotNone(m, "set_timesteps missing") args = [a.arg for a in m.args.args] - self.assertIn("num_inference_steps", args, - "set_timesteps must accept num_inference_steps (verified against upstream source)") + self.assertIn( + "num_inference_steps", + args, + "set_timesteps must accept num_inference_steps (verified against upstream source)", + ) self.assertIn("device", args, "set_timesteps must accept device") def test_step_signature(self): @@ -106,6 +111,7 @@ def test_timesteps_count(self): def test_output_type(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) @@ -116,6 +122,7 @@ def test_output_type(self): def test_same_seed_same_output(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) @@ -126,6 +133,7 @@ def test_same_seed_same_output(self): def test_euler_ode_epsilon_update(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) diff --git a/tests/schedulers/test_scheduling_heun_lite.py b/tests/schedulers/test_scheduling_heun_lite.py index 61e18fb0d..67e504072 100644 --- a/tests/schedulers/test_scheduling_heun_lite.py +++ b/tests/schedulers/test_scheduling_heun_lite.py @@ -5,11 +5,13 @@ See examples/scaffolded_scheduler + tests/schedulers/test_scheduling_ddpm_lite.py for a filled-in example. """ + import ast import importlib.util import unittest from pathlib import Path + # ---- edit these two for your scheduler ------------------------------------- TARGET = Path(__file__).resolve().parents[2] / "src" / "diffusers" / "schedulers" / "scheduling_heun_lite.py" CLASS = "HeunLiteScheduler" @@ -53,8 +55,11 @@ def test_set_timesteps_signature(self): m = _method(self.node, "set_timesteps") self.assertIsNotNone(m, "set_timesteps missing") args = [a.arg for a in m.args.args] - self.assertIn("num_inference_steps", args, - "set_timesteps must accept num_inference_steps (verified against upstream source)") + self.assertIn( + "num_inference_steps", + args, + "set_timesteps must accept num_inference_steps (verified against upstream source)", + ) self.assertIn("device", args, "set_timesteps must accept device") def test_step_signature(self): @@ -90,6 +95,7 @@ def test_timesteps_count(self): def test_output_type(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) @@ -100,6 +106,7 @@ def test_output_type(self): def test_same_seed_same_output(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) From 49474cda43c85676ad06515d7355983a8d65e110 Mon Sep 17 00:00:00 2001 From: alex-16moro Date: Thu, 24 Sep 2026 08:49:02 +0100 Subject: [PATCH 14/14] Fix Cursor setup for the Ramp Kit demo --- .cursor/environment.json | 6 ++---- .cursor/ramp-cloud-setup.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 .cursor/ramp-cloud-setup.sh diff --git a/.cursor/environment.json b/.cursor/environment.json index 8b5a0d140..f95f68472 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,6 +1,4 @@ { - "install": "if [ ! -d ramp-kit/.git ]; then git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit; fi && bash ramp-kit/.cursor/cloud-install.sh", - "repositoryDependencies": [ - "github.com/alex-16moro/diffuser_agent" - ] + "install": "bash .cursor/ramp-cloud-setup.sh install", + "start": "bash .cursor/ramp-cloud-setup.sh start" } diff --git a/.cursor/ramp-cloud-setup.sh b/.cursor/ramp-cloud-setup.sh new file mode 100644 index 000000000..9a28049d6 --- /dev/null +++ b/.cursor/ramp-cloud-setup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +PHASE=${1:?Pass install or start} +case "$PHASE" in + install|start) ;; + *) echo 'Expected install or start.' >&2; exit 2 ;; +esac +REPO=$(git rev-parse --show-toplevel) +cd "$REPO" + +if [ -e ramp-kit ] || [ -L ramp-kit ]; then + echo 'Legacy ramp-kit path detected. Create a fresh Cursor environment without the diffuser_agent repository or legacy install command. Do not reuse this Build for clean acceptance.' >&2 + exit 2 +fi + +if [ ! -f .ramp-kit/runtime/bootstrap.sh ]; then + if [ "$PHASE" = install ]; then + echo 'Default-branch preparation complete. No external kit repository was cloned. Start the Ramp Kit session on ramp-demo; startup will prepare its installed .ramp-kit runtime.' + exit 0 + fi + echo 'This branch has no installed .ramp-kit. Start the Ramp Kit session on ramp-demo.' >&2 + exit 2 +fi + +if [ "$PHASE" = install ] || [ ! -x .ramp-venv/bin/python ]; then + bash .ramp-kit/runtime/bootstrap.sh "$REPO" +fi +.ramp-venv/bin/python .ramp-kit/run.py --repo "$REPO" doctor