fix: auto-size multipart parts for File uploads (from_local_sync + hashed from_local) - #1480
Merged
Conversation
…shed from_local) File.from_local_sync uploaded via raw fs.open(remote, "wb"), and File.from_local with a HashMethod streamed through storage.put_stream with the writer's fixed 10 MiB buffer. On obstore-backed filesystems each buffer becomes one multipart part, so a single file was capped at ~97.6 GiB (10,000-part S3 limit) -- the same class of bug #1462 fixed for Dir.from_local_sync, at 2x the threshold. - File.from_local_sync (no hash / precomputed hash): syncify(storage.put), which auto-sizes the part per file. Mirrors Dir.from_local_sync (#1462). - put_stream gains size_hint=; when the hinted size would exceed the part limit at the default buffer, chunk_size is scaled up. Smaller streams are untouched. - New _upload_hashed helper shared by from_local and from_local_sync (via syncify) so both hashed paths pass the file size as size_hint. DataFrame is intentionally left alone: its encoders write through pandas/pyarrow file-likes with no size known up front, and a single >97 GiB in-memory parquet frame is not a realistic path today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ketan Umare <kumare3@users.noreply.github.com>
…HashMethod Exercises File.from_local_sync (no hash / streaming sha256 / precomputed) and File.from_local(hash_method=...) end to end, then downloads each object and verifies size + sha256. Verified on demo.hosted.unionai.cloud. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ketan Umare <kumare3@users.noreply.github.com>
mypy flagged PrecomputedValue as abstract (HashMethod.reset has an empty body). Drop the private _hashing_io imports: implement a small streaming Sha256Accumulator in the example and pass the precomputed hash as a str. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ketan Umare <kumare3@users.noreply.github.com>
cosmicBboy
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1462, which fixed
Dir.from_local_sync.Filehad the same class of bug in two places.Summary
On obstore-backed filesystems (s3/gs/abfs), fsspec's
BufferedFile("wb")and our_open_obstore_bypassboth write with a fixed 10 MiB buffer, and obstore uploads one multipart part per buffer. That caps a single object at ~97.6 GiB (10,000 parts × 10 MiB) — 2× theDirthreshold from #1462, but the same failure (InvalidRange/ part-limit rejection atCompleteMultipartUpload). The 160 GiB file in #1462 would also fail throughFile.from_local_sync.Two paths were affected:
File.from_local_sync(non-hash and precomputed-hash branches) uploaded via rawfs.open(remote_path, "wb")+shutil.copyfileobj— never touched the obstore-awarestorage.putthat auto-sizes parts.File.from_local/from_local_syncwith aHashMethodstreamed throughstorage.put_stream, which had no way to know the total size and so kept the fixed 10 MiB part. This one affected the async path too.Changes
File.from_local_sync: route throughsyncify(storage.put), mirroringDir.from_local_sync(fix: auto-size multipart parts for Dir.from_local_sync uploads #1462) andDir.download_sync.storage.put_stream(..., size_hint=): new optional kwarg. If the hinted size would blow past_MAX_SAFE_PARTSat the default buffer,chunk_sizeis scaled up via the existing_compute_upload_chunk_size. Streams below ~88 GiB are byte-for-byte unchanged (no override is passed). Explicitchunk_size=/buffer_size=kwargs still win._upload_hashed()helper in_file.py, shared byfrom_localandfrom_local_sync(viasyncify), passingos.path.getsize(local_path)assize_hint. This also collapses the two duplicated hashed-upload implementations into one.10 * 2**20literal into_STREAM_WRITE_BUFFER_SIZE.Not changed (on purpose)
DataFrame: encoders write viadf.to_parquet(storage_options=…)/pq.write_table(filesystem=…), i.e. pandas/pyarrow drive an fsspec file-like with no size known up front, so there's no clean place to plumb a hint. A single >97 GiB parquet file written from one in-memory frame isn't a realistic path today, so leaving it.File.download_sync: still rawfs.get. Not a correctness issue (downloads have no part cap); it just skips the parallel reader. Can follow up separately if wanted.File.open_sync("wb"): stream API with no size knowledge; callers can passbuffer_size=.Tests
from_local_syncremote tests (they mockedfs.open) to assertstorage.put/storage.put_stream(size_hint=…)are used and the hash is still computed from the streamed bytes.from_localhashed test assertingsize_hintis passed.put_streamtest: no hint / small hint → nochunk_sizeoverride; 200 GiB hint →chunk_size> default andceil(size / chunk_size) <= _MAX_SAFE_PARTS.🤖 Generated with Claude Code