Pinterest engineered a scalable model evaluation platform by fusing inference and metrics computation using Ray and Apache Spark. This approach addresses the overheads of traditional separate stacks, such as cluster startup and data movement, by enabling in-batch partial metric computations. The core innovation lies in a 'ReducibleMetric' contract that allows decomposable, holistic, and group-wise metrics to be calculated efficiently at scale, significantly reducing evaluation time.
Read original on Pinterest EngineeringEvaluating Machine Learning models at scale presents significant infrastructure challenges, especially in systems running numerous experiments in parallel, like feature importance platforms. Traditionally, inference and model evaluation are separate stages, leading to inefficiencies due to data serialization/deserialization and cluster spin-up times. Pinterest's solution optimizes this by fusing these stages.
In an unfused pipeline, ML model predictions are first generated (e.g., using Ray) and written to storage (e.g., S3). A separate Apache Spark job then reads this data to compute evaluation metrics. This creates two primary overheads:
Pinterest redesigned its ML infrastructure to fuse inference and metrics computation. This involves calculating metrics per batch directly within the Ray inference job, leveraging Ray's shared object store to avoid cross-stack data handoffs. This strategy reduced end-to-end evaluation time by up to 32% and scaled linearly with the number of experiments.
A crucial abstraction for this fusion is the `ReducibleMetric` contract. This interface standardizes how different metrics are computed and aggregated, ensuring consistency across execution frameworks like Ray and Spark. It decomposes metric calculation into three abstract methods:
from abc import ABC, abstractmethod
class ReducibleMetric(ABC):
@abstractmethod
def dataset_preprocessor(self, ds):
"""Prepare the dataset to satisfy the metric's data layout requirements."""
pass
@abstractmethod
def compute_partials(self, batch):
"""Compute a partial result from one batch"""
pass
@abstractmethod
def reduce_partials(self, partials):
"""Combine partial results into the final result."""
passKey System Design Takeaway
Fusing computational stages and designing flexible metric contracts are powerful techniques for optimizing complex data pipelines. By identifying the 'decomposability' of metrics and designing data partitioning strategies (like bucketing for group-wise metrics), engineers can significantly reduce overheads and achieve linear scalability in high-throughput data systems.