Skip to content

Event-driven scheduler: remove 10ms poll floor and 1s dependency-chain stalls - #91

Open
andre-merzky wants to merge 8 commits into
mainfrom
feature/nosleep
Open

Event-driven scheduler: remove 10ms poll floor and 1s dependency-chain stalls#91
andre-merzky wants to merge 8 commits into
mainfrom
feature/nosleep

Conversation

@andre-merzky

Copy link
Copy Markdown
Member

Summary

The run loop detected component completion only by polling future.done() once per pass, with a hardcoded asyncio.sleep(0.01) after every active pass. Two consequences:

  • ~10ms latency floor per task: submission of new work waits out the tail sleep of the previous pass (measured 10.9ms round-trip for a no-op function task on LocalExecutionBackend).
  • ~1s stall per dependency edge: a dependent behind a task running longer than one poll interval is only submitted when the idle event-wait hits its 1.0s timeout, since nothing woke the loop on completion (measured 1.03s for a slow(50ms) -> fast chain).

This PR makes completion wake the loop through the existing _component_change_event: every component future gets a done callback that sets the event (asyncio future callbacks run in the event loop, so this is thread-safe), and the post-activity sleep becomes a bare yield. The idle branch still waits on the same event with its existing 1s safety timeout as a backstop.

Measured after the change: no-op task round-trip 10.9ms → 2.0ms (raw ProcessPoolExecutor floor is 1.5ms); 50ms-task → dependent chain 1.03s → 0.05s.

Dependency tracking fix (first commit)

Removing the poll interval exposed a latent correctness bug, fixed separately in the first commit: dependency detection only scanned positional args for futures, while _extract_dependency_values resolves futures in args and kwargs. A future passed as a kwarg was therefore never registered as a dependency, and its consumer could be submitted while the producer was still running (InvalidStateError: Result is not set). This is reachable on current main with any kwarg producer slower than one poll interval; the faster loop made it visible even for no-op producers. The commit also deduplicates dependencies (the same future passed twice previously double-counted and deadlocked the consumer).

Tests

  • tests/unit/test_scheduler_responsiveness.py: chain behind a 50ms task completes well under the 1s event-wait; sequential no-op round-trip beats the former 10ms poll floor. Both fail on current main (1.03s / 11.8ms) and pass here.
  • tests/unit/test_dependencies_detection.py: kwarg-future producer is awaited before its consumer runs (fails on main with InvalidStateError); the same future passed positionally and as kwarg completes (guards the dedup).
  • Full unit suite green 10×/10 (the kwarg race previously flaked ~1/6 with the fast loop); integration suite green; ruff clean on all touched files.

Context

Found while profiling ROSE's streaming learner: at batch size 1 the end-to-end window rate was pinned at ~45/s by the 22ms latency of its two chained no-op tasks — entirely scheduler sleep, not execution.

🤖 Generated with Claude Code

andre-merzky and others added 2 commits August 14, 2026 15:41
Dependency detection only scanned positional args for futures, while
_extract_dependency_values resolves futures in args and kwargs alike. A
future passed as a keyword argument was therefore resolved at submission
but never registered as a dependency: the consumer could be submitted
while its kwarg producer was still running, failing with
'InvalidStateError: Result is not set'. The scheduler's 10ms poll
interval mostly masked this for fast tasks; any kwarg producer running
longer than one poll interval triggers it.

Also deduplicate detected dependencies: the same future passed more than
once (e.g. positionally and as a kwarg) incremented the dependency count
twice but was only decremented once on completion, deadlocking the
consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The run loop detected component completion only by polling future.done()
once per pass, and every active pass ended in a hardcoded 10ms sleep.
This put a ~10ms floor on each task round-trip and, worse, stalled
dependency chains: a dependent behind a task running longer than one
poll interval was only submitted when the idle event-wait hit its 1s
timeout (~1s per dependency edge).

Wake the loop through _component_change_event instead: every component
future gets a done callback setting the event (callbacks run in the
event loop, so this is thread-safe), and the post-activity sleep becomes
a bare yield. The idle branch still waits on the same event with its
existing 1s safety timeout.

Measured with LocalExecutionBackend no-op function tasks: sequential
round-trip 10.9ms -> 2.0ms; 50ms-task -> dependent chain 1.03s -> 0.05s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR updates the workflow scheduler to be event-driven (waking promptly on component completion) and fixes dependency detection so futures passed via kwargs are correctly tracked and duplicate dependencies don’t deadlock.

Changes:

  • Register done-callbacks on component futures to set _component_change_event and replace the fixed 10ms post-activity sleep with an asyncio.sleep(0) yield.
  • Fix dependency detection to include kwargs values and deduplicate repeated dependency futures.
  • Add unit regression tests covering responsiveness (no 10ms latency floor / no 1s dependency-chain stalls) and kwarg/duplicate dependency tracking.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/radical/asyncflow/workflow_manager.py Event-driven run-loop wakeups on completion; kwargs dependency detection and dependency deduplication.
tests/unit/test_scheduler_responsiveness.py New latency/regression tests for scheduler responsiveness improvements.
tests/unit/test_dependencies_detection.py New tests for kwarg dependency tracking and dedup behavior; updated imports.
Suppressed comments (1)

tests/unit/test_dependencies_detection.py:82

  • Same cleanup issue here: if the test fails before the last line, flow.shutdown() won’t run. Use try/finally to guarantee shutdown (and avoid leaking executors across tests).
    backend = await LocalExecutionBackend()
    flow = await WorkflowEngine.create(backend=backend)


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/unit/test_dependencies_detection.py Outdated
Comment thread src/radical/asyncflow/workflow_manager.py
andre-merzky and others added 6 commits August 14, 2026 16:09
Reject plain asyncio.Futures passed as dependencies with a clear
TypeError instead of failing later with 'Future is not subscriptable' —
dependency tracking can only resolve futures created by asyncflow tasks
or blocks. Wrap the two new dependency tests in try/finally so the
engine is shut down even when an assertion fails, and re-wrap their
docstrings as the pre-commit docformatter hook (v1.7.7) demands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andre-merzky
andre-merzky requested a review from AymenFJA August 20, 2026 11:21
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.

2 participants