LatentOperationNormalizeAlongAxis
Normalize the channel, not the whole world
- op
LatentOperationNormalize z-scores the whole tensor and pretends every channel shares one personality. LatentOperationNormalizeAlongAxis is the version that admits they don't. It computes mean and standard deviation along a single axis - so with the right axis, it normalizes each channel independently, which is usually what a sick latent actually needs. One integer input, one decision, and the decision is where the problem lives.
What the axis means
The implementation is (latent - mean) / (std + eps) computed with mean(dim=axis, keepdim=True) and std(dim=axis, keepdim=True). ComfyUI latents are 4D: [batch, channels, height, width]. So:
axis = 1- the useful one. Normalizes per channel: each of the four (SD/SDXL) or sixteen (Flux) channels gets its own mean and variance. If a render looks off because one channel's distribution drifted, this is your fix.axis = -1(the default) - normalizes along the width dimension. That's a weird, per-row kind of normalization, and the default is honestly a strange choice for latents.axis = 0- normalizes across the batch, treating each batch member as a statistic. Only sensible if you're confident all batch entries share a distribution.
The one input is axis (INT, default -1, tooltip: "Axis along which to normalize."). That's the whole UI - no epsilon, no min/max, no flags. And for the love of stable output: set axis=1 unless you have a specific reason not to. The default -1 will "work" but it normalizes a dimension you almost certainly didn't mean.
When you'd reach for it
Real-world case: you're feeding a latent into a custom operation or a second pass that expects per-channel statistics to be sane - say, before blending two latents from different sources, or before an interpolation where the channels don't line up statistically. The KB's concepts doc makes the underlying point: each channel of a VAE latent is doing different representational work (that's why SD1.5 is 4 channels and Flux is 16 - more per-unit-of-latent, not more compression), so normalizing them all with one global mean is often too blunt. AlongAxis with axis=1 respects that structure.
The op output, again
Like every node in this pack, the output is op of type LATENT_OPERATION - a deferred closure, not an applied result. ComfyUI-latent-ops builds operations and ships no apply node, so you need a consumer (Sonar's SonarApplyLatentOperationCFG is the one that exists in the wild) or your own tiny apply node. Directly wiring op into a VAE Decode yields a type mismatch, which is the pack's universal first-bounce. Not your bug; the pack's design.
Install
Same as the rest. ComfyUI Manager → search ComfyUI-latent-ops, or:
cd ComfyUI/custom_nodes
git clone https://github.com/hnmr293/ComfyUI-latent-ops
Restart. No requirements.txt, no downloads, plain PyTorch. This is the workbench of hnmr293 (the sd-webui-cutoff / llul author), with essentially zero community footprint - the source in latent_ops/normalize.py is your documentation, and it's clean enough to read in a minute. Everything registers under hnmr/latent_ops.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| axis | INT | -1-10000–10000 | Axis along which to normalize. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| op | LATENT_OPERATION | — |