2424standing in for TensorStore's chunk index.
2525
2626Byte runs from every tensor a process needs in one file are coalesced under a
27- single bounded-over-read policy: a `max_over_read_ratio` cap on
28- `block_size / needed_bytes` keeps cross-host egress from blowing up to
29- N x file_size on inner-dim sharding. Each coalesced block is then split at a
27+ bounded-over-read policy: a `max_over_read_ratio` cap on
28+ `block_size / needed_bytes`. By default the cap is picked per file from the
29+ planned runs themselves -- no over-read when the sharding already maps to few
30+ contiguous reads, whole-span reads when the plan would otherwise fragment
31+ into a very large number of small strided requests (a warning then reports
32+ the over-read cost of that sharding). Each coalesced block is then split at a
3033fixed chunk size and the chunks are read concurrently, with total in-flight
3134bytes bounded by the shared restore budget
3235(`MemoryOptions.read_concurrent_bytes`). Peak host memory beyond the
7780# (`max_over_read_ratio`) and the chunk size (`read_chunk_bytes`); the
7881# in-flight budget comes from `MemoryOptions.read_concurrent_bytes`.
7982#
80- # `_DEFAULT_MAX_OVER_READ_RATIO` caps `block_size / needed_bytes` for any
81- # coalesced read; a host's bytes pulled from one file are bounded at
82- # `ratio * ideal_bytes`. The default trades up to 1x over-read for fewer
83- # requests; the row-parallel inner-dim worst case (which would otherwise
84- # collapse to "read the whole file on every host") is bounded at 2x and the
85- # partitioner emits more, smaller reads instead .
86- _DEFAULT_MAX_OVER_READ_RATIO = 2.0
83+ # With `max_over_read_ratio` unset, each file's ratio is picked from its
84+ # planned runs (`_auto_over_read_ratio`): 1.0 when coalescing at the gap floor
85+ # already leaves at most this many reads -- large contiguous shards gain
86+ # nothing from over-reading -- and the file's `span / needed` otherwise, which
87+ # collapses a fragmented (strided inner-dim) plan into whole-span reads
88+ # instead of an unbounded number of small requests .
89+ _AUTO_MAX_READS_PER_FILE = 1024
8790# Always coalesce runs separated by less than this gap, regardless of the
8891# over-read ratio. Below roughly a page the per-read overhead -- a syscall
8992# locally, an RTT on object storage -- dwarfs the cost of reading the gap, so a
105108# streams.
106109_DEFAULT_READ_CHUNK_BYTES = 128 * 1024 * 1024 # 128 MiB
107110
108- # When the over-read cap fragments a load into many small reads -- the strided
109- # inner-dim regime -- hint the user toward `max_over_read_ratio`. The warning
110- # fires only when reads-per-file exceeds this factor *and* the achieved
111- # over-read stayed near 1x (the cap blocked coalescing, rather than the load
112- # genuinely spanning many files).
113- _FRAGMENTED_READ_WARN_FACTOR = 8
114- _FRAGMENTED_OVER_READ_CEILING = 1.2
111+ # Heavy over-read under the automatic ratio means the target sharding
112+ # fragments the checkpoint's byte layout and whole spans were read to keep the
113+ # request count bounded. Surface it so the extra bytes are diagnosable: the
114+ # sharding, not the loader, is the thing to change. Over-read from gap-floor
115+ # merges alone stays well below this.
116+ _OVER_READ_WARN_RATIO = 1.5
115117
116118
117119def _get_dtypes () -> dict [str , Any ]:
@@ -378,6 +380,44 @@ def _partition_runs(
378380 return blocks
379381
380382
383+ def _auto_over_read_ratio (runs : Sequence [tuple [int , int ]]) -> float :
384+ """Picks one file's over-read ratio from its planned run geometry.
385+
386+ No single ratio suits every sharding. A process reading a few large
387+ contiguous runs gains nothing from merging across gaps -- that only
388+ wastes bytes. One reading thousands of tiny strided runs is better off
389+ reading the whole span, but the ratio where the plan collapses is the
390+ file's `span / needed`, which shifts with sharding and topology. Values
391+ in between buy more requests *and* more waste, so the choice is binary
392+ and the runs themselves decide it.
393+
394+ Args:
395+ runs: `(offset, length)` byte runs planned for one file; need not be sorted.
396+ Assumed non-overlapping, as `index_domain_to_byte_runs` produces.
397+
398+ Returns:
399+ `1.0` when merging only sub-floor gaps already leaves at most
400+ `_AUTO_MAX_READS_PER_FILE` reads; otherwise the file's whole-span ratio
401+ `span / needed`, which bounds every block and collapses uniform strided
402+ plans into whole-span reads.
403+ """
404+ if not runs :
405+ return 1.0
406+ ordered = sorted (runs )
407+ needed = 0
408+ num_reads = 1
409+ prev_end = ordered [0 ][0 ]
410+ for offset , length in ordered :
411+ needed += length
412+ if offset - prev_end > _DEFAULT_MIN_COALESCE_GAP :
413+ num_reads += 1
414+ prev_end = max (prev_end , offset + length )
415+ if num_reads <= _AUTO_MAX_READS_PER_FILE :
416+ return 1.0
417+ span = prev_end - ordered [0 ][0 ]
418+ return max (1.0 , span / needed )
419+
420+
381421def _replicated_sharding () -> jax .sharding .Sharding :
382422 """A `NamedSharding` that fully replicates an array across all devices."""
383423 mesh = jax .sharding .Mesh (np .asarray (jax .devices ()), ("_safetensors_replica" ,))
@@ -427,7 +467,7 @@ class _ReadStats(NamedTuple):
427467
428468def _plan_chunk_reads (
429469 reads : Sequence [_Read ],
430- max_over_read_ratio : float ,
470+ max_over_read_ratio : float | None ,
431471 chunk_bytes : int ,
432472) -> tuple [list [_ChunkRead ], _ReadStats ]:
433473 """Plans the ranged reads serving one file's byte runs.
@@ -442,15 +482,17 @@ def _plan_chunk_reads(
442482 Args:
443483 reads: The byte runs to serve, each with its destination buffer.
444484 max_over_read_ratio: Upper bound on `block_size / needed_bytes` per block.
485+ `None` picks the ratio from this file's runs (`_auto_over_read_ratio`).
445486 chunk_bytes: Maximum size of one ranged read. Must be positive and at most
446487 the in-flight byte budget, so any single chunk can be reserved.
447488
448489 Returns:
449490 The chunk reads covering every run, and the file's read accounting.
450491 """
451- blocks = _partition_runs (
452- [(r .offset , r .length ) for r in reads ], max_over_read_ratio
453- )
492+ runs = [(r .offset , r .length ) for r in reads ]
493+ if max_over_read_ratio is None :
494+ max_over_read_ratio = _auto_over_read_ratio (runs )
495+ blocks = _partition_runs (runs , max_over_read_ratio )
454496 chunks : list [_ChunkRead ] = []
455497 for block in blocks :
456498 members = block .members # Sorted by run offset (see `_partition_runs`).
@@ -531,7 +573,7 @@ async def _read_file(
531573 path : Path ,
532574 reads : Sequence [_Read ],
533575 byte_budget : limits .LimitInFlightBytes ,
534- max_over_read_ratio : float ,
576+ max_over_read_ratio : float | None ,
535577 chunk_bytes : int ,
536578) -> _ReadStats :
537579 """Serves all of one file's byte runs with concurrent ranged reads.
@@ -541,6 +583,7 @@ async def _read_file(
541583 reads: The byte runs this host needs from the file.
542584 byte_budget: The shared in-flight byte limiter (shared across files).
543585 max_over_read_ratio: Upper bound on `block_size / needed_bytes` per block.
586+ `None` picks the ratio from this file's runs (`_auto_over_read_ratio`).
544587 chunk_bytes: Maximum size of one ranged read.
545588
546589 Returns:
@@ -716,54 +759,45 @@ def _record_read_stats(stats: Sequence[_ReadStats]) -> None:
716759 )
717760
718761
719- def _warn_if_reads_fragmented (
762+ def _warn_if_over_read (
720763 stats : Sequence [_ReadStats ],
721- num_files : int ,
722- max_over_read_ratio_is_default : bool ,
764+ max_over_read_ratio_is_auto : bool ,
723765) -> None :
724- """Logs a one-shot hint when the over- read cap fragmented the load .
766+ """Logs a one-shot hint when the load read far more bytes than it needed .
725767
726- In the strided inner-dim regime the `max_over_read_ratio` cap rejects merges
727- and the load degrades into many small ranged reads -- RTT-bound and slow on
728- object storage. This surfaces that case so a slow load is diagnosable without
729- profiling. It fires once, only on the primary process, and only when the user
730- is on the default ratio (someone who set the knob has already weighed the
731- tradeoff). The signal is a high reads-per-file count together with an achieved
732- over-read near 1x -- exactly "the cap blocked coalescing" rather than "the
733- load genuinely spans many files."
768+ Under the automatic per-file ratio, heavy over-read happens only when the
769+ target sharding maps to many small strided runs per process and whole spans
770+ were read instead to keep the request count bounded. The extra bytes are
771+ the cost of that sharding's byte layout, not a loader inefficiency, so the
772+ actionable advice is a sharding whose per-process ranges are contiguous.
773+ Fires once, only on the primary process, and only for the automatic ratio
774+ (someone who set the knob has already weighed the tradeoff).
734775
735776 Args:
736777 stats: Per-file read accounting from each file's reads.
737- num_files: Number of distinct files the load read from.
738- max_over_read_ratio_is_default: Whether the user left `max_over_read_ratio`
739- unset; only then is "raise the ratio" actionable advice.
778+ max_over_read_ratio_is_auto: Whether the user left `max_over_read_ratio`
779+ unset (the per-file automatic choice).
740780 """
741- if not max_over_read_ratio_is_default or num_files == 0 :
781+ if not max_over_read_ratio_is_auto :
742782 return
743783 if multihost .process_index () != 0 :
744784 return
745785 total_needed = sum (s .needed_bytes for s in stats )
746786 total_read = sum (s .read_bytes for s in stats )
747- total_reads = sum (s .num_reads for s in stats )
748787 if total_needed == 0 :
749788 return
750- achieved_over_read = total_read / total_needed
751- if (
752- total_reads <= _FRAGMENTED_READ_WARN_FACTOR * num_files
753- or achieved_over_read >= _FRAGMENTED_OVER_READ_CEILING
754- ):
789+ over_read = total_read / total_needed
790+ if over_read < _OVER_READ_WARN_RATIO :
755791 return
756792 logging .warning (
757- "Safetensors load issued %d ranged reads for %s across %d file(s) "
758- " (achieved over-read %.2fx ). The target sharding produces strided reads "
759- " that the default max_over_read_ratio=%.1f keeps fragmented; raising "
760- " SafetensorsOptions.max_over_read_ratio trades bounded over-read for far "
761- " fewer reads (notably faster on object storage) ." ,
762- total_reads ,
793+ "Safetensors load read %s to serve %s of needed bytes (%.1fx "
794+ " over-read). The target sharding needs many small strided ranges per "
795+ " process, so whole spans were read instead to keep the request count "
796+ " bounded. A sharding whose per-process ranges are contiguous (e.g. "
797+ " partitioning the leading dimension) avoids the extra bytes ." ,
798+ humanize . naturalsize ( total_read , binary = True ) ,
763799 humanize .naturalsize (total_needed , binary = True ),
764- num_files ,
765- achieved_over_read ,
766- _DEFAULT_MAX_OVER_READ_RATIO ,
800+ over_read ,
767801 )
768802
769803
@@ -930,11 +964,6 @@ async def _load(
930964 """Background load phase: plans reads, reads files, assembles arrays."""
931965 context = context_lib .get_context ()
932966 opts = context .safetensors_options
933- max_over_read_ratio = (
934- opts .max_over_read_ratio
935- if opts .max_over_read_ratio is not None
936- else _DEFAULT_MAX_OVER_READ_RATIO
937- )
938967 # In-flight read bytes reuse the shared restore knob,
939968 # `MemoryOptions.read_concurrent_bytes` (the same limit TensorStore restore
940969 # uses). `None` means "no limit" there, but the chunk sizing here needs a
@@ -1005,7 +1034,7 @@ async def load_file(file_path: Path) -> _ReadStats:
10051034 file_path ,
10061035 reads_by_file [file_path ],
10071036 byte_budget ,
1008- max_over_read_ratio ,
1037+ opts . max_over_read_ratio ,
10091038 chunk_bytes ,
10101039 )
10111040 # Drop the read plan so each shard's host memory can be released as
@@ -1017,7 +1046,5 @@ async def load_file(file_path: Path) -> _ReadStats:
10171046
10181047 stats = await asyncio .gather (* map (load_file , list (reads_by_file )))
10191048 _record_read_stats (stats )
1020- _warn_if_reads_fragmented (
1021- stats , len (builds_by_file ), opts .max_over_read_ratio is None
1022- )
1049+ _warn_if_over_read (stats , opts .max_over_read_ratio is None )
10231050 return {name : arrays [name ] for name in names }
0 commit comments