Nodes/WAS Node Suite v3/Affine Sampler
ComfyUI Node Runs on cloud

Affine Sampler

Texture pushed into the latent while the sampler is still running

By WASasquatch·Created 3 years ago·Updated 4 days ago· 1,844
Affine Sampler
  • sampler
  • affine_schedule
  • external_mask
  • affine_options
  • sampler
affine_interval1
max_scale1.020
max_bias0.000
patternwhite_noise
affine_seed0
affine_seed_incrementfalse
temporal_modestatic
affine_streamsvideo
affine_spacelatent
debugfalse
affine_acts_oncontent

Affine Sampler takes a sampler and gives you back a sampler that scales and offsets the latent from inside its own denoising loop. That's the whole pitch, and the reason it's interesting is inside. The lazy version of this trick is to stop sampling, scale the latent, and start again - which throws away the sampler's multistep history and its noise sequence, so a DPM++ 2M run becomes something else entirely. This one doesn't restart anything. It transforms the sampler's own state at the step boundary. Packed audio-and-video latents survive it too, which is why the WAS docs use it on video.

How it actually works

Under the hood the node wraps the SAMPLER's sampler_function and transforms the sampler's x in place at the opening model call of each scheduled step. Every stock sampler carries it except four: dpm_fast and dpm_adaptive drive their own internal solver so there's no step boundary to hook, and uni_pc / uni_pc_bh2 rescale the latent before every model call so a transform there never reaches the trajectory. Those four don't fail the run - the console tells you it couldn't be applied and then samples normally. Read that line if your affine "did nothing".

The strength comes from a curve, and the curve is a schedule: unwired, the affine ramps in from a fifth of the way through the run to four fifths. Mid-run is the right window - detail gets established in the middle steps, and the last few steps should be left alone to resolve cleanly. Same logic the Detail Daemon crowd landed on, and the same failure mode if you ignore it: crank the numbers and you get the over-detailed, slightly crunchy "AI look" instead of a photograph.

The inputs that matter

sampler comes from KSampler Select (or anything else emitting a SAMPLER). max_scale is what the latent is multiplied by at the peak, compounding over every step it lands on: 1.0 is no change, 1.02 adds visible texture across a 20-step run, 1.05 is strong, and past 1.08 the picture breaks up. Below 1.0 it softens. affine_interval decides how often it lands - 1 is every step, 4 gives the sampler room to settle in between.

affine_acts_on is the setting to actually understand, because it's the one that produces mystery grain:

  • content - only the picture the model has resolved so far. The sampler's own noise is untouched.
  • latent - the whole latent, noise included. That amplifies the sampler's noise draw as well, and on a model that holds a high noise level most of the way (a video model at a big shift, say) it prints as grain frozen in the frame.

Default is content, and that's the right default. There's a companion switch, affine_space, choosing whether max_scale/max_bias are measured against the latent space (latent, matching Latent Affine) or the sampler's internal one (model). Keep them consistent or your numbers mean different things in different nodes.

affine_seed seeds the mask, and affine_seed_increment decides whether that mask advances each application (moving grain) or holds still (affine landing in the same places every step). temporal_mode is for video latents only: static holds one mask for the clip, per_frame redraws it, drift slides it according to the drift_* values on Affine Options. Content-aware patterns ignore it.

Optional inputs are where the power is. affine_schedule takes the DICT from Affine Schedule when you want to design the curve yourself. affine_options takes the DICT from Affine Options - and a pattern set there beats the pattern widget here. external_mask either is the mask (on pattern external_mask) or gates a generated one; one mask covers every frame, or one per frame. affine_streams picks which streams of a packed AV latent get touched. debug logs every application to the console - step, sigma, strength, resolved scale and bias, streams. Use it. Once.

Output is a single sampler, wired where the plain KSampler Select would have gone:

KSamplerSelect ──► Affine Sampler ──► SamplerCustomAdvanced (sampler)
                          ▲
Affine Schedule / Affine Options / external MASK

Installing it

It ships in WAS Node Suite v3, which is a ComfyUI Manager install - search WAS Node Suite v3 - or:

cd ComfyUI/custom_nodes
git clone https://github.com/WASasquatch/was-node-suite-comfyui.git

ComfyUI 0.14.0+, Python 3.10+, and no pip packages: the pack runs on torch alone. It's written against ComfyUI's newer backend API, which is why that version floor is real - don't force it onto an old ComfyUI and then wonder why nothing loads.

Where it goes wrong

The classic mistake is expecting the whole image to improve. Use Affine Options with content_gate on smooth_region or edges_sobel and the effect follows the picture instead of covering it. Second: max_bias is not a baby version of max_scale - 0.02 already shifts overall colour, and bias_field on the options node turns that offset into a noise field. Third, inherited 2024 workflows won't contain this node; the affine nodes are new in v3.

CategoryWAS Suite/Sampling

Inputs (15)

NameTypeDefaultDescription
samplerSAMPLERThe sampler to wrap, from KSamplerSelect or any other SAMPLER source. Every stock sampler carries the affine except dpm_fast, dpm_adaptive, uni_pc and uni_pc_bh2, which is reported in the console rather than failing the run.
affine_intervalINT11–100Apply on every Nth step of the schedule. 1 = every step, 4 = every fourth, which leaves the sampler more room to settle between applications.
max_scaleFLOAT1.0200–2What the latent is multiplied by at the peak of the schedule, compounding over every step it lands on. 1.0 = no change, 1.02 adds visible texture over a 20 step run, 1.05 is strong, and past 1.08 the picture breaks up. Below 1.0 softens instead.
max_biasFLOAT0.000-2–2What is added at the peak of the schedule, beside max_scale rather than through it. 0.0 = nothing, 0.005 = a gentle drift, 0.02 shifts the whole colour. bias_field on Affine Options decides whether that is one flat offset or a noise field.
patternCOMBOwhite_noiseWhich mask decides where the affine lands. 'solid' covers everything; 'white_noise' and the coloured noises are grain of different coarseness; 'perlin', 'checker', 'bayer', 'cross_hatch', 'worley_edges' and the rest are shapes; 'detail_region', 'smooth_region', 'edges_sobel' and 'edges_laplacian' are read off the latent itself; 'external_mask' uses the mask wired in.
affine_seedINT00–2147483647Seeds the mask. The same seed always draws the same mask, so change it to move the grain without changing anything else. Ignored by the content-aware patterns and by 'external_mask', which read what they are given.
affine_seed_incrementBOOLEANfalse`true` advances the seed on every application, so the grain moves from step to step. `false` holds one mask for the whole run, which keeps the affine landing in the same places.
temporal_modeCOMBOstaticHow a video latent's mask varies over time. 'static' = one mask on every frame, in the same place all clip. 'per_frame' = an unrelated mask each frame. 'drift' = one mask slid across the frame, set by drift_speed, drift_angle_deg and drift_renew on Affine Options. The content-aware patterns ignore this, and so does an image latent.
affine_scheduleoptDICTThe per-step strength curve from an Affine Schedule node. Left unwired the affine ramps up over the middle of the run, from a fifth of the way in to four fifths.
affine_streamsoptCOMBOvideoWhich streams of a packed audio and video latent the affine reaches. 'video' = stream 0, 'audio' = the rest, 'both' = all of them. An ordinary latent has only a video stream, so 'audio' does nothing to it.
affine_spaceoptCOMBOlatentWhich latent max_scale and max_bias are measured against. 'latent' = the same scale Latent Affine uses, so a value means the same thing in both places; 'model' = the sampler's own internal latent, which some models hold at a very different magnitude.
external_maskoptMASKA mask of your own, resized onto the latent. On pattern 'external_mask' it is the mask; on any other pattern it gates the generated one, so the affine reaches only where this is white. One mask covers every frame, or one per frame.
affine_optionsoptDICTPattern parameters and mask shaping from an Affine Options node. Leave it unwired and every value takes its default. A pattern set there wins over the pattern widget.
debugoptBOOLEANfalse`true` logs every application to the console: the step, its sigma, the strength, the resolved scale and bias, and which streams were touched. `false` stays quiet.
affine_acts_onoptCOMBOcontentWhat max_scale multiplies. 'content' = only the picture the model has resolved, so the sampler's own noise is left alone. 'latent' = the whole latent, which amplifies that noise as well and prints it as fixed grain on a model that holds noise for most of its run.

Outputs (1)

NameTypeDescription
samplerSAMPLERThe same sampler, now applying the affine as it runs.