perf(hi3): implement the batched-step replay hook for HunyuanImage3 - #34
Draft
leviking98z-rgb wants to merge 1 commit into
Draft
perf(hi3): implement the batched-step replay hook for HunyuanImage3#34leviking98z-rgb wants to merge 1 commit into
leviking98z-rgb wants to merge 1 commit into
Conversation
leviking98z-rgb
force-pushed
the
refactor/model-diffusion-runner
branch
from
August 4, 2026 04:15
f717a5c to
5b0bafa
Compare
The single-stream runner exposes ``_replay_batched`` as an optional one-forward replay fast path, but only SD3 implements it — HI3 still issues one transformer forward per selected SDE step. HI3's DiT is a large MoE, so each extra forward pays its own kernel launches, FSDP all-gather and activation recompute at a batch of only B, which does not saturate the device. Implement the hook. Replay is stateless (``_step_kwargs`` returns no state outside ``mode="sample"``), so the S selected transitions have no cross-step dependency and can be stacked on the batch dim: sample/prev_sample become ``[S*B, C, H, W]`` step-major, sigma/sigma_next ride as ``[S*B]`` vectors, and one ``step_with_logp`` does a single forward plus one vectorized SDE transition. Log-probs reshape back to ``[B, S]``. The tiling is the non-obvious part. The runner CFG-expands before replay (``_prepare_conditions`` -> ``_expand_cfg_for_forward``), so the incoming fused batch is BRANCH-major ``[cond_B, uncond_B]`` while the latents are STEP-major. Since ``predict_noise`` splits the fused batch at its midpoint, the tiled layout must stay branch-major with each branch internally step-major: ``[cond_step0, cond_step1, ..., uncond_step0, ...]``. Getting this nesting backwards is silent — it produces wrong log-probs, not a crash. Note ``rope_cache`` tiles like any other row-aligned field here: it is now a per-sample ``[B, 2, L, D]`` CONCAT tensor that ``predict_noise`` genuinely consumes, not the base's shared ``(cos, sin)`` tuple. Conditional-image payloads are lists in the it2i path despite tensor annotations, so both tensor- and sequence-backed fields are handled, mirroring ``_expand_cfg_for_forward``. Guards fall back to the serial loop (return None) when the flag is off, when S <= 1, when the strategy is not a stateless ``SDEStrategy``, or when the guidance scale varies across the target steps — a per-step schedule cannot be folded into one forward because the CFG combine would need a per-row scale. Default is False on the config; the two HI3 unified recipes opt in. Verified on a CPU fake-kernel harness whose per-row output mixes in the conditioning rows, so a wrong tiling permutation changes the result (19 checks): batched log_probs and prev_sample_means are equal to the serial path at both cfg=1 and cfg=2 (branch-major); all four guards fall back to serial; the tiled layout is branch-major with step-major inside; repeats=1 is identity; rope_cache is tiled rather than passed verbatim; non-positive repeats are rejected. A mutation check confirms the harness has teeth: swapping the tiling nesting order makes log_probs diverge by 1.9e4 and fails three assertions. Also ran: ruff check/format on the touched files, check_recipe_targets (2434 paths), check_experimental_boundaries, and a recipe-key audit against the config dataclass. NOT run: GPU real-checkpoint parity or any throughput measurement on this base — see the PR description.
leviking98z-rgb
force-pushed
the
codex/hi3-batched-step-replay
branch
from
August 4, 2026 04:19
84506f9 to
e6760d1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tencent-Hunyuan#299 introduces
SingleStreamDiffusionRunner._replay_batchedas an optional one-forward replay fast path, and the runner's serial loop is the default when it returnsNone. SD3 implements the hook; HI3 does not. So HunyuanImage3 still issues one transformer forward per selected SDE step.That is worth fixing for HI3 specifically: its DiT is a large MoE, so every extra replay forward pays its own kernel launches, FSDP all-gather and activation recompute at a batch of only
B— a shape that does not saturate the device.This implements the hook. Replay is stateless (
_step_kwargsreturns no state outsidemode="sample"), so theSselected transitions carry no cross-step dependency and can be stacked on the batch dim:sample/prev_sample→[S*B, C, H, W], step-major (rows[k*B:(k+1)*B]are all B samples attarget[k])sigma/sigma_next→[S*B]vectors, since one batch now mixes different stepsstep_with_logp→ one transformer forward + one vectorized SDE transition[B, S]With
num_sde_steps=2that removes one of two serial forwards; the win scales withnum_sde_steps.The tiling is the non-obvious part, and the reason this isn't a copy of SD3's. The runner CFG-expands before replay (
_prepare_conditions→_expand_cfg_for_forward), so the incoming fused batch is branch-major[cond_B, uncond_B]while the latents are step-major.predict_noisesplits the fused batch at its midpoint into the cond/uncond halves, so the tiled layout must stay branch-major with each branch internally step-major:Getting that nesting backwards is silent — wrong log-probs, no crash. See the mutation check in the Test Plan.
Two further details specific to HI3:
rope_cachetiles like any other row-aligned field. In this stack it is a per-sample[B, 2, L, D]CONCAT tensor thatpredict_noisegenuinely consumes (unbound to(cos, sin)at the model boundary), not the base's shared(cos, sin)tuple. Passing it through verbatim would feed the wrong rows._tilehandles tensor- and sequence-backed fields alike — mirroring what_expand_cfg_for_forwardalready does.Guards fall back to the serial loop (return
None) when: the flag is off;S <= 1; the strategy is not a statelessSDEStrategy(a stateful solver may consumestep_indexor carry cross-step state); or the guidance scale varies across the target steps — a per-step schedule cannot be folded into one forward, because the CFG combine is per-row and would need a per-row scale.Because the π_old anchor is replayed through this same method, the on-policy ratio stays exactly 1.
Default is
FalseonHunyuanImage3PipelineConfig; the two HI3 unified recipes opt in.Related Issue
Depends on Tencent-Hunyuan#299. Extends the batched-step replay work in Tencent-Hunyuan#144 / Tencent-Hunyuan#156 to HunyuanImage3.
Test Plan
Run from repo root at
84506f9(on top of Tencent-Hunyuan#299'sf717a5c2); venv torch 2.13.0+cu130.1. The property that matters — batched output equals serial output. A CPU fake-kernel harness whose per-row result deliberately mixes in the conditioning rows, so a wrong tiling permutation changes the numbers (uncommitted, per the no-
tests/policy):2. Mutation check — the harness has teeth. Swapping the tiling nesting order (step-major outer instead of branch-major outer, i.e. exactly the bug this layout guards against):
So the 19 passes above are not vacuous. Reverted after the check.
3. Hook conforms to the base contract:
4. Recipe keys audited against the config dataclass:
5. Repo gates:
(
ruff check unirl/repo-wide reports 4 pre-existing F841 inunirl/models/boogu_image/vendor/attention_processor.py, untouched by this PR.)Not run: GPU real-checkpoint parity, and any throughput number on this base. See Reviewer Notes — this is the main gap and I would rather flag it than paper over it.
Compatibility / Risk
batch_replay_stepsdefaults toFalseon the config; with it off,_replay_batchedreturnsNoneand the runner's serial loop runs exactly as today.false.Falsedefault.unirl/models/hunyuan_image3/*and the two HI3 recipes are touched. SD3's own implementation and the shared runner are untouched.S× the serial one's, since the batch isS*Binstead ofB. Atnum_sde_steps=2that is 2×. This is the real cost and I have not measured it on HI3 — a recipe that is already near its memory ceiling should keep the flag off until it has.Verification
I reviewed the full diff and ran every command quoted above.
Duplicate-work check: scanned the open PRs. Tencent-Hunyuan#299 defines the hook but leaves HI3 unimplemented (
_replay_batchedappears 4× insd3/diffusion.py, 0× inhunyuan_image3/diffusion.pyon its head). Tencent-Hunyuan#156 extends batched replay to qwen_image / z_image / flux2_klein and does not touch HI3. Tencent-Hunyuan#144 is the original SD3 work. Nothing else touchesunirl/models/hunyuan_image3/.Reviewer Notes
The evidence gap, stated plainly. What I have verified is equivalence — batched replay produces the same log-probs and prev-sample means as the serial path, on a fake kernel, including a mutation check that the test would catch a wrong layout. What I have not verified on this base is (a) parity with a real HI3 checkpoint on GPU, and (b) any speedup number.
I do have a measurement of this mechanism from an earlier, pre-Tencent-Hunyuan#299 HI3 stack: training-phase time −24.2%, with per-phase timing attributing effectively all of it to the image backward (118.07s → 86.85s, −26.4%) while AR backward stayed flat (19.04s → 19.60s). I am deliberately not presenting that as evidence for this diff — different base, different conditions plumbing (
fused_uncondwas not split out then, andrope_cachewas a shared tuple rather than a per-sample tensor), so the code paths differ in exactly the places this PR had to get right. Treat it as motivation for why the hook is worth implementing, not as a result.Happy to hold this as draft until Tencent-Hunyuan#299 lands and I can run a real-checkpoint parity check (
ratio=1.0000, log-probmax_abs_diff) plus a controlled A/B on the same 8×H20 shape Tencent-Hunyuan#299's own test plan uses.Where I'd look first:
_tile_conditions, specifically the branch-major/step-major nesting and thecfg_factorbranch that decides whether aB-sized payload is duplicated per branch or split. That is where a subtle error would be silent. Second: whether theSDEStrategy+ constant-guidance guard pair is sufficient for every HI3 strategy you expect in practice.