Event-driven scheduler: remove 10ms poll floor and 1s dependency-chain stalls - #91
Open
andre-merzky wants to merge 8 commits into
Open
Event-driven scheduler: remove 10ms poll floor and 1s dependency-chain stalls#91andre-merzky wants to merge 8 commits into
andre-merzky wants to merge 8 commits into
Conversation
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>
There was a problem hiding this comment.
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_eventand replace the fixed 10ms post-activity sleep with anasyncio.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.
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>
…al.asyncflow into feature/nosleep
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
The run loop detected component completion only by polling
future.done()once per pass, with a hardcodedasyncio.sleep(0.01)after every active pass. Two consequences:LocalExecutionBackend).slow(50ms) -> fastchain).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
ProcessPoolExecutorfloor 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_valuesresolves 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 withInvalidStateError); the same future passed positionally and as kwarg completes (guards the dedup).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