sunandbean's picture
Add files using upload-large-folder tool
33d23e1 verified
Raw History Blame Contribute Delete
10.9 kB
# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import pytest
import torch
from diffusers.pipelines.mochi.pipeline_mochi import linear_quadratic_schedule
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
from diffusers.schedulers.scheduling_unipc_multistep import UniPCMultistepScheduler
import ttnn
from models.tt_dit.solvers.euler import EulerSolver
from models.tt_dit.solvers.factory import CustomSigmaScheduler, solver_for_scheduler
from models.tt_dit.solvers.unipc import UniPCSolver, UniPCVariant
from models.tt_dit.utils import tensor
from models.tt_dit.utils.check import assert_quality
_NUM_STEPS = 17
def _unipc_scheduler(*, variant: UniPCVariant = UniPCVariant.BH2, flow_shift: float = 3.0) -> UniPCMultistepScheduler:
return UniPCMultistepScheduler(
use_flow_sigmas=True,
flow_shift=flow_shift,
prediction_type="flow_prediction",
solver_order=2,
solver_type=variant.value,
)
# Euler solver tests
def _motif_sigmas(*, step_count: int, linear_quadratic_emulating_steps: int) -> torch.Tensor:
assert step_count % 2 == 0
s = step_count
n = linear_quadratic_emulating_steps
a = s // 2 / n - 1
sigmas1 = torch.linspace(1, 0, n + 1)[: s // 2]
sigmas2 = torch.linspace(0, 1, s // 2 + 1).pow(2) * a - a
return torch.concat([sigmas1, sigmas2])
def _assert_euler_matches_scheduler(
mesh_device: ttnn.MeshDevice,
*,
schedule_kwargs: dict[str, object],
expected_timesteps: torch.Tensor | None = None,
expected_sigmas: torch.Tensor | None = None,
) -> None:
ref_scheduler = FlowMatchEulerDiscreteScheduler()
solver = EulerSolver(scheduler=ref_scheduler)
solver.set_schedule(**schedule_kwargs)
if expected_timesteps is not None:
assert torch.allclose(ref_scheduler.timesteps, expected_timesteps)
if expected_sigmas is not None:
assert ref_scheduler.sigmas.tolist() == expected_sigmas.tolist()
torch.manual_seed(0)
torch_latent = torch.randn(1, 1, 32, 32)
ref = torch_latent.clone()
latent = tensor.from_torch(torch_latent, device=mesh_device, dtype=ttnn.float32)
for step_idx in range(len(ref_scheduler.timesteps)):
torch_velocity = torch.randn_like(torch_latent)
ref = ref_scheduler.step(torch_velocity, ref_scheduler.timesteps[step_idx], ref, return_dict=False)[0]
velocity = tensor.from_torch(torch_velocity, device=mesh_device, dtype=ttnn.float32)
latent = solver.step(step=step_idx, latent=latent, velocity_pred=velocity)
result_ours = ttnn.to_torch(latent)
assert_quality(result_ours, ref, pcc=0.999_999, relative_rmse=1e-5)
@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
def test_euler_set_schedule_with_sigmas_and_mu_matches_scheduler(mesh_device: ttnn.MeshDevice) -> None:
"""EulerSolver should match scheduler outputs for the flux/qwen sigmas+mu path."""
sigmas = torch.linspace(1.0, 1 / _NUM_STEPS, _NUM_STEPS).tolist()
_assert_euler_matches_scheduler(mesh_device, schedule_kwargs={"sigmas": sigmas, "mu": 0.75})
@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
def test_euler_set_schedule_with_mochi_sigmas_matches_scheduler(mesh_device: ttnn.MeshDevice) -> None:
"""EulerSolver should match scheduler outputs for the mochi custom sigma path."""
sigmas = linear_quadratic_schedule(_NUM_STEPS, 0.025)
_assert_euler_matches_scheduler(mesh_device, schedule_kwargs={"sigmas": sigmas})
@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
def test_euler_set_schedule_with_motif_sigmas_matches_main(mesh_device: ttnn.MeshDevice) -> None:
"""EulerSolver should reproduce motif's main-branch timesteps and sigma schedule."""
step_count = 18
sigmas = _motif_sigmas(step_count=step_count, linear_quadratic_emulating_steps=1000)
_assert_euler_matches_scheduler(
mesh_device,
schedule_kwargs={"sigmas": sigmas[:-1].tolist()},
expected_timesteps=sigmas[:-1] * 1000,
expected_sigmas=sigmas,
)
def test_euler_without_scheduler_accepts_only_sigmas(expect_error) -> None:
"""A scheduler-less EulerSolver takes sigmas verbatim and rejects scheduler arguments."""
solver = EulerSolver()
sigmas = [1.0, 0.5, 0.0]
solver.set_schedule(sigmas=sigmas)
assert solver.sigmas == (1.0, 0.5, 0.0)
assert solver.alphas == (0.0, 0.5, 1.0)
assert solver.timesteps == (1000.0, 500.0)
with expect_error(ValueError, "accepts only `sigmas`"):
solver.set_schedule(_NUM_STEPS)
with expect_error(ValueError, "accepts only `sigmas`"):
solver.set_schedule(sigmas=sigmas, shift=5.0)
with expect_error(ValueError, "accepts only `sigmas`"):
solver.set_schedule(sigmas=sigmas, mu=0.75)
def test_solver_without_schedule_rejects_access(expect_error) -> None:
"""Reading the schedule before setting one should fail loudly."""
with expect_error(ValueError, "schedule must be set"):
_ = EulerSolver().sigmas
@pytest.mark.parametrize("family", ["unipc", "euler"])
def test_omitted_shift_restores_construction_value(family: str) -> None:
"""Every solver spells the per-run shift `shift`, and it must not persist into the next run."""
default_shift = 3.0
if family == "unipc":
solver = solver_for_scheduler(_unipc_scheduler(flow_shift=default_shift))
else:
solver = solver_for_scheduler(FlowMatchEulerDiscreteScheduler(shift=default_shift))
solver.set_schedule(_NUM_STEPS)
expected = solver.sigmas
solver.set_schedule(_NUM_STEPS, shift=12.0)
shifted = solver.sigmas
assert shifted != expected
solver.set_schedule(_NUM_STEPS)
assert solver.sigmas == expected
solver.set_schedule(_NUM_STEPS, shift=12.0)
assert solver.sigmas == shifted
def test_solver_for_scheduler_dispatch(expect_error) -> None:
"""The scheduler class selects the solver family, and unsupported ones are rejected."""
assert isinstance(solver_for_scheduler(_unipc_scheduler()), UniPCSolver)
assert isinstance(solver_for_scheduler(FlowMatchEulerDiscreteScheduler()), EulerSolver)
with expect_error(ValueError, "use_flow_sigmas=True"):
solver_for_scheduler(UniPCMultistepScheduler())
with expect_error(ValueError, "no solver available"):
solver_for_scheduler(object())
def test_custom_sigma_scheduler_dispatches_to_a_scheduler_less_euler() -> None:
"""The marker asks for Euler over sigmas the caller supplies, taken as given."""
solver = solver_for_scheduler(CustomSigmaScheduler())
assert isinstance(solver, EulerSolver)
assert solver.scheduler is None
solver.set_schedule(sigmas=[1.0, 0.5, 0.0])
assert solver.sigmas == (1.0, 0.5, 0.0)
def test_solver_for_scheduler_takes_solver_config_from_scheduler() -> None:
"""UniPC order and variant come from the scheduler, not from the caller."""
solver = solver_for_scheduler(_unipc_scheduler(variant=UniPCVariant.BH1))
assert solver.order == 2 # solver_order=2 in _unipc_scheduler
assert solver.variant is UniPCVariant.BH1
@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
def test_euler_matches_diffusers(mesh_device: ttnn.MeshDevice) -> None:
"""EulerSolver should match FlowMatchEulerDiscreteScheduler at every step."""
torch.manual_seed(0)
torch_latent = torch.randn(1, 1, 32, 32)
scheduler = FlowMatchEulerDiscreteScheduler()
solver = EulerSolver(scheduler=scheduler)
solver.set_schedule(_NUM_STEPS)
ref = torch_latent.clone()
latent = tensor.from_torch(torch_latent, device=mesh_device, dtype=ttnn.float32)
for step_idx in range(_NUM_STEPS):
torch_velocity = torch.randn_like(torch_latent)
ref = scheduler.step(torch_velocity, scheduler.timesteps[step_idx], ref, return_dict=False)[0]
velocity = tensor.from_torch(torch_velocity, device=mesh_device, dtype=ttnn.float32)
latent = solver.step(step=step_idx, latent=latent, velocity_pred=velocity)
result_ours = ttnn.to_torch(latent)
assert_quality(result_ours, ref, pcc=0.999_999, relative_rmse=1e-5)
# UniPC solver tests
def test_unipc_constructor_validation(expect_error) -> None:
"""UniPCSolver should reject unsupported orders."""
with expect_error(ValueError, "only order 1 and 2 are supported"):
UniPCSolver(order=3, variant=UniPCVariant.BH2, scheduler=_unipc_scheduler())
@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
def test_unipc_set_schedule_resets_state(mesh_device: ttnn.MeshDevice) -> None:
"""set_schedule should reset logical history without reallocating state buffers."""
solver = UniPCSolver(order=2, variant=UniPCVariant.BH2, scheduler=_unipc_scheduler())
solver.set_schedule(_NUM_STEPS)
latent = tensor.from_torch(torch.randn(1, 1, 32, 32), device=mesh_device, dtype=ttnn.float32)
velocity = tensor.from_torch(torch.randn(1, 1, 32, 32), device=mesh_device, dtype=ttnn.float32)
solver.step(step=0, latent=latent, velocity_pred=velocity)
assert solver._state is not None
clean_preds = solver._state.clean_preds
corrected = solver._state.corrected
assert solver._state.oldest_idx == 1
solver.set_schedule(_NUM_STEPS)
assert solver._state is not None
assert solver._state.clean_preds == clean_preds
assert solver._state.corrected is corrected
assert solver._state.oldest_idx == 0
@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
@pytest.mark.parametrize("variant", [UniPCVariant.BH1, UniPCVariant.BH2])
@pytest.mark.parametrize("shift", [5.0, 12.0])
def test_unipc_matches_diffusers(mesh_device: ttnn.MeshDevice, variant: UniPCVariant, shift: float) -> None:
"""UniPCSolver should match UniPCMultistepScheduler at every step."""
torch.manual_seed(0)
torch_latent = torch.randn(1, 1, 32, 32)
scheduler = _unipc_scheduler(variant=variant, flow_shift=shift)
solver = UniPCSolver(order=2, variant=variant, scheduler=scheduler)
solver.set_schedule(_NUM_STEPS)
ref = torch_latent.clone()
latent = tensor.from_torch(torch_latent, device=mesh_device, dtype=ttnn.float32)
for step_idx in range(_NUM_STEPS):
if step_idx == _NUM_STEPS - 1 and variant is UniPCVariant.BH1:
# Diffusers bh1 produces NaN on the final step; skip.
break
torch_velocity = torch.randn_like(torch_latent)
ref = scheduler.step(torch_velocity, scheduler.timesteps[step_idx], ref, return_dict=False)[0]
velocity = tensor.from_torch(torch_velocity, device=mesh_device, dtype=ttnn.float32)
latent = solver.step(step=step_idx, latent=latent, velocity_pred=velocity)
result_ours = ttnn.to_torch(latent)
assert_quality(result_ours, ref, pcc=1 - 3e-10, relative_rmse=3e-7)