<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Pinterest Engineering Blog - Medium]]></title>
        <description><![CDATA[Inventive engineers building the first visual discovery engine, 300 billion ideas and counting. - Medium]]></description>
        <link>https://medium.com/pinterest-engineering?source=rss----4c5a5f6279b6---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Pinterest Engineering Blog - Medium</title>
            <link>https://medium.com/pinterest-engineering?source=rss----4c5a5f6279b6---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 04:10:08 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/pinterest-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Scaling Model Evaluation at Pinterest with Ray (Part 4)]]></title>
            <link>https://medium.com/pinterest-engineering/scaling-model-evaluation-at-pinterest-with-ray-part-4-4a96cd12cc11?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/4a96cd12cc11</guid>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[distributed-computing]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 16:01:04 GMT</pubDate>
            <atom:updated>2026-11-04T16:01:04.588Z</atom:updated>
            <content:encoded><![CDATA[<p>Vishnu Arun | Tech Lead, Feature Importance Platform; Anderson Lam | Sr Software Engineer; Andrew Qiu | Machine Learning Engineer I; Helena Xu | Data Scientist I; Tianyou Wang | Data Scientist II</p><h3>Introduction</h3><p>In our multi-part blog series, we have discussed <a href="https://medium.com/pinterest-engineering/last-mile-data-processing-with-ray-629affbf34ff">why we chose Ray</a> as a last mile data processing framework, how we <a href="https://medium.com/pinterest-engineering/ray-infrastructure-at-pinterest-0248efe4fd52">integrated Ray </a>into our existing ML Training Infrastructure, and how we enabled efficient Batch Inference at scale. In this blog post, we will discuss how we were able to apply Ray to another critical part of our ML Infrastructure stack: <strong>Model Evaluation</strong>.</p><p>In the <a href="https://medium.com/pinterest-engineering/ray-batch-inference-at-pinterest-part-3-4faeb652e385">Ray Batch Inference Blog Post</a>, we ended on a section on accumulators (counters that would allow us to calculate metrics like ROC-AUC and cross entropy). This post is where those accumulators pay off: a model evaluation platform that combines the strengths of Ray and Apache Spark to compute metrics efficiently at scale.</p><p>Model evaluation may appear to be a relatively straightforward step after inference or training. But as ML workflows scale, evaluation quickly becomes an infrastructure challenge. Systems that run many experiments in parallel, such as feature importance, must efficiently compute and aggregate metrics over large volumes of inference data, as shown in Diagram 1.</p><p>Later in this post, we’ll show how we amortize the metrics compute step, overlapping it with inference for minimal added runtime cost, cutting end-to-end evaluation time by up to 32% while scaling linearly as we run more experiments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IjS1hNUJHyV0NzVGLIiAOA.png" /><figcaption><strong>Diagram 1</strong>: Depicts a feature importance system’s architecture. We illustrate a multi-head model with h heads and metrics calculated per head. In this type of workflow, metrics are calculated for each experiment and model head. <em>Note: We omit a batched view of this data found in large scale production ML systems for simplicity.</em></figcaption></figure><p><strong>Terminology</strong></p><ul><li><strong>Feature Importance (FI):</strong> A technique for quantifying how much each input feature contributes to a model’s predictions. In this post, we will reference a perturbation-based method where we perturb one or more columns, run inference, and compare the results against an unperturbed dataset.</li><li><strong>Experiment / baseline:</strong> One perturbed run vs. the unperturbed run.</li><li><strong>Model head: </strong>One output of a multi-task model.</li><li><strong>Batch:</strong> an arbitrary, compute-sized group of rows. This is the unit of data that one Ray task processes.</li><li><strong>Bucket:</strong> a batch that is group-preserving. It contains one or more complete segments, and no segment may span buckets.</li><li><strong>Segment:</strong> The semantic unit over which a group-wise metric is defined, such as a request.</li><li><strong>Serde [serialization/ deserialization]:</strong> The process and cost of writing data out and reading it back.</li></ul><h3>Background</h3><p>Traditionally, evaluation and inference are two separate stacks stitched together. Diagram 2 below contrasts that unfused pipeline against the fused pipeline we’ll build up over the rest of this post.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dWX431vtwsoZ952WxlAgbw.png" /><figcaption><strong>Diagram 2</strong>: Unfused (top) vs. fused (bottom) inference-and-metrics compute. In the unfused lane, inference and metrics run as separate jobs connected by a serialize/persist/deserialize handoff; in the fused lane, metrics are computed per batch inside the Ray inference job via the shared object store. Red denotes the cross-stack handoff removed by fusion.</figcaption></figure><p>Concretely, this evaluation step is typically implemented as a Apache Spark job that computes metrics across one or more inference experiments. As shown in the diagram above, we previously used Ray inference to write predictions to S3, then have a follow-up compute job to reread all this data and compute the metrics.</p><p>For a FI run, this approach introduces two sources of overhead:</p><ul><li><strong>Cluster startup</strong>: Acquiring and warming up a cluster for the metric job.</li><li><strong>Data movement:</strong> Writing predictions to S3 and reading them back to compute metrics.</li></ul><p>Because inference is done in batches, and metrics typically depend on all of the inference results, the unfused approach can be used to calculate any metric. To save the overheads, we need to fuse the stages, but this also implies that we need to be able to calculate metrics on batches of rows. Therefore, we need a metric platform that treats decomposability as a first principle.</p><p>Metrics fall into three groups:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SCUPm7Prbvl_JGK-ydqfSg.png" /></figure><h4>Metric Contract</h4><p>We define a common metric contract that captures each metric’s compute layout and provides a consistent interface across execution frameworks. We express this interface concretely as an Abstract Base Class.</p><pre>from abc import ABC, abstractmethod<br><br>class ReducibleMetric(ABC):<br>    @abstractmethod<br>    def dataset_preprocessor(self, ds):<br>        &quot;&quot;&quot;Prepare the dataset to satisfy the metric&#39;s data layout requirements.&quot;&quot;&quot;<br>        pass<br><br>    @abstractmethod<br>    def compute_partials(self, batch):<br>        &quot;&quot;&quot;Compute a partial result from one batch&quot;&quot;&quot;<br>        pass<br><br>    @abstractmethod<br>    def reduce_partials(self, partials):<br>        &quot;&quot;&quot;Combine partial results into the final result.&quot;&quot;&quot;<br>        pass</pre><p>Decomposable and holistic metrics can calculate partials from arbitrary batches because they do not require related rows to be colocated.</p><p>Group-wise metrics introduce an additional constraint. Before handing data to Ray, we organize complete segments into buckets. A bucket is therefore a group-preserving batch: it contains one or more complete segments, and no segment crosses a bucket boundary. This lets us independently tune bucket size for efficient execution without changing the semantic unit of the metric.</p><h3>Common Metrics found in Recommender Systems</h3><p>We will walk through three metrics, <a href="https://developers.google.com/machine-learning/glossary/metrics#mean-absolute-error-mae">MAE</a>, <a href="https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc">ROC-AUC</a>, and <a href="https://developers.google.com/machine-learning/glossary/metrics#precision-at-k-precisionk">Precision@K</a>; one from each group above. We will use the following dataset to compute each metric.</p><p>For the examples below:</p><ul><li>model_output contains the predictions, and label_continuous contains the target values.</li><li>label_binarized is derived as 1 if label_continuous ≥ 0.5 and 0 otherwise.</li><li>Batch A contains ReqA, and Batch B contains ReqB. This split lets us demonstrate batch-level computation and aggregation.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wmr5Idz3Mm8qy0OzLQdE0Q.png" /></figure><p><strong>MAE (Decomposable)<br></strong>Mean Absolute Error measures the average absolute difference between predictions and their actual values.</p><p>MAE_b = (1/n_b) · Σ_{i ∈ batch b} |pred_i − label_continuous_i|</p><p>Unlike MSE/RMSE, MAE does not penalize large errors as heavily.</p><p>Per-row absolute errors:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nRqgyneqYBEdvcTDQZB4fQ.png" /></figure><p><strong>Global:</strong> Σ = 1.45, N = 8 → MAE = 0.18125.</p><p><strong>Batch decomposition:<br></strong>Batch A partial (Σ|err| = 0.65, n = 4)<br>Batch B partial (Σ|err| = 0.80, n = 4)<br>Merge (0.65 + 0.80) / (4 + 4) = 0.18125</p><pre>class MAE(ReducibleMetric):<br>    def dataset_preprocessor(self, ds): <br>  # No dataset preparation needed. Rows are independent.<br> return ds<br><br>    def compute_partials(self, batch):<br>  # Emit summed absolute error and row counts from each batch.<br>        error = np.abs(batch[&quot;model_output&quot;] - batch[&quot;label_continuous&quot;])<br>        return error.sum(), len(error)<br><br>    def reduce_partials(self, partials):<br>  # Sum partials and divide once globally. <br>        error, count = np.sum(partials, axis=0)<br>        return error / count</pre><p><strong>ROC-AUC (Holistic)<br></strong>ROC-AUC is a binary classification metric which measures the ability of a model to distinguish between classes across different decision thresholds.</p><h4>Method 1 — Global Sort (what <a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.auc.html">sklearn</a> does)</h4><p><strong>Global Sort</strong></p><p>We sort the rows by model_output in descending order, keeping each prediction paired with its corresponding binary label.</p><p><strong>Cumulative Sum</strong></p><p>After sorting, we calculate cumulative true-positive (TP) and false-positive (FP) counts and prepend 0 to both vectors.</p><p><strong>Calculate the TPR and FPR</strong></p><p>We calculate the TPR at each threshold by dividing the TP vector by the number of positive labels.</p><p>We calculate the FPR at each threshold by dividing the FP vector by the number of negative labels.</p><p><strong>Calculate the Trapezoidal area under TPR/FPR</strong></p><p>We calculate the AUC of TPR/FPR to get the final ROC_AUC value.</p><p>Sorting our eight rows by score:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PJ5_Wx307xXePy8yIfkr0w.png" /></figure><p>With 4 positives and 4 negatives, the area works out to <strong>AUC = 0.9375</strong> (or 15/16 positive/negative pairs are ranked correctly).</p><p>We observe that ROC_AUC using this method is calculated over the entire dataset due to a global sort. We could break the dataset into batches, calculate ROC-AUC for each batch, and average the results. However, this <strong>does not</strong> produce the same result as a global ranking.</p><p>Consider the following calculation:</p><ul><li>Batch A (ReqA) = 1.00</li><li>Batch B (ReqB) = 0.75</li><li>Average = 0.875, not 0.9375</li></ul><p>Averaging a ranking number across batches is wrong, because AUC depends on how rows rank against <em>all</em> other rows, not just those in their batch.</p><p>We could force a real global sort in Apache Spark, but doing that across all metrics, model heads, and experiments is exactly the cross-stack cost we’re trying to avoid.</p><h4>Method 2 — Confusion Matrix</h4><p>Consider the standard <a href="https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc">method</a> to approximate ROC_AUC.</p><p><strong>Define thresholds</strong></p><p>Pick a fixed set of thresholds in [0, 1].</p><p><strong>Calculate Confusion Matrix</strong></p><p>Calculate the confusion matrix (TP, FP, TN, FN) for each threshold</p><p><strong>Calculate the final metric</strong></p><p>Calculate the TPR/FPR and finally derive the AUC.</p><p>This method can be calculated batch-wise because the global confusion matrix can be calculated from the batch confusion matrices.</p><p>We can create the following confusion matrices per batch and then sum:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8OqzT4-lnDst_sa2rzMVrA.png" /></figure><p>From this approximate example, when we derive the area under the curve, we get 0.875. If we use more thresholds, for example [0.00, 1.00] with step size of 0.01, we would get 0.9375. <strong>Note: </strong>This does not mean that threshold-based aggregation generally produces the exact global AUC. In this toy example, a step size of 0.01 recovers the exact value because the prediction scores align with the threshold grid.</p><pre>class ROCAUC(ReducibleMetric):<br>    def dataset_preprocessor(self, ds): <br> # No dataset preparation needed. Rows are independent.<br>return ds<br><br>    def compute_partials(self, batch):<br>  # Emit confusion matrix across thresholds. <br>        pred = batch[&quot;model_output&quot;]<br> truth = batch[&quot;label_binarized&quot;].astype(bool)<br><br>        positive = pred[None, :] &gt;= self.thresholds[:, None]<br>        return np.stack([(positive &amp; truth).sum(1), (positive &amp; ~truth).sum(1),<br>                       (~positive &amp; ~truth).sum(1), (~positive &amp; truth).sum(1)],1)<br><br>    def reduce_partials(self, partials):<br>  # Sum confusion matrices and add ROC endpoints.<br> # Integrate in ascending FPR order. <br>        tp, fp, tn, fn = np.sum(partials, 0).T<br> tpr, fpr = tp / (tp + fn), fp / (fp + tn)<br>        return np.trapz(np.r_[0, tpr[::-1], 1], np.r_[0, fpr[::-1], 1])</pre><p><strong>Precision@K (Group-wise)<br></strong>Precision@K is a common metric used for ranking systems. It measures the fraction of the top K ranked items that are relevant.</p><p>This metric is calculated within an individual segment of data, such as a session or request. A segment may be identified by a single key or a composite key.</p><p>The procedure for calculating Precision@K is as follows.</p><p><strong>Group/Rank Dataset<br></strong>Group the dataset by segment and sort by predicted score.</p><p><strong>Window and Count<br></strong>Select the top K items and count the relevant items within that window.</p><p><strong>Calculate Metric<br></strong>Divide the number of relevant items by K to calculate Precision@K for each segment. Average the per-segment scores to obtain the final result.</p><p>On our data with K = 2:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dtCI5DIvd5RNlQ7sPsfIqA.png" /></figure><p>Average = (1.00 + 0.50) / 2 = 0.75.</p><p>We observe this metric has a similar decomposition pattern. We can break it down into subproblems where we look at each segment, calculate the metric, then average final scores of all subproblems.</p><p>We have two problems when trying to scale this approach:</p><ol><li>The subproblem (batch_size) is tied to the size of each segment. This is inefficient as each executor may receive a small and variably sized amount of data.</li><li>If we combine segments together, we must ensure that no segments span a bucket boundary.</li></ol><p>Our goal is to keep every row for a given segment in the same bucket while ensuring that each bucket contains enough segments to saturate the compute resources.</p><p>More succinctly, we want all rows within each segment to be colocated and each bucket to be sufficiently large. Since we are operating at the global level, we can define this as an Apache Spark procedure. It keeps all rows for each segment together, then packs complete segments into buckets large enough to use each Ray worker efficiently without splitting a segment across buckets.</p><pre>class PrecisionAtK(ReducibleMetric):<br>    def dataset_preprocessor(self, ds):<br>  # Colocate each segment and pack into buckets.<br>        return bucket_complete_segments(<br>            ds,<br>            by=&quot;request_id&quot;,<br>        )<br><br>    def compute_partials(self, bucket):<br>  # Rank within each segment and calculate top-K score.<br>        scores = []<br>        for segment in group_by(bucket, &quot;request_id&quot;):<br>            ranking = np.argsort(<br>                -segment[&quot;model_output&quot;]<br>            )<br>      # Assume each segment contains at least K items.<br>            top_k = ranking[:self.k]<br>            relevant = segment[&quot;label_binarized&quot;][top_k].sum()<br>            scores.append(relevant / self.k)<br><br>        return sum(scores), len(scores)<br><br>    def reduce_partials(self, partials):<br>  # Sum segment scores and divide by segment count.<br>        score, count = np.sum(<br>            partials,<br>            axis=0,<br>        )<br>        return score / count</pre><h4>Fusing Metrics with Inference</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OMUSPm2j8BSisD6mPlilsQ.png" /><figcaption><strong>Diagram 3:</strong> Top Sequential Inference and Metric Compute. Bottom Fused Inference and Metrics Compute. Each cell is one unit of work indexed by bucket, head, and experiment: <strong>B = bucket</strong>, a group of rows that keeps each user/request segment colocated for group-wise metrics such as Hits@K; <strong>H = model head; E = perturbation experiment</strong>, with 0 denoting baseline. The sequential layout runs all inference before any metrics; the fused layout interleaves metric compute per bucket as inference completes. Internal inference batches are not shown.</figcaption></figure><p>In the upper figure in diagram 3, we show a typical Sequential Inference-Metrics multi-experiment pipeline. We run model inference over all the experiments, serialize, and persist those results along with the carryover labels. Note this inference dataset scales with the input data and the number of experiments. Next, we deserialize this data and compute metrics. GPU inference is typically more resource intensive than metrics computation. This design causes the CPU workers to sit idle during inference time.</p><p>The lower figure in diagram 3 illustrates a fused approach where we pipeline the metric computation once inference completes for each bucket. The shared object store minimizes serde overhead. This enables us to effectively overlap metric computation with inference, reducing total runtime.</p><h4>Results</h4><p>We benchmarked two setups against a Pin Ranking Model, sweeping across three dataset sizes (Small ≈ 30K, Medium ≈ 300K, Large ≈ 3M rows) and several experiment counts:</p><ul><li><strong>Unfused:</strong> Ray inference, then a <strong>separate</strong> Ray metric job.</li><li><strong>Fused:</strong> Ray inference <strong>with metrics in the same pass.</strong></li></ul><p>When we gather metrics during inference, we do not see any meaningful runtime increases. The time measured here includes startup time (acquiring the cluster) and runtime (actual computation).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H7f3gpwFikQMfV-pd7YRDg.png" /><figcaption><strong>Diagram 4</strong>: End-to-end wall-clock time (startup + runtime) for the unfused and fused pipelines across three dataset sizes at E = 100 experiments. Gray = unfused, indigo = fused, green = time saved. At E = 100, fusion cuts wall-clock time 25–32% at every size. This comes partly from removing a whole job and its fixed cluster-acquisition cost, and the saving holds as input grows.</figcaption></figure><p>Furthermore, we can see that runtime grows linearly with experiment count while startup stays flat.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rgTfLzc5bjkYdNbjPkNaEg.png" /><figcaption><strong>Diagram 5</strong>: Unfused versus fused pipeline runtime as experiment count scales on a Large dataset (3M rows). The unfused runtime includes the metrics stage immediately after inference; both runtimes exclude cluster-acquisition time.</figcaption></figure><p>As the number of experiments increases from 1 to 300 on the Large dataset, fused runtime grows approximately linearly (~43 min fixed + ~41s per experiment), while startup time remains roughly constant. The separate metric stage (~11s per experiment) is work that fusion absorbs through overlap with inference, keeping the marginal cost per experiment small and predictable.</p><h3>What’s next</h3><p>Other upcoming topics include:</p><ul><li>Ray Tune at Pinterest</li><li>Improving the data loader performance in Ray</li><li>Fault-tolerant Ray Batch Inference and Training</li><li>Feature Importance on Ray</li></ul><h3>Acknowledgments</h3><p>Related Pins: Shivin Thukral, Zilin Ren<br>AI Platform: Rubin Fergerson IV, Qingxian Lai<br>Data Platform Sciences: Darren Reger, Adam Obeng<br>Agent Platform: Tan Wang</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4a96cd12cc11" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/scaling-model-evaluation-at-pinterest-with-ray-part-4-4a96cd12cc11">Scaling Model Evaluation at Pinterest with Ray (Part 4)</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing ModuleLens: Zooming into PyTorch Modules for Human and Agentic Engineering]]></title>
            <link>https://medium.com/pinterest-engineering/introducing-modulelens-zooming-into-pytorch-modules-for-human-and-agentic-engineering-b7e9be9d29f8?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/b7e9be9d29f8</guid>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[agents]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Mon, 19 Oct 2026 15:01:04 GMT</pubDate>
            <atom:updated>2026-10-19T15:01:04.745Z</atom:updated>
            <content:encoded><![CDATA[<p>Lida Li, Sr. Staff Software Engineer, Product ML Infra</p><h3>Introduction</h3><p>Modern recommendation systems, including those used for ads ranking and content discovery, have grown substantially in scale and complexity. This growth stems from both increasingly large model architectures. The resulting memory footprint often exceeds the capacity of a single GPU, making it necessary to shard production models across multiple GPUs using distributed training techniques such as Distributed Model Parallelism (DMP).</p><p>This evolution brings challenges to the everyday engineer work on testing and debugging the model code. The bottleneck includes:</p><ul><li><strong><em>Local Machine Hardware Limits</em></strong>: Because production models require multi-GPU setups, initializing a full model locally on a single-gpu workstation is virtually impossible.</li><li><strong><em>Input Data Dependencies</em></strong>: When an engineer modifies code inside a specific downstream module, getting realistic input data is a major hurdle. Traditionally, obtaining that input requires initializing the entire multi-GPU pipeline from scratch and executing every preceding module in the network — wasting immense amounts of time and computation resources.</li><li><strong><em>Slow Iteration Feedback Loops</em>:</strong> Engineers face prolonged iteration loops when attempting to verify sub-module changes, as every code edit requires re-executing or re-initializing heavy distributed trainer setups.</li></ul><p>To eliminate these bottlenecks, we build <strong>ModuleLens</strong>, which pivots from a legacy “Model-First” paradigm to an isolated “Module-First” engineering framework. The following table contrasts these two operational paradigms across key engineering metrics:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BzwFhaH5R5hIUmjx1mEKow.png" /></figure><p>Under the module-first paradigm, engineers can isolate any given module, avoid multi-GPU model initialization entirely, and reconstruct and profile it locally within seconds. In addition to accelerating developer workflows, this modular foundation offers a fast, structured execution environment that <strong>empowers autonomous AI agents</strong> to analyze, refactor, and optimize PyTorch code independently.</p><h3>Architecture</h3><p>The architecture of ModuleLens is organized into three primary components: ModuleTracer for capturing runtime data, Module Repo for central storage, and a Consumption Layer serving human interfaces and AI-based tooling. The following diagram depicts this structure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M6YXooY-Qb6VW37l_48a-A.png" /></figure><h4>ModuleTracer</h4><p>ModuleTracer is designed to capture and record all essential data — including input and output tensors, model parameter weights, and initialization arguments. This comprehensive tracking allows any target PyTorch module to be reconstructed seamlessly within the consumption layer, enabling offline execution with captured inputs and validation against expected outputs.</p><p>To carry out trace capture, ModuleTracer relies on several key mechanisms:</p><ul><li><strong>Non-Intrusive PyTorch Forward Hooks: </strong>Attaches listeners directly to PyTorch modules to seamlessly log parameter weights (state_dict), input and output tensors, and execution metadata during runtime.</li><li><strong>In-Memory Module Init Tracking: </strong>Ensures target modules can be reconstructed by dynamically tracking initialization arguments (__init__ args and kwargs). It achieves this in memory by inspecting imported classes and patching Python’s __import__ function.</li><li><strong>Resource-Efficient Tracing Loops: </strong>Reduces GPU compute overhead by registering early_stop_func and post_train_step_func callbacks. These wait for model warmup, capture data across a single training step, and terminate early.</li><li><strong>Optimized Handling of Large Tensors: </strong>To efficiently profile large tensors, ModuleTracer automatically enables shape-only tracing (log_shapes), logging solely tensor dimensions so that synthetic inputs can be reconstructed during restoration. Additionally, if an input tensor is shared across multiple modules, ModuleTracer retains only a single copy and uses reference links for all other instances.</li></ul><h4>ModuleRepo</h4><p>Extracted module state is persisted within a queryable backend built on top of the Weights &amp; Biases (WandB) Registry. All captured modules are stored into one registry and organized around Collection and Versioned Artifacts:</p><p><strong>Collection Indexing:</strong> Target collections are indexed using an MD5 hash of the model and module names: hash(model_name, module_name). Additionally, top-level trace executions log overall run history to special collections structured as registered_model_xxx, indexed directly by model name. The registry supports both attribute-based and tag-based search filtering.</p><p><strong>Versioned Artifact Payload</strong>: Every captured Module produces a distinct version entry containing two standardized artifacts:</p><ul><li><em>JSON Metadata File</em>: Contains runtime execution metadata including the model and module metainfo, and the autocast info.</li><li><em>Binary State Payload</em>: Serialized via torch.save, containing the precise input tensors, baseline output tensors, and module parameter state_dict.</li></ul><h4>Consume Layer</h4><p>ModuleLens exposes its capabilities through two tailored surfaces to consume the traced data:</p><p><strong>Human Engineers:</strong> Interactive Jupyter Extension and IPython CLI for hands-on debugging, exploration, and manual profiling.</p><p><strong>AI Agents</strong>: Agent Skills that allow LLM agents (e.g., Claude Code, Cursor) to autonomously discover, rebuild, benchmark, and profile modules without human intervention.</p><p>The consume layer provides the following capabilities:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cynWsOoDlPtSMa_hwj4d_w.png" /></figure><p>The screenshot below illustrates the command-line interface available to users.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4DTZz3E36xkHXiAVF7z7Mg.png" /></figure><h3>Use Cases</h3><p>Ever since its deployment across our Ads and Core models, ModuleLens has also been adopted for various internal tools built by our engineering teams.</p><h4>Training Analyzer</h4><p>Evaluates module-level metrics across different architectures. Through hierarchical visual representations of latency, FLOPs, and memory usage across the module tree, engineers can rapidly identify training bottlenecks.</p><h4>KernelAgent</h4><p>An LLM-driven agent designed to generate high-performance GPU kernels. It leverages captured ModuleLens data to benchmark, profile, test, and validate the accuracy and speed of generated kernels.</p><h4>Scaling Law ROI Tools</h4><p>Evaluates the return on investment (ROI) for model scaling. By utilizing ModuleLens to track training and serving FLOPS, activation and parameter memory usage, and execution throughput, it helps assess infrastructure costs against model scaling gains.</p><h3>Looking Ahead</h3><p>Our immediate priority is stabilizing the core system and fostering adoption across machine learning teams. Concurrently, we are enriching our Agent Skills repository to furnish AI coding agents with advanced optimization capabilities, including automated Triton/CUDA kernel synthesis as well as autonomous training and serving optimization workflows. By building reliable, execution-friendly interfaces for both developers and autonomous agents, ModuleLens optimizes human engineering velocity while laying the groundwork for scalable, fully automated performance optimization.</p><p>Beyond optimization, ModuleLens can serve as an execution substrate for model autoresearch and RSI (recursive self-improvement). By giving agents reproducible access to isolated modules, captured inputs and outputs, and trustworthy performance signals, it enables them to formulate experiments, modify model components, benchmark results, and retain successful changes in a tight feedback loop. Over time, this foundation can support systems that continuously improve both model quality and the tools and strategies used to optimize the model.</p><h3>Acknowledgement</h3><p>This project benefited from the valuable contributions of many individuals. We express our gratitude to Archer Liu, leader of Product ML Infra (PMLI), for his leadership and support, Chen Yang (ATG) for insightful design discussions and feedback; Abhinav Naikawadi (HomeFeed) and Paulo Soares (Ads ranking) for leading testing, adoption, and advocacy efforts; and Jiasong Sun, Xinyi Zhang, Diane Li and Hongze Zhao (Product ML Infra) for their contributions to code development and review.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b7e9be9d29f8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/introducing-modulelens-zooming-into-pytorch-modules-for-human-and-agentic-engineering-b7e9be9d29f8">Introducing ModuleLens: Zooming into PyTorch Modules for Human and Agentic Engineering</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Activity to Intent: Generating User Journeys with LLMs]]></title>
            <link>https://medium.com/pinterest-engineering/from-activity-to-intent-generating-user-journeys-with-llms-9652b9417626?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/9652b9417626</guid>
            <category><![CDATA[user-experience]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Fri, 09 Oct 2026 15:01:02 GMT</pubDate>
            <atom:updated>2026-10-16T23:01:38.400Z</atom:updated>
            <content:encoded><![CDATA[<p>Lin Zhu | Sr. Staff Machine Learning Engineer; Manan Kalra | Machine Learning Engineer II; Byung Eun Jeon | Sr. Machine Learning Engineer; Ye Liu | Staff Machine Learning Engineer; Xiangyi Chen | Sr. Machine Learning Engineer; Jaewon Yang | Principal Machine Learning Engineer; Jinwen Xu | Manager II, Machine Learning Engineering; Tingting Zhu | Sr. Manager, Engineering; Sudarshan Lamkhede | Director, Machine Learning Engineering</p><p>Pinterest is built to get inspired and then turn the inspiration into realization — a dinner, a renovation, a wedding, a new skill. That only works if we understand more than the Pin a user just clicked. The useful unit of personalization isn’t the thing someone just clicked, it’s what they’re trying to accomplish. Such goals unfold over weeks and months, across dozens of sessions, through hundreds of saves.</p><p>We call those goals <strong>journeys</strong>: the intersection of a user’s interests, intent, and context at a point in time. A journey is a sequence of interactions, often spanning sessions, centered on one interest and a clear intent. For example, “Summer dresses” are an interest. “Figuring out what’s in style for a summer wedding I’m attending in August” is a journey. Someone might be interested in five at once: a kitchen remodel, a vegetable garden, learning sourdough and the mix changes as life does.</p><p>If we can name those journeys, recommendations and notifications can help someone finish a project instead of bouncing between unrelated interests. This post is about how we infer them in production: a fine-tuned 4-billion-parameter large language model (LLM) that reads a chronological activity log and emits a ranked list of journey names, in the user’s locale.</p><p>We didn’t start there. The <a href="https://medium.com/pinterest-engineering/identify-user-journeys-at-pinterest-b517f6275b42">initial user journey system</a> clustered keywords and attached attributes with separate machine learning models. This post is the follow-up which covers how we formulate journey inference as generation, how we distill frontier-model quality into a 4B student we can actually serve, how the serving stack holds up at notification scale, and what we’re building next. The recipe is domain-agnostic: any platform with sequential user activity and structured intent targets (e-commerce, streaming, search) can adopt the same activity-to-intent formulation, collapsing classical multi-stage recommendation pipelines into a single generative model, making generative journey inference a general recipe for personalization beyond Pinterest.</p><h3>Why interests aren’t enough</h3><p>Most recommenders optimize the next item. However, journeys on Pinterest are longer-horizon. Planning a kitchen is not the same as a one-off click on a cabinet Pin, and it isn’t well described by a durable interest like “Home Decor”. The cabinet Pin, the backsplash search, and the garden-bed save might be one project, two projects, or noise. Getting that distinction right changes what we retrieve, what we write in a notification, and when we should stay quiet.</p><p>So the output we want is not a taxonomy label. It’s a short, specific, ranked list of a user’s active journeys, with specific names they’d recognize.</p><h3>The legacy clustering system</h3><p>We had almost no labeled journeys when we started, so we stayed lean. We reused pretrained text embeddings, started from a few hundred human examples, and built a multi-stage pipeline:</p><ol><li><strong>Extract keywords</strong> from search queries, annotations and interests on engaged Pins, and Pins on owned boards.</li><li><strong>Embed and cluster</strong> those keywords hierarchically. Each cluster is a journey candidate.</li><li><strong>Name</strong> the cluster by ranking its keywords and taking the top one.</li><li><strong>Rank and diversify</strong> the clusters with a pointwise model, then a similarity penalty so the top of the list isn’t five near-duplicates.</li><li><strong>Classify stage</strong> with heuristics: evergreen vs. situational (roughly, does engagement persist past a few weeks?), and for situational journeys, ongoing vs. ended (time since last engagement).</li></ol><p>That pipeline powered the first journey-aware notifications. Against interest-based notifications, it lifted email click rate by 88%, push open rate by 32%, and positive survey feedback by 23%.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rPutHFYtqfPA2vcx50nd4A.png" /><figcaption>Figure 1: User journey inference via the legacy clustering system</figcaption></figure><p>It also compounded its own errors. Every stage was locally reasonable; together they fragmented coherent goals.</p><ul><li><strong>Fragmentation:</strong> Cabinets, countertops, and backsplash are one kitchen, not three journeys fighting for the same notification slot.</li><li><strong>Generic buckets:</strong> When a cluster was messy, the naming model fell back to the most frequent keywords — “Art”, “Recetas”, “Shopping”. Taxonomically true, useless for personalization.</li><li><strong>Incidental noise:</strong> A handful of meme Pins or celebrity clicks could occupy a top slot. Clustering doesn’t know which activity is a goal and which is a passing click.</li></ul><h3>Journey inference as generation</h3><p>To solve these problems, we stopped treating journey inference as a chain of transformations and started treating it as one act of generation: read the activity, write the intent<strong>.</strong> The model gets a single input prompt containing a semi-static profile plus a chronological activity log, and returns strict JSON — a ranked list of journey names:</p><pre>User Profile:<br>gender: female; language: en-US; country: US; age: 30-35<br>User Owned Boards: Kitchen Reno 2026; Weeknight Dinners; Garden Plans<br>User Activities:<br>2026-06-14 search shaker cabinets.<br>2026-06-14 save subway tile backsplash, kitchen backsplash; quartz countertop, white kitchen.<br>2026-06-12 click raised garden bed, vegetable garden; tomato trellis.<br>2026-06-09 search sourdough starter discard.<br>2026-05-30 save cast iron skillet cornbread, dutch oven recipes.<br>...</pre><pre>{&quot;journeys&quot;: [&quot;Modern Farmhouse Kitchen&quot;, &quot;Raised Bed Vegetable Garden&quot;,<br>              &quot;Sourdough Bread Making&quot;, &quot;Cast Iron Recipes&quot;]}</pre><p>Notice what’s gone: no keyword extraction step, no embeddings, no clusters, no separate naming model. One model reads the log and writes the answer.</p><p>Some deliberate choices in the prompt:</p><ul><li><strong>Actions collapse into a tiny vocabulary</strong>: search, save, like, click, comment, dislike. The model doesn’t need to distinguish a repin from a save-to-device; it needs to know both are strong positives. The prompt states the ordering explicitly: search/save &gt; comment &gt; like &gt; click &gt; dislike.</li><li><strong>Events are grouped by (date, action)</strong>, with a semi-colon (;) separating Pins and a comma (,) separating tags within a Pin. That’s token economy, and it also gives the model a cleaner view of same-session bursts.</li><li><strong>Dislike is deliberately included in the input, as hides are evidence.</strong> The prompt says to suppress a journey whose support is predominantly dislikes unless there is evidence of significant related positive engagements.</li><li><strong>Temperature 0, reasoning tokens off, max 300 output tokens.</strong> We want a deterministic, replayable read of the same log, and with a fixed output schema for decoding purposes. Reasoning is turned off to reduce online inference cost.</li><li><strong>A 360-day lookback, up to 150 events.</strong> Journeys are long-horizon by definition, we experimented with different lookback windows and # of events to balance cost and relevance.</li></ul><p>The output contract is deliberately simple: drop non-strings, dedupe case-insensitively, and the first occurrence wins. The output is parsed defensively because a generative system that occasionally emits prose where you expected JSON is a system that occasionally drops a user’s entire personalization payload.</p><p>The other thing worth saying out loud is that once you collapse the pipeline, the prompt becomes the ranking policy and the safety policy. Everything the staged system encoded in model weights and thresholds now has to be clearly stated. In production it asks for up to about twelve distinct journeys, each 1–5 words, in the user’s locale, ranked by:</p><ol><li>Engagement strength (search/save over click)</li><li>Repeated support</li><li>Recency</li><li>Coherence</li><li>Specificity (“Mid-Century Coffee Table”, not “Furniture”)</li></ol><p>It also encodes the failure modes we observed with the legacy journey clustering system: don’t merge unrelated topics; drop one-off outliers; treat auto-generated tags as noisy; lightly diversify the top of the list only among journeys that are already well supported. Empty output is allowed when the activity doesn’t support anything but inventing unsupported journeys is not.</p><p>These instructions aren’t just decorative. The diversity pass replaced a similarity-penalty diversifier. The dislike rule replaced a hand-tuned negative-signal filter. Few tag-noise instructions exist because auto-generated annotations are genuinely noisy, and the model will happily build a journey out of a classification artifact if you let it. Prompts like this deserve the same review discipline as model code, so we treat changes to them as launches.</p><p>Clustering was the right cold-start system, and it still exists as a fallback. Production inference is now LLM-first: one model, one decode, structured output.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yk5WWtOdEk00T9WnYnJVWA.png" /><figcaption>Figure 2: User journey inference system with feedback</figcaption></figure><h3>Distilling a frontier teacher into a 4B student</h3><p>Frontier models can infer coherent journeys without task-specific training, but they are too expensive to run for hundreds of millions of people on Pinterest regularly. Instead, we use them as teachers to generate synthetic training examples: each activity history is paired with a high-quality target journey. We also found that teacher outputs improved when the model was allowed to use additional reasoning tokens before producing its final structured response.</p><p>We fine-tuned open-weight <a href="https://arxiv.org/abs/2505.09388">Qwen3</a> students with 0.6B, 1.7B, 4B and 8B parameters. Supervised fine-tuning (SFT) teaches the model from the teacher-generated examples, while <a href="https://arxiv.org/abs/2106.09685">low-rank adaptation (LoRA)</a> lets us update small adapters rather than every model parameter.</p><p>Quality improved rapidly during the first few thousand examples, then largely stopped increasing. Adding more similar examples provided little benefit; selecting diverse, high-quality examples — especially difficult consolidation and multilingual cases — was more valuable.</p><p>Model size presented a separate trade-off. The 0.6B and 1.7B students were faster, but produced less relevant and consistent journeys on the harder cases. Among the sizes we tested, the 4B model offered the best balance between output quality and serving throughput, so it became our production student.</p><p>Table 1: Same-user comparison of the staged clustering pipeline against the fine-tuned LLM. Fragments consolidated (Cabinets + Countertops → Modern Farmhouse Kitchen). Generic buckets got specific (Art → Penguin Art Drawing). Dropped intents came back (Cast Iron Recipes). Noise disappeared (Celebrity News, Memes). And user B got journeys in Spanish — something the keyword pipeline structurally couldn’t do, because it named journeys out of an English-annotation vocabulary.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GGtffWBh1hxKMaQhai-Xqg.png" /></figure><h3>Serving at notification scale</h3><p>Scoring hundreds of millions of people on Pinterest is a batch problem with online-shaped latency. We run a full refresh when the algorithm changes, and a daily incremental pass over recently active users, so journeys move when behavior does.</p><p>We benchmarked the offline workflow against serving the same 4B model behind an online inference stack. Online serving won on throughput. The reason is a set of now-mature decode optimizations that a naive offline loop typically doesn’t get for free: PagedAttention, so the key-value cache isn’t wasted on padding; continuous batching, so a finished sequence immediately yields its GPU slot to the next user instead of waiting for the slowest prompt in a fixed batch. The serving stack is <a href="https://github.com/ai-dynamo/dynamo">NVIDIA Dynamo</a> in front of <a href="https://arxiv.org/abs/2309.06180">vLLM</a> decode workers on L40S GPUs. Frontend and decode workers scale independently, and the frontend routes with key-value (KV) cache awareness so requests sharing prompt structure land on workers that already hold the relevant prefix, which matters because every request shares a long, stable system prompt. We enable prefix caching and chunked prefill and cap concurrent sequences per worker.</p><p>In production, a cluster on the order of 100 GPUs sustains about 775 requests per second end-to-end, with a median around 1.2 seconds and p95 around 3.0 seconds. The batch side is where the cheaper wins live. Inference runs as a daily fan-out over journey-eligible users, sharded into small per-job batches with bounded concurrency inside each batch, so a slow model response back-pressures one shard instead of the pipeline. The single most effective throughput lever we added wasn’t a GPU at all: we track when each user was last inferred and skip anyone processed within the last couple of days. Journeys don’t change hour to hour, and paying to re-derive an unchanged journey is the most expensive kind of waste — the kind that looks like progress on a dashboard.</p><p>If a decode fails, we retry that user rather than storing a partial list. An empty but valid JSON list is a successful inference; some users don’t have a supportable journey that day, and we’d rather send nothing journey-specific than a generic “Art”. We suppress inference of sensitive traits at multiple levels before anything is stored:</p><ul><li>Input keywords pass our notification content filter before reaching the prompt, so the model can’t build a journey on a keyword we wouldn’t have sent anyway.</li><li>Output journey names pass that filter and our generative content safety filters, so names that are unsafe in a journey-inference sense are dropped even if they would pass a generic keyword list.</li><li>The model is instructed never to infer or target topics related to health conditions, politics, religion, sexual orientation, gender identity, race, etc.</li><li>Profile inputs are minimized, with ages bucketed at the low end and grouped, because the model needs a rough life stage rather than a birthday. Under-18 users in the EEA, Switzerland, and the UK aren’t eligible yet due to local laws.</li></ul><p>Inference writes a ranked list of journeys per user into a store that notification serving reads at send time, and each journey name then becomes a retrieval query. We built embedding-based retrieval models to pull Pins for each user journey which are further ranked by our Pin ranking models. Journeys the user was recently notified about are fatigued, and journeys they’ve given explicit negative feedback on are suppressed outright. Only then does the notification framework decide whether to send at all, and on which channel.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xmJDorBUBL7Cxc7OP-d7Zg.png" /><figcaption>Figure 3: Notification surfaces for User Journeys</figcaption></figure><h3>How we know it’s better</h3><p>There is no ground-truth label for a user’s latent goal. We evaluate at three levels, from inspection to a causal test.</p><ul><li><strong>Same-user qualitative comparison:</strong> For journey-eligible users, we put clustering output and LLM output side by side on the same user activity. We keep both the strong cases and the honest misses: over-generalization (“Home Cooking” when the log is clearly “Spam Kimchi Fried Rice”) and under-generation (dropping a well-supported side interest). This is slow, but it’s the only way to see fragmentation and language quality that metrics flatten.</li><li><strong>Evidence-grounded diagnosis:</strong> A separate language model scores a sample of emitted journeys on axes we care about in production: relevance to the visible context, support from the profile and activity log (is this name faithful to what we showed the model?), diversity / non-redundancy across the list, and granularity. The support axis is the important one. A fluent, on-brand name that isn’t in the log is a failure, even if a user might have clicked it.</li><li><strong>Online experiment:</strong> Initial experiment ramped to the US and Canada users, with the clustering production system as control, showed lifts of 1.1% on email click-through and 1.3% on push opens, plus multiple statistically significant sitewide engagement and retention gains. A subsequent multilingual international rollout replicated the direction, and shipping both experiments increased the daily, weekly, and monthly active users on the platform.</li></ul><h3>What we’re building now</h3><p>The fine-tuned Qwen-4B model is the production default. Everything in this section is under experiment — trained or rolling out, not what every user gets today.</p><h4>Semantic IDs, so Pins aren’t a bag of tags</h4><p>Tags mean every Pin costs a handful of tokens and drags along a dependency on Pin-to-text mappings. <a href="https://arxiv.org/pdf/2508.10478">Semantic IDs</a> (SIDs) are the alternative: each Pin is compressed by a residual-quantized variational autoencoder (<a href="https://arxiv.org/abs/2203.01941">RQ-VAE</a>) into five levels of 11 bits each, which we pack into a single 64-bit integer and unpack at prompt-build time into special tokens:</p><pre>&lt;sid_begin&gt;&lt;sid1_1263&gt;&lt;sid2_973&gt;&lt;sid3_725&gt;&lt;sid4_793&gt;&lt;sid5_1727&gt;&lt;sid_end&gt;</pre><p>A general-purpose model has no idea what &lt;sid3_725&gt; means, so we bridge the gap with continued pre-training (CPT): extend the tokenizer with the SID vocabulary, then run next-token prediction over interleaved SID sequences, Pin titles, and tags until the model has mapped its world knowledge onto our catalog. SFT goes on top, so that given a SID sequence the model writes journeys.</p><p>SIDs only work better for users with dense engagement, so production is a fallback ladder: fewer than ten SID-carrying events and we fall back to tags for the whole prompt; a single event missing a SID falls back to tags for that event; and search queries always keep their literal text, because a typed query is the highest-fidelity signal we have. The bet is fewer input tokens, no dependency on annotations, and a shared item language between recommenders and LLMs. The cost is interpretability — a tag-based prompt can be read by a human mid-debugging session. A SID prompt cannot.</p><h4>Offsite conversions as a complementary signal</h4><p>Onsite search and saves remain the strongest intent signals, but they can be sparse for dormant or low-activity users. We therefore use recent offsite actions — such as page visits, add-to-carts, sign-ups, and checkouts — as a complementary signal, adding a compact, safety-filtered summary to capture timely shopping or planning intent beyond Pinterest.</p><p>Offsite evidence remains secondary: onsite outweighs offsite, and checkout is stronger than add-to-cart or a page visit. We only add it when onsite activity is sparse, cap its prompt footprint, and avoid ranking offsite-only journeys at the top. We are testing whether better prompting is enough or whether a fine-tuned offsite-aware model is needed.</p><h4>One model, several products</h4><p>Once inference is generative, adding an output is a prompt-and-fine-tune change rather than a new service.</p><ul><li><strong>Multi-granularity journeys:</strong> Rather than a flat list of names, have one call emit each journey together with deeper cuts of it and adjacent topics that broaden discovery, so “Sourdough Bread Making” comes back with a specific next step into the interest and a sibling topic a user might jump to. That gives downstream retrieval a ladder of intent per user instead of a single rung.</li><li><strong>Copy titles</strong>: A good journey name isn’t always good notification copy, and we had engagement data showing which names underperform as titles (“Photography”, “Hairstyles”, “Quotes” — the generic ones again). One model now produces both, with a hard ordering constraint: finalize the journey names first, then write copy, and never reshape a name to make its copy sound better.</li></ul><h4>Closing the loop with User feedback</h4><p>Distillation got us strong journeys, but it has a ceiling. The student is optimized to match the teacher, so its quality is bounded by the teacher’s sense of what users want. To go further, we need to optimize directly toward user preferences, using real feedback from users themselves.</p><p>Much of that signal already exists. Journey emails include a module asking “Are you interested in &lt;journey&gt;?” with Yes and No buttons. Each answer is logged, and a No immediately suppresses that journey for that user. Beyond these explicit answers, engagement inside journey-aware notification sessions gives us a much larger implicit signal: saves, clicks, and hides on the Pins we retrieved for a journey.</p><p>This feedback comes with a caveat: it is pointwise. Each response is a verdict on a single journey, while the model generates a ranked list. To bridge that gap, we split training into two stages. First, a mid-training stage applies preference optimization to single journeys across a large volume of feedback. Given a user’s activity, the model learns to make a journey that users confirm or engage with more likely, and a journey they reject less likely. Then, we formulated feedback-as-edit: the chosen response is the model’s list with the Yes journeys kept or the No journeys removed, and the rejected response applies the reverse edit. Starting from the mid-training checkpoint, fine-tuning restores list-level behavior: ranking, diversity, and the output format described earlier.</p><p>Improving the LLM with preference optimization and real user feedback is still an active area of experimentation. We are exploring how to weight explicit versus implicit feedback, and how to formulate a loss that is amenable to scaling, one whose gains follow a scaling law as feedback volume grows. Offline results are promising, and online A/B tests are now running. We hope to share more details in a follow-up blog post or publication.</p><h3>Acknowledgements</h3><p>We’d like to thank Justin Tran, Darien Boyd, Sarah Schwager, Shipeng Yu, Randy Tumalle, James Hernandez, Mengtian Hu, Dimitra Tsiaousi, Yaxin Li, Ying Wei, Sam Owens, Gloria Lee, Justin Pangilinan, Ye Tian, Rui Liu, Anya Trivedi, Binghui Gong, Tianqi Wang, Fangzheng Tian, Eric Tam, Mengying Yang, Lauren Fratamico, Andrew von Rosenbach, Ignacy Debicki, Zhenyu Tan, Jiajing Xu, and other contributors from Notifications, ATG Applied Science, ML Platform, GenAI Serving, and Content Safety teams for their insights and support!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9652b9417626" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/from-activity-to-intent-generating-user-journeys-with-llms-9652b9417626">From Activity to Intent: Generating User Journeys with LLMs</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Metrics Board: Building an Agent-ready Metrics Layer]]></title>
            <link>https://medium.com/pinterest-engineering/metrics-board-building-an-agent-ready-metrics-layer-2c8fefe68756?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/2c8fefe68756</guid>
            <category><![CDATA[metrics]]></category>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[data-governance]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Tue, 06 Oct 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-10-06T16:01:02.147Z</atom:updated>
            <content:encoded><![CDATA[<p>Michele Ceccacci; Software Engineer I | Jason Coffman; Sr. Software Engineer | Colm O’Shaughnessy; Software Engineer II | Laura Palmer; Staff Product Manager | Adam Podraza; Manager, Engineering | Surya Karri; Manager, Engineering</p><p>At Pinterest, reliable and trustworthy<strong> </strong>metrics are behind every decision: from measuring company-wide business performance to evaluating each feature experiment results. Hundreds of data producers — analysts, data scientists, and engineers across Pinterest — create thousands of metrics from our petabyte scale data lake. A metrics ecosystem this size can’t be ad hoc; it has to be organized, scalable, and resilient. So we built <em>Metrics Board</em>: an end-to-end platform that automates metric creation, enforces quality and enables agent-driven analytics.</p><p>In this post we walk through the product experience, architecture, integrations and explain some of the design choices we made to make metrics work at planet scale.</p><h3>Why Metrics Matter at Pinterest</h3><p>Most organizations rely on a familiar set of metrics — revenue, growth, engagement — to steer the business.</p><p>Metrics are how we understand whether Pinterest is delivering value to users and advertisers. Measures related to areas such as revenue, growth, and engagement turn the complexity of user behavior and Pinterest performance into a shared view of what is working, where we are falling short, and what matters most. These metrics drive the decisions our product, finance, and executive leaders make every day, from what to build to how we forecast and steer company strategy.</p><p>Metrics also power Pinterest’s product experimentation practice, which runs at massive scale — thousands of experiments are active at any given time. Those experiments are used as gates for each new feature launch. For results to be fair, comparable, and statistically significant, metrics must be clearly defined, reliable, and reusable. Beyond common core metrics, individual experiments often need bespoke metrics scoped to a single module, feature, or device. For example, a feature to add a new form of interaction with a Pin might need its own measure to evaluate engagement with the feature — even if core product engagement metrics are flat, the experiment might be successful if it creates a new form of user-engagement that a new metric would need to measure.</p><p>As much as key decision-makers need metrics, the way they get answers from data has changed too. Increasingly, the question isn’t typed as a SQL query or a dashboard filter — it’s asked in natural language to an agent. But agents are only as good as the trusted data they can access. For agentic analytics to work, metrics have to be discoverable, quality-checked and machine-readable.</p><h3>Before <em>Metrics Board</em></h3><p>In the past, users were defining their metrics in bespoke data pipelines and wiring them by hand into our reporting and experimentation products. That approach had several problems:</p><ul><li>Time spent building similar data pipelines by hand was slow</li><li>Inconsistent monitoring and availability were inconsistent, so breakages often only surfaced downstream</li><li>Low awareness of existing metrics, leading to redundancy and trust erosion</li><li>Metric metadata drifted from one place to the next with no source of truth</li></ul><p>Put together, these problems slowed teams down, held experiments back, and left us with more metrics than we could trust. We needed a solution that would:</p><ul><li>Unify metrics across the organization</li><li>Give us transparent definitions and code versioning</li><li>Automate pipeline creation, orchestration and operations as much as possible</li><li>Require and auto-generate documentation and metadata</li><li>Automatically validate and monitor metrics for accuracy, reliability, and anomalies</li><li>Give every metric and dashboard a clear, shared signal of quality and trust</li></ul><p>Our goal was to make metrics easy for everyone who touches them — the people who produce them, the people who consume them, the applications that serve them, and, increasingly, the agents that reason over them.</p><h3>The Solution</h3><p><em>Define a metric once, Metrics Board handles the rest.</em></p><p>We built <em>Metrics Board</em> as a thin semantic layer for metrics on top of the data tools Pinterest already runs: a metric is defined once, and that definition drives everything downstream. It’s built around five pillars: <strong>inventory, compute, quality, publishing, and serving &amp; discovery.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GTyQEs8yUPE3Pr2GsE2GgQ.png" /></figure><h4>1. Inventory</h4><p>The inventory pillar is our single source of truth for metric definitions. Every metric lives as a YAML spec in a dedicated Git repo, and is treated like any other production code, with version control, code review, and change history built in.</p><pre>version: 1<br>metric_id: mb__metrics<br>display_name: Metrics Board metrics count<br>description: |-<br>  Count of metrics defined in MB. Test metrics aren&#39;t included.<br>domain: tech_data<br>metric_sql: |-<br>  with mb as (<br>    SELECT urn<br>    FROM query_analytics.pincat_db_latest_state_v2<br>    WHERE dt = &#39;{end_date}&#39;<br>    AND aspect = &#39;structuredProperties&#39;<br>    AND is_active<br>  )<br>  SELECT <br>    COUNT(mb.urn) as mb_metrics,<br>    &#39;{end_date}&#39; as dt,<br>  FROM mb<br>temporal_columns:<br>  - column_name: dt<br>    format: YYYY-MM-DD<br>    granularity: DAY<br>integrations:<br>  superset:<br>    enabled: true<br>  starrocks:<br>    enabled: false<br>ownership:<br>  nimbus_project: ii<br>compute:<br>  schedule: 0 9 * * *</pre><p>One important consideration at the time of design was how much should <em>Metrics Board</em> be a true semantic layer. Do we enforce a model on top of existing datasets and have metrics defined as expressions leveraging the model? Or do we accept any SQL query and ask users to provide modelling information (temporal columns, metric or dimensions definitions) for the resulting data? We decided on the latter, driven by two main considerations: user feedback (“Put Pinners First” is one of our company values) and the sheer variety of tables which are used for metrics definitions — enforcing a semantic layer for each was simply not realistic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NEqwg2CaIyxEYURbe_yiMw.png" /></figure><p>We don’t ask teams to craft definitions by hand. The <em>Metrics Board</em> UI is a dedicated interface: users define metrics through guided forms, and the system generates the corresponding YAML and opens a pull request on their behalf. The result is one place to manage ownership, governance and metadata, that still integrates cleanly with the rest of the data stack.</p><h4>2. Compute</h4><p>To compute metrics, <em>Metrics Board</em> turns each definition into its own scheduled Airflow DAG. A definition carries all the metadata we need — schedule, SQL, validations, integrations — to generate a dedicated workflow. Isolating each metric in its own DAG lets us tune schedules, retries, and resources individually, and prevents a failure in one cascading to others.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K22Ozm8haDY4XYtn431kFw.png" /></figure><p>At execution time, <em>Metrics Board</em> compiles each metric’s SQL into SparkSQL or PrestoSQL depending on the engine named in the definition. It then analyzes the SQL code to automatically create dependency tasks which wait on upstream data to be ready (we rely on <a href="https://sqlglot.com/sqlglot.html">sqlglot</a> for parsing). It also automatically generates standard data quality checks and lets users define additional ones too (more on that below). Backfills are managed through Airflow, so owners can recompute history easily. <em>Metrics Board</em> also provides a layer for automated operations. For example if upstream data is late, <em>Metrics Board</em> automatically detects when it lands and resumes the metric computation DAG.</p><p>Taken together, the above listed features make metrics creation and management easy even for non-data teams. What used to take days or weeks to build can now be achieved in 1–2 hours — a significant boost for users’ development and experiment velocity.</p><h4>3. Quality</h4><p>A metric nobody trusts is worse than no metric. Every definition can declare validations that run as part of the metric’s own pipeline, backed by two engines:</p><ul><li><strong>Rule-based checks</strong> for explicit expectations: day-over-day, week-over-week, month-over-month, and year-over-year thresholds, custom threshold rules, and comparisons against a parent metric — each applied to either a single point in time or a trend across time.</li><li><strong>ML-based anomaly detection</strong> that models a metric’s historical patterns — seasonality, trend, and normal variation — and surfaces deviations which a static rule would miss (you can read more about <a href="https://medium.com/pinterest-engineering/warden-real-time-anomaly-detection-at-pinterest-210c122f6afa">Warden here</a>).</li></ul><p>When a check fails, the right people hear about it in real time. Alerts fire as soon as validation runs, route to the owning team, and can be snoozed or acknowledged so the signal stays meaningful and doesn’t turn into noise.</p><p>All of this rolls up into a <strong>quality report</strong> per metric — per-check pass rates, a time series of results, gaps where validation didn’t run, and annotations (comments or linked tickets) — available via API and as a PDF export. Because the validations live in the same spec as the metric, quality is versioned and reviewed alongside the definition itself.</p><h4>4. Publishing</h4><p>A metric that lives only in its own pipeline isn’t useful yet — it has to reach the products people actually work in. Publishing does that automatically, driven entirely by the definition. Each metric declares its integrations, and turning a surface on is as simple as a flag: enable it, and <em>Metrics Board</em> fans the metric out to that surface with no per-surface wiring by hand. Today a single definition can publish to:</p><ul><li><strong>PinCat (based on DataHub)</strong>, our data catalog and metadata hub, so the metric is discoverable and its lineage is tracked</li><li><strong>Helium</strong>, our experimentation platform, so the metric is available to experiments the moment it’s defined</li><li><strong>Superset</strong>, our BI layer, where <em>Metrics Board</em> creates or updates the dataset and can stand up a monitoring dashboard automatically</li><li><strong>StarRocks</strong>, our analytics warehouse, for fast interactive querying</li></ul><p>Publishing happens in two phases:</p><ol><li><strong>Metadata-level integrations (DataHub, Helium)</strong> publish at commit time as soon as the definition merges. The metric is catalogued and available to experiments before it has even run.</li><li><strong>Data-level integrations (Superset, StarRocks)</strong> publish at compute time after each run when there’s fresh data to export.</li></ol><p>Either way, the outputs (dataset IDs, the dashboard link, the output table, the pipeline URL) are written back to DataHub, so the catalog stays the single place to find a metric and everything attached to it.</p><h4>5. Serving &amp; Discovery</h4><p><strong>Discovery &amp; governance.</strong> <em>Metrics Board</em> is where the org finds and evaluates metrics — but consumers have to be able to find a metric before they can evaluate it. Metrics Board centralizes definitions into a single source of truth, capturing rich metadata alongside them: ownership, description, certification grade, and quality signals.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dnTGdHiAsp4nh3KLx_Jvow.png" /></figure><p>That metadata is what turns centralization into discovery. Every metric automatically flows into PinCat, so users can discover them in the same place they already go for datasets. With metrics easy to find, the next question is what to trust. Two signals do that work, and they operate at two levels.</p><p><em>At the metric level</em>, every metric carries a <strong>grade</strong> — a certification level that climbs from <strong>Unknown</strong> through <strong>C</strong>, <strong>B</strong>, and <strong>A</strong> up to <strong>A+</strong> (fully certified and manually verified by a Data Quality Steward), with each rung demanding more quality work. It tells consumers what to trust and shows owners exactly how to level up. Furthermore, a metric’s key metadata — grade, certification state, and owner — is surfaced directly within its Superset chart.</p><p><em>At the reporting level</em>, dashboards in Superset are classified into their own three <strong>tiers</strong> that reflect how critical and durable they are, and a dashboard’s tier drives three concrete things: how long it’s <strong>retained</strong> when unused, which Presto cluster its queries run on (and therefore its <strong>performance</strong>), and what <strong>documentation</strong> it must carry.</p><ul><li><strong>Tier 1</strong> is a small, curated set of company-critical dashboards. They carry the fullest documentation and are formally certified, and in return get the strongest guarantees — priority query performance and protection from automatic cleanup — with certification renewed periodically to keep them trustworthy.</li><li><strong>Tier 2</strong> is for team-level dashboards others rely on. They require basic ownership and documentation and are retained as long as they see regular use.</li><li><strong>Tier 3</strong> is the default for exploratory or test dashboards. They need no extra metadata and are cleaned up automatically once they go unused.</li></ul><p>Together, metric grade and dashboard tier give consumers a trust signal at every layer of the stack, while retention and query-lane routing fall out of the dashboard tier automatically.</p><p><strong>Agent enablement.</strong> The same structured metadata that powers discovery makes <em>Metrics Board</em> a foundational platform for analytics agents. We expose an MCP server so any LLM-based tools can find metrics, read their definitions, and reason about them. The clearest example is our <a href="https://medium.com/pinterest-engineering/unified-context-intent-embeddings-for-scalable-text-to-sql-793635e60aac">Analytics Agent</a>, which can answer natural-language questions about metrics. By referencing golden queries and certified metric definitions, the agent ensures its responses are grounded in trusted, governed data. As it turns out, the work we did to make metrics trustworthy for humans is what makes them usable by agents.</p><h3>Impact</h3><p>Adoption is the primary success measure we track, and we’ve exceeded adoption goals quarter after quarter — both in metrics created and in active users. Today nearly <strong>150 new metrics</strong> are created each month (we also built out a lifecycle policy and engine to garbage-collect metrics no longer in use). Over 98% of metrics created for experimentation go through the Metrics Board today.</p><p>That adoption rides on dramatically improved velocity. On one recent high-profile project revamping key engagement metrics, <em>Metrics Board</em> saved roughly <strong>two engineering-months</strong> versus the old approach; metric additions that once took weeks now take days. At scale those savings translate to thousands of hours saved each month.</p><p>And velocity isn’t the whole story: the governance, quality, and cataloging we built along the way are quickly becoming the foundation for the LLM and agentic use cases we’re now shipping.</p><h3>Acknowledgments</h3><p>Analytics Platform: Krishna Gopal<br>Workflow Platform: Dinghang Yu, Martin Yau<br>Metrics Quality: Dennis Marcu, Raj Phull<br>Experiment Platform: Lu Yang, Xinyue Cao<br>Real-time Analytics: Charles Wu</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2c8fefe68756" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/metrics-board-building-an-agent-ready-metrics-layer-2c8fefe68756">Metrics Board: Building an Agent-ready Metrics Layer</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Partition Finalization in Pinterest’s Next-Generation DB Ingestion Framework]]></title>
            <link>https://medium.com/pinterest-engineering/partition-finalization-in-pinterests-next-generation-db-ingestion-framework-4c7da6e4cc8f?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/4c7da6e4cc8f</guid>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[icebergs]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[flink]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 15:01:03 GMT</pubDate>
            <atom:updated>2026-09-25T15:01:03.108Z</atom:updated>
            <content:encoded><![CDATA[<p>Qianrui Zhang | Sr Software Engineer, Logging Platform<br>Kanchi Masalia | Software Engineer II, Stream Processing Platform<br>Liang Mou | Sr Staff Software Engineer, Logging Platform<br>Yi Pan | Principal Engineer, Agent Platform</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RYnr5fSTAfCGM8R9wcHr_w.png" /></figure><h3>Introduction</h3><p>This is the third post in our series on Pinterest’s next-generation database ingestion framework. <a href="https://medium.com/pinterest-engineering/next-generation-db-ingestion-at-pinterest-66844b7153b7">Part 1</a> introduced the DB ingestion framework built on Kafka, Flink, Spark, and Iceberg, and <a href="https://medium.com/pinterest-engineering/automated-schema-evolution-in-pinterests-next-generation-db-ingestion-framework-36c5c07070de">Part 2</a> covered automated schema evolution. This post tackles another challenge in migrating downstream customers to the new ingestion framework: knowing when data is <strong>complete</strong> enough to read.</p><p>We’ll walk through that data completeness challenge and how we solve it: what “partition finalization” means, why it matters to downstream customers, how we built a unified mechanism to generate partition finalization markers, and how those markers are consumed. We’ll also look at how this mechanism generalizes beyond DB ingestion.</p><h3>Background &amp; Motivation</h3><p>Before explaining partition finalization, it helps to define what a partition is and why a downstream consumer cares when one partition is “done.”</p><h4>What is a Partition</h4><p>A time partition is a subset of a table whose rows are grouped by a time column, e.g. all rows whose timestamp falls within a given hour. Grouping data this way lets a consumer read just the slice it needs, such as the 1 AM hour, instead of scanning the whole table.</p><p>How that grouping is physically represented depends on the table format:</p><ul><li><strong>Hive</strong> uses explicit, directory-based partitioning: each partition maps to a physical directory in S3.</li><li><strong>Iceberg</strong> uses hidden partitioning: the partitioning is defined in the metadata layer rather than by directory layout, so consumers don’t need to know the physical file layout to query a partition.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1iXUPa7YgtrHOjGb9VSS3g.png" /></figure><h4>What Finalization Means</h4><p>Now consider a typical consumer of these tables: a downstream batch job that processes a single hour’s partition and runs only once for that hour. The consumer faces the question: when should my job run?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qEll5LdpLTmZZKZj3EV8gw.png" /></figure><p>The answer is straightforward: Because the job runs only once, it should run only after the target partition is ‘finalized’, i.e. data in that partition is less likely to change. If it runs too early and the partition is still being updated after the run, it will miss later updates in that partition. And to know when a partition is finalized, it relies on the producer to put some signals on the data. For example, the producer can emit a lightweight marker in the table location when it thinks the partition is finalized, and the consumer will use a sensor to check that marker.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0vt-ThgywkTnELreCWlSBw.png" /></figure><h4>Challenge: Finalization in the Stream-based DB Ingestion Framework</h4><p>In the previous batch-based system, finalization was trivial. The data was produced by a single batch job that read the whole source DB and wrote it in one shot, so once that job finished and there is data in a partition, we can consider that as finalized and downstream jobs could start consuming.</p><p>The new framework is stream-based. A Flink job writes change events continuously, committing many small batches into a partition over time. Because a record’s event time (when the change happened in the source) can lag when we process it, a partition can keep receiving data after its wall-clock hour has passed and no single moment marks it “done”, and the presence of data in a partition no longer means the partition is complete.</p><p>So finalization is no longer a byproduct of a job finishing. We have to infer it from the stream itself uniformly across the thousands of pipelines the framework supports, and a new mechanism is needed to support this.</p><h3>Our Solution: EventTime Based Partition Finalization</h3><p>We introduced a partition finalization mechanism into the streaming layer of the pipeline. The same Flink job that writes the CDC table also measures how far its event time has progressed and publishes a finalization marker into the destination Iceberg table, where downstream jobs can wait on it via a sensor before they start consuming.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pCiTbP6fNdAN7slAtu_FWg.png" /></figure><h3>Architecture Overview</h3><p>There is no change in the underlying data flow as in <a href="https://medium.com/pinterest-engineering/next-generation-db-ingestion-at-pinterest-66844b7153b7">Part 1</a> (see below figure, blue components are added for partition finalization): Kafka carries CDC events, Flink writes them into the CDC Iceberg table, and Spark upserts into the base table. The partition finalization logic mainly happens inside the Flink-to-Iceberg sink, plus one piece of published metadata that can be carried over in the Iceberg table.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D2YjOOgWRVK8SJQTvw380w.png" /></figure><h3>Partition Finalization Logic</h3><p>We’ll start with how a Flink-to-Iceberg pipeline works in general. As Flink processes the event stream, it first writes incoming records into data files. Those files aren’t visible to readers right away; they become visible during Flink’s <strong>checkpoint</strong> process, which runs every few minutes (configurable) to persist the job’s progress for fault tolerance. Every checkpoint produces a new snapshot in the Iceberg table: an atomic commit that also carries a small summary of metadata describing it. It works much like a Git commit, with each snapshot recording a new visible state of the table on top of the last.</p><p>With this process in mind, our partition finalization logic executes in the following procedures:</p><p><strong>Event time extraction</strong></p><p>Everything starts from a single value per record: the CDC event’s <strong>event time</strong>. Each pipeline configures which column carries it, and an extractor reads that column and normalizes it to a timestamp. Because the extractor is pluggable, the same machinery works for any table by simply pointing it at that table’s event-time column.</p><p><strong>Fact collection: event time statistics</strong></p><p>Between checkpoints, we collect the event time of every record processed within that window and track statistics over them. The core facts are the minimum and maximum event time seen in that window. On top of those, we also capture percentiles, e.g. the 99th and 95th percentile of event time, so we retain the shape of the distribution rather than just its extremes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ezKGoAR8KDcdR_zevdDrRA.png" /></figure><p><strong>Fact storage: Iceberg snapshot summary</strong></p><p>When a checkpoint commits, these statistics are written into the Iceberg snapshot’s summary, the per-snapshot metadata Iceberg already maintains for each commit. Every commit therefore carries its own self-describing record of what event times it contained, with no external store to keep in sync.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*42pFWz149Zu7BYc-8oRmWw.png" /></figure><p>To keep the footprint small, we store the percentiles in a <a href="https://datasketches.apache.org/docs/tdigest/tdigest.html">t-digest</a>, a compact sketch that approximates a distribution in a small, fixed amount of space. The summary stays around a few hundred bytes whether a checkpoint saw a thousand records or millions, which is what makes it affordable to attach to every commit. The sketch is also mergeable, so partial statistics computed in parallel combine into a single summary at checkpoint time.</p><p><strong>From fact to opinion: partition finalization watermark</strong></p><p>After each commit, the framework turns those facts (the per-commit event-time statistics, such as the minimum event time seen in each checkpoint) into an opinion: how far the partition is finalized. It runs an algorithm over the statistics from recent commits and produces a watermark that marks the point up to which the partition is considered complete. By default, it takes the minimum of the per-commit minimum event times across the last X commits, then rounds down to the hour boundary.</p><p>The watermark is kept monotonically increasing. If late-arriving data would pull it backward, the value holds instead of regressing, so a finalized hour stays finalized. When that happens, the application can also send an alert to the data consumer, flagging that late data arrived after the partition was finalized.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Whlx3fTum9f-so8vGzghCw.png" /></figure><p><strong>Opinion storage: Iceberg table property</strong></p><p>Unlike the facts which live on individual snapshots, the opinion is a single table-level value, so we store the partition finalization watermark as an Iceberg table property. Downstream jobs read it with an ordinary metadata lookup and gate their processing on it, without the need of any side channel or separate service.</p><p><strong>Finalization Metadata Propagation</strong></p><p>Since the statistics and finalization watermarks all live in Iceberg metadata, they are easy to propagate. Finalization is computed on the CDC table, but many consumers read the base table produced by the Spark upsert job, which carries the data but not the signal.</p><p>We close that gap in the upsert job: on each run it reads the finalization watermark from the CDC table and writes it onto the base table in the same commit. The signal follows the data to where consumers read it, and because these are ordinary Iceberg properties, propagation is just copying metadata during a commit the pipeline already makes without the need of side channel or services. Downstream jobs can then query those table metadata via sensors.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Jjz3FhhZyV_nWA7tdhp_Ig.png" /></figure><h3>Underlying Implementation: Extensible Iceberg-Flink Sink</h3><p>The above partition finalization logic is built entirely on top of the Flink-to-Iceberg sink. We needed two capabilities the standard sink did not have: (1) a way to collect per-record metrics inside the writer and merge them at commit time, and (2) a way to run custom logic immediately after every successful Iceberg commit. Rather than hard-coding partition finalization into the sink, we introduced two generic extension points — <strong>Accumulators</strong> and <strong>CommitProcessor</strong> — that any Flink-to-Iceberg pipeline can use.</p><h4>Operator Topology</h4><p>The standard sink has two Flink operators in sequence: a writer (one instance per parallelism unit) that flushes records to data files, and a committer (single instance) that makes those files visible by appending a new snapshot to the Iceberg table. The extensible sink keeps exactly the same topology and adds no extra operators.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hjDlYZF2Q8dRm07volrSLA.png" /><figcaption>Zero overhead when no extension points configured — falls back to standar operators.</figcaption></figure><p>When either extension point is configured, the writer and committer swap in extensible variants that carry accumulator and commit processor logic alongside. When neither is configured, the pipeline falls back to the standard operators with zero overhead.</p><h4>Accumulators: collecting facts per record</h4><p>An accumulator is a lightweight aggregator that runs inside each writer subtask and builds up a summary of the records it has seen since the last checkpoint. Between checkpoints it records and aggregates each record, extracting a field value and updating running statistics such as a count, a min/max, or a custom metric. At checkpoint time, each subtask takes a snapshot of its accumulator’s current state and resets it for the next window.</p><p>Those per-subtask snapshots travel to the committer alongside the data-file manifests. The committer, which sees results from every subtask, combines the individual snapshots into a single merged result. Because the subtasks may complete in any order, the merge operation must be order-independent: two snapshots combined must produce the same result regardless of which is folded into which.</p><p>Once merged, the accumulator’s value is written into the Iceberg snapshot summary under a namespaced key, sitting alongside the other metadata Iceberg already records per commit.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BnwLwqvMhLXPp6AMNEkNag.png" /><figcaption>Each writer subtask produces an accumulator snapshot per checkpoint. The committer merges them into a single result and writes it to the Iceberg snapshot summary.</figcaption></figure><h4>Commit processor: acting after each commit</h4><p>A commit processor adds two hooks around the Iceberg commit: one that runs just before the snapshot is written, and one that runs just after it is durable. The pre-commit hook can set additional snapshot properties or abort the commit by throwing, which causes Flink to retry the checkpoint. The post-commit hook is where partition finalization lives: it reads the statistics just written to the snapshot summary, runs the watermark algorithm, and updates the table property if the watermark advances.</p><p>Both hooks receive the same context: a handle to the live table, the checkpoint identifier, and the merged accumulators from all writer subtasks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wQhBdJ11GFPSPAWOnx7h0w.png" /><figcaption>The commit processor wraps every Iceberg commit with two hooks — before and after. Both receive the same CommitContext, including the merged accumulators from all writer substacks.</figcaption></figure><h4>Putting it together</h4><p>The event-time accumulator collects the minimum, maximum, and a compact percentile sketch of event times across all records in a checkpoint window. At commit time those per-subtask observations are merged into a single global summary and written into the snapshot. The commit processor then reads that summary, together with recent snapshot history, runs the non-regressing watermark algorithm, and writes the result back to the table, all within the commit cycle without any external service.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*avfczrlEaOPPZ6o4i-3E8Q.png" /><figcaption>Records flow through the writer where event-time statistics are accumulated per checkpoint window. The commit processor merges and acts on them at commit time — no external service required.</figcaption></figure><h3>Configurable Finalization Policies</h3><p>Because each summary keeps the full distribution rather than a single number, the field the commit processor reads becomes a knob each table can turn. This is where the completeness-versus-latency trade-off is handled.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E_-DBdO9x2-tqSmIZYSEHQ.png" /></figure><p>Most tables use the conservative default. Tables that value freshness and can accept a rare late record opt into a percentile instead, accepting that a small tail of records may arrive after their hour was marked safe to read.</p><h3>Generalizing Beyond DB Ingestion</h3><p>Nothing above is specific to database ingestion. The accumulator and commit processor only need a way to read an event time from each record; everything else, e.g. the sketch, the windowed lookback, the non-regressing watermark are generic. Any Flink-to-Iceberg pipeline that needs a completeness signal can adopt partition finalization by plugging in its own event-time extractor. And we are actively working on adopting this partition finalization mechanism into Pinterest’s next-generation Stream Ingestion framework.</p><h3>Conclusion &amp; What’s Next</h3><p>Moving to streaming ingestion gives us fresh data in minutes, but it costs us the implicit completeness that batch loads provided for free. Partition finalization mechanism restores that guarantee: a cheap, mergeable event-time sketch on every commit, a non-regressing “safe to read” watermark derived from it, and a policy knob to trade-off completeness against freshness, and all of those are reusable beyond our DB ingestion framework.</p><p>In the following post, we will cover the incremental processing logic built on top of this ingestion framework and how we use it to support downstream use cases efficiently. Stay tuned for the future post: Incremental Processing on CDC Pipeline.</p><h3>Acknowledgments</h3><p>Huge thanks to my teammates Yisheng Zhou, Vi Nguyen, and Artem Tetenkin for building the Next Generation DB Ingestion at Pinterest together.</p><p>This project would not have been possible without the significant contributions and support of the following partners:</p><ul><li>Storage Services: Leonardo Marques Maciel Silva, Yuan Gao, Liqi Yi</li><li>Storage Foundations: Tailin Lyu, Yu Su, Istvan Podor, John Grass</li><li>Streaming Processing Platform: Kevin Browne</li><li>Batch Processing Platform: Carlos Benavides</li><li>Big Data Storage: Pucheng Yang, Mian Luo, Jenny Wang</li><li>Data Warehouse: Bryant Xiao, Anita Narra, Pritam Pan</li><li>Logging: Vahid Hashemian, Jeff Xiang, Jesus Zuniga</li></ul><p>Special gratitude goes to Shardul Jewalikar, Ang Zhang, and Roger Wang for their continuous guidance, feedback, and support throughout the project.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4c7da6e4cc8f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/partition-finalization-in-pinterests-next-generation-db-ingestion-framework-4c7da6e4cc8f">Partition Finalization in Pinterest’s Next-Generation DB Ingestion Framework</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Beyond Two Towers: Launching the 3-Tower Engagement Co-Train Model (Part 2)]]></title>
            <link>https://medium.com/pinterest-engineering/beyond-two-towers-launching-the-3-tower-engagement-co-train-model-part-2-0b96167d2c14?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/0b96167d2c14</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[monetization]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Thu, 17 Sep 2026 15:01:05 GMT</pubDate>
            <atom:updated>2026-09-17T15:01:05.274Z</atom:updated>
            <content:encoded><![CDATA[<p>Authors: Longyu Zhao (Staff Machine Learning Engineer), Gwendolyn Zhao (Staff Machine Learning Engineer), Peng Yan (Senior Machine Learning Engineer), Yuanlu Bai (Senior Machine Learning Engineer), Yuan Wang (Senior Machine Learning Engineer), Yao Cheng (Staff Machine Learning Engineer), Ang Xu (Principal Machine Learning Engineer), Zhaohong Han (Manager II, Ads Lightweight Ranking)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H3SwMoNL3rjVC9o6-7p_1Q.png" /></figure><h3>Introduction</h3><p>Previously¹, we launched the next-generation serving stack for standard ads, which we call Nexus. Nexus decoupled candidate generation from scoring and moved us beyond the classic two-tower-only world, enabling richer model architectures while still meeting stringent latency and cost constraints.</p><p>Building on this system, we set out to design the first ads lightweight ranking model that goes beyond two towers. It jointly predicts three probabilities for each candidate ad: pCTR, the probability of a click; pGCTR30, the probability of a good click that lasts at least 30 seconds; and pOCTR, the probability of an outbound click to the advertiser’s destination. To support these objectives efficiently, we partition the query and Pin embeddings into task-specific CTR, gCTR30, and oCTR segments. For the CTR task, the fast two-tower prediction uses the first 64 dimensions of the CTR segment, while the three-tower prediction uses the full CTR segment together with richer cross features. For gCTR30 and oCTR tasks, full embeddings are shared between two-tower and three-tower predictions. This lets each task learn dedicated representations while sharing the overall model. In principle, Nexus places very few hard constraints on the architecture we can serve: cross-attention, sequence modeling, and more expressive interaction modules are all on the table.</p><p>However, in practice we quickly ran into the fundamental reality of ads lightweight ranking at Pinterest scale: for a typical request, we need to score on the order of hundreds of thousands of candidates (P99 post-targeting candidate counts can exceed 200K on some surfaces). We cannot simply keep increasing model complexity and expect to stay within our latency and cost budgets.</p><p>To strike a balance between latency and performance, we landed on a 3-tower co-train model design.</p><p>This design has a few key properties:</p><ul><li>We keep the query tower and Pin tower from the existing two-tower model, which lets us cache Pin embeddings offline and still obtain fast dot-product predictions for all candidates.</li><li>We extend the architecture with a third cross tower that performs cross-attention between user sequences and candidate (Pin) features, plus an inter module that further mixes query, Pin, and cross embeddings.</li><li>We co-train two-tower and three-tower predictions in a single model, giving us both fast but less accurate scores and slower but more accurate scores that we can deploy in different stages of the serving flow.</li></ul><p>Put simply, the same model produces a fast two-tower score for every candidate and a richer three-tower score for a selected subset, so we can spend additional compute where it has the greatest impact.</p><p>Later in this blog, we will walk through the model architecture (cross tower and inter module), serving performance optimizations, and the two-stage scoring flow that leverages both two-tower and three-tower predictions.</p><p>By combining these changes, we maintained two-tower prediction quality while achieving around 30% reduction in offline loss for three-tower predictions across our engagement tasks, compared to the existing production model (details see below Offline Performance section). These offline gains translated into online lifts in CTR and gCTR30, reductions in cost per click (CPC), with a modest increase in infrastructure cost.</p><h3>Model architecture</h3><p>Our starting point was the existing two-tower engagement model, with separated query and Pin towers whose dot product feeds into task-specific heads. On top of this, we introduced two major components:</p><ul><li>A new cross tower that uses reduced-query cross-attention between user sequences and candidate features to capture high-order interactions.</li><li>An inter module that jointly processes the query, Pin, and cross embeddings and produces a shared representation for all engagement tasks.</li></ul><p>Below we describe the main design choices and trade-offs in each part.</p><h4>Cross tower</h4><p>The cross tower is responsible for modeling rich interactions between a user’s recent activity and a candidate ad. We use three on-site user sequence features (organic engagement, ads engagement, and search history) and four candidate features (advertiser ID, campaign ID, GraphSAGE embeddings, and Pin PinnerSAGE embeddings).</p><p>A natural first idea would be to build increasingly complex attention modules over these sequences and candidates. In practice, we explored several options:</p><ul><li>Merging full user sequences (across surfaces) and then running cross-attention with candidate features.</li><li>Using shorter, truncated sequences to reduce compute and memory.</li><li>Replacing attention with simpler interaction functions such as DIN-style pooling or average pooling.</li><li>Crossing each sequence attribute with its corresponding candidate attribute individually, rather than merging first.</li></ul><p>These variants exposed a clear trade-off: more expressive attention patterns (longer sequences, more attributes, per-attribute crossing) tended to improve offline loss but also increased latency, especially at high candidate counts. For example, using more complex cross architectures could reduce loss by several additional percentage points, but at the cost of tens of milliseconds of extra latency per request at 100K candidates.</p><p>We ultimately converged on an architecture that uses candidate side features to generate query tokens, which then interacts with user sequences to calculate attention. In this way, we can pick the most important candidate features and control the cost of transformer computation. This design gives us:</p><ul><li>Strong offline performance improvements versus production.</li><li>A predictable compute profile that is easier to optimize and scale.</li><li>A good balance between modeling capacity and serving latency.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_EWlTdTVi_2FNlcnh780dg.png" /></figure><p>The output of this cross tower is a cross embedding that summarizes how a user’s recent behavior interacts with a particular candidate ad.</p><h4>Inter module</h4><p>The inter module takes three inputs: the query embedding, the Pin embedding, and the cross embedding from the cross tower. Its goal is to produce a compact, shared representation that works well for all three engagement tasks (CTR, gCTR30, and oCTR), while keeping parameter count and serving latency under control.</p><p>Here as well, we evaluated multiple architectures:</p><ul><li>A deep MLP with DCN (Deep &amp; Cross Network) layers.</li><li>A standard MMoE (mixture-of-experts) with DCN.</li><li>A top-K MMoE with DCN.</li><li>A shared-bottom MLP with additive task-specific biases.</li></ul><p>More complex structures such as MMoE with DCN achieved stronger loss reductions but also introduced noticeably higher latency compared to production. The shared-bottom MLP design provided a sweet spot: it delivered most of the performance gains while adding only modest latency, and it is architecturally simpler to optimize further.</p><p>In the final design, the inter module:</p><ul><li>Learns a shared logit for each task from the concatenated query, Pin, and cross embeddings.</li><li>Adds task-specific biases computed from query and Pin embeddings for gCTR30 and oCTR.</li><li>Outputs task logits that are then passed through sigmoid functions to produce probabilities.</li></ul><p>This structure allows us to capture shared patterns across tasks while preserving enough task-specific flexibility.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1bu3hCJ1Oi3YgjqsboiUdg.png" /></figure><h4>Loss function</h4><p>We train the model using a multi-task loss that combines main losses for the three-tower predictions with auxiliary losses for the two-tower co-train task.</p><p>The final loss takes the form of a weighted sum:</p><ul><li>Main three-tower losses for CTR, gCTR30, and oCTR.</li><li>Co-train two-tower losses for CTR, gCTR30, and oCTR, computed from query–Pin dot products. For CTR, this fast auxiliary prediction uses the first 64 dimensions of the CTR embedding.</li></ul><p>We tuned the task weights to balance learning stability and final performance, and landed on the following weighting scheme:</p><ul><li>Strong emphasis on main CTR loss.</li><li>Moderate weight on main gCTR30 loss.</li><li>Lower weight on main oCTR loss.</li><li>Non-trivial but smaller weights on each of the co-train losses.</li></ul><p>This configuration made the three-tower predictions the primary optimization target, while keeping the two-tower co-train task healthy enough to match or slightly improve on the production two-tower model.</p><p>The query tower produces a 192-dimensional task embedding: 144 dimensions for CTR, 32 for gCTR30, and 16 for oCTR. The Pin tower produces the corresponding task embedding and appends a 256-dimensional candidate-feature projection for the cross tower, producing a 448-dimensional Pin representation. For fast two-tower CTR scoring, we use only the first 64 dimensions of the 144-dimensional CTR segment. For three-tower CTR scoring, the inter module uses the full CTR segment together with the cross embedding. The additional Pin projection is computed in the Pin tower and cached offline, so the three-tower path can use these candidate features without per-candidate preprocessing at serving time.</p><p>Formally, our final loss is:</p><p>L = (100 * L_main_ctr + 5 * L_main_gctr30 + 1 * L_main_octr) + (10 * L_cotrain_ctr + 5 * L_cotrain_gctr30 + 1 * L_cotrain_octr)</p><h4>Offline performance</h4><p>We evaluated offline performance on held-out standard-ads data across three tasks (CTR, gCTR30, and oCTR), reporting relative loss reduction versus the production two-tower engagement model for both main (three-tower) and co-train (two-tower) predictions. The table reports ranges because we evaluated the model across multiple log sources; each endpoint is the result observed for a different source. For gCTR30, we observed a small degradation on the co-train loss which we deemed acceptable given the main task gains.</p><p>Across tasks, we observed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G3NhvaIs5IQHrz48NIvZVg.png" /></figure><p>Taken together, these results show that the co-train task maintains performance comparable to the existing production two-tower model, while the three-tower predictions deliver substantial improvements. This is important operationally: we can deprecate the standalone production two-tower engagement model and rely on the co-train head for fast scoring, without sacrificing quality.</p><h3>Serving optimization</h3><p>Serving a three-tower model over hundreds of thousands of candidates per request is expensive. To make the launch feasible, we invested heavily in model-level latency optimizations. Below are several techniques that had meaningful impact. Together, these changes reduced P99 model inference from over 200 ms to about 30 ms, leaving the final model only 1–2 ms slower than production.</p><h4>Optimization 1: Move Pin pre-processing into the Pin tower</h4><p>To perform cross-attention, sequence and candidate features must share the same dimensionality. In an initial design, we handled this with on-the-fly MLPs in the cross module, which added per-request compute proportional to the number of candidates.</p><p>Instead, we moved this preprocessing into the Pin tower. We append the processed Pin features to the original Pin embedding, increasing its dimension by an additional 256, and cache the resulting embedding offline. At serving time, the cross module can directly consume these enriched Pin embeddings with no additional per-request MLPs.</p><p>This change saved roughly 2 ms of latency at 100K candidates in our benchmarks.</p><h4>Optimization 2: Lower precision</h4><p>We also explored reduced-precision inference. By switching from FP32 to BF16 in the three-tower path, we significantly reduced model inference time while keeping model quality neutral.</p><p>On one representative benchmark, we observed:</p><ul><li>At 50K candidates, latency dropped from around 103 ms in FP32 to about 56 ms in BF16.</li><li>At 20K candidates, latency dropped from around 45 ms to about 26 ms.</li></ul><p>These gains played a key role in making the three-tower path practical at high candidate volumes.</p><h4>Optimization 3: Late expansion of user features</h4><p>In the three-tower engagement model, we compute predictions between one user and tens of thousands of candidate ads at once. User features are computed once and then expanded to match the batch size of candidates.</p><p>Earlier, this expansion happened just before the cross module to avoid duplicated computation. We realized we could delay expansion even further: instead of expanding before building the attention keys and values, we expand inside the cross module right before attention is computed.</p><p>This avoids redundant computation on large tensors and yields latency savings of around 15 ms at 100K candidates in our benchmarks.</p><h4>Optimization 4: Pre-layer normalization in attention</h4><p>Finally, we revisited how we apply LayerNorm inside the cross-attention module. Previously, we normalized the larger output sequence after attention, which has a batch size proportional to the number of candidates. We switched to normalizing the input sequence before attention instead; during serving this input has batch size 1, so the normalization work is much lower and independent of how many candidates we score.</p><p>During training, both options behave similarly. During serving, however, pre-layer normalization dramatically reduces the amount of work we do at large candidate counts. Flipping pre_lnorm from False to True reduced latency by about 3 ms at 100K candidates in our benchmarks.</p><h3><strong>Rethinking the serving flow</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UyFNvFuMbAjo0Knq9fCkag.png" /></figure><p>Even with model-level optimizations, running the full three-tower model on every candidate would still be too expensive. Post-targeting candidate counts can exceed 100K at P90 and reach up to over 200K at P99 on some surfaces. In early experiments where we scored all candidates with the three-tower model, model inference P99 latency exceeded 70 ms which is our timeout cutoff.</p><p>To tackle this, we redesigned the serving flow as a two-stage scoring pipeline that leverages both two-tower and three-tower predictions.</p><h4>Stage 1: Fast scoring for all candidates</h4><p>In the first stage, we use the two-tower head from the co-train model to score all candidates. These scores are combined into an initial utility: the overall ranking score that estimates a candidate ad’s value for the request and determines which candidates survive for later selection. This follows the existing production setup, but is powered by the new co-train model.</p><p>This stage is fast and inexpensive enough to run on the full candidate set.</p><h4>Stage 2: Focused refinement with three-tower scoring</h4><p>In the second stage, we identify the top-K candidates by utility and rescore only this subset with the three-tower model. Candidates outside this utility topK bypass the three-tower path and retain their two-tower scores.</p><p>We introduced a new hyperparameter, utility topK, which controls how many candidates the three-tower model sees. We tested several choices of the utility topK and measured both latency and downstream metrics such as clickthrough and web conversion impressions.</p><p>The trade-offs we observed:</p><ul><li>Smaller utility topK values reduce latency but can hurt web conversion impressions, because the final top-K selection stage needs a sufficiently large pool to satisfy different campaign groups and deduplication constraints.</li><li>Very large utility topK values allow more candidates into the three-tower stage but directly increase latency, so we needed to pick a value that balanced candidate coverage and serving cost.</li><li>Retaining candidates outside utility topK for later stages mitigates mixshifts with only a small additional latency cost.</li></ul><p>We ultimately chose a utility topK of 40K. This threshold roughly corresponds to the 70th percentile for Home Feed and Related Pins and the 90th percentile for Search, which means the three-tower model scores the majority of candidates on most requests while keeping P99 latency within budget.</p><h4>Final selection and bias considerations</h4><p>After we obtain updated utility scores from the three-tower model for the utility topK subset, we blend them with the two-tower dot-product utilities. The final topK selection step then runs on this blended set of scores, selects pre-defined quotas from several candidate sources and performs deduplication to select the final set of ads shown to the user.</p><p>We made two design choices which may create concerns:</p><ol><li>Heuristic selection of the utility topK subset for three-tower scoring.</li><li>Blending two-tower and three-tower utilities before final top-K selection.</li></ol><p>Both heuristics are designed to favor high-utility candidates. In principle, they could bias the system toward items that already scored well in the two-tower stage, especially if three-tower predictions further amplify those scores.</p><p>To monitor this, we looked at calibration and mixshift. Encouragingly, we observed that the three-tower model actually reduces over-calibration for auction candidates, moving predicted CTR closer to realized CTR. We also did not observe a drop in standard web conversion impressions when using the blending option.</p><h3>Online results and cost</h3><p>In online A/B experiments on standard ads, the three-tower co-train model delivered around 1% gains in CTR, gCTR30, and oCTR, along with nearly 1% reductions in CPC, with only a modest increase in GPU spend.</p><p>Taken together, this represents a strong trade-off: meaningful engagement and efficiency gains for advertisers and users, with a small and well-understood increase in infra cost.</p><h3>Conclusion</h3><p>In this post, we walked through how we took Nexus beyond two towers by introducing a three-tower engagement co-train model for standard ads. On the modeling side, the cross tower and inter module allow us to capture richer interactions between user behavior and candidate ads. On the systems side, a combination of model-level optimizations and a two-stage scoring flow let us deploy this more powerful architecture while staying within tight latency and cost constraints.</p><p>Looking ahead, this work opens up several promising directions:</p><ul><li>Extending similar three-tower and co-train ideas to other objectives beyond engagement.</li><li>Exploring even richer sequence modeling and attention patterns now that we have a scalable framework for late-stage scoring.</li><li>Further tightening the feedback loop between offline architecture exploration, online performance, and infra-aware serving design.</li></ul><p>Most importantly, it demonstrates that with the right system abstractions, we can continue to innovate on model architectures without losing sight of real-world constraints.</p><h3>Acknowledgements</h3><p>We thank Qingyu Zhou, Yuchen Shen, Li-Chien Lee, Qingmengting Wang, Zhixuan Shao, Tristan Nee, Sihan Wang, Lida Li, and Nuo Dou for their contributions to this project, and Renjun Zheng and Jamieson Kerns for their leadership support.</p><h3>References</h3><p>¹Yang, Xiao, et al. “<a href="https://medium.com/pinterest-engineering/beyond-two-towers-re-architecting-the-serving-stack-for-next-gen-ads-lightweight-ranking-models-1992f2b76cbb">Beyond Two Towers: Re-architecting the Serving Stack for Next-Gen Ads Lightweight Ranking Models (Part 1)</a>.” Pinterest Engineering Blog</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0b96167d2c14" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/beyond-two-towers-launching-the-3-tower-engagement-co-train-model-part-2-0b96167d2c14">Beyond Two Towers: Launching the 3-Tower Engagement Co-Train Model (Part 2)</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Evolving Pinterest’s Embedding Retrieval Platform]]></title>
            <link>https://medium.com/pinterest-engineering/evolving-pinterests-embedding-retrieval-platform-aede4e831e01?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/aede4e831e01</guid>
            <category><![CDATA[retrieval]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[ann]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Fri, 11 Sep 2026 15:01:03 GMT</pubDate>
            <atom:updated>2026-09-11T15:01:03.828Z</atom:updated>
            <content:encoded><![CDATA[<p>Authors: Bowen Zhou | Staff Software Engineer; Shan Gao | Senior Software Engineer; Jingwen Hu | Software Engineer II; Wenjiang Chu | Staff Software Engineer</p><h3>The Billion-Embedding Challenge</h3><p>At Pinterest, the “signal” is our lifeblood. Whether it’s a home decor enthusiast finding the perfect rug or a fashion seeker discovering a new aesthetic, our discovery engine relies on understanding deep semantic relationships to help our users find inspirations. Over the last few years, the explosive growth of embedding-based retrieval has fundamentally transformed how we surface these signals — and at the heart of that transformation is Manas, Pinterest’s in-house distributed search platform.</p><p>Embedding Retrieval is one of the core capabilities of Manas, supporting multiple approximate nearest neighbor search algorithms, hybrid queries with both token and embedding clauses, as well as real-time updates to ensure fresh contents become searchable within seconds. Deployed on over 80 clusters and serving billions of embeddings, Manas embedding retrieval powers all major product surfaces at Pinterest including Home Feed, Search, Related Pins, Ads, and Notifications.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9mEwgCdovwyH5yrcF-oJHw.png" /></figure><p>However, as our corpus scales toward tens of billions of embeddings and our models capture increasingly complex interactions, we face mounting challenges around cost efficiency, scalability, and flexibility. On the infrastructure side, traditional ANN algorithms like HNSW are notoriously memory-hungry — they require the entire index to reside in RAM to maintain low query latency, making cost grow linearly with corpus size. On the modeling side, the classic two-tower retrieval paradigm is too restrictive: it reduces each candidate to a single embedding and scores relevance through a simple dot product, leaving little room to express richer, context-dependent notions of similarity.</p><p>To tackle these challenges, our team has been evolving Manas’s embedding retrieval stack across three fronts:</p><ul><li><strong>Quantization.</strong> We reduce the memory footprint of embedding indices by compressing vectors into lower-bit representations with fewer effective dimensions. Quantization has been rolled out to all major use cases, delivering over 50% memory reduction in embedding indices and 20–30% cost savings in serving infrastructure.</li><li><strong>SSD-based Serving.</strong> Rather than holding entire indices in RAM, we serve ANN queries directly from SSD by carefully bounding the I/O per request — sustaining high throughput with low tail latencies at a fraction of the memory cost. Early experiments demonstrate a 10x reduction in memory usage and 40% CPU savings compared to in-memory serving</li><li><strong>Multi-embedding Retrieval.</strong> We move beyond the single-vector-per-candidate constraint of the two-tower model by supporting richer scoring functions that consider multiple embeddings per candidate. This unlocks more expressive ranking at the retrieval stage. We are currently partnering with a product team to launch a pilot use case.</li></ul><p>In this blog post, we will delve into the technical details and results of each initiative, and discuss what’s next for embedding retrieval in Manas.</p><h3>Quantization: Redefining the Footprint of High-Recall Search</h3><p>For a long time, serving embeddings in 16-bit or 32-bit float precision was considered the common practice. But at Pinterest’s scale, raw precision is a luxury that often provides diminishing returns. We discovered that quantization — transforming these high-dimensional float vectors into compact integer representations — is one of our most effective ways to better cost efficiency.</p><h4>Evaluating the Trade-offs: SQ vs. PQ</h4><p>In the Manas stack, we focused our implementation on two primary quantization algorithms: <strong>Scalar Quantization (SQ)</strong> and <strong>Product Quantization (PQ)</strong>. The core of these methodologies lies in partitioning the vector space into disjoint subspaces and mapping each vector into an integer representation: SQ applies uniform discretization from float to integer per dimension, while PQ performs a K-Means clustering in each subspace and maps a subvector to the ID of the closest cluster centroid.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jPxZ0_hUgusRuRNamxyWig.png" /></figure><p>Benchmarks on a 100-million-embedding GraphSage dataset confirmed this intuition. We evaluated both SQ and PQ across two ANN algorithms (HNSW and IVF) and observed a clear trade-off: PQ achieves higher compression but with a significant recall decrease, while SQ delivers strong compression with minimal loss on recall.</p><ul><li>PQ reduces the HNSW index by 74% and the IVF index by 93%, with a recall in the range of 70–80%</li><li>SQ reduces the HNSW index by 59% and the IVF index by 75%, with a recall over 90% consistently</li></ul><p>Given the trade-off demonstrated by the offline benchmarking exercise, we ran online A/B experiments in production to select the best performing quantizer for each use case, and ensure negligible impact on user engagement metrics when enabling quantization. We launched SQ and PQ across major product use cases, reducing the total memory allocation significantly and realizing 20–30% cost savings for serving.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O9Hv6yg0M7oaPSbXsmIo-A.png" /></figure><h4>Technical Deep Dive: SIMD and Linear Scaling</h4><p>Shifting to 8-bit or 4-bit representations isn’t just a memory win; it’s a compute challenge. Usually, SQ requires a decoding step before distance computation, which can become a CPU bottleneck. To solve this, we implemented Linear Scaling SQ<strong>,</strong> which quantizes a vector by scaling only, and thus eliminates the decoding step before distance computation. The key enabler here is SIMD intrinsics, which allows the CPU to perform multiple 8-bit integer operations with each instruction, and hence reduces the total computing resources needed per query by 10–15% in our use cases.</p><h3>SSD Serving: Moving Beyond the Constraints of RAM</h3><p>Our journey of improving embedding retrieval cost efficiency led us to exploring alternative ANN algorithms that take advantage of recent NVMe SSD performance advancements. The current generation of SSD devices are roughly an order of magnitude cheaper per gigabyte, but with the latency increased from nanoseconds to microseconds, which can slow down queries if I/O operations are not carefully managed. To bridge this gap, we experimented with I/O-aware ANN algorithms that were designed to minimize the number of random reads issued per query while retaining a recall score as good as memory-based algorithms.</p><h4>DiskANN vs. SPANN</h4><p>In our experiments, we benchmarked two disk-based ANN algorithms, DiskANN and SPANN, with a 100M-embedding corpus collected from a Pinterest Search use case. While DiskANN is a robust graph-based approach, SPANN emerged as the better option<strong> </strong>for the Manas use cases.</p><p>Our team’s key observation was applying PQ quantization to the on-disk embedding store while retaining the full precision centroids helped the search accuracy and throughput significantly — a slight tweak from the original SPANN paper. This makes SPANN+PQ 4.5x faster than plain SPANN and achieves over 3x the QPS of DiskANN with 1/3 the latency, with a slight 5% recall drop.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HJv8egiSIKvxNxo3nysrsA.png" /></figure><h4>Implementing SPANN in Manas</h4><p>We implemented the SPANN algorithm in Manas, which stores the centroid index in the memory and the large posting lists in the disk, and guarantees both disk-access efficiency (low latency) and high recall by effectively reducing the disk access number per request. In the index-building stage, we adopt the hierarchical balanced clustering algorithm from SPANN for selecting the centroids, which ensures evenly distributed cluster sizes, and hence similar lengths of posting lists to keep the tail latency low. We build the centroid index using HNSW, which is well-suited for in-memory search over a relatively small set of centroids. In a preliminary evaluation with a Pin recommendation use case that indexes over 5 billion embeddings, our SPANN implementation saves over 40% of CPU time for production queries when compared with HNSW, with a &lt;5% recall drop. Our next step is to adopt SPANN across all major use cases.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q2WJpn5O7vzUZGTYRkE8Sg.png" /></figure><h3>Multi-Embedding Retrieval: The Shift Toward Late Interaction</h3><p>As we improve cost efficiency, we are also evolving the expressivity of the Manas embedding retrieval stack. The traditional “Two-Tower” model, while efficient, collapses an entire Pin or query into a single vector, often losing the nuanced, token-level semantics that define high-quality discovery.</p><h4>Contrast in Paradigms: Sum of MaxSim</h4><p>We are now moving toward <strong>Late Interaction models</strong> , such as ColBERT. Unlike the single dot product of two-tower models, late interaction represents documents and queries as lists of vectors. We use the <strong>“Sum of MaxSim”</strong> scoring logic to capture the maximum similarity between each query token and the document’s tokens.</p><p>Integrating this into Manas required comprehensive changes in our serving stack. We integrated the multi-embedding retrieval as a new query type and updated Manas to handle multiple query embeddings rather than a single vector per query. This required updating the Manas query parser to understand these complex queries, as well as running multiple ANN searches from a multi-embedding query simultaneously. Currently, we are working with a client team to launch a pilot use case for the multi-embedding query support in Manas. This represents the next frontier of Pinterest search: moving from “two tower” to true model-based retrieval.</p><h3>Looking Ahead</h3><p>The future of vector search at Pinterest lies at the intersection of infrastructure efficiency and retrieval model innovation. Over the next five years, our central goal is to evolve Manas embedding retrieval into an architecture that is scalable, cost-efficient, and open to new retrieval paradigms. We are pursuing this along three directions: adopting SPANN and SPFresh to push CPU and memory costs even lower for billion-scale indices; building first-class support for multi-embedding retrieval models like ColBERT that enable richer, interaction-based scoring at the retrieval stage; and embracing GPU-based retrieval systems like SilverTorch and TIGER to unlock new model capabilities.</p><h3>Acknowledgements</h3><p>Many people from Core and Ads Delivery Infra teams contributed to the projects discussed in this blog post. Special thanks to Ellie Madsen, Jennifer Kong, and Jiawei Kuang for working on various Manas embedding retrieval projects. Thanks to our collaborators from client teams, including Bowen Deng, Jiaxing Qu, Ryan Hou, Minhazul Islam SK, Wei-Ting Lin, J.J. Hu, Konik Kothari, Yujiao Guo, Hanlin Lu, Bella Huang, Ai Zhang, Janvi Palan. Last but not least, many thanks to Van Lam, Tao Yang, Deeksha Sharma, Kartik Paramasivam, Abhishek Tayal, Zheng Liu for leadership support.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aede4e831e01" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/evolving-pinterests-embedding-retrieval-platform-aede4e831e01">Evolving Pinterest’s Embedding Retrieval Platform</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Pinterest’s VLM Serving Stack on NVIDIA Dynamo]]></title>
            <link>https://medium.com/pinterest-engineering/building-pinterests-vlm-serving-stack-on-nvidia-dynamo-0dce6e93d0f3?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/0dce6e93d0f3</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[vlm-serving]]></category>
            <category><![CDATA[multimodal-ai]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[nvidia]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Thu, 10 Sep 2026 23:08:16 GMT</pubDate>
            <atom:updated>2026-09-10T23:08:16.104Z</atom:updated>
            <content:encoded><![CDATA[<p>Lei Pan | Senior Software Engineer; Salina Wu | Senior Software Engineer; Cristian Lopez | Software Engineer I; Guangtong Bai | Staff Software Engineer; Soam Acharya | Principal Engineer; Saurabh Vishwas Joshi | Principal Engineer; Chia-Wei Chen | Staff Software Engineer; Ambud Sharma | Principal Engineer</p><h3>Why VLM Serving Matters at Pinterest</h3><p>Pinterest is a visual search and discovery platform, so its AI systems must reason over both language and visual content. Vision-language models (VLMs), which can interpret images, compare visual candidates, and respond naturally to user intent, are becoming the foundation for the next generation of Pinterest experiences: Pinterest Assistant, hybrid search, multimodal reranking, content understanding, signal generation, content safety, and more.</p><p>This direction also reflects <a href="https://venturebeat.com/orchestration/pinterest-cut-ai-costs-90-by-gutting-a-frontier-models-vision-layer">Pinterest’s broader strategy</a> to customize open-source models to meet its product &amp; scale needs. Pinterest Assistant is a standout example. This multi-turn conversational experience covers both user language and visual content. Serving it requires low-latency VLM inference over rich multimodal context as well as reworking Qwen3-VL with proprietary multimodal embeddings to cut runtime cost while improving performance.</p><p>Serving VLMs, however, introduces more challenges compared to text-only LLM workloads. Requests may carry multiple images, require extra vision encoder computation, incur larger and more variable prefill cost, and create higher KV cache pressure. To support this new class of models &amp; product experiences, we built Pinterest’s VLM serving stack on top of NVIDIA Blackwell GPUs and NVIDIA Dynamo. Blackwell GPUs incorporate many architectural innovations that are uniquely positioned for today’s most demanding AI workloads — including higher BF16/FP8 compute throughput, increased memory bandwidth, and larger HBM memory capacity — that enable dramatically higher performance for inference. Dynamo provides a distributed inference orchestration layer that gives us the flexibility to optimize multimodal workloads across the full serving path, including disaggregated encoder/prefill/decode serving, multimodal support in the Dynamo frontend and vLLM, multimodal KV-aware routing with custom payloads, and KV cache offloading.</p><h3>The Serving Challenge</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N6GoYjNObJWG6oqCTqBxKg.png" /><figcaption><strong>Figure 1: Architecture of Dynamo-based serving system</strong></figcaption></figure><p>As mentioned above, serving VLM for Pinterest use cases present several challenges:</p><p><strong>Request payloads and preprocessing</strong>: For text-only serving, the request payload is just text (or tokens), so preprocessing is mostly tokenization plus applying a standard chat template; there are no external assets to fetch and no image-specific constraints. For VLM serving, the payload includes both text and images (URLs, base64, or precomputed embeddings), so the serving stack must download or load images, run image preprocessing (resize, enforce min/max pixels, normalization), map them into the model’s multimodal input format, and build prompts that mix both text and image content while inserting any required vision tokens or projectors, otherwise images get dropped or misinterpreted. Complexity is increased as requests include more images — Pinterest use cases sometimes require sending thousands of images per request.</p><p><strong>Expensive prefill</strong>: In text-only serving, the expensive part is typically decode, with relatively modest and uniform prompt lengths, so KV cache pressure is more predictable. In VLM serving, requests often include many images or content items per query, which makes prefill dominant (encoding visual context is costly), drives much larger and more irregular KV caches, and requires our serving stack to support KV-aware routing, cache offloading, as well as careful prompt design to stay within latency and memory SLOs.</p><p><strong>Multi-turn workloads</strong>: In text-only serving, multi-turn chat mainly increases prompt length and token costs but stays within a uniform text interface, so the serving logic is mostly about truncation and history management. In VLM serving, multi-turn workloads combine long dialog history with repeated or evolving visual context (e.g., revisiting or adding images/boards across turns), which makes prefill much heavier, complicates how visual state is represented across turns, and requires benchmarks and SLOs that reflect realistic multi-turn multimodal interaction patterns rather than single-shot prompts.</p><p><strong>KV cache memory pressure</strong>: For text-only models, KV cache growth is driven by text token counts and is relatively predictable per request and per turn, so standard cache sizing and eviction often suffice. For VLM models, large visual contexts and long conversations can produce far bigger KV states per request, so serving must treat KV as a first-class constraint — using KV-aware routing, offloading, and disaggregated Encoder/Prefill/Decode designs — to avoid frequent evictions and maintain throughput under multimodal, prefill-heavy traffic.</p><p><strong>Custom model and payload support</strong>: Text-only serving can often treat models as interchangeable behind a standard chat/completions API with generic JSON payloads and minimal per-model customization. VLM serving, by contrast, typically requires model-specific support for image fields, multimodal content arrays, projector layers (e.g., custom embedding projections), and custom routing or headers; the serving stack has to understand these payload shapes and model capabilities explicitly, and deployment artifacts and routers must be able to encode and route these richer, non-uniform multimodal requests correctly.</p><p>All of these VLM serving challenges required us to build a robust, flexible system that can meet our multimodal requirements. To build our serving stack, we relied on NVIDIA Dynamo’s multimodal serving capabilities.</p><h3>Scaling with Blackwell and Building on NVIDIA Dynamo</h3><p>Pinterest has been an early industry pioneer in adopting NVIDIA GPUs for online model serving at internet scale, starting with recommendation systems and expanding into LLM and VLM serving. Token costs and performance matters significantly for the viability of these products. Building on that foundation, we standardized on NVIDIA Blackwell GPUs B200 due to their market leading TCO for LLM/VLM inference. Blackwell also makes our LLM/VLM hardware stack future proof as our use cases and models continue to evolve. This cutting edge hardware enables Pinterest to be able to continue to get better TCO over time as we explore quantizations, improved kernels and multi-node inference.</p><p>Our in-house Gen AI Serving Solution is an end-to-end customizable stack centered around NVIDIA Dynamo as the serving orchestration framework. We use the OpenAI Chat Completions API, model-based Envoy routing and a model-aware gateway, Model Router, to provide a centralized way for all customers to call our system, ensuring a smooth client experience. Under the hood, we use vLLM as our inference engine and Weights and Biases for model management. Our compute infrastructure, PinCompute, is built on Pinterest’s internal centralized platform infrastructure that leverages AWS Elastic Kubernetes Service (EKS) along with NVIDIA GPUs. With the help of the Infra org, we manage dedicated EKS clusters that host all Gen AI Serving use cases. Notably, this is one of the first PinCompute EKS (PEKS) use cases at Pinterest. The Dynamo operator and components are installed through Helm charts, and our Dynamo workloads use the DynamoGraphDeployment CRD deployed as K8s manifests. To tailor the deployments to Pinterest’s requirements we inject additional sidecars and add custom containers to support functionality like Envoy (service mesh), model loading, and metrics scraping.</p><p>Our journey to Dynamo started with evaluating several Kubernetes-native frameworks that we found easy to start with but lacked flexibility in traffic management or forced reliance on a single inference ecosystem. We ultimately selected Dynamo as it is Kubernetes native, compatible with Pinterest Kubernetes and service discovery solution, inference engine agnostic, provides a flexible traffic solution, and uses a performant Rust-based router. During this process we developed a close relationship with the NVIDIA Dynamo team who have provided us with exceptional support. Pinterest utilizes many key features of Dynamo that provide flexibility and performance optimizations when powering our Gen AI Serving Stack.</p><p><strong>P/D disaggregated serving</strong></p><p>Our platform uses disaggregated prefilling and decoding inference to tailor serving to specific latency requirements (Time-to-First-Token (TTFT) or Inter-Token Latency (ITL)), optimizing hardware allocation by separating the distinct computational phases of LLM requests. This architecture is particularly effective for unblocking product launches with high traffic volume and tight latency requirements, especially for TTFT. Dynamo provides an easy to use solution to orchestrate distributed, disaggregated inference that allows us to explore the Pareto curve between the ratio of encoder (E), prefill (P), and decode (D) workers.</p><p><strong>KV cache offloading</strong></p><p>We leverage KV cache offloading, specifically via LMCache, for multi-tier offloading to CPU memory and disk, which is critical in high QPS, multi-turn scenarios. LMCache serves as a sophisticated extension for the inference engine, providing tiered storage across GPU, CPU DRAM, and local disk (NVMe) to preserve generation latency while managing high GPU memory pressure. This tiered approach includes asynchronous prefetching and compression, contributing to lower TTFT and increased throughput by effectively managing long-context scenarios where visual tokens would otherwise overwhelm available VRAM. LMCache fits seamlessly into Dynamo as one of the many KV cache integration option for KV cache offloading</p><p><strong>Multimodal support in Dynamo frontend/vLLM</strong></p><p>Pinterest’s image-based products have specific multi-modal serving requirements. We worked closely with the NVIDIA Dynamo team to develop corresponding multimodality features, including a frontend image decoder, multi-modal disaggregated serving, and multimodal KV router support to reduce recomputation for VLMs. These enhancements enable the serving stack to handle complex multimodal payloads, such as base64 encoded images or image URLs, and perform necessary image preprocessing directly in the Dynamo frontend. Furthermore, the implementation of multimodal KV-aware routing allows the system to track prefix cache overlap for visual content, which is essential for maintaining production latency in multi-turn interactions with high visual token counts.</p><p><strong>Custom modality support: Projection Embeddings</strong></p><p>Pinterest Assistant workloads often need to reason over large visual contexts: Pins, boards, products, and other image-heavy inputs that may appear across multi-turn interactions. Sending all of that context as raw image pixels is expensive for VLM serving because each request may require image loading, decoding, preprocessing, and online vision encoder computation before the language model can use the visual information. To reduce that cost, we added support for projection embeddings using PinCLIP, Pinterest’s internal image encoder, for generating Pin embeddings. Instead of sending raw images through the serving path, Assistant requests can send precomputed PinCLIP embeddings. Dynamo and the underlying inference engine then run a projector that maps those precomputed embeddings into the target VLM’s native visual token space, letting us reuse visual representations that already exist for many Pinterest entities while avoiding the most expensive parts of pixel-based image serving.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lyJhuNkfUWpki8GGoUZmKw.png" /><figcaption>Figure 2. Comparison between a vanilla VLM and projection embedding enabled VLM</figcaption></figure><p>The performance impact of this approach has been significant. Compared with pixel-based image inputs in Dynamo, incorporating projection embeddings into Dynamo have yielded results that are substantially faster across our benchmarks: average gains are roughly 85x faster TTFT, 7.3x faster end-to-end latency, and 1.1x faster TPOT. Peak gains are even larger, reaching approximately 369x faster TTFT, 44x faster end-to-end latency, and 2.6x faster TPOT. Just as importantly, this makes much larger visual contexts practical: requests with 250 images represented as PinCLIP visual tokens reached latencies comparable to pixel-based requests with roughly 10 images, while carrying 25x more visual context. Even at that scale, the serving profile remained reasonable and production-ready.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6lFOKJb4DSF0Pzo9-Q5SJg.png" /><figcaption>Figure 3. Mean Time-to-First-Token (TTFT) latency speedup by request rate for 100-output-token requests.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k2QLPXXxcwRCIK750poRIA.png" /><figcaption>Figure 4. Mean End-to-End (E2E) latency speedup by request rate for 100-output-token requests.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*alUmsb_i8fNN3G49hP10EQ.png" /><figcaption>Figure 5. End-to-End architecture of precomputed projection embedding enabled client/server</figcaption></figure><p>Supporting this required changes across the API, artifact, serving, and engine layers. We introduced an updated ChatCompletions request format for projection embeddings, defined a model artifact contract so training and serving teams could package projector weights, model weights, and configs together into a single model artifact, added the new modality path in vLLM alongside image and video to decode embeddings, validate types and shapes, invoke the correct projector, and insert projected visual tokens into the model input sequence, and updated Dynamo to accept and route the new request format while preserving the multimodal contract. Adding multimodal KV-aware routing support for our modality delivered meaningful tail-latency gains: the strongest result improved TTFT p99 by 5.92x, and across the full benchmark matrix Multimodal(MM) KV-aware routing delivered about 1.42x average speedup on TTFT p99. Additionally, because embedding payloads are larger than ordinary text inputs, vLLM frontend processing latency became a bottleneck in some cases; Dynamo’s Rust frontend helps by providing an efficient path for receiving, parsing, and forwarding larger multimodal payloads.</p><p><strong>Tool calling<br></strong>Lastly, Dynamo fully supports tool calling with custom chat templates and multi-modal inputs, which is essential for providing flexibility for Pinterest’s agentic AI systems. This capability allows our agents to interact with internal tools and APIs in the Pinterest ecosystem, enabling more complex workflows that go beyond simple text for text and hybrid search, and other internal services. By leveraging custom chat templates, we can precisely define how the model should format its tool requests and handle the subsequent tool outputs, ensuring seamless integration with Pinterest’s internal services. Furthermore, the support for multi-modal inputs in tool calling means our agents can use visual information to inform their tool use, such as identifying an object in an image and then calling a specific search or recommendation tool to find similar products.</p><h3>Benchmarking Real Multimodal Workloads with AIPerf</h3><p>We use AIPerf, NVIDIA’s distributed benchmarking tool for standardizing our AI inference performance measurement, as the execution layer for our performance benchmarks. AIPerf is designed as a modular benchmarking framework, which makes it a better fit for complex generative AI workloads than tools focused mainly on single request/response patterns.</p><p>This is especially important as both Pinterest and the broader industry move toward agentic AI systems. These workloads are rarely a single model call. They often involve retrieval, routing, multiple model calls, tool use, multimodal inputs, and intermediate reasoning steps before producing a final response. This allows our optimizations to have grounding data and guardrails on whether we are improving or regressing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n-DH4cFNbacxqhKmYul1xw.png" /><figcaption>Figure 6. An example of Pinterest Assistant DAG used in AIPerf benchmark</figcaption></figure><p>AIPerf’s DAG support is a big part of why it works well for us. Instead of flattening an agentic workflow into one artificial request, we can model the actual execution graph: nodes represent meaningful stages in the system, and edges capture dependencies between steps. This lets us benchmark workflows that branch, fan out, join, or depend on earlier outputs, patterns that are increasingly common in real AI applications.</p><p>Just as importantly, AIPerf lets us shape the benchmark traffic to look more like real production usage. We can run benchmarks with configurable QPS, realistic Poisson request arrival patterns, multi-turn interactions, multimodal inputs, and configurable prompt characteristics such as system prompt size, prefix length, input token length, and number of visual/embedding items. This makes the benchmark less about testing an isolated model call and more about understanding how the full workload behaves under realistic load.</p><p>Internally, we pair AIPerf’s execution output with Pinterest-specific reporting. We use the results to power dashboards and summaries for latency, throughput, token usage, success rate, per-request details, and aggregate comparisons. That gives teams a practical way to compare runs, catch regressions, and understand whether a model or deployment can meet production SLOs under realistic multimodal and agentic workloads.</p><h3>Product Use Cases Enabled</h3><p>The VLM serving stack described above was built to support Pinterest Assistant, but the same architecture now serves as a reusable foundation for many GenAI and multimodal use cases across Pinterest. By standardizing on Dynamo for orchestration, vLLM for inference, and a common Chat Completions-compatible API, teams can launch new model-backed product experiences on top of this extensible serving platform.</p><h3>Pinterest Assistant: Real-Time Multimodal Interactions</h3><p>Pinterest Assistant is one of the first major product use cases enabled by this stack. As a conversational agent, Pinterest Assistant needs to support natural multi-turn interactions while reasoning over Pinterest’s visual content. A user may ask for help refining an idea, exploring a style, comparing products, or finding inspiration based on a set of Pins or images. Unlike a text-only assistant, this requires the serving system to handle both dialogue history and multimodal context in real time. Pinterest Assistant inference runs on NVIDIA B200 instances, which showed a greater than 2x latency improvement over Hopper during preliminary benchmarking.</p><p>Pinterest Assistant also benefits from custom modality support such as projection embeddings. Instead of always sending raw image pixels through the serving path, Assistant requests can use precomputed visual embeddings for Pinterest entities such as Pins, boards, and products. This allows the model to reason over much larger visual context while avoiding repeated image decoding and vision encoder computation, making richer real-time conversations practical.</p><h3>A Shared Stack for Multimodal Product Patterns</h3><p>As more Pinterest product surfaces adopt GenAI and multimodal models, this shared stack lets us support a growing range of patterns: conversational agents, re-rankers, OCR, safety systems, signal generation, and future VLM-powered experiences. The result is a serving platform that is not tied to a single product launch, but designed as a reusable foundation for multimodal AI at Pinterest.</p><p>Although Pinterest Assistant motivated many of the original requirements, the serving stack has grown to support a much broader set of use cases. Dynamo has become the out-of-the-box default for many GenAI serving workloads at Pinterest because it offers a flexible path for both text-only and multimodal deployment:</p><ul><li>Multimodal reranking uses VLMs to compare candidate content across textual and visual signals</li><li>OCR workloads extract or reason over text in images</li><li>Safety guardrails apply multimodal understanding to check whether responses or retrieved content meet product and policy requirements</li><li>And more across signal generation, embedding-based workflows, and agentic systems</li></ul><p>Dynamo’s LoRA hot loading has also accelerated experimentation under limited GPU capacity. Instead of standing up a separate full deployment per adapter — which increases GPU usage and operational overhead — client teams can load and evaluate multiple sets of LoRA weights dynamically against an existing base model. This shortens experimentation cycles and creates a smoother path from adapter training to production validation.</p><p>With a common serving foundation, teams reuse the same APIs, deployment patterns, routing layer, model management, observability, benchmarking, and GPU infrastructure rather than each building a custom solution. This gives product teams a paved path to focus on model behavior, integration, and evaluation. Dynamo is powering a reusable foundation for multimodal AI at Pinterest.</p><h3>Lessons Learned and What’s Next</h3><p>In building our Gen AI Serving platform, we’ve learned that VLM workloads are fundamentally prefill-heavy and cache-sensitive: encoding large visual contexts and long histories, not just decode, drives both latency and GPU memory utilization, so KV-aware routing, cache offload tiers, and disaggregated serving need to be designed explicitly. Dynamo’s multimodal KV-aware router and E/PD disaggregation and LMCache-based KV offloading turned out to be essential. We also found that payload design and routing are core serving problems, not just interface glue: the way we encode multimodal content arrays, choose image resolutions, and structure prompts directly determines whether Dynamo can reuse prefixes, route efficiently, and keep TTFT within product targets. On the evaluation side, we learned that benchmarks must reflect real multimodal product traffic — including multi-turn conversations, many images per request, and agentic DAGs — so we invested in AIPerf-based DAG benchmarks that mirror production QPS patterns instead of synthetic single-shot prompts. Finally, a shared serving platform built on Dynamo, vLLM, and EKS has significantly accelerated experimentation: once the stack supported multimodal routing, KV offload, and model management, new use cases like Pinterest Assistant and multimodal reranking could launch by reusing the same paved path instead of re-inventing infra per team.</p><p>Looking ahead, we’re investing in several new directions.</p><p><strong>AI Configurator:</strong> Dynamo’s AI Configurator is a performance optimization tool that can simulate 10K+ deployment configurations in seconds, finding optimal prefill/decode worker counts, tensor/expert/data parallelism settings, and deployment parameters. It evaluates both aggregated and disaggregated serving architectures, and uses hardware-specific performance models to predict TTFT, ITL, and throughput across different GPUs. This tooling can help us create optimized deployments with lower lift, increasing performance and developer velocity across teams.</p><p><strong>Dynamo Planner:</strong> As our workloads scale, so will the need to introduce autoscaling in order to maintain a highly available yet cost-efficient compute infrastructure. Dynamo’s Planner will provide a VLM/LLM-optimized autoscaler, which dynamically adjusts prefill and decode replica counts through four optimization targets: throughput (static queue/KV thresholds), latency (aggressive low-latency thresholds), load (user-defined prefill queue and decode KV utilization thresholds), and SLA (regression-based models targeting specific TTFT/ITL values)</p><h3>Conclusion</h3><p>NVIDIA Dynamo has given us a strong foundation for building Pinterest’s VLM serving stack and expanding it across emerging multimodal use cases. Its flexibility has been critical as we move from individual product launches toward a shared platform for production GenAI serving.</p><p>We’re excited to continue partnering with the NVIDIA Dynamo team and the broader community to push the limits of VLM and multimodal serving, and to make real-time multimodal AI systems faster, more efficient, and easier to deploy at scale.</p><h3>Acknowledgements</h3><p>This work would not be possible without the contributions from our partners and collaborators. Our thanks to:</p><h4>Pinterest</h4><p>AI Platform: Neha Upadhyay, Ananya Prabhu Angadi, Nazanin Farahpour, Howard Nguyen<br>Product ML Infra: Li Tang, Yayun Wang, Archer Liu<br>ATG: Yash Upadhyay, David Xue<br>Cloud Runtime Team: Vaibhav Shankar<br>CDP: Khoi Nguyen<br>Traffic: Peter Leng, James Fish, Scott Beardsley<br>Production Engineering: One Marino, Juan Pablo Daniel Borgna<br>Product Management: Colin Leatherbury<br>Leadership: Karthik Anantha Padmanabhan, Bo Liu, Roger Wang, Kartik Paramasivam, Matthias Zenger</p><p>NVIDIA<br>Elijah Soba, Qi Wang, Anthony Casagrande, Guan Luo, Kris Hung, Ryan McCormick, Harry Kim, Akshatha Kamath, Matthew Rawson</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0dce6e93d0f3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/building-pinterests-vlm-serving-stack-on-nvidia-dynamo-0dce6e93d0f3">Building Pinterest’s VLM Serving Stack on NVIDIA Dynamo</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Becoming an AI Team]]></title>
            <link>https://medium.com/pinterest-engineering/becoming-an-ai-team-866d6b567803?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/866d6b567803</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[engineering-culture]]></category>
            <category><![CDATA[pinterest]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Tue, 01 Sep 2026 15:01:06 GMT</pubDate>
            <atom:updated>2026-09-01T15:01:05.914Z</atom:updated>
            <content:encoded><![CDATA[<p>John Grass | Sr. Manager, Engineering</p><h3>A Fundamental Transformation</h3><p>An AI team is fundamentally more than just a group whose members incorporate AI tools into their existing workflows. The journey to becoming an AI team necessitates a fundamental and comprehensive paradigm shift in how the team defines ownership, engages in strategic planning, and, most critically, executes on its core goals and objectives. This transformation is not merely an addition of new technology; it is a restructuring of the team’s operating model, philosophy, and individual roles.</p><figure><img alt="A two-column diagram compares traditional engineering teams to AI-enabled teams. The traditional side features linear workflows centered on manual tasks and process ownership, while the AI side shows interconnected roles focused on orchestrating AI systems and increased strategic responsibility. A prominent arrow labeled “AI Transformation” visually indicates the fundamental shift in team operating models." src="https://cdn-images-1.medium.com/max/1024/1*kYjFqIQe5NzEX_YR0LCJVQ.png" /><figcaption><em>Becoming an AI team requires a holistic shift: team members transition from routine, manual execution to empowered, AI-augmented strategists and problem-solvers.</em></figcaption></figure><p>In an AI-centric environment, every single member is significantly empowered, not only through access to cutting-edge AI tools and sophisticated models but through an <strong>expanded scope of responsibility and influence</strong>. These new capabilities allow individuals to automate routine tasks, accelerate data analysis, and rapidly prototype solutions, freeing up cognitive resources for higher-level, more strategic thinking. The expectation shifts from simply completing tasks to orchestrating intelligent systems and focusing on solving problems that were previously intractable.</p><p>Consequently, the very nature of the work for both individual contributors and managers will be fundamentally different from anything that has come before.</p><p>At Pinterest, this isn’t theoretical. Our infrastructure teams sit at the core of a product that serves billions of Pins, boards, ads, and real-time signals. That reality has forced us to treat “becoming an AI team” as an operational necessity, not a side project as we cannot keep scaling reliability, cost efficiency, and developer productivity using only traditional playbooks.</p><h3>AI Team Capabilities</h3><p>The advent of accessible Artificial Intelligence (AI) tools has fundamentally raised the performance ceiling for what development teams can accomplish. To illustrate this profound shift, consider a normally challenging project — for example, refactoring an entire legacy codebase to replace a widespread used process named “foo” with a more modern one named “bar.” This type of undertaking, which can be critical for long-term maintainability but often low on the priority list due to its sheer scale, would have previously required a dedicated team of several engineers and a timeline spanning many months, potentially a year or more.</p><p>With the seamless integration of modern AI tools, projects once considered infeasible or prohibitively expensive in terms of human capital and time can now be completed with incredible speed and accuracy. The very refactoring example mentioned above, utilizing sophisticated code-generation and transformation models, can now be executed and verified in a matter of a week or less.</p><p>This new capability changes far more than just project timelines; it fundamentally reshapes how organizations and teams must approach prioritization. The set of “possible projects” that are technically within reach has grown dramatically, multiplying the complexity of deciding where to focus energy. Consequently, the act of prioritization has become even more critical. Teams must internalize a new maxim: <strong>Just because something is feasible does not mean it is strategic.</strong> The ability to accomplish a task quickly with AI does not inherently align it with the core goals of the business or product.</p><figure><img alt="A diagram features two overlapping circles, with a small circle for “Projects Once Possible” and a larger circle labeled “Projects Now Feasible with AI.” Adjacent to these, a funnel illustrates how the expanded set of possible projects narrows down to a smaller set of strategic priorities. Icons represent automation, rapid prototyping, and the need for focused strategy." src="https://cdn-images-1.medium.com/max/1024/1*IpVhn7tWyPGrkdZbJb8Rjw.png" /><figcaption><em>AI raises the ceiling of what teams can accomplish — yet strategic prioritization becomes even more essential amid expanded possibilities.</em></figcaption></figure><p>By offloading repetitive, high-effort engineering tasks to AI, teams will fundamentally shift their focus. Engineers, product managers, and designers must dedicate more time and energy to high-level strategic thinking, in-depth problem definition, and validating genuine user needs, rather than getting caught up in execution details. Consequently, the core value of an engineering team moves from being expert coders to becoming expert strategists and problem-solvers, using AI as a powerful force multiplier for execution.</p><h3>The Manager’s Evolving Role</h3><p>As the daily work of individual contributors, particularly engineers, becomes increasingly elevated and augmented by AI, the role of the manager undergoes a profound transformation. Managers are systematically freed from the time-consuming, routine tasks traditionally associated with project management and oversight such as the meticulous tracking, status updates, resource allocation for predictable tasks, and micro-managing process adherence.</p><p>This liberation allows managers to pivot from being primarily process enforcers and administrative overseers to becoming true strategic guides and visionary leaders. Their focus shifts dramatically toward:</p><ol><li><strong>Strategic Direction and Vision Setting:</strong> The primary responsibility evolves to identifying which ambitious challenges and emerging technologies merit the team’s concentrated pursuit. This involves a higher-level view of the product roadmap, understanding market shifts, and translating the organization’s overarching goals into clear, actionable, and inspiring technical objectives for the team.</li><li><strong>Maximizing Collective Impact:</strong> Instead of simply ensuring projects are completed on time, the manager’s role is to steer collective efforts toward the points of <em>maximum strategic impact</em>. This requires deep critical thinking, understanding the leverage points within the architecture or product, and making difficult trade-off decisions that align the team’s output directly with the most significant business outcomes.</li><li><strong>Talent Development and Mentorship:</strong> With AI handling much of the repetitive code generation and debugging, the value of the human engineer shifts to creativity, complex problem-solving, and system-level architecture. Managers must therefore focus intensely on coaching and mentoring team members, helping them develop higher-order skills, navigate ambiguity, and define career paths that capitalize on advanced technical and strategic competencies.</li></ol><figure><img alt="A side-by-side comparison shows, on the left, traditional management responsibilities like resource tracking and process enforcement, and on the right, modern AI-team manager roles such as strategic guidance, vision setting, and mentoring. Arrows highlight the evolution from process oversight to strategic leadership with more vibrant and engaging visuals." src="https://cdn-images-1.medium.com/max/1024/1*1f3eDUTZrhGKTX_ZcjexYg.png" /><figcaption><em>As AI handles routine oversight, managers shift from process control to visionary leadership — championing strategy, mentorship, and team impact.</em></figcaption></figure><h3>AI work at Pinterest</h3><p>Across Pinterest, we’re seeing similar patterns. Data and platform teams are building agents that write and validate Spark jobs, triage alerts, and help product teams explore data without deep query expertise. Our design, PM, and support functions are using AI for brainstorming, content drafting, and faster decision-making. Initiatives like our AI Acceleration Learning Sessions and GenAI productivity working groups are making these wins visible and repeatable, so teams don’t have to rediscover the same patterns in isolation.</p><p>For infrastructure teams, this has a very concrete flavor: agents that surface distressed databases before they page an engineer, tools that propose safer rollout plans, and coding assistants that make large-scale refactors on our storage platforms possible in days instead of quarters. Those are the kinds of capabilities that make an “AI team” feel less like a slogan and more like an everyday operating model.</p><h3>Future Teams Are AI Teams</h3><p>The integration of Artificial Intelligence into the modern workplace is not merely an incremental technological upgrade; it represents a fundamental shift in how work is conceived, executed, and managed. For every professional, from the newest individual contributor to the most seasoned manager, <strong>adapting to this new paradigm isn’t optional, it’s imperative for survival and success.</strong> The era of the “traditional” high-performing team is rapidly giving way to the <strong>AI-enabled team</strong>, and the difference in capability is staggering.</p><p>The core reality is that the capacity, efficiency, and innovative potential of AI-enabled teams will be an order of magnitude greater than their predecessors. AI tools will automate routine, repetitive tasks, freeing up human talent to focus on complex problem-solving, strategic thinking, and creative endeavors that require uniquely human judgment and emotional intelligence. This shift doesn’t eliminate roles; it elevates them, demanding a new set of skills centered around prompt engineering, data interpretation, critical thinking, and collaborative interaction with intelligent systems.</p><p><strong>To remain effective, competitive, and truly innovative, teams and organizations must fully embrace this transformation.</strong> This involves more than just adopting new AI tools; it requires a deep cultural change.</p><p>The future of high-performance is intrinsically linked to this symbiotic relationship between human expertise and machine intelligence. Teams that hesitate will quickly find themselves outpaced by competitors who have successfully made the leap, highlighting that embracing this AI-driven evolution is not just a strategic advantage but is rapidly becoming the foundational requirement for sustainable innovation and effectiveness.</p><h3>Previous Capacity &amp; Commitments Model</h3><p>Teams would assess available capacity for a work cycle and commit to specific deliverables. The expectation was to meet all commitments. In reality, factors like underestimated effort or reduced resource availability typically led to falling short (e.g., achieving <strong>70%</strong> of commitments).</p><h3>Initial Impact of AI Tools</h3><p>AI tools (for documentation, coding, etc.) have been available and used by some individuals, leading to a modest increase in overall team efficiency. This early, non-uniform adoption has yielded minor capacity gains (e.g., an estimated <strong>20%</strong> increase). Consequently, teams are performing better against commitments, but still slightly missing the mark (e.g., hitting <strong>90%</strong>), meaning the team is still operating at <strong>100% capacity</strong> (fully busy).</p><p>On our Storage Foundations team at Pinterest, we’ve already started to see this shift in very real terms. For example, work such as investigating distressed database clusters could previously require several days of effort is increasingly being front-loaded by AI agents that sift through logs, generate candidate hypotheses, and even draft remediation commands</p><p>We’re also starting to see this show up at the portfolio level. AI-assisted tooling around MySQL, TiDB and Cache Infrastructure has let us move from “only what’s absolutely urgent” to clearing whole classes of maintenance work that used to sit in the backlog indefinitely. That doesn’t mean we’ve “saved X engineers”; it means the same engineers are now spending much more of their time on roadmap-level questions instead of chronic firefighting and manual KTLO.</p><h3>The Need to Pass the Tipping Point</h3><p>To fully realize the benefits of AI, the team needs dedicated time for skill development and deeper integration of the technology. AI adoption increases team capacity. The crucial hurdle is reaching a point where the team’s increased capacity <strong>exceeds its commitments</strong>. This surplus time is essential for developing AI proficiency. The current challenge for most teams is this transitional phase: simultaneously learning and using new tools while struggling to meet existing deliverable expectations.</p><figure><img alt="A horizontal bar chart depicts three stages: traditional teams at 70% of commitments met, teams with initial AI tools at 90%, and teams with full AI integration exceeding 100% commitments. A “tipping point” label marks where surplus capacity emerges, illustrating the gain from dedicated AI adoption." src="https://cdn-images-1.medium.com/max/1024/1*9PTVJfE3ksV0ypmuYdzvLA.png" /><figcaption><em>AI integration increases team capacity, creating the essential surplus needed to master new skills and fully leverage intelligent tools.</em></figcaption></figure><h3>Navigating the Chaos: The Role of Leadership in the AI Transition</h3><p>The transition to an AI-powered work environment is inherently complex and often turbulent. Teams are confronted with an overwhelming deluge of information, including a proliferation of AI tools, new academic research, industry blogs, online courses, and detailed demos. This sheer volume can quickly lead to paralysis and confusion: <em>Which specific tools offer the best return on investment? What are the foundational concepts or practical skills I absolutely need to acquire? How do I strike a sustainable and productive balance between the necessary phase of exploration and real-world execution and delivery?</em> The difficult truth is that no universal, one-size-fits-all answer exists to these critical questions.</p><figure><img alt="A circular process or flowchart illustrates four leadership actions: acknowledging uncertainty, setting vision and filtering information, supporting knowledge sharing and experimentation, and establishing psychological safety. Arrows indicate iterative progress, with “proactive, empathetic leadership” at the core, using friendly and calm color cues." src="https://cdn-images-1.medium.com/max/1024/1*VWO6ORkOgeD6ir_TiJzkpw.png" /><figcaption><em>Effective leaders guide teams through AI-driven transformation by fostering vision, experimentation, and psychological safety.</em></figcaption></figure><p>In this phase of uncertainty, managers assume a role that is not merely supervisory, but truly critical. They must become the primary navigators, charting a course through the technological uncertainty. This guidance involves several key responsibilities:</p><ol><li><strong>Guiding Through Uncertainty:</strong> Leaders must acknowledge and validate the team’s confusion and anxiety, providing a clear vision of the destination even when the immediate path is obscured. They filter the noise, helping the team prioritize a manageable set of tools or learning tracks that align directly with organizational goals.</li><li><strong>Encouraging Information Sharing and Experimentation:</strong> Managers should actively dismantle silos, creating structured and informal forums such as brown bag sessions, dedicated Slack channels, or internal demo days where team members can share their discoveries, successes, and, crucially, their failures. Furthermore, they must allocate dedicated time and resources for experimentation, framing it not as a diversion from work, but as an essential part of the new workflow.</li><li><strong>Creating a Culture of Psychological Safety:</strong> Perhaps the most important leadership function is establishing an environment where it is genuinely “okay to learn and adapt.” This means normalizing the fact that competence in AI will be a journey, not an instant state. Leaders must replace the fear of failure with an appetite for <em>fast, informed learning</em>, celebrating the insights gained from an unsuccessful experiment as much as a successful deployment. This psychological safety accelerates the learning curve and transforms fear into proactive engagement with the new technology.</li></ol><h3>Guiding Your Team Through the AI Transition</h3><p>The integration of Artificial Intelligence represents not just an incremental technological update, but a fundamental shift in the landscape of daily work. While it may unfold more gradually than a traditional corporate reorganization, the transition to an AI-augmented environment will prove to be an even more profound, systemic transformation, altering core processes, roles, and skill requirements. It is crucial for leadership to acknowledge that change, even when promising vast positive outcomes, is an inherently stressful process for employees. Proactive and empathetic guidance is essential to navigate this period successfully.</p><p>For individual contributors, the core of their professional identity and daily tasks will evolve significantly, shifting the focus from execution to strategy, guidance, and unique human insights.</p><p>Prior professional experience remains absolutely essential. However, the <em>application</em> of that experience will fundamentally change. Instead of primarily using experience to execute tasks manually, team members will leverage it for guiding, critiquing, and collaborating with advanced AI agents and tools. Their role shifts to one of a skilled orchestrator, using their deep understanding of the problem space, customer needs, and business context to ensure the AI’s output is accurate, relevant, and fully aligned with strategic goals.</p><figure><img alt="A line or area chart traces the shifting focus of engineering teams over time. The area representing “execution and fulfillment” slopes downward, while “strategy and problem-solving” rises and eventually overtakes, visually depicting the increasing importance of high-level thinking in AI-enabled environments." src="https://cdn-images-1.medium.com/max/1024/1*phGB4ap1zhssGG2jgXMPZw.png" /><figcaption><em>The true value of AI teams moves from task execution to deep strategy and complex problem-solving as intelligent tools become core to the workflow.</em></figcaption></figure><p>As AI takes on an increasing share of repetitive, data-intensive, and complex <em>implementation</em> work (“the how”), the demand for a strategic mindset within individual roles will escalate. Team members must become much more strategic partners, actively participating in and driving decisions about <em>what</em> work is most valuable to pursue, <em>why</em> it matters, and <em>how</em> to define success. This requires moving beyond merely fulfilling assigned tasks to critically analyzing the business problem, prioritizing initiatives, and defining clear, high-leverage prompts and guardrails for the AI to follow. This strategic contribution elevates the value and scope of every role.</p><h3>Conclusion</h3><p>Embracing this holistic shift thoughtfully, strategically, and together is not optional. It is essential for engineering teams aiming to thrive, innovate, and lead in this new era where intelligence is the core layer of the product. The successful team will be one that is centered around the continuous optimization of the AI-driven system.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=866d6b567803" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/becoming-an-ai-team-866d6b567803">Becoming an AI Team</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Scaling Conditional Learned Retrieval for Pinterest Home Feed]]></title>
            <link>https://medium.com/pinterest-engineering/scaling-conditional-learned-retrieval-for-pinterest-home-feed-ecfba7e5a426?source=rss----4c5a5f6279b6---4</link>
            <guid isPermaLink="false">https://medium.com/p/ecfba7e5a426</guid>
            <category><![CDATA[pinner-experience]]></category>
            <category><![CDATA[pinterest]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[eng-culture]]></category>
            <dc:creator><![CDATA[Pinterest Engineering]]></dc:creator>
            <pubDate>Wed, 26 Aug 2026 14:01:05 GMT</pubDate>
            <atom:updated>2026-08-26T14:01:04.249Z</atom:updated>
            <content:encoded><![CDATA[<p>Devin Kreuzer | Sr. Machine Learning Engineer; Yichi Wang | Machine Learning Engineer I; Sujan Reddy Ale | Machine Learning Engineer I; Zelun Wang | Sr. Machine Learning Engineer; Hongtao Lin | Sr. Machine Learning Engineer; Piyush Maheshwari | Staff Machine Learning Engineer</p><p>Pinterest home feed candidate generation is a large-scale User-to-Pin retrieval problem. A common approach is a two-tower model: a user tower encodes the user, an item tower encodes candidate Pins, and approximate nearest neighbor search retrieves Pins close to the user embedding. But Pinterest users often have multiple intentions at once — planning a renovation, saving recipes, exploring fashion, or organizing travel ideas. A single retrieval embedding can struggle to capture this diversity.</p><p>Conditional Learned Retrieval, or CLR, extends the two-tower setup by conditioning the user tower on an explicit retrieval context. Instead of producing only one user embedding, CLR can generate condition-aware embeddings that reflect different aspects of a user’s interests while still grounding retrieval in the user’s overall behavior.</p><p>Prior Pinterest work studied this formulation in two settings. The RecSys’24 paper: <a href="https://arxiv.org/abs/2508.16793">Bootstrapping Conditional Retrieval for User-to-Item Recommendations</a> described how to bootstrap conditional retrieval by constructing training data for (user, condition) -&gt; item retrieval from existing user-item and condition signals, and applied it to interest-based notifications. The KDD’25 paper: <a href="https://arxiv.org/abs/2506.23060">Synergizing Implicit and Explicit User Interests: A Multi-Embedding Retrieval Framework at Pinterest</a> placed Conditional Retrieval within a broader multi-embedding retrieval framework for home feed, where explicit interest conditions complement implicit interests extracted from user behavior.</p><p>In this blog, we describe how CLR evolved from early interest-conditioned retrieval into a broader retrieval system for Pinterest home feed. We focus on three areas: expanding CLR to support more retrieval use cases, scaling the model foundations through sequence modeling and more general condition representations, and redesigning the serving infrastructure to make multi-condition retrieval efficient at production scale.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YQKCAWMXZ4T5suBqVoTUXQ.png" /><figcaption>Figure 1. Conditional Learned Retrieval launch timeline</figcaption></figure><h3>Expanding CLR Across Use Cases</h3><p>Increasing diversity of the retrieval candidates is a reliable source to drive engagement impact, because they provide a broad range of content for ranking and blending to work with. With CLR, this becomes scalable by either expanding to new types of conditions or providing new sources of conditions given a user.</p><p>To support each condition type, we need to train CLR models using (user, condition, engaged Pin) triplets. Fortunately we already have a few heuristic-based candidate generators in home feed that can help bootstrap these use cases.</p><p><strong>Interest Conditions<br></strong>At Pinterest, we have a predefined interest taxonomy to categorize Pins. We initially <a href="https://arxiv.org/abs/2506.23060">launched</a> CLR in home feed by sampling a few interests from user-to-interest signals. Later we leveraged a new user interest signal generated from LLMs as conditions.</p><p><strong>Pin Conditions<br></strong>We later introduced Pins as conditions, by clustering users’ recently engaged Pins, selecting medoids of those clusters and leveraging pre-training embeddings to represent them in the model. In doing so, we were able to deprecate legacy heuristic CGs in favour of CLR; yielding impressive metric wins while simplifying our serving stack.</p><p><strong>Board Conditions<br></strong>At Pinterest, users interact with Pins which are distributed across various <em>Boards </em>on the platform. We can think of Pins and Boards as forming a bipartite graph with Pins on one side, linked to Boards on another. By leveraging random walks, we can traverse this graph to recommend not only Pins, but also Boards to users. Similarly to the above, we then constructed Board conditions using pre-training embeddings in the same space as Pins to represent them, yielding large metric wins and simplified serving stack once more.</p><p><strong>Agentic Condition Budget Tuning<br></strong>Following the migration to GPU-based serving, which significantly reduced latency and freed capacity, the team leveraged Claude Code to automate experimentation by continually reading experiment feedback to optimize the number of conditions inferred per request for CLR across Interest, Pin and Board conditions. By utilizing automated tuning policies, we successfully improved key engagement metrics including with low cost increase.</p><h4>Pin tower improvement: large id embedding table and semantic ids</h4><p>ID embedding is an important feature in our recommendation systems. We began with using image signatures to represent Pin ids (<a href="https://arxiv.org/abs/2508.18700">ref</a>), which behave like random hash numbers. Since this id space is huge (billion-scale), we ended up pretraining an id embedding table of size 20GB for sufficient memorization and tolerable collisions. We used TorchRec to shard this embedding table across multiple GPUs to support efficient training.</p><p>We later introduced semantic ids to complement image signatures. We built semantic ids by quantizing static content embeddings (PinClip fusion <a href="https://www.pinterestcareers.com/media/eoqd5wcs/pinclip.pdf">ref</a>) using residual-quantized VAEs. These semantic ids are hierarchical codes that cluster similar contents together, thus visually and semantically similar Pins could share nearby codes. Long-tail Pins suffer from cold start issues and limited training data. Semantic ids enable long-tail Pins to share the same id embedding space with popular Pins, thus can borrow their collaborative signal. Our semantic ids have five hierarchical layers, each layer has 2048 possible codes. Instead of using another large id embedding table, we found using five small embedding tables (one per layer) for these codes to be sufficient. After semantic id embeddings are looked up from these tables, we use a stack of five MLP layers to fuse these id embeddings sequentially. Finally, the image signature embedding and semantic id embedding are fed into the feature cross layer on the Pin tower.</p><h3>Scaling CLR Model Foundations</h3><h4>Scaling User Understanding: From User Sequences to Foundation Models</h4><p><strong>Conditional user sequence transformer<br></strong>Early CLR models could condition retrieval on an explicit context, but the user representation was still largely built from static or aggregated features. This made it harder to decide which parts of a user’s recent history mattered for a given Interest, Board, or Pin condition.</p><p>To make CLR sequence-aware, we introduced a Conditioned User Sequence Transformer. The model converts raw condition features into condition tokens, appends them to the user sequence, and encodes the combined sequence with a Transformer. The condition token acts like a query over recent user actions, helping the model focus on the sequence signals most relevant to the retrieval context.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dDQTjBB00Qwg56mS6g_HWg.png" /><figcaption>Figure 2. CLR Model Architecture post User Sequence and Pin Condition launches</figcaption></figure><p>The Transformer outputs encoded condition tokens, encoded recent user sequence tokens, and raw condition features through a residual path, which are passed into the User Tower DHEN layer. This established the core abstraction for later model scale-ups: represent the condition as tokens, append them to user history, and encode the extended sequence.</p><p>The next step was to make this encoder more powerful. As CLR became more central to home feed retrieval, we wanted to capture richer action context, longer-term behavior patterns, and stronger alignment between user history and candidate Pins. This motivated the move toward a Foundation Model based CLR architecture.</p><p><strong>Foundation model in CLR<br></strong>To capture deeper user sequence understanding, we upgraded the sequence encoder by integrating the PinFM (<a href="https://arxiv.org/pdf/2507.12704">ref</a>) into the CLR user tower. The Foundation Model is a large-scale transformer trained on global user action sequences across multiple Pinterest surfaces before being fine-tuned within the Unified CLR framework.</p><p>The foundation model uses a large ID embedding table in addition to OmniSage embeddings to memorize the engagement-oriented representations of Pins. This ID embedding table is reused even at the Pin tower to encode the candidate Pin. We believe that using this consistent representation of Pins on both sides of the tower will make it easier for them to align. To reinforce the pretraining objective during finetuning, we also use the next token loss during fine tuning as this helps the model adapt to the user sequence in the CLR dataset. We use positive actions such as saves, repins, sends and downloads to construct this loss. In addition to pooling the user sequence and condition outputs, we also apply an attention pooling to get a weighted average of the user sequence transformer outputs. This would give the feature cross an holistic view of the entire sequence.</p><p>We also introduce a contrastive alignment loss between the condition token representation and the candidate Pin representation. This encourages the condition token to be closely aligned with Pins embedding space, which makes it easier to rationalize a Query-key similarity in cross-attention.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VAT3hYw4Kn8ZTJP-9PYc1A.png" /><figcaption>Figure 3. CLR Model Architecture post Foundation Model Launch</figcaption></figure><h4>Scaling Condition Representation: Unified CLR and Router Simplification</h4><p>As CLR expanded beyond its first use cases, the condition interface became an important scaling challenge. Supporting each condition type with its own model, feature, and serving setup would make every new condition more expensive to launch and maintain. To scale CLR, we needed the model to support heterogeneous conditions through a shared architecture.</p><p><strong>Unified CLR<br></strong>The first step was Unified CLR. Before unification, home feed served separate CLR models for different condition types: Interest CLR used an interest ID in the user tower, while Board CLR used a board embedding. Both were effective, but maintaining separate models created duplicated work across training pipelines, feature upgrades, experiments, serving configs, and indexing.</p><p>Unified CLR consolidated these use cases into a single model trained on both interest and board conditions. The model was modified to accept multiple condition types, with missing condition features imputed by default values, and training used conditional filtering to focus on examples with valid conditions. This gave the team one shared foundation for future condition types, surfaces, feature upgrades, and model architecture improvements.</p><p><strong>Router Simplification<br></strong>While Unified CLR brought our models under a single roof, the interface for defining conditions still suffered from an underlying scaling bottleneck. Historically, our routing logic was highly condition-specific. Introducing a new condition type (e.g., Board, Interest, or Pin) required adding bespoke features to the model and heavily zero-padding the master feature container. This legacy approach led to feature explosion, siloed learning, and high engineering maintenance overhead. To break this bottleneck, we refactored the routing logic into a condition-agnostic Slot Architecture. Instead of creating custom pipelines for every new condition, we bucketed incoming features into three predefined, shared slots:</p><ol><li>condition_os slot: Houses Omnisage-compatible embeddings (e.g., Pin OS, Board OS, or interest text embeddings).</li><li>condition_id slot: Houses specific identifier embeddings (e.g., Interest IDs or Semantic IDs).</li><li>condition_type_id slot: Encodes the condition type into a 32-dimensional learned embedding to preserve candidate generator identities.</li></ol><h3>Scaling CLR Training Efficiency</h3><p>Integrating a large-scale foundation model into the user tower significantly increased computational complexity. To maintain high developer velocity and support larger batch training, we introduced two key lossless infrastructure optimizations that increased our training throughput by 2x.</p><p><strong>Request-level-training<br></strong>Within a single training batch, user features are often repeated across different candidate conditions. We deduplicate these identical user features using a unique combination of user-id and condition identifiers. This drastically reduces the effective batch size passing through the user tower. Post-forward pass, the generated embeddings are re-duplicated to match the original batch size for contrastive loss calculation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P7j2Gw0ZaKGE4lzZAtdq6w.png" /><figcaption>Figure 4. Request level training visualization; duplicate (user, condition) pairs can be grouped and processed once in a given batch.</figcaption></figure><p><strong>M-Falcon Optimization<br></strong>Since we use causal attention, the representation of the user sequence is always independent of the condition tokens. So within a batch, we append all conditions for a user to a single user sequence and ensure the same user sequence is not redundantly computed in the batch. We use a custom block attention mask to ensure that condition tokens cannot attend to each other, while still allowing them to fully attend to the underlying user sequence. This drastically cuts the effective batch size moving through the transformer layers, lowering the GPU memory footprint for both ID embeddings and activations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CVkLT34kMlIIbLrCrKV0Kg.png" /><figcaption>Figure 5. M-Falcon visualization; with causal attention, condition tokens for a given user can be <em>flattened</em> into a single row for more efficient transformer forward passes.</figcaption></figure><p>Attention mask</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*87SyEMLBIBDM2fMMWaxABQ.png" /><figcaption>Figure 6. M-Falcon causal attention mask; <em>flattened</em> condition tokens are masked from each other.</figcaption></figure><h3>Scaling CLR Serving Infrastructure</h3><p>During initial CLR work, we treated each condition as an additional user-level feature — resulting in one model request per condition. As we began scaling to more and more conditions, the cost on our compute clusters was growing rapidly, and we achieved great cost savings and scale unlocking with the following methods:</p><p><strong>Single Model Request<br></strong>Noting that each model request contains duplicate features, we optimized CLR serving by consolidating multiple separate CLR model requests per backend request into a single one with deduplicated request-level features. To achieve this, we modified the model’s TorchScript to batch these features internally and used Torch Jit to parallelize forward passes, shifting the bottleneck from network to CPU while keeping latency neutral.</p><p><strong>GPU Serving + NVEmbed<br></strong>We optimized our logic further by shifting to a paradigm of CLR being a <em>ranker</em> of conditions, leveraging improved internal compute capabilities to form batches; where conditions are treated as <em>items</em> and user level features get broadcasted across these items. This allowed us to significantly simplify our models logic, align CLR inference with the “1 query — N doc” paradigm, mirroring our L2 ranker’s structure. We exported a CUDA-compatible model, deployed on g6e.4xlarge machines, and refactored the serving path — deprecating earlier model-side batch formation logic and improving output parsing.</p><p>Furthermore, to help serve and experiment with multiple large ID embedding tables post Foundation Model launch, our team has adopted NVEmbed; a framework for unifying embedding tables and dense models in a single servable torchscript artifact.</p><p><strong>Key Wins &amp; Impact:</strong></p><ul><li><strong>Financial &amp; Performance:</strong> 7 figure cost savings and reduced p90 model latency by 85% (80ms to 12ms)</li><li><strong>Scalability:</strong> Unblocked major initiatives, including CLR FM and Search CLR, while enabling more conditions per user.<a href="https://docs.google.com/document/d/1DIaW17dNx4KfKcBLamfQ1v-6JcKZ-Qz4WLLj43JnCxI/edit">1</a></li><li><strong>Dev Velocity:</strong> The simplified infrastructure reduces technical debt and lowers the barrier for scaling future conditions.</li></ul><h3>Future work</h3><p>Looking ahead, we plan to continue scaling CLR beyond its current home feed use cases. CLR is designed to support multiple condition types through a shared model architecture; it can also serve as a foundation for retrieval across multiple surfaces. Expanding to new surfaces will also enable training on broader and more diverse training data. As we scale the training data, condition coverage, and model capacity, CLR can become a more general retrieval model for personalized learned retrieval across Pinterest.</p><h3>Conclusion</h3><p>Conditional Learned Retrieval started as a way to add explicit retrieval context to Pinterest’s two-tower candidate generation system. By conditioning the user tower on signals, CLR helps home feed retrieve candidates that better reflect different aspects of a user’s intent.</p><p>Scaling CLR required us to make condition retrieval both more expressive and more efficient. On the modeling side, we improved how the system understands user behavior and represents different retrieval contexts. On the infrastructure side, we made it practical to evaluate many conditions per request to production scale. Together, these changes helped CLR grow from an early interest condition model to a broader retrieval framework for home feed, with a path toward more surfaces, condition types in the future.</p><h3>Acknowledgements</h3><p>Matthew Lawhon, Zili Li, Matt Chun, James Li, Dylan Wang, Bowen Deng, Tao Mo, Nezanin Farahpour</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ecfba7e5a426" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pinterest-engineering/scaling-conditional-learned-retrieval-for-pinterest-home-feed-ecfba7e5a426">Scaling Conditional Learned Retrieval for Pinterest Home Feed</a> was originally published in <a href="https://medium.com/pinterest-engineering">Pinterest Engineering Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>