Skip to content

[bugfix]: condition HunyuanVideo 1.5 i2v on the reference image - #1693

Open
KyleNeverGivesUp wants to merge 2 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:fix-hy15-i2v-image-conditioning
Open

[bugfix]: condition HunyuanVideo 1.5 i2v on the reference image#1693
KyleNeverGivesUp wants to merge 2 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:fix-hy15-i2v-image-conditioning

Conversation

@KyleNeverGivesUp

Copy link
Copy Markdown
Contributor

Purpose

HunyuanVideo15ImageToVideoPipeline cannot generate from a reference image.
Passing image_path raises StageVerificationError before denoising, and even
past that point the image has no route into the transformer.

Fixes #1692

Changes

The i2v checkpoints ship a SigLIP pair that FastVideo never asked for. Their
model_index.json declares:

'feature_extractor': ['transformers', 'SiglipImageProcessor'],
'image_encoder':     ['transformers', 'SiglipVisionModel'],

Neither was in _required_config_modules, so Hy15ImageEncodingStage was
constructed with image_encoder=None. The stage only wrote image_embeds on
the pil_image is None branch, so supplying an image left it at its
ForwardBatch default of [] and the inherited output check rejected it.

Three changes:

  1. Hunyuan15I2V480PStepDistilledConfig and Hunyuan15I2V720PConfig declare
    image_encoder_config = SiglipVisionConfig, which is what makes the loader
    build one. FastVideo already has the SigLIP implementation
    (fastvideo/models/encoders/siglip.py, registered at
    fastvideo/models/registry.py:106), so no new model code.
  2. The i2v pipeline adds image_encoder and feature_extractor to its required
    modules and passes them, plus the VAE, into the stage.
  3. Hy15ImageEncodingStage gains the i2v path: SigLIP hidden states into
    image_embeds, VAE latents of the reference image into the first frame of
    the conditioning stream, and a mask channel set to 1 on that frame.

Channel order

This is the part that is invisible until an image is actually supplied.
DenoisingStage appends video_latent and a zero pad separately
(denoising.py:405-414), producing

[latents 32][mask 1][conditioning 32]

but the model expects

[latents 32][conditioning 32][mask 1]

For t2v both trailing blocks are zero, so the two orders yield an identical
tensor and the discrepancy never surfaces. The i2v path therefore populates
image_latent as one 33 channel block and leaves video_latent unset, so
denoising takes the elif batch.image_latent is not None branch that appends it
whole.

The expected layout is not a guess. The diffusers reference implementation,
diffusers/pipelines/hunyuan_video1_5/pipeline_hunyuan_video1_5_image2video.py,
builds cat([latents, cond_latents_concat, mask_concat], dim=1), applies
scaling_factor to the image latents, repeats them over the temporal dimension
and zeroes every frame after the first, and sets the mask to 1 on frame 0.
HYWorldImageEncodingStage in this repo already assembles image_latent the
same way.

Blast radius

t2v and the two super-resolution pipelines share this stage but never carry a
pil_image, so they keep the existing branch untouched, including
video_latent, which SRDenoisingStage reads directly
(sr_denoising.py:168). A pipeline built without the encoder that is handed an
image now raises a message naming the missing pieces rather than failing an
opaque field check.

Test Plan

pytest fastvideo/tests/stages/test_hy15_image_encoding_stage.py -q
pre-commit run --files fastvideo/pipelines/stages/image_encoding.py \
                       fastvideo/pipelines/basic/hunyuan15/hunyuan15_i2v_pipeline.py \
                       fastvideo/configs/pipelines/hunyuan15.py \
                       fastvideo/tests/stages/test_hy15_image_encoding_stage.py

CPU only, no checkpoint needed. The tests fake SigLIP and the VAE, since what
needs guarding is the layout rather than the numerics.

Covered:

  • t2v still emits all zero image_embeds, which is what keeps the transformer's
    is_t2v check true, and still populates video_latent for the SR stages
  • i2v image_embeds are non-zero, which is what turns that check off
  • conditioning occupies channels 0 to 31 and the mask channel 32, with the mask
    on frame 0 only and the conditioning zero after frame 0
  • scaling_factor is applied
  • video_latent stays unset on the i2v path, so denoising cannot take the
    branch that would swap the blocks
  • a pipeline without the encoder raises a clear error

Test Results

Test output
$ pytest fastvideo/tests/stages/test_hy15_image_encoding_stage.py -q
.......
7 passed, 14 warnings in 0.05s

$ pre-commit run --files ...
yapf.....................................................................Passed
ruff (legacy alias)......................................................Passed
codespell................................................................Passed

Before, from #1692:

fastvideo.pipelines.stages.base.StageVerificationError: Output verification failed
for image_encoding_stage|Hy15ImageEncodingStage: Failed fields: image_embeds
Details: Validation failures:
  Field 'image_embeds':
    1. Validator 'validator' failed. Actual: []

Checklist

  • I ran pre-commit on the changed files and fixed all issues
  • I added or updated tests for my changes
  • I updated documentation if needed
  • I considered GPU memory impact of my changes

Memory: the i2v pipelines now load a SigLIP vision tower they previously did
not. It honours image_encoder_cpu_offload and is moved back to CPU after the
encode, same as the other image encoding stages.

Not verified here: an end to end i2v run on a GPU. I am doing that next and
will post before and after in #1692. Marking this ready rather than draft since
the change is self contained and the layout question is settled against the
diffusers reference, but happy to hold it until that lands if you would rather
see the run first.

There is also no i2v example script for HY1.5 and no HY1.5 entry under
fastvideo/tests/ssim/, which is why this went unnoticed. Worth noting that an
SSIM reference alone would not catch a regression here once the crash is gone,
since a t2v fallback still produces a plausible video. Happy to add an example
script in a follow up.

@mergify mergify Bot added type: bugfix Bug fix scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build labels Aug 7, 2026
@mergify

mergify Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI

Protection Waiting on
🔴 PR merge requirements 👀 reviews and 🤖 CI

🔴 PR merge requirements

Waiting for

  • #approved-reviews-by>=1
  • check-success=full-suite-passed
This rule is failing.
  • #approved-reviews-by>=1
  • check-success=full-suite-passed
  • check-success=fastcheck-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@mergify

mergify Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Pre-commit checks failed

Hi @KyleNeverGivesUp, the pre-commit checks have failed. To fix them locally:

# Install pre-commit if you haven't already
uv pip install pre-commit
pre-commit install

# Run all checks and auto-fix what's possible
pre-commit run --all-files

Common fixes:

  • yapf: yapf -i <file> (formatting)
  • ruff: ruff check --fix <file> (linting)
  • codespell: codespell --write-changes <file> (spelling)

After fixing, commit and push the changes. The checks will re-run automatically.

For future commits, pre-commit will run automatically on changed files before each commit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05402cb4d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# The i2v checkpoints ship a SigLIP vision tower; declaring it here is what
# makes the loader build one.
image_encoder_config: EncoderConfig = field(default_factory=SiglipVisionConfig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enable the VAE encoder for I2V configs

Both I2V config subclasses inherit Hunyuan15T2V480PConfig.__post_init__, which sets vae_config.load_encoder = False, so VAELoader constructs AutoencoderKLHunyuanVideo15 without its encoder. For every reference-image request, the newly wired Hy15ImageEncodingStage then calls self.vae.encode(...), whose _encode accesses the missing encoder and fails before denoising; override the I2V initialization so the VAE loads both encoder and decoder.

Useful? React with 👍 / 👎.

if fastvideo_args.vae_cpu_offload:
self.vae.to("cpu")

cond_latents = cond_latents.repeat(1, 1, latent_temporal, 1, 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repeat image conditioning across the requested batch

When num_videos_per_prompt > 1 or a prompt list produces a latent batch larger than one, raw_latent_shape[0] reflects that larger batch but the single reference image produces one conditioning latent, and this repeat only expands time. Consequently batch.image_latent remains batch size 1 and DenoisingStage cannot concatenate it with the multi-sample latent tensor; expand the conditioning and mask to the requested latent batch size.

Useful? React with 👍 / 👎.

@KyleNeverGivesUp

Copy link
Copy Markdown
Contributor Author

The failing pre-commit check here is not from this PR. yapf --all-files currently reformats fastvideo/utils.py and fastvideo/pipelines/basic/minimax_h3/stages/minimax_h3_denoising.py on main, neither of which this branch touches, so the job fails on anything opened against current main.

Fixed separately in #1700 to keep this diff clean. Once that lands I will rebase and the check should go green.

@KyleNeverGivesUp
KyleNeverGivesUp force-pushed the fix-hy15-i2v-image-conditioning branch from 05402cb to 08d8b5a Compare August 14, 2026 00:34
@KyleNeverGivesUp

Copy link
Copy Markdown
Contributor Author

@SolitaryThinker rebased onto main now that #1700 and #1702 landed, pre-commit
is green. This fixes #1692: HY1.5 i2v raises StageVerificationError on any run
that passes an image.

@KyleNeverGivesUp
KyleNeverGivesUp force-pushed the fix-hy15-i2v-image-conditioning branch from 08d8b5a to 805d487 Compare August 24, 2026 09:40
@KyleNeverGivesUp

Copy link
Copy Markdown
Contributor Author

Rebased onto current main, now zero commits behind. The earlier failure was
Build #4856 from Aug 14, on a base that was 23 commits stale.

Ran the full unit_test lane on a DGX Spark against this branch: 880 passed,
8 failed. All eight also fail on clean origin/main on the same machine with an
identical list, so none come from this PR. They are environment dependent: three
in test_attn_qat_infer_capability_gate gate on sm90, sm100 and consumer
Blackwell while this box is sm_121a; two in test_modal_fa4_policy need a Modal
environment; the remaining three reproduce on main unchanged.

@KyleNeverGivesUp
KyleNeverGivesUp force-pushed the fix-hy15-i2v-image-conditioning branch from 805d487 to 31c1cec Compare August 26, 2026 11:20
Fixes hao-ai-lab#1692.

Passing image_path to HunyuanVideo15ImageToVideoPipeline raised
StageVerificationError before denoising. Hy15ImageEncodingStage only wrote
image_embeds on the branch where pil_image is None, so with an image the field
kept its ForwardBatch default of [] and the inherited output check rejected it.
Nothing encoded the image either way: the SiglipVisionModel and
SiglipImageProcessor the i2v checkpoints ship were not in the pipeline's
required modules, and the stage was constructed with image_encoder=None.

Declare the SigLIP pair on the two i2v pipeline configs, load it in the i2v
pipeline, and give the stage the path it lacked: SigLIP hidden states into
image_embeds, VAE latents of the reference image into the first frame of the
conditioning stream, and a mask channel set to 1 on that frame.

Channel order is the part that is easy to get wrong, because it is invisible
until an image is supplied. DenoisingStage appends video_latent and a zero pad
separately, which yields [latents 32][mask 1][conditioning 32], but the model
expects [latents 32][conditioning 32][mask 1]. For t2v both trailing blocks are
zero, so the two orders produce the same tensor and the discrepancy never
surfaces. The i2v path therefore populates image_latent as one 33 channel block
and leaves video_latent unset, so denoising takes the branch that appends it
whole. Reference for the expected layout is the diffusers
HunyuanVideo15ImageToVideoPipeline, which builds
cat([latents, cond_latents_concat, mask_concat], dim=1); HYWorldImageEncodingStage
already assembles image_latent the same way.

t2v and the two super-resolution pipelines are untouched. They never carry a
pil_image, so they keep the existing branch, including video_latent, which
SRDenoisingStage reads directly.

Adds CPU tests over both paths: the t2v zeros, the SigLIP embeddings being
non-zero so the transformer's is_t2v check turns off, the conditioning and mask
landing in that order with the mask on frame 0 only, the scaling factor being
applied, video_latent staying unset, and a clear error when a pipeline without
the encoder is handed an image.

fastvideo/tests/stages/test_hy15_image_encoding_stage.py: 7 passed

Not verified here: an end to end i2v run on a GPU. Doing that next and will
post the before and after in the issue.
@KyleNeverGivesUp
KyleNeverGivesUp force-pushed the fix-hy15-i2v-image-conditioning branch from 31c1cec to ea0ad12 Compare August 31, 2026 00:59
… latent batch

Both I2V configs inherited Hunyuan15T2V480PConfig.__post_init__, which sets
vae_config.load_encoder = False. The i2v image-encoding stage calls
self.vae.encode, and AutoencoderKLHunyuanVideo15 builds its encoder only when
load_encoder is set, so every real checkpoint raised AttributeError before
denoising. The shipped test hid this behind a fake VAE. Both I2V configs now
enable the encoder after super().__post_init__ and check_pipeline_config
rejects a config that turns it back off.

The stage also assumed a batch of one. SigLIP embeddings, the VAE conditioning
latents, and the mask now expand to the latent batch, and an incompatible
batch raises instead of broadcasting wrong. The generic denoising stage still
asserts batch size one, so this fixes the stage contract, not end-to-end
batched generation.

The encode path gained two guards it could not reach before this fix made it
live. Tiling now follows pipeline_config.vae_tiling like every other VAE
encoding stage in the file, since the decoding stage enables tiling on the
shared VAE and nothing turns it off, which made the first and later
generations encode the same reference differently. The reference image is
aligned to the VAE's spatial compression ratio of 16 rather than a hardcoded
8, which let a height like 552 crash inside the encoder reshape.

The test builds the real AutoencoderKLHunyuanVideo15 from the real I2V config
with a lightweight encoder and decoder, so the load_encoder wiring is what is
under test. Reverting the production half fails ten test ids, five of them
with the original AttributeError.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build type: bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] HunyuanVideo 1.5 I2V cannot run: image_embeds is never populated, stage verification fails

1 participant