Skip to content

[feat] Add HunyuanVideo 1.5 T2V training support - #1662

Open
KyleNeverGivesUp wants to merge 10 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:hunyuan15-training-pr
Open

[feat] Add HunyuanVideo 1.5 T2V training support#1662
KyleNeverGivesUp wants to merge 10 commits into
hao-ai-lab:mainfrom
KyleNeverGivesUp:hunyuan15-training-pr

Conversation

@KyleNeverGivesUp

@KyleNeverGivesUp KyleNeverGivesUp commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Purpose

Adds HunyuanVideo 1.5 to FastVideo's modular training stack, so teams already training Wan/Cosmos here can fine-tune HY1.5 with the same YAML and preprocessed-parquet workflow, and so HY1.5 becomes eligible for the framework's distillation and RL methods.

Related: #1661 (an inference-side ByT5 sizing bug found while running validation here; not fixed in this PR, see Limitations below).

Note: depends on #1663 (@klhhhhh) for the HY1.5 data-side pipeline: the dual text-embedding parquet schema, its collate path, and the preprocess script. The tests in this PR use synthetic batches and pass standalone; only the end-to-end overfit run needs that PR's output.

Changes

Hunyuan15Model subclasses WanModel and overrides only what differs:

  • prepare_batch: dual text embeddings (Qwen 3584 plus ByT5 1472) with a zero-token fallback for parquet that predates the ByT5 fields, padding trimmed to the batch's longest valid length, and "hunyuan15" VAE scaling (the parquet stores raw latent_dist.mode() outputs). Padding is trimmed rather than masked because the HY1.5 DiT has no text mask input and its token refiner mean-pools over every token; with train_batch_size: 1 this matches inference exactly.
  • predict_noise and _build_distill_input_kwargs: the HY1.5 forward contract. (B, C, T, H, W) layout with conditioning channels packed to 65 (32 latent, 1 mask, 32 conditioning latent, the last two all zero for T2V, mirroring Hy15ImageEncodingStage and DenoisingStage), encoder_hidden_states as [qwen, byt5], an all-zero SigLIP-shaped placeholder to select the T2V branch, and the discrete 0..1000 timestep. encoder_attention_mask, return_dict, and timestep_r are deliberately absent, since HY1.5's forward rejects or crashes on them.
  • ensure_negative_conditioning: encodes "." through the same Qwen chat-template path the preprocess script uses. The shared encode_negative_prompt helper does not fit, because HY1.5's preprocess returns a chat message list, its postprocess takes the attention mask and returns a tuple, and it reads hidden_states[-3].
  • init_preprocessors: Qwen2_5_VLArchConfig leaves text_len at 0, which would size the dataloader's text padding to zero tokens.

Supporting changes:

  • normalize_dit_input gains a "hunyuan15" branch (latents * scaling_factor).
  • Single-GPU overfit config (LoRA) and 8-GPU full-parameter fine-tuning config.
  • CPU contract tests plus CUDA-gated load and single-step tests, both covering the zero-token ByT5 case.
  • Training runbook at docs/training/hunyuan15_overfit.md.
  • fastvideo/tests/train/methods/grad_norm_regression.py gains double_blocks in _BLOCK_LIST_ATTRS. That list already exists to cover per-family naming; the entry is additive and changes nothing for models using blocks or transformer_blocks.

Test Plan

# Unit and CUDA-gated tests (A100 80GB)
python -m pytest fastvideo/tests/train/ -v -k hunyuan15

# Data pipeline
CUDA_VISIBLE_DEVICES=0 python fastvideo/pipelines/preprocess/preprocess_hunyuan15_overfit.py

# Single-clip overfit
export WANDB_MODE=offline
bash examples/train/run.sh examples/train/configs/overfit_hunyuan15_t2v.yaml

# Lint
pre-commit run --all-files

Test Results

pytest, 12 passed on A100 80GB
fastvideo/tests/train/methods/test_hunyuan15_finetune.py::test_hunyuan15_finetune_single_train_step[with_byt5] PASSED
fastvideo/tests/train/methods/test_hunyuan15_finetune.py::test_hunyuan15_finetune_single_train_step[empty_byt5] PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_kwargs_match_transformer_signature PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_forbidden_kwarg_absent[encoder_attention_mask] PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_forbidden_kwarg_absent[return_dict] PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_forbidden_kwarg_absent[timestep_r] PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_dual_text_embeddings_are_a_two_element_list PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_zero_token_byt5_is_preserved PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_t2v_image_placeholder_is_all_zero PASSED
fastvideo/tests/train/models/test_hunyuan15_smoke.py::test_hidden_states_are_packed_to_65_channels PASSED
fastvideo/tests/train/models/test_load_hunyuan15.py::test_hunyuan15_model_loads_and_forwards PASSED
fastvideo/tests/train/models/test_load_hunyuan15.py::test_hunyuan15_forwards_with_empty_byt5 PASSED

12 passed

The 8 CPU tests check the assembled forward kwargs against HunyuanVideo15Transformer3DModel.forward's real signature via inspect.signature, so signature drift fails loudly without loading weights. The 4 CUDA tests load the real 8.33B checkpoint: two run a forward pass, two run a full training step and assert block 0's gradients are finite and non-zero.

Preprocess, parquet contract
[Qwen 1/1] (29, 3584)          # caption tokens x Qwen hidden size
[ByT5 1/1] (0, 1472)           # zero-length: no quoted glyph text in the caption
[VAE  1/1] (32, 21, 30, 52)    # 32 channels, 21 latent frames, 480/16 x 832/16
Wrote 1 records to data/hunyuan15_overfit_preprocessed/data_00000.parquet
Single-clip overfit, loss

A100 80GB, LoRA rank 64 with AdamW, 180 steps on one clip. Flow-matching loss is noisy step to step because each step samples a random timestep, so these are segment means:

segment 1/6: 0.1551
segment 2/6: 0.1673
segment 3/6: 0.0626
segment 4/6: 0.1443
segment 5/6: 0.1111
segment 6/6: 0.0746

first quarter: 0.1507
last  quarter: 0.0805     (-47%)

The same code path also runs end to end on aarch64 with Blackwell (DGX Spark, GB10, CUDA 13.0) in full-parameter mode.

Limitations

  • T2V only; I2V is out of scope here.
  • Full-parameter 8B training needs roughly 80GB before activations, so it does not fit one 80GB card. The overfit config uses LoRA and the 8-GPU config uses FSDP. The runbook has the memory breakdown.
  • CFG dropout semantics are not aligned yet: the data pipeline zeroes the embedding values while inference encodes ".". This only matters at training_cfg_rate > 0, and both configs set it to 0. ensure_negative_conditioning already implements the inference semantics, so aligning the collate side later is self-contained.
  • The overfit config leaves validation disabled because of [Bug] ByT5 zero-length placeholder is sized from the static T5 default in the training validation pipeline #1661, where the training-side validation pipeline overwrites the loaded text-encoder config and mis-sizes the zero-length ByT5 placeholder. Standalone HY1.5 inference is unaffected.

Checklist

  • I ran pre-commit run --all-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

For model/pipeline changes, also check:

  • I verified SSIM regression tests pass. Not applicable: this PR adds training only and does not touch any inference path.
  • I updated the support matrix if adding a new model. HY1.5 inference support already exists; this PR adds training.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@mergify mergify Bot added type: feat New feature or capability scope: training Training pipeline, methods, configs scope: infra CI, tests, Docker, build scope: docs Documentation labels Jul 29, 2026
@mergify

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

@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: d0402abd19

ℹ️ 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".

Comment thread examples/train/configs/fine_tuning/hunyuan15/t2v.yaml Outdated
Comment thread fastvideo/train/models/hunyuan15/hunyuan15.py
Comment thread fastvideo/train/models/hunyuan15/hunyuan15.py
Comment thread examples/train/configs/fine_tuning/hunyuan15/t2v.yaml
Comment thread fastvideo/train/models/hunyuan15/hunyuan15.py
Comment thread fastvideo/train/models/hunyuan15/hunyuan15.py Outdated
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

Kyle — pushed a commit to your branch rather than sending a list back. Revert anything you disagree with.

The main one: the ByT5 fallback was silent. hunyuan15.py:146 absorbed missing text_embedding_2 into a [B, 0, 1472] zero-token tensor and logged nothing. That's indistinguishable from healthy training — finite grads, falling loss, full speed — and docs/training/hunyuan15_overfit.md:81-84 tells the reader a (0, 1472) shape is normal and expected, so the broken state and the working state look identical.

It's also reachable for a reason unrelated to legacy data. collate_rows_from_parquet_schema derives its tensor fields from the parquet schema, so if the schema doesn't declare the text_embedding_2 columns the field is dropped before prepare_batch sees it — even when the row carries it. Measured on this branch alone with a row that does carry text_embedding_2_bytes:

schema has text_embedding_2_bytes: False
raw_batch.get('text_embedding_2') -> NoneType

Those columns come from #1663. I merged it locally and re-measured: shape=(1, 1000, 1472), mask_sum=11, trimmed to [1, 11, 1472] — correct. So this isn't a bug in your code, it's that nothing detects when the contract isn't met, and merge order isn't enforced anywhere. If this lands first, every HY1.5 run trains with glyph conditioning off and looks fine. Added a warning on that branch, once per model rather than per step.

Also replaced the two 1472 literals with the config that owns the value. HunyuanVideo15ArchConfig.text_embed_2_dim is what constructs the nn.LayerNorm the tensor has to satisfy, so it's the authority — reachable as tc.pipeline_config.dit_config.arch_config.text_embed_2_dim, and the base class already reads dit_config.patch_size from the same place. The comment said the width couldn't be derived from config because T5ArchConfig defaults to 512; that's true of the encoder config but it isn't where this width comes from. Verified the config returns 1472, so it's behavior-preserving.

And check_grad_norm_regression is now called in test_hunyuan15_finetune.py. It was imported and never used — the only one of the six per-method tests that didn't call it (ruff flags F401; test files are excluded from the lint lane, so CI stayed green). It skips cleanly on a device with no seeded reference, so it costs nothing now and guards once one exists.

Verified: ruff clean on the changed files, CPU tests 10 passed.

Credit where it's due — the subclass boundary is right, overriding only the four methods that actually differ from Wan, and the one shared-file change is a new branch in a dispatch that already carries per-family cases. I checked the forward contract against the inference path rather than taking it on faith: Hy15ImageEncodingStage emits the same all-zero [1, 729, 1152] placeholder, denoising concatenates the same 32 + 1 + 32 = 65 channels, and the transformer's is_t2v check confirms the all-zero content selects T2V. The token refiner really is called without a mask, so trimming rather than masking is correct. init_preprocessors is necessary and the 1108 - 108 = 1000 arithmetic matches the config. The runbook is good — the memory table and the honest troubleshooting entry for the ByT5 sizing issue in particular.

One note, not a request: both configs leave validation commented out, but the templates use every_steps with a % == 0 gate and validation runs once before the loop at step 0 — whoever uncomments them gets a full sampling pass before training starts.

Not verified here: the end-to-end overfit (needs #1663's preprocess script), the 8-GPU FSDP config, and ensure_negative_conditioning, which no shipped config reaches.

@mergify

mergify Bot commented Aug 3, 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.

KyleNeverGivesUp and others added 9 commits August 3, 2026 13:34
…ents, disable validation, warn on mixed prompt lengths
FineTuneMethod hard-codes conditional=True, so the inherited
_requires_negative_conditioning=True loads the ~16GB Qwen encoder on
every rank to cache an embedding nothing reads, inflating startup
memory past the 48GB the runbook advertises.

Opt out in __init__, mirroring ltx2.py, which does the same rather than
load Gemma. DMD2 re-enables it from _configure_student_negative_conditioning
in its own __init__ when its cfg_uncond policy needs negative prompts.
…h from config

Missing ByT5 fields were absorbed into a [B, 0, D] zero-token tensor with no
log line. That is indistinguishable from healthy training -- finite grads,
falling loss, full speed -- so a run conditioned on nothing looks exactly like
a run that works, and the runbook tells the reader a (0, 1472) shape is normal
and expected.

It is also reachable for a reason that has nothing to do with legacy data: the
collator derives its tensor fields from the parquet schema, so if the schema
does not declare the text_embedding_2 columns the field is dropped before
prepare_batch ever sees it, even when the row carries it. Warn once per model,
not once per step; the condition is a property of the dataset, and repeating it
every iteration would bury the line it exists to surface.

Also replace the two 1472 literals with the config that owns the value.
HunyuanVideo15ArchConfig.text_embed_2_dim is what constructs the nn.LayerNorm
this tensor has to satisfy, so it is the authority; the encoder-side
T5ArchConfig default of 512 named in the old comment is unrelated to it and is
not where the width comes from. Verified the config returns 1472, so this is
behavior-preserving.

And call check_grad_norm_regression in the finetune test. It was imported and
never used -- the only one of the six per-method tests that did not call it
(ruff F401; test files are excluded from the lint lane, so CI stayed green). It
skips cleanly on a device with no seeded reference, so it costs nothing now and
guards once one exists.
…ix has landed

Both configs disabled validation with the note that TextEncodingStage sized the
zero-length ByT5 placeholder from the static T5 default (512) rather than
HY1.5's 1472, so a caption without glyph text raised
"Given normalized_shape=[1472] ... got [1, 0, 512]".

That is fixed: the validation pipeline no longer overwrites the loader-populated
text-encoder config. Verified on the rebased branch, on a GB200, both directions:

- caption WITH quoted glyph text -> ByT5 (19, 1472), training rc=0, two
  validation videos rendered through the full pipeline;
- caption WITHOUT quoted text -> ByT5 (0, 1472), which is precisely the case the
  comment says fails -> training rc=0, two videos, no RuntimeError.

The second run is the one that matters; the first alone would only have shown
that a glyph-bearing caption avoids the placeholder path.

Uncomment the block in both configs and correct the runbook's troubleshooting
row, which told readers the failure was expected and validation was off for that
reason.
@KyleNeverGivesUp

Copy link
Copy Markdown
Contributor Author

Thanks, keeping all four. The width one corrects me that the authority is the LayerNorm text_embed_2_dim builds, not the encoder config I was reading.

@KyleNeverGivesUp

Copy link
Copy Markdown
Contributor Author

@SolitaryThinker this has been ready since your commits landed. All
review threads are resolved and pre-commit is green, so it just needs an
approve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Documentation scope: infra CI, tests, Docker, build scope: training Training pipeline, methods, configs type: feat New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants