Skip to content

greedy_scheduler: cache Batches#7193

Merged
alessandrod merged 3 commits intoanza-xyz:masterfrom
alessandrod:greedy-cache-batches
Aug 5, 2025
Merged

greedy_scheduler: cache Batches#7193
alessandrod merged 3 commits intoanza-xyz:masterfrom
alessandrod:greedy-cache-batches

Conversation

@alessandrod
Copy link
Copy Markdown

This avoids a bunch of allocations/deallocations in the hot path.

This must be the 4th time I do this change. Finally committing and PRing so I don't have to do it again next month.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jul 27, 2025

Codecov Report

❌ Patch coverage is 97.87234% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.8%. Comparing base (d3038f3) to head (e9d3efa).
⚠️ Report is 2659 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #7193   +/-   ##
=======================================
  Coverage    82.8%    82.8%           
=======================================
  Files         801      801           
  Lines      363284   363292    +8     
=======================================
+ Hits       300802   300830   +28     
+ Misses      62482    62462   -20     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alessandrod alessandrod requested a review from Copilot July 27, 2025 18:40

This comment was marked as outdated.

@apfitzge
Copy link
Copy Markdown

I'd avoid doing anything like this, because these are just freed by same thread as allocated them, i.e. scheduler. jemalloc should (I think) just push them back into thread-local cache.
is this not the case?

@apfitzge apfitzge self-requested a review July 28, 2025 13:35
self.working_account_set.clear();
// Use zero here to avoid allocating since we are done with `Batches`.
num_sent += self.common.send_batches(&mut batches, 0)?;
num_sent += self.common.send_batches(&mut self.batches, 0)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: except in case of early exit (there shouldn't be any) this will guarantee the batches are empty by the end of each schedule call.

common: SchedulingCommon<Tx>,
working_account_set: ReadWriteAccountSet,
unschedulables: Vec<TransactionPriorityId>,
batches: Batches<Tx>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if allocation/deallocation of batches is an issue, we could put them in the common so that prio_graph variant also gets the benefit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah happy to move it there

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done this now. There are a couple of early returns in schedule(), but SchedulingError is unrecoverable (the banking thread dies), so it's ok

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't read, it doesn't die

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code wise this looks bad, but in practice we error only if the workers get disconnected, which never happens unless the validator gets in some kind of hosed state anyway

}
}

pub fn clear(&mut self) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitant to have a clear on this.

We should never have batches that last longer than a schedule call. If we do, then it's a bug.

Because at the end of each schedule call we send_batches which sends out all non-empty batches, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, I did a mechanical change: saw it in profiles, before this change it was dropping/reallocating, this is functionally equivalent to doing that modulo the churn.

I see your point, in which case we should assert that it's cleared by the time we return.

Comment on lines +48 to +57
for ids in &mut self.ids {
ids.clear();
}
for transactions in &mut self.transactions {
transactions.clear();
}
for max_ages in &mut self.max_ages {
max_ages.clear();
}
for total_cus in &mut self.total_cus {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please ignore co-pilot here 😆

@alessandrod
Copy link
Copy Markdown
Author

I'd avoid doing anything like this, because these are just freed by same thread as allocated them, i.e. scheduler. jemalloc should (I think) just push them back into thread-local cache. is this not the case?

Yeah but the problem is doing all these allocations thousands of times per slot, this currently takes 1/3 of process transactions:

Screenshot 2025-07-28 at 8 52 21 pm

@alessandrod alessandrod force-pushed the greedy-cache-batches branch from eab774e to 76100be Compare August 3, 2025 14:18
@alessandrod alessandrod requested a review from Copilot August 3, 2025 14:43
Copy link
Copy Markdown

Copilot AI left a comment

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 caches Batches instances within the SchedulingCommon struct to avoid repeated allocations and deallocations in the hot path of transaction scheduling. The change moves batch creation from being instantiated per scheduling pass to being reused across scheduling operations.

  • Moves Batches instance from local variable to SchedulingCommon field
  • Updates constructor to accept target_num_transactions_per_batch parameter
  • Removes batches parameter from batch sending methods since it's now internal state

Reviewed Changes

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

File Description
scheduler_common.rs Adds batches field to SchedulingCommon, updates constructor and batch methods
prio_graph_scheduler.rs Removes local Batches creation, uses cached instance from SchedulingCommon
greedy_scheduler.rs Removes local Batches creation, uses cached instance from SchedulingCommon

total_cus: vec![0; num_threads],
}
}

Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_empty method lacks documentation explaining its purpose and when it should be called. Consider adding a docstring to explain that this method is used for debug assertions to verify batches are properly cleared after scheduling.

Suggested change
/// Returns true if all batches are empty and no compute units are allocated.
///
/// This method is intended for use in debug assertions to verify that
/// batches are properly cleared after scheduling. It should be called
/// after batch processing to ensure no residual state remains.

Copilot uses AI. Check for mistakes.
Comment thread core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs Outdated
apfitzge
apfitzge previously approved these changes Aug 4, 2025
Copy link
Copy Markdown

@apfitzge apfitzge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. left small nit and a potential for more clean-up


debug_assert!(
self.common.batches.is_empty(),
"batches must be empty after scheduling"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "batches must start empty for scheduling".
weird to say after scheduling when this check is before?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heh, I initially put the check after scheduling, but then saw the early returns and moved it. Fixed!

@@ -27,17 +27,31 @@ pub struct Batches<Tx> {

impl<Tx> Batches<Tx> {
pub fn new(num_threads: usize, target_num_transactions_per_batch: usize) -> Self {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we now store Batches in common, we don't need to do the 0 target size to avoid allocation on the final send. So the target_num_transactions_per_batch is constant.

We could store it in the Batches itself now, making all the calls to send_* a bit cleaner.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

@alessandrod alessandrod merged commit e2ca749 into anza-xyz:master Aug 5, 2025
41 checks passed
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.

4 participants