Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace mmap with file io in merkle tree hash calculation #3547

Merged
merged 7 commits into from
Feb 4, 2025

Conversation

HaoranYi
Copy link

@HaoranYi HaoranYi commented Nov 8, 2024

Problem

We have noticed that during hash calculation, the performance of block producing
process degrades. Part of it is due to the stress that was put on memory and
disk/io from the accounts hash threads.

When we computes merkle tree hash, we use mmap to store the extracted accounts'
hashes. Mmap is heavy on resource usages, such as memory and disk/io, and put a
stress on the whole system.

In this PR, we propose to switch to use file io, less resource stressful, for
merkle tree hash computation.

Studies on mainnet with this PR shows that file-io use less memory and put less
stress on disk io. The "pure" hash computation time with file io is a little
longer than with mmap. But, we also save the mmap drop time with file io. And
the saving from mmap drop time is more to offset the extra time spent on hash
calculation. Thus, it makes the overall time for computing the hash smaller.

Note that there is an upcoming lattice hash feature, which will be the ultimate
solution to hash, i.e. it remove all the merkle tree hash calculation. However,
before that feature is activated, we could still use this PR as an interim
enhancement for merkle tree hash computation.

Summary of Changes

  • replace mmap with file io for merkle tree hash calcuation

Fixes #

@HaoranYi HaoranYi marked this pull request as draft November 8, 2024 17:22
@HaoranYi HaoranYi changed the title cli hash bins Replace mmap with file io in merkle tree hash calculation Nov 8, 2024
@HaoranYi HaoranYi force-pushed the cli_hash_bins branch 2 times, most recently from f8d3ca6 to d9b0c8a Compare November 9, 2024 02:50
@HaoranYi
Copy link
Author

HaoranYi commented Nov 11, 2024

Performance comparison

mmap-64Kbins (pink)
mmap-4kbins (orange)
file/io - 64kbins (blue)

image

@HaoranYi
Copy link
Author

HaoranYi commented Nov 11, 2024

Summary

  • smaller bins use more memory and a bit slower in hashtime, but is faster on drop time.
  • file-io use less memory and but requires a bit longer time to compute hash but the save over drop time is larger than mmap, which makes it overall faster than mmap with the same number of bins.
  • file-io use less disk/io.

@HaoranYi
Copy link
Author

rebase to pick up #3589

@HaoranYi HaoranYi force-pushed the cli_hash_bins branch 2 times, most recently from beb8a1f to f6c5c61 Compare November 15, 2024 15:41
@@ -1160,16 +1255,15 @@ impl AccountsHasher<'_> {
let binner = PubkeyBinCalculator24::new(bins);

// working_set hold the lowest items for each slot_group sorted by pubkey descending (min_key is the last)
let (mut working_set, max_inclusive_num_pubkeys) = Self::initialize_dedup_working_set(
let (mut working_set, _max_inclusive_num_pubkeys) = Self::initialize_dedup_working_set(
Copy link
Author

Choose a reason for hiding this comment

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

max_inclusive_num_pubkeys is an estimate of the upper bound for the hash file size. It is only required when we use mmap - creating an mmap requires to specify the initial size, which could be over allocated too. After switching to file writer, we don't need this any more.

@HaoranYi HaoranYi marked this pull request as ready for review January 15, 2025 16:11
@@ -619,6 +540,180 @@ impl AccountsHasher<'_> {
(num_hashes_per_chunk, levels_hashed, three_level)
}

// This function is called at the top lover level to compute the merkle. It

Choose a reason for hiding this comment

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

this fn is copied from fn compute_merkle_root_from_slices<'b, F, T>(. Do we need the other fn still?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, we do.
compute_merkle_root_from_slices is still used when we compute merkle tree at 2 level and above, where we have all the data in memory already.

The comments here may be helpful to understand.

// This function is called at the top level to compute the merkle hash. It
// takes a closure that returns an owned vec of hash data at the leaf level
// of the merkle tree. The input data for this bottom level are read from a
// file. For non-leaves nodes, where the input data is already in memory, we
// will use `compute_merkle_root_from_slices`, which is a version that takes
// a borrowed slice of hash data instead.

.unwrap();

let mut result_bytes: Vec<u8> = vec![];
data.read_to_end(&mut result_bytes).unwrap();

Choose a reason for hiding this comment

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

note that this allocates what could be a large Vec. Depends on hashing tuning parameters (num bins, etc.)

Choose a reason for hiding this comment

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

we have found in pop testing that large mem allocations can cause us to oom.

Copy link
Author

Choose a reason for hiding this comment

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

Good point.

We could add a cap on how many bytes we load here. The downside is that we may need to call this function multiple times.

Copy link
Author

Choose a reason for hiding this comment

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

I commit a change to cap the file read buffer size to 64M.

@jeffwashington
Copy link

I'm certainly happy and supportive with the idea of the change here. What is the estimate of the lattice hash creation? Even then, we may need to use this code to create a full lattice hash from scratch initially or for full accountsdb verification.


// initial fetch - could return entire slice
let data_bytes = get_hash_slice_starting_at_index(0);
let data: &[T] = bytemuck::cast_slice(&data_bytes);
Copy link
Author

Choose a reason for hiding this comment

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

@jeffwashington The caller cast the data to the generic type T here.

for _k in 0..end {
if data_index >= data_len {
// we exhausted our data, fetch next slice starting at i
data_bytes = get_hash_slice_starting_at_index(i);
Copy link
Author

Choose a reason for hiding this comment

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

@jeffwashington With a cap only how many bytes we load each time, we may find that we will need to load the time more times.

Copy link

@brooksprumo brooksprumo left a comment

Choose a reason for hiding this comment

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

I still need to go through the merkle tree code.

Heh, trying to fit the file io impl into the existing mmap api is really clunky... That's probably the right choice though, as this code will go away after the accounts lt hash is activated.

@brooksprumo brooksprumo self-requested a review January 17, 2025 16:13
@HaoranYi
Copy link
Author

@jeffwashington, @brooksprumo I pushed several commits to address your reviews.
Can you take a look again?
FYI I have also restarted my node to test with the updated commits.

Copy link

@brooksprumo brooksprumo left a comment

Choose a reason for hiding this comment

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

There are some suboptimal pieces here, but doing it "right" would be much more intrusive. At the same time, nothing here seems egregiously bad. So assuming the mnb test runs show good results, I'd be on board with the changes.

@HaoranYi
Copy link
Author

Performance Comparison

  • blue: this PR
  • red: canary-mc3

Result summary

  1. Much less time for "drop_hash" files. (mmap is expensive to drop)
  2. A small increase on "hash" calc time.
  3. Overall less time for "total_hash".
  4. Less total "used" memory too.
  5. More disk spike during hash calc.

Overall, this PR looks like a "win" on performance and resource usage.

hash_time: drop_us

image

hash_time: hash_us

image

hash_time:total_us

image

disk: rw

image

mem

image

@brooksprumo
Copy link

  1. Much less time for "drop_hash" files. (mmap is expensive to drop)

I still don't understand why this PR takes 5 seconds to drop the file io cache files. Do you have more info here?

  1. A small increase on "hash" calc time.
  2. Overall less time for "total_hash".

Lower overall is definitely the important one, esp if it's around one hundred seconds!

  1. Less total "used" memory too.

The memory usage doesn't look any different to me. Neither node is at steady state, so comparing absolute used bytes isn't particularly useful here. That said, the deltas on each seem about the same, so that's good. Especially since the current mmap version won't show up in used bytes at all.

  1. More disk spike during hash calc.

Which disk metric specifically is this? It is likely fine. Also the canary nodes do vary quite widely between each other even.

Overall, this PR looks like a "win" on performance and resource usage.

Definitely agree 🚀🚀🚀

Copy link

@brooksprumo brooksprumo left a comment

Choose a reason for hiding this comment

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

:shipit:

Code looks good to me. Overall this is a win. I'd love to get answers to my specific questions from above, too. Please get sign off from jwash/alessandro too before merging.

@HaoranYi
Copy link
Author

HaoranYi commented Jan 25, 2025

  1. I believe that 5 seconds are spent on deleting the temp directory and all hash files ~ total number is 64K. We can probably use io_uring to speed up the deleting.
  2. The disk-metrics is "time_io_ms" including both read and write time.

Copy link

@jeffwashington jeffwashington left a comment

Choose a reason for hiding this comment

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

lgtm

@HaoranYi HaoranYi merged commit cb62abf into anza-xyz:master Feb 4, 2025
47 checks passed
@HaoranYi HaoranYi deleted the cli_hash_bins branch February 4, 2025 15: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.

3 participants