1818
1919* **dense** (``type`` missing or ``"dense"``): ``/<split>`` is an ``(N, D)``
2020 matrix of ``INT8`` or ``FLOAT32`` values. One-byte unsigned integers are
21- reinterpreted as ``INT8`` to match the evaluator's ``ALPHA_I8`` reads.
21+ numerically converted to ``INT8`` by HDF5 to match the evaluator's
22+ ``ALPHA_I8`` reads (out-of-range values saturate at 127).
2223* **sparse** (``type="sparse"``): ``/<split>`` is a flat ``INT8`` byte stream.
2324 Each record is ``uint32 len | uint32 ids[len] | float32 vals[len]`` in
2425 little-endian order. ``/<split>_offsets`` is an optional ``UINT64`` array of
3839import numpy as np
3940
4041DEFAULT_CHUNK_BYTES = 64 * 1024 * 1024
42+ _OFFSET_INDEX_BUDGET_BYTES = DEFAULT_CHUNK_BYTES
43+ _OFFSET_VALIDATION_ROWS = 1024 * 1024
4144_LEN_STRUCT = struct .Struct ("<I" )
4245_LEN_SIZE = _LEN_STRUCT .size
4346
@@ -175,27 +178,21 @@ def _read_metadata(self) -> DatasetMetadata:
175178 )
176179 offsets_name = self .split + "_offsets"
177180 if offsets_name in f :
178- offsets = np .asarray (f [offsets_name ][()], dtype = np .uint64 )
179- if len (offsets ) < 1 :
180- raise DatasetFormatError (
181- "'/%s' must contain at least the trailing sentinel" % offsets_name
182- )
183- if int (offsets [0 ]) != 0 :
184- raise DatasetFormatError (
185- "'/%s' must start at byte offset 0" % offsets_name
186- )
187- if np .any (offsets [1 :] < offsets [:- 1 ]):
188- raise DatasetFormatError (
189- "'/%s' must be non-decreasing" % offsets_name
190- )
191- if int (offsets [- 1 ]) != int (dset .shape [0 ]):
192- raise DatasetFormatError (
193- "'/%s' sentinel (%d) does not match '/%s' size (%d)"
194- % (offsets_name , int (offsets [- 1 ]), self .split , int (dset .shape [0 ]))
195- )
196- count = len (offsets ) - 1
181+ offsets_dset = f [offsets_name ]
182+ offset_count = _validate_offsets (
183+ offsets_dset , int (dset .shape [0 ]), offsets_name
184+ )
185+ count = offset_count - 1
197186 has_offsets = True
198- self ._offsets = offsets
187+ offset_bytes = offset_count * np .dtype (np .uint64 ).itemsize
188+ if offset_bytes <= _OFFSET_INDEX_BUDGET_BYTES :
189+ self ._offsets = np .asarray (offsets_dset [()], dtype = np .uint64 )
190+ else :
191+ warnings .append (
192+ "sparse '/%s' exceeds the %d-byte retained-index budget; "
193+ "using the bounded sequential decoder"
194+ % (offsets_name , _OFFSET_INDEX_BUDGET_BYTES )
195+ )
199196 else :
200197 warnings .append (
201198 "sparse dataset has no '/%s_offsets'; records are counted by scanning"
@@ -238,7 +235,7 @@ def iter_dense_blocks(
238235 total = int (dset .shape [0 ])
239236 for start in range (0 , total , block_rows ):
240237 end = min (start + block_rows , total )
241- yield start , _evaluator_dense_view (dset [ start : end ] )
238+ yield start , _read_evaluator_dense (dset , slice ( start , end ) )
242239
243240 def read_dense_rows (self , indices : Sequence [int ]) -> np .ndarray :
244241 """Read selected rows from a dense matrix (indices must be sorted)."""
@@ -250,7 +247,7 @@ def read_dense_rows(self, indices: Sequence[int]) -> np.ndarray:
250247 if np .any (idx [1 :] <= idx [:- 1 ]):
251248 raise ValueError ("dense row indices must be strictly increasing" )
252249 dset = self ._file [self .split ]
253- return _evaluator_dense_view (dset [ idx .tolist ()] )
250+ return _read_evaluator_dense (dset , idx .tolist ())
254251
255252 # -- sparse --------------------------------------------------------
256253 def iter_sparse_records (
@@ -264,7 +261,13 @@ def iter_sparse_records(
264261 if self ._offsets is not None :
265262 yield from self ._iter_sparse_with_offsets (max_chunk_bytes )
266263 else :
267- yield from self ._iter_sparse_without_offsets (max_chunk_bytes )
264+ # With no index, or with an index too large to retain, decode the
265+ # byte stream directly. An oversized index still supplies the
266+ # record count, which is cross-checked by the decoder.
267+ yield from self ._iter_sparse_without_offsets (
268+ max_chunk_bytes ,
269+ expected_count = self .metadata .count if self .metadata .has_offsets else None ,
270+ )
268271
269272 def _iter_sparse_with_offsets (
270273 self , max_chunk_bytes : int
@@ -297,7 +300,7 @@ def _iter_sparse_with_offsets(
297300 start = end
298301
299302 def _iter_sparse_without_offsets (
300- self , max_chunk_bytes : int
303+ self , max_chunk_bytes : int , expected_count : Optional [ int ] = None
301304 ) -> Iterator [Tuple [int , np .ndarray , np .ndarray ]]:
302305 dset = self ._file [self .split ]
303306 total_bytes = int (dset .shape [0 ])
@@ -334,6 +337,11 @@ def _iter_sparse_without_offsets(
334337 "sparse byte stream has %d trailing byte(s) that do not form a "
335338 "complete record" % len (buffer )
336339 )
340+ if expected_count is not None and index != expected_count :
341+ raise DatasetFormatError (
342+ "sparse decoded record count (%d) does not match '/%s_offsets' (%d)"
343+ % (index , self .split , expected_count )
344+ )
337345
338346 def read_sparse_rows (
339347 self , indices : Sequence [int ], max_chunk_bytes : int = DEFAULT_CHUNK_BYTES
@@ -357,12 +365,43 @@ def read_sparse_rows(
357365 return out
358366
359367
360- def _evaluator_dense_view (values ) -> np .ndarray :
361- """Match the evaluator's signed interpretation of one-byte dense values."""
362- array = np .asarray (values )
363- if array .dtype .kind == "u" and array .dtype .itemsize == 1 :
364- return array .view (np .int8 )
365- return array
368+ def _read_evaluator_dense (dset , selection ) -> np .ndarray :
369+ """Read dense values with the evaluator's HDF5 destination datatype."""
370+ dtype = np .dtype (dset .dtype )
371+ if dtype .kind == "u" and dtype .itemsize == 1 :
372+ # ALPHA_I8 is a signed destination type. HDF5 performs a numeric
373+ # conversion and saturates uint8 values above 127; viewing the bytes
374+ # would instead wrap them to negative values.
375+ return np .asarray (dset .astype (np .int8 )[selection ])
376+ return np .asarray (dset [selection ])
377+
378+
379+ def _validate_offsets (dset , total_bytes : int , name : str ) -> int :
380+ """Validate an on-disk offset index using bounded read windows."""
381+ if len (dset .shape ) != 1 :
382+ raise DatasetFormatError ("'/%s' must be a 1-D offset array" % name )
383+ count = int (dset .shape [0 ])
384+ if count < 1 :
385+ raise DatasetFormatError (
386+ "'/%s' must contain at least the trailing sentinel" % name
387+ )
388+ first = int (np .asarray (dset [0 ], dtype = np .uint64 ))
389+ last = int (np .asarray (dset [count - 1 ], dtype = np .uint64 ))
390+ if first != 0 :
391+ raise DatasetFormatError ("'/%s' must start at byte offset 0" % name )
392+ if last != int (total_bytes ):
393+ raise DatasetFormatError (
394+ "'/%s' sentinel (%d) does not match the sparse byte stream size (%d)"
395+ % (name , last , int (total_bytes ))
396+ )
397+ previous = first
398+ for start in range (1 , count , _OFFSET_VALIDATION_ROWS ):
399+ end = min (start + _OFFSET_VALIDATION_ROWS , count )
400+ window = np .asarray (dset [start :end ], dtype = np .uint64 )
401+ if int (window [0 ]) < previous or np .any (window [1 :] < window [:- 1 ]):
402+ raise DatasetFormatError ("'/%s' must be non-decreasing" % name )
403+ previous = int (window [- 1 ])
404+ return count
366405
367406
368407def _describe_dtype (dtype : np .dtype ) -> str :
0 commit comments