From 555b6d22a689b01214714243ba556d8f66a9c65e Mon Sep 17 00:00:00 2001 From: Margubur rahman Date: Mon, 20 Apr 2026 15:04:07 +0000 Subject: [PATCH 1/2] update prefetcher logic --- .../_static/prefetch_dynamic_transition.png | Bin 0 -> 73577 bytes docs/source/prefetcher.rst | 15 ++ gcsfs/prefetcher.py | 197 ++++++++++++++---- gcsfs/tests/test_prefetcher.py | 130 ++++++++++-- 4 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 docs/source/_static/prefetch_dynamic_transition.png diff --git a/docs/source/_static/prefetch_dynamic_transition.png b/docs/source/_static/prefetch_dynamic_transition.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3cef4458d105f786513980faf58f361ac30634 GIT binary patch literal 73577 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 cn=@pFlV>}u&p1T>FF5Xyu>b%7 literal 0 HcmV?d00001 diff --git a/docs/source/prefetcher.rst b/docs/source/prefetcher.rst index bec545285..b24afaa47 100644 --- a/docs/source/prefetcher.rst +++ b/docs/source/prefetcher.rst @@ -189,3 +189,18 @@ Multi Stream Performance (48 Process) +---------+--------------+-----------------+-----------------+--------------------------+------------------+------------------+--------------------------+ | rand | 100.00 | 8276.16 | 10171.59 | 10172.54 | 20621.17 | 23598.05 | 24086.18 | +---------+--------------+-----------------+-----------------+--------------------------+------------------+------------------+--------------------------+ + +Seeing the Prefetcher in Action +=============================== + +Real-world applications rarely do just one thing; they often switch between entirely different reading patterns mid-file. To understand how the engine balances aggressive fetching with careful memory management, we can look at a dynamic workload transition. + +The graph below plots the size of the data requested by the application (User Read Size) against the volume of data the prefetcher is fetching in the background (Scheduled/Queued Data). + +.. image:: _static/prefetch_dynamic_transition.png + +Notice how perfectly the prefetcher mirrors the application's changing behaviour across three distinct phases: + +1. **Smooth Sequential Reading (0–10s):** The application starts by reading steady 16MB chunks. After three consistent reads, the prefetcher confirms the pattern and confidently ramps up its background fetching. It maintains a comfortable buffer ahead of the application so the user never waits on the network. +2. **Pausing for Random Seeks (10–20s):** The application suddenly stops reading in a straight line and starts jumping randomly around the file. Prefetching is actively harmful here. The engine instantly detects the broken streak and drops the background buffer to zero, ensuring no network bandwidth or memory is wasted downloading unneeded data. +3. **Adjusting to Massive Reads (20–30s):** The application resumes reading sequentially, but this time stepping up to massive 100MB chunks. The algorithm quickly catches on. It detects the new streak, calculates the new rolling average, and rebuilds the background buffer—this time scaling it up safely to handle the much larger data chunks. diff --git a/gcsfs/prefetcher.py b/gcsfs/prefetcher.py index 840cec63b..4a3050214 100644 --- a/gcsfs/prefetcher.py +++ b/gcsfs/prefetcher.py @@ -78,6 +78,24 @@ def average(self) -> int: return 1024 * 1024 # 1MB return self._sum // count + @property + def is_variable(self) -> bool: + """Determines if the history contains distinct chunk sizes.""" + count = len(self._history) + if count < 2: + return False + + first_val = self._history[0] + return any(val != first_val for val in self._history) + + @property + def last_value(self) -> int: + """Returns the most recent entry in the history.""" + if not self._history: + raise RuntimeError("No entry found in history") + + return self._history[-1] + def clear(self): """Clears the history and resets the sum to zero.""" logger.debug("Clearing RunningAverageTracker history.") @@ -101,6 +119,24 @@ class PrefetchProducer: # to maximum of 2 * io_size and 128MB MIN_PREFETCH_SIZE = 128 * 1024 * 1024 + # The prefetching starts on the third read. + MIN_STREAKS_FOR_PREFETCHING = 3 + + # Threshold for disabling proactive prefetching on large, variable reads. + # + # If the average read size exceeds this value and patterns are variable, + # prefetching shifts from an I/O bottleneck to a CPU bottleneck. When a user + # requests random massive sizes (e.g., jumping between 100MB and INF), the + # producer still fetches chunks based on the rolling average. The consumer + # then has to pick up multiple chunks and stitch them together to match the + # exact requested size. + # + # For small average read sizes, this byte assembly is fast and the bottleneck + # remains the network I/O. However, for massive reads (>= 100MB), the extra + # step of copying and assembling huge byte strings in memory severely slows + # down the operation. + VARIABLE_IO_THRESHOLD = 100 * 1024 * 1024 + def __init__( self, fetcher, @@ -108,10 +144,9 @@ def __init__( concurrency: int, queue: asyncio.Queue, wakeup_event: asyncio.Event, - get_user_offset, - get_io_size, - get_sequential_streak, - on_error, + consumer: "PrefetchConsumer", + tracker: RunningAverageTracker, + orchestrator: "BackgroundPrefetcher", user_max_prefetch_size=None, ): """Initializes the background producer. @@ -122,10 +157,9 @@ def __init__( concurrency (int): Maximum number of concurrent fetch tasks. queue (asyncio.Queue): The shared queue to push download tasks into. wakeup_event (asyncio.Event): Event used to wake the producer from an idle state. - get_user_offset (Callable): Function returning the user's current read offset. - get_io_size (Callable): Function returning the adaptive IO size. - get_sequential_streak (Callable): Function returning the current sequential read streak. - on_error (Callable): Callback triggered when a background error occurs. + consumer (PrefetchConsumer): The consumer reading the prefetched chunks. + tracker (RunningAverageTracker): Tracker for history of read sizes. + orchestrator (BackgroundPrefetcher): The parent object managing the operation. user_max_prefetch_size (int, optional): A hard limit for prefetch size overrides. """ logger.debug( @@ -140,10 +174,9 @@ def __init__( self.queue = queue self.wakeup_event = wakeup_event - self.get_user_offset = get_user_offset - self.get_io_size = get_io_size - self.get_sequential_streak = get_sequential_streak - self.on_error = on_error + self.consumer = consumer + self.tracker = tracker + self.orchestrator = orchestrator self._user_max_prefetch_size = user_max_prefetch_size self.current_offset = 0 @@ -161,9 +194,9 @@ def max_prefetch_size(self) -> int: if self._user_max_prefetch_size is not None: return min( self._user_max_prefetch_size, - max(2 * self.get_io_size(), self.MIN_PREFETCH_SIZE), + max(2 * self.tracker.average, self.MIN_PREFETCH_SIZE), ) - return max(2 * self.get_io_size(), self.MIN_PREFETCH_SIZE) + return max(2 * self.tracker.average, self.MIN_PREFETCH_SIZE) def start(self): """Starts the background producer loop. @@ -246,23 +279,60 @@ async def _loop(self): if self.is_stopped: break - io_size = self.get_io_size() - streak = self.get_sequential_streak() - prefetch_size = min((streak + 1) * io_size, self.max_prefetch_size) + avg_io_size = self.tracker.average + streak = self.consumer.sequential_streak + is_variable = self.tracker.is_variable + last_read_size = self.tracker.last_value + + exceeds_user_max = ( + self._user_max_prefetch_size is not None + and avg_io_size > self._user_max_prefetch_size + ) + + # Disable prefetching ahead if highly variable AND average > 100MB, or if it exceeds user max + if ( + is_variable and avg_io_size > PrefetchProducer.VARIABLE_IO_THRESHOLD + ) or exceeds_user_max: + logger.debug( + "Large IO detected (variable > 100MB or > user max). Disabling background prefetching." + ) + prefetch_multiplier = 1 + elif streak < self.MIN_STREAKS_FOR_PREFETCHING: + prefetch_multiplier = 1 + else: + prefetch_multiplier = streak - self.MIN_STREAKS_FOR_PREFETCHING + 1 + + if self.queue.empty() or prefetch_multiplier == 1: + io_size = last_read_size + else: + io_size = avg_io_size + + prefetch_size = min( + prefetch_multiplier * io_size, self.max_prefetch_size + ) + if self.consumer.offset + prefetch_size < self.consumer.target_offset: + prefetch_size = self.consumer.target_offset - self.consumer.offset + + if is_variable: + effective_prefetch_size = prefetch_size + else: + effective_prefetch_size = (prefetch_size // io_size) * io_size + if effective_prefetch_size == 0: + effective_prefetch_size = prefetch_size logger.debug( "Producer awake. Current offset: %d, User offset: %d, Prefetch size: %d", self.current_offset, - self.get_user_offset(), + self.consumer.offset, prefetch_size, ) while ( not self.is_stopped - and (self.current_offset - self.get_user_offset()) < prefetch_size + and (self.current_offset - self.consumer.offset) < prefetch_size and self.current_offset < self.size ): - user_offset = self.get_user_offset() + user_offset = self.consumer.offset space_remaining = self.size - self.current_offset prefetch_space_available = prefetch_size - ( self.current_offset - user_offset @@ -278,14 +348,22 @@ async def _loop(self): else: actual_size = min(io_size, space_remaining) - if streak < 2: + if prefetch_space_available < actual_size: + if is_variable or prefetch_space_available == prefetch_size: + actual_size = prefetch_space_available + else: + break + + if streak < PrefetchProducer.MIN_STREAKS_FOR_PREFETCHING: sfactor = self.concurrency else: sfactor = min( self.concurrency, max( 1, - actual_size * self.concurrency // prefetch_size, + actual_size + * self.concurrency + // effective_prefetch_size, ), ) @@ -317,7 +395,7 @@ async def _loop(self): exc_info=True, ) self.is_stopped = True - self.on_error(e) + self.orchestrator._set_error(e) await self.queue.put(e) @@ -332,24 +410,25 @@ def __init__( self, queue: asyncio.Queue, wakeup_event: asyncio.Event, - is_producer_stopped, - on_error, + tracker: RunningAverageTracker, + orchestrator: "BackgroundPrefetcher", ): """Initializes the consumer. Args: queue (asyncio.Queue): The shared queue containing fetch tasks. wakeup_event (asyncio.Event): Event used to wake the producer when more data is needed. - is_producer_stopped (Callable): Function returning whether the producer has been halted. - on_error (Callable): Callback triggered when a fetch error is encountered. + tracker (RunningAverageTracker): Tracker for history of read sizes. + orchestrator (BackgroundPrefetcher): The parent object managing the operation. """ logger.debug("Initializing PrefetchConsumer.") self.queue = queue self.wakeup_event = wakeup_event - self.is_producer_stopped = is_producer_stopped - self.on_error = on_error + self.tracker = tracker + self.orchestrator = orchestrator self.sequential_streak = 0 self.offset = 0 + self.target_offset = 0 self._current_block = b"" self._current_block_idx = 0 @@ -364,6 +443,7 @@ def seek(self, new_offset: int): new_offset, ) self.offset = new_offset + self.target_offset = new_offset self.sequential_streak = 0 self._current_block = b"" self._current_block_idx = 0 @@ -384,12 +464,18 @@ async def _advance(self, size: int, save_data: bool) -> list[bytes]: chunks = [] processed = 0 + self.target_offset = self.offset + size while processed < size: available = len(self._current_block) - self._current_block_idx + trigger_wakeup = False if not available: - if self.is_producer_stopped() and self.queue.empty(): + is_producer_stopped = ( + not hasattr(self.orchestrator, "producer") + or self.orchestrator.producer.is_stopped + ) + if is_producer_stopped and self.queue.empty(): logger.debug("Consumer reached EOF.") break @@ -401,15 +487,38 @@ async def _advance(self, size: int, save_data: bool) -> list[bytes]: if isinstance(task, Exception): logger.error("Consumer retrieved an exception: %s", task) - self.on_error(task) + self.orchestrator._set_error(task) raise task try: block = await task self.sequential_streak += 1 - if self.sequential_streak >= 2: - self.wakeup_event.set() + if ( + self.sequential_streak + >= PrefetchProducer.MIN_STREAKS_FOR_PREFETCHING + ): + is_variable = self.tracker.is_variable + avg_io_size = self.tracker.average + + exceeds_user_max = ( + self.orchestrator.max_prefetch_size is not None + and avg_io_size > self.orchestrator.max_prefetch_size + ) + is_massive_variable = ( + is_variable + and avg_io_size > PrefetchProducer.VARIABLE_IO_THRESHOLD + ) + + # Suppress proactive wakeups to prevent large CPU assembly + # on erratic large reads or exceeding max + if not (is_massive_variable or exceeds_user_max): + trigger_wakeup = True + else: + logger.debug( + "Suppressing proactive producer wakeup due to massive variable" + " workload or exceeding user max prefetch." + ) self._current_block = block self._current_block_idx = 0 @@ -418,7 +527,7 @@ async def _advance(self, size: int, save_data: bool) -> list[bytes]: raise except Exception as e: logger.error("Consumer caught an error: %s", e, exc_info=True) - self.on_error(e) + self.orchestrator._set_error(e) raise e if not self._current_block: @@ -440,6 +549,8 @@ async def _advance(self, size: int, save_data: bool) -> list[bytes]: self._current_block_idx += take processed += take self.offset += take + if trigger_wakeup: + self.wakeup_event.set() return chunks @@ -504,6 +615,7 @@ def __init__(self, fetcher, size: int, concurrency: int, max_prefetch_size=None) ) self.size = size self.concurrency = concurrency + self.max_prefetch_size = max_prefetch_size if max_prefetch_size is not None and max_prefetch_size <= 0: logger.error("Invalid max_prefetch_size provided: %s", max_prefetch_size) @@ -523,8 +635,8 @@ def __init__(self, fetcher, size: int, concurrency: int, max_prefetch_size=None) self.consumer = PrefetchConsumer( queue=self.queue, wakeup_event=self.wakeup_event, - is_producer_stopped=self._is_producer_stopped, - on_error=self._set_error, + tracker=self.read_tracker, + orchestrator=self, ) self.producer = PrefetchProducer( @@ -533,10 +645,9 @@ def __init__(self, fetcher, size: int, concurrency: int, max_prefetch_size=None) concurrency=self.concurrency, queue=self.queue, wakeup_event=self.wakeup_event, - get_user_offset=lambda: self.consumer.offset, - get_io_size=self._get_adaptive_io_size, - get_sequential_streak=lambda: self.consumer.sequential_streak, - on_error=self._set_error, + consumer=self.consumer, + tracker=self.read_tracker, + orchestrator=self, user_max_prefetch_size=max_prefetch_size, ) @@ -554,12 +665,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit point. Ensures the prefetcher is cleanly closed.""" self.close() - def _get_adaptive_io_size(self) -> int: - return self.read_tracker.average - - def _is_producer_stopped(self) -> bool: - return self.producer.is_stopped if hasattr(self, "producer") else True - def _set_error(self, e: Exception): logger.error("Global error state set in BackgroundPrefetcher: %s", e) self._error = e diff --git a/gcsfs/tests/test_prefetcher.py b/gcsfs/tests/test_prefetcher.py index e0dc8e859..acba2213e 100644 --- a/gcsfs/tests/test_prefetcher.py +++ b/gcsfs/tests/test_prefetcher.py @@ -136,9 +136,9 @@ def test_producer_concurrency_streak_and_min_chunk(): original_min_chunk = bp.producer.MIN_CHUNK_SIZE bp.producer.MIN_CHUNK_SIZE = 10 - bp._fetch(0, 50) - bp._fetch(50, 100) - bp._fetch(100, 150) + # Do 6 reads to push the streak well past the MIN_STREAKS threshold + for i in range(6): + bp._fetch(i * 50, (i + 1) * 50) fsspec.asyn.sync(bp.loop, asyncio.sleep, 0.1) @@ -172,17 +172,14 @@ def test_producer_loop_space_constraints(): def test_producer_error_propagation(): - fetcher = MockFetcher(b"A" * 1000, fail_at_call=3) - bp = BackgroundPrefetcher(fetcher=fetcher, size=1000, concurrency=4) - bp.read_tracker.add(100) + fetcher = MockFetcher(b"A" * 2000, fail_at_call=3) + bp = BackgroundPrefetcher(fetcher=fetcher, size=2000, concurrency=4) - assert bp._fetch(0, 100) == b"A" * 100 + for i in range(2): + bp._fetch(i * 100, (i + 1) * 100) with pytest.raises(OSError, match="Simulated Network Timeout"): - bp._fetch(100, 500) - - assert bp.is_stopped is True - bp.close() + bp._fetch(400, 500) def test_read_after_close_or_error(): @@ -353,18 +350,18 @@ def test_producer_min_chunk_logic(): def test_producer_loop_exception(): - bp = BackgroundPrefetcher(fetcher=MockFetcher(b""), size=100, concurrency=4) + bp = BackgroundPrefetcher(fetcher=MockFetcher(b"A" * 100), size=100, concurrency=4) error_object = ValueError("Producer crash") - bp.producer.get_io_size = mock.Mock(side_effect=error_object) - with pytest.raises(ValueError, match="Producer crash"): - bp._fetch(0, 10) + with mock.patch( + "gcsfs.prefetcher.RunningAverageTracker.average", new_callable=mock.PropertyMock + ) as mocked_avg: + mocked_avg.side_effect = error_object + with pytest.raises(ValueError, match="Producer crash"): + bp._fetch(0, 10) assert bp.is_stopped is True assert bp._error == error_object - - with pytest.raises(ValueError, match="Producer crash"): - bp._fetch(0, 10) bp.close() @@ -507,7 +504,10 @@ def test_producer_min_chunk_inner_break(): async def trigger_loop(): bp.producer.current_offset = 250 bp.consumer.offset = 0 - bp.consumer.sequential_streak = 3 # makes prefetch_size = (3+1) * 100 = 400 + bp.consumer.target_offset = 0 + # streak=6 makes prefetch_multiplier = 4 (6 - 3 + 1) + # prefetch_size = 4 * 100 = 400 + bp.consumer.sequential_streak = 6 bp.wakeup_event.set() await asyncio.sleep(0.05) @@ -533,3 +533,95 @@ async def trigger_stop_and_wake(): # Verify the producer gracefully exited without doing work assert fetcher.call_count == 0 bp.close() + + +def test_massive_read_disables_proactive_prefetching(): + fetcher = MockFetcher(b"X" * 1000) + + # max_prefetch_size = 40 + bp = BackgroundPrefetcher( + fetcher=fetcher, size=1000, concurrency=4, max_prefetch_size=40 + ) + + # Do enough reads to build a sequential streak and trigger large averages + # Reading 60 bytes at a time. Average = 60. Threshold = 50. + for i in range(4): + bp._fetch(i * 60, (i + 1) * 60) + + fsspec.asyn.sync(bp.loop, asyncio.sleep, 0.1) + + # Because average (60) > threshold (40), prefetch_multiplier is pinned to 1. + # The producer should only fetch what the user specifically read (4 * 60 = 240) + # and should NOT have pre-fetched any additional data ahead into the queue. + assert bp.producer.current_offset == 240 + bp.close() + + +def test_normal_read_allows_proactive_prefetching(): + fetcher = MockFetcher(b"X" * 1000) + + # max_prefetch_size = 200 makes dynamic threshold = 100 + bp = BackgroundPrefetcher( + fetcher=fetcher, size=1000, concurrency=4, max_prefetch_size=200 + ) + + # Reading 60 bytes at a time. Average = 60. Threshold = 100. + for i in range(4): + bp._fetch(i * 60, (i + 1) * 60) + + fsspec.asyn.sync(bp.loop, asyncio.sleep, 0.1) + + # Because average (60) <= threshold (100), the producer allows prefetching. + # It calculates a normal prefetch_multiplier > 1 and pre-fetches data ahead. + assert bp.producer.current_offset > 240 + bp.close() + + +def test_target_offset_expands_prefetch(): + fetcher = MockFetcher(b"X" * 1000) + bp = BackgroundPrefetcher(fetcher=fetcher, size=1000, concurrency=4) + + # Seed tracker to keep the default `max_prefetch_size` calculation small + bp.read_tracker.add(10) + + # The consumer requests a massive chunk (500 bytes), far exceeding normal prefetch windows + bp._fetch(0, 500) + + fsspec.asyn.sync(bp.loop, asyncio.sleep, 0.1) + + # The new target_offset logic should explicitly tell the producer to expand its + # boundary to cover the requested 500 bytes, overriding the tiny multiplier logic. + assert bp.consumer.target_offset == 500 + assert bp.producer.current_offset >= 500 + bp.close() + + +def test_producer_min_chunk_inner_empty_queue_shrink(): + fetcher = MockFetcher(b"X" * 1000) + bp = BackgroundPrefetcher( + fetcher=fetcher, size=1000, concurrency=4, max_prefetch_size=400 + ) + + bp.read_tracker.add(100) + + original_min_chunk = bp.producer.MIN_CHUNK_SIZE + bp.producer.MIN_CHUNK_SIZE = 200 + + async def trigger_loop(): + # Setup conditions where the queue is empty and the user is waiting + # This makes prefetch_space_available exactly equal to prefetch_size + bp.producer.current_offset = 0 + bp.consumer.offset = 0 + bp.consumer.target_offset = 0 + bp.consumer.sequential_streak = 6 + bp.wakeup_event.set() + await asyncio.sleep(0.05) + + fsspec.asyn.sync(bp.loop, trigger_loop) + + # Because space_available == prefetch_size, it triggers the shrink condition + # instead of breaking, ensuring the blocked consumer gets its data. + assert fetcher.call_count > 0 + + bp.producer.MIN_CHUNK_SIZE = original_min_chunk + bp.close() From bf79c7b49d282d23d940df5e8e643017c434008c Mon Sep 17 00:00:00 2001 From: Margubur rahman Date: Wed, 13 May 2026 12:32:25 +0000 Subject: [PATCH 2/2] change 100MB to 64MB --- gcsfs/prefetcher.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gcsfs/prefetcher.py b/gcsfs/prefetcher.py index 4a3050214..639bee1fa 100644 --- a/gcsfs/prefetcher.py +++ b/gcsfs/prefetcher.py @@ -126,16 +126,16 @@ class PrefetchProducer: # # If the average read size exceeds this value and patterns are variable, # prefetching shifts from an I/O bottleneck to a CPU bottleneck. When a user - # requests random massive sizes (e.g., jumping between 100MB and INF), the + # requests random massive sizes (e.g., jumping between 64MB and INF), the # producer still fetches chunks based on the rolling average. The consumer # then has to pick up multiple chunks and stitch them together to match the # exact requested size. # # For small average read sizes, this byte assembly is fast and the bottleneck - # remains the network I/O. However, for massive reads (>= 100MB), the extra + # remains the network I/O. However, for massive reads (>= 64MB), the extra # step of copying and assembling huge byte strings in memory severely slows # down the operation. - VARIABLE_IO_THRESHOLD = 100 * 1024 * 1024 + VARIABLE_IO_THRESHOLD = 64 * 1024 * 1024 def __init__( self, @@ -289,12 +289,12 @@ async def _loop(self): and avg_io_size > self._user_max_prefetch_size ) - # Disable prefetching ahead if highly variable AND average > 100MB, or if it exceeds user max + # Disable prefetching ahead if highly variable AND average > 64MB, or if it exceeds user max if ( is_variable and avg_io_size > PrefetchProducer.VARIABLE_IO_THRESHOLD ) or exceeds_user_max: logger.debug( - "Large IO detected (variable > 100MB or > user max). Disabling background prefetching." + "Large IO detected (variable > 64MB or > user max). Disabling background prefetching." ) prefetch_multiplier = 1 elif streak < self.MIN_STREAKS_FOR_PREFETCHING: