Repository navigation
fix(hi3): fail fast when the AR KV cache cannot be built - #508
Merged
CjhHa1 merged 3 commits intoSep 24, 2026
Merged
Conversation
`HunyuanImage3ARStep._build_kv_cache` returned None when the upstream `HunyuanStaticCache` class was missing, the transformer had no config, or the constructor raised. Upstream `HunyuanImage3ForCausalMM` never creates a cache on its own, so a None cache made prefill correct but every decode step attend only to its own single token, silently producing context-free rollouts. Build the cache directly and let the original error propagate.
2 tasks
CjhHa1
added a commit
to PushpakAg/UniRL
that referenced
this pull request
Sep 26, 2026
…onflict in ar.py)
CjhHa1
added a commit
to PushpakAg/UniRL
that referenced
this pull request
Sep 26, 2026
…lpers Review follow-ups for Tencent-Hunyuan#479 after merging Tencent-Hunyuan#508: - Require conditions.tokenizer_output and fused.prompt_lengths in init_state at every batch size. Upstream sets the decode position_ids from tokenizer_output.real_pos; without it the loop silently re-feeds the shifted prompt, at B=1 too. This makes the real_pos-is-None fallbacks (tail append, logits[:, -1] at prefill, early mask return) unreachable, so they are removed. - Resolve real_pos once into HunyuanImage3ARState from fused.prompt_lengths, which embed_for_ar already normalizes to [B], instead of re-deriving it from tokenizer_output every step. - Inline the single-caller mask and token-placement helpers into step, branching on step_idx instead of on the presence of attention_mask, and read past_key_values.dynamic directly now that the cache is never None. - Drop the pad_id fill: the appended column is always overwritten or never gathered before it is. - README: point the gotchas at the inlined code, note that padded B=1 prompts were also fixed, document the real_pos requirement, and fix the upstream attention class name.
CjhHa1
added a commit
that referenced
this pull request
Sep 27, 2026
* fix(hi3): support batched trainside AR with dynamic KV cache Cached train-side HunyuanImage3 AR only worked at B=1. Fixes #452. The reported crash is upstream's `assert len(cache_position) == 1` in `HunyuanStaticCache.update`, where `len()` on a 2-D cache position is the batch dim. A dynamic cache trims the returned KV to one scalar end position, which cannot represent per-row right-pad, so `_build_kv_cache` now only requests `dynamic=True` at B=1 and lets B>1 use the full static cache. That makes the assert unreachable rather than patching upstream. Three further defects blocked B=1 parity once the crash was gone: - Upstream deletes the attention mask during `gen_text` decode, which is only safe for a per-row trimmed cache. Over a static cache the decode query also attends right-pad KV and never-written slots, so `_cache_attention_mask` admits row i's keys [0, real_pos_i + step) and right-pads the prefill mask's key axis to the cache width. - Upstream re-reads the next input token with `input_ids.gather(1, position_ids)` at `real_pos`, so a sampled token must be scattered to `real_pos_i + step` (`_place_token`); appending at the padded tail fed short rows their own pad token. - `FusedMultimodalCondition.concat` padded `position_ids` with 0, and those are the cache write indices, so every pad position of a short row index_copy_'d its KV onto slot 0 and silently replaced that row's first real token. `_pad_positions` pads with continuing indices instead. B=1 keeps the dynamic cache and is bit-identical. Batched AR without `real_pos` now raises in `init_state` instead of failing later on an opaque SDPA shape mismatch. * fix(hi3): require real_pos at every B and inline the batched-cache helpers Review follow-ups for #479 after merging #508: - Require conditions.tokenizer_output and fused.prompt_lengths in init_state at every batch size. Upstream sets the decode position_ids from tokenizer_output.real_pos; without it the loop silently re-feeds the shifted prompt, at B=1 too. This makes the real_pos-is-None fallbacks (tail append, logits[:, -1] at prefill, early mask return) unreachable, so they are removed. - Resolve real_pos once into HunyuanImage3ARState from fused.prompt_lengths, which embed_for_ar already normalizes to [B], instead of re-deriving it from tokenizer_output every step. - Inline the single-caller mask and token-placement helpers into step, branching on step_idx instead of on the presence of attention_mask, and read past_key_values.dynamic directly now that the cache is never None. - Drop the pad_id fill: the appended column is always overwritten or never gathered before it is. - README: point the gotchas at the inlined code, note that padded B=1 prompts were also fixed, document the real_pos requirement, and fix the upstream attention class name. * refactor(hi3): drop redundant real_pos state and dead guards from batched AR - Read real_pos from conditions.fused.prompt_lengths in step instead of shadowing it on HunyuanImage3ARState; step already receives the same conditions every call. - Fold the tokenizer_output / prompt_lengths requirement into the existing fused-field check in autoregress, the one validation site. - Drop the int() on max_cache_len (already int) and the clamp on the prefill logits index (real_pos is in [1, L] by construction). - Collapse _pad_positions' early returns; move its rationale pointer to the concat docstring and drop the measured numbers from the README. * docs(hi3): trim README gotchas to the upstream constraints * refactor(hi3): drop redundant helpers and fast paths in fused concat - Remove the leading empty/single-item fast path: the equal-length check already covers both, and Batch.concat raises on empty itself. - Collect sequence lengths as a set from input_ids directly; every producer of this condition sets input_ids. - Replace the three function-local Batch imports and Batch.concat.__func__(cls, ...) with super().concat(...). - Drop _pad_attn: padding the mask's key and query axes is two _pad_seq calls, with identical zero fill. - Build _pad_seq's F.pad spec directly for the negative dims it is called with, and fold its None / already-long early returns. - Rebuild each shard with dataclasses.replace so unpadded fields (scatter indices, prompt_lengths) carry over instead of being re-listed by hand. * refactor(hi3): reuse hydrate and drop the constant pad value in fused concat - Replace the local _materialize duck-type check with the canonical unirl.distributed.tensor.ref.hydrate, which tests isinstance(TensorRef) and maps an empty ref to None instead of torch.empty(0). - Drop _pad_seq's value parameter: every caller passed a zero (0, False, 0.0), which is F.pad's default fill for every dtype here. * refactor(hi3): hoist fused-concat pad helpers to module level _pad_seq and _pad_positions were closures only to capture max_L. Take the target length as an argument instead, matching the module-level _pad_* helpers in types/conditions/text.py and janus_pro/conditions.py, so they are no longer redefined on every concat call. The negative-dim constraint and the README pointer move into their docstrings. * refactor(hi3): drop the default-equal value=False on the prefill mask pad --------- Co-authored-by: CjhHa1 <cjh18671720497@outlook.com> Co-authored-by: Jianghai <72591262+CjhHa1@users.noreply.github.com>
CjhHa1
added a commit
to ruiling-smartbear/UniRL
that referenced
this pull request
Oct 15, 2026
- In modes/t2t.py, call sys.modules[type(transformer).__module__].get_system_prompt directly instead of importing system_prompt dynamically inside a silent try/except fallback. - In diffusion.py, inline _build_kv_cache into predict_noise and construct HunyuanStaticCache directly without the unreachable fused/cache_cls None fallbacks (matching ar.py after Tencent-Hunyuan#508).
CjhHa1
added a commit
that referenced
this pull request
Oct 16, 2026
) * fix(hi3): fail loudly when the batched chat template drops samples The checkpoint's `apply_general_template(batchify=True)` hands `batch_gen_infer` a one-element `prompt_list` and `zip`s it against the per-sample kwargs, so only the first prompt is encoded and the rest are dropped without an error. `apply_chat_template` with three prompts returns one row (two at `cfg_factor=2`). Today that surfaces later, as a `Part.fill` count mismatch or an it2i row-count check. Raise at the source instead, from the row count the call was asked for. Recipes that pin `rollout.forward_batch_size: 1` are unaffected, since one prompt with `cfg_factor=2` already returns two rows. * fix(hi3): correct the batch-drop gotcha and neutralize the guard message The diffusion path already fails at `split_cfg_fused` with an explicit row-count error, not a `fused.input_ids` shape mismatch, so the guard's new coverage is the AR path. Drop the per-prompt-concat workaround and the padding claim, which #544 contradicts. The error now names the dropped samples instead of declaring the checkpoint unable to batch, so it stays accurate once the tokenizer is repaired. * refactor(hi3): drop dead batch_message_list parameter and refine AR gotcha No caller passes batch_message_list to embed_for_ar or _apply_chat_template, so batch_prompt is always p.texts and the row-count guard reads len(batch_prompt) and tokens.shape[0] directly. Also note in the README gotcha that i2t fails earlier in instantiate_continuous_tokens before reaching Part.fill. * refactor(hi3): consolidate _fused_common into _build_fused Both embed_for_ar and embed_for_gen_image unpacked the 6-tuple from _fused_common, repeated the cond_vae_image_mask and cond_timestep_scatter_index lookups, and built HunyuanImage3FusedMultimodalCondition. Have _build_fused construct the condition container directly. * docs(hi3): align README gotcha and _apply_chat_template docstring with repo guide * refactor(hi3): drop dead parameters and redundant coercions in text_embed - Deduplicate _resolve_build_batch_2d_rope and _resolve_get_system_prompt into _resolve_upstream_symbol and inline the single-caller _resolve_system_prompt_preset. - Drop the unused max_sequence_length attribute on HunyuanImage3TextEmbedStage and the unused max_length parameter on embed_for_ar and _apply_chat_template. - Read gen_config.max_length, output.real_pos.reshape(-1), input_ids.shape, and p.texts directly without redundant getattr fallbacks, int/str coercions, or list copies. * refactor(hi3): resolve upstream helpers via transformer module directly FSDP2 wraps transformer.model rather than the outer HunyuanImage3ForCausalMM wrapper, so sys.modules[type(transformer).__module__] always points to the remote-code module (matching HunyuanImage3ARStep.init_state). * refactor(hi3): resolve get_system_prompt and HunyuanStaticCache directly - In modes/t2t.py, call sys.modules[type(transformer).__module__].get_system_prompt directly instead of importing system_prompt dynamically inside a silent try/except fallback. - In diffusion.py, inline _build_kv_cache into predict_noise and construct HunyuanStaticCache directly without the unreachable fused/cache_cls None fallbacks (matching ar.py after #508). * refactor(hi3): replace _optional_output_tensor with upstream to_device Both Base and Instruct checkpoints define to_device(data, device) on the modeling module and cond_timestep_scatter_index on TokenizerEncodeOutput. Reuse upstream_mod.to_device for optional condition tensors and resolve the vae/vit_image_mask vs cond_vae/vit_image_mask attribute rename inline. * fix(hi3): resolve Instruct stop tokens and drop dead AR/mode scaffolding - Read special tokens from added_tokens_encoder when special_token_map is absent so _stop_tokens_for_bot_task resolves </recaption>, </answer>, </think>, and <img_ratio_0..32> on the Instruct tokenizer as well as Base; fold _cot_stop_tokens into _stop_tokens_for_bot_task. - Drop the 10 unread fields on HunyuanImage3ARParams and the duplicate container constructions in modes/t2t.py, modes/i2t.py, and modes/t2ti.py; inline _pack_text_segment in ar.py. - Deduplicate prepare_seeded_sampling in seed.py and _as_sample_batches in modes/t2t.py across t2i, it2i, and i2t. * refactor(hi3): drop remaining falsy fallbacks, coercions, and ARSamplingParams rebuilds - Read control dicts and fields directly (control.get('ar', {}), bot_task, sys_type, sequence_template, stop_token_ids) without 'or {}' / 'or []' / str(...) coercions. - Forward the frontier ARSamplingParams directly to autoregress instead of rebuilding ARSamplingParams in t2t, i2t, and t2ti. - Read tokenizer special_map and eos_token_id directly in _stop_tokens_for_bot_task, and read out.logits and typed sampling fields directly in ar.py. --------- Co-authored-by: SmartBea_Linlin_r <80329948+GaoRuilin1@users.noreply.github.com> Co-authored-by: CjhHa1 <cjh18671720497@outlook.com>
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
HunyuanImage3ARStep._build_kv_cachereturnedNonein three cases: the upstreamHunyuanStaticCacheclass could not be found, the transformer had noconfig, or the cache constructor raised (swallowed byexcept Exception). The AR loop then kept running withpast_key_values=None.That fallback does not degrade gracefully. Upstream
HunyuanImage3ForCausalMMnever creates a cache itself: attention only writes KV whenpast_key_value is not None, andHunyuanImage3Model.forwardhands the incoming cache straight back out. So withNone:prepare_inputs_for_generationgathersinput_idsdown to[B, 1]atposition_ids), with no KV, so it attends only to itself.The result is context-free generations with valid shapes and no error. The most realistic trigger is a transformers upgrade that changes the
StaticCacheconstructor signature thatHunyuanStaticCacheinherits.This PR builds the cache directly in
init_stateand lets the original error propagate. With the fallbacks gone,_build_kv_cachewas a single-caller wrapper around one constructor call, so it is inlined.Related Issue
N/A. Found while reviewing #479.
Test Plan
HunyuanStaticCachewith the requestedbatch_size/max_cache_len/dynamic=True;TypeError;HunyuanStaticCacheraisesAttributeError: module ... has no attribute 'HunyuanStaticCache'.pre-commit run --files unirl/models/hunyuan_image3/ar.py: all hooks pass.Compatibility / Risk
init_statewith the underlying error. They were already producing wrong decode tokens.type(transformer)stays the upstream class under FSDP (trainable_module()wrapstransformer.model) and LoRA (inject_adapter_in_modelis in-place), so the module lookup is unaffected by training wrappers.HunyuanImage3ARStepwith a stub transformer that relied on theNonepath.Reviewer Notes
_build_kv_cacheis intentionally untouched:predict_noisewithstate=Noneis a legitimate uncached mode that recomputes the full sequence every step. ARreplayalso usespast_key_values=Nonelegitimately (use_cache=False, single full forward) and does not go through_build_kv_cache._build_kv_cache(adds adynamicargument), so it will need a rebase: passdynamic=batch_size == 1to the inlined constructor ininit_state.getattr(past_key_values, "dynamic", True)in_cache_attention_mask, whose default currently absorbs thisNonecase. After this lands it can becomepast_key_values.dynamic.Checklist