@@ -160,14 +160,31 @@ def __init__(self, dataset, features, data_cache_fp, training_set_metadata=None)
160160 # During preprocessing, lazy features stored file paths as strings. Here
161161 # we wrap those path arrays with the appropriate per-sample decode function
162162 # so that batches are decoded on demand rather than all at once.
163+ self ._auto_prefetch_size = 0
163164 if training_set_metadata is not None :
164165 self ._init_lazy_columns (features , training_set_metadata )
165166
166167 self .size = len (list (self .dataset .values ())[0 ])
167168
169+ def _decoded_cache_path (self , proc_col : str , n : int , sample_shape : tuple ) -> str :
170+ """Return the path for the decoded numpy memmap file for a given column.
171+
172+ Placed next to the Parquet cache when ``data_cache_fp`` is set, otherwise
173+ falls back to ``~/.cache/ludwig/lazy_media/``.
174+ """
175+ flat_shape = "_" .join (str (d ) for d in sample_shape )
176+ fname = f"{ proc_col } _decoded_n{ n } _{ flat_shape } _f32.npy"
177+ if self .data_cache_fp :
178+ return os .path .join (os .path .dirname (self .data_cache_fp ), fname )
179+ from ludwig .data .lazy_utils import get_default_lazy_cache_dir
180+
181+ return str (get_default_lazy_cache_dir () / fname )
182+
168183 def _init_lazy_columns (self , features : dict , training_set_metadata : dict ) -> None :
169- """Replace string-path arrays for lazy features with LazyColumn objects."""
170- from ludwig .data .lazy_utils import LazyColumn
184+ """Replace string-path arrays for lazy features with LazyColumn or CachedLazyColumn objects."""
185+ from ludwig .data .lazy_utils import CachedLazyColumn , LazyColumn
186+
187+ max_prefetch = 0
171188
172189 for proc_col , feat_cfg in features .items ():
173190 if proc_col not in self .dataset :
@@ -179,6 +196,11 @@ def _init_lazy_columns(self, features: dict, training_set_metadata: dict) -> Non
179196
180197 paths = self .dataset [proc_col ]
181198 feat_type = feat_cfg .get ("type" , "" )
199+ mode = feat_meta .get ("mode" , "lazy" )
200+ # Per-feature prefetch override (None means auto → 4 for lazy modes)
201+ feat_prefetch = feat_meta .get ("prefetch_size" )
202+ effective_prefetch = feat_prefetch if feat_prefetch is not None else 4
203+ max_prefetch = max (max_prefetch , effective_prefetch )
182204
183205 if feat_type == "audio" and "lazy_audio_params" in feat_meta :
184206 from ludwig .features .audio_feature import AudioFeatureMixin
@@ -198,7 +220,16 @@ def _init_lazy_columns(self, features: dict, training_set_metadata: dict) -> Non
198220 import torch as _torch
199221
200222 _audio_workers = max (1 , (os .cpu_count () or 4 ) // max (1 , _torch .get_num_threads ()))
201- self .dataset [proc_col ] = LazyColumn (paths , decode_fn , max_workers = _audio_workers )
223+
224+ if mode == "lazy_cached" :
225+ # decode_fn returns (max_length, feature_dim) — match that order.
226+ sample_shape = (p ["max_length" ], p ["feature_dim" ])
227+ cache_path = self ._decoded_cache_path (proc_col , len (paths ), sample_shape )
228+ self .dataset [proc_col ] = CachedLazyColumn (
229+ paths , decode_fn , cache_path , sample_shape , max_workers = _audio_workers
230+ )
231+ else :
232+ self .dataset [proc_col ] = LazyColumn (paths , decode_fn , max_workers = _audio_workers )
202233
203234 elif feat_type == "image" and "lazy_image_params" in feat_meta :
204235 import torch
@@ -220,7 +251,15 @@ def _init_lazy_columns(self, features: dict, training_set_metadata: dict) -> Non
220251 channel_class_map = channel_class_map ,
221252 default_image = default_image ,
222253 )
223- self .dataset [proc_col ] = LazyColumn (paths , decode_fn )
254+
255+ if mode == "lazy_cached" :
256+ sample_shape = tuple (p ["default_image_shape" ])
257+ cache_path = self ._decoded_cache_path (proc_col , len (paths ), sample_shape )
258+ self .dataset [proc_col ] = CachedLazyColumn (paths , decode_fn , cache_path , sample_shape )
259+ else :
260+ self .dataset [proc_col ] = LazyColumn (paths , decode_fn )
261+
262+ self ._auto_prefetch_size = max_prefetch
224263
225264 def to_df (self , features : Iterable [BaseFeature ] | None = None ) -> DataFrame :
226265 """Convert the dataset to a Pandas DataFrame.
@@ -271,6 +310,19 @@ def _has_lazy_columns(self) -> bool:
271310
272311 return any (is_lazy_column (v ) for v in self .dataset .values ())
273312
313+ def is_fully_cached (self ) -> bool :
314+ """Return ``True`` when every ``CachedLazyColumn`` in this dataset has finished its first-pass decode.
315+
316+ Returns ``False`` if there are no ``CachedLazyColumn`` instances (i.e. the dataset
317+ uses plain ``LazyColumn`` or eager mode).
318+ """
319+ from ludwig .data .lazy_utils import is_cached_lazy_column
320+
321+ cached_cols = [v for v in self .dataset .values () if is_cached_lazy_column (v )]
322+ if not cached_cols :
323+ return False
324+ return all (col .is_fully_cached () for col in cached_cols )
325+
274326 @contextlib .contextmanager
275327 def initialize_batcher (
276328 self ,
@@ -282,12 +334,29 @@ def initialize_batcher(
282334 augmentation_pipeline = None ,
283335 prefetch_size : int | None = None ,
284336 ) -> Batcher :
337+ """Yield a :class:`RandomAccessBatcher` configured for this dataset.
338+
339+ Parameters
340+ ----------
341+ prefetch_size:
342+ Number of batches to pipeline in a background thread while the GPU
343+ processes the current batch. ``None`` (default) uses
344+ ``self._auto_prefetch_size``, which is derived from the
345+ ``prefetch_size`` field in each feature's preprocessing config
346+ (``None`` → 4 for ``lazy``/``lazy_cached`` features, 0 for
347+ ``eager``). Pass ``0`` to disable prefetch entirely.
348+
349+ For ``lazy_cached`` mode, ``RandomAccessBatcher.set_epoch`` will
350+ automatically reset ``prefetch_size`` to 0 after epoch 1 once
351+ ``dataset.is_fully_cached()`` returns ``True`` — memmap reads are
352+ fast enough that background pipelining adds no measurable benefit.
353+ """
285354 # When the dataset contains lazy columns (audio / image file paths that
286355 # are decoded per-batch), automatically enable prefetch so the GPU is
287356 # not idle during decode. Callers can override by passing prefetch_size
288357 # explicitly (0 to disable, N to set a specific depth).
289358 if prefetch_size is None :
290- prefetch_size = 4 if self ._has_lazy_columns () else 0
359+ prefetch_size = self . _auto_prefetch_size if self ._has_lazy_columns () else 0
291360
292361 sampler = DistributedSampler (
293362 len (self ), shuffle = should_shuffle , random_seed = random_seed , distributed = distributed
0 commit comments