File size: 10,910 Bytes
33d23e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# 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)