Skip to content

Fewer scope schedules from progress tracking - #822

Draft
antiguru wants to merge 5 commits into
TimelyDataflow:masterfrom
antiguru:progress-fewer-wakeups
Draft

antiguru wants to merge 5 commits into
TimelyDataflow:masterfrom
antiguru:progress-fewer-wakeups

Conversation

@antiguru

@antiguru antiguru commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

With nested regions, most scope schedules come from progress bookkeeping rather than operator work. Scheduling a scope also schedules every scope that contains it, so extra progress activations cost once per nesting level. This PR removes three sources of such activations:

  • A subgraph applies the progress its child scopes report before it returns, instead of on a self-requested later activation. A change no longer climbs one nesting level per worker step. A subgraph with children yet to observe their new frontiers now reports itself incomplete, so a dataflow is not dropped before a probe sees its final frontier.
  • A worker applies its own progress updates directly and broadcasts only to peers, through the new Allocate::broadcast_peers. Progcaster::send takes the batch to add the local copy to.
  • The progress channel's puller no longer schedules its scope again after being drained (new Pull::quiet). Data channels keep this behavior.

Against master, medians of six runs on an x86 machine with 32 cores:

  • A benchmark of seven nested regions with an iterative scope, run closed-loop, completes 10% more rounds per second with one worker, 7.5% with four, and 3% with sixteen, and p99 round latency drops by 19%, 18%, and 6%.
  • event_driven 100 10 completes 20% more rounds per second with one worker, and 4% fewer with sixteen.
  • barrier takes 8% longer with four workers, and is unchanged with one and sixteen.

The barrier regression comes mostly from applying progress early in the dataflow root, which has no parent to report to. Limiting it to nested scopes removes most of that regression but gives up part of the gain with four workers.

Posted by Claude Code.

🤖 Generated with Claude Code

antiguru and others added 5 commits September 25, 2026 17:59
`Allocate::broadcast_peers` allocates a broadcast channel that delivers to
every worker except the sender, while the sender's puller still receives what
other workers push. The default implementation drops the sender's own pusher
from `allocate`, and the zero-copy allocators share their existing broadcast
paths, so remote processes and local peers still receive one serialization.

`Pull::quiet` stops a puller from recording an event for its channel once it
has been drained. By default, a drained channel that yielded messages
schedules its recipient once more, which a recipient that finishes its work in
the draining call does not need. The default implementation does nothing, and
the counting and zero-copy pullers forward it to the pullers they wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A subgraph collects the progress its child scopes report while scheduling
them, and previously applied it only on its next activation, which it
requested for itself. A change deep in nested scopes therefore climbed one
level per worker step, and each step scheduled every enclosing scope again.

The subgraph now propagates these updates before it returns, so that the
effect on its outputs reaches the parent in the same call. Children whose
frontiers change as a result still run on the next activation, which the
subgraph requests when any remain.

Propagating early can drain the tracker while such children have yet to
observe their new frontiers. A subgraph with pending children now reports
itself incomplete, so that the worker does not drop the dataflow before a
probe sees its final frontier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Progcaster` broadcast each batch of progress updates to all workers,
including the sender. The sender's copy went through its own channel, and on
arrival activated the scope and every scope around it on a later step.

The progress channel now excludes the sender, and `Progcaster::send` adds
the sender's copy to the caller's batch of received updates. Correctness
needs each worker's updates to reach every worker in the order sent, and
batches applied locally keep that order. `Subgraph::schedule` propagates the
copy before it returns, together with the progress of its child scopes.

Progress logging records the local delivery as a receive, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A counting puller records an event for its channel once it has been drained
of messages, so that the channel's recipient runs once more. For progress
channels the recipient is `Subgraph::schedule`, which applies everything it
receives in the same call and activates itself when work remains. The extra
activation scheduled the scope and all of its enclosing scopes to find
nothing to do.

The progress puller is now quiet. Data channels keep the event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applying progress before a subgraph returns lets its effect reach the parent
in the same call. A dataflow root has no parent, so propagating its own sent
updates early only splits one propagation per step into two, one for its own
updates and one for those that arrive from other workers. In a loop at the
root, `barrier` with four workers took 8% longer than before this series.

A dataflow root now leaves the updates for its next activation, which it
requests, and applies them together with those it receives by then. Nested
scopes still propagate before they return. The root counts unapplied updates
as pending work when it reports whether it is complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant