Nodes/comfyUI-JigSaw-Hi-Res/🧩 Jigsaw Hi-Res
ComfyUI Node

🧩 Jigsaw Hi-Res

Patch Flux's position embedding instead

By zeus-onlΒ·Created about a month agoΒ·Updated 27 days agoΒ· 1
🧩 Jigsaw Hi-Res
  • model
  • latent
  • MODEL
β—„width1024β–Ί
β—„height1024β–Ί
β—„methodyarnβ–Ί
β—„rope_percentage1.00β–Ί
β—„dype_enabledtrueβ–Ί
β—„max_pe_length64β–Ί
β—„timestep_modulationtrueβ–Ί

Flux.1 is trained around 1024Γ—1024 (comfortable to about 2MP, per the usual community testing). Push it to 2K or beyond and the classic failure modes show up fast: duplicated limbs, tiled repetition, or a hazy "gray veil" wash over the whole image. Most people hit this and reach for the hires-fix dance - generate at native res, upscale, re-denoise at low strength. 🧩 Jigsaw Hi-Res takes a completely different road: it leaves the sampler alone and patches the model's positional encoding so the diffusion transformer itself can think at your target resolution.

It's not an upscaler. You feed it the model you already have, it hands back a patched MODEL, and you sample directly at whatever size you want. That distinction is why it's worth having next to the SeedVR2s and Ultimate SD Upscales of the world.

How it works

Flux and the Krea2/Flux.2-Klein family encode spatial position with RoPE (rotary position embeddings), tuned to the training resolution. Ask for something much bigger and the embeddings extrapolate past anything the model has seen - which is exactly what produces the repeats and the veil. Jigsaw borrows the fix LLMs already use for long context: YaRN and NTK-aware frequency scaling, applied to 2D positions instead of 1D tokens.

At apply time it clones your model, reads the target resolution (your width/height, or auto-detected from a connected latent), and computes an adaptive scaling ratio from how far you're overshooting the 1024px reference - a blend of a linear and an area-based ratio, softly capped at 1.25Γ—. That cap is the interesting bit: it's what keeps contrast stable and stops the gray-veil oversaturation on aggressive high-res attempts. It then swaps the model's pe_embedder for a custom RoPE that reproduces ComfyUI's exact rotation-matrix format, but scales each frequency band individually - near-DC frequencies stay extrapolated, high frequencies get interpolated, following the standard YaRN "NTK-by-parts" ramp.

There's also a DyPE mode (the timestep_modulation toggle, on by default): instead of one fixed scale for every step, the ratio ramps from ~untouched at the start of sampling to full strength as denoising progresses. The logic is coarse-structure-first, fine-detail-later. It's a free quality win in most cases; flip it off if you want the old static behavior.

The inputs that matter

  • model - the Flux/Krea2-family checkpoint you're sampling with. Required.
  • latent (optional) - connect it and the node derives the true target size from the latent's shape (Γ—8 VAE factor), overriding width/height. Easiest way to stay consistent with the latent you're actually sampling.
  • method - yarn (default, per-frequency-band interpolation, best quality), ntk (single global rescale, cheaper), or base (pass-through, scaling disabled).
  • rope_percentage - blend factor for how much of the computed scaling to apply. 1.0 is full effect; dialing down is your manual "less aggressive" knob.
  • timestep_modulation / dype_enabled - the DyPE switch and the master switch for the whole scaling path. Off = the model's original, untouched RoPE.

Output is a single patched MODEL, wired straight into your sampler at the target resolution.

Install

No dependencies, no model downloads, no requirements.txt - it's plain Python plus einops, which ComfyUI already ships. Easiest is ComfyUI Manager (search the pack title "comfyUI-JigSaw-Hi-Res"), or the manual route:

cd ComfyUI/custom_nodes
git clone https://github.com/zeus-onl/comfyUI-JigSaw-Hi-Res

Restart ComfyUI and the node shows up under Jigsaw/Patches β†’ 🧩 Jigsaw Hi-Res. This is a young, low-traffic pack from "Jigsaw & Zeus" (zeus.onl), so don't expect a huge support community around it - the code is small enough to read if something behaves oddly.

Where people get burned

The big one is compatibility: this only works on models whose backbone exposes a pe_embedder with .theta and .axes_dim and the same [..., N, dim/2, 2, 2] rotation-matrix RoPE convention as comfy.ldm.flux.math.rope() - Flux.1, Krea2, and the Flux.2-Klein family. Anything with a different RoPE convention raises a clear error at load rather than silently producing garbage, which is the right behavior but means you shouldn't chain it onto, say, a DiT that isn't in that family.

Two smaller gotchas: max_pe_length is in the schema but explicitly not wired into the scaling math yet - ignore it. And if you connect a latent that was built at one resolution while your empty-latent sampler runs at another, the latent wins, and the two can disagree on what "target resolution" means. Keep them in sync.

CategoryJigsaw/Patches

Inputs (9)

NameTypeDefaultDescription
modelMODELβ€”
widthINT102464–8192β€”
heightINT102464–8192β€”
methodCOMBOyarn3 options: yarn, ntk, base
rope_percentageFLOAT1.000–1β€”
dype_enabledBOOLEANtrueβ€”
max_pe_lengthINT641–4096β€”
timestep_modulationBOOLEANtrueThe 'Dy' in DyPE: blend the YaRN ratio from 0 (fully extrapolated/untouched) at the start of sampling up to full strength as sampling progresses, instead of applying one fixed scale for every step. This follows the coarse-structure-first, fine-detail-later spectral progression that the DyPE paper is built around. Off = old static-scale behavior.
latentoptLATENTβ€”

Outputs (1)

NameTypeDescription
MODELMODELβ€”