From 470b38231f145aa5f8e32df0d2b9f27647b0ea07 Mon Sep 17 00:00:00 2001 From: blepping <157360029+blepping@users.noreply.github.com> Date: Sun, 4 Aug 2024 11:28:06 -0600 Subject: [PATCH] Refactor (#1) Refactor all the things! --- LICENSE | 2 +- README.md | 645 ++++++++-- __init__.py | 9 +- assets/basic_sampling.png | Bin 0 -> 49070 bytes assets/group_time_example.png | Bin 0 -> 31539 bytes docs/expression.md | 139 +++ docs/filter.md | 187 +++ py/expression/__init__.py | 18 + py/expression/expression.py | 239 ++++ py/expression/handler.py | 364 ++++++ py/expression/parser.py | 112 ++ py/expression/types.py | 220 ++++ py/expression/util.py | 36 + py/expression/validation.py | 190 +++ py/expression_handlers.py | 623 ++++++++++ py/external.py | 18 + py/filtering.py | 536 ++++++++ py/latent.py | 114 ++ py/model.py | 264 ++++ py/nodes.py | 460 +++++-- py/noise.py | 231 ++++ py/restart.py | 91 ++ py/sampling.py | 125 +- py/step_samplers.py | 2157 +++++++++++++++++++++++++++++++++ py/substep_merging.py | 656 +++++++--- py/substep_samplers.py | 624 ---------- py/substep_sampling.py | 263 ++-- py/utils.py | 76 +- 28 files changed, 7237 insertions(+), 1162 deletions(-) create mode 100644 assets/basic_sampling.png create mode 100644 assets/group_time_example.png create mode 100644 docs/expression.md create mode 100644 docs/filter.md create mode 100644 py/expression/__init__.py create mode 100644 py/expression/expression.py create mode 100644 py/expression/handler.py create mode 100644 py/expression/parser.py create mode 100644 py/expression/types.py create mode 100644 py/expression/util.py create mode 100644 py/expression/validation.py create mode 100644 py/expression_handlers.py create mode 100644 py/external.py create mode 100644 py/filtering.py create mode 100644 py/latent.py create mode 100644 py/model.py create mode 100644 py/noise.py create mode 100644 py/restart.py create mode 100644 py/step_samplers.py delete mode 100644 py/substep_samplers.py diff --git a/LICENSE b/LICENSE index 51a2cc5..b633294 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 blepping +Copyright (c) 2024 blepping Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 26130dd..4bbea1f 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,584 @@ # Overly Complicated Sampling -Wildly unsound and experimental sampling for [ComfyUI](https://github.com/comfyanonymous/ComfyUI). -## Description +Experimental and mathematically unsound (but fun!) sampling for [ComfyUI](https://github.com/comfyanonymous/ComfyUI). -Very unstable, experimental and mathematically unsound sampling for ComfyUI. +**Status**: In flux, may be useful but likely to change/break workflows frequently. Mainly for advanced users. -Current status: In flux, not suitable for general use. -*Note*: You will basically always have to tweak settings like `s_noise` to get a good result. If the generation looks smooth/undetailed increase `s_noise` somewhere. If it looks crunchy, super high contrast, etc then try reducing noise. +## Features -## Nodes +* Many different samplers. +* Allows scheduling samplers (i.e. run `euler` for steps 1-4, then switch to `dpmpp_sde`). +* CFG++ support (for some samplers). +* Native support for Restart sigmas. +* Supports custom noise types. +* Immiscible noise for sampling and Restart. See https://arxiv.org/abs/2406.12303 (note that it was designed for training not inference). +* Allows splitting/combining steps in various ways for (potentially) more accurate sampling. +* Supports Diffrax, torchdiffeq, torchode and torchsde solver backends. (SDE mode not recommended currently.) +* Many tuneable parameters to play with. -### ComposableSampler - -**Possible Parameters** - -* `avgmerge_stretch`(`0.4`): Used for `average` and `sample` merge types. See below. -* `model_call_cache`(unset): Caches the result of model calls. For example, Bogacki is 3 model calls per step. The first one usually depends on the merge strategy: `average` for example shares the first model evaluation between substeps, but subsequent model calls (i.e. Bogacki 2nd and 3rd model evaluations) still occur. When the model call cache is active, it's possible to cache those evalutions and avoid a model call for the remaining substeps. If you set `model_call_cache` to `1` then the result of that second call will be cached and if you're running two Bogacki substeps then the second one will use the cached version. Massively accelerates inference (especially when using the `average` merge strategy) but is likely very unsound and inaccurate. Does not apply to the sampler call for the `sample` merge strategy. -* `model_call_cache_threshold`(`1`): Disables caching model call results with a call index below the threshold value (starting at 0). For example, if set to `2` and using a sampler like Bogacki that calls the model two extra times, the first will never be cached. The default value of `1` disabling caching for the first model call per substep. I generally would not recommend setting it to `0`, especially with `average` or `sample` merge strategies. -* `model_call_cache_max_use`(`1000000`): The number of times cache items can be re-used. The default is effectively no limit. Where would this be useful? Let's say you're using the `average` merge strategy and a multi step sampler that calls the model at least one more time with 50 substeps. If you set the value to `25`, the model cache result will be updated around substep 25 which _may_ produce better results than reusing the result 50 times. - -Since it's kind of confusing even for me, a little more explanation: The model call cache caches results for model call indexes between `model_call_cache_threshold` and `model_call_cache - 1`. If you set `model_call_cache_threshold` to `0` and `model_call_cache` to `1` then only the first model call will be cached. If you set `model_call_cache_threshold` to `1` and `model_call_cache` to `2` then call 0 will not be cached, call 1 will be cached, call 2 will be cached, call 3 will not be cached, and so on. - -#### Merging - -When running multiple substeps per step, the results will combined based on the merge strategy. Possible strategies (in order of least weird to most weird): - -* `divide`: Creates a linear schedule between the current sigma and the next and runs the substeps in sequence. The model is called at least once per substep. -* `normal`: The model is called at least once per substep (and possibly additional times for higher order samplers). The result of each substep is noised and the next substep uses that result. Then all the results are averaged. -* `average`: The model is called once at the beginning of the step and substeps share that result (but it may be called additional times for higher order samplers). This means substeps for samplers like reversible Euler, Heun 1s, DPM++ 2m SDE are essentially free. May be theoretically very unsound and inaccurate, requires manual tweaking of settings like `s_noise`. Supports the parameter `avgmerge_stretch`(`0.4`) which basically rolls back the current sigma and adds some noise (otherwise running a substep is deterministic and there would be no point to running a sampler like Euler more than once). -* `sample`: Like `average` (and uses `avgmerge_stretch`) but instead of simply using the average, it does a sampler step toward that instead. You can plug in any substep sampler to the `merge_sampler_opt` input (if unconnected and the merge method is `sample` then Euler will be used). *Note*: Substeps in the attached sampler will be ignored. -* `sample_uncached`: Similar to `sample`, however it calls the model per substep instead of caching the result and sharing it. Aside from sampling toward the result, it works more like the `normal` merge strategy. Theoretically it should be better because it's taking less shortcuts but results seem worse. - -When using `average` and `sample` merge strategies and with model call caching enabled you can get away with setting substeps super high. Running something like 100 substeps is actually quite practical and seems to work well. - -### ComposableStepSampler - -This node has a text input for YAML (or JSON) advanced parameters. - -For example, you could enter something like this in the field: - -```yaml -reta: 1.1 -leap: 3 -dyn_deta_mode: "deta" -``` - -**Possible Parameters** - -#### General - -* `eta`(`1.0`): Will override `eta` in the node if set. -* `dyn_eta_start`(`unset`) and `dyn_eta_end`(`unset`): No effect unless both values are set. Will interpolate between start and end based on the percentage of sampling. *Note*: This is a factor applied to ETA, not a flat value. -* `s_noise`(`1.0`): Will override `s_noise` in the node if set. -* `solver_type`(`midpoint`): Applies to DPM++ 2m SDE. May be one of `midpoint` or `heun` (`midpoint` is generally recommended). - -#### Reversible - -* `reta`(`1.0`): Reverse ETA. -* `dyn_reta_start`(`unset`) and `dyn_reta_end`(`unset`): No effect unless both values are set. Will interpolate between start and end based on the percentage of sampling. *Note*: This is a factor applied to RETA, not a flat value. - -#### Dancing - -* `leap`(`2`): Distance to try to leap forward. If you set `leap` to `1` you just get plain old Euler ancestral. -* `deta`(`1.0`): ETA used for dance steps. -* `dyn_deta_start`(`unset`) and `dyn_deta_end`(`unset`): No effect unless both values are set. Will interpolate between start and end based on the percentage of sampling. *Note*: This is a factor applied to DETA, not a flat value. -* `dyn_deta_mode`(`lerp`): May be one of: - * `deta`: Scales `deta` based on the value from `dyn_deta_start/end`. - * `lerp`: Does the dance step according to `deta` and then LERPs the non-dance sample result with the dance sample result based on the scale calculated from `dyn_deta_start/end` (which is `1.0` if they are unset). For example, if the dance scale is `0.5` you will get 50% normal sampling, 50% dancing sampling. - * `lerp_alt`: Similar to `lerp` except it LERPs with the leap result instead of a normal Euler ancestral result. - -#### RES - -* `res_simple_phi`(`false`): Uses a faster but possibly less accurate method for calculating phi. What does phi do? I haven't the foggiest! -* `res_c2`(`0.5`): Solver partial step size, the default of `0.5` appears to use the midpoint. Setting it to a lower value might possibly be more accurate but slower? - -#### TTM JVP - -`alterate_phi_2_calc`(`true`): Supposedly works better than disabled when ETA isn't 0. I didn't notice a difference. - -**Note**: TTM is a weird sampler. If you're using model caching you must make sure the entries TTM uses are populated first (by having before any other samplers that call the model multiple times). It may also not work with some other model patches and upscale methods. ## Credits I can move code around but sampling math and creating samplers is far beyond my ability. I didn't write any of the original samplers: -* Euler, DPMPP SDE, DPMPP 2S, DPM++ 2m, 2m SDE and 3m SDE samplers based on ComfyUI's implementation. +* Euler, Heun++2, DPMPP SDE, DPMPP 2S, DPM++ 2m, 2m SDE and 3m SDE samplers based on ComfyUI's implementation. * Reversible Heun, Reversible Heun 1s, RES, Trapezoidal, Bogacki, Reversible Bogacki, RK4 and Euler Dancing samplers based on implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers * TTM JVP sampler based on implementation written by Katherine Crowson (but yoinked from the Extra-Samplers repo mentioned above). +* IPNDM, IPNDM_V and DEIS adapted from https://github.com/zju-pi/diff-sampler/blob/main/diff-solvers-main/solvers.py (I used the Comfy version as a reference). * Normal substep merge strategy based on implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +* Immiscible noise processing based on implementation from https://github.com/kohya-ss/sd-scripts/pull/1395 and idea for sampling with it from https://github.com/Clybius +* Precedence climbing (Pratt) expression parser based on implementation from https://github.com/andychu/pratt-parsing-demo -Thanks! +This repo wouldn't be possible without building on the work of others. Thanks! + +## Usage + +First, a note on the basic structure: + +![Basic nodes example](assets/basic_sampling.png) + +The sampler node connects to a group node. You can chain group nodes, however only one can match per step. Checking for a match starts at the group furthest from the sampler node. So if you have `Group1 -> Group2 -> Group3 -> Sampler`, the groups will be tried starting from `Group1`. Currently matching groups is only time based. + +You will then connect a substeps node to the group. These can also be chained and like groups, execution starts with the node furthest from the group. I.E.: `Substeps1 -> Substeps2 -> Substeps3 -> Group` will start with `Substeps1`. + +Most of these nodes have a text parameter input (YAML format - JSON is also valid YAML so you can use that instead if you prefer) and a parameter input. The parameter input can be used to specify stuff like custom noise types. + +You may use filters and expressions in the text parameter input. See: + +* [Filters](docs/filter.md) +* [Expressions](docs/expression.md) + +## Nodes + +### `OCS Sampler` + +The main sampler node, with an output suitable for connecting to a `SamplerCustom`. This node has builtin support for Restart sampling, if you are +using Restart don't use the `RestartSampler` node. + +You can connect a chain of `OCS Group` nodes to it and it will choose one per step (based on conditions like time). + +#### Input Parameters + +* `restart_custom_noise`: Value type: `SONAR_CUSTOM_NOISE`. Allows specifying a custom noise type when used with Restart sampling. + +#### Text Parameters + +Shown in YAML with default values. + +
+★★ Expand ★★ + +```yaml +# Noise scale. May not do anything currently. +s_noise: 1.0 + +# ETA (basically ancestralness). May not do anything currently. +eta: 1.0 + +# Reversible ETA (used for reversible samplers). May not do anything currently. +reta: 1.0 + +# Parameters related to restart sampling. +restart: + # Scales the noise added by restart sampling. + s_noise: 1.0 + # Immiscible block same as described below. + immiscible: + size: 0 + + +# The noise block allows defining global noise sampling parameters. +noise: + # You can disable this to allow GPU noise generation. I believe it only makes a difference for Brownian. + cpu_noise: true + + # ComfyUI has a bug where if you disable add_noise in the sampler, no seed gets set. If you + # are manually noising a sample and have add_noise turned off then you should enable this if + # you want reproducible generations. + set_seed: false + + # Global scale scale for generated noise + scale: 1.0 + + # Whether the generated noise should be normalized before use. Generally a good idea to leave enabled. + normalize_noise: true + + # Dimensions to normalize over (when normalization is enabled). Negative values mean starting + # from the end (i.e. -1 means the last dimension, -2 means the penultimate dimension). + # Latents generally have these dimensions: batch, channels, height, width + # The default of [-3, -2, -1] normalizes noise over the batch. You can try something like + # [-2, -1] to normalize over the batch and channels. See: https://pytorch.org/docs/stable/generated/torch.std.html + normalize_dims: [-3, -2, -1] + + # When caching, the batch size for chunks of noise to generate in advance. Generating a batch of noise + # can be more efficient than generating on demand when using a high number of substeps (>10) per step. + batch_size: 32 + + # Whether to cache noise. + caching: true + + # Interval (in full steps) to reset the cache. Brownian noise takes time into account so + # if using Brownian you will generally want to reset each step. + cache_reset_interval: 1 + + # Immiscible noise processing, see: https://arxiv.org/abs/2406.12303 + immiscible: + # Batch size, 0 disables. + size: 0 + + # Reference mode, values can be one of: + # x: Uses the current latent as a reference. + # noise: Uses the current noise as a reference (x - denoised) + # denoised: Uses the model image prediction as a reference (factors in positive and negative prompts). + # uncond: The model unconditional prediction (negative prompt) + # cond: The model conditional prediction (positive prompt) + # Advanced feature: Additionally you may enter a string of operations in the format: + # "x - denoised * 2 + cond" (just an example, not a recommended setting) + # Possible operations: + - / * min max add sub div mul + # Note: Each value and operation must be space delimited (i.e. "x-1" will not work). + # Also normal operator precedence does not apply here. + ref: default + + # Batching mode, one of: + # batch: Matches vs batches. Immiscible mode is disabled if size < 2 + # channel: Splits the batch into a list of channels and matches against those. + # row: Splits the batch into a list of rows and matches against those. + # column: Splits the batch into a list of columns and matches against those. + # Note: Requires reshaping both the noise and x, may be slow and consume + # a lot of VRAM. + batching: channel + + # Scale for reference latent. Can be negative. + scale_ref: 1.0 + + # Allows normalizing the reference. If this is a list, you can specify the dimensions to + # normalize. See normalize_dims above and https://pytorch.org/docs/stable/generated/torch.std.html + normalize_ref: false + + # The proportion of immiscible-ized noise. + # You get (immiscible_noise * strength) + ((1.0 - strength) * normal_noise) - LERP. + strength: 1.0 + + # See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html#scipy.optimize.linear_sum_assignment + maximize: false + + filter: null + + +# Model calls can be cached. This is very experimental: I don't recommend using it +# unless you know what you're doing. +model: + cache: + # The cache size. + size: 0 + + # Threshold for model call caching. For example if you have size=3 and threshold=1 + # then model calls 1 through 3 will be cached, but model call 0 will not be (the first one). + # Additional explanation: Some samplers call the model multiple times per step. For example, + # Bogacki uses three model calls: 0, 1, 2 + threshold: 1 + + # Maximum use count for cache items. + max_use: 1000000 + + filter: + input: null + denoised: null + jdenoised: null + cond: null + uncond: null +``` + +

+ +Any parameters you don't specify will use the defaults. For example if your text parameter block is: + +```yaml +noise: + cpu_noise: false +``` + +Then the rest of the parameters will use the defaults shown above. + +### `OCS Group` + +Defines a group of substeps. + + +#### Merging + +When running multiple substeps per step, the results will combined based on the merge strategy. Possible strategies (in order of least weird to most weird): + +* `simple`: Doesn't merge anything: only runs a single substep per step. +* `divide`: Creates a linear schedule between the current sigma and the next and runs the substeps in sequence. The model is called at least once per substep. +* `normal`: The model is called at least once per step (and possibly additional times for higher order samplers). Each substep shares the first model call result. The results are averaged together. *Note*: Since the first model call is shared and the initial input is the same for each substep, there is no point in running multiple identical substeps. Also note: This merge strategy doesn't work well with non-ancestral samplers (i.e. dpmpp_2m or any sampler with `eta: 0`). +* `overshoot`: The model is called at least once per step. It will sample steps equal to the number of substeps, starting from the current step. Then it will restart back to the expected step. + + +#### Node Parameters + +* `merge_method`: One of the merge methods described above in the Merging section. +* `time_mode`(`step`): One of `step`, `step_pct`, `sigma`. Time matching mode. Matching based on steps generally will be simplest. Matches are inclusive and steps start at 0 (so step 0 is the first step). `step_pct` is the percentage of total steps (1.0=100%, 0.5=50%, etc). +* `time_start`(`0`): Match start time. +* `time_end`(`999`): Match end time. + +Example: + +![Group time filter example](assets/group_time_example.png) + +The left side group matches steps 0, 1, 2. The right side group matches all steps. This setup will use whatever substeps are connected to the first group for the first three steps and the second group will handle the rest. + + +#### Input Parameters + + + +* `restart_custom_noise`: Currently only used by the `overshoot` merge method. + +#### Text Parameters + +Shown in YAML with default values. + +
+★★ Expand ★★ + +```yaml +# Noise scale. May not do anything currently. +s_noise: 1.0 + +# ETA (basically ancestralness). May not do anything currently. +eta: 1.0 + +# Reversible ETA (used for reversible samplers). May not do anything currently. +reta: 1.0 + +# Expression. +when: null + +# Interpolate the schedule by the specified factor. Only used by the overshoot merge method. +#: Example if factor 2 and steps [0,1,2] you'd get [0, 0.5, 1.0, 1.5, 2] +overshoot_expand_steps: 1 + +# Only used by the overshoot merge method currently. +restart: + # Scales the noise added by restart sampling. + s_noise: 1.0 + # Immiscible block same as described above. + immiscible: + size: 0 + +pre_filter: null + +post_filter: null +``` + +
+ +### `OCS Substeps` + +#### Step Methods (Samplers) + +In alphabetical order. + +* `adapter`: Wraps a normal ComfyUI `SAMPLER`. (Attach a `SAMPLER` parameter to the node.) +* `bogacki`: +* `deis`: See parameters: `history_limit` +* `dpmpp_2m_sde`: See parameters: `history_limit` +* `dpmpp_2m`: `eta` and `s_noise` parameters are ignored. See parameters: `history_limit` +* `dpmpp_2s` +* `dpmpp_3m_sde`: See parameters: `history_limit` +* `dpmpp_sde` +* `euler_cycle`: See parameters: `cycle_pct` +* `euler_dancing`: Pretty broken currently, will probably require increased `s_noise` values. See parameters: `deta`, `leap`, `deta_mode` +* `euler`: +* `heun`: Alternate Heun implementation. Supports reversible parameters. See parameters: `history_limit` +* `heun_1s`: Alternate Heun one step implementation. Supports reversible parameters. +* `heunpp`: See parameters: `max_order` +* `ipndm_v`: See parameters: `history_limit` +* `ipndm`: See parameters: `history_limit` +* `res` +* `reversible_bogacki`: +* `reversible_heun`: +* `reversible_heun_1s`: See parameters: `history_limit` +* `rk4`: +* `solver_diffrax`: Uses the [Diffrax](https://github.com/patrick-kidger/diffrax) solver backend. See `de_*` parameters below. +* `solver_torchdiffeq`: Uses the [torchdiffeq](https://github.com/rtqichen/torchdiffeq) backend. See `de_*` parameters below. +* `solver_torchode`: Uses the [torchode]((https://github.com/martenlienen/torchode)) backend. See `de_*` parameters below. +* `solver_torchsde`: Uses the [torchsde](https://github.com/google-research/torchsde) backend. See `de_*` parameters below. +* `trapezoidal`: +* `trapezoidal_cycle`: See parameters: `cycle_pct` +* `ttm_jvp`: TTM is a weird sampler. If you're using model caching you must make sure the entries TTM uses are populated first (by having it run before any other samplers that call the model multiple times). It may also not work with some other model patches and upscale methods. See parameters: `alternate_phi_2_calc` + +**Sampler Feature Support** + +|Name|Cost|History|Order|Reversible|CFG++| +|-|-|-|-|-|-| +|`adapter`|?|?|?|?|?| +|`bogacki`|2||||| +|`deis`|1|1-3 (1)|||| +|`dpmpp_2m_sde`|1|1|||| +|`dpmpp_2m`|1|1|||| +|`dpmpp_2s`|2||||| +|`dpmpp_3m_sde`|1|1-2 (2)|||| +|`dpmpp_sde`|2||||| +|`euler_cycle`|1||||X| +|`euler_dancing`|1||||| +|`euler`|1||||X| +|`heun`|2|||X|| +|`heun_1s`|1|1||X|| +|`heunpp`|1-3||X||| +|`ipndm_v`|1|1-3 (1)|||| +|`ipndm`|1|1-3 (1)|||| +|`res`|2||||| +|`reversible_bogacki`|2|||X|| +|`reversible_heun`|2|||X|| +|`reversible_heun_1s`|1|1||X|| +|`rk4`|4||||| +|`solver_diffrax`|variable||||| +|`solver_torchdiffeq`|variable||||| +|`solver_torchode`|variable||||| +|`solver_torchsde`|variable||||| +|`trapezoidal`|2||||| +|`trapezoidal_cycle`|2||||| +|`ttm_jvp`|2||||| + + +`deis`, `ipndm*` do not seem to work well with ancestralness, I recommend `eta: 0.25` or disable it completely. + +**Solver Backend Samplers**: + +You will need to have the relevant Python package installed in your venv to use these. TDE cannot handle batches and +each batch item will be evaluated separately. Using `tode` may be faster for batch sizes over 1. + +`ode_solver` types for TDE: adaptive: `dopri8`, `dopri5`, `bosh3`, `fehlberg2`, `adaptive_heun`, fixed step: `euler`, `midpoint`, `rk4`, `explicit_adams`, `implicit_adams` + +`ode_solver` types for TODE: adaptive only: `dopri5`, `tsit5`, `heun`. I haven't much luck with anything other than `dopri5`. + +Note that adaptive solvers may be _very_ slow. Think along the lines of 20-100 model calls per substep (or in other words, the equivalent for running that many `euler` steps). Tolerances only apply to adaptive solvers. + +**Cycle Samplers** (`euler_cycle`, `trapezoidal_cycle`) + +Basically a different approach to ancestral sampling. First a crash course on how sampling works: + +Each step has an expected noise level, with the first step generally being pure noise and the end of the last step aiming to end with no noise remaining. Let's say the image on the current step is called `x`, calling the model with `x` gives us a prediction of what the image looks like with all noise removed (`denoised`), however the model is not capable of just removing all the noise in a single step: its prediction will be imprecise. `x - denoised` leaves us with just the noise (we subtract the prediction which theoretically has no noise from the noisy sample). This is a very simplified, but the idea is basically to add the noise back into `denoised`, but scaled so that it matches the amount of noise expected on the _next_ step. `denoised + noise * expected_noise_at_next_step`. + +When doing ancestral sampling, we actually _overshoot_ expected noise for the next step and add less than that amount back to `denoised`. Then we generate some of our own noise and add it, scaled so that the result matches `expected_noise_at_next_step`. `eta` controls how the scale of the overshoot. + +The difference with cycle is that instead of adding `noise * expected_noise_at_next_step`, we instead first add `noise * (expected_noise_at_next_step * (1.0 - cycle_pct))` and then we generate noise and scale it to `cycle_pct` and add it too. Just for example, suppose `cycle_pct` is `0.2`: we'll add 80% of the expected noise at the next step (`1.0 - 0.2 == 0.8`) and then generate the remaining 20% and add it in to meet the expected amount. I don't recommend setting `cycle_pct` to values over `0.5`, especially if using "weird" noise types. + +#### Node Parameters + +* `substeps`(`1`): Number of substeps. Generally involves a model call per substep, so for example setting this to 4 would approximately quadruple sampling time. +* `step_method`(`euler`): Method used for sampling the substeps. May include a parenthesized number (i.e. `rk4 (3)`) which denotes the number of _extra_ model calls required per sample. At least one is always required. So `euler` requires 1 in total, `rk4` requires 4 in total. RK4 is about 4 times slower than `euler`. + +#### Input Parameters + +* `custom_noise`: Value type: `SONAR_CUSTOM_NOISE`. Allows specifying a custom noise type for samplers that generate noise (most of them). + +#### Text Parameters + +Shown in YAML with default values. + +
+★★ Expand ★★ + +```yaml +# Scale for added noise. +s_noise: 1.0 + +# ETA (basically ancestralness). +eta: 1.0 +# No effect unless both start and end are set. Will scale the eta value based on the +# percentage of sampling. In other words, eta*dyn_eta_start at the beginning, +# eta*dyn_eta_end at the end. +dyn_eta_start: null +dyn_eta_end: null + +# alt CFG++ scale (see https://cfgpp-diffusion.github.io/) +# Based on the initial incorrect ComfyUI implementation, but it seems to +# produce decent results sometimes. +# Can also be set to a negative value (I don't recommend going lower than -0.5). +alt_cfgpp_scale: 0 + +# CFG++ (see https://cfgpp-diffusion.github.io/) +cfgpp: false + +### Reversible Settings ### + +# Reversible ETA (used for reversible samplers). +reta: 1.0 +# Scale of the reversible correction. Can also be set to a negative value. +reversible_scale: 1.0 +# No effect unless both start and end are set. Will scale the reta value based on the +# percentage of sampling. In other words, reta*dyn_reta_start at the beginning, +# reta*dyn_reta_end at the end. +dyn_reta_start: null +dyn_reta_end: null + +pre_filter: null + +post_filter: null + +### ODE Sampler Settings ### + +# Solver type. +de_solver: dopri5 # Example - varies based on solver sampler. +# Relative tolerance (log 10) +de_rtol: -1.5 +# Absolute tolerance (log 10) +de_atol: -3.5 +# Max model calls allowed to compute the solution. If the limit is exceeded, it is an error. +de_max_nfe: 1000 +# Min sigma to sample to. If the current step start <= min sigma, then the sampler will run +# a Euler step. If the current step end <= min sigma then the slover will sample to the min +# sigma and then to a Euler step from min sigma for the rest. +de_min_sigma: 0.0292 +# Hack that seems to help results by stretching the down sigma a bit. Set to 0 to disable. +de_fixup_hack: 0.025 + +# Used to split the step into sections. Useful for fixed step methods. +# Applies to: solver_torchode, solver_diffrax +de_split: 1 + +# Initial step size (as a percentage). +# Applies to: solver_torchode, solver_diffrax +de_initial_step: 0.25 + +# Coefficients for the step size PID controller. +# See https://en.wikipedia.org/wiki/Proportional%E2%80%93integral%E2%80%93derivative_controller +# These values seem okay with dopri5. +# Applies to: solver_tode, solver_diffrax +de_ctl_pcoeff: 0.3 +de_ctl_icoeff: 0.9 +de_ctl_dcoeff: 0.2 + +# Controls whether to compile the solver. May or may not work, +# also may or may not be a speed increase as the compiled solver is +# not cached between substeps. +# Applies to: solver_torchode +tode_compile: false + +### torchsde solver specific parameters. +tsde_noise_type: "scalar" +tsde_sde_type: "stratonovich" +tsde_levy_area_approx: "none" +tsde_noise_channels: 1 +tsde_g_multiplier: 0.05 +tsde_g_reverse_time: true +tsde_g_derp_mode: false +tsde_batch_channels: true + +### diffrax solver specific parameters. + +# Turns on adaptive stepping. When enabled, de_split is not used. +# When disabled, it may be desirable to set de_split. +diffrax_adaptive: false +# Hack to make some solver methods work. May not be safe. +diffrax_fake_pure_callback: true +# Some diffrax methods don't allow adaptive stepping, enabling this +# makes them usable although it's less efficient (3x cost, 2x accuracy). +diffrax_half_solver: false +diffrax_batch_channels: false +# Some solvers require specific types of Levy area approximation. +# See: https://docs.kidger.site/diffrax/api/brownian/#levy-areas +diffrax_levy_area_approx: "brownian_increment" +# Some solvers may require manually specifying the error order. +diffrax_error_order: null +# Enables SDE mode (and SDE-specific solvers). May not be worth using. +diffrax_sde_mode: false +# Noise multiplier when SDE mode is enabled. +diffrax_g_multiplier: 0.0 +# Only applies when time scaling is enabled. Reverses time. +diffrax_g_reverse_time: false +# Scales the g multiplier based on the current time. +diffrax_g_time_scaling: false +# Experimental option to flip the sign on the g multiplier when time >= half the step. +# i.e. if you'd get 1,2,3,4 as g values for the step, with this it would be 1,2,-3,-4. +diffrax_g_split_time_mode: false + + +### Other Sampler Specific Parameters ### + +# Used for some samplers that use history from previous steps. +# List of samplers and default value below: +# dpmpp_2m: 1 +# dpmpp_2m_sde: 1 +# dpmpp_3m_sde: 2 +# reversible_heun_1s: 1 +# ipndm: 1 (max 3) +# ipndm_v: 1 (max 3) +# deis: 1 (max 3) +history_limit: 999 # Varies based on sampler. + +# Used for some samplers with variable order. List of samplers and default value below: +# heunpp2: 3 +max_order: 999 # Varies based on sampler. + +# Used for dpmpp_2m. One of midpoint, heun +solver_type: "midpoint" + +# Coefficients mode for DEIS. One of tab or rhoab. +deis_mode: "tab" + +# Used for samplers with cycle in the name. Controls how much noise is cycled per step. +cycle_pct: 0.25 + +# Used for ttm_jvp. Supposed works better when ETA > 0 +alternate_phi_2_calc: true + +# Parameters for dancing samplers: +# Number of steps to leap ahead. +leap: 2 +# ETA for dance steps +deta: 1.0 +# dyn_deta works the same as dyn_eta/reta. See above. +dyn_deta_start: null +dyn_deta_end: null +# One of lerp, lerp_alt, deta +dyn_deta_mode: "lerp" +``` + +
+ +### `OCS SimpleRestartSchedule` + +Generates a restart schedule. + +#### Node Parameters + +* `start_step`: 0-based first step for the restart schedule to apply. + +#### Input Parameters + +* `sigmas`: Sigmas to restartify. Output from any normal schedule node. + +#### Text Parameters + +JSON or YAML schedule in list form. + +```yaml +- [4, -3] +- [2, -1] +- 1 +``` + +Each item should be one of: + +* A pair `[interval, jump]` - after `interval` steps, make a relative jump of `jump` steps. +* A single integer `schedule_index`: resume the schedule at the specified 0-based index. + +The example above means: +1. After 4 steps, jump back 3 steps. +2. After 2 steps, jump back one step. +3. Go to the second item (after 2 steps, jump back one step). + +The node `start_step` parameter is effectively the same as `[start_step, 0]` as a schedule item. diff --git a/__init__.py b/__init__.py index f7e3aba..4ce7f18 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,12 @@ from .py import nodes NODE_CLASS_MAPPINGS = { - "ComposableSampler": nodes.ComposableSampler, - "ComposableStepSampler": nodes.ComposableStepSampler, + "OCS Sampler": nodes.SamplerNode, + "OCS Substeps": nodes.SubstepsNode, + "OCS Group": nodes.GroupNode, + "OCS Param": nodes.ParamNode, + "OCS MultiParam": nodes.MultiParamNode, + "OCS ModelSetMaxSigma": nodes.ModelSetMaxSigmaNode, + "OCS SimpleRestartSchedule": nodes.SimpleRestartSchedule, } __all__ = ["NODE_CLASS_MAPPINGS"] diff --git a/assets/basic_sampling.png b/assets/basic_sampling.png new file mode 100644 index 0000000000000000000000000000000000000000..25830fbb3db82066045b47292d54b71825e35e9c GIT binary patch literal 49070 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%0D<$B=+x?wa_xOARnTc8}%SE>>n0P2?-7 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, `<`, `>=`, `<=` + +Operator precedence should generally work the way you'd expect. + +You may surround a function name with backticks to turn it into a binary operator (only for functions that take two arguments). + +Functions are called via `name(param1, param2)`. Keyword arguments may be passed using the `:>` operator. Example: +`name(param1, key :> 123, key2 :> otherfunction(10))`. + +Symbols (simple string type) are defined using `'symbol_name` - note the solitary single quote. They may not contain spaces. + +`;` can be used to sequence operations. I.E. `exp1 ; exp2` evaluates `exp1`, then `exp2` and then result of the expression is whatever `exp2` returned. + +Like Python, a parenthesized expression with a trailing comma can be used to create an empty tuple. Example: `(1,)` + +## Filter Variables + +Indexes like `step` are zero-based: `0` will be the first step. + +### Basic Variables + +* `default`: Context specific default value. i.e. if used in an `input` expression this would be `x`, if used for `output` this would be the current result. +* `step`: Current step. +* `substep`: Current substep. +* `dt`: `sigma_next - sigma` +* `sigma_idx`: Index of the current sigma. Note that when using restarts this will be based on the restart sigma chunks, not full sigma list. +* `sigma`: The current sigma. +* `sigma_next`: The next sigma. +* `sigma_down`: The down sigma in ancestral sampling. +* `sigma_up`: The up sigma in ancestral sampling. +* `sigma_prev`: The previous sigma (may be `None`). +* `hist_len`: Current available history length. "Now" counts as one. +* `sigma_min`: The minimum sigma (based on the full list). +* `sigma_max`: The maximum sigma (based on the full list). +* `step_pct`: Percentage for the current step (based on total steps). +* `total_steps`: Total steps to be sampled. + +### Extended Variables + +* `denoised`: From the current step or substep. May not be available in model `input` or group `pre_filter`. +* `cond`: From the current step or substep. May not be available in model `input` or group `pre_filter`. +* `uncond`: From the current step or substep. May not be available in model `input` or group `pre_filter`. +* `denoised_prev`: Only available when model history exists. +* `cond_prev`: Only available when model history exists. +* `cond_prev`: Only available when model history exists. + +### Model Filter Variables + +* `model_call`: Only applicable to `model` filters, will be the model call index. I.E. if the sampler calls the model three times, the filter would be called with model call indexes `0`, `1` and `2`. + +Available in model filters, with the exception of the `input` filter. + +* `denoised_curr` +* `cond_curr` +* `uncond_curr` + +## Basic Expression Functions + + | | Name | Input | Output | + | :--- | :--- | :--- | :--- | + |⬤| `all` | `B`\* | `B` | + | Evaluates to true if all its arguments evaluate to true.
**Example:** `all(x > 1, y < 1)` | + |⬤| `any` | `B`\* | `B` | + | Evaluates to true if any of its arguments evaluate to true.
**Example:** `any(x > 1, y < 1)` | + |⬤| `between` | value:`N`, from:`N`, to:`N` | `B` | + | Boolean range checking.
**Example:** `between(value, low, high)` | + |⬤| `comment` | `*` | `null` | + | Ignores any arguments passed to it (they won't be evaluated at all but must parse as a valid expression) and returns `None` | + |⬤| `dict` | `*`* | `dict` | + | Constructs a dictionary from its keyword arguments. _Note_: You may not pass positional arguments.
**Example:** `dict(key1 :> value1, keyN :> valueN)` | + |⬤| `get` | name:`SY`, fallback:`*` | `*` | + | Returns a variable if set, otherwise the fallback.
**Example:** `get('somevar, 123)` | + |⬤| `if` | condition:`B`, then:`*`, else:`*` | `*` | + | Conditional expressions.
**Example:** `if(condition, true_expression, false_expression)` | + |⬤| `index` | index:`IDX`, value:`S \| T` | `*` | + | Index function. | + |⬤| `is_set` | name:`SY` | `B` | + | Tests whether a variable is set. | + |⬤| `max` | values:`SN` | `N` | + | Maximum operation. _Note_: Takes one sequence argument.
**Example:** `min((1, 2, 3))` | + |⬤| `min` | values: `SN` | `N` | + | Minimum operation. _Note_: Takes one sequence argument.
**Example:** `max((1, 2, 3))` | + |⬤| `mod` | lhs:`N`, rhs:`N` | `N` | + | Modulus operation:
**Example:** `mod(5, 2)` | + |⬤| `neg` | `N` | `N` | + | Negation.
**Example:** `neg(2)` | + |⬤| `not` | `B` | `B` | + | Boolean negation | + |⬤| `s_` | start:`I(null)`, end:`I(null)`, step:`I(null)` | `slice` | + | Creates a slice object from the `start`, `end`, `step` values. See Numpy [s_](https://numpy.org/doc/stable/reference/generatednumpy.s_.html) | + |⬤| `unsafe_call` | `callable`, `*`\* | `*` | + | Allows calling an arbitrary callable.
**Example:** `unsafe_call(some_callable, arg1, arg2, kwarg1 :> 123)` + +**Legend**: `B`=boolean, `N`=numeric, `NS`=scalar numeric, `I`=integer, `F`=float, `T`=tensor, `S`=sequence, `SN`=numeric sequence, `SY`=symbol, `*`=any -- parenthized values indicate argument defaults. `*` following the type indicates variable length arguments. For functions that take keyword arguments, the type will be written like "_name: `TYPE(default_value)`_". + +## Tensor Expression Functions + +*Tensor dimensions hint*: Most tensors you'll be dealing with are laid out as `batch`, `channels`, `height`, `width`. Negative indexes start from the end, so dimension `-1` would mean _width_ just the same as `3`. + + | | Name | Input | Output | + | :--- | :--- | :--- | :--- | + |⬤| `t_bleh_enhance` | tensor:`T`, mode:`SY`, scale:`N(1.0)` | `T` + | Available if you have the [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) node pack installed. See [Filtering](filter.md#bleh_enhance).
**Example:** `bleh_enhance(some_tensor, 'bandpass, 0.5)` | + |⬤| `t_blend` | tensor1:`T`, tensor2:`T`, scale:`N(0.5)`, mode:`SY(lerp)` | `T` | + | Tensor blend operation.
**Example:** `t_blend(t1, t2, 0.75, 'lerp)` | + |⬤| `t_contrast_adaptive_sharpening` | tensor:`T`, scale:`N(0.5)` | `T` | + | Contrast adaptive sharpening. _Note_: Not recommended to call on noisy tensors (so `denoised` but probably not `x`).
**Example:** `t_contrast_adaptive_sharpening(some_tensor, 0.1)` | + |⬤| `t_flip` | tensor:`T`, dim:`NS`, mirror:`B(false)` | `T` | + | Flips a tensor on the specified dimension. If the third argument is true, it will be mirrored around the center in that dimension.
**Example:** `t_flip(some_tensor, -1, true)` | + |⬤| `t_mean` | tensor:`T`, dim:`SN(-3, -2, -1)` | `T` | + | Tensor mean, second argument is dimensions.
**Example:** `t_mean(some_tensor, (-2, -1))` | + |⬤| `t_noise` | tensor:`T`, type:`SY(gaussian)` | `T` | + | Generates un-normalized noise (use `t_norm` if you want to normalize it). If you have ComfyUI-sonar you can use any noise type that supports, otherwise only `gaussian`. The generated noise will have the same shape as the supplied tensor (hopefully, may not be true for every exotic noise type but at least should be broadcastable to the tensor).
Example: `t_noise(some_tensor, 'pyramid)` | + |⬤| `t_norm` | tensor:`T`, factor:`N(1.0)`, dim:`SN(-3, -2, -1)` | `T` | + | Tensor normalization (subtracts mean, divides by std).
**Example:** `t_norm(some_tensor, 1.0, (-2, -1))` | + |⬤| `t_sonar_power_filter` | tensor:`T`, filter:`dict` | `T` | + | Available if you have [ComfyUI-sonar](https://github.com/blepping/ComfyUI-sonar) installed. See [Filtering](filter.md#sonar_power_filter). Constructs a power filter from a dictionary argument. _Note_: May be slow as the filter is reconstructed on every evaluation.
**Example:** `t_sonar_power_filter(some_tensor, dict(alpha :> 0.1, min_freq :> 0.2, max_freq :> 0.6))` | + |⬤| `t_roll` | tensor:`T`, amount:`NS(0.5)`, dim:`SN((-2,))` | `T` | + | Rolls a tensor along the specified dimensions. If amount is >= -1.0 and < 1.0 this will be interpreted as a percentage.
**Example:** `t_roll(some_tensor, 10, (-2,))` | + |⬤| `t_scale` | tensor:`T`, scale:`SN \| NS`, mode:`SY(bicubic)`, absolute_scale:`B(false)` | `T` | + | Scales a tensor. If scale is a tuple, it will be interpreted as `(height, width)`. When `absolute_scale` is not set, the scales will be interpreted as percentages otherwise absolute values will be used.
Example: `t_scale(some_tensor, (0.75, 0.5), 'bilinear)` | + |⬤| `t_std` | tensor:`T`, dim:`SN(-3, -2, -1)` | `T` | + | Tensor std, second argument is dimensions.
**Example:** `t_std(some_tensor, (-2, -1))` | + |⬤| `unsafe_tensor_method` | `T`, `SY`, `*`\* | `*` | + | Unsafe tensor method call. See note below.
**Example:** `unsafe_tensor_method(some_tensor, 'mul, 10)` | + |⬤| `unsafe_torch` | path:`SY` | `*` | + | Unsafe Torch module attribute access. See note below.
**Example:** `unsafe_torch('nn.functional.interpolate)` | + +**Note on `unsafe_tensor_method` and `unsafe_torch`**: These functions are disabled by default. If the environment variable `COMFYUI_OCS_ALLOW_UNSAFE_EXPRESSIONS` is set to anything then you can use `unsafe_tensor_method` with a whitelisted set of methods (best effort to avoid anything actually unsafe). If the environment variable `COMFYUI_OCS_ALLOW_ALL_UNSAFE` is set to anything then `unsafe_torch` is enabled and `unsafe_tensor_method` will allow calling any method. ***WARNING***: Allowing _all_ unsafe with workflows you don't trust is _not_ recommended and a malicious workflow will likely have access to anything ComfyUI can access. It is effectively the same as letting the workflow run an arbitrary script on your system. diff --git a/docs/filter.md b/docs/filter.md new file mode 100644 index 0000000..c7a0e9f --- /dev/null +++ b/docs/filter.md @@ -0,0 +1,187 @@ +# OCS Filters + +Filters allow changing sampler inputs/outputs, model input/outputs, generated noise and so on. They are +configured by the advanced YAML/JSON parameter block in the node. + +## Filter Support + +### `OCS Substeps` + +Set via the `pre_filter` and `post_filter` keys. + +### `OCS Group` + +Set via the `pre_filter` and `post_filter` keys. + +*Note*: Since the group pre-filter may be called before any model calls, variables like `denoised` may not be available +in expressions. + +### `OCS Sampler` + +**Noise** + +```yaml +noise: + # Or set to a valid filter definition. + filter: null +``` + +**Model** + +*Note*: Since the model filters may be called before any other model calls, variables like `denoised` may not be available +in expressions. With the exception of the `input` filter you will have access to `denoised_curr`, `cond_curr`, etc. +See [Expressions](expression.md#model-filter-variables). + +```yaml +model: + filter: + # Applies to the input passed to the model. + input: null + + # Applies to denoised output. + denoised: null + + # Applies to JVP denoised output (only used by TTM sampler) + jdenoised: null + + # Applies to cond output. + cond: null + + # Applies to uncond output. + uncond: null + +``` + +### Immiscible + +`immiscible` is a special type of filter: in this case, you do not set `filter_type`. Normal filter keys +apply in the places where `immiscible` can be set. + +## Filter Definitions + +For information about expressions, see [Expressions](expression.md). + +A basic filter supports these keys: + +```yaml +enabled: true + +filter_type: simple + +# Expression that is evaluated to determine whether the filter applies. May be null. +# If set, should evaluate to a boolean. +when: null + +blend_mode: lerp + +# Blend strength applied to output. +strength: 1.0 + +# Input expression. input, ref, output and final should evaluate to a tensor. +input: default + +# Reference expression (only used for immiscible noise currently). +ref: default + +# Output expression. +output: default + +# Final expression - occurs *after* blending. +final: default +``` + +There may be additional keys depending on the filter type. + +## Filter Types + +### `simple` + +Base filter, no special behavior. No additional parameters. + +### `blend` + +Blends the result of two other filters. + +Keys: + +```yaml +# No default, value for example purpose only. +filter1: + filter_type: simple + +# No default, value for example purpose only. +filter2: + filter_type: simple +``` + +### `list` + +A list of filters. The output of the previous is given to the next as input. +The `list` filter's blend applies to the output from the final filter in the list. + +Keys: + +```yaml +# Values for example only, default is an empty list. +filters: + - filter_type: simple + strength: 1.0 + - filter_type: simple + strength: 1.0 +``` + +### `bleh_enhance` + +Available if you have the [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) node pack installed. See: +https://github.com/blepping/ComfyUI-bleh#enhancement-types + +Keys: + +```yaml +enhance_mode: null +enhance_scale: 1.0 +``` + +### `bleh_ops` + +Available if you have the [ComfyUI-bleh](https://github.com/blepping/ComfyUI-bleh) node pack installed. See: +https://github.com/blepping/ComfyUI-bleh#blehblockops + +Keys: + +```yaml +# May be specified as a string containing the YAML rule definitions or inline. +# Values for example only, default is an empty list of ops. +ops: + - if: + to_percent: 0.5 + ops: # Not recommended to actually do this. + - [flip, { direction: h }] + - [roll, { direction: channels, amount: -2 }] +``` + +### `sonar_power_filter` + +Available if you have [ComfyUI-sonar](https://github.com/blepping/ComfyUI-sonar) installed. See: +https://github.com/blepping/ComfyUI-sonar/blob/main/docs/advanced_power_noise.md + +Keys: + +```yaml +power_filter: + mix: 1.0 + normalization_factor: 1.0 + common_mode: 0.0 + channel_correlation: "1,1,1,1,1,1" + alpha: 0.0 + min_freq: 0.0 + max_freq: 0.7071 + stretch: 1.0 + rotate: 0.0 + pnorm: 2.0 + scale: 1.0 + compose_mode: max + + # If specified should be another power filter definition. + compose_with: null +``` diff --git a/py/expression/__init__.py b/py/expression/__init__.py new file mode 100644 index 0000000..07fc6c9 --- /dev/null +++ b/py/expression/__init__.py @@ -0,0 +1,18 @@ +from . import types, expression, handler, util, validation + +from .expression import Expression +from .validation import Arg, ValidateArg +from .handler import BASIC_HANDLERS, BaseHandler + +__all__ = ( + "types", + "expression", + "handler", + "util", + "validation", + "ValidateArg", + "Expression", + "Arg", + "BaseHandler", + "BASIC_HANDLERS", +) diff --git a/py/expression/expression.py b/py/expression/expression.py new file mode 100644 index 0000000..df3c836 --- /dev/null +++ b/py/expression/expression.py @@ -0,0 +1,239 @@ +import re +import operator + +from .parser import Parser, ParserSpec, ParseError +from .types import ( + Empty, + ExpBase, + ExpOp, + ExpBinOp, + ExpSym, + ExpStatements, + ExpFunAp, + ExpTuple, + ExpDict, + ExpKV, +) + + +class Expression: + EXPR_RE = re.compile( + r""" + \s* + ( + \d+ # Possibly negative numeric literal + (?: \. \d* )? # Floating point + (?: e [+-] \d+)? # Scientific notation + | (?: \*\* | // ) # Doubled operators + | [<>]=? # Relative comparison + | [!=]= # Equality + | (?: \|\| | && ) # Logic + | [-+*/|!(),] # Operators + | :> # Key value binop + | ; + | \[ | ] + | \.\.\. + | '[\w.]+ # Symbol + | `?[a-z][\w.]*`? # Function/variable names + ) + \s* + """, + re.I | re.S | re.X | re.A, + ) + + def __init__(self, toks): + if isinstance(toks, str): + toks = tuple(self.tokenize(toks)) + self.expr = Parser(ExprParserSpec(), iter(toks)).go() + + def __repr__(self): + return f"" + + def __call__(self, *args, **kwargs): + return self.eval(*args, **kwargs) + + def eval(self, handlers, *args, **kwargs): + print("\nEVAL", self.expr) + if not isinstance(self.expr, ExpBase): + return self.expr + return self.expr.eval(handlers, *args, **kwargs) + + def __len__(self): + return len(self.expr) + + def pretty_string(self, depth=0): + sval = ( + repr(self.expr) + if not isinstance(self.expr, ExpBase) + else self.expr.pretty_string(depth=depth + 1) + ) + pad = " " * (depth + 1) * 2 + return f"" + + FIXUP = {"true": True, "false": False, "...": Ellipsis, "none": None} + + @classmethod + def fixup_token(cls, t): + if t == "": + return t + t = t.lower() + val = cls.FIXUP.get(t, Empty) + if val is not Empty: + return val + if t[0] == "`": + return ExpBinOp(t.strip("`")) + if t[0] == "'": + return ExpSym(t[1:]) + if (len(t) > 1 and t[0] == "-" and t[1].isdigit()) or t[0].isdigit(): + return float(t) if "." in t else int(t) + return ExpOp(t) + + @classmethod + def tokenize(cls, s): + yield from (cls.fixup_token(m.group(1)) for m in cls.EXPR_RE.finditer(s)) + + +STATIC_OP_HANDLERS = { + "+": operator.add, + "-": operator.sub, + "*": operator.mul, + "/": operator.truediv, + "//": operator.floordiv, + "**": operator.pow, + "%": operator.mod, + "add": operator.add, + "sub": operator.sub, + "mul": operator.mul, + "div": operator.truediv, + "idiv": operator.floordiv, + "pow": operator.pow, + "mod": operator.mod, +} + + +def make_funap(op, args=(), kwargs=None): + if kwargs is None: + kwargs = ExpDict() + argc = len(args) + if argc > 2 or len(kwargs) or not all(isinstance(v, (int, float)) for v in args): + return ExpFunAp(op, args, kwargs) + if argc == 1 and op in "-+": + return -args[0] if op == "-" else args[0] + h = STATIC_OP_HANDLERS.get(op) + if h is None: + return ExpFunAp(op, args, kwargs) + return h(*args) + + +class ExprParserSpec(ParserSpec): + def __init__(self): + super().__init__() + self.populate() + + @staticmethod + def split_funap_args(toks): + if not isinstance(toks, (list, tuple)): + return ExpTuple((toks,)), ExpDict() + return ExpTuple(t for t in toks if not isinstance(t, ExpKV)), ExpDict({ + str(t.k): t.v for t in toks if isinstance(t, ExpKV) + }) + + @staticmethod + def null_constant(p, token, bp): + return token + + @staticmethod + def null_paren(p, token, bp): + result = p.parse_until(bp) if p.token != ")" else ExpTuple() + if p.token == ",": + p.advance() + p.expect(")") + return result + + @staticmethod + def null_prefixop(p, token, bp): + val = p.parse_until(bp) + return make_funap(token, ExpTuple((val,))) + + @classmethod + def left_binop(cls, p, token, left, bp): + return make_funap(token, *cls.split_funap_args((left, p.parse_until(bp)))) + + @staticmethod + def left_kv(p, token, left, bp): + if not isinstance(left, (ExpOp, ExpSym)): + raise ParseError(f"{left!r} is not a valid key") + return ExpKV(left, p.parse_until(bp)) + + @classmethod + def left_funcall(cls, p, token, left, bp): + if not isinstance(left, ExpOp): + raise ParseError(f"{left!r} is not a valid function/variable name") + args = [] + while p.lexer and p.token != ")": + args.append(p.parse_until(1)) + if p.token == ",": + p.advance() + p.expect(")") + return make_funap(left, *cls.split_funap_args(args)) + + @staticmethod + def left_comma(p, token, left, bp): + if p.token == ")": + return left if isinstance(left, ExpTuple) else ExpTuple((left,)) + r = p.parse_until(bp) + return ExpTuple((*left, r) if isinstance(left, ExpTuple) else (left, r)) + + @staticmethod + def left_semicolon(p, token, left, bp): + if p.token == ")" or p.token is None: + return ( + left + if isinstance(left, ExpStatements) + else ExpStatements(ExpTuple((left,))) + ) + r = p.parse_until(bp) + return ExpStatements( + ExpTuple(*left.statements, r) + if isinstance(left, ExpStatements) + else ExpTuple((left, r)) + ) + + @staticmethod + def left_index(p, token, left, bp): + idx = p.parse_until(0) + p.expect("]") + return make_funap("index", ExpTuple((idx, left))) + + @staticmethod + def get_type(token): + if isinstance(token, (int, float)): + return "number" + if isinstance(token, ExpSym): + return "sym" + if isinstance(token, ExpBinOp): + return "binop" + if isinstance(token, ExpOp) and token[0].isalpha(): + return "op" + return token + + def populate(self): + self.add_left(31, self.left_funcall, ("(",)) + self.add_left(31, self.left_index, ("[",)) + self.add_leftright(29, self.left_binop, ("**",)) + self.add_null(27, self.null_prefixop, ("+", "-", "!")) + self.add_left(25, self.left_binop, ("*", "/")) + self.add_left(23, self.left_binop, ("+", "-")) + self.add_left(22, self.left_binop, ("binop",)) + self.add_left(19, self.left_binop, ("<", ">", "<=", ">=")) + self.add_left(19, self.left_binop, ("==", "!=")) + self.add_left(9, self.left_binop, ("&&",)) + self.add_left(7, self.left_binop, ("||",)) + self.add_left(6, self.left_kv, (":>",)) + self.add_left(5, self.left_semicolon, (";",)) + self.add_left(1, self.left_comma, (",",)) + self.add_null(0, self.null_paren, ("(",)) + self.add_null( + -1, self.null_constant, ("number", "op", "sym", Ellipsis, True, False, None) + ) + self.add_null(-1, ParserSpec.null_error, (")", "]")) diff --git a/py/expression/handler.py b/py/expression/handler.py new file mode 100644 index 0000000..2a5d83a --- /dev/null +++ b/py/expression/handler.py @@ -0,0 +1,364 @@ +import operator + +from .validation import ValidateArg, Arg, ValidateError +from .types import Empty, ExpDict +from .util import torch + + +class HandlerError(Exception): + pass + + +class BaseHandler: + input_validators = () + + def __init__(self): + self.input_validators_by_key = { + v.name: (idx, v) for idx, v in enumerate(self.input_validators) + } + + def __call__(self, obj, *, getter): + try: + val = self.handle(obj, getter) + return self.validate_output(obj, val) + except Exception as exc: + raise HandlerError(f'Error evaluating "{obj.name}":\n {exc!r}') from exc + + def safe_get(self, key, obj, getter=None, *, default=Empty): + str_key = isinstance(key, str) + if str_key: + argidx, validator = self.input_validators_by_key.get(key, (-1, None)) + else: + argidx, validator = ( + key, + ( + self.input_validators[key] + if key < len(self.input_validators) + else None + ), + ) + default = ( + default + if default is not Empty or validator is None + else getattr(validator, "default", Empty) + ) + if argidx >= 0 and argidx < len(obj.args): + eff_key = argidx + str_eff_key = False + elif str_key: + eff_key = key + str_eff_key = True + else: + raise ValidateError( + f"Error validating input argument {key}, out of range for actual function arguments" + ) + if getter is None: + if str_eff_key: + val = obj.kwargs.get(eff_key) + else: + val = default if eff_key > len(obj.args) else obj.args[eff_key] + else: + val = getter(eff_key, default=default) + if validator is None: + return val + try: + return validator(key, val) + except ValidateError as exc: + raise ValidateError( + f"Error validating input argument {key}, type {type(val)}: {exc!r}" + ) from None + + def safe_get_multi(self, keys, obj, getter=None, *, default=Empty): + return (self.safe_get(k, obj, getter, default=default) for k in keys) + + def safe_get_all(self, obj, getter=None, *, default=Empty): + return self.safe_get_multi( + (v.name for v in self.input_validators), obj, getter, default=default + ) + + def handle(self, obj, getter): + raise NotImplementedError + + def validate_output(self, obj, value): + return value + + +class BinopLogicHandler(BaseHandler): + input_validators = ( + Arg.present("lhs"), + Arg.present("rhs"), + ) + + def validate_output(self, obj, value): + return operator.truth(value) + + +class OrHandler(BinopLogicHandler): + def handle(self, obj, getter): + return operator.truth( + self.safe_get("lhs", obj, getter=getter) + ) or operator.truth(self.safe_get("rhs", obj, getter=getter)) + + +class AndHandler(BinopLogicHandler): + def handle(self, obj, getter): + return operator.truth( + self.safe_get("lhs", obj, getter=getter) + ) and operator.truth(self.safe_get("rhs", obj, getter=getter)) + + +class AllHandler(BinopLogicHandler): + input_validators = () + + def handle(self, obj, getter): + return all( + operator.truth(self.safe_get(idx, obj, getter=getter)) + for idx in range(len(obj.args)) + ) and all( + operator.truth(self.safe_get(key, obj, getter=getter)) for key in obj.kwargs + ) + + +class AnyHandler(BinopLogicHandler): + def handle(self, obj, getter): + return any( + operator.truth(self.safe_get(idx, obj, getter=getter)) + for idx in range(len(obj.args)) + ) or any( + operator.truth(self.safe_get(key, obj, getter=getter)) for key in obj.kwargs + ) + + +class EqHandler(BinopLogicHandler): + def handle(self, obj, getter): + a1, a2 = self.safe_get_all(obj, getter) + if isinstance(a1, torch.Tensor) and isinstance(a2, torch.Tensor): + return torch.equal(a1, a2) + return a1 == a2 + + +class NeqHandler(BinopLogicHandler): + def handle(self, *args, **kwargs): + return not super().handle(*args, **kwargs) + + +class NotHandler(BinopLogicHandler): + input_validators = (Arg.present("value"),) + + def handle(self, obj, getter): + return not operator.truth(self.safe_get("value", obj, getter=getter)) + + +class IfHandler(BaseHandler): + input_validators = ( + Arg.present("condition"), + Arg.present("then"), + Arg.present("else"), + ) + + def handle(self, obj, getter): + if operator.truth(self.safe_get("condition", obj, getter=getter)): + return self.safe_get("then", obj, getter=getter) + return self.safe_get("else", obj, getter=getter) + + +class BetweenHandler(BaseHandler): # Inclusive + input_validators = ( + Arg.numeric("value"), + Arg.numeric("from", 0.0), + Arg.numeric("to"), + ) + + def handle(self, obj, getter): + value, low, high = self.safe_get_all(obj, getter) + return low <= value <= high + + +class SimpleMathHandler(BaseHandler): + input_validators = (Arg.numeric("lhs"), Arg.numeric("rhs")) + + def __init__(self, handler): + super().__init__() + self.handler = handler + + def validate_output(self, obj, value): + return ValidateArg.validate_numeric(-1, value) + + def handle(self, obj, getter): + args = ( + self.safe_get(idx, obj, getter=getter) + for idx in range(len(self.input_validators)) + ) + return self.handler(*args) + + +class MinusHandler(SimpleMathHandler): + input_validators = (Arg.numeric("lhs"), Arg.numeric("rhs", default=Empty)) + + __init__ = BaseHandler.__init__ + + def handle(self, obj, getter): + lhs, rhs = self.safe_get_all(obj, getter) + if rhs is Empty: + return operator.neg(lhs) + return operator.sub(lhs, rhs) + + +class RelComparisonHandler(SimpleMathHandler): + def validate_output(self, obj, value): + return operator.truth(value) + + +class UnarySimpleMathHandler(SimpleMathHandler): + input_validators = (Arg.numeric("lhs"),) + + +class IsSetHandler(BaseHandler): + input_validators = (Arg.string("name"),) + + def handle(self, obj, getter): + key = self.safe_get(0, obj, getter=getter) + return key in getter.handlers + + def validate_output(self, obj, value): + return operator.truth(value) + + +class GetHandler(BaseHandler): + input_validators = ( + Arg.string("name"), + Arg.present("fallback"), + ) + + def handle(self, obj, getter): + key = self.safe_get("name", obj, getter=getter) + h = getter.handlers.get(key) + if h is None: + return self.safe_get("fallback", obj, getter=getter) + return h(getter.handlers, *getter.args, **getter.kwargs) + + +class S_Handler(BaseHandler): + input_validators = ( + Arg.integer("start", None), + Arg.integer("end", None), + Arg.integer("step", None), + ) + + def handle(self, obj, getter): + return slice(*self.safe_get_all(obj, getter=getter)) + + +class IndexHandler(BaseHandler): + input_validators = ( + Arg.present("index"), + Arg.one_of( + "value", (ValidateArg.validate_sequence, ValidateArg.validate_tensor) + ), + ) + + def handle(self, obj, getter): + idx, value = self.safe_get_all(obj, getter=getter) + return value[idx] + + +class MinHandler(BaseHandler): + input_validators = (Arg.numscalar_sequence("values"),) + + def handle(self, obj, getter): + return min(*self.safe_get("values", obj, getter)) + + def validate_output(self, obj, value): + return ValidateArg.validate_numeric(-1, value) + + +class MaxHandler(MinHandler): + def handle(self, obj, getter): + return max(*self.safe_get("values", obj, getter)) + + +class UnsafeCallHandler(BaseHandler): + input_validators = (Arg.present("__callable"),) + + def handle(self, obj, getter): + if "__callable" in obj.kwargs: + raise ValueError( + "unsafe_call does not support passing the callable via keyword arg" + ) + fun = self.safe_get("__callable", obj, getter) + if not callable(fun): + raise ValueError("Cannot call supplied value: not a callable") + args = (self.safe_get(idx, obj, getter) for idx in range(1, len(obj.args))) + kwargs = {k: self.safe_get(k, obj, getter) for k in obj.kwargs} + return fun(*args, **kwargs) + + +class DictHandler(BaseHandler): + def handle(self, obj, getter): + if len(obj.args): + raise ValueError("Non-KV items passed to dict constructor") + return ExpDict({k: self.safe_get(k, obj, getter) for k in obj.kwargs.keys()}) + + +class CommentHandler(BaseHandler): + def handle(self, obj, getter): + return None + + +LOGIC_HANDLERS = { + "||": OrHandler(), + "&&": AndHandler(), + "==": EqHandler(), + "!=": NeqHandler(), + "not": NotHandler(), + "if": IfHandler(), + "all": AllHandler(), + "any": AnyHandler(), +} +for k, alias in ( + ("||", "or"), + ("&&", "and"), + ("==", "eq"), + ("!=", "neq"), +): + LOGIC_HANDLERS[alias] = LOGIC_HANDLERS[k] + + +MATH_HANDLERS = { + "+": SimpleMathHandler(operator.add), + "-": MinusHandler(), + "*": SimpleMathHandler(operator.mul), + "/": SimpleMathHandler(operator.truediv), + "//": SimpleMathHandler(operator.floordiv), + "**": SimpleMathHandler(operator.pow), + "mod": SimpleMathHandler(operator.mod), + "neg": UnarySimpleMathHandler(operator.neg), + "between": BetweenHandler(), + "<": RelComparisonHandler(operator.lt), + "<=": RelComparisonHandler(operator.le), + ">": RelComparisonHandler(operator.gt), + ">=": RelComparisonHandler(operator.ge), + "min": MinHandler(), + "max": MaxHandler(), +} +for k, alias in ( + ("+", "add"), + ("-", "sub"), + ("*", "mul"), + ("/", "div"), + ("//", "idiv"), + ("**", "pow"), +): + MATH_HANDLERS[alias] = MATH_HANDLERS[k] + +MISC_HANDLERS = { + "is_set": IsSetHandler(), + "get": GetHandler(), + "index": IndexHandler(), + "s_": S_Handler(), + "unsafe_call": UnsafeCallHandler(), + "dict": DictHandler(), + "comment": CommentHandler(), +} + +BASIC_HANDLERS = LOGIC_HANDLERS | MATH_HANDLERS | MISC_HANDLERS diff --git a/py/expression/parser.py b/py/expression/parser.py new file mode 100644 index 0000000..2ac29e0 --- /dev/null +++ b/py/expression/parser.py @@ -0,0 +1,112 @@ +class ParseError(Exception): + pass + + +# Pratt parsing referenced from https://github.com/andychu/pratt-parsing-demo +class ParserSpec: + @staticmethod + def null_error(p, token, bp): + raise ParseError(f"{token!r} cannot be used in prefix position") + + @staticmethod + def left_error(p, token, bp): + raise ParseError(f"{token!r} cannot be used in infix position") + + class LeftInfo: + def __init__(self, led=None, lbp=0, rbp=0): + self.led, self.lbp, self.rbp = led or ParserSpec.left_error, lbp, rbp + + class NullInfo: + def __init__(self, nud=None, bp=0): + self.nud, self.bp = nud or ParserSpec.null_error, bp + + def __init__(self): + self.null_lookup = {} + self.left_lookup = {} + + def add_null(self, bp, nud, tokens): + for token in tokens: + self.null_lookup[token] = self.NullInfo(nud, bp) + if token not in self.left_lookup: + self.left_lookup[token] = self.LeftInfo() + + def add_led(self, lbp, rbp, led, tokens): + for token in tokens: + self.left_lookup[token] = self.LeftInfo(led, lbp, rbp) + if token not in self.null_lookup: + self.null_lookup[token] = self.NullInfo(self.null_error) + + def add_left(self, bp, led, tokens): + return self.add_led(bp, bp, led, tokens) + + def add_leftright(self, bp, led, tokens): + return self.add_led(bp, bp - 1, led, tokens) + + def lookup(self, token, is_left): + result = (self.left_lookup if is_left else self.null_lookup).get(token) + if result is None: + raise ParseError(f"Unexpected token {token!r}") + return result + + @staticmethod + def get_type(token): + if isinstance(token, (int, float)): + return "number" + if isinstance(token, str) and token.isidentifier(): + return "op" + return token + + +class Parser: + def __init__(self, spec, lexer): + self.spec = spec + self.lexer = lexer + self.token = None + self.token_type = None + self.pos = -1 + + def advance(self): + if self.lexer is None: + self.token_type = self.token = None + return None + try: + self.token = next(self.lexer) + self.token_type = self.spec.get_type(self.token) + self.pos += 1 + except StopIteration: + self.token = self.token_type = self.lexer = None + return self.token + + def expect(self, val): + if val is not None and (self.lexer is None or self.token != val): + raise ParseError(f"expected {val!r}, got {self.token!r}") + return self.advance() + + def parse_until(self, rbp): + if self.lexer is None: + raise ParseError("unexpected end of input") + spec = self.spec + token, token_type = self.token, self.token_type + self.advance() + ni = spec.lookup(token_type, False) + node = ni.nud(self, token, ni.bp) + while self.lexer: + token, token_type = self.token, self.token_type + li = spec.lookup(token_type, True) + if rbp >= li.lbp: + break + self.advance() + node = li.led(self, token, node, li.rbp) + return node + + def go(self): + self.advance() + try: + result = self.parse_until(0) + except ParseError as exc: + raise ParseError( + f"pos {self.pos} at token {self.token!r}: parse error: {exc}" + ) from None + if self.lexer: + raise ParseError(f"pos {self.pos}: unexpected end of input") + return result diff --git a/py/expression/types.py b/py/expression/types.py new file mode 100644 index 0000000..8172452 --- /dev/null +++ b/py/expression/types.py @@ -0,0 +1,220 @@ +class Empty: + def __bool__(self): + return False + + +class ExpBase: + def __bool__(self): + return True + + def pretty_string(self, *, depth=0): + return repr(self) + + def eval(self, *args, **kwargs): + return self + + def clone(self, *, mapper=None): + return self if not mapper else mapper(self) + + +class ExpOp(str, ExpBase): + __slots__ = () + + def eval(self, handlers, *args, **kwargs): + h = handlers.get(self) + if h is None: + raise KeyError(f"No handler for op/var {self}") + return h(handlers, *args, **kwargs) + + +class ExpBinOp(ExpOp): + __slots__ = () + + +class ExpSym(str, ExpBase): + __slots__ = () + + def __repr__(self): + return f"'{self}" + + +class ExpTuple(tuple, ExpBase): + __slots__ = () + + def clone(self): + return self.__class__(v.clone() if isinstance(ExpBase) else v for v in self) + + def get_eval(self, k, handlers, *args, default=None, **kwargs): + val = super().__getitem__(k) + if isinstance(val, ExpBase): + return val.eval(handlers, *args, **kwargs) + return val + + def pretty_string(self, depth=0): + vals = ( + repr(v) if not isinstance(v, ExpBase) else v.pretty_string(depth=depth + 1) + for v in self + ) + pad = " " * (depth + 1) * 2 + nlpad = f",\n{pad}" + return f"(\n{pad}{nlpad.join(vals)}\n{pad[:-2]})" + + def eval(self, handlers, *args, **kwargs): + return tuple( + v.eval(handlers, *args, **kwargs) if isinstance(v, ExpBase) else v + for v in self + ) + + +class ExpKV(ExpBase): + __slots__ = ("k", "v") + + def __init__(self, k, v): + self.k = k + self.v = v + + +class ExpDict(dict, ExpBase): + __slots__ = () + + def clone(self): + return self.__class__(v.clone() if isinstance(ExpBase) else v for v in self) + + def pop(self, *args, **kwargs): + raise NotImplementedError + + def get_eval(self, k, handlers, *args, default=Empty, **kwargs): + val = super().get(k, default) + if isinstance(val, ExpBase): + return val.eval(handlers, *args, **kwargs) + return val + + def pretty_string(self, depth=0): + vals = ( + f"{k}: {v!r}" + if not isinstance(v, ExpBase) + else f"{k}: {v.pretty_string(depth=depth + 1)}" + for k, v in self.items() + ) + pad = " " * (depth + 1) * 2 + nlpad = f",\n{pad}" + return f"{{\n{pad}{nlpad.join(vals)}\n{pad[:-2]}}}" + + def eval(self, handlers, *args, **kwargs): + return { + k: v.eval(handlers, *args, **kwargs) if isinstance(v, ExpBase) else v + for k, v in self.items() + } + + popitem = pop + update = pop + clear = pop + __delitem__ = pop + __setitem__ = pop + __ior__ = pop + + +class ExpStatements(ExpBase): + def __init__(self, statements): + if not isinstance(statements, ExpTuple) or not len(statements): + raise ValueError("Must have at least one statement") + self.statements = statements + + def eval(self, handlers, *args, **kwargs): + result = Empty + for stmt in self.statements: + result = ( + stmt.eval(handlers, *args, **kwargs) + if isinstance(stmt, ExpBase) + else stmt + ) + return result + + def __repr__(self): + return f"@{self.statements}" + + +class ExprGetter: + GetterEmpty = Empty + # class GetterEmpty: + # def __bool__(self): + # return False + + def __init__(self, obj, handlers, *args, **kwargs): + self.obj = obj + self.handlers = handlers + self.args = args + self.kwargs = kwargs + + def __call__(self, k, *, default=GetterEmpty): + obj = self.obj + result = ( + obj.kwargs.get_eval( + k, self.handlers, *self.args, default=default, **self.kwargs + ) + if isinstance(k, str) + else obj.args.get_eval(k, self.handlers, *self.args, **self.kwargs) + ) + if result is self.GetterEmpty: + raise KeyError(f"Unknown key {k!r}") + return result + + +class ExpFunAp(ExpBase): + __slots__ = ("name", "args", "kwargs") + + def __init__(self, name, args=None, kwargs=None): + self.name = name + self.args = args if args is not None else ExpTuple() + self.kwargs = kwargs if kwargs is not None else ExpDict() + + def eval(self, handlers, *args, **kwargs): + handler = handlers.get(self.name) + if handler is None: + raise KeyError(f"No handler for op: {self.name!r}") + return handler( + self, getter=ExprGetter(self, handlers, *args, **kwargs), **kwargs + ) + + def clone(self): + return self.__class__(self.name, self.args.clone(), self.kwargs.clone()) + + def pretty_string(self, depth=0): + pad = " " * (depth + 1) * 2 + return f"" + + def __repr__(self): + return ( + f"" + ) + + +class ExpBoundFunAp(ExpFunAp): + __slots__ = ("fun",) + + def __init__(self, name, fun, args, kwargs): + super().__init__(name, args, kwargs) + self.fun = fun + + def eval(self, handlers, *args, **kwargs): + def get_evaled(k, default=None): + return ( + self.kwargs.get_eval(k, handlers, *args, default=default, **kwargs) + if isinstance(k, str) + else self.args.get_eval(k, handlers, *args, **kwargs) + ) + + return self.fun(self.name, self.args, *args, getter=get_evaled, **kwargs) + + +__all__ = ( + "ExpBase", + "ExpOp", + "ExpBinOp", + "ExpSym", + "ExpTuple", + "ExpKV", + "ExpDict", + "ExpFunAp", + "ExpBoundFunAp", +) diff --git a/py/expression/util.py b/py/expression/util.py new file mode 100644 index 0000000..e1a41df --- /dev/null +++ b/py/expression/util.py @@ -0,0 +1,36 @@ +import itertools + +try: + import torch +except ImportError: + # To facilitate testing. + class torch: + class Tensor: + pass + + +class WrapGenerator: + def __init__(self, g): + self.g = g + self._value = None + self.ready = False + + @property + def value(self): + if not self.ready: + raise ValueError("Value not ready") + return self._value + + def __iter__(self): + self._value = yield from self.g + self.ready = True + return self._value + + +def split_iterable(seq, pred): + it = iter(seq) + while True: + toks = tuple(itertools.takewhile(pred, it)) + if toks == (): + break + yield toks diff --git a/py/expression/validation.py b/py/expression/validation.py new file mode 100644 index 0000000..b7ff72c --- /dev/null +++ b/py/expression/validation.py @@ -0,0 +1,190 @@ +import functools + +from .util import torch + + +class Arg: + __slots__ = ("name", "default", "validator") + + class Empty: + pass + + def __init__(self, name, default=Empty, *, validator=None): + self.name = name + self.default = default + self.validator = validator + + def __call__(self, _key, value, *args, **kwargs): + return self.validate(value, *args, **kwargs) + + def validate(self, value): + # FIXME: This shouldn't be using None. + if value is None: + if self.default is self.Empty: + raise ValueError(f"Missing value for argument {self.name}") + return self.default + try: + return self.validator(self.name, value) if self.validator else value + except ValidateError as exc: + raise ValidateError(f"Failed to validate argument {self.name}: {exc}") + + @classmethod + def tensor(cls, name): + return cls(name, validator=ValidateArg.validate_tensor) + + @classmethod + def numeric(cls, name, default=Empty): + return cls(name, default=default, validator=ValidateArg.validate_numeric) + + @classmethod + def numeric_scalar(cls, name, default=Empty): + return cls(name, default=default, validator=ValidateArg.validate_numeric_scalar) + + @classmethod + def integer(cls, name, default=Empty): + return cls(name, default=default, validator=ValidateArg.validate_integer) + + @classmethod + def numscalar_sequence(cls, name, default=Empty): + return cls( + name, default=default, validator=ValidateArg.validate_numscalar_sequence + ) + + @classmethod + def sequence(cls, name, default=Empty, *, item_validator=None): + return cls( + name, + default=default, + validator=functools.partial( + ValidateArg.validate_sequence, item_validator=item_validator + ), + ) + + @classmethod + def string(cls, name, default=Empty): + return cls(name, default=default, validator=ValidateArg.validate_string) + + @classmethod + def boolean(cls, name, default=Empty): + return cls(name, default=default, validator=ValidateArg.validate_boolean) + + @classmethod + def present(cls, name): + return cls(name, validator=ValidateArg.validate_passthrough) + + @classmethod + def one_of(cls, name, validators, *, default=Empty): + def validate(idx, val): + for validator in validators: + try: + return validator(idx, val) + except ValidateError: + continue + raise ValidateError( + f"Failed to validate argument at {idx} of type {type(val)}" + ) + + return cls(name, default=default, validator=validate) + + +class ValidateError(Exception): + pass + + +class ValidateArg: + __slots__ = ("valfuns", "groupfun", "kwargs", "kwargslist") + + def __init__(self, name, *args, kwargslist=(), group=all, **kwargs): + if not isinstance(name, (list, tuple)): + return self.__init__((name,), (args,), group=group, kwargslist=kwargs) + self.valfuns = (getattr(self, f"validate_{n}", None) for n in name) + if not all(self.valfuns): + raise ValueError("Unknown validator") + self.groupfun = group + self.kwargs = kwargs + self.kwargslist = kwargslist if kwargslist is not None else {} + + def __call__(self, *args, **kwargs): + kalen = len(self.kwargslist) + return self.groupfun( + vf( + *args, + **(self.kwargslist if idx < kalen else {}), + **self.kwargs, + ) + for idx, vf in enumerate(self.valfuns) + ) + + @staticmethod + def validate_numeric(idx, val): + if not isinstance(val, (int, float, torch.Tensor)): + raise ValidateError( + f"Expected numeric or tensor argument at {idx}, got {type(val)}" + ) + return val + + @classmethod + def validate_numeric_scalar(cls, idx, val): + if not isinstance(val, (int, float)): + raise ValidateError(f"Expected numeric argument at {idx}, got {type(val)}") + return val + + @classmethod + def validate_integer(cls, idx, val): + if not isinstance(val, int): + raise ValidateError(f"Expected integer argument at {idx}, got {type(val)}") + return val + + @staticmethod + def validate_tensor(idx, val): + if not isinstance(val, torch.Tensor): + raise ValidateError(f"Expected tensor argument at {idx}, got {type(val)}") + return val + + @staticmethod + def validate_sequence(idx, val, *, item_validator=None): + if not isinstance(val, (list, tuple)): + raise ValidateError(f"Expected sequence argument at {idx}, got {type(val)}") + if item_validator is None: + return val + try: + return tuple(item_validator(iidx, v) for iidx, v in enumerate(val)) + except ValidateError as exc: + raise ValidateError(f"Item validation failed for in sequence: {exc}") + + @classmethod + def validate_numscalar_sequence(cls, idx, val): + return cls.validate_sequence( + idx, val, item_validator=cls.validate_numeric_scalar + ) + + # @classmethod + # def validate_numscalar_sequence(cls, idx, val): + # if not isinstance(val, (list, tuple)): + # raise ValidateError(f"Expected sequence argument at {idx}, got {type(val)}") + # try: + # _ = all( + # cls.validate_numeric_scalar(f"{idx}[{i}]", v) is not None + # for i, v in enumerate(val) + # ) + # except ValidateError as exc: + # raise ValidateError( + # f"Expected numeric sequence argument at {idx}, got {type(val)}: {exc}" + # ) + # return val + + @classmethod + def validate_string(cls, idx, val): + if not isinstance(val, str): + raise ValidateError(f"Expected string argument at {idx}, got {type(val)}") + return val + + @classmethod + def validate_boolean(cls, idx, val): + if val is not True and val is not False: + raise ValidateError(f"Expected boolean argument at {idx}, got {type(val)}") + return val + + @classmethod + def validate_passthrough(cls, idx, val): + return val diff --git a/py/expression_handlers.py b/py/expression_handlers.py new file mode 100644 index 0000000..3f01b3f --- /dev/null +++ b/py/expression_handlers.py @@ -0,0 +1,623 @@ +import os + +import torch +import numpy as np + +from . import expression as expr +from . import latent + +from .external import MODULES as EXT +from .utils import scale_noise, resolve_value + +ALLOW_UNSAFE = os.environ.get("COMFYUI_OCS_ALLOW_UNSAFE_EXPRESSIONS") is not None +ALLOW_ALL_UNSAFE = os.environ.get("COMFYUI_OCS_ALLOW_ALL_UNSAFE") is not None + +EXT_BLEH = EXT.get("bleh") +EXT_SONAR = EXT.get("sonar") + +if "bleh" in EXT: + BLENDING_MODES = EXT_BLEH.latent_utils.BLENDING_MODES +else: + BLENDING_MODES = { + "lerp": lambda a, b, t: (1 - t) * a + t * b, + } + +HANDLERS = {} + + +class NormHandler(expr.BaseHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.numeric("factor", 1.0), + expr.Arg.numscalar_sequence("dim", (-3, -2, -1)), + ) + + def handle(self, obj, getter): + tensor, factor, dim = self.safe_get_all(obj, getter) + return scale_noise(tensor, factor, normalize_dims=dim) + + validate_output = expr.Arg.tensor("output") + + +class MeanHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.numscalar_sequence("dim", (-3, -2, -1)), + ) + + def handle(self, obj, getter): + tensor, dim = self.safe_get_all(obj, getter) + return tensor.mean(keepdim=True, dim=dim) + + +class StdHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.numscalar_sequence("dim", (-3, -2, -1)), + ) + + def handle(self, obj, getter): + tensor, dim = self.safe_get_all(obj, getter) + return tensor.std(keepdim=True, dim=dim) + + +class RollHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.numeric_scalar("amount", 0.5), + expr.Arg.numscalar_sequence("dim", (-2,)), + ) + + def handle(self, obj, getter): + tensor, amount, dim = self.safe_get_all(obj, getter) + if isinstance(amount, float) and amount < 1.0 and amount > -1.0: + if len(dim) > 1: + raise ValueError( + "Cannot use percentage based amount with multiple roll dimensions", + ) + amount = int(tensor.shape[dim[0]] * amount) + return tensor.roll(amount, dims=dim) + + +class FlipHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.integer("dim"), + expr.Arg.boolean("mirror", False), + ) + + def handle(self, obj, getter): + tensor, dim, mirror = self.safe_get_all(obj, getter) + if dim < 0: + dim += tensor.ndim + if dim < 0 or dim >= tensor.ndim: + raise ValueError( + f"Dimension out of range, wanted {dim}, tensor has {tensor.ndim} dimension(s)" + ) + if not mirror: + return torch.flip(tensor, (dim,)) + result = tensor.detach().clone() + pivot = tensor.shape[dim] // 2 + out_slice = ( + np.s_[:] if d != dim else np.s_[pivot:] for d in range(tensor.ndim) + ) + in_slice = (np.s_[:] if d != dim else np.s_[:pivot] for d in range(tensor.ndim)) + result[*out_slice] = torch.flip(tensor[*in_slice], dims=(dim,)) + return result + + +class BlendHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor1"), + expr.Arg.tensor("tensor2"), + expr.Arg.numeric("scale", 0.5), + expr.Arg.string("mode", "lerp"), + ) + + def handle(self, obj, getter): + t1, t2, scale, mode = self.safe_get_all(obj, getter) + blend_handler = BLENDING_MODES.get(mode) + if not blend_handler: + raise KeyError(f"Unknown blend mode {mode!r}") + return blend_handler(t1, t2, scale) + + +class ContrastAdaptiveSharpeningHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.numeric("scale", 0.5), + ) + + def handle(self, obj, getter): + t, scale = self.safe_get_all(obj, getter) + return latent.contrast_adaptive_sharpening(t, scale) + + +class ScaleHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.one_of( + "scale", + ( + expr.ValidateArg.validate_numeric_scalar, + expr.ValidateArg.validate_numscalar_sequence, + ), + ), + expr.Arg.string("mode", "bicubic"), + expr.Arg.boolean("absolute_scale", False), + ) + + def handle(self, obj, getter): + t, scale, mode, abs_scale = self.safe_get_all(obj, getter) + if isinstance(scale, (list, tuple)): + if len(scale) != 2: + raise ValueError( + "When passing scale as a tuple, it must be in the form (h, w)" + ) + else: + scale = (scale, scale) + if abs_scale: + scale = tuple(int(v) for v in scale) + else: + scale = (int(t.shape[-1] * scale[0]), int(t.shape[-2] * scale[1])) + if not all(v > 0 for v in scale): + raise ValueError(f"Invalid scale: scale values must be > 0, got: {scale!r}") + return latent.scale_samples(t, scale[1], scale[0], mode=mode) + + +class NoiseHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.string("type", "gaussian"), + ) + + def handle(self, obj, getter): + t, typ = self.safe_get_all(obj, getter) + ctx = getter.handlers + smin, smax, s, sn = ( + h(ctx, *getter.args, **getter.kwargs) if h is not None else None + for h in ( + ctx.get(k) for k in ("sigma_min", "sigma_max", "sigma", "sigma_next") + ) + ) + ns = latent.get_noise_sampler(typ, t, smin, smax, normalized=False) + return ns(s, sn) + + +class UnsafeTorchTensorMethodHandler(NormHandler): + input_validators = ( + expr.Arg.tensor("__tensor"), + expr.Arg.string("__method"), + ) + + if ALLOW_ALL_UNSAFE: + + class AlwaysContains: + def __contains__(self, k): + return True + + whitelist = AlwaysContains() + elif ALLOW_UNSAFE: + whitelist = { + "abs", + "absolute", + "acos", + "acosh", + "add", + "addbmm", + "addcdiv", + "addcmul", + "addmm", + "addmv", + "addr", + "adjoint", + "all", + "allclose", + "amax", + "amin", + "aminmax", + "angle", + "any", + "arccos", + "arccosh", + "arcsin", + "arcsinh", + "arctan", + "arctan2", + "arctanh", + "argmax", + "argmin", + "argsort", + "argwhere", + "as_strided", + "asin", + "asinh", + "atan", + "atan2", + "atanh", + "baddbmm", + "bernoulli", + "bincount", + "bitwise_and", + "bitwise_left_shift", + "bitwise_not", + "bitwise_or", + "bitwise_right_shift", + "bitwise_xor", + "bmm", + "broadcast_to", + "ceil", + "cholesky", + "cholesky_inverse", + "cholesky_solve", + "chunk", + "clamp", + "clip", + "clone", + "conj", + "conj_physical", + "contiguous", + "copysign", + "corrcoef", + "cos", + "cosh", + "count_nonzero", + "cov", + "cross", + "cummax", + "cummin", + "cumprod", + "cumsum", + "deg2rad", + "det", + "detach", + "diag", + "diag_embed", + "diagflat", + "diagonal", + "diagonal_scatter", + "diff", + "digamma", + "dim", + "dist", + "div", + "divide", + "dot", + "dsplit", + "eq", + "equal", + "erf", + "erfc", + "erfinv", + "exp", + "expand", + "expand_as", + "expm1", + "fix", + "flatten", + "flip", + "fliplr", + "flipud", + "float_power", + "floor", + "floor_divide", + "fmax", + "fmin", + "fmod", + "frac", + "frexp", + "gather", + "gcd", + "ge", + "geqrf", + "ger", + "greater", + "greater_equal", + "gt", + "hardshrink", + "heaviside", + "histc", + "hsplit", + "hypot", + "i0", + "igamma", + "igammac", + "index_add", + "index_copy", + "index_fill", + "index_put", + "index_reduce", + "index_select", + "inner", + "inverse", + "isclose", + "isfinite", + "isinf", + "isnan", + "isneginf", + "isposinf", + "kthvalue", + "lcm()", + "ldexp", + "le", + "lerp", + "less", + "less_equal", + "lgamma", + "log", + "log10", + "log1p", + "log2", + "logaddexp", + "logaddexp2", + "logcumsumexp", + "logdet", + "logical_and", + "logical_not", + "logical_or", + "logical_xor", + "logit", + "logsumexp", + "lt", + "lu", + "lu_solve", + "masked_fill", + "masked_scatter", + "masked_select", + "matmul", + "matrix_exp", + "max", + "maximum", + "mean", + "median", + "min", + "minimum", + "mm", + "mode", + "moveaxis", + "movedim", + "msort", + "mul", + "multinomial", + "multiply", + "mv", + "mvlgamma", + "nan_to_num", + "nanmean", + "nanmedian", + "nanquantile", + "nansum", + "narrow", + "narrow_copy", + "ne", + "neg", + "negative", + "new_empty", + "new_full", + "new_ones", + "new_zeros", + "nextafter", + "nonzero", + "norm", + "not_equal", + "numel", + "orgqr", + "ormqr", + "outer", + "permute", + "polygamma", + "positive", + "pow", + "prod", + "qr", + "quantile", + "rad2deg", + "ravel", + "reciprocal", + "remainder", + "renorm", + "repeat", + "repeat_interleave", + "reshape", + "reshape_as", + "resolve_conj", + "resolve_neg", + "roll", + "rot90", + "round", + "rsqrt", + "scatter", + "scatter_add", + "scatter_reduce", + "select", + "select_scatter", + "sgn", + "sigmoid", + "sign", + "signbit", + "sin", + "sinc", + "sinh", + "slice_scatter", + "slogdet", + "smm", + "softmax", + "sort", + "sparse_mask", + "split", + "sqrt", + "square", + "squeeze", + "sspaddmm", + "std", + "stft", + "sub", + "subtract", + "sum", + "sum_to_size", + "svd", + "swapaxes", + "swapdims", + "t", + "take", + "take_along_dim", + "tan", + "tanh", + "tensor_split", + "tile", + "topk", + "transpose", + "triangular_solve", + "tril", + "triu", + "true_divide", + "trunc", + "unflatten", + "unfold", + "unique", + "unique_consecutive", + "unsqueeze", + "var", + "vdot", + "view", + "view_as", + "vsplit", + "where", + "xlogy", + } + else: + whitelist = set() + + def handle(self, obj, getter): + if "__method" in obj.kwargs or "__tensor" in obj.kwargs: + raise ValueError( + "Tensor method call doesn't support passing method or tensor with keyword args" + ) + tensor = self.safe_get("__tensor", obj, getter=getter) + method = self.safe_get("__method", obj, getter=getter) + args = ( + self.safe_get(idx, obj, getter=getter) for idx in range(2, len(obj.args)) + ) + kwargs = {k: self.safe_get(k, obj, getter=getter) for k in obj.kwargs.keys()} + if method not in self.whitelist: + raise ValueError(f"Method {method} not whitelisted: cannot call") + methodfun = getattr(tensor, method, None) + if methodfun is None: + raise KeyError(f"Unknown method {method} for Torch tensor") + return methodfun(*args, **kwargs) + + +class UnsafeTorchHandler(expr.BaseHandler): + input_validators = (expr.Arg.string("path"),) + + if not ALLOW_ALL_UNSAFE: + + def handle(self, obj, getter): + raise ValueError("Unsafe Torch access not allowed") + + else: + + def handle(self, obj, getter): + path = self.safe_get("path", obj, getter) + keys = path.split(".") + if not keys or not all(k for k in keys): + raise ValueError(f"Bad path {path}") + return resolve_value(keys, torch) + + +if EXT_BLEH: + + class BlehEnhanceHandler(expr.BaseHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.string("mode"), + expr.Arg.numeric_scalar("scale", 1.0), + ) + output_validator = expr.Arg.tensor("output") + + def handle(self, obj, getter): + tensor, mode, scale = self.safe_get_all(obj, getter) + return EXT_BLEH.latent_utils.enhance_tensor( + tensor, mode, scale=scale, adjust_scale=False + ) + + HANDLERS["t_bleh_enhance"] = BlehEnhanceHandler() + +if EXT_SONAR: + + class SonarPowerFilterHandler(expr.BaseHandler): + input_validators = ( + expr.Arg.tensor("tensor"), + expr.Arg.present("filter"), + ) + output_validator = expr.Arg.tensor("output") + + default_power_filter = { + "mix": 1.0, + "normalization_factor": 1.0, + "common_mode": 0.0, + "channel_correlation": "1,1,1,1,1,1", + } + + @classmethod + def make_power_filter(cls, fdict, *, toplevel=True): + fdict = fdict.copy() + compose_with = fdict.pop("compose_with", None) + if compose_with: + if not isinstance(compose_with, dict): + raise TypeError("compose_with must be a dictionary") + fdict["compose_with"] = cls.make_power_filter( + compose_with, toplevel=False + ) + topargs = { + k: fdict.pop(k, dv) for k, dv in cls.default_power_filter.items() + } + power_filter = EXT_SONAR.powernoise.PowerFilter(**fdict) + if not toplevel: + return power_filter + cc = topargs.get("channel_correlation") + if cc is not None: + if not isinstance(cc, (list, tuple)) or not all( + isinstance(v, (int, float)) for v in cc + ): + raise TypeError( + "Bad channel correlation type: must be comma separated string or numeric sequence" + ) + topargs["channel_correlation"] = ",".join(repr(v) for v in cc) + return EXT_SONAR.powernoise.PowerNoiseItem( + 1, power_filter=power_filter, time_brownian=True, **topargs + ) + + def handle(self, obj, getter): + tensor, filter_def = self.safe_get_all(obj, getter) + if not isinstance(filter_def, dict): + raise TypeError("filter argument must be a dictionary") + power_filter = self.make_power_filter(filter_def) + filter_rfft = power_filter.make_filter(tensor.shape).to( + tensor.device, non_blocking=True + ) + ns = power_filter.make_noise_sampler_internal( + tensor, + lambda *_unused, latent=tensor: latent, + filter_rfft, + normalized=False, + ) + return ns(None, None) + + HANDLERS["t_sonar_power_filter"] = SonarPowerFilterHandler() + +TENSOR_OP_HANDLERS = { + "t_norm": NormHandler(), + "t_mean": MeanHandler(), + "t_std": StdHandler(), + "t_blend": BlendHandler(), + "t_roll": RollHandler(), + "t_flip": FlipHandler(), + "t_contrast_adaptive_sharpening": ContrastAdaptiveSharpeningHandler(), + "t_scale": ScaleHandler(), + "t_noise": NoiseHandler(), + "unsafe_tensor_method": UnsafeTorchTensorMethodHandler(), + "unsafe_torch": UnsafeTorchHandler(), +} + +HANDLERS |= TENSOR_OP_HANDLERS diff --git a/py/external.py b/py/external.py new file mode 100644 index 0000000..0d75255 --- /dev/null +++ b/py/external.py @@ -0,0 +1,18 @@ +import contextlib +import importlib + +MODULES = {} + +with contextlib.suppress(ImportError, NotImplementedError): + bleh = importlib.import_module("custom_nodes.ComfyUI-bleh") + bleh_version = getattr(bleh, "BLEH_VERSION", -1) + if bleh_version < 1: + raise NotImplementedError + MODULES["bleh"] = bleh.py + +with contextlib.suppress(ImportError, NotImplementedError): + sonar = importlib.import_module("custom_nodes.ComfyUI-sonar") + MODULES["sonar"] = sonar.py + + +__all__ = ("MODULES",) diff --git a/py/filtering.py b/py/filtering.py new file mode 100644 index 0000000..77a8c81 --- /dev/null +++ b/py/filtering.py @@ -0,0 +1,536 @@ +import collections + +import torch + +from . import expression as expr +from . import expression_handlers + +from .external import MODULES as EXT +from .utils import fallback + +OD = collections.OrderedDict + +EXT_BLEH = EXT.get("bleh") +EXT_SONAR = EXT.get("sonar") + +if "bleh" in EXT: + BLENDING_MODES = EXT_BLEH.latent_utils.BLENDING_MODES +else: + BLENDING_MODES = { + "lerp": lambda a, b, t: (1 - t) * a + t * b, + } + +BLENDING_MODES = BLENDING_MODES | { + "a_only": lambda a, b, t: a * t, + "b_only": lambda a, b, t: b * t, +} + +FILTER = {} + + +class FilterHandlerCollection: + def __init__(self, op_handlers, refs): + self.op_handlers = op_handlers + self.refs = refs + + def get(self, k, default=None): + result = self.op_handlers.get(k) + if result is not None: + return result + result = self.refs.get(k) + if result is None: + return None + + def h(obj, *_args, __k=k, __val=result, **_kwargs): + if not isinstance(obj, FilterHandlerCollection): + raise ValueError(f"Unexpected arguments to variable reference {k}") + return result + + return h + + def __contains__(self, k): + return k in self.op_handlers or k in self.refs + + def clone_with_refs(self, refs): + return self.__class__(self.op_handlers, refs) + + +FILTER_HANDLERS = FilterHandlerCollection( + expr.BASIC_HANDLERS | expression_handlers.HANDLERS, {} +) + + +class FilterRefs: + def __init__(self, kvs=None): + self.kvs = fallback(kvs, {}) + + def get(self, k, default=None): + return self.kvs.get(k, default) + + def __getitem__(self, k): + return self.kvs[k] + + def __setitem__(self, k, v): + self.kvs[k] = v + + def clone(self): + return self.__class__(self.kvs.copy()) + + def __or__(self, other): + return self.__class__(self.kvs | other.kvs) + + def __ior__(self, other): + self.kvs |= other.kvs + return self + + def __delitem__(self, k): + del self.kvs[k] + + def __contains__(self, k): + return k in self.kvs + + def __missing__(self, k): + return self.kvs.__missing__(k) + + def __len__(self): + return len(self.kvs) + + def __iter__(self): + return self.kvs.__iter__() + + @classmethod + def from_ss(cls, ss, *, have_current=False): + ms = ss.model.model_sampling + fr = cls({ + "step": ss.step, + "substep": ss.substep, + "dt": ss.dt, + "sigma_idx": ss.idx, + "sigma": ss.sigma, + "sigma_next": ss.sigma_next, + "sigma_down": ss.sigma_down, + "sigma_prev": ss.sigma_prev, + "hist_len": len(ss.hist), + "sigma_min": ms.sigma_min.item(), + "sigma_max": ms.sigma_max.item(), + "step_pct": float(ss.step / ss.total_steps), + "total_steps": ss.total_steps, + "sampling_pct": (999 - ms.timestep(ss.sigma).item()) / 999, + }) + if have_current and len(ss.hist) > 0: + fr |= cls.from_mr(ss.hcur) + fr["d"] = ss.d + if not have_current and len(ss.hist) > 0: + hist_offs = -1 + elif len(ss.hist) > 1: + hist_offs = -2 + else: + hist_offs = None + if hist_offs is not None: + hprev = ss.hist[hist_offs] + fr.kvs |= {f"{k}_prev": v for k, v in cls.from_mr(hprev).kvs.items()} + fr["d_prev"] = hprev.d + return fr + + @classmethod + def from_mr(cls, mr): + return cls({ + k: getattr(mr, ak) + for k, ak in ( + ("cond", "denoised_cond"), + ("denoised", "denoised"), + ("model_call", "call_idx"), + ("sigma", "sigma"), + ("uncond", "denoised_uncond"), + ("x", "x"), + ) + if getattr(mr, ak, None) is not None + }) + + @classmethod + def from_sr(cls, sr): + return cls({ + k: getattr(sr, ak) + for k, ak in ( + ("cond", "denoised_cond"), + ("denoised", "denoised"), + ("noise", "noise_pred"), + ("sigma_down", "sigma_down"), + ("sigma_next", "sigma_next"), + ("sigma_up", "sigma_up"), + ("sigma", "sigma"), + ("step", "step"), + ("substep", "substep"), + ("uncond", "denoised_uncond"), + ("x", "x"), + ) + if getattr(sr, ak, None) is not None + }) + + +class Filter: + name = "unknown" + uses_ref = False + default_options = { + "enabled": True, + "when": None, + "input": "default", + "output": "default", + "ref": "default", + "final": "default", + "blend_mode": "lerp", + "strength": 1.0, + } + + def __init__(self, **options): + self.options = options + self.set_options(self.default_options) + if self.when is not None: + self.when = expr.Expression(self.when) + for key in ("input", "output", "ref", "final"): + if not self.uses_ref and key == "ref": + continue + val = getattr(self, key) + val = make_filter(val) if isinstance(val, dict) else expr.Expression(val) + setattr(self, key, val) + if self.blend_mode not in BLENDING_MODES: + raise ValueError("Bad blend mode") + + def set_options(self, defaults): + for k, v in defaults.items(): + setattr(self, k, self.options.pop(k, v)) + + def apply(self, input_latent, default_ref=None, refs=None, **kwargs): + if not self.check_applies(refs): + return input_latent + refs = fallback(refs, FilterRefs()).clone() + latent = self.get_ref("input", input_latent, self.input, refs=refs) + refs["input"] = latent + if not self.uses_ref: + ref_latent = None + else: + ref_latent = ( + self.get_ref("ref", default_ref, self.ref, refs=refs) + if default_ref is not None + else None + ) + refs["ref"] = ref_latent + output_latent = self.get_ref( + "output", + self.filter(latent, ref_latent, refs=refs, **kwargs), + self.output, + refs=refs, + ) + refs["output"] = output_latent + return self.get_ref( + "final", + BLENDING_MODES[self.blend_mode]( + input_latent[: output_latent.shape[0]], output_latent, self.strength + ), + self.final, + refs=refs, + ) + + def filter(self, latent, ref_latent, *, refs, **kwargs): + raise NotImplementedError + + def check_applies(self, refs=None): + if not self.enabled: + return False + if self.when is None: + return True + refs = fallback(refs, FilterRefs()) + matched = self.when.eval(FILTER_HANDLERS.clone_with_refs(refs)) + # if matched: + # print("\nMATCH", self.name) + return matched + + def get_ref(self, name, default_ref, ops, *, refs=None): + if isinstance(ops, Filter): + return ops.apply(default_ref, ops, refs=refs) + drefs = FilterRefs({"default": default_ref}) + refs = drefs if refs is None else refs | drefs + return ops.eval(FILTER_HANDLERS.clone_with_refs(refs)) + + +class SimpleFilter(Filter): + name = "simple" + + def filter(self, latent, *args, **kwargs): + return latent + + +class BlendFilter(Filter): + name = "blend" + default_options = Filter.default_options | {"filter1": None, "filter2": None} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not (isinstance(self.filter1, dict) and isinstance(self.filter2, dict)): + raise ValueError("Must set filter1 and filter2") + self.filter1 = make_filter(self.filter1) + self.filter2 = make_filter(self.filter2) + + def filter(self, latent, ref_latent, *, refs, **kwargs): + if self.blend_mode == "lerp": + if self.strength == 0: + return self.filter1(latent, ref_latent, refs=refs, **kwargs) + if self.strength == 1: + return self.filter2(latent, ref_latent, refs=refs, **kwargs) + return BLENDING_MODES[self.blend_mode]( + self.filter1.apply(latent, ref_latent, refs=refs, **kwargs), + self.filter2.apply(latent, ref_latent, refs=refs, **kwargs), + self.strength, + ) + + +class ListFilter(Filter): + name = "list" + default_options = Filter.default_options | {"filters": ()} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not isinstance(self.filters, (list, tuple)): + raise ValueError("filters key must be a sequence") + self.filters = tuple(make_filter(filt) for filt in self.filters) + + def filter(self, latent, ref_latent, *, refs, **kwargs): + if not self.filters: + return latent + for filt in self.filters: + latent = filt.apply(latent, ref_latent, refs=refs, **kwargs) + return latent + + +class NormalizeFilter(Filter): + name = "normalize" + uses_ref = True + default_options = Filter.default_options | { + "adjust_target": 0, + "balance_scale": 1.0, + "adjust_scale": 1.0, + "dims": (-2, -1), + } + + def __init__( + self, + start_step=0, + end_step=9999, + phase="after", + adjust_target=0, + balance_scale=1.0, + adjust_scale=1.0, + dims=(-2, -1), + ): + self.start_step = start_step + self.end_step = end_step + self.phase = phase.lower().strip() # before, after, all + if isinstance(adjust_target, str): + adjust_target = adjust_target.lower().strip() + if adjust_target not in ("x",): + raise ValueError("Bad target mean") + # "x", scalar or array matching mean dims + self.adjust_target = adjust_target + # multiplier on adjustment, scalar or array matching mean dims + self.adjust_scale = adjust_scale + self.balance_scale = balance_scale + self.dims = dims + + def __call__(self, ss, sigma, latent, phase, orig_x=None): + if ss.step < self.start_step or ss.step > self.end_step: + return latent + if self.phase != "all" and phase != self.phase: + return latent + if self.adjust_target == "x" and orig_x is None: + raise ValueError("Can only use source x in after phase") + adjust_scale, balance_scale = ( + torch.tensor(v, dtype=latent.dtype).to(latent) + if isinstance(v, (list, tuple)) + else v + for v in (self.adjust_scale, self.balance_scale) + ) + latent_mean = latent.mean(dim=self.dims, keepdim=True) + # print("MEAN", latent_mean) + latent = latent - latent_mean * balance_scale + if self.adjust_target == "x": + latent += orig_x.mean(dim=self.dims, keepdim=True) * adjust_scale + elif isinstance(self.adjust_target, (list, tuple)): + adjust_target = torch.tensor(self.adjust_target, dtype=latent.dtype).to( + latent + ) + latent += adjust_target * adjust_scale + else: + latent += self.adjust_target * adjust_scale + return latent + + +class NormalizeFilter_: + def __init__( + self, + start_step=0, + end_step=9999, + phase="after", + adjust_target=0, + balance_scale=1.0, + adjust_scale=1.0, + dims=(-2, -1), + ): + self.start_step = start_step + self.end_step = end_step + self.phase = phase.lower().strip() # before, after, all + if isinstance(adjust_target, str): + adjust_target = adjust_target.lower().strip() + if adjust_target not in ("x",): + raise ValueError("Bad target mean") + # "x", scalar or array matching mean dims + self.adjust_target = adjust_target + # multiplier on adjustment, scalar or array matching mean dims + self.adjust_scale = adjust_scale + self.balance_scale = balance_scale + self.dims = dims + + def __call__(self, ss, sigma, latent, phase, orig_x=None): + if ss.step < self.start_step or ss.step > self.end_step: + return latent + if self.phase != "all" and phase != self.phase: + return latent + if self.adjust_target == "x" and orig_x is None: + raise ValueError("Can only use source x in after phase") + adjust_scale, balance_scale = ( + torch.tensor(v, dtype=latent.dtype).to(latent) + if isinstance(v, (list, tuple)) + else v + for v in (self.adjust_scale, self.balance_scale) + ) + latent_mean = latent.mean(dim=self.dims, keepdim=True) + # print("MEAN", latent_mean) + latent = latent - latent_mean * balance_scale + if self.adjust_target == "x": + latent += orig_x.mean(dim=self.dims, keepdim=True) * adjust_scale + elif isinstance(self.adjust_target, (list, tuple)): + adjust_target = torch.tensor(self.adjust_target, dtype=latent.dtype).to( + latent + ) + latent += adjust_target * adjust_scale + else: + latent += self.adjust_target * adjust_scale + return latent + + +Normalize = NormalizeFilter + +if EXT_BLEH: + + class BlehEnhanceFilter(Filter): + name = "bleh_enhance" + default_options = Filter.default_options | { + "enhance_mode": None, + "enhance_scale": 1.0, + } + + def filter(self, latent, *args, **kwargs): + if self.enhance_mode is None or self.enhance_scale == 1: + return latent + return EXT_BLEH.latent_utils.enhance_tensor( + latent, self.enhance_mode, scale=self.enhance_scale, adjust_scale=False + ) + + class BlehOpsFilter(Filter): + name = "bleh_ops" + default_options = Filter.default_options | {"ops": ()} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if isinstance(self.ops, (tuple, list)): + self.ops = EXT_BLEH.nodes.ops.RuleGroup( + tuple( + r + for rs in self.ops + for r in EXT_BLEH.nodes.ops.Rule.from_dict(rs) + ) + ) + return + if not isinstance(self.ops, str): + raise ValueError("ops key must be a YAML string or list of object") + self.ops = EXT_BLEH.nodes.ops.RuleGroup.from_yaml(self.ops) + + def filter(self, latent, ref_latent, *args, refs=None, **kwargs): + if not self.ops: + return latent + refs = fallback(refs, {}) + bops = EXT_BLEH.nodes.ops + state = { + bops.CondType.TYPE: bops.PatchType.LATENT, + bops.CondType.PERCENT: 0.0, + bops.CondType.BLOCK: -1, + bops.CondType.STAGE: -1, + bops.CondType.STEP: refs.get("step", 0), + bops.CondType.STEP_EXACT: refs.get("step", -1), + "h": latent, + "hsp": ref_latent, + "target": "h", + } + self.ops.eval(state, toplevel=True) + return state["h"] + + FILTER |= { + "bleh_enhance": BlehEnhanceFilter, + "bleh_ops": BlehOpsFilter, + } + +if EXT_SONAR: + + class SonarPowerFilter(Filter): + name = "sonar_power_filter" + default_options = Filter.default_options + + def __init__(self, **kwargs): + super().__init__(**kwargs) + power_filter = self.options.pop("power_filter", None) + if power_filter is None: + self.power_filter = None + return + if not isinstance(power_filter, dict): + raise ValueError("power_filter key must be dict or null") + self.power_filter = ( + expression_handlers.SonarPowerFilterHandler.make_power_filter( + power_filter + ) + ) + + def filter(self, latent, ref_latent, *args, refs=None, **kwargs): + if not self.power_filter: + return latent + filter_rfft = self.power_filter.make_filter(latent.shape).to( + latent.device, non_blocking=True + ) + ns = self.power_filter.make_noise_sampler_internal( + latent, + lambda *_unused, latent=latent: latent, + filter_rfft, + normalized=False, + ) + return ns(None, None) + + FILTER |= {"sonar_power_filter": SonarPowerFilter} + + +def make_filter(args): + if not isinstance(args, dict): + raise TypeError(f"Bad type for filter: {type(args)}") + args = args.copy() + filter_type = args.pop("filter_type", "simple") + if not isinstance(filter_type, str): + raise ValueError("Missing or invalid filter_type") + filter_fun = FILTER.get(filter_type) + if filter_fun is None: + raise ValueError(f"Unknown filter_type: {filter_type}") + return filter_fun(**args) + + +FILTER |= { + "simple": SimpleFilter, + "blend": BlendFilter, + "list": ListFilter, + "normalize": NormalizeFilter, +} diff --git a/py/latent.py b/py/latent.py new file mode 100644 index 0000000..b20f33c --- /dev/null +++ b/py/latent.py @@ -0,0 +1,114 @@ +import torch +import torch.nn.functional as F + +from comfy.utils import bislerp + +from .external import MODULES as EXT + + +# The following is modified to work with latent images of ~0 mean from https://github.com/Jamy-L/Pytorch-Contrast-Adaptive-Sharpening/tree/main. +def contrast_adaptive_sharpening(x, amount=0.8, *, epsilon=1e-06): + """ + Performs a contrast adaptive sharpening on the batch of images x. + The algorithm is directly implemented from FidelityFX's source code, + that can be found here + https://github.com/GPUOpen-Effects/FidelityFX-CAS/blob/master/ffx-cas/ffx_cas.h + + Parameters + ---------- + x : Tensor + Image or stack of images, of shape [batch, channels, ny, nx]. + Batch and channel dimensions can be ommited. + amount : int [0, 1] + Amount of sharpening to do, 0 being minimum and 1 maximum + + Returns + ------- + Tensor + Processed stack of images. + + """ + + def on_abs_stacked(tensor_list, f, *args, **kwargs): + return f(torch.abs(torch.stack(tensor_list)), *args, **kwargs)[0] + + x_padded = F.pad(x, pad=(1, 1, 1, 1)) + x_padded = torch.complex(x_padded, torch.zeros_like(x_padded)) + # each side gets padded with 1 pixel + # padding = same by default + + # Extracting the 3x3 neighborhood around each pixel + # a b c + # d e f + # g h i + + a = x_padded[..., :-2, :-2] + b = x_padded[..., :-2, 1:-1] + c = x_padded[..., :-2, 2:] + d = x_padded[..., 1:-1, :-2] + e = x_padded[..., 1:-1, 1:-1] + f = x_padded[..., 1:-1, 2:] + g = x_padded[..., 2:, :-2] + h = x_padded[..., 2:, 1:-1] + i = x_padded[..., 2:, 2:] + + # Computing contrast + cross = (b, d, e, f, h) + mn = on_abs_stacked(cross, torch.min, axis=0) + mx = on_abs_stacked(cross, torch.max, axis=0) + + diag = (a, c, g, i) + mn2 = on_abs_stacked(diag, torch.min, axis=0) + mx2 = on_abs_stacked(diag, torch.max, axis=0) + + mx = mx + mx2 + mn = mn + mn2 + + # Computing local weight + inv_mx = torch.reciprocal(mx + epsilon) # 1/mx + + amp = inv_mx * mn + + # scaling + amp = torch.sqrt(amp) + + w = -amp * (amount * (1 / 5 - 1 / 8) + 1 / 8) + # w scales from 0 when amp=0 to K for amp=1 + # K scales from -1/5 when amount=1 to -1/8 for amount=0 + + # The local conv filter is + # 0 w 0 + # w 1 w + # 0 w 0 + div = torch.reciprocal(1 + 4 * w) + output = ((b + d + f + h) * w + e) * div + + return output.real.clamp(x.min(), x.max()) + + +if "bleh" in EXT: + scale_samples = EXT["bleh"].latent_utils.scale_samples + UPSCALE_METHODS = EXT["bleh"].latent_utils.UPSCALE_METHODS +else: + UPSCALE_METHODS = ("bicubic", "bislerp", "bilinear", "nearest-exact", "area") + + def scale_samples( + samples, + width, + height, + mode="bicubic", + sigma=None, # noqa: ARG001 + ): + if mode == "bislerp": + return bislerp(samples, width, height) + return F.interpolate(samples, size=(height, width), mode=mode) + + +if "sonar" in EXT: + get_noise_sampler = EXT["sonar"].noise.get_noise_sampler +else: + + def get_noise_sampler(noise_type, x, *_args: list, **_kwargs: dict): + if noise_type != "gaussian": + raise ValueError("Only gaussian noise supported") + return lambda _s, _sn: torch.randn_like(x) diff --git a/py/model.py b/py/model.py new file mode 100644 index 0000000..e0a109b --- /dev/null +++ b/py/model.py @@ -0,0 +1,264 @@ +from collections import namedtuple + +import torch + +import comfy +from comfy.k_diffusion.sampling import to_d + +from . import filtering + +from .utils import fallback + + +class History: + def __init__(self, size): + self.history = [] + self.size = size + + def __len__(self): + return len(self.history) + + def __getitem__(self, k): + return self.history[k] + + def push(self, val): + if len(self.history) >= self.size: + self.history = self.history[-(self.size - 1) :] + self.history.append(val) + + def reset(self): + self.history = [] + + def clone(self): + obj = self.__new__(self.__class__) + obj.__init__(self.size) + obj.history = self.history.copy() + return obj + + +class ModelResult: + def __init__( + self, + call_idx, + sigma, + x, + denoised, + **kwargs, + ): + self.call_idx = call_idx + self.sigma = sigma + self.x = x + self.denoised = denoised + for k in ("denoised_uncond", "denoised_cond", "tangents", "jdenoised"): + setattr(self, k, kwargs.pop(k, None)) + if len(kwargs) != 0: + raise ValueError(f"Unexpected keyword arguments: {tuple(kwargs.keys())}") + + def to_d( + self, + /, + x=None, + sigma=None, + denoised=None, + denoised_uncond=None, + alt_cfgpp_scale=0, + cfgpp=False, + ): + x = fallback(x, self.x) + sigma = fallback(sigma, self.sigma) + denoised = fallback(denoised, self.denoised) + denoised_uncond = fallback(denoised_uncond, self.denoised_uncond) + if alt_cfgpp_scale != 0: + x = x - denoised * alt_cfgpp_scale + denoised_uncond * alt_cfgpp_scale + return to_d(x, sigma, denoised if not cfgpp else denoised_uncond) + + @property + def d(self): + return self.to_d() + + def clone(self, deep=False): + obj = self.__new__(self.__class__) + for k in ( + "denoised", + "call_idx", + "sigma", + "x", + "denoised_uncond", + "denoised_cond", + "tangents", + "jdenoised", + ): + val = getattr(self, k) + if deep and isinstance(val, torch.Tensor): + val = val.copy() + setattr(obj, k, val) + return obj + + +ModelCallCacheConfig = namedtuple( + "ModelCallCacheConfig", ("size", "max_use", "threshold"), defaults=(0, 1000000, 1) +) + + +class ModelCallCache: + def __init__( + self, + model, + x, + s_in, + extra_args, + *, + cache=None, + filter=None, + ): + self.cache = ModelCallCacheConfig(**fallback(cache, {})) + filtargs = fallback(filter, {}).copy() + self.filters = {} + for key in ("input", "denoised", "jdenoised", "cond", "uncond"): + filt = filtargs.pop(key, None) + if filt is None: + continue + self.filters[key] = filtering.make_filter(filt) + self.model = model + self.s_in = s_in + self.extra_args = extra_args + if self.cache.size < 1: + return + self.reset_cache() + + def maybe_filter(self, name, latent, *args, **kwargs): + filt = self.filters.get(name) + if filt is None: + return latent + return filt.apply(latent, *args, **kwargs) + + def filter_result(self, result, *args, **kwargs): + if not self.filters: + return result + result = result.clone() + for key in ("denoised", "cond", "uncond", "jdenoised"): + filt = self.filters.get(key) + if filt is None: + continue + attk = f"denoised_{key}" if key in ("cond", "uncond") else key + inpval = getattr(result, attk, None) + if inpval is None: + continue + setattr(result, attk, filt.apply(inpval, *args, **kwargs)) + return result + + @staticmethod + def _fr_add_mr(fr, mr): + frmr = filtering.FilterRefs.from_mr(mr) + fr.kvs |= {f"{k}_curr": v for k, v in frmr.kvs.items()} + return fr + + def reset_cache(self): + size = self.cache.size + self.slot = [None] * size + self.slot_use = [self.cache.max_use] * size + + def get(self, idx, *, jvp=False): + idx -= self.cache.threshold + if ( + idx >= self.cache.size + or idx < 0 + or self.slot[idx] is None + or self.slot_use[idx] < 1 + ): + return None + result = self.slot[idx] + if jvp and result.jdenoised is None: + return None + self.slot_use[idx] -= 1 + return result + + def set(self, idx, mr): + idx -= self.cache.threshold + if idx < 0 or idx >= self.cache.size: + return + self.slot_use[idx] = self.cache.max_use + self.slot[idx] = mr + + def call_model(self, x, sigma, **kwargs): + return self.model(x, sigma * self.s_in, **self.extra_args | kwargs) + + @property + def model_sampling(self): + return self.model.inner_model.inner_model.model_sampling + + def __call__( + self, + x, + sigma, + *, + call_index=0, + ss, + s_in=None, + tangents=None, + return_cached=False, + **kwargs, + ): + filter_refs = ss.refs | filtering.FilterRefs({ + "model_call": call_index, + "orig_x": x, + }) + result = self.get(call_index, jvp=tangents is not None) + # print( + # f"MODEL: idx={call_index}, size={self.size}, threshold={self.threshold}, cached={result is not None}" + # ) + if result is not None: + self._fr_add_mr(filter_refs, result) + result = self.filter_result(result, default_ref=x, refs=filter_refs) + return (result, True) if return_cached else result + + comfy.model_management.throw_exception_if_processing_interrupted() + + model_options = self.extra_args.get("model_options", {}).copy() + denoised_cond = denoised_uncond = None + + def postcfg(args): + nonlocal denoised_cond, denoised_uncond + denoised_uncond = args["uncond_denoised"] + denoised_cond = args["cond_denoised"] + return args["denoised"] + + extra_args = self.extra_args | { + "model_options": comfy.model_patcher.set_model_options_post_cfg_function( + model_options, postcfg, disable_cfg1_optimization=True + ) + } + s_in = fallback(s_in, self.s_in) + x = self.maybe_filter("input", x, refs=filter_refs) + + def call_model(x, sigma, **kwargs): + return self.model(x, sigma * s_in, **extra_args | kwargs) + + if tangents is None: + denoised = call_model(x, sigma, **kwargs) + mr = ModelResult( + call_index, + sigma, + x, + denoised, + denoised_uncond=denoised_uncond, + denoised_cond=denoised_cond, + ) + self.set(call_index, mr) + self._fr_add_mr(filter_refs, mr) + mr = self.filter_result(mr, default_ref=x, refs=filter_refs) + return (mr, False) if return_cached else mr + denoised, denoised_prime = torch.func.jvp(call_model, (x, sigma), tangents) + mr = ModelResult( + call_index, + sigma, + x, + denoised, + jdenoised=denoised_prime, + denoised_uncond=denoised_uncond, + denoised_cond=denoised_cond, + ) + self.set(call_index, mr) + self._fr_add_mr(filter_refs, mr) + mr = self.filter_result(mr, default_ref=x, refs=filter_refs) + return (mr, False) if return_cached else mr diff --git a/py/nodes.py b/py/nodes.py index 3b5b764..fcef401 100644 --- a/py/nodes.py +++ b/py/nodes.py @@ -1,14 +1,23 @@ -from .sampling import composable_sampler, STEP_SAMPLERS -from .substep_sampling import StepSamplerChain -from .substep_merging import MERGE_SUBSTEPS_CLASSES - -import comfy import yaml +import comfy -class ComposableSampler: +from .sampling import composable_sampler +from .substep_sampling import StepSamplerChain, StepSamplerGroups, ParamGroup +from .step_samplers import STEP_SAMPLERS +from .substep_merging import MERGE_SUBSTEPS_CLASSES +from .restart import Restart + +DEFAULT_YAML_PARAMS = """\ +# JSON or YAML parameters +s_noise: 1.0 +eta: 1.0 +""" + + +class SamplerNode: RETURN_TYPES = ("SAMPLER",) - CATEGORY = "sampling/custom_sampling/samplers" + CATEGORY = "sampling/custom_sampling/OCS" FUNCTION = "go" @@ -16,34 +25,17 @@ class ComposableSampler: def INPUT_TYPES(cls): return { "required": { - "s_noise": ( - "FLOAT", - { - "default": 1.0, - "min": -100.0, - "max": 100.0, - "step": 0.01, - "round": False, - }, - ), - "eta": ( - "FLOAT", - { - "default": 1.0, - "min": -100.0, - "max": 100.0, - "step": 0.01, - "round": False, - }, - ), - "merge_method": (tuple(MERGE_SUBSTEPS_CLASSES.keys()),), - "step_sampler_chain": ("STEP_SAMPLER_CHAIN",), + "groups": ("OCS_GROUPS",), }, "optional": { - "merge_sampler_opt": ("STEP_SAMPLER_CHAIN",), + "params_opt": ("OCS_PARAMS",), "parameters": ( "STRING", - {"default": "", "multiline": True, "dynamicPrompts": False}, + { + "default": DEFAULT_YAML_PARAMS, + "multiline": True, + "dynamicPrompts": False, + }, ), }, } @@ -51,41 +43,31 @@ class ComposableSampler: def go( self, *, - s_noise, - eta, - merge_method, - step_sampler_chain, - merge_sampler_opt=None, + groups, + params_opt=None, parameters="", ): - if merge_sampler_opt is not None: - merge_sampler = merge_sampler_opt.items[0] - else: - merge_sampler = ComposableStepSampler().go(step_method="euler")[0].items[0] - options = { - "s_noise": s_noise, - "eta": eta, - "merge_method": merge_method, - "merge_sampler": merge_sampler, - } + options = {} parameters = parameters.strip() if parameters: extra_params = yaml.safe_load(parameters) - if not isinstance(extra_params, dict): - raise ValueError("Parameters must be a JSON or YAML object") - options |= extra_params - options["chain"] = step_sampler_chain.clone() + if extra_params is not None: + if not isinstance(extra_params, dict): + raise ValueError("Parameters must be a JSON or YAML object") + options |= extra_params + if params_opt is not None: + options |= params_opt.items + options["_groups"] = groups.clone() return ( comfy.samplers.KSAMPLER( - composable_sampler, - {"composable_sampler_options": options}, + composable_sampler, {"overly_complicated_options": options} ), ) -class ComposableStepSampler: - RETURN_TYPES = ("STEP_SAMPLER_CHAIN",) - CATEGORY = "sampling/custom_sampling/samplers" +class GroupNode: + RETURN_TYPES = ("OCS_GROUPS",) + CATEGORY = "sampling/custom_sampling/OCS" FUNCTION = "go" @@ -93,52 +75,362 @@ class ComposableStepSampler: def INPUT_TYPES(cls): return { "required": { - "s_noise": ( + "merge_method": (tuple(MERGE_SUBSTEPS_CLASSES.keys()),), + "time_mode": (("step", "step_pct", "sigma"),), + "time_start": ( "FLOAT", - { - "default": 1.0, - "min": -100.0, - "max": 100.0, - "step": 0.01, - "round": False, - }, + {"default": 0, "min": 0.0, "step": 0.1, "round'": False}, ), - "eta": ( + "time_end": ( "FLOAT", - { - "default": 1.0, - "min": -100.0, - "max": 100.0, - "step": 0.01, - "round": False, - }, + {"default": 999, "min": 0.0, "step": 0.1, "round'": False}, ), - "substeps": ("INT", {"default": 1, "min": 1, "max": 1000}), - "step_method": (tuple(STEP_SAMPLERS.keys()),), + "substeps": ("OCS_SUBSTEPS",), }, "optional": { - "step_sampler_opt": ("STEP_SAMPLER_CHAIN",), - "custom_noise_opt": ("SONAR_CUSTOM_NOISE",), + "groups_opt": ("OCS_GROUPS",), + "params_opt": ("OCS_PARAMS",), "parameters": ( "STRING", - {"default": "", "multiline": True, "dynamicPrompts": False}, + { + "default": DEFAULT_YAML_PARAMS, + "multiline": True, + "dynamicPrompts": False, + }, ), }, } - def go(self, *, parameters="", step_sampler_opt=None, **kwargs): - if step_sampler_opt is not None: - chain = step_sampler_opt.clone() + def go( + self, + *, + merge_method, + time_mode, + time_start, + time_end, + substeps, + groups_opt=None, + params_opt=None, + parameters="", + ): + group = StepSamplerGroups() if groups_opt is None else groups_opt.clone() + chain = substeps.clone() + chain.merge_method = merge_method + chain.time_mode = time_mode + chain.time_start, chain.time_end = time_start, time_end + options = {} + parameters = parameters.strip() + if parameters: + extra_params = yaml.safe_load(parameters) + if extra_params is not None: + if not isinstance(extra_params, dict): + raise ValueError("Parameters must be a JSON or YAML object") + options |= extra_params + if params_opt is not None: + options |= params_opt.items + chain.options |= options + group.append(chain) + return (group,) + + +class SubstepsNode: + RETURN_TYPES = ("OCS_SUBSTEPS",) + CATEGORY = "sampling/custom_sampling/OCS" + + FUNCTION = "go" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "substeps": ("INT", {"default": 1, "min": 1, "max": 1000}), + "step_method": (tuple(STEP_SAMPLERS.keys()),), + }, + "optional": { + "substeps_opt": ("OCS_SUBSTEPS",), + "params_opt": ("OCS_PARAMS",), + "parameters": ( + "STRING", + { + "default": DEFAULT_YAML_PARAMS, + "multiline": True, + "dynamicPrompts": False, + }, + ), + }, + } + + def go( + self, + *, + parameters="", + substeps_opt=None, + params_opt=None, + **kwargs, + ): + if substeps_opt is not None: + chain = substeps_opt.clone() else: chain = StepSamplerChain() parameters = parameters.strip() if parameters: extra_params = yaml.safe_load(parameters) - if not isinstance(extra_params, dict): - raise ValueError("Parameters must be a JSON or YAML object") - kwargs |= extra_params - chain.items.append(kwargs) + if extra_params is not None: + if not isinstance(extra_params, dict): + raise ValueError("Parameters must be a JSON or YAML object") + kwargs |= extra_params + if params_opt is not None: + kwargs |= params_opt.items + chain.append(kwargs) return (chain,) -__all__ = ("ComposableStepSampler", "ComposableSampler") +class Wildcard(str): + __slots__ = () + + def __ne__(self, _unused): + return False + + +class ParamNode: + RETURN_TYPES = ("OCS_PARAMS",) + CATEGORY = "sampling/custom_sampling/OCS" + FUNCTION = "go" + + WC = Wildcard("*") + + OCS_PARAM_TYPES = { + "custom_noise": lambda v: hasattr(v, "make_noise_sampler"), + "merge_sampler": lambda v: isinstance(v, StepSamplerChain), + "restart_custom_noise": lambda v: hasattr(v, "make_noise_sampler"), + "SAMPLER": lambda _v: True, + } + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "key": (tuple(cls.OCS_PARAM_TYPES.keys()),), + "value": (cls.WC,), + }, + "optional": { + "params_opt": ("OCS_PARAMS",), + "parameters": ( + "STRING", + { + "default": "# Additional YAML or JSON parameters\n", + "multiline": True, + "dynamicPrompts": False, + }, + ), + }, + } + + def go(self, *, key, value, params_opt=None, parameters=""): + if not self.OCS_PARAM_TYPES[key](value): + raise ValueError(f"CSamplerParam: Bad value type for key {key}") + if parameters: + extra_params = yaml.safe_load(parameters) + if extra_params is not None: + if not isinstance(extra_params, dict): + raise ValueError("Parameters must be a JSON or YAML object") + else: + extra_params = None + params = ParamGroup(items={}) if params_opt is None else params_opt.clone() + params[key] = value + if extra_params is not None: + params[f"{key}.params"] = extra_params + return (params,) + + +class MultiParamNode: + RETURN_TYPES = ("OCS_PARAMS",) + CATEGORY = "sampling/custom_sampling/OCS" + FUNCTION = "go" + + PARAM_COUNT = 5 + + @classmethod + def INPUT_TYPES(cls): + param_keys = (("", *ParamNode.OCS_PARAM_TYPES.keys()),) + return { + "required": { + f"key_{idx}": param_keys for idx in range(1, cls.PARAM_COUNT + 1) + }, + "optional": { + "params_opt": ("OCS_PARAMS",), + "parameters": ( + "STRING", + { + "default": """\ +# Additional YAML or JSON parameters +# Should be an object with key corresponding to the index of the input +""", + "multiline": True, + "dynamicPrompts": False, + }, + ), + } + | { + f"value_opt_{idx}": (ParamNode.WC,) + for idx in range(1, cls.PARAM_COUNT + 1) + }, + } + + def go(self, *, params_opt=None, parameters="", **kwargs): + params = ParamGroup(items={}) if params_opt is None else params_opt.clone() + if parameters: + extra_params = yaml.safe_load(parameters) + if extra_params is not None: + if not isinstance(extra_params, dict): + raise ValueError("Parameters must be a JSON or YAML object") + else: + extra_params = {} + else: + extra_params = {} + for idx in range(1, self.PARAM_COUNT + 1): + key, value = kwargs.get(f"key_{idx}"), kwargs.get(f"value_opt_{idx}") + if not key or value is None: + continue + if not ParamNode.OCS_PARAM_TYPES[key](value): + raise ValueError(f"CSamplerParamGroup: Bad value type for key {key}") + params[key] = value + extra = extra_params.get(str(idx)) + if extra is not None: + params[f"{key}.params"] = extra + + return (params,) + + +class SimpleRestartSchedule: + RETURN_TYPES = ("SIGMAS",) + CATEGORY = "sampling/custom_sampling/OCS" + FUNCTION = "go" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "sigmas": ("SIGMAS",), + "start_step": ("INT", {"min": 0, "default": 0}), + }, + "optional": { + "schedule": ( + "STRING", + { + "default": """\ +# YAML or JSON restart schedule +# Every 5 steps, jump back 3 steps +- [5, -3] +# Jump to schedule item 0 +- 0 +""", + "multiline": True, + "dynamicPrompts": False, + }, + ), + }, + } + + def go(self, *, sigmas, start_step=0, schedule="[]"): + if schedule: + parsed_schedule = yaml.safe_load(schedule) + if parsed_schedule is not None: + if not isinstance(parsed_schedule, (list, tuple)): + raise ValueError("Schedule must be a JSON or YAML list") + else: + parsed_schedule = [] + else: + parsed_schedule = [] + return (Restart.simple_schedule(sigmas, start_step, parsed_schedule),) + + +class ModelSetMaxSigmaNode: + RETURN_TYPES = ("MODEL",) + CATEGORY = "hacks" + FUNCTION = "go" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "mode": (("recalculate", "simple_multiply"),), + "sigma_max": ( + "FLOAT", + { + "default": -1.0, + "min": -10000.0, + "max": 10000.0, + "step": 0.01, + "round'": False, + }, + ), + "fake_sigma_min": ( + "FLOAT", + { + "default": 0.0, + "min": 0.0, + "max": 1000.0, + "step": 0.01, + "round'": False, + }, + ), + } + } + + def go(self, model, mode="recalculate", sigma_max=-1.0, fake_sigma_min=0.0): + if sigma_max == 0: + raise ValueError("ModelSetMaxSigma: Invalid sigma_max value") + if mode not in ("recalculate", "simple_multiply"): + raise ValueError("ModelSetMaxSigma: Invalid mode value") + orig_ms = model.get_model_object("model_sampling") + model = model.clone() + orig_max_sigma, orig_min_sigma = ( + orig_ms.sigma_max.item(), + orig_ms.sigma_min.item(), + ) + max_multiplier = abs(sigma_max) if sigma_max < 0 else sigma_max / orig_max_sigma + if max_multiplier == 1: + return (model,) + mcfg = model.get_model_object("model_config") + orig_sigmas = orig_ms.sigmas + fake_sigma_min = orig_sigmas.new_full((1,), fake_sigma_min) + + class NewModelSampling(orig_ms.__class__): + if fake_sigma_min != 0: + + @property + def sigma_min(self): + return fake_sigma_min + + ms = NewModelSampling(mcfg) + if mode == "simple_multiply": + ms.set_sigmas(orig_sigmas * max_multiplier) + else: + ss = getattr(mcfg, "sampling_setting", None) or {} + if ss.get("beta_schedule", "linear") != "linear": + raise NotImplementedError( + "ModelSetMaxSigma: Can only handle linear beta schedules in reschedule mode" + ) + ms.set_sigmas((orig_sigmas**2 * max_multiplier**2) ** 0.5) + new_max_sigma, new_min_sigma = ms.sigma_max.item(), ms.sigma_min.item() + if new_min_sigma >= new_max_sigma: + raise ValueError( + "ModelSetMaxSigma: Invalid fake_min_sigma value, result max <= min" + ) + model.add_object_patch("model_sampling", ms) + print( + f"ModelSetMaxSigma: Set model sigmas({mode}): old_max={orig_max_sigma:.04}, old_min={orig_min_sigma:.03}, new_max={new_max_sigma:.04}, new_min={new_min_sigma:.03}" + ) + return (model,) + + +__all__ = ( + "SamplerNode", + "GroupNode", + "SubstepsNode", + "ParamNode", + "MultiParamNode", + "ModelSetMaxSigmaNode", +) diff --git a/py/noise.py b/py/noise.py new file mode 100644 index 0000000..a24cd73 --- /dev/null +++ b/py/noise.py @@ -0,0 +1,231 @@ +import gc +import random + +import scipy +import torch + +from .filtering import Filter, make_filter +from .utils import scale_noise, fallback + + +class ImmiscibleNoise(Filter): + name = "immiscible" + uses_ref = True + default_options = Filter.default_options | { + "size": 0, + "batching": "channel", + "maximize": False, + } + + def __call__(self, noise_sampler, x_ref, *, refs=None): + if not self.check_applies(refs): + return noise_sampler() + return self.apply( + torch.cat(tuple(noise_sampler() for _ in range(self.size))) + if self.size > 0 + else noise_sampler(), + default_ref=x_ref, + refs=refs, + output_shape=x_ref.shape, + ) + + def filter(self, latent, ref_latent, *, refs, output_shape): + if self.size == 0: + return latent + return self.unbatch( + self.immiscible(self.batch(latent), self.batch(ref_latent)), output_shape + ) + + def batch(self, latent): + if self.batching == "batch": + return latent + sz = latent.shape + if latent.ndim != 4: + raise ValueError("Both latent and reference must be four-dimensional") + if self.batching == "channel": + return latent.view(sz[0] * sz[1], *sz[2:]) + if self.batching == "row": + return latent.view(sz[0] * sz[1] * sz[2], sz[3]) + if self.batching == "column": + return latent.permute(0, 1, 3, 2).reshape(sz[0] * sz[1] * sz[3], sz[2]) + raise ValueError("Bad Immmiscible noise batching type") + + def unbatch(self, latent, sz): + if self.batching == "column": + return latent.view(*sz[:2], sz[3], sz[2]).permute(0, 1, 3, 2) + return latent.view(*sz) + + # Based on implementation from https://github.com/kohya-ss/sd-scripts/pull/1395 + # Idea from https://github.com/Clybius + def immiscible(self, latent, ref_latent): + # "Immiscible Diffusion: Accelerating Diffusion Training with Noise Assignment" (2024) Li et al. arxiv.org/abs/2406.12303 + # Minimize latent-noise pairs over a batch + n = latent.shape[0] + ref_latent_expanded = ( + ref_latent.half().unsqueeze(1).expand(-1, n, *ref_latent.shape[1:]) + ) + latent_expanded = ( + latent.half().unsqueeze(0).expand(ref_latent.shape[0], *latent.shape) + ) + dist = (ref_latent_expanded - latent_expanded) ** 2 + dist = dist.mean(list(range(2, dist.dim()))).cpu() + try: + assign_mat = scipy.optimize.linear_sum_assignment( + dist, maximize=self.maximize + ) + except ValueError as _exc: + # print("\nImmiscible: Failed optimization, skipping") + return latent[: ref_latent.shape[0]] + # print("IMM IDX", assign_mat[1]) + return latent[assign_mat[1]] + + +class NoiseSamplerCache: + def __init__( + self, + x, + seed, + min_sigma, + max_sigma, + *, + normalize_noise=True, + cpu_noise=True, + batch_size=32, + caching=True, + cache_reset_interval=1, + set_seed=False, + scale=1.0, + normalize_dims=(-3, -2, -1), + immiscible=None, + filter=None, + **_unused, + ): + self.x = x + self.mega_x = None + self.seed = seed + self.seed_offset = 0 + self.min_sigma = min_sigma + self.max_sigma = max_sigma + self.cache = {} + self.batch_size = max(1, batch_size) + self.normalize_noise = normalize_noise + self.cpu_noise = cpu_noise + self.caching = caching + self.cache_reset_interval = max(1, cache_reset_interval) + self.scale = float(scale) + self.normalize_dims = tuple(int(v) for v in normalize_dims) + self.immiscible = ImmiscibleNoise(**fallback(immiscible, {})) + if filter is None: + self.filter = None + else: + self.filter = make_filter(filter) + self.update_x(x) + if set_seed: + random.seed(seed) + torch.manual_seed(seed) + + def reset_cache(self): + self.cache = {} + gc.collect() + + def scale_noise(self, noise, factor=1.0, normalized=None, normalize_dims=None): + normalized = self.normalize_noise if normalized is None else normalized + normalize_dims = ( + self.normalize_dims if normalize_dims is None else normalize_dims + ) + return scale_noise( + noise, factor, normalized=normalized, normalize_dims=normalize_dims + ) + + def update_x(self, x): + if self.x.shape == x.shape and self.mega_x is not None: + self.x = x + return + self.x = x + self.mega_x = None + self.reset_cache() + if self.batch_size == 1: + self.mega_x = x + return + self.mega_x = x.repeat(x.shape[0] * self.batch_size, *((1,) * (x.dim() - 1))) + + def set_cache(self, key, noise_sampler): + if not self.caching: + return + self.cache[key] = noise_sampler + + def make_caching_noise_sampler( + self, + nsobj, + size, + sigma, + sigma_next, + immiscible=None, + ): + size = min(size, self.batch_size) + cache_key = (nsobj, size) + if self.caching: + noise_sampler = self.cache.get(cache_key) + if noise_sampler: + return noise_sampler + curr_seed = self.seed + self.seed_offset + self.seed_offset += 1 + curr_x = self.mega_x[: self.x.shape[0] * size, ...] + if nsobj is None: + + def ns(_s, _sn, *_unused, **_unusedkwargs): + return torch.randn_like(curr_x) + + else: + ns = nsobj.make_noise_sampler( + curr_x, + self.min_sigma, + self.max_sigma, + seed=curr_seed, + normalized=False, + cpu=self.cpu_noise, + ) + + orig_h, orig_w = self.x.shape[-2:] + remain = 0 + noise = None + if immiscible is None: + immiscible = self.immiscible + + def noise_sampler_( + *_unused, + out_hw=(orig_h, orig_w), + **_unusedkwargs, + ): + nonlocal remain, noise + if out_hw != (orig_h, orig_w): + raise NotImplementedError( + f"Noise size mismatch: {out_hw} vs {(orig_h, orig_w)}" + ) + if remain < 1: + noise = self.scale_noise(ns(sigma, sigma_next)).view( + size, + *self.x.shape, + ) + remain = size + result = noise[-remain] + remain -= 1 + return result + + def noise_sampler(*args, x_ref=None, refs=None, **kwargs): + if immiscible is False: + noise = noise_sampler_(*args, **kwargs) + else: + noise = immiscible( + lambda args=args, kwargs=kwargs: noise_sampler_(*args, **kwargs), + fallback(x_ref, self.x), + refs=refs, + ) + return ( + self.filter.apply(noise, refs=refs) + if self.filter is not None + else noise + ) + + self.set_cache(cache_key, noise_sampler) + return noise_sampler diff --git a/py/restart.py b/py/restart.py new file mode 100644 index 0000000..ef5d12c --- /dev/null +++ b/py/restart.py @@ -0,0 +1,91 @@ +import torch + + +class Restart: + def __init__(self, *, s_noise=1.0, custom_noise=None, immiscible=False): + from .noise import ImmiscibleNoise + + self.s_noise = s_noise + if immiscible is not False: + immiscible = ImmiscibleNoise(**immiscible) + self.immiscible = immiscible + self.custom_noise = custom_noise + + def get_noise_sampler(self, nsc): + return nsc.make_caching_noise_sampler( + self.custom_noise, + 1, + nsc.max_sigma, + nsc.min_sigma, + immiscible=self.immiscible, + ) + + @staticmethod + def get_segment(sigmas: torch.Tensor) -> torch.Tensor: + last_sigma = sigmas[0] + for idx in range(1, len(sigmas)): + sigma = sigmas[idx] + if sigma > last_sigma: + return sigmas[:idx] + last_sigma = sigma + return sigmas + + def split_sigmas(self, sigmas): + prev_seg = None + while len(sigmas) > 1: + seg = self.get_segment(sigmas) + sigmas = sigmas[len(seg) :] + if prev_seg is not None and seg[0] > prev_seg[-1]: + noise_scale = self.get_noise_scale(prev_seg[-1], seg[0]) + else: + noise_scale = 0.0 + prev_seg = seg + yield (noise_scale, seg) + + def get_noise_scale(self, s_min, s_max): + result = (s_max**2 - s_min**2) ** 0.5 + if isinstance(result, torch.Tensor): + result = result.item() + return result * self.s_noise + + @classmethod + def simple_schedule(cls, sigmas, start_step, schedule=(), max_iter=1000): + if sigmas.ndim != 1: + raise ValueError("Bad number of dimensions for sigmas") + siglen = len(sigmas) - 1 + if siglen <= start_step or not len(schedule): + return sigmas + siglist = sigmas.cpu().tolist() + out = siglist[:start_step] + sched_len = len(schedule) + sched_idx = 0 + sig_idx = start_step + iter_count = 0 + while 0 <= sched_idx < sched_len: + # print(f"LOOP: sched_idx={sched_idx}, sig_idx={sig_idx}: {out}") + iter_count += 1 + if iter_count > max_iter: + raise RuntimeError("Hit max iteration count. Loop in schedule?") + item = schedule[sched_idx] + if not isinstance(item, (list, tuple)): + if item < 0: + item = sched_len + item + if item < 0 or item >= sched_len: + raise ValueError("Schedule jump index out of range") + sched_idx = item + continue + if sig_idx >= siglen or sig_idx < 0: + break + interval, jump = item + chunk = siglist[sig_idx : sig_idx + interval + 1] + # print(f"{out} + {chunk}") + out += chunk + sig_idx += interval + jump + if jump >= 0: + sig_idx += 1 + sched_idx += 1 + if sig_idx < siglen and sig_idx >= 0: + out += siglist[sig_idx:] + if out[-1] > siglist[-1]: + out.append(siglist[-1]) + return torch.tensor(out).to(sigmas) diff --git a/py/sampling.py b/py/sampling.py index d373c20..48b44bc 100644 --- a/py/sampling.py +++ b/py/sampling.py @@ -2,9 +2,22 @@ import torch from tqdm.auto import trange -from .substep_samplers import STEP_SAMPLERS -from .substep_sampling import SamplerState, History, ModelCallCache +from .filtering import FILTER_HANDLERS +from .model import ModelCallCache +from .noise import NoiseSamplerCache +from .substep_sampling import SamplerState from .substep_merging import MERGE_SUBSTEPS_CLASSES +from .restart import Restart + + +def find_merge_sampler(merge_samplers, ss) -> object | None: + handlers = None + for merge_sampler in merge_samplers: + if merge_sampler.when is not None and handlers is None: + handlers = FILTER_HANDLERS.clone_with_refs(ss.refs) + if merge_sampler.check_match(handlers, ss=ss): + return merge_sampler + return None def composable_sampler( @@ -14,14 +27,14 @@ def composable_sampler( *, s_noise=1.0, eta=1.0, - composable_sampler_options, + overly_complicated_options, extra_args=None, callback=None, disable=None, noise_sampler=None, **kwargs, ): - copts = composable_sampler_options.copy() + copts = overly_complicated_options.copy() if extra_args is None: extra_args = {} if noise_sampler is None: @@ -29,64 +42,82 @@ def composable_sampler( def noise_sampler(_s, _sn): return torch.randn_like(x) - samplers = [] - substeps = 0 - for sitem in copts["chain"].items: - custom_noise = sitem.get("custom_noise_opt") - if custom_noise is None: - curr_ns = noise_sampler - else: - curr_ns = custom_noise.make_noise_sampler( - x, sigmas[-1], sigmas[0], normalized=True - ) - ssampler = STEP_SAMPLERS[sitem["step_method"]](noise_sampler=curr_ns, **sitem) - samplers.append(ssampler) - # samplers += (ssampler,) * sitem["substeps"] - substeps += ssampler.substeps - msitem = copts["merge_sampler"] - if copts["merge_method"] in ("sample", "sample_uncached"): - custom_noise = msitem.get("custom_noise_opt") - if custom_noise is None: - curr_ns = noise_sampler - else: - curr_ns = custom_noise.make_noise_sampler( - x, sigmas[-1], sigmas[0], normalized=True - ) - merge_sampler = STEP_SAMPLERS[msitem["step_method"]]( - noise_sampler=curr_ns, **msitem - ) - pass - else: - merge_sampler = None + restart_params = copts.get("restart", {}) + restart = Restart( + s_noise=restart_params.get("s_noise", 1.0), + custom_noise=copts.get("restart_custom_noise"), + immiscible=restart_params.get("immiscible", False), + ) + ss = SamplerState( ModelCallCache( model, x, x.new_ones((x.shape[0],)), extra_args, - size=copts.get("model_call_cache", 0), - max_use=copts.get("model_call_cache_max_use", 1000000), - threshold=copts.get("model_call_cache_threshold", 0), + **copts.get("model", {}), ), sigmas, 0, - History(x, 3), - History(x, 2), extra_args, noise_sampler=noise_sampler, callback=callback, eta=eta if eta != 1.0 else copts["eta"], s_noise=s_noise if s_noise != 1.0 else copts["s_noise"], reta=copts.get("reta", 1.0), + disable_status=disable, ) - merge_sampler = MERGE_SUBSTEPS_CLASSES[copts["merge_method"]]( - ss, - samplers, - **(copts | {"merge_sampler": merge_sampler}), + groups = copts["_groups"] + merge_samplers = tuple( + MERGE_SUBSTEPS_CLASSES[g.merge_method](ss, g) for g in groups.items ) - for idx in trange(len(sigmas) - 1, disable=disable): - print(f"STEP {idx+1}") - ss.update(idx) - ss.model.reset_cache() - x = merge_sampler.step(x) + nsc = NoiseSamplerCache( + x, + extra_args.get("seed", 42), + sigmas[-1], + sigmas[0], + **copts.get("noise", {}), + ) + ss.noise = nsc + sigma_chunks = tuple(restart.split_sigmas(sigmas)) + step_count = sum(len(chunk) - 1 for _noise, chunk in sigma_chunks) + ss.total_steps = step_count + step = 0 + restart_snoise = copts.get("restart_s_noise", 1.0) + with trange(step_count, disable=ss.disable_status) as pbar: + for noise_scale, chunk_sigmas in sigma_chunks: + ss.sigmas = chunk_sigmas + ss.update(0, step=step, substep=0) + if step != 0: + nsc.reset_cache() + ss.hist.reset() + for ms in merge_samplers: + ms.reset() + nsc.min_sigma, nsc.max_sigma = chunk_sigmas[-1], chunk_sigmas[0] + if step != 0 and noise_scale != 0: + restart_ns = restart.get_noise_sampler(nsc) + x += nsc.scale_noise( + restart_ns(refs=ss.refs), + noise_scale * restart_snoise, + ) + del restart_ns + for idx in range(len(chunk_sigmas) - 1): + if idx > 0: + ss.update(idx, step=step, substep=0) + # print( + # f"STEP {step + 1:>3}: {ss.sigma.item():.03} -> {ss.sigma_next.item():.03} || up={ss.sigma_up.item():.03}, down={ss.sigma_down.item():.03}" + # ) + ss.model.reset_cache() + nsc.update_x(x) + merge_sampler = find_merge_sampler(merge_samplers, ss) + if merge_sampler is None: + raise RuntimeError(f"No matching sampler group for step {step + 1}") + pbar.set_description( + f"{merge_sampler.name}: {ss.sigma.item():.03} -> {ss.sigma_next.item():.03}" + ) + x = merge_sampler(x) + if (idx + 1) % nsc.cache_reset_interval == 0: + nsc.reset_cache() + step += 1 + pbar.update(1) return x diff --git a/py/step_samplers.py b/py/step_samplers.py new file mode 100644 index 0000000..c43c079 --- /dev/null +++ b/py/step_samplers.py @@ -0,0 +1,2157 @@ +import contextlib +import inspect +import math +import os +import typing +import warnings + +import torch +import tqdm +import torchsde +import numpy + +import comfy +from comfy.k_diffusion.sampling import ( + get_ancestral_step, + to_d, +) + +from . import filtering, noise, res_support, utils +from .utils import fallback + +HAVE_DIFFRAX = HAVE_TDE = HAVE_TODE = False + +with contextlib.suppress(ImportError): + import torchdiffeq as tde + + HAVE_TDE = True + +with contextlib.suppress(ImportError, RuntimeError): + import torchode as tode + + HAVE_TODE = True + +if not os.environ.get("COMFYUI_OCS_NO_DIFFRAX_SOLVER"): + with contextlib.suppress(ImportError): + import diffrax + import jax + + if not os.environ.get("COMFYUI_OCS_NO_DISABLE_JAX_PREALLOCATE"): + os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] = "false" + # jax.config.update("jax_enable_x64", True) + + HAVE_DIFFRAX = True + + +class SamplerResult: + CLONE_KEYS = ( + "denoised_cond", + "denoised_uncond", + "denoised", + "final", + "noise_pred", + "noise_sampler", + "s_noise", + "sampler", + "sigma_down", + "sigma_next", + "sigma_up", + "sigma", + "step", + "substep", + "x_", + ) + + def __init__( + self, + ss, + sampler, + x, + sigma_up=None, + *, + split_result=None, + sigma=None, + sigma_next=None, + sigma_down=None, + s_noise=None, + noise_sampler=None, + final=True, + ): + self.sampler = sampler + self.sigma_up = fallback(sigma_up, ss.sigma.new_zeros(1)) + self.s_noise = fallback(s_noise, sampler.s_noise) + self.sigma = fallback(sigma, ss.sigma) + self.sigma_next = fallback(sigma_next, ss.sigma_next) + self.sigma_down = fallback(sigma_down, self.sigma_next) + self.noise_sampler = fallback(noise_sampler, sampler.noise_sampler) + self.final = final + self.step = ss.step + self.substep = ss.substep + self.x_ = x + if split_result is not None: + self.denoised, self.noise_pred = split_result + elif x is None: + raise ValueError("SamplerResult requires at least one of x, split_result") + else: + self.denoised = self.noise_pred = None + _ = self.extract_pred(ss) + self.denoised_uncond = ss.hcur.denoised_uncond + self.denoised_cond = ss.hcur.denoised_cond + + def get_noise(self, *, scaled=True, ss=None): + if self.sigma_next == 0 or self.noise_scale == 0: + return torch.zeros_like(self.x_) + return self.noise_sampler( + self.sigma, + self.sigma_next, + out_hw=self.x.shape[-2:], + x_ref=self.x, + refs=filtering.FilterRefs.from_sr(self) if ss is None else ss.refs, + ).mul_(self.noise_scale if scaled else 1.0) + + def extract_pred(self, ss): + if self.denoised is None or self.noise_pred is None: + self.denoised, self.noise_pred = utils.extract_pred( + ss.hcur.x, self.x_, ss.sigma, self.sigma_down + ) + return self.denoised, self.noise_pred + + @property + def x(self): + if self.x_ is None: + self.x_ = self.denoised + self.sigma_down * self.noise_pred + return self.x_ + + @property + def noise_scale(self): + return self.sigma_up * self.s_noise + + def noise_x(self, x=None, scale=1.0, *, ss=None): + x = fallback(x, self.x) + if self.sigma_next == 0 or self.noise_scale == 0: + return x + x = x + self.get_noise(ss=ss) * scale + return x + + def clone(self): + obj = self.__new__(self.__class__) + for k in self.CLONE_KEYS: + if hasattr(self, k): + setattr(obj, k, getattr(self, k)) + return obj + + +class CFGPPStepMixin: + allow_cfgpp = False + allow_alt_cfgpp = False + + def __init__(self): + self.cfgpp = self.allow_cfgpp and self.options.pop("cfgpp", False) + alt_cfgpp_scale = self.options.pop("alt_cfgpp_scale", 0.0) + self.alt_cfgpp_scale = 0.0 if not self.allow_alt_cfgpp else alt_cfgpp_scale + + def to_d(self, mr, **kwargs): + return mr.to_d(alt_cfgpp_scale=self.alt_cfgpp_scale, cfgpp=self.cfgpp, **kwargs) + + +class SingleStepSampler(CFGPPStepMixin): + name = None + self_noise = 0 + model_calls = 0 + ancestralize = False + sample_sigma_zero = False + immiscible = None + + def __init__( + self, + *, + noise_sampler=None, + substeps=1, + s_noise=1.0, + eta=1.0, + dyn_eta_start=None, + dyn_eta_end=None, + weight=1.0, + pre_filter=None, + post_filter=None, + immiscible=None, + **kwargs, + ): + self.options = kwargs + super().__init__() + self.s_noise = s_noise + self.eta = eta + self.dyn_eta_start = dyn_eta_start + self.dyn_eta_end = dyn_eta_end + self.noise_sampler = noise_sampler + self.immiscible = ( + noise.ImmiscibleNoise(**immiscible) + if immiscible not in (False, None) + else immiscible + ) + self.weight = weight + self.substeps = substeps + self.pre_filter = ( + None if pre_filter is None else filtering.make_filter(pre_filter) + ) + self.post_filter = ( + None if post_filter is None else filtering.make_filter(post_filter) + ) + + def __call__(self, x, ss): + orig_x = x + if not self.sample_sigma_zero and ss.sigma_next == 0: + return (yield from self.denoised_result(ss)) + if self.pre_filter or self.post_filter: + filter_refs = ss.refs | filtering.FilterRefs({"orig_x": orig_x}) + if self.pre_filter: + x = self.pre_filter.apply(x, refs=filter_refs) + next_x = None + sg = self.step(x, ss) + with contextlib.suppress(StopIteration): + while True: + sr = sg.send(next_x) + if sr.final: + if self.ancestralize: + sr = self.ancestralize_result(ss, sr) + curr_x = sr.x + if self.post_filter: + curr_x = self.post_filter.apply(curr_x, refs=filter_refs) + if not torch.equal(curr_x, sr.x): + sr.x_ = curr_x + return (yield sr) + next_x = sr.x + yield sr + + def step(self, x, ss): + raise NotImplementedError + + # Euler - based on original ComfyUI implementation + def euler_step(self, x, ss): + sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) + d = self.to_d(ss.hcur) + return (yield from self.result(ss, ss.denoised + d * sigma_down, sigma_up)) + + def denoised_result(self, ss, **kwargs): + return ( + yield SamplerResult(ss, self, ss.denoised, ss.sigma.new_zeros(1), **kwargs) + ) + + def result(self, ss, x, noise_scale=None, **kwargs): + return (yield SamplerResult(ss, self, x, noise_scale, **kwargs)) + + def split_result( + self, ss, denoised, noise_pred, sigma_up=None, sigma_down=None, **kwargs + ): + return ( + yield SamplerResult( + ss, + self, + None, + sigma_up, + sigma_down=sigma_down, + split_result=(denoised, noise_pred), + **kwargs, + ) + ) + + def ancestralize_result(self, ss, sr): + new_sr = sr.clone() + if new_sr.sigma_down is not None and new_sr.sigma_down != new_sr.sigma_next: + return sr + eta = self.get_dyn_eta(ss) + if sr.sigma_next == 0 or eta == 0: + return sr + sd, su = ss.get_ancestral_step(eta, sigma=sr.sigma, sigma_next=sr.sigma_next) + _ = new_sr.extract_pred(ss) + new_sr.x_ = None + new_sr.sigma_up = su + new_sr.sigma_down = sd + return new_sr + + def __str__(self): + return f"" + + def get_dyn_value(self, ss, start, end): + if None in (start, end): + return 1.0 + if start == end: + return start + main_idx = getattr(ss, "main_idx", ss.idx) + main_sigmas = getattr(ss, "main_sigmas", ss.sigmas) + step_pct = main_idx / (len(main_sigmas) - 1) + dd_diff = end - start + return start + dd_diff * step_pct + + def get_dyn_eta(self, ss): + return self.eta * self.get_dyn_value(ss, self.dyn_eta_start, self.dyn_eta_end) + + def max_noise_samples(self): + return (1 + self.self_noise) * self.substeps + + +class HistorySingleStepSampler(SingleStepSampler): + default_history_limit, max_history = 0, 0 + + def __init__(self, *args, history_limit=None, **kwargs): + super().__init__(*args, **kwargs) + self.history_limit = min( + self.max_history, + max( + 0, + self.default_history_limit if history_limit is None else history_limit, + ), + ) + + def available_history(self, ss): + return max( + 0, min(ss.idx, self.history_limit, self.max_history, len(ss.hist) - 1) + ) + + +class ReversibleSingleStepSampler(HistorySingleStepSampler): + def __init__( + self, + *, + reversible_scale=1.0, + reta=1.0, + dyn_reta_start=None, + dyn_reta_end=None, + reversible_start_step=0, + **kwargs, + ): + super().__init__(**kwargs) + self.reversible_scale = reversible_scale + self.reta = reta + self.reversible_start_step = reversible_start_step + self.dyn_reta_start = dyn_reta_start + self.dyn_reta_end = dyn_reta_end + + def reversible_correction(self, ss): + raise NotImplementedError + + def get_dyn_reta(self, ss): + if ss.step < self.reversible_start_step: + return 0.0 + return self.reta * self.get_dyn_value( + ss, self.dyn_reta_start, self.dyn_reta_end + ) + + def get_reversible_cfg(self, ss): + if ss.step < self.reversible_start_step: + return 0.0, 0.0 + return self.get_dyn_reta(ss), self.reversible_scale + + +class DPMPPStepMixin: + @staticmethod + def sigma_fn(t): + return t.neg().exp() + + @staticmethod + def t_fn(t): + return t.log().neg() + + +class MinSigmaStepMixin: + @staticmethod + def adjust_step(sigma, min_sigma, threshold=5e-04): + if min_sigma - sigma > threshold: + return sigma.clamp(min=min_sigma) + return sigma + + def adjusted_step(self, ss, sn, result, mcc, sigma_up): + if sn == ss.sigma_next: + return sigma_up, result + # FIXME: Make sure we're noising from the right sigma. + result = yield from self.result( + ss, result, sigma_up, sigma=ss.sigma, sigma_next=sn, final=False + ) + mr = ss.model(result, sn, ss=ss, call_index=mcc) + dt = ss.sigma_next - sn + result = result + self.to_d(mr) * dt + return sigma_up.new_zeros(1), result + + +class EulerStep(SingleStepSampler): + name = "euler" + allow_cfgpp = True + step = SingleStepSampler.euler_step + + +class CycleSingleStepSampler(SingleStepSampler): + def __init__(self, *, cycle_pct=0.25, **kwargs): + super().__init__(**kwargs) + self.cycle_pct = cycle_pct + + def get_cycle_scales(self, sigma_next): + keep_scale = sigma_next * (1.0 - self.cycle_pct) + add_scale = ((sigma_next**2.0 - keep_scale**2.0) ** 0.5) * ( + 0.95 + 0.25 * self.cycle_pct + ) + # print(f">> keep={keep_scale}, add={add_scale}") + return keep_scale, add_scale + + +class EulerCycleStep(CycleSingleStepSampler): + name = "euler_cycle" + allow_alt_cfgpp = True + allow_cfgpp = True + + def step(self, x, ss): + if ss.sigma_next == 0: + return (yield from self.denoised_result(ss)) + d = self.to_d(ss.hcur) + keep_scale, add_scale = self.get_cycle_scales(ss.sigma_next) + yield from self.result(ss, ss.denoised + d * keep_scale, add_scale) + + +class DPMPP2MStep(HistorySingleStepSampler, DPMPPStepMixin): + name = "dpmpp_2m" + default_history_limit, max_history = 1, 1 + ancestralize = True + + def step(self, x, ss): + s, sn = ss.sigma, ss.sigma_next + t, t_next = self.t_fn(s), self.t_fn(sn) + h = t_next - t + st, st_next = self.sigma_fn(t), self.sigma_fn(t_next) + if self.available_history(ss) > 0: + h_last = t - self.t_fn(ss.sigma_prev) + r = h_last / h + denoised, old_denoised = ss.denoised, ss.hprev.denoised + denoised_d = (1 + 1 / (2 * r)) * denoised - (1 / (2 * r)) * old_denoised + else: + denoised_d = ss.denoised + yield from self.result(ss, (st_next / st) * x - (-h).expm1() * denoised_d) + + +class DPMPP2MSDEStep(HistorySingleStepSampler): + name = "dpmpp_2m_sde" + default_history_limit, max_history = 1, 1 + + def __init__(self, *, solver_type="midpoint", **kwargs): + super().__init__(**kwargs) + self.solver_type = solver_type + + def step(self, x, ss): + denoised = ss.denoised + # DPM-Solver++(2M) SDE + t, s = -ss.sigma.log(), -ss.sigma_next.log() + h = s - t + eta_h = self.get_dyn_eta(ss) * h + + x = ( + ss.sigma_next / ss.sigma * (-eta_h).exp() * x + + (-h - eta_h).expm1().neg() * denoised + ) + noise_strength = ss.sigma_next * (-2 * eta_h).expm1().neg().sqrt() + if self.available_history(ss) == 0: + return (yield from self.result(ss, x, noise_strength)) + h_last = (-ss.sigma.log()) - (-ss.sigma_prev.log()) + r = h_last / h + old_denoised = ss.hprev.denoised + if self.solver_type == "heun": + x = x + ( + ((-h - eta_h).expm1().neg() / (-h - eta_h) + 1) + * (1 / r) + * (denoised - old_denoised) + ) + elif self.solver_type == "midpoint": + x = x + 0.5 * (-h - eta_h).expm1().neg() * (1 / r) * ( + denoised - old_denoised + ) + yield from self.result(ss, x, noise_strength) + + +class DPMPP3MSDEStep(HistorySingleStepSampler): + name = "dpmpp_3m_sde" + default_history_limit, max_history = 2, 2 + + def step(self, x, ss): + denoised = ss.denoised + t, s = -ss.sigma.log(), -ss.sigma_next.log() + h = s - t + eta = self.get_dyn_eta(ss) + h_eta = h * (eta + 1) + + x = torch.exp(-h_eta) * x + (-h_eta).expm1().neg() * denoised + noise_strength = ss.sigma_next * (-2 * h * eta).expm1().neg().sqrt() + ah = self.available_history(ss) + if ah == 0: + return (yield from self.result(ss, x, noise_strength)) + hist = ss.hist + h_1 = (-ss.sigma.log()) - (-ss.sigma_prev.log()) + denoised_1 = hist[-2].denoised + if ah == 1: + r = h_1 / h + d = (denoised - denoised_1) / r + phi_2 = h_eta.neg().expm1() / h_eta + 1 + x = x + phi_2 * d + else: # 2+ history items available + h_2 = (-ss.sigma_prev.log()) - (-ss.sigmas[ss.idx - 2].log()) + denoised_2 = hist[-3].denoised + r0 = h_1 / h + r1 = h_2 / h + d1_0 = (denoised - denoised_1) / r0 + d1_1 = (denoised_1 - denoised_2) / r1 + d1 = d1_0 + (d1_0 - d1_1) * r0 / (r0 + r1) + d2 = (d1_0 - d1_1) / (r0 + r1) + phi_2 = h_eta.neg().expm1() / h_eta + 1 + phi_3 = phi_2 / h_eta - 0.5 + x = x + phi_2 * d1 - phi_3 * d2 + yield from self.result(ss, x, noise_strength) + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class ReversibleHeunStep(ReversibleSingleStepSampler): + name = "reversible_heun" + model_calls = 1 + allow_alt_cfgpp = True + allow_cfgpp = True + + def step(self, x, ss): + sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) + reta, reversible_scale = self.get_reversible_cfg(ss) + sigma_down_reversible, _sigma_up_reversible = ss.get_ancestral_step(reta) + dt_reversible = sigma_down_reversible - ss.sigma + + # Calculate the derivative using the model + d = self.to_d(ss.hcur) + + # Predict the sample at the next sigma using Euler step + x_pred = ss.denoised + d * sigma_down + + # Denoised sample at the next sigma + mr_next = ss.model(x_pred, sigma_down, ss=ss, call_index=1) + + # Calculate the derivative at the next sigma + d_next = self.to_d(mr_next) + + # Update the sample using the Reversible Heun formula + correction = dt_reversible**2 * (d_next - d) / 4 + x = ( + mr_next.denoised + + (sigma_down * (d + d_next) / 2) + - correction * reversible_scale + ) + yield from self.result(ss, x, sigma_up) + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class ReversibleHeun1SStep(ReversibleSingleStepSampler): + name = "reversible_heun_1s" + model_calls = 1 + default_history_limit, max_history = 1, 1 + allow_alt_cfgpp = True + allow_cfgpp = True + + def step(self, x, ss): + if self.available_history(ss) < 1: + return (yield from ReversibleHeunStep.step(self, x, ss)) + s = ss.sigma + # Reversible Heun-inspired update (first-order) + sd, su = ss.get_ancestral_step(self.get_dyn_eta(ss)) + reta, reversible_scale = self.get_reversible_cfg(ss) + sdr, _sur = ss.get_ancestral_step(reta) + dt, dtr = sd - s, sdr - s + # eff_x = ss.hist[-1].x if ah > 0 else x + eff_x = x + + # Calculate the derivative using the model + # d_prev = self.to_d( + # ss.hist[-2] if ah > 0 else ss.hist[-1], + # x=eff_x, + # sigma=s, + # ) + prev_mr = ss.hist[-2] + + # d_prev = self.to_d(prev_mr, x=eff_x, sigma=s) + d_prev = self.to_d(prev_mr, sigma=ss.sigma_prev) + + # Predict the sample at the next sigma using Euler step + # x_pred = ss.denoised + d_prev * sd + x_pred = eff_x + d_prev * dt + # x_pred = ss.denoised + d_prev * sd + + # Calculate the derivative at the next sigma + d_next = self.to_d(ss.hcur, x=x_pred, sigma=sd) + + # Update the sample using the Reversible Heun formula + correction = dtr**2 * (d_next - d_prev) / 4 + x = x + (dt * (d_prev + d_next) / 2) - correction * reversible_scale + yield from self.result(ss, x, su) + + # def __step(self, x, ss): + # if ss.sigma_next == 0: + # return self.euler_step(x, ss) + # # Reversible Heun-inspired update (first-order) + # sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) + # sigma_down_reversible, sigma_up_reversible = ss.get_ancestral_step( + # self.get_dyn_reta(ss) + # ) + # sigma_i, sigma_i_plus_1 = ss.sigma, sigma_down + # dt = sigma_i_plus_1 - sigma_i + # dt_reversible = sigma_down_reversible - sigma_i + + # eff_x = ss.hist[-2 if len(ss.hist) > 1 else -1].x + # # eff_x = ss.hist[-2].x if len(ss.hist) > 1 else x + + # # Calculate the derivative using the model + # eff_mr = ss.hprev if len(ss.hist) > 1 else ss.hcur + # d_i_old = self.to_d(eff_mr) + # # d_i_old = self.to_d(ss.hprev if len(ss.hist) > 1 else ss.hcur) + # # d_i_old = to_d( + # # eff_x, + # # sigma_i if len(ss.hist) == 1 else ss.sigma_prev, + # # ss.hist[-2].denoised + # # if len(ss.hist) > 1 + # # else ss.model(eff_x, sigma_i, ss=ss,call_index=1).denoised, + # # ) + + # # Predict the sample at the next sigma using Euler step + # x_pred = eff_x + d_i_old * dt + + # # Calculate the derivative at the next sigma + # d_i_plus_1 = to_d(x_pred, sigma_i_plus_1, ss.denoised) + + # # Update the sample using the Reversible Heun formula + # x = ( + # x + # + dt * (d_i_old + d_i_plus_1) / 2 + # - dt_reversible**2 * (d_i_plus_1 - d_i_old) / 4 + # ) + # yield from self.result(ss, x, sigma_up) + # # return x, sigma_up + + # def _step(self, x, ss): + # if ss.sigma_next == 0: + # return (yield from self.euler_step(x, ss)) + # ah = self.available_history(ss) + # s = ss.sigma + # # Reversible Heun-inspired update (first-order) + # sd, su = ss.get_ancestral_step(self.get_dyn_eta(ss)) + # sdr, _sur = ss.get_ancestral_step(self.get_dyn_reta(ss)) + # dt, dtr = sd - s, sdr - s + # # eff_mr = ss.hprev if ah > 0 else ss.hcur + # # eff_x = ss.hist[-1].x if ah > 0 else x # This probably doesn't make sense. + + # # Calculate the derivative using the model + # # mr_prev = ss.hist[-2] if ah > 0 else ss.model(eff_x, s, ss=ss,call_index=1) + # mr_prev = ss.hist[-2 if ah > 0 else -1] + # d_prev = self.to_d(mr_prev, x=x, sigma=ss.sigma) + # # d_prev = self.to_d( + # # mr_prev, sigma=ss.sigma_prev if ss.sigma_prev is not None else ss.sigma + # # ) + + # # Predict the sample at the next sigma using Euler step + # x_pred = ss.denoised + d_prev * sd + # # x_pred = mr_prev.denoised + d_prev * sd + # # x_pred = eff_x + d_prev * dt + + # # Calculate the derivative at the next sigma + # d_next = self.to_d(ss.hcur, x=x_pred, sigma=sd) + + # # Update the sample using the Reversible Heun formula + # correction = dtr**2 * (d_next - d_prev) / 4 + # # x = x + (dt * (d_prev + d_next) / 2) - correction * self.reversible_scale + # x = ( + # ss.denoised + # + (sd * (d_prev + d_next) / 2) + # - correction * self.reversible_scale + # ) + # yield from self.result(ss, x, su) + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class RESStep(SingleStepSampler): + name = "res" + model_calls = 1 + + def __init__(self, *, res_simple_phi=False, res_c2=0.5, **kwargs): + super().__init__(**kwargs) + self.simple_phi = res_simple_phi + self.c2 = res_c2 + + def step(self, x, ss): + eta = self.get_dyn_eta(ss) + sigma_down, sigma_up = ss.get_ancestral_step(eta) + denoised = ss.denoised + lam_next = sigma_down.log().neg() if eta != 0 else ss.sigma_next.log().neg() + lam = ss.sigma.log().neg() + + h = lam_next - lam + a2_1, b1, b2 = res_support._de_second_order( + h=h, c2=self.c2, simple_phi_calc=self.simple_phi + ) + + c2_h = 0.5 * h + + x_2 = math.exp(-c2_h) * x + a2_1 * h * denoised + lam_2 = lam + c2_h + sigma_2 = lam_2.neg().exp() + + denoised2 = ss.model(x_2, sigma_2, ss=ss, call_index=1).denoised + + x = math.exp(-h) * x + h * (b1 * denoised + b2 * denoised2) + yield from self.result(ss, x, sigma_up) + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class TrapezoidalStep(SingleStepSampler): + name = "trapezoidal" + model_calls = 1 + allow_alt_cfgpp = True + + def step(self, x, ss): + sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) + + # Calculate the derivative using the model + d_i = self.to_d(ss.hcur) + + # Predict the sample at the next sigma using Euler step + x_pred = x + d_i * ss.dt + + # Denoised sample at the next sigma + mr_next = ss.model(x_pred, ss.sigma_next, ss=ss, call_index=1) + + # Calculate the derivative at the next sigma + d_next = self.to_d(mr_next) + dt_2 = sigma_down - ss.sigma + + # Update the sample using the Trapezoidal rule + x = x + dt_2 * (d_i + d_next) / 2 + yield from self.result(ss, x, sigma_up) + + +class TrapezoidalCycleStep(CycleSingleStepSampler): + name = "trapezoidal_cycle" + model_calls = 1 + allow_alt_cfgpp = True + + def step(self, x, ss): + # Calculate the derivative using the model + d_i = self.to_d(ss.hcur) + + # Predict the sample at the next sigma using Euler step + x_pred = x + d_i * ss.dt + + # Denoised sample at the next sigma + mr_next = ss.model(x_pred, ss.sigma_next, ss=ss, call_index=1) + + # Calculate the derivative at the next sigma + d_next = self.to_d(mr_next) + + # Update the sample using the Trapezoidal rule + keep_scale, add_scale = self.get_cycle_scales(ss.sigma_next) + noise_pred = (d_i + d_next) * 0.5 # Combined noise prediction + denoised_pred = x - noise_pred * ss.sigma # Denoised prediction + yield from self.result(ss, denoised_pred + noise_pred * keep_scale, add_scale) + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class BogackiStep(ReversibleSingleStepSampler): + name = "bogacki" + reversible = False + model_calls = 2 + allow_alt_cfgpp = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.reversible: + self.reversible_scale = 0 + + def step(self, x, ss): + s = ss.sigma + sd, su = ss.get_ancestral_step(self.get_dyn_eta(ss)) + reta, reversible_scale = self.get_reversible_cfg(ss) + sdr, _sur = ss.get_ancestral_step(reta) + dt, dtr = sd - s, sdr - s + + # Calculate the derivative using the model + d = self.to_d(ss.hcur) + + # Bogacki-Shampine steps + k1 = d * dt + k2 = self.to_d(ss.model(x + k1 / 2, s + dt / 2, ss=ss, call_index=1)) * dt + k3 = ( + self.to_d( + ss.model(x + 3 * k1 / 4 + k2 / 4, s + 3 * dt / 4, ss=ss, call_index=2) + ) + * dt + ) + + # Reversible correction term (inspired by Reversible Heun) + correction = dtr**2 * (k3 - k2) / 6 + + # Update the sample + x = (x + 2 * k1 / 9 + k2 / 3 + 4 * k3 / 9) - correction * reversible_scale + yield from self.result(ss, x, su) + + +class ReversibleBogackiStep(BogackiStep): + name = "reversible_bogacki" + reversible = True + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class RK4Step(SingleStepSampler): + name = "rk4" + model_calls = 3 + allow_alt_cfgpp = True + + def step(self, x, ss): + sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) + sigma = ss.sigma + # Calculate the derivative using the model + d = to_d(x, sigma, ss.denoised) + dt = sigma_down - sigma + + # Runge-Kutta steps + k1 = d * dt + k2 = self.to_d(ss.model(x + k1 / 2, sigma + dt / 2, ss=ss, call_index=1)) * dt + k3 = self.to_d(ss.model(x + k2 / 2, sigma + dt / 2, ss=ss, call_index=2)) * dt + k4 = self.to_d(ss.model(x + k3, sigma + dt, ss=ss, call_index=3)) * dt + + # Update the sample + x = x + (k1 + 2 * k2 + 2 * k3 + k4) / 6 + yield from self.result(ss, x, sigma_up) + + +# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +class EulerDancingStep(SingleStepSampler): + name = "euler_dancing" + self_noise = 1 + + def __init__( + self, + *, + deta=1.0, + ds_noise=None, + leap=2, + dyn_deta_start=None, + dyn_deta_end=None, + dyn_deta_mode="lerp", + **kwargs, + ): + super().__init__(**kwargs) + self.deta = deta + self.ds_noise = ds_noise if ds_noise is not None else self.s_noise + self.leap = leap + self.dyn_deta_start = dyn_deta_start + self.dyn_deta_end = dyn_deta_end + if dyn_deta_mode not in ("lerp", "lerp_alt", "deta"): + raise ValueError("Bad dyn_deta_mode") + self.dyn_deta_mode = dyn_deta_mode + + def step(self, x, ss): + eta = self.eta + deta = self.deta + leap_sigmas = ss.sigmas[ss.idx :] + leap_sigmas = leap_sigmas[: utils.find_first_unsorted(leap_sigmas)] + zero_idx = (leap_sigmas <= 0).nonzero().flatten()[:1] + max_leap = (zero_idx.item() if len(zero_idx) else len(leap_sigmas)) - 1 + is_danceable = max_leap > 1 and ss.sigma_next != 0 + curr_leap = max(1, min(self.leap, max_leap)) + sigma_leap = leap_sigmas[curr_leap] if is_danceable else ss.sigma_next + del leap_sigmas + sigma_down, sigma_up = get_ancestral_step(ss.sigma, sigma_leap, eta) + print("???", sigma_down, sigma_up) + d = to_d(x, ss.sigma, ss.denoised) + # Euler method + dt = sigma_down - ss.sigma + x = x + d * dt + if curr_leap == 1: + return (yield from self.result(ss, x, sigma_up)) + noise_strength = self.ds_noise * sigma_up + if noise_strength != 0: + x = yield from self.result( + ss, x, sigma_up, sigma_next=sigma_leap, final=False + ) + + # x = x + self.noise_sampler(ss.sigma, sigma_leap).mul_( + # self.ds_noise * sigma_up + # ) + # sigma_down2, sigma_up2 = get_ancestral_step(sigma_leap, ss.sigma, eta=deta) + # _sigma_down2, sigma_up2 = get_ancestral_step(sigma_leap, ss.sigma, eta=deta) + # sigma_up2 = ss.sigma_next + (ss.sigma - ss.sigma_next) * 0.5 + sigma_up2 = get_ancestral_step(ss.sigma_next, sigma_leap, eta=deta)[1] + ( + ss.sigma_next * 0.5 + ) + sigma_down2, _sigma_up2 = get_ancestral_step( + ss.sigma_next, sigma_leap, eta=deta + ) + print(">>>", sigma_down2, sigma_up2, "--", ss.sigma, "->", sigma_leap) + # sigma_down2, sigma_up2 = get_ancestral_step(ss.sigma_next, sigma_leap, eta=deta) + d_2 = to_d(x, sigma_leap, ss.denoised) + dt_2 = sigma_down2 - sigma_leap + x = x + d_2 * dt_2 + yield from self.result(ss, x, sigma_up2) + + def _step(self, x, ss): + eta = self.get_dyn_eta(ss) + leap_sigmas = ss.sigmas[ss.idx :] + leap_sigmas = leap_sigmas[: utils.find_first_unsorted(leap_sigmas)] + zero_idx = (leap_sigmas <= 0).nonzero().flatten()[:1] + max_leap = (zero_idx.item() if len(zero_idx) else len(leap_sigmas)) - 1 + is_danceable = max_leap > 1 and ss.sigma_next != 0 + curr_leap = max(1, min(self.leap, max_leap)) + sigma_leap = leap_sigmas[curr_leap] if is_danceable else ss.sigma_next + # DANCE 35 6 tensor(10.0947, device='cuda:0') -- tensor([21.9220, + # print("DANCE", max_leap, curr_leap, sigma_leap, "--", leap_sigmas) + del leap_sigmas + sigma_down, sigma_up = get_ancestral_step(ss.sigma, sigma_leap, eta) + d = to_d(x, ss.sigma, ss.denoised) + # Euler method + dt = sigma_down - ss.sigma + x = x + d * dt + if curr_leap == 1: + return x, sigma_up + dance_scale = self.get_dyn_value(ss, self.dyn_deta_start, self.dyn_deta_end) + if curr_leap == 1 or not is_danceable or abs(dance_scale) < 1e-04: + print("NODANCE", dance_scale, self.deta, is_danceable, ss.sigma_next) + yield SamplerResult(ss, self, x, sigma_up) + print( + "DANCE", dance_scale, self.deta, self.dyn_deta_mode, self.ds_noise, sigma_up + ) + sigma_down_normal, sigma_up_normal = get_ancestral_step( + ss.sigma, ss.sigma_next, eta + ) + if self.dyn_deta_mode == "lerp": + dt_normal = sigma_down_normal - ss.sigma + x_normal = x + d * dt_normal + else: + x_normal = x + sigma_down2, sigma_up2 = get_ancestral_step( + sigma_leap, + ss.sigma_next, + eta=self.deta * (1.0 if self.dyn_deta_mode != "deta" else dance_scale), + ) + print( + "-->", + sigma_down2, + sigma_up2, + "--", + self.deta * (1.0 if self.dyn_deta_mode != "deta" else dance_scale), + ) + x = x + self.noise_sampler(ss.sigma, sigma_leap).mul_(self.ds_noise * sigma_up) + d_2 = to_d(x, sigma_leap, ss.denoised) + dt_2 = sigma_down2 - sigma_leap + result = x + d_2 * dt_2 + # SIGMA: norm_up=9.062416076660156, up=10.703859329223633, up2=19.376544952392578, str=21.955078125 + noise_strength = sigma_up2 + ((sigma_up - sigma_up_normal) ** 5.0) + noise_strength = sigma_up2 + ((sigma_up2 - sigma_up) * 0.5) + # noise_strength = sigma_up2 + ( + # (sigma_up2 - sigma_up) ** (1.0 - (sigma_up_normal / sigma_up2)) + # ) + noise_diff = ( + sigma_up - sigma_up_normal + if sigma_up > sigma_up_normal + else sigma_up_normal - sigma_up + ) + noise_div = ( + sigma_up / sigma_up_normal + if sigma_up > sigma_up_normal + else sigma_up_normal / sigma_up + ) + noise_diff = sigma_up2 - sigma_up_normal + noise_div = sigma_up2 / sigma_up_normal + noise_div = ss.sigma / sigma_leap + + # noise_strength = sigma_up2 + (noise_diff * noise_div) + # noise_strength = sigma_up2 + ((noise_diff * 0.5) ** 2.0) + # noise_strength = sigma_up2 + ((1.0 - noise_diff) ** 0.5) + # noise_strength = sigma_up2 + (((sigma_up2 - sigma_up) * 0.5) ** 2.0) + # noise_strength = sigma_up2 + (((sigma_up2 - sigma_up_normal) * 0.5) ** 1.5) + # noise_strength = sigma_up2 + ( + # (noise_diff * 0.1875) ** (1.0 / (noise_div - 0.0)) + # ) + # noise_strength = sigma_up2 + ( + # (noise_diff * 0.125) ** (1.0 / (noise_div * 1.25)) + # ) + # noise_strength = sigma_up2 + ((noise_diff * 0.2) ** (1.0 / (noise_div * 1.0))) + noise_strength = sigma_up2 + (noise_diff * 0.9 * max(0.0, noise_div - 0.8)) + noise_strength = sigma_up2 + ( + (noise_diff / (curr_leap * 0.4)) + * ((noise_div - (curr_leap / 2.0)).clamp(min=0, max=1.5) * 1.0) + ) + # (1.0 / (noise_div * 1.25))) + # noise_strength = sigma_up2 + ((noise_diff * 0.5) ** noise_div) + print( + f"SIGMA: norm_up={sigma_up_normal}, up={sigma_up}, up2={sigma_up2}, str={noise_strength}", + # noise_diff, + noise_div, + ) + return result, noise_strength + + noise_diff = sigma_up2 - sigma_up * dance_scale + noise_scale = sigma_up2 + noise_diff * (0.025 * curr_leap) + # noise_scale = sigma_up2 * self.ds_noise + if self.dyn_deta_mode == "deta" or dance_scale == 1.0: + return result, noise_scale + result = torch.lerp(x_normal, result, dance_scale) + # FIXME: Broken for noise samplers that care about s/sn + return result, noise_scale + + # def step(self, x, ss): + # eta = self.get_dyn_eta(ss) + # leap_sigmas = ss.sigmas[ss.idx :] + # leap_sigmas = leap_sigmas[: find_first_unsorted(leap_sigmas)] + # zero_idx = (leap_sigmas <= 0).nonzero().flatten()[:1] + # max_leap = (zero_idx.item() if len(zero_idx) else len(leap_sigmas)) - 1 + # is_danceable = max_leap > 1 and ss.sigma_next != 0 + # curr_leap = max(1, min(self.leap, max_leap)) + # sigma_leap = leap_sigmas[curr_leap] if is_danceable else ss.sigma_next + # # print("DANCE", max_leap, curr_leap, sigma_leap, "--", leap_sigmas) + # del leap_sigmas + # sigma_down, sigma_up = get_ancestral_step(ss.sigma, sigma_leap, eta) + # d = to_d(x, ss.sigma, ss.denoised) + # # Euler method + # dt = sigma_down - ss.sigma + # x = x + d * dt + # if curr_leap == 1: + # return x, sigma_up + # dance_scale = self.get_dyn_value(ss, self.dyn_deta_start, self.dyn_deta_end) + # if not is_danceable or abs(dance_scale) < 1e-04: + # print("NODANCE", dance_scale, self.deta) + # return x, sigma_up + # print("NODANCE", dance_scale, self.deta) + # sigma_down_normal, _sigma_up_normal = get_ancestral_step( + # ss.sigma, ss.sigma_next, eta + # ) + # if self.dyn_deta_mode == "lerp": + # dt_normal = sigma_down_normal - ss.sigma + # x_normal = x + d * dt_normal + # else: + # x_normal = x + # x = x + self.noise_sampler(ss.sigma, sigma_leap).mul_(self.s_noise * sigma_up) + # sigma_down2, sigma_up2 = get_ancestral_step( + # sigma_leap, + # ss.sigma_next, + # eta=self.deta * (1.0 if self.dyn_deta_mode != "deta" else dance_scale), + # ) + # d_2 = to_d(x, sigma_leap, ss.denoised) + # dt_2 = sigma_down2 - sigma_leap + # result = x + d_2 * dt_2 + # noise_diff = sigma_up2 - sigma_up * dance_scale + # noise_scale = sigma_up2 + noise_diff * (0.025 * curr_leap) + # if self.dyn_deta_mode == "deta" or dance_scale == 1.0: + # return result, noise_scale + # result = torch.lerp(x_normal, result, dance_scale) + # # FIXME: Broken for noise samplers that care about s/sn + # return result, noise_scale + + +class DPMPP2SStep(SingleStepSampler, DPMPPStepMixin): + name = "dpmpp_2s" + model_calls = 1 + + def step(self, x, ss): + t_fn, sigma_fn = self.t_fn, self.sigma_fn + sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) + # DPM-Solver++(2S) + t, t_next = t_fn(ss.sigma), t_fn(sigma_down) + r = 1 / 2 + h = t_next - t + s = t + r * h + x_2 = (sigma_fn(s) / sigma_fn(t)) * x - (-h * r).expm1() * ss.denoised + denoised_2 = ss.model(x_2, sigma_fn(s), ss=ss, call_index=1).denoised + x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised_2 + yield from self.result(ss, x, sigma_up) + + +class DPMPPSDEStep(SingleStepSampler, DPMPPStepMixin): + name = "dpmpp_sde" + self_noise = 1 + model_calls = 1 + + def __init__(self, *args, r=1 / 2, **kwargs): + super().__init__(*args, **kwargs) + self.r = r + + def step(self, x, ss): + t_fn, sigma_fn = self.t_fn, self.sigma_fn + r, eta = self.r, self.get_dyn_eta(ss) + # DPM-Solver++ + t, t_next = t_fn(ss.sigma), t_fn(ss.sigma_next) + h = t_next - t + s = t + h * r + fac = 1 / (2 * r) + + # Step 1 + sd, su = get_ancestral_step(sigma_fn(t), sigma_fn(s), eta) + s_ = t_fn(sd) + x_2 = (sigma_fn(s_) / sigma_fn(t)) * x - (t - s_).expm1() * ss.denoised + x_2 = yield from self.result( + ss, x_2, su, sigma=sigma_fn(t), sigma_next=sigma_fn(s), final=False + ) + denoised_2 = ss.model(x_2, sigma_fn(s), ss=ss, call_index=1).denoised + + # Step 2 + sd, su = get_ancestral_step(sigma_fn(t), sigma_fn(t_next), eta) + t_next_ = t_fn(sd) + denoised_d = (1 - fac) * ss.denoised + fac * denoised_2 + x = (sigma_fn(t_next_) / sigma_fn(t)) * x - (t - t_next_).expm1() * denoised_d + yield from self.result(ss, x, su) + + +# Based on implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers +# Which was originally written by Katherine Crowson +class TTMJVPStep(SingleStepSampler): + name = "ttm_jvp" + model_calls = 1 + + def __init__(self, *args, alternate_phi_2_calc=True, **kwargs): + super().__init__(*args, **kwargs) + self.alternate_phi_2_calc = alternate_phi_2_calc + + def step(self, x, ss): + eta = self.get_dyn_eta(ss) + sigma, sigma_next = ss.sigma, ss.sigma_next + # 2nd order truncated Taylor method + t, s = -sigma.log(), -sigma_next.log() + h = s - t + h_eta = h * (eta + 1) + + eps = to_d(x, sigma, ss.denoised) + denoised_prime = ss.model( + x, sigma, tangents=(eps * -sigma, -sigma), ss=ss, call_index=1 + ).jdenoised + + phi_1 = -torch.expm1(-h_eta) + if self.alternate_phi_2_calc: + phi_2 = torch.expm1(-h) + h # seems to work better with eta > 0 + else: + phi_2 = torch.expm1(-h_eta) + h_eta + x = torch.exp(-h_eta) * x + phi_1 * ss.denoised + phi_2 * denoised_prime + + noise_scale = ( + sigma_next * torch.sqrt(-torch.expm1(-2 * h * eta)) + if eta + else ss.sigma.new_zeros(1) + ) + yield from self.result(ss, x, noise_scale) + + +# Adapted from https://github.com/zju-pi/diff-sampler/blob/main/diff-solvers-main/solvers.py +# under Apache 2 license +class IPNDMStep(HistorySingleStepSampler): + name = "ipndm" + ancestralize = True + default_history_limit, max_history = 1, 3 + allow_alt_cfgpp = True + + IPNDM_MULTIPLIERS = ( + ((1,), 1), + ((3, -1), 2), + ((23, -16, 5), 12), + ((55, -59, 37, -9), 24), + ) + + def step(self, x, ss): + order = self.available_history(ss) + 1 + if order > 1: + hd = tuple(self.to_d(ss.hist[-hidx]) for hidx in range(order, 1, -1)) + (dm, *hms), divisor = self.IPNDM_MULTIPLIERS[order - 1] + noise = dm * self.to_d(ss.hcur) + for hidx, hm in enumerate(hms, start=1): + noise += hm * hd[-hidx] + noise /= divisor + yield from self.result(ss, x + ss.dt * noise) + + +# Adapted from https://github.com/zju-pi/diff-sampler/blob/main/diff-solvers-main/solvers.py +# under Apache 2 license +class IPNDMVStep(HistorySingleStepSampler): + name = "ipndm_v" + ancestralize = True + default_history_limit, max_history = 1, 3 + allow_alt_cfgpp = True + + def step(self, x, ss): + dt = ss.dt + d = self.to_d(ss.hcur) + order = self.available_history(ss) + 1 + if order > 1: + hd = tuple(self.to_d(ss.hist[-hidx]) for hidx in range(order, 1, -1)) + hns = ( + ss.sigmas[ss.idx - (order - 2) : ss.idx + 1] + - ss.sigmas[ss.idx - (order - 1) : ss.idx] + ) + if order == 1: + noise = d + elif order == 2: + coeff1 = (2 + (dt / hns[-1])) / 2 + coeff2 = -(dt / hns[-1]) / 2 + noise = coeff1 * d + coeff2 * hd[-1] + elif order == 3: + temp = ( + 1 + - dt + / (3 * (dt + hns[-1])) + * (dt * (dt + hns[-1])) + / (hns[-1] * (hns[-1] + hns[-2])) + ) / 2 + coeff1 = (2 + (dt / hns[-1])) / 2 + temp + coeff2 = -(dt / hns[-1]) / 2 - (1 + hns[-1] / hns[-2]) * temp + coeff3 = temp * hns[-1] / hns[-2] + noise = coeff1 * d + coeff2 * hd[-1] + coeff3 * hd[-2] + else: + temp1 = ( + 1 + - dt + / (3 * (dt + hns[-1])) + * (dt * (dt + hns[-1])) + / (hns[-1] * (hns[-1] + hns[-2])) + ) / 2 + temp2 = ( + ( + (1 - dt / (3 * (dt + hns[-1]))) / 2 + + (1 - dt / (2 * (dt + hns[-1]))) + * dt + / (6 * (dt + hns[-1] + hns[-2])) + ) + * (dt * (dt + hns[-1]) * (dt + hns[-1] + hns[-2])) + / (hns[-1] * (hns[-1] + hns[-2]) * (hns[-1] + hns[-2] + hns[-3])) + ) + coeff1 = (2 + (dt / hns[-1])) / 2 + temp1 + temp2 + coeff2 = ( + -(dt / hns[-1]) / 2 + - (1 + hns[-1] / hns[-2]) * temp1 + - ( + 1 + + (hns[-1] / hns[-2]) + + (hns[-1] * (hns[-1] + hns[-2]) / (hns[-2] * (hns[-2] + hns[-3]))) + ) + * temp2 + ) + coeff3 = ( + temp1 * hns[-1] / hns[-2] + + ( + (hns[-1] / hns[-2]) + + (hns[-1] * (hns[-1] + hns[-2]) / (hns[-2] * (hns[-2] + hns[-3]))) + * (1 + hns[-2] / hns[-3]) + ) + * temp2 + ) + coeff4 = ( + -temp2 + * (hns[-1] * (hns[-1] + hns[-2]) / (hns[-2] * (hns[-2] + hns[-3]))) + * hns[-1] + / hns[-2] + ) + noise = coeff1 * d + coeff2 * hd[-1] + coeff3 * hd[-2] + coeff4 * hd[-3] + yield from self.result(ss, x + ss.dt * noise) + + +class DEISStep(HistorySingleStepSampler): + name = "deis" + ancestralize = True + default_history_limit, max_history = 1, 3 + allow_alt_cfgpp = True + + def __init__(self, *args, deis_mode="tab", **kwargs): + super().__init__(*args, **kwargs) + self.deis_mode = deis_mode + self.deis_coeffs_key = None + self.deis_coeffs = None + + def get_deis_coeffs(self, ss): + key = ( + self.history_limit, + len(ss.sigmas), + ss.sigmas[0].item(), + ss.sigmas[-1].item(), + ) + if self.deis_coeffs_key == key: + return self.deis_coeffs + self.deis_coeffs_key = key + self.deis_coeffs = comfy.k_diffusion.deis.get_deis_coeff_list( + ss.sigmas, self.history_limit + 1, deis_mode=self.deis_mode + ) + return self.deis_coeffs + + def step(self, x, ss): + dt = ss.dt + d = self.to_d(ss.hcur) + order = self.available_history(ss) + 1 + if order < 2: + noise = dt * d # Euler + else: + c = self.get_deis_coeffs(ss)[ss.idx] + hd = tuple(self.to_d(ss.hist[-hidx]) for hidx in range(order, 1, -1)) + noise = c[0] * d + for i in range(1, order): + noise += c[i] * hd[-i] + yield from self.result(ss, x + noise) + + +class HeunPP2Step(SingleStepSampler): + name = "heunpp2" + ancestralize = True + model_calls = 2 + allow_alt_cfgpp = True + + def __init__(self, *args, max_order=3, **kwargs): + super().__init__(*args, **kwargs) + self.max_order = max(1, min(self.model_calls + 1, max_order)) + + def step(self, x, ss): + steps_remain = max(0, len(ss.sigmas) - (ss.idx + 2)) + order = min(self.max_order, steps_remain + 1) + sn = ss.sigma_next + if order == 1: + return (yield from self.euler_step(x, ss)) + d = self.to_d(ss.hcur) + dt = ss.dt + w = order * ss.sigma + w2 = sn / w + x_2 = x + d * dt + d_2 = self.to_d(ss.model(x_2, sn, ss=ss, call_index=1)) + if order == 2: + # Heun's method (ish) + w1 = 1 - w2 + d_prime = d * w1 + d_2 * w2 + else: + # Heun++ (ish) + snn = ss.sigmas[ss.idx + 2] + dt_2 = snn - sn + x_3 = x_2 + d_2 * dt_2 + d_3 = self.to_d(ss.model(x_3, snn, ss=ss, call_index=2)) + w3 = snn / w + w1 = 1 - w2 - w3 + d_prime = w1 * d + w2 * d_2 + w3 * d_3 + yield from self.result(ss, x + d_prime * dt) + + +class DESolverStep(SingleStepSampler, MinSigmaStepMixin): + de_default_solver = None + sample_sigma_zero = True + + def __init__( + self, + *args, + de_solver=None, + de_max_nfe=100, + de_rtol=-2.5, + de_atol=-3.5, + de_fixup_hack=0.025, + de_split=1, + de_min_sigma=0.0292, + **kwargs, + ): + self.check_solver_support() + if not HAVE_TDE: + raise RuntimeError( + "TDE sampler requires torchdiffeq installed in venv. Example: pip install torchdiffeq" + ) + super().__init__(*args, **kwargs) + de_solver = self.de_default_solver if de_solver is None else de_solver + self.de_solver_name = de_solver + self.de_max_nfe = de_max_nfe + self.de_rtol = 10**de_rtol + self.de_atol = 10**de_atol + self.de_fixup_hack = de_fixup_hack + self.de_split = de_split + self.de_min_sigma = de_min_sigma if de_min_sigma is not None else 0.0 + + def check_solver_support(self): + raise NotImplementedError + + def de_get_step(self, ss, x): + eta = self.get_dyn_eta(ss) + s, sn = ss.sigma, ss.sigma_next + sn = self.adjust_step(sn, self.de_min_sigma) + sigma_down, sigma_up = ss.get_ancestral_step(eta, sigma_next=sn) + if self.de_fixup_hack != 0: + sigma_down = (sigma_down - (s - sigma_down) * self.de_fixup_hack).clamp( + min=0 + ) + return s, sn, sigma_down, sigma_up + + @staticmethod + def reverse_time(t, t0, t1): + return t1 + (t0 - t) + + +class TDEStep(DESolverStep): + name = "tde" + model_calls = 2 + allow_alt_cfgpp = True + allow_cfgpp = False + de_default_solver = "rk4" + + def __init__( + self, + *args, + de_split=1, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.de_split = de_split + + def check_solver_support(self): + if not HAVE_TDE: + raise RuntimeError( + "TDE sampler requires torchdiffeq installed in venv. Example: pip install torchdiffeq" + ) + + def step(self, x, ss): + s, sn, sigma_down, sigma_up = self.de_get_step(ss, x) + if self.de_min_sigma is not None and s <= self.de_min_sigma: + return (yield from self.euler_step(x, ss)) + delta = (s - sigma_down).item() + mcc = 0 + bidx = 0 + pbar = None + + def odefn(t, y): + nonlocal mcc + if t < 1e-05: + return torch.zeros_like(y) + if mcc >= self.de_max_nfe: + raise RuntimeError("TDEStep: Model call limit exceeded") + + pct = (s - t) / delta + pbar.n = round(min(999, pct.item() * 999)) + pbar.update(0) + pbar.set_description( + f"{self.de_solver_name}({mcc}/{self.de_max_nfe})", refresh=True + ) + + if t == ss.sigma and torch.equal(x[bidx], y): + mr = ss.hcur + mcc = 1 + else: + mr = ss.model( + y.unsqueeze(0), t, ss=ss, call_index=mcc, s_in=t.new_ones(1) + ) + mcc += 1 + return self.to_d(mr)[0] + + result = torch.zeros_like(x) + t = sigma_down.new_zeros(self.de_split + 1) + torch.linspace(ss.sigma, sigma_down, t.shape[0], out=t) + + for batch in tqdm.trange( + 1, + x.shape[0] + 1, + desc="batch", + leave=True, + disable=x.shape[0] == 1 or ss.disable_status, + ): + bidx = batch - 1 + mcc = 0 + if pbar is not None: + pbar.close() + pbar = tqdm.tqdm( + total=1000, + desc=self.de_solver_name, + leave=True, + disable=ss.disable_status, + ) + solution = tde.odeint( + odefn, + x[bidx], + t, + rtol=self.de_rtol, + atol=self.de_atol, + method=self.de_solver_name, + options={ + "min_step": 1e-05, + "dtype": torch.float64, + }, + )[-1] + result[bidx] = solution + + sigma_up, result = yield from self.adjusted_step(ss, sn, result, mcc, sigma_up) + if pbar is not None: + pbar.n = pbar.total + pbar.update(0) + pbar.close() + yield from self.result(ss, result, sigma_up, sigma_down=sigma_down) + + +class TODEStep(DESolverStep): + name = "tode" + model_calls = 2 + allow_alt_cfgpp = True + de_default_solver = "dopri5" + + def __init__( + self, + *args, + de_initial_step=0.25, + tode_compile=False, + de_ctl_pcoeff=0.3, + de_ctl_icoeff=0.9, + de_ctl_dcoeff=0.2, + **kwargs, + ): + if not HAVE_TODE: + raise RuntimeError( + "TODE sampler requires torchode installed in venv. Example: pip install torchode" + ) + super().__init__(*args, **kwargs) + self.de_solver_method = tode.interface.METHODS[self.de_solver_name] + self.de_ctl_pcoeff = de_ctl_pcoeff + self.de_ctl_icoeff = de_ctl_icoeff + self.de_ctl_dcoeff = de_ctl_dcoeff + self.de_compile = tode_compile + self.de_initial_step = de_initial_step + + def check_solver_support(self): + if not HAVE_TODE: + raise RuntimeError( + "TODE sampler requires torchode installed in venv. Example: pip install torchode" + ) + + def step(self, x, ss): + s, sn, sigma_down, sigma_up = self.de_get_step(ss, x) + if self.de_min_sigma is not None and s <= self.de_min_sigma: + return (yield from self.euler_step(x, ss)) + delta = (ss.sigma - sigma_down).item() + mcc = 0 + pbar = None + b, c, h, w = x.shape + + def odefn(t, y_flat): + nonlocal mcc + if torch.all(t <= 1e-05).item(): + return torch.zeros_like(y_flat) + if mcc >= self.de_max_nfe: + raise RuntimeError("TDEStep: Model call limit exceeded") + + pct = (s - t) / delta + pbar.n = round(pct.min().item() * 999) + pbar.update(0) + pbar.set_description( + f"{self.de_solver_name}({mcc}/{self.de_max_nfe})", refresh=True + ) + y = y_flat.reshape(-1, c, h, w) + t32 = t.to(torch.float32) + del y_flat + + if mcc == 0 and torch.all(t == s): + mr = ss.hcur + mcc = 1 + else: + mr = ss.model(y, t32.clamp(min=1e-05), ss=ss, call_index=mcc) + mcc += 1 + result = self.to_d(mr).flatten(start_dim=1) + for bi in range(t.shape[0]): + if t[bi] <= 1e-05: + result[bi, :] = 0 + return result + + t = torch.stack((s, sigma_down)).to(torch.float64).repeat(b, 1) + + pbar = tqdm.tqdm( + total=1000, desc=self.de_solver_name, leave=True, disable=ss.disable_status + ) + + term = tode.ODETerm(odefn) + method = self.de_solver_method(term=term) + controller = tode.PIDController( + term=term, + atol=self.de_atol, + rtol=self.de_rtol, + dt_min=1e-05, + pcoeff=self.de_ctl_pcoeff, + icoeff=self.de_ctl_icoeff, + dcoeff=self.de_ctl_dcoeff, + ) + solver_ = tode.AutoDiffAdjoint(method, controller) + solver = solver_ if not self.de_compile else torch.compile(solver_) + problem = tode.InitialValueProblem( + y0=x.flatten(start_dim=1), t_start=t[:, 0], t_end=t[:, -1] + ) + dt0 = ( + (t[:, -1] - t[:, 0]) * self.de_initial_step + if self.de_initial_step + else None + ) + solution = solver.solve(problem, dt0=dt0) + + # print("\nSOLUTION", solution.stats, solution.ys.shape) + result = solution.ys[:, -1].reshape(-1, c, h, w) + del solution + + sigma_up, result = yield from self.adjusted_step(ss, sn, result, mcc, sigma_up) + if pbar is not None: + pbar.n = pbar.total + pbar.update(0) + pbar.close() + yield from self.result(ss, result, sigma_up, sigma_down=sigma_down) + + +class TSDEStep(DESolverStep): + name = "tsde" + model_calls = 2 + allow_alt_cfgpp = True + de_default_solver = "reversible_heun" + + def __init__( + self, + *args, + de_initial_step=0.25, + de_split=1, + de_adaptive=False, + tsde_noise_type="scalar", + tsde_sde_type="stratonovich", + tsde_levy_area_approx="none", + tsde_noise_channels=1, + tsde_g_multiplier=0.05, + tsde_g_reverse_time=True, + tsde_g_derp_mode=False, + tsde_batch_channels=True, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.de_initial_step = de_initial_step + self.de_adaptive = de_adaptive + self.de_split = de_split + self.de_noise_type = tsde_noise_type + self.de_sde_type = tsde_sde_type + self.de_levy_area_approx = tsde_levy_area_approx + self.de_g_multiplier = tsde_g_multiplier + self.de_noise_channels = tsde_noise_channels + self.de_g_reverse_time = tsde_g_reverse_time + self.de_g_derp_mode = tsde_g_derp_mode + self.de_batch_channels = tsde_batch_channels + + def check_solver_support(self): + pass + + def step(self, x, ss): + s, sn, sigma_down, sigma_up = self.de_get_step(ss, x) + if self.de_min_sigma is not None and s <= self.de_min_sigma: + return (yield from self.euler_step(x, ss)) + delta = (ss.sigma - sigma_down).item() + mcc = 0 + pbar = None + _b, c, h, w = x.shape + outer_self = self + + class SDE(torch.nn.Module): + noise_type = outer_self.de_noise_type + sde_type = outer_self.de_sde_type + + @torch.no_grad() + def f(self, t_rev, y_flat): + nonlocal mcc + t = s - (t_rev - sigma_down) + # print(f"\nf at t_rev={t_rev}, t={t} :: {y_flat.shape}") + if torch.all(t <= 1e-05).item(): + return torch.zeros_like(y_flat) + if mcc >= outer_self.de_max_nfe: + raise RuntimeError("TSDEStep: Model call limit exceeded") + + pct = (s - t) / delta + pbar.n = round(pct.min().item() * 999) + pbar.update(0) + pbar.set_description( + f"{outer_self.de_solver_name}({mcc}/{outer_self.de_max_nfe})", + refresh=True, + ) + flat_shape = y_flat.shape + y = y_flat.view(1, c, h, w) + t32 = t.to(torch.float32) + del y_flat + + if mcc == 0 and torch.all(t == s): + mr = ss.hcur + mcc = 1 + else: + mr = ss.model(y, t32.clamp(min=1e-05), ss=ss, call_index=mcc) + mcc += 1 + return -outer_self.to_d(mr).view(*flat_shape) + + @torch.no_grad() + def g(self, t_rev, y_flat): + t = (s - sigma_down) - (t_rev - sigma_down) + pct = t / (s - sigma_down) + if outer_self.de_g_reverse_time: + pct = 1.0 - pct + multiplier = outer_self.de_g_multiplier + if outer_self.de_g_derp_mode and mcc % 2 == 0: + multiplier *= -1 + val = t * pct * multiplier + if self.noise_type == "diagonal": + out = val.repeat(*y_flat.shape) + elif self.noise_type == "scalar": + out = val.repeat(*y_flat.shape, 1) + else: + out = val.repeat(*y_flat.shape, outer_self.de_noise_channels) + return out + + t = torch.stack((sigma_down, s)).to(torch.float) + + pbar = tqdm.tqdm( + total=1000, desc=self.de_solver_name, leave=True, disable=ss.disable_status + ) + + dt0 = ( + delta * self.de_initial_step if self.de_adaptive else delta / self.de_split + ) + results = [] + for bidx in range(x.shape[0]): + sde = SDE() + if self.de_batch_channels: + y_flat = x[bidx].flatten(start_dim=1) + else: + y_flat = x[bidx].unsqueeze(0).flatten(start_dim=1) + if sde.noise_type == "diagonal": + bm_size = (y_flat.shape[0], y_flat.shape[1]) + elif sde.noise_type == "scalar": + bm_size = (y_flat.shape[0], 1) + else: + bm_size = (y_flat.shape[0], self.de_noise_channels) + bm = torchsde.BrownianInterval( + dtype=x.dtype, + device=x.device, + t0=-s, + t1=s, + entropy=ss.noise.seed, + levy_area_approximation=self.de_levy_area_approx, + tol=1e-06, + size=bm_size, + ) + + ys = torchsde.sdeint( + sde, + y_flat, + t, + method=self.de_solver_name, + adaptive=self.de_adaptive, + atol=self.de_atol, + rtol=self.de_rtol, + dt=dt0, + bm=bm, + ) + del y_flat + # print("DONE", ys.shape) + results.append(ys[-1].view(1, c, h, w)) + # result = ys[-1].reshape(-1, c, h, w) + del ys + result = torch.cat(results) + del results + + sigma_up, result = yield from self.adjusted_step(ss, sn, result, mcc, sigma_up) + if pbar is not None: + pbar.n = pbar.total + pbar.update(0) + pbar.close() + yield from self.result(ss, result, sigma_up, sigma_down=sigma_down) + + +if HAVE_DIFFRAX: + + class RevVirtualBrownianTree(diffrax.VirtualBrownianTree): + def evaluate(self, t0, t1, *args, **kwargs): + if t1 is not None: + return super().evaluate(t1, t0, *args, **kwargs) + return super().evaluate(t0, t1, *args, **kwargs) + + class StepCallbackTqdmProgressMeter(diffrax.TqdmProgressMeter): + step_callback: typing.Callable = None + + def _init_bar(self, *args, **kwargs): + if self.step_callback is None: + return super()._init_bar(*args, **kwargs) + bar_format = "{percentage:.2f}%{step_callback}|{bar}| [{elapsed}<{remaining}, {rate_fmt}{postfix}]" + step_callback = self.step_callback + + class WrapTqdm(tqdm.tqdm): + @property + def format_dict(self): + d = super().format_dict + d.update(step_callback=step_callback()) + return d + + return WrapTqdm(total=100, unit="%", bar_format=bar_format) + + +class DiffraxStep(DESolverStep): + name = "diffrax" + model_calls = 2 + allow_alt_cfgpp = True + de_default_solver = "dopri5" + + def __init__( + self, + *args, + de_split=1, + de_initial_step=0.25, + de_ctl_pcoeff=0.3, + de_ctl_icoeff=0.9, + de_ctl_dcoeff=0.2, + diffrax_adaptive=False, + diffrax_fake_pure_callback=True, + diffrax_g_multiplier=0.0, + diffrax_half_solver=False, + diffrax_batch_channels=False, + diffrax_levy_area_approx="brownian_increment", + diffrax_error_order=None, + diffrax_sde_mode=False, + diffrax_g_reverse_time=False, + diffrax_g_time_scaling=False, + diffrax_g_split_time_mode=False, + **kwargs, + ): + super().__init__(*args, **kwargs) + solvers = dict( + euler=diffrax.Euler, + heun=diffrax.Heun, + midpoint=diffrax.Midpoint, + ralston=diffrax.Ralston, + bosh3=diffrax.Bosh3, + tsit5=diffrax.Tsit5, + dopri5=diffrax.Dopri5, + dopri8=diffrax.Dopri8, + implicit_euler=diffrax.ImplicitEuler, + # kvaerno3=diffrax.Kvaerno3, + # kvaerno4=diffrax.Kvaerno4, + # kvaerno5=diffrax.Kvaerno5, + semi_implicit_euler=diffrax.SemiImplicitEuler, + reversible_heun=diffrax.ReversibleHeun, + leapfrog_midpoint=diffrax.LeapfrogMidpoint, + euler_heun=diffrax.EulerHeun, + ito_milstein=diffrax.ItoMilstein, + stratonovich_milstein=diffrax.StratonovichMilstein, + sea=diffrax.SEA, + sra1=diffrax.SRA1, + shark=diffrax.ShARK, + general_shark=diffrax.GeneralShARK, + slow_rk=diffrax.SlowRK, + spark=diffrax.SPaRK, + ) + levy_areas = dict( + brownian_increment=diffrax.BrownianIncrement, + space_time=diffrax.SpaceTimeLevyArea, + space_time_time=diffrax.SpaceTimeTimeLevyArea, + ) + # jax.config.update("jax_disable_jit", True) + self.de_solver_method = solvers[self.de_solver_name]() + if diffrax_half_solver: + self.de_solver_method = diffrax.HalfSolver(self.de_solver_method) + self.de_ctl_pcoeff = de_ctl_pcoeff + self.de_ctl_icoeff = de_ctl_icoeff + self.de_ctl_dcoeff = de_ctl_dcoeff + self.de_initial_step = de_initial_step + self.de_adaptive = diffrax_adaptive + self.de_split = de_split + self.de_fake_pure_callback = diffrax_fake_pure_callback + self.de_g_multiplier = diffrax_g_multiplier + self.de_batch_channels = diffrax_batch_channels + self.de_levy_area_approx = levy_areas[diffrax_levy_area_approx] + self.de_error_order = diffrax_error_order + self.de_sde_mode = diffrax_sde_mode + self.de_g_reverse_time = diffrax_g_reverse_time + self.de_g_time_scaling = diffrax_g_time_scaling + self.de_g_split_time_mode = diffrax_g_split_time_mode + + # As slow and safe as possible. + @staticmethod + def t2j(t): + return jax.block_until_ready( + jax.numpy.array(numpy.array(t.detach().cpu().contiguous())) + ) + + @staticmethod + def j2t(t): + return torch.from_numpy(numpy.array(jax.block_until_ready(t))).contiguous() + + def check_solver_support(self): + if not HAVE_DIFFRAX: + raise RuntimeError( + "Diffrax sampler requires diffrax and jax installed in venv." + ) + + def step(self, x, ss): + s, sn, sigma_down, sigma_up = self.de_get_step(ss, x) + if self.de_min_sigma is not None and s <= self.de_min_sigma: + return (yield from self.euler_step(x, ss)) + mcc = 0 + _b, c, h, w = x.shape + interrupted = None + t0, t1 = sigma_down.item(), s.item() + + def odefn_(t_orig, y_flat, args=()): + nonlocal mcc, interrupted + t = self.reverse_time(self.j2t(t_orig).to(s), t0, t1) + if t <= 1e-05: + return jax.numpy.zeros_like(y_flat) + if mcc >= self.de_max_nfe: + raise RuntimeError("DiffraxStep: Model call limit exceeded") + y = self.j2t(y_flat.reshape(1, c, h, w)).to(x) + t32 = t.to(s).clamp(min=1e-05) + flat_shape = y_flat.shape + del y_flat + + if not args and mcc == 0 and torch.all(t == s): + mr = ss.hcur + mcc = 1 + else: + try: + if not args: + mr = ss.model(y, t32, ss=ss, call_index=mcc) + else: + print("TANGENTS") + mr = ss.model(y, t32, ss=ss, call_index=mcc, tangents=args) + except comfy.model_management.InterruptProcessingException as exc: + interrupted = exc + raise + mcc += 1 + result = self.to_d(mr).reshape(*flat_shape) + return self.t2j(-result) + + if not self.de_fake_pure_callback: + + def odefn(t, y_flat, args): + return jax.experimental.io_callback( + odefn_, y_flat, t, y_flat, ordered=True + ) + + else: + + def odefn(t, y_flat, args): + return jax.pure_callback(odefn_, y_flat, t, y_flat) + + def g(t, y, _args): + if self.de_g_split_time_mode: + val = jax.lax.cond( + t < t0 + (t1 - t0) * 0.5, + lambda: self.de_g_multiplier, + lambda: -self.de_g_multiplier, + ) + else: + val = self.de_g_multiplier + if self.de_g_time_scaling: + val *= self.reverse_time(t, t0, t1) if self.de_g_reverse_time else t + if not self.de_batch_channels: + return val + return jax.numpy.float32(val).broadcast((y.shape[0],)) + + def progress_callback(): + return f" ({mcc:>3}/{self.de_max_nfe:>3}) {self.de_solver_name}" + + term = diffrax.ODETerm(odefn) + method = self.de_solver_method + if self.de_adaptive: + controller = diffrax.PIDController( + atol=self.de_atol, + rtol=self.de_rtol, + dtmin=1e-05, + pcoeff=self.de_ctl_pcoeff, + icoeff=self.de_ctl_icoeff, + dcoeff=self.de_ctl_dcoeff, + error_order=self.de_error_order, + ) + else: + controller = diffrax.ConstantStepSize() + + if not self.de_adaptive: + dt0 = (t1 - t0) / self.de_split + else: + dt0 = (t1 - t0) * self.de_initial_step + if self.de_sde_mode: + bm = diffrax.VirtualBrownianTree( + # t0, + # t1 + 1e-06, + t0=ss.sigmas.min().item(), + t1=ss.sigmas.max().item(), + tol=1e-06, + levy_area=self.de_levy_area_approx, + shape=(c,) if self.de_batch_channels else (), + key=jax.random.PRNGKey(ss.noise.seed + ss.noise.seed_offset), + ) + term = diffrax.MultiTerm(term, diffrax.ControlTerm(g, bm)) + results = [] + for bidx in range(x.shape[0]): + if self.de_batch_channels: + y_flat = x[bidx].flatten(start_dim=1) + else: + y_flat = x[bidx].unsqueeze(0).flatten(start_dim=1) + y_flat = self.t2j(y_flat) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=FutureWarning) + try: + solution = diffrax.diffeqsolve( + terms=term, + solver=method, + t0=t0, + t1=t1, + dt0=dt0, + y0=y_flat, + saveat=diffrax.SaveAt(t1=True), + stepsize_controller=controller, + progress_meter=StepCallbackTqdmProgressMeter( + step_callback=progress_callback, + refresh_steps=1, + ), + ) + except Exception: + if interrupted is not None: + raise interrupted + raise + results.append(self.j2t(solution.ys).view(*x.shape)) + del solution + result = torch.cat(results).to(x) + sigma_up, result = yield from self.adjusted_step(ss, sn, result, mcc, sigma_up) + yield from self.result(ss, result, sigma_up, sigma_down=sigma_down) + + +class HeunStep(ReversibleSingleStepSampler): + name = "heun" + model_calls = 1 + default_history_limit, max_history = 0, 0 + allow_alt_cfgpp = True + + def reversible_correction(self, ss, d_from, d_to): + reta, reversible_scale = self.get_reversible_cfg(ss) + if reversible_scale == 0: + return 0 + sdr = ss.get_ancestral_step(reta)[0] + dtr = sdr - ss.sigma + return (dtr**2 * (d_to - d_from) / 4) * self.reversible_scale + + def step(self, x, ss): + s = ss.sigma + sd, su = ss.get_ancestral_step(self.get_dyn_eta(ss)) + dt = sd - s + hcur = ss.hcur + d = self.to_d(hcur) + x_next = hcur.denoised + d * sd + d_next = self.to_d(ss.model(x_next, sd, ss=ss, call_index=1)) + result = hcur.denoised + d * s + result += (dt * (d + d_next)) * 0.5 + result -= self.reversible_correction(ss, d, d_next) + yield from self.result(ss, result, su) + + +class Heun1SStep(HeunStep): + name = "heun_1s" + model_calls = 1 + allow_alt_cfgpp = True + default_history_limit, max_history = 1, 1 + + def step(self, x, ss): + s = ss.sigma + if self.available_history(ss) == 0: + return (yield from super().step(x, ss)) + hcur, hprev = ss.hcur, ss.hprev + d_prev = self.to_d(hprev) + sd, su = ss.get_ancestral_step(self.get_dyn_eta(ss)) + dt = sd - s + d = self.to_d(hcur) + result = hcur.denoised + hcur.sigma * self.to_d(hcur) + result += (dt * (d_prev + d)) * 0.5 + result -= self.reversible_correction(ss, d_prev, d) + yield from self.result(ss, result, su) + + +class AdapterStep(SingleStepSampler): + name = "adapter" + model_calls = 2 + immiscible = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.external_sampler = self.options.pop( + "SAMPLER", comfy.samplers.sampler_object("euler") + ) + sig = inspect.signature(self.external_sampler.sampler_function) + self.external_sampler_options = { + k: v + for k, v in self.options.pop("external_sampler", {}).items() + if k in sig.parameters + } + self.external_sampler_uses_noise = "noise_sampler" in sig.parameters + self.ancestralize = self.options.pop("ancestralize", self.ancestralize) is True + + def step(self, x, ss): + sigmas = ss.sigmas[ss.idx : ss.idx + 2] + kwargs = { + "callback": None, + "disable": True, + "extra_args": {"seed": ss.noise.seed + ss.noise.seed_offset}, + } | self.external_sampler_options + if self.external_sampler_uses_noise: + kwargs["noise_sampler"] = ss.noise.make_caching_noise_sampler( + self.options.get("custom_noise"), + 1, + sigmas[-1], + sigmas[0], + immiscible=fallback(self.immiscible, ss.noise.immiscible), + ) + + mcc = 1 + + def model_wrapper(x_, sigma_, *args, **kwargs): + nonlocal mcc + if torch.equal(x_, x) and sigma_ == ss.sigma: + return ss.hcur.denoised.clone() + mr = ss.model(x_, sigma_, *args, ss=ss, call_index=mcc, **kwargs) + mcc += 1 + return mr.denoised.clone() + + result = self.external_sampler.sampler_function( + model_wrapper, x.clone(), sigmas, **kwargs + ) + yield from self.result(ss, result, ss.sigma.new_zeros(1)) + + +STEP_SAMPLERS = { + "default (euler)": EulerStep, + "adapter (variable)": AdapterStep, + "bogacki (2)": BogackiStep, + "deis": DEISStep, + "dpmpp_2m_sde": DPMPP2MSDEStep, + "dpmpp_2m": DPMPP2MStep, + "dpmpp_2s": DPMPP2SStep, + "dpmpp_3m_sde": DPMPP3MSDEStep, + "dpmpp_sde (1)": DPMPPSDEStep, + "euler_cycle": EulerCycleStep, + "euler_dancing": EulerDancingStep, + "euler": EulerStep, + "heun (1)": HeunStep, + "heun_1s (1)": Heun1SStep, + "heunpp (1-2)": HeunPP2Step, + "ipndm_v": IPNDMVStep, + "ipndm": IPNDMStep, + "res (1)": RESStep, + "reversible_bogacki (2)": ReversibleBogackiStep, + "reversible_heun (1)": ReversibleHeunStep, + "reversible_heun_1s": ReversibleHeun1SStep, + "rk4 (3)": RK4Step, + "solver_diffrax (variable)": DiffraxStep, + "solver_torchdiffeq (variable)": TDEStep, + "solver_torchode (variable)": TODEStep, + "solver_torchsde (variable)": TSDEStep, + "trapezoidal (1)": TrapezoidalStep, + "trapezoidal_cycle (1)": TrapezoidalCycleStep, + "ttm_jvp (1)": TTMJVPStep, +} + +__all__ = ( + "STEP_SAMPLERS", + "EulerStep", + "EulerCycleStep", + "DPMPP2MStep", + "DPMPP2MSDEStep", + "DPMPP3MSDEStep", + "DPMPP2SStep", + "ReversibleHeunStep", + "ReversibleHeun1SStep", + "RESStep", + "TrapezoidalCycleStep", + "TrapezoidalStep", + "BogackiStep", + "ReversibleBogackiStep", + "EulerDancingStep", + "TTMJVPStep", + "IPNDMStep", + "IPNDMVStep", + "TDEStep", +) diff --git a/py/substep_merging.py b/py/substep_merging.py index 8230dc4..6705bcb 100644 --- a/py/substep_merging.py +++ b/py/substep_merging.py @@ -1,202 +1,363 @@ -import torch +import operator -from .utils import scale_noise, find_first_unsorted -from .substep_sampling import History +import torch +import tqdm + +from . import expression as expr +from . import utils + +from .filtering import make_filter, FilterRefs +from .restart import Restart +from .step_samplers import STEP_SAMPLERS +from .utils import check_time, fallback class MergeSubstepsSampler: - def __init__(self, ss, samplers, **_kwargs): + name = "unknown" + + def __init__(self, ss, group): + samplers = tuple( + STEP_SAMPLERS[sitem["step_method"]](**sitem) for sitem in group.items + ) + options = group.options.copy() + self.time_mode = group.time_mode + self.time_start = group.time_start + self.time_end = group.time_end self.ss = ss self.samplers = samplers self.substeps = sum(sampler.substeps for sampler in samplers) + when_expr = options.pop("when", None) + self.when = expr.Expression(when_expr) if when_expr else None + pre_filter = options.pop("pre_filter", None) + post_filter = options.pop("post_filter", None) + self.pre_filter = None if pre_filter is None else make_filter(pre_filter) + self.post_filter = None if post_filter is None else make_filter(post_filter) + self.options = options + + def check_match(self, handlers: None | object, *, ss: None | object = None): + ss = fallback(ss, self.ss) + if not check_time( + self.time_mode, + self.time_start, + self.time_end, + ss.sigma, + ss.step, + ss.total_steps, + ): + return False + if self.when is None: + return True + if handlers is None: + raise ValueError("Group has when expression but handlers not passed") + return operator.truth(self.when.eval(handlers)) + + def step_input(self, x, *, ss=None): + if self.pre_filter is None: + return x + ss = fallback(ss, self.ss) + return self.pre_filter.apply(x, refs=fallback(ss, self.ss).refs) + + def step_output(self, x, *, orig_x=None, ss=None): + if self.post_filter is None: + return x + ss = fallback(ss, self.ss) + refs = ss.refs if orig_x is None else ss.refs | FilterRefs({"orig_x": orig_x}) + return self.post_filter.apply(x, refs=refs) + + def __call__(self, x): + orig_x = x + x = self.step_input(x) + x = self.step(x) + return self.step_output(x, orig_x=orig_x) def step(self, x): raise NotImplementedError - def merge_steps(self, _x, result): + def substep(self, x, sampler, ss=None): + sg = sampler(x, fallback(ss, self.ss)) + yield from utils.step_generator(sg, get_next=lambda sr: sr.x) + + def simple_substep(self, x, sampler, ss=None): + for sr in self.substep(x, sampler, ss=ss): + if not sr.final: + sr.noise_x(ss=fallback(ss, self.ss)) + return sr + + def merge_steps(self, x, result=None, *, noise=None, ss=None, denoised=True): + ss = ss if ss is not None else self.ss + result = fallback(result, x) + if noise is not None: + result = result + noise return result + def step_max_noise_samples(self): + return sum( + (1 + sampler.self_noise) * sampler.substeps for sampler in self.samplers + ) + + def reset(self): + pass + + +class SimpleSubstepsSampler(MergeSubstepsSampler): + name = "simple" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not len(self.samplers): + raise ValueError("Missing sampler") + + def step_max_noise_samples(self): + return 1 + self.samplers[0].self_noise + + def step(self, x): + ss, ssampler = self.ss, self.samplers[0] + custom_noise = ssampler.options.get( + "custom_noise", self.options.get("custom_noise") + ) + noise_sampler = ss.noise.make_caching_noise_sampler( + custom_noise, + 1, + ss.sigma, + ss.sigma_next, + immiscible=fallback(ssampler.immiscible, ss.noise.immiscible), + ) + ssampler.noise_sampler = noise_sampler + ss.hist.push(ss.model(x, ss.sigma, ss=ss)) + ss.refs = FilterRefs.from_ss(ss, have_current=True) + ss.callback() + sr = self.simple_substep(x, ssampler) + return self.merge_steps(sr.x, noise=sr.get_noise(ss=ss)) + class NormalMergeSubstepsSampler(MergeSubstepsSampler): - def __init__(self, ss, samplers, **kwargs): - super().__init__(ss, samplers, **kwargs) - self.ss = ss + name = "normal" def step(self, x): ss = self.ss substeps = self.substeps renoise_weight = 1.0 / substeps z_avg = torch.zeros_like(x) - noise = torch.zeros_like(x) + noise = z_avg.clone() noise_total = 0.0 - for idx, ssampler in enumerate( - sampler for sampler in self.samplers for _ in range(sampler.substeps) - ): - print(f" SUBSTEP {idx+1}: {ssampler.name}") - ss.denoised = ss.model(x, ss.sigma) - z_k, noise_strength = ssampler.step(x, ss) - z_avg += renoise_weight * z_k - noise_strength *= ssampler.s_noise - if ss.sigma_next == 0 or noise_strength == 0: - continue - noise_curr = ssampler.noise_sampler(ss.sigma, ss.sigma_next) - x = z_k - if idx != substeps - 1: - x += noise_curr * noise_strength - noise_total += noise_strength.item() * renoise_weight - noise += noise_curr * noise_strength - ss.dhist.push(ss.denoised) - ss.denoised = None - x = self.merge_steps(x, z_avg) - if ss.sigma_next != 0 and noise_total != 0: - x += scale_noise(noise, noise_total * ss.s_noise) - ss.xhist.push(x) - ss.callback(x) - return x - - -class AverageMergeSubstepsSampler(NormalMergeSubstepsSampler): - def __init__(self, ss, samplers, *, avgmerge_stretch=0.4, **kwargs): - super().__init__(ss, samplers, **kwargs) - self.ss = ss - self.stretch = avgmerge_stretch - - def step(self, x): - ss = orig_ss = self.ss - substeps = self.substeps - renoise_weight = 1.0 / substeps - z_avg = torch.zeros_like(x) - noise = torch.zeros_like(x) - stretch = (ss.sigma - ss.sigma_next) * self.stretch - sig_adj = ss.sigma + stretch - ss = self.ss.clone_edit(sigma=sig_adj) - orig_x = x - x = x + ss.noise_sampler(orig_ss.sigma, ss.sigma_next) * stretch * ss.s_noise - ss.denoised = ss.model(x, sig_adj) - noise_total = 0.0 - step = 0 - for idx, ssampler in enumerate(self.samplers): - print( - f" SUBSTEP {step+1} .. {step+ssampler.substeps}: {ssampler.name}, stretch={stretch}" + substep = 0 + pbar = tqdm.tqdm(total=self.substeps, initial=1, disable=ss.disable_status) + ss.hist.push(ss.model(x, ss.sigma, ss=ss)) + ss.refs = FilterRefs.from_ss(ss, have_current=True) + ss.callback() + for ssampler in self.samplers: + custom_noise = ssampler.options.get( + "custom_noise", self.options.get("custom_noise") ) - for sidx in range(ssampler.substeps): - curr_x = orig_x + scale_noise( - ssampler.noise_sampler(sig_adj, ss.sigma_next), stretch - ) - z_k, noise_strength = ssampler.step(curr_x, ss) - z_avg += renoise_weight * z_k - if ss.sigma_next == 0: - continue - noise_strength *= ssampler.s_noise - if noise_strength == 0: - continue - noise_curr = ssampler.noise_sampler(ss.sigma, ss.sigma_next) - noise_total += noise_strength.item() * renoise_weight - noise += noise_curr * noise_strength - step += ssampler.substeps - ss.dhist.push(ss.denoised) - ss.denoised = None - x = self.merge_steps(x, z_avg) - if ss.sigma_next != 0 and noise_total != 0: - x += scale_noise(noise, noise_total * ss.s_noise) - ss.xhist.push(x) - ss.callback(x) - return x - - -class SampleMergeSubstepsSampler(AverageMergeSubstepsSampler): - cache_model = True - - def __init__(self, ss, samplers, *, merge_sampler, **kwargs): - super().__init__(ss, samplers, **kwargs) - self.merge_sampler = merge_sampler - self.merge_ss = None - - def step(self, x): - ss = self.ss - substeps = self.substeps - renoise_weight = 1.0 / substeps - z_avg = torch.zeros_like(x) - curr_x = x - ss.denoised = None - stretch = (ss.sigma - ss.sigma_next) * self.stretch - sig_adj = ss.sigma + stretch - ss = self.ss.clone_edit(sigma=sig_adj) - step = 0 - for idx, ssampler in enumerate(self.samplers): - print( - f" SUBSTEP {step+1} .. {step+ssampler.substeps}: {ssampler.name}, stretch={stretch}" + noise_sampler = ss.noise.make_caching_noise_sampler( + custom_noise, + ssampler.max_noise_samples(), + ss.sigma, + ss.sigma_next, + immiscible=fallback(ssampler.immiscible, ss.noise.immiscible), ) - if idx == 0 or not self.cache_model: - ss.denoised = ss.model( - curr_x, - # + ss.noise_sampler(sig_adj.sigma, ss.sigma_next) * stretch * ss.s_noise, - sig_adj, - ) - for sidx in range(ssampler.substeps): - curr_x = ( - x - + ssampler.noise_sampler(sig_adj, ss.sigma_next) - * ssampler.s_noise - * stretch - ) - z_k, noise_strength = ssampler.step(curr_x, ss) - z_avg += renoise_weight * z_k - curr_x = z_k - if noise_strength == 0 or ss.sigma_next == 0: - continue - curr_x += ( - ssampler.noise_sampler(ss.sigma, ss.sigma_next) - * ssampler.s_noise - * noise_strength - ) - step += ssampler.substeps - ss.dhist.push(ss.denoised) - ss.denoised = None - x = self.merge_steps(curr_x, z_avg) - ss.xhist.push(x) - ss.callback(x) - return x + ssampler.noise_sampler = noise_sampler + for subidx in range(ssampler.substeps): + pbar.set_description(f"{ssampler.name}: {substep + 1}/{substeps}") + sr = self.simple_substep(x, ssampler) + z_avg += renoise_weight * sr.x + if sr.noise_scale != 0 and ss.sigma_next != 0: + noise_total += renoise_weight * sr.noise_scale + noise += renoise_weight * sr.get_noise(ss=ss) + substep += 1 + ss.substep = substep + pbar.update(1) - def merge_steps(self, x, result): - self.ss.model.reset_cache() - msampler = self.merge_sampler - if self.merge_ss is None: - merge_ss = self.merge_ss = self.ss.clone_edit( - denoised=result, - dhist=History(x, 3), - xhist=History(x, 2), - s_noise=msampler.s_noise, - eta=msampler.eta, - # model_call_cache=None, - ) - else: - merge_ss = self.merge_ss - merge_ss.denoised = result - merge_ss.update(self.ss.idx) - final = merge_ss.sigma_next == 0 - merged, noise_strength = msampler.step(x, merge_ss) - if not final: - ss = self.ss - merged = ( - merged - + msampler.noise_sampler(ss.sigma, ss.sigma_next) - * msampler.s_noise - * ss.sigma_up - ) - merge_ss.dhist.push(result) - merge_ss.xhist.push(merged) - merge_ss.denoised = None - return merged + noise = ss.noise.scale_noise( + noise, + noise_total * self.options.get("s_noise", 1.0), + normalized=True, + ) + return self.merge_steps( + x, z_avg, noise=None if noise_total == 0 else noise, denoised=ss.denoised + ) -class SampleUncachedMergeSubstepsSampler(SampleMergeSubstepsSampler): - cache_model = False +# class AverageMergeSubstepsSampler(NormalMergeSubstepsSampler): +# name = "average" + +# def __init__(self, ss, sitems, *, avgmerge_stretch=0.4, **kwargs): +# super().__init__(ss, sitems, **kwargs) +# self.stretch = avgmerge_stretch + +# def step_max_noise_samples(self): +# return sum( +# 1 + (2 + sampler.self_noise) * sampler.substeps for sampler in self.samplers +# ) + +# def step(self, x): +# ss = orig_ss = self.ss +# substeps = self.substeps +# renoise_weight = 1.0 / substeps +# z_avg = torch.zeros_like(x) +# noise = torch.zeros_like(x) +# stretch = (ss.sigma - ss.sigma_next) * self.stretch +# sig_adj = ss.sigma + stretch +# ss = self.ss.clone_edit(sigma=sig_adj) +# orig_x = x +# stretch_strength = stretch * ss.s_noise +# if stretch_strength != 0: +# noise_sampler = ss.noise.make_caching_noise_sampler( +# self.options.get("custom_noise"), 1, orig_ss.sigma, ss.sigma_next +# ) +# x = x + ( +# noise_sampler(orig_ss.sigma, ss.sigma_next).mul_(stretch * ss.s_noise) +# ) +# self.ss.denoised = ss.denoised = ss.model(x, sig_adj) +# noise_total = 0.0 +# substep = 0 +# for idx, ssampler in enumerate(self.samplers): +# print( +# f" SUBSTEP {substep + 1} .. {substep + ssampler.substeps}: {ssampler.name}, stretch={stretch}" +# ) +# custom_noise = ssampler.options.get( +# "custom_noise", self.options.get("custom_noise") +# ) +# noise_sampler = ss.noise.make_caching_noise_sampler( +# custom_noise, +# ssampler.substeps +# + (0 if ss.sigma_next == 0 else ssampler.max_noise_samples()), +# ss.sigma, +# ss.sigma_next, +# ) +# ssampler.noise_sampler = noise_sampler +# for sidx in range(ssampler.substeps): +# curr_x = orig_x + noise_sampler(sig_adj, ss.sigma_next).mul_(stretch) +# sr = self.simple_substep(curr_x, ssampler, ss=ss) +# z_avg += renoise_weight * sr.x +# noise_strength = sr.noise_scale +# if ss.sigma_next == 0 or noise_strength == 0: +# continue +# if noise_strength != 0 and ss.sigma_next != 0: +# noise_curr = sr.get_noise() +# noise_total += noise_strength.item() * renoise_weight +# noise += noise_curr +# substep += 1 +# substep += ssampler.substeps +# return self.merge_steps( +# x, +# z_avg, +# noise=None +# if not noise_total +# else ss.noise.scale_noise(noise, noise_total * ss.s_noise, normalized=True), +# ss=ss, +# ) + + +# class SampleMergeSubstepsSampler(AverageMergeSubstepsSampler): +# name = "sample" +# cache_model = True + +# def __init__(self, ss, sitems, *, merge_sampler=None, **kwargs): +# super().__init__(ss, sitems, **kwargs) +# if merge_sampler is None: +# merge_sampler = STEP_SAMPLERS["euler"](step_method="euler") +# else: +# msitem = merge_sampler.items[0] +# merge_sampler = STEP_SAMPLERS[msitem["step_method"]](**msitem) +# self.merge_sampler = merge_sampler +# self.merge_ss = None + +# def step(self, x): +# ss = self.ss +# substeps = self.substeps +# renoise_weight = 1.0 / substeps +# z_avg = torch.zeros_like(x) +# curr_x = x +# ss.denoised = None +# stretch = (ss.sigma - ss.sigma_next) * self.stretch +# sig_adj = ss.sigma + stretch +# ss = self.ss.clone_edit(sigma=sig_adj) +# step = 0 +# for idx, ssampler in enumerate(self.samplers): +# print( +# f" SUBSTEP {step + 1} .. {step + ssampler.substeps}: {ssampler.name}, stretch={stretch}" +# ) +# custom_noise = ssampler.options.get( +# "custom_noise", self.options.get("custom_noise") +# ) +# noise_sampler = ss.noise.make_caching_noise_sampler( +# custom_noise, +# ssampler.max_noise_samples() + ssampler.substeps, +# ss.sigma, +# ss.sigma_next, +# ) +# ssampler.noise_sampler = noise_sampler +# for sidx in range(ssampler.substeps): +# if idx + sidx == 0 or not self.cache_model: +# self.ss.denoised = ss.denoised = ss.model( +# curr_x, +# ss.sigma, +# # + ss.noise_sampler(sig_adj.sigma, ss.sigma_next) * stretch * ss.s_noise, +# # sig_adj, +# ) +# curr_x = x + noise_sampler(sig_adj, ss.sigma_next).mul_( +# ssampler.s_noise * stretch +# ) +# sr = self.simple_substep(curr_x, ssampler, ss=ss) +# z_avg += renoise_weight * sr.x +# curr_x = sr.noise_x(sr.x) +# step += ssampler.substeps +# return self.merge_steps(curr_x, z_avg) + +# def merge_steps(self, x, result): +# ss = self.ss +# ss.dhist.push(ss.denoised) +# ss.denoised = None +# ss.model.reset_cache() +# msampler = self.merge_sampler +# if self.merge_ss is None: +# merge_ss = self.merge_ss = self.ss.clone_edit( +# denoised=result, +# dhist=History(x, 3), +# xhist=History(x, 2), +# s_noise=msampler.s_noise, +# eta=msampler.eta, +# ) +# else: +# merge_ss = self.merge_ss +# merge_ss.denoised = result +# merge_ss.update(self.ss.idx, step=self.ss.step) +# final = merge_ss.sigma_next == 0 +# noise_sampler = merge_ss.noise.make_caching_noise_sampler( +# msampler.options.get("custom_noise", self.options.get("custom_noise")), +# msampler.max_noise_samples() + int(not final), +# merge_ss.sigma, +# merge_ss.sigma_next, +# ) +# msampler.noise_sampler = noise_sampler +# sr = self.simple_substep(x, msampler, ss=merge_ss) +# self.ss.callback(sr.x) +# sr.noise_x() +# merge_ss.dhist.push(result) +# merge_ss.xhist.push(sr.x) +# merge_ss.denoised = None +# ss.xhist.push(sr.x) +# return sr.x + +# def reset(self): +# if self.merge_ss is None: +# return +# self.merge_ss.reset() +# self.merge_ss.sigmas = self.ss.sigmas +# self.merge_ss.update(self.ss.idx, step=self.ss.step) + + +# class SampleUncachedMergeSubstepsSampler(SampleMergeSubstepsSampler): +# name = "sample_uncached" +# cache_model = False class DivideMergeSubstepsSampler(MergeSubstepsSampler): - def __init__(self, ss, samplers, *, schedule_multiplier=4, **kwargs): - super().__init__(ss, samplers, **kwargs) + name = "divide" + + def __init__(self, ss, group, *, schedule_multiplier=4, **kwargs): + super().__init__(ss, group, **kwargs) self.schedule_multiplier = schedule_multiplier def make_schedule(self, ss): @@ -204,11 +365,9 @@ class DivideMergeSubstepsSampler(MergeSubstepsSampler): sigmas_slice = ss.sigmas[ ss.idx : min(max_steps + 1, ss.idx + self.schedule_multiplier) ] - # print("SLICE", sigmas_slice) - unsorted_idx = find_first_unsorted(sigmas_slice) + unsorted_idx = utils.find_first_unsorted(sigmas_slice) if unsorted_idx is not None: sigmas_slice = sigmas_slice[:unsorted_idx] - # print("SLICE ADJ", sigmas_slice) chunks = tuple( torch.linspace( sigmas_slice[idx], @@ -219,42 +378,147 @@ class DivideMergeSubstepsSampler(MergeSubstepsSampler): )[0 if not idx else 1 :] for idx in range(len(sigmas_slice) - 1) ) - # print("CHUNKS", chunks) return torch.cat(chunks) def step(self, x): ss = self.ss - # print("SUBSIGMAS", subsigmas) subss = self.ss.clone_edit(idx=0, sigmas=self.make_schedule(ss)) subss.main_idx = ss.idx subss.main_sigmas = ss.sigmas - - for idx, ssampler in enumerate( - sampler for sampler in self.samplers for _ in range(sampler.substeps) - ): - print(f" SUBSTEP {idx+1}: {ssampler.name}") - subss.update(idx) - subss.denoised = subss.model(x, subss.sigma) - x, noise_strength = ssampler.step(x, subss) - if noise_strength == 0 or subss.sigma_next == 0: - continue - x = ( - x - + ssampler.noise_sampler(subss.sigma, subss.sigma_next) - * ssampler.s_noise - * noise_strength + substep = 0 + pbar = tqdm.tqdm(total=self.substeps, initial=0, disable=ss.disable_status) + for ssampler in self.samplers: + custom_noise = ssampler.options.get( + "custom_noise", self.options.get("custom_noise") ) - subss.xhist.push(x) - subss.dhist.push(subss.denoised) - subss.denoised = None - ss.callback(x) + noise_sampler = ss.noise.make_caching_noise_sampler( + custom_noise, + ssampler.max_noise_samples(), + ss.sigma, + ss.sigma_next, + immiscible=fallback(ssampler.immiscible, ss.noise.immiscible), + ) + ssampler.noise_sampler = noise_sampler + for subidx in range(ssampler.substeps): + subss.update(substep, substep=substep) + pbar.set_description( + f"substep({ssampler.name}): {subss.sigma.item():.03} -> {subss.sigma_next.item():.03}" + ) + subss.hist.push(subss.model(x, subss.sigma, ss=subss)) + subss.refs = FilterRefs.from_ss(subss, have_current=True) + if substep == 0: + subss.callback() + sr = self.simple_substep(x, ssampler, ss=subss) + x = sr.x + noise_strength = sr.noise_scale + if noise_strength != 0 and subss.sigma_next != 0: + x = sr.noise_x(ss=subss) + substep += 1 + pbar.update(1) + pbar.update(0) + return x + + +class OvershootMergeSubstepsSampler(MergeSubstepsSampler): + name = "overshoot" + + def __init__( + self, + ss, + group, + *, + overshoot_expand_steps=1, + restart_custom_noise=None, + restart=None, + **kwargs, + ): + super().__init__(ss, group, **kwargs) + self.overshoot_expand_steps = overshoot_expand_steps + restart = fallback(restart, {}) + self.restart = Restart( + s_noise=restart.get("s_noise", 1.0), + custom_noise=restart_custom_noise, + immiscible=restart.get("immiscible", False), + ) + + def make_schedule(self, ss): + expand = self.overshoot_expand_steps + if expand > self.substeps: + raise ValueError( + "overshoot_expand_steps > substeps: can't make it to the end of step 1" + ) + if expand < 2: + return ss.sigmas, ss.idx + sigmas_cpu = ss.sigmas.cpu() + sigmas = torch.cat( + tuple( + torch.linspace(f, t, expand + 1)[:-1] + for f, t in torch.stack((sigmas_cpu[:-1], sigmas_cpu[1:]), dim=1) + ) + + (sigmas_cpu[-1].unsqueeze(0),) + ) + return sigmas.to(ss.sigmas), ss.idx * expand + + def step(self, x): + ss = self.ss + sigmas, sigidx = self.make_schedule(ss) + subss = ss.clone_edit(idx=sigidx, sigmas=sigmas) + subss.hist = subss.hist.clone() + substep = 0 + pbar = tqdm.tqdm(total=self.substeps, initial=0, disable=ss.disable_status) + max_idx = len(subss.sigmas) - 2 + last_down = None + for ssampler in self.samplers: + custom_noise = ssampler.options.get( + "custom_noise", self.options.get("custom_noise") + ) + noise_sampler = ss.noise.make_caching_noise_sampler( + custom_noise, + ssampler.max_noise_samples(), + ss.sigma, + ss.sigma_next, + immiscible=fallback(ssampler.immiscible, ss.noise.immiscible), + ) + ssampler.noise_sampler = noise_sampler + for subidx in range(ssampler.substeps): + subss.update(subss.idx + substep, substep=substep) + pbar.set_description( + f"substep({ssampler.name}): {subss.sigma.item():.03} -> {subss.sigma_next.item():.03}" + ) + subss.hist.push(subss.model(x, subss.sigma, ss=subss)) + subss.refs = FilterRefs.from_ss(subss, have_current=True) + if substep == 0: + ss.hist.push(subss.hcur) + subss.callback() + sr = self.simple_substep(x, ssampler, ss=subss) + x = sr.x + noise_strength = sr.noise_scale + if noise_strength != 0 and subss.sigma_next != 0: + x = sr.noise_x(ss=subss) + substep += 1 + pbar.update(1) + last_down = subss.sigma_next.item() + if subss.idx + substep >= max_idx: + break + if subss.idx >= max_idx: + break + if last_down is not None and last_down < ss.sigma_next: + restart_ns = self.restart.get_noise_sampler(ss.noise) + x += ss.noise.scale_noise( + restart_ns(refs=ss.refs), + self.restart.get_noise_scale(last_down, ss.sigma_next), + ) + pbar.update(0) return x MERGE_SUBSTEPS_CLASSES = { + "default (simple)": SimpleSubstepsSampler, "normal": NormalMergeSubstepsSampler, "divide": DivideMergeSubstepsSampler, - "average": AverageMergeSubstepsSampler, - "sample": SampleMergeSubstepsSampler, - "sample_uncached": SampleUncachedMergeSubstepsSampler, + "overshoot": OvershootMergeSubstepsSampler, + # "average": AverageMergeSubstepsSampler, + # "sample": SampleMergeSubstepsSampler, + # "sample_uncached": SampleUncachedMergeSubstepsSampler, + "simple": SimpleSubstepsSampler, } diff --git a/py/substep_samplers.py b/py/substep_samplers.py deleted file mode 100644 index a5a13dd..0000000 --- a/py/substep_samplers.py +++ /dev/null @@ -1,624 +0,0 @@ -import math - -import torch - -from comfy.k_diffusion.sampling import ( - get_ancestral_step, - to_d, -) - -from .res_support import _de_second_order -from .utils import find_first_unsorted - - -class SingleStepSampler: - name = None - - def __init__( - self, - *, - noise_sampler=None, - substeps=1, - s_noise=1.0, - eta=1.0, - dyn_eta_start=None, - dyn_eta_end=None, - weight=1.0, - **kwargs, - ): - self.s_noise = s_noise - self.eta = eta - self.dyn_eta_start = dyn_eta_start - self.dyn_eta_end = dyn_eta_end - self.noise_sampler = noise_sampler - self.weight = weight - self.substeps = substeps - self.kwargs = kwargs - - def step(self, x, ss): - raise NotImplementedError - - # Euler - based on original ComfyUI implementation - def euler_step(self, x, ss): - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - d = to_d(x, ss.sigma, ss.denoised) - dt = sigma_down - ss.sigma - return x + d * dt, sigma_up - - def __str__(self): - return f"" - - def get_dyn_value(self, ss, start, end): - if None in (start, end): - return 1.0 - if start == end: - return start - main_idx = getattr(ss, "main_idx", ss.idx) - main_sigmas = getattr(ss, "main_sigmas", ss.sigmas) - step_pct = main_idx / (len(main_sigmas) - 1) - dd_diff = end - start - return start + dd_diff * step_pct - - def get_dyn_eta(self, ss): - return self.eta * self.get_dyn_value(ss, self.dyn_eta_start, self.dyn_eta_end) - - -class ReversibleSingleStepSampler(SingleStepSampler): - def __init__(self, *, reta=1.0, dyn_reta_start=None, dyn_reta_end=None, **kwargs): - super().__init__(**kwargs) - self.reta = reta - self.dyn_reta_start = dyn_reta_start - self.dyn_reta_end = dyn_reta_end - - def get_dyn_reta(self, ss): - return self.reta * self.get_dyn_value( - ss, self.dyn_reta_start, self.dyn_reta_end - ) - - -class EulerStep(SingleStepSampler): - name = "euler" - step = SingleStepSampler.euler_step - - -class DPMPPStepBase(SingleStepSampler): - @staticmethod - def sigma_fn(t): - return t.neg().exp() - - @staticmethod - def t_fn(t): - return t.log().neg() - - -class DPMPP2MStep(DPMPPStepBase): - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - t, t_next = self.t_fn(ss.sigma), self.t_fn(ss.sigma_next) - h = t_next - t - st, st_next = self.sigma_fn(t), self.sigma_fn(t_next) - if len(ss.dhist) == 0 or ss.sigma_prev is None: - return (st_next / st) * x - (-h).expm1() * ss.denoised, 0.0 - h_last = t - self.t_fn(ss.sigma_prev) - r = h_last / h - denoised, old_denoised = ss.denoised, ss.dhist[-1] - denoised_d = (1 + 1 / (2 * r)) * denoised - (1 / (2 * r)) * old_denoised - return (st_next / st) * x - (-h).expm1() * denoised_d, 0.0 - - -class DPMPP2MSDEStep(SingleStepSampler): - name = "dpmpp_2m_sde" - - def __init__(self, *, solver_type="midpoint", **kwargs): - super().__init__(**kwargs) - self.solver_type = solver_type - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - denoised = ss.denoised - if ss.sigma_next == 0: - return denoised, None - # DPM-Solver++(2M) SDE - t, s = -ss.sigma.log(), -ss.sigma_next.log() - h = s - t - eta_h = self.get_dyn_eta(ss) * h - - x = ( - ss.sigma_next / ss.sigma * (-eta_h).exp() * x - + (-h - eta_h).expm1().neg() * denoised - ) - noise_strength = ss.sigma_next * (-2 * eta_h).expm1().neg().sqrt() - if len(ss.dhist) == 0 or ss.sigma_prev is None: - return x, noise_strength - h_last = (-ss.sigma.log()) - (-ss.sigma_prev.log()) - r = h_last / h - old_denoised = ss.dhist[-1] - if self.solver_type == "heun": - x = x + ( - ((-h - eta_h).expm1().neg() / (-h - eta_h) + 1) - * (1 / r) - * (denoised - old_denoised) - ) - elif self.solver_type == "midpoint": - x = x + 0.5 * (-h - eta_h).expm1().neg() * (1 / r) * ( - denoised - old_denoised - ) - return x, noise_strength - - -class DPMPP3MSDEStep(SingleStepSampler): - name = "dpmpp_3m_sde" - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - denoised = ss.denoised - if ss.sigma_next == 0: - return denoised, 0 - t, s = -ss.sigma.log(), -ss.sigma_next.log() - h = s - t - eta = self.get_dyn_eta(ss) - h_eta = h * (eta + 1) - - x = torch.exp(-h_eta) * x + (-h_eta).expm1().neg() * denoised - noise_strength = ss.sigma_next * (-2 * h * eta).expm1().neg().sqrt() - if len(ss.dhist) == 0 or ss.sigma_prev is None: - return x, noise_strength - h_1 = (-ss.sigma.log()) - (-ss.sigma_prev.log()) - denoised_1 = ss.dhist[-1] - if len(ss.dhist) == 1: - r = h_1 / h - d = (denoised - denoised_1) / r - phi_2 = h_eta.neg().expm1() / h_eta + 1 - x = x + phi_2 * d - else: - h_2 = (-ss.sigma_prev.log()) - (-ss.sigmas[ss.idx - 2].log()) - denoised_2 = ss.dhist[-2] - r0 = h_1 / h - r1 = h_2 / h - d1_0 = (denoised - denoised_1) / r0 - d1_1 = (denoised_1 - denoised_2) / r1 - d1 = d1_0 + (d1_0 - d1_1) * r0 / (r0 + r1) - d2 = (d1_0 - d1_1) / (r0 + r1) - phi_2 = h_eta.neg().expm1() / h_eta + 1 - phi_3 = phi_2 / h_eta - 0.5 - x = x + phi_2 * d1 - phi_3 * d2 - return x, noise_strength - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class ReversibleHeunStep(ReversibleSingleStepSampler): - name = "reversible_heun" - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - sigma_down_reversible, sigma_up_reversible = ss.get_ancestral_step( - self.get_dyn_reta(ss) - ) - dt = sigma_down - ss.sigma - dt_reversible = sigma_down_reversible - ss.sigma - - # Calculate the derivative using the model - d = to_d(x, ss.sigma, ss.denoised) - - # Predict the sample at the next sigma using Euler step - x_pred = x + d * dt - - # Denoised sample at the next sigma - denoised_next = ss.model(x_pred, sigma_down, model_call_idx=1) - - # Calculate the derivative at the next sigma - d_next = to_d(x_pred, sigma_down, denoised_next) - - # Update the sample using the Reversible Heun formula - x = x + dt * (d + d_next) / 2 - dt_reversible**2 * (d_next - d) / 4 - return x, sigma_up - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class ReversibleHeun1SStep(ReversibleSingleStepSampler): - name = "reversible_heun_1s" - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - # Reversible Heun-inspired update (first-order) - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - sigma_down_reversible, sigma_up_reversible = ss.get_ancestral_step( - self.get_dyn_reta(ss) - ) - sigma_i, sigma_i_plus_1 = ss.sigma, sigma_down - dt = sigma_i_plus_1 - sigma_i - dt_reversible = sigma_down_reversible - sigma_i - - eff_x = ss.xhist[-1] if len(ss.xhist) else x - - # Calculate the derivative using the model - d_i_old = to_d( - eff_x, - sigma_i, - ss.dhist[-1] - if len(ss.dhist) - else ss.model(eff_x, sigma_i, model_call_idx=1), - ) - - # Predict the sample at the next sigma using Euler step - x_pred = eff_x + d_i_old * dt - - # Calculate the derivative at the next sigma - d_i_plus_1 = to_d(x_pred, sigma_i_plus_1, ss.denoised) - - # Update the sample using the Reversible Heun formula - x = ( - x - + dt * (d_i_old + d_i_plus_1) / 2 - - dt_reversible**2 * (d_i_plus_1 - d_i_old) / 4 - ) - return x, sigma_up - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class RESStep(SingleStepSampler): - name = "res" - - def __init__(self, *, res_simple_phi=False, res_c2=0.5, **kwargs): - super().__init__(**kwargs) - self.simple_phi = res_simple_phi - self.c2 = res_c2 - pass - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - eta = self.get_dyn_eta(ss) - sigma_down, sigma_up = ss.get_ancestral_step(eta) - denoised = ss.denoised - lam_next = sigma_down.log().neg() if eta != 0 else ss.sigma_next.log().neg() - lam = ss.sigma.log().neg() - - h = lam_next - lam - a2_1, b1, b2 = _de_second_order( - h=h, c2=self.c2, simple_phi_calc=self.simple_phi - ) - - c2_h = 0.5 * h - - x_2 = math.exp(-c2_h) * x + a2_1 * h * denoised - lam_2 = lam + c2_h - sigma_2 = lam_2.neg().exp() - - denoised2 = ss.model(x_2, sigma_2, model_call_idx=1) - - x = math.exp(-h) * x + h * (b1 * denoised + b2 * denoised2) - return x, sigma_up - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class TrapezoidalStep(SingleStepSampler): - name = "trapezoidal" - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - dt = ss.sigma_next - ss.sigma - denoised = ss.denoised - - # Calculate the derivative using the model - d_i = to_d(x, ss.sigma, denoised) - - # Predict the sample at the next sigma using Euler step - x_pred = x + d_i * dt - - # Denoised sample at the next sigma - denoised_next = ss.model(x_pred, ss.sigma_next, model_call_idx=1) - - # Calculate the derivative at the next sigma - d_next = to_d(x_pred, ss.sigma_next, denoised_next) - - dt_2 = sigma_down - ss.sigma - # Update the sample using the Trapezoidal rule - x = x + dt_2 * (d_i + d_next) / 2 - return x, sigma_up - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class BogackiStep(ReversibleSingleStepSampler): - name = "bogacki" - reversible = False - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - sigma_down_reversible, sigma_up_reversible = ss.get_ancestral_step( - self.get_dyn_reta(ss) - ) - sigma, sigma_next = ss.sigma, sigma_down - dt = sigma_next - sigma - dt_reversible = sigma_down_reversible - sigma - denoised = ss.denoised - - # Calculate the derivative using the model - d = to_d(x, sigma, denoised) - - # Bogacki-Shampine steps - k1 = d * dt - k2 = ( - to_d( - x + k1 / 2, - sigma + dt / 2, - ss.model(x + k1 / 2, sigma + dt / 2, model_call_idx=1), - ) - * dt - ) - k3 = ( - to_d( - x + 3 * k1 / 4 + k2 / 4, - sigma + 3 * dt / 4, - ss.model(x + 3 * k1 / 4 + k2 / 4, sigma + 3 * dt / 4, model_call_idx=2), - ) - * dt - ) - - # Reversible correction term (inspired by Reversible Heun) - correction = dt_reversible**2 * (k3 - k2) / 6 if self.reversible else 0.0 - - # Update the sample - x = x + 2 * k1 / 9 + k2 / 3 + 4 * k3 / 9 - correction - return x, sigma_up - - -class ReversibleBogackiStep(BogackiStep): - name = "reversible_bogacki" - reversible = True - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class RK4Step(SingleStepSampler): - name = "rk4" - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - sigma = ss.sigma - # Calculate the derivative using the model - d = to_d(x, sigma, ss.denoised) - dt = sigma_down - sigma - - # Runge-Kutta steps - k1 = d * dt - k2 = ( - to_d( - x + k1 / 2, - sigma + dt / 2, - ss.model(x + k1 / 2, sigma + dt / 2, model_call_idx=1), - ) - * dt - ) - k3 = ( - to_d( - x + k2 / 2, - sigma + dt / 2, - ss.model(x + k2 / 2, sigma + dt / 2, model_call_idx=2), - ) - * dt - ) - k4 = ( - to_d( - x + k3, - sigma + dt, - ss.model(x + k3, sigma + dt, model_call_idx=3), - ) - * dt - ) - - # Update the sample - x = x + (k1 + 2 * k2 + 2 * k3 + k4) / 6 - return x, sigma_up - - -# Based on original implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -class EulerDancingStep(SingleStepSampler): - name = "euler_dancing" - - def __init__( - self, - *, - deta=1.0, - ds_noise=1.0, - leap=2, - dyn_deta_start=None, - dyn_deta_end=None, - dyn_deta_mode="lerp", - **kwargs, - ): - super().__init__(**kwargs) - self.deta = deta - self.ds_noise = ds_noise - self.leap = leap - self.dyn_deta_start = dyn_deta_start - self.dyn_deta_end = dyn_deta_end - if dyn_deta_mode not in ("lerp", "lerp_alt", "deta"): - raise ValueError("Bad dyn_deta_mode") - self.dyn_deta_mode = dyn_deta_mode - - def step(self, x, ss): - eta = self.get_dyn_eta(ss) - leap_sigmas = ss.sigmas[ss.idx :] - leap_sigmas = leap_sigmas[: find_first_unsorted(leap_sigmas)] - zero_idx = (leap_sigmas <= 0).nonzero().flatten()[:1] - max_leap = (zero_idx.item() if len(zero_idx) else len(leap_sigmas)) - 1 - is_danceable = max_leap > 1 and ss.sigma_next != 0 - curr_leap = max(1, min(self.leap, max_leap)) - sigma_leap = leap_sigmas[curr_leap] if is_danceable else ss.sigma_next - print("DANCE", max_leap, curr_leap, sigma_leap, "--", leap_sigmas) - del leap_sigmas - sigma_down, sigma_up = get_ancestral_step(ss.sigma, sigma_leap, eta) - d = to_d(x, ss.sigma, ss.denoised) - # Euler method - dt = sigma_down - ss.sigma - x = x + d * dt - if curr_leap == 1: - return x, sigma_up - dance_scale = self.get_dyn_value(ss, self.dyn_deta_start, self.dyn_deta_end) - if not is_danceable or abs(dance_scale) < 1e-04: - return x, sigma_up - sigma_down_normal, sigma_up_normal = get_ancestral_step( - ss.sigma, ss.sigma_next, eta - ) - if self.dyn_deta_mode == "lerp": - dt_normal = sigma_down_normal - ss.sigma - x_normal = x + d * dt_normal - else: - x_normal = x - x = x + self.noise_sampler(ss.sigma, sigma_leap) * self.s_noise * sigma_up - sigma_down2, sigma_up2 = get_ancestral_step( - sigma_leap, - ss.sigma_next, - eta=self.deta * (1.0 if self.dyn_deta_mode != "deta" else dance_scale), - ) - d_2 = to_d(x, sigma_leap, ss.denoised) - dt_2 = sigma_down2 - sigma_leap - result = x + d_2 * dt_2 - noise_diff = sigma_up2 - sigma_up * dance_scale - noise_scale = sigma_up2 + noise_diff * (0.025 * curr_leap) - if self.dyn_deta_mode == "deta" or dance_scale == 1.0: - return result, noise_scale - result = torch.lerp(x_normal, result, dance_scale) - # FIXME: Broken for noise samplers that care about s/sn - return result, noise_scale - - -class DPMPP2SStep(DPMPPStepBase): - name = "dpmpp_2s" - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - t_fn, sigma_fn = self.t_fn, self.sigma_fn - sigma_down, sigma_up = ss.get_ancestral_step(self.get_dyn_eta(ss)) - # DPM-Solver++(2S) - t, t_next = t_fn(ss.sigma), t_fn(sigma_down) - r = 1 / 2 - h = t_next - t - s = t + r * h - x_2 = (sigma_fn(s) / sigma_fn(t)) * x - (-h * r).expm1() * ss.denoised - denoised_2 = ss.model(x_2, sigma_fn(s), model_call_idx=0) - x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised_2 - return x, sigma_up - - -class DPMPPSDEStep(DPMPPStepBase): - name = "dpmpp_sde" - - def __init__(self, *args, r=1 / 2, **kwargs): - super().__init__(*args, **kwargs) - self.r = r - - def step(self, x, ss): - if ss.sigma_next == 0: - return self.euler_step(x, ss) - t_fn, sigma_fn = self.t_fn, self.sigma_fn - r, eta, s_noise = self.r, self.get_dyn_eta(ss), self.s_noise - noise_sampler = self.noise_sampler - sigma_down, sigma_up = ss.get_ancestral_step(eta) - # DPM-Solver++ - t, t_next = t_fn(ss.sigma), t_fn(ss.sigma_next) - h = t_next - t - s = t + h * r - fac = 1 / (2 * r) - - # Step 1 - sd, su = get_ancestral_step(sigma_fn(t), sigma_fn(s), eta) - s_ = t_fn(sd) - x_2 = (sigma_fn(s_) / sigma_fn(t)) * x - (t - s_).expm1() * ss.denoised - x_2 = x_2 + noise_sampler(sigma_fn(t), sigma_fn(s)) * s_noise * su - denoised_2 = ss.model(x_2, sigma_fn(s), model_call_idx=1) - - # Step 2 - sd, su = get_ancestral_step(sigma_fn(t), sigma_fn(t_next), eta) - t_next_ = t_fn(sd) - denoised_d = (1 - fac) * ss.denoised + fac * denoised_2 - x = (sigma_fn(t_next_) / sigma_fn(t)) * x - (t - t_next_).expm1() * denoised_d - return x, su - - -# Based on implementation from https://github.com/Clybius/ComfyUI-Extra-Samplers -# Which was originally written by Katherine Crowson -class TTMJVPStep(SingleStepSampler): - name = "ttm_jvp" - - def __init__(self, *args, alternate_phi_2_calc=True, **kwargs): - super().__init__(*args, **kwargs) - self.alternate_phi_2_calc = alternate_phi_2_calc - - def step(self, x, ss): - if ss.sigma_next == 0: - return ss.denoised, ss.sigma.new_zeros(1) - eta = self.get_dyn_eta(ss) - sigma_down, sigma_up = ss.get_ancestral_step(eta) - sigma, sigma_next = ss.sigma, ss.sigma_next - # 2nd order truncated Taylor method - t, s = -sigma.log(), -sigma_next.log() - h = s - t - h_eta = h * (eta + 1) - - eps = to_d(x, sigma, ss.denoised) - denoised, denoised_prime = ss.model( - x, sigma, tangents=(eps * -sigma, -sigma), model_call_idx=1 - ) - - phi_1 = -torch.expm1(-h_eta) - if self.alternate_phi_2_calc: - phi_2 = torch.expm1(-h) + h # seems to work better with eta > 0 - else: - phi_2 = torch.expm1(-h_eta) + h_eta - x = torch.exp(-h_eta) * x + phi_1 * ss.denoised + phi_2 * denoised_prime - - if not eta: - return x, ss.sigma.new_zeros(1) - - phi_1_noise = torch.sqrt(-torch.expm1(-2 * h * eta)) - return x, sigma_next * phi_1_noise - - -STEP_SAMPLERS = { - "euler": EulerStep, - "dpmpp_sde": DPMPPSDEStep, - "dpmpp_2m": DPMPP2MStep, - "dpmpp_2m_sde": DPMPP2MSDEStep, - "dpmpp_3m_sde": DPMPP3MSDEStep, - "dpmpp_2s": DPMPP2SStep, - "reversible_heun": ReversibleHeunStep, - "reversible_heun_1s": ReversibleHeun1SStep, - "res": RESStep, - "trapezoidal": TrapezoidalStep, - "bogacki": BogackiStep, - "reversible_bogacki": ReversibleBogackiStep, - "rk4": RK4Step, - "euler_dancing": EulerDancingStep, - "ttm_jvp": TTMJVPStep, -} - -__all__ = ( - "STEP_SAMPLERS", - "EulerStep", - "DPMPP2MStep", - "DPMPP2MSDEStep", - "DPMPP3MSDEStep", - "DPMPP2SStep", - "ReversibleHeunStep", - "ReversibleHeun1SStep", - "RESStep", - "TrapezoidalStep", - "BogackiStep", - "ReversibleBogackiStep", - "EulerDancingStep", - "TTMJVPStep", -) diff --git a/py/substep_sampling.py b/py/substep_sampling.py index ec2836a..2636d83 100644 --- a/py/substep_sampling.py +++ b/py/substep_sampling.py @@ -2,188 +2,207 @@ import torch from comfy.k_diffusion.sampling import get_ancestral_step +from .filtering import FilterRefs +from .model import History -class StepSamplerChain: + +class Items: def __init__(self, items=None): self.items = [] if items is None else items def clone(self): return self.__class__(items=self.items.copy()) + def append(self, item): + self.items.append(item) + return item -class History: - def __init__(self, x, size): - self.history = torch.zeros(size, *x.shape, device=x.device, dtype=x.dtype) - self.size = size - self.pos = 0 - self.last = None + def __getitem__(self, key): + return self.items[key] + + def __setitem__(self, key, value): + self.items[key] = value def __len__(self): - return min(self.pos, self.size) + return len(self.items) - def __getitem__(self, k): - idx = (self.pos + k if k < 0 else self.pos + -self.size + k) % self.size - # print(f"\nFETCH {k}: pos={self.pos}, size={self.size}, at={idx}") - return self.history[idx] - - def push(self, val): - # print(f"\nPUSH {self.pos % self.size}: pos={self.pos}, size={self.size}") - self.last = self.pos % self.size - self.history[self.last] = val - self.pos += 1 - - def reset(self): - self.pos = 0 - self.last = None + def __iter__(self): + return self.items.__iter__() -class ModelCallCache: +class CommonOptionsItems(Items): + def __init__(self, *, s_noise=1.0, eta=1.0, items=None, **kwargs): + super().__init__(items=items) + self.options = kwargs + self.s_noise = s_noise + self.eta = eta + + def clone(self): + obj = super().clone() + obj.options = self.options.copy() + obj.s_noise = self.s_noise + obj.eta = self.eta + return obj + + +class StepSamplerChain(CommonOptionsItems): def __init__( - self, model, x, s_in, extra_args, *, size=0, max_use=1000000, threshold=1 + self, + *, + merge_method="divide", + time_mode="step", + time_start=0, + time_end=999, + **kwargs, ): - self.size = size - self.model = model - self.threshold = threshold - self.s_in = s_in - self.extra_args = extra_args - self.max_use = max_use - if self.size < 1: - return - self.mcc = torch.zeros(size, *x.shape, device=x.device, dtype=x.dtype) - self.jmcc = torch.zeros_like(self.mcc) - self.reset_cache() + super().__init__(**kwargs) + self.merge_method = merge_method + if time_mode not in ("step", "step_pct", "sigma"): + raise ValueError("Bad time mode") + self.time_mode = time_mode + self.time_start, self.time_end = time_start, time_end - def reset_cache(self): - size = self.size - self.slot = [None] * size - self.jslot = [None] * size - self.slot_use = [self.max_use] * size + def clone(self): + obj = super().clone() + obj.merge_method = self.merge_method + obj.time_mode = self.time_mode + obj.time_start, obj.time_end = self.time_start, self.time_end + obj.options = self.options.copy() + return obj - def get(self, idx, *, jvp=False): - idx -= self.threshold - if ( - idx >= self.size - or idx < 0 - or self.slot[idx] is None - or self.slot_use[idx] < 1 - ): - return None - if jvp and self.jslot[idx] is None: - return None - self.slot_use[idx] -= 1 - return self.slot[idx] if not jvp else (self.slot[idx], self.jslot[idx]) - def set(self, idx, denoised, jdenoised=None): - idx -= self.threshold - if idx < 0 or idx >= self.size: - return - self.slot_use[idx] = self.max_use - self.slot[idx] = denoised - self.jslot[idx] = jdenoised +class ParamGroup(Items): + pass - def call_model(self, x, sigma, **kwargs): - return self.model(x, sigma * self.s_in, **self.extra_args, **kwargs) - def __call__(self, x, sigma, *, model_call_idx=0, tangents=None, **kwargs): - result = self.get(model_call_idx, jvp=tangents is not None) - # print( - # f"MODEL: idx={model_call_idx}, size={self.size}, threshold={self.threshold}, cached={result is not None}" - # ) - if result is not None: - return result - if tangents is None: - denoised = self.call_model(x, sigma, **kwargs) - self.set(model_call_idx, denoised) - return denoised - denoised, denoised_prime = torch.func.jvp(self.call_model, (x, sigma), tangents) - self.set(model_call_idx, denoised, jdenoised=denoised_prime) - return denoised, denoised_prime +class StepSamplerGroups(CommonOptionsItems): + pass class SamplerState: + CLONE_KEYS = ( + "model", + "hist", + "extra_args", + "disable_status", + "eta", + "reta", + "s_noise", + "sigmas", + "callback_", + "noise_sampler", + "noise", + "idx", + "total_steps", + "step", + "substep", + "sigma", + "sigma_next", + "sigma_prev", + "sigma_down", + "sigma_up", + "refs", + ) + def __init__( self, model, sigmas, idx, - dhist, - xhist, extra_args, *, + step=0, + substep=0, noise_sampler, callback=None, denoised=None, + noise=None, eta=1.0, reta=1.0, s_noise=1.0, + disable_status=False, + history_size=4, ): self.model = model - self.dhist = dhist - self.xhist = xhist + self.hist = History(max(1, history_size)) self.extra_args = extra_args self.eta = eta self.reta = reta self.s_noise = s_noise self.sigmas = sigmas - self.denoised = denoised self.callback_ = callback self.noise_sampler = noise_sampler - self.update(idx) + self.noise = noise + self.disable_status = disable_status + self.step = 0 + self.substep = 0 + self.total_steps = len(sigmas) - 1 + self.update(idx) # Sets idx, sigma_prev, sigma, sigma_down, refs - def update(self, idx=None): + @property + def hcur(self): + return self.hist[-1] + + @property + def hprev(self): + return self.hist[-2] + + @property + def denoised(self): + return self.hcur.denoised + + @property + def dt(self): + return self.sigma_next - self.sigma + + @property + def d(self): + return self.hcur.d + + def update(self, idx=None, step=None, substep=None): idx = self.idx if idx is None else idx self.idx = idx self.sigma_prev = None if idx < 1 else self.sigmas[idx - 1] self.sigma, self.sigma_next = self.sigmas[idx], self.sigmas[idx + 1] - # if self.sigma_prev is not None and self.sigma < self.sigma_prev: - # self.dhist.reset() - # self.xhist.reset() self.sigma_down, self.sigma_up = get_ancestral_step( self.sigma, self.sigma_next, eta=self.eta ) - self.sigma_down_reversible, self.sigma_up_reversible = get_ancestral_step( - self.sigma, self.sigma_next, eta=self.reta - ) + if step is not None: + self.step = step + if substep is not None: + self.substep = substep + self.refs = FilterRefs.from_ss(self) - def get_ancestral_step(self, eta=1.0): - return get_ancestral_step(self.sigma, self.sigma_next, eta=eta) + def get_ancestral_step(self, eta=1.0, sigma=None, sigma_next=None): + sigma = self.sigma if sigma is None else sigma + sigma_next = self.sigma_next if sigma_next is None else sigma_next + sd, su = ( + v if isinstance(v, torch.Tensor) else sigma.new_full((1,), v) + for v in get_ancestral_step( + sigma, sigma_next, eta=eta if sigma_next != 0 else 0 + ) + ) + return sd, su def clone_edit(self, **kwargs): obj = self.__class__.__new__(self.__class__) - for k in ( - "model", - "dhist", - "xhist", - "extra_args", - "eta", - "reta", - "s_noise", - "sigmas", - "denoised", - "callback_", - "noise_sampler", - "idx", - "sigma", - "sigma_next", - "sigma_prev", - "sigma_down", - "sigma_up", - "sigma_down_reversible", - "sigma_up_reversible", - ): + for k in self.CLONE_KEYS: setattr(obj, k, kwargs[k] if k in kwargs else getattr(self, k)) obj.update() return obj - def callback(self, x): + def callback(self, hi=None): if not self.callback_: return None - return self.callback_( - { - "x": x, - "i": self.idx, - "sigma": self.sigma, - "sigma_hat": self.sigma, - "denoised": self.dhist[-1], - } - ) + hi = self.hcur if hi is None else hi + return self.callback_({ + "x": hi.x, + "i": self.step, + "sigma": hi.sigma, + "sigma_hat": hi.sigma, + "denoised": hi.denoised, + }) + + def reset(self): + self.hist.reset() + self.denoised = None diff --git a/py/utils.py b/py/utils.py index 33f29a8..b93f38e 100644 --- a/py/utils.py +++ b/py/utils.py @@ -1,17 +1,24 @@ -import math +import contextlib + import torch +from comfy.k_diffusion.sampling import to_d -def scale_noise(noise, factor=1.0, *, normalized=True, threshold_std_devs=2.5): + +def scale_noise( + noise, + factor=1.0, + *, + normalized=True, + normalize_dims=(-3, -2, -1), +): if not normalized or noise.numel() == 0: return noise.mul_(factor) if factor != 1 else noise - mean, std = noise.mean().item(), noise.std().item() - threshold = threshold_std_devs / math.sqrt(noise.numel()) - if abs(mean) > threshold: - noise -= mean - if abs(1.0 - std) > threshold: - noise /= std - return noise.mul_(factor) if factor != 1 else noise + mean, std = ( + noise.mean(dim=normalize_dims, keepdim=True), + noise.std(dim=normalize_dims, keepdim=True), + ) + return noise.sub_(mean).div_(std).mul_(factor) def find_first_unsorted(tensor, desc=True): @@ -20,3 +27,54 @@ def find_first_unsorted(tensor, desc=True): fun = torch.gt if desc else torch.lt first_unsorted = fun(tensor[1:], tensor[:-1]).nonzero().flatten()[:1].add_(1) return None if not len(first_unsorted) else first_unsorted.item() + + +def fallback(val, default, exclude=None): + return val if val is not exclude else default + + +def step_generator(gen, *, get_next, initial=None): + next_val = initial + with contextlib.suppress(StopIteration): + while True: + result = gen.send(next_val) + next_val = get_next(result) + yield result + + +# From Gaeros. Thanks! +def extract_pred(x_before, x_after, sigma_before, sigma_after): + if sigma_after == 0: + return x_after, torch.zeros_like(x_after) + alpha = sigma_after / sigma_before + denoised = (x_after - alpha * x_before) / (1 - alpha) + return denoised, to_d(x_after, sigma_after, denoised) + + +def resolve_value(keys, obj): + if not len(keys): + raise ValueError("Cannot resolve empty key list") + result = obj + + class Empty: + pass + + for idx, key in enumerate(keys): + if not (hasattr(result, "__getattr__") or hasattr(obj, "__getattribute__")): + raise ValueError( + f"Cannot access key {key}: value does not support attribute access" + ) + result = getattr(result, key, Empty) + if result is Empty: + raise AttributeError(f"Key {key} from path {'.'.join(keys)} does not exist") + + +def check_time(time_mode, time_start, time_end, sigma, step, steps): + step_pct = step / steps if steps != 0 else 0.0 + if time_mode == "step": + return time_start <= step <= time_end + if time_mode == "step_pct": + return time_start <= step_pct <= time_end + if time_mode == "sigma": + return time_start >= sigma >= time_end + raise ValueError("Bad time mode")