Qwen-Image 2.1's VAE is a Wan-2.2-layout VAE with a 16x spatial downscale
and FOUR output channels (comfy/sd.py:841-847 — output_channels read from
the checkpoint tensor decoder.head.2.weight, pad_channel_value 1.0 for
opaque alpha). VAE.decode therefore returns [B, H, W, 4].
Every cascade node in this pack speaks the 3-channel convention, and the
damage is concrete rather than aesthetic: PixelRush's and FreeScale's
layout probes match on exactly 3 channels, so a 4-channel decode slips
through them channels-LAST into a channels-FIRST pipeline; HiFlow's
_sharpen probes shape[-1] == 3 for the same reason.
src/vae_channels.py adds the two pure helpers that normalize the boundary
— strip_alpha_channel on decode, pad_to_vae_channels on encode — wired
into all three adapters. Both gate on getattr(vae, "output_channels", 3),
so the other six architectures take a byte-identical path (pinned). The
encode side deliberately mirrors ComfyUI's own vae_encode_crop_pixels
(pad with pad_channel_value / truncate / leave alone when unset), which
makes it idempotent with core: it can never change what a real VAE
receives, it exists so three adapters share one decision. Alpha is not
preserved end to end — every consumer here is RGB — but the drop is
logged once per session, naming the node, rather than happening silently.
New node EmptyQwenImage21LatentImage: core ships no 2.1 t2i empty latent
(EmptyQwenImageLayeredLatentImage is the layered/edit latent — 16ch, //8,
5-D — and EmptyLatentImage is 4ch at //8), so the pack provides the 64ch
//16 counterpart TextEncodeQwenImage21 only emits as a by-product of
encoding a prompt. Width/height are first-class inputs, snapped to
multiples of 16 with a graph-time validate_inputs message.
Also shipped: example_workflows/DyPE-Qwen21-workflow.json (loaders ->
DyPE_FLUX with model_type qwen21 -> TextEncodeQwenImage21 ->
EmptyQwenImage21LatentImage -> KSampler -> decode), TextEncodeQwenImage21
and QwenImage21Cache added to the workflow validator's core allowlist,
and the structure guard's node-class/module lists extended (their two
tests were named "8_classes"/"8_node_extension" and are now named for
what they assert, since the literal count was the only thing keeping them
from breaking on every node addition).
Tests: tests/test_vae_channels.py (pure helpers, CPU, no ComfyUI) and
tests/test_cascade_qwen21.py (the three adapters end to end against a
fake RGBA VAE, the latent node's shape/validation/round-trip, plus pins
that read ComfyUI's real sd.py and latent_formats.py when COMFYUI_ROOT is
set — the 16x/RGBA/latent_dim==2 facts the whole design rests on).
User: absolute pixel target unintuitive and vague. New param is
relative to the input latent: 2 = double each side, 1 = unchanged,
0.5 = half. Semantics:
- scale applies PER SIDE (old absolute form over-upscaled the short
side of non-square inputs — both sides doubled until the smaller
hit the pixel target)
- up >1 keeps paper 2x-stage quantization: 1.2-2.0 -> one 2x stage,
4 -> two; final may land above exact scale
- scale <1 (0.25-1): single guided refinement stage at the smaller
size; reference trajectory bicubic-resized down, walk unchanged
- range [0.25, 8] validated at node + core
_stage_latent_sizes rewritten (nearest-snap helper split out);
cascade + node params renamed; workflow JSON updated; 158 hiflow
tests (new: downscale stage, per-side non-square, range, scale-2
single stage); suite 1310 passed / 4 skipped; ruff F,E9 green;
v2.13.0.