Skip to content

kernels-common: stream files during hashing to reduce peak memory - #955

Open
nrenvo wants to merge 2 commits into
huggingface:mainfrom
nrenvo:main
Open

nrenvo wants to merge 2 commits into
huggingface:mainfrom
nrenvo:main

Conversation

@nrenvo

@nrenvo nrenvo commented Nov 1, 2026

Copy link
Copy Markdown

Replace fs::read (entire file buffered into memory) with a 64 KiB streaming loop so large shared libraries or model weights do not need to fit in RAM during digest computation.

Previously, hashing a 1 GB .so file would spike the process's RSS by ~1 GB. With this change, peak memory during hashing is bounded by the 64 KiB buffer regardless of file size.

A new test (hash_variant_streams_large_file_without_changing_digest) verifies that the streaming path produces identical SHA-256 and SHA-512 digests as a reference implementation on a 16 MiB synthetic file.

Replace fs::read (full file into memory) with a 64 KiB streaming loop
so large shared libraries (.so, .gguf) do not need to fit in RAM during
digest computation. Adds a test that verifies the streaming path produces
the same SHA-256 and SHA-512 digests as a reference implementation on a
16 MiB synthetic file.
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

Hi @nrenvo, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/huggingface/kernels/blob/main/CONTRIBUTING.md for more details.

@danieldk danieldk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks a lot! Added two small comments.

Comment on lines +119 to +125
loop {
let count = file
.read(&mut buffer)
.wrap_err_with(|| format!("Cannot read `{}` for hashing", path.display()))?;
if count == 0 {
break;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should probably check for interrupted as well:

An error of the ErrorKind::Interrupted kind is non-fatal and the read operation should be retried if there is nothing else to do.

https://doc.rust-lang.org/beta/std/io/trait.Read.html

So something like:

let count = match file.read(&mut buffer) {
    Ok(n) => n,
    Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
    Err(e) => return Err(e).wrap_err_with(|| format!("Cannot read `{}` for hashing", path.display())),
};


Ok(())
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The test uses multiples of 64KiB of the same value, we'd probably need an odd-sized tail and varying content. I also think it does not need to be very large, just 2-3 64KiB chunks and then something that's not 64KiB (and odd because the digest may work internally with chunks that are have a power-of-2 size)?

`Read::read` may return an error of kind `Interrupted`, which is non-fatal and should be retried. Handle it instead of failing digest computation.

Also rework the streaming digest test to use varying content and an odd-sized tail after three full 64 KiB chunks, so it no longer only exercises buffer-aligned reads.
@nrenvo

nrenvo commented Nov 3, 2026

Copy link
Copy Markdown
Author

Thanks Daniel, good catches! I added the Interrupted retry, and reworked the test to use varying content with an odd-sized tail after three full 64 KiB chunks. Just pushed the update.

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.

2 participants