Buffer Design¶
Fallback to Legacy Design
If any instabilities occur, users/developers can make the use of old buffering implementation.
The new buffering system transitions from a monolithic design to a decoupled, policy-driven architecture. By separating how data is stored from how it is sampled or evicted, the system becomes highly extensible and easier to test.

Policy-based Approach¶
- Readiness Policy: Determines if the buffer has enough data to start training (e.g., waiting for a threshold).
- Sampling Policy: Defines how items are selected for the trainer (e.g., FIFO, Random, or Importance Sampling).
- Eviction Policy: Defines which items to remove when the buffer is full (e.g., drop oldest, drop random, or block).
Components¶
Storage (storage.py)¶
Storage classes are containers responsible only for the physical layout of data in memory.
-
BaseStorage: An abstract interface requiring__len__andappend. -
DequeStorage: For standard FIFO operations (fast pops from the left). -
ListStorage: Designed for Reservoir-like sampling, maintaining separate lists forseenandnot_seensamples to track data usage. This can also act as a single list for general-purpose usage.
Policies (policy.py)¶
-
ReadinessPolicy: Manages the transition between the Reception Phase (simulation running) and the Draining Phase (simulation finished). -
SamplingPolicy: This allows a single sampler to act as a "Reservoir" (keep data) during reception and a "Queue" (remove data) during the final flush. -
EvictionPolicy: Handles overflow logic separately from sampling logic when_on_fullis called. (e.g, Eviction on write in case of Reservoir, Do nothing (block) in case of FIFO, FIRO)
The Queue (base.py)¶
BaseQueue handles the thread synchronization (threading.Condition), while PolicyQueue acts as the orchestrator.
-
signal_reception_over(): This method propagates a signal through the policies, switching the buffer from "accumulation mode" to "drain mode." -
_post_reception_error_handling(): A mechanism that monitors buffer size during the drain phase. If the buffer stops shrinking after the reception is over, it raises aFatalErrorto prevent infinite training loops.
The "Flush/Drain Phase" Lifecycle¶
-
Reception:
readiness.reception_overisFalse. TheSamplingPolicyyields data but keeps it inStorage. -
Signal:
signal_reception_over()is called. -
Draining: The
ReadinessPolicynow ignores thresholds (allowing the last few items to be read). TheSamplingPolicyreceivesevict=Trueand begins removing items fromStorage. -
Validation: The
PolicyQueueverifies that the storage size decreases with everyget().
Adding a Custom Buffering Scheme¶
To implement a custom buffering scheme, you must define the Storage (how data is held), the SamplingPolicy (how data is retrieved and/or evicted at the end), and the EvictionPolicy (how to handle a full buffer). The core PolicyQueue then orchestrates these components, handling thread safety and the transition from data reception to the final draining phase.
To implement a priority-based scheme, you need a Sampling Policy that evaluates the "priority" of each item in the storage and a Storage backend that supports searching or filtering.
Example Implementation: Priority Reservoir¶
In this example, we use ListStorage and a placeholder priority_criterion function. This demonstrates how to identify the "best" item to sample and, more importantly, how to ensure that same logic handles eviction on read during the drain phase. Unlike the regular Reservoir that randomly samples items from the storage, this version balances exploration (random sampling) with exploitation (priority sampling) using a priority_ratio.
It also ensures that the _get method correctly bridges the internal state of the buffer (the "drain" status) with the logic required by the sampler.
import random
import numpy as np
from typing_extensions import override
from melissa.server.deep_learning.metric_logger import BaseLogger
from melissa.server.deep_learning.buffer import (
PolicyQueue,
SamplingPolicy,
ThresholdReadiness,
ListStorage,
Sample,
)
from melissa.server.deep_learning.buffer.reservoir import BasicReservoirEviction
def calculate_priority(sample: Sample) -> float:
"""Example function to determine the importance of a sample.
Lower 'seen' count results in higher priority to ensure
data variety during training."""
return 1.0 / (sample.seen + 1)
class PrioritySampler(SamplingPolicy[ListStorage]):
@override
def sample(self, storage: ListStorage, evict: bool, **kwargs) -> Sample:
# probability of picking the 'best' item vs a random item
ratio = kwargs.get("priority_ratio", 0.5)
if np.random.rand() > ratio:
# exploration
idx = random.randrange(len(storage))
else:
# exploitation
idx = max(
range(len(storage)),
key=lambda i: calculate_priority(storage[i])
)
# evict while draining after reception is over
if evict:
return storage.pop(idx)
# return the item but keep it in storage for reuse
return storage[idx]
class PriorityReservoir(PolicyQueue[ListStorage]):
def __init__(
self,
maxsize: int,
threshold: int,
metric_logger: BaseLogger | None = None,
priority_ratio: float = 0.6,
):
super().__init__(
maxsize=maxsize,
readiness=ThresholdReadiness(threshold), # begin sampling after threshold is met
sampler=PrioritySampler(), # priority sampling defined above
evictor=BasicReservoirEviction(), # random eviction of seen items
storage_factory=ListStorage # unified list storage of seen and unseen lists
metric_logger=metric_logger,
)
self.priority_ratio = priority_ratio
@override
def _get(self) -> Sample:
"""Overrides the internal get logic to pass the priority."""
return self.sampler.sample(
self.queue,
evict=self.readiness.reception_over,
priority_ratio=self.priority_ratio,
)
Finally, update types.py and __init__.py from the melissa/server/deep_learning/buffer module to add the custom implementation.