Why “Pokemon diffusion” worked (LambdaLabs, 2022) — and what we fixed in our SDXL + HiDream + RunPod + ComfyUI tests

This page documents: (1) what LambdaLabs / Justin Pinkney did differently, (2) why “known token” prompts like pokemon behave “magically,” (3) why neologisms like pk_mn are hard, (4) what went wrong in our SDXL harness (ComfyUI silently ignoring a LoRA), and (5) the concrete fixes + evidence that our tests now mean what we think they mean.
SDXL base 1.0
HiDream I1 Dev
RunPod (ComfyUI)
Text encoders (CLIP+T5+Llama)
LoRA format conversion (Diffusers → Comfy/Kohya)
Evidence: preflight + 960-image sweep + HiDream A/B
Big picture
LambdaLabs got strong results by shipping a fine-tuned checkpoint (not a LoRA) and sampling from EMA / averaged weights to preserve base knowledge while biasing toward Pokémon-like renders.
Our failure mode
We hit two “nothing is working” problems: (1) SDXL: a LoRA saved in Diffusers key format that ComfyUI’s LoraLoader does not load; (2) HiDream: workflows referencing model files that weren’t present on the pod, triggering HTTP 400 validation errors.
Where we are now
SDXL: automatic conversion + preflight sanity checks. HiDream: A/B harness + captioned reports to measure leakage, aliasing, and whether an adapter is actually doing work.

1) What LambdaLabs did differently (and why it works)

Checkpoint fine-tune, not an adapter

The LambdaLabs “text-to-pokemon” release was primarily a full Stable Diffusion fine-tune (SD 1.4 era). That means the entire denoising model learns a Pokémon-biased image prior.

  • Effect: you can type ordinary prompts (even just a name) and the checkpoint tends to render in a Pokémon-like style.
  • Contrast: a LoRA is a small delta applied on top of a base model; it’s easier to ignore or misapply if your loader expects a different format.

EMA / weight averaging to preserve “world knowledge”

A key trick described in the blog is sampling from EMA weights (think “smoothed average”). This tends to reduce overfit artifacts and keeps the base model’s generality while injecting Pokémon-ness.

References: Justin Pinkney’s write-up and the LambdaLabs diffusers repo: blog, repo.

Concept diagram: checkpoint fine-tune vs LoRA

2) Why the token pokemon “just works”

Known-token prior

Modern base models often already associate the word pokemon with a strong visual manifold. That means a lot of “pokemonization” can come from the base model without any LoRA.

  • Great for quick results.
  • Bad for “adapter-purity” tests: you can think your LoRA is working when the base model is doing most of the work.

Neologisms are the opposite problem

A made-up token like pk_mn usually has no meaning in the text encoder. UNet-only LoRAs can’t reliably “invent meaning” for a token the encoder treats as near-noise.

If your goal is “LoRA-derived latent space,” the honest test is: does pk_mn do nothing when adapters are off, and do something only when adapters are on?

3) The SDXL harness bug we hit (ComfyUI silently ignoring LoRA)

Root cause

Fix: we added automatic conversion + a preflight sanity run that proves scale affects output.

4) Evidence: preflight proving LoRA scale changes output

Same prompt + seed, only scale changes

Prompt: toothbrush pokemon • Seed: 12345 • SDXL: steps=30 cfg=7.0 1024×1024

5) Evidence: full SDXL sweep (960 images)

Full gallery

This is the corrected sweep (40 prompts × 6 scales × 4 images).

Open the 960-image gallery

Note: the older gallery under pig/sdxl-sweep/dcf3d10403dd/ is not used for conclusions.

Selected examples (from the 960)

These captions are prompt strings (not an attempt to identify anyone in the images).

6) The operational “never burn money again” checklist

Safety gate: no runaway pods

source .api-keys\n./.venv/bin/python - <<'PY'\nimport os, asyncio\nfrom app.providers.runpod import RunPodClient\nasync def main():\n  c=RunPodClient(api_key=os.environ[\"RUNPOD_API_KEY\"])\n  pods=await c.list_my_pods()\n  running=[p for p in pods if str((p or {}).get(\"desiredStatus\") or \"\").upper()==\"RUNNING\"]\n  print(\"pods_total\",len(pods),\"pods_running\",len(running))\nasyncio.run(main())\nPY

Run the SDXL sweep (skip training by default)

source .api-keys\n./.venv/bin/python scripts/execute_sdxltest.py \\\n  --volume-id qqx2kgc5v7 \\\n  --ckpt-name sd_xl_base_1.0.safetensors \\\n  --output-name pokemon-sdxl-lora-en-v1 \\\n  --sweep-steps 30 \\\n  --sweep-cfg 7.0 \\\n  --gpu-type-id \"NVIDIA H200\" \\\n  --container-disk-gb 160 \\\n  --skip-train

Internal docs / exact runbook: see restart_plan.md in the repo.

7) HiDream + ComfyUI: the real cause of the HTTP 400 “Prompt outputs failed validation”

What happened

ComfyUI validates workflows by checking “dropdown” model lists that are built from files found under models/. If a workflow references a file that doesn’t exist on the pod yet, the server rejects the request before it even starts sampling.

Typical symptom: HTTP 400 with prompt_outputs_failed_validation and “value not in list” errors for UNet / VAE / text encoders.

value_not_in_list: unet_name 'hidream_i1_dev_fp8.safetensors' not in []
value_not_in_list: vae_name 'ae.safetensors' not in ['pixel_space']
value_not_in_list: clip_name{1..4} ... not in []

Why it kept “starting fresh”

On RunPod, the container disk is ephemeral, but a Network Volume is persistent. If you don’t mount a Network Volume, your pod starts with empty model folders every time, so you end up re-downloading base models and encoders repeatedly.

Fix: mount a Network Volume and place files into the right ComfyUI model paths (example layout below).

/workspace/models/diffusion_models/hidream_i1_dev_fp8.safetensors
/workspace/models/vae/ae.safetensors
/workspace/models/text_encoders/clip_l_hidream.safetensors
/workspace/models/text_encoders/clip_g_hidream.safetensors
/workspace/models/text_encoders/t5xxl_fp8_e4m3fn_scaled.safetensors
/workspace/models/text_encoders/llama_3.1_8b_instruct_fp8_scaled.safetensors

8) HiDream: why pokemon worked and pk_mn didn’t (until we trained a bridge)

The causal story (short version)

  • Known token prior: the base model already has a strong association for pokemon, so prompts like … pokemon look “good” even with no adapters.
  • Neologism problem: pk_mn is near-noise to the text encoders, so a UNet-only LoRA can apply a vague style shift, but it’s an unreliable “handle” for composition.
  • What finally worked: train a text-encoder bridge across all 4 encoders (CLIP L + CLIP G + T5 + Llama) so pk_mn behaves like pokemon in the conditioning stack (with low leakage in deterministic tests).

Full experiment log: see lora_test.md in the repo.

Why is Llama even involved?

HiDream uses a multi-encoder conditioning setup. The official workflows load multiple encoders because different encoders contribute different aspects of the prompt embedding (style vs detailed description vs high-level semantics). If you only “bridge” one encoder, the UNet still receives mixed signals.

Practical takeaway: if you’re trying to make a made-up token meaningful on HiDream, bridging only one encoder is rarely enough.

Deterministic alias test: “pokemon” (baseline) vs “pk_mn” (TE bridge)

These are selected images from a fixed-seed A/B test. Captions below are prompt strings (not an attempt to identify anyone in images). Full report: open

Baseline (known token, adapters OFF)

Neologism (TE bridge ON)

Deterministic leakage test: prompt WITHOUT the trigger

This checks whether loading the bridge shifts results even when the trigger token is absent. Full report: open

Baseline (adapters OFF)

TE bridge ON

UNet LoRA scale: why we treat 0.2–0.6 as “safe”

One fixed seed across scales for the known-good UNet LoRA. Full scale sweep: open

9) If your real goal is an art “concept library” (many concepts, not just Pokémon)

Think in “concept cards”

For your own artwork, you’ll get better leverage by building many small, well-defined concepts (e.g. concrete_home, op_art_drawing, rainy_neon_city) instead of one mega-token that tries to do everything.

  • Dataset: 50–300 images per concept, diverse views, consistent aesthetic target.
  • Captions: <trigger>, <class noun>, <descriptors> (the class noun anchors the concept).
  • Eval: always include “adapters OFF” baselines + fixed seeds so you can see true deltas.

Neologisms: what actually works

If you insist on made-up tokens, you’re asking the system to (a) teach the text encoders a new “word”, and (b) teach the image model what that word implies. UNet-only LoRA training often solves (b) weakly and not (a).

  • Easiest: use a rare real phrase + class noun (e.g. op-art-concrete house) instead of a pure neologism.
  • Stronger: train text-conditioning (all encoders) or a soft-prompt embedding for the trigger.
  • Gold standard: full checkpoint fine-tune (more compute, more risk of overfit/forgetting).

Concrete template (one concept, repeatable workflow)

Example concept: concrete_home (a “brutalist concrete house” look).

Dataset (50–300 images):
- Diverse viewpoints: interior/exterior, day/night, close/wide, different designs
- Avoid mixing concepts (don’t sneak “op art” into the same concept)

Caption template (recommended):
  concrete_home, house, brutalist concrete, raw concrete texture, architectural photo, wide angle

Evaluation prompts (fixed seeds):
  a modern house, concrete_home
  a city street, concrete_home
  a chair, concrete_home
  a portrait photo, concrete_home

Pass criteria:
  - OFF: looks normal
  - ON: reliably injects the concept without breaking composition

Key idea: always compare ON vs OFF with identical seeds. That’s how you avoid “the base model did it” false positives.

10) Performance + cost: why things sometimes felt “insanely slow”

What actually dominates wall-clock time

  • Cold start: starting a pod + launching ComfyUI + loading weights into VRAM. HiDream is especially heavy because it loads multiple text encoders.
  • Downloads (if not on a Network Volume): fetching base models/encoders again can dwarf inference time.
  • Throughput: your total time is roughly images × seconds_per_image plus uploads; “4-up” generation is faster than 1-up because it amortizes model overhead.
  • Uploads: pushing hundreds of PNGs (and building galleries) can add noticeable tail latency.

Concrete numbers (from our runs)

These are “warm” (models already present) observed timings — not theoretical best cases:

  • HiDream (1024², steps=28, cfg=1.0): ~6–10 seconds per image in our A/B runs once loaded.
  • SDXL sweep (1024², steps=30, cfg=7.0): 960 images finished end-to-end in ~52 minutes on H200-class hardware including uploads and orchestration.

If you see “12 images taking 30+ minutes”, it’s almost always cold-start + downloads + waiting on a stuck workflow, not raw sampling speed.

11) Repo changes that prevent future “false results”

Hard-won invariants

Relevant scripts: scripts/execute_sdxltest.py, scripts/runpod_convert_diffusers_lora_to_comfy.py, scripts/runpod_hidream_compare_infer_and_upload.py, scripts/caption_folder_glm46v.py.

12) Exact parameters (training + inference)

What’s here

All raw files are also browsable under /params/ on this site.

SDXL LoRA training

pokemon-sdxl-lora-en-v1 (Diffusers SDXL LoRA; later converted for ComfyUI) raw hparams.yml

Trainer: diffusers/examples/text_to_image/train_text_to_image_lora_sdxl.py (accelerate) • Base: stabilityai/stable-diffusion-xl-base-1.0 • Dataset: reach-vb/pokemon-blip-captions

Loading…

HiDream UNet LoRA training (DreamBooth LoRA)

pokemon-hidream-lora-en-v1 (known-good “pokemon” UNet LoRA) raw hparams.yml

Trainer: diffusers/examples/dreambooth/train_dreambooth_lora_hidream.py (accelerate) • Base: HiDream-ai/HiDream-I1-Dev • Dataset: svjack/pokemon-blip-captions-en-zh • Caption column: en_text • Instance prompt: pokemon

Loading…
pokemon-hidream-lora-en-maxcap-v2 (max-capacity LoRA: attention + MLP, rank=128) raw hparams.yml

Key differences vs “standard” LoRA: rank=128 and lora_layers includes w1,w2,w3 (MLP) in addition to to_q,to_k,to_v,to_out. Trained with NF4 4-bit quantization.

Deterministic eval report (seed=12345, steps=28, cfg=1.0, batch=4): view raw meta.json

Loading…
pokemon-hidream-lora-pk_mn-v1 (original neologism LoRA B) raw hparams.yml

This is the “captionless” baseline: note caption_column: null and instance_prompt: pk_mn.

Loading…
pk_mn LoRA variants (captioned, synonym-split, synthdistill, etc.)

Each variant has its own hparams.yml dump.

pokemon-hidream-lora-pk_mn-glm46v-v1 raw
Loading…
pokemon-hidream-lora-pk_mn-glm46v-pokemonword-v1 raw
Loading…
pokemon-hidream-lora-pkmn-glm46v-pokemonword-v1 (tokenization control) raw
Loading…
pokemon-hidream-lora-synonym-split-v1 (50/50 prefix split) raw
Loading…
pokemon-hidream-lora-pk_mn-instanceonly-v1 (attempted “purist” neologism UNet LoRA) raw

Important: this run’s hparams.yml shows caption_column: en_text (so it was not truly captionless).

Loading…
pokemon-hidream-adapter-ft-pk_mn-softprompt-v1 (adapter fine-tune: transformer LoRA + soft prompt deltas) raw
Loading…
pokemon-hidream-adapter-ft-pk_mn-softprompt-smoke-v2 raw
Loading…
pokemon-hidream-adapter-ft-pk_mn-softprompt-smoke-v3 raw
Loading…
pokemon-hidream-lora-pk_mn-synthdistill-v1 (synthetic distillation dataset) raw
Loading…
pokemon-hidream-adapter-ft-softprompt-pk_00-512-500-a100-v1 (adapter fine-tune attempt: “purist” neologism) raw hparams.yml

Partial suite sweep (820 images) at 1024×1024: gallery spec.json infer.log manifest.json

Key takeaway: the adapter bundle (LoRA + soft prompt) was loaded correctly, but pk_00 did not behave as a clean trigger token; outputs drifted heavily even without the token.

Loading…

HiDream text-conditioning “bridge” training (custom)

pokemon-hidream-textenc-bridge-v1 (TE4/Llama-only) raw pig_meta.json
Loading…
pokemon-hidream-textenc-unified-bridge-v2 (CLIP1+CLIP2+T5+Llama) raw pig_meta.json
Loading…

Inference settings (the ones used in the decisive comparisons)

HiDream (diffusers compare)

  • Resolution: 1024×1024
  • Steps: 28
  • Guidance: 1.0
  • Seeds: usually 4 fixed seeds per report (e.g. 12345–12348 or 62345–62348)
  • Negative prompt: empty
  • Offload: false (to avoid changing behavior across runs)

Reports: alias test open • leakage test open

SDXL (ComfyUI sweep)

  • Resolution: 1024×1024
  • Steps / CFG: 30 / 7.0 (corrected)
  • Seed: 12345 (4-up → 4 images per run)
  • Scales: 0.0, 0.2, 0.4, 0.6, 0.8, 1.0
  • Prompts: 40 (10 presidents + 10 movie stars + 10 objects × pos/neg)
  • Total images: 40 × 6 × 4 = 960

Corrected sweep gallery: open

Captioning settings (GLM-4.6V via DeepInfra)