From 7bdce2ac4a9a88d1803726a7d46bf9cf7b3a4a26 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 09:27:23 +0000 Subject: [PATCH 1/2] Reject an empty custom timestep list with a clear ValueError Co-authored-by: alex-16moro --- src/diffusers/schedulers/scheduling_ddpm.py | 5 ++- .../schedulers/scheduling_ddpm_parallel.py | 5 ++- tests/schedulers/test_scheduler_ddpm.py | 33 ++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/diffusers/schedulers/scheduling_ddpm.py b/src/diffusers/schedulers/scheduling_ddpm.py index 972c46c6e..1ae240608 100644 --- a/src/diffusers/schedulers/scheduling_ddpm.py +++ b/src/diffusers/schedulers/scheduling_ddpm.py @@ -289,13 +289,16 @@ def set_timesteps( timesteps (`list[int]`, *optional*): Custom timesteps used to support arbitrary spacing between timesteps. If `None`, then the default timestep spacing strategy of equal spacing between timesteps is used. If `timesteps` is passed, - `num_inference_steps` must be `None`. + `num_inference_steps` must be `None`. The list must be non-empty. """ if num_inference_steps is not None and timesteps is not None: raise ValueError("Can only pass one of `num_inference_steps` or `custom_timesteps`.") if timesteps is not None: + if len(timesteps) == 0: + raise ValueError("`timesteps` cannot be empty.") + for i in range(1, len(timesteps)): if timesteps[i] >= timesteps[i - 1]: raise ValueError("`custom_timesteps` must be in descending order.") diff --git a/src/diffusers/schedulers/scheduling_ddpm_parallel.py b/src/diffusers/schedulers/scheduling_ddpm_parallel.py index 725958b01..1230bc6b9 100644 --- a/src/diffusers/schedulers/scheduling_ddpm_parallel.py +++ b/src/diffusers/schedulers/scheduling_ddpm_parallel.py @@ -304,13 +304,16 @@ def set_timesteps( timesteps (`list[int]`, *optional*): Custom timesteps used to support arbitrary spacing between timesteps. If `None`, then the default timestep spacing strategy of equal spacing between timesteps is used. If `timesteps` is passed, - `num_inference_steps` must be `None`. + `num_inference_steps` must be `None`. The list must be non-empty. """ if num_inference_steps is not None and timesteps is not None: raise ValueError("Can only pass one of `num_inference_steps` or `custom_timesteps`.") if timesteps is not None: + if len(timesteps) == 0: + raise ValueError("`timesteps` cannot be empty.") + for i in range(1, len(timesteps)): if timesteps[i] >= timesteps[i - 1]: raise ValueError("`custom_timesteps` must be in descending order.") diff --git a/tests/schedulers/test_scheduler_ddpm.py b/tests/schedulers/test_scheduler_ddpm.py index 056b5d833..cf845b9e3 100644 --- a/tests/schedulers/test_scheduler_ddpm.py +++ b/tests/schedulers/test_scheduler_ddpm.py @@ -1,6 +1,6 @@ import torch -from diffusers import DDPMScheduler +from diffusers import DDPMParallelScheduler, DDPMScheduler from .test_schedulers import SchedulerCommonTest @@ -190,6 +190,37 @@ def test_custom_timesteps_too_large(self): ): scheduler.set_timesteps(timesteps=timesteps) + def test_set_timesteps_empty_list_raises_value_error(self): + for scheduler_class in (DDPMScheduler, DDPMParallelScheduler): + scheduler = scheduler_class(**self.get_scheduler_config()) + with self.assertRaisesRegex( + ValueError, "Can only pass one of `num_inference_steps` or `custom_timesteps`." + ): + scheduler.set_timesteps(num_inference_steps=4, timesteps=[]) + with self.assertRaisesRegex(ValueError, r"`timesteps` cannot be empty"): + scheduler.set_timesteps(timesteps=[]) + + def test_set_timesteps_keeps_nonempty_custom_lists(self): + scheduler_config = self.get_scheduler_config() + custom = [100, 87, 50, 1, 0] + scheduler = self.scheduler_classes[0](**scheduler_config) + scheduler.set_timesteps(timesteps=custom) + self.assertEqual(scheduler.timesteps.tolist(), custom) + self.assertEqual(scheduler.timesteps.device.type, "cpu") + + scheduler = self.scheduler_classes[0](**scheduler_config) + scheduler.set_timesteps(timesteps=[0]) + self.assertEqual(scheduler.timesteps.tolist(), [0]) + self.assertEqual(scheduler.timesteps.device.type, "cpu") + + def test_set_timesteps_num_inference_steps_still_works(self): + scheduler = self.scheduler_classes[0](**self.get_scheduler_config()) + scheduler.set_timesteps(num_inference_steps=10) + self.assertEqual(len(scheduler.timesteps), 10) + timesteps = scheduler.timesteps.tolist() + self.assertEqual(timesteps, sorted(timesteps, reverse=True)) + self.assertFalse(scheduler.custom_timesteps) + def test_full_loop_with_noise(self): scheduler_class = self.scheduler_classes[0] scheduler_config = self.get_scheduler_config() From e42a269ce5ed5b59b5e6b178d9aa40443133bba2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 09:31:47 +0000 Subject: [PATCH 2/2] Add Ramp Kit task record for fork CI verification Co-authored-by: alex-16moro --- .../empty-timesteps/architecture-history.json | 25 +++++++++ .ramp/empty-timesteps/architecture.json | 12 +++++ .ramp/empty-timesteps/task.json | 51 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 .ramp/empty-timesteps/architecture-history.json create mode 100644 .ramp/empty-timesteps/architecture.json create mode 100644 .ramp/empty-timesteps/task.json diff --git a/.ramp/empty-timesteps/architecture-history.json b/.ramp/empty-timesteps/architecture-history.json new file mode 100644 index 000000000..ffc5954ec --- /dev/null +++ b/.ramp/empty-timesteps/architecture-history.json @@ -0,0 +1,25 @@ +[ + { + "decision": "REVISE", + "rationale": "Replacing an empty custom timestep list with [999] silently invents a one-step schedule the caller did not request. code_style.md forbids guessing user intent and silently correcting unsupported input; philosophy.md prefers a concise error over silent correction. 999 is also only meaningful when num_train_timesteps is 1000, so it is not a general default.", + "sources": [ + ".ai/references/code_style.md", + "docs/source/en/conceptual/philosophy.md" + ], + "alternative": "Raise ValueError in DDPMScheduler.set_timesteps when timesteps is empty, after the existing mutually exclusive argument check and before timesteps[0] is read. Do not invent a default list.", + "task_sha256": "cf0af9c5d0cd003518d722c54d0951d63f15b68f68116e1696ad8aec2b6bd780", + "kind": "agent_assessment_not_human_approval" + }, + { + "decision": "COMPATIBLE", + "rationale": "A local ValueError in DDPMScheduler.set_timesteps fits the existing custom-timestep checks in that method. Empty input is rejected after the mutual-exclusion error and before timesteps[0] is read, so valid lists, single-element lists, and num_inference_steps schedules stay unchanged. The parallel scheduler already copies this method; propagate the edit with the copy tool instead of adding a shared validation service.", + "sources": [ + "src/diffusers/schedulers/scheduling_ddpm.py", + "src/diffusers/schedulers/scheduling_ddpm_parallel.py", + ".ai/references/code_style.md" + ], + "alternative": "", + "task_sha256": "cf0af9c5d0cd003518d722c54d0951d63f15b68f68116e1696ad8aec2b6bd780", + "kind": "agent_assessment_not_human_approval" + } +] diff --git a/.ramp/empty-timesteps/architecture.json b/.ramp/empty-timesteps/architecture.json new file mode 100644 index 000000000..fe199f582 --- /dev/null +++ b/.ramp/empty-timesteps/architecture.json @@ -0,0 +1,12 @@ +{ + "decision": "COMPATIBLE", + "rationale": "A local ValueError in DDPMScheduler.set_timesteps fits the existing custom-timestep checks in that method. Empty input is rejected after the mutual-exclusion error and before timesteps[0] is read, so valid lists, single-element lists, and num_inference_steps schedules stay unchanged. The parallel scheduler already copies this method; propagate the edit with the copy tool instead of adding a shared validation service.", + "sources": [ + "src/diffusers/schedulers/scheduling_ddpm.py", + "src/diffusers/schedulers/scheduling_ddpm_parallel.py", + ".ai/references/code_style.md" + ], + "alternative": "", + "task_sha256": "cf0af9c5d0cd003518d722c54d0951d63f15b68f68116e1696ad8aec2b6bd780", + "kind": "agent_assessment_not_human_approval" +} diff --git a/.ramp/empty-timesteps/task.json b/.ramp/empty-timesteps/task.json new file mode 100644 index 000000000..e0e5e2221 --- /dev/null +++ b/.ramp/empty-timesteps/task.json @@ -0,0 +1,51 @@ +{ + "id": "empty-timesteps", + "title": "Reject an empty custom timestep list with a clear ValueError", + "request": "Make passing an empty custom timestep list produce a clear error while preserving valid inputs.", + "proposed_approach": "The engineer asked whether an empty list could be replaced with [999]. Treat that as a hypothesis and reject a silent default if library guidance prefers a clear error.", + "recipe": "scheduler-input-validation", + "editable_files": [ + "src/diffusers/schedulers/scheduling_ddpm.py", + "src/diffusers/schedulers/scheduling_ddpm_parallel.py", + "tests/schedulers/test_scheduler_ddpm.py" + ], + "regression_test": "tests/schedulers/test_scheduler_ddpm.py::DDPMSchedulerTest::test_set_timesteps_empty_list_raises_value_error", + "baseline_error": "IndexError", + "criteria": [ + { + "id": "C-EMPTY", + "text": "DDPMScheduler.set_timesteps and the copied DDPMParallelScheduler.set_timesteps raise ValueError when timesteps is an empty list; the existing both-argument conflict error still wins when both arguments are supplied.", + "tests": [ + "tests/schedulers/test_scheduler_ddpm.py::DDPMSchedulerTest::test_set_timesteps_empty_list_raises_value_error" + ] + }, + { + "id": "C-VALID", + "text": "A descending custom list and a single-element custom list keep their values on CPU when no device is requested.", + "tests": [ + "tests/schedulers/test_scheduler_ddpm.py::DDPMSchedulerTest::test_set_timesteps_keeps_nonempty_custom_lists" + ] + }, + { + "id": "C-MUTEX", + "text": "Passing num_inference_steps and timesteps together still raises the existing mutual-exclusion ValueError.", + "tests": [ + "tests/schedulers/test_scheduler_ddpm.py::DDPMSchedulerTest::test_custom_timesteps_passing_both_num_inference_steps_and_timesteps" + ] + }, + { + "id": "C-COUNT", + "text": "Calling set_timesteps with only num_inference_steps still builds a descending schedule of that length.", + "tests": [ + "tests/schedulers/test_scheduler_ddpm.py::DDPMSchedulerTest::test_set_timesteps_num_inference_steps_still_works" + ] + } + ], + "exclusions": [ + "Do not change scheduler numerical updates", + "Do not add a shared validation helper or new dependency", + "Do not silently replace an empty list with a default schedule", + "Do not publish or deploy" + ], + "base_sha": "0121a91f9d419ff7234c8a5923f82c244e6f1914" +}