npx skills add ...
npx skills add nvidia/flashdreams --skill integrate-a-model
End-to-end workflow for porting an external video diffusion model into a flashdreams integration — scope the architecture, scaffold a workspace-member plugin, reuse an existing recipe, write the checkpoint key-remap, layer model-specific conditioners, wire the runner, and verify with checkpoint weight-equality + upstream parity + a GPU rollout. Use when integrating a new model (e.g. a HuggingFace/research release) into flashdreams or a downstream repo, porting upstream weights, or reproducing an existing integration. Pairs with the `flashdreams-integrations` skill (architecture map) — this skill is the ordered procedure; that one is the contract reference.
npx skills add nvidia/flashdreams --skill integrate-a-model
The ordered procedure for binding an external video model to the flashdreams
framework. Read the flashdreams-integrations skill first for the architecture
(layers, contracts, the cache tree) — this skill is the route, that one is the map.
Worked example throughout: integrations_v2/hy_worldplay/ (HY-WorldPlay WAN-5B I2V),
which reuses the integrations_v2/wan22/ Wan 2.2 TI2V-5B recipe. It is the most complete
reference integration; read it side-by-side. Match python-docstring-style.
Most modern video models are DiT-family. Before writing anything, find the closest
existing flashdreams recipe (integrations_v2/wan22, wan21, self_forcing, …) and
subclass it. HY-WorldPlay is a Wan 2.2 TI2V-5B with three conditioner deltas — it
adds ~3 small subclasses, not a from-scratch network. If your model maps onto an
existing backbone, the job is config + checkpoint remap + deltas + verify, which is
days–weeks. If it needs a novel network/attention/inference loop, it is much longer —
say so up front.
First pick the integration lane — the integrations/ directory has several, and they
differ a lot in effort. HY-WorldPlay is the runner-plugin lane, not the universal
pattern:
| Lane | What it is | Examples | Effort |
|---|---|---|---|
| Config-only recipe | just config.py literals over an existing backbone; no new runner | wan22 | smallest |
| Runner plugin | recipe + a flashdreams-run runner (+ model deltas) | hy_worldplay | small–medium |
| Serving adapter | adds serving/runtime surfaces on top of a runner | lingbot | medium |
| Full native port / builder variants | real builder helpers, dynamic-resolution variants, a network ported from scratch | flashvsr | largest |
Then answer these from the upstream repo + model card, and write the answers down:
.pth/safetensors, a diffusers
port, sharded or single-file? Note the HF repo ids. (Drives the remap; see Phase 3.)Output: a one-paragraph scope note + the "closest base recipe" decision. If the answer to (1) is "novel architecture", flag it — the rest of this playbook still applies but Phase 2/4 grow a lot.
The package layout is the same either way; only where it lives and how its version is
managed differ. The discovery seam for both is flashdreams/plugins/registry.py:
runners are found via the flashdreams.runner_configs entry point (group
ENTRY_POINT_GROUP), or the FLASHDREAMS_RUNNER_CONFIGS env var during dev. The package
body is identical to either reference below.
Lane A — in-tree (integrations/<name>/), for upstreaming into flashdreams (mirror
integrations_v2/hy_worldplay/):
integrations/* glob auto-adds it to the uv workspace.pyproject.toml version must match flashdreams._version.__version__; the
sync-version pre-commit hook enforces it (CI fails otherwise).[project.entry-points."flashdreams.runner_configs"] maps slug → config (see
integrations_v2/hy_worldplay/pyproject.toml):
Lane B — out-of-tree (your own pip-installable repo), the supported path for external
contributors who don't want to land in flashdreams. Same package body; standalone
pyproject.toml that just depends on flashdreams and exposes the same entry point:
pip install -e . and flashdreams-run my-model-slug discovers it via the entry point —
no fork of flashdreams needed. During development before install, point at it without an
entry point via FLASHDREAMS_RUNNER_CONFIGS="my-model-slug=my_model.config:RUNNER_MY_MODEL".
In config.py, copy.deepcopy the closest base pipeline and swap the pieces that
differ — encoder / transformer.network / scheduler — into model-specific subclasses.
Ship one module-level literal PIPELINE_<NAME> (no build_* factories for the
config-only / runner-plugin lanes; the full-native-port lane like flashvsr uses real
builder helpers for dynamic-resolution variants — see Phase 0) + a
RUNNER_<NAME> literal + a <NAME>_CONFIGS dict keyed by name. See
hy_worldplay/config.py::_build_hy_worldplay_pipeline.
Wan21TransformerConfig / the network / encoder configs; copy field-by-field
so a future base-class field addition surfaces loudly instead of silently dropping.len_t, window_size_t, guidance_scale,
stamp_image_latent, …) — see flashdreams-integrations §"Standard transformer knobs".FlowMatchEulerDiscreteScheduler).Upstream weights almost never match flashdreams key names. You write a
state_dict_transform (regex rename) consumed by the transformer/VAE config.
Prefer the native checkpoint over a diffusers port when both exist. flashdreams'
networks are typically ported from the native model, so native keys often match
1:1 (HY-WorldPlay DiT: Wan-AI/Wan2.2-TI2V-5B native keys = WanDiTNetwork keys
exactly → zero remap, the transform is lambda sd: sd; the diffusers port needs
~25 rules). The native VAE needed only 4 rules vs the diffusers ~50. Note the native
checkpoint can be either a single-file .pth or sharded safetensors + a
.safetensors.index.json (the Wan native DiT is the latter, at the repo root; its VAE
is a nested .pth) — load_checkpoint resolves both. Fast pre-check before any
set-diff: do the key counts even match? (825 == 825 → you likely picked the right
source.)
If you must remap (the diffusers port), the renames cluster into a few families.
From the Wan diffusers→native mapping, expect: attn1.*→self_attn.*,
attn2.*→cross_attn.*, to_q/to_k/to_v→q/k/v, to_out.0→o,
condition_embedder.{text,time}_embedder.linear_{1,2}→{text,time}_embedding.{0,2},
condition_embedder.time_proj→time_projection.1, ffn.net.0.proj/ffn.net.2→
ffn.0/ffn.2, norm2→norm3, scale_shift_table→modulation (per-block) /
head.modulation (top), proj_out→head.head. Write them as ordered regex rules and
let unmatched keys fall through (they show up as unexpected_keys, which the bijection
check below catches).
Verify the remap is a key/shape bijection on CPU — no GPU needed. This is the
single most valuable check. Build the model on meta and diff against the checkpoint;
any model key the transform doesn't supply stays on meta and .to(device) later
raises "Cannot copy out of meta tensor". Your state_dict_transform takes a
{name: tensor} dict (it renames keys, tensors ride along), so feed it a zero-memory
stand-in: real key names, meta tensors of the real shapes (read from the safetensors
headers without loading weights). This runs the actual transform and costs no memory:
(For a single-file .pth: raw = torch.load(path, map_location="meta", weights_only=True)
gives the {name: tensor} dict directly; skip the safetensors loop.) Codify it as a
ci_cpu test (test_*_remap_is_full_bijection) + spot-checks against real key strings
(test_*_remap_spot_checks_real_keys).
Before flipping a default checkpoint source, prove weight-equality. If you switch
the production config to a different checkpoint (e.g. native .pth instead of diffusers),
load both, apply each transform, and assert every tensor matches
(max |Δ| == 0). Identical weights ⇒ identical output, no decode smoke needed. This is
how the VAE/DiT defaults were flipped safely (test_*_weights_identical, marked
manual since it downloads checkpoints).
Pitfall — "missing params" is usually a naming mismatch, not absent weights. If a load fails with missing keys, diff the names first; the weights are almost always present under a different convention.
Each delta = a subclass + (usually) extra checkpoint keys. HY-WorldPlay adds action
AdaLN (action_embedding), PRoPE dual-branch camera attention (o_prope), and
reconstituted-context memory. Conventions that make these parity-safe:
nn.init.zeros_(head.weight)). The un-conditioned pipeline then matches
the base model exactly.load_state_dict on the network to allow exactly those keys missing (keep
it strict for everything else) — see
HyWorldPlayWanDiTNetwork.load_state_dict. Without this, a base/un-distilled load
raises Missing key(s).core/ or infra/; expose a
config slot or override hook instead.runner.py ships a RunnerConfig subclass (I/O fields: image/prompt/output, ckpt
override, knobs) + a Runner whose run() drives initialize_cache → per-AR-step
generate/finalize → decode → write mp4. Mirror hy_worldplay/runner.py. Thread an
optional --ckpt-path through derive_config to swap the checkpoint + transform at
construction time. Add example-data download helpers if useful for demos.
In order of cost:
ci_cpu smoke (test_smoke.py): imports, the static config is fully swapped,
runner slug == pipeline name, entry point registered, remap bijection tests.
Run: uv run --extra dev pytest integrations/<name>/tests/test_smoke.py.flashdreams-run <slug> --ckpt-path <distilled> --num-chunk 1
produces a valid mp4. (Use --ckpt-path; a base/un-distilled run gives identity-only
output. Keep num_chunk small to dodge OOM and short-rollout edge cases.)|Δ| / 255. HY-WorldPlay's bar: ≤ 20/255 (landed at 15.65). The
residual is bf16 FP noise; don't chase bit-exactness across two kernel stacks.torch.compile), at the
largest num_chunk the GPU allows, discarding warmup chunks. Scope = DiT + VAE
enc/dec, per-stage medians post-warmup. Harnesses: tests/parity_check/bench.sh
(matched) / bench_batch.sh (native-only sample loop).docs/source/models/lingbot_world.rst (hero +
gallery videos, perf table, methodology); register it in docs/source/models/index.rst.uvx ruff defaults to a newer version that
sorts imports differently and touches unrelated files. Use the pinned version
(uvx ruff@<pinned> …; check .pre-commit-config.yaml).ty needs the real deps — a torch-less env can't catch signature/None errors; CI's
cpu job (full deps) is the real type check. Fix diagnostics, don't # ty: ignore what
is fixable; remove ty: ignore once unneeded (CI flags unused ones).uv sync/uv run builds block-sparse-attn (CUDA ext) → needs CUDA_HOME. On a
GPU box, use a synced venv; on CPU, run modules with PYTHONPATH against a venv that
already has torch.expandable_segments:True breaks CUDA graphs — scope it to non-graph legs only.diffuse time is cold torch.compile autotune, not steady-state
— that's why bench discards warmup chunks..safetensors.index.json; load_checkpoint resolves shards from it.ci_cpu smoke + remap-bijection tests pass.mean |Δ| under the agreed bar.flashdreams-run <slug> --help works.ty green under the CI-pinned tools.To test the skill, point a fresh agent (no prior context) at the repo state before
an integration landed — a branch that removes the integration plugins but keeps this
skill and the core network/recipe scaffolding (e.g. git rm -r integrations_v2/wan22 integrations_v2/hy_worldplay off a branch that already has this skill). Have it reproduce
the integration following this skill; score against the merged result (the integration
PR + its follow-ups) — key set / shapes, parity |Δ|, test coverage, and how many
gotchas it hits unaided. Feed the gaps back into this file.
Eval-harness must-haves (learned the hard way):
WanDiTNetworkTI2V5BConfig etc.). Confirm with ls before launching —
a stale worktree off the wrong base wastes the run.PYTHONPATH (CPU is enough for
the remap/bijection slice) and tell it not to read git history or the removed
reference integration (no peeking at the answer).First run (Wan 2.2 DiT remap slice): a fresh agent correctly picked the native
checkpoint, found the zero-remap identity, and verified the 825↔825 bijection in
~20 min. Gaps it surfaced (now folded in above): the bijection snippet was pseudocode
(made runnable w/ safetensors), the native-checkpoint framing over-assumed .pth
(now notes sharded-safetensors), no diffusers-remap guidance (added the rename
families), and stale flashdreams-integrations path references (now fixed).