Flux 2 Dev Image Creator V3.1.6 — Handbook
Build a character from reference photos on FLUX.2 [dev], then put that character in a new scene at any size you like — up to ten references, no training, no LoRA, no custom nodes. For a RunPod pod running ComfyUI on an RTX PRO 6000 Blackwell (96 GB, sm_120).
VRAM. 52 GB minimum (estimated, ComfyUI swapping models between stages), 88 GB comfortable (everything resident).
| file | role | goes on the pod |
|---|---|---|
SoulCraft Image Creator (Flux 2 Dev).json |
the graph | yes |
image-creator-flux-2-dev-script.sh |
the thin installer: Flux’s rows and hooks on ComfyUI Base | yes |
suite.py + pytest.ini |
the acceptance suite (7 tiers), run by … test |
yes |
score.py |
the identity scorer behind … score IMG |
yes |
taef2_convert.py |
TAEF2 key renumbering for the preview decoder | yes |
image-creator-flux-2-dev-runpod.zip |
the pod files, bundled (built by base/comfyui-base/_build/package.py) |
uploaded, then extracted |
image-creator-flux-2-dev-handbook.md |
this | yes (in the zip) |
_build/build_workflow.py |
generates the workflow — the JSON is an artifact, this is its source | no |
Step one on a fresh pod is comfyui-base/comfyui-base-script.sh — the shared toolchain (uv, Python, torch, ComfyUI tag,
the four shared node packs, launch args, restart policy, one deletion prompt, the ledger). This package is step two and
owns only what is Flux. The Base handbook documents the contract this script follows.
There is deliberately no local_rig.sh and no backup directory: the shared testbed.sh one
level up runs the pod-only tiers off-pod, and git holds the previous releases.
t_config_build_tooling_is_present_and_valid asserts local_rig.sh stays absent.
0. Read this first
Design rules the graph and the installer rely on
- No muted or bypassed nodes, ever. Optional reference slots are switched with a visible
boolean through
ComfySwitchNode, never by node mode. Muted nodes are invisible state and read as broken.t_graph_no_muted_or_bypassed_nodesenforces this. - The model library is a contract. The four loader widgets are derived from the
MODELSmanifest in the script, never restated.t_unit_no_hardcoded_model_filenamesfails if any filename is written down a second time. - Shared pod, rule 1 — never rebuild a venv this script did not create. If one exists and fails verification the run stops.
- Shared pod, rule 2 —
comfyui_args.txtis read-only here. The MiniMax H3 installer owns it.t_unit_never_writes_the_args_fileenforces it. - Shared pod, rule 3 — the deletion pass only ever names Flux basenames.
SUPERSEDEDholds one entry (the v1 Turbo LoRA). Separately, the unrecognised-file scan reports only and never deletes. resolution_stepsis 16 and must stay 16 on all three scalers.EmptyFlux2LatentImagebuilds its latent asheight // 16, width // 16, so a non-multiple silently returns an image up to 15 px shorter per axis.- One version everywhere.
FX_VERSION,extra.fx_version, the script banner and line 1 of this handbook must agree;fx_require_workflowhard-fails on a mismatch. - The output size is set by the user, never derived from a reference. V2.0.0 wired
GetImageSize(image 1)into bothEmptyFlux2LatentImageandFlux2Scheduler, so the canvas inherited reference 1 and Flux 2 edited that image instead of building a new scene. Width and height are controls now.t_graph_output_size_is_independent_of_referencesandt_graph_size_reaches_latent_and_schedulerenforce it, and both are mutation-guarded. - Notes on the canvas are one sentence.
t_graph_notes_are_one_linerscaps them at 220 characters. Reference material belongs here, not on the graph.
Quick start
cd /workspacepython3 -m zipfile -e "comfyui-base-runpod.zip" . && bash "comfyui-base/comfyui-base-script.sh" # step one, once per podpython3 -m zipfile -e "image-creator-flux-2-dev-runpod.zip" .bash "image-creator-flux-2-dev-script.sh" --check # look first, changes nothingbash "image-creator-flux-2-dev-script.sh"Six command forms, the same as every package on the base: bare (everything), --check, --latest, test [tier],
rescue, help — plus score IMG. Tiers: unit graph config comfyui selftest runpod gpu.
Requires ComfyUI >= v0.34.0 and NVIDIA driver >= 580. Both are hard gates — the run stops before anything is downloaded or the venv is touched.
1. Using the workflow
Everything you touch is on the top canvas, left to right. The engine subgraph holds the loaders and the sampler; you should never need to open it.
| group | what it does |
|---|---|
| 1 · Your images | Ten slots, the first five on. Photos of one subject; vary angle and lighting. Image 1 is always applied; Use image 2…Use image 10 switch the rest in and out. Reference detail (MP) scales all ten together. |
| 2 · The scene | The prompt. Describe the whole scene, and name your subject by appearance — never by image number. |
| 3 · Output size | Width and height. Independent of every reference; both must be multiples of 16. |
| 4 · Speed | Turbo mode swaps in the 8-step LoRA and switches steps 20 → 8 through two switches. |
| 5 · Tuning | Guidance 4 follows the prompt closely, lower is looser. Seed re-rolls every run. |
| 6 · Preview | Reference 1 after scaling, plus its resolved size — keep it near the output size. |
| 7 · Result | Saved to ComfyUI’s output folder with the Flux2 prefix. |
Reference images
Ten slots ship wired; the first five are switched on. Image 1 is applied unconditionally;
images 2–10 each sit behind a Use image N toggle on the panel. Image 1 no longer sets the output
size — nothing does but you. For a single subject four to six references is the effective range:
below three the model has too little angle and lighting information, past eight is VRAM rather
than fidelity, and variety of angle and light matters more than count. Ten is FLUX.2’s
documented ceiling (BFL: “Reference up to 10 images
simultaneously”; their editing docs say 8 via API, 10 in the playground).
How it works: ReferenceLatent appends to a list (conditioning_set_values(..., append=True)) and
ComfyUI’s Flux2 model loops over every reference, concatenating each into the attention sequence.
The chain is a passthrough spine with each reference hanging off a switch:
prompt → FluxGuidance → Reference 1 ─┬→ Switch(use 2) ─┬→ Switch(use 3) ─ … ─┬→ Switch(use 10) → guider │ ▲ │ ▲ │ ▲ └───┴ Reference 2 └───┴ Reference 3 └───┴ Reference 10ComfySwitchNode’s branches are lazy, so a slot switched off costs no VAE encode and no
attention tokens — the conditioning routes straight past it and comes out identical to a graph
that never had that slot.
Read this before switching more slots on. Every reference and the output canvas contribute
(width ÷ 16) × (height ÷ 16) tokens to one attention sequence, and attention is quadratic in
its length. All ten references now scale alike from the single Reference detail (MP) control,
so there is no longer a widget to forget:
| configuration | sequence length | note |
|---|---|---|
| 1 reference @ 1 MP | ~8,200 | reference plus a 1248×832 canvas |
| 5 refs as shipped @ 1 MP | ~24,500 | the default — four to six is the effective range |
| 10 refs @ 1 MP | ~45,000 | every slot on |
| 10 refs @ 2 MP | ~86,000 | one control changes all ten |
V2.0.0 shipped ~53,000 because slot 1 ran at 2 MP and the canvas inherited its size, so it was counted twice. Decoupling the canvas is most of the drop; shipping five slots on is the rest.
Attention compute is quadratic in sequence length, so ten references is roughly 10× the attention work of one, not 10% more. Memory is better behaved — ComfyUI uses memory-efficient attention, so VRAM grows closer to linearly — but expect renders to take substantially longer. If a run is too slow, switch slots off from the right; each one you disable is genuinely free.
To raise a reference back to 2 MP, change megapixels on that slot’s scaler. To go past ten,
duplicate a slot’s LoadImage → ImageScaleToTotalPixels, add an engine input, and inside the
engine add VAEEncode → ReferenceLatent → ComfySwitchNode in the same shape. The suite derives
its expectations from the graph, so it follows automatically.
All ten LoadImage nodes read “missing” until you pick images. That is correct and it is not
avoidable: ComfyUI’s validate_inputs recurses through every upstream dependency with no
lazy-awareness (lazy governs execution only), so a switched-off slot is still validated. Give
every slot an image, or switch the unused ones off.
1b. What leaves this pod
Nothing you make. Audited 2026-09-04 and guarded by t_unit_nothing_is_ever_uploaded, which
fails the suite if any request in the script could ever carry a body.
Outbound traffic, in full — four hosts, all of it fetching:
| host | what | carries your data? |
|---|---|---|
huggingface.co |
the four models, TAEF2, YuNet, AdaFace, DINOv2 | no — GETs and HEADs; your HF token if you saved one |
download.pytorch.org |
the cu130 wheel index and torch itself | no |
pypi.org |
huggingface_hub, opencv-python-headless |
no |
127.0.0.1 |
/system_stats, /object_info on your own server |
local, never leaves the box |
Every curl in the script is -sIL (HEAD), -sf or -sfL (GET). There is no -X POST, no
--data, no -F, and no urlopen(..., data=) anywhere — a request with a body is not merely
absent, it is a test failure. HF_HUB_DISABLE_TELEMETRY=1 is exported too, so downloads do not
report back what is being loaded.
The graph is entirely local compute. All 27 node types are core ComfyUI, and none is an API
or cloud node — the suite’s registry is even built with init_api_nodes=False. Prompts, reference
photos and renders are read from and written to disk on the pod and go nowhere else.
score is local too. YuNet, AdaFace and DINOv2 all run on the pod’s CPU. It reads your
images, prints a number, and writes nothing. The only network it ever needs is the first fetch of
its three models (~250 MB into $COMFY/user/flux2/score); after that it runs with the box offline.
The tests are local. The only tier that touches the network is config, and only for
t_config_manifest_resolves_on_the_hub, which sends four HEAD requests to confirm the model URLs
still resolve and the sizes still match. It uploads nothing. Set FX_NO_NET=1 to skip it and the
whole suite runs with no outbound traffic at all.
2. The model library
74.05 GB across four files, from two public, ungated repos. No Hugging Face token is required; one only lifts anonymous rate limits.
| folder | file | GB | source |
|---|---|---|---|
diffusion_models/Flux.2/ |
flux2_dev_fp8mixed.safetensors |
35.456 | Comfy-Org/flux2-dev |
text_encoders/Flux.2/ |
mistral_3_small_flux2_bf16.safetensors |
35.585 | Comfy-Org/flux2-dev |
vae/Flux.2/ |
full_encoder_small_decoder.safetensors |
0.250 | black-forest-labs/FLUX.2-small-decoder |
loras/Flux.2/Turbo/ |
Flux2TurboComfyv2.safetensors |
2.761 | Comfy-Org/flux2-dev |
Provenance (sha256 = the Hub’s LFS oid, verified 2026-09-03)
863a82e4ff950a42a6b0e80bea824828f129eb1a8fbbdbd9e8cb29859127b486 flux2_dev_fp8mixed.safetensors 354555995927d79902f60b1aeb3a6de2cfad02f4367b5e300a1387de3d03ac717cfa3df117c mistral_3_small_flux2_bf16.safetensors 35584897447ea4273f02d1fafbf8e1d1c2cf6018ed8748652eb0bf34f2dd91171f16f15ab62 full_encoder_small_decoder.safetensors 249519092dfc97af0180d432269361a7bc36b4a7df6a2a3ffb630763f8c3343d3d1991d87 Flux2TurboComfyv2.safetensors 2760814872All four confirmed against the Hub’s x-linked-etag. The script verifies byte-exact size from the
Hub HEAD; it does not re-hash 74 GB locally, because hf download is Xet-backed and verifies
content hashes in transit.
The VAE is a deliberate swap, not what Comfy-Org ships
Comfy-Org/flux2-dev ships split_files/vae/flux2-vae.safetensors, and the stock Comfy.org
template uses it. This package instead uses BFL’s full_encoder_small_decoder.safetensors, which
is a different repo. The reason is performance, not provenance: the encoder is unchanged
(34.4 M parameters), so the reference latents your edit conditions on are identical; only the
decoder is distilled (49.6 M → 27.9 M), giving ~1.4× faster decode and ~1.4× less VRAM at
decode time. Apache-2.0.
ComfyUI supports this: comfy/sd.py computes
decoder_ch = sd['decoder.conv_in.weight'].shape[0] // ddconfig['ch_mult'][-1] and builds a
separate decoder_ddconfig when the decoder is narrower than the encoder.
To revert, change the vae/ line in the script’s MODELS array and the VAELoader widget. The
suite derives its expectations from the manifest, so nothing else needs editing.
The text encoder is not swappable
ComfyUI’s flux2 CLIP type resolves to Mistral3Tokenizer (embedding 5120, Tekken tokenizer read
from the file). Qwen3 encoders — including MiniMax-H3’s — cannot load here. Only the precision
varies: bf16 (shipped), fp8 (18.0 GB), fp4_mixed (12.3 GB), both in the same repo.
Superseded
SUPERSEDED holds one basename, offered for deletion (default N) and never taken
automatically:
Flux_2-Turbo-LoRA_comfyui.safetensors— the v1 Turbo LoRA. Same weights as the shipped v2, different key naming; listed only to avoid two copies of one LoRA.
flux2-vae.safetensors is no longer offered: the small decoder replaced it here, but FLUX 3 Klein Image Creator
installs it (vae/Flux.2/), and no package may supersede a file another one installs.
3. The toolchain
Twelve stages, in this order. --check narrates every one and changes nothing.
| # | stage | what it does |
|---|---|---|
| 0 | fx_init |
env, logging, ComfyUI discovery, banner, driver gate |
| 1 | fx_require_workflow |
the JSON must sit beside the script and carry a matching fx_version |
| 2 | fx_tokens |
optional HF token, saved to $COMFY/user/flux2/tokens.env mode 600 |
| 3 | fx_update_comfyui + fx_comfy_gate |
fast-forward to the newest v* tag on branch flux2-stable; stop if < 0.34.0 |
| 4 | fx_venv |
verify, never rebuild one it did not create |
| 5 | fx_models |
index the pod, relocate what is already there, disk gate, download the rest |
| 6 | fx_superseded |
list superseded files; scan for and report unrecognised Flux files |
| 6b | fx_previews |
fetch TAEF2 (10 MB) and convert it into models/vae_approx/taef2_decoder.safetensors |
| 7 | fx_import_check |
main.py --quick-test-for-ci |
| 8 | fx_restart |
does not restart anything. Reports what the running server cannot see until it reloads, and stops. FX_RESTART=1 opts in to an actual restart |
| 9 | fx_smoke |
/object_info covers every node type; loaders resolve their paths |
| 10 | fx_test |
the embedded suite plus the bash integration tier |
| 11 | fx_summary |
the report; exits 1 if anything is in FX_FAILED |
On a fresh pod with nothing else installed
The installer is fully standalone — H3 is not a prerequisite, only a neighbour it is careful around. On a bare pod it does all five things:
| it needs to | how |
|---|---|
| find ComfyUI | by main.py, across $COMFY_DIR, the cwd of any running main.py, then /workspace/runpod-slim/ComfyUI, /workspace/ComfyUI, /workspace/madapps/ComfyUI, ~/ComfyUI, /ComfyUI |
| find files already on the pod | _fx_build_index walks /workspace, /runpod-volume, $COMFY, the models dir, /root, /home, /opt for every *.safetensors, then matches on /-anchored basename and size |
| move them into place | same-device mv, otherwise cp → verify → rm, into models/<category>/Flux.2/ |
| update ComfyUI | git fetch --tags --force --prune, then checkout the newest v* tag onto branch flux2-stable |
| download the rest | hf download (Xet-backed), after a disk gate of needed + 10 GB |
It also reads extra_model_paths.yaml and will install into that base_path instead of
ComfyUI/models when the first section points at a real library.
The one thing it will not do for you: rebuild someone else’s venv.
- No venv at all → it builds one on the picked Python + torch and carries on.
- A venv that verifies → reused as-is. See Which venv Flux will replace above for what “verifies” means for each kind of venv.
- An unstamped venv that fails verification → taken over and rebuilt, old one kept as a rollback. Only a venv another installer has stamped is left alone.
On the RunPod ComfyUI Blackwell Edition (CUDA 13) template the bundled venv verifies and this never comes up. On a CUDA 12 or Python 3.11 template it will stop, name the reason, and tell you the fix: delete that venv and re-run, at which point it builds its own. That is the only manual step on an otherwise cold pod.
Two other hard stops, both before anything is written: ComfyUI older than v0.34.0, and NVIDIA driver older than 580.
Download speed
The Hub is fully Xet-backed, so hf-xet is the only fast path — the legacy HF transfer
accelerator is deprecated upstream and is explicitly unset here, along with HF_HUB_DISABLE_XET
in case the pod image sets it. What the script tunes on top of that, and why:
| setting | value | why |
|---|---|---|
HF_XET_HIGH_PERFORMANCE |
1 |
tells xet-core to saturate the link and use every core |
HF_XET_NUM_CONCURRENT_RANGE_GETS |
32 (upstream default 16) | concurrent byte-range GETs per file; a RunPod uplink beats 16 comfortably. Override with FX_XET_CONCURRENCY |
HF_XET_CHUNK_CACHE_SIZE_BYTES |
0 |
74 GB of unique weights fetched once — the chunk cache can never hit, and every byte it holds is a second write. Also upstream’s default; pinned so it cannot drift |
HF_XET_CACHE |
container disk, not /workspace |
upstream is explicit that it wants local SSD/NVMe and not a distributed filesystem. With the chunk cache off there is nothing in it worth persisting, so the volume buys nothing and costs network round-trips |
UV_CACHE_DIR, PIP_CACHE_DIR |
on /workspace |
wheels are worth persisting across a pod restart |
Two things make the result visible rather than assumed:
- A transfer-path check. If
hf_xetis not importable, downloads fall back to plain HTTPS — several times slower, with no error anywhere. The venv step now imports it and prints the version and the concurrency it will use, or warns loudly. - Measured throughput. Each file prints its own seconds and MB/s, and the summary prints the
average across everything downloaded. If that number looks low for the pod, raise
FX_XET_CONCURRENCYand re-run — every stage is idempotent, so a re-run costs only what is actually missing.
The four files download one at a time, deliberately. With high-performance mode already saturating the link and using every core, running them concurrently contends for the same bandwidth rather than adding to it, and it makes per-file size verification and resume much harder to reason about. The throughput line is there so you can check that assumption on your own pod rather than take it on trust.
Outbound hosts, and nothing else: huggingface.co, pypi.org, download.pytorch.org.
t_unit_outbound_hosts_allowlisted enforces the list.
This workflow needs no custom nodes. All 26 node types are ComfyUI core, so there is no pack
stage and custom_nodes/ is never written to.
What the suite actually proves
Seven tiers (the old integration tier became the base’s install tier). The comfyui tier imports the real ComfyUI on the pod — that is where being
wrong costs a failed queue, so that is where the strongest checks live.
| tier | what it does |
|---|---|
unit |
the manifest (4 rows, Flux.2, two ungated repos, exact bytes), the tool files, this thin script’s own contract: thin, no toolchain, score read-only, previews verified not assumed. The toolchain contracts (venv, torch, restart, args file, downloads) are the base’s suite now |
graph |
the workflow as data. t_graph_integrity audits both canvases for eleven defect classes: dangling links, links to missing nodes, a link whose recorded type disagrees with either slot it joins, stale inputs[].link and outputs[].links back-references, unconnected required inputs, duplicate node and link ids, subgraph IO whose linkIds no longer match, an engine input left undriven, and id counters behind the graph. The rest cover the reference chain and its switches, every slot toggled on, labels, one-sentence notes, layout, and no muted nodes |
config |
one version everywhere, the handbook names the shipped files, and every manifest URL still resolves on the Hub at the declared size |
comfyui |
against the real node registry: every type registered, the Flux.2/ path convention resolves through folder_paths, the flattened workflow passes ComfyUI’s own validate_prompt, and every shipped control value is inside the consuming node’s declared min/max |
selftest |
breaks the workflow 22 ways and asserts the tests above notice. A green suite proves nothing if its assertions cannot fail. Verified 22 caught / 0 missed against a real checkout |
runpod |
the live server’s /object_info covers every node type; the four models are on disk at the right size |
gpu |
pod only — the base’s gpu tier checks the device; nothing Flux-specific remains here |
t_comfyui_prompt_validates is the one that matters most. It inlines the engine subgraph into an
API-format prompt exactly as the frontend does, then runs execution.validate_prompt — the same
function the server runs when you press Queue. It catches an unregistered node type, a loader
pointing at a file the installer never writes, a missing required input, an invalid combo value,
and a widget path missing its Flux.2/ prefix, all before you queue anything.
Two things that test had to get right, both found by deliberately breaking it:
validate_promptreturnsvalid=Truewhen any single output is still reachable. A broken sampler that only killsSaveImagestill reports valid, becausePreviewImagesurvives. The test therefore requires every output node reachable and zero node errors.--cpuis ignored unlesscomfy.options.enable_args_parsing()is called first.comfy/cli_args.pyonly readssys.argvoncemain.pyflips that switch, so a naivesys.argv = ["main.py", "--cpu"]does nothing and importing nodes claims the pod’s GPU while the server is running. The suite enables parsing first and then asserts the device really is CPU before going further.
Scoring a render
bash "image-creator-flux-2-dev-script.sh" score path/to/render.pngThe one question the graph cannot answer about itself: did the identity actually survive into the new scene? The command reads the reference filenames straight out of the workflow and honours the slot toggles, so it scores against exactly the set the engine saw.
Two measurements, deliberately separate, then blended 60/40:
- Face — YuNet detects and gives five landmarks, the crop is warped to the canonical ArcFace 112×112 template, and AdaFace IR101 embeds it; the score is the best cosine across the reference faces. The embedding ships unit-norm, so cosine is a plain dot product.
- Subject — DINOv2 CLS embedding against the centroid of the whole reference set. This is what catches wardrobe and build when the face is turned away, small, or absent.
Why AdaFace and not SFace, which OpenCV bundles for free and which V3.0.0 originally used:
SFace is a MobileFaceNet — a phone-scale backbone. The two tie on LFW (99.57 vs 99.80) because
LFW is saturated and nearly frontal. They do not tie on IJB-C, which is built from pose,
lighting and quality variation: AdaFace 97.4% against ArcFace’s 91.8%. A character rendered into
a new scene at a new angle is the IJB-C regime, not the LFW one. This is the TorchScript repack,
so nothing executes remote code — the CVLface originals need trust_remote_code=True.
Alignment is load-bearing. Two unaligned crops of the same person score like two different
people. The warp to the five-point template is verified equivalent to OpenCV’s own
FaceRecognizerSF.alignCrop — identical to within one grey level of interpolation, which is why
SFace is no longer downloaded just to borrow its warp. Channel order matters too: the model wants
BGR, and feeding RGB costs about 0.12 of cosine on identical input.
There is deliberately no pass/fail line. AdaFace publishes TAR@FAR, not a cosine cutoff, and those numbers are photograph-vs-photograph while this compares a diffusion render against real photos — a regime with a documented synthetic-real gap. Use it comparatively: rank your own takes against each other and calibrate the band from renders you have judged by eye. A borrowed threshold would be false precision.
If no face is found anywhere it falls back to subject-only and says so; a bare number that
quietly meant something different would be worse than no number. Nothing here feeds back into
generation — it is a measurement, and t_unit_score_is_a_measurement_not_a_step keeps it one.
The detector (233 KB) and the embedder (249 MB) are fetched once into $COMFY/user/flux2/score
and are deliberately not in the MODELS manifest: they are not ComfyUI models and never
enter models/. They follow the TAEF2 precedent instead. Licensing is personal/research use —
AdaFace’s weights are the WebFace12M checkpoint, whose dataset terms bar commercial use.
Changing the number of reference slots
The workflow is generated. Edit N_SLOTS in _build/build_workflow.py and re-run:
cd _build && DST="../SoulCraft Image Creator (Flux 2 Dev).json" python3 build_workflow.pyThen re-run the suite. Every graph test derives the slot count from the workflow, so nothing in the suite needs editing — verified by regenerating at N = 1, 4, 10, 12 and 20 and getting an identical result each time, 0 failures. Structural changes belong in the generator; a re-save from ComfyUI rewrites positions and ids, so structure edited in the UI is lost on the next regenerate.
Running the pod-only tiers off-pod
comfyui needs an importable ComfyUI; runpod needs a live server. Both run on the pod
automatically. Off-pod they use the shared testbed.sh one directory up — the same harness
every project folder here uses. Do not build a second one.
bash ../testbed.sh --server # once, in your OWN shell (see below)bash "image-creator-flux-2-dev-script.sh" test # that is all — the testbed is found for youtest auto-detects the testbed: if ../testbed.sh and ../ComfyUI/main.py both exist it
points COMFY_DIR there, and if a server answers on TESTBED_PORT (8199) it uses that too. Both
markers are required so an unrelated checkout is never mistaken for it. COMFY_DIR and FX_HOST
still override, and COMFY_DIR is Flux’s analogue of the H3_NODE_SRC the H3 package uses.
This happens in test mode and nowhere else. The testbed is deliberately invisible to
_fx_discover so a plain run or --check can never install into it, rebuild its venv or restart
it — --check off-pod correctly reports “no ComfyUI tree found” rather than finding the testbed.
t_unit_testbed_is_test_mode_only fails if that boundary is ever crossed.
Start the server from a shell that will outlive the command. A server launched from inside a
tool call dies with the process group when that call is interrupted, even under nohup.
The suite never writes into the testbed. t_comfyui_model_paths_resolve and
t_comfyui_prompt_validates redirect folder_names_and_paths and set_input_directory to a
mkdtemp sandbox, so the four model placeholders and the input image land there and are removed
on exit. Verified by fingerprinting the testbed before and after: identical, 0 files changed.
Watching the image form
ComfyUI ships with previews off (--preview-method defaults to NoPreviews), which is why a
render shows only a percentage. The launch line this script prints turns them up as far as they go:
--preview-method taesd --preview-size 1024Two decoders can serve a preview, and Flux 2 supports both:
| method | needs | quality |
|---|---|---|
latent2rgb |
nothing — the coefficients are baked into ComfyUI (class Flux2 has latent_rgb_factors) |
approximate colour; composition reads, detail does not |
taesd |
models/vae_approx/taef2_decoder.safetensors |
a real decode — near what the final image looks like |
--preview-method taesd is always safe: with no TAEF2 present ComfyUI logs a warning and falls
back to latent2rgb by itself.
The launch flag is not the whole story. ComfyUI PR #11261 made the preview method a
per-queue value: execution.py calls set_preview_method(extra_data["preview_method"]) on
every prompt you queue, sourced from the browser’s Settings → Execution → Live preview method.
| that setting | what happens |
|---|---|
default (its shipped value) |
defers to the server’s --preview-method flag |
none |
overrides the flag — no previews, however the server was launched |
taesd / latent2rgb / auto |
overrides the flag with that method, no restart needed |
So a server launched without the flag shows nothing even with TAEF2 installed, and a UI set to
none shows nothing even with the flag. _fx_preview_effective reads both — the running
server’s /proc/PID/cmdline and Comfy.Execution.PreviewMethod in
user/default/comfy.settings.json — and reports what is actually in effect, warning when the
combination yields no previews. Setting the UI to taesd fixes it either way, with no restart.
TAEF2 needs converting on the way in. madebyollin/taef2 ships diffusers-style keys
(decoder.layers.<i>.<rest>), but ComfyUI loads the decoder alone and wants bare indices shifted
by one (<i+1>.<rest>). Dropped in as-is it fails with Missing key(s) “1.weight”, “3.conv.0.weight”…
fx_previews does that conversion. Verified end to end: 79 tensors converted, ComfyUI’s TAESD
loads the result and decodes a 16×16 latent to 256×256.
Previews cost a little time per step. With all ten references on at ~45,000 tokens you are at the
expensive end — if steps drag, relaunch with --preview-method latent2rgb, or none.
Disk space on a RunPod volume
The banner’s volume figure comes from df, and on a RunPod network volume that is MooseFS,
which reports the whole storage cluster rather than your quota:
mfs#us-nc-2.runpod.net:9421 707 TB total 55% used 323 TB "available"The arithmetic is right; the meaning is not. A 400 GB volume shows ~323 TB free, so any size comparison against it passes unconditionally — the pre-download gate could never fire, and a genuinely full volume would fail 60 GB into a 74 GB download instead of before it started.
_fx_fs_is_pool recognises MooseFS, Ceph, Gluster, NFS, SMB and FUSE mounts (plus anything
claiming more than 50 TiB free) and marks the number <- shared pool, NOT your volume quota.
The download stage then says the check cannot run and records a warning, rather than
printing a pass it did not earn. Confirm your volume size in the RunPod console instead.
FX_FAKE_FREE_GB still overrides everything, so the full-disk test keeps exercising the gate.
Latest everything
The standing rule for this pod is that torch, CUDA, Python and ComfyUI are all current, and Flux delivers that on its own — the MiniMax H3 installer may never have run here.
| component | how it is chosen | today |
|---|---|---|
| ComfyUI | fast-forward to the newest v* tag every run |
v0.34.3 |
| Python | newest CPython uv can provide that ComfyUI’s requirements actually resolve on, stepping back a minor if not |
3.14 |
| torch | newest on the cu130 index with a wheel for that Python — no soak window | 2.14.0 |
| CUDA | fixed at 13.x: the only stable sm_120 wheels |
13.0 |
Nothing is hardcoded. When 3.15 ships with wheels, the Python pick moves itself; the resolve step is what stops it choosing a version nothing has built for yet.
Which venv Flux will replace
A stamp is a claim of ownership. H3 writes .h3-venv-<ver>; this script writes .fx-venv-<ver>.
what is at $VENV |
what happens |
|---|---|
| nothing | builds one on the picked Python + torch, stamps it |
| unstamped — the stock RunPod template venv | takes ownership: moves it to $VENV.pre-flux-<ts> as a rollback, rebuilds, stamps it |
ours (.fx-venv-*) |
reused if current; rebuilt in place if the picks have moved on |
H3’s (.h3-venv-*) |
never replaced. Judged only on whether it runs — torch imports, CUDA 13.x, sm_120. If it does, Flux installs onto it and prints how far behind it is. If it does not, Flux stops and tells you to re-run H3 |
A rebuild always moves the old venv aside rather than deleting it, and rolls back automatically if the new one fails to verify.
Notes are not failures. A torch coming from system site-packages is normal on the stock RunPod
image and is reported with ~, not x. The hard failures are: torch will not import, CUDA is not
13.x, no sm_120 kernels, or — for a venv we maintain — the Python or torch version is behind the
pick.
Test hooks: FX_FAKE_ROOT, FX_NO_NET=1, FX_FAKE_FREE_GB, FX_FAKE_DL=fail,
FX_FAKE_BYTES, FX_INNER, FX_YES=1, FX_SEARCH_ROOTS, COMFY_DIR.
3.9 V3.0.1 — the pod comes back after a stop/start
Ported from MiniMax H3 V5.7.1, where it was found. This installer had the identical defect and would have bricked a pod the same way.
uv venv --python X fetches a managed CPython into $HOME/.local/share/uv/python — container
disk — and writes an ABSOLUTE symlink at $VENV/bin/python plus an absolute home = in
pyvenv.cfg. We build at $COMFY/.venv*, the path the template’s start.sh activates, and
start.sh runs python main.py & wait rather than exec. So a pod stop/start restores the
container disk from the image, the interpreter is gone, the child dies at once, start.sh returns,
PID 1 exits and the pod stops — JupyterLab with it, leaving no terminal to repair from and no
cure anyone found but deleting the network volume.
The caches were already moved onto the volume for speed; the interpreter sat in the same place and was never re-examined, because it read as a cache too. A cache on ephemeral disk costs a re-download; a runtime dependency on ephemeral disk costs the pod.
UV_PYTHON_INSTALL_DIRdefaults to/workspace/h3/python— deliberately the same directory the H3 installer uses, since both target the same pod and the same venv path — plusUV_MANAGED_PYTHON=1so there is one source of Python. Not under/workspace/.cache/*:uv cache cleanis documented and safe and would otherwise brick the pod._fx_venv_probenow fails hard when the base interpreter orpyvenv.cfg’shomeis on a runtime-written path. Because the probe gates the reuse decision, that one change both refuses to reuse a poisoned venv and sends it to_fx_venv_buildto be rebuilt. Note what it deliberately does not condemn:/usr/bin/python3.12is restored from the image at every start, so the stock template venv is sound — only$HOME,/root,/tmpdisappear.- The backup wording now says to delete it once the pod has been stopped, started and rendered. A render proves nothing about a restart, which is the only thing that backup guards against.
Two tests, both mutation-proven: t_unit_the_venv_interpreter_lives_on_the_volume and
t_unit_the_probe_condemns_a_container_disk_interpreter.
3.10 V3.1.0 — onto ComfyUI Base (2026-09-04)
The 3124-line installer became a ~110-line declaration on the shared base: four MODELS rows with exact byte sizes, the
SUPERSEDED pair, two hooks (pkg_post_models installs and verifies the TAEF2 preview decoder; pkg_cmd_score is the
identity scorer), and base_main. Everything the old script did about the pod — venv ownership and the restart-survival
probe, the Python/torch picks, the ComfyUI tag, the args file, the never-restart policy, the disk gate, the deletion prompt,
the run log — is the base’s now and is tested there once for every package. The Flux suite keeps all 21 graph tests, the
config, comfyui, selftest and runpod tiers, and ten unit tests about what is still Flux’s; 32 toolchain self-contracts moved
to the base’s suite. Three shared-pod rules this file used to state are now structural: one venv stamp for every package,
the base owns comfyui_args.txt, and deletion is scoped by the ledger so nothing another package installed is ever offered.
score.py and taef2_convert.py were heredocs; they are files now. The previous installer is git show :"image-creator-flux-2-dev/image-creator-flux-2-dev-script.sh".
3.11 V3.1.1: the base’s own pack rows are derived (2026-10-16)
test_unit_manifest_parses listed the base’s packs by hand (rgthree, KJNodes, VideoHelperSuite, cg-use-everywhere); the base now
ships six (ComfyUI-Manager and ComfyUI-advanced-model-manager joined), so the gate was red with PACKS=() unchanged. The test now
reads them from the base (basetest.base_packs()). No graph change but the version stamp.
3.12 V3.1.2: flux2-vae.safetensors is no longer superseded (2026-10-16)
FLUX 3 Klein Image Creator installs vae/Flux.2/flux2-vae.safetensors, inside this package’s family folder, and the base’s
cross-package rule fails when one package supersedes a file another installs. SUPERSEDED keeps only the v1 Turbo LoRA
(§Superseded). No graph change but the version stamp.
3.13 V3.1.3: every Hub row is pinned to a commit (2026-10-16)
ComfyUI Base 3.12.0 honours the revision in a row and checks each Hub file against the sha256 the Hub states for that commit, once, at download, so a repo that moves or renames a file upstream can no longer 404 a row or change what installs (the H3 latent upscaler broke that way on 2026-09-17). The four rows name the commit base.sh gen-models <dir> --pin printed on 2026-10-16 (Comfy-Org/flux2-dev at ed33133, black-forest-labs/FLUX.2-small-decoder at a3efc24), each file’s size and Hub sha256 unchanged. The graph’s own download hints (properties.models on four loaders) still name main; they are the frontend’s missing-model links, not the installer’s. Files already on a volume are kept as they are: no download and no re-hash. BASE_MIN is 3.12.0, because an older base drops the revision and fetches main. No graph change but the version stamp.
3.14 V3.1.4: BASE_MIN 3.17.1, and the version test skips without _build (2026-10-16)
BASE_MIN 3.17.1 (the base marks the App Mode tour seen on install, so the form opens clear). The suite’s test_config_versions_agree ran its builder check behind an if, so a run from the extracted zip passed without it; it now SKIPS when _build/ is missing, and CI’s VERIFY_SUITE=zip runs it again from the repo (error log row 106). No graph change but the version stamp.
3.15 V3.1.5: every warning is a failure (2026-10-16)
pytest.ini gains filterwarnings = error, the house rule (warnings are errors). It surfaced one cause, ten times over: the suite read files with a bare open(...).read() and left each handle to the garbage collector (ResourceWarning: unclosed file). Every read now closes its file (_text() in suite.py), and so do score.py’s workflow read and the builder’s write. No test is skipped or filtered. One place records instead of raising: registry() imports ComfyUI’s own nodes.py into the suite’s interpreter, and there the rule reached into ComfyUI and torch (on Python 3.14, torch.jit.script warns while kornia loads, so comfy_extras/nodes_post_processing.py failed to import and ImageScaleToTotalPixels went missing on CI). That import now runs under catch_warnings(record=True); every warning is kept, and the new test_comfyui_no_warning_comes_from_this_package fails on any whose source is this package. No graph change but the version stamp.
3.16 V3.1.6: GPU_MIN_GB for the site’s catalog (2026-10-17)
GPU_MIN_GB="52", estimated: the smallest card the default flow runs on, for the site’s catalog. It is the heaviest stage’s weights at the precision the loader picks, plus ComfyUI’s own memory_required activations and its 1.2 GiB inference reserve, with ComfyUI swapping models between stages; the script’s comment holds the arithmetic. Comfortable: 88 GB. A measured peak replaces the estimate at the next free GPU smoke. No graph change but the version stamp.
4. What changed in V3.0.0
V2.0.0 was an image editor and was coherent about it at six separate layers. Flux 2 was doing exactly what it was told; it was told the wrong thing.
The two causes
- The canvas followed reference 1.
GetImageSizefed bothEmptyFlux2LatentImageandFlux2Scheduler, so the output could only ever be reference 1’s size and shape. - The defaults taught editing. The shipped prompt ended in “Keep the face”, and the canvas note read “Describe the change you want, not the whole scene.” Reference dominance rises as the prompt gets shorter and more edit-shaped, so fixing the wiring alone would not have been enough — a short edit prompt hands control straight back to the references.
What changed
- Output width and height are controls, feeding the latent canvas and the scheduler together.
The scheduler derives
seq_len = width × height ÷ 256; if it disagreed with the canvas it would schedule sigmas for a resolution nothing is denoising. - Reference detail is one control for all ten slots, replacing slot 1’s special 2 MP. A reference far from the output resolution bakes over-fit texture, so they should track.
- Five slots ship on, not ten. Four to six references is the effective range for one subject. The old default left six slots pointing at the stock placeholder — a genuinely different subject fused in at full attention cost, silently. Slots may now ship off: that is visible state on a labelled control, which is what rule 1 has always actually required.
- A
scoresubcommand answers the one question the graph cannot ask about itself. - Every label, note and title that described editing now describes what it does.
Superseded claims
- “Image 1 sets the output size” — it does not, and no longer should.
- “Describe the change you want, not the whole scene” — exactly backwards for this workflow. Name your subject by visual attributes rather than by slot number; the references are concatenated latent tokens, not indices the Mistral text encoder can address.
4b. What changed in V2.0.0
Capability
- Ten reference image slots, all switched on (was one), gated by visible booleans and chained
through
ReferenceLatent. Ten is FLUX.2’s documented ceiling. - The download path is tuned rather than left on upstream defaults — see §3.
Layout
- Top canvas is the control panel. The six controls that were
proxyWidgetson the engine node are now real, labelled subgraph inputs driven by primitive nodes you can see and title.proxyWidgetshas no label field, which is why they showed asnoise_seed,sampler_name,batch_size; that was the blocker. - The 3,000-character canvas note is gone, replaced by six one-sentence notes, one per group.
- No collapsed nodes and no muted nodes.
Corrected claims
- The VAE is not “what Comfy-Org ships” — it comes from a different repo (see §2).
- The models come from two repos, not “all four from one ungated repo”. Both are ungated, so the token is still optional.
SUPERSEDEDholds two entries, not “exactly one”.- The stage table has twelve rows, not eleven.
- The driver check is now a real gate, matching what this handbook always claimed.
Script bugs fixed
| id | bug |
|---|---|
| S1 | _fx_fake put "$@" in env’s NAME=VALUE slot, so _fx_fake --check aborted with rc 127 and the swallowed failure made the --check inertness test vacuous — it had never tested anything |
| S2 | an HTTP error body’s Content-Length became a permanent bogus “exact” size |
| S3 | offline runs re-downloaded files already on the pod (strict size equality vs the ±2% the checker allows) |
| S4 | the deletion offer matched any path ending in a superseded basename (my_flux2-vae.safetensors) |
| S5 | $(( )) on an empty operand aborted the run; the || echo 0 guard could never fire because awk exits 0 while printing nothing |
| S6 | df -Pk is 1024-byte blocks; the script multiplied by 1000 |
| S7 | the runpod tier could never run from script.sh test |
| S8 | a failed staging cleanup reported a good 35 GB download as failed |
| S9 | an unguarded torch pipeline aborted with “unexpected failure” instead of the designed message |
| S10 | grep -c ... || echo 0 yielded "0\n0" and logged [: integer expression expected on every clean run |
| S11 | fake runs inherited the real HF_TOKEN and wrote it into a temp dir |
| S12 | the integration tier created ~74 GB of sparse files per fake run (now FX_FAKE_BYTES) |
| S13 | the retry loop slept and announced “retrying” after the final attempt |
| S14 | the venv stamp was written to $VENV but mkdir’d $FX_STATE |
| S15/S16 | unquoted $base (word-split + glob) and unquoted $root inside ${..} |
Dead code removed: _fx_replace_file, _fx_confirm_yes, CN, the never-checked required
manifest column, '*.partial' in the index scan, two unused allow-list hosts, ${FX_DRY:+}.
5. Verification
Offline, V3.1.0 on ComfyUI Base (2026-09-04)
bash "image-creator-flux-2-dev-script.sh" test against the shared testbed with its server live: 45 passed, 1 skipped
(runpod models-on-disk, which needs the pod); every tier ran, the comfyui tier under the testbed venv. A fake-pod
--check changed nothing and a fake run relocated/“downloaded” the four rows, synced the workflow’s loader paths and wrote
the flux2 ledger entry; a second run downloaded nothing and reused the venv. The base’s own suite: 91 passed.
Offline (2026-09-03, reproduced on this machine)
bash -nclean;shellcheckdefault severity 4 findings, down from 8, all intentional (oneSC1090sourcing a variable path, two deliberate word-splits for PID and tier lists).- The embedded suite wired to the shared
testbed.shComfyUI (v0.34.3, 12 custom node packs) with its server live: 73 passed, 0 failed, 2 skipped (the two skips are the pod-onlyrunpodmodel check and thegputier). V1.0.0 was 26 passed with 4 skipped and no way to run them. 47 tests ship insuite.py(V3.1.5; V3.1.0 had 46; V3.0.1 had 77 python + 5 bash, of which 32 toolchain self-contracts and the bash tier now live in ComfyUI Base). - The
selftesttier ships, so mutation testing is repeatable rather than done once by hand: 22 cases, 22 caught, 0 missed against a real checkout. It found two real defects on its first run — two graph tests were reading module-level globals derived at import, so they asserted against the pre-mutation graph; a third crashed withIndexErrorinstead of asserting. Neutering a guarded test makes the selftest name it: “assertions that cannot fail: t_graph_no_muted_or_bypassed_nodes did not catch: a bypassed node”. - Nothing used to verify this package now lives outside it. The standalone graph validator I
worked with became
t_graph_integrity; the ad-hoc mutation harness became theselftesttier; the prompt-flattening validator becamet_comfyui_prompt_validates; the workflow generator became_build/build_workflow.py. There is no scratch tooling left that a future session would have to rebuild. - Regenerating at N = 1, 4, 10, 12, 20 gives an identical result each time with 0 failures against a real checkout, so the slot count is genuinely parameterised and the suite follows it. A throwaway rig was built for this — ComfyUI cloned at v0.34.0, its full requirements installed in a venv, and a CPU server started — specifically so the tiers that used to skip could be run rather than trusted.
- The only two tests that still skip genuinely need the pod:
t_runpod_models_are_on_disk(needs the real 74 GB) andt_gpu_blackwell_96gb(needs the GPU). Everything else now executes. - The flattened workflow passes ComfyUI’s own
validate_promptagainst v0.34.0: 83 API nodes, both output nodes reachable, zero node errors. - The suite parses and runs on Python 3.9 as well as 3.12 —
fx_testfalls back to the system interpreter when the venv is absent, so that path is exercised too. - Mutation testing now ships as the
selftesttier (22 cases, above). Separately, the five download-tuning settings were each mutated by hand and every one was caught: high-performance mode off, concurrency below upstream’s own default, the chunk cache re-enabled, the Xet cache moved back onto the network volume, and the deprecated accelerator left enabled. - The tuning variables were verified to actually export, including the
FX_XET_CONCURRENCYoverride. - The
integrationtier: 5 passed. - Graph integrity on both canvases: 0 failures across dangling links, type mismatches, stale back-references, unconnected required inputs, duplicate ids, and muted nodes.
- S1 proven with a before/after. Injecting a deliberate mutation into the
--checkpath makes V2.0.0 reportFAIL integration --check mutated the tree. The same injection against_v1.0.0_backup/still reportsok integration 5 passed— which is what a vacuous test looks like. - S2, S3, S4, S5 each reproduced against the live Hub and a synthetic index.
On the pod — first install, 2026-09-04
The acceptance install ran end to end on the RTX PRO 6000 pod. 74.05 GB in 105 s, 673 MB/s
average (786 / 789 / 30 / 239 MB/s per file — the VAE is small enough that setup dominates).
Suite on the pod: 69 passed, 0 failed, 4 skipped. Three of those skips are config tests that
need the handbook and _build/, neither of which ships in the zip — correct behaviour, not a gap.
The fourth was runpod’s model check against a server that predated the run. The gpu tier
passed on the pod for the first time.
It reused H3’s venv rather than rebuilding it, exactly as designed, and said so twice: Python 3.13.15 against 3.14 available, torch 2.13.0 against 2.14.0. Both are notes, not failures — the stamp rule means Flux does not fight another installer for the venv.
The disk gate could not run: /workspace/runpod-slim/ComfyUI/models reported 716,517 GB free
because RunPod volumes are MooseFS and df answers for the whole cluster. The run said so and
filed a warning rather than pretending to have checked.
Two defects the run exposed, both fixed in this version:
- The launch line carried
--preview-methodtwice. The script added--preview-method taesd, then appendedcomfyui_args.txt— which H3 owns and which sets--preview-method auto— and argparse takes the last flag, so ours lost silently. Previews still worked (the UI setting overrides the CLI, andautofinds TAEF2 anyway), but the printed command was wrong._fx_preview_flagsnow defers when the args file sets its own, andt_unit_preview_flags_defer_to_the_args_filekeeps it that way. The launcher used byFX_RESTART=1had a related bug: it added no preview flags at all, so it did not run what the hand-off printed. Both paths now build the flags in one place. - The hand-off still described the V2 layout — “Both numbers in 5 · Preview must be x16”.
Preview is group 6 now and has nothing to type into; the ×16 numbers are
3 · Output size.
Before you queue anything
--check is safe to run first — it changes nothing, and the model-on-disk test correctly skips
because a dry run downloads nothing.
--check judges the version you would end up on, not the one you are on. The dry run cannot
fetch, so it asks the remote read-only for the newest v* tag and gates against that:
-> would fast-forward ComfyUI v0.30.0 -> v0.34.3 (the gate below judges v0.34.3) OK version gate: v0.34.3 >= v0.34.0Without this, any pod below the gate saw --check report a blocking problem that a real run
fixes by updating first. If the remote is unreachable it says so and falls back to judging the
current version.
On a network volume, the disk line is df reporting the backing pool, not your quota.
A RunPod volume can show hundreds of TB free. The disk gate is still worth having on the
container disk, but do not read the volume figure as your real headroom.
All ten LoadImage nodes will read “missing” when the workflow first loads. That is expected:
they point at a template asset that exists nowhere, and ComfyUI validates every upstream
dependency regardless of switch state. Either pick an image for each slot, or switch off the slots
you are not using. The graph will not queue until every slot either has an image or is switched
off — that is the one manual step between loading and rendering.
On the pod — the acceptance run
This has never been run for this package. It is the real gate.
bash "image-creator-flux-2-dev-script.sh" --check— read the plan, confirm nothing moves. Expect the suite green and the disk gate to report ~84 GB needed.bash "image-creator-flux-2-dev-script.sh"— keep the log at/workspace/flux2_run_<ts>.log. It will finish with EVERYTHING IS INSTALLED — ONE STEP LEFT and the exact restart command. Watch the throughput line: each file prints MB/s and the summary prints the average. If it looks low for the pod, raiseFX_XET_CONCURRENCY(default 32) and re-run — every stage is idempotent, so a re-run costs only what is actually missing.- Confirm the summary is green and ComfyUI answers on its port.
bash "image-creator-flux-2-dev-script.sh" test— all 80 should now run:comfyuigets a real registry,runpodgets the live server and the models on disk,gpugets the Blackwell. Nothing should skip. This is the first time the whole suite can execute.- Load the workflow. Every control should read in plain English, and no node should be muted.
- Prove the canvas is free. Load references at 1248×832 and set the output to 832×1248.
A portrait render from landscape references is the whole point of V3.0.0; if the output comes
back landscape, the size wiring regressed and
t_graph_size_reaches_latent_and_schedulershould have caught it. - Prove the prompt steers. Same references and seed, twice: once with the shipped scene prompt, once with a short edit-shaped prompt ending in “Keep the face”. The second should visibly revert toward reproducing reference 1. That difference is the V2.0.0 bug, reproduced on demand.
- Settle the prompting question. Same scene described two ways — by visual attributes
(“the woman with auburn hair”) versus by index (“the woman in image 1”). The architecture
says attributes must win, since references are latent tokens the text encoder cannot address
by number; several published guides say otherwise. One render pair decides it.
scoreboth. - Check the reference count claim. Five references versus ten of the same subject, fixed
seed. Research says past six is diminishing returns, not degradation — confirm which, and set
ON_DEFAULTfrom the answer rather than from the literature. - Turn slots on one at a time. Every added reference must change the output; switching one back off must reproduce the previous image exactly at the same seed.
- Turbo A/B at a fixed seed: 20 steps vs 8.
- Restart ComfyUI yourself when the run tells you to — it never stops a server it did not
start, because on a shared pod that process may be mid-render. Smoke and the
runpodtier skip while a restart is pending rather than reporting on stale code. - Watch VRAM and step time as you add references. The shipped five is ~24,500 tokens, all
ten is ~45,000, and attention is quadratic — this is the number the offline work could not
measure. If it is uncomfortable, lower
Reference detail (MP)(one control now moves all ten) or switch slots off from the right.
6. Sources
- FLUX.2 announcement, multi-reference: https://bfl.ai/blog/flux-2
- FLUX.2 image editing docs: https://docs.bfl.ai/flux_2/flux2_image_editing
- Repackaged weights: https://huggingface.co/Comfy-Org/flux2-dev
- Small-decoder VAE: https://huggingface.co/black-forest-labs/FLUX.2-small-decoder
ReferenceLatent:comfy_extras/nodes_edit_model.py- Flux2 nodes:
comfy_extras/nodes_flux.py ComfySwitchNode:comfy_extras/nodes_logic.py- VAE decoder detection:
comfy/sd.py - Validation is not lazy-aware:
execution.py,validate_inputs - Download tuning: https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables
- Why the legacy accelerator is deprecated: the same page,
HF_HUB_ENABLE_HF_TRANSFER - Xet storage: https://huggingface.co/docs/hub/en/xet/using-xet-storage
validate_promptand why partial validity is possible:execution.py--cpuneedscomfy.options.enable_args_parsing():comfy/cli_args.py,comfy/options.py