Skip to content

Recover scheduler throughput lost to the snapshot-staleness check - #754

Open
wbarnha wants to merge 1 commit into
claude/faust-cython-rewrites-vwltqsfrom
claude/faust-cython-scheduler-throughput
Open

Recover scheduler throughput lost to the snapshot-staleness check#754
wbarnha wants to merge 1 commit into
claude/faust-cython-rewrites-vwltqsfrom
claude/faust-cython-scheduler-throughput

Conversation

@wbarnha

@wbarnha wbarnha commented Aug 4, 2026

Copy link
Copy Markdown
Member

Description

Stacked on #751 — based on claude/faust-cython-rewrites-vwltqs, not master, because it optimises code that only exists there. Retarget to master once #751 lands.

Fixing the record-loss bug reported in review of #751 cost most of the scheduler's speedup. The length-only shortcut it replaced was unsound — it could not tell "unchanged" from "swapped" — but the identity check that replaced it walks the mapping with .items(), allocating a view, an iterator and a tuple per entry. A single-partition topic reaches a pass boundary on every record, so that walk lands squarely on the hot path, and the accelerator fell from 5.7–7.7x over pure Python to 2.2–2.8x.

Two changes get it back without weakening the guarantee.

TopicBuffer._buffers becomes a plain dict

It was an OrderedDict, carrying this comment:

# note: this is a regular dict, but ordered on Python 3.6
# we use this alias to signify it must be ordered.

Dicts have been insertion-ordered by language guarantee since 3.7, and Faust requires 3.10, so the subclass bought nothing. It also cost something: OrderedDict is not an exact dict, so it cannot be walked with PyDict_Next.

Both _snapshot_is_current() methods use PyDict_Next

When the mapping is an exact dict, the check walks it with no allocation at all, comparing borrowed references by identity. Anything that is not an exact dict still goes through .items(), so a custom mapping handed to records_iterator keeps working.

Benchmarks

ns per record, CPython 3.11:

topology python cython before (this PR) after
1t × 1p × 500 525.8 90.2 2.23x 5.83x
1t × 8p × 200 422.5 78.2 5.40x
4t × 8p × 100 336.6 64.3 2.63x 5.23x
8t × 16p × 50 324.8 69.3 2.77x 4.68x

Back to roughly where the unsound version was, with the correctness guarantee kept.

Correctness

Unchanged from #751 and re-verified rather than assumed:

  • Both mid-iteration replacement scenarios (a TopicBuffer swapped in under an existing topic name, and an iterator swapped in under an existing TP) still match the pure-Python iterator exactly.
  • The randomised differential test is at 4000/4000.
  • Full suite passes both ways: 2314 with the extension, 2308 with NO_CYTHON=1.

New tests cover the two things this PR makes load-bearing:

  • a non-dict mapping still iterating correctly, exercising the .items() fallback;
  • _buffers actually being an exact dict, since PyDict_Next silently would not apply otherwise and the speedup would quietly disappear.

Note on the OrderedDict change

This is a small behavioural surface worth a reviewer's eye: anything relying on _buffers being an OrderedDict specifically — move_to_end, popitem(last=False), equality being order-sensitive — would be affected. Nothing in the tree does; _buffers is only assigned, popped and iterated. Flagging it because it is the one change here that is not purely internal to the .pyx.

🤖 Generated with Claude Code

https://claude.ai/code/session_019oX4oGCQGgvPJHabjwfaBk


Generated by Claude Code

Fixing the record-loss bug in #751 cost most of the scheduler's speedup:
the length-only shortcut it replaced was unsound, but the identity check
that replaced it walks the mapping with .items(), allocating a view, an
iterator and a tuple per entry. A single-partition topic reaches a pass
boundary on every record, so that walk lands squarely on the hot path and
the accelerator fell from 5.7-7.7x over pure Python to 2.2-2.8x.

Two changes get it back without weakening the guarantee.

TopicBuffer._buffers becomes a plain dict. It was an OrderedDict, with a
comment noting it was "a regular dict, but ordered on Python 3.6" -- dicts
have been insertion-ordered by language guarantee since 3.7 and Faust
requires 3.10, so the subclass bought nothing. It also cost something:
OrderedDict is not an exact dict, so it cannot be walked with PyDict_Next.

Both _snapshot_is_current() methods then use PyDict_Next when the mapping
is an exact dict, walking it with no allocation at all and comparing
borrowed references by identity. Anything that is not an exact dict still
goes through .items(), so a custom mapping handed to records_iterator
keeps working.

    topology            python    cython   before    after
    1t x  1p x 500      525.8n     90.2n    2.23x    5.83x
    1t x  8p x 200      422.5n     78.2n       --    5.40x
    4t x  8p x 100      336.6n     64.3n    2.63x    5.23x
    8t x 16p x  50      324.8n     69.3n    2.77x    4.68x

The correctness properties from #751 are unchanged and re-verified: both
mid-iteration replacement scenarios still match the pure-Python iterator,
and the randomised differential test is at 4000/4000.

Adds tests for the non-dict mapping fallback and for _buffers being an
exact dict, since that is now load-bearing rather than incidental.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oX4oGCQGgvPJHabjwfaBk
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