Nodes/comfyui-vae-float32/ANDRO VAE Decode
ComfyUI Node

ANDRO VAE Decode

Your VAE decode has been deleting pixels — this node gives them back

By AndreiOrehov·Created 28 days ago·Updated about 18 hours ago· 4
ANDRO VAE Decode
  • samples
  • vae
  • image
  • range_report
keep_out_of_rangetrue
precisionfloat32
tiledtrue
tile_size384
overlap64
temporal_size4096
temporal_overlap8

Every VAEDecode in ComfyUI ends the same way. Before you ever see the image, comfy/sd.py runs clamp_(0.0, 1.0) on the decoded tensor, and because most VAEs execute in bfloat16, the whole pass only has ~77 distinct values to draw from in a mid-tone window like [0.2, 0.3] - versus millions in float32. Neither of those is visible from inside a graph, which is the entire reason this pack exists. ANDRO VAE Decode is a drop-in replacement for the stock node that keeps the out-of-range values and runs the decode in float32. Same latent in, same VAE, same wiring - just two fewer losses on the way out.

Where people get burned: nothing here looks broken. Two decodes of the same latent look identical side by side, which is exactly why this goes unnoticed for so long. The out-of-range material is a fraction of a percent of samples - measured across real LTX-2.5 generations at −0.07 to +1.04, roughly 0.01–0.34% - and it's the tops of speculars and the toes of shadows. The kind of thing a grade reaches for first.

How it works

The node does two surgical things and undoes both in a finally block, so other graphs in the same session are unaffected.

Keeping the range. ComfyUI finishes every decode with that [-1,1] → [0,1] rescale plus clamp. ANDRO VAE Decode swaps in the same maths minus the clamp. The clever bit: it doesn't assume every VAE uses that transform. Some already emit [0,1] natively - TAEHV/lighttae, MiniMax H3, StageA - and forcing the default rescale on those would wreck the image. It probes the VAE's own process_output with −1/0/1 and only replaces transforms it recognises; anything unfamiliar gets left alone and reported.

float32. precision defaults to float32, casting the VAE weights and setting vae_dtype so the decode actually runs at full precision. This costs roughly 3× the decode time and 2× the VAE's VRAM. vae default reproduces stock bf16 output for an A/B.

The inputs that matter

  • keep_out_of_range - ON (default) skips the clamp. Only useful if whatever follows can carry values above 1.0: an EXR writer, or ANDRO Remap Range before an 8-bit save. OFF reproduces stock ComfyUI exactly.
  • precision - float32 or vae default.
  • tiled - ON by default, because float32 blows through VRAM at sizes the bf16 path swallows. With the defaults only the spatial split is active and temporal_size sits at 4096, meaning nothing is cut along time - that's deliberate, see below.
  • tile_size (384 default) - the knob to cut when memory is tight. This one's a cliff, not a slope: while a tile fits in VRAM the size is nearly free, and the moment it doesn't, weights start paging and a 44-second decode becomes 21 minutes with no error. The node estimates your tile against your free VRAM and prints fits / TIGHT / WILL NOT FIT.

Outputs are image (still carrying out-of-range values when enabled) and range_report - a string that states the measured range, what the clamp would have deleted, and the dtypes the decode actually ran in. Wire that report into ANDRO Save EXR and it gets stored in the file header.

The trap that will actually cost you

ComfyUI's own LTX-2.5 template ships VAEDecodeTiled at tile_size 768. That's a sensible choice for bf16, where the tile size is free. Drop this node's float32 decode in and leave 768 in place and the same graph takes twenty minutes on a 32 GB card. Halve the tile. And don't reach for temporal_size to fix memory - cutting along time is what leaves a soft frame on every seam, evenly spaced and easy to miss. Cut space, never time. (Also worth knowing: LTX latent grids floor-divide by 32, so 1080 silently becomes 1056 - use 1088.)

Install

ComfyUI-Manager → search comfyui-vae-float32 → install → restart. Or by hand:

cd ComfyUI/custom_nodes
git clone https://github.com/AndreiOrehov/comfyui-vae-float32

Then restart ComfyUI. The node appears under the ANDRO category. Want the side-by-side proof on your own VAE? The pack ships example_workflows/01_measure_your_vae.json - it round-trips one image and decodes the latent both ways so you can see the level count difference for yourself.

Honest caveats

This pack reaches into vae.process_output, vae.vae_dtype and first_stage_model - none of them public API, so a ComfyUI refactor can break it. The guarded probe means a surprise degrades to "no change, with a note in the report" rather than a corrupted image. If output ever looks wrong, A/B against stock with ANDRO Compare before filing anything. MiniMax H3 hit a one-off Buffer too small error in testing (didn't reproduce); if you see it, precision: vae default avoids the cast entirely.

CategoryANDRO

Inputs (9)

NameTypeDefaultDescription
samplesLATENTThe latent to decode - the same input the stock VAE Decode takes, so this node drops straight into an existing graph in its place. Nested latents are unbound to their first element, as the stock node does.
vaeVAEThe VAE that belongs to the model. Any VAE works: this node probes the VAE's own output transform instead of assuming the default one, so VAEs that already emit [0,1] are left alone rather than rescaled.
keep_out_of_rangeBOOLEANtrueComfyUI ends every decode with clamp_(0.0, 1.0) (comfy/sd.py:502), and the decoder does emit outside that: measured -0.0715 .. +1.0445 on a real LTX-2.5 generation, 0.01-0.34% of samples depending on the shot. Those are specular highlights and shadow detail, deleted before any node downstream can see them. ON keeps them - only useful if what follows can carry them (EXR, or Remap Range first). OFF reproduces stock ComfyUI exactly, for an A/B.
precisionCOMBOfloat32'vae default' is bfloat16 on most VAEs, which is coarser than it sounds: across [0.2, 0.3] of a frame it can represent 77 distinct values, and that is the FORMAT's entire grid there, not a property of the picture. float32 gives ~3.3 million in the same window. Costs roughly 3x the decode time and 2x the VAE's VRAM. Nothing recovers this afterwards - a float32 container around bf16 values is empty precision.
tiledBOOLEANtrueDecode in tiles instead of whole frames. On by default: float32 decoding needs far more VRAM than the stock bf16 path. Only the SPATIAL split is active with these defaults, and overlap blends it - temporal_size is left high on purpose, because cutting along time is what leaves a soft frame on every seam.
tile_sizeoptINT38464–4096Spatial tile in PIXELS, and the knob to cut when you run out of memory - cut it before ever touching temporal_size. Cost is a CLIFF, not a slope: while the decode fits in VRAM the tile size is nearly free, and the moment it stops fitting, weights start paging and the same decode takes tens of times longer, with no error and no warning - just a progress bar that stops moving. WHERE that cliff sits depends on your card, your VAE and the frame size, so there is no universal safe number; this node estimates it for YOUR machine and says so in the report. One measured example, RTX 5090 32GB on LTX-2.5 at 1280x704x121 in float32: 384 = 44s, 512 = 42s, 768 = 1247s. In bfloat16 the same run is 12.1s at both 384 and 768, which is why ComfyUI's LTX-2.5 template ships 768 - right for bf16, ruinous in float32 on that card.
overlapoptINT640–4096Pixels of overlap between neighbouring spatial tiles, blended so the join does not show. 64 against a 384 tile is enough in practice: gradient excess at the tile boundaries measures 1.03-1.05x, well under the 1.30x ANDRO Seam Check needs before it calls anything a peak. Raise it only if Seam Check reports REGULAR vertical or horizontal peaks - a single strong line is content, not a seam. Bigger overlap means more pixels decoded twice, so it costs time.
temporal_sizeoptINT40968–4096Video VAEs only: how many frames are decoded per temporal tile. LEAVE IT HIGH. At 4096 nothing is cut along time at all, which is the point - a diffusion decoder has no context at a temporal tile edge, so the blend leaves a visibly SOFTER frame on every seam, evenly spaced and easy to miss. Measured: temporal_size 32 put a soft frame every 24 frames. Overlap softens that but never removes it. If you are short on memory, cut tile_size instead - the spatial seam is the one overlap can genuinely blend away. ANDRO Seam Check finds these.
temporal_overlapoptINT84–4096Frames of overlap between temporal tiles. Does nothing while temporal_size stays at 4096, because then there is only one temporal tile. It cannot rescue temporal tiling either: more overlap softens the seam frame, it never removes it, since the decoder still had no context at that edge.

Outputs (2)

NameTypeDescription
imageIMAGEThe decoded batch, still carrying values outside [0,1] when that is on.
range_reportSTRINGWhat the decode actually did: measured range, percentiles, how much the stock clamp would have deleted, and the dtypes it really ran in.