Nodes/comfyui-timesaver/TS Shifted Sigmas
ComfyUI Node

TS Shifted Sigmas

The sampling schedule your Qwen turbo LoRA was actually distilled on

By AlexYez·Created 2 years ago·Updated a day ago· 17
TS Shifted Sigmas
  • latent
  • sigmas
◄nodes1.0, 0.9375, 0.875, 0.75, 0.5, 0.25►
◄base_shift0.50►
◄max_shift0.90►

A turbo LoRA is a bet on a specific curve

"Six steps, CFG 1, euler" is only two-thirds of the recipe. A few-step turbo LoRA for Qwen Image 2.1 - Viggle's viggle-turbo is the one this node was written for - was distilled on a specific handful of raw timesteps: by default 1.0, 0.9375, 0.875, 0.75, 0.5, 0.25. The diffusers pipeline doesn't sample at those numbers directly. It bends them with a shift that grows with the size of the picture, because flow-matching models behave differently when the same idea spans four times as many image tokens.

None of ComfyUI's schedulers produce that curve. They either space the steps themselves or apply a fixed shift that ignores resolution - and a turbo LoRA sampled on a schedule it wasn't distilled for loses sharpness. You get a picture that's nearly right, and no error message telling you why.

TS Shifted Sigmas gives you the schedule number for number.

The maths, since it's short

tokens = (height / 16) · (width / 16)
mu     = base_shift + (max_shift − base_shift) · (tokens − 256) / (8192 − 256)
sigma  = e^mu / (e^mu + (1/t − 1))

…then a final zero bolted onto the end. Bigger picture, more tokens, larger mu, stronger shift. base_shift is the shift at 256 image tokens (256×256 px), max_shift the shift at 8192 tokens (about 1448×1448 px); for Qwen Image 2.1 those are 0.5 and 0.9, which are the defaults. The output is float32, but the arithmetic runs in float64 first - at t = 0.9375, float32 is already wrong in the seventh digit.

The author checked the curve against diffusers' own FlowMatchEulerDiscreteScheduler and against Viggle's node across 28 sizes, with zero difference. The one detail that makes that true is how it reads the size: an empty latent from the stock Empty Latent Image (grid /8) gets resized by the sampler to Qwen's /16, so the node counts it on /16; a latent that already has content is counted at the size it has. That's the difference between matching the reference and being "close enough" - and it's not something you can eyeball.

Inputs and output

  • latent - the latent going into the sampler. Only its size is read: a bigger picture gets a stronger shift.
  • nodes - a STRING, default 1.0, 0.9375, 0.875, 0.75, 0.5, 0.25 for six steps. Comma separated, and fractions like 1/6 or the model card's square brackets are fine, so you can paste a line straight off the card.
  • base_shift - FLOAT, default 0.5, range 0–5. Qwen Image 2.1: 0.5.
  • max_shift - FLOAT, default 0.9, range 0–5. Qwen Image 2.1: 0.9.

One output: sigmas, one per node plus the trailing 0. Steps = number of entries, so the string is your step count.

Wire it into SamplerCustom or SamplerCustomAdvanced with the euler sampler and no CFG (BasicGuider, or cfg 1).

The rule from the tooltip, worth obeying: add or drop steps at the noisy end only, and keep 0.875, 0.75, 0.5, 0.25. Five steps is 1.0, 0.875, 0.75, 0.5, 0.25. Seven is 1.0, 0.9583, 0.9167, 0.875, 0.75, 0.5, 0.25. The tail is what the LoRA was distilled against; shuffling it is how you turn a sharp six-step image into a soft one and blame the model.

The 9-step mode, if you want it

viggle-turbo v0.3 supports a hybrid: 7 steps with the turbo LoRA, the last 2 finished by the base model without it. Built from stock nodes:

  1. TS Shifted Sigmas with [1.0, 0.9583, 0.9167, 0.875, 0.75, 0.5, 0.25, 1/6, 1/12]
  2. SplitSigmas at step = 7
  3. High half → SamplerCustomAdvanced with the LoRA model and RandomNoise
  4. Low half → a second SamplerCustomAdvanced with the same model without the LoRA and DisableNoise, fed by the first one's output latent

The seam is exact rather than approximately exact: euler keeps no memory between steps, and ComfyUI resets Qwen 2.1's text-and-reference cache on every sampler run, so the second half rebuilds it without the LoRA - the same thing Viggle does with its kv_cache_mode switch.

Pair it with TS LoRA Unmerged, which applies the LoRA without merging it into the weights. Both nodes are about the same thing: a six-step LoRA has nowhere to hide a rounding error, so neither the schedule nor the merge is a rounding error you can afford.

Installing it

Part of the Timesaver pack - no model downloads, no optional dependencies. The pack also injects extra samplers and schedulers straight into ComfyUI's own dropdowns (res_2s, bong_tangent, beta57, reimplemented clean-room from RES4LYF's public math), but this node doesn't need any of them.

ComfyUI Manager → Custom Nodes Manager → search Timesaver → install → restart. Or:

cd ComfyUI/custom_nodes
git clone https://github.com/AlexYez/comfyui-timesaver
cd comfyui-timesaver
python -m pip install -r requirements.txt

Where it goes wrong

Your step count and your schedule disagree. The nodes string decides how many sigmas come out, so if you also have a step-count widget on the sampling node, the two have to say the same number - edit one and you must edit the other.

It made a normal model worse. It's a schedule for a distilled few-step LoRA on the diffusers path. On a 30-step run, or with a LoRA that was trained the sensible way, this is simply the wrong curve - go back to the stock scheduler and stop reading the rest of this page.

Sharpness didn't come back and you're at CFG above 1. The distilled LoRA assumes no negative branch. A real CFG pass fights it; use BasicGuider or cfg 1.

Your 512×512 test told you nothing. The shift is resolution-dependent on purpose. A small test image gets a nearly unshifted schedule, so the bug you're hunting may only exist at the size you actually generate.

A malformed nodes string. The node validates it when you type it and says what's wrong rather than dying inside a sampler, so read the red message instead of retyping the list from memory.

CategoryTS/Utils

Inputs (4)

NameTypeDefaultDescription
latentLATENTThe latent that goes into the sampler. Only its size is read: a bigger picture gets a stronger shift.
nodesSTRING1.0, 0.9375, 0.875, 0.75, 0.5, 0.25Raw timesteps from noise (1.0) down, comma separated; fractions like 1/6 and the model card's square brackets are fine. The default is viggle-turbo 6 steps (v0.2.1 and v0.3). Add or drop steps at the noisy end only and keep 0.875, 0.75, 0.5, 0.25.
base_shiftFLOAT0.500–5Shift at 256 image tokens (256x256 px). Qwen Image 2.1: 0.5.
max_shiftFLOAT0.900–5Shift at 8192 image tokens (about 1448x1448 px). Qwen Image 2.1: 0.9.

Outputs (1)

NameTypeDescription
sigmasSIGMASOne sigma per node, then 0. Steps = number of nodes.