Skip to content

feat: Context windows - add causal_window_fix to improve blending of context windows (CORE-100)#13563

Open
drozbay wants to merge 3 commits intoComfy-Org:masterfrom
drozbay:20260425a_context_causal_fix
Open

feat: Context windows - add causal_window_fix to improve blending of context windows (CORE-100)#13563
drozbay wants to merge 3 commits intoComfy-Org:masterfrom
drozbay:20260425a_context_causal_fix

Conversation

@drozbay
Copy link
Copy Markdown
Contributor

@drozbay drozbay commented Apr 25, 2026

Adds a causal_window_fix toggle to context windows that prepends a throwaway anchor frame at sub-position 0 of every non-initial window, absorbing the model's learned "scene start" content bias on a slot that gets stripped before output.

Without this fix, windows starting mid-video have their first real frame contaminated by the bias the model applies to position 0 (since the model was trained on clips that genuinely start at frame 0), causing visible seams or quality degradation at window boundaries. The anchor is the latent immediately preceding the window's first frame, prepended via an anchor-aware get_tensor and stripped from sub_conds_out after the model forward pass.

Having this option enabled does add one extra latent frame to the sampling for each window beyond the starting window.

The option is exposed as a Boolean input on ContextWindowsManualNode (default True). It will be enabled by default with model-specific nodes like WanContextWindowsManualNode.

Example workflow:

This is a very basic example workflow using Wan2.1 I2V with standard_static window mode and splitting the I2V conditionings over several windows, low overlap to emphasize the effect.
droz_contextwindows_causalwindowfix_test.json

Output:

ComfyUI_00217_.mp4

Single frame comparison:
image

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

Adds a causal_window_fix boolean (default true) to IndexListContextHandler and to the ContextWindowsManual node API. When enabled, a synthetic anchor index (one position before a window start, if in-bounds) is prepended to window extraction; conditioning slice logic is adjusted for temporal_offset > 0; model outputs for anchor-augmented windows are narrowed to remove the prepended timestep before combining window results. The node now forwards the new parameter to the handler.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a causal_window_fix feature to improve context window blending, with a specific ticket reference.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly explains the causal_window_fix feature, its purpose in addressing window boundary seams, implementation details, and includes workflow examples with visual comparisons.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
comfy/context_windows.py (2)

66-75: ⚠️ Potential issue | 🟠 Major

retain_index_list clobbers the anchor slot instead of the intended window position.

When causal_anchor_index is set, indices becomes [anchor_idx] + index_list, so window-local position 0 now corresponds to the anchor frame. The retain_index_list block right below still indexes both window and full with the same numbers:

if retain_index_list:
    idx = tuple([slice(None)] * dim + [retain_index_list])
    window[idx] = full[idx]

So with cond_retain_index_list = "0" (the documented "use the initial start image for each window" feature) and causal_window_fix=True, this overwrites the anchor slot with full[0] — and that slot is then discarded by the narrow(...) strip in evaluate_context_windows. The first real frame of the window keeps its original sliced value, silently breaking cond_retain_index_list for any non-initial window.

Both features are enabled together by default on ContextWindowsManualNode (causal_window_fix=True, cond_retain_index_list=""), so the typical workflow doesn't trigger this, but anyone enabling cond_retain_index_list will hit it.

🛠️ Suggested fix — shift the retain destinations to skip the anchor
         indices = self.index_list
         anchor_idx = getattr(self, 'causal_anchor_index', None)
-        if anchor_idx is not None and anchor_idx >= 0:
+        anchor_offset = 0
+        if anchor_idx is not None and anchor_idx >= 0:
             indices = [anchor_idx] + list(indices)
+            anchor_offset = 1
         idx = tuple([slice(None)] * dim + [indices])
         window = full[idx]
         if retain_index_list:
-            idx = tuple([slice(None)] * dim + [retain_index_list])
-            window[idx] = full[idx]
+            src_idx = tuple([slice(None)] * dim + [retain_index_list])
+            dst_positions = [i + anchor_offset for i in retain_index_list]
+            dst_idx = tuple([slice(None)] * dim + [dst_positions])
+            window[dst_idx] = full[src_idx]
         return window.to(device)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy/context_windows.py` around lines 66 - 75, The retain block currently
writes full[retain_index_list] back into window using the same indices even when
you prepended the causal_anchor_index into indices, so the anchor at local
position 0 gets clobbered; fix by keeping the source indices for full as
retain_index_list but offsetting the destination indices into window when
causal_anchor_index is present (e.g. compute dest_retain = retain_index_list if
anchor_idx is None else [i+1 for i in retain_index_list]) and use that for the
window assignment (window[dest_retain] = full[retain_index_list]); update the
code around indices, anchor_idx, retain_index_list, window and full accordingly
so evaluate_context_windows no longer discards the intended retained frames.

100-141: ⚠️ Potential issue | 🟠 Major

Handle anchor in slice_cond offset/scale branches to match fast-path behavior.

The fast path at lines 114-116 calls window.get_tensor() which prepends causal_anchor_index when present. However, the temporal_offset > 0 and temporal_scale > 1 branches (lines 119-141) rebuild indices from window.index_list directly, bypassing this anchor logic.

WAN22_Animate exercises these non-default branches with temporal_offset=1 (face_pixel_values, pose_latents) and temporal_scale=4 (face_pixel_values), making the inconsistency active code in models using the new Animate architecture.

Apply the same anchor-aware index handling to offset/scale paths for consistency:

Suggested fix
     # skip leading latent positions that have no corresponding conditioning (e.g. reference frames)
     if temporal_offset > 0:
         indices = [i - temporal_offset for i in window.index_list[temporal_offset:]]
         indices = [i for i in indices if 0 <= i]
     else:
         indices = list(window.index_list)

+    anchor_idx = getattr(window, 'causal_anchor_index', None)
+    if anchor_idx is not None and anchor_idx >= 0:
+        prepend = anchor_idx - temporal_offset
+        if prepend >= 0:
+            indices = [prepend] + indices
+
     if not indices:
         return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy/context_windows.py` around lines 100 - 141, slice_cond's
temporal_offset>0 and temporal_scale>1 branches rebuild indices directly and
miss the window.causal_anchor_index handling that window.get_tensor() applies;
update those branches to detect anchor = getattr(window, "causal_anchor_index",
None) and prepend the anchor-aware indices the same way the fast path does: for
offset branch compute anchor_index = anchor - temporal_offset (and only include
if 0 <= anchor_index) and insert it at the start of indices before any scaling;
for temporal_scale>1, when expanding scaled indices also produce and prepend the
corresponding scaled anchor positions (anchor_index * temporal_scale + k) if
they are within cond_size so the offset/scale paths mirror window.get_tensor()'s
anchor behavior.
🧹 Nitpick comments (1)
comfy/context_windows.py (1)

327-353: Anchor inject/strip flow looks correct.

anchor_idx = window.index_list[0] - 1 with the 0 <= anchor_idx < x_in.size(self.dim) guard correctly skips the first window (anchor would be -1) and avoids out-of-range on small tensors. The narrow(self.dim, 1, shape-1) matches the prepend-at-position-0 contract from get_tensor, so sub_conds_out is back to context_length along self.dim before combine_context_window_results indexes it via window.index_list. Nice and tight.

One small nit — window.causal_anchor_index is a dynamically-attached attribute used as an out-of-band channel to get_tensor. Threading it explicitly (e.g., a parameter or local override) would make the dependency obvious to future readers, but the windows are freshly constructed each execute() so there's no actual lifetime hazard today. Fine to leave as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy/context_windows.py` around lines 327 - 353, The anchor injection/strip
flow is correct; no functional change is required — the causal anchor logic
using anchor_idx, window.causal_anchor_index, get_tensor, and the subsequent
narrow(self.dim, 1, ...) on sub_conds_out correctly preserves context_length for
combine_context_window_results which indexes by window.index_list; you can leave
the implementation as-is, or optionally make the dependency explicit by
threading causal_anchor_index into get_tensor (e.g., add an explicit parameter
to get_tensor and propagate it from the caller) if you prefer to avoid the
dynamic attribute for future clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@comfy/context_windows.py`:
- Around line 66-75: The retain block currently writes full[retain_index_list]
back into window using the same indices even when you prepended the
causal_anchor_index into indices, so the anchor at local position 0 gets
clobbered; fix by keeping the source indices for full as retain_index_list but
offsetting the destination indices into window when causal_anchor_index is
present (e.g. compute dest_retain = retain_index_list if anchor_idx is None else
[i+1 for i in retain_index_list]) and use that for the window assignment
(window[dest_retain] = full[retain_index_list]); update the code around indices,
anchor_idx, retain_index_list, window and full accordingly so
evaluate_context_windows no longer discards the intended retained frames.
- Around line 100-141: slice_cond's temporal_offset>0 and temporal_scale>1
branches rebuild indices directly and miss the window.causal_anchor_index
handling that window.get_tensor() applies; update those branches to detect
anchor = getattr(window, "causal_anchor_index", None) and prepend the
anchor-aware indices the same way the fast path does: for offset branch compute
anchor_index = anchor - temporal_offset (and only include if 0 <= anchor_index)
and insert it at the start of indices before any scaling; for temporal_scale>1,
when expanding scaled indices also produce and prepend the corresponding scaled
anchor positions (anchor_index * temporal_scale + k) if they are within
cond_size so the offset/scale paths mirror window.get_tensor()'s anchor
behavior.

---

Nitpick comments:
In `@comfy/context_windows.py`:
- Around line 327-353: The anchor injection/strip flow is correct; no functional
change is required — the causal anchor logic using anchor_idx,
window.causal_anchor_index, get_tensor, and the subsequent narrow(self.dim, 1,
...) on sub_conds_out correctly preserves context_length for
combine_context_window_results which indexes by window.index_list; you can leave
the implementation as-is, or optionally make the dependency explicit by
threading causal_anchor_index into get_tensor (e.g., add an explicit parameter
to get_tensor and propagate it from the caller) if you prefer to avoid the
dynamic attribute for future clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f99b27f-ed22-48b2-bf5f-8ba671642ed4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3f15a and 4fcc05e.

📒 Files selected for processing (2)
  • comfy/context_windows.py
  • comfy_extras/nodes_context_windows.py

@drozbay
Copy link
Copy Markdown
Contributor Author

drozbay commented Apr 26, 2026

Responding to Coderabbit comments:

  1. Concerning "retain_index_list clobbers the anchor slot instead of the intended window position":
    The current behavior is intentional. The retained reference is placed at the new anchor slot, which is positioned to represent the single frame starting latent. If we "fixed" it to move the start latent to sub-pos 1 instead, it would be placing a single frame latent into a multi-frame slot. The behavior now does overwrite the anchor slot which is then discarded after the forward call, but during the model call it would see the I2V conditioning at the proper zero position. The stripped output isn't a loss, it still greatly influences the kept frames and only 1 frame is discarded, keeping the accumulated latents afterwards purely aligned as multi-frame latents instead of having single-frame latents mixed in.
  2. Concerning "Handle anchor in slice_cond offset/scale branches to match fast-path behavior.":
    The proposed fix seems to introduce a 1-latent misalignment, prepending anchor_idx - temporal_offset puts the anchors own cond data at the slot where the first kept frame's cond data should go. There could be a more correct fix for this but in reality the actual offset is only off by 1 real frame and that's practically not a big deal, according to my testing. I'll look into a fix but I think this is actually minor and shouldn't hold up this merge.
    (Update: I did end up pushing an update to fix the 2nd concern, after testing I found the results were better with the fix.)

@alexisrolland alexisrolland changed the title feat: Context windows - add causal_window_fix to improve blending of context windows feat: Context windows - add causal_window_fix to improve blending of context windows (CORE-100) Apr 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
comfy/context_windows.py (2)

334-340: Looped/wrap-around windows skip the fix when index_list[0] == 0.

For closed_loop / UNIFORM_LOOPED schedules a window can legitimately start at sub-frame 0 after wrap-shifting (e.g., [0, 1, 2, ...] representing a continuation of an earlier wrap), in which case anchor_idx = -1 and anchor_applied stays False — so this particular window misses the causal fix even though it's not the "true" first window of the video. The non-looped case (STATIC_STANDARD, the one shown in the PR images) is unaffected, so this is just an edge case worth being aware of.

If you want to cover it, you could wrap to num_frames - 1 for looped schedules:

♻️ Optional: wrap anchor for looped windows
             if self.causal_window_fix:
-                anchor_idx = window.index_list[0] - 1
-                if 0 <= anchor_idx < x_in.size(self.dim):
+                first = window.index_list[0]
+                if first > 0:
+                    anchor_idx = first - 1
+                elif self.closed_loop:
+                    anchor_idx = x_in.size(self.dim) - 1
+                else:
+                    anchor_idx = -1
+                if 0 <= anchor_idx < x_in.size(self.dim):
                     window.causal_anchor_index = anchor_idx
                     anchor_applied = True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy/context_windows.py` around lines 334 - 340, The causal-anchor logic
skips looped windows that start at index 0 because anchor_idx becomes -1; update
the block in the method using self.causal_window_fix to detect looped schedules
(e.g., check self.schedule == UNIFORM_LOOPED or closed_loop mode) and when
anchor_idx < 0 set anchor_idx = x_in.size(self.dim) - 1 (or num_frames - 1)
before assigning window.causal_anchor_index and setting anchor_applied = True so
wrap-around windows receive the causal anchor fix; keep existing bounds check
for non-looped schedules.

163-177: Handler default is safe for node-level integration, but document if this is a new default.

The node module already explicitly passes causal_window_fix to the handler (line 55), so the handler-level default doesn't affect the node's current behavior. However, if the causal_window_fix=True default is new to this PR, external custom nodes that instantiate IndexListContextHandler directly without naming this parameter would silently inherit the new behavior. Consider adding a note in release documentation so custom node authors are aware of the default, or document the parameter in the handler's docstring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy/context_windows.py` around lines 163 - 177, The handler's __init__ now
defaults causal_window_fix=True which may change behavior for external users who
instantiate IndexListContextHandler directly; update the IndexListContextHandler
class docstring (or the __init__ docstring) to explicitly document the
causal_window_fix parameter, its default value (True), and its behavioral
effect, and mention that node-level code still passes causal_window_fix
explicitly (so current node behavior is unchanged) so external custom nodes are
informed of the new default.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@comfy/context_windows.py`:
- Around line 334-340: The causal-anchor logic skips looped windows that start
at index 0 because anchor_idx becomes -1; update the block in the method using
self.causal_window_fix to detect looped schedules (e.g., check self.schedule ==
UNIFORM_LOOPED or closed_loop mode) and when anchor_idx < 0 set anchor_idx =
x_in.size(self.dim) - 1 (or num_frames - 1) before assigning
window.causal_anchor_index and setting anchor_applied = True so wrap-around
windows receive the causal anchor fix; keep existing bounds check for non-looped
schedules.
- Around line 163-177: The handler's __init__ now defaults
causal_window_fix=True which may change behavior for external users who
instantiate IndexListContextHandler directly; update the IndexListContextHandler
class docstring (or the __init__ docstring) to explicitly document the
causal_window_fix parameter, its default value (True), and its behavioral
effect, and mention that node-level code still passes causal_window_fix
explicitly (so current node behavior is unchanged) so external custom nodes are
informed of the new default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dfe46824-29e4-4f4b-af93-3d59bdf0de6a

📥 Commits

Reviewing files that changed from the base of the PR and between 4fcc05e and 8eee52d.

📒 Files selected for processing (1)
  • comfy/context_windows.py

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants