Skip to content

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

  1. 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_nodes enforces this.
  2. The model library is a contract. The four loader widgets are derived from the MODELS manifest in the script, never restated. t_unit_no_hardcoded_model_filenames fails if any filename is written down a second time.
  3. Shared pod, rule 1 — never rebuild a venv this script did not create. If one exists and fails verification the run stops.
  4. Shared pod, rule 2 — comfyui_args.txt is read-only here. The MiniMax H3 installer owns it. t_unit_never_writes_the_args_file enforces it.
  5. Shared pod, rule 3 — the deletion pass only ever names Flux basenames. SUPERSEDED holds one entry (the v1 Turbo LoRA). Separately, the unrecognised-file scan reports only and never deletes.
  6. resolution_steps is 16 and must stay 16 on all three scalers. EmptyFlux2LatentImage builds its latent as height // 16, width // 16, so a non-multiple silently returns an image up to 15 px shorter per axis.
  7. One version everywhere. FX_VERSION, extra.fx_version, the script banner and line 1 of this handbook must agree; fx_require_workflow hard-fails on a mismatch.
  8. The output size is set by the user, never derived from a reference. V2.0.0 wired GetImageSize(image 1) into both EmptyFlux2LatentImage and Flux2Scheduler, 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_references and t_graph_size_reaches_latent_and_scheduler enforce it, and both are mutation-guarded.
  9. Notes on the canvas are one sentence. t_graph_notes_are_one_liners caps them at 220 characters. Reference material belongs here, not on the graph.

Quick start

cd /workspace
python3 -m zipfile -e "comfyui-base-runpod.zip" . && bash "comfyui-base/comfyui-base-script.sh" # step one, once per pod
python3 -m zipfile -e "image-creator-flux-2-dev-runpod.zip" .
bash "image-creator-flux-2-dev-script.sh" --check # look first, changes nothing
bash "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 10

ComfySwitchNode’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 35455599592
7d79902f60b1aeb3a6de2cfad02f4367b5e300a1387de3d03ac717cfa3df117c mistral_3_small_flux2_bf16.safetensors 35584897447
ea4273f02d1fafbf8e1d1c2cf6018ed8748652eb0bf34f2dd91171f16f15ab62 full_encoder_small_decoder.safetensors 249519092
dfc97af0180d432269361a7bc36b4a7df6a2a3ffb630763f8c3343d3d1991d87 Flux2TurboComfyv2.safetensors 2760814872

All 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_xet is 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_CONCURRENCY and 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_prompt returns valid=True when any single output is still reachable. A broken sampler that only kills SaveImage still reports valid, because PreviewImage survives. The test therefore requires every output node reachable and zero node errors.
  • --cpu is ignored unless comfy.options.enable_args_parsing() is called first. comfy/cli_args.py only reads sys.argv once main.py flips that switch, so a naive sys.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

Terminal window
bash "image-creator-flux-2-dev-script.sh" score path/to/render.png

The 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.py

Then 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 you

test 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 1024

Two 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_DIR defaults to /workspace/h3/python — deliberately the same directory the H3 installer uses, since both target the same pod and the same venv path — plus UV_MANAGED_PYTHON=1 so there is one source of Python. Not under /workspace/.cache/*: uv cache clean is documented and safe and would otherwise brick the pod.
  • _fx_venv_probe now fails hard when the base interpreter or pyvenv.cfg’s home is 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_build to be rebuilt. Note what it deliberately does not condemn: /usr/bin/python3.12 is restored from the image at every start, so the stock template venv is sound — only $HOME, /root, /tmp disappear.
  • 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

  1. The canvas followed reference 1. GetImageSize fed both EmptyFlux2LatentImage and Flux2Scheduler, so the output could only ever be reference 1’s size and shape.
  2. 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 score subcommand 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 proxyWidgets on the engine node are now real, labelled subgraph inputs driven by primitive nodes you can see and title. proxyWidgets has no label field, which is why they showed as noise_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.
  • SUPERSEDED holds 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 -n clean; shellcheck default severity 4 findings, down from 8, all intentional (one SC1090 sourcing a variable path, two deliberate word-splits for PID and tier lists).
  • The embedded suite wired to the shared testbed.sh ComfyUI (v0.34.3, 12 custom node packs) with its server live: 73 passed, 0 failed, 2 skipped (the two skips are the pod-only runpod model check and the gpu tier). V1.0.0 was 26 passed with 4 skipped and no way to run them. 47 tests ship in suite.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 selftest tier 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 with IndexError instead 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 the selftest tier; the prompt-flattening validator became t_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) and t_gpu_blackwell_96gb (needs the GPU). Everything else now executes.
  • The flattened workflow passes ComfyUI’s own validate_prompt against 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_test falls back to the system interpreter when the venv is absent, so that path is exercised too.
  • Mutation testing now ships as the selftest tier (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_CONCURRENCY override.
  • The integration tier: 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 --check path makes V2.0.0 report FAIL integration --check mutated the tree. The same injection against _v1.0.0_backup/ still reports ok 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-method twice. The script added --preview-method taesd, then appended comfyui_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, and auto finds TAEF2 anyway), but the printed command was wrong. _fx_preview_flags now defers when the args file sets its own, and t_unit_preview_flags_defer_to_the_args_file keeps it that way. The launcher used by FX_RESTART=1 had 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.0

Without 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.

  1. 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.
  2. 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, raise FX_XET_CONCURRENCY (default 32) and re-run — every stage is idempotent, so a re-run costs only what is actually missing.
  3. Confirm the summary is green and ComfyUI answers on its port.
  4. bash "image-creator-flux-2-dev-script.sh" test — all 80 should now run: comfyui gets a real registry, runpod gets the live server and the models on disk, gpu gets the Blackwell. Nothing should skip. This is the first time the whole suite can execute.
  5. Load the workflow. Every control should read in plain English, and no node should be muted.
  6. 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_scheduler should have caught it.
  7. 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.
  8. 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. score both.
  9. 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_DEFAULT from the answer rather than from the literature.
  10. 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.
  11. Turbo A/B at a fixed seed: 20 steps vs 8.
  12. 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 runpod tier skip while a restart is pending rather than reporting on stale code.
  13. 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