@@ -219,6 +219,7 @@ def __init__(
219219 streaming_shuffle_seed : int = 0 ,
220220 train_split_percentage : float = 1.0 ,
221221 split_salt : int = 0 ,
222+ use_offline_ts_indices : bool = False ,
222223 * args ,
223224 ** kwargs ,
224225 ) -> None :
@@ -283,6 +284,9 @@ def __init__(
283284 self ._active : Optional [np .ndarray ] = None
284285 self .is_eval : bool = False
285286 self ._anchor_ts : Optional [np .ndarray ] = None
287+ self ._use_offline_ts_indices = use_offline_ts_indices
288+ self ._ts_indices : Optional [np .ndarray ] = None
289+ self ._ts_offsets : Optional [np .ndarray ] = None
286290 self ._t_min : Optional [int ] = None
287291 self ._t_max : Optional [int ] = None
288292 self ._cache_dir : Optional [str ] = cache_dir
@@ -315,6 +319,15 @@ def __init__(
315319 self ._ensure_positions_built (
316320 cache_dir , self ._positions_name , self ._min_history
317321 )
322+ if self ._use_offline_ts_indices :
323+ from generative_recommenders .dlrm_v4 .datasets .offline_ts_indices import (
324+ build_offline_ts_indices ,
325+ )
326+
327+ build_offline_ts_indices (
328+ cache_dir , history_length , self ._min_history ,
329+ window_seconds = self ._streaming_window_seconds ,
330+ )
318331 self ._positions : np .ndarray = _load_npy_readonly (
319332 os .path .join (cache_dir , self ._positions_name )
320333 )
@@ -596,6 +609,25 @@ def _ensure_streaming_index(self) -> None:
596609 Multi-rank safe via an exclusive file lock + atomic rename; all ranks
597610 then mmap the result read-only (shared physical pages, ~0 anon).
598611 """
612+ if self ._use_offline_ts_indices :
613+ if self ._ts_indices is None :
614+ directory = Path (self ._cache_dir )
615+ tag = self ._positions_name [len ("positions_" ):- 4 ]
616+ tag += f"_W{ self ._streaming_window_seconds } "
617+ indices = _load_npy_readonly (directory / f"ts_indices_{ tag } .npy" )
618+ offsets = _load_npy_readonly (directory / f"ts_offsets_{ tag } .npy" )
619+ if (
620+ indices .dtype != np .int64 or indices .ndim != 1
621+ or len (indices ) != len (self ._positions )
622+ or offsets .dtype != np .int64 or offsets .ndim != 1
623+ or len (offsets ) == 0 or offsets [0 ] != 0
624+ or offsets [- 1 ] != len (indices )
625+ or np .any (offsets [1 :] < offsets [:- 1 ])
626+ ):
627+ raise ValueError ("Invalid offline TS indices or offsets" )
628+ self ._ts_offsets = offsets
629+ self ._ts_indices = indices
630+ return
599631 if self ._anchor_ts is not None :
600632 return
601633 import fcntl
@@ -626,9 +658,21 @@ def _ensure_streaming_index(self) -> None:
626658 self ._t_min = int (self ._anchor_ts .min ())
627659 self ._t_max = int (self ._anchor_ts .max ())
628660
661+ def _offline_window_bounds (self , start_ts : int , num_ts : int = 1 ) -> Tuple [int , int ]:
662+ windows = len (self ._ts_offsets ) - 1
663+ start = min (windows , max (0 , start_ts ))
664+ end = min (windows , max (0 , start_ts + max (0 , num_ts )))
665+ return int (self ._ts_offsets [start ]), int (self ._ts_offsets [end ])
666+
667+ def _offline_window_indices (self , start_ts : int , num_ts : int = 1 ) -> np .ndarray :
668+ lo , hi = self ._offline_window_bounds (start_ts , num_ts )
669+ return self ._ts_indices [lo :hi ]
670+
629671 def num_windows (self ) -> int :
630672 """Number of fixed-duration windows spanning [t_min, t_max]."""
631673 self ._ensure_streaming_index ()
674+ if self ._ts_offsets is not None :
675+ return len (self ._ts_offsets ) - 1
632676 assert self ._t_min is not None and self ._t_max is not None
633677 span = self ._t_max - self ._t_min + 1
634678 w = self ._streaming_window_seconds
@@ -653,6 +697,13 @@ def window_indices(
653697 O(N) mask here keeps only one ~26 GB array resident and is robust.
654698 """
655699 self ._ensure_streaming_index ()
700+ if self ._ts_indices is not None :
701+ idx = self ._offline_window_indices (ts )
702+ do_sort = self ._streaming_sort_within_window if sort_by_time is None else sort_by_time
703+ if do_sort and idx .size :
704+ timestamps = self .store .flat_timestamps [self ._positions [idx ]]
705+ idx = idx [np .argsort (timestamps , kind = "stable" )]
706+ return idx
656707 assert self ._anchor_ts is not None and self ._t_min is not None
657708 w = self ._streaming_window_seconds
658709 lo = self ._t_min + ts * w
@@ -778,6 +829,15 @@ def total_train_anchors(self, start_ts: int, num_ts: int) -> int:
778829 by a fraction of a window).
779830 """
780831 self ._ensure_streaming_index ()
832+ if self ._ts_indices is not None :
833+ lo , hi = self ._offline_window_bounds (start_ts , num_ts )
834+ if self ._train_split_percentage >= 1.0 :
835+ return hi - lo
836+ indices = self ._ts_indices [lo :hi ]
837+ return sum (
838+ int (np .count_nonzero (~ self ._eval_anchor_mask (indices [start :start + 1_000_000 ])))
839+ for start in range (0 , len (indices ), 1_000_000 )
840+ )
781841 assert self ._anchor_ts is not None and self ._t_min is not None
782842 if num_ts <= 0 :
783843 return 0
0 commit comments