Skip to content

Support Bounded Feedback Loops in the DAG Execution Engine#14550

Open
Prohect wants to merge 4 commits into
Comfy-Org:masterfrom
Prohect:feat/bounded-feedback-loops
Open

Support Bounded Feedback Loops in the DAG Execution Engine#14550
Prohect wants to merge 4 commits into
Comfy-Org:masterfrom
Prohect:feat/bounded-feedback-loops

Conversation

@Prohect

@Prohect Prohect commented Jun 19, 2026

Copy link
Copy Markdown

Closes #14551

Summary

Make ComfyUI's DAG execution engine support bounded feedback loops — where a sampler node's internal iteration variable (like step_index) can flow back upstream through the graph via ComfyMathExpression nodes to control its own parameters (CFG, s_noise, eta, r) on a per-step basis, without triggering a dependency cycle error.

Example workflow

SamplerCustomAdvanced → step_index → ComfyMathExpression("3 + a*2") → cfg → CFGGuider → SamplerCustomAdvanced

The core insight: the sampler loop is bounded (N steps, then terminates), so this isn't an infinite cycle. The DAG should recognize this and allow it.

Architecture

Two-Level Cycle Handling

Level 1 — Static Validation (validate_inputs DFS)
_is_bounded_feedback_cycle() checks whether at least one node in the detected cycle declares BOUNDED_FEEDBACK. If so, the cycle is allowed — the repeated node is marked valid and traversal continues.

Level 2 — Graph Building (TopologicalSort.add_node)
_is_feedback_output() checks whether a link's source socket is a declared bounded-iteration output. Feedback edges are:

  1. Recorded in execution_list.feedback_links
  2. NOT added as strong/blocking links → the cycle is broken
  3. A cache-link is still created so the downstream node can read the placeholder value

Multi-Hop Chain Walking (_build_feedback_fns)

DFS walks from the feedback-receiving ComfyMathExpression node through intermediate MathExpression nodes to find terminal CFGGuider / SamplerXXX targets. Each chain is composed into a callable fn(step, total_steps) evaluated with simple_eval + all 15 math functions.

Per-Step Re-Read in Sampler Functions

  • _init_dynamic_options() pops _dynamic_sampler_options from extra_args
  • _refresh_dynamic_params() re-reads s_noise and eta with proper noise_scale handling
  • _apply_dynamic_s_noise() re-reads s_noise from the mutable dict
  • 23 stochastic sampler functions patched to re-read at every loop iteration

Safety

  • KSAMPLER.sample() only injects _dynamic_sampler_options when _feedback_param_fns exists
  • Each sampler function pops _dynamic_sampler_options at function top, preventing model call leaks
  • The callback in SamplerCustomAdvanced fires after the model call for step i, passing step + 1

Bootstrap Flow

  1. Graph building skips feedback edges → records in feedback_links
  2. get_input_data() supplies type-appropriate defaults for feedback sources not yet executed
  3. Pre-execution bootstrap iterates feedback_links, calls _build_feedback_fns()
  4. After target node execution, feedback fns are attached to guider/sampler objects
  5. The sampler's callback wrapper applies per-step updates at each iteration

Files Changed

File Lines Description
execution.py +320 _FEEDBACK_DEFAULTS, _is_bounded_feedback_cycle(), _is_feedback_link(), _get_feedback_default(), _build_feedback_fns() (multi-hop DFS chain builder with simple_eval), _resolve_input_value(), _collect_extra_names(), bootstrap + injection logic, validate_inputs() bounded-cycle bypass
comfy_execution/graph.py +44 feedback_links dict on TopologicalSort, _is_feedback_output(), strong-link skip + cache-link for feedback edges
comfy/k_diffusion/sampling.py +114 3 helpers (_init_dynamic_options, _refresh_dynamic_params, _apply_dynamic_s_noise), per-loop s_noise/eta/r re-read in 23 stochastic sampler functions
comfy_extras/nodes_custom_sampler.py +31 BOUNDED_FEEDBACK = {"step_index"}, io.Int.Output(display_name="step_index"), per-step callback wrapper updating guider.cfg and sampler.extra_options
comfy/samplers.py +6 Conditional _dynamic_sampler_options injection in KSAMPLER.sample()

Verification

Tested end-to-end with a 40-step sampler using expression 3 + a * 2 where a = step/40:

  • CFG varies from 3.0 → 5.0 across the 40 steps in the expected linear ramp
  • Multi-hop chains, multi-input expressions, and pow() functions all verified

Design Principles

  • One-line opt-in: BOUNDED_FEEDBACK = {"step_index"} on any node enables feedback
  • Works with all sampler types
  • Safe by design — dynamic options never leak to model calls
  • Extensible — new iteration-producing nodes only need the BOUNDED_FEEDBACK declaration

Allow sampler nodes' internal iteration variables (e.g. step_index)
to flow back upstream through ComfyMathExpression nodes to control
per-step parameters (cfg, s_noise, eta, r) without triggering a
dependency cycle error.

Architecture: Two-level cycle handling
- Static validation: _is_bounded_feedback_cycle() allows cycles
  where any node declares BOUNDED_FEEDBACK
- Graph building: _is_feedback_output() skips strong links for
  declared feedback sockets, records them in feedback_links

Multi-hop chain walking via _build_feedback_fns() resolves
expression->CFGGuider/Sampler chains with simple_eval + MATH_FUNCTIONS,
composing per-step fn(step, total_steps) callables.

Sampler functions now re-read s_noise/eta/r each iteration via
_init_dynamic_options() / _refresh_dynamic_params() / _apply_dynamic_s_noise().
KSAMPLER.sample() conditionally injects mutable extra_options ref.

Safety: _dynamic_sampler_options popped at function top before model() calls.
One-line opt-in: BOUNDED_FEEDBACK = {'step_index'} on any node.
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: baefaeff-e1ed-4c5c-8abc-f8e4054fa1a1

📥 Commits

Reviewing files that changed from the base of the PR and between 4fece5b and 0edb25d.

📒 Files selected for processing (1)
  • execution.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • execution.py

📝 Walkthrough

Walkthrough

This PR implements a "bounded feedback" iteration mechanism for the ComfyUI sampling pipeline. TopologicalSort in comfy_execution/graph.py is extended to track feedback edges separately from blocking dependencies, preventing cycle deadlocks. execution.py adds default-seeding logic so feedback inputs receive type-appropriate placeholder values on the first pass, a callable builder that walks ComfyMathExpression chains to generate per-step update functions via simple_eval, and injection wiring that attaches those functions to cached guider/sampler objects after each node executes. Cycle validation is updated to accept bounded-feedback cycles as valid. SamplerCustomAdvanced declares the BOUNDED_FEEDBACK contract and a step_index output, wraps its callback to apply per-step CFG and sampler parameter updates, and returns total_steps. KSampler.sample exposes extra_options through extra_args when per-step update functions are present.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.54% 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 'Support Bounded Feedback Loops in the DAG Execution Engine' clearly and concisely summarizes the main feature being introduced.
Description check ✅ Passed The description comprehensively explains the bounded feedback loops feature, architecture, and implementation details with examples and file changes.
Linked Issues check ✅ Passed The PR fully addresses all requirements from issue #14551: static validation bypass, graph building with feedback edges, multi-hop chain walking, per-step re-reads in 23 sampler functions, and safety mechanisms.
Out of Scope Changes check ✅ Passed All changes directly support bounded feedback loop implementation across the five modified files with no extraneous modifications detected.

✏️ 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.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
execution.py (1)

744-751: 💤 Low value

Output socket 0 is hardcoded in _find_consumers.

The consumer search only matches links from source_id at output socket 0. This is correct for ComfyMathExpression (single output), but could silently miss connections if the feature is ever extended to nodes with multiple outputs.

Consider parameterizing the output socket
-    def _find_consumers(source_id):
+    def _find_consumers(source_id, source_socket=0):
         consumers = []
         for nid, n in prompt.items():
             for iname, ival in n.get("inputs", {}).items():
                 if isinstance(ival, list) and len(ival) == 2 \
-                   and ival[0] == source_id and ival[1] == 0:
+                   and ival[0] == source_id and ival[1] == source_socket:
                     consumers.append((nid, n.get("class_type"), iname))
         return consumers
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution.py` around lines 744 - 751, The _find_consumers function has the
output socket hardcoded to 0 in the condition ival[1] == 0, which will only find
consumers connected to output socket 0 and miss connections from other outputs.
Parameterize the function by adding an output_socket parameter to
_find_consumers alongside the existing source_id parameter, replace the
hardcoded 0 with this new parameter in the comparison, and update all calls to
_find_consumers throughout the code to pass the appropriate output socket index
as an argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@execution.py`:
- Around line 744-751: The _find_consumers function has the output socket
hardcoded to 0 in the condition ival[1] == 0, which will only find consumers
connected to output socket 0 and miss connections from other outputs.
Parameterize the function by adding an output_socket parameter to
_find_consumers alongside the existing source_id parameter, replace the
hardcoded 0 with this new parameter in the comparison, and update all calls to
_find_consumers throughout the code to pass the appropriate output socket index
as an argument.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd5a4a3b-a9e5-45db-b35e-33fbf67738fd

📥 Commits

Reviewing files that changed from the base of the PR and between bd39bbf and 983d6a1.

📒 Files selected for processing (5)
  • comfy/k_diffusion/sampling.py
  • comfy/samplers.py
  • comfy_execution/graph.py
  • comfy_extras/nodes_custom_sampler.py
  • execution.py

@Prohect

Prohect commented Jun 19, 2026

Copy link
Copy Markdown
Author

a demo workflow tested:
demo_anima.json

it's tested on Windows 11 25h2 26200.8457, Nvidia RTX 3080 10G(610.62),

result:
ComfyUI_00650_

- _FEEDBACK_DEFAULTS: add module-level docstring comment
- _find_consumers: add source_socket parameter (default 0), add docstring
- _is_sampler_target: add docstring
@Prohect

Prohect commented Jun 19, 2026

Copy link
Copy Markdown
Author

Updates pushed in 4fece5b:

  • Docstrings added to _FEEDBACK_DEFAULTS (module-level), _find_consumers, and _is_sampler_target
  • Nitpick addressed: _find_consumers now accepts source_socket=0 parameter instead of hardcoding socket 0

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.

Feature Request: Support Bounded Feedback Loops (per-step parameter control via step_index)

1 participant