Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .ramp/empty-timesteps/architecture-history.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
12 changes: 12 additions & 0 deletions .ramp/empty-timesteps/architecture.json
Original file line number Diff line number Diff line change
@@ -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"
}
51 changes: 51 additions & 0 deletions .ramp/empty-timesteps/task.json
Original file line number Diff line number Diff line change
@@ -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"
}
5 changes: 4 additions & 1 deletion src/diffusers/schedulers/scheduling_ddpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
5 changes: 4 additions & 1 deletion src/diffusers/schedulers/scheduling_ddpm_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
33 changes: 32 additions & 1 deletion tests/schedulers/test_scheduler_ddpm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import torch

from diffusers import DDPMScheduler
from diffusers import DDPMParallelScheduler, DDPMScheduler

from .test_schedulers import SchedulerCommonTest

Expand Down Expand Up @@ -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()
Expand Down
Loading